TM02. Collocation

分析完單一文字的詞頻後,通常會開始思考「用詞的脈絡」。用操作型定義來說就是,哪些字會出現在某個詞的附近?又哪些字經常會在一起出現?這類的方法可以從Collocation開始談起。

Def. Collocation [Wikipedia:collocation]. In corpus linguistics, a collocation is a series of words or terms that co-occur more often than would be expected by chance. In phraseology, collocation is a sub-type of phraseme. An example of a phraseological collocation, as propounded by Michael Halliday,[1] is the expression strong tea. While the same meaning could be conveyed by the roughly equivalent powerful tea, this expression is considered excessive and awkward by English speakers.

本單元將依照政大資科黃瀚萱老師的教材建議採用一個較長文本《共產黨宣言(The Communist Manifesto)》來介紹Collocation,並測試能否找到一些該文本的特徵。該文件可由Project Gutenberg免費電子書處下載,你也可以下載其他的英文書籍來做測試,以觀察文本間的差異。

1. 詞頻

載入資料

with open("data/corpus02.txt", encoding="utf8") as fin:
    text = fin.read()
print("Number of characters: %d" % len(text))

Number of characters: 75346

文字前處理

篩去停用詞的時機。當在處理單一文字時,若為一般性目的通常會篩去停用詞,例如要找到文本中的關鍵字就不需要這些停用詞,但也有些狀況是要保留停用詞的,比方說,你要研究美國總統候選人的說話習慣,通常這種「說話習慣」的分析在詞頻上可有兩種面向,一種是他會用什麼生冷或特殊字詞?另一種是,他講話習慣上會怎麼使用主持、連結詞等。第二種的範例就比方說,他會怎麼用We、You、I這類代名詞,這類代名詞分析往往是演講稿的分析的主要結果,甚至會發現在前後兩次競選候選人的說話習慣差異。就Collocation(詞對)分析而言,一樣要去取捨要不要篩去停用詞,但更要去評估篩去停用詞的時機,比方說,是在建立詞對前做篩除,還是在建立詞對後?若在建立詞對後,要用「且」或者是「或」的條件?這些取捨受文本型態影響,也會影響到後續的分析與詮釋。

前面我提到文本型態指的是什麼?比方說,Twitter分析、新聞內文分析、演講稿、書籍、研究論文的用字和風格都差非常多,所以,必須要根據文本特性來做分析。比方說,假設我現在有數萬篇研究論文的摘要,每篇論文摘要都有好幾個句子,但這些句子大體可分為是對BACKGROUND、MOTIVATION、METHOD、RESULTS、DISCUSSION的描述,當我要判斷這些句子究竟是哪一類型的描述時,通常我不會針對句子或文本的特有關鍵字去做分析,因為這些關鍵字是該文本特有的用詞,無助於分析出是哪一類型的句子。反倒是,我們只要看到The result shows that或者we conclude或we review...等再一般也不過的片語時,就知道他是在描述哪一種類型的句子。因此,在這種案例,我會選擇不要去除停用詞來做分析。

import nltk
# nltk.download('punkt')

from nltk.tokenize import word_tokenize
# from nltk.corpus import stopwords
# stopword_list = stopwords.words('english')

raw_tokens = word_tokenize(text)
tokens = []
for token in raw_tokens:
    if token.isalpha():
#         if token.lower() not in stopword_list: 
        tokens.append(token.lower())
print("Number of tokens: %d" % len(tokens))

印出文本中的常見的關鍵字

from collections import Counter
word_counts = Counter(tokens)
for w, c in word_counts.most_common(20):
    print("%s\t%d" % (w, c))

2. 詞對 Collocation

N-Grams:Collocation的概念可從N-Grams開始講起,所謂的N-Grams指的是把文本中的文字當成一個長長的List,每次只兩兩、三三、四四取出N個字。例如"Programming for Social Scientists"

  • 2-grams就是["Programming for", "for Social", "Social Scientists"]

  • 3-grams是["Programming for Social", "for Social Scientists"],依此類推。

