2010年5月8日 星期六

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

1.3 開始計算語言吧:簡單的統計

現在我們再把焦點拉回大量文本的計算方法(就像1.1),在開始進行前,希望你對之前兩節談到的一些基本指令與資料結構有所瞭解,而且也可以預測執行了程式後Python直譯器會有什麼反映:

>>> saying = ['After', 'all', 'is', 'said', 'and', 'done',
...           'more', 'is', 'said', 'than', 'done']
>>> tokens = set(saying)
>>> tokens = sorted(tokens)
>>> tokens[-2:]
這行會跑出什麼呢?
>>>


次數分配(Frequency Distributions)

我們怎麼去判斷那些文本中的字是不是能夠反應文本的主題性或種類之類的訊息?想像一下我們試著找出一個文本中出現頻率最高的50個字,從前我們會用「正」字標記法來一筆一筆統計次數,但是我們可以想像整本書做這樣的統計是會累死人的!

這種統計每個文字(word token)在文本中所出現的次數統計對自然語言處理才說是十分重要的!所以NLTK也內建的支援這項功能。接下來我們就來使用「FreqDist()」去找出Moby Dick中出現次數最高的前50個字吧:

>>> fdist1 = FreqDist(text1)
>>> fdist1
<FreqDist with 260819 outcomes>
>>> vocabulary1 = fdist1.keys()
>>> vocabulary1[:50]
[',', 'the', '.', 'of', 'and', 'a', 'to', ';', 'in', 'that', "'", '-', 'his', 'it', 'I', 's', 'is', 'he', 'with', 'was', 'as', '"', 'all', 'for', 'this', '!', 'at', 'by', 'but', 'not', '--', 'him', 'from', 'be', 'on', 'so', 'whale', 'one', 'you', 'had', 'have', 'there', 'But', 'or', 'were', 'now', 'which', '?', 'me', 'like']
>>> fdist1['whale']
906
>>>

透過對text1的存取,FreqDist會把該文本中260819個word tokens的出現次數進行統計,接著使用keys()來取得一個依照次數高低排序token的串列,我們運用前面學到的切片方法把出現次數前50個token顯示出來!

可是...從上面這些字詞裡頭好像也不太瞭解該文本再說什麼?唯一與語文本內容有呼應的可能只有出現900多次的「whale」,其他的幾乎都是沒有意義的「English Plumbing」!好吧,出現次數太多的詞可能沒什麼用,那我們看看那種只出現一次的字(hapaxes)好了,可以用「fdist1.hapaxes()」去取出這種特性的字詞串列,但是結果是可以直接預期的,就是一些冷僻用語,即使翻看了這本書的大半篇幅也沒看到它的蹤影!唉~所以就算找到出現次數太多跟太少的字詞好像也沒啥用處,我們再來想想其他辦法吧!

註:若使用FreqDist出現尚未定義的情況時,請記得先匯入「from nltk.book import *」模組!

更細膩地去選取字詞(Fine-grained Selection of Words)

那試試看比較長的字體吧,也許會比較能反映出文本的主題之類的訊息!這部分我們將運用到數學的集合概念,而目標是找出在文本中字詞長度超過15的字母的字,我們把符合這樣條件的字詞集合用P來表示,而V則表示文本中所有的字詞,現在來看看下面a.的數學集合表示法。不過這樣的條件式,轉換成python的話就是b.的樣子。

a.   {w | wV & P(w)}

b.   [w for w in V if p(w)]

不過不同之處在於,Python會把這個表達式的結果變成串列(list),而不是集合:

>>> V = set(text1)
>>> long_words = [w for w in V if len(w) > 15]
>>> sorted(long_words)
['CIRCUMNAVIGATION', 'Physiognomically', 'apprehensiveness', 'cannibalistically', 'characteristically', 'circumnavigating', 'circumnavigation', 'circumnavigations', 'comprehensiveness', 'hermaphroditical', 'indiscriminately', 'indispensableness', 'irresistibleness', 'physiognomically', 'preternaturalness', 'responsibilities', 'simultaneousness', 'subterraneousness', 'supernaturalness', 'superstitiousness', 'uncomfortableness', 'uncompromisedness', 'undiscriminating', 'uninterpenetratingly']
>>>


首先把text1的所有字都裝到V裡頭,接著把前面談到的條件表達式(出現在V、長度15)所產生的串列指向給變數「long_words」,最後排序印出來。這個條件式有許多小細節在後頭會慢慢解釋!

我們還是得把焦點放在那些
能反應文本內容的字詞才行,像是在text4中,這些很長的字反應了國家行政上的焦點,如constitutionally, transcontinental。但是在text5卻是反映出一些非正式用語,如boooooooooooglyyyyyy或是 yuuuuuuuuuuuummmmmmmmmmmm。我們這樣算是抓到文本中有特色的文詞了嗎?呃...這些很長的字詞大多都是出現一次(Hapaxes)的獨特詞,但是已經比找到那些出現次數高的字詞好多了。那似乎意味著我們可以試著去屏除那些常出現的短字(如the)跟極少出現的長字(如antiphilosophists),所以我們試著在談話性文本(text5)中抓出字詞長度大於7,出現次數也大於7的字詞吧:

>>> fdist5 = FreqDist(text5)
>>> sorted([w for w in set(text5) if len(w) > 7 and fdist5[w] > 7])
['#14-19teens', '#talkcity_adults', '((((((((((', '........', 'Question', 'actually', 'anything', 'computer', 'cute.-ass', 'everyone', 'football', 'innocent', 'listening', 'remember', 'seriously', 'something', 'together', 'tomorrow', 'watching']
>>>


可以發現到這次我們採用了兩個條件進行字詞的篩選(長度>7 & 出現次數>7)!到此,我們已經會利用一些條件自動去找出一些隱藏在文本中的字詞組,這是非常中要的一步。

詞組與二元語法(Collocations and Bigrams)

詞組(collocation)其實就是一組經常會同時出現的組合,如red wine就是詞組,而wine不是。詞組有一個很特別的地方,就是往往有不可替代性,像是你突然把其中一個字換掉,變成maroon wine就非常的奇怪。

為了要處理詞組,我們必須試著去擷取文本中的字詞,以一對一對的方式綁起來,這就是所謂的「二元語法(bigrams)」,最直接的辦法就是利用函數
bigrams()

>>> bigrams(['more', 'is', 'said', 'than', 'done'])
[('more', 'is'), ('is', 'said'), ('said', 'than'), ('than', 'done')]
>>>

從上面的例子可以看到,串列會被Python兩兩包成一組,原本分開的「than, done」也會變成「('than', 'done')」一組(這是新的資料結構,叫做tuple,可以先不用裡它)。換言之,所謂的詞組也不過就是那些出現次數比較多的bigram嘛~(除非有個文本專門在探討罕見字的片語)值得一提的是,比起那些個別出現次數較高的字來說,我們也許會對出現次數多的bigrams更感興趣,所以有現成的工具「collocations()」可用,它的運作方式以後會在詳談:

>>> text4.collocations()
Building collocations list
United States; fellow citizens; years ago; Federal Government; General Government; American people; Vice President; Almighty God; Fellow citizens; Chief Magistrate; Chief Justice; God bless; Indian tribes; public debt; foreign nations; political parties; State governments; National Government; United Nations; public money
>>> text8.collocations()
Building collocations list
medium build; social drinker; quiet nights; long term; age open; financially secure; fun times; similar interests; Age open; poss rship; single mum; permanent relationship; slim build; seeks lady; Late 30s; Photo pls; Vibrant personality; European background; ASIAN LADY; country drives
>>>

這些也能計算

除了計算文字本身,我們還可以把歪腦筋動到其他地方。像是綜合利用之前學會的方法把整個文本中每個文字的長度都拿出來計算:

>>> [len(w) for w in text1]
[1, 4, 4, 2, 6, 8, 4, 1, 9, 1, 1, 8, 2, 1, 4, 11, 5, 2, 1, 7, 6, 1, 3, 4, 5, 2, ...]
>>> fdist = FreqDist([len(w) for w in text1])
>>> fdist <FreqDist with 260819 outcomes>
>>> fdist.keys()
[3, 1, 4, 2, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 20]
>>>

先取出text1中所有字詞長度的串列,接著運用「FreqDist()」把前面這個串列中的一堆數字(約26萬個token項目)進行次數分配,並把結果存給fdist1,最後我們一樣可以用方法「keys()」排出順序。這裡面只會有20種item,因為如上面所見,這26多個token的長度都落於1~20之間。這樣一來我們就可以關心一些事情:這些長度的文字出現次數是多少?長度4的字母比長度5的字母多出現在文本中多少次...

>>> fdist.items()
[(3, 50223), (1, 47933), (4, 42345), (2, 38513), (5, 26597), (6, 17111), (7, 14399), (8, 9966), (9, 6428), (10, 3528), (11, 1873), (12, 1053), (13, 567), (14, 177), (15, 70), (16, 22), (17, 12), (18, 1), (20, 1)]
>>> fdist.max()
3
>>> fdist[3]
50223
>>>
fdist.freq(3)
0.19255882431878046
>>>


利用方法「items()」可以取出排名與其出現的次數,可以清楚看到長度3是出現最多的,次數高達5萬次(約佔整個文本的20%)。在此我們不再繼續深入探討下去,但是透過這類型的計算方法可以有效地運用到許多用途上,如判斷文件的作者、種類甚至是語言。下表為NLTK所定義的一些次數分配的函數:

範例 說明
fdist = FreqDist(samples) 根據samples(串列)產生一個次數分配的統計結果
fdist.inc(sample) 把sample(字串)也加入目前的次數運算中
fdist['monstrous'] 計算字串monstrous的出現次數
fdist.freq('monstrous') 計算字串monstrous的出現率
fdist.N() 顯示所有次數分配的總次數
fdist.keys() 排序顯示所有被計算次數分配的字串
for sample in fdist: 利用統計次數撰寫迴圈指令
fdist.max() 取出次數最多者
fdist.tabulate() 印出次數分配的結果表格
fdist.plot() 顯示次數分配的圖
fdist.plot(cumulative=True) 以累積計算的方式顯示次數分配的圖
fdist1 < fdist2 測試fdist1的次數統計結果是否小於fdist2(用於判斷式)

1.4 回到Python:怎麼做選擇?怎麼控制流程

沒有留言: