CoderEval: A Benchmark of Pragmatic Code Generation with Generative Pre-trained Models
- 标题:CoderEval:基于生成式预训练模型的实用代码生成基准
- 摘要:基于预训练和微调范式的代码生成模型在学术界和工业界中得到了广泛应用,催生了许多知名的工业模型,如Codex、CodeGen和PanGu-Coder。为了评估这些模型的有效性,目前已有多个基准(如HumanEval和AiXBench)被提出,这些基准仅包括生成独立函数的案例,即仅调用或访问内置函数和标准库的函数。然而,非独立函数(即依赖于外部上下文的函数)在热门开源项目中占比超过70%,现有基准中的独立函数评估难以反映这些模型在实用代码生成场景(如开源或专有代码的真实环境生成)中的有效性。为弥补这一差距,本文提出了一个名为CoderEval的基准,包含230个Python和230个Java代码生成问题,这些问题从流行的真实开源项目中精心筛选,并提供了一个自包含的执行平台,用于自动评估生成代码的功能正确性。CoderEval支持六个层级的上下文依赖性,其中上下文指的是在目标函数之外但位于第三方库、当前类、文件或项目中定义的代码元素(如类型、API、变量和常量)。CoderEval可以用于评估模型在生成超出独立函数范围的代码时的有效性。通过在CoderEval和HumanEval上对三种先进的代码生成模型(CodeGen、PanGu-Coder和ChatGPT)进行评估,我们发现这些模型在生成独立函数时的有效性显著高于生成非独立函数时的表现。我们的分析总结了当前的进展,并指明了未来通过利用上下文信息进一步提升模型在实用代码生成中的有效性的方向。
- 关键词:Code Generation(代码生成)、LLMs(大模型)、Benchmark(基准测试)
- 开源地址:GitHub - CoderEval/CoderEval: A collection of practical code generation tasks and tests in open source projects. Complementary to HumanEval by OpenAI.
1. Introduction – 问题陈述
传统基准(如HumanEval)主要评估独立函数的生成能力,忽略了非独立函数在实际开发中的高占比(通过分析 GitHub 上分别用 Java 和 Python 编写的 100 个最受欢迎的项目发现非独立功能占开源项目功能的 70% 以上),导致无法全面评估模型在复杂依赖环境下的表现。
文章的主要贡献包括以下几点:
- 指出现有基准的局限性:通过分析100个最受欢迎的Java和Python开源项目,发现现有的基准(如HumanEval)主要包含独立函数,而非独立函数占据了开源项目中70%以上的比例。现有基准无法全面评估代码生成模型在实际应用场景中的表现。
- 提出CoderEval基准:CoderEval是一个面向实际代码生成的基准,从各种开源项目中收集问题,涵盖非原始类型、第三方库和项目特定的上下文引用。此外,CoderEval引入了人工标注的文档字符串,补充了原始文档字符串,为目标函数提供了更丰富的描述。
- 评估并比较三种先进的代码生成模型:文章在CoderEval上对CodeGen、PanGu-Coder和ChatGPT三种模型进行了实验,得到三大重要结论:(1) 这些模型在生成非独立函数时表现不如独立函数;(2) 即使是最强大的ChatGPT,生成具有上下文依赖的代码仍然具有挑战性;(3) 使用人工标注的文档字符串相较于原始文档字符串对生成代码的效果有显著影响。
2. Background – 代码生成及其基准测试的背景
2.1 用于代码生成的大型语言模型
- PanGu-Coder:一种用于文本到代码生成的预训练语言模型,基于PanGu-α架构,并采用双阶段训练策略。
- CodeGen:一系列用于文本到代码的大型语言模型,训练数据包括自然语言语料、多语言代码语料(多种编程语言)和Python代码数据集。
- Codex:首个使用大型生成预训练模型从自然语言生成完整函数的模型。
- AlphaCode:专注于编程竞赛,在表现上与普通人类开发者相当。
- InCoder:一个统一生成模型,既能进行程序生成(从左到右生成),也能进行代码编辑(通过填充完成)
2.2 代码生成的基准测试
-
CoderEval(本文提出):目前唯一支持项目级别代码生成的基准,使用Pass@K作为评估指标,验证生成代码的功能正确性。
【构建包含项目级别功能的代码生成基准的重要性和挑战性】
项目构建和沙箱化难度:基准测试需要保证所选择的项目能够正确编译和在受控的沙箱环境中执行,但许多开源项目的依赖和配置非常复杂,导致它们很难成功构建和运行。
测试用例的优化和依赖分析复杂:为了提高测试效率,需要减少项目中的测试用例数量,只保留覆盖目标功能的测试用例。然而,判断哪些测试用例能覆盖目标功能并不简单,需要构建功能依赖图和进行覆盖分析。并且,如果项目本身的测试用例没有覆盖目标功能,基准测试的构建者还需要深入理解项目的复杂逻辑,才能编写出准确、高质量的测试用例。
-
大多数现有基准(如HumanEval、MultiPL-E、DS-1000、AiXBench)仅包含独立函数。尽管DS-1000包含了非独立函数,但其非独立函数仅限于调用七个常用的第三方库。
HumanEval是一个用于评估代码生成模型的基准,关注生成代码的功能正确性。它包含164个手写的编程问题(每个问题的功能通过docstring体现),以及对应的解决方案(使用Python实现),每个方案包含函数签名、主体和多个单元测试。
AiXBench被提出用于评估Java代码生成模型。AiXBench包含175个自动评估的问题和161个手动评估的问题。AiXBench的作者引入了一种新的指标来自动评估生成代码的正确性,并设立了一系列标准来手动评估生成代码的整体质量。
MultiPL-E是第一个多语言并行的文本到代码生成基准。MultiPL-E扩展了HumanEval和MBPP,支持18种编程语言。MultiPL-E包含一个编译器套件和一个评估框架,用于将Python代码生成基准(包括单元测试、docstring、Python特有术语和类型注释)翻译到其他编程语言。MultiPL-E有两个并行基准部分(即HumanEval和MBPP),用于18种语言的代码生成,涵盖了各种编程范式、语言特性和流行度。
DS-1000包含非独立函数。具体来说,DS-1000包含1000个问题,涉及七个广泛使用的Python数据科学库:NumPy、Pandas、TensorFlow、PyTorch、Scipy、Scikit-learn和Matplotlib。DS-1000的作者通过手动修改函数来减轻数据泄露问题,并强调使用真实开发场景的数据来构建DS-1000。尽管DS-1000包含非独立函数,但它存在两个主要限制:第一,DS-1000的函数仅来自七个Python数据科学的第三方库;第二,尽管DS-1000中的一些函数依赖于第三方库,但DS-1000所反映的开发场景与实际代码生成相去甚远,因为DS-1000中没有任何函数依赖于目标函数外的用户定义函数。
-
Concode数据集包含约10万个Java函数,尽管支持非独立函数生成,但与CoderEval相比存在五大差异,如执行验证的方式、数据泄漏问题的处理、多语言支持等。
Concode是一个新的大型数据集,包含来自开源项目的超过10万个Java类。Concode的作者从GitHub的公共Java项目中收集了每个至少有一个上下文依赖的函数及其对应的自然语言描述(即Javadoc风格的方法注释)和代码。Concode的数据集来自约33,000个库,按照库的粒度(而非函数粒度)分为训练、验证和测试集。尽管Concode中的函数包含上下文依赖,但它只使用BLEU作为评估指标,且未对生成函数的正确性进行评估,因为Concode中的每个问题都不包含测试用例。
3. CoderEval Benchmark
3.1 Dataset Collection – 数据集收集
-
候选项目选择(Candidate Projects):首先从GitHub上筛选出候选项目,过滤条件包括项目标签和星级数
【筛选标签】
“gson”、“music”、“logging”、“chat”、“websocket”、“mvc”、“leetcode”、“microservices”、“jdbc”、“json”、“crud”、 “数据结构”、“log4j”和“序列化”
对于每个标签,文章选择星级最高的前五个项目
-
候选函数选择(Candidate Functions):在选定的项目中,进一步筛选出候选函数。过滤标准包括排除测试函数、弃用函数、接口等,要求带有英文函数级注释,且可以在验证平台中成功运行并通过原始测试用例
-
函数手动筛选(Manual Selection - Code Quality):对候选函数进行人工筛选,文章根据每个项目中包含的所选功能的数量来获取项目,这个过程可以使得在相同数量的所选函数总数下编译更少的项目
【人工筛选规则】
(1)包含少于10个上下文标记的函数
(2)由招募的工程师任意判断出在实际开发场景中经常使用的功能(不同的开发人员有不同的偏好)
(3)包含可以反映函数实现的文档字符串的函数
(4)代码实现超过三行的函数
(5)排除测试或已弃用的函数
-
测试生成(Test Generation):在每个选定项目中生成或获取测试代码,确保每个函数都有对应的测试用例,以便在后续阶段对生成的代码进行准确评估。
【如何构建高覆盖率的测试用例】
(1)首先,对CoderEval中包含的每个项目运行原项目中已有的单元测试用例,测量CoderEval中每个函数的分支覆盖率。分支覆盖率反映了代码执行的不同路径被测试的程度。
(2)如果某函数的分支覆盖率未达到100%,作者会手动编写额外的测试用例,尽可能达到100%的覆盖率。
【基于原项目中的单元测试用例,通过两步来构建测试用例】
Step1 构建静态函数调用图:调用图展示了函数之间的调用关系,通过该图可以找到能够到达目标函数的所有测试用例。故在验证目标函数的正确性时,可以避免执行整个项目的测试用例,从而降低测试的时间成本和计算资源消耗。
Step2 转换为统一的调用接口:为了统一和简化测试用例的调用接口 ,将目标函数对应的测试用例(通常基于JUnit、TestNG等框架)自动转换为普通的非测试函数。这些函数不再依赖测试框架,重命名后以“main”为后缀,以独立文件形式存在。
3.2 Dataset Inspection-- 数据集检查
-
人工标注(Human Labeling):人工标注每个函数的文档字符串,生成人类标注的描述(docstring),以避免数据泄漏并提供更高质量的自然语言提示
-
上下文依赖识别(Contextual Dependency Identification):通过分析每个函数的上下文依赖,识别该函数依赖的外部代码元素(如类型、API、变量和常量)
【通过三步对其所属项目进行程序分析来识别函数的上下文依赖关系】
Step1 构建知识库:创建一个包含不同Python和Java版本的内置类、库函数、常量和常用第三方库的版本信息的知识库,确保能够识别目标函数对这些通用资源的依赖。
Step2 列表获取:借助知识库,给定要分析的函数,解析包含该函数的源文件以获取类型/函数/变量/常量定义的列表
Step3 静态分析:通过静态程序分析来识别目标函数所需的类型、变量和API调用,并进行分类,以便区分不同的上下文依赖元素
【区分两种类型的上下文】oracle_context 和 all_context
oracle_context:最小依赖集,仅包含目标函数实际使用的上下文元素(即由前面三个步骤识别的上下文依赖关系)。oracle_context信息不仅可以用来评估LLM生成的代码中上下文依赖的准确性,还可以作为输入的一部分帮助LLM更准确地生成代码。
all_context:完整依赖集,包含所有潜在可用的上下文信息(即函数所属文件中定义/导入的所有类型、变量和 API)。本次实验并未使用all_context信息
-
可运行级别分类(Runnable-level Classification)
Dependency Type Dependencies Examples(Python) Examples(Java) self-contained built-in types/functions, no need to import
【内置类型/函数,无需导入】min()
,print()
System.xxx
slib-runnable standard libraries/modules, no need to install
【标准库/模块,无需安装】os
,subprocess
,sys
Arrays.sort()
plib-runnable publicly available libraries on pypi/maven
【pypi/maven 公开可用的库】unittest
,requests
com.google.code.gson
class-runnable code outside the function but within class
【函数外但在类内定义的代码】self.xxx
,X.f()
this.f()
file-runnable code outside the class but within the file
【类外但在文件内定义的代码】func()
,URL
,name
func()
project-runnable code in the other source files
【来自其他源文件的代码】superclass
,utils
superclass
,utils
【备注】每个可运行级别必须依赖于该级别定义的依赖关系,不得依赖于其后续级别定义的依赖关系,并且可能依赖于也可能不依赖于其先前级别定义的依赖关系。
3.3 Evaluation Process – 评估过程
- Docker环境配置(Docker Environments):通过基于Linux的Docker镜像构建评估环境,使平台能够在独立的沙箱环境中运行
- Python的评估(Evaluation for Python):Python项目的评估步骤包括版本控制和依赖管理,以确保生成函数可以在原项目的环境中执行。通过
pyenv
和venv
管理Python版本和虚拟环境,pip
用于安装依赖,并在运行时替换目标函数。测试通过直接调用生成函数的测试用例输入来完成,并将结果与期望输出进行对比。 - Java的评估(Evaluation for Java):与Python不同,Java需要在运行前进行编译。因此,在评估过程中,首先将生成的函数替换到目标项目中,再进行增量编译和测试。使用
javac
编译更改后的文件,并通过java
命令执行生成的字节码。通过“-cp”参数来管理依赖关系。 如果编译失败,平台会直接判定生成代码未通过测试用例,而无需进一步执行测试。
4. Experiments
4.1 实验设置
-
**模型选择:**CodeGen、PanGu-Coder和ChatGPT
CodeGen: CodeGen-Mono、CodeGen-Multi模型(350M)
PanGu-Coder:300M
ChatGPT: gpt-3.5-turbo
在推理阶段,对于所有模型,我们将最大窗口长度设置为1024,使用核采样(nucleus sampling),样本数量为10(即每个函数生成10段代码),温度设置为0.8
-
**基准选择:**CoderEval和HumanEval
-
评价指标: Pass@K 和 Acc@K
Pass@K:该指标表示在生成的前K个样本中,至少有一个样本能够通过所有测试用例的比例,评估模型的整体生成成功率。使用无偏估计器Pass@K降低高采样方差,保证结果的准确性。
【无偏估计器是一种统计方法,能够在有限采样的情况下,得到对真实比例更准确的估计,不会系统性地偏向某一方向】
Acc@K:该指标用于评估代码生成模型在生成代码所需的上下文依赖信息时的准确性的指标,旨在衡量模型在生成代码时对外部依赖(如变量、类型和API调用等)信息的正确引用程度。
【如何计算Acc@K】
(1)对于每个目标函数,检查它的每个oracle_context标记,看看是否在模型生成的前K个样本中的至少一个中正确地出现。如果目标函数的所有oracle_context标记在前K个样本中至少出现一次,则该目标函数计为“成功”。
(2)统计所有目标函数中,符合上述条件的函数的比例。例如,假设有230个目标函数,如果其中200个目标函数的所有oracle_context标记在K个生成样本中至少出现一次,那么Acc@K的值就是200/230。
4.2 研究问题
-
RQ1:CodeGen、PanGu-Coder 和 ChatGPT 的表现如何,尤其是在生成独立函数与非独立函数方面?
【总体表现】
(1)ChatGPT的表现始终优于其他两种模型 <ChatGPT的参数规模远大于其他模型 >
(2)CodeGen和PanGu-Coder在HumanEval上的表现逊色于CoderEval <HumanEval中的函数虽然是独立函数但往往更偏向算法性质,而CoderEval中的独立函数更简单实用(如整数转换为字符串或浮点数)>
(3)ChatGPT在HumanEval上的效果优于CoderEval <ChatGPT在算法学习上更具优势>
【三种模型的互补性与重叠性】
CoderEval中的重叠问题总数为88个,其中52个是独立函数,独立函数仅占CoderEval中的约30%。这说明三种模型在生成独立函数方面表现出色(尤其是ChatGPT表现特别好);而扩展到非独立函数的生成能力,并探索如何结合不同模型的代码生成能力,是值得研究的方向。
【独立函数与非独立函数生成效果的比较】
独立函数包括两个可运行级别:self-contained和slib-runnable
非独立函数包括四个可运行级别:plib-runnable、class-runnable、file-runnable和project-runnable
【总结】在Python和Java的CoderEval上,三种模型在生成独立函数时的效果明显高于生成非独立函数。不同模型在代码生成中各具优势,如何结合不同模型的代码生成能力是值得探索的研究方向。
-
RQ2:这些模型在生成代码时正确整合 oracle_context 信息的表现如何?
将oracle_context标记分为三类:TypeReference、APIInvocation和VarReference
【总结】如表3和图4所示,LLMs生成oracle_context标记的能力(通过Acc@K反映)与生成正确代码的能力(通过Pass@K反映)一致。正如图4所示,LLMs生成不同类别(即TypeReference、APIInvocation和VariableReference)oracle_context标记的能力在不同语言中有所差异。
-
RQ3:不同的提示如何影响这些模型的有效性?
【原始docstring与人工标注docstring的相似度】
原始docstring与人工标注docstring在Python项目中表达的差异较小,而Java项目中的docstring差异较大
【不同docstring对生成效果的影响】
在Python上,使用原始docstring和人工标注docstring生成的代码表现较为相似;而在Java上,CodeGen和ChatGPT在使用原始docstring生成时效果更好,而PanGu-Coder在使用人工标注docstring时效果更佳。
<这种差异可以归因于模型的训练方式。Python模型通常经过专门训练或微调,而Java则只有PanGu-Coder进行了专门的Java语料预训练。因此,对于Python模型,docstring的表达差异不会显著影响效果,而对于Java模型,特别是没有进行专门Java训练的模型,这种差异则更为明显>
【总结】不同类型的docstring提示(原始docstring与人工标注docstring)对代码生成模型的效果具有显著影响。特别是,对于单一语言的任务,模型在同语义不同表达的情况下表现更佳,表明专门的单一语言训练或微调可以提升模型的生成效果。这一发现提示在未来的模型训练中,可以考虑为特定语言进行定制化的微调,以获得更好的生成效果。