
简介Keras构建多层感知器MLP诊断印第安糖尿病是一份面向AI初学者与机器学习实践者的微型实战项目围绕Pima印第安人糖尿病数据集演示从数据预处理、模型搭建到训练评估的完整闭环。内容重点覆盖Sequential模型结构、隐藏层神经元数量设置、ReLU与sigmoid激活函数的选择以及二分类常用的binary_crossentropy损失、Adam优化器和accuracy评估指标帮助理解神经网络处理表格型医疗数据的关键技巧同时涉及缺失值处理、数据标准化与训练/验证集划分等数据工程细节。压缩包共3个文件包含完整的Keras训练脚本、CSV数据集与数据集说明文档整体仅11KB体量精简适合快速离线对照学习。目前已有342人学习代码注释详细可指导读者避开数据清洗与模型调参中的常见误区直接复现糖尿病二分类实验并将同一思路迁移到其他表格型预测任务。1. Keras多层感知器诊断印第安糖尿病这份资源到底能复现什么用深度学习做医学诊断听起来像大厂 AI Lab 才能碰的项目但实际上用 Keras 搭一个多层感知器就能在公开数据集上跑到 75% 到 80% 的准确率这份资源就是把这件事完整走了一遍。资源核心是一个基于 Keras 的 MLP 二分类模型针对印第安人糖尿病数据集 Pima Indians Diabetes 做患病预测8 个临床特征输入、1 个二分类概率输出覆盖数据加载、预处理、网络搭建、训练评估和保存部署的完整链路。它适合两类人一类是刚学完 Keras 基础、想找一个完整可复现的分类实战项目的人另一类是人工智能课程设计或大作业卡在建模环节、需要参考网络结构和调参思路的从业者或学生。资源本身把数据集和代码组织好了跑通之后你能看到每个部件的作用也能改参数去适配其他表格分类任务。下面按我拆解的顺序把数据背景、网络设计、训练流程、调参避坑和部署逐层讲清楚。2. 多层感知器与糖尿病诊断网络结构设计逻辑2.1 为什么选 MLP 而不是 CNN任务与数据的匹配Pima Indians Diabetes 数据集是 UCI 上非常经典的医学二分类数据集768 条有效样本每条包含 8 个特征怀孕次数、口服葡萄糖耐量试验 2 小时血糖值、舒张压、三头肌皮褶厚度、2 小时血清胰岛素、BMI、糖尿病家族函数pedigree function和年龄。标签是 0 或 1表示 5 年内是否被诊断出糖尿病。数据本身是典型的表格数据每一列对应独立的临床测量值特征之间不存在图像里那种空间邻域相关性也不存在时间序列上的先后依赖。这就是为什么在这个场景下选 MLP 而不是 CNN。CNN 的本质假设是局部相关性卷积核要在相邻区域提取模式图像像素、语音帧的位置顺序是有意义的但表格数据里第 2 列是血糖、第 4 列是皮脂厚度打乱列顺序模型学到的映射关系本质不变强行套 CNN 只是在制造一个没有物理意义的结构。MLP 的全连接结构让每个输入特征都可以直接连接到每个隐藏层神经元网络能自由学习特征之间的非线性组合这才是表格数据需要的能力。Keras 里实现 MLP 就是Sequential加Dense层拼一个 CNN 的时间MLP 已经完成一轮训练了。2.2 网络结构设计从输入到输出这个数据集上最稳妥的结构是三层输入层 8 个神经元对应 8 个特征隐藏层 16 个神经元激活函数用 ReLU输出层 1 个神经元激活函数用 Sigmoid。Keras 实现如下from tensorflow import keras from tensorflow.keras import layers model keras.Sequential([ layers.Input(shape(8,)), layers.Dense(16, activationrelu, namehidden), layers.Dense(1, activationsigmoid, nameoutput) ]) model.summary()model.summary()会打印每一层的输出形状和参数量。hidden 层参数是 8×1616144output 层是 16×1117总计 161 个参数。这个数字在 768 条样本面前是合理的参数维度远小于样本数模型有足够容量去拟合数据同时不会因容量过剩而过拟合。16 这个数字怎么来的常见经验是取输入维度和输出维度之间的值或者输入维度的 2 倍左右。也可以从 (81)/2 约等于 4 起步但那个容量在这个任务上会欠拟合取 32 或 64 时验证曲线往往抖得更厉害参数量变大后小数据集上的经验风险最小化和泛化之间的差距被拉大。我的习惯是先用 16 跑一遍看训练曲线和验证曲线的 gapgap 大就降神经元数gap 小且验证精度还有上升空间就加。2.3 激活函数的分工ReLU 与 Sigmoid 各司其职隐藏层激活函数最常用 ReLU两个原因计算梯度便宜正值区间导数为 1不像 Sigmoid 在深层网络里容易梯度消失负值直接截断给网络带来一定的稀疏性在小规模分类任务上有隐式正则效果。输出层必须用 Sigmoid因为任务是把得分压缩成概率Sigmoid 的输出区间是 (0,1) 且单调递增模型输出的排序和概率意义明确。常见的翻车写法是输出层不用激活函数或者用 ReLU。训练 loss 可能还是能降但输出值域不再是概率有的样本输出 1.5有的输出 0.2后续阈值比较和评估脚本全部失去意义。隐藏层如果换成 tanh需要更长 epoch 收敛因为 tanh 的输出范围是 (-1,1)零中心但饱和区梯度也小。这个数据集上我试过 tanh 隐藏层最终准确率差异很小但前期 loss 下降明显更慢——ReLU 在正区间的恒定梯度让 Adam 的收敛更省事。2.4 为什么不堆更多隐藏层看到一些教程在这个数据集上叠四层五层768 条样本根本撑不起那么大的参数量。经验值是样本量和参数量要在同一数量级或者参数远小于样本量。三层结构在这个数据集上是性价比最高的选择再加层只是在验证集上反复横跳训练集精度上去了验证集精度不升反降。以 161 个参数来说用 522 条实际训练样本去约束它模型自由度已经足够强行加深网络只会让优化更困难。3. 从数据到模型完整搭建与训练流程3.1 数据加载与标签分布检查资源包里的数据集是 csv 格式没有表头需要手动指定列名。加载代码import pandas as pd df pd.read_csv(pima-indians-diabetes.csv, headerNone) df.columns [pregnancies, glucose, blood_pressure, skin_thickness, insulin, bmi, pedigree, age, outcome] print(df.shape) print(df[outcome].value_counts())输出能看到数据集规模是 768 行 × 9 列正样本 268 条、负样本 500 条。正样本占比约 34.9%属于轻度类别不平衡。这个比例直接影响后面的评估策略如果直接以 accuracy 为核心指标模型全预测为 0 也能拿到 65% 的准确率看起来还行但对实际诊断完全没用。所以后面切分数据用分层采样评估重点看 recall 和 precision。3.2 训练测试集切分stratify 是关键from sklearn.model_selection import train_test_split X df.iloc[:, :8].values y df.iloc[:, 8].values X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.2, random_state42, stratifyy )stratifyy的作用是按 y 的类别比例来采样确保训练集和测试集的正负比例都接近 65:35。如果用默认随机切分768 条样本上运气不好时测试集可能只有 20% 的正样本评估数值就失真了。random_state42是所有调参实验的基础随机状态不固定每次切分数据集不同你调完网络参数后看到的精度变化可能来自数据分布变化而不是模型修改根本无法判断改动有没有效果。编码习惯是把这个固定值放在配置里后续所有实验都以它为基准。3.3 标准化为什么必须做看数据列的数值范围就明白血糖从 44 到 199胰岛素从 0 到 846BMI 从 18 到 67年龄从 21 到 81。特征不在同一个量纲上梯度下降在未标准化数据上的损失面是拉长的椭圆优化路径曲折训练不稳定甚至不收敛。标准化把每个特征变成均值为 0、标准差为 1 的分布。from sklearn.preprocessing import StandardScaler scaler StandardScaler() X_train_scaled scaler.fit_transform(X_train) X_test_scaled scaler.transform(X_test)StandardScaler的计算公式是 (x - mu) / sigmamu 是该特征的均值sigma 是标准差。fit这一步只计算训练集上每个特征的 mu 和 sigmatransform用这套训练集的参数去做变换。注意测试集上只用transform不能用fit_transform因为测试集的分布不应该影响训练预处理参数否则会造成数据泄露。胰岛素这一列本身有很多 0 值标准化会把 0 映射到均值以下的负值但 0 值本身的语义不会改变——更正确的做法是先做缺失值处理避坑章节展开。3.4 模型编译三个参数的选型依据编译阶段三个参数分别对应优化器、损失函数、评估指标model.compile( optimizerkeras.optimizers.Adam(learning_rate0.001), lossbinary_crossentropy, metrics[accuracy] )优化器选 Adam默认学习率 0.001。这个 161 参数的网络上SGD 需要手动调学习率和 momentum训练速度慢Adam 对每个参数自适应调整更新步长收敛更快更稳。loss 用binary_crossentropy这是二分类的标准选择模型输出的概率和真实标签之间的交叉熵值越小越好。metrics 里的 accuracy 只是训练过程方便观察用的最终评估以 sklearn 的精确指标为准。3.5 训练流程与 batch_size 的直觉history model.fit( X_train_scaled, y_train, validation_split0.15, epochs100, batch_size16, verbose1 )validation_split0.15表示从训练集内部再切 15% 做验证集用于监控训练中的泛化表现不参与梯度更新。这里训练集本身已经是原始数据的 80%再切 15% 后实际训练样本约 522 条。batch_size16表示每 16 条样本计算一次梯度并更新参数一个 epoch 内大约 32 次更新更新频繁尾部收敛更平滑。可以改成 32 或 64 对比但 batch 太大比如 128时一个 epoch 只有 4 到 5 次梯度更新100 个 epoch 共 400 多次更新不够网络收敛。epochs100 是上限在 3.6 节配合 EarlyStopping 使用。可以先跑一遍看训练 loss 和 val_loss 曲线的拐点再决定是否增加轮次。3.6 训练曲线诊断方法import matplotlib.pyplot as plt def plot_history(history): fig, (ax1, ax2) plt.subplots(1, 2, figsize(12, 4)) ax1.plot(history.history[loss], labeltrain_loss) ax1.plot(history.history[val_loss], labelval_loss) ax1.set_xlabel(epoch) ax1.set_ylabel(loss) ax1.legend() ax2.plot(history.history[accuracy], labeltrain_acc) ax2.plot(history.history[val_accuracy], labelval_acc) ax2.set_xlabel(epoch) ax2.set_ylabel(accuracy) ax2.legend() plt.show() plot_history(history)曲线判断逻辑训练 loss 持续下降验证 loss 下降到某个拐点后反弹就是过拟合开始两条 loss 曲线始终差距很大说明模型容量过大或正则不足训练 loss 和验证 loss 都高位不下是欠拟合模型容量不够。这个数据集上合理的形态是验证 loss 停在 0.5 附近波动验证 accuracy 在 0.76 到 0.80 之间震荡。如果验证 loss 一直在 0.6 以上降不下去先回头检查数据预处理和缺失值。4. 模型调参与评估从准确率到 ROC 曲线4.1 调参优先级先数据、再结构、后训练参数模型性能不行时先改什么我的顺序是数据预处理 → 网络结构 → 训练参数 → 阈值。数据在前是因为很多精度问题其实是数据分布问题比如类别不平衡、缺失值处理不当被模型训练过程掩盖了。网络结构先动隐藏层神经元数再考虑加深。训练参数里优先 batch_size 和 epochs配合 EarlyStopping最后才动学习率。这个顺序背后有一个逻辑先确定上限数据质量和网络容量再决定怎么训练最后调整决策边界。跳步调参容易陷入局部最优网格搜索一堆组合还是老样子浪费时间不说最后也不知道问题是数据还是网络。4.2 EarlyStopping给训练一颗后悔药from tensorflow.keras.callbacks import EarlyStopping early_stop EarlyStopping( monitorval_loss, patience10, restore_best_weightsTrue ) history model.fit( X_train_scaled, y_train, validation_split0.15, epochs200, batch_size16, callbacks[early_stop], verbose1 )EarlyStopping 监控val_loss连续patience10个 epoch 没有下降就终止训练restore_best_weightsTrue会把模型权重回滚到验证 loss 最低的那个 epoch。这样 epochs 设 200 也没关系模型会自动停在最佳位置。为什么用 val_loss 而不是 val_acc 监控accuracy 是跳变的非连续指标验证集只有一百多条样本时一个小批次预测错误就会让 accuracy 来回波动容易误触发早停loss 是连续平滑的变化趋势更稳定。如果加了 EarlyStopping 发现每个模型都在差不多同一个 epoch 停住说明那里确实是网络自然收敛位置epoch 上限不用再调转去改其他训练参数更有效。4.3 分类报告与 AUC先别急着看 accuracyfrom sklearn.metrics import classification_report, roc_auc_score y_pred_prob model.predict(X_test_scaled).ravel() y_pred (y_pred_prob 0.5).astype(int) print(classification_report(y_test, y_pred, target_names[negative, positive])) print(fAUC {roc_auc_score(y_test, y_pred_prob):.3f})classification_report 输出每一类的 precision、recall、f1 以及总体 accuracy。在这个任务里重点看 positive 列的 recall——它表示真实患病的人中有多少被模型识别出来漏诊率等于 1 减 recall。这个数据集上常见的结果是 precision 0.70 左右、recall 0.55 到 0.65往往要牺牲一点 precision 换 recall具体阈值看业务是筛查还是确诊。AUC 在 0.80 到 0.86 都算合理。AUC 表示随机抽取一个患病样本和一个健康样本模型给患病样本打分更高的概率它不依赖阈值是模型排序能力的稳健度量。这里补充一句如果只拿 accuracy 当唯一指标在这个不平衡的数据集上你很容易被假象骗过去。4.4 阈值扫描找到业务需要的决策点from sklearn.metrics import precision_recall_fscore_support thresholds np.arange(0.3, 0.71, 0.05) for t in thresholds: pred_t (y_pred_prob t).astype(int) precision_t, recall_t, _, _ precision_recall_fscore_support( y_test, pred_t, averagebinary) print(fthreshold{t:.2f} | precision{precision_t:.3f} | recall{recall_t:.3f})把 0.3 到 0.7 之间的阈值全部扫一遍每次重新算 precision 和 recall打印结果能直观看到权衡曲线。部署时如果是筛查场景不希望漏掉任何一个高危患者阈值可以降到 0.35如果是确诊场景比如决定是否上治疗方案希望减少误治阈值提高到 0.6。这类表格任务我一般不做大范围网格搜索只扫阈值因为参数网格太大容易在验证集上过拟合。4.5 交叉验证的必要性固定 train_test_split 只能得到一个切分的结果运气成分还在。更稳的做法是 5 折交叉验证每折单独做标准化和训练最后汇总 5 折测试集上的预测结果再算指标。Keras 里可以用StratifiedKFold手动循环小数据集上一个完整 5 折大约就是 5 次单模型训练的时间完全可承受。交叉验证里最容易犯的错是先用全量数据 fit 标准化再切折这样每折的验证集信息都提前渗入了预处理参数和单次切分里的数据泄露是同一个问题。5. 避坑指南糖尿病诊断模型搭建中的常见问题5.1 Keras 版本错位import 只走一条路现象照着教程写from keras.layers import Dense跑的时候报AttributeError或者import keras之后 Dense 行为异常模型结构对不上。原因Keras 2.x 和 Keras 3.x 的 API 有差异pip install keras安装的原生 Keras 和 TensorFlow 内部自带的tf.keras可能同时存在并互相覆盖。解决明确用tf.keras一条路走到底。检查环境用python -c import tensorflow as tf; print(tf.__version__); print(tf.keras.__version__)之后所有依赖统一从tf.keras导入不单独pip install keras。如果项目确实依赖原生 Keras 3 的新特性就把 requirements 里的 keras 版本锁死并和 tensorflow 版本对齐避免隐式覆盖。5.2 数据泄露的隐蔽路径scaler 在全量数据上 fit现象训练集和测试集精度都不错测试集跑到 0.85模型上线后对新病例预测效果跌到 0.65。原因预处理时对全量数据执行了fit_transform包括测试集在内。均值和标准差来自全量数据测试集信息提前进入了预处理参数测试指标虚高。新数据没见过这些统计量对应的真实分布精度自然缩水。解决严格按训练集fit、测试集transform的顺序执行。交叉验证时每折都要单独 fit 和 transform不能全局统一标准化再切折。我通常把 scaler 和模型封装成 sklearn Pipeline从流程上切断泄露路径。5.3 类别不平衡被 accuracy 掩盖现象总体 accuracy 0.72看起来能用但 classification_report 里 positive 类 recall 只有 0.3模型几乎把患者全漏掉了。原因数据集 500 对 268模型倾向预测多数类accuracy 被大比例负样本撑起来真实的阳性识别能力很差。解决训练时给少数类更高的损失权重让模型对错判阳性样本付出更高代价。Keras 的 fit 直接传class_weightmodel.fit( X_train_scaled, y_train, class_weight{0: 1.0, 1: 1.5}, epochs100, batch_size16, validation_split0.15 )调整后 recall 会明显上升但 precision 会下降这是 trade-off。也可以在评估阶段把阈值降低两种方式本质相同但class_weight是在训练阶段改变模型学到的边界阈值是在预测阶段调整决策点建议都试一下对比效果。5.4 胰岛素和皮肤厚度列的 0 值陷阱现象模型训练曲线波动大验证 loss 不降翻看数据发现胰岛素和皮肤厚度特征里大量 0 值。原因这两个特征的 0 值实际是缺失值标记不是真实测量为 0。让缺失值以 0 的身份进模型等于给特征凭空制造了一个错误分布标准化也救不回来。解决先用非零值的中位数填充for col in [insulin, skin_thickness]: median_val df.loc[df[col] 0, col].median() df[col] df[col].replace(0, median_val)这个操作在 Pima 数据集上经常能带来 1 到 2 个百分点的验证集提升而且训练曲线更稳。注意填充操作要先用训练集算中位数再填训练集和测试集不要混在一起算和标准化同理。5.5 实验不可复现的玄学问题现象同样代码、同样数据第二次跑训练精度差了 2 个百分点甚至相同参数两次结果不同。原因深度学习框架的随机性来自多个源头数据切分、权重初始化、mini-batch shuffle、GPU 上的非确定性算子。解决把所有随机种子固定import random import numpy as np import tensorflow as tf seed 42 random.seed(seed) np.random.seed(seed) tf.random.set_seed(seed)如果还需要更强确定性设置环境变量TF_DETERMINISTIC_OPS1再跑代价是训练速度下降。这个网络很小CPU 上几秒一个 epoch完全跑得起。另外验证集和测试集固定下来之后不要频繁更换切分方式否则你会分不清精度变化来自模型还是数据。6. 模型持久化与新样本预测从训练到部署的最后一公里6.1 模型和 scaler 一起保存训练完成后模型文件和预处理参数必须配套保存。模型保存用 Keras 3 推荐的格式model.save(diabetes_mlp.keras)加载loaded_model keras.models.load_model(diabetes_mlp.keras)scaler 用 joblib 保存import joblib joblib.dump(scaler, scaler.pkl)两个文件放同一目录部署时先加载 scaler再加载模型路径从同一目录解析避免路径不一致。6.2 单条样本的完整预测流程loaded_scaler joblib.load(scaler.pkl) loaded_model keras.models.load_model(diabetes_mlp.keras) new_patient np.array([[2, 138, 72, 33, 180, 34.2, 0.42, 46]]) new_patient_scaled loaded_scaler.transform(new_patient) risk float(loaded_model.predict(new_patient_scaled, verbose0)[0][0]) print(f患病风险概率: {risk:.3f}) if risk 0.5: print(高风险) else: print(低风险)new_patient 的 8 个值依次对应怀孕次数、血糖、血压、皮脂厚度、胰岛素、BMI、pedigree、年龄。预测阶段必须用训练阶段保存的 scaler 做 transform不能重新 fit否则归一化的中心点变了模型输入分布错位。我实际交付时还会在接口加输入合法性检查比如血糖值在 30 到 300 之间、血压在 40 到 150 之间异常值直接拦截防止脏数据喂给模型产生一个看似合理的概率。6.3 上线前强制走一遍推理一致性测试上线前用一个固定样本做回归测试保存当时的输入、预处理后的中间值、模型输出概率三样东西后续改代码或重装环境后再跑一次三个数值完全一致才算通过。这个习惯有一次帮了大忙——重装环境后发现模型结构载入正常但预测输出全部变成 0.99排查半天发现是 keras 版本升级后 Sigmoid 的计算精度变了模型对某些输入饱和。从那以后我每次交付模型都强制走一遍这个一致性测试希望这个习惯也能帮到你。本文还有配套的精品资源点击获取