2010年6月17日 星期四

用Python也能輕鬆玩自然語言處理(2.2)

2.2 條件次數分配(Conditional Frequency Distributions)


我們之前在1.3有先初步介紹過了次數分配的統計函數「FreqDist(mylist)」,可以幫助我們輕鬆地計算串列中各項目的出現情形,而現在我們將在這裡好好的談它。當語料庫的文本被區分各種類別(像是種類、主題、作者等),我們則可以個別地針對不同類別的文本進行次數統計,這將會讓我們有系統地去研究不同類別的差異。在之前的章節你應該已經看到不少次NLTK裡「條件次數分配」的應用了!實際上條件次數分配就是一種次數分配,只是他所強調的是特定的「條件(condition)」!而這個所謂的條件呢,則經常會是文本的類別。下圖就是要表示兩種不同條件的次數分配結果,一邊是新聞類型、一邊是言情類型。


條件與事件(Conditions and Events)



次數分配就是拿來計算並顯示出一些可以觀察的事件(events),像是那些在文本中的詞彙出現頻率。不過條件次數分配則需要去關心的是在某條件情境下字詞間的配對問題,因此比起詞彙的順序,詞彙配對的順序更是我們要處理的重點:

>>> text = ['The', 'Fulton', 'County', 'Grand', 'Jury', 'said', ...] 
>>> pairs = [('news', 'The'), ('news', 'Fulton'), ('news', 'County'), ...]


上例每組配對的形式為(條件,事件),如果我們根據布朗語料庫(Brown Corpus)為分析對象,他將會有15種條件(每類型算一種)、1,161,192種事件。

依照文別(類型)計算文字(Counting Words by Genre)



我們在2.1曾利用過布朗語料庫進行條件次數分配,每一種文別都視為一種條件來計算特定的一些動詞。較起FreqDist() 僅能處理單一的串列,ConditionalFreqDist()是處理一組配對(特定詞與特定條件)的串列:


>>> from nltk.corpus import brown 
>>> cfd = nltk.ConditionalFreqDist( 
...         (genre, word) 
...         for genre in brown.categories() 
...         for word in brown.words(categories=genre))


好啦!我們先從簡單的看起(上面的有點小複雜),先關注兩種類型「新聞」、「言情」,然後用迴圈去跑過每個文本類型裡頭的詞彙,最後就會產生出一組組的(類別,詞彙)配對了:


>>> genre_word = [(genre, word) 
...         for genre in ['news', 'romance'
...         for word in brown.words(categories=genre)] 
>>> len(genre_word) 
170576


接著我們可以從下面看到「genre_word」配對的串列組的開頭是「('news', word)」,然後最後結尾是「('romance', word)


>>> genre_word[:4] 
[('news', 'The'), ('news', 'Fulton'), ('news', 'County'), ('news', 'Grand')] # [_start-genre]
>>> genre_word[-4:] 
[('romance', 'afraid'), ('romance', 'not'), ('romance', "''"), ('romance', '.')] # [_end-genre]


現在我們就可以運用特個配對的串列表來產生一個條件次數分配並存到變數「cfd」裡頭。同樣地,我們可以輸入變數名稱去檢查或檢視它的那兩種條件:


>>> cfd = nltk.ConditionalFreqDist(genre_word) 
>>> cfd 
<ConditionalFreqDist with 2 conditions>
>>> cfd.conditions() 
['news', 'romance'] # [_conditions-cfd]


讓我們來存取一下這兩種條件並應用他去顯示一些次數統計!


>>> cfd['news'
<FreqDist with 100554 outcomes>
>>> cfd['romance'
<FreqDist with 70022 outcomes>
>>> list(cfd['romance']) 
[',', '.', 'the', 'and', 'to', 'a', 'of', '``', "''", 'was', 'I', 'in', 'he', 'had', '?', 'her', 'that', 'it', 'his', 'she', 'with', 'you', 'for', 'at', 'He', 'on', 'him', 'said', '!', '--', 'be', 'as', ';', 'have', 'but', 'not', 'would', 'She', 'The', ...]
>>> cfd['romance']['could'
193


利用圖表來呈現次數分配



除了同時檢視多組次數分配之外,「ConditionalFreqDist」還提供了方便的呈現工具,「圖表」!如果你還有印象,我們在2.1曾經看過一些次數統計的圖表,以下我們將慢慢仔細回味下面這支程式碼一番吧。它的條件是比對兩個詞彙「america」與「citizen」並計算它們在各年代就職演說內容的出現頻率後,最終以圖表呈現。你可能會對「fileud[:4]」感到疑惑,其實是因為演說稿的檔名前四個字元就是該年的年代(如1865-Lincoln.txt),這段程式碼的結果會產生一個組組的配對(如「('america', '1865')」)來告訴我們哪個字出現哪一年的演說稿,並且確保大小寫、開頭字等都要納入考量!

>>> from nltk.corpus import inaugural 
>>> cfd = nltk.ConditionalFreqDist( 
...         (target, fileid[:4]) 
...          for fileid in inaugural.fileids() 
...         for w in inaugural.words(fileid) 
...         for target in ['america', 'citizen'
...         if w.lower().startswith(target))


另外一個我們曾看過的例子,當條件次數分配被應用去比較同一文本,但不同語言間的詞彙長度。基本上的規則都差不多,但值得注意的是在提領詞彙時的語言別參數,除了語言別以外,還要加上編碼方式,如:「(lang + '-Latin1'))」。

>>> from nltk.corpus import udhr 
>>> languages = ['Chickasaw', 'English', 'German_Deutsch', ... 'Greenlandic_Inuktikut', 'Hungarian_Magyar', 'Ibibio_Efik'
>>> cfd = nltk.ConditionalFreqDist( 
... (lang, len(word))
... for lang in languages 
... for word in udhr.words(lang + '-Latin1'))


當你的配對變數(cfd)正確地建立好之後,則可以開始選擇利用「折線圖plot() 」或「表格tabulate()」來視覺化地呈現結果,只需要給他條件的參數「conditions= parameter」。或是你不給他指定的條件,則會預設為所有條件。同樣地,可以限制意欲進行比較的樣本大小(samples= parameter)。因此你可以載入大量的資料到條件次數分配裡頭,而結果呈現時在選擇所需的的條件與樣本大小,給我們充分的掌控權!例如我們只想選定兩種語言來分析、以表格呈現、字元長度在10以內並且以累積增加的計算方式。從下面可以得知,在英文的文本中,有1638個字小於9個字元!



你可能已經發現在使用條件次數分配時這些多行的表達式看起來很像之前曾介紹的串列理解式(list comprehensions)但卻沒有刮號!一般來說,當有一個參數必須由一個函數產生時(如「set([w.lower for w in t])」)可以把串列理解式的中刮號刪掉,改寫成像這樣「set(w.lower() for w in t)」!這是另一種更易讀且簡便的表達方式,稱為「generator expressions」,我們會在4.2再來詳談這部分!


利用Bigrams來產生隨機文本



我們以經知道可以運用條件次數分配來產生一個配對表格,那我們曾經在1.3介紹過的Bigrams呢?「bigrams()」可以將串列中的字詞變成另一個配對串列,而且是將原本字詞一個一個連續的連結配對起來:

>>> sent = ['In', 'the', 'beginning', 'God', 'created', 'the', 'heaven', ... 'and', 'the', 'earth', '.'
>>> nltk.bigrams(sent) 
[('In', 'the'), ('the', 'beginning'), ('beginning', 'God'), ('God', 'created'), ('created', 'the'), ('the', 'heaven'), ('heaven', 'and'), ('and', 'the'), ('the', 'earth'), ('earth', '.')]


在下面的例子中,我們把每一個字都當成一種條件,並針對每個字都能有效地計算在其後面的字詞出現次數。程式碼的前四行定義了一個新函數「generate_model()」用一個簡單的迴圈來產生文本,當我們呼叫它時,會給它一個字(如'living')作為初始值,接著把它丟進迴圈裡面,會先印一次出來接著找出與它同時出現(bigram裡的另一個字)最多次的那個字(就是max()做的事),然後就把變數「word」置換成那個字在重新執行一次迴圈,一共執行15次,因此最後等於會生出一個15次的印出結果!

def generate_model(cfdist, word, num=15): 
    for i in range(num): 
        print word, 
        word = cfdist[word].max() 

text = nltk.corpus.genesis.words(
'english-kjv.txt'
bigrams = nltk.bigrams(text) 
cfd = nltk.ConditionalFreqDist(bigrams)
  >>> print cfd['living'
<FreqDist: 'creature': 7, 'thing': 4, 'substance': 2, ',': 1, '.': 1, 'soul': 1>
>>> generate_model(cfd, 'living'
living creature that he said , and the land of the land of the land

介紹到這裡應該不難發現條件次數分配真的是自然語言處理中相當好用的工具!下表是一些常見常用的指令整理:

範例 說明
cfdist = ConditionalFreqDist(pairs) 產生一個條件次數分配的配對串列
cfdist.conditions() 按字順排列顯示所有條件
cfdist[condition] 顯示某條件的次數分配情況
cfdist[condition][sample] 顯示某條件中某樣本的次數分配情況
cfdist.tabulate() 以表格顯示條件次數分配情形
cfdist.tabulate(samples, conditions) 以表格顯示條件次數分配情形(樣本、條件限定)
cfdist.plot() 以圖形顯示條件次數分配情形
cfdist.plot(samples, conditions) 以圖形顯示條件次數分配情形(樣本、條件限定)
cfdist1 < cfdist2 測試某樣本在cfdist1是否小於在cfdist2

下一節: 2.3 多講一點Python嘛:可以重複使用的程式碼


沒有留言: