赞
踩
背景介绍:MNIST数据集识别黑白的手写数字图片,不适合彩色模型的RGB三通道图片。用深度残差网络学习多通道图片。
简单介绍一下深度残差网络:普通的深度网络随着网络深度的加深,拟合效果可能会越来越好,也可能会变差,换句话说在不停地学习,但是有可能学歪了。本次介绍的深度残差网络最后输出H(x)=x+f(x)。其中x是本层网络的输入,f(x)是本层网络的输出,H(x)是最终得到的结果。由以上公式可以表明,最终结果包含输入x,也就是说不论怎么学习,起码效果不会变差,不会学歪。x和f(x)之间的变换的网络层就被成为残差模块。
有不懂的地方可以看代码下面的解释与讲解
目录
- # 残差模块
- def __init__(self, filter_num, stride=1):
- super(BasicBlock, self).__init__()
- #输入x经过两个卷积层得到f(x),f(x)+x=H(x),对应元素相加得到残差模块H(x)
- # 第一个卷积单元 卷积核大小3*3是超参数,需要学习,自己制定
- self.conv1 = layers.Conv2D(filter_num, (3, 3), strides=stride, padding='same')
- self.bn1 = layers.BatchNormalization()
- self.relu = layers.Activation('relu')
- # 第二个卷积单元
- self.conv2 = layers.Conv2D(filter_num, (3, 3), strides=1, padding='same')
- self.bn2 = layers.BatchNormalization()
- #当x与f(x)形状不同的时候,无法进行相加,新建identity(x)卷积层,完成x的形状转换
- if stride != 1:# 步长不为1,需要通过1x1卷积完成shape匹配
- self.downsample = Sequential()
- self.downsample.add(layers.Conv2D(filter_num, (1, 1), strides=stride))
- else:# shape匹配,直接短接
- self.downsample = lambda x:x

再重复一遍残差模块是x和f(x)之间的网络变换,包括两个卷积单元。
第一个卷积单元:卷积核的数量由传入参数给定,使用3*3卷积核,步长设定为1,经过卷积变换后形状不变;经过BN层主要对参数进行标准化,对网络有益;最后经过激活层。第二个卷积单元同上,不过不需要激活函数了。
我们上面说过经过残差模块输出f(x)需要与x相加,因此需要保证二者形状相同。如果shape不相同:用1*1的卷积核对矩阵通道数进行变换。在此细说一下x与f(x)的形状:由于padding都是same因此矩阵形状是保持不变的,但是由于卷积层有多个卷积核,则导致最终的矩阵通道维数和卷积核数量一样。因此f(x)和x仅仅在通道维度上不同,则使用1*1卷积核变换。如果shape相同:直接拼接就行。
- def call(self, inputs, training=None):
- #向前传播
- # [b, h, w, c],通过第一个卷积单元
- out = self.conv1(inputs)
- out = self.bn1(out)
- out = self.relu(out)
- # 通过第二个卷积单元
- out = self.conv2(out)
- out = self.bn2(out)
- # 通过identity模块,进行identity转换
- identity = self.downsample(inputs)
- # 2条路径输出直接相加;out-f(x),identity-x,实现f(x)+x
- output = layers.add([out, identity])
- output = tf.nn.relu(output) # 激活函数
-
- return output
'运行
原始输入x经过两个卷积单元一层一层输出。注意identity模块,需要进行通道数调整,因此输入不是上一个输出,而是原始输入x,要将x的shape修改为f(x)的shape,进行相加。另外注意第二个卷积单元的relu函数从残差模块中提取出来了,放在了H(x)后面,当然这个是不固定的,也可以放在第二个卷积单元内部。
-
- def __init__(self, layer_dims, num_classes=10): # [2, 2, 2, 2]
- super(ResNet, self).__init__()
- # 根网络,预处理 在这个容器中经过卷积层,标准化层,激活函数,池化层减半
- self.stem = Sequential([layers.Conv2D(64, (3, 3), strides=(1, 1)),
- layers.BatchNormalization(),
- layers.Activation('relu'),
- layers.MaxPool2D(pool_size=(2, 2), strides=(1, 1), padding='same')
- ])
- # 堆叠4个Block,每个block包含了多个BasicBlock,设置步长不一样
- self.layer1 = self.build_resblock(64, layer_dims[0])
- self.layer2 = self.build_resblock(128, layer_dims[1], stride=2)
- self.layer3 = self.build_resblock(256, layer_dims[2], stride=2)
- self.layer4 = self.build_resblock(512, layer_dims[3], stride=2)
-
- # 通过Pooling层将高宽降低为1x1
- self.avgpool = layers.GlobalAveragePooling2D()
- # 最后连接一个全连接层分类
- self.fc = layers.Dense(num_classes)
'运行
先经过一个容器,不是残差网络的根网络:包括卷积层-BN层-激活层-池化层。
下面是四个残差层,每个残差层都利用build_resblock函数进行残差网络层的建立,并传入参数。
通过池化层完成高宽的转化,最后连接一个全连接层,转化为十个属性的输出,判断结果到底是什么。
- #通过该函数一次完成多个残差模块的建立
- def build_resblock(self, filter_num, blocks, stride=1):
- # 辅助函数,堆叠filter_num个BasicBlock
- res_blocks = Sequential()
- # 只有第一个BasicBlock的步长可能不为1,实现下采样
- res_blocks.add(BasicBlock(filter_num, stride))
-
- for _ in range(1, blocks):#其他BasicBlock步长都为1
- res_blocks.add(BasicBlock(filter_num, stride=1))
-
- return res_blocks
调用残差模块类,传入卷积核数量,步长。blocks表示要建立的残差模块的数量。这些参数都由调用该方法的代码传入。
该方法的调用完成表明残差神经网络构建完成。
- def call(self, inputs, training=None):
- # 通过根网络
- x = self.stem(inputs)
- # 一次通过4个模块
- x = self.layer1(x)
- x = self.layer2(x)
- x = self.layer3(x)
- x = self.layer4(x)
-
- # 通过池化层
- x = self.avgpool(x)
- # 通过全连接层
- x = self.fc(x)
-
- return x
'运行
这一部分注意区别于残差模块中的向前传播,先通过根网络,再逐步通过每一个残差模块,最后经过池化层和全连接层得到输出。
- def resnet18():
- # 通过调整模块内部BasicBlock的数量和配置实现不同的ResNet
- return ResNet([2, 2, 2, 2])
-
-
- def resnet34():
- # 通过调整模块内部BasicBlock的数量和配置实现不同的ResNet
- return ResNet([3, 4, 6, 3])
'运行
ResNet18直接调用产生网络的方法,传入参数[2,2,2,2],表明layer_dims是个一维矩阵,元素都是2。ResNet18是指17层卷积层,1层全连接层的网络。每个layer都调用两次建立残差模块的方法,每个残差模块有两个卷积单元也就是两个卷积层,如此一来四个layer,就有16个卷积层,再加上根网络的一个卷积层和最后的一个全连接层刚好是17+1。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。