当Bert遇上Keras:这可能是Bert最简单的打开姿势

标签: bert keras bert | 发表时间:2019-06-21 18:31 | 作者:PaperWeekly
出处:https://www.jiqizhixin.com/

Bert 是什么,估计也不用笔者来诸多介绍了。虽然笔者不是很喜欢Bert,但不得不说,Bert 确实在 NLP 界引起了一阵轩然大波。现在不管是中文还是英文,关于 Bert 的科普和解读已经满天飞了,隐隐已经超过了当年 Word2Vec 刚出来的势头了。有意思的是,Bert 是 Google 搞出来的,当年的 word2vec 也是 Google 搞出来的,不管你用哪个,都是在跟着 Google 大佬的屁股跑。

Bert 刚出来不久,就有读者建议我写个解读,但我终究还是没有写。一来,Bert 的解读已经不少了,二来其实 Bert 也就是基于 Attention 搞出来的大规模语料预训练的模型,本身在技术上不算什么创新,而关于 Google 的 Attention 我已经写过解读了,所以就提不起劲来写了。

 Bert的预训练和微调(图片来自Bert的原论文)

总的来说,我个人对 Bert 一直也没啥兴趣,直到上个月末在做信息抽取比赛时,才首次尝试了 Bert。毕竟即使不感兴趣,终究也是得学会它,毕竟用不用是一回事,会不会又是另一回事。再加上在 Keras 中使用(fine tune)Bert,似乎还没有什么文章介绍,所以就分享一下自己的使用经验。

当Bert遇上Keras

很幸运的是,已经有大佬封装好了 Keras 版的 Bert,可以直接调用官方发布的预训练权重,对于已经有一定 Keras 基础的读者来说,这可能是最简单的调用 Bert 的方式了。所谓“站在巨人的肩膀上”,就是形容我们这些 Keras 爱好者此刻的心情了。 

keras-bert

个人认为,目前在 Keras 下对 Bert 最好的封装是: 

keras-bert:

https://github.com/CyberZHG/keras-bert 

本文也是以此为基础的。 顺便一提的是,除了 keras-bert 之外,CyberZHG 大佬还封装了很多有价值的 keras 模块,比如 keras-gpt-2(你可以用像用 Bert 一样用 GPT2 模型了)、keras-lr-multiplier(分层设置学习率)、keras-ordered-neurons(就是前不久介绍的 ON-LSTM)等等。看来也是一位 Keras 铁杆粉丝,致敬大佬。

汇总可以看:

https://github.com/CyberZHG/summary 

事实上,有了 keras-bert 之后,再加上一点点 Keras 基础知识,而且 keras-bert 所给的 demo 已经足够完善,调用、微调 Bert 都已经变成了意见没有什么技术含量的事情了。所以后面笔者只是给出几个中文的例子,来让读者上手 keras-bert 的基本用法。 

Tokenizer

正式讲例子之前,还有必要先讲一下 Tokenizer 相关内容。我们导入 Bert 的 Tokenizer 并重构一下它:

from keras_bert import load_trained_model_from_checkpoint, Tokenizer
import codecs

config_path = '../bert/chinese_L-12_H-768_A-12/bert_config.json'
checkpoint_path = '../bert/chinese_L-12_H-768_A-12/bert_model.ckpt'
dict_path = '../bert/chinese_L-12_H-768_A-12/vocab.txt'

token_dict = {}
with codecs.open(dict_path, 'r', 'utf8') as reader:
    for line in reader:
        token = line.strip()
        token_dict[token] = len(token_dict)

class OurTokenizer(Tokenizer):
    def _tokenize(self, text):
        R = []
        for c in text:
            if c in self._token_dict:
                R.append(c)
            elif self._is_space(c):
                R.append('[unused1]') # space类用未经训练的[unused1]表示
            else:
                R.append('[UNK]') # 剩余的字符是[UNK]
        return R

