前言
生活中,大多數人會將看電視或看電影作為一種休閑娛樂方式,而在觀看的途中或結束后也會產生相應的評論,這一系列的評論往往代表了評論者當時的情感傾向,下面我們就優酷電視劇《回到明朝當王爺之楊凌傳》的評論進行情感分析。
目錄
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
聲明: 本文僅用作學習交流