在上一節《Tokenization - NLP(1)》的學習中,我們主要學習了如何將一串字符串分割成單獨的字符,並且形成一個詞匯集(vocabulary),之后我們將形成的詞匯集合轉換成計算機可以處理的數字信息,以方便我們做進一步文本分析。這篇博客的主題還是我們如何將文本轉成成更有用的成分,讓我們能從文本當中提取到更多的信息以便作為特征輸入到模型中訓練,首先會介紹一下N-grams算法,之后會提到停頓詞及英文文本常見的標准化處理手段,如大小寫的改變、詞干提取等(文章中的某些例子會涉及到正則表達式的使用,但是因為不是主要的內容,對使用到的正則表達式不做過多解釋,如果有需要的話自己找下書籍和在網上搜索下如何使用正則表達式)。
一、N-grams
自然語言處理過程中,一個值得我們注意的是,如果我們僅僅是將文本字符串分割成單獨的文本,此時我們只是簡單的去分析文本中每個字符所代表的潛在意義與我們需要分析的結果的關系性,然而我們忽略一個非常重要的信息,文本的順序是含有非常的重要信息。舉一個簡單的例子,“釣魚”兩個詞,如果我們單獨去分析這兩個詞,而不是看作一個整體的話,那么我們得到的語意意思就是“釣”是一個動作詞,“魚”是一個名詞,而當兩個字放在一起的時候,我們知道其實我們想表述的“釣魚”是我們要做的一個活動(event)。又比如英文“hot dog",我們都知道這個詞組想表達的是我們吃的食物”熱狗香腸包“,所以我們不希望單獨去看hot和dog兩個意思,如果是這樣子我們可以看出意思相差非常的遠,由此我們可以看出文本順序的重要性。
而實際操作中,我們將這種把文本順序保留下來的行為稱之為建立N-grams模型,也就是我們將一個字符串分割成含有多個詞的標識符(tokens)。當然,需要記住的一點是不論是上一節說的還是N-grams,他們都屬於文本字符串Tokenization的一個過程。
1 import re 2 from nltk.util import ngrams 3 4 sentence = "I love deep learning as it can help me resolve some complicated problems in 2018." 5 6 # tokenize the sentence into tokens 7 pattern = re.compile(r"([-\s.,;!?])+") 8 tokens = pattern.split(sentence) 9 tokens = [x for x in tokens if x and x not in '- \t\n.,;!?'] 10 11 bigrams = list(ngrams(tokens, 2)) 12 print([" ".join(x) for x in bigrams])
上述代碼的輸出結果是:
['I love', 'love deep', 'deep learning', 'learning as', 'as it', 'it can', 'can help', 'help me', 'me resolve', 'resolve some', 'some complicated', 'complicated problems', 'problems in', 'in 2018']
上述代碼的執行是首先將文本字符串分割成單獨(unique)標識符,並且引入了正則表達式(更多的正則表達式請參看其他資料,這里有必要指出,當我們做一些大型的文本分析時,其實真正用正則表達式去書寫相應的規則執行起來的效率是很低的,因為文本是千變萬化,幾乎沒有相同的,例如每個人在微博上post的東西,所附在文本上的字符是千差萬別,然后我們一般電子書上的文本又與網絡的不同,所以就形成了無法用一套正則表達式的規則去完成所有的任務,普適性是很差的。)來更精准的分割字符串。除此之外,我們運用了NLTK的庫來分割出一個含有兩個詞(Bi-Gram)的標識符。所以從上面我們可以看出,如“deep learning"和”complicated problems“這樣子的組合更切合我們想要表達的意思,但是獨個字符看的話我們就未必看得出了。
雖然N-grams模型可以讓我們更好的去分割出具有更好語意的標識符,進而讓我們做進一步文本分析,但是缺點也是同樣明顯,那就是運用N-grams模型可能讓我們的詞匯量成指數級的增長,並且並不是所有的Bigram都含有有用信息,而這個情況在甚至乎在Trigram或者Quad gram等含有更多單獨字符在內的N-grams模型會更嚴重。這樣子做產生的問題就是我們最終拿到的特征向量(the dimension of the feature vectors)的維度將會超過我們本身的文件樣本數(length of the documents),而最終當我們將這些提取出來的特征放入到機器學習算法中的話,就會導致過擬合(over fitting)的情況。如此訓練出來的模型將沒有什么太好的performance和預測能力。
二、Stop Words
造成上述問題的一個原因可能是我們分割出來的標識符(n-grams)含有太多的不具備有用信息的組合,如帶有停頓詞(stop words)的詞組組合,停頓詞在英文中出現的頻率是非常高的,如a, an, and, or, of, at, the等等單詞,這些單詞攜帶的信息量(substantive information)是極度有限的。所以我們需要做的就是在NLP分析過程中將文本中的停頓詞去掉,這樣子做的好處是我們減少詞匯量,進而降低我們特征向量的維度。But.......我們還是需要再次注意一個問題,那就是雖然停頓詞本身所攜帶的信息不是很多,但是stop words卻可能在n-grams中存在關系性信息(relational information),考慮下面兩種情況:
- Mark reported to the CEO
- Susan reported as the CEO to the board
在上述例子中,如果我們將to the和as the去掉的話,那么我們就會得到reported CEO,這是很迷惑的,因為這兩個句子中本身是有一個層級意思的,但是因為我們remove掉了as,the和to這三個stop words導致了關系信息的缺失。正常情況下,我們需要創建一個4-grams的詞(如上述紫色字部分標注高亮的部分)。這也就延申出我們需要討論的關於NLP模型創立過程中碰到的一個問題,那就是基本是特定問題需要特定的解決辦法。具體為根據實際運用而定,創建一個過濾器適當的過濾掉我們不需要的stop words。
下面我們通過NLTK的庫來看看英文中大概都有那些stop words:
1 import nltk 2 3 nltk.download("stopwords") 4 stopwords = nltk.corpus.stopwords.words("english") 5 print(len(stopwords)) 6 print(stopwords[:50])
輸出結果為:
[nltk_data] Downloading package stopwords to [nltk_data] C:\Users\JielongSSS\AppData\Roaming\nltk_data... [nltk_data] Package stopwords is already up-to-date! 179 ['i', 'me', 'my', 'myself', 'we', 'our', 'ours', 'ourselves', 'you', "you're", "you've", "you'll", "you'd", 'your', 'yours', 'yourself', 'yourselves', 'he', 'him', 'his', 'himself', 'she', "she's", 'her', 'hers', 'herself', 'it', "it's", 'its', 'itself', 'they', 'them', 'their', 'theirs', 'themselves', 'what', 'which', 'who', 'whom', 'this', 'that', "that'll", 'these', 'those', 'am', 'is', 'are', 'was', 'were', 'be']
三、標准化處理(Normalization)
討論那么多,我們也應該意識到一個問題:一個NLP模型的表現(performance)很大程度取決於我們所擁有的詞匯量(額。。。其實嘛,很難有一個定量的分析,起碼目前在學習過程中給我的感覺是如此,詞匯量應該掌握在一個具體什么樣的程度呢?希望有大神看了我的博客文章也留言給我,是否有一個有效的衡量方法去查看究竟我們所需要的詞匯量是多少?)。當然這部分我們要講的主要是如何通過其他方式來縮減我們tokenize之后創建的feature vector的維度,也就是減少我們的詞匯量,以最大程度的保留我們所需要的覺得有用的信息。具體一般有三種處理方式:CASE FOLDING(大小寫的改變),Stemming和Lemmatization,下面會展開詳細的說明。
3.1 CASE Folding
在英文的NLP模型中,單詞的大小寫是非常敏感的,這跟我們中文比較不一樣,中文是沒有所謂的大小寫之說的,在這里因為主要以英文NLP為主,所以就只講英文的標准了,未來有機會更新博客的時候我會嘗試引入中文相關的NLP處理方式。我們都知道,在書寫英文句子的時候,我們總是讓開頭的第一個單詞的首字母處於大寫狀況,或者說我們想要強調某些事件的時候,我們就希望用全大寫來表示,但是我們同樣知道But和but是同一個單詞but並且表示同一個意思,然而文本分析過程中這是兩個不同的單詞,僅僅是因為他們的首字母不一樣,這樣子計算機自動分析的時候得到的結果就會導致有偏差,所以我們需要對這But這個單詞進行大小寫的規范處理,從而減少我們的詞匯量。
1 tokens = ['Horse', 'horse', 'Dog', 'dog', 'Cat', 'cat'] 2 print(tokens) 3 print("單詞數量為: ",len(set(tokens))) 4 5 normalized_tokens = [x.lower() for x in tokens] 6 print(normalized_tokens) 7 print("Normalized之后的單詞數量為: ",len(set(normalized_tokens)))
輸出結果為:
['Horse', 'horse', 'Dog', 'dog', 'Cat', 'cat'] 單詞數量為: 6 ['horse', 'horse', 'dog', 'dog', 'cat', 'cat'] Normalized之后的單詞數量為: 3
從上面的結果我們可以看出,我們單詞的數量從6個變為了3個,因為Horse和horse表達的就是同一個東西。當然,就如我們開頭所說的,英文單詞對於大小寫是很敏感的,也就意味着大小寫的單詞對於英文單詞所要表達的意思可能是不同的,如Doctor和doctor在大小寫方面前者表示為博士,后者我們說的一般是醫生的意思,這是我們需要注意的一點,當然你並無法完全針對每個大小寫敏感的單詞去做case normalization,所以一般情況我們根據需求而定,取舍來做分析,大部分時候的做法是我們只對句子的首個單詞的首字母進行case normalization,這只是提供一種分析方法,根據學習過程獲得信息,英文的NLP模型最終都是不采用case normalization的,以免丟失太多的信息,對於中文等一些語言,大小寫不敏感的,這個就更沒意義了。
3.2 Stemming
Stemming是另外一個處理英文文本會用到的技巧,主要是單詞的復數形式中或者指代所有格結果等單詞中提取出相應的詞干(stem)。例如,我們知道cats,horses的詞干形式是cat和horse,又比如doing的詞干為do。通過這樣子的處理,我們將很多不同形式的詞回復為其原本的詞干形式,這樣子做有很大的作用。一個實例就是搜索引擎,當你搜索某樣的東西的時候,很多時候你可能不知道你所需要搜索的東西的具體拼寫方式,所以我們只是鍵入你覺得可能的詞,但是此時我們需要機器反饋給我具有相關聯系的搜索結果,這個結果不僅僅是需要語意上盡可能地相同,大部分時候我們是基於關鍵字匹配的,如果采取的是100%的匹配的話,得到的結果將會是很有限,這時候通過詞干的匹配來檢索呈現出相應的結果就顯得異常的重要。而對於我們搭建模型,在預處理文本的階段,則大大的減少了我們的詞匯量(意味着我們不需要大空間儲存)與此同時它也盡可能地規避減少信息地丟失。不僅如此,提取詞干也同時讓我們地模型更具普適性,這點符合我們剛才說的搜索引擎的例子。這里有一點需要注意的是,這里的詞干並非嚴格意義上的詞干,而只是我們所說的字符或者標識符,這個標識符可能表示的是好幾種不同拼寫形式的單詞。
1 def stemming(sent): 2 return ' '.join([re.findall('^(.*ss|.*?)(s)?$', word)[0][0].strip("'") for word in sent.lower().split()]) 3 4 stemming('horses')
上述代碼的輸出結果為:
'horse'
正則表達式中想要表達的是如果一個單詞的結尾為s的話則詞干為去掉s之后的單詞,如果多余一個s作為結尾的話,那么這個詞保持原型。上述的代碼示例能解決的問題是很有限的,因為更復雜的諸如dishes這樣子的單詞,我們知道去掉的是es,而不僅僅是s,如果要達到足夠高的精准度,那我們需要寫的正則表達式也會逐步增多。這樣子代碼執行起來的效率也不夠高。下面介紹一下用NLTK庫中的PorterStemmer來提取文本的詞干。
1 from nltk.stem.porter import PorterStemmer 2 3 stemmer = PorterStemmer() 4 print(' '.join([stemmer.stem(w).strip("'") for w in "dishes washer's washed dishes".split()]))
輸出結果為:
dish washer wash dish
3.3 Lemmatization
詞形還原(lemmatization)也是一種在英文語言處理中比較常見的的技巧,大致的作用與詞干提取類似,也是希望不同形式的單詞可以在經過處理之后恢復為他們原本的模樣,但是詞形還原更多的是放在了單詞本身的語意上。所以,詞形還原其實比詞干提取和大小寫的改變更適合預處理文本,因為他們不是簡單的改變單詞的大小寫或者單復數或者所有格的形式,而是基於語意去做還原。比如,我們如果用詞干提取去處理better這個單詞的時候,我們可能會把單詞的er去掉,這樣子單詞就會編程bet或者bett,這完全改變了單詞的意思,但是如果是基於詞形還原,那么我們就得到類似的詞,如good,best等等。在正式NLP模型創建過程中,我們一般是希望詞形還原的運用是在詞干提取前面,因為在英文文本中,lemmatization處理過后的單詞更接近單詞本身所要表達的意思,並且同樣的也可以減少我們特征的維度。下面是通過NLTK上的WordNetLemmatizer函數來讓你了解下詞形還原是如何工作的:
1 from nltk.stem import WordNetLemmatizer 2 3 lemmatizer = WordNetLemmatizer() 4 print(lemmatizer.lemmatize('better')) 5 print(lemmatizer.lemmatize('better', pos='a'))
輸出結果為:
better
good
上述代碼第五行中的pos是part of speech是詞性標注的意思,a代表的形容詞的形式。
綜上,我們可以看出,詞干提取和詞形還原都可以減少單詞的詞匯量,但是同時他們也增加了文本的迷惑性,因為不可能將不同形式的單詞100%的恢復成所要表達的單詞形式,更需要明白的是,即使詞干一樣,基於該呈現出來的不同形式的單詞的意思也會差很多,所以迷惑性也就增加了,這樣子對我們自然語言文本分析其實變相的增加了難度,在實際的運用做,我們需要根據實際情況運用上述講到的算法原理和技巧。