Pytorch学习笔记 (参考官方教程)

参考: pytorch官网教程

文章目录

  • 一、快速开始(Quick Start)
    • 数据处理(Working with data)
    • 创建模型(Creating Models)
    • 优化模型参数(Optimizing the Model Parameters)
    • 保存模型(Saving Models)
    • 加载模型(Loading Models)
  • 二、张量(Tensors)
    • 初始化张量(Initializing a Tensor)
    • 张量的属性(Attributes of a Tensor)
    • 用Numpy桥接(Bridge with NumPy)
      • Tensor → NumPy (Tensor to NumPy array)
      • NumPy→Tensor (NumPy array to Tensor)
  • 三、数据集和数据加载器(DATASETS & DATALOADERS)
    • 加载数据集(Loading a Dataset)
    • 迭代和可视化数据集(Iterating and Visualizing the Dataset)
    • 创建自定义数据集(Creating a Custom Dataset for your files)
    • 使用DataLoaders为训练准备数据
    • 遍历Dataloader
  • 四、变换(Transfroms)
    • ToTensor()
    • Lambda Transforms
  • 五、建立神经网络
    • 获取训练设备(Get Device for Training)
    • 定义类(Define the Class)
    • 模型层(Model Layers)
      • nn.Flatten
      • nn.Linear
      • nn.ReLU
      • nn.Sequential
      • nn.Softmax
    • 模型参数(**Model Parameters**)
  • 六、自动求导(Autograd)
    • 张量、函数和计算图(Tensors, Functions and Computational graph)
    • 计算梯度(Computing Gradients)
    • 禁用梯度跟踪(Disabling Gradient Tracking)
    • 更多关于计算图
    • 可选阅读: 张量梯度和雅可比矩阵乘积(Tensor Gradients and Jacobian Products)
  • 七、优化模型参数(OPTIMIZING MODEL PARAMETERS)
    • 前提代码(Prerequisite Code)
    • 超参数(Hyperparameters)
    • 循环优化(Optimization Loop)
    • 损失函数(Loss Function)
    • 优化器(Optimizer)
    • 完整实现(Full Implementation)
  • 八、保存和加载模型
    • 加载和保存模型的权重(Saving and Loading Model Weights)
    • 通过形状来保存和加载模型(Saving and Loading Models with Shapes)

一、快速开始(Quick Start)

数据处理(Working with data)

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提供了特定领域的库,如TorchTextTorchVisionTorchAudio,所有这些库都包含数据集。在本教程中,我们将使用一个TorchVision数据集。

torchvision.datassets模块包含许多现实世界视觉数据的Dataset对象,如CIFAR, COCO。在本教程中,我们使用FashionMNIST数据集。每个TorchVision Dataset包含两个参数:transformtarget_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

创建模型(Creating Models)

要在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)

优化模型参数(Optimizing the Model Parameters)

为了训练一个模型,我们需要一个损失函数和一个优化器。

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

保存模型(Saving Models)

保存模型的常用方法是序列化内部状态字典(包含模型参数)。

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

加载模型(Loading Models)

加载模型的过程包括重新创建模型结构并将状态字典加载到其中。

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

二、张量(Tensors)

张量(tensor)一种特殊的数据结构,非常类似于数组和矩阵。在PyTorch中,我们使用张量对模型的输入和输出以及模型的参数进行编码。

张量与NumPy的ndarray相似,不同之处在于张量可以在gpu或其他硬件加速器上运行。事实上,张量和NumPy数组通常可以共享相同的底层内存,消除了复制数据的需要。张量也为自动微分进行了优化(我们将在后面的自动求导部分看到更多关于它的内容)。

导包:

import torch
import numpy as np

初始化张量(Initializing a Tensor)

张量可以用各种方式初始化。看看下面的例子:

  • 直接从数据中初始化(Directly from data

torch.tensor(data):张量可以直接从数据中创建。自动推断数据类型。

data = [[1, 2],[3, 4]]
x_data = torch.tensor(data)
  • Numpy数组中初始化(From a NumPy array

torch.from_numpy(np_array): 张量可以从NumPy数组中创建(反之亦然)。

np_array = np.array(data)
x_np = torch.from_numpy(np_array)
  • 从另外一个张量中初始化(From another tensor)

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]])
  • 从随机值或者常量值初始化(With random or constant values

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.]])

张量的属性(Attributes of a Tensor)

张量属性描述了它们的形状(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很容易使用。

  • 标准numpy类索引和切片(Standard numpy-like indexing and slicing)
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.]])
  • 算术运算(Arithmetic operations)
# 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桥接(Bridge with NumPy)

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

Tensor → NumPy (Tensor to NumPy array)

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.]

NumPy→Tensor (NumPy array to Tensor)

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.]

三、数据集和数据加载器(DATASETS & DATALOADERS)

处理数据样本的代码可能会变得混乱且难以维护;理想情况下,我们希望数据集代码与模型训练代码分离,以获得更好的可读性和模块化。

Pytorch 提供了两个预加载数据的基本体:torch.utils.data.DataLoader 和**torch.utils.data.Dataset**

Dataset 存储数据样本以及他们的响应的标签,DataLoader在数据集周围包装了一个可迭代对象,以方便访问数据样本。

PyTorch域库提供了许多预加载的数据集(如FashionMNIST),它们是torch.utils.data.Dataset的子类,并实现了基于特定数据的函数。它们可以用于模型的原型和基准测试。

加载数据集(Loading a Dataset)

下面是一个如何从TorchVision加载Fashion-MNIST数据集的例子。Fashion-MNIST是Zalando文章图像的一个数据集,包含6万个训练示例和1万个测试示例。每个示例包括一个28×28灰度图像和来自10个类中的一个相关标签。

我们用以下参数加载FashionMNIST数据集:

  • root是存储训练/测试数据的路径
  • train指定训练或测试数据集
  • download=True如果根目录下不可用,则从互联网上下载数据。
  • Transformtarget_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()
)

迭代和可视化数据集(Iterating and Visualizing the Dataset)

我们可以手动索引数据集,就像一个列表: 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()

Pytorch学习笔记 (参考官方教程)_第1张图片

创建自定义数据集(Creating a Custom Dataset for your files)

自定义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

使用DataLoaders为训练准备数据

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

我们已经将该数据集加载到DataLoader中,并可以根据需要迭代该数据集。下面的每个迭代都返回一批train_featurestrain_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

Pytorch学习笔记 (参考官方教程)_第2张图片

四、变换(Transfroms)

数据并不总是以训练机器学习算法所需的最终处理形式出现。我们使用Transfrom来执行数据的一些操作,并使其适合于训练。

所有TorchVision数据集都有两个参数——transform用于修改特征,target_transform用于修改标签——它们接受包含转换逻辑的可调用对象。torchvision.transforms模块提供了几个开箱即用的常用转换。

FashionMNIST特征为PIL Image格式,标签为整数。在训练中,我们需要将特征作为归一化张量,将标签作为独热编码(ont-hot)张量。为了进行这些转换,我们使用ToTensorLambda

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()

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

Lambda Transforms

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

获取训练设备(Get Device for Training)

我们希望能够在硬件加速器上训练我们的模型,如GPU,如果它是可用的。让我们检查一下torch.cuda是否可用的,否则我们继续使用CPU。

device = "cuda" if torch.cuda.is_available() else "cpu"
print(f"Using {device} device")

定义类(Define the Class)

我们通过子类化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')

模型层(Model Layers)

让我们分解FashionMNIST模型中的层。为了说明这一点,我们将使用3张大小为28x28的图片组成的小批样本,看看在网络中传递它时会发生什么。

input_image = torch.rand(3,28,28)
print(input_image.size())
torch.Size([3, 28, 28])

nn.Flatten

我们初始化nn.Flatten 层将每个2D 28x28图像转换为一个784像素值的连续数组(保持小批尺寸(dim=0))。

flatten = nn.Flatten()
flat_image = flatten(input_image)
print(flat_image.size())
torch.Size([3, 784])

nn.Linear

线性层是一个模块,它使用存储的权值和偏差对输入应用线性转换。

layer1 = nn.Linear(in_features=28*28, out_features=20)
hidden1 = layer1(flat_image)
print(hidden1.size())
torch.Size([3, 20])

nn.ReLU

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

在这个模型中,在我们的线性层之间使用nn.ReLU作为激活函数,但还有其他非线性的激活函数。

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

nn.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)

nn.Softmax

对数被缩放到值[0,1],表示模型对每个类的预测概率。dim参数指示数值之和必须为1的维度。

softmax = nn.Softmax(dim=1)
pred_probab = softmax(logits)

模型参数(Model Parameters

神经网络中的许多层都是参数化的,即具有相关的权重和偏差,并在训练过程中进行优化。子类化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>)

六、自动求导(Autograd)

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

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

考虑最简单的单层神经网络,输入x,参数wb,以及一些损失函数。它可以在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)

