K_DM Book
用語集
対象者 標準
対象者別の入口 →

対象者の設定はこのブラウザの中にだけ保存されます。

言語 JA

BLEU 指標

中級

03.6.1

BLEU 指標

最終更新 2020-01-29 読了時間 5 分
まとめ
BLEU は機械翻訳の候補文を、人手で作った参照文との n-gram の一致度で採点する指標です。1-gram から 4-gram までそれぞれ「候補文の n-gram のうち参照文にも出てくる割合」(修正精度)を数えて幾何平均し、候補文が参照文より短いときは brevity penalty を掛けて、0〜1 の値にします。候補 “the cat is on the mat” を 2 本の参照文と比べると、各 n-gram の精度と長さ比から BLEU = 0.638 が出ます。読み終えると、同じ単語を繰り返して精度を水増しできない理由(修正精度)、短い訳が罰される理由(brevity penalty)、4-gram が 1 つも一致しないと 0 になる弱点と smoothing の役割を説明できるようになります。

直感:候補文の断片がどれだけ参照文に見つかるか #

翻訳モデルが “the cat is on the mat” と出力し、人間の訳が “there is a cat on the mat” だったとします。BLEU は候補文を 1 語ずつ(1-gram)、2 語ずつ(2-gram)…と切り出し、その断片が参照文にも現れる割合を数えます。1 語単位なら “the”, “cat”, “is”, “on”, “mat” が見つかって高い値、3 語単位になると “on the mat” しか一致せず値は下がります。長い断片まで一致するほど語順も合っているとみなせるので、1〜4-gram を幾何平均して 1 つのスコアにします。ただし “the the the” のような候補でも “the” が参照文にある以上 1-gram 精度が 1.0 になってしまうため、参照文での出現回数を上限にする修正を入れ、“on the” だけの 2 語の候補が高得点を取らないように短さへの罰(brevity penalty)も掛けます。

式で書くと何を数えているか #

修正精度(Modified Precision) #

通常の精度は候補文の n-gram が参照文に出現する割合ですが、同じ n-gram を何度も繰り返す候補に対して不当に高くなります。修正精度は、各 n-gram の一致数を「参照文に出現する最大回数」で上限を設けて数えます:

$$p_n = \frac{\sum_{C \in \text{Candidates}} \sum_{\text{n-gram} \in C} \min\!\left(\text{Count}(\text{n-gram}),\; \text{MaxRef}(\text{n-gram})\right)}{\sum_{C \in \text{Candidates}} \sum_{\text{n-gram} \in C} \text{Count}(\text{n-gram})}$$

分子の \(\min\) が上限で、候補に “the” が 3 回あっても参照文に 2 回しかなければ 2 回分しか数えません。分母は候補文の n-gram の総数です。

BLEU スコアの計算 #

BLEU は $N$-gram(通常 $N=4$)までの修正精度の幾何平均に、brevity penalty を掛けた値です:

$$\text{BLEU} = \text{BP} \cdot \exp\!\left(\sum_{n=1}^{N} w_n \ln p_n\right)$$

ここで $w_n = 1/N$(均等重み)です。対数の平均を \(\exp\) で戻しているので算術平均ではなく幾何平均になり、どれか 1 つの \(p_n\) が 0 なら全体も 0 になります。

Brevity Penalty(BP) #

候補文が参照文より短い場合にペナルティを課します:

$$\text{BP} = \begin{cases} 1 & \text{if } c > r \\\\ e^{1 - r/c} & \text{if } c \leq r \end{cases}$$

$c$ は候補文のトークン数、$r$ は候補文に最も長さが近い参照文のトークン数です。候補が参照の半分の長さなら \(e^{1 - 2} \approx 0.37\) が掛かり、長い候補にはペナルティがありません(長すぎる候補は精度側で下がります)。

計算例 #

候補文: “the cat is on the mat”(6 トークン) 参照文: “there is a cat on the mat”(7 トークン)

n-gram レベル一致数候補 n-gram 総数$p_n$
15 (“the”, “cat”, “is”, “on”, “the mat” → 重複修正で 5)65/6
23 (“the cat”, “on the”, “the mat”)53/5
31 (“on the mat”)41/4
41 (“on the mat” の延長なし)30/3

$p_4 = 0$ のため、この例では参照文 1 本の BLEU-4 = 0 になります。実務では smoothing を適用して 0 を回避し、下のコードでは 2 本目の参照文 “the cat sits on the mat” が 4-gram “the cat is on” に近い断片を補うため 0 を免れています。


Python で実装する #

