前言
TransCoder是Facebook推出的一個開源的transcompiler模型,其作用是給定一個以某種編程語言寫成的函數,將它轉換為另一種編程語言的形式,並保留其原本的功能。目前TransCoder支持的語言有C++、Java和Python。
TransCoder在github上的repo戳這里
ATP的上一篇blog解讀了TransCoder的原論文,包括模型結構、實驗過程等,戳這里
然而關於模型的許多細節原論文解釋得並不甚清楚。模型詳細的架構是什么?文中的三步訓練過程具體如何操作?
於是ATP讀了TransCoder的代碼,整理了一些東西放在這里。ATP基本上是按照從頂至底的順序讀代碼。即按照調用的順序,從train的過程開始。
因為東西實在太多了,ATP又有很啰嗦的習慣,所以就把它們分成很多篇blog來講。
這(一系列的)blog或許需要配合完整的源碼食用。。。否則你可能會不知道ATP到底在說些什么東西。
訓練過程train.py
TransCoder主目錄下有三個文件夾:data、preprocessing,和XLM。
前面的兩個文件夾都是與數據有關的東西,最后一個文件夾才是模型。
通過readme可以知道,在train和evaluate的時候運行的都是XLM文件夾下的train.py。我們從這個腳本開始看。
train.py包括兩個主要的函數,get_parser和main。其中get_parser是負責解析調用時傳進來的參數的,重點是main函數。
main函數開頭先做了些初始化。然后是這樣一個部分:
# build model
if params.encoder_only:
model = build_model(params, data['dico'])
else:
encoder, decoder = build_model(params, data['dico'])
# build trainer, reload potential checkpoints / build evaluator
if params.encoder_only:
trainer = SingleTrainer(model, data, params)
evaluator = SingleEvaluator(trainer, data, params)
else:
trainer = EncDecTrainer(encoder, decoder, data, params)
evaluator = EncDecEvaluator(trainer, data, params)
可以發現這段代碼分了兩種情況討論,一種情況是只有encoder,另一種情況是encoder和decoder都有。這兩種情況在訓練過程上有所區別。
在ATP之前的blog里它講過,這個模型本身是一個enc-dec結構的transformer。一開始ATP很疑惑為什么要有一個只有encoder的選項,直到它又學了一遍 Masked LM 的訓練過程。
TransCoder的MLM訓練應該是與BERT差不多的,都是只使用encoder的embedding功能,去讓encoder學到詞匯的contextual-embedding(與上下文相關的embedding)。大致方法是將帶有MASK的句子送入encoder,輸出每個單詞的embedding。然后把MASK位置的embedding過一個線性分類器,輸出它對應詞表中每個單詞的概率。
觀察TransCoder的readme中給出的訓練參數,可以發現,使用MLM進行pretrain的時候encoder_only為true,而使用DAE和back-translation進行訓練的時候encoder_only為false。
這與原文中的描述相符。原文中也提到,MLM是為了讓模型學習到representation,而此時decoder沒有被訓練,參數仍然是隨機初始化的狀態。真正訓練decoder的是后面的兩個過程。
main函數中另外一個重要部分是訓練的主循環,負責跑一個一個的epoch。
trainer.n_sentences = 0
while trainer.n_sentences < trainer.epoch_size:
# 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)
# 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, params.bt_sample_temperature)
trainer.iter()
表面上看來,每個epoch需要執行5個步驟。但是觀察一下訓練的命令行里提供的參數就可以發現,這5個步驟並不是每次都要全部執行的,取決於params.xx_steps。
例如,當我們希望用MLM來pretrain模型的時候,就只有params.mlm_steps是有值的,其含義為需要進行訓練的所有語言的列表,在這里是'cpp,java,python';其它的都是空串,所以對應的步驟不會被執行。
而值得注意的是,DAE和back-translation是一起訓練而不是分開訓練的。在執行這部分訓練的時候,bt_steps和ae_steps都有值。
clm_steps和mt_steps一直都是空串,在TransCoder的訓練過程里沒有被用過。有這些沒用的東西在里面是因為TransCoder直接用的XLM的代碼,而XLM的訓練過程需要clm和tlm。
在這一系列控制過程中,起到關鍵作用的函數是shuf_order。這個函數返回一個列表。列表有多長,當前這個epoch就需要跑幾輪循環。一個epoch中可能要循環若干次,針對不同的語言進行訓練。而shuf_order生成的這個列表,就是指明每次循環具體訓練的是什么語言。
shuf_order函數有三個參數,第一個參數langs是語言的列表,即前文所述的xx_steps里面的內容。第二個參數是params,里面有可能會用到的一些參數。
第三個是n,默認值為5。這個n值的意義是生成的列表的最大長度。也就是說,一個epoch最多跑5輪內循環。
def shuf_order(langs, params=None, n=5):
"""
Randomize training order.
"""
if len(langs) == 0:
return []
if params is None:
return [langs[i] for i in np.random.permutation(len(langs))]
# sample monolingual and parallel languages separately
mono = [l1 for l1, l2 in langs if l2 is None]
para = [(l1, l2) for l1, l2 in langs if l2 is not None]
# uniform / weighted sampling
if params.lg_sampling_factor == -1:
p_mono = None
p_para = None
else:
......
s_mono = [mono[i] for i in np.random.choice(len(mono), size=min(
n, len(mono)), p=p_mono, replace=True)] if len(mono) > 0 else []
s_para = [para[i] for i in np.random.choice(len(para), size=min(
n, len(para)), p=p_para, replace=True)] if len(para) > 0 else []
assert len(s_mono) + len(s_para) > 0
return [(lang, None) for lang in s_mono] + s_para
這個shuf_order函數設計得非常巧妙,它把單語言語料的處理和平行語料的處理合並在了一起。這樣在train或evaluate的時候只需要調用同一個函數就可以了。
shuf_order的第一條命令就是如果langs是空串,返回空列表。這也印證了ATP前面說的,只有params.xx_steps不是空串,對應的循環才會被執行。
我們只分析單語言語料(mono)的處理方法。平行語料(para)的處理方法基本上是同理的。
函數首先提取出了可用的語言列表mono。例如在MLM的訓練過程里,mono的值就是['cpp','java','python']。
然后在這個列表里進行隨機采樣得到s_mono。這里可以發現它訪問了一個名為“lg_sampling_factor”的參數,這個參數是指定特定的取樣概率的。如果這個參數是-1,就說明需要平均(uniform)地采樣,否則就按照lg_sampling_factor指定的概率采樣。
查看訓練命令可以發現,無論是MLM的過程還是DAE/BT的過程,lg_sampling_factor這個參數都是-1,也就是原模型在訓練的時候都是隨機采樣的。所以后面的具體細節就先忽略不看了。
最后一個需要注意的點是,它使用np.random_choice這個函數進行采樣。這個函數相當於一個有放回的取樣過程,也就是最后形成的s_mono列表里可能有重復的元素。
例如,雖然可用的語言列表langs里面有三種不同的語言,但最后生成的s_mono列表可能是['cpp', 'cpp', 'java']這個樣子。
最后以元組的形式返回列表。在單語言語料的情況下元組的第二個值是None。但如果用到平行語料,比如在test的時候,這個元組的兩個值就分別代表source語言和target語言。
To be continued
通過閱讀最頂層代碼的結構,我們大概知道了這個模型的訓練過程:跑若干epoch,每個epoch內部循環3-5次,針對不同的語言進行訓練。
而MLM和DAE/BT的訓練過程是分開的,這與原論文中的描述相符。
接下來我們希望知道模型的具體結構。核心在於build_model這個過程。該函數位於XLM/src/model/init.py中。
由於篇幅原因,ATP會在下一篇blog中具體講解。
(另外,看一下命令行參數可以發現,MLM過程的epoch數目就已經是100000???tkpl.jpg,這要自己訓練得訓練到猴年馬月x)