使用朴素贝叶斯和Python进行情感分析
情感分析是理解客户反馈、社交媒体评论和产品评论的强大工具。它使我们能够以编程方式确定一段文本是正面、负面还是中性的。虽然像Transformer(如BERT)这样的复杂模型经常成为头条新闻,但经典的多项式朴素贝叶斯分类器仍然是一个令人惊讶的有效、高效且可解释的基线,特别是对于基于文本的任务。
在本指南中,我们将使用Python和Scikit-learn完成一个完整的情感分析项目。我们将涵盖:
- 为什么朴素贝叶斯是文本分析的理想起点
- 探索性数据分析(EDA)以理解我们的数据集
- 数据预处理以清理和准备文本
- 使用TF-IDF进行向量化
- 使用Scikit-learn Pipeline进行模型训练
- 使用混淆矩阵、ROC-AUC和精确率-召回率曲线进行性能评估
- 对新数据进行预测
为什么选择朴素贝叶斯处理文本?
在我们深入之前,为什么选择朴素贝叶斯?
- 速度:训练速度极快,即使在大型数据集上也是如此
- 效率:只需要相对较少的训练数据就能产生不错的结果
- 处理高维数据能力强:文本分类是一个高维问题(词汇表中每个独特单词都是一个维度)。朴素贝叶斯能够优雅地处理这种"宽"数据
它被称为"朴素"是因为它做了一个"朴素"的假设:文档中一个单词的出现与所有其他单词的出现是独立的。虽然这显然是错误的(单词"New"高度依赖于单词"York"),但该模型在实践中效果出奇地好。
步骤1:设置和探索性数据分析(EDA)
在编写任何机器学习代码之前,我们必须首先理解我们的数据。对于这个项目,我们将模拟一个带有预标记正面(1)和负面(0)评论的电影评论数据集。
首先,让我们准备好导入和数据。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
|
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
import re
import nltk
from nltk.corpus import stopwords
from nltk.stem import WordNetLemmatizer
from wordcloud import WordCloud
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.model_selection import train_test_split
from sklearn.naive_bayes import MultinomialNB
from sklearn.pipeline import Pipeline
from sklearn.metrics import (
accuracy_score,
confusion_matrix,
classification_report,
RocCurveDisplay,
PrecisionRecallDisplay
)
# --- 下载NLTK资源 ---
# (只需要运行一次)
# nltk.download('stopwords')
# nltk.download('wordnet')
# nltk.download('omw-1.4') # 用于WordNet
# --- 1. 模拟我们的DataFrame ---
# 在实际项目中,您会在这里使用pd.read_csv()
data = {
'review': [
"This movie was absolutely fantastic! The acting was superb.",
"I loved this film. The story was compelling and beautiful.",
"A truly great and moving picture. Highly recommend.",
"What a wonderful movie. I'll watch it again.",
"The best film I've seen all year.",
"Completely boring. I fell asleep halfway through.",
"A terrible plot and awful acting. Do not recommend.",
"This was a bad movie. Just plain bad.",
"I hated it. The end was a letdown.",
"The characters were flat and the story was predictable."
],
'sentiment': [1, 1, 1, 1, 1, 0, 0, 0, 0, 0] # 1 = 正面, 0 = 负面
}
df = pd.DataFrame(data)
|
现在,让我们进行探索。
1. 检查类别平衡
我们的数据集是否平衡?不平衡的数据集(例如,90%正面,10%负面)可能会误导我们的模型。
1
2
3
|
sns.countplot(x='sentiment', data=df)
plt.title('类别分布 (0=负面, 1=正面)')
plt.show()
|
我们的小型数据集完全平衡,这是理想的。
2. 使用词云可视化
词云显示每种情感的最频繁单词。
1
2
3
4
5
6
7
8
9
10
|
positive_text = ' '.join(df[df['sentiment'] == 1]['review'])
negative_text = ' '.join(df[df['sentiment'] == 0]['review'])
# 正面词云
wc_positive = WordCloud(width=800, height=400, background_color='white').generate(positive_text)
plt.figure(figsize=(10, 5))
plt.imshow(wc_positive, interpolation='bilinear')
plt.title('正面评论中最频繁的单词')
plt.axis('off')
plt.show()
|
正面评论由"fantastic"、“great”、“loved”、“superb"和"beautiful"等词主导。
1
2
3
4
5
6
7
|
# 负面词云
wc_negative = WordCloud(width=800, height=400, background_color='black', colormap='Reds').generate(negative_text)
plt.figure(figsize=(10, 5))
plt.imshow(wc_negative, interpolation='bilinear')
plt.title('负面评论中最频繁的单词')
plt.axis('off')
plt.show()
|
负面评论包含"terrible”、“awful”、“boring”、“bad"和"hated"等词。
3. 分析评论长度
评论长度与其情感之间是否存在相关性?
1
2
3
4
|
df['review_length'] = df['review'].apply(len)
sns.histplot(data=df, x='review_length', hue='sentiment', multiple='stack', bins=20)
plt.title('按情感划分的评论长度分布')
plt.show()
|
在我们的简单数据中,没有明显的模式,但在更大的数据集中,您可能会发现(例如)负面评论通常更短且更"直截了当”。
步骤2:数据预处理 - 清理我们的文本
原始文本很混乱。为了使其可用,我们需要清理它。我们将编写一个函数来完成此操作。
- 转换为小写:确保"Movie"和"movie"被视为同一个单词
- 删除标点符号和数字:这些字符通常不添加情感价值
- 删除停用词:消除不携带情感的常见单词,如"the"、“a"和"is”
- 词形还原:将单词还原为其词根形式(例如,“running"变为"run”,“was"变为"be”)。这将相关单词分组在一起。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
stop_words = set(stopwords.words('english'))
lemmatizer = WordNetLemmatizer()
def preprocess_text(text):
# 1. 转换为小写
text = text.lower()
# 2. 删除标点符号和数字
text = re.sub(r'[^a-zA-Z\s]', '', text)
# 3. 分词(拆分成单词)
words = text.split()
# 4. 删除停用词并进行词形还原
words = [lemmatizer.lemmatize(word) for word in words if word not in stop_words]
# 5. 重新连接成字符串
return ' '.join(words)
# 让我们看看"前后"对比
print(f"原始: {df['review'][0]}")
print(f"清理后: {preprocess_text(df['review'][0])}")
# 将此函数应用于整个'review'列
df['cleaned_review'] = df['review'].apply(preprocess_text)
|
输出:
1
2
|
原始: This movie was absolutely fantastic! The acting was superb.
清理后: movie absolutely fantastic acting superb
|
步骤3:构建和训练朴素贝叶斯模型
现在我们将清理后的文本转换为我们的模型可以理解的数字。
向量化(TF-IDF)
我们将使用TF-IDF(词频-逆文档频率)。
- 词频(TF):一个单词在单个文档(评论)中出现的频率
- 逆文档频率(IDF):一个单词在所有文档中的稀有程度
这种技术给那些在一个评论中频繁出现但在所有其他评论中罕见的单词赋予高分。这有助于模型找到独特的、带有情感的单词。
使用Pipeline(正确的方式)
Scikit-learn中的最佳实践是使用Pipeline。Pipeline将我们的步骤(向量化器和模型)链接到一个对象中。这可以防止数据泄漏并简化代码。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
# 1. 定义我们的特征(X)和目标(y)
X = df['cleaned_review']
y = df['sentiment']
# 2. 分割为训练集和测试集
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, random_state=42, stratify=y
)
# 3. 创建Scikit-learn Pipeline
# 这个Pipeline将:
# 1. 应用TfidfVectorizer
# 2. 训练MultinomialNB模型
pipeline = Pipeline([
('tfidf', TfidfVectorizer(max_features=5000, ngram_range=(1, 2))),
('model', MultinomialNB())
])
# 4. 训练模型
# 我们只需要在pipeline上调用.fit()!
pipeline.fit(X_train, y_train)
# 5. 进行预测
y_pred = pipeline.predict(X_test)
|
步骤4:评估模型性能
我们的模型表现如何?我们根据X_test数据评估其预测。
分类报告和混淆矩阵
- 准确率:正确预测的总体百分比。(在不平衡数据上使用时要小心!)
- 精确率:在我们预测为正面评论的所有评论中,有多少实际上是正面的?
- 召回率:在所有实际正面评论中,我们找到了多少?
- F1分数:精确率和召回率的调和平均数。一个很好的全能指标。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
# 1. 准确率
print(f"准确率: {accuracy_score(y_test, y_pred):.2f}")
print("-" * 40)
# 2. 分类报告
print(classification_report(y_test, y_pred))
print("-" * 40)
# 3. 混淆矩阵
cm = confusion_matrix(y_test, y_pred)
sns.heatmap(cm, annot=True, fmt='d', cmap='Blues',
xticklabels=['预测负面', '预测正面'],
yticklabels=['实际负面', '实际正面'])
plt.title('混淆矩阵')
plt.show()
|
ROC曲线(接收者操作特征)
ROC曲线绘制了真正例率对假正例率。
- 左上角的曲线(AUC = 1.0)是完美模型
- 对角线(AUC = 0.5)是比随机猜测好不了多少的模型
1
2
3
4
5
6
|
# 绘制ROC曲线
RocCurveDisplay.from_estimator(pipeline, X_test, y_test)
plt.title('朴素贝叶斯分类器的ROC曲线')
plt.plot([0, 1], [0, 1], 'r--', label='随机猜测')
plt.legend()
plt.show()
|
精确率-召回率曲线
该曲线在处理不平衡数据集时特别有用,因为它关注正类的性能。
1
2
3
4
|
# 绘制精确率-召回率曲线
PrecisionRecallDisplay.from_estimator(pipeline, X_test, y_test)
plt.title('精确率-召回率曲线')
plt.show()
|
步骤5:对新数据进行预测
最棒的部分!让我们使用训练好的pipeline来预测新原始文本的情感。Pipeline将自动应用所有预处理步骤和TF-IDF向量化。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
def predict_sentiment(text):
# Pipeline完成所有工作:
# 1. 预处理文本
# 2. TF-IDF向量化
# 3. 预测
prediction = pipeline.predict([text])[0]
probability = pipeline.predict_proba([text])[0]
if prediction == 1:
return f"正面 (置信度: {probability[1]:.2f})"
else:
return f"负面 (置信度: {probability[0]:.2f})"
# 试试看
print(predict_sentiment("This was the best movie I have ever seen!"))
print(predict_sentiment("The acting was stiff and the plot was just awful."))
|
示例输出:
1
2
|
正面 (置信度: 0.95)
负面 (置信度: 0.99)
|
结论
我们已经成功构建了一个端到端的情感分析分类器。我们首先探索了文本数据,然后继续清理文本,构建了一个健壮的Pipeline,训练了一个朴素贝叶斯模型,最后使用行业标准指标评估了其性能。
这个过程提供了一个可以应用于各种文本分类问题的可靠基线。从这里,您可以尝试不同的向量化器(如CountVectorizer),调整模型超参数(如朴素贝叶斯的alpha),或使用此模型的性能作为基准,看看更复杂的模型(如逻辑回归或Transformer)是否能提供显著提升。