简介:这是一份基于朴素贝叶斯算法的疫情微博评论情感分析实战资源,面向正在学习自然语言处理、情感分析任务,以及需要完成课程设计或毕业设计的高校学生。项目以完整链路呈现,从需求分析、数据采集,到文本预处理、数据分析,再到贝叶斯定理建模与情感倾向柱状图输出,每一步均有对应Python代码和Word说明文档,便于读者对照实践和二次开发。压缩包约12.97MB,包含微博评论文本数据集、源码文件及说明文档,代码结构清晰、注释完整,可直接在PyCharm或Jupyter中运行,也支持替换为自有数据进行扩展。已有5938人学习下载,适合作为中文短文本情感分析的入门模板,也可作为后续扩展深度学习情感分析的基础参照。整体体量轻、流程完整,能帮助读者快速获得一套可运行的情感分析原型。
1. 先说结论:贝叶斯算法在疫情微博情感分析里为什么还是最优解
一条微博评论“核酸点多了,排队十分钟搞定”和另一条“凌晨三点还在排队,嗓子已经捅出茧子”,人一眼就能判断情绪,但让 Python 脚本批量判断几万条,浮点数层面的差距立马显现。用贝叶斯算法做疫情微博评论情感分析,是这类短文本任务里性价比最高的起点:不需要 GPU、不需要微调预训练模型,一条普通笔记本跑完全流程不超过半小时,而且每一步的结果都能解释。深度学习在长文本和大语料上占优,但微博评论平均十几个字、语料通常只有几千条,能榨出的上下文信息有限,朴素贝叶斯的“朴素”假设反而在这里变成了抗过拟合的优势。这篇文章按真实落地顺序走一遍:拿语料、清洗、分词、构造标签、训练贝叶斯模型、评估、部署成可调用的脚本。适合两类人——刚入门 Python 想做一个完整实战项目的新手,以及手里有标注数据、想快速出一版基线模型的工程师。
2. 语料准备与中文预处理:把一条微博变成能进模型的干净句子
模型的上限在数据预处理阶段就定死了,后面调参只是在这个上限上做修补。微博评论区是中文情感分析里最难处理的语料之一:短、乱、重复多、口语化严重,还混着大量表情、话题标签和转发串。处理不当,后面特征工程和贝叶斯分类做得再漂亮也会被垃圾输入拖垮。所以这一步值得花掉整个项目一半的时间。
2.1 语料从哪来:关键词搜索接口与数据存储字段
常见做法是拿微博移动端搜索接口按关键词抓取公开评论,登录后带上 Cookie,用 requests 直接请求 JSON 数据。个人学习场景下小批量采集没有问题,但注意控制频率、不要并发,正式商用或大规模采集前要重新确认平台规则。
import requests import csv import time headers = { "User-Agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 16_0 like Mac OS X)", "Cookie": "你的登录Cookie" } def fetch_weibo_by_keyword(keyword, page=1): # containerid 里的 q 参数是搜索关键词,页码从 1 开始 url = "https://m.weibo.cn/api/container/getIndex" params = { "containerid": f"100103type=1&q={keyword}", "page": page } resp = requests.get(url, params=params, headers=headers, timeout=10) return resp.json() def parse_cards(data): # 返回结构:cards -> card_group -> mblog -> text items = [] for card in data.get("cards", []): group = card.get("card_group", []) for g in group: mblog = g.get("mblog", {}) text = mblog.get("text", "") created_at = mblog.get("created_at", "") items.append({"text": text, "created_at": created_at}) return items def save_to_csv(rows, path="weibo_data.csv"): with open(path, "a", encoding="utf-8-sig", newline="") as f: writer = csv.DictWriter(f, fieldnames=["id", "text", "created_at", "label"]) for i, r in enumerate(rows): writer.writerow({ "id": f"{int(time.time())}_{i}", "text": r["text"], "created_at": r["created_at"], "label": -1 # -1 表示待标注 })这段代码的逻辑是:搜索接口按关键词返回多页数据,每页的 cards 里嵌套着 card_group,真正的微博文本在 mblog.text 字段里,而且是 HTML 格式,需要后续清洗。我一般会先打印一次 resp.json().keys() 确认返回结构,因为接口字段调整过不止一次,直接照抄解析路径容易踩空。
数据入库用 csv 而不是 excel,字段固定五个:id、text、created_at、label。label 先给 -1,等标注完再回填。编码必须用 utf-8-sig,否则 Excel 打开会乱码。接口请求间隔建议至少 sleep 2 秒,抓个两三千条就足够做第一版模型了,不需要贪多。
提示:接口字段随平台调整会变化,跑批量抓取前先打印一次返回结构确认,再写解析逻辑,否则容易在解析层翻车。
2.2 清洗规则:链接、@、话题标签、重复转发怎么处理
清洗顺序直接影响分词结果。我的习惯是严格按下面这个顺序处理:去 URL、去 HTML 标签、去 @用户名、去话题标签符号(保留话题词内容)、压缩连续空格。最后单独做一步重复文本过滤,这一步经常被忽略,但微博里“转发微博”开头的文本重复率极高,不处理的话“转发”这个词会变成模型权重最高的特征,把真实情感完全盖掉。
import re import hashlib def clean_weibo(text: str) -> str: # 1. 去掉 URL text = re.sub(r"https?://\S+", "", text) # 2. 去掉 HTML 标签,微博正文里的 <a>、<br> 等 text = re.sub(r"<[^>]+>", "", text) # 3. 去掉 @用户名 及后面的冒号 text = re.sub(r"@[\w\u4e00-\u9fa5]+[::]?", "", text) # 4. 话题标签只保留文字,去掉 # 号 text = re.sub(r"[##]", "", text) # 5. 压缩空白 text = re.sub(r"\s+", " ", text).strip() return text def is_duplicate(text: str, seen: set) -> bool: # 对清洗后的文本做 md5,完全相同的只保留一条 digest = hashlib.md5(text.encode("utf-8")).hexdigest() if digest in seen: return True seen.add(digest) return False这里有个细节值得展开:为什么不直接用正则把表情删掉?因为 emoji 在微博评论里携带很强的情感信号,一个“笑哭”和一句“绝了”组合起来是讽刺,单独删掉表情会丢失信号。我会在清洗阶段先用 emoji 库把表情转成文字词元,让“笑哭”变成一个普通词进入特征空间,后面分词时会被当作普通词汇处理。这个操作要在分词之前做,顺序反了效果差一大截。
import emoji def normalize_emoji(text: str) -> str: # 将 emoji 转为 [词元] 形式,例如 😂 -> [笑哭] return emoji.demojize(text, delimiters=(" [", "] "), language="zh")清洗后的文本如果长度低于两个字符,直接丢弃。这类短残留通常是纯链接、纯 @ 或者空串,留在数据集里只会制造噪音。
2.3 分词、停用词与自定义词典:中文短文本的预处理三件套
中文分词是绕不开的一步。微博文本没有空格边界,jieba 是最常用的分词库,但它的默认词典对疫情相关新词收录不全,所以必须加载自定义词典。词典文件每行一个词,格式是“词 词频 词性”,词频可以不写。
import jieba import jieba.analyse # 自定义词典,每行一个词,例如:核酸检测 jieba.load_userdict("covid_words.txt") # 停用词表:常规中文停用词 + 微博特有词 STOPWORDS = set(open("stopwords.txt", encoding="utf-8").read().split()) def cut_words(text: str): words = jieba.lcut(text) result = [] for w in words: w = w.strip() # 过滤停用词、单字、纯数字和空白 if not w: continue if w in STOPWORDS: continue if len(w) <= 1: continue if w.isdigit(): continue result.append(w) return " ".join(result)covid_words.txt 里我至少会放这些词:核酸检测、行程码、接种疫苗、方舱、密接、次密接、健康码、无症状、流调、封控、解封。你拿到自己的语料后可以跑一遍 jieba.analyse.extract_tags 抽取 Top 100 关键词,再人工补词。停用词表除了“的、了、是、在”这类常规词,还要加微博特有词:转发、分享、链接、微博、网页。注意“赞”这类词不要加进停用词,它本身有情感倾向,加了会损失信号。
分词做完后,每一条文本保存成一个用空格连接的字符串,这是 sklearn 特征抽取的标准输入格式。到这里,原始微博评论已经变成了干净、可分词、可向量化的语料,可以进入下一章讲标签构造和贝叶斯原理。
3. 标签与贝叶斯原理:为什么“朴素”假设反而适合短文本
3.1 没有现成标签时的三种做法
情感分析是监督学习,没有标签就谈不上训练。但现实里拿到的微博评论基本都是裸数据,标签通常用这三种方式补:
第一种是情感词典粗标。常见做法是用公开的中文情感词典(比如大连理工情感词汇本体库),给每个词打正负面分,把一条句子里所有情感词得分累加,超过阈值标为正面、低于阈值标为负面、接近零标为中性。这种方式冷启动快,不需要人工,但词典覆盖不了微博口语和网络新词,粗标结果的准确率大概在六到七成,只能当弱标签用。
第二种是人工标注。从清洗后的语料里随机抽一千到两千条,在三分类里逐条打标签。如果只有一个人标,速度大概是每小时一百五到两百条,一个下午能标完。两个人标需要算标注一致性,分歧大的样本直接丢掉。
第三种是找公开数据集。疫情相关的微博情绪分类在高校和竞赛平台上有过公开数据集,做算法对比时可以用,但拿来做业务落地前要先检查类别分布和时间跨度,因为舆情数据时效性很强,半年前的情绪分布和今天可能完全不一样。
我的建议是:先用情感词典粗标跑通全流程,同时人工精标一千条做测试集。粗标模型得到的分类器可以反过来辅助人工标注——把模型预测结果和人工标注结果不一致的样本优先挑出来人审,标注效率会高很多。
3.2 贝叶斯公式在情感分类上是怎么展开的
贝叶斯算法在情感分析里的核心公式不长,展开写就是:
某个类别(正面、负面、中性)的后验概率 = 该类别先验概率 × 全部分词在该类别下的条件概率乘积 ÷ 文本概率。
比较不同类别谁的概率大时,分母的文本概率对所有类别都一样,可以忽略,所以实际只需要算分子。这里面的“朴素”假设是指:把一条文本里各个词出现的概率当作相互独立。这个假设在语言学上显然站不住脚——“不”和“喜欢”放在一起,意思跟单独看完全不一样。但在短文本场景里,微博评论本身就没有多少上下文可以依赖,词与词之间的组合信息有限,独立假设造成的损失反而被稀疏特征抹平了。
还有一个反直觉的结论:文本越短,朴素贝叶斯的效果越接近深度学习。原因很简单,十几个字能提供的语义线索就那么多,复杂模型也榨不出更多信息,这时候贝叶斯的训练成本和推理速度优势就被放大了。做疫情微博情感分析这种句子级别的分类任务,贝叶斯作为基线模型的表现经常超出预期。
3.3 多项式、伯努利和高斯:到底选哪个
sklearn 里给的是三个不同的朴素贝叶斯变体,很多人不知道该怎么选。我直接给结论:
| 模型 | 特征输入 | 适合场景 | 主要缺点 |
|---|---|---|---|
| MultinomialNB | 词频或 TF-IDF 值 | 短文本分类、情感分析 | 对特征共现不敏感 |
| BernoulliNB | 0/1 是否出现 | 文本极短、重复词干扰大 | 丢失词频信息 |
| GaussianNB | 连续特征 | 非文本数据 | 不适合稀疏矩阵 |
文本情感分析首选 MultinomialNB,因为词频本身携带强度信息,“太棒了”和“棒”虽然都表示正面,但出现的频率能给模型更多判别依据。BernoulliNB 只在一种情况下更优:语料里大量出现“哈哈哈哈哈哈”这类堆叠重复词,词频被情绪性重复彻底污染时,出现与否反而比出现多少次更稳健。GaussianNB 在文本场景基本不碰,它假设特征服从高斯分布,直接套稀疏矩阵上效果非常差,大多数人在这一步踩坑。
3.4 平滑参数 alpha 和先验概率 class_prior
MultinomialNB 有两个实际调参点:alpha 和 class_prior。alpha 是拉普拉斯平滑系数,默认 1.0,作用是防止某个词在某个类别下从未出现导致概率计算得零。alpha 太小会过拟合训练集里的生僻词,太大则把所有词的概率拉平、模型变得迟钝。我的习惯是在 0.1、0.5、1.0、2.0 四个值上做交叉验证,选择 F1 最高的那个,而不是直接信赖默认值。
class_prior 是手动指定类别的先验概率。默认情况下模型根据训练集里各类别的占比自动计算,但如果训练集类别不均衡、正面占 70%,模型会偏向把不确定的样本判成正面。这时可以手动设置 class_prior=[0.3, 0.4, 0.3] 之类,比做下采样更省事,还不会损失数据。需要提醒的是,使用 class_prior 时要在交叉验证里观察它对各类别 recall 的实际影响,先验调过头会让少数类的 precision 崩掉。
4. 从 TF-IDF 到训练贝叶斯模型:可直接复制的最小代码
原理讲清楚了就该动手了。这一章的代码按可运行的标准写,复制到 Jupyter 或脚本里,把语料路径换成自己的就能跑通。
4.1 词袋还是 TF-IDF:微博短文本怎么选
特征抽取这一步,CountVectorizer 和 TfidfVectorizer 最主要的差别在于:词袋只统计词出现的次数,TF-IDF 在词频基础上乘了一个逆文档频率,惩罚那些在大量文本里都出现、没什么区分度的常用词。微博语料里“微博”“转发”“疫情”这种高频噪间词特别多,用 TF-IDF 能自动把它们压下去。短文本场景下我基本直接上 TF-IDF,偶尔遇到语料特别干净、没有明显高频噪音词的情况,词袋的效果跟 TF-IDF 差不多,那就无所谓了。
还有一个必调参数是 ngram_range。默认只有单个词,但中文里“不好”和“不 好”在单字词情况下会被拆开,“不”反而可能被当成负面信号叠加到正面词上。设置 ngram_range=(1,2) 把相邻词对也纳入特征,能部分缓解朴素贝叶斯独立性假设带来的信息缺失。代价是特征维度翻倍,所以要在下一节配合 max_features 做截断。
4.2 用 Pipeline 一条龙串特征和模型
直接写 Pipeline,把 TF-IDF 和朴素贝叶斯放进去。Pipeline 最大的价值是防止数据泄露:fit 的时候向量器只在训练集上学习词表,之后 transform 测试集时复用这个词表,不会让测试集的特征分布提前混进模型。
from sklearn.feature_extraction.text import TfidfVectorizer from sklearn.naive_bayes import MultinomialNB from sklearn.pipeline import Pipeline # 特征 + 模型串成一条流水线 pipeline = Pipeline([ ("tfidf", TfidfVectorizer( max_features=5000, # 最多保留 5000 个特征,短文本词表 5000 足够 min_df=2, # 至少在 2 条文本里出现过才保留 ngram_range=(1, 2), # 保留单个词 + 相邻词对 stop_words=None # 中文停用词在分词阶段已经处理,这里不再重复 )), ("nb", MultinomialNB( alpha=1.0 # 拉普拉斯平滑系数,后续用交叉验证调 )) ])几个参数按我的经验说明一下。max_features=5000 意味着只保留 TF-IDF 值最高的 5000 个特征。微博评论的词表本身不大,去掉低频生僻词后 5000 基本覆盖了绝大多数有效信息,设太大反而引入稀疏噪音。min_df=2 表示某个词至少出现在两条不同文本里才纳入词表,这样能把只出现过一次、可能是打错字的词过滤掉。ngram_range 从默认的 (1,1) 改成 (1,2) 是对贝叶斯独立性假设最简单的补偿办法。
4.3 训练、预测和评估:不要只盯准确率
数据已经经过了清洗和分词,假设你现在有一份 CSV,里面是 text(空格连接已经做好了)和 label(0/1/2)两列。剩下的训练过程代码非常短:
import pandas as pd from sklearn.model_selection import train_test_split from sklearn.metrics import classification_report, confusion_matrix df = pd.read_csv("weibo_clean.csv", encoding="utf-8-sig") X = df["text"].astype(str) y = df["label"].astype(int) # stratify 保证切分后各类别比例和原始数据一致 X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.2, random_state=42, stratify=y ) pipeline.fit(X_train, y_train) y_pred = pipeline.predict(X_test) print(classification_report(y_test, y_pred, target_names=["负面", "正面", "中性"])) print(confusion_matrix(y_test, y_pred))stratify 参数一定要写,尤其在类别不均衡时,没有它随机切分可能把某个小类别全部切到训练集或测试集里,结果完全失真。random_state 固定成 42 是为了复现,调参时不要随意换。
评估指标只看 accuracy 会骗人。假设样本里 60% 是正面,模型全预测正面也有 60% 准确率,但这个模型没有任何价值。正确做法是看 classification_report 里每一类的 precision 和 recall,以及 macro avg 那一行。做舆情监测时最容易漏的是负面评论——如果负面类别的 recall 只有 0.2,意味着八成负面情绪被模型漏掉了,这种模型拿去上线会出事。confusion_matrix 里能直接看到哪些类别之间互相混淆,通常正面和负面之间的混淆最严重,中性样本往两边偏是常态。
4.4 调参经验:四个方向手动试一遍
调参不要盲扫,按下面四个方向来,每一步都看 macro-F1 的变化。
第一是 max_features。从 3000 到 10000 每隔 1000 跑一遍,对短文本来说通常在 5000 附近收敛。如果 F1 一直不涨说明有效特征不够,问题出在清洗或分词,不是调参能解决的。
第二是 alpha。用 GridSearchCV 或者最简单的手动 for 循环,在 [0.1, 0.5, 1.0, 2.0] 里选。alpha 对词表小、样本少的语料影响很大,alpha 太小模型会被生僻词带偏,太大则把区分度磨平。
第三是 ngram_range。改成 (1,2) 后如果 macro-F1 提升还不到 0.5%,就换回 (1,1),因为引入双词组合会让推理时对 OOV 词更敏感,代价大于收益。
第四是和 BernoulliNB 做一次对比。把 Pipeline 里的分类器换掉,跑同样的交叉验证。文本极短、词频分布极不均匀时,BernoulliNB 有时反而更稳。不要凭感觉选,谁在验证集上 F1 高就用谁。
5. 落地避坑清单:5 个让模型翻车的常见原因
贝叶斯算法本身简单,模型出问题大概率不在算法,而在数据链路。下面这五条都是踩过、排查过的真问题,每一条都按现象、原因、解决三个层次记录。
5.1 准确率 0.92,但预测新数据全是一类
现象:训练集分类报告很好看,macro-F1 接近 0.9,拿网上的新评论去预测时,几乎所有结果都落在同一个类别上,明显不符合语感。
原因:最常见的是标签泄漏或切分方式错误。一种典型情况是数据按时间顺序排列,后 20% 作为测试集,而疫情不同阶段的情绪分布差异很大,模型只学到了前期的模式;另一种是在清洗阶段针对某个类别写了特殊处理分支,比如把所有含“加油”的句子强制过滤,等于把标签信息泄漏进了预处理。
解决:用 train_test_split 随机切分且 stratify=y 保证类别比例一致;检查清洗函数里有没有针对特定情感词的硬编码分支;如果数据确实带时间属性,改用时间滑动窗口方式切分训练集和测试集,而不是随机切。
5.2 jieba 把“新冠”切成了“新”和“冠”
现象:打印高频词时发现“新”“冠”“核”“酸”这类单字大量出现,连“核酸检测”这种完整概念都被拆散,模型学不到疫情专有搭配。
原因:jieba 默认词典收录跟不上疫情新词,两个单字组合成新词的频率低于默认词表里单字的词频,分词器就会切错。
解决:维护一份 covid_words.txt 自定义词典,每行一个词,在分词前用 jieba.load_userdict 加载。加载后跑一次 Top 100 高频词,人工扫一遍看还有没有漏网的新词。这个动作每次换语料都要做,新词产生的速度比你想象的快。
5.3 类别不均衡时模型变成“复读机”
现象:语料里正面占 70%,模型在测试集上的 accuracy 看着有 0.75,但看混淆矩阵发现负面类别的 recall 只有 0.2,几乎全被预测成正面或中性。
原因:MultinomialNB 的类别先验是从训练集统计出来的,多数类天然被偏好。这在新闻语料里不明显,但在舆情评论里非常致命,因为负面声音本来就少,还被模型吞掉了。
解决:先看 training set 里各标签的数量分布,如果确实不均衡,手动给 class_prior 设一个合理比例,比如 [0.35, 0.3, 0.35];或者对少数类做简单过采样(复制样本)再做交叉验证。评估时不要看 accuracy,用 macro-F1。舆情场景里更极端一点,甚至会只关心负面的 recall,其他类别的表现可以适当牺牲。
5.4 emoji 和繁体字让特征变成黑匣子
现象:一条明显表达不满的评论,分词后全是“[泪]”“[怒]”这类未登录词,模型输出却判成中性。或者繁体字文本被切得支离破碎,“确诊”变成“確”和“診”两个字,在模型眼里跟另一个词完全不同。
原因:微博评论里表情以特殊编码形式存在,jieba 词典不认识;繁体字在原样状态下分词效果极差,而清洗阶段没有做转码。
解决:在清洗阶段先用 emoji 库把表情转成中文词元,比如“[笑哭]”,分词时它就是一个普通词;繁体字统一用 OpenCC 转成简体,转换要在分词之前。这一步做完,重新跑一遍高频词统计,会发现很多之前消失的有效特征回来了。
5.5 模型保存再加载后预测报错
现象:joblib.dump 保存了模型,放到另一台电脑或另一个 Python 环境里加载后,predict 要么报错,要么结果跟原来完全不同。
原因:一个常见原因是训练环境 sklearn 版本和加载环境不一致,特征顺序对不上;另一个是把 TfidfVectorizer 和模型分开保存,加载时只有模型没有词表,特征空间完全错位。
解决:保存时直接保存完整的 Pipeline 对象而不是拆散保存,Pipeline 里包含训练好的向量器词表,加载之后就可以完整预测。同时记录训练环境的 pip freeze,至少把 sklearn、joblib 的版本记下来。更保险的做法是保存一份测试集的预测结果,加载模型后先跑一遍对比,不一致就说明环境有问题,不要直接上线。
6. 从脚本到能演示的成果:交叉验证、词云与模型持久化
6.1 用交叉验证确认模型稳定性
一次 train_test_split 的结果有运气成分,交叉验证能把这种运气抹平。用五折交叉验证,每折训练一次、评估一次,最后看 macro-F1 的均值和方差。
from sklearn.model_selection import cross_val_score scores = cross_val_score( pipeline, X, y, cv=5, scoring="f1_macro" ) print("每折 F1:", scores) print("均值: {:.3f}, 标准差: {:.3f}".format(scores.mean(), scores.std()))标准差的含义比均值更重要。均值高但标准差大,说明模型在某些划分下表现不稳定,可能是少数类样本太少,或者某些文本质量有问题。标准差在 0.02 以内算稳定,超过 0.05 就需要回去看数据了。这段代码输入是完整数据的 X 和 y,Pipeline 会在内部自动做特征提取和模型训练,不用重复写 fit。
6.2 中文词云与情感占比可视化
模型跑完,结果要能给别人看。词云是最直观的展示方式,但 wordcloud 库默认不支持中文,必须指定中文字体路径,否则画出来全是方块。
import matplotlib.pyplot as plt from wordcloud import WordCloud def build_wordcloud(texts, save_path, font_path="C:/Windows/Fonts/simhei.ttf"): all_text = " ".join(texts) wc = WordCloud( width=800, height=400, background_color="white", font_path=font_path, # Linux 用 /usr/share/fonts/... 下的 Noto Sans CJK max_words=200 ).generate(all_text) plt.imshow(wc, interpolation="bilinear") plt.axis("off") plt.savefig(save_path, dpi=150, bbox_inches="tight")按情感标签分别生成正面词云和负面词云,对比看差异,比单独一张总词云有价值得多。正面词云里出现“感谢、稳稳、有序、按时”,负面词云里出现“排到、深夜、封了、抢不到”,一眼就能讲清楚模型学到的语义模式。
情感占比可以直接用 pandas 统计预测结果的 value_counts,画一张简单的饼图。这个环节对非技术受众汇报特别有用,模型权重和 TF-IDF 别人不关心,但“这周负面评论占比 12%、较上周下降了 3 个百分点”是能直接做决策的信息。
6.3 保存模型与日常预测脚本
模型训练好之后,用 joblib 保存整个 Pipeline,下次启动时直接加载,不需要重新训练。
import joblib # pipeline 是上一章训练好的完整 Pipeline 对象 joblib.dump(pipeline, "nb_emotion_model.joblib")预测新文本时,写一个独立脚本,按训练时完全相同的顺序走清洗、分词、预测三步。
import joblib import jieba model = joblib.load("nb_emotion_model.joblib") def predict_one(text): # 清洗顺序要和训练时完全一致 cleaned = clean_weibo(text) cleaned = normalize_emoji(cleaned) words = cut_words(cleaned) # 向量器期望输入是空格连接的字符串 probs = model.predict_proba([" ".join(words)])[0] label = model.predict([" ".join(words)])[0] return label, probs这里最容易出错的是预测脚本漏掉某个清洗步骤。比如训练时做了 emoji 转换,预测脚本没做,那么“😂”在预测时会被分词器切开,特征对不上。我的习惯是在 predict_one 里显式调用训练时用过的所有清洗函数,并且打印出清洗后的文本和预测概率,方便排查。
6.4 更进一步:把模型包成一个可以点开的小页面
模型调通之后,如果还想让不懂技术的人也能用,常见做法是用 Streamlit 写一个最小交互页面,输入一条微博短文本,点按钮输出情感类别和概率。这个页面十几行代码就能立起来,适合团队内部演示和标注时的辅助工具。
import streamlit as st import joblib st.title("微博评论情感分析") model = joblib.load("nb_emotion_model.joblib") text = st.text_area("粘贴一条微博评论") if st.button("分析"): label, probs = predict_one(text) st.write("预测结果:", ["负面", "正面", "中性"][label]) st.write("概率分布:", probs)贝叶斯模型在这类简单工具上的优势特别明显:推理速度毫秒级,没有 GPU 依赖,一台内网服务器就能跑。如果后面要接入实时数据流,更新模型也很简单——贝叶斯是增量友好的,新语料进来重新 fit 一次的成本很低,不需要像深度学习那样重新跑几十个 epoch。
我自己在第一版项目里犯过的错是爬虫阶段没有过滤“转发微博”开头的重复文本,导致“转发”成了训练集里权重最高的特征,模型一度把所有评论都判成中性。后来清洗规则里加了一步对重复文本的过滤,指标立刻恢复正常。做这类文本挖掘项目,顺序永远是清洗先行、模型殿后,把数据规则写完整比调任何一个超参数都重要。希望这份完整的落地路径能帮你少踩几个我的老坑。
本文还有配套的精品资源,点击获取