本次赛题的本质是nlp的阅读理解,由于本次比赛可以使用预训练模型和外部语料库,因此如何选择预训练模型以及如何对模型输出的概率进行答案提取就成为了比赛的关键,本文针对预训练模型的选择和使用以及提取规则的使用进行说明。
本次题目每个训练集样本由四部分组成“样本id”,“文本句子”,“事件类型”以及“事件主体”作为标签,事件主题就是从文本句子中筛选出来的。比赛的评分指标是F1,就是一个样本你可以预测多个事件主体,和真实的事件主体比较,考虑召回率和准确率的协调。这个F1应该是以词作为对应指标的,而不是字;
在不借助任何语料库和预训练模型,自己搭一个带多头注意力机制的w2v,然后靠这1.4w的数据集去训练,结果很一般,因为数据太少了,a榜只有83.4左右;然后思考自己找语料库(搜狗新闻)去训练词向量,由于机器和时间因素本人没有尝试;最后就是使用预训练模型进行迁移学习,这里选取的是谷歌提供的bert模型进行微调,用的是其官方训练的中文词向量;然后根据规则过滤一些答案,大致成绩是a榜单模89.8,b榜78.3;经过和其他模型的融合,最佳成绩为榜92.6和b榜82.8左右,最终rank17.
本赛题属于nlp任务中的阅读理解,bert在其相关的领域表现出色,这里就使用迁移学习的思想对bert预训练进行微调,为了代码简介,这里使用keras来表现:
bert_model = load_trained_model_from_checkpoint(config_path, checkpoint_path, seq_len=None)
j = 0
for l in bert_model.layers:
l.trainable = True
x1_in = Input(shape=(None,)) # 待识别句子输入
x2_in = Input(shape=(None,)) # 待识别句子输入
s1_in = Input(shape=(None,)) # 实体左边界(标签)
s2_in = Input(shape=(None,)) # 实体右边界(标签)
x1, x2, s1, s2 = x1_in, x2_in, s1_in, s2_in
x_mask = Lambda(lambda x: K.cast(K.greater(K.expand_dims(x, 2), 0), 'float32'))(x1)
x = bert_model([x1, x2])
ps1 = Dense(1, use_bias=False)(x)
ps1 = Dense(1, use_bias=False)(ps1)
ps1 = Lambda(lambda x: x[0][..., 0] - (1 - x[1][..., 0]) * 1e10)([ps1, x_mask])
ps2 = Dense(1, use_bias=False)(x)
ps2 = Lambda(lambda x: x[0][..., 0] - (1 - x[1][..., 0]) * 1e10)([ps2, x_mask])
model = Model([x1_in, x2_in], [ps1, ps2])
train_model = Model([x1_in, x2_in, s1_in, s2_in], [ps1, ps2])
loss1 = K.mean(K.categorical_crossentropy(s1_in, ps1, from_logits=True))
ps2 -= (1 - K.cumsum(s1, 1)) * 1e10
loss2 = K.mean(K.categorical_crossentropy(s2_in, ps2, from_logits=True))
loss = loss1 + loss2
train_model.add_loss(loss)
其中关于mask的问题请参考这里
我们可以看出,针对标注问题,输出的答案和输入的文本是对齐的,这里的模型输出是答案开头和结尾的概率值,那么如何选取好的提取答案规则就十分重要了。
我们很容易发现答案基本全是名词,因此词性标注可以作为答案过滤的方式,这里我们选择在模型融合阶段进行词性过滤。如何从star和end的概率空间映射回答案是一个策略性问题。这里有两种方案。第一种就是先对概率进行预处理然后在选择最优解,具体做法是降低特殊字符的概率,依据是整个训练集答案集合中从未出现的特殊符号出现在测试集的概率很小。其次是从star的概率空间中选出概率最高的文字作为答案的开始,然后遍历之后所有的信息,直到出现特殊字符停止,然后选出在end概率空间中最有可能的作为结束,具体实现如下:
def extract_entity(text_in, c_in):
if c_in not in classes:
return 'NaN'
text_in = u'___%s___%s' % (c_in, text_in)
text_in = text_in[:510]
_tokens = tokenizer.tokenize(text_in)
_x1, _x2 = tokenizer.encode(first=text_in)
_x1, _x2 = np.array([_x1]), np.array([_x2])
_ps1, _ps2 = model.predict([_x1, _x2])
_ps1, _ps2 = softmax(_ps1[0]), softmax(_ps2[0])
for i, _t in enumerate(_tokens):
if len(_t) == 1 and re.findall(u'[^\u4e00-\u9fa5a-zA-Z0-9\*]', _t) and _t not in additional_chars:
_ps1[i] -= 10
start = _ps1.argmax()
for end in range(start, len(_tokens)):
_t = _tokens[end]
if len(_t) == 1 and re.findall(u'[^\u4e00-\u9fa5a-zA-Z0-9\*]', _t) and _t not in additional_chars:
break
end = _ps2[start:end+1].argmax() + start
a = text_in[start-1: end]
return a
还有一种规则是先从end和star概率空间中选出n个候选,然后对这n个候选进行排列组合,保证end大于star而且差别不大于20。然后统计每个组合的总概率(损失or概率)筛选出最佳答案。
由于这两种方法都是只有一个答案,而且只考虑最佳概率的组合因此可以保证召回率但是准确率很难保证,所以后期模型的融合和规则的融合很关键。所以我们单模和融合的差别是很大的,大致提高了百分之三;
规则的巧妙应用会让我们的分数有所提升,但是这一切都是建立在模型输出概率空间上,因此模型调优和概率层次的融合也很重要;对于输出结果上概率的加权融合,交叉验证可能会让结果更好,但是之前的一些弯路,到后面时间太过紧张来不及交叉验证去训练了,试错成本太高了。
对于nlp迁移学习的调参,主要是传统的超参数——学习率,Batch,训练代数以及主模型的可调层数。
还有迁移学习的模型设计就是主模型后段接的自己的层数,经试验这里的层数虽好只是全连接即可,以防过拟合。
对题目的评分细则没有好好阅读,忽略的F1指标的特性,没有在准确率和召回率之间做一个很好的平衡,过于追求end和star概率空间的可信度。
只是选用bert的预训练模型,而bert的词向量针对性不强,我们的核心主题是金融新闻,所以如果用相似的语料库去训练词向量可能会有更好的结果。
bert的核心机制是transformer,以此为为契机好好研究其工作原理和其他应用。还有对bert源码的阅读,做到可以理解里面机制和运作原理,而不是仅仅当作工具调用。