Pytorch学习笔记

Pytorch笔记

Tensor 的创建

在PyTorch中,创建张量(Tensor)是一项基本操作,可以通过多种方式实现。以下是一些常见的创建张量的方法:

  1. 直接从数据创建:

    import torch
    data = [[1, 2], [3, 4]]
    x_data = torch.tensor(data)
    
  2. 从NumPy数组创建:

    import numpy as np
    np_array = np.array(data)
    x_np = torch.from_numpy(np_array)
    

torch.from_numpy() 函数创建的 PyTorch 张量与原始的 NumPy 数组共享内存

  1. 从另一个张量创建:

    x_data = torch.tensor(data)
    x_ones = torch.ones_like(x_data)  # 保持x_data的属性
    x_rand = torch.rand_like(x_data, dtype=torch.float)  # 覆盖x_data的数据类型
    
  2. 通过指定维度创建:

    shape = (2, 3,)
    zeros = torch.zeros(shape)
    ones = torch.ones(shape)
    rand = torch.rand(shape)
    
  3. 通过arange和linspace创建连续值张量:

    x_arange = torch.arange(start=0, end=5, step=1)  # [0, 1, 2, 3, 4]
    x_linspace = torch.linspace(start=0, end=5, steps=5)  # [0.0, 1.25, 2.5, 3.75, 5.0]
    
  4. 通过特定分布创建张量:

    normal = torch.randn((2, 3))  # 标准正态分布
    uniform = torch.rand((2, 3))  # 均匀分布
    

torch.arange ()和 torch.linspace() 的区别?

torch.arange()torch.linspace() 都是PyTorch中用来生成一定范围内数值的函数,但它们在生成这些数值时使用不同的方法。

  1. torch.arange():

    • torch.arange(start=0, end, step=1, out=None, dtype=None, layout=torch.strided, device=None, requires_grad=False)
    • 这个函数生成一系列数值,这些数值从 start 开始,以 step 为间隔,增加到 end(但不包括 end)。step 是必须指定的参数。
    • 例如,torch.arange(0, 5, 1) 将输出 [0, 1, 2, 3, 4]
  2. torch.linspace():

    • torch.linspace(start, end, steps=100, out=None, dtype=None, layout=torch.strided, device=None, requires_grad=False)
    • torch.linspace() 也生成一系列数值,这些数值从 start 开始到 end 结束,包括两端的值。不同的是,这个函数将在包括开始和结束值之间均匀分布 steps 个点,其中 steps 参数指定了要生成的总数值的数量
    • 例如,torch.linspace(0, 5, 5) 将输出 [0.0, 1.25, 2.5, 3.75, 5.0]

总结来说,torch.arange() 根据指定的步长生成数值,可能因为步长设置而不包含结束值。而 torch.linspace() 在指定范围内生成固定数量的等间隔的数值,始终包含开始和结束值。

tensor的 shape 属性和 size() 方法的区别是什么?

在 PyTorch 中,shape 属性和 size() 方法都用于获取张量的形状(即各维度的大小),但有一些细微的区别:

  1. shape 属性是一个属性,可以直接通过张量对象访问,返回一个元组(tuple),包含了张量每个维度的大小。例如,对于一个二维张量 tensor,可以通过 tensor.shape 获取形状,返回值为 (rows, columns)

  2. size() 方法是一个函数,不指定参数时和 shape 属性输出一样。可以通过指定维度作为参数来获取特定维度的大小,也可以不指定参数获取整个张量的大小。如果没有指定参数,则返回一个包含所有维度大小的元组。如果指定了参数,则返回该维度的大小。

下面是一个示例来说明两者的用法:

import torch

# 创建一个二维张量
tensor = torch.tensor([[1, 2, 3], [4, 5, 6]])

# 使用 shape 属性获取形状
print("shape 属性:", tensor.shape)

# 使用 size() 方法获取整个张量的大小
print("整个张量的大小:", tensor.numel())  # 或者 len(tensor.view(-1))

# 使用 size() 方法获取特定维度的大小
print("第一个维度的大小:", tensor.size(0))  # 行数
print("第二个维度的大小:", tensor.size(1))  # 列数

输出结果为:

shape 属性: torch.Size([2, 3])
整个张量的大小: 6
第一个维度的大小: 2
第二个维度的大小: 3

总的来说,shape 属性更直观地表示了张量的形状,而 size() 方法则更灵活,可以用于获取整个张量大小或特定维度的大小。

怎么确定 Tensor 的维度和各个维度是几维的?

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

