Gale和Church的句對齊算法解析

Gale和Church在1993年提出了一個基於長度進行句對齊的算法,並在附錄裏公開了C源代碼。這篇論文相當經典,以至於之後的關於句對齊的論文大多數要引用它。論文的題目是《 A Program for Aligning Sentences in Bilingual Corpora》。論文並不好懂,主要是因爲其中的術語和目標沒有給出清晰的定義。這篇博客就是要解讀一下這篇論文,順便解讀一個Python實現。
論文說,對齊分兩步。第一步是段落對其,第二步是在段落內部進行句對齊。論文說段落對齊重要,不過簡單,問題在於段落內部的句對齊。所以我只講已知段落對齊,怎樣在段落內進行句對齊。首先定義幾個概念,這些定義至關重要。所有論文中出現的符號都對應定義裏的符號。

  • 段落: 語文裏的自然段。分爲源語言L1 的段落和目標語言L2 的段落,或稱原文段落和譯文段落。
  • 句子: 以句號、問號、省略號和感嘆號結尾的一段文字。或以分號和雙引號組成的對話。
  • 句塊: 一個或多個連續的句子組成的片段。對應論文中的portion of text。
  • 句塊對: 原文段落的一個句塊和譯文段落的一個句塊組成的對。
  • l1,l2 : l1 是句塊對中原文部分的長度,以字符爲單位,不包括空格(中文句子的字符是單個漢字)。 l2 是句塊對中譯文部分的長度。
  • cs2 : 假設源語言中的一個字符在目標語言中對應的字符數是一個隨機變量,且該隨機變量服從正態分佈N(c,s2)
  • δ : 論文中定義爲l2l1c)/l1s2 。每一個句塊對都有自己的一個δ
  • 對齊模式: 或稱匹配模式,描述一個句塊對由幾個原文句子和幾個譯文句子組成。比如1-2表示一個原文句子翻譯成兩個譯文句子。
  • match : match 是對齊模式的隨機變量,它的分佈由論文的Table 5描述。
  • 距離(distance measure): 衡量一個句塊對中兩個句塊之間的距離,針對一個句塊對而言。距離度量是對 log(Prob(matchδ)) 的估計,估計的意思是距離越大,log(Prob(matchδ)) 越大,那麼Prob(matchδ) 就越小。Prob(matchδ) 的表述不準確,在這裏matchδ 都應該表示事件。當一個句塊對確定後,我們就知道它的對齊模式,也知道句塊對內部句塊的長度,即確定了matchδ 。所以Prob(matchδ) 的準確表述應該是Prob(match=mδ=n) ,其中m,n 是確定句塊對後相應的對齊模式和δ 值。距離越大,Prob(match=mδ=n) 越小的意思是,距離越大,此句塊對越不可能真的相互對齊。Prob(matchδ)) 正比於Prob(δmatch))Prob(match)
  • 句塊對序列:一個序列的句塊對,這些句塊對的原文部分的集合是原文段落的一個劃分(集合劃分),譯文部分的集合是譯文段落的一個劃分。
  • 距離和: 距離和針對一個句塊對序列而言。對某個句塊對序列中的所有句塊對計算距離,再把這些距離加起來就是距離和。
  • 對齊序列: 距離和最小的句塊對序列。距離和最小意味着此序列中的句塊對絕大部分最有可能相互對齊。

句對齊算法的輸入是某一對相互對齊的段落,輸出是對齊序列。

那麼問題來了,句塊對序列那麼多,哪個是對齊序列呢?如果用窮舉法,計算量太大,顯然不現實。換個角度想,假設我們已經知道了對齊序列,用D(i,j) 表示該對齊序列的距離和(就是論文中的D(i,j) ),其中i 是原文段落最後一個句子的索引,j 是譯文段落最後一個句子的索引。該對齊序列的距離和可以表示成該對齊序列最後一個句塊對的距離加上去掉最後一個句塊對的剩下的句塊對序列的距離和(可以認爲對齊序列的子序列也是對齊序列)。最後一個句塊對有六種對齊模式,所以要對每種模式分情況討論,選擇結果最小的那個。動態規劃的遞歸式就是這麼來的。
這裏寫圖片描述

遞歸式的基礎情況D(0,0)=0 ,通過遞歸式,我們可以求出對齊序列的距離和,在求距離和的過程中,我們順便記錄了對齊軌跡,也就是順便求出了對齊序列。這就是算法的主幹思想,還有很多細節相信大家可以自己看明白。下面我們看一下Python實現。

代碼鏈接在這裏Gale句對齊算法實現。如果不能科學上網可以去我的網盤下載。解壓後的文件夾是這樣的:
這裏寫圖片描述

運行也很簡單,在當前目錄python gale_church.py ntumc.eng ntumc.jpn,輸出如下:
這裏寫圖片描述
可以看到英文和日文已經對齊了。ntumc.eng和nutme.jpn是英文和日文的文本,文本的格式必須是段落以#開頭,一個句子一行。
下面開始正式解讀代碼。先從開頭的常量開始,然後沿着程序運行的脈絡講起。第24行

BEAD_COSTS = {(1, 1): 0, (2, 1): 230, (1, 2): 230, (0, 1): 450, 
              (1, 0): 450, (2, 2): 440 }

bead指的就是對齊情況,在第一版的代碼中(有三百多行,我們現在看的是第二版)作者對此進行了解釋:

self.penalty = {(0, 1): 450,  # inserted   : -100 * log(p(0,1))/p(1,1))
                        (1, 0): 450,  # deleted    : -100 * log(p(1,0))/p(1,1))
                        (1, 1): 0,    # substituted: -100 * log(p(1,1))/p(1,1))
                        (2, 1): 230,  # contracted : -100 * log(p(2,1))/p(1,1))
                        (1, 2): 230,  # expanded   : -100 * log(p(1,1))/p(1,1))
                        (2, 2): 440   # merged     : -100 * log(p(2,2))/p(1,1))

這個p(1,0)等可以從論文的Table 5查到。cost是跟距離相對應的,cost越大,距離越大。BEAD_COST記錄了每種對齊情況對距離的相對貢獻,以1-1爲基準,1-1對距離貢獻最小(1-1的時候,在同等條件下,距離相對最小),1-0對距離貢獻最大。因爲動態規劃最後比的是相對大小, 所以沒有必要算絕對大小。
代碼首先進入第121行:

  for src,trg in izip(readFile(corpusx),readFile(corpusy)):
    assert src[1] == trg[1]
    print src[1]
    for (sentence_x, sentence_y) in align(src[0], trg[0], mean, variance, bc):
      print sentence_x + "\t" + sentence_y

這裏src[0],trg[0]指的是原文段落,譯文段落。align函數的輸入是一對相互對齊的段落,輸出是對齊序列。
再看第76行的align函數:

def align(sx, sy, mean_xy, variance_xy, bc):
  """ Main alignment function. """
  cx = map(sent_length,sx); cy = map(sent_length, sy) 
  for (i1, i2), (j1, j2) in \
  reversed(list(_align(cx, cy, mean_xy, variance_xy, bc))):
    yield ' '.join(sx[i1:i2]), ' '.join(sy[j1:j2])

這裏sx,sy是原文段落和譯文段落。首先求出段落中每個句子的長度,這很合理,因爲算法就是基於長句子對齊長句子的思想。然後長度鏈表傳給了_align函數。
第46行,_align函數:

def _align(x, y, mean_xy, variance_xy, bead_costs):
  """ 
  The minimization function to choose the sentence pair with 
  cheapest alignment cost. 
  """
  m = {}
  for i in range(len(x) + 1):
    for j in range(len(y) + 1):
      if i == j == 0:
        m[0, 0] = (0, 0, 0)
      else:
        m[i, j] = min((m[i-di, j-dj][0] +
                      length_cost(x[i-di:i], y[j-dj:j], mean_xy, variance_xy) \
                      + bead_cost, di, dj)
                      for (di, dj), bead_cost in BEAD_COSTS.iteritems()
                      if i-di>=0 and j-dj>=0)

  i, j = len(x), len(y)
  while True:
    (c, di, dj) = m[i, j]
    if di == dj == 0:
      break
    yield (i-di, i), (j-dj, j)
    i -= di
    j -= dj

x,y是段落的句子長度鏈表。m記錄了對其軌跡。m是一個字典,字典的鍵是某個對齊句塊對在相應段落(原文段落和譯文段落)裏的索引,鍵值是一個元組,元組的第一個元素是句塊對的距離,第二個元素是句塊對原文部分的句子個數,第三個元素是譯文部分的句子個數。第57-61行就是動態規劃過程。輸出的時候先從對齊序列最後一個句塊對開始,所以第80行有個reversed操作。
到這裏程序的主幹也講完了,細節還得大家自己慢慢看。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章