用bert4keras做三元组抽取
By 苏剑林 | 2020-01-03 | 250736位读者 |在开发bert4keras的时候就承诺过,会逐渐将之前用keras-bert实现的例子逐渐迁移到bert4keras来,而那里其中一个例子便是三元组抽取的任务。现在bert4keras的例子已经颇为丰富了,但还没有序列标注和信息抽取相关的任务,而三元组抽取正好是这样的一个任务,因此就补充上去了。
模型简介 #
关于数据格式和模型的基本思路,在《基于DGCNN和概率图的轻量级信息抽取模型》一文中已经详细介绍过了,在此不再重复。数据集百度已经公开了,在这里就可以下载。
跟之前的策略一样,模型依然是基于“半指针-半标注”的方式来做抽取,顺序是先抽取s,然后传入s来抽取o、p,不同的只是将模型的整体架构换成了bert:
1、原始序列转id后,传入bert的编码器,得到编码序列;
2、编码序列接两个二分类器,预测s;
3、根据传入的s,从编码序列中抽取出s的首和尾对应的编码向量;
4、以s的编码向量作为条件,对编码序列做一次条件Layer Norm;
5、条件Layer Norm后的序列来预测该s对应的o、p。
类别失衡 #
不难想到,用“半指针-半标注”结构做实体抽取时,会面临类别不均衡的问题,因为通常来说目标实体词比非目标词要少得多,所以标签1会比标签0少得多。常规的处理不平衡的方法都可以用,比如focal loss或者人工调节类权重,但这些方法用了之后,阈值就不大好取了。我这里用了一种自以为比较恰当的方法:将概率值做$n$次方。
具体来说,原来输出一个概率值$p$,代表类别1的概率是$p$,我现在将它变为$p^n$,也就是认为类别1的概率是$p^n$,除此之外不变,loss还是用正常的二分类交叉熵loss。由于原来就有$0\leq p \leq 1$,所以$p^n$整体会更接近于0,因此初始状态就符合目标分布了,所以最终能加速收敛。
从loss角度也可以比较两者的差异,假设标签为$t\in\{0, 1\}$,那么原来的loss是:
\begin{equation}- t \log p - (1 - t) \log (1 - p)\end{equation}
而$n$次方之后的loss就变成了
\begin{equation}- t \log p^n - (1 - t) \log (1 - p^n)\end{equation}
注意到$- t \log p^n = -nt \log p$,所以当标签为1时,相当于放大了loss的权重,而标签为0时,$(1 - p^n)$更接近于1,所以对应的loss $\log(1 - p^n)$更小(梯度也更小)。因此,这算是一种自适应调节loss权重(梯度权重)的思路了。
相比于focal loss或人工调节类权重,这种方法的好处是不改变原来内积($p$通常是内积加sigmoid得到的)的分布就能使得分布更加贴近目标,而不改变内积分布通常来说对优化更加友好。
源码效果 #
Github:task_relation_extraction.py
在没有任何前处理和后处理的情况下,最终在验证集上的f1为0.822,基本上比之前的DGCNN模型都要好。注意这是没有任何前后处理的,如果加上一些前后处理,估计可以f1达到0.83。
同时,我们可以发现训练集和验证集的标注有不少错漏之处,而当初我们做比赛的时候,线上测试集的标注质量比训练集和验证集都要高(更规范、更完整),所以当时提交测试的f1基本上要比线下验证集的f1高4%~5%,也就是说,加上一些规则修正后,这个结果如果提交到当时的排行榜上,单模型估计有0.87的f1。
值得注意 #
开头已经提到了,之前用keras-bert就写过一个用bert来抽三元组的例子了,而这里主要谈谈本文模型跟之前的例子不同之处以及一些值得注意的地方。
第一个不同之处是,当时仅仅是简单的尝试,所以仅仅是将s的向量加到编码序列中,然后做o、p的预测,而不是像本文一样用到条件Layer Norm;条件Layer Norm的方案有更好的表达能力,效果略微有提升。
第二个不同之处,也是值得留意的地方,就是本文的模型用的是标准的bert的tokenizer,而之前的例子是直接按字切分。标准的tokenizer出来的序列,并不是简单地按字切分的,尤其是函数英文和数字的情况下,输出的分词结果跟原始序列的字并非对齐的,所以在构建训练样本和输出结果时,都要格外留意这一点。
读者可能会疑问:那为什么不用回原来的按字切分的方式呢?笔者觉得,既然用了bert,应该按照bert的tokenizer,就算不对齐其实也有办法处理好的;而之前的按字切分,只是笔者当时还不够熟悉bert所导致的不规范用法,是不值得提倡的;遵循bert的tokenizer,还有可能比自己强行按字切分获得更好的finetune效果。
此外,笔者还发现一个有点意外的事实,就是中文bert所带的字表(vocab.txt)是不全的,比如“符箓”的“箓”字就不在bert的vocab.txt里边,所以要输出最终结果的时候,最好别用tokenizer自带的decode方法,而是直接对应到原始序列,在原始序列中切片输出。
最后,这一版模型的训练还加入了权重滑动平均,它可以稳定模型的训练,甚至能轻微提升模型的效果。关于权重滑动平均的相关介绍,可以参考这里。
文章小结 #
本文给出了用bert4keras来做三元组抽取的一个例子,并且指出了一些值得注意的事情,欢迎大家参考试用。
转载到请包括本文地址:https://kexue.fm/archives/7161
更详细的转载事宜请参考:《科学空间FAQ》
如果您还有什么疑惑或建议,欢迎在下方评论区继续讨论。
如果您觉得本文还不错,欢迎分享/打赏本文。打赏并非要从中获得收益,而是希望知道科学空间获得了多少读者的真心关注。当然,如果你无视它,也不会影响你的阅读。再次表示欢迎和感谢!
如果您需要引用本文,请参考:
苏剑林. (Jan. 03, 2020). 《用bert4keras做三元组抽取 》[Blog post]. Retrieved from https://kexue.fm/archives/7161
@online{kexuefm-7161,
title={用bert4keras做三元组抽取},
author={苏剑林},
year={2020},
month={Jan},
url={\url{https://kexue.fm/archives/7161}},
}
February 8th, 2021
苏神你好,看了后面unilm的文章,回头再看这个,那是不是可以直接用unilm来做spo的抽取会更简单?输入[cls]text[step]s[step]p[step]o[step],输出s[step]p[step]o[step],训练时多组spo去训练,生成时多次调用生成?
你这个应该属于用seq2seq做三元组抽取吧,是有不少工作是这样做的。但它是将所有要抽取的三元组拼接在一起作为一个目标文本来套用seq2seq模型的,不是你这样的单个三元组。
就是一次性抽取完所有spo组?我去了解下seq2seq做三元组抽取这种方式
对,直接生成所有的。
March 6th, 2021
A Novel Cascade Binary Tagging Framework for Relational Triple Extraction
你看这个是不是和你一样的
差不多的。这论文作者也有我...
March 9th, 2021
请教博主,三元组抽取代码中output = bert.model.layers[-2].get_output_at(-1)这里-2是为了取Transformer-11-FeedForward-Add做Conditional Layer Normalization吗,做Conditional Layer Normalization就不需要Transformer-11-FeedForward-Norm了?
是。
April 7th, 2021
苏老师,貌似代码有问题,data_generator 的迭代函数_iter__中的随机选取 subject 居然用了两个 random.choice 分别随机选 start 和 end, 这样会导致 start 和 end 不匹配,比如有两个subject 坐标 (10, 24) (3, 5), start 随机选了3, end 随机选了24,得到的 subject 就是(3, 24) ,这明显是错的,我想本意应该是在 (10, 24) (3, 5) 里随机选一个,应该用一个random.choice
不要那么迷之自信,我得到(3, 24)犯法了吗?不允许我故意得到(3, 24)吗?下结论之前多搜搜,这个问题已经被问了不下5次了。
看代码逻辑,spoes.get(subject_ids, []), 如果这样有概率会使得 subject 得到的对应 object 为 [], 那么object 和 predicate 对应的序列标记都为0,这样的话逻辑上没问题,那么相当于负样本?
不好意思,翻看了评论,相当于构造负样本,在代码注释里说明一下应该可以减少被问的次数
代码里声明了一下了
May 8th, 2021
请问 对于一个样本里面 多个subject真么处理的呢,还是说只能随机选一个subject得到这一个subject对应的多个objects和relations
认真思考并认真搜索后再来提这个问题吧。
在训练的时候一个subject和词向量矩阵融合去预测多个object,只是在预测的时候多个subject分别融合到词向量矩阵 分别预测多个object的吧?那这和casrel有什么区别吗?
没区别,casrel就是本文提出的模型。
June 19th, 2021
苏神您好,实际生产中您是用您这个算法么?如果生产里用Casrel每个类别需要多少条数据,能得到比较满意的准确率?
我没将这个算法落地的经验。另外“比较满意”的标准因人而异。
June 25th, 2021
您好,我使用其他数据运行了这篇代码,但是在某个epoch后evaluator时出现了以下错误(第250行代码):
((mapping[subject[0]][0],
IndexError: list index out of range
想向您请教下
数据格式就是这种:
{"text": "马云已从阿里卸任,但仍是蚂蚁金服的实际控制人", "spo_list": [{"predicate": "实际控制人", "object_type": "人名", "subject_type": "企业", "object": "马云", "subject": "蚂蚁金服"}]}
跟百度比赛数据的格式一样的。
关键在前几个epoch都没报错,应该不是数据的格式的问题。
可是换成百度比赛数据,又并不会报错。
自行理解代码和debug,然后修改extract_spoes函数。
August 3rd, 2021
苏神,本来还想着加您的群打扰一下您问一下为什么subject的位置信息不能喝bert直接输出的张量进行交互,后来又重看了transformer的结构图,顿悟了,是不是因为bert的最后一层本身就是Add&norm,而我们要做的是根据subject的位置信息,使用tf.gather和bert的编码向量进行交互,如果直接使用最后一次进行交互的话会导致,怎么说呢,被非subject污染?因此选择[-2]层而非直接输出的张量,这是我自己的理解,不知道对不对。
最后非常感谢有像您这样的分享者,之前的seq2seq文本生成也是从您这里入的门,每次用tf2复现您的代码都能对我本身有非常好的提升!
这个问题其实非常简单,不能理解的读者都是“不清楚bert的结构 + 毫无强迫症”的。bert的最后一层是layernorm,如果我直接用最后一层再接个cond layernorm,两个layernorm接在一起不别扭吗?
是的,我三本自学的,大多时候对自己的定位是会用就行,自己已经给自己打上了数学不行,起点太低,会用就行的思路,不去探究模型本身,包括attention什么的也是近期才去细究的明白的,后来发现其实了解模型每一步做什么,目的是什么也不是很难....
对于这个项目我在您的思路上面又加了点自己不成熟的想法:
1: bert+crf预测subject,序列标注的思路处理,标注subject在text中的位置,先不考虑s对多个o的问题,用双学习率训练bert和crf
2: 按照您的思路通过subject的位置信息与bert向量的fed&norm(在transformers中貌似是bert.trainable_weights[-5]),如果对应多个s则按照顺序交互并预测对应的p,考虑这一点我有在处理数据的时候将s和o对应的位置信息一并保存
3:最后则是s,o以及bert编码向量(个人觉得里面包含的其他词信息与p是有关系的)做交互(可能s和o加权bert编码向量?)用于预测p
这样的话输入数据的格式以及在计算loss和acc和prec时简单点,用tf.GradienTape梯度训练也可以看看中间过程是怎么样,不知道您有什么指导或者补充的没有?
最后,能加您的微信群吗?非常感谢!
不是fed&norm,是feed-forward也就是那两个dense层,不好意思手误
简单来说就是对于s和o共享编码层,使用序列标注获取s和p,然后使用tf.gather()抽取s和o对应位置的张量做一些处理后作为权重乘以bert的编码输出,最后加一个dense层分类获取p或者加一个seq2seq生成p
不好意思,我没看低别人的意思。强迫症这个东西,因人而异,跟水平无关,就算你直接用最后一层接cond layernorm效果也没啥差别,所以这是个强迫症问题,不是水平问题。至于bert的结构,如果你想要理解这个选择背后的原因,自然是要知道的。
===========
我理解你说的几点,是想要将预测实体的模块换成CRF?如果是,这个没啥问题,你随意,但可能效果不会有太大区别。至于其他,合理就好,还是以实际效果为主,别人的意见也不一定是对的。
===========
微信群直接加机器人微信 spaces_ac_cn
没有,我不是那个意思,只是说明一下我自己以前学东西确实太糙了,最后感谢苏神的回复以及代码!
August 3rd, 2021
苏神,我想请教一个偏题的问题。我看到一个说法,CRF只能依靠当前位置和之前位置的特征去进行预测,不能利用上下文信息。而bert在attention时,是利用到了上下文信息的。那么这个说法是合理的吗?如果合理,是因为在预训练时的任务,与下游任务不太一样,所以在进行实体识别时还是需要结合上下文进行考虑?
crf是一种输出设计,假设当前输出结果仅跟相邻的输出结果有直接关联,crf本身对跟输入没有任何关系,不管给什么输入,它都是直接用。
September 10th, 2021
苏神,我按照您的思路尝试很多种可能,但是最优情况下f1值也才0.5左右,仔细对比了您的代码,唯一不同的可能就是loss的设计了,我那边只用了loss_value = tf.keras.losses.binary_crossentropy(y_true=t, y_pred=p)
loss_value = tf.reduce_mean(loss_value),但是看您的代码里面有个mask的东西,所以我想请问一下您这里的mask扮演的是什么角色了?