拿上面这个 tensor 举例,对于给定的张量,可以按照以下步骤来确定其形状:

  1. 从左往右数,有几个连续的 [ 括号,就是张量的维数。
  2. 对于每个维度,去掉最外层的 [] 括号后,剩余的数据被括起来的部分,有几个并列的就是该维度的大小。

继续去掉最外层的 [] 括号直到只剩下 1,2,3 三个并列的数字,那么最后一个维度的数值是 3

根据这个方法,这个张量的形状为 (2, 6, 3)。第一个维度有两个并列的数据,第二个维度有六个并列的数据,第三个维度有三个并列的数据。

在这里插入图片描述

torch.normal() 和 torch.randn() 的区别?

torch.normal()torch.randn() 都是 PyTorch 中用于生成服从正态分布的张量的函数,但它们之间有一些区别:

  1. torch.normal(mean, std, size):生成一个具有指定均值(mean)和标准差(std)的正态分布张量。meanstd 分别指定了正态分布的均值和标准差,size 参数用于指定生成张量的形状。

  2. torch.randn(size):生成一个具有标准正态分布(均值为 0,标准差为 1)的张量。size 参数用于指定生成张量的形状。

因此,主要区别在于生成的分布的参数不同。torch.normal() 可以生成具有任意均值和标准差的正态分布张量,而 torch.randn() 只能生成均值为 0,标准差为 1 的标准正态分布张量。

以下是两个函数的简单示例:

import torch

# 使用 torch.normal() 生成指定均值和标准差的正态分布张量
mean = torch.tensor([0.0])
std = torch.tensor([1.0])
size = (3, 3)
normal_tensor = torch.normal(mean, std, size)
print("torch.normal() 生成的张量:")
print(normal_tensor)

# 使用 torch.randn() 生成标准正态分布张量
size = (3, 3)
randn_tensor = torch.randn(size)
print("\ntorch.randn() 生成的张量:")
print(randn_tensor)

这个示例将生成两个 3x3 形状的张量,一个是指定均值和标准差的正态分布张量,另一个是标准正态分布张量。

Tensor 的操作

获取元素总个数

使用 numel() 方法
在这里插入图片描述

就地操作和非就地操作

在PyTorch中,就地操作(in-place operation)是指直接修改调用它的张量,而不是创建一个新的张量。通常,就地操作的函数名以一个下划线_结尾。例如:

  • 就地操作:

    x = torch.tensor([1, 2, 3])
    x.add_(1)  # 就地将x中的每个元素加1,x被直接修改
    
  • 非就地操作:

    y = x.add(1)  # 创建一个新的张量y,其中的元素是x中的元素加1,x不被修改
    

在上面的例子中,x.add_(1) 是一个就地操作,它直接修改了 x。而 x.add(1) 是一个非就地操作,它创建了一个新的张量 y,而 x 保持不变。

在使用PyTorch时,应该注意就地操作可能会导致梯度计算出现问题,特别是在构建神经网络时。因此,在不确定的情况下,建议使用非就地操作以避免潜在的问题。

为什么下面这行代码中 3 后面会有逗号?

shape = (2, 3,)

在Python中,当定义只有一个元素的元组时,需要在该元素后面加上逗号,以区分单个元素和整数或表达式之间的差异。例如,(2,) 是一个元素为2的元组,而 (2) 只是数字2。

例子 shape = (2, 3,) 中,虽然逗号不是必需的(因为元组中有多个元素),但加上它没有坏处,且有时候可以提高代码的一致性和可读性。

所以,这里的逗号主要是出于编码风格的考虑,而不是区分元组和其他类型。你完全可以写成 shape = (2, 3),效果是一样的。

转置

t() 方法

仅适用于二维张量(即矩阵)。它用于转置矩阵,即交换矩阵的行和列。

x = torch.tensor([[1, 2, 3], [4, 5, 6]])
x.t()

在这里插入图片描述

transpose() 方法

transpose() 方法是 PyTorch 中用于对张量进行转置操作的函数。转置是指将张量的维度重新排列,使得原来的维度顺序发生变化。在 PyTorch 中,transpose() 方法有两种形式:transpose(dim0, dim1)transpose(*dims)

  1. transpose(dim0, dim1):该形式的 transpose() 方法允许你指定要交换的两个维度。参数 dim0dim1 是要交换的维度的索引。例如,transpose(0, 1) 表示交换第一个和第二个维度。返回一个交换了两个指定维度的新张量,而不会更改原始张量

一个示例如下,

在这里插入图片描述

从矩阵的角度理解,

当你对一个三维张量使用 transpose(0, 1) 时,你是在交换第一个维度和第二个维度。在这个例子中,原始张量 tensor_rand 的形状是 [2, 3, 4],即有 2 个 3x4 的矩阵。转置后,张量的形状变为 [3, 2, 4],即有 3 个 2x4 的矩阵。

原始张量 tensor_rand 的两个 3x4 矩阵是:

0.6197  0.3613  0.9128  0.0042
0.6963  0.3591  0.5795  0.6355
0.7525  0.3287  0.4007  0.3191

0.0939  0.2464  0.6736  0.1737
0.8777  0.0826  0.9153  0.3885
0.3252  0.5918  0.1457  0.6855

当你执行 transpose(0, 1) 时,第一个和第二个维度被交换。这意味着原始张量中的两个 3x4 矩阵变成了三个 2x4 矩阵,其中每个 2x4 矩阵的两行分别来自原始张量的两个 3x4 矩阵的对应行:

0.6197  0.3613  0.9128  0.0042  <-- 第一个矩阵的第一行
0.0939  0.2464  0.6736  0.1737  <-- 第二个矩阵的第一行

0.6963  0.3591  0.5795  0.6355  <-- 第一个矩阵的第二行
0.8777  0.0826  0.9153  0.3885  <-- 第二个矩阵的第二行

0.7525  0.3287  0.4007  0.3191  <-- 第一个矩阵的第三行
0.3252  0.5918  0.1457  0.6855  <-- 第二个矩阵的第三行

所以,转置后的张量 r_tans 就是:

[[[0.6197, 0.3613, 0.9128, 0.0042],
  [0.0939, 0.2464, 0.6736, 0.1737]],

 [[0.6963, 0.3591, 0.5795, 0.6355],
  [0.8777, 0.0826, 0.9153, 0.3885]],

 [[0.7525, 0.3287, 0.4007, 0.3191],
  [0.3252, 0.5918, 0.1457, 0.6855]]]

每个新的 2 × 4 2\times4 2×4 矩阵都是由原始的两个 3 × 4 3\times4 3×4 矩阵的对应行组成的。

同理,下面的示例,在交换第一个维度和第三个维度。在这个例子中,原始张量 tensor_rand 的形状是 [2, 3, 4],即有 2 个 3 × 4 3\times4 3×4 的矩阵。转置后,张量的形状变为 [4, 3, 2],即有 4 个 3 × 2 3\times2 3×2 的矩阵。

在这里插入图片描述

切片和索引

x = torch.tensor([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
print(x[1, :])  # 第二行
print(x[:, 1])  # 第二列
print(x[0:2, 0:2])  # 左上角的2x2子矩阵

x[1, :]第一个参数是行,下标从0开始,第二个参数是列,: 代选择所有列
x[0:2, 0:2] 中的0:2左闭右开,所以是 2 × 2 2\times 2 2×2 子矩阵

x[:2, :3]x[0:2, 0:3]是相同的

在这里插入图片描述

当你切片一个张量时,结果是原始数据的视图,而不是复制。这意味着如果你修改了切片,原始数据也会被修改

如果你需要一个切片的副本,请使用.clone()方法。

使用步长 stride 的切片

两个冒号::
在这里插入图片描述
x[:, 1::2]1::2 代表选中第一列和第三列的元素,其中 2 是步长

负索引

x[:, -1] 选中所有行, − 1 -1 1 则代表最后一列

在这里插入图片描述

# 获取最后两列的元素
last_two_columns = x[:, -2:]
# 输出: tensor([[3, 4], [7, 8]])

last_two_columns = x[:, -2:]last_two_columns = x[:, -2:-1] 的区别是 ?

  • last_two_columns = x[:, -2:] 表示从倒数第二列开始直到最后一列的所有列。由于 切片操作的结束索引在Python中是排他的(exclusive),并且当省略结束索引时,切片会一直扩展到维度的末尾,所以这个操作会选取张量中的最后两列。

  • last_two_columns = x[:, -2:-1] 表示从倒数第二列开始到倒数第一列(但不包括倒数第一列)的所有列。因此,这个操作只会选取倒数第二列。

下面通过一个示例来演示这两个操作的区别:

import torch

x = torch.tensor([[1, 2, 3, 4], [5, 6, 7, 8]])

last_two_columns = x[:, -2:]
# 输出将会是:
# tensor([[3, 4],
#         [7, 8]])

last_column_excluded = x[:, -2:-1]
# 输出将会是:
# tensor([[3],
#         [7]])

在第一个操作中,我们得到一个形状为 (2, 2) 的张量,它包含了原始张量 x 的最后两列。
在第二个操作中,我们得到一个形状为 (2, 1) 的张量,它只包含了原始张量 x 的倒数第二列。

高级索引

当使用比原张量维度更多的索引数组时,可以进行更复杂的操作

# 创建一个3x3的张量
y = torch.tensor([[9, 8, 7], [6, 5, 4], [3, 2, 1]])

# 选取(0,0),(1,1)和(2,2)位置的元素
elements = y[[0, 1, 2], [0, 1, 2]]
# 输出: tensor([9, 5, 1])

在这里插入图片描述

改变形状

view()

PyTorch中的view()方法是一个非常有用的工具,允许用户在不改变数据本身的前提下改变张量的形状。这种操作在神经网络的前向传播和后向传播过程中经常会用到,尤其是在层与层之间进行转换时。

当你使用view()时,必须确保新形状与原始张量包含的元素数量兼容

在这里插入图片描述
可以看到,x 本身并未被 view 改变形状。

z = x.view(-1, 3) 这行代码,哪个维度被填入 − 1 -1 1,意味着这个维度利用其他已经指定的维度来自动计算这个维度应该是几,这行代码意思是,指定了列数为 3 3 3,让系统自动计算行数,因为 tensor 总共 9 9 9 个元素,所以自动计算出为 3 3 3

示例解释

假设我们有一个初始的一维张量:

import torch

# 创建一个有12个元素的一维张量
x = torch.arange(12)
# 输出: tensor([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11])

此张量x有12个元素,从0到11。现在,如果我们想把它变成一个形状为3x4的二维张量,我们可以使用view()方法:

# 使用view()来改变张量的形状
x_reshaped = x.view(3, 4)
# 输出: tensor([[ 0,  1,  2,  3],
#               [ 4,  5,  6,  7],
#               [ 8,  9, 10, 11]])

现在,x_reshaped是一个3行4列的二维张量。注意,view()方法并没有改变原始张量中的数据,只是改变了数据的展现方式, 此时 x 并未被改变 。

另外,可以使用-1作为view()方法中的一个维度参数,让PyTorch自动计算这个维度:

# 使用-1来让PyTorch自动计算其他维度
x_auto = x.view(3, -1)
# 输出: tensor([[ 0,  1,  2,  3],
#               [ 4,  5,  6,  7],
#               [ 8,  9, 10, 11]])

在这个例子中,-1告诉PyTorch自动计算剩余的维度,因此它将会创建一个大小为(3, 4)的张量,因为12 / 3 = 4

使用view()的时候需要确保张量已经在内存中是连续的(contiguous)

在执行某些操作后,比如.transpose(),张量可能不再连续,这时候需要在调用.view()之前先调用.contiguous()方法。

重要的是要注意view()操作返回的新张量与源张量共享相同的数据内存

因此,改变其中一个张量的元素也会改变另一个张量。如果你需要一个数据上完全独立的副本,则应该使用.clone()来创建一个副本,再使用.view()改变形状。

可以看到,改变 z 之后,x 对应的元素也发生变化

在这里插入图片描述

简而言之,view()方法是一种非常高效的操作,因为它不涉及数据的实际物理复制。它提供了一种方便的方式来重构张量的形状,以适应不同操作的需求。

reshape()

torch.reshape() 用于改变张量的形状而不改变其数据。这个函数可以重新排列张量的维度和大小,但保持元素总数不变。

函数的定义如下:

torch.reshape(input, shape) → Tensor

参数解释:

  • input:输入的张量。
  • shape:一个整数元组,表示输出张量的新形状。新形状的元素总数必须与输入张量的元素总数相同。

返回值:

  • 返回一个新的张量,其形状按照指定的shape参数重新排列。

注意,torch.reshape()可能会返回一个与输入张量共享内存的张量,也可能会返回一个复制了数据的新张量,这取决于输入张量的内存布局和请求的形状。

举个例子,假设我们有一个一维张量A,我们想要将其重新排列为一个二维张量:

import torch

A = torch.arange(1, 7)  # 创建一个一维张量 [1, 2, 3, 4, 5, 6]
B = torch.reshape(A, (2, 3))  # 将 A 重新排列为一个 2x3 的二维张量

print(B)

输出将会是:

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

这里,我们将一维张量A重新排列为一个形状为(2, 3)的二维张量B,元素总数保持不变。

torch.squeeze()torch.unsqueeze() 是 PyTorch 中的两个函数,用于调整张量的形状,特别是处理维度为 1 的情况。

torch.squeeze()

torch.squeeze() 函数用于移除张量中所有维度为 1 的维度

这在某些情况下很有用,比如当你想要移除由于某些操作(如unsqueeze)而产生的额外维度。

函数的定义如下:

torch.squeeze(input, dim=None) → Tensor

参数解释:

  • input:输入的张量。
  • dim:可选参数,指定要压缩的维度。如果指定了dim,则只会压缩该维度(如果该维度的大小为 1);如果未指定,则移除所有大小为 1 的维度。

返回值:

  • 返回一个新的张量,其形状为移除了大小为 1 的维度后的形状。

举个例子:

import torch

A = torch.tensor([[[1, 2, 3]]])  # 形状为 (1, 1, 3)
B = torch.squeeze(A)             # 移除所有大小为 1 的维度,变为 (3,)

print(B.shape)

输出将会是:

torch.Size([3])

在这里插入图片描述

torch.unsqueeze()

torch.unsqueeze() 函数用于在指定位置添加一个大小为 1 的维度

这在某些情况下很有用,比如当需要增加一个额外的维度以满足某个操作的需求。

函数的定义如下:

torch.unsqueeze(input, dim) → Tensor

参数解释:

  • input:输入的张量。
  • dim:要插入维度的索引。插入后,原来在该位置及其后面的所有维度的索引都会增加 1。

返回值:

  • 返回一个新的张量,其形状在指定位置添加了一个大小为 1 的维度。

举个例子:

import torch

A = torch.tensor([1, 2, 3])  # 形状为 (3,)
B = torch.unsqueeze(A, 0)    # 在第 0 个维度添加大小为 1 的维度,变为 (1, 3)

print(B.shape)

输出将会是:

torch.Size([1, 3])

在这里插入图片描述

在这个例子中,我们在一维张量A的第 0 个维度位置添加了一个大小为 1 的维度,从而将其形状从(3,)变为(1, 3)

广播机制

x = torch.tensor([[1, 2, 3], [4, 5, 6]])
y = torch.tensor([1, 2, 3])
print(x + y)  # y被广播以匹配x的形状,然后逐元素相加

在这里插入图片描述

拼接

当处理PyTorch张量时,有几个函数可以用来合并或分割它们,torch.cat(), torch.stack(), torch.chunk(), 和 torch.split() 是其中一些常用的函数。下面我会详细解释它们各自的作用:

torch.cat()

torch.cat() 用于将一系列张量按指定的维度连接起来。所有被连接的张量的形状必须在除了被连接维度外的所有维度上相同。

在这里插入图片描述
从矩阵角度来理解

t 1 = [ 1 2 3 4 ] t1 = \left.\begin{bmatrix} 1 & 2 \\ 3 & 4 \\ \end{bmatrix}\right. t1=[1324] t 2 = [ 5 6 7 8 ] t2= \left.\begin{bmatrix} 5 & 6 \\ 7 & 8 \\ \end{bmatrix}\right. t2=[5768],那么从第一个维度,即行维度进行拼接,即 t o r c h . c a t ( [ t 1 , t 2 ] , d i m = 0 ) = [ 1 2 3 4 5 6 7 8 ] torch.cat([t1, t2], dim=0)= \left.\begin{bmatrix} 1 & 2 \\ 3 & 4 \\ 5 & 6 \\ 7 & 8 \\ \end{bmatrix}\right. torch.cat([t1,t2],dim=0)= 13572468

那么从第二个维度,即列维度进行拼接,即 t o r c h . c a t ( [ t 1 , t 2 ] , d i m = 1 ) = [ 1 2 5 6 3 4 7 8 ] torch.cat([t1, t2], dim=1)= \left.\begin{bmatrix} 1 & 2 & 5 & 6 \\ 3 & 4 & 7 & 8 \\ \end{bmatrix}\right. torch.cat([t1,t2],dim=1)=[13245768]

torch.stack()

堆叠后返回的张量,维度个数 + 1 +1 +1

下面 2 2 2 个 Tensor 的 shape 都是 [ 3 , 3 ] [3, 3] [3,3]

在这里插入图片描述

dim=0 堆叠时,输出是 2 2 2 3 × 3 3\times 3 3×3 矩阵,
dim=1 堆叠时,输出是 3 3 3 2 × 3 2\times 3 2×3 矩阵,T1的第一行和T2的第一行,以此类推
dim=2 堆叠时,输出是 3 3 3 3 × 2 3\times 2 3×2 矩阵,将T1和T2第一行转置组成 2 2 2

T1 = torch.tensor([[1, 2, 3],
                   [4, 5, 6],
                   [7, 8, 9]])
                       
T2 = torch.tensor([[10, 20, 30],
                   [40, 50, 60],
                   [70, 80, 90]])
 
my_stack_00 = torch.stack((T1,T2),dim=0)
my_stack_01 = torch.stack((T1, T2), dim=1) 
my_stack_02 = torch.stack((T1, T2), dim=2)
my_stack_03 = torch.stack((T1, T2), dim=3)

运行的结果是:

my_stack_00:tensor([[[ 1,  2,  3],
			         [ 4,  5,  6],
      				 [ 7,  8,  9]],
 
       				[[10, 20, 30],
         			[40, 50, 60],
         			[70, 80, 90]]]) shape:torch.Size([2, 3, 3])
 
my_stack_01:tensor([[[ 1,  2,  3],
                    [10, 20, 30]],
 
                    [[ 4,  5,  6],
                    [40, 50, 60]],
 
       				[[ 7,  8,  9],
        		    [70, 80, 90]]]) shape:torch.Size([3, 2, 3])
 
my_stack_02:tensor([[[ 1, 10],
         			 [ 2, 20],
         			 [ 3, 30]],
 
        			[[ 4, 40],
        		 	 [ 5, 50],
         		     [ 6, 60]],
 
        			[[ 7, 70],
                     [ 8, 80],
         			 [ 9, 90]]]) shape:torch.Size([3, 3, 2])
Traceback (most recent call last):
  File "C:\Users\DELL\PycharmProjects\hellepytorch\lesson-03.py", line 73, in <module>

torch.cat() 和 torch.stack() 的区别?

torch.cat()torch.stack() 都是用于将多个张量沿指定维度进行连接的 PyTorch 函数,但它们之间有一些区别:

  1. torch.cat():沿着现有的维度对张量进行连接。连接后的张量的维度不会增加。需要保证除连接维度外,其他维度的大小必须一致。

  2. torch.stack():在新创建的维度上对张量序列进行堆叠。堆叠后的张量的维度会增加。需要保证所有要堆叠的张量的形状必须完全相同

下面是一个简单的示例来说明两者之间的区别:

import torch

# 创建两个张量
tensor1 = torch.tensor([[1, 2, 3], [4, 5, 6]])
tensor2 = torch.tensor([[7, 8, 9], [10, 11, 12]])

# 使用 torch.cat() 进行连接
cat_result = torch.cat((tensor1, tensor2), dim=0)
print("torch.cat() 连接后的结果:")
print(cat_result)
print("连接后的形状:", cat_result.shape)

# 使用 torch.stack() 进行堆叠
stack_result = torch.stack((tensor1, tensor2), dim=0)
print("\ntorch.stack() 堆叠后的结果:")
print(stack_result)
print("堆叠后的形状:", stack_result.shape)

输出结果为:

torch.cat() 连接后的结果:
tensor([[ 1,  2,  3],
        [ 4,  5,  6],
        [ 7,  8,  9],
        [10, 11, 12]])
连接后的形状: torch.Size([4, 3])

torch.stack() 堆叠后的结果:
tensor([[[ 1,  2,  3],
         [ 4,  5,  6]],

        [[ 7,  8,  9],
         [10, 11, 12]]])
堆叠后的形状: torch.Size([2, 2, 3])

可以看到,使用 torch.cat() 连接后的张量形状为 [4, 3],而使用 torch.stack() 堆叠后的张量形状为 [2, 2, 3],多了一个维度。

torch.chunk()

torch.chunk() 将一个张量分割成特定数量的块。就是切成几块

你指定想要得到多少块,PyTorch会尽可能平均地分割张量

函数签名如下:

torch.chunk(input, chunks, dim=0)

参数说明:

  • input:要分割的张量。
  • chunks:整数,指定要分割成的块的数量。如果无法均匀分割,则最后一个块会小于其他块。
  • dim:整数,指定沿哪个维度进行分割。默认为0。

返回值:

  • 返回一个张量列表,其中包含分割后的块

示例:

import torch

# 创建一个张量
x = torch.tensor([[1, 2, 3], 
				  [4, 5, 6],
				  [7, 8, 9]]) # shape = [3,3]

# 沿第0维(行)将张量分割成3个块
chunks = torch.chunk(x, chunks=3, dim=0)
for i, chunk in enumerate(chunks):
    print(f"Chunk {i}:\n{chunk}")

# 输出:
# Chunk 0:
# tensor([[1, 2, 3]])  # shape = [1,3]
# Chunk 1:
# tensor([[4, 5, 6]])  # shape = [1,3]
# Chunk 2:
# tensor([[7, 8, 9]])  # shape = [1,3]

# 沿第1维(列)将张量分割成2个块
chunks = torch.chunk(x, chunks=2, dim=1)
for i, chunk in enumerate(chunks):
    print(f"Chunk {i}:\n{chunk}")

# 输出:
# Chunk 0:
# tensor([[1, 2],
#         [4, 5],
#         [7, 8]]) # shape = [3,2]
# Chunk 1:
# tensor([[3],
#         [6],
#         [9]]) # shape = [3,1]

在这个例子中,首先沿着第0维(行)将张量x分割成3个块,然后沿着第1维(列)将其分割成2个块。

注意,当沿着列分割时,由于无法均匀分割成2个块,最后一个块只包含一列。

torch.split()

tensor.split() 函数用于将张量分割成指定大小的块

tensor.chunk() 不同,tensor.split() 允许你指定每个块的大小,而不是块的数量

函数签名如下:

torch.split(tensor, split_size_or_sections, dim=0)

参数说明:

  • tensor:要分割的张量。
  • split_size_or_sections:如果是单个整数,则表示每个块的大小(沿着指定的维度)。如果是整数列表,则列表中每个整数元素表示每个块在指定维度的具体大小,此时列表中的整数之和应该等于沿着分割维度的张量大小
  • dim:整数,指定沿哪个维度进行分割。默认为0。

返回值:

  • 返回一个张量列表,其中包含分割后的块。

示例:

import torch

# 创建一个张量
x = torch.tensor([[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]]) #shape = [7,4]

# 沿第0维(行)将张量分割成行数为 2 的块
split1 = torch.split(x, split_size_or_sections=2, dim=0)
for i, chunk in enumerate(split1):
    print(f"Split {i}:\n{chunk}")

# 输出:
# Split 0:
# tensor([[1, 2, 3, 4],
#         [5, 6, 7, 8]]) #shape = [2,4]
# Split 1:
# tensor([[ 9, 10, 11, 12],
#        [13, 14, 15, 16]]) #shape = [2,4]
# Split 2:
# tensor([[17, 18, 19, 20],
#        [21, 22, 23, 24]]) #shape = [2,4]
# Split 3:
# tensor([[25, 26, 27, 28]]) #shape = [1,4]

# 沿第1维(列)将张量分割成大小为[1, 3]的块,
# 也就是第一个块列数为1,第二个块列数为3
# list[1, 3]的元素相加起来必须等于沿着分割的维度的shape值
split2 = torch.split(x, split_size_or_sections=[1, 3], dim=1) 
for i, chunk in enumerate(split2):
    print(f"Split {i}:\n{chunk}")

# 输出:
# Split 0:
#tensor([[ 1],
#        [ 5],
#        [ 9],
#        [13],
#        [17],
#        [21],
#        [25]])
# Split 1:
#tensor([[ 2,  3,  4],
#        [ 6,  7,  8],
#        [10, 11, 12],
#        [14, 15, 16],
#        [18, 19, 20],
#        [22, 23, 24],
#        [26, 27, 28]])

索引

torch.index_select()

torch.index_select()是PyTorch中的一个函数,用于根据索引在指定维度上选择张量的元素

当你想要从一个张量中挑选出特定行、列或者其他维度的元素时,可以使用它。

函数的定义如下:

torch.index_select(input, dim, index, out=None) → Tensor

参数解释:

  • input:输入的张量。
  • dim:要选择元素的维度。例如,对于二维张量(矩阵),dim=0表示选择行,dim=1表示选择列。
  • index:一个包含索引的一维张量(即一维数组),表示在指定维度上要选择的元素的索引。这个索引张量中的每个值都应该是小于input在指定维度上的大小的非负整数。
  • out:可选参数,用于指定输出张量。如果指定了,输出将被写入这个张量中。

返回值:

  • 返回一个新的张量,包含了根据indexdim维度上选择的input张量的元素。

举个例子,假设我们有一个二维张量(矩阵)A,我们想要选择第0维(行)上的第1行和第3行:

import torch

A = torch.tensor([[1, 2, 3],
                  [4, 5, 6],
                  [7, 8, 9],
                  [10, 11, 12]])

indices = torch.tensor([1, 3])
B = torch.index_select(A, dim=0, index=indices)

print(B)

输出将会是:

tensor([[ 4,  5,  6],
        [10, 11, 12]])

这里,我们选择了A的第1行和第3行(索引从0开始)在新的张量B中。

torch.masked_select()

torch.masked_select() 用于根据一个布尔掩码(mask)选择张量中的元素

这个函数会返回一个一维张量,包含了输入张量中所有在掩码中对应位置为True的元素

函数的定义如下:

torch.masked_select(input, mask, out=None) → Tensor

参数解释:

  • input:输入的张量。
  • mask:一个与input形状相同的布尔张量(torch.bool类型),用于指定要选择的元素。掩码中的True值表示对应位置的元素将被选中。
  • out:可选参数,用于指定输出张量。如果指定了,输出将被写入这个张量中。

返回值:

  • 返回一个一维张量,包含了根据掩码在输入张量中选择的元素。

举个例子,假设我们有一个二维张量A,我们想要选择所有大于5的元素:

import torch

A = torch.tensor([[1, 2, 3],
                  [4, 5, 6],
                  [7, 8, 9]])

mask = A > 5
B = torch.masked_select(A, mask)

print(B)

输出将会是:

tensor([6, 7, 8, 9])

这里,我们首先创建了一个掩码mask,它是一个与A形状相同的布尔张量,其中大于5的元素对应位置为True。然后,我们使用torch.masked_select()函数根据这个掩码从A中选择元素,结果是一个一维张量B,包含了所有被选中的元素。

条件操作

x = torch.tensor([1, 2, 3])
# 如果x中的元素大于1,则保留,否则乘以10
print(torch.where(x > 1, x, x * 10))  

在这里插入图片描述

算术操作

x = torch.tensor([1, 2, 3])
y = torch.tensor([4, 5, 6])
print(x + y)  # 逐元素相加
print(x * y)  # 逐元素相乘
print(torch.dot(x, y))  # 点积

在这里插入图片描述

torch.add()

torch.add() 用于对两个张量进行逐元素的加法操作。这个函数可以将两个形状相同的张量相加,或者将一个标量加到张量的每个元素上

函数的定义如下:

torch.add(input, other, *, alpha=1, out=None) → Tensor

参数解释:

  • input:第一个输入张量。
  • other:第二个输入,可以是一个张量或一个标量。如果是张量,则其形状必须与input相同或可广播为相同形状
  • alpha:一个可选的标量乘数,用于缩放other(默认值为1)。如果other是张量,这相当于先将other乘以alpha,然后再与input相加。
  • out:可选参数,用于指定输出张量。如果指定了,输出将被写入这个张量中。

返回值:

  • 返回一个新的张量,其元素为inputother逐元素相加的结果,即input + alpha * other

举个例子:

import torch

# 两个张量相加
a = torch.tensor([1, 2, 3])
b = torch.tensor([4, 5, 6])
c = torch.add(a, b)
print(c)  # 输出: tensor([5, 7, 9])

# 张量和标量相加
d = torch.add(a, 10)
print(d)  # 输出: tensor([11, 12, 13])

# 使用alpha参数
e = torch.add(a, b, alpha=0.5)
print(e)  # 输出: tensor([3.0, 4.5, 6.0])

在这个例子中,我们展示了如何使用torch.add()来进行张量与张量、张量与标量的加法操作,以及如何使用alpha参数来缩放第二个输入。

autograd

Pytorch 的计算图

PyTorch是一个流行的深度学习框架,它使用计算图(computation graph)来高效地执行和微分操作。计算图是一个用来表示和执行运算的图结构,在PyTorch中,它被称为动态计算图(dynamic computation graph),因为它允许在运行时动态地构建和修改图。

计算图的组成

计算图主要由三种类型的节点组成:

  1. 张量(Tensor)节点:表示数据,例如模型的输入、输出和参数。它们是计算图的叶子节点,不执行任何操作。

  2. 操作(Operation)节点:表示对张量的计算,例如加法、乘法、激活函数等。操作节点将一个或多个张量作为输入,并产生一个或多个张量作为输出。

  3. 变量(Variable)节点:在PyTorch中,torch.autograd.Variable已经被整合到torch.Tensor中,所以现在张量节点本身就可以存储数据、梯度以及计算图中的位置信息。这些张量可以通过设置requires_grad=True来跟踪它们的操作历史,以便进行自动求导。

动态计算图的特点

  • 动态性:PyTorch的计算图是动态的,这意味着图是在运行时逐步构建的。每当执行一个操作时,就会动态地添加一个新节点到图中。这使得PyTorch非常灵活,能够适应复杂的模型和变化的输入结构。

  • 自动微分:PyTorch** 利用计算图来自动计算梯度**。当计算图构建完成后,可以通过反向传播(backpropagation)自动计算出每个张量节点的梯度。这是通过跟踪计算图中的操作并应用链式法则来实现的。

  • 即时执行(Eager Execution):PyTorch的计算图在定义的同时就被执行,这与TensorFlow的静态计算图不同,后者需要先定义整个图,然后启动一个会话来执行图。即时执行使得调试和交互式开发更加容易。

示例

下面是一个简单的例子,展示了如何在PyTorch中构建和使用计算图:

import torch

# 创建两个张量,并设置requires_grad=True以跟踪它们的计算历史
x = torch.tensor([1.0, 2.0, 3.0], requires_grad=True)
y = torch.tensor([4.0, 5.0, 6.0], requires_grad=True)

# 执行一个操作
z = x * y

# 计算z关于x和y的梯度
z.sum().backward()

# 打印梯度
print(x.grad)  # 输出: tensor([4., 5., 6.])
print(y.grad)  # 输出: tensor([1., 2., 3.])

在这个例子中,xy是张量节点,乘法操作创建了一个新的操作节点。当调用z.sum().backward()时,PyTorch会自动计算出z关于xy的梯度,并将它们存储在x.grady.grad中。

z.sum().backward() 为什么需要执行 .sum() 而不是直接 z.backward() 来计算关于 xy 的梯度?

在PyTorch中,backward()函数用于计算梯度,但它要求调用它的张量(即梯度的源)是一个标量(即只有一个元素的张量)。这是因为backward()函数计算的是调用张量相对于图中其他张量的梯度,如果调用张量不是标量,那么就不清楚应该如何计算这些梯度。

在上面的例子中,z是一个包含多个元素的张量,因此不能直接调用z.backward()

为了解决这个问题,我们使用z.sum()z中的所有元素相加得到一个标量,然后再调用backward()来计算梯度。

这样做的效果是计算了z中每个元素关于xy的梯度之和。

简而言之,z.sum().backward()是一种常用的技巧,用于在需要计算梯度时将多元素张量转换为标量。

总结

PyTorch的计算图提供了一个灵活和高效的框架来构建和训练深度学习模型。动态计算图的特性使得PyTorch非常适合研究和开发复杂的模型,而自动微分功能则简化了梯度计算和反向传播的过程。

torch.autograd.backward()

torch.autograd.backward()是PyTorch中的一个核心函数,用于自动计算张量的梯度。这个函数在神经网络的训练过程中非常重要,因为它允许通过反向传播算法自动地更新模型参数。

当你在PyTorch中定义一个计算图(例如,一个神经网络),并通过前向传播得到输出后,你可以使用torch.autograd.backward()来计算相对于某些输入张量的输出的梯度。这些梯度可以用于随后的优化步骤,比如梯度下降,来更新网络的权重。

函数的定义如下:

torch.autograd.backward(tensors, grad_tensors=None, retain_graph=None, create_graph=False, grad_variables=None)

参数解释:

  • tensors:包含要对其进行梯度计算的张量的序列(通常是损失函数的输出)。
  • grad_tensors:与tensors相对应的梯度张量序列。这些张量表示对应于每个tensors元素的梯度。如果为None,则假定为全1的张量。
  • retain_graph:一个布尔值,指示是否保留用于梯度计算的计算图。如果False,计算图在梯度计算后将被释放。这对于节省内存很有用,但如果你需要再次进行反向传播,则需要将其设置为True
  • create_graph:一个布尔值,指示是否创建用于计算高阶导数的计算图。
  • grad_variables:(不推荐使用,已废弃)旧版本的PyTorch使用的参数,现在应使用grad_tensors

举个例子,假设我们有一个简单的神经网络,我们想要计算损失函数相对于网络参数的梯度:

import torch

# 创建一个简单的神经网络
input = torch.randn(1, 3, requires_grad=True)
target = torch.randn(1, 3)
weights = torch.randn(3, 3, requires_grad=True)

# 前向传播
output = torch.matmul(input, weights)
loss = torch.mean((output - target) ** 2)

# 反向传播
loss.backward()

# 查看梯度
print(weights.grad)

在这个例子中,我们首先定义了一个具有3个输入和3个输出节点的简单线性层。然后我们计算了均方误差损失,并通过调用loss.backward()来自动计算损失相对于权重的梯度。最后,我们打印出权重的梯度,这些梯度可以用于优化步骤。

torch.autograd.grad()

torch.autograd.grad() 是 PyTorch 中的一个函数,用于计算和返回指定张量(通常是模型参数)相对于一组输出张量(通常是损失函数)的梯度。这个函数在自动微分中非常重要,特别是在需要更细粒度的梯度控制时。

函数的定义如下:

torch.autograd.grad(outputs, inputs, grad_outputs=None, retain_graph=None, create_graph=False, only_inputs=True, allow_unused=False)

参数解释:

  • outputs:包含一个或多个张量的序列,通常是计算图中的输出张量(例如,损失函数的输出)。
  • inputs:包含一个或多个需要计算梯度的张量的序列(例如,模型参数)。
  • grad_outputs:与outputs相对应的梯度张量序列。如果为None,则假定为全1的张量。这个参数通常用于多任务学习或者当输出是向量而不是标量时。
  • retain_graph:一个布尔值,指示是否保留用于梯度计算的计算图。如果False,计算图在梯度计算后将被释放。这对于节省内存很有用,但如果你需要再次进行反向传播,则需要将其设置为True
  • create_graph:一个布尔值,指示是否创建用于计算高阶导数的计算图。
  • only_inputs:(不推荐使用,已废弃)旧版本的PyTorch使用的参数。
  • allow_unused:一个布尔值,指示是否允许inputs中的某些张量在outputs的计算中未被使用。如果设置为False(默认值),那么当inputs中的任何张量在计算outputs时未被使用时,将抛出一个错误。

返回值:

  • 返回一个与inputs相对应的梯度张量的元组。

举个例子,假设我们有一个简单的神经网络,我们想要计算损失函数相对于网络参数的梯度:

import torch

# 创建一个简单的神经网络
input = torch.randn(1, 3, requires_grad=True)
target = torch.randn(1, 3)
weights = torch.randn(3, 3, requires_grad=True)

# 前向传播
output = torch.matmul(input, weights)
loss = torch.mean((output - target) ** 2)

# 使用torch.autograd.grad计算梯度
grads = torch.autograd.grad(loss, weights)

# 查看梯度
print(grads[0])

在这个例子中,我们首先定义了一个具有3个输入和3个输出节点的简单线性层。然后我们计算了均方误差损失,并通过调用torch.autograd.grad()来计算损失相对于权重的梯度。最后,我们打印出权重的梯度。与torch.autograd.backward()不同,torch.autograd.grad()不会自动将计算的梯度累加到.grad属性中,而是直接返回梯度张量。

数据加载

torch.utils.data.Dataset

torch.utils.data.Dataset 是 PyTorch 中用于表示数据集的抽象类。

所有自定义数据集都应该继承 Dataset 并覆盖以下方法:

class torch.utils.data.Dataset:
    def __init__(self):
        pass

    def __getitem__(self, index):
        raise NotImplementedError

    def __len__(self):
        raise NotImplementedError

这里:

  • __init__(self):是一个初始化函数,你可以在这里初始化数据集的相关参数。
  • __getitem__(self, index):是一个必须要实现的方法,它用于获取数据集中的一个样本。index 是样本的索引。
  • __len__(self):也是一个必须要实现的方法,它返回数据集中样本的总数。

自定义数据集的例子:

class CustomDataset(torch.utils.data.Dataset):
    def __init__(self, data, labels):
        self.data = data
        self.labels = labels

    def __getitem__(self, index):
        return self.data[index], self.labels[index]

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

在这个例子中,CustomDataset 继承了 torch.utils.data.Dataset,并实现了 __init__, __getitem__, 和 __len__ 方法。这样你就可以创建自定义数据集的实例,并将其用于 DataLoader 以便于批量加载和处理数据。

torch.utils.data.Dataloader

torch.utils.data.DataLoader 在 PyTorch 中用于加载数据集的类。它的签名如下:

torch.utils.data.DataLoader(
    dataset,
    batch_size=1,
    shuffle=False,
    sampler=None,
    batch_sampler=None,
    num_workers=0,
    collate_fn=None,
    pin_memory=False,
    drop_last=False,
    timeout=0,
    worker_init_fn=None,
    multiprocessing_context=None,
    generator=None,
    prefetch_factor=2,
    persistent_workers=False,
    pin_memory_device='',
)

其中:

  • dataset (Dataset): 要加载的数据集。
  • batch_size (int, optional): 每个批次的样本数。默认为 1。
  • shuffle (bool, optional): 是否在每个 epoch 开始时打乱数据。默认为 False。
  • sampler (Sampler, optional): 定义从数据集中抽取样本的策略。如果指定,则 shuffle 必须为 False。
  • batch_sampler (Sampler, optional): 与 sampler 类似,但一次返回一个批次的索引。
  • num_workers (int, optional): 用于数据加载的子进程数。默认为 0,表示数据将在主进程中加载。
  • collate_fn (callable, optional): 将一批样本合并成一个批次数据的函数。
  • pin_memory (bool, optional): 如果为 True,数据加载器会在返回前将数据复制到 CUDA 固定内存中。
  • drop_last (bool, optional): 如果数据集大小不能被批量大小整除,则设置为 True 以丢弃最后一个不完整的批次。
  • timeout (numeric, optional): 正在收集一个批次数据的工作进程的超时值。默认为 0。
  • worker_init_fn (callable, optional): 每个工作进程启动时调用的函数。
  • multiprocessing_context (str or multiprocessing.context, optional): 用于启动工作进程的上下文。默认为 None。
  • generator (torch.Generator, optional): 控制采样的生成器。
  • prefetch_factor (int, optional): 每个工作进程预取的批次数。默认为 2。
  • persistent_workers (bool, optional): 如果为 True,则工作进程在数据集迭代结束后不会关闭。默认为 False。
  • pin_memory_device (str, optional): 指定固定内存所在的设备。默认为空字符串,表示使用当前设备。

这个类的实例是一个可迭代的对象,可以在 for 循环中使用,每次迭代返回一个批次的数据

torch.utils.data.DataLoader 是 PyTorch 中的一个类,用于加载和处理数据集。它提供了一种方便的方式来迭代地加载数据,使得数据加载更加高效和易于管理。以下是 DataLoader 的一些主要特点和使用方法:

主要特点

  1. 批量加载DataLoader 可以一次性加载多个数据项,形成一个批次(batch)。这有助于加快训练速度,因为可以同时对多个数据项进行处理。

  2. 数据打乱:在训练过程中,为了增加模型的泛化能力,通常需要打乱数据。DataLoader 提供了一个选项来自动打乱数据。

  3. 多进程加载DataLoader 支持使用多个进程来并行加载数据,这可以显著提高数据加载的速度,特别是在处理大型数据集时。

  4. 自定义数据处理:通过与 torch.utils.data.Dataset 配合使用,DataLoader 允许用户自定义数据加载和处理的方式。

基本使用方法

假设我们已经有一个继承自 torch.utils.data.Dataset 的数据集类 MyDataset

from torch.utils.data import DataLoader, Dataset

class MyDataset(Dataset):
    def __init__(self):
        # 初始化数据集,例如加载数据文件
        pass

    def __len__(self):
        # 返回数据集中的数据项数量
        return len(self.data)

    def __getitem__(self, index):
        # 根据索引返回一个数据项
        return self.data[index]

接下来,我们可以使用 DataLoader 来加载这个数据集:

# 创建数据集实例
dataset = MyDataset()

# 创建 DataLoader 实例
dataloader = DataLoader(dataset, batch_size=4, shuffle=True, num_workers=2)

# 迭代地加载数据
for batch in dataloader:
    # 处理每个批次的数据
    pass

在这个例子中,DataLoader 会每次加载 4 个数据项,数据会被随机打乱,并且使用 2 个进程来并行加载数据。

参数详解

  • dataset:一个继承自 torch.utils.data.Dataset 的数据集对象。
  • batch_size:每个批次加载的数据项数量。
  • shuffle:是否在每个 epoch 开始时打乱数据。
  • num_workers:用于数据加载的进程数量。设置为 0 表示在主进程中加载数据。
  • collate_fn:用于将多个数据项合并成一个批次的函数。默认情况下,会将数据项堆叠成一个 Tensor。
  • drop_last:如果数据集大小不能被批次大小整除,设置为 True 会丢弃最后一个不完整的批次,否则会保留。

图像预处理和增强

torchvision 的主要模块

torchvision是PyTorch的一个附加库,专门用于计算机视觉任务。它提供了一些常用的数据集、预训练模型、图像转换工具等。以下是torchvision中的一些主要模块:

  1. torchvision.datasets: 这个模块提供了许多常见的数据集,如MNIST、CIFAR-10/100、ImageNet、COCO等。这些数据集可以用来快速构建和测试模型。

  2. torchvision.models: 这个模块包含了许多预训练的模型,如ResNet、VGG、Inception、MobileNet等。这些模型可以用于迁移学习或直接用于预测。

  3. torchvision.transforms: 这个模块提供了一系列图像转换操作,如裁剪、旋转、翻转、归一化等。这些操作可以用来预处理图像数据,以便用于模型训练或推理。

  4. torchvision.utils: 这个模块提供了一些实用工具,如将张量转换为图像、保存图像等。

  5. torchvision.ops: 这个模块提供了一些用于对象检测和分割任务的操作,如非最大抑制(NMS)、RoI Align等。

torchvision.transforms

torchvision.transforms 用于对图像进行预处理和数据增强。

这个模块提供了许多常用的图像变换方法,可以帮助你在训练深度学习模型之前准备和处理数据。以下是一些常用的 torchvision.transforms 方法:

  1. Resize:调整图像大小。

    transforms.Resize((height, width))
    
  2. CenterCrop:从图像中心裁剪出指定大小的图像。

    transforms.CenterCrop(size)
    
  3. RandomCrop:随机裁剪图像的一部分。

    transforms.RandomCrop(size)
    
  4. ToTensor:将 PIL 图像或 NumPy 数组转换为张量,并且将像素值从 [0, 255] 缩放到 [0.0, 1.0]。

    transforms.ToTensor()
    
  5. Normalize:标准化张量图像,给定均值和标准差。

    transforms.Normalize(mean, std)
    
  6. RandomHorizontalFlip:以给定的概率随机水平翻转图像。

    transforms.RandomHorizontalFlip(p=0.5)
    
  7. RandomVerticalFlip:以给定的概率随机垂直翻转图像。

    transforms.RandomVerticalFlip(p=0.5)
    
  8. ColorJitter:随机改变图像的亮度、对比度、饱和度和色调。

    transforms.ColorJitter(brightness=0, contrast=0, saturation=0, hue=0)
    
  9. Grayscale:将图像转换为灰度图像。

    transforms.Grayscale(num_output_channels=1)
    
  10. Compose:将多个变换方法组合在一起。

    transforms.Compose([
        transforms.Resize((height, width)),
        transforms.ToTensor(),
        transforms.Normalize(mean, std)
    ])
    

这些变换方法可以单独使用,也可以通过 Compose 组合在一起形成一个变换序列,以便于对图像进行多步骤的处理。

网络模型构建的两个要素

在PyTorch中,构建网络模型通常涉及两个主要步骤:构建子模块和拼接子模块。以下是这两个步骤的详细解释:

1. 构建子模块

子模块是网络模型的基本构建块,通常代表了模型中的一个层或一组相关的层。在PyTorch中,子模块通常通过继承torch.nn.Module类来创建。

每个子模块需要定义其构造函数(__init__)和前向传播函数(forward

示例:

import torch
import torch.nn as nn

class ConvBlock(nn.Module):
    def __init__(self, in_channels, out_channels, kernel_size, stride, padding):
        super(ConvBlock, self).__init__()
        self.conv = nn.Conv2d(in_channels, out_channels, kernel_size, stride, padding)
        self.relu = nn.ReLU()

    def forward(self, x):
        x = self.conv(x)
        x = self.relu(x)
        return x

在这个示例中,ConvBlock是一个子模块,它包含一个卷积层(nn.Conv2d)和一个激活层(nn.ReLU)。__init__函数用于初始化这些层,而forward函数定义了数据如何通过这些层进行前向传播。

2. 拼接子模块

一旦定义了所需的子模块,就可以将它们拼接起来构建完整的网络模型。这通常在一个继承自nn.Module的类中完成,其中子模块作为该类的属性被初始化,并在forward方法中按顺序连接。

示例:

class MyModel(nn.Module):
    def __init__(self):
        super(MyModel, self).__init__()
        self.block1 = ConvBlock(3, 64, 3, 1, 1)
        self.block2 = ConvBlock(64, 128, 3, 1, 1)
        self.fc = nn.Linear(128, 10)

    def forward(self, x):
        x = self.block1(x)
        x = self.block2(x)
        x = torch.flatten(x, 1)
        x = self.fc(x)
        return x

在这个示例中,MyModel是一个完整的网络模型,它通过拼接两个ConvBlock子模块和一个全连接层(nn.Linear)来构建。forward方法定义了数据如何通过这些层进行前向传播。

nn.Sequential

在PyTorch中,nn.Sequential是一个容器模块,用于按顺序堆叠不同的子模块(如层和激活函数)。

nn.Sequential 容器默认情况下只能处理单个张量(tensor)作为输入

使用nn.Sequential可以简化模型的构建过程,使代码更加简洁和易于理解。下面是nn.Sequential的一些关键特点和使用方式:

关键特点

  1. 顺序性nn.Sequential中的模块按照它们被添加的顺序进行排列,并且数据会依次通过这些模块。
  2. 自动化前向传播:当输入数据传递给nn.Sequential时,它会自动进行前向传播,依次通过所有子模块,无需手动编写forward方法
  3. 灵活性nn.Sequential可以包含各种类型的子模块,如卷积层、池化层、全连接层、激活函数等。

使用方式

基本用法
import torch.nn as nn

# 定义一个简单的序列模型
model = nn.Sequential(
    nn.Linear(in_features=10, out_features=20),
    nn.ReLU(),
    nn.Linear(in_features=20, out_features=5)
)

# 查看模型结构
print(model)
添加子模块

可以通过add_module方法向nn.Sequential中添加子模块,并为其指定一个名称:

model = nn.Sequential()
model.add_module('linear1', nn.Linear(in_features=10, out_features=20))
model.add_module('relu', nn.ReLU())
model.add_module('linear2', nn.Linear(in_features=20, out_features=5))

print(model)
访问子模块

可以通过索引或名称访问nn.Sequential中的子模块:

# 通过索引访问
first_layer = model[0]

# 通过名称访问
relu_layer = model.relu

注意事项

  • nn.Sequential适用于简单的前向传播模型。如果模型具有复杂的控制流(如残差连接、多输入多输出等),则需要自定义模型类并实现forward方法。
  • 所有添加到nn.Sequential中的模块都应该是nn.Module的子类。

torch.nn

Convolution Layers

nn.Conv2d

nn.Conv2d是PyTorch中用于实现二维卷积操作的类,其完整签名如下:

torch.nn.Conv2d(in_channels, out_channels, kernel_size, stride=1,
 padding=0, dilation=1, groups=1, bias=True, padding_mode='zeros')

参数解释:

  • in_channels (int):输入图像的通道数。
  • out_channels (int):卷积产生的通道数。
  • kernel_size (int or tuple):卷积核的大小,可以是一个整数或者一个 (height, width) 的元组。
  • stride (int or tuple, optional):卷积步长,可以是一个整数或者一个 (height, width) 的元组。默认为 1。
  • padding (int or tuple, optional):输入的每一条边补充0的层数,可以是一个整数或者一个 (height, width) 的元组。默认为 0。
  • dilation (int or tuple, optional):卷积核元素之间的间距,可以是一个整数或者一个 (height, width) 的元组。默认为 1。
  • groups (int, optional):分组卷积的组数,默认为 1。
  • bias (bool, optional):是否添加偏置项,默认为 True。
  • padding_mode (string, optional):填充模式,可以是 ‘zeros’, ‘reflect’, ‘replicate’ 或 ‘circular’。默认为 ‘zeros’。

示例代码:

import torch
import torch.nn as nn

# 创建一个输入张量,假设是一个批量大小为1,通道数为3,高度和宽度都为5的图像
input = torch.randn(1, 3, 5, 5)

# 创建一个Conv2d层
conv = nn.Conv2d(in_channels=3, out_channels=6, kernel_size=3, stride=1, padding=1)

# 应用卷积层
output = conv(input)

print("Input shape:", input.shape)
print("Output shape:", output.shape)

在这个示例中,我们创建了一个输入张量,其形状为 (1, 3, 5, 5),表示一个批量大小为 1、通道数为 3、高度和宽度都为 5 的图像。然后我们创建了一个 Conv2d 层,指定输入通道数为 3、输出通道数为 6、卷积核大小为 3、步长为 1、填充为 1。应用这个卷积层后,我们得到了输出张量,其形状为 (1, 6, 5, 5),表示一个批量大小为 1、通道数为 6、高度和宽度都为 5 的图像。由于我们使用了填充,所以输出的高度和宽度与输入相同。

对于二维卷积操作(nn.Conv2d()),输出特征图(feature maps)的尺寸可以通过以下公式计算:

对于每个空间维度(高度和宽度),输出尺寸(H_outW_out)可以分别按照下面的公式计算:

H out = ⌊ H in + 2 × padding − dilation × ( kernel_size − 1 ) − 1 stride + 1 ⌋ H_{\text{out}} = \left\lfloor \frac{H_{\text{in}} + 2 \times \text{padding} - \text{dilation} \times (\text{kernel\_size} - 1) - 1}{\text{stride}} + 1 \right\rfloor Hout=strideHin+2×paddingdilation×(kernel_size1)1+1

W out = ⌊ W in + 2 × padding − dilation × ( kernel_size − 1 ) − 1 stride + 1 ⌋ W_{\text{out}} = \left\lfloor \frac{W_{\text{in}} + 2 \times \text{padding} - \text{dilation} \times (\text{kernel\_size} - 1) - 1}{\text{stride}} + 1 \right\rfloor Wout=strideWin+2×paddingdilation×(kernel_size1)1+1

其中:

  • H_inW_in 分别是输入特征图的高度和宽度。
  • padding 是在输入特征图边缘填充的大小(假设高度和宽度方向的填充相同)。
  • dilation 是卷积核中元素之间的间距。
  • kernel_size 是卷积核的大小(假设高度和宽度相同)。
  • stride 是卷积的步长(假设高度和宽度方向的步长相同)。

注意:以上公式假设高度和宽度方向的参数都是相同的。如果高度和宽度方向的参数不同(例如,不同的填充、步长或卷积核大小),则需要分别为高度和宽度计算这些公式。

nn.ConvTranspose2d

nn.ConvTranspose2d是PyTorch中用于实现二维转置卷积(也称为反卷积)操作的类,转置卷积通常用于实现卷积神经网络中的上采样操作,其完整签名如下:

torch.nn.ConvTranspose2d(in_channels, out_channels, kernel_size, 
stride=1, padding=0, output_padding=0, groups=1, bias=True, 
dilation=1, padding_mode='zeros')

参数解释:

  • in_channels (int):输入图像的通道数。
  • out_channels (int):转置卷积产生的通道数。
  • kernel_size (int or tuple):卷积核的大小,可以是一个整数或者一个 (height, width) 的元组。
  • stride (int or tuple, optional):卷积步长,可以是一个整数或者一个 (height, width) 的元组。默认为 1。
  • padding (int or tuple, optional):输入的每一条边补充0的层数,可以是一个整数或者一个 (height, width) 的元组。默认为 0。
  • output_padding (int or tuple, optional):输出的额外填充,可以是一个整数或者一个 (height, width) 的元组。默认为 0。
  • groups (int, optional):分组卷积的组数,默认为 1。
  • bias (bool, optional):是否添加偏置项,默认为 True。
  • dilation (int or tuple, optional):卷积核元素之间的间距,可以是一个整数或者一个 (height, width) 的元组。默认为 1。
  • padding_mode (string, optional):填充模式,可以是 ‘zeros’, ‘reflect’, ‘replicate’ 或 ‘circular’。默认为 ‘zeros’。

示例代码:

import torch
import torch.nn as nn

# 创建一个输入张量,假设是一个批量大小为1,通道数为6,高度和宽度都为5的图像
input = torch.randn(1, 6, 5, 5)

# 创建一个ConvTranspose2d层
conv_transpose = nn.ConvTranspose2d(in_channels=6, out_channels=3, kernel_size=3, stride=1, padding=1)

# 应用转置卷积层
output = conv_transpose(input)

print("Input shape:", input.shape) # shape = [1,6,5,5]
print("Output shape:", output.shape)# shape = [1,3,5,5]

在这个示例中,我们创建了一个输入张量,其形状为 (1, 6, 5, 5),表示一个批量大小为 1、通道数为 6、高度和宽度都为 5 的图像。然后我们创建了一个 ConvTranspose2d 层,指定输入通道数为 6、输出通道数为 3、卷积核大小为 3、步长为 1、填充为 1。应用这个转置卷积层后,我们得到了输出张量,其形状为 (1, 3, 5, 5),表示一个批量大小为 1、通道数为 3、高度和宽度都为 5 的图像。由于我们使用了填充,所以输出的高度和宽度与输入相同。转置卷积通常用于实现卷积神经网络中的上采样操作。

转置卷积实现卷积神经网络中的上采样操作的示例

下面是一个使用转置卷积(nn.ConvTranspose2d)实现卷积神经网络中的上采样操作的示例。在这个示例中,我们将使用一个简单的卷积层来降采样图像,然后使用转置卷积层来上采样图像,以恢复到原始尺寸。

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

# 定义一个简单的卷积神经网络
class SimpleCNN(nn.Module):
    def __init__(self):
        super(SimpleCNN, self).__init__()
        # 定义一个卷积层,用于降采样
        self.conv = nn.Conv2d(in_channels=1, out_channels=2, \\
        kernel_size=3, stride=2, padding=1)
        # 定义一个转置卷积层,用于上采样
        self.conv_transpose = nn.ConvTranspose2d(in_channels=2, \\
        out_channels=1, kernel_size=3, stride=2, padding=1, \\
        output_padding=1)

    def forward(self, x):
        # 通过卷积层降采样
        x = F.relu(self.conv(x)) # shape = [1, 2, 4, 4]
        # 通过转置卷积层上采样
        x = F.relu(self.conv_transpose(x)) # shape = [1, 1, 8, 8]
        return x

# 创建一个输入张量,假设是一个批量大小为1,通道数为1,高度和宽度都为8的图像
input = torch.randn(1, 1, 8, 8)

# 创建模型并应用
model = SimpleCNN()
output = model(input)

print("Input shape:", input.shape) # shape = [1, 1, 8, 8]
print("Output shape:", output.shape) # shape = [1, 1, 8, 8]

在这个示例中,我们首先定义了一个简单的卷积神经网络 SimpleCNN,它包含一个卷积层和一个转置卷积层。卷积层的作用是对输入图像进行降采样,而转置卷积层则用于将降采样后的图像上采样回原始尺寸。

我们创建了一个形状为 (1, 1, 8, 8) 的输入张量,表示一个批量大小为 1、通道数为 1、高度和宽度都为 8 的图像。通过模型前向传播后,输出张量的形状仍然是 (1, 1, 8, 8),这说明经过降采样和上采样后,图像的尺寸被恢复到了原始大小。这种上采样方法在一些生成模型(如生成对抗网络)和分割模型(如U-Net)中非常常见。

Pooling Layer

什么是池化层?
在 PyTorch 中,池化(Pooling)是一种常用于卷积神经网络(CNN)中的下采样技术,用于减少特征图的尺寸降低计算复杂度,同时保留重要的特征信息

池化操作主要有两种类型:平均池化(Average Pooling)和最大池化(Max Pooling)。

在这里插入图片描述

平均池化 (Average Pooling)

平均池化是一种池化操作,它将输入特征图划分为不同的区域,并计算每个区域的平均值作为输出特征图的相应元素。这种操作有助于平滑特征图,并且减少了特征图的尺寸。

nn.AvgPool2d()是PyTorch中的一个函数,用于在卷积神经网络中进行平均池化操作。它的签名如下:

torch.nn.AvgPool2d(kernel_size, stride=None, padding=0, 
ceil_mode=False, count_include_pad=True, divisor_override=None)

参数解释:

  • kernel_size (int or tuple): 池化窗口的大小。
  • stride (int or tuple, optional): 池化操作的步长。默认值为kernel_size
  • padding (int or tuple, optional): 输入数据的边缘填充。默认为0。
  • ceil_mode (bool, optional): 如果设置为True,则会使用向上取整的方式计算输出大小。默认为False。
  • count_include_pad (bool, optional): 如果设置为True,则在计算平均值时会包括填充的零值。默认为True。
  • divisor_override (int, optional): 如果指定,它将用作除数,否则使用池化区域的元素数作为除数。

示例:

import torch
import torch.nn as nn

# 定义输入数据
input = torch.tensor([[[[1, 2, 3, 4],
                         [5, 6, 7, 8],
                         [9, 10, 11, 12],
                         [13, 14, 15, 16]]]], dtype=torch.float32)

# 定义平均池化层
avg_pool = nn.AvgPool2d(kernel_size=2, stride=2)

# 应用平均池化
output = avg_pool(input)

print("Input:")
print(input)
print("Output:")
print(output)

输出:

Input:
tensor([[[[ 1.,  2.,  3.,  4.],
          [ 5.,  6.,  7.,  8.],
          [ 9., 10., 11., 12.],
          [13., 14., 15., 16.]]]])
Output:
tensor([[[[ 3.5000,  5.5000],
          [11.5000, 13.5000]]]])

在这个示例中,我们使用了一个2x2的池化窗口和步长为2来对4x4的输入数据进行平均池化操作。结果是一个2x2的输出张量,其中包含了每个2x2区域内的平均值。

最大池化 (Max Pooling)

最大池化是另一种池化操作,它将输入特征图划分为不同的区域,并从每个区域中选取最大值作为输出特征图的相应元素。这种操作有助于保留特征图中的显著特征,并减少了特征图的尺寸。

nn.MaxPool2d()是PyTorch中的一个函数,用于在卷积神经网络中进行最大池化操作。它的签名如下:

torch.nn.MaxPool2d(kernel_size, stride=None, padding=0, dilation=1, 
return_indices=False, ceil_mode=False)

参数解释:

  • kernel_size (int or tuple): 池化窗口的大小。
  • stride (int or tuple, optional): 池化操作的步长。默认值为kernel_size
  • padding (int or tuple, optional): 输入数据的边缘填充。默认为0。
  • dilation (int or tuple, optional): 控制池化窗口中元素的间距。默认为1。
  • return_indices (bool, optional): 如果设置为True,则会返回池化操作中最大值的索引。默认为False。
  • ceil_mode (bool, optional): 如果设置为True,则会使用向上取整的方式计算输出大小。默认为False。

示例:

import torch
import torch.nn as nn

# 定义输入数据
input = torch.tensor([[[[1, 2, 3, 4],
                         [5, 6, 7, 8],
                         [9, 10, 11, 12],
                         [13, 14, 15, 16]]]], dtype=torch.float32)

# 定义最大池化层
max_pool = nn.MaxPool2d(kernel_size=2, stride=2)

# 应用最大池化
output = max_pool(input)

print("Input:")
print(input)
print("Output:")
print(output)

输出:

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

在这个示例中,我们使用了一个2x2的池化窗口和步长为2来对4x4的输入数据进行最大池化操作。结果是一个2x2的输出张量,其中包含了每个2x2区域内的最大值。

nn.MaxUnpool2d()

nn.MaxUnpool2d()是PyTorch中的一个函数,用于执行最大池化的逆操作,即最大反池化。它的签名如下:

torch.nn.MaxUnpool2d(kernel_size, stride=None, padding=0)

参数解释:

  • kernel_size (int or tuple): 反池化窗口的大小。
  • stride (int or tuple, optional): 反池化操作的步长。默认值为kernel_size
  • padding (int or tuple, optional): 输出数据的边缘填充。默认为0。

示例:

import torch
import torch.nn as nn

# 定义输入数据和索引,索引从0开始,从左往右,从上往下
input = torch.tensor([[[[ 6.,  8.],
                        [14., 16.]]]], dtype=torch.float32)
indices = torch.tensor([[[[ 5,  7],
                          [13, 15]]]], dtype=torch.int64)

# 定义最大反池化层
max_unpool = nn.MaxUnpool2d(kernel_size=2, stride=2)

# 应用最大反池化
output = max_unpool(input, indices)

print("Input:")
print(input)
print("Indices:")
print(indices)
print("Output:")
print(output)

输出:

Input:
tensor([[[[ 6.,  8.],
          [14., 16.]]]])
Indices:
tensor([[[[ 5,  7],
          [13, 15]]]])
Output:
tensor([[[[ 0.,  0.,  0.,  0.],
          [ 0.,  6.,  0.,  8.],
          [ 0.,  0.,  0.,  0.],
          [ 0., 14.,  0., 16.]]]])

在这个示例中,我们使用了一个 2 × 2 2\times2 2×2 的反池化窗口和步长为2来对2x2的输入数据进行最大反池化操作。indices参数是在前向最大池化过程中获得的,它记录了每个池化区域中最大值的位置。输出是一个 4 × 4 4\times4 4×4 的张量,其中只有对应于indices中记录的位置的元素被设置为输入张量中的相应值,其他位置被填充为零。这样,最大反池化操作实现了最大池化的逆过程。

Linear Layer

nn.Linear()

nn.Linear() 用于实现一个全连接(线性)层。它的签名如下:

torch.nn.Linear(in_features, out_features, bias=True)

参数解释:

  • in_features (int): 输入特征的数量。
  • out_features (int): 输出特征的数量。
  • bias (bool, optional): 如果设置为True,则会添加一个可学习的偏置向量到输出。默认为True

示例:

import torch
import torch.nn as nn

# 定义输入数据
input = torch.tensor([[1.0, 2.0, 3.0],
                      [4.0, 5.0, 6.0]])

# 定义全连接层
linear = nn.Linear(in_features=3, out_features=2)

# 应用全连接层
output = linear(input)

print("Input:")
print(input)
print("Output:")
print(output)

输出(示例):

Input:
tensor([[1., 2., 3.],
        [4., 5., 6.]])
Output:
tensor([[ 0.1422, -1.3738],
        [ 0.9394, -2.5841]], grad_fn=<AddmmBackward0>)

在这个示例中,我们定义了一个全连接层,其输入特征数量为3,输出特征数量为2。当输入数据通过这个全连接层时,它会应用一个线性变换(加权和加偏置)来生成输出特征。输出的形状取决于out_features参数,每个输出特征是输入特征的加权和,权重由全连接层内部的参数决定。由于这些参数是可学习的,因此在训练过程中,全连接层可以调整它们以适应特定的任务。

Activate Layer

nn.Sigmoid()

在这里插入图片描述

nn.Sigmoid() 用于实现Sigmoid激活函数。它的签名如下:

torch.nn.Sigmoid()

Sigmoid激活函数的数学表达式为:

σ ( x ) = 1 1 + e − x \sigma(x) = \frac{1}{1 + e^{-x}} σ(x)=1+ex1

其中, x x x是输入值。

示例:

import torch
import torch.nn as nn

# 定义输入数据
input = torch.tensor([[-1.0, 0.0, 1.0],
                      [2.0, -2.0, 3.0]])

# 定义Sigmoid激活层
sigmoid = nn.Sigmoid()

# 应用Sigmoid激活函数
output = sigmoid(input)

print("Input:")
print(input)
print("Output:")
print(output)

输出:

Input:
tensor([[-1.,  0.,  1.],
        [ 2., -2.,  3.]])
Output:
tensor([[0.2689, 0.5000, 0.7311],
        [0.8808, 0.1192, 0.9526]])

在这个示例中,我们定义了一个Sigmoid激活层,并将其应用于输入数据。Sigmoid函数将每个输入值映射到(0, 1)区间内,这使得它非常适合用于二分类问题中,作为输出层的激活函数,输出值可以被解释为概率。

nn.Tanh()

nn.Tanh() 用于实现双曲正切(Tanh)激活函数。它没有参数,因为Tanh是一个固定的非线性函数。Tanh函数的数学表达式为:

tanh ⁡ ( x ) = e x − e − x e x + e − x \tanh(x) = \frac{e^x - e^{-x}}{e^x + e^{-x}} tanh(x)=ex+exexex

其中, x x x 是输入值。 tanh ⁡ ( ) \tanh() tanh() 函数的输出范围在 ( − 1 , 1 ) (-1, 1) (1,1) 之间,它可以将输入值压缩到这个范围内。

示例:

import torch
import torch.nn as nn

# 定义输入数据
input = torch.tensor([-2.0, -1.0, 0.0, 1.0, 2.0])

# 定义Tanh激活层
tanh = nn.Tanh()

# 应用Tanh激活函数
output = tanh(input)

print("Input:")
print(input)
print("Output:")
print(output)

输出:

Input:
tensor([-2., -1.,  0.,  1.,  2.])
Output:
tensor([-0.9640, -0.7616,  0.0000,  0.7616,  0.9640])

nn.ReLU()

nn.ReLU() 用于实现修正线性单元(ReLU)激活函数。ReLU函数的数学表达式为:

ReLU ( x ) = max ⁡ ( 0 , x ) \text{ReLU}(x) = \max(0, x) ReLU(x)=max(0,x)

其中, x x x 是输入值。 ReLU \text{ReLU} ReLU 函数将所有负输入值置为 0 0 0,而保留所有正输入值不变。这种非线性激活函数在深度学习中非常流行,因为它可以加速训练并减少梯度消失问题

示例:

import torch
import torch.nn as nn

# 定义输入数据
input = torch.tensor([-2.0, -1.0, 0.0, 1.0, 2.0])

# 定义ReLU激活层
relu = nn.ReLU()

# 应用ReLU激活函数
output = relu(input)

print("Input:")
print(input)
print("Output:")
print(output)

输出:

Input:
tensor([-2., -1.,  0.,  1.,  2.])
Output:
tensor([0., 0., 0., 1., 2.])

nn.LeakyReLU()

nn.LeakyReLU() 用于实现泄露修正线性单元(Leaky ReLU)激活函数。

LeakyReLU \text{LeakyReLU} LeakyReLU ReLU \text{ReLU} ReLU 的一个变种,允许小的负梯度通过,以解决 ReLU \text{ReLU} ReLU 的"死亡ReLU"问题。

LeakyReLU \text{LeakyReLU} LeakyReLU 函数的数学表达式为:

LeakyReLU ( x ) = { x if  x > 0 α x if  x ≤ 0 \text{LeakyReLU}(x) = \begin{cases} x & \text{if } x > 0 \\ \alpha x & \text{if } x \leq 0 \end{cases} LeakyReLU(x)={xαxif x>0if x0

其中, α \alpha α 是一个小的正常数(通常接近于0),用于控制负输入值的梯度。

示例:

import torch
import torch.nn as nn

# 定义输入数据
input = torch.tensor([-2.0, -1.0, 0.0, 1.0, 2.0])

# 定义LeakyReLU激活层
leaky_relu = nn.LeakyReLU(negative_slope=0.01)

# 应用LeakyReLU激活函数
output = leaky_relu(input)

print("Input:")
print(input)
print("Output:")
print(output)

输出:

Input:
tensor([-2., -1.,  0.,  1.,  2.])
Output:
tensor([-0.0200, -0.0100,  0.0000,  1.0000,  2.0000])

在这个示例中,我们设置了negative_slope参数为0.01,这意味着所有负输入值将被乘以0.01。

nn.functional.softmax()

PyTorch 中的 softmax() 函数是一种激活函数,常用于多分类问题中。它将输入的张量(tensor)转换为概率分布,使得输出的每个元素的值介于 0 和 1 之间,并且所有元素的和为 1。这使得 softmax() 成为处理多分类问题的理想选择,特别是在神经网络的输出层中。

函数签名:

torch.nn.functional.softmax(input, dim=None, _stacklevel=3, 
dtype=None)

参数解释:

  • input (Tensor): 输入张量。
  • dim (int): 指定进行 softmax 计算的维度。例如,在二维张量中,dim=0 表示沿着每一列计算 softmax,而 dim=1 表示沿着每一行计算 softmax。
  • _stacklevel (int, 可选): 仅用于内部调试,通常可以忽略。
  • dtype (torch.dtype, 可选): 指定输出张量的数据类型。如果为 None,则使用输入张量的数据类型。

返回值:

  • 返回一个与输入张量形状相同的张量,其中包含了经过 softmax 转换的概率值。

使用示例:

import torch
import torch.nn.functional as F

# 创建一个二维张量,模拟两个样本的三个类别的得分
logits = torch.tensor([[1.0, 2.0, 3.0], [1.0, 2.0, 3.0]])

# 沿着每一行(dim=1)应用 softmax,将得分转换为概率
probabilities = F.softmax(logits, dim=1)

print(probabilities)

输出结果为每个样本对应的三个类别的概率值,且每一行的概率和为 1。

在这里插入图片描述

权重初始化

为什么需要好的权重初始化方法?

  • 梯度消失:反向传播过程中需对激活函数进行求导,如果导数值 < 1 <1 <1,那么随着网络层数的增加,越接近输入层,这些层的梯度更新信息会朝着指数衰减的方式减少,即梯度消失。梯度消失时,越靠近输入层的参数 w w w 越是几乎纹丝不动。

  • 梯度爆炸:如果导数值 > 1 >1 >1,那么随着网络层数的增加,梯度更新将会朝着指数爆炸的方式增加。梯度爆炸时,越是靠近输入层的参数 w w w 越难以收敛。

什么是均匀分布?

均匀分布是概率论和统计学中的一种连续概率分布,它在指定的区间内所有的取值具有相同的概率密度。

在一个区间 [ a , b ] [a, b] [a,b] 上,均匀分布的概率密度函数为常数,

即在 [ a , b ] [a, b] [a,b] 内的任何值的概率密度都是 1 b − a \frac{1}{b-a} ba1,而在 [ a , b ] [a, b] [a,b] 之外的概率密度为 0 0 0

均匀分布的特点是在指定区间内取值的概率是相等的

什么是Xavier初始化?

Xavier初始化,是一种在训练深度神经网络时常用的权重初始化方法。它由Xavier Glorot和Yoshua Bengio在2010年的论文《Understanding the difficulty of training deep feedforward neural networks》中提出。

该方法旨在解决深度神经网络中的梯度消失或梯度爆炸问题,以保持激活函数的输出和梯度的方差在网络的每一层中保持相对一致

Xavier 初始化的核心思想是根据网络中每一层的输入和输出连接数(fan-in和fan-out)来初始化权重

具体来说,对于线性层或卷积层的权重张量,Xavier 初始化将权重从一个均匀分布或正态分布中随机抽取,这个分布的范围或标准差取决于fan-in和fan-out的值

对于均匀分布,权重初始化的公式为:

W ∼ U [ − 6 fan_in + fan_out , 6 fan_in + fan_out ] W \sim U\left[-\sqrt{\frac{6}{\text{fan\_in} + \text{fan\_out}}}, \sqrt{\frac{6}{\text{fan\_in} + \text{fan\_out}}}\right] WU[fan_in+fan_out6 ,fan_in+fan_out6 ]

对于正态分布,权重初始化的公式为:

W ∼ N ( 0 , 2 fan_in + fan_out ) W \sim N\left(0, \sqrt{\frac{2}{\text{fan\_in} + \text{fan\_out}}}\right) WN(0,fan_in+fan_out2 )

其中, W W W是权重矩阵, U U U表示均匀分布, N N N表示正态分布, fan_in \text{fan\_in} fan_in是权重矩阵的输入单元数, fan_out \text{fan\_out} fan_out是权重矩阵的输出单元数。

Xavier初始化的 目标是在网络的前向传播和反向传播过程中保持激活函数值和梯度的方差,从而避免梯度消失或梯度爆炸,提高网络的训练稳定性和收敛速度

这种初始化方法特别适合于使用 Sigmoid  \text{Sigmoid } Sigmoid  Tanh \text{Tanh} Tanh 激活函数的网络,但对于使用 ReLU \text{ReLU} ReLU 激活函数的网络,通常推荐使用何恺明提出的初始化方法。

nn.init.uniform_()

nn.init.uniform_() 用于将张量的元素初始化为均匀分布中的随机数

它是torch.nn.init模块的一部分,该模块提供了多种初始化方法。

nn.init.uniform_()函数的签名如下:

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

参数解释:

  • tensor (Tensor): 要初始化的张量。
  • a (float, optional): 均匀分布的下界。默认为0.0。
  • b (float, optional): 均匀分布的上界。默认为1.0。

函数作用:

  • tensor的元素初始化为介于ab之间的均匀分布随机数。

示例:

import torch
import torch.nn.init as init

# 创建一个未初始化的张量
tensor = torch.empty(3, 3)

# 使用均匀分布初始化张量
init.uniform_(tensor, a=-1, b=1)

print("Initialized Tensor:")
print(tensor)

输出(示例):

Initialized Tensor:
tensor([[-0.6872,  0.3796, -0.2345],
        [ 0.0461, -0.5820,  0.2237],
        [ 0.0567,  0.7295,  0.2923]])

在这个示例中,首先创建了一个3x3的未初始化张量。

然后,我们使用nn.init.uniform_()函数将张量的元素初始化为介于-1和1之间的均匀分布随机数。

每次运行这段代码时,张量中的元素都会有不同的随机值。

这种初始化方法通常用于初始化神经网络的权重和偏置,以确保它们具有足够的随机性,从而有助于打破对称性并提高模型的训练效果。

nn.init.xavier_uniform_()

nn.init.xavier_uniform_() 用于根据Xavier均匀初始化方法将张量的元素初始化为均匀分布中的随机数

这种初始化方法是为了保持输入和输出的方差一致,从而有助于梯度的传播,特别是在使用Sigmoid或Tanh激活函数时。

它是torch.nn.init模块的一部分,该模块提供了多种初始化方法。

nn.init.xavier_uniform_()函数的签名如下:

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

参数解释:

  • tensor (Tensor): 要初始化的张量。
  • gain (float, optional): 用于缩放均匀分布范围的增益系数。通常用于调整特定激活函数的初始化。默认为1.0。

函数作用:

  • tensor的元素初始化为根据Xavier均匀初始化方法得到的均匀分布随机数

Xavier 均匀初始化的计算公式为:

bound = 6 fan_in + fan_out \text{bound} = \sqrt{\frac{6}{\text{fan\_in} + \text{fan\_out}}} bound=fan_in+fan_out6

其中,fan_in是张量的输入单元数,fan_out是张量的输出单元数。随机数将从[- bound \text{bound} bound, bound \text{bound} bound] 的均匀分布中抽取。

示例:

import torch
import torch.nn.init as init

# 创建一个未初始化的张量
tensor = torch.empty(3, 3)

# 使用Xavier均匀初始化方法初始化张量
init.xavier_uniform_(tensor)

print("Initialized Tensor:")
print(tensor)

输出(示例):

Initialized Tensor:
tensor([[-0.5595, -0.5037,  0.0791],
        [-0.4920,  0.3322,  0.2105],
        [ 0.0539, -0.5764, -0.2091]])

在这个示例中,首先创建了一个3x3的未初始化张量。然后,我们使用nn.init.xavier_uniform_()函数将张量的元素初始化为根据Xavier均匀初始化方法得到的均匀分布随机数。

这种初始化方法通常用于初始化深度神经网络的权重,以提高模型的训练效果和收敛速度。

nn.init.kaiming_normal_()

nn.init.kaiming_normal_() 用于根据 Kaiming 正态初始化方法将张量的元素初始化为正态分布中的随机数

这种初始化方法由Kaiming He等人提出,旨在解决使用修正线性单元( ReLU \text{ReLU} ReLU)激活函数的深度神经网络中的梯度消失或梯度爆炸问题。

nn.init.kaiming_normal_()函数的签名如下:

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

参数解释:

  • tensor (Tensor): 要初始化的张量。
  • a (float, optional): 用于计算负斜率的修正线性单元(Leaky ReLU)的负斜率。默认为0,对应于标准的ReLU。
  • mode (str, optional): 可以是'fan_in'(默认)或'fan_out'。选择'fan_in'保持权重张量的前向传递的方差不变,而'fan_out'保持反向传递的方差不变。
  • nonlinearity (str, optional): 使用的非线性激活函数。默认为'leaky_relu'

函数作用:

  • tensor的元素初始化为根据Kaiming正态初始化方法得到的正态分布随机数。

Kaiming正态初始化的计算公式为:

W ∼ N ( 0 , 2 ( 1 + a 2 ) × fan_mode ) W \sim N\left(0, \sqrt{\frac{2}{(1 + a^2) \times \text{fan\_mode}}}\right) WN(0,(1+a2)×fan_mode2 )

其中, W W W是权重矩阵, N N N表示正态分布, a a a 是Leaky ReLU的负斜率, fan_mode \text{fan\_mode} fan_mode根据mode参数选择为fan_infan_out

示例:

import torch
import torch.nn.init as init

# 创建一个未初始化的张量
tensor = torch.empty(3, 3)

# 使用Kaiming正态初始化方法初始化张量
init.kaiming_normal_(tensor)

print("Initialized Tensor:")
print(tensor)

输出(示例):

Initialized Tensor:
tensor([[ 0.2024,  0.5111,  0.1928],
        [-0.4517,  0.2013, -0.0195],
        [-0.0303, -0.5325,  0.1205]])

在这个示例中,我们首先创建了一个3x3的未初始化张量。然后,我们使用nn.init.kaiming_normal_()函数将张量的元素初始化为根据Kaiming正态初始化方法得到的正态分布随机数。

这种初始化方法通常用于初始化使用ReLU激活函数的深度神经网络的权重,以提高模型的训练效果和收敛速度。

损失函数

在深度学习中,Loss Function(损失函数)、Cost Function(成本函数)和Objective Function(目标函数)是三个相关但略有不同的概念。

  1. Loss Function(损失函数):损失函数是用来衡量模型预测结果与实际标签之间的差异程度的函数。它通常是针对单个样本的误差度量,用于衡量模型在单个样本上的表现。在训练过程中,损失函数的值越小,代表模型的预测结果与实际标签越接近,模型的性能越好。常见的损失函数有均方误差(MSE)、交叉熵损失(Cross-Entropy Loss)等。

  2. Cost Function(成本函数)成本函数是损失函数的平均值或总和用于衡量整个训练集上模型的性能。成本函数是对损失函数在整个训练集上的表现进行汇总,因此它可以看作是对模型在整个数据集上的平均误差的度量。在训练过程中,优化算法的目标是最小化成本函数。通常情况下,成本函数可以通过计算所有样本的损失函数值的平均值或总和来得到。

  3. Objective Function(目标函数):目标函数是用来衡量整个模型的性能的函数,它通常是成本函数加上正则化项(如果有)。目标函数的优化是整个深度学习模型训练的最终目标。通过最小化目标函数,可以使模型在训练集上达到最佳性能,并且在测试集或实际应用中具有良好的泛化能力。

总的来说,

  • Loss Function 是针对单个样本的误差度量,
  • Cost Function 是对整个训练集上的损失函数的平均或总和,
  • Objective Function 是对整个模型的性能进行综合考虑的函数,包括了成本函数和可能的正则化项。在深度学习中,通常优化的目标是最小化目标函数。
  • 索引

分类任务损失函数中什么是负对数似然?以及为什么求损失函数时要取负对数似然

在分类任务中,负对数似然(Negative Log-Likelihood,NLL)是一种常用的损失函数。为了理解负对数似然,我们需要先了解似然函数(Likelihood)和对数似然(Log-Likelihood)。

似然函数(Likelihood)

似然函数是一个统计学概念,用于表示一组参数在给定观测数据下的概率。在分类任务中,似然函数通常表示模型预测的概率分布与实际观测数据之间的匹配程度。假设我们有一个分类模型,它对每个类别输出一个概率值,那么似然函数可以表示为模型对实际类别的预测概率。

对数似然(Log-Likelihood)

对数似然是似然函数取对数后的结果。在实践中,我们通常使用对数似然而不是原始似然,因为对数似然具有一些数学上的优势,比如将乘法转换为加法,这可以简化计算并提高数值稳定性

负对数似然(Negative Log-Likelihood)

负对数似然是对数似然的相反数。在分类任务中,我们通常希望最大化对数似然,即使模型预测的概率分布尽可能接近实际观测数据。然而,在优化问题中,我们通常是最小化一个损失函数,因此我们使用负对数似然作为损失函数。通过最小化负对数似然,我们实际上是在最大化对数似然,从而使模型预测更准确

为什么要取负对数似然

  1. 最大化似然:在统计学中,通常目标是最大化似然函数,即找到使数据出现概率最大的参数。在机器学习中,这转化为最小化损失函数,因此我们取似然函数的负对数作为损失函数。

  2. 数值稳定性:直接计算似然函数可能会涉及到很小的概率值的乘积,这可能导致数值不稳定或下溢。取对数可以将乘法转换为加法,从而提高数值稳定性。

  3. 梯度优化:在梯度下降或其他优化算法中,对数似然的梯度通常更容易计算,并且可以提供更稳定和有效的优化过程。

总的来说,负对数似然作为损失函数可以帮助我们通过最小化损失来训练模型,使得模型预测的概率分布尽可能接近真实的数据分布。

nn.NLLLoss()

nn.NLLLoss 在 PyTorch 中的签名如下:

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

参数说明:

  • weight (Tensor, optional): 类别权重,用于处理不平衡的类别问题。如果提供了该参数,它应该是一个形状为 (C,) 的 1D Tensor,其中 C 是类别数。
  • size_average (bool, optional): 已废弃,不建议使用。
  • ignore_index (int, optional): 指定一个目标值,该目标值会被忽略并且不会对损失计算产生影响。通常用于忽略某些特定的类别。
  • reduce (bool, optional): 已废弃,不建议使用。
  • reduction (string, optional): 指定损失的减少方式,可选值有 'none''mean''sum''none' 表示不进行减少,直接返回每个样本的损失;'mean' 表示返回所有样本损失的平均值;'sum' 表示返回所有样本损失的总和。

示例:

假设我们有一个简单的分类任务,其中有三个类别(0, 1, 2),我们的模型输出了一批大小为 2 2 2 的经过 softmax 处理的概率分布和对应的真实标签。

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

# 设置随机种子以确保结果可复现
torch.manual_seed(0)

# 模型预测的 logits (未经 softmax 归一化)
logits = torch.tensor([[1.2, 0.9, -0.5], [0.3, 2.1, -1.7]])

# 应用 softmax 得到概率分布
probs = F.softmax(logits, dim=1)

# 取对数得到 log-probabilities,nn.NLLLoss 需要 log-probabilities 作为输入
log_probs = torch.log(probs)

# 真实标签
labels = torch.tensor([2, 1])

# 创建负对数似然损失函数
criterion = nn.NLLLoss()

# 计算损失
loss = criterion(log_probs, labels)

print(f'Loss: {loss.item()}')

在这个例子中,我们首先计算模型输出的 softmax 概率分布,然后取对数得到 log-probabilities,因为 nn.NLLLoss 需要 log-probabilities 作为输入。nn.NLLLoss 会根据真实标签中的索引,从 log-probabilities 中选取相应的值,并计算它们的负值作为损失。如果设置 reduction='mean',最后会返回所有样本损失的平均值。这个过程是交叉熵损失的一部分,通常用于多分类问题中。

交叉熵损失函数

交叉熵损失(CrossEntropyLoss)是一种常用的损失函数,用于评估模型预测结果与真实标签之间的差异,尤其适用于分类问题。在深度学习中,交叉熵损失经常用于训练分类器,如神经网络。下面是交叉熵损失的详细解释:

定义

对于一个多类分类问题,假设有 N N N 个样本,每个样本属于 C C C 个类别中的一个。

对于第 i i i 个样本,其真实标签用一个独热编码向量 y i y_i yi 表示,其中只有对应于真实类别的元素为 1 1 1,其余元素为 0 0 0

模型对于第 i i i 个样本的预测输出为一个概率分布向量 p i p_i pi,其中每个元素表示该样本属于对应类别的预测概率。

交叉熵损失函数可以表示为:
L = − 1 N ∑ i = 1 N ∑ c = 1 C y i , c log ⁡ ( p i , c ) L = -\frac{1}{N} \sum_{i=1}^{N} \sum_{c=1}^{C} y_{i,c} \log(p_{i,c}) L=N1i=1Nc=1Cyi,clog(pi,c)

解释

  • 在交叉熵损失函数表达式中, y i , c y_{i,c} yi,c 表示第 i i i 个样本的真实标签在第 c c c 个类别上的值。具体来说,如果使用独热编码(one-hot encoding)来表示标签,那么:

    • 如果第 i i i 个样本的真实类别是 c c c,则 y i , c = 1 y_{i,c} = 1 yi,c=1
    • 如果第 i i i 个样本的真实类别不是 c c c,则 y i , c = 0 y_{i,c} = 0 yi,c=0

换句话说, y i , c y_{i,c} yi,c 是一个指示函数,用于指示第 i i i 个样本是否属于第 c c c 个类别。在交叉熵损失函数中,只有当 y i , c = 1 y_{i,c} = 1 yi,c=1 时,即样本真实属于该类别时,对应的预测概率 p i , c p_{i,c} pi,c 才会对损失值有贡献。这样,损失函数就只关注模型对每个样本真实类别的预测概率,从而指导模型学习正确分类。

  • log ⁡ ( p i , c ) \log(p_{i,c}) log(pi,c) 的底数通常是自然对数的底 e e e,也就是 ln ⁡ ( ) \ln () ln()函数。在计算机科学和深度学习领域,对数函数 log ⁡ \log log 通常默认为自然对数,除非另有明确指定。
  • p i , c p_{i,c} pi,c 是第 i i i 个样本属于类别 c c c 的概率
  • 交叉熵损失计算的是真实标签分布与预测概率分布之间的差异。如果预测概率分布接近真实标签分布,损失值较小;如果预测概率分布远离真实标签分布,损失值较大。
  • 对于每个样本,损失函数只考虑真实类别对应的概率。如果模型对真实类别的预测概率高,损失值较低;如果模型对真实类别的预测概率低,损失值较高。
  • 对数函数 log ⁡ ( p i , c ) \log(p_{i,c}) log(pi,c) 用于惩罚低概率的错误预测。当 p i , c p_{i,c} pi,c 接近0时, log ⁡ ( p i , c ) \log(p_{i,c}) log(pi,c) 的值会变得非常小(负值),导致损失值增大。
  • 通过最小化交叉熵损失,模型可以学习调整其参数以提高对真实类别的预测概率,从而提高分类准确性。

注意事项

  • 交叉熵损失通常与 Softmax 激活函数结合使用,将模型的输出转换为概率分布。
  • 在实现时,为了数值稳定性,通常使用对数Softmax(log-Softmax)和负对数似然(NLLLoss)的组合来代替直接计算交叉熵损失。

总的来说,交叉熵损失是衡量分类模型性能的重要指标,通过最小化这个损失,可以使模型在分类任务上表现得更好。

nn.CrossEntropyLoss()

nn.CrossEntropyLoss 在 PyTorch 中的签名如下:

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

参数说明:

  • weight (Tensor, optional): 类别权重,用于处理不平衡的类别问题。如果提供了该参数,它应该是一个形状为 (C,) 的 1D Tensor,其中 C 是类别数。
  • size_average (bool, optional): 已废弃,不建议使用。
  • ignore_index (int, optional): 指定一个目标值,该目标值会被忽略并且不会对损失计算产生影响。通常用于忽略某些特定的类别。
  • reduce (bool, optional): 已废弃,不建议使用。
  • reduction (string, optional): 指定损失的减少方式,可选值有
    • 'none': 不进行任何操作,返回各个样本的损失值。
    • 'mean': 返回所有样本损失的平均值。
    • 'sum': 返回所有样本损失的总和。

nn.CrossEntropyLossnn.NLLLoss 中的 weight 参数是一个可选的 Tensor,用于对不同类别的损失赋予不同的权重。这在处理类别不平衡的数据集时特别有用,可以帮助模型更关注那些较少出现的类别。

假设我们有一个三类的分类问题,即类别标签为 0、1 和 2。如果我们定义了一个权重 Tensor 如下:

weight = torch.tensor([0.5, 2.0, 1.0])

那么,这个 Tensor 中的各个元素含义如下:

  • 0.5 是类别 0 的权重。这意味着属于类别 0 的样本的损失将乘以 0.5。
  • 2.0 是类别 1 的权重。这意味着属于类别 1 的样本的损失将乘以 2.0,因此这个类别的样本在计算总损失时会有更大的影响。
  • 1.0 是类别 2 的权重。这意味着属于类别 2 的样本的损失将乘以 1.0,即保持原样。

通过这种方式,我们可以通过调整权重来控制不同类别在损失函数中的重要性,从而帮助模型在训练过程中更加关注那些对我们更重要的类别。

示例:

假设我们有一个简单的分类任务,其中有三个类别(0, 1, 2),我们的模型输出了一批大小为 2 的预测和对应的真实标签。

import torch
import torch.nn as nn

# 设置随机种子以确保结果可复现
torch.manual_seed(0)

# 模型预测的 logits (未经 softmax 归一化)
logits = torch.tensor([[1.2, 0.9, -0.5], [0.3, 2.1, -1.7]])

# 真实标签
labels = torch.tensor([2, 1])

# 创建交叉熵损失函数
criterion = nn.CrossEntropyLoss()

# 计算损失
loss = criterion(logits, labels)

print(f'Loss: {loss.item()}')

在这个例子中,logits 是模型的原始输出(未经过 softmax 处理)。

nn.CrossEntropyLoss 内部会对这些 logits 应用 softmax 函数,然后计算交叉熵损失。这也是为什么在使用 nn.CrossEntropyLoss 时,模型的最后一层通常不包含 softmax 函数的原因。最终,loss 变量包含了这批数据的平均损失值。

nn.CrossEntropyLoss() 结合了 nn.LogSoftmax()nn.NLLLoss()(负对数似然损失)两个步骤。它适用于多分类问题,其中输入是一个未经归一化的分数向量,目标是一个包含类别索引的张量。损失的计算公式如下:

loss ( x , class ) = − log ⁡ ( exp ⁡ ( x [ class ] ) ∑ j exp ⁡ ( x [ j ] ) ) = − x [ class ] + log ⁡ ( ∑ j exp ⁡ ( x [ j ] ) ) \text{loss}(x, \text{class}) = -\log\left(\frac{\exp(x[\text{class}])}{\sum_j \exp(x[j])}\right) = -x[\text{class}] + \log\left(\sum_j \exp(x[j])\right) loss(x,class)=log(jexp(x[j])exp(x[class]))=x[class]+log(jexp(x[j]))

其中, x x x 是模型输出的未经归一化的分数向量, class \text{class} class 是目标类别的索引。该损失函数会自动为每个样本应用 LogSoftmax 函数,然后计算它们的负对数似然损失。

NLLLoss() 和 CrossEntropyLoss() 的区别?

nn.NLLLoss()nn.CrossEntropyLoss() 在功能上有一些区别,但在某些情况下可以等效使用。这两个损失函数通常用于多分类问题。

  • nn.NLLLoss():负对数似然损失。在使用该损失函数时,需要将模型的输出通过 softmax 函数转换为概率分布。然后,NLLLoss 根据实际标签的索引计算该概率分布下标签对应位置的概率的负对数似然。这意味着你需要手动应用 softmax 函数,而不是在模型中包含 softmax 层。

  • nn.CrossEntropyLoss():交叉熵损失。这个损失函数结合了 softmax 和负对数似然。它不要求手动应用 softmax 函数,因为它在内部处理了这一步骤。你只需将模型的输出直接传递给 CrossEntropyLoss,它会自动将其转换为概率分布并计算损失。

两者的效果在数学上是等效的,但在使用上有一些细微差别。如果你的模型最后一层是 softmax,那么两者之间的选择通常没有太大差异。如果模型的最后一层不是 softmax,而是 logits(未经过 softmax 处理的原始输出),则应该使用 nn.CrossEntropyLoss(),因为它会在内部应用 softmax。

nn.BCELoss()

nn.BCELoss() 用于计算二元交叉熵损失(Binary Cross Entropy Loss)。通常用于二分类任务中,特别是当模型输出是一个概率值(介于 0 和 1 之间)时。这个损失函数会计算目标值(真实标签)和预测值(模型输出)之间的二元交叉熵。

函数签名:

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

参数解释:

  • weight (Tensor, 可选):一个形状为 (nbatch,) 的张量,用于对损失进行加权。如果指定,损失会乘以相应的权重。
  • size_average (布尔值, 可选):已弃用(用 reduction 代替)。如果为 True,则损失会在每个小批量的元素上取平均;否则会求和。默认为 True
  • reduce (布尔值, 可选):已弃用(用 reduction 代替)。如果为 True,则返回标量损失;否则返回每个元素的损失。
  • reduction (字符串, 可选):指定损失的缩减方式。可选值有 'none'(无缩减),'mean'(取平均),'sum'(求和)。默认为 'mean'

示例:

import torch
import torch.nn as nn

# 创建损失函数
criterion = nn.BCELoss()

# 模拟预测值和真实标签
predictions = torch.tensor([0.7, 0.3, 0.5], requires_grad=True)
targets = torch.tensor([1, 0, 1], dtype=torch.float32)

# 计算损失
loss = criterion(predictions, targets)

print(loss)

在这个示例中,predictions 是模型的输出,targets 是真实的标签。损失函数 nn.BCELoss() 会计算这两者之间的二元交叉熵损失。

nn.BCEWithLogitsLoss()

nn.BCEWithLogitsLoss() 用于计算二元交叉熵损失,同时结合了 Sigmoid 层 和 BCELoss(二元交叉熵损失)。这种组合是为了提高数值稳定性,并且通常用于二分类任务中,特别是当模型输出是未经 Sigmoid 激活的原始分数(logits)时

函数签名:

torch.nn.BCEWithLogitsLoss(weight=None, size_average=None, 
reduce=None, reduction='mean', pos_weight=None)

参数解释:

  • weight (Tensor, 可选):一个形状为 (nbatch,) 的张量,用于对损失进行加权。如果指定,损失会乘以相应的权重。
  • size_average (布尔值, 可选):已弃用(用 reduction 代替)。如果为 True,则损失会在每个小批量的元素上取平均;否则会求和。默认为 True
  • reduce (布尔值, 可选):已弃用(用 reduction 代替)。如果为 True,则返回标量损失;否则返回每个元素的损失。
  • reduction (字符串, 可选):指定损失的缩减方式。可选值有 'none'(无缩减),'mean'(取平均),'sum'(求和)。默认为 'mean'
  • pos_weight (Tensor, 可选):一个形状为 (nclass,) 的张量,用于平衡正负样本的权重。这对于不平衡的数据集特别有用。如果指定,对于正样本的损失会乘以这个权重。

示例:

import torch
import torch.nn as nn

# 创建损失函数
criterion = nn.BCEWithLogitsLoss()

# 模拟未经 Sigmoid 激活的预测值(logits)和真实标签
logits = torch.tensor([0.2, -0.3, 0.1], requires_grad=True)
targets = torch.tensor([1, 0, 1], dtype=torch.float32)

# 计算损失
loss = criterion(logits, targets)

print(loss)

在这个示例中,logits 是模型的输出(未经 Sigmoid 激活),targets 是真实的标签。损失函数 nn.BCEWithLogitsLoss() 会首先对 logits 应用 Sigmoid 激活,然后计算二元交叉熵损失。这种方法可以提高数值稳定性,尤其是在处理极端值时。

nn.BCELoss() 和 nn.BCEWithLogitsLoss() 的区别

nn.BCELoss()nn.BCEWithLogitsLoss() 都是 PyTorch 中用于二分类任务的损失函数,但它们之间有一些关键区别:

  1. Sigmoid 激活

    • nn.BCELoss() 需要模型的输出是经过 Sigmoid 激活的概率值(介于 0 和 1 之间)。因此,在计算损失之前,你需要确保模型的最后一层是 Sigmoid 函数,或者手动对输出应用 Sigmoid 函数。
    • nn.BCEWithLogitsLoss() 结合了 Sigmoid 激活和二元交叉熵损失计算。它接受的输入是未经 Sigmoid 激活的原始分数(logits),然后在内部应用 Sigmoid 函数。这种方法可以提高数值稳定性,尤其是在处理极端值时。
  2. 数值稳定性

    • 使用 nn.BCELoss() 时,如果模型输出的概率值非常接近 0 或 1,可能会导致数值不稳定,从而影响损失的计算。
    • nn.BCEWithLogitsLoss() 在内部使用一种数值稳定的方法来计算损失,这可以避免因概率值过于极端而导致的数值问题。
  3. 使用场景

    • 如果你的模型已经包含了 Sigmoid 激活层,或者你希望在计算损失之前手动对输出应用 Sigmoid 函数,那么可以使用 nn.BCELoss()
    • 如果你的模型输出是原始分数(logits),并且你希望在损失计算中自动包含 Sigmoid 激活,那么推荐使用 nn.BCEWithLogitsLoss()

综上所述,nn.BCEWithLogitsLoss() 通常是更好的选择,因为它在计算损失时提供了更好的数值稳定性,并且简化了模型结构,无需在模型的最后显式添加 Sigmoid 激活层。

nn.L1Loss()

nn.L1Loss() 用于计算预测值和真实值之间的 L 1 L1 L1 距离,也称为绝对误差。这种损失函数常用于回归任务中,尤其是当你希望对异常值(outliers)不那么敏感时。

函数签名:

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

参数解释:

  • size_average (布尔值, 可选):已弃用(用 reduction 代替)。如果为 True,则损失会在每个小批量的元素上取平均;否则会求和。默认为 True
  • reduce (布尔值, 可选):已弃用(用 reduction 代替)。如果为 True,则返回标量损失;否则返回每个元素的损失。
  • reduction (字符串, 可选):指定损失的缩减方式。可选值有 'none'(无缩减),'mean'(取平均),'sum'(求和)。默认为 'mean'

示例:

import torch
import torch.nn as nn

# 创建损失函数
criterion = nn.L1Loss()

# 模拟预测值和真实值
predictions = torch.tensor([1.2, 2.5, 3.8], requires_grad=True)
targets = torch.tensor([1.0, 2.0, 3.0], dtype=torch.float32)

# 计算损失
loss = criterion(predictions, targets)

print(loss)

在这个示例中,predictions 是模型的输出,targets 是真实的目标值。损失函数 nn.L1Loss() 会计算这两者之间的 L1 距离(绝对误差)。如果 reduction 参数设置为 'mean',则返回损失的平均值;如果设置为 'sum',则返回损失的总和。

nn.MSELoss()

均方误差函数

均方误差(Mean Squared Error, MSE)是一种常用的损失函数,广泛应用于深度学习和计算机视觉领域,特别是在回归任务和一些特定的分类问题中。MSE衡量的是预测值与真实值之间的差异的平方的平均值,用于评估模型的预测准确性。

定义

对于给定的数据集,假设我们有 N N N 个样本,对于每个样本 i i i,我们有一个真实值 y i y_i yi 和模型预测的值 y ^ i \hat{y}_i y^i。MSE的定义如下:

MSE = 1 N ∑ i = 1 N ( y i − y ^ i ) 2 \text{MSE} = \frac{1}{N} \sum_{i=1}^{N} (y_i - \hat{y}_i)^2 MSE=N1i=1N(yiy^i)2

解释

  • 差异的平方 ( y i − y ^ i ) 2 (y_i - \hat{y}_i)^2 (yiy^i)2 计算了每个样本的真实值与预测值之间的差异的平方。这样做的目的是确保差异总是非负的,并且对较大的误差赋予更大的权重(因为误差的平方会放大误差)

  • 平均值:将所有样本的平方差异相加后,我们再除以样本的总数 N N N ,以得到平均误差。这使得 MSE 成为一个评估整个数据集预测性能的指标。

优点

  • 直观:MSE提供了一种直观的方式来衡量模型预测值与真实值之间的差异。
  • 易于计算:MSE的计算相对简单,易于实现。
  • 连续可微:MSE是连续可微的,这在使用基于梯度的优化算法(如梯度下降)时非常重要。

缺点

  • 对异常值敏感:由于MSE对较大的误差赋予更大的权重,因此它对异常值(outliers)非常敏感。在存在异常值的情况下,MSE可能不是最佳的损失函数选择。
  • 缩放依赖性:MSE的值依赖于数据的缩放。不同的缩放可能导致不同的MSE值,这使得比较不同问题或数据集的MSE值变得困难。

应用

在计算机视觉中,MSE常用于回归任务,如预测图像中的某些数值属性(例如物体的位置、大小等),或者在图像重建和超分辨率等任务中,作为评估图像质量的一种指标。在这些应用中,MSE帮助衡量预测图像与真实图像之间的相似度。

nn.MSELoss() 用于计算预测值和真实值之间的均方误差(Mean Squared Error, MSE)。这种损失函数常用于回归任务中,尤其是当你希望对大的误差赋予更大的惩罚时。

函数签名:

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

参数解释:

  • size_average (布尔值, 可选):已弃用(用 reduction 代替)。如果为 True,则损失会在每个小批量的元素上取平均;否则会求和。默认为 True
  • reduce (布尔值, 可选):已弃用(用 reduction 代替)。如果为 True,则返回标量损失;否则返回每个元素的损失。
  • reduction (字符串, 可选):指定损失的缩减方式。可选值有 'none'(无缩减),'mean'(取平均),'sum'(求和)。默认为 'mean'

示例:

import torch
import torch.nn as nn

# 创建损失函数
criterion = nn.MSELoss()

# 模拟预测值和真实值
predictions = torch.tensor([1.2, 2.5, 3.8], requires_grad=True)
targets = torch.tensor([1.0, 2.0, 3.0], dtype=torch.float32)

# 计算损失
loss = criterion(predictions, targets)

print(loss)

在这个示例中,predictions 是模型的输出,targets 是真实的目标值。损失函数 nn.MSELoss() 会计算这两者之间的均方误差。如果 reduction 参数设置为 'mean',则返回损失的平均值;如果设置为 'sum',则返回损失的总和。这种损失函数对于大的误差会施加更大的惩罚,因为误差会被平方。

其余的损失函数

nn.SmoothL1Loss

nn.PoissonNLLLoss

nn.KLDivLoss

nn.MarginRankingLoss

nn.MultiLabelMarginLoss

nn.SoftMarginLoss

nn.MultiLabelSortMarginLoss

nn.MultiMarginLoss(hingLoss)

nn.TripletMarginLoss

nn.HingeEmbeddingLoss

nn.CosineEmbeddingLoss

nn.CTCLoss

Optimizer

什么是 Optimizer?

PyTorch中的优化器(Optimizer)是用于更新神经网络参数以最小化损失函数的一种工具。优化器的选择和配置对模型的训练效果和速度有重要影响。下面是一些常见的PyTorch优化器以及它们的特点:

  1. 随机梯度下降(SGD):

    • 最基本的优化算法。
    • 公式:θ = θ - lr * ∇θ,其中θ是参数,lr是学习率,∇θ是参数的梯度。
    • 可以添加动量(momentum)来加速收敛。
  2. 带动量的SGD(SGD with Momentum):

    • 在传统的SGD基础上增加了动量项,可以加快收敛速度并减少震荡
    • 公式:v = momentum * v - lr * ∇θθ = θ + v,其中v是动量项。
  3. Adam(Adaptive Moment Estimation):

    • 结合了RMSProp和带动量的SGD的优点。
    • 能够自适应地调整每个参数的学习率。
    • 维护了梯度的一阶矩估计(即动量)和二阶矩估计(即梯度的未中心化的方差)。
  4. RMSProp(Root Mean Square Propagation):

    • 旨在解决Adagrad学习率急剧下降的问题。
    • 维护了一个梯度平方的滑动平均,用于调整每个参数的学习率。
  5. Adagrad(Adaptive Gradient Algorithm):

    • 适合处理稀疏梯度的优化器。
    • 为每个参数维护一个自适应的学习率,依据参数的更新频率进行调整。

使用优化器时,需要先实例化一个优化器对象,然后在训练循环中调用其step()方法来更新参数,例如:

import torch
import torch.nn as nn
import torch.optim as optim

model = nn.Linear(10, 1)  # 示例模型
optimizer = optim.SGD(model.parameters(), lr=0.01)  # 使用SGD优化器

# 训练循环
for input, target in dataset:
    optimizer.zero_grad()   # 清除梯度
    output = model(input)   # 前向传播
    loss = loss_fn(output, target)  # 计算损失
    loss.backward()         # 反向传播
    optimizer.step()        # 更新参数

在使用优化器时,通常需要根据具体问题和模型进行调参,包括选择合适的优化器、设置学习率和其他超参数。

为什么在使用optimizer时,首先需要执行zero_grad()?

在PyTorch中,执行optimizer.zero_grad()是为了清除累积的梯度,这是因为PyTorch的设计中默认会累积梯度。

在每次反向传播(backward())时,PyTorch会计算每个参数的梯度,并将其累加到参数的.grad属性中。如果不清除之前的梯度,那么新的梯度会与旧的梯度累加,导致梯度值不准确。这样会影响模型的学习过程,可能导致模型收敛得更慢,甚至无法收敛。

因此,在每次进行参数更新之前(即在每次调用optimizer.step()之前),都需要先执行optimizer.zero_grad()来清除累积的梯度,确保每次更新都是基于当前批次数据计算的梯度。

示例代码:

for input, target in dataset:
    optimizer.zero_grad()   # 清除累积的梯度
    output = model(input)   # 前向传播
    loss = loss_fn(output, target)  # 计算损失
    loss.backward()         # 反向传播,计算当前批次的梯度
    optimizer.step()        # 更新参数

在这个循环中,每次迭代都会先清除梯度,然后进行前向传播、计算损失、反向传播和参数更新,这样可以确保每次更新都是基于最新计算的梯度

detach() 方法

在PyTorch中,detach()方法用于将一个张量(tensor)从计算图中分离出来。分离后的张量不再参与梯度的计算和传播,即它的requires_grad属性将被设置为False。这意味着对分离后的张量进行的任何操作都不会影响梯度的计算,也不会影响原始计算图中其他张量的梯度。

detach()方法的使用场景包括:

  1. 阻止梯度的传播:在某些情况下,我们可能只想对计算图中的一部分进行梯度更新,而不希望梯度传播到计算图的其他部分。通过使用detach()方法,我们可以阻止梯度的传播。

  2. 减少计算和内存开销:在进行推理(inference)或评估(evaluation)时,通常不需要计算梯度。通过将输入张量或中间结果分离出来,可以减少不必要的梯度计算和内存开销。

  3. 避免梯度累积:在某些复杂的模型或训练过程中,可能需要手动控制梯度的流动,以避免不必要的梯度累积或梯度消失/爆炸问题。

示例代码:

import torch

x = torch.tensor([1.0, 2.0, 3.0], requires_grad=True)
y = x * 2
z = y.sum()

z.backward()
print(x.grad)  # 输出: tensor([2., 2., 2.])

# 使用detach()分离y
y_detached = y.detach()
z_detached = y_detached.sum()

# 对分离后的张量进行操作不会影响梯度
z_detached.backward()
print(x.grad)  # 输出仍然是: tensor([2., 2., 2.])

在这个示例中,y是依赖于x的张量,当我们对z调用backward()时,x的梯度被计算出来。然后我们对y使用detach()方法,得到分离后的y_detached,对y_detached进行操作(如计算z_detached)不会影响x的梯度。因此,当我们再次对z_detached调用backward()时,x的梯度不会发生变化。

Class Optimizer 的属性

在PyTorch中,torch.optim.Optimizer类是所有优化器的基类,它定义了优化器的基本结构和行为。Optimizer类的主要属性包括:

  1. param_groups:

    • 类型:列表(list)
    • 描述:包含所有参数组的列表。每个参数组都是一个字典,包含一组参数以及这组参数的优化选项(如学习率、权重衰减等)。通过对不同的参数组设置不同的优化选项,可以对模型的不同部分应用不同的优化策略
  2. state:

    • 类型:字典(dict)
    • 描述:存储优化器的状态信息,用于保存一些额外的信息。例如,在Adam优化器中,state会存储每个参数的梯度的一阶矩和二阶矩。这些信息对于实现一些复杂的优化算法是必要的。
  3. defaults:

    • 类型:字典(dict)
    • 描述:包含优化器默认参数的字典。这些默认参数是在初始化优化器时提供的,例如学习率、动量等。如果在param_groups中没有为某个参数组指定某个选项,就会使用defaults中的默认值。

除了这些属性,Optimizer类还提供了一些方法,如step()用于执行一步参数更新,zero_grad()用于清除梯度等。

以下是一个使用param_groups属性的示例,展示了如何为模型的不同部分设置不同的学习率:

import torch
import torch.nn as nn
import torch.optim as optim

model = nn.Sequential(
    nn.Linear(10, 5),
    nn.ReLU(),
    nn.Linear(5, 1)
)

# 设置不同的学习率
optimizer = optim.SGD([
    {'params': model[0].parameters(), 'lr': 0.01},  # 第一层的学习率为0.01
    {'params': model[2].parameters(), 'lr': 0.001}  # 第三层的学习率为0.001
], lr=0.1)  # 其他层使用默认学习率0.1

# 检查param_groups
for param_group in optimizer.param_groups:
    print(param_group['lr'])

在这个示例中,我们为模型的第一层和第三层分别设置了不同的学习率,而第二层(ReLU激活层)没有参数,所以不需要设置。通过这种方式,可以灵活地调整模型不同部分的学习率,以达到更好的训练效果。

Class Optimizer 的方法

step()

每执行一次,就更新一次相关的参数
在这里插入图片描述

zero_grad

print("weight in optimizer:{}\nweight in weight:{}\n".format(id(optimizer.param_groups[0]['params'][0]), id(weight)))
print("weight.grad is {}\n".format(weight.grad))
optimizer.zero_grad()
print("after optimizer.zero_grad(), weight.grad is\n{}".format(weight.grad))

执行结果如图:
在这里插入图片描述

add_param_groups()

add_param_group()方法用于向优化器中添加一个新的参数组。这在某些情况下非常有用,比如当你在训练过程中决定添加新的层或模块到你的模型中,并且希望为这些新参数设置不同的优化选项时。

每个参数组都是一个字典,包含参数和这些参数的优化选项。添加参数组后,优化器将在后续的训练步骤中考虑这些新参数。

示例代码:

import torch
import torch.nn as nn
import torch.optim as optim

# 定义一个简单的模型
class MyModel(nn.Module):
    def __init__(self):
        super(MyModel, self).__init__()
        self.fc1 = nn.Linear(4, 3)
        self.fc2 = nn.Linear(3, 2)

    def forward(self, x):
        x = self.fc1(x)
        x = self.fc2(x)
        return x

model = MyModel()

# 初始化优化器,只包含模型的第一层参数
optimizer = optim.SGD([{'params': model.fc1.parameters(), 'lr': 0.01}])

# 检查优化器的参数组
print("初始参数组:", optimizer.param_groups)

# 在训练过程中,决定添加第二层参数到优化器中,并为其设置不同的学习率
optimizer.add_param_group({'params': model.fc2.parameters(), 'lr': 0.001})

# 检查添加新参数组后的优化器参数组
print("添加新参数组后:", optimizer.param_groups)

执行结果打印信息:

初始参数组: [{'params': [Parameter containing:
tensor([[ 0.0628,  0.4983, -0.3158,  0.2664],
        [-0.2767, -0.4701, -0.1063,  0.2881],
        [ 0.4642, -0.3105,  0.1085,  0.4314]], requires_grad=True),
Parameter containing:
tensor([0.3313, 0.3116, 0.3553], requires_grad=True)], 'lr': 0.01, 'momentum': 0, 
'dampening': 0, 'weight_decay': 0, 'nesterov': False, 'maximize': False, 'foreach': None, 'differentiable': False}]

添加新参数组后: [{'params': [Parameter containing:
tensor([[ 0.0628,  0.4983, -0.3158,  0.2664],
        [-0.2767, -0.4701, -0.1063,  0.2881],
        [ 0.4642, -0.3105,  0.1085,  0.4314]], requires_grad=True), 
Parameter containing:
tensor([0.3313, 0.3116, 0.3553], requires_grad=True)], 'lr': 0.01, 'momentum': 0, 
'dampening': 0, 'weight_decay': 0, 'nesterov': False, 'maximize': False, 'foreach': None, 'differentiable': False}, {'params': [Parameter containing:
tensor([[ 0.3652,  0.1491, -0.3948],
        [-0.4848, -0.2646, -0.0672]], requires_grad=True), Parameter containing:
tensor([-0.3539,  0.2112], requires_grad=True)], 'lr': 0.001, 'momentum': 0,
'dampening': 0, 'weight_decay': 0, 'nesterov': False, 'maximize': False, 'foreach': None, 'differentiable': False}]

可以看到打印信息中,param_groups 是一个元素为 list 的 dict,[]是list标识符,{} 是dict标识符,在初始化优化器之后,打印 param_groups 输出的信息中,params 包含 2 个张量(来自 model.fc1.parameters() ),分别是 权重矩阵偏置向量,所以这两个Tensor的形状分别是 [ 3 , 4 ] [3, 4] [3,4] [ 3 ] [3] [3]

  1. 权重矩阵(weights):形状为[3, 4]。这是因为该层需要将4维输入特征转换为3维输出特征,所以权重矩阵的形状是输出维度乘以输入维度,即3 x 4

  2. 偏置向量(bias):形状为[3]。偏置向量的长度等于输出特征的维度,即3。

可以看到,在添加第二层参数之前,param_groups 只有一对 {},也就是只有一个字典元素(第一层参数),
在添加第二层参数之后,param_groups 有 2 对 {}

在这个示例中,首先定义了一个包含两个全连接层的简单模型。然后初始化了一个优化器,最初只包含模型的第一层参数。

接着,在训练过程中,决定将第二层参数也加入到优化器中,并为这些新参数设置了不同的学习率。通过调用add_param_group()方法,我们可以轻松地将新的参数组添加到优化器中,而无需重新创建一个新的优化器实例。这种灵活性使得在复杂的训练场景中进行动态调整成为可能。

这部分代码 [{'params': model.fc1.parameters(), 'lr': 0.01}] 的含义是,方括号是列表标识符,{}是创建了一个字典,存在2个键,分别是 ‘params’ 和 ‘lr’

这个字典有两个键:

  1. 'params':它的值是model.fc1.parameters(),表示模型第一层的所有可训练参数。
  2. 'lr':它的值是0.01,表示这些参数的学习率。

这个列表可以包含多个这样的字典,每个字典代表一个参数组,可以有自己的优化选项(如学习率、权重衰减等)。当这个列表传递给优化器时,优化器会为每个参数组分别进行优化。

state_dict()

在这里插入图片描述
执行后打印信息如下:

weight before step:tensor([[0.6614, 0.2669],
        [0.0617, 0.6213]])
weight after step:tensor([[ 0.5614,  0.1669],
        [-0.0383,  0.5213]])
state_dict before step:
 {'state': {}, 'param_groups': [{'lr': 0.1, 'momentum': 0.9, 'dampening': 0, 'weight_decay': 0,
  'nesterov': False, 'maximize': False, 'foreach': None, 'differentiable': False, 'params': [0]}]}
state_dict after step:
 {'state': {0: {'momentum_buffer': tensor([[6.5132, 6.5132],
        [6.5132, 6.5132]])}}, 'param_groups': [{'lr': 0.1, 'momentum': 0.9, 'dampening': 0, 
        'weight_decay': 0, 'nesterov': False, 'maximize': False, 'foreach': None, 'differentiable': False, 'params': [0]}]}

在上面的代码中,当使用带动量(momentum)的 SGD 优化器时,优化器会维护一个额外的状态信息,即动量缓冲区(momentum_buffer)。这个缓冲区用于存储每个参数的动量值,它是动量方法中的关键部分。

动量方法旨在加速学习过程,特别是在梯度的方向一致的情况下。它通过结合当前梯度和过去梯度的加权平均来更新参数,从而实现更平滑的更新过程。

具体来说,动量更新规则如下:

v = momentum * v_prev - lr * grad
param = param + v

其中 v 是当前的动量缓冲区值,v_prev 是上一次的动量缓冲区值,lr 是学习率,grad 是当前梯度,param 是要更新的参数。

在上面的代码中,momentum_buffer 在第一次调用 optimizer.step() 后被创建,并在后续的迭代中更新。这就是为什么在 state_dict 的输出中出现了 momentum_buffer 的原因。它记录了每个参数对应的动量缓冲区值,这些值在优化过程中会被用来更新参数。

load_state_dict()

加载状态字典信息:

optimizer = optim.SGD([weight], lr=0.1, momentum=0.9)
state_dict = torch.load(os.path.join(os.getcwd(), "optimizer_state_dict.pkl"))
 
print("state_dict before load state:\n", optimizer.state_dict())
optimizer.load_state_dict(state_dict)
print("state_dict after load state:\n", optimizer.state_dict())

在你提供的代码中:

  • torch.load() 是用来加载一个保存的对象(在这个例子中是优化器的状态字典)的 PyTorch 函数。这个函数从指定的文件中读取数据,并返回对应的 Python 对象。在这个例子中,它从文件 "optimizer_state_dict.pkl" 中加载了优化器的状态字典。

  • optimizer.load_state_dict() 是一个方法,用于将加载的状态字典应用到优化器实例上。这个方法接受一个状态字典作为输入,并将优化器的内部状态更新为该状态字典中的值。这通常用于恢复之前训练的优化器的状态,例如在继续训练或进行模型评估时。

总的来说,torch.load() 负责从文件中加载状态字典,而 optimizer.load_state_dict() 负责将加载的状态字典应用到优化器实例上。这两个步骤合起来实现了优化器状态的保存和恢复。

state_dict before load state:
 {'state': {}, 'param_groups': [{'lr': 0.1, 'momentum': 0.9, 'dampening': 0, 'weight_decay': 0,
  'nesterov': False, 'maximize': False, 'foreach': None, 'differentiable': False, 'params': [0]}]}
state_dict after load state:
 {'state': {0: {'momentum_buffer': tensor([[6.5132, 6.5132],
        [6.5132, 6.5132]])}}, 'param_groups': [{'lr': 0.1, 'momentum': 0.9, 'dampening': 0, 'weight_decay': 0, 
  'nesterov': False, 'maximize': False, 'foreach': None, 'differentiable': False, 'params': [0]}]}

torch.optim.SGD()

在PyTorch中,torch.optim.SGD是随机梯度下降(Stochastic Gradient Descent,SGD)优化器的实现。SGD是最常用的优化算法之一,它用于更新神经网络的参数以最小化损失函数。

签名

torch.optim.SGD(params, lr=<required parameter>, momentum=0, 
dampening=0, weight_decay=0, nesterov=False)

参数

  • params (iterable):需要优化的参数,通常是一个包含torch.Tensor的可迭代对象。
  • lr (float):学习率,控制参数更新的步长。
  • momentum (float, 可选):动量系数,用于加速SGD在相关方向上的收敛,并抑制震荡。
  • dampening (float, 可选):动量的阻尼系数,通常设置为0。
  • weight_decay (float, 可选):权重衰减(L2正则化)系数,用于防止过拟合。
  • nesterov (bool, 可选):是否使用Nesterov动量,它是一种改进的动量方法。

示例

import torch
import torch.optim as optim

# 创建参数张量
param_tensor = torch.randn(3, requires_grad=True)

# 初始化SGD优化器
optimizer = optim.SGD([param_tensor], lr=0.01, momentum=0.9)

# 假设我们有一个简单的损失函数
loss = param_tensor.sum()

# 反向传播计算梯度
loss.backward()

# 使用优化器更新参数
optimizer.step()

# 清除梯度,为下一轮更新做准备
optimizer.zero_grad()

在这个示例中,我们首先创建了一个需要梯度的参数张量param_tensor。然后,我们初始化了一个SGD优化器,指定了学习率和动量。接下来,我们定义了一个简单的损失函数(这里只是为了演示,实际中损失函数通常会更复杂)。通过调用loss.backward(),我们计算了损失相对于参数的梯度。使用optimizer.step()更新参数,并通过optimizer.zero_grad()清除梯度,为下一次迭代做准备。

SGD优化器在训练神经网络时非常重要,它通过不断更新参数来最小化损失函数,从而提高模型的性能。

torch.optim.SGD中,params参数接收的是一个列表(list)。这个列表中的每个元素可以是一个张量(torch.Tensor)或者一个包含张量的字典(dict)。如果是字典,它应该包含一个'params'键,其对应的值是一个张量或张量的可迭代对象,以及其他可选的优化选项,如学习率'lr'、动量'momentum'等。

例如:

import torch
import torch.optim as optim

# 创建参数张量
param1 = torch.randn(3, requires_grad=True)
param2 = torch.randn(3, requires_grad=True)

# 使用列表传递参数张量
optimizer = optim.SGD([param1, param2], lr=0.01)

# 使用包含字典的列表传递参数和学习率
optimizer = optim.SGD([
    {'params': param1, 'lr': 0.01},
    {'params': param2, 'lr': 0.001}
], lr=0.01)  # 注意:这里的lr=0.01是默认学习率,会被字典中的'lr'覆盖

在第一个例子中,params是一个包含两个张量的列表。在第二个例子中,params是一个包含两个字典的列表,每个字典指定了一组参数和该组参数的学习率。

Learning rate

学习率为 0.5

import torch
import matplotlib.pyplot as plt

# 定义损失函数,显然在 x = 0 处取得最小值
def func(x):
    return 4 * x ** 2

# 初始化x
x = torch.tensor([1.0], requires_grad=True)

# iter_rec 列表用来记录每次迭代的次数,即迭代的轮数。
# loss_rec 列表用来记录每次迭代后的损失值。
# x_rec 列表用来记录每次迭代后的x值。
iter_rec, loss_rec, x_rec = list(), list(), list()

lr = 0.5   # 学习率
max_iteration = 4  # 最大迭代次数

for i in range(max_iteration):
    y = func(x)
    y.backward() #y.backward() 执行之后,y包含了损失函数的值

    print("Iter:{}, X:{:8}, X.grad:{:8}, loss:{:10}".format(
        i, x.detach().numpy()[0], x.grad.detach().numpy()[0], y.item()))

    x_rec.append(x.item()) #x.item()表示将PyTorch张量x转换为Python标量值。

    x.data.sub_(lr * x.grad)  # x -= lr * x.grad
    x.grad.zero_()

    iter_rec.append(i)
    loss_rec.append(y.detach().numpy())

#参数121代表绘制1行2列图中左边第一列的子图
#'-ro' 是控制绘图样式的字符串参数,表示绘制红色('r')的圆点('o')连线('-')
plt.subplot(121).plot(iter_rec, loss_rec, '-ro')
plt.xlabel("Iteration")
plt.ylabel("Loss value")

x_t = torch.linspace(-3, 3, 100)
y = func(x_t)
plt.subplot(122).plot(x_t.numpy(), y.numpy(), label="y = 4*x^2")
plt.grid()
#是一个列表推导式,用于计算每个 x 值对应的 y 值。对于 x_rec 中的每个值,
# 通过 torch.tensor(i) 将其转换为张量,然后调用 func 函数计算对应的 y 值,
# 并使用 item() 方法将结果转换为标量值,最终构建一个 y 值的列表 y_rec
y_rec = [func(torch.tensor(i)).item() for i in x_rec]
#右边子图蓝色线条是y=4*x^2在(-3,3)上的函数曲线,而红色线条是损失函数值曲线
plt.subplot(122).plot(x_rec, y_rec, '-ro')
plt.legend()
plt.show()

在这里插入图片描述
在这张图像中,我们看到两个子图。左侧的子图显示的是迭代次数与损失值的关系,而右侧的子图表示了函数 y = 4*x^2 的图像以及在迭代过程中 x 的变化轨迹。

从左侧的子图可以看出,损失值(Loss value)随着迭代次数(Iteration)的变化情况。起始时损失值较低,但在迭代过程中损失值急剧上升,这表明学习率设置得太高,导致了超过最小损失点,这种现象通常称为“梯度爆炸”。

右侧的子图展示了函数 y = 4*x^2 的图形,以及在迭代过程中 x 值的变化(红色点)。起始时,x 从1开始,然后因为梯度下降算法不断更新 x 的值,试图找到损失函数的最小值。然而,由于学习率过大,x 在每次迭代后都超过了损失函数的最小值点(x=0),这导致损失值在最小值点附近震荡,而没有稳定下来。

通常,我们希望损失值随着迭代次数的增加而减少,稳步接近全局最小值,这需要通过调整学习率、使用更复杂的优化器或改变迭代策略等方法来实现。在这个例子中,减小学习率将是解决问题的直接方法。在实际应用中,找到合适的学习率是机器学习和深度学习中的重要任务之一。

学习率为 1

然后修改学习率为 1 1 1 看一看,可以看到损失函数值剧增
在这里插入图片描述

学习率为 0.1

再修改学习率为 0.1 0.1 0.1 看一看,可以看到损失函数值减小
在这里插入图片描述

设置多个学习率来观察不同 lr 对 loss 的影响

import numpy as np

iteration = 100
num_lr = 10
lr_min, lr_max = 0.01, 0.2  # .5 .3 .2
 
lr_list = np.linspace(lr_min, lr_max, num=num_lr).tolist()
loss_rec = [[] for l in range(len(lr_list))]
iter_rec = list()

for i, lr in enumerate(lr_list):
    x = torch.tensor([2.], requires_grad=True)
    for iter in range(iteration):
 
        y = func(x)
        y.backward()
        x.data.sub_(lr * x.grad)  # x.data -= x.grad
        x.grad.zero_()
 
        loss_rec[i].append(y.item())
 
for i, loss_r in enumerate(loss_rec):
    plt.plot(range(len(loss_r)), loss_r, label="LR: {}".format(lr_list[i]))
plt.legend()
plt.xlabel('Iterations')
plt.ylabel('Loss value')
plt.show()

在这里插入图片描述

StepLR

torch.optim.lr_scheduler.StepLR 是 PyTorch 中的一个学习率调整策略,它按固定间隔(步数)对学习率进行衰减。具体来说,每隔一定数量的训练轮数(epoch),学习率会乘以一个给定的衰减因子(gamma),从而实现学习率的逐步降低。

函数签名如下:

torch.optim.lr_scheduler.StepLR(optimizer, step_size, gamma=0.1, 
last_epoch=-1, verbose=False)

参数解释:

  • optimizer:绑定的优化器,学习率调整将会应用到这个优化器上。
  • step_size:学习率衰减的间隔轮数(epoch)。每隔 step_size 个 epoch,学习率就会乘以 gamma
  • gamma:学习率衰减因子,默认为0.1。学习率在每个 step_size epoch后会乘以这个因子
  • last_epoch:最后一个epoch的索引,默认为-1,表示从头开始调整学习率。如果设置为非负值,则表示从该epoch继续调整学习率。
  • verbose:如果为 True,则会在每次更新学习率时打印一条消息。

示例用法:

import torch
import torch.optim as optim
import matplotlib.pyplot as plt

torch.manual_seed(1)

LR = 0.1
iteration = 10
max_epoch = 200

weights = torch.randn((1), requires_grad=True)
target = torch.zeros((1))

optimizer = optim.SGD([weights], lr=LR, momentum=0.9)

scheduler_lr = optim.lr_scheduler.StepLR(optimizer, step_size=50, gamma=0.1)  # 设置学习率下降策略

lr_list, epoch_list = list(), list()
for epoch in range(max_epoch):

    # 获取当前lr,新版本用 get_last_lr()函数
    lr_list.append(scheduler_lr.get_last_lr())
    epoch_list.append(epoch)

    for i in range(iteration):

        loss = torch.pow((weights - target), 2)
        loss.backward()

        optimizer.step()
        optimizer.zero_grad()

    scheduler_lr.step()

plt.plot(epoch_list, lr_list, label="Step LR Scheduler")
plt.xlabel("Epoch")
plt.ylabel("Learning rate")
plt.legend()
plt.show()

要补充上述 PyTorch 代码的引用包,你需要导入 torch, torch.optim 以及用于绘图的 matplotlib.pyplot。以下是完整的代码:

import torch
import torch.optim as optim
import matplotlib.pyplot as plt

torch.manual_seed(1)

LR = 0.1
iteration = 10
max_epoch = 200

weights = torch.randn((1), requires_grad=True)
target = torch.zeros((1))

optimizer = optim.SGD([weights], lr=LR, momentum=0.9)

scheduler_lr = optim.lr_scheduler.StepLR(optimizer, step_size=50, gamma=0.1)  # 设置学习率下降策略

lr_list, epoch_list = list(), list()
for epoch in range(max_epoch):

    # 获取当前lr,新版本用 get_last_lr()函数
    lr_list.append(scheduler_lr.get_last_lr())
    epoch_list.append(epoch)

    for i in range(iteration):

        loss = torch.pow((weights - target), 2)
        loss.backward()

        optimizer.step()
        optimizer.zero_grad()

    scheduler_lr.step()#步长是 50 个epoch

plt.plot(epoch_list, lr_list, label="Step LR Scheduler")
plt.xlabel("Epoch")
plt.ylabel("Learning rate")
plt.legend()
plt.show()

这段代码首先设置了随机种子,定义了学习率、迭代次数和最大 epoch 数。然后初始化权重和目标张量,并创建了一个 SGD 优化器和一个 StepLR 学习率调度器。在训练循环中,它记录了每个 epoch 的学习率,并在每次迭代后更新权重。最后,使用 matplotlib 绘制了学习率随 epoch 变化的图。
在这里插入图片描述

MultiStepLR

torch.optim.lr_scheduler.MultiStepLR 允许在预定义的一组 epoch 时刻对学习率进行多次衰减。这与 StepLR 不同,后者在固定的间隔内进行学习率衰减。MultiStepLR 提供了更灵活的方式来根据训练进度调整学习率。

函数签名如下:

torch.optim.lr_scheduler.MultiStepLR(optimizer, milestones, 
gamma=0.1, last_epoch=-1, verbose=False)

参数解释:

  • optimizer:绑定的优化器,学习率调整将会应用到这个优化器上。
  • milestones:一个列表,包含了需要降低学习率的epoch索引。当当前epoch达到 milestones 中的任一值时,学习率会乘以 gamma
  • gamma:学习率衰减因子,默认为0.1。学习率在达到 milestones 中的任一epoch后会乘以这个因子。
  • last_epoch:最后一个epoch的索引,默认为-1,表示从头开始调整学习率。如果设置为非负值,则表示从该epoch继续调整学习率。
  • verbose:如果为 True,则会在每次更新学习率时打印一条消息。

示例用法:

import torch
import torch.optim as optim
import matplotlib.pyplot as plt

torch.manual_seed(1)

LR = 0.1
iteration = 10
max_epoch = 200

weights = torch.randn((1), requires_grad=True)
target = torch.zeros((1))

optimizer = optim.SGD([weights], lr=LR, momentum=0.9)

milestones = [50, 125, 160]
scheduler_lr = optim.lr_scheduler.MultiStepLR(optimizer, milestones=milestones, gamma=0.1)

lr_list, epoch_list = list(), list()
for epoch in range(max_epoch):

    # 获取当前lr,新版本用 get_last_lr()函数
    lr_list.append(scheduler_lr.get_lr())
    epoch_list.append(epoch)

    for i in range(iteration):

        loss = torch.pow((weights - target), 2)
        loss.backward()

        optimizer.step()
        optimizer.zero_grad()

    scheduler_lr.step()

plt.plot(epoch_list, lr_list, label="Multi Step LR Scheduler\nmilestones:{}".format(milestones))
plt.xlabel("Epoch")
plt.ylabel("Learning rate")
plt.legend()
plt.show()

在这个例子中,初始学习率为0.1,当epoch达到30、60、90时,学习率分别衰减为原来的0.1倍。

在这里插入图片描述

ExponentialLR

torch.optim.lr_scheduler.ExponentialLR 按照指数衰减的方式逐步降低学习率。具体来说,每过一个 epoch,学习率会乘以一个给定的衰减因子(gamma),从而实现学习率的指数级衰减。

函数签名如下:

torch.optim.lr_scheduler.ExponentialLR(optimizer, gamma, 
last_epoch=-1, verbose=False)

参数解释:

  • optimizer:绑定的优化器,学习率调整将会应用到这个优化器上。
  • gamma:学习率衰减因子,必须小于1。学习率在每个 epoch 后都会乘以这个因子。
  • last_epoch:最后一个epoch的索引,默认为-1,表示从头开始调整学习率。如果设置为非负值,则表示从该epoch继续调整学习率。
  • verbose:如果为 True,则会在每次更新学习率时打印一条消息。

示例用法:

import torch
import torch.optim as optim
import matplotlib.pyplot as plt

torch.manual_seed(1)


LR = 0.1
iteration = 10
max_epoch = 200

weights = torch.randn((1), requires_grad=True)
target = torch.zeros((1))

optimizer = optim.SGD([weights], lr=LR, momentum=0.9)

gamma = 0.95
scheduler_lr = optim.lr_scheduler.ExponentialLR(optimizer, gamma=gamma)
lr_list, epoch_list = list(), list()
for epoch in range(max_epoch):
 
  lr_list.append(scheduler_lr.get_lr())
  epoch_list.append(epoch)
 
  for i in range(iteration):
    loss = torch.pow((weights - target), 2)
    loss.backward()
 
    optimizer.step()
    optimizer.zero_grad()
 
  scheduler_lr.step() 
plt.plot(epoch_list, lr_list, label="Exponential  LR Scheduler\nGAMMA:{}".format(gamma))
plt.xlabel("Epoch")
plt.ylabel("Learning rate")
plt.legend()
plt.show()

在这里插入图片描述

CosineAnnealingLR

ReduceLRonPlateau

LambdaLR

Momentum

参考链接 动量梯度下降

在这里插入图片描述
在动量梯度下降中,权重更新规则通常表示为:

v t = β v t − 1 + ( 1 − β ) ∇ f ( θ t − 1 ) v_{t} = \beta v_{t-1} + (1 - \beta) \nabla f(\theta_{t-1}) vt=βvt1+(1β)f(θt1) θ t = θ t − 1 − α v t \theta_{t} = \theta_{t-1} - \alpha v_{t} θt=θt1αvt

其中, v t v_{t} vt 是当前时刻的动量项, β \beta β 是动量因子(类似于 exp_w_func 中的 beta 参数), ∇ f ( θ t − 1 ) \nabla f(\theta_{t-1}) f(θt1) 是当前参数的梯度, α \alpha α 是学习率, θ t \theta_{t} θt 是更新后的参数。

在带动量的梯度下降中,权重更新规则如下:

  1. 计算当前梯度: ∇ f ( θ t − 1 ) \nabla f(\theta_{t-1}) f(θt1)
  2. 更新动量项: v t = β v t − 1 + ( 1 − β ) ∇ f ( θ t − 1 ) v_{t} = \beta v_{t-1} + (1 - \beta) \nabla f(\theta_{t-1}) vt=βvt1+(1β)f(θt1)
  3. 更新参数: θ t = θ t − 1 − α v t \theta_{t} = \theta_{t-1} - \alpha v_{t} θt=θt1αvt

其中, β \beta β 是动量因子, α \alpha α 是学习率, v t v_{t} vt 是动量项, θ t \theta_{t} θt 是参数。

在这个过程中, β \beta β 控制了之前梯度的权重,类似于 exp_w_func 函数中计算的权重。较大的 β \beta β 值意味着之前梯度的影响更大,从而使梯度更新更加平滑。这有助于减少参数更新过程中的震荡,加快收敛速度。

具体来说,下面的 exp_w_func计算的权重可以用来直观理解动量项中不同时间点梯度的贡献随着时间的增加,旧的梯度对当前动量的贡献逐渐减小,这就是为什么动量项能够平滑梯度并提高优化效率的原因。在实际的带动量的梯度下降算法实现中,你不需要直接使用 exp_w_func 计算的权重,而是通过调整 β \beta β 参数来控制动量的衰减速率。

import numpy as np
import matplotlib.pyplot as plt

def exp_w_func(beta, time_list):
    return [(1 - beta) * np.power(beta, exp) for exp in time_list]

beta = 0.9
num_point = 100
time_list = np.arange(num_point).tolist()

weights = exp_w_func(beta, time_list)

plt.plot(time_list, weights, '-ro', label="Beta: {}\ny = B^t * (1-B)".format(beta))
plt.xlabel("time")
plt.ylabel("weight")
plt.legend()
plt.title("exponentially weighted average")
plt.show()

print(np.sum(weights))

这段代码首先定义了一个函数 exp_w_func,用于计算指数加权平均的权重。然后,它设置了 beta 值和时间点数量,生成了一个时间列表,并使用这些参数调用了 exp_w_func 函数来计算权重。最后,它使用 matplotlib 绘制了权重随时间变化的图,并打印了权重的总和。

上面的代码中,exp_w_func 里, 1 − β = 0.1 1-\beta = 0.1 1β=0.1,而 np.power(beta, exp) ( 0.9 ) e x p (0.9)^{exp} (0.9)exp 次幂乘
在这里插入图片描述
在这里插入图片描述

设置不同的 β \beta β 观察不同时期指数平均加权的权重

import numpy as np
import matplotlib.pyplot as plt

def exp_w_func(beta, time_list):
    return [(1 - beta) * np.power(beta, exp) for exp in time_list]

beta_list = [0.98, 0.95, 0.9, 0.8]
num_point = 100
time_list = np.arange(num_point).tolist()

w_list = [exp_w_func(beta, time_list) for beta in beta_list]
for i, w in enumerate(w_list):
  plt.plot(time_list, w, label="Beta: {}".format(beta_list[i]))
  plt.xlabel("time")
  plt.ylabel("weight")
plt.legend()
plt.show()

在这里插入图片描述
曲线最初以较高的权重开始,因为在时间 0 时,最近梯度的权重达到最大值,即 1 − β 1-\beta 1β

随着“时间”的推移,由于 β \beta β 提升为“时间”次方,初始权重的影响呈指数下降。 这种衰减的速率取决于 β \beta β 的值:

  • 较高的 β \beta β(例如 0.98)意味着权重下降得更慢,更加强调过去的梯度(更平滑的变化),表明平均效应较重且下降更平滑。
  • 较低的 β \beta β(例如 0.8)意味着权重下降得更快,不太重视过去的梯度,而更多地关注最近的梯度,这样可以更快地适应变化,但可能具有更大的波动性。

从图中,我们观察到以下几点:

  • 蓝线( β \beta β= 0.98)下降最慢,表明它在较长时间内为旧梯度保留了较高的权重。
  • 红线( β \beta β = 0.8)下降最快,这意味着它很快降低了旧梯度的重要性。
  • 绿色( β \beta β = 0.9)和橙色( β \beta β = 0.95)线是中间情况。

在动量梯度下降等优化算法中, β \beta β 参数控制梯度的平滑。

较高的 β \beta β 会导致更加平滑,这可以通过减少振荡并可能避免局部最小值来帮助导航优化环境。

较低的 β \beta β 可以使算法对最近的梯度变化更加敏感,这对于非平稳问题非常有用。

观察带 momentum 的 loss function 值的变化

首先观察都不带动量时,学习率分别为 0.01 和 0.03 时,损失函数的收敛情况

import torch
import torch.optim as optim
import matplotlib.pyplot as plt

def func(x):
    return torch.pow(2*x, 2)    # y = (2x)^2 = 4*x^2        dy/dx = 8x

iteration = 100
m = 0     # .9 .63

lr_list = [0.01, 0.03]

momentum_list = list()
loss_rec = [[] for l in range(len(lr_list))]
iter_rec = list()

for i, lr in enumerate(lr_list):
    x = torch.tensor([2.], requires_grad=True)

    momentum = 0. if lr == 0.03 else m
    momentum_list.append(momentum)

    optimizer = optim.SGD([x], lr=lr, momentum=momentum)

    for iter in range(iteration):

        y = func(x)
        y.backward()

        optimizer.step()
        optimizer.zero_grad()

        loss_rec[i].append(y.item())

for i, loss_r in enumerate(loss_rec):
    plt.plot(range(len(loss_r)), loss_r, label="LR: {} M:{}".format(lr_list[i], momentum_list[i]))
plt.legend()
plt.xlabel('Iterations')
plt.ylabel('Loss value')
plt.show()

在这里插入图片描述
可以看到,lr = 0.03 更快收敛,然后为学习率 0.01 添加一个 β = 0.9 \beta = 0.9 β=0.9 的动量项

import torch
import torch.optim as optim
import matplotlib.pyplot as plt

def func(x):
    return torch.pow(2*x, 2)    # y = (2x)^2 = 4*x^2    dy/dx = 8x

iteration = 100
m = 0.9     # .9 .63

lr_list = [0.01, 0.03] # 学习率

momentum_list = list()
loss_rec = [[] for l in range(len(lr_list))]
iter_rec = list()

for i, lr in enumerate(lr_list):
    x = torch.tensor([2.], requires_grad=True)

    momentum = 0. if lr == 0.03 else m
    momentum_list.append(momentum)

    optimizer = optim.SGD([x], lr=lr, momentum=momentum)

    for iter in range(iteration):

        y = func(x)
        y.backward()

        optimizer.step()
        optimizer.zero_grad()

        loss_rec[i].append(y.item())

for i, loss_r in enumerate(loss_rec):
    plt.plot(range(len(loss_r)), loss_r, label="LR: {} M:{}".format(lr_list[i], momentum_list[i]))
plt.legend()
plt.xlabel('Iterations')
plt.ylabel('Loss value')
plt.show()

在这里插入图片描述

此时 lr = 0.01 最开始比 lr = 0.03 收敛得快,但是又开始震荡, 这说明动量设置的太大了,将动量设置小一点吧,动量 momentum=0.65, 再看看效果

在这里插入图片描述
也就是说,即便学习率较小,添加适量的动量项,也可以取得较好效果

正则化

在深度学习中,什么是 Regularization?

在深度学习中,Regularization(正则化)是一种技术,用于防止模型过拟合(overfitting),即在训练数据上表现得很好,但在未见过的数据上表现较差。

通过添加一个正则化项到损失函数中,可以限制模型的复杂度,使其能够更好地泛化到新数据。常见的正则化方法有以下几种:

  1. L1 Regularization(L1 正则化):也称为 Lasso 正则化,它通过在损失函数中添加模型权重的绝对值之和来工作。这种方法可以导致稀疏权重矩阵(即很多权重为零),有助于特征选择

  2. L2 Regularization(L2 正则化):也称为 Ridge 正则化,它通过在损失函数中添加模型权重的平方和来工作。这种方法倾向于使权重均匀地接近零,而不是完全为零,从而降低模型的复杂度。

  3. Dropout:这是一种在训练过程中随机丢弃神经网络中一些节点的方法,以防止模型对训练数据的过拟合。这种方法可以看作是一种模型平均的方法,有助于提高模型的泛化能力。

  4. Early Stopping:这是一种在训练过程中监控模型在验证集上的性能,并在性能不再提升时停止训练的方法。这有助于防止模型在训练数据上过度拟合。

  5. Batch Normalization虽然它主要用于加速训练过程,但批量归一化也可以作为一种正则化手段,因为它有助于减少内部协变量偏移,从而提高模型的泛化能力。

Bias(偏差)、Error(误差)、Variance(方差),Noise(噪声)的区别和联系?

在深度学习和机器学习中,Bias(偏差)、Error(误差)、Variance(方差)、Noise(噪声)这几个概念是评估模型性能的关键指标,它们之间既有区别又有联系。

  1. Bias(偏差)

    • 偏差是指模型的预测值与真实值之间的差距,反映了模型在样本上的平均表现。高偏差意味着模型无法很好地捕捉数据的真实关系,可能是由于模型过于简单(欠拟合)导致的。
  2. Variance(方差)

    • 方差是指模型在不同数据集上的表现差异,反映了模型的稳定性。高方差意味着模型对训练数据过于敏感,容易受到数据噪声的影响,可能是由于模型过于复杂(过拟合)导致的。
  3. Noise(噪声)

    • 噪声是指数据本身的随机误差,通常是由于数据收集或测量过程中的随机因素引起的。噪声是不可避免的,对模型的预测性能有一定的影响。
  4. Error(误差)

    • 误差是指模型的预测值与真实值之间的差异,通常分为三个部分:偏差的平方、方差和噪声。即总误差 = 偏差² + 方差 + 噪声。这是模型性能评估中的一个重要概念,反映了模型在整体上的准确性。

联系

  • 偏差和方差是衡量模型误差的两个方面,它们之间通常存在权衡关系,即偏差-方差权衡(Bias-Variance Tradeoff)。为了减少总误差,需要在偏差和方差之间找到一个平衡点。
  • 噪声是数据本身的属性,影响了模型的预测性能,但它不是由模型的复杂度或简单度决定的。即使是最优模型,也无法消除噪声引起的误差。
  • 在实际应用中,目标是通过合适的模型设计和参数调整,最小化总误差,从而提高模型的预测准确性。

nn.Dropout()

nn.Dropout() 用于实现 dropout 正则化技术。Dropout 是一种在训练神经网络时常用的正则化方法,它的目的是防止模型过拟合。

在训练过程中,dropout 通过随机地丢弃(置为零)一部分神经元的输出,从而减少神经元之间的相互依赖,增强模型的泛化能力。

函数签名:

torch.nn.Dropout(p=0.5, inplace=False)

参数解释:

  • p:一个浮点数,表示丢弃神经元的概率。默认值为0.5。
  • inplace:一个布尔值,如果设置为 True,则会在原地进行操作,节省内存。默认值为 False

使用示例:

import torch
import torch.nn as nn

# 定义一个简单的模型,包含 dropout 层
class MyModel(nn.Module):
    def __init__(self):
        super(MyModel, self).__init__()
        self.linear = nn.Linear(10, 10)
        self.dropout = nn.Dropout(p=0.2)

    def forward(self, x):
        x = self.linear(x)
        x = self.dropout(x)
        return x

# 创建模型实例
model = MyModel()

# 创建一个随机输入
input = torch.randn(5, 10)

# 前向传播
output = model(input)

在这个示例中,我们定义了一个包含线性层和 dropout 层的简单模型。在前向传播过程中,dropout 层会随机地丢弃 20% 的神经元输出。

需要注意的是,dropout 通常只在训练阶段使用,在评估模型时应该禁用 dropout(即设置 model.eval())。

nn.BatchNorm1d

nn.BatchNorm1d 用于实现一维批量归一化(Batch Normalization)。批量归一化是一种用于加速深度神经网络训练的技术,它通过对每个小批量数据的输入进行归一化,使得均值接近 0,方差接近 1,从而提高训练的稳定性和收敛速度。

函数签名:

torch.nn.BatchNorm1d(num_features, eps=1e-05, momentum=0.1,
 affine=True, track_running_stats=True)

参数解释:

  • num_features:一维输入数据的特征数量。
  • eps:一个用于数值稳定性的小值,防止除以零。默认值为1e-5。
  • momentum:用于计算运行平均值和方差的动量。默认值为0.1。
  • affine:一个布尔值,如果设置为 True,则该层具有可学习的仿射参数(即缩放和偏移)。默认值为 True
  • track_running_stats:一个布尔值,如果设置为 True,则在训练过程中会跟踪运行平均值和方差;在评估模式下,会使用这些统计信息进行归一化。默认值为 True

使用示例:

import torch
import torch.nn as nn

# 定义一个简单的模型,包含一维批量归一化层
class MyModel(nn.Module):
    def __init__(self):
        super(MyModel, self).__init__()
        self.linear = nn.Linear(10, 10)
        self.bn = nn.BatchNorm1d(10)

    def forward(self, x):
        x = self.linear(x)
        x = self.bn(x)
        return x

# 创建模型实例
model = MyModel()

# 创建一个随机输入
input = torch.randn(5, 10)

# 前向传播
output = model(input)

在这个示例中,我们定义了一个包含线性层和一维批量归一化层的简单模型。在前向传播过程中,批量归一化层会对输入进行归一化处理,使得输出的均值接近 0,方差接近 1。这有助于提高模型的训练效率和稳定性。

import torch
import torch.nn as nn

m = nn.BatchNorm1d(2)
m1 = nn.BatchNorm1d(2,affine=False)
input = torch.randn(2,2)
output = m(input)
output1 = m1(input)
 
print(output,output1)
print(output.shape,output1.shape)

终端打印信息:

tensor([[ 0.9999,  0.9999],
        [-0.9999, -0.9999]], grad_fn=<NativeBatchNormBackward0>) 
tensor([[ 0.9999,  0.9999],
        [-0.9999, -0.9999]])
torch.Size([2, 2]) torch.Size([2, 2])

可以看到输出值接近于 1 和 -1,这表明输入数据经过归一化处理后,其分布的均值接近 0,方差接近 1

nn.BatchNorm2d

nn.BatchNorm2d 用于实现二维批量归一化(Batch Normalization)。它主要应用于卷积神经网络中的卷积层输出,用于对每个特征图进行归一化处理,以加速训练并提高模型的收敛速度和稳定性。

函数签名:

torch.nn.BatchNorm2d(num_features, eps=1e-05, momentum=0.1, 
affine=True, track_running_stats=True)

参数解释:

  • num_features:期望输入的特征图数量,即卷积层的输出通道数。
  • eps:一个用于数值稳定性的小值,防止除以零。默认值为1e-5。
  • momentum:用于计算运行平均值和方差的动量。默认值为0.1。
  • affine:一个布尔值,如果设置为 True,则该层具有可学习的仿射参数(即缩放和偏移)。默认值为 True
  • track_running_stats:一个布尔值,如果设置为 True,则在训练过程中会跟踪运行平均值和方差;在评估模式下,会使用这些统计信息进行归一化。默认值为 True

使用示例:

import torch
import torch.nn as nn

# 定义一个简单的卷积神经网络,包含二维批量归一化层
class MyCNN(nn.Module):
    def __init__(self):
        super(MyCNN, self).__init__()
        self.conv = nn.Conv2d(in_channels=3, out_channels=10, kernel_size=3, stride=1, padding=1)
        self.bn = nn.BatchNorm2d(10)

    def forward(self, x):
        x = self.conv(x)
        x = self.bn(x)
        return x

# 创建模型实例
model = MyCNN()

# 创建一个随机输入,假设为一个批次包含4张3通道的32x32图像
input = torch.randn(4, 3, 32, 32)

# 前向传播
output = model(input)

在这个示例中,我们定义了一个包含卷积层和二维批量归一化层的简单卷积神经网络。在前向传播过程中,批量归一化层会对每个特征图进行归一化处理,使得输出的均值接近 0,方差接近 1。这有助于提高模型的训练效率和稳定性。

nn.BatchNorm3d

nn.BatchNorm3d 用于实现三维批量归一化(Batch Normalization)。它主要应用于具有三维输入数据的场景,如三维卷积神经网络(用于处理视频或体积数据),对每个特征图进行归一化处理,以加速训练并提高模型的收敛速度和稳定性。

函数签名:

torch.nn.BatchNorm3d(num_features, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)

参数解释:

  • num_features:期望输入的特征图数量,即三维卷积层的输出通道数。
  • eps:一个用于数值稳定性的小值,防止除以零。默认值为1e-5。
  • momentum:用于计算运行平均值和方差的动量。默认值为0.1。
  • affine:一个布尔值,如果设置为 True,则该层具有可学习的仿射参数(即缩放和偏移)。默认值为 True
  • track_running_stats:一个布尔值,如果设置为 True,则在训练过程中会跟踪运行平均值和方差;在评估模式下,会使用这些统计信息进行归一化。默认值为 True

使用示例:

import torch
import torch.nn as nn

# 定义一个简单的三维卷积神经网络,包含三维批量归一化层
class My3DCNN(nn.Module):
    def __init__(self):
        super(My3DCNN, self).__init__()
        self.conv3d = nn.Conv3d(in_channels=3, out_channels=10, kernel_size=3, stride=1, padding=1)
        self.bn3d = nn.BatchNorm3d(10)

    def forward(self, x):
        x = self.conv3d(x)
        x = self.bn3d(x)
        return x

# 创建模型实例
model = My3DCNN()

# 创建一个随机输入,假设为一个批次包含4个3通道的视频,每个视频有5帧,每帧大小为32x32
input = torch.randn(4, 3, 5, 32, 32)

# 前向传播
output = model(input)

在这个示例中,我们定义了一个包含三维卷积层和三维批量归一化层的简单三维卷积神经网络。在前向传播过程中,批量归一化层会对每个特征图进行归一化处理,使得输出的均值接近 0,方差接近 1。这有助于提高模型在处理三维数据(如视频或体积数据)时的训练效率和稳定性。

BN,LN,IN,GN

在深度学习计算机视觉中,Batch Normalization、Layer Normalization、Instance Normalization和Group Normalization都是一种归一化技术,它们用于提高模型的训练速度和性能。这些技术通过规范化神经网络层的输入,有助于缓解梯度消失或梯度爆炸的问题,同时也使得模型对初始化参数和学习率的选择更加鲁棒。下面将详细解释这四种归一化方法:

  1. Batch Normalization (批归一化)

    • Batch Normalization (BN) 是一种广泛使用的归一化技术,它通过规范化每一批数据的均值和方差来加速模型的训练。
    • 在每一层的激活函数之前或之后应用,BN 通过减少内部协变量偏移(当网络的参数更新导致激活分布发生变化时)来加速训练。
    • 具体来说,对于每个特征,BN 计算该批次中该特征的均值和方差,并使用这些统计量对该特征进行归一化。然后,它使用两个可学习参数(缩放因子和偏移量)对归一化的特征进行线性变换。
  2. Layer Normalization (层归一化)

    • Layer Normalization (LN) 是一种类似于 BN 的技术,但它在每一层内部对所有激活进行归一化,而不是对每个特征在不同批次中进行归一化。
    • LN 计算每个样本在单个层内所有激活的均值和方差,并使用这些统计量对该层的激活进行归一化。
    • LN 在循环神经网络(RNN)和自注意力机制(如 Transformer)中特别有用,因为它们通常处理可变长度的输入序列,这使得批归一化难以应用。
  3. Instance Normalization (实例归一化)

    • Instance Normalization (IN) 主要用于风格迁移和生成对抗网络(GAN)中。它在每个样本和每个特征通道上独立地进行归一化。
    • 对于图像数据,IN 会计算每个样本的每个通道的均值和方差,并使用这些统计量对每个通道进行归一化。
    • IN 通过归一化每个实例的特征映射,有助于模型在保留内容信息的同时捕捉和修改图像的风格。
  4. Group Normalization (组归一化)

    • Group Normalization (GN) 是一种介于 BN 和 LN 之间的归一化技术。它将特征通道分成若干组,并在每组内部进行归一化。
    • GN 的工作原理是将每个样本的通道分成 G 组,然后计算每组内的均值和方差,用于归一化该组内的通道。
    • GN 在小批量大小的情况下表现更好,因为它不依赖于批次的统计量,这使得它在某些应用场景(如目标检测和分割)中比 BN 更受欢迎。

nn.LayerNorm

nn.LayerNorm 用于实现层归一化(Layer Normalization)。

层归一化是一种归一化技术,与批量归一化不同,它是在单个样本的所有特征上进行归一化,而不是在批次的维度上。层归一化在很多自然语言处理模型和一些深度学习模型中被广泛使用,特别是在循环神经网络(RNN)和Transformer中。

函数签名:

torch.nn.LayerNorm(normalized_shape, eps=1e-05, 
elementwise_affine=True)

参数解释:

  • normalized_shape:输入的形状,从最后一个维度开始计算。例如,如果输入是一个形状为 (batch_size, num_features) 的二维张量,那么 normalized_shape 应该是一个单元素的元组 (num_features,)
  • eps:一个用于数值稳定性的小值,防止除以零。默认值为1e-5。
  • elementwise_affine:一个布尔值,如果设置为 True,则该层具有可学习的仿射参数(即缩放和偏移)。默认值为 True

使用示例:

import torch
import torch.nn as nn

# 定义一个简单的模型,包含层归一化层
class MyModel(nn.Module):
    def __init__(self):
        super(MyModel, self).__init__()
        self.linear = nn.Linear(10, 10)
        self.ln = nn.LayerNorm(10)

    def forward(self, x):
        x = self.linear(x)
        x = self.ln(x)
        return x

# 创建模型实例
model = MyModel()

# 创建一个随机输入
input = torch.randn(5, 10)

# 前向传播
output = model(input)

在这个示例中,我们定义了一个包含线性层和层归一化层的简单模型。在前向传播过程中,层归一化层会对每个样本的所有特征进行归一化处理,使得输出的均值接近 0,方差接近 1。这有助于提高模型的训练效率和稳定性,特别是在处理序列数据时。

nn.InstanceNorm

nn.InstanceNorm 是 PyTorch 中的一组函数,包括 nn.InstanceNorm1d, nn.InstanceNorm2d, 和 nn.InstanceNorm3d,分别用于实现一维、二维和三维的实例归一化(Instance Normalization)。实例归一化是一种归一化技术,它在每个样本的每个通道上进行独立的归一化。这种方法在风格迁移和生成对抗网络(GANs)等应用中比较常见。

函数签名:

torch.nn.InstanceNorm1d(num_features, eps=1e-05, momentum=0.1, affine=False, track_running_stats=False)
torch.nn.InstanceNorm2d(num_features, eps=1e-05, momentum=0.1, affine=False, track_running_stats=False)
torch.nn.InstanceNorm3d(num_features, eps=1e-05, momentum=0.1, affine=False, track_running_stats=False)

参数解释:

  • num_features:期望输入的特征数量,即卷积层的输出通道数。
  • eps:一个用于数值稳定性的小值,防止除以零。默认值为1e-5。
  • momentum:用于计算运行平均值和方差的动量。在实例归一化中通常不使用,因此默认值通常不会被修改。
  • affine:一个布尔值,如果设置为 True,则该层具有可学习的仿射参数(即缩放和偏移)。默认值为 False
  • track_running_stats:一个布尔值,如果设置为 True,则在训练过程中会跟踪运行平均值和方差;在评估模式下,会使用这些统计信息进行归一化。默认值为 False

使用示例:

import torch
import torch.nn as nn

# 定义一个简单的模型,包含二维实例归一化层
class MyModel(nn.Module):
    def __init__(self):
        super(MyModel, self).__init__()
        self.conv = nn.Conv2d(in_channels=3, out_channels=10, kernel_size=3, stride=1, padding=1)
        self.instance_norm = nn.InstanceNorm2d(10)

    def forward(self, x):
        x = self.conv(x)
        x = self.instance_norm(x)
        return x

# 创建模型实例
model = MyModel()

# 创建一个随机输入,假设为一个批次包含4张3通道的32x32图像
input = torch.randn(4, 3, 32, 32)

# 前向传播
output = model(input)

在这个示例中,我们定义了一个包含卷积层和二维实例归一化层的简单模型。在前向传播过程中,实例归一化层会对每个样本的每个通道进行独立的归一化处理,使得输出的均值接近 0,方差接近 1。这有助于提高模型在某些特定应用中的性能,例如风格迁移和生成对抗网络。

nn.GroupNorm

nn.GroupNorm 用于实现组归一化(Group Normalization)。组归一化是一种归一化技术,它在批量归一化(Batch Normalization)和实例归一化(Instance Normalization)之间提供了一种折中方案。在组归一化中,特征通道被分成若干组,然后对每组内的特征进行归一化。这种方法不依赖于批次大小,因此适用于批次大小较小或变化的场景。

函数签名:

torch.nn.GroupNorm(num_groups, num_channels, eps=1e-05, affine=True)

参数解释:

  • num_groups:要分组的组数。例如,如果有 32 个特征通道,将 num_groups 设置为 8 意味着这 32 个通道将被分成 8 组,每组 4 个通道。
  • num_channels:输入张量的通道数。
  • eps:一个用于数值稳定性的小值,防止除以零。默认值为1e-5。
  • affine:一个布尔值,如果设置为 True,则该层具有可学习的仿射参数(即缩放和偏移)。默认值为 True

使用示例:

import torch
import torch.nn as nn

# 定义一个简单的卷积神经网络,包含组归一化层
class MyCNN(nn.Module):
    def __init__(self):
        super(MyCNN, self).__init__()
        self.conv = nn.Conv2d(in_channels=32, out_channels=64, kernel_size=3, stride=1, padding=1)
        self.gn = nn.GroupNorm(num_groups=8, num_channels=64)

    def forward(self, x):
        x = self.conv(x)
        x = self.gn(x)
        return x

# 创建模型实例
model = MyCNN()

# 创建一个随机输入
input = torch.randn(2, 32, 24, 24)

# 前向传播
output = model(input)

在这个示例中,我们定义了一个包含卷积层和组归一化层的简单卷积神经网络。在前向传播过程中,组归一化层会对卷积层的输出进行归一化处理,使得每组内的特征具有相似的分布。这有助于提高模型的训练效率和稳定性,特别是在批次大小较小或变化时。

模型保存和加载

torch.save

torch.save 是 PyTorch 中的一个函数,用于将对象(如模型的状态字典、张量等)序列化并保存到磁盘文件中。这个函数通常用于保存训练过的模型,以便之后可以重新加载模型进行推理或继续训练。

函数签名:

torch.save(obj, f, pickle_module=pickle, pickle_protocol=DEFAULT_PROTOCOL, 
_use_new_zipfile_serialization=True)

参数解释:

  • obj:要保存的对象。可以是任何可以被序列化的对象,如模型的状态字典、张量等。
  • f:一个文件名或文件对象,指定保存对象的位置。
  • pickle_module:用于序列化的 pickle 模块。默认为 pickle
  • pickle_protocol:指定 pickle 使用的协议版本。默认为 pickle.DEFAULT_PROTOCOL
  • _use_new_zipfile_serialization:一个布尔值,如果设置为 True,则使用新的 zipfile 序列化格式。默认为 True

使用示例:

import torch
import torch.nn as nn

# 定义一个简单的模型
class MyModel(nn.Module):
    def __init__(self):
        super(MyModel, self).__init__()
        self.linear = nn.Linear(10, 10)

    def forward(self, x):
        return self.linear(x)

# 创建模型实例并初始化参数
model = MyModel()

# 假设我们训练了模型,并希望保存模型的状态字典
model_state_dict = model.state_dict()

# 保存模型的状态字典到磁盘文件
torch.save(model_state_dict, 'model_state_dict.pth')

# 也可以保存整个模型
torch.save(model, 'model.pth')

在这个示例中,我们首先定义了一个简单的模型,然后创建了该模型的一个实例。假设在训练过程中模型的参数已经被更新,我们可以通过调用 model.state_dict() 获取模型的状态字典,然后使用 torch.save 函数将状态字典保存到磁盘文件中。此外,我们还可以直接保存整个模型对象。这样,我们就可以在之后的时间点重新加载模型并使用它进行推理或继续训练。

pkl和pth的区别?

.pkl.pth 是两种不同的文件扩展名,通常用于保存序列化数据。在 PyTorch 的上下文中,它们经常用于保存模型的状态字典或整个模型,但它们的使用和内部格式有所不同:

  1. .pkl (Pickle 文件):

    • .pkl 文件是使用 Python 的 pickle 模块创建的序列化文件。
    • pickle 是 Python 的一个标准库,可以序列化几乎所有的 Python 对象,包括模型、张量和其他数据结构。
    • 使用 .pkl 文件保存模型的好处是它可以保存几乎任何对象,包括一些非标准的数据结构。
    • 然而,pickle 的一个缺点是它可能不够安全(特别是当加载来自不受信任源的文件时)和不够高效(对于大型张量或模型)。
  2. .pth (PyTorch 文件):

    • .pth 文件通常是使用 PyTorch 的 torch.save 函数创建的,专门用于保存 PyTorch 对象,如模型的状态字典、张量等。
    • .pth 文件实际上是一个使用 Python 的 pickle 模块序列化的文件,但通常只用于保存 PyTorch 相关的对象。
    • 使用 .pth 文件的好处是它通常与 PyTorch 更加兼容,尤其是在保存和加载模型的状态字典时。
    • .pth 文件可能比 .pkl 文件更优化,特别是在处理大型张量或模型时。

总的来说,虽然两种文件格式在技术上都可以用于保存 PyTorch 模型,但 .pth 文件通常更适合 PyTorch 模型和张量,而 .pkl 文件可以用于保存更广泛的 Python 对象。在实践中,推荐使用 .pth 文件保存 PyTorch 模型的状态字典或整个模型,以确保最佳的兼容性和性能。

torch.load

torch.load 是 PyTorch 中的一个函数,用于从磁盘文件中加载序列化的对象,如模型的状态字典、张量等。这个函数通常与 torch.save 配合使用,以便在训练、保存模型后能够重新加载模型进行推理或继续训练。

函数签名:

torch.load(f, map_location=None, pickle_module=pickle, **pickle_load_args)

参数解释:

  • f:一个文件名或文件对象,指定要加载的序列化对象所在的位置。
  • map_location:指定如何映射存储位置。它可以是一个字符串(如 'cpu''cuda:0')、一个函数或一个字典。这对于在不同设备间迁移模型时非常有用。
  • pickle_module:用于反序列化的 pickle 模块。默认为 pickle
  • **pickle_load_args:传递给 pickle_module.load 函数的额外关键字参数。

使用示例:

import torch
import torch.nn as nn

# 定义一个简单的模型(与保存时相同的结构)
class MyModel(nn.Module):
    def __init__(self):
        super(MyModel, self).__init__()
        self.linear = nn.Linear(10, 10)

    def forward(self, x):
        return self.linear(x)

# 加载模型的状态字典
model_state_dict = torch.load('model_state_dict.pth')

# 创建模型实例
model = MyModel()

# 将加载的状态字典应用到模型
model.load_state_dict(model_state_dict)

# 如果保存的是整个模型,则可以直接加载
# model = torch.load('model.pth')

# 现在模型已经加载,可以用于推理或继续训练

在这个示例中,我们首先定义了一个与保存模型时相同结构的简单模型。然后,我们使用 torch.load 函数加载保存的模型状态字典,并将其应用到新创建的模型实例上。如果保存的是整个模型而不仅仅是状态字典,我们也可以直接使用 torch.load 函数加载整个模型。这样,我们就可以重新使用模型进行推理或继续训练。

模型断点训练

在 PyTorch 中,断点训练(Checkpoint Training)是一种在训练过程中定期保存模型状态和优化器状态的技术。这样做的目的是在训练过程中如果发生意外(如电脑突然关机、程序崩溃等),可以从最近的检查点(Checkpoint)恢复训练,而不是从头开始。这对于训练大型模型和长时间的训练尤其重要。

实现断点训练通常涉及以下几个步骤:

  1. 保存检查点: 在训练过程中的某些时刻(如每个 epoch 结束时),保存模型的状态字典和 Optimizer 的状态字典到磁盘。

  2. 加载检查点: 在训练开始时,检查是否存在检查点文件。如果存在,加载检查点文件以恢复模型状态和优化器状态。

  3. 继续训练: 从加载的检查点继续训练。

以下是一个简单的示例,展示了如何在 PyTorch 中实现断点训练:

import torch
import torch.nn as nn
import torch.optim as optim

# 定义一个简单的模型
class MyModel(nn.Module):
    def __init__(self):
        super(MyModel, self).__init__()
        self.linear = nn.Linear(10, 10)

    def forward(self, x):
        return self.linear(x)

# 创建模型和优化器
model = MyModel()
optimizer = optim.SGD(model.parameters(), lr=0.01)

# 检查是否存在检查点文件,并加载
checkpoint_path = 'checkpoint.pth'
if os.path.exists(checkpoint_path):
    checkpoint = torch.load(checkpoint_path)
    model.load_state_dict(checkpoint['model_state_dict'])
    optimizer.load_state_dict(checkpoint['optimizer_state_dict'])
    start_epoch = checkpoint['epoch']
else:
    start_epoch = 0

# 训练模型
num_epochs = 10
for epoch in range(start_epoch, num_epochs):
    # 训练模型...
    
    # 保存检查点
    torch.save({
        'epoch': epoch + 1,
        'model_state_dict': model.state_dict(),
        'optimizer_state_dict': optimizer.state_dict(),
    }, checkpoint_path)

在这个示例中,我们首先检查是否存在检查点文件。如果存在,我们加载检查点以恢复模型和优化器的状态。然后,我们从加载的检查点的 epoch 继续训练。在每个 epoch 结束时,我们保存一个包含当前 epoch、模型状态字典和优化器状态字典的检查点。这样,如果训练过程中断,我们可以从最近的检查点恢复训练,而不是从头开始。

Fine-Tuning

这段代码是一个用于微调(fine-tuning)预训练的ResNet18模型的例子。

数据集链接 dataset

该数据集训练数据有120张,验证数据有70张,训练数据太少,所以用模型重新训练可能达不到想要的效果,这里只是为了展示如何简单地实现迁移学习

微调是迁移学习的一种形式,其中一个预训练模型(通常在大型数据集上训练)被调整以适应一个新的,但相关的任务。在这个例子中,ResNet18模型被用来分类蚂蚁和蜜蜂的图像。以下是对代码的详细解释和注释:

import os
import numpy as np
import torch
import torch.nn as nn
from torch.utils.data import DataLoader
import torchvision.transforms as transforms
import torch.optim as optim
from matplotlib import pyplot as plt
import sys

# 设置路径以导入自定义模块和数据集
hello_pytorch_DIR = os.path.abspath(os.path.dirname(__file__)+os.path.sep+".."+os.path.sep+"..")
sys.path.append(hello_pytorch_DIR)

from tools.my_dataset import AntsDataset  # 导入自定义数据集
from tools.common_tools import set_seed  # 导入设置随机种子的函数
import torchvision.models as models
import torchvision

# 设置基础目录和设备(GPU或CPU)
BASEDIR = os.path.dirname(os.path.abspath(__file__))
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
print("use device :{}".format(device))

set_seed(1)  # 设置随机种子

# 设置标签名和相关参数
label_name = {"ants": 0, "bees": 1}
MAX_EPOCH = 25
BATCH_SIZE = 16
LR = 0.001
log_interval = 10
val_interval = 1
classes = 2
start_epoch = -1
lr_decay_step = 7

# 设置数据目录
data_dir = os.path.abspath(os.path.join(BASEDIR, "..", "..", "data", "07-02-数据-模型finetune"))
if not os.path.exists(data_dir):
    raise Exception("\n{} 不存在,请下载 07-02-数据-模型finetune.zip  放到\n{} 下,并解压即可".format(
        data_dir, os.path.dirname(data_dir)))

train_dir = os.path.join(data_dir, "train")
valid_dir = os.path.join(data_dir, "val")

# 设置数据预处理
norm_mean = [0.485, 0.456, 0.406]
norm_std = [0.229, 0.224, 0.225]

train_transform = transforms.Compose([
    transforms.RandomResizedCrop(224),
    transforms.RandomHorizontalFlip(),
    transforms.ToTensor(),
    transforms.Normalize(norm_mean, norm_std),
])

valid_transform = transforms.Compose([
    transforms.Resize(256),
    transforms.CenterCrop(224),
    transforms.ToTensor(),
    transforms.Normalize(norm_mean, norm_std),
])

# 构建训练和验证数据集
train_data = AntsDataset(data_dir=train_dir, transform=train_transform)
valid_data = AntsDataset(data_dir=valid_dir, transform=valid_transform)

# 构建数据加载器
train_loader = DataLoader(dataset=train_data, batch_size=BATCH_SIZE, shuffle=True)
valid_loader = DataLoader(dataset=valid_data, batch_size=BATCH_SIZE)

# 加载预训练的ResNet18模型
resnet18_ft = models.resnet18()

# 加载预训练权重(如果存在)
flag = 1
if flag:
    path_pretrained_model = os.path.join(BASEDIR, "..", "..", "data", "finetune_resnet18-5c106cde.pth")
    if not os.path.exists(path_pretrained_model):
        raise Exception("\n{} 不存在,请下载 07-02-数据-模型finetune.zip\n放到 {}下,并解压即可".format(
            path_pretrained_model, os.path.dirname(path_pretrained_model)))
    state_dict_load = torch.load(path_pretrained_model)
    resnet18_ft.load_state_dict(state_dict_load)

# 冻结卷积层(如果需要)
flag_m1 = 0
if flag_m1:
    for param in resnet18_ft.parameters():
        param.requires_grad = False

# 替换全连接层以适应新的分类任务
num_ftrs = resnet18_ft.fc.in_features
resnet18_ft.fc = nn.Linear(num_ftrs, classes)

resnet18_ft.to(device)

# 设置损失函数
criterion = nn.CrossEntropyLoss()

# 设置优化器(使用不同学习率对卷积层和全连接层)
flag = 1
if flag:
    fc_params_id = list(map(id, resnet18_ft.fc.parameters()))  # 获取全连接层参数的ID
    base_params = filter(lambda p: id(p) not in fc_params_id, resnet18_ft.parameters())  # 获取非全连接层参数
    optimizer = optim.SGD([
        {'params': base_params, 'lr': LR * 0},  # 设置卷积层学习率为0
        {'params': resnet18_ft.fc.parameters(), 'lr': LR}], momentum=0.9)  # 设置全连接层学习率
else:
    optimizer = optim.SGD(resnet18_ft.parameters(), lr=LR, momentum=0.9)  # 设置统一学习率

# 设置学习率衰减策略
scheduler = torch.optim.lr_scheduler.StepLR(optimizer, step_size=lr_decay_step, gamma=0.1)

# 训练和验证
train_curve = list()
valid_curve = list()

for epoch in range(start_epoch + 1, MAX_EPOCH):
    loss_mean = 0.
    correct = 0.
    total = 0.

    resnet18_ft.train()
    for i, data in enumerate(train_loader):
        # 前向传播
        inputs, labels = data
        inputs, labels = inputs.to(device), labels.to(device)
        outputs = resnet18_ft(inputs)

        # 反向传播和参数更新
        optimizer.zero_grad()
        loss = criterion(outputs, labels)
        loss.backward()
        optimizer.step()

        # 统计分类准确率
        _, predicted = torch.max(outputs.data, 1)
        total += labels.size(0)
        correct += (predicted == labels).squeeze().cpu().sum().numpy()

        # 打印训练信息
        loss_mean += loss.item()
        train_curve.append(loss.item())
        if (i + 1) % log_interval == 0:
            loss_mean = loss_mean / log_interval
            print("Training:Epoch[{:0>3}/{:0>3}] Iteration[{:0>3}/{:0>3}] Loss: {:.4f} Acc:{:.2%}".format(
                epoch, MAX_EPOCH, i + 1, len(train_loader), loss_mean, correct / total))
            loss_mean = 0.

    scheduler.step()  # 更新学习率

    # 验证模型
    if (epoch + 1) % val_interval == 0:
        correct_val = 0.
        total_val = 0.
        loss_val = 0.
        resnet18_ft.eval()
        with torch.no_grad():
            for j, data in enumerate(valid_loader):
                inputs, labels = data
                inputs, labels = inputs.to(device), labels.to(device)

                outputs = resnet18_ft(inputs)
                loss = criterion(outputs, labels)

                _, predicted = torch.max(outputs.data, 1)
                total_val += labels.size(0)
                correct_val += (predicted == labels).squeeze().cpu().sum().numpy()

                loss_val += loss.item()

            loss_val_mean = loss_val / len(valid_loader)
            valid_curve.append(loss_val_mean)
            print("Valid:\t Epoch[{:0>3}/{:0>3}] Iteration[{:0>3}/{:0>3}] Loss: {:.4f} Acc:{:.2%}".format(
                epoch, MAX_EPOCH, j + 1, len(valid_loader), loss_val_mean, correct_val / total_val))

# 绘制训练和验证损失曲线
train_x = range(len(train_curve))
train_y = train_curve

train_iters = len(train_loader)
valid_x = np.arange(1, len(valid_curve) + 1) * train_iters * val_interval  # 转换验证记录点到迭代次数
valid_y = valid_curve

plt.plot(train_x, train_y, label='Train')
plt.plot(valid_x, valid_y, label='Valid')

plt.legend(loc='upper right')
plt.ylabel('Loss Value')
plt.xlabel('Iteration')
plt.show()

使用 GPU

CPU 和 GPU 数据迁移

在PyTorch中,我们可以使用.to().cuda().cpu() 方法在CPU和CUDA(通常指的是GPU)之间转移数据。以下是这些函数的详细解释和示例:

1. .to()

这是一个通用的方法,用于将张量(tensor)转移到指定的设备上。这个设备可以是CPU或者CUDA(GPU)。

示例:

import torch

# 创建一个张量
x = torch.randn(2, 2)

# 将张量转移到GPU上
x_gpu = x.to('cuda')

# 将张量转回CPU上
x_cpu = x_gpu.to('cpu')

2. .cuda()

这个方法用于将张量从CPU转移到CUDA(GPU)上。如果你有多个GPU,你还可以指定要使用的GPU编号。

示例:

import torch

# 创建一个张量
x = torch.randn(2, 2)

# 将张量转移到GPU上
x_gpu = x.cuda()

# 如果你有多个GPU,可以指定GPU编号
x_gpu_1 = x.cuda(1)

3. .cpu()

这个方法用于将张量从CUDA(GPU)转回CPU上。

示例:

import torch

# 创建一个张量并转移到GPU上
x = torch.randn(2, 2).cuda()

# 将张量转回CPU上
x_cpu = x.cpu()

注意事项

  • 在使用.cuda().cpu()方法之前,你需要确保你的环境已经正确安装了CUDA,并且你的PyTorch版本是支持CUDA的。
  • 当你将张量转移到CUDA上时,PyTorch会为该张量在GPU上分配新的内存空间。因此,转移操作后你会得到一个新的张量对象,原始张量不会被修改。
  • 在多GPU环境中,使用.to()方法时,你可以通过指定device参数来选择特定的GPU,例如:x.to('cuda:1')将张量转移到编号为1的GPU上。

torch.cuda 常用的方法

torch.cuda 模块提供了一系列用于与CUDA设备(即GPU)进行交互的方法和功能。下面是一些常用的方法及其解释:

1. torch.cuda.is_available()

检查CUDA是否可用。这是在编写代码时进行设备兼容性检查的常用方法。

示例:

import torch

if torch.cuda.is_available():
    print("CUDA is available! Training on GPU.")
else:
    print("CUDA is not available. Training on CPU.")

2. torch.cuda.device_count()

返回可用的CUDA设备数量(即GPU数量)。

示例:

import torch

num_gpus = torch.cuda.device_count()
print(f"Number of available GPUs: {num_gpus}")

3. torch.cuda.current_device()

获取当前选中的CUDA设备的索引。

示例:

import torch

current_device = torch.cuda.current_device()
print(f"Current CUDA device index: {current_device}")

4. torch.cuda.get_device_name(device)

获取指定CUDA设备的名称。

示例:

import torch

device_name = torch.cuda.get_device_name(0)  # 获取第一个GPU的名称
print(f"Name of the first CUDA device: {device_name}")

5. torch.cuda.set_device(device)

设置当前使用的CUDA设备。

示例:

import torch

torch.cuda.set_device(1)  # 设置当前使用的CUDA设备为第二个GPU

6. torch.cuda.memory_allocated(device=None)

返回指定CUDA设备上已分配的内存量(以字节为单位)。

示例:

import torch

allocated_memory = torch.cuda.memory_allocated()
print(f"Memory allocated on the current CUDA device: {allocated_memory} bytes")

7. torch.cuda.memory_reserved(device=None)

返回指定CUDA设备上保留的内存量(以字节为单位)。保留的内存可能大于实际分配的内存,因为PyTorch会预留一些内存以减少内存分配和释放的开销。

示例:

import torch

reserved_memory = torch.cuda.memory_reserved()
print(f"Memory reserved on the current CUDA device: {reserved_memory} bytes")

8. torch.cuda.empty_cache()

释放当前CUDA设备上未使用的缓存内存,使其能够被其他GPU应用程序使用。这通常在你的程序占用大量GPU内存,但不再需要这么多时调用。

示例:

import torch

torch.cuda.empty_cache()

这些方法是与CUDA设备进行交互的基本工具,可以帮助你管理和优化GPU资源的使用。

torch.nn.DataParallel

torch.nn.DataParallel 是一个模块包装器,用于在多个GPU上并行地训练神经网络。当你有多个GPU时,你可以使用这个包装器来自动地将你的模型和数据分配到各个GPU上,并在训练过程中进行同步。这有助于加速训练过程。

签名

torch.nn.DataParallel(module, device_ids=None, output_device=None, 
dim=0)
  • module (torch.nn.Module): 需要并行化的模型。
  • device_ids (list of int, 可选): 要使用的CUDA设备的索引列表。默认情况下,使用所有可用的设备。
  • output_device (int, 可选): 输出数据所在的设备的索引。默认情况下,与module相同。
  • dim (int, 可选): 指定哪个维度的数据应该被分割以在不同的设备之间并行处理。通常对于批处理数据,dim=0(默认值)。

示例

假设你有一个简单的卷积神经网络模型,并且你想在两个GPU上并行地训练它:

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

# 定义一个简单的卷积神经网络
class ConvNet(nn.Module):
    def __init__(self):
        super(ConvNet, self).__init__()
        self.conv1 = nn.Conv2d(1, 10, kernel_size=5)
        self.conv2 = nn.Conv2d(10, 20, kernel_size=5)
        self.fc1 = nn.Linear(320, 50)
        self.fc2 = nn.Linear(50, 10)

    def forward(self, x):
        x = F.relu(F.max_pool2d(self.conv1(x), 2))
        x = F.relu(F.max_pool2d(self.conv2(x), 2))
        x = x.view(-1, 320)
        x = F.relu(self.fc1(x))
        x = self.fc2(x)
        return F.log_softmax(x, dim=1)

# 创建模型实例
model = ConvNet()

# 如果有多个GPU,使用DataParallel来并行化模型
if torch.cuda.device_count() > 1:
    print("Let's use", torch.cuda.device_count(), "GPUs!")
    model = nn.DataParallel(model)

# 将模型转移到GPU上(如果可用)
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
model.to(device)

在这个示例中,如果有多个GPU可用,DataParallel会自动将模型分布到这些GPU上,并在训练过程中同步更新模型的参数。这样,你可以利用多个GPU加速模型的训练。

模型剪枝

模型剪枝(Model Pruning)是一种压缩深度学习模型的技术,旨在通过移除模型中的一些参数(通常是权重)来减小模型的大小,提高推理速度,并减少计算资源的消耗。剪枝可以分为结构化剪枝和非结构化剪枝:

  1. 非结构化剪枝:在这种方法中,单个权重或者一组权重被设为零,而不考虑它们在模型中的位置。这种方法可以显著减少模型的参数数量,但可能需要专门的硬件或软件来利用稀疏性以达到加速效果。

  2. 结构化剪枝:这种方法涉及按整个结构(如卷积核、神经元或层)来移除权重。这可能不如非结构化剪枝在减少参数方面有效,但它通常能更好地利用现有的硬件加速器,从而提高推理速度。

下面是一个使用PyTorch进行非结构化剪枝的简单示例:

import torch
import torch.nn as nn
import torch.nn.utils.prune as prune

# 定义一个简单的神经网络
class Net(nn.Module):
    def __init__(self):
        super(Net, self).__init__()
        self.fc1 = nn.Linear(10, 10)
        self.fc2 = nn.Linear(10, 2)

    def forward(self, x):
        x = self.fc1(x)
        x = self.fc2(x)
        return x

# 创建模型实例
model = Net()
print("原始模型:", model)

# 对第一个全连接层应用非结构化剪枝,移除50%的权重
prune.l1_unstructured(model.fc1, name='weight', amount=0.5)

# 打印剪枝后的模型
print("剪枝后的模型:", model)

# 查看剪枝后的权重
print("剪枝后的权重:", model.fc1.weight)

在这个示例中,我们定义了一个包含两个全连接层的简单神经网络,并对第一个全连接层应用了L1非结构化剪枝,移除了50%的权重。剪枝后,你可以观察到一些权重被设置为零。这只是一个简单的示例,实际应用中可能需要更复杂的剪枝策略和技术来达到最佳的压缩和加速效果。

在这里插入图片描述

  • 28
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值