前言
在残差网络的文章中,我们知道残差网格,能够应用在特别深的网络中的一个重要原因是,无论正向计算精度还是反向计算梯度,信息都能毫无损失的从一层传到另一层。如果我们的目的是保证信息毫无阻碍的传播,那么残差网络的stacking残差块的设计便不是信息流通最合适的结构。
基于信息流通的原理,一个最简单的思想便是在网络中的每个卷积操作中,将其低层的所有特征作为该网络的输入,也就是在一个层数为L的网络中加入L ( L + 1 ) 2 \frac{L(L+1)}{2} 2 L ( L + 1 ) 个short-cut, 如图1。为了更好的保存低层网络的特征,DenseNet 使用的是将不同层的输出拼接在一起,而在残差网络中使用的是单位加操作。以上便是DenseNet算法的动机。
图1:DenseNet中一个Dense Block的设计
1. DenseNet算法解析及源码实现
在DenseNet中,如果全部采用图1的结构的话,第L层的输入是之前所有的Feature Map拼接到一起。考虑到现今内存/显存空间的问题,该方法显然是无法应用到网络比较深的模型中的,故而DenseNet采用了图2所示的堆积Dense Block的形式,下面我们针对图2详细解析DenseNet算法。
图2:DenseNet网络结构
1.1 Dense Block
图1便是一个Dense Block,在Dense Block中,第l l l 层的输入x l x_l x l 是这个块中前面所有层的输出:
x l = [ y 0 , y 1 , . . . , y l − 1 ] x_l = [y_0, y_1, ..., y_{l-1}] x l = [ y 0 , y 1 , ... , y l − 1 ]
y l = H l ( x l ) y_l = H_l(x_l) y l = H l ( x l )
其中,中括号[ y 0 , y 1 , . . . , y l − 1 ] [y_0, y_1, ..., y_{l-1}] [ y 0 , y 1 , ... , y l − 1 ] 表示拼接操作,即按照Feature Map将l − 1 l-1 l − 1 个输入拼接成一个Tensor。H l ( ⋅ ) H_l(\cdot) H l ( ⋅ ) 表示合成函数(Composite function)。在实现时,我使用了stored_features存储每个合成函数的输出。
复制 def dense_block(x, depth=5, growth_rate = 3):
nb_input_feature_map = x.shape[3].value
stored_features = x
for i in range(depth):
feature = composite_function(stored_features, growth_rate = growth_rate)
stored_features = concatenate([stored_features, feature], axis=3)
return stored_features
1.2 合成函数(Composite function)
合成函数位于Dense Block的每一个节点中,其输入是拼接在一起的Feature Map, 输出则是这些特征经过BN->ReLU->3*3
卷积的三步得到的结果,其中卷积的Feature Map的数量是成长率(Growth Rate)。在DenseNet中,成长率k一般是个比较小的整数,在论文中,k = 12 k=12 k = 12 。但是拼接在一起的Feature Map的数量一般比较大,为了提高网络的计算性能,DenseNet先使用了1 × 1 1\times1 1 × 1 卷积将输入数据降维到4 k 4k 4 k ,再使用3 × 3 3\times3 3 × 3 卷积提取特征,作者将这一过程标准化为BN->ReLU->1*1卷积->BN->ReLU->3*3卷积
,这种结构定义为DenseNetB。
复制 def composite_function(x, growth_rate):
if DenseNetB: #Add 1*1 convolution when using DenseNet B
x = BatchNormalization()(x)
x = Activation('relu')(x)
x = Conv2D(kernel_size=(1,1), strides=1, filters = 4 * growth_rate, padding='same')(x)
x = BatchNormalization()(x)
x = Activation('relu')(x)
output = Conv2D(kernel_size=(3,3), strides=1, filters = growth_rate, padding='same')(x)
return output
1.3 成长率(Growth Rate)
成长率k k k 是DenseNet的一个超参数,反应的是Dense Block中每个节点的输入数据的增长速度。在Dense Block中,每个节点的输出均是一个k k k 维的特征向量。假设整个Dense Block的输入数据是k 0 k_0 k 0 维的,那么第l l l 个节点的输入便是k 0 + k × ( l − 1 ) k_0 + k\times(l-1) k 0 + k × ( l − 1 ) 。作者通过实验验证,k k k 一般取一个比较小的值,作者通过实验将k k k 设置为12。
1.4 Compression
至此,DenseNet的Dense Block已经介绍完毕,在图2中,Dense Block之间的结构叫做压缩层(Compression Layer)。压缩层有降维和降采样两个作用。假设Dense Block的输出是m m m 维的特征向量,那么下一个Dense Block的输入是⌊ θ m ⌋ \lfloor \theta m \rfloor ⌊ θ m ⌋ ,其中θ \theta θ 是压缩因子(Compression Factor),用户自行设置的超参数。当θ \theta θ 等于1时,Dense Block的输入和输出的维度相同,当θ < 1 \theta < 1 θ < 1 时,网络叫做DenseNet-C,在论文中,θ = 0.5 \theta=0.5 θ = 0.5 。包含瓶颈层和压缩层的DenseNet叫做DenseNet-BC。Pooling层使用的是2 × 2 2\times2 2 × 2 的Average Pooling层。
下面Demo是在MNIST数据集上的DenseNet代码,完整代码见:https://github.com/senliuy/CNN-Structures/blob/master/DenseNet.ipynb
复制 def dense_net(input_image, nb_blocks = 2):
x = Conv2D(kernel_size=(3,3), filters=8, strides=1, padding='same', activation='relu')(input_image)
for block in range(nb_blocks):
x = dense_block(x, depth=NB_DEPTH, growth_rate = GROWTH_RATE)
if not block == nb_blocks-1:
if DenseNetC:
theta = COMPRESSION_FACTOR
nb_transition_filter = int(x.shape[3].value * theta)
x = Conv2D(kernel_size=(1,1), filters=nb_transition_filter, strides=1, padding='same', activation='relu')(x)
x = AveragePooling2D(pool_size=(2,2), strides=2)(x)
x = Flatten()(x)
x = Dense(100, activation='relu')(x)
outputs = Dense(10, activation='softmax', kernel_initializer='he_normal')(x)
return outputs
2. 分析:
DenseNet具有如下优点:
由于DenseNet需要在内存中保存Dense Block的每个节点的输出,此时需要极大的显存才能支持较大规模的DenseNet,这也导致了现在工业界主流的算法依旧是残差网络。