J3、DenseNet算法实战与解析
- 🍨 本文为🔗365天深度学习训练营 中的学习记录博客
- 🍦 参考文章:365天深度学习训练营-第P2周:彩色识别
- 🍖 原作者:K同学啊|接辅导、项目定制
ResNet模型的核心是通过建立前面层与后面层之间的“短路连接”(shortcuts,skip connection),进而训练出更深的CNN网络。
DenseNet模型的基本思路与ResNet一致,但是它建立的是前面所有层与后面层的密集连接(dense connection),它的名称也是由此而来。DenseNet的另一大特色是通过特征在channel上的连接来实现特征重用(feature reuse)。这些特点让DenseNet在参数和计算成本更少的情形下实现比ResNet更优的性能,DenseNet也因此斩获CVPR 2017的最佳论文奖。
一、论文解读¶
1、设计理念¶
相比ResNet,DenseNet提出了一个更激进的密集连接机制:即互相连接所有的层,具体来说就是每个层都会接受其前面所有层作为其额外的输入。
ResNet是每个层与前面的某层(一般是2-4层)短路连接在一起,连接方式是通过元素相加。
而在DenseNet中,每个层都会与前面所有层在channel维度上连接(concat)在一起(即元素叠加),并作为下一层的输入。所有的层的输入都来源于前面所有层在channel维度的concat。
2、网络结构¶
CNN网络一般要经过Pooling或者stride>1的Conv来降低特征图的大小,而DenseNet的密集连接方式需要特征图大小保持一致。为了解决这个问题,DenseNet网络中使用DenseBlock+Transition的结构,其中DenseBlock是包含很多层的模块,每个层的特征图大小相同,层与层之间采用密集连接方式。而Transition层是连接两个相邻的DenseBlock,并且通过Pooling使特征图大小降低。
DenseBlock示意如下,各个DenseBlock之间通过Transition层连接在一起
在DenseBlock中,各个层的特征图大小一致,可以在channel维度上连接。DenseBlock中的非线性组合函数H(·)的是 BN+ReLU+3x3 Conv 的结构,如下图所示。另外值得注意的一点是,与ResNet不同,所有DenseBlock中各个层卷积之后均输出k个特征图,即得到的特征图的channel数为k,或者说采用k个卷积核。k在DenseNet称为growth rate,这是一个超参数。一般情况下使用较小的k(比如12),就可以得到较佳的性能。假定输入层的特征图的channel数为k0,那么l层输入的channel数为k~0~+k~(1,2,...,l-1)~,因此随着层数增加,尽管k设定得较小,DenseBlock的输入会非常多,不过这是由于特征重用所造成的,每个层仅有k个特征是自己独有的。
由于后面层的输入会非常大,DenseBlock内部可以采用bottleneck层来减少计算量,主要是原有的结构中增加1x1 Conv,如图7所示,即BN+ReLU+1x1 Conv+BN+ReLU+3x3 Conv,称为DenseNet-B结构。其中1x1 Conv得到4k个特征图它起到的作用是降低特征数量,从而提升计算效率。
对于Transition层,,它主要是连接两个相邻的DenseBlock,并且降低特征图大小。Transition层包括一个1x1的卷积和2x2的AvgPooling,结构为BN+ReLU+1x1Conv+2x2AvgPooling。另外,Transition层可以起到压缩模型的作用。假定层的上接DenseBlock得到的特征图channels数为m,Transition层可以产生【θm】个特征(通过卷积层),其中 是压缩系数θ∈(0,1](compression rate)。当 θ=1时,特征个数经过Transition层没有变化,即无压缩,而当压缩系数小于1时,这种结构称为DenseNet-C,文中使用θ=0.5。对于使用bottleneck层的DenseBlock结构和压缩系数小于1的Transition组合结构称为DenseNet-BC。
对于ImageNet数据集,图片输入大小为224×224,网络结构采用包含4个DenseBlock的DenseNet-BC,其首先是一个stride=2的7x7卷积层,然后是一个stride=2的3x3 MaxPooling层,后面才进入DenseBlock。ImageNet数据集所采用的网络配置如下表所示:
3、与其他算法的对比¶
# PyTorch搭建DenseBlock
import torch
import torch.nn as nn
import torchvision.transforms as transforms
import torchvision
import matplotlib.pyplot as plot
import torch.nn.functional as F
class DenseLayer(nn.Sequential):
def __init__(self, in_channel, growth_rate, bn_size, drop_rate):
super(DenseLayer, self).__init__()
self.add_module('norm1', nn.BatchNorm2d(in_channel))
self.add_module('relu1', nn.ReLU(inplace=True))
self.add_module('conv1', nn.Conv2d(in_channel, bn_size*growth_rate,
kernel_size=1, stride=1, bias=False))
self.add_module('norm2', nn.BatchNorm2d(bn_size*growth_rate))
self.add_module('relu2', nn.ReLU(inplace=True))
self.add_module('conv2', nn.Conv2d(bn_size*growth_rate, growth_rate,
kernel_size=3, stride=1, padding=1, bias=False))
self.drop_rate = drop_rate
def forward(self, x):
new_feature = super(DenseLayer, self).forward(x)
if self.drop_rate>0:
new_feature = F.dropout(new_feature, p=self.drop_rate, training=self.training)
return torch.cat([x, new_feature], 1)
# TensorFlow搭建DenseBlock
import tensorflow as tf
from tensorflow.keras import layers, models
from tensorflow import keras
class BottleNeck(keras.Model):
def __init__(self, growth_rate, bn_size = 4, dropout = 0.3):
super().__init__()
self.bn1 = layers.BatchNormalization()
self.relu = layers.Activation("relu"),
self.conv1 = layers.Conv2D(filters=bn_size * growth_rate, kernel_size=(1, 1),
strides=1, padding='same')
self.bn2 = layers.BatchNormalization()
self.conv2 = layers.Conv2D(filters=growth_rate, kernel_size=(3, 3),
strides=1, padding='same')
self.dropout = layers.Dropout(rate=dropout)
self.listLayers = [
self.bn1,
self.relu,
self.conv1,
self.bn2,
self.relu,
self.conv2,
self.dropout
]
def call(self, x):
tem = x
for layer in self.listLayers.layers:
x = layer(x)
return layers.concatenate([tem, x], axis=-1)
2、实现DenseBlock¶
# PyTorch
''' DenseBlock '''
class DenseBlock(nn.Sequential):
def __init__(self, num_layers, in_channel, bn_size, growth_rate, drop_rate):
super(DenseBlock, self).__init__()
for i in range(num_layers):
layer = DenseLayer(in_channel+i*growth_rate, growth_rate, bn_size, drop_rate)
self.add_module('denselayer%d'%(i+1,), layer)
# TensorFlow
class DenseBlock(tf.keras.Model):
def __init__(self, num_layer, growth_rate, bn_size = 4, dropout = 0.3, efficient=False):
super().__init__()
self.efficient = efficient
self.listLayers = []
if self.efficient:
_x = tf.recompute_grad(BottleNeck(growth_rate, bn_size = bn_size, dropout = dropout))
else:_x =BottleNeck(growth_rate, bn_size = bn_size, dropout = dropout)
for _ in range(num_layer):
self.listLayers.append(BottleNeck(growth_rate, bn_size = bn_size, dropout = dropout))
def call(self, x):
for layer in self.listLayers.layers:
x = layer(x)
return x
3、实现Transition:主要是一个卷积层和一个池化层¶
# PyTorch
''' Transition layer between two adjacent DenseBlock '''
class Transition(nn.Sequential):
def __init__(self, in_channel, out_channel):
super(Transition, self).__init__()
self.add_module('norm', nn.BatchNorm2d(in_channel))
self.add_module('relu', nn.ReLU(inplace=True))
self.add_module('conv', nn.Conv2d(in_channel, out_channel,
kernel_size=1, stride=1, bias=False))
self.add_module('pool', nn.AvgPool2d(2, stride=2))
# TensorFlow
class Transition(tf.keras.Model):
def __init__(self, growth_rate):
super().__init__()
self.bn1 = layers.BatchNormalization()
self.relu = layers.Activation('relu')
self.conv1 = layers.Conv2D(filters = growth_rate, kernel_size=(1, 1),
strides = 1, activation = 'relu', padding='same')
self.pooling = layers.AveragePooling2D(pool_size=(2,2), strides = 2,padding='same')
self.listLayers = [
self.bn1,
self.relu,
self.conv1,
self.pooling
]
def call(self,x):
for layer in self.listLayers.layers:
x = layer(x)
return x
4、实现DenseNet网络¶
# PyTorch
class DenseNet(nn.Module):
def __init__(self, growth_rate=32, block_config=(6,12,24,16), init_channel=64,
bn_size=4, compression_rate=0.5, drop_rate=0, num_classes=1000):
"""
:param growth_rate: (int) number of filters used in DenseLayer, `k` in the paper
:param block_config: (list of 4 ints) number of layers in eatch DenseBlock
:param init_channel: (int) number of filters in the first Conv2d
:param bn_size: (int) the factor using in the bottleneck layer
:param compression_rate: (float) the compression rate used in Transition Layer
:param drop_rate: (float) the drop rate after each DenseLayer
:param num_classes: (int) number of classes for classification
"""
super(DenseNet, self).__init__()
# first Conv2d
self.features = nn.Sequential(OrderedDict([
('conv0', nn.Conv2d(3, init_channel, kernel_size=7, stride=2, padding=3, bias=False)),
('norm0', nn.BatchNorm2d(init_channel)),
('relu0', nn.ReLU(inplace=True)),
('pool0', nn.MaxPool2d(3, stride=2, padding=1))
]))
# DenseBlock
num_features = init_channel
for i, num_layers in enumerate(block_config):
block = DenseBlock(num_layers, num_features, bn_size, growth_rate, drop_rate)
self.features.add_module('denseblock%d'%(i+1), block)
num_features += num_layers*growth_rate
if i != len(block_config)-1:
transition = Transition(num_features, int(num_features*compression_rate))
self.features.add_module('transition%d'%(i+1), transition)
num_features = int(num_features*compression_rate)
# final BN+ReLU
self.features.add_module('norm5', nn.BatchNorm2d(num_features))
self.features.add_module('relu5', nn.ReLU(inplace=True))
# classification layer
self.classifier = nn.Linear(num_features, num_classes)
# params initialization
for m in self.modules():
if isinstance(m, nn.Conv2d):
nn.init.kaiming_normal_(m.weight)
elif isinstance(m, nn.BatchNorm2d):
nn.init.constant_(m.bias, 0)
nn.init.constant_(m.weight, 1)
elif isinstance(m, nn.Linear):
nn.init.constant_(m.bias, 0)
def forward(self, x):
x = self.features(x)
x = F.avg_pool2d(x, 7, stride=1).view(x.size(0), -1)
x = self.classifier(x)
return x
# TensorFlow
class DenseNet(tf.keras.Model):
def __init__(self, num_init_feature, growth_rate, block_config, num_classes,
bn_size=4, dropout=0.3, compression_rate=0.5, efficient=False):
super().__init__()
self.num_channels = num_init_feature
self.conv = layers.Conv2D(filters = num_init_feature, kernel_size=7,
strides = 2, padding='same')
self.bn = layers.BatchNormalization()
self.relu = layers.Activation('relu')
self.max_pool = layers.MaxPool2D(pool_size=3, strides=2, padding='same')
self.dense_block_layers = []
for i in block_config[:-1]:
self.dense_block_layers.append( DenseBlock(num_layer =i, growth_rate = growth_rate,
bn_size = bn_size, dropout = dropout, efficient=efficient))
self.num_channels = compression_rate * (self.num_channels + growth_rate * i)
self.dense_block_layers.append( Transition(self.num_channels))
self.dense_block_layers.append( DenseBlock(num_layer =block_config[-1], growth_rate = growth_rate,
bn_size = bn_size, dropout = dropout, efficient=efficient))
self.avgpool = layers.GlobalAveragePooling2D()
self.fc = tf.keras.layers.Dense(units=num_classes, activation=tf.keras.activations.softmax)
def call(self,x):
x = self.conv(x)
x = self.bn(x)
x = self.relu(x)
x = self.max_pool(x)
for layer in self.dense_block_layers.layers:
x = layer(x)
x = self.avgpool(x)
return self.fc(x)
5、实现DenseNet-121网络¶
# PtTorch
def densenet121(n_classes=1000, pretrained=False, **kwargs):
model = DenseNet(init_channel=64, growth_rate=32, block_config=(6,12,24,16),
num_classes=n_classes, **kwargs)
if pretrained:
# '.'s are no longer allowed in module names, but previous DenseLayer
# has keys 'norm.1', 'relu.1', 'conv.1', 'norm.2', 'relu.2', 'conv.2'.
# They are also in the checkpoints in model_urls. This pattern is used
# to find such keys.
pattern = re.compile(
r'^(.*denselayer\d+\.(?:norm|relu|conv))\.((?:[12])\.(?:weight|bias|running_mean|running_var))$')
state_dict = model_zoo.load_url(model_urls['densenet121'])
for key in list(state_dict.keys()):
res = pattern.match(key)
if res:
new_key = res.group(1) + res.group(2)
state_dict[new_key] = state_dict[key]
del state_dict[key]
model.load_state_dict(state_dict)
return model
# TensorFlow
model = DenseNet(num_init_feature=64,
growth_rate=32,
block_config=[6,12,24,16],
compression_rate=0.5,
num_classes = 4,
dropout=0.0,
efficient=True)