从零开始学Keras(二)

2022-08-24 15:46:05 浏览数 (1)

 【导读】Keras是一个由Python编写的开源人工神经网络库,可以作为Tensorflow、和Theano的高阶应用程序接口,进行深度学习模型的设计、调试、评估、应用和可视化。本系列将教你如何从零开始学Keras,从搭建神经网络到项目实战,手把手教你精通Keras。相关内容参考《Python深度学习》这本书。                   

二分类问题

  二分类问题可能是应用最广泛的机器学习问题。在这篇文章中,你将学习根据电影评论的文字内容将其划分为正面或负面。

  本文章使用 IMDB 数据集,它包含来自互联网电影数据库(IMDB)的 50 000 条严重两极分化的评论。数据集被分为用于训练的 25 000 条评论与用于测试的 25 000 条评论,训练集和测试集都包含 50% 的正面评论和 50% 的负面评论。

  为什么要将训练集和测试集分开?因为你不应该将训练机器学习模型的同一批数据再用于测试模型!模型在训练数据上的表现很好,并不意味着它在前所未见的数据上也会表现得很好,而且你真正关心的是模型在新数据上的性能(因为你已经知道了训练数据对应的标签,显然不再需要模型来进行预测)。例如,你的模型最终可能只是记住了训练样本和目标值之间的映射关系,但这对在前所未见的数据上进行预测毫无用处。下一章将会更详细地讨论这一点。

  与 MNIST 数据集一样,IMDB 数据集也内置于 Keras 库。它已经过预处理:评论(单词序列) 已经被转换为整数序列,其中每个整数代表字典中的某个单词。下列代码将会加载 IMDB 数据集(第一次运行时会下载大约 80MB 的数据,可以不访问国外网站,反复试几次)。

代码语言:javascript复制
import keras
from keras.datasets import imdb
(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)。

代码语言:javascript复制
train_data[0]train_labels[0]输出为1由于限定为前 10000 个最常见的单词,单词索引都不会超过 10 000。max([max(sequence) for sequence in train_data])输出为9999

准备数据

  你不能将整数序列直接输入神经网络。你需要将列表转换为张量。转换方法有以下两种。

  • 填充列表,使其具有相同的长度,再将列表转换成形状为 (samples, word_indices) 的整数张量,然后网络第一层使用能处理这种整数张量的层(即 Embedding 层,本书后面会详细介绍)。
  • 对列表进行 one-hot 编码,将其转换为 0 和 1 组成的向量。举个例子,序列 [3, 5] 将会 被转换为 10 000 维向量,只有索引为 3 和 5 的元素是 1,其余元素都是 0。然后网络第一层可以用 Dense 层,它能够处理浮点数向量数据。
  • 下面我们采用后一种方法将数据向量化。为了加深理解,你可以手动实现这一方法,如下所示。 
代码语言:javascript复制
import numpy as np
def vectorize_sequences(sequences, dimension=10000):

# (创建一个形状为 (len(sequences), dimension) 的零矩阵)

results = np.zeros((len(sequences), dimension))
for i, sequence in enumerate(sequences):
results[i, sequence] = 1.  # (将 results[i] 的指定索引设为 1)

return results
# Our vectorized training data(将训练数据向量化)

x_train = vectorize_sequences(train_data)
# Our vectorized test data(将测试数据向量化)

x_test = vectorize_sequences(test_data)

样本变为:

代码语言:javascript复制
x_train[0]
array([ 0.,  1.,  1., ...,  0.,  0.,  0.])

你还应该将标签向量化,这很简单。

代码语言:javascript复制
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] 区间内,其输出值可以看作概率值。

网络架构如下:

代码实现如下

代码语言:javascript复制
from keras import models
 
from keras import layers
 
model = models.Sequential()
 
model.add(layers.Dense(, activation='relu', input_shape=(,)))
 
model.add(layers.Dense(, activation='relu'))
 
model.add(layers.Dense(, activation='sigmoid'))
 

  最后,你需要选择损失函数和优化器。由于你面对的是一个二分类问题,网络输出是一个概率值(网络最后一层使用 sigmoid 激活函数,仅包含一个单元),那么最好使用 binary_crossentropy (二元交叉熵)损失。这并不是唯一可行的选择,比如你还可以使用 mean_squared_error(均方误差)。但对于输出概率值的模型,交叉熵(crossentropy)往往是最好的选择。交叉熵是来自于信息论领域的概念,用于衡量概率分布之间的距离,在这个例子中就是真实分布与预测值之间的距离。

  下面的步骤是用 rmsprop 优化器和 binary_crossentropy 损失函数来配置模型。注意,我们还在训练过程中监控精度。

