1、Pytorch神经网络基础

区分Module,block和Sequential

  1. Module(模块,最基础单位)

定义:
nn.Module 是 PyTorch 所有神经网络层、模型的基类(base class)
所有的网络层(Linear、Conv2d、RNN等)以及你自己写的自定义网络,都必须继承它。

特点:

  • 一切网络结构的根基。
  • 包含参数(weightbias)、前向传播逻辑(forward())等。
  • 可以嵌套其他 Module(例如一个模型可以由若干层组成)。

举例:

import torch.nn as nn

class MLP(nn.Module):
    def __init__(self):
        super().__init__()
        self.fc1 = nn.Linear(784, 256)
        self.relu = nn.ReLU()
        self.fc2 = nn.Linear(256, 10)
    
    def forward(self, x):
        x = self.relu(self.fc1(x))
        return self.fc2(x)

Module神经网络的通用容器,一切层与模型都是它的子类。

  1. Block(块,结构单元)

定义:
Block 并不是 PyTorch 中的一个正式类名,而是我们在设计网络时人为定义的中间层次结构
它是由若干 Module 组成的一个子结构。

用途:

  • 让网络结构更清晰、可复用。
  • 例如 CNN 里一个 “残差块(Residual Block)” 就是一个 Block。
  • Block 也继承自 nn.Module(本质上仍是一个 Module)。

举例(ResNet 残差块):

class ResidualBlock(nn.Module):
    def __init__(self, in_channels, out_channels):
        super().__init__()
        self.conv1 = nn.Conv2d(in_channels, out_channels, 3, padding=1)
        self.bn1 = nn.BatchNorm2d(out_channels)
        self.relu = nn.ReLU()
        self.conv2 = nn.Conv2d(out_channels, out_channels, 3, padding=1)
        self.bn2 = nn.BatchNorm2d(out_channels)

    def forward(self, x):
        out = self.relu(self.bn1(self.conv1(x)))
        out = self.bn2(self.conv2(out))
        return self.relu(out + x)  # 残差连接

Block 是由多个层组成的中间结构单元,通常代表网络的一个功能模块。

  1. Sequential(顺序容器)

定义:
nn.Sequentialnn.Module 的一种特殊子类,用来按顺序堆叠层,让代码更简洁。

特点:

  • 自动把层按定义顺序依次执行。
  • 适合线性结构的模型(没有分支或跳跃连接)。
  • 不需要手动写 forward()

举例:

net = nn.Sequential(
    nn.Flatten(),
    nn.Linear(784, 256),
    nn.ReLU(),
    nn.Linear(256, 10)
)

等价于手动写的:

class Net(nn.Module):
    def __init__(self):
        super().__init__()
        self.flatten = nn.Flatten()
        self.fc1 = nn.Linear(784, 256)
        self.relu = nn.ReLU()
        self.fc2 = nn.Linear(256, 10)
    
    def forward(self, x):
        x = self.flatten(x)
        x = self.relu(self.fc1(x))
        return self.fc2(x)

Sequential 是一种简化的模块组合容器,用于快速堆叠线性结构的层。

层次 名称 作用 是否继承自 nn.Module 是否需要 forward()
底层 Module 一切层与模型的基类 ✅(一般要自己写)
中层 Block 功能模块,由若干层组成 ✅(自定义结构)
高层 Sequential 顺序组合层的容器 ❌(自动执行)
  1. 混合使用的方式

在实际项目中,这三者往往组合使用

class MyNet(nn.Module):
    def __init__(self):
        super().__init__()
        # Sequential 组合基础层
        self.block1 = nn.Sequential(
            nn.Conv2d(3, 16, 3, padding=1),
            nn.ReLU(),
            nn.MaxPool2d(2)
        )
        # 自定义 Block
        self.block2 = ResidualBlock(16, 16)
        # 普通线性层
        self.fc = nn.Linear(16 * 16 * 16, 10)
    
    def forward(self, x):
        x = self.block1(x)
        x = self.block2(x)
        x = x.view(x.size(0), -1)
        return self.fc(x)
  • MyNet 是一个 Module(模型)
  • block1 是一个 Sequential 容器
  • block2 是一个 Block(ResidualBlock)

模型构造

(block)可以描述单个层、由多个层组成的组件或整个模型本身。使用块进行抽象的一个好处是可以将一些块组合成更大的组件, 这一过程通常是递归的,如下图所示。 通过定义代码来按需生成任意复杂度的块, 我们可以通过简洁的代码实现复杂的神经网络

在这里插入图片描述

从编程的角度来看,块由(class)表示。 它的任何子类都必须定义一个将其输入转换为输出的前向传播函数, 并且必须存储任何必需的参数

使用 Sequential 实现层

nn.Sequential定义了一种特殊的 Module,通过实例化 nn.Sequential 来构建我们的模型, 的执行顺序是作为参数传递的。 是 PyTorch 提供的一种顺序容器(Sequential Container),用于把多个神经网络层顺序地堆叠起来

# 回顾一下多层感知机
import torch
from torch import nn
from torch.nn import functional as F
net = nn.Sequential(nn.Linear(20,256),nn.ReLU(),nn.Linear(256,10))
X = torch.rand(2,20)
net(X)

输出:
tensor([[-0.1418, -0.0352, -0.1629, -0.1160, -0.2405, -0.0673,  0.1312, -0.0932,
         -0.2044,  0.1256],
        [-0.2932,  0.0930, -0.1489, -0.1889, -0.2302, -0.0259,  0.1906,  0.0554,
         -0.1938,  0.0438]], grad_fn=<AddmmBackward0>)

使用 Block 实现块

任何一个层、神经网络都可以看作 Module 的一个子类。

class MLP(nn.Module):
    def __init__(self):
        super().__init__()  # 调用父类的__init__函数
        self.hidden = nn.Linear(20,256)
        self.out = nn.Linear(256,10)
        # 必须重新定义前馈过程
    def forward(self, X):
        return self.out(F.relu(self.hidden(X)))
    
# 实例化多层感知机的层,然后在每次调用正向传播函数调用这些层
net = MLP()
X = torch.rand(2,20)
net(X)

输出:
tensor([[-0.1728,  0.0589, -0.2289,  0.2459,  0.1132,  0.1369,  0.1149,  0.2799,
         -0.0008, -0.0085],
        [-0.1964,  0.0675, -0.1473,  0.2637,  0.1521,  0.2004,  0.2347,  0.1325,
         -0.1322,  0.0660]], grad_fn=<AddmmBackward0>)

自定义 Sequential 实现—顺序块

class MySequential(nn.Module):
    def __init__(self, *args):
        super().__init__()
        for block in args:
   # 这里,`module`是`Module`子类的一个实例。我们把它保存在'Module'类的成员
 # 变量`_modules` 中。`module`的类型是OrderedDict
            self._modules[block] = block # block 本身作为它的key,存在_modules里面的为层,以字典的形式
            
    def forward(self, X):
        for block in self._modules.values():
            print(block)
            X = block(X)
        return X
    
net = MySequential(nn.Linear(20,256),nn.ReLU(),nn.Linear(256,10))
X = torch.rand(2,20)
net(X)

输出:
Linear(in_features=20, out_features=256, bias=True)
ReLU()
Linear(in_features=256, out_features=10, bias=True)
tensor([[ 0.2262, -0.0092,  0.3643,  0.0619, -0.2567, -0.1004,  0.0672, -0.1767,
         -0.0982,  0.0855],
        [ 0.1904, -0.0297,  0.3234,  0.0858, -0.1112, -0.1544, -0.0252, -0.1135,
         -0.0491, -0.0197]], grad_fn=<AddmmBackward0>)

自定义 Block 实现正向传播

# 在正向传播函数中执行代码
class FixedHiddenMLP(nn.Module):
    def __init__(self):
        super().__init__()
        self.rand_weight = torch.rand((20,20),requires_grad=False)
        self.linear = nn.Linear(20,20)
    
    def forward(self, X):
        # 第一层线性变换
        X = self.linear(X)
        # 固定权重矩阵运算 + ReLU 激活
        # mm表示矩阵乘法
        X = F.relu(torch.mm(X, self.rand_weight + 1))
        # 再次线性变换
        X = self.linear(X)
        # 循环控制输出范围,如果总和 > 1,就不断把 X 除以 2
        while X.abs().sum() > 1:
            X /= 2
        # 对最终的张量所有元素求和
        return X.sum()
    
net = FixedHiddenMLP()
X = torch.rand(2,20)
net(X)

在这个FixedHiddenMLP模型中,我们实现了一个隐藏层,
其权重(self.rand_weight)在实例化时被随机初始化,之后为常量。
这个权重不是一个模型参数,因此它永远不会被反向传播更新。
然后,神经网络将这个固定层的输出通过一个全连接层。

注意,在返回输出之前,模型做了一些不寻常的事情:
它运行了一个 while 循环,在 L 1 L_1 L1 范数大于 1 1 1的条件下,
将输出向量除以 2 2 2,直到它满足条件为止。
最后,模型返回了X中所有项的和。
注意,此操作可能不会常用于在任何实际任务中,
我们只是向你展示如何将任意代码集成到神经网络计算的流程中。

混合 Sequential 和 Block 使用

# 混合代培各种组合块的方法
class NestMLP(nn.Module):
    def __init__(self):
        super().__init__()
        self.net = nn.Sequential(nn.Linear(20,64),nn.ReLU(),
                                nn.Linear(64,32),nn.ReLU())
        self.linear = nn.Linear(32,16)
        
    def forward(self, X):
        return self.linear(self.net(X))
    
chimear = nn.Sequential(NestMLP(),nn.Linear(16,20),FixedHiddenMLP())
X = torch.rand(2,20)
chimear(X)   #tensor(-0.0979, grad_fn=<SumBackward0>)

综上,块可以理解为能够实现一个或多个层的类,通过定义类的实例化来完成神经网络的运算。

参数管理

参数访问

我们从已有模型中访问参数。当通过Sequential类定义模型时,我们可以通过索引来访问模型的任意层。这就像模型是一个列表一样,每层的参数都在其属性中。如下所示,我们可以检查第二个全连接层的参数。

state_dict() 查看字典形式的模型参数数值

# 可以把Sequential看作一个list,可以用索引拿出每一层的参数。得到一个有序字典。
print(net[2].state_dict())
# Out:OrderedDict([('weight', tensor([[-0.0284,  0.0011, -0.2123,  0.2835,  0.3124,  0.0953, -0.2331, -0.2731]])), ('bias', tensor([-0.3001]))])
# module.state_dict().keys()=['weight','bias']

输出的结果告诉我们一些重要的事情:
首先,这个全连接层包含两个参数,分别是该层的权重和偏置。
两者都存储为单精度浮点数(float32)。
注意,参数名称允许唯一标识每个参数,即使在包含数百个层的网络中也是如此。

目标参数

每个参数都表示为 参数类(Parameter) 的一个实例。要对参数执行任何操作,首先我们需要访问底层的数值。有几种方法可以做到这一点。有些比较简单,而另一些则比较通用。下面的代码从第二个全连接层(即第三个神经网络层)提取偏置,提取后返回的是一个参数类实例,并进一步访问该参数的值。

nn.bias/.weight(.data/.grad) 直接查看参数

# 首先关注具有单隐藏层的多层感知机
import torch
from torch import nn

net = nn.Sequential(nn.Linear(4,8),nn.ReLU(),nn.Linear(8,1))
X = torch.rand(size=(2,4))
print(net(X))

print(net[2].state_dict()) # 访问参数,net[2]就是最后一个输出层
print(type(net[2].bias)) # 目标参数
print(net[2].bias)
print(net[2].bias.data)

print(net[2].weight.grad == None) # 还没进行反向计算,所以grad为None
print(*[(name, param.shape) for name, param in net[0].named_parameters()])  # 一次性访问所有参数         
print(*[(name, param.shape) for name, param in net.named_parameters()])  # 0是第一层名字,1是ReLU,它没有参数
print(net.state_dict()['2.bias'].data) # 通过名字获取参数
tensor([[0.2581],
        [0.2723]], grad_fn=<AddmmBackward0>)

OrderedDict([('weight', tensor([[-0.0136, -0.2769,  0.2449,  0.1905, -0.1589,  0.2474,  0.3295,  0.0100]])), ('bias', tensor([0.2557]))])
<class 'torch.nn.parameter.Parameter'>
Parameter containing:
tensor([0.2557], requires_grad=True)
tensor([0.2557])

True
('weight', torch.Size([8, 4])) ('bias', torch.Size([8]))
('0.weight', torch.Size([8, 4])) ('0.bias', torch.Size([8])) ('2.weight', torch.Size([1, 8])) ('2.bias', torch.Size([1]))
tensor([0.2557])

参数是复合的对象,包含值.data、梯度.grad和额外信息。 这就是我们需要显式参数值的原因。 除了值之外,我们还可以访问每个参数的梯度。

一次访问所有元素

当我们需要对所有参数执行操作时,逐个访问它们可能会很麻烦。 当我们处理更复杂的块(例如,嵌套块)时,情况可能会变得特别复杂, 因为我们需要递归整个树来提取每个子块的参数。 下面,我们将通过演示来比较访问第一个全连接层的参数和访问所有层。

.named_parameters() 返回 iterator,用于循环,返回(参数名, 参数数值)。

print(*[(name, param.shape) for name, param in net[0].named_parameters()])
print(*[(name, param.shape) for name, param in net.named_parameters()])
# *代表把list/tuple里的元素分开,而非整个输出

还提供了另一种访问网络参数的方式,通过名称(默认以层数序号.weight or .bias),如下所示。

net.state_dict()['2.bias'].data

从嵌套块收集参数

.add_module(name, module) 在当前模块添加含名称的子模块,相比较Sequential()而言可以指定各层名称(而不是默认的“0、1、2……”)

# 从嵌套块收集参数
def block1():
    return nn.Sequential(nn.Linear(4,8),nn.ReLU(),nn.Linear(8,4),nn.ReLU())

def block2():
    net = nn.Sequential()
    for i in range(4):
        net.add_module(f'block{i}',block1()) # f'block{i}' 可以传一个字符串名字过来,block2可以嵌套四个block1                                      
    return net

rgnet = nn.Sequential(block2(), nn.Linear(4,1))
print(rgnet(X))
print(rgnet)
这段程序构建了一个层层嵌套的神经网络,最内层是一个小块 block1,block2 由多个 block1 组成,
最外层 rgnet 又在 block2 外面接了一个线性层。
block2()结构如下:
Sequential(
  (block0): block1()
  (block1): block1()
  (block2): block1()
  (block3): block1()
)
输出:
tensor([[-0.1371],
        [-0.1374]], grad_fn=<AddmmBackward0>)

Sequential(
  (0): Sequential(
    (block0): Sequential(
      (0): Linear(in_features=4, out_features=8, bias=True)
      (1): ReLU()
      (2): Linear(in_features=8, out_features=4, bias=True)
      (3): ReLU()
    )
    (block1): Sequential(
      (0): Linear(in_features=4, out_features=8, bias=True)
      (1): ReLU()
      (2): Linear(in_features=8, out_features=4, bias=True)
      (3): ReLU()
    )
    (block2): Sequential(
      (0): Linear(in_features=4, out_features=8, bias=True)
      (1): ReLU()
      (2): Linear(in_features=8, out_features=4, bias=True)
      (3): ReLU()
    )
    (block3): Sequential(
      (0): Linear(in_features=4, out_features=8, bias=True)
      (1): ReLU()
      (2): Linear(in_features=8, out_features=4, bias=True)
      (3): ReLU()
    )
  )
  (1): Linear(in_features=4, out_features=1, bias=True)
)

因为层是分层嵌套的,所以我们也可以像通过嵌套列表索引一样访问它们。 下面,我们访问第一个主要的块中、第二个子块的第一层的偏置项。

