文字情感分析(一):基於詞袋模型(VSM、LSA、n-gram)的文字表示

Luv_GEM發表於2019-05-19

現在自然語言處理用深度學習做的比較多,我還沒試過用傳統的監督學習方法做分類器,比如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()

 

第二步:對影評資料做預處理。

大概有以下環節:

  1. 去掉html標籤
  2. 移除標點
  3. 切分成詞/token
  4. 去掉停用詞
  5. 重組為新的句子
"""第二步:資料預處理"""

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章

相關文章