资讯动态

手写高斯朴素贝叶斯做糖尿病预测:从原理到二次开发

发布时间:2026/10/27 3:07:00 来源:尧图企业网站定制
简介这份PDF面向模式识别与机器学习入门学习者围绕朴素贝叶斯分类器在医疗预测场景中的落地展开帮助读者理解如何用统计学原理完成糖尿病风险判别。内容涵盖贝叶斯定理分类原理、Pima Indians糖尿病数据集说明、特征均值方差与标准差计算、高斯概率密度估计、按类别汇总特征、概率连乘与最大后验判别以及准确率评估和图表可视化并附有loadCsv、SplitDataset、separateByClass、summarizeByClass、calculateProbability等关键函数的实现讲解可作为课程大作业或项目实践参考。资源包共1个PDF文件大小约771KB篇幅紧凑便于通读。已有236人学习适合希望把分类算法从公式推导推进到代码实现与结果分析的读者对照研读。1. 从一份课程大作业说起朴素贝叶斯做糖尿病预测到底能跑出什么前阵子帮一个做模式识别课程的朋友看代码他扔过来一个压缩包说“跑是能跑但准确率忽高忽低图表也画得乱七八糟”。打开一看是一个用纯 Python 手写朴素贝叶斯、对皮马印第安人糖尿病数据集做二分类的小系统外加一份用 sklearn 线性回归做特征可视化的脚本。没有框架、没有模型持久化、没有 Web 界面就是两个.py文件加一个 CSV但恰恰是这种“裸写算法”的项目最适合拿来把贝叶斯分类的每一步拆开看清楚。它适合两类人一是想搞明白高斯朴素贝叶斯内部到底怎么算的初学者二是需要一个干净基线做二次开发、往上叠特征工程或换模型的开发者。下面按“能干什么 → 怎么跑 → 怎么改”的顺序把这个项目从头拆一遍。2. 项目定位与技术选型为什么用纯手写而不是直接调 sklearn2.1 这个系统实际在解决什么问题把问题说白一点给一条病人的生理指标记录怀孕次数、血糖、血压、皮褶厚度、胰岛素、BMI、糖尿病家族史函数、年龄这 8 个特征外加一个 0/1 的类别标签判断这个人五年内会不会发病。这是一个标准的二分类任务特征全是数值型量纲不统一样本量不大。项目选朴素贝叶斯核心假设是“在给定类别的条件下各特征相互独立”于是联合概率可以拆成各特征条件概率的连乘计算量从指数级降到线性级。对这么小的数据集来说这个假设虽然不严谨BMI 和皮褶厚度显然相关但工程上够用而且训练阶段只需要存每个类别下每个特征的均值和标准差预测时套高斯公式即可没有迭代、没有梯度、没有调参属于“一次遍历就能出结果”的模型。项目正文里把流程写得很清楚载入 CSV → 按比例拆训练/测试 → 按类别分组 → 算每类每特征的均值与标准差 → 用高斯概率密度算条件概率 → 连乘得类别概率 → 取最大者 → 对比真实标签算准确率。这条链路没有任何黑盒每一行都能对应到公式这正是它作为教学与二次开发基线的价值。2.2 技术栈与运行环境技术栈非常轻Python 3 标准库csv、random、math可视化部分用numpy、pandas、matplotlib线性回归对比实验用scikit-learn。开发环境是 PyCharm Community Edition硬件是 i7-8550U 8GB 内存的普通笔记本也就是说这套东西在任何一台能装 Python 的机器上都能跑没有 GPU、没有数据库、没有服务端依赖。组件用途是否必需csv / random / math数据载入、随机拆分、高斯计算必需numpy / pandas线性回归脚本的数据组织可视化脚本必需matplotlib散点图与拟合直线可视化脚本必需scikit-learnLinearRegression 与 load_diabetes可视化脚本必需这里有个容易忽略的点主分类脚本其实完全不依赖第三方库只用标准库就能跑通。这意味着你可以把它拷到一台没装任何包的机器上验证算法逻辑排除了“到底是算法错了还是库版本不对”的干扰调试时这一点很省心。2.3 目录与文件组织项目本身没有复杂的工程结构按功能拆成两个入口文件加一份数据project/ ├── naive_bayes_diabetes.py # 主分类脚本载入、拆分、训练、预测、评估 ├── linear_regression_plot.py # 可视化脚本逐特征线性回归 散点拟合图 └── diabetes.data.csv # 皮马印第安人糖尿病数据集数值型逗号分隔主脚本里所有函数都是模块级函数没有类封装if __name__ __main__:里串起完整流程。这种写法在工程上不算优雅但对“读代码学算法”这个目标来说反而友好——从上往下读就是执行顺序。二次开发时我一般会先把这些函数收进一个GaussianNB类再把数据路径、拆分比例、随机种子抽成配置这一步后面第 5 章会具体讲。3. 核心算法拆解从 CSV 到准确率的六个函数3.1 数据载入与类型转换第一步是把 CSV 读成二维浮点列表。原始代码里路径写死成了绝对路径还带中文目录这是典型的“换台机器就跑不了”的写法先看原版import csv def loadCsv(): # 原版绝对路径 中文目录换机器必挂 lines csv.reader(open(rD:\课程\大作业\diabetes.data.csv, r, encodingutf-8)) dataset list(lines) for i in range(len(dataset)): dataset[i] [float(x) for x in dataset[i]] # 字符串转浮点 return dataset逻辑说明csv.reader逐行返回字符串列表list()一次性物化成二维列表然后逐元素float()转换。参数上唯一需要改的就是路径我一般改成相对路径./diabetes.data.csv或者用pathlib.Path(__file__).parent / diabetes.data.csv这样脚本挪到哪都能找到数据。另外原版在转换前有一行print打印整个数据集调试时有用正式跑一定要注释掉否则几千行输出会把终端刷爆也会拖慢启动。3.2 随机拆分训练集与测试集拆分用的是“从副本里随机弹出”的策略而不是先打乱再切片import random def splitDataset(dataset, splitRatio): trainSize int(len(dataset) * splitRatio) # 训练集条数 trainSet [] copy list(dataset) # 拷贝避免改动原数据 while len(trainSet) trainSize: index random.randrange(len(copy)) # 随机下标 trainSet.append(copy.pop(index)) # 弹出并放入训练集 return [trainSet, copy] # 剩余即测试集逻辑说明splitRatio是训练集占比正文里主流程用的是 0.67而描述里提到过 90%/10%这两个值都出现过实际以__main__里的赋值为准。copy.pop(index)保证同一条样本不会同时进训练和测试这是正确的。但这里有个隐患没有设随机种子每次运行拆分结果不同准确率自然会在几个百分点内浮动这就是我朋友说的“忽高忽低”。解决办法是在文件头加random.seed(42)或者在函数签名里加seed参数保证实验可复现。参数建议小数据集上 0.67~0.8 的训练占比比较稳0.9 会让测试集只剩几十条准确率方差更大。3.3 按类别分组与统计量汇总朴素贝叶斯训练阶段要的就是“每个类别下每个特征的均值和标准差”。先按标签分组def separateByClass(dataset): separated {} for i in range(len(dataset)): vector dataset[i] label vector[-1] # 最后一列是类别 if label not in separated: separated[label] [] separated[label].append(vector) return separatedvector[-1]取最后一列作为类别这是整个项目里唯一约定“标签在末尾”的地方改数据结构时务必同步。接着算均值和标准差import math def mean(numbers): return sum(numbers) / float(len(numbers)) def stdev(numbers): avg mean(numbers) variance sum([pow(x - avg, 2) for x in numbers]) / float(len(numbers) - 1) return math.sqrt(variance) def summarize(dataset): summaries [(mean(attr), stdev(attr)) for attr in zip(*dataset)] del summaries[-1] # 去掉标签列对应的统计量 return summarieszip(*dataset)是这里的关键技巧把二维列表转置得到“每个特征一列”然后对每列求均值与标准差。del summaries[-1]删掉的是标签列因为标签本身不该参与高斯计算。注意stdev用的是样本标准差除以 n-1样本量小时比总体标准差略大属于合理选择。summarizeByClass只是把分组和汇总串起来返回{类别: [(均值, 标准差), ...]}的字典这个字典就是整个模型的“全部参数”训练到此结束。3.4 高斯概率密度与类别概率连乘预测阶段对每个特征套高斯公式def calculateProbability(x, mean, stdev): exponent math.exp(-(math.pow(x - mean, 2) / (2 * math.pow(stdev, 2)))) return (1 / (math.sqrt(2 * math.pi) * stdev)) * exponent对应公式f(x) 1/(σ√(2π)) · exp(-(x-μ)²/(2σ²))。参数说明x是待预测样本在该特征上的取值mean、stdev来自训练阶段该类别该特征的统计量。这里有个必须防的坑如果某个特征在某个类别下所有样本取值相同stdev会等于 0除零直接抛异常。稳妥做法是在stdev返回前加一个下限比如return max(math.sqrt(variance), 1e-9)或者对这类特征直接跳过。连乘得到类别概率def calculateClassProbabilities(summaries, inputVector): probabilities {} for classValue, classSummaries in summaries.items(): probabilities[classValue] 1 for i in range(len(classSummaries)): mean, stdev classSummaries[i] x inputVector[i] probabilities[classValue] * calculateProbability(x, mean, stdev) return probabilities注意这里算的是“似然连乘”没有乘先验概率也没有除以证据因子。因为分母对所有类别相同比较大小不影响结果但如果两个类别样本数严重不均衡忽略先验会让预测偏向多数类。二次开发时如果想更严谨可以在循环前乘上P(classValue) 该类样本数 / 总样本数改动只有一行。3.5 取最大概率与准确率评估def predict(summaries, inputVector): probabilities calculateClassProbabilities(summaries, inputVector) bestLabel, bestProb None, -1 for classValue, probability in probabilities.items(): if bestLabel is None or probability bestProb: bestProb probability bestLabel classValue return bestLabel def getAccuracy(testSet, predictions): correct 0 for x in range(len(testSet)): if testSet[x][-1] predictions[x]: correct 1 return (correct / float(len(testSet))) * 100.0predict遍历字典找最大值getAccuracy逐条比对真实标签与预测标签返回百分比。主流程把这几步串起来if __name__ __main__: random.seed(42) # 建议加固定拆分结果可复现 splitRatio 0.67 dataset loadCsv() trainingSet, testSet splitDataset(dataset, splitRatio) summaries summarizeByClass(trainingSet) predictions getPredictions(summaries, testSet) accuracy getAccuracy(testSet, predictions) print(Accuracy: , accuracy)跑通后终端会打印一个准确率数字通常在 70%~76% 区间波动加种子后固定。这个数字不算高但对朴素贝叶斯在这个数据集上的表现来说是正常水平别指望调到 95%那是过拟合或者数据泄漏的信号。3.6 线性回归可视化脚本在做什么第二个脚本和分类模型是两套东西它用sklearn.datasets.load_diabetes载入另一份糖尿病数据442 条、10 个特征、一个连续的目标值然后对每个特征单独做一元线性回归画散点加拟合直线标题上带 R² 分数import numpy as np import matplotlib.pyplot as plt from sklearn.datasets import load_diabetes from sklearn.linear_model import LinearRegression data_diabetes load_diabetes() data data_diabetes[data] target data_diabetes[target] feature_names data_diabetes[feature_names] plt.figure(figsize(12, 25)) for i, col in enumerate(feature_names): train_X data[:, i].reshape(-1, 1) # 一维转二维模型要求 model LinearRegression() model.fit(train_X, target) score model.score(train_X, target) # R² axes plt.subplot(5, 2, i 1) plt.scatter(train_X, target, s8) k, b model.coef_[0], model.intercept_ x np.linspace(train_X.min(), train_X.max(), 100) plt.plot(x, k * x b, cred) axes.set_title(f{col}: {score:.3f}) plt.tight_layout() plt.show()逻辑说明reshape(-1, 1)是因为LinearRegression要求二维输入model.score返回的是 R²越接近 1 说明单特征线性关系越强。从图上能直观看到 BMI 那一格的散点有明显斜率而性别那一格几乎是一条水平线这正好对应正文里“发病概率和性别无关、和 BMI 关系很大”的结论。参数上figsize和subplot(5, 2, ...)要匹配特征数量特征多于 10 个时得改成6 x 2之类否则图会挤在一起。4. 跑起来与避坑环境、路径、随机性三处最容易翻车4.1 从零跑通的最小步骤先把数据文件放到脚本同级目录然后按顺序执行# 1. 建虚拟环境可选但推荐 python -m venv venv source venv/bin/activate # Windows 用 venv\Scripts\activate # 2. 装可视化脚本需要的包主脚本只用标准库 pip install numpy pandas matplotlib scikit-learn # 3. 跑分类主脚本 python naive_bayes_diabetes.py # 4. 跑可视化脚本 python linear_regression_plot.py第 3 步终端输出Accuracy: xx.x即成功第 4 步会弹出一个 5 行 2 列的子图窗口。如果只想验证算法第 2 步可以跳过主脚本照样能跑这一点在排查“是不是库的问题”时很有用。4.2 避坑与常见问题排查现象一FileNotFoundError提示找不到 CSV。原因几乎都是原版写死的绝对路径在别的机器上不存在或者中文目录在部分终端下编码异常。解决改成相对路径并用pathlib定位脚本所在目录同时确认 CSV 文件名大小写一致。现象二每次运行准确率都不一样差好几个点。原因是splitDataset用了random.randrange但没设种子训练/测试划分每次不同。解决在__main__开头加random.seed(42)或者把种子作为参数传进拆分函数做对比实验时尤其要固定。现象三ZeroDivisionError或概率全为 0。前者是某特征在某类别下标准差为 0后者是连乘太多小概率导致浮点下溢。解决标准差加下限1e-9连乘改成对数求和即sum(math.log(p))再比较数值稳定性会好很多。现象四可视化脚本报reshape相关错误。原因是把一维数组直接喂给了LinearRegression。解决确认data[:, i].reshape(-1, 1)这一步没被删掉sklearn 的模型接口统一要求二维特征矩阵。现象五图表中文标题显示成方块。matplotlib 默认字体不含中文。解决要么标题全用英文特征名推荐feature_names本来就是英文要么显式设置plt.rcParams[font.sans-serif] [SimHei]并处理负号显示。提示改完路径和种子后先把主脚本单独跑三遍确认准确率稳定再动可视化脚本两个脚本分开验证能省掉一半排查时间。5. 二次开发把脚本收成类、补上先验与对数概率5.1 从函数式脚本到可复用类原版所有函数平铺在模块里状态靠参数传递想同时持有两个模型就得复制变量。我一般第一步先收成一个类把训练产物summaries和类别先验挂在实例上import math, random class GaussianNB: def __init__(self, eps1e-9): self.eps eps self.summaries {} self.priors {} def fit(self, dataset): separated {} for row in dataset: separated.setdefault(row[-1], []).append(row) total len(dataset) for label, rows in separated.items(): self.priors[label] len(rows) / total # 补上先验 cols list(zip(*rows)) stats [] for col in cols[:-1]: # 跳过标签列 mu sum(col) / len(col) var sum((x - mu) ** 2 for x in col) / (len(col) - 1) stats.append((mu, max(math.sqrt(var), self.eps))) self.summaries[label] stats return self def _log_prob(self, x, mu, sigma): return -math.log(sigma * math.sqrt(2 * math.pi)) - ((x - mu) ** 2) / (2 * sigma ** 2) def predict_one(self, row): best_label, best_score None, -float(inf) for label, stats in self.summaries.items(): score math.log(self.priors[label]) # 先验取对数 for i, (mu, sigma) in enumerate(stats): score self._log_prob(row[i], mu, sigma) # 对数似然累加 if score best_score: best_score, best_label score, label return best_label改动点有三个先验概率进模型、连乘换对数累加、标准差加下限。对数化之后不再有下溢风险先验让类别不均衡时预测更合理。验证方法和原版一致用同一份拆分数据对比准确率正常情况下差异在 1~2 个百分点内如果差很多先检查priors是不是算错了。5.2 值得往上叠的几个方向第一是交叉验证替代单次拆分把splitDataset换成 k 折准确率报均值加标准差比单次数字可信得多。第二是特征标准化虽然高斯贝叶斯对量纲不敏感每个特征独立算均值和方差但做特征筛选时标准化后的系数更好比较。第三是把可视化脚本里的单特征 R² 做成特征重要性排序用来决定哪些特征值得保留。第四是加一个简单的命令行入口用argparse接收数据路径和拆分比例避免每次改代码。改造点改动位置预期收益固定随机种子__main__或拆分函数结果可复现对数概率calculateClassProbabilities避免下溢类别先验训练阶段不均衡数据更稳k 折交叉验证主流程评估更可信5.3 一个我踩过的验证习惯这个项目最容易被忽略的不是算法而是“改了参数之后到底有没有变好”这件事本身没法判断——因为每次拆分不同、没有基线、没有固定种子。我最早做类似的小模型实验时改完先验、改完对数化跑出来准确率从 73% 变成 71%一度以为改坏了回头一查是随机拆分换了测试集。从那以后我每次动模型参数前都强制先把数据拆分结果落盘成固定的训练/测试文件所有对比都在这两份文件上跑改完再换种子复验一遍。这个习惯看着笨但能省掉大量“玄学调参”的时间。希望这个拆解能帮你把这个小系统跑顺也能顺着这几个改造点把它变成自己实验的基线。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