内容参考以及代码整理自“深度学习四大名“著之一《Python深度学习》
查看完整代码,请看: https://github.com/ubwshook/MachineLearning
这一次我们将开始深度学习用于文本和序列的旅程,计算机视觉的相关内容暂时告一段落,回顾请点击下面链接:
文本是最常用的序列之一,可以理解为字符序列或单词序列,但最常见的是单词级处理。与其他神经网络一样,深度学习模型不会接收原始文本作为输入,只能处理数值张量。文本向量化是指将文本转换为数值张量的过程。它有很多实现方法。
- 将文本分割为单词,并将每个单词转换为一个向量。
- 将文本分割为字符,并将每个字符转换为一个向量。
- 提取单词或字符的n-gram,并将每一个n-gram转换为一个向量。n-gram是多个连续单词或字符的集合(n-gram之间可重叠)
将文本分解而成的单叫作标记(token),将文本分解成标记的过程叫做分词。所有文本向量化得过程都是应用某种分词的方案,然后将数值向量与生成的标记相关联。这些向量组成序列张量,被输入到深度神经网络。将向量与标记相关联的方法有很多种。本节介绍两种主要方法: 对标记做one-hot编码(one-hot encoding)与标记嵌入。
一、单词和字符的one-hot编码
它将每个单词与一个唯一的证书索引相关联,然后将这个整数索引i转换成长度为N的二进制向量(N是词大小),这个向量只有第i个元素是1,其余元素都为0。下面是一个单词级的one-hot编码:
def example_one_hot_word(samples, max_length):
"""
单词级的one-hot,每个单词分配一个索引,将单词表示为长度为max_length且仅在
单词对应索引上为1,其余位置为0的序列
:param samples: 输入的字符串列表
:param max_length: 不同单词的最大个数
:return: one-hot矩阵
"""
token_index = {}
for sample in samples:
for word in sample.split():
if word not in token_index:
token_index[word] = len(token_index) + 1
results = np.zeros(shape=(len(samples), max_length, max(token_index.values()) + 1))
for i, sample in enumerate(samples):
for j, word in list(enumerate(sample.split()))[:max_length]:
index = token_index.get(word)
results[i, j, index] = 1.
return results
下面这一段是一个字符级的one-hot:
def example_one_hot_char(samples, max_length):
"""
字符级one-hot,将所有字符赋予索引值,并进行one-hot编码
:param samples: 输入的字符串列表
:param max_length: 不同字符的最大个数
:return: one-hot矩阵
"""
characters = string.printable
# 书中代码疑似有问题,书中方式会使下面token_index.get(character)
token_index = dict(zip(characters, range(1, len(characters) + 1)))
results = np.zeros((len(samples), max_length, len(characters) + 1))
for i, sample in enumerate(samples):
for j, character in enumerate(sample):
index = token_index.get(character)
results[i, j, index] = 1.
return
Keras种内置函数可以对文本进行one-hot编码,并且提供丰富的功能,比如字符串中去除特殊的字符、只考虑数据高频的N个单词等等,下面我们Keras如何实现one-hot:
def keras_one_hot(samples):
"""
Keras API 实现one-hot
:param samples: 输入的字符串列表
:return: 语句对应的索引序列,one-hot编码结果
"""
tokenizer = Tokenizer(num_words=1000) # 分词器,处理前1000高频出现的单词
tokenizer.fit_on_texts(samples) # 创建单词索引
sequences = tokenizer.texts_to_sequences(samples) # 把单词转换为序列
one_hot_results = tokenizer.texts_to_matrix(samples, mode='binary') # one_hot编码
word_index = tokenizer.word_index # 单词索引表
print('Found %s unique tokens.' % len(word_index))
return sequences, one_hot_results
one-hot的另一种变体是散列one-hot,如果词表中唯一标记的数量够太大而无法直接处理,就可以使用散列技巧。这种方法没有为每个单体显式的分配一个索引并将索引保存在一个字典中,而是将单词散列编码为固定长度的向量。这种方法的优点是,避免了维护一个显式的单词索引,从而节省内存。这有一个缺点就是散列冲突,即两个单词有可能具有相同的散列值。下面是散列one-hot的简单实现:
def hash_one_hot(samples, max_length, dimensionality):
"""
哈希one-hot,并不为单词固定索引,而是将单词散列编码为长度固定的向量。
:param samples: 输入的字符串列表
:param max_length: 不同字符的最大个数
:param dimensionality: 散列的维度
:return: hash one-hot结果
"""
results = np.zeros((len(samples), max_length, dimensionality))
for i, sample in enumerate(samples):
for j, word in list(enumerate(sample.split()))[:max_length]:
index = abs(hash(word)) % dimensionality
results[i, j, index] = 1.
return results
二、词嵌入方式进行文本向量化
one-hot编码得到的向量是二进制的、稀疏的、维度很高的向量。而词嵌入式低维度的浮点数向量,它能将更多信息塞进低维度中。词嵌入有两种方式可以完成:
- 在完成主任务的同时学习词嵌入。这种情况下,一概是是随机向量,然后对这些词向量进行学习,其学习方式与神经网络的权重相同。
- 在不同于待解决问题的机器学习任务上预计算好词嵌入,然后将其加载到模型中。这些词嵌入叫做预训练词嵌入。
1.利用Embeddig层进行词嵌入
词向量之前的几何关系应该表示这些词之间的语义关系。词嵌入的的作用是价格人类语言映射到几何空间中。在一个合理的词嵌入空间中,同义词应该被嵌入到想死的词向量中,一般来说,两个词向量之间的几何距离应该和这两个词的语义关系有关。例如,将king(国王)向量加上female(女性)向量,得到的是queen(女王)向量。
在Keras中,我们使用一个Embedding层置于网络的最前端来进行词嵌入工作:
def simple_embedding():
"""
一个简单词嵌入的例子,使用的imdb数据集
:return:
"""
max_features = 10000
maxlen = 20 # 在20额单词后截断文本
(x_train, y_train), (x_test, y_test) = load_local(num_words=max_features)
x_train = preprocessing.sequence.pad_sequences(x_train, maxlen=maxlen) # 整数列表转化为二维数组
x_test = preprocessing.sequence.pad_sequences(x_test, maxlen=maxlen)
model = Sequential()
model.add(Embedding(10000, 8, input_length=maxlen)) # 参数1是标记的个数,参数2是嵌入的维度,参数3是输入的最大长度
model.add(Flatten()) # 进入分类器之前要展平
model.add(Dense(1, activation='sigmoid'))
model.compile(optimizer='rmsprop', loss='binary_crossentropy', metrics=['acc'])
model.summary()
history = model.fit(x_train, y_train, epochs=10, batch_size=32, validation_split=0.2)
show_results(history)
验证精度接近75%,考虑到使用每条评论前20个单词,这个结果还是不错的。
训练精度
三、从原始文本开始,进行词嵌入训练
1.数据准备
IMDB的原始文本下载路径是:http://mng.bz/0tIo
首先, 我们将训练评论转换成字符串列表,标签页转化为列表:
def raw_data_pro():
"""
原始数据处理,获取文本序列和标签序列
:return:
"""
imdb_dir = 'E:/git_code/data/aclImdb'
train_dir = os.path.join(imdb_dir, 'train')
labels = []
texts = []
for label_type in ['neg', 'pos']:
dir_name = os.path.join(train_dir, label_type)
for fname in os.listdir(dir_name):
if fname[-4:] == '.txt':
f = open(os.path.join(dir_name, fname), encoding='utf-8')
texts.append(f.read())
f.close()
if label_type == 'neg':
labels.append(0)
else:
labels.append(1)
return texts, labels
第二步我们要讲文本列表转换为向量化数据:
def tokenizing_data(texts, labels, maxlen, max_words, training_samples, validation_samples):
"""
对文本序列和label进行分词、向量化得处理,为进入神经网络做好准备。
:param texts: 文本序列
:param labels: 标签序列
:param maxlen: 评论截断的最大长度
:param max_words: 处理出现频率最高的max_words个词语
:param training_samples: 在多少个词上进行训练
:param validation_samples: 在多少个词上进行验证
:return: 返回训练数据、验证数据、词索引
"""
tokenizer = Tokenizer(num_words=max_words)
tokenizer.fit_on_texts(texts) # 获取分词索引
sequences = tokenizer.texts_to_sequences(texts) # 将文本转换为数字序列
word_index = tokenizer.word_index
print('Found %s unique tokens.' % len(word_index))
data = pad_sequences(sequences, maxlen=maxlen) # 填充或截断序列使其长度董伟maxlen
labels = np.asarray(labels) # 标签数据向量化
print('Shape of data tensor:', data.shape)
print('Shape of label tensor:', labels.shape)
indices = np.arange(data.shape[0])
np.random.shuffle(indices) # 将数据打乱,因为原先正面和负面是排好序的
data = data[indices]
labels = labels[indices]
x_train = data[:training_samples]
y_train = labels[:training_samples]
x_val = data[training_samples: training_samples + validation_samples]
y_val = labels[training_samples: training_samples + validation_samples]
return x_train, y_train, x_val, y_val, word_index
2.嵌入矩阵准备
Glove词嵌入下载,https://nlp.stanford.edu/projects/glove。然后对词嵌入文件解析,映射为其向量表示的索引。然后构建一个嵌入矩阵,这个嵌入矩阵的形状是 (max_words, embedding_dim)的矩阵。对于单词索引中索引为i的单词,这个矩阵的元素i就是这个单词对应的embedding_dim维向量。
def get_glove(embedding_dim, word_index):
"""
加载Glove词嵌入文件
:param embedding_dim: 嵌入的维度
:param word_index: 词索引列表
:return: GloVe词嵌入矩阵
"""
glove_dir = 'E:/git_code/data/glove.6B'
embeddings_index = {}
f = open(os.path.join(glove_dir, 'glove.6B.100d.txt'), encoding='utf-8')
for line in f:
values = line.split()
word = values[0]
coefs = np.asarray(values[1:], dtype='float32')
embeddings_index[word] = coefs
f.close()
print('Found %s word vectors.' % len(embeddings_index))
# 组装词嵌入矩阵
embedding_matrix = np.zeros((max_words, embedding_dim))
for word, i in word_index.items():
if i < max_words:
embedding_vector = embeddings_index.get(word)
if embedding_vector is not None:
embedding_matrix[i] = embedding_vector
return embedding_matrix
3.定义模型并嵌入矩阵,然后进行训练
def pretrained_embedding():
"""
使用预训练的词嵌入处理IMDB评论情感分类问题
:return:
"""
texts, labels = raw_data_pro()
x_train, y_train, x_val, y_val, word_index = tokenizing_data(texts, labels, maxlen, max_words,
training_samples, validation_samples)
embedding_matrix = get_glove(embedding_dim, word_index)
model = Sequential()
model.add(Embedding(max_words, embedding_dim, input_length=maxlen))
model.add(Flatten())
model.add(Dense(32, activation='relu'))
model.add(Dense(1, activation='sigmoid'))
model.summary()
model.layers[0].set_weights([embedding_matrix]) # 嵌入矩阵
model.layers[0].trainable = False
model.compile(optimizer='rmsprop', loss='binary_crossentropy', metrics=['acc'])
history = model.fit(x_train, y_train, epochs=10, batch_size=32, validation_data=(x_val, y_val))
model.save_weights('pre_trained_glove_model.h5')
show_results(history)
我们使用的样本(200个)以及截断的字符个数(20个)都很少,所以模型很快就过拟合了,即便这样训练精度仍然接近60%。
训练精度
以上的训练存在一个问题,仅仅将嵌入序列展开并在上面训练一个全连接层,会导致模型对输入序列中的每个单词单独处理,而没有考虑单词之间的关系和句子结构,比如this move is a bomb和this movie is the bomb两条都可能被归类为负面评论,更好的做法在嵌入序列上添加循环层或一维卷积层,将每个序列作为整体来学习特征,这些我们下次会讨论。
网友评论