Gale和Church在1993年提出了一個基於長度進行句對齊的算法,並在附錄裏公開了C源代碼。這篇論文相當經典,以至於之後的關於句對齊的論文大多數要引用它。論文的題目是《 A Program for Aligning Sentences in Bilingual Corpora》。論文並不好懂,主要是因爲其中的術語和目標沒有給出清晰的定義。這篇博客就是要解讀一下這篇論文,順便解讀一個Python實現。
論文說,對齊分兩步。第一步是段落對其,第二步是在段落內部進行句對齊。論文說段落對齊重要,不過簡單,問題在於段落內部的句對齊。所以我只講已知段落對齊,怎樣在段落內進行句對齊。首先定義幾個概念,這些定義至關重要。所有論文中出現的符號都對應定義裏的符號。
- 段落: 語文裏的自然段。分爲源語言
L1 的段落和目標語言L2 的段落,或稱原文段落和譯文段落。 - 句子: 以句號、問號、省略號和感嘆號結尾的一段文字。或以分號和雙引號組成的對話。
- 句塊: 一個或多個連續的句子組成的片段。對應論文中的portion of text。
- 句塊對: 原文段落的一個句塊和譯文段落的一個句塊組成的對。
l1,l2 :l1 是句塊對中原文部分的長度,以字符爲單位,不包括空格(中文句子的字符是單個漢字)。l2 是句塊對中譯文部分的長度。c ,s2 : 假設源語言中的一個字符在目標語言中對應的字符數是一個隨機變量,且該隨機變量服從正態分佈N(c,s2) 。δ : 論文中定義爲(l2−l1c)/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) 。 - 句塊對序列:一個序列的句塊對,這些句塊對的原文部分的集合是原文段落的一個劃分(集合劃分),譯文部分的集合是譯文段落的一個劃分。
- 距離和: 距離和針對一個句塊對序列而言。對某個句塊對序列中的所有句塊對計算距離,再把這些距離加起來就是距離和。
- 對齊序列: 距離和最小的句塊對序列。距離和最小意味着此序列中的句塊對絕大部分最有可能相互對齊。
句對齊算法的輸入是某一對相互對齊的段落,輸出是對齊序列。
那麼問題來了,句塊對序列那麼多,哪個是對齊序列呢?如果用窮舉法,計算量太大,顯然不現實。換個角度想,假設我們已經知道了對齊序列,用
遞歸式的基礎情況
代碼鏈接在這裏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
操作。
到這裏程序的主幹也講完了,細節還得大家自己慢慢看。