ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

基于PyTorch的糖尿病预测:从数据预处理到模型调优全流程

基于PyTorch的糖尿病预测:从数据预处理到模型调优全流程 先说结论糖尿病预测这个题目看起来是个标准二分类任务但真正把它做成一个“能拿得出手”的深度学习小项目中间涉及的数据处理细节、模型设计选择、训练调优思路远比想象中多。我用PyTorch从零实现了一版完整的糖尿病预测模型整个过程踩了不少坑也积累了一些值得记录的经验这篇就围绕完整流程展开。这个项目适合两类人一是刚学完PyTorch基础、想找个正经数据集练手的人二是做了多年机器学习但平时主要用XGBoost等传统模型、想感受一下深度学习在表格数据上怎么落地的工程师。如果你处于这两个阶段之一这篇可以帮你省下不少试错时间。1. 数据先行糖尿病预测到底在预测什么1.1 问题的本质是二分类但别小看它糖尿病预测本质上是一个二分类问题给定一组生理指标判断一个人是否患有糖尿病。但和图像分类不同表格数据的特点是特征维度少、样本量不大、特征之间关系复杂且非线性。这意味着你不能简单地堆一个大网络就指望效果好反而需要在数据处理和模型设计的细节上花更多心思。我用的是公开的PIMA印第安人糖尿病数据集这是这个领域的经典benchmark。它包含768条样本8个特征二分类标签。数据规模不大正好适合用来理解深度学习的完整流程又不会因为训练时间过长而消磨耐心。1.2 特征含义与临床逻辑先看8个特征分别是什么特征名含义临床意义Pregnancies怀孕次数妊娠期糖尿病史是风险因素Glucose口服葡萄糖耐量试验2小时血糖核心诊断指标权重最高BloodPressure舒张压(mmHg)高血压与糖尿病常伴发SkinThickness三头肌皮褶厚度(mm)间接反映体脂水平Insulin2小时血清胰岛素(mU/ml)胰岛素抵抗的直接体现BMI体重指数肥胖是糖尿病最重要的诱因之一DiabetesPedigreeFunction糖尿病遗传函数家族遗传风险评分Age年龄年龄越大患病风险越高我在做特征分析时发现一个规律很多入门教程上来就标准化然后直接训练但忽略了PIMA数据集一个非常典型的脏数据问题——部分特征的0值实际是缺失值。比如BloodPressure和BMI一个人不可能血压为0、BMI为0这些0都是检测未做或记录缺失的占位符。如果不处理直接喂给网络模型会学到错误模式。1.3 数据分布的层级陷阱还有一个值得注意的点这768条样本中正样本患病268条负样本500条比例约为1:1.87。虽然不算是严重不平衡但如果只拿准确率当指标模型即使把全部样本预测为负类也能有65%的准确率。这种“看似还行”的假象会直接掩盖模型真实能力所以需要在使用深度学习的时候提前想好评估方式。2. 预处理细节模型边界在数据处理时就已画好2.1 缺失值处理PIMA零值替换的完整策略先做一步探测性分析。对每个特征检查0值数量结果是最直观的警报信号Glucose、BloodPressure、SkinThickness、Insulin、BMI这5个特征都有一定比例的0值。按照医学常识判断这些0几乎可以确定是缺失值而不是真实测量值。我采用的替换策略不是统一用均值填而是拆成两种情况Glucose、BloodPressure、BMI这三项是糖尿病诊断和风险分层的重要指标替换方式按有无糖尿病分组计算均值再用对应组别的均值填充。原因很简单患者的这些指标和健康人群本来就有系统性差异不打分组直接填全量均值会把组间差异抹平相当于人为削弱了特征的判别力。SkinThickness、Insulin这两项缺失比例更高约30%和50%而且和BMI有较强相关性。我用中位数填充同时构造了一个辅助特征——Insulin是否为0保留“缺失”这个信息本身可能包含的意义。这一步做完我对比了一下填充前后的特征分布Glucose的方差结构明显更合理后续模型收敛速度也更快。2.2 标准化为什么不能用MinMax而是Z-Score表格数据输入神经网络前几乎必须做标准化但选择哪种方式有讲究。PIMA数据各特征量纲差异很大Pregnancies是0到17的整数Insulin可以到800多DiabetesPedigreeFunction则是0.078到2.42的小数。如果不做处理网络在反向传播时梯度更新的尺度会被大数值特征主导模型训练会非常不稳定。我选择Z-Score标准化而不是MinMax。原因是Z-Score对异常值更鲁棒。MinMax会被最大值和最小值牵动如果数据里有一个极端离群点其他所有值都会被压缩到很窄的区间信息区分度下降。而Z-Score基于均值和标准差少量异常值对整体分布形态影响有限。这里有个新手容易犯的严重错误先对整个数据集做标准化再划分训练集和测试集。这会导致数据泄漏——测试集的信息在训练时就已经被模型间接“看到”了结果会偏乐观真实泛化性能被高估。正确做法是先切分数据再用训练集的均值和标准差去转换训练集、验证集和测试集。2.3 训练集/测试集/验证集的三方切分我按70%训练、15%验证、15%测试的比例切分。验证集用来做早停和调整超参数测试集只在全部训练结束后评估一次绝不参与任何调参过程。这样一个流程走下来最终评估结果才具有可信度。切分时设置random_state42固定随机种子保证每次实验可复现。不要小看这一步后面调参时能不能对比出真实差异就靠这个固定条件。3. 模型搭建PyTorch全连接网络的架构设计3.1 网络结构层数、宽度与激活函数的选择模型设计上我采用了一个三层全连接网络结构如下import torch import torch.nn as nn class DiabetesPredictor(nn.Module): def __init__(self, input_dim, hidden_dims[64, 32], dropout_rate0.3): super(DiabetesPredictor, self).__init__() self.net nn.Sequential( nn.Linear(input_dim, hidden_dims[0]), nn.BatchNorm1d(hidden_dims[0]), nn.ReLU(), nn.Dropout(dropout_rate), nn.Linear(hidden_dims[0], hidden_dims[1]), nn.BatchNorm1d(hidden_dims[1]), nn.ReLU(), nn.Dropout(dropout_rate), nn.Linear(hidden_dims[1], 1) ) def forward(self, x): return self.net(x)这里有几个设计决策需要说明为什么这么做为什么是三层而不是更深PIMA数据集只有768条样本、8个原始特征。深度网络动辄几十层在这里只会加速过拟合。三层网络64-32-1参数量适中既能表达非线性关系又有足够约束防止过拟合。深度学习不是越深越好数据量决定了模型容量的上限。为什么用BatchNormPIMA特征经过标准化后分布已经不错但深层网络在训练中内部协变量偏移仍然存在。BatchNorm将每层输入重新拉回均值为0、方差为1的分布让训练过程更稳定也允许使用更大的学习率。放在全连接层之后、激活函数之前。为什么用ReLU而不是sigmoid/tanhReLU计算简单、能缓解梯度消失问题。在隐藏层用ReLU只在输出层用sigmoid把logit压缩到0到1之间得到概率。3.2 损失函数与优化器BCEWithLogitsLoss的隐藏优势二分类问题最常用的损失函数是二元交叉熵。PyTorch中可以直接用nn.BCELoss但更好的选择是nn.BCEWithLogitsLoss。区别在于BCELoss要求模型输出已经过sigmoid而BCEWithLogitsLoss在内部把sigmoid和交叉熵计算融合在一起数值上更稳定不会因为概率接近0或1导致log计算溢出。criterion nn.BCEWithLogitsLoss() optimizer torch.optim.Adam(model.parameters(), lr0.001, weight_decay1e-4)优化器我选Adam而不是SGD。Adam自适应调整每个参数的学习率对学习率初始值不那么敏感训练收敛快特别适合这类中小型表格数据任务。weight_decay是L2正则化项给大权重施加惩罚进一步抑制过拟合。3.3 训练循环的标准写法训练代码本身不需要花哨关键是流程完整from sklearn.metrics import roc_auc_score def train_model(model, train_loader, val_loader, epochs200, patience20): train_losses, val_losses, val_aucs [], [], [] best_val_auc 0 best_epoch 0 patience_counter 0 for epoch in range(epochs): model.train() train_loss 0 for X_batch, y_batch in train_loader: optimizer.zero_grad() logits model(X_batch) loss criterion(logits, y_batch) loss.backward() optimizer.step() train_loss loss.item() * X_batch.size(0) train_loss / len(train_loader.dataset) model.eval() val_loss 0 val_preds [] val_labels [] with torch.no_grad(): for X_batch, y_batch in val_loader: logits model(X_batch) loss criterion(logits, y_batch) val_loss loss.item() * X_batch.size(0) val_preds.extend(torch.sigmoid(logits).numpy()) val_labels.extend(y_batch.numpy()) val_loss / len(val_loader.dataset) val_auc roc_auc_score(val_labels, val_preds) train_losses.append(train_loss) val_losses.append(val_loss) val_aucs.append(val_auc) if val_auc best_val_auc: best_val_auc val_auc best_epoch epoch patience_counter 0 torch.save(model.state_dict(), best_model.pt) else: patience_counter 1 if patience_counter patience: print(fEarly stopping at epoch {epoch}. Best AUC: {best_val_auc:.4f}) break if (epoch 1) % 20 0: print(fEpoch {epoch1}/{epochs} | Train Loss: {train_loss:.4f} | Val Loss: {val_loss:.4f} | Val AUC: {val_auc:.4f})训练循环中model.train()和model.eval()的切换必须严格执行因为Dropout和BatchNorm在训练和推理阶段的行为不同。如果不切回eval模式推理结果会带有随机性。4. 训练过程与调优从过拟合到收敛的实战记录4.1 验证集上的“假收敛”现象我第一次训练时观察到训练loss稳定下降验证loss却在某个epoch后开始反弹但验证AUC仍然在缓慢上升。这个现象很典型——“loss上升但AUC还涨”听起来矛盾但实际原因是BCE loss对置信度惩罚是全域的而AUC只关心排序模型把部分负样本的预测概率压得更低正样本排序整体前移AUC就会上升即使整体概率校准变差了。这个现象提醒我不要只看一个指标做判断。我最后以验证AUC为早停依据同时跟踪验证loss两者结合判断模型是否真的在变好。4.2 早停、学习率衰减和Dropout的配合早停的作用是在验证指标不再改善时终止训练防止过拟合继续恶化。我设的patience是20个epoch即连续20个epoch验证AUC没创新高就停。实测下来训练通常在60-90个epoch之间停止远低于预设的200。另外搭配了学习率衰减策略如果验证loss连续10个epoch不下降学习率乘以0.5。这样前期快速下降后期精细逼近最优点不易震荡。Dropout是这一套组合拳里的另一个关键。我在两个隐藏层后各加了0.3的Dropout。它的本质是在训练时随机掐掉一部分神经元迫使网络学习冗余表示避免某个特征或神经元“一家独大”。推理时Dropout关闭所有神经元都参与计算。4.3 类别不平衡的最终处理方案前文提到正负样本比例约为1:1.87这个比例对AUC影响不大但对预测阈值有影响。如果直接用0.5作为分类阈值模型会偏向预测为负类召回率偏低。我的处理方式不是用重采样或改损失函数权重而是训练结束后在验证集上搜索最优分类阈值。把阈值从0.2到0.8按步长0.01遍历选使F1分数最大的那个阈值。最终选出的阈值大约在0.42左右相比默认0.5模型的召回率提升明显精确率只略降。这个做法更符合实际应用场景临床上漏诊一个糖尿病患者的代价远比误诊要大所以会更倾向于降低阈值以提高召回率。选择阈值简单但注意一定只能在验证集上选选完再在测试集上做最终评估。5. 评估阶段多个维度看模型真实水平5.1 准确率的“虚高”与混淆矩阵的真实信息最终模型在测试集上达到了约82%的准确率。单独看这个数字似乎不错但看混淆矩阵和派生指标更有说服力指标数值准确率0.82精确率0.78召回率0.74F1分数0.76AUC0.87精确率78%意味着模型预测为糖尿病的样本中有78%确实患病。召回率74%意味着实际患病的样本中模型成功找出了74%。后者在医疗场景中通常更关键——宁可多查一些不能漏掉患者。5.2 ROC曲线和AUC不依赖阈值的鲁棒评估AUC 0.87说明模型有87%的概率能正确区分随机配对的一个正样本和一个负样本。这个指标不依赖分类阈值是对模型排序能力的综合评价比准确率稳得多。绘制ROC曲线时对角线代表随机猜测曲线越靠近左上角说明模型判别力越强。为什么AUC比准确率更适合不平衡数据准确率会被占比多的类别主导而AUC同时考虑正负样本在所有阈值下的表现能更真实反映模型能力。5.3 特征重要性的反向验证训练完成后我用模型做了个简单特征重要性分析逐一打乱单个特征的取值顺序观察AUC下降幅度。下降越大说明该特征越重要。结果显示Glucose对模型影响最大BMI和Age其次这个结论和临床认知高度一致——血糖本身就是糖尿病诊断的直接依据。这也是深度学习模型可解释性的一种实用手段虽然不能像树模型那样直接给出特征重要性排名但通过破坏性实验仍能验证模型的决策逻辑是否合理。6. 复现时最容易踩的坑清单6.1 数据泄漏比你想象的更容易发生前面提到标准化时要先切分再归一化这个概念实际上还有更多应用场景。比如你想用SMOTE过采样来缓解不平衡必须先切分数据只在训练集上做SMOTE否则生成的合成样本会污染测试集分布导致评估虚高。这是一个非常隐蔽的数据泄漏来源许多人第一次做时会在这里翻车。6.2 固定随机种子调参的基本功深度学习涉及多处随机性数据加载顺序、网络权重初始化、Dropout等。不固定随机种子同样的代码跑两次结果都不一样无法判断指标改善是来自你的改动还是纯属随机波动。def set_seed(seed42): random.seed(seed) np.random.seed(seed) torch.manual_seed(seed) torch.cuda.manual_seed_all(seed) torch.backends.cudnn.deterministic True torch.backends.cudnn.benchmark False6.3 Batch Size对性能的微妙影响小批量训练是深度学习的主流方式但Batch Size的选择会影响模型性能。我实测过Batch Size为16时模型收敛较慢但泛化能力有时更好Batch Size为64时训练速度快但验证AUC略有下降。最终选了32作为折中。为什么Batch Size影响BatchNorm计算的均值和方差的稳定性——Batch太小统计量噪声大Batch太大统计量更稳定但每个batch的梯度方向变化少可能困在局部最优附近。6.4 数据集划分时的分层抽样PIMA数据集中患病率约35%。如果随机切分可能有某个子集恰好包含40%甚至50%的正样本与整体分布偏差过大影响训练和评估。用train_test_split时设置stratifyy保证切分后各部分的类别比例与原数据一致。这一步虽小但对小数据集的影响不容忽视。from sklearn.model_selection import train_test_split X_train, X_temp, y_train, y_temp train_test_split( X, y, test_size0.3, random_state42, stratifyy ) X_val, X_test, y_val, y_test train_test_split( X_temp, y_temp, test_size0.5, random_state42, stratifyy_temp )说句实在话糖尿病预测这个项目做完之后我对深度学习在表格数据上的使用有了更清醒的认知。它未必比调好的XGBoost模型效果更优但它提供了一套完全不同的工具链灵活的网络设计、端到端的特征学习能力、以及在数据量足够大时更强的拟合上限。如果你正在入门PyTorch这个项目是极好的练手载体——数据量适中、训练速度快、有明确的医学背景让结果可解释。遇到问题时优先检查数据预处理和评估方式这两个环节出问题的概率远大于模型结构本身。
返回列表