一、前言
在电商平台上,每天都有数以百万计的用户评论产生。这些评论中蕴含着用户对商品质量、物流服务、购物体验的真实反馈。如果能自动、快速地判断一条评论是好评还是差评,商家就可以及时发现问题商品、监控口碑变化、针对性地改进服务------这就是情感分析(Sentiment Analysis) 的典型应用场景。
本文将基于一份某电商评论数据集,从零开始完整走一遍文本情感分类的建模流程。包括:
- 中文文本的清洗与预处理流程(缺失值处理、标签转换)
- 使用 jieba 进行中文分词与停用词过滤
- TF-IDF 文本向量化的原理与参数调优
- 朴素贝叶斯、逻辑回归、随机森林三种模型在文本分类上的表现对比
max_features参数对模型效果的影响分析- 错误案例分析:模型在哪些评论上容易判断失误
- 通过逻辑回归权重解读哪些词是"好评词"、哪些是"差评词"
- 封装可直接调用的情感预测函数
数据集包含约 4.5 万条训练评论和 5000 条验证评论,正负样本基本均衡,标签 1 代表好评,0 代表差评。
二、数据探索与清洗
2.1 导入依赖库
python
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
# 设置 pandas 显示优化
pd.set_option('display.max_columns', None)
pd.set_option('display.width', 2000)
pd.set_option('display.max_colwidth', 35)
# 设置显示中文
plt.rcParams['font.sans-serif'] = ['SimHei']
plt.rcParams['axes.unicode_minus'] = False
2.2 读取训练集
python
df_train = pd.read_csv("6.train.csv")
print(f"数据形状:{df_train.shape}")
print(f"\n前五行数据:\n{df_train.head()}")
print(f"\n数据类型与缺失值情况:")
print(df_train.info())
print(f"\n标签分布情况:\n{df_train.value_counts('label')}")
txt
数据形状:(45366, 3)
前五行数据:
sentence label dataset
0 一百多和三十的也看不出什么区别,包装精美,质量应该不错。 1.0 jd
1 质量很好 料子很不错 做工细致 样式好看 穿着很漂亮 1.0 jd
2 会卷的 建议买大的小的会卷 胖就别买了 没用 0.0 jd
3 大差了 布料很差 我也不想多说 0.0 jd
4 一点也不好,我买的东西拿都拿到快递员自己签收了还不给我,恶心恶... 0.0 jd
数据类型与缺失值情况:
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 45366 entries, 0 to 45365
Data columns (total 3 columns):
# Column Non-Null Count Dtype
--- ------ -------------- -----
0 sentence 45364 non-null object
1 label 45012 non-null float64
2 dataset 45012 non-null object
dtypes: float64(1), object(2)
memory usage: 1.0+ MB
None
标签分布情况:
label
0.0 22588
1.0 22424
Name: count, dtype: int64
2.3 读取验证集
python
df_dev = pd.read_csv("6.dev.csv")
print(f"数据形状:{df_dev.shape}")
print(f"\n前五行数据:\n{df_dev.head()}")
print(f"\n数据类型与缺失值情况:")
print(df_dev.info())
print(f"\n标签分布情况:\n{df_dev.value_counts('label')}")
txt
数据形状:(5032, 3)
前五行数据:
sentence label dataset
0 擦玻璃很好、就是太小了 1.0 jd
1 店家太不负责任了,衣服质量太差劲了,和图片上的不一样 0.0 jd
2 送国际友人挺好的,不错不错! 1.0 jd
3 很好,装好一定很漂亮 1.0 jd
4 东西给你退回去了,你要黑我钱!!! 0.0 jd
数据类型与缺失值情况:
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 5032 entries, 0 to 5031
Data columns (total 3 columns):
# Column Non-Null Count Dtype
--- ------ -------------- -----
0 sentence 5032 non-null object
1 label 4988 non-null float64
2 dataset 4988 non-null object
dtypes: float64(1), object(2)
memory usage: 118.1+ KB
None
标签分布情况:
label
0.0 2547
1.0 2441
Name: count, dtype: int64
数据集包含三列:sentence(评论文本)、label(情感标签,1=好评,0=差评)、dataset(数据来源)。正负样本比例接近 1:1,属于均衡数据集,可以直接使用准确率作为主要评估指标。
2.4 数据清洗
数据中存在少量缺失值,dataset 列对建模没有意义,label 是 float 类型需要转为 int,统一做清洗。
python
# 去掉有缺失值的行
df_train = df_train.dropna(subset=["sentence", "label"])
df_dev = df_dev.dropna(subset=["sentence", "label"])
# 去掉没有用的特征 dataset
df_train = df_train.drop("dataset", axis=1)
df_dev = df_dev.drop("dataset", axis=1)
# 将 label 由 float ==> int
df_train["label"] = df_train["label"].astype(int)
df_dev["label"] = df_dev["label"].astype(int)
print(f"{df_train.columns.tolist()}\n")
print("训练集shape", df_train.shape)
print("验证集shape", df_dev.shape)
print("\n训练集标签分布:")
print(df_train["label"].value_counts())
print("\n验证集标签分布:")
print(df_dev["label"].value_counts())
txt
['sentence', 'label']
训练集shape (45010, 2)
验证集shape (4988, 2)
训练集标签分布:
label
0 22588
1 22422
Name: count, dtype: int64
验证集标签分布:
label
0 2547
1 2441
Name: count, dtype: int64
清洗后训练集 45010 条、验证集 4988 条,正负样本依旧保持均衡。
三、文本预处理:分词与停用词过滤
机器学习模型无法直接处理文本,需要先将中文句子切分成词语,再转化为数值向量。使用 jieba 进行中文分词,并过滤掉停用词和标点符号。
python
import jieba
# 简易停用词列表
stop_words = {"的", "了", "是", "就", "也", "都", "而", "及", "和", "与", "这", "那", "有", "很", "还"}
def text_process(raw_text):
# 去掉首尾空格
raw_text = str(raw_text).strip()
# jieba 精确分词
words = jieba.lcut(raw_text)
out = []
for w in words:
# 过滤:不在停用词,不是标点,并且长度 > 0
if w not in stop_words and w not in ",。!?;:、,.!?" and len(w.strip()) > 0:
out.append(w)
# 把分词结果拼接成空格隔开的字符串,供TF-IDF输入
return " ".join(out)
对训练集和验证集批量应用分词处理,并查看一条样本的处理前后对比。
python
df_train["cut_text"] = df_train["sentence"].apply(text_process)
df_dev["cut_text"] = df_dev["sentence"].apply(text_process)
# 查看处理前后对比
print("原 始:", df_train["sentence"].iloc[0])
print("分词后:", df_train["cut_text"].iloc[0])
txt
原 始: 一百多和三十的也看不出什么区别,包装精美,质量应该不错。
分词后: 一百多 三十 看不出 什么 区别 包装 精美 质量 应该 不错
可以看到,停用词"和""的""也"以及标点符号都被过滤掉了,保留了有实际语义的词语。
四、TF-IDF 文本向量化
分词之后,使用 TF-IDF 将文本转化为数值向量。TF-IDF 的核心思想是:一个词在某条评论中出现频率越高、在整个语料库中越罕见,它对这条评论的区分度就越大。
python
from sklearn.feature_extraction.text import TfidfVectorizer
# 尝试不同的 max_features,看看结果会不会有很大的变化
"""
max_features: 最多保留 max_features 个最重要词,控制维度
min_df: 至少出现在 min_df 篇及以上评论的词才保留
"""
# tfidf = TfidfVectorizer(max_features=1000, min_df=2)
# tfidf = TfidfVectorizer(max_features=2000, min_df=2)
tfidf = TfidfVectorizer(max_features=3000, min_df=2)
# tfidf = TfidfVectorizer(max_features=4000, min_df=2)
# tfidf = TfidfVectorizer(max_features=5000, min_df=2)
X_train_tfidf = tfidf.fit_transform(df_train["cut_text"])
X_test_tfidf = tfidf.transform(df_dev["cut_text"])
y_train = df_train["label"]
y_test = df_dev["label"]
print("训练集TF-IDF维度:", X_train_tfidf.shape)
print("测试集TF-IDF维度:", X_test_tfidf.shape)
txt
训练集TF-IDF维度: (45010, 3000)
测试集TF-IDF维度: (4988, 3000)
以 max_features=3000 为例,每条评论被转化为一个 3000 维的稀疏向量。min_df=2 表示只保留至少出现在 2 条评论中的词,过滤掉过于生僻的低频词。
五、多模型训练与评估
5.1 训练三个模型
同时训练朴素贝叶斯、逻辑回归和随机森林三个模型,横向对比效果。
python
from sklearn.naive_bayes import MultinomialNB
from sklearn.linear_model import LogisticRegression
from sklearn.ensemble import RandomForestClassifier
# 1.朴素贝叶斯
mnb = MultinomialNB()
mnb.fit(X_train_tfidf, y_train)
y_pred_mnb = mnb.predict(X_test_tfidf)
# 2.逻辑回归
lr = LogisticRegression(max_iter=500)
lr.fit(X_train_tfidf, y_train)
y_pred_lr = lr.predict(X_test_tfidf)
# 3.随机森林
rf = RandomForestClassifier(n_estimators=100, random_state=42)
rf.fit(X_train_tfidf, y_train)
y_pred_rf = rf.predict(X_test_tfidf)
5.2 统一评估函数
python
from sklearn.metrics import accuracy_score, precision_score, recall_score, f1_score
from sklearn.metrics import confusion_matrix, classification_report
def evaluate(y_true, y_pred, model):
acc = accuracy_score(y_true, y_pred)
prec = precision_score(y_true, y_pred)
rec = recall_score(y_true, y_pred)
f1 = f1_score(y_true, y_pred)
print(f"==================== {model} ====================")
print(f"Acc:{acc:.4f} Prec:{prec:.4f} Rec:{rec:.4f} F1:{f1:.4f}")
print(f"\n混淆矩阵\n{confusion_matrix(y_true, y_pred)}")
print(f"\n分类报告\n{classification_report(y_true, y_pred)}")
5.3 评估结果
python
evaluate(y_test, y_pred_mnb, "MultinomialNB")
evaluate(y_test, y_pred_lr, "LogisticRegression")
evaluate(y_test, y_pred_rf, "RandomForest")
txt
==================== MultinomialNB ====================
Acc:0.8478 Prec:0.8555 Rec:0.8292 F1:0.8421
混淆矩阵
[[2205 342]
[ 417 2024]]
分类报告
precision recall f1-score support
0 0.84 0.87 0.85 2547
1 0.86 0.83 0.84 2441
accuracy 0.85 4988
macro avg 0.85 0.85 0.85 4988
weighted avg 0.85 0.85 0.85 4988
==================== LogisticRegression ====================
Acc:0.8597 Prec:0.8514 Rec:0.8640 F1:0.8577
混淆矩阵
[[2179 368]
[ 332 2109]]
分类报告
precision recall f1-score support
0 0.87 0.86 0.86 2547
1 0.85 0.86 0.86 2441
accuracy 0.86 4988
macro avg 0.86 0.86 0.86 4988
weighted avg 0.86 0.86 0.86 4988
==================== RandomForest ====================
Acc:0.8380 Prec:0.8334 Rec:0.8361 F1:0.8348
混淆矩阵
[[2139 408]
[ 400 2041]]
分类报告
precision recall f1-score support
0 0.84 0.84 0.84 2547
1 0.83 0.84 0.83 2441
accuracy 0.84 4988
macro avg 0.84 0.84 0.84 4988
weighted avg 0.84 0.84 0.84 4988
在 max_features=3000 下,逻辑回归以 85.97% 的准确率和 0.8577 的 F1 排名第一,朴素贝叶斯次之(84.78%),随机森林最低(83.80%)。在高维稀疏文本数据上,线性模型往往比树模型表现更好。
六、max_features 参数对比实验
TF-IDF 的 max_features 控制保留多少个最重要的词,维度太小会丢失信息,维度太大可能引入噪声。分别测试 1000、2000、3000、4000、5000 五个取值,观察三个模型的表现变化。
6.1 各 max_features 下的模型表现
max_features = 1000
| 模型 | Accuracy | Precision | Recall | F1 |
|---|---|---|---|---|
| MultinomialNB | 0.8350 | 0.8411 | 0.8173 | 0.8290 |
| LogisticRegression | 0.8426 | 0.8340 | 0.8562 | 0.8450 |
| RandomForest | 0.8268 | 0.8188 | 0.8296 | 0.8242 |
max_features = 2000
| 模型 | Accuracy | Precision | Recall | F1 |
|---|---|---|---|---|
| MultinomialNB | 0.8414 | 0.8505 | 0.8202 | 0.8350 |
| LogisticRegression | 0.8557 | 0.8457 | 0.8642 | 0.8540 |
| RandomForest | 0.8350 | 0.8286 | 0.8357 | 0.8321 |
max_features = 3000
| 模型 | Accuracy | Precision | Recall | F1 |
|---|---|---|---|---|
| MultinomialNB | 0.8478 | 0.8555 | 0.8292 | 0.8421 |
| LogisticRegression | 0.8597 | 0.8514 | 0.8640 | 0.8577 |
| RandomForest | 0.8380 | 0.8334 | 0.8361 | 0.8348 |
max_features = 4000
| 模型 | Accuracy | Precision | Recall | F1 |
|---|---|---|---|---|
| MultinomialNB | 0.8484 | 0.8553 | 0.8308 | 0.8429 |
| LogisticRegression | 0.8625 | 0.8566 | 0.8636 | 0.8601 |
| RandomForest | 0.8398 | 0.8332 | 0.8410 | 0.8371 |
max_features = 5000
| 模型 | Accuracy | Precision | Recall | F1 |
|---|---|---|---|---|
| MultinomialNB | 0.8498 | 0.8561 | 0.8333 | 0.8445 |
| LogisticRegression | 0.8629 | 0.8567 | 0.8644 | 0.8605 |
| RandomForest | 0.8404 | 0.8370 | 0.8370 | 0.8370 |
6.2 分模型趋势汇总
MultinomialNB
| max_features | Accuracy | Precision | Recall | F1 |
|---|---|---|---|---|
| 1000 | 0.8350 | 0.8411 | 0.8173 | 0.8290 |
| 2000 | 0.8414 | 0.8505 | 0.8202 | 0.8350 |
| 3000 | 0.8478 | 0.8555 | 0.8292 | 0.8421 |
| 4000 | 0.8484 | 0.8553 | 0.8308 | 0.8429 |
| 5000 | 0.8498 | 0.8561 | 0.8333 | 0.8445 |
LogisticRegression
| max_features | Accuracy | Precision | Recall | F1 |
|---|---|---|---|---|
| 1000 | 0.8426 | 0.8340 | 0.8562 | 0.8450 |
| 2000 | 0.8557 | 0.8457 | 0.8642 | 0.8540 |
| 3000 | 0.8597 | 0.8514 | 0.8640 | 0.8577 |
| 4000 | 0.8625 | 0.8566 | 0.8636 | 0.8601 |
| 5000 | 0.8629 | 0.8567 | 0.8644 | 0.8605 |
RandomForest
| max_features | Accuracy | Precision | Recall | F1 |
|---|---|---|---|---|
| 1000 | 0.8268 | 0.8188 | 0.8296 | 0.8242 |
| 2000 | 0.8350 | 0.8286 | 0.8357 | 0.8321 |
| 3000 | 0.8380 | 0.8334 | 0.8361 | 0.8348 |
| 4000 | 0.8398 | 0.8332 | 0.8410 | 0.8371 |
| 5000 | 0.8404 | 0.8370 | 0.8370 | 0.8370 |
结论 :逻辑回归在所有维度下都是最优模型。随着 max_features 从 1000 增大到 5000,三个模型的效果都在稳步提升,但提升幅度逐渐收窄------从 3000 到 5000,逻辑回归 F1 仅从 0.8577 提升到 0.8605,收益有限但计算成本增加。实际应用中可以在效果和效率之间取折中,3000 是个性价比不错的选择。
七、错误案例分析
把表现最好的逻辑回归预测错误的样本捞出来,看看模型在哪些评论上容易"翻车"。
python
""" 错误案例分析 """
# 把最好的模型------逻辑回归,预测错误的样本捞出来,看看是个什么情况
err_df = df_dev.copy()
err_df["pred"] = y_pred_lr
err_df["is_wrong"] = (err_df["label"] != err_df["pred"])
err_samples = err_df[err_df["is_wrong"]]
print(f"总错误样本数量: {len(err_samples)}")
# 打印前 10 条错误评论
for idx, row in err_samples.head(10).iterrows():
print(f"真实:{row['label']}, 预测:{row['pred']}, 评论:{row['sentence']}")
txt
总错误样本数量: 700
真实:0, 预测:1, 评论:非常垃圾的一本书,毫无价值,适合新股民入入门吧
真实:0, 预测:1, 评论:面料硬,不给退
真实:0, 预测:1, 评论:不厚,不全,不精美。\n\n简单的词组都查不到呢~\n\n有很大的期望,可惜了了。
真实:1, 预测:0, 评论:宝贝收到了,很漂亮,跟实物一样,找了专业师傅看了,说是手表真的,机芯还好,就是高仿的,。。买的时候明明标注发票,为什么发来没有??店主有必要解释下,把发票补给我们,让我们有保障。。
真实:0, 预测:1, 评论:被坑了被坑了被坑了被坑了被坑了被坑了被坑了被坑了被坑了被坑了被坑了被坑了被坑了被坑了被坑了
真实:1, 预测:0, 评论:布料挺好的,给商家赞一个!
真实:1, 预测:0, 评论:感觉有点不值,就是很薄的一个塑料板
真实:0, 预测:1, 评论:不保温,用起也不方便
真实:0, 预测:1, 评论:一般般,穿着还不错
真实:1, 预测:0, 评论:穿过几次了不掉色,很修身的。就是有点贵。
进一步区分假正例(FP:真实差评被预测为好评)和假负例(FN:真实好评被预测为差评)。
python
fp = err_samples[(err_samples["label"] == 0) & (err_samples["pred"] == 1)] # 假正例
fn = err_samples[(err_samples["label"] == 1) & (err_samples["pred"] == 0)] # 假负例
print(f"FP(真实差评→预测好评)数量:{len(fp)}")
print(f"FN(真实好评→预测差评)数量:{len(fn)}")
txt
FP(真实差评→预测好评)数量:368
FN(真实好评→预测差评)数量:332
错误样本共 700 条,FP 和 FN 数量接近。从案例可以看出,模型容易出错的场景包括:
- 反讽/委婉表达:如"非常垃圾的一本书,毫无价值,适合新股民入入门吧",句末的"入门吧"带有正向词,干扰了模型判断。
- 混合情感:如"宝贝收到了,很漂亮......就是高仿的......为什么发来没有发票",前半段夸、后半段骂,整体标签是好评但含大量负面词。
- 重复强调:如"被坑了"重复 15 次,但模型仍预测为好评,说明 TF-IDF 对重复词的权重提升有限。
- 中性偏评价:如"一般般,穿着还不错",正负信号并存,标注本身也可能存在主观性。
八、逻辑回归特征权重解读
逻辑回归的优势之一是可解释性强。通过查看每个词的系数权重,可以直观地知道哪些词是"好评词"、哪些是"差评词"。
python
""" 查看逻辑回归特征权重 """
features = tfidf.get_feature_names_out()
coef = lr.coef_[0]
df_coef = pd.DataFrame({"word": features, "weight": coef})
# 正向 top 10,权重越大越偏向好评 1
top_pos = df_coef.sort_values("weight", ascending=0).head(10)
# 负向 top 10,权重越小越偏向差评 0
top_neg = df_coef.sort_values("weight", ascending=1).head(10)
print("最正向的十个词:\n", top_pos)
print("\n最负向的十个词:\n", top_neg)
txt
最正向的十个词:
word weight
298 不错 6.682112
1406 很漂亮 4.825586
1400 很快 3.481142
2746 还会 3.445870
1403 很棒 3.337032
2078 物美价廉 3.318478
2377 给力 3.264195
937 喜欢 3.155193
1182 实惠 3.033407
1134 好评 2.969497
最负向的十个词:
word weight
1286 差评 -7.007493
993 垃圾 -5.282893
1884 根本 -5.207250
1398 很差 -4.783610
244 不好 -4.731136
226 不值 -4.573509
2798 退货 -3.993529
1280 差劲 -3.869697
197 上当 -3.745317
1076 太差 -3.730596
结果非常符合直觉:
- 好评词 Top 10:不错、很漂亮、很快、还会、很棒、物美价廉、给力、喜欢、实惠、好评------都是明确的正向表达,其中"不错"以 6.68 的权重遥遥领先。
- 差评词 Top 10:差评、垃圾、根本、很差、不好、不值、退货、差劲、上当、太差------"差评"权重 -7.01,是最强的负面信号词。
这也验证了 TF-IDF + 逻辑回归这套方案在中文情感分析上的可解释性。
九、线上推理函数与自测
最后,将分词、向量化、预测整个流程封装成一个函数,输入任意一条评论即可输出情感判断和概率。
python
# 自测函数
def predict_sentiment(text):
processed_text = text_process(text) # 分词
vec = tfidf.transform([processed_text]) # tfidf 向量化
pred_label = lr.predict(vec)[0] # 预测 0/1
pred_proba = lr.predict_proba(vec)[0] # 类别概率
if pred_label:
return f"【好评】,正向概率:{pred_proba[1]:.4f}"
else:
return f"【差评】,负向概率:{pred_proba[0]:.4f}"
写 8 条测试评论,覆盖明显好评、明显差评、中性评价、反讽等场景。
python
""" 自己写评论,测试 """
test_list = [
"质量很好,物流快,非常满意,推荐购买。",
"质量太差,刚收到就坏掉,非常生气不推荐",
"东西一般,不算特别好也不算很差。",
"包装破损,体验感很差,不会再来买了",
"性价比很高,做工精细,物超所值",
"好家伙,这个质量可真好,一天就坏了",
"一般般,不过极具性价比!",
"一分钱一分货,这个价格来说还可以。"
]
for txt in test_list:
res = predict_sentiment(txt)
print(f"评论: {txt}")
print(f"结果: {res}\n")
txt
评论: 质量很好,物流快,非常满意,推荐购买。
结果: 【好评】,正向概率:0.8785
评论: 质量太差,刚收到就坏掉,非常生气不推荐
结果: 【差评】,负向概率:0.5496
评论: 东西一般,不算特别好也不算很差。
结果: 【差评】,负向概率:0.8645
评论: 包装破损,体验感很差,不会再来买了
结果: 【差评】,负向概率:0.6032
评论: 性价比很高,做工精细,物超所值
结果: 【好评】,正向概率:0.9450
评论: 好家伙,这个质量可真好,一天就坏了
结果: 【差评】,负向概率:0.6619
评论: 一般般,不过极具性价比!
结果: 【好评】,正向概率:0.6342
评论: 一分钱一分货,这个价格来说还可以。
结果: 【好评】,正向概率:0.5876
8 条自测全部判断正确,包括反讽句"好家伙,这个质量可真好,一天就坏了"也被正确识别为差评。不过值得注意的是,第 3 条"东西一般,不算特别好也不算很差"是中性偏差评,负向概率高达 0.8645;而第 4 条明显的差评"包装破损,体验感很差"负向概率反而只有 0.6032------说明模型在中性表达和程度副词的理解上还有提升空间,这也是基于词袋模型的固有局限。
十、项目总结
由于我做的时候使用的是 Jupyter Notebook,所以代码都是一段一段的看起来可能不方便,还请见谅!此外如果聪明的你发现了代码和表述有错误或者有更好的提议,可以在评论区写下你的建议,谢谢!数据下载: