这次是利用TensorFlow进行文本分类,判断电影评价是正面还是负面的.IMDB数据集包含5万个评论,其中2.5万作为训练集,2.5万作为测试集.训练集和数据集相当意味着正负样本数一样.
一.下载IMDB数据集
IMDB数据集经过处理,将单词序列转成数字序列,每一个数字在字典中代表中一个特定的单词.下载的代码如下,下载在文件夹/root/.keras/datasets下面,文件名是imdb.npz.
代码如下:
import tensorflow as tf
from tensorflow import keras
import numpy as np
import matplotlib.pyplot as plt
imdb=keras.datasets.imdb
(train_data,train_labels),(test_data,test_labels)=imdb.load_data(num_words=10000)
这里的参数num_words=10000使得训练样本中的前1万个单词是频率最高的.
二.探索数据
下面将展示一个样本,是一组整数的数组,代表着电影评论的单词.标签是0或者1,0表示的是负面的评论,1代表的是正面的评论.
代码:
print("Training entries:{},labels:{}".format(len(train_data),len(train_labels)))
print(train_data.shape)
print(test_labels.shape)
print(train_data[0])
结果:
Training entries:25000,labels:25000
(25000,)
(25000,)
[1, 14, 22, 16, 43, 530, 973, 1622, 1385, 65, 458, 4468, 66, 3941, 4, 173, 36, 256, 5, 25, 100, 43, 838, 112, 50, 670, 2, 9, 35, 480, 284, 5, 150, 4, 172, 112, 167, 2, 336, 385, 39, 4, 172, 4536, 1111, 17, 546, 38, 13, 447, 4, 192, 50, 16, 6, 147, 2025, 19, 14, 22, 4, 1920, 4613, 469, 4, 22, 71, 87, 12, 16, 43, 530, 38, 76, 15, 13, 1247, 4, 22, 17, 515, 17, 12, 16, 626, 18, 2, 5, 62, 386, 12, 8, 316, 8, 106, 5, 4, 2223, 5244, 16, 480, 66, 3785, 33, 4, 130, 12, 16, 38, 619, 5, 25, 124, 51, 36, 135, 48, 25, 1415, 33, 6, 22, 12, 215, 28, 77, 52, 5, 14, 407, 16, 82, 2, 8, 4, 107, 117, 5952, 15, 256, 4, 2, 7, 3766, 5, 723, 36, 71, 43, 530, 476, 26, 400, 317, 46, 7, 4, 2, 1029, 13, 104, 88, 4, 381, 15, 297, 98, 32, 2071, 56, 26, 141, 6, 194, 7486, 18, 4, 226, 22, 21, 134, 476, 26, 480, 5, 144, 30, 5535, 18, 51, 36, 28, 224, 92, 25, 104, 4, 226, 65, 16, 38, 1334, 88, 12, 16, 283, 5, 16, 4472, 113, 103, 32, 15, 16, 5345, 19, 178, 32]
从上可知,数组包含25000的样本,每一个样本是一个list,代表的是这个评论的单词在字典中的下标.
代码:对于不同的样本,也就是不同的评论的长度是不一样大小的.
print(len(train_data[0]),len(train_data[1]))
结果:
218 189
上面的第一个训练样本长度是218,第二个样本的长度是189.
2.1 将整数转回单词
代码如下:
# A dictionary mapping words to an integer index
word_index = imdb.get_word_index()
# The first indices are reserved
word_index = {k:(v+3) for k,v in word_index.items()}
word_index[""] = 0
word_index[""] = 1
word_index[""] = 2 # unknown
word_index[""] = 3
reverse_word_index = dict([(value, key) for (key, value) in word_index.items()])
def decode_review(text):
return ' '.join([reverse_word_index.get(i, '?') for i in text])
print(decode_review(train_data[0]))
结果:
" this film was just brilliant casting location scenery story direction everyone's really suited the part they played and you could just imagine being there robert is an amazing actor and now the same being director father came from the same scottish island as myself so i loved the fact there was a real connection with this film the witty remarks throughout the film were great it was just brilliant so much that i bought the film as soon as it was released for and would recommend it to everyone to watch and the fly fishing was amazing really cried at the end it was so sad and you know what they say if you cry at a film it must have been good and this definitely was also to the two little boy's that played the of norman and paul they were just brilliant children are often left out of the list i think because the stars that play them all grown up are such a big profile for the whole film but these children are amazing and should be praised for what they have done don't you think the whole story was so lovely because it was true and was someone's life after all that was shared with us all"
三.准备数据
这些评论必须转成tensor在传入卷积神经网络中,下面几种方式可以:
第一种:因为我们之前取得是1万个单词数,所以如果评论是[3,5],我们就需要有一个1万个元素的向量,只有下标3和5的地方为1,其他都为0.这样,我们需要大小为1万*评论数的矩阵.这种思路为词嵌入是对传统的词袋模型编码方案的改进,传统方法使用大而稀疏的矢量来表示每个单词或者在矢量内对每个单词进行评分以表示整个词汇表,这些表示是稀疏的,因为每个词汇的表示是巨大的,给定的词或文档主要由零值组成的大向量表示。
第二种:我们可以将每个评论的都设置成相同的长度,创造一个最大长度*评论数的tensor.单词由密集向量表示,其中向量表示将单词投影到连续向量空间中。
向量空间中的单词的位置是从文本中学习的,并且基于在使用单词时围绕单词的单词。
学习到的向量空间中的单词的位置被称为它的嵌入:Embedding。
我们使用方法二.
我们将评论的数组不满256的补充0,扩充成256个单词数目.padding参数表示不足补0的位置为post在后面补0,
此外这里truncating参数默认为pre即若超过256字符,取前面的256,后面的被截掉了。
train_data=keras.preprocessing.sequence.pad_sequences(train_data,
value=word_index[''],
padding='post',
maxlen=256)
test_data=keras.preprocessing.sequence.pad_sequences(test_data,
value=word_index[''],
padding='post',
maxlen=256)
print(len(train_data[0]),len(train_data[1]),len(test_data[1]))
print(train_data[0])
结果:
256 256 256
[ 1 14 22 16 43 530 973 1622 1385 65 458 4468 66 3941
4 173 36 256 5 25 100 43 838 112 50 670 2 9
35 480 284 5 150 4 172 112 167 2 336 385 39 4
172 4536 1111 17 546 38 13 447 4 192 50 16 6 147
2025 19 14 22 4 1920 4613 469 4 22 71 87 12 16
43 530 38 76 15 13 1247 4 22 17 515 17 12 16
626 18 2 5 62 386 12 8 316 8 106 5 4 2223
5244 16 480 66 3785 33 4 130 12 16 38 619 5 25
124 51 36 135 48 25 1415 33 6 22 12 215 28 77
52 5 14 407 16 82 2 8 4 107 117 5952 15 256
4 2 7 3766 5 723 36 71 43 530 476 26 400 317
46 7 4 2 1029 13 104 88 4 381 15 297 98 32
2071 56 26 141 6 194 7486 18 4 226 22 21 134 476
26 480 5 144 30 5535 18 51 36 28 224 92 25 104
4 226 65 16 38 1334 88 12 16 283 5 16 4472 113
103 32 15 16 5345 19 178 32 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0]
四.建立模型
Keras的核心数据结构是model,一种组织网络层的方式,最简单的数据模型是Sequential模型,它是由多个网络层线性堆叠的栈,对于更复杂的结构,你应该使用Keras函数式,它允许构建任意的神经网络图。
可以简单地使用 .add() 来堆叠模型:
model.add(keras.layers.Dense(units= 64, activation= 'relu', input_dim= 100))
model.add(keras.layers.Dense(units= 10, activation= 'softmax'))
也可以是一个完整的形式:
vocab_size=10000
model=keras.Sequential()
model.add(keras.layers.Embedding(vocab_size,16))
model.add(keras.layers.GlobalAveragePooling1D())
model.add(keras.layers.Dense(16,activation=tf.nn.relu))
model.add(keras.layers.Dense(1,activation=tf.nn.sigmoid))
model.summary()
指定输入数据的 shape
模型需要知道输入数据的shape,因此, Sequential 的第一层需要接受一个关于输入数据shape的参数,后面的各个层则可以自动的推导出中间数据的 shape ,因此不需要为每个层都指定这个参数。有几种方法来为第一层指定输入数据的 shape:
1 传递一个input_shape的关键字参数给第一层, input_shape是一个tuple类型的数据,其中也可以填入None,则表示此位置可能是任何正整数。数据的batch大小不应包含在其中。
2 传递一个 batch_input_shape 的关键字参数给第一层,该参数包含数据的batch大小。该参数在指定固定大小 batch 时比较有用,例如在 stateful RNNs 中。事实上, Keras 在内部会通过添加一个 None 将 input_shape 转化为 batch_input_shape。
3 有些2D层,如Dense,支持通过指定其输入维度input_dim来隐含的指定输入数据shape。一些3D的时域层支持通过参数input_dim和input_dim input_length来指定输入shape。
Merge层主要做的是将输入数据进行数据处理,通过Merge函数中的mode参数来确定数据处理方式,其中主要方法包括:
sum(defualt):逐元素相加
concat:张量串联,可以通过提供concat_axis的关键字参数指定按照哪个轴进行串联
mul:逐元素相乘
ave:张量平均
dot:张量相乘,可以通过dot_axis关键字参数来指定要消去的轴
cos:计算2D张量(即矩阵)中各个向量的余弦距离
此外还可以自定义处理方式:
merged = Merge([left_branch, right_branch], mode=lambda x, y: x - y)
所以说merge方法的处理方式还是很灵活的
结果展示:
_________________________________________________________________
Layer (type) Output Shape Param #
=================================================================
embedding_1 (Embedding) (None, None, 16) 160000
_________________________________________________________________
global_average_pooling1d_1 ( (None, 16) 0
_________________________________________________________________
dense_1 (Dense) (None, 16) 272
_________________________________________________________________
dense_2 (Dense) (None, 1) 17
=================================================================
Total params: 160,289
Trainable params: 160,289
Non-trainable params: 0
4.1嵌入层 Embedding
keras.layers.embeddings.Embedding(input_dim, output_dim, embeddings_initializer='uniform', embeddings_regularizer=None, activity_regularizer=None, embeddings_constraint=None, mask_zero=False, input_length=None)
嵌入层将正整数(下标)转换为具有固定大小的向量,如[[4],[20]]->[[0.25,0.1],[0.6,-0.2]]
Embedding层只能作为模型的第一层
input_dim:大或等于0的整数,字典长度,即输入数据最大下标+1
output_dim:大于0的整数,代表全连接嵌入的维度
embeddings_initializer: 嵌入矩阵的初始化方法,为预定义初始化方法名的字符串,或用于初始化权重的初始化器。参考initializers
embeddings_regularizer: 嵌入矩阵的正则项,为Regularizer对象
embeddings_constraint: 嵌入矩阵的约束项,为Constraints对象
mask_zero:布尔值,确定是否将输入中的‘0’看作是应该被忽略的‘填充’(padding)值,该参数在使用递归层处理变长输入时有用。设置为True
的话,模型中后续的层必须都支持masking,否则会抛出异常。如果该值为True,则下标0在字典中不可用,input_dim应设置为|vocabulary| + 1。
input_length:当输入序列的长度固定时,该值为其长度。如果要在该层后接Flatten
层,然后接Dense
层,则必须指定该参数,否则Dense
层的输出维度无法自动推断。
形如(samples,sequence_length)的2D张量
形如(samples, sequence_length, output_dim)的3D张量
model = Sequential()
model.add(Embedding(1000, 64, input_length=10))
# the model will take as input an integer matrix of size (batch, input_length).
# the largest integer (i.e. word index) in the input should be no larger than 999 (vocabulary size).
# now model.output_shape == (None, 10, 64), where None is the batch dimension.
input_array = np.random.randint(1000, size=(32, 10))
model.compile('rmsprop', 'mse')
output_array = model.predict(input_array)
assert output_array.shape == (32, 10, 64)
keras.layers.pooling.GlobalAveragePooling1D()
为时域信号施加全局平均值池化
keras.layers.core.Dense(units, activation=None, use_bias=True, kernel_initializer='glorot_uniform', bias_initializer='zeros', kernel_regularizer=None, bias_regularizer=None, activity_regularizer=None, kernel_constraint=None, bias_constraint=None)
Dense就是常用的全连接层,所实现的运算是output = activation(dot(input, kernel)+bias)
。其中activation
是逐元素计算的激活函数,kernel
是本层的权值矩阵,bias
为偏置向量,只有当use_bias=True
才会添加。
如果本层的输入数据的维度大于2,则会先被压为与kernel
相匹配的大小。
这里是一个使用示例:
# as first layer in a sequential model:
model = Sequential()
model.add(Dense(32, input_shape=(16,)))
# now the model will take as input arrays of shape (*, 16)
# and output arrays of shape (*, 32)
# after the first layer, you don't need to specify
# the size of the input anymore:
model.add(Dense(32))
units:大于0的整数,代表该层的输出维度。
activation:激活函数,为预定义的激活函数名(参考激活函数),或逐元素(element-wise)的Theano函数。如果不指定该参数,将不会使用任何激活函数(即使用线性激活函数:a(x)=x)
use_bias: 布尔值,是否使用偏置项
kernel_initializer:权值初始化方法,为预定义初始化方法名的字符串,或用于初始化权重的初始化器。参考initializers
bias_initializer:偏置向量初始化方法,为预定义初始化方法名的字符串,或用于初始化偏置向量的初始化器。参考initializers
kernel_regularizer:施加在权重上的正则项,为Regularizer对象
bias_regularizer:施加在偏置向量上的正则项,为Regularizer对象
activity_regularizer:施加在输出上的正则项,为Regularizer对象
kernel_constraints:施加在权重上的约束项,为Constraints对象
bias_constraints:施加在偏置上的约束项,为Constraints对象
形如(batch_size, ..., input_dim)的nD张量,最常见的情况为(batch_size, input_dim)的2D张量
形如(batch_size, ..., units)的nD张量,最常见的情况为(batch_size, units)的2D张量
4.2 损失函数和优化器
我们这里的损失函数设置成binary_crossentropy,这个并不是唯一的损失函数,还可以使用mean_squared_error,但是因为binary_crossentrpy这个能够更好的处理我们的概率问题,所以这里采用这个.为什么采用这个交叉熵函数,而不是普通的MSE呢?
下面简要的解释一下:(因为结果比较的复杂,这里我们写在草稿纸上进行展示)
一般来说sigmoid是用在二分类的问题,softmax是用于多分类的问题,同样的对应着这样的损失函数binary_crossentopy和categorical_crossentropy.但是注意一般我们在构建模型的时候,sigmoid激活函数可以放在最后一层也可以放在中间层,softmax只放在最后一层(一般认为是单独的一层,而不是单单的一个激活函数).且很特殊的是softmax是多输入多输出的,但是其他的激活函数仅仅是单输入和单输出.
代码如下:
model.compile(optimizer=tf.train.AdamOptimizer(),
loss='binary_crossentropy',
metrics=['accuracy'])
五.建立交叉验证集合
如果你看过机器学习的课程,你应该知道交叉验证集的作用,这里不讨论,可以查看我之前的博客.
这里的代码如下:
x_val=train_data[:10000]
partial_x_train=train_data[10000:]
y_val=train_labels[:10000]
partial_y_train=train_labels[10000:]
六.训练模型
代码如下:(结果不展示了就是迭代了40次,很长)每次迭代完成输出的是训练的误差和准确率还有交叉验证集的误差和准确率.
history=model.fit(partial_x_train,
partial_y_train,
epochs=40,
batch_size=512,
validation_data=(x_val,y_val),
verbose=1
)
七.评估模型
代码如下:(在测试集合上测试损失和准确率)
八.在时间上建立一个关于准确率和损失的图
我们在这里建立了两个图:第一个是关于迭代步数的损失图,第二个是关于迭代步数的准确率图.
代码如下:
results=model.evaluate(test_data,test_labels)
print(results)
结果:
[0.30745334438323974, 0.8748]
可以看到,这里的history是由fit返回的,是一个包含了训练过程中的结果的字典.返回的是训练和交叉验证的过程中的损失和准确率.对于我们的训练集合展示的是蓝色的点,对于我们的交叉验证集我们采用的是蓝色的实线.
history_dict=history.history
history_dict.keys()
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,loss,'bo',label='Training loss')
plt.plot(epochs,val_loss,'b',label='Validation loss')
plt.title('Trainig and validation loss')
plt.xlabel('Epochs')
plt.ylabel('Loss')
plt.legend()
plt.show()
plt.clf()
acc_values=history_dict['acc']
val_acc_values=history_dict['val_acc']
plt.plot(epochs,acc,'bo',label='Training acc')
plt.plot(epochs,val_acc,'b',label='Validation acc')
plt.title('Training and validation accuracy')
plt.xlabel('Epochs')
plt.ylabel('Loss')
plt.legend()
plt.show()
结果展示:
下面的两个图你可以看到对于训练集合损失一直在下降,准确率一直在上升,但是对于交叉验证集他到差不多20次迭代的时候就基本平了,这就是所谓的过拟合的现象,即我们的模型对于训练集合能够达到很高的准确率,但是在训练反而对一般的未见过的数据很难准确的预测结果.一般的步骤就是只训练大约20次这样就行了.
图一:
图2
嵌入层将正整数(下标)转换为具有固定大小的向量,如[[4],[20]]->[[0.25,0.1],[0.6,-0.2]]
这涉及到词向量,具体看可以参考这篇文章:Word2vec 之 Skip-Gram 模型,下面只进行简单的描述,
上图的流程是把文章的单词使用词向量来表示。
(1)提取文章所有的单词,把其按其出现的次数降序(这里只取前50000个),比如单词‘network’出现的次数最多,编号ID为0,依次类推…
(2)每个编号ID都可以使用50000维的二进制(one-hot)表示
(3)最后,我们会生产一个矩阵M,行大小为词的个数50000,列大小为词向量的维度(通常取128或300),比如矩阵的第一行就是编号ID=0,即network对应的词向量。
那这个矩阵M怎么获得呢?在Skip-Gram 模型中,我们会随机初始化它,然后使用神经网络来训练这个权重矩阵
那我们的输入数据和标签是什么?如下图,输入数据就是中间的哪个蓝色的词对应的one-hot编码,标签就是它附近词的one-hot编码(这里windown_size=2,左右各取2个)
就上述的Word2Vec中的demo而言,它的单词表大小为1000,词向量的维度为300,所以Embedding的参数 input_dim=1000,output_dim=300
回到最初的问题:嵌入层将正整数(下标)转换为具有固定大小的向量,如[[4],[20]]->[[0.25,0.1],[0.6,-0.2]]
举个栗子:假如单词表的大小为1000,词向量维度为2,经单词频数统计后,tom对应的id=4,而jerry对应的id=20,经上述的转换后,我们会得到一个M1000×2的矩阵,而tom对应的是该矩阵的第4行,取出该行的数据就是[0.25,0.1]
如果输入数据不需要词的语义特征语义,简单使用Embedding层就可以得到一个对应的词向量矩阵,但如果需要语义特征,我们大可把以及训练好的词向量权重直接扔到Embedding层中即可,具体看参考keras提供的栗子:在Keras模型中使用预训练的词向量