☰
商品评论情感分析毕业设计:TF-IDF+逻辑回归的完整实践指南
2026/10/12 19:19:56 网站建设 项目流程

简介:面向计算机相关专业学生的商品评论情感分析毕业设计项目,采用机器学习与深度学习方法,覆盖评论爬取、文本预处理、特征向量化、SVM与LSTM模型训练及结果可视化等完整流程,适用于毕业设计、课程设计、项目初期演示或进阶学习。压缩包共44个文件,以14个Python源码文件为核心,同时包含CSV/XLS评论文本数据集、训练完成的h5与pkl模型文件、配置说明及演示脚本,整体大小约66.66MB,目录结构清晰便于按模块查阅。目前已有48人学习下载,项目代码经测试运行正常,另附详细文档与README,方便快速理解算法原理与工程实现;既可整体复现答辩项目,也可在现有框架上修改参数或扩展功能,满足不同阶段的实践需求。

1. 商品评论情感分析为什么看着简单、做起来却容易翻车

基于机器学习的商品评论情感分析,是机器学习课程设计选题里出现频率最高的文本任务之一,也是看起来门槛低、实际交付时最容易出问题的一类毕业设计。不少同学拿到项目的第一反应是直接上深度学习模型,但以我做这类项目的经验来看,模型只占工作量的小头,数据标注、文本清洗、特征工程和实验流程设计才真正决定分数。这个课题的核心是监督分类:把中文商品评论识别成好评、差评、中性三类,解决的是“评论量大到人工看不过来”的问题,适合想用最稳妥的方式完成毕设、又不想在部署环境上反复折腾的同学。

2. 数据准备是第一步:评论集怎么建、怎么标注、怎么平衡

2.1 评论集从哪来:公开数据配合自采小样本,标注三类标签

数据是情感分析的地基。在毕设场景里,我一般建议“公开数据集为主、自采小样本为辅”的搭配:公开的电商评论数据集能保证数据量,但领域相对偏老;自采小样本能体现你处理真实数据的能力,又不会把时间全耗在抓取和反爬对抗上。字段至少包括评论内容、评分星级、商品类目,其中评分星级是给评论打标签最直接的依据。

import pandas as pd # 读取公开评论数据,先确认字段名再动手 df = pd.read_csv("reviews.csv", encoding="utf-8") print(df.columns.tolist()) print(df.shape) # 常见做法:按评分星级映射成三分类标签 def rating_to_label(star): if star >= 4: return "positive" elif star == 3: return "neutral" else: return "negative" df["label"] = df["star"].apply(rating_to_label) # 看一眼标签分布,这一步几乎一定会触发不平衡处理 print(df["label"].value_counts(normalize=True))

这段代码里,star列名按你自己的数据集为准,先打印columns确认,不要想当然。映射到三分类而不是二分类,是因为“还行”“一般”这类中性表达在真实电商场景里占比不低,硬塞进二分类会让模型在两个类别之间剧烈抖动。normalize=True输出的是每类占比,如果positive超过六成,下面的平衡处理就必须安排上。

没评分的评论怎么办?只能人工标注。标注时先定清楚“只看最终态度、不逐句断章取义”,最好找两个人各标一遍再对结果,这个细节第 5 章会展开。答辩时评委的第一个问题永远是“数据哪来的、怎么标、分布如何”,提前把这段话准备成两分钟的叙述,比临时翻代码有效得多。

2.2 清洗与分词:正则去噪、jieba 切词、停用词表要保留否定词

所谓机器学习中的数据处理,在这个项目里具体是清洗、分词、去停用词、数值化四件事。商品评论文本比新闻语料脏得多,无意义符号、URL、HTML 标签、全角半角混用、重复标点经常混在一起,必须先用正则做一轮噪声清理,再交给分词器。

import re import jieba def clean_review(text): text = re.sub(r"<[^>]+>", "", text) # 去掉 HTML 标签 text = re.sub(r"http\S+", "", text) # 去掉 URL text = re.sub(r"[^\u4e00-\u9fa5a-zA-Z0-9]", " ", text) # 只保留中文、英文、数字 text = re.sub(r"\s+", " ", text).strip() return text # 自定义词典:避免“性价比”被拆成“性/价/比” jieba.load_userdict("user_dict.txt") # 停用词表读取,但“不、没、别、太、很”必须保留 STOPWORDS = {"的", "了", "吧", "啊", "呢", "呀", "嘛"} with open("stopwords.txt", encoding="utf-8") as f: for line in f: w = line.strip() if w and w not in {"不", "没", "别", "太", "很", "挺"}: STOPWORDS.add(w) def cut(text): words = jieba.lcut(clean_review(text)) return [w for w in words if w.strip() and w not in STOPWORDS]

这里最关键的是第三行正则:保留字母和数字是为了处理yyds、666这类网络词,它们在评论文本里是情感信号,不能当噪声丢掉。load_userdict要把“性价比”“掉电快”“售后”这类领域词加进去,否则分词器会把它们拆碎,TF-IDF 特征就再也拼不回来了。

停用词表是下面最容易翻车的地方。“的、了、吧”确实该过滤,但“不、没、别、太”这类否定词和程度词一旦被塞进停用词表,“不好吃”会变成“好吃”,整句情感直接反转。清洗完成后,建议把分词结果打印十行人工过一眼,确认“不好吃”没有被切开、yyds没有被丢掉。

2.3 正负样本不平衡:先看分布再决定欠采样还是类别加权

电商评论的分布天然偏向好评。商品只要不是特别差,四星五星能占六成以上,差评常常不足两成。如果不处理,模型学到的就是“把全部评论说成好评”的捷径,测试集准确率看起来不错,实际一个差评都抓不住。

# 方法一:统计并做简单的随机欠采样 from sklearn.utils import resample pos = df[df["label"] == "positive"] neu = df[df["label"] == "neutral"] neg = df[df["label"] == "negative"] n = min(len(pos), len(neu), len(neg)) pos_down = resample(pos, replace=False, n_samples=n, random_state=42) neu_down = resample(neu, replace=False, n_samples=n, random_state=42) neg_down = resample(neg, replace=False, n_samples=n, random_state=42) df_balanced = pd.concat([pos_down, neu_down, neg_down]) print(df_balanced["label"].value_counts()) # 方法二:不删样本,给模型损失加类别权重 from sklearn.utils.class_weight import compute_class_weight import numpy as np weights = compute_class_weight( class_weight="balanced", classes=np.unique(df["label"]), y=df["label"] ) print(dict(zip(np.unique(df["label"]), weights)))

欠采样实现简单,但会把大量好评样本直接扔掉,数据量小的场景里有点浪费。相比之下类别加权是性价比更高的做法:balanced会根据每类样本数自动放大少数类的错误惩罚,训练时间不变,数据一点不丢。毕业设计里我建议两种都跑一遍,把结果做成对比表放进论文,比单选一种更有说服力。

样本分布和处理的截图,从这一节开始就要按顺序存好。答辩材料的图表不是最后补的,是每做一个环节顺手截的,后面你会感谢这个习惯。

3. 特征工程与模型选型:先拿 TF-IDF 和传统机器学习把基线打出来

3.1 TF-IDF 特征:中文分词之后的向量空间

评论经过清洗和分词后变成了词列表,但分类器吃的是固定长度的数值向量。TF-IDF 就是把每条评论映射到词表上的稀疏向量:TF 看词在当条评论里出现多少次,IDF 看词在整个语料里稀不稀有,两者相乘之后,“性价比”这种有区分度的词会被抬起来,“这个”这类泛化词会被压下去。商品评论本身很短,低频词往往就是领域特征,所以 IDF 在这个任务里特别有用。

from sklearn.feature_extraction.text import TfidfVectorizer vectorizer = TfidfVectorizer( tokenizer=cut, # 复用上一章的分词函数 token_pattern=None, # 避免与自定义 tokenizer 冲突 ngram_range=(1, 2), # 保留“性价比高”这类二元短语 max_features=8000, # 限制词表规模,防止维度爆炸 sublinear_tf=True, # 用 1+log(tf) 平滑词频 min_df=2 # 只保留至少出现在 2 条评论里的词 ) X = vectorizer.fit_transform(df["content"]) print(X.shape)

ngram_range=(1, 2)是短文本任务里最容易出效果的一档:它保留“不是很好”这样的二元组合,让否定结构不会被词袋完全打散。max_features从 5000 开始试即可,不必盲目调大,词表超过一两万后模型效果基本不再提升,训练时间却成倍增加。

sublinear_tf=True的作用是防刷词。一条评论里写十遍“好”,不意味着“好”的权重应当线性涨十倍,用对数压缩更合理。min_df=2把只出现一次的噪声词过滤掉,新闻语料可以用 3 或 5,但短评论用 2 更合适,低了噪声多,高了特征稀疏。

3.2 表情符号与领域词典:用低成本特征逼近多模态信息

评论里信息密度最高的部分,有时候不只在中文词里,而在表情符号和网络变体词中。老用户习惯写“很满意”,年轻用户可能只发“😄”或者一句“绝绝子”。把表情符号和文本联合起来建模,是当前多模态情感分析在做的事,但毕设阶段不需要上图像模型,把这类信号手工提成特征列就够用。

def emoji_score(text): # 统计表情里的正负向信号:正向表情加分,负向表情减分 return text.count("😄") + text.count("👍") - text.count("😡") - text.count("👎") df["emoji_feat"] = df["content"].apply(emoji_score) def neg_word_count(text): # 否定词数量是 TF-IDF 容易漏掉的全局线索 return sum(1 for w in ["不", "没", "别", "无"] if w in text) df["neg_feat"] = df["content"].apply(neg_word_count) # 把两列特征拼接到 TF-IDF 稀疏矩阵右边 from scipy.sparse import hstack X_extra = df[["emoji_feat", "neg_feat"]].values X_all = hstack([X, X_extra]) print(X_all.shape)

emoji_feat在“东西还行😡”这类样本里是救命的特征:字面词是中性的,但表情已经暴露了真实态度。neg_feat弥补词袋模型的一个天然短板——否定词被拆到不同位置后,模型很难感知“不”到底修饰了谁,而统计整条评论的否定词数量至少能给出全局倾向。

特征拼接用scipy.sparse.hstack,稀疏矩阵和稠密列可以直接合并,sklearn 里的模型都能处理。唯一要注意的是特征列顺序必须固定,训练和预测时保持同一套拼接流程,否则模型上线后行为会变。

3.3 三个基线模型的选择逻辑:逻辑回归、朴素贝叶斯、SVM

特征定了,模型选型其实比想象中简单。文本经过 TF-IDF 后是高维稀疏向量,线性模型在这种输入上最稳定,训练也最快。我从逻辑回归起步,因为它有概率输出、权重直接对应每个词对情感的贡献,后续做错误分析时特别好用。

模型特点训练速度可解释性毕设推荐度
逻辑回归权重对应词汇贡献,支持概率输出快高首选基线
朴素贝叶斯小样本短文本表现好,但概率估计偏激进很快中备选对照
线性 SVM决策边界更锐利,配合 C 调参可防过拟合中中效果不足时再上

我一般先用逻辑回归把整个机器学习应用流程跑通,输出一张“正面贡献最大词 / 负面贡献最大词”的表,答辩演示时非常直观。SVM 在相同特征上通常比逻辑回归高 1 到 2 个百分点,但网格搜索调C的训练时间会长不少。朴素贝叶斯速度最快,可一旦特征做了 ngram 扩维,它的概率估计容易偏激,中性样本会被硬推向好差两极。

这里要讲清楚一个反直觉的结论:为什么优先传统机器学习而不是深度学习?商品评论平均只有几十个字,信息高度集中,词序带来的增益有限。TF-IDF 加逻辑回归在这个规模的任务上普遍能到 85% 附近,而 LSTM 在数据量不够、超参没调好的时候反而可能更差。深度模型的优势在于泛化到没见过的表达,但毕设的数据量撑不起这种优势。深度学习适合放在论文的扩展讨论里,主线用传统机器学习,主动权在自己手里。

4. 训练与评估:用 Pipeline 调参、混淆矩阵定指标、从误判里找论文素材

4.1 用 Pipeline 固定流程,避免训练预测两套预处理

特征工程一多,最常见的坑就出现了:训练时跑了一遍清洗加 TF-IDF,预测新样本时忘了做同样的处理,或者把fit_transform和transform混用,结果测试集成绩虚高、上线一测完全不是一回事。Pipeline 的意义在于把向量化和分类器打包成一个整体,fit 和 predict 自动走同一套流程。

from sklearn.pipeline import Pipeline from sklearn.linear_model import LogisticRegression from sklearn.model_selection import train_test_split, GridSearchCV, StratifiedKFold X_train, X_test, y_train, y_test = train_test_split( df["content"], df["label"], test_size=0.2, stratify=df["label"], random_state=42 ) pipe = Pipeline([ ("tfidf", TfidfVectorizer( tokenizer=cut, token_pattern=None, ngram_range=(1, 2), max_features=8000, sublinear_tf=True, min_df=2 )), ("clf", LogisticRegression(max_iter=1000, class_weight="balanced")) ]) # 类别不平衡时交叉验证必须分层抽样 cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=42) param_grid = { "tfidf__ngram_range": [(1, 1), (1, 2)], "clf__C": [0.1, 1.0, 10.0] } grid = GridSearchCV(pipe, param_grid, cv=cv, scoring="f1_macro", n_jobs=2) grid.fit(X_train, y_train) print(grid.best_params_) print(grid.best_score_)

stratify=df["label"]保证切分后三类比例和全量接近,差评本来就少,随机切分很可能让测试集里没有差评,导致评估结果完全失真。StratifiedKFold也是同样的逻辑,交叉验证的每一折都保持类别比例,分数才稳。

param_grid里的ngram_range和C是逻辑回归在文本任务上最值得调的两个参数。C是正则强度的倒数,C=0.1惩罚更强,适合特征噪声大的场景;C=10则更信任特征,适合信息密度高的评论。建议三个档位都跑一遍,不要凭感觉拍一个值。scoring="f1_macro"而不是accuracy,原因下一节直接说明。

4.2 评估指标怎么定:混淆矩阵、F1 和按类别拆解的精确率召回率

只看准确率是这个项目里最容易犯的错误。评论数据里好评占大头,模型把全部评论都判成好评,准确率也有六成以上,看着不错,实际一个差评都没抓到。做情感分析,从第三天开始就要学会看混淆矩阵和按类拆解的精确率、召回率、F1。

from sklearn.metrics import classification_report, confusion_matrix, ConfusionMatrixDisplay import matplotlib.pyplot as plt y_pred = grid.predict(X_test) print(classification_report(y_test, y_pred, target_names=["negative", "neutral", "positive"])) cm = confusion_matrix(y_test, y_pred) disp = ConfusionMatrixDisplay(confusion_matrix=cm, display_labels=["negative", "neutral", "positive"]) disp.plot() plt.savefig("confusion_matrix.png", dpi=200, bbox_inches="tight")

classification_report会打印每个类别的精确率、召回率和 F1。重点看差评的召回率:如果整体准确率很高但差评召回率低于 60%,说明模型在真实场景里形同虚设。混淆矩阵的对角线是正确预测;如果某一列明显偏大,说明模型倾向于把评论硬分到那个类别,比如全判成好评。

模型做完后,还要跟一个无脑基线对比:把所有评论全部预测成多数类,算它的准确率和 F1。模型必须明显高于这个基线才有意义。把基线、传统模型、深度学习模型做成一张三行对比表放进论文,答辩时非常经得起追问。

4.3 误判样本分析:反讽、网络变体和口味的三个坑

模型看错的样本不是随机的,翻来覆去就那么几类。第一类是反讽,“这质量真是绝了”字面看是正向,实际传达的是强烈不满,TF-IDF 丢掉了词序,“真是绝了”里的正词全部被当成正面证据。第二类是网络变体,“yyds”“绝绝子”“666”如果不在词表里,就落不到任何特征上。第三类是口味主观描写,“挺辣的”在火锅类目里是好评信号,在婴儿零食里是差评信号,而大多数训练集没有类目上下文。

这三类误判在答辩时是加分项,不是黑匣子。把误判样本收集成一张表格,每条样本记录预测标签、真实标签、模型置信度和猜测原因,然后在论文里单独写一节“错误模式分析”。主动讲清楚模型在哪里、为什么错、需要什么数据才能救,比藏着掩着效果好得多。

调参本身其实有点玄学味道,同一个参数在不同数据集上表现可能完全相反。与其反复赌参数,不如把精力花在误判分析上——每一类误判都对应一个特征工程方案,比如加表情特征、补领域词典、加类目特征。这样模型迭代有方向,论文素材也越攒越厚。

5. 毕业设计避坑清单:五个让项目返工的血泪教训

5.1 标注一致性差:两个人标出的结果对不上,模型学不动

现象:请同学帮忙给无评分评论打标签,同一句话一个人标好评、另一个人标中性,训练集里全是噪声,模型怎么调都停在 80% 以下。原因:标注前没有统一规则,混合态度句怎么处理没有约定,比如“东西一般,但物流很快”,有人按整体态度标,有人按最后一句标。解决:开工前先抽 30 条做试标,各标一遍后算标注重合率,把不一致的条目逐条讨论,形成书面规则。规则里明确“只看最终的购后态度”,中性表达不要硬分好坏,剩余争议样本用第三人投票解决。标注结果出来后再随机抽查 50 条做二次校验。

5.2 数据泄露:先 fit 全量语料再切分,让测试集成绩虚高

现象:模型在测试集上准确率 92%,部署到新数据上立刻掉到 75%,前后差距大得离谱。原因:一个非常隐蔽的操作——先拿全量评论算词频统计,再切分训练测试集。这样测试集里的词汇信息已经渗入 TF-IDF 词表,等于考试时提前看了答案。解决:切分必须先于所有fit步骤。TfidfVectorizer只对训练集调用fit_transform,测试集只用transform。自定义分词里如果用了全量语料训练的领域词典,同样属于泄露,用户词典只应基于训练集词频,或者在论文里单独说明。检查方法是在干净环境里从原始文件重跑一遍完整流程,对比前后分数。

5.3 分词器版本不一致:换台机器跑就报维度崩溃

现象:笔记本上模型正常,交到另一台机器上加载后一预测就报维度不匹配,或者报“特征数量不一致”。原因:换机器的环境里 jieba 版本不同,分词结果变了,词表也跟着变;训练时保存了 TF-IDF 矩阵,却没有保存向量化器对象。解决:requirements.txt里锁死jieba==0.42.1和 sklearn 版本,保存模型时把分类器和向量化器打包进同一个 pickle 文件。演示端加载同一个向量化器做transform,绝对不要在新环境里重新fit词表。这个坑我踩过一次,后果是全部重新训练。

5.4 只看准确率:92% 的准确率拦不住差评全漏

现象:测试集准确率 92%,把验证集里的差评翻一遍,十几条只抓出来两三条。原因:好评占比太高,模型学会了“无脑好评”的捷径,准确率被多数类样本堆积支撑。解决:评估指标换成 macro F1,打印classification_report逐类看精确率和召回率;训练时给逻辑回归加class_weight="balanced"。论文里把混淆矩阵和分类别指标一起放进去,不要只放一条准确率曲线。准确率唯一适合出现的位置,是在和基线模型对比的表格里作为一行参考。

5.5 一上来就上 LSTM:显存爆掉、训练半天不收敛

现象:有人不甘心只用传统模型,加了 word2vec 加 LSTM 的支线实验,结果 GPU 显存直接不够,内存也爆了一次。原因:Embedding 层词表开得太大,batch_size 设得太高,长文本没有截断,训练冗长且不收敛。解决:词表卡在 5000 左右,max_len统一截断到 64,batch_size 降到 32,加早停。更重要的是想清楚:评论短、信息局部集中,词序优势在这个任务上本来就不明显,LSTM 的收益可能只有一两个点,却要付出成倍的调试成本。主线用逻辑回归跑流程,深度学习作为扩展实验,只对比不大篇幅展开,时间分配要清醒。

6. 从模型到交付:把分类器打包成页面服务、录好演示视频

6.1 打包一个最小可演示的服务:pickle 落盘 + Flask 读取

毕设验收时,代码和文档再多,都不如现场跑起来一条新评论有冲击力。我用 Flask 做一个最小演示服务:页面输入一句评论,后端加载训练好的模型,返回情感类别和置信度。

import pickle from flask import Flask, request, jsonify app = Flask(__name__) # 模型和向量化器必须一起落盘,只存分类器是常见的翻车操作 with open("sentiment_model.pkl", "rb") as f: saved = pickle.load(f) clf, vectorizer = saved["clf"], saved["vectorizer"] @app.route("/predict", methods=["POST"]) def predict(): text = request.json.get("text", "") # 预测文本走和训练时相同的清洗与分词流程 X = vectorizer.transform([cut(text)]) proba = clf.predict_proba(X)[0] label = str(clf.classes_[proba.argmax()]) return jsonify({"label": label, "prob": proba.tolist()}) if __name__ == "__main__": app.run(host="0.0.0.0", port=5000, debug=False)

保存模型时用字典把分类器和向量化器包在一起,pickle.dump({"clf": ..., "vectorizer": ...}),加载时再一起取出来。只存分类器而丢掉词表,是这类项目最常见的后悔药场景。Flask 做演示已经足够,不需要上 gunicorn 和 Docker。host="0.0.0.0"让你可以在演示当天用手机连同一局域网访问页面,现场自由度更高,演示效果也更好。

6.2 演示视频与论文写作:把误判案例当亮点写进去

录演示视频前,先准备 8 到 10 条新评论,覆盖明显的正负面、中性表达,以及一条反讽句。开场先展示混淆矩阵和分类别指标,再切到服务页面逐条输入新评论。遇到模型误判反讽句,不要慌张,直接说“这个问题出在词袋模型缺少词序信息,是这类方法公认的边界”,这一段恰恰是最能体现你理解深度的地方。

论文写作时,不要只贴代码和准确率。把三块内容串成主线:一是基线模型与最终模型的对比表,说明每一处特征或参数带来多少提升;二是消融实验,去掉表情特征再看指标变化;三是误判分析表,配上 5 到 8 个典型样本。这三块做好,论文的含金量会明显高于同题目的平均水平。

我自己做这个题时最后悔的,是把前两周耗在调深度学习模型上,后来换回传统模型加完整评估流程,三天就把效果和论文素材补齐了。演示视频也因为只录了静态页面重录了一次。一个项目的成败,很多时候不在于模型多先进,而在于每一步有没有留下证据、有没有把边界摸清楚。希望这次整理的流程,能帮你少走这几步弯路。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询