Python做文本情感分析之情感極性分析

導(dǎo)語

「NLP」最為目前及其火熱的一個(gè)領(lǐng)域,已經(jīng)逐漸滲透進(jìn)越來越多產(chǎn)業(yè)的各項(xiàng)業(yè)務(wù)中,不知死活的胖子決定對常用的應(yīng)用功能挨個(gè)進(jìn)行嘗試,死活不論……

0. 介紹

「情感極性分析」是對帶有感情色彩的主觀性文本進(jìn)行分析、處理、歸納和推理的過程。按照處理文本的類別不同,可分為基于新聞評論的情感分析和基于產(chǎn)品評論的情感分析。其中,前者多用于輿情監(jiān)控和信息預(yù)測,后者可幫助用戶了解某一產(chǎn)品在大眾心目中的口碑。
目前常見的情感極性分析方法主要是兩種:基于情感詞典的方法和基于機(jī)器學(xué)習(xí)的方法。

1. 基于情感詞典的文本情感極性分析

筆者是通過情感打分的方式進(jìn)行文本情感極性判斷,score > 0判斷為正向,score < 0判斷為負(fù)向。

1.1 數(shù)據(jù)準(zhǔn)備

1.1.1 情感詞典及對應(yīng)分?jǐn)?shù)

詞典來源于BosonNLP數(shù)據(jù)下載情感詞典,來源于社交媒體文本,所以詞典適用于處理社交媒體的情感分析。

詞典把所有常用詞都打上了唯一分?jǐn)?shù)有許多不足之處。

  • 之一,不帶情感色彩的停用詞會影響文本情感打分。在
  • 之二,由于中文的博大精深,詞性的多變成為了影響模型準(zhǔn)確度的重要原因。
    一種情況是同一個(gè)詞在不同的語境下可以是代表完全相反的情感意義,用筆者模型預(yù)測偏差最大的句子為例(來源于朋友圈文本):
    有車一族都用了這個(gè)寶貝,后果很嚴(yán)重哦[偷笑][偷笑][偷笑]1,交警工資估計(jì)會打5折,沒有超速罰款了[呲牙][呲牙][呲牙]2,移動(dòng)聯(lián)通公司大幅度裁員,電話費(fèi)少了[呲牙][呲牙][呲牙]3,中石化中石油裁員2成,路癡不再迷路,省油[悠閑][悠閑][悠閑]5,保險(xiǎn)公司裁員2成,保費(fèi)折上折2成,全國通用[憨笑][憨笑][憨笑]買不買你自己看著辦吧[調(diào)皮][調(diào)皮][調(diào)皮]
    里面嚴(yán)重等詞都是表達(dá)的相反意思,甚至整句話一起表示相反意思,不知死活的筆者還沒能深入研究如何用詞典的方法解決這類問題,但也許可以用機(jī)器學(xué)習(xí)的方法讓神經(jīng)網(wǎng)絡(luò)進(jìn)行學(xué)習(xí)能夠初步解決這一問題。
    另外,同一個(gè)詞可作多種詞性,那么情感分?jǐn)?shù)也不應(yīng)相同,例如:
    這部電影真垃圾
    垃圾分類
    很明顯在第一句中垃圾表現(xiàn)強(qiáng)烈的貶義,而在第二句中表示中性,單一評分對于這類問題的分類難免有失偏頗。
1.1.2 否定詞詞典

否定詞的出現(xiàn)將直接將句子情感轉(zhuǎn)向相反的方向,而且通常效用是疊加的。常見的否定詞:不、沒、無、非、莫、弗、勿、毋、未、否、別、無、休、難道等。

1.1.3 程度副詞詞典

既是通過打分的方式判斷文本的情感正負(fù),那么分?jǐn)?shù)絕對值的大小則通常表示情感強(qiáng)弱。既涉及到程度強(qiáng)弱的問題,那么程度副詞的引入就是勢在必行的。詞典可從《知網(wǎng)》情感分析用詞語集(beta版)下載。詞典內(nèi)數(shù)據(jù)格式可參考如下格式,即共兩列,第一列為程度副詞,第二列是程度數(shù)值,> 1表示強(qiáng)化情感,< 1表示弱化情感。

