,

Introduction to PyTorch

1. QuickStart

Working Data

      PyTorch 提供了两个数据类型:torch.utils.data.DataLoadertorch.utils.data.Dataset. Dataset用来储存数据和标签(Label),DataLoader用来迭代数据。

1
2
3
4
5
import torch
from torch import nn
from torch.utils.data import DataLoader
from torchvision import datasets
from torchvision.transforms import ToTensor

PyTorch提供了数据库的接口:TorchText, TorchVision, TorchAudio. 我们将用TorchVision作为示例,它包含了真实的图片集,例如CIFAR,COCO以及其他,我们接下来使用FusionMNIST,每个数据集(Dataset)中都包含了两个参数 transform 和 target_transform 分别用来修改样本和样本标签。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#从数据集中下载训练数据
training_data = datasets.FashionMNIST(
root="data",
train=True,
download=True,
transform=ToTensor(),
)

# 从数据集中下载测试数据
test_data = datasets.FashionMNIST(
root="data",
train=False,
download=True,
transform=ToTensor(),
)

我们将Dataset作为参数传递给DataLoader,在我们的数据集上进行可迭代,并支持自动批处理、采样、洗牌和多进程数据加载。在这里,我们定义了batch_size=64 即数据加载器可迭代中的每个元素将返回一批64个特征和标签。

1
2
3
4
5
6
7
8
9
10
batch_size = 64

# Create data loaders.
train_dataloader = DataLoader(training_data, batch_size=batch_size)
test_dataloader = DataLoader(test_data, batch_size=batch_size)

for X, y in test_dataloader:
print(f"Shape of X [N, C, H, W]: {X.shape}")
print(f"Shape of y: {y.shape} {y.dtype}")
break

Creat Model

为了在PyTorch中定义神经网络,我们创建一个继承于nn.Module的类。我们在__init__函数中定义网络层,并在前向传播(forward function)中指定数据如何通过网络。为了加速神经网络的操作,我们将其移动到CUDA、MPS、MTIA或XPU等加速器,如果你的设备没有加速器,我们就使用CPU

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
import os  							# 导入操作系统接口模块,用于文件路径等操作
import torch # 导入 PyTorch 框架
from torch import nn # 从 PyTorch 中导入神经网络模块
from torch.utils.data import DataLoader # 导入数据加载器,用于批量加载数据
from torchvision import datasets, transforms # 导入常用数据集和图像预处理模块

device = torch.accelerator.current_accelerator().type if torch.accelerator.is_available() else "cpu" # 获取当前加速设备(GPU、MPS等),否则使用CPU
print(f"Using {device} device") # 打印使用的设备类型

class NeuralNetwork(nn.Module): # 定义一个继承自 nn.Module 的神经网络类
def __init__(self): # 构造函数
super().__init__() # 调用父类构造函数
self.flatten = nn.Flatten() # 定义一个 Flatten 层,将输入的图像展开为一维向量
self.linear_relu_stack = nn.Sequential( # 定义一个包含多层的顺序网络
nn.Linear(28*28, 512), # 第一个全连接层,输入大小784(28x28图像),输出512
nn.ReLU(), # ReLU激活函数
nn.Linear(512, 512), # 第二个全连接层,输入和输出都是512
nn.ReLU(), # ReLU激活函数
nn.Linear(512, 10) # 输出层,输出10类(用于MNIST手写数字识别)
)
def forward(self,x): # 前向传播函数
x = self.flatten(x) # 展平特征图
logits = self.linear_relu_stack(x) # 依次通过线性+ReLU网络
return logits # 返回未经过 softmax 的输出(logits)

model = NeuralNetwork().to(device) # 创建模型实例,并移动到计算设备(CPU/GPU)
print(model) # 打印模型结构

Optimizing model parameters

 要训练一个模型,我们需要一个损失函数(loss function)和一个优化器(optimizer)

