语义匹配(一)【NLP论文复现】Sentence-BERT 句子语义匹配模型的tensorflow实现以及训练Trick
Sentence-BERT 句子語義匹配模型的tensorflow實現以及訓練trick
- 論文模型回顧
- 建模與訓練
- 模型代碼部分
- 數據處理
- 訓練
- 模型訓練Trick
- trick1 warm up
- 代碼實現:
- trick2 focal loss
- 代碼實現:
- 總結與思考
論文模型回顧
論文鏈接:https://arxiv.org/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)bert_ner_model.compile(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#使用方法: model.compile(optimizer=optimizer, loss=softmax_focal_loss,metrics=[tf.keras.metrics.SparseCategoricalAccuracy()])效果:
在公開數據集上:該focal_loss可以很好的抑制模型過擬合且模型效果也有1個多點的提升。
總結與思考
本次訓練使用的數據集:LCQMC 是哈爾濱工業大學在自然語言處理國際頂會 COLING2018 構建的問題語義匹配數據集,其目標是判斷兩個問題的語義是否相同。準確率能達到90%+
但在實際測試時發現,模型推理相似的問句條件比較嚴格,無法做到真的根據語義進行匹配,(對于同義詞、別名等無法識別區分)需要應用到實際生產工作則對訓練樣本的要求比較嚴格。
拓展思考:由于該孿生模型的兩個句子共享一個bert參數,因此要求兩個句子的分布或者說兩個句子必須來自統一場景,需要在格式、長度、風格、句式上比較相近。因此在問句匹配、句子相似度判斷等工作上能有不錯的表現。但可能不適用于類似評論于商品相關度等任務的分析(因為評論文本于商品介紹文本不統一,經過同一個Bert會產生偏差)因此思考對于此類問題,借鑒雙塔模型,使用兩個不同的Bert來提取兩種分布的句子特征,或許仍能有不錯的標簽,之后有機會會試驗一下~
總結
以上是生活随笔為你收集整理的语义匹配(一)【NLP论文复现】Sentence-BERT 句子语义匹配模型的tensorflow实现以及训练Trick的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 文本分类(一)EWECT微博情绪分类大赛
- 下一篇: 信息抽取(一)机器阅读理解——样本数据处