目录
1. 学习基础知识
大多数机器学习工作流程涉及处理数据、创建模型、优化模型参数和保存训练好的模型。本教程向你介绍一个用PyTorch实现的完整的ML工作流程,并提供链接来了解这些概念中的每一个。
我们将使用FashionMNIST数据集来训练一个神经网络,预测输入图像是否属于以下类别之一。T恤/上衣、长裤、套头衫、连衣裙、外套、凉鞋、衬衫、运动鞋、包、或踝靴。
本教程假定对Python和深度学习概念有基本的熟悉。
1.1运行教程代码
你可以用几种方式运行这个教程。
在云中。这是最简单的入门方法 每个部分的顶部都有一个 "在Microsoft Learn中运行 "的链接,它可以在完全托管的环境中打开Microsoft Learn中的集成笔记本和代码。
在本地。这个选项要求您首先在本地机器上设置PyTorch和TorchVision(安装说明)。下载笔记本或将代码复制到你喜欢的IDE中。
1.2如何使用本指南
如果您熟悉其他深度学习框架,请先查看
0.快速入门Quickstart — PyTorch Tutorials 1.11.0+cu102 documentation
快速熟悉PyTorch的API。
如果你是深度学习框架的新手,请直接进入我们分步指南的第一部分。
0. Quickstart
1. Tensors
3. Transforms
4. Build Model
2.快速启动
本节介绍了机器学习中常见任务的API。请参考每一节中的链接来深入了解。
2.1与数据打交道
PyTorch有两个处理数据的基元:Torch.utils.data.DataLoader和Torch.utils.data.Dataset。Dataset存储了样本及其相应的标签,而DataLoader则围绕Dataset包装了一个可迭代的数据。
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 数据集。
torchvision.datasets模块包含了许多真实世界的视觉数据的数据集对象,如CIFAR、COCO(完整列表在这里)。在本教程中,我们使用FashionMNIST数据集。每个TorchVision数据集都包括两个参数:transform和target_transform,分别用来修改样本和标签。
# Download training data from open datasets.
training_data = datasets.FashionMNIST(
root="data",
train=True,
download=True,
transform=ToTensor(),
)
# Download test data from open datasets.
test_data = datasets.FashionMNIST(
root="data",
train=False,
download=True,
transform=ToTensor(),
)
我们将数据集作为参数传递给DataLoader。这在我们的数据集上包裹了一个可迭代的数据集,并支持自动批处理、采样、洗牌和多进程数据加载。在这里,我们定义了一个64的批处理大小,即dataloader可迭代的每个元素将返回64个特征和标签的批次。
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
阅读更多关于在PyTorch中加载数据的信息。
2.2建立模型
为了在PyTorch中定义一个神经网络,我们创建一个继承自nn.Module的类。我们在__init__函数中定义网络的层,并在forward函数中指定数据将如何通过网络。为了加速神经网络的操作,如果有条件的话,我们把它移到GPU上。
# 获取 cpu 或 gpu 设备进行训练。
device = "cuda" if torch.cuda.is_available() else "cpu"
print(f"Using {device} device")
# 定义模型
class NeuralNetwork(nn.Module):
def __init__(self):
super(NeuralNetwork, self).__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().to(device)
print(model)
阅读有关在 PyTorch 中构建神经网络的更多信息。
2.3优化模型参数
loss_fn = nn.CrossEntropyLoss()
optimizer = torch.optim.SGD(model.parameters(), lr=1e-3)
在一个单一的训练循环中,模型对训练数据集(分批送入)进行预测,并通过反向传播预测误差来调整模型的参数。
def train(dataloader, model, loss_fn, optimizer):
size = len(dataloader.dataset)
model.train()
for batch, (X, y) in enumerate(dataloader):
X, y = X.to(device), y.to(device)
# Compute prediction error
pred = model(X)
loss = loss_fn(pred, y)
# Backpropagation
optimizer.zero_grad()
loss.backward()
optimizer.step()
if batch % 100 == 0:
loss, current = loss.item(), batch * len(X)
print(f"loss: {loss:>7f} [{current:>5d}/{size:>5d}]")
我们还根据测试数据集检查模型的性能,以确保它在学习。
def test(dataloader, model, loss_fn):
size = len(dataloader.dataset)
num_batches = len(dataloader)
model.eval()
test_loss, correct = 0, 0
with torch.no_grad():
for X, y in dataloader:
X, y = X.to(device), y.to(device)
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")
训练过程是通过几个迭代(epochs)进行的。在每个历时中,模型学习参数以做出更好的预测。我们在每个历时中打印模型的准确度和损失;我们希望看到准确度在每个历时中增加,损失在每个历时中减少。
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!")
阅读更多关于培训你的模型。
2.4保存模型
保存模型的一个常见方法是序列化内部状态字典(包含模型参数)。
torch.save(model.state_dict(), "model.pth")
print("Saved PyTorch Model State to model.pth")
2.5装载模型
加载模型的过程包括重新创建模型结构并将状态字典加载到其中。
model = NeuralNetwork()
model.load_state_dict(torch.load("model.pth"))
这个模型现在可以用来进行预测。
classes = [
"T-shirt/top",
"Trouser",
"Pullover",
"Dress",
"Coat",
"Sandal",
"Shirt",
"Sneaker",
"Bag",
"Ankle boot",
]
model.eval()
x, y = test_data[0][0], test_data[0][1]
with torch.no_grad():
pred = model(x)
predicted, actual = classes[pred[0].argmax(0)], classes[y]
print(f'Predicted: "{predicted}", Actual: "{actual}"')
阅读更多关于保存和加载你的模型。
3.张量
张量是一种专门的数据结构,与数组和矩阵非常相似。在PyTorch中,我们使用张量来编码一个模型的输入和输出,以及模型的参数。
张量类似于NumPy的ndarrays,只是张量可以在GPU或其他硬件加速器上运行。事实上,张量和NumPy数组通常可以共享相同的底层内存,不需要复制数据(参见与NumPy的桥接)。张量还为自动分化进行了优化(我们将在后面的Autograd部分看到更多关于这一点)。如果你熟悉ndarrays,你就会对张量API很熟悉了。如果没有,请跟上!
import torch
import numpy as np
3.1初始化一个张量
张量可以通过各种方式进行初始化。请看下面的例子。
3.1.1直接来自于数据
张量可以直接从数据中创建。数据类型是自动推断出来的。
data = [[1, 2],[3, 4]]
x_data = torch.tensor(data)
3.1.2从一个NumPy数组
张量可以从NumPy数组中创建(反之亦然--见Bridge with NumPy)。
np_array = np.array(data)
x_np = torch.from_numpy(np_array)
3.1.3来自另一个张量。
新的张量保留了参数张量的属性(形状、数据类型),除非明确重写。
x_ones = torch.ones_like(x_data) # 保留了x_data的属性
print(f"Ones Tensor: \n {x_ones} \n")
x_rand = torch.rand_like(x_data, dtype=torch.float) #覆盖了x_data的数据类型
print(f"Random Tensor: \n {x_rand} \n")
3.1.4使用随机或恒定值:
shape是一个张量的元组。在下面的函数中,它决定了输出张量的维度。
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}")
3.2张量的属性
张量属性描述了它们的形状、数据类型以及存储它们的设备。
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}")
3.3对张量的操作
这里全面介绍了100多种张量操作,包括算术、线性代数、矩阵操作(转置、索引、切片)、采样等。
这些操作中的每一个都可以在GPU上运行(速度通常比在CPU上高)。如果你使用Colab,通过进入Runtime > Change runtime type > GPU来分配一个GPU。
默认情况下,张量是在CPU上创建的。我们需要使用.to方法明确地将张量移动到GPU上(在检查GPU的可用性之后)。请记住,在不同的设备上复制大的张量,在时间和内存上都是很昂贵的!
# We move our tensor to the GPU if available
if torch.cuda.is_available():
tensor = tensor.to("cuda")
尝试一下列表中的一些操作。如果你熟悉NumPy API,你会发现Tensor API使用起来很容易。
3.3.1标准的类似numpy的索引和切片。
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)
3.3.2连接张量
你可以使用torch.cat沿着给定的维度连接一连串的张量。也请看torch.stack,它是另一个与torch.cat有细微差别的张量连接操作。
t1 = torch.cat([tensor, tensor, tensor], dim=1)
print(t1)
3.3.3算术运算
# 这将计算两个张量之间的矩阵乘法。 y1, y2, y3将有相同的值
y1 = tensor @ tensor.T
y2 = tensor.matmul(tensor.T)
y3 = torch.rand_like(tensor)
torch.matmul(tensor, tensor.T, out=y3)
# 这将计算出元素的乘积。 z1, z2, z3将有相同的值
z1 = tensor * tensor
z2 = tensor.mul(tensor)
z3 = torch.rand_like(tensor)
torch.mul(tensor, tensor, out=z3)
3.3.4单元素张量
如果你有一个单元素张量,例如通过将一个张量的所有值聚集成一个值,你可以使用 item() 将其转换为一个 Python 数值。
agg = tensor.sum()
agg_item = agg.item()
print(agg_item, type(agg_item))
3.3.5原地操作
将结果存储到操作数中的操作被称为原地操作。它们用后缀_来表示。例如:x.copy_(y), x.t_(), 将改变x。
print(f"{tensor} \n")
tensor.add_(5)
print(tensor)
原地操作可以节省一些内存,但在计算导数时可能会有问题,因为会立即失去历史记录。因此,我们不鼓励使用这些操作。
3.4与NumPy的桥梁
CPU上的张量和NumPy数组可以共享它们的底层内存位置,改变一个将改变另一个。
3.4.1张量到NumPy数组
t = torch.ones(5)
print(f"t: {t}")
n = t.numpy()
print(f"n: {n}")
张量的变化反映在NumPy数组中。
t.add_(1)
print(f"t: {t}")
print(f"n: {n}")
3.4.2NumPy数组到张量
n = np.ones(5)
t = torch.from_numpy(n)
NumPy数组中的变化反映在张量中。
np.add(n, 1, out=n)
print(f"t: {t}")
print(f"n: {n}")
4.数据集和数据加载器
处理数据样本的代码可能会变得杂乱无章且难以维护;我们希望我们的数据集代码能够与我们的模型训练代码解耦,以提高可读性和模块化程度。PyTorch提供了两个数据基元:torch.utils.data.DataLoader和torch.utils.data.Dataset,允许你使用预先加载的数据集以及你自己的数据。Dataset存储了样本及其相应的标签,而DataLoader在Dataset周围包裹了一个可迭代的数据集,以便能够方便地访问这些样本。
PyTorch领域库提供了一些预加载的数据集(如FashionMNIST),这些数据集子类为torch.utils.data.Dataset,并实现了针对特定数据的功能。它们可以用来为你的模型建立原型和基准。你可以在这里找到它们。图像数据集、文本数据集和音频数据集。
4.1加载数据集
下面是一个如何从TorchVision加载Fashion-MNIST数据集的例子。Fashion-MNIST是一个由60,000个训练实例和10,000个测试实例组成的Zalando的文章图像数据集。每个例子包括一个28×28的灰度图像和10个类别中的一个相关标签。
我们用以下参数加载FashionMNIST数据集。
root是存储训练/测试数据的路径。
train指定训练或测试数据集。
download=True如果根目录下没有数据,则从互联网上下载数据。
transform和target_transform指定特征和标签的转换。
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()
)
4.2迭代和可视化数据集
我们可以像列表一样手动索引数据集:training_data[index]。我们使用matplotlib来可视化训练数据中的一些样本。
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()
4.3为你的文件创建一个自定义数据集
一个自定义的数据集类必须实现三个函数。__init__, __len__, 和 __getitem__。看看这个实现;FashionMNIST图像被存储在一个目录img_dir中,它们的标签被分别存储在一个CSV文件annotations_file中。
在接下来的章节中,我们将对这些函数中的每一个发生的事情进行分解。
import os
import pandas as pd
from torchvision.io import read_image
class CustomImageDataset(Dataset):
def __init__(self, annotations_file, img_dir, transform=None, target_transform=None):
self.img_labels = pd.read_csv(annotations_file)
self.img_dir = img_dir
self.transform = transform
self.target_transform = target_transform
def __len__(self):
return len(self.img_labels)
def __getitem__(self, idx):
img_path = os.path.join(self.img_dir, self.img_labels.iloc[idx, 0])
image = read_image(img_path)
label = self.img_labels.iloc[idx, 1]
if self.transform:
image = self.transform(image)
if self.target_transform:
label = self.target_transform(label)
return image, label
4.4__init__
在实例化数据集对象时,__init__函数会运行一次。我们初始化包含图像的目录、注释文件和两种转换(在下一节有更详细的介绍)。
labels.csv文件看起来像:
tshirt1.jpg, 0
tshirt2.jpg, 0
......
ankleboot999.jpg, 9
def __init__(self, annotations_file, img_dir, transform=None, target_transform=None):
self.img_labels = pd.read_csv(annotations_file)
self.img_dir = img_dir
self.transform = transform
self.target_transform = target_transform
4.5__len__
函数__len__返回我们数据集中的样本数。
例子。
def __len__(self):
return len(self.img_labels)
4.6__getitem__
函数 __getitem__ 在给定的索引idx处加载并返回数据集中的一个样本。基于索引,它确定图像在磁盘上的位置,使用read_image将其转换为张量,从self.img_labels中的csv数据中获取相应的标签,对其调用转换函数(如果适用),并在一个元组中返回张量图像和相应标签。
def __getitem__(self, idx):
img_path = os.path.join(self.img_dir, self.img_labels.iloc[idx, 0])
image = read_image(img_path)
label = self.img_labels.iloc[idx, 1]
if self.transform:
image = self.transform(image)
if self.target_transform:
label = self.target_transform(label)
return image, label
4.7使用DataLoaders为训练准备数据
数据集每次都会检索我们的数据集的特征和标签。在训练一个模型时,我们通常希望以 "小批 "的形式传递样本,在每个周期重新洗牌以减少模型的过拟合,并使用Python的多处理来加快数据的检索速度。
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)
4.8遍历数据加载器
我们已经将该数据集加载到DataLoader中,并可以根据需要对数据集进行迭代。下面的每次迭代都会返回一批train_features和train_labels(分别包含batch_size=64的特征和标签)。因为我们指定了shuffle=True,所以在我们迭代完所有的批次后,数据会被洗牌(要想对数据加载顺序进行更精细的控制,请看Samplers)。
# 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}")
进一步阅读
5.转变
数据并不总是以训练机器学习算法所需的最终处理形式出现。我们使用转换来对数据进行一些操作,使其适合训练。
所有的TorchVision数据集都有两个参数--用于修改特征的transform和用于修改标签的target_transform--它们接受包含转换逻辑的callables。torchvision.transform模块提供了几个常用的转换,开箱即用。
FashionMNIST的特征是PIL图像格式的,标签是整数。对于训练,我们需要将特征作为归一化的张量,将标签作为单热编码的张量。为了进行这些转换,我们使用ToTensor和Lambda。
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))
)
5.1ToTensor()
ToTensor将PIL图像或NumPy ndarray转换为FloatTensor,并将图像的像素强度值在[0., 1.]范围内进行缩放。
5.2Lambda Transforms
兰姆达变换应用任何用户定义的兰姆达函数。在这里,我们定义了一个函数,把整数变成一个一热编码的张量。它首先创建一个大小为10(我们数据集中的标签数量)的零张量,并调用scatter_,在标签y给出的索引上分配一个值=1。
target_transform = Lambda(lambda y: torch.zeros(
10, dtype=torch.float).scatter_(dim=0, index=torch.tensor(y), value=1))
进一步阅读
6.建立神经网络
神经网络由对数据进行操作的层/模块组成。torch.nn命名空间提供了您构建自己的神经网络所需的所有构件。PyTorch中的每个模块都子类化了nn.Module。一个神经网络本身就是一个由其他模块(层)组成的模块。这种嵌套结构允许轻松构建和管理复杂的架构。
在下面的章节中,我们将建立一个神经网络来对FashionMNIST数据集中的图像进行分类。
import os
import torch
from torch import nn
from torch.utils.data import DataLoader
from torchvision import datasets, transforms
6.1获得培训的设备
我们希望能够在像GPU这样的硬件加速器上训练我们的模型,如果它是可用的。让我们检查一下torch.cuda是否可用,否则我们继续使用CPU。
device = "cuda" if torch.cuda.is_available() else "cpu"
print(f"Using {device} device")
6.2定义类
我们通过子类化 nn.Module 来定义我们的神经网络,并在 __init__ 中初始化神经网络层。每个 nn.Module 子类都在 forward 方法中实现了对输入数据的操作。
class NeuralNetwork(nn.Module):
def __init__(self):
super(NeuralNetwork, self).__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的实例,并将其移动到设备上,并打印其结构。
model = NeuralNetwork().to(device)
print(model)
为了使用这个模型,我们把输入数据传给它。这就执行了模型的转发,以及一些后台操作。请不要直接调用model.forward()!
在输入数据上调用模型会返回一个10维的张量,其中包含每个类别的原始预测值。我们通过nn.Softmax模块的一个实例来获得预测概率。
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}")
6.3模型分层
让我们分解一下FashionMNIST模型中的各层。为了说明这一点,我们将采取一个由3张大小为28x28的图像组成的样本迷你批,看看当我们把它通过网络时发生了什么。
input_image = torch.rand(3,28,28)
print(input_image.size())
6.4nn.Flatten
我们初始化nn.Flatten层,将每个28x28的二维图像转换为784个像素值的连续数组(minibatch的维度(dim=0)被保持)。
flatten = nn.Flatten()
flat_image = flatten(input_image)
print(flat_image.size())
6.5nn.Linear
线性层是一个模块,使用其存储的权重和偏置对输入进行线性转换。
layer1 = nn.Linear(in_features=28*28, out_features=20)
hidden1 = layer1(flat_image)
print(hidden1.size())
6.6nn.ReLU
非线性激活是在模型的输入和输出之间建立复杂的映射关系。它们被应用在线性变换之后,以引入非线性,帮助神经网络学习各种各样的现象。
在这个模型中,我们在线性层之间使用了nn.ReLU,但还有其他激活可以在你的模型中引入非线性。
print(f"Before ReLU: {hidden1}\n\n")
hidden1 = nn.ReLU()(hidden1)
print(f"After ReLU: {hidden1}")
6.7nn.Sequential
nn.Sequential是一个有序的模块容器。数据以定义的相同顺序通过所有的模块。你可以使用顺序容器来拼凑一个快速网络,比如seq_modules。
seq_modules = nn.Sequential(
flatten,
layer1,
nn.ReLU(),
nn.Linear(20, 10)
)
input_image = torch.rand(3,28,28)
logits = seq_modules(input_image)
6.8nn.Softmax
神经网络的最后一个线性层返回对数--[-infty, infty]中的原始值--这些值被传递给nn.Softmax模块。对数被缩放为数值[0, 1],代表模型对每个类别的预测概率。 dim参数表示数值必须和为1的维度。
softmax = nn.Softmax(dim=1)
pred_probab = softmax(logits)
6.9模型参数
神经网络中的许多层都是参数化的,也就是说,有相关的权重和偏置,在训练中被优化。子类化 nn.Module 会自动跟踪你的模型对象中定义的所有字段,并使用你的模型的 parameters() 或 named_parameters() 方法访问所有参数。
在这个例子中,我们遍历每个参数,并打印其大小和预览其值。
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")
进一步阅读
7.用Torch.autograd进行自动区分
在训练神经网络时,最常使用的算法是反向传播算法。在这种算法中,参数(模型权重)是根据损失函数相对于给定参数的梯度来调整的。
为了计算这些梯度,PyTorch有一个内置的微分引擎,叫做torch.autograd。它支持对任何计算图的梯度进行自动计算。
考虑最简单的单层神经网络,输入x,参数w和b,以及一些损失函数。它可以在PyTorch中以如下方式定义。
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)
7.1张量、函数和计算图
此代码定义了以下计算图:
在这个网络中,w和b是参数,我们需要进行优化。因此,我们需要能够计算损失函数相对于这些变量的梯度。为了做到这一点,我们设置了这些张量的 requires_grad 属性。
你可以在创建张量时设置requires_grad的值,或者在以后使用x.requires_grad_(True)方法。
我们应用于张量来构建计算图的函数实际上是一个函数类的对象。这个对象知道如何在前进方向上计算函数,也知道如何在后向传播步骤中计算其导数。对后向传播函数的引用被存储在张量的grad_fn属性中。你可以在文档中找到更多关于Function的信息。
print(f"Gradient function for z = {z.grad_fn}")
print(f"Gradient function for loss = {loss.grad_fn}")
7.2计算梯度
为了优化神经网络中的参数权重,我们需要计算损失函数相对于参数的导数,即:
为了计算这些导数,我们调用loss.backward(),然后从w.grad和b.grad中检索出数值。
loss.backward()
print(w.grad)
print(b.grad)
我们只能获得计算图的叶子节点的梯度属性,这些节点的requires_grad属性设置为True。对于我们图中的所有其他节点,梯度将不可用。
出于性能方面的考虑,我们只能在一个给定的图上使用后向计算一次来进行梯度计算。如果我们需要在同一个图上进行多次后向调用,我们需要在后向调用中传递 retain_graph=True。
7.3禁用梯度跟踪
默认情况下,所有带有require_grad=True的张量都在跟踪它们的计算历史并支持梯度计算。然而,在某些情况下,我们不需要这样做,例如,当我们已经训练了模型,只是想把它应用于一些输入数据,也就是说,我们只想通过网络进行前向计算。我们可以通过用torch.no_grad()块包围我们的计算代码来停止跟踪计算。
z = torch.matmul(x, w)+b
print(z.requires_grad)
with torch.no_grad():
z = torch.matmul(x, w)+b
print(z.requires_grad)
实现相同结果的另一种方法是在张量上使用 detach() 方法:
z = torch.matmul(x, w)+b
z_det = z.detach()
print(z_det.requires_grad)
您可能希望禁用梯度跟踪的原因如下:
将你的神经网络中的一些参数标记为冻结参数。这是对预训练的网络进行微调的一个非常常见的情况。
当你只做前向传递时,为了加快计算速度,因为对不跟踪梯度的张量的计算会更有效率。
7.4关于计算图的更多信息
从概念上讲,autograd在一个由Function对象组成的有向无环图(DAG)中保存了数据(张量)和所有执行的操作(以及产生的新张量)的记录。在这个DAG中,叶子是输入张量,根部是输出张量。通过追踪这个图从根到叶,你可以使用链式规则自动计算梯度。
在一个前向传递中,autograd同时做两件事:
运行所请求的操作,计算出一个结果张量
在DAG中保持该操作的梯度函数。
当在DAG根上调用.backward()时,后向传递就开始了,然后autograd:
计算每个.grad_fn的梯度。
将它们累积到各自张量的 .grad 属性中
使用连锁规则,一直传播到叶子张量。
DAG在PyTorch中是动态的 需要注意的是,图是从头开始重新创建的;在每次调用.backward()后,autograd开始填充一个新的图。这正是允许你在模型中使用控制流语句的原因;如果需要,你可以在每次迭代时改变形状、大小和操作。
(选读)张量梯度和雅各布乘积
进一步阅读
8.优化模型参数
现在,我们有了一个模型和数据,是时候通过在数据上优化模型的参数来训练、验证和测试我们的模型了。训练模型是一个迭代的过程;在每个迭代中(称为epoch),模型对输出进行猜测,计算其猜测的误差(损失),收集误差相对于其参数的导数(正如我们在上一节看到的),并使用梯度下降优化这些参数。关于这个过程的更详细的演练,请看3Blue1Brown提供的关于反向传播的视频。
8.1先决条件代码
我们从前面关于数据集和数据加载器以及建立模型的章节中加载代码。
import torch
from torch import nn
from torch.utils.data import DataLoader
from torchvision import datasets
from torchvision.transforms import ToTensor, Lambda
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(NeuralNetwork, self).__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()
8.2超参数
超参数是可调整的参数,让你控制模型优化过程。不同的超参数值会影响模型的训练和收敛率(阅读更多关于超参数调整的内容)
我们为训练定义了以下超参数:
纪元数 - 在数据集上迭代的次数
批量大小--在参数更新之前,通过网络传播的数据样本的数量。
学习率--在每个批次/阶段更新模型参数的程度。较小的值产生缓慢的学习速度,而较大的值可能会导致训练期间的不可预测的行为。
learning_rate = 1e-3
batch_size = 64
epochs = 5
8.3优化循环
一旦我们设定了超参数,我们就可以通过优化循环来训练和优化我们的模型。优化循环的每一次迭代被称为一个epoch。
每个周期由两个主要部分组成。
训练循环--在训练数据集上迭代,试图收敛到最佳参数。
验证/测试循环--迭代测试数据集,以检查模型性能是否在提高。
让我们简单地熟悉一下训练循环中使用的一些概念。继续往前走,看看优化循环的完整实现。
8.4损失函数
当遇到一些训练数据时,我们未经训练的网络很可能不会给出正确的答案。损失函数衡量的是获得的结果与目标值的不相似程度,它是我们在训练期间想要最小化的损失函数。为了计算损失,我们使用给定数据样本的输入进行预测,并与真实数据标签值进行比较。
常见的损失函数包括用于回归任务的nn.MSELoss(均方误差)和用于分类的nn.NLLLoss(负对数似然)。nn.CrossEntropyLoss结合了nn.LogSoftmax和nn.NLLLoss。
我们将模型的输出对数传递给 nn.CrossEntropyLoss,它将对对数进行标准化处理并计算预测误差。
# Initialize the loss function
loss_fn = nn.CrossEntropyLoss()
8.5优化器
优化是在每个训练步骤中调整模型参数以减少模型误差的过程。优化算法定义了这个过程是如何进行的(在这个例子中,我们使用随机梯度下降法)。所有的优化逻辑都被封装在优化器对象中。在这里,我们使用SGD优化器;此外,PyTorch中还有许多不同的优化器,如ADAM和RMSProp,它们对不同类型的模型和数据有更好的效果。
我们通过注册需要训练的模型参数来初始化优化器,并传入学习率超参数。
optimizer = torch.optim.SGD(model.parameters(), lr=learning_rate)
在训练循环中,优化分三步进行:
调用optimizer.zero_grad()来重置模型参数的梯度。梯度默认为累加;为了防止重复计算,我们在每次迭代中明确地将其归零。
通过调用loss.backward()对预测损失进行反向传播。PyTorch将损失的梯度与每个参数联系在一起。
一旦我们有了梯度,我们就可以调用optimizer.step(),通过后向传递中收集的梯度来调整参数。
8.6全面执行
我们定义了train_loop和test_loop,train_loop负责循环我们的优化代码,test_loop负责根据测试数据评估模型的性能。
def train_loop(dataloader, model, loss_fn, optimizer):
size = len(dataloader.dataset)
for batch, (X, y) in enumerate(dataloader):
# Compute prediction and loss
pred = model(X)
loss = loss_fn(pred, y)
# Backpropagation
optimizer.zero_grad()
loss.backward()
optimizer.step()
if batch % 100 == 0:
loss, current = loss.item(), batch * len(X)
print(f"loss: {loss:>7f} [{current:>5d}/{size:>5d}]")
def test_loop(dataloader, model, loss_fn):
size = len(dataloader.dataset)
num_batches = len(dataloader)
test_loss, correct = 0, 0
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_loop和test_loop。随意增加epochs的数量,以跟踪模型的改进性能。
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!")
进一步阅读
9.保存和加载模型
在这一节中,我们将研究如何通过保存、加载和运行模型预测来保持模型状态。
import torch
import torchvision.models as models
9.1保存和加载模型权重
PyTorch模型将学到的参数存储在内部状态字典中,称为state_dict。这些可以通过torch.save方法持久化:
model = models.vgg16(pretrained=True)
torch.save(model.state_dict(), 'model_weights.pth')
要加载模型的权重,你需要先创建一个相同模型的实例,然后用load_state_dict()方法加载参数。
model = models.vgg16() # 我们不指定pretrained=True,即不加载默认权重
model.load_state_dict(torch.load('model_weights.pth'))
model.eval()
请确保在推理前调用model.eval()方法,以将dropout和batch normalization层设置为评估模式。如果不这样做,将产生不一致的推理结果。
9.2保存和加载带形状的模型
在加载模型权重时,我们需要先将模型类实例化,因为该类定义了网络的结构。我们可能想把这个类的结构和模型一起保存,在这种情况下,我们可以把模型(而不是model.state_dict())传给保存函数。
torch.save(model, 'model.pth')
然后我们可以像这样加载模型:
model = torch.load('model.pth')
这种方法在序列化模型时使用Python pickle模块,因此它依赖于实际的类定义在加载模型时可用。