☰
朴素贝叶斯实现情感文本分类:源码解析与实战避坑指南
2026/9/25 6:10:42 网站建设 项目流程

简介:面向计算机相关专业学生与从业者,基于朴素贝叶斯算法的情感文本分析与分类项目源码及数据集,是期末大作业的完整方案。项目以微博短文本情感分类为核心,利用预训练词向量完成文本向量化,配合朴素贝叶斯分类器实现训练与预测,覆盖数据清洗、特征构建、模型评估等环节,代码结构清晰,可作为自然语言处理课程设计的参考模板。压缩包共12个文件,约173MB,包含两个Python脚本、两个CSV情感数据集、微博预训练词向量压缩包,以及PyCharm工程配置文件,导入开发环境即可直接调试。项目评审分达95分以上,所有模块经严格调试可运行,能帮助掌握朴素贝叶斯在真实文本数据上的建模流程,理解从数据处理到分类结果输出的完整链路。已有1025人学习下载,适合需完成情感分析类课程设计或系统提升机器学习实践能力的学生。

1. 基于朴素贝叶斯机器学习算法实现情感文本分析与分类源码+数据集:这份 zip 到底该怎么用

如果刚下载了这份《基于朴素贝叶斯机器学习算法实现情感文本分析与分类源码+数据集(高分大作业).zip》,别急着把里面所有文件读一遍。它做的事情其实很集中:给一段中文文本,判断它是正面还是负面情绪,数据清洗、分词、特征提取、朴素贝叶斯训练和评估,全用 Python 串起来了,数据集也打包在 zip 里。对机器学习入门阶段的同学来说,它是理解监督学习闭环的现成样本;对要交期末大作业的人来说,它最省事的地方是省掉了到处找数据集这一步——毕竟免费 python 源码很多,能直接跑通的不多。下面我会从原理、跑通步骤、换数据、避坑到验证,把这个项目真正吃透。

2. 朴素贝叶斯凭什么适合做情感分类:从公式到词袋,再到参数选型

情感文本分析是文本分类里最好落地的一个方向:电商评论、微博短评、影评,都是“一句话判好坏”的问题。很多黑马朴素贝叶斯案例拿它当入门模型,不是因为它在所有数据集上准确率最高,而是因为它训练快、可解释性强,而且在短文本情感分类这种高维稀疏场景下,效果并不比复杂模型差太多。我自己的体会是:用 CountVectorizer 把几万条评论转成词频矩阵,再训练一个多项式朴素贝叶斯,普通笔记本十几秒就完事。

2.1 贝叶斯公式在情感分析里的实际含义:先验、似然和后验

要判断一句话是正面还是负面,模型实际算的是 P(正面|这句话) 和 P(负面|这句话) 哪个更大。由贝叶斯公式,P(类别|文本)=P(文本|类别)P(类别)/P(文本)。对同一个句子,分母 P(文本) 是常数,所以只需要比较分子的相对大小。P(类别) 是先验概率,也就是训练集里正面/负面的占比;P(文本|类别) 是似然,表示在已知类别时这段文本出现的概率。

朴素贝叶斯的“朴素”,在于假设文本里的词在给定类别下是相互独立的。于是似然可以拆成每个词概率的连乘:P(文本|类别)=Π P(w_i|类别)。现实中这个假设显然不成立,比如“不”和“好吃”之间有强依赖,但文本分类任务并不需要精确估计概率,只要类别间的相对大小别错就行。短文本每个句子只有几十个字,词与词之间的共现关系被大量训练样本平均掉以后,误差往往互相抵消。这正是它几十年来在垃圾邮件识别和情感分析里还没被淘汰的原因。

训练朴素贝叶斯的过程很简单:遍历训练集,统计每个类别下每个词出现的次数,除以该类别总词数,得到条件概率;再统计每个类别的样本占比,得到先验概率。预测时把 log 先验概率加上每个词的 log 条件概率,取最大者作为预测类别。取 log 是为了防止连乘几千个小数后下溢成 0。sklearn 的 predict_proba 内部也是这么计算的,所以输出很稳定。

有一个值得说的点:从机器学习处理任务的角度看,情感分类属于监督学习里的分类任务;朴素贝叶斯是生成式模型,它学的是联合分布 P(文本,类别),而不是像逻辑回归那样直接拟合 P(类别|文本)。生成式模型的优点是小样本时收敛快,缺点是特征相关性完全没建模。周志华《机器学习》里对朴素贝叶斯的评价是“简单、高效、可解释”,用在情感分析这种词特征稀疏的任务上,恰好扬长避短。

2.2 中文文本怎么变成机器能读的数字:分词、停用词与词频矩阵

模型不认识汉字,需要把每句话变成固定长度的特征向量。最常见做法是:先对中文分词,再去掉停用词,然后用 CountVectorizer 或 TfidfVectorizer 统计词频。下面的代码演示用 jieba 分词后,再用 CountVectorizer 构造词频矩阵:

import jieba from sklearn.feature_extraction.text import CountVectorizer sentences = [ "这家店的东西很划算,服务态度也好", "太难吃了,再也不来了", ] # 先分词再空格连接,方便调试时查看中间结果 tokenized = [" ".join(jieba.lcut(s)) for s in sentences] print(tokenized) # 输出类似:['这家 店 的 东西 很 划算 , 服务态度 也 好', '太 难吃 了 , 再也 不 来 了'] # 直接指定 tokenizer,让向量化器内部调用 jieba 分词 vec = CountVectorizer(tokenizer=jieba.lcut, min_df=1, max_features=5000) X = vec.fit_transform(sentences) print(X.shape) # (2, 特征数) print(vec.get_feature_names_out()[:10])

这里要解释两个参数。min_df=1 表示某个词至少在一条样本里出现过才保留,写 1 其实就是全保留;写成 5 则可以去掉只在极少数评论里出现的词,减小噪声。max_features=5000 是只取词频最高的 5000 个词,这是控制维度和内存最直接的旋钮,10 万条评论的原始词表可能到十几万,全留下不仅慢,很多生僻词对分类也没帮助。

停用词一般单独维护一个文本文件,加载后传给 stop_words 参数,比如“的”“了”“吗”。注意,情感分析里不要把所有标点都无脑删掉,感叹号“!”在某些场景下是有情感倾向的。常见做法是先分词,再用停用词表过滤,最后再做向量化。顺序反了会导致词表里有大量“的”这类无意义特征,模型会被稀释。

2.3 多项式朴素贝叶斯 vs 伯努利朴素贝叶斯:选型理由

同样是贝叶斯分类器,sklearn 里有两个常用变体。MultinomialNB 适合特征取值为非负计数的场景,比如词频或 TF-IDF;BernoulliNB 则把特征看成 0/1 二值,只关心词出现没有。对情感文本分析,我一般优先选 MultinomialNB,因为“好吃”出现 3 次和出现 1 次,情感强度显然不同,频次是有信息量的。Bernoulli 更适合朴素贝叶斯垃圾邮件分类那种“提到某个词就大概率是垃圾”的场景,或者文本特别短、频次不稳定时。

模型特征含义适合场景
MultinomialNB词频 / TF-IDF,词出现次数有区分度电商评论、影评、长短不一的文本
BernoulliNB0/1 出现与否,不关心次数短文本、垃圾邮件检测、字典特征

选型可以拍脑袋,也可以量化:用同样的词频矩阵,分别跑一遍 MultinomialNB 和 BernoulliNB,在验证集上对比 F1。文本情感分析数据集通常不大,交叉验证成本很低,不要怕麻烦。

