阅读笔记(一):Dual Residual Networks Leveraging the Potential of Paired Operations for Image Restoration

Dual Residual Networks文章中加性高斯噪声去除部分的阅读笔记

原文: Dual Residual Networks Leveraging the Potential of Paired Operations for Image Restoration

原文代码github

博主的阅读笔记:



文章概要

  • 作者在研究中发现成对操作在各种图像处理任务的有效性,如一个CNN迭代地执行一对上采样和下采样有助于提高图像超分辨率的性能,网络中反复执行一对大和小卷积核表现出良好的去噪效果。
  • 假设这种重复成对操作的有效性,作者提出了一种新颖的残差连接方式,称为“双残差连接(dual residual connection)”,因此设计了一个模块块:它具备两个容器,其中可以插入任意的成对操作。所提出的模块化块的堆栈允许块中的第一个操作与任何后续块中的第二个操作交互。
  • 作者用9个数据集,通过指定每个堆叠块中的两个操作,为每个单独的图像恢复任务构建一个完整的网络,在五个图像恢复任务(Gaussian noise removal,Motion blur removal,Haze removal,Raindrop detection and removal ,Rain-streak removal)中进行了实验评估。结果证明了该方法的优越性。
双残差连接(dual residual connection)

如图的连接方式中, f f f g g g为配对操作, f i f_i fi 操作可以和后面的 g j ( j ≤ i ) g_j (j\leq i) gj(ji) 进行配对,这种连接方式可以保证在每一条路径上操作都是成对出现的。两个操作的所有组合: ( f 1 , g 1 ) , ( f 2 , g 2 ) , ( f 3 , g 3 ) , ( f 1 , g 3 ) , ( f 2 , g 3 ) (f_1,g_1), (f_2,g_2),(f_3,g_3),(f_1,g_3),(f_2,g_3) (f1,g1),(f2,g2),(f3,g3),(f1,g3),(f2,g3)
dual residual connection

Fig1.双残差连接

双残差块 Dual Residual Block (DuRB)

把实现双残差连接的块称为双残差块(DuRB)。DuRB是通用的结构,有两个容器进行成对的操作,根据具体的任务可以自定义这两个容器。
residual connection

Fig2.具有双残差连接的单元块的结构

T 1 l , T 2 l T_1^l,T_2^l T1l,T2l : 分别表示两个成对操作的容器

c c c : 表示一个卷积层

数据集

使用BSD500-grayscale dataset,均为灰度图片,训练集200张图像,验证集100张图像,测试集100张图像。但作者将训练集和验证集的全部300张图片都用于训练



代码理解(加性高斯噪声消除)

构建模型

代码来自 pietorch 文件夹中的 DuRN_P.py(另进行中文注释)


