PyTorch学习笔记【5】:使用卷积进行泛化

文章目录

  • 前言
  • 1. 卷积介绍
    • 1.1. 卷积的定义
    • 1.2. 卷积的特征
  • 2. 卷积实战
    • 2.1. nn.Conv2d
    • 2.2. 填充边界
    • 2.3. 用卷积检测特征
    • 2.4. 使用深度和池化技术进一步研究
      • 2.4.1. 从大到小:下采样
      • 2.4.2. 将卷积和下采样结合
    • 2.5. 整合网络
  • 3. 训练我们的convnet
    • 3.1. 训练循环
    • 2.2. 测量精度
    • 2.3. 保存并加载我们的模型
    • 2.4. 在GPU上训练
  • 3. 模型设计
    • 3.1. 增加内存容量:宽度
    • 3.2. 正则化
      • 3.2.1. 检查参数:权重惩罚
      • 3.2.2. 不太依赖于单一输入:Dropout
      • 3.2.3. 保持激活状态:批量归一化
    • 3.3. 深入学习更复杂的结构:深度
      • 3.3.1. 跳跃连接
      • 3.3.2. 使用PyTorch建立非常深的网络
  • 总结


前言

本文是基于《Pytorch深度学习实战》一书第八章的内容所整理的学习笔记
相关代码的解释以及对应的拓展。

本文使用的代码均基于jupyter


1. 卷积介绍

1.1. 卷积的定义

卷积,离散卷积,被定义为二维图像的权重矩阵的标量积,即该函数与输入中的每个邻域的标量积。

卷积的操作:我们先从一个小小的权重矩阵,也就是卷积核(kernel)开始,让它逐步在二维输入数据上“扫描”。卷积核“滑动”的同时,计算权重矩阵和扫描所得的数据矩阵的乘积,然后把结果汇总成一个输出像素。
PyTorch学习笔记【5】:使用卷积进行泛化_第1张图片

卷积核会在其经过的所有位置上都重复以上操作,直到把输入特征矩阵转换为另一个二维的特征矩阵。

1.2. 卷积的特征

  • 邻域的局部操作
    输出即转换后的内核与图像之间的标量积的矩阵
  • 平移不变性
    在图像中使用相同的核权重
  • 模型的参数大幅减少
    如上面gif所展示的,原本6x6的矩阵经过卷积操作后,变成了4x4的矩阵

2. 卷积实战

%matplotlib inline
from matplotlib import pyplot as plt
import numpy as np
import collections

import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim

torch.set_printoptions(edgeitems=2)
torch.manual_seed(123)
class_names = ['airplane','automobile','bird','cat','deer',
               'dog','frog','horse','ship','truck']
from torchvision import datasets, transforms
data_path = 'data/p1ch7/'
cifar10 = datasets.CIFAR10(
    data_path, train=True, download=True,
    transform=transforms.Compose([
        transforms.ToTensor(),
        transforms.Normalize((0.4915, 0.4823, 0.4468),
                             (0.2470, 0.2435, 0.2616))
    ]))

cifar10_val = datasets.CIFAR10(
    data_path, train=False, download=True,
    transform=transforms.Compose([
        transforms.ToTensor(),
        transforms.Normalize((0.4915, 0.4823, 0.4468),
                             (0.2470, 0.2435, 0.2616))
    ]))
label_map = {0: 0, 2: 1}
class_names = ['airplane', 'bird']
cifar2 = [(img, label_map[label])
          for img, label in cifar10
          if label in [0, 2]]
cifar2_val = [(img, label_map[label])
              for img, label in cifar10_val
              if label in [0, 2]]

2.1. nn.Conv2d

  • 用于二维卷积
    提供给nn.Conv2d的参数至少包括输入特征(或通道,因为我们处理的是多通道图像,也就是说,每个像素有多个值)的数量、输出特征的数量以及核的大小。
    约定俗成的,在所有维度上都使用相同大小的卷积核(二维上使用大小为3x3的卷积核)
conv = nn.Conv2d(3, 16, kernel_size=3)
conv
  • 权重张量的形状
    对于单个输出像素值,我们的卷积核考虑有in_ch=3个输入通道,因此对于单个输出像素值,其权重分量(平移整个输出通道的不变量)为in_chx3x3。最后,我们有和输出通道一样多的通道。所以完整的权重张量是out_chxin_chx3x3。
conv.weight.shape, conv.bias.shape

一个二维卷积核产生一个二维图像并将其作为输出,它的像素是输入图像邻域的加权和。

nn.Conv2d()期望输入一个BxCxHxW的张量

img, _ = cifar2[0]
output = conv(img.unsqueeze(0))
img.unsqueeze(0).shape, output.shape

显示输入与输出

plt.figure(figsize=(10, 4.8))
ax1 = plt.subplot(1, 2, 1)
plt.title('output')
plt.imshow(output[0, 0].detach(), cmap='gray')
plt.subplot(1, 2, 2, sharex=ax1, sharey=ax1)
plt.imshow(img.mean(0), cmap='gray')
plt.title('input')
plt.show()

PyTorch学习笔记【5】:使用卷积进行泛化_第2张图片

2.2. 填充边界

当我们把输入的特征矩阵转换成了输出的特征矩阵,输入图像的边缘被“修剪”掉了,这是因为边缘上的像素永远不会位于卷积核中心,而卷积核也没法扩展到边缘区域以外。这是不理想的,通常我们都希望输入和输出的大小应该保持一致。

Padding就是针对这个问题提出的一个解决方案:它会用额外的“假”像素填充边缘(值一般为0),这样,当卷积核扫描输入数据时,它能延伸到边缘以外的伪像素,从而使输出和输入大小相同。

conv = nn.Conv2d(3, 1, kernel_size=3, padding=1)
output = conv(img.unsqueeze(0))
img.unsqueeze(0).shape, output.shape

无论是否使用填充,权重和偏置的大小都不会改变

2.3. 用卷积检测特征

手动设置权重来处理卷积

  • 直接打印卷积后的图像
    将偏置归零,然后将权重设置为一个常熟值,这样输出中的每个像素都能得到其相邻像素的均值
with torch.no_grad():
    conv.bias.zero_()

with torch.no_grad():
    conv.weight.fill_(1.0 / 9.0)
output = conv(img.unsqueeze(0))
plt.figure(figsize=(10, 4.8))
ax1 = plt.subplot(1, 2, 1)
plt.title('output') 
plt.imshow(output[0, 0].detach(), cmap='gray')
plt.subplot(1, 2, 2, sharex=ax1, sharey=ax1)
plt.imshow(img.mean(0), cmap='gray')
plt.title('input')
plt.show()

PyTorch学习笔记【5】:使用卷积进行泛化_第3张图片

  • 设置卷积核的权重后打印图像
conv = nn.Conv2d(3, 1, kernel_size=3, padding=1)

with torch.no_grad():
    conv.weight[:] = torch.tensor([[-1.0, 0.0, 1.0],
                                   [-1.0, 0.0, 1.0],
                                   [-1.0, 0.0, 1.0]])
    conv.bias.zero_()
output = conv(img.unsqueeze(0))
plt.figure(figsize=(10, 4.8))
ax1 = plt.subplot(1, 2, 1)
plt.title('output')
plt.imshow(output[0, 0].detach(), cmap='gray')
plt.subplot(1, 2, 2, sharex=ax1, sharey=ax1)
plt.imshow(img.mean(0), cmap='gray')
plt.title('input')
plt.show()

PyTorch学习笔记【5】:使用卷积进行泛化_第4张图片

2.4. 使用深度和池化技术进一步研究

2.4.1. 从大到小:下采样

将图像缩放一半相当于取4个相邻像素作为输入,产生1个像素作为输出

可选择的操作:

  • 取4个像素的平均值,即平均池化(现在用得少)
  • 取4个像素的最大值,即最大池化(目前最常用的方法之一,但会导致丢失3个点)
  • 使用带步长的卷积,只将第N个像素纳入计算,步长为2的3x4卷积仍然包含来自前一层所有像素的输入

最大池化由`nn.MaxPool2d模块提供

pool = nn.MaxPool2d(2)
output = pool(img.unsqueeze(0))

img.unsqueeze(0).shape, output.shape

2.4.2. 将卷积和下采样结合

可以理解为开始的卷积核对一阶、第几特征的小邻域进行操作,而第二组卷积核则有效地对更宽的邻域进行操作,生成由先前特征组成的特征。
池化可以理解为提取卷积产生的特征中最关键的部分进行训练,往往进行几次卷积就要进行一次池化操作。

2.5. 整合网络

model = nn.Sequential(
            nn.Conv2d(3, 16, kernel_size=3, padding=1),
            nn.Tanh(),
            nn.MaxPool2d(2),
            nn.Conv2d(16, 8, kernel_size=3, padding=1),
            nn.Tanh(),
            nn.MaxPool2d(2),
            # ... 还需要补充的地方
            nn.Linear(8 * 8 * 8, 32),
            nn.Tanh(),
            nn.Linear(32, 2))

第1个卷积将我们从2个RGB通道带到16个RGB通道,因此给网络一个机会来生成16个独立的特征,以(希望)区分鸟和飞机的低级特征。然后应用Tanh活化函数。得到的有 16个通道的、32x32的图像被第1个MaxPool2d池化成有16个通道的、16×16的图像。在这一点上,下采样图像进行另一个卷积,产生一个有8个通道的、16×16的输出。如果幸运的话,这个输出将包含更高级的特性。同样,我们应用 Tanh 激活函数,然后将其池化到有8个通道的8x8的输出

计算一下这个小模型的参数数目

numel_list = [p.numel() for p in model.parameters()]
sum(numel_list), numel_list

运行一下这个模型

model(img.unsqueeze(0))

编译器将会报错,我们可以发现,这里还缺少从有8个通道的、8x8的图像转换为有512个元素的一维向量的步骤(忽律批处理的纬度)。


3. 训练我们的convnet

3.1. 训练循环

convnet的核心是2个嵌套的循环:

  • 一个是跨迭代周期的外部循环
  • 另一个是从数据集生成批次的DataLoader的内部循环

在每个循环中,我们都需要:

  • 通过模型提供输入(正向传播)
  • 计算损失(正向传播的一部分)
  • 将任何上一次的梯度归零
  • 调用loss.backward()来计算损失相对所有参数的梯度(反向传播)
  • 让优化器朝着更低的损失迈进
import datetime

def training_loop(n_epochs, optimizer, model, loss_fn, train_loader):
    for epoch in range(1, n_epochs + 1):  # 循环变了从1开始到n_epochs
        loss_train = 0.0
        for imgs, labels in train_loader:  # 在数据加载器为我们船舰的批中循环数据集

            outputs = model(imgs)  # 通过我们的模型提供一个批次

            loss = loss_fn(outputs, labels)  # 计算我们希望最小化的损失

            optimizer.zero_grad()  # 梯度归零

            loss.backward()  # 计算我们希望网络学习的参数的梯度

            optimizer.step()  # 更新模型

            loss_train += loss.item()  # 对整个训话遍历中得到的损失求和

        if epoch == 1 or epoch % 10 == 0:
            print('{} Epoch {}, Training loss {}'.format(
                datetime.datetime.now(), epoch,
                loss_train / len(train_loader)))  # 除以训练数据加载器的长度,得到每批平均损失
train_loader = torch.utils.data.DataLoader(cifar2, batch_size=64,
                                           shuffle=True)  # 数据加载器批量处理cifar2的样本数据集,并随机打乱数据集中样本的顺序

model = Net()  #  实例化网络
optimizer = optim.SGD(model.parameters(), lr=1e-2)  #  设置优化器
loss_fn = nn.CrossEntropyLoss()  #  设置损失函数

training_loop(  # 调用定义的训练循环
    n_epochs = 100,
    optimizer = optimizer,
    model = model,
    loss_fn = loss_fn,
    train_loader = train_loader,
)

2.2. 测量精度

比较模型在训练集和验证集上的精确度

train_loader = torch.utils.data.DataLoader(cifar2, batch_size=64,
                                           shuffle=False)
val_loader = torch.utils.data.DataLoader(cifar2_val, batch_size=64,
                                         shuffle=False)

def validate(model, train_loader, val_loader):
    for name, loader in [("train", train_loader), ("val", val_loader)]:
        correct = 0
        total = 0

        with torch.no_grad():  # 不需要梯度,因为在验证集上不希望更新参数
            for imgs, labels in loader:
                outputs = model(imgs)
                _, predicted = torch.max(outputs, dim=1) # 将最大值的索引作为输出,_为占位符
                total += labels.shape[0]  # 计算样本的数量,因为total会随着批处理的大小而增加
                correct += int((predicted == labels).sum())  # 比较具有最大概率的预测类和真实值标签,我们首先得到一个bool数组,统计这个批次中预测值和实际值一致的项的总数

        print("Accuracy {}: {:.2f}".format(name , correct / total))

validate(model, train_loader, val_loader)

2.3. 保存并加载我们的模型

  • 保存模型
torch.save(model.state_dict(), data_path + 'birds_vs_airplanes.pt')
  • 将参数加载到模型实例中
loaded_model = Net()  # 我们必须确保在报错模型状态和稍后加载模型状态期间不会改变Net的定义
loaded_model.load_state_dict(torch.load(data_path
                                        + 'birds_vs_airplanes.pt'))

2.4. 在GPU上训练

  • 设置变量device
device = (torch.device('cuda') if torch.cuda.is_available()
          else torch.device('cpu'))
print(f"Training on device {device}.")
  • 将待训练的张量移动到GPU上面
import datetime

def training_loop(n_epochs, optimizer, model, loss_fn, train_loader):
    for epoch in range(1, n_epochs + 1):
        loss_train = 0.0
        for imgs, labels in train_loader:
            imgs = imgs.to(device=device)  # 将imgs和labels移动到我们正在训练的设备上
            labels = labels.to(device=device)
            outputs = model(imgs)
            loss = loss_fn(outputs, labels)

            optimizer.zero_grad()
            loss.backward()
            optimizer.step()

            loss_train += loss.item()

        if epoch == 1 or epoch % 10 == 0:
            print('{} Epoch {}, Training loss {}'.format(
                datetime.datetime.now(), epoch,
                loss_train / len(train_loader)))
  • 实例化模型,并将其移动到和device对应的设备上面
train_loader = torch.utils.data.DataLoader(cifar2, batch_size=64,
                                           shuffle=True)

model = Net().to(device=device)  # 将我们的模型(所有参数)移动到GPU。如果你忘记将模型或输入移动到GPU,你会得到张量不在同一设备上的错误
optimizer = optim.SGD(model.parameters(), lr=1e-2)
loss_fn = nn.CrossEntropyLoss()

training_loop(
    n_epochs = 100,
    optimizer = optimizer,
    model = model,
    loss_fn = loss_fn,
    train_loader = train_loader,
)
  • 更新validate()
train_loader = torch.utils.data.DataLoader(cifar2, batch_size=64,
                                           shuffle=False)
val_loader = torch.utils.data.DataLoader(cifar2_val, batch_size=64,
                                         shuffle=False)
all_acc_dict = collections.OrderedDict()

def validate(model, train_loader, val_loader):
    accdict = {}
    for name, loader in [("train", train_loader), ("val", val_loader)]:
        correct = 0
        total = 0

        with torch.no_grad():
            for imgs, labels in loader:
                imgs = imgs.to(device=device)
                labels = labels.to(device=device)
                outputs = model(imgs)
                _, predicted = torch.max(outputs, dim=1) # <1>
                total += labels.shape[0]
                correct += int((predicted == labels).sum())

        print("Accuracy {}: {:.2f}".format(name , correct / total))
        accdict[name] = correct / total
    return accdict

all_acc_dict["baseline"] = validate(model, train_loader, val_loader)
  • 加载网络权重
    PyTorch将尝试将权重加载到与保存它的设备相同的设备上
    可通过在加载权重时,指示PyTorch覆盖设备信息会更加简洁
loaded_model = Net().to(device=device)
loaded_model.load_state_dict(torch.load(data_path
                                        + 'birds_vs_airplanes.pt',
                                        map_location=device))

3. 模型设计

3.1. 增加内存容量:宽度

在第1个卷积中指定更多的输出通道

class NetWidth(nn.Module):
    def __init__(self):
        super().__init__()
        self.conv1 = nn.Conv2d(3, 32, kernel_size=3, padding=1)
        self.conv2 = nn.Conv2d(32, 16, kernel_size=3, padding=1)
        self.fc1 = nn.Linear(16 * 8 * 8, 32)
        self.fc2 = nn.Linear(32, 2)

    def forward(self, x):
        out = F.max_pool2d(torch.tanh(self.conv1(x)), 2)
        out = F.max_pool2d(torch.tanh(self.conv2(out)), 2)
        out = out.view(-1, 16 * 8 * 8)
        out = torch.tanh(self.fc1(out))
        out = self.fc2(out)
        return out

通过传参指定宽度

class NetWidth(nn.Module):
    def __init__(self, n_chans1=32):
        super().__init__()
        self.n_chans1 = n_chans1
        self.conv1 = nn.Conv2d(3, n_chans1, kernel_size=3, padding=1)
        self.conv2 = nn.Conv2d(n_chans1, n_chans1 // 2, kernel_size=3,
                               padding=1)
        self.fc1 = nn.Linear(8 * 8 * n_chans1 // 2, 32)
        self.fc2 = nn.Linear(32, 2)

    def forward(self, x):
        out = F.max_pool2d(torch.tanh(self.conv1(x)), 2)
        out = F.max_pool2d(torch.tanh(self.conv2(out)), 2)
        out = out.view(-1, 8 * 8 * self.n_chans1 // 2)
        out = torch.tanh(self.fc1(out))
        out = self.fc2(out)
        return out
model = NetWidth(n_chans1=32).to(device=device)
optimizer = optim.SGD(model.parameters(), lr=1e-2)
loss_fn = nn.CrossEntropyLoss()

training_loop(
    n_epochs = 100,
    optimizer = optimizer,
    model = model,
    loss_fn = loss_fn,
    train_loader = train_loader,
)

all_acc_dict["width"] = validate(model, train_loader, val_loader)

查看现在模型参数的数量

sum(p.numel() for p in model.parameters())

3.2. 正则化

帮助我们的模型收敛和泛化
训练模型的2个关键步骤

  • 优化,当我们需要减少训练集上的损失时
  • 泛化,当模型不仅要处理训练集,还要处理一起没有见过的数据时

3.2.1. 检查参数:权重惩罚

稳定泛化的一种方法是在损失中添加一个正则化项。
这个术语的设计是为了减小模型本身的权重,从而限制训练对它们增长的影响。换向话说,这是对较大权重的惩罚。这使得损失更平滑,并且从拟合单个样本中获得的收益相对较少。
- L1正则化:模型中所有权重的绝对值之和
- L2正则化:模型中所有权重的平方和
- 它们都通过一个因子进行缩放,即超参数

L2 正则化也称为权重衰减。叫这个名字的原因是考虑到 SGD 和反向传播,L2正则化对参数w_i的负梯度为-2lambdaw_i,其中 lambda 是前面提到的超参数,在PyTorch中简称为权重衰减。因此,在损失函数中加人 L2 正则化,相当于在优化步骤中将每个权重按其当前值的比例递减(因此称为权重衰减)。注意,权重哀减适用于网络的所有参数,例如偏置。

def training_loop_l2reg(n_epochs, optimizer, model, loss_fn,
                        train_loader):
    for epoch in range(1, n_epochs + 1):
        loss_train = 0.0
        for imgs, labels in train_loader:
            imgs = imgs.to(device=device)
            labels = labels.to(device=device)
            outputs = model(imgs)
            loss = loss_fn(outputs, labels)

            l2_lambda = 0.001
            l2_norm = sum(p.pow(2.0).sum()
                          for p in model.parameters())
            loss = loss + l2_lambda * l2_norm

            optimizer.zero_grad()
            loss.backward()
            optimizer.step()

            loss_train += loss.item()
        if epoch == 1 or epoch % 10 == 0:
            print('{} Epoch {}, Training loss {}'.format(
                datetime.datetime.now(), epoch,
                loss_train / len(train_loader)))

model = Net().to(device=device)
optimizer = optim.SGD(model.parameters(), lr=1e-2)
loss_fn = nn.CrossEntropyLoss()

training_loop_l2reg(
    n_epochs = 100,
    optimizer = optimizer,
    model = model,
    loss_fn = loss_fn,
    train_loader = train_loader,
)
all_acc_dict["l2 reg"] = validate(model, train_loader, val_loader)

3.2.2. 不太依赖于单一输入:Dropout

思想:将网络每轮训练迭代中的神经元随机部分清零

Dropout 在每次迭代中有效地生成具有不同神经元拓扑的模型,使得模型中的神经元在过拟合过程中协调记忆过程的机会更少。另一种观点是,Dropout 在整个网络中干扰了模型生成的特征,产生了一种接近于增强的效果。
在 PyTorch 中,我们可以通过在非线性激活与后面的线性或卷积模块之间添加一个
加nn.Dropout 模块在模型中实现 Dropout。作为一个参数,我们需要指定输入归零的概率。如果是卷积,我们将使用专门的 nn.Dropout2d 或者nn.Dropout3d,将输人的所有通道归零:

class NetDropout(nn.Module):
    def __init__(self, n_chans1=32):
        super().__init__()
        self.n_chans1 = n_chans1
        self.conv1 = nn.Conv2d(3, n_chans1, kernel_size=3, padding=1)
        self.conv1_dropout = nn.Dropout2d(p=0.4)
        self.conv2 = nn.Conv2d(n_chans1, n_chans1 // 2, kernel_size=3,
                               padding=1)
        self.conv2_dropout = nn.Dropout2d(p=0.4)
        self.fc1 = nn.Linear(8 * 8 * n_chans1 // 2, 32)
        self.fc2 = nn.Linear(32, 2)

    def forward(self, x):
        out = F.max_pool2d(torch.tanh(self.conv1(x)), 2)
        out = self.conv1_dropout(out)
        out = F.max_pool2d(torch.tanh(self.conv2(out)), 2)
        out = self.conv2_dropout(out)
        out = out.view(-1, 8 * 8 * self.n_chans1 // 2)
        out = torch.tanh(self.fc1(out))
        out = self.fc2(out)
        return out
model = NetDropout(n_chans1=32).to(device=device)
optimizer = optim.SGD(model.parameters(), lr=1e-2)
loss_fn = nn.CrossEntropyLoss()

training_loop(
    n_epochs = 100,
    optimizer = optimizer,
    model = model,
    loss_fn = loss_fn,
    train_loader = train_loader,
)
all_acc_dict["dropout"] = validate(model, train_loader, val_loader)

3.2.3. 保持激活状态:批量归一化

批量归一化背后的主要思想是将输入重新调整到网络的激活状态,从而使小批量具有一定的理想分布。回想一下学习机制和非线性激活函数的作用,这有助于避免激活函数的输入过多地进入函数的饱和部分,从而消除梯度并减慢训练速度。

实际上,批量归一化使用在该中间位置收集的小批量样本的平均值和标准差来对中间输入进行移位和缩放。正则化效应是这样一个事实的结果,即单个样本及其下游激活函数总是被模型视为平移和缩放,这取决于随机提取的小批量的统计数据。

PyTorch 提供了 nn.BatchNormld、nn.BatchNorm2d 和 nn.BatchNorm3d 来实现批量归一化,使用哪种模块取决于输人的维度。由于批量归一化的目的是重新调整激活的输入,因此其位置是在线性变换和激活函数之后

class NetBatchNorm(nn.Module):
    def __init__(self, n_chans1=32):
        super().__init__()
        self.n_chans1 = n_chans1
        self.conv1 = nn.Conv2d(3, n_chans1, kernel_size=3, padding=1)
        self.conv1_batchnorm = nn.BatchNorm2d(num_features=n_chans1)
        self.conv2 = nn.Conv2d(n_chans1, n_chans1 // 2, kernel_size=3,
                               padding=1)
        self.conv2_batchnorm = nn.BatchNorm2d(num_features=n_chans1 // 2)
        self.fc1 = nn.Linear(8 * 8 * n_chans1 // 2, 32)
        self.fc2 = nn.Linear(32, 2)

    def forward(self, x):
        out = self.conv1_batchnorm(self.conv1(x))
        out = F.max_pool2d(torch.tanh(out), 2)
        out = self.conv2_batchnorm(self.conv2(out))
        out = F.max_pool2d(torch.tanh(out), 2)
        out = out.view(-1, 8 * 8 * self.n_chans1 // 2)
        out = torch.tanh(self.fc1(out))
        out = self.fc2(out)
        return out

3.3. 深入学习更复杂的结构:深度

增加深度与增加网络在处理输入时能够执行的操作序列的长度有关

3.3.1. 跳跃连接

损失函数对参数的导数,特别是在早期层中的导数,需要乘许多其他数字,这些数字来自损失和参数之问的导数运算链。这些被相乘的数字可能很小,生成的数字越来越小,也可能很大,由于浮点近似而吞并了更小的数字。最重要的是,一长串乘法会使参数对梯度的贡献消失,导致该层的训练无效、因为该参数和其他类似参数不会得到适当的更新。

跳跃连接只是将输入添加到层块的一个输出中

class NetRes(nn.Module):
    def __init__(self, n_chans1=32):
        super().__init__()
        self.n_chans1 = n_chans1
        self.conv1 = nn.Conv2d(3, n_chans1, kernel_size=3, padding=1)
        self.conv2 = nn.Conv2d(n_chans1, n_chans1 // 2, kernel_size=3,
                               padding=1)
        self.conv3 = nn.Conv2d(n_chans1 // 2, n_chans1 // 2,
                               kernel_size=3, padding=1)
        self.fc1 = nn.Linear(4 * 4 * n_chans1 // 2, 32)
        self.fc2 = nn.Linear(32, 2)

    def forward(self, x):
        out = F.max_pool2d(torch.relu(self.conv1(x)), 2)
        out = F.max_pool2d(torch.relu(self.conv2(out)), 2)
        out1 = out
        out = F.max_pool2d(torch.relu(self.conv3(out)) + out1, 2)
        out = out.view(-1, 4 * 4 * self.n_chans1 // 2)
        out = torch.relu(self.fc1(out))
        out = self.fc2(out)
        return out
model = NetRes(n_chans1=32).to(device=device)
optimizer = optim.SGD(model.parameters(), lr=1e-2)
loss_fn = nn.CrossEntropyLoss()

training_loop(
    n_epochs = 100,
    optimizer = optimizer,
    model = model,
    loss_fn = loss_fn,
    train_loader = train_loader,
)
all_acc_dict["res"] = validate(model, train_loader, val_loader)

缓解梯度消失的原因:
考虑到反向传播,我们可以理解一个跳跃连接或者深层网络中的一系列跳跃连接
——其创建了一条从深层参数到损失的直接路径,这使得它们对损失梯度的贡献更直接,因为损失相对这些参数的偏导数有可能不会和其他操作的长链相乘。

3.3.2. 使用PyTorch建立非常深的网络

通过定义构建块实现深层的网络架构

定义一个模块子类,包含一组组卷积、激活函数和跳跃连接

class ResBlock(nn.Module):
    def __init__(self, n_chans):
        super(ResBlock, self).__init__()
        self.conv = nn.Conv2d(n_chans, n_chans, kernel_size=3,
                              padding=1, bias=False)
        self.batch_norm = nn.BatchNorm2d(num_features=n_chans)
        torch.nn.init.kaiming_normal_(self.conv.weight,
                                      nonlinearity='relu')  # 批范数层会抵消偏执的影响,因此它通常被排除在外;自定义的初始化,使用RestNet论文中计算标准差的正态随机元素初始化
        torch.nn.init.constant_(self.batch_norm.weight, 0.5)
        torch.nn.init.zeros_(self.batch_norm.bias)

    def forward(self, x):
        out = self.conv(x)
        out = self.batch_norm(out)
        out = torch.relu(out)
        return out + x

为了生成一个深度模型,所以我们在块中添加了批量归一化,浙江有助于防止梯度在训练期间消失。

在定义网络时,定义了nn.Sequential,其确保一个块的输出被用作下一个块的输入,它还将取保块中的所有参数对网络是可见的

class NetResDeep(nn.Module):
    def __init__(self, n_chans1=32, n_blocks=10):
        super().__init__()
        self.n_chans1 = n_chans1
        self.conv1 = nn.Conv2d(3, n_chans1, kernel_size=3, padding=1)
        self.resblocks = nn.Sequential(
            *(n_blocks * [ResBlock(n_chans=n_chans1)]))
        self.fc1 = nn.Linear(8 * 8 * n_chans1, 32)
        self.fc2 = nn.Linear(32, 2)

    def forward(self, x):
        out = F.max_pool2d(torch.relu(self.conv1(x)), 2)
        out = self.resblocks(out)
        out = F.max_pool2d(out, 2)
        out = out.view(-1, 8 * 8 * self.n_chans1)
        out = torch.relu(self.fc1(out))
        out = self.fc2(out)
        return out

总结

本文主要讲解了:

  • 理解什么是卷积
  • 构建卷积神经网络
  • 创建自定义的nn.Modeule的子类
  • 模块和函数API之间的区别
  • 神经网络的设计选择

你可能感兴趣的:(#,PyTorch,pytorch,学习,深度学习)