0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

HumanEvalってなんだ?〜AIのコーディング力を測る「実技試験」〜

0
Posted at

この記事の対象読者

  • LLM(大規模言語モデル)のコード生成能力に興味がある方
  • Pythonの基本文法を理解している方
  • GitHub Copilotなどのコード補完ツールを使ったことがある方

この記事で扱わないこと

  • LLMの学習方法やTransformerアーキテクチャの詳細
  • 深層学習フレームワークの環境構築
  • コード生成モデルのファインチューニング

この記事で得られること

  • HumanEvalベンチマークの仕組みと評価方法が理解できる
  • pass@kメトリクスの意味と計算方法がわかる
  • Pythonで実際にHumanEval評価を実行できる

1. HumanEvalとの出会い

「このAI、コード書けるって言うけど、本当に動くの?」

GitHub Copilotを初めて使ったとき、私は半信半疑だった。
確かにコードを補完してくれる。でも、それが正しく動く保証はどこにある?

BLEUスコア? 文字列の類似度? そんなの、コードが動くかどうかとは別問題だ。

そこで出会ったのがHumanEvalだった。

HumanEvalは「AIのコーディング実技試験」だ。
164問のプログラミング課題を出題し、生成されたコードが「本当に動くか」をテストする。
文字列の類似度ではなく、ユニットテストをパスするかどうか。それが全てだ。

「動くコードを書けるかどうか」という本質を突いたこのベンチマーク。
これを理解してから、コード生成AIの見方が変わった。


2. HumanEvalが生まれた背景

2.1 「似ているけど動かない」問題

2021年以前、コード生成の評価には自然言語処理の指標が流用されていた。
BLEUスコアやROUGEスコアで「正解コードとどれだけ似ているか」を測定していたのだ。

しかし、これには致命的な欠陥があった。

# 正解コード
def add(a, b):
    return a + b

# 生成コード(BLEUスコアは低いが正しい)
def add(x, y):
    result = x + y
    return result

# 生成コード(BLEUスコアは高いが間違い)
def add(a, b):
    return a - b  # マイナスになっている!

変数名が違っても動けばOK。似ていても動かなければNG。
当たり前のことだが、従来の評価方法では捉えられなかった。

2.2 OpenAIのCodexと同時発表

2021年7月、OpenAIはCodexモデルとともにHumanEvalを発表した。

"On HumanEval, a new evaluation set we release to measure functional correctness for synthesizing programs from docstrings, our model solves 28.8% of the problems"
(HumanEval—docstringからプログラムを合成する際の機能的正確性を測定するために我々がリリースした新しい評価セット—において、我々のモデルは28.8%の問題を解決した)

出典: Chen et al., 2021, "Evaluating Large Language Models Trained on Code"

当時、GPT-3(コード専用学習なし)は0%だった。
「コードを学習させると、ちゃんと動くコードが書ける」ことを証明したのだ。


3. HumanEvalの基本概念

3.1 データセット構成

HumanEvalは以下の構成で成り立っている。

項目 内容
総問題数 164問
言語 Python
問題形式 関数シグネチャ + docstring → 実装を生成
平均テスト数 7.7件/問題

全ての問題は手作りされている。
GitHubからのコピペではなく、人間が一問一問作成した。
これにより、学習データへの「漏洩」を防いでいる。

3.2 問題の具体例

実際の問題を見てみよう。

def has_close_elements(numbers: List[float], threshold: float) -> bool:
    """
    リスト内に、互いの差がthreshold以下の要素のペアがあるかを判定する。
    
    >>> has_close_elements([1.0, 2.0, 3.0], 0.5)
    False
    >>> has_close_elements([1.0, 2.8, 3.0, 4.0, 5.0, 2.0], 0.3)
    True
    """
    # ここをLLMが生成する

LLMはdocstringを読み、関数本体を生成する。
生成されたコードは、隠されたユニットテストで検証される。

テストにパスすれば「正解」、一つでも失敗すれば「不正解」。実にシンプルだ。

3.3 評価指標:pass@k

HumanEvalの核心はpass@kメトリクスにある。

「k回試行して、少なくとも1回正解できる確率」を測定する。

pass@1 = 1回の生成で正解できる確率
pass@10 = 10回生成して、少なくとも1回正解できる確率
pass@100 = 100回生成して、少なくとも1回正解できる確率

なぜこの指標が重要なのか?

人間の開発者を考えてみよう。
コードを1回で完璧に書ける人は少ない。何度か試行錯誤して、動くコードにたどり着く。
pass@kはこの「試行錯誤」を反映した、現実的な指標なのだ。

3.4 pass@kの計算式

統計的に正確なpass@kは以下の式で計算される。

pass@k = 1 - C(n-c, k) / C(n, k)

ここで、

  • n = 生成したサンプル数
  • c = テストをパスしたサンプル数
  • k = 試行回数
  • C(a, b) = 組み合わせ(a個からb個を選ぶ)

直感的に言えば「k個選んだとき、全部失敗する確率」の補数を取っている。


4. 実際にHumanEvalを動かしてみよう

4.1 環境構築

まず必要なライブラリをインストールする。

pip install datasets transformers torch accelerate numpy

4.2 データセットの取得と確認

from datasets import load_dataset

# HumanEvalデータセットの読み込み
dataset = load_dataset("openai_humaneval", split="test")

# データ構造を確認
print(f"問題数: {len(dataset)}")
print(f"キー: {dataset.features.keys()}")

# 問題例を1つ表示
example = dataset[0]
print(f"\n【Task ID】{example['task_id']}")
print(f"\n【Prompt】\n{example['prompt']}")
print(f"\n【Canonical Solution】\n{example['canonical_solution']}")
print(f"\n【Test】\n{example['test']}")

実行結果の例:

問題数: 164
キー: dict_keys(['task_id', 'prompt', 'canonical_solution', 'test', 'entry_point'])

【Task ID】HumanEval/0

【Prompt】
from typing import List

def has_close_elements(numbers: List[float], threshold: float) -> bool:
    """ Check if in given list of numbers, are any two numbers closer to each other than
    given threshold.
    >>> has_close_elements([1.0, 2.0, 3.0], 0.5)
    False
    >>> has_close_elements([1.0, 2.8, 3.0, 4.0, 5.0, 2.0], 0.3)
    True
    """

【Canonical Solution】
    for idx, elem in enumerate(numbers):
        for idx2, elem2 in enumerate(numbers):
            if idx != idx2:
                distance = abs(elem - elem2)
                if distance < threshold:
                    return True
    return False

【Test】
def check(candidate):
    assert candidate([1.0, 2.0, 3.9, 4.0, 5.0, 2.2], 0.3) == True
    ...

4.3 pass@kの計算を実装する

公式論文に記載されている、数値的に安定なpass@k計算を実装しよう。

import numpy as np
from typing import List

def estimate_pass_at_k(n: int, c: int, k: int) -> float:
    """
    pass@k の不偏推定量を計算する(数値的に安定な実装)
    
    Args:
        n: 生成したサンプル総数
        c: テストをパスしたサンプル数
        k: 試行回数
    
    Returns:
        pass@k の推定値
    
    Example:
        >>> estimate_pass_at_k(100, 30, 1)
        0.30
        >>> estimate_pass_at_k(100, 30, 10)
        0.972...
    """
    if n - c < k:
        # 失敗サンプルがk個未満なら、k個選べば必ず1つは正解
        return 1.0
    
    # 1 - C(n-c, k) / C(n, k) を計算
    # 数値的安定性のため、対数を使わず積で計算
    return 1.0 - np.prod(1.0 - k / np.arange(n - c + 1, n + 1))


def calculate_pass_at_k(results: List[List[bool]], k_values: List[int] = [1, 10, 100]) -> dict:
    """
    複数の問題に対するpass@kを計算する
    
    Args:
        results: 各問題の結果リスト。results[i][j] = i番目の問題のj番目のサンプルが正解か
        k_values: 計算するkの値のリスト
    
    Returns:
        各kに対するpass@kの平均値
    """
    pass_at_k = {k: [] for k in k_values}
    
    for problem_results in results:
        n = len(problem_results)
        c = sum(problem_results)  # 正解数
        
        for k in k_values:
            if k <= n:
                pass_at_k[k].append(estimate_pass_at_k(n, c, k))
    
    return {k: np.mean(v) for k, v in pass_at_k.items()}


# 使用例
if __name__ == "__main__":
    # 例: 3問、各問題10サンプル生成した結果
    results = [
        [True, True, False, True, False, False, True, False, False, True],   # 問題1: 5/10正解
        [False, False, False, False, False, False, False, False, False, False],  # 問題2: 0/10正解
        [True, True, True, True, True, True, True, True, True, True],        # 問題3: 10/10正解
    ]
    
    metrics = calculate_pass_at_k(results, k_values=[1, 5, 10])
    
    print("=== pass@k 計算結果 ===")
    for k, score in metrics.items():
        print(f"pass@{k}: {score:.3f}")

実行結果:

=== pass@k 計算結果 ===
pass@1: 0.500
pass@5: 0.749
pass@10: 0.833

4.4 LLMでHumanEval評価を実行する

実際にLLMを使ってHumanEvalを評価するコードを書いてみよう。

import torch
from transformers import AutoTokenizer, AutoModelForCausalLM
from datasets import load_dataset
from tqdm import tqdm
import re

def evaluate_humaneval(
    model_name: str,
    num_samples: int = 5,
    num_problems: int = 10,
    temperature: float = 0.8
) -> dict:
    """
    HumanEvalベンチマークでLLMを評価する
    
    Args:
        model_name: Hugging Faceのモデル名
        num_samples: 各問題に対する生成サンプル数
        num_problems: 評価する問題数
        temperature: 生成時のtemperature
    
    Returns:
        評価結果の辞書
    """
    # モデルとトークナイザーの読み込み
    tokenizer = AutoTokenizer.from_pretrained(model_name)
    model = AutoModelForCausalLM.from_pretrained(
        model_name,
        torch_dtype=torch.float16,
        device_map="auto"
    )
    
    if tokenizer.pad_token is None:
        tokenizer.pad_token = tokenizer.eos_token
    
    # データセットの読み込み
    dataset = load_dataset("openai_humaneval", split="test")
    
    all_results = []
    
    for prob_idx in tqdm(range(min(num_problems, len(dataset))), desc="Problems"):
        problem = dataset[prob_idx]
        prompt = problem['prompt']
        test_code = problem['test']
        entry_point = problem['entry_point']
        
        problem_results = []
        
        for sample_idx in range(num_samples):
            # コード生成
            inputs = tokenizer(prompt, return_tensors="pt").to(model.device)
            
            with torch.no_grad():
                outputs = model.generate(
                    **inputs,
                    max_new_tokens=256,
                    temperature=temperature,
                    do_sample=True,
                    top_p=0.95,
                    pad_token_id=tokenizer.pad_token_id
                )
            
            generated = tokenizer.decode(outputs[0], skip_special_tokens=True)
            completion = generated[len(prompt):]
            
            # 関数定義の終わりを検出(次の関数定義または空行2つ)
            lines = completion.split('\n')
            code_lines = []
            for line in lines:
                if line.strip().startswith('def ') and code_lines:
                    break
                code_lines.append(line)
            completion = '\n'.join(code_lines)
            
            # テスト実行
            full_code = prompt + completion + "\n\n" + test_code + f"\ncheck({entry_point})"
            
            try:
                exec(full_code, {})
                problem_results.append(True)
            except Exception as e:
                problem_results.append(False)
        
        all_results.append(problem_results)
    
    # pass@k計算
    metrics = calculate_pass_at_k(all_results, k_values=[1, min(5, num_samples)])
    
    return {
        'model': model_name,
        'num_problems': num_problems,
        'num_samples': num_samples,
        'pass_at_k': metrics
    }


if __name__ == "__main__":
    # 警告: exec()を使用するため、信頼できないモデルでは実行しないこと
    result = evaluate_humaneval(
        model_name="microsoft/phi-2",
        num_samples=5,
        num_problems=5,
        temperature=0.8
    )
    
    print(f"\n=== 評価結果 ===")
    print(f"モデル: {result['model']}")
    print(f"問題数: {result['num_problems']}")
    print(f"サンプル数/問題: {result['num_samples']}")
    for k, score in result['pass_at_k'].items():
        print(f"pass@{k}: {score:.3f}")

4.5 よくあるエラーと対処法

エラー 原因 対処法
SyntaxError 生成コードの構文エラー 正常な終了処理、インデント修正
TimeoutError 無限ループの生成 タイムアウト設定を追加
NameError インポート文の欠落 プロンプトにimport文を含める
セキュリティ警告 exec()の危険性 サンドボックス環境で実行

重要: exec()は任意のコードを実行するため、本番環境では必ずサンドボックス化すること。


5. 主要モデルのスコア推移

HumanEvalのスコアは、この3年で劇的に向上した。

モデル 発表年 pass@1
GPT-3 2021 0.0%
Codex 2021 28.8%
GPT-3.5 2023 72.0%
GPT-4 2023 67.0%(base)/ 85%+(tuned)
Claude 3.5 2024 92.0%
O1 Preview 2025 96.3%

2021年に0%だったGPT-3から、わずか4年で96%を超えるモデルが登場した。
「AIがコードを書く」という概念が、夢物語から現実になった証拠だ。


6. HumanEvalの限界と発展形

6.1 HumanEvalの問題点

HumanEvalには以下の限界がある。

  1. 問題の多様性不足: 164問は包括的なテストには少なすぎる
  2. Pythonのみ: 他言語の評価ができない
  3. 単一関数のみ: クラス設計やモジュール構成は評価不能
  4. データ汚染リスク: 学習データに問題が含まれている可能性

6.2 後継ベンチマーク

ベンチマーク 特徴
HumanEval+ テストケース強化版(80倍のテスト)
HumanEval-X 多言語版(C++, Java, JavaScript, Go)
MBPP 974問のPython問題集
SWE-bench GitHubの実issue解決タスク

6.3 pass@1 vs pass@k の使い分け

実務でどちらを重視すべきか?

指標 適したシーン
pass@1 1回で正解が必要な場面(自動修正、CI/CD)
pass@10 人間がレビューできる場面(コード補完、提案)
pass@100 理論上の上限を知りたいとき

7. まとめ

この記事では、HumanEvalベンチマークについて以下を解説した。

  1. HumanEvalとは: AIのコーディング「実技試験」。164問のPython課題
  2. 背景: 「動くかどうか」を測る指標がなかった問題を解決
  3. pass@k: k回試行で少なくとも1回正解できる確率
  4. 実装: Hugging Face Datasetsで取得、exec()でテスト実行
  5. 限界: 問題数、言語、複雑さの制限あり

HumanEvalは「コードが動くかどうか」という本質を突いたベンチマークだ。
BLEUスコアのような「似ているかどうか」ではなく、「機能するかどうか」を測る。

次にコード生成AIのニュースを見たとき、「pass@1で○○%」という数字の重みがわかるはずだ。


参考文献

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?