基本文本分类

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

本教程演示了从磁盘上存储的纯文本文件开始的文本分类。您将训练一个二元分类器,以对 IMDB 数据集执行情感分析。在笔记本的最后,有一个供您尝试的练习,您将在其中训练一个多类分类器来预测 Stack Overflow 上编程问题的标签。

import matplotlib.pyplot as plt
import os
import re
import shutil
import string
import tensorflow as tf

from tensorflow.keras import layers
from tensorflow.keras import losses
print(tf.__version__)

情感分析

本笔记本训练了一个情感分析模型,根据评论的文本将电影评论分类为正面负面。这是一个二元(或两类)分类的示例,这是一种重要且应用广泛的机器学习问题。

您将使用大型电影评论数据集,其中包含来自互联网电影数据库的 50,000 条电影评论的文本。这些评论被分成 25,000 条用于训练,25,000 条用于测试。训练集和测试集是平衡的,这意味着它们包含相同数量的正面和负面评论。

下载并探索 IMDB 数据集

让我们下载并解压缩数据集,然后探索目录结构。

url = "https://ai.stanford.edu/~amaas/data/sentiment/aclImdb_v1.tar.gz"

dataset = tf.keras.utils.get_file("aclImdb_v1", url,
                                    untar=True, cache_dir='.',
                                    cache_subdir='')

dataset_dir = os.path.join(os.path.dirname(dataset), 'aclImdb')
os.listdir(dataset_dir)
train_dir = os.path.join(dataset_dir, 'train')
os.listdir(train_dir)

The aclImdb/train/posaclImdb/train/neg 目录包含许多文本文件,每个文件都是一篇电影评论。让我们看看其中之一。

sample_file = os.path.join(train_dir, 'pos/1181_9.txt')
with open(sample_file) as f:
  print(f.read())

加载数据集

接下来,您将从磁盘加载数据并将其准备成适合训练的格式。为此,您将使用有用的 text_dataset_from_directory 实用程序,它期望目录结构如下。

main_directory/
...class_a/
......a_text_1.txt
......a_text_2.txt
...class_b/
......b_text_1.txt
......b_text_2.txt

要为二元分类准备数据集,您需要在磁盘上创建两个文件夹,分别对应于 class_aclass_b。这些将是正面和负面的电影评论,可以在 aclImdb/train/posaclImdb/train/neg 中找到。由于 IMDB 数据集包含其他文件夹,因此在使用此实用程序之前,您将删除它们。

remove_dir = os.path.join(train_dir, 'unsup')
shutil.rmtree(remove_dir)

接下来,您将使用 text_dataset_from_directory 实用程序来创建一个带标签的 tf.data.Datasettf.data 是一个功能强大的工具集,用于处理数据。

在运行机器学习实验时,最佳做法是将数据集分成三个部分:训练验证测试

IMDB 数据集已经分为训练集和测试集,但缺少验证集。让我们使用训练数据的 80:20 分割创建一个验证集,方法是使用下面的 validation_split 参数。

batch_size = 32
seed = 42

raw_train_ds = tf.keras.utils.text_dataset_from_directory(
    'aclImdb/train',
    batch_size=batch_size,
    validation_split=0.2,
    subset='training',
    seed=seed)

如您在上面看到的,训练文件夹中有 25,000 个示例,其中您将使用 80%(或 20,000 个)进行训练。正如您将在稍后看到的那样,您可以通过将数据集直接传递给 model.fit 来训练模型。如果您不熟悉 tf.data,您也可以遍历数据集并打印出一些示例,如下所示。

for text_batch, label_batch in raw_train_ds.take(1):
  for i in range(3):
    print("Review", text_batch.numpy()[i])
    print("Label", label_batch.numpy()[i])

请注意,评论包含原始文本(带标点符号和偶尔的 HTML 标签,例如 <br/>)。您将展示如何在下一节中处理这些问题。

标签是 0 或 1。要查看这些标签中哪些对应于正面和负面的电影评论,您可以检查数据集上的 class_names 属性。

print("Label 0 corresponds to", raw_train_ds.class_names[0])
print("Label 1 corresponds to", raw_train_ds.class_names[1])

接下来,您将创建一个验证集和测试集。您将使用训练集中剩余的 5,000 条评论进行验证。

raw_val_ds = tf.keras.utils.text_dataset_from_directory(
    'aclImdb/train',
    batch_size=batch_size,
    validation_split=0.2,
    subset='validation',
    seed=seed)
raw_test_ds = tf.keras.utils.text_dataset_from_directory(
    'aclImdb/test',
    batch_size=batch_size)

准备用于训练的数据集

接下来,您将使用有用的 tf.keras.layers.TextVectorization 层来标准化、标记化和向量化数据。

标准化是指预处理文本,通常是删除标点符号或 HTML 元素以简化数据集。标记化是指将字符串拆分为标记(例如,通过在空格处拆分来将句子拆分为单个单词)。向量化是指将标记转换为数字,以便可以将其馈送到神经网络。所有这些任务都可以通过此层来完成。

如您在上面看到的,评论包含各种 HTML 标签,例如 <br />。这些标签不会被 TextVectorization 层中的默认标准化器删除(默认情况下,它将文本转换为小写并删除标点符号,但不会删除 HTML)。您将编写一个自定义标准化函数来删除 HTML。

def custom_standardization(input_data):
  lowercase = tf.strings.lower(input_data)
  stripped_html = tf.strings.regex_replace(lowercase, '<br />', ' ')
  return tf.strings.regex_replace(stripped_html,
                                  '[%s]' % re.escape(string.punctuation),
                                  '')

接下来,您将创建一个 TextVectorization 层。您将使用此层来标准化、标记化和向量化我们的数据。您将 output_mode 设置为 int,以便为每个标记创建唯一的整数索引。

请注意,您正在使用默认的拆分函数,以及您上面定义的自定义标准化函数。您还将为模型定义一些常量,例如显式最大 sequence_length,这将导致层将序列填充或截断为正好 sequence_length 个值。

max_features = 10000
sequence_length = 250

vectorize_layer = layers.TextVectorization(
    standardize=custom_standardization,
    max_tokens=max_features,
    output_mode='int',
    output_sequence_length=sequence_length)

接下来,您将调用 adapt 来使预处理层的状态适应数据集。这将导致模型构建一个字符串到整数的索引。

# Make a text-only dataset (without labels), then call adapt
train_text = raw_train_ds.map(lambda x, y: x)
vectorize_layer.adapt(train_text)

让我们创建一个函数来查看使用此层预处理一些数据的結果。

def vectorize_text(text, label):
  text = tf.expand_dims(text, -1)
  return vectorize_layer(text), label
# retrieve a batch (of 32 reviews and labels) from the dataset
text_batch, label_batch = next(iter(raw_train_ds))
first_review, first_label = text_batch[0], label_batch[0]
print("Review", first_review)
print("Label", raw_train_ds.class_names[first_label])
print("Vectorized review", vectorize_text(first_review, first_label))

如您在上面看到的,每个标记都被替换为一个整数。您可以通过在层上调用 .get_vocabulary() 来查找每个整数对应的标记(字符串)。

print("1287 ---> ",vectorize_layer.get_vocabulary()[1287])
print(" 313 ---> ",vectorize_layer.get_vocabulary()[313])
print('Vocabulary size: {}'.format(len(vectorize_layer.get_vocabulary())))

您几乎准备好训练您的模型了。作为最后的预处理步骤,您将对训练集、验证集和测试集应用之前创建的 TextVectorization 层。

train_ds = raw_train_ds.map(vectorize_text)
val_ds = raw_val_ds.map(vectorize_text)
test_ds = raw_test_ds.map(vectorize_text)

配置数据集以提高性能

以下两种方法是您在加载数据时应该使用的重要方法,以确保 I/O 不会成为阻塞因素。

