引言
这是第二篇文章,因为主客体提取需要依赖触发词识别。上一篇是讯飞2020年事件提取比赛第一名-触发词提取。
1. 跑通代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| args = TrainArgs().get_parser() args.gpu_ids = '0' args.mode = "train" args.raw_data_dir = './data/final/raw_data' args.mid_data_dir = './data/final/mid_data' args.aux_data_dir = "./data/final/preliminary_clean" args.bert_dir = '/home/yuzhang/PycharmProjects/xf_event_extraction2020Top1/bert/torch_roberta_wwm' args.output_dir = './out' args.bert_type = 'roberta_wwm' args.task_type = 'role1' args.max_seq_len = 320 args.train_epochs = 6 args.train_batch_size = 3 args.lr = 2e-5 args.other_lr = 2e-4 args.attack_train = "pgd" args.swa_start = 4 args.eval_model = True args.enhance_data = True args.use_trigger_distance = True args.use_distant_trigger = True
|
2. 模型结构
3. 模型结构
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| Role1Extractor( (bert_module): BertModel() (dropout_layer): Dropout(p=0.1, inplace=False) (conditional_layer_norm): ConditionalLayerNorm( (weight_dense): Linear(in_features=1536, out_features=768, bias=False) (bias_dense): Linear(in_features=1536, out_features=768, bias=False) ) (trigger_distance_embedding): Embedding(512, 256) (layer_norm): LayerNorm((1024,), eps=1e-12, elementwise_affine=True) (mid_linear): Sequential( (0): Linear(in_features=1024, out_features=128, bias=True) (1): ReLU() (2): Dropout(p=0.1, inplace=False) ) (obj_classifier): Linear(in_features=128, out_features=2, bias=True) (sub_classifier): Linear(in_features=128, out_features=2, bias=True) (activation): Sigmoid() (criterion): BCELoss() )
|
4. 训练中需要注意的点
1. label构造
具体代码这里。因为这个任务作者只提取主客体,所以每一个label的长度为4。
前两个为客体的开始和结束,后两个为主体的开始和结束。
所在的index对应句子的位置。
具体代码这里。
比如:
1 2 3 4 5 6 7 8 9 10 11
| label = [ [0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0], [1, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0], [0, 1, 0, 0], [0, 0, 0, 0],
]
|
看到这里,基本就明白主体思路和触发词提取是一样的。
2.相对位置编码
这个是指引入了一个新的feature,这个feature是以trigger位置来算前面和后面的位置编码。比如trigger为(32,33),那(32,33)的位置编码为0,左右两边递增。
比如:
1
| distince_feature = [3,2,1,0,0,1,2,3]
|
我对这一步的做法能带来多大的提升保留质疑。
所以此处就不细讲了。以后有机会试试效果。
3. conditional layer norm
这个作者的思想来自苏剑林的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 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 41 42 43 44 45
| class ConditionalLayerNorm(nn.Module): def __init__(self, normalized_shape, eps=1e-12): super().__init__()
self.eps = eps
self.weight = nn.Parameter(torch.Tensor(normalized_shape)) self.bias = nn.Parameter(torch.Tensor(normalized_shape))
self.weight_dense = nn.Linear(normalized_shape * 2, normalized_shape, bias=False) self.bias_dense = nn.Linear(normalized_shape * 2, normalized_shape, bias=False)
self.reset_weight_and_bias()
def reset_weight_and_bias(self): """ 此处初始化的作用是在训练开始阶段不让 conditional layer norm 起作用 """ nn.init.ones_(self.weight) nn.init.zeros_(self.bias)
nn.init.zeros_(self.weight_dense.weight) nn.init.zeros_(self.bias_dense.weight)
def forward(self, inputs, cond=None): assert cond is not None, 'Conditional tensor need to input when use conditional layer norm' cond = torch.unsqueeze(cond, 1)
weight = self.weight_dense(cond) + self.weight bias = self.bias_dense(cond) + self.bias
mean = torch.mean(inputs, dim=-1, keepdim=True) outputs = inputs - mean
variance = torch.mean(outputs ** 2, dim=-1, keepdim=True) std = torch.sqrt(variance + self.eps)
outputs = outputs / std outputs = outputs * weight + bias
return outputs
|
作者说添加了这个layer后效果有小幅度提升,以后可以试试。
如果说有很大创新的吧,算不上,我觉得把bert output不做layer norm最终效果也大差不差。
备注:
这里有个问题,如果触发词的长度是变长的话,怎么用呢?估计引入一个mask,算出来。
以后试试。
4. 多feature layer norm
这地方作者代码是在使用了trigger相对位置编码后和bert output进行concat到一起时用到的,如下所示。
1 2 3 4 5 6 7 8
| if self.use_trigger_distance: assert trigger_distance is not None, \ 'When using trigger distance features, trigger distance should be implemented'
trigger_distance_feature = self.trigger_distance_embedding(trigger_distance) seq_out = torch.cat([seq_out, trigger_distance_feature], dim=-1) seq_out = self.layer_norm(seq_out)
|
这里的做法觉得是平时没注意到的一个点~
平时俩向量直接concat到一起完事。这里还进行了一个layer norm。算是一个挺细心的点。至于能不能带来效果提升,此处就不特别注意啦。
5. 计算loss
1 2 3 4 5 6 7 8
| if labels is not None: masks = torch.unsqueeze(attention_masks, -1)
labels = labels.float() obj_loss = self.criterion(obj_logits * masks, labels[:, :, :2]) sub_loss = self.criterion(sub_logits * masks, labels[:, :, 2:])
loss = obj_loss + sub_loss
|
这里没什么特别需要注意的地方了,这里和trigger的做法类似,只是这里分成了两个loss,一个是subject loss,一个是object loss。
6. 解码
这部分没细看了,猜测和trigger解码应该也是类似的。
7. 备注
关于conditional layer norm
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 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64
|
from torch import nn import torch
class ConditionalLayerNorm(nn.Module): def __init__(self, normalized_shape, eps=1e-12): super().__init__()
self.eps = eps
self.weight = nn.Parameter(torch.Tensor(normalized_shape)) self.bias = nn.Parameter(torch.Tensor(normalized_shape))
self.weight_dense = nn.Linear(normalized_shape * 2, normalized_shape, bias=False) self.bias_dense = nn.Linear(normalized_shape * 2, normalized_shape, bias=False)
self.reset_weight_and_bias()
def reset_weight_and_bias(self): """ 此处初始化的作用是在训练开始阶段不让 conditional layer norm 起作用 """ nn.init.ones_(self.weight) nn.init.zeros_(self.bias)
nn.init.zeros_(self.weight_dense.weight) nn.init.zeros_(self.bias_dense.weight)
def forward(self, inputs, cond=None): assert cond is not None, 'Conditional tensor need to input when use conditional layer norm' cond = torch.unsqueeze(cond, 1)
weight = self.weight_dense(cond) + self.weight bias = self.bias_dense(cond) + self.bias
mean = torch.mean(inputs, dim=-1, keepdim=True) outputs = inputs - mean
variance = torch.mean(outputs ** 2, dim=-1, keepdim=True) std = torch.sqrt(variance + self.eps)
outputs = outputs / std outputs = outputs * weight + bias
return outputs
if __name__ == '__main__':
bert_output = torch.rand(32, 128, 768) trigger_feature = bert_output[:, 56:58, :]
trigger_feature = trigger_feature.view(32, 768 * 2) cln = ConditionalLayerNorm(768) output = cln(bert_output, trigger_feature) print(output)
|