资讯动态

从零手写高斯朴素贝叶斯:糖尿病检测实战与sklearn对照

发布时间:2026/10/19 10:04:25 来源:尧图企业网站定制
简介这份PDF文档面向模式识别、机器学习入门学习者与课程设计者围绕朴素贝叶斯分类器在医疗场景的落地展开完整呈现糖尿病检测系统的设计思路与实现路径。资源包仅含1个PDF文件体积约771KB内容涵盖贝叶斯定理分类原理、Pima Indians数据集说明、特征提取与高斯概率计算、分类预测流程、准确率评估与可视化分析并附有loadCsv、SplitDataset、summarizeByClass、calculateProbability等核心函数的代码实现与注释。读者可借此理解条件概率累乘、均值方差标准差估计、高斯概率密度函数应用等关键环节掌握从数据加载、训练测试拆分到预测评估的完整链路同时获得硬件软件环境配置参考与图表展示思路。目前已有235人学习适合希望以具体项目吃透朴素贝叶斯算法、完成课程大作业或夯实统计学习基础的读者参考。1. 从一份课程设计拆出来的朴素贝叶斯糖尿病检测它到底能跑出什么结果很多人第一次接触朴素贝叶斯都是在模式识别或者机器学习课的课程设计里。这份《基于贝叶斯定理的糖尿病检测系统的设计与实现》就是典型的课程设计产物——用 Python 从零手写高斯朴素贝叶斯不依赖 sklearn 的GaussianNB把加载数据、拆分训练测试集、按类别汇总均值方差、高斯概率密度计算、连乘求类别概率、预测与准确率评估整条链路全部自己实现一遍最后再用线性回归和散点图做特征与发病结果的关联分析。它解决的不是“工业级糖尿病诊断”问题而是“让你真正搞懂朴素贝叶斯每一步在算什么”的问题。适合正在做模式识别大作业的学生、想从零手推一遍贝叶斯分类器的开发者以及需要一份可复现基线代码来对照 sklearn 结果的从业者。数据集用的是皮马印第安人糖尿病数据442 条记录、9 项数值型生理指标所有属性单位不统一这也是后面要讲标准化坑的根源。2. 高斯朴素贝叶斯的数学链路从条件概率到连乘决策2.1 为什么选高斯而不是多项式或伯努利朴素贝叶斯有三个常见变体多项式、伯努利、高斯。选哪个不是拍脑袋而是看特征的数据类型。多项式适合词频这类离散计数伯努利适合二值特征而皮马印第安数据集里的年龄、BMI、血清化验值全是连续数值单位还各不相同。对连续值做概率估计常见做法是假设它服从某个分布最省事也最常用的就是正态分布——只需要均值和标准差两个参数就能描述整条曲线。这就是高斯朴素贝叶斯的选型理由不假设特征之间独立虽然“朴素”这个词本身就意味着独立假设而是假设每个特征在给定类别下的条件分布是正态的。具体到这份代码summarizeByClass()先按类别把数据分开再对每个类别下的每个属性调用mean()和stdev()用zip(*dataset)把列转置后逐列计算。这里有个容易忽略的细节summarize()里最后有一句del summaries[-1]目的是把类别标签那一列从统计结果里删掉因为标签不是特征不参与概率计算。如果你自己写的时候忘了删后面calculateClassProbabilities()遍历classSummaries时就会多算一个属性虽然不报错但概率会偏。2.2 高斯概率密度函数的代码实现与参数含义核心公式就是标准正态分布的概率密度函数F(x) 1/(σ√(2π)) · exp(-(x-μ)²/(2σ²))代码里拆成两步算先算指数部分再算前面的系数import math def calculateProbability(x, mean, stdev): # 指数部分exp(-(x-μ)²/(2σ²)) exponent math.exp(-(math.pow(x - mean, 2) / (2 * math.pow(stdev, 2)))) # 系数部分1/(σ√(2π))再乘指数部分 return (1 / (math.sqrt(2 * math.pi) * stdev)) * exponent参数说明x是待预测样本在某个特征上的取值mean和stdev是该特征在某个类别下的均值和标准差来自训练集的汇总结果。math.pow(stdev, 2)就是方差 σ²。这里有个血泪经验如果某个特征在某个类别下的标准差为 0比如某个属性所有值都一样stdev做分母会直接抛ZeroDivisionError。皮马数据集里不太会出现这种情况但换成自己的数据就要加一个极小值兜底常见做法是stdev max(stdev, 1e-6)。2.3 连乘求类别概率与最大值决策有了单特征的条件概率接下来要算整个样本属于某个类别的概率。朴素贝叶斯的“朴素”就体现在这里假设各特征相互独立所以联合概率等于各特征条件概率的连乘。def calculateClassProbabilities(summaries, inputVector): probabilities {} for classValue, classSummaries in summaries.items(): probabilities[classValue] 1 # 初始化为1连乘的幺元 for i in range(len(classSummaries)): mean, stdev classSummaries[i] x inputVector[i] # 逐个特征概率连乘 probabilities[classValue] * calculateProbability(x, mean, stdev) return probabilities 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逻辑说明probabilities字典的 key 是类别0 表示五年后不发病1 表示发病value 是该样本属于这个类别的联合概率。predict()遍历字典找最大值对应的类别。这里有个理论上的关键点——贝叶斯公式的分母 P(x) 对所有类别都是一样的所以比较分子就够了不需要真的去除以分母。这也是为什么代码里从头到尾没有出现 P(x) 的计算。提示连乘容易下溢。特征多的时候几十个小于 1 的概率相乘会趋近于 0float 精度扛不住。常见做法是取对数把连乘变连加即 log P(x|c) log P(c)比较对数概率大小。这份课程设计特征只有 9 个暂时不会触发下溢但换成高维数据就必须改。2.4 数据加载与训练测试拆分的实操步骤数据加载用标准库csv就够了不需要 pandas。关键是把字符串转成 float否则后面算均值方差会报类型错误。import csv import random def loadCsv(): # 注意实际使用时把路径换成你自己的数据集路径 lines csv.reader(open(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 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% 测试这两个值不一致。我一般会固定用 0.8 或者做多次随机拆分取平均单次拆分的结果波动很大尤其是数据量只有几百条的时候。random.randrange(len(copy))配合copy.pop(index)实现无放回随机抽样保证训练集和测试集不重叠。主程序串起来就是if __name__ __main__: splitRatio 0.8 dataset loadCsv() trainingSet, testSet splitDataset(dataset, splitRatio) summaries summarizeByClass(trainingSet) predictions getPredictions(summaries, testSet) accuracy getAccuracy(testSet, predictions) print(Accuracy: , accuracy)getAccuracy()的逻辑很直白遍历测试集预测类别和真实类别一致就correct 1最后correct / len(testSet) * 100就是准确率。这个指标在类别不平衡时会有误导性皮马数据集里阳性样本比例不算极端所以还能用但换成极端不平衡的数据就要看召回率和 F1。3. 线性回归与特征可视化哪些指标真的和发病相关3.1 用 sklearn 加载数据集并构建 DataFrame课程设计后半部分换了一条路线不再手写贝叶斯而是用 sklearn 的load_diabetes加载数据再用线性回归逐个特征拟合看每个特征和 target 的关系。import numpy as np import pandas as pd import matplotlib.pyplot as plt from sklearn.datasets import load_diabetes data_diabetes load_diabetes() data data_diabetes[data] target data_diabetes[target] feature_names data_diabetes[feature_names] df pd.DataFrame(data, columnsfeature_names) df.info()load_diabetes返回的是一个字典三个 keydata是特征矩阵target是回归目标值feature_names是特征名列表。转成 DataFrame 之后df.info()可以看到每个特征都是 float64没有缺失值。注意这里的数据和前面 CSV 加载的皮马数据集不是同一份——load_diabetes是 sklearn 内置的另一个糖尿病数据集442 个样本、10 个特征target 是连续值而不是 0/1 类别。原文把两者混在一起讲实际上前半段分类用的是 CSV 数据后半段回归用的是 sklearn 内置数据读者自己复现时要注意区分。3.2 逐特征线性回归与散点图绘制核心思路是对每个特征单独建一个线性回归模型看它单独能解释多少 target 的变化用score()返回的 R² 衡量。from sklearn.linear_model import LinearRegression from sklearn.model_selection import train_test_split plt.figure(figsize(12, 25)) for i, col in enumerate(df.columns): # 取单列特征reshape 成二维因为 sklearn 要求二维输入 train_X df.loc[:, col].values.reshape(-1, 1) train_Y target linear_model LinearRegression() linear_model.fit(train_X, train_Y) score linear_model.score(train_X, train_Y) axes plt.subplot(5, 2, i 1) plt.scatter(train_X, train_Y, s5) # 画拟合直线 y kx b k linear_model.coef_ b linear_model.intercept_ x np.linspace(train_X.min(), train_X.max(), 100) y k * x b plt.plot(x, y, cred) axes.set_title(col : str(round(score, 3))) plt.tight_layout() plt.show()参数说明reshape(-1, 1)是把一维 Series 转成 n 行 1 列的二维数组sklearn 的fit()不接受一维输入。linear_model.coef_是回归系数 klinear_model.intercept_是截距 b。score()返回的是 R²越接近 1 说明这个特征单独对 target 的解释力越强。从结果看BMI 对应的 R² 明显高于其他特征年龄有一点点关系性别几乎无关——这和原文的分析结论一致。注意这里的 R² 是单特征对 target 的拟合优度不是分类准确率不能直接拿来和前面贝叶斯的准确率比较。两者衡量的是不同任务。3.3 特征与发病关系的解读边界从散点图和拟合直线能看出几件事BMI 和 target 呈明显正相关说明体重指数越高病情发展指标越差年龄有弱正相关性别那一列几乎是一条水平线说明在这个数据集里性别对 target 没有区分力。但这里有个容易翻车的地方——load_diabetes的 target 是连续值代表“一年后病情发展情况”不是“是否患病”的二分类标签。所以严格来说这部分做的是回归分析不是分类。原文把它和前面的贝叶斯分类放在一起讲逻辑上有点混读者自己写论文或报告时最好把两条线分开表述分类用皮马 CSV 数据回归用 sklearn 内置数据各自说明数据来源和任务类型。4. 避坑与排查这份代码最容易翻车的五个地方4.1 准确率每次跑都不一样现象同一份代码每次运行输出的 Accuracy 都在变有时候差好几个百分点。原因splitDataset()用的是random.randrange()做随机拆分没有固定随机种子每次训练集和测试集的划分都不同。数据量只有几百条时单次拆分的方差很大。解决在拆分前加random.seed(42)固定随机序列或者做 10 次随机拆分取平均准确率。更稳妥的做法是用 sklearn 的train_test_split配合stratify参数做分层抽样保证训练集和测试集里正负样本比例一致。4.2 标准差为零导致除零错误现象程序跑到calculateProbability()时抛ZeroDivisionError提示 division by zero。原因某个特征在某个类别下的所有取值完全相同stdev()算出来是 0高斯公式里 σ 做分母就炸了。解决在stdev()返回前加一个下限保护比如return max(math.sqrt(variance), 1e-6)。或者换数据前先检查每个类别下每个特征的方差方差为 0 的特征直接剔除。4.3 文件路径写死导致换机器就跑不了现象在别人电脑上运行报FileNotFoundError提示找不到D:\研究生课程\...\diabetes.data.csv。原因loadCsv()里用的是绝对路径而且路径里带中文和空格换一台机器路径就不存在了。解决把数据文件和代码放同一目录用相对路径open(diabetes.data.csv, r, encodingutf-8)。如果一定要用绝对路径用os.path.join()拼接避免手写反斜杠转义问题。4.4 连乘下溢导致概率全为零现象特征数量增加后probabilities[classValue]打印出来全是 0.0predict()返回的类别随机。原因多个小于 1 的概率连乘结果趋近于浮点数下限被截断为 0。解决改用对数概率把probabilities[classValue] * p改成probabilities[classValue] math.log(p)最后比较对数概率大小。注意math.log(0)会报错所以calculateProbability()返回前也要加下限保护。4.5 把回归 R² 当成分类准确率现象报告里写“模型准确率达到 0.5 以上”但贝叶斯分类的准确率和线性回归的 R² 被混在一起说。原因前半段分类任务用getAccuracy()算的是百分比准确率后半段回归任务用score()算的是 R²两个指标量纲和含义完全不同。解决报告里分开写。分类部分写“在皮马数据集上高斯朴素贝叶斯的分类准确率为 XX%”回归部分写“BMI 特征对 target 的单变量线性回归 R² 为 XX”。不要跨任务比较指标。5. 从手写版到 sklearn 对照验证一个我常用的自检习惯手写朴素贝叶斯最大的价值不是拿去生产而是拿来对照 sklearn 的GaussianNB验证自己每一步算得对不对。我一般会这么做先用自己写的loadCsv()和splitDataset()拿到训练集和测试集然后分别用手写版和 sklearn 版跑一遍比较两者的准确率差异。如果差超过 2 个百分点说明手写版某一步有问题通常是均值方差算错或者概率连乘时多算/少算了特征。from sklearn.naive_bayes import GaussianNB from sklearn.metrics import accuracy_score import numpy as np # 假设 trainingSet 和 testSet 已经通过前面的 splitDataset 拿到 train_X np.array([row[:-1] for row in trainingSet]) train_y np.array([row[-1] for row in trainingSet]) test_X np.array([row[:-1] for row in testSet]) test_y np.array([row[-1] for row in testSet]) # sklearn 高斯朴素贝叶斯 gnb GaussianNB() gnb.fit(train_X, train_y) sk_pred gnb.predict(test_X) sk_acc accuracy_score(test_y, sk_pred) print(sklearn Accuracy:, sk_acc * 100) # 手写版 summaries summarizeByClass(trainingSet) my_pred getPredictions(summaries, testSet) my_acc getAccuracy(testSet, my_pred) print(手写版 Accuracy:, my_acc)这段对照代码的关键在于两边必须用完全相同的训练集和测试集否则比较没有意义。GaussianNB默认也是用均值和方差做高斯概率估计和手写版理论一致所以准确率应该非常接近。如果手写版明显偏低优先检查summarize()里有没有正确del summaries[-1]以及calculateClassProbabilities()里inputVector的索引有没有和classSummaries对齐。还有一个我踩过的坑sklearn 的GaussianNB默认会使用先验概率priorsNone也就是从训练数据里统计类别频率作为先验。手写版代码里没有显式乘先验概率 P(c)只算了似然 P(x|c)。如果训练集里正负样本比例不均衡手写版和 sklearn 版的结果会有偏差。要完全对齐手写版需要在calculateClassProbabilities()里把probabilities[classValue]初始化为该类别的先验概率而不是 1。从那以后我每次手写分类器都会强制走一遍“手写版 vs sklearn 版”的对照流程准确率对不上就先查先验、再查方差计算、最后查特征索引。这个习惯帮我省了很多翻论文找公式的时间。希望帮到你。本文还有配套的精品资源点击获取

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

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

免费获取报价 →
↑