该教程是在notebook上运行的,而不是脚本,下载notebook文件。
PyTorch提供了设计优雅的模块和类:torch.nn, torch.optim, Dataset, DataLoader,以创建和训练神经完了过。为了充分利用其功能,并根据问题进行自定义,需要充分理解它们做的是什么。为了提高认知,我们首先在MNIST上训练一个基础的神经网络,而不使用这些模块的任何特性;仅使用最基础的PyTorch tensor函数初始化。然后,一次添加一个来自torch.nn, torch.optim, Dataset, DataLoader的特性,准确展示每个部分的功能,以及如何使代码简洁和更灵活。
该教程假设你已经安装PyTorch,并且对tensor操作的基础知识熟悉。
我们将使用经典的MNIST数据集,它是由手写数字(0-9)的黑白图片组成。
我们将使用pathlib(Python3标准库的一部分)解决有关路径的问题,并使用requests。我们只在使用他们时才导入,这样就可以准确地看到每一部分都在使用什么。
from pathlib import Pathimport requestsDATA_PATH = Path('data')PATH = DATA_PATH / "mnist"PATH.mkdir(parents=True, exist_ok=True)URL = "https://github.com/pytorch/tutorials/raw/master/_static/"FILENAME = "mnist.pkl.gz"if not (PATH / FILENAME).exits(): content = requests.get(URL + FILENAME).content (PATH / FILENAME).open('wb').write(content)数据集是Numpy数组格式,使用pickle(python中用于序列化数据的特定格式)保存。
import pickleimport gzipwith gzip.open((PATH / FILENAME).as_posix(), 'rb') as f: ((x_train, y_train), (x_valid, y_valid), _) = pickle.load(f, encoding='latin-1')每个图片的大小是28*28,被保存成展平的行,长度是784。查看其中一个,首先需要将其reshape至二维。
from matplotlib import pyplotimport numpy as nppyplot.imshow(x_train[0].reshape((28, 28)), cmap = 'gray')print(x_train.shape)
输出:
(50000, 784)PyTorch使用torch.tensor,而不是numpy数组,所以我们进行数据转换。
import torchx_train, y_train, x_valid, y_valid = map( torch.tensor, (x_train, y_train, x_valid, y_valid))n, c = x_train.shapeprint(x_train, y_train)print(x_train.shape)print(y_train.min(), y_train.max())输出:
tensor([[0., 0., 0., ..., 0., 0., 0.], [0., 0., 0., ..., 0., 0., 0.], [0., 0., 0., ..., 0., 0., 0.], ..., [0., 0., 0., ..., 0., 0., 0.], [0., 0., 0., ..., 0., 0., 0.], [0., 0., 0., ..., 0., 0., 0.]]) tensor([5, 0, 4, ..., 8, 4, 8])torch.Size([50000, 784])tensor(0) tensor(9)首先仅使用PyTorch tensor操作创建模型。假设你已经对神经网络基础知识熟悉。
PyTorch提供了创建随机值或0值tensors的方法,可以用来为简单的线性模型构建权重和偏置。这些只是常规的tensors,还有一个非常特殊的补充:告诉PyTorch它们需要梯度。使得PyTorch记录发生在tensor上的所有操作,从而可以在自动反向传播中计算梯度。
对于权重,我们在初始化之后设置 requires_grad,因为我们不想让这个步骤包含在梯度更新中。(注意,PyTorch中,后缀_代表in-place操作)。
注意:本文使用Xavier initialisation(乘以 1/sqrt(n) )初始化权重。
import mathweights = torch.randn(784, 10) / math.sqrt(784)weights.requires_grad_()bias = torch.zeros(10, requires_grad=True)得益于PyTorch自动计算梯度的能力,我们可以使用任意标准Python函数(或调用对象)构建模型。所以这里写了一个利用纯矩阵乘法和广播加法创建一个简单的线性模型。还需要一个激活函数,所以我们将编写并使用log_softmax。记住:尽管PyTorch提供了大量写好的损失函数,激活函数等,你依然使用纯python编写它们。PyTorch甚至会自动为你的函数创建快速GPU或矢量化CPU代码。
def log_softmax(X): return x - x.enp().sum(-1).log.unsqueeze(-1)def model(xb): return log_softmax(xb @ weights + bias)其中,@代表矩阵乘法。每个batch(本例64张图片)都会调用我们的函数。这是一个前向传递。注意,在这个阶段我们的预测不会比随机更好,因为我们使用的是随机初始化权重。
bs = 64 # batch sizexb = x_train[0:bs] # a mini-batch from xpreds = model(xb) # predictionsprint(preds[0], preds.shape)输出:
tensor([-1.8235, -2.3674, -2.6933, -2.0418, -2.2708, -2.3946, -2.1448, -2.5031, -2.7917, -2.3786], grad_fn=<SelectBackward0>) torch.Size([64, 10])正如你所见,predstensor不仅包含tensor数值,还有一个梯度函数。接下来我们将使用这个来做反向传播。
现在来实现负对数似然函数作为损失函数(再一次,使用标准Python)
def nll(input, target): return -input[range(target.shape[0]), target].mean()loss_func = nll计算损失,这样稍后在反向传播后看到是否有所提升。
yb = y_train[0:bs]print(loss_func(preds, yb))输出:
tensor(2.2979, grad_fn=<NegBackward0>)使用一个函数计算模型准确率。对于每次预测,如果最大值的索引与目标值一致,则预测正确。
def accuracy(out, yb): preds = torch.argmax(out, dim=1) return (preds == yb).float().mean()检查模型准确率额,这样就可以看到当loss提升时,准确率是否会提升
print(accuracy(preds, yb))输出:
tensor(0.1094)我们现在可以运行一个训练循环了,对于每次循环,包括:
bs)loss.backward()更新模型梯度,在此例中是weights和bias现在使用梯度更新weights和bias。通过torch.no_grad()上下文管理器,因为我们不想让这些行为被记录到下一次梯度计算中。你可以在here阅读更多关于PyTorch的Autograd记录操作。
接下来将梯度置0,准备好下一个循环。否则,我们的梯度将会记录所有已发生操作的运行记录(即loss.backward()将梯度添加到已存储的任何内容中,而不是替换它们)。
TIP:你可以使用标准python中的调试器单步调试PyTroch代码,允许你检查每一步各种变量的值。取消下面的set_trace()注释尝试一下。
from IPython.core.debugger import set_tracelr = 0.5 # learning rateepochs = 2 # how many epochs to train forfor epoch in range(epochs): for i in range((n - 1) // bs + 1)): # set_trace() start_i = i * bs end_i = start_i + bs xb = x_train[start_i:end_i] yb = y_train[start_i:end_i] pred = model(xb) loss = loss_func(pred, yb) loss.backward() with torch.no_grad(): weights -= weights.grad * lr bias -= bias.grad * lr weights.grad.zero_() bias.grad.zero_()以上,我们从0构建和训练了一个小型的神经网络(此例中,一个logistic回归,因为没有隐藏层)。
检查loss和准确率相较于之前如何,loss降低,准确率上升,事实如此。
print(loss_func(model(xb), yb), accuracy(model(xb), yb))输出:
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联合了这两个。所以我们可以甚至将激活函数从我们的模型移除。
import torch.nn.functional as Floss_func = F.cross_entropydef model(xb): return xb @ weights + bias注意,我们不再在model函数中调用log_softmax,确认loss和accuracy是否跟之前一致。
print(loss_func(model(xb), yb), accuracy(model(xb), yb))输出:
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与Python中小写m的module的概念混淆,后者是可以被导入的Python文件。
from torch import nnclass Mnist_Logistic(nn.Module): def __init__(self): super().__init__() self.weights = nn.Parameter(torch.randn(784, 10) / math.sqrt(784)) self.bias = nn.Parameter(torch.zerors(10)) def forward(self, xb): return xb @ self.weights + self.bias因为我们是使用了一个对象而不是函数,我们首先需要实例化模型。
model = Mnist_Logistic()现在就可以像上面那样计算loss。注意nn.Module对象被用作函数(即它们是可调用的),但在底层,Pytorch将自动调用forward方法。
print(loss_func(model(xb), yb))输出:
tensor(2.2899, grad_fn=<NllLossBackward0>)对于训练循环,我们需要根据名字更新每一个参数值,并手动分别将每个参数的梯度置0:
with torch.no_grad(): weights -= weights.grad * lr bias -= bias.grad * lr weights.grad.zero_() bias.grad.zero_()现在,我们可以利用model.parameters()和model.zero_grad()(均是由Pytorch为nn.Module定义的)使这些步骤更加简单,并且不太容易发生忘记某些参数的错误,特别是一些跟家复杂的模型:
with torch.no_grad(): for p in model.parameters(): p -= p.grad * lr model.zero_grad()将训练循环包围在fit函数中,以便在后面再次运行
def fit(): for epoch in range(epochs): for i in range((n - 1) // bs + 1): start_i = i * bs end_i = start_i + bs xb = x_train[start_i:end_i] yb = y_train[start_i:end_i] pred = model(xb) loss = loss_func(pred, yb) loss.backward() with torch.no_grad(): for p in model.parameters(): p -= p.grad * lr model.zero_grad()fit()仔细检查loss是否下降了:
print(loss_func(model(xb), yb))输出:
tensor(0.0835, grad_fn=<NllLossBackward0>)让我们继续重构代码。替代手动定义和初始化self.weights和self.bias,以及计算xb @ self.weights + self.bias,对于一个线性层,我们将使用Pytorch类nn.Linear,可以实现以上所有功能。Pytorch由很多预先定义的layers,可以大大简化和加速我们的代码。
class Mnist_Logistic(nn.Module): def __init__(self): super().__init__() self.lin = nn.Linear(784, 10) def forward(): return self.lin(xb)实例化模型并计算loss
model = Mnist_Logistc()print(loss_func(model(xb), yb))输出:
tensor(2.3694, grad_fn=<NllLossBackward0>)使用fit方法训练:
fit()print(loss_func(model(xb), yb))输出:
tensor(0.0820, grad_fn=<NllLossBackward0>)Pytorch还有包含各种优化算法的库:torch.optim。我们可以使用step方法替代手动更新参数。
这样就可以替代目前手动的优化步骤:
with torch.no_grad(): for p in model.parameters(): p -= p.grad * lr model.zero_grad()而仅用:
opt.step()opt.zero_grad()(optim.zero_grad()重置梯度为0,需要在计算下一个minibatch的梯度前调用)
for torch import optim定义一个小函数创建模型和优化器,以便在之后使用:
def get_model(): model = Mnist_Logistic() return model, optim.SGD(model.parameters(), lr=lr)model, opt = get_modle()print(loss_func(model(xb), yb))for epoch in range(epochs): for i in range((n - 1) // bs + 1): start_i = i * bs end_i = start_i + bs xb = x_train[start_i:end_i] yb = y_train[start_i:end_i] pred = model(xb) loss = loss_func(pred, yb) loss.backward() opt.step() opt.zero_grad()print(loss_func(model(xb), yb))输出:
tensor(2.3185, grad_fn=<NllLossBackward0>)tensor(0.0827, grad_fn=<NllLossBackward0>)PyTorch有一个抽象的Dataset类。一个Dataset可以是任意一个拥有__len__函数(可以被Python的标准len函数调用),和一个__getitem__()函数,用于索引。This tutorial完整描述了创建自定义Dataset的例子,将FacialLandmarkDataset类作为Dataset的子类。
Pytorch的TensorDataset是一个包装tensor的Dataset。通过定义length和索引方式,它还提供了迭代、索引和沿着tensor第一维度进行切片的方式。这会使得我们在训练时更容易在同一行访问自变量和因变量。
from torch.utils.data import TensorDatasetx_train和y_train可以一同放入单个TensorDataset,以便迭代和切片。
train_ds = TensorDataset(x_train, y_train)之前,我们需要分别迭代x和y的minibatches:
xb = x_train[start_i:end_i]yb = y_train[start_i:end_i]现在,可以把这两步合成一步:
xb, yb = train_ds[i*bs : i*bs+bs]model, opt = get_model()for epcoh in range(epochs): for i in range((n - 1) // bs + 1): xb, yb = train_ds[i * bs: i * bs + bs] pred = model(xb) loss = loss_func(pred, yb) loss.backward() opt.step() opt.zero_grad()print(loss_func(model(xb), yb))输出:
tensor(0.0812, grad_fn=<NllLossBackward0>)Pytorch的 DataLoader负责管理batches。你可以从任何Dataset创建DataLoader。DataLoader使得迭代batch更加方便,相比使用train_ds[i * bs: i * bs + bs],DataLoader自动提供每一个minibatch。
from torch.utils.data import DataLoadertrain_ds = TensorDataset(x_train, y_train)train_dl = DataLoader(train_ds, batch_size=bs)先前,循环迭代batches (xb, yb)是这样的:
for i in range((n-1)//bs + 1): xb,yb = train_ds[i*bs : i*bs+bs] pred = model(xb)现在,(xb, yb)自动从dataloader加载,循环更加简洁:
for xb, yb in train_dl: pred = model(xb)model, opt = get_model()for epoch in range(epochs): for xb, yb in train_dl: pred = model(xb) loss = loss_func(pred, yb) loss.backward() opt.step() opt.zero_grad()print(loss_func(model(xb), yb))输出:
tensor(0.0824, grad_fn=<NllLossBackward0>)得益于Pytorch的nn.Module,nn.Parameter,Dataset,和DataLoader,我们的训练循环现在大大的缩小,并更容易理解。现在尝试添加实践中有效的模型所必须的基本功能。
在第一节中,我们只是试图建立一个合理的训练循环以用于训练数据。实际上,你还需要一个validation set,以验证是否过拟合。
打乱训练数据对于防止batch之间的联系和过拟合很重要。但是另一方面,无论是否shuffle验证集,验证集的loss都是确定的,因此shuffle验证集是无意义的。
我们使用2倍于训练集batch的数据作为验证集batch。这是因为验证集不需要反向传播,无需保存梯度,仅占很少的内存。利用这一点,我们使用更大的batch size并更快地计算损失。
train_ds = TensorDataset(x_train, y_train)train_dl = DataLoader(train_ds, batch_size=bs, shuffle=True)valid_ds = TensorDataset(x_valid, y_valid)valid_dl = DataLoader(valid_ds, batch_size=bs*2)我们将在每个epoch结束计算并打印验证集loss
(注意,在训练之前调用model.train(),在推理之前调用model.eval(),这是因为layers使用了如nn.BatchNorm2d、nn.Dropout,这些在验证时是不需要的)
model, opt = get_model()for epoch in range(epochs): model.train() for xb, yb in train_dl: pred = model(xb) loss = loss_func(pred, yb) loss.backward() opt.step() opt.zero_grad() model.eval() with torch.no_grad(): valid_loss = sum(loss_func(model(xb), yb) for xb, yb in valid_dl) print(epoch, valid_loss / len(valid_dl))输出:
0 tensor(0.3260)1 tensor(0.2793)现在做一些自定义的重构。因为在训练和验证时经过了两次计算损失的类似过程,我们现在将其放入function,loss_batch,它可以为每一个batch计算损失。
我们为训练集传入optimizer,并用以反向传播,而对于验证集,不需要optimizer,所以该函数就不会进行反向传播。
def loss_batch(model, loss_func, xb, yb, opt=None): loss = loss_func(model(xb), yb) if opt is not None: loss.backward() opt.step() opt.zero_grad() return loss.item(), len(xb)fit 运行必要的操作训练我们的模型,并计算每一epoch的训练和验证loss
import numpy as npdef fit(epochs, model, loss_func, opt, train_dl, valid_dl): for epoch in range(epochs): model.train() for xb, yb in train_dl: loss_batch(model, loss_func, xb, yb, opt) model.eval() with torch.no_grad(): losses, nums = zip( *[loss_batch(model, loss_func, xb, yb) for xb, yb in valid_dl] ) val_loss = np.sum(np.multiply(losses, nums)) / np.sum(nums) print(epoch, val_loss)get_data 返回训练集和验证集的dataloaders
def get_data(train_ds, valid_ds, bs): return( DataLoader(train_ds, batch_size=bs, shuffle=True), DataLoader(valid_ds, batch_size=bs * 2),)现在,所有过程,包括数据加载、拟合模型可以用3行代码运行
train_dl, valid_dl = get_data(train_ds, valid_ds, bs)model, opt = get_model()fit(epochs, model, loss_func, opt, train_dl, valid_dl)输出:
0 0.379309638965129871 0.28746109589338303你可以利用这3行基础代码训练各种各样的模型。来看一下我们能否利用这三行代码训练CNN。
我们现在要构建具有3层卷积层的神经网络。因为上一节没有假设关于模型形式的函数,所以可以利用上面3行代码直接训练CNN,无需任何修改。
我们将利用Pytorch的预定义的Conv2d类作为我们的卷积层。定义一个具有3层卷积层的CNN。每个卷积层跟一个ReLU。最后,使用一个均值池化层(注意,view时numpy中的reshape对应的pytorch版本)
class Mnist_CNN(nn.Module): def __init__(self): super().__init__() self.conv1 = nn.Conv2d(1, 16, kernel_size=3, stride=2, padding=1) self.conv2 = nn.Conv2d(16, 16, kernel_size=3, stride=2, padding=1) self.conv3 = nn.Conv2d(16, 10, kernel_size=3, stride=2, padding=1) def forward(self, xb): xb = xb.view(-1, 1, 28, 28) xb = F.relu(self.conv1(xb)) xb = F.relu(self.conv2(xb)) xb = F.relu(self.conv2(xb)) xb = F.avg_pool2d(xb, 4) return xb.view(-1, xb.size(1))lr = 0.1Momentum是随机梯度下降的一个变体,它考虑而落之前的更新,通常会使训练更快。
model = Mnist_CNN()opt = optim.SGD(model.parameters(), lr=lr, momentum=0.9)fit(epochs, model, loss_func, opt, train_dl, valid_dl)输出:
0 0.365610588598251351 0.2279014788389206torch.nn有另一个方便的类可以用来简化我们的代码:Sequential 。一个Sequential对象按照顺序运行其中包含的每个模块。这是编写神经网络一种更简单的方法。
为了利用这一点,我们需要从给定函数定义一个custom layer。例如,PyTorch没有view层,我们需要创建一个,Lambda将创建一个层,当利用Sequential定义神经网络时可以使用。
class Lambda(nn.Module): def __init__(self, func): super().__init__() self.func = func def forward(self, x): return self.func(x)def preprocess(x): return x.view(-1, 1, 28, 28)使用Sequential创建model更加简单:
model = nn.Sequential( Lambda(preprocess), nn.Conv2d(1, 16, kernel_size=3, stride=2, padding=1), nn.ReLU(), nn.Conv2d(16, 16, kernel_size=3, stride=2, padding=1), nn.ReLU(), nn.Conv2d(16, 10, kernel_size=3, stride=2, padding=1), nn.ReLU(), nn.AvgPool2d(4), Lambda(lambda x: x.view(x.size(0), -1)),)opt = optim.SGD(model.parameters(), lr=lr, momentum=0.9)fit(epochs, model, loss_func, opt, train_dl, valid_dl)输出:
0 0.32668319871425631 0.24143515722751618我们的CNN相当简洁,但是它仅能在MNIST上使用,因为:
让我们跳出上面两个假设,使得我们的模型可以用于任何2维单通道图片。首先,我们可以通过将数据预处理移到生成器中来移除初始Lambda层。
def preprocess(x, y): return x.view(-1, 1, 28, 28), yclass WrapperDataLoader: def __init__(self, dl, func): self.dl = dl self.func = func def __len__(self): return len(self.dl) def __iter__(self): for b in batches: yield (self.func(*b))train_dl, valid_dl = get_data(train_ds, valid_ds, bs)train_dl = WrapperDataLoader(train_dl, preprocess)valid_dl = WrapperDataLoader(valid_dl, preprocess)接下来,使用nn.AdaptiveAvgPool2d代替nn.AvgPool2d,它能让我们定义所需的output tensor大小,而而不是我们已有的input tensor。这样,模型就可以适用任意输入大小了。
model = nn.Sequential( nn.Conv2d(1, 16, kernel_size=3, stride=2, padding=1), nn.ReLU(), nn.Conv2d(16, 16, kernel_size=3, stride=2, padding=1), nn.ReLU(), nn.Conv2d(16, 10, kernel_size=3, stride=2, padding=1), nn.ReLU(), nn.AdaptiveAvgPool2d(1), Lambda(lambda x: x.view(x.size(0), -1)),)opt = optim.SGD(model.parameters(), lr=lr, momentum=0.9)试一试:
fit(epochs, model, loss_func, opt, train_dl, valid_dl)输出:
0 0.36534340913295751 0.31836258018016816如果你可以使用支持CUDA的GPU(你可以从大多数云供应商以0.5美元/每小时的价钱租用一个)加速你的代码。首先检查你的GPU能否在Pytorch中运行:
print(torch.cuda.is_available())输出:
True然后创建一个device对象
dev = torch.device( "cuda") if torch.cuda.is_available() else torch.device("cpu")更新preprocess,将batches移到GPU上:
def preprocess(x, y): return x.view(-1, 1, 28, 28).to(dev), y.to(dev)train_dl, valid_dl = get_data(train_ds, valid_ds, bs)train_dl = WrappedDataLoader(train_dl, preprocess)valid_dl = WrappedDataLoader(valid_dl, preprocess)最后,将model移到GPU:
model.to(dev)opt = optim.SGD(model.parameters(), lr=lr, momentum=0.9)你会发现现在运行的更快:
fit(epochs, model, loss_func, opt, train_dl, valid_dl)输出:
0 0.229358701264858241 0.21556809792518616现在我们已经有了一个通用的数据pipeline和训练循环,利用Pytorch,你可以用来训练许多类型的models。要了解现在训练模型有多简单,可以查看mnist_sample样例notebook。
当然,还有很多你想要添加的,如数据增强、超参数调整、监视训练、迁移学习等等。这些功能都在fastai库中,它是使用本教程所示的相同设计方法开发的,为希望进一步开发模型的从业者提供了方便。
我们在开头保证,会通过示例解释torch.nn、'torch.optim'、'Dataset'、'DataLoader'。所以让我们对上述内容总结一下:
torch.nn
Module:创建一个类似于函数的可调用对象,但还可以包括一些状态(如神经网络层的权重)。它知道它包含哪些Parameter(s),并能将它们的梯度归0,循环遍历它们以进行权重更新等。
Parameter:a wrapper for a tensor。它告诉Module在反向传播时需要更新的权重。只有带有requires_grad属性的tensor才会被更新。
functional:一个包含激活函数、损失函数等的模块(通常导入为F),还包括non-stateful版本的层,例如卷积层和线性层。
torch.optim:包含优化器,例如SGD等,在反向传播step中更新Paramter的权重。
Dataset:一个具有__len__和__getitem__的对象的抽象接口,包括Pytorch提供的类,如TensorDataset。
DataLoader:接收任意Dataset,并创建一个迭代器,该迭代器返回数据的batches。