本文提出用于程序语言(PL)和自然语言(NL)的双模态预训练模型 CodeBERT。CodeBERT 学习支持下游任务(如: natural language code search, code documentation generation)的通用表示,然后基于混合的目标函数对其进行训练,该目标函数包含预训练任务和 replaced token detection(detect plausible alternatives sampled from generators) 任务。这使得我们能利用 NL-PL 对的 “bimodal” data 和 “unimodal” data。前者为模型训练提供 input tokens,后者用来帮助学习更好的 generators。评估:
【个人理解】这里的 RTD 任务类似 GAN,数据是 mask 的数据,然后通过 generator 生成可能的候选方案,然后再用 discriminator 判断候选方案是否正确。
大规模预训练模型,如 ELMo、GPT、BERT、XLNet、RoBERTa 等模型,极大提高了各种 NLP 任务的 SOTA。这些预训练模型通过自监督目标在大规模无标签的文本中学习有效的上下文表示,如 masked language modeling 从人工屏蔽的输入序列中预测原始词。预训练模型的成功也推动了多模态预训练模型的发展,如 ViLBERT、VideoBERT 等。
本文提出的 NL-PL 双模态预训练模型 CodeBERT 在包含6种编程语言的数据中进行预训练,其中的双模态数据是指代码和其对应的函数级别的自然语言形式的文档。模型训练在类似多语言 BERT 的设置下进行,输入的时候不会特别说明程序语言的类型。
本文主要贡献:1)首个面向多编程语言的 NL-PL 预训练大模型;2)CodeBERT 在自然语言代码搜索和代码文档生成任务中都取得了当前(2020年) SOTA。3)创建了用于研究代码预训练模型的 probing 能力的数据集。
self-supervised:无需人工标注自动获得用于预训练的监督信息;
主要的学习目标是 language modeling and its variations;
由于预训练的最终目标不是训练一个好的语言模型,因此最好同时考虑前后文,以学习更好的通用上下文表示。这导致 BERT 采用了 masked language modeling objective。
【说实话,论文这里的表述方式我觉得很怪,这里真的有因果关系?怎么去定义“好”的语言模型?为什么不是为了得到“好”的语言模型就要考虑前后文?好的语言模型是指能预测下文的能力才叫好吗?】
多模态预训练模型:学习不同模态输入之间的隐式对齐。本文不但包含 NL-PL 的双模态数据,还包括大量的单模态数据(不带文档的代码);
同期一项工作:采用 masked language modeling 和 next sentence prediction 作为训练 BERT 的目标,并在 Python 源码上训练模型。 这里的 sentence 指:Python 标准定义的 logical code line。在预训练过程上 CodeBERT 与上述工作的差异是:1)CodeBERT 以跨模态方式进行训练,同时利用双模态 NL-PL 数据和单模态 PL/NL 数据;2)CodeBERT 在6种编程语言上进行预训练;3)CodeBERT 基于一种新的 replaced token detection 目标进行训练。
CodeBERT: multi-layer bidirectional Transformer,结构与 RoBERTa-base 完全相同,因此不再赘述,模型参数共 125M。
预训练阶段的输入:[CLS],w1,w2,..,wn,[SEP],c1,c2,...,cm,[EOS][CLS],w_1,w_2,..,w_n,[SEP],c_1,c_2,...,c_m,[EOS][CLS],w1,w2,..,wn,[SEP],c1,c2,...,cm,[EOS];(NL Text + PL Code)
输出:1)对 NL/PL 输出每个 token 的 contextual vector representation;2)[CLS][CLS][CLS] token 作为 aggregated sequence representation;
分别用 bimodal/unimodal data 训练 CodeBERT;
数据:datapoints from Github Repos
用于预训练的数据集的构造方法 爬取公开非 fork 的 github repo,根据下述规则过滤:1)至少被一个别的项目使用;2)each documentation is truncated to the first paragraph(看下面的图);3)注释不能少于 3 tokens;4)函数不能小于三行;5)函数名不能带有 test;
使用了两个预训练任务来预训练 CodeBERT:
Objective #1: Masked Language Modeling (MLM)
输入: datapoint of NL-PL pair x={w,c}x=\{w,c\}x={w,c};www:NL words sequence;ccc:PL tokens sequence;
首先为 NL、PL 随机选择约 ~15% 的 token 替换为 [MASK][MASK][MASK]。
MLM 的目标是预测 masked position 处的 original token,下面的 pD1p^{D_1}pD1 是判别器 discriminator,从大量的词汇表中预测出一个 token:
Objective #2: Replaced Token Detection (RTD)
RTD 目标最初是用于高效学习自然语言预训练模型。本文同时采用双模态和单模态数据用 RTD objective 进行训练。这里有两个 generators:NL generator pGwp^{G_w}pGw,PL generator pGcp^{G_c}pGc 都用于对 masked positions 生成 plausible alternatives。
判别器 discriminator 被训练来判断一个词是否是原始词,这是一个二分类问题。对输入的每个位置,都应用 RTD objective,这与 GAN 的区别是:如果 generator 恰好生成了正确的 token,该 token 的 label 是 “real” 而不是 “fake”。【因为 GAN 里是通过 generator 和 discriminator 左右互搏,造 fake 和 鉴别 fake 来实现模型的提升,所以 generator 的目标是生成 fake 数据】
δ(i)\delta(i)δ(i):指示函数;pD2p^{D_2}pD2:预测 i-th word 是原始词的概率的判别器。
有很多方式实现 generator,本文实现了2个具有 bidirectional contexts 的 n-gram 语言模型,分别用于 NL、PL,也分别从相应的数据点学习。这个方法也很容易推广到学习双模态生成器或以一种联合方式使用像基于 Transformer 这样的神经架构的复杂生成器,这部分作为未来可以拓展的工作。
当前的 PL 训练数据是单模态代码数据,NL 训练数据来自双模态数据中的代码文档。最终的 Loss 函数:
将 CodeBERT 应用到下游的 NL-PL 任务:
Natural Language Code Search 任务:通过输入的自然语言查询代码集合中语义最相似的代码;
Model Comparisons:
上面的模型都通过把代码视为 tokens sequence 来进行预训练。本文也以 MLM 任务只在 code 上训练 RoBERTa。CodeBERT(MLM) 从头开始学习的性能超越了 RoBERTa。用 RoBERTa 初始化 CodeBERT 可以提高性能。
Task Formulation and Data Construction:在相关研究 NLP probing 实验之后,本文研究 NL-PL probing。由于目前没有面向该问题的工作,因此本文创造了数据集,并形式化该问题。
对给定的 NL-PL 对 (c,w)(c,w)(c,w),NL-PL probing 的目标是测试模型在干扰下正确预测/恢复 masked token of interest 的能力(可能是cic_ici,也可能是wjw_jwj)。干扰有两种:
本文采用第二种干扰,将 NL-PL probing 任务设计为有多个选项的 QA 问题。问题是完型填空,一个特定的 token 被 [MASK][MASK][MASK] 替换,然后专家设计了一些候选答案。具体而言,本节分别在 NL 侧和 PL 侧进行评估。而为了减少数据构建的工作量,自动从 CodeSearchNet 数据集中的验证集和测试集中的 NL-PL pairs 获取数据(该数据在预训练阶段不可见)。
为了在 NL 方面评估,选择了 NL-PL pairs 中 NL docs 包含6个关键词 (max,maximize,min,minimize,less,greater) 之一的 pair,然后合并1/2关键词和3/4关键词的 pairs,将数据分为4组候选。任务是让预训练模型从4个选项中选择1个正确的选项。即,该设置的输入包括完整的代码和带有 mask 的文档。
对于 PL 方面,选择包含关键字 max 和 min 的代码,并将任务设计为二选一问题。由于代码补全是一个重要的场景,我们想测试模型仅仅基于前文的 PL context 预测正确 token 的能力。因此,此处为 PL 添加了一个额外配置,其输入是完整的 NL 文档和前面的代码。
Model Comparisons:这里采用 CodeBERT(MLM) 因为其输出层自然契合 probing。结果表明在 NL 和 PL probing 上 CodeBERT 都比基线方法更好,只有在 proceding context 设置下更差,这表明 code completion 是一个挑战性的任务,这项研究留给未来的工作。
本节进一步的对 NL-PL probing 进行 case study。分别 mask NL token、PL token,然后报告 RoBERTa 和 CodeBERT 的预测概率。我们可以看到 RoBERTa 在下图的两种情况下都失败了,而 CodeBERT 在 NL 和 PL 设置中都做出了正确的预测。
CodeBERT 的预训练目标不包括 generation-base objective,但本文想调查 CodeBERT 在生成任务上的扩展能力。本节研究 code-to-NL generation 并报告在 CodeSearchNet Corpus(6种编程语言) 上的文档生成任务的结果。因为生成的文档很短并且 higher order n-grams may not overlap,本节通过使用 smoothed BLEU score 解决了该问题。
Model Comparisons:baselines model 如带有 attention 机制的 RNN 模型、Transformer、RoBERTa 和仅在代码上预训练的模型。为了证明 CodeBERT 在 code-to-NL generation 任务上的有效性,采用了不同的预训练模型作为编码器并保持超参数一致(详细的超参数见附录B.3)。
本节评估在预训练中未见过的编程语言上模型的泛化性,选择的任务:natural language summary of a C# code snippet。在 CodeNN 论文中的数据集上进行实验,包含了 66015 个来自 StackOverflow 的 QA 对。这个数据集由于数量级小,因此具有挑战性。采用 smoothed BLEU-4 score 评估模型。
Model Comparisons:但这里效果仅次于 Code2Seq,可能原因是因为 Code2Seq 使用在其 AST 上使用了 compositional paths。但 CodeBERT 只将原始代码作为输入。本文通过按照一定顺序遍历 AST 的树结构来训练 CodeBERT,但这样做并没有对生成任务带来改进。因此结合 AST 来改进 CodeBERT 也是未来研究的一个潜在方向。
潜在研究方向
本节分析 CodeBERT 是否适合作为 unified encoder。CodeBERT 首先对 NL 和 PL 分别进行编码,然后通过内积计算相似度,这样代码搜索就相当于在共享向量空间中寻找最近的代码。这样做还有助于预先计算代码表示的线上系统中使用 CodeBERT。在运行时,系统只需要计算 NL 的表示和基于向量的点积。根据以下目标对 CodeBERT 进行微调:最大化 ground truth 的内积,同时让干扰项的内积最小化。
我们只在数据量少的语言上使用了这个设定。可以看到 CodeBERT 比 RoBERTa 和仅用代码预训练的模型表现得更好。late fusion 与 standard 方法效果相当,但更有效,因为这种设定可用于线上系统。