torch.nn入门
PyTorch 提供设计优雅的模块和类torch.nn,torch.optim,Dataset 和 DataLoader来帮助您创建和训练神经网络
为了充分利用它们的功能并针对您的问题对其进行自定义,您需要真正地了解他们的工作
为了建立这种理解,我们将首先在 MNIST 数据集上训练基本神经网络,而无需使用这些模型的任何功能; 我们最初只会使用最基本的 PyTorch 张量功能。
然后,我们将一次从torch.nn
,torch.optim
,Dataset
或DataLoader
中逐个添加一个功能,确切地显示每个功能,以及如何使代码更简洁或更灵活
本教程假定您已经安装了 PyTorch,并且熟悉张量操作的基础知识
(如果您熟悉 Numpy 数组操作,将会发现此处使用的 PyTorch 张量操作几乎相同)
MNIST 数据设置
我们将使用经典的MNIST数据集,该数据集由手绘数字的黑白图像组成(介于 0 到 9 之间)
我们将使用pathlib 处理路径(Python 3 标准库的一部分),并下载数据集
我们只会在使用模块时才导入它们,因此您可以确切地看到正在使用模块的每个细节
1 | from pathlib import Path |
该数据集为 numpy 数组格式,并已使用 pickle(一种用于序列化数据的 python 特定格式)存储
1 | import pickle |
每个图像为 28 x 28,并存储被拍平长度为 784(= 28x28)的向量,让我们来看一个
我们需要先将其重塑为 2d
1 | from matplotlib import pyplot |
正常输出应该是一个数组和MNIST包当中的随机的一张图片,例如
1 | (50000, 784) |
我们可以看到输出了(50000,784),这表明包内有50000个数据
PyTorch 使用torch.tensor
而不是 numpy 数组,因此我们需要转换数据
1 | import torch |
正常的输出应该是两个张量内容及张量的尺寸和最大维度,例如
1 | tensor([[0., 0., 0., ..., 0., 0., 0.], |
从零开始的神经网络(无 torch.nn)
首先,我们仅使用 PyTorch 张量操作创建模型。 我们假设您已经熟悉神经网络的基础知识。 (如果您不是,则可以在course.fast.ai中学习它们)。
PyTorch 提供了创建随机或零填充张量的方法,我们将使用它们来为简单的线性模型创建权重和偏差。 这些只是常规张量,还有一个非常特殊的附加值:我们告诉 PyTorch 它们需要梯度。 这使 PyTorch 记录了在张量上完成的所有操作,因此它可以在反向传播时自动地计算梯度!
对于权重,我们在初始化之后设置requires_grad
,因为我们不希望该步骤包含在梯度中。 (请注意,PyTorch 中的尾随_表示该操作是就地执行的。)
我们在这里用Xavier初始化(通过乘以 1 / sqrt(n))来初始化权重
1 | import math |
由于 PyTorch 具有自动计算梯度的功能,我们可以将任何标准的 Python 函数(或可调用对象)用作模型
因此,让我们编写一个简单的矩阵乘法和广播加法来创建一个简单的线性模型
我们还需要激活函数,因此我们将编写并使用
请记住:尽管 PyTorch 提供了许多预先编写的损失函数,激活函数等,但是您可以使用纯 Python 轻松编写自己的函数
PyTorch 甚至会自动为您的函数创建快速 GPU 或矢量化的 CPU 代码
1 | def log_softmax(x): |
在上面,@
代表点积运算。 我们将对一批数据(在这种情况下为 64 张图像)调用函数。 这是一个前向传播
请注意,由于我们从随机权重开始,因此在这一阶段,我们的预测不会比随机预测更好
1 | bs = 64 # batch size |
输出为
1 | tensor([-2.5454, -2.5716, -1.7979, -2.6673, -2.4757, -2.4538, -2.1775, -1.9078, |
如您所见,preds
张量不仅包含张量值,还包含梯度函数。 稍后我们将使用它进行反向传播
让我们实现负对数似然作为损失函数(同样,我们只能使用标准 Python)
1 | def nll(input, target): |
让我们用随机模型来检查损失,以便我们以后看向后传播后是否可以改善
1 | yb = y_train[0:bs] |
输出为
1 | tensor(2.3733, grad_fn=<NegBackward0>) |
我们还实现一个函数来计算模型的准确性。 对于每个预测,如果具有最大值的索引与目标值匹配,则该预测是正确的
1 | def accuracy(out, yb): |
让我们检查一下随机模型的准确性,以便我们可以看出随着损失的增加,准确性是否有所提高
1 | print(accuracy(preds, yb)) |
输出为
1 | tensor(0.0469) |
现在,我们可以运行一个训练循环
对于每次迭代,我们将:
- 选择一个小批量数据(大小为
bs
) - 使用模型进行预测
- 计算损失
loss.backward()
更新模型的梯度,在这种情况下为weights
和bias
现在,我们使用这些梯度来更新权重和偏差
我们在torch.no_grad()
上下文管理器中执行此操作,因为我们不希望在下一步的梯度计算中记录这些操作
您可以在上阅读有关 PyTorch 的 Autograd 如何记录操作的更多信息
然后,将梯度设置为零,以便为下一个循环做好准备
否则,我们的梯度会记录所有已发生操作的运行记录(即loss.backward()
将梯度添加到已存储的内容中,而不是替换它们)
可以使用标准的 python 调试器逐步浏览 PyTorch 代码,从而可以在每一步检查各种变量值
取消注释以下set_trace()
即可尝试
1 | from IPython.core.debugger import set_trace |
就是这样:我们完全从头开始创建并训练了一个最小的神经网络(在这种情况下,是逻辑回归,因为我们没有隐藏的层)
让我们检查损失和准确性,并将其与我们之前获得的进行比较
我们希望损失会减少,准确性会增加,而且确实如此
1 | print(loss_func(model(xb), yb), accuracy(model(xb), yb)) |
输出为
1 | tensor(0.0803, grad_fn=<NegBackward0>) tensor(1.) |
使用 torch.nn.functional
现在,我们将重构代码,使其与以前相同,只是我们将开始利用 PyTorch 的nn
类使其更加简洁和灵活
从这里开始的每一步,我们都应该使代码达成一个或多个:更短,更易理解和/或更灵活
第一步也是最简单的步骤,就是用torch.nn.functional
(通常按照惯例将其导入到名称空间F中)替换我们的手写激活和损失函数,从而缩短代码长度
该模块包含torch.nn
库中的所有函数(而该库的其他部分包含类)
除了广泛的损失和激活函数外,您还会在这里找到一些合适的函数来创建神经网络,例如池化函数
还有一些用于进行卷积,线性图层等的函数,但是正如我们将看到的那样,通常可以使用库的其他部分来更好地处理这些函数
如果您使用的是负对数似然损失和 log softmax 激活,那么 Pytorch 会提供将两者结合的单个函数F.cross_entropy
因此,我们甚至可以从模型中删除激活函数
1 | import torch.nn.functional as F |
请注意,我们不再在model函数中调用log_softmax
让我们确认我们的损失和准确性与以前相同
1 | print(loss_func(model(xb), yb), accuracy(model(xb), yb)) |
输出为
1 | tensor(0.0803, grad_fn=<NllLossBackward0>) tensor(1.) |
使用 nn.Module 进行重构
接下来,我们将使用nn.Module
和nn.Parameter
进行更清晰,更简洁的训练循环
我们将nn.Module
子类化(它本身是一个类并且能够跟踪状态)
在这种情况下,我们要创建一个类,该类包含前进步骤的权重,偏差和方法
nn.Module
具有许多我们将要使用的属性和方法,例如.parameters()
和.zero_grad()
nn.Module
(大写 M)是 PyTorch 的特定概念,也是我们将经常使用的一个类
nn.Module
不要与(小写m)模块的 Python 概念混淆,该模块是可以导入的 Python 代码文件
1 | from torch import nn |
由于我们现在使用的是对象而不是仅使用函数,因此我们首先必须实例化模型
1 | model = Mnist_Logistic() |
现在我们可以像以前一样计算损失
请注意,nn.Module
对象的使用就像它们是函数一样(即,它们是可调用的)
但是在后台 Pytorch 会自动调用我们的forward
方法
1 | print(loss_func(model(xb), yb)) |
输出为
1 | tensor(2.4099, grad_fn=<NllLossBackward0>) |
以前,在我们的训练循环中,我们必须按名称更新每个参数的值
并手动将每个参数的 grads 分别归零,如下所示
1 | with torch.no_grad(): |
现在我们可以利用 model.parameters()和 model.zero_grad()
它们都由 PyTorch 为nn.Module
定义
从而使这些步骤更简洁,并且更不会出现忘记某些参数的错误
特别是当我们有一个更复杂的模型的时候
1 | with torch.no_grad(): |
我们将把小的训练循环包装在fit
函数中,以便稍后再运行
1 | def fit(): |
让我们仔细检查一下我们的损失是否下降了
1 | print(loss_func(model(xb), yb)) |
输出为
1 | tensor(0.0806, grad_fn=<NllLossBackward0>) |
使用 nn.Linear 重构
我们继续重构我们的代码
代替手动定义和初始化self.weights
和self.bias
并计算xb @ self.weights + self.bias
,我们将对线性层使用 Pytorch 类 nn.Linear ,这将为我们完成所有工作
Pytorch 具有许多类型的预定义层,可以大大简化我们的代码,并且通常也可以使其速度更快
1 | class Mnist_Logistic(nn.Module): |
我们用与以前相同的方式实例化模型并计算损失
1 | model = Mnist_Logistic() |
输出为
1 | tensor(2.2902, grad_fn=<NllLossBackward0>) |
我们仍然可以使用与以前相同的fit方法
1 | fit() |
输出为
1 | tensor(0.0812, grad_fn=<NllLossBackward0>) |
使用优化重构
Pytorch 还提供了一个包含各种优化算法的软件包torch.optim
我们可以使用优化器中的step
方法采取向前的步骤,而不是手动更新每个参数
这就是我们将要替换之前手动编码的优化步骤
1 | with torch.no_grad(): |
我们只需使用下面的代替
1 | opt.step() |
optim.zero_grad()
将梯度重置为 0,我们需要在计算下一个小批量的梯度之前调用它。
1 | from torch import optim |
我们将定义一个小函数来创建模型和优化器,以便将来再次使用
1 | def get_model(): |
输出为
1 | tensor(2.3324, grad_fn=<NllLossBackward0>) |
使用数据集进行重构
PyTorch 有一个抽象的 Dataset 类
数据集可以是具有__len__
函数(由 Python 的标准len
函数调用)和具有__getitem__
函数作为对其进行索引的一种方法
本教程演示了一个不错的示例,该示例创建一个自定义FacialLandmarkDataset
类作为Dataset
的子类
PyTorch 的TensorDataset是一个数据集包装张量。 通过定义索引的长度和方式,这也为我们提供了沿张量的一维进行迭代,索引和切片的方法
这将使我们在训练的同一行中更容易访问自变量和因变量
1 | from torch.utils.data import TensorDataset |
x_train
和y_train
都可以合并为一个TensorDataset
,这将更易于迭代和切片
1 | train_ds = TensorDataset(x_train, y_train) |
以前,我们不得不分别遍历 x 和 y 值的迷你批处理
1 | xb = x_train[start_i:end_i] |
现在,我们可以将两个步骤一起执行
1 | xb,yb = train_ds[i * bs : i * bs + bs] |
1 | model, opt = get_model() |
输出为
1 | tensor(0.0833, grad_fn=<NllLossBackward0>) |
使用 DataLoader 进行重构
Pytorch的DataLoader
负责批次管理。 您可以从任何Dataset
创建一个DataLoader
DataLoader
使迭代迭代变得更加容易
不必使用train_ds[i * bs : i * bs + bs]
,DataLoader
会自动为我们提供每个小批量
1 | from torch.utils.data import DataLoader |
以前,我们的循环遍历批处理(xb,yb),如下所示
1 | for i in range((n-1)//bs + 1): |
现在,我们的循环更加简洁了,因为(xb,yb)是从数据加载器自动加载的
1 | for xb,yb in train_dl: |
1 | model, opt = get_model() |
输出为
1 | tensor(0.0820, grad_fn=<NllLossBackward0>) |
得益于 Pytorch的nn.Module
,nn.Parameter
,Dataset
和DataLoader
,我们的训练循环现在变得更小,更容易理解
现在,让我们尝试添加在实践中创建有效模型所需的基本功能
添加验证
在第 1 部分中,我们只是试图建立一个合理的训练循环以用于我们的训练数据
实际上,您总是也应该具有验证集,以便识别您是否过度拟合
打乱训练数据顺序对于防止批次与过度拟合之间的相关性很重要
另一方面,无论我们是否打乱验证集,验证损失都是相同的
由于打乱顺序需要花费更多时间,因此打乱验证集数据顺序没有任何意义
我们将验证集的批次大小设为训练集的两倍
这是因为验证集不需要反向传播,因此占用的内存更少(不需要存储渐变)
我们利用这一优势来使用更大的批量,并更快地计算损失
1 | train_ds = TensorDataset(x_train, y_train) |
我们将在每个 epoch 结束时计算并打印验证损失
(请注意,我们总是在训练之前调用model.train()
,并在推断之前调用model.eval()
,因为诸如nn.BatchNorm2d
和nn.Dropout
之类的图层会使用它们,以确保这些不同阶段的行为正确)
1 | model, opt = get_model() |
输出为
1 | 0 tensor(0.3302) |
创建 fit()和 get_data()
现在,我们将自己进行一些重构
由于我们经历了两次相似的过程来计算训练集和验证集的损失,因此我们将其设为自己的函数loss_batch,该函数可计算一批损失
我们将优化器传入训练集中,并使用它执行反向传播
对于验证集,我们没有通过优化程序,因此该方法不会执行反向传播
1 | def loss_batch(model, loss_func, xb, yb, opt=None): |
fit
运行必要的操作来训练我们的模型,并计算每个时期的训练和验证损失
1 | import numpy as np |
get_data
返回用于训练和验证集的数据加载器
1 | def get_data(train_ds, valid_ds, bs): |
现在,我们获取数据加载器和拟合模型的整个过程可以在3行代码中运行
1 | train_dl, valid_dl = get_data(train_ds, valid_ds, bs) |
输出为
1 | 0 0.319864363270998 |
您可以使用这些基本的 3 行代码来训练各种各样的模型
让我们看看是否可以使用它们来训练卷积神经网络(CNN)
切换到 CNN
现在,我们将构建具有三个卷积层的神经网络
由于上一节中的所有函数都不包含任何有关模型组合的内容,因此我们将能够使用它们来训练 CNN,而无需进行任何修改
我们将使用 Pytorch 的预定义Conv2d类作为我们的卷积层
我们定义具有 3 个卷积层的 CNN。 每个卷积后跟一个 ReLU
最后,我们执行平均池化(请注意,view
是 numpy 的reshape
的 PyTorch 版本)
1 | class Mnist_CNN(nn.Module): |
动量是随机梯度下降的一种变体,它也考虑了以前的更新,通常可以加快训练速度
1 | model = Mnist_CNN() |
输出为
1 | 0 0.40830671231746674 |
nn.Sequential
torch.nn还有另一个灵活的类,可以用来简化我们的代码:Sequential
Sequential
对象以顺序方式运行其中包含的每个模块。 这是编写神经网络的一种简单方法
要利用此优势,我们需要能够从给定的函数轻松定义自定义层
例如,PyTorch 没有视图图层,我们需要为网络创建一个图层
Lambda
将创建一个层,然后在使用Sequential
定义网络时可以使用该层
1 | class Lambda(nn.Module): |
用Sequential
创建的模型很简单
1 | model = nn.Sequential( |
输出为
1 | 0 0.39486207270622253 |
包装 DataLoader
虽然我们的 CNN 网络很简洁,但是它只能在 MNIST 数据集上面有效,因为
- MNIST 数据集假设输入为 28 * 28 长向量
- MNIST 数据集假设 CNN 的最终网格尺寸为 4 * 4(这是因为我们使用的平均池化卷积核的大小)
让我们摆脱这两个假设,因此我们的模型需要适用于任何 2d 单通道图像
首先,我们可以删除初始的 Lambda 层,并将数据预处理移至生成器中
1 | def preprocess(x, y): |
接下来,我们可以将nn.AvgPool2d
替换为nn.AdaptiveAvgPool2d
,这使我们可以定义所需的输出张量的大小,而不是所需的输入张量的大小
结果,我们的模型将适用于任何大小的输入
1 | model = nn.Sequential( |
试试看
1 | fit(epochs, model, loss_func, opt, train_dl, valid_dl) |
输出为
1 | 0 0.3495198380470276 |
使用您的 GPU
如果您足够幸运地能够使用具有 CUDA 功能的 GPU(您可以从大多数云提供商处以每小时$ 0.50 的价格租用一个 GPU),则可以使用它来加速代码
首先检查您的 GPU 是否在 Pytorch 中正常工作
1 | print(torch.cuda.is_available()) |
如果GPU可用,则会有以下输出
1 | True |
然后为其创建一个设备对象
1 | dev = torch.device("cuda") if torch.cuda.is_available() else torch.device("cpu") |
让我们更新preprocess
,将批次移至 GPU
1 | def preprocess(x, y): |
最后,我们可以将模型移至 GPU
1 | model.to(dev) |
您应该发现它现在运行得更快
1 | fit(epochs, model, loss_func, opt, train_dl, valid_dl) |
输出为
1 | 0 0.2331619877576828 |
总结思想
现在,我们有了一个通用的数据管道和训练循环,您可以将其用于使用 Pytorch 训练多种类型的模型。 要了解现在可以轻松进行模型训练,请查看 mnist_sample 示例笔记本
当然,您需要添加很多内容,例如数据增强,超参数调整,监控训练,转移学习等
这些功能在 fastai 库中可用,该库是使用本教程中所示的相同设计方法开发的,为希望进一步推广模型的从业人员提供了自然的下一步
我们承诺在本教程开始时将通过示例分别说明torch.nn
,torch.optim
,Dataset
和DataLoader
因此,让我们总结一下我们所看到的:
torch.nnModule
:创建一个类似函数行为功能的,但可以包含状态(例如神经网络层权重)的可调用对象。它知道它包含的Parameter
,并且可以将其所有梯度归零,通过其循环进行权重更新等
Parameter
:张量的包装器,它告诉Module具有在反向传播期间需要更新的权重
仅更新具有 require_grad 属性集的张量
functional
:一个模块(通常按照常规导入到F名称空间中),包含激活函数,损失函数等
以及卷积和线性层之类的无状态版本
torch.optim
:包含其中SGD之类的优化程序,这些优化程序可以在反向传播期间更新权重参数
Dataset
:一个具有__len__
和__getitem__
的抽象接口对象,包括 Pytorch 提供的类,例如Tensor
DatasetDataLoader
:获取任何Dataset
并创建一个迭代器,该迭代器返回批量数据