現在自然語言處理用深度學習做的比較多,我還沒試過用傳統的監督學習方法做分類器,比如SVM、Xgboost、隨機森林,來訓練模型。因此,用Kaggle上經典的電影評論情感分析題,來學習如何用傳統機器學習方法解決分類問題。
通過這個情感分析的題目,我會整理做特徵工程、引數調優和模型融合的方法,這一系列會有四篇文章。這篇文章整理文字特徵工程的內容。
文字的特徵工程主要包括資料清洗、特徵構造、降維和特徵選擇等。
首先是資料清洗,比如去停用詞、去非字母漢字的特殊字元、大寫轉小寫、去掉html標籤等。
然後是特徵構建,可以基於詞袋模型構造文字特徵,比如向量空間模型的詞頻矩陣、Tf-Idf矩陣,又比如LSA和LDA,也可以用word2vec、glove等文字分散式表示方法,來構造文字特徵。此外還可以用n-gram構造文字特徵。
接下來可以選擇是否降維,可以用PCA或SVD等方法對文字特徵矩陣進行降維。
最後選擇效果比較突出的1個或幾個特徵來訓練模型。
一、基於向量空間模型的文字特徵表示
向量空間模型(Vector Space Model,VSM)也就是單詞向量空間模型,區別於LSA、PLSA、LDA這些話題向量空間模型,但是單詞向量空間模型和話題向量空間模型都屬於詞袋模型,又和word2vec等文字分散式表示方法相區別。
向量空間模型的基本想法是:給定一個文字,用一個向量表示該文字的語義,向量的每一維對應一個單詞,其數值是該單詞在該文字中出現的頻數或Tf-Idf。那麼每個文字就是一個向量,特徵數量為所有文字中的單詞總數,通過計算向量之間的餘弦相似度可以得到文字的相似度。
而文字集合中的所有文字的向量就會構成一個單詞-文字矩陣,元素為單詞的頻數或Tf-Idf。
在我們這個Kaggle案例中,單詞-文字矩陣的行數為樣本的數量,列數為單詞的數量,訓練集中樣本有25000條,選取最高頻的5000個單詞,故矩陣X是(25000,5000)的矩陣。我們以詞頻和Tf-Idf作為文字特徵,計算出兩個單詞-文字矩陣,然後分別訓練隨機森林二分類器。
首先匯入所需要的庫。
import os,re import numpy as np import pandas as pd from bs4 import BeautifulSoup from sklearn.feature_extraction.text import CountVectorizer from sklearn.feature_extraction.text import TfidfVectorizer from sklearn.ensemble import RandomForestClassifier from sklearn import metrics import nltk from nltk.corpus import stopwords
第一步:讀取訓練資料,一共25000條評論資料。
"""第一步:用pandas讀取訓練資料""" datafile = os.path.join('..', 'data', 'labeledTrainData.tsv') # escapechar='\\'用來去掉轉義字元'\' df = pd.read_csv(datafile, sep='\t', escapechar='\\') print('Number of reviews: {}'.format(len(df))) df.head()
第二步:對影評資料做預處理。
大概有以下環節:
- 去掉html標籤
- 移除標點
- 切分成詞/token
- 去掉停用詞
- 重組為新的句子
"""第二步:資料預處理""" eng_stopwords = stopwords.words('english') # 去掉html標籤 # 去掉非英文字元 # 去停用詞 # 重新組合為句子 def clean_text(text): text = BeautifulSoup(text, 'html.parser').get_text() text = re.sub(r'[^a-zA-Z]', ' ', text) words = text.lower().split() words = [w for w in words if w not in eng_stopwords] return ' '.join(words) df['clean_review'] = df.review.apply(clean_text) df.head()
第三步:用向量空間模型抽取文字特徵
分別計算單詞的詞頻和Tf-Idf,作為文字特徵,計算單詞-文字矩陣。
"""第三步:用VSM抽取文字特徵""" # 統計詞頻,作為文字特徵,計算文字-單詞矩陣 vectorizer_freq = CountVectorizer(max_features = 5000) train_vsm_freq = vectorizer_freq.fit_transform(df.clean_review).toarray() print("以詞頻為元素的文字-單詞矩陣的維度是:\n\n",train_vsm_freq.shape) # 計算tfidf,作為另一種文字特徵,計算文字-單詞矩陣 vectorizer_tfidf=TfidfVectorizer(max_features=5000) train_vsm_tfidf=vectorizer_tfidf.fit_transform(df.clean_review).toarray() print("\n用單詞向量空間模型成功抽取文字特徵!\n")
以詞頻為元素的文字-單詞矩陣的維度是: (25000, 5000) 用單詞向量空間模型成功抽取文字特徵!
第四步:訓練分類器
決策樹為200棵。
"""第四步:用隨機森林訓練二分類器""" """首先使用以詞頻為元素的文字-單詞矩陣訓練一個分類器""" # 使用包外估計作為模型泛化誤差的估計,即oob_score=True,那麼無須再做交叉驗證 forest = RandomForestClassifier(oob_score=True,n_estimators = 200) forest = forest.fit(train_vsm_freq, df.sentiment)
第五步:評估分類器的效能
這裡沒有進行交叉驗證了,沒有劃分驗證集來計算準確率、召回率和AUC,直接用訓練集來計算這些指標。因為隨機森林通過自助取樣,可以得到大約36.8%的驗證集,用於評估模型的泛化誤差,稱這為包外估計,因此我們主要觀察包外估計這個指標,來評估分類器的效能。
從評估結果來看,包外估計為0.84232。
"""第五步:評估模型""" def model_eval(train_data): print("1、混淆矩陣為:\n") print(metrics.confusion_matrix(df.sentiment, forest.predict(train_data))) print("\n2、準確率、召回率和F1值為:\n") print(metrics.classification_report(df.sentiment,forest.predict(train_data))) print("\n3、包外估計為:\n") print(forest.oob_score_) print("\n4、AUC Score為:\n") y_predprob = forest.predict_proba(train_data)[:,1] print(metrics.roc_auc_score(df.sentiment, y_predprob)) print("\n====================評估以詞頻為特徵訓練的模型==================\n") model_eval(train_vsm_freq)
第六步:以Tf-Idf作為文字特徵,訓練分類器
結果包外估計為0.84168,比詞頻矩陣的要低一點,問題不大。
"""再使用以tfidf為元素的文字-單詞矩陣訓練一個分類器""" forest = RandomForestClassifier(oob_score=True,n_estimators = 200) forest = forest.fit(train_vsm_tfidf, df.sentiment) print("\n====================評估以tfidf為特徵訓練的模型==================\n") model_eval(train_vsm_tfidf)
二、基於潛在語義分析的文字特徵表示
潛在語義分析(Laten Semantic Analysis,LSA)是一種文字話題分析的方法,特點是可以通過矩陣分解(SVD或者NMF),來發現文字與單詞之間的基於話題的語義關係。
LSA和VSM有什麼關係呢?
1、VSM的優點是單詞向量稀疏,計算效率高,但是由於自然語言中一詞多義和多詞一義現象的存在,基於單詞向量的文字表示未必能準確表達兩個文字的相似度。而LSA是用文字的話題來表示文字,文字的話題相似則文字的語義也相似,這樣可以解決同義詞和多義詞的問題。
2、VSM得到的是單詞-文字矩陣,而LSA得到的是話題-文字矩陣。LSA的話題-文字矩陣就是通過對VSM的矩陣進行矩陣分解得到的,矩陣分解的方法包括SVD奇異值分解和NMF非負矩陣分解。
如下圖,NMF非負矩陣分解後得到話題-文字矩陣Y,話題為k個,樣本為n個。
在這個情感分析案例中,我們把話題設為300個,把單詞-文字矩陣降維成(25000, 300)的話題-文字矩陣,採用的是NMF非負矩陣分解的方法。
要注意的一點是,如果單詞-文字矩陣的維度是(單詞數,文字數)這種格式,也就是我們在程式碼中用到的格式,那麼在用 sklearn.decomposition.NMF 這個包計算話題-文字矩陣時,NMF().fit_transform() 所得的是話題-文字矩陣,NMF().components_得到的是單詞-話題矩陣。
而如果單詞-文字矩陣的格式和上圖的格式一樣(轉置了),那麼NMF().components_得到的是話題-文字矩陣。
下面首先用NMF計算LSA的話題-文字矩陣,取以詞頻為元素的單詞-文字矩陣來計算。對高維矩陣進行矩陣分解的時間複雜度非常高,所以我用一下LSA就好了,LDA就不敢再去嘗試,因為LDA的時間複雜度更高,效果可能不一定好。
第一步:用NMF計算LSA的話題-文字矩陣
"""用NMF計算LSA的話題-文字矩陣""" from sklearn.decomposition import NMF # 對以詞頻為特徵的單詞-文字矩陣進行NMF分解 nmf = NMF(n_components=300) # 得到話題-文字矩陣,注意如果輸入進行了轉置,那麼得到的是單詞-話題矩陣 train_lsa_freq = nmf.fit_transform(train_vsm_freq) print("話題-文字矩陣的維度是:\n\n",train_lsa_freq.shape)
話題-文字矩陣的維度是:
(25000, 300)
第二步:使用LSA的話題-文字矩陣訓練隨機森林分類器
包外估計為0.82236,比基於VSM的效果要差2個百分點左右,畢竟特徵維度降低了。本來想把話題設定為500,也就是把特徵維度降到500維,可是計算時間太恐怖了,久久得不到結果。
這真是費力不討好。
"""再使用LSA的話題-文字矩陣訓練一個分類器""" forest = RandomForestClassifier(oob_score=True,n_estimators = 200) forest = forest.fit(train_lsa_freq, df.sentiment) print("\n====================評估以LSA為特徵訓練的模型==================\n") model_eval(train_lsa_freq)
三、用n-gram做文字表示
n-gram的意思比較簡單了,沒啥好說的。我這裡讓n-gram=(2,2),也就是每個單元都是兩個單片語合而成的。
"""使用sklearn計算2-gram,得到詞語-文字矩陣""" # token_pattern的作用是,出現"bi-gram"、"two:three"這種時,可以切成"bi gram"、"two three"的形式 vectorizer_2gram = CountVectorizer(ngram_range=(2,2),token_pattern=r'\b\w+\b',max_features=5000) train_vsm_2gram = vectorizer_2gram.fit_transform(df.clean_review).toarray() print("2-gram構成的語料庫中前10個元素為:\n") print(vectorizer_2gram.get_feature_names()[:10])
2-gram構成的語料庫中前10個元素為: ['able get', 'able make', 'able see', 'able watch', 'absolute worst', 'absolutely brilliant',
'absolutely hilarious', 'absolutely love', 'absolutely loved', 'absolutely nothing']
接著再訓練隨機森林分類器,並評估模型。不知道怎麼回事,跑了很久,死活收斂不了,有毒。。。算了,放著讓它跑,我也再想想到底咋回事。
"""使用以2-gram的詞頻為元素的單詞-文字矩陣訓練一個分類器""" forest = RandomForestClassifier(oob_score=True,n_estimators = 200) forest = forest.fit(train_vsm_2gram, df.sentiment) print("\n====================評估以2-gram為特徵訓練的模型==================\n") model_eval(train_vsm_2gram)
最後還是重新訓練第一個模型,也就是用詞頻作為特徵的單詞-文字矩陣,訓練隨機森林分類器。我們讀取測試資料進行預測,並儲存預測結果。
# 刪除不用的佔內容變數 #del df #del train_vsm_freq # 讀取測試資料 datafile = os.path.join('..', 'data', 'testData.tsv') df = pd.read_csv(datafile, sep='\t', escapechar='\\') print('Number of reviews: {}'.format(len(df))) df['clean_review'] = df.review.apply(clean_text) vectorizer = CountVectorizer(max_features = 5000) test_data_features = vectorizer.fit_transform(df.clean_review).toarray() result = forest.predict(test_data_features) output = pd.DataFrame({'id':df.id, 'sentiment':result}) # 儲存 output.to_csv(os.path.join('..', 'data', 'Bag_of_Words_model.csv'), index=False)
參考資料:
李航:《統計學習方法》(第二版) 第17章