分享一篇文章,这篇文章对图算法做了一个概览,主要分成三块:
- 路径搜索(DFS、BFS、prime tree, Dijkstra, ranom rank)
- 中心性计算(度中心性,紧密性中心性,pagerank)
- 社群发现(聚类,等)
同样更推荐《算法》,是一本系统完备的书,不管从工程还是算法角度都很适合。
文本匹配是研究两段文本之间的关系。
此处介绍两种,分别是point-wise和pair-wise语义匹配模型。
point-wise是ptm+二分类,判断句子相似度。
pair-wise是ptm+score,判断两个句子相似度得分,可用于排序。最近实现了一个,可参考pairwise-match。
粗排方面有sentence transformer以及SimBERT,再比如DSSM。
这些先记下,等后面有时间了再实现总结。
关于双塔模型中的sentence transformer,网络结构如下:
其中pooling为比如Sentence的维度为(1,7,768)
,那么就对7
那一维做mean操作。
由于共用同一个pretrained model,将向量提前保存到数据库。当用户搜索query时,只需计算query的sequence_output特征与保存在数据库中的title sequence_output特征,通过一个简单的mean_pooling和全连接层进行二分类即可。从而大幅提升预测效率,同时也保障了模型性能。
此次开辟一个新的话题,叫做prompting learning。prompting learning常用于零样本、小样本领域,通过将下游训练数据转换成预训练模型任务,进行小样本微调或者零样本进行预测。
之前在UIE-事件提取中也有涉及prompt,但是UIE的做法是构造prompt输入,再和原句一起微调模型,完成提取任务。那这里我们来看看OpenPrompt是怎么做的。
关于OpenPrompt,此处不做过多介绍,请直接看官网。
官方有张图阐述了OpenPrompt的架构,请看下图。
下面通过官方示例来说明它的工作原理。
这个从网上一搜prompting learning,不可避免搜到template,这个是将下游训练数据通过template转换成符合预训练模型任务的训练数据,保持和预训练任务一致。
这个应该怎么翻译呢,姑且称为同义词吧。。。它的作用是从预训练模型vocab中获取符合label的词,比如:
1 | { |
此处先不展开介绍,后续会介绍它的作用和比较坑的地方。
这个就没什么可解释的了,就是预训练模型。
整体流程代码可见附录。
比如这个情感分类任务,经过Template转换后变成了如下:
1 | Albert Einstein was one of the greatest intellects of his time. It was [MASK] |
接着输入给bert模型,拿到mask位置的预测结果output,此时shape为(2, 28996),表示batch_size为2,整个bert vocab size为28996。
那如何将这个预测结果对应到negative
或positive
呢,这里就引出来Verbalizer了。
将['bad', 'good', 'wonderful', 'great']
转成tokenizer vocab的index,然后获取output所对应的index,比如下面伪代码:
1 |
|
样本 | bad | good | wonderful | great |
---|---|---|---|---|
1 | 0.0508 | 0.1770 | 0.9680 | 0.0091 |
2 | 0.2045 | 0.9504 | 0.2127 | 0.1417 |
通过aggreate就拿到最终对应negative和postive的logit了。
1 | print(aggreate(logits, mask)) |
拿到这个后,直接和label做交叉熵不就拿到loss啦~
可看到,['bad', 'good', 'wonderful', 'great']
都是在bert vocab中存在的词,但是在中文里面是以字进行拆分的,那么不会存在不好,精彩,漂亮,优秀...
这种词,即使是在WWM模型里面也不存在这些词,那么这个对中文构造这些verbalizer带来了不足。
而这种方式被称为硬解码,后续再说。
1 | # -*- coding: utf8 -*- |
关于跨任务抽取,搜了圈没发现准确定义的,有介绍多任务的,有介绍prompt来做多任务的。
所以此处暂不纠结具体细节了,看下paddleNLP UIE怎么做的。
关于数据标注,官方没有给具体的方式,那么就从推理的角度来看是怎么实现的。
官方代码:
1 | from paddlenlp import Taskflow |
代码debug部分就不介绍了,直接说结论。
提取顺序 | prompt构造方式 | 推理结果 |
---|---|---|
1 | 法院 | 北京市海淀区人民法院 |
2 | 原告 | 张三 |
3 | 被告 | B公司 |
4 | 张三的委托代理人 | 李四 |
5 | B公司的委托代理人 | 王五 |
看完百度paddleNLP UIE代码,就觉得优秀的代码就是优秀,这个优秀不是因为简单容易看懂这种,而是优秀的工程能力,优秀的建模能力,和简洁明了的实现方式。不像打比赛,啥子有的没的feature都往上堆。。。
不管怎样,建议下次不记得的时候,一定要先从UIE-事件提取看起。这种方式值得学习!
这是关于UIE的第三篇文章,但是呢,我更倾向上一篇是UIE-事件提取,因为UIE-情感分类做法上和本篇关系不大。
本篇文章继续介绍基于prompt实体识别的做法。
如果你看懂了UIE-事件提取的做法,那这里也是同一类任务,都是属于抽取式任务项目。
doccano_txt.json
1 | {"id": 11, "data": "2月8日上午北京冬奥会自由式滑雪女子大跳台决赛中中国选手谷爱凌以188.25分获得金牌!", "label": {"entities": [{"id": 45, "start_offset": 0, "end_offset": 6, "label": "时间"}, {"id": 46, "start_offset": 24, "end_offset": 31, "label": "人名"}]}} |
使用抽取式任务数据转换转换后,生成的样本如下所示:
train.txt
1 | {"content": "2月8日上午北京冬奥会自由式滑雪女子大跳台决赛中中国选手谷爱凌以188.25分获得金牌!", "result_list": [{"text": "2月8日上午", "start": 0, "end": 6}], "prompt": "时间"} |
test.txt
1 | {"content": "3月6日下午张三获得金牌!", "result_list": [{"text": "3月6日下午", "start": 0, "end": 6}], "prompt": "时间"} |
这部分没什么好说的了😂😂😂,以时间
或人名
作为prompt。
在上一篇文章UIE-事件提取中,介绍了doccano的用法,也介绍了基于prompt事件提取的做法。本篇文章继续介绍基于prompt情感分类的做法。
关于doccano使用,请看构建分类式任务标签和句子级分类任务。标注完成后生成下面文件。
doccano_txt.json
1 | {"id": 7, "data": "这个产品用起来真的很流畅,我非常喜欢", "label": ["正向"]} |
使用句子级分类任务数据转换转换后,生成的样本如下所示:
train.txt
1 | {"content": "这个产品用起来真的很流畅,我非常喜欢", "result_list": [{"text": "正向", "start": -7, "end": -5}], "prompt": "情感倾向[正向,负向]"} |
test.txt
1 | {"content": "这个产品非常low", "result_list": [{"text": "负向", "start": -7, "end": -5}], "prompt": "情感倾向[负向,正向]"} |
注意,输入到模型的prompt是固定的,每个句子的prompt都是固定的,比如:
[cls]
情感倾向[负向,正向][SEP]
这个产品用起来真的很流畅,我非常喜欢[SEP][cls]
情感倾向[负向,正向][SEP]
这个产品非常low[SEP]
** 这里你会好奇一个问题了,正向和负向这个label怎么表示呢,貌似原句中也没有这俩词?**
注意:模型结构都是固定的(这里不知道的请看上篇文章)
请看训练样本,其中的start和end都是负值,这个经过convert_example转换后,就变成了prompt中对应的正向
和负向
所对应的index,即答案来自prompt中。
prompt是固定的,即情感倾向[负向,正向]
,其他没有什么特殊需要注意的了。
对于prompt learning,模板的构造是比较关键的,百度这种做法值得借鉴!
此篇文章分析paddlenlp中universal information extraction(UIE)对于事件提取的实现方式。
** 后续在没有特殊声明的情况下,UIE均代表paddlenlp的实现方式。**
在这篇文章产业级信息抽取技术开源,为什么Prompt更有效? 中,作者突出UIE的优势:
而上述两点,基本也突出了目前深度学习算法的几个问题:
** 一、多任务统一建模 **
** 二、零样本或者小样本 **
监督学习的特点,就是希望拥有更多的标注样本,数据是算法的上限,搞算法的希望越多越好。那这里又可以分为几部分来看:
扯了这么多,下面进入正文。
1 | # Copyright (c) 2022 PaddlePaddle Authors. All Rights Reserved. |
是不是非常简单!!!
不过对于ERNIE3的模型结构,如下:
1 | UIE( |
其中token_type_embeddings为4,另外引入了task_type_embeddings。
这地方我在网上也木有看到很多相关介绍,此处以后再补充。
事件提取,是将事件的核心触发词和相关论元进行提取出来,构成结构化的数据。比如:
泰安今早发生2.9级地震!
key | value |
---|---|
触发词 | 地震 |
等级 | 2.9级 |
时间 | 今早 |
… | … |
有了概念后,接下来我们介绍UIE的实现方式。
UIE使用了doccano来进行数据标注,相关的使用示例请看开发使用,注意哦,作者这里doccano使用的版本是1.6.2
,我一开始使用最新的,很多和文章对应不到一起。
导出后的样例:
![]() |
![]() |
---|---|
{“id”: 5, “data”: “泰安今早发生2.9级地震!”, “label”: {“relations”: [{“id”: 12, “from_id”: 41, “to_id”: 39, “type”: “时间”}, {“id”: 13, “from_id”: 41, “to_id”: 40, “type”: “震级”}], “entities”: [{“id”: 39, “start_offset”: 2, “end_offset”: 4, “label”: “时间”}, {“id”: 40, “start_offset”: 6, “end_offset”: 10, “label”: “等级”}, {“id”: 41, “start_offset”: 10, “end_offset”: 12, “label”: “地震触发词”}]}} | {“id”: 6, “data”: “日本昨日发生10级地震!”, “label”: {“relations”: [{“id”: 14, “from_id”: 44, “to_id”: 42, “type”: “时间”}, {“id”: 15, “from_id”: 44, “to_id”: 43, “type”: “震级”}], “entities”: [{“id”: 42, “start_offset”: 2, “end_offset”: 4, “label”: “时间”}, {“id”: 43, “start_offset”: 6, “end_offset”: 9, “label”: “等级”}, {“id”: 44, “start_offset”: 9, “end_offset”: 11, “label”: “地震触发词”}]}} |
接着走数据转换,形成的train.txt和test.txt如下所示。
train.txt
1 | {"content": "泰安今早发生2.9级地震!", "result_list": [{"text": "地震", "start": 10, "end": 12}], "prompt": "地震触发词"} |
test.txt(此处对应的prompt都是有result_list的哦)
1 | {"content": "日本昨日发生10级地震!", "result_list": [{"text": "昨日", "start": 2, "end": 4}], "prompt": "时间"} |
可以看到,转换后的prompt由两部分组成:
时间
,等级
,地震触发词
。实体词+的+relation
。比如地震的时间
,地震的震级
,今早的时间
,今早的震级
,2.9级的震级
,2.9级的时间
。官方微调介绍。
这部分就一个点需要注意,就是数据构造方式,看下面:
[CLS]
prompt[SEP]
Content[SEP]
Content对应原句。相关代码可看check和convert example。
到这里整体训练流程就明白了,那有一个问题,就是如何使用呢???
从上面标注图片可以看到,所有的出发点都是来自地震触发词
,那么就应该先预测触发词
,然后根据这个触发词来找到与之相关的所有的论元
,比如地震
的震级
, 地震
的时间
。中间通过一个的
字来进行连接形成prompt。
那如何找与之相关的所有的论元
呢???通过relation,那如何定义relation呢?通过schema
!!!
下面看官方示例:
1 |
|
set_schema干了什么事呢?看官方代码SchemaTree,这里对schema形成了一个tree,构造完的示例如下:
也就是说,通过遍历SchemaTree,预测顺序为:地震触发词,地震强度,时间,震中位置,震源深度。
对应的输入为:
- [CLS]地震触发词[SEP]中国地震台网正式测定:5月16日06时08分在云南临沧市凤庆县(北纬24.34度,东经99.98度)发生3.5级地震,震源深度10千米。[SEP]
输出为:
地震
接着以地震
作为触发词,构造接下来的prompt,核心哦!!
- [CLS]地震的地震强度[SEP]中国地震台网正式测定:5月16日06时08分在云南临沧市凤庆县(北纬24.34度,东经99.98度)发生3.5级地震,震源深度10千米。[SEP]
- [CLS]地震的时间[SEP]中国地震台网正式测定:5月16日06时08分在云南临沧市凤庆县(北纬24.34度,东经99.98度)发生3.5级地震,震源深度10千米。[SEP]
- [CLS]地震的震中位置[SEP]中国地震台网正式测定:5月16日06时08分在云南临沧市凤庆县(北纬24.34度,东经99.98度)发生3.5级地震,震源深度10千米。[SEP]
- [CLS]地震的震源深度[SEP]中国地震台网正式测定:5月16日06时08分在云南临沧市凤庆县(北纬24.34度,东经99.98度)发生3.5级地震,震源深度10千米。[SEP]
整理流程至此就已经非常明白了。
事件提取作者这里使用关系抽取的标注方式来标注的,包括实体和对应关系,但是做了层转换,转换成prompt训练样本。具体做的事情分成两步:
的
字将触发词和关系做拼接。比如地震的时间
1 | {"content": "泰安今早发生2.9级地震!", "result_list": [{"text": "今早", "start": 2, "end": 4}], "prompt": "地震的时间"} |
时间
1 | {"content": "泰安今早发生2.9级地震!", "result_list": [{"text": "今早", "start": 2, "end": 4}], "prompt": "时间"} |
预测的时候也是先找核心触发词
,然后再构成prompt。
语义角色标注任务能否通过这种方式来进行改进或者说能够通过语义角色标注的方式来减少UIE标注的标签数量?
比如:
泰安今早发生2.9级地震!
主体: 泰安
谓语: 发生
客体: 地震
时间: 今早
等级: 2.9级
…
核心论元就三个,主体、客体、触发词。然后围绕这三个,比如时间,地点等构成这个事件。
如果这样的话,流程就变成了如下方式:
问题来了,那如何构造呢?
比如发生
,发生什么?如果按照UIE的方式来讲,是地震触发词
,简洁明了,不会有任何歧义,以地震作为事件主体来找触发词以及相应的论元。
如果换成发生的话,关系就变成了比如:
构造上没有任何问题~~~,只是觉得发生
不够直接表达意思,但是形式上的确是可行的,这个需要试下就知道效果了,如果效果好,那对于标签构造的减少非常有帮助。
又比如我吃饭,她吃菜。
,一个吃字,怎么根据这个吃字来构造数据呢?
那么这个吃就会提出两个主体和客体,所以这里我觉得把触发词
所在的index也给传进去从而会对训练和预测时的效果有所提升。
而对于UIE呢,他的触发词估计就变成了吃饭触发词
,所以就不会有这种歧义性的问题了,除非一个句子有多个吃饭事件。。。这个概率就会极低了。。。
对于上述思考,UIE的做法很直接,定义地震触发词
,表明了输入一句话那我就是要提取和地震相关的论元,先提取地震触发词
(同时也作为prompt输入),然后接着构造prompt提取其他相应的论元。
整个过程直接明了,歧义与理解困难上都非常低。另外对于不同的事件,其实它都是有对应的主体,比如可以定义海啸触发词
和泥石流触发词
。
即: 触发词即事件!!
而对于语义角色标注呢,触发词更倾向的是动词,它属于格语论。以动词为中心所展开的事件论元。那么它所对应的主体或者客体才是事件。
此文不是对预训练模型融入实体信息、知识图谱等类似ERNIE,k-bert这种,而是在拿到bert输出后,突出指定位置信息进去,从而控制判定的结果。
比如这句话:
该报还报道,法国达能集团日前宣布将投资1亿欧元,加强在中国市场的奶粉生产和研发,并表示“我们对中国市场的长期增长能力充满信心”。
谁加强和谁表示呢?是法国达能集团,而不是该报。
主体 | 触发词 | 客体 |
---|---|---|
法国达能集团 | 加强 | 在中国市场的奶粉生产和研发 |
法国达能集团 | 表示 | “我们对中国市场的长期增长能力充满信心” |
那假设,我们在知道触发词
和客体
的情况下,如何从原句中获取主体
呢?
这怕是最容易想到的方法了。比如将句子
、触发词
和客体
分别输入到bert,然后将这三者concat。emmm,这也怕是最蠢的方法了。
触发词
和客体
所在bert输出索引的权重比如一句话长度为128,拿到bert输出后为(128, 768)
,假设触发词
和客体
对应的span为(10,12)
和(15, 30)
,那如何手动修改对应span的weight呢??
方法一,直接进行mask_fill,将对应span的值改为比如0
。这种方式,,,emmmm,怎么说呢,看看就行。
方法二,也是操作对应span的weights,但是分成两步,第一是取触发词
的权重,第二是取客体
的权重,各自经过各自的linear,得到新的权重,然后再和bert output做一个交互,意思还是想突出触发词
和客体
的权重。和方法一一样,只不过对应的weight不是0,而是经过反向传播更新后的值。
比如conditional layer norm(对原conditional layer norm做修改了哦)。
伪代码如下:
1 | class NewConditionalLayerNorm(nn.Module): |
关于Conditional Layer Normalization,可看:讯飞2020年事件提取比赛第一名-主客体提取中Conditional Layer Normalization实现方式,难点在于变长罢了。
效果没试,总感觉复杂了些。
另外如果输入的feature个数是变化的,那这种方式就不可行了。
输入一句话,tokenizer后拿到input_ids
,token_type_ids
, attention_mask
,那其中的token_type_ids
是干嘛的呢?百度下就有结果,说是如果是0
就表示第一句话,如果是1
就表示第二句话。
如果这样的话,那直接将触发词
和客体
的所对应的token_type_ids
置为1
不就又是一种方式么~,试了下效果出奇的不错。准确率嗖嗖的往上。而且还没引入额外的layer,相当拿bert就把这件事情搞定了。
这也是我写这篇文章的动力。。。
实现方式如下:
比如一句话”我们喜欢晴天。”,label设计为如下:
1 | # 假设我们为主语 |
模型就是bert+linear,linear输出hidden_size为2,loss使用binary_cross_entropy
即可。
缺陷嘛就是它没有区分每个feature本身的label。客体就是客体,触发词就是触发词~~
所以通篇看下来,目的是想突出触发词
和客体
的weight,从而提取其对应的主体。前两者不同,那主体也有可能不同。
实现下来呢,如果不区分触发词和客体的话呢,可以使用上面那种方式。如果区分呢,那貌似只能引入新的layer来解决。
bert本身就够大的了,就不能一个预训练模型就能解决这种问题的么?比如下面这种方式:
1 | 预训练模型: |
嘿嘿,现在也有这种,不过坑更大,而且消耗的资源也更多,,,以后有机会再聊。
额外说一句,和本文无关,如果一个任务业界没有相关的研究,那你对这个任务进行建模、训练等,怎么评估这个模型是有效的呢?
又水了一篇~
这是第三篇文章,和第二篇本质没有顺序之分,上一篇是讯飞2020年事件提取比赛第一名-主客体提取。
只需要改动task_type,如下:
1 | args = TrainArgs().get_parser() |
1 | AttributionClassifier( |
根据挑战可知事件属性抽取分成两部分:
这俩分别对应tense_classifier
和polarity_classifier
。
在属性分类优化
ppt那页,作者认为:
1 | 能决定事件属性的词大多存在触发词左右,故舍弃CLS中的全局特征,采用trigger左右两端动态池化特征作为全局特征; |
具体做法即以触发词所在位置为准,设置window_size=20,只考虑这个范围内的bert output。
示例:
1 | # 假设爱是trigger word。 |
1 |
|
1 | logits = torch.cat([pooled_out, trigger_label_feature], dim=-1) |
1 | bert_outputs = self.bert_module( |
核心在于作者的这句话:
1 | 能决定事件属性的词大多存在触发词左右,故舍弃CLS中的全局特征,采用trigger左右两端动态池化特征作为全局特征; |
比如我们可以这么做,直接拿[CLS]位置,然后输入两个linear(一个极性,一个时态),分别拿到各自loss,就ok了。
但是作者这里没有拿[CLS]来代表整个句子,因为作者认为能决定事件属性的词大多存在触发词左右
。所以他这里采用了以trigger左右window_size=20来缩减范围。
从个人角度来讲,他这种方式第一能加快速度,第二是可能会有更好的泛化效果。
因为打比赛这种东西,恨不得啥奇淫巧技都上。但是这种思想可以借鉴。
关于AdaptiveMaxPool1d
的用法
1 | a = torch.arange(24, dtype=torch.float32).view(2,3,4) |
这是第二篇文章,因为主客体提取需要依赖触发词识别。上一篇是讯飞2020年事件提取比赛第一名-触发词提取。
1 | args = TrainArgs().get_parser() |
1 | Role1Extractor( |
具体代码这里。因为这个任务作者只提取主客体,所以每一个label的长度为4。
前两个为客体的开始和结束,后两个为主体的开始和结束。
所在的index对应句子的位置。
具体代码这里。
比如:
1 | label = [ |
看到这里,基本就明白主体思路和触发词提取是一样的。
这个是指引入了一个新的feature,这个feature是以trigger位置来算前面和后面的位置编码。比如trigger为(32,33),那(32,33)的位置编码为0,左右两边递增。
比如:
1 | distince_feature = [3,2,1,0,0,1,2,3] |
我对这一步的做法能带来多大的提升保留质疑。
所以此处就不细讲了。以后有机会试试效果。
这个作者的思想来自苏剑林的CondiationalLayerNorm,但是我没找到它的源码。
** 作用:**
作者这里采用的是利用Conditional Layer Normalization来将外部条件和bert output做了一次注意力。
** 具体流程:**
1、通过trigger index获取对应的bert output(看batch_gather函数),这里假设叫做trigger feature。
2、接着将trigger feature和bert output通过conditional layer norm进行融合。
** conditional layer norm流程:**
1、对bert output做layer norm,这一步没什么可说的。
2、将trigger feature经过weight linear和bias linear,这个做法其实和正常的layer norm指定elementwise_affine做法是类似的,正常的layer norm做归一化没有训练参数。
3、和bert_output进行相乘。这个地方可以理解成trigger和其他词做了一个注意力。
** 举例说明 **
在本次任务里,触发词
的长度都为2
,所以self.weight_dense
和self.bias_dense
都是normalized_shape * 2
。batch_gather后拿到的触发词shape为(32,2,768)
,这步叫做trigger feature。然后进行reshape,变成了(32, 1, 1536)
,经过self.weight_dense
和self.bias_dense
变换后变成了(32, 1, 768)
,随后和bert_output进行相乘,即和每个字做了一个注意力。
作者代码如下:
1 |
|
作者说添加了这个layer后效果有小幅度提升,以后可以试试。
如果说有很大创新的吧,算不上,我觉得把bert output不做layer norm最终效果也大差不差。
** 备注: **
这里有个问题,如果触发词的长度是变长的话,怎么用呢?估计引入一个mask,算出来。
以后试试。
这地方作者代码是在使用了trigger相对位置编码后和bert output进行concat到一起时用到的,如下所示。
1 |
|
这里的做法觉得是平时没注意到的一个点~
平时俩向量直接concat到一起完事。这里还进行了一个layer norm。算是一个挺细心的点。至于能不能带来效果提升,此处就不特别注意啦。
1 | if labels is not None: |
这里没什么特别需要注意的地方了,这里和trigger的做法类似,只是这里分成了两个loss,一个是subject loss,一个是object loss。
这部分没细看了,猜测和trigger解码应该也是类似的。
关于conditional layer norm
1 | # -*- coding: utf8 -*- |
缺失模块。
1、请确保node版本大于6.2
2、在博客根目录(注意不是yilia根目录)执行以下命令:
npm i hexo-generator-json-content --save
3、在根目录_config.yml里添加配置:
jsonContent: meta: false pages: false posts: title: true date: true path: true text: false raw: false content: false slug: false updated: false comments: false link: false permalink: false excerpt: false categories: false tags: true