Smoothly BLEU

これは[あくあたん工房 GW Advent Calendar](https://gw-advent.9wick.com/calendars/36)4日目の記事です．

こんにちは，taxioです．最近MLベースの自然言語処理の勉強をしていて，Transformerとその関連の論文を読んでtf2.0でゴリゴリ実装したりしていました[^trans]．今回はそんな機械翻訳などの評価値としてよく使われるBLEU[^bleu-abb]とその平滑化手法について書いていきます．

## BLEUのおさらい
BLEUとは[Kishore Papineni, Salim Roukos and Todd Ward, et al, BLEU: a Method for Automatic Evaluation of Machine Translation, ACL'02](https://www.aclweb.org/anthology/P02-1040.pdf)にて紹介された手法です．次の式に沿って，n-gramによって正解文章と機械翻訳が生成した文章を比較し数値化していきます．

```math
BLEU = BP * exp(\sum_{n=1}^{N} w_n log P_n)
```

$P_n$はn-gram精度のことで，以下の式で表されます．

```math
P_n = \frac{Count_{clip}(ngram_{Ref})}{Count(ngram_{MT})}
```

$Count(ngram_{MT})$は機械翻訳のn-gramの数のことで，$Count_{clip}(ngram_{Ref})$は機械翻訳と参照訳[^refseq]に共通するn-gramの数のことです．
例えば，次のような機械翻訳と参照訳があったとします．
> 機械翻訳 : the the the the the the the.
参照訳1: The cat is on the mat.
参照訳2: There is a cat on the mat.

unigram(1-gram)を考えていきます．
まず，機械翻訳の文章から生成されるunigramの数は7なので，$Count(ngram_{MT})=7$となります．次に，参照訳のunigramとの一致を考えていくのですが普通に一致をとってしまった場合，参照訳のどちらにも「the」という単語が入っているため，一致度は$\frac{7}{7}$となってしまいます．これでは頻出単語を並べただけの文章のスコアが不自然に高くなってしまうので，BLEUでは参照訳で一度一致した候補語は使わないという制約を与えます．すると，一致度$P_1$は$\frac{2}{7}$となります．

$BP$(Brevity Penalty)は機械翻訳が参照訳より短い場合にペナルティを課す係数です．
$c$を機械翻訳の長さ，$r$を機械翻訳の長さに一番近い参照訳の長さとすると，

```math
BP = \left\{
\begin{array}{ll}
1 & (c > r) \\
e^{1 - \frac{r}{c}} & (c \leq r)
\end{array}
\right.
```

となります．
機械翻訳の方が長い場合は何もせずとも自然にスコアは下がっていくため，BPは1となっています．

$w_n$は各n-gramに関する$P_n$に対する重みで，

```math
\sum_{n=1}^{N} w_n = 1
```

となるように設定します．
論文中では$N=4$，$w_n=\frac{1}{N}$がベースラインであると紹介されており，実際$N=4$で使われているのをよく見かけます[^bleu4]．つまり，全ての$w_n$が等しい場合，算術平均であるため，

```math
BLEU = BP * exp(\sum_{n=1}^{N} \frac{1}{N} log P_n)
```

と，他の記事でもよく見る形になります．
これをさらに変形すると，

```math
BLEU = BP * (\prod_{n=1}^{N} P_n)^{\frac{1}{N}}
```

となり，非常に見やすい形になります．

## BLEUの問題点
はい，ここから本題です．
このBLEUの評価値は，各$P_n$を掛け合わせるため，少々困った問題が起きます．それは高いオーダーでのn-gramの不一致によるスコアの急減少です[^dec]．例えば，$n=1,2,3$まである程度一致が見つかっていたのに，$n=4$の時に一致が0だとBLEUの値も0になってしまうという感じです．そのため，これを平滑化する手法が色々な論文で紹介されています．

```python
import nltk

mt = ['This', 'is', 'an', 'fruit']
ref1 = ['This', 'is', 'an', 'apple']
ref2 = ['There', 'is', 'an', 'apple']
print(nltk.translate.bleu_score.sentence_bleu([ref1, ref2], mt))
# 8.636168555094496e-78
```

`nltk`ライブラリ[^nltk]ではn-gramの一致が0の場合，warningを出して代わりに`sys.float_info.min`を使っているので0にはなってません[^method0]．


## Smooth BLEU
今回は[Boxing Chen and Colin Cherry, A Systematic Comparison of Smoothing Techniques for Sentence-Level BLEU, ACL'14](http://acl2014.org/acl2014/W14-33/pdf/W14-3346.pdf)という論文で紹介されている平滑化手法について書いていきます．

ここからは便宜上，機械翻訳と参照訳の共通のn-gramの数を$m_n$，機械翻訳のn-gramの数を$l_n$として解説していきます．

### 手法1
$m_n$が0個なら小さい値$\epsilon$を足し合わせる手法です．`nltk`では[method1](https://github.com/nltk/nltk/blob/7d6a8d42f6/nltk/translate/bleu_score.py#L532-L541)というメソッドで実装されています．論文では$\epsilon$は経験的に決まると書いてあり，ライブラリのデフォルト値は0.1です．

### 手法2
[Chin-Yew Lin and Franz Josef Och, Automatic Evaluation of Machine Translation Quality Using Longest Common Subsequence and Skip-Bigram Statistics, ACL'04](https://www.aclweb.org/anthology/P04-1077)で紹介された手法です．
$n>1$において，各$m_n$，$l_n$に1を足して計算します．

`nltk`では[method2](https://github.com/nltk/nltk/blob/7d6a8d42f6/nltk/translate/bleu_score.py#L543-L553)というメソッドで実装されていますが，こちらはn>1ではなくn=1から足し合わせの対象にしているようです．

### 手法3
NIST (National Institute of Standards and Technology)が提供している[mteval-v13a](https://www.nist.gov/itl/iad/mig/tools)に実装されている手法です．

```
invcnt = 1
for n in 1 to N
    if m_n = 0
        invcnt = invcnt * 2
        m_n = 1 / invcnt
    endif
endfor
```

$m_n$が0だと$invcnt$を足していきます．定数はn-gramの一致が0の場合が多いほど$\frac{1}{2}$ずつ小さくなっていきます．

`nltk`では[method3](https://github.com/nltk/nltk/blob/7d6a8d42f6/nltk/translate/bleu_score.py#L555-L575)というメソッドで実装されています．

### 手法4
この論文で新規に紹介された手法です．先程の手法3の$invcnt$の更新を2倍ではなく，

```math
invcnt = invcnt * \frac{K}{ln(len(T))}
```

とします．$K$は適当な平滑化係数で，$len(T)$は機械翻訳の文字列の長さ(単語数)です．こうすることで，短い文章の方が有利になってしまう点に対応しています．

`nltk`では[method4](https://github.com/nltk/nltk/blob/7d6a8d42f6/nltk/translate/bleu_score.py#L577-L591)というメソッド実装されています．$K$は経験的に決まり，ライブラリのデフォルト値は5となっています．

### 手法5
この論文で新規に紹介された手法です．

```math
m'_0 = m_1 + 1\\
m'_n = \frac{m'_{n-1} + m_n + m_{n+1}}{3}
```

$m_n$の前後の値を使って平滑化しています．$m_N$を計算するために，n-gramは$N+1$まで算出します．
`nltk`では[method5](https://github.com/nltk/nltk/blob/7d6a8d42f6/nltk/translate/bleu_score.py#L593-L607)というメソッドで実装されています．

### 手法6
[Jianfeng Gao and Xiaodong He, Training MRF-Based Phrase Translation Models using Gradient Ascent, NAACL'13](https://www.aclweb.org/anthology/N13-1048)で紹介された手法です．

```math
p^0_n = p_{n-1} * \frac{p_{n-1}}{p_{n-2}} \\
p_n = \frac{m_n + \alpha p^0_n}{l_n + \alpha} \\
BP = exp(1 - \beta \frac{r}{c})
```

$n > 2$において，それより前の値を使用して$P_n$を計算します．また，$BP$を計算する際，参照訳の全長と最良の翻訳の全長との比で計算される$\beta$を掛け合わせています[^beta]．

`nltk`では[method6](https://github.com/nltk/nltk/blob/7d6a8d42f6/nltk/translate/bleu_score.py#L609-L633)というメソッドで実装さていますが，どうやら$BP$の計算に関してはサポートしていないようです．$\alpha$は経験的に決まり，ライブラリのデフォルト値は5となっています．

### 手法7
この論文で新規に紹介された手法です．
先程の手法4で$P_n$を算出したあとに，さらに手法5でもう一度平滑化します．`nltk`では[method7](https://github.com/nltk/nltk/blob/7d6a8d42f6/nltk/translate/bleu_score.py#L635-L644)というメソッドで実装されています．

### で，どれがいいの？
この論文では手法7が一番人間との評価に近いとされていました．ちなみにTransformerの[公開実装](https://github.com/tensorflow/tensor2tensor/blob/f9f63c3f3c5aab8ed8070f10662ad8012c09c17b/tensor2tensor/utils/bleu_hook.py#L60-L129)では手法3が使われているような気がします．TensorflowのNMT Tutorialである[tensorflow/nmt](https://github.com/tensorflow/nmt/blob/master/nmt/scripts/bleu.py#L48-L112)では手法2が使われています．
平滑化した方がいいというのは分かりますが，どれがいいのかはよく分からないですね．というかSoTA競ってるのに評価手法を統一しなくていいのでしょうか...？

## 最後に
BLEUの平滑化手法について書いていきました．
元々はBLEUでの評価の実装をしている際，`nltk`の実装が自分の知っているものと少し違うという違和感と，なんか内部のコメントに論文のリンクがあるという理由で調べ始めました．この記事を書くために関連論文を調べている最中もBLEUの平滑化について記述のある論文が紹介できないほど見つかりました．自然言語処理って勉強すればするほど新知識が出てきますね．

まだまだ勉強の身ですので，間違っていたり情報が古かったりした場合は指摘していただけるとありがたいです．

[^trans]: GNMTベースのBeamSearchの実装のせいで白髪が増えた気がします
[^bleu-abb]: bilingual evaluation understudy
[^bleu4]: BLEU4などと書かれていたりします
[^beta]: すいませんこの論文はしっかり読めてないので，$\beta$に関しては理解してません
[^refseq]: 熟練した翻訳者による参考翻訳
[^method0]: https://github.com/nltk/nltk/blob/7d6a8d42f6/nltk/translate/bleu_score.py#L516-L529
[^dec]: 急減少というか0ですね．良い日本語が思いつきませんでした．
[^nltk]: https://github.com/nltk/nltk