程度副詞詞典

1.1.4 停用詞詞典

中科院計(jì)算所中文自然語言處理開放平臺發(fā)布了有1208個(gè)停用詞的中文停用詞表,也有其他不需要積分的下載途徑。

1.2 數(shù)據(jù)預(yù)處理

1.2.1 分詞

即將句子拆分為詞語集合,結(jié)果如下:
e.g. 這樣/的/酒店/配/這樣/的/價(jià)格/還算/不錯(cuò)

Python常用的分詞工具:

  • 結(jié)巴分詞 Jieba
  • Pymmseg-cpp
  • Loso
  • smallseg
from collections import defaultdict
import os
import re
import jieba
import codecs

"""
1. 文本切割
"""

def sent2word(sentence):
    """
    Segment a sentence to words
    Delete stopwords
    """
    segList = jieba.cut(sentence)
    segResult = []
    for w in segList:
        segResult.append(w)

    stopwords = readLines('stop_words.txt')
    newSent = []
    for word in segResult:
        if word in stopwords:
            # print "stopword: %s" % word
            continue
        else:
            newSent.append(word)

    return newSent

在此筆者使用Jieba進(jìn)行分詞。

1.2.2 去除停用詞

遍歷所有語料中的所有詞語,刪除其中的停用詞
e.g. 這樣/的/酒店/配/這樣/的/價(jià)格/還算/不錯(cuò)
--> 酒店/配/價(jià)格/還算/不錯(cuò)

1.3 構(gòu)建模型

1.3.1 將詞語分類并記錄其位置

將句子中各類詞分別存儲并標(biāo)注位置。

"""
2. 情感定位
"""
def classifyWords(wordDict):
    # (1) 情感詞
    senList = readLines('BosonNLP_sentiment_score.txt')
    senDict = defaultdict()
    for s in senList:
        senDict[s.split(' ')[0]] = s.split(' ')[1]
    # (2) 否定詞
    notList = readLines('notDict.txt')
    # (3) 程度副詞
    degreeList = readLines('degreeDict.txt')
    degreeDict = defaultdict()
    for d in degreeList:
        degreeDict[d.split(',')[0]] = d.split(',')[1]
    
    senWord = defaultdict()
    notWord = defaultdict()
    degreeWord = defaultdict()
    
    for word in wordDict.keys():
        if word in senDict.keys() and word not in notList and word not in degreeDict.keys():
            senWord[wordDict[word]] = senDict[word]
        elif word in notList and word not in degreeDict.keys():
            notWord[wordDict[word]] = -1
        elif word in degreeDict.keys():
            degreeWord[wordDict[word]] = degreeDict[word]
    return senWord, notWord, degreeWord
1.3.2 計(jì)算句子得分

在此,簡化的情感分?jǐn)?shù)計(jì)算邏輯:所有情感詞語組的分?jǐn)?shù)之和

定義一個(gè)情感詞語組:兩情感詞之間的所有否定詞和程度副詞與這兩情感詞中的后一情感詞構(gòu)成一個(gè)情感詞組,即notWords + degreeWords + sentiWords,例如不是很交好,其中不是為否定詞,為程度副詞,交好為情感詞,那么這個(gè)情感詞語組的分?jǐn)?shù)為:
finalSentiScore = (-1) ^ 1 * 1.25 * 0.747127733968
其中1指的是一個(gè)否定詞,1.25是程度副詞的數(shù)值,0.747127733968交好的情感分?jǐn)?shù)。

偽代碼如下:
finalSentiScore = (-1) ^ (num of notWords) * degreeNum * sentiScore
finalScore = sum(finalSentiScore)

