【AI】超分辨率经典论文复现(1)——2016年
好久不见, 最近搞了一会与超分辨率相关的机器学习的东西, 所以这次是这几天简单用Pytorch复现的超分辨率论文和一点笔记. 这里我复现的几篇文章顺序都是按照知乎帖子 [从SRCNN到EDSR,总结深度学习端到端超分辨率方法发展历程] https://zhuanlan.zhihu.com/p/31664818 整理而来(文末 点击原文 可以跳转), 由于最近研究的就是这方面的东西, 因此接下来还会继续复现一些新的超分辨率论文, 攒够一波就发出来. 图形学的文章背地里我也有在写, 但是还没准备好因此还没发出来, 见谅.
才疏学浅, 错漏在所难免, 如果我的复现中有对论文的理解问题希望大家在留言处指出. 这些复现的代码风格各自有些不同, 这是因为我本身也是借此机会在学习Pytorch, 因此故意用不同的写法编写, 见谅.
全文3.5k字, 篇幅不长, 不难的. 本文同步存于我的Github仓库( https://github.com/ZFhuang/Study-Notes/tree/main/Content/%E6%9C%BA%E5%99%A8%E5%AD%A6%E4%B9%A0/%E8%B6%85%E5%88%86%E8%BE%A8%E7%8E%87%E5%AE%9E%E8%B7%B5 )
SRCNN(2014) 最基础的卷积神经网络
Learning a Deep Convolutional Network for Image Super-Resolution
SRCNN网络结构
SRCNN
作为最早的超分辨率神经网络, 结构很简单, 就是三个卷积层, 两个激活层的组合, 效果自然也不敢恭维
SRCNN简单实现
class SRCNN(nn.Module):
def __init__(self):
super(SRCNN, self).__init__()
# 输出大小计算: O=(I-K+2P)/S+1
# 三层的大小都是不变的, 通道数在改变
# 原文没有使用padding因此图片会变小, 这里使用了padding
self.conv1=nn.Conv2d(1,64,9, padding=4)
self.conv2=nn.Conv2d(64,32,1, padding=0)
self.conv3=nn.Conv2d(32,1,5, padding=2)
def forward(self, img):
# 三层的学习率不同
# 两个激活层
img=torch.relu(self.conv1(img))
img=torch.relu(self.conv2(img))
# 注意最后一层不要激活
return self.conv3(img)
SRCNN一些经验
- 多通道超分辨率训练难度大且效果不佳, 因此通过将RGB图像转到YCrCb空间中, 然后只取其Y通道进行超分辨率计算, 完成计算后再配合简单插值处理的CrCb通道. 这种处理方法也被用在了后来的很多超分辨率网络中
- 尽管卷积网络不好训练, 原文使用了ImageNet这样庞大的数据集, 但事实上对于这样很浅的网络用T91就可以得到训练效果
- 用阶段改变学习率的动量SGD效果比Adam更好
- 小batch收敛起来更有效些
FSRCNN(2016) 更快的SRCNN
Accelerating the Super-Resolution Convolutional Neural Network
FSRCNN网络结构
FSRCNN
从上面论文中的对比图可以发现其与SRCNN最大的区别就是结尾使用的反卷积层, 反卷积让我们可以直接用没有插值的低分辨率图片进行超分辨率学习, 从而减少超分辨途中的参数数量, 加快网络效率. 并且使用了PReLU作为激活层, 使得激活层本身也可以被学习来提高网络效果
FSRCNN简单实现
class FSRCNN(nn.Module):
def __init__(self,d,s,m,ratio=2):
super(FSRCNN, self).__init__()
feature_extraction=nn.Conv2d(1,d,5, padding=2)
shrinking=nn.Conv2d(d,s,1)
seq=[]
for i in range(m):
seq.append(nn.Conv2d(s,s,3,padding=1))
non_linear=nn.Sequential(*seq)
expanding=nn.Conv2d(s,d,1,padding=0)
# 反卷积尺寸计算 O=(I-1)×s+k-2P
deconvolution=nn.ConvTranspose2d(d,1,9,stride=ratio,padding=4)
self.body=nn.Sequential(
feature_extraction,
nn.PReLU(),
shrinking,
nn.PReLU(),
non_linear,
nn.PReLU(),
expanding,
nn.PReLU(),
deconvolution
def forward(self, img):
return self.body(img)
FSRCNN一些经验
- 由于输入输出大小不一样(相差一个像素), 因此误差计算等需要注意写好
- 由于反卷积的计算问题, 实际输出的结果图会比HR图小一个像素, 因此在使用的时候需要将HR层手动裁剪一个像素来适配网络
ESPCN(2016) 实时进行的亚像素卷积
Real-Time Single Image and Video Super-Resolution Using an Efficient Sub-Pixel Convolutional Neural Network
ESPCN网络结构
ESPCN
核心的优化在于最后一层的亚像素卷积过程, 其思想就是将卷积得到的多通道低分辨率图的像素按照周期排列得到高分辨率的图片, 这样训练出能够共同作用来增强分辨率的多个滤波器. 借用 [一边Upsample一边Convolve:Efficient Sub-pixel-convolutional-layers详解] https://oldpan.me/archives/upsample-convolve-efficient-sub-pixel-convolutional-layers 的示意图可以更好理解亚像素卷积的过程.
亚像素卷积
ESPCN的简单实现
class ESPCN(nn.Module):
def __init__(self, ratio=2):
super(ESPCN, self).__init__()
self.add_module('n1 conv', nn.Conv2d(1,64,5,padding=2))
self.add_module('tanh 1',nn.Tanh())
self.add_module('n2 conv', nn.Conv2d(64,32,3,padding=1))
self.add_module('tanh 2',nn.Tanh())
self.add_module('n3 conv', nn.Conv2d(32,1*ratio*ratio,3,padding=1))
# 亚像素卷积层
self.add_module('pixel shuf',nn.PixelShuffle(ratio))
def forward(self, img):
for module in self._modules.values():
img = module(img)
return img
ESPCN一些经验
- ESPCN用小LR块训练效果更好
- 注意网络最后一层不要再放入激活层了, 会有反作用
- 这个网络训练很快效果也很不错, 思路也有很大参考价值
VDSR(2016) 深度残差神经网络
Accurate Image Super-Resolution Using Very Deep Convolutional Networks
VDSR网络结构
VDSR
使用大量3*3的 卷积-激活块 进行串联, 用padding保证输入输出的尺寸, 整体用一个残差来优化训练. 网络的目标是得到的残差尽可能接近HR-LR, 用MSE作为loss训练.
VDSR简单实现
class VDSR(nn.Module):
def __init__(self):
super(VDSR, self).__init__()
self.body=VDSR_Block()
def forward(self, img):
img=self.body(img)
for i in range(img.shape[0]):
# 由于Relu的存在, 得到的残差需要移动平均来应用
img[i,0,:,:]-=torch.mean(img[i,0,:,:])
# 由于网络只有一个残差块, 所以把残差的相加写到了loss计算中
return img
class VDSR_Block(nn.Module):
def __init__(self):
super(VDSR_Block, self).__init__()
self.inp=nn.Conv2d(1,64,3,bias=False,padding=1)
seq=[]
# 20层卷积
for j in range(20):
seq.append(nn.Conv2d(64,64,3,padding=1))
seq.append(nn.ReLU(True))
self.conv=nn.Sequential(*seq)
self.out=nn.Conv2d(64,1,3,padding=1)
def forward(self, img):
img=torch.relu(self.inp(img))
img=self.conv(img)
img=self.out(img)
return img
VDSR一些经验
- 很不好训练, 效果也不理想, 不知道是不是实现有问题, 也可能是只有一个残差块的缺点, 梯度很快消失
- 用阶段改变学习率的动量SGD来训练
- 上面的代码中残差损失是在我的训练函数中计算的, 因此看起来比较奇怪
DRCN(2016) 深度递归残差神经网络
Deeply-Recursive Convolutional Network for Image Super-Resolution
DRCN网络结构
DRCN
DRCN的亮点就在于中间的递归结构, 其使得每层都按照相同的参数进行了一次处理, 得到的残差通过跳接层相加得到一份结果, 然后所有级数的结果加权合在一起得到最终图像. 由于中间的递归结构每层使用的滤波都是相同的参数, 因此网络的训练难度低了很多, 训练比较高效而且效果也很不错.
DRCN简单实现
class DRCN(nn.Module):
def __init__(self, recur_time=16):
super(DRCN, self).__init__()
self.recur_time = recur_time
self.Embedding = nn.Sequential(
nn.Conv2d(1, 256, 3, padding=1),
nn.ReLU(True)
self.Inference = nn.Sequential(
nn.Conv2d(256, 256, 3, padding=1),
nn.ReLU(True)
self.Reconstruction = nn.Sequential(
nn.Conv2d(256, 1, 3, padding=1),
nn.ReLU(True)
self.WeightSum = nn.Conv2d(recur_time, 1, 1)
def forward(self, img):
skip = img
img = self.Embedding(img)
output = torch.empty(
(img.shape[0], self.recur_time, img.shape[2], img.shape[3]),device='cuda')
# 残差连接, 权值共享
for i in range(self.recur_time):
img = self.Inference(img)
output[:, i, :, :] = (skip+self.Reconstruction(img)).squeeze(1)
# 加权合并
output = self.WeightSum(output)
return output
DRCN一些经验
- 论文用到了自适应衰减的学习率和提前终止机制, 这能让训练效率大大提升, pytorch中对应的学习率调整器是: torch.optim.lr_scheduler.ReduceLROnPlateau
- 显存足够的话用大batch大学习率也能得到很好的效果, 还能加快训练
- 论文中提到了越深的递归效果越好, 实践中10层左右的递归就已经能有很好的结果了
- DRCN还用到了称作递归监督的组合损失, 一边计算每个递归层输出的损失一边评判最后的加权损失, 以求所有递归都能得到较好的训练, 目的是避免梯度爆炸/消失. 这是个值得一试的思想, 能让网络一开始更好收敛, 不过直接使用最后的加权误差来进行训练效果也不错.
RED(2016) 编码-解码残差
Image Restoration Using Convolutional Auto-encoders with Symmetric Skip Connections
RED网络结构
RED
可以看作FSRCNN和VDSR的结合体, 网络自身是卷积和反卷积组合成的对称结构, 每step层就进行一次残差连接, 通过这样反复的特征提取以期望得到质量更高的低分辨率图, 最后用一个反卷积恢复大小. 对称的特征提取组合有学习意义, 可惜最后的反卷积层过于粗暴使得效果不佳
RED简单实现
class RED(nn.Module):
def __init__(self,ratio=2, num_feature=32, num_con_decon_mod=5, filter_size=3, skip_step=2):
super(RED, self).__init__()
self.num_con_decon_mod = num_con_decon_mod
self.skip_step = skip_step
self.input_conv = nn.Sequential(
nn.Conv2d(1, num_feature, 3, padding=1),
nn.ReLU(True)
# 提取特征, 要保持大小不变
conv_seq = []
for i in range(0, num_con_decon_mod):
conv_seq.append(nn.Sequential(
nn.Conv2d(num_feature, num_feature,
filter_size, padding=filter_size//2),
nn.ReLU(True)
self.convs= nn.Sequential(*conv_seq)
# 反卷积返还特征, 要保持大小不变
deconv_seq = []
for i in range(0, num_con_decon_mod):
deconv_seq.append(nn.Sequential(
nn.ConvTranspose2d(num_feature, num_feature, filter_size,padding=filter_size//2),
nn.ReLU(True)
self.deconvs=nn.Sequential(*deconv_seq)
# 真正的放大步骤
self.output_conv = nn.ConvTranspose2d(num_feature, 1, 3,stride=ratio,padding=filter_size//2)
def forward(self, img):
img = self.input_conv(img)
skips = []
# 对称残差连接
for i in range(0, self.num_con_decon_mod):
if i%self.skip_step==0:
skips.append(img)
img = self.convs[i](img)
for i in range(0, self.num_con_decon_mod):
img = self.deconvs[i](img)
if i%self.skip_step==0:
img=img+skips.pop()