FAST.AI 文本分类实践
除了前面介绍的图像分类,深度学习的另一个主要应用场景是自然语言处理。自然语言处理的研究对象是语言文本,而研究任务主要集中在:文本分类、摘要提取、情感分析、机器翻译、自动问答等方面。除了前面我们已经学习的计算机视觉任务模块 fastai.vision,FAST.AI 也有专门针对自然语言处理 fastai.text 模块。本次实验中,我们将学习 FAST.AI 在自然语言处理任务中的应用方法和技巧。
fastai.text 模块
FAST.AI 中用于自然语言处理的相关类和方法都位于 fastai.text 模块下方。其中:
text.transform:包含所有用于预处理数据的类和方法,例如字典编码。
text.data:包含的定义 TextDataBunch 的类和方法。
text.learner:包含快速创建语言模型或 RNN 分类器的相关类和方法。
事实上,通过学习 FAST.AI 针对计算机视觉任务设计的 fastai.vision 模块,你会发现 fastai.text 模块在接口设计和命名上与其非常相似。所以,接下来,我们将选择一个简单的文本分类示例,学习使用 FAST.AI 来完成文本分类任务。
文本分类
文本分类是自然语言处理最常见也是最简单的应用场景。文本分类就是将多个文档按某种属性来进行划分。例如,图书馆会把人文社科这一类的书籍放到一个区域,把科学技术类的书籍放到另一个区域,这样既方便馆内工作人员整理,也方便读者查阅。再比如说电子邮箱常用的垃圾邮件分类功能,也是一个常用的文本分类应用场景。
本次实验,我们选择 IMDB 数据集进行演示。前面的实验中,我们已经了解到 IMDB 数据集实际上是 IMDB 网站中的电影评论,每个样本拥有积极或消极标签。IMDB 总共包含 100,000 条评论,其中 25,000 条样本为训练集,另外 25,000 条则被标记为验证集。剩余的 50,000 条是未标记数据。
数据处理
首先,我们下载由 FAST.AI 整理好的 IMDB 数据集。由于原数据集太大,所以实验选择 IMDB_SAMPLE 精简数据集进行加载,该数据集只包括 1000 条采样样本,其中 20% 为验证集。
from fastai.text import URLs, untar_data
imdb_path = untar_data(URLs.IMDB_SAMPLE)
imdb_path.ls()
如果你了解 Pandas 的使用,那么我们可以直接读取该 CSV 文件进行预览。
import pandas as pd
imdb_df = pd.read_csv(imdb_path/'texts.csv')
imdb_df.head()
可以看到,整理好的 CSV 文件中包含 3 列,分别是标签 label
,文本内容 text
,和 is_valid
标记是否为验证数据。此时,我们使用 fastai.text.TextDataBunch
将文本数据读取为 DataBunch 对象,这一点和之前图像数据的处理过程非常相似。
from fastai.text import TextDataBunch
imdb_data = TextDataBunch.from_csv(imdb_path, 'texts.csv')
imdb_data.show_batch()
你会发现,FAST.AI 会自动依据 is_valid 标记将数据样本读取为训练集和验证集。这一点非常关键,后续你在处理自己的数据集时应该明白该标记设置的含义。
Tokenization
如果你仔细观察上方 TextDataBunch 的输出,会觉得有一些怪异,其中出现了很多以 xx 开头的标记。事实上,FAST.AI 在将文本处理成 TextDataBunch 时会自动执行 Tokenization 过程。Tokenization 在处理中文时被称为「分词」,而英语文本本身由单词构成,所以 Tokenization 的过程会针对其中一些特征进行标记,例如:
将 didn't 这类英文缩写分解成 did 和 n't 两部分。
对包含的 HTML 文档内容进行清洁。
将大写字母全部转化为小写字母。
加入以 xx 开头的特殊标记,xxunk 表示未知单词,xxbos 表示文本开头,xxup 表示下一个单词在原文本中为大写字母。
更多关于 Tokenization 的处理过程和步骤,请阅读 官方文档。
为了更加直观看出 Tokenization 的过程,我们使用单个样本进行对照演示。
example_text = imdb_df.iloc[1][1] # 取出单个样本
example_text
from fastai.text import Tokenizer, SpacyTokenizer
# 调用 FAST.AI 默认 spaCy Tokenizer 进行处理
tokenizer = Tokenizer()
' '.join(tokenizer.process_text(example_text, SpacyTokenizer('en')))
FAST.AI 执行 Tokenization 后是以列表形式储存数据,上面为了演示方便使用 join
操作进行了句子还原。由于 FAST.AI 默认使用 spaCy 文本处理工具,所以并不支持针对中文文本的 Tokenization 过程。中文文本分词,必须用到像 jieba 这类中文分词工具进行单独处理。
Numericalization
在执行 Tokenization 时,还会执行 Numericalization 操作,Numericalization 意思是将词处理成用数字表示的字典。机器学习算法只能处理数值,无法直接理解文本。所以,我们往往会将分词之后的单词按出现频次进行标记,例如 the 这个单词出现频次最多,那么标记为序号 1,以此类推。FAST.AI 默认只保留出现频次最高的 60000 个词汇,剩下出现次数较少的就会以 xxunk 标记,也就代表不认识该单词。
我们可以读取 TextDataBunch 中的 vocab 字典:
imdb_data.vocab.itos
同时,为了更直观看出 Numericalization 前后结果,这里使用单个样本对比观察:
imdb_data.train_ds[0][0].text # 输出训练集中第一条样本
imdb_data.train_ds[0][0].data # 训练集中第一条样本对应的向量
你可以对照上方字典来观察文本和向量元素之间的对应关系。
所以,由此可见 FAST.AI 使用的方便程度。正常情况下我们在对文本预处理过程中的分词和向量化过程,FAST.AI 使用 TextDataBunch 单个操作进行了封装,一次性将文本转换为能够用于训练模型的向量。
建模评估
完成以上步骤之后,接下来就可以开始训练文本分类模型。FAST.AI 提供了专门用于文本分类的接口 text_classifier_learner
,与前面图片分类相似,我们使用该类定义一个 Learner 最终完成模型训练。
一般情况下,文本分类较常使用到 RNN 循环神经网络模型。FAST.AI 在 text.models
中提供了多个经过实验反复验证的预训练模型方便调用,例如 AWD_LSTM,Transformer,TransformerXL 等。相关网络的原理需要你参考论文进行学习。接下来,我们使用 AWD_LSTM 模型结构完成文本分类任务。由于原 AWD_LSTM 预训练模型,速度很慢,实验首先从蓝桥云课镜像服务器下载该模型。
# 由于原 AWD_LSTM 预训练模型,速度很慢,实验首先从蓝桥云课镜像服务器下载该模型。直接运行即可。
!wget -nc "https://labfile.oss.aliyuncs.com/courses/1445/wt103-fwd.tgz" -P /root/.fastai/models
from fastai.text.models import AWD_LSTM
from fastai.text import text_classifier_learner
# 定义一个小 batch_size 的 TextDataBunch
imdb_data_lite = TextDataBunch.from_csv(imdb_path, 'texts.csv')
# 定义 Learner
learner = text_classifier_learner(imdb_data_lite, AWD_LSTM)
如果代码报网络错误,可能是因为有模型更新的原因,而官方模型又挂载在外网,反复重试一下可以解决。你可以通过提交课程问题告诉我们,我们会及时更新新的模型镜像。
learner.fit(3)
等待训练完成即可,效果并不太好。原因主要有 2 点,首先,预训练模型 AWD_LSTM 是基于维基百科语料进行训练的,和 IMDB 语料相差较大,我们并没有进行微调。此外,实验使用到的数据实在太少。
语言模型
为了使得文本分类的效果更好,很多时候会先训练新的语言模型。语言模型是在不考虑标签的基础上,使用无监督学习方法来完成训练。基于训练好的语言模型,我们可以直接预测和生成新的句子。
训练语言模型前,需要重新制作专用的 DataBunch 数据,你需要使用到 TextLMDataBunch 方法。过程和之前处理 TextDataBunch 是非常相似,示例如下:
from fastai.text import TextLMDataBunch
imdb_data_lm = TextLMDataBunch.from_csv(imdb_path, 'texts.csv')
imdb_data_lm.show_batch()
你可以看到,输出的数据不再含有标签,只包括文本内容。
接下来,我们使用 FAST.AI 用于语言模型训练的方法 fastai.text.language_model_learner
开始训练过程。该方法使用和上方的分类过程几乎一致。
from fastai.text import language_model_learner
learner_lm = language_model_learner(imdb_data_lm, AWD_LSTM)
learner_lm.fit(3)
等待训练完成之后,我们可以使用模型进行预测。语言模型可以预测完整的句子,而不是分类标签。例如,我们指定句子的开头和长度,使用训练好的模型来补充完整的句子。
TEXT = "I loved that movie because" # 指定句子的开头
N_WORDS = 40 # 指定句子长度
N_SENTENCES = 5 # 指定预测句子数量
print("\n".join(learner_lm.predict(TEXT, N_WORDS, temperature=0.75)
for _ in range(N_SENTENCES)))
最终,模型将会给出不同的预测结果。语言模型训练往往需要大量的数据语料,所以上方的句子可能读起来并不通顺。很多时候,我们会使用 BERT,XLNet 等在大规模语料上训练好的语言模型,这些模型使用到 TB 级的数据进行训练。
此时,我们使用 .save_encoder
保存学习器中的编码器部分,其主要负责创建和更新 RNN 网络中的隐藏状态。
learner_lm.save_encoder('fine_tuned_encoder')
接下来,我们使用保存好的编码器来重新完成分类模型训练过程。这里,我们选择上方语言模型的生成的 vocab 来定义新的 TextDataBunch 对象。
imdb_data_ft = TextDataBunch.from_csv(imdb_path, 'texts.csv', vocab=imdb_data_lm.vocab)
imdb_data_ft
训练新的文本分类器,调用语言模型提供的编码器对模型进行微调。
learner_ft = text_classifier_learner(imdb_data_ft, AWD_LSTM)
learner_ft.load_encoder('fine_tuned_encoder')
learner_ft.fit(3)
最终,你会发现效果会比上方直接训练要好一些。但受限于迭代次数和语料大小,提升程度并不是非常显著。
与图像分类相似,FAST.AI 也提供的 fastai.text.TextClassificationInterpretation
方法来对结果进行进一步分析。我们可以通过 interp.plot_confusion_matrix
直接绘制出真实标签和预测标签之间的混淆矩阵。
from fastai.text import TextClassificationInterpretation
# 载入学习器
interp = TextClassificationInterpretation.from_learner(learner_ft)
interp.plot_confusion_matrix(figsize=(5, 5), dpi=100)
通过混淆矩阵,我们可以看出被预测错误最多的类别,从而来调整输入数据。同样,我们可以输出损失最大的样本进行人工查看。
interp.show_top_losses(5)
IMDB 文本分类挑战
前面的挑战中,我们使用 IMDB_SAMPLE 数据集训练了文本分类和语言模型。由于 IMDB_SAMPLE 仅有 1000 条数据,模型的表现并不算好。本次挑战中,请使用完整的 IMDB 尝试训练出更好的文本分类和语言模型。为了提升训练速度,本次挑战同样配置了 GPU 环境。
首先,我们从蓝桥云课服务器下载完整的 IMDB 数据集,并使用 FAST.AI 读取路径。该数据集来源于 FAST.AI,蓝桥云课仅完成了镜像托管,以便于提高数据下载速度。
from fastai.datasets import untar_data, URLs, download_data
download_data("http://labfile.oss.aliyuncs.com/courses/1445/imdb")
imdb_path = untar_data(URLs.IMDB)
imdb_path.ls()
# 由于原 AWD_LSTM 预训练模型,速度很慢,实验首先从蓝桥云课镜像服务器下载该模型。直接运行即可。
!wget -nc "https://labfile.oss.aliyuncs.com/courses/1445/wt103-fwd.tgz" -P /root/.fastai/models
数据路径中,train 为 25000 条有标签的训练数据,test 为 25000 条有标签验证数据,unsup 为 50000 条未标注数据。其他文件可以不予考虑。接下来,请使用完整的 IMDB 数据训练一个文本分类模型,并得到验证数据集上的准确度结果。
首先,我们使用 50000 条未标注数据训练语言模型。这里,挑战告诉大家一种自定义程度更高的划分方法,通过 TextList 而不是 TextDataBunch 将数据处理成自己想要的 DataBunch。
from fastai.text import TextList
imdb_data_lm = (TextList.from_folder(imdb_path)
# 过滤我们需要的文件夹
.filter_by_folder(include=['unsup'])
# 随机划分一定百分比数据到验证集
.split_by_rand_pct(0.1)
# 指定标签类型为语言模型
.label_for_lm()
# 处理成 DataBunch
.databunch())
imdb_data_lm
接下来,请补充训练语言模型的代码,并保存相应的编码器。
from fastai.text.models import AWD_LSTM
from fastai.text import language_model_learner
learner_lm = language_model_learner(imdb_data_lm, AWD_LSTM)
learner_lm.fit(1)
learner_lm.save_encoder('fine_tuned_encoder')
我们可以生成以 I loved that movie because 开头的语句,并与前面实验对比效果。
TEXT = "I loved that movie because" # 指定句子的开头
N_WORDS = 40 # 指定句子长度
N_SENTENCES = 5 # 指定预测句子数量
print("\n".join(learner_lm.predict(TEXT, N_WORDS, temperature=0.75)
for _ in range(N_SENTENCES)))
接下来,我们开始训练分类模型。由于 train 和 test 各有 25000 标记数据,实际上我们无需 25000 条数据用于验证。所以,在此希望能合并数据后进行重新划分。
下面,我们使用 TextList 读取全部标注数据,并划分其中 10% 用于验证。相关代码的作用已进行了注释。
imdb_data = (TextList.from_folder(imdb_path, vocab=imdb_data_lm.vocab)
# 过滤我们需要的两个文件夹
.filter_by_folder(include=['train', 'test'])
# 0.1 划为验证集
.split_by_rand_pct(0.1)
# 指定标签类型
.label_from_folder(classes=['neg', 'pos'])
# 处理成 DataBunch
.databunch())
imdb_data
此时,我们得到了包含 45000 条训练数据和 5000 条验证数据的 DataBunch 对象。
接下来,请使用 FAST.AI 提供的文本分类方法,针对上述数据集进行分类。你可以参考官方文档或其他资料自由选择模型,需保证最终验证集分类准确度高于 80% 即可。
from fastai.text import text_classifier_learner
# 定义 Learner
learner_ft = text_classifier_learner(imdb_data, AWD_LSTM)
learner_ft.load_encoder('fine_tuned_encoder')
learner_ft.fit(5)