当前位置:   article > 正文

依存句法分析

依存句法分析

捂脸

欢迎star ^_^

定义

HanLP的定义

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

百度ddparser的定义

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

LTP的定义

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

示例图

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

数据集解释

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

123456789
1	新华社	_	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为词和词构成依存关系,以及对应的关系是什么,所以这里是一个纯粹的分类问题。

数据处理代码可参考这里

按照依存句法的定义:

  1. 当前词只能依存一个其他词,但是可以被多个其他词所组成依存关系。
  2. 如果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的代码如下:

12345678910111213141516171819202122232425262728293031323334353637383940
import 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的实现方式,他的方式
更为直接,这里

1234567891011121314151617181920212223242526272829303132
def 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_darc_mlp_h,一组叫rel_mlp_drel_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,为啥我这么叫,因为就是获取概率最大的那棵树嘛,这里作者提供了两种算法来实现,eisnermst,具体实现就不讲了。

总结

至此可以看出,在biaffine那层获取词和词之间的关联程度,非常nice的做法,后面就是将其变成一个分类问题来解决,arc分类和rel分类是不同的,这个需要注意。

再额外插一句,感觉目前的句法分析就是依存句法的天下了哇,像Constituency Parser感觉没有很宽广的发展了。更多可看我这,手动狗头。

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/寸_铁/article/detail/884781
推荐阅读
相关标签
  

闽ICP备14008679号