rgnet[0][1][0].bias.data
# Out: tensor([0.4441, 0.0795, 0.3999, 0.3522, 0.3384, 0.0372, 0.1860, 0.3830])

参数初始化

知道了如何访问参数后,现在我们看看如何正确地初始化参数。 深度学习框架提供默认随机初始化, 也允许我们创建自定义初始化方法, 满足我们通过其他规则实现初始化权重。

默认情况下,PyTorch 会根据一个范围均匀地初始化权重和偏置矩阵, 这个范围是根据输入和输出维度计算出的。 PyTorch 的 nn.init 模块提供了多种预置初始化方法。

内置初始化

  • 使用正态分布初始化:nn.init.normal(tensor, mean=0, std=1)
net = nn.Sequential(nn.Linear(4,8),nn.ReLU(),nn.Linear(8,1))

def init_normal(m):
    if type(m) == nn.Linear:
        nn.init.normal_(m.weight, mean=0, std=0.01) # 下划线表示把m.weight的值替换掉   
        nn.init.zeros_(m.bias)
        
net.apply(init_normal) # 会递归调用 直到所有层都初始化
print(net[0].weight.data[0]) # tensor([ 0.0099, -0.0135,  0.0069, -0.0029])
print(net[0].bias.data[0])  # tensor(0.)
  • 使用常数初始化:torch.nn.init.constant(tensor, val)
net = nn.Sequential(nn.Linear(4,8),nn.ReLU(),nn.Linear(8,1))
def init_constant(m):
    # 如果当前模块 m 是一个线性层(nn.Linear),则:
    if type(m) == nn.Linear:
        # 把权重矩阵全部初始化为常数 1;
        nn.init.constant_(m.weight,1)
        # 把偏置向量全部初始化为 0
        nn.init.zeros_(m.bias)
        
net.apply(init_constant)
print(net[0].weight.data[0])  # tensor([1., 1., 1., 1.])
print(net[0].bias.data[0])    # tensor(0.)

实际深度学习训练中,不能把参数初始化为全1,会造成无法训练

  • 使用Xavier随机初始化:torch.nn.init.xavier_uniform(tensor, gain=1)
def xavier(m):
    if type(m) == nn.Linear:
        nn.init.xavier_uniform_(m.weight)
        #uniform distribution
# 针对不同层使用不同初始化
def init_42(m):
    if type(m) == nn.Linear:
        nn.init.constant_(m.weight, 42)
        #nn.init函数设置模块初始值

net[0].apply(xavier)
net[2].apply(init_42)
print(net[0].weight.data[0])  # tensor([ 0.3424, -0.2301,  0.3559, -0.2669])
print(net[2].weight.data)   # tensor([[42., 42., 42., 42., 42., 42., 42., 42.]])

自定义初始化

有时,深度学习框架没有提供我们需要的初始化方法。在下面的例子中,我们使用以下的分布为任意权重参数$ w $定义初始化方法:

w ∼ { U ( 5 , 10 )  可能性  1 4 0  可能性  1 2 U ( − 10 , − 5 )  可能性  1 4 \begin{aligned} w \sim \begin{cases} U(5, 10) & \text{ 可能性 } \frac{1}{4} \\ 0 & \text{ 可能性 } \frac{1}{2} \\ U(-10, -5) & \text{ 可能性 } \frac{1}{4} \end{cases} \end{aligned} w U(5,10)0U(10,5) 可能性 41 可能性 21 可能性 41

同样,我们实现了一个my_init函数来应用到net

# 自定义初始化
def my_init(m):
    if type(m) == nn.Linear:
        print("Init",
              *[(name, param.shape) for name,
                param in m.named_parameters()][0])  # 打印名字是啥,形状是啥       
        nn.init.uniform_(m.weight, -10, 10)
# 这里*=的代码相当于先计算一个布尔矩阵(先判断>=),然后再用布尔矩阵的对应元素去乘以原始矩阵的每个元素。
# 保留绝对值大于5的权重,不是的话就设为0
        m.weight.data *= m.weight.data.abs() >=  5 

net.apply(my_init)
print(net[0].weight[:2])
# 简单粗暴的直接赋值方式
net[0].weight.data[:] += 1 # 参数替换
net[0].weight.data[0,0] = 42
print(net[0].weight.data[0])

  • 参数绑定(共享权重)
# 参数绑定
shared = nn.Linear(8,8)
 # 第2个隐藏层和第3个隐藏层是share权重的,第一个和第四个是自己的
net = nn.Sequential(nn.Linear(4,8),nn.ReLU(),shared,nn.ReLU(),shared,nn.ReLU(),nn.Linear(8,1))   
net(X)
print(net[2].weight.data[0] == net[4].weight.data[0])
# 改掉了第二层同时第四层也会被改掉
net[2].weight.data[0,0] = 100
print(net[2].weight.data[0] == net[4].weight.data[0])

输出:
tensor([True, True, True, True, True, True, True, True])
tensor([True, True, True, True, True, True, True, True])

自定义层

深度学习成功背后的一个因素是神经网络的灵活性: 我们可以用创造性的方式组合不同的层,从而设计出适用于各种任务的架构。 例如,研究人员发明了专门用于处理图像、文本、序列数据和执行动态规划的层。 未来,你会遇到或要自己发明一个现在在深度学习框架中还不存在的层。 在这些情况下,你必须构建自定义层。在本节中,我们将向你展示如何构建。

不带参数的层

# 构造一个没有任何参数的自定义层
import torch
import torch.nn.functional as F
from torch import nn

class CenteredLayer(nn.Module):
    def __init__(self):
        super().__init__()
# 将输入张量减去它的平均值。
    def forward(self, X):
        return X - X.mean()
    
layer = CenteredLayer()
print(layer(torch.FloatTensor([1,2,3,4,5])))  #tensor([-2., -1.,  0.,  1.,  2.])

# 将层作为组件合并到构建更复杂的模型中
net = nn.Sequential(nn.Linear(8,128),CenteredLayer())
Y = net(torch.rand(4,8))
print(Y.mean())  #tensor(-9.3132e-10, grad_fn=<MeanBackward0>)

带参数的层

nn.Parameter(tensor, required_grad=True) #把传入张量当作模块参数,可以对其求导的

# 带参数的图层
class MyLinear(nn.Module):
    def __init__(self, in_units, units):
        super().__init__()
# nn.Parameter() 的作用是把普通的张量 Tensor 注册为模型参数,让 PyTorch 自动跟踪梯度。
        self.weight = nn.Parameter(torch.randn(in_units,units)) # nn.Parameter使得这些参数加上了梯度    
        self.bias = nn.Parameter(torch.randn(units,))

    def forward(self, X):
        linear = torch.matmul(X, self.weight.data) + self.bias.data
        return F.relu(linear)
dense = MyLinear(5,3)
print(dense.weight)

# 使用自定义层直接执行正向传播计算
# torch.rand(2,5) 表示输入两个样本,每个5个特征
print(dense(torch.rand(2,5)))
# 使用自定义层构建模型
net = nn.Sequential(MyLinear(64,8),MyLinear(8,1))
print(net(torch.rand(2,64)))

输出:
Parameter containing:
tensor([[ 0.9883,  0.7476, -0.4670],
        [ 0.8354, -1.8016, -0.7590],
        [ 2.3720,  2.2379,  0.5443],
        [ 0.9369,  0.6526,  1.3551],
        [ 0.0348,  1.0096, -0.3058]], requires_grad=True)

tensor([[1.3499, 0.0000, 0.0000],
        [0.2607, 0.0000, 0.0000]])
tensor([[2.0195],
        [0.6422]])

读写文件

到目前为止,我们讨论了如何处理数据,以及如何构建、训练和测试深度学习模型。然而,有时我们希望保存训练的模型,以备将来在各种环境中使用(比如在部署中进行预测)。此外,当运行一个耗时较长的训练过程时,最佳的做法是定期保存中间结果,以确保在服务器电源被不小心断掉时,我们不会损失几天的计算结果。因此,现在是时候学习如何加载和存储权重向量和整个模型了。

Pytorch存储本质上使用的是Python实现的 Pickle序列化(Serialization) 操作

  • 存储、读取矩阵

torch.save(tensor, ‘filename’)

torch.load(‘filename’)

# 加载和保存张量
import torch
from torch import nn
from torch.nn import functional as F
#存储一个tensor
x = torch.arange(4)
torch.save(x, 'x-file')
x2 = torch.load("x-file")
print(x2)

#存储一个张量列表,然后把它们读回内存
y = torch.zeros(4)
torch.save([x,y],'x-files')
x2, y2 = torch.load('x-files')
print(x2)
print(y2)

# 写入或读取从字符串映射到张量的字典
mydict = {'x':x,'y':y}
torch.save(mydict,'mydict')
mydict2 = torch.load('mydict')
print(mydict2)
输出:
tensor([0, 1, 2, 3])

tensor([0, 1, 2, 3])
tensor([0., 0., 0., 0.])

{'x': tensor([0, 1, 2, 3]), 'y': tensor([0., 0., 0., 0.])}
  • 存储模型参数

torch.save(net.state_dict(),‘net.params’)

net.load_state_dict(torch.load(‘net.params’))

# 加载和保存模型参数
class MLP(nn.Module):
    def __init__(self):
        super().__init__()
        self.hidden = nn.Linear(20,256)
        self.output = nn.Linear(256,10)
    
    def forward(self, x):
        return self.output(F.relu(self.hidden(x)))
    
net = MLP()
X = torch.randn(size=(2,20))
Y = net(X)

# 将模型的参数存储为一个叫做"mlp.params"的文件
torch.save(net.state_dict(),'mlp.params')

# 实例化了原始多层感知机模型的一个备份。直接读取文件中存储的参数
clone = MLP() # 必须要先声明一下,才能导入参数
clone.load_state_dict(torch.load("mlp.params"))
print(clone.eval()) # eval()是进入测试模式

Y_clone = clone(X)
print(Y_clone == Y)

输出:
MLP(
  (hidden): Linear(in_features=20, out_features=256, bias=True)
  (output): Linear(in_features=256, out_features=10, bias=True)
)
tensor([[True, True, True, True, True, True, True, True, True, True],
        [True, True, True, True, True, True, True, True, True, True]])

2、卷积神经网络

卷积神经网络(Coyolutiomal Neural Ntwork)是含有卷积层的神经网络,卷积层的作用就是用来自动学习,提取图像的特征。
CNN网络主要由三部分构成:卷积层,池化层和全连接层构成:
卷积层负责提取图像中的局部特征:
池化层用来大幅降低参数量级(降维):
全连接层用来输出想要的结果

卷积计算:

为什么需要卷积

我们仅仅通过将图像数据展平成一维向量而忽略了每个图像的空间结构信息,再将数据送入一个全连接的多层感知机中。 因为这些网络特征元素的顺序是不变的,因此最优的结果是利用先验知识,即利用相近像素之间的相互关联性,从图像数据中学习得到有效的模型。

上图为游戏“Waldo 在哪里”的示例图,要求在一幅图片中找特定的对象。可以引申出两点:

  • 平移不变性(translation invariance):分类器不因出现位置改变而改变识别标准。
  • 局部性(locality):只需要在局部寻找对象,而非要远处无关区域。
  • 对全连接层使用平移不变性和局部性得到卷积层

卷积神经网络(convolutional neural network,CNN)是一类强大的、为处理图像数据而设计的神经网络。 卷积神经网络需要的参数少于全连接架构的网络,而且卷积也很容易用 GPU 并行计算。 因此卷积神经网络除了能够高效地采样从而获得精确的模型,还能够高效地计算。

从全连接层到卷积

回顾单隐藏层MLP

重新考察全连接层:

平移不变性

局部性

数学上的卷积运算

在进一步讨论之前,先简要回顾一下为什么上面的操作被称为“卷积”。在数学和信号处理领域中,两个函数( f , g : R d → R f,g:\mathbb{R}^d\rightarrow \mathbb{R} f,g:RdR)之间的卷积定义为:

也就是说,卷积是当把一个函数“翻转”并移位 x 时,测量 f 和 g 之间的重叠面积。以下动图展示的两个函数进行卷积计算的过程展示:

当为离散对象时,积分就变成求和。例如:对于由索引为 $ \mathbb{Z} $ 的、平方可和的、无限维向量集合中抽取的向量,我们得到以下定义:

对于二维张量,则为$ f 的索引 的索引 的索引 (a,b) 和 和 g 的索引 的索引 的索引 (i-a,j-b) $上对应加和:

上式形式与上文“局部性”得到的公式类似。

数学中,卷积与互相关十分类似,区别就是运算时有没有“翻转”的操作。下图所示卷积与交叉相关、自相关的区别:

在这里插入图片描述

图像的通道

这些通道有时也被称为特征映射(feature maps),因为每个通道都向后续层提供一组空间化的学习特征。 直观上你可以想象在靠近输入的底层,一些通道专门识别边缘,而一些通道专门识别纹理。

为了支持输入 X \mathbf{X} X和隐藏表示 H \mathbf{H} H中的多个通道,可以在 V \mathbf{V} V中添加第四个坐标 d,即 [ V ] a , b , c , d [V]_{a,b,c,d} [V]a,b,c,d,综上:

[ H ] i , j , d = ∑ a = − Δ Δ ∑ b = − Δ Δ ∑ c [ V ] a , b , c , d [ X ] i + a , j + b , c + u [\mathbf{H}]_{i,j,d}=\sum_{a=-\Delta}^\Delta\sum_{b=-\Delta}^\Delta \sum_c [\mathbf{V}]_{a,b,c,d}[\mathbf{X}]_{i+a,j+b,c}+u [H]i,j,d=a=ΔΔb=ΔΔc[V]a,b,c,d[X]i+a,j+b,c+u

其中隐藏表示 H \mathbf{H} H 中的索引 d d d 表示输出通道,而随后的输出将继续以三维张量 H \mathbf{H} H 作为输入进入下一个卷积层。 所以,上式可以定义具有多个通道的卷积层,而其中 V \mathbf{V} V 是该卷积层的权重。

总结

对全连接层使用平移不变性和局部性得到卷积层。

在深度学习研究社区中, V V V 被称为卷积核(convolution kernel)或者滤波器(filter),它仅仅是可学习的一个层的权重。 当图像处理的局部区域很小时,卷积神经网络与多层感知机的训练差异可能是巨大的:以前,多层感知机可能需要数十亿个参数来表示网络中的一层,而现在卷积神经网络通常只需要几百个参数,而且不需要改变输入或隐藏表示的维数。

参数大幅减少的代价是,我们的特征现在是平移不变的,并且当确定每个隐藏活性值时,每一层只包含局部的信息。 以上所有的权重学习都将依赖于归纳偏置。当这种偏置与现实相符时,我们就能得到样本有效的模型,并且这些模型能很好地泛化到未知数据中。 但如果这偏置与现实不符时,比如当图像不满足平移不变时,我们的模型可能难以拟合我们的训练数据。

卷积层

二维卷积层(二维交叉相关)

在这里插入图片描述

交叉相关 vs 卷积

  • 二维交叉相关

  • 二维卷积

由于对称性,在实际使用中没有区别。

一维和三维交叉相关

  • 一维

多用于文本、语言、时序序列

  • 三维(与 RGB 通道卷积表达式类似)

多用于含时间维度的视频、气象地图、空间维度医学图像(CT)

总结

  • 卷积层将输入和核矩阵进行交叉相关,加上偏移后得到输出
  • 核矩阵和偏移是可学习的参数
  • 核矩阵的大小是超参数,其大小控制卷积层的局部性

代码实现

  • 实现互相关运算
# 互相关运算
import torch
from torch import nn
from d2l import torch as d2l

def corr2d(X, K):   # X 为输入,K为核矩阵
    """计算二维互相关信息"""
    h, w = K.shape  # 核矩阵的行数和列数
    # 输出矩阵的形状
    Y = torch.zeros((X.shape[0] - h + 1, X.shape[1] - w + 1)) # X.shape[0]为输入高    
    for i in range(Y.shape[0]):
        for j in range(Y.shape[1]):
        # 从输入矩阵 X 中取出一个与核 K 同大小的小块区域;
        # 对应元素相乘;
        # 再把结果求和。
            Y[i, j] = (X[i:i + h, j:j + w] * K).sum() # 图片的小方块区域与卷积核做点积
    return Y

# 验证上述二维互相关运算的输出
# 输入图像3*3
X = torch.tensor([[0.0,1.0,2.0],[3.0,4.0,5.0],[6.0,7.0,8.0]])
# 卷积核2*2
K = torch.tensor([[0.0,1.0],[2.0,3.0]])
corr2d(X,K)

输出:
tensor([[19., 25.],
        [37., 43.]])

互相关运算的定义:将卷积核在输入图像上“滑动”,每次计算对应区域的加权和,得到输出特征图的过程。
它本质是局部加权求和,帮助提取图像的局部特征(如边缘、纹理)

  • 实现二维卷积层
# 实现二维卷积层
class Conv2D(nn.Module):
    def __init__(self, kernel_size):
        self.weight = nn.Parameter(torch.rand(kernel_size))
        self.bias = nn.Parameter(torch.zeros(1))
        
    def forward(Self, x):
        return corr2d(x, self.weight) + self.bias
    
# 卷积层的一个简单应用:检测图片中不同颜色的边缘
X = torch.ones((6,8))
X[:,2:6] = 0  # 把中间四列设置为0
print(X)  # 0 与 1 之间进行过渡,表示边缘

# 这就是卷积核,形状是 1×2,即[ 1  -1 ]
K = torch.tensor([[1.0,-1.0]])  # 如果左右原值相等,那么这两原值乘1和-1相加为0,则不是边缘
Y = corr2d(X, K)
print(Y)
# 通过对输入矩阵 X 做转置,再用同样的卷积核 [1, -1],实现对图像“水平边缘”的检测。
print(corr2d(X.t(), K)) # X.t() 为X的转置,而K卷积核只能检测垂直边缘

  • 通过梯度下降来学习核参数
# 学习由X生成Y的卷积核
conv2d = nn.Conv2d(1, 1, kernel_size=(1,2), bias=False) # 单个矩阵,输入通道为1,黑白图片通道为1,彩色图片通道为3。这里输入通道为1,输出通道为1.   
X = X.reshape((1,1,6,8)) # 通道维:通道数,RGB图3通道,灰度图1通道,批量维就是样本维,就是样本数
Y = Y.reshape((1,1,6,7))
for i in range(10):
    Y_hat = conv2d(X)
    l = (Y_hat - Y) ** 2
    conv2d.zero_grad()
    l.sum().backward()
    conv2d.weight.data[:] -= 3e-2 * conv2d.weight.grad # 3e-2是学习率
    if(i+1) % 2 == 0:
        print(f'batch {i+1},loss {l.sum():.3f}')

# 所学的卷积核的权重张量
print(conv2d.weight.data.reshape((1,2)))

Q&A

Q:卷积层的感受野(kernel size)不应该越大越好吗?为什么实际中常见的都是3x3、5x5等小尺寸?

�‍*♂️**:与 MLP 类似,很宽的隐藏层(非常多神经元)不如多层窄一些的隐藏层好训练,与之类似,在 CNN 中,尺寸很大的卷积层不如一组尺寸更小但层数更多的卷积层好训练。研究表明,由简单的神经元构成的多层结构,有助于将复杂特征的抽取任务“分而治之,层层击破”,先有底层卷积抽取低级特征(纹理、线条等),再通过层叠结构,越高层的卷积的间接作用范围会随着层数增长而逐渐扩大,最终获得大尺寸下的特征,这比一开始就学习一个复杂的特征或模式要更容易。

3、步幅和填充

填充和步幅

假设输入形状为 n h × n w n_h \times n_w nh×nw ,卷积核形状为 k h × k w k_h\times k_w kh×kw ,那么输出形状将是 ( n h − k h + 1 ) × ( n w − k w + 1 ) (n_h−k_h+1)\times(n_w−k_w+1) (nhkh+1)×(nwkw+1) 。 因此,卷积的输出形状取决于输入形状和卷积核的形状。

除了以上的两个因素外,填充(padding)步幅(stride) 也会影响大小

给定 ( 32 × 32 ) (32\times32) (32×32)像素的输入图像,应用 ( 5 × 5 ) (5\times5) (5×5)卷积核

  • 第$ 1 $层得到输出大小 28 × 28 28\times28 28×28
  • 第$ 7 $层得到输出大小 4 × 4 4\times4 4×4

更大的卷积核可以更快地减小输出大小:形状从 n k × n w n_k\times n_w nk×nw减小到 ( n h − n k + 1 ) × ( n w − k w + 1 ) (n_h-n_k+1)\times(n_w-k_w+1) (nhnk+1)×(nwkw+1),如果需要做更深的神经网络,那么就需要别的方法降低缩小作用

填充(Padding):增大输出尺寸

假如我们不想让输出变小(甚至扩大),一种方法可以在输入周围添加额外的行和列

在这里插入图片描述

如填充$ p_h 行和 行和 行和 p_w $列全 0 元素,则输出形状为:

通常取 p h = k h − 1 p_h=k_h-1 ph=kh1 p w = k w − 1 p_w=k_w-1 pw=kw1,使输入输出形状相同;

  • k h k_h kh为奇数:在上下左右各填充 p h / 2 p_h/2 ph/2
  • k h k_h kh为偶数:在上侧填充 ⌈ p h / 2 ⌉ \lceil p_h/2\rceil ph/2,下侧填充 ⌊ p h / 2 ⌋ \lfloor p_h/2\rfloor ph/2

步幅(Stride):减小输出尺寸

填充减小的输出大小与层数线性相关:

  • 给定输入大小 224 × 224 224\times 224 224×224,在使用 5 × 5 5\times5 5×5卷积核的情况下,需要 55 个卷积层
  • 需要大量计算才能得到较小的输出

在这里插入图片描述

步幅是指行、列的滑动步长。默认步幅为 1,就是卷积核依次一列列滑动,不进行跳跃

  • 给定高度$ s_h 和宽度 和宽度 和宽度 s_w $的步幅,输出形状是:

  • 如果$ p_h=k_h-1 , , p_w=k_w-1 $

⌊ ( n h + s h − 1 ) / s h ⌋ × ⌊ ( n w + s w − 1 ) / s w ⌋ \lfloor(n_h+s_h-1)/s_h\rfloor\times\lfloor(n_w+s_w-1)/s_w\rfloor ⌊(nh+sh1)/sh×⌊(nw+sw1)/sw

  • 如果输入高度和宽度可以被步幅整除(步幅与卷积核大小相当)

( n h / s h ) × ( n w / s w ) (n_h/s_h)\times(n_w/s_w) (nh/sh)×(nw/sw)

总结

  • 填充和步幅是卷积层的超参数;
  • 填充在输入周围添加额外的行、列,来控制输出形状的减少量;
  • 步幅是每次滑动核窗口时的行、列步长,可以成倍地减少输出形状。

代码实现

  • Padding填充
import torch
from torch import nn

def comp_conv2d(conv2d, X):  # conv2d 是一个卷积层对象
    # (1, 1) + X.shape   →  (1, 1, 8, 8)
    X = X.reshape((1, 1) + X.shape)  # 在维度前加上批量大小和通道数
    Y = conv2d(X)  # 调用卷积层进行计算,输出四维张量
    return Y.reshape(Y.shape[2:])  # 去掉前两维,只保留输出的高和宽

conv2d = nn.Conv2d(1, 1, kernel_size=3, padding=1)  # 定义卷积层:3×3卷积核,padding=1
X = torch.rand(size=(8, 8))  # 生成一个8×8的随机输入
print(comp_conv2d(conv2d, X).shape)  # torch.Size([8, 8])
# 填充不同的高度和宽度
# 输入通道数是1,输出通道数是1
# 卷积核的高=5,宽=3,填充高度方向(上下)加2行,宽度方向(左右)加一行
conv2d = nn.Conv2d(1, 1, kernel_size=(5, 3), padding=(2, 1))
print(comp_conv2d(conv2d,X).shape)  #torch.Size([8, 8])
  • Stride步幅
# 将高度和宽度的步幅设置为2
conv2d = nn.Conv2d(1, 1, kernel_size=3, padding=1, stride=2)
print(comp_conv2d(conv2d,X).shape)  #torch.Size([4, 4])
# 一个稍微复杂的例子
conv2d = nn.Conv2d(1, 1, kernel_size=(3, 5), padding=(0, 1), stride=(3, 4))
comp_conv2d(conv2d, X).shape   # torch.Size([2, 2])

4、卷积层通道

每个输入通道核识别并组合输入中的模式

每个输出通道可以识别特定的模式

类型 输入通道数 输出通道数 含义
单输入单输出 1 1 最简单的灰度卷积
多输入单输出 >1 1 融合多个输入通道的信息(如RGB)
单输入多输出 1 >1 用多个卷积核提取不同特征
多输入多输出 >1 >1 真实CNN中的卷积层,最常见

多输入 = 输入有多层(如RGB);
多输出 = 有多个卷积核,每个核输出一层结果;
CNN 卷积层 = 多输入、多输出的综合体

多个输入通道

通道数表示每个样本(图像)中有几层不同的“特征平面”。在图像里,它通常对应不同颜色层或特征层

图像类型 通道数 含义
灰度图 (黑白) 1 每个像素只有一个灰度值
RGB 彩色图 3 每个像素有红、绿、蓝三个通道
特征图 (深层卷积输出) 多个 每个通道代表一种特征(如边缘、纹理等)
  • 输入通道数 = 输入特征图的层数
  • 输出通道数 = 卷积核数量
    每个卷积核都会生成一个输出通道

彩色图像可能有 R、G、B 三个通道(有的格式如 PNG 还包含 4 通道,多一个透明度 Alpha 通道、RGB-D 图像多一个深度信息通道等),如果使用上一节单通道卷积层,要转换为灰度图,这样会丢失很多特征信息

当输入包含多个通道时,需要构造一个与输入数据具有相同输入通道数的卷积核,以便与输入数据进行互相关运算

为解决这个问题,我们可以给每个通道一个卷积核,输出是所有通道卷积结果的和。下图表示一个两通道输入的例子

在这里插入图片描述

下图表示在 RGB 三输入通道做卷积的过程:

输入:RGB (3个通道)
卷积核:3层(分别对R、G、B卷积)
输出:1个通道(R卷+G卷+B卷后相加)

多输入通道的意思是:输入不是单层图像,而是多层特征图
卷积核在每个通道分别卷积,然后把结果加起来,得到一个输出。

公式化表示:

其中,$ c_i $ 表示输入的通道维

多个输出通道-- 多个卷积核并行学习不同特征

在最流行的神经网络架构中,随着神经网络层数的加深,我们常会增加输出通道的维数,通过减少空间分辨率以获得更大的通道深度。直观地说,我们可以将每个通道看作是对不同特征的响应。而现实可能更为复杂一些,因为每个通道不是独立学习的,而是为了共同使用而优化的。因此,多输出通道并不仅是学习多个单通道的检测器。

多输出通道的意思是:卷积层里有多个卷积核,每个卷积核产生一个输出通道。

公式化表示:

  • 输入 X \bf X X c i × n h × n w c_i\times n_h\times n_w ci×nh×nw
  • W \bf W W c o × c i × k h × k w c_o\times c_i\times k_h\times k_w co×ci×kh×kw
  • 输出 Y \bf Y Y c o × m h × m w c_o\times m_h\times m_w co×mh×mw

1 X 1卷积层——多通道的全连接层

在 “1×1 卷积层” 里:

前一个 1 → 卷积核的“高度(height)”

后一个 1 → 卷积核的“宽度(width)”

说白了就是卷积核的大小1*1

假设输入是一个 3 通道的 RGB 图像,每个像素是:

[R, G, B]

1×1 卷积相当于在每个像素点上,
对这三个值进行一次加权求和(线性变换):

新像素 = 0.5*R + 0.3*G + 0.2*B

所以它:

  • 不改变图像的空间位置;
  • 只改变像素“通道的混合方式”

对每个像素的所有通道做一次线性组合

1 × 1 1\times 1 1×1卷积层,即$k_h=k_w=1 $看起来似乎没有多大意义,失去了卷积层的特的在高度和宽度维度上,识别相邻元素间相互作用的能力。但实际是一个受欢迎的选择,它不识别空间模式,只是融合输入通道的信息

以像素为基础应用时,1x1卷积层相当于全连接层

1x1卷积层通常用于调整网络层的通道数量和控制模型复杂度

二维卷积层

二维卷积层(Conv2d)是卷积神经网络(CNN)中最常用的层,它的输入一般是图像或特征图(Feature Map)。
二维卷积的核心思想是:用小卷积核在图像上滑动,通过加权求和提取局部特征

  • 输入 X \bf X X:(输入通道,高,宽)
  • 核$ \bf W $:(输出通道,输入通道,卷积核高,卷积核宽)
  • 偏差 B \bf B B c o × c i c_o\times c_i co×ci(每个输出通道一个偏置)
  • 输出 Y \bf Y Y:(输出通道,输出高,输出宽)

输出计算公式:

  • 计算复杂度(浮点计算数 FLOP):$ O(c_ic_ok_hk_wm_hm_w) $

c i = c o = 100 k h = h w = 5 → 1GFLOP m h = m w = 64 \begin{aligned} c_i&=c_o=100\\ k_h&=h_w=5 \qquad \rightarrow \text{1GFLOP}\\ m_h&=m_w=64 \end{aligned} cikhmh=co=100=hw=51GFLOP=mw=64

FLOPs=100×100×5×5×64×64=1.024×109≈1GFLOP

假设有 10 层这样的卷积神经网络,1M(百万)样本,则总的计算复杂度为 10PFlops,一般 CPU 计算能力为 0.15TFLOPS/s,该神经网络使用 CPU 前向计算时间则为 18h;一般 GPU 计算能力为 12TF/s,则 GPU 需要 14min。CNN 相当于用计算量换存储成本

总结

以 RGB 三通道输入、多输出为例:

  • 输出通道数是该层卷积层的超参数
  • 每个输入通道有独立的二维卷积核,所有通道结果相加得到一个输出通道结果
  • 每个输出通道有独立的三维卷积核
概念 含义
输出通道数 卷积层要生成几层输出特征图,由我们设定(超参数)
多输入通道 输入有多层(如RGB),每层卷积结果相加形成一个输出
多输出通道 每个输出通道有独立的三维卷积核(跨所有输入通道)
输入(3层):
 ├─ 红色通道
 ├─ 绿色通道
 └─ 蓝色通道
       ↓
多个卷积核(每个卷积核也是3层)
 ├─ 卷积核1 → 输出通道1(边缘特征)
 ├─ 卷积核2 → 输出通道2(亮度特征)
 └─ 卷积核3 → 输出通道3(纹理特征)
       ↓
输出(3层)

代码实现

  • 多输入多输出通道互相关运算
# 多输入通道互相关运算
import torch
from d2l import torch as d2l
from torch import nn

# 多通道输入单通道输出运算
def corr2d_multi_in(X,K):
    return sum(d2l.corr2d(x,k) for x,k in zip(X,K)) # X,K为3通道矩阵,for使得对最外面通道进行遍历        

X = torch.tensor([[[0.0,1.0,2.0],[3.0,4.0,5.0],[6.0,7.0,8.0]],
                  [[1.0,2.0,3.0],[4.0,5.0,6.0],[7.0,8.0,9.0]]])
K = torch.tensor([[[0.0,1.0],[2.0,3.0]],[[1.0,2.0],[3.0,4.0]]])
print(corr2d_multi_in(X,K))

# 多输出通道运算
def corr2d_multi_in_out(X,K):  # X为3通道矩阵,K为4通道矩阵,最外面维为输出通道      
    return torch.stack([corr2d_multi_in(X,k) for k in K],0) # 大k中每个小k是一个3D的Tensor。0表示stack堆叠函数里面在0这个维度堆叠。           
# (输入通道数,卷积核高度,卷积核宽度)
print(K.shape)
print((K+1).shape)
print((K+2).shape)
print(K)
print(K+1)
K = torch.stack((K, K+1, K+2),0) # K与K+1之间的区别为K的每个元素加1
# (输出通道数,输入通道数,卷积核高度,卷积核宽度)
print(K.shape)
print(corr2d_multi_in_out(X,K))
tensor([[ 56.,  72.],
        [104., 120.]])
torch.Size([2, 2, 2])
torch.Size([2, 2, 2])
torch.Size([2, 2, 2])
tensor([[[0., 1.],
         [2., 3.]],

        [[1., 2.],
         [3., 4.]]])
tensor([[[1., 2.],
         [3., 4.]],

        [[2., 3.],
         [4., 5.]]])
torch.Size([3, 2, 2, 2])
tensor([[[ 56.,  72.],
         [104., 120.]],

        [[ 76., 100.],
         [148., 172.]],

        [[ 96., 128.],
         [192., 224.]]])
  • 1x1 卷积
# 1×1卷积的多输入、多输出通道运算
def corr2d_multi_in_out_1x1(X,K):
    c_i, h, w = X.shape # 输入的通道数、宽、高
    c_o = K.shape[0]    # 输出的通道数
    X = X.reshape((c_i, h * w)) # 拉平操作,每一行表示一个通道的特征
    K = K.reshape((c_o,c_i)) 
    Y = torch.matmul(K,X) 
    return Y.reshape((c_o, h, w))

X = torch.normal(0,1,(3,3,3))   # norm函数生成0到1之间的(3,3,3)矩阵 
K = torch.normal(0,1,(2,3,1,1)) # 输出通道是2,输入通道是3,核是1X1
# 用矩阵乘法实现 1×1 卷积
Y1 = corr2d_multi_in_out_1x1(X,K)
# 用常规滑动卷积实现同样操作
Y2 = corr2d_multi_in_out(X,K)
# 检查两个结果是否几乎相同
assert float(torch.abs(Y1-Y2).sum()) < 1e-6
# 打印二者误差
print(float(torch.abs(Y1-Y2).sum()))   # 7.972121238708496e-07
  • 简明实现
def comp_conv2d(conv2d, X):
    X = X.reshape((1,1)+X.shape)  # reshape成(批量,通道,高,宽)
    Y = conv2d(X)                 # 进行卷积
    return Y.reshape(Y.shape[2:]) # 去掉批量和通道维度,只保留(高,宽)

X = torch.rand(size=(8,8))
conv2d = nn.Conv2d(1,1,kernel_size=3,padding=1,stride=2) # Pytorch里面卷积函数的第一个参数为输出通道,第二个参数为输入通道   
print(comp_conv2d(conv2d,X).shape)   #torch.Size([4, 4])

conv2d = nn.Conv2d(1,1,kernel_size=(3,5),padding=(0,1),stride=(3,4)) # 一个稍微复杂的例子
print(comp_conv2d(conv2d,X).shape)   # torch.Size([2, 2])

Q&A

Q:一般卷积的尺寸和输出通道该怎么设计?

�‍*♂️**:一般如果卷积使得原有输入高宽减半,那么需要将通道数增加为原来的 2 倍,以防止因压缩过多而丢失重要信息。可以近似看作压缩了空间尺度(高、宽),则需要更多的语义尺度(通道数)来表示提取的特征。

Q:卷积层中的 bias 对结果影响大吗?怎样理解 bias 的作用?

�‍*♂️**:bias 的作用相当于对数据的分布做平移,其实在后期随着各种归一化方法的使用(如 BatchNorm 等),bias 的作用越来越小,因为 bias 等价于输入数据均值的负数,虽然不要 bias 也可以,但其实计算成本来说可以忽略不计,加上也无妨。

5、池化层

池化层 (Pooling)

池化层通常放在卷积层之后。在卷积神经网络(CNN)中,池化层的主要作用是降低空间维度(如图像的宽度和高度),从而减少计算量和内存消耗,同时也有助于提取更抽象的特征。池化层一般用于以下几种情况:

  1. 卷积层之后:卷积层提取特征后,池化层通常跟随其后。池化操作可以帮助减少卷积后特征图的大小,进而减少后续网络层的计算量。
  2. 作为过渡层:在多个卷积层后,可以插入池化层,逐渐减少特征图的空间尺寸,以便更高层的网络能够专注于更加抽象的特征。

池化层通常有两种类型:最大池化(max pooling)和平均池化(average pooling),最大池化较常见,因为它能够保留最显著的特征。

二维最大池化

加入二维最大池化后:

  • 和卷积层类似,也有填充和步幅,但没有可学习的参数
  • 在每个输入通道应用池化层以获得相应的输出通道
  • 输出通道数=输入通道数
  • 输出每个窗口最强的信号

平均池化层

总结

  • 对于给定输入元素,最大池化层会输出该窗口内的最大值,平均池化层会输出该窗口内的平均值。
  • 池化层的主要优点之一是减轻卷积层对位置的过度敏感。
  • 我们可以指定池化层的填充和步幅、窗口大小
  • 使用最大池化层以及大于 1 的步幅,可减少空间维度(如高度和宽度)
  • 池化层的输出通道数与输入通道数相同

代码实现

  • 从零实现
import torch
from torch import nn
from d2l import torch as d2l

# 实现池化层的正向传播
def pool2d(X, pool_size, mode='max'):  # 拿到输入,池化窗口大小
    p_h, p_w = pool_size
    Y = torch.zeros((X.shape[0] - p_h + 1, X.shape[1] - p_w + 1)) # 输入的高减去窗口的高,再加上1,这里没有padding
    for i in range(Y.shape[0]): # 行遍历
        for j in range(Y.shape[1]): # 列遍历
            if mode == 'max':
                Y[i,j] = X[i:i + p_h, j:j + p_w].max()
            elif mode == 'avg':
                Y[i,j] = X[i:i + p_h, j:j + p_w].mean()
    return Y

# 验证二维最大池化层的输出
X = torch.tensor([[0.0,1.0,2.0],[3.0,4.0,5.0],[6.0,7.0,8.0]])
print(pool2d(X, (2,2)))

# 验证平均池化层
print(pool2d(X, (2,2), 'avg')) 

# 填充和步幅
X = torch.arange(16,dtype=torch.float32).reshape((1,1,4,4)) 
print(X)
pool2d = nn.MaxPool2d(3) # 深度学习框架中的步幅默认与池化窗口的大小相同,下一个窗口和前一个窗口没有重叠的
pool2d(X)

# 填充和步幅可以手动设定
pool2d = nn.MaxPool2d(3,padding=1,stride=2)
print(pool2d(X))

# 设定一个任意大小的矩形池化窗口,并分别设定填充和步幅的高度和宽度
pool2d = nn.MaxPool2d((2,3),padding=(1,1),stride=(2,3))
print(pool2d(X))

# 池化层在每个通道上单独运算
X = torch.cat((X,X+1),1)
print(X.shape) # 合并起来,变成了1X2X4X4的矩阵
print(X)

pool2d = nn.MaxPool2d(3,padding=1,stride=2)
print(pool2d(X))
tensor([[4., 5.],
        [7., 8.]])
tensor([[2., 3.],
        [5., 6.]])
tensor([[[[ 0.,  1.,  2.,  3.],
          [ 4.,  5.,  6.,  7.],
          [ 8.,  9., 10., 11.],
          [12., 13., 14., 15.]]]])
tensor([[[[ 5.,  7.],
          [13., 15.]]]])
tensor([[[[ 1.,  3.],
          [ 9., 11.],
          [13., 15.]]]])
torch.Size([1, 2, 4, 4])
tensor([[[[ 0.,  1.,  2.,  3.],
          [ 4.,  5.,  6.,  7.],
          [ 8.,  9., 10., 11.],
          [12., 13., 14., 15.]],

         [[ 1.,  2.,  3.,  4.],
          [ 5.,  6.,  7.,  8.],
          [ 9., 10., 11., 12.],
          [13., 14., 15., 16.]]]])
tensor([[[[ 5.,  7.],
          [13., 15.]],

         [[ 6.,  8.],
          [14., 16.]]]])

Q&A🤓

Q:为什么现在用池化层比较少?

�‍*♂️**:沐神认为,一方面现在多用卷积层加 stride 来减小计算量,另一方面目前也使用了许多数据增强(Argument)的方法,效果与池化类似。

6、LeNet

LeNet

LeNet的架构非常简单,但却能有效地提取图像特征。下面是LeNet-5的基本结构:

Input  —— 1×32×32
 ↓
C1: 卷积层(65×5 卷积核,stride=1) → 6×28×28
 ↓
S2: 平均池化(2×2) → 6×14×14
 ↓
C3: 卷积层(165×5 卷积核) → 16×10×10
 ↓
S4: 平均池化(2×2) → 16×5×5
 ↓
C5: 卷积变成全连接(1205×5×16 的卷积核) → 120
 ↓
F6: 全连接层 → 84
 ↓
Output: Softmax → 10
  1. 输入层
    • 输入图像尺寸为 32x32 像素(LeNet最初的设计是为更大的图像设计的,MNIST数据集经过预处理,图像尺寸为28x28,但通常会在模型中对图像进行填充以达到32x32)。
  2. 卷积层 C1
    • 卷积层:使用6个5x5的卷积核(滤波器),输出6个特征图,每个特征图的大小是28x28(32x32的输入减去5x5的卷积核尺寸,再加上1个步幅)。
    • 激活函数:使用Sigmoid激活函数(原始LeNet使用Sigmoid,后期的现代CNN更常用ReLU)。
  3. 池化层 S2
    • 最大池化:采用2x2的平均池化层,将每个特征图的尺寸减半,输出6个14x14的特征图(28x28经过2x2池化后变为14x14)。
  4. 卷积层 C3
    • 卷积层:使用16个5x5的卷积核进行卷积操作,输出16个特征图。此时,特征图的尺寸是10x10(14x14的输入经过5x5的卷积核后,尺寸变为10x10)。
    • 每个卷积核并不与所有的输入特征图进行卷积,而是对部分特征图进行卷积,增加了参数的稀疏性。
  5. 池化层 S4
    • 最大池化:再次进行2x2的池化,输出16个5x5的特征图(10x10经过池化后变为5x5)。
  6. 全连接层 C5
    • 全连接层:使用一个5x5的卷积核,输出120个节点。由于输入的特征图是5x5的,卷积核的大小恰好将其展平成一个单一的特征向量。
    • 这层相当于一个全连接层,但通过卷积的方式实现。
  7. 全连接层 F6
    • 全连接层:输出84个节点,使用Sigmoid激活函数。
  8. 输出层
    • 全连接层:最终输出10个类别,用于进行数字分类(如果是手写数字识别任务,输出层的大小为10,表示0到9的数字)

每个卷积块中的基本单元是一个卷积层、一个sigmoid 激活函数平均汇聚层。请注意,虽然 ReLU 和最大汇聚层更有效,但它们在 20 世纪 90 年代还没有出现。每个卷积层使用 ( 5 × 5 ) (5\times 5) (5×5)卷积核和一个 sigmoid 激活函数。这些层将输入映射到多个二维特征输出,通常同时增加通道的数量。第一卷积层有 6 个输出通道,而第二个卷积层有 16 个输出通道。每个 ( 2 × 2 ) (2\times2) (2×2)池操作(步骤 2)通过空间下采样将维数减少 4 倍。卷积的输出形状由批量大小、通道数、高度、宽度决定。

为了将卷积块的输出传递给稠密块,我们必须在小批量中展平每个样本。换言之,我们将这个四维输入转换成全连接层所期望的二维输入。这里的二维表示的第一个维度是索引小批量中的样本,第二个维度是给出每个样本的平面向量表示。LeNet 的稠密块有三个全连接层,分别有 120、84 和 10 个输出。因为我们在执行分类任务,所以输出层的 10 维对应于最后输出结果的数量。

总结

  • 是早期成功的神经网络。先使用卷积层来学习图片的空间信息,然后使用全连接层来转换到类别空间。
  • 使用卷积层提取特征,池化层减少特征图尺寸。
  • 使用全连接层进行最终的分类。
  • 较小的网络,适合小规模数据集(如MNIST)。
  • 激活函数最早使用Sigmoid,但现代的CNN架构通常使用ReLU激活函数。

代码实现

  • 定义网络
# LeNet(LeNet-5) 由两个部分组成:卷积编码器和全连接层密集块
import torch
from torch import nn
from d2l import torch as d2l

class Reshape(torch.nn.Module):
    def forward(self,x):
        return x.view(-1,1,28,28) # 批量数自适应得到,通道数为1,图片为28X28
    
net = torch.nn.Sequential(
    Reshape(),                                 # 1. 重新调整输入形状为 (batch, 1, 28, 28)
    nn.Conv2d(1, 6, kernel_size=5, padding=2), # 2. 卷积层1:输入1通道,输出6通道,5x5卷积核,padding=2
    nn.Sigmoid(),                              # 3. 激活函数
    nn.AvgPool2d(2, stride=2),                 # 4. 平均池化层1:2x2窗口,步幅2
    nn.Conv2d(6, 16, kernel_size=5),           # 5. 卷积层2:输入6通道,输出16通道,5x5卷积核
    nn.Sigmoid(),                              # 6. 激活函数
    nn.AvgPool2d(kernel_size=2, stride=2),     # 7. 平均池化层2:2x2窗口,步幅2
    nn.Flatten(),                              # 8. 展平成一维向量
    nn.Linear(16 * 5 * 5, 120),                # 9. 全连接层1:输入400,输出120
    nn.Sigmoid(),                              # 10. 激活函数
    nn.Linear(120, 84),                        # 11. 全连接层2:输入120,输出84
    nn.Sigmoid(),                              # 12. 激活函数
    nn.Linear(84, 10)                          # 13. 输出层:输入84,输出10(10类)
)

X = torch.rand(size=(1,1,28,28),dtype=torch.float32)
for layer in net:
    X = layer(X)
    print(layer.__class__.__name__,'output shape:\t',X.shape) # 上一层的输出为这一层的输入
Reshape output shape:	 torch.Size([1, 1, 28, 28])
Conv2d output shape:	 torch.Size([1, 6, 28, 28])
Sigmoid output shape:	 torch.Size([1, 6, 28, 28])
AvgPool2d output shape:	 torch.Size([1, 6, 14, 14])
Conv2d output shape:	 torch.Size([1, 16, 10, 10])
Sigmoid output shape:	 torch.Size([1, 16, 10, 10])
AvgPool2d output shape:	 torch.Size([1, 16, 5, 5])
Flatten output shape:	 torch.Size([1, 400])
Linear output shape:	 torch.Size([1, 120])
Sigmoid output shape:	 torch.Size([1, 120])
Linear output shape:	 torch.Size([1, 84])
Sigmoid output shape:	 torch.Size([1, 84])
Linear output shape:	 torch.Size([1, 10])
  • 训练
# 对evaluate_accuracy函数进行轻微的修改
def evaluate_accuracy_gpu(net, data_iter, device=None):
    """使用GPU计算模型在数据集上的精度"""
    if isinstance(net, torch.nn.Module):
        net.eval() # net.eval()开启验证模式,不用计算梯度和更新梯度
        # 如果未指定 device,则自动获取模型参数所在的设备(如 GPU 或 CPU)。
        if not device:
            device = next(iter(net.parameters())).device 
    # 创建一个累加器,metric[0]:累计正确预测的样本数,metric[1]:累计样本总数
    metric = d2l.Accumulator(2)
    for X, y in data_iter:
        if isinstance(X,list):
            X = [x.to(device) for x in X] # 如果X是个List,则把每个元素都移到device上
        else:
            X = X.to(device) # 如果X是一个Tensor,则只用移动一次,直接把X移动到device上
        y = y.to(device)
        metric.add(d2l.accuracy(net(X),y),y.numel()) # y.numel() 为y元素个数 
    return metric[0]/metric[1]
  • 训练函数
# 为了使用GPU,还需要一点小改动
def train_ch6(net, train_iter, test_iter, num_epochs, lr, device):
    # 对权重进行初始化
    def init_weights(m):
        if type(m) == nn.Linear or type(m) == nn.Conv2d:
            nn.init.xavier_uniform_(m.weight) # 根据输入、输出大小,使得随即初始化后,输入和输出的的方差是差不多的              
            
    net.apply(init_weights)
    print('training on',device)
    net.to(device)
    optimizer = torch.optim.SGD(net.parameters(),lr=lr)
    # 定义损失函数为交叉熵损失
    loss = nn.CrossEntropyLoss()
    animator = d2l.Animator(xlabel='epoch',xlim=[1,num_epochs],
                           legend=['train loss', 'train acc', 'test acc'])
    timer, num_batches = d2l.Timer(), len(train_iter)
    
    for epoch in range(num_epochs):
        # metric[0]:累计训练损失。metric[1]:累计正确预测的样本数。metric[2]:累计样本总数。
        metric = d2l.Accumulator(3)
        net.train()
        
        for i, (X,y) in enumerate(train_iter):
            timer.start()
            optimizer.zero_grad()
            X, y = X.to(device), y.to(device)
            y_hat = net(X)
            l = loss(y_hat, y)
            l.backward()
            optimizer.step()
            with torch.no_grad():
                # 累加当前批次的损失(l * X.shape[0])。累加当前批次的正确预测数(d2l.accuracy(y_hat, y))。
                # 累加当前批次的样本数(X.shape[0])。
                metric.add(l * X.shape[0], d2l.accuracy(y_hat,y),X.shape[0])                
            timer.stop()
            train_l = metric[0] / metric[2]
            train_acc = metric[1] / metric[2]
            
            if(i+1) % (num_batches//5) == 0 or i == num_batches - 1:
                animator.add(epoch + (i+1) / num_batches,
                            (train_l, train_acc, None))
        test_acc = evaluate_accuracy_gpu(net, test_iter)
        animator.add(epoch + 1, (None, None, test_acc))
        
    print(f'loss {train_l:.3f},train acc {train_acc:.3f},'
         f'test acc {test_acc:.3f}')
    print(f'{metric[2] * num_epochs / timer.sum():.1f} examples/sec'
         f'on{str(device)}')
# 训练和评估LeNet-5模型
lr, num_epochs = 0.9, 10
train_ch6(net, train_iter, test_iter, num_epochs, lr, d2l.try_gpu())

Q&A

Q:max pooling 和 average pooling 哪个用的更多?

�‍*♂️**:二者差别不大(可能在具体问题有细微差别),一般来说 max pooling 用的更多,因为 max pooling 得到的数值更大,相对梯度比 average 也个更大,更好训练。

7、AlexNet

机器学习发展历程

机器学习理论的发展

  • 2000 年前后:核方法,有一套完整的数学模型,如 SVM
  • 2000 年前后:几何学,把计算机视觉的问题描述成几何问题,如经典 CV 算法
  • 2010 前后: 特征工程:如何抽取图片的特征,如 SIFT、视觉词袋

计算机硬件的快速发展

摩尔定律展示了半导体技术进步带来的计算能力的突飞猛进。

互联网的发展带来数据量的增长

  • ImageNet(2010)

自然物体的彩色图: 469X387;
样本数:1.2M
类数:1000

AlexNet

Alex Krizhevsky、Ilya Sutskever 和 Geoff Hinton 提出了一种新的卷积神经网络变体 AlexNet。在 2012 年 ImageNet 挑战赛中取得了轰动一时的成绩。

  • 赢了 2012 年的 ImageNet 竞赛;
  • 更深更大的 LeNet;
  • 主要改进:
    • 丢弃法
    • ReLU(减缓梯度消失)
    • MaxPooling(增大输出值,带来更大的梯度)
    • 使用了数据增强(Data Arguments),包括随机裁剪,翻转,颜色扰动
  • 计算机视觉方法论的改变:从人工提取特征(SVM)到通过CNN学习获得特征,端到端学习;并且构造 CNN 简单高效——从原始数据(字符串、像素)到最终学习结果。

基本架构

Input: 3×224×224
 ↓
Conv1: 96×55×5511×11 卷积,stride=4)
 ↓
MaxPool
 ↓
Conv2: 256×27×275×5 卷积)
 ↓
