如何在 fast.ai 用 BERT 做中文文本分类?

 
  

鱼我所欲也,熊掌亦我所欲也。

640?wx_fmt=jpeg

痛点

我之前用 BERT ,就没有痛快过。

最初,是 Google 发布的原始 Tensorflow 代码,一堆堆参数,一行行代码,扑面而来。让人看着,就眼晕。

后来,Google 把 BERT 在 Tensorflow Hub 上面打了个包。

还是需要很多行代码,许多参数设置,才能让它学习你自己提供的数据。不过我还是很兴奋地帮你重构代码,搞了个十行代码可执行版本

640?wx_fmt=jpeg

但这其实,不过是隐藏了大量细节而已。

那些繁琐的代码,依然在那里。

代码越多,修改和维护就越困难。

你看人家 fast.ai ,需要什么功能,只要找到对应的 API ,输入三样东西:

一般而言,只需要几行代码。

然后,结果就出来了。

640?wx_fmt=jpeg

这样,你可以很轻易尝试自己的想法,并且在不同数据集上面加以验证。

这种快速迭代反馈,对于机器学习研究来说,是非常有益处的。

因此,当 Huggingface 的 Kaushal Trivedi 真的起心动念,仿照 fast.ai 搞了个 fastbert 时,我特别开心。

于是又写了份教程,教你如何用它来做多标签文本分类。

但是,这个 fastbert ,与 fast.ai 比起来,差别还是太大了。

首先是设置起来依旧繁琐,你得照顾到许多参数;

其次是维护并不及时。两次更新之间的时间,竟然可能相差一个月;

640?wx_fmt=jpeg

第三是缺乏文档和样例。这对于新手,是非常不友好的。

几乎所有遭遇到这些问题的人,都在问一个问题:

什么时候,才能在 fast.ai 里面,方便地调用 BERT 来完成自然语言处理任务呢?

大部分人,只是动动念头,然后继续将就着使用 fast.ai 提供的 ULMfit 来处理英文文本。

毕竟,对于英文、波兰文来说,效果也不错

可是,中文怎么办?

ULMfit 推出1年多了,至今却没有一个公开发布、广泛使用的中文预训练模型。

这真是令人烦恼的事儿啊!

黑客

有需求,也就有人来琢磨解决。

fast.ai 结合 BERT 这问题,对研究者的要求,着实不低。至少包括:

此外,还得能够融汇贯通,把二者结合起来,这就需要对 PyTorch 的熟练掌握。

能做出这样工作的人,大约就算是黑客了。

幸好,这样的高人真的出手了。

他就是卡内基梅隆大学的研究生,Keita Kurita。

640?wx_fmt=jpeg

Keita 有个博客,叫 Machine Learning Explained ,干货非常多,这里一并推荐给你。

他写的 fast.ai 结合 BERT 的英文教程,地址在这里。

640?wx_fmt=jpeg

其实,有时候问题的解决,就如同窗户纸,一捅就破。

Keita 并没有尝试“重新发明轮子”,即从头去解决这个问题。

他只是巧妙地借用了第三方的力量,并且将其与 fast.ai 融合起来。

这个第三方,就是咱们前面提过的 Huggingface 。

640?wx_fmt=jpeg

自从 BERT 的 Tensorflow 源代码经由 Google 发布出来,他们就在 Github 上面,搞了一个 PyTorch 版本的克隆。

640?wx_fmt=jpeg

这个克隆,还包含了预训练的结果。

也就是说,他们提供了一个完整版的模型架构,只要配上相应的数据和损失函数, fast.ai 就可以开工了!

fast.ai 文本处理一直不支持中文,是因为它其实也调用了第三方库,就是咱们介绍过的 Spacy

到今天为止, Spacy 也并不能完整支持中文处理,这就导致了 fast.ai 对中文无能为力。

640?wx_fmt=jpeg

但是, BERT 可不是这样。

它很早就有专门的中文处理工具和预训练模型。

关键是,如何在 fast.ai 中,用它替换掉 Spacy 来使用。

Keita 的文章,一举解决了上述两个问题。

便捷的 fast.ai 框架就这样和强大的 BERT 模型嫁接了起来。

变化

受 Keita 的影响,其他作者也尝试了不同的任务和数据集,并且把自己的代码和工作流程也做了发布。

例如 abhik jha 这篇 “Fastai integration with BERT: Multi-label text classification identifying toxicity in texts”(地址在这里),还在 Twitter 受到了 Jeremy Howard (fast.ai 联合创始人)点赞。

640?wx_fmt=jpeg

蒙天放这篇知乎教程,更讲解了如何处理中文数据分类。

看起来,我似乎没有必要再写一篇教程了。

然而环境是在变化的。

Huggingface 现在,已经不仅仅做 BERT 预训练模型的 PyTorch 克隆了。

