简介这份资源是面向模式识别课程学习者与机器学习入门者的Python图像分类实践项目围绕贝叶斯分类器实现图片自动归类适合作为课程大作业参考或算法练手素材。压缩包共17个文件约1.79MB以3个py源码文件为核心辅以txt依赖说明、csv特征数据、json配置、spec打包脚本及md说明文档另含示例图片与图标资源结构完整便于直接运行与二次修改。项目同时提供控制台与GUI两种交互方式控制台版通过命令行输入图片路径与分类基准点坐标完成分类GUI版借助图形界面支持文件浏览与鼠标点击选点覆盖图像预处理、特征提取、模型训练与概率判别等关键环节采用高斯贝叶斯假设简化特征分布计算。目前已有994人学习下载读者可借此理解贝叶斯方法在图像分类中的落地流程掌握从特征构建到界面交互的完整实现思路并积累Python工程组织与打包经验。1. 从一张 32×32 的灰度图说起贝叶斯分类器到底在算什么模式识别大作业里最容易被低估的一题就是「基于 Python 实现图片的贝叶斯分类器分类」。很多人第一反应是调个sklearn.naive_bayes三行跑完交差结果答辩被问「先验概率怎么来的、似然为什么取对数、像素独立性假设站不站得住」直接卡壳。这篇笔记就按一线做作业的顺序把这件事从数学到代码拆开输入是一批已经标注好的图片常见是手写数字、人脸、交通标志输出是每个类别的后验概率和最终预测标签核心是贝叶斯决策规则加参数估计。适合正在做模式识别大作业、想真正搞懂而不是复制粘贴的同学也适合已经跑通但想补上参数和踩坑细节的人。下面所有代码基于 Python 3 加 NumPy不依赖任何深度学习框架一台普通笔记本就能跑完。2. 先验、似然、后验把贝叶斯分类器的数学骨架搭起来2.1 贝叶斯决策规则到底在比什么图片分类的本质是给定一个观测向量 x把一张图拉平成像素向量判断它属于哪一类 ω。贝叶斯分类器不直接学一个「黑匣子」映射而是用概率说话。最小错误率准则下的决策规则是对每一类计算后验概率 P(ω|x)取最大的那一类。后验通过贝叶斯公式展开P(ω|x) P(x|ω) · P(ω) / P(x)分母 P(x) 对所有类别都一样比较时可以丢掉所以真正要比的是 P(x|ω) · P(ω)。这里 P(ω) 是先验概率反映「不看图之前某类出现的可能性」P(x|ω) 是似然反映「如果这张图真是 ω 类它长成现在这样的概率有多大」。很多人写代码时只算似然忘了乘先验在类别均衡的数据集上看不出问题一旦某类样本特别少就会系统性偏向多数类这是血泪经验。2.2 为什么工程上几乎都用朴素贝叶斯直接估计 P(x|ω) 有个致命问题x 的维度等于像素数。一张 32×32 的图就是 1024 维要估计 1024 维的联合分布参数量随维度指数爆炸样本根本不够。朴素贝叶斯做了一个强假设在给定类别的前提下各个像素或各个特征相互独立。于是似然可以拆成连乘P(x|ω) ∏ P(x_i|ω)这个假设在图像上其实并不成立——相邻像素高度相关所以叫「朴素」。但它的好处是参数量从指数级降到线性级而且实践中效果常常不差尤其是特征做过二值化或离散化之后。做模式识别大作业老师要看的往往不是你把假设证伪而是你清楚它在哪失效、你怎么处理。2.3 连续像素怎么变成可估计的概率像素值是 0 到 255 的连续量直接对连续变量做连乘需要假设分布形式。常见两条路方案似然假设适用场景参数高斯朴素贝叶斯每个像素服从正态分布像素连续、近似对称每类每维的均值 μ、方差 σ²多项式/伯努利朴素贝叶斯像素离散为计数或 0/1二值化图、词袋式特征每类每维的概率或计数高斯版对图像更自然但要注意方差为 0 的维度会导致除零必须加平滑项。伯努利版适合把图二值化后统计「黑点出现概率」实现简单、数值稳定是作业里最稳的选择。我一般先用伯努利版跑通流程再用高斯版对比两个结果都写进报告答辩时能讲出差异。2.4 用对数把连乘变成连加连乘几百上千个小于 1 的概率浮点数会直接下溢成 0这是新手最常见的翻车点。解决办法是全程取对数把连乘变连加log P(ω|x) ∝ log P(ω) Σ log P(x_i|ω)比较时只比这个对数值最大的就是预测类。取对数不改变单调性所以决策结果完全一致但数值稳定性天差地别。下面所有实现都走对数路线。3. 用 NumPy 从零实现伯努利朴素贝叶斯图片分类3.1 数据准备把图片读成矩阵并二值化假设数据按类别放在不同文件夹每个文件夹是一类。先写一个加载函数把每张图统一尺寸、转灰度、二值化再拉平成一行。import os import numpy as np from PIL import Image def load_images(root, img_size(32, 32), threshold128): 读取 root 下按类别分文件夹的图片返回 X 和 y。 X: (N, D) 的 0/1 矩阵D img_size[0]*img_size[1] y: (N,) 的整数标签 X, y [], [] classes sorted(os.listdir(root)) # 类别顺序固定保证标签一致 for label, cls in enumerate(classes): cls_dir os.path.join(root, cls) if not os.path.isdir(cls_dir): continue for fname in os.listdir(cls_dir): path os.path.join(cls_dir, fname) try: img Image.open(path).convert(L) # 转灰度 img img.resize(img_size) # 统一尺寸 arr np.asarray(img, dtypenp.float32) arr (arr threshold).astype(np.float32) # 二值化暗为1 X.append(arr.ravel()) y.append(label) except Exception as e: print(跳过, path, e) # 坏图直接跳过别让一张图毁掉整个流程 return np.array(X), np.array(y), classes逻辑说明convert(L)保证通道统一resize保证维度一致二值化阈值 128 是经验值对光照不均的图可以改成自适应阈值。ravel()把二维图拉成一维向量这是后续所有概率计算的基础。参数上img_size越小训练越快但信息损失越多32×32 是手写数字类作业的常用折中threshold决定黑点判定如果你的图是白底黑字 threshold取暗点是对的反过来就要改。3.2 训练估计先验和每维条件概率伯努利模型下每个像素只有「出现/不出现」两种状态条件概率就是每类中该像素为 1 的频率。加平滑项避免某维概率为 0 导致整个对数似然变成负无穷。def train_bnb(X, y, num_classes, alpha1.0): 训练伯努利朴素贝叶斯。 alpha: 拉普拉斯平滑系数防止 log(0) 返回 log_prior 和 log_likelihood N, D X.shape log_prior np.zeros(num_classes) log_likelihood np.zeros((num_classes, D)) # 存 log P(x_i1|ω) for c in range(num_classes): Xc X[y c] n_c Xc.shape[0] log_prior[c] np.log(n_c / N) # 先验类样本占比 # 每个像素为 1 的次数加 alpha 平滑 count_1 Xc.sum(axis0) alpha prob_1 count_1 / (n_c 2 * alpha) # 伯努利分母是 n 2*alpha log_likelihood[c] np.log(prob_1) return log_prior, log_likelihood逻辑说明log_prior用样本占比估计这是最大似然估计简单可靠。平滑系数alpha1.0就是拉普拉斯平滑等价于假设每个像素先验上见过一次 1 和一次 0分母n_c 2*alpha对应两种状态。参数上alpha调大让概率更平滑、抗过拟合调小更贴近数据但容易在稀疏维度上出问题作业里 0.5 到 1.0 都合理建议在报告里做个对比。3.3 预测对数似然求和再取最大预测时对每个样本、每个类别算对数后验取最大值的类别。注意伯努利模型要同时考虑像素为 1 和为 0 的贡献不能只加log P(x_i1)。def predict_bnb(X, log_prior, log_likelihood): 返回每个样本的预测标签。 # log P(x_i0|ω) log(1 - P(x_i1|ω)) log_like_0 np.log(1 - np.exp(log_likelihood) 1e-12) scores [] for c in range(log_prior.shape[0]): # x1 的位置加 log P(1)x0 的位置加 log P(0) s X log_likelihood[c] (1 - X) log_like_0[c] log_prior[c] scores.append(s) scores np.vstack(scores).T # (N, num_classes) return np.argmax(scores, axis1), scores逻辑说明X log_likelihood[c]用矩阵乘法一次性把「像素为 1 的那些维」的对数概率加起来(1 - X) log_like_0[c]处理像素为 0 的维两者相加再加先验就是完整对数后验。1e-12是防止log(0)的兜底。参数上scores返回出来可以画混淆矩阵、看每类置信度答辩时很有用。3.4 划分数据集并跑通完整流程from sklearn.model_selection import train_test_split X, y, classes load_images(data, img_size(32, 32)) X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.3, random_state42, stratifyy) # stratify 保证类别比例 log_prior, log_likelihood train_bnb(X_train, y_train, num_classeslen(classes)) y_pred, scores predict_bnb(X_test, log_prior, log_likelihood) acc (y_pred y_test).mean() print(测试集准确率: %.4f % acc)逻辑说明stratifyy保证训练测试集类别比例一致类别不均衡时尤其重要。random_state固定后结果可复现报告里写清楚。跑通后你会得到一个基线准确率接下来所有优化都跟它比。4. 参数怎么调、结果怎么读让作业从「能跑」到「能讲」4.1 平滑系数和图像尺寸的联合影响alpha和img_size是两个最该做实验的参数。尺寸太小像素独立性假设的破坏被掩盖但信息也少尺寸太大维度爆炸平滑项的影响被稀释。建议做一张二维对比表img_sizealpha0.1alpha0.5alpha1.0alpha2.016×16待测待测待测待测32×32待测待测待测待测64×64待测待测待测待测把实测准确率填进去你会发现小尺寸下 alpha 影响不明显大尺寸下 alpha 太小会掉点。这张表放进报告比一句「调参后效果提升」有说服力得多。4.2 用混淆矩阵定位「哪两类在互相误判」准确率是个标量看不出问题在哪。画混淆矩阵能直接暴露易混类别。from sklearn.metrics import confusion_matrix import numpy as np cm confusion_matrix(y_test, y_pred) print(cm) # 归一化后看每类的召回率 cm_norm cm / cm.sum(axis1, keepdimsTrue) for i, cls in enumerate(classes): print(%s 召回率: %.3f % (cls, cm_norm[i, i]))逻辑说明对角线是正确分类非对角线是误判。如果某两类互相误判严重说明它们的像素分布太接近可以考虑加特征比如投影直方图或换高斯模型。参数上cm.sum(axis1)是每类真实样本数归一化后每行和为 1读起来直观。4.3 和高斯朴素贝叶斯对比什么时候该换模型伯努利版把像素压成 0/1丢掉了灰度信息。如果图片灰度层次丰富高斯版可能更好。用sklearn的GaussianNB快速对比from sklearn.naive_bayes import GaussianNB gnb GaussianNB() gnb.fit(X_train, y_train) # 注意这里用未二值化的灰度特征更合适 acc_g gnb.score(X_test, y_test) print(高斯朴素贝叶斯准确率: %.4f % acc_g)逻辑说明GaussianNB对每类每维估计均值和方差适合连续特征。对比时要用原始灰度矩阵而不是二值矩阵否则高斯假设没意义。参数上var_smoothing是它的平滑项默认 1e-9数据尺度大时可以调大。两个模型都跑报告里写清楚各自适用条件这是加分项。5. 避坑与排查图片贝叶斯分类器最容易翻车的五个地方5.1 现象准确率永远等于多数类占比原因先验没乘或者似然计算时所有类别得分几乎一样argmax退化成选样本最多的类。解决打印scores看各类得分差距确认log_prior不是全 0检查log_likelihood是否因为平滑过大被抹平。5.2 现象训练集准确率 100%测试集惨不忍睹原因alpha太小甚至为 0模型把训练集每个像素的取值都当成必然过拟合。解决把alpha调到 0.5 以上或者做交叉验证选alpha。别用训练集准确率当卖点那是自欺欺人。5.3 现象程序报RuntimeWarning: divide by zero或结果全是nan原因某类某维方差为 0高斯版或概率为 0伯努利版取对数炸掉。解决伯努利版加alpha平滑高斯版加var_smoothing预测时对log参数加1e-12兜底。5.4 现象换了数据集准确率暴跌原因新数据集图片尺寸、通道、背景不一致load_images里resize和convert(L)没覆盖所有情况或者二值化阈值不适合新图。解决先可视化几张读进来的矩阵确认预处理没把图搞坏阈值改成按图像均值自适应。5.5 现象预测结果每次运行都不一样原因没固定random_state或者os.listdir顺序在不同系统上不一致导致标签错位。解决train_test_split加random_state类别用sorted固定顺序标签和类别名的映射写死并保存。6. 把作业做成能复用的工具保存模型与批量预测新图跑通一次不算完能对新图片直接出结果才算落地。把训练好的log_prior、log_likelihood和类别名存下来下次不用重训。import pickle def save_model(path, log_prior, log_likelihood, classes, img_size, threshold): with open(path, wb) as f: pickle.dump({ log_prior: log_prior, log_likelihood: log_likelihood, classes: classes, img_size: img_size, threshold: threshold, }, f) def load_and_predict(model_path, img_path): with open(model_path, rb) as f: m pickle.load(f) img Image.open(img_path).convert(L).resize(m[img_size]) arr (np.asarray(img, dtypenp.float32) m[threshold]).astype(np.float32) x arr.ravel().reshape(1, -1) pred, scores predict_bnb(x, m[log_prior], m[log_likelihood]) return m[classes][pred[0]], scores[0]逻辑说明pickle把模型参数和预处理配置一起存避免「训练时 32×32、预测时 64×64」这种低级错位。load_and_predict复用同一套预处理保证线上线下一致。参数上img_size和threshold必须和训练时完全一致这是最容易忽略的坑。一个具体技巧如果你想让报告更有深度可以统计每个类别被判错时「哪些像素贡献了最大的错误对数似然」把那些像素在原图上标出来。做法是把log_likelihood[true] - log_likelihood[pred]按像素展开成二维图看差值最大的区域。这能直观解释「模型到底被图的哪个部分骗了」比单纯报准确率有意思得多。我自己做这类作业时习惯先把基线跑通、把混淆矩阵和参数对比表填满再动任何花哨想法——因为大部分失分不是模型不够强而是预处理和参数没交代清楚。希望帮到你。本文还有配套的精品资源点击获取