900字范文,内容丰富有趣,生活中的好帮手!
900字范文 > 语义匹配(一)【NLP论文复现】Sentence-BERT 句子语义匹配模型的tensorflow实现以及训练Trick

语义匹配(一)【NLP论文复现】Sentence-BERT 句子语义匹配模型的tensorflow实现以及训练Trick

时间:2019-06-03 20:56:37

相关推荐

语义匹配(一)【NLP论文复现】Sentence-BERT 句子语义匹配模型的tensorflow实现以及训练Trick

Sentence-BERT 句子语义匹配模型的tensorflow实现以及训练trick

论文模型回顾建模与训练模型代码部分数据处理训练模型训练Tricktrick1 warm up代码实现:trick2 focal loss代码实现:总结与思考

论文模型回顾

论文链接:/abs/1908.10084

文章在已有的语义匹配模型的基础上提出了基于Bert的句义匹配孪生网络

模型介绍:将两个句子通过Bert(注意:在对句子相似度建模时,两个句子经过的Bert层应该是共享权重的,及同一个Bert)进行特征提取后,取最后一层的hidde_layers进行pooling,文章试验了直接取CLS向量、max_pooling、mean_pooling,结果显示mean_pooling效果最好。将pooling后得到的两个句子向量进行特征交叉,文章尝试了多种交叉方式,|u-v|的效果最好,当然使用者可以根据具体任务和场景自行尝试多种交叉方法;最后通过softmax层。

训练好模型之后,我们可以将语料库中的句子通过单塔转化为对应的句子向量,当待匹配句子进入时,通过向量相似度检索来直接搜索相似句子,节省了大量的模型推理时间。


建模与训练

tensorflow 2.0.0

transformers 3.1.0

模型代码部分

class BertNerModel(tf.keras.Model):dense_layer = 512class_num = 2drop_out_rate = 0.5def __init__(self,pretrained_path,config,*inputs,**kwargs):super(BertNerModel,self).__init__()config.output_hidden_states = Trueself.bert = TFBertModel.from_pretrained(pretrained_path,config=config,from_pt=True)self.liner_layer = tf.keras.layers.Dense(self.dense_layer,activation='relu')self.softmax = tf.keras.layers.Dense(self.class_num,activation='softmax')self.drop_out = tf.keras.layers.Dropout(self.drop_out_rate)def call(self,input_1):hidden_states_1,_,_ = self.bert((input_1['input_ids'],input_1['token_type_ids'],input_1['attention_mask']))hidden_states_2,_,_ = self.bert((input_1['input_ids_2'],input_1['token_type_ids_2'],input_1['attention_mask_2']))hidden_states_1 = tf.math.reduce_mean(hidden_states_1,1)hidden_states_2 = tf.math.reduce_mean(hidden_states_2,1)concat_layer = tf.concat((hidden_states_1,hidden_states_2,tf.abs(tf.math.subtract(hidden_states_1, hidden_states_2))),1,)drop_out_l = self.drop_out(concat_layer)Dense_l = self.liner_layer(drop_out_l)outputs = self.softmax(Dense_l)print(outputs.shape)return outputs

这里比较难受的是,在自定义模型的时候本来想直接继承transformers的TFBertPreTrainedModel类,但是发现这传入训练数据的时候需要以元组的形式传入,但是在tf model.fit的时候会报错无法识别元组+datasets的数据,因此这里改为继承tf.keras.Model,在类中直接加入TFBertModel.from_pretrained加载之后的TFBertModel,再在后面接自定义的层。

数据处理

def data_proceed(path,batch_size,tokenizer):data = pd.read_csv(path)data = data.sample(frac=1)inputs_1 = tokenizer(list(data['sentence1']), padding=True, truncation=True, return_tensors="tf",max_length=30)inputs_2 = tokenizer(list(data['sentence2']), padding=True, truncation=True, return_tensors="tf",max_length=30)inputs_1 = dict(inputs_1)inputs_1['input_ids_2'] = inputs_2['input_ids']inputs_1['token_type_ids_2'] = inputs_2['token_type_ids']inputs_1['attention_mask_2'] = inputs_2['attention_mask']label = list(data['label'])steps = len(label)//batch_sizex = tf.data.Dataset.from_tensor_slices((dict(inputs_1),label))return x,steps

训练

optimizer = tf.keras.optimizers.Adam(learning_rate=5e-5)pile(optimizer=optimizer,loss='sparse_categorical_crossentropy',metrics=['acc'])bert_ner_model.fit(train_data,epochs=5,verbose=1,steps_per_epoch=steps_per_epoch,validation_data=test_data,validation_steps=validation_steps)


模型训练Trick

trick1 warm up

原文中提到了在训练时warm up learning rate的训练技巧

由于刚开始训练时,模型的权重(weights)是随机初始化的,此时若选择一个较大的学习率,可能带来模型的不稳定(振荡),选择Warmup预热学习率的方式,可以使得开始训练的几个epoches或者一些steps内学习率较小,在预热的小学习率下,模型可以慢慢趋于稳定,等模型相对稳定后再选择预先设置的学习率进行训练,使得模型收敛速度变得更快,模型效果更佳。

该段摘自深度学习训练策略-学习率预热Warmup

代码实现:

class WarmupExponentialDecay(Callback):def __init__(self,lr_base=0.0002,decay=0,warmup_epochs=0,steps_per_epoch=0):self.num_passed_batchs = 0 #一个计数器self.warmup_epochs=warmup_epochs self.lr=lr_base #learning_rate_baseself.decay=decay #指数衰减率self.steps_per_epoch=steps_per_epoch #也是一个计数器def on_batch_begin(self, batch, logs=None):# params是模型自动传递给Callback的一些参数if self.steps_per_epoch==0:#防止跑验证集的时候呗更改了if self.params['steps'] == None:self.steps_per_epoch = np.ceil(1. * self.params['samples'] / self.params['batch_size'])else:self.steps_per_epoch = self.params['steps']if self.num_passed_batchs < self.steps_per_epoch * self.warmup_epochs:K.set_value(self.model.optimizer.lr,self.lr*(self.num_passed_batchs + 1) / self.steps_per_epoch / self.warmup_epochs)else:K.set_value(self.model.optimizer.lr,self.lr*((1-self.decay)**(self.num_passed_batchs-self.steps_per_epoch*self.warmup_epochs)))self.num_passed_batchs += 1def on_epoch_begin(self,epoch,logs=None):#用来输出学习率的,可以删除print("learning_rate:",K.get_value(self.model.optimizer.lr))


trick2 focal loss

在实际应用中,负样本往往来自于负采样,大量的负采样会时训练时负样本数量远多余正样本数量导致训练样本不平衡,且软负采样的负样本往往非常弱,在模型推理时置信度一般较高,加入focal loss可以让模型专注于那些置信度低的比较难区分的样本,提高模型的训练效果。

详细可以查看我的之前的博客Tensorlfow2.0 二分类和多分类focal loss实现和在文本分类任务效果评估

代码实现:

def sparse_categorical_crossentropy(y_true, y_pred):y_true = tf.reshape(y_true, tf.shape(y_pred)[:-1])y_true = tf.cast(y_true, tf.int32)y_true = tf.one_hot(y_true, K.shape(y_pred)[-1])return tf.keras.losses.categorical_crossentropy(y_true, y_pred)def loss_with_gradient_penalty(model,epsilon=1):def loss_with_gradient_penalty_2(y_true, y_pred):loss = tf.math.reduce_mean(sparse_categorical_crossentropy(y_true, y_pred))embeddings = model.variables[0]gp = tf.math.reduce_sum(tf.gradients(loss, [embeddings])[0].values**2)return loss + 0.5 * epsilon * gpreturn loss_with_gradient_penalty_2#使用方法:pile(optimizer=optimizer, loss=softmax_focal_loss,metrics=[tf.keras.metrics.SparseCategoricalAccuracy()])

效果:

在公开数据集上:该focal_loss可以很好的抑制模型过拟合且模型效果也有1个多点的提升。


总结与思考

本次训练使用的数据集:LCQMC 是哈尔滨工业大学在自然语言处理国际顶会 COLING 构建的问题语义匹配数据集,其目标是判断两个问题的语义是否相同。准确率能达到90%+

但在实际测试时发现,模型推理相似的问句条件比较严格,无法做到真的根据语义进行匹配,(对于同义词、别名等无法识别区分)需要应用到实际生产工作则对训练样本的要求比较严格。

拓展思考:由于该孪生模型的两个句子共享一个bert参数,因此要求两个句子的分布或者说两个句子必须来自统一场景,需要在格式、长度、风格、句式上比较相近。因此在问句匹配、句子相似度判断等工作上能有不错的表现。但可能不适用于类似评论于商品相关度等任务的分析(因为评论文本于商品介绍文本不统一,经过同一个Bert会产生偏差)因此思考对于此类问题,借鉴双塔模型,使用两个不同的Bert来提取两种分布的句子特征,或许仍能有不错的标签,之后有机会会试验一下~

本内容不代表本网观点和政治立场,如有侵犯你的权益请联系我们处理。
网友评论
网友评论仅供其表达个人看法,并不表明网站立场。