参考: pytorch官网教程
PyTorch有两个处理数据的基本体:torch.utils.data.DataLoader
and torch.utils.data.Dataset
其中Dataset
存储样本及其对应的标签,DataLoader
在数据集周围包装一个可迭代对象。
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.datassets
模块包含许多现实世界视觉数据的Dataset对象,如CIFAR, COCO。在本教程中,我们使用FashionMNIST数据集。每个TorchVision Dataset
包含两个参数: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(),
)
我们将Dataset
作为参数传递给DataLoader
。它在数据集上封装了一个可迭代对象,并支持自动批处理、采样、变换和多进程数据加载。这里我们定义的批处理大小为64,也就是说,数据加载器迭代器中的每个元素将返回一批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
Shape of X [N, C, H, W]: torch.Size([64, 1, 28, 28])
Shape of y: torch.Size([64]) torch.int64
要在PyTorch中定义一个神经网络,我们需要创建一个继承自nn.Module
的类。我们在__init__
函数中定义网络的层,并在forward
函数中指定数据将如何通过网络。为了加速神经网络中的操作,如果可用的话,我们将它移到GPU上。
# Get cpu or gpu device for training.
device = "cuda" if torch.cuda.is_available() else "cpu"
print(f"Using {device} device")
# Define model
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().to(device)
print(model)
为了训练一个模型,我们需要一个损失函数和一个优化器。
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 = 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!")
保存模型的常用方法是序列化内部状态字典(包含模型参数)。
torch.save(model.state_dict(), "model.pth")
print("Saved PyTorch Model State to model.pth")
加载模型的过程包括重新创建模型结构并将状态字典加载到其中。
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}"')
张量(tensor)一种特殊的数据结构,非常类似于数组和矩阵。在PyTorch中,我们使用张量对模型的输入和输出以及模型的参数进行编码。
张量与NumPy的ndarray相似,不同之处在于张量可以在gpu或其他硬件加速器上运行。事实上,张量和NumPy数组通常可以共享相同的底层内存,消除了复制数据的需要。张量也为自动微分进行了优化(我们将在后面的自动求导部分看到更多关于它的内容)。
导包:
import torch
import numpy as np
张量可以用各种方式初始化。看看下面的例子:
torch.tensor(data)
:张量可以直接从数据中创建。自动推断数据类型。
data = [[1, 2],[3, 4]]
x_data = torch.tensor(data)
torch.from_numpy(np_array)
: 张量可以从NumPy数组中创建(反之亦然)。
np_array = np.array(data)
x_np = torch.from_numpy(np_array)
torch.ones_like(x_data)
、torch.rand_like(x_data, dtype=torch.float)
新的张量保留参数张量的属性(形状、数据类型),除非显式重写。
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")
Ones Tensor:
tensor([[1, 1],
[1, 1]])
Random Tensor:
tensor([[0.8397, 0.3300],
[0.3356, 0.7086]])
torch.rand(shape)
、torch.ones(shape)
、torch.zeros(shape)
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}")
Random Tensor:
tensor([[0.7453, 0.7993, 0.8484],
[0.3592, 0.3243, 0.7226]])
Ones Tensor:
tensor([[1., 1., 1.],
[1., 1., 1.]])
Zeros Tensor:
tensor([[0., 0., 0.],
[0., 0., 0.]])
张量属性描述了它们的形状(tensor.shape
)、数据类型(tensor.dtype
)和存储它们的设备(tensor.device
)。
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}")
Shape of tensor: torch.Size([3, 4])
Datatype of tensor: torch.float32
Device tensor is stored on: cpu
超过100张量操作,包括算术,线性代数,矩阵操作(转置,索引,切片),采样和更多的综合描述在
torch - PyTorch 1.13 documentation
这些操作都可以在GPU上运行(速度通常比CPU快)。如果你正在使用Colab,分配一个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,你会发现张量API很容易使用。
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)
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.]])
连接张量: 你可以使用torch.cat
将张量序列沿着给定的维数连接起来。参见torch.stack
,另一个张量连接操作,它与torch.cat略有不同。
t1 = torch.cat([tensor, tensor, tensor], dim=1)
print(t1)
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.]])
# This computes the matrix multiplication between two tensors. y1, y2, y3 will have the same value
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)
tensor([[1., 0., 1., 1.],
[1., 0., 1., 1.],
[1., 0., 1., 1.],
[1., 0., 1., 1.]])
单元素张量(Single-element tensors):如果你有一个单元素张量,例如通过将一个张量的所有值聚合到一个值,你可以使用item()
将它转换为一个Python数值:
agg = tensor.sum()
agg_item = agg.item()
print(agg_item, type(agg_item))
12.0 <class 'float'>
就地操作(In-place operations) : 将结果存储到操作数中的操作称为in-place
。他们被一个_
后缀所标记。例如:x.copy_(y)
,x.t_()
,会改变x的值。
print(f"{tensor} \n")
tensor.add_(5)
print(tensor)
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.]])
就地操作节省了一些内存,但在计算导数时可能会出现问题,因为会立即丢失历史记录。因此,不鼓励使用它们。
NumPy数组和在cpu上的张量可以共享它们的底层内存位置,更改其中一个将更改另一个。
t. numpy()
: 把张量转换成numpy 数组。
t = torch.ones(5)
print(f"t: {t}")
n = t.numpy()
print(f"n: {n}")
t: tensor([1., 1., 1., 1., 1.])
n: [1. 1. 1. 1. 1.]
张量的变化反映在NumPy数组中:
t.add_(1)
print(f"t: {t}")
print(f"n: {n}")
t: tensor([2., 2., 2., 2., 2.])
n: [2. 2. 2. 2. 2.]
torch.from_numpy(n)
: 把numpy数组转换成张量。
n = np.ones(5)
t = torch.from_numpy(n)
NumPy数组的变化反映在张量中:
np.add(n, 1, out=n)
print(f"t: {t}")
print(f"n: {n}")
t: tensor([2., 2., 2., 2., 2.], dtype=torch.float64)
n: [2. 2. 2. 2. 2.]
处理数据样本的代码可能会变得混乱且难以维护;理想情况下,我们希望数据集代码与模型训练代码分离,以获得更好的可读性和模块化。
Pytorch 提供了两个预加载数据的基本体:torch.utils.data.DataLoader
和**torch.utils.data.Dataset
**
Dataset
存储数据样本以及他们的响应的标签,DataLoader
在数据集周围包装了一个可迭代对象,以方便访问数据样本。
PyTorch域库提供了许多预加载的数据集(如FashionMNIST),它们是torch.utils.data.Dataset
的子类,并实现了基于特定数据的函数。它们可以用于模型的原型和基准测试。
下面是一个如何从TorchVision加载Fashion-MNIST数据集的例子。Fashion-MNIST是Zalando文章图像的一个数据集,包含6万个训练示例和1万个测试示例。每个示例包括一个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()
)
我们可以手动索引数据集,就像一个列表: 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()
自定义Dataset类必须实现三个函数: __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
__init__
在实例化Dataset对象时,__init__
函数只运行一次。我们初始化包含图像、注释文件和两个转换的目录(下一节将详细介绍)。
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
__len__
__len__
函数返回数据集中样本的数量。
def __len__(self):
return len(self.img_labels)
__getitem__
__getitem__
函数从给定索引idx
处的数据集中加载并返回一个样本。基于索引,它识别图像在磁盘上的位置,使用read_image
将其转换为一个张量,从csv数据中检索相应的标签self.Img_labels
,调用它们transform函数(如果适用),并返回一个元组(张量图像, 对应的标签)
。
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
Dataset
检索我们的数据集的特征,并每次标记一个样本。在训练模型时,我们通常希望以“小批”的方式传递样本,在每个epoch重新调整数据以减少模型过拟合,并使用Python的multiprocessing
来加速数据检索。
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)
我们已经将该数据集加载到DataLoader中,并可以根据需要迭代该数据集。下面的每个迭代都返回一批train_features
和train_labels
(分别包含batch_size=64
个特征和标签)。因为我们指定了shuffle=True
,所以在遍历所有批之后,数据会被打乱。
# 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}")
Feature batch shape: torch.Size([64, 1, 28, 28])
Labels batch shape: torch.Size([64])
Label: 9
数据并不总是以训练机器学习算法所需的最终处理形式出现。我们使用Transfrom
来执行数据的一些操作,并使其适合于训练。
所有TorchVision数据集都有两个参数——transform
用于修改特征,target_transform
用于修改标签——它们接受包含转换逻辑的可调用对象。torchvision.transforms
模块提供了几个开箱即用的常用转换。
FashionMNIST特征为PIL Image格式,标签为整数。在训练中,我们需要将特征作为归一化张量,将标签作为独热编码(ont-hot)张量。为了进行这些转换,我们使用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))
)
ToTensor将PIL图像或NumPy ndarray
转换为FloatTensor
。并在范围内缩放图像的像素强度值在范围[0.,1.]
Lambda转换应用任何用户定义的Lambda函数。在这里,我们定义一个函数将整数转换为一个单次编码张量。它首先创建一个大小为10(数据集中标签的数量)的零张量,并调用scatter_
,在标签y给出的索引上赋值为1
target_transform = Lambda(lambda y: torch.zeros(
10, dtype=torch.float).scatter_(dim=0, index=torch.tensor(y), value=1))
神经网络由对数据执行操作的层/模块组成。
神经网络由对数据执行操作的**层/模块(layers/modules)**组成。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
我们希望能够在硬件加速器上训练我们的模型,如GPU,如果它是可用的。让我们检查一下torch.cuda
是否可用的,否则我们继续使用CPU。
device = "cuda" if torch.cuda.is_available() else "cpu"
print(f"Using {device} device")
我们通过子类化nn.Module
来定义我们的神经网络。并初始化__init__
中的神经网络层。每一个nn.Moudule
子类在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)
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
,以及一些background operation。不要直接调用model.forward()
!
在输入上调用模型将返回一个二维张量,dim=0
对应10行,每一行是一个类的预测值;dim=1
对应于每个输出的单个值。我们通过将它传递给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}")
Predicted class: tensor([0], device='cuda:0')
让我们分解FashionMNIST模型中的层。为了说明这一点,我们将使用3张大小为28x28的图片组成的小批样本,看看在网络中传递它时会发生什么。
input_image = torch.rand(3,28,28)
print(input_image.size())
torch.Size([3, 28, 28])
我们初始化nn.Flatten
层将每个2D 28x28图像转换为一个784像素值的连续数组(保持小批尺寸(dim=0))。
flatten = nn.Flatten()
flat_image = flatten(input_image)
print(flat_image.size())
torch.Size([3, 784])
线性层是一个模块,它使用存储的权值和偏差对输入应用线性转换。
layer1 = nn.Linear(in_features=28*28, out_features=20)
hidden1 = layer1(flat_image)
print(hidden1.size())
torch.Size([3, 20])
非线性激活在模型的输入和输出之间创建了复杂的映射。它们被应用在线性变换后引入非线性,帮助神经网络学习各种各样的现象。
在这个模型中,在我们的线性层之间使用nn.ReLU
作为激活函数,但还有其他非线性的激活函数。
print(f"Before ReLU: {hidden1}\n\n")
hidden1 = nn.ReLU()(hidden1)
print(f"After ReLU: {hidden1}")
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)
对数被缩放到值[0,1],表示模型对每个类的预测概率。dim
参数指示数值之和必须为1的维度。
softmax = nn.Softmax(dim=1)
pred_probab = softmax(logits)
神经网络中的许多层都是参数化的,即具有相关的权重和偏差,并在训练过程中进行优化。子类化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")
Model structure: 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)
)
)
Layer: linear_relu_stack.0.weight | Size: torch.Size([512, 784]) | Values : tensor([[ 0.0125, -0.0280, 0.0184, ..., -0.0006, -0.0179, 0.0052],
[ 0.0332, -0.0088, 0.0086, ..., -0.0284, 0.0238, -0.0240]],
device='cuda:0', grad_fn=<SliceBackward0>)
Layer: linear_relu_stack.0.bias | Size: torch.Size([512]) | Values : tensor([ 0.0045, -0.0279], device='cuda:0', grad_fn=<SliceBackward0>)
Layer: linear_relu_stack.2.weight | Size: torch.Size([512, 512]) | Values : tensor([[ 0.0392, 0.0210, -0.0063, ..., 0.0264, -0.0129, 0.0172],
[-0.0415, 0.0196, 0.0027, ..., 0.0205, -0.0392, 0.0401]],
device='cuda:0', grad_fn=<SliceBackward0>)
Layer: linear_relu_stack.2.bias | Size: torch.Size([512]) | Values : tensor([-0.0169, 0.0353], device='cuda:0', grad_fn=<SliceBackward0>)
Layer: linear_relu_stack.4.weight | Size: torch.Size([10, 512]) | Values : tensor([[-0.0427, 0.0441, 0.0027, ..., 0.0201, 0.0362, -0.0359],
[ 0.0062, 0.0343, 0.0296, ..., -0.0399, -0.0287, -0.0368]],
device='cuda:0', grad_fn=<SliceBackward0>)
Layer: linear_relu_stack.4.bias | Size: torch.Size([10]) | Values : tensor([-0.0219, -0.0387], device='cuda:0', grad_fn=<SliceBackward0>)
在训练神经网络时,最常用的算法是反向传播 (back propagation)。该算法根据损失函数相对于给定参数的梯度(gradient) 调整参数(模型权值)。
为了计算这些梯度,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)
这段代码定义了以下计算图:
在这个网络中,w
和b
是我们需要优化的参数。因此,我们需要能够计算损失函数相对于这些变量的梯度。为了做到这一点,我们设置这些张量的requires_grad
属性。
您可以在创建张量时设置requires_grad
的值,或者稍后使用x.requires_grad_(True)
方法。
应用于张量构造计算图的函数,实际上是函数类的对象。这个对象知道如何在正向方向上计算函数,也知道如何在反向传播步骤中计算其导数。对反向传播函数的引用存储在张量的grad_fn
属性中。
print(f"Gradient function for z = {z.grad_fn}")
print(f"Gradient function for loss = {loss.grad_fn}")
Gradient function for z = <AddBackward0 object at 0x7f623391cfd0>
Gradient function for loss = <BinaryCrossEntropyWithLogitsBackward0 object at 0x7f623391cdf0>
为了优化神经网络中参数的权重,我们需要计算损失函数对参数的导数。也就是说,我们需要固定x
和y
的值,得到 ∂ l o s s ∂ w \frac{\partial l o s s}{\partial w} ∂w∂loss 和 ∂ l o s s ∂ b \frac{\partial l o s s}{\partial b} ∂b∂loss 。为了计算这些导数,我们调用loss.backward()
然后从w.grad
和b.grad
中得到导数值。
loss.backward()
print(w.grad)
print(b.grad)
tensor([[0.0048, 0.0694, 0.0747],
[0.0048, 0.0694, 0.0747],
[0.0048, 0.0694, 0.0747],
[0.0048, 0.0694, 0.0747],
[0.0048, 0.0694, 0.0747]])
tensor([0.0048, 0.0694, 0.0747])
我们只能获得计算图的叶节点的grad
属性,它们的requires_grad
属性设置为True
。对于图中的所有其他节点,梯度将不可用。
由于性能原因,我们只能在给定的图上反向(backward
)执行一次梯度计算。如果我们需要在同一个图上执行多个反向调用
,我们需要将retain_graph=True
传递给反向调用
。
默认情况下,所有requires_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)
True
False
另一种实现相同结果的方法是在张量上使用detach()
方法:
z = torch.matmul(x, w)+b
z_det = z.detach()
print(z_det.requires_grad)
False
禁用梯度跟踪可能有以下原因:
frozen parameters
)。这是对预训练网络进行微调以加快计算的一个非常常见的场景。从概念上讲,autograd
在一个由Function
对象组成的有向无环图(DAG)中保存数据(张量)和所有执行的操作(以及产生的新张量)的记录。在这个DAG中,叶是输入张量,根是输出张量。通过从根到叶跟踪这个图,您可以使用链式法则(chain rule
)自动计算梯度。
Forward: 在向前传递中,autograd
会同时做两件事:
Backward: 当在DAG根节点上调用backward()
时,向后传递开始。Autograd 会开始做如下事:
.grad_fn
计算梯度.grad
属性中PyTorch中的DAG是动态的,需要注意的是,图形是从头创建的; 在每次.backward()
调用之后,autograd
开始填充一个新的图。这正是允许您在模型中使用控制流语句的原因; 如果需要,您可以在每次迭代中更改形状、大小和操作。
在很多情况下,我们有一个标量损失函数,我们需要计算关于一些参数的梯度。然而,也有输出函数是任意张量的情况。在本例中,PyTorch允许您计算所谓的雅可比矩阵乘积,而不是实际的梯度。
对于一个向量函数 y ⃗ = f ( x ⃗ ) \vec{y}=f(\vec{x}) y=f(x) , 其中 x ⃗ = ⟨ x 1 , … , x n ⟩ \vec{x}=\left\langle x_1, \ldots, x_n\right\rangle x=⟨x1,…,xn⟩ 和 y ⃗ = ⟨ y 1 , … , y m ⟩ \vec{y}=\left\langle y_1, \ldots, y_m\right\rangle y=⟨y1,…,ym⟩, y ⃗ \vec{y} y 对 x ⃗ \vec{x} x 的梯度如下面的雅克比矩阵
J = ( ∂ y 1 ∂ x 1 ⋯ ∂ y 1 ∂ x n ⋮ ⋱ ⋮ ∂ y m ∂ x 1 ⋯ ∂ y m ∂ x n ) J=\left(\begin{array}{ccc} \frac{\partial y_1}{\partial x_1} & \cdots & \frac{\partial y_1}{\partial x_n} \\ \vdots & \ddots & \vdots \\ \frac{\partial y_m}{\partial x_1} & \cdots & \frac{\partial y_m}{\partial x_n} \end{array}\right) J=⎝⎜⎛∂x1∂y1⋮∂x1∂ym⋯⋱⋯∂xn∂y1⋮∂xn∂ym⎠⎟⎞
对于给定的向量 v = ( v 1 … v m ) v=\left(v_1 \ldots v_m\right) v=(v1…vm), Pytorch 允许你计算Jacobian Product v T v^T vT ⋅ J J J 而不是直接计算雅克比矩阵 J J J。这是通过反向调用 v v v作为参数来实现的。 v v v的大小应该和原始张量的大小相同,我们想要针对它计算乘积:
inp = torch.eye(4, 5, requires_grad=True)
out = (inp+1).pow(2).t()
out.backward(torch.ones_like(out), retain_graph=True)
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_()
out.backward(torch.ones_like(out), retain_graph=True)
print(f"\nCall after zeroing gradients\n{inp.grad}")
First call
tensor([[4., 2., 2., 2., 2.],
[2., 4., 2., 2., 2.],
[2., 2., 4., 2., 2.],
[2., 2., 2., 4., 2.]])
Second call
tensor([[8., 4., 4., 4., 4.],
[4., 8., 4., 4., 4.],
[4., 4., 8., 4., 4.],
[4., 4., 4., 8., 4.]])
Call after zeroing gradients
tensor([[4., 2., 2., 2., 2.],
[2., 4., 2., 2., 2.],
[2., 2., 4., 2., 2.],
[2., 2., 2., 4., 2.]])
注意,当我们用相同的参数第二次反向调用时,梯度的值是不同的。这是因为在进行反向传播时,PyTorch会累积梯度,即计算的梯度值被添加到计算图的所有叶节点的grad属性中。如果你想计算合适的梯度,你需要把梯度属性归零。在现实训练中,优化器帮助我们做到这一点。
以前我们调用的是不带参数的backward()
函数。这本质上相当于调用backward(torch.tensor(1.0))
,这是在标量值函数的情况下计算梯度的一种有用的方法,比如在神经网络训练期间的损失。
现在我们有了一个模型和数据,是时候通过优化数据上的参数来训练、验证和测试我们的模型了。训练模型是一个迭代的过程; 在每次迭代(称为epoch
)中,模型对输出进行猜测,计算猜测中的误差(loss
),收集误差对其参数的导数(正如我们在上一节中看到的),并使用梯度下降(gradient descent
)优化这些参数。
我们从前一节的数据集和数据加载器(Datasets & DataLoaders)和构建模型加载代码(Build Model)。
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(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()
超参数是可调整的参数,可以让您控制模型优化过程。不同的超参数值会影响模型的训练和收敛速度
我们为训练定义了以下超参数:
learning_rate = 1e-3
batch_size = 64
epochs = 5
一旦我们设置了超参数,我们就可以用优化循环(optimization loop)训练和优化我们的模型。优化循环的每次迭代称为一个epoch。
每个epoch由两个部分组成:
当面对一些训练数据时,我们未经训练的网络很可能不会给出正确的答案。损失函数衡量的是得到的结果与目标值的不相似程度,是我们在训练过程中想要最小化的损失函数。为了计算损失,我们使用给定数据样本的输入进行预测,并将其与真实的数据标签值进行比较。
常见的损失函数包括: nn.MSELoss
(Mean Square Error: 均方差) 对于回归任务; nn.NLLLoss
(Negative Log Likelihood: 负对数似然) 对于分类任务。nn.CrosspyLoss
结合了nn.LogSoftmax
和 nn.NLLLoss
我们将模型的输出logits传递给nn.CrossEntropyLoss
,它将规范化日志并计算预测误差。
# Initialize the loss function
loss_fn = nn.CrossEntropyLoss()
优化是在每个训练步骤中调整模型参数以减少模型误差的过程。优化算法定义了这个过程是如何执行的(在本例中,我们使用随机梯度下降)。所有优化逻辑都封装在optimizer
对象中。这里,我们使用SGD优化器;此外,PyTorch中还有许多不同的优化器,如ADAM和RMSProp,它们可以更好地处理不同类型的模型和数据。
我们通过注册需要训练的模型参数,并传入学习率超参数来初始化优化器。
optimizer = torch.optim.SGD(model.parameters(), lr=learning_rate)
在训练循环中,优化分为三个步骤:
optimizer.zero_grad()
重置模型参数的梯度。默认情况下,梯度是累加的;为了防止重复计算,我们在每次迭代时显式地将它们归零。loss.backward()
反向传播预测损失。PyTorch存储每个参数的损耗梯度。optimizer.step()
来根据在向后传递中收集的梯度调整参数。我们定义遍历优化代码的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
。您可以随意增加epoch的数量,以跟踪模型不断改进的性能。
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!")
在本节中,我们将看看如何通过保存、加载和运行模型预测来持久保存模型状态。
import torch
import torchvision.models as models
PyTorch模型将学习到的参数存储在一个名为state_dict
的内部状态字典中。这些可以通过方法保存:
model = models.vgg16(pretrained=True)
torch.save(model.state_dict(), 'model_weights.pth')
要加载模型权重,您需要首先创建同一个模型的实例,然后使用load_state_dict()
方法加载参数。
model = models.vgg16() # we do not specify pretrained=True, i.e. do not load default weights
model.load_state_dict(torch.load('model_weights.pth'))
model.eval()
确保在推断之前调用model.eval()
方法,将退出层和批处理归一化层设置为求值模式。如果不这样做,将产生不一致的推断结果.
当加载模型权重时,我们需要首先实例化模型类,因为类定义了网络的结构。我们可能想要将这个类的结构和模型一起保存,在这种情况下,我们可以将model
(而不是model.state_dict()
)传递给保存函数:
torch.save(model, 'model.pth')
然后我们可以像这样加载模型:
model = torch.load('model.pth')