tokenizer = OurTokenizer(token_dict)
tokenizer.tokenize(u'今天天气不错')
# 输出是 ['[CLS]', u'今', u'天', u'天', u'气', u'不', u'错', '[SEP]']

这里简单解释一下 Tokenizer 的输出结果。首先,默认情况下,分词后句子首位会分别加上 [CLS] 和 [SEP] 标记,其中 [CLS] 位置对应的输出向量是能代表整句的句向量(反正 Bert 是这样设计的),而 [SEP] 则是句间的分隔符,其余部分则是单字输出(对于中文来说)。

本来 Tokenizer 有自己的 _tokenize 方法,我这里重写了这个方法,是要保证 tokenize 之后的结果,跟原来的字符串长度等长(如果算上两个标记,那么就是等长再加 2)。 Tokenizer 自带的 _tokenize 会自动去掉空格,然后有些字符会粘在一块输出,导致 tokenize 之后的列表不等于原来字符串的长度了,这样如果做序列标注的任务会很麻烦。

而为了避免这种麻烦,还是自己重写一遍好了。主要就是用 [unused1] 来表示空格类字符,而其余的不在列表的字符用 [UNK] 表示,其中 [unused*] 这些标记是未经训练的(随即初始化),是 Bert 预留出来用来增量添加词汇的标记,所以我们可以用它们来指代任何新字符。

三个例子

这里包含 keras-bert 的三个例子,分别是文本分类、关系抽取和主体抽取,都是在官方发布的预训练权重基础上进行微调来做的。 

Bert官方Github:https://github.com/google-research/bert 

官方的中文预训练权重:https://storage.googleapis.com/bert_models/2018_11_03/chinese_L-12_H-768_A-12.zip

例子所在Github:https://github.com/bojone/bert_in_keras/ 

根据官方介绍,这份权重是用中文维基百科为语料进行训练的。 

文本分类

作为第一个例子,我们做一个最基本的文本分类任务,熟悉做这个基本任务之后,剩下的各种任务都会变得相当简单了。这次我们以之前已经讨论过多次的文本感情分类任务 [1] 为例,所用的标注数据 [2] 也是以前所整理的。 

让我们来看看模型部分全貌,完整代码见:

https://github.com/bojone/bert_in_keras/blob/master/sentiment.py

bert_model = load_trained_model_from_checkpoint(config_path, checkpoint_path)

for l in bert_model.layers:
    l.trainable = True

x1_in = Input(shape=(None,))
x2_in = Input(shape=(None,))

x = bert_model([x1_in, x2_in])
x = Lambda(lambda x: x[:, 0])(x) # 取出[CLS]对应的向量用来做分类
p = Dense(1, activation='sigmoid')(x)

model = Model([x1_in, x2_in], p)
model.compile(
    loss='binary_crossentropy',
    optimizer=Adam(1e-5), # 用足够小的学习率
    metrics=['accuracy']
)
model.summary()

在 Keras 中调用 Bert 来做情感分类任务就这样写完了。

是不是感觉还没有尽兴,模型代码就结束了?Keras 调用 Bert 就这么简短。事实上,真正调用 Bert 的也就只有 load_trained_model_from_checkpoint 那一行代码,剩下的只是普通的 Keras 操作(再次感谢 CyberZHG 大佬)。所以,如果你已经入门了 Keras,那么调用 Bert 是无往不利啊。 

如此简单的调用,能达到什么精度?经过5个epoch的fine tune后,验证集的最好准确率是95.5%+!之前我们在《文本情感分类(三):分词 OR 不分词》[1] 中死调烂调,也就只有 90% 上下的准确率;而用了 Bert 之后,寥寥几行,就提升了 5 个百分点多的准确率!也难怪 Bert 能在 NLP 界掀起一阵热潮。

在这里,用笔者的个人经历先回答读者可能关心的两个问题。 

第一个问题应该是大家都很关心的,那就是“要多少显存才够?”。事实上,这没有一个标准答案,显存的使用取决于三个因素:句子长度、batch size、模型复杂度。像上面的情感分析例子,在笔者的 GTX1060 6G 显存上也能跑起来,只需要将 batch size 调到 24 即可。