1
2
lose_fn = nn.CrossEntropyLoss() # 用交叉熵损失
optimizer = torch.optim.SGD(model.parameters(), lr=1e-3) # 使用随机梯度下降优化器,lr表示学习率

在单个训练循环中,模型对训练数据集进行预测,并反向传播预测误差以调整模型的参数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
def train(dataloader, model, loss_fn, optimizer):
size = len(dataloader.dataset) # 训练集的总样本数
model.train() # 设置模型为训练模式

for batch, (X, y) in enumerate(dataloader): # 遍历每一个 batch
X, y = X.to(device), y.to(device) # 将数据移到指定设备(CPU 或 GPU)

# 前向传播,计算预测值和损失
pred = model(X) # 得到模型预测结果
loss = loss_fn(pred, y) # 计算当前 batch 的损失

# 反向传播和参数更新
loss.backward() # 反向传播,计算梯度
optimizer.step() # 更新模型参数
optimizer.zero_grad() # 清除累积梯度,防止梯度混叠

# 每隔100个 batch 打印一次训练进度
if batch % 100 == 0:
loss_val = loss.item() # 获取当前损失的标量值
current = (batch + 1) * len(X) # 当前处理的数据量
print(f"loss: {loss_val:>7f} [{current:>5d}/{size:>5d}]")

我们接下来在测试集上测试我们训练模型的性能:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def test(dataloader, model, loss_fn):
size = len(dataloader.dataset) # 测试集总样本数量
num_batches = len(dataloader) # 批次数量
model.eval() # 将模型设置为评估模式(不启用dropout等)
test_loss, correct = 0, 0 # 初始化测试损失和预测正确的样本数

with torch.no_grad(): # 禁用梯度计算,提高测试效率
for X, y in dataloader: # 遍历测试数据的每一个 batch
X, y = X.to(device), y.to(device) # 将输入和标签移动到指定设备(CPU/GPU)
pred = model(X) # 模型前向传播,得到预测结果
test_loss += loss_fn(pred, y).item() # 累加当前 batch 的损失
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") # 打印测试结果

培训过程在几个epoches进行。在每个epoch中,模型都会学习参数,以做出更好的预测。我们在每个epoch打印模型的准确性和损失;我们希望看到每个epoch准确性的提高和损失的减少。

1
2
3
4
5
6
epochs = 5
for t in range(epochs):
print(f"Epoch {t+1}\n-------------------------------")
train(train_dataloader, model, loss_fn, optimizer)
test(test_dataloader, model, loss_fn)
print("Done!")

Save Model

我们首先来保存模型

1
2
torch.save(model.state_dict(),"model.pth")
print("Saved PyTorch Model State to model.pth")

再尝试加载模型:

1
2
model = NeuralNetwork().to(device)
model.load_state_dict(torch.load("model.pth", weights_only=True))

用我们自己训练的模型来预测类别:

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
classes = [                                  # 类别标签,对应 FashionMNIST 的 10 类
"T-shirt/top",
"Trouser",
"Pullover",
"Dress",
"Coat",
"Sandal",
"Shirt",
"Sneaker",
"Bag",
"Ankle boot",
]

model.eval() # 设置模型为评估模式(不启用 dropout/BN 等)

x, y = test_data[0][0], test_data[0][1] # 取测试集中第 0 个样本的图像和标签

with torch.no_grad(): # 禁用梯度计算,节省资源
x = x.to(device) # 将图像移到指定设备(GPU 或 CPU)
pred = model(x) # 前向传播,获得模型输出(logits)

predicted = classes[pred[0].argmax(0)] # 取概率最大对应的类别作为预测结果
actual = classes[y] # 获取实际标签对应的类别名称

print(f'Predicted: "{predicted}", Actual: "{actual}"') # 打印预测结果
  • to(device)什么时候用?
    • 凡是需要计算的时候都要使用。

2. Tensors

