2021年11月14日 星期日

NLTK 學習筆記 (三) : nltk.book.text1~9 語料庫

NLTK 安裝以來一直沒有時間學習, 雖然我更想學較新的 SpaCy, 但 NLTK 是 NLP 很經典的自然語言學習套件, 所以還是要涉獵一番, 這樣在學 SpaCy 時也好有個比較的對象. 本篇主要是檢視 NLTK 語料庫中的 book.text1~9 這 9 個語料庫, 是最近閱讀下列這本書第一章的測試筆記 : 


本系列之前的文章參考 :



1. Text 物件的屬性與方法 :

在 nltk.book 子套件中收錄了 text1~text9 共九本書的語料庫, 它們都是 Text 物件, 首先從 nltk.book 匯入全部物件, 它會馬上回應 text1~textt9 這九本書的書名 :

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

以內建函式 type() 檢查 nltk.book.text1~9 可知 text1~text9 均為 Text 物件, 呼叫 dir() 函式可檢視 Text 物件的成員, 以 text1 為例 : 

>>> type(text1)               # text1~9 都是 Text 物件 
<class 'nltk.text.Text'>   

將 text1~text9 傳入 dir() 會傳回 Text 物件的成員串列, 以 text1 為例 : 

>>> dir(text1)                 # 傳回 Text 物件的成員串列
['_CONTEXT_RE', '_COPY_TOKENS', '__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__getitem__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__len__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', '_context', '_train_default_ngram_lm', 'collocation_list', 'collocations', 'common_contexts', 'concordance', 'concordance_list', 'count', 'dispersion_plot', 'findall', 'generate', 'index', 'name', 'plot', 'readability', 'similar', 'tokens', 'vocab']

但從這些成員名稱無法得知那些是屬性, 哪些是方法, 這可以先將 dir() 傳回值指派給一個變數, 然後用迴圈以 print() 來檢視這些成員的內容, 例如 : 

>>> members=dir(text1)   
>>> type(members)    
<class 'list'>   
>>> for mbr in members:         # 走訪 Text 物件成員
    obj=eval('text1.' + mbr)        # 用 eval() 求值取得 text1.成員之參考
    if not mbr.startswith('_'):     # 走訪所有不是 "_" 開頭的成員
        print(mbr, type(obj))   
        
collocation_list <class 'method'>
collocations <class 'method'>
common_contexts <class 'method'>
concordance <class 'method'>
concordance_list <class 'method'>
count <class 'method'>
dispersion_plot <class 'method'>
findall <class 'method'>
generate <class 'method'>
index <class 'method'>
name <class 'str'>
plot <class 'method'>
readability <class 'method'>
similar <class 'method'>
tokens <class 'list'>
vocab <class 'method'>

此處在迴圈中先使用求值函式 eval() 來取得 text1.成員名稱之參考, 此法可用來將以字串表示之變動物件名稱轉成物件之參考, 對於需要在迴圈中存取一群物件非常好用. 除了使用 eval() 外也可以用 vars()[text] 或 locals()[text],  參考 :

接著用字串的 startswith() 濾掉所有以 '_' 開頭的成員, 留下屬性與方法, 結果中 class 是 'method' 者為方法, 其他則為屬性. 由上面的結果可知, nltk.book.text1~9  物件成員中只有 name 與 tokens 這兩個屬性, 其餘皆為方法 (method). 可用 for 迴圈來顯示這九本書的書名 :

>>> for i in range(1, 10):      
    text='text' + str(i)              # 利用求值函式 eval() 取得 text1~9 之物件參考
    print(f'{text} name={eval(text).name}')         # 印出 text1~9 的 name 屬性值
    
text1 name=Moby Dick by Herman Melville 1851
text2 name=Sense and Sensibility by Jane Austen 1811
text3 name=The Book of Genesis
text4 name=Inaugural Address Corpus
text5 name=Chat Corpus
text6 name=Monty Python and the Holy Grail
text7 name=Wall Street Journal
text8 name=Personals Corpus
text9 name=The Man Who Was Thursday by G . K . Chesterton 1908


2. 計算 token 數目 :

所謂的 token 是自然語言處理中用來指涉一串字元的術語, 它可以是英文字, 標點符號, 數字, 或特殊符號等, 並非只是英文字 (word) 而已. 以下將檢視 NLTK 內 book 子套件底下的 text1~text9 共九本書語料中的 token 數目. 

將變數 text1~text9 傳入 len() 即可計算每本書的 token 數 :

>>> len(text1)  
260819
>>> len(text2)  
141576
>>> len(text3)   
44764
>>> len(text4)  
149797
>>> len(text5)   
45010
>>> len(text6)  
16967
>>> len(text7)  
100676
>>> len(text8)   
4867
>>> len(text9)   
69213

這樣一步步呼叫 len() 很冗長, 可以用 for 迴圈配合求值函式 eval() 來做 : 

>>> for i in range(1, 10):   
    text='text' + str(i)   
    length=len(eval(text))            # 用 eval() 將字串 text 轉成變數
    print(f'total tokens of {text} : {length}')    
    