MaxPool
 ↓
Conv3: 384×13×133×3 卷积)
 ↓
Conv4: 384×13×133×3 卷积)
 ↓
Conv5: 256×13×133×3 卷积)
 ↓
MaxPool
 ↓
Flatten
 ↓
FC6: 4096
 ↓
FC7: 4096
 ↓
FC8: 1000

  • AlexNet与LeNet架构对比

在这里插入图片描述

总结

  • AlexNet 是更大更深的 LeNet,10x 参数个数,260x 计算复杂度;
  • 新加入了丢弃法、LeRU、最大池化层和数据增强;
  • AlexNet 当赢下了 2012ImageNet 竞赛后,标志着新的一轮神经网络热潮的开始。

代码实现

  • 定义网络结构
# 深度卷积神经网络 (AlexNet)
import torch
from torch import nn
from d2l import torch as d2l

# 定义 AlexNet 网络结构
net = nn.Sequential(
    # 第一层卷积层:输入通道为 1(灰度图像),输出通道为 96,卷积核大小为 11×11,步幅为 4,填充为 1
    nn.Conv2d(1, 96, kernel_size=11, stride=4, padding=1), nn.ReLU(),
    # 第一层最大池化层:池化窗口大小为 3×3,步幅为 2
    nn.MaxPool2d(kernel_size=3, stride=2),
    
    # 第二层卷积层:输入通道为 96,输出通道为 256,卷积核大小为 5×5,填充为 2
    nn.Conv2d(96, 256, kernel_size=5, padding=2), nn.ReLU(),
    # 第二层最大池化层:池化窗口大小为 3×3,步幅为 2
    nn.MaxPool2d(kernel_size=3, stride=2),
    
    # 第三层卷积层:输入通道为 256,输出通道为 384,卷积核大小为 3×3,填充为 1
    nn.Conv2d(256, 384, kernel_size=3, padding=1), nn.ReLU(),
    # 第四层卷积层:输入通道为 384,输出通道为 384,卷积核大小为 3×3,填充为 1
    nn.Conv2d(384, 384, kernel_size=3, padding=1), nn.ReLU(),
    # 第五层卷积层:输入通道为 384,输出通道为 256,卷积核大小为 3×3,填充为 1
    nn.Conv2d(384, 256, kernel_size=3, padding=1), nn.ReLU(),
    # 第三层最大池化层:池化窗口大小为 3×3,步幅为 2
    nn.MaxPool2d(kernel_size=3, stride=2),
    
    # 展平层:将多维特征图展平成一维向量
    nn.Flatten(),
    # 第一层全连接层:输入大小为 6400,输出大小为 4096
    nn.Linear(6400, 4096), nn.ReLU(),
    # Dropout 层:随机丢弃 50% 的神经元,防止过拟合
    nn.Dropout(p=0.5),
    # 第二层全连接层:输入大小为 4096,输出大小为 4096
    nn.Linear(4096, 4096), nn.ReLU(),
    # Dropout 层:随机丢弃 50% 的神经元
    nn.Dropout(p=0.5),
    # 输出层:输入大小为 4096,输出大小为 10(对应 10 个类别)
    nn.Linear(4096, 10)
)

# 创建一个随机输入张量,形状为 (1, 1, 224, 224)
# 1 表示批量大小,1 表示通道数(灰度图像),224×224 表示图像分辨率
X = torch.randn(1, 1, 224, 224)

# 遍历网络中的每一层,将输入 X 依次传入,打印每一层的输出形状
for layer in net:
    X = layer(X)
    print(layer.__class__.__name__, 'Output shape:\t', X.shape)
输出:

Conv2d Output shape:	 torch.Size([1, 96, 54, 54])
ReLU Output shape:	 torch.Size([1, 96, 54, 54])
MaxPool2d Output shape:	 torch.Size([1, 96, 26, 26])
Conv2d Output shape:	 torch.Size([1, 256, 26, 26])
ReLU Output shape:	 torch.Size([1, 256, 26, 26])
MaxPool2d Output shape:	 torch.Size([1, 256, 12, 12])
Conv2d Output shape:	 torch.Size([1, 384, 12, 12])
ReLU Output shape:	 torch.Size([1, 384, 12, 12])
Conv2d Output shape:	 torch.Size([1, 384, 12, 12])
ReLU Output shape:	 torch.Size([1, 384, 12, 12])
Conv2d Output shape:	 torch.Size([1, 256, 12, 12])
ReLU Output shape:	 torch.Size([1, 256, 12, 12])
MaxPool2d Output shape:	 torch.Size([1, 256, 5, 5])
Flatten Output shape:	 torch.Size([1, 6400])
Linear Output shape:	 torch.Size([1, 4096])
ReLU Output shape:	 torch.Size([1, 4096])
Dropout Output shape:	 torch.Size([1, 4096])
Linear Output shape:	 torch.Size([1, 4096])
ReLU Output shape:	 torch.Size([1, 4096])
Dropout Output shape:	 torch.Size([1, 4096])
Linear Output shape:	 torch.Size([1, 10])
  • 训练
# Fashion-MNIST图像的分辨率低于ImageNet图像。将它们增加到224×224
batch_size = 128  # 设置批量大小为 128

# 加载 Fashion-MNIST 数据集
# resize=224:将原始 28×28 的图像调整为 224×224,以适配 AlexNet 的输入要求
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size, resize=224)

# 设置学习率和训练轮数
lr, num_epochs = 0.01, 10

d2l.train_ch6(net, train_iter, test_iter, num_epochs, lr, d2l.try_gpu())


Q&A

Q:使用GPU训练AlexNet时,报错CUDA error: CUBLAS_STATUS_NOT_INITIALIZED when calling cublasCreate(handle)是什么原因?

�‍*♂️**:一般是显卡显存不够了,可以尝试将batch_size调小一些试试。

Q:一般CNN要求输入图像是固定尺寸,实际应用中,数据尺寸不一,会怎样处理?强行resize吗?

�‍*♂️**:一般不会强行resize,否则会改变图像特征,而是保持长宽比不变的resize,在其中crop出符合要求的尺寸来。

8、使用块的网络VGG

构想缘由——模块化思想

因为 AlexNet 比 LeNet 更深更大而获得更好的精度,那么能不能更深更大?有以下几种途径:

更多的全连接层(占存储空间,成本高)

更多的卷积层(不好标准化)

将卷积层组合成快 √

虽然 AlexNet 证明深层神经网络卓有成效,但它没有提供一个通用的模板来指导后续的研究人员设计新的网络。

与新能源汽车模块化生产、芯片设计中工程师从放置晶体管到逻辑元件再到逻辑块的过程类似,神经网络架构的设计也逐渐变得更加抽象。研究人员开始从单个神经元的角度思考问题,发展到整个层,现在又转向块,重复层的模式。

使用块的想法首先出现在牛津大学的VGG 网络中。通过使用循环和子程序,可以很容易地在任何现代深度学习框架的代码中实现这些重复的架构。

进度:

VGG 块

每个 VGG 块由以下组件组成:

  • n 个 3x3 卷积层,填充 padding=1(可重复 n 层,m 通道,输入通道等于输出通道)
  • 2x2 最大池化层(步幅 stride=2)

作者实验证明,更深的 3x3 效果好于浅的 5x5;由于有步幅的存在,每个块的输出尺寸减半,一般使用时每个块设定使通道数翻倍,空间尺寸减半。

VGG 架构

AlexNet比LeNet更深更大来得到更好的精度

将多个 VGG 块串连后接全连接层,不同次数的重复块得到不同的架构,如:VGG-16,VGG-19……

在这里插入图片描述

总结

VGG使用可重复使用的卷积块来构建深度卷积神经网络
不同的卷积块个数和超参数可以得到不同复杂度的变种

代码实现

  • 实现 VGG 块
import torch
from torch import nn
from d2l import torch as d2l

# 定义 VGG 块
def vgg_block(num_convs, in_channels, out_channels): 
    """
    VGG 块由多个卷积层和一个最大池化层组成。
    参数:
    - num_convs: 卷积层的数量
    - in_channels: 输入通道数
    - out_channels: 输出通道数
    """
    layers = []
    for _ in range(num_convs):  # 添加指定数量的卷积层
        layers.append(nn.Conv2d(in_channels, out_channels, kernel_size=3, padding=1))  # 3x3卷积核,保持输入大小
        layers.append(nn.ReLU())  # 激活函数
        in_channels = out_channels  # 更新输入通道数为当前输出通道数
    layers.append(nn.MaxPool2d(kernel_size=2, stride=2))  # 添加最大池化层,窗口大小为2x2,步幅为2
    return nn.Sequential(*layers)  # 使用 nn.Sequential 将层组合成一个块

  • 实现VGG11
# 定义 VGG 的卷积架构
"""
conv_arch 是一个元组列表,每个元组包含:
- 第一个元素: 卷积层的数量
- 第二个元素: 输出通道数
"""
conv_arch = ((1, 64), (1, 128), (2, 256), (2, 512), (2, 512))

# 定义 VGG 网络
def vgg(conv_arch):
    """
    根据指定的卷积架构构建 VGG 网络。
    参数:
    - conv_arch: 卷积架构,包含每块的卷积层数量和输出通道数
    """
    conv_blks = []  # 存储每个 VGG 块
    in_channels = 1  # 输入通道数(灰度图像为1)
    for (num_convs, out_channels) in conv_arch:  # 遍历每个块的配置
        conv_blks.append(vgg_block(num_convs, in_channels, out_channels))  # 添加 VGG 块
        in_channels = out_channels  # 更新输入通道数为当前块的输出通道数

    # 将卷积块与全连接层组合成完整的 VGG 网络
    return nn.Sequential(
        *conv_blks,  # 添加所有卷积块
        nn.Flatten(),  # 展平层,将多维特征图展平成一维向量
        nn.Linear(out_channels * 7 * 7, 4096), nn.ReLU(),  # 全连接层1
        nn.Dropout(0.5),  # Dropout 防止过拟合
        nn.Linear(4096, 4096), nn.ReLU(),  # 全连接层2
        nn.Dropout(0.5),  # Dropout 防止过拟合
        nn.Linear(4096, 10)  # 输出层,10 个类别
    )

# 创建 VGG 网络
net = vgg(conv_arch)
  • 检查各层输出尺寸
# 测试网络,观察每个层的输出形状
X = torch.randn(size=(1, 1, 224, 224))  # 创建一个随机输入张量,形状为 (1, 1, 224, 224)
for blk in net:  # 遍历网络中的每一层
    X = blk(X)  # 将输入传递给当前层
    print(blk.__class__.__name__, 'output shape:\t', X.shape)  # 打印层的类型和输出形状
Sequential output shape:	 torch.Size([1, 64, 112, 112])
Sequential output shape:	 torch.Size([1, 128, 56, 56])
Sequential output shape:	 torch.Size([1, 256, 28, 28])
Sequential output shape:	 torch.Size([1, 512, 14, 14])
Sequential output shape:	 torch.Size([1, 512, 7, 7])
Flatten output shape:	 torch.Size([1, 25088])
Linear output shape:	 torch.Size([1, 4096])
ReLU output shape:	 torch.Size([1, 4096])
Dropout output shape:	 torch.Size([1, 4096])
Linear output shape:	 torch.Size([1, 4096])
ReLU output shape:	 torch.Size([1, 4096])
Dropout output shape:	 torch.Size([1, 4096])
Linear output shape:	 torch.Size([1, 10])

第一个输出“Sequential output shape: torch.Size([1, 64, 112, 112])“表示:

  • 经过第一个卷积块后,输出的张量形状为 (1, 64, 112, 112)。
  • 1:批量大小(输入的样本数量)。
  • 64:输出通道数(第一个卷积块的输出通道数)。
  • 112×112:图像的高和宽,经过卷积和池化后从 224×224 减半。
  • 训练
# 由于VGG-11比AlexNet计算量更大,因此构建了一个通道数较少的网络
ratio = 4
small_conv_arch = [(pair[0], pair[1]//ratio) for pair in conv_arch] # 所有输出通道除以4
net = vgg(small_conv_arch)

lr, num_epochs, batch_size = 0.05, 10, 128
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size,resize=224)    
d2l.train_ch6(net,train_iter,test_iter,num_epochs,lr,d2l.try_gpu())

分析结果可知,对比AlexNet精度更高,计算速度慢了将近1倍

import os
# 显式设置数据存储路径
os.environ['D2L_DATA_DIR'] = '/root/autodl-tmp/data'

# 然后执行您的代码
ratio = 4
small_conv_arch = [(pair[0], pair[1]//ratio) for pair in conv_arch]
net = vgg(small_conv_arch)

lr, num_epochs, batch_size = 0.05, 10, 128
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size, resize=224)    
d2l.train_ch6(net, train_iter, test_iter, num_epochs, lr, d2l.try_gpu())

9、网络中的网络NiN

全连接层的问题

LeNet、AlexNet 和 VGG 都有一个共同的设计模式:通过一系列的卷积层与池化层来提取空间结构特征;然后通过全连接层对特征的表征进行处理。 AlexNet 和 VGG 对 LeNet 的改进主要在于如何扩大和加深这两个模块。而全连接层存在以下问题:

  1. 相比卷积层,全连接层参数存储空间大得多,同时占用很大的计算带宽

$ c_i 表示输入通道数, 表示输入通道数, 表示输入通道数, c_o 表示输出通道数, 表示输出通道数, 表示输出通道数, k $表示卷积核尺寸,

卷积层需要较少的参数: c i × c o × k 2 c_i\times c_o\times k^2 ci×co×k2
以卷积层后的第一个全连接层的参数大小为例
* LeNet: 16 × 5 × 5 × 120 = 48 k 16\times5\times5\times120=48k 16×5×5×120=48k
* AlexNet: 256 × 5 × 5 × 4096 = 26 M 256\times5\times5\times4096=26M 256×5×5×4096=26M
* VGG: 512 × 7 × 7 × 4096 = 102 M 512\times7\times7\times4096=102M 512×7×7×4096=102M

  1. 大尺寸的全连接层很容易引起过拟合问题
  2. 使用了全连接层,相当于放弃特征的空间结构

NiN

全局平均池化 (GAP) 的作用与理由:

  1. 参数更少
    • 传统:最后特征图 C×H×W → 拉平成 C·H·W,再接巨大全连接层,参数 = C·H·W·num_classes。
    • GAP:对每个通道做平均 → 得到 C 个数,再直接作为类别分数(或再接一个小层),几乎不引入新参数。
  2. 降低过拟合
    • 去掉大规模全连接层后,模型无法记住大量细碎的空间模式,泛化更好。
  3. 保留空间到通道的对应关系
    • 最后一层的通道可以视作“类别响应图”,平均后即该类别整体响应,更容易做可解释(类激活图 CAM 的前提)。
  4. 输入尺寸更灵活
    • 只要前面卷积能跑,最后无论 H、W 是多少,平均成 1×1;不再要求固定尺寸给全连接。
  5. 规避超大中间向量的内存与计算
    • 不需要构造巨大的 Flatten 向量,提高效率。
  6. 作为一种正则化
    • 强制网络在整张图上聚合统计,不依赖某些局部孤立高激活点。
  7. 与 1×1 卷积配合自然
    • 最后用 1×1 卷积把通道变成“类别数”,再 GAP → 直接得到 logits。
  8. 适合部署
    • 模型体积更小,移动端/嵌入式友好。

简单对比示例(假设最后特征图为 256×7×7,类别数 10):

  • 全连接:参数约 = 256*_7*_7*10 = 125,440
  • GAP:仅对每通道求平均,新增参数≈0(或再接一个很小的线性层)。

网络中的网络(NiN)提供了一个非常简单的解决方案:在每个像素的通道上分别使用多层感知机来增强卷积层的表达能力,并在网络末端通过全局平均池化取代传统的全连接层

  • 使用 1×1 卷积 在卷积层内部实现小型多层感知机(MLP-Conv),增强特征表示能力
  • 使用 全局平均池化(GAP) 完全取代最终的全连接层 → 大幅减少参数、降低过拟合
  • 保留空间结构 → 泛化能力更好

NiN 块

NiN 块 = 卷积层 + 两个 1×1 卷积层(每个都带激活函数)

┌───────────────┐
│   Conv(k×k)   │ ← 大卷积(提取空间特征)
└───────────────┘
         │
       ReLU
         │
┌───────────────┐
│   Conv(1×1)   │ ← 小卷积(相当于一个小 MLP)
└───────────────┘
         │
       ReLU
         │
┌───────────────┐
│   Conv(1×1)   │ ← 再来一个小 MLP
└───────────────┘
         │
       ReLU
         │
       输出

一个卷积层后跟两个全连接层(即$ (1 \times 1) 卷积层,用于混合通道,可参考卷积层通道一节内容),每个 卷积层,用于混合通道,可参考卷积层通道一节内容),每个 卷积层,用于混合通道,可参考卷积层通道一节内容),每个 (1 \times 1) $卷积层:

  • 步幅 stride=1,无填充,通道数等于卷积层通道数,输出形状跟卷积层输出一样,不改变输出尺寸与通道数
  • 起到全连接层的作用
  • 对每个像素增加了非线性性

上图为 ( 1 × 1 ) (1 \times 1) (1×1)卷积层老示例图仅供参考。对应 NiN 块,输入三通道时,则卷积层也需要输出 3 通道而不是上图的的 2 通道

NiN 架构

  • 无全连接层
  • 交替使用 NiN 块和步幅为 2 的最大池化层
    • 逐步减小高宽和增大通道数
  • 最后使用全局平均池化层得到输出替代 AlexNet、VGG 的全连接层
    • 其输入通道数是类别数
    • 从每个通道拿出一个值,作为对其类比的预测,再求 softmax
    • 减小全连接层过拟合问题,减少参数个数,降低存储空间使用
Input
  │
  ▼
────────── NiN Block 1 ──────────
 Conv 5×5
 1×1 Conv
 1×1 Conv
  ↓
 Max Pooling

────────── NiN Block 2 ──────────
 Conv 5×5
 1×1 Conv
 1×1 Conv
  ↓
 Max Pooling

────────── NiN Block 3 ──────────
 Conv 3×3
 1×1 Conv
 1×1 Conv
  ↓
 Global Average Pooling (GAP)

Output (10)

总结:

  • NiN块使用卷积层加两个1X1卷积层,后者对每个像素增加了非线性
  • NiN使用全局平均池化层(GAP)来代替VGG和AlexNet中的全连接层
    • 不容易过拟合,更少的参数个数

代码实现

  • 实现 NiN Block
# NiN 网络:使用 1×1 卷积堆叠形成局部“多层感知机”并用全局平均池化替代全连接
import torch
from torch import nn
from d2l import torch as d2l

def nin_block(in_channels, out_channels, kernel_size, stride, padding):
    return nn.Sequential(
        # 基础卷积:提取局部空间特征
        nn.Conv2d(in_channels, out_channels, kernel_size, stride, padding),
        nn.ReLU(),
        # 两层 1×1 卷积:在同一空间位置上做通道级的非线性组合
        # 在每个空间位置上只做通道之间的线性组合,相当于对该像素的通道向量做“全连接”再加非线性。
        nn.Conv2d(out_channels, out_channels, kernel_size=1),
        nn.ReLU(),
        nn.Conv2d(out_channels, out_channels, kernel_size=1),
        nn.ReLU()
    )
  • 定义 NiN 网络(以 AlexNet 为模板)
net = nn.Sequential(
    nin_block(1, 96, kernel_size=11, stride=4, padding=0),   # 输入是灰度图(1通道) → 96通道特征
    nn.MaxPool2d(3, stride=2),                               # 下采样:高宽减小
    nin_block(96, 256, kernel_size=5, stride=1, padding=2),
    nn.MaxPool2d(3, stride=2),
    nin_block(256, 384, kernel_size=3, stride=1, padding=1),
    nn.MaxPool2d(3, stride=2),
    nn.Dropout(0.5),                                         # 随机丢弃部分通道,防止过拟合
    # 最后一块输出通道设为类别数(10),后续做全局平均池化
    nin_block(384, 10, kernel_size=3, stride=1, padding=1),
    # 自适应全局平均池化到 (1,1),使输入尺寸可变
    nn.AdaptiveAvgPool2d((1, 1)),
    nn.Flatten()  # 输出形状 (batch, 10)
)
  • 测试各层输出尺寸
# 权重初始化(可选)
def init_weights(m):
    if isinstance(m, nn.Conv2d):
        nn.init.xavier_uniform_(m.weight)
net.apply(init_weights)

# 测试各层输出形状
X = torch.rand(1, 1, 224, 224)
for layer in net:
    X = layer(X)
    print(f"{layer.__class__.__name__} output shape:\t{X.shape}")

输出:

Sequential output shape:	 torch.Size([1, 96, 54, 54])
MaxPool2d output shape:	 torch.Size([1, 96, 26, 26])
Sequential output shape:	 torch.Size([1, 256, 26, 26])
MaxPool2d output shape:	 torch.Size([1, 256, 12, 12])
Sequential output shape:	 torch.Size([1, 384, 12, 12])
MaxPool2d output shape:	 torch.Size([1, 384, 5, 5])
Dropout output shape:	 torch.Size([1, 384, 5, 5])
Sequential output shape:	 torch.Size([1, 10, 5, 5])
AdaptiveAvgPool2d output shape:	 torch.Size([1, 10, 1, 1])
Flatten output shape:	 torch.Size([1, 10])
  • 训练
# 训练模型
lr, num_epochs, batch_size = 0.1, 10, 128
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size, resize=224)    
d2l.train_ch6(net, train_iter, test_iter, num_epochs, lr, d2l.try_gpu())

根据结果对比 AlexNet 可知,由于有多个 1x1 卷积存在,训练速度要比 AlexNet 慢,而且精度没有后者高(可能数据集过小)

Q&A

Q:为什么最近几个模型做分类问题,在网络定义中都没看到 Softmax 层?

�‍*♂️**:一般现有深度学习框架为避免反向传播过程中可能会困扰我们的数值稳定性问题,大都将 softmax 和交叉熵损失函数结合在一起。详细内容可参考 👉这里

10、含并行连结的网络GoogLeNet

问题的引出

在 2014 年的 ImageNet 图像识别挑战赛中,一个名叫 GoogLeNet 的网络架构大放异彩。 GoogLeNet 吸收了 NiN 中串联网络的思想,并在此基础上做了改进。 这篇论文的一个重点是解决了什么样大小的卷积核最合适的问题。 毕竟,以前流行的网络使用小到 1×1 ,大到 11×11 的卷积核。 该文的一个观点是,有时使用不同大小的卷积核组合是有利的。

Inception 块

在 GoogLeNet 中,基本的卷积块被称为Inception 块(Inception block)。这很可能得名于电影《盗梦空间》(Inception),因为电影中的一句话“我们需要走得更深”(“We need to go deeper”)。

Inception 块使用用 4 个路径从不同层面抽取信息,然后在输出通道维合并。

在这里插入图片描述

  • 输出和输入等高宽,将 4 条路径的结果按通道合并
  • 1x1 卷积层:进行通道融合,降低通道数来控制模型复杂度
  • 非 1x1 的卷积层、MaxPooling:提取空间特性,增加鲁棒性
  • 每条路径的通道数可能不一样

为什么 1×1 卷积这么重要?有三个作用:

  1. 降维(Reduction)
    • 输入通道很多,比如 256 通道
    • 如果直接做 5×5 卷积,计算量 ~ 5×5×输入通道×输出通道
    • 先用 1×1 把 256 压到 32,再做 5×5,计算量直接降很多。
  2. 增加非线性
    • 1×1 卷积后接 ReLU,相当于对通道间做一个线性变换 + 非线性
    • 提升表达能力。
  3. 跨通道信息交互
    • 1×1 卷积在空间上不“看邻居”,但会线性组合不同通道
    • 这相当于在“通道维”上做一次“全连接”。

Inception 的优势

跟单 3x3 或 5x5 卷积层相比,输出相同通道数,Inception 块只需要更少的参数个数和计算复杂度.

结构(layer type) #parameters(参数量) FLOPS(计算量)
Inception 0.16 M(16万) 128 M
3×3 Conv 0.44 M(44万) 346 M
5×5 Conv 1.22 M(122万) 963 M

FLOPS,Floating-point Operations Per Second

GoogLeNet

包含 5 Stages, 9 Inception 块

  • 架构图

  • 各层信息

Inception 变种

  • Inception-BN(V2):使用了 batch normalization
  • Inception-V3:修改了 Inception
    • 替换5x5为多个3x3卷积层
    • 替换5x5为1x7和7x1卷积层
    • 替换3x3为1x3和3x1卷积层
    • 更深
  • Inception-V4:使用残差连接

总结

  • Inception 块使用 4 条有不同超参数的卷积层和池化层的通路来抽取不同的信息
    • 一个主要优点是模型参数小,计算复杂度低
  • GooLeNet 用了 9 个 Inception 块,是第一个达到上百层的网络
    • 后续有一系列改进变种

代码实现

  • 实现 Inception 块
# Inception 模块: 通过并行的 1x1, 3x3, 5x5 卷积以及 3x3 池化+1x1 卷积的分支实现多尺度特征抽取
# 参数说明:
#   in_channels: 输入特征图的通道数
#   c1: 第一条路(单独1x1卷积)的输出通道数
#   c2: 第二条路的 (1x1 降维卷积, 3x3 卷积) 的输出通道配置, 形式为 (减维后通道, 3x3输出通道)
#   c3: 第三条路的 (1x1 降维卷积, 5x5 卷积) 的输出通道配置, 形式为 (减维后通道, 5x5输出通道)
#   c4: 第四条路 (3x3 最大池化 + 1x1 卷积) 中 1x1 卷积的输出通道数
# **kwargs: 允许向 nn.Module 传入其它关键字参数(例如名称等), 在这里通常保持默认即可
import torch
from torch import nn
from torch.nn import functional as F
from d2l import torch as d2l