他们居然希望把所有的 Transformer 模型,全都搞一遍。

于是把原先的 Github 项目“pytorch-pretrained-BERT”,改成了“pytorch-transformers”这样一个野心勃勃的名字。

640?wx_fmt=jpeg

新的项目地址在这里。

你的想象空间,也就可以因此而开启了。

能不能用这些基于 Transformer 的预训练模型,来做自己的下游任务呢?

一如既往, Huggingface 的技术还是那么过硬。

然而,提供的接口,还是那么繁琐。虽然用户手册比之前有了较大改进,可教程样例依然不够友好。

我于是在思考,既然老版本 BERT 预训练模型可以和 fast.ai 对接,那能否把新版本的各种 Transformer,也用这种方式简化调用呢?

如果这样做可以的话,咱们就不必再眼巴巴等着 Huggingface 改进教程与用户接口了,直接用上 fast.ai ,取两者的长处,结合起来就好了。

于是,我开始了尝试。

一试才发现,新版本“pytorch-transformers”的预训练模型,与老版本还有一些变化。倘若直接迁移代码,会报错的。

所以,这篇文章里,我从头到尾,为你提供一个在新版本“pytorch-transformers” 中 BERT 预训练模型上直接能用的样例,并且加以详细讲解。

这样一来,相信我踩了一遍的坑,你就可以躲开了。这可以大量节省你的时间。

同时,我也希望你能够以这个样例作为基础,真正做到举一反三,将其他的 Transformer 模型尝试迁移,并且把你的试验结果,分享给大家。

环境

本文的配套源代码,我放在了 Github 上。

你可以在我的公众号“玉树芝兰”(nkwangshuyi)后台回复“aibert”,查看完整的代码链接。

640?wx_fmt=jpeg

如果你对我的教程满意,欢迎在页面右上方的 Star 上点击一下,帮我加一颗星。谢谢!

注意这个页面的中央,有个按钮,写着“在 Colab 打开”(Open in Colab)。请你点击它。

然后,Google Colab 就会自动开启。

640?wx_fmt=jpeg

我建议你点一下上图中红色圈出的 “COPY TO DRIVE” 按钮。这样就可以先把它在你自己的 Google Drive 中存好,以便使用和回顾。

640?wx_fmt=jpeg

Colab 为你提供了全套的运行环境。你只需要依次执行代码,就可以复现本教程的运行结果了。

如果你对 Google Colab 不熟悉,没关系。我这里有一篇教程,专门讲解 Google Colab 的特点与使用方式。

为了你能够更为深入地学习与了解代码,我建议你在 Google Colab 中开启一个全新的 Notebook ,并且根据下文,依次输入代码并运行。在此过程中,充分理解代码的含义。

这种看似笨拙的方式,其实是学习的有效路径。

代码

首先提示一下,fast.ai 给我们提供了很多便利,例如你只需要执行下面这一行,许多数据科学常用软件包,就都已经默认读入了。

from fastai.text import *import *

因此,你根本就不需要执行诸如 import numpy as npimport torch 之类的语句了。

下面,我们本着 fast.ai 的三元素(数据、架构、损失函数)原则,首先处理数据。

先把数据下载下来。

!wget https://github.com/wshuyi/public_datasets/raw/master/dianping.csv

这份大众点评情感分类数据,你应该已经很熟悉了。之前的教程里面,我多次用它为你演示中文二元分类任务的处理。

让我们用 Pandas ,读入数据表。

df = pd.read_csv("dianping.csv")

下面是划分训练集、验证集和测试集。我们使用 scikit-learn 软件包协助完成。

from sklearn.model_selection import train_test_splitimport train_test_split

首先,我们把全部数据,分成训练和测试集。

注意这里我们设定 random_state ,从而保证我这儿的运行结果,在你那里可复现。这可是“可重复科研”的基本要件。

train, test = train_test_split(df, test_size=.2, random_state=2)2)

之后,我们再从原先的训练集里,切分 20% ,作为验证集。依旧,我们还是要设置random_state

train, valid = train_test_split(train, test_size=.2, random_state=2)2)

之后,咱们检查一下不同数据集合的长度。

训练集:

len(train)
640?wx_fmt=jpeg

验证集:

len(valid)
640?wx_fmt=jpeg

测试集:

len(test)
640?wx_fmt=jpeg

然后,来看看训练集前几行内容。

train.head()
640?wx_fmt=jpeg

数据预处理的第一步,已经做完了。

但是,我们都知道,机器学习模型是不认识中文的,我们必须做进一步的处理。

这时候,就需要 Huggingface 的 Transformers 预训练模型登场了。

!pip install pytorch-transformers

本文演示的是 BERT ,所以这里只需要读入两个对应模块。

一个是 Tokenizer ,用于把中文句子,拆散成一系列的元素,以便映射成为数字来表示。

