PyTorch 入门指南
学习 PyTorch
图像和视频
音频
后端
强化学习
在生产环境中部署 PyTorch 模型
Profiling PyTorch
代码变换与FX
前端API
扩展 PyTorch
模型优化
并行和分布式训练
边缘端的 ExecuTorch
推荐系统
多模态

简介 || 张量 || 自动求导 || 构建模型 || TensorBoard 支持 || 训练模型 || 模型理解

PyTorch 简介

请跟随以下视频或前往 YouTube 继续学习。

PyTorch 张量

从视频的 03:50 处开始跟随操作。

首先,我们将导入 pytorch。

importtorch

让我们来看一些基本的张量操作。首先,介绍几种创建张量的方法:

z = torch.zeros(5, 3)
print(z)
print(z.dtype)
tensor([[0., 0., 0.],
        [0., 0., 0.],
        [0., 0., 0.],
        [0., 0., 0.],
        [0., 0., 0.]])
torch.float32

在上面,我们创建了一个填充了零的 5x3 矩阵,并查询其数据类型,发现这些零是 32 位浮点数,这是 PyTorch 的默认设置。

如果您想要整数而不是浮点数呢?您始终可以覆盖默认设置:

i = torch.ones((5, 3), dtype=torch.int16)
print(i)
tensor([[1, 1, 1],
        [1, 1, 1],
        [1, 1, 1],
        [1, 1, 1],
        [1, 1, 1]], dtype=torch.int16)

您可以看到,当我们更改默认值时,张量在打印时会清晰地报告这一变化。

通常,我们会随机初始化学习权重,为了结果的可复现性,通常会为伪随机数生成器(PRNG)指定一个特定的种子:

torch.manual_seed(1729)
r1 = torch.rand(2, 2)
print('A random tensor:')
print(r1)

r2 = torch.rand(2, 2)
print('\nA different random tensor:')
print(r2) # new values

torch.manual_seed(1729)
r3 = torch.rand(2, 2)
print('\nShould match r1:')
print(r3) # repeats values of r1 because of re-seed
A random tensor:
tensor([[0.3126, 0.3791],
        [0.3087, 0.0736]])

A different random tensor:
tensor([[0.4216, 0.0691],
        [0.2332, 0.4047]])

Should match r1:
tensor([[0.3126, 0.3791],
        [0.3087, 0.0736]])

PyTorch 张量可以直观地执行算术运算。形状相似的张量可以进行加法、乘法等操作。与标量的运算会分布到整个张量上:

ones = torch.ones(2, 3)
print(ones)

twos = torch.ones(2, 3) * 2 # every element is multiplied by 2
print(twos)

threes = ones + twos       # addition allowed because shapes are similar
print(threes)              # tensors are added element-wise
print(threes.shape)        # this has the same dimensions as input tensors

r1 = torch.rand(2, 3)
r2 = torch.rand(3, 2)
# uncomment this line to get a runtime error
# r3 = r1 + r2
tensor([[1., 1., 1.],
        [1., 1., 1.]])
tensor([[2., 2., 2.],
        [2., 2., 2.]])
tensor([[3., 3., 3.],
        [3., 3., 3.]])
torch.Size([2, 3])

以下是可用的数学操作的一个小示例:

r = (torch.rand(2, 2) - 0.5) * 2 # values between -1 and 1
print('A random matrix, r:')
print(r)

# Common mathematical operations are supported:
print('\nAbsolute value of r:')
print(torch.abs(r))

# ...as are trigonometric functions:
print('\nInverse sine of r:')
print(torch.asin(r))

# ...and linear algebra operations like determinant and singular value decomposition
print('\nDeterminant of r:')
print(torch.det(r))
print('\nSingular value decomposition of r:')
print(torch.svd(r))

# ...and statistical and aggregate operations:
print('\nAverage and standard deviation of r:')
print(torch.std_mean(r))
print('\nMaximum value of r:')
print(torch.max(r))
A random matrix, r:
tensor([[ 0.9956, -0.2232],
        [ 0.3858, -0.6593]])

