简介:自然语言处理中的情感分析任务,旨在让计算机自动判断文本表达的情绪倾向,是舆情监测、用户反馈挖掘等场景的关键技术基础。在电商领域,商品评论中蕴含着大量关于质量、物流、服务等方面的真实用户感受,仅依赖系统自带的评分标签往往无法捕捉细节差异。情感分析通过文本分类技术,能有效区分正向、负向与中性评价,并输出置信度供业务决策。实现路线通常从TF-IDF结合逻辑回归的经典基线开始,其可解释性强、训练高效,适合快速验证;进一步可引入TextCNN等深度学习模型,利用卷积核捕捉局部语义组合,在短文本评论上取得更优效果。该技术已广泛应用于电商评论分析、舆情预警、客服工单分类等场景。本文围绕一套完整的中文商品评论情感分析项目,系统梳理了数据清洗、分词、特征构建、模型训练与评估的全流程实践,并提供可复用的代码与数据集。 先说说这个项目是哪来的吧。做电商运营那阵子,每天要跟一堆商品评论打交道,光是看“好评差评”的标签远远不够,用户真正吐槽的点、表扬的点,全都埋在自由文本里。后来我想着与其等人力去逐条标注,不如自己动手做一个“能自动判断评论情感倾向”的小系统。这个中文商品评论情感分析项目,就是从那时候开始攒起来的。整个项目包含完整的源代码、可复用的数据集、训练脚本和评估脚本,把评论数据从清洗、分词、建模到预测的完整链路走了一遍,适合想入门NLP实战、或者需要给商品评论做舆情监测的朋友直接参考。
这套项目的核心目标很明确:输入一段中文商品评论文本,模型输出它属于正向、负向还是中性(或者细分为好评/差评),并给出置信度。除了源码之外,我还把整理过、去重过、标注过的数据集一起打进了压缩包,方便你拿到手就能跑通,不需要再辛苦四处找数据。下面我把整个项目的设计思路、数据准备、模型实现、训练评估以及实际踩过的坑都梳理一遍,希望能帮你省掉几个晚上的摸索时间。
1. 项目整体设计与思路拆解
1.1 业务场景与核心需求
先回到最开始的业务问题。商品评论的典型量级是多少?我看过不少店铺后台的数据,一个爆款链接的评论数少则几千条,多则十几万条。用户评论里除了“好评”“差评”这种系统默认标签,更多的是一两句真实反馈,比如“物流很快但包装压扁了”“屏幕清晰,就是电池不耐用”。这类评论往往同时包含正面和负面信息,单靠标签判断会漏掉很多信号。
所以这个项目的第一个设计原则是:不做简单的两极分类,而是用“正向、负向、中性”三分类来做情感极性判断。这样既不丢失中性表达(比如“一般般”“还能用”),也能为后续精细化分析留出余地。第二个原则是给出置信度,模型输出要带概率值,这样下游系统可以设置阈值,置信度低的自动转人工复核。
1.2 技术选型:规则、机器学习还是深度学习
情感分析的技术路线,常见有四种:基于情感词典的规则方法、传统机器学习(TF-IDF加分类器)、深度学习序列模型(TextCNN/BiLSTM)、预训练语言模型(BERT/ERNIE)。
我在这套项目里是做了两条路线的对比,一套用朴素贝叶斯/逻辑回归跑TF-IDF特征,另一套用TextCNN做嵌入训练。为什么这么选?
- 纯词典规则方法实现最快,但是中文商品评论的口语化表达、网络新词非常多(比如“绝绝子”“yyds”),词典永远跟不上,而且规则没法处理否定词范围(“不是不好”到底是好还是不好)。
- 传统机器学习从TF-IDF特征开始,能覆盖词频和文档频率信息,适合做baseline,代码跑起来非常快,CPU就能搞定,对想快速上手的同学很友好。
- TextCNN是深度学习入门的经典模型,结构简单、训练速度快,在短文本分类上有稳定表现——评论正是典型的短文本,TextCNN的信息抽取能力足够用。
- BERT效果最好,但需要GPU环境,显存要求也高,对源码开源和易复现不太友好。
最终我把重心放在传统机器学习基线和TextCNN两个方案上,两者共享同一套数据预处理代码,你可以随时切换模型来对比效果,这是写代码时我有意保留的“性价比最高”的结构。
1.3 项目文件结构与代码组织
压缩包解开之后,目录是这样的:
sentiment-analysis/ ├── data/ │ ├── raw/ # 原始采集评论 │ ├── processed/ # 去重、清洗后的数据 │ ├── train.csv # 训练集 │ ├── valid.csv # 验证集 │ └── test.csv # 测试集 ├── models/ # 训练好的模型权重和词表 ├── src/ │ ├── __init__.py │ ├── preprocess.py # 文本清洗与分词 │ ├── features.py # TF-IDF / word2vec特征构建 │ ├── train_lr.py # 逻辑回归训练脚本 │ ├── train_nb.py # 朴素贝叶斯训练脚本 │ ├── train_cnn.py # TextCNN训练脚本 │ ├── predict.py # 单条/批量预测 │ └── evaluate.py # 评估指标输出 ├── requirements.txt └── README.md这种组织方式是我在多个项目里验证过的习惯:数据、模型、源码分离,训练和预测解耦。evaluate.py单独拎出来,避免每次训练完都要重新copy评估逻辑,后面调阈值、做badcase分析都会方便很多。
2. 数据集准备与处理
2.1 数据来源与采集原则
数据集是这类项目最关键的资产之一。我在压缩包里附带的数据集并不是从某个单一渠道抓来的,而是综合了三类来源:
- 公开的中文情感分析语料(类似ChnSentiCorp这类公开用途的评论数据)
- 爬取的公开电商评论(数量较少,仅用于扩充语料多样性,采集时严格遵守网站条款,并只保留非敏感内容)
- 我自己做标注的补充样本,主要针对一些网络新词和商品领域词汇
这里要特别说一句:如果你打算自己采集电商评论,务必先确认目标平台的服务条款,有些平台明确禁止爬虫。公开的数据集加上自己手工标注少量数据,通常是性价比最高、风险最低的组合。数据合规这一点别存侥幸心理。
2.2 数据清洗与去重
原始语料中最常见的问题有几类:网页HTML残留、重复评论(用户复制粘贴或数据采集重复)、异常标点、乱码、无效短文本。
清洗逻辑我写在preprocess.py里,核心步骤是这样的:
import re import pandas as pd def clean_text(text: str) -> str: # 去HTML标签 text = re.sub(r'<.*?>', '', text) # 去URL text = re.sub(r'http\S+|www\.\S+', '', text) # 去不可见字符 text = re.sub(r'[\x00-\x1f\x7f]', '', text) # 全角转半角(简版) text = text.replace(' ', ' ').replace(',', ',').replace('。', '.') # 合并多余空格 text = re.sub(r'\s+', ' ', text).strip() return text去重这块有个容易被忽略的细节:不是简单的drop_duplicates()就完事,评论经常是“同一个意思但字面略有不同”,比如“很好用”和“好用很好”,所以我额外做了一步基于哈希的近似去重,对每条评论计算文本的char-level n-gram哈希,再用相似度阈值去重。这一步能有效减少训练集里的重复噪声,防止模型对高频套话过拟合。
清洗之后还做了长度过滤:低于2个字符的评论视为无效(比如“好”、“差”这类极短文本,信息量太低且容易造成标签噪声),超过200个字符的长文本也会被单独处理,因为这类通常是复制粘贴的营销内容或说明书式评论,与真实用户体验差别较大。
2.3 标注策略与标签分布
我使用的是三分类标签:0表示负向,1表示中性,2表示正向。标注策略上有一条非常重要:先定规则,再标注,最后审核。
规则尽量简单:
- 出现明显正向词(满意、完美、推荐、超值)且无否定词修饰 -> 正向
- 出现明显负向词(差劲、失望、退货、垃圾)且无否定词修饰 -> 负向
- 既无明显正向也无明显负向,或同时有正负向表达 -> 中性
- “虽然...但是...”这类转折句,以后半句的情感为准
实际标注下来的分布大概是:正向约60%,负向约24%,中性约16%。这个分布其实符合电商评论的真实情况——大部分人只会在体验特别好或特别差的时候写评论,中性表达占比天然偏低。如果你发现自己的数据集分布差异极大,建议先做下采样或者调整损失函数里的类别权重,避免模型变成“全预测正向”的懒汉。
2.4 训练集/验证集/测试集划分
我采用的划分比例是8:1:1,并且刻意使用了分层抽样(train_test_split(stratify=y))。为什么强调分层?因为三分类分布本身就不均衡,如果随机划分,很可能验证集里负向样本特别少,导致评估结果虚高或偏低,不容易发现模型在少样本类别上的问题。
划分时还注意了一个点:同一商品的评论尽量放到同一个集合中。网上很多教程直接随机切分,会带来标签泄漏的风险——同一商品的相似评论可能同时出现在训练集和测试集里,模型在测试集上的表现虚高,真实场景上线的效果会明显缩水。这里我按商品ID做了分组切分,虽然会让数据利用变“浪费”一点,但评估结果更可信。
3. 核心实现细节与要点
3.1 中文分词与停用词
中文情感分析绕不开分词。我用的分词方案是jieba,但在实际项目中,光有默认词典不够。商品评论里有很多品牌名、型号、网络新词,比如“徕芬”“吹风机X3”“绝绝子”,默认词典根本分不对。
所以项目里增加了一个自定义词典user_dict.txt,格式很简单,一行一个词,可以带词频和词性:
徕芬 10 nz 绝绝子 5 nz 品控 10 n 踩雷 10 v加载方式:
import jieba jieba.load_userdict("src/user_dict.txt") text = "这款吹风机绝绝子,品控在线" words = jieba.lcut(text) # ['这款', '吹风机', '绝绝子', ',', '品控', '在线']停用词表我也单独维护了一份stopwords.txt,除了“的、了、是”这类常见停用词,还加上了评论里出现频率高但无情感含义的词,比如“东西”“感觉”“整体”“就是”。保留这些词会稀释特征维度,还会让TF-IDF的权重偏向无意义词。需要注意:不要一股脑把所有标点都去掉,尤其是感叹号“!”和省略号“...”,它们在评论情感里往往有强化信号,本文里“!!!”出现的评论,通常是强情绪表达,这个信息值得保留到特征中。
3.2 TF-IDF特征与逻辑回归基线
传统路线的特征我用TF-IDF。具体做法是:
from sklearn.feature_extraction.text import TfidfVectorizer vectorizer = TfidfVectorizer( token_pattern=r'\S+', max_features=50000, ngram_range=(1, 2), min_df=2, max_df=0.8, ) train_features = vectorizer.fit_transform(train_corpus)这里有两个关键参数值得展开说说。
第一是ngram_range=(1, 2)。单字/单词的TF-IDF丢失了词序信息,“不好”和“不 好”在分词后都包含“不好”这个词,影响不大;但“不是很好”这种表达,word ngram(2-gram)会同时捕捉“不是”和“很好”的组合,对否定语义很有帮助。我实际试过只用unigram,逻辑回归在验证集上的F1值低大约2个百分点,加了bigram之后明显回升。
第二是max_df=0.8。这个参数的作用是过滤掉在80%以上文档中都出现的词(比如“这个”“东西”),这些词对分类几乎无贡献,却会干扰模型的判断。min_df=2则过滤只在1条评论里出现的生僻词,避免模型记忆噪声。
分类器方面,逻辑回归是我首选的baseline,稳定性好、可解释性强。朴素贝叶斯我也放了训练脚本,但经验是:在评论这种内容高度重叠的语料上,朴素贝叶斯的条件独立性假设太强,效果通常比逻辑回归差3到5个百分点,所以实际应用我更推荐逻辑回归,朴素贝叶斯可以用来做快速pipeline的验证。
3.3 TextCNN模型的实现思路
深度学习的路线上,TextCNN是一个非常经典的选项。网上TextCNN的代码很多,但把细节写清楚并和完整训练流程串好的不多。我实现的网络结构是这样的:
import torch import torch.nn as nn class TextCNN(nn.Module): def __init__(self, vocab_size, embed_dim=128, num_class=3, filter_sizes=(2, 3, 4), num_filters=128): super().__init__() self.embedding = nn.Embedding(vocab_size, embed_dim, padding_idx=0) self.convs = nn.ModuleList([ nn.Conv2d(1, num_filters, (k, embed_dim), padding=(k // 2, 0)) for k in filter_sizes ]) self.dropout = nn.Dropout(0.5) self.fc = nn.Linear(len(filter_sizes) * num_filters, num_class) def forward(self, x): x = self.embedding(x) # (batch, seq_len, embed_dim) x = x.unsqueeze(1) # (batch, 1, seq_len, embed_dim) conv_outputs = [] for conv in self.convs: conv_out = conv(x).relu() # (batch, num_filters, seq_len, 1) pool_out = nn.functional.max_pool1d( conv_out.squeeze(-1), conv_out.size(2) ) # (batch, num_filters, 1) conv_outputs.append(pool_out.squeeze(-1)) x = torch.cat(conv_outputs, dim=1) # (batch, total_filters) x = self.dropout(x) return self.fc(x)几个容易踩坑的细节:
- 必须用
padding_idx=0,对应词表里的<pad>符号,这样补零的位置不会参与训练。 - 卷积核大小选2/3/4,对应短文本中的bigram、trigram、4-gram,这正好覆盖情感词常见的组合模式。
- max pooling之后特征维度固定,不依赖句子长度,这点对批量训练非常关键。
- Dropout设在池化之后、全连接之前,而不是放在Embedding后,实测对防止过拟合效果更好。
3.4 预训练词向量的选择与处理
Embedding层可以随机初始化也可以载入预训练词向量。我测试过两种方式:随机初始化和使用腾讯开源的Tencent AI Lab中文词向量(原谅我直接这么叫,它确实是在中文NLP里很大众的预训练向量)。
结果很清晰:在数据量较小(1万条左右)的情况下,使用预训练词向量让F1值提升了4个百分点。这不是魔法,而是因为预训练词向量包含了大规模语料的语义信息,模型不需要从零学习“性价比”和“划算”在语义上接近。
但用预训练词向量有个坑:词表匹配。如果只保留Embedding层中词表覆盖到的词,很多在向量表里没有的评论新词会被直接映射成<unk>,效果反而下降。我的处理方式是:词表里包含训练集所有词,一部分词加载预训练向量,未覆盖到的词用较小的随机均匀分布初始化,并在训练过程中微调整个Embedding层。epochs设3到5个即可,太小训练不充分,太大容易过拟合。
4. 实操过程与核心环节跑通
4.1 环境准备与依赖安装
先把环境搭好。项目requirements.txt内容大致如下:
jieba==0.42.1 pandas==1.5.3 scikit-learn==1.2.2 torch==1.13.1 numpy==1.24.3安装命令:
pip install -r requirements.txtCPU版本PyTorch也能跑训练。我所有实验都在MacBook的CPU上完成,1万条评论、50个epoch,TextCNN大概只需要5分钟,这个量级根本不需要GPU。如果你用GPU,记得把to('cuda')相关的条件判断代码保留好,train_cnn.py里已经写好了torch.device('cuda' if torch.cuda.is_available() else 'cpu')的逻辑。
4.2 数据预处理执行流程
打开项目后,建议按这个顺序跑:
cd sentiment-analysis python src/preprocess.py --input data/raw --output data/processed这一步会读取原始数据,清洗、去重、分词,并生成三个文件:train.csv、valid.csv、test.csv。每行包含label和text两列,text是已经分好词的文本(用空格分隔)。
手动检查一下生成的数据,这是很多教程不会强调但我强烈建议做的步骤。打开train.csv,快速浏览200条样本,确认以下几点:
- 标签是否和文本内容匹配
- 是否还有未清洗掉的异常字符
- 分词结果是否符合常识(比如“不 好吃”是否被正确切分)
这一步花10分钟,能让后面训练少走很多弯路。我见过不少人直接跳过去跑代码,最后模型指标莫名低,回查数据才发现清洗环节就有问题。
4.3 运行逻辑回归基线
传统机器学习路线的训练命令很简单:
python src/train_lr.py --train data/processed/train.csv --valid data/processed/valid.csv脚本内部会先做TF-IDF向量化,然后训练逻辑回归,最后输出验证集上的准确率、精确率、召回率和F1。我用的是solver='liblinear',因为在小数据集上它比lbfgs收敛更快,多分类问题也支持。正则化强度C我默认设成1.0,实际调参时可以在0.1~10之间搜索,这个参数对结果影响比较明显,网上很多代码直接写死,我特意在脚本里留了--C命令行参数方便调。
4.4 训练TextCNN模型的完整过程
TextCNN的训练入口是:
python src/train_cnn.py \ --train data/processed/train.csv \ --valid data/processed/valid.csv \ --epochs 30 \ --batch_size 64 \ --lr 0.001 \ --embed_dim 128训练过程中每轮都输出验证集F1,并保存效果最好的模型到models/best_cnn.pt。这里有个很实际的调参经验:不要只盯训练集loss,你会发现训练集loss一路下降,但验证集F1在某个epoch开始震荡退化。这时模型正在过拟合。
我观测到的典型曲线是:前8到10个epoch,验证集F1稳步上升;到15个epoch左右达到峰值;超过20个epoch后,验证集F1开始缓慢下降。所以最终我采用的早停策略是patience=5,即验证集指标连续5个epoch不增长就停止训练,并回滚到最优模型。
优化器方面我只用了Adam,初始学习率0.001。这个组合在实际项目中非常稳定,不需要过多调试。如果你用的是SGD,那需要warmup和更细致的学习率调度,对新手不友好,没必要。
4.5 评估指标详解与结果对比
评估脚本输出这样几类指标:
- 准确率(Accuracy):整体预测正确的比例。
- 精确率(Precision):预测为正类的样本中真的为正类的比例。
- 召回率(Recall):实际为正类的样本中被正确预测的比例。
- F1值:精确率和召回率的调和平均。
在不均衡分类的场景下,准确率是最有迷惑性的指标。如果负向样本只占24%,模型全部预测为正向,准确率也有76%,看起来很“不错”,但实际上负向样本一个都没识别出来。所以我在evaluate.py里额外输出每个类别的报告,并使用classification_report这种格式,方便逐类排查。
我跑出来的最终结果大概是这样的:
| 模型 | 准确率 | 宏平均F1 | 训练耗时(CPU) |
|---|---|---|---|
| TF-IDF + 朴素贝叶斯 | 0.863 | 0.815 | 10秒 |
| TF-IDF + 逻辑回归 | 0.891 | 0.852 | 20秒 |
| TextCNN + 预训练词向量 | 0.918 | 0.895 | 5分钟 |
注意我用的宏平均F1,即三个类别的F1取平均,这样能更公平地反映模型在少数类(中性)上的表现。TextCNN的优势在中性类上体现得最充分,它比逻辑回归多了大约6个百分点,因为中性评论往往含有混合情感,需要捕捉短语级语义。
4.6 预测模块与结果落库
训练完成后,预测模块提供了两种使用方式:
# 单条评论 python src/predict.py --model models/best_cnn.pt \ --text "物流很快,但包装盒有点压扁了" # 批量预测csv python src/predict.py --model models/best_cnn.pt \ --input data/new_comments.csv --output data/predict_result.csvpredict.py的输出会包含三列:原始文本、预测标签、置信度。置信度我取的是softmax概率的最大值。生产环境里,我一般会设定阈值:置信度低于0.6的预测,系统判定为“待人工复核”。这样能大幅降低误判带来的运营风险,尤其是差评漏检这种高成本错误。
用户评价情感分析项目的价值,并不仅仅在于训练了一个模型,而是能够把预测结果以结构化的方式落到业务系统里:按商品维度统计差评率、按时间维度观察情感趋势、按属性维度(物流、质量、服务)聚类问题,这些下游应用才是情感分析真正发挥价值的地方。
5. 常见问题与排查技巧实录
5.1 分词结果不对怎么办
最常遇到的问题就是分词不理想。比如“充电线”被切成“充电”和“线”,“不咋地”被切成“不”和“咋地”。
遇到这种情况,先不要急着改算法,最简单的方式就是往自定义词典里加词。词频参数一般写5~10,词性可以留空。我维护的user_dict.txt已经包含了大量商品评论常见词,但每个领域都有自己的黑话,你跑自己的数据时,先提取高频词,人工扫一遍,把明显有领域含义的词补进去,效果立竿见影。
如果加词还不够,再考虑引入jieba.suggest_freq调整个别词的词频。这个方法在教程里提到得不多,但实际很好用:
jieba.suggest_freq('充电线', True)这样jieba在切分时会更倾向于把“充电线”识别为一个整体。
5.2 训练时报维度不匹配的错误
这个错误很多人遇到过,尤其是改了embed_dim或者filter_sizes之后。TextCNN里卷积核的宽度必须等于Embedding维度,也就是Conv2d(1, num_filters, (k, embed_dim))中的embed_dim与nn.Embedding(vocab_size, embed_dim, padding_idx=0)的embed_dim要保持一致。如果你只修改了其中一处,另一处没改,就会报维度错误。
排查思路是检查三处是否同步:
- Embedding层的
embed_dim - 卷积层的
kernel_size第二个值 - 预训练词向量的向量维度(如果加载的话)
这是一个细节活,但也最容易通过报错信息定位,不用慌。
5.3 模型总是预测为同一个类别
模型整体预测集中在一个类别、其他类别识别不出来,大概率是标签分布不平衡导致的。解决办法有几种:
- 类别权重:在
CrossEntropyLoss中传weight参数,给少数类更高的权重。 - 过采样/欠采样:对少数类过采样,但要注意避免直接复制同样文本导致过拟合,可以用
SMOTE算法生成合成样本。 - 收集更多少数类数据:最朴素但也最有效,找更多中性评论,让模型看到更多“模棱两可”的真实表达。
我在项目里默认已经给CrossEntropyLoss加了跟类别频率成反比的权重,这样即使不调参,结果也不会太难看。
5.4 预测结果置信度整体偏低
置信度普遍在0.5到0.7之间,没有0.9以上的高置信样本,说明模型不够“自信”。这种情况,先看是不是训练轮数太少或者数据量太小。如果数据量只有几千条,这种现象很常见;加大数据量或使用预训练模型都能改善。
还有一个原因是中文评论里很多表达本身就模糊。比如“还行”到底算中性还是正向?“价格便宜但是质量一般”算正还是负?如果是这类问题,低置信度说明模型捕捉到了文本里的矛盾信号,这是合理的,不该强行提高。
5.5 数据文件里出现乱码
我打包的数据集统一采用UTF-8编码,但如果你自己采集的数据是GBK或GB2312,那么pandas.read_csv默认按UTF-8解析会乱码。处理方式是在读取时指定encoding='gb18030',这个编码兼容GBK且覆盖更多汉字,比'gbk'更稳:
df = pd.read_csv("data/raw/comments.csv", encoding="gb18030")另外,尽量在数据入库阶段就统一编码,别在清洗阶段来回转码,转来转去容易出问题。
结语与个人经验分享
这套项目从整理数据到跑通模型,前后迭代了差不多两周。我踩过的最大一个坑是:一开始直接拿原始评论做训练,没做垃圾评论过滤,结果模型把“有奖好评返现”这类营销套话学成了强正向信号,上线后一堆垃圾评论被标成高置信好评,整个指标虚高得离谱。后来在预处理阶段加入“疑似广告评论”过滤规则,并根据置信度做人工抽检,才把这个问题压住。所以最后想跟你认真说一句:数据清洗和数据合规的重要性,怎么强调都不过分。
如果你打算把这个项目继续往深做,我建议下一步可以尝试引入BERT等预训练模型,或者把单条评论的情感判断升级为“情感+属性”的联合抽取,比如识别出“物流慢”和“质量好”分别对应哪些商品属性。这样从情感分析走向细粒度的用户反馈挖掘,才是这类系统真正值钱的地方。希望这些代码和踩坑记录能帮你少走点弯路。
本文还有配套的精品资源,点击获取