class Inception(nn.Module):
    def __init__(self, in_channels, c1, c2, c3, c4, **kwargs):
        # super 调用父类构造函数, 以保证模块正确注册
        super(Inception, self).__init__(**kwargs)
        # 第一条路径: 1x1 卷积(保持空间尺寸, 仅调整/抽取通道信息)
        self.p1_1 = nn.Conv2d(in_channels, c1, kernel_size=1)
        # 第二条路径: 先 1x1 降维再 3x3 卷积(在保持较低计算成本的前提下提取局部空间特征)
        self.p2_1 = nn.Conv2d(in_channels, c2[0], kernel_size=1)
        self.p2_2 = nn.Conv2d(c2[0], c2[1], kernel_size=3, padding=1)  # padding=1 保持特征图尺寸不变
        # 第三条路径: 先 1x1 降维再 5x5 卷积(更大感受野, 通过降维减少参数)
        self.p3_1 = nn.Conv2d(in_channels, c3[0], kernel_size=1)
        self.p3_2 = nn.Conv2d(c3[0], c3[1], kernel_size=5, padding=2)  # padding=2 使 5x5 后尺寸不变
        # 第四条路径: 3x3 最大池化 (保持尺寸) + 1x1 卷积(再次抽取并融合池化后的信息)
        self.p4_1 = nn.MaxPool2d(kernel_size=3, stride=1, padding=1)  # stride=1 + padding=1 保持尺寸
        self.p4_2 = nn.Conv2d(in_channels, c4, kernel_size=1)

    def forward(self, x):
        # 每条路径分别计算并使用 ReLU 激活
        p1 = F.relu(self.p1_1(x))               # 第一分支输出
        p2 = F.relu(self.p2_2(F.relu(self.p2_1(x))))  # 第二分支: 1x1 -> ReLU -> 3x3 -> ReLU
        p3 = F.relu(self.p3_2(F.relu(self.p3_1(x))))  # 第三分支: 1x1 -> ReLU -> 5x5 -> ReLU
        p4 = F.relu(self.p4_2(self.p4_1(x)))    # 第四分支: 池化后接 1x1 卷积 + ReLU
        # 在通道维 (dim=1) 拼接四条路径的结果形成输出
        return torch.cat((p1, p2, p3, p4), dim=1)
  • 定义 Stage1 结构:使用 64 个通道、 7×7 卷积层
# 第一阶段 (b1): 大核 7x7 卷积抽取初始空间特征 + 最大池化降采样
b1 = nn.Sequential(
    nn.Conv2d(1, 64, kernel_size=7, stride=2, padding=3),  # 输出通道 64, 空间尺寸约减半
    nn.ReLU(),
    nn.MaxPool2d(kernel_size=3, stride=2, padding=1)        # 再次降采样
)
  • 定义 Stage2 结构:第一个卷积层是 64 个通道、 1×1 卷积层;第二个卷积层使用将通道数量增加三倍的 3×3 卷积层
# 第二阶段 (b2): 1x1 降维 + 3x3 卷积扩展表征能力, 后接池化
b2 = nn.Sequential(
    nn.Conv2d(64, 64, kernel_size=1),  # 轻量通道变换
    nn.ReLU(),
    nn.Conv2d(64, 192, kernel_size=3, padding=1),  # 3x3 抽取局部特征
    nn.MaxPool2d(kernel_size=3, stride=2, padding=1)        # 降采样
)
  • 定义 Stage3 结构:第一个 Inception 块的输出通道数为 64+128+32+32=256。第二个 Inception 块的输出通道数增加到 128+192+96+64=480
# 第三阶段 (b3): 两个 Inception 模块 + 池化
# Inception(192,64,(96,128),(18,32),32) 输出通道: 64 + 128 + 32 + 32 = 256
# Inception(256,128,(128,192),(32,96),64) 输出通道: 128 + 192 + 96 + 64 = 480
b3 = nn.Sequential(
    Inception(192, 64, (96, 128), (18, 32), 32),
    Inception(256, 128, (128, 192), (32, 96), 64),
    nn.MaxPool2d(kernel_size=3, stride=2, padding=1)
)
  • 定义 Stage4 结构:串联 5 个 Inception 块
# 第四阶段 (b4): 多个 Inception 模块堆叠, 通道数逐渐增大
# 依次输出通道总数: 480->512->512->528->832
b4 = nn.Sequential(
    Inception(480, 192, (96, 208), (16, 48), 64),   # 输出: 192+208+48+64 = 512
    Inception(512, 160, (112, 224), (24, 64), 64),  # 输出: 160+224+64+64 = 512
    Inception(512, 128, (128, 256), (24, 64), 64),  # 输出: 128+256+64+64 = 512
    Inception(512, 112, (144, 288), (32, 64), 64),  # 输出: 112+288+64+64 = 528
    Inception(528, 256, (160, 320), (32, 128), 128),# 输出: 256+320+128+128 = 832
    nn.MaxPool2d(kernel_size=3, stride=2, padding=1)
)
  • 定义 Stage5 结构:两个 Inception 块后面紧跟输出层
# 第五阶段 (b5): 两个 Inception + 全局平均池化 + Flatten
# 832 -> 1024 (最终通道数)
b5 = nn.Sequential(
    Inception(832, 256, (160, 320), (32, 128), 128),  # 输出: 256+320+128+128 = 832
    Inception(832, 384, (192, 384), (48, 128), 128),  # 输出: 384+384+128+128 = 1024
    nn.AdaptiveAvgPool2d((1, 1)),  # 全局平均池化到 1x1
    nn.Flatten()                   # 展平成向量, 长度=1024
)
  • 测试各层输出
# 最终分类层: 输入特征维度=1024, 输出类别数=10 (Fashion-MNIST)
net = nn.Sequential(b1, b2, b3, b4, b5, nn.Linear(1024, 10))
# 输入张量形状: (batch=1, channels=1, H=96, W=96)
X = torch.rand(size=(1, 1, 96, 96))
for layer in net: 
    X = layer(X)   # 前向传播
    print(layer.__class__.__name__, 'output shape:\t', X.shape)
输出:

Sequential output shape:	 torch.Size([1, 64, 24, 24])
Sequential output shape:	 torch.Size([1, 192, 12, 12])
Sequential output shape:	 torch.Size([1, 480, 6, 6])
Sequential output shape:	 torch.Size([1, 832, 3, 3])
Sequential output shape:	 torch.Size([1, 1024])
Linear output shape:	 torch.Size([1, 10])
# 再次定义与上方结构等价的 GoogLeNet 架构 (保持一致性, 可用于对比或重建 net)
# 若与前面定义重复, 实际使用时只保留一个即可
b1 = nn.Sequential(
    nn.Conv2d(1, 64, kernel_size=7, stride=2, padding=3),
    nn.ReLU(),
    nn.MaxPool2d(kernel_size=3, stride=2, padding=1)
)

b2 = nn.Sequential(
    nn.Conv2d(64, 64, kernel_size=1), nn.ReLU(),
    nn.Conv2d(64, 192, kernel_size=3, padding=1),
    nn.MaxPool2d(kernel_size=3, stride=2, padding=1)
)

b3 = nn.Sequential(
    Inception(192, 64, (96, 128), (16, 32), 32),
    Inception(256, 128, (128, 192), (32, 96), 64),
    nn.MaxPool2d(kernel_size=3, stride=2, padding=1)
)

b4 = nn.Sequential(
    Inception(480, 192, (96, 208), (16, 48), 64),
    Inception(512, 160, (112, 224), (24, 64), 64),
    Inception(512, 128, (128, 256), (24, 64), 64),
    Inception(512, 112, (144, 288), (32, 64), 64),
    Inception(528, 256, (160, 320), (32, 128), 128),
    nn.MaxPool2d(kernel_size=3, stride=2, padding=1)
)

b5 = nn.Sequential(
    Inception(832, 256, (160, 320), (32, 128), 128),
    Inception(832, 384, (192, 384), (48, 128), 128),
    nn.AdaptiveAvgPool2d((1, 1)), nn.Flatten()
)

net = nn.Sequential(b1, b2, b3, b4, b5, nn.Linear(1024, 10))  # 分类层: 1024 -> 10
  • 训练
# 训练配置与执行
# lr: 学习率; num_epochs: 训练轮数; batch_size: 每批样本数量
lr, num_epochs, batch_size = 0.1, 10, 128
# 加载数据: 使用 d2l 工具函数加载 Fashion-MNIST, resize=96 与网络输入对齐
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size, resize=96)
# 训练函数: d2l.train_ch6 封装了典型的训练循环 (前向->计算损失->反向->更新->评估)
# d2l.try_gpu() 自动选择可用 GPU (否则回退到 CPU)
d2l.train_ch6(net, train_iter, test_iter, num_epochs, lr, d2l.try_gpu())

11、批量归一化(Batch Normalization)

网络越深产生的问题

  • 反向传播,损失的梯度从输出层向后传,靠近输出的层训练较快
    • 梯度往下越传递越小(小数相乘)
  • 数据在最底部
    • 靠近数据的底部层训练较慢
    • 底部层一变化,所有都得跟着变,相当于低层特征改变,不断抽象得到的高层特征也会随之改变
    • 顶部的那些层需要重新学习多次
    • 导致收敛变慢

对于典型的多层感知机或卷积神经网络。当我们训练时,中间层中的变量(例如,多层感知机中的仿射变换输出)可能具有更广的变化范围:不论是沿着从输入到输出的层,跨同一层中的单元,或是随着时间的推移,模型参数的随着训练更新变幻莫测。 批量归一化的发明者非正式地假设,这些变量分布中的这种偏移可能会阻碍网络的收敛。 直观地说,我们可能会猜想,如果一个层的可变值是另一层的 100 倍,这可能需要对学习率进行补偿调整。

同时,更深层的网络很复杂,容易过拟合。 这意味着正则化变得更加重要。

  • 前面层参数一变,后面层收到的输入分布就变了(内部协变量偏移) → 网络像在不停“重新学习”。
  • BN 把每一层的激活归一化为均值为 0、方差为 1 → 使得每一层的输入保持稳定。
  • 分布稳定 → 梯度稳定 → 学习率能用得更大 → 收敛更快 → 精度更高

如何解决

批量归一化(batch normalization)

批量归一化(batch normalization)是一种流行且有效的技术,可持续加速深层网络的收敛速度。 再结合后期介绍的残差块,批量归一化使得研究人员能够训练100层以上的网络。

批量规范化应用于单个可选层(也可以应用到所有层),其原理如下:在每次训练迭代中,我们首先规范化输入,即通过减去其均值并除以其标准差,其中两者均基于当前小批量处理。

批量归一化固定每一个小批量(在不同层输出)里面的均值和方差:

μ B = 1 ∣ B ∣ ∑ i ∈ B x i σ B 2 = 1 ∣ B ∣ ∑ i ∈ B ( x i − μ B ) 2 + ϵ \mu_B={1 \over |B|} \sum_{i \in B} x_i \\ \sigma_B^2={1 \over |B|} \sum_{i \in B} (x_i-\mu_B)^2 + \epsilon μB=B1iBxiσB2=B1iB(xiμB)2+ϵ

其中 B B B指一个批量 Batch, ϵ \epsilon ϵ为一个很小的数,防止方差为零,在下文无法进行除零运算

然后再通过下式对每个批量在不同层的输出值数据做额外的调整,应用比例系数 γ \gamma γ和比例偏移 β {\beta} β,将每层输出值固定为均值为 β {\beta} β、方差为 γ {\gamma} γ的分布:

x i + 1 = γ x i − μ B σ B + β x_{i+1}=\gamma{x_i-\mu_B \over \sigma_B} + \beta xi+1=γσBxiμB+β

批量归一化层

  • 比例系数 γ {\gamma} γ和偏移系数 β {\beta} β是学习出来的
  • 批量归一化是一个线性变换
  • 作用位置
    • 全连接层和卷积层输出上激活函数之前

因为一般激活函数(如 relu) 会将数据映射为正数,所以不能再带回正负各异的状态

  • 全连接层和卷积层输入上
  • 对于全连接层,作用在特征维(独立改变每个特征的分布)
  • 对于卷积层,作用于通道维(即一个滑动窗口里像素的特征)

批量归一化的作用

  • 可以加速收敛并让训练更稳定(因为可以用更大的学习率,而防止学习率过大造成的无法收敛抖动或者靠近输出层梯度爆炸的问题)
  • 一般不改变模型的精度
  • 只有批量足够大和运用在深层网络时,批量归一化效果才能有效且稳定。如果我们尝试使用大小为1的小批量应用批量规范化,将无法学到任何东西。

因为在减去均值之后,每个隐藏单元将为0。 所以,只有使用足够大的小批量,批量规范化这种方法才是有效且稳定的。 请注意,在应用批量规范化时,批量大小的选择可能比没有批量规范化时更重要

上图以使用 VGG 网络为例展示 BatchNorm 的效果,橙色代表标准结构,蓝色代表增加了 BatchNorm 的对比结构,品红色代表增加了“Noisy BatchNorm”的对比结构。从左侧图可看出加入 BatchNorm 后,训练精读收敛得更快,同时抖动更小(但不改变最终的精度);从右侧图可看出加入 BatchNorm 后,各层输出分布更加“均衡”。

通过上图对比实验,可以看出使用 BN 后,损失下降更快更平稳,梯度抖动更稳定。

批量归一化作用的原理

  • 最初的论文表示可以减少内部协变量转移
  • 后续论文指出 batch normalization 相当于在小批量里增加噪音$ \mu,\sigma $,对数据进行了随机偏移和缩放(目前还没有一个统一的结论)
  • 没必要和丢弃法混合使用

总结:

批量归一化固定小批量中的均值和方差,然后学习出适合的偏移和缩放

可以加速收敛速度,但一般不改变模型精度

代码实现

  • 定义 batch_norm 运算
import torch
from torch import nn
from d2l import torch as d2l

def batch_norm(X,gamma,beta,moving_mean,moving_var,eps,momentum): # X为输入,gamma、beta为学的参数。moving_mean、moving_var为全局的均值、方差。eps为避免除0的参数。momentum为更新moving_mean、moving_var的。                
    if not torch.is_grad_enabled(): # 'is_grad_enabled' 来判断当前模式是训练模式还是预测模式。就是在做推理的时候,推理不需要反向传播,所以不需要计算梯度    
        X_hat = (X - moving_mean) / torch.sqrt(moving_var + eps) # 做推理时,可能只有一个图片进来,没有一个批量进来,因此这里用的全局的均值、方差。在预测中,一般用整个预测数据集的均值和方差。加eps为了避免方差为0,除以0了。       
    else: # 训练模式
        # 保证只支持 2D(全连接)或 4D(卷积)输入。
        assert len(X.shape) in (2,4)
        if len(X.shape) == 2:  # 2 表示2表示有两个维度,样本和特征,表示全连接层应该是:2 代表全连接层 (batch_size, feature)
            mean = X.mean(dim=0) # 按行求均值,即对每一列求一个均值出来。mean为1Xn的行向量   
            var = ((X-mean)**2).mean(dim=0) # 方差也是行向量
        else: # 4 表示卷积层
            mean = X.mean(dim=(0,2,3),keepdim=True) # 0为批量大小,1为输出通道,2、3为高宽。这里是沿着通道维度求均值,0->batch内不同样本,2 3 ->同一通道层的所有值求均值,获得一个1xnx1x1的4D向量。       
            var = ((X-mean)**2).mean(dim=(0,2,3),keepdim=True) # 同样对批量维度、高宽取方差。每个通道的每个像素位置 计算均值方差。
        X_hat = (X-mean) / torch.sqrt(var + eps) # 训练用的计算出来的均值、方差,推理用的全局的均值、方差
        moving_mean = momentum * moving_mean + (1.0 - momentum) * mean # 累加,将计算的均值累积到全局的均值上,更新moving_mean
        moving_var = momentum * moving_var + (1.0 - momentum) * var # 当前全局的方差与当前算的方差做加权平均,最后会无限逼近真实的方差。仅训练时更新,推理时不更新。          
    Y = gamma * X_hat + beta # Y 为归一化后的输出
    return Y, moving_mean.data, moving_var.data
  • 定义 BatchNorm 层

因为依据上文所述,gamma、beta是需要更新的参数,所以需要使用nn.Parameter来构造保证可存储梯度从而可被优化器进行更新

