ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

VDSR超分辨率复现指南:残差学习与PyTorch实现全解析

VDSR超分辨率复现指南:残差学习与PyTorch实现全解析 简介面向需要复现VDSR超分辨率重建论文的PyTorch学习者这份资源把Matlab版数据制作流程改写为Python实现统一了编程语言并覆盖旋转、翻转、缩放等数据增强环节训练参数与论文保持一致而非照搬GitHub参考代码。压缩包共64个文件、约86.62MB核心是9个Python脚本依次负责数据增强、HDF5数据集制作、VDSR网络搭建、训练和测试另有3个预训练权重、多张BMP/PNG测试图、MAT/H5数据文件以及XML项目配置文件权重同时覆盖PyTorch版本与Matlab对照版本便于复现论文比较。运行后可直接得到PSNR评估结果和可视化重建图像并与Bicubic双三次插值结果对比便于验证VDSR效果、理解超分原理加载权重后无需训练即可完成测试。配套文章给出非常详细的分步说明从制作数据集到调用权重都能按序操作已有668人学习下载。1. 论文复现最怕的不是看不懂公式而是照着论文轮子重造后跑不出数字论文复现最怕的不是看不懂公式而是照着论文轮子重造之后得到的结果跟作者晒出来的曲线差一大截。Pytorch实现VDSR这件事在超分辨率方向是个很典型的试金石模型结构本身用二十行代码就能写完真正决定PSNR高低的反而是数据处理和那些写在实验设置角落里的小trick。VDSR是Kim等人在2016年提出的深度超分网络核心就一句话让网络学习低分辨率图与真实高分辨率图之间的残差。它把超分任务当成同尺寸回归问题用20层卷积堆出一个足够大的感受野。这个方向到今天看仍然有复现价值后来的EDSR、RCAN、SRGAN做对比实验时基本都会带上VDSR这条基线。对从业者来说VDSR不需要昂贵的多卡环境一张消费级显卡就能把整个训练流程跑通而且模型参数量不到70万部署成本很低。适合三类人准备做毕设和论文复现的学生、想在业务里做老照片修复或监控图像放大的工程师、以及刚进超分领域想找一个干净基线的人。下面按“网络结构 → 数据处理 → 训练配置 → 避坑清单 → 验证进阶”的顺序展开所有代码都是Pytorch可直接换数据跑的版本。2. 先看网络长什么样20层卷积堆出来的“残差机”2.1 为什么学习残差而不是直接学习原图超分网络有两种监督方式直接让网络输出高清图或者让网络输出低分图和高清图之间的差值。设 \(x\) 是bicubic插值放大后的低分辨率图\(y\) 是对应的真实高清图VDSR学的是 \(F(x) \approx y - x\)推理时再做一次 \(y x F(x)\)。直接学 \(y\) 的问题在于像素值跨度大网络必须在每一层都维持完整的图像表示特征图里全是“大数”回传的梯度也一直在跟大数值做斗争。残差图则不同平坦区域接近0只有边缘和纹理位置有较大的非零值视觉上非常稀疏。网络只需把表达能力集中在边缘和纹理附近从“大数到大数”的映射变成“大数到小数”的映射收敛速度和稳定性都会改善。推理时多一次逐元素相加几乎不增加耗时。这个设计后来成了超分网络的通用写法你会在很多后续模型里看到“global residual learning”这个词源头就在这里。2.2 3x3卷积堆20层感受野到底有多大VDSR全部使用 \(3\times3\) 卷积、stride为1、padding为1特征图尺寸全程不变输入输出都是单通道。感受野的计算公式是 \(R (k-1) \times L 1\)其中 \(k\) 是卷积核尺寸\(L\) 是卷积层数。代入 \(k3, L20\)感受野正好是 \(41\times41\)。超分本质上是一个邻域回归问题中心像素的新值要靠周围像素共同推断。3层5层的浅网络感受野只有不到10个像素纹理密集区域会被抹成一片而堆到40层以上训练难度陡增对小数据集极不友好。20层41像素的感受野是一个折中点这也是作者做实验试出来的稳定区间。顺便算一笔账每层64个通道第一层卷积参数是 \(1\times64\times3\times3576\)中间18层的参数是 \(64\times64\times3\times336864\)最后一层是 \(64\times1\times3\times3576\)加上bias总参数量约66万。这个体量对显存和CPU推理都非常友好也是我一直推荐用它做基线的理由。2.3 用Pytorch定义VDSR完整模型代码与初始化方式模型结构用Pytorch写出来非常直白我这里把每一层的职责写清楚import torch import torch.nn as nn class VDSR(nn.Module): def __init__(self, n_channels1, n_layers20, n_features64): super(VDSR, self).__init__() layers [] # 第一层单通道 - 64通道 layers.append(nn.Conv2d(n_channels, n_features, kernel_size3, padding1)) layers.append(nn.ReLU(inplaceTrue)) # 中间层64通道 - 64通道共 n_layers - 2 层 for _ in range(n_layers - 2): layers.append(nn.Conv2d(n_features, n_features, kernel_size3, padding1)) layers.append(nn.ReLU(inplaceTrue)) # 最后一层64通道 - 单通道不加ReLU输出是残差 layers.append(nn.Conv2d(n_features, n_channels, kernel_size3, padding1)) self.net nn.Sequential(*layers) def forward(self, x): # 输入是bicubic放大后的单通道图像输出是同尺寸残差 return self.net(x)代码里有两个关键点。第一最后一层卷积后面不能接ReLU因为残差值有正有负边缘处可能是负值接上ReLU等于强行截断了高频信息输出图会出现奇怪的亮斑。第二中间层用inplaceTrue省一点显存训练大patch或大batch时能感受到差别。n_layers和n_features都留成了可调参数复现论文固定用20和64做消融实验时可以直接改这两个值。初始化方式在原论文中用的是标准差0.01的高斯分布而Pytorch默认的卷积初始化是kaiming均匀分布。两种都能收敛但如果你发现训练初期loss波动大可以显式切成小标准差初始化def weights_init(m): if isinstance(m, nn.Conv2d): nn.init.normal_(m.weight, std0.01) nn.init.constant_(m.bias, 0) model VDSR() model.apply(weights_init)nn.init.normal_配合std0.01是为了让网络在一开始输出接近0的残差这样初始预测就是“什么都不改”训练以一个相对合理的起点开始优化。不要忽略这一步它对小数据集训练的影响比想象中更大。3. 数据和预处理超分复现里决定成败的第一关3.1 训练样本是怎么生成的bicubic下采样再bicubic放大VDSR的输入不是原始小图而是已经放大到目标尺寸的模糊图。训练样本的生成方式是取一张高清图先用bicubic下采样到 \(1/scale\)再用bicubic放大回原尺寸得到一张“糊掉但尺寸一致”的低分辨率图。代码很短from PIL import Image def make_lr_with_bicubic(hr_path, scale): # 读入高清图转RGB hr Image.open(hr_path).convert(RGB) w, h hr.size # 先缩到 1/scale再放大回原尺寸 lr hr.resize((w // scale, h // scale), Image.BICUBIC) lr_up lr.resize((w, h), Image.BICUBIC) return hr, lr_up这里有两个容易被带偏的地方。第一w // scale可能因为除不尽造成最终lr_up的尺寸和hr不一致最好在进入流程前把所有训练图裁成能被最大scale整除的尺寸。第二下采样必须用bicubic而不是nearest或area因为模型推理时面对的输入是插值放大的结果训练数据里必须包含这种插值伪影否则会出现“训练时低分辨率图很干净、测试时一塌糊涂”的分布偏移。这一步是整个复现的基石比网络结构的微小改动影响大得多。3.2 YCbCr通道处理与PSNR口径VDSR只在亮度通道上做超分这是它的一个重要前提。人眼对亮度变化最敏感色度通道的低频特性让bicubic放大已经够用把三通道全喂给网络参数翻三倍指标还容易因为色度误差拖后腿。论文的评估口径同样在Y通道上算PSNR这就是后面复现数字对不上的主要来源之一。RGB转Y通道用BT.601系数就够不需要调库import numpy as np def rgb2ycbcr(img_rgb): # 输入是 HxWx3 的RGB图像值域可以是uint8或float r img_rgb[..., 0] g img_rgb[..., 1] b img_rgb[..., 2] y 0.299 * r 0.587 * g 0.114 * b cb 128 - 0.168736 * r - 0.331264 * g 0.5 * b cr 128 0.5 * r - 0.418688 * g - 0.081312 * b return y, cb, cr训练时只保留y通道cb和cr在测试阶段直接用bicubic放大不需要深度学习参与。这里有一个数值口径问题如果训练时把像素范围从0到255归一化到0到1那么残差目标也要同步归一化不能一个0到255一个0到1否则loss量级会出现问题。很多复现跑出的PSNR低一半的原因都在这个归一化不一致上。3.3 随机裁剪、翻转和多尺度增广数据集不够大怎么办VDSR原论文使用的训练数据是291张图这个量级放到今天并不算大但靠着增广策略撑住了训练。核心增广有三个随机裁剪、随机翻转旋转、多尺度。先看随机裁剪patch大小取41是因为它和网络的感受野完全一致这样每个输出像素都拥有完整的上下文信息。裁剪代码如下import random def get_patch(hr_y, lr_up_y, patch_size41): # hr_y 和 lr_up_y 已经是同尺寸的单通道Y图 h, w hr_y.shape x random.randint(0, w - patch_size) y random.randint(0, h - patch_size) hr_patch hr_y[y:y patch_size, x:x patch_size] lr_patch lr_up_y[y:y patch_size, x:x patch_size] return hr_patch, lr_patch随机翻转和旋转90度不用写额外代码Pytorch里用torchvision.transforms的RandomHorizontalFlip和RandomRotation调度即可也可以用numpy手动翻转每张图翻出4到8个版本。多尺度增广是整个数据管线里最妙的部分每个epoch随机给每张训练图分配一个缩放倍数2倍、3倍、4倍轮流来模型被迫学会跨尺度的超分能力。测试时不需要为每个scale单独训练模型一个权重走天下。实现时直接在__getitem__里随机选scale动态生成对应的LR图既能吃到全部尺度的数据又不需要预先存三份文件。4. 训练设置从SGD到稳定收敛的完整配置4.1 损失函数和评估指标的关系VDSR的训练目标是最小化预测残差和真实残差之间的MSE。MSE在数学上等价于最大化PSNR因为PSNR公式里的分子是像素值峰值平方分母就是MSE。如果目标是为了论文对比MSE是唯一需要用的损失如果目标是主观画质可以考虑换成L1损失它的梯度在误差接近0时不会剧烈变化画面锐度稍好一些。这里先按论文原版用MSE。criterion nn.MSELoss()训练时把残差作为监督目标前向传播让网络输出残差不把输入图加回去。这样网络的学习目标更稀疏收敛路径更短。推理阶段再做加法。4.2 学习率、动量、权重衰减与梯度裁剪的推荐取值VDSR原论文的优化器配置在超分领域属于比较激进的SGD加动量学习率可以给到0.1。但这是配合梯度裁剪才能成立的组合直接裸跑0.1的学习率很容易爆炸。我一般把训练配置拆成两套跑通用保守参数调优再接近论文参数。optimizer torch.optim.SGD( model.parameters(), lr1e-2, # 跑通阶段用1e-2想对齐论文可提到1e-1 momentum0.9, weight_decay1e-4 ) scheduler torch.optim.lr_scheduler.MultiStepLR( optimizer, milestones[20, 35], # 40个epoch的方案 gamma0.1 )momentum0.9是通用配置weight_decay1e-4能防止权重范数过大。学习率这块如果你第一次跑先用1e-2loss下降平稳后再尝试把初始学习率提到1e-1同时必须打开梯度裁剪。梯度裁剪是VDSR训练稳定最关键的一行代码# 每个step backward之后、optimizer.step之前调用 torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm0.1)clip_grad_norm_计算所有参数梯度的全局L2范数超过max_norm就整体等比例缩放。VDSR深达20层反向传播时靠前卷积层的梯度容易出现异常大值裁剪后训练曲线会平滑很多。阈值给0.1偏保守给1也行看你训练时的梯度范数表现。batch size建议32到64patch是41x41单通道输入显存占用很小消费级显卡完全可以吃住。4.3 训练主循环与Dataset读出完整训练需要把前文的预处理串成一个Pytorch Dataset我在运行时把HR的Y通道提前转存为numpy数组避免每个epoch都做PIL转RGB再转Y的重复计算。核心训练循环如下import torch.nn as nn import torch def train_one_epoch(model, loader, optimizer, criterion, device, clip0.1): model.train() total_loss 0.0 total_num 0 for hr_y, lr_up_y in loader: hr_y hr_y.to(device) # (B, 1, 41, 41) lr_up_y lr_up_y.to(device) # (B, 1, 41, 41) # 真实残差是 HR 减去插值放大图也要做归一化 target_residual hr_y - lr_up_y optimizer.zero_grad() residual model(lr_up_y) loss criterion(residual, target_residual) loss.backward() # VDSR训练稳定的关键全局梯度裁剪 nn.utils.clip_grad_norm_(model.parameters(), max_normclip) optimizer.step() total_loss loss.item() * hr_y.size(0) total_num hr_y.size(0) return total_loss / total_num代码中的target_residual hr_y - lr_up_y是每个batch动态计算的不需要提前存储标签文件。注意hr_y和lr_up_y在送入网络前都要除以255归一化这样target_residual的数值范围也在0到1之间MSE的量级才合理。这块训练时间用一张中端显卡跑40个epoch量级在半天左右。5. 避坑清单复现VDSR最容易翻车的5个位置5.1 训练loss变成NaN或突然炸高现象前几个iteration loss正常某个step之后直接变成NaN或者loss曲线突然跳高一个数量级。原因20层卷积叠加加上初始学习率偏高浅层卷积的梯度积累了过大范数一步更新就把权重推到了数值不稳定区域。解决检查是否有梯度裁剪确认clip_grad_norm_放在了loss.backward()之后、optimizer.step()之前。如果裁剪已经生效仍闪烁把学习率降到1e-3跑几个epoch确认能收敛再逐步提升。另外看一下学习率调度器是否从一开始就生效milestones不要设成0。5.2 验证集PSNR跟论文对不上先查评估口径现象在Set5上跑出来的PSNR只有26dB左右论文里是32dB以上。原因很多人直接在RGB整图上算PSNR而VDSR论文的PSNR是在Y通道上计算的。RGB色彩空间会把色度通道的微小误差也计入MSE而色度通道的插值误差天然比亮度通道大指标一下子就被拉低好几分。另一个原因是训练数据规模差异论文用291张图你用DIV2K或十几张图结果差0.5dB以内都算合理。解决评估前先把RGB转Y通道只拿Y算PSNR。转换系数的微小差异也会影响零点几个dBBT.601和BT.709的结果略有不同跟论文对比时保持统一即可。5.3 输出图整体发灰或偏色现象模型跑完输出的图像比输入的低分图还灰或者彩色边缘有一圈异常色带。原因最常见的是归一化不一致。输入除以255但预测残差没有乘以255就直接跟像素值域在0到255的图相加导致输出被压缩到很暗的范围。偏色则是直接把RGB三通道都喂给模型训练而测试时又只对Y通道做深度学习处理颜色通道前后处理流程不一致。解决推理时统一按“输入归一化到0到1输出反归一化乘255”的流程走彩色图只把Y通道交给网络CbCr用bicubic放大。输出前做一次np.clip(out, 0, 255)防止残差累加导致越界。5.4 多尺度训练时loss曲线周期性振荡现象loss曲线整体下降但每隔一段时间会出现一次明显跳高看起来像模型在“抽风”。原因不同倍数的超分难度不均衡4倍任务的残差能量远大于2倍任务。如果同一个batch里混入不同scale的样本大scale样本的loss会主导梯度模型在两种难度之间来回拉扯。解决按scale分组训练一个batch内只放同一种scale的样本每个scale轮流送入模型或者不分组但分开记录每个scale的loss方便确认每个尺度都在收敛。不要因为混合batch的loss波动就断定训练失败先看每个scale单独的趋势。5.5 训练过程慢到怀疑人生现象epoch时间极长显卡利用率却不高数据加载成了瓶颈。原因在__getitem__里每取一个样本都重新做PIL读取、RGB转Y、bicubic下采样再放大重复计算量太大。尤其是bicubic放大是最耗时的操作之一。解决训练开始前把所有HR图像离线转换为Y通道的numpy数组并缓存到内存bicubic的LR图也预先计算好__getitem__里只做裁剪和翻转。DataLoader的num_workers调到4以上让数据加载和GPU计算重叠。这个优化通常能让epoch时间缩短一半以上。6. 先验证再谈优化把VDSR跑成一个可复用的基线模型模型训练完之后第一件事不是在测试集上“看图爽”而是写一段和论文口径一致的评估脚本把PSNR数值固化下来。我的评估代码通常长这样def compute_psnr_y(model, lr_path, hr_path, scale): hr Image.open(hr_path).convert(RGB) lr hr.resize((hr.width // scale, hr.height // scale), Image.BICUBIC) lr_up lr.resize((hr.width, hr.height), Image.BICUBIC) hr_y, hr_cb, hr_cr rgb2ycbcr(np.array(hr).astype(np.float32)) lr_up_y, lr_up_cb, lr_up_cr rgb2ycbcr(np.array(lr_up).astype(np.float32)) lr_tensor torch.from_numpy(lr_up_y / 255.0).float().view(1, 1, hr.height, hr.width) with torch.no_grad(): residual model(lr_tensor.to(device)) sr_y (lr_tensor residual).clamp(0, 1).cpu().numpy()[0, 0] * 255.0 mse np.mean((sr_y - hr_y) ** 2) psnr 10 * np.log10(255.0 ** 2 / mse) return psnr这段代码直接把Y通道的PSNR算到数值方便跟论文表格做对齐。验证数据集推荐用Set5和Set14图少、结果可比性强网上可以找到预处理好的标准版本。训练跑通之后我一般会把VDSR当成一柄“手术刀”去做三类进阶验证第一把MSE换成L1损失对比主观锐度的变化第二把末端加上亚像素卷积层让网络直接接收小图、输出放大图就能向FSRCNN这类轻量化模型靠拢第三在业务数据上做小规模微调因为VDSR的66万参数体量很适合作为老照片修复任务的热启动模型。我自己复现论文的习惯是先写评估脚本再写训练代码把“能跑”和“跑得对”分开验证这样每次改完网络结构都能在十分钟内知道有没有变差。这条路走通一遍之后再回头去看EDSR那类论文你会发现自己已经能读懂它们到底在哪个环节做了升级。希望帮到你。本文还有配套的精品资源点击获取
返回列表