.cache() 将数据加载到磁盘后保留在内存中。这将确保数据集在训练模型时不会成为瓶颈。如果您的数据集太大而无法放入内存,您也可以使用此方法来创建一个高性能的磁盘缓存,它比许多小文件更有效地读取。

.prefetch() 在训练时重叠数据预处理和模型执行。

您可以在 数据性能指南 中了解更多关于这两种方法以及如何将数据缓存到磁盘的信息。

AUTOTUNE = tf.data.AUTOTUNE

train_ds = train_ds.cache().prefetch(buffer_size=AUTOTUNE)
val_ds = val_ds.cache().prefetch(buffer_size=AUTOTUNE)
test_ds = test_ds.cache().prefetch(buffer_size=AUTOTUNE)

创建模型

现在是创建神经网络的时候了

embedding_dim = 16
model = tf.keras.Sequential([
  layers.Embedding(max_features, embedding_dim),
  layers.Dropout(0.2),
  layers.GlobalAveragePooling1D(),
  layers.Dropout(0.2),
  layers.Dense(1, activation='sigmoid')])

model.summary()

这些层按顺序堆叠以构建分类器

  1. 第一层是 Embedding 层。此层接收整数编码的评论,并为每个单词索引查找一个嵌入向量。这些向量是在模型训练时学习的。这些向量向输出数组添加了一个维度。生成的维度是:(batch, sequence, embedding)。要了解更多关于嵌入的信息,请查看 词嵌入 教程。
  2. 接下来,GlobalAveragePooling1D 层通过对序列维度求平均值,为每个示例返回一个固定长度的输出向量。这允许模型以最简单的方式处理可变长度的输入。
  3. 最后一层是密集连接的,只有一个输出节点。

损失函数和优化器

模型需要一个损失函数和一个优化器进行训练。由于这是一个二元分类问题,并且模型输出一个概率(一个带有 sigmoid 激活函数的单单元层),因此您将使用 losses.BinaryCrossentropy 损失函数。

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

model.compile(loss=losses.BinaryCrossentropy(),
              optimizer='adam',
              metrics=[tf.metrics.BinaryAccuracy(threshold=0.5)])

训练模型

您将通过将 dataset 对象传递给 fit 方法来训练模型。

epochs = 10
history = model.fit(
    train_ds,
    validation_data=val_ds,
    epochs=epochs)

评估模型

让我们看看模型的性能。将返回两个值。损失(一个表示我们误差的数字,越低越好)和准确率。

loss, accuracy = model.evaluate(test_ds)

print("Loss: ", loss)
print("Accuracy: ", accuracy)

这种相当简单的做法实现了大约 86% 的准确率。

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

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

history_dict = history.history
history_dict.keys()

有四个条目:一个用于训练和验证期间监控的每个指标。您可以使用这些条目来绘制训练损失和验证损失以进行比较,以及训练准确率和验证准确率

