《深入浅出PyTorch》学习笔记——第三章:PyTorch的主要组成模块

目录

0 完整章节内容

1 思考:完成一套深度学习流程需要哪些关键环节

2 基本配置

3 数据读入

3.1 自定义数据集类——例1

3.2 自定义数据集类——例2 

4 模型构建

4.1 神经网络的构造

4.2 神经网络中常见的层

4.2.1 不含模型参数的层

4.2.2 含模型参数的层

4.2.3 二维卷积层

4.2.4 池化层

4.3 torch.nn 和 torch.nn.functional的区别与联系

4.3.1. torch.nn

4.3.2. torch.nn.functional

4.3.3 总结

4.4 模型示例

4.4.1 LeNet

4.4.2 AlexNet

5 模型初始化

5.1 torch.nn.init内容

5.2 torch.nn.init使用

5.3 初始化函数的封装

6 损失函数

6.1 二分类交叉熵损失函数

6.2 交叉熵损失函数

6.3 L1损失函数

6.4 MSE损失函数

6.5 平滑L1 (Smooth L1)损失函数

6.6 目标泊松分布的负对数似然损失

6.7 KL散度

6.8 MarginRankingLoss

6.9 多标签边界损失函数

6.10 二分类损失函数

6.11 多分类的折页损失

6.12 三元组损失

6.13 HingEmbeddingLoss

6.14 余弦相似度

6.15 CTC损失函数

6.16 各损失函数的应用场景

7 训练和评估

8 可视化

9 优化器

9.1 Pytorch提供的优化器

9.2 实际操作

9.3 输出结果

9.4 实验

9.4.1 导包

9.4.2 数据集

9.4.3 构建Dataloader

9.4.4 定义损失函数

9.4.5 定义网络结构

9.4.6 定义优化器

9.4.7 定义模型训练函数

9.4.8 开始训练

9.4.9 查看loss曲线


0 完整章节内容

《深入浅出PyTorch》学习笔记-优快云博客

1 思考:完成一套深度学习流程需要哪些关键环节

经过本节的学习,你将收获:

  • 一项机器学习/深度学习任务的整体流程

  • 在机器学习和深度学习的每个部分的作用

一项机器学习任务时常常有以下的几个重要步骤,首先是数据的预处理,其中重要的步骤包括数据格式的统一、异常数据的消除和必要的数据变换,同时划分训练集、验证集、测试集,常见的方法包括:按比例随机选取,KFold方法(我们可以使用sklearn带的test_train_split函数、kfold来实现)。接下来选择模型,并设定损失函数和优化方法,以及对应的超参数(当然可以使用sklearn这样的机器学习库中模型自带的损失函数和优化器)。最后用模型去拟合训练集数据,并在验证集/测试集上计算模型表现

深度学习和机器学习在流程上类似,但在代码实现上有较大的差异。首先,由于深度学习所需的样本量很大,一次加载全部数据运行可能会超出内存容量而无法实现;同时还有批(batch)训练等提高模型表现的策略,需要每次训练读取固定数量的样本送入模型中训练,因此深度学习在数据加载上需要有专门的设计。

在模型实现上,深度学习和机器学习也有很大差异。由于深度神经网络层数往往较多,同时会有一些用于实现特定功能的层(如卷积层、池化层、批正则化层、LSTM层等),因此深度神经网络往往需要“逐层”搭建,或者预先定义好可以实现特定功能的模块,再把这些模块组装起来。这种“定制化”的模型构建方式能够充分保证模型的灵活性,也对代码实现提出了新的要求。

接下来是损失函数和优化器的设定。这部分和经典机器学习的实现是类似的。但由于模型设定的灵活性,因此损失函数和优化器要能够保证反向传播能够在用户自行定义的模型结构上实现

上述步骤完成后就可以开始训练了。我们前面介绍了GPU的概念和GPU用于并行计算加速的功能,不过程序默认是在CPU上运行的,因此在代码实现中,需要把模型和数据“放到”GPU上去做运算,同时还需要保证损失函数和优化器能够在GPU上工作。如果使用多张GPU进行训练,还需要考虑模型和数据分配、整合的问题。此外,后续计算一些指标还需要把数据“放回”CPU。这里涉及到了一系列有关于GPU的配置和操作

深度学习中训练和验证过程最大的特点在于读入数据是按批的,每次读入一个批次的数据,放入GPU中训练,然后将损失函数反向传播回网络最前面的层,同时使用优化器调整网络参数。这里会涉及到各个模块配合的问题。训练/验证后还需要根据设定好的指标计算模型表现。

经过以上步骤,一个深度学习任务就完成了。我们在详细讲解每个部分之前,先梳理了完成各个部分所需的功能,下面我们就去进一步了解一下PyTorch是如何实现各个部分的,以及PyTorch作为一个深度学习框架拥有的模块化特点。

2 基本配置

对于一个PyTorch项目,我们需要导入一些Python常用的包来帮助我们快速实现功能。常见的包有os、numpy等,此外还需要调用PyTorch自身一些模块便于灵活使用,比如torch、torch.nn、torch.utils.data.Dataset、torch.utils.data.DataLoader、torch.optimizer等等。

经过本节的学习,你将收获:

  • 在深度学习/机器学习中常用到的包

  • GPU的配置

首先导入必须的包。注意这里只是建议导入的包导入的方式,可以采用不同的方案,比如涉及到表格信息的读入很可能用到pandas,对于不同的项目可能还需要导入一些更上层的包如cv2等。如果涉及可视化还会用到matplotlib、seaborn等。涉及到下游分析和指标计算也常用到sklearn。

import os 
import numpy as np 
import torch
import torch.nn as nn
from torch.utils.data import Dataset, DataLoader
import torch.optim as optimizer

根据前面我们对深度学习任务的梳理,有如下几个超参数可以统一设置,方便后续调试时修改:

  • batch size

  • 初始学习率(初始)

  • 训练次数(max_epochs)

  • GPU配置

batch_size = 16
# 批次的大小
lr = 1e-4
# 优化器的学习率
max_epochs = 100

除了直接将超参数设置在训练的代码里,我们也可以使用yaml、json,dict等文件来存储超参数,这样可以方便后续的调试和修改,这种方式也是常见的深度学习库(mmdetection,Paddledetection,detectron2)和一些AI Lab里面比较常见的一种参数设置方式。

我们的数据和模型如果没有经过显式指明设备,默认会存储在CPU上,为了加速模型的训练,我们需要显式调用GPU,一般情况下GPU的设置有两种常见的方式:

# 方案一:使用os.environ,这种情况如果使用GPU不需要设置
import os
os.environ['CUDA_VISIBLE_DEVICES'] = '0,1' # 指明调用的GPU为0,1号

# 方案二:使用“device”,后续对要使用GPU的变量用.to(device)即可
device = torch.device("cuda:1" if torch.cuda.is_available() else "cpu") # 指明调用的GPU为1号

当然还会有一些其他模块或用户自定义模块会用到的参数,有需要也可以在一开始进行设置。  

3 数据读入

PyTorch数据读入是通过Dataset+DataLoader的方式完成的,Dataset定义好数据的格式和数据变换形式,DataLoader用iterative的方式不断读入批次数据。

经过本节的学习,你将收获:

  • PyTorch常见的数据读取方式

  • 构建自己的数据读取流程

我们可以定义自己的Dataset类来实现灵活的数据读取,定义的类需要继承PyTorch自身的Dataset类。主要包含三个函数:

  • __init__: 用于向类中传入外部参数,同时定义样本集

  • __getitem__: 用于逐个读取样本集合中的元素,可以进行一定的变换,并将返回训练/验证所需的数据

  • __len__: 用于返回数据集的样本数

下面以cifar10数据集为例给出构建Dataset类的方式:

import torch
from torchvision import datasets
train_data = datasets.ImageFolder(train_path, transform=data_transform)
val_data = datasets.ImageFolder(val_path, transform=data_transform)

这里使用了PyTorch自带的ImageFolder类的用于读取按一定结构存储的图片数据(path对应图片存放的目录,目录下包含若干子目录,每个子目录对应属于同一个类的图片)。

其中data_transform可以对图像进行一定的变换,如翻转、裁剪等操作,可自己定义。这里我们会在下一章通过实战加以介绍并在notebook中做了示例代码。

这里我们给出一个自己定制Dataset的例子

import os
import pandas as pd
from torchvision.io import read_image
from torch.utils.data import Dataset

class MyDataset(Dataset):
    def __init__(self, annotations_file, img_dir, transform=None, target_transform=None):
        """
        初始化方法,用于定义数据集的基本属性。
        
        参数:
            annotations_file (string): 包含图像文件名和对应标签的 CSV 文件路径。
            img_dir (string): 存放所有图像文件的目录路径。
            transform (callable, optional): 对图像样本应用的可选转换函数。
            target_transform (callable, optional): 对标签应用的可选转换函数。
        """
        self.img_labels = pd.read_csv(annotations_file)  # 读取CSV文件,存储图像文件名及其标签
        self.img_dir = img_dir  # 图像文件所在的目录
        self.transform = transform  # 图像转换函数
        self.target_transform = target_transform  # 标签转换函数

    def __len__(self):
        """
        返回数据集的样本数量。
        """
        return len(self.img_labels)

    def __getitem__(self, idx):
        """
        获取指定索引的图像及其对应标签。
        
        参数:
            idx (int): 索引值
        
        返回:
            image (Tensor): 图像的张量表示。
            label: 图像对应的标签。
        """
        # 构造图像的完整路径
        img_path = os.path.join(self.img_dir, self.img_labels.iloc[idx, 0])
        # 读取图像为张量
        image = read_image(img_path)
        # 获取标签
        label = self.img_labels.iloc[idx, 1]
        # 如果定义了图像转换函数,则对图像应用转换
        if self.transform:
            image = self.transform(image)
        # 如果定义了标签转换函数,则对标签应用转换
        if self.target_transform:
            label = self.target_transform(label)
        return image, label

其中,我们的标签类似于以下的形式:

image1.jpg, 0
image2.jpg, 1
......
image9.jpg, 9 

构建好Dataset后,就可以使用DataLoader来按批次读入数据了,实现代码如下:

train_data = MyDataset(
    annotations_file="train_labels.csv",
    img_dir="images/train",
    transform=train_transform,
)

test_data = MyDataset(
    annotations_file="test_labels.csv", img_dir="images/test", transform=test_transform
)
from torch.utils.data import DataLoader

train_loader = torch.utils.data.DataLoader(train_data, batch_size=batch_size, num_workers=4, shuffle=True, drop_last=True)
val_loader = torch.utils.data.DataLoader(val_data, batch_size=batch_size, num_workers=4, shuffle=False)

其中:

  • batch_size:样本是按“批”读入的,batch_size就是每次读入的样本数,每次只用一个batch_size的数据进行前向传播、损失计算和反向传播,从而更新模型参数。

  • num_workers:有多少个进程用于读取数据,Windows下该参数设置为0,Linux下常见的为4或者8,根据自己的电脑配置来设置

  • shuffle:是否将读入的数据打乱,一般在训练集中设置为True,验证集中设置为False

  • drop_last:对于样本最后一部分没有达到批次数的样本,使其不再参与训练 

这里可以看一下我们的加载的数据。PyTorch中的DataLoader的读取可以使用next和iter来完成

import matplotlib.pyplot as plt
images, labels = next(iter(val_loader))
print(images.shape)
plt.imshow(images[0].transpose(1,2,0))
plt.show()

3.1 自定义数据集类——例1

from torch.utils.data import Dataset, DataLoader

# 简单的自定义数据集
class MyDataset(Dataset):
    def __init__(self, data):
        self.data = data  # 假设 data 是一个列表

    def __len__(self):
        return len(self.data)  # 数据集大小

    def __getitem__(self, idx):
        return self.data[idx]  # 返回索引对应的样本

# 数据
data = [1, 2, 3, 4, 5]
dataset = MyDataset(data)

# 通过 DataLoader 读取数据
dataloader = DataLoader(dataset, batch_size=2, shuffle=True)

# 遍历数据
for batch in dataloader:
    print(batch)

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

3.2 自定义数据集类——例2 

import os
from PIL import Image
from torch.utils.data import Dataset

class ImageDataset(Dataset):
    def __init__(self, img_dir, transform=None):
        self.img_dir = img_dir
        self.img_names = os.listdir(img_dir)
        self.transform = transform

    def __len__(self):
        return len(self.img_names)

    def __getitem__(self, idx):
        img_path = os.path.join(self.img_dir, self.img_names[idx])
        image = Image.open(img_path)
        
        if self.transform:
            image = self.transform(image)
        
        return image

# 假设有一个图像文件夹 "./images/"
from torchvision.transforms import ToTensor
dataset = ImageDataset(img_dir="./images/", transform=ToTensor())

# 使用 DataLoader 加载
dataloader = DataLoader(dataset, batch_size=4, shuffle=True)
for batch in dataloader:
    print(batch.size())

假如该目录下只有一个图片:

torch.Size([1, 3, 1660, 1242])

4 模型构建

人工智能的第三次浪潮受益于卷积神经网络的出现和BP反向传播算法的实现,随着深度学习的发展,研究人员研究出了许许多多的模型,PyTorch中神经网络构造一般是基于nn.Module类的模型来完成的,它让模型构造更加灵活。

经过本节的学习,你将收获:

  • PyTorch中神经网络的构造方法

  • PyTorch中特殊层的构建

  • LeNet的PyTorch实现

4.1 神经网络的构造

Module 类是 torch.nn 模块里提供的一个模型构造类,是所有神经网络模块的基类,我们可以继承它来定义我们想要的模型。

也可以参见官网对torch.Module的解释:Module — PyTorch 2.5 documentation

nn.Module 的自定义定义中,__init__forward 是必须的核心函数,但根据需求,也可以覆盖其他函数来实现额外的功能或灵活性。

__init__:

  • 用于定义模型的结构和参数。
  • 初始化层(如卷积层、线性层)、权重、偏置以及任何其他需要的模型组件。

forward:

  • 定义模型的前向传播逻辑,描述输入如何经过定义的层及运算得到输出。
  • 当你调用模型实例时(如 output = model(input)),forward 会被自动调用。

为什么 __init__forward 是必须的?

  • __init__ 定义了模型的基本组件,而 forward 描述了这些组件的计算逻辑。
  • 这是 PyTorch 模块化设计的核心,允许你自由组合和定义计算流程。

什么时候需要其他函数?

  • 模型有额外的行为需要实现,例如:
    • 需要打印详细信息(覆盖 extra_repr)。
    • 动态生成或更新参数(自定义参数初始化逻辑)。
    • 自定义保存和加载模型逻辑。
    • 添加辅助函数处理复杂计算,方便 forward 调用。

下面继承 Module 类构造多层感知机。这里定义的 MLP 类重载了 Module 类的 __init__ 函数和 forward 函数。它们分别用于创建模型参数和定义前向计算(正向传播)。下面的 MLP 类定义了一个具有两个隐藏层的多层感知机。

import torch
from torch import nn


class MLP(nn.Module):
    # 声明带有模型参数的层,这里声明了两个全连接层
    def __init__(self, **kwargs):
        # 调用MLP父类Block的构造函数来进行必要的初始化。这样在构造实例时还可以指定其他函数
        # 你可以把 super(MLP, self).__init__(**kwargs) 想象成一句“继承父类的基本功能,别忘了初始化父类的一些必要部分”。
        super(MLP, self).__init__(**kwargs)
        self.hidden = nn.Linear(784, 256)
        self.act = nn.ReLU()
        self.output = nn.Linear(256, 10)

    # 定义模型的前向计算,即如何根据输入x计算返回所需要的模型输出
    def forward(self, x):
        o = self.act(self.hidden(x))
        return self.output(o)

注:这里的self.hidden、act、ouput等都是你自己指定命名的,可以按照自己喜好进行更改。 

以上的 MLP 类中⽆须定义反向传播函数。系统将通过⾃动求梯度⽽自动⽣成反向传播所需的 backward 函数。  

我们可以实例化 MLP 类得到模型变量 net 。下⾯的代码初始化 net 并传入输⼊数据 X 做一次前向计算。其中, net(X) 会调用 MLP 继承⾃自 Module 类的 __call__ 函数,这个函数将调⽤用 MLP 类定义的forward 函数来完成前向计算。因此我们自己构造模型时需要明确定义模型的forward过程

在 PyTorch 中,nn.Module 类的 __call__ 方法是一个非常关键的功能,它的作用是让模型对象(继承自 nn.Module 的类实例)可以像函数一样被调用。简单来说,当你调用一个 nn.Module 实例时(例如 output = model(input)),背后实际调用的就是 __call__ 方法。

MLP(
  (hidden): Linear(in_features=784, out_features=256, bias=True)
  (act): ReLU()
  (output): Linear(in_features=256, out_features=10, bias=True)
)

tensor([[-0.0112,  0.1593, -0.1599,  0.0178,  0.0718,  0.0186,  0.0311, -0.1528,
          0.2062, -0.1399],
        [ 0.0359,  0.2079,  0.0261, -0.0441,  0.1552, -0.0692,  0.0219, -0.0616,
          0.1161, -0.0473]], grad_fn=<AddmmBackward>) 

注意,这里并没有将 Module 类命名为 Layer (层)或者 Model (模型)之类的名字,这是因为该类是一个可供⾃由组建的部件。它的子类既可以是⼀个层(如PyTorch提供的 Linear 类),⼜可以是一个模型(如这里定义的 MLP 类),或者是模型的⼀个部分。  

4.2 神经网络中常见的层

深度学习的一个魅力在于神经网络中各式各样的层,例如全连接层、卷积层、池化层与循环层等等。虽然PyTorch提供了⼤量常用的层,但有时候我们依然希望⾃定义层。这里我们会介绍如何使用 Module 来自定义层,从而可以被反复调用。

4.2.1 不含模型参数的层

我们先介绍如何定义一个不含模型参数的自定义层。下⾯构造的 MyLayer 类通过继承 Module 类自定义了一个将输入减掉均值后输出的层,并将层的计算定义在了 forward 函数里。这个层里不含模型参数。

import torch
from torch import nn

class MyLayer(nn.Module):
    def __init__(self, **kwargs):
        super(MyLayer, self).__init__(**kwargs)
    def forward(self, x):
        return x - x.mean()  

测试,实例化该层,然后做前向计算

layer = MyLayer()
layer(torch.tensor([1, 2, 3, 4, 5], dtype=torch.float))

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

4.2.2 含模型参数的层

我们还可以自定义含模型参数的自定义层。其中的模型参数可以通过训练学出。

Parameter 类其实是 Tensor 的子类,如果一个 TensorParameter ,那么它会⾃动被添加到模型的参数列表里。所以在⾃定义含模型参数的层时,我们应该将参数定义成 Parameter ,除了直接定义成 Parameter 类外,还可以使⽤ ParameterListParameterDict 分别定义参数的列表和字典。

ParameterList :

class MyListDense(nn.Module):
    def __init__(self):
        super(MyListDense, self).__init__()
        self.params = nn.ParameterList(
            [nn.Parameter(torch.randn(4, 4)) for i in range(3)]
        )
        self.params.append(nn.Parameter(torch.randn(4, 1)))

    def forward(self, x):
        for i in range(len(self.params)):
            # torch.mm 是 PyTorch 的矩阵乘法操作,输入 x 和当前参数相乘。
            x = torch.mm(x, self.params[i])
        return x


net = MyListDense()
x = torch.rand(2,4)
print(net)
print(net(x))

MyListDense(
  (params): ParameterList(
      (0): Parameter containing: [torch.FloatTensor of size 4x4]
      (1): Parameter containing: [torch.FloatTensor of size 4x4]
      (2): Parameter containing: [torch.FloatTensor of size 4x4]
      (3): Parameter containing: [torch.FloatTensor of size 4x1]
  )
)
tensor([[-1.4022],
        [ 0.1373]], grad_fn=<MmBackward>) 

ParameterDict: 

class MyDictDense(nn.Module):
    def __init__(self):
        super(MyDictDense, self).__init__()
        self.params = nn.ParameterDict(
            {
                "linear1": nn.Parameter(torch.randn(4, 4)),
                "linear2": nn.Parameter(torch.randn(4, 1)),
            }
        )
        self.params.update({"linear3": nn.Parameter(torch.randn(4, 2))})  # 新增

    def forward(self, x, choice="linear1"):
        return torch.mm(x, self.params[choice])


net = MyDictDense()
x = torch.rand(2, 4)
print(net)
print(net(x))

MyDictDense(
  (params): ParameterDict(
      (linear1): Parameter containing: [torch.FloatTensor of size 4x4]
      (linear2): Parameter containing: [torch.FloatTensor of size 4x1]
      (linear3): Parameter containing: [torch.FloatTensor of size 4x2]
  )
)
tensor([[-0.6617,  1.7359, -0.3392, -0.0664],
        [-0.5069,  1.3711, -0.3191, -0.4450]], grad_fn=<MmBackward>)

下面给出常见的神经网络的一些层,比如卷积层、池化层,以及较为基础的AlexNet,LeNet等。

4.2.3 二维卷积层

二维卷积层将输入和卷积核做互相关运算,并加上一个标量偏差来得到输出。卷积层的模型参数包括了卷积核和标量偏差。在训练模型的时候,通常我们先对卷积核随机初始化,然后不断迭代卷积核和偏差。

import torch
from torch import nn


# 卷积运算(二维互相关)
def corr2d(X, K):
    h, w = K.shape
    X, K = X.float(), K.float()
    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


# 二维卷积层
class Conv2D(nn.Module):
    def __init__(self, kernel_size):
        super(Conv2D, self).__init__()
        self.weight = nn.Parameter(torch.randn(kernel_size))
        self.bias = nn.Parameter(torch.randn(1))

    def forward(self, x):
        return corr2d(x, self.weight) + self.bias
    
kernel_size = (4, 4)
x = torch.rand(5, 5)
net = Conv2D(kernel_size=kernel_size)
print(net)
print(net(x))

Conv2D()
tensor([[ 1.1300, -0.7518],
        [-0.6968, -2.2891]], grad_fn=<AddBackward0>)

卷积窗口形状为 p * q 的卷积层称为 p * q 卷积层。同样, p * q 卷积或 p * q 卷积核说明卷积核的高和宽分别为 p和 q,一般情况下,p=q。

填充(padding)是指在输⼊高和宽的两侧填充元素(通常是0元素)。

下面的例子里我们创建一个⾼和宽为3的二维卷积层(nn中自定义的)。

然后设输⼊高和宽两侧的填充数分别为1。给定一个高和宽为8的输入,我们发现输出的高和宽也是8(8+2-3+1=8)。

import torch
from torch import nn


# 定义一个函数来计算卷积层。它对输入和输出做相应的升维和降维
def comp_conv2d(conv2d, X):
    # (1, 1)代表批量大小和通道数
    X = X.view((1, 1) + X.shape)
    Y = conv2d(X)
    return Y.view(Y.shape[2:])  # 排除不关心的前两维:批量和通道


# 注意这里是两侧分别填充1⾏或列,所以在两侧一共填充2⾏或列
conv2d = nn.Conv2d(in_channels=1, out_channels=1, kernel_size=3, padding=1)

X = torch.rand(8, 8)
comp_conv2d(conv2d, X).shape

torch.Size([8, 8]) 

在 PyTorch 的 torch.nn.Conv2d 中,默认的数据形状是 (batch_size, channels, height, width),也被称为 NCHW 格式:

  • N (Batch size):表示批次的大小,一次输入多少张图片或多少个样本。
  • C (Channels):表示通道数,灰度图像通常是 1,RGB 图像是 3。
  • H (Height):表示图片的高度(行数)。
  • W (Width):表示图片的宽度(列数)。

当卷积核的高和宽不同时,我们也可以通过设置高和宽上不同的填充数使输出和输入具有相同的高和宽。

# 使用高为5、宽为3的卷积核。在⾼和宽两侧的填充数分别为2和1
conv2d = nn.Conv2d(in_channels=1, out_channels=1, kernel_size=(5, 3), padding=(2, 1))
comp_conv2d(conv2d, X).shape

 torch.Size([8, 8])

在二维互相关运算中,卷积窗口从输入数组的最左上方开始,按从左往右、从上往下 的顺序,依次在输⼊数组上滑动。我们将每次滑动的行数和列数称为步幅(stride)。  

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

torch.Size([2, 2]) 

  • 填充可以增加输出的高和宽。这常用来使输出与输入具有相同的高和宽。
  • 步幅可以减小输出的高和宽,例如输出的高和宽仅为输入的高和宽的 ( 为大于1的整数)。  

4.2.4 池化层

池化层每次对输入数据的一个固定形状窗口(⼜称池化窗口)中的元素计算输出。不同于卷积层里计算输⼊和核的互相关性,池化层直接计算池化窗口内元素的属性(均值、最大值等)。常见的池化包括最大池化或平均池化。在二维最⼤池化中,池化窗口从输入数组的最左上方开始,按从左往右、从上往下的顺序,依次在输⼊数组上滑动。当池化窗口滑动到某⼀位置时,窗口中的输入子数组的最大值即输出数组中相应位置的元素。

下面把池化层的前向计算实现在pool2d函数里。

import torch
from torch import nn


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
X = torch.tensor([[0, 1, 2], [3, 4, 5], [6, 7, 8]], dtype=torch.float)
pool2d(X, (2, 2))

tensor([[4., 5.],
        [7., 8.]])

pool2d(X, (2, 2), 'avg')

 tensor([[2., 3.],
        [5., 6.]])

我们可以使用torch.nn包来构建神经网络。我们已经介绍了autograd包,nn包则依赖于autograd包来定义模型并对它们求导。一个nn.Module包含各个层和一个forward(input)方法,该方法返回output

4.3 torch.nntorch.nn.functional的区别与联系

4.3.1. torch.nn

  • 模块化封装torch.nn 提供了一个封装好的类接口,像 nn.Linearnn.Conv2dnn.ReLU 等,这些类不仅定义了网络层的计算逻辑,还会自动包含参数(如权重和偏置)。
  • 适合构建模型:使用 torch.nn 时,你直接实例化这些类,它们会在 forward 中自动调用其功能,并维护模型的参数。

4.3.2. torch.nn.functional

  • 函数式操作torch.nn.functional 提供的是一些函数,这些函数通常是 torch.nn 中模块的底层实现,比如 F.relunn.ReLU 的函数式版本,F.linearnn.Linear 的实现。
  • 不包含状态:使用 torch.nn.functional 时,参数(如权重和偏置)需要显式传入,因为这些函数是无状态的,不会自动维护参数。

4.3.3 总结

4.4 模型示例

LeNet和AlexNet都是卷积神经网络发展历程中的经典网络,如果你对这两个网络还不了解,请参见我之前的一篇笔记:

第四课:卷积神经网络_卷积神经网络的发展历程-优快云博客

4.4.1 LeNet

这是一个简单的前馈神经网络 (feed-forward network)(LeNet)。它接受一个输入,然后将它送入下一层,一层接一层的传递,最后给出输出。

一个神经网络的典型训练过程如下:

  1. 定义包含一些可学习参数(或者叫权重)的神经网络

  2. 在输入数据集上迭代

  3. 通过网络处理输入

  4. 计算 loss (输出和正确答案的距离)

  5. 将梯度反向传播给网络的参数

  6. 更新网络的权重,一般使用一个简单的规则:weight = weight - learning_rate * gradient

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


class Net(nn.Module):
    def __init__(self):
        super(Net, self).__init__()
        # 定义第一个卷积层,输入通道数为1,输出通道数为6,卷积核大小为5x5,步幅为1
        self.conv_layer1 = nn.Conv2d(
            in_channels=1, out_channels=6, kernel_size=5, stride=1
        )
        # 定义第二个卷积层,输入通道数为6,输出通道数为16,卷积核大小为5x5,步幅为1
        self.conv_layer2 = nn.Conv2d(
            in_channels=6, out_channels=16, kernel_size=5, stride=1
        )
        # 定义第一个全连接层,将16个5x5特征图展平后连接到120个神经元
        self.fc1 = nn.Linear(in_features=16 * 5 * 5, out_features=120)
        # 定义第二个全连接层,将120个神经元连接到84个神经元
        self.fc2 = nn.Linear(in_features=120, out_features=84)
        # 定义输出层,将84个神经元连接到10个输出类别
        self.fc3 = nn.Linear(in_features=84, out_features=10)

    def forward(self, x):
        """
        前向传播函数,用于定义数据流经模型的计算过程。
        """
        # 通过第一个卷积层和激活函数后,进行2x2最大池化
        x = F.max_pool2d(F.relu(self.conv_layer1(x)), kernel_size=2)
        # 通过第二个卷积层和激活函数后,进行2x2最大池化
        x = F.max_pool2d(F.relu(self.conv_layer2(x)), kernel_size=2)
        # 展平张量,准备输入全连接层
        x = x.view(-1, self.calculate_flatten_size(x))
        # 通过第一个全连接层和ReLU激活函数
        x = F.relu(self.fc1(x))
        # 通过第二个全连接层和ReLU激活函数
        x = F.relu(self.fc2(x))
        # 通过最后的全连接层得到输出(PyTorch 中没有直接内置的 nn.Gaussian 模块,这里用全连接层代替)
        x = self.fc3(x)
        return x

    def calculate_flatten_size(self, x):
        """
        计算展平后的张量大小。
        Args:
            x (torch.Tensor): 输入张量。
        Returns:
            int: 展平后的大小。
        """
        # 只计算张量的通道数和空间维度大小,不包括批次大小
        flattened_size = 1
        for dim in x.size()[1:]:
            flattened_size *= dim
        return flattened_size


# 创建模型实例
net = Net()
print(net)

Net(
  (conv_layer1): Conv2d(1, 6, kernel_size=(5, 5), stride=(1, 1))
  (conv_layer2): Conv2d(6, 16, kernel_size=(5, 5), stride=(1, 1))
  (fc1): Linear(in_features=400, out_features=120, bias=True)
  (fc2): Linear(in_features=120, out_features=84, bias=True)
  (fc3): Linear(in_features=84, out_features=10, bias=True)

我们只需要定义 forward 函数,backward函数会在使用autograd时自动定义,backward函数用来计算导数。我们可以在 forward 函数中使用任何针对张量的操作和计算。

一个模型的可学习参数可以通过net.parameters()返回

params = list(net.parameters())
print(len(params))
print(params[0].size())  # conv1的权重

10
torch.Size([6, 1, 5, 5]) 

注:在 PyTorch 中,卷积层的权重张量的形状是按照以下顺序排列的:(out_channels, in_channels, kernel_height, kernel_width)。

让我们尝试一个随机的 32x32 的输入。注意:这个网络 (LeNet)的期待输入是 32x32 的张量。如果使用 MNIST 数据集来训练这个网络,要把图片大小重新调整到 32x32。  

torch.manual_seed(42)  # 设置随机种子为 42
input = torch.randn(1, 1, 32, 32)
out = net(input)
print(out)

tensor([[-0.0881,  0.0762,  0.0108,  0.1320,  0.0529,  0.1117,  0.0431, -0.0201,
          0.0652,  0.0300]], grad_fn=<AddmmBackward>) 

清零所有参数的梯度缓存,然后进行随机梯度的反向传播:  

net.zero_grad()
out.backward(torch.randn(1, 10))# 由于out不是标量,因此需要乘上这个v

注意:torch.nn只支持小批量处理 (mini-batches)。整个 torch.nn 包只支持小批量样本的输入,不支持单个样本的输入。比如,nn.Conv2d 接受一个4维的张量,即nSamples x nChannels x Height x Width 如果是一个单独的样本,只需要使用input.unsqueeze(0) 来添加一个“假的”批大小维度。

  • torch.Tensor - 一个多维数组,支持诸如backward()等的自动求导操作,同时也保存了张量的梯度。

  • nn.Module - 神经网络模块。是一种方便封装参数的方式,具有将参数移动到GPU、导出、加载等功能。

  • nn.Parameter - 张量的一种,当它作为一个属性分配给一个Module时,它会被自动注册为一个参数。

  • autograd.Function - 实现了自动求导前向和反向传播的定义,每个Tensor至少创建一个Function节点,该节点连接到创建Tensor的函数并对其历史进行编码。

4.4.2 AlexNet

nn.Sequential 是 PyTorch 中的一个容器,用于将多个层按顺序连接在一起,简化模型的定义和代码结构。它将模型的各个层逐个顺序排列,输入数据按顺序传递给每一层,然后逐步计算输出。下面我们将使用这个容器来构建模型,将卷积、池化层归为一个模块,全连接层归为另一个模块,使得网络结构更加规范严谨。

主要特点:

  1. 简化模型构建如果模型的层按顺序排列,并且没有分支结构,可以使用 nn.Sequential 来简化代码,避免显式定义 forward 方法。
  2. 顺序执行nn.Sequential 中的各层会按添加的顺序依次执行,因此非常适合像传统的卷积神经网络(CNN)这种层与层之间有顺序关系的网络。

官方文档:Sequential — PyTorch 2.5 documentation 

class AlexNet(nn.Module):
    def __init__(self):
        super(AlexNet, self).__init__()
        self.conv = nn.Sequential(
            nn.Conv2d(
                1, 96, 11, 4
            ),  # in_channels, out_channels, kernel_size, stride, padding
            nn.ReLU(),
            nn.MaxPool2d(3, 2),  # kernel_size, stride
            # 减小卷积窗口,使用填充为2来使得输入与输出的高和宽一致,且增大输出通道数
            nn.Conv2d(96, 256, 5, 1, 2),
            nn.ReLU(),
            nn.MaxPool2d(3, 2),
            # 连续3个卷积层,且使用更小的卷积窗口。除了最后的卷积层外,进一步增大了输出通道数。
            # 前两个卷积层后不使用池化层来减小输入的高和宽
            nn.Conv2d(256, 384, 3, 1, 1),
            nn.ReLU(),
            nn.Conv2d(384, 384, 3, 1, 1),
            nn.ReLU(),
            nn.Conv2d(384, 256, 3, 1, 1),
            nn.ReLU(),
            nn.MaxPool2d(3, 2),
        )
        # 这里全连接层的输出个数比LeNet中的大数倍。使用丢弃层来缓解过拟合
        self.fc = nn.Sequential(
            nn.Linear(256 * 5 * 5, 4096),
            nn.ReLU(),
            nn.Dropout(0.5),
            nn.Linear(4096, 4096),
            nn.ReLU(),
            nn.Dropout(0.5),
            # 输出层。由于这里使用Fashion-MNIST,所以用类别数为10,而非论文中的1000
            nn.Linear(4096, 10),
        )

    def forward(self, img):
        feature = self.conv(img)
        output = self.fc(feature.view(img.shape[0], -1))
        return output
net = AlexNet()
print(net)

AlexNet(
  (conv): Sequential(
    (0): Conv2d(1, 96, kernel_size=(11, 11), stride=(4, 4))
    (1): ReLU()
    (2): MaxPool2d(kernel_size=3, stride=2, padding=0, dilation=1, ceil_mode=False)
    (3): Conv2d(96, 256, kernel_size=(5, 5), stride=(1, 1), padding=(2, 2))
    (4): ReLU()
    (5): MaxPool2d(kernel_size=3, stride=2, padding=0, dilation=1, ceil_mode=False)
    (6): Conv2d(256, 384, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (7): ReLU()
    (8): Conv2d(384, 384, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (9): ReLU()
    (10): Conv2d(384, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (11): ReLU()
    (12): MaxPool2d(kernel_size=3, stride=2, padding=0, dilation=1, ceil_mode=False)
  )
  (fc): Sequential(
    (0): Linear(in_features=6400, out_features=4096, bias=True)
    (1): ReLU()
    (2): Dropout(p=0.5, inplace=False)
    (3): Linear(in_features=4096, out_features=4096, bias=True)
    (4): ReLU()
    (5): Dropout(p=0.5, inplace=False)
    (6): Linear(in_features=4096, out_features=10, bias=True)
  )
)

5 模型初始化

在深度学习模型的训练中,权重的初始值极为重要。一个好的初始值,会使模型收敛速度提高,使模型准确率更精确。

我在之前的笔记中也介绍过权重初始化的重要性以及常用的初始化方法:

例如“神经网络的权重如何初始化?为何合理的初始化可以缓解梯度消失或梯度爆炸?”、“为什么随机初始化时权重随机到非常大的值模型工作的会非常不好? ”等,链接如下:

第二课:改进神经网络:超参数调整、正则化和优化-优快云博客

一般情况下,我们不使用全0初始值训练网络。为了利于训练和减少收敛时间,我们需要对模型进行合理的初始化。PyTorch也在torch.nn.init中为我们提供了常用的初始化方法。 通过本章学习,你将学习到以下内容:

  • 常见的初始化函数

  • 初始化函数的使用

5.1 torch.nn.init内容

通过访问torch.nn.init的官方文档链接 ,我们发现torch.nn.init提供了以下初始化方法:

1 . torch.nn.init.uniform_(tensor, a=0.0, b=1.0)

2 . torch.nn.init.normal_(tensor, mean=0.0, std=1.0)

3 . torch.nn.init.constant_(tensor, val)

4 . torch.nn.init.ones_(tensor)

5 . torch.nn.init.zeros_(tensor)

6 . torch.nn.init.eye_(tensor)

7 . torch.nn.init.dirac_(tensor, groups=1)

8 . torch.nn.init.xavier_uniform_(tensor, gain=1.0)

9 . torch.nn.init.xavier_normal_(tensor, gain=1.0)

10 . torch.nn.init.kaiming_uniform_(tensor, a=0, mode='fan__in', nonlinearity='leaky_relu')

11 . torch.nn.init.kaiming_normal_(tensor, a=0, mode='fan_in', nonlinearity='leaky_relu')

12 . torch.nn.init.orthogonal_(tensor, gain=1)

13 . torch.nn.init.sparse_(tensor, sparsity, std=0.01)

14 . torch.nn.init.calculate_gain(nonlinearity, param=None)

关于计算增益如下表所示:

nonlinearitygain
Linear/Identity1
Conv{1,2,3}D1
Sigmod1
Tanh5/3
ReLUsqrt(2)
Leaky Relusqrt(2/1+neg_slop^2)

我们可以发现这些函数除了calculate_gain,所有函数的后缀都带有下划线,意味着这些函数将会直接原地更改输入张量的值。

5.2 torch.nn.init使用

我们通常会根据实际模型来使用torch.nn.init进行初始化,通常使用isinstance()来进行判断模块(回顾4模型构建)属于什么类型。

import torch
import torch.nn as nn

conv = nn.Conv2d(1, 3, 3)
linear = nn.Linear(10, 1)

print(isinstance(conv, nn.Conv2d))  # 判断conv是否是nn.Conv2d类型
print(isinstance(linear, nn.Conv2d))  # 判断linear是否是nn.Conv2d类型

True
False

# 查看随机初始化的conv参数
print(conv.weight.data)
# 查看linear的参数
print(linear.weight.data)

tensor([[[[ 0.2904, -0.3278,  0.1068],
          [ 0.0004, -0.0445,  0.2048],
          [-0.0774,  0.2936, -0.3072]]],


        [[[ 0.2034, -0.0355,  0.2836],
          [ 0.3005,  0.1235,  0.1443],
          [ 0.1659,  0.0117, -0.1094]]],


        [[[-0.2878, -0.1635,  0.1753],
          [ 0.1194,  0.2977,  0.1679],
          [ 0.1414, -0.2566,  0.0008]]]])
tensor([[-0.0464, -0.2952, -0.1795,  0.1941,  0.2205,  0.1592,  0.1007, -0.2115,
          0.2295, -0.2886]])

# 对conv进行kaiming初始化
torch.nn.init.kaiming_normal_(conv.weight.data)
print(conv.weight.data)
# 对linear进行常数初始化
torch.nn.init.constant_(linear.weight.data, 0.3)
print(linear.weight.data)

tensor([[[[-0.2712,  0.0738, -0.0749],
          [-0.0519,  0.2561,  0.2940],
          [-0.4030,  0.6918, -0.3640]]],


        [[[ 0.6180, -0.3163,  0.5267],
          [-0.0574, -0.2355, -1.0345],
          [ 0.7453,  0.8769, -0.5873]]],


        [[[ 0.0353,  0.5155,  0.6390],
          [ 0.1614,  0.5988,  0.4704],
          [-0.3791, -0.2761,  0.9453]]]])
tensor([[0.3000, 0.3000, 0.3000, 0.3000, 0.3000, 0.3000, 0.3000, 0.3000, 0.3000,
         0.3000]])

5.3 初始化函数的封装

人们常常将各种初始化方法定义为一个initialize_weights()的函数并在模型初始后进行使用。

根据不同模块的类型进行相应权重的初始化有许多好处:

不同类型的神经网络层(如卷积层、全连接层、批量归一化层等)有不同的激活函数和数据流动方式。如果使用不适当的权重初始化,可能导致训练过程中的梯度消失或梯度爆炸。针对不同层使用不同的初始化方法,有助于缓解这些问题。

  • 卷积层 (Conv2D):卷积层的权重通常需要采用较小的随机值初始化。初始化为零会导致每个神经元的输出相同,无法学到有用的特征;而将其初始化为较大的值则可能导致梯度爆炸。使用如 Xavier/GlorotHe 初始化方法有助于避免这些问题。比如,这里使用 zeros_() 只是为了示例,通常卷积层的初始化方式会有所不同,选择合适的初始化方法是很重要的。
  • 全连接层 (Linear):全连接层的初始化方式(如使用 normal_xavier_uniform_)可以帮助确保输出的分布合理,避免输出在开始时就非常偏离均值。

此外,该方法还可以提高网络的训练效率,并有助于模型的收敛速度和稳定性。通过合适的初始化方法,神经网络的训练可以更加平稳,收敛速度也能加快。

def initialize_weights(model):
    for m in model.modules():
        if isinstance(m, nn.Conv2d):  # 对卷积层的权重初始化
            torch.nn.init.zeros_(m.weight.data)  # 初始化卷积层的权重为0
            if m.bias is not None:
                torch.nn.init.constant_(m.bias.data, 0.3)  # 偏置项初始化为0.3
        elif isinstance(m, nn.Linear):  # 对全连接层的权重初始化
            torch.nn.init.normal_(
                m.weight.data, 0.1
            )  # 权重初始化为均值0,标准差为0.1的正态分布
            if m.bias is not None:
                torch.nn.init.zeros_(m.bias.data)  # 偏置项初始化为0
        elif isinstance(m, nn.BatchNorm2d):  # 对批量归一化层的初始化
            m.weight.data.fill_(1)  # 权重初始化为1
            m.bias.data.zeros_()  # 偏置项初始化为0

这段代码流程是遍历当前模型的每一层,然后判断各层属于什么类型,然后根据不同类型层,设定不同的权值初始化方法。

 注:将卷积层的权重初始化为零是一个简化示例,但在实际应用中,通常会采用 Xavier/GlorotHe 初始化方法。bias 初始化为常数(0.3)通常用于避免偏置初始为零导致的激活值为零。

我们可以通过下面的例程进行一个简短的演示:  

# 模型的定义
class MLP(nn.Module):
    # 声明带有模型参数的层,这里声明了两个全连接层
    def __init__(self, **kwargs):
        # 调用MLP父类Block的构造函数来进行必要的初始化。这样在构造实例时还可以指定其他函数
        super(MLP, self).__init__(**kwargs)
        self.hidden = nn.Conv2d(1, 1, 3)
        self.act = nn.ReLU()
        self.output = nn.Linear(10, 1)

    # 定义模型的前向计算,即如何根据输入x计算返回所需要的模型输出
    def forward(self, x):
        o = self.act(self.hidden(x))
        return self.output(o)


mlp = MLP()
print(mlp.hidden.weight.data)
print("-------初始化-------")

mlp.apply(initialize_weights)
# 或者initialize_weights(mlp)
print(mlp.hidden.weight.data)

tensor([[[[ 0.3069, -0.1865,  0.0182],
          [ 0.2475,  0.3330,  0.1352],
          [-0.0247, -0.0786,  0.1278]]]])
"-------初始化-------"
tensor([[[[0., 0., 0.],
          [0., 0., 0.],
          [0., 0., 0.]]]]) 

注意:我们在初始化时,最好不要将模型的参数初始化为0,因为这样会导致梯度消失,从而影响模型的训练效果。因此,我们在初始化时,可以使用其他初始化方法或者将模型初始化为一个很小的值,如0.01,0.1等。  

6 损失函数

我在之前的笔记“4.4 常见的损失函数有哪些?”里详细介绍过常用的损失函数,链接如下:

Course2:进阶的机器学习算法-优快云博客

在深度学习广为使用的今天,我们可以在脑海里清晰的知道,一个模型想要达到很好的效果需要学习,也就是我们常说的训练。一个好的训练离不开优质的负反馈,这里的损失函数就是模型的负反馈。

所以在PyTorch中,损失函数是必不可少的。它是数据输入到模型当中,产生的结果与真实标签的评价指标,我们的模型可以按照损失函数的目标来做出改进。

下面我们将开始探索PyTorch的所拥有的损失函数。这里将列出PyTorch中常用的损失函数(一般通过torch.nn调用),并详细介绍每个损失函数的功能介绍、数学公式和调用代码。当然,PyTorch的损失函数还远不止这些,在解决实际问题的过程中需要进一步探索、借鉴现有工作,或者设计自己的损失函数。

官方文档链接(21种损失函数):torch.nn — PyTorch 2.5 documentation

经过本节的学习,你将收获:

  • 在深度学习中常见的损失函数及其定义方式

  • PyTorch中损失函数的调用

6.1 二分类交叉熵损失函数

功能:计算二分类任务时的交叉熵(Cross Entropy)函数。在二分类中,label是{0,1}。对于进入交叉熵函数的input为概率分布的形式。一般来说,input为sigmoid激活层的输出,或者softmax的输出。

主要参数

weight:每个类别的loss设置权值

size_average:数据为bool,为True时,返回的loss为平均值;为False时,返回的各样本的loss之和。

reduce:数据类型为bool,为True时,loss的返回是标量。

计算公式:

torch.nn.BCELoss(weight=None, size_average=None, reduce=None, reduction="mean")

m = nn.Sigmoid()
loss = nn.BCELoss()
input = torch.randn(3, requires_grad=True)
target = torch.empty(3).random_(2)
output = loss(m(input), target)
output.backward()

print("BCELoss损失函数的计算结果为", output)

BCELoss损失函数的计算结果为 tensor(0.8529, grad_fn=<BinaryCrossEntropyBackward>)

6.2 交叉熵损失函数

功能:计算交叉熵函数,一般用于多分类问题.

主要参数

weight:每个类别的loss设置权值。

size_average:数据为bool,为True时,返回的loss为平均值;为False时,返回的各样本的loss之和。

ignore_index:忽略某个类的损失函数。

reduce:数据类型为bool,为True时,loss的返回是标量。

计算公式如下:

torch.nn.CrossEntropyLoss(
    weight=None, size_average=None, ignore_index=-100, reduce=None, reduction="mean"
)

loss = nn.CrossEntropyLoss()
input = torch.randn(3, 5, requires_grad=True)
target = torch.empty(3, dtype=torch.long).random_(5)
output = loss(input, target)
output.backward()

print(output)

tensor(2.2376, grad_fn=<NllLossBackward>) 

6.3 L1损失函数

功能: 计算输出y和真实标签target之间的差值的绝对值。用于回归问题,特别是当数据中有离群点时,L1损失更为鲁棒

我们需要知道的是,reduction参数决定了计算模式。有三种计算模式可选:none:逐个元素计算。 sum:所有元素求和,返回标量。 mean:加权平均,返回标量。 如果选择none,那么返回的结果是和输入元素相同尺寸的。默认计算方式是求平均。

计算公式如下:

torch.nn.L1Loss(size_average=None, reduce=None, reduction="mean")

loss = nn.L1Loss()
input = torch.randn(3, 5, requires_grad=True)
target = torch.randn(3, 5)
output = loss(input, target)
output.backward()

print("L1损失函数的计算结果为", output)

L1损失函数的计算结果为 tensor(0.6314, grad_fn=<L1LossBackward>) 

6.4 MSE损失函数

功能: 计算输出y和真实标签target之差的平方。用于回归问题,度量预测值与真实值之间的均方误差。

L1Loss一样,MSELoss损失函数中,reduction参数决定了计算模式。有三种计算模式可选:none:逐个元素计算。 sum:所有元素求和,返回标量。默认计算方式是求平均。

计算公式如下:

torch.nn.MSELoss(size_average=None, reduce=None, reduction="mean")

loss = nn.MSELoss()
input = torch.randn(3, 5, requires_grad=True)
target = torch.randn(3, 5)
output = loss(input, target)
output.backward()

print("MSE损失函数的计算结果为", output)

MSE损失函数的计算结果为 tensor(2.4686, grad_fn=<MseLossBackward>) 

6.5 平滑L1 (Smooth L1)损失函数

功能: L1的平滑输出,其功能是减轻离群点带来的影响.常用于目标检测,结合L1和L2损失的优点,适合处理离群点。

reduction参数决定了计算模式。有三种计算模式可选:none:逐个元素计算。 sum:所有元素求和,返回标量。默认计算方式是求平均。

提醒: 之后的损失函数中,关于reduction 这个参数依旧会存在。所以,之后就不再单独说明。

计算公式如下:

torch.nn.SmoothL1Loss(size_average=None, reduce=None, reduction="mean", beta=1.0)

loss = nn.SmoothL1Loss()
input = torch.randn(3, 5, requires_grad=True)
target = torch.randn(3, 5)
output = loss(input, target)
output.backward()

print("SmoothL1Loss损失函数的计算结果为", output)

SmoothL1Loss损失函数的计算结果为 tensor(0.6322, grad_fn=<SmoothL1LossBackward>) 

import matplotlib.pyplot as plt

inputs = torch.linspace(-10, 10, steps=5000)
target = torch.zeros_like(inputs)

loss_f_smooth = nn.SmoothL1Loss(reduction="none")
loss_smooth = loss_f_smooth(inputs, target)
loss_f_l1 = nn.L1Loss(reduction="none")
loss_l1 = loss_f_l1(inputs, target)

plt.plot(inputs.numpy(), loss_smooth.numpy(), label="Smooth L1 Loss")
plt.plot(inputs.numpy(), loss_l1, label="L1 loss")
plt.xlabel("x_i - y_i")
plt.ylabel("loss value")
plt.legend()
plt.grid()
plt.show()

平滑L1与L1的对比

这里我们通过可视化两种损失函数曲线来对比平滑L1和L1两种损失函数的区别。

可以看出,对于smoothL1来说,在 0 这个尖端处,过渡更为平滑。  

6.6 目标泊松分布的负对数似然损失

功能: 泊松分布的负对数似然损失函数,用于泊松回归问题,特别是在处理计数数据时。

主要参数:

log_input:输入是否为对数形式,决定计算公式。

full:计算所有 loss,默认为 False。

eps:修正项,避免 input 为 0 时,log(input) 为 nan 的情况。

数学公式:

torch.nn.PoissonNLLLoss(log_input=True, full=False, size_average=None, eps=1e-08, reduce=None, reduction='mean')

loss = nn.PoissonNLLLoss()
log_input = torch.randn(5, 2, requires_grad=True)
target = torch.randn(5, 2)
output = loss(log_input, target)
output.backward()

print("PoissonNLLLoss损失函数的计算结果为", output)

PoissonNLLLoss损失函数的计算结果为 tensor(1.1259, grad_fn=<MeanBackward0>) 

6.7 KL散度

功能: 计算KL散度,也就是计算相对熵。用于连续分布的距离度量,并且对离散采用的连续输出空间分布进行回归通常很有用。用于衡量两个概率分布之间的差异,常用于自编码器、生成对抗网络等

主要参数:

reduction:计算模式,可为 none/sum/mean/batchmean。

none:逐个元素计算。
sum:所有元素求和,返回标量。
mean:加权平均,返回标量。
batchmean:batchsize 维度求平均值。

计算公式:

torch.nn.KLDivLoss(size_average=None, reduce=None, reduction="mean", log_target=False)

inputs = torch.tensor([[0.5, 0.3, 0.2], [0.2, 0.3, 0.5]])
target = torch.tensor([[0.9, 0.05, 0.05], [0.1, 0.7, 0.2]], dtype=torch.float)
loss = nn.KLDivLoss()
output = loss(inputs, target)

print("KLDivLoss损失函数的计算结果为", output)

KLDivLoss损失函数的计算结果为 tensor(-0.3335)

6.8 MarginRankingLoss

功能: 计算两个向量之间的相似度,用于排序任务。该方法用于计算两组数据之间的差异。用于排序任务,计算两个输入张量之间的相对排名差异。

主要参数:

margin:边界值,$x{1}$ 与$x{2}$ 之间的差异值。

reduction:计算模式,可为 none/sum/mean。

计算公式:

torch.nn.MarginRankingLoss(margin=0.0, size_average=None, reduce=None, reduction="mean")

loss = nn.MarginRankingLoss()
input1 = torch.randn(3, requires_grad=True)
input2 = torch.randn(3, requires_grad=True)
target = torch.randn(3).sign()
output = loss(input1, input2, target)
output.backward()

print("MarginRankingLoss损失函数的计算结果为", output)

MarginRankingLoss损失函数的计算结果为 tensor(0.3438, grad_fn=<MeanBackward0>) 

6.9 多标签边界损失函数

功能: 对于多标签分类问题计算损失函数。用于多标签分类问题,考虑到多个标签的独立性。

主要参数:

reduction:计算模式,可为 none/sum/mean。

计算公式:

torch.nn.MultiLabelMarginLoss(size_average=None, reduce=None, reduction="mean")

loss = nn.MultiLabelMarginLoss()
x = torch.FloatTensor([[0.9, 0.2, 0.4, 0.8]])
# for target y, only consider labels 3 and 0, not after label -1
y = torch.LongTensor([[3, 0, -1, 1]])  # 真实的分类是,第3类和第0类
output = loss(x, y)

print("MultiLabelMarginLoss损失函数的计算结果为", output)

MultiLabelMarginLoss损失函数的计算结果为 tensor(0.4500) 

6.10 二分类损失函数

功能: 计算二分类的 logistic 损失。用于二分类问题的标签为-1或1时,衡量预测与标签之间的差异。

主要参数:

reduction:计算模式,可为 none/sum/mean。

计算公式:

torch.nn.SoftMarginLoss(size_average=None, reduce=None, reduction='mean')

inputs = torch.tensor([[0.3, 0.7], [0.5, 0.5]])  # 两个样本,两个神经元
target = torch.tensor([[-1, 1], [1, -1]], dtype=torch.float)  # 该 loss 为逐个神经元计算,需要为每个神经元单独设置标签

loss_f = nn.SoftMarginLoss()
output = loss_f(inputs, target)

print('SoftMarginLoss损失函数的计算结果为',output)

SoftMarginLoss损失函数的计算结果为 tensor(0.6764) 

6.11 多分类的折页损失

功能: 计算多分类的折页损失,用于多分类任务,特别是当样本不平衡时,可以通过调整权重来优化。

主要参数:

reduction:计算模式,可为 none/sum/mean。

p:可选 1 或 2。

weight:各类别的 loss 设置权值。

margin:边界值

计算公式:

torch.nn.MultiMarginLoss(p=1, margin=1.0, weight=None, size_average=None, reduce=None, reduction='mean')

inputs = torch.tensor([[0.3, 0.7], [0.5, 0.5]])
target = torch.tensor([0, 1], dtype=torch.long)

loss_f = nn.MultiMarginLoss()
output = loss_f(inputs, target)

print("MultiMarginLoss损失函数的计算结果为", output)

MultiMarginLoss损失函数的计算结果为 tensor(0.6000) 

6.12 三元组损失

功能: 计算三元组损失。用于度量学习,尤其是在面部识别和检索任务中,确保相同类样本靠近,不同类样本远离。

三元组: 这是一种数据的存储或者使用格式。<实体1,关系,实体2>。在项目中,也可以表示为< anchor, positive examples , negative examples>

在这个损失函数中,我们希望去anchor的距离更接近positive examples,而远离negative examples

主要参数:

reduction:计算模式,可为 none/sum/mean。

p:可选 1 或 2。

margin:边界值

计算公式:

torch.nn.TripletMarginLoss(margin=1.0, p=2.0, eps=1e-06, swap=False, size_average=None, reduce=None, reduction='mean')

triplet_loss = nn.TripletMarginLoss(margin=1.0, p=2)
anchor = torch.randn(100, 128, requires_grad=True)
positive = torch.randn(100, 128, requires_grad=True)
negative = torch.randn(100, 128, requires_grad=True)
output = triplet_loss(anchor, positive, negative)
output.backward()
print("TripletMarginLoss损失函数的计算结果为", output)

TripletMarginLoss损失函数的计算结果为 tensor(1.1902, grad_fn=<MeanBackward0>) 

6.13 HingEmbeddingLoss

功能: 对输出的embedding结果做Hing损失计算。用于度量学习和分类任务,特别是在支持向量机(SVM)中使用。

主要参数:

reduction:计算模式,可为 none/sum/mean。

margin:边界值

计算公式:

注意事项: 输入x应为两个输入之差的绝对值。

可以这样理解,让个输出的是正例yn=1,那么loss就是x,如果输出的是负例y=-1,那么输出的loss就是要做一个比较。

torch.nn.HingeEmbeddingLoss(margin=1.0, size_average=None, reduce=None, reduction='mean')

loss_f = nn.HingeEmbeddingLoss()
inputs = torch.tensor([[1.0, 0.8, 0.5]])
target = torch.tensor([[1, 1, -1]])
output = loss_f(inputs, target)

print("HingEmbeddingLoss损失函数的计算结果为", output)

HingEmbeddingLoss损失函数的计算结果为 tensor(0.7667) 

6.14 余弦相似度

功能: 对两个向量做余弦相似度。用于度量向量之间的相似性,广泛用于文本相似度、推荐系统等。

主要参数:

reduction:计算模式,可为 none/sum/mean。

margin:可取值[-1,1] ,推荐为[0,0.5] 。

计算公式:

这个损失函数应该是最广为人知的。对于两个向量,做余弦相似度。将余弦相似度作为一个距离的计算方式,如果两个向量的距离近,则损失函数值小,反之亦然。

torch.nn.CosineEmbeddingLoss(margin=0.0, size_average=None, reduce=None, reduction='mean')

loss_f = nn.CosineEmbeddingLoss()
inputs_1 = torch.tensor([[0.3, 0.5, 0.7], [0.3, 0.5, 0.7]])
inputs_2 = torch.tensor([[0.1, 0.3, 0.5], [0.1, 0.3, 0.5]])
target = torch.tensor([1, -1], dtype=torch.float)
output = loss_f(inputs_1, inputs_2, target)

print("CosineEmbeddingLoss损失函数的计算结果为", output)

CosineEmbeddingLoss损失函数的计算结果为 tensor(0.5000) 

6.15 CTC损失函数

功能: 用于解决时序类数据的分类,用于序列标注任务,如语音识别、手写识别等,处理标签序列与预测序列长度不一致的问题。

计算连续时间序列和目标序列之间的损失。CTCLoss对输入和目标的可能排列的概率进行求和,产生一个损失值,这个损失值对每个输入节点来说是可分的。输入与目标的对齐方式被假定为 "多对一",这就限制了目标序列的长度,使其必须是≤输入长度。

主要参数:

reduction:计算模式,可为 none/sum/mean。

blank:blank label。

zero_infinity:无穷大的损失值或梯度值为0。

torch.nn.CTCLoss(blank=0, reduction="mean", zero_infinity=False)

# Target are to be padded
T = 50  # Input sequence length
C = 20  # Number of classes (including blank)
N = 16  # Batch size
S = 30  # Target sequence length of longest target in batch (padding length)
S_min = 10  # Minimum target length, for demonstration purposes

# Initialize random batch of input vectors, for *size = (T,N,C)
input = torch.randn(T, N, C).log_softmax(2).detach().requires_grad_()

# Initialize random batch of targets (0 = blank, 1:C = classes)
target = torch.randint(low=1, high=C, size=(N, S), dtype=torch.long)

input_lengths = torch.full(size=(N,), fill_value=T, dtype=torch.long)
target_lengths = torch.randint(low=S_min, high=S, size=(N,), dtype=torch.long)
ctc_loss = nn.CTCLoss()
loss = ctc_loss(input, target, input_lengths, target_lengths)
loss.backward()


# Target are to be un-padded
T = 50  # Input sequence length
C = 20  # Number of classes (including blank)
N = 16  # Batch size

# Initialize random batch of input vectors, for *size = (T,N,C)
input = torch.randn(T, N, C).log_softmax(2).detach().requires_grad_()
input_lengths = torch.full(size=(N,), fill_value=T, dtype=torch.long)

# Initialize random batch of targets (0 = blank, 1:C = classes)
target_lengths = torch.randint(low=1, high=T, size=(N,), dtype=torch.long)
target = torch.randint(low=1, high=C, size=(sum(target_lengths),), dtype=torch.long)
ctc_loss = nn.CTCLoss()
loss = ctc_loss(input, target, input_lengths, target_lengths)
loss.backward()

print("CTCLoss损失函数的计算结果为", loss)

CTCLoss损失函数的计算结果为 tensor(inf, grad_fn=<MeanBackward0>) 

6.16 各损失函数的应用场景

  1. 二分类交叉熵损失函数:用于二分类问题,计算预测与真实标签之间的差异。
  2. 交叉熵损失函数:用于多分类问题,计算多类预测与真实标签之间的差异。
  3. L1损失函数:用于回归问题,特别是当数据中有离群点时,L1损失更为鲁棒。
  4. MSE损失函数:用于回归问题,度量预测值与真实值之间的均方误差。
  5. 平滑L1损失函数:常用于目标检测,结合L1和L2损失的优点,适合处理离群点。
  6. 目标泊松分布的负对数似然损失:用于泊松回归问题,特别是在处理计数数据时。
  7. KL散度:用于衡量两个概率分布之间的差异,常用于自编码器、生成对抗网络等。
  8. MarginRankingLoss:用于排序任务,计算两个输入张量之间的相对排名差异。
  9. 多标签边界损失函数:用于多标签分类问题,考虑到多个标签的独立性。
  10. 二分类损失函数:用于二分类问题的标签为-1或1时,衡量预测与标签之间的差异。
  11. 多分类的折页损失:用于多分类任务,特别是当样本不平衡时,可以通过调整权重来优化。
  12. 三元组损失:用于度量学习,尤其是在面部识别和检索任务中,确保相同类样本靠近,不同类样本远离。
  13. HingeEmbeddingLoss:用于度量学习和分类任务,特别是在支持向量机(SVM)中使用。
  14. 余弦相似度:用于度量向量之间的相似性,广泛用于文本相似度、推荐系统等。
  15. CTC损失函数:用于序列标注任务,如语音识别、手写识别等,处理标签序列与预测序列长度不一致的问题。

7 训练和评估

我们在完成了模型的训练后,需要在测试集/验证集上完成模型的验证,以确保我们的模型具有泛化能力、不会出现过拟合等问题。在PyTorch中,训练和评估的流程是一致的,只是在训练过程中需要将模型的参数进行更新,而在评估过程中则不需要更新参数。

经过本节的学习,你将收获:

  • PyTorch的训练/评估模式的开启

  • 完整的训练/评估流程

完成了上述设定后就可以加载数据开始训练模型了。首先应该设置模型的状态:如果是训练状态,那么模型的参数应该支持反向传播的修改;如果是验证/测试状态,则不应该修改模型参数。在PyTorch中,模型的状态设置非常简便,如下的两个操作二选一即可:

model.train()   # 训练状态
model.eval()   # 验证/测试状态

我们前面在DataLoader构建完成后介绍了如何从中读取数据,在训练过程中使用类似的操作即可,区别在于此时要用for循环读取DataLoader中的全部数据。

for data, label in train_loader:

之后将数据放到GPU上用于后续计算,此处以.cuda()为例

data, label = data.cuda(), label.cuda()

开始用当前批次数据做训练时,应当先将优化器的梯度置零:

optimizer.zero_grad()

之后将data送入模型中训练:

output = model(data)

根据预先定义的criterion计算损失函数:

loss = criterion(output, label)

将loss反向传播回网络:

loss.backward()

使用优化器更新模型参数:

optimizer.step()

这样一个训练过程就完成了,后续还可以计算模型准确率等指标,这部分会在下一节的图像分类实战中加以介绍。

验证/测试的流程基本与训练过程一致,不同点在于:

  • 需要预先设置torch.no_grad,以及将model调至eval模式

  • 不需要将优化器的梯度置零

  • 不需要将loss反向回传到网络

  • 不需要更新optimizer

一个完整的图像分类的训练过程如下所示:

def train(epoch):
    # 设置模型为训练模式
    model.train()
    train_loss = 0  # 初始化训练损失
    for data, label in train_loader:
        # 将数据和标签移动到 GPU
        data, label = data.cuda(), label.cuda()
        # 梯度清零
        optimizer.zero_grad()
        # 模型前向传播,计算输出
        output = model(data)
        # 计算损失
        loss = criterion(output, label)
        # 反向传播
        loss.backward()
        # 更新模型参数
        optimizer.step()
        # 累加每批数据的损失,乘以数据量以便后续计算平均值
        train_loss += loss.item() * data.size(0)
    
    # 计算平均训练损失
    train_loss = train_loss / len(train_loader.dataset)
    
    # 打印当前轮次的训练损失
    print('Epoch: {} \tTraining Loss: {:.6f}'.format(epoch, train_loss))

对应的,一个完整图像分类的验证过程如下所示:  

def val(epoch):
    # 设置模型为评估模式
    model.eval()
    val_loss = 0  # 初始化验证损失
    running_accu = 0  # 初始化准确率计数
    with torch.no_grad():  # 在验证阶段关闭梯度计算,节省显存和计算资源
        for data, label in val_loader:
            # 将数据和标签移动到 GPU
            data, label = data.cuda(), label.cuda()
            # 模型前向传播,计算输出
            output = model(data)
            # 计算预测值
            preds = torch.argmax(output, 1)  # 获取每个样本的预测类别
            # 计算损失
            loss = criterion(output, label)
            # 累加每批数据的损失,乘以数据量以便后续计算平均值
            val_loss += loss.item() * data.size(0)
            # 累加正确预测的样本数
            running_accu += torch.sum(preds == label.data)

    # 计算平均验证损失
    val_loss = val_loss / len(val_loader.dataset)
    # 打印当前轮次的验证损失
    print("Epoch: {} \tValidation Loss: {:.6f}".format(epoch, val_loss))

对于图像分类任务,我们还可以使用sklearn.metrics中的classification_report函数来计算模型的准确率、召回率、F1值等指标,如下所示:

from sklearn.metrics import classification_report

"""
将下方代码的labels和preds替换为模型预测出来的所有label和preds,
target_names替换为类别名称,
既可得到模型的分类报告
"""
print(classification_report(labels.cpu(), preds.cpu(), target_names=class_names))

除此之外,我们还可以使用torchevaltorchmetric来对模型进行评估。  

8 可视化

在PyTorch深度学习中,可视化是一个可选项,指的是某些任务在训练完成后,需要对一些必要的内容进行可视化,比如分类的ROC曲线,卷积网络中的卷积核,以及训练/验证过程的损失函数曲线等等。具体的可视化方法我们将在第七章中展开介绍。

9 优化器

深度学习的目标是通过不断改变网络参数,使得参数能够对输入做各种非线性变换拟合输出,本质上就是一个函数去寻找最优解,只不过这个最优解是一个矩阵,而如何快速求得这个最优解是深度学习研究的一个重点,以经典的resnet-50为例,它大约有2000万个系数需要进行计算,那么我们如何计算出这么多系数,有以下两种方法:

  1. 第一种是直接暴力穷举一遍参数,这种方法从理论上行得通,但是实施上可能性基本为0,因为参数量过于庞大。

  2. 为了使求解参数过程更快,人们提出了第二种办法,即BP+优化器逼近求解。

因此,优化器是根据网络反向传播的梯度信息来更新网络的参数,以起到降低loss函数计算值,使得模型输出更加接近真实标签。

经过本节的学习,你将收获:

  • 了解PyTorch的优化器

  • 学会使用PyTorch提供的优化器进行优化

  • 优化器的属性和构造

  • 优化器的对比

9.1 Pytorch提供的优化器

官方文档:torch.optim — PyTorch 2.5 documentation

PyTorch很人性化的给我们提供了一个优化器的库torch.optim,在这里面提供了多种优化器。

  • torch.optim.SGD

  • torch.optim.ASGD

  • torch.optim.Adadelta

  • torch.optim.Adagrad

  • torch.optim.Adam

  • torch.optim.AdamW

  • torch.optim.Adamax

  • torch.optim.RAdam

  • torch.optim.NAdam

  • torch.optim.SparseAdam

  • torch.optim.LBFGS

  • torch.optim.RMSprop

  • torch.optim.Rprop

而以上这些优化算法均继承于Optimizer下面我们先来看下所有优化器的基类Optimizer。定义如下:

class Optimizer(object):
    def __init__(self, params, defaults):        
        self.defaults = defaults
        self.state = defaultdict(dict)
        self.param_groups = []

Optimizer有三个属性:

  • defaults:存储的是优化器的超参数,例子如下:

{'lr': 0.1, 'momentum': 0.9, 'dampening': 0, 'weight_decay': 0, 'nesterov': False}
  • state:参数的缓存,例子如下:

defaultdict(<class 'dict'>, {tensor([[ 0.3864, -0.0131],
        [-0.1911, -0.4511]], requires_grad=True): {'momentum_buffer': tensor([[0.0052, 0.0052],
        [0.0052, 0.0052]])}})
  • param_groups:管理的参数组,是一个list,其中每个元素是一个字典,顺序是params,lr,momentum,dampening,weight_decay,nesterov,例子如下:

[{'params': [tensor([[-0.1022, -1.6890],[-1.5116, -1.7846]], requires_grad=True)], 'lr': 1, 'momentum': 0, 'dampening': 0, 'weight_decay': 0, 'nesterov': False}]

Optimizer还有以下的方法:

  • zero_grad():清空所管理参数的梯度,PyTorch的特性是张量的梯度不自动清零,因此每次反向传播后都需要清空梯度。

def zero_grad(self, set_to_none: bool = False):
    for group in self.param_groups:
        for p in group['params']:
            if p.grad is not None:  #梯度不为空
                if set_to_none: 
                    p.grad = None
                else:
                    if p.grad.grad_fn is not None:
                        p.grad.detach_()
                    else:
                        p.grad.requires_grad_(False)
                    p.grad.zero_()# 梯度设置为0
  • step():执行一步梯度更新,参数更新

def step(self, closure): 
    raise NotImplementedError
  • add_param_group():添加参数组

def add_param_group(self, param_group):
    assert isinstance(param_group, dict), "param group must be a dict"
# 检查类型是否为tensor
    params = param_group['params']
    if isinstance(params, torch.Tensor):
        param_group['params'] = [params]
    elif isinstance(params, set):
        raise TypeError('optimizer parameters need to be organized in ordered collections, but '
                        'the ordering of tensors in sets will change between runs. Please use a list instead.')
    else:
        param_group['params'] = list(params)
    for param in param_group['params']:
        if not isinstance(param, torch.Tensor):
            raise TypeError("optimizer can only optimize Tensors, "
                            "but one of the params is " + torch.typename(param))
        if not param.is_leaf:
            raise ValueError("can't optimize a non-leaf Tensor")

    for name, default in self.defaults.items():
        if default is required and name not in param_group:
            raise ValueError("parameter group didn't specify a value of required optimization parameter " +
                             name)
        else:
            param_group.setdefault(name, default)

    params = param_group['params']
    if len(params) != len(set(params)):
        warnings.warn("optimizer contains a parameter group with duplicate parameters; "
                      "in future, this will cause an error; "
                      "see github.com/PyTorch/PyTorch/issues/40967 for more information", stacklevel=3)
# 上面好像都在进行一些类的检测,报Warning和Error
    param_set = set()
    for group in self.param_groups:
        param_set.update(set(group['params']))

    if not param_set.isdisjoint(set(param_group['params'])):
        raise ValueError("some parameters appear in more than one parameter group")
# 添加参数
    self.param_groups.append(param_group)
  • load_state_dict() :加载状态参数字典,可以用来进行模型的断点续训练,继续上次的参数进行训练

def load_state_dict(self, state_dict):
    r"""Loads the optimizer state.

    Arguments:
        state_dict (dict): optimizer state. Should be an object returned
            from a call to :meth:`state_dict`.
    """
    # deepcopy, to be consistent with module API
    state_dict = deepcopy(state_dict)
    # Validate the state_dict
    groups = self.param_groups
    saved_groups = state_dict['param_groups']

    if len(groups) != len(saved_groups):
        raise ValueError("loaded state dict has a different number of "
                         "parameter groups")
    param_lens = (len(g['params']) for g in groups)
    saved_lens = (len(g['params']) for g in saved_groups)
    if any(p_len != s_len for p_len, s_len in zip(param_lens, saved_lens)):
        raise ValueError("loaded state dict contains a parameter group "
                         "that doesn't match the size of optimizer's group")

    # Update the state
    id_map = {old_id: p for old_id, p in
              zip(chain.from_iterable((g['params'] for g in saved_groups)),
                  chain.from_iterable((g['params'] for g in groups)))}

    def cast(param, value):
        r"""Make a deep copy of value, casting all tensors to device of param."""
   		.....

    # Copy state assigned to params (and cast tensors to appropriate types).
    # State that is not assigned to params is copied as is (needed for
    # backward compatibility).
    state = defaultdict(dict)
    for k, v in state_dict['state'].items():
        if k in id_map:
            param = id_map[k]
            state[param] = cast(param, v)
        else:
            state[k] = v

    # Update parameter groups, setting their 'params' value
    def update_group(group, new_group):
       ...
    param_groups = [
        update_group(g, ng) for g, ng in zip(groups, saved_groups)]
    self.__setstate__({'state': state, 'param_groups': param_groups})
  • state_dict():获取优化器当前状态信息字典

def state_dict(self):
    r"""Returns the state of the optimizer as a :class:`dict`.

    It contains two entries:

    * state - a dict holding current optimization state. Its content
        differs between optimizer classes.
    * param_groups - a dict containing all parameter groups
    """
    # Save order indices instead of Tensors
    param_mappings = {}
    start_index = 0

    def pack_group(group):
		......
    param_groups = [pack_group(g) for g in self.param_groups]
    # Remap state to use order indices as keys
    packed_state = {(param_mappings[id(k)] if isinstance(k, torch.Tensor) else k): v
                    for k, v in self.state.items()}
    return {
        'state': packed_state,
        'param_groups': param_groups,
    }

9.2 实际操作

import os
import torch

# 设置权重,服从正态分布  --> 2 x 2
weight = torch.randn((2, 2), requires_grad=True)
# 设置梯度为全1矩阵  --> 2 x 2
weight.grad = torch.ones((2, 2))  # 手动设置为一个全1矩阵
# 输出现有的weight和data
print("The data of weight before step:\n{}".format(weight.data))
print("The grad of weight before step:\n{}".format(weight.grad))
# 实例化优化器
optimizer = torch.optim.SGD([weight], lr=0.1, momentum=0.9)
# 进行一步操作
optimizer.step()  # 优化器读取 weight.grad,计算参数的更新值,并将更新应用到 weight.data
# 查看进行一步后的值,梯度
print("The data of weight after step:\n{}".format(weight.data))
print("The grad of weight after step:\n{}".format(weight.grad))
# 权重清零
optimizer.zero_grad()
# 检验权重是否为0
print("The grad of weight after optimizer.zero_grad():\n{}".format(weight.grad))
# 输出参数
print("optimizer.params_group is \n{}".format(optimizer.param_groups))
# 查看参数位置,optimizer和weight的位置一样,我觉得这里可以参考Python是基于值管理
print(
    "weight in optimizer:{}\nweight in weight:{}\n".format(
        id(optimizer.param_groups[0]["params"][0]), id(weight)
    )
)
# 添加参数:weight2
weight2 = torch.randn((3, 3), requires_grad=True)
optimizer.add_param_group({"params": weight2, "lr": 0.0001, "nesterov": True})
# 查看现有的参数
print("optimizer.param_groups is\n{}".format(optimizer.param_groups))
# 查看当前状态信息
opt_state_dict = optimizer.state_dict()
print("state_dict before step:\n", opt_state_dict)
# 进行5次step操作
for _ in range(50):
    optimizer.step()
# 输出现有状态信息
print("state_dict after step:\n", optimizer.state_dict())
# 保存参数信息
torch.save(
    optimizer.state_dict(),
    os.path.join(r"F:\\Notes\\thorough-pytorch\\SZ", "optimizer_state_dict.pkl"),
)
print("----------done-----------")
# 加载参数信息
state_dict = torch.load(
    r"F:\\Notes\\thorough-pytorch\\SZ\\optimizer_state_dict.pkl"
)  # 需要修改为你自己的路径
optimizer.load_state_dict(state_dict)
print("load state_dict successfully\n{}".format(state_dict))
# 输出最后属性信息
print("\n{}".format(optimizer.defaults))
print("\n{}".format(optimizer.state))
print("\n{}".format(optimizer.param_groups))

9.3 输出结果

# 进行更新前的数据,梯度
The data of weight before step:
tensor([[-0.3077, -0.1808],
        [-0.7462, -1.5556]])
The grad of weight before step:
tensor([[1., 1.],
        [1., 1.]])
# 进行更新后的数据,梯度
The data of weight after step:
tensor([[-0.4077, -0.2808],
        [-0.8462, -1.6556]])
The grad of weight after step:
tensor([[1., 1.],
        [1., 1.]])
# 进行梯度清零的梯度
The grad of weight after optimizer.zero_grad():
tensor([[0., 0.],
        [0., 0.]])
# 输出信息
optimizer.params_group is 
[{'params': [tensor([[-0.4077, -0.2808],
        [-0.8462, -1.6556]], requires_grad=True)], 'lr': 0.1, 'momentum': 0.9, 'dampening': 0, 'weight_decay': 0, 'nesterov': False}]

# 证明了优化器的和weight的储存是在一个地方,Python基于值管理
weight in optimizer:1841923407424
weight in weight:1841923407424
    
# 输出参数
optimizer.param_groups is
[{'params': [tensor([[-0.4077, -0.2808],
        [-0.8462, -1.6556]], requires_grad=True)], 'lr': 0.1, 'momentum': 0.9, 'dampening': 0, 'weight_decay': 0, 'nesterov': False}, {'params': [tensor([[ 0.4539, -2.1901, -0.6662],
        [ 0.6630, -1.5178, -0.8708],
        [-2.0222,  1.4573,  0.8657]], requires_grad=True)], 'lr': 0.0001, 'nesterov': True, 'momentum': 0.9, 'dampening': 0, 'weight_decay': 0}]

# 进行更新前的参数查看,用state_dict
state_dict before step:
 {'state': {0: {'momentum_buffer': tensor([[1., 1.],
        [1., 1.]])}}, 'param_groups': [{'lr': 0.1, 'momentum': 0.9, 'dampening': 0, 'weight_decay': 0, 'nesterov': False, 'params': [0]}, {'lr': 0.0001, 'nesterov': True, 'momentum': 0.9, 'dampening': 0, 'weight_decay': 0, 'params': [1]}]}
# 进行更新后的参数查看,用state_dict
state_dict after step:
 {'state': {0: {'momentum_buffer': tensor([[0.0052, 0.0052],
        [0.0052, 0.0052]])}}, 'param_groups': [{'lr': 0.1, 'momentum': 0.9, 'dampening': 0, 'weight_decay': 0, 'nesterov': False, 'params': [0]}, {'lr': 0.0001, 'nesterov': True, 'momentum': 0.9, 'dampening': 0, 'weight_decay': 0, 'params': [1]}]}

# 存储信息完毕
----------done-----------
# 加载参数信息成功
load state_dict successfully
# 加载参数信息
{'state': {0: {'momentum_buffer': tensor([[0.0052, 0.0052],
        [0.0052, 0.0052]])}}, 'param_groups': [{'lr': 0.1, 'momentum': 0.9, 'dampening': 0, 'weight_decay': 0, 'nesterov': False, 'params': [0]}, {'lr': 0.0001, 'nesterov': True, 'momentum': 0.9, 'dampening': 0, 'weight_decay': 0, 'params': [1]}]}

# defaults的属性输出
{'lr': 0.1, 'momentum': 0.9, 'dampening': 0, 'weight_decay': 0, 'nesterov': False}

# state属性输出
defaultdict(<class 'dict'>, {tensor([[-1.3031, -1.1761],
        [-1.7415, -2.5510]], requires_grad=True): {'momentum_buffer': tensor([[0.0052, 0.0052],
        [0.0052, 0.0052]])}})

# param_groups属性输出
[{'lr': 0.1, 'momentum': 0.9, 'dampening': 0, 'weight_decay': 0, 'nesterov': False, 'params': [tensor([[-1.3031, -1.1761],
        [-1.7415, -2.5510]], requires_grad=True)]}, {'lr': 0.0001, 'nesterov': True, 'momentum': 0.9, 'dampening': 0, 'weight_decay': 0, 'params': [tensor([[ 0.4539, -2.1901, -0.6662],
        [ 0.6630, -1.5178, -0.8708],
        [-2.0222,  1.4573,  0.8657]], requires_grad=True)]}]

注意:

  • 每个优化器都是一个类,我们一定要进行实例化才能使用,比如下方实现:

class Net(nn.Moddule):
    ···
net = Net()
optim = torch.optim.SGD(net.parameters(),lr=lr)
optim.step()
  • optimizer在一个神经网络的epoch中需要实现下面两个步骤:

    1. 梯度置零

    2. 梯度更新

optimizer = torch.optim.SGD(net.parameters(), lr=1e-5)
for epoch in range(EPOCH):
	...
	optimizer.zero_grad()  #梯度置零
	loss = ...             #计算loss
	loss.backward()        #BP反向传播
	optimizer.step()       #梯度更新
  • 给网络不同的层赋予不同的优化器参数。

from torch import optim
from torchvision.models import resnet18

net = resnet18()

optimizer = optim.SGD([
    {'params':net.fc.parameters()},#fc的lr使用默认的1e-5
    {'params':net.layer4[0].conv1.parameters(),'lr':1e-2}],lr=1e-5)

# 可以使用param_groups查看属性

9.4 实验

为了更好的帮大家了解优化器,我们对PyTorch中的优化器进行了一个小测试

9.4.1 导包

import torch
import torch.nn as nn
import torch.optim as optim
import matplotlib.pyplot as plt
from torch.utils.data import DataLoader, TensorDataset

9.4.2 数据集

a = torch.linspace(-1, 1, 1000)
# 升维操作
x = torch.unsqueeze(a, dim=1)
y = x.pow(2) + 0.1 * torch.normal(torch.zeros(x.size()))

查看数据集:

import matplotlib.pyplot as plt

# 转换为 numpy 数组以便绘图
x_np = x.numpy().flatten()
y_np = y.numpy().flatten()

# 绘制散点图
plt.scatter(x_np, y_np, s=10, label="y = x^2 + noise")
plt.title("Scatter Plot of x and y")
plt.xlabel("x")
plt.ylabel("y")
plt.legend()# 图例
plt.grid()# 网格
plt.show()

数据分布曲线如下图所示

9.4.3 构建Dataloader

from torch.utils.data import DataLoader, TensorDataset

dataset = TensorDataset(x, y)

train_loader = DataLoader(dataset, batch_size=32, shuffle=True, num_workers=2)

9.4.4 定义损失函数

因为是回归问题,所以采用交叉熵损失函数

# 定义均方误差损失
criterion = nn.MSELoss()

9.4.5 定义网络结构

import torch.nn.functional as F

class Net(nn.Module):
    def __init__(self):
        super(Net, self).__init__()
        self.hidden = nn.Linear(1, 20)
        self.predict = nn.Linear(20, 1)

    def forward(self, x):
        x = self.hidden(x)
        x = F.relu(x)
        x = self.predict(x)
        return x

model = Net().cuda()

9.4.6 定义优化器

# 定义不同的优化器
optimizers = [
    "SGD",
    "ASGD",
    "Momentum",
    "Adadelta",
    "Adagrad",
    "Adam",
    "AdamW",
    "Adamax",
    "RMSprop",
]
def get_optimizer(model, optimizer_name, lr=0.1, momentum=0.9):
    """
    根据给定的优化器名称返回相应的优化器。
    :param model: 需要优化的模型
    :param optimizer_name: 选择的优化器名称
    :param lr: 学习率,默认为 0.1
    :param momentum: 动量,默认为 0.9
    :return: 对应的优化器
    """
    if optimizer_name == "SGD":
        return optim.SGD(model.parameters(), lr=lr)
    elif optimizer_name == "ASGD":
        return optim.ASGD(model.parameters(), lr=lr)
    elif optimizer_name == "Momentum":
        return optim.SGD(model.parameters(), lr=lr, momentum=momentum)
    elif optimizer_name == "Adadelta":
        return optim.Adadelta(model.parameters(), lr=1.0)
    elif optimizer_name == "Adagrad":
        return optim.Adagrad(model.parameters(), lr=lr)
    elif optimizer_name == "Adam":
        return optim.Adam(model.parameters(), lr=0.001)
    elif optimizer_name == "AdamW":
        return optim.AdamW(model.parameters(), lr=0.001)
    elif optimizer_name == "Adamax":
        return optim.Adamax(model.parameters(), lr=0.002)
    elif optimizer_name == "RMSprop":
        return optim.RMSprop(model.parameters(), lr=0.01)
    else:
        raise ValueError(f"Unknown optimizer: {optimizer_name}")

9.4.7 定义模型训练函数

# 记录每个优化器的损失曲线
losses = {}

def train(epochs, optimizer_name):
    model = Net().cuda()  # 每次重新初始化模型
    optimizer = get_optimizer(model, optimizer_name)
    loss_values = []

    for epoch in range(epochs):
        # 设置模型为训练模式
        model.train()
        train_loss = 0  # 初始化训练损失
        for data, label in train_loader:
            # 将数据和标签移动到 GPU
            data, label = data.cuda(), label.cuda()
            # 梯度清零
            optimizer.zero_grad()
            # 模型前向传播,计算输出
            output = model(data)
            # 计算损失
            loss = criterion(output, label)
            # 反向传播
            loss.backward()
            # 更新模型参数
            optimizer.step()
            # 累加每批数据的损失,乘以数据量以便后续计算平均值
            train_loss += loss.item() * data.size(0)

        # 计算平均训练损失
        train_loss = train_loss / len(train_loader.dataset)
        loss_values.append(train_loss)
        # 打印当前轮次的训练损失
        print(
            "{}_Epoch: {} \tTraining Loss: {:.6f}".format(
                optimizer_name, epoch, train_loss
            )
        )  # 定义训练轮次

    losses[optimizer_name] = loss_values

注意:optimizer要和model一一对应,否则loss曲线不会变化,举个例子:

import torch.nn.functional as F


class Net(nn.Module):
    def __init__(self):
        super(Net, self).__init__()
        self.hidden = nn.Linear(1, 20)
        self.predict = nn.Linear(20, 1)

    def forward(self, x):
        x = self.hidden(x)
        x = F.relu(x)
        x = self.predict(x)
        return x


model1 = Net().cuda()
optimizer1 = optim.SGD(model1.parameters(), lr=0.1)
print(optimizer1.param_groups)
model2 = Net().cuda()
optimizer2 = optim.SGD(model2.parameters(), lr=0.1)
print(optimizer2.param_groups)
[{'params': [Parameter containing:
tensor([[ 0.1400],
        [-0.2828],
        [-0.5219],
        [ 0.4931],
        [ 0.5972],
        [-0.9816],
        [-0.3870],
        [-0.7150],
        [ 0.3099],
        [ 0.8617],
        [ 0.5654],
        [ 0.7250],
        [-0.9256],
        [ 0.3025],
        [-0.6910],
        [-0.4537],
        [ 0.8165],
        [-0.3432],
        [ 0.3256],
        [-0.9518]], device='cuda:0', requires_grad=True), Parameter containing:
tensor([-9.0673e-01,  9.0644e-01, -1.5883e-01,  6.9712e-01, -8.0039e-01,
         6.1430e-01,  2.0146e-05, -4.5959e-01,  8.0348e-01,  4.1588e-01,
        -2.7670e-01,  6.1961e-01,  4.5842e-01,  8.4449e-01, -5.2944e-01,
         6.3783e-02, -6.5253e-01, -5.3383e-01, -8.1351e-01, -4.3764e-01],
       device='cuda:0', requires_grad=True), Parameter containing:
tensor([[ 0.0631,  0.1532, -0.0404, -0.0549, -0.1947, -0.1402, -0.1900,  0.1285,
          0.1497, -0.1026,  0.0403, -0.1569,  0.1037, -0.0086,  0.1841,  0.0768,
          0.1236, -0.1554, -0.1467,  0.0567]], device='cuda:0',
       requires_grad=True), Parameter containing:
tensor([0.1260], device='cuda:0', requires_grad=True)], 'lr': 0.1, 'momentum': 0, 'dampening': 0, 'weight_decay': 0, 'nesterov': False}]
[{'params': [Parameter containing:
tensor([[-0.1754],
        [ 0.2470],
        [-0.2981],
        [-0.6169],
        [-0.4851],
        [ 0.2017],
        [-0.4956],
        [-0.6405],
        [-0.1223],
        [ 0.6771],
        [-0.5270],
        [ 0.8098],
        [ 0.2729],
        [ 0.8045],
        [ 0.3576],
        [-0.1010],
        [-0.4853],
        [-0.6071],
        [ 0.9380],
        [ 0.3663]], device='cuda:0', requires_grad=True), Parameter containing:
tensor([ 0.3290, -0.7945,  0.7811, -0.1526,  0.4757, -0.3252, -0.1946,  0.7696,
         0.3602,  0.9773, -0.3324, -0.7455,  0.7890, -0.7141, -0.0050, -0.4623,
        -0.9743,  0.3672, -0.2122,  0.8800], device='cuda:0',
       requires_grad=True), Parameter containing:
tensor([[-0.0177, -0.1399,  0.1147,  0.0263, -0.0336, -0.1196, -0.1704,  0.0720,
         -0.0674,  0.0690,  0.1642, -0.1475, -0.1569,  0.0337, -0.2108, -0.2020,
          0.1206, -0.0788,  0.2089, -0.0504]], device='cuda:0',
       requires_grad=True), Parameter containing:
tensor([-0.0203], device='cuda:0', requires_grad=True)], 'lr': 0.1, 'momentum': 0, 'dampening': 0, 'weight_decay': 0, 'nesterov': False}]

在每次创建新的模型时,model = Net().cuda() 会重新初始化模型的权重。由于神经网络的权重是通过随机初始化生成的(通常是均匀分布或正态分布),因此每次重新创建模型时,权重会有不同的初始值。这就是为什么你在每次运行 print(optimizer.param_groups) 时看到的参数不同。

当你用 optimizer2 去优化 model1 时,损失(loss)不会变化的原因在于,optimizer2 是在 model2 的参数上创建的,两个模型(model1model2)的参数是完全独立的,即使它们的结构相同,它们的权重也是分别初始化的。

9.4.8 开始训练

# 定义训练轮次
epochs = 20  # 训练 20 个 epoch

# 开始训练
for optimizer_name in optimizers:
    train(epochs, optimizer_name)  # 调用 train 函数
SGD_Epoch: 0 	Training Loss: 0.175653
SGD_Epoch: 1 	Training Loss: 0.074614
SGD_Epoch: 2 	Training Loss: 0.071039
SGD_Epoch: 3 	Training Loss: 0.067600
SGD_Epoch: 4 	Training Loss: 0.064227
SGD_Epoch: 5 	Training Loss: 0.061237
SGD_Epoch: 6 	Training Loss: 0.058338
SGD_Epoch: 7 	Training Loss: 0.055446
SGD_Epoch: 8 	Training Loss: 0.052653
SGD_Epoch: 9 	Training Loss: 0.050170
SGD_Epoch: 10 	Training Loss: 0.047690
SGD_Epoch: 11 	Training Loss: 0.045258
SGD_Epoch: 12 	Training Loss: 0.043105
SGD_Epoch: 13 	Training Loss: 0.040779
SGD_Epoch: 14 	Training Loss: 0.038811
SGD_Epoch: 15 	Training Loss: 0.036761
SGD_Epoch: 16 	Training Loss: 0.034865
SGD_Epoch: 17 	Training Loss: 0.033287
SGD_Epoch: 18 	Training Loss: 0.031622
SGD_Epoch: 19 	Training Loss: 0.030094
ASGD_Epoch: 0 	Training Loss: 0.120926
ASGD_Epoch: 1 	Training Loss: 0.100723
ASGD_Epoch: 2 	Training Loss: 0.097301
ASGD_Epoch: 3 	Training Loss: 0.094148
ASGD_Epoch: 4 	Training Loss: 0.091359
ASGD_Epoch: 5 	Training Loss: 0.088359
ASGD_Epoch: 6 	Training Loss: 0.085589
ASGD_Epoch: 7 	Training Loss: 0.082768
ASGD_Epoch: 8 	Training Loss: 0.080251
ASGD_Epoch: 9 	Training Loss: 0.077459
ASGD_Epoch: 10 	Training Loss: 0.074818
ASGD_Epoch: 11 	Training Loss: 0.072248
ASGD_Epoch: 12 	Training Loss: 0.069697
ASGD_Epoch: 13 	Training Loss: 0.067213
ASGD_Epoch: 14 	Training Loss: 0.064631
ASGD_Epoch: 15 	Training Loss: 0.062146
ASGD_Epoch: 16 	Training Loss: 0.059698
ASGD_Epoch: 17 	Training Loss: 0.057142
ASGD_Epoch: 18 	Training Loss: 0.054840
ASGD_Epoch: 19 	Training Loss: 0.052401
Momentum_Epoch: 0 	Training Loss: 0.105373
Momentum_Epoch: 1 	Training Loss: 0.048070
Momentum_Epoch: 2 	Training Loss: 0.028848
Momentum_Epoch: 3 	Training Loss: 0.018829
Momentum_Epoch: 4 	Training Loss: 0.014814
Momentum_Epoch: 5 	Training Loss: 0.013528
Momentum_Epoch: 6 	Training Loss: 0.013184
Momentum_Epoch: 7 	Training Loss: 0.013370
Momentum_Epoch: 8 	Training Loss: 0.012710
Momentum_Epoch: 9 	Training Loss: 0.012706
Momentum_Epoch: 10 	Training Loss: 0.012498
Momentum_Epoch: 11 	Training Loss: 0.012283
Momentum_Epoch: 12 	Training Loss: 0.012194
Momentum_Epoch: 13 	Training Loss: 0.011929
Momentum_Epoch: 14 	Training Loss: 0.011830
Momentum_Epoch: 15 	Training Loss: 0.011631
Momentum_Epoch: 16 	Training Loss: 0.011885
Momentum_Epoch: 17 	Training Loss: 0.011642
Momentum_Epoch: 18 	Training Loss: 0.011240
Momentum_Epoch: 19 	Training Loss: 0.011180
Adadelta_Epoch: 0 	Training Loss: 0.085456
Adadelta_Epoch: 1 	Training Loss: 0.040699
Adadelta_Epoch: 2 	Training Loss: 0.028017
Adadelta_Epoch: 3 	Training Loss: 0.019943
Adadelta_Epoch: 4 	Training Loss: 0.015753
Adadelta_Epoch: 5 	Training Loss: 0.013904
Adadelta_Epoch: 6 	Training Loss: 0.013305
Adadelta_Epoch: 7 	Training Loss: 0.012874
Adadelta_Epoch: 8 	Training Loss: 0.012705
Adadelta_Epoch: 9 	Training Loss: 0.012380
Adadelta_Epoch: 10 	Training Loss: 0.012277
Adadelta_Epoch: 11 	Training Loss: 0.011937
Adadelta_Epoch: 12 	Training Loss: 0.011697
Adadelta_Epoch: 13 	Training Loss: 0.011444
Adadelta_Epoch: 14 	Training Loss: 0.011567
Adadelta_Epoch: 15 	Training Loss: 0.011429
Adadelta_Epoch: 16 	Training Loss: 0.011310
Adadelta_Epoch: 17 	Training Loss: 0.011310
Adadelta_Epoch: 18 	Training Loss: 0.011156
Adadelta_Epoch: 19 	Training Loss: 0.010971
Adagrad_Epoch: 0 	Training Loss: 0.246438
Adagrad_Epoch: 1 	Training Loss: 0.064902
Adagrad_Epoch: 2 	Training Loss: 0.037900
Adagrad_Epoch: 3 	Training Loss: 0.030526
Adagrad_Epoch: 4 	Training Loss: 0.026471
Adagrad_Epoch: 5 	Training Loss: 0.023386
Adagrad_Epoch: 6 	Training Loss: 0.020951
Adagrad_Epoch: 7 	Training Loss: 0.019052
Adagrad_Epoch: 8 	Training Loss: 0.017478
Adagrad_Epoch: 9 	Training Loss: 0.016227
Adagrad_Epoch: 10 	Training Loss: 0.015246
Adagrad_Epoch: 11 	Training Loss: 0.014444
Adagrad_Epoch: 12 	Training Loss: 0.013833
Adagrad_Epoch: 13 	Training Loss: 0.013352
Adagrad_Epoch: 14 	Training Loss: 0.012960
Adagrad_Epoch: 15 	Training Loss: 0.012661
Adagrad_Epoch: 16 	Training Loss: 0.012396
Adagrad_Epoch: 17 	Training Loss: 0.012187
Adagrad_Epoch: 18 	Training Loss: 0.012013
Adagrad_Epoch: 19 	Training Loss: 0.011878
Adam_Epoch: 0 	Training Loss: 0.303009
Adam_Epoch: 1 	Training Loss: 0.165425
Adam_Epoch: 2 	Training Loss: 0.097807
Adam_Epoch: 3 	Training Loss: 0.069818
Adam_Epoch: 4 	Training Loss: 0.058352
Adam_Epoch: 5 	Training Loss: 0.051228
Adam_Epoch: 6 	Training Loss: 0.045836
Adam_Epoch: 7 	Training Loss: 0.040853
Adam_Epoch: 8 	Training Loss: 0.036157
Adam_Epoch: 9 	Training Loss: 0.032023
Adam_Epoch: 10 	Training Loss: 0.028294
Adam_Epoch: 11 	Training Loss: 0.025326
Adam_Epoch: 12 	Training Loss: 0.022797
Adam_Epoch: 13 	Training Loss: 0.020765
Adam_Epoch: 14 	Training Loss: 0.019065
Adam_Epoch: 15 	Training Loss: 0.017749
Adam_Epoch: 16 	Training Loss: 0.016716
Adam_Epoch: 17 	Training Loss: 0.015834
Adam_Epoch: 18 	Training Loss: 0.015133
Adam_Epoch: 19 	Training Loss: 0.014580
AdamW_Epoch: 0 	Training Loss: 0.111923
AdamW_Epoch: 1 	Training Loss: 0.100028
AdamW_Epoch: 2 	Training Loss: 0.092435
AdamW_Epoch: 3 	Training Loss: 0.084679
AdamW_Epoch: 4 	Training Loss: 0.077338
AdamW_Epoch: 5 	Training Loss: 0.068840
AdamW_Epoch: 6 	Training Loss: 0.060869
AdamW_Epoch: 7 	Training Loss: 0.053098
AdamW_Epoch: 8 	Training Loss: 0.046124
AdamW_Epoch: 9 	Training Loss: 0.039714
AdamW_Epoch: 10 	Training Loss: 0.034196
AdamW_Epoch: 11 	Training Loss: 0.029482
AdamW_Epoch: 12 	Training Loss: 0.025664
AdamW_Epoch: 13 	Training Loss: 0.022628
AdamW_Epoch: 14 	Training Loss: 0.020305
AdamW_Epoch: 15 	Training Loss: 0.018254
AdamW_Epoch: 16 	Training Loss: 0.016893
AdamW_Epoch: 17 	Training Loss: 0.015755
AdamW_Epoch: 18 	Training Loss: 0.015070
AdamW_Epoch: 19 	Training Loss: 0.014533
Adamax_Epoch: 0 	Training Loss: 0.202550
Adamax_Epoch: 1 	Training Loss: 0.109890
Adamax_Epoch: 2 	Training Loss: 0.086472
Adamax_Epoch: 3 	Training Loss: 0.075268
Adamax_Epoch: 4 	Training Loss: 0.066507
Adamax_Epoch: 5 	Training Loss: 0.058666
Adamax_Epoch: 6 	Training Loss: 0.051664
Adamax_Epoch: 7 	Training Loss: 0.045487
Adamax_Epoch: 8 	Training Loss: 0.040220
Adamax_Epoch: 9 	Training Loss: 0.035473
Adamax_Epoch: 10 	Training Loss: 0.031388
Adamax_Epoch: 11 	Training Loss: 0.028214
Adamax_Epoch: 12 	Training Loss: 0.025620
Adamax_Epoch: 13 	Training Loss: 0.023453
Adamax_Epoch: 14 	Training Loss: 0.021551
Adamax_Epoch: 15 	Training Loss: 0.019928
Adamax_Epoch: 16 	Training Loss: 0.018586
Adamax_Epoch: 17 	Training Loss: 0.017402
Adamax_Epoch: 18 	Training Loss: 0.016468
Adamax_Epoch: 19 	Training Loss: 0.015690
RMSprop_Epoch: 0 	Training Loss: 0.052094
RMSprop_Epoch: 1 	Training Loss: 0.020346
RMSprop_Epoch: 2 	Training Loss: 0.017410
RMSprop_Epoch: 3 	Training Loss: 0.015630
RMSprop_Epoch: 4 	Training Loss: 0.013828
RMSprop_Epoch: 5 	Training Loss: 0.012763
RMSprop_Epoch: 6 	Training Loss: 0.012641
RMSprop_Epoch: 7 	Training Loss: 0.013144
RMSprop_Epoch: 8 	Training Loss: 0.012431
RMSprop_Epoch: 9 	Training Loss: 0.012544
RMSprop_Epoch: 10 	Training Loss: 0.011821
RMSprop_Epoch: 11 	Training Loss: 0.012286
RMSprop_Epoch: 12 	Training Loss: 0.011211
RMSprop_Epoch: 13 	Training Loss: 0.011523
RMSprop_Epoch: 14 	Training Loss: 0.011254
RMSprop_Epoch: 15 	Training Loss: 0.012431
RMSprop_Epoch: 16 	Training Loss: 0.011523
RMSprop_Epoch: 17 	Training Loss: 0.010601
RMSprop_Epoch: 18 	Training Loss: 0.012104
RMSprop_Epoch: 19 	Training Loss: 0.012227

9.4.9 查看loss曲线

import matplotlib.pyplot as plt

plt.figure(figsize=(10, 6))
for optimizer_name, loss_values in losses.items():
    plt.plot(range(epochs), loss_values, label=optimizer_name)

plt.xlabel("Epochs")
plt.ylabel("Loss")
plt.title("Comparison of Optimizers")
plt.legend()
plt.show()

纵坐标代表Loss,横坐标代表的是Step: 

在上面的图片上,曲线下降的趋势和对应的steps代表了在这轮数据,模型下的收敛速度

注意:

优化器的选择是需要根据模型进行改变的,不存在绝对的好坏之分,我们需要多进行一些测试。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值