《Deep Learning with Python》第三章 3.4 走进神经网络之电影影评分类

3.4 电影影评分类

二元分类,或者称为二值分类,可能是应用最广泛的机器学习问题。通过学习本例,你将掌握如何基于文本内容将影评分为正、负二类。

3.4.1 IMDB数据集

本文将从互联网电影数据库(IMDB)获取50,000个流行电影影评作为数据集。这里将其分割为25,000个影评的训练集和25,000个影评的测试集。其中每个数据集都包含50%的好评和50%的差评。

为什么要将数据集分割成训练集和测试集呢?因为测试机器学习模型所使用的数据集不能和训练该模型的数据集是同一个。在训练集上表现良好的模型,并不意味着一定会在“未曾见过”的测试集上也有相同的表现。也就是说,你更关注的是训练的模型在新数据集上的性能(因为训练集数据的标签是已知的,很显然这些是不需要去预测的)。例如,可能你的模型可以将训练样本和对应的目标在内存中进行一一映射,但是这个模型对从“未见过的”数据无法进行预测。下一章会更详细的讨论该观点。

Keras已经包括IMDB数据集,并进行了数据预处理:影评(单词序列)转换成整数序列,这里每个整数代表对应单词在字典的索引值。

下面的代码将加载IMDB数据集,当你首次运行该代码,将会在服务器上下载大约80M的数据。

1
2
3
4
#Listing 3.1 Loading the IMDB dataset
from keras.datasets import imdv
(train_data, train_labels), (test_data, test_labels) = imdb.load_data(
num_words=10000)

设置参数num_words=10000,保留训练集中词频为top 10000的单词,低频单词丢弃。变量train_data和test_data是影评列表(list),每条影评看成是单词序列,用单词索引进行编码。train_labels和test_labels是0和1的列表,其中0代表差评(negative),1代表好评(positive)。

1
2
3
4
>>> train_data[0]
[1, 14, 22, 16, ... 178, 32]
>>> train_labels[0]
1

前面限制影评中的单词词频为top 10000,所以单词的索引不会超过10000:

1
2
>>> max([max(sequence) for sequence in train_data])
9999

下面来个好玩的,如何将编码后的影评进行解码得到单词呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
'''
word_index is a dictionary mapping
words to an integer index.
'''
word_index = imdb.get_word_index()
reverse_word_index = dict(
'''
Reverses it, mapping integer indices to words
'''
[(value, key) for (key, value) in word_index.items()])
decoded_review = ' '.join(
'''
Decodes the review. Note that the indices
are offset by 3 because 0, 1, and 2 are
reserved indices for “padding,” “start of
sequence,” and “unknown.”
'''
[reverse_word_index.get(i - 3, '?') for i in train_data[0]])
3.4.2 准备数据

神经网络不能输入整数列表,所以需要将整数列表转换成张量。有两种方式可以实现:

  • 填充列表:先将列表填充成相同长度的,再转成形状为(样本数,单词索引长度)的整数张量。接着用神经网络的第一层layer(Embedding layer)处理整数张量。
  • one-hot编码:one-hot编码是将单词索引转成0、1的向量。比如,将序列[3, 5]转成10,000维向量,其中索引3和5的值为1,其它索引对应的值为0。然后使用神经网络的Dense layer作为第一层layer处理浮点型向量数据。

下面采用后一种方法向量化数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import numpy as np
def vectorize_sequences(sequences, dimension=10000):
#Creates an all-zero matrix of shape (len(sequences), dimension)
results = np.zeros((len(sequences), dimension))
for i, sequence in enumerate(sequences):
'''
Sets specific indices of results[i] to 1s
'''
results[i, sequence] = 1.
return results
'''
Vectorized training data and test data
'''
x_train = vectorize_sequences(train_data)
x_test = vectorize_sequences(test_data)

下面看下向量化后的结果:

