基於Smadja算法的搭配詞自動提取實踐

搭配詞(collocation)

廣義而言,搭配詞(collocation)是指兩個或多個詞因爲語言習慣性結合在一起表示某種特殊意義的詞彙。搭配詞在不同的研究領域上有不同的解讀,尚未有一致性的定義。大概的意思就是詞語的習慣搭配了,就是學英語時老師一直拿來搪塞我們的那種『習慣搭配』。比如sit in traffic,表示堵車或者在通勤上花費了非常多時間的意思,那麼sit_traffic就是一個搭配詞。其中sit稱爲base word,traffic稱爲collocate. 所以collocations = base word + collocate.

前面的例子中介詞in去哪裏了呢?研究搭配詞時通常只研究base word和collocate兩個單詞,可以記爲(w,w1)(w, w_1),比如sit和traffic的collocation表示爲 (sit, traffic),中間的單詞都省略掉。這樣的表示類似bigram的形式,稱爲skip bigram,即跳過(skip)中間的一些詞,只取首尾兩個詞記爲bigram的形式。普通的bigram也是skip bigram的一種。

雖然skip bigram跳過了中間的詞,但不意味着這部分信息丟失了。skip bigram有個距離屬性,表示skip bigram中首尾兩個詞中間相差多少個詞。兩個相連的詞構成的skip bigram距離是1,比如sit in traffic的(sit, in)的距離爲1,所以沒有距離爲0的bigram! 但是有距離是負數的bigram,比如(in, sit)的距離爲-1,表示sit應該在前面。對於英語,這個距離一般取5和-5.

類似地,sit in traffic的所有skip bigram(包含普通bigram在內)有如下,括號中的數字表示距離。

  • (sit, in, 1)
  • (in, traffic, 1)
  • (in, sit, -1)
  • (traffic, in, -1)
  • (sit, traffic, 2)
  • (traffic, sit, -2)

搭配詞研究的意義在於有些詞合在一起可能符合語法,但平常幾乎不會用,或者合在一起沒有什麼意義,那麼從語料庫中找出常用的搭配詞就可以用於英文學習或句子改錯。比如你非要說stand in traffic,語法上看起來沒有錯誤,但(stand, traffic, 1)在語料庫中出現的次數很低,因爲沒有人這麼說。

Smadja算法

算法思想

搭配詞選取通常從語料庫中用統計的方法選出一些候選詞,然後判斷哪些搭配詞是合理的搭配詞。常用的統計方法有互信息,log likelihood ratio (LLR),t檢驗,chi-square檢驗等。另外一種方法是Smadja於1993年提出的基於ngram距離的語法關係搭配詞提取方法(Syntactic relation by distanced ngram analysis),這種方法很好用,應用也很廣泛。基本思想是:

  • 計算任意兩個單詞在距離dd時一起出現的次數,即距離爲dd的skip bigram的個數
    對於英文最大距離取5,最小距離取-5. 比如(play, role)在距離-4~4之間出現的次數如下表所示。
距離dd (play, role) count
-4 81230
-3 161358
-2 920270
-1 255149
1 27584
2 1428845
3 3452577
4 325548
  • 選取出現次數最高的skip bigram作爲最終的collocation,此時它們的距離是dd.
    比如上表中可以看到當play和role的距離爲3是他們出現的次數是最多的,此時(play, role, 3)就是最終的搭配詞,例如play an important role.

Smadaj算法有兩個基本假設:

  • 搭配詞出現的頻率遠高於非搭配詞,即(base word, collocate)出現的頻率高於其它組合
  • 搭配詞出現的次數在距離上的分佈有峯值

如下圖是(play, role)在距離-5~5之間的頻率分佈,可以看到在距離爲3時出現峯值。

這裏寫圖片描述

爲了應用Smadaj算法,我們需要計算出任意兩個單詞的skip bigram出現的次數以及它在每個距離上出現的次數。下圖是Smadaj論文中的圖。圖中base word是takeover,顯示的是takeover與所有搭配詞的頻率分佈。

這裏寫圖片描述

得到上表後Smadja提出了三個條件來篩選合理的搭配詞。一些符號表示如下。

記某個base word ww與它的搭配詞wiw_i的頻率是freqifreq_i,即上表中FreqFreq那一列。記某個base word的平均頻率爲fˉ\bar{f},即上表中FreqFreq一列的平均值,表示base word takeover 的平均頻率。記pijp_i^j爲搭配詞(w,wi)(w, w_i)在距離jj上出現的次數,piˉ\bar{p_i}爲搭配詞(w,wi)(w, w_i)在所有距離上出現次數的平均值,即:
piˉ=110j=55pij (j0)\bar{p_i} = \frac{1}{10}\sum_{j=-5}^5 p_i^j \ (j \neq 0)

