1.文本數(shù)據(jù)的特征提取、中文分詞及詞袋模型
本節(jié)我們一起學(xué)習(xí)如何對文本數(shù)據(jù)進(jìn)行特征提取,如何對中文分詞處理,以及如何使用詞袋模型將文本特征轉(zhuǎn)化為數(shù)組的形式,以便將文本轉(zhuǎn)化為機器可以“看懂”的數(shù)字形式。
1.1使用CountVectorizer對文本進(jìn)行特征提取
在前面的章節(jié),我們用來展示的數(shù)據(jù)特征大致可以分為兩種:一種是用來表示數(shù)值的連續(xù)特征;另一種是表示樣本所在分類的類型特征。而在自然語言處理的領(lǐng)域中,我們會接觸到的第三種數(shù)據(jù)類型--文本數(shù)據(jù)。舉個例子,假如我們想知道用戶對某個商品的評價是“好”還是“差”,就需要使用用戶評價的內(nèi)容文本對模型進(jìn)行訓(xùn)練。例如,用戶評論說“剛買的手機總是死機,太糟糕了!” 或者“新買的衣服很漂亮,老公很喜歡?!边@就需要我們提取出兩個不同評論中的關(guān)鍵特征,并進(jìn)行標(biāo)注用于訓(xùn)練機器學(xué)習(xí)模型。
文本數(shù)據(jù)在計算機中往往被存儲為字符串類型(String),在不同的場景中,文本數(shù)據(jù)的長度差異會非常大,這也使得文本數(shù)據(jù)的處理方式與數(shù)值型數(shù)據(jù)的處理方式完全不同。而中文的處理尤其困難,因為在一個句子當(dāng)中,中文的詞與詞之間沒有邊界,也就是說,中文不像英語那樣,在每個詞之間有空格作為分界線,這就要求我們在處理中文文本的時候,需要先進(jìn)行分詞處理。
例如這句英語:“The quick brown fox jumps over a lazy dog”,翻成中文是“那只敏捷的棕色狐貍跳過了一直懶惰的狗”。這兩句話在處理中非常不同,我們來看下面的代碼:
#導(dǎo)入向量化工具
from sklearn.feature_extraction.text import CountVectorizer
vect = CountVectorizer()
#使用vector擬合文本數(shù)據(jù)
en = ['The quick brown fox jumps over a lazy dog']
vect.fit(en)
#打印結(jié)果
print('單詞數(shù):{}'.format(len(vect.vocabulary_)))
print('分詞:{}'.format(vect.vocabulary_))
運行代碼,得到如下圖所示的結(jié)果:

結(jié)果分析:可能讀者朋友們對這個結(jié)果會感覺到有點奇怪,明明這句話當(dāng)中有9個單次,為什么程序告訴我們單次數(shù)是8個呢?我們來檢查一下分詞的結(jié)果,原來程序沒有將冠詞“a”統(tǒng)計進(jìn)來。因為“a”只有一個字母,所以程序沒有把她作為一個單次。
下面來看中文的情況,輸入代碼如下:
#使用中文分詞作為實驗
cn = ['那只敏捷的棕色狐貍跳過了一只懶惰的狗']
#擬合中文文本數(shù)據(jù)
vect.fit(cn)
#打印結(jié)果
print('單詞數(shù):{}'.format(len(vect.vocabulary_)))
print('分詞:{}'.format(vect.vocabulary_))
運行代碼,得到如下圖的結(jié)果:

結(jié)果分析:可以看到,程序無法對中文語句進(jìn)行分詞,它把整句話當(dāng)成了一個詞,因為中文與英語不同,英語的詞與詞之間有空格作為天然的分割詞,而中文卻沒有。在這種情況下,我們就需要使用專門的工具來對中文進(jìn)行分詞。目前市面上有幾款用于中文分詞的工具,使用較多的工具之一是“結(jié)巴分詞”,下面我們以“結(jié)巴分詞”為例,介紹一下中文分詞的分詞方法。
1.2使用分詞工具對中文文本進(jìn)行分詞
我們使用“結(jié)巴分詞”來對上文中的中文語句進(jìn)行分詞,輸入代碼如下:
import jieba
cn = jieba.cut('那只敏捷的棕色狐貍跳過了一只懶惰的狗')
cn = [' '.join(cn)]
print(cn)
運行代碼,得到如下圖所示的結(jié)果:

借助“結(jié)巴分詞”,我們把這句中文語句進(jìn)行了分詞操作,并在每個單詞之間插入空格作為分界線。下面我們重新使用CountVectorizer對其進(jìn)行特征抽取,輸入代碼如下:
#使用CountVectorizer對中文文本進(jìn)行向量化
vect.fit(cn)
print('單詞數(shù):{}'.format(len(vect.vocabulary_)))
print('分詞:{}'.format(vect.vocabulary_))
運行代碼,得到如下圖所示的結(jié)果:

結(jié)果分析:經(jīng)過了分詞工具的處理,我們看到CountVectorizer已經(jīng)可以從中文文本中提取出若干個整型數(shù)值,并且生成了一個字典。
接下來,我們要將使用這個字典將文本的特征表達(dá)出來,以便可以用來訓(xùn)練模型。
1.3使用詞袋模型將文本數(shù)據(jù)轉(zhuǎn)為數(shù)組
在上面的實驗中,CountVectorizer給每個詞編碼為一個從0到5的整型數(shù)。經(jīng)過這樣的處理后,我們便可以用一個稀疏矩陣(sparse matrix)對這個文本數(shù)據(jù)進(jìn)行表示了。
輸入代碼如下:
#定義詞袋模型
bag_of_words = vect.transform(cn)
#打印詞袋模型中的數(shù)據(jù)特征
print('轉(zhuǎn)化為詞袋的特征:{}'.format(repr(bag_of_words)))
運行代碼,可以得到如下圖所示的結(jié)果:

結(jié)果分析:從結(jié)果中可以看到,原來的那句話被轉(zhuǎn)化為一個1行6列的稀疏矩陣,類型為64位整型數(shù)值,其中有6個元素。
下面我們看看6個元素都是什么,輸入代碼如下:
#打印詞袋密度的表達(dá)
print('詞袋的密度表達(dá):{}'.format(bag_of_words.toarray()))
運行代碼,會得到如下圖所示的結(jié)果:

結(jié)果分析:可能看到結(jié)果會讓人有點費解。它的意思是,在這一句話中,我們通過分詞工具拆分出來的6個單詞在這句話中出現(xiàn)的次數(shù)是1次;第二個元素1,代表這句話中,“懶惰”這個詞出現(xiàn)的次數(shù)也是1.
為了更加容易理解,我們試著換一句話來看看結(jié)果有什么不同,例如,“懶惰的狐貍不如敏捷的狐貍敏捷,敏捷的狐貍不如懶惰的狐貍懶惰”。輸入代碼:
cn1 = jieba.cut('懶惰的狐貍不如敏捷的狐貍敏捷,敏捷的狐貍不如懶惰的狐貍懶惰')
print(type(cn))
cn2 = [' '.join(cn1)]
print(cn2)
上面這段代碼主要是使用“結(jié)巴分詞”將我們編造的這段話進(jìn)行分詞,運行代碼,得到如下圖所示的結(jié)果:

接下來,我們再使用CountVectorizer將這句文本進(jìn)行轉(zhuǎn)化,輸入代碼如下:
#導(dǎo)入向量化工具
from sklearn.feature_extraction.text import CountVectorizer
#建立詞袋模型
new_bag = vect.transform(cn2)
print('轉(zhuǎn)為詞袋的特征:{}'.format(repr(new_bag)))
print('詞袋的密度表達(dá):{}'.format(new_bag.toarray()))
運行代碼,得到如下圖所示的結(jié)果:

結(jié)果分析:我么發(fā)現(xiàn),同樣還是1行6列的,不過存儲的元素只有3個,而數(shù)組[[0 3 3 0 4 0]]的意思是,“一只”這個詞出現(xiàn)的次數(shù)是0,而“懶惰”這個詞出現(xiàn)了3次,“敏捷”這個詞出現(xiàn)了3次,“棕色”這個詞出現(xiàn)了0次,“狐貍”這個詞出現(xiàn)了4次,“跳過”這個詞出現(xiàn)了0次。
上面這種用數(shù)組表示一句話中,單詞出現(xiàn)次數(shù)的方法,被稱為“詞袋模型”(bag-of-words)。這種方法是忽略了一個文本中的詞序和語法,僅僅將它看作一個詞的集合。這種方法對于自然語言進(jìn)行了簡化,以便于機器可以讀取并且進(jìn)行模型的訓(xùn)練。但是詞袋模型也具有一定的局限性,下面我們繼續(xù)介紹對于文本類型數(shù)據(jù)的進(jìn)一步優(yōu)化處理。
2.對文本數(shù)據(jù)進(jìn)一步優(yōu)化處理
本節(jié),我們將和大家一起學(xué)習(xí)如何使用n_Gram算法來改善詞袋模型,以及如何使用tf-idf算法對文本數(shù)據(jù)進(jìn)行處理,和如何刪除文本數(shù)據(jù)中的停用詞。
2.1使用n-Gram改善詞袋模型
雖然用詞袋模型可以簡化自然語言,利于機器學(xué)習(xí)算法建模,但是它的劣勢也是很明顯-----由于詞袋模型把句子看成單詞的簡單集合,那么單詞出現(xiàn)的順序就會被無視,這樣一來可能會導(dǎo)致包含同樣單詞,但是順序不一樣的兩句話在機器學(xué)習(xí)看來成了完全一樣的意思。
比如:“道士看見和尚親吻了尼姑的嘴唇”,我們用詞袋模型將這句話的特征進(jìn)行提?。?/p>
import jieba
joke = jieba.cut('道士看見和尚親吻了尼姑的嘴唇')
joke = [' '.join(joke)]
vect.fit(joke)
joke_feature = vect.transform(joke)
print(joke_feature.toarray())
這里我們首先用“結(jié)巴分詞”對這句話進(jìn)行了分詞,然后使用CountVectorizer將其表達(dá)為數(shù)組
運行代碼,結(jié)果如下:
[[1 1 1 1 1 1]]
接下來,我們把這句話的順序打亂,變成“尼姑看見道士的嘴唇親吻了和尚”,再看看結(jié)果會有什么不同,輸入代碼如下:
joke2 = jieba.cut('尼姑看見道士的嘴唇親吻了和尚')
joke2 = [' '.join(joke2)]
#進(jìn)行特征提取
joke2_feature = vect.transform(joke2)
print('特征表達(dá):{}'.format(joke2_feature.toarray()))
運行代碼,得到如下結(jié)果:
特征表達(dá):[[1 1 1 1 1 1]]
結(jié)果分析:和上一個代碼的結(jié)果進(jìn)行對比的話,發(fā)現(xiàn)兩個結(jié)果完全一樣!也就是說,這兩句意思完全不同的話,對于機器來講,意思是一模一樣!
要解決這個問題,我么可以對CountVectorizer中的ngram_range參數(shù)進(jìn)行調(diào)節(jié)。這里我們先介紹一下,n_Gram是大詞匯連續(xù)文本或語音識別中常用的一種語言模型,它是利用上下文相鄰詞的搭配信息來進(jìn)行文本數(shù)據(jù)轉(zhuǎn)換的,其中n代表一個整型數(shù)值,例如n等于2的時候,模型稱為bi-Gram,意思是會對相鄰的兩個單詞進(jìn)行搭配;而n等于3時,模型稱為tri-Gram,也就是會對相鄰的3個單詞進(jìn)行配對。下面我們來演示如何在CountVectorizer中調(diào)節(jié)n-Gram函數(shù),來進(jìn)行詞袋模型的優(yōu)化,輸入代碼如下:
#修改CountVectorizer的ngram參數(shù)
vect = CountVectorizer(ngram_range=(2,2))
#重新進(jìn)行文本數(shù)據(jù)的特征提取
cv = vect.fit(joke)
joke_feature = cv.transform(joke)
print('特征表達(dá):{}'.format(joke_feature.toarray()))
print('分詞:{}'.format(vect.vocabulary_))
這里,我們將CountVectorizer的ngram_range參數(shù)調(diào)節(jié)為(2,2),意思是進(jìn)行組合的單詞數(shù)量的下限是2,上限也是2.也就是說,我們限制CountVectorizer將句子中相鄰的兩個單詞進(jìn)行組合,運行代碼,得到如下圖所示的結(jié)果:

