微博评论情感分析:NB+SVM组合的完整实践与避坑指南
简介这是一份基于朴素贝叶斯与支持向量机算法的微博评论情感分析可视化项目面向计算机相关专业学生可直接用于期末大作业、课程设计或项目实战练习。该作品出自大三学生之手经导师指导并获得99分高分代码结构完整、运行可靠适合小白直接上手。压缩包内含五十一个文件约17.79MB涵盖Python核心脚本、已训练好的TF-IDF模型、微博评论数据集、停用词表与情感词典、可视化HTML页面、项目截图及说明文档等附有完整的项目流程图目录划分清晰便于按需查阅。目前已有三百零四人学习浏览。借助项目可系统理解朴素贝叶斯与支持向量机在情感分析中的实际应用完整走通数据清洗、中文分词、TF-IDF特征提取、模型训练与评估以及基于Pyecharts的可视化展示流程同时可当作模板进行二次开发替换数据源即可适配其他平台的评论情绪分析。1. 微博评论情感分析用NBSVM为什么期末大作业选这个组合最省力期末大作业选微博评论情感分析还要用朴素贝叶斯和支持向量机双算法乍看像自找麻烦——毕竟现在随便跑个深度学习框架都能拿到更高准确率。但真把这件事做完你会发现NBSVM这套组合在短文本情感分析上依然有它的生态位数据量小、训练快、可解释性强而且期末项目要的不只是分数是你能把“为什么选这个模型、参数怎么来的、失败在哪”讲清楚。这个项目适合两类人一类是机器学习课设想稳拿分的学生另一类是想用最短路径验证情感分析全流程的入门者。我按自己做过的一套方案把整条链路拆开讲从理论分工到代码复现再到踩坑点照着走能省掉大半调试时间。2. 先立住理论朴素贝叶斯与SVM在短文本情感分析里的分工2.1 朴素贝叶斯为什么先拿它跑基线朴素贝叶斯的核心是贝叶斯定理P(y|x) P(y)P(x|y) / P(x)。放在文本分类场景里y是情感类别正面、负面、中性x是评论里的词项集合。模型假设特征之间条件独立——这个假设在自然语言里明显不成立“不好”和“好”两个词高度相关但正是因为它“朴素”计算量被压到了极低。我每次做文本分类项目都会先跑一版NB理由有三个一是训练几乎瞬时完成数据预处理完了3秒出模型适合快速验证特征工程做得好不好二是对短文本来说微博评论平均就十几个词特征独立假设的破坏程度远低于长文本NB往往能拿到一个不错的准确率基线三是期末项目需要对比NB作为弱分类器基线能反过来衬托SVM的增益是多少。sklearn里朴素贝叶斯有三个变体GaussianNB、MultinomialNB、BernoulliNB。文本分类几乎只用后两个。MultinomialNB假设特征服从多项式分布适合TF-IDF或词频向量BernoulliNB适合二值化的词出现特征。我在微博评论情感分析里用TF-IDF配MultinomialNB作为默认组合原因后面在特征工程部分会展开。这里有个容易被忽略的参数是alpha平滑系数默认alpha1.0是拉普拉斯平滑如果语料里生僻词多可以往0.5调调完你会发现F1会有零点几个点的波动——这属于正常范畴不要为了一两个点去疯狂搜参期末项目更看重你解释得清楚。生成式模型和判别式模型的差别也值得在文档里写一笔。NB是典型的生成式模型它先估出每个类别的分布再算新样本属于哪个类别的概率SVM是判别式模型它不关心数据是怎么生成的只找类别之间的决策边界。这个区别决定了它们在同一个数据集上出错的方式不同NB大概率会在类别重叠区域犹豫SVM则会在边界附近有更明确的划分。写文档时把这一段放进去评委一眼就知道你不是只会调包。2.2 支持向量机线性不可分文本怎么用核函数撬动SVM要找的是最大间隔超平面把正负样本分开。文本特征向量通常在高维空间里TF-IDF展开后轻松几千维这反而是SVM的舒适区——样本量不大的时候高维稀疏特征配合核函数比神经网络更不容易过拟合。微博评论这种短文本类别边界往往不是一条直线能切开的比如“这家火锅太一般了但服务还行”一句话里同时出现正负情感这时核函数的作用就出来了RBF核把原始特征映射到更高维空间让原本纠缠的样本在映射后变得线性可分。但SVM有两个坑必须先说一是对特征尺度敏感TF-IDF特征如果量级差异大RBF核的gamma会被大数值维度牵着走。二是核函数和C、gamma要靠搜索不能拍脑袋。我在文本项目里一般先试线性核因为短文本情感分类往往是近似线性可分的线性核效果不够再切RBF配网格搜索。这里还有个实践技巧类别不平衡时SVC里加class_weightbalanced相当于给少数类更高的错分代价比手动重采样省事得多。SVM的训练复杂度也需要注意。RBF核SVM的样本间距离矩阵是O(n²)量级的数据到5000条以上训练时间会明显拉长。期末项目的数据量一般控制在2000-5000条这个区间里SVM表现稳定如果超过1万条建议直接换LinearSVC它在保证接近精度的前提下训练速度快一个数量级。这个选择本身也能写进文档——说明你知道SVM能力边界在哪而不是盲目上RBF核。为什么是“NBSVM”组合而不是NB逻辑回归或者只用SVM期末项目的评分点通常在“流程完整对比分析”。NB和SVM恰好是两种截然不同的分类思想概率生成式vs几何判别式。用同一份TF-IDF特征跑两个模型你在文档里能写清楚“同一份数据两种模型分别从什么角度分类”——NB算的是每个类别的后验概率SVM找的是类别间的决策边界。这个对比本身就是文档说明里最值钱的部分。另外这两个模型都是sklearn一行能调用的不用写训练循环期末项目时间本来就紧省下的时间可以花在特征工程和可视化上。提示模型选型不要贪多。NB和SVM两个模型足够撑起对比分析再加逻辑回归和决策树会稀释文档重点评分上反而不讨好。2.3 项目文件结构源码、文档、数据怎么组织一个期末大作业源码和文档说明的分工要清楚。我一般这样组织项目目录weibo_sentiment/ ├── data/ │ ├── raw/ # 爬虫或手工采集的原始评论 │ ├── cleaned/ # 清洗后带标签的中间数据 │ └── processed/ # TF-IDF向量化后的训练集/测试集 ├── src/ │ ├── preprocess.py # 清洗分词停用词 │ ├── feature.py # TF-IDF向量化与标签编码 │ ├── model_nb.py # 朴素贝叶斯训练与评估 │ ├── model_svm.py # SVM训练与网格搜索 │ └── visualize.py # 可视化图表输出 ├── output/ │ ├── figures/ # 生成的图表 │ └── metrics.json # 各模型评估指标 └── README.md # 运行说明src下每个文件对应一条独立的流水线模块可视化单独放一个脚本。这样做的好处是答辩演示的时候你可以现场分步跑每一步都有对应脚本而不是在一个大文件里翻代码讲不清。数据标注我建议用人工规则情感分成正面、负面、中性三类而不是二分类——微博评论里中性占比很高“哈哈哈哈”“路过”这类评论大量存在强行二分类会把模型先验带偏后面F1会很难看。数据规模上我建议至少准备2000条以上带标签的评论太少的话SVM容易欠拟合NB的先验也估不准。数据来源方面常见做法是用爬虫抓微博公开评论或者用公开数据集。抓数据时要注意接口频率限制我一般用pandas分批存储原始数据保留JSON格式避免清洗出错后没法回溯。这里建议机器学习入门阶段别自己造轮子项目里标注好的数据能省一半时间。另外如果机器上还没有Python环境先装好Python 3.8以上版本IDE用VSCode配好Python插件再pip install jieba scikit-learn pandas matplotlib环境问题别拖到写代码时再解决。README文档是期末项目最容易忽略的部分但它决定了别人能不能复现你的结果。我在README里固定写三块内容运行环境Python版本、依赖包及版本号、运行步骤按顺序写python src/preprocess.py到python src/visualize.py的命令、结果说明两个模型在测试集上的准确率和F1。这三点写完文档说明的主干就有了剩下就是把每个模块的设计思路填进去。3. 从微博评论到训练集文本清洗、分词与TF-IDF特征工程3.1 微博评论特有的脏数据清洗微博评论和普通新闻语料最大的区别是噪音格式多用户名、#话题标签#、URL链接、表情代码、多余空白。这些内容对情感判断没有信息量还容易在分词时产生一堆垃圾词比如“小明”如果不清洗会被切成“小明”这个人名成为无意义的特征。我的清洗函数长这样import re import pandas as pd def clean_weibo_comment(text: str) - str: 清洗单条微博评论返回纯文本 # 去掉用户名如 小明同学 text re.sub(r[\w\u4e00-\u9fa5\-], , text) # 去掉#话题标签#如 #快乐大本营# text re.sub(r#.*?#, , text) # 去掉URL如 https://t.cn/xxxx text re.sub(rhttp\S, , text) # 去掉表情代码如 [哈哈] [伤心] text re.sub(r\[.?\], , text) # 压缩多余空白并去掉首尾空格 text re.sub(r\s, , text).strip() return text # 示例 sample 小明 这家店真的太好吃了[赞] https://t.cn/123 #美食探店# print(clean_weibo_comment(sample)) # 输出: 这家店真的太好吃了清洗逻辑按顺序执行是有讲究的先去掉和话题因为它们内部可能包含表情或URL先处理外层再处理内层避免正则冲突。URL用http\S匹配这里的\S是“非空白字符”能吃掉URL后面可能跟着的标点之外的所有字符。表情代码在微博里通常是方括号包裹的\[.?\]里的?是非贪婪匹配确保只匹配最近的一对方括号避免一次吞掉整条评论。清洗后建议再检查一遍空文本有些评论可能全是表情和清洗完就没了这类样本直接丢弃或标记为中性。我在清洗完会再做一个统计打印每条评论清洗前后字符长度的差值。如果大量评论清洗后长度为0说明采集的数据源里机器生成内容太多需要重新评估数据质量。这一步很便宜但能避免后面模型训练时出现一堆空向量。数据标注规则方面我定的是明确表达喜欢、推荐、满足的标为正面明确表达厌恶、失望、愤怒的标为负面仅陈述事实、疑问、无情感倾向的标为中性。规则要在文档里写明因为同一个词在不同语境下情感不同“这个价格还行”算中性还是正面需要标准一致。3.2 分词与停用词jieba的参数与自定义词典中文文本不能像英文那样按空格切词需要用分词工具。项目里我默认用jieba它支持三种模式精确模式默认适合文本分析、全模式把所有可能的词都切出来噪音多、搜索引擎模式适合检索场景。情感分析用精确模式足够jieba.lcut(text)返回词列表。import jieba import jieba.analyse # 加载自定义词典词典格式: 词 词频 词性每行一个词 jieba.load_userdict(userdict.txt) # userdict.txt 示例: # 不好吃 3 n # 真香 3 a stopwords set() with open(stopwords.txt, r, encodingutf-8) as f: for line in f: stopwords.add(line.strip()) def cut_comment(text: str) - list: 分词并去除停用词 words jieba.lcut(text) # 过滤单字、停用词和纯数字 words [w for w in words if w not in stopwords and len(w.strip()) 1 and not w.isdigit()] return words # 示例 print(cut_comment(这家店真的太好吃了)) # 输出: [这家, 真的, 好吃]这里自定义词典的作用是防止jieba把领域词切碎。“不好吃”默认会被切成“不好”和“吃”但“不好吃”在情感分析里是一个整体负向词加载自定义词典后cut函数会优先按词典里的词切分。停用词表我习惯用现成的中文停用词表再人工补充微博特有词比如“哈哈哈哈”“路过”“转发了”。注意过滤条件里len(w.strip()) 1会把所有单字词去掉这是有代价的——动词“爱”“恨”都是单字如果语料里这些字对情感判断很重要需要去掉这个条件。我建议先保留单字跑一版看特征重要性再决定要不要过滤。jieba还有一个参数是分词粒度默认HMM在新词识别上是开启的对微博这种网络用语多的文本有帮助。“yyds”“绝绝子”这类新词HMM未必认识优先往自定义词典里加。网络用语的时效性很强每周的词都可能变实测中我发现定期补充用户词典比调任何算法参数都管用。跑完分词后我建议打印20条评论的分词结果贴在文档里这样既验证了清洗逻辑又能直观展示特征工程的中间产物。3.3 向量化TF-IDF的ngram_range与max_features怎么定分词之后文本还是字符串列表机器学习模型吃的是数值矩阵。常用的文本向量化方式有词袋CountVectorizer和TF-IDFTfidfVectorizer。TF-IDF的直觉是一个词如果在某条评论里出现得多TF高但在整个语料里很少见IDF高那它对该评论的区分度就强。情感词“难吃”“绝了”天然符合这个特性所以文本分类里TF-IDF通常优于纯词频。from sklearn.feature_extraction.text import TfidfVectorizer # 先把每条评论的分词结果还原成空格分隔的字符串 corpus [ .join(cut_comment(t)) for t in df[text]] vectorizer TfidfVectorizer( ngram_range(1, 2), # 考虑单个词和两个词的组合 max_features5000, # 最多保留5000个特征 min_df2, # 至少在2条评论中出现 max_df0.8, # 在超过80%的评论中出现的词忽略 sublinear_tfTrue # 对词频做log变换抑制高频词 ) X vectorizer.fit_transform(corpus) print(X.shape) # (样本数, 5000)这四个参数每个都有实际意义。ngram_range(1, 2)让“不好”作为整体特征而不是“不”和“好”两个独立特征是情感分析里性价比最高的一个参数如果设成(1,1)“不好”和“好”在向量空间里会被同等对待语义直接反转。max_features5000控制特征维度微博评论数据量级在几千条的规模5000维足够覆盖常见情感词再多只会引入噪音和内存压力。min_df2滤掉只出现一次的词——这种词往往是错别字或特定人名保留它们会让模型过拟合到个体。max_df0.8滤掉在超过80%评论里都出现的词“的”“了”这类词停用词表虽然删过一茬但仍可能漏网这个参数是第二道保险。sublinear_tfTrue是个容易被忽略的参数。它把词频做了log(1tf)变换避免某条评论里一个词重复5次就把该维度数值拉得过高。微博评论里“哈哈哈哈哈哈哈”重复刷屏很常见这个参数能显著抑制这类词对样本的支配作用。向量化之后要用train_test_split切分数据注意加stratifyy保持三分类比例一致否则切出来的测试集可能某一类占比失衡导致评估指标失真。这一步做完特征工程部分就完整了后面模型训练和调参都是在这份矩阵上操作。4. 两条模型主线朴素贝叶斯跑通与SVM调参对比4.1 朴素贝叶斯MultinomialNB的参数与交叉验证特征工程做完后模型部分反而简单。先跑MultinomialNB我把它定义为项目的“基线模型”from sklearn.naive_bayes import MultinomialNB from sklearn.model_selection import cross_val_score, train_test_split from sklearn.metrics import classification_report X_train, X_test, y_train, y_test train_test_split( X, df[sentiment], test_size0.2, random_state42, stratifydf[sentiment] ) nb MultinomialNB(alpha0.5, fit_priorTrue) # 用交叉验证看稳定性避免单次切分的偶然性 scores cross_val_score(nb, X_train, y_train, cv5, scoringf1_macro) print(fNB cross-val F1 macro: {scores.mean():.4f} (±{scores.std():.4f})) nb.fit(X_train, y_train) y_pred_nb nb.predict(X_test) print(classification_report(y_test, y_pred_nb))MultinomialNB只有两个主要参数。alpha是平滑系数默认1.0作用是在估计条件概率时加一个伪计数避免某个词在训练集没出现过就概率为0。微博评论里生僻词多我习惯把alpha往0.3-0.8区间调调大的作用是让先验更均匀。fit_priorTrue表示从训练数据中学习类别先验概率P(y)如果类别严重不平衡比如负面评论只有10%而这个先验恰恰是你想教的信号可以设False。为什么要先做交叉验证而不是直接看测试集因为单次切分测试集的结果波动很大尤其数据量只有两三千条时换一个random_state F1可能差5个点。交叉验证的输出的均值和中位数能让你判断模型是真的好还是只是这轮切分运气好。我在项目里把交叉验证结果写进了文档说明作为“模型稳定性”依据。NB训练完后我还习惯做一个特征检查把每个类别下对数概率最大的前10个词打印出来看是否和直觉一致。如果负面类别下排名靠前的词里有“微笑”“开心”说明数据标注或者清洗出了问题需要回溯如果确实都是“难吃”“失望”“垃圾”那特征工程和模型行为就对上了。这个检查只花5分钟但能避免模型当黑匣子交上去。4.2 SVM线性核与RBF核的C和gamma怎么搜SVM这边的路数完全不同。MultinomialNB几乎不用调参SVM则必须面对C和gamma两个超参数。C是误分类惩罚系数越大越不愿意放过错分样本容易过拟合gammaRBF核控制单个样本的影响半径gamma越小决策边界越平缓。from sklearn.svm import SVC from sklearn.model_selection import GridSearchCV from sklearn.pipeline import Pipeline import numpy as np # 用Pipeline把SVC串起来方便后续加处理步骤 pipe Pipeline([ (svm, SVC(kernelrbf, class_weightbalanced, random_state42)) ]) param_grid { svm__C: [0.1, 1, 10], svm__gamma: [scale, 0.01, 0.1, 1.0], } grid GridSearchCV( pipe, param_grid, cv5, scoringf1_macro, n_jobs-1, verbose1 ) grid.fit(X_train, y_train) print(fbest params: {grid.best_params_}) print(fbest score: {grid.best_score_:.4f})这里有个容易犯的错我一开始会在Pipeline里加StandardScaler后来发现TF-IDF矩阵本身就是稀疏的标准化会把矩阵变成稠密矩阵几千维直接内存爆炸。处理稀疏特征的正确做法是不做标准化或者用MaxAbsScaler它不破坏稀疏性。TF-IDF的数值范围天然是0到1附近SVM其实能接受不一定需要缩放。所以上面的代码里Pipeline没有加scaler这就是原因。网格搜索的搜索空间设计比搜索本身更重要。C和gamma之间有关系gamma越大决策边界越弯曲需要的C越小防止过拟合反过来gamma小边界平滑C可以给大一点。先固定gamma用scale跑一遍C找到C的大致量级再在附近细化能省一半搜索时间。数据量几千条时网格搜索很快但如果数据上万条RBF核SVM的训练时间是O(n²)往上走建议先试LinearSVC——它是线性SVM的liblinear实现训练快得多如果验证集上表现接近就直接用线性核。这个判断过程也要写进文档体现你对模型复杂度的理解。4.3 模型评估用混淆矩阵和多分类指标说话期末项目的文档里光给一个准确率是不够的。微博评论是三分类任务正面/负面/中性准确率会被占比最大的类别带着走。比如数据里中性占50%瞎猜全预测中性也有50%准确率。所以评估部分必须同时给精确率、召回率和F1。from sklearn.metrics import confusion_matrix, accuracy_score, f1_score y_pred_svm grid.best_estimator_.predict(X_test) print(fNB test acc: {accuracy_score(y_test, y_pred_nb):.4f}) print(fSVM test acc: {accuracy_score(y_test, y_pred_svm):.4f}) print(fNB test F1: {f1_score(y_test, y_pred_nb, averagemacro):.4f}) print(fSVM test F1: {f1_score(y_test, y_pred_svm, averagemacro):.4f}) cm confusion_matrix(y_test, y_pred_svm) print(SVM Confusion Matrix:) print(cm)我一般会把两个模型的评估结果整理成一张对比表写进文档说明的“结果分析”部分。这张表长这样模型准确率F1(macro)训练时间(秒)MultinomialNB0.850.820.3SVM-RBF0.890.8612.5除了报告数字更重要的是解释差异来源。SVM比NB准的原因通常在于NB的特征独立性假设在三分类任务中被破坏得更厉害——类别之间存在重叠“哈哈哈”既是中性也是正面NB强行把它看成独立条件概率的乘积SVM用核函数在特征空间划边界对重叠区域的处理更精细。这个解释写进文档比任何调参都有说服力。还要检查混淆矩阵里哪一对类别最容易混淆。我做这个项目时发现“中性”和“正面”的混淆最多原因是很多正面评论里没有强烈情感词模型分不清“陈述事实”和“轻度推荐”。这个观察写进文档顺便引出改进方向——可以针对中性评论做更严格的标注规则或者给模型加情感极性词典特征。期末答辩时你能主动说出模型的弱点并提出改进方案观感比单纯报告准确率高得多。5. 避坑指南微博评论情感分析项目里最常见的5个翻车点这一章是我做这个项目时踩过的坑汇总按“现象→原因→解决”的格式写每条都值得在动手前先看一遍。5.1 准确率很高但F1很低类别不平衡把模型带偏了现象训练完NB后在测试集上准确率0.83但看分类报告发现负面类别的F1只有0.2。原因数据里正面评论占了60%、中性30%、负面只有10%。NB在估计先验概率P(y)时学到了这个分布为了最小化整体错误它倾向于把不确定的样本押到多数类上准确率看起来不低但少数类的召回率极低。SVM如果不加设置也会这样只是程度比NB轻一些。解决两条路。一是模型层面SVM加class_weightbalancedNB没有直接等价参数但可以通过重采样实现。二是数据层面用imblearn的RandomOverSampler对负面评论过采样或者简单点用train_test_split里的stratify保证切分后的三个类别比例和全量一致。我在这个项目里先用stratify切分再对少数类做SMOTE负面F1从0.2提到0.6以上。验证方式很简单重新跑classification_report看负面类的precision和recall是否同时上升。5.2 分词把“不好”切成两个词情感语义直接反转现象模型把“这家店不好吃”预测成正面。查看特征后才发现“不好吃”被jieba切成“不好”和“吃”模型学到的信号是“不好”和“好吃”的位置离得很近反而强化了正面判断。原因jieba默认词库里没有“不好吃”这个整体词分词粒度太细情感短语被拆散后n-gram只覆盖到(1,2)二元词组“不好 吃”没有作为整体特征被保留。实际上ngram_range(1,2)在“不好吃”这种三个字的短语上只能覆盖“不好”和“好吃”这两种相邻组合覆盖不到“不-好-吃”的完整三角关系。解决两层保险。第一层在自定义词典里加入“不好吃”“太一般”“真难吃”这类高频情感短语让jieba优先按整体词切分。第二层跑完特征提取后打印vectorizer.get_feature_names_out()人工扫一遍特征词表看情感短语有没有被拆散这是排查分词问题最直接的手段别只盯着准确率猜。我还会专门搜一下负面评论里含“不”字的分词结果看有没有“不太行”“不咋地”这类词被切碎。5.3 SVM网格搜索跑很久RBF核在几千维稀疏矩阵上硬扛现象SVM的GridSearchCV跑完要一个多小时笔记本风扇狂转中途想停又怕浪费已跑的时间。原因RBF核SVM的复杂度在样本量和特征维度上都很高交叉验证还要重复5次再叠加C和gamma的12个组合3个C×4个gamma实际上等于训练了60次完整SVM。文本特征又是几千维每一步矩阵运算量都很大。另一个隐性因素是verbose1会频繁打印进度在终端里也拖慢速度。解决先做降维排查。第一步用n_jobs-1并行网格搜索内部可以同时跑多个参数组合。第二步先把param_grid缩小到2×2试探走向确定最佳参数在哪个区间再细化。第三步如果数据超过5000条优先用LinearSVC或SGDClassifier线性SVM在短文本上通常有90%以上的RBF核效果训练速度快一个数量级。不要为了“反正能跑通”就放任训练时间答辩现场如果模型重新训练要十几分钟演示环节会很尴尬。我后来养成了习惯先把网格搜索结果存成joblib文件模型只训练一次之后所有评估都直接load。5.4 TF-IDF特征维度爆炸内存直接溢出的边缘现象pandas DataFrame占内存不到100MB但TfidfVectorizer跑完.fit_transform()后提示MemoryError或者系统开始用交换分区卡到无法操作。原因max_features没有设置默认None时词表包含所有训练语料的词。如果语料有2万条评论分词后有3万个不同词展开成2万×3万的稀疏矩阵虽然还算可控但如果你后续又转成稠密阵做可视化2万×3万×8字节直接是4.8GB内存占用笔记本扛不住。平时跑代码不报错是因为sklearn返回的是稀疏矩阵一旦你写X.toarray()或者标准化操作里有dense属性内存就爆了。解决TfidfVectorizer里显式设max_features5000再配合min_df2和max_df0.8过滤低频词和高频噪音词。如果确实需要看全特征把可视化用的词云基于原始分词结果做而不要基于TF-IDF矩阵转稠密词云要的是词频统计不是模型输入。另外在代码里少写.toarray()需要转稠密的地方单独处理用完后及时del释放内存。5.5 Matplotlib图表中文乱码所有可视化变成方框现象画混淆矩阵热力图和情感分布饼图时中文标签全部显示成方框或者乱码图表输出到文档说明里也没法用。原因Matplotlib默认字体是DejaVu Sans不包含中文字形。系统有中文字体Windows的SimHei、macOS的Arial Unicode MS、Linux的Noto Sans CJK但Matplotlib不会自动去找它有自己的字体目录缓存。解决在可视化脚本开头声明中文字体用matplotlib.font_manager查可用字体名然后plt.rcParams[font.sans-serif] [SimHei]并加axes.unicode_minusFalse解决负号显示问题。如果系统里完全没有中文字体先去装一个再从系统字体目录复制到Matplotlib字体目录并清缓存。一次性解决这个坑别放在最后做可视化时才来查提前在环境配置阶段验证一段中文字符能不能正常渲染。我在项目里专门写了一个fonts_check.py只做一件事画一张带中文标题的图跑通再继续做其他可视化。这个方法也顺带验证了vscode python环境配置里的每一项依赖是否安装到位。6. 可视化与期末答辩用图表把数据讲清楚可视化是期末项目文档里加分最快的一环不是为了好看而是为了在答辩时用一张图讲清楚一个结论。我实际项目里至少做四张图情感分布饼图先说数据构成、TF-IDF高频特征词云说明特征工程的结果、混淆矩阵热力图展示模型在哪个类别上出错、NB vs SVM的对比柱状图呈现最终结论。这四张图分别对应文档说明的数据、特征、模型、结论四个部分答辩时讲到哪就切到哪张图。import matplotlib.pyplot as plt import seaborn as sns from sklearn.metrics import confusion_matrix # 中文字体设置兼容win/mac/linux三种环境 plt.rcParams[font.sans-serif] [SimHei, Arial Unicode MS, Noto Sans CJK SC] plt.rcParams[axes.unicode_minus] False # 混淆矩阵热力图 cm confusion_matrix(y_test, y_pred_svm) sns.heatmap(cm, annotTrue, fmtd, cmapBlues, xticklabels[负面, 中性, 正面], yticklabels[负面, 中性, 正面]) plt.xlabel(预测标签) plt.ylabel(真实标签) plt.title(SVM混淆矩阵) plt.savefig(output/figures/confusion_matrix_svm.png, dpi150, bbox_inchestight)答辩的时候我的讲解顺序是先讲数据来源和标注规则再讲清洗过程中删掉了哪些噪音展示一两条清洗前后的对比然后说TF-IDF的两个关键参数为什么这么设最后拿出NB和SVM的对比表。每张图都对应一句话的结论不要光念数字——比如“混淆矩阵显示负面类别的召回率只有0.6是因为数据里负面样本占比低下一步可以采集更多负面数据”。文档说明里最好附上交叉验证的稳定性和网格搜索的参数范围这些都是评分点。有一点我自己反复踩过的教训不要在答辩时现场跑网格搜索。就算数据只有2000条RBF核的网格搜索也要几分钟现场跑容易卡住演示变成事故。正确的做法是提前把最优参数跑出来存成模型文件sklearn的joblib.dump答辩演示只跑预测和可视化。另一个习惯是把SVM的最优参数和测试集结果直接写进README即使评委问参数怎么来的你也能说清楚是网格搜索得到的最优组合而不需要依赖现场演示。如果后续想升级方向多模态情感分析是把表情图片和文本一起建模但那是把这份工作做扎实之后的事了。希望这个方案能帮你把期末大作业的路走顺。我做过几次之后最大的感受是传统机器学习在短文本情感分析上并不过时NBSVM的组合在今天仍然是性价比最高的课程设计选题之一把数据和特征讲透比盲目上BERT更能在答辩现场站住脚。希望帮到你。本文还有配套的精品资源点击获取