张量(Tensor)是一种专门的数据结构,与数组和矩阵非常相似。在PyTorch中,我们使用张量对模型的输入和输出以及模型的参数进行编码.张量与NumPy的ndarrays相似,只是张量可以在GPU或其他硬件加速器上运行。事实上,张量和NumPy数组通常可以共享相同的底层内存。

1
2
3
# 导入torch和numpy包
import torch
import numpy as np

Tensor的初始化方式:

  1. 直接从数据创建,自动推导数据类型
1
2
data = [[1, 2],[3, 4]]
x_data = torch.tensor(data)
  1. 从Numpy数据类型推导
1
2
np_array = np.array(data)
x_np = torch.from_numpy(np_array)
  1. 从其他Tensor推导,直接保留原来Tensor的形状和类型
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. 随机或者常量生成

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}")

Tensor的属性

Tensor 的属性包括形状、类型、运行设备

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}")

Tensor 的操作

这里全面描述了1200多个张量运算,包括算术、线性代数、矩阵操作(转置、索引、切片)、采样等。这些操作中的每一个都可以在CPU和加速器上运行,如CUDA、MPS、MTIA或XPU。默认情况下,张量是在CPU上创建的。我们需要使用.to方法(在检查加速器可用性后)明确地将张量移动到加速器上。这种跨设备复制大型张量在时间和内存方面可能很昂贵!

1
2
3
# We move our tensor to the current accelerator if available
if torch.accelerator.is_available():
tensor = tensor.to(torch.accelerator.current_accelerator())
  1. 索引和切片(indexing and slicing**)**
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. 连接张量(joining Tensor),可以使用torch.cat沿着给定维度串联一系列张量。另一个张量连接运算符torch.stack,与torch.cat有微妙的不同。
1
2
t1 = torch.cat([tensor, tensor, tensor], dim=1)
print(t1)

dim=1 表示沿着第1维度(列方向,注意维度从0开始)进行拼接

假设原始 tensor 的形状是 (3, 4):

  • 拼接前:三个 (3, 4) 的张量
  • 拼接后:t1 的形状将是 (3, 12),因为第1维度的大小是 4+4+4=12

如果是 dim=0,则会在第0维度(行方向)进行拼接,结果形状会是 (9, 4)。

  1. 算术运算(Arithmetic operations**)**
1
2
3
4
5
6
7
8
9
10
11
12
# 下面计算了矩阵y1,y2,y3之间的乘法
# ``tensor.T`` 表示转置矩阵
y1 = tensor @ tensor.T
print(f"y1: {y1}")

y2 = tensor.matmul(tensor.T)
print(f"y2: {y2}")

y3 = torch.rand_like(y1)
print(f"y3:{y3}")

torch.matmul(tensor, tensor.T,out=y3)
1
2
3
4
5
6
7
8
9
10
11
# 下面是元素的乘法,矩阵相同位置的元素进行相乘
z1 = tensor*tensor
print(f"z1: {z1}")

z2 = tensor.mul(tensor)
print(f"z2: {z2}")

z3 = torch.rand_like(z1)
print(f"z3: {z3}")

torch.mul(tensor,tensor,out=z3)
  1. 单元素张量(single-element tensors) 如果您有一个单元素张量,例如通过将张量的所有值汇总为一个值,您可以使用item()将其转换为Python数值:
1
2
3
agg = tensor.sum()  # sum() 表示对张量求和
agg_item = agg.item()
print(agg_item,type(agg_item))

5.** In-place operations** 将结果存储到操作数中的操作称为就地操作。它们用后缀_表示。例如:x.copy_(y),x.t_()

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

Tensors and Numpy

Tensor 和 Numpy 共享内存,它们指向同一个数据,改变一个的同时会改变另一个

  1. Tensor to Numpy
1
2
3
4
t = torch.ones(5)
print(f"t: {t}")
n = t.numpy()
print(f"n: {n}")
  1. Numpy to Tensor
1
2
n = np.ones(5)
t = torch.from_numpy(n)
  1. 改变Numpy也会改变Tensor
