从互联网某电商平台抓取手机的中文评论内容。然后对中文评论进行分词处理。为了区分评论中的好评和差评,我们使用支持向量机模型,和word2vec模型来对数据进行建模,并分析模型的输出结果。
任何行业领域,用户对产品的评价都显得尤为重要。通过用户评论,可以对用户情感倾向进行判定。例如目前最为普遍的网购行为:对于用户来说,参考评论可以做出更优的购买决策;对于商家来说,对商品评论按照情感倾向进行分类,并通过文本聚类得到普遍提及的商品优缺点,可以进一步改良产品。本案例主要讨论如何对商品评论进行情感倾向判定。下图为某电商平台上针对某款手机的评论:
由于数据集包含中文评论,我们需要设置编码为utf-8
。
import sys
# 设置编码utf-8,并保持stdin,stdout,stderr正常输出。
stdi, stdo, stde = sys.stdin, sys.stdout, sys.stderr
reload(sys)
sys.setdefaultencoding('utf-8')
sys.stdin, sys.stdout, sys.stderr = stdi, stdo, stde
import jieba, numpy, pandas, sklearn, gensim, wordcloud, matplotlib, logging
print 'jieba %s' % jieba.__version__
print 'gensim %s' % gensim.__version__
print 'numpy %s' % numpy.__version__
print 'pandas %s' % pandas.__version__
print 'sklearn %s' % sklearn.__version__
print 'wordcloud %s' % wordcloud.__version__
print 'matplotlib %s' % matplotlib.__version__
print 'logging %s' % logging.__version__
这份某款手机的商品评论信息数据集,包含2个属性,共计8187个样本。
列名 | 说明 | 类型 | 示例 |
---|---|---|---|
对该款手机的评论 | 客服特别不负责,明明备注了也不看,发错了东西。 | ||
该评论的情感倾向: 0 ----- 中评 1 ------ 好评 |
使用Pandas
中的read_excel
函数读取xls
格式的数据集文件,注意文件的编码设置为gb18030
。
import pandas as pd
#读入数据集
data = pd.read_excel("./input/data.xls", encoding='gb18030')
data.head()
查看数据集的相关信息,包括行列数,列名,以及各个类别的样本数。
# 数据集的大小
data.shape
# 数据集的列名
data.columns.values
# 不同类别数据记录的统计
data['Class'].value_counts()
现在,我们要将Comment
列的文本信息,转化成数值矩阵表示,也就是将文本映射到特征空间。首先,通过jieba
,使用HMM
模型,对文本进行中文分词。
# 导入中文分词库jieba
import jieba
import numpy as np
# 对数据集的每个样本的文本进行中文分词,如遇到缺失值,使用“还行 一般吧”进行填充
cutted = []
for row in data.values:
try:
raw_words = (" ".join(jieba.cut(row[0])))
cutted.append(raw_words)
except AttributeError:
print row[0]
cutted.append(u"还行 一般吧")
cutted_array = np.array(cutted)
# 生成新数据文件,Comment字段为分词后的内容
data_cutted = pd.DataFrame({
'Comment': cutted_array,
'Class': data['Class']
})
data_cutted.head()
为了更直观地观察词频高的词语,我们使用第三方库wordcloud
进行文本的可视化。
# 导入第三方库wordcloud
from wordcloud import WordCloud
import matplotlib.pyplot as plt
针对好评,中评和差评的文本,建立WordCloud
对象,绘制词云。
# 好评
wc = WordCloud(font_path='./input/KaiTi_GB2312.ttf')
wc.generate(''.join(data_cutted['Comment'][data_cutted['Class'] == 1]))
fig = plt.figure(figsize = (10, 10))
plt.axis('off')
plt.imshow(wc)
plt.show()
# 中评
wc = WordCloud(font_path='./input/KaiTi_GB2312.ttf')
wc.generate(''.join(data_cutted['Comment'][data_cutted['Class'] == 0]))
fig = plt.figure(figsize = (10, 10))
plt.axis('off')
plt.imshow(wc)
plt.show()
# 差评
wc = WordCloud(font_path='./input/KaiTi_GB2312.ttf')
wc.generate(''.join(data_cutted['Comment'][data_cutted['Class'] == -1]))
fig = plt.figure(figsize = (10, 10))
plt.axis('off')
plt.imshow(wc)
plt.show()
从词云展现的词频统计图来看,"手机","就是","屏幕","收到"等词对于区分毫无帮助而且会造成偏差。因此,需要把这些对区分类没有意义的词语筛选出来,放到停用词文件stopwords.txt
中。
# 读入停用词文件
import codecs
with codecs.open('./input/stopwords.txt', 'r', encoding='utf-8') as f:
stopwords = [item.strip() for item in f]
for item in stopwords[0:200]:
print item,
使用jieba
库的extract_tags
函数,统计好评,中评,差评文本中的top 20
关键词
#设定停用词文件,在统计关键词的时候,过滤停用词
import jieba.analyse
jieba.analyse.set_stop_words('./input/stopwords.txt')
# 好评关键词
keywords_pos = jieba.analyse.extract_tags(''.join(data_cutted['Comment'][data_cutted['Class'] == 1]), topK=20)
for item in keywords_pos:
print item,
#中评关键词
keywords_med = jieba.analyse.extract_tags(''.join(data_cutted['Comment'][data_cutted['Class'] == 0]), topK=20)
for item in keywords_med:
print item,
#差评关键词
keywords_neg = jieba.analyse.extract_tags(''.join(data_cutted['Comment'][data_cutted['Class'] == -1]), topK=20)
for item in keywords_neg:
print item,
经过以上步骤的处理,整个数据集的预处理工作“告一段落”。在中文文本分析和情感分析的工作中,数据预处理的内容主要是分词。只有经过分词处理后的文本数据集才可以进行下一步的向量化操作,满足输入模型的条件。
经过分词之后的文本数据集要先进行向量化之后才能输入到分类模型中进行运算。
我们使用sklearn
库实现向量化方法,去掉停用词,并将其通过tf
,tf-idf
映射到特征空间。
$\text{tf-idf} = (1 + \log \text{tf})\cdot \log \dfrac{\text{N}}{\text{df}}$
其中,$\text{tf}$为词频,即分词后每个词项在该条评论中出现的次数;$\text{df}$为出现该词项评论数目;$\text{N}$为评论总数,使用对数来适当抑制$\text{tf}$和$\text{df}$值的影响。
向量化方法 | 0/1模型 | TF模型 | TF-IDF模型 |
---|---|---|---|
数字代码 | 0 | 1 | 2 |
我们使用sklearn
库中的函数直接实现SVM算法。在这里,我们选取以下形式的SVM模型参与运算。
分类模型 | SVC | LinearSVC | SGDClassifier |
---|---|---|---|
数字代码 | 1 | 2 | 3 |
为了方便,创建文本情感分析类CommentClassifier
,来实现建模过程:
__init__
为类的初始化函数,输入参数classifier_type
和vector_type
,分别代表分类模型的类型和向量化方法的类型。
fit()
函数,来实现向量化与模型建立的过程。
# 实现向量化方法
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.feature_extraction.text import CountVectorizer
#实现svm和贝叶斯模型
from sklearn.svm import SVC
from sklearn.svm import LinearSVC
from sklearn.linear_model import SGDClassifier
# 实现交叉验证
from sklearn.cross_validation import train_test_split
from sklearn.cross_validation import cross_val_score
# 实现评价指标
from sklearn import metrics
# 文本情感分类的类:CommentClassifier
class CommentClassifier:
def __init__(self, classifier_type, vector_type):
self.classifier_type = classifier_type #分类器类型:支持向量机或贝叶斯分类
self.vector_type = vector_type #文本向量化模型:0\1模型,TF模型,TF-IDF模型
def fit(self, train_x, train_y, max_df):
list_text = list(train_x)
#向量化方法:0 - 0/1,1 - TF,2 - TF-IDF
if self.vector_type == 0:
self.vectorizer = CountVectorizer(max_df, stop_words = stopwords, ngram_range=(1, 3)).fit(list_text)
elif self.vector_type == 1:
self.vectorizer = TfidfVectorizer(max_df, stop_words = stopwords, ngram_range=(1, 3), use_idf=False).fit(list_text)
else:
self.vectorizer = TfidfVectorizer(max_df, stop_words = stopwords, ngram_range=(1, 3)).fit(list_text)
self.array_trainx = self.vectorizer.transform(list_text)
self.array_trainy = train_y
#分类模型选择:1 - SVC,2 - LinearSVC,3 - SGDClassifier,三种SVM模型
if self.classifier_type == 1:
self.model = SVC(kernel='linear', gamma=10 ** -5, C=1).fit(self.array_trainx, self.array_trainy)
elif self.classifier_type == 2:
self.model = LinearSVC().fit(self.array_trainx, self.array_trainy)
else:
self.model = SGDClassifier().fit(self.array_trainx, self.array_trainy)
def predict_value(self, test_x):
list_text = list(test_x)
self.array_testx = self.vectorizer.transform(list_text)
array_predict = self.model.predict(self.array_testx)
return array_predict
def predict_proba(self, test_x):
list_text = list(test_x)
self.array_testx = self.vectorizer.transform(list_text)
array_score = self.model.predict_proba(self.array_testx)
return array_score
使用train_test_split()
函数划分训练集和测试集。训练集:80%;测试集:20%。
建立classifier_type
和vector_type
两个参数的取值列表,来表示选择的向量化方法以及分类模型
输出每种向量化方法和分类模型的组合所对应的分类评价结果,内容包括混淆矩阵以及含Precision
、Recall
和F1-score
三个指标的评分矩阵
#划分训练集,测试集
train_x, test_x, train_y, test_y = train_test_split(data_cutted['Comment'].ravel().astype('U'), data_cutted['Class'].ravel(),
test_size=0.2, random_state=4)
classifier_list = [1,2,3]
vector_list = [0,1,2]
for classifier_type in classifier_list:
for vector_type in vector_list:
commentCls = CommentClassifier(classifier_type, vector_type)
#max_df 设置为0.98
commentCls.fit(train_x, train_y, 0.98)
if classifier_type == 0:
value_result = commentCls.predict_value(test_x)
proba_result = commentCls.predict_proba(test_x)
print classifier_type,vector_type
print 'classification report'
print metrics.classification_report(test_y, value_result, labels=[-1, 0, 1])
print 'confusion matrix'
print metrics.confusion_matrix(test_y, value_result, labels=[-1, 0, 1])
else:
value_result = commentCls.predict_value(test_x)
print classifier_type,vector_type
print 'classification report'
print metrics.classification_report(test_y, value_result, labels=[-1, 0, 1])
print 'confusion matrix'
print metrics.confusion_matrix(test_y, value_result, labels=[-1, 0, 1])
从结果上来看,选择tfidf
向量化方法,使用LinearSVC
模型效果比较好,f1-socre
为0.73
从混淆矩阵来看,我们会发现多数的错误分类都出现在中评和差评上。我们可以将原始数据集的中评删除。
data_bi = data_cutted[data_cutted['Class'] != 0]
data_bi['Class'].value_counts()
再次运行分类模型,查看分类结果。
train_x, test_x, train_y, test_y = train_test_split(data_bi['Comment'].ravel().astype('U'), data_bi['Class'].ravel(),
test_size=0.2, random_state=4)
classifier_list = [1,2,3]
vector_list = [0,1,2]
for classifier_type in classifier_list:
for vector_type in vector_list:
commentCls = CommentClassifier(classifier_type, vector_type)
commentCls.fit(train_x, train_y,0.98)
if classifier_type == 0:
value_result = commentCls.predict_value(test_x)
proba_result = commentCls.predict_proba(test_x)
print classifier_type,vector_type
print 'classification report'
print metrics.classification_report(test_y, value_result, labels=[-1, 1])
print 'confusion matrix'
print metrics.confusion_matrix(test_y, value_result, labels=[-1, 1])
else:
value_result = commentCls.predict_value(test_x)
print classifier_type,vector_type
print 'classification report'
print metrics.classification_report(test_y, value_result, labels=[-1, 1])
print 'confusion matrix'
print metrics.confusion_matrix(test_y, value_result, labels=[-1, 1])
删除差评之后,不同组合的分类模型效果均有显著提升。这也说明,分类模型能够有效地将好评区分出来。
数据集中存在标注不准确的问题,主要集中在中评。由于人在评论时,除非有问题否则一般都会打好评,如果打了中评说明对产品有不满意之处,在情感的表达上就会趋向于负向情感,同时评论具有很大主观性,很多中评会将其归为差评,但数据集中却认为是中评。因此,将一条评论分类为好评、中评、差评是不够客观,中评与差评之间的边界很模糊,因此识别率很难提高。
开源文本向量化工具word2vec,可以为文本数据寻求更加深层次的特征表示。词语之间可以进行运算:
w2v(woman)-w2v(man)+w2v(king)=w2v(queen)
基于word2vec的doc2vec,将每个文档表示为一个向量,并且通过余弦距离可以计算两个文档的相似程度,那么就可以计算一句话和一句极好的好评的距离,以及一句话到极差的差评的距离。
在本案例的数据集中:
好评:快 就是 手感 满意 也好 喜欢 也 流畅 很 服务态度 实用 超快 挺快 用着 速度 礼品 也不错 非常好 挺好 感觉 才来 还行 好看 也快 不错的 送了 非常不错 超级 赞 好多东西 很实用 各方面 挺好的 很多 漂亮 配件 还不错 也多 特意 慢 满分 好用 非常漂亮......
差评:不多说 上当 差差 刚用 服务差 一点也不 不要 简直 还是去 实体店 大家 保证 不肯 生气 开发票 磨损 后悔 印记 网 什么破 烂烂 左边 失效 太 骗 掉价 走下坡路 不说了 彻底 三星手机 自营 几次 真心 别的 看完 简单说 机会 这是 生气了 触动 缝隙 冲动了 失望......
我们使用第三方库gensim
来实现doc2vec
模型。
import pandas as pd
from gensim.models import Doc2Vec
from gensim.models.doc2vec import TaggedDocument
import logging
logging.basicConfig(format='%(asctime)s : %(levelname)s : %(message)s', level=logging.INFO)
train_x = data_bi['Comment'].ravel()
train_y = data_bi['Class'].ravel()
#为train_x列贴上标签"TRAIN"
def labelizeReviews(reviews, label_type):
labelized = []
for i, v in enumerate(reviews):
label = '%s_%s' % (label_type, i)
labelized.append(TaggedDocument(v.split(" "), [label]))
return labelized
train_x = labelizeReviews(train_x, "TRAIN")
#建立Doc2Vec模型model
size = 300
all_data = []
all_data.extend(train_x)
model = Doc2Vec(min_count=1, window=8, size=size, sample=1e-4, negative=5, hs=0, iter=5, workers=8)
model.build_vocab(all_data)
# 设置迭代次数10
for epoch in range(10):
model.train(train_x)
#建立空列表pos和neg以对相似度计算结果进行存储,计算每个评论和极好评论之间的余弦距离,并存在pos列表中
#计算每个评论和极差评论之间的余弦距离,并存在neg列表中
pos = []
neg = []
for i in range(0,len(train_x)):
pos.append(model.docvecs.similarity("TRAIN_0","TRAIN_{}".format(i)))
neg.append(model.docvecs.similarity("TRAIN_1","TRAIN_{}".format(i)))
#将pos列表和neg列表更新到原始数据文件中,分别表示为字段PosSim和字段NegSim
data_bi[u'PosSim'] = pos
data_bi[u'NegSim'] = neg
data_bi.head()
from matplotlib import pyplot as plt
label= data_bi['Class'].ravel()
values = data_bi[['PosSim' , 'NegSim']].values
plt.scatter(values[:,0], values[:,1], c=label, alpha=0.4)
plt.show()
从上图中可以看到,好评与差评基本上可以通过一条直线区分开(紫色为差评,黄色为好评)
该方法与传统思路完全不同,没有使用词频率,情感词等特征,其优点有: