pytorch基础!🍧

Pytorch基础

1.Tensors张量

张量是一种专门的数据结构,与数组和矩阵非常相似。在 PyTorch 中,我们使用张量对模型的输入和输出以及模型的参数进行编码。

张量类似于 NumPy 的 ndarrays,不同之处在于张量可以在 GPU 或其他硬件加速器上运行。事实上,张量和 NumPy 数组通常可以共享相同的底层内存,从而消除了复制数据的需要。

1.1 初始化张量

1
2
import torch
import numpy as np

张量可以通过多种方式进行初始化。

直接来自数据

1
2
data = [[1, 2],[3, 4]]
x_data = torch.tensor(data)

从 NumPy 数组

1
2
np_array = np.array(data)
x_np = torch.from_numpy(np_array)

从另一个张量

新张量保留参数张量的属性(形状、数据类型),除非显式覆盖。

1
2
3
4
5
x_ones = torch.ones_like(x_data) # retains the properties of x_data
print(f"Ones Tensor: \n {x_ones} \n")

x_rand = torch.rand_like(x_data, dtype=torch.float) # overrides the datatype of x_data
print(f"Random Tensor: \n {x_rand} \n")

输出:

1
2
3
4
5
6
7
Ones Tensor:
tensor([[1, 1],
[1, 1]])

Random Tensor:
tensor([[0.8823, 0.9150],
[0.3829, 0.9593]])

使用随机值或常量值

shape 是张量维度的元组。在下面的函数中,它决定了输出张量的维数。

1
2
3
4
5
6
7
8
shape = (2,3,)
rand_tensor = torch.rand(shape)
ones_tensor = torch.ones(shape)
zeros_tensor = torch.zeros(shape)

print(f"Random Tensor: \n {rand_tensor} \n")
print(f"Ones Tensor: \n {ones_tensor} \n")
print(f"Zeros Tensor: \n {zeros_tensor}")

tips:在给定形状(shape)时,逗号在 Python 中通常用于表示元组。在这种情况下,shape = (2, 3,) 中的逗号实际上是一个元组的标志,即使在没有逗号的情况下,它也是一个合法的元组。

这种写法是为了确保在定义元组时即使只有一个元素也使用逗号,以避免与普通的括号运算符产生歧义。例如,如果写成 shape = (2, 3),它将被解释为一个包含两个整数的表达式,而不是一个包含一个元组的表达式。

在 Python 中,单个元素的元组需要在元素后面添加逗号,以明确表示这是一个元组。这是为了区分元组和括号内的表达式。所以,(2, 3,)(2, 3) 在这里是等价的,都表示一个包含两个整数的元组。

输出:

1
2
3
4
5
6
7
8
9
10
11
Random Tensor:
tensor([[0.3904, 0.6009, 0.2566],
[0.7936, 0.9408, 0.1332]])

Ones Tensor:
tensor([[1., 1., 1.],
[1., 1., 1.]])

Zeros Tensor:
tensor([[0., 0., 0.],
[0., 0., 0.]])

1.2 张量的属性

张量属性描述它们的形状、数据类型和存储它们的设备。

1
2
3
4
5
tensor = torch.rand(3,4)

print(f"Shape of tensor: {tensor.shape}")
print(f"Datatype of tensor: {tensor.dtype}")
print(f"Device tensor is stored on: {tensor.device}")

输出:

1
2
3
Shape of tensor: torch.Size([3, 4])
Datatype of tensor: torch.float32
Device tensor is stored on: cpu

1.3 张量操作

https://pytorch.org/docs/stable/torch.html

这里全面描述了 100 多种张量运算,包括算术、线性代数、矩阵操作(转置、索引、切片)、采样等。这些操作中的每一个都可以在 GPU 上运行(速度通常高于在 CPU 上)。

默认情况下,张量是在 CPU 上创建的。我们需要显式地将张量移动到 GPU using .to 方法(在检查 GPU 可用性之后)。