現(xiàn)在我們再來試試另外一句“尼姑看見道士的嘴唇親吻了和尚”,看看轉(zhuǎn)化的特征是否有了變化,輸入代碼如下:
#調(diào)整文本順序
joke2 = jieba.cut('尼姑看見道士的嘴唇親吻了和尚')
#插入空格
joke2 = [' '.join(joke2)]
#提取文本特征
joke2_feature = vect.transform(joke2)
#特征表達(dá)
print(joke2_feature.toarray())
運行代碼,得到如下圖所示的結(jié)果:

結(jié)果分析:現(xiàn)在我們看到,在調(diào)整了CountVectorizer的ngram_range參數(shù)之后,機器不再認(rèn)為這兩句是同一個意思了。而除了使用n-Gram模型對文本特征提取進(jìn)行優(yōu)化之外,在scikit-lean中,還可以使用另外一種tf-idf模型來進(jìn)行文本特征提取的類,稱為TfidfVector。
2.2使用tf-idf模型對文本數(shù)據(jù)進(jìn)行處理
tf-idf全稱為“term frequency-inverse document frequency”,一般翻譯為“詞頻-逆向文件頻率”。它是一種用來評估某個詞對于一個語料庫中某一份文件的重要程度,如果某個詞在某個文件中出現(xiàn)的次數(shù)非常高,但在其他文件中出現(xiàn)的次數(shù)很少,那么tf-idf就會認(rèn)為這個詞能夠很好地將文件進(jìn)行區(qū)分,重要程度就會較高,反之則認(rèn)為該單詞的重要程度較低。下面我們看一下tf-idf的公式。
首先是計算tf值的公式:
式中:表示某個詞在語料庫中某個文件內(nèi)出現(xiàn)的次數(shù);
表示的是該文件中所有單詞出現(xiàn)的次數(shù)之和。
而在scikit-lean中,idf的計算公式如下:
式中:N代表的是語料庫中文件的總數(shù);代表語料庫中包含上述單詞的文件數(shù)量。
那么最終計算tf-idf值的公式就是:
tf-idf = tf* idf
注意
讀者朋友可能會在其他地方看到和此處不太一樣的公式,不要覺得奇怪,這是因為tf-idf的計算公式本身就有很多種變體
在scikit-lean當(dāng)中,有兩個類使用了tf-idf方法,其中一個是TfidfTransformer,它用來將CountVectorizer從文本中提取的特征矩陣進(jìn)行轉(zhuǎn)化;另一個是TfidfVectorizer,它和CountVectorizer的用法是相同的--簡單理解的話,它相當(dāng)于把CountVectorizer和TfidfTransformer所做的工作整合在了一起。
為了進(jìn)一步介紹TfidfVectorizer的用法,已經(jīng)它和CountVectorizer的區(qū)別,我們下面使用一個相對復(fù)雜的數(shù)據(jù)集,也是一個非常經(jīng)典的用于進(jìn)行自然語言處理的案例,就是IMDB電影評論數(shù)據(jù)集。這個數(shù)據(jù)集是由斯坦福大學(xué)的研究人員創(chuàng)建的,包括100000條IMDB網(wǎng)站用戶對于不同電影的評論,每條評論被標(biāo)注為“正面”(positive)或者“負(fù)面”(negtive)兩種類型。如果用戶在IMDB網(wǎng)站上給某個電影的評分大于或等于6,那么他的評論將被標(biāo)注為“正面”,否則被標(biāo)注為“負(fù)面”。
值得稱贊的是,創(chuàng)建者已經(jīng)將數(shù)據(jù)集拆分成了訓(xùn)練集和測試集,分別有25000條數(shù)據(jù),并且放在了不同的文件夾中,正面評論放在“pos”文件夾中,而負(fù)面評論放在了“neg”文件夾中,還有5000條沒有進(jìn)行分類的數(shù)據(jù)集,可以供我們進(jìn)行無監(jiān)督學(xué)習(xí)的實驗??梢栽?a target="_blank">http://ai.stanford.edu/~amaas/data/setiment/中下載這個數(shù)據(jù)集來進(jìn)行實驗。
接下來,載入IMDB電影評論數(shù)據(jù)集,來看看它的結(jié)構(gòu),輸入命令:
!tree C:\\Users\\1003342\\Desktop\\study\\20190528_sklearn\\datasets\\aclImdb
運行,得到如下圖所示的樹狀文件夾列表:

