《动手学深度学习》Paddle 版源码-5.11章(ResNet)
时间:2026-02-15 09:55:58
提出了一种创新性的模型类型残差网络(ResNet),这是由何恺明等人开发并解决深层神经网络训练中误差不断上升问题的关键技术。通过拟合残差映射简化优化过程,输入可以跨越多个层传播。该模型保留了VGG的积设计,并采用了包含残差块模块的结构,使得网络架构简单清晰。ResNet不仅在Fashion-MNIST上成功验证了其有效性,还显著影响了深度神经网络的设计领域。
残差网络(ResNet)
我们先思考一个问题:是否添加新的层后,模型训练完成后只可能更有效地降低训练误差?理论上讲,原模型解的空间只是新模型解的空间的一个子集。换句话说,如果我们能够将新添加的层训练成恒等映射f(x)=x,那么两个模型的结果应该是完全相同的,从而在拟合训练数据集上表现相同。由于新模型可能得出更好的解来拟合训练数据集,因此似乎添加层后会更有效地降低训练误差。然而,在实践中,这通常不会是如此。何恺明等人提出的残差网络(ResNet) [ 解决了这个问题。这个网络通过在每一层后面加一个恒等映射来模拟模型的前向传播路径,从而允许深层神经网络的参数更新更加容易。此外,批量归一化技术使训练更深层的模型更加可行,并且也改善了梯度消失的问题。残差网络的设计思路是,在每一个新的模块中添加一个反向传播路径,即所谓的“残差”,这个路径将输入和输出连接起来,使得在训练过程中可以利用已学到的信息来优化参数。这种设计方式让网络能够从简单的初始层开始,然后逐步过渡到更深的网络结构。总之,尽管最初的解释似乎暗示着添加更多的层会降低训练误差,但实际的情况往往与预期相反。随着神经网络深度的增加,梯度消失的问题变得更加严重,并且深层模型在训练过程中经常需要大量的参数来收敛。因此,为了有效地解决这个问题并获得更好的性能,人们提出了残差网络和其他改进技术。
残差块
聚焦于神经网络局部,参见图设输入为xx。希望学出的理想映射为f(x)f(x),作为图方激活函数的输入。左图虚线框中的部分需直接拟合出该映射f(x)f(x);而右图虚线框中的部分则需要拟合出有关恒等映射的残差映射f(x)xf(x)x,这在实际中往往更容易优化。以本节开头提到的恒等映射作为理想映射f(x)f(x),只需将图右图虚线框内上方的加权运算(如仿射)权重和偏差参数学成那么f(x)f(x)即为恒等映射。当理想映射f(x)f(x)极接近于恒等映射时,残差映射也易于捕捉恒等映射的细微波动。图图是ResNet的基础块,即残差块(residual block)。在残差块中,输入可通过跨层的数据线路更快地向前传播。

ResNet沿用了VGG全积层的设计。在残差块中,首先有两个具有相同输出通道数的积层。每个卷积层后接一个批量归一化层和ReLU激活函数。然后将输入跳过这卷积运算并直接连接到最后的ReLU激活函数前。这种设计要求两个卷积层的输出与输入形状相同,这样可以直接相加。如果需要改变通道数,引入一个额外的积层来处理输入数据,使其匹配所需形状后再进行相加操作。
残差模块的实现示例如下,包括指定输出通道数量、决定是否启用额外积以调整通道数,及确定卷积层的步幅。
import paddleimport paddle.nn as nnimport numpy as npimport warnings warnings.filterwarnings("ignore", category=Warning) # 过滤报警信息class Residual(nn.Layer): def __init__(self, num_channels, num_filters, use_1x1conv=False, stride=1): super(Residual, self).__init__() self.use_1x1conv = use_1x1conv model = [ nn.Conv2D(num_channels, num_filters, 3, stride=stride, padding=1), nn.BatchNorm2D(num_filters), nn.ReLU(), nn.Conv2D(num_filters, num_filters, 3, stride=1, padding=1), nn.BatchNorm2D(num_filters), ] self.model = nn.Sequential(*model) if use_1x1conv: model_1x1 = [nn.Conv2D(num_channels, num_filters, 1, stride=stride)] self.model_1x1 = nn.Sequential(*model_1x1) def forward(self, X): Y = self.model(X) if self.use_1x1conv: X = self.model_1x1(X) return paddle.nn.functional.relu(X + Y)登录后复制
下面我们来查看输入和输出形状一致的情况。 In []
blk = Residual(3, 3) X = paddle.to_tensor(np.random.uniform(-1., 1., [4, 3, 6, 6]).astype('float32')) Y = blk(X)print(Y.shape)登录后复制
[4, 3, 6, 6]登录后复制
我们也可以在增加输出通道数的同时减半输出的高和宽。 In []
blk = Residual(3, 6, use_1x1conv=True, stride=2) X = paddle.to_tensor(np.random.uniform(-1., 1., [4, 3, 6, 6]).astype('float32')) Y = blk(X)print(Y.shape)登录后复制
[4, 6, 3, 3]登录后复制
GoogleNet随后采用了四个由Inception模块构成的子网络,这些子网络中的每个部分通过相同的输出通道数量来构建。不同于ResNet的结构,GoogleNet在初始阶段并未削减输入图像的高度或宽度。接下来的每个子网络都采用第一个残差块将上一阶段的通道数翻倍,并且高度和宽度都会相应地减半。
下面我们来实现这个模块。注意,这里对第一个模块做了特别处理。 In []
class ResnetBlock(nn.Layer): def __init__(self, num_channels, num_filters, num_residuals, first_block=False): super(ResnetBlock, self).__init__() model = [] for i in range(num_residuals): if i == 0: if not first_block: model += [Residual(num_channels, num_filters, use_1x1conv=True, stride=2)] else: model += [Residual(num_channels, num_filters)] else: model += [Residual(num_filters, num_filters)] self.model = nn.Sequential(*model) def forward(self, X): return self.model(X)登录后复制
ResNet模型
In []
这是一个示例代码,展示了如何实现一个简单的ResNet模型。首先,我们定义了`ResNet`类,并在初始化时包含了GoogLeNet中的前两层:一个卷积层和步幅为最大池化层。不同之处在于,每个残差块(Block)后增加了批量归一化层。接着,我们在`model`中添加了多个ResnetBlock模块,每个模块包含两个残差块。最后一个模块在接上全局平均池化层和全连接层后输出。最后,我们实现了模型的前向传播函数,并使用随机生成的数据进行了测试。通过`print(Y.shape)`可以查看输入数据的形状为[ ,这通常表示一个批次中的四个样本,每个样本有通道和小的图像。如果您需要进一步帮助,请随时告诉我!
[4, 10]登录后复制
本文将揭示深度学习中的一个经典模型ResNet的详细构造和其背后的机制。ResNet由卷积层组成(不包括积层),加上最开始的卷积层以及最后的全连接层,构成总共有的架构。这种设计使得ResNet可以随着网络深度逐渐增加而保持高效性。ResNet系列模型通过调整通道数和模块里的残差块数量来生成不同版本的ResNet模型。例如,版本的ResNet-ResNet-深,但仍然采用了相同的架构设计,只不过增加了更多的网络层以实现更高层次的学习能力。尽管ResNet的主体架构与GoogLeNet相似,但它具有更简单的设计和容易修改的优势。这些特点使得ResNet能够快速普及并广泛应用于实际应用中。
在训练ResNet之前,我们来观察一下输入形状在ResNet不同模块之间的变化。 In []
resnet = ResNet(10) param_info = paddle.summary(resnet, (1, 1, 96, 96))print(param_info)登录后复制
------------------------------------------------------------------------------- Layer (type) Input Shape Output Shape Param # =============================================================================== Conv2D-26 [[1, 1, 96, 96]] [1, 64, 48, 48] 3,200 BatchNorm2D-22 [[1, 64, 48, 48]] [1, 64, 48, 48] 256 ReLU-12 [[1, 64, 48, 48]] [1, 64, 48, 48] 0 MaxPool2D-2 [[1, 64, 48, 48]] [1, 64, 24, 24] 0 Conv2D-27 [[1, 64, 24, 24]] [1, 64, 24, 24] 36,928 BatchNorm2D-23 [[1, 64, 24, 24]] [1, 64, 24, 24] 256 ReLU-13 [[1, 64, 24, 24]] [1, 64, 24, 24] 0 Conv2D-28 [[1, 64, 24, 24]] [1, 64, 24, 24] 36,928 BatchNorm2D-24 [[1, 64, 24, 24]] [1, 64, 24, 24] 256 Residual-11 [[1, 64, 24, 24]] [1, 64, 24, 24] 0 Conv2D-29 [[1, 64, 24, 24]] [1, 64, 24, 24] 36,928 BatchNorm2D-25 [[1, 64, 24, 24]] [1, 64, 24, 24] 256 ReLU-14 [[1, 64, 24, 24]] [1, 64, 24, 24] 0 Conv2D-30 [[1, 64, 24, 24]] [1, 64, 24, 24] 36,928 BatchNorm2D-26 [[1, 64, 24, 24]] [1, 64, 24, 24] 256 Residual-12 [[1, 64, 24, 24]] [1, 64, 24, 24] 0 ResnetBlock-5 [[1, 64, 24, 24]] [1, 64, 24, 24] 0 Conv2D-31 [[1, 64, 24, 24]] [1, 128, 12, 12] 73,856 BatchNorm2D-27 [[1, 128, 12, 12]] [1, 128, 12, 12] 512 ReLU-15 [[1, 128, 12, 12]] [1, 128, 12, 12] 0 Conv2D-32 [[1, 128, 12, 12]] [1, 128, 12, 12] 147,584 BatchNorm2D-28 [[1, 128, 12, 12]] [1, 128, 12, 12] 512 Conv2D-33 [[1, 64, 24, 24]] [1, 128, 12, 12] 8,320 Residual-13 [[1, 64, 24, 24]] [1, 128, 12, 12] 0 Conv2D-34 [[1, 128, 12, 12]] [1, 128, 12, 12] 147,584 BatchNorm2D-29 [[1, 128, 12, 12]] [1, 128, 12, 12] 512 ReLU-16 [[1, 128, 12, 12]] [1, 128, 12, 12] 0 Conv2D-35 [[1, 128, 12, 12]] [1, 128, 12, 12] 147,584 BatchNorm2D-30 [[1, 128, 12, 12]] [1, 128, 12, 12] 512 Residual-14 [[1, 128, 12, 12]] [1, 128, 12, 12] 0 ResnetBlock-6 [[1, 64, 24, 24]] [1, 128, 12, 12] 0 Conv2D-36 [[1, 128, 12, 12]] [1, 256, 6, 6] 295,168 BatchNorm2D-31 [[1, 256, 6, 6]] [1, 256, 6, 6] 1,024 ReLU-17 [[1, 256, 6, 6]] [1, 256, 6, 6] 0 Conv2D-37 [[1, 256, 6, 6]] [1, 256, 6, 6] 590,080 BatchNorm2D-32 [[1, 256, 6, 6]] [1, 256, 6, 6] 1,024 Conv2D-38 [[1, 128, 12, 12]] [1, 256, 6, 6] 33,024 Residual-15 [[1, 128, 12, 12]] [1, 256, 6, 6] 0 Conv2D-39 [[1, 256, 6, 6]] [1, 256, 6, 6] 590,080 BatchNorm2D-33 [[1, 256, 6, 6]] [1, 256, 6, 6] 1,024 ReLU-18 [[1, 256, 6, 6]] [1, 256, 6, 6] 0 Conv2D-40 [[1, 256, 6, 6]] [1, 256, 6, 6] 590,080 BatchNorm2D-34 [[1, 256, 6, 6]] [1, 256, 6, 6] 1,024 Residual-16 [[1, 256, 6, 6]] [1, 256, 6, 6] 0 ResnetBlock-7 [[1, 128, 12, 12]] [1, 256, 6, 6] 0 Conv2D-41 [[1, 256, 6, 6]] [1, 512, 3, 3] 1,180,160 BatchNorm2D-35 [[1, 512, 3, 3]] [1, 512, 3, 3] 2,048 ReLU-19 [[1, 512, 3, 3]] [1, 512, 3, 3] 0 Conv2D-42 [[1, 512, 3, 3]] [1, 512, 3, 3] 2,359,808 BatchNorm2D-36 [[1, 512, 3, 3]] [1, 512, 3, 3] 2,048 Conv2D-43 [[1, 256, 6, 6]] [1, 512, 3, 3] 131,584 Residual-17 [[1, 256, 6, 6]] [1, 512, 3, 3] 0 Conv2D-44 [[1, 512, 3, 3]] [1, 512, 3, 3] 2,359,808 BatchNorm2D-37 [[1, 512, 3, 3]] [1, 512, 3, 3] 2,048 ReLU-20 [[1, 512, 3, 3]] [1, 512, 3, 3] 0 Conv2D-45 [[1, 512, 3, 3]] [1, 512, 3, 3] 2,359,808 BatchNorm2D-38 [[1, 512, 3, 3]] [1, 512, 3, 3] 2,048 Residual-18 [[1, 512, 3, 3]] [1, 512, 3, 3] 0 ResnetBlock-8 [[1, 256, 6, 6]] [1, 512, 3, 3] 0 AdaptiveAvgPool2D-2 [[1, 512, 3, 3]] [1, 512, 1, 1] 0 Flatten-3 [[1, 512, 1, 1]] [1, 512] 0 Linear-2 [[1, 512]] [1, 10] 5,130 =============================================================================== Total params: 11,186,186 Trainable params: 11,170,570 Non-trainable params: 15,616 ------------------------------------------------------------------------------- Input size (MB): 0.04 Forward/backward pass size (MB): 10.77 Params size (MB): 42.67 Estimated Total Size (MB): 53.47 ------------------------------------------------------------------------------- {'total_params': 11186186, 'trainable_params': 11170570}登录后复制
训练模型
下面我们在Fashion-MNIST数据集上训练ResNet。 In []
import paddleimport paddle.vision.transforms as Tfrom paddle.vision.datasets import FashionMNIST# 数据集处理transform = T.Compose([ T.Resize(96), T.Transpose(), T.Normalize([127.5], [127.5]), ]) train_dataset = FashionMNIST(mode='train', transform=transform) val_dataset = FashionMNIST(mode='test', transform=transform)# 模型定义model = paddle.Model(ResNet(10))# 设置训练模型所需的optimizer, loss, metricmodel.prepare( paddle.optimizer.Adam(learning_rate=0.001, parameters=model.parameters()), paddle.nn.CrossEntropyLoss(), paddle.metric.Accuracy(topk=(1, 5)))# 启动训练、评估model.fit(train_dataset, val_dataset, epochs=2, batch_size=64, log_freq=100)登录后复制
The loss value printed in the log is the current step, and the metric is the average value of previous step. Epoch 1/2 step 100/938 - loss: 0.3613 - acc_top1: 0.7612 - acc_top5: 0.9820 - 3s/step step 200/938 - loss: 0.4060 - acc_top1: 0.7972 - acc_top5: 0.9881 - 3s/step step 300/938 - loss: 0.3635 - acc_top1: 0.8171 - acc_top5: 0.9908 - 3s/step step 400/938 - loss: 0.3369 - acc_top1: 0.8292 - acc_top5: 0.9926 - 3s/step step 500/938 - loss: 0.2733 - acc_top1: 0.8390 - acc_top5: 0.9937 - 3s/step step 600/938 - loss: 0.1964 - acc_top1: 0.8469 - acc_top5: 0.9943 - 3s/step登录后复制
小结
残差块借助多层交互路径,使深度神经架构有效;ResNet启发后续设计趋势。
以上就是动手学深度学习Paddle 版源码-5.11章(ResNet)的详细内容,更多请关注其它相关文章!