1
2
>>> x_train[0]
array([ 0., 1., 1., ..., 0., 0., 0.]

同理,向量化对应的label:

1
2
y_train = np.asarray(train_labels).astype('float32')
y_test = np.asarray(test_labels).astype('float32')

数据准备好了,就等着传入神经网络模型。

3.4.3 构建神经网络模型

输入数据为向量,label为标量(1和0),相当简单。一系列带有relu激活函数的全联接层(Dense layer)的神经网络就可以很好的解决影评分类:Dense(16, activation=’relu’)。

每个Dense layer设置隐藏单元(hidden unit)数为16。hidden unit是layer的一维表征空间。由第二章得知,每个带有relu激励函数的Dense layer可以实现下面链式的张量操作:

1
output = relu(dot(W, input) + b)

16个hidden unit意味着权重矩阵的形状为(输入维度,16):输入数据与权重矩阵W点积的结果是投影到16维表征空间(,接着加上偏置向量b,然后应用relu激活操作)。给人的直觉是表征空间的维数即是中间学习表示的自由度。hidden unit越多(高维表征空间)允许神经网络学习更复杂的表示,但同时也让神经网络计算成本增加,可能导致不可预期的模式(模式会提高训练集上的性能,降低测试集上的表现,也就是常说的“过拟合”现象)。

逐层排列的Dense layer架构有两个关键点:

  • 选择多少层Dense layer
  • 每个Dense layer选择多少个hidden unit

在第四章中的常规性原则将会指导你对上述问题做出选择。此时,你就暂时相信下面的架构选择哦:

  • 两个具有16个hidden unit的中间层
  • 第三层layer将输出当前影评的情感预测值(标量)

中间层使用relu作为激活函数,最后一层layer使用sigmoid激活函数,输出0到1之间的概率值。激活函数relu(rectified linear unit(修正线性单元),ReLU),对于所有负值都置为0,而正值不变,见图3.4;而激活函数sigmoid将变量值映射为[0, 1]区间,可以看作是概率值,见图3.5。

image

图3.4 Relu激活函数

image

图3.5 Sigmoid激活函数

image

图3.6 三层layer神经网络

图3.6显示了神经网络的大体架构。下面是Keras的实现,和前面MNIST数字识别的例子类似:

1
2
3
4
5
6
#Listing 3.3 The model definition
from keras import models
from keras import layers
model = models.Sequential()
model.add(layers.Dense(16, activation='relu', input_shape=(10000,)))
model.add(layers.Dense(16, activation='relu')) model.add(layers.Dense(1, activation='sigmoid'))

什么是激活函数呢?为什么要使用激活函数?

没有像relu这样的激活函数(俗称非线性单元)的话,那Dense layer只剩下两个线性操作:点积和加法。

1
2
> output = dot(W, input) + b
>

>

这样layer只能学习到输入数据的线性变换(仿射变换):layer的假设空间就成了输入数据的所有可能的线性变换到16维表征空间的集合。这样的假设空间并不能学习到多层layer的表征,因为一系列的线性layer等效于一个线性操作:layer数的增加并不会扩展假设空间。

为了从深度学习中得到更丰富的假设空间,你需要加入非线性部分,或者激活函数。relu是深度学习中最常用的激活函数之一,但是也有其它可选:prelu激活函数、elu激活函数等等。

接着,选择损失函数和优化器。因为本例是二值分类问题,神经网络模型输出是概率值(网络的最后一层layer带有sigmoid激活函数,输出一维数据),所以最好的损失函数是binary_crossentropy损失函数。但这不是唯一的选择,你也可以使用mean_squared_error损失函数。一般输出为概率值的模型优先选择交叉熵损失函数(crossentropy)。交叉熵是信息论中的指标,用来度量概率分布之间的距离。本例是用来度量实际分布与预测值的差距。

这里为模型选择binary_crossentropy损失函数和rmsprop优化器。注意监控模型训练过程中的准确度。

1
2
3
4
#Listing 3.4 Compiling the model
model.compile(optimizer='rmsprop',
loss='binary_crossentropy',
metrics=['accuracy'])

上面传入的优化器optimizer、损失函数loss和指标metrics三个参数都是字符串型,这是因为’rmsprop’、’binary_crossentropy’和’accuracy’都是Keras内置实现的。如果想配置自定义的优化器或者损失函数或者指标函数,你可以用参数optimizer传入优化器类,见代码3.5;用参数lossmetrics传入函数对象,见代码3.6:

1
2
3
4
5
6
7
8
9
10
11
12
#Listing 3.5 Configuring the optimiser
from keras import optimisers
model.compile(optimizer=optimizers.RMSprop(lr=0.001),
loss='binary_crossentropy',
metrics=['accuracy'])
#Listing 3.6 Using custom losses and metrics
from keras import losses
from keras import metrics
model.compile(optimizer=optimizers.RMSprop(lr=0.001),
loss=losses.binary_crossentropy,
metrics=[metrics.binary_accuracy])
3.4.4 验证模型

为了监控模型训练过程中模型在新数据上的准确度,需要从原始的训练数据集中分出10,000个样本作为验证集。

1
2
3
4
5
6
#Listing 3.7 Setting aside a validation set
x_val = x_train[:10000]
partial_x_train = x_train[10000:]
y_val = y_train[:10000]
partial_y_train = y_train[10000:]

现在开始模型训练,迭代训练的epoch(在所有训练集数据上跑完一次称为一个epoch)为20个,mini-batch大小为512。训练过程中监控验证集数据上的损失函数和准确度,设置参数validation_data。

1
2
3
4
5
6
7
8
9
#Listing 3.8 Training your model
model.compile(optimizer='rmsprop',
loss='binary_crossentropy',
metrics=['acc'])
history = model.fit(partial_x_train,
partial_y_train,
epochs=20,
batch_size=512,
validation_data=(x_val, y_val))

在CPU上训练模型时,每个epoch耗时不到2秒,整个训练过程大概持续20秒。每个epoch结束时,会有个短暂的停顿,这时模型会计算验证集数据上的损失值和准确度。

注意,调用model.fit()会返回一个History对象,该对象有个history成员,它是一个包含训练过程的每个数据的字典。下面来看下:

1
2
3
>>> history_dict = history.history
>>> history_dict.keys()
[u'acc', u'loss', u'val_acc', u'val_loss']

history字典有四项:模型训练和验证中每个指标一项。接下来的两段代码,使用Matplotlib在同一幅图中绘制训练集损失和验证集损失,见图3.7;同时将训练集准确度和验证集准确度绘制在同一幅图中,见图3.8。注意,因为神经网络的初始化是随机的,可能会导致你的结果与本例稍有差别。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#Listing 3.9 Plotting the training and validation loss
import matplotlib.pyplot as pet
history_dict = history.history
loss_values = history_dict['loss']
val_loss_values = history_dict['val_loss']
epochs = range(1, len(acc) + 1)
'''
“bo” is for “blue dot.”
“b” is for “solid blue line.”
'''
plt.plot(epochs, loss_values, 'bo', label='Training loss') plt.plot(epochs, val_loss_values, 'b', label='Validation loss')
plt.title('Training and validation loss')
plt.xlabel('Epochs')
plt.ylabel('Loss')
plt.legend()
plt.show()

image

图3.7 迭代训练中训练集和验证集的损失趋势

1
2
3
4
5
6
7
8
9
10
11
12
13
#Listing 3.10 Plotting the training and validation accuracy
#Clears the figure
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()

image

图3.8 迭代训练中的训练集和验证集的准确度趋势

正如你所见,随着迭代训练的epoch,训练损失值不断减小,而准确度不断提升。这也是执行梯度下降优化器期待的结果:不断迭代训练减小损失。但是验证集数据上的损失值和准确度表现的并不是如此:验证集在第四个epoch后效果达到最好。这也是前面提醒过的:模型在训练集上表现良好并不代表在新数据集上也有同样的表现。准确地来讲,这是过拟合(overfiting):在迭代训练第2个epoch后,模型在训练集上出现了过度优化,最终的学习表征像是为训练集特制的,对新数据丧失了泛化能力。

本例中,为了防止过拟合出现,需要在迭代训练3个epoch后停止训练。一般来讲,我们可以使用多种技术解决过拟合,这些会在第四章中详细介绍。

下面从头迭代训练4个epoch生成新的神经网络,并在测试集上评估效果:

1
2
3
4
5
6
7
8
#Listing 3.11 Retraining a model from scratch
model = models.Sequential()
model.add(layers.Dense(16, activation='relu', input_shape=(10000,))) model.add(layers.Dense(16, activation='relu')) model.add(layers.Dense(1, activation='sigmoid'))
model.compile(optimizer='rmsprop',
loss='binary_crossentropy',
metrics=['accuracy'])
model.fit(x_train, y_train, epochs=4, batch_size=512)
results = model.evaluate(x_test, y_test)

最好的评估结果如下:

1
2
>>> results
[0.2929924130630493, 0.88327999999999995]

这个相当直白的方法获得了88%的准确度。使用最新的方法,你将会得到接近95%的准确度。

3.4.5 模型预测

训练完神经网络模型,使用predict方法进行影评情感预测:

1
2
3
4
5
6
7
8
>>> model.predict(x_test)
array([[ 0.98006207]
[ 0.99758697]
[ 0.99975556]
...,
[ 0.82167041]
[ 0.02885115]
[ 0.65371346]], dtype=float32)

正如你所看到的结果,神经网络模型对一些样本数据的预测结果自信(概率为0.99或者更高,或者0.01或者更小),但是对另外一些的预测结果不是太自信(概率为0.6,0.4的情况)。

3.4.6 延伸实验

下面的一些实验使得神经网络的架构选择更合理些(虽然还是有待提升的空间):

  • 本例使用的两个隐藏层。可以尝试选择一个或者三个隐藏层,看下会怎样影响验证集和测试集的准确度;
  • 选择更多的hidden unit或者更少的hidden unit:32个unit,64个unit等等;
  • 使用mse损失函数代替binary_crossentropy损失函数;
  • 使用tanh激活函数(在神经网络早期常用的激活函数)代替relu激活函数。
3.4.7 总结

从本实例学到的知识点:

  • 原始数据集预处理为张量传入神经网络。单词序列编码为二值向量或者其它形式;
  • 一系列带有relu激活函数的Dense layer能解决广泛的问题,包括情感分类,后续会常用到的;
  • 二值分类问题(输出两个类别)中,最后的一个Dense layer带有一个sigmoid激活函数和一个单元:网络输出是0到1之间的标量,代表概率值;
  • 二分类问题中有sigmoid标量输出的,损失函数选择binary_crossentropy损失函数;
  • rmsprop优化器对于大部分深度学习模型来说是足够好的选择;
  • 随着在训练集上表现越来越好,神经网络模型开始过拟合,在新数据上表现越来越差。关注验证集上的监控指标

未完待续。。。

Enjoy!

翻译本书系列的初衷是,觉得其中把深度学习讲解的通俗易懂。不光有实例,也包含作者多年实践对深度学习概念、原理的深度理解。最后说不重要的一点,François Chollet是Keras作者。
声明本资料仅供个人学习交流、研究,禁止用于其他目的。如果喜欢,请购买英文原版。


侠天,专注于大数据、机器学习和数学相关的内容,并有个人公众号:bigdata_ny分享相关技术文章。

若发现以上文章有任何不妥,请联系我。

image