前言
生活中,大多数人会将看电视或看电影作为一种休闲娱乐方式,而在观看的途中或结束后也会产生相应的评论,这一系列的评论往往代表了评论者当时的情感倾向,下面我们就优酷电视剧《回到明朝当王爷之杨凌传》的评论进行情感分析。
目录
1. 明确分析目的和思路
2. 收集数据
3. 数据预处理
4 构建模型
5. 情感分析
6. 词云图分析
7. 总结
环境
win8, python3.7, jupyter notebook
正文
在开始之前,我们先来了解清楚什么是情感分析?(以下引用百度百科定义)
情感分析(Sentiment analysis),又称倾向性分析,意见抽取(Opinion extraction),意见挖掘(Opinion mining),情感挖掘(Sentiment mining),主观分析(Subjectivity analysis),它是对带有情感色彩的主观性文本进行分析、处理、归纳和推理的过程,如从评论文本中分析用户对“数码相机”的“变焦、价格、大小、重量、闪光、易用性”等属性的情感倾向。
简单来说, 就是从文本中总结归纳出个人对某一话题的主观态度(褒义或贬义的两种或者更多种类型)。
1. 明确分析目的和思路
分析目的:对优酷电视剧《回答明朝当王爷之杨凌传》的评论进行情感分析
分析思路:1. 通过爬虫爬取优酷上电视剧《回到明朝当王爷之杨凌传》的评论;
2. 对评论进行预处理工作;
3. 在当前情感分析的方法中监督学习是主流, 而朴素贝叶斯模型也常用于情感分析,因此我们选择监督学习中的朴素贝叶斯模型来进行分析建模。
那么, 接下来第一步便是进行数据的收集工作。
2. 收集数据
在数据收集的环节中,我们利用Python中的scrapy框架来完成数据爬取工作。
2.1 目标站点分析
通过对目标站点的分析,找出真正的url,请求方式以及其他信息,限于篇幅,这里不做过多介绍。
2.2 新建爬虫项目
1. 在cmd命令行中执行以下命令, 新建youku项目文件
scrapy startproject yok
2. 进入yok项目中, 新建spider文件
cd yok
scrapy genspider pinglun yok.com
3. 新建main.py文件, 用来执行项目
2.3 定义要爬取的字段
在items.py文件中编辑如下代码
import scrapy from scrapy.item import Item, Field class YoukuItem(Item): content = Field()
2.4 编辑爬虫主程序
在pinglun.py文件中编辑
import scrapy import json from yok.items import YokItem class PinglunSpider(scrapy.Spider): name = 'pinglun' allowed_domains = ['yok.com'] episodes = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13] def start_requests(self): for ep in self.episodes: url = 'https://p.Id={0}&Page=1&pageSize=30'.format(str(ep)) yield scrapy.Request(url=url, callback=self.parse_pages, meta={'ep':ep}, dont_filter=True) def parse_pages(self,response): '''获取总页数''' ep, results= response.meta['ep'], response.text results = results.replace('n_commentList(', '').replace(')', '') results = json.loads(results, strict=False) if results.get('code') == 0: pages = results.get('data').get('totalPage') for page in range(1, int(pages)+1): url = 'https://p.Id={0}Page={1}&pageSize=30'.format(str(ep), str(page)) yield scrapy.Request(url=url, callback=self.parse, dont_filter=True) def parse(self, response): '''解析内容''' results = response.text results= results.replace('n_commentList(', '').replace(')', '') #strict表示对json语法要求不严格 results = json.loads(results, strict = False) item = YoukuItem() if results.get('data'): if results.get('data').get('comment'): comments = results.get('data').get('comment') for comment in comments: item['content'] =comment.get('content').strip().replace(r'\n','')yield item
2.5 保存到Mysql数据库
在pipelines.py中编辑
import pymysql class YokPipeline(object): def __init__(self): self.db = pymysql.connect( host='localhost', user='root', password='1234', db='yok', port=3306, charset='utf8' ) self.cursor = self.db.cursor() def process_item(self, item, spider): sql = "INSERT INTO pinglun(content) values(%s)" data = (item["content"]) try: self.cursor.execute(sql, data) self.db.commit() except: print('存储失败') self.db.rollback() def closed_spider(self, spider): self.cursor.close() self.db.close()
2.6 配置settings
BOT_NAME = 'yok' SPIDER_MODULES = ['yok.spiders'] NEWSPIDER_MODULE = 'yok.spiders' USER_AGENT = 'Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.106 Safari/537.36' ROBOTSTXT_OBEY = False DOWNLOAD_DELAY = 2 COOKIES_ENABLED = False DEFAULT_REQUEST_HEADERS = { ':authority': 'p.comments.yok.com', ':method': 'GET',':scheme': 'https', 'accept': '*/*', 'accept-encoding': 'gzip, deflate, br', 'accept-language': 'zh-CN,zh;q=0.9', 'referer': 'https://www.yok.com/id.html' } ITEM_PIPELINES = { 'yok.pipelines.YokPipeline': 300, }
2.7 执行程序
在main.py中编辑
from scrapy import cmdline cmdline.execute('scrapy crawl pinglun'.split())
最终爬取到1万5千多条数据.
需要注意的是:我们爬取到的数据是无标签的,但朴素贝叶斯模型是有监督的学习,因此我这之后又爬取了豆瓣上同类型电视剧评论,该评论中是有标签的,即将问题转化为情感分类问题,也就是通过这些标签数据来进行训练模型,进而对无标签数据进行情感分类。
3. 数据预处理
3.1 从数据库中读取数据
这里读取的是有标签的电视剧评论
import pandas as pd import pymysql #连接数据库 conn = pymysql.connect(host='localhost', user='root', password='1234', port=3306, db='dn', charset='utf8') sql = 'select * from pinglun' #读取数据 df = pd.read_sql(sql, conn) #删除重复记录 df.drop_duplicates(inplace = True) #设置行的最大显示为2条 pd.set_option('display.max_rows', 2)
df
3.2 对数据进行分类
我们依据评分进行情感分类, 大于3分为积极情感, 小于3分为消极情感, 积极情感用1表示, 消极情感用0表示.
import numpy as np #转换数据类型 df.score = df.score.astype(int) teleplay_0 = df[df.score <3] #通过比对,发现消极情感的总数较少,随机抽取同数量的积极情感 teleplay_1 = df[df.score > 3].sample(n=teleplay_0.shape[0]) #对两者进行拼接 teleplay = pd.concat([teleplay_1, teleplay_0], axis=0) #对情感进行分类, 0消极,1积极 teleplay['emotion'] = np.where(teleplay.score > 3, 1, 0) teleplay
3.3 分词处理
我这里采用两种库, 一个jieba, 一个是snownlp
1. 安装两个库
pip install jieba
pip install snownlp
2. 对评论进行分词
from snownlp import SnowNLP import jieba #对评论进行分词, 并以空格隔开 teleplay['cut_jieba'] = teleplay.content.apply(lambda x: ' '.join(jieba.cut(x))) teleplay['cut_snownlp'] = teleplay.content.apply(lambda x: ' '.join(SnowNLP(x).words)) teleplay
可以看到两者的分词结果还是有区别的
3.4 停用词处理
什么是停用词? 我们把"看到", "和", "的", "基本"等这类可忽略的词汇, 称为停用词. 它们的存在反而影响处理效率, 因此将它们除去.
def get_stopwords(path): '''读取停用词''' with open(path) as f: stopwords = [i.strip() for i in f.readlines()] return stopwords path = r'D:\stopword.txt' stopwords = get_stopwords(path) #分别去除cut_jieba和cut_snownlp中的停用词 teleplay['cut_jieba'] = teleplay.cut_jieba.apply(lambda x: ' '.join([w for w in (x.split(' ')) if w not in stopwords])) teleplay['cut_snownlp'] = teleplay.cut_snownlp.apply(lambda x: ' '.join([w for w in (x.split(' ')) if w not in stopwords])) teleplay
3.5 特征向量化
特征向量化的目的就是将mX1的矩阵转化为mXn的矩阵(其中1表示1维文本, n表示1维文本中的n个词汇), 分别计算n个特征词汇在m行中出现的频数
1. 在特征向量化之前按照2:8的比例将数据集随机划分为训练集和测试集.
from sklearn.model_selection import train_test_split #分别对cut_jieba和cut_snownlp进行划分 X1, X2, y = teleplay[['cut_jieba']], teleplay[['cut_snownlp']], teleplay.emotion X1_train, X1_test, y1_train, y1_test = train_test_split(X1, y, test_size=0.2) X2_train, X2_test, y2_train, y2_test = train_test_split(X2, y, test_size=0.2)
2. 特征向量化(只对训练集进行向量化)
from sklearn.feature_extraction.text import CountVectorizer #初始化 vect = CountVectorizer() #分别对cut_jieba和cut_snownlp进行向量化, 并转化为dataframe. vect_matrix_1 = pd.DataFrame(vect.fit_transform(X1_train.cut_jieba).toarray(), columns = vect.get_feature_names()) vect_matrix_2 = pd.DataFrame(vect.fit_transform(X2_train.cut_snownlp).toarray(), columns = vect.get_feature_names()) vect_matrix_1
vect_matrix_2
可以看到向量化的结果中存在05, 07, 08...等数字, 而且这些数字对于分类的结果无太大作用, 需要剔除, 可以选择在向量化之前借助正则表达式进行剔除, 下面直接在CountVectorizer中筛选
#max_df的类型若为int, 则表示过滤掉文档中出现次数大于max_df的词汇, 若为float时, 则是百分比; min_df类似, token_pattern表示token的正则表达式 vect = CountVectorizer(max_df = 0.8, min_df = 2, token_pattern = r"(?u)\b[^\d\W]\w+\b") vect_matrix_1 = pd.DataFrame(vect.fit_transform(X1_train.cut_jieba).toarray(), columns = vect.get_feature_names()) vect_matrix_2 = pd.DataFrame(vect.fit_transform(X2_train.cut_snownlp).toarray(), columns = vect.get_feature_names()) vect_matrix_1 vect_matrix_2
两种分词方式经过同样的处理之后均过滤掉一半以上.
4. 构建模型
1. 构建朴素贝叶斯模型与交叉验证
from sklearn.pipeline import make_pipeline from sklearn.naive_bayes import MultinomialNB from sklearn.model_selection import cross_val_score #初始化朴素贝叶斯模型 nb = MultinomialNB() #利用pipeline管道进行特征向量化 pipe = make_pipeline(vect, nb) #交叉验证,将训练集分成10份,即计算10次,在每次计算中用10份中的一份当作验证集,对应的另外9份用作训练集,最后对10次计算出的准确率求平均值 score1 = cross_val_score(pipe, X1_train.cut_jieba, y1_train, cv=10, scoring='accuracy').mean() score2 = cross_val_score(pipe, X2_train.cut_snownlp, y2_train, cv=10, scoring='accuracy').mean() score1 score2
0.8002292107510046
0.7559003695080964
在训练集上, jieba分词后的模型的准确率比snownlp能高些
2. 模型评估
from sklearn import metrics #训练出模型 pipe1= pipe.fit(X1_train.cut_jieba, y1_train)
pipe2= pipe.fit(X2_train.cut_jieba, y2_train) #用训练的模型分别预测cut_jieba和cut_snownlp的测试集 y1_pre = pipe1.predict(X1_test.cut_jieba) y2_pre = pipe2.predict(X2_test.cut_snownlp) #评价模型预测结果 metrics.accuracy_score(y1_test, y1_pre) metrics.accuracy_score(y2_test, y2_pre)
0.8071278825995807
0.7631027253668763
在测试集上, jieba同样比snownlp的准确率高, 对于分类问题通常用混淆矩阵中的精准度和召回率进行评价.
1) 混淆矩阵
1和0表示真实值(1和0代表两个类别, 非数字意义), P和N表示预测值, T表示预测正确, F则错误
准确率: (TP+TN)/(1+0)
灵敏度: TP/1
精准度: TP/(TP+FP)
召回率: TP/(TP+FN)等价于灵敏度
2) 在朴素贝叶斯怎么才能得到混淆矩阵?
#分别求得cut_jieba和cut_snownlp的混淆矩阵 con_matrix1 = metrics.confusion_matrix(y1_test, y1_pre) con_matrix2 = metrics.confusion_matrix(y2_test, y2_pre) #分别计算精准率和召回率 accu_rate1, accu_rate2= con_matrix1[0][0]/(con_matrix1[1][0]+con_matrix1[0][0]), con_matrix2[0][0]/(con_matrix2[1][0]+con_matrix2[0][0]) recall_rate1, recall_rate2 = con_matrix1[0][0]/(con_matrix1[0][1]+con_matrix1[0][0]), con_matrix2[0][0]/(con_matrix2[0][1]+con_matrix2[0][0]) print('jieba_confused_matrix:') print(con_matrix1) print('accurate_rate:{0}, recall_rate:{1}'.format(accu_rate1, recall_rate1)) print() print('snownlp_confused_matrix:') print(con_matrix2) print('accurate_rate:{0}, recall_rate:{1}'.format(accu_rate2, recall_rate2))
jieba_confused_matrix: [[351 130] [ 54 419]] accurate_rate:0.8666666666666667, recall_rate:0.7297297297297297 snownlp_confused_matrix: [[353 146] [ 80 375]] accurate_rate:0.815242494226328, recall_rate:0.7074148296593187
在精准度和召回率上jieba同样表现更佳, 也从另一个层面上说明了特征的重要性, snownlp还有情感分类功能, 不妨看看结果如何
3) snownlp情感分类
y_pred_snow = X1_test.cut_jieba.apply(lambda x: SnowNLP(x).sentiments) y_pred_snow= np.where(y_pred_snow > 0.5, 1, 0) metrics.accuracy_score(y1_test, y_pred_snow) metrics.confusion_matrix(y1_test, y_pred_snow)
0.640461215932914
array([[179, 299], [ 44, 432]], dtype=int64)
可以看到在电视剧的评论上准确率不高, 但是精确率还是很高的: 0.80, 召回率: 0.37
下面将利用训练出的模型对新数据集进行情感分类
5. 情感分析
通过以上可以看到通过jieba分词后训练的朴素贝叶斯模型还是相当不错的, 那么现在就用该模型对新数据集进行分类
#读取数据, 删除重复项以及jieba分词 conn = pymysql.connect(host = 'localhost', user='root', password = '1234', port=3306,db='yok', charset='utf8') sql = 'select * from pinglun' yk = pd.read_sql(sql, conn) yk.drop_duplicates(inplace=True) yk['cut_content'] = yk.content.apply(lambda x: ' '.join(jieba.cut(x))) #用模型进行情感分类 yk['emotion'] = pipe1.predict(yk.cut_content) #计算积极情感占比 yk[yk.emotion_nb == 1].shape[0]/yk.shape[0]
0.5522761760242793
另外根据模型在测试集的精准度0.87(预测结果为1有87%真实值也是1)和召回率0.73(真实值为1有73%预测值为1), 因此根据召回率推算出真实情感为1的占0.55/0.73=0.75,也就是说利用朴素贝叶斯模型进行预测的话,结果偏向于积极情感。下面不妨再利用snownlp对情感进行分类
#利用snownlp进行情感分类 emotion_pred = yk.cut_content.apply(lambda x: SnowNLP(x).sentiments) yk['emotion_snow'] = np.where(emotion_pred > 0.5, 1, 0) #计算积极情感占比 yk[yk.emotion_snow == 1].shape[0]/yk.shape[0]
0.6330804248861912
使用snownlp进行情感分析,结果同样显示为积极,但是与朴素贝叶斯模型相差12个百分点,但是鉴于是两个模型在训练集上的表现,认为朴素贝叶斯模型更为可靠。‘’
6. 词云图分析
import matplotlib.pyplot as plt from wordcloud import WordCloud bg_image = plt.imread(r'tig.jpg') wc = WordCloud(width=1080, #设置宽度 height=840, #设置高度 background_color='white', #背景颜色 mask=bg_image, #背景图 font_path='STKAITI.TTF', #中文字体 stopwords=stopwords, #停用词 max_font_size=400, #字体最大值 random_state=50 #随机配色方案 ) wc.generate_from_text(' '.join(yk.cut_content)) #生成词云 plt.imshow(wc) # 绘制图像 plt.axis('off') # 关闭坐标轴显示 wc.to_file('pig.jpg') plt.show()
通过词云图,评论主要围绕“穿越”、“好看”、“小说”、“剧情”等词汇,可以看出该电视剧类型为穿越剧,如果只看“好看”这个词汇,说明当时评论者带有的是积极的情感,但是如果与“原著”、“没有”等词相组合的话,则会表现出一定的消极情感,又由于“没有”等否定词比“好看”小一个等级,因此总体来说,评论的情感偏向于积极。
总结
1. snownlp情感词典结果为0.63,朴素贝叶斯模型结果为0.75,即情感分析结果为积极,且朴素贝叶斯模型的精准度和召回率分别为0.87和0.73,表现相当不错。
2. 词云图总体显示出的是积极情感,但是从中也可以看到一些消极情绪,比如“原著”、“乱七八糟”等,又由于该电视剧是由小说改编而来的,因此建议在改编前对原著进行分析,包括原著的优势和不足、大家对原著的情感和原著所处的历史环境,尽量避免去修改原著的优势,应当在符合当时历史环境的状态下去弥补原著的不足之处,让剧本更符合大众的口味。
============================================================================================================================================================================
添加内容
1. 由于在建模之前, 我特意控制两个类别的总数相等, 那要是不相等(所有数据), 结果如何呢? 对比如下:
两个类别总数不相等(所有数据): 两个类别总数相等(随机抽取):
训练集上结果: 0.8204944800769489 0.8002292107510046
测试集上结果: 0.8154613466334164 0.8071278825995807
混淆矩阵: [[311, 166], [[351 130],
[56, 670]] [[54 419]]
精准度: 0.8474114441416893 0.8666666666666667
召回率: 0.6519916142557652 0.7297297297297297
利用所有数据, 也就相当于是扩充数据集, 在训练集和测试集上准确率均有提升, 但仅仅是扩充其中一个类别的数据量, 对于召回率和精准度来说, 均有不同程度下降, 且召回率下降更明显, 达到7个百分点. 猜想: 数据集中各类别数目是否相等, 对模型的好坏是有影响的.
2. 在博主jclian91的建议之下, 用TF-IDF方法进行特征向量化, 其中TF表示词频, 词频即文本中出现某词的次数/总词数; IDF表示逆文本频率, lg(总文本数/出现某词的文本数), 也就是说出现某词的文本数越大, 对应的结果越小, 当某词在所有文本中都出现时, 结果为0, 也就是说该词不能用来区分文本.
tf-idf 模型的主要思想是:如果词w在一篇文档d中出现的频率高,并且在其他文档中很少出现,则认为词w具有很好的区分能力,适合用来把文章d和其他文章区分开来
由于之前关闭了jupyter notebook, 重启之后需要重新运行(就会重新随机抽取数据), 因此我这里就直接在所有数据的基础上用TF-IDF方法进行特征向量化.
1) 与CountVectorizer参数一致
from sklearn.feature_extraction.text import TfidfVectorizer from sklearn import metrics vect = TfidfVectorizer(max_df = 0.8, min_df = 2, token_pattern = r"(?u)\b[^\d\W]\w+\b") nb = MultinomialNB() pipe = make_pipeline(vect, nb) #训练模型 pipe.fit(X_train.cut_jieba, y_train) #对测试数据进行预测 y_pre = pipe.predict(X_test.cut_jieba) #训练数据集分数 print(cross_val_score(pipe, X_train.cut_jieba, y_train, cv=10, scoring='accuracy').mean()) #测试数据分数 print(metrics.accuracy_score(y_test, y_pre)) #混淆矩阵 con_matrix = metrics.confusion_matrix(y_test, y_pre) #精准度 con_matrix[0][0]/(con_matrix[1][0]+con_matrix[0][0]) #召回率 con_matrix[0][0]/(con_matrix[0][1]+con_matrix[0][0])
0.8069697070993753
0.8088113050706567
[[273 204]
[ 26 700]]
0.9130434782608695
0.5723270440251572
对比CountVectorizer的结果, 除了精准度有所提升, 其余均有不同程度的降低, 既然换了方法, 参数应当也有所变化.
2) 改变参数
可看出TF-IDF方法在精准度上表现更佳, 如果应用场景对精准度要求较高适宜该方法.
以上便是对学习过程的总结, 若出现错误, 还望指正, 谢谢!
参考:
http://www.cnblogs.com/mxp-neu/articles/5316989.html
https://www.jianshu.com/p/29aa3ad63f9d
以上便是我本次分享的内容,如有任何疑问,请在下方留言,或在公众号【转行学数据分析】联系我!!!