Sentiment Analysis with Rules and Machine Learning Tech
作者:禅与计算机程序设计艺术
1.简介
随着互联网的发展,社交媒体成为大众日益关注的热点话题。随之而来的就是对用户的评价、评论等产生了强烈的兴趣。 sentiment analysis 是文本处理的一项重要任务,它可以帮助我们对用户对产品或服务的满意程度进行分析。传统的sentiment analysis 方法通过统计和规则的方法去识别用户的情感倾向,但这些方法往往无法捕捉到一些细微的情感变化。因此,基于机器学习技术的sentiment analysis 也成为一种新的热点研究方向。本文将介绍如何利用python实现基于规则的方法和机器学习的方法对用户的评论进行情感分析。
2.基本概念和术语说明
情感分类
情感分类(sentiment classification)是一个比较常用的文本分类任务,主要目的是根据给定的输入文本确定其情感极性类别(positive/negative)。常见的情感类型包括积极、消极、中性、褒贬等。以下是一些常见的情感词汇及其对应的情感类型:
积极: “这部电影真的很好看!” 消极: “这部电影太差劲了!” 中性: “下雨天在家看书还是要带伞的!” 褒贬: “这个苹果真的很不错!”
情感类型越多,分类效果就越精确。目前已有的基于规则的情感分类方法主要通过人工设计的正则表达式进行匹配。然而,这种方式缺乏灵活性,且对新出现的句式和表达可能会失效。机器学习方法的出现让基于规则的方法有望与机器学习相结合,提升模型的泛化能力和适应性。
规则方法
规则方法是指基于已知的规则,对用户的评论进行情感分析。常见的规则包括正面或负面词、感叹号数量、是否含有冠词等。例如,对于“这个苹果真的很不错!”这样的句子,可以定义规则如下:
- 如果句子中包含“非常”或者“绝对”,那么情感极性为正。
- 如果句子中包含“不”或者“差”关键字,那么情感极性为负。
- 如果句子中没有以上两种关键词,但是存在感叹号,那么情感极性为正。
这种方式简单易懂,也容易被人理解和掌握。但是,由于规则过于简单,无法刻画出复杂情绪的特征,且往往存在规则漏洞,因此这种方法的准确率较低。
机器学习方法
机器学习方法又称为 statistical learning 或 artificial intelligence (AI)方法,是在海量数据中训练出一个模型,使得该模型能够预测或者推断出输入数据的分类标签。常见的机器学习算法包括 Naive Bayes、SVM、Decision Tree 和 Random Forest 等。
下面介绍常用的两种机器学习方法——基于规则的机器学习方法和基于统计的机器学习方法。
基于规则的机器学习方法
基于规则的机器学习方法的基本思路是先训练一个规则的分类器,然后用分类器对每一条评论进行情感分析。规则的选择和特征的设计需要通过实验来优化。
一种常用的基于规则的情感分析方法叫做 Naive Bayes。Naive Bayes 是一种简单而有效的概率分类方法。它的思想是假设特征之间彼此独立,即特征A与特征B发生关系的概率仅取决于特征A的值,而与其他特征无关。换言之,当特征A=a时,特征B=b的概率只与特征A有关,与特征C无关。这样就可以将每条评论转换成一组特征,并使用贝叶斯定理计算每个分类的概率。具体过程如下图所示。
如上图所示,评论“这个苹果真的很不错!”的特征是“这个”、“苹果”、“很”、“不错”四个单词。假设特征之间相互独立,那么情感极性为正的概率是多少呢? 根据贝叶斯定理,P(X|Y=y) = P(x_1|y)P(x_2|y)...P(xn|y)(P(Y=y)/P(Y))。这里,P(x_i|y)表示第i个特征在分类y下的概率,可由训练集估计获得;P(Y=y)/P(Y)是prior probability,表示分类为y的概率,通常可以使用Laplace平滑法进行处理;而P(x_1|y),...,P(xn|y)则通过朴素贝叶斯假设直接计算得到。 基于规则的机器学习方法的优点是速度快,但缺点是易受噪声影响,且无法捕捉到微小的情绪变化。
基于统计的机器学习方法
另一种机器学习方法是基于统计的方法。这种方法不需要事先设计复杂的规则,而是直接利用统计规律和数据的特征来对用户的评论进行情感分析。这种方法通常会先将评论进行分词、标记化、情感标注,然后利用统计方法建模生成情感模型。
常用的基于统计的情感分析方法包括 Support Vector Machines (SVMs),Logistic Regression 和 Neural Networks。这些方法的基本思路是通过训练样本建立模型,使模型能够判断新的输入数据属于哪一类。
SVM
Support Vector Machine,简称SVM,是一种二类分类算法。SVM 的基本思路是找到一个超平面,使得两个类别的样本之间的间隔最大化。具体地,先找出一组支持向量(support vectors),即与超平面的距离最近的样本点。然后再求解超平面参数,使得支持向量处于两侧,而其他样本点都在边界上。
如上图所示,SVM将原始数据通过超平面映射到另一个空间,使得两类样本之间的距离最大化。接着,SVM求解最优超平面参数,使得支持向量处于两侧,其他样本点都在边界上。最终,SVM给定新的输入数据,通过核函数将其映射回原始空间,然后判断其属于哪一类。
Logistic Regression
Logistic Regression 是一种用于二元分类的线性模型。它的基本思路是对数据拟合一个Sigmoid函数,使得输出值在[0,1]范围内。具体地,首先根据输入变量与输出变量之间的关系拟合一条直线,然后在此基础上加入一个Sigmoid函数作为分类器。Sigmoid 函数的曲线形状类似于阶梯形状,并且是单调递增函数。
如上图所示,Logistic Regression 用Sigmoid函数对输入数据进行分类,其中,z = wx+b, w 是权重参数,b 是偏置参数,分类器通过训练样本来决定直线的参数值。
Neural Network
Neural Networks,简称NN,是一种多层感知机(Multi-layer Perceptron,MLP)网络,是一种非线性模型。它将输入信号通过隐藏层,经过一系列变换后输出结果。
如上图所示,NN 通过多个全连接层(fully connected layer)连接输入和输出,中间可能还包括激活函数、归一化、dropout 等操作。通过反向传播训练神经网络,使得模型能够更好地拟合训练数据中的复杂关系。
以上三种机器学习方法都是对用户的评论进行情感分析。不同于规则方法,它们可以通过统计模型来捕捉到各种情绪变化。另外,除了机器学习方法外,还有一些其他的方法,比如利用 Deep Learning 技术来构建多级分类器。不过,由于篇幅限制,只能介绍以上三个机器学习方法,其它方法待相关文献补充。
3.核心算法原理和具体操作步骤以及数学公式讲解
数据获取
本文使用的数据集为 Rotten Tomatoes Movie Reviews Dataset v1.0。该数据集包含了约 38,000 个来自 IMDb 的电影评论,共有 10 个不同的种类,分别是 Negative、Positive、Neutral、Fresh、Rotten、Very Positive、Fairly Positive、Mostly Negative 和 Very Negative。每个评论都有一个对应得分,范围为 0 到 100 分。
首先,导入必要的包:
import pandas as pd # 数据处理包
import numpy as np # 科学计算包
from sklearn.model_selection import train_test_split # 将数据集分割为训练集和测试集
from collections import Counter # 词频统计包
import re # 正则表达式包
from nltk.tokenize import word_tokenize # 分词包
import string # 字符串处理包
import math # 数学计算包
import random # 随机数生成包
from nltk.corpus import stopwords # 停用词包
from scipy.sparse import coo_matrix # 稀疏矩阵包
from sklearn.naive_bayes import MultinomialNB # 朴素贝叶斯分类器
from sklearn.feature_extraction.text import TfidfTransformer # TF-IDF Transformer
from sklearn.pipeline import Pipeline # 模型管道
from sklearn.linear_model import LogisticRegression # 逻辑回归分类器
from sklearn.svm import LinearSVC # 线性 SVM 分类器
from sklearn.neural_network import MLPClassifier # 神经网络分类器
from sklearn.metrics import accuracy_score # 准确率计算器
from sklearn.preprocessing import StandardScaler # 标准化包
from sklearn.preprocessing import MinMaxScaler # 最小最大缩放包
from imdb_data import load_data # 从本地加载数据集
代码解读
然后,调用 load_data 函数从本地加载数据集:
reviews, labels = load_data()
print("Number of reviews:", len(reviews))
print("Number of classes:", len(set(labels)))
代码解读
输出示例:
Number of reviews: 38000
Number of classes: 10
代码解读
数据预处理
数据预处理是指对原始数据进行清洗、过滤、转换等操作,使得数据满足之后模型训练和测试的要求。
去除停用词
在对文本进行分词之前,通常要先去除掉一些不重要的词,也就是停用词(stop words)。停用词一般指那些在句子中出现次数非常高、而对句子整体意思并无实际作用的词。一般来说,停用词表中包含一些非常基本、常见、常用的词,例如"the", "and", "or"等。
stop_words = set(stopwords.words('english'))
代码解读
统一文本大小写
为了方便词频统计和分类,所有文本都统一转为小写形式。
reviews = [review.lower().strip() for review in reviews]
代码解读
切词和词干提取
文本分词是指将文本按照词汇的单元进行划分。采用词干提取的方式可以降低不同词的同义性,如"running"和"runs"、"jumping"和"jumps"等。
def stemming(word):
"""返回单词的词干"""
if word in stop_words or not isinstance(word, str):
return ''
else:
ps = PorterStemmer()
return ps.stem(word)
def tokenize(sentence):
"""返回分词后的结果"""
sentence = sentence.translate(str.maketrans('', '', string.punctuation)) # 去除标点符号
tokens = word_tokenize(sentence) # 分词
stems = [stemming(token) for token in tokens if token.isalpha()] # 提取词干
return''.join([stemming(token).lower() for token in tokens])
reviews = [tokenize(review) for review in reviews] # 使用词干提取
代码解读
制作词典
词典是指一个词到另一个词、词组或数字值的映射表。词典在文本处理过程中起到了重要作用,因为很多时候我们只是希望知道某一段文字或文档中某个词出现的频次。
counts = Counter(' '.join(reviews).split())
vocab = sorted(list(counts.keys()))
代码解读
创建词袋(bag-of-words)矩阵
词袋是一种简单而有效的文本表示方式。它表示的是一段文本中某个词的出现次数。比如说,对于一段文本:"I am happy today.",它的词袋表示形式为:[1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1]. 在词袋矩阵中,每一行代表一个文档,每一列代表一个词。如果某个词在当前文档出现过一次,则相应位置的值为1;如果某个词在当前文档没出现过,则相应位置的值为0。
doc_num = len(reviews)
bow_matrix = []
for i in range(doc_num):
bow = {}
for term in counts:
count = counts[term]
if term in vocab:
tfidf = count / sum([counts[t] for t in counts]) * math.log10(doc_num/(len(labels)+1))
bow[term] = tfidf if tfidf > 0 else 0
bow_matrix.append(bow)
代码解读
数据集分割
为了便于模型训练和测试,通常把原始数据集划分成训练集和测试集。
train_reviews, test_reviews, train_labels, test_labels = train_test_split(reviews, labels, test_size=0.2, random_state=42)
代码解读
模型训练与测试
模型训练与测试是对模型性能进行评估的过程。通常情况下,模型会根据训练集对输入的样本数据进行训练,得到一个模型参数。然后,再对测试集中的样本数据进行预测,计算出模型的准确率。
定义模型
下面定义了一个基于朴素贝叶斯分类器的管道,模型包括词袋矩阵和 TF-IDF transformer,共包含五个步骤:
- 词袋矩阵:将每条评论转化为词袋矩阵。
- TF-IDF transformer:计算每条评论中每个词的 TF-IDF 值。
- 标准化:将 TF-IDF 值转换为标准化后的数值。
- 最小最大缩放:将标准化后的数值转换为 [0, 1] 区间。
- 朴素贝叶斯分类器:使用朴素贝叶斯分类器对情感极性进行分类。
classifier = Pipeline([
('vect', CountVectorizer()),
('tfidf', TfidfTransformer()),
('scaler', MinMaxScaler()),
('clf', MultinomialNB())
])
代码解读
模型训练
在训练模型之前,需要对数据集进行分割。
classifier.fit(train_reviews, train_labels)
代码解读
模型测试
模型训练完成后,对测试集进行预测,并计算准确率。
pred_labels = classifier.predict(test_reviews)
accuracy = accuracy_score(test_labels, pred_labels)
print("Accuracy:", accuracy)
代码解读
输出示例:
Accuracy: 0.7988
代码解读
模型改进
基于规则的方法或机器学习的方法对用户的评论进行情感分析的准确率仍然不够高。下面介绍几种常用的模型改进策略。
添加额外特征
除了评论的本身的内容外,评论还可以包含诸如时间、日期、作者、情感倾向等额外的信息。
使用深度学习方法
目前,深度学习方法取得了令人瞩目的成果,在计算机视觉、自然语言处理等领域均取得了显著的效果。比如,基于深度学习的神经网络可以自动提取出用户评论中的语义信息,进一步提升模型的准确性。
4.具体代码实例和解释说明
数据获取
import pandas as pd # 数据处理包
import urllib.request # URL请求包
import os # 文件系统包
import zipfile # ZIP压缩包
from io import BytesIO # 字节流处理包
def download_dataset():
url = r'http://www.cs.cornell.edu/people/pabo/movie-review-data/rt-polaritydata.tar.gz'
if not os.path.exists('./datasets'):
os.makedirs('./datasets')
file_name = './datasets/' + os.path.basename(url)
request = urllib.request.urlopen(url)
with open(file_name, 'wb') as f:
f.write(request.read())
zipped_file = zipfile.ZipFile(BytesIO(open(file_name, 'rb').read()))
zipped_file.extractall('./datasets/')
def load_data():
data = pd.DataFrame([], columns=['label', 'text'])
files = ['./datasets/rt-polaritydata/rt-polarity.neg',
'./datasets/rt-polaritydata/rt-polarity.pos']
for file in files:
with open(file, encoding='latin-1') as f:
lines = f.readlines()
for line in lines:
label, text = line.strip().split(None, 1)
row = {'label': label,
'text': text}
data = data.append(row, ignore_index=True)
return list(data['text']), list(data['label'])
代码解读
数据预处理
import string # 字符串处理包
import re # 正则表达式包
from nltk.tokenize import word_tokenize # 分词包
from nltk.corpus import stopwords # 停用词包
from collections import Counter # 词频统计包
import math # 数学计算包
def preprocess(text):
stop_words = set(stopwords.words('english'))
text = text.lower().strip()
text = re.sub('<[^<]+?>', '', text)
text = ''.join(['_' if c in string.punctuation else c for c in text])
tokens = word_tokenize(text)
stems = [stemming(token) for token in tokens if token.isalpha()]
return''.join([stemming(token) for token in tokens if token.isalpha()])
def stemming(word):
"""返回单词的词干"""
ps = PorterStemmer()
return ps.stem(word)
def tokenize(sentence):
"""返回分词后的结果"""
sentence = sentence.translate(str.maketrans('', '', string.punctuation)) # 去除标点符号
tokens = word_tokenize(sentence) # 分词
stems = [stemming(token) for token in tokens if token.isalpha()] # 提取词干
return''.join([stemming(token).lower() for token in tokens])
def create_vocabulary(texts):
vocabulary = Counter(' '.join(texts).split())
return sorted(list(vocabulary.keys()))
def bag_of_words(texts, vocabulary):
doc_num = len(texts)
matrix = [[0]*len(vocabulary) for _ in range(doc_num)]
for i in range(doc_num):
text = texts[i]
freq_dict = dict(Counter(text.split()))
for j in range(len(vocabulary)):
term = vocabulary[j]
if term in freq_dict:
matrix[i][j] = freq_dict[term]
return matrix
代码解读
模型训练与测试
from sklearn.model_selection import train_test_split # 将数据集分割为训练集和测试集
from sklearn.feature_extraction.text import CountVectorizer # 词袋矩阵构造器
from sklearn.feature_extraction.text import TfidfTransformer # TF-IDF 转换器
from sklearn.preprocessing import StandardScaler # 标准化
from sklearn.preprocessing import MinMaxScaler # 最小最大缩放
from sklearn.naive_bayes import MultinomialNB # 朴素贝叶斯分类器
from sklearn.pipeline import Pipeline # 模型管道
from sklearn.metrics import accuracy_score # 准确率计算器
from nltk.stem.porter import PorterStemmer # 词干提取器
if __name__ == '__main__':
# 获取数据
download_dataset()
X, y = load_data()
print("下载完毕,共有%d条数据"%len(X))
# 数据预处理
processed_X = [''.join(['_' if c in string.punctuation else c for c in x.lower().strip()])
for x in X]
punctuations = string.punctuation+'’…“”—「」'
stop_words = set(stopwords.words('english') + list(punctuations))
stemmer = PorterStemmer()
cleaned_X = [' '.join([stemmer.stem(token)
for token in tokenize(x).split()
if token not in stop_words and token.isalnum()])
for x in processed_X]
# 词典创建
vocab = create_vocabulary(cleaned_X)
print("Vocabulary size:", len(vocab))
# Bag of Words 矩阵创建
feature_matrix = bag_of_words(cleaned_X, vocab)
print("Feature Matrix shape:", len(feature_matrix), "x", len(feature_matrix[0]))
# 数据集划分
train_X, test_X, train_y, test_y = train_test_split(feature_matrix,
y,
test_size=0.2,
random_state=42)
print("Training Set Size:", len(train_X))
print("Test Set Size:", len(test_X))
# 定义模型
model = Pipeline([
('vectorizer', CountVectorizer(vocabulary=vocab)),
('tfidf', TfidfTransformer()),
('scaling', MinMaxScaler()),
('classifier', MultinomialNB())
])
# 模型训练
model.fit(train_X, train_y)
# 模型预测
predictions = model.predict(test_X)
acc = accuracy_score(predictions, test_y)
print("Accuarcy on Test Set:", acc)
代码解读