次のコードは標準ライブラリのみで BLEU を実装し、候補文 “the cat is on the mat” と 2 本の参照文に適用して BLEU = 0.638 を出力します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
from __future__ import annotations

import math
from collections import Counter
from collections.abc import Iterable, Sequence

def ngram_counts(tokens: Sequence[str], n: int) -> Counter[tuple[str, ...]]:
    """トークン列から n-gram の頻度を数える。"""
    return Counter(tuple(tokens[i : i + n]) for i in range(len(tokens) - n + 1))

def modified_precision(
    candidate: Sequence[str],
    references: Iterable[Sequence[str]],
    n: int,
) -> tuple[int, int]:
    """候補文の n-gram が参照文に出現した件数と候補文の n-gram 総数を返す。"""
    cand_counts = ngram_counts(candidate, n)
    max_ref: Counter[tuple[str, ...]] = Counter()
    for ref in references:
        max_ref |= ngram_counts(ref, n)
    overlap = {ng: min(count, max_ref[ng]) for ng, count in cand_counts.items()}
    return sum(overlap.values()), max(1, sum(cand_counts.values()))

def brevity_penalty(candidate_len: int, reference_lens: Iterable[int]) -> float:
    """候補文が短すぎる場合に課す brevity penalty を計算する。"""
    if candidate_len == 0:
        return 0.0
    closest_ref_len = min(reference_lens, key=lambda r: (abs(r - candidate_len), r))
    ratio = candidate_len / closest_ref_len
    if ratio > 1:
        return 1.0
    return math.exp(1 - 1 / ratio)

def bleu(candidate: str, references: Sequence[str], max_n: int = 4) -> float:
    """候補文と参照文の集合から BLEU スコアを計算する。"""
    candidate_tokens = candidate.split()
    reference_tokens = [ref.split() for ref in references]
    precisions: list[float] = []
    for n_value in range(1, max_n + 1):
        overlap, total = modified_precision(candidate_tokens, reference_tokens, n_value)
        precisions.append(overlap / total)
    if min(precisions) == 0:
        return 0.0
    geometric_mean = math.exp(sum(math.log(p) for p in precisions) / max_n)
    penalty = brevity_penalty(len(candidate_tokens), (len(ref) for ref in reference_tokens))
    return penalty * geometric_mean

if __name__ == "__main__":
    candidate_sentence = "the cat is on the mat"
    reference_sentences = [
        "there is a cat on the mat",
        "the cat sits on the mat",
    ]
    score = bleu(candidate_sentence, reference_sentences)
    print(f"BLEU = {score:.3f}")

出力例:

BLEU = 0.638

実行結果の読み方 #

0.638 は「1〜4-gram の修正精度の幾何平均が 0.638 で、長さの罰はなし」という意味です。BLEU は 0〜1(論文や sacrebleu では 100 倍して 0〜100)で、参照文と完全に一致した候補だけが 1.0 になるので、1.0 に近い値が多いときは参照文の丸写し(テストデータの混入)を疑います。


長所と注意点 #

実装が Counter と数行の算術で済み高速なので、WMT などの翻訳ベンチマークで 2002 年から比較指標として使われ続けています。参照文を 2 本、4 本と増やすほど正しい言い換えが一致する機会が増え、言い換えへの耐性が上がります。一方、同義語や語順の違いには弱く、“the cat sat on the mat” と “on the mat sat the cat” は意味が同じでも 3-gram 以上の一致が減って低く出ます。文単位ではなくコーパス全体で \(p_n\) を集計するのが本来の定義で、1 文ごとに計算すると 4-gram が 0 になりやすいため smoothing が必要です。長文要約では人手評価との相関が低くなる場合があるため、ROUGE や BERTScore と併用します。日本語など語境界が曖昧な言語では、MeCab などで分かち書きしてから split() 相当のトークン化をしないと、文全体が 1 トークンになって一致が取れません。


まとめ #

BLEU は候補文の 1〜4-gram が参照文に現れる割合(修正精度)を幾何平均し、候補が短いときは brevity penalty \(e^{1 - r/c}\) を掛けた 0〜1 の指標で、“the cat is on the mat” と 2 本の参照文では 0.638 になります。修正精度の min(count, max_ref[ng]) が単語の繰り返しによる水増しを防ぎ、幾何平均が 4-gram の一致ゼロをそのまま 0 点にします。同義語と語順の入れ替えには弱いので、参照文を複数用意し、短文では smoothing を使い、日本語は分かち書きしてから計算します。要約のように「参照の情報をどれだけ拾えたか」を測るなら ROUGE、言い換えまで評価するなら埋め込みの コサイン類似度 を使った BERTScore 系の指標を併用します。