Absolute value of r:
tensor([[0.9956, 0.2232],
        [0.3858, 0.6593]])

Inverse sine of r:
tensor([[ 1.4775, -0.2251],
        [ 0.3961, -0.7199]])

Determinant of r:
tensor(-0.5703)

Singular value decomposition of r:
torch.return_types.svd(
U=tensor([[-0.8353, -0.5497],
        [-0.5497,  0.8353]]),
S=tensor([1.1793, 0.4836]),
V=tensor([[-0.8851, -0.4654],
        [ 0.4654, -0.8851]]))

Average and standard deviation of r:
(tensor(0.7217), tensor(0.1247))

Maximum value of r:
tensor(0.9956)

关于 PyTorch 张量的强大功能还有很多需要了解的,包括如何设置它们以在 GPU 上进行并行计算——我们将在另一个视频中深入探讨。

PyTorch 模型

10:00 开始跟随视频。

让我们讨论一下如何在 PyTorch 中表达模型。

importtorch                     # for all things PyTorch
importtorch.nnasnn            # for torch.nn.Module, the parent object for PyTorch models
importtorch.nn.functionalasF  # for the activation function

le-net-5 示意图

图:LeNet-5

上图展示了LeNet-5的结构,它是最早的卷积神经网络之一,也是推动深度学习爆发的重要驱动力之一。它的设计初衷是读取手写数字的小图像(MNIST数据集),并正确分类图像中代表的数字。

以下是其工作原理的简要说明:

  • 层 C1 是一个卷积层,这意味着它会扫描输入图像以寻找在训练过程中学到的特征。它会输出一张地图,显示它在图像中看到的每个学习到的特征的位置。这个“激活图”在层 S2 中被下采样。

  • 层 C3 是另一个卷积层,这次它会扫描 C1 的激活图以寻找特征的组合。它也会输出一张激活图,描述这些特征组合的空间位置,并在层 S4 中被下采样。

  • 最后,最末端的全连接层 F5、F6 和 OUTPUT 是一个分类器,它会接收最终的激活图并将其分类为十个类别中的一个,这些类别代表了 10 个数字。

如何在代码中表达这个简单的神经网络?

classLeNet(nn.Module):

    def__init__(self):
        super(LeNet, self).__init__()
        # 1 input image channel (black & white), 6 output channels, 5x5 square convolution
        # kernel
        self.conv1 = nn.Conv2d(1, 6, 5)
        self.conv2 = nn.Conv2d(6, 16, 5)
        # an affine operation: y = Wx + b
        self.fc1 = nn.Linear(16 * 5 * 5, 120)  # 5*5 from image dimension
        self.fc2 = nn.Linear(120, 84)
        self.fc3 = nn.Linear(84, 10)

    defforward(self, x):
        # Max pooling over a (2, 2) window
        x = F.max_pool2d(F.relu(self.conv1(x)), (2, 2))
        # If the size is a square you can only specify a single number
        x = F.max_pool2d(F.relu(self.conv2(x)), 2)
        x = x.view(-1, self.num_flat_features(x))
        x = F.relu(self.fc1(x))
        x = F.relu(self.fc2(x))
        x = self.fc3(x)
        return x

    defnum_flat_features(self, x):
        size = x.size()[1:]  # all dimensions except the batch dimension
        num_features = 1
        for s in size:
            num_features *= s
        return num_features

查看这段代码时,您应该能够发现它与上图的一些结构上的相似之处。

这展示了一个典型的 PyTorch 模型的结构:

  • 它继承自 torch.nn.Module —— 模块可以嵌套,事实上,即使是 Conv2dLinear 层类也继承自 torch.nn.Module

  • 模型会有一个 __init__() 函数,在其中实例化其层,并加载可能需要的任何数据(例如,一个 NLP 模型可能会加载词汇表)。

  • 模型会有一个 forward() 函数。这是实际计算发生的地方:输入通过网络层和各种函数传递以生成输出。

  • 除此之外,您可以像构建任何其他 Python 类一样构建模型类,添加任何属性和方法来支持模型的计算。

让我们实例化这个对象并通过它运行一个示例输入。

