ResNet
极大地改变了如何参数化深层网络中函数的观点。稠密连接网络(DenseNet
)在某种程度上是ResNet
的逻辑扩展。让我们先从数学上了解一下。
从ResNet到DenseNet
回想一下任意函数的泰勒展开式(Taylor expansion
),它把这个函数分解成越来越高阶的项。在接近0
时。
同样,ResNet
将函数展开为:
也就是说,ResNet
将分解为两部分:一个简单的线性项和一个复杂的非线性项。那么再向前拓展一步,如果我们想将拓展成超过两部分的信息呢?一种方案便是DenseNet
。
如上图所示,ResNet
和DenseNet
的关键区别在于,DenseNet
输出是连接(用图中的[,]表示)而不是如ResNet
的简单相加。因此,在应用越来越复杂的函数序列后,我们执行从到其展开式的映射:
最后,将这些展开式结合到多层感知机中,再次减少特征的数量。实现起来非常简单:我们不需要添加术语,而是将它们连接起来。DenseNet
这个名字由变量之间的“稠密连接”而得来,最后一层与之前的所有层紧密相连。稠密连接如下图所示。
稠密网络主要由2部分构成:稠密块(dense block
)和过渡层(transition layer
)。前者定义如何连接输入和输出,而后者则控制通道数量,使其不会太复杂。
稠密块体
DenseNet
使用了ResNet
改良版的“批量规范化、激活和卷积”架构。我们首先实现一下这个架构。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
| import tensorflow as tf
class ConvBlock(tf.keras.layers.Layer): def __init__(self, num_channels): super(ConvBlock, self).__init__() self.bn = tf.keras.layers.BatchNormalization() self.relu = tf.keras.layers.ReLU() self.conv = tf.keras.layers.Conv2D(filters=num_channels, kernel_size=(3, 3), padding='same') self.listLayers = [self.bn, self.relu, self.conv]
def call(self, x): y = x for layer in self.listLayers.layers: y = layer(y) y = tf.keras.layers.concatenate([x,y], axis=-1) return y
class DenseBlock(tf.keras.layers.Layer): def __init__(self, num_convs, num_channels): super(DenseBlock, self).__init__() self.listLayers = [] for _ in range(num_convs): self.listLayers.append(ConvBlock(num_channels))
def call(self, x): for layer in self.listLayers.layers: x = layer(x) return x
blk = DenseBlock(2, 10) X = tf.random.uniform((4, 8, 8, 3)) Y = blk(X) Y.shape
|
过渡层
由于每个稠密块都会带来通道数的增加,使用过多则会过于复杂化模型。而过渡层可以用来控制模型复杂度。它通过卷积层来减小通道数,并使用步幅为2
的平均汇聚层减半高和宽,从而进一步降低模型复杂度。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| class TransitionBlock(tf.keras.layers.Layer): def __init__(self, num_channels, **kwargs): super(TransitionBlock, self).__init__(**kwargs) self.batch_norm = tf.keras.layers.BatchNormalization() self.relu = tf.keras.layers.ReLU() self.conv = tf.keras.layers.Conv2D(num_channels, kernel_size=1) self.avg_pool = tf.keras.layers.AvgPool2D(pool_size=2, strides=2)
def call(self, x): x = self.batch_norm(x) x = self.relu(x) x = self.conv(x) return self.avg_pool(x)
blk = TransitionBlock(10) blk(Y).shape
|
DenseNet模型
我们来构造DenseNet
模型。DenseNet
首先使用同ResNet
一样的单卷积层和最大汇聚层。接下来,类似于ResNet
使用的4
个残差块,DenseNet
使用的是4
个稠密块。与ResNet
类似,我们可以设置每个稠密块使用多少个卷积层。这里我们设成4
,从而与ResNet-18
保持一致。稠密块里的卷积层通道数(即增长率)设为32
,所以每个稠密块将增加128
个通道。在每个模块之间,ResNet
通过步幅为2
的残差块减小高和宽,DenseNet
则使用过渡层来减半高和宽,并减半通道数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| def block_1(): return tf.keras.Sequential([ tf.keras.layers.Conv2D(64, kernel_size=7, strides=2, padding='same'), tf.keras.layers.BatchNormalization(), tf.keras.layers.ReLU(), tf.keras.layers.MaxPool2D(pool_size=3, strides=2, padding='same')])
def block_2(): net = block_1() num_channels, growth_rate = 64, 32 num_convs_in_dense_blocks = [4, 4, 4, 4]
for i, num_convs in enumerate(num_convs_in_dense_blocks): net.add(DenseBlock(num_convs, growth_rate)) num_channels += num_convs * growth_rate if i != len(num_convs_in_dense_blocks) - 1: num_channels //= 2 net.add(TransitionBlock(num_channels)) return net
def net(): net = block_2() net.add(tf.keras.layers.BatchNormalization()) net.add(tf.keras.layers.ReLU()) net.add(tf.keras.layers.GlobalAvgPool2D()) net.add(tf.keras.layers.Flatten()) net.add(tf.keras.layers.Dense(10)) return net
|
训练模型
由于这里使用了比较深的网络,我们将输入高和宽从224
降到96
来简化计算。
总结
在跨层连接上,不同于ResNet
中将输入与输出相加,稠密连接网络(DenseNet
)在通道维上连结输入与输出。DenseNet
的主要构建模块是稠密块和过渡层。在构建DenseNet
时,我们需要通过添加过渡层来控制网络的维数,从而再次减少通道的数量。