来自 B 站刘二大人的《PyTorch深度学习实践》P11 的学习笔记
上一篇 卷积神经网络进阶(GoogLeNet、ResNet) 我们实践了 GoogleNet 和 ResNet 两大经典网络:
- GoogleNet 从多路选择学习(Inception Module)出发,构建更好的神经网络;
- ResNet 从残差连接(Residual Block)保留浅层特征出发,构建更深的神经网络。
DenseNet 紧随其后,再次研究了超深度神经网络的梯度消失问题和跳连解决方案。他们提到,ResNet 等的方法尽管在神经网络拓扑和训练过程上有所不同,但它们都有一个关键特征:使用短路径连接前后两层。
DenseNet 结构
DenseNet 更加偏执,为了确保网络在深度传播中能保留更多信息,他们不仅连接前后两层,在一个 Dense Block 中每一层都和后面所有层相连,并且是将特征在通道上堆叠再传入下一层(像 Inception Module 的输出),而不是 ResNet 那样的元素相加。
这当然能保留更多信息,不过这显存消耗可想而知,这就是为什么它叫 Dense,一个 L 层的 Dense Block 有
L
(
L
+
1
)
2
\frac{L(L+1)}{2}
2L(L+1)? 条连接。
一、Dense Block
每个 Dense Block 里的卷积层都是 3×3 卷积,下表第三列乘号后面的数表示一个 Dense Block 中有多少层这样的卷积。
- Bottleneck Layers
在每个 3×3 卷积之前引入 1×1 卷积作为瓶颈层,可以减少输入特征的通道数,从而提高计算效率。
所以,如果 Dense Block 里面是 1×1 卷积和 3×3 卷积的组合,那么这些 Dense Blocks 组成的 DenseNet 被称为 DenseNet-B。
二、Transition Layer
当然,为了控制显存占用,DenseNet 主要结构除了这 Dense Block 外还有每个 Block 后连接的过渡层(Transition Layer),它通过一个 1×1 的卷积层来控制通道数,并使用平均池化来减半特征图的高和宽。 所以,Dense Blocks 之间存在过渡层,那么这些 Dense Blocks 组成的 DenseNet 被称为 DenseNet-C。
两者都存在的 DenseNet,称为 DenseNet-BC,这是 DenseNet 的终极配置。。。
三、Growth Rate
增长率
k
k
k,它表示每一个卷积层输出的通道数。论文中说:设
k
0
k_0
k0? 是初始输入的图像的通道数,那么
l
l
l 层的 Dense Net 就会产生
k
0
+
k
(
l
?
1
)
k_0 + k(l-1)
k0?+k(l?1) 个通道的特征图,这个
k
k
k 就被称为增长率。
这表示通道数“爆炸”速度,但是多搞一个新名字出来吓唬人,我们当然可以直接说这是每个卷积层输出的通道数:
o
u
t
_
c
h
a
n
n
e
l
s
out\_channels
out_channels,但是在代码实现过程中你就能发现,输出通道数要用于计算
利用 Bottleneck layer,Translation layer 以及较小的 Growth rate 使得网络变窄,参数减少,有效抑制了过拟合,同时计算量也减少了。
DenseNet 最后的分类器使用全局平均池化接一层全连接层。
四、Implementation Details
对于图像较小的数据集,比如 CIFAR-10/100、SVHN,DenseNet 由 3 个 Dense Block 组成。
- 在输入第一个 Dense Block 之前先经过一个 padding=1 的 3×3 卷积,输出 32 通道;
- 使用 DenseNet-BC 配置,在每个 Dense Block 之间加入过渡层,使特征图通道数和宽高减半;
- 在最后一个 Dense Block 的末尾,执行全局平均池化,然后连接 Softmax 分类器。
输入 3 个 Dense Block 中的特征图大小分别为:32×32、16×16、8×8。采用
{
L
=
40
,
k
=
12
}
\{L = 40,k = 12\}
{L=40,k=12} 的 DenseNet-BC 配置。
五、代码实现
-
conv_block 是 Dense Block 的基础结构,包含一层 3×3 卷积的 basic_block,和一层可选的 1×1 卷积的 bottleneck:
关于 nn.Sequential 可以查阅官方文档,里面举的例子十分清晰易懂。
from collections import OrderedDict
import torch
from torch import nn
def conv_block(in_channels, out_channels, bo=True):
"""
Dense Block 的基本组件, 一层 3×3 卷积和一层可选的 1×1 卷积
:param in_channels:
:param out_channels: growth rate k
:param bo: 是否使用 bottleneck
:return: 一个 conv_block
"""
in_channels_ = out_channels * 2 if bo else in_channels
bo_layers = nn.Sequential(OrderedDict([
('bn0', nn.BatchNorm2d(in_channels)),
('relu0', nn.ReLU()),
('conv1x1', nn.Conv2d(in_channels, in_channels_, kernel_size=1)),
]))
basic_blk = nn.Sequential(OrderedDict([
('bn1', nn.BatchNorm2d(in_channels_)),
('relu1', nn.ReLU()),
('conv3x3', nn.Conv2d(in_channels_, out_channels, kernel_size=3, padding=1)),
]))
bottleneck = bo_layers if bo else nn.Sequential()
blk = nn.Sequential()
blk.add_module('bottleneck', bottleneck)
blk.add_module('basic_blk', basic_blk)
return blk
-
Dense Block 当前 conv_block 的输入是前面所有 conv_block 的输出堆叠起来的,所以第
l
l
l 层的输入通道数为:
k
0
+
k
×
(
l
?
1
)
k_0 + k \times (l - 1)
k0?+k×(l?1)
k
0
k_0
k0? 是初始输入层的通道数
k
k
k 就是上面提到的 Growth Rate,也就是 conv_block 中每个卷积层的输出通道数 out_channels 。 def dense_block(in_channels, conv_blk_num=4, k=12, bo=True):
"""
dense_block 由上面多个 conv_block 组成,用 for 循环添加到 nn.Sequential() 中
:param in_channels:
:param conv_blk_num: 3×3卷积层个数
:param k: = out_channels,每个3×3卷积层的输出通道数
:param bo: 是否加入 bottleneck
:return: 一个 dense_block
"""
dense_block = nn.Sequential()
for i in range(conv_blk_num):
in_channels_ = in_channels + i * k
dense_block.add_module(f'conv_blk_{i}', conv_block(in_channels_, k, bo=bo))
return dense_block
-
transition_layer 是 Dense Block 之间的过渡层,用 1×1 卷积减少 Dense Block 的输出通道数,否则会越叠越多,导致内存爆炸: def transition_layer(input, in_channels, out_channels):
"""
过渡层,在 Dense Block 和 Dense Block 之间,把前一个的输出通道减半
:param in_channels: 前一个的输出通道
:param out_channels: 输出通道减半
:return:
"""
blk = nn.Sequential(OrderedDict([
('bn', nn.BatchNorm2d(in_channels)),
('relu', nn.ReLU()),
('conv1x1', nn.Conv2d(in_channels, out_channels, kernel_size=1)),
('avgpool', nn.AvgPool2d(2))
]))
return blk(input)
-
我们可以先试验一个多层 1×1 和 3×3 卷积组成的 Dense Block 的网络结构和输出特征图的形状: def forward(x, model, transition=True):
for i, blk in enumerate(model):
print(i)
y = blk(x)
x = torch.cat((x, y), dim=1)
print(x.shape)
if transition:
x = transition_layer(x, x.shape[1], x.shape[1]//2)
print("x final size:", x.shape)
return x
if __name__ == '__main__':
in_channels = 16
input = torch.randn(1, in_channels, 28, 28)
dense_block = dense_block(in_channels=in_channels, conv_blk_num=4, k=12, bo=True)
print(dense_block)
output_dense = forward(input, model=dense_block, transition=False)
-
DenseNet 的基础结构由 3 个 Dense Blocks 以及可选的 Dense Block 之间的过渡层(Transition layers)组成,这些可选组件都可以通过参数来设定,因为我们已经全部实现了: from collections import OrderedDict
import torch
from torch import nn
from torch.nn import functional as F
def transition_layer(input, in_channels, out_channels):
"""
过渡层,在 Dense Block 和 Dense Block 之间,把前一个的输出通道和长宽减半
:param in_channels: 前一个的输出通道
:param out_channels: 输出通道减半
:return:
"""
blk = nn.Sequential(OrderedDict([
('bn', nn.BatchNorm2d(in_channels)),
('relu', nn.ReLU()),
('conv1x1', nn.Conv2d(in_channels, out_channels, kernel_size=1)),
('avgpool', nn.AvgPool2d(2))
]))
return blk(input)
def conv_block(in_channels, out_channels, bo=False):
"""
Dense Block 的基本组件
:param in_channels:
:param out_channels: growth rate k
:return:
"""
in_channels_ = out_channels * 2 if bo else in_channels
bo_layers = nn.Sequential(OrderedDict([
('bn0', nn.BatchNorm2d(in_channels)),
('relu0', nn.ReLU()),
('conv1x1', nn.Conv2d(in_channels, in_channels_, kernel_size=1)),
]))
basic_blk = nn.Sequential(OrderedDict([
('bn1', nn.BatchNorm2d(in_channels_)),
('relu1', nn.ReLU()),
('conv3x3', nn.Conv2d(in_channels_, out_channels, kernel_size=3, padding=1)),
]))
bottleneck = bo_layers if bo else nn.Sequential()
blk = nn.Sequential()
blk.add_module('bottleneck', bottleneck)
blk.add_module('basic_blk', basic_blk)
return blk
class DenseBlock(nn.Module):
"""
由 conv_blk 组成
:param in_channels: 动态的,由上一个 Dense Block 和过渡层决定
:param out_channels: growth rate k
:param dense_blk_num: 由多少 conv_blk 组成一个 Dense Block
"""
def __init__(self,
in_channels,
out_channels,
conv_blk_num=4,
bo=False,
transition=True):
super(DenseBlock, self).__init__()
self.transition = transition
self.net = nn.Sequential()
for i in range(conv_blk_num):
in_channels_ = in_channels + i * out_channels
self.net.add_module(f'conv_blk_{i}',
conv_block(in_channels_, out_channels, bo))
def forward(self, x):
for blk in self.net:
y = blk(x)
x = torch.cat((x, y), dim=1)
if self.transition:
out_channels = x.shape[1]
x = transition_layer(x, out_channels, out_channels // 2)
print(x.shape)
return x
class DenceNet(nn.Module):
"""
由 Dense Block 组成
:param in_channels: 3-cifar-10, 1-mnist
:param out_channels: growth rate k
:param dense_blk_num: 由多少 Dense Block 组成
"""
def __init__(self,
in_channels=16,
out_channels=12,
conv_blk_num=4,
dense_blk_num=3,
bo=False,
transition=True):
super(DenceNet, self).__init__()
self.bn0 = nn.BatchNorm2d(3)
self.conv0 = nn.Conv2d(3, 16, kernel_size=3, padding=1)
self.dense_net = nn.Sequential()
in_channels_ = in_channels
for i in range(dense_blk_num):
in_channels_ += conv_blk_num * out_channels if i > 0 else 0
in_channels_ = in_channels_ // 2 if i > 0 else in_channels_
print("in_channels_:", in_channels_)
if transition:
self.is_transition = dense_blk_num - 1 - i
self.dense_net.add_module(f"dense_blk_{i}",
DenseBlock(in_channels_,
out_channels,
conv_blk_num=conv_blk_num,
bo=bo,
transition=self.is_transition))
self.aap = nn.AdaptiveAvgPool2d(10)
self.fc = nn.Linear(8800, 10)
def forward(self, x):
batch_size = x.size(0)
x = self.conv0(F.relu(self.bn0(x)))
x = self.dense_net(x)
x = self.aap(x)
x = x.view(batch_size, -1)
x = F.softmax(self.fc(x))
return x
if __name__ == '__main__':
in_channels, k = 16, 12
model = DenceNet(in_channels=in_channels,
out_channels=k,
conv_blk_num=4,
dense_blk_num=3,
bo=False,
transition=True
)
print(model)
input = torch.randn(1, 3, 32, 32)
output = model(input)
print("final x shape:", output.shape)
skill:在不知道全连接层接收的参数到底为多少的情况下(比如,这里的8800),我们可以注释掉 self.fc() 的调用,然后构造和训练数据集一个 mini-batch 相同大小的数据作为 input,得到的输出很轻易能告诉你答案。
|