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的训练过程里没有被用过。有这些没用的东西在里面是因为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)

posted @ 2020-08-03 11:36  FromATP  阅读(1376)  评论(0编辑  收藏  举报