Collocation的概念。Collocation指的是詞對,但怎樣的詞可以是一對的概念卻是彈性的,也就是說,我們常常在閱讀文章的時候,會覺得某兩個字老是一起出現,這兩個字可能會出現在前後,但也不一定會緊鄰,可能會隔幾個字,到底隔幾個字就可以算他們似乎在一起出現,這也是可以彈性定義的。N-Grams指的就是前後字才算,但Collocation並沒有這麼嚴謹的定義,大致上可以分為以下四種,我們將在本節中一一介紹。

  • Frequency-based:一定距離內的詞對出現頻率

  • Mean and Variance:並非單計算出現頻率,還要考慮詞對距離

  • Hypothesis Testing:用統計方法來檢驗詞對出現頻率的顯著性

  • Mutual Information:用機率的概念來檢驗詞對出現的相依與獨立性

計算並印出文本中的常見詞對

word_pair_counts = Counter()
for i in range(len(tokens) - 1):
    (w1, w2) = (tokens[i], tokens[i + 1])
    word_pair_counts[(w1, w2)] += 1

去除停用詞

# nltk.download('stopwords')
from nltk.corpus import stopwords
stopword_list = stopwords.words('english')

word_pair_nosw_counts = Counter()
for i in range(len(tokens) - 1):
    (w1, w2) = (tokens[i], tokens[i + 1])
    if w1 not in stopword_list and w2 not in stopword_list:
        word_pair_nosw_counts[(w1, w2)] += 1
    
for (w1, w2), c in word_pair_nosw_counts.most_common(20):
    print("%s\t%s\t%d" % (w1, w2, c))

3. 考慮詞對平均距離

在前面我們僅考慮2-grams也就是前後字的狀況。但往往詞對在應用上可能中間會插入若干個字,例如「open...door」這樣的兩個字可能就可以有以下數種情形,估計一下「open...door」的平均距離差不多是2到4之間。

  • open the door

  • open the black door

  • open the third closet door

  • open a bottle of wine and put on the table near the door

計算詞彙距離

window_size = 9

word_pair_counts = Counter()
word_pair_distance_counts = Counter()
for i in range(len(tokens) - 1):
    for distance in range(1, window_size):
        if i + distance < len(tokens):
            w1 = tokens[i]
            w2 = tokens[i + distance]
            word_pair_distance_counts[(w1, w2, distance)] += 1
            word_pair_counts[(w1, w2)] += 1

用平均距離來篩選詞對

pair_mean_distances = Counter()

for (w1, w2, distance), c in word_pair_distance_counts.most_common():
    pair_mean_distances[(w1, w2)] += distance * (c / word_pair_counts[(w1, w2)])

for (w1, w2), distance in pair_mean_distances.most_common(20):
    print("%s\t%s\t%f\t%d" % (w1, w2, distance, word_pair_counts[(w1, w2)]))

觀察詞對:平均詞對距離

pair_mean_distances = Counter()

for (w1, w2, distance), c in word_pair_distance_counts.most_common():
    if word_pair_counts[(w1, w2)] > 1:
        pair_mean_distances[(w1, w2)] += distance * (c / word_pair_counts[(w1, w2)])

觀察詞對:詞對距離的標準差

相同比較基準下看標準差,所獲得的是詞對距離的變異性,如果一個詞對的距離總是相同,那代表變異性很低,很固定;如果詞對距離變異性很大,那標準差就會高。像下面的Top20是標準差最高的(因為剛好兩對,一對應該是8、一對是距離1,所以除以二後距離恰成4.5)。由於高標準差的均是出現頻率最少的,所以,觀察Top20能獲得的資訊不多;但觀看Bottom20,也發現大部分都是兩對的詞對,而距離都一樣,所以標準差是0。因此,觀察Top20和Bottom20能夠獲得的資訊,單純就這邊的結果來說,似乎無法提供有意義的資訊,主因是Top20和Bottom20都是那些兩對的。據此,在下一節,我們原本只列計超過一次的詞對,但我們可以把他改為,只列計超過10對的詞對來觀察結果。

其公式為

pair_deviations = Counter()
for (w1, w2, distance), c in word_pair_distance_counts.most_common():
    if word_pair_counts[(w1, w2)] > 1:
        pair_deviations[(w1, w2)] += c * ((distance - pair_mean_distances[(w1, w2)]) ** 2)
    
for (w1, w2), dev_tmp in pair_deviations.most_common():
    s_2 = dev_tmp / (word_pair_counts[(w1, w2)] - 1)
    pair_deviations[(w1, w2)] = s_2 ** 0.5

