
简介本资源为课程实验配套的朴素贝叶斯分类器设计源码包面向计算机、人工智能、通信工程等专业的在校学生及教师也适合具备一定Python基础的学习者进阶练习可用于课程设计、作业提交或毕设初期立项演示。压缩包共26个文件约256KB以10个py源码文件为核心辅以6张png结果图、4个npy与3个npz数据文件以及md说明和txt依赖清单覆盖朴素贝叶斯与AODE两类分类器的实现并延伸至RBF、BP、K-means、LVQ、MoG等实验模块数据涉及MNIST、lung、yale等数据集。项目代码均经测试运行成功配有README说明与绘图脚本便于读者理解算法流程、复现实验结果并在此基础上修改扩展。目前已有373人学习下载适合需要完整实验方案与可运行代码参考的读者。1. 朴素贝叶斯分类器课程实验包从 MNIST 到肺部基因表达数据的完整复现路径如果你正在上人工智能或机器学习课程大概率会遇到这样一个实验手写一个朴素贝叶斯分类器不许调 sklearn 的GaussianNB还得在真实数据集上跑出可对比的准确率曲线。这个资源包就是为这类课程实验准备的——它用纯 Python 实现了朴素贝叶斯和 AODE 两种分类器附带 MNIST、肺部基因表达lung和耶鲁人脸yale三套数据集还额外包含了 BP 神经网络、RBF 网络、K-means、LVQ、MoG 等实验代码。整个包按 exp1 到 exp4 分目录组织每个实验有独立的main.py和README.mdrequirements.txt锁定了依赖版本。适合计算机、人工智能、通信工程等专业的在校学生直接用于课程设计或作业提交也适合想从零理解贝叶斯决策理论的开发者拿来拆解学习。下面我从数据加载、模型实现、参数调优到踩坑排查把这份资源完整走一遍。2. 数据管线拆解npz 文件怎么读、npy 参数怎么用2.1 三套数据集的格式与加载方式资源包里npz目录下有三个文件mnist.npz、lung.npz、yale.npz。npz 是 NumPy 的压缩归档格式内部可以存多个数组读取时用np.load()返回一个类似字典的对象。以 MNIST 为例常见做法是内部包含x_train、y_train、x_test、y_test四个键。lung 数据集是基因表达数据通常维度高、样本少适合测试朴素贝叶斯在超高维小样本下的表现。yale 是人脸数据常用于降维后分类。import numpy as np # 加载 MNIST 数据 data np.load(npz/mnist.npz) print(data.files) # 查看内部键名确认结构 x_train data[x_train] y_train data[y_train] x_test data[x_test] y_test data[y_test] # 归一化到 [0,1]朴素贝叶斯对尺度敏感 x_train x_train.astype(np.float64) / 255.0 x_test x_test.astype(np.float64) / 255.0 # 展平图像为向量如果原始是二维图像 if x_train.ndim 2: x_train x_train.reshape(x_train.shape[0], -1) x_test x_test.reshape(x_test.shape[0], -1)这段代码做了三件事先探查 npz 内部键名避免硬编码猜错再把像素值归一化到 0 到 1 之间因为高斯朴素贝叶斯假设特征服从正态分布原始 0 到 255 的尺度会让方差估计偏大最后把图像展平成二维矩阵每行一个样本。如果你拿到的 npz 键名不同用data.files打印出来对照修改即可。2.2 npy 参数文件的作用与复用逻辑HW目录下的 npy 文件命名很有规律alpha_beta_ACC_MNIST.npy、alpha_beta_NMI_MNIST.npy、alpha_beta_NMI_lung.npy等。从命名推断这些是超参数搜索的结果缓存——alpha 和 beta 是 AODE 或朴素贝叶斯中的平滑参数ACC 和 NMI 分别对应准确率和归一化互信息。把搜索结果存成 npy下次跑实验直接加载不用重新网格搜索。# 加载已保存的超参数搜索结果 alpha_beta_acc np.load(HW/alpha_beta_ACC_MNIST.npy) print(alpha_beta_acc.shape) # 通常是 (n_alpha, n_beta) 的二维网格 # 找到最优 alpha-beta 组合 best_idx np.unravel_index(np.argmax(alpha_beta_acc), alpha_beta_acc.shape) print(f最优 alpha 索引: {best_idx[0]}, beta 索引: {best_idx[1]})这里的关键是理解 npy 存的是网格搜索结果而非模型权重。np.unravel_index把扁平索引还原成二维坐标对应到 alpha 和 beta 的取值。如果你要换数据集需要重新跑一遍搜索并覆盖保存不能直接套用 MNIST 的最优参数到 lung 上——两者的特征分布差异很大。2.3 图像输出文件与实验记录img目录下的 png 文件是实验产出的可视化结果mnist_ACC.png、mnist_NMI.png、lung_ACC.png、lung_Y.png等。ACC 图通常是准确率随参数变化的曲线NMI 图是聚类质量指标Y 图可能是标签分布或混淆矩阵的可视化。plot_loss.py是独立的绘图脚本负责从训练日志中读取 loss 并生成曲线。# plot_loss.py 的典型用法 import matplotlib.pyplot as plt import numpy as np # 假设 loss 记录在 npy 或 txt 中 loss np.load(HW/loss_history.npy) plt.figure(figsize(8, 5)) plt.plot(loss, labelTraining Loss) plt.xlabel(Epoch) plt.ylabel(Loss) plt.legend() plt.savefig(img/loss_curve.png, dpi150) plt.show()绘图脚本单独抽出来的好处是训练和可视化解耦调图的时候不用重跑模型。我一般会把dpi设到 150 以上否则插入实验报告时文字会糊。3. 朴素贝叶斯与 AODE 实现从公式到代码的映射3.1 朴素贝叶斯的条件概率与拉普拉斯平滑NaiveBayes.py是核心实现文件。朴素贝叶斯的决策规则是对每个类别 $c$计算后验概率 $P(c|x) \propto P(c) \prod_{i1}^{d} P(x_i|c)$然后取最大后验对应的类别。对于连续特征$P(x_i|c)$ 用高斯分布估计对于离散特征用频率估计并加拉普拉斯平滑。class NaiveBayes: def __init__(self, alpha1.0): self.alpha alpha # 拉普拉斯平滑系数 def fit(self, X, y): self.classes np.unique(y) self.n_classes len(self.classes) self.n_features X.shape[1] self.mean np.zeros((self.n_classes, self.n_features)) self.var np.zeros((self.n_classes, self.n_features)) self.prior np.zeros(self.n_classes) for idx, c in enumerate(self.classes): X_c X[y c] self.mean[idx, :] X_c.mean(axis0) # 方差加 alpha 防止为零导致 log 出现 -inf self.var[idx, :] X_c.var(axis0) self.alpha self.prior[idx] X_c.shape[0] / X.shape[0] def predict(self, X): y_pred [] for x in X: posteriors [] for idx, c in enumerate(self.classes): prior np.log(self.prior[idx]) # 高斯对数似然 likelihood -0.5 * np.sum( np.log(2 * np.pi * self.var[idx]) ((x - self.mean[idx]) ** 2) / self.var[idx] ) posteriors.append(prior likelihood) y_pred.append(self.classes[np.argmax(posteriors)]) return np.array(y_pred)这段代码里有两个容易翻车的点。第一方差必须加一个正数这里用alpha否则某个特征在某个类别下所有样本取值相同方差为零对数运算直接报-inf。第二先验和似然都取对数再相加避免多个小概率相乘导致下溢。alpha默认取 1.0 是拉普拉斯平滑的经典值实际调参时可以在 0.1 到 10 之间搜索。3.2 AODE 的加权平均与超父节点选择AODE.py实现的是 Averaged One-Dependence Estimators。它和朴素贝叶斯的区别在于不再假设所有特征在给定类别下条件独立而是为每个特征选一个“超父节点”构建一棵深度为 2 的树然后对所有可能的超父节点取平均。class AODE: def __init__(self, alpha1.0, beta1.0): self.alpha alpha # 条件概率平滑 self.beta beta # 超父节点选择阈值 def fit(self, X, y): self.classes np.unique(y) self.n_features X.shape[1] # 存储每个超父节点下的条件概率表 self.tables {} for parent in range(self.n_features): # 只保留出现频率足够高的超父节点 if np.mean(X[:, parent]) self.beta: continue self.tables[parent] self._build_table(X, y, parent) def _build_table(self, X, y, parent): # 构建 P(x_i | c, x_parent) 的计数表 table {} for c in self.classes: mask (y c) X_c X[mask] for val in np.unique(X_c[:, parent]): key (c, val) table[key] {} for i in range(self.n_features): if i parent: continue table[key][i] np.mean(X_c[X_c[:, parent] val, i]) return tableAODE 的核心参数是beta它控制哪些特征有资格当超父节点。如果某个特征取值过于稀疏作为超父节点会导致条件概率表大量为空反而拉低分类效果。beta设得太高会退化成朴素贝叶斯设得太低则计算量暴涨。资源包里alpha_beta_ACC_MNIST.npy存的就是这两个参数的网格搜索结果直接加载可以省去重复调参。3.3 训练与评估入口main.py 的调用链每个 exp 目录下的main.py是实验入口。以 exp2 为例它加载数据、实例化分类器、训练、预测、输出准确率并调用绘图函数保存结果。# exp2/main.py 的典型结构 import numpy as np from NaiveBayes import NaiveBayes from AODE import AODE def load_data(path): data np.load(path) return data[x_train], data[y_train], data[x_test], data[y_test] if __name__ __main__: x_train, y_train, x_test, y_test load_data(../npz/mnist.npz) x_train x_train.reshape(x_train.shape[0], -1) / 255.0 x_test x_test.reshape(x_test.shape[0], -1) / 255.0 # 朴素贝叶斯 nb NaiveBayes(alpha1.0) nb.fit(x_train, y_train) acc_nb np.mean(nb.predict(x_test) y_test) print(fNaiveBayes Accuracy: {acc_nb:.4f}) # AODE aode AODE(alpha1.0, beta0.1) aode.fit(x_train, y_train) acc_aode np.mean(aode.predict(x_test) y_test) print(fAODE Accuracy: {acc_aode:.4f})运行方式是在 exp2 目录下执行python main.py。注意路径问题如果从项目根目录运行../npz/mnist.npz要改成npz/mnist.npz。我一般会在 main.py 开头加一行os.chdir(os.path.dirname(os.path.abspath(__file__)))把工作目录切到脚本所在位置这样不管从哪运行都不会找不到文件。4. 避坑与排查数据加载、数值稳定性和参数搜索的五个血泪教训4.1 现象准确率只有 10% 左右和随机猜差不多原因MNIST 是十分类随机猜的准确率就是 10%。出现这种情况通常是标签和特征没对齐或者数据加载时把x_train和y_train的键名搞混了。npz 文件内部键名不统一有的用x_train有的用train_x。解决加载后先打印data.files和每个数组的 shape确认第一维是样本数且特征与标签样本数一致。再用np.unique(y_train)看类别数是否等于 10。4.2 现象训练时 loss 变成 nan 或 -inf原因高斯朴素贝叶斯的方差为零对数运算出现负无穷。或者某个类别下某个特征的所有取值相同方差估计为零。解决在方差上加一个小的正数代码里就是self.var[idx, :] X_c.var(axis0) self.alpha。alpha不要设成 0最小取 1e-9。另外检查数据里有没有全零列如果有先做特征筛选或加噪。4.3 现象AODE 跑得极慢内存爆掉原因AODE 要为每个超父节点构建条件概率表如果特征维度是 784MNIST每个特征都当超父节点表的大小是类别数乘以超父节点取值数乘以特征数。MNIST 像素取值连续np.unique会返回几百个不同值表直接爆炸。解决对连续特征先做离散化比如把像素值分箱到 8 个区间。或者用beta参数过滤掉取值过于稀疏的超父节点。资源包里 lung 数据维度更高但样本少反而适合 AODEMNIST 上 AODE 需要先降维或离散化。4.4 现象换了数据集后加载 npy 参数文件报 shape 不匹配原因alpha_beta_ACC_MNIST.npy是在 MNIST 上搜索的网格alpha 和 beta 的取值范围与 lung 数据集不匹配。直接套用会导致索引越界或结果无意义。解决每个数据集单独跑一遍网格搜索保存时文件名带上数据集标识。搜索范围根据数据特性调整lung 数据样本少alpha 可以取小一点0.01 到 1MNIST 样本多alpha 取 0.1 到 10。4.5 现象绘图脚本报错找不到文件或中文乱码原因plot_loss.py里硬编码了文件路径换目录后路径失效。中文乱码是因为 matplotlib 默认字体不支持中文。解决路径用os.path.join拼接基于脚本所在目录计算相对路径。中文显示问题加两行配置import matplotlib matplotlib.rcParams[font.sans-serif] [SimHei] matplotlib.rcParams[axes.unicode_minus] FalseSimHei是 Windows 自带黑体Linux 下换成WenQuanYi Micro Hei或系统已装的中文字体。5. 进阶技巧用 alpha-beta 网格搜索把 AODE 准确率再提两个点资源包里已经存了alpha_beta_ACC_MNIST.npy和alpha_beta_NMI_lung.npy但很多人下载后直接加载就完事没有真正理解这两个参数怎么影响模型。我建议你至少手动跑一次网格搜索亲眼看到准确率曲面的形状这比看任何理论推导都管用。具体做法是固定 beta 为几个候选值比如 0.01、0.05、0.1、0.5对每个 beta 遍历 alpha 从 0.01 到 10 的对数刻度记录测试集准确率。代码框架如下import numpy as np from AODE import AODE alphas np.logspace(-2, 1, 20) # 0.01 到 10 betas [0.01, 0.05, 0.1, 0.5] results np.zeros((len(alphas), len(betas))) for i, alpha in enumerate(alphas): for j, beta in enumerate(betas): aode AODE(alphaalpha, betabeta) aode.fit(x_train, y_train) acc np.mean(aode.predict(x_test) y_test) results[i, j] acc # 保存结果下次直接加载 np.save(HW/alpha_beta_ACC_custom.npy, results) best np.unravel_index(np.argmax(results), results.shape) print(f最优 alpha{alphas[best[0]]:.4f}, beta{betas[best[1]]:.4f}, ACC{results[best]:.4f})这段代码的关键在于np.logspace生成对数等距的 alpha 序列因为平滑系数在数量级上的差异比线性差异更重要。跑完之后你会看到一张二维热力图通常 alpha 在 0.1 到 1 之间、beta 在 0.05 到 0.2 之间会出现一个明显的峰值区域。如果曲面很平坦说明模型对参数不敏感这时候优先选计算量小的参数组合。还有一个容易被忽略的点AODE 的预测阶段可以用对数域累加代替概率连乘避免浮点下溢。具体就是在predict里对每个超父节点的条件概率取 log 再求和最后取平均。这个改动在 lung 这种高维小样本数据上尤其明显因为特征多、概率值小连乘很容易变成 0。验证改进是否有效的方法很简单固定随机种子跑三次取平均准确率对比改进前后的数值。如果提升不到 0.5 个百分点可能是数据集本身噪声太大不必强求。我一般会在实验报告里附上参数搜索的热力图和三次运行的方差这样即使准确率提升有限也能体现实验的严谨性。从那以后我每次拿到新的分类数据集都强制先跑一遍参数网格搜索再下结论绝不拍脑袋设默认值。希望帮到你。本文还有配套的精品资源点击获取