深度学习之文本与序列--基于Keras的IMDB电影评论分类
【应用场景】
在深度学习中,文本和序列有着很多的应用场景:
- 文本分类、时间序列分类。eg. 确定一篇文章的主题,确定一本书的作者
- 时间序列的相互比较。eg. 文本相似度,股票行情预测
- 语言序列的学习。eg. 英译汉,汉译英,翻译系统
- 情感分析。eg. 一条微博携带的情感色彩,电影评论好与坏
- 时间序列预测。eg. 在一个确定地点预测未来的天气,给出最近的天气
【用文本数据工作】
在深度学习的模型,并不会将原始的文本数据直接送进神经网络中,会将文本装换成数值张量,向量化是其中的一种方式。有很多种不同的方式:
- 将text分割成word, 将每个word装换成vector;
- 将text分割成character,将每个character装换成vector;
- 提取word和character的n-gram,并将每个n-gram转换成一个vector。
解释一下几个在文本处理中常用的几个名词:
- token:指的是将文本分割成word、character、n-gram,其中word、character、n-gram均可称为是token
- tokenization:将文本转化成token的过程;
- n-grams:从一句话中抽出N个连续词组成的集合。举个例子:“The cat sat on the mat.”;
那么2-grams:{"The", "The cat", "cat", "cat sat", "sat", "sat on", "on", "on the", "the", "the mat", "mat"}
,同样,3-grams:{"The", "The cat", "cat", "cat sat", "The cat sat", "sat", "sat on", "on", "cat sat on", "on the", "the", "sat on the", "the mat", "mat", "on the mat"}; - bag-of-words:指的是无序的词库集合,也就是经过tokenization之后产生的集合。
Tips:
- 在提取n-grams时,其实这个过程就像是在提取一句话的特征,那么在深度学习中,就是用一维卷积、RNN等方法去替代n-grams;
- 虽然现在越来越多的复杂的任务均转移到了深度学习,但是在处理一些轻量级的任务时,不可避免的去使用n-grams,还有一些传统高效的方法,比如:逻辑回归、随机森林。
【token的两种编码方式】
- one-hot
one-hot是一种常见的编码方式,通过几个toy example来了解一下
词(word)级别的one-hot编码
import numpy as np
samples = ['The cat sat on the mat.', 'The dog ate my homework.']
# 10
# 定义一个集合,得到{'The': 1, 'cat': 2, 'sat': 3, 'on': 4, 'the': 5, 'mat.': 6, 'dog': 7, 'ate': 8, 'my': 9, 'homework.': 10},也就是筛选出这个句子中对应的了哪些词,然后并赋予索引值,其实就是个词库
token_index = {}
for sample in samples:
for word in sample.split():
if word not in token_index:
token_index[word] = len(token_index) + 1
# 限制了读取的句子的长度,一句话最长10个词
max_length = 10
results = np.zeros(shape=(len(samples),
max_length,
max(token_index.values()) + 1))
# print(results) 2, 10, 11
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.
print(results)
The cat sat on the mat.
The dog ate my homework.
字符(character)级别的one-hot编码
import numpy as np
import string
samples = ['The cat sat on the mat.', 'The dog ate my homework.']
# 预先定义一个字符集 '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!"#$%&\\'()*+,-./:;<=>?@[\\]^_`{|}~‘
characters = string.printable
token_index = dict(zip(range(1, len(characters) + 1), characters))
max_length = 50
results = np.zeros((len(samples), max_length, max(token_index.keys()) + 1))
for i, sample in enumerate(samples):
for j, character in enumerate(sample):
for key, value in token_index.items():
if value == character:
index = key
results[i, j, index] = 1.
print(results)
截取部分图
用Keras实现基于词(word)级别的one-hot编码,封装的是真的好。
samples = ['The cat sat on the mat.', 'The dog ate my homework.']
tokenizer = Tokenizer(num_words=1000)
tokenizer.fit_on_texts(samples)
sequences = tokenizer.texts_to_sequences(samples)
one_hot_results = tokenizer.texts_to_matrix(samples, mode='binary')
word_index = tokenizer.word_index
print('Found %s unique tokens.' % len(word_index))
还有一种多样化的one-hot编码,称为one-hot散列法。由于hash值是唯一的,因此这种方法应用于由于在词典中的不同token的数量太大而不能明确处理。但是唯一的缺陷就是会产生哈希碰撞,也就是两个不同词可能对应同一个哈希值,然而神经网络并不能说出这两个词之间的差别。解决方案是让哈希空间的维度大小远远大于不同token已有哈希值的数量
samples = ['The cat sat on the mat.', 'The dog ate my homework.']
dimensionality = 1000
max_length = 10
results = np.zeros((len(samples), max_length, dimensionality))
for i, sample in enumerate(samples):
for j, word in list(enumerate(sample.split()))[:max_length]:
# 计算hash值
index = abs(hash(word)) % dimensionality
results[i, j, index] = 1
print(results)
-
词嵌入(word-embedding)
word-embedding也是对文本进行编码的一种方法,相比与one-hot,word-embedding显得更为灵活,我们来对比一下:
one-hot编码
one-hot是高维的(词库中有多少个token,就有多少维),稀疏的(大部分均为0),是二分的,硬编码的;
word-emedding编码
word-emedding是低维的浮点型张量,能够用更低的维度去叠加更多的信息,全连接的,是从数据中学习而来;word-embedding具体获取的方式有两种:
1)在神经网络中添加embedding层去学习word-embedding,它是在词向量之间反映出语义关系,将人类自然语言映射到几何空间中。比如,我们希望同义词被嵌入到相似的词向量中。
toy example
拿上面的例子简单解释一下,这四个词是嵌入到2D空间中的向量,这几个词中一些语义关系会通过几何代换进行编码,我们发现两组的相似的向量,从cat到tiger,从dog到wolf,这组向量会被解释为“从宠物到野生动物”这种语义关系,同样的,从dog到cat, 从wolf到tiger,这组向量会被解释为“从犬类到猫科动物”。因此我们能更好的去理解word-embedding相比于one-hot具有好的灵活性,每一个词向量所在的空间位置都是有确定的语义关系,我们通过神经网络将这种语义关系学习出来。word-embedding类似于找到一个空间(或多个空间),在每个空间中都包含着许多词(同义词),这些词在这个空间不是孤立,都是有特定语义的。但是因为不同的任务中的embedding-space不同,都是唯一的,正所谓“具体问题具体分析”,所以需要进行重新学习。
2)另一种方式是利用预训练的word-embedding,尤其是适用于拥有少量的训练数据的情况下,预训练的原理在此不再赘述,原理和做图像分类时利用ImgaeNet中的VGG16结构一样,重利用在复杂问题上学习到的特征应用到自己的任务中,这是一种简单而有效的方法。我们在预训练中采用已有的word-embedding预计算的数据库,例如,word2vec,Glove。
Notes:在预训练模型我们始终应该注意的是所有预训练的模型在训练的时候参数不应该被更新,若将所有参数进行更新,由于随机初始化隐藏层引发的巨大的梯度更新将会破坏掉已经训练的特征。
【举个栗子】
我们利用IMDB电影评论数据做好的评论和不好的评论的分类,并以GLoVE作为预计算的word-embedding空间。 - 数据集
IMDB数据集------自行翻墙下载或者下方留言邮箱
GLoVE数据------百度网盘 - 模型建立与完整代码
该模型是添加了embedding层
import os
from keras.preprocessing.text import Tokenizer
from keras.preprocessing.sequence import pad_sequences
import numpy as np
from keras.models import Sequential
from keras.layers import Embedding, Flatten, Dense
import matplotlib.pyplot as plt
# settings
max_len = 100
training_samples = 200
validation_samples = 10000
max_words = 10000
embedding_dim = 100
def process_data():
'''
处理IMDB数据,将数据按标签分为pos,neg
:return: labels,texts
'''
imdb_dir = 'D:\\text2sequences\\aclImdb\\aclImdb'
train_dir = os.path.join(imdb_dir, 'train')
labels = []
texts = []
for label_type in ['pos', 'neg']:
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), 'r', encoding='UTF-8')
texts.append(f.read())
f.close()
if label_type == 'neg':
labels.append(0)
else:
labels.append(1)
return labels, texts
def tokennize_data():
'''
将text向量化,切分训练集和验证集
:return:x_train, y_train, x_val, y_val即训练集和验证集的label和text
'''
labels, texts = process_data()
tokenizer = Tokenizer(num_words=max_words)
tokenizer.fit_on_texts(texts=texts)
sequences = tokenizer.texts_to_sequences(texts=texts)
word_index = tokenizer.word_index
print('Found %s unique tokens.' % len(word_index))
data = pad_sequences(sequences, maxlen=max_len)
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
def parse_word_embedding(word_index):
'''
将预计算的词向量空间的word建立索引和矩阵
:return:
'''
glove_dir = 'D:\\text2sequences\\glove.6B'
embeddings_index = {}
f = open(os.path.join(glove_dir, 'glove.6B.100d.txt'), 'r', 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
def train_model():
'''
训练模型
:return:训练时loss,acc
'''
model = Sequential()
model.add(Embedding(max_words, embedding_dim, input_length=max_len))
model.add(Flatten())
model.add(Dense(32, activation='relu'))
model.add(Dense(1, activation='sigmoid'))
model.summary()
# 将GLOVE加载到模型中
x_train, y_train, x_val, y_val, word_index = tokennize_data()
embedding_matrix = parse_word_embedding(word_index)
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('pre_trained_glove_model.h5')
return history
def plott_results():
'''
作图
'''
history = train_model()
acc = history.history['acc']
val_acc = history.history['val_acc']
loss = history.history['loss']
val_loss = history.history['val_loss']
epochs = range(1, len(acc) + 1)
plt.plot(epochs, acc, 'bo', label='Training acc')
plt.plot(epochs, val_acc, 'b', label='Validation acc')
plt.title('Training and validation accuracy')
plt.legend()
plt.figure()
plt.plot(epochs, loss, 'bo', label='Training loss')
plt.plot(epochs, val_loss, 'b', label='Validation loss')
plt.title('Training and validation loss')
plt.legend()
plt.show()
if __name__ == '__main__':
plott_results()
-
训练效果与评估分析
训练效果
评估分析
从上面的训练结果可以看出,使用预训练的embedding层出现了严重的过拟合,验证集的准确率只在0.5左右。因为我们的训练集太少了,模型的性能严重的依赖我们选择的200个样本(并且这200个样本还是随机选取的)。为了解决过拟合这一问题,我们对这个base 版本进行改进,不进行预训练再来看看
- 版本改进
我们将embedding层变为可训练的,可以对embedding层的参数进行更新。修改train_model函数为以下:
def train_model():
'''
训练模型
:return:训练时loss,acc
'''
model = Sequential()
model.add(Embedding(max_words, embedding_dim, input_length=max_len))
model.add(Flatten())
model.add(Dense(32, activation='relu'))
model.add(Dense(1, activation='sigmoid'))
model.summary()
# 将GLOVE加载到模型中
x_train, y_train, x_val, y_val, word_index = tokennize_data()
embedding_matrix = parse_word_embedding(word_index)
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('pre_trained_glove_model.h5')
return history
-
训练结果和评估分析
训练效果
评估分析
看来,联合学习的embeeding层训练出来的效果不如预训练的embeeding层效果好,这个效果貌似更差。同时我们对模型进行评估,在测试集上得到的准确率只有0.51,可见在小样本上进行训练是有多么的困难。
loss+acc
【参考文献】
- 《DEEP LEARNING with Python》
一本很好的有关深度学习的书籍,能让你构建一个正确的、系统的深度学习的思想和体系,值得推荐~ - 电子版链接--百度网盘
【代码】
【感谢】
感谢每一个读到这里的朋友,如有疑问,请在下方留言与评论,或者发到我的邮箱,互相学习,互相分享,走过路过,点个赞呗😀