本项目的目标是基于用户提供的评论,通过算法自动去判断其评论是正面的还是负面的情感。比如给定一个用户的评论:
- 评论1: “我特别喜欢这个电器,我已经用了3个月,一点问题都没有!”
- 评论2: “我从这家淘宝店卖的东西不到一周就开始坏掉了,强烈建议不要买,真实浪费钱”
对于这两个评论,第一个明显是正面的,第二个是负面的。 我们希望搭建一个AI算法能够自动帮我们识别出评论是正面还是负面。
情感分析的应用场景非常丰富,也是NLP技术在不同场景中落地的典范。比如对于一个证券领域,作为股民,其实比较关注舆论的变化,这个时候如果能有一个AI算法自动给网络上的舆论做正负面判断,然后把所有相关的结论再整合,这样我们可以根据这些大众的舆论,辅助做买卖的决策。 另外,在电商领域评论无处不在,而且评论已经成为影响用户购买决策的非常重要的因素,所以如果AI系统能够自动分析其情感,则后续可以做很多有意思的应用。
情感分析是文本处理领域经典的问题。整个系统一般会包括几个模块:
- 数据的抓取: 通过爬虫的技术去网络抓取相关文本数据
- 数据的清洗/预处理:在本文中一般需要去掉无用的信息,比如各种标签(HTML标签),标点符号,停用词等等
- 把文本信息转换成向量: 这也成为特征工程,文本本身是不能作为模型的输入,只有数字(比如向量)才能成为模型的输入。所以进入模型之前,任何的信号都需要转换成模型可识别的数字信号(数字,向量,矩阵,张量……)
- 选择合适的模型以及合适的评估方法。 对于情感分析来说,这是二分类问题(或者三分类:正面,负面,中性),所以需要采用分类算法比如逻辑回归,朴素贝叶斯,神经网络,SVM等等。另外,我们需要选择合适的评估方法,比如对于一个应用,我们是关注准确率呢,还是关注召回率呢?
在本次项目中,我们已经给定了训练数据和测试数据,它们分别是 train_positive.txt,train_negative.txt, test_combined.txt。请注意训练数据和测试数据的格式不一样,详情请见文件内容。 整个项目你需要完成以下步骤:
数据的读取以及清洗: 从给定的.txt中读取内容,并做一些数据清洗,这里需要做几个工作:
- (1) 文本的读取,需要把字符串内容读进来。
- (2)去掉无用的字符比如标点符号,多余的空格,换行符等
- (3) 把文本转换成 TF-IDF 向量: 这部分直接可以利用 sklearn 提供的 TfidfVectorizer 类来做。
- (4) 利用逻辑回归等模型来做分类,并通过交叉验证选择最合适的超参数
项目中需要用到的数据:
- train.positive_txt, train_negative.txt, test_combined.txt: 训练和测试数据
- stopwords.txt: 停用词库
1. File Reading:文本读取
# 导入相关库
from bs4 import BeautifulSoup
def process_file():
"""
读取训练数据和测试数据,并对它们做一些预处理
"""
train_pos_file = "train_positive.txt"
train_neg_file = "train_negative.txt"
test_comb_file = "test_combined.txt"
# 读取文件部分,把具体的内容写入到变量里面
train_comments = []
train_labels = []
test_comments = []
test_labels = []
# 处理训练数据正样本
with open(train_pos_file,mode="r",encoding="utf-8") as fp:
soup = BeautifulSoup(fp,"html.parser")
# 寻找review标签
review_list = soup.find_all("review")
for review in review_list:
# 提取标签内容并去除空格和换行
train_comments.append(review.text.replace(" ","").replace("\n",""))
train_labels.append(1)
# 处理训练数据负样本
with open(train_neg_file,mode="r",encoding="utf-8") as fp:
soup = BeautifulSoup(fp,"html.parser")
# 寻找review标签
review_list = soup.find_all("review")
for review in review_list:
# 提取标签内容并去除空格和换行
train_comments.append(review.text.replace(" ","").replace("\n",""))
train_labels.append(0)
# 处理测试数据
with open(test_comb_file,mode="r",encoding="utf-8") as fp:
soup = BeautifulSoup(fp,"html.parser")
review_list = soup.find_all("review")
for review in review_list:
# 提取标签内容并去除空格和换行
test_comments.append(review.text.replace(" ","").replace("\n",""))
# 提取标签属性值
test_labels.append(int(review['label']))
return train_comments,train_labels,test_comments,test_labels
train_comments, train_labels, test_comments, test_labels = process_file()
2. Explorary Analysis:做一些简单的可视化分析
# 训练数据和测试数据大小
print (len(train_comments), len(test_comments))
- 8065 2500
这里有一个假设想验证。我觉得,如果一个评论是负面的,则用户留言时可能会长一些,因为对于负面的评论,用户很可能会把一些细节写得很清楚。但对于正面的评论,用户可能就只写“非常好”,这样的短句。我们想验证这个假设。 为了验证这个假设,打算画两个直方图,分别对正面的评论和负面的评论。 具体的做法是:
- 把正面和负面评论分别收集,之后分别对正面和负面评论画一个直方图。
- 直方图的X轴是评论的长度,所以从是小到大的顺序。然后Y轴是对于每一个长度,出现了多少个正面或者负面的评论。 通过两个直方图的比较,即可以看出 评论 是否是一个靠谱的特征。
# 对于训练数据中的正负样本,分别画出一个histogram
# histogram的x抽是每一个样本中字符串的长度,y轴是拥有这个长度的样本的数量
# 分析样本长度是否对情感有相关性
# 导入相关库
import numpy as np
import matplotlib.pyplot as plt
%matplotlib inline
plt.rcParams ['font.sans-serif'] = ['SimHei']
data0 = []
data1 = []
for i in range(len(train_comments)):
# 统计长度小于等于100的字符串
if len(train_comments[i]) <= 100:
if train_labels[i] == 0:
data0.append(len(train_comments[i]))
else:
data1.append(len(train_comments[i]))
plt.title('负样本分布')
plt.hist(data0, bins=100)