# 创建一个正确的BatchNorm图层
class BatchNorm(nn.Module):
    def __init__(self, num_features, num_dims):
        super().__init__()
        if num_dims == 2:
            shape = (1, num_features) # num_features 为 feature map 的多少,即通道数的多少  
        else:
            shape = (1, num_features,1,1)
        self.gamma = nn.Parameter(torch.ones(shape)) # 伽马初始化为全1,贝塔初始化为全0
        self.beta = nn.Parameter(torch.zeros(shape)) # 伽马为要拟合的均值,贝塔为要拟合的方差
        self.moving_mean = torch.zeros(shape) # 伽马、贝塔需要在反向传播时更新,所以放在nn.Parameter里面,moving_mean、moving_var不需要迭代,所以不放在里面      
        self.moving_var = torch.ones(shape)
        
    def forward(self, X):
        if self.moving_mean.device != X.device:
            self.moving_mean = self.moving_mean.to(X.device) # 
            self.moving_var = self.moving_var.to(X.device)
        Y, self.moving_mean, self.moving_var = batch_norm(
            X, self.gamma, self.beta,self.moving_mean,self.moving_var,
            eps=1e-5,momentum=0.9)
        return Y
  • 将 BatchNorm 嵌套进一个 LeNet 神经网络
# 应用BatchNorm于LeNet模型
net = nn.Sequential(nn.Conv2d(1,6,kernel_size=5),BatchNorm(6,num_dims=4), # 在第一个卷积后面加了BatchNorm                       
                   nn.Sigmoid(),nn.MaxPool2d(kernel_size=2,stride=2),
                   nn.Conv2d(6,16,kernel_size=5),BatchNorm(16,num_dims=4), # BatchNorm的feature map为卷积层的输出通道数。这里BatchNorm加在激活函数前面。      
                   nn.Sigmoid(),nn.MaxPool2d(kernel_size=2,stride=2),      
                   nn.Flatten(),nn.Linear(16*4*4,120),
                   BatchNorm(120,num_dims=2),nn.Sigmoid(),
                   nn.Linear(120,84),BatchNorm(84,num_dims=2),
                   nn.Sigmoid(),nn.Linear(84,10))   
  • 训练
# 在Fashion-MNIST数据集上训练网络
lr,num_epochs,batch_size = 1.0, 10, 256
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size)
d2l.train_ch6(net, train_iter, test_iter, num_epochs, lr, d2l.try_gpu())  # 变快是指收敛所需的迭代步数变少了,但每次迭代计算量更大了呀,所以从时间上来讲跑得慢了   

对比原始LeNet结果:可知收敛更快,推理速度减慢20%

# Out:(Original LeNet)
# loss 0.473, train acc 0.823, test acc 0.786
# 40832.5 examples/sec on gpu(0)
# 拉伸参数gamma和偏移参数beta
net[1].gamma.reshape((-1,)), net[1].beta.reshape((-1,))

输出:
(tensor([2.3782, 1.5538, 1.9044, 2.7635, 1.0332, 1.3225], device='cuda:0',
        grad_fn=<ReshapeAliasBackward0>),
 tensor([-0.3879, -0.1192, -2.2361,  1.6586, -0.8143,  0.2300], device='cuda:0',
        grad_fn=<ReshapeAliasBackward0>))
  • 简明实现
# 简洁使用
net = nn.Sequential(nn.Conv2d(1,6,kernel_size=5),nn.BatchNorm2d(6),
                   nn.Sigmoid(),nn.MaxPool2d(kernel_size=2,stride=2),
                   nn.Conv2d(6,16,kernel_size=5),nn.BatchNorm2d(16),
                   nn.Sigmoid(),nn.MaxPool2d(kernel_size=2,stride=2),
                   nn.Flatten(),nn.Linear(256,120),nn.BatchNorm1d(120),
                   nn.Sigmoid(),nn.Linear(120,84),nn.BatchNorm1d(84),
                   nn.Sigmoid(),nn.Linear(84,10))

# 使用相同超参数来训练模型
d2l.train_ch6(net, train_iter, test_iter, num_epochs, lr, d2l.try_gpu())

12、残差网络ResNet

问题引出:加更多的层总是改善精度吗?

以下图示例来说,对于非嵌套函数(non-nested function)类,较复杂的函数类并不总是向“真”函数 f ∗ f^∗ f 靠拢(区域大小代表模型复杂度,复杂度由 F 1 \mathcal{F1} F1 F 6 \mathcal{F6} F6 递增)。 在下图左边,虽然 F 3 \mathcal{F3} F3 F 1 \mathcal{F1} F1 更接近 f ∗ f^∗ f ,但 F 6 \mathcal{F6} F6 却离的更远了。 相反对于下图右侧的嵌套函数(nested function)类 F 1 ⊆ … ⊆ F 6 \mathcal{F1}\subseteq…\subseteq \mathcal{F6} F1F6 ,我们可以避免上述问题。

因此,只有当较复杂的函数类包含较小的函数类时,我们才能确保提高它们的性能(相当于在原有区域逐渐增大覆盖面积来逼近最优解)。 对于深度神经网络,如果我们能将新添加的层训练成恒等映射(identity function) f ( x ) = x f(x)=x f(x)=x,新模型和原模型将同样有效。 同时,由于新模型可能得出更优的解来拟合训练数据集,因此添加层似乎更容易降低训练误差。

公式:

H(x)=F(x)+x

符号 意思
x 残差块的输入(原始特征)
F(x) 输入经过卷积、BN、ReLU 后提取的新特征
F(x) + x 把“新特征 + 原特征”加在一起
H(x) 这个残差块最终输出

残差块(Residual blocks)

假设我们的原始输入为 x x x ,而希望学出的理想映射为 f ( x ) f(x) f(x) (作为下图上方激活函数的输入)。 下图虚线框中的部分需要直接拟合出该映射 f ( x ) f(x) f(x) ,而右图虚线框中的部分则需要拟合出残差映射 f ( x ) − x f(x)−x f(x)x 。 残差映射在现实中往往更容易优化。 以本节开头提到的恒等映射作为我们希望学出的理想映射 f ( x ) f(x) f(x) ,我们只需将下图右侧虚线框内上方的加权运算(如仿射)的权重和偏置参数设成 0 0 0,那么 f ( x ) f(x) f(x) 即为恒等映射。 实际中,当理想映射 f ( x ) f(x) f(x) 极接近于恒等映射时,残差映射也易于捕捉恒等映射的细微波动。 右图是 ResNet 的基础架构–残差块(residual block)。 在残差块中,输入可通过跨层数据线路更快地向前传播。

  • 串联一个层改变函数类,我们希望能扩大函数类。
  • 残差块加入快速通道(右边)来得到 f ( x ) = x + g ( x ) f(x)=x+g(x) f(x)=x+g(x)
  • 相当于在后面复杂网络嵌入了前面的简单网络。

残差块细节

残差块有两种实现方式:

  • 一种是当use_1x1conv=False时,应用 ReLU 非线性函数之前,将
    输入添加到输出。也就是 输入 x 和输出 F(x) 形状一样 → 直接相加
  • 一种是当use_1x1conv=True时,添加通过 1 × 1 1×1 1×1 卷积调整通道和分辨率。也就是 输入 x 和输出形状不一样 → 用 1×1 Conv 调整后再加

在这里插入图片描述

可以使用不同的残差块:

ResNet 网络结构

ResNet 的前两层跟之前介绍的 GoogLeNet 中的一样: 在输出通道数为 64、步幅为 2 的 7 × 7 7×7 7×7 卷积层后,接步幅为 2 的 3 × 3 3×3 3×3 的最大汇聚层。 不同之处在于 ResNet 每个卷积层后增加了批量规范化层。

每个模块有 4 个卷积层(不包括恒等映射的 1 × 1 1×1 1×1 卷积层)。 加上第一个 7 × 7 7×7 7×7 卷积层和最后一个全连接层,共有 18 层。 因此,这种模型通常被称为 ResNet-18。 通过配置不同的通道数和模块里的残差块数可以得到不同的 ResNet 模型,例如更深的含 152 层的 ResNet-152。

  • 每个stage的第一个ResNet块会让高宽减半(stride=2)
  • 后接多个高宽不变的 ResNet(stride=1)
    • 用 1x1Conv skip 可以改变输出通道匹配 ResNet
  • 类似于 VGG 和 GooleNet 的总体架构
    • 一般是 5 个 Stage
    • 7 × 7 7×7 7×7 Conv + BN + 3 × 3 3×3 3×3 MaxPooling
    • 每一个 Stage 的具体框架很灵活
  • 但替换成了 ResNet 块
  • 当通道不匹配时,用 1×1 卷积 skip connection 调整 x 的通道数
输入: 224×224×3
↓
Conv1: 7×7, stride=2, 64通道     输出:112×112×64
↓
MaxPool: 3×3, stride=2           输出:56×56×64
↓
------------------------------------------------
Stage 1 (conv2_x): 2 个 BasicBlock,stride=1
输出:56×56×64
------------------------------------------------
Stage 2 (conv3_x): 2 个 BasicBlock,第一个 stride=2
输出:28×28×128
------------------------------------------------
Stage 3 (conv4_x): 2 个 BasicBlock,第一个 stride=2
输出:14×14×256
------------------------------------------------
Stage 4 (conv5_x): 2 个 BasicBlock,第一个 stride=2
输出:7×7×512
------------------------------------------------
Global Average Pooling → 1×1×512
↓
FC(全连接层)→ 分类

总结

  • 残差块使得很深的网络更加容易训练
    • 甚至可以训练一千层的网络
  • 残差网络对随后的深层神经网络设计产生了深远影响,无论是卷积累网络还是全连接类网络

代码实现

  • 定义 Residual class
定义残差块 Residual:

两个 3x3 卷积 + BN;可选的 1x1 卷积用于在通道数或空间尺寸变化时匹配捷径分支(X)。
前向: Y = Conv→BN→ReLU → Conv→BN,不先激活;若需要用 1x1 调整 X;然后残差相加 Y += X,再 ReLU。
作用:缓解深层网络训练中的退化,允许恒等映射直接传递信息。
resnet_block 函数:

构造一个“stage”,含多个 Residual 小块。
每个 stage 的第一个块(除第一阶段外)用 strides=2 做下采样并升/变更通道。
整体网络 net:

输入部分 b1:7x7 大卷积 + BN + ReLU + 3x3 最大池化(两次下采样,22456)。
后续四个 stage:通道数依次 64128256512;空间尺寸每个新 stage 首块再减半。
末尾:自适应全局平均池化 → 展平 → 全连接输出 10 类(用于 Fashion-MNIST 分类)。
形状变化示例(224x224 输入):

b1 后: (64,56,56)
b2 后: (64,56,56)
b3 后: (128,28,28)
b4 后: (256,14,14)
b5 后: (512,7,7)
池化后: (512,1,1) → 展平 → (512,) → 线性层输出 10
训练部分:

使用 d2l.load_data_fashion_mnist,resize=96(与经典 ImageNet 尺寸不同,简化实验)。
调用 d2l.train_ch6 进行多轮训练(学习率 0.05,批量 256)。
import torch
from torch import nn
from torch.nn import functional as F
from d2l import torch as d2l
# conv1 → BN1 → ReLU → conv2 → BN2 → ( + shortcut ) → ReLU
class Residual (nn.Module):
    # num_channels: 残差分支输出通道数
    def __init__(self, input_channels, num_channels, use_1x1conv=False,strides=1): # num_channels为输出channel数  
        super().__init__()
        self.conv1 = nn.Conv2d(input_channels, num_channels, kernel_size=3, padding=1, stride=strides) # 可以使用传入进来的strides 
        self.conv2 = nn.Conv2d(num_channels, num_channels, kernel_size=3, padding=1)   # 使用nn.Conv2d默认的strides=1
        if use_1x1conv:
            self.conv3 = nn.Conv2d(input_channels, num_channels, kernel_size=1, stride=strides)   
        else:
            self.conv3 = None
        self.bn1 = nn.BatchNorm2d(num_channels)
        self.bn2 = nn.BatchNorm2d(num_channels)
        self.relu = nn.ReLU(inplace=True) # inplace原地操作,不创建新变量,对原变量操作,节约内存
        
    def forward(self, X):
        Y = F.relu(self.bn1(self.conv1(X)))
        Y = self.bn2(self.conv2(Y))
        if self.conv3:
            X = self.conv3(X)
        Y += X
        return F.relu(Y)

  • 测试
# 输入和输出形状一致
blk = Residual(3,3) # 输入三通道,输出三通道
X = torch.rand(4,3,6,6) 
Y = blk(X) # stride用的默认的1,所以宽高没有变化。如果strides用2,则宽高减半
Y.shape   # torch.Size([4, 3, 6, 6])

# 输出通道数翻倍的同时,减半输出的高和宽
blk = Residual(3,6,use_1x1conv=True,strides=2)  # 由3变为6,通道数加倍
blk(X).shape  # torch.Size([4, 6, 3, 3])
  • 定义 Residual block
# ResNet的第一个stage
b1 = nn.Sequential(nn.Conv2d(1,64,kernel_size=7,stride=2,padding=3),
                  nn.BatchNorm2d(64),nn.ReLU(),
                  nn.MaxPool2d(kernel_size=3,stride=2,padding=1))

# class Residual为小block,resnet_block 为大block,为Resnet网络的一个stage
def resnet_block(input_channels,num_channels,num_residuals,first_block=False):
    blk = []
    for i in range(num_residuals):
        if i == 0 and not first_block: # stage中不是第一个block则高宽减半
            blk.append(Residual(input_channels, num_channels, use_1x1conv=True,strides=2))   
        else:
            blk.append(Residual(num_channels, num_channels))
    return blk
  • 构建 Stage
b2 = nn.Sequential(*resnet_block(64,64,2,first_block=True)) # 因为b1做了两次宽高减半,nn.Conv2d、nn.MaxPool2d,所以b2中的首次就不减半了      
b3 = nn.Sequential(*resnet_block(64,128,2)) # b3、b4、b5的首次卷积层都减半
b4 = nn.Sequential(*resnet_block(128,256,2))
b5 = nn.Sequential(*resnet_block(256,512,2))

net = nn.Sequential(b1,b2,b3,b4,b5,nn.AdaptiveAvgPool2d((1,1)),nn.Flatten(),nn.Linear(512,10))    
  • 测试网络输出
# 观察一下ReNet中不同模块的输入形状是如何变化的
X = torch.rand(size=(1,1,224,224))
for layer in net:
    X = layer(X)
    print(layer.__class__.__name__,'output shape:\t',X.shape) # 通道数翻倍、模型减半
Sequential output shape:	 torch.Size([1, 64, 56, 56])
Sequential output shape:	 torch.Size([1, 64, 56, 56])
Sequential output shape:	 torch.Size([1, 128, 28, 28])
Sequential output shape:	 torch.Size([1, 256, 14, 14])
Sequential output shape:	 torch.Size([1, 512, 7, 7])
AdaptiveAvgPool2d output shape:	 torch.Size([1, 512, 1, 1])
Flatten output shape:	 torch.Size([1, 512])
Linear output shape:	 torch.Size([1, 10])
  • 模型训练
# 训练模型
lr, num_epochs, batch_size = 0.05, 10, 256
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size, resize=96)  
d2l.train_ch6(net, train_iter, test_iter, num_epochs, lr, d2l.try_gpu())

通过与之前模型结果的对比,可以看出 ResNet 得益于残差设计,使得梯度传播更快,模型收敛更快、训练精度更高,也就是模型特征提取能力更强,速度也更快。(比 Alexnet 稍快、比 VGG 快将近 100%、比 NiN 快将近 50%、比 GoogLeNet 快将近 35%)

Logo

魔乐社区(Modelers.cn) 是一个中立、公益的人工智能社区,提供人工智能工具、模型、数据的托管、展示与应用协同服务,为人工智能开发及爱好者搭建开放的学习交流平台。社区通过理事会方式运作,由全产业链共同建设、共同运营、共同享有,推动国产AI生态繁荣发展。

更多推荐