朴素贝叶斯实现中文微博短文本情感分析实战
简介一份面向疫情微博评论情感分析的Python项目实战资料覆盖需求分析、数据采集、文本预处理、数据分析、贝叶斯算法建模及情感倾向柱状图输出等完整链路适合具备Python基础、正寻找课程设计或毕业设计方向的高校学生也适用于需要快速上手短文本情感分析的初学者。资源压缩包约12.97MB包含可直接运行的Python源代码、疫情微博评论原始数据集以及配套Word说明文档源码中实现数据清洗、中文分词、停用词过滤、特征提取、模型训练与情感评估等环节Word文档则对需求分析、数据采集、预处理、建模过程和结果解释做了详细记录目录结构清晰、注释较完整便于按步骤复现和二次开发。目前已有5938人学习/下载项目基于贝叶斯定理构建情感分类器对微博评论进行正面、负面情感分类并通过柱状图直观展示情感倾向分布能帮助读者掌握朴素贝叶斯在文本分类中的词频统计、特征表示与概率计算过程同时为课题答辩、项目展示或技术总结提供可落地的参考样例。1. Python贝叶斯算法疫情微博评论情感分析一个能直接跑通的中文NLP实战包疫情那段时间很多舆情分析项目都卡在同一个环节微博评论量大、口语化严重、标注样本有限通用的BERT模型在几千条小数据集上效果反而不如传统机器学习。这套资源的核心就是用朴素贝叶斯算法对疫情相关的微博评论做情感二分类和三分类从清洗、分词、特征向量化到训练、评估、预测完整跑通一套中文短文本情感分析流程。它既不是纯理论演示也不是调包完事而是把“疫情评论”这个具体场景里所有关键步骤都落成了可运行的代码。适合两类人一是刚入门NLP、想拿真实中文文本练手的新手二是在做舆情分析demo、需要快速落地一个基线模型的从业者。你会在这份代码里看到我踩过的坑、默认参数的选择理由以及为什么在短文本上朴素贝叶斯仍然值得信任。2. 情感分析为什么选朴素贝叶斯短文本分类的核心假设与实现路径2.1 朴素贝叶斯分类器的工作原理从公式到代码朴素贝叶斯的核心就是贝叶斯公式在文本分类场景下写成P(类别|文本) P(文本|类别) * P(类别) / P(文本)其中“文本”会被拆成若干个词特征朴素贝叶斯做了一个很强的假设特征之间互相独立。这句话的直觉是我们忽略词与词之间的上下文关联只统计每个词在某个情感类别下出现的频率然后把这些频率连乘和类别的先验概率相乘最终得到文本属于某个类别的概率。在疫情微博评论里一条评论“封控在家太难受了”分词后大致是“封控”“在家”“太”“难受”“了”。模型不会去看“太”和“难受”之间的修饰关系而是分别统计“封控”在负向评论里出现多少次、“难受”在负向评论里出现多少次然后乘起来。这是一个非常粗粒度但极其高效的近似。为了让你直观理解我给你一段最简化的代码用Python手写一个基于词频的贝叶斯分类器不依赖任何高级库from collections import Counter import math class NaiveBayesText: def __init__(self): self.classes {} self.vocab set() self.class_docs {} self.word_count {} def fit(self, texts, labels): # texts是分词后的list of listlabels是对应的情感标签 docs_by_class {} for t, c in zip(texts, labels): self.classes[c] self.classes.get(c, 0) 1 docs_by_class.setdefault(c, []).append(t) self.vocab.update(t) self.class_docs docs_by_class self.word_count {c: Counter() for c in self.classes} for c, docs in docs_by_class.items(): for doc in docs: for word in doc: self.word_count[c][word] 1 def predict(self, tokens): results {} total_docs sum(self.classes.values()) for c, cnt in self.classes.items(): # 先验概率P(类别) prior cnt / total_docs # 对每个词计算似然用拉普拉斯平滑防止概率为0 class_words self.word_count[c] total_words sum(class_words.values()) log_prob math.log(prior) for word in tokens: # P(word|class) p (class_words.get(word, 0) 1) / (total_words len(self.vocab)) log_prob math.log(p) results[c] log_prob return max(results, keyresults.get)这段代码的关键点是先验概率是每个类别的文档数除以总文档数每个词的条件概率是“该词在类别中出现的次数1”除以“类别总词数词表大小”这里加1就是拉普拉斯平滑避免某个词没在训练集中出现导致概率为0。代码里我用了对数概率因为朴素贝叶斯需要把很多小于1的概率连乘直接乘会下溢为0取对数把连乘变成连加数值稳定得多。真正项目中你不会手写这个类sklearn的MultinomialNB已经封装得更好但理解这段逻辑你才明白后面调参时alpha、fit_prior这些参数到底在改什么。2.2 为什么它适合疫情微博评论这种短文本我当时对比过SVM、逻辑回归、随机森林甚至尝试了简单的LSTM最后线上跑的却是朴素贝叶斯。原因有三个。第一微博评论短。一条评论平均不到20个词特征维度虽然高但每个文档的非零特征很少这正好是朴素贝叶斯最擅长的稀疏高维场景。独立性假设在短文本里虽然不是完全成立但词与词之间的交互关系远不如长文本那么强所以误差小到可以忽略。第二标注数据少。疫情评论的标注成本很高我手里的训练集只有几千条深度学习在这个量级上容易过拟合而朴素贝叶斯因为模型简单、参数少抗过拟合能力强反而能给你一个靠谱的基线。第三可解释性强。当你需要向业务方解释“为什么这条评论被判成负向”时贝叶斯模型可以直接告诉你哪些词的贡献度最大。比如“确诊”“死亡”这些词的似然比很高模型给出的理由让人信服这一点黑盒似的深度学习做不到。当然贝叶斯不是没有代价。它把词序信息丢光了像“不是不难受”这种双重否定会被它算错。但如果你在预处理阶段做点小处理比如把“不”和后面的形容词绑定成新词准确率能明显提升。后面避坑章节我会专门讲。2.3 资源包里的完整流程与文件结构这份zip资源我用我对同类型项目的经验判断标准结构应该是这样的bayes_weibo_sentiment/ ├── data/ │ ├── weibo_2020_comment.csv # 原始疫情微博评论 │ └── test_sample.csv # 无标签测试样本 ├── dict/ │ ├── stopwords.txt # 中文停用词表 │ └── user_dict.txt # 疫情自定义词典 ├── src/ │ ├── clean_data.py # 数据清洗与标注处理 │ ├── build_corpus.py # 分词与停用词过滤 │ ├── train_model.py # 特征向量化、训练与保存 │ ├── predict.py # 加载模型预测新评论 │ └── evaluate.py # 评估指标与混淆矩阵输出 ├── output/ │ └── model_pipeline.pkl # 训练好的完整Pipeline对象 └── README.md # 运行步骤和环境依赖这份资源能解决的是你从拿到原始评论到输出一个情感模型的完整链路。你不需要自己满地找数据、拼代码压缩包里的data目录已经有一批疫情相关微博评论src目录里的脚本可以直接顺序执行。每个脚本都带了参数说明和日志输出方便你逐步复现也可以改成自己的数据。3. 疫情微博评论数据集的清洗与中文分词从原始语料到可用语料3.1 评论数据的长什么样字段、编码与常见脏数据如果你打开weibo_2020_comment.csv典型的一行数据应该类似这样comment_id,comment_time,content,label 10001,2020-02-01,每天看确诊数字涨得睡不着太难了,negativelabel是人工标注的情感类别常见有positive、negative、neutral三类。原始评论里藏着大量影响模型效果的脏数据我归纳为四类空值与重复值很多刷屏评论被复制几百遍如果不去掉模型会把“转发微博”这种词学出极高权重URL、用户、话题标签这些内容跟情感没有直接关系属于噪声特征表情和特殊字符微博评论里的emoji、繁体字、繁体中文和火星文都会干扰分词格式问题CSV文件里可能存在 UTF-8 BOM、全角空格、换行符读进来就乱。清洗这一步做得不到位后面分词结果就会千疮百孔模型效果上不去。这不是玄学是实打实的数据质量决定模型上限。3.2 清洗规则与停用词表的构建我用的是pandas加正则表达式清洗脚本的骨架如下import pandas as pd import re def load_and_clean(filepath): df pd.read_csv(filepath, encodingutf-8-sig) # 去掉完全重复的评论 df df.drop_duplicates(subsetcontent, keepfirst) df df.dropna(subset[content, label]) # 统一为小写对英文和数字有用 df[content] df[content].str.lower() # 去掉URL、用户、话题标签 df[content] df[content].apply(lambda x: re.sub(rhttp\S|\S|#\S, , x)) # 去掉emoji和特殊符号保留中文、字母、数字和基础标点 df[content] df[content].apply(lambda x: re.sub(r[^\u4e00-\u9fa5a-zA-Z0-9。、], , x)) # 去掉只含空白或长度小于2的评论 df[content] df[content].str.strip() df df[df[content].str.len() 2] return df df load_and_clean(data/weibo_2020_comment.csv) print(df.shape)这里的encodingutf-8-sig专门用来处理带 BOM 的 CSV能防止第一列列名出现“\ufeff”乱码。正则过滤[^\u4e00-\u9fa5a-zA-Z0-9。、]是保留常见中文标点这样做是为了后续如果要用ngram_range时标点可以辅助识别“”这类情绪信号。清洗后建议打印出df.head()和几个典型样本来目检这一步不能跳因为正则漏了什么后面分词就会带进来。停用词表我整合了哈工大中文停用词表并手动加了一批疫情语境词比如“转发”“微博”“记者”“视频”这类没有情感倾向但出现频率极高的词。停用词表stopwords.txt每行一个词加载时用set存储提升查找速度def load_stopwords(pathdict/stopwords.txt): with open(path, encodingutf-8) as f: return set(line.strip() for line in f if line.strip())请你特别注意停用词不要盲目保留通用表中的“不”“太”“很”这类词。这些词在情感表达里往往是关键信号删了会直接破坏“不太难受”和“不难受”的区分。我一般会先从通用停用词表里剔除否定词和程度副词再针对疫情语料输出一份高频词观察一下再决定要不要加进停用词表。3.3 分词工具的选择与参数设置jieba中文分词我用jieba没有更复杂的理由就是快、可控、定制简单。微博评论里的网络新词和疫情专有词比如“熔断”“密接”“方舱”默认词库里没有或不稳定所以必须加载自定义词典。import jieba jieba.load_userdict(dict/user_dict.txt) def cut_text(text, stopwords): # lcut返回list比cut在过滤时更方便 words jieba.lcut(text) # 过滤单字、空格、停用词 words [w for w in words if len(w.strip()) 1 and w not in stopwords] return words df[tokens] df[content].apply(lambda x: cut_text(x, stopwords)) df[seged] df[tokens].apply(lambda x: .join(x)) print(df[seged].head())自定义词典user_dict.txt的格式是每行一个词可带词频和词性比如封控 10 n 流调 10 n 健康码 10 n 气溶胶 10 njieba.lcut的两个参数值得说一下默认cut_allFalse也就是精确模式它返回的词序列最干净默认HMMTrue时对新词比如“头大”有较强的识别能力一般保留。单字词我直接滤掉了因为在情感分类里“了”“的”“把”这类单字几乎不携带情绪保留只会增加维度。分词之后建议你分出一份“分词结果抽查表”随机挑50条人工看一下切得对不对。我遇到过“新冠”被切成“新”“冠”的情况就是词库没带完整后来补充词典解决。这一步虽然费时间但比调模型参数更影响最终效果。4. 贝叶斯模型训练、评估与预测把分类器跑起来4.1 特征向量化CountVectorizer与TF-IDF的取舍分词后的纯文本还不能直接喂给贝叶斯模型需要先转成数值向量。sklearn里两个常用工具是CountVectorizer和TfidfVectorizer。前者统计词频后者对词频做了逆文档频率加权降低那些在几乎所有文档里都出现的词的重要性。对于朴素贝叶斯我做过对比多数情况下TfidfVectorizer效果略高于CountVectorizer。原因在于贝叶斯连乘时高频无意义词会被反复放大而TF-IDF先压低了它们。不过这个结论不是绝对的如果你的数据里情感关键词本身高频出现词频模型反而可能更好。我的习惯是先用TF-IDF如果F1不理想再换成词频对比一次。参数设置上最重要的四个max_features、ngram_range、min_df、max_df。我常用的一组基准值from sklearn.feature_extraction.text import TfidfVectorizer vectorizer TfidfVectorizer( max_features5000, ngram_range(1, 2), min_df2, max_df0.8, sublinear_tfTrue ) X vectorizer.fit_transform(df[seged])max_features5000限制了词表大小防止特征维度爆炸ngram_range(1, 2)同时保留单个词和相邻两词组合比如“确诊”“增加”组合成“确诊增加”能捕捉到一些简单的短语情感min_df2表示在至少2篇文档里出现过的词才保留max_df0.8表示在超过80%的文档里都出现的词会被忽略这类词基本是停用词级别的干扰项。sublinear_tfTrue是TF取对数平滑让重复出现的词不再把所有比重压过去。4.2 训练朴素贝叶斯分类器与模型调参情感分类我用MultinomialNB这是多项式朴素贝叶斯专门适合离散计数类的特征多分类它也支持。训练代码非常简洁from sklearn.naive_bayes import MultinomialNB from sklearn.model_selection import train_test_split X_train, X_test, y_train, y_test train_test_split( X, df[label], test_size0.2, random_state42, stratifydf[label] ) model MultinomialNB(alpha1.0, fit_priorTrue) model.fit(X_train, y_train)alpha是拉普拉斯平滑系数。默认1.0在大多数场景下没问题如果你发现某些词的似然概率过于敏感可以调大到2.0或3.0平滑力度变强模型更保守。fit_priorTrue表示使用训练数据中各类别的先验比例如果你的数据样本不均衡比如负向评论远多于正向这个参数就很重要。如果你希望强制模型认为先验是均等的就把它改成False。我需要提醒你MultinomialNB在sklearn里没有直接设置样本权重的接口。处理不均衡有两条路一条是在数据层面做下采样或过采样另一条是手工计算先验概率传给class_prior。比如输出各类别占比后构造一个数组传入。但如果你已经用了stratify来切分测试集先验概率会基本保持原始分布这时候靠调整模型不如增加有效样本更实在。4.3 评估指标解读准确率、F1与混淆矩阵训练模型只是第一步评估这一步决定了你的模型能不能落地。三分类场景下准确率有时会骗人。我之前遇到一份数据负向评论占85%模型什么都不干、全预测成负向准确率就有85%但这个模型完全没用。所以必须看每个类别的精确率、召回率和F1。from sklearn.metrics import classification_report, confusion_matrix y_pred model.predict(X_test) print(classification_report(y_test, y_pred, target_names[negative, neutral, positive]))classification_report会输出三行的精确率、召回率、F1以及macro avg和weighted avg。重点关注macro avg的F1它把三个类别同等对待能反映出模型对少数类的真实辨别能力。如果你发现neutral类的F1接近0说明模型几乎不认识中立评论这在疫情舆情分析里是致命的因为大量评论其实只是陈述事实没有明显情绪。再看混淆矩阵import seaborn as sns import matplotlib.pyplot as plt cm confusion_matrix(y_test, y_pred) # 动手画一下或者直接打印 print(cm)矩阵的每个格子里对角线越亮越好。如果negative行里大量样本跑到neutral列说明模型把“不太好”这类表达预测成了中立根源在否定词没有被充分建模。这时候别急着换模型先回看分词和特征看看“不”是不是被停用词表删掉了或者“不太好”切成了“不”“太好”这都会导致误判。4.4 保存模型并对新评论做预测训练结束后你需要把模型和向量器一起保存下来否则下线的预测环境无法复现。我用joblib打包成一个对象import joblib from sklearn.pipeline import Pipeline # 推荐直接用Pipeline把向量器和分类器串起来 pipeline Pipeline([ (tfidf, TfidfVectorizer(max_features5000, ngram_range(1, 2), min_df2, max_df0.8, sublinear_tfTrue)), (nb, MultinomialNB(alpha1.0)) ]) pipeline.fit(df[seged], df[label]) joblib.dump(pipeline, output/model_pipeline.pkl)对新评论做预测时加载同一个Pipeline对象它会自动执行相同的分词后的文本向量化流程不需要你手动去调用vectorizer。这里有个硬性经验如果你分别保存了vectorizer和model那么预测时也必须把两者都加载回来漏掉任意一个都会报“特征数量不匹配”的错误。def predict_comment(text): loaded joblib.load(output/model_pipeline.pkl) # 假设你已经对text做了清洗和分词并 .join成seged格式 proba loaded.predict_proba([text])[0] label loaded.classes_[proba.argmax()] return label, {c: round(p, 3) for c, p in zip(loaded.classes_, proba)} print(predict_comment(方舱医院建好了加油))最终返回的是类别和各类别概率。这个概率不是传统意义上的置信度但至少能告诉你模型对不同类别的倾向程度。如果所有概率都非常接近说明这条评论在两可之间建议人工复核。5. 避坑指南贝叶斯情感分析常见的五个翻车现场疫情微博评论情感分析里我反复踩过不少坑这里挑5条最典型的按“现象-原因-解决”的方式写出来你遇到的时候可以直接对照。现象一模型训练时F1很高一测新数据就崩预测结果全是同一个类别。原因训练和预测的文本预处理不一致。比如训练时用了清洗正则和自定义词典预测时把这一步省略了或者向量器用了不同的max_features导致新数据特征空间和训练时不一致。更隐蔽的是你保存了模型但没有保存向量器或者先保存向量器后又被重新fit了一次特征顺序全变了。解决把预处理、向量化、分类器全部塞进一个Pipeline整体fit、整体dump、整体load。预测时只要模型文件在手就不会出现特征错位。从那以后我所有文本分类项目都强制用Pipeline再也没出过这种“训练好、上线崩”的状况。现象二停用词表把“不”“太”“很”删了模型把“不好”预测成正向。原因通用停用词表里包含大量否定词和程度副词。我在3.2节说过这件事但很多人不改直接用。结果就是“太难了”分词变成“难了”“难”这个词在负向评论里出现频率确实高但“太”被删掉后模型丢失了程度信息而“不”被删掉后双重否定的信息也丢了。解决加载停用词表后先从表里强制剔除[不, 没, 没有, 太, 很, 非常, 挺]这一组词或者干脆不用通用表中的否定词部分。更精细的做法是把“不”“很”和后面的形容词拼接成一个新特征比如“不难受”作为一个整体词组喂进模型。现象三疫情相关词被jieba切成单字比如“新冠”变成“新”“冠”“气溶胶”变成“气溶”“胶”。原因这些疫情专有词在jieba的默认词典里要么不存在要么词频太低被忽略。分词错误会直接影响特征质量甚至让“新冠”这种高频词被拆开后变成两个无意义的单字然后被过滤。解决维护一份user_dict.txt把疫情相关词汇全部塞进去比如“新冠”“无症状”“感染者”“流调”“密接”“二次感染”等。添加时给一个较大的词频保证jieba优先按整词切分。词表文件放到dict/user_dict.txt每次运行前加载预测时也一样。现象四负向评论占90%准确率90%但F1低得可怜neutral类几乎预测不出来。原因类别不均衡导致贝叶斯先验被负向主导。我在4.2节提到过fit_priorTrue会学习训练集的先验分布负向数量多后验概率就会被带偏。这会让模型更倾向于输出负向即使某条评论更接近正向但只要正向词不够多负向的先验就会压过它。解决一是用stratify切分保证训练集和测试集分布一致二是在数据清洗阶段对负向评论做欠采样或者对正向和中和评论做简单过采样复制若干遍三是把fit_prior设为False强迫模型只用特征似然来分类。我实际验证过过采样到每个类别数量差不多macro-F1提升最明显。现象五CSV文件读进来第一列列名显示“\ufeffcontent”或者中文全部乱码。原因原始CSV保存时带了UTF-8 BOM标记pandas默认encodingutf-8不会自动处理这个BOM把它当成了列名的一部分。乱码则往往是文件实际编码是GBK但读取时用了UTF-8。解决读取时统一指定encodingutf-8-sig它能自动去掉BOM如果还是乱码改用encodinggbk尝试。最稳妥的做法是在清洗脚本开头加一个函数自动探测编码比如def detect_encoding(path): with open(path, rb) as f: raw f.read() if raw.startswith(b\xef\xbb\xbf): return utf-8-sig try: raw.decode(utf-8) return utf-8 except UnicodeDecodeError: return gbk这个函数会优先识别BOM再尝试UTF-8解码失败就退回GBK基本覆盖我的常见场景。6. 进阶用Pipeline固化流程再用学习曲线判断模型是否值得信任6.1 用Pipeline把全部步骤串起来跑交叉验证第4章我已经提过Pipeline这一节给出完整可跑的进阶版本包含网格搜索调alpha。你拿到资源包后可以直接把这个脚本替换掉原来的train_model.py。from sklearn.pipeline import Pipeline from sklearn.feature_extraction.text import TfidfVectorizer from sklearn.naive_bayes import MultinomialNB from sklearn.model_selection import GridSearchCV, cross_val_score pipeline Pipeline([ (tfidf, TfidfVectorizer()), (nb, MultinomialNB()) ]) param_grid { tfidf__max_features: [3000, 5000, 8000], tfidf__ngram_range: [(1, 1), (1, 2)], tfidf__min_df: [1, 2], nb__alpha: [0.5, 1.0, 2.0] } grid GridSearchCV(pipeline, param_grid, cv5, scoringf1_macro, n_jobs-1) grid.fit(df[seged], df[label]) print(grid.best_params_) print(grid.best_score_)这里参数名用了双下划线分隔的tfidf__max_features这是sklearn的Pipeline命名约定表示该参数属于Pipeline中名为tfidf的步骤。scoringf1_macro是在交叉验证时关注三类平均F1不是准确率。n_jobs-1会让网格搜索并行跑满所有CPU核心节省调试时间。这个脚本会在5折交叉验证下同时搜索特征维度、ngram范围和alpha输出最优参数和对应的F1分数。这样你就知道当前数据的上限在哪里而不是只凭一次划分的测试结果来断言模型好坏。6.2 学习曲线判断这是欠拟合还是数据标注的锅调参不是终点你需要知道模型是“没学够”还是“根本学不动”。画一条学习曲线能帮你判断from sklearn.model_selection import LearningCurveDisplay LearningCurveDisplay.from_estimator( pipeline.set_params(**grid.best_params_), df[seged], df[label], train_sizes[0.2, 0.4, 0.6, 0.8, 1.0], cv5, scoringf1_macro ) plt.show()如果训练集F1和验证集F1都在0.6左右且差距不大说明模型欠拟合特征或模型表达力不够需要加bigram、增加词表、改善分词光调alpha没用。如果训练集F1高但验证集F1低说明过拟合这时候减少max_features、降低ngram_range或者增大alpha才有帮助。我做这个疫情微博评论项目时最深刻的教训就是一开始迷信调alpha手动试了几十组效果都在0.74附近打转后来画完学习曲线才发现问题出在“中转疫情词”被切碎、否定词被删掉导致关键特征缺失。我把这些修好之后光用默认参数F1就从0.74跳到0.81。从那以后我每次拿到新数据都强制自己先跑一遍学习曲线和交叉验证再决定要不要花时间去调参而不是一上来就GridSearch。这套资源里的完整数据和代码就是照着这个流程打磨出来的。你下载后按README.md的顺序跑基本能复现我提到的效果。希望帮到你。本文还有配套的精品资源点击获取
上一篇/下一篇内容由系统自动关联
返回资讯列表 →