前言
本文使用朴素贝叶斯算法实现 豆瓣Top250电影评价的情感分析与预测。
最近在学习自然语言正负面情感的处理问题,但是绝大部分能搜索到的实践都是Kggle上IMDB影评的情感分析。
所以在这里我就用最基础的朴素贝叶斯算法来对豆瓣的影评进行情感分析与预测。
在这里我参考了 https://github.com/aeternae/IMDb_Review,万分感谢。
朴素贝叶斯分类器
贝叶斯分类是一类分类算法的总称,这类算法均以贝叶斯定理为基础,故统称为贝叶斯分类。
这种算法常用来做文章分类,垃圾邮、件垃圾评论分类,朴素贝叶斯的效果不错并且成本很低。
已知某条件概率,如何得到两个事件交换后的概率,也就是在已知P(A|B)的情况下如何求得P(B|A)。
P(B|A)表示事件A已经发生的前提下,事件B发生的概率,叫做事件A发生下事件B的条件概率。
朴素贝叶斯的公式
P(B∣A)=P(A)P(A∣B)P(B)
一个通俗易懂的视频教程
Youtube https://www.youtube.com/watch?v=AqonCeZUcC4
举个不太恰当的例子
我们想知道做程序员与秃头之间的关系,我们就可以用朴素贝叶斯公式来进行计算。
我们现在想求 P(秃头|做程序员) 的概率, 也就是做程序员就会秃头的概率
我这辈子都不会秃头 (((o(゚▽゚)o))) !!!
代入朴素贝叶斯公式
P(秃头∣做程序员)=P(做程序员)P(做程序员∣秃头)P(秃头)
已知数据如下表
姓名 | 职业 | 是否秃头 |
---|---|---|
奎托斯 | 战神 | 是 |
杀手47号 | 杀手 | 是 |
埼玉 | 超人 | 是 |
灭霸 | 计生办主任 | 是 |
杰森 斯坦森 | 硬汉 | 是 |
某某996程序员 | 程序员 | 是 |
我 | 程序员 | 否 |
基于朴素贝叶斯公式,由以上这张表我们可以求出:
P(秃头∣做程序员)=7261∗76=4221=21
上面这个例子就简单的描述了朴素贝叶斯公式的基本用法。
接下来我就使用豆瓣Top250排行榜的影评来使用朴素贝叶斯进行好评与差评的训练与预测。
豆瓣Top250影评情感分析
首先需要豆瓣Top250影评的语料,我用Scrapy抓取了5w份语料,用于训练与验证。
豆瓣影评爬虫 https://github.com/3inchtime/douban_movie_review
有了语料之后我们就可以开始实际开发了。
这里建议使用jupyter来开发操作。
以下代码全部在我的Github上可以看到,欢迎大家提出建议。
https://github.com/3inchtime/douban_sentiment_analysis
首先加载语料
# -*- coding: utf-8 -*-
import random
import numpy as np
import csv
import jieba
file_path = './data/review.csv'
jieba.load_userdict('./data/userdict.txt')
# 读取保存为csv格式的语料
def load_corpus(corpus_path):
with open(corpus_path, 'r') as f:
reader = csv.reader(f)
rows = [row for row in reader]
review_data = np.array(rows).tolist()
random.shuffle(review_data)
review_list = []
sentiment_list = []
for words in review_data:
review_list.append(words[1])
sentiment_list.append(words[0])
return review_list, sentiment_list
在训练之前,一般均会对数据集做shuffle,打乱数据之间的顺序,让数据随机化,这样可以避免过拟合。所以使用random.shuffle()
方法打乱数据。
jieba.load_userdict('./data/userdict.txt')
这里我自己做了一个词典,防止部分结巴分词的不准确,可以提高约1%左右的准确率。
比如不是很喜欢这句,jieba会分成’不是‘,’很喜欢‘两个词,这样导致这句话很大概率会被预测为好评。
所以这里我在自定义的词典中分好了很多类似这样的词,提高了一点点准确率。
然后将全部的语料按1:4分为测试集与训练集
n = len(review_list) // 5
train_review_list, train_sentiment_list = review_list[n:], sentiment_list[n:]
test_review_list, test_sentiment_list = review_list[:n], sentiment_list[:n]
分词
使用jieba分词,将语料进行分词,并且去除stopwords。
import re
import jieba
stopword_path = './data/stopwords.txt'
def load_stopwords(file_path):
stop_words = []
with open(file_path, encoding='UTF-8') as words:
stop_words.extend([i.strip() for i in words.readlines()])
return stop_words
def review_to_text(review):
stop_words = load_stopwords(stopword_path)
# 去除英文
review = re.sub("[^\u4e00-\u9fa5^a-z^A-Z]", '', review)
review = jieba.cut(review)
# 去掉停用词
if stop_words:
all_stop_words = set(stop_words)
words = [w for w in review if w not in all_stop_words]
return words
# 用于训练的评论
review_train = [' '.join(review_to_text(review)) for review in train_review_list]
# 对于训练评论对应的好评/差评
sentiment_train = train_sentiment_list
# 用于测试的评论
review_test = [' '.join(review_to_text(review)) for review in test_review_list]
# 对于测试评论对应的好评/差评
sentiment_test = test_sentiment_list
TF*IDF与词频向量化
TF-IDF(是一种常用于信息处理和数据挖掘的加权技术。根据词语的在文本中出现的次数和在整个语料中出现的文档频率来计算一个词语在整个语料中的重要程度。
它的优点是能过滤掉一些常见的却无关紧要本的词语,同时保留影响整个文本的重要字词。
使用Countvectorizer()
将一个文档转换为向量,计算词汇在文本中出现的频率。
CountVectorizer
类会将文本中的词语转换为词频矩阵,例如矩阵中包含一个元素a[i] [j],它表示j词在i类文本下的词频。它通过fit_transform函数计算各个词语出现的次数。
TfidfTransformer用于统计vectorizer中每个词语的TF-IDF值。
from sklearn.feature_extraction.text import CountVectorizer, TfidfVectorizer
from sklearn.pipeline import Pipeline
from sklearn.naive_bayes import MultinomialNB
count_vec = CountVectorizer(max_df=0.8, min_df=3)
tfidf_vec = TfidfVectorizer()
# 定义Pipeline对全部步骤的流式化封装和管理,可以很方便地使参数集在新数据集(比如测试集)上被重复使用。
def MNB_Classifier():
return Pipeline([
('count_vec', CountVectorizer()),
('mnb', MultinomialNB())
])
max_df
这个参数的作用是作为一个阈值,当构造语料库的关键词集的时候,如果某个词的词频大于max_df
,这个词不会被当作关键词。
如果这个参数是float,则表示词出现的次数与语料库文档数的百分比,如果是int,则表示词出现的次数。
min_df
类似于max_df
,不同之处在于如果某个词的词频小于min_df
,则这个词不会被当作关键词
这样我们就成功的构造出了用于训练以及测试的Pipeline
然后用 Pipeline.fit()对训练集进行训练
再直接用 Pipeline.score() 对测试集进行预测并评分
mnbc_clf = MNB_Classifier()
# 进行训练
mnbc_clf.fit(review_train, sentiment_train)
# 测试集准确率
print('测试集准确率: {}'.format(mnbc_clf.score(review_test, sentiment_test)))
这样我们就完成了整个从训练到测试的全部流程。
基本上测试集的正确率在79%-80%左右。
因为电影评论中有很大一部分好评中会有负面情感的词语,例如在纪录片《海豚湾》中
我觉得大部分看本片会有感的人,都不知道,中国的白暨豚已经灭绝8年了,也不会知道,长江里的江豚也仅剩1000左右了。与其感慨,咒骂日本人如何捕杀海豚,不如做些实际的事情,保护一下长江里的江豚吧,没几年,也将绝迹了。中国人做出来的事情,也不会比小日本好到哪儿去。
所以说如果将这种类似的好评去除,则可以提高准确率。
保存训练好的模型
# 先转换成词频矩阵,再计算TFIDF值
tfidf = tfidftransformer.fit_transform(vectorizer.fit_transform(review_train))
# 朴素贝叶斯中的多项式分类器
clf = MultinomialNB().fit(tfidf, sentiment_train)
with open(model_export_path, 'wb') as file:
d = {
"clf": clf,
"vectorizer": vectorizer,
"tfidftransformer": tfidftransformer,
}
pickle.dump(d, file)
使用训练好的模型进行影评情感预测
这里我直接贴上全部的源代码,代码非常简单,我将整个处理逻辑封装为一个类,这样就非常方便使用了。
有需要直接可以在我的Github上clone
# -*- coding: utf-8 -*-
import re
import pickle
import numpy as np
import jieba
class SentimentAnalyzer(object):
def __init__(self, model_path, userdict_path, stopword_path):
self.clf = None
self.vectorizer = None
self.tfidftransformer = None
self.model_path = model_path
self.stopword_path = stopword_path
self.userdict_path = userdict_path
self.stop_words = []
self.tokenizer = jieba.Tokenizer()
self.initialize()
# 加载模型
def initialize(self):
with open(self.stopword_path, encoding='UTF-8') as words:
self.stop_words = [i.strip() for i in words.readlines()]
with open(self.model_path, 'rb') as file:
model = pickle.load(file)
self.clf = model['clf']
self.vectorizer = model['vectorizer']
self.tfidftransformer = model['tfidftransformer']
if self.userdict_path:
self.tokenizer.load_userdict(self.userdict_path)
# 过滤文字中的英文与无关文字
def replace_text(self, text):
text = re.sub('((https?|ftp|file)://)?[-A-Za-z0-9+&@#/%?=~_|!:,.;]+[-A-Za-z0-9+&@#/%=~_|].(com|cn)', '', text)
text = text.replace('\u3000', '').replace('\xa0', '').replace('”', '').replace('"', '')
text = text.replace(' ', '').replace('↵', '').replace('\n', '').replace('\r', '').replace('\t', '').replace(')', '')
text_corpus = re.split('[!。?;……;]', text)
return text_corpus
# 情感分析计算
def predict_score(self, text_corpus):
# 分词
docs = [self.__cut_word(sentence) for sentence in text_corpus]
new_tfidf = self.tfidftransformer.transform(self.vectorizer.transform(docs))
predicted = self.clf.predict_proba(new_tfidf)
# 四舍五入,保留三位
result = np.around(predicted, decimals=3)
return result
# jieba分词
def __cut_word(self, sentence):
words = [i for i in self.tokenizer.cut(sentence) if i not in self.stop_words]
result = ' '.join(words)
return result
def analyze(self, text):
text_corpus = self.replace_text(text)
result = self.predict_score(text_corpus)
neg = result[0][0]
pos = result[0][1]
print('差评: {} 好评: {}'.format(neg, pos))
使用时只要实例化这个分析器,并使用analyze()
方法就可以了。
# -*- coding: utf-8 -*-
from native_bayes_sentiment_analyzer import SentimentAnalyzer
model_path = './data/bayes.pkl'
userdict_path = './data/userdict.txt'
stopword_path = './data/stopwords.txt'
corpus_path = './data/review.csv'
analyzer = SentimentAnalyzer(model_path=model_path, stopword_path=stopword_path, userdict_path=userdict_path)
text = '倍感失望的一部诺兰的电影,感觉更像是盗梦帮的一场大杂烩。虽然看之前就知道肯定是一部无法超越前传2的蝙蝠狭,但真心没想到能差到这个地步。节奏的把控的失误和角色的定位模糊绝对是整部影片的硬伤。'
analyzer.analyze(text=text)
https://github.com/3inchtime/douban_sentiment_analysis
以上全部代码均push到了我的Github上,欢迎大家提出建议。