依存句法分析

本文介绍了依存句法分析的定义及其在自然语言处理中的应用,重点讨论了HanLP、百度ddparser和LTP的实现技术,特别是biaffine方法的详细步骤,包括数据处理、模型结构和loss计算。文章还对比了不同实现方式并指出其优缺点。
摘要由CSDN通过智能技术生成

捂脸

欢迎star ^_^

定义

HanLP的定义

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

百度ddparser的定义

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

LTP的定义

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

示例图

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

数据集解释

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

1
2
3
4
5
6
7
8
9
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的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
import torch

# 假设768是mlp出来的hidden_size.
# batch_size, sequence_length, hidden_size = 32, 128,768

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
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感觉没有很宽广的发展了。更多可看我这,手动狗头。

  • 1
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值