"""
3. 情感聚合
"""
def scoreSent(senWord, notWord, degreeWord, segResult):
    W = 1
    score = 0
    # 存所有情感詞的位置的列表
    senLoc = senWord.keys()
    notLoc = notWord.keys()
    degreeLoc = degreeWord.keys()
    senloc = -1
    # notloc = -1
    # degreeloc = -1
    
    # 遍歷句中所有單詞segResult,i為單詞絕對位置
    for i in range(0, len(segResult)):
        # 如果該詞為情感詞
        if i in senLoc:
            # loc為情感詞位置列表的序號
            senloc += 1
            # 直接添加該情感詞分?jǐn)?shù)
            score += W * float(senWord[i])
            # print "score = %f" % score
            if senloc < len(senLoc) - 1:
                # 判斷該情感詞與下一情感詞之間是否有否定詞或程度副詞
                # j為絕對位置
                for j in range(senLoc[senloc], senLoc[senloc + 1]):
                    # 如果有否定詞
                    if j in notLoc:
                        W *= -1
                    # 如果有程度副詞
                    elif j in degreeLoc:
                        W *= float(degreeWord[j])
        # i定位至下一個(gè)情感詞
        if senloc < len(senLoc) - 1:
            i = senLoc[senloc + 1]
    return score

1.4 模型評價(jià)

將600多條朋友圈文本的得分排序后做出散點(diǎn)圖:

Score Distribution

其中大多數(shù)文本被判為正向文本符合實(shí)際情況,且絕大多數(shù)文本的情感得分的絕對值在10以內(nèi),這是因?yàn)楣P者在計(jì)算一個(gè)文本的情感得分時(shí),以句號作為一句話結(jié)束的標(biāo)志,在一句話內(nèi),情感詞語組的分?jǐn)?shù)累加,如若一個(gè)文本中含有多句話時(shí),則取其所有句子情感得分的平均值。

然而,這個(gè)模型的缺點(diǎn)與局限性也非常明顯:

  • 首先,段落的得分是其所有句子得分的平均值,這一方法并不符合實(shí)際情況。正如文章中先后段落有重要性大小之分,一個(gè)段落中前后句子也同樣有重要性的差異。
  • 其次,有一類文本使用貶義詞來表示正向意義,這類情況常出現(xiàn)與宣傳文本中,還是那個(gè)例子:
    有車一族都用了這個(gè)寶貝,后果很嚴(yán)重哦[偷笑][偷笑][偷笑]1,交警工資估計(jì)會打5折,沒有超速罰款了[呲牙][呲牙][呲牙]2,移動(dòng)聯(lián)通公司大幅度裁員,電話費(fèi)少了[呲牙][呲牙][呲牙]3,中石化中石油裁員2成,路癡不再迷路,省油[悠閑][悠閑][悠閑]5,保險(xiǎn)公司裁員2成,保費(fèi)折上折2成,全國通用[憨笑][憨笑][憨笑]買不買你自己看著辦吧[調(diào)皮][調(diào)皮][調(diào)皮]2980元軒轅魔鏡帶回家,推廣還有返利[得意]
    Score Distribution中得分小于-10的幾個(gè)文本都是與這類情況相似,這也許需要深度學(xué)習(xí)的方法才能有效解決這類問題,普通機(jī)器學(xué)習(xí)方法也是很難的。
  • 對于正負(fù)向文本的判斷,該算法忽略了很多其他的否定詞、程度副詞和情感詞搭配的情況;用于判斷情感強(qiáng)弱也過于簡單。

總之,這一模型只能用做BENCHMARK...

2. 基于機(jī)器學(xué)習(xí)的文本情感極性分析

2.1 還是數(shù)據(jù)準(zhǔn)備

2.1.1 停用詞

(同1.1.4)

2.1.2 正負(fù)向語料庫

來源于有關(guān)中文情感挖掘的酒店評論語料,其中正向7000條,負(fù)向3000條(筆者是不是可以認(rèn)為這個(gè)世界還是充滿著滿滿的善意呢…),當(dāng)然也可以參考情感分析資源(轉(zhuǎn))使用其他語料作為訓(xùn)練集。

2.1.3 驗(yàn)證集

Amazon上對iPhone 6s的評論,來源已不可考……

2.2 數(shù)據(jù)預(yù)處理

2.2.1 還是要分詞

(同1.2.1)

import numpy as np
import sys
import re
import codecs
import os
import jieba
from gensim.models import word2vec
from sklearn.cross_validation import train_test_split
from sklearn.externals import joblib
from sklearn.preprocessing import scale
from sklearn.svm import SVC
from sklearn.decomposition import PCA
from scipy import stats
from keras.models import Sequential
from keras.layers import Dense, Dropout, Activation
from keras.optimizers import SGD
from sklearn.metrics import f1_score
from bayes_opt import BayesianOptimization as BO
from sklearn.metrics import roc_curve, auc
import matplotlib.pyplot as plt

def parseSent(sentence):
    seg_list = jieba.cut(sentence)
    output = ''.join(list(seg_list)) # use space to join them
    return output
2.2.2 也要去除停用詞

(同1.2.2)

2.2.3 訓(xùn)練詞向量

(重點(diǎn)來了!)模型的輸入需是數(shù)據(jù)元組,那么就需要將每條數(shù)據(jù)的詞語組合轉(zhuǎn)化為一個(gè)數(shù)值向量

常見的轉(zhuǎn)化算法有但不僅限于如下幾種:
(請?jiān)彶恢阑畹呐肿又苯佑谜故镜膒pt截圖作說明,沒錯(cuò),我就是懶,你打我呀)

  • Bag of Words
    Bag of Words
  • TF-IDF
    TF-IDF
  • Word2Vec
    Word2Vec

在此筆者選用Word2Vec將語料轉(zhuǎn)化成向量,具體步驟可參考筆者的文章問答機(jī)器人的Python分類器。

def getWordVecs(wordList):
    vecs = []
    for word in wordList:
        word = word.replace('\n', '')
        try:
            vecs.append(model[word])
        except KeyError:
            continue
    # vecs = np.concatenate(vecs)
    return np.array(vecs, dtype = 'float')


def buildVecs(filename):
    posInput = []
    with open(filename, "rb") as txtfile:
        # print txtfile
        for lines in txtfile:
            lines = lines.split('\n ')
            for line in lines:            
                line = jieba.cut(line)
                resultList = getWordVecs(line)
                # for each sentence, the mean vector of all its vectors is used to represent this sentence
                if len(resultList) != 0:
                    resultArray = sum(np.array(resultList))/len(resultList)
                    posInput.append(resultArray)

    return posInput

# load word2vec model
model = word2vec.Word2Vec.load_word2vec_format("corpus.model.bin", binary = True)
# txtfile = [u'標(biāo)準(zhǔn)間太差房間還不如3星的而且設(shè)施非常陳舊.建議酒店把老的標(biāo)準(zhǔn)間從新改善.', u'在這個(gè)西部小城市能住上這樣的酒店讓我很欣喜,提供的免費(fèi)接機(jī)服務(wù)方便了我的出行,地處市中心,購物很方便。早餐比較豐富,服務(wù)人員很熱情。推薦大家也來試試,我想下次來這里我仍然會住這里']
posInput = buildVecs('pos.txt')
negInput = buildVecs('pos.txt')

# use 1 for positive sentiment, 0 for negative
y = np.concatenate((np.ones(len(posInput)), np.zeros(len(negInput))))

X = posInput[:]
for neg in negInput:
    X.append(neg)
X = np.array(X)
2.2.4 標(biāo)準(zhǔn)化

雖然筆者覺得在這一問題中,標(biāo)準(zhǔn)化對模型的準(zhǔn)確率影響不大,當(dāng)然也可以嘗試其他的標(biāo)準(zhǔn)化的方法。
# standardization
X = scale(X)

2.2.5 降維

根據(jù)PCA結(jié)果,發(fā)現(xiàn)前100維能夠cover 95%以上的variance。


PCA
# PCA
# Plot the PCA spectrum
pca.fit(X)
plt.figure(1, figsize=(4, 3))
plt.clf()
plt.axes([.2, .2, .7, .7])
plt.plot(pca.explained_variance_, linewidth=2)
plt.axis('tight')
plt.xlabel('n_components')
plt.ylabel('explained_variance_')

