Backbone-DenseNet
1.介绍
DenseNet是2017年论文Densely Connected Convolutional Networks中介绍的基于卷积的网络,通常将其和ResNet做对比。关于DenseNet的思想和网络结构,很多文章中都提到了,没有必要翻来覆去说一个东西,所以本文对DenseNet的有关介绍只做总结,并详细以论文中DenseNet-121为例,讲一讲网络的具体细节与代码实现。
论文的思想很简单:在连续多个卷积操作的情况下,对于卷积层n,将前面所有卷积层(1、2…n-1)的输出合并后,传给卷积层n。比如下图中的卷积层3的输入,是卷积层1的输出(蓝)和卷积层2的输出(黄)合并起来的结果(蓝、黄),卷积层3的输出为红色部分。
这样的思想带来的好处很明显,从直觉上来看有两个,其一是在梯度反向传导时,因为较前卷积层的输出传递到了较后卷积层中,所以复合函数求导时,导数能穿越到最前面去,从而更好地更新参数;其二是,较前卷积层的底层特征传递到了较后卷积层中,网络能利用更多图像的细节信息。
2.结构
论文中给出了各种类型ResNet的表格。这里我们选取DenseNet-121进行实现。
现在我们将DenseNet-121分成几个部分(看代码,在DenseNet121类的init函数):prenet、denseBlock、transition和classification。只有这四个部分,当然denseBlock和transition有很多个。prenet是对应着上表中的Convolution和Pooling,classification对应着Classification Layer。这两层没什么说的,表格中写得很清晰。
重点是Dense Block和Transition Layer的实现,以表格中的Dense Block(1)和Transition Layer(1)举例,下图是细节的展示,一图胜千言了吧。DenseNet-121的第一个Dense Block有6个layer,第2、3、4个Dense Block分别有12、24、16个layer。同一个Dense Block中,前面的layer的输出传递给后面的layer,细节见DenseBlock类的forward部分代码。
还有一张图画得很好(下图),和上面的图可以对照着看。
3.实现
from typing import Optional
from torch.nn import Sequential, BatchNorm2d, ReLU, Conv2d, Dropout2d
from torch.nn import Sequential, Conv2d, BatchNorm2d, ReLU
from typing import Optional
import torch
from torch.nn import Module
import torch.nn as nn
from torchsummary import summary
# k=32
class DenseNet121(nn.Module):
def __init__(self, num_classes=1000):
super(DenseNet121, self).__init__()
self.add_module('prenet', nn.Sequential(
nn.Conv2d(in_channels=3, out_channels=64, kernel_size=7, stride=2, padding=3),
nn.MaxPool2d(kernel_size=3, stride=2, padding=1),
))
self.denseBlock1 = DenseBlock(in_channels=64, growth_rate=32, num_layers=6, concat_input=True)
self.transition1 = Transition(in_channels=256, out_channels=256, reduce_ratio=0.5)
self.denseBlock2 = DenseBlock(in_channels=128, growth_rate=32, num_layers=12, concat_input=True)
self.transition2 = Transition(in_channels=512, out_channels=512, reduce_ratio=0.5)
self.denseBlock3 = DenseBlock(in_channels=256, growth_rate=32, num_layers=24, concat_input=True)
self.transition3 = Transition(in_channels=1024, out_channels=1024, reduce_ratio=0.5)
self.denseBlock4 = DenseBlock(in_channels=512, growth_rate=32, num_layers=16, concat_input=True)
self.classification = Classification(1024, 1000)
def forward(self, x):
x = self.prenet(x)
x = self.denseBlock1(x) # torch.Size([1, 256, 56, 56])
x = self.transition1(x) # torch.Size([1, 128, 28, 28])
x = self.denseBlock2(x) # torch.Size([1, 512, 28, 28])
x = self.transition2(x) # torch.Size([1, 256, 14, 14])
x = self.denseBlock3(x) # torch.Size([1, 1024, 14, 14])
x = self.transition3(x) # torch.Size([1, 512, 7, 7])
x = self.denseBlock4(x) # torch.Size([1, 1024, 7, 7])
x = self.classification(x)
return x
class Classification(nn.Module):
def __init__(self, in_channels, out_channels):
super(Classification, self).__init__()
self.add_module('norm', nn.BatchNorm2d(in_channels))
self.add_module('relu', nn.ReLU(inplace=True))
self.add_module('pool', nn.AvgPool2d(kernel_size=7, stride=1))
# flatten
self.add_module('relu', nn.ReLU(inplace=True))
self.add_module('linear', nn.Linear(in_channels, out_channels))
self.add_module('softmax', nn.Softmax(dim=1))
def forward(self, x):
x = self.norm(x)
x = self.relu(x)
x = self.pool(x)
x = torch.flatten(x, 1)
x = self.linear(x)
x = self.softmax(x)
return x
class Transition(nn.Module):
def __init__(self, in_channels: int, out_channels: int, reduce_ratio: float = 1.):
super(Transition, self).__init__()
self.add_module('tran', nn.Sequential(
BatchNorm2d(num_features=in_channels),
ReLU(inplace=True),
nn.Conv2d(in_channels=in_channels, out_channels=int(out_channels*reduce_ratio), kernel_size=1, stride=1, bias=False),
nn.MaxPool2d(kernel_size=3, stride=2, padding=1),
))
def forward(self, x):
x = self.tran(x)
return x
class DenseBlock(nn.Module):
r"""
Dense Block as described in [DenseNet](https://arxiv.org/abs/1608.06993)
and implemented in https://github.com/liuzhuang13/DenseNet
- Consists of several DenseLayer (possibly using a Bottleneck and Dropout) with the same output shape
- The first DenseLayer is fed with the block input
- Each subsequent DenseLayer is fed with a tensor obtained by concatenating the input and the output
of the previous DenseLayer on the channel axis
- The block output is the concatenation of the output of every DenseLayer, and optionally the block input,
so it will have a channel depth of (growth_rate * num_layers) or (growth_rate * num_layers + in_channels)
"""
def __init__(self, in_channels: int, growth_rate: int, num_layers: int,
concat_input: bool = False, dense_layer_params: Optional[dict] = None):
super(DenseBlock, self).__init__()
self.concat_input = concat_input
self.in_channels = in_channels
self.growth_rate = growth_rate
self.num_layers = num_layers
self.out_channels = growth_rate * num_layers
if self.concat_input:
self.out_channels += self.in_channels
if dense_layer_params is None:
dense_layer_params = {}
for i in range(num_layers):
# 增添dense_layer:norm->relu->bottleneck->conv->dropout
self.add_module(
f'layer_{i}',
DenseLayer(in_channels=in_channels + i * growth_rate, out_channels=growth_rate, **dense_layer_params)
)
def forward(self, block_input):
layer_input = block_input
# empty tensor (not initialized) + shape=(0,)
layer_output = block_input.new_empty(0)
all_outputs = [block_input] if self.concat_input else []
print(self._modules.values())
for layer in self._modules.values(): # 对于每一层
layer_input = torch.cat([layer_input, layer_output], dim=1) # 输入为上一层输入和上一层输出的concatenation
layer_output = layer(layer_input) # 更新输出
all_outputs.append(layer_output)
return torch.cat(all_outputs, dim=1)
class DenseLayer(nn.Module):
"""Consists of:
- Batch Normalization
- ReLU
- (Bottleneck)
- 3x3 Convolution
- (Dropout)
"""
def __init__(self, in_channels: int, out_channels: int,
bottleneck_ratio: Optional[int] = None, dropout: float = 0.0):
super(DenseLayer, self).__init__()
self.bottleneck_ratio = bottleneck_ratio
self.dropout = dropout
self.in_channels = in_channels
self.out_channels = out_channels
self.add_module('norm', BatchNorm2d(num_features=in_channels))
self.add_module('relu', ReLU(inplace=True))
if bottleneck_ratio is not None:
self.add_module('bottleneck', Bottleneck(in_channels, bottleneck_ratio * out_channels))
in_channels = bottleneck_ratio * out_channels
self.add_module('conv', Conv2d(in_channels, out_channels, kernel_size=3, padding=1, bias=False))
if dropout > 0:
self.add_module('drop', Dropout2d(dropout, inplace=True))
def forward(self, x):
x = self.norm(x)
x = self.relu(x)
if self.bottleneck_ratio is not None:
x = self.bottleneck(x)
x = self.conv(x)
if self.dropout > 0:
self.drop(x)
return x
class Bottleneck(nn.Module):
r"""
A 1x1 convolutional layer, followed by Batch Normalization and ReLU
"""
def __init__(self, in_channels: int, out_channels: int):
super(Bottleneck, self).__init__()
self.in_channels = in_channels
self.out_channels = out_channels
self.add_module('conv', Conv2d(in_channels, out_channels, kernel_size=1, bias=False))
self.add_module('norm', BatchNorm2d(num_features=out_channels))
self.add_module('relu', ReLU(inplace=True))
def forward(self, x):
x = self.conv(x)
x = self.norm(x)
x = self.relu(x)
return x
if __name__ == '__main__':
# Example
net = DenseNet121()
# net = DenseBlock(in_channels=3, growth_rate=32, num_layers=3, concat_input=False)
x = torch.rand(1, 3, 224, 224)
out = net.forward(x)
print(out.size())
print(net)
# net = net.to('cuda')
# summary(net, (3, 224, 224))
4.总结与反思
有以下几个收获吧。
- 看paper能力提高了,能一点一点啃完。
- 代码能力也提高了,比如说
self.add_module
这种用法,还有DenseBlock的forward部分用for循环来动态传递,还是很骚气的。当然还有一些奇怪的表达bottleneck_ratio: Optional[int] = None
,等我完全弄明白了再补上。。 - torchsummary工具真的很好用啊,可以打印出网络的各层结构和输出尺寸,还可以统计参数量,爱了爱了。
Backbone部分到此结束了,整体来看难度不大,所有的代码实现部分我只写了model,并未训练和调参。下一个专题是Detection,理论上讲我会加上training的环节
5.附录
论文链接:https://arxiv.org/pdf/1608.06993.pdf
思路参考:https://zhuanlan.zhihu.com/p/141178215
代码参考:https://blog.csdn.net/sinat_33761963/article/details/83958802