2010年8月11日 星期三

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

2.4 語彙資源大集合(Lexical Resources)


詞彙或語彙資源(lexicon),指的就是一些字詞或片語的集合體,它們通常會伴隨一些詞性或語意的資訊在裡頭!語彙資源算是次級的文本,因為只是用來輔助文本用的。例如我們定義了一份文本「my_text」,接著定義「vocab = sorted(set(my_text))」用來裝my_text裡面的詞彙,以及「word_freq = FreqDist(my_text)」用來計算每個字詞出現的頻率。這樣一來,「vocab」與「word_freq」就成了最基本的語彙資訊。還有向我們之前在1.1看到的「concordance 」功能也算是一種語彙資源,提供了單詞的用法(就像字典那樣)。下圖列出了標準的語彙術語:一個語彙的款目(lexical entry)由其標題字(headword or lemma)所組成,並且可能提供一些額外資訊,如詞性(part of speech)或詞義說明。如果兩組字有相同的拼法叫做同音異義詞(homonyms)。


最簡單的語彙類型就是照字順排列字詞清單,而較為精密的則會擁有複雜的結構且具有橫跨個別款目的連結功能。在這一節中我們會看到一些NLTK提供的詞彙資源!



字詞列表(Wordlist Corpora)


NLTK所附的語料庫裡包括了不少字詞列表,「Words Corpus」儲存在「/usr/share/dict/words」底下(Unix的話)並提供拼寫檢查器,我們可以用來找尋文本中一些不常見或拼錯的字(如下例):

def unusual_words(text):
      text_vocab = set(w.lower()
for w in text if w.isalpha())
      english_vocab = set(w.lower()
for w in nltk.corpus.words.words())
      unusual = text_vocab.difference(english_vocab)       return sorted(unusual)
>>> unusual_words(nltk.corpus.gutenberg.words('austen-sense.txt')) ['abbeyland', 'abhorrence', 'abominably', 'abridgement', 'accordant', 'accustomary', 'adieus', 'affability', 'affectedly', 'aggrandizement', 'alighted', 'allenham', 'amiably', 'annamaria', 'annuities', 'apologising', 'arbour', 'archness', ...] >>> unusual_words(nltk.corpus.nps_chat.words()) ['aaaaaaaaaaaaaaaaa', 'aaahhhh', 'abou', 'abourted', 'abs', 'ack', 'acros', 'actualy', 'adduser', 'addy', 'adoted', 'adreniline', 'ae', 'afe', 'affari', 'afk', 'agaibn', 'agurlwithbigguns', 'ahah', 'ahahah', 'ahahh', 'ahahha', 'ahem', 'ahh', ...]


接著是另一個好用的字詞庫「停用詞表(stopwords」,它就是那些出現頻率極高的字群,像是「the, to, also」之類的,通常我們會希望在深入處理文本內容之前先將他們過濾掉!停用詞通常沒有什麼詞彙價值,而且他們會出現在各種文本中,讓我們在區別文本上產生混淆。

>>> from nltk.corpus import stopwords >>> stopwords.words('english') ['a', "a's", 'able', 'about', 'above', 'according', 'accordingly', 'across', 'actually', 'after', 'afterwards', 'again', 'against', "ain't", 'all', 'allow', 'allows', 'almost', 'alone', 'along', 'already', 'also', 'although', 'always', ...]


現在讓我們來建一個函數計算文本中不屬於停用詞組的文字區塊:

>>> def content_fraction(text): ...           stopwords = nltk.corpus.stopwords.words('english') ...           content = [w for w in text if w.lower() not in stopwords] ...           return len(content) / len(text) ... >>> content_fraction(nltk.corpus.reuters.words()) 0.65997695393285261


因此透過停用詞表我們可以直接過濾掉三分之一的字!你應該發現~現在我們已經學會了兩種過濾文本的方法囉XDD


字詞表也很適合用來應付字謎(word puzzles)!像上圖的那種造字遊戲,我們的程式要重複地爬過每一個字,並檢查它是否符合題目的各種條件(強制字母或字詞長度等)。只倚靠字母的合併來檢查並決定候選詞(candidate, 即將產生的那些符合條件字詞)是不容易的事,尤其是碰到來源字母還出現兩次時(如此例中的v)。所以我們利用比較「FreqDist」的方法來檢查那些候選詞的字母出現頻率是否小於或等於字謎的字母。

>>> puzzle_letters = nltk.FreqDist('egivrvonl') >>> obligatory = 'r' >>> wordlist = nltk.corpus.words.words() >>> [w for w in wordlist if len(w) >= 6 ...                                       and obligatory in w ...                                       and nltk.FreqDist(w) <= puzzle_letters] ['glover', 'gorlin', 'govern', 'grovel', 'ignore', 'involver', 'lienor', 'linger', 'longer', 'lovering', 'noiler', 'overling', 'region', 'renvoi', 'revolving', 'ringle', 'roving', 'violer', 'virole']


另外還有一個字詞表是人名語料庫,包括了8000組人名(first name)並依性別分類。男與女的人名分別儲存在兩個檔案,讓我們來找找看這兩個檔案裡頭有沒有重複的人名(就是找那些可以用在男生也可以用在女生的人名XD)

>>> names = nltk.corpus.names >>> names.fileids() ['female.txt', 'male.txt'] >>> male_names = names.words('male.txt') >>> female_names = names.words('female.txt') >>> [w for w in male_names if w in female_names] ['Abbey', 'Abbie', 'Abby', 'Addie', 'Adrian', 'Adrien', 'Ajay', 'Alex', 'Alexis', 'Alfie', 'Ali', 'Alix', 'Allie', 'Allyn', 'Andie', 'Andrea', 'Andy', 'Angel', 'Angie', 'Ariel', 'Ashley', 'Aubrey', 'Augustine', 'Austin', 'Averil', ...]


大家都知道人名裡的字母以「a」為結尾的通常都是女生,我們可以利用一段簡單的程式來利用統計圖讓這種現象一目了然(應該還記得name[-1]就表示為name的最後一個字母吧?):

>>> cfd = nltk.ConditionalFreqDist( ...              (fileid, name[-1]) ...              for fileid in names.fileids() ...              for name in names.words(fileid)) >>> cfd.plot()
*



發音字典( Pronouncing Dictionary)


發音字典是一種較為豐富的詞彙資源,NLTK提供卡內基美隆大學的CMU Pronouncing Dictionary,這是設計給語音合成器的工具。

>>> entries = nltk.corpus.cmudict.entries() >>> len(entries) 127012 >>> for entry in entries[39943:39951]: ...           print entry ... ('fir', ['F', 'ER1']) ('fire', ['F', 'AY1', 'ER0']) ('fire', ['F', 'AY1', 'R']) ('firearm', ['F', 'AY1', 'ER0', 'AA2', 'R', 'M']) ('firearm', ['F', 'AY1', 'R', 'AA2', 'R', 'M']) ('firearms', ['F', 'AY1', 'ER0', 'AA2', 'R', 'M', 'Z']) ('firearms', ['F', 'AY1', 'R', 'AA2', 'R', 'M', 'Z']) ('fireball', ['F', 'AY1', 'ER0', 'B', 'AO2', 'L'])


每個字都提供一組發音編碼的清單,清楚地標示每段的發聲。你可以注意到「fire」有兩個讀音,有一音節的念法「F AY1 R」或兩音節的「F AY1 ER0」,這些符號是由美國國防部高研院制訂的音素符號表「Arpabet」而來(詳情可參見http://en.wikipedia.org/wiki/Arpabet)。

每個款目由兩個部分組成,我們將它們用稍微複雜一點的for敘述式來個別處理,將entry拆成兩個變數「word」與「pron」。現在每執行一次迴圈,這兩個變數就會依序被讀取進來:

>>> for word, pron in entries: ...       if len(pron) == 3: ...               ph1, ph2, ph3 = pron ...               if ph1 == 'P' and ph3 == 'T': ...                       print word, ph2, ... pait EY1 pat AE1 pate EY1 patt AE1 peart ER1 peat IY1 peet IY1 peete IY1 pert ER1 pet EH1 pete IY1 pett EH1 piet IY1 piette IY1 pit IH1 pitt IH1 pot AA1 pote OW1 pott AA1 pout AW1 puett UW1 purt ER1 put UH1 putt AH1


上面的程式會掃瞄每個由三個發聲符號所組成的字詞款目,如果符合,將會把原本的變數pron切成三塊ph1, ph2, ph3。注意那些你還不太熟悉的敘述形式所產生的作用!

接下來的例子也是使用for敘述式,這次卻是利用一個簡單的串列理解式來處理,它的功能是找出所有尾音是「nicks」的字詞,這個用來找押韻字超方便的~

>>> syllable = ['N', 'IH0', 'K', 'S'] >>> [word for word, pron in entries if pron[-4:] == syllable] ["atlantic's", 'audiotronics', 'avionics', 'beatniks', 'calisthenics', 'centronics', 'chetniks', "clinic's", 'clinics', 'conics', 'cynics', 'diasonics', "dominic's", 'ebonics', 'electronics', "electronics'", 'endotronics', "endotronics'", 'enix', ...]


所以同一種發音可能會有不同的拼寫方式「nics, niks, nix, ntic's 」,接著我們來瞧瞧其他不同的拼寫組合吧,看看以下的例子你可以清楚瞭解它們的功能跟運作原理嗎?

>>> [w for w, pron in entries if pron[-1] == 'M' and w[-1] == 'n'] ['autumn', 'column', 'condemn', 'damn', 'goddamn', 'hymn', 'solemn'] >>> sorted(set(w[:2] for w, pron in entries if pron[0] == 'N' and w[0] != 'n')) ['gn', 'kn', 'mn', 'pn']


這些音素符號會包括一些數字在裡面,它們代表重音的程度,主要重音(1)、次要(2)與無重音(0),如同下面我們定義了一個可以擷取重音數字的函數,並且利用它來找尋各種我們希望看到的重音樣式的字詞們:

>>> def stress(pron): ...           return [char for phone in pron for char in phone if char.isdigit()] >>> [w for w, pron in entries if stress(pron) == ['0', '1', '0', '2', '0']] ['abbreviated', 'abbreviating', 'accelerated', 'accelerating', 'accelerator', 'accentuated', 'accentuating', 'accommodated', 'accommodating', 'accommodative', 'accumulated', 'accumulating', 'accumulative', 'accumulator', 'accumulators', ...] >>> [w for w, pron in entries if stress(pron) == ['0', '2', '0', '1', '0']] ['abbreviation', 'abbreviations', 'abomination', 'abortifacient', 'abortifacients', 'academicians', 'accommodation', 'accommodations', 'accreditation', 'accreditations', 'accumulation', 'accumulations', 'acetylcholine', 'acetylcholine', 'adjudication', ...]


我們可以利用條件次數分配來找尋最小比對(minimally-contrasting)的詞組,在這邊我們找尋以P開頭的三音節字,根據它們的首尾音來加以分群(忘記條件次數了嗎?可以回2.2複習一下唷):

註:這邊我也看好久才懂,所以簡單memo一下。先利用串列理解式產生一個置放一堆發音P開頭、三音節的字,並且做一個樣式配對「P-xx」而生出一組組的tuple,像是「[(P-CH, Perch), (P-K, pik), (P-CH, poach)...]」這樣一個串列p3。然後計算每種樣式的出現頻率(就是條件次數分配啦,所以每一種樣式就是它這邊的變數template),接著判斷出現超過10次以上的樣式,然後把那些字取出、串接好,然後顯示出來!

>>> p3 = [(pron[0]+'-'+pron[2], word) ...            for (word, pron) in entries ...            if pron[0] == 'P' and len(pron) == 3] >>> cfd = nltk.ConditionalFreqDist(p3) >>> for template in cfd.conditions(): ...           if len(cfd[template]) > 10: ...                  words = cfd[template].keys() ...                  wordlist = ' '.join(words) ...                  print template, wordlist[:70] + "..." ... P-CH perch puche poche peach petsche poach pietsch putsch pautsch piche pet... P-K pik peek pic pique paque polk perc poke perk pac pock poch purk pak pa... P-L pil poehl pille pehl pol pall pohl pahl paul perl pale paille perle po... P-N paine payne pon pain pin pawn pinn pun pine paign pen pyne pane penn p... P-P pap paap pipp paup pape pup pep poop pop pipe paape popp pip peep pope... P-R paar poor par poore pear pare pour peer pore parr por pair porr pier... P-S pearse piece posts pasts peace perce pos pers pace puss pesce pass pur... P-T pot puett pit pete putt pat purt pet peart pott pett pait pert pote pa... P-Z pays p.s pao's pais paws p.'s pas pez paz pei's pose poise peas paiz p...


除了去看整份字典外,我們也可以針對一些特定詞來做存取。我們隨時可以透過方括號查詢特定詞的音素內容:

>>> prondict = nltk.corpus.cmudict.dict() >>> prondict['fire'] [['F', 'AY1', 'ER0'], ['F', 'AY1', 'R']] >>> prondict['blog'] Traceback (most recent call last):      File "<stdin>", line 1, in <module> KeyError: 'blog' >>> prondict['blog'] = [['B', 'L', 'AA1', 'G']] >>> prondict['blog'] [['B', 'L', 'AA1', 'G']]


如果你不小心查詢到不存在的字詞,就會出現「KeyError」,就像當我們在在針對串列做索引值的指定時,若是輸入了超出原本索引範圍的數值時會出現「IndexError」一樣。如「blog」不在發音字典裡頭,所以我們指派了一個新的值給這個原本不存在的key(就是blog啦),但其實這只有現階段有用,對於NLTK語料庫是毫無影響的,下一次你存取blog時,一樣是找不到的!

我們可以使用任何詞彙資源來處理我們的文本,像是找出文本中各式條件的字詞、或是做一些對應。例如下面的例子中可以將文本的字詞的發音從發音字典中找出來顯示:

>>> text = ['natural', 'language', 'processing'] >>> [ph for w in text for ph in prondict[w][0]] ['N', 'AE1', 'CH', 'ER0', 'AH0', 'L', 'L', 'AE1', 'NG', 'G', 'W', 'AH0', 'JH', 'P', 'R', 'AA1', 'S', 'EH0', 'S', 'IH0', 'NG']



比較型文字列表(Comparative Wordlists)


另一個例子為比較型的文字列表,NLTK提供了所謂的「Swadesh wordlists(我也不知道怎麼翻)」,收錄各種語言的200個常用字,語言識別碼是依照 ISO 639的兩碼制:

>>> from nltk.corpus import swadesh >>> swadesh.fileids() ['be', 'bg', 'bs', 'ca', 'cs', 'cu', 'de', 'en', 'es', 'fr', 'hr', 'it', 'la', 'mk', 'nl', 'pl', 'pt', 'ro', 'ru', 'sk', 'sl', 'sr', 'sw', 'uk'] >>> swadesh.words('en') ['I', 'you (singular), thou', 'he', 'we', 'you (plural)', 'they', 'this', 'that', 'here', 'there', 'who', 'what', 'where', 'when', 'how', 'not', 'all', 'many', 'some', 'few', 'other', 'one', 'two', 'three', 'four', 'five', 'big', 'long', 'wide', ...]


我們可以透過「entries()」來製作一個配對組的串列來存取不同語言的同源字(cognate words,就是相同意思啦),甚至深入一點,我們可以把整個字典都做轉換(不過這個等到後面的章節在來詳談吧XD)

>>> fr2en = swadesh.entries(['fr', 'en']) >>> fr2en [('je', 'I'), ('tu, vous', 'you (singular), thou'), ('il', 'he'), ...] >>> translate = dict(fr2en) >>> translate['chien'] 'dog' >>> translate['jeter'] 'throw'


接著我們可以試著增加各種語言來進行轉換,像是下例中的德語轉英語、西語轉英語,利用「dict()」來建立翻譯字典,接著使用「update()」來更新這個字典,使他可以對應更多語言。

>>> de2en = swadesh.entries(['de', 'en'])    # German-English >>> es2en = swadesh.entries(['es', 'en'])    # Spanish-English >>> translate.update(dict(de2en)) >>> translate.update(dict(es2en)) >>> translate['Hund'] 'dog' >>> translate['perro'] 'dog'


也可以比較日耳曼語系與拉丁語系在一些字上的差異唷!

>>> languages = ['en', 'de', 'nl', 'es', 'fr', 'pt', 'la'] >>> for i in [139, 140, 141, 142]: ...           print swadesh.entries(languages)[i] ... ('say', 'sagen', 'zeggen', 'decir', 'dire', 'dizer', 'dicere') ('sing', 'singen', 'zingen', 'cantar', 'chanter', 'cantar', 'canere') ('play', 'spielen', 'spelen', 'jugar', 'jouer', 'jogar, brincar', 'ludere') ('float', 'schweben', 'zweven', 'flotar', 'flotter', 'flutuar, boiar', 'fluctuare')



現成的詞彙工具盒(Shoebox and Toolbox Lexicons)


最後介紹一下獨立使用上功能很不錯的「Toolbox」(它的前身為「Shoebox」),常被語言學家用來管理資料並可以自由地下載來使用(http://www.sil.org/computing/toolbox/)。Toolbox由大量的字詞款目組成,每個款目都包括一個或一個以上的欄位值,大部分的欄位值是選擇性或可重複的,這表示這種詞彙資源不能轉換為表格形式儲存。以下是Rotokas語的字典,我們就來看看它的第一條款目「kaa」意思是英文的「to gag」

>>> from nltk.corpus import toolbox >>> toolbox.entries('rotokas.dic') [('kaa', [('ps', 'V'), ('pt', 'A'), ('ge', 'gag'), ('tkp', 'nek i pas'), ('dcsv', 'true'), ('vx', '1'), ('sc', '???'), ('dt', '29/Oct/2005'), ('ex', 'Apoka ira kaaroi aioa-ia reoreopaoro.'), ('xp', 'Kaikai i pas long nek bilong Apoka bikos em i kaikai na toktok.'), ('xe', 'Apoka is gagging from food while talking.')]), ...]


可以清楚看到它的款目是由一系列的屬性/值的配對所組成的,如('ps', 'V')就表示詞性(part-of-speech)為「'V' (verb)動詞」; ('ge', 'gag')表示對應到英文(gloss-into-English)為「'gag'」這個字。最後三組配對是一個例句,分別是Rotokas語、Tok Pisin語以及英文。

這種Toolbox的鬆散結構讓我們在應用上困難許多,不過我們將會在11章學會利用強大的XML來處理。

下一節 2.5鼎鼎大名的詞網(WordNet)
 

沒有留言: