从零手写朴素贝叶斯:糖尿病预测实战与避坑指南

发布时间:2026/10/19 2:25:22
从零手写朴素贝叶斯:糖尿病预测实战与避坑指南
简介这份PDF文档面向模式识别、机器学习入门学习者与课程设计者围绕朴素贝叶斯分类器在医疗场景中的落地展开解决如何用统计学方法从生理指标中预测糖尿病发病风险的问题。资源包共1个PDF文件大小约771KB内容为完整的课程设计报告涵盖贝叶斯定理分类原理、Pima Indians糖尿病数据集说明、特征提取与高斯概率计算、分类预测流程、准确率评估与图表可视化并附有loadCsv、SplitDataset、summarizeByClass、calculateProbability等核心函数的代码实现与流程图。读者可借此掌握从数据加载、训练测试拆分到条件概率累乘、最大概率判类的完整建模链路理解高斯概率密度函数在连续特征上的应用方式同时获得一份可直接参考的实验报告结构与代码模板。目前已有235人学习适合需要完成模式识别大作业或希望快速理解朴素贝叶斯实战流程的读者。1. 从一份课程大作业说起朴素贝叶斯做糖尿病预测到底靠不靠谱一份模式识别课程的大作业用朴素贝叶斯在皮马印第安人糖尿病数据集上跑分类准确率能到多少很多人第一反应是“这数据集都被人跑烂了还能有什么新东西”。但真把代码拉下来跑一遍你会发现两件事一是高斯朴素贝叶斯在这个数据集上的表现其实相当能打二是从零手写一遍贝叶斯分类器比直接调 sklearn 的 GaussianNB 更能看清条件概率、高斯密度、连乘这些环节到底在干什么。这份资源就是围绕这个思路展开的用 Python 从 CSV 加载数据按比例拆训练集和测试集逐类别算均值方差套高斯概率密度函数连乘得后验取最大概率类别作为预测最后算准确率并用线性回归画图看特征与发病的关系。适合正在学模式识别、机器学习入门或者想搞明白朴素贝叶斯底层计算流程的开发者。2. 项目架构拆解从 CSV 到预测结果的数据链路2.1 整体流程与模块划分这个项目的结构不复杂但链路很完整。从代码组织上看它分成两个独立文件一个是朴素贝叶斯分类主程序负责数据加载、拆分、训练、预测、评估另一个是线性回归可视化脚本负责用 sklearn 加载糖尿病数据集对每个特征单独建模并画散点图加拟合直线。主程序的执行流程是这样的先调loadCsv()把 CSV 读成二维列表并把字符串转成浮点数再调splitDataset()按比例随机拆出训练集和测试集接着summarizeByClass()按类别分组并计算每个属性的均值和标准差然后getPredictions()对测试集逐条预测最后getAccuracy()拿预测值和真实标签对比算出准确率。可视化脚本走的是另一条路用sklearn.datasets.load_diabetes()直接加载内置数据集转成 DataFrame然后循环对每个特征列做线性回归把散点图和拟合直线画在 5 行 2 列的画板上。两个文件各司其职一个管分类预测一个管特征分析。这种拆分方式在课程作业里很常见好处是逻辑清晰坏处是数据源不统一——主程序读的是本地 CSV可视化脚本用的是 sklearn 内置数据。如果你要二次开发第一件事可能就是统一数据入口。2.2 数据加载与类型转换数据加载看着简单但有几个细节不注意就会翻车。原始代码里文件路径是硬编码的绝对路径而且带中文目录这在 Windows 上问题不大换到 Linux 或者别人机器上直接报 FileNotFoundError。import csv def loadCsv(): # 打开CSV文件指定utf-8编码防止中文路径乱码 lines csv.reader(open(diabetes.data.csv, r, encodingutf-8)) dataset list(lines) # 将每一行的字符串逐个转成float否则后面算均值会报类型错误 for i in range(len(dataset)): dataset[i] [float(x) for x in dataset[i]] return dataset这里csv.reader返回的是迭代器list()之后每一行是一个字符串列表。关键在最后的类型转换如果不转 float后面mean()里做sum(numbers)会直接抛 TypeError。另外注意原始代码里有一行print(dataset)和一段被注释掉的逐行打印那是调试用的数据集有几百行全打出来终端会刷屏正式跑的时候记得注释掉。提示文件路径建议改成相对路径或者用os.path.join拼接方便在不同机器上跑。2.3 训练集拆分与按类别分组拆分逻辑用的是随机不放回抽样先算出训练集应该有多少条然后每次从剩余数据里随机抽一条放进训练集直到凑够数量。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)) # 抽一条就从copy里删一条 return [trainSet, copy] # 剩下的就是测试集splitRatio在主程序里设的是 0.67也就是约三分之二训练、三分之一测试。原始文档里提到 90% 训练 10% 测试但代码实际用的是 0.67这个不一致要注意。copy.pop(index)保证同一条数据不会被重复抽到这是随机不放回抽样的标准写法。按类别分组用separateByClass()逻辑很直白遍历每条记录看它的类别标签最后一列在不在字典里不在就新建一个空列表然后把这条记录追加进去。最终得到一个{类别值: [记录列表]}的字典。2.4 均值、标准差与高斯概率密度计算这是整个项目的数学核心。朴素贝叶斯假设每个特征在给定类别下服从高斯分布所以只需要算出每个类别下每个特征的均值和标准差就能代入高斯密度公式算条件概率。import math def mean(numbers): return sum(numbers) / float(len(numbers)) def stdev(numbers): avg mean(numbers) # 方差用n-1做分母样本标准差的无偏估计 variance sum([pow(x - avg, 2) for x in numbers]) / float(len(numbers) - 1) return math.sqrt(variance) def summarize(dataset): # zip(*dataset)把列转成行对每一列算均值和标准差 summaries [(mean(attribute), stdev(attribute)) for attribute in zip(*dataset)] del summaries[-1] # 最后一列是类别标签不是特征删掉 return summarieszip(*dataset)这个操作值得说一下它把二维列表转置原来每一行是一条记录转置后每一行变成同一个特征的所有取值。这样就能对每个特征单独算统计量。del summaries[-1]是因为最后一列是类别标签不能当特征算进去。summarizeByClass()就是把分组和汇总串起来先按类别分开再对每个类别的数据调summarize()返回{类别值: [(均值1,标准差1), (均值2,标准差2), ...]}。高斯概率密度函数的代码实现def calculateProbability(x, mean, stdev): # 指数部分-(x-mean)^2 / (2*stdev^2) exponent math.exp(-(math.pow(x - mean, 2) / (2 * math.pow(stdev, 2)))) # 系数部分1 / (sqrt(2*pi) * stdev) return (1 / (math.sqrt(2 * math.pi) * stdev)) * exponent参数x是某个特征的具体取值mean和stdev是该特征在某个类别下的均值和标准差。返回值就是该特征取值在这个类别下的条件概率密度。注意这里算的是密度不是概率但在比较不同类别的后验时密度值的大小关系等价于概率的大小关系所以不影响分类结果。2.5 后验概率连乘与预测有了每个特征的条件概率密度接下来要算一条记录属于某个类别的整体概率。朴素贝叶斯的核心假设是特征之间相互独立所以直接把所有特征的条件概率乘起来。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 bestLabelcalculateClassProbabilities返回一个字典键是类别值值是该记录属于这个类别的未归一化后验概率。因为贝叶斯公式的分母对所有类别都一样所以只需要比较分子不用做归一化。predict就是遍历这个字典找最大值对应的类别。getPredictions对测试集逐条调predict把结果收集成列表。getAccuracy拿预测列表和测试集真实标签对比统计正确个数除以总数乘 100。主程序入口if __name__ __main__: splitRatio 0.67 dataset loadCsv() trainingSet, testSet splitDataset(dataset, splitRatio) summaries summarizeByClass(trainingSet) predictions getPredictions(summaries, testSet) accuracy getAccuracy(testSet, predictions) print(Accuracy: , accuracy)整个流程没有用到任何第三方机器学习库纯 Python 标准库实现这也是这份代码适合教学的原因——每一行都能对应到公式。3. 跑起来环境配置与完整执行步骤3.1 环境准备与依赖安装这个项目对环境的依赖很低。主分类程序只需要 Python 标准库里的csv、random、math不需要额外装包。可视化脚本需要numpy、pandas、matplotlib、sklearn四个库。# 创建虚拟环境推荐避免污染全局包 python -m venv venv # Windows激活 venv\Scripts\activate # macOS/Linux激活 source venv/bin/activate # 安装可视化脚本的依赖 pip install numpy pandas matplotlib scikit-learnPython 版本建议 3.8 以上原始代码用的是 PyCharm Community Edition 2021.3.1但代码本身不依赖特定 IDEVS Code 或者命令行都能跑。硬件方面这个项目计算量很小普通笔记本完全够用原始环境是 i7-8550U 加 8GB 内存实际上任何能跑 Python 的机器都不会有性能瓶颈。3.2 数据集准备与路径调整皮马印第安人糖尿病数据集是一个经典的二分类数据集包含若干生理指标和是否患病的标签。原始代码里路径写的是绝对路径你需要改成自己机器上的实际路径。# 改成相对路径把CSV放在脚本同目录下 lines csv.reader(open(diabetes.data.csv, r, encodingutf-8))数据集每一行的最后一列是类别标签0 表示五年内未发病1 表示五年内发病。前面的列是各种生理指标。所有值都是数值型不需要做独热编码之类的处理但不同特征的单位不一样如果后面要改进模型可以考虑做标准化。注意CSV 文件不要有表头行否则第一行转 float 会报 ValueError。如果下载的数据集自带表头先用next(reader)跳过。3.3 运行分类程序与结果解读在命令行里直接运行主程序python naive_bayes_diabetes.py输出就是一行Accuracy: XX.XX。由于拆分是随机的每次跑出来的准确率会有波动一般在 70% 到 76% 之间。如果想看更稳定的结果可以把splitRatio调大比如 0.8 或 0.9训练数据多了准确率会略高但测试集变小评估的方差也会变大。原始文档里提到准确率输出如图 6 所示但没有给出具体数值。实际跑下来这个数据集上高斯朴素贝叶斯的准确率大概在 72% 到 76% 区间和 sklearn 的 GaussianNB 默认参数结果接近。如果准确率明显偏低先检查数据加载有没有问题再检查类别标签是不是在最后一列。3.4 运行可视化脚本看特征关系可视化脚本是独立运行的python linear_regression_plot.py它会弹出一个 5 行 2 列的画板每个子图是一个特征和 target 的散点图加拟合直线标题上带 R² 分数。从图上能直观看到哪些特征和发病进展关系大。原始文档的结论是性别和发病基本无关年龄有一点点关系BMI 关系很大部分血清指标有正相关或负相关。import numpy as np import pandas as pd 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] df pd.DataFrame(data, columnsfeature_names) plt.figure(figsize(12, 25)) for i, col in enumerate(df.columns): # reshape(-1,1)把一维Series转成二维线性回归要求二维输入 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) 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)这步很关键DataFrame 取一列出来是一维的sklearn 的fit要求特征矩阵是二维的不 reshape 会直接报错。score返回的是 R²越接近 1 说明线性拟合越好。不过要注意R² 高不代表因果关系强只是说明线性关系明显。4. 避坑与常见问题排查4.1 文件路径报错 FileNotFoundError现象运行loadCsv()直接抛 FileNotFoundError提示找不到文件。原因原始代码用的是绝对路径而且路径里带中文目录名和反斜杠。Windows 上反斜杠在 Python 字符串里是转义字符\研这种写法可能被解析成其他字符。另外换到别人机器上路径肯定对不上。解决把 CSV 文件和脚本放同一目录路径改成diabetes.data.csv。如果一定要用绝对路径用原始字符串rD:\path\to\file.csv或者在路径里用正斜杠/。4.2 类型转换报错 ValueError现象dataset[i] [float(x) for x in dataset[i]]这行抛 ValueError提示 could not convert string to float。原因CSV 里混了非数值内容最常见的是表头行或者某些缺失值用?或空字符串表示。解决先打印前几行看看数据长什么样。如果有表头在list(lines)之后加一行dataset dataset[1:]跳过。如果有缺失值要么删掉那些行要么用该列均值填充。4.3 准确率异常低或每次波动很大现象跑出来准确率只有 50% 多或者每次运行结果差异很大。原因splitRatio设得太小导致训练数据不够或者随机拆分的运气不好测试集里某个类别的样本特别多。另外如果数据没有打乱按原始顺序拆分也可能导致分布偏差。解决把splitRatio调到 0.8 以上多跑几次取平均。更稳妥的做法是做 k 折交叉验证但那就需要改代码结构了。另外检查一下类别标签是不是在最后一列如果标签列位置不对del summaries[-1]删掉的就是一个特征而不是标签结果肯定不对。4.4 可视化脚本中文标题乱码现象plt.title或set_title里的中文显示成方框。原因matplotlib 默认字体不支持中文。解决在绘图前加两行配置plt.rcParams[font.sans-serif] [SimHei] plt.rcParams[axes.unicode_minus] FalseSimHei是 Windows 自带黑体macOS 可以换成Arial Unicode MSLinux 看系统装了哪个中文字体。第二行是解决负号显示成方块的问题。4.5 标准差为零导致除零错误现象calculateProbability里math.pow(stdev, 2)做分母时抛 ZeroDivisionError。原因某个类别下某个特征的所有取值完全一样标准差算出来是 0。解决在stdev()里加一个极小值兜底比如return math.sqrt(variance) if variance 0 else 1e-6。或者检查数据看看是不是某个特征在某个类别下没有变化。这个数据集里一般不会出现这种情况但换数据集就可能碰到。5. 二次开发从课程作业到可用工具的改造思路这份代码作为教学示例是完整的但要真拿来做点东西有几个方向可以改。我一般会先从数据入口统一开始把两个脚本的数据源合并成一个都用 pandas 读 CSV这样后面加特征工程或者换数据集都方便。第一个改造点是加入交叉验证。现在的单次随机拆分评估方差太大跑三次可能得到三个不同的准确率。改成 5 折交叉验证把数据集分成 5 份每次用 4 份训练 1 份测试最后取平均。代码改动不大在splitDataset外面套一层循环就行但评估结果会稳定很多。第二个改造点是处理数据不平衡。这个数据集里阳性样本比例偏低如果直接看准确率一个全预测为阴性的模型也能到 65% 左右。所以应该同时看精确率、召回率和 F1。在getAccuracy旁边加一个getConfusionMatrix函数统计 TP、TN、FP、FN然后算这几个指标。第三个改造点是特征标准化。现在的高斯朴素贝叶斯对特征尺度不敏感因为每个特征单独算均值和方差但如果后面想换其他分类器做对比标准化就是必须的。用sklearn.preprocessing.StandardScaler对特征列做 z-score 标准化标签列不动。第四个改造点是模型持久化。每次跑都要重新训练如果调好参数想保存下来可以用pickle把summaries字典存到文件里下次直接加载。这样部署的时候就不用带着训练数据了。import pickle # 保存模型 with open(model.pkl, wb) as f: pickle.dump(summaries, f) # 加载模型 with open(model.pkl, rb) as f: summaries pickle.load(f)验证改造效果的方法很简单固定随机种子跑改造前后的代码对比准确率和混淆矩阵。random.seed(42)加在splitDataset调用之前保证每次拆分结果一样这样才能公平对比。还有一个容易忽略的点是概率下溢。当特征数量多的时候连乘很多个小于 1 的概率密度值结果会趋近于零浮点数精度不够就会变成 0。解决办法是取对数把连乘变成连加比较对数后验的大小。这个改动在特征少的时候看不出差别但特征一多就是必须的。从那以后我每次写概率连乘的代码都会先想一下要不要转对数域这个习惯帮我省了不少调试时间。希望帮到你。本文还有配套的精品资源点击获取