ResNet学习目标
- 什么是ResNet
- 为什么要引入ResNet?
- ResNet网络结构的特点
- 利用ResNet完成图像分类
ResNet原理:
残差学习思想
传统的深度神经网络在前向传播过程中,每一层试图直接学习一个复杂的非线性映射(H(x)),随着网络深度加深,这种映射可能变得非常复杂且难以优化。ResNet提出了一个假设:对于深层网络,与其让每一层学习一个全局映射,不如让它学习输入与期望输出之间的残差(Residual),即 F(x) = H(x) - x。这样,网络实际上只需要学习一个更简单的映射 F(x),使得叠加了输入 x 后(F(x) + x),得到的就是原始的期望映射 H(x)。
残差块结构
基于上述思想,ResNet构建了残差块作为其基本构建单元。一个典型的残差块通常包含两个或更多卷积层,它们之间可能还包括批量归一化(Batch Normalization)和激活函数(如ReLU)。关键在于,残差块内部引入了一条“捷径(Shortcut Connection)”或“跳跃连接(Skip Connection)”,该连接直接将输入传递到残差块的输出端,与经过内部卷积层处理后的特征图相加。这种结构可以直观地表达为:
优势与效果
梯度传播
- 缓解梯度消失/爆炸:捷径连接提供了从输入到输出的直接路径,使得梯度可以通过这条路径无阻碍地回传,避免了深度网络中常见的梯度消失或爆炸问题,从而使得深层网络能够更容易地训练。
优化难度
- 简化学习目标:通过残差学习,网络不必一次性学习复杂的全局映射,而是学习一个相对简单的残差映射。这使得网络更容易收敛,并可能达到更高的准确率。
深度可扩展性
- 避免退化问题:在ResNet之前,增加网络深度往往导致训练集和测试集的准确率饱和甚至下降(即所谓的“退化”现象)。残差块的设计使得网络深度可以大幅度增加(如ResNet-152有152层),而不会出现明显的性能退化,反而能持续提升模型的表示能力。
特征重用
- 保持浅层特征:捷径连接使得浅层特征可以直接传递到深层,深层网络可以同时利用浅层的粗糙特征和深层的精细特征,有利于多尺度特征融合。
总结
ResNet通过引入残差块及其内部的捷径连接,革新了深度神经网络的设计理念,成功解决了深度学习中长期存在的深度瓶颈问题。这种架构不仅显著提高了网络的训练效率和性能,还为构建极深的卷积神经网络奠定了基础,对计算机视觉乃至整个深度学习领域产生了深远影响。自其提出以来,ResNet已成为众多视觉任务的标准模型之一,并启发了一系列基于残差或类似短路连接的深度学习架构设计。
ResNet最重要的作用
ResNet(Residual Network)最重要的作用在于它成功地解决了深度神经网络中随着网络深度增加所出现的梯度消失和梯度爆炸问题,从而使得能够训练出非常深的神经网络模型,并保持甚至提升了网络的性能与学习效率。具体而言,ResNet的重要作用体现在以下几个方面:
-
克服深度瓶颈:在ResNet之前,尽管深度对于神经网络的表征能力至关重要,但随着网络层数增加,梯度传播过程中会出现严重衰减或剧烈波动,导致训练困难和性能退化。ResNet通过引入残差块(Residual Block)的设计,允许梯度直接通过恒等映射(shortcut connections)回传至较早层,有效地缓解了深度带来的梯度消失问题,使得网络深度得以大幅度增加(如达到150层甚至更多)而不至于性能下降。
-
提升网络表达能力:深层的ResNet能够捕捉到更丰富、更复杂的图像特征层次结构,从低级边缘、纹理特征到高级语义概念,都能得到有效的学习和建模。这种强大的表征能力使得ResNet在各类计算机视觉任务中展现出卓越的性能,不仅在图像分类任务(如ImageNet竞赛)中取得显著成果,还广泛应用于对象检测、语义分割、人脸识别、动作识别等多个领域,显著提高了这些任务的准确性和泛化能力。
-
加速训练与优化:由于残差结构使得网络更容易优化,ResNet不仅能够训练更深,而且通常比同等深度的普通(plain)网络收敛速度更快。残差块使得网络能够专注于学习输入与输出之间的残差(residual)而非完整的映射关系,这往往是一个更简单的学习任务,降低了网络的学习难度。
-
推动深度学习研究与应用发展:ResNet的成功不仅在于其技术本身的创新性,还在于它对深度学习领域产生了深远的影响。ResNet的设计理念和残差学习机制启发了后续一系列深度神经网络架构的发展,如DenseNet、ResNeXt、SENet等,这些变体网络在ResNet的基础上进一步改进了网络结构和信息流动方式,共同推动了深度学习模型在复杂视觉任务中的性能边界。
综上,ResNet最重要的作用在于通过创新的残差学习架构,从根本上解决了深度神经网络训练中的梯度难题,开启了深度神经网络的“深度革命”,显著提升了网络的表达能力和学习效率,推动了计算机视觉乃至整个深度学习领域的技术进步与广泛应用。
什么是ResNet?
ResNet(Residual Network)是一种深度残差网络,由何凯明等人在2015年提出,是深度学习领域中一项突破性的进展,尤其在计算机视觉任务中表现突出。ResNet 解决了随着神经网络加深而导致的梯度消失和训练困难的问题,这一问题在之前阻碍了构建更深的网络模型以提高模型性能。
ResNet 的核心创新是引入了残差连接(Residual Connections)的概念。在传统的网络结构中,每一层网络试图直接学习输入到输出的复杂映射。而在ResNet中,每一层并不直接学习原始输入到输出的映射,而是学习输入与前面某一层输出之间的残差(或称“shortcut”)。也就是说,一个残差块(Residual Block)包含至少两层卷积层,其输出不是直接提供给下一层,而是与原始输入相加,这样网络可以更容易地学习深层次网络中微小的残差变化。
具体来说,ResNet 的基本结构单元如下:
H(x) = F(x, {W_i}) + x
这里,F(x, {W_i})
表示多层卷积层组成的残差函数,x
是输入特征图,H(x)
是该残差块的输出。通过引入残差连接,即使深层网络的输出只是恒等映射(Identity Mapping),网络也能轻易地学习到这一点,避免了梯度消失问题,使得训练数百乃至上千层的网络成为可能。
ResNet 不仅在图像分类任务上取得了出色的效果,还在许多其他视觉任务,如目标检测、图像分割等上展示了优越性,并在当年的ImageNet大规模视觉识别挑战赛中获得了优异的成绩。此外,ResNet 的设计理念对后来的深度学习模型架构产生了深远影响,成为了现代深度学习模型设计的基础组成部分。
为什么要引入ResNet?
网络越深,获取的信息就越多,特征也越丰富。但是在实践中,随着网络的加深,优化效果反而越差,测试数据和训练数据的准确率反而降低了
针对这一问题,何恺明等人提出了残差网络(ResNet)在2015年的ImageNet图像识别挑战赛夺魁,并深刻影响了后来的深度神经网络的设计。
ResNet网络结构的特点
1 残差块
假设 F(x) 代表某个只包含有两层的映射函数, x 是输入, F(x)是输出。假设他们具有相同的维度。在训练的过程中我们希望能够通过修改网络中的 w和b去拟合一个理想的 H(x)(从输入到输出的一个理想的映射函数)。也就是我们的目标是修改F(x) 中的 w和b逼近 H(x) 。如果我们改变思路,用F(x) 来逼近 H(x)-x ,那么我们最终得到的输出就变为 F(x)+x(这里的加指的是对应位置上的元素相加,也就是element-wise addition),这里将直接从输入连接到输出的结构也称为shortcut,那整个结构就是残差块,ResNet的基础模块。
左侧是简单的堆叠层("Plain" Layers),右侧是残差块(Residual Block)。
在左边,“Plain” Layers 是指那些没有特殊设计、简单地将一层接一层的网络层堆叠在一起的架构。顶部的圆圈代表输入信号 X,然后通过两个连续的卷积层(Conv),每个卷积层后面跟着一个 ReLU 激活函数。最后得到的输出为 H(X)。
在右边,展示了一个残差块(Residual Block)。残差块是深度神经网络中的一个重要概念,尤其是对于非常深的网络。它的核心思想是在每一层之间添加一个恒等映射(identity mapping),即让信号可以直接跨过一些层,从而更容易优化深层网络。
在这个残差块中,输入信号 X 先通过一个卷积层(Conv),然后经过一个 ReLU 激活函数,再通过第二个卷积层(Conv),最后再经过一个 ReLU 激活函数。但是,在第一个卷积层之后,还有一个分支直接连接到了第二个卷积层之后,这就形成了一个短路(shortcut),使得输入信号可以直接到达最终的 ReLU 激活函数。这样做的目的是确保即使在网络很深的时候,信息也可以顺利传递下去。
整个残差块的计算过程可以用公式 F(X)+X 来表示,其中 F(X) 表示通过卷积层和 ReLU 的计算部分,而 +X 则表示将这个计算结果与输入信号 X 相加。这就是为什么这种架构被称为“残差块”,因为每一块都试图学习一个残差函数 F(X),而不是直接学习目标函数本身。
ResNet沿用了VGG全3×33×3卷积层的设计。残差块里首先有2个有相同输出通道数的3×33×3卷积层。每个卷积层后接BN层和ReLU激活函数,然后将输入直接加在最后的ReLU激活函数前,这种结构用于层数较少的神经网络中,比如ResNet34。若输入通道数比较多,就需要引入1×11×1卷积层来调整输入的通道数,这种结构也叫作瓶颈模块,通常用于网络层数较多的结构中。如下图所示:
上图左中的残差块的实现如下,可以设定输出通道数,是否使用1*1的卷积及卷积层的步幅。
- 输入(X)首先通过一个1x1卷积层(Convolutional Layer)。这个操作通常用于减少输入通道的数量或增加输出通道的数量。
- 然后,经过卷积后的特征映射会通过批量归一化(Batch Normalization),这一步骤有助于标准化数据并提高模型训练的速度和稳定性。
- 接着,应用ReLU激活函数(Rectified Linear Unit),这是常用的非线性激活函数之一,用于引入非线性特性到神经网络中。
- 再次,将结果通过另一个权重(weight)和批量归一化(BN)层,这可能表示该层具有多个卷积核或者是一个深度可分离卷积(Depthwise Separable Convolution)的一部分。
- 最后,将上述处理过的特征映射与原始输入(X)相加,并再次应用ReLU激活函数。
这种结构在深度学习中很常见,特别是在卷积神经网络(CNNs)中,它可以用来构建复杂的特征提取器。通过重复使用这些基本块,可以创建出多层的深层网络,以解决各种计算机视觉任务。
# 导入相关的工具包
import tensorflow as tf
from tensorflow.keras import layers, activations
# 定义ResNet的残差块
class Residual(tf.keras.Model):
# 指明残差块的通道数,是否使用1*1卷积,步长
def __init__(self, num_channels, use_1x1conv=False, strides=1):
super(Residual, self).__init__()
# 卷积层:指明卷积核个数,padding,卷积核大小,步长
self.conv1 = layers.Conv2D(num_channels,
padding='same',
kernel_size=3,
strides=strides)
# 卷积层:指明卷积核个数,padding,卷积核大小,步长
self.conv2 = layers.Conv2D(num_channels, kernel_size=3, padding='same')
if use_1x1conv:
self.conv3 = layers.Conv2D(num_channels,
kernel_size=1,
strides=strides)
else:
self.conv3 = None
# 指明BN层
self.bn1 = layers.BatchNormalization()
self.bn2 = layers.BatchNormalization()
# 定义前向传播过程
def call(self, X):
# 卷积,BN,激活
Y = activations.relu(self.bn1(self.conv1(X)))
# 卷积,BN
Y = self.bn2(self.conv2(Y))
# 对输入数据进行1*1卷积保证通道数相同
if self.conv3:
X = self.conv3(X)
# 返回与输入相加后激活的结果
return activations.relu(Y + X)
1*1卷积用来调整通道数。
2 ResNet模型
ResNet模型的构成如下图所示:
ResNet网络中按照残差块的通道数分为不同的模块。第一个模块前使用了步幅为2的最大池化层,所以无须减小高和宽。之后的每个模块在第一个残差块里将上一个模块的通道数翻倍,并将高和宽减半。
下面我们来实现这些模块。注意,这里对第一个模块做了特别处理。
# ResNet网络中模块的构成
class ResnetBlock(tf.keras.layers.Layer):
# 网络层的定义:输出通道数(卷积核个数),模块中包含的残差块个数,是否为第一个模块
def __init__(self,num_channels, num_residuals, first_block=False):
super(ResnetBlock, self).__init__()
# 模块中的网络层
self.listLayers=[]
# 遍历模块中所有的层
for i in range(num_residuals):
# 若为第一个残差块并且不是第一个模块,则使用1*1卷积,步长为2(目的是减小特征图,并增大通道数)
if i == 0 and not first_block:
self.listLayers.append(Residual(num_channels, use_1x1conv=True, strides=2))
# 否则不使用1*1卷积,步长为1
else:
self.listLayers.append(Residual(num_channels))
# 定义前向传播过程
def call(self, X):
# 所有层依次向前传播即可
for layer in self.listLayers.layers:
X = layer(X)
return X
ResNet的前两层跟之前介绍的GoogLeNet中的一样:在输出通道数为64、步幅为2的7×77×7卷积层后接步幅为2的3×33×3的最大池化层。不同之处在于ResNet每个卷积层后增加了BN层,接着是所有残差模块,最后,与GoogLeNet一样,加入全局平均池化层(GAP)后接上全连接层输出。
# 构建ResNet网络
class ResNet(tf.keras.Model):
# 初始化:指定每个模块中的残差快的个数
def __init__(self,num_blocks):
super(ResNet, self).__init__()
# 输入层:7*7卷积,步长为2
self.conv=layers.Conv2D(64, kernel_size=7, strides=2, padding='same')
# BN层
self.bn=layers.BatchNormalization()
# 激活层
self.relu=layers.Activation('relu')
# 最大池化层
self.mp=layers.MaxPool2D(pool_size=3, strides=2, padding='same')
# 第一个block,通道数为64
self.resnet_block1=ResnetBlock(64,num_blocks[0], first_block=True)
# 第二个block,通道数为128
self.resnet_block2=ResnetBlock(128,num_blocks[1])
# 第三个block,通道数为256
self.resnet_block3=ResnetBlock(256,num_blocks[2])
# 第四个block,通道数为512
self.resnet_block4=ResnetBlock(512,num_blocks[3])
# 全局平均池化
self.gap=layers.GlobalAvgPool2D()
# 全连接层:分类
self.fc=layers.Dense(units=10,activation=tf.keras.activations.softmax)
# 前向传播过程
def call(self, x):
# 卷积
x=self.conv(x)
# BN
x=self.bn(x)
# 激活
x=self.relu(x)
# 最大池化
x=self.mp(x)
# 残差模块
x=self.resnet_block1(x)
x=self.resnet_block2(x)
x=self.resnet_block3(x)
x=self.resnet_block4(x)
# 全局平均池化
x=self.gap(x)
# 全链接层
x=self.fc(x)
return x
# 模型实例化:指定每个block中的残差块个数
mynet=ResNet([2,2,2,2])
这里每个模块里有4个卷积层(不计算 1×1卷积层),加上最开始的卷积层和最后的全连接层,共计18层。这个模型被称为ResNet-18。通过配置不同的通道数和模块里的残差块数可以得到不同的ResNet模型,例如更深的含152层的ResNet-152。虽然ResNet的主体架构跟GoogLeNet的类似,但ResNet结构更简单,修改也更方便。这些因素都导致了ResNet迅速被广泛使用。 在训练ResNet之前,我们来观察一下输入形状在ResNe的架构:
X = tf.random.uniform(shape=(1, 224, 224 , 1))
y = mynet(X)
mynet.summary()
ResNet网络结构全部代码:
# 环境minist/001
# 导入相关的工具包
import tensorflow as tf
from tensorflow.keras import layers, activations
# 定义ResNet的残差块
class Residual(tf.keras.Model):
# 指明残差块的通道数,是否使用1*1卷积,步长
def __init__(self, num_channels, use_1x1conv=False, strides=1):
super(Residual, self).__init__()
# 卷积层:指明卷积核个数,padding,卷积核大小,步长
self.conv1 = layers.Conv2D(num_channels,
padding='same',
kernel_size=3,
strides=strides)
# 卷积层:指明卷积核个数,padding,卷积核大小,步长
self.conv2 = layers.Conv2D(num_channels, kernel_size=3, padding='same')
if use_1x1conv:
self.conv3 = layers.Conv2D(num_channels,
kernel_size=1,
strides=strides)
else:
self.conv3 = None
# 指明BN层
self.bn1 = layers.BatchNormalization()
self.bn2 = layers.BatchNormalization()
# 定义前向传播过程
def call(self, X):
# 卷积,BN,激活
Y = activations.relu(self.bn1(self.conv1(X)))
# 卷积,BN
Y = self.bn2(self.conv2(Y))
# 对输入数据进行1*1卷积保证通道数相同
if self.conv3:
X = self.conv3(X)
# 返回与输入相加后激活的结果
return activations.relu(Y + X)
# ResNet网络中模块的构成
class ResnetBlock(tf.keras.layers.Layer):
# 网络层的定义:输出通道数(卷积核个数),模块中包含的残差块个数,是否为第一个模块
def __init__(self,num_channels, num_residuals, first_block=False):
super(ResnetBlock, self).__init__()
# 模块中的网络层
self.listLayers=[]
# 遍历模块中所有的层
for i in range(num_residuals):
# 若为第一个残差块并且不是第一个模块,则使用1*1卷积,步长为2(目的是减小特征图,并增大通道数)
if i == 0 and not first_block:
self.listLayers.append(Residual(num_channels, use_1x1conv=True, strides=2))
# 否则不使用1*1卷积,步长为1
else:
self.listLayers.append(Residual(num_channels))
# 定义前向传播过程
def call(self, X):
# 所有层依次向前传播即可
for layer in self.listLayers.layers:
X = layer(X)
return X
# 构建ResNet网络
class ResNet(tf.keras.Model):
# 初始化:指定每个模块中的残差快的个数
def __init__(self,num_blocks):
super(ResNet, self).__init__()
# 输入层:7*7卷积,步长为2
self.conv=layers.Conv2D(64, kernel_size=7, strides=2, padding='same')
# BN层
self.bn=layers.BatchNormalization()
# 激活层
self.relu=layers.Activation('relu')
# 最大池化层
self.mp=layers.MaxPool2D(pool_size=3, strides=2, padding='same')
# 第一个block,通道数为64
self.resnet_block1=ResnetBlock(64,num_blocks[0], first_block=True)
# 第二个block,通道数为128
self.resnet_block2=ResnetBlock(128,num_blocks[1])
# 第三个block,通道数为256
self.resnet_block3=ResnetBlock(256,num_blocks[2])
# 第四个block,通道数为512
self.resnet_block4=ResnetBlock(512,num_blocks[3])
# 全局平均池化
self.gap=layers.GlobalAvgPool2D()
# 全连接层:分类
self.fc=layers.Dense(units=10,activation=tf.keras.activations.softmax)
# 前向传播过程
def call(self, x):
# 卷积
x=self.conv(x)
# BN
x=self.bn(x)
# 激活
x=self.relu(x)
# 最大池化
x=self.mp(x)
# 残差模块
x=self.resnet_block1(x)
x=self.resnet_block2(x)
x=self.resnet_block3(x)
x=self.resnet_block4(x)
# 全局平均池化
x=self.gap(x)
# 全链接层
x=self.fc(x)
return x
# 模型实例化:指定每个block中的残差块个数
mynet=ResNet([2,2,2,2])
X = tf.random.uniform(shape=(1, 224, 224 , 1))
y = mynet(X)
mynet.summary()
运行输出:
Model: "res_net"
_________________________________________________________________
Layer (type) Output Shape Param #
=================================================================
conv2d (Conv2D) multiple 3200
batch_normalization (BatchN multiple 256
ormalization)
activation (Activation) multiple 0
max_pooling2d (MaxPooling2D multiple 0
)
resnet_block (ResnetBlock) multiple 148736
resnet_block_1 (ResnetBlock multiple 526976
)
resnet_block_2 (ResnetBlock multiple 2102528
)
resnet_block_3 (ResnetBlock multiple 8399360
)
global_average_pooling2d (G multiple 0
lobalAveragePooling2D)
dense (Dense) multiple 5130
=================================================================
Total params: 11,186,186
Trainable params: 11,178,378
Non-trainable params: 7,808
利用ResNet完成图像分类
2.手写数字势识别
因为ImageNet数据集较大训练时间较长,我们仍用前面的MNIST数据集来演示resNet。读取数据的时将图像高和宽扩大到ResNet使用的图像高和宽224。这个通过tf.image.resize_with_pad
来实现。
2.1 数据读取
首先获取数据,并进行维度调整:
import tensorflow as tf
import matplotlib.pyplot as plt
import numpy as np
# 获取手写数字数据集
(train_images, train_labels), (test_images, test_labels) = tf.keras.datasets.mnist.load_data()
# 训练集数据维度的调整:N H W C
train_images = np.reshape(train_images,(train_images.shape[0],train_images.shape[1],train_images.shape[2],1))
# 测试集数据维度的调整:N H W C
test_images = np.reshape(test_images,(test_images.shape[0],test_images.shape[1],test_images.shape[2],1))
由于使用全部数据训练时间较长,我们定义两个方法获取部分数据,并将图像调整为224*224大小,进行模型训练:
# 定义两个方法随机抽取部分样本演示
# 获取训练集数据
def get_train(size):
# 随机生成要抽样的样本的索引
index = np.random.randint(0, np.shape(train_images)[0], size)
# 将这些数据resize成22*227大小
resized_images = tf.image.resize_with_pad(train_images[index],224,224,)
# 返回抽取的
return resized_images.numpy(), train_labels[index]
# 获取测试集数据
def get_test(size):
# 随机生成要抽样的样本的索引
index = np.random.randint(0, np.shape(test_images)[0], size)
# 将这些数据resize成224*224大小
resized_images = tf.image.resize_with_pad(test_images[index],224,224,)
# 返回抽样的测试样本
return resized_images.numpy(), test_labels[index]
调用上述两个方法,获取参与模型训练和测试的数据集:
# 获取训练样本和测试样本
train_images,train_labels = get_train(256)
test_images,test_labels = get_test(128)
2.2 模型编译
# 指定优化器,损失函数和评价指标
optimizer = tf.keras.optimizers.SGD(learning_rate=0.01, momentum=0.0)
mynet.compile(optimizer=optimizer,
loss='sparse_categorical_crossentropy',
metrics=['accuracy'])
2.3 模型训练
# 模型训练:指定训练数据,batchsize,epoch,验证集
mynet.fit(train_images,train_labels,batch_size=128,epochs=3,verbose=1,validation_split=0.1)
训练输出为:
Epoch 1/3
2/2 [==============================] - 10s 5s/step - loss: 2.7811 - accuracy: 0.1391 - val_loss: 4.7931 - val_accuracy: 0.1923
Epoch 2/3
2/2 [==============================] - 8s 4s/step - loss: 2.2579 - accuracy: 0.2478 - val_loss: 2.9262 - val_accuracy: 0.2692
Epoch 3/3
2/2 [==============================] - 15s 7s/step - loss: 2.0874 - accuracy: 0.2609 - val_loss: 2.5882 - val_accuracy: 0.2692
2.4 模型评估
# 指定测试数据
mynet.evaluate(test_images,test_labels,verbose=1)
ResNet完成手写数字势识别,全部代码:
# 环境minist/001/优化之前准确率较低
import tensorflow as tf
import matplotlib.pyplot as plt
import numpy as np
from tensorflow.keras import layers, activations
# 获取手写数字数据集
(train_images, train_labels), (test_images, test_labels) = tf.keras.datasets.mnist.load_data()
# 训练集数据维度的调整:N H W C
train_images = np.reshape(train_images,(train_images.shape[0],train_images.shape[1],train_images.shape[2],1))
# 测试集数据维度的调整:N H W C
test_images = np.reshape(test_images,(test_images.shape[0],test_images.shape[1],test_images.shape[2],1))
# 定义ResNet的残差块
class Residual(tf.keras.Model):
# 指明残差块的通道数,是否使用1*1卷积,步长
def __init__(self, num_channels, use_1x1conv=False, strides=1):
super(Residual, self).__init__()
# 卷积层:指明卷积核个数,padding,卷积核大小,步长
self.conv1 = layers.Conv2D(num_channels,
padding='same',
kernel_size=3,
strides=strides)
# 卷积层:指明卷积核个数,padding,卷积核大小,步长
self.conv2 = layers.Conv2D(num_channels, kernel_size=3, padding='same')
if use_1x1conv:
self.conv3 = layers.Conv2D(num_channels,
kernel_size=1,
strides=strides)
else:
self.conv3 = None
# 指明BN层
self.bn1 = layers.BatchNormalization()
self.bn2 = layers.BatchNormalization()
# 定义前向传播过程
def call(self, X):
# 卷积,BN,激活
Y = activations.relu(self.bn1(self.conv1(X)))
# 卷积,BN
Y = self.bn2(self.conv2(Y))
# 对输入数据进行1*1卷积保证通道数相同
if self.conv3:
X = self.conv3(X)
# 返回与输入相加后激活的结果
return activations.relu(Y + X)
# ResNet网络中模块的构成
class ResnetBlock(tf.keras.layers.Layer):
# 网络层的定义:输出通道数(卷积核个数),模块中包含的残差块个数,是否为第一个模块
def __init__(self,num_channels, num_residuals, first_block=False):
super(ResnetBlock, self).__init__()
# 模块中的网络层
self.listLayers=[]
# 遍历模块中所有的层
for i in range(num_residuals):
# 若为第一个残差块并且不是第一个模块,则使用1*1卷积,步长为2(目的是减小特征图,并增大通道数)
if i == 0 and not first_block:
self.listLayers.append(Residual(num_channels, use_1x1conv=True, strides=2))
# 否则不使用1*1卷积,步长为1
else:
self.listLayers.append(Residual(num_channels))
# 定义前向传播过程
def call(self, X):
# 所有层依次向前传播即可
for layer in self.listLayers.layers:
X = layer(X)
return X
# 构建ResNet网络
class ResNet(tf.keras.Model):
# 初始化:指定每个模块中的残差快的个数
def __init__(self,num_blocks):
super(ResNet, self).__init__()
# 输入层:7*7卷积,步长为2
self.conv=layers.Conv2D(64, kernel_size=7, strides=2, padding='same')
# BN层
self.bn=layers.BatchNormalization()
# 激活层
self.relu=layers.Activation('relu')
# 最大池化层
self.mp=layers.MaxPool2D(pool_size=3, strides=2, padding='same')
# 第一个block,通道数为64
self.resnet_block1=ResnetBlock(64,num_blocks[0], first_block=True)
# 第二个block,通道数为128
self.resnet_block2=ResnetBlock(128,num_blocks[1])
# 第三个block,通道数为256
self.resnet_block3=ResnetBlock(256,num_blocks[2])
# 第四个block,通道数为512
self.resnet_block4=ResnetBlock(512,num_blocks[3])
# 全局平均池化
self.gap=layers.GlobalAvgPool2D()
# 全连接层:分类
self.fc=layers.Dense(units=10,activation=tf.keras.activations.softmax)
# 前向传播过程
def call(self, x):
# 卷积
x=self.conv(x)
# BN
x=self.bn(x)
# 激活
x=self.relu(x)
# 最大池化
x=self.mp(x)
# 残差模块
x=self.resnet_block1(x)
x=self.resnet_block2(x)
x=self.resnet_block3(x)
x=self.resnet_block4(x)
# 全局平均池化
x=self.gap(x)
# 全链接层
x=self.fc(x)
return x
# 模型实例化:指定每个block中的残差块个数
mynet=ResNet([2,2,2,2])
X = tf.random.uniform(shape=(1, 224, 224 , 1))
y = mynet(X)
mynet.summary()
# 定义两个方法随机抽取部分样本演示
# 获取训练集数据
def get_train(size):
# 随机生成要抽样的样本的索引
index = np.random.randint(0, np.shape(train_images)[0], size)
# 将这些数据resize成22*227大小
resized_images = tf.image.resize_with_pad(train_images[index],224,224,)
# 返回抽取的
return resized_images.numpy(), train_labels[index]
# 获取测试集数据
def get_test(size):
# 随机生成要抽样的样本的索引
index = np.random.randint(0, np.shape(test_images)[0], size)
# 将这些数据resize成224*224大小
resized_images = tf.image.resize_with_pad(test_images[index],224,224,)
# 返回抽样的测试样本
return resized_images.numpy(), test_labels[index]
# 获取训练样本和测试样本
train_images,train_labels = get_train(256)
test_images,test_labels = get_test(128)
# 指定优化器,损失函数和评价指标
optimizer = tf.keras.optimizers.SGD(learning_rate=0.01, momentum=0.0)
mynet.compile(optimizer=optimizer,
loss='sparse_categorical_crossentropy',
metrics=['accuracy'])
# 模型训练:指定训练数据,batchsize,epoch,验证集
mynet.fit(train_images,train_labels,batch_size=128,epochs=3,verbose=1,validation_split=0.1)
# 指定测试数据
mynet.evaluate(test_images,test_labels,verbose=1)
输出结果:
Model: "res_net"
_________________________________________________________________
Layer (type) Output Shape Param #
=================================================================
conv2d (Conv2D) multiple 3200
batch_normalization (BatchN multiple 256
ormalization)
activation (Activation) multiple 0
max_pooling2d (MaxPooling2D multiple 0
)
resnet_block (ResnetBlock) multiple 148736
resnet_block_1 (ResnetBlock multiple 526976
)
resnet_block_2 (ResnetBlock multiple 2102528
)
resnet_block_3 (ResnetBlock multiple 8399360
)
global_average_pooling2d (G multiple 0
lobalAveragePooling2D)
dense (Dense) multiple 5130
=================================================================
Total params: 11,186,186
Trainable params: 11,178,378
Non-trainable params: 7,808
_________________________________________________________________
Epoch 1/3
2/2 [==============================] - 5s 2s/step - loss: 2.6417 - accuracy: 0.1435 - val_loss: 4.8785 - val_accuracy: 0.0769
Epoch 2/3
2/2 [==============================] - 1s 279ms/step - loss: 2.0891 - accuracy: 0.3130 - val_loss: 7.6229 - val_accuracy: 0.0769
Epoch 3/3
2/2 [==============================] - 1s 281ms/step - loss: 1.9228 - accuracy: 0.3609 - val_loss: 6.5439 - val_accuracy: 0.0769
4/4 [==============================] - 1s 24ms/step - loss: 6.7895 - accuracy: 0.1484