irpas技术客

依存句法分析_ox180x

未知 2697

捂脸

欢迎star ^_^

定义 HanLP的定义

依存句法分析,是指识别语句中词与词之间的依存关系,并揭示其句法结构,包括主谓关系、动宾关系、核心关系等。用依存语言学来理解语义,精准掌握用户意图

百度ddparser的定义

依存句法分析是自然语言处理核心技术之一,旨在通过分析句子中词语之间的依存关系来确定句子的句法结构。依存句法分析作为底层技术,可直接用于提升其他NLP任务的效果,这些任务包括但不限于语义角色标注、语义匹配、事件抽取等。

LTP的定义

依存语法 (Dependency Parsing, DP) 通过分析语言单位内成分之间的依存关系揭示其句法结构。 直观来讲,依存句法分析识别句子中的“主谓宾”、“定状补”这些语法成分,并分析各成分之间的关系。

小插曲,这些项目中的依存句法实现均来自yzhangcs/parser。

数据集解释

标注数据集分成两种格式(conllu和ocnllx),其中一种是以conllx结尾,标注示例如下:

1234567891 新华社 _ NN NN _ 7 dep _ _2 兰州 _ NR NR _ 7 dep _ _3 二月 _ NT NT _ 7 dep _ _4 十五日 _ NT NT _ 7 dep _ _5 电 _ NN NN _ 7 dep _ _6 ( _ PU PU _ 7 punct _ _7 记者 _ NN NN _ 0 root _ _8 曲直 _ NR NR _ 7 dep _ _9 ) _ PU PU _ 7 punct _ _

其中第二列表示分词,第四或者第五表示词性,第七列表示当前词和第几个位置的词是有依存关系的,第八列表示其对应的依存关系是什么。

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的代码如下:

12345678910111213141516171819202122232425262728293031323334353637383940import torch# 假设768是mlp出来的hidden_size.# batch_size, sequence_length, hidden_size = 32, 128,768class Biaffine(object): def __init__(self, n_in=768, n_out=2, bias_x=True, bias_y=True): self.n_in = n_in self.n_out = n_out self.bias_x = bias_x self.bias_y = bias_y self.weight = nn.Parameter(torch.Tensor(n_out, n_in + bias_x, n_in + bias_y)) def forward(self, x, y): if self.bias_x: x = torch.cat((x, torch.ones_like(x[..., :1])), -1) if self.bias_y: y = torch.cat((y, torch.ones_like(y[..., :1])), -1) b = x.shape[0] # 32 o = self.weight.shape[0] # 2 x = x.unsqueeze(1).expand(-1, o, -1, -1) # torch.Size([32, 2, 128, 769]) weight = self.weight.unsqueeze(0).expand(b, -1, -1, -1) # torch.Size([32, 2, 769, 769]) y = y.unsqueeze(1).expand(-1, o, -1, -1) # torch.Size([32, 2, 128, 769]) # torch.matmul(x, weight): torch.Size([32, 2, 128, 769]) # y.permute((0, 1, 3, 2)).shape: torch.Size([32, 2, 769, 128]) s = torch.matmul(torch.matmul(x, weight), y.permute((0, 1, 3, 2))) if s.shape[1] == 1: s = s.squeeze(dim=1) return s # torch.Size([32, 2, 128, 128])if __name__ == '__main__': biaffine = Biaffine() x = torch.rand(32, 128, 768) y = torch.rand(32, 128, 768) print(biaffine.forward(x, y).shape)

关于biaffine的解释,当然还有triaffine,这个后面有机会再看。总之这里将其变成了batch_size seq_length seq_length * n_label的矩阵。那如何理解biaffine呢,我觉得下图说的非常在理。

关于bilinear,也可以看ltp bilinear。

当然,这里不止这一种方式,你也可以参考ShannonAI/mrc-for-flat-nested-ner的实现方式,他的方式更为直接,这里:

1234567891011121314151617181920212223242526272829303132def forward(self, input_ids, token_type_ids=None, attention_mask=None): """ Args: input_ids: bert input tokens, tensor of shape [seq_len] token_type_ids: 0 for query, 1 for context, tensor of shape [seq_len] attention_mask: attention mask, tensor of shape [seq_len] Returns: start_logits: start/non-start probs of shape [seq_len] end_logits: end/non-end probs of shape [seq_len] match_logits: start-end-match probs of shape [seq_len, 1] """ bert_outputs = self.bert(input_ids, token_type_ids=token_type_ids, attention_mask=attention_mask) sequence_heatmap = bert_outputs[0] # [batch, seq_len, hidden] batch_size, seq_len, hid_size = sequence_heatmap.size() start_logits = self.start_outputs(sequence_heatmap).squeeze(-1) # [batch, seq_len, 1] end_logits = self.end_outputs(sequence_heatmap).squeeze(-1) # [batch, seq_len, 1] # for every position $i$ in sequence, should concate $j$ to # predict if $i$ and $j$ are start_pos and end_pos for an entity. # [batch, seq_len, seq_len, hidden] start_extend = sequence_heatmap.unsqueeze(2).expand(-1, -1, seq_len, -1) # [batch, seq_len, seq_len, hidden] end_extend = sequence_heatmap.unsqueeze(1).expand(-1, seq_len, -1, -1) # [batch, seq_len, seq_len, hidden*2] span_matrix = torch.cat([start_extend, end_extend], 3) # [batch, seq_len, seq_len] span_logits = self.span_embedding(span_matrix).squeeze(-1) return start_logits, end_logits, span_logits

两个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感觉没有很宽广的发展了。更多可看我这,手动狗头。


1.本站遵循行业规范,任何转载的稿件都会明确标注作者和来源;2.本站的原创文章,会注明原创字样,如未注明都非原创,如有侵权请联系删除!;3.作者投稿可能会经我们编辑修改或补充;4.本站不提供任何储存功能只提供收集或者投稿人的网盘链接。

标签: #依存句法分析 #捂脸欢迎star