1
2
3
np.add(n, 1, out=n)
print(f"t: {t}")
print(f"n: {n}")

Datasets and DataLoaders

前面我们已经简单介绍过这个例子,在这一部分,我们进一步学习。

1
2
3
4
5
6
# 导入模块
import torch
from torch.utils.data import Dataset
from torchvision import datasets
from torchvision.transform import ToTensor
import matlibplot.pyplot as plt
1
# 数据集下载

root: 下载数据的存储路径
train: 选择训练数据还是测试数据
download:如果root下没有数据,则从网络中下载
transform and target_transform :特征和标签的转化

1
2
3
4
5
6
7
8
9
10
11
12
13
14

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

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

利用matplotlib来可视化一些数据集中的样本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
labels_map = {
0: "T-Shirt",
1: "Trouser",
2: "Pullover",
3: "Dress",
4: "Coat",
5: "Sandal",
6: "Shirt",
7: "Sneaker",
8: "Bag",
9: "Ankle Boot",
}
figure = plt.figure(figsize=(8, 8))
cols, rows = 3, 3
for i in range(1, cols * rows + 1):
sample_idx = torch.randint(len(training_data), size=(1,)).item()
img, label = training_data[sample_idx]
figure.add_subplot(rows, cols, i)
plt.title(labels_map[label])
plt.axis("off")
plt.imshow(img.squeeze(), cmap="gray")
plt.show()

创建一个定义的数据集合

自定义数据集类必须实现三个函数:initlen__和__getitem。FashionMNIST图像存储在目录img_dir中,它们的标签单独存储在CSV文件notonations_file中,我们逐步解释每个函数的作用。

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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
import os
import pandas as pd
from torchvision.io import decode_image

class CustomImageDataset(Dataset):
"""
自定义图像数据集类,用于加载图像及其对应的标签
继承自PyTorch的Dataset类
"""

def __init__(self, annotations_file, img_dir, transform=None, target_transform=None):
"""
初始化数据集

参数:
annotations_file (str): 包含图像文件名和标签的CSV文件路径
img_dir (str): 存储所有图像的目录路径
transform (callable, 可选): 应用于图像的可选变换函数
target_transform (callable, 可选): 应用于标签的可选变换函数
CSV格式:
tshirt1.jpg, 0
tshirt2.jpg, 0
......
ankleboot999.jpg, 9
"""
# 读取包含图像文件名和标签的CSV文件到pandas DataFrame
self.img_labels = pd.read_csv(annotations_file)
# 存储图像所在目录的路径
self.img_dir = img_dir
# 存储图像和标签的转换函数
self.transform = transform
self.target_transform = target_transform

def __len__(self):
"""
返回数据集中的样本总数

返回:
int: 数据集中的样本数量
"""
return len(self.img_labels)

def __getitem__(self, idx):
"""
加载并返回给定索引处的样本(图像和标签)

参数:
idx (int): 要获取的样本索引

返回:
tuple: (图像, 标签)
图像是转换后的图像张量
标签是对应的标签(可能经过转换)
"""
# 通过将图像目录与CSV中的文件名拼接,获取图像完整路径
img_path = os.path.join(self.img_dir, self.img_labels.iloc[idx, 0])
# 读取并将图像文件解码为张量
image = decode_image(img_path)
# 从CSV中获取对应的标签
label = self.img_labels.iloc[idx, 1]

# 如果指定了图像变换,则应用
if self.transform:
image = self.transform(image)
# 如果指定了标签变换,则应用
if self.target_transform:
label = self.target_transform(label)

return image, label

Preparing data for training with DataLoaders

Dataset检索我们数据集的特征,并一次标记一个样本。在训练模型时,我们通常希望在”minibatches”中传递样本,在每个epoch重新打乱数据以减少模型过度拟合,并使用Python的multiprocessing来加快数据检索速度。

1
2
3
4
5
# DataLoader API
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)

Iterate through the DataLoader

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 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()}")