模型:
class cleaner(nn.Module):
  #定义cleaner的初始化函数,这个函数定义了该神经网络的基本结构
    def __init__(self):
        super(cleaner, self).__init__()
        #复制并使用cleaner的父类的初始化方法,即先运行nn.Module的初始化函数

        # Initial convolutional layers
        self.conv1 = ConvLayer(1, 32, kernel_size=3, stride=1)
        # 定义conv1函数调用ConvLayer卷积函数:输入为图像(1个通道,即灰度图),输出为32张特征图, 卷积核为5x5
        self.norm1 = FeatNorm("batch_norm", 32) # 批处理归一化层
        self.conv2 = ConvLayer(32, 32, kernel_size=3, stride=1)
        # 定义conv2函数:输入为32张特征图,输出为32张特征图, 卷积核为3x3
        self.norm2 = FeatNorm("batch_norm", 32)

        # DuRBs
        # 定义6个残差块(DuRB-p x 6)
        self.block1 = DuRB_p(k1_size=5, k2_size=3, dilation=1)
        self.block2 = DuRB_p(k1_size=7, k2_size=5, dilation=1)
        self.block3 = DuRB_p(k1_size=7, k2_size=5, dilation=2)
        self.block4 = DuRB_p(k1_size=11, k2_size=7, dilation=2)
        self.block5 = DuRB_p(k1_size=11, k2_size=5, dilation=1)
        self.block6 = DuRB_p(k1_size=11, k2_size=7, dilation=3)

        # Last layers
        self.conv3 = ConvLayer(32, 32, kernel_size=3, stride=1)
        self.norm3 = FeatNorm("batch_norm", 32)
        self.conv4 = ConvLayer(32, 1, kernel_size=3, stride=1)

        self.relu = nn.ReLU() # 定义激活函数ReLU
        self.tanh = nn.Tanh() # 定义激活函数Tanh

    def forward(self, x):
        out = self.relu(self.norm1(self.conv1(x)))
        # 输入x经过卷积conv1后,经过批归一化函数norm1,再经过激活函数ReLU,然后更新到x
        out = self.relu(self.norm2(self.conv2(out)))
        res = out # 上一步的输出out作为残差res,out和res将作为下一步(第一个残差块即block1)的输入
        
        out, res = self.block1(out, res) # 输入(out, res)经过block1(即第一个残差块DuRB_p),然后更新到(out, res)
        out, res = self.block2(out, res)
        out, res = self.block3(out, res)
        out, res = self.block4(out, res)
        out, res = self.block5(out, res)
        out, res = self.block6(out, res)

        out = self.relu(self.norm3(self.conv3(out)))
        # 输入out依次经过卷积层conv3、归一化层norm3、激活函数relu,然后更新到out
        out = self.tanh(self.conv4(out))
        out = out + x
        # 整个网络的输出与最原始的输入相加,即残差连接,补全数据经过网络后丢失的信息

        return out

可画出模型结构图如下:
net

Fig3.网络结构图

norm1,2,3均为批归一化函数,使用 nn.BatchNorm2d() 方法。

批归一化(batch normalization)工作原理:训练过程中在内部保存已读取每批数据均值和方差的指数移动平均值;其主要效果是:有助于梯度传播,有效解决梯度消失问题,因此允许更深的网络。

残差连接:将前面的输出张量与后面的输出张量相加,从而将前面的表示重新注入下游的数据流中,这有助于防止信息处理流程中的信息损失

单元残差块(DuRB-P):
class DuRB_p(nn.Module):
    def __init__(self, in_dim=32, out_dim=32, res_dim=32, k1_size=3, k2_size=1, dilation=1, norm_type="batch_norm",
                 with_relu=True):
        super(DuRB_p, self).__init__()

        self.conv1 = ConvLayer(in_dim, in_dim, 3, 1)
        self.norm1 = FeatNorm(norm_type, in_dim)
        self.conv2 = ConvLayer(in_dim, in_dim, 3, 1)
        self.norm2 = FeatNorm(norm_type, in_dim)

        # T^{l}_{1}: (conv.+ bn)
        # 成对操作的第一个操作:上采样+归一化
        self.up_conv = ConvLayer(in_dim, res_dim, kernel_size=k1_size, stride=1, dilation=dilation)
        self.up_norm = FeatNorm(norm_type, res_dim)

        # T^{l}_{2}: (conv.+ bn)
        # 成对操作的第二个操作:下采样+归一化
        self.down_conv = ConvLayer(res_dim, out_dim, kernel_size=k2_size, stride=1)
        self.down_norm = FeatNorm(norm_type, out_dim)

        self.with_relu = with_relu
        self.relu = nn.ReLU()

    def forward(self, x, res):
        x_r = x # 提取残差信息,将作为后续的残差输入

        x = self.relu(self.norm1(self.conv1(x)))
        x = self.conv2(x)
        x += x_r
        x = self.relu(self.norm2(x))

        # T^{l}_{1}
        x = self.up_norm(self.up_conv(x)) # up_conv:上采样卷积层
        x += res # 残差连接:与上一个残差块得到的res相加
        x = self.relu(x)
        res = x # res作为下一个残差块res输入

        # T^{l}_{2}
        x = self.down_norm(self.down_conv(x)) # down_conv:下采样卷积层
        x += x_r # 残差连接:与起始输入到该残差块的输入x(即X_r)相加

        if self.with_relu:
            x = self.relu(x)
        else:
            pass

        return x, res