一个是序列分类模块。在一堆 Transformer 的顶部,通过全连接层,来达到分类功能。

640?wx_fmt=jpeg
from pytorch_transformers import BertTokenizer, BertForSequenceClassificationimport BertTokenizer, BertForSequenceClassification

我们指定几个必要的参数。

例如每句话,最长不能超过128个字。

每次训练,用32条数据作为一个批次。

当然,我们用的预训练模型,是中文的,这也得预先讲好。

bert_model = "bert-base-chinese"max_seq_len = 128batch_size = 32
max_seq_len = 128
batch_size = 32

设置参数之后,我们就可以读取预置的 Tokenizer 了,并且将它存入到 bert_tokenizer 变量中。

bert_tokenizer = BertTokenizer.from_pretrained(bert_model)
640?wx_fmt=jpeg

我们检查一下,看预训练模型都认识哪些字。

这里我们随意选取从 2000 到 2005 位置上的 Token 来查看。

list(bert_tokenizer.vocab.items())[2000:2005]2005]
640?wx_fmt=jpeg

这里我们看到, BERT 还真是认识不少汉字的。

我们把全部的词汇列表存储起来,下面要用到。

bert_vocab = Vocab(list(bert_tokenizer.vocab.keys()))

注意 fast.ai 在 Tokenizer 环节,实际上设计的时候有些偷懒,采用了“叠床架屋”的方式。

640?wx_fmt=jpeg

反正最终调用的,是 Spacy ,因此 fast.ai 就把 Spacy Tokenizer 作为底层,上层包裹,作为自己的 Tokenizer 。

我们这里做的工作,就是重新定义一个新的 BertFastaiTokenizer ,最重要的功能,就是把 Spacy 替掉。另外,在每一句话前后,根据 BERT 的要求,加入起始的 [CLS] 和结束位置的 [SEP],这两个特殊 Token 。

class BertFastaiTokenizer(BaseTokenizer):    def __init__(self, tokenizer, max_seq_len=128, **kwargs):        self.pretrained_tokenizer = tokenizer        self.max_seq_len = max_seq_len    def __call__(self, *args, **kwargs):        return self    def tokenizer(self, t):        return ["[CLS]"] + self.pretrained_tokenizer.tokenize(t)[:self.max_seq_len - 2] + ["[SEP]"]
def __init__(self, tokenizer, max_seq_len=128, **kwargs):
self.pretrained_tokenizer = tokenizer
self.max_seq_len = max_seq_len

def __call__(self, *args, **kwargs):
return self

def tokenizer(self, t):
return ["[CLS]"] + self.pretrained_tokenizer.tokenize(t)[:self.max_seq_len - 2] + ["[SEP]"]

我们把这个类的调用,作为一个函数保存。

tok_func = BertFastaiTokenizer(bert_tokenizer, max_seq_len=max_seq_len)

然后,最终的 Tokenizer, 是把这个函数作为底层,融入其中的。

bert_fastai_tokenizer = Tokenizer(    tok_func=tok_func,    pre_rules = [],    post_rules = [])
pre_rules = [],
post_rules = []
)

我们设定工作目录为当前目录。

path = Path(".")

之后,得把训练集、验证集和测试集读入。

注意我们还需要指定数据框里面,哪一列是文本,哪一列是标记。

另外,注意 fast.ai 和 BERT 在特殊 Token 定义上的不同。include_bosinclude_eos 要设定为 False ,否则两套系统间会冲突。

databunch = TextClasDataBunch.from_df(path, train, valid, test,                  tokenizer=bert_fastai_tokenizer,                  vocab=bert_vocab,                  include_bos=False,                  include_eos=False,                  text_cols="comment",                  label_cols='sentiment',                  bs=batch_size,                  collate_fn=partial(pad_collate, pad_first=False, pad_idx=0),             )
vocab=bert_vocab,
include_bos=False,
include_eos=False,
text_cols="comment",
label_cols='sentiment',
bs=batch_size,
collate_fn=partial(pad_collate, pad_first=False, pad_idx=0),
)

让我们来看看预处理之后的数据吧:

databunch.show_batch()
640?wx_fmt=jpeg

在 fast.ai 里面,正常出现了 BERT 风格的中文数据预处理结果,还是很令人兴奋的。

注意,前面我们指定了 pre_rulespost_rules 两个参数,都写成 [] 。这是必要的。

我尝试了一下,如果按照默认值,不提这两个参数,那么二者默认都是 None 。这样一来,数据预处理结果就会成这样。

640?wx_fmt=jpeg

这和我们需要的结果,不一致。所以此处需要留意。

第一个元素,数据有了。

下面我们来处理架构

Huggingface 的网页上面介绍,说明了新的 Transformer 模型和原先版本的 BERT 预训练模型差异。

640?wx_fmt=jpeg

