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

导语

「NLP」最为目前及其火热的一个领域,已经逐渐渗透进越来越多产业的各项业务中,不知死活的胖子决定对常用的应用功能挨个进行尝试,死活不论……

0. 介绍

「情感极性分析」是对带有感情色彩的主观性文本进行分析、处理、归纳和推理的过程。按照处理文本的类别不同,可分为基于新闻评论的情感分析和基于产品评论的情感分析。其中,前者多用于舆情监控和信息预测,后者可帮助用户了解某一产品在大众心目中的口碑。
目前常见的情感极性分析方法主要是两种:基于情感词典的方法和基于机器学习的方法。

1. 基于情感词典的文本情感极性分析

笔者是通过情感打分的方式进行文本情感极性判断,score > 0判断为正向,score < 0判断为负向。

1.1 数据准备

1.1.1 情感词典及对应分数

词典来源于BosonNLP数据下载情感词典,来源于社交媒体文本,所以词典适用于处理社交媒体的情感分析。

词典把所有常用词都打上了唯一分数有许多不足之处。

  • 之一,不带情感色彩的停用词会影响文本情感打分。在
  • 之二,由于中文的博大精深,词性的多变成为了影响模型准确度的重要原因。
    一种情况是同一个词在不同的语境下可以是代表完全相反的情感意义,用笔者模型预测偏差最大的句子为例(来源于朋友圈文本):
    有车一族都用了这个宝贝,后果很严重哦[偷笑][偷笑][偷笑]1,交警工资估计会打5折,没有超速罚款了[呲牙][呲牙][呲牙]2,移动联通公司大幅度裁员,电话费少了[呲牙][呲牙][呲牙]3,中石化中石油裁员2成,路痴不再迷路,省油[悠闲][悠闲][悠闲]5,保险公司裁员2成,保费折上折2成,全国通用[憨笑][憨笑][憨笑]买不买你自己看着办吧[调皮][调皮][调皮]
    里面严重等词都是表达的相反意思,甚至整句话一起表示相反意思,不知死活的笔者还没能深入研究如何用词典的方法解决这类问题,但也许可以用机器学习的方法让神经网络进行学习能够初步解决这一问题。
    另外,同一个词可作多种词性,那么情感分数也不应相同,例如:
    这部电影真垃圾
    垃圾分类
    很明显在第一句中垃圾表现强烈的贬义,而在第二句中表示中性,单一评分对于这类问题的分类难免有失偏颇。
1.1.2 否定词词典

否定词的出现将直接将句子情感转向相反的方向,而且通常效用是叠加的。常见的否定词:不、没、无、非、莫、弗、勿、毋、未、否、别、無、休、难道等。

1.1.3 程度副词词典

既是通过打分的方式判断文本的情感正负,那么分数绝对值的大小则通常表示情感强弱。既涉及到程度强弱的问题,那么程度副词的引入就是势在必行的。词典可从《知网》情感分析用词语集(beta版)下载。词典内数据格式可参考如下格式,即共两列,第一列为程度副词,第二列是程度数值,> 1表示强化情感,< 1表示弱化情感。

程度副词词典

1.1.4 停用词词典

中科院计算所中文自然语言处理开放平台发布了有1208个停用词的中文停用词表,也有其他不需要积分的下载途径

1.2 数据预处理

1.2.1 分词

即将句子拆分为词语集合,结果如下:
e.g. 这样/的/酒店/配/这样/的/价格/还算/不错

Python常用的分词工具:

  • 结巴分词 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进行分词。

1.2.2 去除停用词

遍历所有语料中的所有词语,删除其中的停用词
e.g. 这样/的/酒店/配/这样/的/价格/还算/不错
--> 酒店/配/价格/还算/不错

1.3 构建模型

1.3.1 将词语分类并记录其位置

将句子中各类词分别存储并标注位置。

