XLM解讀(論文 + PyTorch源碼)

這篇論文是Facebook在BERT的基礎上發展出來的Cross-Lingual版本,即多語的。BERT的github上實際上也有一個多語版本的,但卻沒有提到是怎麼訓練的,也沒有任何的信息。這裏的XLM提出了一些策略用於多語言學習,並與multi-lingual的BERT進行了對比,效果確實會好。

一. 前言

一開始BERT出來的時候,只有英語的,這對於各個國家的廣大AI愛好者,是十分不便的,大家都希望能有自己國家語言的版本。這不,後面BERT又出了多語言版本,FB也緊跟着出了一個更好的多語言版本(不過貌似語言比較少?主要還是針對翻譯和XNLI任務而定製的,不像BERT的那個那麼多語言,而且很通用)

這裏複述一下作者在第一章總結的他們的貢獻:

  1. 引入了一個新的無監督方法,用於訓練多語的表徵,並且提出兩個單語的預訓練LM目標
  2. 提出了一種新的有監督方法,使用平行語料,來增強多語預訓練的表現
  3. 在跨語言分類、有/無監督機器翻譯任務上,達到了新的SoTA
  4. 對於resource比較少的語言,用這種預訓練方式很有幫助
  5. 重點來了!有源碼和預訓練模型

下面來看看XLM具體是怎麼操作的~

PS:不知道有沒有讀者和筆者一樣,一看到這種多語的啊,pair的啊,parallel的啊這類的詞眼,就覺得整個邏輯非常的暈,這篇論文,尤其是實驗部分,真的是結合源碼看了好久,大概率是筆者太菜了吧。。。

二. XLM原理

1. 多語詞表構建

既然是多語的模型嘛,總不可能是一個語言一個model,然後封裝在一起,假裝是多語模型吧,肯定是隻有一個模型的,那麼就要求這個模型能接收各個語言的句子作爲輸入,因此這裏就需要構建包含各個語言詞的多語詞表。

與BERT一樣,這裏也是使用BPE,但不是簡單地把各個語言的bpe詞表進行拼接,那樣也太大了,,這裏是先對多語語料按照如下的概率進行採樣,然後將多語語料進行拼接,最後進行正常的BPE統計。採樣的目的是對大語種的語料和小語種的語料進行一下平滑,省的全採到大語種上面了,小語種連詞表都沒有了(或者小語種都被按照char拆分了)。。這裏取α=0.5\alpha=0.5

qi=piαj=1Npjα with pi=nik=1Nnkq_i = \frac{p_i^ \alpha}{\sum_{j=1}^N p_j^ \alpha}\ with\ p_i = \frac{n_i}{\sum_{k=1}^N n_k}

2. 預訓練任務

這裏作者提出了三種預訓練任務:CLM、MLM和TLM,下面分別介紹:

  1. CLM:Causal Language Model,無監督單語單向LM訓練任務,就是用Transformer進行LM的單向訓練。
  2. MLM:Masked Language Model,無監督單語雙向LM訓練任務,與BERT一樣。
  3. 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、無監督機器翻譯和有監督機器翻譯任務的效果,下面分別說:

  1. XNLI

這個任務其實筆者也是去扒拉了FB之前的論文,才知道是個什麼任務。。其實還是文本蘊含任務,但只不過訓練集裏面都是英文的,驗證集裏面是多語的(15種語言,獲取方法就是把英文的驗證集翻譯過來,同一個pair裏面是同一種語言的)。

先用MLM在各個語言的單語語料上進行訓練(也有加上額外的平行語料進行TLM訓練的部分),然後再用英文訓練集進行finetune,最後在多個語種上評估。結果如下:
在這裏插入圖片描述

  1. 無監督MT

無監督MT的任務,筆者也是特意去查閱了相關的資料,知道大概是怎麼回事。用大白話說就是,給出兩個語言各自的語料(不一定要平行),機器應該就能學會翻譯,就像人類一樣,在學會了中文和英語之後,應該能進行翻譯,因爲中間的連接是語義,而不是詞表的對應。

無監督MT就是基於這麼一個設定,一般在不考慮pretrain的時候,用的比較多的方法是用去噪自編碼器+循環翻譯,具體來說,對於英譯德這個任務,搭建起encoder-decoder這個模型之後,可以用英文語料加上噪聲,輸入encoder,然後decoder出來原始的英文語料,同理也可以用德文語料加上噪聲,輸入encoder,然後decoder出來原始的德文語料,這個就叫去噪自編碼器,目的其實是在於讓encoder學到語義信息;循環翻譯是個啥?比如en->de->en,就是先讓英文經過encoder和decoder,得到翻譯的德文僞數據,然後將這個德文僞數據,再輸入encoer和decoder,得到原來的英文數據,這樣進行訓練。

那麼在這裏,其實就是用CLM或MLM去初始化encoder和decoder,decoder就初始化那些encoder有的部分,然後後面用上面的套路進行正常流程的訓練即可,這裏對比不同的初始化方法的結果:

  1. 有監督MT

這裏對比了幾種方法用不同的預訓練方式,結果如下:

  • Sennich:這個是之前的SoTA,好像還是用了back-translation+ensemble的方法,也是一個強baseline
  • ro->en:這個是用單向的數據進行finetune
  • ro<->en:這個使用雙向的數據進行finetune
  • ro<->en + BT:用雙向的數據進行finetune,同時進行back-translation(這個好像又是那種先從A->B生成B的僞數據,然後再翻譯回到A)
  1. 小語種LM

這裏主要是驗證多語訓練對小語種語言模型建模的影響,結果如下:

  1. 無監督多語embedding

這裏主要是驗證無監督情況下生成的多語embedding的優秀程度,通過驗證各種源單詞和其翻譯對應的詞之間的距離,結果如下:

四. PyTorch實現

這裏主要是分析XLM源碼中關於模型和訓練的部分,因筆者對於論文中的這些任務(如翻譯等)不是特別熟悉,所以全憑README的內容和代碼一步一步摸索,如果有理解錯誤的地方,還請指正~

下面我將按照源碼中README給出的思路順序進行剖析:

  1. 有/無監督機器翻譯

在機器翻譯這個場景下,論文首先用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_stepmlm_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訓練。

  1. 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)

五. 總結

優勢

  1. 提供了多語預訓練的思路,並且確實效果很好
  2. 幾個預訓練任務的設計和訓練,都非常巧妙
  3. 對小語種的訓練很有幫助,並且可以提供無監督的多語embedding
  4. 提供了源碼和所有預訓練模型

不足

  1. 語言比較少,而且基本都是針對下游任務的,是否不太通用?
  2. 論文整體思路比較不夠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/

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章