赛题分析
数据样例
首先,对比赛提供的数据进行分析,数据的内容和形式如下:
文 号:(2016)豫1402刑初53号 段落内容:商丘市梁园区人民检察院指控:1、2015年7月17日、18日,被告人刘磊、杜严二人分别在山东省单县中心医院和商丘市工行新苑盗窃现代瑞纳轿车两辆,共价值人民币107594元。其中将一辆轿车低价卖给被告人苗某某,被告人苗某某明知是赃车而予以收购。公诉机关向法庭提供了被告,是被告人供述、被害人陈述、证人证言、鉴定意见、有关书证等证据,认为被告刘磊、杜严的行为触犯了《中华人民共和国刑法》第二百六十四条之规定,构成盗窃罪。系共同犯罪。被害人苗某某的行为触犯了《中华人民共和国刑法》第二百一十二条第一款之规定,构成掩饰、隐瞒犯罪所得罪。请求依法判处。 被告人集合:[“刘磊”,“杜严”,“苗某某”] 句 子:1、2015年7月17日、18日,被告人刘磊、杜严二人分别在山东省单县中心医院和商丘市工行新苑盗窃现代瑞纳轿车两辆,共价值人民币107594元 要素原始值:盗窃现代瑞纳轿车 要素名称:盗窃、抢劫、诈骗、抢夺的机动车 被告人:[“刘磊”]
Baseline(official)
任务定义:序列分类
模型描述
class ErnieForElementClassification(ErnieModel): def __init__(self, cfg, name=None): super(ErnieForElementClassification, self).__init__(cfg, name=name) initializer = F.initializer.TruncatedNormal(scale=cfg['initializer_range']) self.classifier = _build_linear(cfg['hidden_size'], cfg['num_labels'], append_name(name, 'cls'), initializer) prob = cfg.get('classifier_dropout_prob', cfg['hidden_dropout_prob']) self.dropout = lambda i: L.dropout(i, dropout_prob=prob, dropout_implementation="upscale_in_train",) if self.training else i @add_docstring(ErnieModel.forward.__doc__) def forward(self, *args, **kwargs): labels = kwargs.pop('labels', None) pooled, encoded = super(ErnieForElementClassification, self).forward(*args, **kwargs) hidden = self.dropout(pooled) logits = self.classifier(hidden) logits = L.sigmoid(logits) sqz_logits = L.squeeze(logits, axes=[1]) if labels is not None: if len(labels.shape) == 1: labels = L.reshape(labels, [-1, 1]) part1 = L.elementwise_mul(labels, L.log(logits)) part2 = L.elementwise_mul(1-labels, L.log(1-logits)) loss = - L.elementwise_add(part1, part2) loss = L.reduce_mean(loss) return loss, sqz_logits else: return sqz_logits
数据去噪
Original:官方提供的原始数据集train.txt。 Preprocessed:将Original数据重新整理,将“被告人集合”拆分成单独的“被告人”。 Denoised:去除Preprocessed中,同时满足问题(1)和(2)的样本。 Denoised_without_no_person:去除Denoised中,存在问题(1)的样本。
def pad_data(file_name, tokenizer, max_len): """ This function is used as the Dataset Class in PyTorch """ # configuration: file_content = json.load(open(file_name, encoding='utf-8')) data = [] for line in file_content: paragraph = line['paragraph'] person = line['person'] element = line['element_name'] sentence = line['sentence'] ovalue = line["ovalue"] label = line['label'] sentence_a = add_dollar2person(person) + element sentence_b = add_star2sentence(sentence, ovalue) src_id, sent_id = tokenizer.encode(sentence_a, sentence_b, truncate_to=max_len-3) # 3 special tokens # pad src_id and sent_id (with 0 and 1 respectively) src_id = np.pad(src_id, [0, max_len-len(src_id)], 'constant', constant_values=0) sent_id = np.pad(sent_id, [0, max_len-len(sent_id)], 'constant', constant_values=1) data.append((src_id, sent_id, label)) return data def make_batches(data, batch_size, shuffle=True): """ This function is used as the DataLoader Class in PyTorch """ if shuffle: np.random.shuffle(data) loader = [] for j in range(len(data)//batch_size): one_batch_data = data[j * batch_size:(j + 1) * batch_size] src_id, sent_id, label = zip(*one_batch_data) src_id = np.stack(src_id) sent_id = np.stack(sent_id) label = np.stack(label).astype(np.float32) # change the data type to compute BCELoss conveniently loader.append((src_id, sent_id, label)) return loader
def train(model, dataset, lr=1e-5, batch_size=1, epochs=10): max_steps = epochs * (len(dataset) // batch_size) # max_train_steps = args.epoch * num_train_examples // args.batch_size // dev_count optimizer = AdamW( # learning_rate=LinearDecay(lr, int(0), max_steps), learning_rate=lr, parameter_list=model.parameters(), weight_decay=0) model.train() logging.info('start training process!') for epoch in range(epochs): # shuffle the dataset every epoch by reloading it data_loader = make_batches(dataset, batch_size=batch_size, shuffle=True) running_loss = 0.0 for i, data in enumerate(data_loader): # prepare inputs for the model src_ids, sent_ids, labels = data # convert numpy variables to paddle variables src_ids = D.to_variable(src_ids) sent_ids = D.to_variable(sent_ids) labels = D.to_variable(labels) # feed into the model outs = model(src_ids, sent_ids, labels=labels) loss = outs[0] loss.backward() optimizer.minimize(loss) model.clear_gradients() running_loss += loss.numpy()[0] if i % 10 == 9: print('epoch: ', epoch + 1, '\tstep: ', i + 1, '\trunning_loss: ', running_loss) running_loss = 0.0 state_dict = model.state_dict() F.save_dygraph(state_dict, './saved/plan3_all/model_'+str(epoch+1)+'epoch') print('model_'+str(epoch+1)+'epoch saved') logging.info('all model parameters saved!')
效果对比
最终与baseline相比,我们的方案在F1、Precision和Recall三项指标上都有明显的提升。在所有25支参赛队伍中排名第一,其中F1和Precision值均为所有参赛队伍最好成绩。
方案总结
本方案将比赛任务重新定义为序列分类任务,这一任务形式将判断要素名称与被告人之间关系所需的关键信息直接作为模型的输入,并且在关键信息处添加了特殊符号,有效增强了关键信息,降低了模型判断的难度。在训练数据方面,本方案剔除了部分噪声数据。
实验结果也表明这一操作能够提升模型的预测表现。在测试阶段,本方案对于句子中没有被告人的情况采取了向前扩一句的方式。这一方式能够解决部分问题,但对于前一句仍不包含被告人的情况效果较差。并且在扩句后,输入序列的长度增加,而输入序列的最大长度不能超过512。因此,本方案仍需解决以下两种情况:
(1) 向前扩句后,句子中仍不包含被告人的情况;
(2) 输入序列较长的情况(分词之后达到1000个token以上)
方案改进