机器学习项目:电商评论情感分析

一、前言

在电商平台上,每天都有数以百万计的用户评论产生。这些评论中蕴含着用户对商品质量、物流服务、购物体验的真实反馈。如果能自动、快速地判断一条评论是好评还是差评,商家就可以及时发现问题商品、监控口碑变化、针对性地改进服务——这就是情感分析(Sentiment Analysis) 的典型应用场景。

本文将基于一份某电商评论数据集,从零开始完整走一遍文本情感分类的建模流程。包括:

  • 中文文本的清洗与预处理流程(缺失值处理、标签转换)
  • 使用 jieba 进行中文分词与停用词过滤
  • TF-IDF 文本向量化的原理与参数调优
  • 朴素贝叶斯、逻辑回归、随机森林三种模型在文本分类上的表现对比
  • max_features 参数对模型效果的影响分析
  • 错误案例分析:模型在哪些评论上容易判断失误
  • 通过逻辑回归权重解读哪些词是"好评词"、哪些是"差评词"
  • 封装可直接调用的情感预测函数

数据集包含约 4.5 万条训练评论和 5000 条验证评论,正负样本基本均衡,标签 1 代表好评,0 代表差评。


二、数据探索与清洗

2.1 导入依赖库

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 读取训练集

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')}")
数据形状:(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 读取验证集

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')}")
数据形状:(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,统一做清洗。

# 去掉有缺失值的行
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())
['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 进行中文分词,并过滤掉停用词和标点符号。

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)

对训练集和验证集批量应用分词处理,并查看一条样本的处理前后对比。

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])
原  始: 一百多和三十的也看不出什么区别,包装精美,质量应该不错。
分词后: 一百多 三十 看不出 什么 区别 包装 精美 质量 应该 不错

可以看到,停用词"和""的""也"以及标点符号都被过滤掉了,保留了有实际语义的词语。


四、TF-IDF 文本向量化

分词之后,使用 TF-IDF 将文本转化为数值向量。TF-IDF 的核心思想是:一个词在某条评论中出现频率越高、在整个语料库中越罕见,它对这条评论的区分度就越大。

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)
训练集TF-IDF维度: (45010, 3000)
测试集TF-IDF维度: (4988, 3000)

max_features=3000 为例,每条评论被转化为一个 3000 维的稀疏向量。min_df=2 表示只保留至少出现在 2 条评论中的词,过滤掉过于生僻的低频词。


五、多模型训练与评估

5.1 训练三个模型

同时训练朴素贝叶斯、逻辑回归和随机森林三个模型,横向对比效果。

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 统一评估函数

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 评估结果

evaluate(y_test, y_pred_mnb, "MultinomialNB")
evaluate(y_test, y_pred_lr, "LogisticRegression")
evaluate(y_test, y_pred_rf, "RandomForest")
==================== 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 是个性价比不错的选择。


七、错误案例分析

把表现最好的逻辑回归预测错误的样本捞出来,看看模型在哪些评论上容易"翻车"。

""" 错误案例分析 """
# 把最好的模型——逻辑回归,预测错误的样本捞出来,看看是个什么情况
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']}")
总错误样本数量: 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:真实好评被预测为差评)。

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)}")
FP(真实差评→预测好评)数量:368
FN(真实好评→预测差评)数量:332

错误样本共 700 条,FP 和 FN 数量接近。从案例可以看出,模型容易出错的场景包括:

  • 反讽/委婉表达:如"非常垃圾的一本书,毫无价值,适合新股民入入门吧",句末的"入门吧"带有正向词,干扰了模型判断。
  • 混合情感:如"宝贝收到了,很漂亮……就是高仿的……为什么发来没有发票",前半段夸、后半段骂,整体标签是好评但含大量负面词。
  • 重复强调:如"被坑了"重复 15 次,但模型仍预测为好评,说明 TF-IDF 对重复词的权重提升有限。
  • 中性偏评价:如"一般般,穿着还不错",正负信号并存,标注本身也可能存在主观性。

八、逻辑回归特征权重解读

逻辑回归的优势之一是可解释性强。通过查看每个词的系数权重,可以直观地知道哪些词是"好评词"、哪些是"差评词"。

""" 查看逻辑回归特征权重 """
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)
最正向的十个词:
        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 + 逻辑回归这套方案在中文情感分析上的可解释性。


九、线上推理函数与自测

最后,将分词、向量化、预测整个流程封装成一个函数,输入任意一条评论即可输出情感判断和概率。

# 自测函数
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 条测试评论,覆盖明显好评、明显差评、中性评价、反讽等场景。

""" 自己写评论,测试 """
test_list = [
    "质量很好,物流快,非常满意,推荐购买。",
    "质量太差,刚收到就坏掉,非常生气不推荐",
    "东西一般,不算特别好也不算很差。",
    "包装破损,体验感很差,不会再来买了",
    "性价比很高,做工精细,物超所值",
    "好家伙,这个质量可真好,一天就坏了",
    "一般般,不过极具性价比!",
    "一分钱一分货,这个价格来说还可以。"
]

for txt in test_list:
    res = predict_sentiment(txt)
    print(f"评论: {txt}")
    print(f"结果: {res}\n")
评论: 质量很好,物流快,非常满意,推荐购买。
结果: 【好评】,正向概率: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,所以代码都是一段一段的看起来可能不方便,还请见谅!此外如果聪明的你发现了代码和表述有错误或者有更好的提议,可以在评论区写下你的建议,谢谢!数据下载:

posted @ 2026-09-07 16:55  myLv  阅读(0)  评论(0)    收藏  举报