在本期文章中,小生向您介紹了天然語言工具包(Natural Language Toolkit),它是一個將學術語言技術應用於文本數據集的 Python 庫。稱爲「文本處理」的程序設計是其基本功能;更深刻的是專門用於研究天然語言的語法以及語義分析的能力。html
鄙人並不是見多識廣, 語言處理(linguistic processing) 是一個相對新奇的領域。若是在對意義非凡的天然語言工具包(NLTK)的說明中出現了錯誤,請您諒解。NLTK 是使用 Python 教學以及實踐計算語言學的極好工具。此外,計算語言學與人工智能、語言/專門語言識別、翻譯以及語法檢查等領域關係密切。node
NLTK 會被天然地看做是具備棧結構的一系列層,這些層構建於彼此基礎之上。那些熟悉人工語言(好比 Python)的文法和解析的讀者來講,理解天然語言模型中相似的 —— 但更深奧的 —— 層不會有太大困難。python
儘管 NLTK 附帶了不少已經預處理(一般是手工地)到不一樣程度的全集,可是概念上每一層都是依賴於相鄰的更低層次的處理。首先是斷詞;而後是爲單詞加上 標籤;而後將成組的單詞解析爲語法元素,好比名詞短語或句子(取決於幾種技術中的某一種,每種技術都有其優缺點);最後對最終語句或其餘語法單元進行分類。經過這些步驟,NLTK 讓您能夠生成關於不一樣元素出現狀況的統計,並畫出描述處理過程自己或統計合計結果的圖表。linux
在本文中,您將看到關於低層能力的一些相對完整的示例,而對大部分高層次能力將只是進行簡單抽象的描述。如今讓咱們來詳細分析文本處理的首要步驟。正則表達式
斷詞(Tokenization)算法
您可使用 NLTK 完成的不少工做,尤爲是低層的工做,與使用 Python 的基本數據結構來完成相比,並無太大的區別。不過,NLTK 提供了一組由更高的層所依賴和使用的系統化的接口,而不僅是簡單地提供實用的類來處理加過標誌或加過標籤的文本。具體講,nltk.tokenizer.Token
類被普遍地用於存儲文本的有註解的片段;這些註解能夠標記不少不一樣的特性,包括詞類(parts-of-speech)、子標誌(subtoken)結構、一個標誌(token)在更大文本中的偏移位置、語形詞幹(morphological stems)、文法語句成分,等等。實際上,一個 Token
是一種特別的字典 —— 而且以字典形式訪問 —— 因此它能夠容納任何您但願的鍵。在 NLTK 中使用了一些專門的鍵,不一樣的鍵由不一樣的子程序包所使用。api
讓咱們來簡要地分析一下如何建立一個標誌並將其拆分爲子標誌:cookie
1 >>> from nltk.tokenize import * 2 3 >>> t = Token(TEXT='This is my first test sentence') 4 5 >>> WSTokenizer().tokenize(t, addlocs=True) # break on whitespace 6 7 >>> print t['TEXT'] This is my first test sentence 8 9 >>> print t['SUBTOKENS'] [@[0:4c], @[5:7c], @[8:10c], @[11:16c], @[17:21c], @[22:30c]] 10 11 >>> t['foo'] = 'bar' 12 13 >>> t @[0:4c], @[5:7c], @[8:10c], @[11:16c], @[17:21c], @[22:30c]]> 14 15 >>> print t['SUBTOKENS'][0] @[0:4c] 16 17 >>> print type(t['SUBTOKENS'][0])
概率(Probability)數據結構
對於語言全集,您可能要作的一件至關簡單的事情是分析其中各類 事件(events) 的頻率分佈,並基於這些已知頻率分佈作出機率預測。NLTK 支持多種基於天然頻率分佈數據進行機率預測的方法。我將不會在這裏介紹那些方法(參閱 參考資料 中列出的機率教程),只要說明您確定會 指望的那些與您已經 知道的 那些(不止是顯而易見的縮放比例/正規化)之間有着一些模糊的關係就夠了。less
基原本講,NLTK 支持兩種類型的頻率分佈:直方圖和條件頻率分佈(conditional frequency)。 nltk.probability.FreqDist
類用於建立直方圖;例如,能夠這樣建立一個單詞直方圖:
>>> from nltk.probability import * >>> article = Token(TEXT=open('cp-b17.txt').read()) >>> WSTokenizer().tokenize(article) >>> freq = FreqDist() >>> for word in article['SUBTOKENS']: ... freq.inc(word['TEXT']) >>> freq.B() 1194 >>> freq.count('Python') 12
機率教程討論了關於更復雜特性的直方圖的建立,好比「以元音結尾的詞後面的詞的長度」。 nltk.draw.plot.Plot
類可用於直方圖的可視化顯示。固然,您也能夠這樣分析高層次語法特性或者甚至是與 NLTK 無關的數據集的頻率分佈。條件頻率分佈可能比普通的直方圖更有趣。條件頻率分佈是一種二維直方圖 —— 它按每一個初始條件或者「上下文」爲您顯示一個直方圖。例如,教程提出了一個對應每一個首字母的單詞長度分佈問題。咱們就以這樣分析:
>>> cf = ConditionalFreqDist() >>> for word in article['SUBTOKENS']: ... cf[word['TEXT'][0]].inc(len(word['TEXT'])) ... >>> init_letters = cf.conditions() >>> init_letters.sort() >>> for c in init_letters[44:50]: ... print "Init %s:" % c, ... for length in range(1,6): ... print "len %d/%.2f," % (length,cf[c].freq(n)), ... print ... Init a: len 1/0.03, len 2/0.03, len 3/0.03, len 4/0.03, len 5/0.03, Init b: len 1/0.12, len 2/0.12, len 3/0.12, len 4/0.12, len 5/0.12, Init c: len 1/0.06, len 2/0.06, len 3/0.06, len 4/0.06, len 5/0.06, Init d: len 1/0.06, len 2/0.06, len 3/0.06, len 4/0.06, len 5/0.06, Init e: len 1/0.18, len 2/0.18, len 3/0.18, len 4/0.18, len 5/0.18, Init f: len 1/0.25, len 2/0.25, len 3/0.25, len 4/0.25, len 5/0.25,
條件頻率分佈在語言方面的一個極好應用是分析全集中的語段分佈 —— 例如,給出一個特定的詞,接下來最可能出現哪一個詞。固然,語法會帶來一些限制;不過,對句法選項的選擇的研究屬於語義學、語用論和術語範疇。
詞幹提取(Stemming)
nltk.stemmer.porter.PorterStemmer
類是一個用於從英文單詞中得到符合語法的(前綴)詞幹的極其便利的工具。這一能力尤爲讓我心動,由於我之前曾經用 Python 建立了一個公用的、全文本索引的搜索工具/庫(見 Developing a full-text indexer in Python 中的描述,它已經用於至關多的其餘項目中)。儘管對大量文檔進行關於一組確切詞的搜索的能力是很是實用的( gnosis.indexer
所作的工做),可是,對不少搜索用圖而言,稍微有一些模糊將會有所幫助。也許,您不能特別肯定您正在尋找的電子郵件是否使用了單詞 「complicated」、「complications」、「complicating」或者「complicates」,但您卻記得那是大概涉及的內容(可能與其餘一些詞共同來完成一次有價值的搜索)。
NLTK 中包括一個用於單詞詞幹提取的極好算法,而且讓您能夠按您的喜愛定製詞幹提取算法:
>>> from nltk.stemmer.porter import PorterStemmer >>> PorterStemmer().stem_word('complications') 'complic'
實際上,您能夠怎樣利用 gnosis.indexer 及其衍生工具或者徹底不一樣的索引工具中的詞幹提取功能,取決於您的使用情景。幸運的是,gnosis.indexer 有一個易於進行專門定製的開放接口。您是否須要一個徹底由詞幹構成的索引?或者您是否在索引中同時包括完整的單詞和詞幹?您是否須要將結果中的詞幹匹配從確切匹配中分離出來?在將來版本的 gnosis.indexer 中我將引入一些種類詞幹的提取能力,不過,最終用戶可能仍然但願進行不一樣的定製。
不管如何,通常來講添加詞幹提取是很是簡單的:首先,經過特別指定 gnosis.indexer.TextSplitter
來從一個文檔中得到詞幹;而後,固然執行搜索時,(可選地)在使用搜索條件進行索引查找以前提取其詞幹,多是經過定製您的 MyIndexer.find()
方法來實現。
在使用 PorterStemmer
時我發現 nltk.tokenizer.WSTokenizer
類確實如教程所警告的那樣很差用。它能夠勝任概念上的角色,可是對於實際的文本而言,您能夠更好地識別出什麼是一個 「單詞」。幸運的是, gnosis.indexer.TextSplitter
是一個健壯的斷詞工具。例如:
>>> from nltk.tokenizer import * >>> article = Token(TEXT=open('cp-b17.txt').read()) >>> WSTokenizer().tokenize(article) >>> from nltk.probability import * >>> from nltk.stemmer.porter import * >>> stemmer = PorterStemmer() >>> stems = FreqDist() >>> for word in article['SUBTOKENS']: ... stemmer.stem(word) ... stems.inc(word['STEM'].lower()) ... >>> word_stems = stems.samples() >>> word_stems.sort() >>> word_stems[20:40] ['"generator-bas', '"implement', '"lazili', '"magic"', '"partial', '"pluggable"', '"primitives"', '"repres',
'"secur', '"semi-coroutines."', '"state', '"understand', '"weightless', '"whatev', '#', '#-----', '#----------', '#-------------',
'#---------------', '#b17:']
查看一些詞幹,集合中的詞幹看起來並非均可用於索引。不少根本不是實際的單詞,還有其餘一些是用破折號鏈接起來的組合詞,單詞中還被加入了一些不相干的標點符號。讓咱們使用更好的斷詞工具來進行嘗試:
>>> article = TS().text_splitter(open('cp-b17.txt').read()) >>> stems = FreqDist() >>> for word in article: ... stems.inc(stemmer.stem_word(word.lower())) ... >>> word_stems = stems.samples() >>> word_stems.sort() >>> word_stems[60:80] ['bool', 'both', 'boundari', 'brain', 'bring', 'built', 'but', 'byte', 'call', 'can', 'cannot', 'capabl', 'capit', 'carri', 'case', 'cast', 'certain', 'certainli', 'chang', 'charm']
在這裏,您能夠看到有一些單詞有多個可能的擴展,並且全部單詞看起來都像是單詞或者詞素。斷詞方法對隨機文本集合來講相當重要;公平地講,NLTK 捆綁的全集已經經過 WSTokenizer()
打包爲易用且準確的斷詞工具。要得到健壯的實際可用的索引器,須要使用健壯的斷詞工具。
添加標籤(tagging)、分塊(chunking)和解析(parsing)
NLTK 的最大部分由複雜程度各不相同的各類解析器構成。在很大程度上,本篇介紹將不會解釋它們的細節,不過,我願意大概介紹一下它們要達成什麼目的。
不要忘記標誌是特殊的字典這一背景 —— 具體說是那些能夠包含一個 TAG
鍵以指明單詞的語法角色的標誌。NLTK 全集文檔一般有部分專門語言已經預先添加了標籤,不過,您固然能夠將您本身的標籤添加到沒有加標籤的文檔。
分塊有些相似於「粗略解析」。也就是說,分塊工做的進行,或者基於語法成分的已有標誌,或者基於您手工添加的或者使用正則表達式和程序邏輯半自動生成的標誌。不過,確切地說,這不是真正的解析(沒有一樣的生成規則)。例如:
>>> from nltk.parser.chunk import ChunkedTaggedTokenizer >>> chunked = "[ the/DT little/JJ cat/NN ] sat/VBD on/IN [ the/DT mat/NN ]" >>> sentence = Token(TEXT=chunked) >>> tokenizer = ChunkedTaggedTokenizer(chunk_node='NP') >>> tokenizer.tokenize(sentence) >>> sentence['SUBTOKENS'][0] (NP: ) >>> sentence['SUBTOKENS'][0]['NODE'] 'NP' >>> sentence['SUBTOKENS'][0]['CHILDREN'][0] >>> sentence['SUBTOKENS'][0]['CHILDREN'][0]['TAG'] 'DT' >>> chunk_structure = TreeToken(NODE='S', CHILDREN=sentence['SUBTOKENS']) (S: (NP: ) (NP: ))
所說起的分塊工做能夠由 nltk.tokenizer.RegexpChunkParser
類使用僞正則表達式來描述構成語法元素的一系列標籤來完成。這裏是機率教程中的一個例子:
>>> rule1 = ChunkRule('?*', ... 'Chunk optional det, zero or more adj, and a noun') >>> chunkparser = RegexpChunkParser([rule1], chunk_node='NP', top_node='S') >>> chunkparser.parse(sentence) >>> print sent['TREE'] (S: (NP: ) (NP: ))
真正的解析將引領咱們進入不少理論領域。例如,top-down 解析器能夠確保找到每個可能的產品,但可能會很是慢,由於要頻繁地(指數級)進行回溯。Shift-reduce 效率更高,可是可能會錯過一些產品。不論在哪一種狀況下,語法規則的聲明都相似於解析人工語言的語法聲明。本專欄曾經介紹了其中的一些: SimpleParse
、 mx.TextTools
、 Spark
和 gnosis.xml.validity
(參閱 參考資料)。
甚至,除了 top-down 和 shift-reduce 解析器之外,NLTK 還提供了「chart 解析器」,它能夠建立部分假定,這樣一個給定的序列就能夠繼而完成一個規則。這種方法能夠是既有效又徹底的。舉一個生動的(玩具級的)例子:
>>> from nltk.parser.chart import * >>> grammar = CFG.parse(''' ... S -> NP VP ... VP -> V NP | VP PP ... V -> "saw" | "ate" ... NP -> "John" | "Mary" |
"Bob" | Det N | NP PP ... Det -> "a" | "an" | "the" | "my" ... N -> "dog" | "cat" | "cookie" ... PP -> P NP ...
P -> "on" | "by" | "with" ... ''') >>> sentence = Token(TEXT='John saw a cat with my cookie') >>> WSTokenizer().tokenize(sentence) >>> parser = ChartParser(grammar, BU_STRATEGY, LEAF='TEXT') >>> parser.parse_n(sentence) >>> for tree in sentence['TREES']: print tree (S: (NP: ) (VP: (VP: (V: ) (NP: (Det: ) (N: )))
(PP: (P: ) (NP: (Det: ) (N: ))))) (S: (NP: ) (VP: (V: ) (NP: (NP: (Det: ) (N: ))
(PP: (P: ) (NP: (Det: ) (N: ))))))
probabilistic context-free grammar(或者說是 PCFG)是一種上下文無關語法,它將其每個產品關聯到一個機率。一樣,用於機率解析的解析器也捆綁到了 NLTK 中。