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操作。
到这里程序的主干也讲完了,细节还得大家自己慢慢看。

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