简介:这是一份针对商品评论情感分析任务的机器学习毕业设计项目,完整包含可运行的Python源码、已标注的商品评论数据集、训练好的模型文件以及图形化操作界面;资源主要面向计算机相关专业正在准备毕业设计的学生,同时也适合需要项目实战练习的学习者,可作为课程设计、期末大作业的参考。项目经过严格调试,下载后即可运行,能帮助学习者快速掌握评论预处理、特征工程、模型训练与情感预测的完整流程。压缩包共43个文件,大小约66.66MB,文件类型覆盖Python脚本、CSV与XLS格式数据集、H5及Pkl保存的模型、XML工程配置、说明文档等,结构清晰易于检索;内容上包含SVM和LSTM两种情感分类模型、Word2Vec词向量训练、评论爬虫与数据清洗工具,以及基于PyQt的GUI展示界面,既能看到模型对比,也能直接体验交互效果。目前已有318人学习或下载,配有README说明和分模块目录,适合用于毕设答辩、课程设计或作为算法学习的落地范本。
1. 这个压缩包里到底是什么:商品评论情感分析为什么值得做成毕设
如果你也是临答辩前一个月才把那个python实现基于机器学习的商品评论情感分析源码+数据集+训练好的模型+GUI界面(高分毕设).zip解压出来,大概率和我当年第一次拿到这类毕设包时的反应一样:先翻代码,然后被一屏一屏的类和函数劝退,最后又回到起点问“它到底是怎么跑起来的”。这个压缩包本质上只讲了一件事:用 Python 读一批商品评论,通过机器学习把评论分成好评和差评,再套一个 GUI 界面让人能点着玩。它拿高分的原因不在某个算法,而在链路完整——数据清洗、中文分词、TF-IDF 特征、模型训练与保存、界面调用,每一环你都能指着它说清楚“为什么这样做”。这个方案适合时间紧、想拿应用型高分、并且愿意把一条链路讲明白的本科生;如果你打算背代码蒙混过关,我劝你换个题,因为答辩老师几句话就能问穿。
2. 从评论到特征:中文分词、停用词与训练/测试切分
2.1 为什么机器读不懂“好吃”:文本向量化的基本思路
商品评论情感分析本质是一个文本分类问题:给定一条“物流很快但包装破了”,判断它是正面还是负面。机器不认识中文句子,只认识数字,所以第一步必须把文本变成向量。常见做法是先把句子切成一串词,再统计每个词的重要性。比如“这家店的薯片很好吃”切成“这家、店、薯片、很、好吃”,分类器看到“好吃”这个特征权重高,就容易判正面。评论里还经常出现“不太行”“巨难吃”这种口语化表达,所以分词质量直接决定模型上限。需要先明确一点:这里说的机器学习,不是深度学习那一套又大又重的方案,而是 TF-IDF 加线性分类器,速度快、可解释、答辩也好讲。对短文本评论来说,这个组合已经是性价比最高的基线。
2.2 商品评论数据集的最简格式:CSV列名与标注约定
拿到压缩包以后,先去数据集目录看一眼。一份能直接用的商品评论 CSV,至少要有两列:一列是文本,一列是标签。常见列名是text和label,标签可以是“正面/负面”,也可以是“pos/neg”,为了方便建模我会把标签映射成 0 和 1。打开 CSV 时有一个容易忽略的细节:用 Excel 保存的 CSV 经常带 BOM 头,read_csv用默认utf-8读会在第一列列名上多一个\ufeff,后面取列名时疯狂报错,所以我一般直接用utf-8-sig。
import pandas as pd df = pd.read_csv("reviews.csv", encoding="utf-8-sig") df = df[["label", "text"]].dropna().reset_index(drop=True) df["label"] = df["label"].map({"正面": 1, "负面": 0}) print(df["label"].value_counts())逻辑说明:dropna()去掉空评论和空标签,随后的reset_index(drop=True)很关键,它让行索引重新从 0 开始连续排列,避免后续切分时索引错位。map把中文标签映射成 0/1,二分类模型只需要这两个值。最后用value_counts()看一下正负样本比例,如果一边明显偏多,就得在切分时用分层抽样,否则训练出来的模型会偏向多数类。
2.3 预处理全流程代码:清洗、分词、去停用词、切分
分词我一般用 jieba,它对中文口语支持不错而且零门槛。清洗阶段先把英文转小写,去掉对情感判断没用的标点和特殊符号,只保留中文、英文字母和数字。停用词表准备一个文本文件,每行一个词,读进来转成set,判断时速度更快。有一点要特别提醒:停用词表里不要放“不、没、别”这类否定词,否则“不好吃”会变成“好吃”,差评直接判成好评,这个坑几乎每届毕设都有人踩。
import re import jieba def clean_text(s: str) -> str: s = str(s).lower().strip() return re.sub(r"[^\u4e00-\u9fa5a-zA-Z0-9]", " ", s) stop_words = set() with open("stopwords.txt", encoding="utf-8") as f: for line in f: stop_words.add(line.strip()) def cut_words(s: str) -> list: text = clean_text(s) words = jieba.lcut(text) return [w for w in words if w not in stop_words and len(w.strip()) > 0]逻辑说明:正则[^\u4e00-\u9fa5a-zA-Z0-9]表示匹配所有不是中文、英文、数字的字符,把它替换成空格,这样“。”、“,”、emoji 都被清掉。jieba.lcut返回的是词列表,直接遍历过滤掉停用词和空白词。这里的stop_words是全局变量,如果后续把功能封装到多个文件,建议用类或参数传递,别靠全局变量跨文件引用,否则 GUI 阶段很容易出现“训练时停用词生效,预测时没生效”的灵异事件。
预处理完后,要马上做训练/测试切分。切分必须用stratify按标签比例分层,避免测试集恰好全是正面样本。
from sklearn.model_selection import train_test_split df["cut"] = df["text"].map(lambda t: " ".join(cut_words(t))) X_train, X_test, y_train, y_test = train_test_split( df["cut"], df["label"], test_size=0.2, random_state=42, stratify=df["label"] )逻辑说明:分词属于无监督预处理,不读取标签信息,所以在全量数据上切分词没有问题。但后面 TF-IDF 的词典学习必须在切分之后只靠训练集完成,如果对整个数据集拟合词表再切分,就等于让测试集信息混进了特征构建过程,这种数据泄漏一旦被答辩老师发现,前面的工作全部归零。
3. 训练并保存模型:TF-IDF、逻辑回归的参数与jolib落地
3.1 TF-IDF向量化:min_df与max_features怎么定
现在手上已经有一列以空格连接的分词结果,下一步是把它转换成 TF-IDF 特征。TF-IDF 的核心思想是:一个词在一条评论里出现次数越多越重要,但在越多评论里都出现它就越廉价。对商品评论来说,“好吃”这种情感词在少数好评里高频出现,权重会很高;“的”“了”这种词被停用词表清掉后,剩余低频词也能被min_df过滤。
from sklearn.feature_extraction.text import TfidfVectorizer vectorizer = TfidfVectorizer( min_df=2, max_features=10000, sublinear_tf=True, ngram_range=(1, 2) ) X_train_vec = vectorizer.fit_transform(X_train) X_test_vec = vectorizer.transform(X_test) print("训练集特征维度:", X_train_vec.shape)参数说明:min_df=2表示一个词至少出现在 2 条评论里才保留,只出现一次的词多半是错别字或输入噪声;max_features=10000限制特征总数,防止维度爆炸,也避免词表太大拖慢 GUI 加载速度;sublinear_tf=True对词频做1 + log(tf)平滑,防止“好吃”出现 20 次时权重线性飙升;ngram_range=(1, 2)同时保留单个词和相邻两词组合,这样“不好”能被当作一个整体特征,是缓解否定词误判的有效手段。注意这里fit_transform和transform是两个动作:训练集上拟合词表并转换,测试集上只用同一套词表转换,不重新拟合。
3.2 两个候选模型:朴素贝叶斯与逻辑回归的选型逻辑
特征准备好以后,模型选择其实很固定。朴素贝叶斯和逻辑回归是短文本分类里最常见的两个选项。朴素贝叶斯训练快、参数少,适合高维稀疏向量,但它的独立性假设在真实语言数据里并不成立;逻辑回归速度同样很快,而且每个特征都对应一个权重,能打印出哪些词让模型判定为差评,答辩时非常好讲故事。我的建议是都试一遍,最终交付用逻辑回归。
from sklearn.naive_bayes import MultinomialNB from sklearn.linear_model import LogisticRegression nb_model = MultinomialNB() nb_model.fit(X_train_vec, y_train) print("朴素贝叶斯准确率:", nb_model.score(X_test_vec, y_test)) lr_model = LogisticRegression(C=1.0, max_iter=1000) lr_model.fit(X_train_vec, y_train) print("逻辑回归准确率:", lr_model.score(X_test_vec, y_test))参数说明:C=1.0是逻辑回归正则强度的倒数,C越小正则越强,对高维稀疏文本来说能抑制过拟合;max_iter=1000给足迭代次数,防止默认 100 次不收敛时报警告。如果两个模型准确率差不多,优先选逻辑回归,因为你可以直接从coef_里拿出最有说服力的证据。
import numpy as np feature_names = vectorizer.get_feature_names_out() coef = lr_model.coef_[0] top_neg_idx = np.argsort(coef)[:10] print("模型认为最像差评的词:", [feature_names[i] for i in top_neg_idx])逻辑说明:coef_[0]是模型在“正面/负面”二分类任务里对每个特征的权重,权重越负说明该词越倾向差评。打印出这些词后,你会看到“差评、垃圾、漏发、客服、退货”一类真实业务词,这组结果可以直接放进毕设文档作为模型可解释性的证据。注意get_feature_names_out是较新版本 sklearn 的方法,旧版本用get_feature_names,以你当前环境为准。
3.3 保存整个pipeline:让GUI阶段不再重复造词表
训练完成后的核心问题是:如何把“特征构建 + 模型”这个整体保存下来。初学者最容易犯的错误是只保存分类器,等到写 GUI 时又重新构建一次 TF-IDF,结果词表对不上,预测结果全乱。正确做法是用Pipeline把向量器和分类器串起来,整体保存,GUI 阶段加载后直接对原始文本预测。
from sklearn.pipeline import Pipeline import joblib model = Pipeline([ ("tfidf", TfidfVectorizer( min_df=2, max_features=10000, sublinear_tf=True, ngram_range=(1, 2) )), ("clf", LogisticRegression(C=1.0, max_iter=1000)) ]) model.fit(X_train, y_train) test_acc = model.score(X_test, y_test) print("pipeline测试准确率:", test_acc) joblib.dump(model, "sentiment_model.joblib")逻辑说明:训练时直接传入X_train,而不再传入已经向量化的X_train_vec,因为 Pipeline 内部会先调用 TF-IDF 的fit_transform,再调用分类器的fit。joblib.dump把整个 Pipeline 序列化到磁盘,里面包含词表、IDF 权重、模型参数和类别顺序,GUI 加载时只要joblib.load回来就能直接.predict()。这个训练好的模型文件就是压缩包里最值钱的部分,一个文件替代了几十行预处理代码。另外建议在文档里加上运行环境清单,至少注明 Python 版本和scikit-learn、joblib的版本,否则换一台电脑加载极容易报错。
4. 给模型套上GUI外壳:用tkinter把预测做成可演示工具
4.1 为什么选tkinter:零依赖的GUI方案
这个标题里的 GUI 是很多人的心理门槛,总觉得界面应该很高端。实际上商品评论情感分析这种工具型应用,tkinter 已经足够了。选 tkinter 的理由很实在:Python 自带、不需要额外安装、打包也简单。PyQt 固然好看,但学习成本和打包体积都更大;如果只是做一个输入框加一个按钮,用 PyQt 属于杀鸡用牛刀。还有一个现实原因:答辩现场最怕环境出问题,tkinter 在 Windows 上只要 Python 装好了就不会缺依赖,用 VS Code 配好解释器后,直接python gui.py就能跑,少一条报错就少一分翻车风险。
4.2 GUI骨架:加载模型、单条输入与批量预测
GUI 的核心逻辑很简单:启动时用joblib.load加载训练好的模型,用户输入一条评论,点击按钮,程序对输入做和训练时完全相同的清洗与分词,然后交给模型预测,把结果展示在界面上。注意这里千万别省略清洗和分词,GUI 里拿原始句子直接塞给模型,等于让模型看一堆它没见过的话。
import re import tkinter as tk from tkinter import filedialog import joblib import jieba import pandas as pd STOP_WORDS = set() def load_stopwords(path): with open(path, encoding="utf-8") as f: for line in f: STOP_WORDS.add(line.strip()) def clean_text(s: str) -> str: s = str(s).lower().strip() return re.sub(r"[^\u4e00-\u9fa5a-zA-Z0-9]", " ", s) def cut_words(s: str) -> str: words = [w for w in jieba.lcut(clean_text(s)) if w not in STOP_WORDS and len(w.strip()) > 0] return " ".join(words) class SentimentApp: def __init__(self, root): self.model = joblib.load("sentiment_model.joblib") self.text_input = tk.Text(root, height=6, width=60) self.text_input.pack(pady=10) self.btn = tk.Button(root, text="分析情感", command=self.predict_text) self.btn.pack() self.result_label = tk.Label(root, text="", font=("Arial", 14)) self.result_label.pack(pady=10) def predict_text(self): raw = self.text_input.get("1.0", "end").strip() if not raw: self.result_label.config(text="请输入评论") return cut = cut_words(raw) proba = self.model.predict_proba([cut])[0] label = 1 if proba[1] >= 0.5 else 0 text_label = "正面" if label == 1 else "负面" self.result_label.config(text=f"情感: {text_label}, 负面概率: {proba[0]:.2f}") load_stopwords("stopwords.txt") root = tk.Tk() root.title("商品评论情感分析") app = SentimentApp(root) root.mainloop()逻辑说明:predict_proba返回的是各类别概率数组,proba[0]是负面概率,proba[1]是正面概率,这里用 0.5 作为决策阈值。加载模型后,predict和predict_proba内部会调用同一个 Pipeline,所以词表、清洗、向量化都不会有偏差。这个界面已经是可用的最小版本,如果你觉得不够展示,可以再加两个按钮:一个“清空”,一个“批量预测”。批量预测的思路是用filedialog打开 CSV,对每行文本做同样的分词后调用model.predict,把结果写回新列再另存。
def predict_batch(self): path = filedialog.askopenfilename(filetypes=[("CSV files", "*.csv")]) df = pd.read_csv(path, encoding="utf-8-sig") df["predict"] = df["text"].map(lambda t: self.model.predict([cut_words(t)])[0]) out_path = path.replace(".csv", "_result.csv") df.to_csv(out_path, index=False, encoding="utf-8-sig")逻辑说明:批量预测接口在毕设演示时很加分,可以现场展示模型对几十条未见评论的分类结果,比单条输入更有说服力。输出文件继续用utf-8-sig,确保老师在 Windows Excel 里打开不下乱码。这里还隐含一个问题:如果 CSV 里文本很长、数据量很大,预测过程会卡住界面,属于典型的主线程阻塞问题。简单处理是预测前把按钮设为禁用状态,预测完再恢复,或者用threading.Thread把批量任务放到子线程,GUI 只负责刷新结果。
4.3 解压后的目录结构:模型、词表与代码的组织方式
拿到这个 zip 之后,建议不要把所有文件堆在一个目录里直接开跑,先整理成一个清晰的结构。常见做法是分成data/、models/、ui/三层,数据、模型、界面代码互相隔离,提交作业时也显得专业。我一般会组织成这样:
| 文件/目录 | 作用 | 说明 |
|---|---|---|
data/reviews.csv | 原始标注数据 | 至少包含 text 和 label 两列 |
data/stopwords.txt | 停用词表 | 每行一个词,不含否定词 |
models/sentiment_model.joblib | 训练好的完整 Pipeline | GUI 加载的核心文件 |
train.py | 训练脚本 | 从 CSV 读到模型保存全流程 |
gui.py | 图形界面入口 | 加载模型并提供交互预测 |
路径问题非常值得注意:在 GUI 代码里写死"models/sentiment_model.joblib"这种相对路径,直接跑没问题;一旦用 PyInstaller 打包成 exe 换个目录双击运行,工作目录变了,文件就找不到。后续打包时务必用可执行文件所在目录或sys._MEIPASS拼接路径。
5. 避坑:训练集污染、预测不一致与打包后的暗坑
5.1 标签与文本错位:模型“表现很好”但实际全错
现象:训练过程一切正常,准确率超过九成,但随机抽几条肉眼可见的差评,模型预测结果却很离谱。
原因:绝大多数情况是行索引错位。dropna()删掉空行后如果没有reset_index(drop=True),DataFrame 的索引会留下空洞;后面切分或者循环时一旦用旧索引去取值,X和y就对不上了。更隐蔽的是在read_csv之后先做了一次sort_values,行序发生变化但标签列没有跟着“绑在一起”处理。
解决:所有清洗和筛选操作完成后立刻执行df.reset_index(drop=True),并且让X和y从同一个 DataFrame 里取,不要各取各的再合并。我习惯把切分写成一条命令,保证两者永远同时被切分。
5.2 训练和预测两套预处理:准确率不错但GUI一用就废
现象:训练脚本里测试集准确率 0.89,模型和数据都保存好了,结果 GUI 里输入“这破手机用了三天就死机”,模型给了个“正面”。这是整类项目里最常见的翻车。
原因:训练时 Pipeline 里的 TF-IDF 接收的是“空格连接的分词结果”,而 GUI 里如果没有调用同一个cut_words,直接把原始句子传给model.predict,TF-IDF 词表里全是词和二元词组,原始句子里的词根本没被切开,特征全部失效。
解决:把clean_text、cut_words和STOP_WORDS相关代码直接从训练脚本复制到 GUI 脚本,保证两边是同一套逻辑。更稳妥的方案是写一个build_features.py公共模块,训练和 GUI 都去导入它。交付前用一条训练集外的评论手工验证 GUI,而不是只看训练脚本的输出。
5.3 停用词表把否定词删了:差评识别率骤降
现象:模型对“不好吃”“不满意”“不值得”几乎全部判错,但直接输入“垃圾”这类强情感词又能判对。
原因:网上随手找的通用停用词表里往往包含“不、没、无、别”这些否定词。jieba.lcut会把“不好吃”切成“不、好、吃”,过滤停用词后变成“好吃”,语义完全翻转。这是情感分析特有的坑,通用停用词表不能直接照搬。
解决:加载停用词表后,把常见的否定词集合剔除掉,例如执行stop_words = stop_words - {"不", "没", "别", "无", "莫"}。同时 3.1 节里的ngram_range=(1, 2)也起到兜底作用,“不好”作为二元组合特征被保留下来,模型就有机会学到“不”对情感的转向作用。
5.4 joblib模型换机加载报错:本地能跑,老师电脑闪退
现象:训练完的joblib文件在你自己机器上加载正常,发给老师或者换一台实验室电脑再运行,报出各种ValueError、AttributeError,核心信息指向 sklearn 的某个类找不到。
原因:joblib 和 pickle 一样,序列化的是 Python 对象的类路径。scikit-learn 不同版本之间类的内部结构会变,旧版本训练出来的对象拿新版本加载,经常出现反序列化失败。同理,joblib 无法可靠地序列化 pipeline 里的lambda匿名函数,如果特征构建时图方便用了 lambda,换机加载报错的概率更高。
解决:交付时附一份requirements.txt,写明scikit-learn==版本号、joblib==版本号、pandas==版本号,老师机器上按这份文件安装依赖。代码里不要写 lambda,统一用命名函数。如果你的目标环境版本跨度很大,彻底的办法是导出成 ONNX 格式再用 onnxruntime 推理,不过对毕设来说锁版本已经足够。
5.5 打包exe后找不到模型文件:双击没反应或窗口闪退
现象:用 PyInstaller 打包出来的 exe 在自己机器上双击就闪退,命令行运行时提示找不到sentiment_model.joblib或stopwords.txt。
原因:打包后的资源文件路径和源码目录路径不一样。PyInstaller 默认把所有文件释放到一个临时目录,代码里的相对路径指向的是当前工作目录,而不是临时解压目录,所以模型加载失败,GUI 直接退出。这不属于玄学,是路径解析问题。
解决:运行时先判断是否处于 PyInstaller 环境,再决定资源根目录。常规写法是:
import os import sys def resource_path(relative_path): base = getattr(sys, "_MEIPASS", os.path.dirname(os.path.abspath(__file__))) return os.path.join(base, relative_path) model_path = resource_path("models/sentiment_model.joblib") stopwords_path = resource_path("data/stopwords.txt")逻辑说明:打包后的 exe 在运行时会有一个sys._MEIPASS属性指向解压临时目录,getattr的第二个参数是源码运行时的目录,这样两种环境下都能找到文件。打包命令里还需要用--add-data "models/sentiment_model.joblib;models"把模型和停用词表带进去。每次改完打包配置,都去一台没有 Python 环境的机器上实测一次,这是最稳妥的验收方式。
6. 从能跑到能答辩:评测指标、演示脚本与多模态进阶方向
很多人在毕设文档里只写一个准确率,这是最容易被追问的漏洞。准确率在正负样本均衡时还凑合,如果数据集本身 80% 是好评,模型全判好评也有 80% 准确率,这种话在答辩老师耳朵里等于说自己没做评估。所以评测部分至少打印一份precision、recall、f1-score,再看一眼混淆矩阵,把每类里最容易误判的几条评论摘出来,准备一页 PPT 专门讲“模型在哪些评论上犯错”。误判样例反而是答辩亮点,说明你真正看过数据,不是只会调包。
演示脚本也要提前设计。我一般会让用户在现场输入一句没见过的、带口语的评论,比如“这价格还要啥自行车”,模型需要同时处理俗语和复合情感,这比照着 PPT 念准确率得分高。演示前给自己列一个检查清单:模型文件路径是否生效、停用词表是否加载、输入空文本是否会崩、断网状态下 GUI 能不能完整跑通。断网这一步特别重要,答辩现场的网络状况永远是黑匣子,你得保证这套方案离线可用。
如果你时间充裕,想冲更高的分,可以在现有文本模型上叠加商品图片特征,往多模态情感分析方向延伸,具体做法是用预训练的视觉模型抽取评论配图的深度特征,和文本的 TF-IDF 特征拼接后再训练一个分类器。这个方向比单纯调参有意义,也是当前热搜里多模态情感分析比较受关注的原因。但注意控制工作量,先保住文本基线的完整交付,再考虑扩展。
我做这类题目养成的习惯是:所有代码交付前,一定断开网络,从空白工作目录重新执行一遍完整流程——训练、保存、加载、GUI 预测。那次经验帮我躲过了运行环境导致的现场翻车。希望帮到你。
本文还有配套的精品资源,点击获取