acc = history_dict['binary_accuracy']
val_acc = history_dict['val_binary_accuracy']
loss = history_dict['loss']
val_loss = history_dict['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.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(loc='lower right')

plt.show()

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

请注意,训练损失在每个 epoch 中减少,训练准确率在每个 epoch 中增加。当使用梯度下降优化时,这是预期的结果——它应该在每次迭代中最小化所需的数量。

验证损失和准确率并非如此——它们似乎在训练准确率达到峰值之前就达到了峰值。这是一个过拟合的例子:模型在训练数据上的表现比在从未见过的测试数据上的表现更好。在此之后,模型过度优化并学习了特定于训练数据的表示,这些表示不会泛化到测试数据。

对于这种情况,您可以通过在验证准确率不再增加时简单地停止训练来防止过拟合。一种方法是使用 tf.keras.callbacks.EarlyStopping 回调。

导出模型

在上面的代码中,您在将文本馈送到模型之前将 TextVectorization 层应用于数据集。如果您想让您的模型能够处理原始字符串(例如,为了简化部署),您可以在模型中包含 TextVectorization 层。为此,您可以使用刚刚训练的权重创建一个新模型。

export_model = tf.keras.Sequential([
  vectorize_layer,
  model,
  layers.Activation('sigmoid')
])

export_model.compile(
    loss=losses.BinaryCrossentropy(from_logits=False), optimizer="adam", metrics=['accuracy']
)

# Test it with `raw_test_ds`, which yields raw strings
loss, accuracy = export_model.evaluate(raw_test_ds)
print(accuracy)

对新数据进行推断

要获取新示例的预测,您只需调用 model.predict()

examples = tf.constant([
  "The movie was great!",
  "The movie was okay.",
  "The movie was terrible..."
])

export_model.predict(examples)

将文本预处理逻辑包含在模型中,使您能够导出一个模型用于生产,这简化了部署,并减少了 训练/测试偏差 的可能性。

在选择将 TextVectorization 层应用于何处时,需要注意一个性能差异。在模型之外使用它,使您能够在 GPU 上训练时异步进行 CPU 处理和数据缓冲。因此,如果您在 GPU 上训练模型,您可能希望选择此选项以在开发模型时获得最佳性能,然后在准备好进行部署时切换到将 TextVectorization 层包含在模型中。

访问此 教程,了解有关保存模型的更多信息。

练习:在 Stack Overflow 问题上进行多类分类

本教程展示了如何在 IMDB 数据集上从头开始训练二元分类器。作为练习,您可以修改此笔记本以训练多类分类器,以预测在 Stack Overflow 上的编程问题的标签。

一个 数据集 已为您准备使用,其中包含数千个发布到 Stack Overflow 的编程问题(例如,“如何在 Python 中按值对字典进行排序?”)。每个问题都用一个标签标记(Python、CSharp、JavaScript 或 Java)。您的任务是将问题作为输入,并预测相应的标签,在本例中为 Python。

您将使用的数据集包含从 BigQuery 上更大的公共 Stack Overflow 数据集中提取的数千个问题,该数据集包含超过 1700 万个帖子。

下载数据集后,您会发现它与您之前使用过的 IMDB 数据集具有类似的目录结构。

train/
...python/
......0.txt
......1.txt
...javascript/
......0.txt
......1.txt
...csharp/
......0.txt
......1.txt
...java/
......0.txt
......1.txt

要完成此练习,您应该修改此笔记本以使用 Stack Overflow 数据集,方法是进行以下修改。

  1. 在笔记本的顶部,使用下载 Stack Overflow 数据集 的代码更新下载 IMDB 数据集的代码。由于 Stack Overflow 数据集具有类似的目录结构,因此您无需进行太多修改。

  2. 将模型的最后一层修改为 Dense(4),因为现在有四个输出类别。

  3. 在编译模型时,将损失更改为 tf.keras.losses.SparseCategoricalCrossentropy(from_logits=True)。这是在多类分类问题中使用的正确损失函数,当每个类别的标签为整数时(在本例中,它们可以是 0、123)。此外,将指标更改为 metrics=['accuracy'],因为这是一个多类分类问题(tf.metrics.BinaryAccuracy 仅用于二元分类器)。

  4. 在绘制随时间变化的准确率时,将 binary_accuracyval_binary_accuracy 分别更改为 accuracyval_accuracy

  5. 完成这些更改后,您将能够训练多类分类器。

了解更多

本教程介绍了从头开始的文本分类。要详细了解文本分类工作流程,请查看 Google Developers 的 文本分类指南

# MIT License
#
# Copyright (c) 2017 François Chollet
#
# Permission is hereby granted, free of charge, to any person obtaining a
# copy of this software and associated documentation files (the "Software"),
# to deal in the Software without restriction, including without limitation
# the rights to use, copy, modify, merge, publish, distribute, sublicense,
# and/or sell copies of the Software, and to permit persons to whom the
# Software is furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
# DEALINGS IN THE SOFTWARE.