贝叶斯最小错误率分类器:从决策规则到手写数字识别实战
简介这套基于贝叶斯最小错误率的手写数字识别项目面向机器学习初学者与模式识别课程设计者解决手写数字高效分类问题。项目在贝叶斯决策框架下不仅计算后验概率还引入错误成本使不同误判损失能得到合理权衡避免将“1”误判为“7”与将“9”误判为“4”产生同等代价。压缩包共5个文件大小仅52KB包括两个M程序文件、一个FIG界面、一个DAT特征数据和一个MAT样本库M主程序完成识别流程特征提取函数负责预处理与特征编码样本文件提供预训练数据GUI便于用户绘图测试。已有551人学习下载。借助这套资源读者能完整走通从特征提取、模型训练到界面测试的贝叶斯分类流程也可将其作为课程设计或实验报告的参考方案深入理解最小错误率决策的实际应用。1. 贝叶斯最小错误率手写数字识别里先立住决策规则如果你已经用深度学习跑惯了 MNIST可能会觉得“贝叶斯最小错误率手写数字识别”是个老古董。但真在手写数字场景里做过基线、做过拒识、做过小样本分类的人会有个反直觉的体会贝叶斯最小错误率分类器的瓶颈从来不是那个公式而是你给它的类条件概率密度靠不靠谱。换个说法数字 4 和 9 的像素分布高度重叠时再精确的后验概率也没法把错误率压到 0但它至少能告诉你“这个样本不能信”。这正是这个标题的价值它解决的问题不是“能不能识别数字”而是“在已知每个数字类别先验和像似然的前提下如何做出错误率最小的决策”。它适合两类人一类是想把模式识别里的生成式分类器真正落到代码上的工程师另一类是手里只有几百张标注样本、但又不想一上来就上神经网络的团队。全文会从决策规则一路走到参数估计、混淆矩阵分析和调优技巧把整个链路用最小可运行代码串起来。2. 用贝叶斯最小错误率分类器判别数字先验、似然与决策面2.1 从贝叶斯公式到最小错误率决策规则贝叶斯最小错误率分类器的起点是贝叶斯公式P(C_k|x) P(x|C_k) * P(C_k) / P(x)在手写数字识别里C_k 是 0 到 9 这十个类别x 是一张归一化后的特征向量。P(C_k) 是类别先验P(x|C_k) 是该类别的类条件概率密度分母 P(x) 对所有类别相同所以决策时只需要比较分子大小。最小错误率决策规则是什么呢它不做任何花哨的投影或距离计算只做一件事选择后验概率最大的那个数字类别。这个规则在 0-1 损失下被证明是错误率最小的。0-1 损失的意思是分类正确损失为 0分类错误损失为 1而且所有类别的错误损失完全相同。手写数字识别的大部分落地场景天然满足这个前提把 4 认成 9 和把 9 认成 4代价是一样的所以最小错误率分类器是合理的起点。2.2 最小错误率与最小风险什么时候不能只看后验如果业务场景里错误代价不对称最小错误率就不是最优目标。举个例子银行表单里的手写金额识别把 3 认成 8 属于金额录入错误把 8 认成 3 同样严重这是对称的。但在“单据自动通过 低置信转人工”的场景里漏过一张错票的成本远高于把好票转人工去核验此时该用的是最小风险决策而不是最小错误率。最小错误率和最小风险的区别可以看下面的表格。决策目标损失矩阵设定适用场景决策方式最小错误率0-1 损失所有错误代价相同普通手写数字分类、MNIST 基准实验argmax 后验概率最小风险非对称损失不同错误代价不同票据审核、金额录入、拒识场景按损失矩阵加权后比较回归到手写数字识别这个标题默认就采用 0-1 损失下的最小错误率分类器。只有当你发现误识和拒识的业务代价不同才需要把决策规则从“argmax 后验”改成“argmin 期望损失”。2.3 高斯假设下的决策面为什么不是像素距离最近很多人以为写数字分类就是拿距离度量比如算一下测试样本和每类均值的欧氏距离然后选最近的。这种做法其实隐含了一个很强的概率假设每一类数字的像素分布都是各向同性的高斯分布协方差矩阵是单位阵的倍数。手写数字的笔画变异根本不满足这个假设。数字 1 的横向摆动通常不大但纵向笔画的粗细变化明显数字 0 的椭率在不同人笔下差异很大。若用全协方差矩阵建模贝叶斯分类器会退化为马氏距离分类样本到每类中心的距离要被该类协方差矩阵重新加权。这也是贝叶斯最小错误率分类器比“取均值然后算距离”更优的根本原因它显式刻画了每一类数字的协方差结构。3. 手写数字识别特征准备从灰度图到可估计协方差的向量3.1 灰度读取、归一化与反色处理先用 MNIST 作为演示但要注意很多真实手写数字扫描件是白底黑字和 MNIST 的黑底白字相反。如果直接拿反色图像喂给模型均值向量和协方差矩阵都会偏移识别率往往掉 2 到 3 个百分点。读取和预处理的常见做法是先把图像拉平成向量再做标准化import numpy as np from tensorflow.keras.datasets import mnist (x_train_raw, y_train), (x_test_raw, y_test) mnist.load_data() X_train_raw x_train_raw.reshape(x_train_raw.shape[0], -1).astype(np.float32) X_test_raw x_test_raw.reshape(x_test_raw.shape[0], -1).astype(np.float32) # 如果图像是白底黑字先反色MNIST 本身是黑底白字 # X_train_raw 255.0 - X_train_raw mean X_train_raw.mean(axis0) std X_train_raw.std(axis0) 1e-6 X_train (X_train_raw - mean) / std X_test (X_test_raw - mean) / std标准化参数只在训练集上计算测试集复用同一个 mean 和 std。不要在测试集上重新估计均值和方差这会让模型看到测试集的全局统计信息实验结论会偏乐观。如果原始图像尺寸不是 28×28需要先统一缩放常见的插值参数建议用 INTER_AREA 做缩小、用 INTER_CUBIC 做放大避免出现明显锯齿。3.2 PCA 降维维度是一个需要调的参数原始 784 维向量直接用于估计协方差矩阵理论上需要 784 维以上、数量充足且覆盖各类变异的样本。如果训练集只有几千张全协方差矩阵几乎必然奇异。PCA 是最常见的降维手段而且 PCA 的另一个好处是它做了去相关让后续协方差矩阵估计更稳定。from sklearn.decomposition import PCA pca PCA(n_components0.95) X_train_pca pca.fit_transform(X_train) X_test_pca pca.transform(X_test) print(保留维度:, pca.n_components_) print(累计解释方差比:, pca.explained_variance_ratio_.sum())n_components0.95 的含义是保留 95% 的总方差这是一个经验起点。手写数字这种低层视觉任务多数情况下维度会落在 70 到 120 之间。也可以用固定维度比如 n_components64配合后面的贝叶斯优化来调。PCA 的 fit 只能在训练集上测试集只做 transform原因是 PCA 的基向量属于训练集特征分布的一部分。3.3 数据划分与信息泄漏问题MNIST 自带训练集和测试集但自有数据集时需要分层划分。手写数字通常类别不平衡程度不高但为了避免某个数字恰好全落在验证集里建议用分层采样from sklearn.model_selection import StratifiedShuffleSplit sss StratifiedShuffleSplit(n_splits1, test_size0.2, random_state42) train_idx, val_idx next(sss.split(X_train_raw, y_train)) Xfold_train X_train_raw[train_idx] yfold_train y_train[train_idx] Xfold_val X_train_raw[val_idx] yfold_val y_train[val_idx]注意一个容易踩的坑如果要做数据增强比如旋转、平移、加噪声增强操作必须发生在划分之后而且只能对训练集增强。否则原始样本和它的增强版本可能同时出现在训练集和验证集里验证集误差会被严重低估。4. 高斯类条件概率密度估计与最小错误率分类器实现4.1 每类独立估计均值与协方差高斯判别分析的核心是假设每个数字类别的特征向量服从多维高斯分布。对第 k 类数字需要估计均值向量 mu_k 和协方差矩阵 Sigma_kmu_k 1/N_k * sum(x_i)Sigma_k 1/N_k * sum((x_i - mu_k)(x_i - mu_k)^T)这里用的是最大似然估计。注意一个细节样本协方差的分母用 N 而不是 N-1 在分类任务里影响很小但如果训练样本数不大更稳妥的做法是用除以 (N-1) 的无偏版本即 numpy 中 np.cov 的默认行为。这个差异在高维小样本下会略微影响矩阵的条件数。4.2 协方差矩阵的三种策略与正则化参数在实际手写数字识别里全协方差矩阵并不总是最优。特征维度高、样本少的时候协方差矩阵的估计误差会直接放大到判别函数里。下表是三种常见策略。协方差策略参数数量决策面类型手写数字场景下的表现每类独立全协方差多约 d d(d1)/2 每类二次决策面能捕捉笔画结构但样本不足时易过拟合各类共享一个协方差中等只估计一个全局 Sigma线性决策面样本效率高但忽略了不同数字的形态差异对角协方差朴素贝叶斯少每类 d 个方差线性决策面按维度独立加权参数少、训练快但像素相关性被丢弃全协方差在高维下的推荐做法是加正则约束。常见实现是 Sigma_k (1 - lambda) * Sigma_k_mle lambda * diag(Sigma_k_mle)lambda 取值通常从 1e-4 到 1e-1。lambda 过大会把协方差拉成对角阵退化成朴素贝叶斯过小则矩阵可能不可逆。4.3 用 log 域实现后验计算与预测实现贝叶斯最小错误率分类器时最怕的是高维高斯密度数值下溢。特征维度几十维时概率密度值会小到超出浮点数范围。解决办法是全程在 log 域计算只比较对数后验的大小。import numpy as np class BayesMinErrorClassifier: def __init__(self, reg1e-3): self.reg reg def fit(self, X, y): self.classes np.unique(y) self.means {} self.covs {} self.priors {} n len(X) for c in self.classes: Xc X[y c] self.priors[c] len(Xc) / n self.means[c] Xc.mean(axis0) cov np.cov(Xc, rowvarFalse) # 正则化防止协方差奇异 cov self.reg * np.eye(cov.shape[0]) self.covs[c] cov return self def _log_likelihood(self, X, c): mu self.means[c] cov self.covs[c] d X - mu # pinv 比 inv 更稳奇异时不会直接报错 cov_inv np.linalg.pinv(cov) logdet np.linalg.slogdet(cov)[1] maha np.sum((d cov_inv) * d, axis1) return -0.5 * maha - 0.5 * logdet def predict_log_proba(self, X): log_posterior np.column_stack([ self._log_likelihood(X, c) np.log(self.priors[c]) for c in self.classes ]) # 减掉最大值让数值稳定不回传概率也成立 return log_posterior - log_posterior.max(axis1, keepdimsTrue) def predict(self, X): idx np.argmax(self.predict_log_proba(X), axis1) return self.classes[idx]这里使用了 np.linalg.pinv 而不是 np.linalg.inv因为即便加了正则高维协方差矩阵仍可能出现接近奇异的特征值pinv 会通过奇异值分解给出稳定的伪逆。predict_log_proba 中减去每行最大值不会改变 argmax 的结果但可以防止后面对数概率做指数变换时溢出。这个类已经是一个可用的最小错误率贝叶斯分类器训练后直接调 predict 就能得到数字标签。如果你的特征是 PCA 降维后的向量并且数据做了标准化这套实现基本不会遇到数值问题。MATLAB 用户也不必拘泥于 Python拟合分别估计 mu 和 sigma分类时用 mvnpdf 计算每个类别的似然再乘先验取最大值即可逻辑完全一样。5. 手写数字识别的评估混淆矩阵与错误模式分析5.1 用混淆矩阵而不是准确率定位问题贝叶斯最小错误率分类器训练完成后最常见的评估错误是只看总准确率。总准确率掩盖了所有结构性的错误关系。比如某个模型整体准确率 95%但其中 4 被认成 9 的比例高达 20%这在特定业务里是不可接受的。正确做法是打印逐类指标和混淆矩阵。from sklearn.metrics import confusion_matrix, classification_report y_pred clf.predict(X_test_pca) report classification_report(y_test, y_pred, digits3) print(report) cm_normalized confusion_matrix(y_test, y_pred, normalizetrue) print(cm_normalized.round(3))classification_report 给出每个数字类别的精确率、召回率和 F1。normalizetrue 会对每一行真实类别归一化这样第 i 行第 j 列的含义是“真实为 i 的样本中有多少比例被预测成了 j”。它是排查类别混淆的主力工具。5.2 常见混淆对与对应的修正方向基于高斯判别分析的手写数字识别器错误往往集中在这几个方向。混淆对典型原因尝试的修正方向4 和 9上半部分结构相似PCA 保留维度偏低提高 PCA 维度或加入局部结构特征3 和 8中间笔画闭合关系的差异被特征向量稀释保留更多主成分或对图像做细化7 和 17 的短横缺失或过浅做笔画宽度归一化或增加相应训练样本0 和 6下半部闭合差异在低分辨率下不显著上采样到更大尺寸再重新降维看到某个混淆对后我一般会先回去检查该类别在 PCA 子空间下的分布图。直接取训练集里 500 个样本做二维可视化观察 4 和 9 的重叠区域。如果重叠区域过大就算改分类器结构也救不回来问题出在特征表示或图像本身的分辨率上。5.3 用 t-SNE 检查特征空间的重叠程度from sklearn.manifold import TSNE sample_idx np.random.default_rng(0).choice(len(X_train_pca), 2000, replaceFalse) X_sample X_train_pca[sample_idx] y_sample y_train[sample_idx] tsne TSNE(n_components2, perplexity30, random_state0) X_emb tsne.fit_transform(X_sample) # 按 y_sample 分别散点绘制即可观察类别边界perplexity 一般取 5 到 50样本量 2000 时 30 是常用起点。t-SNE 的结果不是分类器它只是帮你判断特征是否可分。如果两个数字类在 t-SNE 图上完全纠缠贝叶斯最小错误率分类器无论怎么调参准确率天花板都摆在那里。反过来如果类别边界清晰但分类器表现不佳那问题更可能出在协方差矩阵估计或正则化强度上。6. 贝叶斯最小错误率分类器调优的 3 个实用技巧6.1 用贝叶斯优化调 PCA 维度和正则化系数PCA 维度和协方差正则化参数 lambda 之间存在交互维度越高全协方差矩阵需要的正则化越强维度越低模型偏差越大。手动调这两个参数很折磨常见做法是交给贝叶斯优化来搜。以 Optuna 为例它使用 TPE 这类贝叶斯优化策略相比网格搜索能更快找到较好的参数组合。import optuna def objective(trial): n_comp trial.suggest_int(n_comp, 40, 120) reg trial.suggest_float(reg, 1e-5, 1e-1, logTrue) pca_tmp PCA(n_componentsn_comp) X_tr pca_tmp.fit_transform(X_train) X_te pca_tmp.transform(X_val) clf_tmp BayesMinErrorClassifier(regreg) clf_tmp.fit(X_tr, y_train) acc np.mean(clf_tmp.predict(X_te) y_val) return acc study optuna.create_study(directionmaximize) study.optimize(objective, n_trials50) print(study.best_params)贝叶斯优化的意义在于它能尝试那些人工调参容易忽略的参数组合比如高维度配较高正则化。运行时建议把验证集固定避免不同 trial 之间因数据划分不同而产生噪声。6.2 用后验概率阈值做拒识而不只是输出标签最小错误率分类器的天然副产品是每个类别的后验概率。在真实业务里很多错误可以通过“低置信拒识”转化掉。设置一个概率阈值比如后验最大值低于 0.9 时拒绝分类转到人工处理。log_proba clf.predict_log_proba(X_test_pca) max_prob np.exp(log_proba.max(axis1)) reject_mask max_prob 0.9 y_pred_final clf.predict(X_test_pca) y_pred_final[reject_mask] -1 # -1 表示拒识阈值的选择可以用“拒绝率对错误率曲线”来定。横轴是阈值从 0 到 0.99 变化纵轴是剩余样本的错误率。挑一个业务能接受的人工处理量对应的阈值即可。这就是把最小错误率思想从分类层延伸到决策层也是贝叶斯分类器相比纯判别模型的实用优势。6.3 对比训练集、验证集和拒识集的分布差异最后一步是判断模型是否真的在泛化。把贝叶斯分类器在训练集和验证集上各自的混淆矩阵做差如果差异集中在几个特定类别上说明这些类存在过拟合如果差异均匀说明整体都过拟合了优先加大正则化或降低 PCA 维度。拒识集也要单独看。那些后验概率低于阈值的样本它们的真实标签如果集中在某几类说明这些类在特征空间里本身叠得厉害需要回到第 5 章的错误模式分析去补样本或改特征。校准曲线、混淆矩阵差异和拒绝率曲线这三样放在一起基本就能判断分类器是在泛化还是在背训练集。本文还有配套的精品资源点击获取