Smadja算法中判斷搭配詞(w,wi)(w, w_i)是否合理的三個條件是:
{strength=freqifˉσk0(C1)spreadU0(C2)pjipiˉ+(k1×Ui)(C3)\begin{cases} strength &= \frac{freq_i - \bar{f}}{\sigma} \ge k_0 & (C_1)\\ spread &\ge U_0 & (C_2) \\ p_j^i &\ge \bar{p_i} + (k_1 \times \sqrt{U_i}) & (C_3) \end{cases}

其中Ui=j=110(pijpiˉ)210U_i = \frac{\sum_{j=1}^{10} (p_i^j - \bar{p_i})^2}{10}spread=Uispread = U_i. k0k_0k1k_1U0U_0是需要自己定義的閾值。Smadja給出的經驗值是k0=1k_0=1k1=1k_1=1U0=10U_0=10.

這三個條件直接,簡單,粗暴。解釋如下。

C1C_1中的strengthstrength實際上是將freqifreq_i標準化,計算freqifreq_izz-score. C1C_1可以篩掉在整個語料庫中出現頻率不高的搭配詞。

C2C_2中的spreadspread就是搭配詞在各個距離上頻率分佈的方差。如果某個搭配詞(w,wi)(w, w_i)spreadspread很小,表示它在各距離上的頻率分佈十分均勻,表示它可能並不是一個合理的搭配詞。因爲合理的搭配詞一般在某個距離上出現的次數很多,在其它距離上出現的次數很少,導致spreadspread會很大。

C1C_1C2C_2可以篩選出上表中那些搭配詞是合理的,C3C_3可以篩選出這些合理的搭配詞的距離。C3C_3在搭配詞在距離上的分佈基礎上,將距均值piˉ\bar{p_i} k1k_1倍標準差的項目篩選出來,實際上跟C1C_1一樣,也是利用zz-score來篩選。

三個條件的作用如下圖所示。C1C_1可以選出(takeover,possible)(takeover, possible)(takeover,corporate)(takeover, corporate),接着C2C_2從這兩個候選詞中選出(takeover,possible)(takeover, possible),最後C3C_3告訴我們(takeover,possible)(takeover, possible)搭配的距離是-1,即這個搭配詞的合理用法可能是 possible + some word + takeover.

這裏寫圖片描述

雖然Smadja的方法提出迄今好二十多年了,但是仍然很有用。

算法實現

以下是Smadja算法的Python實現。

求n-gram

如果距離取5,那麼就需要求出所有的bigram,trigram,4-gram和5-gram的組合,然後計算出skip bigram. 下面是求這些n-gram的代碼。

定義一個defaultdict類型的變量ngram_counts來存放n-gram以及它在每個距離上出現的次數。ngram_counts的key是n-gram中的n,value是一個Counter. Counter也相當於dict,它的key是以tuple形式保存的n-gram,value是這個n-gram在語料庫中出現的次數。

from nltk.tokenize import  wordpunct_tokenize 
from nltk.corpus import stopwords 
from collections import defaultdict, Counter

k0            = 1
k1            = 1
U0            = 10
max_distance  = 5

eng_stopwords = set(stopwords.words('english'))
eng_symbols   = '{}"\'()[].,:;+!?-*/&|<>=~$'

def ngram_is_valid(ngram):
    first, last = ngram[0], ngram[-1]
    if first in eng_stopwords or last in eng_stopwords: return False
    if any( num in first or num in last for num in '0123456789'): return False
    if any( eng_symbol in word for word in ngram for eng_symbol in eng_symbols): return False
    return True

# 求句子的n-gram    
def to_ngrams( unigrams, length):
    return zip(*[unigrams[i:] for i in range(length)])  

ngram_counts = defaultdict(Counter)
with open('citeseerx_descriptions_sents.txt.50000') as text_file:
    for index,line in enumerate(text_file): 
        words = wordpunct_tokenize(line)
        for n in range(2, max_distance + 2):
            ngram_counts[n].update(filter(ngram_is_valid, to_ngrams(words, n)))

比如要查看5-gram,上述代碼輸出的結果如下:

>> ngram_counts[5]
Counter({('In', 'this', 'paper', 'we', 'present'): 179, ('In', 'this', 'paper', 'we', 'describe'): 73, ('In', 'this', 'paper', 'we', 'propose'): 66, ('This', 'paper', 'presents', 'a', 'new'): 39})

求skip bigram

求出n-gram後需要將n-gram轉化爲skip bigram. 即求出skip bigram在各個距離上的頻率分佈。

skip bigram的形式是(w,wi)(w, w_i),同樣定義一個三層的defaultdict類型變量skip_bigram_info來保存skip bigram結果。skip_bigram_info的key是base word,value是一個dict,value的key是collocate,對應的value是一個Counter,Counter的key是距離,value是這個搭配詞在對應距離上出現的次數。

如何求skip bigram?

可以發現,比如一個5-gram ('In', 'this', 'paper', 'we', 'present')的次數是179,那麼('In', 'present', 4)出現的次數一定是179(最後一個數字代表skip bigram的距離)。所以只要取每個n-gram首尾的兩個詞組成skip bigram,就可以得到這個skip bigram在距離n1n-1上的次數。

skip_bigram_info = defaultdict(lambda: defaultdict(Counter))
for dist in range(2, max_distance + 2):
    for ngram, count in ngram_counts[dist].items():
        skip_bigram_info[ngram[0]][ngram[-1]] += Counter({dist-1: count})
        skip_bigram_info[ngram[-1]][ngram[0]] += Counter({1-dist: count}) # 求負向距離,單詞對調,距離求相反數即可

我們想看看base word爲play的搭配詞,可以輸入skip_bigram_info['play'],得到如下結果:

defaultdict(collections.Counter,
            {'Acquaintances': Counter({-1: 1}),
             'Agent': Counter({-2: 1}),
             'C': Counter({-1: 1}),
             'Domain': Counter({-3: 1}),
             'Elimination': Counter({-2: 1}),
             'Groups': Counter({-3: 1}),
             'In': Counter({-5: 1}),       
             'Interconnect': Counter({-2: 1}),
             'Interest': Counter({-4: 1, -2: 2}))

查看(play, role)在各個距離上的分佈,輸入skip_bigram_info['play']['role']:得到

Counter({-5: 1, -4: 2, -2: 2, 2: 8, 3: 51, 4: 5, 5: 1})

計算篩選條件

根據C1C_1C2C_2C3C_3計算一些統計量freq, fˉ, σ, ui, piˉfreq,\ \bar{f},\ \sigma,\ u_i,\ \bar{p_i}。這些統計量都按照tuple的形式存在dict類型的變量skip_bigram_abc中。以(play, role)爲例,存儲的形式如下:

skip_bigram_abc[play, 'avg_freq']
skip_bigram_abc[play, 'avg_freq']
skip_bigram_abc[('play','role','spread')]
skip_bigram_abc[('play','role','freq')]

計算各統計量代碼如下。

skip_bigram_abc = defaultdict(lambda: 0)
for word, vals in skip_bigram_info.items():
    count = []
    for coll, val in vals.items():
        c = val.values()
        c_bar = sum(c) / (2*max_distance)
        skip_bigram_abc[(word, coll, 'freq')] = sum(c)
        skip_bigram_abc[(word, coll, 'spread')] = (sum([x**2 for x in c]) - 2*c_bar*sum(c) + 2*max_distance*c_bar**2) / (2 * max_distance)
        count.append(sum(c))
    skip_bigram_abc[(word, 'avg_freq')] = np.mean(count)
    skip_bigram_abc[(word, 'dev')] = np.std(count)

根據上述計算的統計量,再計算符合條件的skip gram,同樣保存在一個dict類型的變量cc中,存儲格式如下:

# cc = [('base word', 'collocate', 'distance', 'strength', 'spread', 'peak', 'p'), ...]

篩選符合條件的skip bigram代碼如下:

def skip_bigram_filter(skip_bigram_info, skip_bigram_abc):
    cc = []
    for word, vals in skip_bigram_info.items():
        f = skip_bigram_abc[(word, 'avg_freq')]
        for coll, val in vals.items():
            if skip_bigram_abc[(word, 'dev')]-0 < 1E-6:
                strength = 0
            else:
                strength = (skip_bigram_abc[(word, coll, 'freq')] - f) / skip_bigram_abc[(word, 'dev')]
            if strength < k0:
                continue
            spread = skip_bigram_abc[(word, coll, 'spread')]
            if spread < U0:
                continue
            c_bar = sum(val.values()) / (2*max_distance)
            peak = c_bar + k1 * math.sqrt(spread)
            for dist, count in val.items():
                if count >= peak:
                    cc.append((word, coll, dist, strength, spread, peak, count))
    return cc

cc = skip_bigram_filter(skip_bigram_info, skip_bigram_abc)

結果展示

cc轉成pandas的DataFrame格式,比較好操作。

import pandas
collocations_df = pandas.DataFrame(cc,
                                   columns = ['base word', 'collocate', 'distance', 'strength', 'spread', 'peak', 'p'])
collocations_df = collocations_df.set_index(['base word', 'collocate', 'distance']).sort_index()

pandas的DataFrame結構可以在Jupyter Notebook顯示成表格,效果如下圖所示。

這裏寫圖片描述

完整的代碼可以參考Smadja Algorithm.

完整的代碼中還包括如何篩選特定POS tagging的搭配詞的代碼,比如篩選VB-NN關係的搭配詞(動詞-名詞形式)。有興趣的可以參考。

Reference

  1. Frank Smadja, Retrieving Collocations from Texts: Xtract, Computational Linguistics, Volume 19, 1993.
  2. 基於統計方法之中文搭配詞自動擷取
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章