total tokens of text1 : 260819
total tokens of text2 : 141576
total tokens of text3 : 44764
total tokens of text4 : 149797
total tokens of text5 : 45010
total tokens of text6 : 16967
total tokens of text7 : 100676
total tokens of text8 : 4867
total tokens of text9 : 69213

注意, 此處必須先用 eval() 將字串求值以取得變數 text1~text9 的參考位址, 若用 len(text) 表示是計算 'text1', 'text2', ... 之長度, 因此將總是得到 5 (因 'text1' 字元數為 5), 而不是變數 text1~text9 內容的長度. 

由上述可知, text1~9 物件的 token 屬性值為 list, 它被用來紀錄語料中的所有 token, 因為串列的元素很多, 直接 print() 其實看不到全貌, 但可用迴圈顯示前 10 個元素, 例如 :

>>> type(text1.tokens)             # tokens 屬性的資料型態為串列
<class 'list'>   
>>> for i in range(1, 9):           # 顯示 text1~9 物件的前 10 個 token
    text=eval('text' + str(i))       # 利用 eval() 字串求值取得變數
    print(text.tokens[:10])         # 顯示 tokens 屬性前 10 個元素
    
['[', 'Moby', 'Dick', 'by', 'Herman', 'Melville', '1851', ']', 'ETYMOLOGY', '.']
['[', 'Sense', 'and', 'Sensibility', 'by', 'Jane', 'Austen', '1811', ']', 'CHAPTER']
['In', 'the', 'beginning', 'God', 'created', 'the', 'heaven', 'and', 'the', 'earth']
['Fellow', '-', 'Citizens', 'of', 'the', 'Senate', 'and', 'of', 'the', 'House']
['now', 'im', 'left', 'with', 'this', 'gay', 'name', ':P', 'PART', 'hey']
['SCENE', '1', ':', '[', 'wind', ']', '[', 'clop', 'clop', 'clop']
['Pierre', 'Vinken', ',', '61', 'years', 'old', ',', 'will', 'join', 'the']
['25', 'SEXY', 'MALE', ',', 'seeks', 'attrac', 'older', 'single', 'lady', ',']

可見 token 不僅僅是英文字而已, 還包括了數字與標點符號等等. 用 len() 檢查 text1~9 的 token 屬性可得到與上面用 len(text1)~len(text9) 相同的 token 數目, 例如 : 

>>> len(text1.tokens)    
260819
>>> len(text2.tokens)    
141576
>>> len(text3.tokens)   
44764
>>> len(text4.tokens)  
149797
>>> len(text5.tokens)  
45010
>>> len(text6.tokens)   
16967
>>> len(text7.tokens)  
100676
>>> len(text8.tokens)  
4867
>>> len(text9.tokens)    
69213

也可用迴圈來檢視 :

>>> for i in range(1, 10):      
    text='text' + str(i)      
    length=len(eval(text).tokens)                      # 用 eval() 將字串 text 轉成變數
    print(f'total tokens of {text} : {length}')   
    
total tokens of text1 : 260819
total tokens of text2 : 141576
total tokens of text3 : 44764
total tokens of text4 : 149797
total tokens of text5 : 45010
total tokens of text6 : 16967
total tokens of text7 : 100676
total tokens of text8 : 4867
total tokens of text9 : 69213

可見結果與上面用 len(text1) ~ len(text9) 是一樣的. 


3. 用 set() 去除重複的 token :

len() 所統計的 token 數並未排除重複的 token, 若要去除重複的 token, 可利用 Python 的集合型態, 因為集合的元素不可重複, 都是 unique 的. 因此只要把 text1~text9 的語料傳入 set() 即可剔除重複的 token, 再將集合傳給 len() 即可得到語料中不重複計算的 token 數, 例如 :

>>> text1_set=set(text1)   
>>> type(text1_set)   
<class 'set'>  
>>> len(text1_set)     
19317  
>>> len(text1)   
260819

可見 text1 總 token 數有 26 萬多, 但其中有許多 token 重複出現, 經過 set() 轉成集合剔除重複的 token 後, 真正獨一無二的 token 數才 1 萬 9 千多而已. 我們可用 for 迴圈來計算 uniqe token 數 : 

>>> for i in range(1, 10):     
    text='text' + str(i)      
    tokens=len(eval(text))     
    unique_tokens=len(set(eval(text)))      # 先將語料 text?  傳給 set() 轉成集合
    print(f'{text} : total tokens={tokens} unique tokens={unique_tokens}')    
    
text1 : total tokens=260819 unique tokens=19317
text2 : total tokens=141576 unique tokens=6833
text3 : total tokens=44764 unique tokens=2789
text4 : total tokens=149797 unique tokens=9913
text5 : total tokens=45010 unique tokens=6066
text6 : total tokens=16967 unique tokens=2166
text7 : total tokens=100676 unique tokens=12408
text8 : total tokens=4867 unique tokens=1108
text9 : total tokens=69213 unique tokens=6807

可見不重複的 token 數目就少很多了. 

沒有留言:

張貼留言