1
2
3
# We move our tensor to the GPU if available
if torch.cuda.is_available():
tensor = tensor.to("cuda")

索引和切片

1
2
3
4
5
6
tensor = torch.ones(4, 4)
print(f"First row: {tensor[0]}")
print(f"First column: {tensor[:, 0]}")
print(f"Last column: {tensor[..., -1]}")
tensor[:,1] = 0
print(tensor)

输出:

1
2
3
4
5
6
7
First row: tensor([1., 1., 1., 1.])
First column: tensor([1., 1., 1., 1.])
Last column: tensor([1., 1., 1., 1.])
tensor([[1., 0., 1., 1.],
[1., 0., 1., 1.],
[1., 0., 1., 1.],
[1., 0., 1., 1.]])

tips:省略符号的作用是省略掉其余的维度。例如:

1
2
3
4
tensor = torch.rand((2, 3, 3,4))
print(tensor)
print(f"{tensor[:,:,:,1]}")
print(f"{tensor[..., 1]}")

...等价于:,:,:

连接张量 可用于 torch.cat 沿给定维度连接一系列张量。

1
2
t1 = torch.cat([tensor, tensor, tensor], dim=1)
print(t1)

输出:

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

tips:dim表示连接的维度。

切片操作:

1
2
3
4
5
6
7
8
9
10
11
12
import torch

# 假设 pos_embedding 是一个形状为 (5, 6) 的张量
pos_embedding = torch.arange(30).reshape(5, 6)

# 对 pos_embedding 进行切片操作
sliced_pos_embedding = pos_embedding[:, 0::2]

print("原始张量:")
print(pos_embedding)
print("切片后的张量:")
print(sliced_pos_embedding)

输出结果为:

1
2
3
4
5
6
7
8
9
10
11
12
原始张量:
tensor([[ 0, 1, 2, 3, 4, 5],
[ 6, 7, 8, 9, 10, 11],
[12, 13, 14, 15, 16, 17],
[18, 19, 20, 21, 22, 23],
[24, 25, 26, 27, 28, 29]])
切片后的张量:
tensor([[ 0, 2, 4],
[ 6, 8, 10],
[12, 14, 16],
[18, 20, 22],
[24, 26, 28]])

tips:

若在维度上使用0:3:2,表示从索引 0 开始到索引 3 结束。举例:

1
2
3
4
5
6
7
sequence = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

# 对 sequence 进行切片操作
sliced_sequence = sequence[0:3:2]

print("原始序列:", sequence)
print("切片后的序列:", sliced_sequence)

输出结果为:

1
2
原始序列: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
切片后的序列: [0, 2]

算术运算

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# This computes the matrix multiplication between two tensors. y1, y2, y3 will have the same value
# ``tensor.T`` returns the transpose of a tensor 转置矩阵
y1 = tensor @ tensor.T
y2 = tensor.matmul(tensor.T)

y3 = torch.rand_like(y1)
torch.matmul(tensor, tensor.T, out=y3)


# This computes the element-wise product. z1, z2, z3 will have the same value
z1 = tensor * tensor
z2 = tensor.mul(tensor)

z3 = torch.rand_like(tensor)
torch.mul(tensor, tensor, out=z3)
  1. 矩阵乘法(Matrix Multiplication)
    • y1 = tensor @ tensor.T:这是Python中使用@运算符进行矩阵乘法的简洁写法,它计算了tensor与其转置的矩阵相乘,并将结果存储在y1中。
    • y2 = tensor.matmul(tensor.T):这是torch.Tensor类的matmul方法的调用方式,实现了与@运算符相同的功能,将两个张量相乘。
    • y3 = torch.rand_like(y1)torch.matmul(tensor, tensor.T, out=y3):这两行代码将矩阵乘法的结果存储在预先分配的张量y3中。
  2. 元素级乘法(Element-wise Multiplication)
    • z1 = tensor * tensor:这是Python中进行元素级乘法的简洁写法,它将tensor中的每个元素与另一个tensor对应位置的元素相乘,并将结果存储在z1中。
    • z2 = tensor.mul(tensor):这是torch.Tensor类的mul方法的调用方式,实现了与*运算符相同的功能,进行元素级乘法。
    • z3 = torch.rand_like(tensor)torch.mul(tensor, tensor, out=z3):这两行代码将元素级乘法的结果存储在预先分配的张量z3中。