plt.title('正样本分布')
plt.hist(data1, bins=100)

由此可见,情感跟评论长度有一定的相关性。
3. 文本预处理
在此部分需要做文本预处理方面的工作。 分为几大块:
- 去掉特殊符号 比如#$….
- 把数字转换成特殊单词 把数字转换成 “NUM”。
- 分词并过滤掉停用词 停用词库已经提供,需要读取停用词库,并按照此停用词库做过滤。 停用词库使用给定的文件:stopwords.txt
import re
import jieba
def clean_symbols(text):
"""
对特殊符号做一些处理
"""
text = re.sub('[!!]+', "!", text)
text = re.sub('[??]+', "?", text)
text = re.sub("[a-zA-Z#$%&\'()*+,-./:;:<=>@,。★、…【】《》“”‘’[\\]^_`{|}~]+", "OOV", text)
text = re.sub("\s+", " ", text)
return text
def clean_num(text):
"""
把数字转换成特殊字符
"""
text = re.sub('\d+\.\d+','NUM',text)
text = re.sub('\d+','NUM',text)
return text
def cut_word(text):
"""
分词
"""
text = jieba.cut(text,cut_all=False)
return list(text)
def clean_stopwords(text):
"""
处理停用词
"""
f = open('stopwords.txt', mode='r',encoding='utf-8')
stopwords = f.readlines()
f.close()
for i in range(len(stopwords)):
stopwords[i] = stopwords[i].replace('\n','')
clean_words = [word for word in text if word not in stopwords]
return clean_words
train_comments_cleaned = []
test_comments_cleaned = []
train_labels_clearned = []
test_labels_clearned = []
# 批量处理
for i in range(len(train_comments)):
train_comments[i] = clean_symbols(train_comments[i])
train_comments[i] = clean_num(train_comments[i])
train_comments[i] = cut_word(train_comments[i])
train_comments[i] = clean_stopwords(train_comments[i])
# 过滤长度为0的句子
if len(train_comments[i]) !=0:
train_comments_cleaned.append(' '.join(train_comments[i]))
train_labels_clearned.append(train_labels[i])
for i in range(len(test_comments)):
test_comments[i] = clean_symbols(test_comments[i])
test_comments[i] = clean_num(test_comments[i])
test_comments[i] = cut_word(test_comments[i])
test_comments[i] = clean_stopwords(test_comments[i])
if len(test_comments[i]) !=0:
test_comments_cleaned.append(' '.join(test_comments[i]))
test_labels_clearned.append(test_labels[i])
# 打印一下看看
print (train_comments_cleaned[0])
print (test_comments_cleaned[0])
4. 把文本转换成向量
预处理好文本之后,我们就需要把它转换成向量形式,这里我们使用tf-idf的方法。 sklearn自带此功能,直接调用即可。输入就是若干个文本,输出就是每个文本的tf-idf向量。详细的使用说明可以在这里找到: 参考:https://scikit-learn.org/stable/modules/generated/sklearn.feature_extraction.text.TfidfVectorizer.html 这里需要特别注意的一点是:对于训练数据调用fit_transform, 也就是训练的过程。 但对于测试数据,不能再做训练,而是直接使用已经训练好的object做transform操作。
from sklearn.feature_extraction.text import TfidfVectorizer
# 利用tf-idf从文本中提取特征,写到数组里面
tfidf_model = TfidfVectorizer(token_pattern=r"(?u)\b\w+\b").fit(train_comments_cleaned)
# 训练数据的特征
X_train = tfidf_model.transform(train_comments_cleaned)
# 训练数据的label
y_train = train_labels_clearned
# 测试数据的特征
X_test = tfidf_model.transform(test_comments_cleaned)
# 测试数据的label
y_test = test_labels_clearned
print (np.shape(X_train), np.shape(X_test), np.shape(y_train), np.shape(y_test))
- (8064, 24794) (2500, 24794) (8064,) (2500,)
5. 通过交叉验证来训练模型
接下来需要建模了! 这里我们分别使用逻辑回归,朴素贝叶斯和SVM来训练。针对于每一个方法我们使用交叉验证(gridsearchCV), 并选出最好的参数组合,然后最后在测试数据上做验证。
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import GridSearchCV
from sklearn.metrics import classification_report
# 利用逻辑回归来训练模型
# 1. 评估方式: F1-score
# 2. 超参数(hyperparater)的选择利用grid search
# 3. 打印出在测试数据中的最好的结果(precision, recall, f1-score, 需要分别打印出正负样本,以及综合的)
# 请注意:做交叉验证时绝对不能用测试数据。测试数据只能用来最后的“一次性”检验。
# 对于逻辑回归,经常调整的超参数为: C
params_c = np.logspace(-5,2,15)
hyperparameters = dict(C=params_c)
clf = GridSearchCV(LogisticRegression(penalty='l2',max_iter=10000), param_grid=hyperparameters, scoring ='f1', cv=5, verbose=0)
model = clf.fit(X_train, y_train)
print(model.best_params_)
- {‘C’: 1.0}
predictions = model.predict(X_test)
print(classification_report(y_test, predictions))