# 获取批次中的第一个图像并去除多余的维度(squeeze()移除大小为1的维度)
img = train_features[0].squeeze() # 从[1,28,28]变为[28,28](灰度图情况)

# 获取对应的第一个标签
label = train_labels[0]

# 使用matplotlib显示图像,cmap="gray"表示以灰度显示
plt.imshow(img, cmap="gray")
# 显示图形窗口
plt.show()

# 打印该图像对应的标签
print(f"Label: {label}")

Transforms

       数据并不总是以训练机器学习算法所需的最终处理形式出现。我们使用Tranforms来对数据进行一些操作,并使其适合训练。所有TorchVision数据集都有两个参数——用于修改特征的transform和用于修改标签的target_transform。torchvision.transforms模块开箱即用提供了几种常用的变换。

       FashionMNIST功能采用PIL图像格式,标签为整数。对于训练,我们需要将特征作为归一化张量,将标签作为one-hot编码张量。为了进行这些转换,我们使用ToTensor和Lambda。

1
2
3
4
5
6
7
8
9
10
11
import torch
from torchvision import datasets
from torchvision.transforms import ToTensor, Lambda

ds = datasets.FashionMNIST(
root="data",
train=True,
download=True,
transform=ToTensor(),
target_transform=Lambda(lambda y: torch.zeros(10, dtype=torch.float).scatter_(0, torch.tensor(y), value=1))
)

ToTensor将PIL图像或NumPy ndarray转换为FloatTensor。并将图像的像素强度值缩放在[0., 1.]范围内。

target_transform = Lambda( # 使用Lambda定义一个匿名函数作为transform
lambda y: # 输入y是原始标签(假设是0-9的整数)
torch.zeros(10, dtype=torch.float) # 先创建一个全0的张量,长度为10(对应10个类别)
.scatter_( # 使用scatter_方法按索引填充1
0, # 沿第0维度操作(即对一维张量)
torch.tensor(y), # 将y转换为张量作为索引位置
value=1 # 在索引位置填充值为1
)
)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

**输入**:整数标签 `y`(例如 `y=2`)。

**输出**:长度为10的one-hot张量,在位置 `y` 处为1,其余为0。

例如,`y=2` → `[0, 0, 1, 0, 0, 0, 0, 0, 0, 0]`。

## Build the Neural Network
        神经网络由对数据执行操作的层/模块组成。Torch.nn命名空间提供了构建自己的神经网络所需的所有构建块。PyTorch中的每个模块都是nn.Module的子类。神经网络是一个由其他模块(层)组成的模块本身。这种嵌套结构允许轻松构建和管理复杂的架构。在接下来的部分中,我们将构建一个神经网络,对FashionMNIST数据集中的图像进行分类。

