人类历史上以文字形式记载和流传的知识占整体知识的80%以上,随着社会的发展以及互联网产业的爆发,文本表示的数据规模呈爆炸式增长。自然语言处理(NLP)作为处理大规模文本数据的核心技术,在信息检索、智能问答、智慧决策等众多领域扮演着举足轻重的角色,机器阅读理解(Machine Reading Comprehension,MRC)则是其中最新最热门的课题之一。本次百度人工智能开源大赛设立面向观点型问题的机器阅读理解任务,聚焦于预测答案段落摘要中所表述的是非观点极性。
在此次大赛的冠军方案中,笔者所在团队基于飞桨深度学习框架,通过基于数据分布的数据增强、预训练模型微调、标签平滑、对抗训练、伪标签、多模型融合等方法来提升模型的稳定性与泛化能力并在该任务上取得了不错的表现。
赛题背景
本次竞赛设立了面向观点型问题的机器阅读理解任务,机器阅读理解(Machine Reading Comprehension)是指让机器阅读文本,然后回答和阅读内容相关的问题,阅读理解是自然语言处理和人工智能领域的重要前沿课题,对于提升机器智能水平、使机器具有持续知识获取能力具有重要价值,近年来受到学术界和工业界的广泛关注。面向观点型问题的机器阅读理解源于真实的应用场景:在智能搜索问答等产品中,针对用户输入的观点型问题,搜索引擎首先会检索相关候选文档,然后从候选文档中抽取出能够回答用户问题的答案段落摘要,最后给出答案段落摘要所包含的是非观点。本次任务聚焦于预测答案段落摘要中所表述的是非观点极性。
数据集范围:所有问题均为搜索引擎中按照用户行为分布的观点类的问题,对应的文档为搜索引擎搜索出来的网页,答案片段摘要是经过人工标注的回答此问题的答案片段,是非观点极性是此答案片段摘要相对于问题的观点极性。
是非观点极性:所有极性均在{“ Yes”, “No“, “Depends“}集合中,对应的含义为:
Yes:肯定观点,肯定观点指的是答案给出了较为明确的肯定态度。有客观事实的从客观事实的角度出发,主观态度类的从答案的整体态度来判断。
No:否定观点,否定观点通常指的是答案较为明确的给出了与问题相反的态度。
Depends:无法确定/分情况,主要指的是事情本身存在多种情况,不同情况下对应的观点不一致;或者答案本身对问题表示不确定,要具体具体情况才能判断。
评价指标:竞赛基于测试集人工标注的观点答案,采用答案分类的准确率(Accuracy)作为评价指标:
数据预处理与数据增强
1. 数据清洗
处理文本数据的第一步就是预处理。从网页上爬取的文本数据绝大多数都是高度非结构化,本质上充满噪声。为了获得更好的理解,或者构建更好的算法,使用干净的数据才是根本。本次比赛中提供的数据集中,训练集与测试集数据中question与answer字段已经通过人工清洗与标注,本身比较干净,相比之下测试集的数据则更加原始,里面存在大量的噪声字符,需要进一步清洗,主要考虑从以下方面进行处理:
HTML标签清理:测试集的数据包含了大量html标签,应该去掉这些实体,可以用正则表达式直接删除。
移除不必要的标点符号:所有的标点符号都要按照优先级处理。比如句号(英文中是点号)、逗号、问号是重要的标点,应该被保留,而其他的则需要移除。
移除表情符:数据集中包含人类表情符号或颜文字等特殊符号。这些表情通常跟说话内容不相关,因此需要移除。
移除特殊符号:将#$¥%+<=>^`{|}~#%《{}“”‘’【】等特殊字符清理去除。
去除URL:测试集中包含大量的URL链接,但是这些链接通常对观点判断没有直接关系,因此应当去除。
去除连续多余符号:数据中通常有大量的连续空格、连续句号等现象,应当进行删减。
去除末尾固定签名:数据中含有大量如:“若有帮助,请采纳吧“等对观点预测无用的冗余信息,应当清除。
2. 文本长度分布
如上图所示,训练集、验证集与测试集的文本长度分布跨度大,总体上测试集文本相对较长,训练集与验证集分布大体一致。了解文本长度分布有助于指导后续的数据增强策略与模型超参选择。
对于数据集中的长文本,由于观点型数据中指示性强的回答通常在文本的开头或结尾,因此可将长本文按照head-tail策略进行截断(保留前后max_seq_len//2个字符)。同时,对于长文本问题可以在模型训练时设置更长max seq len,本团队方案中将max seq len设置为256至384。
3. 数据增强
本次比赛采用多种方式进行数据增强,主要有EDA、回译、多并一等,后通过一定比例抽取与原训练集和验证集组成新的数据集,再对数据进行划分,用于K折交叉验证。
EDA,即简单数据增强(easy data augmentation)[1],包括了四种方法:同义词替换、随机插入、随机交换、随机删除。作者表示,平均情况下,仅使用50%的原始数据,再使用EDA进行数据增强,能取得和使用所有数据情况下训练得到的准确率。EDA生成类似于原始数据的增强数据会引入一定程度的噪声,有助于防止过拟合;使用EDA可以通过同义词替换和随机插入操作引入新的词汇,允许模型泛化到那些在测试集中但不在训练集中的词汇。
同义词替换(Synonym Replacement, SR):以一定比例,从句子中随机选取n个不属于停用词集的单词,并随机选择其同义词替换它们;
随机插入(Random Insertion, RI):随机的找出句中某个不属于停用词集的词,并求出其随机的同义词,将该同义词插入句子的一个随机位置。重复n次;
随机交换(Random Swap, RS):随机的选择句中两个单词并交换它们的位置。重复n次;
随机删除(Random Deletion, RD):以一定的概率,随机的移除句中的每个单词;
值得一提的是,长句子相对于短句子存在一个特性:长句比短句有更多的词汇,因此长句在保持原有的类别标签的情况下,能吸收更多的噪声。短句子可能在进行EDA后反而可能因为噪声过大而影响性能,因此在使用EDA进行数据增强时考虑较长的句子(seq_len > 32)进行增强。
回译是通过使用百度或谷歌的翻译平台API将原始的数据从中文翻译为英文后再回译,通过将训练集中的question与answer进行中英互译的操作,可以使得在大体上不改变语义的情况下进行数据增强。
多并一是通过将相同question与label样本的answer进行合多(2~)为一进行数据增强,值得一提的是在本次比赛中的训练集的样本长度分布小于测试集的样本长度分布,通过此方案在一定程度上可以缓解训练集和测试集的文本分布不一致。
原始数据通过以上方式增强后,从中选取新的数据加入至训练集中组成新的数据集,并使新数据集的标签分布大致与原数据保持一致。通过多并一以及EDA增强时只针对较长的文本的策略在一定程度上还可以缓解训练集与测试集的文本分布不一致的问题。数据增强可以对数据进行扩充并适量引入一定的噪声,能在一定程度上提升模型的鲁棒性,有效的防止过拟合。
模型微调
观点阅读理解模型主要基于多种预训练模型进行微调,其中涉及RoBERTa[2]、ERNIE[3]、BERT[4]的多个中文预训练模型,如:RoBERTa-wwm-large-ext、RoBERTa-large-pair、RoBERTa-zh-L、RoBERTa-wwm-ext、ERNIE1.0、BERT_zh_large等。
微调结构上,通常使用类bert模型进行文本分类预测时,样本通过encoder进行embdding后通过由fc层与softmax层组成的Task Layer进行预测。本团队方案中,采用多头机制,任务预测层由多个Task Layer组成,并将预测结果进行求和平均;Task Layer中在fc层前进行一定比例的dropout,由于不同Task Layer中dropout的差异,使得多头具有多样性,从而提升模型的鲁棒性和准确性。
损失函数
分类问题通常使用交叉熵损失函数,本次赛题数据集样本类别分布存在一定的不均衡,为了缓解样本不均衡问题,通常在交叉熵损失函数前加一个参数,即:
数据样本除了类别分布不均衡外,仍存在一定的难易样本不均衡的问题。尽管平衡了正负样本,但是对难以样本的平衡并没有帮助。易分样本通常较多,但是对模型的提升效果较小,因此模型应该适度的关注哪些难分样本。因此,引入focal loss损失函数[5]:
这样通过参数可以将不同置信度的样本损失进行不同程度的衰减,易分样本的衰减相对更剧烈,因此可以是模型将注意力更倾向于难分样本。
Focal Loss损失函数包含两个超参数:。参数的设定可以根据数据集通过类别分布计算得出,也可以手动设定;实验中发现通过类别分布计算出的并不是最优,手动精调设定可以达到更优的效果。参数控制着模型对于难例样本的倾向性,如果设置过大,容易导致模型过度关注于难例样本,反而使模型的效果变差,因此以1.1~1.5为宜。Focal Loss的飞桨实现如下:
def softmax_with_focal_loss(logits, label, class_dim=2, gamma=2.0, alpha=None, smooth=None, return_softmax=True):
if alpha is None:
alpha = np.ones((class_dim, 1), dtype=float)
else:
alpha = np.reshape(np.array(alpha, dtype=float), (class_dim, 1))
alpha = fluid.layers.create_parameter(shape=[class_dim, 1],
dtype='float32',
name='alpha',
default_initializer=fluid.initializer.NumpyArrayInitializer(alpha))
alpha.stop_gradient = True
epsilon = 1e-8
one_hot_key = fluid.layers.one_hot(input=label, depth=class_dim)
alpha_matrix = fluid.layers.matmul(one_hot_key, alpha, transpose_x=False, transpose_y=False)
alpha_raw = fluid.layers.squeeze(input=alpha_matrix, axes=[1])
if smooth:
one_hot_key = fluid.layers.label_smooth(label=one_hot_key, epsilon=smooth, dtype="float32")
probs = fluid.layers.softmax(logits) # softmax of logits
pt = fluid.layers.reduce_sum(one_hot_key * probs, dim=-1) + epsilon
logpt = fluid.layers.log(pt)
loss = -1.0 * fluid.layers.pow((1 - pt), gamma) * logpt * alpha_raw
if return_softmax:
return loss, probs
else:
return loss
标签平滑
在本文方案中,focal loss的损失函数中加入了标签平滑(label smoothing),标签平滑[6]是一种防止模型过拟合的正则化手段。在传统的分类任务计算损失的过程中,是将真实的标签做成one-hot的形式,然后使用损失函数来计算损失。而label smoothing是将真实的one hot标签做一个标签平滑处理,使得标签变成soft label。其中,在真实label处的概率值接近于1,其他位置的概率值是个非常小的数。在label smoothing中有个参数epsilon,描述了将标签软化的程度,该值越大,经过label smoothing后的标签向量的标签概率值越小,标签越平滑,反之,标签越趋向于hard label。较大的模型使用label smoothing可以有效的提升模型的精度,较小的模型使用此种方法可能会降低模型精度。本次比赛中,在多个roberta_large预训练模型微调过程中使用有一定的提升。
优化器
本次比赛,团队采用的是AdamW优化器[7]。AdamW是在Adam的基础上加入了权重衰减。AdamW是在Adam+L2正则化的基础上进行改进的算法。实际上,L2正则化和权重衰减在大部分情况下并不等价,只在SGD优化的情况下是等价的。而大多数框架中对于Adam+L2正则使用的是权重衰减的方式,将两者混为一谈。如果优化器引入L2正则项,在计算梯度的时候会加上对正则项求梯度的结果。
那么如果本身比较大的一些权重对应的梯度也会比较大,由于Adam计算步骤中减去项会有除以梯度平方的累积,使得减去项偏小。按常理说,越大的权重应该惩罚越大,但是在Adam并不是这样。而权重衰减对所有的权重都是采用相同的系数进行更新,越大的权重显然惩罚越大。Ilya Loshchilov和Frank Hutter在他们的文章中[7]建议应该使用Adam的权重衰减。
optimizer = fluid.optimizer.Adam(learning_rate=scheduled_lr)
for param in train_program.global_block().all_parameters():
param_list[param.name] = param * 1.0
param_list[param.name].stop_gradient = True
_, param_grads = optimizer.minimize(loss)
if weight_decay > 0:
for param, grad in param_grads:
if exclude_from_weight_decay(param.name):
continue
with param.block.program._optimized_guard(
[param, grad]), fluid.framework.name_scope("weight_decay"):
updated_param = param - param_list[
param.name] * weight_decay * scheduled_lr
fluid.layers.assign(output=param, input=updated_param)
学习率衰减
在模型训练过程中,采用warmpu以及学习率衰减的方式。学习率衰减可选linear_warmup_decay、noam_decay、piecewise_decay、polynomial_decay。团队实验中,采用精调的分段常数衰减的方法相对较优。
对抗训练
对抗训练目前已经成为提高模型鲁棒性的有效手段,它可以有效减少过拟合,提高泛化能力。对抗训练有两个作用,一是提高模型对恶意攻击的鲁棒性,二是提高模型的泛化能力。在CV任务,根据经验性的结论,对抗训练往往会使得模型在非对抗样本上的表现变差,然而神奇的是,在NLP任务中,模型的泛化能力反而更强,在NLP任务中,对抗训练的角色不再是为了防御基于梯度的恶意攻击,反而更多的是作为一种regularization,提高模型的泛化能力。
本文采用FGM(Fast Gradient Method)对抗训练方法[8],在Embedding层通过loss的梯度对参数矩阵添加扰动,假设输入的文本序列的embedding vectors 为 ,使用对抗训练后的损失函数为:
伪标签
伪标签(Pseudo Labelling)的操作流程如下:
使用训练集训练得到自己最好的模型,然后对测试集进行预测;
筛选出测试集合中的高概率的预测样本(比如预测prob大于0.9,并将预测结果作为样本标签);
将伪标签样本加入模型一起训练再得到自己最好的模型,然后对测试集进行预测,再回到步骤2.
这个操作可以重复多轮,进行前两轮提升较为明显,最后将训练的模型用于测试集的预测。为什么伪标签有效?因为伪标签可以将更多的数据加入到训练集中,且这些数据与测试集其他数据来自同一分布,有助于模型学习。伪标签的实现如下:
for i, prob in enumerate(probs):
if max(prob) >= threshold:
target_qids.append(qids[i])
label_n = int(np.argmax(prob, axis=0).astype(np.float32))
if label_n == 0:
target_labels.append('Yes')
elif label_n == 1:
target_labels.append('No')
else:
target_labels.append('Depends')
模型融合
实验过程中采用了多模型融合的策略,通过将不同模型进行融合可以结合各个模型得到预测特点,从而提高模型的鲁棒性。首先对单个模型进行K折交叉验证训练,获取测试集预测结果的probs,单模的多个预测probs做平均,再与其他模型的预测结果做加权平均或bagging进行融合,最终的预测结果做线上测试。
def mean_ensemble(probs):
probs = np.array(probs)
prob = np.mean(probs, axis=0)
prob_preds = np.argmax(prob, axis=1).astype(np.float32)
return prob, prob_preds
def weight_ensemble(probs, weights):
probs = np.array(probs)
assert len(probs) == len(weights)
total_prob = np.zeros(probs[0].shape)
print(total_prob.shape)
for weight, prob in zip(weights, probs):
total_prob += weight * prob
prob = total_prob / sum(weights)
prob_preds = np.argmax(prob, axis=1).astype(np.float32)
return prob, prob_preds
赛后总结
在此次是非观点阅读理解任务中本文方案重点关注了数据特征与预测方案的泛化能力,采用数据增强、多头预测、标签平滑、对抗训练、多模型融合等技术来提高模型的稳定性与泛化能力。并在一定程度上关注于数据分布的差异、样本标签分布的不均衡以及难例的预测等问题,并最终在测试集上达到准确率83.49%的不错表现。
由于本次赛事时间有限,许多策略尚未尝试,未来可从领域数据多任务深度预训练、更优的预训练模型微调(NEZHA[9]、ERNIE2.0[10]、Albert[11]等)、更优的对抗训练(PGD[12]、FreeLB[13]、YOPO[14]等)以及更有效的模型融合策略(XGBoost[15]等)等方面入手,进一步提升模型的预测性能。