TF2.0基于fashion数据集的DenseNet神经网络搭建
1、DenseNet简介
DenseNet在2017提出,论文链接:https://arxiv.org/pdf/1608.06993.pdf 。文章提出的DenseNet(Dense Convolutional Network)主要还是和ResNet及Inception网络做对比,思想上有借鉴,但却是全新的结构,网络结构并不复杂,却非常有效!众所周知,最近一两年卷积神经网络提高效果的方向,要么深(比如ResNet,解决了网络深时候的梯度消失问题)要么宽(比如GoogleNet的Inception),而作者则是从feature入手,通过对feature的极致利用达到更好的效果和更少的参数。
1.1 dense block的结构
DenseNet的精髓在于dense block的结构图。在传统的卷积神经网络中,如果你有L层,那么就会有L个连接,但是在DenseNet中,会有L(L+1)/2个连接。简单讲,就是每一层的输入来自前面所有层的输出。如下图:x0是input,H1的输入是x0(input),H2的输入是x0和x1(x1是H1的输出)……
回忆一下resnet中的残差网络是加上跳转链接,非线性映射关系:
X(t)=H(X(t-1))+X(t-1)
ResNet的一个优点是,梯度可以直接通过恒等函数从后面的层流向前面的层。但是,恒等函数和H的输出是累加的,这可能会阻碍网络中的信息流。
为了改善不同层之间信息流的的问题,DenseNet采用直接将所有输入连接到输出层。
与残差网络不同的是不用相加而是连接,输入直接传入到输出如图1,非线性映射关系:
X(t)=H([X0,X1,X2,…,X(t-1)])
1.2 DenseNet结构
了解了 dense block的结构,就可以看到DenseNet的一个优点是网络更窄,参数更少,很大一部分原因得益于这种dense block的设计,后面有提到在dense block中每个卷积层的输出feature map的数量都很小(小于100),而不是像其他网络一样动不动就几百上千的宽度。同时这种连接方式使得特征和梯度的传递更加有效,网络也就更加容易训练。原文的一句话非常喜欢:Each layer has direct access to the gradients from the loss function and the original input signal, leading to an implicit deep supervision.直接解释了为什么这个网络的效果会很好。前面提到过梯度消失问题在网络深度越深的时候越容易出现,原因就是输入信息和梯度信息在很多层之间传递导致的,而现在这种dense connection相当于每一层都直接连接input和loss,因此就可以减轻梯度消失现象,这样更深网络不是问题。另外作者还观察到这种dense connection有正则化的效果,因此对于过拟合有一定的抑制作用。
DenseNet结构图如下:
上图表示的是一个DenseNet的结构图,在这个结构图中包含了3个dense block。作者将DenseNet分成多个dense block,原因是希望各个dense block内的feature map的size统一,这样在做concatenation就不会有size的问题。
根据结构图可以得到网络图如下:
这个表中的k=32,k=48中的k是growth rate,表示每个dense block中每层输出的feature map个数。为了避免网络变得很宽,作者都是采用较小的k,比如32这样,作者的实验也表明小的k可以有更好的效果。根据dense block的设计,后面几层可以得到前面所有层的输入,因此concat后的输入channel还是比较大的。另外这里每个dense block的33卷积前面都包含了一个11的卷积操作,就是所谓的bottleneck layer,目的是减少输入的feature map数量,既能降维减少计算量,又能融合各个通道的特征。另外作者为了进一步压缩参数,在每两个dense block之间又增加了11的卷积操作。因此在后面的实验对比中,如果你看到DenseNet-C这个网络,表示增加了这个Translation layer,该层的11卷积的输出channel默认是输入channel到一半。如果你看到DenseNet-BC这个网络,表示既有bottleneck layer,又有Translation layer。
2、使用tensorflow2.0实现DenseNet网络并训练Fashion-mnist数据集
关于Fashion-mnist数据集的简介请参考前一篇文章《深度学习分类任务(1):TF2.0基于fashion数据集的LeNet神经网络搭建》
2.1 准备数据集
同样,这部分和之前的一样,没有改变过。
import tensorflow as tf
import os
import numpy as np
from matplotlib import pyplot as plt
from tensorflow.keras.layers import Conv2D, BatchNormalization, Activation, MaxPool2D, Dropout, Flatten, Dense
from tensorflow.keras import Model
np.set_printoptions(threshold=np.inf)
fashion = tf.keras.datasets.fashion_mnist
(x_train, y_train), (x_test, y_test) = fashion.load_data()
x_train, x_test = x_train / 255.0, x_test / 255.0
print("x_train.shape", x_train.shape)
x_train = x_train.reshape(x_train.shape[0], 28, 28, 1) # 给数据增加一个维度,使数据和网络结构匹配
x_test = x_test.reshape(x_test.shape[0], 28, 28, 1)
print("x_train.shape", x_train.shape)
2.2 搭建resnet网络
首先封装BottleNeck,即瓶颈层,相当于每一个稠密块中若干个相同的H函数。
class BottleNeck(tf.keras.layers.Layer):
# growth_rate对应的是论文中的增长率k,指经过一个BottleNet输出的特征图的通道数;drop_rate指失活率。
def __init__(self, growth_rate, drop_rate):
super(BottleNeck, self).__init__()
self.bn1 = tf.keras.layers.BatchNormalization()
self.conv1 = tf.keras.layers.Conv2D(filters=4 * growth_rate, # 使用1*1卷积核将通道数降维到4*k
kernel_size=(1, 1),
strides=1,
padding="same")
self.bn2 = tf.keras.layers.BatchNormalization()
self.conv2 = tf.keras.layers.Conv2D(filters=growth_rate, # 使用3*3卷积核,使得输出维度(通道数)为k
kernel_size=(3, 3),
strides=1,
padding="same")
self.dropout = tf.keras.layers.Dropout(rate=drop_rate)
# 将网络层存入一个列表中
self.listLayers = [self.bn1,
tf.keras.layers.Activation("relu"),
self.conv1,
self.bn2,
tf.keras.layers.Activation("relu"),
self.conv2,
self.dropout]
def call(self, x):
y = x
for layer in self.listLayers.layers:
y = layer(y)
# 每经过一个BottleNet,将输入和输出按通道连结。作用是:将前l层的输入连结起来,作为下一个BottleNet的输入。
y = tf.keras.layers.concatenate([x, y], axis=-1)
return y
然后构建最主要的模块——稠密块,由若干个相同的瓶颈层构成。
class DenseBlock(tf.keras.layers.Layer):
# num_layers表示该稠密块存在BottleNet的个数,也就是一个稠密块的层数L
def __init__(self, num_layers, growth_rate, drop_rate=0.5):
super(DenseBlock, self).__init__()
self.num_layers = num_layers
self.growth_rate = growth_rate
self.drop_rate = drop_rate
self.listLayers = []
# 一个DenseBlock由多个相同的BottleNeck构成,我们将它们放入一个列表中。
for _ in range(num_layers):
self.listLayers.append(BottleNeck(growth_rate=self.growth_rate, drop_rate=self.drop_rate))
def call(self, x):
for layer in self.listLayers.layers:
x = layer(x)
return x
再构建过渡层
class TransitionLayer(tf.keras.layers.Layer):
# out_channels代表输出通道数
def __init__(self, out_channels):
super(TransitionLayer, self).__init__()
self.bn = tf.keras.layers.BatchNormalization()
self.conv = tf.keras.layers.Conv2D(filters=out_channels,
kernel_size=(1, 1),
strides=1,
padding="same")
self.pool = tf.keras.layers.MaxPool2D(pool_size=(2, 2), # 2倍下采样
strides=2,
padding="same")
def call(self, inputs):
x = self.bn(inputs)
x = tf.keras.activations.relu(x)
x = self.conv(x)
x = self.pool(x)
return x
现在所有子模块已构建完毕,可以搭建完整的DenseNet网络。
class DenseNet(tf.keras.Model):
# num_init_features:代表初始的通道数,即输入稠密块时的通道数
# growth_rate:对应的是论文中的增长率k,指经过一个BottleNet输出的特征图的通道数
# block_layers:每个稠密块中的BottleNet的个数
# compression_rate:压缩因子,其值在(0,1]范围内
# drop_rate:失活率
def __init__(self, num_init_features, growth_rate, block_layers, compression_rate, drop_rate):
super(DenseNet, self).__init__()
# 第一层,7*7的卷积层,2倍下采样。
self.conv = tf.keras.layers.Conv2D(filters=num_init_features,
kernel_size=(7, 7),
strides=2,
padding="same")
self.bn = tf.keras.layers.BatchNormalization()
# 最大池化层,3*3卷积核,2倍下采样
self.pool = tf.keras.layers.MaxPool2D(pool_size=(3, 3), strides=2, padding="same")
# 稠密块 Dense Block(1)
self.num_channels = num_init_features
self.dense_block_1 = DenseBlock(num_layers=block_layers[0], growth_rate=growth_rate, drop_rate=drop_rate)
# 该稠密块总的输出的通道数
self.num_channels += growth_rate * block_layers[0]
# 对特征图的通道数进行压缩
self.num_channels = compression_rate * self.num_channels
# 过渡层1,过渡层进行下采样
self.transition_1 = TransitionLayer(out_channels=int(self.num_channels))
# 稠密块 Dense Block(2)
self.dense_block_2 = DenseBlock(num_layers=block_layers[1], growth_rate=growth_rate, drop_rate=drop_rate)
self.num_channels += growth_rate * block_layers[1]
self.num_channels = compression_rate * self.num_channels
# 过渡层2,2倍下采样,输出:14*14
self.transition_2 = TransitionLayer(out_channels=int(self.num_channels))
# 稠密块 Dense Block(3)
self.dense_block_3 = DenseBlock(num_layers=block_layers[2], growth_rate=growth_rate, drop_rate=drop_rate)
self.num_channels += growth_rate * block_layers[2]
self.num_channels = compression_rate * self.num_channels
# 过渡层3,2倍下采样
self.transition_3 = TransitionLayer(out_channels=int(self.num_channels))
# 稠密块 Dense Block(4)
self.dense_block_4 = DenseBlock(num_layers=block_layers[3], growth_rate=growth_rate, drop_rate=drop_rate)
# 全局平均池化,输出size:1*1
self.avgpool = tf.keras.layers.GlobalAveragePooling2D()
# 全连接层,进行10分类
self.fc = tf.keras.layers.Dense(units=10, activation=tf.keras.activations.softmax)
def call(self, inputs):
x = self.conv(inputs)
x = self.bn(x)
x = tf.keras.activations.relu(x)
x = self.pool(x)
x = self.dense_block_1(x)
x = self.transition_1(x)
x = self.dense_block_2(x)
x = self.transition_2(x)
x = self.dense_block_3(x)
x = self.transition_3(x, )
x = self.dense_block_4(x)
x = self.avgpool(x)
x = self.fc(x)
return x
2.3装配网络
model.compile(loss='sparse_categorical_crossentropy',
optimizer=tf.keras.optimizers.SGD(),
metrics=['accuracy'])
2.4 设置权重文件保存路径和callback
checkpoint_save_path = "./checkpoint/ResNet18.ckpt"
if os.path.exists(checkpoint_save_path + '.index'):
print('-------------load the model-----------------')
model.load_weights(checkpoint_save_path)
cp_callback = tf.keras.callbacks.ModelCheckpoint(filepath=checkpoint_save_path,
save_weights_only=True,
save_best_only=True)
2.5 开始训练
history = model.fit(x_train, y_train,
batch_size=64,
epochs=5,
validation_split=0.2)
model.summary()
2.6 显示训练集和验证集的acc和loss曲线
plt.plot(history.history['accuracy'])
plt.plot(history.history['val_accuracy'])
plt.legend(['training', 'validation'], loc='upper left')
plt.show()