第一章我們簡單了解了NER任務和基線模型Bert-Bilstm-CRF基線模型詳解&代碼實現,這一章按解決問題的方法來划分,我們聊聊多任務學習,和對抗遷移學習是如何優化實體識別中邊界模糊,垂直領域標注樣本少等問題的。Github-DSXiangLi/ChineseNER中提供了bert_bilstm_crf_mtl多任務, 和bert_bilstm_crf_adv對抗遷移兩個模型,支持任意NER+NER,CWS+NER的Joint Training。Repo里上傳了在MSRA+MSR上訓練好的bert_bilstm_crf_mtl模型以及serving相關的代碼, 可以開箱即用喲~
多任務學習
以下Reference中1,2,3都是有關多任務學習來提升NER效果的,簡單說多任務的好處有兩個:
- 引入額外信息:幫助學習直接從主任務中難以提取出的特征
- 學到更通用的文本特征:多個任務都需要的信息才是通用信息,也可以理解成正則化,也可以理解為不同任務會帶來噪聲起到類似bagging的作用
MTL有很多種模型結構,之后我們主要會用到的是前三種,hard,Asymemetry和Customized Sharing, 下面讓我們具體看下MTL在NER任務中的各種使用方式。
詞邊界增強:ner+cws
paper: Improving Named Entity Recognition for Chinese Social Media with Word Segmentation Representation Learning,2016
分詞任務和實體識別任務進行聯合訓練主要體現了以上第一個優點’增加額外信息‘,讓分詞樣本的詞邊界標注信息來提高NER邊界識別的准確率。以下是Ref1中的模型結構,基本就是上面的Asymmetry Sharing。NER和CWS共享character embedding,在NER的CRF層,除了使用character emebdding, NER相關特征以外,還會使用CWS包含分詞信息的最后一層。這里我對使用Asymmetry結構是存疑的,如果CWS和NER任務是對相同樣本分別標注了分詞和實體的話,用Asy確實更合理,但paper中一個是新聞樣本一個是社交領域的樣本,感覺asy會比hard sharing引入更多的噪音,后面我們會用MSRA和MSR數據來做測試。
訓練時因為CWS和NER的樣本量差異較大,作者提出在每個iteration,subsample大樣本會顯著加快模型收斂。我用的樣本本身相差不大,所以也沒有做相應的處理,感覺subsample,或者用不同的batch_size+task weight應該會有相似的效果。
跨領域半監督學習:ner+ner
paper: A unified Model for Cross-Domain and Semi-Supervised Named entity Recognition in Chinese Social Media, 2017
不同領域的NER任務進行聯合學習主要體現了第二個優點‘通用文本特征提取’,用領域外標注樣本和領域內未標注樣本來幫助該領域標注樣本,學習更加通用的文本特征和實體特征。
領域外到內的遷移,主要需要解決樣本差異性問題,畢竟最終目標是希望幫助領域內文本學到合理的文本表達,所以需要penalize和目標領域差異過大的領域外樣本。作者對比了3種方式來衡量樣本x和目標領域的相似度\(func(x, IN)\),其中cosine距離效果最好
- cross-entropy: 用目標領域n-gram模型計算x的熵
- Gaisssian: 用所有目標領域文本embedding求平均構建\(v_{IN}\), 計算\(v_x\)和\(v_{IN}\)的歐式距離
- Polynomial Kernel:\(v_x\)和\(v_{IN}\)的cosine距離
領域內未標注樣本的半監督學習,因為是直接用模型預測來做真實label,因此需要penalize預測置信度低的樣本,這里作者用最優預測,相對次優預測提升的百分比做\(confid(x)\),置信度是動態的需要在每個iteration先對未標注進行預測再得到\(confid(x)\)
整個模型框架是領域內標注/未標注樣本和領域外標注樣本的聯合訓練,以上相似度和置信度用於調整每個iteration訓練時,不同樣本的學習率\(lr = lr_0 *weight(x,t)\)
這篇論文的創新一個在於對無標注樣本的使用,不過個人認為在實際應用時直接使用的概率比較小,因為NER是token級別的分類任務,樣本噪音對全局表現的干擾是比較大,不過用\(confid(x)\)作為主動學習的選擇策略來篩選樣本,讓標注同學進行標注倒是一個可以嘗試的思路。
其二是提出了要用領域相似度來調整lr,雖然考慮到了領域差異,不過解決方案還是相對簡單,只能降低並不能排除領域差異的影響。這里只它當作引子,看之后的對抗遷移學習是如何解決領域差異問題的。
模型實現
repo里的model/bert_bilstm_crf_mtl實現了基於bert-bilstm-crf的多任務聯合訓練框架,根據傳入數據集是ner+ner還是ner+cws可以實現以上的詞增強和跨領域學習。MTL的相關參數主要是task_weight控制兩個任務的loss權重,asymmetry控制模型結構是hard sharing(多任務只共享bert),還是asymmetry(task2使用task1的hidden output)。這里默認傳入數據集順序對應task1&2。
def build_graph(features, labels, params, is_training):
input_ids = features['token_ids']
label_ids = features['label_ids']
input_mask = features['mask']
segment_ids = features['segment_ids']
seq_len = features['seq_len']
task_ids = features['task_ids']
embedding = pretrain_bert_embedding(input_ids, input_mask, segment_ids, params['pretrain_dir'],
params['embedding_dropout'], is_training)
load_bert_checkpoint(params['pretrain_dir']) # load pretrain bert weight from checkpoint
mask1 = tf.equal(task_ids, 0)
mask2 = tf.equal(task_ids, 1)
batch_size = tf.shape(task_ids)[0]
with tf.variable_scope(params['task_list'][0], reuse=tf.AUTO_REUSE):
task_params = params[params['task_list'][0]]
lstm_output1 = bilstm(embedding, params['cell_type'], params['rnn_activation'],
params['hidden_units_list'], params['keep_prob_list'],
params['cell_size'], params['dtype'], is_training)
logits = tf.layers.dense(lstm_output1, units=task_params['label_size'], activation=None,
use_bias=True, name='logits')
add_layer_summary(logits.name, logits)
trans1, loglikelihood1 = crf_layer(logits, label_ids, seq_len, task_params['label_size'], is_training)
pred_ids1 = crf_decode(logits, trans1, seq_len, task_params['idx2tag'], is_training, mask1)
loss1 = tf.reduce_sum(tf.boolean_mask(-loglikelihood1, mask1, axis=0)) * params['task_weight'][0]
tf.summary.scalar('loss', loss1)
with tf.variable_scope(params['task_list'][1], reuse=tf.AUTO_REUSE):
task_params = params[params['task_list'][1]]
lstm_output2 = bilstm(embedding, params['cell_type'], params['rnn_activation'],
params['hidden_units_list'], params['keep_prob_list'],
params['cell_size'], params['dtype'], is_training)
if params['asymmetry']:
# if asymmetry, task2 is the main task using task1 information
lstm_output2 = tf.concat([lstm_output1, lstm_output2], axis=-1)
logits = tf.layers.dense(lstm_output2, units=task_params['label_size'], activation=None,
use_bias=True, name='logits')
add_layer_summary(logits.name, logits)
trans2, loglikelihood2 = crf_layer(logits, label_ids, seq_len, task_params['label_size'], is_training)
pred_ids2 = crf_decode(logits, trans2, seq_len, task_params['idx2tag'], is_training, mask2)
loss2 = tf.reduce_sum(tf.boolean_mask(-loglikelihood2, mask2, axis=0)) * params['task_weight'][1]
tf.summary.scalar('loss', loss2)
loss = (loss1+loss2)/tf.cast(batch_size, dtype=params['dtype'])
pred_ids = tf.where(tf.equal(task_ids, 0), pred_ids1, pred_ids2) # for infernce all pred_ids will be for 1 task
return loss, pred_ids, task_ids
這里我在NER(MSRA)+NER(people_daily), NER+CWS(MSR)上分別嘗試了hard和asymmetry sharing的多任務學習,和前一章的Bert-Bilstm+CRF的benchmark進行對比,整體上來看MTL對MSRA樣本基本沒啥提升,但是對樣本更小的people daily任務有非常顯著約3~4%F1的提升。不過以上paper中使用的asy的多任務結構顯著被沒有帶來顯著提升,反倒是hard sharing只引入輔助任務來幫助bert finetune的效果更好些。不過MTL和任務選擇關系很大,所以以上結論並不能直接遷移到其他任務。
對抗遷移學習
以上多任務學習還有一個未解決的問題就是hard和asymmmetry對共享參數層沒有任何約束,在多任務訓練時任務間的差異會導致帶來信息增益的同時也帶來了額外的噪音,抽取通用特征的同時也抽取了任務相關的私有特征。當輔助任務和主任務差異過大,或者輔助任務噪聲過多時,MTL反而會降低主任務效果
這里任務差異可能是分詞任務和實體識別詞粒度的差異,不同領域NER任務文本的差異等等。前面提到的用領域相似度來對lr加權的方法只能緩解並不能解決問題,下面我們來看下對抗學習是如何把任務相關特征/噪音和通用特征區分開來的。
梯度反轉 GRL
paper: Adversarial Transfer Learning for Chinese Named Entity Recognition with Self-Attention Mechanism, 2018
這里就需要用到以上第三種MTL結構Customized Sharing。我們以NER+CWS任務為例,保留之前的NER tower和CWS tower,加入一個額外的share tower。理想情況是所有通用特征例如粒度相同的詞邊界信息都被share tower學到,而ner/cws任務相關的私有特征分別被ner/cws tower學到。作者通過對share tower加入對抗學習機制,來限制share tower盡可能保留通用特征。模型結構如下【我們用了Bert來抽取信息,self-attention層就可以先忽略了】
中間的share tower是一個task descriminator,先不看Gradient Reversal。其實是從輸入文本中提取雙向文本特征,過maxpooling層得到\(2*d_h\)領域特征,過softmax識別樣本是來自NER還是CWS任務的二分類問題(或多分類問題如果有多個任務)
從propensity score的角度,如果softmax得到的概率都在0.5附近,說明share tower學到的特征無法有效區分task,也就是我們希望得到的通用特征。為了實現這一效果,作者引入minmax對抗機制,softmax判別層盡可能去識別task,share-bilstm特征抽取層盡可能抽取混淆task的通用特征。
其中K是任務,\(N_k\)是任務k的樣本,\(E_s\)是用於通用信息提取的bilstm,\(x_i^k\)是任務k的第i個樣本,以上公式是按多分類任務給出的。
這里作者用了GRL梯度反轉層來實現minmax。softmax學到的用於識別task的特征梯度,反向傳播過gradient reversal層會調轉正負\(-1 * gradient\)再對share-bilstm的參數進行更新,有點像生成器和判別器按相同步數進行同步訓練的GAN的另一種工程實現。之前有評論說梯度反轉有些奇怪,因為目標是讓share-bilstm學到通用特征,而不是學到把CWS判斷成NER,把NER判斷是CWS這種顛倒黑白的特征,個人感覺其實不會因為有minmax對抗機制在,在實際訓練過程中task descriminator確實在一段時間后就會到達probability=0.5 cross-entropy=0.7上下的動態平衡。
模型實現
def build_graph(features, labels, params, is_training):
input_ids = features['token_ids']
label_ids = features['label_ids']
input_mask = features['mask']
segment_ids = features['segment_ids']
seq_len = features['seq_len']
task_ids = features['task_ids']
embedding = pretrain_bert_embedding(input_ids, input_mask, segment_ids, params['pretrain_dir'],
params['embedding_dropout'], is_training)
load_bert_checkpoint(params['pretrain_dir']) # load pretrain bert weight from checkpoint
mask1 = tf.equal(task_ids, 0)
mask2 = tf.equal(task_ids, 1)
batch_size = tf.shape(task_ids)[0]
with tf.variable_scope('task_discriminator', reuse=tf.AUTO_REUSE):
share_output = bilstm(embedding, params['cell_type'], params['rnn_activation'],
params['hidden_units_list'], params['keep_prob_list'],
params['cell_size'], params['dtype'], is_training) # batch * max_seq * (2*hidden)
share_max_pool = tf.reduce_max(share_output, axis=1, name='share_max_pool') # batch * (2* hidden) extract most significant feature
# reverse gradient of max_output to only update the unit use to distinguish task
share_max_pool = flip_gradient(share_max_pool, params['shrink_gradient_reverse'])
share_max_pool = tf.layers.dropout(share_max_pool, rate=params['share_dropout'],
seed=1234, training=is_training)
add_layer_summary(share_max_pool.name, share_max_pool)
logits = tf.layers.dense(share_max_pool, units=len(params['task_list']), activation=None,
use_bias=True, name='logits')# batch * num_task
add_layer_summary(logits.name, logits)
adv_loss = tf.nn.sparse_softmax_cross_entropy_with_logits(labels=features['task_ids'], logits=logits)
adv_loss = tf.reduce_mean(adv_loss, name='loss')
tf.summary.scalar('loss', adv_loss)
with tf.variable_scope('task1_{}'.format(params['task_list'][0]), reuse=tf.AUTO_REUSE):
task_params = params[params['task_list'][0]]
lstm_output = bilstm(embedding, params['cell_type'], params['rnn_activation'],
params['hidden_units_list'], params['keep_prob_list'],
params['cell_size'], params['dtype'], is_training)
lstm_output = tf.concat([share_output, lstm_output], axis=-1) # bath * (4* hidden)
logits = tf.layers.dense(lstm_output, units=task_params['label_size'], activation=None,
use_bias=True, name='logits')
add_layer_summary(logits.name, logits)
trans1, loglikelihood1 = crf_layer(logits, label_ids, seq_len, task_params['label_size'], is_training)
pred_ids1 = crf_decode(logits, trans1, seq_len, task_params['idx2tag'], is_training, mask1)
loss1 = tf.reduce_sum(tf.boolean_mask(-loglikelihood1, mask1, axis=0)) * params['task_weight'][0]
tf.summary.scalar('loss', loss1)
with tf.variable_scope('task2_{}'.format(params['task_list'][1]), reuse=tf.AUTO_REUSE):
task_params = params[params['task_list'][1]]
lstm_output = bilstm(embedding, params['cell_type'], params['rnn_activation'],
params['hidden_units_list'], params['keep_prob_list'],
params['cell_size'], params['dtype'], is_training)
lstm_output = tf.concat([share_output, lstm_output], axis=-1) # bath * (4* hidden)
logits = tf.layers.dense(lstm_output, units=task_params['label_size'], activation=None,
use_bias=True, name='logits')
add_layer_summary(logits.name, logits)
trans2, loglikelihood2 = crf_layer(logits, label_ids, seq_len, task_params['label_size'], is_training)
pred_ids2 = crf_decode(logits, trans2, seq_len, task_params['idx2tag'], is_training, mask2)
loss2 = tf.reduce_sum(tf.boolean_mask(-loglikelihood2, mask2, axis=0)) * params['task_weight'][1]
tf.summary.scalar('loss', loss2)
loss = (loss1+loss2)/tf.cast(batch_size, dtype=params['dtype']) + adv_loss * params['lambda']
pred_ids = tf.where(tf.equal(task_ids, 0), pred_ids1, pred_ids2)
return loss, pred_ids, task_ids
這里我們對比下adv和mtl的效果,。。。不排除我們用了強大的Bert做底層抽取,以及這里的3個任務本身差異並不太大,畢竟在people daily上MTL的效果提升已經十分顯著,所以adv和mtl的差異感覺也就是個隨機波動,之后要是有比較垂的樣本再試試看吧~
Reference
- 【CWS+NER MTL】Improving Named Entity Recognition for Chinese Social Media with Word Segmentation Representation Learning,2016
- 【Cross-Domain LR Adjust】A unified Model for Cross-Domain and Semi-Supervised Named entity Recognition in Chinese Social Media, 2017
- 【MTL】Multi-Task Learning for Sequence Tagging: An Empirical Study, 2018
- 【CWS+NER Adv MTL】Adversarial Transfer Learning for Chinese Named Entity Recognition with Self-Attention Mechanism, 2018
- 【Adv MTL】Adversarial Multi-task Learning for Text Classification, 2017
- Dual Adversarial Neural Transfer for Low-Resource Named Entity Recognition, 2019
- 【GRL】Unsupervised Domain Adaptation by Backpropagation,2015
- 【GRL】Domain-Adversarial Training of Neural Networks, 2016
- https://www.zhihu.com/question/266710153