2026/10/20 22:25:05

用CatBoost应对医疗数据不平衡:机制、参数与实战

用CatBoost应对医疗数据不平衡:机制、参数与实战 医疗数据不平衡这五个字我每次跟做临床数据的朋友聊起来都能引发一阵共鸣。体检数据、化验数据、疾病早筛数据绝大部分场景里阳性样本占比低得吓人可能只有1%到3%。模型稍微“偷个懒”把所有样本都判成阴性准确率照样95%以上写着好看实际什么都没学到。这几年我处理这类问题主流梯度提升框架里用得最多、最顺手的还是CatBoost。它有几个专门针对不平衡场景的机制比之前手工做上采样、下采样省事很多实测效果也稳。这篇文章就围绕“用CatBoost处理医疗数据不平衡”这件事把底层逻辑、核心参数、实操流程和踩过的坑一次性讲清楚适合刚接触医疗数据建模的算法工程师也适合准备把这类模型用到临床辅助筛查项目的朋友。1. 医疗数据不平衡先看清问题有多重1.1 不平衡的本质模型根本学不到少数类类别不平衡指的是一批数据里各类别样本数量严重不对等的状态。放在医疗场景里最常见的就是目标病变检出比如一个早筛项目收集了10万例体检记录其中真正阳性可能只有1000例甚至更少阳性占比1%左右。你说这个数据没有信息吗有。但机器学习模型是个“应试机器”它优化的是整体损失最小化。在极端不平衡下模型发现只要把所有样本都判为多数类损失就已经很小了于是少数类的特征它根本懒得学。这里可以打个简单的比方一场考试100道题其中98道是基础送分题2道是拔高难题。一个学生如果只想及格他只要把98道送分题做对就行难题看都不看。模型也是一样真实世界里少数类恰恰是最需要被识别出来的那部分比如早期病变信号。如果用交叉熵这类损失函数直接训练多数类样本的梯度淹没掉少数类的梯度模型逐渐变成“只会复读多数类”的工具。1.2 传统套路在医学场景的掣肘以前遇到不平衡第一反应就是上采样或下采样。上采样最常见的是SMOTE在少数类样本之间插值生成新样本原理上说得通但医疗场景里有点微妙。医学数据的特征很多时候是化验指标、影像特征、基因表达量人工插值出来的“虚拟患者”是否符合病理逻辑很难论证。临床专家看到你用一个插值生成的样本做依据第一反应就是质疑。下采样倒是不造假但把大量多数类样本直接丢掉在医疗领域特别浪费因为这些样本是医院投入了大量人力物力才标注出来的一条阳性样本的标注可能就要花掉医生十几分钟下采样相当于把钱扔进水沟。代价敏感学习是更理性的路线也就是给不同类别的样本分配不同权重让少数类样本的损失在整体loss里占更大比例。这个思路不改变样本空间只改变优化目标保留真实数据的同时强迫模型关注少数类。CatBoost在这条路线上做得比较完整所以我对它偏爱有加。2. CatBoost为什么在医疗场景里能打2.1 对称树结构天然不容易“钻牛角尖”CatBoost最出名的特点是它的对称树oblivious tree结构。传统梯度提升树每层分裂时不同分支可以选不同特征结构非常灵活但CatBoost强制同一层所有叶子用同一个特征、同一个阈值分裂。这种约束听着像是“自缚手脚”实际在医疗数据上反而很划算。道理很简单医疗数据噪声大、阳性样本少模型很容易把少数几条阳性样本上的特征噪声当成规律记下来。非对称树灵活度太高深挖几个分支就可能记住某个样本的特殊组合这就是过拟合的温床。对称树结构简单、每个分裂都被全量数据复用泛化能力反而更好。我在某个早筛项目上对比过同样参数下CatBoost的验证集P-R曲线明显比同期的另外两个梯度提升框架平稳靠的就是这套保守结构。2.2 Ordered Boosting把罕见样本的梯度算明白传统梯度提升在计算每个样本的梯度时用的模型是由全部样本训练出来的这其中就包含了该样本自身。也就是说模型在“考试”前已经偷看了“答案”算出来的残差会被低估学到的信息量打折扣。这个问题在样本充足时不算严重但数据不平衡时少数类样本数量本来就少每一条样本的信息都极其宝贵再被“偷看”一次就更吃亏了。CatBoost的Ordered Boosting算法简单理解就是每个样本的残差不是用全量模型算出来的而是用“在排列顺序上排在该样本之前的那部分数据”临时训练出来的模型来算。相当于每次打分都用一个没见过这条样本的模型来评得到的梯度更真实小样本的信息能被保住。代价是训练耗时比普通GBDT稍长但医疗数据建模最不缺的就是耐心多等几分钟换更稳的泛化完全划算。2.3 类别特征与缺失值原生支持省心不少医疗数据里除了数值型的化验指标还有大量类别特征比如血型、性别、家族史分组、检查科室编码。以往用其他框架这些特征要么做独热编码要么做标签编码编码方式选不对还会引数据泄漏。CatBoost原生支持类别特征只要在训练时把特征列名传给cat_features参数就行它内部会用目标统计加排序的方式处理省去一大段手工编码代码。缺失值也是医疗数据绕不开的坎。一项化验不是每个患者都做了数据里就会出现大量缺失。CatBoost对缺失值有原生处理策略直接保留为NaN就能参与训练不需要额外填充成0或均值。这两个特性叠加起来让它的数据处理管线比传统方案短了一大截对医疗这种“脏乱差”现实非常友好。3. 实操用CatBoost处理医疗不平衡数据的完整流程3.1 数据准备与分层划分先说数据准备。我以一个虚拟的疾病早筛项目为例样本总量8万目标变量是“是否检出病灶”阳性1800条阳性率2.25%。特征包含年龄、性别、几项血液指标、几项影像量化参数缺失值天然存在。第一步是按患者ID做分组划分这是很多人会忽略的致命细节。医疗数据里同一个患者可能有多条历史记录如果直接随机切分同一个患者的不同记录可能同时出现在训练集和验证集里模型就等于提前看到了答案验证指标虚高上线后立刻露馅。正确做法是用GroupKFold或者先按患者ID聚合再切分。第二步是分层抽样。在保证患者ID不重叠的前提下训练集、验证集、测试集的阳性比例要尽量和全量数据保持一致否则验证集阳性占比一动模型早停的节奏就全乱了。import pandas as pd import numpy as np from sklearn.model_selection import GroupShuffleSplit data pd.read_csv(screening_data.csv) X data.drop(label, axis1) y data[label] groups data[patient_id] gss GroupShuffleSplit(n_splits1, test_size0.3, random_state42) train_idx, val_idx next(gss.split(X, y, groups)) X_train, X_val X.iloc[train_idx], X.iloc[val_idx] y_train, y_val y.iloc[train_idx], y.iloc[val_idx] group_train, group_val groups.iloc[train_idx], groups.iloc[val_idx] print(训练集阳性率:, y_train.mean()) print(验证集阳性率:, y_val.mean())这个切分逻辑我在多个项目里复用重点就一句话先按实体分组再谈随机。分组没做干净后面全白干。3.2 设置类别权重并训练模型切完数据直接训练。CATBoost处理不平衡最直接的入口是class_weights参数和auto_class_weights参数。先说auto_class_weights这是最省事的选择。设置成Balanced框架会自动按类别频率计算权重逻辑大致是总样本数除以“类别数乘以该类样本数”。按我们上面8万样本、1800阳性来算负类样本78200条正类1800条计算完正类权重接近22负类权重约0.51。这个权重比例会给正类损失放大约44倍非常激进但很多场景下有效。我更常用的是SqrtBalanced它取平方根比例正类权重变成4到5倍左右激进度低一些在小样本场景不容易让模型过度盯着少数类抖动。如果手头时间充裕我会两个都跑一遍看验证集PR-AUC再定。class_weights则是完全手动控制。二分类时我会用字典形式把标签的真实值映射到具体权重最直观from catboost import CatBoostClassifier, Pool cat_features [gender, blood_type, family_history] train_pool Pool( X_train, y_train, cat_featurescat_features, ) val_pool Pool( X_val, y_val, cat_featurescat_features, ) model CatBoostClassifier( iterations2000, learning_rate0.05, depth5, loss_functionLogloss, auto_class_weightsSqrtBalanced, eval_metricPRAUC, l2_leaf_reg5, random_seed42, early_stopping_rounds200, verbose100, ) model.fit(train_pool, eval_setval_pool)这段代码有几点可以具体说。eval_metric我选了PRAUC也就是精确率-召回率曲线下面积。为什么不选AUC因为当负类占绝对主导时AUC对多数类的排序变化非常敏感就算少数类召回率很低AUC也可能很好看很容易让人产生模型很准的错觉。PR-AUC只关注少数类表现更贴合“把阳性找出来”的核心目标。权重这块我的经验是不要一次拉太满。很多新手上来就把正类权重设成1:100结果召回率是上去了但假阳性也跟着爆炸模型把所有边界样本全判成阳性这种模型进临床等于把医生淹没在警报里。医疗场景的终极目标永远是精确率和召回率的平衡不是单点最大化。3.3 评估指标怎么选、阈值怎么调模型训练完之后第一件事不是看准确率而是看少数类相关的指标。我把常用指标在医疗场景下的适用性整理成一张表方便直接对照指标计算逻辑医疗场景下的问题结论Accuracy预测正确比例阳性占2%全判阴性也98%直接放弃AUC正负样本排序能力对极端不平衡过于乐观可参考不单一使用PR-AUC精确率与召回率权衡聚焦少数类能看出真实水平重点使用Recall阳性被检出的比例漏诊率直接相关重点关注F1/F2精确率与召回加权调和平均F2更侧重召回贴合“少漏诊”推荐作为核心决策指标指标看完还要调阈值。模型输出的概率默认以0.5为界来判阳性这在极度不平衡下通常不合理。因为模型训练时被多数类拖累正类概率普遍被压低0.5的阈值会把大量本该被判出来的阳性卡在门外面。正确操作是拿验证集的预测概率重新扫描阈值寻找能让F2分数最大化的切分点。from sklearn.metrics import fbeta_score proba_val model.predict_proba(val_pool)[:, 1] best_score 0 best_threshold 0.5 for t in np.arange(0.1, 0.9, 0.01): score fbeta_score(y_val, proba_val t, beta2) if score best_score: best_score score best_threshold t print(最优阈值:, best_threshold) print(对应F2:, best_score)这里有个小坑predict_proba返回的是二维数组二分类时要取索引1对应的那一列也就是正类概率。我之前见过同事直接取第0列等于用负类概率当分数阈值扫描一整圈全是反的调试了半天。4. 常见问题与排查技巧实录4.1 模型全预测成多数类怎么办这个现象几乎每个做不平衡数据的人都遇到过。模型训完一统计预测结果里一个正类都没有。先不要急按顺序排查第一确认权重是否真的生效。有人直接在CatBoostClassifier里写了class_weights但用的是fit(X, y)而不是Pool某些版本里参数传递可能出现偏差导致权重没被正确加载。最直接的验证方法调高权重重训一次看验证集是否至少出现正类预测如果还是零大概率是参数没传进去。第二降低学习率。学习率太高会让模型前几轮直接“惯性冲到”多数类最优解里后续再拟合少数类就很难翻身。把learning_rate调到0.05以下同时把iterations提到2000以上让模型有充分轮次去缓慢适应少数类分布。第三检查验证集划分。如果验证集里阳性样本本身就不多模型为了降低验证集损失可能会选择输出全负类。这时候回头检查GroupShuffleSplit的test_size够不够以及划分前有没有做类别分层如果验证集只剩几十条阳性那模型的召回信号根本没法稳定表达。4.2 过拟合和欠拟合的边界怎么找医疗小样本场景里过拟合是最常见的反面角色。表现是训练集PRAUC非常高验证集一路走低最终梯度提升模型在几百轮后停止提升。对策是把树的复杂度降下来depth控制在4到6之间不要贪深同时调大l2_leaf_reg默认是3我一般从5起步往上试加到10之前基本能把训练集和验证集的差距拉平。欠拟合的表现在验证集上更明显eval_metric一直在涨涨到最后早停触发了但指标绝对值还很低比如PR-AUC不到0.2。这种情况说明模型容量不够优先加iterations或提高learning_rate而不是立刻去动权重。很多人一看到指标差就拼命加正类权重结果是精确率崩盘方向完全搞错。记住一个原则权重是改变偏好不是补能力模型本身能力不够权重越大越容易造成假阳性成片。4.3 权重之外还能做什么除了权重还有几个在医疗场景里特别有效的小技巧。第一把连续型特征做“医学化分箱”。比如年龄、体重指数、某项化验指标很多疾病的发病风险其实是非线性的甚至存在“窗口区间”。直接喂原始数值给树模型它能学非线性但需要较多数据才能充分刻画。分箱之后例如把年龄做成“0-18、19-40、41-60、60以上”可以让模型在很小的数据量下更稳定地利用这些常识性分段。第二关注特征组合。医疗指标之间的组合往往比单指标更有意义比如“某项指标偏高另一项指标偏低”比任何单指标都更接近异常。CatBoost无法自动构造这种组合特征需要人工在特征工程阶段合成。我的习惯是先把单特征跑一遍基线再依据初步特征重要性结果做几个交叉特征比如两两指标比值通常在验证集上能收获几个百分点的提升。第三如果项目允许把少数类样本的预测概率输出给临床人员。模型做初步排序把最可疑的前5%样本推给医生重点复核而不是用二分类结果直接下结论。这个思路在实际落地里非常管用因为医疗决策的最后一环始终需要专业判断模型更合适的定位是“提效工具”替医生把最需要看的那一小部分找出来。5. 写在最后的个人经验我在好几个医疗相关项目里反复用过CatBoost踩过最大的坑就是“迷信权重”。头一次处理一个失衡极其严重的数据时我一看阳性率只有1%直接设定正类权重50训练完验证集召回率漂亮得不行结果拿到独立测试集上一看假阳性多到没法看。后来才明白权重这类机制一定是跟着评估目标走的先想清楚“漏诊一个代价多少、误诊一个代价多少”再换算成权重和阈值整个流程才真正闭环。所以如果你现在正准备上手我的建议是第一次跑就当练手用SqrtBalanced开自动权重训练完看PR-AUC再用阈值扫描找F2最优切分点把这条链路跑通之后再回来慢慢调深度和正则。模型参数的坑可以慢慢踩但评估心态一定要从“准确率高不高”切换成“少数类找得准不准”这一步想通了后面所有操作都顺了。