• 从基础到卷积神经网络(第14天)


    1. PyTorch 神经网络基础

    1.1 模型构造

    1. 块和层

    首先,回顾一下多层感知机

    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) # 生成随机输入(批量大小=2, 输入维度=20)
    net(X) # 输出(批量大小=2, 输出维度=10)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    在这里插入图片描述

    2. 自定义块

    自定义MLP实现上一节的功能

    class MLP(nn.Module): # 定义nn.Mudule的子类
        def __init__(self): 
            super().__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))) # X-> hidden-> relu-> out
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    实例化MLP的层,然后再每次调用正向传播函数时调用这些层

    net = MLP()
    net(X)
    
    • 1
    • 2

    在这里插入图片描述

    3. 实现Sequential类

    class MySequential(nn.Module):
        def __init__(self, *args):
            super().__init__()
            for block in args:
                self._modules[block] = block
            
        def forward(self, X):
            for block in self._modules.values():
                X = block(X)
            return X
    
    net = MySequential(nn.Linear(20, 256), nn.ReLU(), nn.Linear(256, 10))
    net(X)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    在这里插入图片描述

    4. 在正向传播中执行代码

    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)
            X = F.relu(torch.mm(X, self.rand_weight) + 1) # 输入和随机权重做矩阵乘法 + 1(偏移)-》激活函数
            X = self.linear(X)
            while X.abs().sum() > 1: # 控制X小于1
                X /= 2
            return X.sum() # 返回一个标量
    
    net = FixedHiddenMLP()
    net(X)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    5. 混合搭配各种组合块的方法

    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)) # 输入-> net-> linear中
    
    chimera = nn.Sequential(NestMLP(), nn.Linear(16, 20), FixedHiddenMLP()) # (32, 16)->(16, 20) ->(20, 1)
    chimera(X)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    在这里插入图片描述
    总结:
    1、在init中写各种层
    2、在前向函数中调用init中各种层
    有很强的灵活性

    1.2 参数构造

    具有单隐藏层的MLP

    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))
    net(X)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    在这里插入图片描述
    参数访问

    print(net[2].state_dict()) # 拿到nn.Linear的相关参数
    
    • 1

    在这里插入图片描述
    访问目标参数

    print(type(net[2].bias))
    print(net[2].bias)
    print(net[2].bias.data)
    net[2].weight.grad == None # 梯度是否为0,因为此时还没有计算,所以没有梯度
    
    • 1
    • 2
    • 3
    • 4

    在这里插入图片描述
    一次访问所有参数

    print(*[(name, param.shape) for name, param in net[0].named_parameters()])
    print(*[(name, param.shape) for name, param in net.named_parameters()])
    
    • 1
    • 2

    输出没有block1是因为第二层是ReLU是没有参数的
    在这里插入图片描述

    net.state_dict()['2.bias'].data # 访问最后一层的偏移
    
    • 1

    在这里插入图片描述
    从嵌套块收集参数

    def block1():
        return nn.Sequential(nn.Linear(4, 8), nn.ReLU(), 
                             nn.Linear(8, 4), nn.ReLU())
    
    def block2(): # block2嵌套4个block1
        net = nn.Sequential()
        for i in range(4): 
            net.add_module(f'block {i}', block1()) 
        return net
    
    rgnet = nn.Sequential(block2(), nn.Linear(4, 1))
    rgnet(X)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    在这里插入图片描述

    print(rgnet) # 查看网络结构
    
    • 1

    在这里插入图片描述
    内置初始化

    def init__normal(m): # 传入的module
        if type(m) == nn.Linear: # 如果传入的是全连接层
            nn.init.normal_(m.weight, mean=0, std=0.01) # 内置初始化,均值为0方差为1,.normal_替换函数不返回
            nn.init.zeros_(m.bias) # 所有的bias赋0
    
    net.apply(init__normal) # 对神经网络模型net中的所有参数进行初始化,使用init_normal()函数对参数进行随机初始化
    net[0].weight.data[0], net[0].bias.data[0]
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    在这里插入图片描述

    def init_constant(m):
        if type(m) == nn.Linear:
            nn.init.constant_(m.weight, 1) # 将m.weight初始常数化为1
            nn.init.zeros_(m.bias)
    
    net.apply(init_constant)
    net[0].weight.data[0], net[0].bias.data[0]
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    在这里插入图片描述
    不建议权重全部常数化,会导致所有向量向一致的方向发展

    对某些块应用不同的初始化方法

    def xavier(m):
        if type(m) == nn.Linear:
            nn.init.xavier_uniform_(m.weight) # 使用Xavier均匀分布进行初始化
    
    def init_42(m):
        if type(m) == nn.Linear:
            nn.init.constant_(m.weight, 42)
    
    net[0].apply(xavier) # 第一个层用xavier初始化
    net[2].apply(init_42) # 第二个层用init_42进行初始化
    print(net[0].weight.data[0])
    print(net[2].weight.data)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    在这里插入图片描述
    自定义初始化

    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)
            m.weight.data *= m.weight.data.abs() >= 5 # 对大于等于5的位置进行保留,小于5的位置进行置零操作
    
    net.apply(my_init) # 使用my_init对net进行初始化
    net[0].weight[:2]
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    在这里插入图片描述
    直接进行替换

    net[0].weight.data[:] += 1
    net[0].weight.data[0, 0] = 42
    net[0].weight.data[0]
    
    • 1
    • 2
    • 3

    在这里插入图片描述
    参数绑定(在不同的网络之间共享权重的方法)

    shared = nn.Linear(8, 8)
    net = nn.Sequential(nn.Linear(4, 8), nn.ReLU(), shared, nn.ReLU(), shared,  # 2和4层共享权重
                        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])
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    1.3 自定义层

    构造一个没有任何参数的自定义层

    import torch
    import torch.nn.functional as F
    from torch import nn
    
    class CenteredLayer(nn.Module): # 层也是nn.Module的子类
        def __init__(self):
            super().__init__()
        
        def forward(self, X):
            return X - X.mean()
    
    layer = CenteredLayer()
    layer(torch.FloatTensor([1, 2, 3, 4, 5]))
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    在这里插入图片描述
    将层作为组件合并到构建更复杂的模型

    net = nn.Sequential(nn.Linear(8, 128), CenteredLayer())
    
    Y = net(torch.rand(4, 8))
    Y.mean() # 不会真等于0是因为存在浮点的误差
    
    • 1
    • 2
    • 3
    • 4

    带参数的图层

    class MyLinear(nn.Module):
        def __init__(self, in_units, units):
            super().__init__()
            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) # 输入是5.输出是3
    dense.weight
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    使用自定义层直接执行正向传播计算

    dense(torch.rand(2, 5))
    
    • 1

    使用自定义层构建模型

    net = nn.Sequential(MyLinear(64, 8), MyLinear(8, 1))
    net(torch.rand(2, 64))
    
    • 1
    • 2

    1.4 读写文件

    加载和保存张量

    import torch 
    from torch import nn
    from torch.nn import functional as F
    
    x = torch.arange(4) # 构造长为4的向量
    torch.save(x, 'x-file') # 把x存在当前文件目录下
    
    x2 = torch.load("x-file") # 从指定的文件路径("x-file")加载一个PyTorch模型或张量
    x2
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    存储一个张量列表,然后把它们读回内存

    y = torch.zeros(4)
    torch.save([x, y], 'x-file')
    x2, y2 = torch.load('x-file')
    (x2, y2)
    
    • 1
    • 2
    • 3
    • 4

    在这里插入图片描述
    写入或读取从字符串映射到张量的字典

    mydict = {'x' : x, 'y' : y}
    torch.save(mydict, 'mydict')
    mydict2 = torch.load('mydict')
    mydict2
    
    • 1
    • 2
    • 3
    • 4

    在这里插入图片描述
    加载和保存模型参数

    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)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    将模型的参数(主要是权重)存储到一个叫‘mlp.params’的文件

    torch.save(net.state_dict(), 'mlp.params')
    
    • 1

    实例化了原始多层感知机的一个备份,直接读取文件中存储的参数

    clone = MLP()
    clone.load_state_dict(torch.load("mlp.params")) # 从指定的文件路径("mlp.params")加载一个PyTorch模型的参数,并将这些参数应用到一个新的模型实例(clone)上
    clone.eval()
    
    • 1
    • 2
    • 3

    在这里插入图片描述
    验证

    Y_clone = clone(X)
    Y_clone == Y
    
    • 1
    • 2

    在这里插入图片描述

    使用GPU

    查看gpu

    !nvidia-smi
    
    • 1

    查询可用gpu的数量

    import torch
    from torch import nn
    
    torch.cuda.device_count()
    
    • 1
    • 2
    • 3
    • 4

    计算设备

    import torch
    from torch import nn
    
    torch.device('cpu'), torch.cuda.device('cuda')
    
    • 1
    • 2
    • 3
    • 4

    这两个函数允许我们在请求的GPU不存在的情况下运行代码

    def try_gpu(i=0):  
        """如果存在,则返回gpu(i),否则返回cpu()。"""
        if torch.cuda.device_count() >= i + 1:
            return torch.device(f'cuda:{i}')
        return torch.device('cpu')
    
    def try_all_gpus():  
        """返回所有可用的GPU,如果没有GPU,则返回[cpu(),]。"""
        devices = [
            torch.device(f'cuda:{i}') for i in range(torch.cuda.device_count())]
        return devices if devices else [torch.device('cpu')]
    
    try_gpu(), try_gpu(10), try_all_gpus()
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    查看张量所在的设备,默认是在cpu上

    x = torch.tensor([1, 2, 3])
    x.device
    
    • 1
    • 2

    存储在GPU上

    X = torch.ones(2, 3, device=try_gpu())
    X
    Y = torch.rand(2, 3, device=try_gpu())
    Y
    
    • 1
    • 2
    • 3
    • 4

    计算X+Y,要决定在哪里执行这个操作,如果不在同一个gpu要执行拷贝

    Z = X.cuda(0)
    Y + Z
    
    • 1
    • 2

    神经网络和GPU

    net = nn.Sequential(nn.Linear(3, 1))
    net = net.to(device=try_gpu()) # 将cpu上创建好的网络挪到gpu上
    
    net(X)
    
    • 1
    • 2
    • 3
    • 4

    确认模型参数存储在同一个GPU上

    net[0].weight.data.device
    
    • 1

    2. 卷积层

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

    2.1 卷积层的简单实现

    实现:
    互相关运算
    【当我们使用 nn.Parameter() 将张量包装成可优化参数时,该参数将被注册到模型的参数列表中,以便在反向传播期间更新和优化。这样,我们可以在定义模型时,使用 nn.Parameter() 创建可训练的权重、偏置等参数,并在训练过程中对其进行优化。

    例如,可以使用 nn.Parameter() 创建一个可训练的权重矩阵参数:

    weight = nn.Parameter(torch.randn(3, 4))
    在模型训练过程中,优化器可以自动更新这个权重参数,以最小化损失函数。】

    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)) # 输出的行列
        for i in range(Y.shape[0]):
            for j in range(Y.shape[1]):
                Y[i, j] = (X[i:i + h, j:j + w] * K).sum()
        return Y
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    验证:

    # 验证
    X = torch.tensor([[0.0, 1.0, 2.0], [3.0, 4.0, 5.0], [6.0, 7.0, 8.0]])
    K = torch.tensor([[0.0, 1.0], [2.0, 3.0]])
    corr2d(X, K)
    
    • 1
    • 2
    • 3
    • 4

    在这里插入图片描述
    实现二维卷积层:

    class Conv2D(nn.Module):
        def __init__(self, kernel_size):
            super().__init__()
            self.weight = nn.Parameter(torch.rand(kernel_size)) # 生成一个指定大小kernel_size的随机张量,nn.Parameter张量包装成可优化参数
            self.bias = nn.Parameter(torch.zeros(1)) # 生成一个大小为1的全零张量(标量)
        
        def forward(self, x):
            return corr2d(x, self.weight) + self.bias # 用x和weight做互相关运算再加上偏移
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    卷积层的一个简单应用:检测图像中不同颜色的边缘

    X = torch.ones((6, 8))
    X[:, 2:6] = 0
    X
    
    • 1
    • 2
    • 3

    在这里插入图片描述
    定义一个卷积核

    K = torch.tensor([[1.0, -1.0]]) # 卷积核已知
    
    • 1
    # 输出Y 中的1代表从白色到黑色的边缘(从1-》0),-1代表从黑色到白色的边缘(从0到-1)
    Y = corr2d(X, K)
    Y
    
    • 1
    • 2
    • 3

    在这里插入图片描述
    缺点:自定义的卷积核K 只能检测垂直边缘

    corr2d(X.t(), K) # X.t()对X做转置,X边缘成横向
    
    • 1

    在这里插入图片描述
    学习由X生成Y的卷积核:在K未知的情况下,由输入和输出学习出K

    conv2d = nn.Conv2d(1, 1, kernel_size = (1, 2), bias=False) # 输入通道= 1, 输出通道= 1
    
    X = X.reshape((1, 1, 6, 8))
    Y = Y.reshape((1, 1, 6, 7))
    
    for i in range(10): # 迭代十轮
        Y_hat = conv2d(X)
        l = (Y_hat - Y)**2 # 使用均方误差作为损失
        conv2d.zero_grad() # 把con2d的梯度设为0
        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}')
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    在这里插入图片描述
    所学的卷积核的权重张量

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

    在这里插入图片描述
    可以看到经过10论学习后的K 值和最开始自定的(1, -1)非常接近,可以完成由检测X的边缘

    2.2 卷积层里的填充和步幅

    填充:

    • 给定(32*32)输入图像
    • 应用5*5大小的卷积核
      • 第一层得到输出28*28
      • 第七层得到输出4*4
    • 更大的卷积核可以更快的减小输出大小
      • 形状从(hw)减少到(h-k+1)(w-k+1)

    输入不够大, 如何解决更深网络模型的使用----填充
    在输入周围添加额外的行/列,通常取k-1,保持图像输入输出不变
    在这里插入图片描述
    这样可以保证输出和输入形状相同

    步幅:

    • 填充减小的输出大小核层数线性相关

      • 给定输入大小224x224,在使用5x5卷积核的情况下,需要55层将输出降低到4x4
      • 需要大量计算才能得到较小输出
    • 步幅是指行/列的滑动步长
      在这里插入图片描述
      在这里插入图片描述
      总结:

    • 填充和步幅都是卷积层的超参数

    • 填充在输入周围添加额外的行/列,来控制输出形状的减少量

    • 步幅是每次滑动核窗口时的行/列的步长,可以成倍的减少输出形状

    代码实现:

    在所有侧边填充一个像素

    import torch
    from torch import nn
    
    def comp_conv2d(conv2d, X):
        X = X.reshape((1, 1) + X.shape)# (1, 1) + X.shape 表示将 (1, 1) 作为一个元组与 X.shape 进行连接,形成一个新的元组 (1, 1, 8, 8)
        Y = conv2d(X)
        return Y.reshape(Y.shape[2:])# 返回Y的后两维
    
    conv2d = nn.Conv2d(1, 1, kernel_size=3, padding=1)# padding=1上下左右各填充一行
    X = torch.rand(size=(8, 8))
    comp_conv2d(conv2d, X).shape
    # 未填充:(8-3+1, 8-3+1)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    在这里插入图片描述
    填充不同的高度和宽度

    conv2d = nn.Conv2d(1, 1, kernel_size=(5, 3), padding=(2, 1)) # 上下填充2列,左右填充1列
    comp_conv2d(conv2d, X).shape
    
    • 1
    • 2

    在这里插入图片描述
    将高度和宽度的步幅设置为2

    conv2d = nn.Conv2d(1, 1, kernel_size=3, padding=1, stride=2)
    comp_conv2d(conv2d, X).shape
    
    • 1
    • 2

    在这里插入图片描述
    更复杂的情况

    conv2d = nn.Conv2d(1, 1, kernel_size=(3, 5), padding=(0, 1), stride=(3, 4))
    comp_conv2d(conv2d, X).shape
    
    • 1
    • 2

    在这里插入图片描述
    计算过程:

    在这里插入图片描述

    2.3 卷积层里的输入和输出通道

    • 输出通道数是卷积层的超参数
    • 每个输入通道有独立的二维卷积核,所有通道结果相加得到每一个输出通道结果
    • 每个输出通道有独立的三维卷积核

    【zip(X, K) 将输入张量集合 X 和核张量集合 K 打包成一个元组的序列,每个元组包含对应位置的输入张量和核张量。】

    1x1卷积是指核大小为1x1的卷积操作。在卷积神经网络(CNN)中,常用的卷积操作是通过滑动一个核或滤波器在输入张量上提取特征。这个核的大小决定了感受野的大小和提取的特征的空间范围。

    实现多输入通道互相关运算

    import torch
    from d2l import torch as d2l 
    
    def corr2d_multi_in(X, K):
        return sum(d2l.corr2d(x, k) for x, k in zip(X, K)) 
    
    • 1
    • 2
    • 3
    • 4
    • 5

    验证互相关运算的输出

    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]]])
    
    corr2d_multi_in(X, K)
    
    • 1
    • 2
    • 3
    • 4
    • 5

    在这里插入图片描述
    计算多个通道的输出的互相关函数

    def corr2d_multi_in_out(X, K):
        return torch.stack([corr2d_multi_in(X, k) for k in K], 0) # 对 X 的多通道输入与 K 的多核进行卷积操作,并将每个卷积结果张量按照第0维度进行堆叠,得到一个具有多个通道的输出张量
    
    K = torch.stack((K, K+1, K+2), 0)
    K.shape
    
    • 1
    • 2
    • 3
    • 4
    • 5

    在这里插入图片描述

    corr2d_multi_in_out(X, K)
    
    • 1

    在这里插入图片描述

    1x1卷积与其他卷积操作相比具有以下特点:

    核的大小为1x1,所以只对一个像素进行卷积操作。
    由于卷积核的大小为1x1,因此没有空间上的局部连接和权值共享。
    1x1卷积可以改变通道数,可以通过控制输出通道的数量来调整特征映射的维度。
    1x1卷积可以用于降维和增加非线性。
    在网络中,1x1卷积通常与其他卷积层、池化层和全连接层一起使用,用于增加网络的表达能力和非线性。
    1x1卷积常用于改变特征映射的通道数,例如用于减少计算量和参数数量,或者用于增加网络的非线性。在有些情况下,1x1卷积也可以用于提取全局特征或将多个特征图融合为一个特征图。

    # 1x1卷积 等级于一个全连接层
    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) # 对K 和 X 执行矩阵乘法
        return Y.reshape((c_o, h, w))
    
    X = torch.normal(0, 1, (3, 3, 3))
    K = torch.normal(0, 1, (2, 3, 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
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    3. 池化层

    池化层返回窗口中最大或者平均值;
    缓解卷积层对位置的敏感性;
    同样有窗口大小,填充和步幅作为超参数,但是没有可以学习的参数

    实现:

    # 实现池化层的正向传播
    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)) # 创建输出(假设为单通道)
        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
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    验证二维最大池化层的输出

    X = torch.tensor([[0.0, 1.0, 2.0], [3.0, 4.0, 5.0], [6.0, 7.0, 8.0]])
    pool2d(X, (2, 2))
    
    • 1
    • 2

    在这里插入图片描述
    验证平均池化层

    pool2d(X, (2, 2), mode='avg')
    
    • 1

    在这里插入图片描述
    填充和步幅

    X = torch.arange(16, dtype=torch.float32).reshape((1, 1, 4, 4))
    X
    
    • 1
    • 2

    在这里插入图片描述
    深度学习框架中的步幅与池化窗口的大小相同

    pool2d = nn.MaxPool2d(3) # 3*3 的池化窗口
    pool2d(X)
    
    • 1
    • 2

    在这里插入图片描述
    填充和步幅可以手动设定

    pool2d = nn.MaxPool2d(3, padding=1, stride=2)
    pool2d(X)
    
    • 1
    • 2

    在这里插入图片描述
    设定一个任意大小的矩阵池化窗口,并分别设置填充和步幅的高度和宽度

    pool2d = nn.MaxPool2d((2, 3), padding=(1, 1), stride=(2, 3))
    pool2d(X)
    
    • 1
    • 2

    在这里插入图片描述
    池化层在每输入通道上单独运算
    【torch.cat() 函数将两个张量沿着指定的维度进行拼接。

    X 是一个张量。
    X+1 表示对 X 的每个元素都加1,得到一个新的张量。
    (X, X+1) 是待拼接的张量元组。
    1 是表示拼接的维度,即沿着第1维度进行拼接。
    最终,torch.cat((X, X+1), 1) 的作用是将张量 X 和 X+1 沿着第1维度进行拼接,生成一个新的张量。拼接后的张量将具有与原始张量 X 相同的形状,只是在第1维度上增加了元素的数量。

    例如,如果 X 是一个形状为 (3, 4) 的张量,那么拼接后的张量形状将为 (3, 8),其中第1维度的大小变为原来的两倍。拼接后的张量将包含原始张量 X 和 X+1 对应位置的元素。】

    X = torch.cat((X, X+1), 1) # 将张量 X 和 X+1 沿着第1维度进行拼接,生成一个新的张量。拼接后的张量将具有与原始张量 X 相同的形状,只是在第1维度上增加了元素的数量
    X
    
    • 1
    • 2

    在这里插入图片描述

    pool2d = nn.MaxPool2d(3, padding=1, stride=2)
    pool2d(X)
    
    • 1
    • 2

    在这里插入图片描述

    4. 批量归一化

    4.1 批量归一化相关

    forward是从下往上计算梯度
    backward是从上往下计算梯度-> 梯度在上层变化比较大,下层变化比较小-> 在顶部变化快,底部变化慢,上层梯度不断更新,下层梯度更新缓慢-> 整体收敛慢

    • 损失出现在最后,后面的层训练较快

    • 数据在最底部

      • 底部的层训练较慢
      • 底部层一旦变化,所有都要跟着变
      • 导致最后的那些层(顶部)需要重新学习很多次
      • 导致收敛变慢
    • 批量归一化:在学习底部层的时候避免顶部层重复训练
      在这里插入图片描述

    批量归一化:->固定分布,添加线性

    • 固定小批量里面的均值和方差【可以提前计算好】:
      μ B = 1 ∣ B ∣ ∑ i ∈ B x i a n d σ B 2 = 1 ∣ B ∣ ∑ i ∈ B ( x i − μ B ) 2 + ϵ \mu_{B}= \frac{1}{|B|}\sum _{i \in B}x_{i}and \sigma _{B}^{2}= \frac{1}{|B|}\sum _{i \in B}(x_{i}- \mu _{B})^{2}+\epsilon μB=B1iBxiandσB2=B1iB(xiμB)2+ϵ
      B-小批量下标的索引
      μ B \mu _{B} μB:均值
      ϵ \epsilon ϵ:很小的数,防止方差==0

    • 然后再做额外的调整(可以学习的参数)
      x i + 1 = γ x i − μ B σ B + β x_{i+1}= \gamma \frac{x_{i}- \mu _{B}}{\sigma _{B}}+\beta xi+1=γσBxiμB+β
      γ \gamma γ β \beta β是【可以学习的参数】

    • 作用在:

      • 全连接层和卷积层输出上,激活函数前【因为激活函数是添加非线性,所以批量归一化一定要作用在激活函数之前用以添加线性】
      • 全连接层和卷积层输入上
    • 对全连接层,作用在特征维

    • 对于卷积层,作用在通道维【以1x1卷积为例子,1个通道的信息就想到与1个样本】
      在这里插入图片描述

    总结:

    • 批量归一化固定小批量中的均值和方差,然后学习出适合的偏移和缩放
    • 可以加速收敛速度【允许使用更大的学习率】,但是一般不改变模型精度

    4.2 代码实现

    1. 从0实现

    import torch
    from torch import nn
    from d2l import torch as d2l
    """
    X:该层的输入
    gamma, beta:可以学习的参数
    moving_mean, moving_var:全局均值和方差【做推理的时候使用】-》可以简单理解为整个数据集上的均值和方差,而非小批量上的
    eps:为避免方差变成0的很小的数值
    momentum:用来更新moving_mean, moving_var【一般会提前预设】
    """
    def batch_norm(X, gamma, beta, moving_mean, moving_var, eps, momentum):
        if not torch.is_grad_enabled(): # 不计算梯度,在做推理(预测模式),推理不需要反向传播
            X_hat = (X - moving_mean) / torch.sqrt(moving_var + eps) # 推理用的是全局的均值和方差
        # 训练
        else:
            assert len(X.shape) in (2, 4)
            if len(X.shape) == 2: # 全连接层
                # 一行是一个样本,一列是一个特征,求特征的均值和方差
                mean = X.mean(dim=0) # 按行求均值,对每一列求一个均值(压缩成一行,拍扁)求特征均值
                var = ((X - mean)**2).mean(dim=0)
            else: # len(X.shape) == 4 2D卷积层
                mean = X.mean(dim=(0, 2, 3), keepdim=True) # 按照通道求出均值,keepdim=True维度不变依然为4D
                var = ((X - mean)**2).mean(dim=(0, 2, 3), keepdim=True)
            X_hat = (X - mean) / torch.sqrt(var + eps) # 训练是用当前小批量样本的均值和方差
            # 更新moving_mean和moving_var,只在训练时更新全局的均值和方差
            moving_mean = momentum * moving_mean + (1.0 - momentum) * mean
            moving_var = momentum * moving_var + (1.0 - momentum) * var
            
        Y = gamma * X_hat + beta
        return Y, moving_mean.data, moving_var.data
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30

    创建一个正确的BatchNorm图层

    class BatchNorm(nn.Module):
        def __init__(self, num_features, num_dims): # 输入样本个数,和num_dims【2:全连接层,4:卷积层】
            super().__init__()
            if num_dims == 2:
                shape = (1, num_features)
            else:
                shape = (1, num_features, 1, 1)
            self.gamma = nn.Parameter(torch.ones(shape)) # 需要拟合的方差,需要被迭代
            self.beta = nn.Parameter(torch.zeros(shape)) # 需要拟合的均值,需要被迭代
            self.moving_mean = torch.zeros(shape)
            self.moving_var = torch.ones(shape)
    
        def forward(self, X):
            if self.moving_mean.device != X.device: # 因为moving_mean和var不在Parameter里,所以要确保其在同一个device下
                self.moving_mean = self.moving_mean.to(X.device)
                self.moving_var = self.moving_var.to(X.device)
            # 调用batch_norm
            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
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    应用BatchNorm 于LeNet模型

    net = nn.Sequential(
        nn.Conv2d(1, 6, kernel_size=5), BatchNorm(6, num_dims=4), # 卷积层num_dims=4
        nn.Sigmoid(), nn.MaxPool2d(kernel_size=2, stride=2),
        nn.Conv2d(6, 16, kernel_size=5), BatchNorm(16, num_dims=4),
        nn.Sigmoid(), nn.MaxPool2d(kernel_size=2, stride=2),
        nn.Flatten(), 
        nn.Linear(16 * 4 * 4, 120), BatchNorm(120, num_dims=2), # 全连接层num_dims=2
        nn.Sigmoid(), 
        nn.Linear(120, 84), BatchNorm(84, num_dims=2),
        nn.Sigmoid(), 
        nn.Linear(84, 10)) # 输出层不需要再归一化
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    在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())
    
    • 1
    • 2
    • 3

    在这里插入图片描述

    可以看到在lr设置1.0时,收敛也很快

    2. 调用包 nn.BatchNorm2d()

    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))
    # 在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())
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
  • 相关阅读:
    MySQL数据库创建用户并授权查询某个表
    Spring Ioc源码分析系列--Ioc容器注册BeanPostProcessor后置处理器以及事件消息处理
    13 项目相关方管理
    C# 中的那些锁,在内核态都是怎么保证同步的?
    揭秘LLM计算数字的障碍的底层原理
    RabbitMQ单机部署
    Leetcode第306场周赛(附数位DP总结)
    Vue2.0开发之——Vue基础用法-侦听器(27)
    图文加多个测试带你彻底搞懂Netty ChannelPipeline的执行顺序(附源码)
    Alibaba Canal 使用记录
  • 原文地址:https://blog.csdn.net/Kunjpg/article/details/133815206