单元素张量

如果你有一个单元素张量,例如通过将张量的所有值聚合为一个值,你可以使用以下命令 item() 将其转换为 Python 数值:

1
2
3
agg = tensor.sum()
agg_item = agg.item()
print(agg_item, type(agg_item))

输出:

1
12.0 <class 'float'>

tips:agg也是一个张量,为单元素张量。

就地操作

将结果存储到操作数中的操作称为就地操作。它们由 _ 后缀表示。例如: x.copy_(y) 、、 x.t_() 将更改 x

1
2
3
print(f"{tensor} \n")
tensor.add_(5)
print(tensor)

输出:

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

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

常见的PyTorch就地操作:

  1. **add_、sub_、mul_、div_**:
    • add_():就地执行张量的加法。
    • sub_():就地执行张量的减法。
    • mul_():就地执行张量的乘法。
    • div_():就地执行张量的除法。
  2. 其他数学函数
    • abs_():就地执行张量的绝对值操作。
    • neg_():就地执行张量的取负操作。
    • pow_():就地执行张量的指数操作。
    • clamp_():就地执行张量的截断操作。
  3. 归约操作
    • sum_():就地计算张量的元素之和。
    • mean_():就地计算张量的平均值。
    • max_():就地计算张量的最大值。
    • min_():就地计算张量的最小值。
  4. 其他操作
    • fill_():用指定的标量值填充张量。
    • zero_():将张量的所有元素设置为0。
    • fill_diagonal_():将张量的对角线元素填充为指定值。

这些就地操作都是在函数名后面添加下划线_来表示的,例如add_()mul_()等。在使用时需要小心,因为它们会直接修改原始的张量,可能会导致不可预测的结果或难以调试的错误。

1.4 使用 NumPy 桥接

CPU 上的张量和 NumPy 数组可以共享其底层内存位置,更改一个将更改另一个。

Tensor 到 NumPy 数组

1
2
3
4
t = torch.ones(5)
print(f"t: {t}")
n = t.numpy()
print(f"n: {n}")

输出:

1
2
t: tensor([1., 1., 1., 1., 1.])
n: [1. 1. 1. 1. 1.]

张量的变化反映在 NumPy 数组中。

1
2
3
t.add_(1)
print(f"t: {t}")
print(f"n: {n}")

输出:

1
2
t: tensor([2., 2., 2., 2., 2.])
n: [2. 2. 2. 2. 2.]

NumPy 数组转 Tensor

1
2
n = np.ones(5)
t = torch.from_numpy(n)

NumPy 数组中的更改会反映在张量中。

1
2
3
np.add(n, 1, out=n)
print(f"t: {t}")
print(f"n: {n}")

输出:

1
2
t: tensor([2., 2., 2., 2., 2.], dtype=torch.float64)
n: [2. 2. 2. 2. 2.]

2.Datasets & DataLoaders 数据集和数据加载器

用于处理数据样本的代码可能会变得混乱且难以维护;理想情况下,我们希望我们的数据集代码与模型训练代码解耦,以获得更好的可读性和模块化。PyTorch 提供了两个数据原语: torch.utils.data.DataLoadertorch.utils.data.Dataset 允许你使用预加载的数据集以及你自己的数据。

Dataset 存储样本及其相应的标签,而 DataLoader 则在 Dataset 周围封装了一个可迭代器,以方便访问样本。

2.1 加载数据集

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import torch
from torch.utils.data import Dataset
from torchvision import datasets
from torchvision.transforms import ToTensor
import matplotlib.pyplot as plt


training_data = datasets.FashionMNIST(
root="data",
train=True,
download=True,
transform=ToTensor()
)

test_data = datasets.FashionMNIST(
root="data",
train=False,
download=True,
transform=ToTensor()
)
  • root 是存储训练/测试数据的路径,
  • train 指定训练或测试数据集,
  • download=True 如果数据在 上不可用 root ,则从 Internet 下载数据。
  • transformtarget_transform 指定要素和标注转换

2.2 数据加载器

检索 Dataset 数据集的特征,并一次标记一个样本。在训练模型时,我们通常希望以“小批量”的方式传递样本,在每个时期重新洗牌数据以减少模型过拟合,并使用 Python multiprocessing 来加快数据检索速度。

DataLoader 是一个可迭代的对象,它通过一个简单的 API 为我们抽象了这种复杂性。

1
2
3
4
from torch.utils.data import DataLoader

train_dataloader = DataLoader(training_data, batch_size=64, shuffle=True)
test_dataloader = DataLoader(test_data, batch_size=64, shuffle=True)

我们已将该数据集加载到DataLoader中,并可以根据需要遍历该数据集。下面的每次迭代都会返回一批 train_features and train_labels (分别包含 batch_size=64 特征和标签)。因为我们指定 shuffle=True 了 ,在我们遍历所有批次后,数据会被洗牌。

1
2
3
4
5
6
7
8
9
# Display image and label.
train_features, train_labels = next(iter(train_dataloader))
print(f"Feature batch shape: {train_features.size()}")
print(f"Labels batch shape: {train_labels.size()}")
img = train_features[0].squeeze()
label = train_labels[0]
plt.imshow(img, cmap="gray")
plt.show()
print(f"Label: {label}")

tips:tensor.shape和tensor.size()获取到的内容相同,但一个是属性,一个是方法。可以把tensor看成是一个类。

3.构建神经网络

神经网络由对数据执行操作的层/模块组成。torch.nn 命名空间提供了构建自己的神经网络所需的所有构建块。PyTorch 中的每个模块都对 nn.模块。神经网络本身是由其他模块(层)组成的模块。这种嵌套结构允许轻松构建和管理复杂的架构。

3.1 获取用于训练的设备

1
2
3
4
5
6
7
8
device = (
"cuda"
if torch.cuda.is_available()
else "mps"
if torch.backends.mps.is_available()
else "cpu"
)
print(f"Using {device} device")

3.2 定义类

我们通过子类化来定义我们的神经网络 nn.Module ,并在 __init__ 中初始化神经网络层。每个 nn.Module 子类都实现对方法中输入数据的 forward 操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class NeuralNetwork(nn.Module):
def __init__(self):
super().__init__()
self.flatten = nn.Flatten()
self.linear_relu_stack = nn.Sequential(
nn.Linear(28*28, 512),
nn.ReLU(),
nn.Linear(512, 512),
nn.ReLU(),
nn.Linear(512, 10),
)

def forward(self, x):
x = self.flatten(x)
logits = self.linear_relu_stack(x)
return logits

我们创建一个 NeuralNetwork 的实例,并将其移动到 device 中,并打印其结构。

1
2
model = NeuralNetwork().to(device)
print(model)

结果:

1
2
3
4
5
6
7
8
9
10
NeuralNetwork(
(flatten): Flatten(start_dim=1, end_dim=-1)
(linear_relu_stack): Sequential(
(0): Linear(in_features=784, out_features=512, bias=True)
(1): ReLU()
(2): Linear(in_features=512, out_features=512, bias=True)
(3): ReLU()
(4): Linear(in_features=512, out_features=10, bias=True)
)
)

为了使用模型,我们将输入数据传递给它。这将执行模型的 forward ,以及一些后台操作。不要直接调用model.forward()

tips:

  • 在 PyTorch 中,实现了 nn.Module 的子类中的 forward 方法是一个特殊的约定。当您调用模型的实例(例如 model)时,PyTorch 会自动调用 forward 方法,而不需要显式地调用 model.forward()
  • 这是因为torch.nn.Module类中已经定义了__call__方法,而该方法内部实际上会调用forward()方法。
  • 在Python中,__call__是一个特殊方法,允许类的实例像函数一样被调用。