```python
import os
import torch
from torch import nn
from torch.utils.data import DataLoader
from torchvision import datasets, transforms

Get Device for Training

1
2
device = torch.accelerator.current_accelerator().type if torch.accelerator.is_available() else "cpu"
print(f"Using {device} device")

Define the class

我们通过子类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):  # 定义神经网络类,继承自nn.Module
def __init__(self): # 初始化函数
super().__init__() # 调用父类构造函数
self.flatten = nn.Flatten() # 创建展平层,将二维图像展平为一维向量
self.linear_relu_stack = nn.Sequential( # 创建顺序容器,包含多个线性层和激活函数
nn.Linear(28*28, 512), # 第一全连接层:输入28*28=784维,输出512维
nn.ReLU(), # ReLU激活函数
nn.Linear(512, 512), # 第二全连接层:输入512维,输出512维
nn.ReLU(), # ReLU激活函数
nn.Linear(512, 10), # 输出层:输入512维,输出10维(对应10个类别)
)

def forward(self, x): # 前向传播函数
x = self.flatten(x) # 将输入图像展平
logits = self.linear_relu_stack(x) # 通过线性层和激活函数堆栈
return logits # 返回未归一化的预测值(logits)

创建一个对象,并且转移到device上运算:

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

输入数据,模型返回一个二维张量,其中dim=0对应于每个类的10个原始预测值的输出,dim=1对应于每个输出的单个值。我们通过nn.Softmax模块的实例获得预测概率。

1
2
3
4
5
6
7
8
9
X = torch.rand(1, 28, 28, device=device)  # 生成一个随机28x28的测试图像张量(批量大小为1),并放到指定设备(CPU/GPU)上

logits = model(X) # 将输入X传入神经网络模型,得到未归一化的预测输出(logits)

pred_probab = nn.Softmax(dim=1)(logits) # 对logits在维度1(类别维度)上应用Softmax,得到概率分布

y_pred = pred_probab.argmax(1) # 取概率最大的类别作为预测结果(返回类别索引)

print(f"Predicted class: {y_pred}") # 打印预测的类别编号

Model Layers

我们假设一个图片的大小:

1
2
input_image = torch.rand(3,28,28)
print(input_image.size())

nn.Flatten

我们初始化nn.Flatten层,将每个2D 28x28图像转换为784像素值的连续数组。

1
2
3
flatten = nn.Flatten()
flat_image = flatten(input_image)
print(flat_image.size())

nn.Linear

线性层是一个使用其存储的权重和偏置对输入应用线性变换的模块。

1
2
3
layer1 = nn.Linear(in_features=28*28, out_features=20)
hidden1 = layer1(flat_image)
print(hidden1.size())

nn.ReLU

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

1
2
3
print(f"Before ReLU: {hidden1}\n\n")
hidden1 = nn.ReLU()(hidden1)
print(f"After ReLU: {hidden1}")

nn.Sequential

nn.Sequential是一个有序的模块容器。数据按照定义的相同顺序通过所有模块。您可以使用顺序容器来组装一个像seq_modules这样的快速网络。

1
2
3
4
5
6
7
8
9
seq_modules = nn.Sequential(
flatten,
layer1,
nn.ReLU(),
nn.Linear(20, 10)
)
input_image = torch.rand(3,28,28)
logits = seq_modules(input_image)
print(logits)

nn.Softmax

神经网络的最后一个线性层返回 logits,在[-infty,infty]的原始值——传递给nn.Softmax模块。对数被缩放为值[0, 1],表示模型对每个类的预测概率。dim参数表示值必须相加为1的维度。

1
2
3
softmax = nn.Softmax(dim=1)
pred_probab = softmax(logits)
print(pred_probab)

Model Parameters

神经网络中的许多层都是参数化的,即在训练期间优化了相关的权重和偏差。子类nn.Module自动跟踪nn.Module对象中定义的所有字段,并使用模型的parameters()或name_parameters()方法访问所有参数。

1
2
3
4
print(f"Model structure: {model}\n\n")

for name, param in model.named_parameters():
print(f"Layer: {name} | Size: {param.size()} | Values : {param[:2]} \n")

Automatic Differentiation with torch.autograd

在训练神经网络时,最常用的算法是反向传播(back propagation)。在这个算法中,参数(模型权重)根据损失函数相对于给定参数的梯度(gradient)进行调整。为了计算这些梯度,PyTorch有一个名为torch.autograd的内置微分引擎。它支持自动计算任何计算图的梯度。考虑最简单的单层神经网络,具有输入x、参数w和b以及一些损失函数。它可以在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)

在这个网络中,w和b是我们需要优化的参数。因此,我们需要能够根据这些变量计算损失函数的梯度。为了做到这一点,我们设置了这些张量的requires_grad属性。

我们应用于张量来构造计算图的函数实际上是类函数的对象。这个对象知道如何向前方向计算函数,以及如何在向后传播步骤中计算其导数。向后传播函数的引用存储在张量的grad_fn属性中。

1
2
print(f"Gradient function for z = {z.grad_fn}")
print(f"Gradient function for loss = {loss.grad_fn}")

计算导数:

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

默认情况下,所有requires_grad=True的张量都在跟踪其计算历史并支持梯度计算。然而,在某些情况下,我们不需要这样做,例如,当我们训练了模型,只想将其应用于一些输入数据时,即我们只想通过网络进行正向计算。我们可以通过使用torch.no_grad()块包围我们的计算代码来停止跟踪计算,另一种方法是使用detach()函数:

1
2
3
4
5
6
7
8
9
10
z = torch.matmul(x, w)+b
print(z.requires_grad)

with torch.no_grad():
z = torch.matmul(x, w)+b
print(z.requires_grad)

z = torch.matmul(x,w)+b
z_det = z.detach()
print(z_det.requires_grad)

Tensor Gradients and Jacobian Products

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 创建一个4x5的单位矩阵张量,并启用梯度跟踪
inp = torch.eye(4, 5, requires_grad=True) # 形状(4,5),对角线为1,其余为0

# 对输入进行变换操作:(inp+1) → 平方 → 转置 → 得到输出(形状变为5x4)
out = (inp+1).pow(2).t() # 正向传播计算图构建

# 第一次反向传播:用全1梯度矩阵回传(形状需与out一致)
out.backward(torch.ones_like(out), retain_graph=True) # retain_graph保留计算图
print(f"First call\n{inp.grad}") # 打印第一次梯度

# 第二次反向传播:梯度会累加到第一次结果上
out.backward(torch.ones_like(out), retain_graph=True)
print(f"\nSecond call\n{inp.grad}") # 打印第二次梯度(值是第一次的两倍)

# 清空梯度缓冲区
inp.grad.zero_() # 将inp.grad重置为0

# 第三次反向传播:重新计算梯度
out.backward(torch.ones_like(out), retain_graph=True)
print(f"\nCall after zeroing gradients\n{inp.grad}") # 打印清零后的梯度(与第一次相同)

Optimizing Model Parameters

我们现在可以通过数据集来对我们自己构建模型的参数进行训练,我们前面构建的模型如下:

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
40
41
import torch
from torch import nn
from torch.utils.data import DataLoader
from torchvision import datasets
from torchvision.transforms import ToTensor

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

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

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

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

model = NeuralNetwork()

超参数:

1
2
3
4
```
Epoch-在数据集中迭代的次数
Batch size-在参数更新之前通过网络传播的数据样本数量
Learning Rate-每个batch/epoch更新多少模型参数。较小的值会导致学习速度变慢,而较大的值可能会导致训练期间的不可预测行为。