觀察詞對:只考慮高頻詞對

由於在前一節發現低頻詞對下的詞對距離平均與標準差會使得我們難以觀察文本特徵,於是,在這一節我們要把納入計算的門檻值提高到超過10,來看看能不能得到一些有用的資訊。觀看變異性最低的前20對,終於可以看到一些,出現頻率高但詞對距離很穩定的詞對。

pair_deviations = Counter()
for (w1, w2, distance), c in word_pair_distance_counts.most_common():
    if word_pair_counts[(w1, w2)] > 10:
        pair_deviations[(w1, w2)] += c * ((distance - pair_mean_distances[(w1, w2)]) ** 2)
    
for (w1, w2), dev_tmp in pair_deviations.most_common():
    s_2 = dev_tmp / (word_pair_counts[(w1, w2)] - 1)
    pair_deviations[(w1, w2)] = s_2 ** 0.5

4. Pearson's Chi-Square Test

我們也可以用統計檢定來找到顯著的詞對。通常有t-test與Chi-square兩種,但是t-test需要有機率分佈近似常態的假設。Chi-square則概念上相對容易理解。

Chi-square的概念理解:可以試想一個生活中的問題,我觀察了一個學期,我懷疑我們班上w1w2是班對,這種「他們是一對」的感覺要怎麼操作化?簡單地說,就從你這學期觀察所有課堂和所有系所活動來觀察,假設你這學期一共出席了100次系所活動和課程,你如何深信w1w2是班對?這時候你應該很容易就可以想到,就有w1就有w2,沒w1w2也跟著不來了,也就是說w1出現w2不出現,和w2出現但w1不出現的次數很少。所以,你可以想像可以寫成(w1w2都來的次數 + w1w2都不來的次數 - w1w2不來的次數 - w2w1不來的次數)

下面那張表的O11, O12, O21, O22分別是

  • O11:w1w2都來的次數

  • O22:w1w2都不來的次數

  • O21:w1w2不來的次數

  • O12w2w1不來的次數

所以如果是上面的想法把它轉換為下面的表達方式就會是(O11 + O22 - O21 - O12),但Chi-Square的定義方法是(O11*O22 - O21*O12),那效果也是一樣的,就是兩個人一起來和一起不來的次數相乘,減去其中一個人來的次數相乘。而Chi-Square方程式其他的項在數理上你都可以當他是在做正規化。

Chi-square的定義

設計Chi-square函式

def chisquare(o11, o12, o21, o22):
    n = o11 + o12 + o21 + o22
    x_2 = (n * ((o11 * o22 - o12 * o21)**2)) / ((o11 + o12) * (o11 + o21) * (o12 + o22) * (o21 + o22)) 
    return x_2

回到二元詞組 Back to bigrams

word_pair_counts = Counter()
word_counts = Counter(tokens)
num_bigrams = 0

for i in range(len(tokens) - 1):
    w1 = tokens[i]
    w2 = tokens[i + 1]
    word_pair_counts[(w1, w2)] += 1
    num_bigrams += 1
print(num_bigrams) # 11780

計算每一詞對的chi-square

pair_chi_squares = Counter()
for (w1, w2), w1_w2_count in word_pair_counts.most_common():
    w1_only_count = word_counts[w1] - w1_w2_count
    w2_only_count = word_counts[w2] - w1_w2_count
    rest_count = num_bigrams - w1_only_count - w2_only_count - w1_w2_count
    pair_chi_squares[(w1, w2)] = chisquare(w1_w2_count, w1_only_count, w2_only_count, rest_count)

調整結果(TopN, 篩除停用詞)

pair_chi_squares = Counter()
for (w1, w2), w1_w2_count in word_pair_counts.most_common():
    if w1_w2_count > 5:
        w1_only_count = word_counts[w1] - w1_w2_count
        w2_only_count = word_counts[w2] - w1_w2_count
        rest_count = num_bigrams - w1_only_count - w2_only_count - w1_w2_count
        pair_chi_squares[(w1, w2)] = chisquare(w1_w2_count, w1_only_count, w2_only_count, rest_count)
for (w1, w2), x_2 in pair_chi_squares.most_common(20):
    print("%s\t%s\t%d\t%f" % (w1, w2, word_pair_counts[(w1, w2)], x_2))
