简介这份PDF文档面向模式识别、机器学习入门学习者与课程设计者围绕朴素贝叶斯分类器在医疗数据上的落地展开解决糖尿病早期预测这一典型二分类问题。资源包共1个PDF文件大小约771KB内容为完整的课程设计报告涵盖贝叶斯定理分类原理、Pima Indians数据集说明、特征提取与高斯概率计算、分类预测流程、准确率评估与可视化分析并附有loadCsv、SplitDataset、summarizeByClass、calculateProbability等核心函数的代码实现与流程图。读者可借此掌握从CSV数据加载、90%训练与10%测试拆分、均值方差标准差统计到高斯概率密度计算、概率累乘与最大类别判定的完整链路理解条件概率与贝叶斯公式在真实医学数据中的应用方式。文档还给出硬件软件环境配置与结果图表展示思路适合作为模式识别大作业参考或朴素贝叶斯实战练手材料。目前已有235人学习下载。1. 从一份课程大作业说起朴素贝叶斯做糖尿病预测到底靠不靠谱一份研究生模式识别课程的作业用不到 200 行 Python 把皮马印第安人糖尿病数据集跑出了约 75% 的预测准确率还顺手用线性回归把 BMI、年龄、血清指标和发病风险的关系画成了散点拟合图。听起来像是教科书里的标准案例但真正把它从 PDF 报告变成能跑、能改、能复现的工程代码中间踩的坑比想象中多。这份资源的核心价值在于它完整展示了一个从 CSV 加载、数据拆分、高斯概率计算到准确率评估和可视化分析的闭环流程没有依赖任何重型框架纯 Python 标准库加 sklearn 和 matplotlib 就能跑通。适合正在学模式识别、机器学习入门或者需要快速搭一个贝叶斯分类 Demo 的开发者。数据集用的是 442 条皮马印第安人医学观察记录包含年龄、性别、BMI 和九项血清化验指标每条记录标注了五年内是否患糖尿病。整个系统分两条线一条是手写朴素贝叶斯分类器做预测另一条是用 sklearn 的线性回归做特征与发病风险的关联分析。2. 系统架构与数据流从 CSV 到预测结果的完整链路2.1 整体模块划分与调用关系这份代码没有用类来组织而是用一组函数串起了整个流程。从入口if __name__ __main__开始依次调用loadCsv()加载数据、splitDataset()拆分训练测试集、summarizeByClass()按类别汇总特征统计量、getPredictions()批量预测、getAccuracy()计算准确率。另一条线是线性回归可视化用 sklearn 的load_diabetes()重新加载数据集然后对每个特征单独建模画图。这种函数式写法在课程作业里很常见好处是逻辑直白、调试方便坏处是全局变量和硬编码路径满天飞。如果你要把它改造成可复用的模块第一步就是把数据加载和路径配置抽出来。程序流程图在原文里已经画得很清楚开始 → 载入数据 → 拆分训练集和测试集 → 计算均值、方差、标准差并按类别汇总 → 计算每一类别的概率并做出预测 → 图表显示并评估准确率 → 结束。这个流程是标准的朴素贝叶斯分类流程没有做交叉验证也没有做特征标准化因为高斯朴素贝叶斯本身对量纲不敏感——它假设每个特征独立且服从正态分布用均值和标准差来描述分布形状所以不需要额外归一化。2.2 数据加载与类型转换的细节数据加载看起来简单但原文代码里有一个很容易翻车的点文件路径写死在loadCsv()函数里而且用了 Windows 反斜杠路径。换一台机器或者换一个目录结构直接报FileNotFoundError。更稳妥的做法是把路径作为参数传入或者用pathlib处理跨平台路径。import csv from pathlib import Path def loadCsv(filepath): 加载 CSV 数据集并转换为浮点数列表 :param filepath: CSV 文件路径支持 str 或 Path 对象 :return: 二维列表每行是一条记录最后一列是类别标签 path Path(filepath) if not path.exists(): raise FileNotFoundError(f数据集文件不存在: {path.resolve()}) with path.open(r, encodingutf-8) as f: lines csv.reader(f) dataset list(lines) # 逐行转换为 float遇到空行或格式错误时跳过 cleaned [] for row in dataset: try: cleaned.append([float(x) for x in row]) except ValueError: continue # 跳过表头或异常行 return cleaned这里做了三件事用pathlib替代硬编码路径、加了文件存在性检查、用try-except跳过非数值行。原文代码里直接dataset[i] [float(x) for x in dataset[i]]如果 CSV 第一行是表头这一行就会抛ValueError。很多公开数据集的第一行确实是列名所以这个坑几乎必踩。参数说明filepath建议用绝对路径或相对于项目根目录的路径encoding用utf-8兼容大多数场景如果数据集来自旧系统可能是gbk需要根据实际情况调整。2.3 训练集拆分与类别分离splitDataset()用随机抽样的方式把数据集按比例拆成训练集和测试集。原文里splitRatio在__main__中设为 0.67但正文描述里写的是 90% 训练、10% 测试代码和文档不一致。实际跑的时候以代码为准0.67 意味着约三分之二训练、三分之一测试。这个比例对 442 条数据来说测试集大约 146 条评估结果波动会比较大。import random def splitDataset(dataset, splitRatio): 按比例随机拆分数据集 :param dataset: 完整数据集列表 :param splitRatio: 训练集占比0~1 之间 :return: [训练集, 测试集] 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]random.randrange(len(copy))每次从剩余数据里随机取一条取完就从copy中移除保证不重复。这种写法在数据量小的时候没问题但数据量大了之后pop(index)是 O(n) 操作整体复杂度会到 O(n²)。常见做法是直接用random.shuffle(copy)打乱后切片效率更高。separateByClass()负责按类别把数据分开返回一个字典key 是类别值0 或 1value 是该类别下所有记录的列表。这个函数是后续按类别计算均值和标准差的基础。def separateByClass(dataset): 按最后一列的类别值将数据分组 :param dataset: 二维列表 :return: {类别值: [记录列表]} separated {} for vector in dataset: classValue vector[-1] if classValue not in separated: separated[classValue] [] separated[classValue].append(vector) return separated这里用vector[-1]取类别标签意味着数据集的最后一列必须是类别。如果特征列和标签列的顺序变了这个函数就会出错。我一般会在加载数据后先打印一行看看结构确认标签位置。3. 高斯概率计算与预测朴素贝叶斯的核心实现3.1 均值、标准差与高斯概率密度函数朴素贝叶斯的核心假设是在给定类别的前提下各个特征之间相互独立。对于连续型特征通常假设它服从高斯分布用均值和标准差来描述。summarize()函数对每个属性计算均值和标准差然后用zip(*dataset)把同一属性的所有值聚在一起。import math def mean(numbers): 计算平均值 return sum(numbers) / float(len(numbers)) def stdev(numbers): 计算样本标准差分母为 n-1 avg mean(numbers) variance sum([pow(x - avg, 2) for x in numbers]) / float(len(numbers) - 1) return math.sqrt(variance) def summarize(dataset): 对每个属性计算 (均值, 标准差) :param dataset: 同一类别下的记录列表 :return: [(mean, stdev), ...]已去掉最后一列的类别标签 summaries [(mean(attribute), stdev(attribute)) for attribute in zip(*dataset)] del summaries[-1] # 去掉类别列的统计量 return summarieszip(*dataset)是一个很巧妙的用法它把二维列表转置让每一列也就是每个属性的所有值聚到一个元组里。del summaries[-1]删掉最后一列因为类别标签不需要计算概率分布。calculateProbability()实现了高斯概率密度函数def calculateProbability(x, mean, stdev): 高斯概率密度函数 :param x: 特征值 :param mean: 该特征在该类别下的均值 :param stdev: 该特征在该类别下的标准差 :return: 概率密度值 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σ²))。这里有一个隐藏风险如果某个特征在某个类别下的标准差为 0除法会直接抛ZeroDivisionError。在实际数据中如果某个类别下某个特征所有值都相同就会出现这种情况。稳妥的做法是在stdev计算后加一个极小值保护比如stdev max(stdev, 1e-6)。3.2 类别概率累乘与最大后验决策calculateClassProbabilities()把每个特征的概率密度值累乘起来得到该样本属于某个类别的似然值。注意这里没有计算贝叶斯公式的分母因为分母对所有类别都一样比较分子就能得到最大后验类别。def calculateClassProbabilities(summaries, inputVector): 计算输入向量属于每个类别的概率未归一化的似然值 :param summaries: {类别: [(mean, stdev), ...]} :param inputVector: 一条测试记录的特征值列表 :return: {类别: 概率值} 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累乘操作在特征数量多的时候容易导致数值下溢因为每个概率密度值都小于 1十几个特征乘下来可能变成 10 的负几十次方。常见做法是取对数把累乘变成累加比较对数概率的大小。不过在这个项目里只有 8 个特征下溢风险不大原文的写法可以跑通。predict()函数遍历概率字典返回概率最大的类别def predict(summaries, inputVector): 预测单条记录的类别 :return: 概率最大的类别标签 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 bestLabelgetPredictions()和getAccuracy()是批量预测和准确率评估的封装逻辑很直白对测试集每条记录调用predict()然后和真实标签对比统计正确数量除以总数乘以 100%。3.3 线性回归可视化特征与发病风险的关联分析原文的第二段代码用 sklearn 的load_diabetes()重新加载数据集然后对每个特征单独做线性回归并画散点图。这里有一个容易混淆的地方load_diabetes()返回的数据集和 CSV 文件里的皮马印第安数据集不是同一份数据。load_diabetes()是 sklearn 内置的 442 条糖尿病数据特征包括 age、sex、bmi、bp 和六项血清指标 s1~s6target 是一年后病情发展的定量指标。而 CSV 文件里的数据集标签是二分类的 0/1。两者虽然都叫糖尿病数据集但任务类型不同。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): train_X df.loc[:, col].values.reshape(-1, 1) # 转成二维适配 sklearn train_Y target model LinearRegression() model.fit(train_X, train_Y) score model.score(train_X, train_Y) axes plt.subplot(5, 2, i 1) plt.scatter(train_X, train_Y, s5, alpha0.5) k model.coef_[0] b model.intercept_ x np.linspace(train_X.min(), train_X.max(), 100) y k * x b plt.plot(x, y, cred, linewidth1) axes.set_title(f{col}: R²{score:.3f}) plt.tight_layout() plt.show()这段代码的关键点在于reshape(-1, 1)sklearn 的线性回归要求输入是二维数组而df.loc[:, col].values是一维的必须 reshape。另外model.score()返回的是 R² 决定系数越接近 1 说明线性拟合越好。从结果来看BMI 的 R² 通常最高说明体重指数和糖尿病进展的相关性最强性别几乎不相关这和原文的分析结论一致。4. 避坑与排查跑通这份代码必须注意的几个问题4.1 文件路径硬编码导致加载失败现象运行loadCsv()直接报FileNotFoundError提示路径不存在。原因原文代码里路径写的是D:\研究生课程\模式识别\大作业\于杨\diabetes.data.csv这是作者本机的绝对路径换任何一台机器都不存在。解决把路径改成相对路径或通过命令行参数传入同时用pathlib处理跨平台分隔符问题。我一般会在项目根目录建一个data/文件夹把 CSV 放进去代码里写Path(__file__).parent / data / diabetes.data.csv。4.2 CSV 表头导致 float 转换报错现象ValueError: could not convert string to float: Age。原因很多公开数据集的 CSV 第一行是列名直接float(x)会失败。解决在加载时判断第一行是否为数值或者用try-except跳过无法转换的行。更稳妥的做法是用pandas.read_csv()加载它会自动处理表头然后df.values转成 numpy 数组。4.3 标准差为零导致除零异常现象ZeroDivisionError: float division by zero。原因某个类别下某个特征的所有值都相同stdev计算结果为 0高斯概率密度函数里分母出现 0。解决在stdev()返回前加保护return max(math.sqrt(variance), 1e-6)或者在calculateProbability()里判断stdev是否接近 0如果是就返回一个极小的概率值。4.4 训练测试比例与文档不一致现象按正文说的 90% 训练、10% 测试去设置splitRatio0.9但代码里写的是 0.67跑出来的准确率波动很大。原因442 条数据按 0.67 拆分测试集约 146 条按 0.9 拆分测试集只有 44 条评估结果的方差完全不同。解决小数据集建议用交叉验证代替单次拆分sklearn 的cross_val_score几行代码就能搞定比手写拆分稳定得多。4.5 概率累乘下溢导致预测结果异常现象所有类别的概率都变成 0.0predict()返回的bestLabel始终是第一个类别。原因多个小于 1 的概率密度值连乘结果小于浮点数最小表示范围变成 0。解决改用对数概率累加math.log(prob)相加比较时取最大值对应的类别。这个改动在特征数量超过 20 个时几乎是必须的。5. 二次开发与改造从课程作业到可复用模块5.1 把函数式代码重构成类原文的代码全是顶层函数加全局变量改起来牵一发动全身。最直接的改造是把朴素贝叶斯分类器封装成一个类数据加载、训练、预测、评估各司其职。class GaussianNB: def __init__(self): self.summaries {} self.classes [] def fit(self, X, y): 训练按类别汇总均值和标准差 dataset [list(x) [label] for x, label in zip(X, y)] separated separateByClass(dataset) self.summaries {} for classValue, instances in separated.items(): self.summaries[classValue] summarize(instances) self.classes list(self.summaries.keys()) return self def predict(self, X): 批量预测 return [predict(self.summaries, list(x)) for x in X] def score(self, X, y): 准确率 preds self.predict(X) correct sum(1 for p, t in zip(preds, y) if p t) return correct / len(y) * 100.0改造之后训练和预测的接口和 sklearn 的fit/predict/score保持一致后续想换成 sklearn 自带的GaussianNB只需要改一行 import。这种封装方式也方便加日志、加参数校验、加模型持久化。5.2 用 sklearn 的 GaussianNB 做对照验证手写分类器的准确率到底靠不靠谱最直接的验证方式是和 sklearn 的GaussianNB对比。同样的数据、同样的拆分比例如果两者准确率差距在 2% 以内说明手写实现没有逻辑错误。from sklearn.naive_bayes import GaussianNB from sklearn.model_selection import train_test_split from sklearn.metrics import accuracy_score import numpy as np # 假设 dataset 是加载好的二维列表 data np.array(dataset) X, y data[:, :-1], data[:, -1] X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.33, random_state42 ) # sklearn 版本 sk_model GaussianNB() sk_model.fit(X_train, y_train) sk_pred sk_model.predict(X_test) print(fsklearn 准确率: {accuracy_score(y_test, sk_pred) * 100:.2f}%) # 手写版本 my_model GaussianNB() my_model.fit(X_train, y_train) my_acc my_model.score(X_test, y_test) print(f手写版本准确率: {my_acc:.2f}%)这里有一个细节sklearn 的GaussianNB默认使用全体数据的先验概率而手写版本没有显式计算先验相当于假设各类别先验相等。如果数据集中正负样本比例不均衡两者的结果会有差异。皮马印第安数据集的 0/1 比例大约是 65:35不算极端不均衡所以差距通常不大。5.3 特征工程与模型改进方向原始代码把所有特征直接扔进模型没有做特征选择。从线性回归的 R² 可以看出BMI、s5血清指标和年龄的相关系数较高而性别几乎不相关。一个自然的改进是只保留相关性最高的几个特征观察准确率是否提升。特征含义与 target 的 R²近似是否建议保留age年龄0.03可选sex性别0.00否bmi体重指数0.34是bp平均血压0.19是s1~s6血清指标0.03~0.32部分保留另一个改进方向是处理缺失值。皮马印第安数据集里有些特征的 0 值实际上是缺失值比如 BMI 为 0 在生理上不可能直接当数值处理会引入噪声。常见做法是把这些 0 替换成该特征的中位数或均值再重新训练。5.4 可视化与结果输出的工程化原文的可视化代码把散点图和拟合直线画在同一个 figure 里5 行 2 列共 10 个子图。实际使用时plt.show()会阻塞程序如果要在服务器上跑或者批量生成报告需要改成plt.savefig()保存到文件。另外plt.tight_layout()在子图较多时可能报 warning可以改用constrained_layoutTrue。plt.figure(figsize(12, 25), constrained_layoutTrue) # ... 绘图逻辑 ... plt.savefig(feature_regression.png, dpi150, bbox_inchestight) plt.close()从那以后我每次拿到一份课程作业级别的代码都会先做三件事把硬编码路径改成配置项、把顶层函数封装成类、用 sklearn 的对应实现做一次对照验证。这三步走完代码基本就能从“只能跑一次”变成“可以反复用”。希望帮到你。本文还有配套的精品资源点击获取