本文的示例为使用 IMDB 的评论数据来做情感分类(sentiment analysis):
数据源地址:https://ai.stanford.edu/~amaas/data/sentiment/aclImdb_v1.tar.gz
1. 加载数据集
使用 tf.keras.preprocessing.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 是分类标签,示例中数据集为 IMDB的评论数据,目录结构为:
➜ aclImdb ll
total 3432
-rw-r--r-- 1 hongbin.dhb staff 4037 6 26 2011 README
-rw-r--r-- 1 hongbin.dhb staff 845980 4 13 2011 imdb.vocab
-rw-r--r-- 1 hongbin.dhb staff 903029 6 12 2011 imdbEr.txt
drwxr-xr-x 7 hongbin.dhb staff 224 4 13 2011 test
drwxr-xr-x 9 hongbin.dhb staff 288 10 10 19:51 train
加载数据集的代码如下:
# 每批大小
batch_size = 32
seed = 42
# 从train目录中抽取80%作为训练集
raw_train_ds = tf.keras.preprocessing.text_dataset_from_directory(
'aclImdb/train',
batch_size=batch_size,
validation_split=0.2,
subset='training',
seed=seed)
# 从目录train中抽取20%作为验证集
raw_val_ds = tf.keras.preprocessing.text_dataset_from_directory(
'aclImdb/train',
batch_size=batch_size,
validation_split=0.2,
subset='validation',
seed=seed)
# 从目录test中加载测试集
raw_test_ds = tf.keras.preprocessing.text_dataset_from_directory(
'aclImdb/test',
batch_size=batch_size)
返回值 raw_train_ds 是带标注的数据集,类型为 tf.data.Dataset,
需要注意的是,从训练集中摘取验证集需要使用 validation_split 和 subset 参数,同时需要指定 seed 参数,或者使用 shuffle=False 来保证训练集和验证集数据没有交叉。
下面看一下加载的数据集的标注 lable 和 class_name:
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])
标注label的值为 0 / 1,标注的类别名用 raw_train_ds.class_name[0/1] 获取:
print("Label 0 corresponds to", raw_train_ds.class_names[0])
print("Label 1 corresponds to", raw_train_ds.class_names[1])
输出
Label 0 corresponds to neg
Label 1 corresponds to pos
2. 数据预处理
数据预处理需要对文本进行 标准化(Standardize)、分词(Tokenize)、向量化(Vectorize) 处理
- 标准化通常指去掉文本中的标点符号或 HTML 标签
- 分词指将文本切分为一个一个的单词
- 向量化指将一个一个单词转化为数字,方便喂入神经网络
这里是一个自定义的标准化处理方法
def custom_standardization(input_data):
# 文本转换为小写字母
lowercase = tf.strings.lower(input_data)
# 替换 <br /> 标签为空格(单词分隔符)
stripped_html = tf.strings.regex_replace(lowercase, '<br />', ' ')
# 删除标点符号,一般标点符号后面都有空格
return tf.strings.regex_replace(stripped_html,
'[%s]' % re.escape(string.punctuation),
'')
使用 TextVectorization 方法 做文本标准化,分词,及向量化处理
from tensorflow.keras.layers.experimental.preprocessing import TextVectorization
# 最大的特征数量,也就是分词后最大的单词数量
max_features = 10000
# 最大的向量长度
sequence_length = 250
# 构建向量化层(用于做文本向量化处理)
# 用自定义标准化方法做标准化处理
# 最大分词数量 10000
# 输出的向量长度 250
vectorize_layer = TextVectorization(
standardize=custom_standardization,
max_tokens=max_features,
output_mode='int',
output_sequence_length=sequence_length)
接下来需要调用 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
处理训练集,验证集和测试集数据
# 数据集的每一行都是 文本 和 标注
train_ds = raw_train_ds.map(vectorize_text)
val_ds = raw_val_ds.map(vectorize_text)
test_ds = raw_test_ds.map(vectorize_text)
最后还有重要的一步,为了数据处理性能,需要将向量化后的数据放入缓存
AUTOTUNE = tf.data.experimental.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)
到这里,数据预处理基本完成了,train_ds, val_ds, test_ds 可以用于输入我们的模型。
3. 构建网络模型
embedding_dim = 16
model = tf.keras.Sequential([
layers.Embedding(max_features + 1, embedding_dim),
layers.Dropout(0.2),
layers.GlobalAveragePooling1D(),
layers.Dropout(0.2),
layers.Dense(1)])
model.summary()
输出
Model: "sequential"
_________________________________________________________________
Layer (type) Output Shape Param #
=================================================================
embedding (Embedding) (None, None, 16) 160016
_________________________________________________________________
dropout (Dropout) (None, None, 16) 0
_________________________________________________________________
global_average_pooling1d (Gl (None, 16) 0
_________________________________________________________________
dropout_1 (Dropout) (None, 16) 0
_________________________________________________________________
dense (Dense) (None, 1) 17
=================================================================
Total params: 160,033
Trainable params: 160,033
Non-trainable params: 0
_________________________________________________________________
模型说明:
- 第一层是嵌入层,嵌入层的输入以单词索引下标作为整数编码后的数据,然后查每个单词索引下标对应的向量,这些向量是模型训练的时候学出来的,具体的原理和过程我们以后说
- 接着GlobalAveragePooling1D层
- 最后是一个只有1个输出节点的全连接层(激活函数为 sigmod)
4. 损失函数和优化器
因为是二分类问题,并且模型输出了分类的概率
所以使用 losses.BinaryCrossentropy 损失函数
model.compile(
loss=losses.BinaryCrossentropy(from_logits=True),
optimizer='adam',
metrics=tf.metrics.BinaryAccuracy(threshold=0.0)
)
5. 模型训练
仅需要传入 dataset 给模型的 fit 方法
epochs = 10
history = model.fit(
train_ds,
validation_data=val_ds,
epochs=epochs)
6. 评估模型
loss, accuracy = model.evaluate(test_ds)
print("Loss: ", loss)
print("Accuracy: ", accuracy)
输出
782/782 [==============================] - 3s 3ms/step - loss: 0.3104 - binary_accuracy: 0.8734
Loss: 0.31037017703056335
Accuracy: 0.8733999729156494
7. 绘制模型损失和准确率曲线
模型训练方法 model.fit 返回了 history 对象,包含了训练过程中的精确率和损失的数据,使用history来绘制相关曲线
绘制损失曲线
history_dict = history.history
# 训练集上的准确率
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)
# 绘制训练损失
plt.plot(epochs, loss, 'bo', label='Training loss')
# 绘制验证损失
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, 'ro', label='Training acc')
# 绘制验证准确率
plt.plot(epochs, val_acc, 'r', label='Validation acc')
plt.title('Training and validation accuracy')
plt.xlabel('Epochs')
plt.ylabel('Accuracy')
plt.legend(loc='lower right')
plt.show()
8. 模型导出
上门我们使用TextVectorization 方法处理了文本数据,再喂入模型训练,如果想直接使用原始的文本数据来训练模型并做预测,可以构建一个新模型,在第一层使用 vectory_layer 层,第二层为上面我们训练好的模型,最后一层接一个 sigmod 目标函数,这个新模型不需要训练了,可直接编译使用
# 构建新模型,使用之前训练好的模型
export_model = tf.keras.Sequential([
vectorize_layer,
model,
layers.Activation('sigmoid')
])
# 模型编译,指定损失函数及优化器
export_model.compile(
loss=losses.BinaryCrossentropy(from_logits=False),
optimizer="adam",
metrics=['accuracy']
)
# 使用原始的测试集数据评估模型准确性
loss, accuracy = export_model.evaluate(raw_test_ds)
print(accuracy)
输出
782/782 [==============================] - 4s 5ms/step - loss: 0.3104 - accuracy: 0.8734
0.8733999729156494
使用新的模型来预测新数据的情感分类:
examples = [
"The movie was great!",
"The movie was okay.",
"The movie was terrible..."
]
export_model.predict(examples)
输出
array([[0.634246 ],
[0.45762002],
[0.37179616]], dtype=float32)
将文本处理层(标准化,分词,向量化)放入模型中,导出训练好的模型,可以简单地处理原始文本,并且能减少训练集和测试的数据倾斜问题
但是将文本处理层放到模型外边,能更好的利用CPU的并行处理能力,以及在GPU上训练是数据能够得到缓存,所以通常在模型开发的时候,我们将文本处理层放到外面,在模型部署的时候再将文本处理层放到模型里面。