机器学习tensorflow, keras之影评文本分类

前言

这篇文章将会介绍文本形式的影评分类,分为“正面”或“负面”。

这是一个二元分类,也是一种重要且广泛适用的机器学习问题。

我们将使用IMDB数据集,其中包含来自 IMDB 的 50000 条影评文本。

IMDB 是一个电影评价网站,类似于国内的豆瓣。

我们将这些影评拆分为训练集(25000 条影评)和测试集(25000 条影评)。训练集和测试集数量相等。

我们将继续上一篇文章一样,使用 Keras high level API

导入相关库

import tensorflow as tf
from tensorflow import keras

import numpy as np

print(tf.__version__)
print(keras.__version__)
1.12.0
2.1.6-tf

下载 IMDB 数据集

TensorFlow 中包含 IMDB 数据集。我们已对该数据集进行了预处理,将影评(字词序列)转换为整数序列,其中每个整数表示字典中的一个特定字词。

以下代码会将 IMDB 数据集下载到您的计算机上(如果您已下载该数据集,则会使用缓存副本):

imdb = keras.datasets.imdb
(train_data, train_labels), (test_data, test_labels) = imdb.load_data(num_words=10000)

如果是首次运行这个代码:

Downloading data from https://storage.googleapis.com/tensorflow/tf-keras-datasets/imdb.npz
17465344/17464789 [==============================] - 0s 0us/step

参数 num_words=10000 会保留训练数据中出现频次在前 10000 位的字词。为确保数据规模处于可管理的水平,生僻字词将被舍弃。

探索数据

我们花点时间来了解一下数据的格式。

该数据集已经过预处理:每个样本都是一个整数数组,表示影评中的字词。

每个标签都是整数值 0 或 1,其中 0 表示负面影评,1 表示正面影评。

print("Training entries: {}, labels: {}".format(len(train_data), len(train_labels)))
Training entries: 25000, labels: 25000

影评文本已转换为整数,其中每个整数都表示字典中的一个特定字词。

第一条影评如下所示:

print(train_data[0])
[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]

影评的长度可能会有所不同。以下代码显示了第一条和第二条影评中的字词数。

由于神经网络的输入必须具有相同长度,因此我们稍后需要解决此问题。

len(train_data[0]), len(train_data[1])
(218, 189)

将整数转换回字词

在以下代码中,我们将创建一个辅助函数来查询包含整数到字符串映射的字典对象:

# 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["<PAD>"] = 0
word_index["<START>"] = 1
word_index["<UNK>"] = 2  # unknown
word_index["<UNUSED>"] = 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])

这里PAD就是padding,填充的意思,这段代码主要的作用是,改造imdb数据库的word_index,将其中的元素向后移3位,设计几个标志位:<PAD>, <START>, <UNK>, <UNUSED>

如果是初次执行此代码,会有以下输出:

Downloading data from https://storage.googleapis.com/tensorflow/tf-keras-datasets/imdb_word_index.json
1646592/1641221 [==============================] - 0s 0us/step

现在,我们可以使用 decode_review 函数显示第一条影评的文本:

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"

构建模型

神经网络通过堆叠层创建而成,这需要做出两个架构方面的主要决策:

  • 要在模型中使用多少个层?
  • 要针对每个层使用多少个隐藏单元?

在本示例中,输入数据由字词-索引数组构成。要预测的标签是 0 或 1。接下来,我们为此问题构建一个模型:

# input shape is the vocabulary count used for the movie reviews (10,000 words)
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()

keras的神经网络层如下:

_________________________________________________________________
Layer (type)                 Output Shape              Param #
=================================================================
embedding (Embedding)        (None, None, 16)          160000
_________________________________________________________________
global_average_pooling1d (Gl (None, 16)                0
_________________________________________________________________
dense (Dense)                (None, 16)                272
_________________________________________________________________
dense_1 (Dense)              (None, 1)                 17
=================================================================
Total params: 160,289
Trainable params: 160,289
Non-trainable params: 0
_________________________________________________________________

按顺序堆叠各个层以构建分类器:

  1. 第一层是 Embedding 层。该层会在整数编码的词汇表中查找每个字词-索引的嵌入向量。模型在接受训练时会学习这些向量。这些向量会向输出数组添加一个维度。生成的维度为:(batch, sequence, embedding)
  2. 接下来,一个 GlobalAveragePooling1D 层通过对序列维度求平均值,针对每个样本返回一个长度固定的输出向量。这样,模型便能够以尽可能简单的方式处理各种长度的输入。
  3. 该长度固定的输出向量会传入一个全连接 (Dense) 层(包含 16 个隐藏单元)。
  4. 最后一层与单个输出节点密集连接。应用 sigmoid 激活函数后,结果是介于 0 到 1 之间的浮点值,表示概率或置信水平。

隐藏单元

上述模型在输入和输出之间有两个中间层(也称为“隐藏”层)。输出(单元、节点或神经元)的数量是相应层的表示法空间的维度。换句话说,该数值表示学习内部表示法时网络所允许的自由度。

如果模型具有更多隐藏单元(更高维度的表示空间)和/或更多层,则说明网络可以学习更复杂的表示法。不过,这会使网络耗费更多计算资源,并且可能导致学习不必要的模式(可以优化在训练数据上的表现,但不会优化在测试数据上的表现)。这称为过拟合,我们稍后会加以探讨。

损失函数和优化器

模型在训练时需要一个损失函数和一个优化器。由于这是一个二元分类问题且模型会输出一个概率(应用 S 型激活函数的单个单元层),因此我们将使用 binary_crossentropy 损失函数。

该函数并不是唯一的损失函数,例如,您可以选择 mean_squared_error。但一般来说,binary_crossentropy 更适合处理概率问题,它可测量概率分布之间的“差距”,在本例中则为实际分布和预测之间的“差距”。

稍后,在探索回归问题(比如预测房价)时,我们将了解如何使用另一个称为均方误差的损失函数。

现在,配置模型以使用优化器和损失函数:

model.compile(optimizer=tf.train.AdamOptimizer(),
              loss='binary_crossentropy',
              metrics=['accuracy'])

创建验证集

在训练时,我们需要检查模型处理从未见过的数据的准确率。我们从原始训练数据中分离出 10000 个样本,创建一个验证集。(为什么现在不使用测试集?我们的目标是仅使用训练数据开发和调整模型,然后仅使用一次测试数据评估准确率。)

x_val = train_data[:10000]
partial_x_train = train_data[10000:]
y_val = train_labels[:10000]
partial_y_train = train_labels[10000:]

训练模型

用有 512 个样本的小批次训练模型 40 个周期。这将对 x_trainy_train 张量中的所有样本进行 40 次迭代。在训练期间,监控模型在验证集的 10000 个样本上的损失和准确率:

history = model.fit(partial_x_train,
                    partial_y_train,
                    epochs=40,
                    batch_size=512,
                    validation_data=(x_val, y_val),
                    verbose=1)
Train on 15000 samples, validate on 10000 samples
Epoch 1/40
15000/15000 [==============================] - 1s 53us/step - loss: 0.6916 - acc: 0.5907 - val_loss: 0.6894 - val_acc: 0.6462
Epoch 2/40
15000/15000 [==============================] - 1s 41us/step - loss: 0.6849 - acc: 0.7349 - val_loss: 0.6802 - val_acc: 0.7464
Epoch 3/40
15000/15000 [==============================] - 1s 40us/step - loss: 0.6711 - acc: 0.7663 - val_loss: 0.6632 - val_acc: 0.7627
Epoch 4/40
15000/15000 [==============================] - 1s 40us/step - loss: 0.6472 - acc: 0.7761 - val_loss: 0.6373 - val_acc: 0.7676
Epoch 5/40
15000/15000 [==============================] - 1s 39us/step - loss: 0.6133 - acc: 0.7973 - val_loss: 0.6016 - val_acc: 0.7887
Epoch 6/40
15000/15000 [==============================] - 1s 38us/step - loss: 0.5716 - acc: 0.8146 - val_loss: 0.5617 - val_acc: 0.8029
Epoch 7/40
15000/15000 [==============================] - 1s 38us/step - loss: 0.5259 - acc: 0.8329 - val_loss: 0.5202 - val_acc: 0.8204
Epoch 8/40
15000/15000 [==============================] - 1s 37us/step - loss: 0.4804 - acc: 0.8466 - val_loss: 0.4805 - val_acc: 0.8352
Epoch 9/40
15000/15000 [==============================] - 1s 40us/step - loss: 0.4387 - acc: 0.8589 - val_loss: 0.4454 - val_acc: 0.8442
Epoch 10/40
15000/15000 [==============================] - 1s 40us/step - loss: 0.4013 - acc: 0.8733 - val_loss: 0.4158 - val_acc: 0.8495
Epoch 11/40
15000/15000 [==============================] - 1s 41us/step - loss: 0.3697 - acc: 0.8810 - val_loss: 0.3934 - val_acc: 0.8542
Epoch 12/40
15000/15000 [==============================] - 1s 39us/step - loss: 0.3432 - acc: 0.8869 - val_loss: 0.3712 - val_acc: 0.8627
Epoch 13/40
15000/15000 [==============================] - 1s 39us/step - loss: 0.3192 - acc: 0.8941 - val_loss: 0.3556 - val_acc: 0.8679
Epoch 14/40
15000/15000 [==============================] - 1s 40us/step - loss: 0.2993 - acc: 0.8997 - val_loss: 0.3416 - val_acc: 0.8721
Epoch 15/40
15000/15000 [==============================] - 1s 38us/step - loss: 0.2820 - acc: 0.9046 - val_loss: 0.3307 - val_acc: 0.8736
Epoch 16/40
15000/15000 [==============================] - 1s 39us/step - loss: 0.2672 - acc: 0.9076 - val_loss: 0.3218 - val_acc: 0.8753
Epoch 17/40
15000/15000 [==============================] - 1s 39us/step - loss: 0.2528 - acc: 0.9143 - val_loss: 0.3143 - val_acc: 0.8774
Epoch 18/40
15000/15000 [==============================] - 1s 40us/step - loss: 0.2404 - acc: 0.9192 - val_loss: 0.3080 - val_acc: 0.8817
Epoch 19/40
15000/15000 [==============================] - 1s 41us/step - loss: 0.2289 - acc: 0.9227 - val_loss: 0.3028 - val_acc: 0.8816
Epoch 20/40
15000/15000 [==============================] - 1s 37us/step - loss: 0.2189 - acc: 0.9249 - val_loss: 0.2987 - val_acc: 0.8829
Epoch 21/40
15000/15000 [==============================] - 1s 39us/step - loss: 0.2089 - acc: 0.9293 - val_loss: 0.2951 - val_acc: 0.8834
Epoch 22/40
15000/15000 [==============================] - 1s 41us/step - loss: 0.1999 - acc: 0.9323 - val_loss: 0.2925 - val_acc: 0.8833
Epoch 23/40
15000/15000 [==============================] - 1s 41us/step - loss: 0.1916 - acc: 0.9351 - val_loss: 0.2907 - val_acc: 0.8840
Epoch 24/40
15000/15000 [==============================] - 1s 40us/step - loss: 0.1834 - acc: 0.9402 - val_loss: 0.2882 - val_acc: 0.8849
Epoch 25/40
15000/15000 [==============================] - 1s 41us/step - loss: 0.1761 - acc: 0.9431 - val_loss: 0.2868 - val_acc: 0.8853
Epoch 26/40
15000/15000 [==============================] - 1s 40us/step - loss: 0.1688 - acc: 0.9459 - val_loss: 0.2864 - val_acc: 0.8857
Epoch 27/40
15000/15000 [==============================] - 1s 38us/step - loss: 0.1628 - acc: 0.9490 - val_loss: 0.2861 - val_acc: 0.8859
Epoch 28/40
15000/15000 [==============================] - 1s 42us/step - loss: 0.1562 - acc: 0.9520 - val_loss: 0.2854 - val_acc: 0.8866
Epoch 29/40
15000/15000 [==============================] - 1s 39us/step - loss: 0.1503 - acc: 0.9536 - val_loss: 0.2854 - val_acc: 0.8866
Epoch 30/40
15000/15000 [==============================] - 1s 40us/step - loss: 0.1451 - acc: 0.9556 - val_loss: 0.2861 - val_acc: 0.8864
Epoch 31/40
15000/15000 [==============================] - 1s 41us/step - loss: 0.1390 - acc: 0.9585 - val_loss: 0.2870 - val_acc: 0.8872
Epoch 32/40
15000/15000 [==============================] - 1s 41us/step - loss: 0.1342 - acc: 0.9604 - val_loss: 0.2883 - val_acc: 0.8867
Epoch 33/40
15000/15000 [==============================] - 1s 40us/step - loss: 0.1287 - acc: 0.9631 - val_loss: 0.2896 - val_acc: 0.8869
Epoch 34/40
15000/15000 [==============================] - 1s 40us/step - loss: 0.1242 - acc: 0.9647 - val_loss: 0.2911 - val_acc: 0.8856
Epoch 35/40
15000/15000 [==============================] - 1s 41us/step - loss: 0.1200 - acc: 0.9656 - val_loss: 0.2925 - val_acc: 0.8873
Epoch 36/40
15000/15000 [==============================] - 1s 39us/step - loss: 0.1150 - acc: 0.9681 - val_loss: 0.2949 - val_acc: 0.8851
Epoch 37/40
15000/15000 [==============================] - 1s 40us/step - loss: 0.1110 - acc: 0.9694 - val_loss: 0.2972 - val_acc: 0.8852
Epoch 38/40
15000/15000 [==============================] - 1s 40us/step - loss: 0.1075 - acc: 0.9697 - val_loss: 0.2993 - val_acc: 0.8840
Epoch 39/40
15000/15000 [==============================] - 1s 40us/step - loss: 0.1030 - acc: 0.9715 - val_loss: 0.3016 - val_acc: 0.8826
Epoch 40/40
15000/15000 [==============================] - 1s 40us/step - loss: 0.0991 - acc: 0.9731 - val_loss: 0.3042 - val_acc: 0.8842

评估模型

我们来看看模型的表现如何。模型会返回两个值:损失(表示误差的数字,越低越好)和准确率。

results = model.evaluate(test_data, test_labels)
print(results)
25000/25000 [==============================] - 1s 35us/step
[0.32395469411849975, 0.87264]

使用这种相当简单的方法可实现约 87% 的准确率。如果采用更高级的方法,模型的准确率应该会接近 95%。

创建准确率和损失随时间变化的图

model.fit() 返回一个 History 对象,该对象包含一个字典,其中包括训练期间发生的所有情况:

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

一共有 4 个条目:每个条目对应训练和验证期间的一个受监控指标。我们可以使用这些指标绘制训练损失与验证损失图表以进行对比,并绘制训练准确率与验证准确率图表:

import matplotlib.pyplot as plt

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)