下面是一个简单的例子,演示了如何使用__call__方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
class Multiplier:
def __init__(self, factor):
self.factor = factor

def __call__(self, x):
return self.factor * x

# 创建一个Multiplier实例
double = Multiplier(2)

# 使用__call__方法调用实例,实际上就像调用一个函数一样
result = double(5) # 相当于调用了double.__call__(5)
print(result) # 输出 10

在输入上调用模型将返回一个二维张量,其中 dim=0 对应于每个类的 10 个原始预测值的每个输出,dim=1 对应于每个输出的单个值。我们通过传递模块的 nn.Softmax 实例来获取预测概率。

1
2
3
4
5
X = torch.rand(1, 28, 28, device=device)
logits = model(X)
pred_probab = nn.Softmax(dim=1)(logits)
y_pred = pred_probab.argmax(1)
print(f"Predicted class: {y_pred}")[]()

结果:

1
Predicted class: tensor([7], device='cuda:0')

tips:

  1. tensor.argmax(1) 是对 PyTorch 张量进行操作,用于沿指定维度找到张量中最大值的索引。参数是0表示沿着列找最大值,1表示沿着行找最大值。
  2. nn.Softmax(dim=1) 是PyTorch等深度学习框架中常用的函数。该函数用于沿着指定的维度计算张量的 softmax 激活。在这里,dim=1 表示 softmax 沿着输入张量的第二个维度(从 0 开始索引)进行操作。

3.2 模型层

nn.Flatten

nn.Flatten 是 PyTorch 中的一个层(Layer),用于将输入的多维张量(例如,具有多个轴或维度的张量)转换为一个具有单个轴的张量(通常是一维张量)。其作用是将输入的数据“展平”成一个一维向量,以便于后续的神经网络层(如全连接层)处理。

1
2
3
4
5
6
7
8
9
10
11
import torch
import torch.nn as nn

input_tensor = torch.randn(3, 2, 2,2)
print(input_tensor)
# 定义一个 Flatten 层
flatten_layer = nn.Flatten()

# 使用 Flatten 层将输入张量展平
output = flatten_layer(input_tensor)
print(output)

结果:

在许多情况下,当将卷积层的输出传递给全连接层时,需要使用 nn.Flatten 来将卷积层的输出展平为一维张量,以便于后续的全连接层处理。

nn.Linear

nn.Linear 是 PyTorch 中的一个线性层(Linear Layer),也称为全连接层(Fully Connected Layer)或仿射层(Affine Layer)。这个层将输入张量与权重矩阵相乘,然后加上偏置向量(可选),最后应用激活函数(也可选)。

在神经网络中,全连接层通常用于将输入数据与权重相乘,并加上偏置,从而产生新的特征表示,这些特征表示被传递给下一层。全连接层的作用是将输入数据映射到输出空间中。

以下是 nn.Linear 的基本用法示例:

1
2
3
linear_layer = nn.Linear(8, 4)
output = linear_layer(output)
print(output)

结果:

在训练神经网络时,权重矩阵和偏置向量是可学习的参数,它们会根据反向传播算法进行优化,以最小化损失函数。

tips:

权重矩阵和偏置向量是随机的。

nn.ReLU

非线性激活是在模型的输入和输出之间创建复杂映射的原因。它们在线性变换后应用以引入非线性,帮助神经网络学习各种现象。

1
2
output = nn.ReLU()(output)
print(output)

结果:

