1. 程式人生 > >機器翻譯評測——BLEU算法詳解

機器翻譯評測——BLEU算法詳解

join rec view 做了 委員會 9.png 分享 微軟亞洲研究院 占比

版權聲明:本文出自胖喵~的博客,轉載必須註明出處。

轉載請註明出處:http://www.cnblogs.com/by-dream/p/7679284.html

前言

  近年來,在自然語言研究領域中,評測問題越來越受到廣泛的重視,可以說,評測是整個自然語言領域最核心和關鍵的部分。而機器翻譯評價對於機器翻譯的研究和發展具有重要意義:機器翻譯系統的開發者可以通過評測得知系統存在的問題而不斷改進,用戶也可以根據評測報告選擇滿足自己需求的產品,而對於機器翻譯的研究人員來說,評測能夠給他們的技術發展方向提供最可靠的依據。

——摘自北京郵電大學信息工程系張劍博士在微軟亞洲研究院訪問期間完成的一篇論文中的一段話。  

  早在90年代初,美國國家自然基金委員會和歐盟就資助的國際語言工程標準(ISLE)計劃就專門設立了EWG(Evaluation Working Group)機器翻譯評測工作組。1992年至1994年之間,美國國防部高級研究計劃署(DARPA)專門組織一批專家從翻譯譯文的忠實度、流利度和信息量三個角度對當時的法英、日英、西英的機器翻譯系統進行了大規模的評測。目前比較流行的自動評測方法是是IBM提出的BLEU算法,BLEU(bilingual evaluation understudy),簡單來說,BLEU算法的思想就是機器翻譯的譯文越接近人工翻譯的結果,它的翻譯質量就越高。所以評測算法就是如何定義機器翻譯譯文與參考譯文之間的相似度。

  我們看下BLEU算法具體的細節吧:

N-gram

  BLEU 采用一種N-gram的匹配規則,原理比較簡單,就是比較譯文和參考譯文之間n組詞的相似的一個占比。

  例如:

    原文:今天天氣不錯

    機器譯文:It is a nice day today

    人工譯文:Today is a nice day

  如果用1-gram匹配的話:

    技術分享

  可以看到機器譯文一共6個詞,有5個詞語都命中的了參考譯文,那麽它1-gram的匹配度為 5/6

  我們再以3-gram舉例:

    技術分享

  可以看到機器譯文一共可以分為四個3-gram的詞組,其中有兩個可以命中參考譯文,那麽它3-gram的匹配度為 2/4

  依次類推,我們可以很容易實現一個程序來遍歷計算N-gram的一個匹配度。一般來說1-gram的結果代表了文中有多少個詞被單獨翻譯出來了,因此它反映的是這篇譯文的忠實度;而當我們計算2-gram以上時,更多時候結果反映的是譯文的流暢度,值越高文章的可讀性就越好。

召回率

  上面所說的方法比較好理解,也比較好實現,但是沒有考慮到召回率,舉一個非常簡單的例子說明:

  原文:貓站在地上

  機器譯文:the the the the

  人工譯文:The cat is standing on the ground

  在計算1-gram的時候,the 都出現在譯文中,因此匹配度為4/4 ,但是很明顯 the 在人工譯文中最多出現的次數只有2次,因此BLEU算法修正了這個值的算法,首先會計算該n-gram在譯文中可能出現的最大次數:

  技術分享

  Count是N-gram在機器翻譯譯文中的出現次數,Max_Ref_Count是該N-gram在一個參考譯文中最大的出現次數,最終統計結果取兩者中的較小值。然後在把這個匹配結果除以機器翻譯譯文的N-gram個數。因此對於上面的例子來說,修正後的1-gram的統計結果就是2/4。

  我們將整個要處理的將機器翻譯的句子表示為Ci,標準答案表示為 Si=si1,...sim(m表示有m個參考答案)  

  n-grams表示n個單詞長度的詞組集合,令Wk第k個n-gram

  比如這樣的一句話,”I come from china”,第1個2-gram為:I come; 第2個2-gram為:come from; 第3個2-gram為:from china;

  Hk(Ci) 表示Wk翻譯選譯文Ci中出現的次數

  Hk(Sij) 表示Wk在標準答案Sij中出現的次數

  綜上所述各階N-gram的精度都可以按照下面這個公式計算:

  技術分享

  

  maxi∈mhk(sij)表示某n-gram在多條標準答案中出現最多的次數

  ∑i∑kmin(hk(ci),maxj∈mhk(sij))表示取n-gram在翻譯譯文和標準答案中出現的最小次數

懲罰因子

  上面的算法已經足夠可以有效的翻譯評估了,然而N-gram的匹配度可能會隨著句子長度的變短而變好,因此會存在這樣一個問題:一個翻譯引擎只翻譯出了句子中部分句子且翻譯的比較準確,那麽它的匹配度依然會很高。為了避免這種評分的偏向性,BLEU在最後的評分結果中引入了長度懲罰因子(Brevity Penalty)。

  技術分享

  BP的計算公式如上。lc代表表示機器翻譯譯文的長度,ls表示參考答案的有效長度,當存在多個參考譯文時,選取和翻譯譯文最接近的長度。當翻譯譯文長度大於參考譯文的長度時,懲罰系數為1,意味著不懲罰,只有機器翻譯譯文長度小於參考答案才會計算懲罰因子。

BLEU 

  由於各N-gram統計量的精度隨著階數的升高而呈指數形式遞減,所以為了平衡各階統計量的作用,對其采用幾何平均形式求平均值然後加權,再乘以長度懲罰因子,得到最後的評價公式:

  技術分享

  BLEU的原型系統采用的是均勻加權,即Wn=1/N 。N的上限取值為4,即最多只統計4-gram的精度。

實例

  譯文(Candidate)

Going to play basketball this afternoon ?

  參考答案(Reference)

Going to play basketball in the afternoon ?

  譯文gram長度:7  參考答案gram長度:8

  先看1-gram,除了this這個單詞沒有命中,其他都命中了,因此:

    P1 = 6/7 = 0.85714...

  其他gram以此類推:

    P2 = 4/6 = 0.6666..

    P3 = 2/5 = 0.4

    P4 = 1/4 = 0.25

  再計算logPn,這裏用python自帶的:

  技術分享

  ∑logPn和為-2.8622 ;再乘以Wn,也就是除以4為 0.7156

  BP = e^(1-8/7) 約等於 0.867

  BLEU = 0.867 * e^((P1 + P2 + P3 + P4)/4) = 0.867*0.4889 = 0.4238

  本來打算自己實現一個python的代碼,結果發現已經有國外小哥做了,拿下來稍微修改了點內容,這裏供大家參考

#-*- coding:utf-8 -*-
import sys
import codecs
import os
import math
import operator
import json


# 如果是一份答案的話,務必在答案的後面加上.txt   python Bleu.py Candidate ref.txt 
# 如果是多份答案的話,把多份答案放到一個文件夾中  python Bleu.py Candidate 文件夾

def fetch_data(cand, ref):
    """ Store each reference and candidate sentences as a list """
    references = []
    if .txt in ref:
        reference_file = codecs.open(ref, r, utf-8)
        references.append(reference_file.readlines())
    else:
        for root, dirs, files in os.walk(ref):
            for f in files:
                reference_file = codecs.open(os.path.join(root, f), r, utf-8)
                references.append(reference_file.readlines())
    candidate_file = codecs.open(cand, r, utf-8)
    candidate = candidate_file.readlines()
    return candidate, references


def count_ngram(candidate, references, n):
    clipped_count = 0
    count = 0
    r = 0
    c = 0
    for si in range(len(candidate)):
        # Calculate precision for each sentence
        #print si
        ref_counts = []
        ref_lengths = []
        #print references
        # Build dictionary of ngram counts
        for reference in references:
            #print ‘reference‘ + reference
            ref_sentence = reference[si]
            ngram_d = {}
            words = ref_sentence.strip().split()
            ref_lengths.append(len(words))
            limits = len(words) - n + 1
            # loop through the sentance consider the ngram length
            for i in range(limits):
                ngram =  .join(words[i:i+n]).lower()
                if ngram in ngram_d.keys():
                    ngram_d[ngram] += 1
                else:
                    ngram_d[ngram] = 1
            ref_counts.append(ngram_d)
        # candidate
        cand_sentence = candidate[si]
        cand_dict = {}
        words = cand_sentence.strip().split()
        limits = len(words) - n + 1
        for i in range(0, limits):
            ngram =  .join(words[i:i + n]).lower()
            if ngram in cand_dict:
                cand_dict[ngram] += 1
            else:
                cand_dict[ngram] = 1
        clipped_count += clip_count(cand_dict, ref_counts)
        count += limits
        r += best_length_match(ref_lengths, len(words))
        c += len(words)
    if clipped_count == 0:
        pr = 0
    else:
        pr = float(clipped_count) / count
    bp = brevity_penalty(c, r)
    return pr, bp


def clip_count(cand_d, ref_ds):
    """Count the clip count for each ngram considering all references"""
    count = 0
    for m in cand_d.keys():
        m_w = cand_d[m]
        m_max = 0
        for ref in ref_ds:
            if m in ref:
                m_max = max(m_max, ref[m])
        m_w = min(m_w, m_max)
        count += m_w
    return count


def best_length_match(ref_l, cand_l):
    """Find the closest length of reference to that of candidate"""
    least_diff = abs(cand_l-ref_l[0])
    best = ref_l[0]
    for ref in ref_l:
        if abs(cand_l-ref) < least_diff:
            least_diff = abs(cand_l-ref)
            best = ref
    return best


def brevity_penalty(c, r):
    if c > r:
        bp = 1
    else:
        bp = math.exp(1-(float(r)/c))
    
    return bp


def geometric_mean(precisions):
    return (reduce(operator.mul, precisions)) ** (1.0 / len(precisions))


def BLEU(candidate, references):
    precisions = []
    for i in range(4):
        pr, bp = count_ngram(candidate, references, i+1)
        precisions.append(pr)
        print P+str(i),  = ,round(pr, 2)
    print BP = ,round(bp, 2) 
    bleu = geometric_mean(precisions) * bp
    return bleu

if __name__ == "__main__":
    candidate, references = fetch_data(sys.argv[1], sys.argv[2])
    bleu = BLEU(candidate, references)
    print BLEU = ,round(bleu, 4)
    out = open(bleu_out.txt, w)
    out.write(str(bleu))
    out.close()

機器翻譯評測——BLEU算法詳解