![](https://github.com/wshuyi/github_pub_img/raw/master/assets/2019-05-20-17-42-13-342552.jpg?imageView2/2/w/1120/q/40/interlace/1/ignore-error/1)
10余行代码,借助 BERT 轻松完成多标签(multi-label)文本分类任务。
疑问
之前我写了《如何用 Python 和 BERT 做中文文本二元分类?》一文,为你讲解过如何用 BERT 语言模型和迁移学习进行文本分类。
不少读者留言询问:
王老师,难道 BERT 只能支持二元分类吗?
当然不是。
BERT 是去年以来非常流行的语言模型(包括 ELMO, Ulmfit, BERT, Ernie, GPT-2等)的一种,长期霸榜,非常强悍。
![](https://github.com/wshuyi/github_pub_img/raw/master/assets/2019-05-20-17-42-13-697977.jpg?imageView2/2/w/1120/q/40/interlace/1/ignore-error/1)
研究者已经证明,它可以很好地处理多种自然语言处理任务。甚至在部分任务上,超越了人类水平。
![](https://github.com/wshuyi/github_pub_img/raw/master/assets/2019-05-20-17-42-13-971483.jpg?imageView2/2/w/1120/q/40/interlace/1/ignore-error/1)
它处理自然语言任务,靠的是迁移学习的威力。
复习一下,我在《如何用 Python 和深度迁移学习做文本分类?》一文里,给你讲过迁移学习的范例 ULMfit (Universal language model fine-tuning for text classification)。
![](https://github.com/wshuyi/github_pub_img/raw/master/assets/2019-05-20-17-42-14-503068.jpg?imageView2/2/w/1120/q/40/interlace/1/ignore-error/1)
其原理就是首先让一个深度神经网络在海量文本上自监督学习(self-supervised learning)。
自监督和非监督(unsupervised)学习的区别,在于自监督学习,实际上是有标记的。
例如我们找到大量的语料,把常出现的词语放在一起,配对成(输入,输出)格式,例如(France, Paris)。这里 Paris 就可以看做是 France 的标记。然后学习的方式跟监督学习没有差别。
这也是著名的 word2vec 训练方式。
![](https://github.com/wshuyi/github_pub_img/raw/master/assets/2019-05-20-17-42-14-854222.jpg?imageView2/2/w/1120/q/40/interlace/1/ignore-error/1)
那问题来了,为什么不干脆叫监督学习?
因为监督学习,往往是指需要人工参与打标记的。例如你已经熟悉的情感分类任务,都是人阅读以后标记1或者0的。
可在语言模型这里,你利用了语料自身中词语的自然位置关系,没有主动人工打标记。所以为了区分,我们叫它“自监督学习”。
经过足够长时间的训练,这个神经网络就学会了该领域语言的特性。
然后,我们给这个神经网络,加上一个头部,就可以让它来完成特定的目标。
加上全连接层作为分类器,就可以把输入文本做分类(classification),例如我们讲解过的情感分析。
![](https://github.com/wshuyi/github_pub_img/raw/master/assets/2019-05-20-17-42-15-439772.jpg?imageView2/2/w/1120/q/40/interlace/1/ignore-error/1)
加上解码器(decoder),就可以把输入文本序列,转换成另一种序列。这就可以完成文本翻译、问答,甚至是文本转语音。
![](https://github.com/wshuyi/github_pub_img/raw/master/assets/2019-05-20-17-42-15-666389.jpg?imageView2/2/w/1120/q/40/interlace/1/ignore-error/1)
加上生成器(generator),例如卷积神经网络,就可以把序列转换成多层矩阵。这样,机器就可以根据你的文字输入,为你输出对应的图像来。
![](https://github.com/wshuyi/github_pub_img/raw/master/assets/2019-05-20-17-42-15-948108.jpg?imageView2/2/w/1120/q/40/interlace/1/ignore-error/1)
我以二元分类任务举例,仅仅是因为它足够简单,便于说明。
你完全可以举一反三,直接使用它来做多类别(multi-class)分类任务,例如三分类情感分析(正向、负向、中性)之类。
这时候,《如何用 Python 和 BERT 做中文文本二元分类?》一文中其他的代码,都是可以复用的。
你只需要调整一下测量指标(Evaluation Metrics)。
例如说,f1 分数专门针对二分类。你用它衡量多分类任务,程序会无所适从。
把它删除,或者替换成 micro f 或者 macro f 分数,就好了。
本文,我们来看看其他同学提出的这个更有挑战性的问题:
老师,BERT 能否做多标签(multi-label)分类?
多标签
先来解释一下,什么叫做多标签(multi-label)文本分类问题。
这里咱们结合一个 Kaggle 上的竞赛实例。
竞赛的名字叫做:恶毒评论分类挑战(Toxic Comment Classification Challenge),链接在这里。
![](https://github.com/wshuyi/github_pub_img/raw/master/assets/2019-05-20-17-42-16-213798.jpg?imageView2/2/w/1120/q/40/interlace/1/ignore-error/1)
这个竞赛的数据,取自真实的网络评论。
![](https://github.com/wshuyi/github_pub_img/raw/master/assets/2019-05-20-17-42-16-695748.jpg?imageView2/2/w/1120/q/40/interlace/1/ignore-error/1)
除了序号和原始文本以外,每行数据都包含了6个维度的标注,分别是:
- toxic(恶毒)
- severe_toxic(非常恶毒)
- obscene(污言秽语)
- threat(威胁)
- insult(侮辱)
- identity_hate(憎恨)
这就是我们的任务:
对于一个样本,需要同时在6个不同维度上判断它是否属于该标签范畴。
我觉得这个竞赛的初衷非常好。
因为网上恶毒评论过多,会降低用户高质量内容贡献度,让社区变得沉寂。
而人工处理,显然效率和速度都不理想,而且成本过高。
用机器自动甄别,可以第一时间直接屏蔽恶毒评论,有助于打造良好的网络社群环境和讨论氛围。
并且,成本还很低。
你可以很容易看出,这种多标签标注和多元分类标注的区别。
多元分类任务里面,分类互斥。一个样本属于某种分类,不能同时属于另一种分类。
例如一条评论,不能同时属于“正向”或者“负向”情感。
一张图片,不能同时属于“哆啦A梦”或者“瓦力”。
但是这个多标签分类例子里面,我们不难看出,一个“非常恶毒”的评论,同时也必定是“恶毒”的评论。
因此一个样本,可能同时属于上述两种,甚至全部六种类别。
当然,也有可能不属于任何一种类别。
了解了任务后,下一个问题自然是:怎么做?
最简单的偷懒办法,是分别建立6个独立的模型。
第一个模型,判断是否“恶毒”。
……
最后一个模型,判断是否“憎恨”。
这样一来,我们就可以把一个多标签分类问题,转化成6个二元分类问题。
解决了?
对。
很多论文,就是这么处理多标签分类任务的。
这样做有问题吗?
有。
因为6个独立模型,可能会判断出某条评论“非常恶毒”的同时,却认为它不“恶毒”。
这显然是个荒唐结论。
但既然模型是独立的,哪里管得了这么多?
好在,多标签分类任务,其实是可以只用一个模型来解决的。
一个模型的好处有很多。
例如可以对上述荒唐结论进行惩罚(penalize),从而让机器避免得出这样不合乎逻辑的判定结果。
而且,可以节省大量的时间、存储和计算资源。
本文,我们就讨论如何基于 BERT ,构造这样的多标签分类模型。
发现
本来,我是打算在之前 BERT 二元分类代码的基础上,实现多标签分类功能,然后把代码和教程提供给你的。
再次强调,我做的工作主要是简化(而非从头撰写)代码,使得你可以利用它学习,以及替换成你自己的数据来使用。
但是,现在正是 Tensorflow 大版本切换的过渡期。
之前分享的 BERT 二元分类原始代码采用 Tensorflow 1.X 代码编写,底层代码处理起来非常麻烦。
而且一旦 2.0 正式版推出,之前写好的 1.X 版代码需要大幅修改,甚至重来。
这种西西弗斯推石头般的无用功,让人望而却步。
这时,有人捷足先登了。
完成这件事的,就是我的 LinkedIn 好友 Kaushal Trivedi 。
![](https://github.com/wshuyi/github_pub_img/raw/master/assets/2019-05-20-17-42-17-102758.jpg?imageView2/2/w/1120/q/40/interlace/1/ignore-error/1)
早在今年1月份,他就在 medium 发布了关于 BERT 多标签分类的文章。
![](https://github.com/wshuyi/github_pub_img/raw/master/assets/2019-05-20-17-42-17-570798.jpg?imageView2/2/w/1120/q/40/interlace/1/ignore-error/1)
那一篇文章的配套代码,他是直接基于 PyTorch 撰写的,包含了大量底层细节。
对于应用来说,源代码包含底层细节过多,可不是什么好事儿。
因为这意味着以下几个特点:
- 代码很长
- 为了适应自己的任务,找需要修改的地方很麻烦
- 出错之后,不容易检查
这就是为什么软件工程会强调封装。
所谓封装,就是把已经通过反复测试的内容包裹起来。只在更高层次上,让开发者或者用户跟输入输出接口打交道。
这样可以避免重复造轮子,而且更不容易发生错误。
受 fast.ai 的启发,Kaushal Trivedi 做了一个新的项目,叫做 fast-bert 。
看,连名字都向 fast.ai 致敬了。
这次的代码简洁明快多了。
Kaushal Trivedi 还专门写了一篇文章,讲述了如何用 fast-bert 来进行多标签分类。用的样例就是咱们刚才提到的恶毒评论分类数据。
![](https://github.com/wshuyi/github_pub_img/raw/master/assets/2019-05-20-17-42-17-930502.jpg?imageView2/2/w/1120/q/40/interlace/1/ignore-error/1)
然而,由于这个软件包刚刚研发出来,所以坑非常多,包括但不限于:
- 文章内的代码不完整
- Github 上的样例 ipynb 文件需要特定底层 Linux 编译软件包支持
- 样例数据过大,导致执行时间过长
- Colab 上执行,会出现内存耗尽报错
- ……
把所有坑都踩过来之后,我觉得还是有必要整理出一个可以在 Google Colab 上让你直接执行,并且可以套用自己数据的版本。
毕竟,我们都喜欢免费的 GPU,对吧?
现在,我已经完成了这项工作。
这篇文章就将成果分享给你。
数据
如果你使用恶毒评论分类数据全集的话,训练数据有十几万条。
即便用上了 Colab 的 GPU ,执行起来也会花费好几个小时的时间。
顺便说一句,Colab 的免费 GPU 最近升级了,已经从原来速度慢、内存容量小的 K80,换成了 Tesla T4 。
![](https://github.com/wshuyi/github_pub_img/raw/master/assets/2019-05-20-17-42-18-191185.jpg?imageView2/2/w/1120/q/40/interlace/1/ignore-error/1)
这里为了让你快速看到运行结果,我对数据进行了采样。
目前的训练集包含 4000 条数据,验证集只有 1000 条。比起原始数据,这只是不到20分之一而已。
同样,对于测试集,我也只采样了 1000 条。
这样做,会有不利的影响,那就是分类效果会降低。
请记住现在的结果,是在数据相对较少的基础上训练出来的。因此结果如果不理想,并不能代表 BERT 的能力不够强。
环境
本文的配套源代码,我放在了 Github 上。链接获取方式请见本文末尾。
![](https://github.com/wshuyi/github_pub_img/raw/master/assets/2019-05-20-17-42-18-832028.jpg?imageView2/2/w/1120/q/40/interlace/1/ignore-error/1)
如果你对我的教程满意,欢迎在页面右上方的 Star 上点击一下,帮我加一颗星。谢谢!
注意这个页面的中央,有个按钮,写着“在 Colab 打开”(Open in Colab)。请你点击它。
然后,Google Colab 就会自动开启。
![](https://github.com/wshuyi/github_pub_img/raw/master/assets/2019-05-20-17-42-19-168073.jpg?imageView2/2/w/1120/q/40/interlace/1/ignore-error/1)
我建议你点一下上图中红色圈出的 “COPY TO DRIVE” 按钮。这样就可以先把它在你自己的 Google Drive 中存好,以便使用和回顾。
![](https://github.com/wshuyi/github_pub_img/raw/master/assets/2019-05-20-17-42-19-633181.jpg?imageView2/2/w/1120/q/40/interlace/1/ignore-error/1)
Colab 为你提供了全套的运行环境。你只需要依次执行代码,就可以复现本教程的运行结果了。
如果你对 Google Colab 不熟悉,没关系。我这里有一篇教程,专门讲解 Google Colab 的特点与使用方式。
为了你能够更为深入地学习与了解代码,我建议你在 Google Colab 中开启一个全新的 Notebook ,并且根据下文,依次输入代码并运行。在此过程中,充分理解代码的含义。
这种看似笨拙的方式,其实是学习的有效路径。
代码
为了让你把注意力集中在重要的环节,我这里把全部的准备工作都集中在了第一个代码段落,并且隐藏了其内容。
![](https://github.com/wshuyi/github_pub_img/raw/master/assets/2019-05-20-17-42-19-990189.jpg?imageView2/2/w/1120/q/40/interlace/1/ignore-error/1)
如果你需要查看和修改,只需要点击该代码段即可。
![](https://github.com/wshuyi/github_pub_img/raw/master/assets/2019-05-20-17-42-20-287291.jpg?imageView2/2/w/1120/q/40/interlace/1/ignore-error/1)
或者,你现在可以忽略并直接执行它。这大概需要花几分钟的时间。因为有个底层的软件包需要编译,才能支持 fast-bert 软件包。
![](https://github.com/wshuyi/github_pub_img/raw/master/assets/2019-05-20-17-42-20-751032.jpg?imageView2/2/w/1120/q/40/interlace/1/ignore-error/1)
下面,才是咱们要关注和讲解的部分。
首先,我们把数据下载下来。
!git clone https://github.com/wshuyi/demo-multi-label-classification-bert.git
![](https://github.com/wshuyi/github_pub_img/raw/master/assets/2019-05-20-17-42-21-236473.jpg?imageView2/2/w/1120/q/40/interlace/1/ignore-error/1)
注意这里包含的数据,不只有采样版本,也包含了原始数据。
你在尝试过本教程后,也可以重新载入原始数据,看模型效果是否会有显著提升。
之后,是咱们的主角 fast-bert 登场。
!pip install fast-bert
![](https://github.com/wshuyi/github_pub_img/raw/master/assets/2019-05-20-17-42-21-697980.jpg?imageView2/2/w/1120/q/40/interlace/1/ignore-error/1)
我们需要从 fast-bert 以及它依赖的软件包 pytorch_pretrained_bert
读入一些预置函数。
from fast_bert.data import *
from fast_bert.learner import *
from fast_bert.metrics import *
from pytorch_pretrained_bert.tokenization import BertTokenizer
之后,是参数设定。
DATA_PATH = Path('demo-multi-label-classification-bert/sample/data/')
LABEL_PATH = Path('demo-multi-label-classification-bert/sample/labels/')
BERT_PRETRAINED_MODEL = "bert-base-uncased"
args["do_lower_case"] = True
args["train_batch_size"] = 16
args["learning_rate"] = 6e-5
args["max_seq_length"] = 512
args["fp16"] = True
这里为你解释一下各项参数的含义:
DATA_PATH
:数据路径。包含训练、验证和测试集的csv文件。LABEL_PATH
:标记路径。注意它只是把所有标记的类别每个一行,写在了一个 csv 中,短小精悍。BERT_PRETRAINED_MODEL
:使用的预训练模型。我们这里使用的是英文不分大小写版本bert-base-uncased
。args["do_lower_case"]
:数据处理中是否全部转换小写。这里设定为“是”。args["train_batch_size"]
:训练集批次大小。这里设定为16。如果设定为32的话,Colab 的 GPU 会报告内存溢出错误。args["learning_rate"]
:学习速率。args["max_seq_length"]
:最大序列长度。这里我们设定为512。当然如果你处理 Twitter 数据,140就够了。args["fp16"]
:以16位浮点精度来进行运算。可以加快运算速度,节省存储空间。
下面我们从预训练模型中,获得数据处理器。
tokenizer = BertTokenizer.from_pretrained(BERT_PRETRAINED_MODEL,
do_lower_case=args['do_lower_case'])
把全部的标签类别输入到列表中。
label_cols = ["toxic", "severe_toxic", "obscene", "threat", "insult", "identity_hate"]
终于可以正式读取数据了。
databunch = BertDataBunch(DATA_PATH, LABEL_PATH, tokenizer, train_file='train.csv', val_file='valid.csv',
test_data='test.csv', label_file="labels.csv",
text_col="comment_text", label_col=label_cols,
bs=args['train_batch_size'], maxlen=args['max_seq_length'],
multi_gpu=multi_gpu, multi_label=True)
这里填充的参数,基本上都可以通过其名称直接了解含义。所以我这里只给你讲解以下几个重点:
text_col
是指训练集、验证集和测试集里面,文本所在那一列的表头名称。multi_gpu
是指要不要使用多 GPU 并行运算。这里前面代码已经自动获取了取值,你不需要修改它。multi_label
说明了咱们要进行的是多标签分类任务。
读取后的数据,存在了 databunch
中。模型可以直接使用。
我们指定模型效果测量标准。
metrics = [{'name': 'accuracy', 'function': accuracy_multilabel}]
因为是多标签分类,所以我们用的是准确率衡量指标是 accuracy_multilabel
。
我们把当前的参数设置,存入到日志记录器中。
logger.info(args)
开始构造模型了。
learner = BertLearner.from_pretrained_model(databunch, BERT_PRETRAINED_MODEL, metrics, device, logger,
is_fp16=args['fp16'], loss_scale=args['loss_scale'],
multi_gpu=multi_gpu, multi_label=True)
![](https://github.com/wshuyi/github_pub_img/raw/master/assets/2019-05-20-17-42-22-091049.jpg?imageView2/2/w/1120/q/40/interlace/1/ignore-error/1)
因为指定了 multi_label=True
,程序会自己构造模型的头部,以便正确处理多标签分类任务。
训练开始。
这里我们设定跑4个周期(cycle)。
learner.fit(4, lr=args['learning_rate'], schedule_type="warmup_linear")
根据 BERT 的设定,训练中间学习速率是要进行变化的。我们设定变化方式为 warmup_linear
。
它将在每一个周期中,把学习速率按类似下图这样的方式进行调整:
![](https://github.com/wshuyi/github_pub_img/raw/master/assets/2019-05-20-17-42-22-318821.jpg?imageView2/2/w/1120/q/40/interlace/1/ignore-error/1)
运行结果如下:
![](https://github.com/wshuyi/github_pub_img/raw/master/assets/2019-05-20-17-42-22-879525.jpg?imageView2/2/w/1120/q/40/interlace/1/ignore-error/1)
4轮周期跑下来,模型在验证集准确率达到了 0.993 。这就意味着平均每 1000 个样本,多标签分类准确数量 993 个。
这个结果怎么样?
够不够好?为什么?
这个问题作为今天的思考题。欢迎你把自己的想法记录下来写在留言区,咱们一起交流讨论。
小结
通过阅读本文,希望你已经掌握了以下知识点:
- 除二元分类外,语言模型(例如 BERT )的其他应用场景
- 多类别(multi-class)分类和多标签(multi-label)分类的区别
- 自监督学习(self-supervised learning)的概念
- 多标签分类的独立模型转化法
- 使用 BERT 单模型进行多标签分类
希望这些知识和技能,可以帮助你解决研究和工作中遇到的实际问题。
祝深度学习愉快!
延伸阅读
你可能也会对以下话题感兴趣。点击链接就可以查看。
- 如何高效学 Python ?
- 《文科生数据科学上手指南》分享
- 如何用 Python 和 fast.ai 做图像深度迁移学习?
- 如何用 Python 和深度迁移学习做文本分类?
- 如何用 Python 和 BERT 做中文文本二元分类?
喜欢别忘了点赞。
还可以微信关注我的公众号“玉树芝兰”(nkwangshuyi),别忘了加星标,以免错过新推送提示。
“赞赏”一下,请我喝杯咖啡,就更好了。
如果你对 Python 与数据科学感兴趣,不妨阅读我的系列教程索引贴《如何高效入门数据科学?》,里面还有更多的有趣问题及解法。
题图:Photo by Pro Church Media on Unsplash
代码链接获取方法
第一步,微信关注公众号“玉树芝兰”(nkwangshuyi)。
第二步,在后台回复“bert”(注意大小写)。