在 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/pos
和 aclImdb/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_a
和 class_b
。这些将是正面和负面的电影评论,可以在 aclImdb/train/pos
和 aclImdb/train/neg
中找到。由于 IMDB 数据集包含其他文件夹,因此在使用此实用程序之前,您将删除它们。
remove_dir = os.path.join(train_dir, 'unsup')
shutil.rmtree(remove_dir)
接下来,您将使用 text_dataset_from_directory
实用程序来创建一个带标签的 tf.data.Dataset
。 tf.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()
这些层按顺序堆叠以构建分类器
- 第一层是
Embedding
层。此层接收整数编码的评论,并为每个单词索引查找一个嵌入向量。这些向量是在模型训练时学习的。这些向量向输出数组添加了一个维度。生成的维度是:(batch, sequence, embedding)
。要了解更多关于嵌入的信息,请查看 词嵌入 教程。 - 接下来,
GlobalAveragePooling1D
层通过对序列维度求平均值,为每个示例返回一个固定长度的输出向量。这允许模型以最简单的方式处理可变长度的输入。 - 最后一层是密集连接的,只有一个输出节点。
损失函数和优化器
模型需要一个损失函数和一个优化器进行训练。由于这是一个二元分类问题,并且模型输出一个概率(一个带有 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 数据集,方法是进行以下修改。
在笔记本的顶部,使用下载 Stack Overflow 数据集 的代码更新下载 IMDB 数据集的代码。由于 Stack Overflow 数据集具有类似的目录结构,因此您无需进行太多修改。
将模型的最后一层修改为
Dense(4)
,因为现在有四个输出类别。在编译模型时,将损失更改为
tf.keras.losses.SparseCategoricalCrossentropy(from_logits=True)
。这是在多类分类问题中使用的正确损失函数,当每个类别的标签为整数时(在本例中,它们可以是 0、1、2 或 3)。此外,将指标更改为metrics=['accuracy']
,因为这是一个多类分类问题(tf.metrics.BinaryAccuracy
仅用于二元分类器)。在绘制随时间变化的准确率时,将
binary_accuracy
和val_binary_accuracy
分别更改为accuracy
和val_accuracy
。完成这些更改后,您将能够训练多类分类器。
了解更多
本教程介绍了从头开始的文本分类。要详细了解文本分类工作流程,请查看 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.