张量、函数和计算图(Tensors, Functions and Computational graph)

这段代码定义了以下计算图:

Pytorch学习笔记 (参考官方教程)_第3张图片

在这个网络中,wb是我们需要优化的参数。因此,我们需要能够计算损失函数相对于这些变量的梯度。为了做到这一点,我们设置这些张量的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>

计算梯度(Computing Gradients)

为了优化神经网络中参数的权重,我们需要计算损失函数对参数的导数。也就是说,我们需要固定xy的值,得到 ∂ l o s s ∂ w \frac{\partial l o s s}{\partial w} wloss ∂ l o s s ∂ b \frac{\partial l o s s}{\partial b} bloss 。为了计算这些导数,我们调用loss.backward()然后从w.gradb.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传递给反向调用

禁用梯度跟踪(Disabling Gradient Tracking)

默认情况下,所有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会同时做两件事:

  • 运行请求的操作来计算得到的张量
  • 维持DAG中操作的梯度函数。

Backward: 当在DAG根节点上调用backward()时,向后传递开始。Autograd 会开始做如下事:

  • 从每个.grad_fn计算梯度
  • 将它们累积到各自张量的.grad属性中
  • 使用链式法则一直传播到叶张量

PyTorch中的DAG是动态的,需要注意的是,图形是从头创建的; 在每次.backward()调用之后,autograd开始填充一个新的图。这正是允许您在模型中使用控制流语句的原因; 如果需要,您可以在每次迭代中更改形状、大小和操作。

可选阅读: 张量梯度和雅可比矩阵乘积(Tensor Gradients and Jacobian Products)

在很多情况下,我们有一个标量损失函数,我们需要计算关于一些参数的梯度。然而,也有输出函数是任意张量的情况。在本例中,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=x1y1x1ymxny1xnym

对于给定的向量 v = ( v 1 … v m ) v=\left(v_1 \ldots v_m\right) v=(v1vm), 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)),这是在标量值函数的情况下计算梯度的一种有用的方法,比如在神经网络训练期间的损失。

七、优化模型参数(OPTIMIZING MODEL PARAMETERS)

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

前提代码(Prerequisite Code)

我们从前一节的数据集和数据加载器(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()

超参数(Hyperparameters)

超参数是可调整的参数,可以让您控制模型优化过程。不同的超参数值会影响模型的训练和收敛速度

我们为训练定义了以下超参数:

  • Number of Epochs : 遍历数据集的次数
  • Batch Size :在更新参数之前通过网络传播的数据样本的数量
  • Learning Rate: 在每个批处理/历中要更新多少模型参数。较小的值产生较慢的学习速度,而较大的值可能导致训练过程中不可预测的行为。
learning_rate = 1e-3
batch_size = 64
epochs = 5

循环优化(Optimization Loop)

一旦我们设置了超参数,我们就可以用优化循环(optimization loop)训练和优化我们的模型。优化循环的每次迭代称为一个epoch。

每个epoch由两个部分组成:

  • **The Train Loop:**遍历训练数据集并尝试收敛到最优参数。
  • **The Validation/Test Loop :**遍历测试数据集以检查模型性能是否正在改进。

损失函数(Loss Function)

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

常见的损失函数包括: nn.MSELoss (Mean Square Error: 均方差) 对于回归任务; nn.NLLLoss(Negative Log Likelihood: 负对数似然) 对于分类任务。nn.CrosspyLoss 结合了nn.LogSoftmaxnn.NLLLoss

我们将模型的输出logits传递给nn.CrossEntropyLoss,它将规范化日志并计算预测误差。

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

优化器(Optimizer)

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

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

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

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

  • 调用optimizer.zero_grad()重置模型参数的梯度。默认情况下,梯度是累加的;为了防止重复计算,我们在每次迭代时显式地将它们归零。
  • 通过调用loss.backward()反向传播预测损失。PyTorch存储每个参数的损耗梯度。
  • 有了梯度之后,我们调用optimizer.step()来根据在向后传递中收集的梯度调整参数。

完整实现(Full Implementation)

我们定义遍历优化代码的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_looptest_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

加载和保存模型的权重(Saving and Loading Model Weights)

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()方法,将退出层和批处理归一化层设置为求值模式。如果不这样做,将产生不一致的推断结果.

通过形状来保存和加载模型(Saving and Loading Models with Shapes)

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

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

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

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

你可能感兴趣的:(#,深度学习框架,pytorch,学习,python)