productive	forces	9	9636.544203
middle	ages	7	4928.714781
no	longer	14	4150.496033
working	class	23	2477.732678
modern	industry	11	1128.037662
class	antagonisms	11	1042.736309
private	property	7	1022.522314
ruling	class	11	966.767323
can	not	9	775.745125
their	own	11	759.449519
proportion	as	8	720.619853
have	been	7	702.438620
it	has	20	652.817376
away	with	8	563.758348
to	be	22	468.075784
just	as	6	439.987822
of	the	244	406.859323
the	bourgeoisie	66	397.199063
its	own	8	392.296908
petty	bourgeois	6	381.069314

5. Mutual Information (MI)

另一個現在常用來評估兩個詞間的相依關係的方法是Mutual Information(MI),又稱Pointwise Mutual Information(PMI)。詳細情形可看Wikipedia:Mutual Information

在概率論和資訊理論中,兩個隨機變數的相互資訊(mutual Information,簡稱MI)或轉移資訊(transinformation)是變數間相互依賴性的量度。不同於相關係數,相互資訊並不局限於實值隨機變數,它更加一般且決定著聯合分布 p(X,Y) 和分解的邊緣分布的乘積 p(X)p(Y) 的相似程度。相互資訊是點間相互資訊(PMI)的期望值。from Wikipedia:Mutual Information(中文)

PMI的概念如下,P(x, y)代表x與y一起出現的機率,你可以想像說,如果P(x, y)中的x與y相互獨立的話,那P(x, y)就會等於P(x)P(y)就會等於分母,這樣兩者一除為1再取log就會變成零。所以,若x與y相互獨立的話,那麼PMI就是0,否則就會比0大。

Define a mutual information function

上述式子如果用Chi-Square那張表來表達,就相當於(O11/N)*(N*N/(O11+O21)/(O11+O12)) = O11*N/(O11+O21)/(O11+O21)。概念上分母和分子均與不難理解為什麼要那麼算,只是算式中少了O22這個項目。

import math
def mutual_information(w1_w2_prob, w1_prob, w2_prob):
    return math.log2(w1_w2_prob / (w1_prob * w2_prob))

Computing MI

num_unigrams = sum(word_counts.values())

pair_mutual_information_scores = Counter()
for (w1, w2), w1_w2_count in word_pair_counts.most_common():
    if w1_w2_count > 0:
        w1_prob = word_counts[w1] / num_unigrams
        w2_prob = word_counts[w2] / num_unigrams
        w1_w2_prob = w1_w2_count / num_bigrams
        pair_mutual_information_scores[(w1, w2)] = mutual_information(w1_w2_prob, w1_prob, w2_prob)

for (w1, w2), mi in pair_mutual_information_scores.most_common(20):
    print("%s\t%s\t%d\t%f" % (w1, w2, word_pair_counts[(w1, w2)], mi))
karl	marx	1	13.524297
frederick	engels	1	13.524297
czar	metternich	1	13.524297
police	spies	1	13.524297
nursery	tale	1	13.524297
italian	flemish	1	13.524297
danish	languages	1	13.524297
plebeian	lord	1	13.524297
complicated	arrangement	1	13.524297
manifold	gradation	1	13.524297
patricians	knights	1	13.524297
knights	plebeians	1	13.524297
journeymen	apprentices	1	13.524297
subordinate	gradations	1	13.524297
cape	opened	1	13.524297
proper	serving	1	13.524297
left	remaining	1	13.524297
heavenly	ecstacies	1	13.524297
chivalrous	enthusiasm	1	13.524297
egotistical	calculation	1	13.524297

<練習>詞對分析方法的比較

<練習>斷句對詞對分析的影響

前面在定義詞對的距離時,是定義出一篇文章中每個字的後九個字。但這九個字可能會跨句子而產生一些謬誤。所以,請你嘗試找到一篇有句號或標點符號的文章(Gutenberg的可能會被不自然地斷行而產生錯誤),遇到句號或者逗號就斷開句子,只要是在同一句子內的,就算他們有Collocation的特性,也就是相當於前面所定義的window_size是根據句子長短。請你跑跑看並和這章的結果做比較,看看你能夠觀察到什麼樣的差異。

Last updated