使用 RNN 进行文本分类

在 TensorFlow.org 上查看 在 Google Colab 中运行 在 GitHub 上查看源码 下载笔记本

本文本分类教程在 IMDB 大型电影评论数据集上训练了一个循环神经网络 (RNN),用于情感分析。

设置

import numpy as np

import tensorflow_datasets as tfds
import tensorflow as tf

tfds.disable_progress_bar()

导入 matplotlib 并创建一个用于绘制图形的辅助函数

import matplotlib.pyplot as plt


def plot_graphs(history, metric):
  plt.plot(history.history[metric])
  plt.plot(history.history['val_'+metric], '')
  plt.xlabel("Epochs")
  plt.ylabel(metric)
  plt.legend([metric, 'val_'+metric])

设置输入流水线

IMDB 大型电影评论数据集是一个二分类数据集——所有的评论要么是正面情感,要么是负面情感。

使用 TFDS 下载数据集。有关如何手动加载此类数据的详细信息,请参阅加载文本教程

dataset, info = tfds.load('imdb_reviews', with_info=True,
                          as_supervised=True)
train_dataset, test_dataset = dataset['train'], dataset['test']

train_dataset.element_spec

最初,这会返回一个(文本,标签对)的数据集

for example, label in train_dataset.take(1):
  print('text: ', example.numpy())
  print('label: ', label.numpy())

接下来,对训练数据进行混洗,并创建这些 (文本, 标签) 对的批次

BUFFER_SIZE = 10000
BATCH_SIZE = 64
train_dataset = train_dataset.shuffle(BUFFER_SIZE).batch(BATCH_SIZE).prefetch(tf.data.AUTOTUNE)
test_dataset = test_dataset.batch(BATCH_SIZE).prefetch(tf.data.AUTOTUNE)
for example, label in train_dataset.take(1):
  print('texts: ', example.numpy()[:3])
  print()
  print('labels: ', label.numpy()[:3])

创建文本编码器

tfds 加载的原始文本在用于模型之前需要进行处理。处理训练文本的最简单方法是使用 TextVectorization 层。该层具有许多功能,但本教程仅使用默认行为。

创建该层,并将数据集的文本传递给该层的 .adapt 方法

VOCAB_SIZE = 1000
encoder = tf.keras.layers.TextVectorization(
    max_tokens=VOCAB_SIZE)
encoder.adapt(train_dataset.map(lambda text, label: text))

.adapt 方法会设置层的词汇表。这是前 20 个 token。在填充(padding)和未知 token 之后,它们按频率排序

vocab = np.array(encoder.get_vocabulary())
vocab[:20]

