1. 程式人生 > 實用技巧 >TransCoder 程式碼詳解(一):最頂層的main函式

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)