人工智能python实现-IMDB数据集

3.4 电影评论分类:二分类问题
3.4.1 IMDB数据集
3.4.2 准备数据
3.4.3 构建网络
3.4.4 验证你的方法
3.4.5 使用训练好的网络在新数据上生成预测结果 ….
3.4.6 进一步的实验

3.4 电影评论分类:二分类问题

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

3.4.1 IMDB数据集

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

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

与 MNIST数据集一样,IMDB数据集也内置于   Keras库。它已经过预处理:评论(单词序列)已经被转换为整数序列,其中每个整数代表字典中的某个单词。

下列代码将会加载 IMDB数据集(第一次运行时会下载大约  80MB的数据)。

代码清单 3-1 加载 IMDB数据集

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)。

>>> train_data[0]
[1, 14, 22, 16, ... 178, 32]

>>> train_labels[0]
1

由于限定为前 10 000个最常见的单词,单词索引都不会超过  10 000。

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

下面这段代码很有意思,你可以将某条评论迅速解码为英文单词。

3.4.2 准备数据

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

  • 填充列表,使其具有相同的长度,再将列表转换成形状为(samples,  word_indices)的整数张量,然后网络第一层使用能处理这种整数张量的层(即  Embedding层,本书后面会详细介绍)。
  • 对列表进行  one-hot编码,将其转换为  0和  1组成的向量。举个例子,序列[3,  5]将会被转换为 10  000维向量,只有索引为  3和  5的元素是   1,其余元素都是 0。然后网络第一层可以用Dense层,它能够处理浮点数向量数据。

下面我们采用后一种方法将数据向量化。为了加深理解,你可以手动实现这一方法,如下所示。

代码清单 3-2 将整数序列编码为二进制矩阵

3.4.3 构建网络

输入数据是向量,而标签是标量(    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层的堆叠,你需要确定以下两个关键架构:

  • 网络有多少层;
  • 每层有多少个隐藏单元。

第 4章中的原则将会指导你对上述问题做出选择。现在你只需要相信我选择的下列架构:

  • 两个中间层,每层都有  16个隐藏单元;
  • 第三层输出一个标量,预测当前评论的情感。

中间层使用relu作为激活函数,最后一层使用   sigmoid激活以输出一个  0~1范围内的概率值(表示样本的目标值等于  1的可能性,即评论为正面的可能性)。relu(rectified  linear  unit,整流线性单元)函数将所有负值归零(见图   3-4),而 sigmoid函数则将任意值“压缩”到 [0,1]区间内(见图  3-5),其输出值可以看作概率值。

图 3-4 整流线性单元函数

图 3-5 sigmoid函数

图 3-6显示了网络的结构。代码清单 3-3是其  Keras实现,与前面见过的  MNIST例子类似。

图 3-6 三层网络

代码清单 3-3 模型定义

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层将只包含两个线性运算——点积和加法:

output = dot(W, input) + b

这样Dense层就只能学习输入数据的线性变换(仿射变换):该层的假设空间是从输入数据到 16位空间所有可能的线性变换集合。这种假设空间非常有限,无法利用多个表示层的优势,因为多个线性层堆叠实现的仍是线性运算,添加层数并不会扩展假设空间。

为了得到更丰富的假设空间,从而充分利用多层表示的优势,你需要添加非线性或激活函数。relu是深度学习中最常用的激活函数,但还有许多其他函数可选,它们都有类似的奇怪名称,比如prelu、elu等。

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

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

代码清单 3-4 编译模型

model.compile(optimizer='rmsprop',
        loss='binary_crossentropy',
        metrics=['accuracy'])

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

代码清单 3-5 配置优化器

from keras import optimizers

model.compile(optimizer=optimizers.RMSprop(lr=0.001),
        loss='binary_crossentropy',
        metrics=['accuracy'])

代码清单 3-6 使用自定义的损失和指标

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个样本作为验证集。

代码清单 3-7 留出验证集

x_val = x_train[:10000]
partial_x_train = x_train[10000:]

y_val = y_train[:10000]
partial_y_train = y_train[10000:]

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

代码清单 3-8 训练模型

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上运行,每轮的时间不到   2秒,训练过程将在  20秒内结束。每轮结束时会有短暂的停顿,因为模型要计算在验证集的 10 000个样本上的损失和精度。

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

>>> history_dict = history.history
>>> history_dict.keys()
dict_keys(['val_acc', 'acc', 'val_loss', 'loss'])

字典中包含 4个条目,对应训练过程和验证过程中监控的指标。在下面两个代码清单中,我们将使用 Matplotlib在同一张图上绘制训练损失和验证损失(见图    3-7),以及训练精度和验证精度(见图 3-8)。请注意,由于网络的随机初始化不同,你得到的结果可能会略有不同。

代码清单 3-9 绘制训练损失和验证损失

图 3-7 训练损失和验证损失

代码清单 3-10 绘制训练精度和验证精度

图 3-8 训练精度和验证精度

如你所见,训练损失每轮都在降低,训练精度每轮都在提升。这就是梯度下降优化的预期结果——你想要最小化的量随着每次迭代越来越小。但验证损失和验证精度并非如此:它们似乎在第四轮达到最佳值。这就是我们之前警告过的一种情况:模型在训练数据上的表现越来越好,但在前所未见的数据上不一定表现得越来越好。准确地说,你看到的是过拟合(overfit):在第二轮之后,你对训练数据过度优化,最终学到的表示仅针对于训练数据,无法泛化到训练集之外的数据。

在这种情况下,为了防止过拟合,你可以在  3轮之后停止训练。通常来说,你可以使用许多方法来降低过拟合,我们将在第 4章中详细介绍。

我们从头开始训练一个新的网络,训练 4轮,然后在测试数据上评估模型。

代码清单 3-11 从头开始重新训练一个模型

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)

最终结果如下所示。

>>> results
[0.2929924130630493, 0.88327999999999995]

这种相当简单的方法得到了 88%的精度。利用最先进的方法,你应该能够得到接近 95%的精度。

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

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

>>> 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 进一步的实验

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

  • 前面使用了两个隐藏层。你可以尝试使用一个或三个隐藏层,然后观察对验证精度和测试精度的影响。
  • 尝试使用更多或更少的隐藏单元,比如  32个、64个等。
  • 尝试使用mse损失函数代替binary_crossentropy。
  • 尝试使用tanh激活(这种激活在神经网络早期非常流行)代替relu。

3.4.7 小结

下面是你应该从这个例子中学到的要点。

  • 通常需要对原始数据进行大量预处理,以便将其转换为张量输入到神经网络中。单词序列可以编码为二进制向量,但也有其他编码方式。
  • 带有relu激活的Dense层堆叠,可以解决很多种问题(包括情感分类),你可能会经常用到这种模型。
  • 对于二分类问题(两个输出类别),网络的最后一层应该是只有一个单元并使用sigmoid激活的Dense层,网络输出应该是   0~1范围内的标量,表示概率值。
  • 对于二分类问题的  sigmoid标量输出,你应该使用binary_crossentropy损失函数。
  • 无论你的问题是什么,rmsprop优化器通常都是足够好的选择。这一点你无须担心。
  • 随着神经网络在训练数据上的表现越来越好,模型最终会过拟合,并在前所未见的数据上得到越来越差的结果。一定要一直监控模型在训练集之外的数据上的性能。

作者:

喜欢围棋和编程。

 
发布于 分类 编程标签

发表评论

邮箱地址不会被公开。