
最近帮某团队做医疗AI项目碰到一个很现实的难题模型效果已经到瓶颈想再提升就得拿更多真实病历来训练可医院那边的数据管理非常严格别说导出原始数据连在开发机上查看脱敏后的样本都要走审批流程跑一轮下来一周时间就没了。后来我们换了思路让原始数据留在内网先用字段统计和分布分析把数据规律摸清楚再用CTGAN训练一个生成模型把模型参数带出来批量生成合成医疗数据用于后续开发。整个过程走完我最大的感受是合成数据不是“凭空造数据”而是把真实数据的统计规律“迁移”出来。这篇文章会把我的方案选型、训练配置、质量评估方法、隐私风险控制以及几次翻车后总结的排查经验都整理出来给正在做同类事情的读者一份能直接抄作业的参考。1. 项目背景与整体设计思路1.1 医疗数据为什么这么难用医疗数据有两个天然痛点隐私等级高和样本分布极不均衡。我参与的这个项目属于慢病管理方向原始数据集来自某合作医院的信息系统字段包括年龄、性别、主诊断、合并症数量、收缩压、空腹血糖、总胆固醇、是否30天内再入院等。总样本量约为12000条听起来不算少但真正“再入院”的阳性样本只有800条左右。直接把这种不平衡数据丢给模型训练模型很容易偷懒把所有样本都预测成“不再入院”因为这样准确率也能到百分之九十以上。想解决样本不均衡常规做法是先做上采样或者过采样。但问题是医疗字段之间不是简单的线性关系比如血糖异常和主诊断类别之间、合并症数量与再入院风险之间都存在复杂的交互。用传统插值方式生造出来的样本医生一眼就能看出不合理放到模型里反而会引入噪声。另一个更棘手的问题是数据出不了内网。医院允许你在内网环境里查看统计结果但原始记录级数据不能直接拷贝到外部开发环境。这就意味着要么开发团队全部搬进医院驻场要么找到一种能把“数据规律”带出来但不暴露个体记录的方法。合成数据正好卡在这个需求点上。1.2 合成数据在项目里的实际定位很多人对合成数据有误解以为它是用来“以假乱真”糊弄项目的。其实不是。在我们这个项目里合成数据的定位非常明确作为研发阶段的模型训练原料和算法调试数据而绝对不替代真实数据做临床验证。具体用途有三个第一个是模型训练。我们把合成数据下发到外部GPU服务器上用来做特征筛选、超参数搜索、模型结构对比。这些实验每天要跑几十轮如果都用真实数据光审批流程就够让人崩溃。第二个是算法演示。项目要做成果汇报需要跑一个交互式demo给业务方看。真实数据不能出现在演示环境里合成数据可以放心展示字段结构、统计分布都跟真实数据高度接近演示效果不打折扣。第三个是跨团队协作。标注团队、算法团队、测试团队分散在不同地点原始数据无法共享。合成数据作为“公共数据集”让各团队在统一的数据形态下工作避免每个人都去申请原始数据权限。但这里必须提醒一句合成数据再怎么逼真也不能用于诊断决策支持系统的最终临床验证。它保住了分布的“形”但未必保住了所有医学逻辑的“神”尤其是罕见并发症、极端生理状态这些长尾情况合成数据很难覆盖完整。项目里一定要把这条边界划清楚。1.3 为什么最终选了CTGAN而不是其他方案做表格式合成数据的方案其实不少我在项目初期也做过一轮对比包括SMOTE、VAE、贝叶斯网络和CTGAN。简单说下我的判断依据。SMOTE的原理是在现有样本的近邻之间做线性插值适合处理连续值主导的小规模不平衡数据但它几乎不会产生“新组合”只是在已有样本的邻域内打转而且对离散特征的支持很差。放在医疗数据这种离散列多、类别组合复杂的场景里效果不好。VAE训练稳定生成速度也快但生成的数值普遍偏“糊”连续变量的分布会被过度平滑类别变量容易出现模棱两可的概率输出。对于需要精确模拟多峰分布的场景VAE不够用。贝叶斯网络这类方法擅长描述离散变量之间的条件依赖可一旦连续变量占比高网络结构学习就会变得困难而且对字段间复杂非线性交互的表达能力有限。CTGAN属于GAN家族专门为表格数据设计。它最大的优势是同时处理离散列和连续列连续列的多峰分布有对应的归一化机制离散列的稀有类别有专门的条件生成策略。虽然训练不稳定、调参成本高但在生成质量上限上明显优于前面几种方案。方案连续值多峰分布离散稀有类别生成新组合训练稳定性SMOTE差差差稳定VAE中中中稳定贝叶斯网络中中中稳定CTGAN好好好需调参实际项目里我们也不是完全排斥其他方法。数据量只有几百条时贝叶斯网络反而比CTGAN更实用但如果要生成几万条且要求分布接近真实数据CTGAN是更合适的选择。2. CTGAN核心机制拆解它到底改进了什么2.1 把GAN用到表格数据上难在哪GAN的基本思路是让两个网络互相博弈生成器负责从随机噪声中伪造样本判别器负责分辨输入是真实样本还是伪造样本。训练的目标是让生成器产出的数据越来越接近真实分布直到判别器分不出来。打个通俗的比方生成器是印假钞的判别器是验钞的假钞印得越真验钞的水平也被逼得越高最后两边达到动态平衡。但图像GAN可以直接把像素矩阵当作连续向量处理表格数据就不一样了。表格里每一列都有明确的业务含义变量类型混合有的列是连续值比如血糖的数值有的列是离散值比如性别、诊断类别。离散值不能直接走梯度下降因为“取哪个类别”是一个不可微的选择。更麻烦的是连续变量往往不是简单的高斯分布血糖数据可能出现“低、正常、高”三个峰训练时如果拿单一正态分布去拟合生成出来的数值大概率都会聚集在平均值附近完全丢失多峰信息。CTGAN在论文里提出了三个关键设计来应对这些问题模式归一化、条件生成、以及离散列的softmax映射。下面逐个说。2.2 模式归一化先选“峰”再定“值”模式归一化是CTGAN处理连续列的核心手段也是它区别于普通GAN的关键设计。面对一个多峰的连续变量CTGAN不会尝试用一个高斯分布去描述整个分布而是先用变分高斯混合模型估计这个变量的概率密度分布自动找出若干个高斯分量每个分量对应一个“模式”。以空腹血糖为例模型可能学出“低血糖区间”“正常空腹区间”“偏高区间”三个模式每个模式都有自己的均值和方差。生成数据时分成两步第一步根据模式权重随机选择一个模式第二步在选定模式对应的归一化空间里生成一个连续数值再逆变换回真实范围。这样一来生成器不需要直接预测血糖数值而是先做一次“模式选择”再在模式内部做精细的数值生成多峰特性就能被保留下来。这里有一个人工干预的小技巧如果业务上你知道某个连续变量的峰数或者大概分布形态可以在预处理阶段做一次对数变换或者分组统计帮助模型更稳定地拟合。但别做过头比如手动把连续变量强制分箱会让CTGAN的模式归一化失去意义生成质量反而变差。2.3 条件生成与按频率采样保住稀有类别医疗数据里类别不平衡是常态。比如“30天内再入院”这个标签只有6.7%的阳性率再比如某个主诊断类别只出现了十几次。传统GAN训练时生成器会倾向于只生成高频类别因为这样最容易骗过判别器结果是合成数据里罕见类别几乎消失相当于把本来就稀少的信息又丢掉了一部分。CTGAN的做法是引入条件生成机制。每一条训练样本都对应一个条件向量用来指定当前批次里应该生成哪个类别。训练时不是按原始频率均匀抽样而是按类别频率的对数来采样让稀有类别被“强制”分配到训练批次中。这就是log_frequencyTrue这个参数背后的逻辑原始频率做log变换后高频类别和低频类别的采样差距被压缩稀有类别不再被淹没。我直观测试过一次同一份数据集log_frequency关闭时占比0.5%的某个诊断类别在生成数据里几乎消失只剩几条打开之后能恢复到接近真实比例。如果你发现合成数据的离散列分布明显偏离真实数据第一排查点就是这里。2.4 离散列如何参与反向传播离散列的问题在于“选择类别”这个动作不可微。CTGAN让生成器为每个离散列输出一个softmax概率分布代表各个类别的概率再用Gumbel-softmax之类的技巧做近似采样从而让梯度能穿过离散选择回流到生成器。判别器侧也做了配套处理连续变量和离散变量经过嵌入映射后拼接在一起再送入全连接网络。这里的目的是让离散列不再以孤立的one-hot向量形式参与计算而是先映射到一个稠密向量空间便于神经网络捕捉类别之间的潜在关系。理解了这套机制你就明白为什么CTGAN的输入不能带空值、不能带纯粹的文本长字符串。它本质上是一个数值和类别概率的处理框架文本字段必须先做编码或者直接剔除。3. 数据准备与关键预处理3.1 字段设计不是越多越好很多人在做合成数据的第一步就栽了跟头把能拿到的字段全塞进去几十列往CTGAN里面一丢训练时间翻倍生成质量还稀烂。表格GAN对字段数量非常敏感。字段越多生成器需要学习的联合分布就越复杂但训练样本没有等比例增加最后每个字段都学了个皮毛。我在项目里的做法是先明确业务问题需要哪些字段再做回退。比如项目模型预测的是“是否30天内再入院”那么核心输入字段就是年龄、性别、主诊断、合并症数量、收缩压、空腹血糖、总胆固醇、再入院标签共8个字段。其它像住院编号、科室代码、医生编号这类纯标识性字段统计意义不大全部剔除。一个实用的字段清单示例字段名数据类型说明ageint年龄genderstr性别diagnosisstr主诊断类别comorbidity_countint合并症数量blood_pressure_systolicfloat收缩压glucosefloat空腹血糖cholesterolfloat总胆固醇readmissionbool是否30天内再入院布尔字段要转成字符串类别“是”/“否”或者0/1CTGAN对离散列和连续列的识别方式不同分类逻辑要明确。3.2 清洗原则别把缺失模式也“洗掉”CTGAN本身不接收空值所以缺失值必须提前处理。但处理方式决定了生成数据能否保留真实数据的缺失规律。我见过两种极端做法一种是把所有含缺失值的行直接删掉简单粗暴但样本量小的场景会损失太多信息另一种是用均值填充所有缺失结果缺失比例较高的字段被强行填成一个人为的集中值分布被人为拉瘦。更推荐的做法是把“是否缺失”本身作为一个类别特征保留。比如“血糖”字段缺失的样本不要填血糖均值而是给血糖设一个“missing”类别或者在另一个字段上标记。这样CTGAN能学到“什么情况下血糖容易缺失”这种真实模式生成的合成数据里缺失分布会更贴近原始数据。还有一个细节离群值的处理尺度要控制。血压值超过300、血糖值为负数这类物理上不可能的值直接过滤掉但年龄偏大、血糖偏高等业务合理性范围内的“看似异常”不要轻易删它们往往是长尾分布的重要组成删除后生成数据会把极端风险人群丢掉后续建模反而会偏乐观。3.3 分布检查提前知道数据长什么样训练前我会做一张单变量分布概览把每个连续字段的直方图和离散字段的柱状图打印出来先做到心里有数。重点关注三件事连续字段是否多峰、是否严重右偏、离散字段的类别是否过于稀疏。看清楚之后再决定是否需要预处理。CTGAN自带的模式归一化能处理多峰所以一般不需要手动分桶但是如果某个连续字段严重右偏比如住院费用这种动辄偏到几百倍的字段生成器拟合起来还是有压力。这种字段可以提前做log变换让分布更集中。不过要注意变换后的数值在生成之后还要逆变换回原始业务单位否则业务方拿到的不伦不类。3.4 样本量参考几百条和几万条策略完全不同CTGAN对样本量的容忍度比想象中高但质量分水岭很明显。样本量低于500条时生成结果几乎没有实用价值模式学不完整类别组合稀疏。500到5000条时能跑通但需要反复调参产出只能用于粗筛。5000条以上时生成的合成数据基本可以在研发阶段替代真实数据使用。我们这次用12000条原始数据生成了50000条合成样本分布对比各项指标都达标。如果你的数据量很少建议先用贝叶斯网络生成一版做基线不要一上来就硬上CTGAN。4. 完整实操过程训练、生成与评估4.1 环境准备和安装安装CTGAN只需要Python环境和pip但它依赖PyTorch所以机器的显卡驱动和CUDA环境要提前确认。如果有GPU训练速度快很多没有GPU也能跑只是epoch要适当减少。pip install ctgan我用的Python版本是3.9。需要注意ctgan的版本差异0.x版本和1.x版本的API有些许变化比如个别参数名不同、默认值不同。建议安装后先输出一下版本再对着版本查文档避免看着别人的老代码报错。4.2 训练代码与关键参数直接放核心训练代码这段是从项目里抽出来的可用版本。import pandas as pd from ctgan import CTGAN # 读取原始数据 real_data pd.read_csv(medical_data.csv) # 指定离散列 discrete_columns [ gender, diagnosis, readmission ] # 初始化CTGAN ctgan CTGAN( epochs300, batch_size500, log_frequencyTrue, verboseTrue, cudaTrue, ) # 训练 ctgan.fit(real_data, discrete_columnsdiscrete_columns) # 生成50000条合成数据 syn_data ctgan.sample(50000) # 保存 syn_data.to_csv(synthetic_medical.csv, indexFalse)参数选择这里说下我的经验。epochs不是越大越好。300轮在这个数据规模下已经足够继续增加到500轮以上生成质量提升非常有限训练时间却成倍增加。判断标准不是loss而是看生成数据的分布指标是否收敛。我试过一轮200场的实验真实数据和合成数据在各项指标上的差异在300轮之后几乎没有变化。batch_size建议在256到1024之间尝试。医学数据字段不多的话500是一个比较稳妥的中间值。batch太小生成器容易被单批次带偏batch太大训练过程变慢而且稀有类别的条件生成效果会下降。log_frequencyTrue一定要开上一节已经解释过原因这里不再重复。cudaTrue在有GPU的环境下打开。CPU环境下也能训只是同一份数据可能要多等几个小时。4.3 生成样本时的两个注意事项sample()函数接收你想要的条数但一次性生成几十万条可能会把内存吃爆。建议分批次生成每次生成一两万条然后拼接保存对后续处理也更友好。另外CTGAN的生成过程还依赖随机种子。如果你希望实验结果可复现在训练前设置好随机种子import numpy as np import torch np.random.seed(42) torch.manual_seed(42)不过要说明受PyTorch底层并发影响即使设了种子跨机器环境也不保证完全复现但同一环境下基本稳定。这个特性在做实验对比时很重要。4.4 训练过程中的观察点CTGAN训练时会打印一个个epoch的进度但GAN的loss不像普通分类任务那样单调下降。生成器和判别器在互相博弈loss上下抖动是正常的。我见过很多人在这个阶段被吓到以为模型崩了。实际上你更应该关注的是每训练几十轮后生成一批样本人工看一下字段分布有没有逐渐向真实数据靠拢。loss数值本身没有绝对意义生成结果才是检验标准。实操中还有一个冒烟测试的思路拿真实数据里随机抽200条做一个极小规模训练把整个pipeline跑通确认代码没报错、生成数据能保存再上全量数据。这样能省下大量排错时间。5. 质量评估与隐私风险控制5.1 相似度评估从三个层面打分很多人觉得合成数据“看起来像”就行了这不够。我用三个层面的指标来评估缺一个都不放心。第一层是单变量分布。对连续字段比较真实数据和合成数据的均值、标准差、分位数再用KS检验看分布是否一致p值大于0.05通常说明没有显著差异。对离散字段对比每个类别的占比计算相对误差。第二层是多变量相关性。单变量分布一致不代表变量之间的关系也一致。我计算真实数据和合成数据的Pearson相关系数矩阵然后对比两者差值。比如真实数据中“年龄”和“再入院”之间有0.3的正相关如果合成数据里这个相关系数变成0.05说明变量关系丢了。出现这种情况通常要回去调整epochs和网络结构。第三层是模型效用。这是最贴近业务的一关把合成数据当作训练集真实数据当作测试集跑一个简单的逻辑回归或者树模型看AUC表现是否接近真实数据训练出来的基准。如果能达到基准的90%以上说明合成数据保留了足够的预测信息。如果连60%都不到说明合成数据只是“形态像”内在关系没学好。5.2 隐私风险评估别只看分布像不像合成数据项目的出发点往往是为了保护隐私但如果不做风险评估可能会闹出“伪匿名”的笑话。我主要做两类风险测试。第一类是最近邻距离检查计算每一条合成记录到所有真实记录的最小欧氏距离。如果大量合成记录与某条真实记录的距离几乎为零说明生成器把原始样本“背下来了”这属于严重的记忆泄露。正常情况下合成数据和真实数据的最小距离分布应该与真实数据内部的最小距离分布接近不能出现大量距离远小于正常水平的点。第二类是成员推断测试。拿一部分真实数据训练一个简单的二分类器特征保持不变标签是“该样本是否在训练集中”用合成数据和剩下的真实数据做对照看分类器能不能猜中样本来源。如果准确率明显高于随机水平说明合成数据携带了太多容易被识别的个体特征。如果你打算把合成数据对外公开这些测试必须做。如果只在公司内部研发使用风险等级可以适当降低但对极端值字段还是建议做一次最近邻检查。5.3 差分隐私扩展什么时候值得上CTGAN本身不提供数学意义上的隐私保证它只是“让数据不那么容易被还原”。如果业务方要求强隐私保证比如对外发布数据集那就得考虑差分隐私版本。差分隐私的核心思想是在模型训练过程中加入受控噪声让输出结果对“某一条具体样本是否存在”不敏感。听起来很完美但代价是生成质量明显下降。我们项目评估过DP方案的合成数据单变量分布还行多变量相关性损失较大模型效用比普通CTGAN版本低了差不多十个百分点。我的态度是内部研发用普通CTGAN数据不出内网的前提下风险可控外部共享或发布再上DP方案同时接受效果折损。不要一上来就追求最强隐私否则业务效果可能会被牺牲到没法用。6. 常见问题与排查技巧实录6.1 生成结果全是同一类样本这是典型的模式崩溃。CTGAN的训练本质上是对抗博弈如果生成器找到一个能稳定骗过判别器的“捷径”它就会放弃探索其它模式只输出同一种样本。排查时先确认判别器和生成器的结构是否平衡。可以增大batch_size让判别器看到更多样的真实样本也可以调整discriminator_steps参数增加判别器每轮更新的次数逼它变得更强迫使生成器提高多样性。还有一个笨但有效的办法把embedding_dim和gen_dim调小一点限制生成器的容量减少它“死记硬背”的能力。6.2 连续变量生成出负值或离谱数值如果生成数据里出现血糖负数、收缩压3000这种物理上不可能的值多半是预处理阶段出了问题。最常见的原因是原数据里的离群值没有清干净模式归一化被极端值带偏导致某个高斯模式的方差被拉得过大。另一个原因是对连续字段做了过度变换。比如先做z-score标准化再训练CTGAN自己会再做归一化一来一回反而放大误差。建议只保留业务上必要的变换比如log变换其他交给模型自己学。6.3 类别占比和原始数据差异大出现这种情况优先检查log_frequency是否开启。我在项目里做过对照实验关闭后低频类别在合成数据中的占比会显著下降。如果已经开启但问题依旧考虑把样本量过少的类别适度合并。比如某种主诊断只有几十条可以合并到“其他”类别里先让主干分布学对再去补细节。6.4 训练速度慢得离谱训练慢通常不是机器的错而是参数和数据规模不匹配。字段数量多、batch_size大、epochs高都会让训练时间成倍增长。我的做法是先用1000条数据、50个epoch跑通流程确认管线没有问题再用全量数据正式训练。正式训练时初始epochs不要直接飙到1000先用200一轮的配置跑一遍质量评估如果分布指标还在明显提升再往上加。医学数据字段间的非线性关系并不像图像那样复杂多数场景300轮已经够用。6.5 问题排查速查表现象可能原因建议解决方式生成样本类别单一模式崩溃调大batch_size增加判别器更新次数限制生成器容量连续值范围异常离群值未处理或过度变换清洗极端离群值减少不必要的归一化/标准化稀有类别比例偏低条件生成未生效确认log_frequencyTrue合并过少类别训练loss严重震荡GAN对抗正常现象不只看loss定期生成样本并检查分布训练时间过长参数或字段设置不合理用少量数据冒烟测试先跑200轮看趋势模型效用差距大变量关系学得不充分增加epochs检查关键字段之间的相关性指标最后再分享一个这几个月积累下来的习惯每次训练完我不会直接拿生成数据往下游业务丢而是先跑一遍“真实数据内距离分布”和“合成数据到真实数据最近距离分布”的对比图如果后者出现明显的左移峰值说明模型开始记忆个别样本了需要重新清洗或调整训练策略。合成数据这件事把它当成一个统计工具来用比把它当成“数据魔术”要踏实得多。你可以先从小规模项目试起把分布对比和质量评估流程固定下来再去挑战更复杂的场景。