tips:

  • ReLU(Rectified Linear Unit)是一种常用的非线性激活函数,被广泛应用于深度神经网络中。ReLU函数定义为:f(x)=max(0,x),即,当输入 x 大于等于0时,ReLU函数返回输入 x;当输入 x 小于0时,ReLU函数返回0。如上图结果所示。
  • 常见的非线性激活函数包括:
  1. Sigmoid函数:将输入映射到0到1之间的连续范围,常用于输出层的二分类问题。
  2. Tanh函数:类似于Sigmoid函数,但将输入映射到-1到1之间的连续范围,也常用于隐藏层。
  3. ReLU(Rectified Linear Unit)函数:对于正数输入,输出等于输入;对于负数输入,输出为0。ReLU函数在深度学习中得到了广泛应用,因为它的计算简单且在训练过程中可以加速收敛。
  4. Leaky ReLU函数:与ReLU类似,但对负数输入有小的线性斜率,可以避免ReLU中的“死亡神经元”问题。
  5. Softmax函数:常用于多分类问题的输出层,将输入转换成一个概率分布,使得输出的所有值都在0到1之间且总和为1。

nn.Sequential

nn.Sequential 是模块的有序容器。数据以与定义的相同的顺序传递到所有模块。

1
2
3
4
5
6
linear_relu_stack = nn.Sequential(
nn.Flatten(),
nn.Linear(8, 4),
nn.ReLU(),
)
output2 = linear_relu_stack(input_tensor)

效果与之前的定义相同。

nn.Softmax

Softmax函数是一种常用的激活函数,通常用于多分类问题的输出层,将原始的网络输出转换为表示概率分布的形式。Softmax函数将输入向量 z 的每个元素转换为一个介于0和1之间的实数,同时确保所有元素的总和为1,因此可以看作是对输入向量的归一化。

1
2
output = nn.Softmax(dim=1)(output)
print(output)

结果:

tips:

dim为1表示行的和为1,dim为0表示列的和为 1。

3.3 模型参数

神经网络中的许多层都是参数化的,即具有相关的权重和偏差,这些权重和偏差在训练过程中得到优化。子类 nn.Module 会自动跟踪模型对象中定义的所有字段,并使所有参数都可以使用模型 parameters()named_parameters() 方法访问。

1
2
3
4
5
6
7
8
9
10
11
12
13
input = torch.tensor([[1.0, 2.0, 3.0],
[4.0, 5.0, 6.0]]) # 2个样本,3个特征

# 定义权重矩阵和偏置向量
weight = torch.tensor([[0.1, 0.2, 0.3], # 2个输出特征,每个特征对应3个输入特征
[0.4, 0.5, 0.6]])
bias = torch.tensor([0.1, 0.2]) # 2个输出特征,每个特征都有一个偏置

# 执行线性变换操作
output = F.linear(input, weight, bias)

print("Output after linear transformation:")
print(output)

结果:

tips:

偏差会加在每一行上。

4.torch.autograd自动微分

在训练神经网络时,最常用的算法是反向传播。在该算法中,参数(模型权重)根据损失函数相对于给定参数的梯度进行调整。

为了计算这些梯度,PyTorch 有一个内置的微分引擎,称为 torch.autograd 。它支持自动计算任何计算图的梯度。

考虑最简单的单层神经网络,具有输入 x 、参数 wb 和一些损失函数。可以在 PyTorch 中按以下方式定义它:

1
2
3
4
5
6
7
8
import torch

x = torch.ones(5) # input tensor
y = torch.zeros(3) # expected output
w = torch.randn(5, 3, requires_grad=True)
b = torch.randn(3, requires_grad=True)
z = torch.matmul(x, w)+b
loss = torch.nn.functional.binary_cross_entropy_with_logits(z, y)

PyTorch中实现自动计算梯度的机制是通过动态计算图实现的。当你在PyTorch中定义张量并进行操作时,PyTorch会构建一个计算图,该计算图描述了数据流经过的操作,并且知道每个操作涉及的张量之间的依赖关系。这个计算图是动态的,因为它在每次执行时都会重新构建。

此代码定义以下计算图:

为了优化神经网络中参数的权重,我们需要计算损失函数相对于参数的导数。为了计算这些导数,我们调用 loss.backward() ,然后从 w.gradb.grad 中检索值:

1
2
3
loss.backward()
print(w.grad)
print(b.grad)

