简介:一套基于机器学习朴素贝叶斯与支持向量机算法的微博评论情感分析可视化项目源码,面向计算机相关专业正在准备期末大作业、课程设计或需要项目实战练习的学习者。项目经导师指导并获评审99分,代码完整、可运行,覆盖从微博评论数据获取、中文停用词过滤、情感得分计算到模型训练与可视化展示的完整流程,可作为高分大作业模板参考。压缩包共51个文件,以Python源码(5个py文件)、csv数据集、txt词库/停用词表、png/jpg截图与结构图、html可视化页面及训练好的model模型文件为主,总大小约17.79MB,目录清晰,便于按模块对照学习。当前已有303人学习下载。随包提供README说明、操作命令txt、项目流程图及完整模型文件,适合快速理解情感分析项目从数据到应用的实现思路,能有效降低独立完成类似大作业的上手门槛。
1. 这门期末大作业,选它的人最后都拿到了什么
先给结论:用朴素贝叶斯和支持向量机做微博评论情感分析,是文本分类方向里性价比最高的一套组合拳。它不挑显卡、不依赖深度学习框架、训练时间以秒计,却能把「分词 → 特征提取 → 模型训练 → 评估 → 可视化」这条完整链路走通。对期末大作业来说,这意味着你不需要花两周调超参数,只需要把每个模块做扎实,就能拿到一份结构完整、答辩有东西可讲的源码和文档说明。
很多学生卡在同一个地方:数据有了,模型也跑起来了,但不知道中间每一步在做什么。这篇笔记直接按一套可复现的方案来写——从微博评论怎么清洗、中文分词怎么选词,到朴素贝叶斯和 SVM 两个模型的差异分析,再到混淆矩阵和情感分布图怎么画,最后把最常见的五个翻车现场列出来。全程 Python 实现,依赖只用 sklearn、jieba、pandas 和 matplotlib,环境配好就能跑。这套方案对做文本分类、舆情分析、电商评论分类的人同样适用,期末作业只是它最典型的一个落地场景。
2. 从微博评论到特征向量:文本预处理与中文分词落地
2.1 为什么中文情感分析不能直接喂原始文本
机器学习模型吃的是数值,不是字符串。微博评论是短文本,通常不到 50 个字,噪声却很大:有@用户、URL、表情符号、重复标点、繁体字和网络用语。如果你直接把原始文本传给朴素贝叶斯,模型看到的是一堆无法计算的字符,结果必然是报错或者全部分到同一类。
常见做法是把文本转成向量,这一步决定了模型的天花板。对短文本情感分析来说,最常用的特征是 TF-IDF 权重,它比单纯的词频更好用,因为 TF-IDF 能降低「的」「了」「就」这类高频无意义词的干扰,同时保留「难喝」「客服」「差评」这类判别性强的词。选 TF-IDF 而不是 Word2Vec 或 BERT 的原因很现实:期末项目时间有限,TF-IDF 在几千条样本上几分钟就能跑完,而且 sklearn 里TfidfVectorizer直接封装好了,不需要额外下载预训练词向量。
2.2 清洗规则:去噪比选模型更重要
我先给出一份通用的清洗函数,它处理的是微博评论最常见的几类噪声。这份代码直接复制就能用,但你要理解每一行在干什么,答辩时老师大概率会问。
import re import jieba def clean_weibo_text(text: str) -> str: # 去URL text = re.sub(r'http\S+|www\.\S+', '', text) # 去@用户名 text = re.sub(r'@\w+[::\s]?', '', text) # 去话题标签,如 #苹果发布会# text = re.sub(r'#.*?#', '', text) # 去表情符号占位,微博评论里常见[哈哈]、[泪]这种 text = re.sub(r'\[.*?\]', '', text) # 去重复标点,!!! 和 。。。 统一成单个 text = re.sub(r'[!!]{2,}', '!', text) text = re.sub(r'[。..]{2,}', '。', text) # 去空格和换行 text = re.sub(r'\s+', '', text) return text.strip()这段代码的关键在于处理顺序:先去掉 URL 和 @用户,再处理话题标签和表情占位符。顺序不能乱,比如如果先去空格再匹配 URL,URL 里的空格会导致匹配失败。re.sub的正则模式里,\S+匹配非空白字符,.*?是非贪婪匹配,保证[哈哈]这种短表情能被完整去掉。
清洗之后做分词和去停用词。停用词表建议自己维护一份,网上有现成的中文停用词库,但你要检查它是否包含「不」「没」「别」这类否定词——这三个词如果被当成停用词删掉,「不好看」会变成「好看」,情感极性直接反转。这是一个非常隐蔽的坑,第五章节会展开说。
STOPWORDS = set() with open('stopwords.txt', 'r', encoding='utf-8') as f: for line in f: STOPWORDS.add(line.strip()) def tokenize_for_ml(text: str) -> list: words = jieba.lcut(text) # 过滤停用词、单字词和纯数字 return [w for w in words if w not in STOPWORDS and len(w) > 1 and not w.isdigit()]jieba.lcut返回的是列表,比jieba.cut生成器更直观。过滤条件里len(w) > 1会把大多数单字词去掉,但也会误删「好」「差」「烂」这类单字情感词,这一步要看你的数据分布。如果你的语料里单字情感词出现频率高,就把这个条件去掉,或者单独保留一个情感词白名单。
2.3 TF-IDF 向量化:参数怎么设才不亏
向量化是整个预处理链路里最值得调参的一步。TfidfVectorizer有四个参数直接决定特征质量:max_features、ngram_range、min_df和max_df。我常用的配置和理由如下。
from sklearn.feature_extraction.text import TfidfVectorizer vectorizer = TfidfVectorizer( max_features=5000, ngram_range=(1, 2), min_df=2, max_df=0.8, token_pattern=r'\S+' # jieba 已经分好词,不需要默认的字母切分 ) # corpus 是清洗并分好词后的评论列表,每条评论的分词结果用空格拼接 corpus = [' '.join(tokenize_for_ml(text)) for text in df['clean_comment']] X = vectorizer.fit_transform(corpus)max_features=5000限制特征维度,减少过拟合并加快训练。ngram_range=(1, 2)同时保留单个词和双词组合,双词组合能捕捉「不好」「服务差」这类短语,但三元以上在短文本上提升有限,还容易造成维度爆炸。min_df=2表示词至少出现在 2 条评论里才保留,可以去掉只出现一次的噪声词。max_df=0.8表示词在 80% 以上的评论中都出现则忽略,这类词基本是「的」「了」等高频无意义词。token_pattern=r'\S+'很重要,因为 jieba 已经用空格分隔好了词,默认的正则会把中文标点也切进去。
提示:
fit_transform是在训练集上调用,测试集只调transform,不要对测试集再 fit 一次,否则会引入数据泄露,模型评估分数会虚高。
3. 朴素贝叶斯与 SVM:选型依据和 Python 实现
3.1 两个模型各自擅长什么
朴素贝叶斯和 SVM 在文本分类里都有一席之地,但原理完全不同。朴素贝叶斯基于贝叶斯定理,假设特征之间条件独立——这个假设在文本上显然不成立,「好吃」和「环境」有关联,但朴素贝叶斯假装它们无关。这个「错误」的假设反而让它在短文本上表现稳健,尤其在训练数据少的时候,它的先验概率平滑能力能压住过拟合。
SVM 的思路是找一个最大间隔超平面把不同类别的样本分开。对线性可分的数据,SVM 找到的分隔线不仅要分类正确,还要距离最近样本点最远,这让它的泛化能力比感知机强一个档次。文本经过 TF-IDF 向量化后往往维度很高但稀疏,这种场景下线性 SVM 非常擅长,因为它只在支持向量上计算,不受样本整体分布影响。
淘系和阿里的很多早期文本分类系统里,SVM 都是默认基线模型,今天用深度学习的人多了,但 SVM 在几千条标注样本上的表现依然能打。选两个模型做对比还有一个好处:期末答辩时,你能从「特征独立性假设」和「最大间隔」两个角度解释为什么它们在不同数据规模下各有优势,这是加分项。
3.2 朴素贝叶斯代码:MultinomialNB 与参数细节
sklearn 里朴素贝叶斯有三种:GaussianNB、MultinomialNB 和 BernoulliNB。文本 TF-IDF 特征是离散计数或者权重,满足多项分布,所以这里选 MultinomialNB。
from sklearn.naive_bayes import MultinomialNB from sklearn.model_selection import train_test_split # labels 是情感标签,0 表示负向,1 表示正向 X_train, X_test, y_train, y_test = train_test_split( X, labels, test_size=0.2, random_state=42, stratify=labels ) nb_model = MultinomialNB(alpha=1.0, fit_prior=True) nb_model.fit(X_train, y_train) nb_acc = nb_model.score(X_test, y_test) print(f'MultinomialNB 准确率: {nb_acc:.4f}')alpha是拉普拉斯平滑参数,默认 1.0。它的作用是防止某个特征在训练集中没出现导致概率为 0,调大 alpha 会让概率分布更平滑,对噪声数据的容忍度更高。fit_prior表示是否学习类别先验概率,如果数据类别不平衡,建议保持 True 让模型自动调整。stratify=labels是train_test_split里容易被忽略的参数,它保证划分后训练集和测试集的正负样本比例和原始数据一致,这个不设置,如果原始数据恰好顺序排列,很可能测试集全是同一类,评估分数会严重失真。
3.3 线性 SVM 代码:LinearSVC 为什么比 SVC 更快
SVM 在 sklearn 里有SVC和LinearSVC两个实现。对文本这种高维稀疏数据,LinearSVC用的是线性核的优化版本,训练速度比SVC(kernel='linear')快一个数量级,这是因为LinearSVC基于 liblinear 库,而SVC基于 libsvm,libsvm 在处理大规模稀疏数据时需要计算核矩阵,代价高很多。期末项目几千条数据感受不明显,但如果你后面扩到几万条,差别就出来了。
from sklearn.svm import LinearSVC svm_model = LinearSVC(C=1.0, loss='squared_hinge', max_iter=2000, random_state=42) svm_model.fit(X_train, y_train) svm_acc = svm_model.score(X_test, y_test) print(f'LinearSVC 准确率: {svm_acc:.4f}')C是正则化强度的倒数,C 越大越强调训练集分类正确,越容易过拟合;C 越小则容忍更多误分类,换取更大的间隔和更好的泛化。文本分类通常从 C=1.0 开始试,如果训练集分数远高于测试集,说明过拟合,调小 C 到 0.1 或 0.5。loss='squared_hinge'比默认的hinge收敛更快,在有约束优化问题上更稳定。max_iter=2000是防止默认 1000 次迭代没收敛时直接报警告,加上之后哪怕不收敛也能看到提示,而不是黑匣子一样跑完。
这两个模型训练完之后,建议把它们的预测结果都保存下来,后面的混淆矩阵和可视化两套都要用。
3.4 模型对比:不要只看准确率,还要看差异样本
准确率相近时不代表两个模型真的等价。常见做法是把两个模型的预测结果做差集,找到 NB 预测对但 SVM 预测错的样本,以及反过来的情况。这些样本往往是情感倾向模糊或者包含反讽的评论,它们能直观反映两个模型的性格差异。
import numpy as np nb_pred = nb_model.predict(X_test) svm_pred = svm_model.predict(X_test) # 找两个模型预测不一致的样本 diff_idx = np.where(nb_pred != svm_pred)[0] print(f'两个模型预测不一致的样本数: {len(diff_idx)}') # 把不一致样本里的原始评论和预测结果打印出来,人工观察规律 for i in diff_idx[:10]: print(f'原文: {df_clean.iloc[X_test.indices[i]]}') print(f'真实: {y_test.iloc[i]} | NB: {nb_pred[i]} | SVM: {svm_pred[i]}')这里的X_test.indices能回溯原始样本位置,这依赖于train_test_split返回的索引对应关系。如果你的 X_test 是一个 numpy 数组而没有保留索引,可以直接把原始 DataFrame 切成X_test同顺序的版本再取。这步帮你判断:如果 SVM 在带否定词的句子上更强,说明它的决策边界对特征组合更敏感;如果 NB 在短评上更强,说明它的概率平滑更适合稀疏表示。把差异样本的分析写进文档说明,答辩时是很有分量的内容。
4. 模型训练与评估:准确率之外,还要看混淆矩阵和可视化输出
4.1 评估指标怎么选才不骗自己
准确率在类别不平衡时是最不能信的指标。假设你的数据里 90% 是正向评论,模型全预测正向也能拿到 90% 准确率,但这个模型毫无价值。情感分析至少要看三个指标:精确率、召回率和 F1。精确率回答的是「预测为正的样本里真的正的比例」,召回率回答的是「真实为正的样本里被找出来的比例」,F1 是两者的调和平均。
from sklearn.metrics import classification_report, confusion_matrix # 用 SVM 结果做示例,NB 同理 print(classification_report(y_test, svm_pred, target_names=['负向', '正向'])) cm = confusion_matrix(y_test, svm_pred) print('混淆矩阵:') print(cm)classification_report输出的每一行对应一个类别的精确率、召回率和 F1,最后一行还有宏平均和加权平均。宏平均对类别不平衡敏感,加权平均按类别样本数加权,两者一起看能帮你判断模型是不是只在多数类上表现好。混淆矩阵是 2×2 结构,行是真实标签,列是预测标签。cm[0][1]是真实负向被预测成正向的数量,这个值如果偏高,说明模型有「乐观偏置」,即倾向于输出正向情感。
4.2 用 matplotlib 输出情感分布和 ROC 曲线
可视化的意义不是好看,而是让老师一眼看懂你的数据分布和模型性能。项目里至少要有三张图:情感类别分布柱状图、混淆矩阵热力图、SVM 的 ROC 曲线。这三张图分别回答「数据长什么样」「模型错在哪」「模型的判别能力有多强」。
import matplotlib.pyplot as plt import seaborn as sns from sklearn.metrics import roc_curve, auc from sklearn.preprocessing import label_binarize plt.rcParams['font.sans-serif'] = ['SimHei'] plt.rcParams['axes.unicode_minus'] = False # 图1:情感分布 fig, axes = plt.subplots(1, 3, figsize=(15, 4)) df['label_name'] = df['label'].map({0: '负向', 1: '正向'}) df['label_name'].value_counts().plot(kind='bar', ax=axes[0], color=['#d9534f', '#5cb85c']) axes[0].set_title('微博评论情感类别分布') # 图2:混淆矩阵热力图(用SVM的) sns.heatmap(cm, annot=True, fmt='d', cmap='Blues', xticklabels=['负向', '正向'], yticklabels=['负向', '正向'], ax=axes[1]) axes[1].set_title('SVM 混淆矩阵') axes[1].set_xlabel('预测标签') axes[1].set_ylabel('真实标签') # 图3:ROC 曲线 y_score = svm_model.decision_function(X_test) fpr, tpr, _ = roc_curve(y_test, y_score) roc_auc = auc(fpr, tpr) axes[2].plot(fpr, tpr, label=f'SVM (AUC = {roc_auc:.3f})') axes[2].plot([0, 1], [0, 1], 'k--', label='随机猜测') axes[2].set_xlabel('假正率') axes[2].set_ylabel('真正率') axes[2].set_title('SVM ROC 曲线') axes[2].legend() plt.tight_layout() plt.savefig('sentiment_analysis_result.png', dpi=150, bbox_inches='tight') plt.show()plt.rcParams设置中文字体是关键,不设置的话图里所有中文都会变成方框,这是每次画图都有人翻车的点。ROC 曲线用decision_function拿的是样本到超平面的距离,这个距离表示模型对分类结果的置信度,距离越大说明越确信是正向。AUC 面积越接近 1 越好,0.5 等于随机猜。如果你的 NB 模型也想画 ROC,MultinomialNB 没有decision_function,只能用predict_proba取正类概率替代,这是两个模型在可视化上的一个差异点。
4.3 词云:把特征词变成可视化证据
词云不是必须的,但如果你的文档说明里需要展示「模型学到了什么」,词云比特征重要性排序更直观。按情感类别分别生成正向词云和负向词云,能直接看出模型依据哪些词做判断。
from wordcloud import WordCloud def generate_wordcloud(texts, save_path, color): # 把所有评论拼成一个大字符串 all_text = ' '.join(texts) wc = WordCloud( font_path='simhei.ttf', width=800, height=400, background_color='white', max_words=100, colormap=color ) wc.generate(all_text) wc.to_file(save_path) # 正向评论词云 pos_texts = df[df['label'] == 1]['clean_comment'].tolist() generate_wordcloud(pos_texts, 'pos_wordcloud.png', 'Greens') # 负向评论词云 neg_texts = df[df['label'] == 0]['clean_comment'].tolist() generate_wordcloud(neg_texts, 'neg_wordcloud.png', 'Reds')font_path必须指定中文字体文件路径,Windows 下用C:/Windows/Fonts/simhei.ttf,macOS 下用系统自带的 PingFang 字体路径。不指定字体直接生成词云,会出现中文全部变成方块的问题。词云生成依赖jieba已经把词语用空格隔开,如果你传入的是原始句子,WordCloud 内部会按空格分词,得到的结果就是整个短语而不是词语。把词云放进文档说明时,记住它的作用是辅助验证,而不是证明模型效果好——模型效果还是以 F1 和 AUC 为准。
5. 微博情感分析五大常见坑:现象、原因与解决方案
5.1 否定词被停用词表误删,情感极性全反
现象:模型对「不好吃」「不满意」这类评论全部判为正向,准确率惨不忍睹。
原因:网上找的开源停用词表里收录了「不」「没」「别」等否定词,预处理时把这些词删了,「不好吃」变成「好吃」,情感极性反转。这是情感分析里最典型的预处理事故。
解决:在加载停用词表后,强制把否定词白名单加回来:
NEGATION_WORDS = {'不', '没', '别', '无', '莫', '勿', '未', '难'} STOPWORDS = STOPWORDS - NEGATION_WORDS # 从停用词表里剔除否定词如果你用的是带neg前缀的否定词特征,比如把「不好」整体变成neg_好,那是在特征工程层面处理,当前项目里只要保证否定词不进停用词表就够了。
5.2 类别不平衡导致模型只会输出多数类
现象:训练结束后分类报告显示正向类的 F1 有 0.95,负向类的 F1 只有 0.5,负向类大量被判成正向。
原因:标注数据时负向评论占比不足 20%,模型学到的最优策略是全部输出正向。MultinomialNB 的fit_prior=True会学习这种先验分布,SVM 的决策边界也会偏向多数类。
解决:优先用class_weight参数修正,而不是先做数据增强。对 SVM 加class_weight='balanced',对朴素贝叶斯需要手工调整class_prior,或者直接用compute_class_weight计算权重。
from sklearn.utils.class_weight import compute_class_weight class_weights = compute_class_weight('balanced', classes=np.array([0, 1]), y=labels) print(f'类别权重: 负向={class_weights[0]:.3f}, 正向={class_weights[1]:.3f}') # 传入 LinearSVC svm_balanced = LinearSVC(C=1.0, class_weight='balanced', max_iter=2000)5.3 分词结果太碎,情感词全被拆散
现象:jieba 分词后,「不太行」被切成「不太」和「行」,「难吃」被切成「难」和「吃」。
原因:jieba 默认词典面向通用场景,微博网络用语和口语化表达覆盖不足,情感短语经常被拆分。
解决:加载自定义词典,把高频情感短语整体加入。
# custom_dict.txt 内容格式:词 词频 词性 # 不太行 100 adj # 难吃 100 adj # 绝绝子 100 adj jieba.load_userdict('custom_dict.txt')自定义词典的格式是三列:词、词频(建议填一个大于默认分词频率的数,比如 100)、词性。加载词典后再次分词,你会看到「不太行」和「绝绝子」被保留为整体词。这一步不增加代码量,但对模型效果的影响有时候比调 C 参数还明显,因为特征质量直接决定分类上限。
5.4 SVM 训练报错或不收敛,日志里全是警告
现象:LinearSVC训练时控制台输出大量ConvergenceWarning,或者直接抛出ValueError说需要至少两个类。
原因:两种情况最常见。第一,max_iter默认 1000 在某些稀疏高维数据集上不够收敛;第二,数据划分后训练集里全是同一类,通常是因为原始数据按标签顺序排列,切分时没有洗牌。
解决:train_test_split时加shuffle=True(默认就是 True,但要确认你没手动关掉),并且设置random_state固定随机种子确保结果可复现。如果已经用了stratify,就不会出现训练集缺类的问题。max_iter从 2000 起步,要是还警告,就把tol放宽到 1e-4:
svm_model = LinearSVC(C=1.0, max_iter=5000, tol=1e-4)5.5 可视化中文全部变成方框
现象:柱状图、热力图的横纵坐标中文标签全是方框或者乱码。
原因:matplotlib 默认字体不支持中文,系统找不到对应的中文字形就渲染成方框。
解决:不要只用plt.rcParams['font.sans-serif'] = ['SimHei'],SimHei在 Linux 服务器上没有,直接指定字体路径或者用font_manager加载本地字体文件最稳。
import matplotlib.font_manager as fm font_path = '/usr/share/fonts/truetype/wqy/wqy-zenhei.ttc' fm.fontManager.addfont(font_path) plt.rcParams['font.family'] = fm.FontProperties(fname=font_path).get_name()加上plt.rcParams['axes.unicode_minus'] = False处理负号显示问题,这个也是老坑,忘记写的话 ROC 曲线 X 轴的负号会显示成方块。
6. 从「跑通」到「能讲」:最后一步把项目变成模块化代码
做到这里,你的代码大概率还是一个顺序执行的脚本。期末答辩时如果老师问「如果新增一类样本,你怎么改」,顺序脚本的劣势就暴露了。我把项目按模块拆成五个文件,每个文件职责单一,文档说明也能顺着模块讲清楚。
weibo_sentiment/ ├── data/ │ ├── raw_weibo_comments.csv # 原始评论 + 标签 │ ├── stopwords.txt # 停用词表(已剔除否定词) │ └── custom_dict.txt # 自定义情感词典 ├── src/ │ ├── preprocess.py # 清洗 + 分词 + 自定义词典加载 │ ├── features.py # TF-IDF 向量化 │ ├── models.py # 朴素贝叶斯 + SVM 训练与评估 │ └── visualization.py # 分布图、混淆矩阵、ROC、词云 └── run.py # 主流程,按顺序调用上面四个模块run.py里只需要写一个main()函数,按「读取数据 → 清洗分词 → 向量化 → 训练评估 → 可视化」的顺序调用。这样的结构让文档说明变得很好写:每个模块对应一个小节,配一段关键代码和一句设计理由。额外再准备一个批量验证脚本,模拟用户输入一句新评论,输出预测情感——这个在答辩现场演示效果非常好,老师会看到你的模型不是只在历史测试集上有效,而是真的能对新文本做预测。
演示验证的时候注意一个细节:新输入的文本要走和训练数据完全一样的预处理流程,先清洗再分词再 transform,不能直接调model.predict(['这家店真好吃']),因为模型没见过原始字符串,TfidfVectorizer的transform要求输入已经是空格分隔的分词结果。把「训练和预测走同一套预处理」写进代码注释和文档说明,是证明你真正理解整个链路的标志。
我自己的习惯是:每次运行完,把分类报告、混淆矩阵、AUC 这三组数字记在一个 markdown 文件里。改一次预处理方案,对比一次数字,谁变好谁变坏一目了然。这个习惯做三个版本迭代后,你会对哪些操作对情感分析真正有效有直觉,而不是靠猜。希望你也能从这套代码里得到你自己的结论,祝顺利。
本文还有配套的精品资源,点击获取