
简介一份面向毕业设计与课程设计的Python情感分析项目核心基于朴素贝叶斯算法实现对评论的情感判别适合正在完成机器学习相关课题的学生参考。项目不仅给出可运行的Python源码还提供了基于Tkinter的可视化交互测试页面方便直接输入文本查看预测结果。资源包共10个文件包括2个Python脚本分别承担朴素贝叶斯模型与主控/交互逻辑6个txt文件整理中英文样例评论、停用词表及运行日志docx文档则可用于撰写设计说明整体压缩包大小约4.28MB。在实现中作者还针对大数据量场景做了两处优化先将数据加载、词典构造和先验概率等计算封装为对象成员变量避免测试阶段重复运算再通过统计词频并剔除低频噪声词汇有效减小词典、抑制过拟合提升准确率。已有118人学习适合需要快速上手情感分析实现并进行性能调优的学生。1. 情感分析毕业设计选朴素贝叶斯先问自己三个问题如果你的毕业设计或课程设计题目是「基于朴素贝叶斯算法的情感分析」恭喜你选了一个性价比极高的方向。它不需要 GPU、不需要预训练模型、不需要几万条标注数据几十行 Python 就能跑出一个准确率能看的基线而且数据分析和机器学习的每个环节——分词、向量化、训练、评估、可视化——都能自己讲清楚这正是答辩时老师最想听到的东西。很多人一上来就想着上 BERT结果数据量只有几千条微调出来的效果反而不如朴素贝叶斯稳定。动手之前先问自己三个问题数据集从哪里来、标签怎么定、代码怎么组织。这三个问题想明白了剩下的事情都是体力活。2. 文本预处理流水线中文分词、停用词与词袋矩阵的选型2.1 先用 jieba 还是先做正则清洗顺序与停用词表中文情感分析的第一步不是急着调 sklearn而是先把原始评论变成干净的词序列。我一般会按照「基础清洗 → 分词 → 去停用词 → 向量化」的顺序来。不要先分词再清洗因为像http://xxx这种长串会被 jieba 切成一堆无意义碎片后面停用词表根本拦不住。先用正则把网址、用户、数字、英文、多余空格统一处理掉再交给分词器得到的 tokens 才干净。分词我常用jieba.lcut精确模式不启用全模式。全模式会把「中华人民共和国」切成一堆重叠词情感特征全被冲淡。cut_allFalse是默认值HMMTrue让分词器能通过隐马尔可夫模型猜出未登录词。情感分析场景下不需要关掉 HMM否则网络新词会被切成单字。停用词表建议自己维护一份把「的、了、而且、但是、不过」这类连词和语气词去掉但要注意——「不、没、太、很」这类词绝对不能进停用词表它们是情感翻转的关键信号删了以后「不好看」就只剩「好看」了。import re import jieba def clean_text(raw: str) - str: # 去掉网址和 用户避免产生无意义碎片 text re.sub(rhttp\S, , raw) text re.sub(r\w, , text) # 去掉数字和占多数的英文保留中文字符和中文标点 text re.sub(r[a-zA-Z0-9], , text) return text.strip() def seg_and_filter(text: str, stopwords: set) - list: # 精确模式分词返回词列表 tokens jieba.lcut(clean_text(text), cut_allFalse) # 过滤掉空串和停用词保留情感相关词 return [t for t in tokens if t.strip() and t not in stopwords]这段代码的逻辑很直接清洗函数只保留中文主体分词函数把句子切成词列表最后一步过滤停用词。两个函数分开写是为了后面区分「训练时对全量数据调用」和「推理时对单条文本调用」避免测试集和训练集处理不一致。参数cut_allFalse是关键理论上你也可以用jieba.cut但lcut直接返回列表省一步转换。2.2 词袋与 TF-IDFPython 里 5 分钟完成向量化的代码分词之后面临两个选择CountVectorizer的词袋模型还是TfidfVectorizer的 TF-IDF。对朴素贝叶斯来说我优先选CountVectorizer。原因在于多项式朴素贝叶斯需要的是「这个词出现了多少次」这种离散计数的分布假设TF-IDF 会把高频情感词如「好、赞、烂」的权重压低而这些词恰恰是情感判断最直接的证据。TF-IDF 在文本分类里不是不能用只是它更适合主题区分不适合情感语气识别。向量化的核心参数有三个max_features、min_df、ngram_range。max_features5000对大几千条评论的数据集很合适太多会造成维度爆炸太少丢词min_df1表示词至少在 1 条样本里出现我通常设为 2 或 3过滤掉只在某一条评论里出现的怪词降低噪声ngram_range(1, 2)在情感分析里值得一试因为「不好」和「很好看」这类双词组合能挽回一部分朴素贝叶斯对否定句的天然短板。from sklearn.feature_extraction.text import CountVectorizer # 假设 train_texts 是经过 clean_text seg_and_filter 后重新拼接的字符串列表 # 注意向量化输入必须是 .join(tokens) 后的字符串不能传 list vectorizer CountVectorizer( max_features5000, # 只保留词频最高的 5000 个词 min_df2, # 词至少在 2 条样本里出现过 ngram_range(1, 2), # 保留单个词和相邻双词 token_patternr\S # 因为我们自己已分词不需要默认的字母正则 ) X_train vectorizer.fit_transform(train_texts) print(X_train.shape) # (样本数, 5000)注意这里token_patternr\S很重要。CountVectorizer默认的token_pattern是按空格切分并去除非字母内容中文很可能被全部丢掉。因为我们传入的是已经分词并用空格连接的文本直接让 sklearn 按空白切分即可。fit_transform的作用是先学到词表再转矩阵这一步只能对训练集做测试集在后面单独用transform。2.3 预处理流水线什么地方最容易翻车预处理阶段最常见的翻车是忘记统一词表。如果你在训练时对train_texts做了fit_transform在测试时却写成vectorizer.transform(test_texts)这没问题但如果你图省事把训练和测试合在一起fit_transform词表就包含了测试集信息这在机器学习里叫数据泄漏准确率会虚高答辩时被问两句就露馅。另一个常见问题是没有把分词结果重新拼接成带空格的字符串就直接丢给CountVectorizer它会把你传进来的 Python list 的每个元素当成一篇单独文档产生完全错误的矩阵形状。这两个细节我见过不下十个课程设计栽在上面处理完先print(X_train.shape)核对行数是否等于样本数是最快的检验方法。3. 朴素贝叶斯的三个变体多项式、伯努利、高斯怎么选3.1 贝叶斯公式到底在算哪件事从条件概率到情感判别的推演朴素贝叶斯的核心只有一行公式P(类别 | 文本) P(文本 | 类别) * P(类别) / P(文本)。在情感分析里要算的是给定一串分词后的评论它属于「好评」的概率大还是「差评」的概率大。分母P(文本)对所有类别都一样所以比较时只看分子——先验P(类别)乘以似然P(文本 | 类别)。这里的「朴素」体现在一个强力假设文本里每个词的出现概率彼此独立。比如「电影好看」被拆成「电影」和「好看」两个词模型分别计算它们在好评和差评里的出现频率然后相乘。这个假设在现实中当然不成立——「不好看」里「好」和「不好」高度相关但朴素贝叶斯不管这些它只看到「不」「好」「看」三个词各自对类别的影响。这也决定了它的上限对否定句、转折句、反讽句的判断会出错。你在答辩时能主动说出这个模型的天花板在哪比背一堆公式更能说明你理解了算法本身。3.2 scikit-learn 里的 MultinomialNB 参数alpha、fit_prior 与类别先验情感分析里默认选MultinomialNB因为它假设特征服从多项式分布也就是「一个词在文档里出现 k 次」的概率模型这和CountVectorizer输出的整数计数完美匹配。BernoulliNB适合特征只有 0 和 1 的场景如果你把词频变成「是否出现」再喂给它信息量会有损失GaussianNB假设特征是连续正态分布对稀疏矩阵基本不合适除非你用了分类器之前的嵌入向量如 Word2Vec 平均但那已偏离标题里朴素贝叶斯的常规做法。变体特征输入情感分析里的适用度常用参数MultinomialNB词频计数非负整数最常用alpha1.0, fit_priorTrueBernoulliNB0/1 布尔特征短文本、评论只有「好/坏」binarize0.0GaussianNB连续特征不适合稀疏词袋无特别参数alpha是拉普拉斯平滑系数默认 1.0作用是防止某个词在训练集里没见过导致概率为 0。对几千条数据来说alpha1.0到alpha0.1之间微调有时能提升 12 个百分点我就直接写alpha0.5折中。fit_priorTrue表示让模型根据训练集的类别占比估计先验概率如果你的数据集好评占 70%先验就会偏向好评。这个参数建议保持 True除非你确定样本分布不代表真实场景才手动用class_prior[0.5, 0.5]强制均衡。3.3 最小可运行的训练与预测代码这里给一份从向量化到训练的最小骨架你自己项目里可以在此基础上加交叉验证和调参。代码假设上一章已经生成了X_train、y_train、X_test、y_test四个变量。from sklearn.naive_bayes import MultinomialNB from sklearn.metrics import accuracy_score, classification_report # alpha 用 0.5避免过度平滑同时保留概率运算的稳定性 model MultinomialNB(alpha0.5, fit_priorTrue) model.fit(X_train, y_train) y_pred model.predict(X_test) print(准确率:, accuracy_score(y_test, y_pred)) print(classification_report(y_test, y_pred)) def predict_one(raw_text: str, vectorizer, model, stopwords): # 单条预测清洗、分词、向量化、预测一条龙 tokens seg_and_filter(raw_text, stopwords) # transform 用训练时学到的词表不重新拟合 X_one vectorizer.transform([ .join(tokens)]) proba model.predict_proba(X_one)[0] # 返回 [P(差评), P(好评)] return model.classes_[proba.argmax()], max(proba)fit的执行逻辑是内部估计每个特征在每个类别下的条件概率矩阵本质上是一步计数加平滑所以训练速度极快几千条样本一秒内完成。predict_proba返回的是类别概率数组注意model.classes_的排序是字母序还是数字序取决于你标签的编码方式所以取最大值下标时用proba.argmax()而不是写死下标避免差评好评顺序搞反。4. 评估与调参准确率不涨先别换模型先看这两张图4.1 混淆矩阵、精确率、召回率与 F1 在情感分析里的读法很多同学训练完只看准确率这个习惯在情感分析里很危险。如果数据集里 90% 是好评模型什么都不学全部预测好评也有 90% 准确率看上去漂亮答辩时被老师用测试集一问就原形毕露。正确做法是看confusion_matrix和classification_report里的精确率和召回率——差评的召回率尤其重要它代表「真实的差评里被正确识别出来的比例」。如果这个值低于 60%说明模型对负面表达不敏感可能是训练数据差评太少也可能是停用词表把「不」这类转折词误删了。from sklearn.metrics import confusion_matrix, ConfusionMatrixDisplay import matplotlib.pyplot as plt y_pred model.predict(X_test) tn, fp, fn, tp confusion_matrix(y_test, y_pred).ravel() # 打印人工可读的指标 print(f好评精确率: {tp / (tp fp):.2f}) # 预测为好评里真实好评占比 print(f差评召回率: {tp / (tp fn):.2f}) # 真实好评里被找出的占比 # 画混淆矩阵图便于写进毕业论文 ConfusionMatrixDisplay.from_predictions(y_test, y_pred) plt.title(情感分类混淆矩阵) plt.show()需要澄清一点粗体代码里我把差评映射成标签 1所以tp指的是差评这一类。ravel()会把四格矩阵展开成 [真负例假正例假负例真正例]。如果你的标签顺序正好相反打印的指标名要对应调整。混淆矩阵画出来放进论文里是非常加分的素材老师一眼就能看出你的模型哪里弱——通常是对角线亮差评那一格偏暗说明模型倾向把难样本归为好评。4.2 类别不均衡的三种处理sample_weight、过采样与调阈值情感分析数据集天然不均衡电商评论里好评远多于差评是常态。处理不均衡有三个实用手段按接入成本排序。第一种是给少数类加sample_weight权重在 sklearn 里注意MultinomialNB.fit支持sample_weight参数但不支持class_weight这一点很多人踩坑——因为在逻辑回归里写class_weightbalanced是习惯搬到朴素贝叶斯里直接报错。第二种是过采样imblearn库的SMOTE对密集特征有效但对CountVectorizer输出的稀疏矩阵要先小心处理SMOTE 会在词向量之间内插容易产生不存在的词组合所以文本任务我更建议用简单的随机过采样或欠采样。第三种是不动训练集改预测阈值模型概率默认以 0.5 为分界如果差评召回率太低把predict_proba概率大于 0.3 就判为差评相当于让模型更敏感。# 训练时传 sample_weight给差评样本更高权重 sample_weights np.where(y_train 1, 2.0, 1.0) # 差评权重 2 倍 model_weighted MultinomialNB(alpha0.5) model_weighted.fit(X_train, y_train, sample_weightsample_weights) # 预测时手动调阈值只把概率大于阈值的样本判为差评 proba model_weighted.predict_proba(X_test)[:, 1] y_pred_adj (proba 0.3).astype(int) # 阈值由 0.5 降到 0.3这里sample_weight2.0的含义是每条差评样本相当于两条等价样本参与计数直接改变模型内估计的条件概率。阈值调整不需要重新训练你在答辩时可以说「通过降低决策阈值在差评召回率上提升了 X 个百分点」这是一个很漂亮的分析点。4.3 五折交叉验证比单次切分稳妥单次把数据切成 80% 训练、20% 测试的随机性很大——运气好测出来的 90% 换一批测试集就掉到 82%。我在课程设计里通常先用五折交叉验证看整体水平再决定要不要调参。cross_val_score默认返回每一折的准确率我喜欢把每一折的数组打印出来看方差如果标准差超过 2%说明数据划分不稳定或样本太少此时把网格搜索出来的参数直接用于最终模型反而可能过拟合。from sklearn.model_selection import StratifiedKFold, cross_val_score # 分层抽样保证每一折里好评差评比例与全量一致 skf StratifiedKFold(n_splits5, shuffleTrue, random_state42) scores cross_val_score(model, X_train, y_train, cvskf, scoringaccuracy) print(各折准确率:, scores) print(均值 ± 标准差: {:.3f} ± {:.3f}.format(scores.mean(), scores.std()))这里random_state42不是玄学它保证你每次运行结果一致写论文时能复现同一组数字。StratifiedKFold相比普通KFold更适合分类任务它强制每一折的类别比例接近原始分布对不均衡数据尤其重要。5. 毕业设计避坑指南数据泄漏、编码乱码与复现玄学5.1 坑一清洗阶段就把标签信息泄漏进特征现象模型在训练集上准确率 99%测试集上也 95%但拿一条真实评论去测结果完全不对。原因有些同学在预处理时把每条样本的标签词比如「好评」「差评」这种出现在句子里的词也保留成了特征。如果数据集是从平台爬来的评论文本末尾自带的「好评」红字被正则切出来模型直接学到了「只要出现好评二字就判好评」这个伪规律。解决检查vectorizer.get_feature_names_out()列表里是否出现「好评」「差评」「推荐」这类词出现了就要在停用词表里加进去或者在正则清洗时把句子前后的固定文案截掉。这条一定要自查不然答辩现场真实预测翻车。5.2 坑二Windows 下中文文本编码导致中文全变乱码现象pd.read_csv读进来数据是好的但打印出来全是乱码或者分词后全都变成一个字一个字。原因Windows 上 Excel 保存 CSV 默认可能是GBK编码而你用utf-8去读中文直接变成乱码。还有一种情况是文件是utf-8带 BOM读取时 BOM 被当成特殊字符拼在第一个文本前面。解决读文件时指定encodingutf-8或encodinggbk具体取决于源文件写回结果时用encodingutf-8-sig这样 Excel 打开不乱码。我习惯在代码开头加一行pd.read_csv(path, encodingutf-8-sig)它能同时兼容带 BOM 和不带 BOM 的文件省去很多折腾。5.3 坑三测试集和训练集用了完全不同的预处理现象训练时准确率正常但你自己写一条评论做在线预测结果输出像随机猜的概率都在 0.5 附近。原因推理时忘了对输入文本做clean_text和seg_and_filter直接把原始字符串丢给vectorizer.transform。在向量化层面词对不上特征矩阵全是 0模型没有任何判别信息。解决把预处理的三个步骤封装成一个preprocess(text)函数训练和推理都调用同一个函数。这不是代码风格问题是工程上必须养成的习惯。我在项目里一定写成一个独立的utils.py避免两个脚本各复制一遍逻辑后面改了分词参数忘了同步排查起来非常痛苦。5.4 坑四随机种子不固定结果复现不了现象下午跑的准确率 85%晚上重新跑一遍变成 83%你没改任何代码。原因train_test_split、CountVectorizer内部的词频排序、MultinomialNB的训练都不依赖随机性所以严格来说这个现象不是随机种子缺失而是数据划分本身不稳定——每次划分出的训练集和测试集不同模型学到的参数自然略不同。解决在train_test_split、StratifiedKFold、SMOTE相关的步骤都传入random_state42让整个流程可复现。注意词表本身不会因为随机种子变化但如果你的数据处理里有sample、rng.choice这类随机操作一定要在开头加np.random.seed(42)。做毕设想稳定交付这点务必从第一天就固定。6. 收尾工程把模型封装成可答辩的源代码项目6.1 文件组织分层让老师一眼看懂代码标题既然强调「源代码」交付的目录结构就不能只丢一个main.py。我建议按功能拆成四个文件答辩时老师问哪个环节你都能直接指到对应文件。sentiment_project/ ├── data/ # 原始标注数据CSV 格式 ├── utils.py # clean_text、seg_and_filter、加载停用词 ├── train.py # 向量化 训练 交叉验证 保存模型 ├── predict.py # load 模型单条预测输入输出 ├── requirements.txt # jieba、scikit-learn、pandas └── README.md # 运行步骤和环境说明requirements.txt要锁定三个最核心的库就够了别把环境里的numpy、pandas版本全部堆上去老师跑起来反而容易版本冲突。README.md写清楚「python train.py训练python predict.py -t 这句话很好预测」即可不要写废话。6.2 用 joblib 保存和加载落地模型训练好的分类器加向量器一起保存下来预测脚本里一条joblib.load就能恢复。分开保存三个文件向量器、分类器、停用词表也行但打包成一个字典更省事。import joblib # 保存把向量器、模型一起打包 artifacts { vectorizer: vectorizer, model: model, stopwords: stopwords, classes: model.classes_, } joblib.dump(artifacts, model/sentiment_model.joblib) # 加载与推理 loaded joblib.load(model/sentiment_model.joblib) vec loaded[vectorizer] clf loaded[model]joblib对 numpy 数组的序列化效率高于标准库pickle模型小的时候差别不大但一旦max_features调到一两万文件体积和加载速度的差距就很明显了。保存好之后记得随手测一次加载推理确认文件没有损坏再交付。6.3 可视化验证用一条自己写的句子做完整个闭环最后交付前我会做一次「人工冒烟测试」用一条从未出现在训练集里的中文评论走完整流程分词、向量化、预测、输出概率、画条形图。这一步既是给自己吃定心丸也是答辩现场最稳的演示素材。画图时注意中文字体问题Windows 下用SimHeiLinux 下可能没有可以把字体路径显式指定。import matplotlib.pyplot as plt test_text 这部电影画面很美但剧情拖沓演员演技在线整体一般 prob predict_proba_one(test_text, loaded) plt.rcParams[font.sans-serif] [SimHei] # 解决中文乱码 plt.rcParams[axes.unicode_minus] False plt.bar([差评, 好评], prob) plt.title(单条评论情感概率分布) plt.ylim(0, 1) plt.show()我的习惯是每个模型训练完都至少测 10 条我自己现场写的句子覆盖正话反说、纯好评、纯差评、含否定词这四类典型样本。如果含否定词的句子全判反了说明朴素贝叶斯对否定结构确实不敏感这也是它的算法上限不是 bug。明白这一点你答辩的说辞就多了一层深度——不是吹模型有多准而是说清楚什么场景下它可靠什么场景下该换更强的模型。希望这篇能帮你把毕业设计从「跑通」做到「讲清楚」少踩几个我当年踩过的坑。朴素贝叶斯这个起点不高但它给你留下的调参空间、评估意识和工程组织习惯才是这门课真正值回学费的地方。本文还有配套的精品资源点击获取