Keras实战:heart数据集心脏病风险预测二分类全流程

发布时间:2026/10/16 11:06:45
Keras实战:heart数据集心脏病风险预测二分类全流程
第一次拿Keras训练heart心脏病诊断训练集时我心里其实有点没底数据量不大、字段全是医学缩写还牵扯到医疗场景感觉好像很复杂。真正做下来才发现这其实是一个非常标准的二分类项目只是有几个环节——数据标准化、类别划分、阈值选择——比想象中更值得较真。这篇文章不写花架子就把我从读数据、清洗、建模、训练到独立测试的完整过程扒开讲清楚。核心用的就是Keras搭建一个全连接网络对heart数据集做心脏病风险预测最后在独立测试集上评估效果。适合刚学完Keras基础、想拿真实结构数据练手的人也适合想了解医疗数据建模该怎么规避坑的人。1. 先想清楚heart数据集训练的本质和框架选型1.1 这就是个二分类问题先别自己吓自己我第一次看到heart数据集时被里面的医学缩写整得有点晕cp、trestbps、thalach、oldpeak这些词单独拎出来都认识拼在一起就不知道该怎么处理。后来换个角度看这就是一张普通的表格每行是一个病人的检查记录每列是一个体检指标最后一列是目标值1表示确诊有心脏病0表示没有。所以问题一下子就变简单了模型要做的事情是根据13个特征预测患者属于1还是0。这不是多标签、不是回归就是一个干干净净的二分类。想通这一点之后整个项目的心智负担就小了很多。后面所有工作都是围绕这个二分类目标展开的准备数据、搭网络、训练、评估。不要因为“医疗”两个字就觉得高不可攀建模层面的逻辑和预测用户是否流失、预测邮件是否垃圾邮件没有本质区别。当然医疗场景的特殊性在于评价标准。后面我会专门讲为什么要看混淆矩阵、为什么不能只盯准确率这部分才是医疗数据建模真正的门槛。1.2 为什么我选Keras而不是手写神经网络用Keras做这个项目是最省力的选择。最直接的原因是代码量少一个Sequential模型几行就能把网络结构定义完训练、评估、预测都有现成API不需要自己写反向传播和梯度更新。有人可能会说既然数据量这么小用手写神经网络或者直接用机器学习库不也行吗这话对但要看目标是什么。如果你的目标是彻底搞懂神经网络底层的每个细节手写一个两层网络确实帮助更大如果目标是快速验证“这个数据集能不能学”、把训练到测试的完整链路跑通Keras的抽象层级刚刚好——它替你处理了大部分工程细节但又不像调用机器学习库那样把“网络”这个概念藏得看不见。对比一下更直接。用机器学习库做逻辑回归几行搞定但感受不到隐藏层和激活函数的作用手写神经网络一个for循环里塞满梯度公式光调bug就能耗掉半天。Keras处在中间你仍然需要设计网络结构、选优化器、定损失函数但它把重复劳动压缩到了最低。对新手来说这是效率最高的学习路径。另外Keras的生态足够好后续想做超参数搜索、模型可视化、导出部署都有现成工具链不用中途换框架。这也是很多真实项目选它的原因不只是因为它“简单”。2. 数据准备把heart数据集收拾利索再喂给模型2.1 先看字段哪些特征在影响心脏病风险建模第一步不是写网络而是先把数据看明白。heart数据集常见版本有13个特征和1个目标列特征大致可以分成几类基本属性、血液指标、心电图相关指标、运动相关指标。我习惯拿到数据后先跑一遍df.info()和df.describe()把每个字段的类型、取值范围、缺失情况过一遍。下面是一份典型的字段说明实际使用时要根据你手里的数据版本调整字段名含义类型典型取值范围age年龄数值29-77sex性别类别0/1cp胸痛类型类别0-3trestbps静息血压数值94-200chol血清胆固醇数值126-564fbs空腹血糖是否大于120类别0/1restecg静息心电图结果类别0-2thalach最大心率数值71-202exang运动是否诱发心绞痛类别0/1oldpeak运动引起的ST段压低数值0-6.2slope运动峰值ST段斜率类别0-2ca主要血管数量数值0-4thal地中海贫血类型类别1-3target是否有心脏病类别0/1这个字段表的价值在于它能帮你理解模型的输入到底是什么而不是盲目的数据灌入。比如cp、exang、oldpeak这几个字段在医学上本身就和心肌缺血高度相关模型大概率会重点利用它们。之后再训练得到重要特征时你可以反过来验证学到的规律是否符合常识。2.2 归一化不能省数值范围差距过大直接毁掉训练heart数据集有个非常典型的特征数值字段的量纲差距巨大。年龄只有几十chol能到四五百oldpeak才零点几、个位数。如果不做处理直接喂给神经网络模型会天然认为数值大的特征更重要梯度更新也会被大数值特征主导训练过程会非常不稳定。标准做法是用StandardScaler做Z-score标准化让每个特征都变成均值0、方差1。from sklearn.preprocessing import StandardScaler scaler StandardScaler() X_train scaler.fit_transform(X_train) X_val scaler.transform(X_val) X_test scaler.transform(X_test)注意一个细节fit_transform只能用训练集。测试集和验证集只能调用transform也就是复用训练集上算出的均值和标准差。这里的“为什么”很多人想不通为什么不把全部数据一起fit_transform因为测试集扮演的是“未来的数据”角色如果它参与了均值、方差的计算相当于模型在训练阶段就间接看到了测试集的信息这就是数据泄露会让评估结果虚高。真实场景里新来的病人数据根本没有机会和训练数据一起算归一化参数。2.3 随机切分有讲究训练集、验证集和测试集各司其职很多人一上来就把数据切成训练集和测试集然后开始训练这样做不是不行但你没有验证集可用调参时会把测试集活活“看”偏。我的做法是分三层先用train_test_split分出80%训练验证的数据再把这80%内部切一块作为验证集。heart数据集一共就三百条左右测试集放20%约60条验证集从80%里再切出20%这样训练集大概190条上下。from sklearn.model_selection import train_test_split X_temp, X_test, y_temp, y_test train_test_split( X, y, test_size0.2, random_state42, stratifyy ) X_train, X_val, y_train, y_val train_test_split( X_temp, y_temp, test_size0.2, random_state42, stratifyy_temp )这里stratifyy是另一个容易被忽略的细节。它表示按类别比例分层抽样。如果数据里患病和不患病比例是51:49分层后训练集和测试集也保持这个比例不会出现“测试集里全是患病样本”这种极端情况。切分完成后我习惯顺手打印一下训练集、验证集、测试集的样本量和类别分布确认三层数据的分布是接近的。这一步花不了十秒钟但能避免很多后面“结果忽好忽坏”的问题。3. 模型设计Keras网络结构、损失函数和优化器怎么选3.1 三层小网络就够用隐藏层宽度和Dropout的搭配heart数据集只有几百条样本网络规模必须收敛着来。我第一次跑这个项目时试过两层128节点的隐藏层训练集准确率能到接近100%验证集却只有70%多典型的过拟合。后来把模型改成三层小网络效果反而更好。最终常用的结构是这样的import tensorflow as tf from tensorflow import keras from tensorflow.keras import layers model keras.Sequential([ layers.Input(shape(X_train.shape[1],)), layers.Dense(32, activationrelu), layers.BatchNormalization(), layers.Dropout(0.3), layers.Dense(16, activationrelu), layers.Dropout(0.2), layers.Dense(1, activationsigmoid) ])为什么选32和16这是我试出来的经验值也符合这类小规模表格数据的一般直觉。输入13个特征第一个隐藏层用32个神经元相当于把特征映射到更高维空间让网络有机会学习非线性组合第二层压缩到16提炼出更高层次的模式最后输出层只有1个节点用sigmoid压到0到1之间表示患病的概率。Dropout的作用是随机丢弃一部分神经元的输出防止网络过分依赖某几个特征。这里第一个Dropout设0.3第二个设0.2越靠近输出层丢弃越少避免把已经学到的信息过度破坏。BatchNormalization是为了让每一层的输入分布相对稳定对表格数据来说配合ReLU激活函数训练会稳很多。3.2 binary_crossentropy Adam医疗二分类问题的标准起手式模型结构定了接下来是编译阶段的三个选择损失函数、优化器、评估指标。这三个东西不是随便填的每一步都有对应的理由。model.compile( optimizerkeras.optimizers.Adam(learning_rate1e-3), lossbinary_crossentropy, metrics[accuracy] )损失函数选binary_crossentropy这是二分类问题的标配。它的核心思想是衡量预测概率和真实标签之间的“信息差异”预测越离谱损失越大。为什么不用均方误差MSE因为sigmoid输出和MSE组合时梯度容易饱和模型学得慢而交叉熵在预测错误时能给出足够大的梯度训练效率高得多。优化器选Adam默认学习率1e-3。Adam相当于自带学习率调整的自适应梯度方法对新手来说最大的好处是不需要太精细地调学习率它在训练过程中会自动适配每个参数的更新步长。不过1e-3不是万能药如果发现loss震荡不收敛下面章节会讲怎么下调。评估指标这里用的是accuracy但它只是一个训练过程的参考指标。真正评估模型好坏后面我会用AUC、混淆矩阵这些更全面的指标。3.3 batch size、epochs和早停训练策略的参数玄机编译完还不算完训练时的超参数往往比网络结构更影响最终效果。第一个参数是batch size。我在这类小型数据集上习惯用16或者32。batch size越小每次更新看得数据越少梯度噪声越大反而有点像带了随机扰动有时有利于跳出局部最优缺点是训练时间更长、loss曲线更抖。batch size越大训练越稳但也越容易收敛到比较差的局部点。对heart这种数据量16到32都很合理不用刻意追求大batch。第二个参数是epochs。我一开始喜欢设200其实没必要。网络在几十轮后往往就已经收敛后面纯属过拟合。正确做法是配合EarlyStopping让模型自己决定什么时候停。from tensorflow.keras.callbacks import EarlyStopping early_stop EarlyStopping( monitorval_loss, patience15, restore_best_weightsTrue ) history model.fit( X_train, y_train, validation_data(X_val, y_val), epochs100, batch_size16, callbacks[early_stop], verbose1 )EarlyStopping监控的是val_loss也就是验证集损失。如果连续15轮验证集损失都不下降就停止训练并且restore_best_weightsTrue会把模型权重恢复到验证集损失最小的那一刻。这里要特别提醒不要监控val_accuracy而要用val_loss。原因是准确率是个离散指标可能在几轮里保持不变造成误判而损失是连续值对模型的细微变化更敏感。4. 训练与验证看懂日志、混淆矩阵和ROC曲线4.1 训练日志怎么读loss和accuracy不是越低越好训练开始后终端会不断刷日志一次典型输出长这样Epoch 12/100 10/10 [] - 0s 3ms/step - loss: 0.4211 - accuracy: 0.8158 - val_loss: 0.3977 - val_accuracy: 0.7742很多初学者看到accuracy快接近1就兴奋马上觉得自己模型天下无敌。其实你要先分清楚这里说的是训练集准确率还是验证集准确率。如果训练集准确率95%以上验证集却只有75%这不是好消息是过拟合。正确读法是盯紧val_loss和val_accuracy。训练集loss持续下降是正常的关键是验证集loss能不能同步下降。前几轮如果验证集loss下降说明模型在学到泛化规律训练到后面如果训练集loss还在降、验证集loss开始回升这就是过拟合信号说明模型开始背训练数据了。我通常会在训练结束后把history里的loss和val_loss画成曲线一眼就能看出有没有过拟合。4.2 准确率会骗人混淆矩阵里才有真正信息量假设模型在测试集上准确率到了85%听起来不错。但放在心脏病诊断场景里有个问题医疗场景最怕的是什么最怕把有病的人误判成没病。一个假阴性可能会让病人错过干预窗口风险极高。这时候单看准确率是不够的混淆矩阵才是关键from sklearn.metrics import confusion_matrix, classification_report y_pred (model.predict(X_test) 0.5).astype(int) cm confusion_matrix(y_test, y_pred) print(cm) print(classification_report(y_test, y_pred))输出大概长这样[[25 4] [ 6 26]]解读一下行是真实标签列是预测标签。左上25是正确预测的阴性真阴性右下26是正确预测的阳性真阳性左下6是漏诊假阴性右上4是误报假阳性。从这里可以算出两个关键指标灵敏度/召回率Recall 26 / (266)也就是实际患病的人里有多少被模型找出来了特异度Specificity 25 / (254)也就是实际没病的人里有多少被模型正确排除了对医疗诊断而言灵敏度往往更关键因为你宁可多复查一个也不想漏掉一个。如果你发现灵敏度偏低就该考虑调阈值了。4.3 分类阈值不是死的根据场景调0.5上一节顺手用大于0.5做出了预测结果但真实项目里0.5只是默认值不是最优值。sigmoid输出的不是类别是概率。0.5默认把概率大于50%的判为患病但如果你更在意“不遗漏病人”完全可以把阈值降到0.3、0.4让更多模糊样本被判为阳性召回率就上去了。阈值怎么选最常用的方法是看ROC曲线计算它每个阈值下的灵敏度和特异度组合然后用约登指数灵敏度 特异度 - 1找一个平衡点。from sklearn.metrics import roc_curve, auc fpr, tpr, thresholds roc_curve(y_test, y_pred_prob) auc_score auc(fpr, tpr) print(AUC:, auc_score) # 选约登指数最大的阈值 optimal_idx np.argmax(tpr - fpr) optimal_threshold thresholds[optimal_idx] print(最优阈值:, optimal_threshold)ROC曲线把模型在“漏诊”和“误诊”之间如何取舍完整展示了出来AUC则是一个综合评价。AUC在0.5左右说明模型没啥预测能力接近1说明分类能力很强。在实际操作中AUC比准确率更能反映模型质量特别是类别不平衡的时候。5. 测试环节独立测试集上的最终体检5.1 预测概率输出和标签阈值转换整个项目走到这一步终于轮到真正的“考试”了。测试集从建模开始就被隔离没参与过归一化参数计算、没参与过训练、也没参与过验证集调参现在用它来模拟模型遇到全新病人时的表现。Keras做预测很简单y_pred_prob model.predict(X_test) y_pred (y_pred_prob 0.5).astype(int)但先把预测结果存下来再分析别急着只看准确率。model.predict返回的是概率值它本身信息量很大。比如有个样本预测概率是0.48距离阈值0.5只差一点点属于边界样本另一个样本概率是0.97模型非常确定。这些信息等合成一份result_df看起来一目了然import pandas as pd result_df pd.DataFrame({ 真实标签: y_test, 预测概率: y_pred_prob.flatten(), 预测标签(0.5): y_pred.flatten() }) print(result_df.head(10))把预测概率暴露出来你回头看阈值调整时才知道哪些样本被哪些阈值影响而不是只看到二分类结果。5.2 复盘真实评估流程从调用到指标解读一次完整的测试评估我会按这个顺序来第一算准确率和AUC。这两个指标先给一个整体印象。AUC如果上了0.9说明模型排序能力不错如果只有0.7说明预测能力和随机猜测拉不开太大差距。第二画混淆矩阵。直接看漏诊、误报的数量。即使准确率不错也要确认假阴性是否被控制住。拿上面的混淆矩阵举例如果63个测试样本里有6个漏诊意味着10个左右真正患病的样本里就有1个会被漏掉这个数字在真实场景中是不可接受的。第三根据约登指数算最优阈值。然后把阈值调过去重新生成混淆矩阵对比灵敏度、特异度的变化。这一步常常被人忽略但却是让模型真正适配业务的关键。第四检查不稳定样本。选几个预测概率接近阈值的样本看看它们的特征值是不是介于正常和异常之间。比如一个患者年龄偏大、血压偏高、但没有典型胸痛模型给出0.45的概率这在临床上本身就有不确定性不是模型“错了”而是信息本身模糊。完成这些步骤整个模型才算真正验收完毕。整个过程不要急机器跑几秒钟但分析结果往往要花半小时这半小时才是模型的价值所在。6. 实操避坑训练心脏诊断模型时最容易翻的车6.1 loss卡住不降多半是学习率和初始化的问题我遇到过最诡异的情况是loss在一个位置完全不动。检查了半天最后定位到三个原因第一学习率太大loss在最小值附近来回震荡看起来像是卡住第二数据没归一化某些数值大的特征直接把梯度带偏第三ReLU把神经元杀死了一批梯度无法回传。解决办法按优先级来先把学习率从1e-3降到1e-4看loss有没有缓慢下降然后确认StandardScaler已经正确应用最后可以在网络里加BatchNormalization缓解神经元死亡问题。如果这些都没用就检查一下是不是训练集和验证集切分出了幺蛾子比如某类样本集中在验证集里导致loss始终偏高。打印类别分布就能排除这个可能。6.2 验证集表现远差于训练集过拟合的处理套路这是小型数据集跑神经网络最经典的坑。我在做heart数据集时训练集准确率能到97%验证集只有74%一度以为自己代码写错了。仔细看训练曲线才发现过拟合在十几个epoch之后就开始出现了。处理办法有几个按效果排序先说Dropout我把它从0.2提到0.4后验证集准确率立刻回升了几个点然后是EarlyStopping配合restore_best_weightsTrue让模型停在验证集最佳位置最后是砍网络容量把隐藏层单元数从64/32减到32/16模型学不到训练集的“死规律”过拟合自然缓解。还有一点容易被忽略不要反复用同一份验证集调参。你在验证集上调一次、两次、十次之后验证集已经变成了隐形的训练集测试集才真正反映泛化能力。如果发现测试集效果和验证集差很远很可能是调参过拟合到了验证集上。6.3 数据不平衡让模型变成“都说不患病”的工具有些版本的heart数据集类别比例比较均衡但换一个其他医疗数据集很可能出现“90%没病、10%有病”的情况。这时候模型只要猜所有人“没病”准确率就有90%看起来很强实际就是个废物。此时建议先用value_counts()确认类别分布如果失衡明显再处理。两个常用手段一个是在model.fit里加class_weight给少数类更大的权重让模型误分类它时受到更大惩罚另一个是数据重采样比如对少数类做上采样或生成合成样本。我的建议是先试class_weight简单可控不要一上来就上重采样特别是SMOTE这类生成方法在医疗数据上可能制造出不符合真实生理规律的样本反而引入噪声。6.4 排查速查表对照这个清单快速定位问题现象可能原因排查/解决方向loss不降学习率过大/过小数据未标准化尝试1e-4学习率检查StandardScaler训练集涨、验证集跌过拟合增大Dropout减小网络容量开启EarlyStopping准确率很高但AUC低类别不平衡看混淆矩阵算灵敏度/特异度调阈值训练过程震荡batch size过小从16调到32或降低学习率测试集效果和验证集差异大对验证集反复调参导致过拟合减少验证集上的调参轮次重新切分数据预测概率全是0.5附近网络表达能力不够或特征太弱检查输入是否有有效特征适当增加网络容量这张表是我自己复盘时整理出来的每次跑一个新模型前都会过一遍能省很多无头苍蝇式的时间。7. 这个项目还能往哪走后续扩展方向7.1 特征工程把医学指标变成更有用的信息模型跑通只是起点想进一步提升效果最值得投入的是特征工程。heart数据集里的原始字段是医生直接记录的指标但神经网络不一定擅长自己发现组合特征这时候人工构造一些医学上有意义的组合特征往往比调网络结构更有效。比如age可以分箱成青年、中年、老年血压字段可以参照临床分级生成高血压等级心率和运动能力的关系可以用thalach和age的差值表达。把这类领域知识注入模型它就更接近真正的心脏病风险判断逻辑。这不是玄学我在类似数据集上试过加了一组简单的组合特征后AUC从0.88涨到0.91。特征工程不是加字段而是让模型少走弯路。7.2 模型迭代从MLP到集成和可解释性工具全连接网络只能说是个基线模型。下一步可以从几个方向走一是换成树模型比如梯度提升树这类模型在表格数据上往往比神经网络更强也能输出特征重要性二是用Keras自带的超参数搜索工具调整网络结构、学习率、Dropout比例三是对最终模型做可解释性分析比如用SHAP值看每个特征对预测的具体贡献。医疗场景尤其需要可解释性。医生不会因为一个黑盒模型说“0.9概率患病”就下结论他要看的是你的模型是不是符合临床逻辑年龄大、血压高、运动诱发心绞痛这些因素是不是真正被模型利用了。SHAP值可以画出一张图展示每个特征如何推高或拉低某个病人的预测概率这是从技术Demo走向真实应用的关键一步。我个人在实际操作中最深的体会是能用Keras几行代码把模型跑起来不代表这个模型真的能用。整个heart心脏病诊断训练集项目的价值不在训练那几十秒而在数据是怎么被处理的、评估指标是怎么被选出来的、阈值是怎么被调出来的。这些环节决定了模型是“纸面准确”还是“场景可用”。如果你也想拿类似数据集练手我的建议很简单第一先把混淆矩阵和ROC曲线画出来再谈调参第二不要反复折磨同一个测试集第三永远记住预测概率不是诊断结论在真实医疗场景里模型只能做辅助决策最终判断必须交给专业人员。把这几点想明白你离一个真正合格的AI从业者就更近了一步。