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

痛点

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

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

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

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

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

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

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

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

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

然后,结果就出来了。

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

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

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

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

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

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

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

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

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

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

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

可是,中文怎么办?

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

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

黑客

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

但是, 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 联合创始人)点赞。

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

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

然而环境是在变化的。

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

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

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

新的项目地址在这里。

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

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

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

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

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

于是,我开始了尝试。

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

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

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

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

环境

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

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

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

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

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

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

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)

验证集:

len(valid)

测试集:

len(test)

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

train.head()

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

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

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

!pip install pytorch-transformers

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

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

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

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)

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

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

list(bert_tokenizer.vocab.items())[2000:2005]2005]

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

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

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

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

反正最终调用的,是 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()

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

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

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

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

第一个元素,数据有了。

下面我们来处理架构

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

最大的不同,就是所有的模型运行结果,都是 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)

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

因为要做二元分类,输出的结果按照 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()

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

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

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

learn.fit_one_cycle(2, 2e-5)2e-5)

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

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

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

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

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

定义一个函数。

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]

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

from sklearn.metrics import classification_report, confusion_matriximport classification_report, confusion_matrix

先看分类报告:

print(classification_report(test.sentiment, preds))

f1-score 达到了 0.9 ,很棒!

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

print(confusion_matrix(test.sentiment, preds))

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

小结

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

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

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

祝深度学习愉快!

征稿

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

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

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

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

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

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

谢谢!

延伸阅读

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

题图:Photo by Harley-Davidson on Unsplash


08-26 01:01