目录
引言
卷积残差的原理及作用
卷积残差代码实现
卷积残差处理方式
代码处理
张量输入代码
图像输入代码
需要注意的点
引言
YOLO系列的作者Joseph Redmon仅提出了三个版本的YOLO(You Only Look Once),速度快是其主要优点,在保持快速识别的同时具备相对较好的准确率,全局信息获取具有高鲁棒性。其中YOLOv3的改进,是在YOLOv1、v2基础上引入了残差连接(Residual Connections)作为其主干网络即backbone Darknet-53(53层的深度卷积神经网络,包含多个残差模块)。darknet官方网站中,可以下载所需要的darknet。darknet-53结构图如下:
其中,残差连接概念是何凯明等人在ResNet中提出的一种网络结构,旨在解决深度神经网络训练过程中的梯度消失和梯度爆炸等问题。这里Joseph Redmon将其优点糅合进了YOLO中,这些模块在不同的网络层级上提取图像特征,并且通过残差连接将前一层的特征直接添加到后一层的特征中。这样的设计使得YOLOv3模型更加深层和复杂,能够更好地捕获图像中的语义信息,并提高目标检测的准确性和鲁棒性。
卷积残差的原理及作用
卷积残差的原理基于残差学习的思想,主要包括两个关键点:残差块和跳跃连接。(这个具体在学习ResNet时候再仔细研究。)
- 残差块(Residual Block):残差块由两个或多个卷积层组成,其中第一个卷积层的输出通过激活函数后作为输入传递给下一个卷积层。残差块的关键在于引入了一个跳跃连接,即将输入直接加到残差块的输出上,形成残差(或者说残差映射)。残差块的设计使得网络能够学习残差映射,而不是直接学习原始映射。这种设计有助于解决深层神经网络中的梯度消失和梯度爆炸问题,加速模型的训练过程。
- 跳跃连接(Skip Connection):跳跃连接是指将输入直接与残差块的输出相加。这种连接方式允许信息在网络中直接传递,有助于保留输入特征的细节信息,同时减少了信息的丢失。通过跳跃连接,残差块可以更有效地学习到输入与输出之间的差异,从而提高了网络的性能和泛化能力。
如图所示,是一个正常块和残差块(Residual Block)示意图。
假设在神经网络中,原始输入为x,而期望学习的理想映射为f(x),并且f(x)作为上方激活函数的输入。左侧正常块虚线框中部分需要直接拟合出该映射f(x),而右边残差块则需要拟合出残差映射f(x)-x。在残差块中如果希望得到恒等理想映射f(x),需要将右图虚线框内上方加权运算(如仿射)的权重和偏置参数设成0,那么f(x)即为恒等映射。实际中,当理想映射f(x)极接近于恒等映射时,残差映射也易于捕捉恒等映射的细微波动。
卷积残差块是在残差块的基础上进行了进一步的改进,通常采用了额外的卷积操作来增加网络的非线性表达能力。与普通的残差块相比,卷积残差块可能包含更多的卷积层和非线性激活函数,以增加特征提取的复杂度和多样性,其跳跃连接通常也会被称为短路连接或者残差连接,用于将输入直接添加到卷积层的输出上。
在YOLOv3的backbone主干网络中引入卷积残差设计,主要作用表现如下:
- 替身特征表征能力:卷积残差可以更好的捕捉输入输出之间的残差信息,有助于网络学习更深、更加丰富和抽象的特征表示。
- 加速网络训练:残差结构允许网络更快地收敛,有助于缓解梯度消失和梯度爆炸等问题,加速网络的训练过程。
- 增强网络的泛化能力:通过跳跃连接和残差学习,Darknet-53 可以更好地保留输入特征的细节信息,并且有助于网络学习到更加通用的特征表示,提高了网络的泛化能力和适应性。
- 减少参数数量: 虽然 Darknet-53 是一个较深的网络结构,但由于采用了残差连接,可以减少网络中需要学习的参数数量,从而降低了模型的复杂度和计算成本。
卷积残差代码实现
卷积残差处理方式
根据darknet-53网络结构图,可以得知卷积残差(ConvResidual )采用1x1卷积核下降通道数、增加非线性,而3x3卷积核用于提取特征并且上升通道数。采用1x1卷积核和3x3卷积核共同作用,加深网络的深度,加强特征抽象。
class Conv(nn.Module):
def __init__(self, c_in, c_out, k, s, p, bias=True):
"""
构建一个卷积层
:param c_in: 输入通道数
:param c_out: 输出通道数
:param k: 卷积核大小
:param s: 步幅
:param p: 填充
:param bias: 是否使用偏置
"""
super(Conv, self).__init__()
self.conv = nn.Sequential(
nn.Conv2d(c_in, c_out, k, s, p, bias=bias), # 卷积层
nn.BatchNorm2d(c_out), # 批量归一化层
nn.LeakyReLU(0.1), # LeakyReLU激活函数
)
def forward(self, entry):
"""
前向传播函数
:param entry: 输入数据
:return: 卷积层的输出结果
"""
return self.conv(entry)
class ConvResidual(nn.Module):
def __init__(self, c_in):
"""
构建一个卷积残差块
:param c_in: 输入通道数
"""
c = c_in // 2 # 计算输出通道数
super(ConvResidual, self).__init__()
self.conv = nn.Sequential(
Conv(c_in, c, 1, 1, 0), # 1x1卷积层,用于降维
Conv(c, c_in, 3, 1, 1), # 3x3卷积层,用于升维
)
def forward(self, entry):
"""
前向传播函数
:param entry: 输入数据
:return: 卷积残差块的输出结果
"""
return entry + self.conv(entry) # 将输入数据与卷积层的输出结果相加,实现残差连接
注解:"c = c_in // 2" 计算输出通道数,代码中c_in
表示输入通道数,c_out
表示输出通道数,为了保持特征图的维度不变,通常在残差块中将输入通道数减半,即输出通道数为输入通道数的一半,以便在后续的卷积层中使用。
代码处理
张量输入代码
上述卷积残差在张量进行卷积残差过程中,首先将输入张量传递给一个序列化的卷积操作模块(Conv),该模块中包含一个 1*1 的卷积操作来减少通道数,然后接着一个 3*3 的卷积操作来恢复通道数。最后,将这个卷积后的张量与原始输入张量相加,得到输出张量。这个相加操作确保了残差块的输出张量维度与输入张量相同。这种处理方式可以有效的保证,在增加神经网络深度、丰富和非线性同时,能够很好的保持特征维度不变。
加入输入张量input_tensor = torch.randn(1, 3, 416, 416):
import torch.nn as nn
import torch
class Conv(nn.Module):
def __init__(self, c_in, c_out, k, s, p, bias=True):
super(Conv, self).__init__()
self.conv = nn.Sequential(
nn.Conv2d(c_in, c_out, k, s, p, bias=bias),
nn.BatchNorm2d(c_out),
nn.LeakyReLU(0.1),
)
def forward(self, entry):
return self.conv(entry)
class ConvResidual(nn.Module):
def __init__(self, c_in):
c = c_in // 2
super(ConvResidual, self).__init__()
self.conv = nn.Sequential(
Conv(c_in, c, 1, 1, 0),
Conv(c, c_in, 3, 1, 1),
)
def forward(self, entry):
return entry + self.conv(entry)
# 示例输入
input_tensor = torch.randn(1, 3, 416, 416)
residual_module = ConvResidual(3)
output_tensor= residual_module(input_tensor)
print(output_tensor.shape)
在经过卷积残差处理过程后,张量形状依旧是torch.Size([1, 3, 416, 416])。
图像输入代码
如果输入为张量看的还不够明显,我们采用任意尺寸图片“girlwithdog.jpg”进行卷积残差表现。首先我们要对该尺寸的图片进行处理变成416*416的尺寸。这里不能够直接使用
resized_image = cv2.resize(image, (416, 416))
会造成图像失真。
应该采用周围空白填充的方式。
import torch
import torch.nn as nn
import cv2
import matplotlib.pyplot as plt
import torchvision.transforms as transforms
import numpy as np
# 定义卷积层
class Conv(nn.Module):
def __init__(self, c_in, c_out, k, s, p, bias=True):
super(Conv, self).__init__()
self.conv = nn.Sequential(
nn.Conv2d(c_in, c_out, k, s, p, bias=bias),
nn.BatchNorm2d(c_out),
nn.LeakyReLU(0.1),
)
def forward(self, entry):
return self.conv(entry)
# 定义残差块
class ConvResidual(nn.Module):
def __init__(self, c_in):
c = c_in // 2 # 计算输出通道数
super(ConvResidual, self).__init__()
self.conv = nn.Sequential(
Conv(c_in, c, 1, 1, 0), # 1*1卷积降维
Conv(c, c_in, 3, 1, 1), # 3*3卷积升维
)
def forward(self, entry):
return entry + self.conv(entry) # 加上残差
# 读取图像并进行预处理
image = cv2.imread('/content/drive/MyDrive/上传文件夹/girlwithdog.jpg')
image_rgb = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
h, w, _ = image.shape
diff = abs(h - w)
padding = diff // 2
if h > w:
padded_image = cv2.copyMakeBorder(image, 0, 0, padding, padding, cv2.BORDER_CONSTANT, value=(0, 0, 0))
else:
padded_image = cv2.copyMakeBorder(image, padding, padding, 0, 0, cv2.BORDER_CONSTANT, value=(0, 0, 0))
resized_image = cv2.resize(padded_image, (416,416))
# 转换为张量
transform = transforms.Compose([
transforms.ToTensor(),
])
tensor_image = transform(resized_image).unsqueeze(0)
# 使用卷积残差块处理图像
residual_module = ConvResidual(3)
output_image = residual_module(tensor_image)
# 将输出的张量转换为numpy数组
output_image_numpy = output_image.squeeze(0).permute(1, 2, 0).detach().numpy()
# 显示图像
print(image.shape)
plt.imshow(image)
plt.axis('on')
plt.show()
print(image_rgb.shape)
plt.imshow(image_rgb)
plt.axis('on')
plt.show()
print(resized_image.shape)
plt.imshow(resized_image)
plt.axis('on')
plt.show()
print(output_image_numpy.shape)
plt.imshow(output_image_numpy)
plt.axis('on')
plt.show()
如果想在一张幕布上显示,修改代码如下:
import torch
import torch.nn as nn
import cv2
import matplotlib.pyplot as plt
import torchvision.transforms as transforms
import numpy as np
class Conv(nn.Module):
def __init__(self, c_in, c_out, k, s, p, bias=True):
super(Conv, self).__init__()
self.conv = nn.Sequential(
nn.Conv2d(c_in, c_out, k, s, p, bias=bias),
nn.BatchNorm2d(c_out),
nn.LeakyReLU(0.1),
)
def forward(self, entry):
return self.conv(entry)
class ConvResidual(nn.Module):
def __init__(self, c_in):
c = c_in // 2
super(ConvResidual, self).__init__()
self.conv = nn.Sequential(
Conv(c_in, c, 1, 1, 0),
Conv(c, c_in, 3, 1, 1),
)
def forward(self, entry):
return entry + self.conv(entry)
image = cv2.imread('/content/drive/MyDrive/上传文件夹/girlwithdog.jpg')
image_rgb = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
h, w, _ = image.shape
diff = abs(h - w)
padding = diff // 2
if h > w:
padded_image = cv2.copyMakeBorder(image, 0, 0, padding, padding, cv2.BORDER_CONSTANT, value=(0, 0, 0))
else:
padded_image = cv2.copyMakeBorder(image, padding, padding, 0, 0, cv2.BORDER_CONSTANT, value=(0, 0, 0))
resized_image = cv2.resize(padded_image, (416,416))
transform = transforms.Compose([
transforms.ToTensor(),
])
tensor_image = transform(resized_image).unsqueeze(0)
residual_module = ConvResidual(3)
output_image = residual_module(tensor_image)
output_image_numpy = output_image.squeeze(0).permute(1, 2, 0).detach().numpy()
images = [image, image_rgb, resized_image, output_image_numpy]
titles = ['Original Image', 'RGB Image', 'Resized Image', 'Output Image']
plt.figure(figsize=(15, 10))
for i, (img, title) in enumerate(zip(images, titles), 1):
plt.subplot(2, 2, i)
plt.imshow(img)
plt.title(title)
plt.axis('on')
plt.show()
结果如下:
这里需要注意的是图1色彩与原图有所不同, 在使用OpenCV读取图像时,OpenCV默认使用的是BGR(蓝-绿-红)通道顺序,而在matplotlib中,通常使用的是RGB(红-绿-蓝)通道顺序。因此,如果直接使用OpenCV读取的图像进行显示,颜色通道的顺序会被颠倒,导致图像显示的颜色与预期不符。
为了解决这个问题,需要使用OpenCV的cvtColor
函数将图像的通道顺序从BGR转换为RGB,然后再使用matplotlib显示图像。这样可以确保图像的颜色显示正确。
上述图4即为卷积残差处理后的图像,更加适合于后续的任务,如分类、检测或分割等特征的提取。我们看起来似乎更加不容易识别了,但对于计算机来说,这样的特征处理后更加明显、易于提取分割。
需要注意的点
tensor_image = transform(resized_image).unsqueeze(0)代码,使用 unsqueeze(0)
将张量的形状从 [C, H, W]
变为 [1, C, H, W]
,其中 C
、H
和 W
分别代表通道数、高度和宽度。这是因为大多数深度学习模型要求输入张量的形状包含一个额外的批量维度,即第一个维度。
residual_module = ConvResidual(3)创建了一个名为residual_module
的残差模块实例,其中输入通道数为3。这个残差模块将接受一个形状为(batch_size, 3, height, width)
的张量作为输入,并在内部执行两次卷积操作,然后将残差连接添加到输入上。
padded_image = cv2.copyMakeBorder(image, 0, 0, padding, padding, cv2.BORDER_CONSTANT, value=(0, 0, 0))这行代码将会在输入图像的左右两侧添加宽度为 padding
的黑色边框,而顶部和底部不添加边框。这样就实现了在图像周围添加了指定宽度的黑色边框。
卷积函数本身的作用是执行卷积操作,但它通常不仅仅只是进行卷积,还包括一系列其他的操作, 执行卷积操作的过程通常在神经网络的 forward 方法中完成。
由于代码网络的权重参数是随机初始化的,并且每次前向传播时都会应用不同的权重值,因此残差网络处理后的图片输出结果也会不同。为了使输出结果稳定,可以通过设置PyTorch的随机种子来固定网络的初始化和操作过程中的随机性,以确保每次运行时的结果相同。