赞
踩
引言
在本文中,我们将在 PyTorch 中构建一个非常简单的神经网络来进行手写数字的分类。首先,我们将开始探索 MNIST 数据集,解释我们如何加载和格式化数据。然后,我们将跳转到激励和实施 Logistic regression 模型,包括前向和反向传播,损失函数和优化器。在训练模型之后,我们将评估我们是如何做的,并将我们所学到的可视化。最后,我们将使用更高级的 API,以面向对象的方式重构代码。
在我们开始之前,我们将要使用的软件包的一些导入:
- %matplotlib inline
- import numpy as np
- import matplotlib.pyplot as plt
- import torch
- from tqdm.notebook import tqdm
MNIST 数据集
MNIST 数据集是一个非常流行的机器学习数据集,由70000个手写数字的灰度图像组成,维数为28x28。我们将使用它作为本教程这一部分的示例数据集,目标是预测每个图像中的数字。
机器学习的第一步(也是最重要的一步)是准备数据。这可以包括下载、组织、格式化、转移、预处理、增强和批处理示例,以便将它们提供给模型。torchvision 包通过实现其中的许多功能使这一过程变得简单,允许我们只用几行代码就可以将这些数据集转换成可用的形式。首先,让我们下载 MNIST 的训练集和测试集:
- from torchvision import datasets, transforms
- mnist_train = datasets.MNIST(root="./datasets", train=True, transform=transforms.ToTensor(), download=True)
- mnist_test = datasets.MNIST(root="./datasets", train=False, transform=transforms.ToTensor(), download=True)
- print("Number of MNIST training examples: {}".format(len(mnist_train)))
- print("Number of MNIST test examples: {}".format(len(mnist_test)))
正如我们所期望的,60000个 MNIST 示例在训练集中,其余的在测试集中。我们在格式化数据集时添加了转换 ToTensor() ,将来自 Pillow Image 类型的输入数据转换为 PyTorch Tensor。Tensor 最终将成为我们输入到模型中的输入类型。
让我们来看一个来自训练集及其标签的示例图像。注意,图像张量默认为三维的。第一维中的“1”表示图像只有一个通道(即灰度)。我们需要摆脱这一点,使图像可以通过 imshow 函数进行可视化。
- # Pick out the 4th (0-indexed) example from the training set
- image, label = mnist_train[3]
- # Plot the image
- print("Default image shape: {}".format(image.shape))
- image = image.reshape([28,28])
- print("Reshaped image shape: {}".format(image.shape))
- plt.imshow(image, cmap="gray")
- # Print the label
- print("The label for this image: {}".format(label))
虽然我们可以直接将数据作为 torchvision.dataset 来处理,但我们会发现使用 DataLoader 非常有用,它可以进行随机化和批处理:
- train_loader = torch.utils.data.DataLoader(mnist_train, batch_size=100, shuffle=True)
- test_loader = torch.utils.data.DataLoader(mnist_test, batch_size=100, shuffle=False)
从 DataLoader 获得的小批量示例:
- data_train_iter = iter(train_loader)
- images, labels = data_train_iter.next()
- print("Shape of the minibatch of images: {}".format(images.shape))
- print("Shape of the minibatch of labels: {}".format(labels.shape))
Logistic Regression 模型
现在我们已经对如何加载数据有了很好的了解,接下来让我们开始组合模型。在这个教程中,我们将建立一个 Logistic regression 模型,它本质上是一个没有任何隐藏层的全连接神经网络。虽然相当基础,但是 Logistic regression 模型分类在许多简单的分类任务上表现出色得令人惊讶。
前向传播
虽然我们的数据输入(我们称之为 x)是图像(即二维) ,但是 MNIST 数字非常小,而且我们使用的模型非常简单。因此,我们将把输入视为平面向量。为了将输入转换为行向量(也称为扁平化) ,我们可以使用 NumPy 的 reshape()。与 NumPy 一样,我们也可以将重塑的一个维度替换为 -1,它告诉 PyTorch 根据原始维度和其他指定维度推断该维度。让我们尝试在上一节我们绘制的100张图像的小批量上进行这种扁平化。
- x = images.view(-1, 28*28)
- print("The shape of input x: {}".format(x.shape))
为了得到每个数字的预测概率,让我们先从一个数字成为1的概率开始,就像上面的图片一样。对于我们的简单模型,我们可以从应用线性映射开始。也就是说,我们用输入行向量的每个像素 xi 乘以一个权重 wi,1,把它们加在一起,然后加上一个偏差 b1。这相当于“1”类权重和输入之间的点积:
这个结果的大小是 y1,我们认为这与我们认为输入数字是1的可能性有关。y1的值越高,我们就越可能认为输入的图像 x 是1(也就是说,我们希望对于上面的图像 y1得到一个相对较大的值)。请记住,我们最初的目标是识别所有10个数字,所以我们实际上有:
我们可以用矩阵形式来表示:
为了利用并行计算的优势,我们通常在一个小批处理中同时处理多个输入 x。我们可以把每个输入 x 叠加成一个矩阵 x
可视化维度:
在我们的具体示例中,小批量尺寸 mm 为100,数据维度为28 × 28 = 784,类数 c 为10。由于批处理,x 和 y 是矩阵,按照惯例,它们通常被赋予小写的变量名,就好像它们是一个例子一样。我们将在整个过程中使用 x 和 y。
权重 w 和偏差 b 构成了模型的参数。当我们说我们想要“学习模型”时,我们真正要做的是为 w 和 b 中的每个元素找到好的值。在我们开始学习之前,我们需要将参数初始化为一些值,作为起点。在这里,我们并不真正知道最佳值是什么,因此我们将随机初始化 w (使用称为 Xavier 初始化的东西) ,并将 b 设置为一个零向量。
- # Randomly initialize weights W
- W = torch.randn(784, 10)/np.sqrt(784)
- W.requires_grad_()
- # Initialize bias b as 0s
- b = torch.zeros(10, requires_grad=True)
因为 w 和 b 都是我们希望学习的参数,所以我们设置了 require_grad 为 True。这告诉 PyTorch 的 autograd 跟踪这两个变量的梯度,以及依赖于 w 和 b 的所有变量。
使用这些模型参数,我们计算 y:
- # Linear transformation with W and b
- y = torch.matmul(x, W) + b
例如,我们可以看到在我们的小批量的第一个例子的预测是什么样的。记住,数字越大,模型越认为输入的 x 是这个类。
print(y[0,:])
我们可以将这些值(又名 logits) y 解释为概率,如果我们将它们规范化为正值并且加起来等于1。在 Logistic regression 中,我们使用了 softmax:
注意,因为指数函数的范围总是非负的,而且因为我们是规范化的和,softmax 最大实现了期望的性质,产生0到1之间的值和1。如果我们看一下只有2个类的情况,我们会发现 softmax 是二进制 S形函数的多类扩展:
如果我们愿意,我们可以使用上面的公式自己计算 softmax,但是 PyTorch 已经在 torch.nn.functional 中实现了:
- # Option 1: Softmax to probabilities from equation
- py_eq = torch.exp(y) / torch.sum(torch.exp(y), dim=1, keepdim=True)
- print("py[0] from equation: {}".format(py_eq[0]))
- # Option 2: Softmax to probabilities with torch.nn.functional
- import torch.nn.functional as F
- py = F.softmax(y, dim=1)
- print("py[0] with torch.nn.functional.softmax: {}".format(py[0]))
现在,我们已经定义了模型的前向传播:给定一个输入图像,返回模型认为输入是10个类中每一个的概率。
交叉熵损失
这个教程还没有完成,所以你可能会猜到答案还没有完全完成。我们还不知道 W 和 b 的值!还记得我们是如何随机初始化它们的吗?在调整任何权重之前,我们需要一种方法来度量模型的运行情况。具体来说,我们将衡量这个模型的表现有多糟糕。我们使用一个损失函数来实现这一点,它获取模型的预测并返回一个数字(即一个标量)来总结模型的性能。这种损失将告诉我们如何调整模型的参数。
我们常用的分类损失是交叉熵,这是信息论中的一个概念。确切地解释交叉熵代表什么稍微超出了本课程的范围,但是你可以把它看作是一种量化一个分布 y ′和另一个 y ′之间的距离的方法。
在我们的例子中,y 是由模型预测的概率集(py) ; y ′是目标分布。目标分布是什么?这是真正的标签,这也是我们希望模型能够预测的。
交叉熵不仅捕获了模型答案的正确程度(最大概率与正确答案相对应) ,也解释了模型答案的置信度。这就鼓励了模型为正确答案产生非常高的概率,同时降低了错误答案的概率,而不是仅仅满足于它是 argmax。
我们在这里集中在监督式学习,一个我们有标签的设置。我们 DataLoader 自动为每个输入包含相应的标签。下面是我们第一次检索小批量产品时的标签:
print(labels.shape)
与 softmax 操作一样,我们可以直接从方程出发,利用 softmax 最大输出实现交叉熵。然而,与 softmax 一样,torch.nn.functional 已经实现了交叉熵损失。
- # Cross-entropy loss from equation
- cross_entropy_eq = torch.mean(-torch.log(py_eq)[range(labels.shape[0]),labels])
- print("cross entropy from equation: {}".format(cross_entropy_eq))
- # Option 2: cross-entropy loss with torch.nn.functional
- cross_entropy = F.cross_entropy(y, labels)
- print("cross entropy with torch.nn.functional.cross_entropy: {}".format(cross_entropy))
请注意,PyTorch 的交叉熵损失由于数值稳定性的原因,将 softmax 极大算子和交叉熵结合到一个操作中。不要做两次 softmax 操作。
反向传播
如果我们没有使用像 PyTorch 这样的深度学习框架,我们将不得不自己完成并导出所有梯度,然后将它们编码到我们的程序中。我们当然仍然可以。然而,随着现代 auto-differentiation 库,它是更快,更容易让计算机做到这一点。
首先,我们需要创建一个优化器。有许多选择,但是因为逻辑回归模型相当简单,我们将使用标准的随机梯度下降,这使得以下更新:
其中 θ 是一个参数,α 是我们的学习率(步长) ,∇????L 是我们损失关于 θ 的梯度。
- # Optimizer
- optimizer = torch.optim.SGD([W,b], lr=0.1)
当我们创建参数 W 和 b 时,我们指出它们需要梯度。为了计算 W 和 b 的梯度,我们调用关于交叉熵损失的 backward() 函数。
cross_entropy.backward()
每个需要梯度的变量现在都有累积的梯度。我们可以在 b 上看到这些例子:
b.grad
为了应用梯度,我们可以使用更新规则 ????????+1=????????−????∇????L 手动更新 W 和 b,但是由于我们有一个优化器,我们可以告诉它为我们执行更新步骤:
optimizer.step()
我们将我们的学习速度设置为0.1,因此 b 已经在 -0.1 * b.grad 之前更新:
b
我们现在已经成功的训练了一个小批量!然而,一小批可能是不够的。此时,我们已经在训练集中的60000个示例中的100个示例上训练模型。为了获得更多的数据,我们需要重复这个过程。
不过还有一件事要记住:梯度计算 backward() 不会覆盖旧的值; 相反,它们会累积。因此,在为下一个小批量计算梯度之前,需要清除渐变缓冲区。
- print("b.grad before zero_grad(): {}".format(b.grad))
- optimizer.zero_grad()
- print("b.grad after zero_grad(): {}".format(b.grad))
模型训练
为了训练这个模型,我们只需要重复我们刚才所做的,从训练集中获得更多的小批量。总结一下,步骤如下:
绘制一个小批量
将缓冲区中的 W 和 b 的梯度设为零
执行前向传播(计算预测,计算损失)
执行反向传播(计算梯度,执行 SGD 步骤)
处理完整个数据集曾经被称为一个 epoch。在许多情况下,我们训练神经网络为多个 epoch,但在这里,一个 epoch 是足够的。我们还用 tqdm 包装 train_loader。这是没有必要的,但它增加了一个方便的进度条,以便我们可以跟踪我们的训练进度。
- # Iterate through train set minibatchs
- for images, labels in tqdm(train_loader):
- # Zero out the gradients
- optimizer.zero_grad()
-
- # Forward pass
- x = images.view(-1, 28*28)
- y = torch.matmul(x, W) + b
- cross_entropy = F.cross_entropy(y, labels)
- # Backward pass
- cross_entropy.backward()
- optimizer.step()
测试
现在让我们看看我们做得怎么样!对于测试集中的每个图像,我们通过模型运行数据,并以我们最有信心的数字作为我们的答案。然后,我们通过计算得到的正确数来计算精度。我们将用 torch.no_grad() 来结束评价,因为我们对评价过程中计算梯度不感兴趣。通过关闭 autograd,我们可以加快评估速度。
- correct = 0
- total = len(mnist_test)
- with torch.no_grad():
- # Iterate through test set minibatchs
- for images, labels in tqdm(test_loader):
- # Forward pass
- x = images.view(-1, 28*28)
- y = torch.matmul(x, W) + b
-
- predictions = torch.argmax(y, dim=1)
- correct += torch.sum((predictions == labels).float())
-
- print('Test accuracy: {}'.format(correct/total))
对于一个简单的模型和几行代码来说,这已经不错了。在我们结束这个例子之前,我们还可以做一件有趣的事情。正常情况下,很难检查模型中的过滤器到底在做什么,但是因为这个模型非常简单,而且权重将数据直接转换为它们的日志,我们实际上可以通过简单地绘制权重来可视化模型的学习内容。结果看起来相当合理:
- # Get weights
- fig, ax = plt.subplots(1, 10, figsize=(20, 2))
- for digit in range(10):
- ax[digit].imshow(W[:,digit].detach().view(28,28), cmap='gray')
正如我们所看到的,模型学习了每个数字的模板。请记住,我们的模型在每个数字和输入的权重之间取一个点积。因此,输入与数字模板匹配的越多,该数字的点积值就越高,这使得模型更有可能预测该数字。
完整代码
整个模型,包括完整的模型定义、训练和评估(但去掉了可视化的权重)作为独立的可运行代码:
- import numpy as np
- import torch
- import torch.nn.functional as F
- from torchvision import datasets, transforms
- from tqdm.notebook import tqdm
- # Load the data
- mnist_train = datasets.MNIST(root="./datasets", train=True, transform=transforms.ToTensor(), download=True)
- mnist_test = datasets.MNIST(root="./datasets", train=False, transform=transforms.ToTensor(), download=True)
- train_loader = torch.utils.data.DataLoader(mnist_train, batch_size=100, shuffle=True)
- test_loader = torch.utils.data.DataLoader(mnist_test, batch_size=100, shuffle=False)
- ## Training
- # Initialize parameters
- W = torch.randn(784, 10)/np.sqrt(784)
- W.requires_grad_()
- b = torch.zeros(10, requires_grad=True)
- # Optimizer
- optimizer = torch.optim.SGD([W,b], lr=0.1)
- # Iterate through train set minibatchs
- for images, labels in tqdm(train_loader):
- # Zero out the gradients
- optimizer.zero_grad()
-
- # Forward pass
- x = images.view(-1, 28*28)
- y = torch.matmul(x, W) + b
- cross_entropy = F.cross_entropy(y, labels)
- # Backward pass
- cross_entropy.backward()
- optimizer.step()
- ## Testing
- correct = 0
- total = len(mnist_test)
- with torch.no_grad():
- # Iterate through test set minibatchs
- for images, labels in tqdm(test_loader):
- # Forward pass
- x = images.view(-1, 28*28)
- y = torch.matmul(x, W) + b
-
- predictions = torch.argmax(y, dim=1)
- correct += torch.sum((predictions == labels).float())
-
- print('Test accuracy: {}'.format(correct/total))
注意:上面直接从完整版本得到的准确性可能与我们最初使用的逐步版本得到的测试准确性略有不同。我们用随机梯度下降训练我们的模型,用“随机”这个词强调训练是一个固有的随机过程。
更高级别的 API
到目前为止,我们主要是用基本的 PyTorch 操作构建神经网络。我们这样做是为了更清楚地了解模型实际上是如何工作的,。当您学习概念和各种框架时,这可能很重要,有时如果您想构建一些新颖的东西,对原理的了解是必要的。
然而,大多数时候,我们确实发现自己在重复相当标准的代码行,这会降低我们的速度。更糟糕的是,它不必要地扰乱了我们的代码,并为 bug 和错误引入了空间。最后,作为研究人员或工程师,我们希望把大部分时间花在最高层次的抽象思维上: 我想在这里增加一个卷积层,然后在那里增加一个完全连接的层,等等。不得不编写所有的小细节会分散我们的注意力,影响我们将想法转化为代码的能力。出于这个原因,PyTorch 具有更高层次的抽象,以帮助加速实现和改进模型组织。虽然有许多方法可以组织 PyTorch 代码,但是一个常见的范例是使用 torch.nn.Module。
面向对象的重构
对我们来说,用面向对象的方式编写模型代码通常是有意义的。为了理解为什么让我们回顾一下线性映射 y = xW + b,这是我们在20世纪90年代逻辑回归模型使用的。我们可以看到,虽然操作由矩阵乘法和加法组成,但是与这个操作相关联的还有两个参数 w 和 b 的实例化,而且这两个参数在概念上属于转换。因此,将这两个参数的实例化与实际转换捆绑在一起是有意义的:
- # Note: illustrative example only; see below for torch.nn usage
- class xW_plus_b:
- def __init__(self, dim_in, dim_out):
- self.W = torch.randn(dim_in, dim_out)/np.sqrt(dim_in)
- self.W.requires_grad_()
- self.b = torch.zeros(dim_out, requires_grad=True)
-
- def forward(self, x):
- return torch.matmul(x, self.W) + self.b
为了使用我们刚才写的内容,我们可以使用它的 __init__()方法(构造函数)创建 xW_plus_b 实例。在这种情况下,我们将把维度设置为784和10,就像我们在上面的 Logistic Regression 模型图例中所做的那样。这将创建一个具有两个参数 w 和 b 的 xW_plus_b 实例。
- # Note: illustrative example only; see below for torch.nn usage
- lin_custom = xW_plus_b(784, 10)
- print("W: {}".format(lin_custom.W.shape))
- print("b: {}".format(lin_custom.b.shape))
在实例化实例之后,我们可以通过调用实例的 forward()函数来执行自定义 xW_plus_b 类的实际线性转换:
- # Note: illustrative example only; see below for torch.nn usage
- x_rand = torch.randn(1,784)
- y = lin_custom.forward(x_rand)
- print(y.shape)
使用 torch.nn
虽然我们当然可以为我们想要使用的操作实现我们自己的类,但是我们不必这样做,因为 PyTorch 已经在 torch.nn 子库中有它们了。
import torch.nn as nn
例如,我们刚才经过的线性变换例子叫做 torch.nn.Linear:
- lin = nn.Linear(784, 10)
- print("Linear parameters: {}".format([p.shape for p in lin.parameters()]))
- y = lin(x_rand)
- print(y.shape)
使用 torch.nn.Module
我们刚才看到的线性类 torch.nn.Linear 是 torch.nn.Module 的一个子类。然而,Module 不必仅仅描述单个操作; 它们还可以定义一系列操作,每个操作也可以是 Module。因此,我们可以把整个神经网络放在一个 Module 中。在这种情况下,模块可以跟踪所有相关的参数,其中一些也可能与子模块相关联(例如 nn.Linear) ,同时也在一个地方定义 forward ()函数。
- class MNIST_Logistic_Regression(nn.Module):
- def __init__(self):
- super().__init__()
- self.lin = nn.Linear(784, 10)
- def forward(self, x):
- return self.lin(x)
在这个特定的示例中,我们不需要链接任何操作,但是当我们转移到更复杂的模型时,我们将看到这个方便。此外,本文还提供了。我们子类化模块中其他一些好的特性。
带 nn.Module 的完整代码
使用 nn. Module 重构我们之前完整的 Logit模型代码:
- import numpy as np
- import torch
- import torch.nn as nn
- import torch.nn.functional as F
- from torchvision import datasets, transforms
- from tqdm.notebook import tqdm
- class MNIST_Logistic_Regression(nn.Module):
- def __init__(self):
- super().__init__()
- self.lin = nn.Linear(784, 10)
- def forward(self, x):
- return self.lin(x)
- # Load the data
- mnist_train = datasets.MNIST(root="./datasets", train=True, transform=transforms.ToTensor(), download=True)
- mnist_test = datasets.MNIST(root="./datasets", train=False, transform=transforms.ToTensor(), download=True)
- train_loader = torch.utils.data.DataLoader(mnist_train, batch_size=100, shuffle=True)
- test_loader = torch.utils.data.DataLoader(mnist_test, batch_size=100, shuffle=False)
- ## Training
- # Instantiate model
- model = MNIST_Logistic_Regression()
- # Loss and Optimizer
- criterion = nn.CrossEntropyLoss()
- optimizer = torch.optim.SGD(model.parameters(), lr=0.1)
- # Iterate through train set minibatchs
- for images, labels in tqdm(train_loader):
- # Zero out the gradients
- optimizer.zero_grad()
-
- # Forward pass
- x = images.view(-1, 28*28)
- y = model(x)
- loss = criterion(y, labels)
- # Backward pass
- loss.backward()
- optimizer.step()
- ## Testing
- correct = 0
- total = len(mnist_test)
- with torch.no_grad():
- # Iterate through test set minibatchs
- for images, labels in tqdm(test_loader):
- # Forward pass
- x = images.view(-1, 28*28)
- y = model(x)
-
- predictions = torch.argmax(y, dim=1)
- correct += torch.sum((predictions == labels).float())
-
- print('Test accuracy: {}'.format(correct/total))
对于一个简单的 Logistic regression 模型,模块可能不那么明显,这样的编程风格允许更快更干净的实现更复杂的模型。
一旦你读到这句话,你就已经完成了在 PyTorch 中构建一个非常简单的神经网络来进行手写数字分类的所有步骤。
以下是你今天的成就总结:
MNIST 数据集
Logistic Regression 模型:前向传播,交叉熵损失,反向传播
模型训练
模型测试
其他APIs:Object-oriented Refactorization, torch.nn.Module
· END ·
HAPPY LIFE
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。