简介:这份PDF面向模式识别与机器学习入门学习者,围绕朴素贝叶斯分类器在医疗预测场景中的落地展开,帮助读者理解如何用统计学原理完成糖尿病患病风险判断。文档完整呈现系统设计脉络:从贝叶斯定理分类原理、Pima Indians糖尿病数据集说明,到数据拆分、按类别汇总均值方差标准差、高斯概率密度计算与概率累乘预测,再到准确率评估和特征与发病关系的图表可视化,并附有loadCsv、SplitDataset、separateByClass、summarizeByClass、calculateProbability等关键函数代码及运行环境说明。资源包共1个PDF文件,大小约771KB,适合作为课程大作业参考或算法复现蓝本。目前已有236人学习,读者可据此掌握朴素贝叶斯从数据预处理到模型评估的完整实现思路,并借鉴其代码组织与结果分析方式。
1. 从一份“跑不通”的课设说起:贝叶斯糖尿病检测系统到底要交付什么
很多同学拿到“基于贝叶斯定理的糖尿病检测系统”这个题目,第一反应是打开编辑器抄一段朴素贝叶斯公式,跑出个准确率就交差。真到答辩现场,导师问一句“你的先验概率怎么来的、高斯假设凭什么成立、90% 训练集和 67% 训练集差多少”,当场卡壳。这套方案要解决的不是“能不能跑”,而是把数据加载、按类汇总、高斯条件概率、连乘预测、准确率评估、特征可视化串成一条可复现、可解释、可验收的链路。它适合两类人:正在做模式识别或机器学习课程设计、需要一份结构完整方案的学生,以及想用最小依赖手写一遍朴素贝叶斯、把黑匣子拆开看的从业者。下面按需求、设计、实现、避坑、验收的顺序推演,关键代码可直接对照改。
2. 需求与数据底座:皮马数据集、环境约束与流程拆解
2.1 系统要回答的三个问题
这套系统的业务目标很朴素:给定一位受检者的若干生理指标,判断其在观察窗口内是否属于发病类别。落到工程上,它必须回答三个问题。第一,输入是什么——年龄、BMI、血清化验等数值型特征,单位不统一,且存在缺失或异常值。第二,输出是什么——一个二分类标签,0 表示未发病、1 表示发病,同时给出模型在测试集上的准确率作为可信度参考。第三,凭什么信——不能只报一个数字,还要说清概率是怎么算出来的、哪些特征和结果关系更强。
原始方案把流程定为:载入数据 → 按比例拆分训练集与测试集 → 按类别汇总均值与标准差 → 用高斯分布计算条件概率 → 连乘得到类别概率 → 取最大概率类别 → 对比真实标签算准确率 → 图表展示特征与结果关系。这条链路没有调包,全部手写,好处是每一步都能在答辩时讲清楚,坏处是任何一步写错都会让准确率变成玄学。我一般会先把这条流程画成一张文字版流程图贴在文档里,再逐段落代码,避免写着写着忘了分母不变、只需比较分子这个关键简化。
2.2 数据集描述与字段口径
方案使用的是皮马印第安人糖尿病相关数据集,规模在数百条观察记录量级,字段包括年龄、性别、BMI 以及若干血清化验指标,最后一位是类别标签。所有属性都是数值型,但量纲不同,比如年龄是岁、BMI 是体重身高比、血清指标单位各异。这一点直接决定了不能用欧氏距离之类的度量做相似度,而高斯朴素贝叶斯恰好只依赖每个特征在各类别下的均值和标准差,对量纲不敏感,这是选型上比较顺的一点。
需要提前说清一个容易翻车的点:原始正文里同时出现了“皮马数据集”和通过 sklearn 加载的糖尿病回归数据集,两者字段和任务并不相同。前者是分类任务、带类别标签;后者常用于回归演示、目标值是连续病情指标。方案里把前者作为分类主数据,后者只用于特征与目标的散点拟合展示,两者不要混在一个评估口径里,否则准确率数字没有意义。
| 环节 | 输入 | 输出 | 关键约束 |
|---|---|---|---|
| 数据加载 | CSV 文件 | 二维数值列表 | 字符串转 float,路径与编码固定 |
| 数据拆分 | 全量数据、拆分比例 | 训练集、测试集 | 随机打乱,比例可配 |
| 按类汇总 | 训练集 | 每类每特征的均值、标准差 | 标签列不参与统计 |
| 概率预测 | 汇总字典、测试样本 | 预测标签列表 | 连乘取最大 |
| 评估展示 | 测试集、预测列表 | 准确率、特征图表 | 预测与真实逐条比对 |
2.3 软硬件环境与依赖边界
原始方案的环境是普通笔记本级别的 CPU、8GB 内存、Windows 系统,开发工具为 PyCharm 社区版,语言为 Python。这个配置说明系统不依赖 GPU,也不依赖大型深度学习框架,核心只用标准库 csv、random、math,可视化部分引入 numpy、pandas、matplotlib,回归展示部分引入 sklearn。依赖边界要写进文档:分类主流程零第三方机器学习库,保证算法可讲;可视化与回归展示属于辅助分析,允许调包。这样既满足“自己实现贝叶斯”的要求,又不至于连画图都手搓。
环境准备通常的做法是建一个干净虚拟环境,固定 Python 版本,把依赖写进 requirements。下面这段只装辅助分析需要的包,主流程不需要它们也能跑。
# 建议在虚拟环境中执行,避免污染全局环境 python -m venv venv # Windows 激活 venv\Scripts\activate # 安装可视化与回归展示所需依赖 pip install numpy pandas matplotlib scikit-learn参数说明:虚拟环境目录名可自定;如果只跑分类主流程,其实一个包都不用装,标准库足够。这里把 sklearn 单独列出,是因为后面的线性回归展示会用到它,和分类评估是两条线,文档里要分开写,别让读者误以为贝叶斯部分也依赖 sklearn。
3. 总体设计:五个函数模块与一条预测主链
3.1 模块划分与职责边界
整套系统按职责切成五块:数据加载模块负责把 CSV 读成数值矩阵;数据拆分模块负责随机划分训练集与测试集;统计汇总模块负责按类别计算每个特征的均值与标准差;概率预测模块负责高斯条件概率计算、连乘与取最大;评估展示模块负责准确率计算和图表输出。五块之间只通过列表和字典传递数据,不共享全局状态,这样每一块都能单独测试。
模块划分的价值在答辩时特别明显。导师问“你怎么保证拆分是随机的”,你指拆分模块;问“标准差为什么除以 n-1”,你指统计汇总模块;问“概率连乘会不会下溢”,你指概率预测模块。职责清楚,问题就能定位到具体函数,而不是一句“代码里就是这么写的”。
3.2 预测主链的数据流
主链的数据流可以这样描述:加载得到全量数据集,每条记录是“特征列表 + 标签”;拆分后训练集进入按类汇总,产出形如“类别 → 各特征(均值, 标准差)”的字典;测试集逐条进入预测,对每个类别把各特征的高斯概率密度连乘,得到该样本属于各类的得分,取最大得分对应类别作为预测;最后把预测列表与测试集真实标签逐条比对,正确数除以总数乘 100 得到准确率。
这里有一个设计上的简化必须讲透:贝叶斯公式的分母是样本在各特征上的联合边缘概率,对同一个样本的所有类别是同一个值,比较类别时只需比较分子,也就是“先验 × 各特征条件概率连乘”。原始方案在代码里直接把概率初始化为 1 再连乘,等价于假设各类先验相等,这在类别大致均衡时问题不大,但如果数据类别明显偏斜,准确率会偏向多数类。文档里要如实写出这个假设,并给出改进方向:用训练集中各类别占比作为先验乘进去。
3.3 关键参数与可调项
系统里真正需要调的参数不多,但每一个都影响结果。拆分比例决定训练充分程度与评估稳定性;随机种子决定拆分可复现;标准差计算的分母决定小样本下的方差估计是否无偏;概率连乘的数值范围决定是否需要取对数防下溢。把这些参数集中在一处配置,比散落在代码里强得多。
| 参数 | 典型取值 | 作用 | 调整建议 |
|---|---|---|---|
| splitRatio | 0.67 或 0.9 | 训练集占比 | 数据量小取 0.8 左右,多次运行看波动 |
| random seed | 固定整数 | 拆分可复现 | 写死并记录在文档 |
| 标准差分母 | n-1 | 样本标准差 | 小样本用 n-1,大样本差异可忽略 |
| 概率计算方式 | 直接连乘或取对数 | 防数值下溢 | 特征多时优先取对数再比较 |
提示:拆分比例不要只跑一次就写结论。同一份数据换几个随机种子各跑一遍,把准确率区间写进文档,比单点数字更有说服力。
4. 核心实现:从 CSV 加载到高斯概率连乘的完整代码
4.1 数据加载与类型转换
加载函数的目标是把 CSV 读成二维浮点列表。原始方案用 csv.reader 打开文件,逐行转列表,再对每个元素做 float 转换。这里有两个细节:文件路径不要写死绝对路径,改成相对路径或配置项,换机器才不会翻车;编码显式指定,避免中文路径或特殊字符报错。调试阶段可以打印前几行确认列数和标签位置,正式跑时把打印注释掉,否则几百行输出会把终端刷屏。
import csv def load_csv(path): """加载 CSV 数据集,返回二维浮点列表,最后一列为类别标签""" with open(path, "r", encoding="utf-8") as f: lines = csv.reader(f) dataset = list(lines) # 调试期可打印前 3 行确认结构,正式运行注释掉 # for row in dataset[:3]: # print(row) for i in range(len(dataset)): dataset[i] = [float(x) for x in dataset[i]] return dataset逻辑说明:with 语句保证文件句柄释放;list(lines) 把迭代器一次性转成列表,后续才能按下标随机取;双重循环里外层遍历行、内层把每个字符串转 float。参数 path 建议用相对路径,比如 "data/diabetes.csv",和代码放同一项目目录下,避免换机器后路径失效。如果数据里有缺失值占位符,float 转换会直接抛异常,这时要在转换前做一次清洗,把非法值替换为该特征的中位数,而不是简单跳过整行。
4.2 随机拆分与按类分组
拆分函数用“从副本里随机弹出直到训练集够数”的方式,保证训练集与测试集不重叠。按类分组函数则把同一标签的记录聚到一个列表里,为后续统计做准备。这两个函数都不复杂,但拆分比例和随机性直接影响评估结果,建议把随机种子固定下来。
import random def split_dataset(dataset, split_ratio, seed=42): """按比例随机拆分训练集与测试集""" random.seed(seed) # 固定种子,保证结果可复现 train_size = int(len(dataset) * split_ratio) train_set = [] copy = list(dataset) while len(train_set) < train_size: index = random.randrange(len(copy)) train_set.append(copy.pop(index)) return train_set, copy def separate_by_class(dataset): """按类别标签把数据分组,返回 类别 -> 记录列表 的字典""" separated = {} for vector in dataset: label = vector[-1] if label not in separated: separated[label] = [] separated[label].append(vector) return separated参数说明:split_ratio 取 0.67 时训练集约三分之二,取 0.9 时测试集很小、准确率波动大,文档里要说明选择理由。seed 固定后每次拆分一致,方便对比不同算法的结果;如果要做多次实验取平均,就在外层循环里改 seed。separate_by_class 用 vector[-1] 取标签,前提是标签固定在最后一列,这个约定要在数据说明里写死,避免换数据后错位。
4.3 均值、标准差与高斯条件概率
统计汇总对每个类别下的每个特征分别算均值和标准差,用 zip(*dataset) 把列转置后逐列计算。标准差用样本标准差,分母为 n-1。高斯概率密度函数按公式实现:先算指数部分,再乘前面的系数。这里最容易踩的坑是标准差为 0,除零会让程序直接崩,通常的做法是给标准差加一个极小值兜底。
import math def mean(numbers): return sum(numbers) / float(len(numbers)) def stdev(numbers): avg = mean(numbers) # 样本方差,分母 n-1 variance = sum([(x - avg) ** 2 for x in numbers]) / float(len(numbers) - 1) return math.sqrt(variance) def summarize(dataset): """对一组记录按列计算 (均值, 标准差),去掉标签列""" summaries = [(mean(col), stdev(col)) for col in zip(*dataset)] del summaries[-1] # 最后一列是标签,不参与统计 return summaries def summarize_by_class(dataset): separated = separate_by_class(dataset) summaries = {} for label, instances in separated.items(): summaries[label] = summarize(instances) return summaries def gaussian_probability(x, mu, sigma): """高斯概率密度,sigma 加极小值防止除零""" sigma = max(sigma, 1e-9) exponent = math.exp(-((x - mu) ** 2) / (2 * sigma ** 2)) return (1 / (math.sqrt(2 * math.pi) * sigma)) * exponent逻辑说明:zip(*dataset) 把“行列表”转成“列元组”,每一列是一个特征在所有样本上的取值;summarize 对每列算均值和标准差后删掉最后一列标签。gaussian_probability 里 sigma 兜底是血泪经验,某些特征在某个类别下取值完全相同时标准差为 0,不兜底直接 ZeroDivisionError。参数 x 是待预测样本的某个特征值,mu 和 sigma 来自训练集汇总,三者单位一致才能代入。
4.4 连乘预测与准确率评估
预测阶段对每个类别把各特征的高斯概率连乘,得到得分,取最大得分对应类别。评估阶段逐条比对预测与真实标签,正确数除以测试集大小乘 100。特征较多时连乘结果会趋近于 0,比较时可能因浮点下溢失去区分度,改进做法是对每个概率取对数再相加,比较对数和,结果等价且数值稳定。
def class_probabilities(summaries, input_vector): """计算样本属于各类别的得分(先验相等时即条件概率连乘)""" probabilities = {} for label, class_summaries in summaries.items(): probabilities[label] = 1 for i in range(len(class_summaries)): mu, sigma = class_summaries[i] x = input_vector[i] probabilities[label] *= gaussian_probability(x, mu, sigma) return probabilities def predict(summaries, input_vector): probs = class_probabilities(summaries, input_vector) best_label, best_prob = None, -1 for label, prob in probs.items(): if best_label is None or prob > best_prob: best_prob = prob best_label = label return best_label def get_predictions(summaries, test_set): return [predict(summaries, row) for row in test_set] def get_accuracy(test_set, predictions): correct = sum(1 for i in range(len(test_set)) if test_set[i][-1] == predictions[i]) return (correct / float(len(test_set))) * 100.0参数说明:input_vector 是测试样本的特征列表,长度必须与汇总时特征数一致,标签列不参与。class_probabilities 里概率初始化为 1 再连乘,等价于各类先验相等;若要引入先验,把初始值改成训练集中该类占比即可。get_accuracy 返回百分比,测试集很小时一位小数都可能跳动,文档里建议同时报告测试集样本数。
4.5 主流程串联与运行入口
把上面几块串起来就是完整主流程:加载、拆分、汇总、预测、评估。入口处把拆分比例和随机种子显式写出,方便复现。运行后终端只输出准确率一行,数据集明细在调试期看,正式结果不刷屏。
if __name__ == "__main__": split_ratio = 0.67 dataset = load_csv("data/diabetes.csv") train_set, test_set = split_dataset(dataset, split_ratio, seed=42) summaries = summarize_by_class(train_set) predictions = get_predictions(summaries, test_set) accuracy = get_accuracy(test_set, predictions) print(f"测试集样本数: {len(test_set)}, 准确率: {accuracy:.2f}%")这段代码的价值在于它把前面所有函数串成一条可执行链路,任何一环改动都能立刻从准确率上看出来。我一般会先跑通这条链路,再去做可视化,顺序反过来容易在画图上耗掉大半时间,主流程却没验证。
5. 可视化与结果分析:特征关系图怎么画才不误导
5.1 特征与目标的散点拟合
分类主流程给出的是准确率,但答辩时导师更想看“哪些特征和发病关系大”。原始方案用线性回归对每个特征单独拟合,画散点加拟合直线,并在子图标题里标出该特征的拟合得分。这种做法直观,但要注意:单特征线性拟合的得分低不代表该特征没用,只代表线性关系弱;贝叶斯利用的是分布差异,不是线性相关,两者结论不能互相替代。
import numpy as np import pandas as pd import matplotlib.pyplot as plt from sklearn.linear_model import LinearRegression def plot_feature_relations(df, target): """对每个特征单独做线性拟合,5 行 2 列子图展示""" plt.figure(figsize=(12, 20)) for i, col in enumerate(df.columns): x = df.loc[:, col].values.reshape(-1, 1) # 转二维供模型使用 model = LinearRegression().fit(x, target) score = model.score(x, target) ax = plt.subplot(5, 2, i + 1) ax.scatter(x, target, s=8) xs = np.linspace(x.min(), x.max(), 100) ys = model.coef_ * xs + model.intercept_ ax.plot(xs, ys, c="red") ax.set_title(f"{col}: {score:.3f}") plt.tight_layout() plt.show()逻辑说明:reshape(-1, 1) 把一维 Series 转成二维,满足线性回归输入要求;score 是该特征单变量拟合的决定系数,只作参考;散点透明度和小点尺寸避免重叠糊成一片。参数 df 是特征数据框,target 是目标列,两者行数必须对齐。这张图适合放在文档“结果分析”一节,配合文字说明“BMI 与目标关系较明显、性别几乎无线性关系”,但结论要限定在“线性视角下”,别写成因果。
5.2 准确率的解读边界
准确率是这套系统最容易被误读的指标。测试集只有几十条时,一条样本对错就能让准确率跳几个百分点;类别不均衡时,全预测多数类也能拿到不低的准确率。文档里至少要补两项:测试集样本数,以及按类别分开的预测正确情况。更稳妥的做法是多次随机拆分取平均,报告均值与波动范围,而不是单次结果。
注意:如果数据集中发病与未发病样本比例明显偏离 1:1,务必在文档里说明,并考虑引入先验或改用查准率、查全率补充评估,否则准确率数字会误导读者。
5.3 图表与结论的对应关系
图表不是装饰,每一张都要能回答一个具体问题。散点拟合图回答“单特征与目标有没有线性趋势”;准确率输出回答“整体判别能力如何”;如果再加一张按类别统计的预测混淆情况,就能回答“错判主要发生在哪一类”。三张图对应三个问题,文档结构就立住了。反过来,如果画了图却说不出它支撑什么结论,这张图在答辩时就是减分项。
6. 避坑与排查:这类手写贝叶斯最容易翻车的五个点
6.1 准确率忽高忽低,换次运行就变
现象:同一份代码跑两次,准确率差好几个百分点。原因:拆分用了随机打乱但没固定种子,训练集与测试集构成每次不同,小数据集下波动被放大。解决:在拆分函数入口固定 random.seed,把种子写进文档;同时做多组种子实验,报告准确率区间而非单点。我一般会在文档里放一张“种子-准确率”小表,三到五组即可,读者一看就知道结果稳不稳。
6.2 标准差为零导致程序崩溃
现象:运行到高斯概率计算时抛除零异常。原因:某个特征在某个类别下所有取值相同,样本标准差为 0,公式分母为零。解决:计算概率前给标准差加极小值兜底,或在汇总阶段检测零方差特征并记录。兜底值不宜过大,1e-9 量级足够,太大反而会把该特征的区分度抹平。
6.3 概率连乘下溢,预测全归一类
现象:特征一多,所有类别的连乘得分都变成 0,预测结果全落在同一个类别。原因:多个小于 1 的概率相乘迅速趋近浮点下限。解决:改为对每个高斯概率取对数后相加,比较对数和,数学上等价且数值稳定。改动只涉及概率计算与比较两处,汇总和评估不用动。
6.4 标签列混入统计,特征数对不上
现象:预测时报下标越界,或准确率异常低。原因:汇总时忘了删除最后一列标签,特征数比输入多一个;或换数据后标签不在最后一列。解决:在 summarize 里显式删除标签列,并在数据说明中固定“标签在最后一列”的约定;加载后打印一行数据的长度做自检。
6.5 两套数据混用,结论自相矛盾
现象:分类准确率一套数字,回归图表一套数据,文档里却当成同一个实验。原因:分类用的带标签数据集与回归展示用的数据集字段和任务不同,混在一个评估口径里。解决:文档分两节写,明确分类主实验与回归辅助展示各自的数据来源和目的,准确率只对分类实验负责,回归图只用于观察特征趋势。
7. 验收与答辩:怎么证明这套系统真的成立
验收这套系统,我通常按“可复现、可解释、可对比”三条走。可复现:换一台机器,按文档步骤装环境、放数据、跑入口,能得到与文档一致的准确率区间;拆分种子、数据路径、依赖版本都写在文档里。可解释:随便挑一条测试样本,能手工跟着代码走一遍——取该样本各特征值,代入训练集汇总的均值和标准差,算出各类得分,说明为什么判成这一类;这一步能过,说明你不是在调包。可对比:至少给出两组拆分比例或两组种子的结果,说明波动范围,而不是只报一个漂亮数字。
答辩时最容易被追问的三个问题,提前准备好答案。第一,“为什么用高斯朴素贝叶斯而不是别的分类器”——因为特征为连续数值、样本量不大、需要每一步可解释,高斯假设下只需均值和标准差,计算量小且能讲清;同时承认特征独立假设较强,是简化。第二,“准确率这个数字可信吗”——报测试集样本数、多次拆分区间,并说明类别是否均衡。第三,“这套东西有什么用”——定位为教学与方案验证原型,展示的是完整链路和可解释性,不是临床诊断工具,边界要划清。
下面这张验收清单可以直接贴进文档附录,逐项打勾。
| 验收项 | 通过标准 | 检查方式 |
|---|---|---|
| 环境可重建 | 按文档步骤能跑通 | 换目录重跑入口 |
| 结果可复现 | 固定种子结果一致 | 连续运行两次比对 |
| 单样本可解释 | 能手工复算类别得分 | 随机抽一条测试样本 |
| 波动有说明 | 报告多组种子区间 | 三到五组实验记录 |
| 边界写清楚 | 明确非临床用途 | 文档结论段自查 |
还有一个小技巧:答辩前把主流程输出改成“测试集样本数 + 准确率 + 各类别预测数”三行,现场跑一次给导师看,比 PPT 上的截图有说服力。如果时间允许,再准备一个故意把拆分比例改成 0.9 的对比运行,说明小测试集下准确率波动的原因,这比被动挨问强得多。
从那以后我每次带这类课设,都强制要求先跑通“加载—拆分—汇总—预测—评估”这条最小链路,再谈可视化和文档包装;链路不稳,后面全是空中楼阁。希望帮到你。
本文还有配套的精品资源,点击获取