自然語言處理(NLP)中一個很重要的研究方向就是語義的情感分析(Sentiment Analysis)。例如IMDB上有很多關於電影的評論,那麼我們就可以通過Sentiment Analysis來評估某部電影的口碑,(如果它才剛剛上映的話)甚至還可以據此預測它是否能夠賣座。與此相類似,國內的豆瓣上也有很多對影視作品或者書籍的評論內容亦可以作為情感分析的語料庫。對於那些電子商務網站而言,針對某一件商品,我們也可以看到留言區裡為數眾多的評價內容,那麼同類商品中,哪個產品最受消費者喜愛呢。或許對商品評論的情感分析可以告訴我們答案。
在之前的文章中,筆者已經向讀者介紹了在Python中利用NLTK進行自然語言處理的一些基本方法: 利用NLTK在Python下進行自然語言處理 Python自然語言處理:詞幹、詞形與MaxMatch演算法
同時,我也介紹了在Python中利用Scikit-Learn進行機器學習,尤其是是利用LogisticRegression進行分類的基本方法: Python機器學習之Logistic迴歸
下面本文將在這些文章的基礎之上,嘗試將機器學習和自然語言處理結合起來,以Tweet文為例,示範進行Sentiment Analysis的基本方法。首先需要說明的是內容有三點:
1)下面的例子仍然主要使用Python中NLTK和Scikit-Learn兩個函數庫。
2)SemEval 是NLP領域的帶有競賽性質的年度盛會,類似KDD-Cup。SemEval 創始於1998年,今年(2016)的活動首頁為http://alt.qcri.org/semeval2016/ , 下面程式中所使用的資料即來自 SemEval 2016 的Task(當然在使用時我們已經完成了基本的預先處理過程,而這並非本文的重點,我們略去不表)。
3)我們所示範的方法,主要目的在於協助大家熟悉Sentiment Analysis的基本內容,深化Scikit-Learn函數庫的使用,而且我們所分析的資料來自於實際資料集,而非類比資料集,所以最終的分析結果並不保證得到非常高的準確率。要得到更高的準確率,需要在模型構建和特徵選取上做更深層次的思考。而這些“思考”已經超出本博文所討論的範圍。
我們原始的資料是一條一條的Tweet,例如: Top 5 most searched for Back-to-School topics -- the list may surprise you http://t.co/Xj21uMVo0p @bing @MSFTnews #backtoschool @Microsoft @taehongmin1 We have an IOT workshop by @Microsoft at 11PM on the Friday - definitely worth going for inspiration! #HackThePlanet
當然,我們同時還擁有一個list of labels,即對每條Tweet的Polarity進行評定的標籤,其中:+1表示positive, -1表示negative,0表示neutral。
在預先處理階段,我對每條Tweet進行了分句和分詞,然後:1)剔除了@***這樣的內容;2)對於#引導的Topic,我們將其視為一個獨立的句子進行處理;3)刪除了由http引導的網路地址;4)統一了大小寫。所以上述兩個Tweet處理之後將得到下面兩個結果
[['top', '5', 'most', 'searched', 'for', 'back', '-', 'to', '-', 'school', 'topics', '--', 'the', 'list', 'may', 'surprise', 'you', '.'], ['back', 'to', 'school', '.']] [['we', 'have', 'an', 'iot', 'workshop', 'by', 'at', '11pm', 'on', 'the', 'friday', '-', 'definitely', 'worth', 'going', 'for', 'inspiration', '!'], ['.'], ['hack', 'the', 'planet', '.']]
然後,我們根據訓練資料集建立一個詞袋(BOW,bag-of-word),這個詞袋是一個字典,裡面儲存著所有訓練資料集中出現過的詞彙,以及它們在全文中出現的頻數。這樣做的目的,在於我們期望剔除那些在全部訓練資料集中極少出現的詞彙(生僻詞),以及那些頻繁出現但毫無意義的詞彙(通常我們稱之為停詞 stop words,例如 the, of, a等)。
在BOW基礎之上,接下來就可以為每條Tweet建立建立 feature dictionaries了。特徵字典是指每條Tweet中出現在BOW中的詞(即剔除了罕見的生僻詞和停詞)以及它們在該條Tweet中出現的頻數構成的字典。 {'-': 2, '--': 1, '.': 2, '5': 1, 'back': 2, 'list': 1, 'may': 1, 'school': 2, 'searched': 1, 'surprise': 1, 'top': 1, 'topics': 1}
{'!': 1, '-': 1, '.': 2, '11pm': 1, 'definitely': 1, 'friday': 1, 'going': 1, 'hack': 1, 'inspiration': 1, 'iot': 1, 'planet': 1, 'workshop': 1, 'worth': 1}
到此為止,所有的預先處理工作都已經完成了。我們得到了一個list of dicts 形式的訓練資料集(以及它對應的list of labels),和一個list of dicts形式的測試資料集(以及它對應的list of labels)。但是現在問題來了,這種形式的資料顯然不能被直接使用。回憶一下我們在前篇介紹Logistic Regression的文章中所使用的鳶尾花資料集的樣子,便不難發現與當前我們所擁有的資料形式大相徑庭。這時就要藉助於Scikit-Learn中提供的特徵提取(Feature Extraction)模組。
The sklearn.feature_extraction module can be used to extract features in a format supported by machine learning algorithms from datasets consisting of formats such as text and image.
更直接的說我們將有藉助的函數是DictVectorizer,The class DictVectorizer can be used to convert feature arrays represented as lists of standard Python dict objects to the NumPy/SciPy representation used by scikit-learn estimators.
如果你對Scikit-Learn文檔中的這些描述感到困惑,那麼下面的例子將讓你很容易理解其作用。首先,我們給出它的定義原型:
class sklearn.feature_extraction.DictVectorizer(dtype=<class'numpy.float64'>, separator='=', sparse=True, sort=True)
其中sparse是一個布爾類型的參數,用於指示是否將結果轉換成scipy.sparse matrices,即疏鬆陣列,預設情況下其賦值為True。
來看一個例子,measurements是一個list of dicts,我們把它轉化成矩陣表示,當對應位置出現某個城市名時,其對應行的那一列就被置為1,否則就是0。
>>> from sklearn.feature_extraction import DictVectorizer>>> measurements = [{'city': 'Dubai', 'temperature': 33.},{'city': 'London', 'temperature': 12.},{'city': 'San Fransisco', 'temperature': 18.},]>>> vec = DictVectorizer()>>> vec.fit_transform(measurements).toarray()array([[ 1., 0., 0., 33.], [ 0., 1., 0., 12.], [ 0., 0., 1., 18.]])>>> vec.get_feature_names()['city=Dubai', 'city=London', 'city=San Fransisco', 'temperature']
再來一個補充例子
>>> measurements = [{'city=Dubai': True, 'city=London': True, 'temperature': 33.},{'city=London': True, 'city=San Fransisco': True, 'temperature': 12.},{'city': 'San Fransisco', 'temperature': 18.},]>>> vec.fit_transform(measurements).toarray()array([[ 1., 1., 0., 33.], [ 0., 1., 1., 12.], [ 0., 0., 1., 18.]])
另外的一個常見問題是訓練資料集和測試資料集的字典大小不一致,此時我們希望短的那個能夠通過補零的方式來追平長的那個。這時就需要使用transform。還是來看例子:
>>> D = [{'foo': 1, 'bar': 2}, {'foo': 3, 'baz': 1}]>>> v = DictVectorizer(sparse=False)>>> X = v.fit_transform(D)>>> Xarray([[ 2., 0., 1.], [ 0., 1., 3.]])>>> v.transform({'foo': 4, 'unseen_feature': 3})array([[ 0., 0., 4.]])>>> v.transform({'foo': 4})array([[ 0., 0., 4.]])
可見當使用transform之後,後面的那個總是可以實現同前面的一個相同的維度。當然這種追平可以是補齊,也可以是刪減,所以通常,我們都是用補齊短的這樣的方式來實現維度一致。如果你不使用transform,而是繼續fit_transform,則會得到下面的結果(這顯然不能滿足我們的要求)
>>> v.fit_transform({'foo': 4, 'unseen_feature': 3})array([[ 4., 3.]])
有了這樣的認識,下面就可以為我們後續的Logistic Regression建立疏鬆陣列了,代碼如下
vec = DictVectorizer()sparse_matrix_tra = vec.fit_transform(feature_dicts_tra)sparse_matrix_dev = vec.transform(feature_dicts_dev)
當然,這裡你還可以用下面的代碼來測試一下他們的維度是否按我們預想的那樣
print(sparse_matrix_dev.shape)print(sparse_matrix_tra.shape)
然後我們就可以利用之前用過的Logistic Regression來建立分類模型了。
from sklearn import linear_modellogreg = linear_model.LogisticRegression(C = 1)logreg.fit(sparse_matrix_tra, labels_t)prediction = logreg.predict(sparse_matrix_dev)print(logreg)print("accuracy score: ")print(accuracy_score(labels_d, prediction))print(classification_report(labels_d, prediction))
一同來看一下該模型對測試集的預測結果
LogisticRegression(C=1, class_weight=None, dual=False, fit_intercept=True, intercept_scaling=1, max_iter=100, multi_class='ovr', n_jobs=1, penalty='l2', random_state=None, solver='liblinear', tol=0.0001, verbose=0, warm_start=False)accuracy score: 0.512848551121 precision recall f1-score support -1 0.41 0.28 0.33 360 0 0.46 0.69 0.55 700 1 0.68 0.46 0.55 769avg / total 0.54 0.51 0.51 1829
該Sentiment分類模型的準確率為51.28%。當然,正如我們前面所說,這個模型顯然還有很大的改進空間。你完全可以通過引入新的feature,或者使用其他機器學習模型(或者調整模型參數)等多種途徑來提升模型的準確率。但是本文旨在示範NLP中的Sentiment Analysis的基本步驟和策略,以及進一步示範利用Scikit Learn進行機器學習的更廣泛的方法(例如基於字典的特徵提取和引入疏鬆陣列)等方面的初衷已經完成了。有興趣的讀者完全可以在此基礎上繼續進行模型最佳化,以期實現更準確的分類能力。