所以,如果你的显存不够大,将句子的 maxlen 和 batch size 都调小一点试试。当然,如果你的任务太复杂,再小的 maxlen 和 batch size 也可能 OOM,那就只有升级显卡了。 

第二个问题是“有什么原则来指导 Bert 后面应该要接哪些层?”。答案是:用尽可能少的层来完成你的任务。

比如上述情感分析只是一个二分类任务,你就取出第一个向量然后加个 Dense(1) 就好了,不要想着多加几层 Dense,更加不要想着接个 LSTM 再接 Dense;如果你要做序列标注(比如 NER),那你就接个 Dense+CRF 就好,也不要多加其他东西。

总之,额外加的东西尽可能少。一是因为 Bert 本身就足够复杂,它有足够能力应对你要做的很多任务;二来你自己加的层都是随即初始化的,加太多会对 Bert 的预训练权重造成剧烈扰动,容易降低效果甚至造成模型不收敛。

关系抽取

假如读者已经有了一定的 Keras 基础,那么经过第一个例子的学习,其实我们应该已经完全掌握了 Bert 的 fine tune 了,因为实在是简单到没有什么好讲了。所以,后面两个例子主要是提供一些参考模式,让读者能体会到如何“用尽可能少的层来完成你的任务”。 

在第二个例子中,我们介绍基于 Bert 实现的一个极简的关系抽取模型,其标注原理跟《基于 DGCNN 和概率图的轻量级信息抽取模型》[3] 介绍的一样,但是得益于 Bert 强大的编码能力,我们所写的部分可以大大简化。

在笔者所给出的一种参考实现中,模型部分如下,完整模型见:

https://github.com/bojone/bert_in_keras/blob/master/relation_extract.py

t = bert_model([t1, t2])
ps1 = Dense(1, activation='sigmoid')(t)
ps2 = Dense(1, activation='sigmoid')(t)

subject_model = Model([t1_in, t2_in], [ps1, ps2]) # 预测subject的模型


k1v = Lambda(seq_gather)([t, k1])
k2v = Lambda(seq_gather)([t, k2])
kv = Average()([k1v, k2v])
t = Add()([t, kv])
po1 = Dense(num_classes, activation='sigmoid')(t)
po2 = Dense(num_classes, activation='sigmoid')(t)

object_model = Model([t1_in, t2_in, k1_in, k2_in], [po1, po2]) # 输入text和subject,预测object及其关系


train_model = Model([t1_in, t2_in, s1_in, s2_in, k1_in, k2_in, o1_in, o2_in],
                    [ps1, ps2, po1, po2])

如果读者已经读过《基于 DGCNN 和概率图的轻量级信息抽取模型》一文 [3],了解到不用 Bert 时的模型架构,那么就会理解到上述实现是多么的简介明了。 

可以看到,我们引入了 Bert 作为编码器,然后得到了编码序列 t,然后直接接两个 Dense(1),这就完成了 subject 的标注模型;接着,我们把传入的 s 的首尾对应的编码向量拿出来,直接加到编码向量序列 t 中去,然后再接两个 Dense(num_classes),就完成 object 的标注模型(同时标注出了关系)。 

这样简单的设计,最终 F1 能到多少?答案是:线下 dev 能接近 82%,线上我提交过一次,结果是 85%+(都是单模型)!

相比之下,《基于 DGCNN 和概率图的轻量级信息抽取模型》[3] 中的模型,需要接 CNN,需要搞全局特征,需要将 s 传入到 LSTM 进行编码,还需要相对位置向量,各种拍脑袋的模块融合在一起,单模型也只比它好一点点(大约 82.5%)。

要知道,这个基于 Bert 的简单模型我只写了一个小时就写出来了,而各种技巧和模型融合在一起的 DGCNN 模型,我前前后后调试了差不多两个月!Bert 的强悍之处可见一斑。

注:这个模型的 fine tune 最好有 8G 以上的显存。另外,因为我在比赛即将结束的前几天才接触的 Bert,才把这个基于 Bert 的模型写出来,没有花心思好好调试,所以最终的提交结果并没有包含 Bert。

用 Bert 做关系抽取的这个例子,跟前面情感分析的简单例子,有一个明显的差别是学习率的变化。情感分析的例子中,只是用了恒定的学习率训练了几个 epoch,效果就还不错了。

在关系抽取这个例子中,第一个 epoch 的学习率慢慢从 0 增加到(这样称为 warmup),第二个 epoch 再从降到,总的来说就是先增后减,Bert 本身也是用类似的学习率曲线来训练的,这样的训练方式比较稳定,不容易崩溃,而且效果也比较好。

事件主体抽取

最后一个例子来自 CCKS 2019 面向金融领域的事件主体抽取 [4],这个比赛目前还在进行,不过我也已经没有什么动力和兴趣做下去了,所以放出我现在的模型(准确率为 89%+)供大家参考,祝继续参赛的选手取得更好的成绩。 

简单介绍一下这个比赛的数据,大概是这样的:

输入:“公司 A 产品出现添加剂,其下属子公司 B 和公司 C 遭到了调查”, “产品出现问题” 

输出:“公司 A” 

也就是说,这是个双输入、单输出的模型,输入是一个 query 和一个事件类型,输出一个实体(有且只有一个,并且是 query 的一个片段)。其实这个任务可以看成是 SQUAD 1.0 [5] 的简化版,根据这个输出特性,输出应该用指针结构比较好(两个 softmax 分别预测首尾)。剩下的问题是:双输入怎么搞? 

前面两个例子虽然复杂度不同,但它们都是单一输入的,双输入怎么办呢?当然,这里的实体类型只有有限个,直接 Embedding 也行,只不过我使用一种更能体现 Bert 的简单粗暴和强悍的方案:直接用连接符将两个输入连接成一个句子,然后就变成单输入了!

比如上述示例样本处理成: 

输入:“___产品出现问题___公司 A 产品出现添加剂,其下属子公司 B 和公司 C 遭到了调查” 

输出:“公司 A” 

然后就变成了普通的单输入抽取问题了。说到这个,这个模型的代码也就没有什么好说的了,就简单几行,完整代码请看:

https://github.com/bojone/bert_in_keras/blob/master/subject_extract.py

x = bert_model([x1, x2])
ps1 = Dense(1, use_bias=False)(x)
ps1 = Lambda(lambda x: x[0][..., 0] - (1 - x[1][..., 0]) * 1e10)([ps1, x_mask])
ps2 = Dense(1, use_bias=False)(x)
ps2 = Lambda(lambda x: x[0][..., 0] - (1 - x[1][..., 0]) * 1e10)([ps2, x_mask])

model = Model([x1_in, x2_in], [ps1, ps2])

另外加上一些解码的 trick,还有模型融合,提交上去,就可以做到 89%+ 了。在看看目前排行榜,发现最好的结果也就是 90% 多一点点,所以估计大家都差不多是这样做的了。这个代码重复实验时波动比较大,大家可以多跑几次,取最优结果。

这个例子主要告诉我们,用 Bert 实现自己的任务时,最好能整理成单输入的模式,这样一来比较简单,二来也更加高效。

比如做句子相似度模型,输入两个句子,输出一个相似度,有两个可以想到的做法,第一种是两个句子分别过同一个 Bert,然后取出各自的 [CLS] 特征来做分类;第二种就是像上面一样,用个记号把两个句子连接在一起,变成一个句子,然后过一个 Bert,然后将输出特征做分类,后者显然会更快一些,而且能够做到特征之间更全面的交互。

文章小结

本文介绍了 Keras 下 Bert 的基本调用方法,其中主要是提供三个参考例子,供大家逐步熟悉 Bert 的 fine tune 步骤和原理。其中有不少是笔者自己闭门造车的经验之谈,如果有所偏颇,还望读者指正。 

事实上有了 CyberZHG 大佬实现的 keras-bert,在 Keras 下使用 Bert 也就是小菜一碟,大家折腾个半天,也就上手了。最后祝大家用得痛快~

相关链接

[1] https://kexue.fm/archives/3863

[2] https://kexue.fm/archives/3414

[3] https://kexue.fm/archives/6671

[4] https://biendata.com/competition/ccks_2019_4/

[5] https://rajpurkar.github.io/SQuAD-explorer/explore/1.1/dev/

相关 [bert keras bert] 推荐:

当Bert遇上Keras:这可能是Bert最简单的打开姿势

- - 机器之心
Bert 是什么,估计也不用笔者来诸多介绍了. 虽然笔者不是很喜欢Bert,但不得不说,Bert 确实在 NLP 界引起了一阵轩然大波. 现在不管是中文还是英文,关于 Bert 的科普和解读已经满天飞了,隐隐已经超过了当年 Word2Vec 刚出来的势头了. 有意思的是,Bert 是 Google 搞出来的,当年的 word2vec 也是 Google 搞出来的,不管你用哪个,都是在跟着 Google 大佬的屁股跑.

谷歌最强 NLP 模型 BERT 解读

- - 雷锋网
雷锋网 AI 科技评论按:本文是追一科技潘晟锋基于谷歌论文为 AI 科技评论提供的解读稿件. 最近谷歌研究人员通过新的BERT模型在11项NLP任务中夺得STOA结果,这在自然语言处理学界以及工业界都引起了不小的热议. 作者通过在33亿文本的语料上训练语言模型,再分别在不同的下游任务上微调,这样的模型在不同的任务均得到了目前为止最好的结果,并且有一些结果相比此前的最佳成绩得到了幅度不小的提升.

一大批中文(BERT等)预训练模型等你认领!

- - 机器之心
预训练模型已经成为了 NLP 领域最重要的资源之一. 当我们拿到一个 NLP 数据集时,首要任务是寻找一个预训练模型. 当然,目前大家会趋同的选择 Google 发布的 BERT 模型 [1]. 不过本文将为大家提供更多的中文预训练模型的选择. 本文将介绍如何利用一个预训练模型框架 UER(Universal Encoder Representations)去使用性质各异的中文预训练模型,从而在下游任务上取得比 Google BERT 更好的效果.

如何评价在20个任务上超越BERT的XLNet?

- - 知乎每日精选
谢 @henryWang 邀. (不知道为啥我又 at 不上人. 效果就不比了,屠榜了没什么好说的. BERT 有两个目标,Masked Language Model 和 Next Sentence Prediction,前者用于提取单词之间的关系,后者用来提取句子之间的关系. 前者带来的问题在于, 训练时引入 [MASK] token 使得 train/test 不一致,于是又用了很多乱七八糟的方法来补救(以一定概率保留原词,一定概率换成别的单词等等).

NLP历史突破!谷歌BERT模型狂破11项纪录,全面超越人类!

- - 博客园_新闻
来源:新智元(AI_era). (来源:arXiv、知乎;编辑:新智元编辑部). 今天,NLP 领域取得最重大突破. 谷歌 AI 团队新发布的 BERT 模型,在机器阅读理解顶级水平测试 SQuAD1.1 中表现出惊人的成绩:全部两个衡量指标上全面超越人类,并且还在 11 种不同 NLP 测试中创出最佳成绩.

Keras:基于Theano和TensorFlow的深度学习库

- -
Keras:基于Theano和TensorFlow的深度学习库. Keras是一个高层神经网络API,Keras由纯Python编写而成并基Tensorflow或Theano. Keras 为支持快速实验而生,能够把你的idea迅速转换为结果,如果你有如下需求,请选择Keras:. 简易和快速的原型设计(keras具有高度模块化,极简,和可扩充特性).