简介:这份毕业设计资源包围绕“基于机器学习的商品评论情感分析”展开,面向计算机、人工智能相关专业的学生及需要完成课程设计或毕设的开发者,帮助其系统掌握从文本预处理到模型评估的完整NLP实践流程。包内共43个文件,以14个Python脚本为核心,配合7个CSV与2个XLS数据集、4个NPY及3个PKL序列化文件、2个H5模型权重、4个XML配置和1个YML参数文件等,覆盖数据清洗、分词、词向量训练、SVM与LSTM建模、GUI界面及爬虫采集等模块,压缩包约66.66MB。项目完整呈现了词袋、TF-IDF、Word2Vec等特征提取方法,以及朴素贝叶斯、SVM、LSTM等模型的训练与调参思路,并附带混淆矩阵、ROC曲线等可视化脚本。目前已有166人学习,适合作为情感分析入门到实战的参考范例,也可直接用于二次开发与报告撰写。
1. 商品评论情感分析:从爬虫到模型部署,一个毕业设计项目的完整落地路径
电商平台上一条差评的破坏力,往往抵得过一百条好评带来的信任积累。很多做计算机毕业设计的同学选到「基于机器学习的商品评论情感分析」这个题目时,第一反应是“二分类任务,调个 sklearn 就完事了”,真正动手才发现:中文评论里“质量不错但是物流太慢”这种混合情感怎么标?标注数据从哪来?模型在测试集上 92% 的准确率,一上线跑真实评论就翻车,为什么?这个项目要解决的核心问题,是把非结构化的中文评论文本,自动映射为正面/负面(有时还包括中性)的情感极性,并且让整个流程可复现、可解释、可展示。它适合计算机、软件工程、大数据方向的本科毕业生,也适合刚入门 NLP 想找一个完整项目练手的工程师。下面我按自己带过几届毕设的经验,把这个项目从数据到部署的完整路径拆开讲。
2. 数据从哪来、怎么标:商品评论语料的获取与清洗
2.1 评论数据的三个来源与取舍
做情感分析,数据质量决定天花板。常见做法有三条路:公开数据集、平台爬取、人工构造。公开数据集里,ChnSentiCorp(中文酒店评论)和 online_shopping_10_cats(10 类商品评论)是毕设里用得最多的,优点是标注现成、格式干净,缺点是领域单一、句子偏短,模型容易过拟合到“很好”“太差”这类高频词上。平台爬取能拿到最新、最真实的评论,但要注意只采集公开可见的文本内容,控制请求频率,不涉及任何用户隐私字段。人工构造适合补充长尾场景,比如数码产品的“续航虚标”、服装的“尺码偏小”,但成本高,一般只标几百条做测试集。
我的建议是:用公开数据集做训练主力,爬取 2000~3000 条真实评论做验证集,人工标注 300 条做最终测试。这样既有数据量,又能暴露模型在真实分布上的问题。
2.2 中文评论清洗的五个必做步骤
原始评论里全是噪声,直接喂给模型等于给自己挖坑。下面这段清洗代码是我每次都会用的基础版本:
import re import jieba def clean_chinese_text(text): # 1. 去除 HTML 标签和 URL text = re.sub(r'<[^>]+>', '', text) text = re.sub(r'http[s]?://\S+', '', text) # 2. 去除表情符号和特殊字符,保留中文、英文、数字和基本标点 text = re.sub(r'[^\u4e00-\u9fa5a-zA-Z0-9,。!?、;:]', '', text) # 3. 全角转半角(数字和字母统一) text = text.replace('0', '0').replace('1', '1') # 实际项目用 str.maketrans 批量处理 # 4. 去除连续重复字符,如“好好好好好” text = re.sub(r'(.)\1{2,}', r'\1\1', text) # 5. 结巴分词,过滤单字和停用词 stopwords = set(['的', '了', '是', '在', '我', '有', '和', '就', '不', '人', '都', '一', '一个']) words = [w for w in jieba.lcut(text) if len(w) > 1 and w not in stopwords] return ' '.join(words) # 示例 raw = "这个商品质量很好!!!但是物流太慢了😡😡😡 https://example.com" print(clean_chinese_text(raw)) # 输出:商品 质量 很好 但是 物流 太慢这段代码的逻辑说明:第一步去 HTML 和 URL 是防止爬虫残留;第二步的正则只保留中文、英文、数字和中文标点,把 emoji 和乱码清掉;第三步全角转半角避免“123”和“123”被当成不同 token;第四步处理“好好好好好”这类刷屏评论,避免词频失真;第五步分词后过滤单字和停用词,减少特征维度。
参数上要注意:len(w) > 1这个阈值不是绝对的,像“差”“好”这种单字其实有强情感极性,如果你的数据集里单字情感词占比高,可以放宽到len(w) >= 1,但要在验证集上看效果。停用词表建议用哈工大停用词表做基础,再根据你的品类补充,比如卖服装的要把“尺码”“颜色”加进去,因为它们在所有评论里都高频出现,区分度低。
2.3 标注一致性:三个人标出三种结果怎么办
如果项目需要自己标注,最大的坑是标注标准不统一。我一般会先写一份标注手册,明确三条规则:第一,混合情感以整体倾向为准,“质量好但物流慢”如果整体语气是抱怨,标负面;第二,反讽和疑问句单独讨论,“这质量也是没谁了”要结合上下文判断;第三,中性评论(纯描述、无情感词)单独设一类,不要强行归入正负。标注时让两个人独立标 200 条,算 Kappa 系数,低于 0.7 就坐下来对齐标准,别急着标全量。
3. 特征工程与模型选型:TF-IDF、Word2Vec 还是 BERT
3.1 传统机器学习方案:TF-IDF + 线性模型
毕业设计里最稳的方案是 TF-IDF 加逻辑回归或 SVM。原因很简单:训练快、可解释、调参少,答辩时能说清楚每个特征的含义。下面是一个完整的训练脚本:
from sklearn.feature_extraction.text import TfidfVectorizer from sklearn.linear_model import LogisticRegression from sklearn.model_selection import train_test_split from sklearn.metrics import classification_report import pandas as pd # 假设 df 有 'text' 和 'label' 两列,label 为 0/1 df = pd.read_csv('clean_comments.csv') X_train, X_test, y_train, y_test = train_test_split( df['text'], df['label'], test_size=0.2, random_state=42, stratify=df['label'] ) # TF-IDF 向量化 vectorizer = TfidfVectorizer( max_features=5000, # 保留词频最高的 5000 个词 ngram_range=(1, 2), # 考虑单字和双字组合 min_df=3, # 至少出现在 3 篇文档中才保留 max_df=0.8, # 出现在超过 80% 文档中的词丢弃 sublinear_tf=True # 用 1+log(tf) 替代原始词频 ) X_train_vec = vectorizer.fit_transform(X_train) X_test_vec = vectorizer.transform(X_test) # 逻辑回归 clf = LogisticRegression(C=1.0, max_iter=1000, class_weight='balanced') clf.fit(X_train_vec, y_train) y_pred = clf.predict(X_test_vec) print(classification_report(y_test, y_pred))参数说明:max_features=5000是经验值,评论数据一般 3000~8000 条,5000 维足够覆盖主要情感词;ngram_range=(1,2)能捕捉“不 好”“太 差”这类否定和程度组合,对情感分析很关键;min_df=3过滤只出现一两次的噪声词;max_df=0.8去掉“商品”“东西”这种几乎所有评论都有的词;sublinear_tf=True抑制高频词权重,避免“好”字出现 100 次就主导整个向量。class_weight='balanced'在正负样本不均衡时自动调整权重,评论数据里正面往往多于负面,这个参数能明显提升召回率。
3.2 深度学习方案:BERT 微调值不值得上
如果你的毕设想冲优秀,或者导师明确要求用深度学习,BERT 微调是当前最成熟的选择。但要注意:BERT 需要 GPU,训练时间长,调参空间大,答辩时如果说不清 attention 机制反而容易被问住。我的建议是:传统方案做 baseline,BERT 做对比实验,论文里写清楚两者在准确率、F1、训练时间上的差异,这样既有工作量又有深度。
用 HuggingFace 的 transformers 库微调中文 BERT 的核心代码:
from transformers import BertTokenizer, BertForSequenceClassification, Trainer, TrainingArguments import torch from torch.utils.data import Dataset class CommentDataset(Dataset): def __init__(self, texts, labels, tokenizer, max_len=128): self.texts = texts self.labels = labels self.tokenizer = tokenizer self.max_len = max_len def __len__(self): return len(self.texts) def __getitem__(self, idx): encoding = self.tokenizer( self.texts[idx], truncation=True, padding='max_length', max_length=self.max_len, return_tensors='pt' ) return { 'input_ids': encoding['input_ids'].squeeze(), 'attention_mask': encoding['attention_mask'].squeeze(), 'labels': torch.tensor(self.labels[idx], dtype=torch.long) } tokenizer = BertTokenizer.from_pretrained('bert-base-chinese') model = BertForSequenceClassification.from_pretrained('bert-base-chinese', num_labels=2) training_args = TrainingArguments( output_dir='./results', num_train_epochs=3, # 评论数据 3 轮足够,多了容易过拟合 per_device_train_batch_size=16, # 显存 8G 以下用 8 learning_rate=2e-5, # BERT 微调经典学习率 warmup_ratio=0.1, # 前 10% 步数做 warmup weight_decay=0.01, evaluation_strategy='epoch', save_strategy='epoch', load_best_model_at_end=True )关键参数:max_len=128覆盖绝大多数评论长度,超过的截断,不够的补零;num_train_epochs=3是经验值,评论数据量小,5 轮以上基本过拟合;learning_rate=2e-5是 BERT 微调的标准起点,太大容易震荡,太小收敛慢;warmup_ratio=0.1让学习率在前 10% 步数里线性上升,避免一开始就大步长破坏预训练权重。如果你的显存不够,把 batch_size 降到 8,同时把 gradient_accumulation_steps 设为 2,效果等价。
3.3 模型选型的决策表
| 方案 | 数据量要求 | 训练时间 | 准确率(评论数据) | 可解释性 | 适合场景 |
|---|---|---|---|---|---|
| TF-IDF + LR | 1000+ | 秒级 | 85%~90% | 强 | 毕设 baseline、快速验证 |
| Word2Vec + LSTM | 5000+ | 分钟级 | 88%~92% | 中 | 想展示深度学习能力 |
| BERT 微调 | 2000+ | 小时级 | 92%~96% | 弱 | 冲优秀、有 GPU |
选型原则:如果时间紧、机器差,TF-IDF 方案足够毕业;如果想发小论文或评优,BERT 做主力,传统方案做对比。不要为了用深度学习而用,答辩老师更看重你能否解释清楚为什么选这个方案。
4. 避坑与排查:情感分析项目里最容易翻车的五个地方
4.1 准确率 95% 但预测全是正面
现象:测试集准确率很高,但混淆矩阵显示负面样本几乎全被预测成正面。原因:正负样本不均衡,正面评论占 80% 以上,模型学会了“全猜正面”就能拿高准确率。解决:用 F1 分数替代准确率做主指标,训练时加class_weight='balanced',或者对负面样本做随机过采样。更彻底的做法是分层采样划分数据集,保证训练集和测试集的正负比例一致。
4.2 分词把“不好”切成“不”和“好”
现象:模型把“不好”识别成正面,因为“好”是强正面词。原因:jieba 默认分词会把“不好”拆开,TF-IDF 里“不”和“好”各自成特征,丢失了否定含义。解决:在 jieba 里加载自定义词典,把“不好”“不错”“不差”这类否定组合加进去,或者用ngram_range=(1,2)让“不 好”作为一个二元特征进入模型。BERT 方案不存在这个问题,因为 tokenizer 是按字切分,能保留上下文。
4.3 爬虫数据里混入大量重复评论
现象:训练集里“此用户没有填写评价”出现几千次,模型把这个词学成了强特征。原因:电商平台默认好评和刷单评论大量重复。解决:清洗时用df.drop_duplicates(subset=['text'])去重,同时把“此用户没有填写评价”“默认好评”这类模板文本加入黑名单直接过滤。如果重复率超过 30%,说明数据源有问题,考虑换品类或换平台。
4.4 BERT 微调后验证集 loss 震荡不收敛
现象:训练 loss 正常下降,验证 loss 上下跳动,准确率卡在 85% 上不去。原因:学习率太大、batch_size 太小、或者数据标注噪声太大。解决:先把 learning_rate 降到 1e-5 试一轮,如果还震荡,检查标注数据里有没有明显标错的样本。另外,warmup_ratio调到 0.2 也能缓解初期震荡。如果数据量少于 2000 条,建议冻结 BERT 前 6 层,只微调后 6 层,减少过拟合风险。
4.5 部署时预测结果和测试集不一致
现象:离线测试 F1 0.92,写成一个 Flask 接口后,同样的输入返回不同结果。原因:训练时用了jieba.lcut分词,部署时忘了加载自定义词典;或者 TF-IDF 的 vectorizer 没有保存,重新 fit 了一遍导致词表不一致。解决:用joblib.dump把 vectorizer 和模型一起保存,部署时joblib.load加载,保证预处理和训练完全一致。BERT 方案要把 tokenizer 和模型一起保存到同一个目录。
5. 从脚本到系统:用 Flask 搭一个可演示的情感分析接口
5.1 最小可运行的服务端代码
毕设答辩时,老师最想看的是“能不能跑起来”。下面是一个 Flask 接口,加载训练好的模型,接收评论返回情感极性:
from flask import Flask, request, jsonify import joblib import re import jieba app = Flask(__name__) # 加载训练时保存的 vectorizer 和模型 vectorizer = joblib.load('tfidf_vectorizer.pkl') model = joblib.load('lr_model.pkl') # 加载自定义词典,保证分词和训练一致 jieba.load_userdict('custom_dict.txt') stopwords = set(open('stopwords.txt', encoding='utf-8').read().splitlines()) def preprocess(text): text = re.sub(r'<[^>]+>', '', text) text = re.sub(r'[^\u4e00-\u9fa5a-zA-Z0-9]', '', text) words = [w for w in jieba.lcut(text) if w not in stopwords and len(w) > 1] return ' '.join(words) @app.route('/predict', methods=['POST']) def predict(): data = request.get_json() text = data.get('comment', '') if not text: return jsonify({'error': 'empty comment'}), 400 cleaned = preprocess(text) vec = vectorizer.transform([cleaned]) pred = model.predict(vec)[0] prob = model.predict_proba(vec)[0].max() return jsonify({ 'sentiment': 'positive' if pred == 1 else 'negative', 'confidence': round(float(prob), 4) }) if __name__ == '__main__': app.run(host='0.0.0.0', port=5000)逻辑说明:preprocess函数必须和训练时的清洗逻辑完全一致,否则特征分布偏移会导致预测失准。vectorizer.transform而不是fit_transform,因为词表已经固定。返回结果里带上置信度,方便前端做阈值过滤,比如置信度低于 0.6 的显示“中性”或“待人工确认”。
参数上,port=5000是 Flask 默认端口,如果被占用改成 5001。生产环境不要用app.run,用 gunicorn 启动:gunicorn -w 4 -b 0.0.0.0:5000 app:app,4 个 worker 足够应付答辩演示。
5.2 前端展示与可视化建议
答辩时纯接口演示不够直观,建议加一个简单的 HTML 页面,输入评论实时显示情感标签和置信度进度条。如果想让图表更好看,可以用 ECharts 画一个情感分布饼图,把最近 100 条预测结果按正负统计。注意:前端只做展示,不要在前端做任何模型推理,所有计算都在后端完成。
5.3 性能优化的两个实用技巧
第一,如果 QPS 要求不高(答辩场景基本没要求),不用做批量推理,单条预测延迟在 50ms 以内就够。第二,如果模型是 BERT,启动时加载模型需要几秒,建议在 Flask 启动前就加载好,不要放在请求里加载。可以用@app.before_first_request或者直接在模块顶层加载。
6. 让模型真正可用的三个进阶技巧
第一个技巧是置信度阈值调优。默认的predict返回 0 或 1,但predict_proba能给你概率。我一般会在验证集上画一条 precision-recall 曲线,找到 F1 最大的阈值点,而不是固定用 0.5。评论数据里,阈值调到 0.6 往往能过滤掉一批模棱两可的样本,让线上准确率更高。代价是有一部分评论会被判为“不确定”,需要人工兜底,但这比强行分类错要好。
第二个技巧是错误分析驱动迭代。模型上线后,把预测置信度在 0.5~0.6 之间的样本单独存下来,每周人工看一遍,把标错的加回训练集重新训练。这个闭环跑两三轮,F1 通常能涨 3~5 个百分点。我带的上一届学生就是靠这个把 F1 从 0.87 拉到了 0.92,答辩时老师问“你怎么证明模型在迭代”,他直接拿出三轮的错误样本分析表,效果很好。
第三个技巧是领域适配。如果你做的是数码产品评论,但拿酒店评论训练,效果肯定打折。解决办法是用酒店数据预训练一个基础模型,然后用数码评论做微调,哪怕只有几百条标注也能明显提升。具体做法:先在全量酒店数据上训一个 TF-IDF + LR,把 vectorizer 固定,然后在数码评论上只更新分类器权重。BERT 方案更简单,直接加载预训练权重,在数码数据上跑 1~2 轮微调即可。
| 技巧 | 预期收益 | 实施成本 | 适用阶段 |
|---|---|---|---|
| 置信度阈值调优 | F1 +1%~2% | 低 | 模型训练完成后 |
| 错误分析闭环 | F1 +3%~5% | 中 | 上线后持续迭代 |
| 领域适配微调 | F1 +5%~10% | 中高 | 跨领域部署时 |
最后说一个我自己的习惯:每次训练完模型,我都会把验证集里预测错的 20 条样本打印出来,逐条看是标注错了、分词错了还是模型真的学不会。这个习惯帮我省了很多盲目调参的时间。希望帮到你。
本文还有配套的精品资源,点击获取