画出单元DuRB结构图如下:
DuRB_P

Fig4.DuRB_P结构图

其中:

c c c 为卷积层, T 1 l T_1^l T1l T 2 l T_2^l T2l 为两个成对操作:分别为上采样和下采样操作。

up_norm和down_norm为自定义的归一化函数,使用 N_modeles.pyInsNorm 函数。

特别的:

  1. 输入x经过第一个卷积层 c c c 依序有如下操作:conv1 -> norm1 -> relu,结果更新到x;

  2. x再经过第二个卷积层conv2,与x_r相加得到新的输出x,然后接norm2、relu,结果更新到x;

  3. T 1 l T_1^l T1l :x经up_conv -> up_norm,再与上一个残差块的res相加,最后经过relu,输出为x = res(res作为下一个残差块的res输入);

  4. T 2 l T_2^l T2l :x经down_conv -> down_norm,再与x_r相加,经过relu得到输出(得到的输出作为下一个残差块的out输入)。

    (每个卷积层后都接一个归一化norm操作和激活relu操作)

流程图:
在这里插入图片描述
疑惑:流程图中共进行了三个残差连接,为什么第一个残差是接在conv2后,后两个均接在归一化norm后?


卷积层:
class ConvLayer(nn.Module):
    def __init__(self, in_dim, out_dim, kernel_size, stride, dilation=1):
        super(ConvLayer, self).__init__()
        reflect_padding = int(dilation * (kernel_size - 1) / 2)
        self.reflection_pad = nn.ReflectionPad2d(reflect_padding)  # 利用输入边界的镜像来填充输入张量
        self.conv2d = nn.Conv2d(in_dim, out_dim, kernel_size, stride, dilation=dilation)

    def forward(self, x):
        out = self.reflection_pad(x)  # 先进行镜像填充
        out = self.conv2d(out)  # 再进行二维卷积
        return out

对输入图像镜像填充操作 reflection_pad() ,保证图像经过卷积层后,输出图像的尺寸仍与原来一样。大小卷积核与感受野的关系可查看论文最后的补充材料中Table 1。


模型训练

代码来自 train 文件夹中的 gaussian.py


参数信息
# ------ Options -------
tag = 'DuRN_P'
data_name = 'BSD_gray'
bch_size = 100 # batch=100
base_lr = 0.001 # 初始学习率:0.001
gpus = 1

epoch_size = 3000 # 训练3000次 
crop_size = 64  # 随机裁剪的图片尺寸大小   
Vars = [30, 50, 70] # 三种水平的高斯噪声

l2_loss_weight = 1 # L2损失的权重
locally_training_num = 10
加性高斯噪声:
def AddNoiseToTensor(patchs, Vars):  # Pixels must be in [0,1]
    bch, c, h, w = patchs.size()
    for b in range(bch):
        Var = random.choice(Vars)  # 从Vars = [30, 50, 70]中随机选择一个值

        noise_pad = torch.FloatTensor(c, h, w).normal_(0, Var)
        # 张量的数从正态分布(0,Var)中随机生成(产生高斯噪声)

        noise_pad = torch.div(noise_pad, 255.0)
        # 输入的patchs像素值在[0, 1]之间,因此这里的高斯噪声应除以255,将像素限制在[0,1]的范围内

        patchs[b] += noise_pad  # 把高斯噪声加入到原图像中
    return patchs
优化器和损失函数:
# Optimizer and Loss
optimizer = optim.Adam(cleaner.parameters(), lr=base_lr)
L2_loss = nn.MSELoss() # L2损失采用mse损失