# "bo" is for "blue dot"
plt.plot(epochs, loss, 'bo', label='Training loss')
# b is for "solid blue line"
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['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('Accuracy')
plt.legend()

plt.show()

在该图表中,圆点表示训练损失和准确率,实线表示验证损失和准确率。

训练损失随着周期数的增加而降低,训练准确率随着周期数的增加而提高。在使用梯度下降法优化模型时,这属于正常现象 - 该方法应在每次迭代时尽可能降低目标值。

验证损失和准确率的变化情况并非如此,它们似乎在大约 20 个周期后达到峰值。这是一种过拟合现象:模型在训练数据上的表现要优于在从未见过的数据上的表现。在此之后,模型会过度优化和学习特定于训练数据的表示法,而无法泛化到测试数据。

对于这种特殊情况,我们可以在大约 20 个周期后停止训练,防止出现过拟合。

参考资料


   转载规则


《机器学习tensorflow, keras之影评文本分类》 Harbor Zeng 采用 知识共享署名 4.0 国际许可协议 进行许可。
 上一篇
机器学习tensorflow-keras之预测汽车燃油效率-回归问题 机器学习tensorflow-keras之预测汽车燃油效率-回归问题
前言回归问题的目标是预测一系列连续值的输出,比如价格和概率。和分类问题不一样,分类问题是从一堆中选出一类。(比方说从一堆照片中选出带有苹果的照片等) 这篇文章使用 Auto MPG 数据集来构建模型,预测上世纪70年到80年代的汽车燃油效率。这些数据包含:气缸、排量、马力和重量等属性。 我们需要使用seaborn来绘制一些图像: $ pip install seaborn 首次运行上述代码时:
2019-02-13
下一篇 
机器学习tensorflow, keras之训练首个神经网络:对服饰图像基本分类 机器学习tensorflow, keras之训练首个神经网络:对服饰图像基本分类
本文会训练一个对服饰(例如运动鞋和衬衫)图像进行分类的神经网络模型。 使用tf.keras,它是一种用于在TensorFlow中构建和训练模型的High level API。 导入相关库# TensorFlow and tf.keras import tensorflow as tf from tensorflow import keras # Helper libraries import m
2019-02-03
  目录