learning_rate = 1e-3
batch_size = 64
epochs = 5

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

**Optimization loop:**

一旦我们设置了超参数,我们就可以用循环来训练和优化我们的模型。循环的每一次迭代都被称为epoch。每个epoch由两个主要部分组成:

- Train loop——迭代训练数据集,并尝试收敛到最佳参数。
- The validation/Test loop——迭代测试数据集,以检查模型性能是否在提高。

**Loss Function:**

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

常见的损失函数包括用于回归任务的nn.MSELoss(均方误差)和用于分类的nn.NLLLoss(负对数似然)。nn.CrossEntropyLoss结合了nn.LogSoftmax和nn.NLLLoss。

```python
# Initialize the loss function
loss_fn = nn.CrossEntropyLoss()

Optimizer:

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

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

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

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

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

具体实现如下:

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
40
41
42
43
44
45
46
47
48
49
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")

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!")

Save and Load the Model

  1. model weigth的方式

model weights通过state_dict的方式save:

1
2
3
4
import torch
import torchvision.models as models
model = models.vgg16(weights='IMAGENET1K_V1')
torch.save(model.state_dict(), 'model_weights.pth')

Load model weights你必须先创建一个相同结构的模型,再使用load_**state_dict()**的方式:

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', weights_only=True))
model.eval()
  1. model shapes的方式

保存模型

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

加载模型

1
model = torch.load('model.pth', weights_only=False)