简而言之,PyTorch实现自动计算梯度的主要步骤如下:

  1. 定义张量,并在需要计算梯度的张量上设置requires_grad=True
  2. 执行计算操作,PyTorch会跟踪这些操作并构建计算图。
  3. 当需要计算梯度时,调用.backward()方法。PyTorch会根据计算图自动计算梯度,并将梯度累积到相应的张量的.grad属性中。

tips:

我们只能获取计算图的叶节点的属性,这些节点的 grad requires_grad 属性设置为 True 。对于图中的所有其他节点,梯度将不可用。

5.优化模型参数

现在我们有了模型和数据,是时候通过优化模型的参数来训练、验证和测试我们的模型了。训练模型是一个迭代过程;在每次迭代中,模型对输出进行猜测,计算其猜测中的误差(损失),收集误差相对于其参数的导数(如我们在上一节中看到的),并使用梯度下降优化这些参数

5.1 超参数

超参数是可调整的参数,可用于控制模型优化过程。不同的超参数值会影响模型训练和收敛率

我们定义以下用于训练的超参数:

  • Number of Epochs - 遍历数据集的次数
  • Batch Size 批量大小 - 在更新参数之前通过网络传播的数据样本数,参数.grad会累计一个batch的梯度
  • Learning Rate 学习率 - 在每个批次/周期更新模型参数的程度。较小的值会导致学习速度较慢,而较大的值可能会导致训练期间出现不可预测的行为。

5.2 优化循环

一旦我们设置了超参数,我们就可以使用优化循环来训练和优化我们的模型。优化循环的每次迭代称为一个纪元。

每个纪元由两个主要部分组成:

  • The Train Loop 训练循环 - 遍历训练数据集并尝试收敛到最佳参数。
  • **The Validation/Test Loop **验证/测试循环 - 遍历测试数据集,以检查模型性能是否正在提高。

5.3 损失函数

当呈现一些训练数据时,我们未经训练的网络可能不会给出正确的答案。损失函数衡量获得的结果与目标值的差异程度,它是我们在训练过程中想要最小化的损失函数。为了计算损失,我们使用给定数据样本的输入进行预测,并将其与真实数据标签值进行比较。

5.4 优化

优化是调整模型参数以减少每个训练步骤中的模型误差的过程。优化算法定义了此过程的执行方式。所有优化逻辑都封装在对象中 optimizer 。在这里,我们使用 SGD 优化器;此外,PyTorch 中还有许多不同的优化器,例如 ADAM 和 RMSProp,它们更适合不同类型的模型和数据。

我们通过注册需要训练的模型参数并传入学习率超参数来初始化优化器。

1
optimizer = torch.optim.SGD(model.parameters(), lr=learning_rate)

在训练循环中,优化分三个步骤进行:

  • 调用 optimizer.zero_grad() 以重置模型参数的梯度。默认情况下,渐变相加;为了防止重复计算,我们在每次迭代时都明确地将它们归零。
  • 通过调用 loss.backward() 反向传播预测损失。PyTorch 将损失的梯度与每个参数交汇。
  • 一旦我们有了梯度,我们就会调用 optimizer.step() 通过向后传递中收集的梯度来调整参数。

tips:

model.parameters()为每一层的权重和偏差。比如上述例子中的w,b

举例:

5.5 全面实施

我们在优化代码上定义 train_loop 循环,并 test_loop 根据我们的测试数据评估模型的性能。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
def train_loop(dataloader, model, loss_fn, optimizer):
size = len(dataloader.dataset)
# Set the model to training mode - important for batch normalization and dropout layers
# Unnecessary in this situation but added for best practices
model.train()
for batch, (X, y) in enumerate(dataloader):
# Compute prediction and loss
pred = model(X)
loss = loss_fn(pred, y)

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

if batch % 100 == 0:
loss, current = loss.item(), batch * batch_size + len(X)
print(f"loss: {loss:>7f} [{current:>5d}/{size:>5d}]")