还有一个关键参数——拉普拉斯平滑 alpha。它的作用是给从未出现的词一个非零概率,否则某个测试词在训练集的某类里没出现过,连乘结果直接变 0。alpha 默认是 1.0,也就是分子加 1、分母加词汇表大小。调大 alpha 会让概率估计更均匀,对噪声鲁棒;调小 alpha 则更依赖原始统计,但过拟合风险更高。我一般先在 alpha=1.0 跑一遍,如果验证集方差大,再把 alpha 往 0.1 或 5 方向试。

此外,fit_prior 默认 True,表示让模型从数据里学先验 P(类别);如果 False,则认为两类先验各 0.5。class_prior 则允许手动设置先验。当训练集类别很不平衡时,这两个参数会直接影响预测偏向,后面避坑章节再细说。

3. 把 zip 里的项目跑起来:目录结构、依赖与第一次训练

拿到这样一个 zip,不建议先完整读源码。正确流程是:先看目录结构,了解哪些是入口、哪些是数据;再安装依赖,跑通训练脚本;最后根据报错和输出逐行理解。不然很容易陷入“每个文件都像在读天书”的状态。

3.1 先看目录,识别核心文件

这类大作业包解压后通常包含几个固定角色:一个或两个入口脚本、一个放算法实现的模块、一个数据加载模块、一个数据集文件夹,可能还有 requirements.txt。用命令展开目录会更直观:

unzip 基于朴素贝叶斯机器学习算法实现情感文本分析与分类源码+数据集(高分大作业).zip -d sentiment_project cd sentiment_project tree -L 2

如果系统没有 tree,用find . -maxdepth 2 -type f也能看到平铺结构。文件名里带中文和括号,shell 里记得加上引号,否则会被解释成通配符或命令分隔符。

看到结构后,按优先级读这样几个文件:首先是记录依赖的 requirements.txt 或 import 列表;其次是数据加载文件,确认数据集列名;然后是训练入口,确认模型用什么特征;最后才是算法实现。很多大作业源码里的朴素贝叶斯是手写的,比如用字典统计词频,不依赖 sklearn。手写实现的训练循环反而更好懂,但边界处理容易出 bug,后面避坑章节会提到。

3.2 安装依赖与加载数据

依赖一般不会超过这几个包:jieba、scikit-learn、pandas、joblib。有 requirements.txt 就直接:

pip install -r requirements.txt

没有的话自己装一份,版本不用死磕,sklearn 1.x 都兼容。

然后打开数据文件,确认标签是什么形式:

import pandas as pd df = pd.read_csv('data/comments.csv', encoding='utf-8-sig') print(df.head()) print(df['label'].value_counts())

常见的大作业数据是一个 csv,df 里至少两列:一列是文本原文,列名可能是 text、review、comment;一列是标签,列名可能是 label、sentiment、tag。标签有 0/1 数字,也有“好评/差评”字符串。读进来第一件事就是 print 前几行和 value_counts,确认没有空值和类别全部跑偏。

3.3 最小训练脚本:从词频矩阵到朴素贝叶斯模型

如果源码里已经封装好了训练函数,直接调入口就行。但为了让你能独立复现,我给你一个最小可运行脚本,它等价于这类大作业的核心训练流程:

import pandas as pd import jieba from sklearn.feature_extraction.text import CountVectorizer from sklearn.model_selection import train_test_split from sklearn.naive_bayes import MultinomialNB df = pd.read_csv('data/comments.csv', encoding='utf-8-sig') df = df[['text', 'label']].dropna() df['label'] = df['label'].astype(int) X_train, X_test, y_train, y_test = train_test_split( df['text'], df['label'], test_size=0.2, random_state=42, stratify=df['label'] ) vec = CountVectorizer(tokenizer=jieba.lcut, max_features=5000) X_train_vec = vec.fit_transform(X_train) X_test_vec = vec.transform(X_test) model = MultinomialNB(alpha=1.0) model.fit(X_train_vec, y_train) print('准确率: %.4f' % model.score(X_test_vec, y_test))

这里有三个要点。第一,必须先切分训练集/测试集,再对训练集 fit_transform,测试集只 transform,否则向量化的词表会用到测试集信息,属于数据泄漏。第二,vec.transform(X_test) 用的是训练集学会的词典,所以测试集里出现训练集没见过的新词会被直接忽略,这是正常现象。第三,stratify=df['label'] 会让切分后的训练集和测试集保持和原数据集相同的类别比例,避免切完以后测试集里只有一个类别。

跑通后,可以把 CountVectorizer 换成 TfidfVectorizer 试试,只用改 import 和构造函数,本质上对 MultinomialNB 也适用,因为 TF-IDF 值是非负的。大作业里常常用这个作为“改进点”。

3.4 保存模型与向量化器:避免每次跑都重新训练

训练结束后,只把模型参数打印出来是不够的,占分点在于能不能直接加载新样本预测。用 joblib 把两个对象一起存下来:

import joblib joblib.dump(vec, 'models/vectorizer.joblib') joblib.dump(model, 'models/naive_bayes.joblib')

预测新样本时:

loaded_vec = joblib.load('models/vectorizer.joblib') loaded_model = joblib.load('models/naive_bayes.joblib') new_texts = ["物流很快,客服态度也特别热情", "等了三天才发货,差评"] new_vec = loaded_vec.transform(new_texts) probs = loaded_model.predict_proba(new_vec) for text, prob in zip(new_texts, probs): print(text, prob)

保存时一定要把 vec 和 model 同时保存,因为加载新样本时 transform 要依赖同一个词典,只保存模型不保存 vec,预测时就得重新拟合词典,词表对不上,结果完全不可用。这是我在实际调试里被坑过好几次的地方。另外要确保 models 目录存在,joblib 不会自动创建目录。

4. 把数据集换成自己的:清洗、打标签与训练集划分

大作业答辩时,老师大概率会问:“你这个模型如果换成微博评论或者商品评价,还能用吗?”所以别只守着 zip 里自带的数据集,动手换一套数据,整个项目的完成度会立刻上一个台阶。甚至很多课程的优秀作业,就是基于 Python 的短文本舆情情感倾向分析系统出现的,无外乎是数据换成了自己采集的评论,微调了标签体系。

4.1 标签体系决定后处理:二分类还是三分类

情感文本分析的常见标签体系有两种:二分类(正面/负面)和三分类(正面/负面/中性)。大作业一般建议先做二分类,因为中性样本的主观性很强,不同人标注结果可能完全相反,会让朴素贝叶斯的条件概率统计变得混乱。如果老师的数据源里自带“中性”,可以先把中性样本去掉,或者单独作为第三类训练,但不强求准确率。

标签用什么符号呢?sklearn 的 MultinomialNB 支持字符串标签,所以即使 label 列是“正/负”也可以直接 fit。但后面画混淆矩阵、算 recall 时,字符串标签容易排序不稳定,我一般统一映射成 0 和 1:0 表示负面,1 表示正面。这样 predict_proba 输出的第 1 列是负面概率,第 2 列是正面概率。

4.2 用 pandas 做文本清洗:去掉 URL、数字和无意义符号

自己爬的数据往往脏得离谱:夹杂 URL、表情符号、@ 用户名、数字、乱码。清洗规则不需要很复杂,以下代码覆盖大多数场景:

import pandas as pd import re df = pd.read_csv('raw_comments.csv') print(df.columns) def clean_text(text): text = str(text) text = re.sub(r'https?://\S+', '', text) # 去 URL text = re.sub(r'@\w+', '', text) # 去 @ 用户名 text = re.sub(r'\d+', '', text) # 去数字 text = re.sub(r'[^\u4e00-\u9fa5a-zA-Z]', ' ', text) # 保留中文和英文字母 return text.strip() df['text'] = df['content'].apply(clean_text) df['label'] = df['sentiment'].map({'正面': 1, '负面': 0}) df = df.dropna(subset=['text', 'label']) df.to_csv('data/custom_comments.csv', index=False, encoding='utf-8-sig')

正则最后一行把所有不是中文、不是英文字母的字符替换成空格,手机号、表情、emoji 都会被剔除。注意,表情符号在微博评论里往往有情感信息,比如“太开心了😄”,如果你发现表情对结果影响明显,可以把 emoji 单独转成特征词,例如用 emot 库把😄映射为 happy_face,再把其余字符清洗。大作业做到这个程度,已经是加分项。

保存 csv 时用 encoding='utf-8-sig',是因为 Windows 上的 Excel 只认带 BOM 的 utf-8;pandas 再读这个文件时用 utf-8-sig 也能正常解析。

4.3 训练集/测试集划分:分层抽样与随机种子

清洗完的数据直接喂给训练脚本还不够,一定要先划分。划分的代码在上一章已经出现过,但换数据集后有两个细节要特别留意。

第一个细节是 stratify。如果不带 stratify,切分后可能出现测试集中 90% 都是负面,正面只剩几十条,准确率显示 90%,其实模型没学会正面判断。带上 stratify 可以保证训练和测试集里的类别比例与原数据一致:

from sklearn.model_selection import train_test_split train_df, test_df = train_test_split( df, test_size=0.2, random_state=42, stratify=df['label'] ) print(train_df['label'].value_counts(normalize=True)) print(test_df['label'].value_counts(normalize=True))

第二个细节是 random_state。random_state=42 不是玄学,它让每次运行切分结果完全相同,保证你调参前后的准确率可比。自己实验时想换一种随机切分,就改成其他整数。

如果类别不平衡到少数类只占 10%,最好先做重采样再切分。对文本特征不要用 SMOTE 这类连续值插值方法,朴素贝叶斯用的是离散词频,SMOTE 生成的不是整数特征,没有意义。常见做法是直接复制少数类样本:

from sklearn.utils import resample df_major = df[df['label'] == 0] df_minor = df[df['label'] == 1] df_minor_upsampled = resample( df_minor, replace=True, n_samples=len(df_major), random_state=42 ) df_balanced = pd.concat([df_major, df_minor_upsampled])

这样处理后,先验概率 P(正面)≈P(负面),模型不会天生偏向多数类。但要记住,复制样本会让训练集变大,并且模型对少数类的过拟合风险上升,训练完成后再用原始分布测试集评估,不要用平衡后的测试集。我自己常用的顺序是:先选样本量最小的一类,按它的数量做欠采样,或者不做平衡,直接在分类报告里看 F1。

5. 避坑指南:朴素贝叶斯情感文本分析常见的 5 个翻车现场

前面几章把流程走通了,但这套代码在真实数据上会遇到的坑,远多于模型本身。我把这些翻车点集中整理成 5 条,每一条都是实际运行时报错过、花过时间排查过的。

5.1 编码和分词:两个最常见的第一道坎

  • 现象:pandas 读 csv 直接抛 UnicodeDecodeError,或者训练出来的模型预测结果全是同一个标签。
  • 原因:数据文件不是 UTF-8,而是 GBK 或者带 BOM 的 UTF-8。
  • 解决:读文件时不要省 encoding,直接用 utf-8-sig 或 gbk 试。如果文件很大,可以先用二进制读取前两个字节探测:
with open('data.csv', 'rb') as f: head = f.read(2) print(head) # b'\xff\xfe' => UTF-16 # b'\xef\xbb\xbf' => UTF-8 with BOM
  • 现象:CountVectorizer 出来的特征不是词,而是一整个句子串,比如特征名是“这家店的东西很划算”。

  • 原因:CountVectorizer 默认按空白字符把字符串切成 token,中文句子如果没先用空格连接,会被当成一个连续 token。

  • 解决:要么构造时传 tokenizer=jieba.lcut,要么在送入向量化器之前用空格把分词结果 join 起来。

  • 现象:分词后特征里全是单字,比如“店”“了”,准确率惨不忍睹。

  • 原因:有些源码里用 jieba.cut 但没装词表,或者把 jieba 分词结果过滤得太狠,把双字词全部拆开。

  • 解决:先打印 jieba.lcut 对典型句子的切分结果;再确认停用词表没有把“好吃”这类有情感倾向的词误删。

5.2 数据划分与评估:准确率骗人,问题在后面

  • 现象:训练集准确率 99%,测试集只有 80%,自己写两句预测还经常翻车。

  • 原因:最常见的不是模型过拟合,而是数据泄漏。很多人先对全量文本做了 vec.fit_transform,再 train_test_split,测试集信息已经参与了词表构建和词频统计,模型的“好成绩”是夹带私货的结果。

  • 解决:永远先 split,再对训练集 fit_transform,测试集只 transform。如果是从网上抄的代码,看到 .fit_transform(X) 之后再切分,基本就是这个坑。

  • 现象:准确率挺高,但混淆矩阵里有一整类几乎全错。

  • 原因:类别不平衡,模型先验概率过度偏向大类别。

  • 解决:打印 value_counts 看分布;用上一章的 resample 做上采样;评估不要只报准确率,报 classification_report 里的 F1。注意训练时不要给 MultinomialNB 传 class_weight,sklearn 的这个类没有该参数,传了会直接报错。

  • 现象:设置 alpha=0 后模型报错或预测结果出现 NaN 概率。

  • 原因:测试样本里出现了训练集中某个类别从未见过的词,条件概率为 0,再往下传就变成 NaN。

  • 解决:alpha 不要设 0,拉普拉斯平滑的意义就是兜底。alpha=0.01 可以在低频词上保留一点概率,但对短文本来说,alpha=1.0 是默认安全选择。

  • 现象:标签列是字符串 '0'/'1',训练没问题,画混淆矩阵时报 labels 参数错误。

  • 原因:y 的类型不统一,或者类别编码不一致。

  • 解决:在训练前统一 astype(int),并检查 df['label'].unique()。也可以用 LabelEncoder,但大作业里没必要,0/1 映射最简单。

6. 进阶验证:用混淆矩阵和错误样本回看,让大作业从“能跑”变成“能答”

准确率只是一个开始。很多同学训练完看到 0.87 就当作完工,老师一问模型缺点就答不上来。我自己写情感分析大作业时养成的习惯是,训练完立刻做两件事:打印分类报告,回看预测错误样本。这两步能让你对模型的边界看得非常清楚。

from sklearn.metrics import classification_report, confusion_matrix import numpy as np pred = model.predict(X_test_vec) print(classification_report(y_test, pred, target_names=['负面', '正面'])) print(confusion_matrix(y_test, pred)) wrong_idx = np.where(y_test != pred)[0][:10] for idx in wrong_idx: print('原文:', X_test.iloc[idx]) print('真实:', y_test.iloc[idx], '预测:', pred[idx]) print('---')

看错误样本通常会发现两类典型问题:一是反讽,比如“真是谢谢您嘞”,字面没有负面词,但人是负面态度;二是否定词与情感词搭配,比如“不大好吃”,“好吃”作为正面特征贡献了很大的似然,朴素贝叶斯无法感知“不”对它的否定。答辩时能主动说出这些例子,比把准确率从 88% 调到 89% 更能得分。

还有一个具体技巧,调整分类阈值。MultinomialNB 的 predict 默认取后验概率最大的一类,但如果你更关心某类召回率,可以直接操作预测概率:

prob_pos = model.predict_proba(X_test_vec)[:, 1] y_new = (prob_pos > 0.6).astype(int)

这相当于把正面类别的判定门槛抬高,只有非常像正面的才判为正,负面召回率会下降但精度可能上升;反过来把阈值降到 0.4 则更激进。这段代码写入大作业,可以体现你对“分类不是非黑即白”的理解。

我的习惯是:先跑一版默认参数存底,再调一版带阈值调整的,最后把两次的 precision/recall 对比写进实验结论。这种做法不需要额外依赖,又显得项目有迭代过程。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询