這篇論文是Facebook在BERT的基礎上發展出來的Cross-Lingual版本,即多語的。BERT的github上實際上也有一個多語版本的,但卻沒有提到是怎麼訓練的,也沒有任何的信息。這裏的XLM提出了一些策略用於多語言學習,並與multi-lingual的BERT進行了對比,效果確實會好。
一. 前言
一開始BERT出來的時候,只有英語的,這對於各個國家的廣大AI愛好者,是十分不便的,大家都希望能有自己國家語言的版本。這不,後面BERT又出了多語言版本,FB也緊跟着出了一個更好的多語言版本(不過貌似語言比較少?主要還是針對翻譯和XNLI任務而定製的,不像BERT的那個那麼多語言,而且很通用)
這裏複述一下作者在第一章總結的他們的貢獻:
- 引入了一個新的無監督方法,用於訓練多語的表徵,並且提出兩個單語的預訓練LM目標
- 提出了一種新的有監督方法,使用平行語料,來增強多語預訓練的表現
- 在跨語言分類、有/無監督機器翻譯任務上,達到了新的SoTA
- 對於resource比較少的語言,用這種預訓練方式很有幫助
- 重點來了!有源碼和預訓練模型
下面來看看XLM具體是怎麼操作的~
PS:不知道有沒有讀者和筆者一樣,一看到這種多語的啊,pair的啊,parallel的啊這類的詞眼,就覺得整個邏輯非常的暈,這篇論文,尤其是實驗部分,真的是結合源碼看了好久,大概率是筆者太菜了吧。。。
二. XLM原理
1. 多語詞表構建
既然是多語的模型嘛,總不可能是一個語言一個model,然後封裝在一起,假裝是多語模型吧,肯定是隻有一個模型的,那麼就要求這個模型能接收各個語言的句子作爲輸入,因此這裏就需要構建包含各個語言詞的多語詞表。
與BERT一樣,這裏也是使用BPE,但不是簡單地把各個語言的bpe詞表進行拼接,那樣也太大了,,這裏是先對多語語料按照如下的概率進行採樣,然後將多語語料進行拼接,最後進行正常的BPE統計。採樣的目的是對大語種的語料和小語種的語料進行一下平滑,省的全採到大語種上面了,小語種連詞表都沒有了(或者小語種都被按照char拆分了)。。這裏取。
2. 預訓練任務
這裏作者提出了三種預訓練任務:CLM、MLM和TLM,下面分別介紹:
- CLM:Causal Language Model,無監督單語單向LM訓練任務,就是用Transformer進行LM的單向訓練。
- MLM:Masked Language Model,無監督單語雙向LM訓練任務,與BERT一樣。
- TLM:Translation Language Model,有監督翻譯LM訓練,拼接平行雙語語料,然後執行MLM,以期這樣能學到翻譯的對齊信息?
對於MLM和TLM的形象化表示可以看下圖:
這裏MLM/TLM的輸入構造時與BERT的不同主要在於:
- BERT在預訓練構造輸入的時候,用的都是pair的輸入方式,其實就是先構建NSP的數據,然後再mask並構造MLM的數據。輸入會規定一個最大長度,然後選擇兩個句子組(句子組的概念就是把物理上相鄰的多個句子當成一整個句子,中間不加入任何句子的分隔符),滿足在這個長度內即可。
- XLM在預訓練的時候,對於CLM和MLM都是用的stream的方式,將多個物理上的句子(不一定相鄰?)通過分隔符連接起來作爲輸入,對於TLM的構造與前者一樣,只不過又拼接了一個平行語料。同時,去掉了BERT裏面的句子id標識,改成了語言的id標識。
3. 預訓練流程
簡而言之就是:CLM/MLM (+TLM),也即從CLM或MLM中選一個進行單語LM的預訓練,然後再根據需求和數據情況,決定要不要加入TLM進行訓練,加入的話就是和前面的CLM/MLM進行交替訓練。
三. 實驗
論文裏面主要驗證了XNLI、無監督機器翻譯和有監督機器翻譯任務的效果,下面分別說:
- XNLI
這個任務其實筆者也是去扒拉了FB之前的論文,才知道是個什麼任務。。其實還是文本蘊含任務,但只不過訓練集裏面都是英文的,驗證集裏面是多語的(15種語言,獲取方法就是把英文的驗證集翻譯過來,同一個pair裏面是同一種語言的)。
先用MLM在各個語言的單語語料上進行訓練(也有加上額外的平行語料進行TLM訓練的部分),然後再用英文訓練集進行finetune,最後在多個語種上評估。結果如下:
- 無監督MT
無監督MT的任務,筆者也是特意去查閱了相關的資料,知道大概是怎麼回事。用大白話說就是,給出兩個語言各自的語料(不一定要平行),機器應該就能學會翻譯,就像人類一樣,在學會了中文和英語之後,應該能進行翻譯,因爲中間的連接是語義,而不是詞表的對應。
無監督MT就是基於這麼一個設定,一般在不考慮pretrain的時候,用的比較多的方法是用去噪自編碼器+循環翻譯,具體來說,對於英譯德這個任務,搭建起encoder-decoder這個模型之後,可以用英文語料加上噪聲,輸入encoder,然後decoder出來原始的英文語料,同理也可以用德文語料加上噪聲,輸入encoder,然後decoder出來原始的德文語料,這個就叫去噪自編碼器,目的其實是在於讓encoder學到語義信息;循環翻譯是個啥?比如en->de->en,就是先讓英文經過encoder和decoder,得到翻譯的德文僞數據,然後將這個德文僞數據,再輸入encoer和decoder,得到原來的英文數據,這樣進行訓練。
那麼在這裏,其實就是用CLM或MLM去初始化encoder和decoder,decoder就初始化那些encoder有的部分,然後後面用上面的套路進行正常流程的訓練即可,這裏對比不同的初始化方法的結果:
- 有監督MT
這裏對比了幾種方法用不同的預訓練方式,結果如下:
- Sennich:這個是之前的SoTA,好像還是用了back-translation+ensemble的方法,也是一個強baseline
- ro->en:這個是用單向的數據進行finetune
- ro<->en:這個使用雙向的數據進行finetune
- ro<->en + BT:用雙向的數據進行finetune,同時進行back-translation(這個好像又是那種先從A->B生成B的僞數據,然後再翻譯回到A)
- 小語種LM
這裏主要是驗證多語訓練對小語種語言模型建模的影響,結果如下:
- 無監督多語embedding
這裏主要是驗證無監督情況下生成的多語embedding的優秀程度,通過驗證各種源單詞和其翻譯對應的詞之間的距離,結果如下:
四. PyTorch實現
這裏主要是分析XLM源碼中關於模型和訓練的部分,因筆者對於論文中的這些任務(如翻譯等)不是特別熟悉,所以全憑README的內容和代碼一步一步摸索,如果有理解錯誤的地方,還請指正~
下面我將按照源碼中README給出的思路順序進行剖析:
- 有/無監督機器翻譯
在機器翻譯這個場景下,論文首先用CLM/MLM對MT的encoder和decoder進行預訓練。其實這裏就是用的多種語言的單語語料,輸入詞表是多語的,然後用CLM/MLM訓練語言模型,並將其參數作爲後續MT的encoder和decoder的初始參數,對decoder的初始化是隻初始化其中與encoder相同的部分,即不初始化encoder-decoder-attention的部分。感覺這樣也是一種思路啊,一般都認爲decoder是沒法初始化的,這裏卻可以這樣初始化??
其預訓練的代碼如下:
model = build_model(params, data['dico'])
# CLM steps
for lang1, lang2 in shuf_order(params.clm_steps, params):
trainer.clm_step(lang1, lang2, params.lambda_clm)
# MLM steps (also includes TLM if lang2 is not None)
for lang1, lang2 in shuf_order(params.mlm_steps, params):
trainer.mlm_step(lang1, lang2, params.lambda_mlm)
這裏首先就是定義模型,其實就是Transformer的encoder,這裏就不再贅述。緊接着是有兩種訓練方式,一種是CLM,一種是MLM,分別與論文裏面是對應的。
下面來看clm_step
和mlm_step
各自的實現:
def clm_step(self, lang1, lang2, lambda_coeff):
"""
Next word prediction step (causal prediction).
CLM objective.
"""
# generate batch / select words to predict
x, lengths, positions, langs, _ = self.generate_batch(lang1, lang2, 'causal')
x, lengths, positions, langs, _ = self.round_batch(x, lengths, positions, langs)
alen = torch.arange(lengths.max(), dtype=torch.long, device=lengths.device)
pred_mask = alen[:, None] < lengths[None] - 1
y = x[1:].masked_select(pred_mask[:-1])
# forward / loss
tensor = model('fwd', x=x, lengths=lengths, langs=langs, causal=True)
_, loss = model('predict', tensor=tensor, pred_mask=pred_mask, y=y, get_scores=False)
def mlm_step(self, lang1, lang2, lambda_coeff):
"""
Masked word prediction step.
MLM objective is lang2 is None, TLM objective otherwise.
"""
# generate batch / select words to predict
x, lengths, positions, langs, _ = self.generate_batch(lang1, lang2, 'pred')
x, lengths, positions, langs, _ = self.round_batch(x, lengths, positions, langs)
x, y, pred_mask = self.mask_out(x, lengths)
# forward / loss
tensor = model('fwd', x=x, lengths=lengths, positions=positions, langs=langs, causal=False)
_, loss = model('predict', tensor=tensor, pred_mask=pred_mask, y=y, get_scores=False)
仔細看這兩者的實現,其實只在generate batch上不同,CLM只需要生成正常的序列即可,而MLM則需要進行mask_out
的操作,這裏與BERT一致,也不再贅述。
在預訓練完Encoder和Decoder之後,就開始用task-specific的方法進行finetune,比如對於無監督機器翻譯來說,就是用去噪自編碼器+循環翻譯的方式,比如對於en-fr這種翻譯,去噪自編碼器就是noise_en->en和noise_fr->fr,循環翻譯就是en->fr->en和fr->en->fr;對於有監督機器翻譯來說,目前較好的方式就是比如對於en->fr,就是同時學習en->fr和fr->en(是用同一個MT模型學習en->fr和fr->en?),而後用en->fr的數據爲fr->en進行數據增廣(back-translation,不知道理解是否有誤?)以及fr->en的數據爲en->fr進行數據增廣,這樣來進行finetune。
這裏源碼裏面分別給出了這些方法的訓練方式:
# denoising auto-encoder steps
for lang in shuf_order(params.ae_steps):
trainer.mt_step(lang, lang, params.lambda_ae)
# machine translation steps
for lang1, lang2 in shuf_order(params.mt_steps, params):
trainer.mt_step(lang1, lang2, params.lambda_mt)
# back-translation steps
for lang1, lang2, lang3 in shuf_order(params.bt_steps):
trainer.bt_step(lang1, lang2, lang3, params.lambda_bt)
其中的mt_step
是翻譯訓練,可以是A->B的翻譯,也可以是noise_A->A的翻譯;bt_step
是back-translation訓練,主要是A->B->A的這種訓練。其實現方式如下:
def mt_step(self, lang1, lang2, lambda_coeff):
"""
Machine translation step.
Can also be used for denoising auto-encoding.
"""
# generate batch
if lang1 == lang2:
(x1, len1) = self.get_batch('ae', lang1)
(x2, len2) = (x1, len1)
(x1, len1) = self.add_noise(x1, len1)
else:
(x1, len1), (x2, len2) = self.get_batch('mt', lang1, lang2)
langs1 = x1.clone().fill_(lang1_id)
langs2 = x2.clone().fill_(lang2_id)
# target words to predict
alen = torch.arange(len2.max(), dtype=torch.long, device=len2.device)
pred_mask = alen[:, None] < len2[None] - 1 # do not predict anything given the last target word
y = x2[1:].masked_select(pred_mask[:-1])
# encode source sentence
enc1 = self.encoder('fwd', x=x1, lengths=len1, langs=langs1, causal=False)
enc1 = enc1.transpose(0, 1)
# decode target sentence
dec2 = self.decoder('fwd', x=x2, lengths=len2, langs=langs2, causal=True, src_enc=enc1, src_len=len1)
# loss
_, loss = self.decoder('predict', tensor=dec2, pred_mask=pred_mask, y=y, get_scores=False)
def bt_step(self, lang1, lang2, lang3, lambda_coeff):
"""
Back-translation step for machine translation.
"""
# generate source batch
x1, len1 = self.get_batch('bt', lang1)
langs1 = x1.clone().fill_(lang1_id)
# generate a translation
with torch.no_grad():
# evaluation mode
self.encoder.eval()
self.decoder.eval()
# encode source sentence and translate it
enc1 = _encoder('fwd', x=x1, lengths=len1, langs=langs1, causal=False)
enc1 = enc1.transpose(0, 1)
x2, len2 = _decoder.generate(enc1, len1, lang2_id, max_len=int(1.3 * len1.max().item() + 5))
langs2 = x2.clone().fill_(lang2_id)
# free CUDA memory
del enc1
# training mode
self.encoder.train()
self.decoder.train()
# encode generate sentence
enc2 = self.encoder('fwd', x=x2, lengths=len2, langs=langs2, causal=False)
enc2 = enc2.transpose(0, 1)
# words to predict
alen = torch.arange(len1.max(), dtype=torch.long, device=len1.device)
pred_mask = alen[:, None] < len1[None] - 1 # do not predict anything given the last target word
y1 = x1[1:].masked_select(pred_mask[:-1])
# decode original sentence
dec3 = self.decoder('fwd', x=x1, lengths=len1, langs=langs1, causal=True, src_enc=enc2, src_len=len2)
# loss
_, loss = self.decoder('predict', tensor=dec3, pred_mask=pred_mask, y=y1, get_scores=False)
代碼還是比較清晰的,對於mt_step
,就是直接調用encoder和decoder進行正常的MT訓練;而對於bt_step
,則首先在eval模式下離線生成A->B’,而後再進行B’->A的正常MT訓練。
- XNLI分類任務
這部分是多語言的分類任務,這裏主要看不用翻譯系統的方法,即先用MLM+TLM和多語言的單語語料及平行語料進行encoder的預訓練,而後用純英文的語料進行finetune。
預訓練的部分和前面那個MT任務中的預訓練一樣,都是使用mlm_step
這個函數,只不過在構建語料的時候,加上了使用平行語料進行mask的部分。
在finetune部分,是在頂層加入了一層Linear,用於三分類;而後將輸入的兩個句子進行拼接,進入分類層,代碼如下:
self.proj = nn.Sequential(*[
nn.Dropout(params.dropout),
nn.Linear(self.embedder.out_dim, 3)
]).cuda()
(sent1, len1), (sent2, len2), idx = batch
sent1, len1 = truncate(sent1, len1, params.max_len, params.eos_index)
sent2, len2 = truncate(sent2, len2, params.max_len, params.eos_index)
x, lengths, positions, langs = concat_batches(
sent1, len1, lang_id,
sent2, len2, lang_id,
params.pad_index,
params.eos_index,
reset_positions=False
)
y = self.data['en']['train']['y'][idx]
# loss
output = self.proj(self.embedder.get_embeddings(x, lengths, positions, langs))
loss = F.cross_entropy(output, y)
五. 總結
優勢
- 提供了多語預訓練的思路,並且確實效果很好
- 幾個預訓練任務的設計和訓練,都非常巧妙
- 對小語種的訓練很有幫助,並且可以提供無監督的多語embedding
- 提供了源碼和所有預訓練模型
不足
- 語言比較少,而且基本都是針對下游任務的,是否不太通用?
- 論文整體思路比較不夠clean,而且對於特定任務的介紹不夠充分,導致理解起來比較困難(至少對於筆者這樣的小白來說很困難~),有些需要看代碼甚至要查閱資料才能知道如何處理的
傳送門
論文:https://arxiv.org/pdf/1901.07291.pdf
源碼:https://github.com/facebookresearch/XLM
博客:https://www.lyrn.ai/2019/02/11/xlm-cross-lingual-language-model/