結(jié)果分析:
從結(jié)果中,可以看出IMDB影評數(shù)據(jù)集解壓后是存放在一個名叫aclImdb文件夾中,訓(xùn)練集和測試集分別保存在名為“train”和“test”子文件夾中,每個子文件夾下還有存放正面評論的“pos”文件夾和“neg”文件夾,而“train”文件夾下還有一個“unsump”的子文件夾,存放的是不含分類標(biāo)注的用于進(jìn)行無監(jiān)督學(xué)習(xí)的數(shù)據(jù)。
為了能夠減低數(shù)據(jù)載入的時間,我們從train和test文件夾中各抽取50個正面評論和50個負(fù)面評論,保存在新的文件夾中。
輸入代碼如下:
#導(dǎo)入文件載入工具
#導(dǎo)入文件載入工具
from sklearn.datasets import load_files
#定義訓(xùn)練數(shù)據(jù)集
train_set = load_files('C:\\Users\\1003342\\Desktop\\study\\20190528_sklearn\\datasets\\aclImdb\\train')
X_train,y_train = train_set.data,train_set.target
#打印訓(xùn)練數(shù)據(jù)集文件數(shù)量
print('訓(xùn)練集文件數(shù)量:{}'.format(len(X_train)))
#隨便抽取一條影評打印出來
print('隨便抽一個看看:{}'.format(X_train[22]))
運行代碼,得到如圖所示的結(jié)果:

結(jié)果分析:由于我們各從pos文件夾中的正面評論和neg文件夾的負(fù)面評論中抽取了50個樣本,因此整個訓(xùn)練集中有100個樣本。通過打印第22個樣本,我們看到這段影評的內(nèi)容還是相當(dāng)豐富的,但大家會發(fā)現(xiàn)在評論正文中,有很多<br />的符號,這是在網(wǎng)頁中用來分行的符號。為了不讓它影響機器學(xué)習(xí)的模型,我們把它用空格來替換掉,輸入代碼如下:
#將文本中的<br/>全部去掉
X_train = [doc.replace(b'<br />',b' ') for doc in X_train]
運行這行代碼之后,再打印同一條影評的話,你就會發(fā)現(xiàn)<br />全部被空格替換掉了。
我們再次載入測試集,輸入代碼如下:
#載入測試集
test = load_files('C:\\Users\\1003342\\Desktop\\study\\20190528_sklearn\\datasets\\aclImdb\\test')
X_test,y_test = test.data,test.target
len(X_test)
運行代碼,發(fā)現(xiàn)程序返回的測試集樣本數(shù)100,說明測試集加載成功。同時也把測試集中的
去掉:
#將文本中的<br/>全部去掉
X_test = [doc.replace(b'<br />',b' ') for doc in X_test]
下一步使用CountVectorizer進(jìn)行特征提取:
#轉(zhuǎn)化為向量
from sklearn.feature_extraction.text import CountVectorizer
#使用CountVectorizer擬合訓(xùn)練數(shù)據(jù)
vect = CountVectorizer().fit(X_train)
#將文本轉(zhuǎn)化為向量
X_train_vect = vect.transform(X_train)
#特征數(shù)量
print('訓(xùn)練集樣本特征數(shù)量:{}'.format(len(vect.get_feature_names())))
#打印最后10個訓(xùn)練集樣本特征
print('最后10個訓(xùn)練集樣本特征:{}'.format(vect.get_feature_names()[-10:]))
運行代碼,得到如下圖所示的結(jié)果:

結(jié)果分析:結(jié)果看到,訓(xùn)練集又4000個特征。
下面使用有監(jiān)督學(xué)習(xí)算法進(jìn)行交叉驗證評分,看看模型是否能較好地擬合訓(xùn)練集數(shù)據(jù):
##導(dǎo)入線性SVC分類模型
from sklearn.svm import LinearSVC
# #導(dǎo)入交叉驗證工具
from sklearn.model_selection import cross_val_score
#使用交叉驗證對模型進(jìn)行評分
scores = cross_val_score(LinearSVC(),X_train_vect,y_train)
#打印交叉驗證平均分
print('模型平均分:{:.3f}'.format(scores.mean()))
我們使用LinearSVC算法來進(jìn)行建模,運行代碼,得到如下圖所示的結(jié)果:

