捂脸
定义
HanLP的定义
依存句法分析,是指识别语句中词与词之间的依存关系,并揭示其句法结构,包括主谓关系、动宾关系、核心关系等。用依存语言学来理解语义,精准掌握用户意图
百度ddparser的定义
依存句法分析是自然语言处理核心技术之一,旨在通过分析句子中词语之间的依存关系来确定句子的句法结构。
依存句法分析作为底层技术,可直接用于提升其他NLP任务的效果,这些任务包括但不限于语义角色标注、语义匹配、事件抽取等。
LTP的定义
依存语法 (Dependency Parsing, DP) 通过分析语言单位内成分之间的依存关系揭示其句法结构。 直观来讲,依存句法分析识别句子中的“主谓宾”、“定状补”这些语法成分,并分析各成分之间的关系。
小插曲,这些项目中的依存句法实现均来自yzhangcs/parser。
数据集解释
标注数据集分成两种格式(conllu和ocnllx),其中一种是以conllx结尾,标注示例如下:
1 | 1 新华社 _ NN NN _ 7 dep _ _ |
其中第二列表示分词,第四或者第五表示词性,第七列表示当前词和第几个位置的词是有依存关系的,第八列表示其对应的依存关系是什么。
dataset for ctb8 in Stanford Dependencies 3.3.0 standard.
实现
注意:本文的实现是采用biaffine的方式实现。另外以biaffine_dep进行讲解。
我一共使用两种方式进行实现,一个是一个biaffine,和biaffine_ner任务做法一致。第二种就是
yzhangcs
的做法。
biaffine_ner实现方式
这种方式是将其变成一个n * n 的矩阵问题,在这个矩阵中预测哪些span为词和词构成依存关系,以及对应的关系是什么,所以这里是一个纯粹的分类问题。
数据处理代码可参考这里
按照依存句法的定义:
- 当前词只能依存一个其他词,但是可以被多个其他词所组成依存关系。
- 如果A依存D,B或者C都在A和D中间,那么B和C都只能在A和D之内进行依存。
所以根据上图所示,每一行只会有一个值不为0.
这里额外插一句哈,与biaffine_ner一样,作者是使用这种临接矩阵的方式来解决嵌套ner的问题,不过与依存句法相比,可能存在的问题就是过于稀疏。但是与依存句法相比有一个特征,就是只会上三角(triu/tril)为1,下三角不会为1,这里可以做mask,具体可看biaffine_ner。
模型结构为:
从下往上看,第一层可以使用lstm或者bert进行提取特征,特征有两部分,一是词,二是词性。第二层为FFNN_Start和FFNN_End,为啥子叫这个名字,俺也不清楚,反正你就知道是两个MLP,分别接收第一层的输入。第三层是BIaffine classifiner,BIaffine classifiner的代码如下:
1 | import torch |
关于biaffine的解释,当然还有triaffine,这个后面有机会再看。总之这里将其变成了batch_size * seq_length * seq_length * n_label的矩阵。
那如何理解biaffine呢,我觉得下图说的非常在理。
关于bilinear,也可以看ltp bilinear。
当然,这里不止这一种方式,你也可以参考ShannonAI/mrc-for-flat-nested-ner的实现方式,他的方式
更为直接,这里:
1 | def forward(self, input_ids, token_type_ids=None, attention_mask=None): |
两个mlp在不同的位置进行unsqueeze,然后进行concat,嘿嘿,这种方式挺骚气并容易理解的。
至此模型结构以及整理流程说明基本已经结束,损失函数就是使用交叉熵。
我用这种方式验证了biaffine_ner和使用这种方式来做dependency parser任务,在对dependency parser结果中,效果不是很好,总结原因上述也提到了,临接矩阵太过稀疏,好歹ner还有一个上三角矩阵做mask。
额外插一句,biaffine_ner这论文水的有点严重呀,妥妥的依存句法的思想呀。更多吐槽看这里。
那么,有没有一种方式可以将这个任务分成两个部分,一是预测哪些词之间成依存关系,二是对应的标签是什么。然后分别计算各自的loss??
yzhangcs实现方式
这种数据处理并没有变成临接矩阵,而是简简单单的如这所示
但是模型结构使用了四个MLP,一共分成两组,一组叫arc_mlp_d
,arc_mlp_h
,一组叫rel_mlp_d
,rel_mlp_h
,代码可参考这里,分别用来预测arc和rel,emmmm,就是哪些词成依存关系和对应的relation。
然后各自经过各自的biaffine classfiner,看这一行,作者在非可能位置进行填充-math.inf
,这也算是一个小技巧了吧,get到了。
——————-重头戏来了,如何计算loss呢,这里手动分割—————————
看compute_loss函数,在进行计算arc loss时,就是简单的套交叉熵即可,但是在进行计算relation的时候,这一行,s_rel
根据真实的arcs
所对应的位置索引降维的s_rel
,简单来讲就是我直接获取真实的arcs那一维,从而利用了arcs的特征,然后后续接一个交叉熵进行计算loss,最终俩loss相加最为最终loss。
相应在decode部分这里也能概述这行做法。
不过后续关于生成最大树,emmm,为啥我这么叫,因为就是获取概率最大的那棵树嘛,这里作者提供了两种算法来实现,eisner
和mst
,具体实现就不讲了。
总结
至此可以看出,在biaffine那层获取词和词之间的关联程度,非常nice的做法,后面就是将其变成一个分类问题来解决,arc分类和rel分类是不同的,这个需要注意。
再额外插一句,感觉目前的句法分析就是依存句法的天下了哇,像Constituency Parser感觉没有很宽广的发展了。更多可看我这,手动狗头。