词汇表设置完成后,该层可以将文本编码为索引。索引张量会针对批次中最长的序列进行 0 填充(除非你设置了固定的 output_sequence_length

encoded_example = encoder(example)[:3].numpy()
encoded_example

使用默认设置时,该过程并非完全可逆。主要原因有三个

  1. preprocessing.TextVectorizationstandardize 参数的默认值为 "lower_and_strip_punctuation"(转换为小写并去除标点符号)。
  2. 有限的词汇量和缺乏基于字符的备选方案导致了一些未知 token 的产生。
for n in range(3):
  print("Original: ", example[n].numpy())
  print("Round-trip: ", " ".join(vocab[encoded_example[n]]))
  print()

创建模型

A drawing of the information flow in the model

上图是该模型的示意图。

  1. 此模型可以构建为 tf.keras.Sequential

  2. 第一层是 encoder,它将文本转换为 token 索引序列。

  3. 编码器之后是一个嵌入层(Embedding layer)。嵌入层为每个单词存储一个向量。当被调用时,它将单词索引序列转换为向量序列。这些向量是可训练的。经过训练(在足够的数据上)后,含义相似的单词通常具有相似的向量。

    这种索引查找比通过 tf.keras.layers.Dense 层传递独热编码(one-hot encoded)向量的等效操作要高效得多。

  4. 循环神经网络 (RNN) 通过迭代元素来处理序列输入。RNN 将一个时间步的输出传递给下一个时间步作为输入。

    tf.keras.layers.Bidirectional 封装器也可以与 RNN 层一起使用。它将输入通过 RNN 层向前和向后传播,然后拼接最终输出。

    • 双向 RNN 的主要优点是,输入开头的信号不需要经过每个时间步的处理就能影响输出。

    • 双向 RNN 的主要缺点是,当单词被添加到末尾时,你无法高效地流式传输预测结果。

  5. 在 RNN 将序列转换为单个向量后,两个 layers.Dense 层进行最后的处理,并从这种向量表示转换为作为分类输出的单个 logit。

实现该功能的代码如下

model = tf.keras.Sequential([
    encoder,
    tf.keras.layers.Embedding(
        input_dim=len(encoder.get_vocabulary()),
        output_dim=64,
        # Use masking to handle the variable sequence lengths
        mask_zero=True),
    tf.keras.layers.Bidirectional(tf.keras.layers.LSTM(64)),
    tf.keras.layers.Dense(64, activation='relu'),
    tf.keras.layers.Dense(1)
])

请注意,此处使用了 Keras 序贯模型(sequential model),因为模型中的所有层都只有单个输入并产生单个输出。如果你想使用有状态(stateful)的 RNN 层,你可能需要使用 Keras 函数式 API 或模型子类化来构建模型,以便你可以检索和重用 RNN 层状态。请查阅 Keras RNN 指南了解更多详细信息。

嵌入层使用掩码(masking)来处理不同的序列长度。Embedding 之后的所有层都支持掩码

print([layer.supports_masking for layer in model.layers])

为了确认其按预期工作,请对一个句子进行两次评估。首先,单独评估,这样就没有需要掩盖的填充

# predict on a sample text without padding.

sample_text = ('The movie was cool. The animation and the graphics '
               'were out of this world. I would recommend this movie.')
predictions = model.predict(np.array([sample_text]))
print(predictions[0])

现在,在一个较长的句子批次中再次评估它。结果应该是相同的

# predict on a sample text with padding

padding = "the " * 2000
predictions = model.predict(np.array([sample_text, padding]))
print(predictions[0])

编译 Keras 模型以配置训练过程

model.compile(loss=tf.keras.losses.BinaryCrossentropy(from_logits=True),
              optimizer=tf.keras.optimizers.Adam(1e-4),
              metrics=['accuracy'])

训练模型

history = model.fit(train_dataset, epochs=10,
                    validation_data=test_dataset,
                    validation_steps=30)
test_loss, test_acc = model.evaluate(test_dataset)

print('Test Loss:', test_loss)
print('Test Accuracy:', test_acc)
plt.figure(figsize=(16, 8))
plt.subplot(1, 2, 1)
plot_graphs(history, 'accuracy')
plt.ylim(None, 1)
plt.subplot(1, 2, 2)
plot_graphs(history, 'loss')
plt.ylim(0, None)

对一个新句子运行预测

如果预测值 >= 0.0,则为正面,否则为负面。

sample_text = ('The movie was cool. The animation and the graphics '
               'were out of this world. I would recommend this movie.')
predictions = model.predict(np.array([sample_text]))

堆叠两个或多个 LSTM 层

Keras 循环层有两种可用模式,由 return_sequences 构造函数参数控制

  • 如果为 False,则仅返回每个输入序列的最后一个输出(形状为 (batch_size, output_features) 的二维张量)。这是默认设置,在之前的模型中使用。

  • 如果为 True,则返回每个时间步的连续输出的完整序列(形状为 (batch_size, timesteps, output_features) 的三维张量)。

以下是 return_sequences=True 时信息流的样子

layered_bidirectional

使用带有 return_sequences=TrueRNN 的有趣之处在于,输出仍然具有 3 个轴(与输入一样),因此它可以传递给另一个 RNN 层,如下所示

model = tf.keras.Sequential([
    encoder,
    tf.keras.layers.Embedding(len(encoder.get_vocabulary()), 64, mask_zero=True),
    tf.keras.layers.Bidirectional(tf.keras.layers.LSTM(64,  return_sequences=True)),
    tf.keras.layers.Bidirectional(tf.keras.layers.LSTM(32)),
    tf.keras.layers.Dense(64, activation='relu'),
    tf.keras.layers.Dropout(0.5),
    tf.keras.layers.Dense(1)
])
model.compile(loss=tf.keras.losses.BinaryCrossentropy(from_logits=True),
              optimizer=tf.keras.optimizers.Adam(1e-4),
              metrics=['accuracy'])
history = model.fit(train_dataset, epochs=10,
                    validation_data=test_dataset,
                    validation_steps=30)
test_loss, test_acc = model.evaluate(test_dataset)

print('Test Loss:', test_loss)
print('Test Accuracy:', test_acc)
# predict on a sample text without padding.

sample_text = ('The movie was not good. The animation and the graphics '
               'were terrible. I would not recommend this movie.')
predictions = model.predict(np.array([sample_text]))
print(predictions)
plt.figure(figsize=(16, 6))
plt.subplot(1, 2, 1)
plot_graphs(history, 'accuracy')
plt.subplot(1, 2, 2)
plot_graphs(history, 'loss')

查看其他现有的循环层,例如 GRU 层

如果你有兴趣构建自定义 RNN,请参阅 Keras RNN 指南