from sklearn.naive_bayes import MultinomialNB
# 利用朴素贝叶斯来训练模型
# 1. 评估方式: F1-score
# 2. 超参数(hyperparater)的选择利用grid search
# 3. 打印出在测试数据中的最好的结果(precision, recall, f1-score, 需要分别打印出正负样本,以及综合的)
# 请注意:做交叉验证时绝对不能用测试数据。 测试数据只能用来最后的“一次性”检验。
# 对于朴素贝叶斯,一般不太需要超参数的调节。但如果想调参,有几个参数是可以调节的。
params_alpha = np.linspace(0.1,1,10)
hyperparameters = dict(alpha=params_alpha)
clf = GridSearchCV(MultinomialNB(), param_grid=hyperparameters, scoring ='f1', cv=5, verbose=0)
model = clf.fit(X_train, y_train)
print(model.best_params_)
- {‘alpha’: 0.4}
predictions = model.predict(X_test)
print(classification_report(y_test, predictions))

from sklearn.svm import SVC
# 利用SVM来训练模型
# 1. 评估方式: F1-score
# 2. 超参数(hyperparater)的选择利用grid search https://scikit-learn.org/stable/modules/generated/sklearn.model_selection.GridSearchCV.html
# 3. 打印出在测试数据中的最好的结果(precision, recall, f1-score, 需要分别打印出正负样本,以及综合的)
# 请注意:做交叉验证时绝对不能用测试数据。 测试数据只能用来最后的“一次性”检验。
# 对于SVM模型,经常调整的超参数为:C, gamma, kernel。这里的参数C跟逻辑回归是一样的,gamma和kernel是针对于SVM的参数
params_c = np.logspace(-4, 1, 11)
params_kernel = ['linear', 'poly', 'rbf', 'sigmoid']
hyperparameters = dict(C=params_c, kernel=params_kernel)
clf = GridSearchCV(SVC(), param_grid=hyperparameters, scoring ='f1', cv=5, verbose=0)
model = clf.fit(X_train, y_train)
print(model.best_params_)
- {‘C’: 1.0, ‘kernel’: ‘rbf’}
predictions = model.predict(X_test)
print(classification_report(y_test, predictions))
