2010年5月4日 星期二

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

1.2 再離Python近一點吧:文本只是一個文字組成的列表

從上一節的洗禮你應該已經越來越瞭解Python這個程式語言了,不過這部份我們要介紹更多重要的元素!它將會在未來實際執行自然語言處理時扮演關鍵角色。

Lists(列表或串列)

文本到底是什麼?某些程度上,它只是一些字詞與符號排列而成的一頁頁序列;某星程度上,整個文本只是由每個章節組成的序列,每個章節又只是由一些段落所組成的序列...。總之,要用電腦來搞定文本的首要概念就是把它當成:一個由文字與符號組成的玩意兒!一開始我們來先利用文本1(Moby Dick)的開頭來進行測試吧:


>>> sent1 = ['Call', 'me', 'Ishmael', '.']
>>>


我們仔細看一下這個例子,首先取了一個名字(sent1)之後,接著在等號後面出現了一個中括號,這個中括號告訴我們sent1的資料結構是一個串列(list),這個串列中有四個字串,每個字串用單引號包覆著,並用逗號隔開。我們可以利用sent1來測試一些我們學過的東西,像是敲名字可以在顯示一次內容、計算長度以及運用之前建立的函數來計算語意多樣性:

>>> sent1
['Call', 'me', 'Ishmael', '.']
>>> len(sent1)
4
>>> lexical_diversity(sent1)
1.0
>>>


事實上,NLTK已經為你準備了一系列的sent(從sent1, sent2...sent9),你只需要敲入名稱就可以隨時察看並拿來測試(如果Python直譯器告訴你變數尚未定義的話,請記得再把NLTK導入一次:from nltk.book import *)。

>>> sent2
['The', 'family', 'of', 'Dashwood', 'had', 'long', 'been', 'settled', 'in', 'Sussex', '.']
>>> sent3 [
'In', 'the', 'beginning', 'God', 'created', 'the',
'heaven', 'and', 'the', 'earth', '.']
>>>


利用Python加法,你可以發現串列是非常有趣的!他將會把兩個串列連接起來,稱為串接

>>> ['Monty', 'Python'] + ['and', 'the', 'Holy', 'Grail']
['Monty', 'Python', 'and', 'the', 'Holy', 'Grail']


偷懶一點,只需要打入名稱(假如名稱都是串列)也可以直接連起來:

>>> sent4 + sent1

['Fellow', '-', 'Citizens', 'of', 'the', 'Senate', 'and', 'of', 'the',

'House', 'of', 'Representatives', ':', 'Call', 'me', 'Ishmael', '.']

>>>

那如果要把新東西加入在串列該怎麼做?就使用append()指令吧,把意欲新增的項目放在括弧中當成參數,就可以把它新增到串列當中囉!

>>> sent1.append("Some")

>>> sent1

['Call', 'me', 'Ishmael', '.', 'Some']

>>>

善用串列的索引

如同我們前面看到的,一個文本也不過是裝在一些中括號的引號文字集合而已。所以我們可以輕易查詢一個文本的長度,如len(text1),或是計算某個字的出現次數,如

text1.count('heaven')。


稍微靜下來思考看看,如果我們已經可以確切掌握文本中每個依照順序出現的文字(也可能是符號),那是不是可以輕易地去抓出該文本的第1個字、第173個字或甚至第14278個字?這是當然的!Python在建立串列時就已經把索引值給記錄起來了,我們試著把文本中的第173個字抓出來試試看,只需要把索引值裝在中括號即可:


>>> text4[173]

'awaken'

>>>

也可以反過來,假定想知道某個字出現在文本的位置,只要使用index()指令即可:

>>> text4.index('awaken')

173

>>>

索引來存取文本中的文字是超級實用的方法,因為他可以掌握整個串列中的每一個元素。此外,Python還允許我們提取子串列的方式,叫做切片(slicing),它可以擷取任意大小串列中的某一段特定的內容:

>>> 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']

>>>

索引的原理有許多微妙之處可以慢慢談,我們建立一個串列來解釋一番:

>>> sent = ['word1', 'word2', 'word3', 'word4', 'word5',

...         'word6', 'word7', 'word8', 'word9', 'word10']

>>> sent[0]

'word1'

>>> sent[9]

'word10'

>>>

