【导读】Keras是一个由Python编写的开源人工神经网络库,可以作为Tensorflow、和Theano的高阶应用程序接口,进行深度学习模型的设计、调试、评估、应用和可视化。本系列将教你如何从零开始学Keras,从搭建神经网络到项目实战,手把手教你精通Keras。相关内容参考《Python深度学习》这本书。
二分类问题
二分类问题可能是应用最广泛的机器学习问题。在这篇文章中,你将学习根据电影评论的文字内容将其划分为正面或负面。
本文章使用 IMDB 数据集,它包含来自互联网电影数据库(IMDB)的 50 000 条严重两极分化的评论。数据集被分为用于训练的 25 000 条评论与用于测试的 25 000 条评论,训练集和测试集都包含 50% 的正面评论和 50% 的负面评论。
为什么要将训练集和测试集分开?因为你不应该将训练机器学习模型的同一批数据再用于测试模型!模型在训练数据上的表现很好,并不意味着它在前所未见的数据上也会表现得很好,而且你真正关心的是模型在新数据上的性能(因为你已经知道了训练数据对应的标签,显然不再需要模型来进行预测)。例如,你的模型最终可能只是记住了训练样本和目标值之间的映射关系,但这对在前所未见的数据上进行预测毫无用处。下一章将会更详细地讨论这一点。
与 MNIST 数据集一样,IMDB 数据集也内置于 Keras 库。它已经过预处理:评论(单词序列) 已经被转换为整数序列,其中每个整数代表字典中的某个单词。下列代码将会加载 IMDB 数据集(第一次运行时会下载大约 80MB 的数据,可以不翻墙,反复试几次)。
importkerasfromkeras.datasetsimportimdb(train_data, train_labels), (test_data, test_labels) =imdb.load_data(num_words=10000)
参数 num_words=10000 的意思是仅保留训练数据中前 10 000 个最常出现的单词。低频单词将被舍弃。这样得到的向量数据不会太大,便于处理。
train_data 和 test_data 这两个变量都是评论组成的列表,每条评论又是单词索引组成 的列表(表示一系列单词)。train_labels 和 test_labels 都是 0 和 1 组成的列表,其中 0 代表负面(negative),1 代表正面(positive)。
train_data[0] train_labels[0] 输出为1由于限定为前10000个最常见的单词,单词索引都不会超过10000。max([max(sequence) forsequenceintrain_data]) 输出为9999
准备数据
你不能将整数序列直接输入神经网络。你需要将列表转换为张量。转换方法有以下两种。
- 填充列表,使其具有相同的长度,再将列表转换成形状为 (samples, word_indices) 的整数张量,然后网络第一层使用能处理这种整数张量的层(即 Embedding 层,本书后面会详细介绍)。
- 对列表进行 one-hot 编码,将其转换为 0 和 1 组成的向量。举个例子,序列 [3, 5] 将会 被转换为 10 000 维向量,只有索引为 3 和 5 的元素是 1,其余元素都是 0。然后网络第一层可以用 Dense 层,它能够处理浮点数向量数据。
- 下面我们采用后一种方法将数据向量化。为了加深理解,你可以手动实现这一方法,如下所示。
importnumpyasnpdefvectorize_sequences(sequences, dimension=10000): results=np.zeros((len(sequences), dimension)) fori, sequenceinenumerate(sequences): results[i, sequence] =1.#(将results[i] 的指定索引设为1)returnresultsx_train=vectorize_sequences(train_data) x_test=vectorize_sequences(test_data)
样本变为:
x_train[0]array([ 0., 1., 1., ..., 0., 0., 0.])
你还应该将标签向量化,这很简单。
y_train=np.asarray(train_labels).astype('float32') y_test=np.asarray(test_labels).astype('float32')
现在可以将数据输入到神经网络中。
构建网络
输入数据是向量,而标签是标量(1 和 0),这是你会遇到的最简单的情况。有一类网络在这种问题上表现很好, 就是带有 relu 激活的全连接层(Dense)的简单堆叠,比如Dense(16, activation='relu')。
传入Dense 层的参数(16)是该层隐藏单元的个数。一个隐藏单元(hidden unit)是该层 表示空间的一个维度。我们在第 2 章讲过,每个带有 relu 激活的 Dense 层都实现了下列张量运算:
output = relu(dot(W, input) + b)
16 个隐藏单元对应的权重矩阵 W 的形状为 (input_dimension, 16),与 W 做点积相当于将输入数据投影到 16 维表示空间中(然后再加上偏置向量 b 并应用 relu 运算)。你可以将表示空间的维度直观地理解为“网络学习内部表示时所拥有的自由度”。隐藏单元越多(即更高维的表示空间),网络越能够学到更加复杂的表示,但网络的计算代价也变得更大,而且可能会导致学到不好的模式(这种模式会提高训练数据上的性能,但不会提高测试数据上的性能)。对于这种 Dense 层的堆叠,你需要确定以下两个关键架构:
- 网络有多少层;
- 每层有多少个隐藏单元。
现在你选择下列架构:
- 两个中间层,每层都有 16 个隐藏单元;
- 第三层输出一个标量,预测当前评论的情感。
中间层使用 relu 作为激活函数,最后一层使用 sigmoid 激活以输出一个 0~1 范围内的概率值(表示样本的目标值等于 1 的可能性,即评论为正面的可能性)。relu(rectified linear unit,整流线性单元)函数将所有负值归零,而 sigmoid 函数则将任意值“压缩”到 [0,1] 区间内,其输出值可以看作概率值。
网络架构如下:
代码实现如下
fromkerasimportmodelsfromkerasimportlayersmodel=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'))
最后,你需要选择损失函数和优化器。由于你面对的是一个二分类问题,网络输出是一个概率值(网络最后一层使用 sigmoid 激活函数,仅包含一个单元),那么最好使用 binary_crossentropy
(二元交叉熵)损失。这并不是唯一可行的选择,比如你还可以使用 mean_squared_error
(均方误差)。但对于输出概率值的模型,交叉熵(crossentropy)往往是最好的选择。交叉熵是来自于信息论领域的概念,用于衡量概率分布之间的距离,在这个例子中就是真实分布与预测值之间的距离。
下面的步骤是用 rmsprop 优化器和 binary_crossentropy
损失函数来配置模型。注意,我们还在训练过程中监控精度。
model.compile(optimizer='rmsprop', loss='binary_crossentropy', metrics=['accuracy'])
上述代码将优化器、损失函数和指标作为字符串传入,这是因为 rmsprop、binary_ crossentropy 和 accuracy 都是 Keras 内置的一部分。有时你可能希望配置自定义优化器的 参数,或者传入自定义的损失函数或指标函数。前者可通过向 optimizer 参数传入一个优化器类实例来实现,如代码所示:
from keras import optimizers model.compile(optimizer=optimizers.RMSprop(lr=0.001), loss='binary_crossentropy', metrics=['accuracy'])
验证你的方法
为了在训练过程中监控模型在前所未见的数据上的精度,你需要将原始训练数据留出 10 000个样本作为验证集。
x_val = x_train[:10000] partial_x_train = x_train[10000:] partial_y_train = y_train[10000:]
现在使用 512 个样本组成的小批量,将模型训练 20 个轮次(即对 x_train 和 y_train 两 个张量中的所有样本进行 20 次迭代)。与此同时,你还要监控在留出的 10 000 个样本上的损失和精度。你可以通过将验证数据传入 validation_data 参数来完成。
history = model.fit(partial_x_train, partial_y_train, epochs=20, batch_size=512, validation_data=(x_val, y_val))
结果如下图:
调用 model.fit() 返回了一个 History 对象。这个对象有一个成员 history,它是一个字典,包含训练过程中的所有数据。我们来看一下。
history_dict = history.history history_dict.keys() 输出为: dict_keys(['val_loss', 'val_binary_accuracy', 'loss', 'binary_accuracy'])
字典中包含 4 个条目,对应训练过程和验证过程中监控的指标。在下面两个代码清单中, 我们将使用 Matplotlib 在同一张图上绘制训练损失和验证损失,以及训练精度和验证精度)。
import matplotlib.pyplot as plt %matplotlib inline #使显示的图像在notebook可见 acc = history.history['binary_accuracy'] val_acc = history.history['val_binary_accuracy'] loss = history.history['loss'] val_loss = history.history['val_loss'] epochs = range(1, len(acc) + 1) # "bo" is for "blue dot"('bo' 表示蓝色圆点) plt.plot(epochs, loss, 'bo', label='Training loss') # b is for "solid blue line"('b' 表示蓝色实线) plt.plot(epochs, val_loss, 'b', label='Validation loss') plt.title('Training and validation loss') plt.xlabel('Epochs') plt.ylabel('Loss') plt.legend() plt.show()
结果如下:
plt.clf() # clear figure(清空图像) acc_values = history_dict['binary_accuracy'] val_acc_values = history_dict['val_binary_accuracy'] 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()
点是训练损失和准确率,而实线是验证损失和准确性。请注意,由于网络的随机初始化不同,您自己的结果可能略有不同。
如你所见,训练损失每轮都在降低,训练精度每轮都在提升。这就是梯度下降优化的预期 结果——你想要最小化的量随着每次迭代越来越小。但验证损失和验证精度并非如此:它们似 乎在第四轮达到最佳值。这就是我们之前警告过的一种情况:模型在训练数据上的表现越来越好, 但在前所未见的数据上不一定表现得越来越好。准确地说,你看到的是过拟合(overfit):在第二轮之后,你对训练数据过度优化,最终学到的表示仅针对于训练数据,无法泛化到训练集之外的数据。
在这种情况下,为了防止过拟合,你可以在 3 轮之后停止训练。通常来说,你可以使用许 多方法来降低过拟合,我们将在第 4 章中详细介绍.
我们从头开始训练一个新的网络,训练 4 轮,然后在测试数据上评估模型。
1. model = models.Sequential() 2. model.add(layers.Dense(16, activation='relu', input_shape=(10000,))) 3. model.add(layers.Dense(16, activation='relu')) 4. model.add(layers.Dense(1, activation='sigmoid')) 5. model.compile(optimizer='rmsprop', 6. loss='binary_crossentropy', 7. metrics=['accuracy']) 8. model.fit(x_train, y_train, epochs=4, batch_size=512) 9. results = model.evaluate(x_test, y_test)
迭代结果如下:
1. print(results) 2. 输出为: 3. [0.32315461338043211, 0.87348000000000003]
这种相当简单的方法得到了 88% 的精度。
使用训练好的网络在新数据上生成预测结果
训练好网络之后,你希望将其用于实践。你可以用 predict 方法来得到评论为正面的可能性大小。
1. model.predict(x_test) 2. 输出为: 3. array([[ 0.14026152], 4. [ 0.99970287], 5. [ 0.29552525], 6. ..., 7. [ 0.07234977], 8. [ 0.04342838], 9. [ 0.48153383]], dtype=float32)
如你所见,网络对某些样本的结果非常确信(大于等于 0.99,或小于等于 0.01),但对其他结果却不那么确信(0.6 或 0.4)。
进一步改进
通过以下实验,你可以确信前面选择的网络架构是非常合理的,虽然仍有改进的空间。
前面使用了两个隐藏层。你可以尝试使用一个或三个隐藏层,然后观察对验证精度和测试精度的影响。
- 尝试使用更多或更少的隐藏单元,比如 32 个、64 个等。
- 尝试使用 mse 损失函数代替 binary_crossentropy。
- 尝试使用 tanh 激活(这种激活在神经网络早期非常流行)代替 relu。
这些实验将有助于说服您,我们所做的架构选择都是相当合理的,尽管它们仍然可以改进!