鱼我所欲也,熊掌亦我所欲也。
![640?wx_fmt=jpeg](https://i-blog.csdnimg.cn/blog_migrate/1c15510a9783eead862a42219ee4788c.jpeg)
痛点
我之前用 BERT ,就没有痛快过。
最初,是 Google 发布的原始 Tensorflow 代码,一堆堆参数,一行行代码,扑面而来。让人看着,就眼晕。
后来,Google 把 BERT 在 Tensorflow Hub 上面打了个包。
还是需要很多行代码,许多参数设置,才能让它学习你自己提供的数据。不过我还是很兴奋地帮你重构代码,搞了个十行代码可执行版本。
![640?wx_fmt=jpeg](https://i-blog.csdnimg.cn/blog_migrate/15ded216665e8b218d2b75618335dcd1.jpeg)
但这其实,不过是隐藏了大量细节而已。
那些繁琐的代码,依然在那里。
代码越多,修改和维护就越困难。
你看人家 fast.ai ,需要什么功能,只要找到对应的 API ,输入三样东西:
一般而言,只需要几行代码。
然后,结果就出来了。
![640?wx_fmt=jpeg](https://i-blog.csdnimg.cn/blog_migrate/861733eb9c7d8659740bdb864fea70ee.jpeg)
这样,你可以很轻易尝试自己的想法,并且在不同数据集上面加以验证。
这种快速迭代反馈,对于机器学习研究来说,是非常有益处的。
因此,当 Huggingface 的 Kaushal Trivedi 真的起心动念,仿照 fast.ai 搞了个 fastbert 时,我特别开心。
于是又写了份教程,教你如何用它来做多标签文本分类。
但是,这个 fastbert ,与 fast.ai 比起来,差别还是太大了。
首先是设置起来依旧繁琐,你得照顾到许多参数;
其次是维护并不及时。两次更新之间的时间,竟然可能相差一个月;
![640?wx_fmt=jpeg](https://i-blog.csdnimg.cn/blog_migrate/79f3e73417344019fd4d0c52e461916d.jpeg)
第三是缺乏文档和样例。这对于新手,是非常不友好的。
几乎所有遭遇到这些问题的人,都在问一个问题:
什么时候,才能在 fast.ai 里面,方便地调用 BERT 来完成自然语言处理任务呢?
大部分人,只是动动念头,然后继续将就着使用 fast.ai 提供的 ULMfit 来处理英文文本。
毕竟,对于英文、波兰文来说,效果也不错。
可是,中文怎么办?
ULMfit 推出1年多了,至今却没有一个公开发布、广泛使用的中文预训练模型。
这真是令人烦恼的事儿啊!
黑客
有需求,也就有人来琢磨解决。
fast.ai 结合 BERT 这问题,对研究者的要求,着实不低。至少包括:
此外,还得能够融汇贯通,把二者结合起来,这就需要对 PyTorch 的熟练掌握。
能做出这样工作的人,大约就算是黑客了。
幸好,这样的高人真的出手了。
他就是卡内基梅隆大学的研究生,Keita Kurita。
![640?wx_fmt=jpeg](https://i-blog.csdnimg.cn/blog_migrate/18e0d7b590ac2b87ac2a0b1b4509eb2c.jpeg)
Keita 有个博客,叫 Machine Learning Explained ,干货非常多,这里一并推荐给你。
他写的 fast.ai 结合 BERT 的英文教程,地址在这里。
![640?wx_fmt=jpeg](https://i-blog.csdnimg.cn/blog_migrate/1cc2d0dce933f54db6ba1f2b99a6976d.jpeg)
其实,有时候问题的解决,就如同窗户纸,一捅就破。
Keita 并没有尝试“重新发明轮子”,即从头去解决这个问题。
他只是巧妙地借用了第三方的力量,并且将其与 fast.ai 融合起来。
这个第三方,就是咱们前面提过的 Huggingface 。
![640?wx_fmt=jpeg](https://i-blog.csdnimg.cn/blog_migrate/cd0312f0fa58490dce6f104334a9f9f6.jpeg)
自从 BERT 的 Tensorflow 源代码经由 Google 发布出来,他们就在 Github 上面,搞了一个 PyTorch 版本的克隆。
![640?wx_fmt=jpeg](https://i-blog.csdnimg.cn/blog_migrate/48b06485bcf3829a22c715da39c087f5.jpeg)
这个克隆,还包含了预训练的结果。
也就是说,他们提供了一个完整版的模型架构,只要配上相应的数据和损失函数, fast.ai 就可以开工了!
fast.ai 文本处理一直不支持中文,是因为它其实也调用了第三方库,就是咱们介绍过的 Spacy 。
到今天为止, Spacy 也并不能完整支持中文处理,这就导致了 fast.ai 对中文无能为力。
![640?wx_fmt=jpeg](https://i-blog.csdnimg.cn/blog_migrate/eb5210dbdb043d8db315df685d675fd9.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](https://i-blog.csdnimg.cn/blog_migrate/bfacc0964061efd8deeac3a853eab8a7.jpeg)
蒙天放这篇知乎教程,更讲解了如何处理中文数据分类。
看起来,我似乎没有必要再写一篇教程了。
然而环境是在变化的。
Huggingface 现在,已经不仅仅做 BERT 预训练模型的 PyTorch 克隆了。
他们居然希望把所有的 Transformer 模型,全都搞一遍。
于是把原先的 Github 项目“pytorch-pretrained-BERT”,改成了“pytorch-transformers”这样一个野心勃勃的名字。
![640?wx_fmt=jpeg](https://i-blog.csdnimg.cn/blog_migrate/370e2c9a08ef2e492d9eb7970095f2a5.jpeg)
新的项目地址在这里。
你的想象空间,也就可以因此而开启了。
能不能用这些基于 Transformer 的预训练模型,来做自己的下游任务呢?
一如既往, Huggingface 的技术还是那么过硬。
然而,提供的接口,还是那么繁琐。虽然用户手册比之前有了较大改进,可教程样例依然不够友好。
我于是在思考,既然老版本 BERT 预训练模型可以和 fast.ai 对接,那能否把新版本的各种 Transformer,也用这种方式简化调用呢?
如果这样做可以的话,咱们就不必再眼巴巴等着 Huggingface 改进教程与用户接口了,直接用上 fast.ai ,取两者的长处,结合起来就好了。
于是,我开始了尝试。
一试才发现,新版本“pytorch-transformers”的预训练模型,与老版本还有一些变化。倘若直接迁移代码,会报错的。
所以,这篇文章里,我从头到尾,为你提供一个在新版本“pytorch-transformers” 中 BERT 预训练模型上直接能用的样例,并且加以详细讲解。
这样一来,相信我踩了一遍的坑,你就可以躲开了。这可以大量节省你的时间。
同时,我也希望你能够以这个样例作为基础,真正做到举一反三,将其他的 Transformer 模型尝试迁移,并且把你的试验结果,分享给大家。
环境
本文的配套源代码,我放在了 Github 上。
你可以在我的公众号“玉树芝兰”(nkwangshuyi)后台回复“aibert”,查看完整的代码链接。
![640?wx_fmt=jpeg](https://i-blog.csdnimg.cn/blog_migrate/0a366e6716c528d939132560775f5346.jpeg)
如果你对我的教程满意,欢迎在页面右上方的 Star 上点击一下,帮我加一颗星。谢谢!
注意这个页面的中央,有个按钮,写着“在 Colab 打开”(Open in Colab)。请你点击它。
然后,Google Colab 就会自动开启。
![640?wx_fmt=jpeg](https://i-blog.csdnimg.cn/blog_migrate/6669931ac9aa4dd068458d772d755e98.jpeg)
我建议你点一下上图中红色圈出的 “COPY TO DRIVE” 按钮。这样就可以先把它在你自己的 Google Drive 中存好,以便使用和回顾。
![640?wx_fmt=jpeg](https://i-blog.csdnimg.cn/blog_migrate/4e05f2607d4a1e972998cee801c7d2d6.jpeg)
Colab 为你提供了全套的运行环境。你只需要依次执行代码,就可以复现本教程的运行结果了。
如果你对 Google Colab 不熟悉,没关系。我这里有一篇教程,专门讲解 Google Colab 的特点与使用方式。
为了你能够更为深入地学习与了解代码,我建议你在 Google Colab 中开启一个全新的 Notebook ,并且根据下文,依次输入代码并运行。在此过程中,充分理解代码的含义。
这种看似笨拙的方式,其实是学习的有效路径。
代码
首先提示一下,fast.ai 给我们提供了很多便利,例如你只需要执行下面这一行,许多数据科学常用软件包,就都已经默认读入了。
from fastai.text import *import *
因此,你根本就不需要执行诸如 import numpy as np
,import 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](https://i-blog.csdnimg.cn/blog_migrate/b2295d45d6df4bccbbd1b104ab17cee4.jpeg)
验证集:
len(valid)
![640?wx_fmt=jpeg](https://i-blog.csdnimg.cn/blog_migrate/ad3e19ecd418674de530b95ea6580e94.jpeg)
测试集:
len(test)
![640?wx_fmt=jpeg](https://i-blog.csdnimg.cn/blog_migrate/b2295d45d6df4bccbbd1b104ab17cee4.jpeg)
然后,来看看训练集前几行内容。
train.head()
![640?wx_fmt=jpeg](https://i-blog.csdnimg.cn/blog_migrate/ba82e0ff6c476189c1095e6ec7e47b08.jpeg)
数据预处理的第一步,已经做完了。
但是,我们都知道,机器学习模型是不认识中文的,我们必须做进一步的处理。
这时候,就需要 Huggingface 的 Transformers 预训练模型登场了。
!pip install pytorch-transformers
本文演示的是 BERT ,所以这里只需要读入两个对应模块。
一个是 Tokenizer ,用于把中文句子,拆散成一系列的元素,以便映射成为数字来表示。
一个是序列分类模块。在一堆 Transformer 的顶部,通过全连接层,来达到分类功能。
![640?wx_fmt=jpeg](https://i-blog.csdnimg.cn/blog_migrate/ba82e0ff6c476189c1095e6ec7e47b08.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](https://i-blog.csdnimg.cn/blog_migrate/a0a566b5302d52d8dc764f1797f9bbd7.jpeg)
我们检查一下,看预训练模型都认识哪些字。
这里我们随意选取从 2000 到 2005 位置上的 Token 来查看。
list(bert_tokenizer.vocab.items())[2000:2005]2005]
![640?wx_fmt=jpeg](https://i-blog.csdnimg.cn/blog_migrate/d545e4b26da2b7ca0002cec731660d6c.jpeg)
这里我们看到, BERT 还真是认识不少汉字的。
我们把全部的词汇列表存储起来,下面要用到。
bert_vocab = Vocab(list(bert_tokenizer.vocab.keys()))
注意 fast.ai 在 Tokenizer 环节,实际上设计的时候有些偷懒,采用了“叠床架屋”的方式。
![640?wx_fmt=jpeg](https://i-blog.csdnimg.cn/blog_migrate/4ff4071ce94d0acd36569b4b73082eee.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_bos
和 include_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](https://i-blog.csdnimg.cn/blog_migrate/2aaac4abeee82c601c937bb8f1cf1905.jpeg)
在 fast.ai 里面,正常出现了 BERT 风格的中文数据预处理结果,还是很令人兴奋的。
注意,前面我们指定了 pre_rules
和 post_rules
两个参数,都写成 []
。这是必要的。
我尝试了一下,如果按照默认值,不提这两个参数,那么二者默认都是 None 。这样一来,数据预处理结果就会成这样。
![640?wx_fmt=jpeg](https://i-blog.csdnimg.cn/blog_migrate/f9823a8c6ec2a9b80edfe12433667de8.jpeg)
这和我们需要的结果,不一致。所以此处需要留意。
第一个元素,数据有了。
下面我们来处理架构。
Huggingface 的网页上面介绍,说明了新的 Transformer 模型和原先版本的 BERT 预训练模型差异。
![640?wx_fmt=jpeg](https://i-blog.csdnimg.cn/blog_migrate/f9823a8c6ec2a9b80edfe12433667de8.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](https://i-blog.csdnimg.cn/blog_migrate/2aaac4abeee82c601c937bb8f1cf1905.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](https://i-blog.csdnimg.cn/blog_migrate/da2b2af4a0ee214abbf2f29902e1b772.jpeg)
读图以后,我们发现,最大学习速率的量级,应该在 e-5
上。这里我们设置成 2e-5
试试。
这里,只跑两个轮次,避免过拟合。
当然,也有省时间的考虑。
learn.fit_one_cycle(2, 2e-5)2e-5)
![640?wx_fmt=jpeg](https://i-blog.csdnimg.cn/blog_migrate/db34930940bdd858b128423277c1c8d3.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](https://i-blog.csdnimg.cn/blog_migrate/c93c80b4d1d530dde0e00b6856279d79.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](https://i-blog.csdnimg.cn/blog_migrate/a0a566b5302d52d8dc764f1797f9bbd7.jpeg)
f1-score 达到了 0.9 ,很棒!
再通过混淆矩阵,看看哪里出现判断失误。
print(confusion_matrix(test.sentiment, preds))
![640?wx_fmt=jpeg](https://i-blog.csdnimg.cn/blog_migrate/d545e4b26da2b7ca0002cec731660d6c.jpeg)
基于 BERT 的中文分类任务完成!
小结
通过这篇文章的学习,希望你掌握了以下知识点:
如前文所述,希望你举一反三,尝试把 Huggingface 推出的其他 Transformer 预训练模型与 fast.ai 结合起来。
欢迎你把尝试的结果在留言区分享给其他同学。
祝深度学习愉快!
征稿
SSCI 检索期刊 Information Discovery and Delivery 要做一期《基于语言机器智能的信息发现》( “Information Discovery with Machine Intelligence for Language”) 特刊(Special Issue)。
![640?wx_fmt=jpeg](https://i-blog.csdnimg.cn/blog_migrate/81385f88e902380437674039764b0048.jpeg)
本人是客座编辑(guest editor)之一。另外两位分别是:
![640?wx_fmt=jpeg](https://i-blog.csdnimg.cn/blog_migrate/81385f88e902380437674039764b0048.jpeg)
征稿的主题包括但不限于:
具体的征稿启事(Call for Paper),请查看 Emerald 期刊官网的这个链接(http://dwz.win/c2Q)。
作为本专栏的老读者,欢迎你,及你所在的团队踊跃投稿哦。
如果你不巧并不从事上述研究方向(机器学习、自然语言处理和计算语言学等),也希望你能帮个忙,转发这个消息给你身边的研究者,让他们有机会成为我们特刊的作者。
谢谢!
延伸阅读
你可能也会对以下话题感兴趣。点击链接就可以查看。
题图:Photo by Harley-Davidson on Unsplash