X_reduced = PCA(n_components = 100).fit_transform(X)

2.3 構(gòu)建模型

2.3.1 SVM (RBF) + PCA

SVM (RBF)分類表現(xiàn)更為寬松,且使用PCA降維后的模型表現(xiàn)有明顯提升,misclassified多為負(fù)向文本被分類為正向文本,其中AUC = 0.92,KSValue = 0.7。
關(guān)于SVM的調(diào)參可以參考筆者的另一篇文章Python利用Gausian Process對Hyper-parameter進(jìn)行調(diào)參

"""
2.1 SVM (RBF)
    using training data with 100 dimensions
"""

clf = SVC(C = 2, probability = True)
clf.fit(X_reduced_train, y_reduced_train)

print 'Test Accuracy: %.2f'% clf.score(X_reduced_test, y_reduced_test)

pred_probas = clf.predict_proba(X_reduced_test)[:,1]
print "KS value: %f" % KSmetric(y_reduced_test, pred_probas)[0]

# plot ROC curve
# AUC = 0.92
# KS = 0.7
fpr,tpr,_ = roc_curve(y_reduced_test, pred_probas)
roc_auc = auc(fpr,tpr)
plt.plot(fpr, tpr, label = 'area = %.2f' % roc_auc)
plt.plot([0, 1], [0, 1], 'k--')
plt.xlim([0.0, 1.0])
plt.ylim([0.0, 1.05])
plt.legend(loc = 'lower right')
plt.show()

joblib.dump(clf, "SVC.pkl")
2.3.2 MLP

MLP相比于SVM (RBF),分類更為嚴(yán)格,PCA降維后對模型準(zhǔn)確率影響不大,misclassified多為正向文本被分類為負(fù)向,其實(shí)是更容易o(hù)verfitting,原因是語料過少,其實(shí)用神經(jīng)網(wǎng)絡(luò)未免有些小題大做,AUC = 0.91。

"""
2.2 MLP
    using original training data with 400 dimensions
"""
model = Sequential()
model.add(Dense(512, input_dim = 400, init = 'uniform', activation = 'tanh'))
model.add(Dropout(0.5))
model.add(Dense(256, activation = 'relu'))
model.add(Dropout(0.5))
model.add(Dense(128, activation = 'relu'))
model.add(Dropout(0.5))
model.add(Dense(64, activation = 'relu'))
model.add(Dropout(0.5))
model.add(Dense(32, activation = 'relu'))
model.add(Dropout(0.5))
model.add(Dense(1, activation = 'sigmoid'))

model.compile(loss = 'binary_crossentropy',
              optimizer = 'adam',
              metrics = ['accuracy'])

model.fit(X_train, y_train, nb_epoch = 20, batch_size = 16)
score = model.evaluate(X_test, y_test, batch_size = 16)
print ('Test accuracy: ', score[1])

pred_probas = model.predict(X_test)
# print "KS value: %f" % KSmetric(y_reduced_test, pred_probas)[0]

# plot ROC curve
# AUC = 0.91
fpr,tpr,_ = roc_curve(y_test, pred_probas)
roc_auc = auc(fpr,tpr)
plt.plot(fpr, tpr, label = 'area = %.2f' % roc_auc)
plt.plot([0, 1], [0, 1], 'k--')
plt.xlim([0.0, 1.0])
plt.ylim([0.0, 1.05])
plt.legend(loc = 'lower right')
plt.show()

2.4 模型評價(jià)

  • 實(shí)際上,第一種方法中的第二點(diǎn)缺點(diǎn)依然存在,但相比于基于詞典的情感分析方法,基于機(jī)器學(xué)習(xí)的方法更為客觀
  • 另外由于訓(xùn)練集和測試集分別來自不同領(lǐng)域,所以有理由認(rèn)為訓(xùn)練集不夠充分,未來可以考慮擴(kuò)充訓(xùn)練集以提升準(zhǔn)確率。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容