結(jié)果分析:從結(jié)果中可以看到模型的平均分是0.778,雖然不是很低,但仍有些差強人意,下一步試試泛化到測試集:
#把測試數(shù)據(jù)集轉(zhuǎn)化為向量
X_test_vect = vect.transform(X_test)
clf = LinearSVC().fit(X_train_vect,y_train)
print('測試集得分:{}'.format(clf.score(X_test_vect,y_test)))
運行代碼,得到結(jié)果如下:
測試集得分:0.58
結(jié)果分析:0.58分并不是很理想,所以再嘗試用tf-idf算法來處理一下數(shù)據(jù)試試:
#導(dǎo)入tfidf轉(zhuǎn)化工具
from sklearn.feature_extraction.text import TfidfTransformer
#使用tfidf工具轉(zhuǎn)化訓(xùn)練集和測試集
tfidf = TfidfTransformer(smooth_idf = False)
tfidf.fit(X_train_vect)
X_train_tfidf = tfidf.transform(X_train_vect)
X_test_tfidf = tfidf.transform(X_test_vect)
print('未經(jīng)tfidf處理的特征:{}'.format(X_train_vect[:5,:5].toarray()))
print('經(jīng)過tfidf處理的特征:{}'.format(X_train_tfidf[:5,:5].toarray()))
運行代碼,得到如下圖所示的結(jié)果:

結(jié)果分析:我們打印了前5個樣本的前5個特征。從結(jié)果可以看到,在未經(jīng)TfidfTransform處理前,CountVectorizer只是計算某個詞在該樣本中某個特征出現(xiàn)的次數(shù),而tf-idf計算的是詞頻乘以逆向文檔頻率,所以是一個浮點數(shù)。
下面試驗一下經(jīng)過處理后的數(shù)據(jù)集訓(xùn)練模型評分:
#重新訓(xùn)練線性svc模型
clf = LinearSVC().fit(X_train_tfidf,y_train)
#使用新數(shù)據(jù)集進(jìn)行交叉驗證
scores2 = cross_val_score(LinearSVC(),X_train_tfidf,y_train)
#打印交新的分?jǐn)?shù)進(jìn)行對比
print('經(jīng)過tf-idf處理的訓(xùn)練集交叉驗證得分:{:.3f}'.format(scores2.mean()))
print('經(jīng)過tf-idf處理的測試集得分:{:.3f}'.format(clf.score(X_test_tfidf,y_test)))
運行代碼,得到如下結(jié)果:

結(jié)果分析:看起來模型的表現(xiàn)并沒有得到提升。接下來繼續(xù)嘗試對模型改進(jìn)——去掉文本中的“停用詞”:
# #導(dǎo)人Tfidf模型
from sklearn.feature_extraction.text import TfidfVectorizer
#激活英語停用詞參數(shù)
tfidf = TfidfVectorizer(smooth_idf = False,stop_words='english')
#擬合訓(xùn)練數(shù)據(jù)集
tfidf.fit(X_train)
#將擬合好的訓(xùn)練數(shù)據(jù)集轉(zhuǎn)為向量
X_train_tfidf = tfidf.transform(X_train)
#使用交叉驗證進(jìn)行評分
scores3 = cross_val_score(LinearSVC(),X_train_tfidf,y_train)
clf.fit(X_train_tfidf,y_train)
#將測試數(shù)據(jù)集轉(zhuǎn)化為向量
X_test_tfidf = tfidf.transform(X_test)
print('去掉停用詞后的訓(xùn)練集交叉驗證平均分:{:.3f}'.format(scores3.mean()))
print('去掉停用詞后的測試集模型得分:{:.3f}'.format(clf.score(X_test_tfidf,y_test)))
運行代碼,得到結(jié)果如下:

結(jié)果分析:從結(jié)果中看到,去掉停用詞之后,模型的得分有了顯著提高。這說明去掉停用詞確實可以讓機器學(xué)習(xí)模型更好地擬合文本數(shù)據(jù)。并且能夠有效提高模型的泛化能力。