引言
讯飞之前发布了一个事件抽取挑战赛,这个比赛分成两个任务:
1、任务一:事件触发词及论元抽取
2、任务二:事件属性抽取
搜了下github,发现第一名的解决方案。
看readme介绍主要思路,作者将任务分割为触发词抽取,论元抽取,属性抽取
。
那本篇即讲述触发词(trigger提取器)的实现方式。
1. 跑通代码
修改train.py,增加下面这些:
1 | args = TrainArgs().get_parser() |
2. 模型结构
3. 数据处理
主要代码看这里。
- 将raw_text分成token,空格之类的作者换成了
[BLANK]
,没有tokenize的换成了[INV]
。虽然他这种做法我勉强能接受,但是还是理解不了。。。因为tokenizer该做的都做了,做这一步不就多此一举么。。。(除非有其他理由) - 核心的地方在于label的构造方式。举个例子:
1 |
|
看到有意思的地方了么,以char level进行分割raw_text,然后找到公布
对应的start_index和end_index置为1。
4.模型结构
1 | TriggerExtractor( |
5. 训练
输入input_ids, attention_mask, token_type_ids获取bert output,然后mid_linear,最后classifier,shape变成比如32,128,2
,核心地方在计算了loss和解码部分。
举个例子计算loss:
1 | # -*- coding: utf8 -*- |
6. 解码
代码。
7. 总结
1. 作者为什么没有使用bert+crf?
1 | 缺点:存在5%的句子会出现解码为空的现象,导致误差传播极大; |
这个在作者的ppt里面提到了。
2. 优点
作者舍弃CRF结构,采用指针式解码
的方案。这个地方是我觉得最有意思的地方。
我为什么任务这个地方是优点呢,初看网络结构,我以为的网络结构类似下面这样:
1 | bert |
也就是说bert后面有两个linear,一个是start_index的,一个是end_index的,然后分别预测开始和结束index。
但是这么做有几个坏处:
1、start_index和end_index没有任何交互
2、解码的时候,会解码出很多的结果
那作者的优势就很明显了,start_index和end_index用一个linear表示。
但是即使这样,都绕不开一个地方,就是解码出多个结果
和长度
的问题。
3. 缺点
解码的长度是不确定的,只要start_logits > 0.5,end_logits>0.5,就认为是合理的。那么这两者之间组合,就会有许多的结果。这个是绕不开的。
作者这里做了一个限制,看代码:
1 | candidate_entities = [] |
即限制了触发词
的长度
。
第二,在这个比赛里,绝大多数的样本的触发词就为1个。
如果解码为空的话,作者采用start_logits和end_logits最大的作为输出。代码。
8. 思考
本质来讲,我没太看得出来和crf相对比的优劣。作者也没有公开每个任务分别的score。
但是从模型结构上来讲,这种方式有个好处,就是可以解决嵌套
的问题。第二就是速度的好处。
备注
1. 忽略的点
1、没有考虑数据增强
2、没有use_distant_trigger,即将构造的trigger输入ebedding作为extra feature(即将句子中所有可能的触发词都列出来然后输入到embeddings和bert output concat到一起)。
2. 使用roberta-www-ext
1 | tokenizer = BertTokenizer.from_pretrained(bert_dir) |