AI 文摘

HumanEval是如何进行代码评估的:从数据构成、评估逻辑到pass@k指标计算


  • By AiBard123
  • December 18, 2023 - 2 min read



作者: 老刘说NLP 来源: 老刘说NLP

HumanEval: Hand-Written Evaluation Set,是工作《Evaluating Large Language Models Trained on Code》(https://arxiv.org/abs/2107.03374)中提到的一个代码评测基准。

最近在做代码方面的评估,走了许多弯路,在评估逻辑上有些误解,重新整理了下,供大家一起参考。尤其是针对pass@k的理解、如何做的单元测试等。

一、HumanEval的数据构成

HumanEval评测数据集,一共包括164条样本,还是很少量的,可以用json进行更为直观的理解,地址https://github.com/abacaj/code-eval/blob/main/human-eval/data/HumanEval.jsonl.gz:

{  
    "task_id":"HumanEval/0",  
    "prompt":"from typing import List\n\n\ndef has_close_elements(numbers: List[float], threshold: float) -> bool:\n    \"\"\" Check if in given list of numbers, are any two numbers closer to each other than\n    given threshold.\n    >>> has_close_elements([1.0, 2.0, 3.0], 0.5)\n    False\n    >>> has_close_elements([1.0, 2.8, 3.0, 4.0, 5.0, 2.0], 0.3)\n    True\n    \"\"\"\n",  
    "entry_point":"has_close_elements",  
    "canonical_solution":"    for idx, elem in enumerate(numbers):\n        for idx2, elem2 in enumerate(numbers):\n            if idx != idx2:\n                distance = abs(elem - elem2)\n                if distance < threshold:\n                    return True\n\n    return False\n",  
    "test":"\n\nMETADATA = {\n    'author': 'jt',\n    'dataset': 'test'\n}\n\n\ndef check(candidate):\n    assert candidate([1.0, 2.0, 3.9, 4.0, 5.0, 2.2], 0.3) == True\n    assert candidate([1.0, 2.0, 3.9, 4.0, 5.0, 2.2], 0.05) == False\n    assert candidate([1.0, 2.0, 5.9, 4.0, 5.0], 0.95) == True\n    assert candidate([1.0, 2.0, 5.9, 4.0, 5.0], 0.8) == False\n    assert candidate([1.0, 2.0, 3.0, 4.0, 5.0, 2.0], 0.1) == True\n    assert candidate([1.1, 2.2, 3.1, 4.1, 5.1], 1.0) == True\n    assert candidate([1.1, 2.2, 3.1, 4.1, 5.1], 0.5) == False\n\n"  
}  

如下所示:

task_id表示任务的ID,prompt表示题目(通常直接请求大模型获取答案),entry_point是唯一标记,canonica_solution是参考答案,test是测试单元。

二、HumanEval的评估逻辑

每一个测试问题重复实验n次,然后通过单元测试,计算平均通过率。我们可以在源码地址:https://github.com/abacaj/code-eval/tree/main/human-eval中看到起执行逻辑

1、读取每个样本,请求模型获得结果

如下所示,generate_one_completion为请求大模型生成结果的函数,输入每道题的prompt,然后得到结果。

而由于题目太少,测试结果会有偏,大模型的结果具备多样性(如有top_p, top_k)等,所以,num_samples_per_task用来控制每道题生成多少个结果(代码中设置为200次),从而计算通过率。completion作为补全结果的存储字段。

因此,整体会有32800条样本。

from human_eval.data import write_jsonl, read_problems  
  
problems = read_problems()  
  
num_samples_per_task = 200  
samples = [  
    dict(task_id=task_id, completion=generate_one_completion(problems[task_id]["prompt"]))  
    for task_id in problems  
    for _ in range(num_samples_per_task)  
]  
write_jsonl("samples.jsonl", samples)  

当然,这一块,需要做一个代码的后处理,因为模型会生成其他多余的代码片段,例如https://github.com/abacaj/code-eval/blob/main/core/evaluation.py中所述:

# reference: https://github.com/declare-lab/instruct-eval/blob/main/human_eval/main.py#L35  
def filter_code(completion: str) -> str:  
    # The program tends to overwrite, we only take the first function  
    completion = completion.lstrip("\n")  
    return completion.split("\n\n")[0]  

将后处理得到的结果作为最终代码补全结果。

2、获得模型的结果,进行单元测试

这块的逻辑的在于,针对得到的补全结果,通过构造一个完整的测试用例,送入单元测试中进行测试。

其中,如下代码所示:

def check_correctness(problem: Dict, completion: str, timeout: float, completion_id: Optional[int] = None) ->   
  
    def unsafe_execute():  
        with create_tempdir():  
            # These system calls are needed when cleaning up tempdir.  
            import os  
            import shutil  
            rmtree = shutil.rmtree  
            rmdir = os.rmdir  
            chdir = os.chdir  
  
            # Disable functionalities that can make destructive changes to the test.  
            reliability_guard()  
  
            # Construct the check program and run it.  
            print(completion)  
            check_program = (  
                problem["prompt"] + completion + "\n" +  
                problem["test"] + "\n" +  
                f"check({problem['entry_point']})"  
            )  
            try:  
                exec_globals = {}  
                with swallow_io():  
                    with time_limit(timeout):  
                        exec(check_program, exec_globals)  
                result.append("passed")  
            except TimeoutException:  
                result.append("timed out")  
            except BaseException as e:  
                result.append(f"failed: {e}")  
            # Needed for cleaning up.  
            shutil.rmtree = rmtree  
            os.rmdir = rmdir  
            os.chdir = chdir  
    manager = multiprocessing.Manager()  
    result = manager.list()  
    p = multiprocessing.Process(target=unsafe_execute)  
    p.start()  
    p.join(timeout=timeout + 1)  
    if p.is_alive():  
        p.kill()  
    if not result:  
        result.append("timed out")  
    return dict(  
        task_id=problem["task_id"],  
        passed=result[0] == "passed",  
        result=result[0],  
        completion_id=completion_id,  
    )  

里面对于测试样例的构造,是将题目的prompt、模型预测的内容completion、题目的test的按照换行符进行拼接。

# Construct the check program and run it.  
  print(completion)  
  check_program = (  
      problem["prompt"] + completion + "\n" +  
      problem["test"] + "\n" +  
      f"check({problem['entry_point']})"  
  )  

然后进行单元测试,直接使用python内置的exec函数进行校验,逻辑在于,给定超时timeout时间,如果测试通过,则标记为passed,如果不是,则不通过【比如说出现代码语法错误】。

 try:  
      exec_globals = {}  
      with swallow_io():  
          with time_limit(timeout):  
              exec(check_program, exec_globals)  
      result.append("passed")  
  except TimeoutException:  
      result.append("timed out")  
  except BaseException as e:  
      result.append(f"failed: {e}")  

经过这个测试之后,就可以得到每条样本的预测情况。

三、再看代码模型评估中的pass@k指标计算

代码生成模型的主要基准是将样本与参考解进行匹配,匹配可以是精确的,也可以是模糊的(如BLEU分数)。

例如:

EM(Exact Match)是指生成的代码与真实代码具有相同的token序列的百分比;

BLUE机器翻译结果越接近专业人工翻译的结果,则越好,本质在判断两个句子的相似程度,相似度越高得分越高。

CodeBLEU是BLEU变体。在BLEU在n-gram匹配上的基础上,进一步通过抽象语法树(AST)融入代码语法,通过数据流融入代码语义;

但是,基于匹配的代码衡量标准存在缺陷。例如,BLEU在捕捉代码特有的语义特征方面存在问题。

因此,Kulal等人(2019年)使用pass@k指标评估功能正确性,每个问题生成k个代码样本,如果任何样本通过单元测试,则认为问题已解决,并报告总分数。

是一次实验随机性太大,需要多次实验求平均值。pass@k需要对每一个测试问题重复实验t次,并且每次都生成k个代码,最后计算平均通过率。假如重复实验100次来估计pass@100,就需要生成 100*100=10000个代码,这样的计算量是难以接受的。而t越小,估计的pass@k就越不准(方差越大)。

因此,为了评估pass@k,该工作会为每个任务生成n≥k个样本(本文中使用n=200,k≤100),计算通过单元测试的正确样本c≤n的数量,并计算无偏估计值。

其中,c是生成的n个代码中通过测试的数量。n越大估计越准确,但计算代价肯定远远小于t*k。

假设模型只能生成这n个代码,而且他们每一种被生成出来的概率是相等的,其中有c个可以通过测试。那么模型任意生成k个代码,全都不能通过测试的概率是:生成k个不能通过测试的代码的情况总和与生成k个代码的全部情况总和之比,即:

根据大数定理,当样本总量趋近无穷大的时候,样本的平均值无限接近数学期望。因此只要求出其的均值,即得到了对pass@k的无偏估计。

具体代码实现:

def estimate_pass_at_k(  
    num_samples: Union[int, List[int], np.ndarray],  
    num_correct: Union[List[int], np.ndarray],  
    k: int,  
) -> np.ndarray:  
    """  
    Estimates pass@k of each problem and returns them in an array.  
    """  
  
    def estimator(n: int, c: int, k: int) -> float:  
        """  
        Calculates 1 - comb(n - c, k) / comb(n, k).  
        """  
        if n - c < k:  
            return 1.0  
        return 1.0 - np.prod(1.0 - k / np.arange(n - c + 1, n + 1))  
  
    if isinstance(num_samples, int):  
        num_samples_it = itertools.repeat(num_samples, len(num_correct))  
    else:  
        assert len(num_samples) == len(num_correct)  
        num_samples_it = iter(num_samples)  
  
    return np.array(  
        [estimator(int(n), int(c), k) for n, c in zip(num_samples_it, num_correct)]  
    )  

关于这块,https://zhuanlan.zhihu.com/p/653063532做了公式的推演,感兴趣的可以进一步看看。

最终,即可完成对应的指标,例如官方的脚本运行结果:

$ evaluate_functional_correctness data/example_samples.jsonl --problem_file=data/example_problem.jsonl  
Reading samples...  
6it [00:00, 3397.11it/s]  
Running example suites...  
100%|...| 6/6 [00:03<00:00,  1.96it/s]  
Writing results to data/example_samples.jsonl_results.jsonl...  
100%|...| 6/6 [00:00<00:00, 6148.50it/s]  
{'pass@1': 0.4999999999999999}  

总结

本文主要针对humaneval这一评测任务,从数据、评估逻辑以及pass@k的评估指标计算方式进行了介绍,之前一直对pass@k有误解,认为是预测K次的通过率,读完代码实现本身才有更为准确的理解。

代码评测,也是整个评测体系中十分重要的部分,感兴趣的可关注。

参考文献

1、https://github.com/abacaj/code-eval/blob/main/human-eval/

2、https://arxiv.org/abs/2107.03374

2、https://zhuanlan.zhihu.com/p/653063532

关于我们

老刘,刘焕勇,NLP开源爱好者与践行者,主页:https://liuhuanyong.github.io。

老刘说NLPhttps://zhuanlan.zhihu.com/p/653063532,将定期发布语言资源、工程实践、技术总结等内容,欢迎关注。

对于想加入更优质的知识图谱、事件图谱、大模型AIGC实践、相关分享的,可关注公众号,在后台菜单栏中点击会员社区->会员入群加入。

更多AI工具,参考Github-AiBard123国内AiBard123

可关注我们的公众号:每天AI新工具