MSELoss:均方误差损失(Mean square error)
M S E = 1 n ∑ i = 1 n ( Y i − Y i ^ ) 2 MSE = \frac{1}{n}\sum_{i=1}^n(Y_i - \hat{Y_i})^2 MSE=n1i=1n(YiYi^)2
对二维图像, I I I 为真实标签图像, P P P 为经过网络训练后得到的输出图像,其均方误差损失为:
M S E = 1 m n ∑ i = 0 m − 1 ∑ j = 0 n − 1 [ I ( i , j ) − P ( i , j ) ] 2 MSE = \frac{1}{mn}\sum_{i=0}^{m-1}\sum_{j=0}^{n-1}[I(i, j)-P(i,j)]^2 MSE=mn1i=0m1j=0n1[I(i,j)P(i,j)]2

开始训练:
# Start training
print('Start training...')
for epoch in range(epoch_size):
    for iteration, data in enumerate(dataloader): # 给数据datalaoder进行0,1,2,...编码赋值给iteration,数据给data
        img, label, _ = data  # "img" which is clean, will be added noise.
        # label_var = Variable(label[:, 0, :, :], requires_grad=False).cuda()
        label_var = Variable(label[:, 0, :, :], requires_grad=False)
        label_var = label_var.unsqueeze(1)

        for loc_tr in range(locally_training_num):
            noisy_patchs = AddNoiseToTensor(img.clone(), Vars)
            # noisy_patchs = Variable(noisy_patchs, requires_grad=False).cuda()
            noisy_patchs = Variable(noisy_patchs, requires_grad=False)
            noisy_patchs = noisy_patchs[:, 0, :, :].unsqueeze(1) 
            # 数据预处理时(见data_convertors.py),把图像转变为RGB图像,因此每张图像有三个通道,此时图像已变为三维张量
            # 这里"0"表示只取出第一个通道的图像,然后在轴序号为1的位置增加一个轴,
            # 变成四维张量,送入网络进行训练

            # Cleaning noisy images
            cleaned = cleaner(noisy_patchs)  # 加性高斯噪声图像放入网络中训练

            # Compute L2 loss
            l2_loss = L2_loss(cleaned, label_var)
            loss = l2_loss * l2_loss_weight

            # Backward and update params        
            optimizer.zero_grad()
            # 当网络参量进行反馈时,梯度是被积累的而不是被替换掉;
            # 但是在每一个batch时并不需要将两个batch的梯度混合起来累积
            # 因此每个batch开始训练之前都要清除梯度

            loss.backward()  # 反向传播求梯度
            optimizer.step()  # 更新参数

        print('Epoch(' + str(epoch + 1) + '), iteration(' + str(iteration + 1) + '): ' + str(loss.item())) 
        # 打印出当前训练次数和训练的图片数量

    if epoch % 10 == 9: # 每10个epoch保存一次模型
        if gpus == 1:
            torch.save(cleaner.state_dict(), dstroot + 'epoch_' + str(epoch + 1) + '_model.pt')
        else:
            torch.save(cleaner.module.state_dict(), dstroot + 'epoch_' + str(epoch + 1) + '_model.pt')

    if epoch in [500, 1000, 1500, 2500]:
        for param_group in optimizer.param_groups:
            param_group['lr'] *= 0.1 # 每训练500次,学习速率衰减为原来的0.1

注:路径设置和数据加载部分的代码未贴出

疑惑:代码第10行,为什么要设置locally_training_num循环10次?
个人理解:每次循环都随机选择从[30, 50 , 70]三种水平的高斯噪声加入到输入图片中,然后进行训练,相当于数据增强的效果(每张图片加入10次随机的高斯噪声,相当于把1张图片扩充到10张)?



实验结果:

results

实验显示所提出的网络在三种水平的高斯噪声上都超过了前人的方法,证明了该方法的有效性,残差块DuRB-p能有效地去除加性高斯噪声。


PSRN:峰值信噪比 (Peak Signal-to-Noise Ratio)

SSIM:结构相似性(Structural SIMilarity),是一种衡量两幅图像相似度的指标,用亮度、对比度、结构的不同组合来定义。对于两个样本,用均值作为亮度的估计,标准差作为对比度的估计,协方差作为结构相似程度的度量。

具体公式信息参见博客:图像质量评价指标之 PSNR 和 SSIM


–待解决的问题–

附件下载:将整个模型绘制成层组成的图

  • 2
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值