把 Fill2SR 的仓库拉到本地那天,我正对着一台只有 4 张 24G 显卡的机器发愁。很多人一听“真实世界超分”,下意识就觉得要堆算力,得上 8 卡甚至多机多卡,但这次开源的 Fill2SR 恰恰是冲着“用有限资源把真实照片修好”这个目标去的。它解决的是落地场景里最常见的痛点:输入图片不是实验室里那种合成降质图,而是手机拍摄、老照片扫描、网络压缩之后那种说不清道不明的模糊和伪影;同时它把训练和推理的门槛压到了 4 卡以内,让一般团队也能复现、微调甚至二次开发。这篇文章我会从设计思路、跑通流程、训练调参到问题排查,把我这段实操笔记完整分享出来,适合正在选型图像复原方案的工程师,也适合在 4 卡以内环境做深度学习研究的学生。
1. 为什么真实世界超分一直难落地
1.1 合成退化假设和现实差得太远
传统超分模型(不管是早期的 SRCNN、EDSR,还是后来各种基于 CNN 的变体)都有一个默认前提:低分辨率图像是高分图像经过已知的双三次降采样得到的。这个假设在标准的公开数据集上很好用,但拿到真实世界里立刻露馅。真实图片经历过的退化太杂了,镜头散焦、传感器噪声、暗光下的彩色条纹、压缩算法留下的块效应、老照片扫描时的网点纹理,这些退化叠在一起,根本不是一个简单的降采样核能描述的。
我拿一张用手机翻拍的旧海报做过测试,边缘有彩色条纹,文字发虚,还带着明显的 JPEG 块状感。普通超分模型处理这种图,最大的问题是会把伪影当成细节去放大,结果分辨率是上去了,观感却更脏了。Fill2SR 的思路不是硬猜退化公式,而是先把退化“读”出来,再决定哪些区域值得还原、哪些区域只能靠补全。这个“先估计再处理”的顺序,是它和传统方法最根本的分界点。
真实场景里还有一类很容易被忽视的问题:输入图像本身可能已经带上了不可逆的信息丢失,比如过度压缩后高频细节直接变成了花纹一样的噪声。这类信息丢失没法靠数学反演恢复,只能靠模型对“自然图像长什么样”的语义理解来补。这也就是为什么 Fill2SR 这类方案会把“语义填充”放在核心位置,而不是单纯加深网络去硬拟合。
1.2 4 张显卡背后的资源约束逻辑
超分模型要变强,直观想法是加大网络、加大批大小、堆更多的算力。但现实是,很多研究组和中小公司根本没条件常备 8 卡 A100,实验室里几张 3090 或者 4090 才是常态。Fill2SR 敢把“4 张显卡”写在项目标题里,背后其实做了几件非常务实的事情。
第一是混合精度训练。超分模型对精度很敏感,但训练阶段用自动混合精度(AMP)配合动态损失缩放,显存占用能直接降接近一半,而且基本不掉点。第二是网络结构上的取舍,默认配置不是一个动辄数十亿参数的巨型模型,而是轻量化生成体加细化模块的组合,单卡就能完成推理,24G 显存内基本无压力。第三是训练时的并行策略,官方默认支持数据并行加梯度累积,4 卡环境下也能模拟出较大的等效批大小,而不用担心 batch size 太小导致训练不稳。
我帮 A 同学在他的“某跨平台系统”上调过一次部署环境,四张 3090,跑默认配置的推理完全没有压力,训练时开 AMP 也稳得住。这种“向下兼容”的设计,其实比堆参数更见功力。能跑得动的模型才有被反复打磨的可能,否则论文发完就躺在仓库里吃灰,开源的意义也就少了一大半。
2. Fill2SR 的设计思路拆解
2.1 “填”的哲学:不硬编高频,而是补全结构
Fill2SR 这个名字,关键词是“Fill”,也就是填充。真实世界里丢失的细节是不确定的,模型如果直接学一个从低分到高分的映射,遇到未知退化时就容易“脑补”出错的方向,产生那种看似锐利、实际结构已经扭曲的细节。很多基于 GAN 的超分模型就是栽在这里,训练时越追求和真实高分图接近,推理时越容易出现纹理幻觉。
Fill2SR 的做法是引入一个“填充式”的双域机制:先由一个退化估计分支输出逐像素的信息完整度掩码,标出哪些区域信息还算完整、哪些区域已经彻底丢失;然后对信息丢失严重的区域,模型借助语义先验来“填空”,而不是强行锐化。这个过程很像修复老照片时修复师的操作习惯——纹理和结构线清晰的区域尽量按原样还原,断裂的部分才参考周围构图“补”过去。听起来简单,但把“哪里该补、哪里不该补”用可学习的方式建模,正是项目里最花功夫的部分。
实际看它的推理结果,最明显的特点就是“克制”。放大后的图片不会给你一种假得发光的锐利感,但放大到像素级看,结构是稳的,边缘没有明显的断裂或锯齿。对老照片、监控截图这类素材,这种“克制”反而是最难得的品质。
2.2 结构上怎么撑起“Fill”的机制
我顺着公开仓库的代码和论文描述梳理了一下,实现上大概是四条线索:
- 退化编码器:接收低分辨率图像,输出退化参数向量(类似模糊核、噪声等级、压缩强度的表征),同时生成一张逐像素的软掩码来表示信息完整度。
- 主干重建网络:采用轻量化的 Transformer 与卷积混合结构,在多个尺度上提取特征,负责把低分特征图逐步映射到高分空间。
- 填充模块:这是项目的核心创新点,利用信息完整度掩码和语义特征做交叉注意力,相当于告诉模型“哪里缺了,用周围和全局的上下文补上”。
- 细化网络:在最后用轻量的像素重排层把特征重新排列成 RGB 图像,减少上采样带来的棋盘伪影。
这里值得展开的是“软掩码”这个设计。早期一些复原方案用的是二值化掩码,判断某块区域要么保留、要么丢弃,结果在物体边缘很容易产生生硬的过渡带。Fill2SR 改成软性的、逐像素置信度之后,模型可以在保留和填充之间平滑过渡。我拿一张背景虚化的人像图测过,背景和头发交界处的处理明显更自然,没有那种一圈“修补感”。
与常见方案对比,这种设计在真实场景里优势明显。传统的“退化估计+复原”两阶段方案,退化估计一旦有误差,后面整个重建都会被带偏;Fill2SR 的软掩码相当于给第二阶段的填充留了纠错空间,就算退化估计不完美,也不至于让输出崩掉。
2.3 与主流方案的对比选型
我把自己踩过的几条技术路线放一起比了一下,方便大家做选型判断:
| 方案类型 | 优势 | 劣势 | 资源要求 |
|---|---|---|---|
| 传统迭代复原 | 数学可解释,适合退化已知场景 | 复杂真实退化鲁棒性差 | 低 |
| GAN 类超分(ESRGAN 系列为代表) | 感知质量好,细节锐利 | 容易出现纹理幻觉,训练不稳定 | 单卡可训 |
| 扩散类复原模型 | 效果上限高,细节可信 | 采样步数多,显存和耗时大 | 多卡才舒服 |
| Fill2SR 这类感知填充模型 | 真实场景更稳,结构保持好 | 极端细节处不如大模型激进 | 4 卡可训可推 |
当然,没有哪种方案是银弹。如果你只是做人脸修复,专业的人脸先验模型可能更好;如果是短视频实时处理,轻量级实时超分依然更合适。Fill2SR 真正舒服的区间是“离线处理一批来源不明的真实照片”,比如老照片数字化、监控截图取证、电商平台的历史商品图翻新,这类需求对处理时间不敏感,但对通用性和结构保真度要求很高。
3. 从开源仓库到跑通首个 Demo
3.1 环境准备与依赖坑位
官方建议的底层环境是 PyTorch 2.x 加 CUDA 11.8 以上。我实际用下来的感受是,不要在旧版本的 PyTorch 上硬跑,代码里用了一些较新的算子,老版本会直接报找不到模块。装完依赖后第一次编译扩展算子会花几分钟,这一步很正常,建议先把 ninja 装好,能明显加快编译速度。
我所在的“某实验室”机器配置是 Ubuntu 22.04,驱动支持 CUDA 12.1,PyTorch 用的 2.3 版本,整套流程比较顺利。真正花时间的反而是权重下载,真实世界超分模型的权重文件往往不小,建议提前确认磁盘空间和网络带宽。另一个容易踩的坑是并发下载文件时,部分下载工具对超大文件不够稳定,我后来改用带断点续传的方式才把完整权重拉下来。
第一次跑通前,别急着动代码,先跑一次官方提供的快速示例。如果输出目录里出现了正常的结果图,说明环境基本没问题,再进入自己的数据实验。
3.2 模型权重与数据准备
仓库会提供官方在混合真实数据上预训练好的权重。我的建议是先用官方权重跑通推理,别一上来就折腾训练。真实世界超分的质量评价很依赖输入数据,你拿官方在论文里展示的图去验证,观感自然好,但自己的图可能完全两回事。
准备测试数据时,我一般会建三个文件夹:
real_photos/:手机随手拍的照片,最好包含夜景、逆光、室内灯光等复杂场景。compressed/:从网上找高清图,人为降采样并加入不同程度的 JPEG 压缩,模拟传输和存储链路里的退化。scanned/:老照片、旧杂志扫描件,这类素材通常带着网点、划痕和偏色。
这三个来源能覆盖大部分真实场景的退化组合。我第一次评估时只用了合成数据,结果喜人,马上换真实照片就露馅了——文字区域发虚、天空部分有彩噪。后来养成了“合成数据看上限,真实数据看落地”的评估习惯,踩坑才少了很多。
3.3 推理脚本与参数解读
官方仓库的推理脚本用起来不算复杂,我把它整理成下面这种风格的命令:
python infer.py \ --ckpt ./weights/fill2sr.pth \ --input ./test/real_photos \ --output ./results \ --scale 4 \ --tile 256 \ --overlap 16 \ --use_amp这里几个参数值得细说。
--scale控制放大倍数。真实场景里大家最常用的是 2 倍到 4 倍,超过 4 倍后模型会明显进入“靠先验填空”的状态,空洞感会变强,不如分两次处理或配合检测做区域放大。--tile是切块大小,显存不够就调小到 128,配合--overlap重叠区一起使用。我建议重叠区至少 16 像素,太小容易在切块边缘看到拼缝,太大会增加额外计算量。--use_amp是混合精度开关,打开后推理速度提升很明显,而且普通图片基本不掉点,我一般默认开启。
实测一张 4K 分辨率的大图,从加载到输出大概几十秒到几分钟,取决于是否启用细化网络。这个速度没法跟轻量实时模型比,但真实世界超分本来就是离线批处理场景,等得起。
3.4 四卡训练的实操走查
如果要在 4 卡环境下微调,我按自己的经验整理了一套走查流程。
第一步,先验证数据加载器的吞吐。很多人一上来就开训,结果 GPU 利用率只有 30%,四张卡白瞎。用数据加载工具或者简单的计时脚本看每秒能喂进去多少样本,不够就增并行的加载进程。
第二步,用默认超参先跑一小段,比如 500 步,观察 loss 是否正常下降。如果 loss 纹丝不动,多半是学习率设置的问题,而不是模型问题。
第三步,设置训练参数。参考命令大致如下:
python train.py \ --data_root ./training_set \ --batch_size_per_gpu 2 \ --grad_accum 4 \ --lr 1e-4 \ --amp \ --num_workers 8 \ --save_interval 1000这样等效批大小是 2×4×4=32,对超分任务来说足够稳定。如果你发现验证集 LPIPS 波动大,可以再把梯度累积提高到 8,等效批大小达到 64,曲线通常会平滑不少,代价是单轮训练时间变长。
第四步,打开自动混合精度,同时启用梯度缩放器,这已经是训练超分模型的基本操作了。
第五步,每 1000 步存一次 checkpoint。真实数据里偶尔会出现坏样本,比如带严重色偏或者内容不良的图,会导致 loss 突然抖动。定期存档可以让你从容回滚到之前的状态,不至于白跑十几个小时。
微调时,把学习率调成原始预训练权重的 0.1 倍左右,大概 1e-5 量级,并且建议冻结退化编码器的前几层,只更新重建和填充部分。这样不容易破坏模型已经学好的退化估计能力,收敛也更稳。
4. 训练细节与调参经验
4.1 损失函数怎么配
Fill2SR 的训练目标大致由三部分构成:重建损失(L1)、感知损失(LPIPS)和一个结构一致性正则项。三者按不同权重组合,直接决定了结果是偏向“平滑干净”还是“细节丰富但偶尔失真”。
我之前吃过大亏。一开始把 L1 权重调得过高,结果输出太平滑,皮肤质感和纸张纹理全丢了,看起来像磨皮过度的照片。后来反过来把 LPIPS 权重拉高,细节是回来了,但局部区域开始出现不自然的纹理,天空部分甚至出现了像油画笔触一样的痕迹。
按照仓库给出的经验权重,L1 和 LPIPS 大致可以按 1:0.5 起步,再根据自己的数据做微调。我自己训练时的习惯是每周固定跑一遍真实图片验证集,用视觉反馈来权衡权重,而不是只盯训练曲线。超分这个任务,数值指标和人的主观感受经常不一致,PSNR 高了不代表好看,LPIPS 低了有时也只是因为输出变得更平滑。
4.2 学习率与批大小怎么联动
真实世界数据的噪声分布比合成数据复杂得多,训练曲面也更陡峭,学习率设置不对会带来很明显的体验差异。我用的基线学习率是 1e-4,在前 2000 步做线性热身,之后衰减到 5e-5 继续训练。
批大小和学习率是联动的。等效批大小如果超过 32,可以把学习率稍微调高一点,但我个人不建议超过 2e-4。批大小太小(小于 8)时,梯度噪声太大,loss 曲线抖动明显,这时要保证梯度累积至少到等效 16 以上,同时把学习率再降一半。
我记录每轮实验时,会同时记录 PSNR 和 LPIPS 两个指标,但做决策时以 LPIPS 为主。真实世界超分里,PSNR 这种像素级指标经常被噪声分布带偏:模型如果倾向于输出保守的平滑结果,PSNR 往往会比锐利的结果更高,但观感反而一般。LPIPS 更贴近人眼的感知,对结构保持更敏感。
4.3 验证集的穿插技巧
不要只在训练结束后做一次验证。超分模型在验证集上的表现有很强的偶发性,可能你这周测的三张图效果很好,换一批真实图就崩了。我建议按用途准备三个小验证集。
A 集是“合成退化集”,从高清图降采样并加上多种退化,用来观察模型的理论上限。B 集是“真实拍摄集”,拿手机在不同光线条件下随手拍的照片,不做任何清洗,用来评估落地效果。C 集是“难点切片集”,专门挑文字、人脸、夜景、天空等容易出问题的区域,用来观察模型短板。
每次验证都做完整推理,不要用快速模式。快速模式会简化切块和细化步骤,推理快很多,但对细小结构的处理能力也会打折。验证结果最好保存成固定序列的对比网格图,方便隔几天回看模型是否在退化。我经历过一次训练到第 3 天突然发现文字区域开始发虚的情况,幸好有 C 集验证及时拦住,否则一周的算力就白烧了。
5. 常见问题与排查技巧实录
5.1 推理结果出现彩色噪点
这是真实超分里最常见的问题。彩色噪点通常出现在天空、皮肤、暗部阴影这些低频区域,原因是退化估计分支把真实噪声误判成了高频细节,模型于是“好心”地帮助放大了噪声。
我遇到过一个典型场景:一张 ISO 3200 的夜景照片,天空区域在默认权重下出现了明显的粉红色噪点。解决办法是在推理前对输入图像做一次轻度的去噪预处理,再送入模型,输出后彩噪明显缓解,但真实纹理还被完整保留。如果处理流程不允许外接去噪模块,另一个办法是调低细化网络在低频区域的影响,这需要改配置文件里的对应权重,效果稍弱但胜在省事。
5.2 训练显存溢出
训练时的 OOM 九成来自三个方面:批大小太大、没开混合精度、把整张大图直接送进了网络。我见过有人把 4K 原图直接作为训练样本送进去,别说 4 卡,8 卡也顶不住。正确做法是随机裁剪成固定尺寸的训练块,比如 256×256 或 384×384,既能增广数据,又能保证显存占用可控。
如果已经做了这些还是 OOM,那就把batch_size_per_gpu降到 1,再把tile切小一点。超分训练本质上是在局部块上学映射关系,不需要一次性看全图。最后检查一下数据加载器,确认不是图太大导致 CPU 侧内存暴涨。
5.3 文字区域被修得发虚
真实超分对文字的容忍度很低,比人脸还低。人脸的细节就算修错方向,观感上可能只是一个“长得不一样的五官”,但文字笔画一旦断裂或连在一起,立刻穿帮。
Fill2SR 的设计上对文字区域做了结构保持的倾向,但我实测下来,如果输入图本身文字就过小过糊,输出依然会发虚。我的土办法是做一个锐化预处理,比如 unsharp mask,然后按 7:3 的比例把锐化结果和原始模型结果混合,视觉上清晰度提升明显,代价是极少数纹理边缘会偏硬。如果你要批量处理合同、票证这类文字密集的素材,建议在微调数据里专门加入一批文字渲染图,让模型自己学会如何补笔画。
5.4 常见问题速查表
| 现象 | 可能原因 | 处理方式 |
|---|---|---|
| 训练时显存溢出 | 批大小过大、未开 AMP、输入图太大 | 降 batch、开混合精度、切成小训练块 |
| 输出有彩色伪影 | 噪声被误判为细节 | 轻度去噪预处理或降低细化权重 |
| 文字区域发虚 | 结构先验不足,原图文字过小 | 数据里加入文字样本,或混合锐化结果 |
| loss 抖动剧烈 | 坏样本、学习率过高 | 清洗训练数据,降低学习率并做热身 |
| 切块边缘能看见拼缝 | overlap 太小 | 把--overlap提到 16 或 32 像素 |
| 推理速度比预期慢 | 芯片不支持加速算子 | 关掉细化网络或用--use_amp提速 |
跑完整个 Fill2SR 流程,我最大的体会是:真实世界超分要的不是“更锋利的锐化”,而是“知道什么时候该停”。这个项目用 4 卡把门槛拉下来之后,最大的收益是你可以把自己日常积累的碎片化真实图片当成训练样本,持续打磨自己业务里的盲区。如果你手头有老照片、监控截图、手机旧图这类素材,非常建议把它当成一个实战项目完整跑一遍,这个过程中的收获会比单纯刷榜单高很多。
还有一个后续扩展的思路:把这个模型接进“检测—修复—超分”的完整流程里,先用检测器框出主体(人脸、车牌、票据区域),再只对主体区域做更高倍的填充超分。这样既省显存,效果也更聚焦。踩过几次坑之后你会发现,这类工具的真正价值不在于那篇论文,而在于它让你的图片处理流程多了一个“敢用”的选项。