代码语言:javascript复制
model.compile(optimizer='rmsprop',
 
loss='binary_crossentropy',
 
metrics=['accuracy'])
 

  上述代码将优化器、损失函数和指标作为字符串传入,这是因为 rmsprop、binary_ crossentropy 和 accuracy 都是 Keras 内置的一部分。有时你可能希望配置自定义优化器的 参数,或者传入自定义的损失函数或指标函数。前者可通过向 optimizer 参数传入一个优化器类实例来实现,如代码所示:

代码语言:javascript复制
from keras import optimizers
 
model.compile(optimizer=optimizers.RMSprop(lr=0.001),
 
loss='binary_crossentropy',
 
metrics=['accuracy'])
 

验证你的方法

  为了在训练过程中监控模型在前所未见的数据上的精度,你需要将原始训练数据留出 10 000个样本作为验证集。

代码语言:javascript复制
x_val = x_train[:]
 
partial_x_train = x_train[:]
 
y_val = y_train[:]
 
partial_y_train = y_train[:]
 

  现在使用 512 个样本组成的小批量,将模型训练 20 个轮次(即对 x_train 和 y_train 两 个张量中的所有样本进行 20 次迭代)。与此同时,你还要监控在留出的 10 000 个样本上的损失和精度。你可以通过将验证数据传入 validation_data 参数来完成。

代码语言:javascript复制
history = model.fit(partial_x_train,
 
partial_y_train,
 
epochs=,
 
batch_size=,
 
validation_data=(x_val, y_val))
 

结果如下图:

  调用 model.fit() 返回了一个 History 对象。这个对象有一个成员 history,它是一个字典,包含训练过程中的所有数据。我们来看一下。

代码语言:javascript复制
history_dict = history.history
 
history_dict.keys()
 
输出为:
 
dict_keys(['val_loss', 'val_binary_accuracy', 'loss', 'binary_accuracy'])
 

  字典中包含 4 个条目,对应训练过程和验证过程中监控的指标。在下面两个代码清单中, 我们将使用 Matplotlib 在同一张图上绘制训练损失和验证损失,以及训练精度和验证精度)。

代码语言:javascript复制
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(, len(acc)   )
 
# "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()
 

结果如下:

代码语言:javascript复制
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 轮,然后在测试数据上评估模型。

代码语言:javascript复制
model = models.Sequential()
 
model.add(layers.Dense(, activation='relu', input_shape=(,)))
 
model.add(layers.Dense(, activation='relu'))
 
model.add(layers.Dense(, activation='sigmoid'))
 
model.compile(optimizer='rmsprop',
 
loss='binary_crossentropy',
 
metrics=['accuracy'])
 
model.fit(x_train, y_train, epochs=, batch_size=)
 
results = model.evaluate(x_test, y_test)
 

迭代结果如下:

代码语言:javascript复制
print(results)
 
输出为:
 
[0.32315461338043211, 0.87348000000000003]
 

  这种相当简单的方法得到了 88% 的精度。

使用训练好的网络在新数据上生成预测结果

  训练好网络之后,你希望将其用于实践。你可以用 predict 方法来得到评论为正面的可能性大小。

代码语言:javascript复制
model.predict(x_test)
 
输出为:
 
array([[ 0.14026152],
 
[ 0.99970287],
 
[ 0.29552525],
 
...,
 
[ 0.07234977],
 
[ 0.04342838],
 
[ 0.48153383]], dtype=float32)
 

  如你所见,网络对某些样本的结果非常确信(大于等于 0.99,或小于等于 0.01),但对其他结果却不那么确信(0.6 或 0.4)。

进一步改进

  通过以下实验,你可以确信前面选择的网络架构是非常合理的,虽然仍有改进的空间。

前面使用了两个隐藏层。你可以尝试使用一个或三个隐藏层,然后观察对验证精度和测试精度的影响。

  • 尝试使用更多或更少的隐藏单元,比如 32 个、64 个等。
  • 尝试使用 mse 损失函数代替 binary_crossentropy。
  • 尝试使用 tanh 激活(这种激活在神经网络早期非常流行)代替 relu。

  这些实验将有助于说服您,我们所做的架构选择都是相当合理的,尽管它们仍然可以改进!

0 人点赞