"""
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 计算句子得分

在此,简化的情感分数计算逻辑:所有情感词语组的分数之和

定义一个情感词语组:两情感词之间的所有否定词和程度副词与这两情感词中的后一情感词构成一个情感词组,即notWords + degreeWords + sentiWords,例如不是很交好,其中不是为否定词,为程度副词,交好为情感词,那么这个情感词语组的分数为:
finalSentiScore = (-1) ^ 1 * 1.25 * 0.747127733968
其中1指的是一个否定词,1.25是程度副词的数值,0.747127733968交好的情感分数。

伪代码如下:
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
            # 直接添加该情感词分数
            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定位至下一个情感词
        if senloc < len(senLoc) - 1:
            i = senLoc[senloc + 1]
    return score

1.4 模型评价

将600多条朋友圈文本的得分排序后做出散点图:

Score Distribution

其中大多数文本被判为正向文本符合实际情况,且绝大多数文本的情感得分的绝对值在10以内,这是因为笔者在计算一个文本的情感得分时,以句号作为一句话结束的标志,在一句话内,情感词语组的分数累加,如若一个文本中含有多句话时,则取其所有句子情感得分的平均值

然而,这个模型的缺点与局限性也非常明显:

  • 首先,段落的得分是其所有句子得分的平均值,这一方法并不符合实际情况。正如文章中先后段落有重要性大小之分,一个段落中前后句子也同样有重要性的差异。
  • 其次,有一类文本使用贬义词来表示正向意义,这类情况常出现与宣传文本中,还是那个例子:
    有车一族都用了这个宝贝,后果很严重哦[偷笑][偷笑][偷笑]1,交警工资估计会打5折,没有超速罚款了[呲牙][呲牙][呲牙]2,移动联通公司大幅度裁员,电话费少了[呲牙][呲牙][呲牙]3,中石化中石油裁员2成,路痴不再迷路,省油[悠闲][悠闲][悠闲]5,保险公司裁员2成,保费折上折2成,全国通用[憨笑][憨笑][憨笑]买不买你自己看着办吧[调皮][调皮][调皮]2980元轩辕魔镜带回家,推广还有返利[得意]
    Score Distribution中得分小于-10的几个文本都是与这类情况相似,这也许需要深度学习的方法才能有效解决这类问题,普通机器学习方法也是很难的。
  • 对于正负向文本的判断,该算法忽略了很多其他的否定词、程度副词和情感词搭配的情况;用于判断情感强弱也过于简单。

总之,这一模型只能用做BENCHMARK...

2. 基于机器学习的文本情感极性分析

2.1 还是数据准备

2.1.1 停用词

(同1.1.4)

2.1.2 正负向语料库

来源于有关中文情感挖掘的酒店评论语料,其中正向7000条,负向3000条(笔者是不是可以认为这个世界还是充满着满满的善意呢…),当然也可以参考情感分析资源(转)使用其他语料作为训练集。

2.1.3 验证集

Amazon上对iPhone 6s的评论,来源已不可考……

2.2 数据预处理

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 训练词向量

(重点来了!)模型的输入需是数据元组,那么就需要将每条数据的词语组合转化为一个数值向量

常见的转化算法有但不仅限于如下几种:
(请原谅不知死活的胖子直接用展示的ppt截图作说明,没错,我就是懒,你打我呀)

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

在此笔者选用Word2Vec将语料转化成向量,具体步骤可参考笔者的文章问答机器人的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'标准间太差房间还不如3星的而且设施非常陈旧.建议酒店把老的标准间从新改善.', u'在这个西部小城市能住上这样的酒店让我很欣喜,提供的免费接机服务方便了我的出行,地处市中心,购物很方便。早餐比较丰富,服务人员很热情。推荐大家也来试试,我想下次来这里我仍然会住这里']
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 标准化

虽然笔者觉得在这一问题中,标准化对模型的准确率影响不大,当然也可以尝试其他的标准化的方法。
# standardization
X = scale(X)

2.2.5 降维

根据PCA结果,发现前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 构建模型

2.3.1 SVM (RBF) + PCA

SVM (RBF)分类表现更为宽松,且使用PCA降维后的模型表现有明显提升,misclassified多为负向文本被分类为正向文本,其中AUC = 0.92KSValue = 0.7
关于SVM的调参可以参考笔者的另一篇文章Python利用Gausian Process对Hyper-parameter进行调参

"""
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),分类更为严格,PCA降维后对模型准确率影响不大,misclassified多为正向文本被分类为负向,其实是更容易overfitting,原因是语料过少,其实用神经网络未免有些小题大做,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 模型评价

  • 实际上,第一种方法中的第二点缺点依然存在,但相比于基于词典的情感分析方法,基于机器学习的方法更为客观
  • 另外由于训练集和测试集分别来自不同领域,所以有理由认为训练集不够充分,未来可以考虑扩充训练集以提升准确率。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,311评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,339评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,671评论 0 342
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,252评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,253评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,031评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,340评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,973评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,466评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,937评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,039评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,701评论 4 323
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,254评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,259评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,485评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,497评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,786评论 2 345

推荐阅读更多精彩内容