简介围绕基于机器学习的商品评论情感分析这份毕业设计压缩包面向计算机专业学生可用于课程设计、毕设答辩或自然语言处理入门实战。项目涵盖评论爬取、数据清洗、分词、情感词典构建、词频-逆文档频率与词向量特征提取以及朴素贝叶斯、支持向量机、极端梯度提升、长短期记忆网络等多种模型的训练与评估并提供图形界面和可视化脚本能展示完整的分析流程。资源共43个文件、约66.66MB以Python脚本、表格数据、模型文件及配置文档为主包含14个Python程序、7个CSV数据文件、2个Excel表格和多个训练好的情感分类模型与词向量模型适合直接运行和二次修改。目前已有167人学习下载。总体而言这份压缩包覆盖从数据采集到报告撰写的完整环节并补充了Git版本管理、超参数搜索等实践细节是一份能支撑实际答辩和功能演示的毕业设计资料。1. 情感分析不是看“好评/差评”那么简单这个毕业设计到底在做什么你可能以为“商品评论情感分析”就是把五星好评算正面、一星差评算负面然后用机器学习算法跑一个准确率就完事。但真正动手抓过京东或淘宝评论的人都知道评论区里充斥着“默认好评”的模板文本、和商品毫不相干的凑字评价、还有那种“物流很快但质量稀烂”的混合情感。这个基于机器学习的商品评论情感分析项目核心不是模型有多深而是你能不能把“用户到底对商品持什么态度”这件事从一堆噪声里可靠地抽出来。它适合正在做机器学习课程设计或毕业设计的同学也适合想入门 NLP 分类任务的从业者。本文把从爬虫、清洗、特征工程到模型调参与评估的完整链路拆开讲给你一套能照着复现的落地方案。2. 定技术路线先想清楚数据从哪来、模型学到什么再谈准确率2.1 数据源选型为什么“爬京东评论”是主流选择这个项目的数据来源常见做法是爬取京东商品评论。京东的评论区有一个半公开的 JSON 接口比淘宝的反爬策略温和得多返回字段结构化程度高自带评分、追评时间、购买属性等元信息非常适合做情感分析的初始数据集。而且京东评论区分“好评”“中评”“差评”三个标签页方便你直接拿到标注数据做有监督学习。爬虫的请求头里必须带上User-Agent和Referer否则京东会返回 403。接口核心参数是productId商品 ID、score0 全部 / 1 好评 / 2 中评 / 3 差评、page和pageSize。要注意的是京东这个接口单页最多返回 10 条评论翻页超过一定深度会被风控拦截所以做毕业设计的话多换几个商品 ID 比死磕单商品翻页更稳妥。当接口返回的commentsCount为 0 或code不为0时说明被限流了此时不要继续请求退避 3 到 5 秒再试。代码里要保留原始响应文本方便排查编码问题。抓下来的数据至少要有四个字段评论内容、评分、点赞数、评论时间。评分是后文标注情感标签的基础而点赞数可以作为“这条评论是否具有代表性”的辅助特征。import requests import json import time def fetch_jd_comments(product_id, score0, page1, page_size10): url https://club.jd.com/comment/productPageComments.action params { productId: product_id, score: score, sortType: 5, # 按时间排序保证每次抓取顺序稳定 page: page, pageSize: page_size, # 固定10接口不接受更大的值 isShadowSku: 0, fold: 1, } headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36, Referer: fhttps://item.jd.com/{product_id}.html, } resp requests.get(url, paramsparams, headersheaders, timeout15) data resp.json() if data.get(code) ! 0: print(fpage {page} 被限流任务中止) return [] rows [] for c in data.get(comments, []): rows.append({ content: c.get(content, ), score: c.get(score), recommend: c.get(recommend, ), comment_date: c.get(creationTime, ), product_id: product_id, }) return rows if __name__ __main__: rows fetch_jd_comments(100012043978, score0, page1) print(f抓取到 {len(rows)} 条评论示例内容: {rows[0][content][:30]})参数里sortType5是按时间排序如果不固定排序方式翻页时可能拿到重复评论影响后续去重统计。isShadowSku和fold这两个参数保持默认即可基本不需要改动。建议把抓取结果实时落盘成 JSON 或 CSV避免中途断网导致全部重跑。我一般会加一个断点续爬逻辑用已抓取的文件行数作为下次抓取的起始页但毕设规模的数据量通常不需要这么复杂手动分段跑就够了。2.2 分类粒度二分类、三分类还是五分类分类粒度决定了项目的工程复杂度和评估方式。最省事的是二分类把评分 4 到 5 星视为正面1 到 2 星视为负面3 星直接丢弃。这样边界干净类别均衡也好控制。但答辩时容易被老师追问“中评为什么不要”所以如果你的数据量能支撑建议做三分类好评 / 中评 / 差评对应评分 5 / 3 / 1把 4 星和 2 星作为模糊地带并入相邻类别。五分类看上去更细致但负面样本通常不够且 1 星和 2 星的文本差异极小——“一般般”和“很差”之间的边界词向量根本分不开模型容易在相邻类别上乱跳。我做这个项目时选了四分类好评、差评、中评、默认好评模板文本单独归一类。这样既避免了模糊地带的标注噪声也把京东特有的“此用户未填写评价内容”这类垃圾样本隔离出去不污染正文模型。标注映射关系建议做成配置文件或一个清晰的函数不要散落在代码里。后续如果要换分类粒度只改这一处映射即可别的地方全部复用。def score_to_label(score): if score in (5,): return positive elif score in (4, 3): return neutral elif score in (2, 1): return negative else: return unknown import pandas as pd df pd.read_csv(jd_comments.csv) df[label] df[score].apply(score_to_label) print(df[label].value_counts())这个映射有个细节京东评分是离散的 1 到 5但不同商品的评分分布差异很大有的商品 4 星偏多有的 3 星偏多。直接用评分映射会产生类别不均衡后面的处理章节会详细讲。在这个阶段你要关心的是每个类别的样本量是否能撑起训练我一般要求每个类别不少于 3000 条否则模型学不到可靠模式。2.3 技术栈与评估口径先定标准再谈调参技术栈的选择要匹配你的运行环境。如果只有 CPU就不要一上来就上 BERT训练一个 epoch 可能要几小时答辩前很容易翻车。最稳的组合是Python pandas jieba scikit-learn模型用朴素贝叶斯或逻辑回归打底时间充足再试试 LSTM。BERT 或者transformers这类深度模型可以作为加分项写在“后续工作”里不用强行跑通。评估口径要提前想清楚。类别不均衡时准确率是骗人的——如果负面样本只占 5%全部预测成正面也有 95% 的准确率。正确的评估指标是宏平均 F1macro-F1和混淆矩阵。宏平均 F1 把每个类别的 F1 单独算再取平均能真实反映小类别的表现。这个口径从第一篇实验开始就固定下来中途不要换否则前后对比没有意义。from sklearn.metrics import classification_report, f1_score y_true [...] # 真实标签 y_pred [...] # 模型预测标签 report classification_report(y_true, y_pred, target_names[negative, neutral, positive]) print(report) # 重点看 macro avg 那一行的 f1-score如果你用的是 scikit-learn 的接口classification_report会一次性给出每个类别的精确率、召回率、F1 和样本数。我一般要求宏平均 F1 稳定在 0.80 以上才算及格0.85 以上是良好0.90 以上需要比较干净的语料才能达到。数据噪声很高的情况下0.80 就是很好的结果了不要盲目追求高指标。3. 把京东评论变成干净的训练集清洗、去重与标注修正3.1 清洗去掉模板文本、HTML 标签和超长噪声京东评论区有大量非自然语言的文本。最常见的三类一是“此商品太棒了我很喜欢”之类的系统默认好评提示二是用户粘贴的商品参数或活动链接三是纯表情或“哈哈哈哈”这类无信息量文本。清洗的目标不是把文本变漂亮而是把模型不需要学的噪声去掉否则模型会把“默认好评”学成一个独立类别而不是学到真实的情感表达。清洗顺序很关键。先处理 HTML 实体和解码问题再做长度过滤最后做关键词去噪。如果先做长度过滤含大量标签但正文很短的文本会被误删。我一般用正则把[a-zA-Z];这类 HTML 实体替换成空格再把http[s]?://\S替换成空字符串。长度过滤的阈值设在 2 到 100 字之间低于 2 字的没有信息量高于 100 字的可能是复制粘贴的评测文章对情感分类没有帮助。import re import pandas as pd html_pattern re.compile(r[a-zA-Z];) url_pattern re.compile(rhttps?://\S) def clean_text(text): if not isinstance(text, str): return text html_pattern.sub( , text) text url_pattern.sub( , text) text re.sub(r\s, , text).strip() return text df pd.read_csv(jd_comments.csv) df[clean] df[content].apply(clean_text) df[len] df[clean].apply(len) df df[df[len] 2]这一步要输出一个清洗后可读的 CSV并肉眼抽样检查 200 条左右。检查的目的不是看清洗规则对不对而是看有没有漏网的异常文本。如果发现某种噪声频繁出现就把它对应的正则或规则补进去。清洗是迭代的过程不要指望一次到位。常见的漏网之鱼是“此用户很懒什么都没有留下”这类半模板文本它包含真实情感吗不包含需要单独过滤。3.2 去重同一个用户在不同商品下的重复评论京东用户会在多个商品下复制粘贴同一条评论尤其是参与返京豆活动的用户。这类重复评论如果不处理模型会严重过拟合到重复文本上导致验证集准确率高但真实场景表现差。去重不能只做字符串完全匹配因为用户可能只改一个标点或加一个空格我一般用归一化后的文本做去重去掉所有空格和标点再比较是否相等。更狠一点的做法是算文本之间的编辑距离但毕设数据量在小万级别时直接跑difflib会慢到怀疑人生。折中方案是先用文本哈希粗筛再对哈希相同的分组做两两编辑距离验证。我实践下来归一化完全匹配已经能解决大部分问题编辑距离留给极端情况。import re def normalize_for_dedup(text): return re.sub(r[\s\W_], , text) df[norm] df[clean].apply(normalize_for_dedup) df df.drop_duplicates(subset[norm], keepfirst) print(f去重后剩余 {len(df)} 条评论)去重后会改变类别分布所以去重要在清洗之后、划分训练测试集之前完成。顺序错了的话测试集里可能混着训练集见过的重复文本评估结果虚高。我踩过这个坑第一次实验没去重测试集准确率 0.91去掉重复后直接掉到 0.83这才意识到测试集泄漏了。3.3 标注修正评分不等于情感怎么处理“阴阳怪气”评分与真实情感并不总是对齐。有人给 1 星但评论内容是“东西还行就是快递太慢了”也有人给 5 星但写的是“帮朋友买的不知道质量如何”。如果完全信任评分做标签模型会学到错误模式。但手工重标上万条不现实我一般用规则加抽检来修正先标记出“评分与文本情感冲突”的候选样本再抽 10% 人工确认。候选标记的规则很简单文本里出现明确的矛盾信号词比如“快递慢”“质量差”“客服不理人”的同时评分大于等于 4或者出现“好评”但评分小于等于 2。这类样本通常只占总量 5% 左右人工处理成本可控。也可以在标注修正后直接丢弃这些冲突样本因为它们属于标注噪声保留会让模型的决策边界更模糊。conflict_keywords [快递慢, 质量差, 客服, 退货, 垃圾, 差评] def flag_conflict(row): text row[clean] if any(k in text for k in conflict_keywords) and row[score] 4: return True return False df[conflict] df.apply(flag_conflict, axis1) print(f发现疑似冲突样本 {df[conflict].sum()} 条) df_core df[~df[conflict]].copy()这一步的意义在于你辛辛苦苦用规则修正的每一个标签都在告诉模型“评分只是弱信号文本才是强信号”。我见过很多情感分析项目分数很高但把测试集里的冲突样本挑出来看模型几乎全错。核验方式很简单训练完成后单独抽出冲突样本子集看分类准确率如果低于 0.5说明模型过度依赖评分风格而非文本语义需要加强清洗或调整特征。4. 让中文变成机器能读的数字分词、停用词与向量化4.1 分词为什么不直接用 jieba 默认词典中文文本没有天然空格分词是特征工程的起点。jieba 是最常见的工具自带词典覆盖多数通用词汇但在商品评论领域会出现“入股不亏”“避雷”“YYDS”这类网络新词默认词典分不出来就会被切碎成单字丢失语义信息。解决方法是先对语料做一次词频统计找出被切碎的高频词手动加入自定义词典。自定义词典的格式很简单每行一个词可以带词频和词性。词频不要拍脑袋我一般按语料规模估算如果总词数是 50 万一个出现 200 次以上的词词频给 5 就够。词频给太高会影响 jieba 对其他词的分词判断反而引入噪声。分词之后做一次验证打印 50 条随机文本的分词结果看看品牌名、商品名、情感词是否正确切开。import jieba # user_dict.txt 每行: 词 词频 词性 # YYDS 500 nz # 避雷 300 v jieba.load_userdict(user_dict.txt) def tokenize(text): return [w for w in jieba.cut(text) if w.strip()] df_core[tokens] df_core[clean].apply(tokenize) print(df_core[tokens].head(10).tolist())分词结果要保存下来后续 TF-IDF 和 Word2Vec 都要复用不要每次训练都重新分词。分词参数里唯一需要调的其实是词典jieba.cut的HMM参数默认开对未登录词有补充识别效果但也会把一些英文混着中文的文本切乱。如果语料里英文占比较高可以把HMMFalse试一试对比一下前后效果再决定。4.2 TF-IDF向量化三个必调参数TF-IDF 是把分词结果转成向量最直接的方法。sklearn 的TfidfVectorizer有三个参数对最终效果影响最大max_features、min_df和ngram_range。max_features控制特征数量商品评论文本量级在 1 万条左右时5000 到 10000 个特征就够用设太大内存容易爆设太小信息量不够。min_df是文档频率下限默认 1 会把只出现一次的生僻词也纳入特征我一般设 5让至少出现在 5 条评论里的词才进入词表。ngram_range是容易被忽略的点。二元词组(1, 2)能捕捉“质量好”“物流慢”这类组合情感比单个词更稳定但特征维度会翻几倍。我建议先跑(1, 1)做基线再试(1, 2)对比 F1 有没有提升。如果提升不到 1 个百分点就留在(1, 1)因为特征维度过高会导致训练时间变长逻辑回归还好换成其他模型就吃力了。from sklearn.feature_extraction.text import TfidfVectorizer df_core[text] df_core[tokens].apply(lambda x: .join(x)) vectorizer TfidfVectorizer( max_features8000, min_df5, ngram_range(1, 2), sublinear_tfTrue, # 用 1log(tf) 代替原始词频缓解高频词主导 ) X vectorizer.fit_transform(df_core[text]) y df_core[label] print(f特征矩阵形状: {X.shape})sublinear_tf是我觉得最值得开的一个参数。京东评论区“的”“了”“就”这类虚词即使加了停用词表也未必清干净开了sublinear_tf之后词频差异被压缩模型不再被高频无义词带偏。特征矩阵是稀疏矩阵保存时直接用scipy.sparse.save_npz存成.npz文件下次加载不重复计算能省不少时间。4.3 词向量与序列模型Word2Vec 到底该自己训练还是下载现成的如果你想尝试 LSTM 这类序列模型就得把文本转成词向量序列。有两个选择用现成的中文词向量或者用 gensim 在自己的语料上训练 Word2Vec。毕设层面我更推荐自己在语料上训练因为中文通用词向量是在新闻、百科等正式语料上训练的对“便宜大碗”“踩雷”这类电商口语词覆盖很差。自训练只需设置维度、窗口、最小词频三个参数。维度通常设 100 到 200太小表达不了语义差异太大在小语料上容易过拟合。窗口设 5 是默认值处理短文本评论够用。min_count设 5出现次数少于 5 的词直接丢弃否则向量质量会被稀有词的噪声拖垮。训练完成后一定要做一次相似词测试比如查“质量”的 top 10 相似词如果出来一堆不相关的词说明语料量不够或参数不合适。from gensim.models import Word2Vec from gensim.models.word2vec import LineSentence # 把分词结果写入文件每行一条评论词与词用空格分隔 with open(corpus.txt, w, encodingutf-8) as f: for tokens in df_core[tokens]: f.write( .join(tokens) \n) model Word2Vec( sentencesLineSentence(corpus.txt), vector_size150, window5, min_count5, workers4, epochs10, ) print(model.wv.most_similar(质量, topn10))epochs10在短文本语料上比默认的 5 效果更好因为每条评论太短单次遍历学不到足够的共现信息。这里要注意一个常见翻车点Word2Vec在 gensim 4.0 之后把size参数改名成了vector_size网上老教程抄过来会直接报 TypeError。词向量训练好后要保存为word2vec.model和word2vec.kv两个文件模型给后续微调用kv 给快速加载用。5. 模型评估与避坑清单准确率虚高、样本不均衡和过拟合的血泪记录5.1 基线模型对比朴素贝叶斯、逻辑回归、LSTM 的取舍不要一上来就训深度学习模型。我一般先跑两个传统机器学习基线朴素贝叶斯和逻辑回归。朴素贝叶斯在短文本分类上很强计算快适合做下限参考逻辑回归在 TF-IDF 特征上表现稳定且系数可以直接当特征重要性看答辩时解释起来很有说服力。如果这两个模型的宏平均 F1 已经到 0.82那 LSTM 至少要超过 0.84 才有继续调的必要不然直接交付传统方案更划算。from sklearn.naive_bayes import MultinomialNB from sklearn.linear_model import LogisticRegression from sklearn.model_selection import train_test_split from sklearn.metrics import f1_score X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.2, random_state42, stratifyy ) nb MultinomialNB(alpha1.0) nb.fit(X_train, y_train) y_pred_nb nb.predict(X_test) print(fMultinomialNB macro-F1: {f1_score(y_test, y_pred_nb, averagemacro):.4f}) lr LogisticRegression(max_iter1000, C1.0, class_weightbalanced) lr.fit(X_train, y_train) y_pred_lr lr.predict(X_test) print(fLogisticRegression macro-F1: {f1_score(y_test, y_pred_lr, averagemacro):.4f})class_weightbalanced是处理类别不均衡最省事的办法它会按类别频率反向加权让少数类别在损失函数里的占比提升。注意逻辑回归的max_iter默认 100TF-IDF 特征维度高时经常不收敛要调到 1000 以上。至于 LSTM代码量比前两个大得多我放到下一节讲。你还要记录每个模型的训练时间答辩时这张“传统 vs 深度学习”的对比表非常加分。5.2 避坑清单五条必看的踩坑记录5.2.1 准确率 0.93 但模型实际是废的现象测试集准确率看着很高随手抽 20 条真实评论预测错了一半。 原因类别不均衡。负面样本只占 6%模型全预测成正面也有 94% 的准确率。 解决换成宏平均 F1 作为主指标并在训练时使用class_weightbalanced或过采样/欠采样。我常用imbalanced-learn库的RandomOverSampler但要注意过采样会让验证集评估偏乐观最好在过采样之前划分数据。5.2.2 测试集泄漏去重不彻底导致指标虚高现象模型在测试集上 F1 高达 0.92部署到新数据上直接崩到 0.75。 原因清洗后忘记去重同一条评论同时出现在训练集和测试集。 解决去重必须先于数据划分。更严格的做法是校验用户 ID确保同一个用户的评论不会跨集合出现因为同一个人的用语风格会让模型学到“人”而不是“情感”。5.2.3 jieba 分词把“不”和“好”切开导致情感反转识别失败现象模型对“不好”“不行”“不喜欢”全部预测错误。 原因默认词典没有把“不好”这类组合词切成一个整体TF-IDF 把“不”和“好”当成独立特征语义被割裂。 解决建立否定词典把“不好”“不行”“不满意”等加入自定义词典或者在特征基础上叠加“否定词 相邻情感词”的组合特征。我实测这个操作能让负面类别 F1 提升 3 到 5 个百分点。5.2.4 Word2Vec 训练后相似词一片混乱现象most_similar(质量)返回的全是数字、标点这类无意义 token。 原因语料太小或者min_count太低把噪声词也纳入了词表。 解决把min_count从 5 提到 10同时检查清洗环节是否把数字和标点全清干净了。如果语料只有几千条不要强行用 Word2Vec换 TF-IDF 更稳。5.2.5 模型对“快递”相关评论集体误判为差评现象包含“快递”的评论大量被预测为负面但实际上很多是好评。 原因语料里“快递慢”“快递垃圾”等负面表达占比过高模型学到“快递”这个词与负面的弱相关性。 解决训练前做一次特征分析把高频但与情感无关的强特征降权或者直接用逻辑回归系数定位这类问题。真正的解法是扩充正面语料中“快递”出现的比例让模型看到“快递很快”也能是正面的。5.3 混淆矩阵与错误分析指标准确不代表业务可靠混淆矩阵能告诉你模型具体在哪些类别之间混淆。对于三分类情感分析最常见的现象是“中评”与“好评”之间界限模糊因为京东的 3 星用户常写“还行吧”情感本身就不强烈。如果中评类别被大量分到好评你可以选择调整中评的判定阈值也可以在训练时给中评样本更高的权重。from sklearn.metrics import confusion_matrix, ConfusionMatrixDisplay import matplotlib.pyplot as plt cm confusion_matrix(y_test, y_pred_lr, labels[negative, neutral, positive]) disp ConfusionMatrixDisplay(confusion_matrixcm, display_labels[negative, neutral, positive]) disp.plot(cmapBlues) plt.savefig(confusion_matrix.png, dpi150, bbox_inchestight)画混淆矩阵是给答辩看的但更重要的是从矩阵读出下一步行动。如果中评和好评的混淆数占了总误差的 60%说明分类粒度本身可能不合理与其调参不如回到 2.2 节改分类定义。模型评估做到这个深度在一众只看准确率的毕设里已经能拉开差距了。6. 把模型从“能跑”变成“能答辩”验证细节、可解释性与交付技巧6.1 交叉验证别让随机种子决定你的成绩单次划分训练测试集结果受随机种子影响很大。我做实验时固定用 5 折交叉验证取宏平均 F1 的均值和标准差。标准差能反映模型的稳定性如果五折之间 F1 波动超过 0.03说明数据分布不均匀或模型过拟合了某一类样本。交叉验证的结果写进论文里比单次结果有说服力得多。from sklearn.model_selection import cross_val_score scores cross_val_score(lr, X, y, cv5, scoringf1_macro) print(f5-fold macro-F1: {scores.mean():.4f} ± {scores.std():.4f})6.2 可解释性用逻辑回归系数讲清楚“模型为什么这么判”毕业设计答辩时老师最常问的问题是“你的模型依据什么做出判断”。用深度学习模型很难回答但逻辑回归的系数可以直接对应到词特征。把系数绝对值 top 20 的词打印出来正系数是正面词负系数是负面词这就是一个朴素但直观的解释。如果要展示单条评论的预测理由可以用 LIME 库生成局部解释我在项目里对每类随机抽 5 条评论做了解释放在论文附录里。6.3 交付物清单与验收标准交付时不要只给一个训练好的模型文件要包含完整的四件套清洗与爬虫脚本、特征与训练代码、模型文件、评估报告。评估报告里至少有三张图类别分布图、混淆矩阵、宏平均 F1 的折线对比图。验收标准不是模型跑通而是新抓的 1000 条评论经过你的推理链路后宏平均 F1 不低于训练时的 95%。我自己做这类项目养成的习惯是每次改完数据或模型先把结果记录在一个 Markdown 文件里再继续下一步。没有记录的实验等于没做。这个习惯让后面写论文时省了大量回头补实验的时间。希望帮到你少踩一些我踩过的坑。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?