net = LeNet()
print(net)                         # what does the object tell us about itself?

input = torch.rand(1, 1, 32, 32)   # stand-in for a 32x32 black & white image
print('\nImage batch shape:')
print(input.shape)

output = net(input)                # we don't call forward() directly
print('\nRaw output:')
print(output)
print(output.shape)
LeNet(
  (conv1): Conv2d(1, 6, kernel_size=(5, 5), stride=(1, 1))
  (conv2): Conv2d(6, 16, kernel_size=(5, 5), stride=(1, 1))
  (fc1): Linear(in_features=400, out_features=120, bias=True)
  (fc2): Linear(in_features=120, out_features=84, bias=True)
  (fc3): Linear(in_features=84, out_features=10, bias=True)
)

Image batch shape:
torch.Size([1, 1, 32, 32])

Raw output:
tensor([[ 0.0898,  0.0318,  0.1485,  0.0301, -0.0085, -0.1135, -0.0296,  0.0164,
          0.0039,  0.0616]], grad_fn=<AddmmBackward0>)
torch.Size([1, 10])

上面有几个重要的点需要注意:

首先,我们实例化了 LeNet 类,并打印了 net 对象。torch.nn.Module 的子类会报告它创建的层及其形状和参数。如果您想了解模型的处理概要,这可以提供方便的概览。

接下来,我们创建了一个表示 32x32 大小、1 个颜色通道的虚拟输入。通常,您会加载一个图像块并将其转换为这种形状的张量。

您可能已经注意到我们的张量有一个额外的维度——批次维度。PyTorch 模型假定它们处理的是批次数据——例如,16 个图像块的批次将具有 (16, 1, 32, 32) 的形状。由于我们只使用一个图像,我们创建了一个形状为 (1, 1, 32, 32) 的批次。

我们通过像函数一样调用 net(input) 来请求模型进行推理。此调用的输出表示模型对输入代表特定数字的置信度。(由于此模型实例尚未学习任何内容,我们不应期望在输出中看到任何信号。)查看 output 的形状,我们可以看到它也有一个批次维度,其大小应始终与输入批次维度匹配。如果我们传递了一个包含 16 个实例的输入批次,output 的形状将为 (16, 10)

数据集和数据加载器

请从视频的 14:00 处开始跟随。

接下来,我们将演示如何使用 TorchVision 提供的现成可下载的开源数据集,如何将图像转换为模型可用的格式,以及如何使用 DataLoader 将批量数据输入到模型中。

首先,我们需要将输入的图像转换为 PyTorch 张量。

#%matplotlib inline

importtorch
importtorchvision
importtorchvision.transformsastransforms

transform = transforms.Compose(
    [transforms.ToTensor(),
     transforms.Normalize((0.4914, 0.4822, 0.4465), (0.2470, 0.2435, 0.2616))])

在这里,我们为输入指定了两个转换:

  • transforms.ToTensor() 将 Pillow 加载的图像转换为 PyTorch 张量。

  • transforms.Normalize() 调整张量的值,使其平均值为零,标准差为 1.0。大多数激活函数在 x = 0 附近具有最强的梯度,因此将数据集中在该位置可以加速学习。传递给该变换的值是数据集中图像的 rgb 值的均值(第一个元组)和标准差(第二个元组)。您可以通过运行以下几行代码自行计算这些值:

```

from torch.utils.data import ConcatDataset transform = transforms.Compose([transforms.ToTensor()]) trainset = torchvision.datasets.CIFAR10(root=’./data’, train=True,

download=True, transform=transform)

#将所有的训练图像堆叠成一个形状为 #(50000, 3, 32, 32) 的张量 x = torch.stack([sample[0] for sample in ConcatDataset([trainset])])

#获取每个通道的均值 mean = torch.mean(x, dim=(0,2,3)) #tensor([0.4914, 0.4822, 0.4465]) std = torch.std(x, dim=(0,2,3)) #tensor([0.2470, 0.2435, 0.2616])

```

还有许多其他变换可用,包括裁剪、居中、旋转和反射。

接下来,我们将创建一个 CIFAR10 数据集的实例。这是一组 32x32 的彩色图像块,代表了 10 类对象:6 种动物(鸟、猫、鹿、狗、青蛙、马)和 4 种交通工具(飞机、汽车、船、卡车):

trainset = torchvision.datasets.CIFAR10(root='./data', train=True,
                                        download=True, transform=transform)
  0%|          | 0.00/170M [00:00<?, ?B/s]
  0%|          | 426k/170M [00:00<00:42, 3.96MB/s]
  3%|2         | 4.69M/170M [00:00<00:06, 25.9MB/s]
  6%|5         | 10.1M/170M [00:00<00:04, 38.5MB/s]
  9%|9         | 15.6M/170M [00:00<00:03, 44.8MB/s]
 12%|#2        | 21.1M/170M [00:00<00:03, 48.5MB/s]
 16%|#5        | 26.6M/170M [00:00<00:02, 50.5MB/s]
 19%|#8        | 32.0M/170M [00:00<00:02, 51.6MB/s]
 22%|##1       | 37.5M/170M [00:00<00:02, 52.7MB/s]
 25%|##5       | 43.0M/170M [00:00<00:02, 53.4MB/s]
 28%|##8       | 48.5M/170M [00:01<00:02, 53.8MB/s]
 32%|###1      | 54.0M/170M [00:01<00:02, 54.1MB/s]
 35%|###4      | 59.5M/170M [00:01<00:02, 54.1MB/s]
 38%|###8      | 64.9M/170M [00:01<00:01, 54.1MB/s]
 41%|####1     | 70.5M/170M [00:01<00:01, 54.4MB/s]
 45%|####4     | 75.9M/170M [00:01<00:01, 54.3MB/s]
 48%|####7     | 81.4M/170M [00:01<00:01, 54.3MB/s]
 51%|#####     | 86.8M/170M [00:01<00:01, 54.4MB/s]
 54%|#####4    | 92.7M/170M [00:01<00:01, 55.5MB/s]
 59%|#####9    | 101M/170M [00:01<00:01, 63.5MB/s]
 65%|######5   | 111M/170M [00:02<00:00, 75.7MB/s]
 72%|#######1  | 122M/170M [00:02<00:00, 85.3MB/s]
 78%|#######8  | 133M/170M [00:02<00:00, 92.9MB/s]
 85%|########4 | 144M/170M [00:02<00:00, 97.9MB/s]
 91%|#########1| 155M/170M [00:02<00:00, 102MB/s]
 97%|#########7| 166M/170M [00:02<00:00, 104MB/s]
100%|##########| 170M/170M [00:02<00:00, 66.7MB/s]

当您运行上面的单元格时,下载数据集可能需要一些时间。

这是在 PyTorch 中创建数据集对象的一个示例。可下载的数据集(如上文的 CIFAR-10)是 torch.utils.data.Dataset 的子类。PyTorch 中的 Dataset 类包括 TorchVision、Torchtext 和 TorchAudio 中的可下载数据集,以及一些实用的数据集类,例如 torchvision.datasets.ImageFolder,它可以读取带有标签的图像文件夹。您也可以创建自己的 Dataset 子类。

当我们实例化数据集时,需要指定一些内容:

  • 我们希望数据存储的文件系统路径。

  • 是否将此数据集用于训练;大多数数据集会分为训练子集和测试子集。

  • 如果尚未下载数据集,是否希望下载。

  • 我们希望应用于数据的转换操作。

准备好数据集后,您可以将其传递给 DataLoader

trainloader = torch.utils.data.DataLoader(trainset, batch_size=4,
                                          shuffle=True, num_workers=2)

Dataset 子类封装了对数据的访问,并根据其提供的数据类型进行专门化。DataLoader 对数据本身一无所知,但它会根据您指定的参数将 Dataset 提供的输入张量组织成批次。

在上面的示例中,我们要求 DataLoadertrainset 中提供每批 4 张图像,随机打乱它们的顺序 (shuffle=True),并告诉它启动两个工作线程从磁盘加载数据。

可视化 DataLoader 提供的批次是一个好习惯:

importmatplotlib.pyplotasplt
importnumpyasnp

classes = ('plane', 'car', 'bird', 'cat',
           'deer', 'dog', 'frog', 'horse', 'ship', 'truck')

defimshow(img):
    img = img / 2 + 0.5     # unnormalize
    npimg = img.numpy()
    plt.imshow(np.transpose(npimg, (1, 2, 0)))


# get some random training images
dataiter = iter(trainloader)
images, labels = next(dataiter)

# show images
imshow(torchvision.utils.make_grid(images))
# print labels
print(' '.join('%5s' % classes[labels[j]] for j in range(4)))

introyt1 tutorial

Clipping input data to the valid range for imshow with RGB data ([0..1] for floats or [0..255] for integers). Got range [-0.49473685..1.5632443].
 ship   car horse  ship

运行上述单元格应该会显示四张图像及其对应的正确标签。

训练您的 PyTorch 模型

17:10 开始跟随视频操作。

让我们将所有部分整合起来,并训练一个模型:

#%matplotlib inline

importtorch
importtorch.nnasnn
importtorch.nn.functionalasF
importtorch.optimasoptim

importtorchvision
importtorchvision.transformsastransforms

importmatplotlib
importmatplotlib.pyplotasplt
importnumpyasnp

首先,我们需要训练和测试数据集。如果您还没有准备好,请运行下面的单元格以确保数据集已下载。(这可能需要一分钟。)

transform = transforms.Compose(
    [transforms.ToTensor(),
     transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))])

trainset = torchvision.datasets.CIFAR10(root='./data', train=True,
                                        download=True, transform=transform)
trainloader = torch.utils.data.DataLoader(trainset, batch_size=4,
                                          shuffle=True, num_workers=2)

testset = torchvision.datasets.CIFAR10(root='./data', train=False,
                                       download=True, transform=transform)
testloader = torch.utils.data.DataLoader(testset, batch_size=4,
                                         shuffle=False, num_workers=2)

classes = ('plane', 'car', 'bird', 'cat',
           'deer', 'dog', 'frog', 'horse', 'ship', 'truck')

我们将在 DataLoader 的输出上运行检查:

importmatplotlib.pyplotasplt
importnumpyasnp

# functions to show an image


defimshow(img):
    img = img / 2 + 0.5     # unnormalize
    npimg = img.numpy()
    plt.imshow(np.transpose(npimg, (1, 2, 0)))


# get some random training images
dataiter = iter(trainloader)
images, labels = next(dataiter)

# show images
imshow(torchvision.utils.make_grid(images))
# print labels
print(' '.join('%5s' % classes[labels[j]] for j in range(4)))

introyt1 tutorial

cat   cat  deer  frog

这是我们将要训练的模型。如果它看起来很熟悉,那是因为它是 LeNet 的一个变体——在本视频的前面部分讨论过——并适配了 3 色图像。

classNet(nn.Module):
    def__init__(self):
        super(Net, self).__init__()
        self.conv1 = nn.Conv2d(3, 6, 5)
        self.pool = nn.MaxPool2d(2, 2)
        self.conv2 = nn.Conv2d(6, 16, 5)
        self.fc1 = nn.Linear(16 * 5 * 5, 120)
        self.fc2 = nn.Linear(120, 84)
        self.fc3 = nn.Linear(84, 10)

    defforward(self, x):
        x = self.pool(F.relu(self.conv1(x)))
        x = self.pool(F.relu(self.conv2(x)))
        x = x.view(-1, 16 * 5 * 5)
        x = F.relu(self.fc1(x))
        x = F.relu(self.fc2(x))
        x = self.fc3(x)
        return x


net = Net()

我们最后需要的组件是损失函数和优化器:

criterion = nn.CrossEntropyLoss()
optimizer = optim.SGD(net.parameters(), lr=0.001, momentum=0.9)

正如本视频前面所讨论的,损失函数 是衡量模型预测结果与理想输出之间差距的指标。对于像我们这样的分类模型,交叉熵损失 是一种典型的损失函数。

优化器 是驱动学习的关键。这里我们创建了一个实现 随机梯度下降 的优化器,这是较为直接的优化算法之一。除了算法的参数,例如学习率(lr)和动量,我们还传入了 net.parameters(),这是模型中所有可学习权重的集合——优化器正是通过调整这些权重来工作的。

最后,所有这些都被整合到训练循环中。请继续运行这个单元,因为它可能需要几分钟才能完成执行:

for epoch in range(2):  # loop over the dataset multiple times

    running_loss = 0.0
    for i, data in enumerate(trainloader, 0):
        # get the inputs
        inputs, labels = data

        # zero the parameter gradients
        optimizer.zero_grad()

        # forward + backward + optimize
        outputs = net(inputs)
        loss = criterion(outputs, labels)
        loss.backward()
        optimizer.step()

        # print statistics
        running_loss += loss.item()
        if i % 2000 == 1999:    # print every 2000 mini-batches
            print('[%d, %5d] loss: %.3f' %
                  (epoch + 1, i + 1, running_loss / 2000))
            running_loss = 0.0

print('Finished Training')
[1,  2000] loss: 2.195
[1,  4000] loss: 1.876
[1,  6000] loss: 1.655
[1,  8000] loss: 1.576
[1, 10000] loss: 1.519
[1, 12000] loss: 1.466
[2,  2000] loss: 1.421
[2,  4000] loss: 1.376
[2,  6000] loss: 1.336
[2,  8000] loss: 1.335
[2, 10000] loss: 1.326
[2, 12000] loss: 1.270
Finished Training

在这里,我们只进行 2 个训练周期(第 1 行)——即对训练数据集进行两次遍历。每次遍历都有一个内部循环,该循环会迭代训练数据(第 4 行),提供经过变换的输入图像批次及其正确标签。

梯度清零(第 9 行)是一个重要步骤。梯度会在一个批次中累积;如果我们不为每个批次重置它们,它们会持续累积,这将导致错误的梯度值,使学习无法进行。

在第 12 行,我们要求模型对该批次进行预测。在接下来的第 13 行中,我们计算损失 —— outputs(模型预测)与 labels(正确输出)之间的差异。

在第 14 行,我们进行 backward() 反向传播,并计算将指导学习的梯度。

在第 15 行,优化器执行一次学习步骤 —— 它使用 backward() 调用中的梯度,将学习权重朝着它认为会减少损失的方向调整。

循环的其余部分会对周期编号、已完成多少训练实例以及训练循环中收集的损失进行一些简要报告。

当你运行上面的单元格时,你应该会看到类似如下的输出:

[1,2000]loss:2.235
[1,4000]loss:1.940
[1,6000]loss:1.713
[1,8000]loss:1.573
[1,10000]loss:1.507
[1,12000]loss:1.442
[2,2000]loss:1.378
[2,4000]loss:1.364
[2,6000]loss:1.349
[2,8000]loss:1.319
[2,10000]loss:1.284
[2,12000]loss:1.267
FinishedTraining

需要注意的是,损失值呈单调下降趋势,这表明我们的模型在训练数据集上的性能正在持续提升。

最后一步,我们应该验证模型是否在进行泛化学习,而不是仅仅“记忆”数据集。这种现象被称为过拟合,通常意味着数据集太小(没有足够的样本进行泛化学习),或者模型的参数过多,超出了正确建模数据集所需的范围。

这也是为什么数据集被划分为训练集和测试集的原因——为了测试模型的泛化能力,我们要求它对未训练过的数据进行预测:

correct = 0
total = 0
with torch.no_grad():
    for data in testloader:
        images, labels = data
        outputs = net(images)
        _, predicted = torch.max(outputs.data, 1)
        total += labels.size(0)
        correct += (predicted == labels).sum().item()

print('Accuracy of the network on the 10000 test images: %d%%' % (
    100 * correct / total))
Accuracy of the network on the 10000 test images: 54 %

如果您按照步骤操作,您应该会看到模型此时的准确率大约为 50%。虽然这并不算是最先进的表现,但比起随机输出的 10% 准确率要好得多。这表明模型确实进行了一些通用的学习。

本页目录