最大的不同,就是所有的模型运行结果,都是 Tuple 。即原先的模型运行结果,都用括号包裹了起来。括号里,可能包含了新的数据。但是原先的输出,一般作为新版 Tuple 的第一个元素。

可是 fast.ai 默认却不是这样的。

为了避免两个框架沟通中的误解,我们需要定义一个类。

它只做一件事情,就是把 forward 函数执行结果,只取出来第一项作为结果使用。

代码很简单:

class MyNoTupleModel(BertForSequenceClassification):  def forward(self, *args, **kwargs):    return super().forward(*args, **kwargs)[0]
def forward(self, *args, **kwargs):
return super().forward(*args, **kwargs)[0]

下面,我们来用新构建的类,搭模型架构。

注意你需要说明分类任务要分成几个类别。

我们这里是二元分类,所以写2。

bert_pretrained_model = MyNoTupleModel.from_pretrained(bert_model, num_labels=2)
640?wx_fmt=jpeg

现在,只剩下第三个元素了,那就是损失函数

因为要做二元分类,输出的结果按照 fast.ai 的要求,会是 [0.99, 0.01] 这样。所以损失函数我们选择 nn.CrossEntropyLoss

loss_func = nn.CrossEntropyLoss()

三大要素聚齐,我们终于可以构建学习器 Learner 了。

learn = Learner(databunch,                bert_pretrained_model,                loss_func=loss_func,                metrics=accuracy)
loss_func=loss_func,
metrics=accuracy)

有了 Learner ,剩下的工作就简单了许多。

例如,我们可以寻找一下最优的最大学习速率。

learn.lr_find()

找到后,绘制图形。

learn.recorder.plot()
640?wx_fmt=jpeg

读图以后,我们发现,最大学习速率的量级,应该在 e-5 上。这里我们设置成 2e-5 试试。

这里,只跑两个轮次,避免过拟合。

当然,也有省时间的考虑。

learn.fit_one_cycle(2, 2e-5)2e-5)
640?wx_fmt=jpeg

验证集上,效果还是很不错的。

但是,我们不能只拿验证集来说事儿。还是得在测试集上,看真正的模型分类效果。

这里面的原因,我在《如何正确使用机器学习中的训练集、验证集和测试集?》一文中,已经为你做了详细的解释。

如果忘了,赶紧复习一下。

我们用笨办法,预测每一条测试集上的数据类别。

定义一个函数。

def dumb_series_prediction(n):  preds = []  for loc in range(n):    preds.append(int(learn.predict(test.iloc[loc]['comment'])[1]))  return preds
preds = []
for loc in range(n):
preds.append(int(learn.predict(test.iloc[loc]['comment'])[1]))
return preds

实际执行,结果存入到 preds 里面。

preds = dumb_series_prediction(len(test))

查看一下前 10 个预测结果:

preds[:10]
640?wx_fmt=jpeg

我们还是从 scikit-learn 里面读入分类报告和混淆矩阵模块。

from sklearn.metrics import classification_report, confusion_matriximport classification_report, confusion_matrix

先看分类报告:

print(classification_report(test.sentiment, preds))
640?wx_fmt=jpeg

f1-score 达到了 0.9 ,很棒!

再通过混淆矩阵,看看哪里出现判断失误。

print(confusion_matrix(test.sentiment, preds))
640?wx_fmt=jpeg

基于 BERT 的中文分类任务完成!

小结

通过这篇文章的学习,希望你掌握了以下知识点:

如前文所述,希望你举一反三,尝试把 Huggingface 推出的其他 Transformer 预训练模型与 fast.ai 结合起来。

欢迎你把尝试的结果在留言区分享给其他同学。

祝深度学习愉快!

征稿

SSCI 检索期刊 Information Discovery and Delivery 要做一期《基于语言机器智能的信息发现》( “Information Discovery with Machine Intelligence for Language”) 特刊(Special Issue)。

640?wx_fmt=jpeg

本人是客座编辑(guest editor)之一。另外两位分别是:

640?wx_fmt=jpeg

征稿的主题包括但不限于:

具体的征稿启事(Call for Paper),请查看 Emerald 期刊官网的这个链接(http://dwz.win/c2Q)。

作为本专栏的老读者,欢迎你,及你所在的团队踊跃投稿哦。

如果你不巧并不从事上述研究方向(机器学习、自然语言处理和计算语言学等),也希望你能帮个忙,转发这个消息给你身边的研究者,让他们有机会成为我们特刊的作者。

谢谢!

延伸阅读

你可能也会对以下话题感兴趣。点击链接就可以查看。

题图:Photo by Harley-Davidson on Unsplash


发布了97 篇原创文章 · 获赞 272 · 访问量 23万+

猜你喜欢

转载自blog.csdn.net/nkwshuyi/article/details/97711723