简单生成有用合成数据:Copula+KDE任务对齐方法

发布时间:2026/7/19 5:52:14
简单生成有用合成数据:Copula+KDE任务对齐方法 1. 项目概述为什么“简单生成有用合成数据”这件事比你想象中更紧迫也更值得深挖“Simple Method to Generate Useful Synthetic Data”——这个标题乍看平平无奇甚至有点“反直觉”合成数据Synthetic Data不就该是算法驱动、模型堆叠、GAN训练、差分隐私加噪的复杂工程吗怎么还能“简单”又凭什么敢称“有用”但过去三年我带团队落地的17个AI项目里有12个卡点根本不在模型精度而是在数据这一环医疗影像标注成本超300元/张金融风控场景原始交易数据因合规无法出域工业质检新产线投产前零样本教育类APP想做个性化推荐却连500条真实用户行为都拿不到……这时候“简单生成有用合成数据”不是技术炫技而是业务能否启动的生死线。它解决的不是“有没有数据”的问题而是“有没有合规、可控、可解释、能直接喂进下游任务的数据”的问题。关键词里的“Simple”不是指随便用sklearn.make_classification糊弄而是指方法路径短、依赖组件少、参数可调性高、结果可验证“Useful”则明确指向下游任务效果提升——不是FID分数好看而是分类准确率不掉点、检测mAP稳定、A/B测试转化率正向。适合谁一线算法工程师、MLOps工程师、数据产品经理、甚至懂基础Python的业务分析师——只要你需要快速构建最小可行数据集MVDS而不是等法务走完六个月数据协议。我试过用Diffusion生成CT切片结果医生第一眼就说“边缘太软不像真实扫描伪影”也试过用SMOTE扩增信用卡欺诈样本模型上线后误拒率飙升——这些坑恰恰说明合成数据的价值不在“像”而在“管用”。下面我会从设计逻辑、核心细节、实操步骤到排障经验一层层拆解一个真正能在周级别交付、被业务方签收的合成数据方案。2. 内容整体设计与思路拆解放弃“以假乱真”拥抱“任务对齐”2.1 为什么主流方案在实际场景中频频失灵先说结论90%的合成数据失败源于目标错位。学术论文追求“分布相似性”distributional similarity用Wasserstein距离、MMD、FID等指标衡量合成样本与真实样本的统计距离但业务场景要的是“任务有效性”task utility——即合成数据训练出的模型在真实测试集上的性能是否达标。这两者之间存在巨大鸿沟。我曾用WGAN-GP生成电商用户购买序列FID低至8.2优于基线但用它训练的推荐模型在A/B测试中CTR下降1.7%复盘发现GAN完美复刻了高频购买组合如“纸巾洗衣液”却完全丢失了长尾但高价值的关联如“婴儿车汽车安全座椅”因为后者在原始数据中仅占0.3%GAN的梯度更新根本“看不见”它。这就是典型的“统计相似≠任务有效”。再看另一个陷阱过度依赖生成式大模型。最近很多团队直接调用Llama-3或Qwen生成文本合成数据比如让模型编造1000条“客户投诉电话记录”。问题在于大模型输出受提示词prompt强引导一旦提示词隐含偏见如“请描述一次愤怒的投诉”合成数据就会系统性放大情绪极端化更致命的是模型生成的文本缺乏真实业务约束——真实客服系统有严格的话术模板、转接规则、工单字段格式而LLM生成的“自由发挥”内容下游NLP模型一接入就报错。我们做过对照实验用相同prompt生成1000条投诉文本人工抽样检查发现23%包含虚构的公司部门如“区块链合规部”17%出现不存在的产品型号这些错误在训练时会污染模型认知上线后导致意图识别准确率暴跌。所以本方案的设计原点非常清醒不追求全局分布拟合只锚定下游任务的关键决策边界。比如做信贷审批模型核心是区分“可贷”与“拒贷”人群那合成数据就聚焦在信用分550–650这个敏感区间真实数据中此处样本稀疏用插值扰动生成大量边界案例做OCR文字识别就只合成字体、模糊度、背景噪声等影响识别率的关键变量而非费力渲染整张发票的3D光照效果。这种“任务对齐”Task-aligned Synthesis思路把问题从“生成整个数据空间”降维成“生成关键子空间”自然就“简单”了。2.2 方案选型为什么最终锁定“统计建模可控扰动”而非端到端生成在反复试错后我们放弃了三种主流路径最终选择一条看似“复古”但极其稳健的路线基于真实数据统计特征的参数化建模叠加物理/业务规则驱动的可控扰动。具体来说就是三步走① 用Copula模型捕获多变量依赖结构② 用核密度估计KDE拟合各变量边缘分布③ 在合成过程中按业务规则注入扰动如时间序列加符合设备采样误差的高斯噪声图像加符合光学镜头特性的运动模糊。这个选择背后有硬核理由第一可解释性与可控性碾压生成式模型。Copula模型明确告诉你“收入”和“负债比”的相关性由Gaussian Copula参数ρ0.62控制如果业务方说“年轻人收入与负债比相关性应更低”你直接把ρ调到0.35即可无需重新训练整个GAN。而GAN的隐空间是黑盒想调整一个相关性得反复试错修改损失函数权重耗时耗力。第二小样本鲁棒性极强。当真实数据只有200条时GAN会过拟合噪声生成大量重复样本而KDE在样本量50时就能给出稳定密度估计Copula对样本量要求更低——我们用仅87条工业传感器时序数据成功合成出覆盖全部故障模式的10000条样本下游LSTM故障预测模型F1-score提升12.3%。这得益于Copula的数学本质它分离了“变量如何分布”边缘分布和“变量如何关联”依赖结构即使边缘分布估计有偏差只要依赖结构正确关键业务关系就不会崩。第三合规风险天然可控。生成式模型可能意外复现真实个体的敏感信息如某患者的罕见病组合而参数化模型输出的是统计抽象不存储任何原始样本。更重要的是所有扰动都基于公开物理模型如ISO 12233标准定义的MTF曲线用于图像模糊或行业规范如PCI-DSS对信用卡号掩码规则审计时可逐条溯源法务一眼就能认可。当然这条路也有代价它无法生成超出原始数据范围的“创造性”样本比如用汽车数据合成飞机引擎故障。但我们的经验是95%的业务场景缺的不是“想象力”而是“够用的、不出错的”数据。当你需要在两周内让新产线AI质检系统上线能稳定识别划痕、凹坑、锈迹这三类缺陷的5000张图远比生成100张“艺术感十足”的未知缺陷图更有价值。3. 核心细节解析与实操要点CopulaKDE的工业级实现细节3.1 Copula建模不止是“选一个函数”关键是业务语义映射Copula的核心思想是Sklar定理任意联合分布F(x,y)可分解为边缘分布F_X(x)、F_Y(y)和一个连接函数C即F(x,y)C(F_X(x), F_Y(y))。但实操中90%的人只停留在“用scipy选一个Copula类型”这是巨大误区。真正的难点在于如何让Copula参数对应业务可理解的含义以金融风控为例原始数据含4个关键变量age年龄、income年收入、loan_amount贷款金额、default_flag是否违约。我们发现age与income呈弱正相关ρ≈0.2但loan_amount与default_flag在income15w时强负相关借越多越可能违约在income15w时弱正相关高收入者借多也稳。如果强行用单一Gaussian Copula拟合会把这种分段依赖“平均化”导致合成数据在高收入群体中生成大量不合理的大额贷款违约案例。解决方案是分层Copula建模Hierarchical Copula先用K-means对income聚类自然分成“低收入15w”、“中收入15–50w”、“高收入50w”三组对每组分别拟合独立Copula低收入组用t-Copula捕捉尾部相关性即极端情况下的强关联中收入组用Clayton Copula侧重下尾相关即低收入者违约风险聚集高收入组用Gumbel Copula侧重上尾相关即高收入者大额贷款倾向合成时先按原始数据中各组占比如35%:45%:20%抽样选择Copula再生成变量。这样做的业务价值立竿见影合成数据中低收入组的loan_amount与default_flag呈现真实的“借多必违约”模式高收入组则体现“借多但违约率低”的健康状态下游模型学到的决策边界与真实业务逻辑完全一致。我们用此方法生成5000条数据训练XGBoostAUC从0.72用全量Gaussian Copula提升至0.81且SHAP值分析显示模型对income分段的依赖强度与业务专家经验高度吻合。提示Copula类型选择有速查表。Gaussian Copula适合中度线性相关t-Copula适合有共同极端风险如股市崩盘时所有股票齐跌Clayton Copula适合下尾强相关如低信用分者违约率集体飙升Gumbel Copula适合上尾强相关如高净值客户同时购买多款理财产品。别死记公式记住业务场景对应关系。3.2 KDE边缘分布拟合如何避免“过平滑”与“过尖锐”的双重陷阱核密度估计KDE用公式表示为$$\hat{f}h(x) \frac{1}{n}\sum{i1}^n K_h(x - x_i)$$其中$K_h$是带宽为$h$的核函数。带宽$h$的选择直接决定合成数据的“保真度”与“泛化性”平衡。我们踩过的最大坑是盲目使用scikit-learn默认的bandwidthscott——它在小样本时导致严重过平滑。举个真实案例某医疗器械公司需合成心电图ECGR波峰值电压数据。原始数据仅120个样本scott带宽算出来是0.85mV而实际R波电压集中在1.2–1.8mV窄区间。用此带宽KDE生成的分布像一摊“大饼”峰值电压跑到0.5mV或2.5mV完全脱离生理常识。后来我们改用交叉验证带宽选择CV bandwidth selection但发现CV在小样本下波动极大——120个样本做5折CV每次选的最优带宽标准差高达0.3mV。终极解法是业务知识引导的带宽约束下限设为0.1 * IQRIQR为四分位距确保不会过度平滑保留关键峰谷上限设为0.5 * stdstd为标准差防止噪声被放大最终带宽取max(0.1*IQR, min(0.5*std, CV_bandwidth))。对ECG数据IQR0.3mVstd0.4mVCV选中带宽是0.6mV最终取max(0.03, min(0.2, 0.6)) 0.2mV。用此带宽KDE精准复现了1.4mV处的主峰和1.6mV处的次峰合成数据经心内科医生盲评87%认为“符合临床常见R波形态”。另一个关键细节是核函数选择。高斯核最常用但对有硬边界的变量如年龄不能0贷款金额不能0会产生“边界效应”——KDE在0附近生成负值概率。此时必须用有界核函数如Beta核适用于[0,1]区间或三角核计算快适合实时合成。我们处理信用卡账单金额≥0时用三角核反射边界处理将负值镜像到正值区彻底消除非法值。3.3 可控扰动注入让合成数据“活”起来的业务规则引擎合成数据最怕“死气沉沉”——所有样本都像从同一台完美仪器测出毫无现实世界的毛刺感。但随机加噪又容易破坏关键特征。我们的解法是构建轻量级业务规则引擎按变量类型注入符合物理/业务逻辑的扰动。以工业视觉检测为例原始数据是1000张清晰标注的PCB板图片。我们需要合成不同质量等级的样本光学扰动用OpenCV模拟镜头畸变。不是简单加高斯模糊而是根据真实产线镜头参数焦距25mm光圈f/2.8计算点扩散函数PSF再用cv2.filter2D卷积。实测发现用f/2.8的PSF合成的模糊与真实产线相机拍出的模糊在FFT频谱上重合度达92%。照明扰动真实产线灯光有±5%照度波动。我们不加均匀噪声而是按朗伯余弦定律Lamberts cosine law模拟角度衰减对图像每个像素计算其法向量与光源夹角θ亮度乘以cos(θ)再叠加±5%全局缩放。这样合成的阴影过渡与真实PCB板曲面反射完全一致。标注扰动真实标注员对微小划痕0.1mm存在±0.03mm的定位误差。我们用二维高斯采样模拟对每个标注框中心点加N(0,0.015²)偏移对框尺寸按Uniform(0.95,1.05)缩放。这样生成的标注既保持几何合理性又反映人工标注的真实离散性。这套规则引擎用纯Python实现不到200行代码但效果惊人用合成数据训练YOLOv8mAP0.5从原始数据训练的68.2%提升至71.5%且在真实产线视频流上推理时漏检率下降40%——因为模型真正学会了在各种噪声下稳定识别划痕而非死记硬背清晰图中的像素模式。注意所有扰动参数必须来自真实测量我们曾用“凭经验”设的±10%照度波动导致合成图像过曝模型学会把过曝区域当缺陷。后来用照度计实测产线100个点位得到真实波动范围是±4.7%修正后问题消失。记住合成数据的权威性始于第一手测量数据。4. 实操过程与核心环节实现从原始数据到可用合成集的完整流水线4.1 环境准备与依赖安装极简但精准的工具链本方案追求“开箱即用”所有依赖均选社区成熟、文档完善、无专利风险的开源库。实测在Ubuntu 22.04 Python 3.9环境下100%通过# 创建隔离环境强烈建议 python -m venv synthetic_env source synthetic_env/bin/activate # 安装核心库版本锁定避免兼容问题 pip install numpy1.24.3 pandas2.0.3 scipy1.11.1 scikit-learn1.3.0 pip install copulas0.8.0 # 专为Copula设计的工业级库比statsmodels更易用 pip install opencv-python4.8.0 # 图像扰动必备 pip install matplotlib3.7.2 # 可视化诊断关键点说明copulas库是本方案基石。它封装了20种Copula类型提供MultivariateGaussianCopula、MultivariateStudentTCopula等开箱即用类且内置fit()和sample()方法一行代码完成拟合与采样。比手动用scipy.stats写Copula推导省时90%。版本锁定非过度谨慎。copulas0.8.0修复了0.7.x中t-Copula自由度估计的数值不稳定bugopencv-python4.8.0是最后一个支持Python 3.9且无GPL传染风险的版本后续版本引入了FFmpeg GPL模块。绝不安装PyTorch/TensorFlow。本方案刻意避开深度学习框架降低环境复杂度。所有计算在CPU上完成10000条样本合成耗时3秒i7-11800H。实操心得首次运行前务必用copulas.test()验证安装。我们曾因conda环境混用pip安装导致copulas调用底层C库失败报错ImportError: libgomp.so.1: cannot open shared object file。解决方案是conda install -c conda-forge libgomp而非重装整个环境。4.2 数据预处理清洗不是目的是为建模铺路预处理阶段的目标不是“让数据变干净”而是暴露数据内在结构为Copula建模扫清障碍。我们坚持三个铁律铁律一绝不删除离群值改为建模其生成机制。真实数据中的离群值常蕴含重要业务信号如某客户突然大额转账可能是欺诈或奖金发放。删除它们等于抹去关键模式。正确做法是用Isolation Forest识别离群样本将其单独聚类为该簇拟合独立Copula。例如在电商数据中我们发现0.8%的订单金额10万元构成独立簇其user_age与order_time呈现强负相关年轻用户深夜下单高价商品这与主数据簇的正相关完全相反。为该簇单独建模后合成数据能真实反映“高净值年轻用户”这一细分群体行为。铁律二类别变量必须编码为有序整数且保留业务序。education_level字段含[High School, Bachelor, Master, PhD]若用one-hot编码Copula会丢失“学历越高收入倾向越高”的序关系。正确做法是映射为[0,1,2,3]并在KDE拟合时指定discreteTruecopulas库支持。这样Copula能学习到education_level2Master与income的强正相关而education_level0High School与income弱相关。铁律三时间序列必须分解为平稳分量。原始销售数据含明显季节性和趋势。若直接对原始序列建模Copula会学到虚假的“12月销售额高→1月必然低”的错误依赖。必须先用STL分解statsmodels.tsa.seasonal.STL提取趋势、季节、残差三部分只对残差分量建模Copula因其近似平稳合成后再叠加回趋势和季节。我们处理某快消品销量数据时此步骤使合成数据的月度波动系数CV从原始数据的0.32降至0.33几乎无损而未分解直接建模的CV飙升至0.47严重失真。预处理代码精简版核心逻辑import pandas as pd from copulas.multivariate import MultivariateGaussianCopula from sklearn.preprocessing import OrdinalEncoder from statsmodels.tsa.seasonal import STL def preprocess_data(df): # 类别变量有序编码 cat_cols [education, region] encoder OrdinalEncoder(handle_unknownuse_encoded_value, unknown_value-1) df[cat_cols] encoder.fit_transform(df[cat_cols]) # 时间序列分解以sales列为例 if date in df.columns and sales in df.columns: df df.sort_values(date).set_index(date) stl STL(df[sales], period12) # 年度周期 res stl.fit() df[sales_residual] res.resid # 仅对残差建模 df df.reset_index() return df, encoder # 使用示例 raw_df pd.read_csv(real_data.csv) processed_df, enc preprocess_data(raw_df)4.3 Copula-KDE联合建模三步完成核心拟合建模流程严格遵循“先边缘再依赖最后联合”的顺序每步均可独立验证步骤一KDE拟合各变量边缘分布from copulas.univariate import GaussianKDE # 为每列单独拟合KDE univariates {} for col in processed_df.columns: if col in [date, sales_residual]: # 跳过时间索引和已处理列 continue kde GaussianKDE() kde.fit(processed_df[col].dropna().values) # 自动处理缺失值 univariates[col] kde # 验证绘制原始vs合成边缘分布 import matplotlib.pyplot as plt fig, axes plt.subplots(2, 2, figsize(12, 8)) for i, col in enumerate([age, income, loan_amount, default_flag]): ax axes[i//2, i%2] # 原始分布 processed_df[col].hist(bins30, axax, alpha0.5, labelReal, densityTrue) # KDE拟合分布 x np.linspace(processed_df[col].min(), processed_df[col].max(), 100) y [univariates[col].pdf(xi) for xi in x] ax.plot(x, y, r-, labelKDE) ax.set_title(f{col} Distribution) ax.legend() plt.tight_layout() plt.show()关键检查点KDE曲线必须包络原始直方图主体尤其关注尾部。若default_flag0/1变量的KDE在0.5处出现虚假峰说明样本量不足需切换为Bernoulli分布拟合copulas库支持。步骤二Copula拟合多变量依赖结构# 构建Copula模型自动选择最佳Copula类型 copula MultivariateGaussianCopula() # 初始用Gaussian后续可优化 copula.fit(processed_df[[age, income, loan_amount, default_flag]].dropna()) # 验证生成1000个样本检查相关系数矩阵 synthetic_samples copula.sample(1000) corr_real processed_df.corr().round(2) corr_syn synthetic_samples.corr().round(2) print(Real Correlation:\n, corr_real) print(Synthetic Correlation:\n, corr_syn)关键检查点corr_syn与corr_real的差异应0.05。若age-income相关系数从0.21变成0.08说明Copula拟合失败需换t-Copula或检查数据是否需分层。步骤三联合采样与逆变换# 1. 用Copula采样标准化变量[0,1]区间 copula_samples copula.sample(5000) # 2. 用KDE逆变换还原为原始尺度 final_synthetic pd.DataFrame() for col, kde in univariates.items(): # KDE的inverse_cdf方法将[0,1]值映射回原始分布 final_synthetic[col] kde.inverse_cdf(copula_samples[col]) # 3. 处理类别变量逆编码 if education in final_synthetic.columns: final_synthetic[education] enc.inverse_transform( final_synthetic[[education]].astype(int) )此步骤产出的final_synthetic已是符合原始数据统计特征和业务逻辑的合成集。我们通常额外添加一列synthetic_source标记为copula_kde便于后续追踪数据血缘。4.4 扰动注入与后处理让数据“呼吸”的最后一步合成数据需通过“扰动注入”获得生命力但必须可配置、可复现import cv2 import numpy as np def add_optical_blur(image, psf_size15): 添加符合镜头参数的运动模糊 # 构建PSF点扩散函数 psf np.zeros((psf_size, psf_size)) center psf_size // 2 # 模拟f/2.8光圈的艾里斑Airy disk主瓣 for i in range(psf_size): for j in range(psf_size): r np.sqrt((i-center)**2 (j-center)**2) psf[i,j] (2*j1(r*1.22/2.8)/ (r*1.22/2.8))**2 if r0 else 1 psf psf / psf.sum() # 归一化 return cv2.filter2D(image, -1, psf) def add_illumination_variation(image, variation0.05): 按朗伯余弦定律添加光照变化 h, w image.shape[:2] # 模拟光源在图像中心上方计算每个像素的cosθ y_grid, x_grid np.ogrid[:h, :w] cos_theta 1 - 0.001 * ((x_grid - w//2)**2 (y_grid - h//3)**2) cos_theta np.clip(cos_theta, 0.8, 1.0) # 限制范围 # 叠加全局照度波动 global_scale 1 np.random.uniform(-variation, variation) return (image.astype(float) * cos_theta * global_scale).astype(np.uint8) # 应用扰动示例 for idx, row in final_synthetic.iterrows(): img_path row[image_path] img cv2.imread(img_path) # 依概率应用扰动80%加模糊60%加光照变化 if np.random.rand() 0.8: img add_optical_blur(img) if np.random.rand() 0.6: img add_illumination_variation(img) cv2.imwrite(fsynthetic/{idx}.jpg, img)后处理黄金法则去重检查final_synthetic.drop_duplicates()若去重后样本量95%说明KDE带宽过小或Copula过拟合需调整异常值过滤对连续变量用IQR法过滤Q1-3*IQR或Q33*IQR的样本但阈值放宽至5倍IQR保留合理极端值业务规则校验编写轻量校验函数如assert (synthetic[loan_amount] / synthetic[income]) 10负债收入比10失败则剔除该行。最终我们输出synthetic_data_v1.csv结构化数据和synthetic_images/图像并附data_card.md说明合成方法、各变量扰动参数、验证指标如相关系数误差、边缘分布KL散度、适用场景限制如“不适用于预测收入100w的客户行为”。5. 常见问题与排查技巧实录那些没写在文档里的坑5.1 “合成数据训练的模型在真实数据上效果反而更差”——根本原因与破解这是最高频、最致命的问题。表面看是合成数据“不好”实则90%源于评估方式错误。我们曾用合成数据训练信贷模型测试集AUC达0.85但上线后AUC暴跌至0.62。复盘发现测试集是用同一Copula生成的“合成测试集”而非真实数据这相当于“用练习题考练习题”当然高分。正确评估四步法绝对禁用合成测试集测试集必须100%来自真实数据且与训练合成集无交集设置基线锚点用原始训练集哪怕只有200条训练同架构模型作为效果下限增量验证合成500条→训练→测试合成1000条→训练→测试…观察效果是否随合成量增加而单调提升若出现拐点如1000条时最好1500条时下降说明Copula过拟合需增大KDE带宽业务指标穿透不只看AUC更要盯住业务关心的指标。如风控场景重点看KS统计量区分好坏客户能力和拒绝率合成数据是否导致模型过于保守。我们发现当Copula中default_flag的尾部相关性被高估模型会系统性提高拒绝率此时需降低t-Copula自由度或改用Clayton。排查技巧用SHAP值分析合成数据训练模型的特征重要性排序与真实数据训练模型对比。若排序差异30%说明合成数据扭曲了业务逻辑。例如真实模型中employment_length最重要合成模型中zip_code最重要那一定是地理变量的Copula建模出了问题。5.2 “Copula拟合时报错‘Singular matrix’或‘Numerical instability’”——小样本救星方案当原始数据50条或存在高度共线变量如total_debtcredit_card_debtstudent_loan_debtCopula协方差矩阵会奇异。此时copulas库报错LinAlgError: Singular matrix。三步急救方案第一步变量降维。不用PCA而用业务驱动的变量合并。如上述债务变量直接删除total_debt只保留两个分项并在Copula中显式建模credit_card_debt与student_loan_debt的相关性。我们处理某银行数据时12个财务变量经业务合并后剩7个奇异问题消失。第二步添加微小噪声。对连续变量加N(0, 0.001²)噪声“这不是造假而是模拟测量仪器的固有精度极限”。代码df[col] df[col] np.random.normal(0, 0.001, len(df))。第三步切换Copula类型。Gaussian Copula对奇异最敏感立即换t-CopulaMultivariateStudentTCopula其自由度参数天然容忍一定数值不稳定。copulas库中t-Copula的fit()方法内置了SVD截断稳定性远超Gaussian。实操心得我们曾用47条医疗数据含blood_pressure_systolic,blood_pressure_diastolic,heart_rate建模前两步后仍报错。最终发现systolic与diastolic存在强线性关系systolic ≈ 1.5 * diastolic 40这是生理规律不是噪声。于是我们放弃对两者建模Copula改为用线性回归预测systolic只对diastolic和heart_rate建模Copula问题彻底解决。5.3 “合成图像全是模糊的或者标注框飘移出图像边界”——扰动参数失控的征兆图像扰动最易失控。典型症状合成图像整体发灰光照扰动过大或所有标注框都移到右下角坐标扰动方向错误。系统化调试流程可视化扰动中间态不直接看最终图而用matplotlib画出扰动前后的对比图、PSF核函数、cosθ热力图。我们曾发现add_illumination_variation中cos_theta计算用了y_grid - h//2中心但光源实际在h//3上方导致阴影方向全错。参数范围扫描对每个扰动参数如psf_size,variation在[0.5×target, 2×target]范围内取3个值各生成100张图人工盲评“真实性”。例如psf_size15时模糊过重psf_size8时又太锐利最终选定12。边界安全防护标注框扰动后强制执行# 确保框不越界 x1 max(0, min(w-1, x1_new)) y1 max(0, min(h-1, y1_new)) x2 max(x11, min(w, x2_new)) y2 max(y11, min(h, y2_new))独家技巧用合成数据自监督验证。对一张合成图用原始标注框裁剪