# 1\. 語言處理與 Python
上百萬字的文本,是容易拿到手的。假設我們會寫一些簡單的程序,那我們可以用它來做些什么?在本章中,我們將解決以下幾個問題:
1. 將簡單的程序與大量的文本結合起來,我們能實現什么?
2. 我們如何能自動提取概括文本風格和內容的關鍵詞和短語?
3. Python 編程語言為上述工作提供了哪些工具和技術?
4. 自然語言處理中有哪些有趣的挑戰?
本章分為完全不同風格的兩部分。在“語言計算”部分,我們將選取一些語言相關的編程任務而不去解釋它們是如何實現的。在“近觀 Python”部分,我們將系統地回顧關鍵的編程概念。兩種風格將按章節標題區分,而后面幾章將混合兩種風格而不作明顯的區分。我們希望這種風格的介紹能使你對接下來將要碰到的內容有一個真實的體味,與此同時,涵蓋語言學與計算機科學的基本概念。如果你對這兩個方面已經有了基本的了解,可以跳到第[5](http://www.nltk.org/book/ch01.html#sec-automatic-natural-language-understanding) 節 ; 我們將在后續的章節中重復所有要點,如果錯過了什么,你可以很容易地在`http://nltk.org/`上查詢在線參考材料。如果這些材料對你而言是全新的,那么本章將引發比解答本身更多的問題,這些問題將在本書的其余部分討論。
## 1 語言計算:文本和單詞
我們都對文本非常熟悉,因為我們每天都讀到和寫到。在這里,把文本視為我們寫的程序的原始數據,這些程序以很多有趣的方式處理和分析文本。但在我們能寫這些程序之前,我們必須得從 Python 解釋器開始。
## 1.1 Python 入門
Python 對用戶友好的一個方式是你可以交互式地直接打字給解釋器 —— 將要運行你的 Python 代碼的程序。你可以通過一個簡單的叫做交互式開發環境(Interactive DeveLopment Environment,簡稱 IDLE)的圖形接口來訪問 Python 解釋器。在 Mac 上,你可以在 _ 應用程序 _→_MacPython_ 中找到;在 Windows 中,你可以在 _ 程序 _→_Python_ 中找到。在 Unix 下,你可以在 shell 輸入`idle`來運行 Python(如果沒有安裝,嘗試輸入`python`)。解釋器將會輸出關于你的 Python 的版本簡介,請檢查你運行的是否是 Python 3.2 更高的版本(這里是 3.4.2):
```py
Python 3.4.2 (default, Oct 15 2014, 22:01:37)
[GCC 4.2.1 Compatible Apple LLVM 5.1 (clang-503.0.40)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>>
```
注
如果你無法運行 Python 解釋器可能是因為沒有正確安裝 Python。請訪問`http://python.org/`查閱詳細操作說明。NLTK 3.0 在 Python 2.6 和 2.7 上同樣可以工作。如果你使用的是這些較舊的版本,注意`/` 運算符會向下舍入小數(所以`1/3` 會得到`0`)。為了得到預期的除法行為,你需要輸入︰`from __future__ import division`
`>>>` 提示符表示 Python 解釋器正在等待輸入。復制這本書的例子時,自己不要鍵入"`>>>`"。現在,讓我們開始把 Python 當作計算器使用:
```py
>>> 1 + 5 * 2 - 3
8
>>>
```
一旦解釋器計算并顯示出答案,提示符就會出現。這表示 Python 解釋器在等待另一個指令。
注意
**輪到你來**:輸入一些你自己的表達式。你可以使用星號(`*`)表示乘法,左斜線(`/`)表示除法,你可以用括號括起表達式。
前面的例子演示了如何交互式的使用 Python 解釋器,試驗 Python 語言中各種表達式,看看它們做些什么。現在讓我們嘗試一個無意義的表達式,看看解釋器如何處理:
```py
>>> 1 +
File "<stdin>", line 1
1 +
^
SyntaxError: invalid syntax
>>>
```
產生了一個語法錯誤。在 Python 中,指令以加號結尾是沒有意義的。Python 解釋器會指出發生錯誤的行(“標準輸入”`<stdin>`的第 1 行)。
現在我們學會使用 Python 解釋器了,已經準備好可以開始處理語言數據了。
## 1.2 NLTK 入門
在進一步深入之前,應先安裝 NLTK 3.0,可以從`http://nltk.org/` 免費下載。按照說明下載適合你的操作系統的版本。
安裝完 NLTK 之后,像前面那樣啟動 Python 解釋器,在 Python 提示符后面輸入下面兩個命令來安裝本書所需的數據,然后選擇`book`集合,如[1.1](http://www.nltk.org/book/ch01.html#fig-nltk-downloader)所示。
```py
>>> import nltk
>>> nltk.download()
```

圖 1.1:下載 NLTK Book 集:使用`nltk.download()` 瀏覽可用的軟件包.下載器上**Collections** 選項卡顯示軟件包如何被打包分組,選擇**book** 標記所在行,可以獲取本書的例子和練習所需的全部數據。這些數據包括約 30 個壓縮文件,需要 100MB 硬盤空間。完整的數據集(即下載器中的**all**)在本書寫作期間大約是這個大小的 10 倍,還在不斷擴充。
一旦數據被下載到你的機器,你就可以使用 Python 解釋器加載其中一些。第一步是在 Python 提示符后輸入一個特殊的命令,告訴解釋器去加載一些我們要用的文本:`from nltk.book import *` 。這條語句是說“從 NLTK 的`book` 模塊加載所有的東西”。這個`book` 模塊包含你閱讀本章所需的所有數據。。在輸出歡迎信息之后,將會加載幾本書的文本(這將需要幾秒鐘)。下面連同你將看到的輸出一起再次列出這條命令。注意拼寫和標點符號的正確性,記住不要輸入`>>>`。
```py
>>> from nltk.book import *
*** Introductory Examples for the NLTK Book ***
Loading text1, ..., text9 and sent1, ..., sent9
Type the name of the text or sentence to view it.
Type: 'texts()' or 'sents()' to list the materials.
text1: Moby Dick by Herman Melville 1851
text2: Sense and Sensibility by Jane Austen 1811
text3: The Book of Genesis
text4: Inaugural Address Corpus
text5: Chat Corpus
text6: Monty Python and the Holy Grail
text7: Wall Street Journal
text8: Personals Corpus
text9: The Man Who Was Thursday by G . K . Chesterton 1908
>>>
```
任何時候我們想要找到這些文本,只需要在 Python 提示符后輸入它們的名字:
```py
>>> text1
<Text: Moby Dick by Herman Melville 1851>
>>> text2
<Text: Sense and Sensibility by Jane Austen 1811>
>>>
```
現在我們可以和這些數據一起來使用 Python 解釋器,我們已經準備好上手了。
## 1.3 搜索文本
除了閱讀文本之外,還有很多方法可以用來研究文本內容。詞語索引視角顯示一個指定單詞的每一次出現,連同一些上下文一起顯示。下面我們輸入`text1` 后面跟一個點,再輸入函數名`concordance`,然后將`"monstrous"` 放在括號里,來查一下 _Moby Dick_ 《白鯨記》中的詞 monstrous:
```py
>>> text1.concordance("monstrous")
Displaying 11 of 11 matches:
ong the former , one was of a most monstrous size . ... This came towards us ,
ON OF THE PSALMS . " Touching that monstrous bulk of the whale or ork we have r
ll over with a heathenish array of monstrous clubs and spears . Some were thick
d as you gazed , and wondered what monstrous cannibal and savage could ever hav
that has survived the flood ; most monstrous and most mountainous ! That Himmal
they might scout at Moby Dick as a monstrous fable , or still worse and more de
th of Radney .'" CHAPTER 55 Of the monstrous Pictures of Whales . I shall ere l
ing Scenes . In connexion with the monstrous pictures of whales , I am strongly
ere to enter upon those still more monstrous stories of them which are to be fo
ght have been rummaged out of this monstrous cabinet there is no telling . But
of Whale - Bones ; for Whales of a monstrous size are oftentimes cast up dead u
>>>
```
在一段特定的文本上第一次使用 concordance 會花費一點時間來構建索引,因此接下來的搜索會很快。
注意
**輪到你來:** 嘗試搜索其他詞;為了方便重復輸入,你也許會用到上箭頭,Ctrl-上箭頭或者 Alt-p 獲取之前輸入的命令,然后修改要搜索的詞。你也可以在我們包含的其他文本上搜索。例如, 使用`text2.concordance("affection")`,搜索 _Sense and Sensibility_《理智與情感》中的 affection。使用`text3.concordance("lived")` 搜索 Genesis《創世紀》找出某人活了多久。你也可以看看`text4`,_Inaugural Address Corpus_《就職演說語料》,回到 1789 年看看那時英語的例子,搜索如 nation, terror,god 這樣的詞,看看隨著時間推移這些詞的使用如何不同。我們也包括了`text5`,_NPS Chat Corpus_《NPS 聊天語料庫》:你可以在里面搜索一些網絡詞,如 im ur,lol。(注意這個語料庫未經審查!)
在你花了一小會兒研究這些文本之后,我們希望你對語言的豐富性和多樣性有一個新的認識。在下一章中,你將學習獲取更廣泛的文本,包括英語以外其他語言的文本。
詞語索引使我們看到詞的上下文。例如,我們看到 monstrous 出現的上下文, the ___ pictures 和 a ___ size。還有哪些詞出現在相似的上下文中?我們可以通過在被查詢的文本名后添加函數名`similar`,然后在括號中插入相關的詞來查找到:
```py
>>> text1.similar("monstrous")
mean part maddens doleful gamesome subtly uncommon careful untoward
exasperate loving passing mouldy christian few true mystifying
imperial modifies contemptible
>>> text2.similar("monstrous")
very heartily so exceedingly remarkably as vast a great amazingly
extremely good sweet
>>>
```
觀察我們從不同的文本中得到的不同結果。Austen 使用這些詞與 Melville 完全不同;在她那里,monstrous 是正面的意思,有時它的功能像詞 very 一樣作強調成分。
函數`common_contexts`允許我們研究兩個或兩個以上的詞共同的上下文,如 monstrous 和 very。我們必須用方括號和圓括號把這些詞括起來,中間用逗號分割:
```py
>>> text2.common_contexts(["monstrous", "very"])
a_pretty is_pretty am_glad be_glad a_lucky
>>>
```
注意
**輪到你來:** 挑選另一對詞,使用`similar()` 和`common_contexts()` 函數比較它們在兩個不同文本中的用法。
自動檢測出現在文本中的特定的詞,并顯示同樣上下文中出現的一些詞,這只是一個方面。我們也可以判斷詞在文本中的 _ 位置 _:從文本開頭算起在它前面有多少詞。這個位置信息可以用離散圖表示。每一個豎線代表一個單詞,每一行代表整個文本。在[1.2](http://www.nltk.org/book/ch01.html#fig-inaugural) 中,我們看到在過去 220 年中的一些顯著的詞語用法模式(在一個由就職演說語料首尾相連的人為組合的文本中)。可以用下面的方法畫出這幅圖。你也許會想嘗試更多的詞(如,liberty,constitution)和不同的文本。你能在看到這幅圖之前預測一個詞的分布嗎?跟以前一樣,請保證引號、逗號、中括號及小括號的使用完全正確。
```py
>>> text4.dispersion_plot(["citizens", "democracy", "freedom", "duties", "America"])
>>>
```

圖 1.2:美國總統就職演說詞匯分布圖:可以用來研究隨時間推移語言使用上的變化。
注意
**重要事項:** 為了畫出這本書中用到的圖形,你需要安裝 Python 的 NumPy 和 Matplotlib 包。請參閱`http://nltk.org/` 上的安裝說明。
注意
你還可以使用`https://books.google.com/ngrams` 畫出詞匯隨著時間的使用頻率。
現在輕松一下,讓我們嘗試產生一些剛才看到的不同風格的隨機文本。要做到這一點,我們需要輸入文本的名字后面跟函數名`generate`。(需要帶括號,但括號里沒有也什么。)
```py
>>> text3.generate()
In the beginning of his brother is a hairy man , whose top may reach
unto heaven ; and ye shall sow the land of Egypt there was no bread in
all that he was taken out of the month , upon the earth . So shall thy
wages be ? And they made their father ; and Isaac was old , and kissed
him : and Laban with his cattle in the midst of the hands of Esau thy
first born , and Phichol the chief butler unto his son Isaac , she
>>>
```
Note
`generate()` 方法在 NLTK 3.0 中不可用,但會在后續版本中恢復。
## 1.4 詞匯計數
關于前面例子中出現的文本,最明顯的事實是它們所使用的詞匯不同。在本節中,我們將看到如何使用計算機以各種有用的方式計數詞匯。像以前一樣,你將會馬上開始用 Python 解釋器進行試驗,即使你可能還沒有系統的研究過 Python。通過修改這些例子測試一下你是否理解它們,嘗試一下本章結尾處的練習。
首先,讓我們算出文本從頭到尾的長度,包括文本中出現的詞和標點符號。我們使用函數`len`獲取長度,請看在《創世紀》中使用的例子:
```py
>>> len(text3)
44764
>>>
```
《創世紀》有 44764 個詞和標點符號或者叫“詞符”。詞符 表示一個我們想要整體對待的字符序列 —— 例如`hairy`,`his` 或 `:)`。當我們計數文本如 to be or not to be 這個短語中詞符的個數時,我們計數這些序列出現的次數。因此,我們的例句中出現了 to 和 be 各兩次,or 和 not 各一次。然而在例句中只有 4 個不同的詞。《創世紀》中有多少不同的詞?要用 Python 來回答這個問題,我們處理問題的方法將稍有改變。一個文本詞匯表只是它用到的詞符的 _ 集合 _,因為在集合中所有重復的元素都只算一個。Python 中我們可以使用命令:`set(text3)` 獲得`text3` 的詞匯表。當你這樣做時,屏幕上的很多詞會掠過。現在嘗試以下操作:
```py
>>> sorted(set(text3)) ![[1]](https://img.kancloud.cn/97/aa/97aa34f1d446f0c464068d0711295a9a_15x15.jpg)
['!', "'", '(', ')', ',', ',)', '.', '.)', ':', ';', ';)', '?', '?)',
'A', 'Abel', 'Abelmizraim', 'Abidah', 'Abide', 'Abimael', 'Abimelech',
'Abr', 'Abrah', 'Abraham', 'Abram', 'Accad', 'Achbor', 'Adah', ...]
>>> len(set(text3)) ![[2]](https://img.kancloud.cn/c7/9c/c79c435fbd088cae010ca89430cd9f0c_15x15.jpg)
2789
>>>
```
用`sorted()` 包裹起 Python 表達式`set(text3)` [![[1]](https://img.kancloud.cn/97/aa/97aa34f1d446f0c464068d0711295a9a_15x15.jpg)](http://www.nltk.org/book/ch01.html#sorted-set),我們得到一個詞匯項的排序表,這個表以各種標點符號開始,然后是以 A 開頭的詞匯。大寫單詞排在小寫單詞前面。我們通過求集合中元素的個數間接獲得詞匯表的大小,再次使用`len`來獲得這個數值[![[2]](https://img.kancloud.cn/c7/9c/c79c435fbd088cae010ca89430cd9f0c_15x15.jpg)](http://www.nltk.org/book/ch01.html#len-set)。盡管小說中有 44,764 個詞符,但只有 2,789 個不同的單詞或“詞類型”。一個詞類型是指一個詞在一個文本中獨一無二的出現形式或拼寫 —— 也就是說,這個詞在詞匯表中是唯一的。我們計數的 2,789 個元素中包括標點符號,所以我們把這些叫做唯一元素類型而不是詞類型。
現在,讓我們對文本詞匯豐富度進行測量。下一個例子向我們展示,不同的單詞數目只是單詞總數的 6%,或者每個單詞平均被使用了 16 次(記住,如果你使用的是 Python 2,請在開始輸入`from __future__ import division`)。
```py
>>> len(set(text3)) / len(text3)
0.06230453042623537
>>>
```
接下來,讓我們專注于特定的詞。我們可以計數一個詞在文本中出現的次數,計算一個特定的詞在文本中占據的百分比:
```py
>>> text3.count("smote")
5
>>> 100 * text4.count('a') / len(text4)
1.4643016433938312
>>>
```
注
**輪到你來:** `text5` 中 lol 出現了多少次?它占文本全部詞數的百分比是多少?
你也許想要對幾個文本重復這些計算,但重新輸入公式是乏味的。你可以自己命名一個任務,如“lexical_diversity”或“percentage”,然后用一個代碼塊關聯它。現在,你只需輸入一個很短的名字就可以代替一行或多行 Python 代碼,而且你想用多少次就用多少次。執行一個任務的代碼段叫做一個函數,我們使用關鍵字`def` 給函數定義一個簡短的名字。下面的例子演示如何定義兩個新的函數,`lexical_diversity()` 和`percentage()`:
```py
>>> def lexical_diversity(text): ![[1]](https://img.kancloud.cn/97/aa/97aa34f1d446f0c464068d0711295a9a_15x15.jpg)
... return len(set(text)) / len(text) ![[2]](https://img.kancloud.cn/c7/9c/c79c435fbd088cae010ca89430cd9f0c_15x15.jpg)
...
>>> def percentage(count, total): ![[3]](https://img.kancloud.cn/69/fc/69fcb1188781ff9f726d82da7988a139_15x15.jpg)
... return 100 * count / total
...
```
小心!
當遇到第一行末尾的冒號后,Python 解釋器提示符由`>>>` 變為`...` 。`...`提示符表示 Python 期望在后面是一個縮進代碼塊 。縮進是輸入四個空格還是敲擊 Tab 鍵,這由你決定。要結束一個縮進代碼段,只需輸入一個空行。
`lexical_diversity()` [![[1]](https://img.kancloud.cn/97/aa/97aa34f1d446f0c464068d0711295a9a_15x15.jpg)](http://www.nltk.org/book/ch01.html#fun-parameter1)的定義中,我們指定了一個`text` 參數。這個參數是我們想要計算詞匯多樣性的實際文本的一個“占位符”,并在用到這個函數的時候出現在將要運行的代碼塊中 [![[2]](https://img.kancloud.cn/c7/9c/c79c435fbd088cae010ca89430cd9f0c_15x15.jpg)](http://www.nltk.org/book/ch01.html#locvar)。類似地,`percentage()` 定義了兩個參數,`count` 和`total` [![[3]](https://img.kancloud.cn/69/fc/69fcb1188781ff9f726d82da7988a139_15x15.jpg)](http://www.nltk.org/book/ch01.html#fun-parameter2)。
只要 Python 知道了`lexical_diversity()` 和`percentage()` 是指定代碼段的名字,我們就可以繼續使用這些函數:
```py
>>> lexical_diversity(text3)
0.06230453042623537
>>> lexical_diversity(text5)
0.13477005109975562
>>> percentage(4, 5)
80.0
>>> percentage(text4.count('a'), len(text4))
1.4643016433938312
>>>
```
扼要重述一下,我們使用或調用一個如`lexical_diversity()` 這樣的函數,只要輸入它的名字后面跟一個左括號,再輸入文本名字,然后是右括號。這些括號經常出現,它們的作用是分割任務名—— 如`lexical_diversity()`,與任務將要處理的數據 ——如`text3`。調用函數時放在參數位置的數據值叫做函數的實參。
在本章中你已經遇到了幾個函數,如`len()`, `set()` 和`sorted()`。通常我們會在函數名后面加一對空括號,像`len()`中的那樣,這只是為了表明這是一個函數而不是其他的 Python 表達式。函數是編程中的一個重要概念,我們在一開始提到它們,是為了讓新同學體會編程的強大和富有創造力。如果你現在覺得有點混亂,請不要擔心。
稍后我們將看到如何使用函數列表顯示數據,像表[1.1](http://www.nltk.org/book/ch01.html#tab-brown-types)顯示的那樣。表的每一行將包含不同數據的相同的計算,我們用函數來做這種重復性的工作。
表 1.1:
_Brown 語料庫 _ 中各種文體的詞匯多樣性
```py
>>> sent1 = ['Call', 'me', 'Ishmael', '.']
>>>
```
在提示符后面,我們輸入自己命名的`sent1`,后跟一個等號,然后是一些引用的詞匯,中間用逗號分割并用括號包圍。這個方括號內的東西在 Python 中叫做列表:它就是我們存儲文本的方式。我們可以通過輸入它的名字來查閱它[![[1]](https://img.kancloud.cn/97/aa/97aa34f1d446f0c464068d0711295a9a_15x15.jpg)](http://www.nltk.org/book/ch01.html#inspect-var)。我們可以查詢它的長度[![[2]](https://img.kancloud.cn/c7/9c/c79c435fbd088cae010ca89430cd9f0c_15x15.jpg)](http://www.nltk.org/book/ch01.html#len-sent)。我們甚至可以對它調用我們自己的函數`lexical_diversity()`[![[3]](https://img.kancloud.cn/69/fc/69fcb1188781ff9f726d82da7988a139_15x15.jpg)](http://www.nltk.org/book/ch01.html#apply-function)。
```py
>>> sent1 ![[1]](https://img.kancloud.cn/97/aa/97aa34f1d446f0c464068d0711295a9a_15x15.jpg)
['Call', 'me', 'Ishmael', '.']
>>> len(sent1) ![[2]](https://img.kancloud.cn/c7/9c/c79c435fbd088cae010ca89430cd9f0c_15x15.jpg)
4
>>> lexical_diversity(sent1) ![[3]](https://img.kancloud.cn/69/fc/69fcb1188781ff9f726d82da7988a139_15x15.jpg)
1.0
>>>
```
還定義了其它幾個列表,分別對應每個文本開始的句子,`sent2` … `sent9`。在這里我們檢查其中的兩個;你可以自己在 Python 解釋器中嘗試其余的(如果你得到一個錯誤說`sent2` 沒有定義,你需要先輸入`from nltk.book import *`)。
```py
>>> sent2
['The', 'family', 'of', 'Dashwood', 'had', 'long',
'been', 'settled', 'in', 'Sussex', '.']
>>> sent3
['In', 'the', 'beginning', 'God', 'created', 'the',
'heaven', 'and', 'the', 'earth', '.']
>>>
```
注意
**輪到你來:** 通過輸入名字、等號和一個單詞列表, 組建幾個你自己的句子,如`ex1 = ['Monty', 'Python', 'and', 'the', 'Holy', 'Grail']`。重復一些我們先前在第[1](http://www.nltk.org/book/ch01.html#sec-computing-with-language-texts-and-words) 節看到的其他 Python 操作,如:`sorted(ex1)`, `len(set(ex1))`, `ex1.count('the')`。
令人驚喜的是,我們可以對列表使用 Python 加法運算。兩個列表相加[![[1]](https://img.kancloud.cn/97/aa/97aa34f1d446f0c464068d0711295a9a_15x15.jpg)](http://www.nltk.org/book/ch01.html#list-plus-list)創造出一個新的列表,包括第一個列表的全部,后面跟著第二個列表的全部。
```py
>>> ['Monty', 'Python'] + ['and', 'the', 'Holy', 'Grail'] ![[1]](https://img.kancloud.cn/97/aa/97aa34f1d446f0c464068d0711295a9a_15x15.jpg)
['Monty', 'Python', 'and', 'the', 'Holy', 'Grail']
>>>
```
注意
這種加法的特殊用法叫做連接;它將多個列表組合為一個列表。我們可以把句子連接起來組成一個文本。
不必逐字的輸入列表,可以使用簡短的名字來引用預先定義好的列表。
```py
>>> sent4 + sent1
['Fellow', '-', 'Citizens', 'of', 'the', 'Senate', 'and', 'of', 'the',
'House', 'of', 'Representatives', ':', 'Call', 'me', 'Ishmael', '.']
>>>
```
如果我們想要向鏈表中增加一個元素該如何?這種操作叫做追加。當我們對一個列表使用`append()`時,列表自身會隨著操作而更新。
```py
>>> sent1.append("Some")
>>> sent1
['Call', 'me', 'Ishmael', '.', 'Some']
>>>
```
## 2.2 索引列表
正如我們已經看到的,Python 中的一個文本是一個單詞的列表,用括號和引號的組合來表示。就像處理一頁普通的文本,我們可以使用`len(text1)` 計算`text1`的詞數,使用`text1.count('heaven')`計算一個文本中出現的特定的詞,如`'heaven'`。
稍微花些耐心,我們可以挑選出打印出來的文本中的第 1 個、第 173 個或第 14278 個詞。類似的,我們也可以通過它在列表中出現的次序找出一個 Python 列表的元素。表示這個位置的數字叫做這個元素的索引。在文本名稱后面的方括號里寫下索引,Python 就會表示出文本中這個索引處如`173`的元素:
```py
>>> text4[173]
'awaken'
>>>
```
我們也可以反過來做;找出一個詞第一次出現的索引:
```py
>>> text4.index('awaken')
173
>>>
```
索引是一種常見的用來獲取文本中詞匯的方式,或者更一般的,訪問列表中的元素的方式。Python 也允許我們獲取子列表,從大文本中任意抽取語言片段,術語叫做切片。
```py
>>> text5[16715:16735]
['U86', 'thats', 'why', 'something', 'like', 'gamefly', 'is', 'so', 'good',
'because', 'you', 'can', 'actually', 'play', 'a', 'full', 'game', 'without',
'buying', 'it']
>>> text6[1600:1625]
['We', "'", 're', 'an', 'anarcho', '-', 'syndicalist', 'commune', '.', 'We',
'take', 'it', 'in', 'turns', 'to', 'act', 'as', 'a', 'sort', 'of', 'executive',
'officer', 'for', 'the', 'week']
>>>
```
索引有一些微妙,我們將在一個構造的句子的幫助下探討這些:
```py
>>> sent = ['word1', 'word2', 'word3', 'word4', 'word5',
... 'word6', 'word7', 'word8', 'word9', 'word10']
>>> sent[0]
'word1'
>>> sent[9]
'word10'
>>>
```
請注意,索引從零開始:`sent` 第 0 個元素寫作`sent[0]`,是第一個單詞`'word1'`,而`sent` 的第 9 個元素是`'word10'`。原因很簡單:Python 從計算機內存中的列表獲取內容的時候,它已經位于第一個元素;我們要告訴它向前多少個元素。因此,向前 0 個元素使它留在第一個元素上。
注意
這種從零算起的做法剛開始接觸會有些混亂,但這是現代編程語言普遍使用的。如果你已經掌握了 19XY 是 20 世紀中的一年這樣的計數世紀的系統,或者如果你生活在一個建筑物樓層編號從 1 開始的國家,你很開就會掌握它的竅門,步行 n-1 級樓梯到第 n 層。
現在,如果我們不小心使用的索引過大就會得到一個錯誤:
```py
>>> sent[10]
Traceback (most recent call last):
File "<stdin>", line 1, in ?
IndexError: list index out of range
>>>
```
這次不是一個語法錯誤,因為程序片段在語法上是正確的。相反,它是一個運行時錯誤,它會產生一個`回溯`消息顯示錯誤的上下文、錯誤的名稱:`IndexError` 以及簡要的解釋說明。
讓我們再次使用構造的句子仔細看看切片。這里我們發現切片`5:8` 包含`sent` 中索引為 5,6 和 7 的元素:
```py
>>> sent[5:8]
['word6', 'word7', 'word8']
>>> sent[5]
'word6'
>>> sent[6]
'word7'
>>> sent[7]
'word8'
>>>
```
按照慣例,`m:n` 表示元素 m…n-1。正如下一個例子顯示的那樣,如果切片從列表第一個元素開始,我們可以省略第一個數字[![[1]](https://img.kancloud.cn/97/aa/97aa34f1d446f0c464068d0711295a9a_15x15.jpg)](http://www.nltk.org/book/ch01.html#slice2), 如果切片到列表最后一個元素處結尾,我們可以省略第二個數字 [![[2]](https://img.kancloud.cn/c7/9c/c79c435fbd088cae010ca89430cd9f0c_15x15.jpg)](http://www.nltk.org/book/ch01.html#slice3):
```py
>>> sent[:3] ![[1]](https://img.kancloud.cn/97/aa/97aa34f1d446f0c464068d0711295a9a_15x15.jpg)
['word1', 'word2', 'word3']
>>> text2[141525:] ![[2]](https://img.kancloud.cn/c7/9c/c79c435fbd088cae010ca89430cd9f0c_15x15.jpg)
['among', 'the', 'merits', 'and', 'the', 'happiness', 'of', 'Elinor', 'and', 'Marianne',
',', 'let', 'it', 'not', 'be', 'ranked', 'as', 'the', 'least', 'considerable', ',',
'that', 'though', 'sisters', ',', 'and', 'living', 'almost', 'within', 'sight', 'of',
'each', 'other', ',', 'they', 'could', 'live', 'without', 'disagreement', 'between',
'themselves', ',', 'or', 'producing', 'coolness', 'between', 'their', 'husbands', '.',
'THE', 'END']
>>>
```
我們可以通過賦值給它的索引值來修改列表中的元素。在接下來的例子中,我們把`sent[0]` 放在等號左側[![[1]](https://img.kancloud.cn/97/aa/97aa34f1d446f0c464068d0711295a9a_15x15.jpg)](http://www.nltk.org/book/ch01.html#list-assignment)。我們也可以用新內容替換掉一整個片段[![[2]](https://img.kancloud.cn/c7/9c/c79c435fbd088cae010ca89430cd9f0c_15x15.jpg)](http://www.nltk.org/book/ch01.html#slice-assignment)。最后一個嘗試報錯的原因是這個鏈表只有四個元素而要獲取其后面的元素就產生了錯誤[![[3]](https://img.kancloud.cn/69/fc/69fcb1188781ff9f726d82da7988a139_15x15.jpg)](http://www.nltk.org/book/ch01.html#list-error)。
```py
>>> sent[0] = 'First' ![[1]](https://img.kancloud.cn/97/aa/97aa34f1d446f0c464068d0711295a9a_15x15.jpg)
>>> sent[9] = 'Last'
>>> len(sent)
10
>>> sent[1:9] = ['Second', 'Third'] ![[2]](https://img.kancloud.cn/c7/9c/c79c435fbd088cae010ca89430cd9f0c_15x15.jpg)
>>> sent
['First', 'Second', 'Third', 'Last']
>>> sent[9] ![[3]](https://img.kancloud.cn/69/fc/69fcb1188781ff9f726d82da7988a139_15x15.jpg)
Traceback (most recent call last):
File "<stdin>", line 1, in ?
IndexError: list index out of range
>>>
```
注意
**輪到你來:**花幾分鐘定義你自己的句子,使用前文中的方法修改個別詞和詞組(切片)。嘗試本章結尾關于列表的練習,檢驗你是否理解。
## 2.3 變量
從第[1](http://www.nltk.org/book/ch01.html#sec-computing-with-language-texts-and-words)節一開始,你已經訪問過名為`text1`, `text2` 等的文本。像這樣只輸入簡短的名字來引用一本 250,000 字的書節省了很多打字時間。一般情況下,我們可以對任何我們關心的計算命名。我們在前面的小節中已經這樣做了,例如定義一個變量變量 `sent1`,如下所示:
```py
>>> sent1 = ['Call', 'me', 'Ishmael', '.']
>>>
```
這樣的語句形式是:_ 變量 = 表達式 _。Python 將計算右邊的表達式把結果保存在變量中。這個過程叫做賦值。它并不產生任何輸出,你必須在新的一行輸入變量的名字來檢查它的內容。等號可能會有些誤解,因為信息是從右邊流到左邊的。你把它想象成一個左箭頭可能會有幫助。變量的名字可以是任何你喜歡的名字,如`my_sent`, `sentence`, `xyzzy`。變量必須以字母開頭,可以包含數字和下劃線。下面是變量和賦值的一些例子:
```py
>>> my_sent = ['Bravely', 'bold', 'Sir', 'Robin', ',', 'rode',
... 'forth', 'from', 'Camelot', '.']
>>> noun_phrase = my_sent[1:4]
>>> noun_phrase
['bold', 'Sir', 'Robin']
>>> wOrDs = sorted(noun_phrase)
>>> wOrDs
['Robin', 'Sir', 'bold']
>>>
```
請記住,排序表中大寫字母出現在小寫字母之前。
注意
請注意,在前面的例子中,我們將`my_sent` 的定義分成兩行。Python 表達式可以被分割成多行,只要它出現在任何一種括號內。Python 使用"`...`"提示符表示期望更多的輸入。在這些連續的行中有多少縮進都沒有關系,只是加入縮進通常會便于閱讀。
最好是選擇有意義的變量名,它能提醒你代碼的含義,也幫助別人讀懂你的 Python 代碼。Python 并不會理解這些名稱的意義;它只是盲目的服從你的指令,如果你輸入一些令人困惑的代碼,例如`one = 'two'` 或`two = 3`,它也不會反對。唯一的限制是變量名不能是 Python 的保留字,如`def`, `if`, `not`, 和`import`。如果你使用了保留字,Python 會產生一個語法錯誤:
```py
>>> not = 'Camelot'
File "<stdin>", line 1
not = 'Camelot'
^
SyntaxError: invalid syntax
>>>
```
我們將經常使用變量來保存計算的中間步驟,尤其是當這樣做使代碼更容易讀懂時。因此,`len(set(text1))` 也可以寫作:
```py
>>> vocab = set(text1)
>>> vocab_size = len(vocab)
>>> vocab_size
19317
>>>
```
小心!
為 Python 變量選擇名稱(標識符)時請注意。首先,應該以字母開始,后面跟數字(`0` 到`9`)或字母。因此,`abc23` 是好的,但是`23abc` 會導致一個語法錯誤。名稱是大小寫敏感的,這意味著`myVar` 和`myvar` 是不同的變量。變量名不能包含空格,但可以用下劃線把單詞分開,例如`my_var`。注意不要插入連字符來代替下劃線:`my-var` 不對,因為 Python 會把"`-`"解釋為減號。
## 2.4 字符串
我們用來訪問列表元素的一些方法也可以用在單獨的詞或字符串上。例如可以把一個字符串指定給一個變量[![[1]](https://img.kancloud.cn/97/aa/97aa34f1d446f0c464068d0711295a9a_15x15.jpg)](http://www.nltk.org/book/ch01.html#assign-string),索引一個字符串[![[2]](https://img.kancloud.cn/c7/9c/c79c435fbd088cae010ca89430cd9f0c_15x15.jpg)](http://www.nltk.org/book/ch01.html#index-string),切片一個字符串[![[3]](https://img.kancloud.cn/69/fc/69fcb1188781ff9f726d82da7988a139_15x15.jpg)](http://www.nltk.org/book/ch01.html#slice-string):
```py
>>> name = 'Monty' ![[1]](https://img.kancloud.cn/97/aa/97aa34f1d446f0c464068d0711295a9a_15x15.jpg)
>>> name[0] ![[2]](https://img.kancloud.cn/c7/9c/c79c435fbd088cae010ca89430cd9f0c_15x15.jpg)
'M'
>>> name[:4] ![[3]](https://img.kancloud.cn/69/fc/69fcb1188781ff9f726d82da7988a139_15x15.jpg)
'Mont'
>>>
```
我們還可以對字符串執行乘法和加法:
```py
>>> name * 2
'MontyMonty'
>>> name + '!'
'Monty!'
>>>
```
我們可以把列表中的單詞連接起來組成單個字符串,或者把字符串分割成一個列表,如下面所示:
```py
>>> ' '.join(['Monty', 'Python'])
'Monty Python'
>>> 'Monty Python'.split()
['Monty', 'Python']
>>>
```
我們將在第[3](http://www.nltk.org/book/ch03.html#chap-words)章回到字符串的主題。目前,我們已經有了兩個重要的基石——列表和字符串——已經準備好可以重新做一些語言分析了。
## 3 計算語言:簡單的統計
讓我們重新開始探索用我們的計算資源處理大量文本的方法。我們在第[1](http://www.nltk.org/book/ch01.html#sec-computing-with-language-texts-and-words)節已經開始討論了,在那里我們看到如何搜索詞及其上下文,如何匯編一個文本中的詞匯,如何產生一種文體的隨機文本等。
在本節中,我們重新拾起是什么讓一個文本不同與其他文本這樣的問題,并使用程序自動尋找特征詞匯和文字表達。正如在第[1](http://www.nltk.org/book/ch01.html#sec-computing-with-language-texts-and-words)節中那樣,你可以通過復制它們到 Python 解釋器中來嘗試 Python 語言的新特征,你將在下一節中系統的了解這些功能。
在這之前,你可能會想通過預測下面的代碼的輸出來檢查你對上一節的理解。你可以使用解釋器來檢查你是否正確。如果你不確定如何做這個任務,你最好在繼續之前復習一下上一節的內容。
```py
>>> saying = ['After', 'all', 'is', 'said', 'and', 'done',
... 'more', 'is', 'said', 'than', 'done']
>>> tokens = set(saying)
>>> tokens = sorted(tokens)
>>> tokens[-2:]
what output do you expect here?
>>>
```
## 3.1 頻率分布
我們如何能自動識別文本中最能體現文本的主題和風格的詞匯?試想一下,要找到一本書中使用最頻繁的 50 個詞你會怎么做?一種方法是為每個詞項設置一個計數器,如圖[3.1](http://www.nltk.org/book/ch01.html#fig-tally)顯示的那樣。計數器可能需要幾千行,這將是一個極其繁瑣的過程——如此繁瑣以至于我們寧愿把任務交給機器來做。

圖 3.1:計數一個文本中出現的詞(頻率分布)
圖[3.1](http://www.nltk.org/book/ch01.html#fig-tally) 中的表被稱為頻率分布,它告訴我們在文本中的每一個詞項的頻率。(一般情況下,它能計數任何觀察得到的事件。)這是一個“分布”因為它告訴我們文本中單詞詞符的總數是如何分布在詞項中的。因為我們經常需要在語言處理中使用頻率分布,NLTK 中內置了它們。讓我們使用`FreqDist` 尋找 _《白鯨記》_ 中最常見的 50 個詞:
```py
>>> fdist1 = FreqDist(text1) ![[1]](https://img.kancloud.cn/97/aa/97aa34f1d446f0c464068d0711295a9a_15x15.jpg)
>>> print(fdist1) ![[2]](https://img.kancloud.cn/c7/9c/c79c435fbd088cae010ca89430cd9f0c_15x15.jpg)
<FreqDist with 19317 samples and 260819 outcomes>
>>> fdist1.most_common(50) ![[3]](https://img.kancloud.cn/69/fc/69fcb1188781ff9f726d82da7988a139_15x15.jpg)
[(',', 18713), ('the', 13721), ('.', 6862), ('of', 6536), ('and', 6024),
('a', 4569), ('to', 4542), (';', 4072), ('in', 3916), ('that', 2982),
("'", 2684), ('-', 2552), ('his', 2459), ('it', 2209), ('I', 2124),
('s', 1739), ('is', 1695), ('he', 1661), ('with', 1659), ('was', 1632),
('as', 1620), ('"', 1478), ('all', 1462), ('for', 1414), ('this', 1280),
('!', 1269), ('at', 1231), ('by', 1137), ('but', 1113), ('not', 1103),
('--', 1070), ('him', 1058), ('from', 1052), ('be', 1030), ('on', 1005),
('so', 918), ('whale', 906), ('one', 889), ('you', 841), ('had', 767),
('have', 760), ('there', 715), ('But', 705), ('or', 697), ('were', 680),
('now', 646), ('which', 640), ('?', 637), ('me', 627), ('like', 624)]
>>> fdist1['whale']
906
>>>
```
第一次調用`FreqDist`時,傳遞文本的名稱作為參數[![[1]](https://img.kancloud.cn/97/aa/97aa34f1d446f0c464068d0711295a9a_15x15.jpg)](http://www.nltk.org/book/ch01.html#freq-dist-call)。我們可以看到已經被計算出來的 _《白鯨記》_ 中的總的詞數(“outcomes”)—— 260,819[![[2]](https://img.kancloud.cn/c7/9c/c79c435fbd088cae010ca89430cd9f0c_15x15.jpg)](http://www.nltk.org/book/ch01.html#freq-dist-inspect)。表達式`most_common(50)` 給出文本中 50 個出現頻率最高的單詞類型[![[3]](https://img.kancloud.cn/69/fc/69fcb1188781ff9f726d82da7988a139_15x15.jpg)](http://www.nltk.org/book/ch01.html#freq-dist-most-common)。
注意
**輪到你來:**使用`text2`嘗試前面的頻率分布的例子。注意正確使用括號和大寫字母。如果你得到一個錯誤消息`NameError: name 'FreqDist' is not defined`,你需要在一開始輸入`from nltk.book import *`
上一個例子中是否有什么詞有助于我們把握這個文本的主題或風格呢?只有一個詞,whale,稍微有些信息量!它出現了超過 900 次。其余的詞沒有告訴我們關于文本的信息;它們只是“管道”英語。這些詞在文本中占多少比例?我們可以產生一個這些詞匯的累積頻率圖,使用`fdist1.plot(50, cumulative=True)` 來生成[3.2](http://www.nltk.org/book/ch01.html#fig-fdist-moby) 中的圖。這 50 個詞占了書的將近一半!

圖 3.2: _《白鯨記》_ 中 50 個最常用詞的累積頻率圖:這些詞占了所有詞符的將近一半。
如果高頻詞對我們沒有幫助,那些只出現了一次的詞(所謂的 hapaxes)又如何呢?輸入`fdist1.hapaxes()` 來查看它們。這個列表包含 lexicographer, cetological, contraband, expostulations 以及其他 9,000 多個。看來低頻詞太多了,沒看到上下文我們很可能有一半的 hapaxes 猜不出它們的意義!既然高頻詞和低頻詞都沒有幫助,我們需要嘗試其他的辦法。
## 3.2 細粒度的選擇詞
接下來,讓我們看看文本中的 _ 長 _ 詞,也許它們有更多的特征和信息量。為此我們采用集合論的一些符號。我們想要找出文本詞匯表長度中超過 15 個字符的詞。我們定義這個性質為 P,則 P(w) 為真當且僅當詞 w 的長度大余 15 個字符。現在我們可以用[(1a)](http://www.nltk.org/book/ch01.html#ex-set-comprehension-math) 中的數學集合符號表示我們感興趣的詞匯。它的含義是:此集合中所有 w 都滿足 w 是集合 V V(詞匯表)的一個元素且 w 有性質 P。
```py
>>> 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']
>>>
```
對于詞匯表`V` 中的每一個詞`w`,我們檢查`len(w)` 是否大于 15;所有其他詞匯將被忽略。我們將在后面更仔細的討論這里的語法。
注意
**輪到你來:** 在 Python 解釋器中嘗試上面的表達式,改變文本和長度條件做一些實驗。如果改變變量名,你的結果會產生什么變化嗎,如使用`[word for word in vocab if ...]`?
讓我們回到尋找文本特征詞匯的任務上來。請注意,`text4` 中的長詞反映國家主題 — constitutionally, transcontinental — 而`text5` 中的長詞反映的不是真正的內容 boooooooooooglyyyyyy 和 yuuuuuuuuuuuummmmmmmmmmmm。我們是否已經成功的自動提取出文本的特征詞匯呢?好的,這些很長的詞通常是 hapaxes(即唯一的),也許找出 _ 頻繁出現的 _ 長詞會更好。這樣看起來更有前途,因為這樣忽略了短高頻詞(如 the)和長低頻詞(如 antiphilosophists)。以下是聊天語料庫中所有長度超過 7 個字符,且出現次數超過 7 次的詞:
```py
>>> 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']
>>>
```
注意我們是如何使用兩個條件:`len(w) > 7` 保證詞長都超過七個字母,`fdist5[w] > 7` 保證這些詞出現超過 7 次。最后,我們已成功地自動識別出與文本內容相關的高頻詞。這很小的一步卻是一個重要的里程碑:一小塊代碼,處理數以萬計的詞,產生一些有信息量的輸出。
## 3.3 詞語搭配和雙連詞
一個搭配是異乎尋常地經常在一起出現的詞序列。red wine 是一個搭配,而 the wine 不是。搭配的一個特點是其中的詞不能被類似的詞置換。例如:maroon wine(粟色酒)聽起來就很奇怪。
要獲取搭配,我們先從提取文本詞匯中的詞對,也就是雙連詞開始。使用函數`bigrams()`很容易實現:
```py
>>> list(bigrams(['more', 'is', 'said', 'than', 'done']))
[('more', 'is'), ('is', 'said'), ('said', 'than'), ('than', 'done')]
>>>
```
注意
如果上面省掉`list()`,只輸入`bigrams(['more', ...])`,你將看到`<generator object bigrams at 0x10fb8b3a8>` 的輸出形式。這是 Python 的方式表示它已經準備好要計算一個序列,在這里是雙連詞。現在,你只需要知道告訴 Python 使用`list()`將它轉換成一個列表。
在這里我們看到詞對 than-done 是一個雙連詞,在 Python 中寫成`('than', 'done')`。現在,搭配基本上就是頻繁的雙連詞,除非我們更加注重包含不常見詞的的情況。特別的,我們希望找到比我們基于單個詞的頻率預期得到的更頻繁出現的雙連詞。`collocations()` 函數為我們做這些。我們將在以后看到它是如何工作。
```py
>>> text4.collocations()
United States; fellow citizens; four years; years ago; Federal
Government; General Government; American people; Vice President; Old
World; Almighty God; Fellow citizens; Chief Magistrate; Chief Justice;
God bless; every citizen; Indian tribes; public debt; one another;
foreign nations; political parties
>>> text8.collocations()
would like; medium build; social drinker; quiet nights; non smoker;
long term; age open; Would like; easy going; financially secure; fun
times; similar interests; Age open; weekends away; poss rship; well
presented; never married; single mum; permanent relationship; slim
build
>>>
```
文本中出現的搭配很能體現文本的風格。為了找到 red wine 這個搭配,我們將需要處理更大的文本。
## 3.4 計數其他東西
計數詞匯是有用的,我們也可以計數其他東西。例如,我們可以查看文本中詞長的分布,通過創造一長串數字的列表的`FreqDist`,其中每個數字是文本中對應詞的長度:
```py
>>> [len(w) for w in text1] ![[1]](https://img.kancloud.cn/97/aa/97aa34f1d446f0c464068d0711295a9a_15x15.jpg)
[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) ![[2]](https://img.kancloud.cn/c7/9c/c79c435fbd088cae010ca89430cd9f0c_15x15.jpg)
>>> print(fdist) ![[3]](https://img.kancloud.cn/69/fc/69fcb1188781ff9f726d82da7988a139_15x15.jpg)
<FreqDist with 19 samples and 260819 outcomes>
>>> fdist
FreqDist({3: 50223, 1: 47933, 4: 42345, 2: 38513, 5: 26597, 6: 17111, 7: 14399,
8: 9966, 9: 6428, 10: 3528, ...})
>>>
```
我們以導出`text1` 中每個詞的長度的列表開始[![[1]](https://img.kancloud.cn/97/aa/97aa34f1d446f0c464068d0711295a9a_15x15.jpg)](http://www.nltk.org/book/ch01.html#word-lengths),然后`FreqDist` 計數列表中每個數字出現的次數[![[2]](https://img.kancloud.cn/c7/9c/c79c435fbd088cae010ca89430cd9f0c_15x15.jpg)](http://www.nltk.org/book/ch01.html#freq-word-lengths)。結果[![[3]](https://img.kancloud.cn/69/fc/69fcb1188781ff9f726d82da7988a139_15x15.jpg)](http://www.nltk.org/book/ch01.html#freq-word-lengths-size) 是一個包含 25 萬左右個元素的分布,每一個元素是一個數字,對應文本中一個詞標識符。但是只有 20 個不同的元素,從 1 到 20,因為只有 20 個不同的詞長。也就是說,有由 1 個字符,2 個字符,...,20 個字符組成的詞,而沒有由 21 個或更多字符組成的詞。有人可能會問不同長度的詞的頻率是多少?(例如,文本中有多少長度為 4 的詞?長度為 5 的詞是否比長度為 4 的詞多?等等)。下面我們回答這個問題:
```py
>>> fdist.most_common()
[(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
>>>
```
由此我們看到,最頻繁的詞長度是 3,長度為 3 的詞有 50,000 多個(約占書中全部詞匯的 20%)。雖然我們不會在這里追究它,關于詞長的進一步分析可能幫助我們了解作者、文體或語言之間的差異。
[3.1](http://www.nltk.org/book/ch01.html#tab-freqdist) 總結了 NLTK 頻率分布類中定義的函數。
表 3.1:
NLTK 頻率分布類中定義的函數
```py
>>> sent7
['Pierre', 'Vinken', ',', '61', 'years', 'old', ',', 'will', 'join', 'the',
'board', 'as', 'a', 'nonexecutive', 'director', 'Nov.', '29', '.']
>>> [w for w in sent7 if len(w) < 4]
[',', '61', 'old', ',', 'the', 'as', 'a', '29', '.']
>>> [w for w in sent7 if len(w) <= 4]
[',', '61', 'old', ',', 'will', 'join', 'the', 'as', 'a', 'Nov.', '29', '.']
>>> [w for w in sent7 if len(w) == 4]
['will', 'join', 'Nov.']
>>> [w for w in sent7 if len(w) != 4]
['Pierre', 'Vinken', ',', '61', 'years', 'old', ',', 'the', 'board',
'as', 'a', 'nonexecutive', 'director', '29', '.']
>>>
```
所有這些例子都有一個共同的模式:`[w for w in text if` _condition_ `]`,其中 _condition_ 是 Python 中的一個“測試”,得到真或者假。在前面的代碼例子所示的情況中,條件始終是數值比較。然而,我們也可以使用表[4.2](http://www.nltk.org/book/ch01.html#tab-word-tests) 中列出的函數測試詞匯的各種屬性。
表 4.2:
一些詞比較運算符
```py
>>> sorted(w for w in set(text1) if w.endswith('ableness'))
['comfortableness', 'honourableness', 'immutableness', 'indispensableness', ...]
>>> sorted(term for term in set(text4) if 'gnt' in term)
['Sovereignty', 'sovereignties', 'sovereignty']
>>> sorted(item for item in set(text6) if item.istitle())
['A', 'Aaaaaaaaah', 'Aaaaaaaah', 'Aaaaaah', 'Aaaah', 'Aaaaugh', 'Aaagh', ...]
>>> sorted(item for item in set(sent7) if item.isdigit())
['29', '61']
>>>
```
我們還可以創建更復雜的條件。如果 c 是一個條件,那么`not` c 也是一個條件。如果我們有兩個條件 c<sub>1</sub> 和 c<sub>2</sub>,那么我們可以使用合取和析取將它們合并形成一個新的條件:c<sub>1</sub> `and` c<sub>2</sub>, c<sub>1</sub> `or` c<sub>2</sub>。
注意
**輪到你來:** 運行下面的例子,嘗試解釋每一條指令中所發生的事情。然后,試著自己組合一些條件。
```py
>>> sorted(w for w in set(text7) if '-' in w and 'index' in w)
>>> sorted(wd for wd in set(text3) if wd.istitle() and len(wd) > 10)
>>> sorted(w for w in set(sent7) if not w.islower())
>>> sorted(t for t in set(text2) if 'cie' in t or 'cei' in t)
```
## 4.2 對每個元素進行操作
在[3](http://www.nltk.org/book/ch01.html#sec-computing-with-language-simple-statistics)節中,我們看到計數詞匯以外的其他項目的一些例子。讓我們仔細看看我們所使用的符號:
```py
>>> [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, ...]
>>> [w.upper() for w in text1]
['[', 'MOBY', 'DICK', 'BY', 'HERMAN', 'MELVILLE', '1851', ']', 'ETYMOLOGY', '.', ...]
>>>
```
這些表達式形式為`[f(w) for ...]` 或`[w.f() for ...]`,其中`f` 是一個函數,用來計算詞長,或把字母轉換為大寫。現階段你還不需要理解兩種表示方法:`f(w)` 和`w.f()`。而只需學習對列表上的所有元素執行相同的操作的這種 Python 習慣用法。在前面的例子中,遍歷`text1`中的每一個詞,一個接一個的賦值給變量`w` 并在變量上執行指定的操作。
注意
上面描述的表示法被稱為“列表推導”。這是我們的第一個 Python 習慣用法的例子,一中固定的表示法,我們習慣使用的方法,省去了每次分析的煩惱。掌握這些習慣用法是成為一流 Python 程序員的一個重要組成部分。
讓我們回到計數詞匯的問題,這里使用相同的習慣用法:
```py
>>> len(text1)
260819
>>> len(set(text1))
19317
>>> len(set(word.lower() for word in text1))
17231
>>>
```
由于我們不重復計算像 This 和 this 這樣僅僅大小寫不同的詞,就已經從詞匯表計數中抹去了 2,000 個!還可以更進一步,通過過濾掉所有非字母元素,從詞匯表中消除數字和標點符號:
```py
>>> len(set(word.lower() for word in text1 if word.isalpha()))
16948
>>>
```
這個例子稍微有些復雜:將所有純字母組成的詞小寫。也許只計數小寫的詞會更簡單一些,但這卻是一個錯誤的答案(為什么?)。
如果你對列表推導不那么充滿信心,請不要擔心,因為在下面的章節中你會看到更多的例子及解釋。
## 4.3 嵌套代碼塊
大多數編程語言允許我們在條件表達式或者`if`語句條件滿足時執行代碼塊。我們在`[w for w in sent7 if len(w) < 4]` 這樣的代碼中已經看到條件測試的例子。在下面的程序中,我們創建一個叫`word`的變量包含字符串值`'cat'`。`if` 語句中檢查`len(word) < 5` 是否為真。它確實為真,所以`if` 語句的代碼塊被調用,`print` 語句被執行,向用戶顯示一條消息。別忘了要縮進,在`print`語句前輸入四個空格。
```py
>>> word = 'cat'
>>> if len(word) < 5:
... print('word length is less than 5')
... ![[1]](https://img.kancloud.cn/97/aa/97aa34f1d446f0c464068d0711295a9a_15x15.jpg)
word length is less than 5
>>>
```
使用 Python 解釋器時,我們必須添加一個額外的空白行[![[1]](https://img.kancloud.cn/97/aa/97aa34f1d446f0c464068d0711295a9a_15x15.jpg)](http://www.nltk.org/book/ch01.html#blank-line),這樣它才能檢測到嵌套塊結束。
注意
如果你正在使用 Python 2.6 或 2.7,為了識別上面的`print`函數,需要包括以下行︰
```py
>>> from __future__ import print_function
```
如果我們改變測試條件為`len(word) >= 5`來檢查`word`的長度是否大于等于`5`,那么測試將不再為真。此時,`if`語句后面的代碼段將不會被執行,沒有消息顯示給用戶:
```py
>>> if len(word) >= 5:
... print('word length is greater than or equal to 5')
...
>>>
```
`if`語句被稱為一種控制結構,因為它控制縮進塊中的代碼將是否運行。另一種控制結構是`for`循環。嘗試下面的代碼,請記住包含冒號和四個空格:
```py
>>> for word in ['Call', 'me', 'Ishmael', '.']:
... print(word)
...
Call
me
Ishmael
.
>>>
```
這叫做循環,因為 Python 以循環的方式執行里面的代碼。它以`word = 'Call'`賦值開始,使用變量`word` 命名列表的第一個元素。然后,顯示`word`的值給用戶。接下來它回到`for`語句,執行`word = 'me'`賦值,然后顯示這個新值給用戶,以此類推。它以這種方式不斷運行,直到列表中所有項都被處理完。
## 4.4 條件循環
現在,我們可以將`if`語句和`for`語句結合。循環鏈表中每一項,只輸出結尾字母是 _l_ 的詞。我們將為變量挑選另一個名字以表明 Python 并不在意變量名的意義。
```py
>>> sent1 = ['Call', 'me', 'Ishmael', '.']
>>> for xyzzy in sent1:
... if xyzzy.endswith('l'):
... print(xyzzy)
...
Call
Ishmael
>>>
```
你會發現在`if` 和`for`語句所在行末尾——縮進開始之前——有一個冒號。事實上,所有的 Python 控制結構都以冒號結尾。冒號表示當前語句與后面的縮進塊有關聯。
我們也可以指定當`if`語句的條件不滿足時采取的行動。在這里,我們看到`elif`(else if)語句和`else`語句。請注意,這些在縮進代碼前也有冒號。
```py
>>> for token in sent1:
... if token.islower():
... print(token, 'is a lowercase word')
... elif token.istitle():
... print(token, 'is a titlecase word')
... else:
... print(token, 'is punctuation')
...
Call is a titlecase word
me is a lowercase word
Ishmael is a titlecase word
. is punctuation
>>>
```
正如你看到的,即便只有這么一點兒 Python 知識,你就已經可以開始構建多行的 Python 程序。分塊開發程序,在整合它們之前測試每一塊代碼是否達到你的預期是很重要的。這也是 Python 交互式解釋器的價值所在,也是為什么你必須適應它的原因。
最后,讓我們把一直在探索的習慣用法組合起來。首先,我們創建一個包含 cie 或 cei 的詞的列表,然后循環輸出其中的每一項。請注意 print 語句中給出的額外信息︰<cite>end=' '</cite>。它告訴 Python 在每個單詞后面打印一個空格(而不是默認的換行)。
```py
>>> tricky = sorted(w for w in set(text2) if 'cie' in w or 'cei' in w)
>>> for word in tricky:
... print(word, end=' ')
ancient ceiling conceit conceited conceive conscience
conscientious conscientiously deceitful deceive ...
>>>
```
## 5 自動理解自然語言
我們一直在各種文本和 Python 編程語言的幫助自下而上的探索語言。然而,我們也對通過構建有用的語言技術,開拓我們的語言和計算知識感興趣。現在,我們將借此機會從代碼的細節中退出來,描繪一下自然語言處理的全景圖。
純粹應用層面,我們大家都需要幫助才能找到隱含在網絡上的文本中的浩瀚的信息。搜索引擎在網絡的發展和普及中發揮了關鍵作用,但也有一些缺點。它需要技能、知識和一點運氣才能找到這樣一些問題的答案:我用有限的預算能參觀費城和匹茲堡的哪些景點?專家們怎么評論數碼單反相機?過去的一周里可信的評論員都對鋼材市場做了哪些預測?讓計算機來自動回答這些問題,涉及包括信息提取、推理與總結在內的廣泛的語言處理任務,將需要在一個更大規模更穩健的層面實施,這超出了我們當前的能力。
哲學層面,構建智能機器是人工智能長久以來的挑戰,語言理解是智能行為的重要組成部分。這一目標多年來一直被看作是太困難了。然而,隨著 NLP 技術日趨成熟,分析非結構化文本的方法越來越健壯,應用越來越廣泛,對自然語言理解的期望變成一個合理的目標再次浮現。
在本節中,我們將描述一些語言理解技術,給你一種有趣的挑戰正在等著你的感覺。
## 5.1 詞意消歧
在詞意消歧中,我們要算出特定上下文中的詞被賦予的是哪個意思。思考存在歧義的詞 serve 和 dish:
```py
>>> sorted(set(w.lower() for w in text1))
>>> sorted(w.lower() for w in set(text1))
```
* ? 下面兩個測試的差異是什么:`w.isupper()` 和`not w.islower()`?
* ? 寫一個切片表達式提取`text2`中最后兩個詞。
* ? 找出聊天語料庫(`text5`)中所有四個字母的詞。使用頻率分布函數(`FreqDist`),以頻率從高到低顯示這些詞。
* ? 復習第[4](http://www.nltk.org/book/ch01.html#sec-making-decisions)節中條件循環的討論。使用`for`和`if`語句組合循環遍歷 _《巨蟒和圣杯》_(`text6`)的電影劇本中的詞,`print`所有的大寫詞,每行輸出一個。
* ? 寫表達式找出`text6`中所有符合下列條件的詞。結果應該是單詞列表的形式:`['word1', 'word2', ...]`。
1. 以 ize 結尾
2. 包含字母 z
3. 包含字母序列 pt
4. 除了首字母外是全部小寫字母的詞(即`titlecase`)* ? 定義`sent`為一個單詞列表:`['she', 'sells', 'sea', 'shells', 'by', 'the', 'sea', 'shore']`。編寫代碼執行以下任務:
1. 輸出所有 sh 開頭的單詞
2. 輸出所有長度超過 4 個字符的詞* ? 下面的 Python 代碼是做什么的?`sum(len(w) for w in text1)` 你可以用它來算出一個文本的平均字長嗎?
* ? 定義一個名為`vocab_size(text)`的函數,以文本作為唯一的參數,返回文本的詞匯量。
* ? 定義一個函數`percent(word, text)`,計算一個給定的詞在文本中出現的頻率,結果以百分比表示。
* ? 我們一直在使用集合存儲詞匯表。試試下面的 Python 表達式:`set(sent3) < set(text1)`。實驗在`set()`中使用不同的參數。它是做什么用的?你能想到一個實際的應用嗎?
關于本文檔...
針對 NLTK 3.0 進行更新。本章來自于 _Natural Language Processing with Python_,[Steven Bird](http://estive.net/), [Ewan Klein](http://homepages.inf.ed.ac.uk/ewan/) 和[Edward Loper](http://ed.loper.org/),Copyright ? 2014 作者所有。本章依據 _Creative Commons Attribution-Noncommercial-No Derivative Works 3.0 United States License_ [[http://creativecommons.org/licenses/by-nc-nd/3.0/us/](http://creativecommons.org/licenses/by-nc-nd/3.0/us/)] 條款,與 _ 自然語言工具包 _ [`http://nltk.org/`] 3.0 版一起發行。
本文檔構建于星期三 2015 年 7 月 1 日 12:30:05 AEST
## Docutils System Messages
System Message: ERROR/3 (`ch01.rst2`, line 1889); _[backlink](http://www.nltk.org/book/ch01.html#id9)_
Unknown target name: "finegan2007".