def test_loop(dataloader, model, loss_fn):
# Set the model to evaluation mode - important for batch normalization and dropout layers
# Unnecessary in this situation but added for best practices
model.eval()
size = len(dataloader.dataset)
num_batches = len(dataloader)
test_loss, correct = 0, 0

# Evaluating the model with torch.no_grad() ensures that no gradients are computed during test mode
# also serves to reduce unnecessary gradient computations and memory usage for tensors with requires_grad=True
with torch.no_grad():
for X, y in dataloader:
pred = model(X)
test_loss += loss_fn(pred, y).item()
correct += (pred.argmax(1) == y).type(torch.float).sum().item()

test_loss /= num_batches
correct /= size
print(f"Test Error: \n Accuracy: {(100*correct):>0.1f}%, Avg loss: {test_loss:>8f} \n")

我们初始化损失函数和优化器,并将其传递给 train_looptest_loop 。随意增加 epoch 的数量以跟踪模型的改进性能。

1
2
3
4
5
6
7
8
9
loss_fn = nn.CrossEntropyLoss()
optimizer = torch.optim.SGD(model.parameters(), lr=learning_rate)

epochs = 10
for t in range(epochs):
print(f"Epoch {t+1}\n-------------------------------")
train_loop(train_dataloader, model, loss_fn, optimizer)
test_loop(test_dataloader, model, loss_fn)
print("Done!")

tips:

model.train()model.eval() 是 PyTorch 中用于控制模型模式的两个方法,它们的主要区别在于模型处于不同的运行模式,具体如下:

  1. model.train(): 调用 model.train() 将模型设置为训练模式。在训练模式下,模型中的一些特定层,比如 dropout 和 batch normalization,会以不同的方式处理输入数据。例如,dropout 在训练时会随机丢弃部分节点,以防止过拟合;而 batch normalization 在训练时会根据当前 mini-batch 的统计数据来标准化输入数据。因此,在训练模式下,这些层会执行相应的训练操作。
  2. model.eval(): 调用 model.eval() 将模型设置为评估模式。在评估模式下,模型的行为会发生变化。例如,dropout 层不再随机丢弃节点,而是将所有节点保留,以便获取更加稳定的预测结果;batch normalization 也会使用固定的统计数据进行标准化,而不是使用当前 mini-batch 的统计数据。评估模式下,模型的行为更接近于实际使用场景。

总的来说,model.train() 将模型设置为训练模式,用于训练过程中;model.eval() 将模型设置为评估模式,用于测试、验证或推断过程中,以获得更稳定和可靠的输出结果。

5.6 保存并加载模型

在本节中,我们将了解如何通过保存、加载和运行模型预测来持久化模型状态。

1
2
import torch
import torchvision.models as models

保存和加载模型权重

PyTorch 模型将学习到的参数存储在名为 state_dict 的内部状态字典中。这些可以通过以下 torch.save 方法持久化:

1
2
model = models.vgg16(weights='IMAGENET1K_V1')
torch.save(model.state_dict(), 'model_weights.pth')

要加载模型权重,您需要先创建同一模型的实例,然后使用 load_state_dict() method 加载参数。

1
2
3
model = models.vgg16() # we do not specify ``weights``, i.e. create untrained model
model.load_state_dict(torch.load('model_weights.pth'))
model.eval()

tips:

请务必在推理前调用 model.eval() 方法,将 dropout 和 Batch 归一化层设置为评估模式。如果不这样做,将产生不一致的推理结果。

Saving and Loading Models with Shapes

在加载模型权重时,我们需要先实例化模型类,因为该类定义了网络的结构。我们可能希望将此类的结构与模型一起保存,在这种情况下,我们可以传递model(而不是 model.state_dict() )到保存函数:

1
torch.save(model, 'model.pth')

然后,我们可以像这样加载模型:

1
model = torch.load('model.pth')

pytorch基础!🍧
https://yangchuanzhi20.github.io/2024/01/24/人工智能/Pytorch/基础知识/pytorch基础/
作者
白色很哇塞
发布于
2024年1月24日
许可协议