Dual Residual Networks文章中加性高斯噪声去除部分的阅读笔记
原文: Dual Residual Networks Leveraging the Potential of Paired Operations for Image Restoration
原文代码github
博主的阅读笔记:
- motion blur removal (去运动模糊任务阅读笔记)
- haze removal (去雾任务阅读笔记)
- raindrop removal (去雨滴任务阅读笔记)
- rain-streak removal (去雨线任务阅读笔记)
- 笔记总结(五种任务的对比)
文章目录
- 文章概要
- 双残差连接(dual residual connection)
- 双残差块 Dual Residual Block (DuRB)
- 数据集
- 代码理解(加性高斯噪声消除)
- 构建模型
- 模型:
- 单元残差块(DuRB-P):
- 卷积层:
- 模型训练
- 参数信息
- 加性高斯噪声:
- 优化器和损失函数:
- 开始训练:
- 实验结果:
文章概要
- 作者在研究中发现成对操作在各种图像处理任务的有效性,如一个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(j≤i) 进行配对,这种连接方式可以保证在每一条路径上操作都是成对出现的。两个操作的所有组合:
(
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 Block (DuRB)
把实现双残差连接的块称为双残差块(DuRB)。DuRB是通用的结构,有两个容器进行成对的操作,根据具体的任务可以自定义这两个容器。
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
可画出模型结构图如下:
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结构图如下:
其中:
c c c 为卷积层, T 1 l T_1^l T1l 和 T 2 l T_2^l T2l 为两个成对操作:分别为上采样和下采样操作。
up_norm和down_norm为自定义的归一化函数,使用 N_modeles.py
中 InsNorm
函数。
特别的:
-
输入x经过第一个卷积层 c c c 依序有如下操作:conv1 -> norm1 -> relu,结果更新到x;
-
x再经过第二个卷积层conv2,与x_r相加得到新的输出x,然后接norm2、relu,结果更新到x;
-
T 1 l T_1^l T1l :x经up_conv -> up_norm,再与上一个残差块的res相加,最后经过relu,输出为x = res(res作为下一个残差块的res输入);
-
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=1∑n(Yi−Yi^)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=0∑m−1j=0∑n−1[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张)?
实验结果:
实验显示所提出的网络在三种水平的高斯噪声上都超过了前人的方法,证明了该方法的有效性,残差块DuRB-p能有效地去除加性高斯噪声。
PSRN:峰值信噪比 (Peak Signal-to-Noise Ratio)
SSIM:结构相似性(Structural SIMilarity),是一种衡量两幅图像相似度的指标,用亮度、对比度、结构的不同组合来定义。对于两个样本,用均值作为亮度的估计,标准差作为对比度的估计,协方差作为结构相似程度的度量。
具体公式信息参见博客:图像质量评价指标之 PSNR 和 SSIM
–待解决的问题–
- 有些代码的细节仍未看懂。
- 后续的另外四种恢复图像任务。
- 对于五种不同任务,作者分别设计的残差块所依据的机理。
- 疑惑1:残差连接的位置问题:。
- 疑惑2:设置locally_training_num的原因。
附件下载:将整个模型绘制成层组成的图
更多推荐
阅读笔记(一):Dual Residual Networks Leveraging the Potential of Paired Operations for
发布评论