TransCoder 程式碼詳解(一):最頂層的main函式
前言
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的訓練過程裡沒有被用過。
在這一系列控制過程中,起到關鍵作用的函式是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)