简介:这是一份面向毕业设计或课程设计的完整机器学习实战项目,主题为商品评论情感分析,适合自然语言处理入门至进阶的学生参考。项目覆盖数据预处理、特征提取、模型选择与训练、结果评估全流程,并包含评论爬虫、GUI界面、Word2Vec、SVM、LSTM等实现,可对照学习从评论抓取到模型部署的完整思路。包内共43个文件,其中14个Python脚本负责爬虫、训练与测试,7个CSV和2个XLS为原始及处理后数据,3个pkl和2个h5为训练好的模型权重,其余配置文件与README便于复现环境。压缩包大小66.66MB,目录结构清晰,兼具代码、数据、模型和说明文档。已有167人学习下载,适合需要完整项目参考的毕业生或想快速上手情感分析任务的开发者。
1. 商品评论情感分析这个毕业设计题目,为什么值得认真做
一件商品下面几百条评论,人工逐条判断态度,看得眼睛发酸;放到一万条,你已经不可能看完。商品评论情感分析做的就是这件事——用机器学习模型把评论自动划分成好评、差评或中性,顺带告诉你每一条的置信度。选这个题目做毕业设计,理由很实际:数据好拿、方向明确、有完整的评估指标兜底,而且从朴素贝叶斯到预训练模型,每一层都有可写进论文的实验空间。适合有 Python 基础、想走 NLP 方向但不想碰硬件资源门槛的同学。下面按我实际搭过一套模拟项目X的顺序,把选型、预处理、训练、调参和翻车点一次讲清。
2. 先想清楚再做:问题界定、模型选型与项目骨架
2.1 情感分析的第一刀:二分类还是三分类
商品评论情感分析本质上是文本分类任务,但很多人上来就写代码,忽略了问题界定这一步。最常见的分歧是:做二分类(正/负)还是三分类(正/负/中性)。
我的建议是直接做三分类作为主实验,同时保留二分类作为对比实验。原因在于电商评论里有一类高频样本是中性表达:"还没用,先囤着""给朋友买的,不知道效果咋样"。这类评论在二分类标注里很难处理——标正不对,标负也不对,标注员之间的一致性会很低。你花在跟导师解释"为什么这条不算差评"上的时间,往往比训练模型还多。
标签体系建议用 0/1/2 分别对应负/中/正,标好之后做一个简单的标注一致性抽检。找两三个同学各标 100 条,算一下 Kappa 系数,低于 0.6 就要回头修改标注规范。这一步做到位,后面整个实验的结论才站得住。
2.2 模型选型的现实依据:传统机器学习与深度学习的取舍
毕设不是比赛,你的约束条件是时间、算力和论文讲述空间。我一般会把候选模型分成三档来选:
| 模型 | 数据量要求 | 训练耗时 | 效果基线 | 论文可写点 |
|---|---|---|---|---|
| 朴素贝叶斯 | 几千条可跑 | 秒级 | 偏低,但稳定 | 特征分析、误差样例 |
| 线性 SVM | 几千到几万条 | 分钟级 | 中等偏上 | 调参实验、对比实验 |
| LSTM/GRU | 建议至少 2 万条 | 小时级 | 依赖词向量质量 | 序列特征、消融实验 |
| 预训练模型 | 几百条就能迁移 | 需 GPU | 最高 | 微调策略、少样本对比 |
这套选型逻辑有一个关键点:先跑通传统机器学习,再考虑深度学习。很多同学的翻车经历是——第一天就上了 LSTM,然后花两周调不收敛,最后连一个能用的基线都没有。正确顺序是:用 TF-IDF + 朴素贝叶斯把基线跑出来,再用 TF-IDF + SVM 提一档,数据量和时间允许再上 LSTM。每个模型的实验数据、混淆矩阵、典型错误样本都是论文里的对比素材。
2.3 模拟项目X的目录结构:从一开始就别把代码堆成一座山
毕设项目拿到手,首先要做的是理清内部结构。一个规范的目录本身就说明你对工程有概念。我习惯用这样的骨架:
product_sentiment/ ├── data/ │ ├── raw/ # 原始评论数据,CSV 格式 │ └── processed/ # 清洗分词后的中间文件 ├── src/ │ ├── preprocess.py # 清洗 + 分词 │ ├── feature.py # TF-IDF / Word2Vec 特征 │ ├── train.py # 模型训练与调参 │ └── predict.py # 新评论预测入口 ├── models/ # 保存训练好的模型文件 ├── notebooks/ # 探索性数据分析 EDA └── result/ # 混淆矩阵、分类报告、图表每个模块只做一件事。preprocess.py 不出现模型代码,train.py 不重复写清洗逻辑。这样做的好处是:后期换模型、换特征、补数据时,只改对应模块,不用重写整条链路。你自己查 bug 的时候也会感激这个决定——"准确率突然掉了",先判断是数据变了还是模型变了,直接定位到对应文件。
3. 让模型真正理解中文评论:预处理与特征工程
3.1 清洗与分词:jieba 的标准流程和停用词策略
中文评论和新闻文本不一样,噪音密度非常高。商品评论里常见的噪音包括:URL、"追评:""颜色分类:红色"这类系统前缀、大量连续数字(订单号、货号)、表情符号、拼音缩写。清洗目标不是删干净,而是保留情感信息。
import jieba import re def clean_review(text: str) -> str: # 去掉 URL、@用户名、话题标签 text = re.sub(r'http\S+|@\w+|#\w+#', '', text) # 去掉 5 位以上连续数字,多为订单号/货号 text = re.sub(r'\d{5,}', '', text) # 保留 1-4 位数字,如"3天到货"里的时效信息 return text.strip() def tokenize(text: str) -> list: words = jieba.cut(clean_review(text)) return [w.strip() for w in words if w.strip()]注意一个细节:我没有把所有数字都删掉。"3天到货"和"用了一年才坏"里的数字是情感信号,删了模型就看不到时效类评价。只删长数字串就够了,那是订单号和规格编码的典型形态。
停用词表不要直接网上下载一个通用的就完事。通用停用词表会把"什么""怎么"删掉,但评论里"什么垃圾质量"恰恰是强烈负面信号。我的做法是:先加载通用表,再根据数据集里最高频的 50 个词人工过一遍,把明显和情感相关的剔除出去。这一步看着琐碎,但对效果的影响比换模型还大,属于典型的"数据决定上限"。
3.2 特征表示:TF-IDF 与 Word2Vec 的对比实验设计
特征工程是整个项目里性价比最高的环节。TF-IDF 的优势是稀疏、可解释、训练快,配合线性模型能直接看出哪些词推动了预测。Word2Vec 的优势是稠密、能捕捉语义相似性,但需要大规模语料预训练,自己拿几万条评论从零训练效果通常很差。
毕设里最稳妥的做法是两组特征都做,形成对比实验。TF-IDF 用 sklearn 实现,Word2Vec 用 gensim 在评论语料上训练,或者加载公开中文词向量。论文里可以写一组"特征表示对比实验":同一模型、同一数据划分,只有特征不同,比较 F1 的差异。这个实验设计干净且容易复现,答辩时也经得起追问。
3.3 最小特征管线:用 TfidfVectorizer 跑通第一版
from sklearn.feature_extraction.text import TfidfVectorizer tfidf = TfidfVectorizer( tokenizer=tokenize, ngram_range=(1, 2), # 加入 bigram,"不好吃""太差"这类组合能作为整体特征出现 min_df=2, # 只在整个语料中出现过 1 次的词直接丢弃,属于噪音 max_df=0.9, # 超过 90% 评论都有的词,基本是"的、了、是"级别,没有区分度 sublinear_tf=True # 词频取 1+log(tf),防止长评论靠堆词频压过短评论 ) X = tfidf.fit_transform(df["review_clean"]) y = df["label"] print("特征矩阵形状:", X.shape)这里最关键的两个参数是ngram_range和min_df。ngram_range=(1,2)意味着特征里同时包含单个词和相邻词对,"不 好吃"会被拆开,但"不好吃"作为 bigram 会被保留为一个特征,这对否定表达非常重要。min_df=2把只出现一次的词删掉,这些词往往是错别字或用户自定义表达,留着只会增加维度、引入过拟合。
max_df=0.9是很多人忽略的参数。像"好评""东西"这种词出现在绝大多数评论里,对情感分类没有区分度,但会在特征矩阵里占大量空间。控制这个参数后,模型训练速度和准确率都会有改善。如果你担心遗漏,可以用max_df=0.95做一次对比,看看指标变化,这也是论文里的一个小实验点。
3.4 Word2Vec 的接入方式和维度选择
如果你打算做深度模型对比,Word2Vec 特征可以这样接:
from gensim.models import Word2Vec from gensim.models.word2vec import LineSentence # 先准备分词后的语料文件,每行一条评论 sentences = [tokenize(line) for line in df["review_clean"]] w2v = Word2Vec(sentences, vector_size=128, window=5, min_count=2, workers=4) w2v.save("models/word2vec.model") # 句向量:对评论里所有词向量取平均 import numpy as np def sentence_vector(words: list, model, dim=128): vecs = [model.wv[w] for w in words if w in model.wv] return np.zeros(dim) if not vecs else np.mean(vecs, axis=0) X_w2v = np.vstack([sentence_vector(tokens, w2v) for tokens in df["review_clean"].map(tokenize)])vector_size=128是性价比比较高的选择,window=5是默认值,对于评论这种短文本基本够用。关键坑在于min_count=2:只出现过一次的词不进入词表,否则词向量质量会被低频噪音拉低。句向量取平均是最朴素的做法,LSTM 模型则可以直接拿词向量矩阵做输入,效果会比平均池化好一截。
4. 从训练到评估:跑通模型的关键步骤与调参思路
4.1 数据划分:时间序列评论不能随机打散
这是整个项目里最容易被忽视但后果最严重的陷阱。商品评论天然带时间属性,用户的表达习惯、商品的版本好坏都随时间变化。如果随机打散后划分训练集和测试集,模型会"偷看"未来数据,测试集上的指标虚高,答辩时一旦被问到"你的训练集和测试集有没有时间泄漏",场面会比较难看。
from sklearn.model_selection import train_test_split # 先按时间排序,再切分 df = df.sort_values("timestamp").reset_index(drop=True) cut = int(len(df) * 0.8) train = df.iloc[:cut] test = df.iloc[cut:] # 训练集内部再做分层划分,保证各类别比例一致 train, val = train_test_split( train, test_size=0.1, stratify=train["label"], # 分层采样,防类别比例偏移 random_state=42 ) print("train/val/test:", len(train), len(val), len(test))stratify参数的作用是让验证集和训练集的类别比例保持一致。如果好评占 70%、差评占 10%,不做分层的话,验证集里可能一条差评都没有,F1 的 macro 指标会剧烈抖动。random_state=42固定随机种子,保证每次运行结果一致,这是所有实验可复现的前提。
时间划分的代价是训练集和测试集的分布可能有差异,这恰恰是真实场景。论文里可以补一句:"我们按时间序列划分数据,以模拟模型上线后的真实表现",这句话本身就是加分项。
4.2 训练与调参:朴素贝叶斯、SVM 和 LSTM 的必调参数
先用朴素贝叶斯把基线跑出来。MultinomialNB的核心参数是alpha,它是拉普拉斯平滑系数,默认 1.0。对稀疏的 TF-IDF 特征而言,alpha=0.1到1.0之间的差异不大,但值得网格搜索一下。
from sklearn.naive_bayes import MultinomialNB from sklearn.metrics import classification_report mnb = MultinomialNB(alpha=0.5) mnb.fit(X_train, y_train) pred = mnb.predict(X_val) print(classification_report(y_val, pred, target_names=["负", "中", "正"]))SVM 这边我一般用LinearSVC而不是SVC(kernel="rbf")。TF-IDF 特征是稀疏高维的,线性核已经足够,RBF 核在这个场景下训练慢、参数多,收益很小。真正影响效果的是C值和类别不平衡处理。
from sklearn.svm import LinearSVC from sklearn.model_selection import GridSearchCV from sklearn.pipeline import Pipeline pipe = Pipeline([ ("tfidf", TfidfVectorizer(tokenizer=tokenize, ngram_range=(1, 2), sublinear_tf=True)), ("svm", LinearSVC(class_weight="balanced", max_iter=5000)) ]) param_grid = { "tfidf__min_df": [2, 3], "tfidf__max_df": [0.85, 0.9], "svm__C": [0.1, 0.5, 1.0] } gs = GridSearchCV(pipe, param_grid, cv=5, scoring="f1_macro", n_jobs=-1) gs.fit(train["review_clean"], train["label"]) print("最佳参数:", gs.best_params_)这里把 TF-IDF 和 SVM 封装进同一个 Pipeline,是为了避免一个经典翻车:分别在训练集和测试集上独立做向量化,导致特征维度对不上。在 Pipeline 里,fit只在训练集上学习词表和 IDF 权重,transform会复用同一套标准处理测试集。
class_weight="balanced"是处理类别不平衡最省事的方式,它自动给少数类更高的权重。max_iter=5000是因为 LinearSVC 在稀疏大特征上默认迭代次数偶尔不收敛,给足上限防止训练中断。网格搜索的scoring="f1_macro"也很关键——默认评分是准确率,在类别不平衡下会选出骗人的模型。
LSTM 阶段如果时间允许,核心参数是词向量维度、隐藏层大小、dropout 和 batch size。我的经验值是:词向量维度 128、LSTM 隐藏层 128、dropout 0.5、batch size 64、学习率 0.001,用 Adam 优化器。LSTM 在几万条数据上的提升有限,论文里把它定位为"对比实验之一"就够了,不要指望它吊打 SVM。
4.3 评估指标怎么看:准确率在类别不平衡时是陷阱
商品评论数据集天然不平衡,好评占比往往在 60%-80% 之间。一个什么都不做的模型,全预测成好评,准确率也有 70%,但这对差评识别毫无价值。所以评估要以 F1 的 macro 平均为核心,同时看混淆矩阵。
import matplotlib.pyplot as plt from sklearn.metrics import confusion_matrix, ConfusionMatrixDisplay cm = confusion_matrix(y_val, best_pred) disp = ConfusionMatrixDisplay(cm, display_labels=["负", "中", "正"]) disp.plot(cmap="Blues") plt.savefig("result/confusion_matrix.png", dpi=150)看混淆矩阵时重点看两点:差评被分到哪一类去了、中性类有没有被完全吞掉。常见情况是中性评论大量被分到好评——因为"还没用"在词面上确实偏正面。如果这个现象严重,可以考虑把中性类合并进"非差评",退回二分类任务,或者给中性类增加权重。这个决策本身也可以写进论文作为实验讨论。
5. 商品评论情感分析避坑指南:最容易翻车的五个点
5.1 "不好吃"被预测成好评:否定词被特征工程忽略
现象:模型的差评召回率只有 30%,大量含"不"的评论被归为好评。查看特征权重后发现,"好吃"是强正面特征,"不"是弱负面特征,两者组合后正面信号压过了反面。
原因:ngram_range=(1,1)只看单词,否定词"不"和后面的形容词被拆成两个独立特征。中文里"不"本身区分度弱,单独出现时模型很难利用它。
解决:把ngram_range调成(1,2),让"不好吃""不满意""不推荐"成为整体特征。如果数据里否定结构复杂,可以再加一层规则:检测到"不、没、无"出现在形容词前 2 个词范围内时,把该形容词的极性取反。这两条路都试一下,对比指标变化。
5.2 随机划分数据后测试集指标虚高:时间泄漏问题
现象:用train_test_split默认的随机划分跑出的 F1 是 0.87,按时间划分后掉到 0.81。
原因:评论数据有强烈的时间漂移。某个时间段的爆款词汇、商品质量和用户情绪会整体变化。随机划分时,训练集和测试集互相混杂了相似时间段的数据,模型相当于做了"开卷考试"。更严重的场景是同一个用户的多条评论被同时分进训练集和测试集,文本特征高度相似,测试分数虚高。
解决:所有实验统一采用"按时间排序后 80/20 划分"的策略。训练集内部再用stratify做分层。顺便检查有没有重复评论或同一 ID 的多条评论,做去重后再划分。
5.3 全预测成好评也有 75% 准确率:类别不平衡欺骗了你
现象:模型训练完成后,准确率 75%,但打开混淆矩阵发现差评类别几乎没预测对。查看类别分布,好评占 75%,差评占 10%,中性占 15%。
原因:模型学到了"无脑预测好评"这个最低风险策略,准确率虚高但业务价值为零。像 Accuracy 这类指标在不平衡数据上给出的信息量非常低。
解决:评估指标切换到f1_macro,训练阶段给模型加class_weight="balanced"或者在采样阶段做少数类过采样。还要把差评的召回率单独列出来跟踪,因为商品评论情感分析的核心价值往往在差评发现上——商家最关心的是"哪些用户不满意"。
5.4 新词和表情符号被停用词表误删
现象:某批新品的评论里高频出现网络新词如"绝绝子""yyds",但特征矩阵里根本没有这些词。查代码发现停用词表来自网上,里面恰好有"子"和"y",分词后"绝绝子"被拆成"绝"+"绝"+"子","子"被停用词表删除。
原因:通用停用词表是按一般文本统计的,对商品评论这种网络语料严重不适用。"子"在新闻里是虚词,在评论里是"绝绝子"的情感后缀。一刀切删词会把情感信息一起删掉。
解决:停用词表必须结合数据集自身统计来定制。做法是:分词后统计词频,人工检查高频词里哪些是真正无信息量的,再针对性地从停用词表里剔除。另外要对网络新词做一次盘点——用户调侃的"质量杠杠的""客服态度好到哭"里,每一个都有可能成为特征。给TfidfVectorizer传tokenizer时保留自定义词表,jieba.add_word("绝绝子")可以强制分词器把它当整体处理。
5.5 训练时清洗了、预测时忘了:线上效果雪崩
现象:模型在测试集上 F1 有 0.85,但拿几条真实新评论测试时结果一塌糊涂。检查发现 predict.py 里直接调用了模型接口,没有走清洗和分词。
原因:训练好的 Pipeline 里虽然包含了TfidfVectorizer,但清洗函数clean_review是在 Pipeline 之前手动调用的,没有封装进去。训练时数据已经清洗过,预测时没有清洗,文本形态完全不同,比如"http..."还在评论里、全角半角未统一,特征匹配不上。
解决:把清洗和分词也封装进 Pipeline 的可调用对象里,或者写一个统一的load_model_and_predict()函数,内部固定顺序:clean → tokenize → vecotrize → model。直接用训练好的 Pipeline 对原始文本执行pipe.predict([text]),保证训练和预测走同一条链路。这个教训我后来养成了习惯:任何处理逻辑只写一遍,封装成函数后统一调用,绝不在训练和预测两处各写一份。
6. 让毕业设计从"能跑"到"能答辩":一个低成本提升技巧
如果你做完全部传统模型实验,时间还有富余,我最推荐的投资是一个技巧:高效伪标注与困难样本挖掘——用模型的置信度分数反过来提升数据集质量,而不是直接上更重的模型。
具体做法分三步。第一步,对无标签评论做预测时,把置信度低于 0.4 的样本筛出来,人工标注一小批,加入训练集。这类样本就是模型的"知识盲区",补它们比随机补数据有效得多。第二步,对测试集里预测错误的样本做一次错误归因:分成"标注本身有问题""特征缺失""模型能力不足"三类。你会发现有相当比例的"错误"其实是原始标注错误,修正标注后再评估,F1 提升比换模型还明显。第三步,把高置信度但预测错误的样本作为重点分析对象,写进论文的"错误分析"章节——答辩时,能清晰说出"模型在哪些样本上犯错、为什么犯错、怎么缓解",远比展示一个漂亮的 F1 数字更有说服力。
这个技巧成本极低,只需要在 predict.py 里加一个pipe.predict_proba(text)调用,但它在答辩中的价值很高。我当年做模拟项目X时,就是用这个思路把差评召回从 0.68 拉到 0.79,其中超过一半的提升来自修正错误标注,而不是新换的模型。
最后说一个我的习惯:所有实验配置都记录在一个 YAML 文件里,包括数据版本、特征参数、模型参数、随机种子、指标结果。答辩被问到任何一个数字,三分钟内能给出完整复现链路。一个能复现的成果,和一个只存在于 notebook 里的结果,在答辩时的分量完全不同。做这个方向,最大的坑永远不是算法太难,而是你自己骗了自己——数据泄漏、指标选错、预处理不一致,任何一个都能让实验结果变成废纸。希望这些经验能帮你在做商品评论情感分析时少走几个弯路。
本文还有配套的精品资源,点击获取