041、数据增强的艺术:超分任务中的退化模拟、裁剪策略与数据预处理
去年有个项目让我记忆犹新。团队用EDSR在DIV2K上训得好好的,PSNR飙到34.5,一上真实监控视频,直接崩到28。放大四倍后,人脸糊成一团,边缘全是锯齿。我们当时第一反应是模型不够强,换RCAN、换SAN,甚至上了SwinIR,效果有提升但远不如预期。后来排查了三天,发现是数据预处理环节出了问题——训练时用的双三次下采样,而真实场景的退化是压缩噪声+运动模糊+传感器噪声的复合体。从那以后,我养成了一个习惯:先看数据,再看模型。
退化模拟:别让模型活在理想国
超分任务最容易被忽视的坑就是“退化假设”。大多数论文默认用双三次插值下采样生成LR-HR对,但真实世界的退化过程远比这复杂。我见过太多人直接拿MATLAB的imresize或者OpenCV的resize做下采样,然后抱怨模型泛化差。
退化模型应该怎么搭?我的做法是构建一个退化池,包含多种退化操作:
# 这里踩过坑:别只用一个退化函数defbuild_degradation_pool():pool=[]# 模糊核:高斯、运动、散焦pool.append(lambdax:gaussian_blur(x,sigma=np.random.uniform(0.5,3.0)))pool.append(lambdax:motion_blur(x,angle=np.random.uniform(0,360),size=np.random.randint(3,15)))# 噪声:高斯、泊松、椒盐pool.append(lambdax:add_gaussian_noise(x,std=np.random.uniform(0,25)))pool.append(lambdax:add_poisson_noise(x,scale=np.random.uniform(0.1,1.0)))# JPEG压缩:模拟有损压缩pool.append(lambdax:jpeg_compress(x,quality=np.random.randint(30,95)))returnpool关键点在于随机组合。每次训练时,从池中随机选2-3个退化操作,按随机顺序应用。顺序很重要——先模糊再下采样和先下采样再模糊,结果完全不同。我一般先做模糊,再做下采样,最后加噪声和压缩,这样更接近真实成像管线的物理顺序。
别这样写:固定退化参数。比如sigma固定为1.5,那模型学到的只是特定模糊程度的逆映射,换个场景就失效。应该让sigma在合理范围内均匀采样,甚至可以用截断高斯分布来模拟更真实的退化分布。
还有一个容易被忽略的点:尺度因子随机化。不要只训2倍、3倍、4倍,可以训1.5倍、2.7倍这种非整数倍。做法很简单:下采样时用随机缩放因子,然后通过插值或裁剪对齐到固定尺寸。这能让模型学到尺度不变性,对实际应用中的任意缩放需求特别有用。
裁剪策略:别让模型只看到局部
数据裁剪看起来简单,但里面的门道不少。很多人直接随机裁剪96x96的patch,然后扔进模型训练。这样做的问题在于:模型只看到了局部纹理,学不到全局结构。
多尺度裁剪是我常用的策略。训练时同时裁剪不同尺度的patch:
# 这里踩过坑:别只用固定尺寸defmulti_scale_crop(hr,lr,scale,patch_sizes=[48,64,96,128]):# 随机选一个patch sizepatch_size=np.random.choice(patch_sizes)# 根据scale计算LR侧patch sizelr_patch_size=patch_size//scale# 随机裁剪位置h,w=hr.shape[:2]x=np.random.randint(0,h-patch_size+1)y=np.random.randint(0,w-patch_size+1)hr_patch=hr[x:x+patch_size,y:y+patch_size]lr_patch=lr[x//scale:x//scale+lr_patch_size,y//scale:y//scale+lr_patch_size]returnhr_patch,lr_patch小patch让模型关注高频细节,大patch让模型理解结构上下文。混合使用能让模型既会“画细节”又会“搭骨架”。
别这样写:固定裁剪位置。比如只在图像中心裁剪,或者只裁剪固定区域。这样模型会学到位置偏置,测试时遇到不同构图的图像就崩了。应该让裁剪位置完全随机,甚至可以加上一些边界padding来避免边缘效应。
还有一个进阶技巧:困难样本挖掘。训练过程中,记录每个patch的loss,loss高的patch说明模型当前处理不好,下次训练时多采样这些区域。实现起来很简单:维护一个loss队列,每次裁剪时以一定概率(比如30%)从高loss区域采样。这能加速收敛,尤其对纹理复杂区域效果明显。
数据预处理:细节决定成败
预处理环节的坑比想象中多。我见过最离谱的是有人直接用uint8数据训练,结果模型输出全是255。也见过有人归一化时用错了均值和标准差,导致训练不收敛。
归一化策略:超分任务和分类任务不同,不需要ImageNet的均值和标准差。我一般用两种方式:
- 简单归一化:直接除以255,把像素值映射到[0,1]区间。简单有效,适合大多数场景。
- 零均值归一化:减去数据集均值再除以标准差。这种方式能让模型更快收敛,但需要提前统计数据集统计量。
# 这里踩过坑:别直接用ImageNet的归一化参数defnormalize_sr(hr,lr):# 简单归一化hr=hr.astype(np.float32)/255.0lr=lr.astype(np.float32)/255.0# 或者零均值归一化(需要提前计算)# hr_mean, hr_std = compute_dataset_stats(hr_dataset)# hr = (hr - hr_mean) / (hr_std + 1e-8)returnhr,lr数据增强:除了退化模拟,常规的数据增强也不能少。但要注意,超分任务的增强和分类任务不同,不能随意改变图像内容。
- 几何增强:随机翻转(水平、垂直)、随机旋转(90度倍数)。这些操作不会改变图像内容,适合超分任务。
- 颜色增强:轻微调整亮度、对比度、饱和度。幅度要小,否则会引入伪影。
- 别这样写:随机裁剪后直接resize到固定尺寸。这会破坏LR-HR的对应关系,导致模型学到错误的映射。
边界处理:下采样时,LR和HR的边界对齐是个问题。如果HR尺寸不能被scale整除,LR会多出一些像素。我一般用两种方式:
- 中心裁剪:先下采样,然后从LR和HR的中心各裁剪一个对齐的区域。
- 反射填充:在边界处用反射填充,保证下采样后尺寸匹配。
实战经验:这些坑我替你踩过了
退化模拟的强度要适中。太强会让模型学不到有效信息,太弱又起不到泛化作用。我一般把退化参数控制在“人眼能看出退化但还能辨认内容”的程度。
数据预处理和模型训练要解耦。不要在训练循环里做预处理,应该提前把数据准备好,存成numpy数组或HDF5格式。这样能节省大量IO时间。
验证集要用真实退化。不要用和训练集相同的退化方式做验证,否则PSNR虚高。我一般准备一个真实场景的验证集,比如手机拍摄的照片或监控视频帧。
数据增强要在线做。每次训练时随机应用增强,而不是提前增强后存起来。这样能产生无限多的训练样本,避免过拟合。
记录退化参数。训练时把每次应用的退化参数记录下来,方便后期分析模型对不同退化的敏感度。我一般用wandb或tensorboard记录这些信息。
别迷信大patch。patch不是越大越好,大patch会降低batch size,影响训练效率。我一般用64x64或96x96的patch,配合多尺度策略效果更好。
数据预处理要可复现。设置随机种子,保证每次实验的预处理流程一致。这样调试时才能定位问题出在模型还是数据。
最后说一句:数据预处理花的时间,会在模型训练和测试时十倍百倍地回报你。别急着调模型,先把数据搞明白。