從上面的例子可以清楚瞭解到,索引的起始是「0」,sent[0]才是表示「word1」,而最後的元素word10則是以「sent[9]」表示。這裡由很單純,因為當Python在從電腦記憶體中存取串列的內容時,從第一個元素中就已經告訴電腦,接下來還有多少元素(多少步)要走,因此第0步(不用走)就表示第1個元素。如果你不小心或故意輸入超出範圍的索引直就會像下面這樣,出現錯誤訊息!這一次不是語法的錯誤(語法正確),而是執行時的錯誤,就如同Traceback的訊息所回報,是索引的錯誤:超出串列範圍。

>>> sent[10]

Traceback (most recent call last):

  File "<stdin>", line 1, in ?

IndexError: list index out of range

>>>

現在!繼續測試切片的功能,像是現在來測試切片5:8,則表示提取出索引值5、6、7的內容:

>>> sent[5:8]

['word6', 'word7', 'word8']

>>> sent[5]

'word6'

>>> sent[6]

'word7'

>>> sent[7]

'word8'

>>>

一般來講,「切片m:n」可以直接解讀成「提取元素m...到n-1」!下面的例子中,我們試著刪掉起始的切片值(表示從頭開始),或是刪掉結束的切片值(表示到最後)。

>>> sent[:3]
['word1', 'word2', 'word3']
>>> text2[141525:]
['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']
>>>

我們也可以透過索引來修改串列的元素,同時也可以用新的資料來針對切片進行替換。像是在下面的例子裡,把原本10的元素換成了4個,再輸入原本的索引值時反而出錯了!

>>> sent[0] = 'First'
>>> sent[9] = 'Last'
>>> len(sent) 10
>>> sent[1:9] = ['Second', 'Third']
>>> sent ['First', 'Second', 'Third', 'Last']
>>> sent[9]
Traceback (most recent call last): File "<stdin>", line 1, in ? IndexError: list index out of range
>>>


變數

從1.1開始,我們就使用了各種代號text1、text2來存取文本,當然也包括了剛剛用的一系列sent也是:

>>> sent1 = ['Call', 'me', 'Ishmael', '.']

>>>

上面這一行顯示了:「變數=表達式」的意涵,Python會執行表達式並把結果存給變數,而這個動作就叫做宣告(assignment)。執行後Python直譯器不會產生任何輸出,想要檢視其內容則必須輸入變數名稱後才行。變數名稱可以隨便你取(原則上),主要的限制就是開頭必須是字母,剩下你要加數字、底線都可以。以下的範例可以提供參考:

>>> 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']

>>>

記得喔!排序的時候,大寫字母是會比小寫優先的!

通常還是取有意義的變數名稱會比較好啦!可以用來提醒自己,尤其是程式碼可能會給別人閱讀。當然!Python是無視變數名稱的,它只會根據你的指示辦事而已。唯一的限制就是你取的名稱不能與Python的保留字組有衝突,例如def、if、not或是import等等。如果你用了保留字,就會產生語法錯誤:


>>> not = 'Camelot'
File "<stdin>", line 1
    not = 'Camelot'
        ^
SyntaxError: invalid syntax
>>>

變數也經常被拿來當作運算時的中介角色,像是我們之前使用過的「len(set(text1))」就可以被分解成:

>>> vocab = set(text1)
>>> vocab_size = len(vocab)
>>> vocab_size
19317
>>>

最後在叮嚀你一次!一定要小心選擇你在Python中使用的這些名稱,開頭必需要是字母,接著可以選擇性地加入數字,如abc123是可以的,但是123abc則是語法錯誤的!名稱本身也是相當敏感的,myVar跟myvar兩個是不同的東西(即使只差一個字母的大小寫而已)。使用底線是沒有問題的,如my_var,但是使用連字號就是錯誤的,如my-var,因為連字號是「減號」的意思!

字串

有些我們之前拿來處理串列的方法也可以拿到字串這邊來用,像是宣告一個字串變數,或是在字串中使用索引或切片來指定內容:

>>> name = 'Monty'
>>> name[0]
'M'
>>> name[:4]  
'Mont'
>>>


加法與乘法也可以拿來處理字串:

>>> name * 2
'MontyMonty'
>>> name + '!'
'Monty!'
>>>


另外更可以把串列中的元素連結起來變成字串,也可以反過來把字串拆解成串列喔!

>>> ' '.join(['Monty', 'Python'])
'Monty Python'
>>> 'Monty Python'.split()
['Monty', 'Python']
>>>


有關於更多更詳細的字串探討,將會在第三章詳細討論。目前我們已經對串列與字串有了一些基本概念,也可以開始著手進行一些自然語言分析的工作囉!

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

沒有留言: