简介:本资源为面向计算机、人工智能及相关专业学生的课程实验资料包,围绕朴素贝叶斯分类器与AODE分类器的Python实现展开,适合在校学生完成课程设计、作业或毕设初期立项,也可供教师与企业员工参考学习。压缩包共26个文件,约256KB,以10个py源码文件为核心,辅以6张png实验图表、4个npy与3个npz数据文件,以及2个md说明文档和1个txt依赖清单,覆盖从算法实现到结果可视化的完整流程。内容涉及朴素贝叶斯、AODE、RBF、BP、K-means、LVQ、MoG等多种经典机器学习算法,并配有MNIST、lung、yale等数据集的实验脚本与准确率、NMI等指标曲线图,便于读者理解模型训练与评估过程。目前已有373人学习下载,代码均经测试可运行,读者可在此基础上修改扩展,快速完成实验报告或二次开发。
1. 课程实验里手撸朴素贝叶斯:为什么它仍是分类入门的第一选择
如果你正在做课程实验,要求基于 Python 实现一个朴素贝叶斯分类器,并且需要提交源码和项目说明,那这篇笔记就是为你写的。我带过几届学生的课程设计,也审过不少企业新人的练手项目,发现一个反直觉的现象:很多人宁愿花三天调一个神经网络的超参数,也不愿意花三小时把朴素贝叶斯的概率推导和工程实现吃透。结果就是,模型跑通了,但问他“为什么拉普拉斯平滑要加在分子和分母上”“连续特征的高斯假设到底怎么落到代码里”,答不上来。朴素贝叶斯分类器的源码其实很短,核心逻辑不到一百行,但它是理解生成式模型、贝叶斯决策和特征条件独立假设的最佳入口。这个实验适合两类人:一是正在上机器学习或数据挖掘课程、需要交课程设计的学生;二是想从调包侠往回走一步、搞清楚分类器内部到底在算什么的新手。你不需要 GPU,不需要复杂环境,一台装了 Python 的普通笔记本就能跑完整个流程。接下来我会按“先讲清楚原理和选型理由,再给可复现的代码和参数说明,最后说踩过的坑”这个顺序,把这件事讲透。
2. 朴素贝叶斯分类器的三种变体与选型逻辑
2.1 高斯、多项式、伯努利:你的数据该用哪一个
朴素贝叶斯不是单一算法,而是一族算法的统称,核心区别在于对条件概率 ( P(x_i | y) ) 的建模方式不同。课程实验里最常见的是三种:高斯朴素贝叶斯(GaussianNB)、多项式朴素贝叶斯(MultinomialNB)和伯努利朴素贝叶斯(BernoulliNB)。选错变体,准确率可能直接掉十几个百分点,这不是玄学,是假设与数据分布不匹配导致的。
高斯朴素贝叶斯假设每个特征在给定类别下服从正态分布,适合特征是连续值且大致呈钟形分布的场景,比如鸢尾花数据集的花瓣长度、身高、体重这类测量值。它的条件概率计算依赖均值和方差,训练时就是按类别统计每个特征的均值和方差。
多项式朴素贝叶斯假设特征服从多项分布,典型应用是文本分类中的词频特征。它关心的是“某个词在某个类别下出现了多少次”,而不是“有没有出现”。如果你把 TF-IDF 或词频矩阵喂给它,效果通常不错。
伯努利朴素贝叶斯则假设特征是二值的,只关心“出现”或“不出现”,适合短文本分类或特征本身就是 0/1 的场景。它对未出现的词也会显式建模,这在长文本里反而可能引入噪声。
选型逻辑可以总结成一句话:连续特征用高斯,计数特征用多项式,二值特征用伯努利。课程实验里如果老师没指定数据集,我一般建议用鸢尾花数据集配高斯朴素贝叶斯,因为特征连续、类别平衡、样本量小,跑得快且结果直观;如果想做文本分类,就用 20 新闻组数据集配多项式朴素贝叶斯,能顺便练一下文本向量化。
2.2 条件独立假设:它错得离谱,但为什么还能用
朴素贝叶斯最被诟病的就是“特征条件独立假设”——它假设在给定类别的情况下,各个特征之间互不影响。现实中这个假设几乎从来不成立。比如文本分类里,“机器”和“学习”这两个词同时出现的概率远高于它们独立出现的概率乘积。但有意思的是,即使假设明显错误,朴素贝叶斯在很多任务上仍然表现不错,尤其是样本量不大、特征维度高的场景。
原因有两层。第一层是分类决策只关心后验概率的相对大小,不关心绝对概率值。只要正确类别的后验概率比其他类别大,分类就正确,哪怕概率估计本身有偏差。第二层是特征之间的依赖关系如果在各个类别中表现相似,那么在做类别比较时,这些依赖带来的偏差会相互抵消。这也是为什么朴素贝叶斯常被称为“朴素但有效”。
在课程实验的源码实现里,你不需要去纠正这个假设,但项目说明里最好写清楚:本实现基于条件独立假设,适用于特征冗余度不高的场景,如果特征间强相关,可以考虑半朴素贝叶斯或贝叶斯网络。这样老师一看就知道你理解边界在哪。
2.3 拉普拉斯平滑:一个参数避免零概率灾难
假设你在做文本分类,训练集里“比特币”这个词只在“科技”类别中出现过,在“体育”类别中一次都没出现。那么当测试集里出现“比特币”时,多项式朴素贝叶斯计算 ( P(\text{比特币}|\text{体育}) ) 会得到 0。由于是连乘,整个后验概率直接变成 0,模型完全无法处理这个样本。这就是零概率问题。
拉普拉斯平滑(Laplace Smoothing)解决这个问题,做法是给每个特征的计数加一个小的正数 ( \alpha ),通常取 1。公式从 ( P(x_i|y) = \frac{N_{yi}}{N_y} ) 变成 ( P(x_i|y) = \frac{N_{yi} + \alpha}{N_y + \alpha \cdot n} ),其中 ( n ) 是特征取值个数。分子加 ( \alpha ) 避免零,分母加 ( \alpha \cdot n ) 保证概率和为 1。
在 sklearn 里,alpha参数默认是 1.0,对应拉普拉斯平滑;如果设成小于 1 的值,比如 0.1,叫利德斯通平滑(Lidstone Smoothing),适合特征空间很大、需要更弱平滑的场景。课程实验里我一般先用默认值跑基线,再尝试alpha在[0.01, 0.1, 0.5, 1.0, 2.0]里调一下,看验证集准确率变化。注意,高斯朴素贝叶斯没有alpha参数,它的平滑体现在方差估计时加一个极小值var_smoothing,防止方差为零导致除零错误。
3. 从零实现高斯朴素贝叶斯:源码逐段拆解与参数说明
3.1 数据准备与按类别统计均值方差
先导入必要的库并加载鸢尾花数据集。这个数据集有 150 个样本,4 个连续特征,3 个类别,每个类别 50 个样本,非常适合验证实现是否正确。
import numpy as np from sklearn.datasets import load_iris from sklearn.model_selection import train_test_split # 加载数据 iris = load_iris() X, y = iris.data, iris.target # 划分训练集和测试集,固定随机种子保证可复现 X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.3, random_state=42, stratify=y ) print(f"训练集样本数: {X_train.shape[0]}, 测试集样本数: {X_test.shape[0]}") print(f"特征维度: {X_train.shape[1]}, 类别数: {len(np.unique(y_train))}")这段代码做了三件事:加载数据、按 7:3 划分、打印基本信息。stratify=y保证训练集和测试集的类别比例与原始数据一致,避免某个类别在测试集中缺失。random_state=42是固定随机种子,保证每次运行划分结果相同,方便调试。
接下来按类别统计每个特征的均值和方差。这是高斯朴素贝叶斯训练阶段的核心。
class GaussianNBFromScratch: def __init__(self, var_smoothing=1e-9): self.var_smoothing = var_smoothing self.classes = None self.means = {} self.vars = {} self.priors = {} def fit(self, X, y): self.classes = np.unique(y) n_features = X.shape[1] for c in self.classes: X_c = X[y == c] self.means[c] = np.mean(X_c, axis=0) # 方差加平滑项,防止为零 self.vars[c] = np.var(X_c, axis=0) + self.var_smoothing self.priors[c] = X_c.shape[0] / X.shape[0] return selffit方法遍历每个类别,取出该类别下的所有样本,计算每个特征的均值和方差。var_smoothing默认设成1e-9,加到方差上避免除零。先验概率priors就是类别频率,鸢尾花三个类别各占三分之一。这里没有做任何特征缩放,因为高斯朴素贝叶斯对尺度不敏感——它按特征独立建模,每个特征有自己的均值和方差。
3.2 对数后验概率计算与预测函数
预测阶段需要计算每个类别的对数后验概率,取最大值对应的类别。用对数是为了避免多个小概率连乘导致数值下溢。
def _log_likelihood(self, X, c): mean = self.means[c] var = self.vars[c] # 高斯分布对数概率密度 return -0.5 * np.sum(np.log(2 * np.pi * var)) \ -0.5 * np.sum((X - mean) ** 2 / var, axis=1) def predict(self, X): log_probs = [] for c in self.classes: log_prior = np.log(self.priors[c]) log_lik = self._log_likelihood(X, c) log_probs.append(log_prior + log_lik) # 按列取最大值的索引,映射回类别标签 log_probs = np.array(log_probs).T return self.classes[np.argmax(log_probs, axis=1)]_log_likelihood里第一项是高斯分布对数归一化常数,第二项是指数部分的对数。注意np.sum(..., axis=1)是对每个样本的所有特征求和,因为条件独立假设下联合概率是各特征概率的乘积,取对数后变成求和。predict把先验对数概率和似然对数概率相加,得到每个类别的得分,然后取最大值的索引。np.argmax(log_probs, axis=1)返回每个样本得分最高的类别索引,再用self.classes映射回原始标签。
跑一下看看准确率:
model = GaussianNBFromScratch() model.fit(X_train, y_train) y_pred = model.predict(X_test) accuracy = np.mean(y_pred == y_test) print(f"手写高斯朴素贝叶斯测试集准确率: {accuracy:.4f}")在鸢尾花数据集上,这个简单实现通常能拿到 0.93 到 0.97 的准确率,和 sklearn 的GaussianNB几乎一致。如果差太多,检查一下方差平滑项和求和轴是否正确。
3.3 与 sklearn 版本对比验证
为了确认手写实现没有逻辑错误,用 sklearn 的GaussianNB跑同样的数据,对比准确率和预测结果。
from sklearn.naive_bayes import GaussianNB sk_model = GaussianNB() sk_model.fit(X_train, y_train) sk_pred = sk_model.predict(X_test) sk_accuracy = np.mean(sk_pred == y_test) print(f"sklearn 高斯朴素贝叶斯准确率: {sk_accuracy:.4f}") print(f"两者预测一致的样本数: {np.sum(y_pred == sk_pred)} / {len(y_test)}")如果手写版本和 sklearn 版本准确率差距在 1% 以内,且预测一致的样本数超过 95%,说明实现基本正确。差异可能来自方差平滑项的默认值不同——sklearn 的var_smoothing默认是1e-9,但它是相对于全局最大方差的比例,而手写版本是直接加绝对值。课程实验里不必追求完全一致,但项目说明里要写清楚这个差异。
4. 多项式朴素贝叶斯做文本分类:词频矩阵与平滑参数调优
4.1 文本向量化:CountVectorizer 的关键参数
多项式朴素贝叶斯处理文本时,第一步是把文本转成词频矩阵。用 sklearn 的CountVectorizer可以快速完成,但有几个参数直接影响模型效果。
from sklearn.feature_extraction.text import CountVectorizer from sklearn.naive_bayes import MultinomialNB from sklearn.datasets import fetch_20newsgroups from sklearn.metrics import classification_report # 加载部分新闻组数据,只取四个类别加快速度 categories = ['alt.atheism', 'comp.graphics', 'sci.med', 'soc.religion.christian'] newsgroups_train = fetch_20newsgroups(subset='train', categories=categories, random_state=42) newsgroups_test = fetch_20newsgroups(subset='test', categories=categories, random_state=42) # 向量化:去掉英文停用词,限制最大特征数 vectorizer = CountVectorizer(stop_words='english', max_features=5000) X_train_counts = vectorizer.fit_transform(newsgroups_train.data) X_test_counts = vectorizer.transform(newsgroups_test.data) print(f"训练集词频矩阵形状: {X_train_counts.shape}") print(f"测试集词频矩阵形状: {X_test_counts.shape}")stop_words='english'去掉 the、a、is 这类高频但无区分度的词。max_features=5000只保留出现频率最高的 5000 个词,控制特征维度,避免过拟合和内存爆炸。注意fit_transform只在训练集上调用,测试集用transform,保证词汇表一致。如果测试集出现训练集没有的词,直接忽略,这是标准做法。
4.2 alpha 参数对分类报告的影响
MultinomialNB的alpha参数控制平滑强度。默认alpha=1.0是拉普拉斯平滑,适合大多数场景。如果特征维度很高、样本量相对不足,可以尝试更小的alpha,比如 0.1 或 0.01,让模型更依赖实际计数。
for alpha in [0.01, 0.1, 0.5, 1.0, 2.0]: clf = MultinomialNB(alpha=alpha) clf.fit(X_train_counts, newsgroups_train.target) y_pred = clf.predict(X_test_counts) acc = np.mean(y_pred == newsgroups_test.target) print(f"alpha={alpha:.2f}, 测试集准确率: {acc:.4f}")跑完你会看到准确率随alpha变化,通常alpha在 0.1 到 1.0 之间有一个峰值。如果alpha太小,模型对训练集噪声敏感;alpha太大,概率分布被过度平滑,区分度下降。课程实验里可以把不同alpha的准确率画成折线图,项目说明里解释为什么选某个值。
分类报告能给出每个类别的精确率、召回率和 F1 分数:
clf_best = MultinomialNB(alpha=0.1) clf_best.fit(X_train_counts, newsgroups_train.target) y_pred_best = clf_best.predict(X_test_counts) print(classification_report(newsgroups_test.target, y_pred_best, target_names=newsgroups_test.target_names))如果某个类别的召回率明显偏低,说明该类别的特征词被其他类别“抢”走了,可以考虑增加该类别的训练样本,或者调整特征选择策略。注意,多项式朴素贝叶斯对类别不平衡比较敏感,如果某个类别样本很少,先验概率会很低,预测时容易被忽略。可以在fit时用class_prior参数手动指定先验,或者对训练集做重采样。
5. 避坑与排查:课程实验里最容易翻车的五个地方
5.1 零概率导致预测结果全为同一类
现象:测试集所有样本都被预测成同一个类别,准确率等于该类别在测试集中的占比。原因:某个特征在某个类别下从未出现,概率为零,连乘后整个后验概率为零,模型只能选那个没有零概率的类别。解决:检查是否使用了平滑。多项式朴素贝叶斯设alpha大于 0,高斯朴素贝叶斯设var_smoothing大于 0。如果是手写实现,确认分子分母都加了平滑项。
5.2 特征尺度差异大导致高斯假设失效
现象:高斯朴素贝叶斯在某个数据集上准确率远低于预期,甚至不如随机猜测。原因:某个特征的取值范围是 0 到 1,另一个是 0 到 10000,高斯分布对大方差特征过度敏感,小方差特征被淹没。解决:虽然高斯朴素贝叶斯理论上对尺度不敏感,但实际计算中方差差异过大会导致数值问题。常见做法是对连续特征做标准化,用StandardScaler把每个特征缩放到均值 0、方差 1。注意标准化要在训练集上拟合,再应用到测试集。
5.3 文本分类里 fit_transform 和 transform 混用
现象:测试集准确率异常高,接近 100%,但换一批新数据就崩了。原因:对测试集也调用了fit_transform,导致词汇表包含了测试集的词,相当于提前看到了测试数据。解决:严格区分fit_transform和transform。训练集用fit_transform学习词汇表和文档频率,测试集只能用transform做映射。这是数据泄露的经典案例,课程实验报告里如果出现这种错误,会被直接扣分。
5.4 先验概率被忽略或手动设错
现象:模型在类别不平衡数据上表现很差,少数类几乎不被预测。原因:MultinomialNB默认用训练集的类别频率作为先验,如果训练集里多数类占 90%,少数类占 10%,先验概率会强烈偏向多数类。解决:如果业务上需要提升少数类召回率,可以在fit时用class_prior参数手动指定先验,比如设成[0.5, 0.5]。但要注意,这样做会牺牲整体准确率,需要根据任务目标权衡。课程实验里如果没要求处理不平衡,保持默认即可,但项目说明里要提到这个参数的存在。
5.5 高斯朴素贝叶斯对非正态特征效果差
现象:在某个连续特征数据集上,高斯朴素贝叶斯准确率明显低于逻辑回归或决策树。原因:特征分布严重偏斜,比如收入分布、点击次数分布,高斯假设完全不成立。解决:先画每个特征在每个类别下的直方图,确认是否近似正态。如果偏斜严重,可以做对数变换或 Box-Cox 变换,再喂给高斯朴素贝叶斯。或者换用核密度估计的朴素贝叶斯变体,但 sklearn 没有内置,需要自己实现。课程实验里如果时间有限,直接换多项式朴素贝叶斯做分箱离散化也是一种思路。
6. 进阶技巧:用交叉验证选平滑参数并画出学习曲线
课程实验如果只跑一次训练集测试集划分,结论可能不稳定。我一般会加一层交叉验证,用GridSearchCV自动搜索最优alpha,再画学习曲线看模型是否过拟合或欠拟合。
from sklearn.model_selection import GridSearchCV from sklearn.pipeline import Pipeline # 构建管道:向量化 + 多项式朴素贝叶斯 pipeline = Pipeline([ ('vectorizer', CountVectorizer(stop_words='english', max_features=5000)), ('clf', MultinomialNB()) ]) # 搜索 alpha 参数 param_grid = {'clf__alpha': [0.01, 0.05, 0.1, 0.5, 1.0, 2.0]} grid_search = GridSearchCV(pipeline, param_grid, cv=5, scoring='accuracy', n_jobs=-1) grid_search.fit(newsgroups_train.data, newsgroups_train.target) print(f"最优 alpha: {grid_search.best_params_['clf__alpha']}") print(f"交叉验证最优准确率: {grid_search.best_score_:.4f}")Pipeline把向量化和分类器串在一起,避免在交叉验证中数据泄露——每一折的向量化只在训练部分拟合。cv=5是五折交叉验证,n_jobs=-1用满所有 CPU 核心加速。grid_search.best_params_给出最优alpha,通常和之前手动试出来的峰值一致,但交叉验证的结果更可靠。
学习曲线能看出模型随训练样本增加的表现:
from sklearn.model_selection import learning_curve import matplotlib.pyplot as plt train_sizes, train_scores, test_scores = learning_curve( grid_search.best_estimator_, newsgroups_train.data, newsgroups_train.target, cv=5, n_jobs=-1, train_sizes=np.linspace(0.1, 1.0, 5), scoring='accuracy' ) train_mean = np.mean(train_scores, axis=1) test_mean = np.mean(test_scores, axis=1) plt.plot(train_sizes, train_mean, 'o-', label='训练集准确率') plt.plot(train_sizes, test_mean, 'o-', label='交叉验证准确率') plt.xlabel('训练样本数') plt.ylabel('准确率') plt.legend() plt.title('多项式朴素贝叶斯学习曲线') plt.show()如果训练集准确率远高于交叉验证准确率,说明过拟合,可以增大alpha或减少max_features。如果两条曲线都很低且靠得很近,说明欠拟合,可以减小alpha或增加特征数。我一般会把这张图放进课程实验报告,比单纯列一个准确率数字有说服力得多。
最后说一个我自己的习惯:每次写完朴素贝叶斯源码,都会先用一个极小的手工数据集(比如 6 个样本、2 个特征、2 个类别)跑一遍,把中间概率打印出来,手算验证。这个习惯帮我抓过好几次求和轴搞反、平滑项加错位置的 bug。朴素贝叶斯分类器的源码虽然短,但概率计算的每一步都值得较真。希望帮到你。
本文还有配套的精品资源,点击获取