原文地址:WHAT IS TORCH.NN REALLY?
本人英语学渣,如有错误请及时指出以便更正,使用的源码可点击原文地址进行下载。
pytorch提供了许多优雅的类和模块帮助我们构建与训练网络,比如 torch.nn
, torch.optim
,Dataset
等。为了充分利用这些模块的功能,灵活操作它们解决各种不同的问题,我们需要更好地理解当我们调用这些模块时它们到底干了些什么,为此,我们首先不调用这些模块实现MNIST手写字识别,仅使用最基本的 pytorch 张量函数。然后,我们逐渐增加 torch.nn
, torch.optim
, Dataset
, or DataLoader
,具体地展示每个模块具体干了些什么,展示这些模块是怎样使代码变得更加优雅灵活。
此教程适用范围:熟悉pytorch的张量操作
加载 MNIST 数据集
我们使用经典的 MNIST
数据集,一个包含了0-9数字的二值图像库。
还会用到 pathlib
库用于目录操作,一个python3自带的标准库。使用 requests
下载数据集。当用到一个模块时才会进行导入,而不会一开始全部导入,以便更好地理解每个步骤。
from pathlib import Path
import requests
DATA_PATH = Path('data')
PATH = DATA_PATH / "mnist"
PATH.mkdir(parents=True,exit_ok=True)
URL = "http://deeplearning.net/data/mnist/"
FILENAME = "mnist.pkl.gz"
if not (PATH / FILENAME).exists():
content = requests.get(URL + FILENAME).content
(PATH / FILENAME).open("wb").write(content)
该数据集采用numpy数组格式,并使用pickle存储,pickle是一种特定于python的格式,用于序列化数据。
import pickle
import gzip
with gzip.open((PATH / FILENAME).as_posix(),"rb") as f:
((x_train,y_train),(x_valid,y_valid),_) = pickle.load(f,encoding="latin-1")
每张训练图片分辨率为 28x28, 被存储为 784(=28x28) 的一行。我们输出看一下数据,首先需要转换回 28x28的图像。
form matplotlib import pyplot
import numpy as np
pyplot.imshow(x_train[0].reshape((28,28)),cmap="gray")
print(x_train.shape)
(50000,784)
PyTorch使用 torch.tensor ,所以我们需要对numpy类型数据进行转换
import torch
x_train,y_train,x_valid,y_valid = map(
torch.tensor,(x_train,y_train,x_valid,y_valid)
)
n,c = x_train.shape
x_train,x_train.shape,y_train.min(),y_train.max()
print(x_train,y_train)
print(x_train.shape)
print(y_train.min(),y_train.max())
从头创建神经网络(不使用torch.nn)
让我们仅仅使用 pytorch 中的张量操作来创建模型,假设你已经熟悉神经网络的基础知识(不熟悉请参考corse.fast.ai )
pytorch提供了很多创建张量的操作,我们将用这些方法来初始化权值weights和偏置 bais来创建一个线性模型。这些只是常规张量,有一个非常特别的补充:我们告诉PyTorch这些张量需要支持求导(requires_grad=True)。这样PyTorch将记录在张量上完成的所有操作,以便它可以在反向传播过程中自动计算梯度!
对于权值weights,我们再初始化之后再设置 requires_grad
,因为我们不想这一步包含在梯度的计算中(注:pytorch中以 _
结尾的操作都是在原变量中(in-place)执行的)
import math
weights = torch.randn(780,10) / math.sqrt(784)
weights.requires_grad_()
bias = torch.zeros(10, requires_grad=True)
多亏了pytorch的自动求导功能,我们可以使用python的所有标准函数来构建模型。 我们这儿利用矩阵乘法,加法来构建线性模型。我们编写 log_softmax
函数作为激活函数。 虽然pytorch提供了大量写好的损失函数,激活函数,你依然可以自由地编写自己的函数替代它们。 pytorch 甚至支持创建自己的 GPU函数或者CPU矢量函数。
def log_softmax(x):
return x - x.exp().sum(-1).log().unsqueeze(-1)
def model(xb):
return log_softmax(xb @ weights + bias) # python的广播机制
上面的 @
符号表示向量的点乘,接下来我们会调用一批数据(batch,64张图片)输入此模型。
bs = 64 # batch size
xb = x_train[0:bs] # a mini-batch from x
preds = model(xb) # predictions
print(preds[0],preds.shape)
out:
tensor([-2.4513, -2.5024, -2.0599, -3.1052, -3.2918, -2.2665, -1.9007, -2.2588,
-2.0149, -2.0287], grad_fn=<SelectBackward>) torch.Size([64, 10])
正如我们看到的,preds
张量不仅包含了一组张量,还包含了求导函数。反向传播的时候会用到此函数。让我们使用标准的python语句接着来实现 negative log likelihood loss 损失函数(译者加:也被称为交叉熵损失函数):
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))
out:
tensor(2.3620, grad_fn=<NegBackward>)
接下来定义一个计算准确度的函数
def accuracy(out,yb):
preds = torch.argmax(out,dim=1) # 得到最大值的索引
return (preds == yb).float().mean()
检查模型的准确度:
print(accuracy(preds, yb))
out:
tensor(0.0938)
现在我们开始循环训练模型,每一步我们执行以下操作:
- 选择一批数据(a batch)
- 使用模型进行预测
- 计算损失函数
- 反向传播更新参数 weights 和 bias
我们现在使用 torch.no_grad()
更新参数,以避免参数更新过程被记录入求导函数中。
然后我们清零导数,以便开始下一轮循环,否则导数会在原来的基础上累加,而非替代原来的数
from IPython.core.debugger import set_trace
lr = 0.5 # learning rate
epochs = 2 # how many epochs to train for
for 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_()
目前为止,我们从头创建一个迷你版的神经网络
让我们来检查一下损失和准确率,并于迭代更新参数之前进行比较,我们期望得到更小的损失于更高的准确率。
print(loss_func(model(xb), yb), accuracy(model(xb), yb))
out:
tensor(0.0822, grad_fn=<NegBackward>) tensor(1.)
使用 torch.nn.functional 简化代码
现在我们使用torch.nn.functional
重构之前的代码,这样会使代码变得更加简洁与灵活,更易理解。
首先最简单的一步是,用 torch.nn.functional
( 为了方便后面统一称作F) 中带有的损失函数来代替我们自己编写的函数,使得代码变得更简短。这些函数都包包含于模块 torch.nn
里面,除了大量的损失函数与激活函数,里面还包含了大量用于构建网络的函数。
如果我们的网络中使用 negative log likelihood loss 作为损失函数, log softmax activation 作为激活函数 (即我们上面实现的损失函数与激活函数)。在pytorch中我们直接使用函数 F.cross_entropy
便可实现上面两个函数的功能。所以我们可以用此函数代替上面实现的激活函数与损失函数。
import torch.nn.functional as F
loss_func = F.cross_entropy
def model(xb):
return xb @ weights + bias
让我测试一下是否和上面自己实现的函数效果一致:
print(loss_func(model))
out:
tensor(0.0822, grad_fn=<NllLossBackward>) tensor(1.)
引入 nn.Module 重构代码
接下来我们引入 nn.Module
和nn.Parameter
改进代码。我们创建 nn.Module
的子类。这个例子中我们创建一个包含权重,偏置,以及包含前向传播的类。nn.Module
含有许多的属性与方法可供调用 (比如: .parameters
.zero_grad()
)
from torch import nn
class Mnist_Logistic(nn.Module):
def __init__(self):
super().__init__()
sefl.weights = nn.Parameter(torch.randn(784,10)/math.sqrt(784))
self.bias = nn.Parameter(torch.zeros(10))
def forward(self,xb):
return xb @ self.weights + self.bias
接下来实例化我们的模型:
model = Mnist_Logistic()
现在我们可以和之前一样使用损失函数了。注意:nn.Module
对象可以像函数一样调用,但实际上是自动调用了对象内部的函数 forward
print(loss_func(model(xb),yb))
out:
tensor(2.2082, grad_fn=<NllLossBackward>)
在之前,我们必须进行如下得操作对权重,偏置进行更新,梯度清零:
with torch.no_grad():
weights -= weights.grad * lr
bias -= bias.grad * lr
weights.grad.zero_()
bias.grad.zero_()
现在我们可以充分利用 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(epoches):
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()
让我们再一次确认损失情况:
print(loss_func(model(xb),yb))
out:
tensor(0.0812, grad_fn=<NllLossBackward>)
引入 nn.Linear 重构代码
比起手动定义 权重 与 偏置,并且使用 self.weights
和 self.bias
来计算 xb @ self.weights + self.bias
的方式,我们可以使用pytorch中的 nn.Linear
来定义线性层,他自动为我们实现以上权重参数的定义以及计算的过程。除了线性模型之外,pytorch还有一系列的其它网络层供我们使用,大大简化了我们的编程过程。
class Mnist_Logistic(nn.Module):
def __init__(self):
super().__init__()
self.lin = nn.Linear(784,10)
def forward(self,xb):
return self.lin(xb)
同上面一样实例化模型,计算损失
model = Mnist_Logistic()
print(loss_func(model(xb),yb))
out:
tensor(2.2731, grad_fn=<NllLossBackward>)
训练,并查看训练之后的损失
fit()
print(loss_func(model(xb), yb))
out:
tensor(0.0820, grad_fn=<NllLossBackward>)
引入 optim 重构代码
接下来使用torch.optim
改进训练过程,而不用手动更新参数
之前的手动优化过程如下:
with torch.no_grad():
for p in model.parameters(): p -= p.grad * lr
model.zero_grad()
使用如下代码替代手动的参数更新:
opt.step()
# optim.zero_grad() resets the gradient to 0 and we need to call it
# before computing the gradient for the next minibatch.
opt.zero_grad()
结合之前的完整跟新代码如下:
from torch import optim
def get_model():
model = Mnist_Logistic()
return model, optim.SGD(model.parameters(),lr=lr)
model, opt = get_model()
print(loss_func(model(xb),yb))
for epoch in range(epoches):
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))
out:
tensor(2.3785, grad_fn=<NllLossBackward>)
tensor(0.0802, grad_fn=<NllLossBackward>)
引入 Dataset 处理数据
pytorch定义了 Dataset 类,其中主要包含了 __len__
函数与 __getitem__
函数。此教程以创建 FacialLandmarkDataset
为例详细地介绍了Dataset类的使用。
pytorch的 TensorDataset
是一个包含张量的数据集。通过定义长度索引等方式,使我们更好地利用索引,切片等方法迭代数据。这会让我们很容易地在一行代码中获取我们地数据。
form torch.utils.data import TensorDataset
x_train
y_train
可以被组合进一个TensorDataset
中,这会使得迭代切片更加简单。
train_ds = TensorDataset(x_train,y_train)
之前我们获取数据的方法如下:
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 epoch 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))
out:
tensor(0.0817, grad_fn=<NllLossBackward>)
引入DataLoader加载数据
DataLoader
用于批量加载数据,你可以用他来加载任何来自 Dataset
的数据,它使得数据的批量加载十分容易。
from torch.utils.data import DataLoader
train_ds = TensorDataset(x_train,y_train)
train_dl = DataLoader(train_ds, batch_size=bs)
之前我们读取数据的方式:
for i in range((n-1)//bs + 1):
xb,yb = train_ds[i*bs : i*bs+bs]
pred = model(xb)
现在使用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))
out:
tensor(0.0817, grad_fn=<NllLossBackward>)
目前为止训练模型部分我们就已经完成了,通过使用nn.Module
, nn.Parameter
, DataLoader
, 我们的训练模型以及得到了很大的改进。接下来让我们开始模型的测试部分。
添加测试集
在前一部分,我们尝试了使用训练集训练网络。实际工作中,我们还会使用测试集来观察训练的模型是否过拟合。
打乱数据的分布有助于减小每一批(batch)数据间的关联,有利于模型的泛化。但对于测试集来说,是否打乱数据对结果并没有影响,反而会花费多余的时间,所以我们没有必要打乱测试集的数据。
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)后我们输出测试得到的损失值。
(注:如下代码中,我们调用model.train()
和model.eval
表示进入训练模式与测试模式,以保证模型运行的准确性)
model,opt = get_model()
for epoch in range(epoches):
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))
out:
0 tensor(0.3456)
1 tensor(0.2988)
创建 fit() 和 get_data() 优化代码
我们再继续做一点改进。因为我们再计算训练损失和验证损失时执行了两次相同的操作,所以我们用一个计算每一个batch损失的函数封装这部分代码。
我们为训练集添加优化器,并执行反向传播。对于训练集我们不添加优化器,当然也不会执行反向传播。
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过程中训练和验证的必要操作
import numpy as np
def 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.sum(np.multiply(losses, nums)). np.sum(nums))
print(epoch, val_loss)
现在,获取数据加载模型进行训练的整个过程只需要三行代码便能实现了
train_dl, valid_dl = get_data(train_ds, valid_ds, bs)
model, opt = get_model()
fit(epoches, model, loss_func, opt, train_dl, valid_dl)
out:
0 0.2961075816631317
1 0.28558296990394594
我们可以用这简单的三行代码训练各种模型。下面让我们看看怎么用它训练一个卷积神经网络。
使用卷积神经网络
现在我们用三个卷积层来构造我们的卷积网络。因为之前的实现的函数都没有假定模型形式,这儿我们依然可以使用它们而不需要任何修改。
我们pytorch预定义的Conv2d
类来构建我们的卷积层。我们模型有三层,每一层卷积之后都跟一个 ReLU,然后跟一个平均池化层。
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.conv3(xb))
xb = F.avg_pool2d(xb,4)
return xb.view(-1, xb.size(1))
lr = 0.1
动量momentum是随机梯度下降的一个参数,它考虑到了之前的梯度值使得训练更快。
model = Mnist_CNN()
opt = optim.SGD(model.parameters(), lr=lr, momentum=0.9)
fit(epochs, model, loss_func, opt, train_dl, valid_dl)
out:
0 0.3829730714321136
1 0.2258522843360901
使用 nn.Sequential 搭建网络
torch.nn
还有另外一个方便的类可以简化我们的代码:Sequential
, 一个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
是一种简化代码的好方法。 一个Sequential
对象按顺序执行包含在内的每一个module,使用它可以很方便地建立一个网络。
为了更好地使用Sequential
模块,我们需要自定义 pytorch中没实现地module。例如pytorch中没有自带 改变张量形状地层,我们创建 Lambda
层,以便在Sequential
中调用。
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)
out:
0 0.32739396529197695
1 0.25574398956298827
简易的DataLoader
我们的网络以及足够精简了,但是只能适用于MNIST数据集,因为
- 网络默认输入为 28x28 的张量
- 网络默认最后一个卷积层大小为 4x4 (因为我们的池化层大小为4x4)
现在我们去除这两个假设,使得网络可以适用于所有的二维图像。首先我们移除最初的 Lambda
层,用数据预处理层替代。
def preprocess(x, y):
return x.view(-1, 1, 28, 28), y
class WrappedDataLoader:
def __init__(self, dl, func):
self.dl = dl
self.func = func
def __len__(self):
return len(self.dl)
def __iter__(self):
batches = iter(self.dl)
for b in batches:
yield (self.func(*b))
train_dl, valid_dl = get_data(train_ds, valid_ds, bs)
train_dl = WrappedDataLoader(train_dl, preprocess)
valid_dl = WrappedDataLoader(valid_dl, preprocess)
然后,我们使用nn.AdaptiveAvgPool2d
代替nn.AvgPool2d
。它允许我们自定义输出张量的维度,而于输入的张量无关。这样我们的网络便可以适用于各种size的网络。
model = nn.Sequential(
nn.Conv2d(1, 16, kernal_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)
out:
0 0.32888883714675904
1 0.31000419993400574
使用GPU
如果你的电脑有支持CUDA的GPU(你可以很方便地以 0.5美元/小时 的价格租到支持的云服务器),便可以使用GPU加速训练过程。首先检测设备是否正常支持GPU:
print(torch.cuda.is_available())
out:
Ture
接着创建一个设备对象:
dev = torch.device(
"cuda") if torch.cuda.is_available() else torch.device("cpu")
更新 preprocess(x,y)
把数据移到GPU:
def preprocess(x, y):
return x.view(-1, 1, 28, 28).to(dev), y.to(device)
train_dl, valid_dl = get_data(train_ds, valid_ds, bs)
train_dl = WrappedDataLoader(train_dl, preprocess)
valid_dl = WrappedDataLoader(valid_dl, preprocess)
最后移动网络模型到GPU:
model.to(dev)
opt = optim.SGD(model.parameters(),lr=lr, momentum=0.9)
进行训练,能发现速度快了很多:
fit(epochs, model, loss_func, opt, train_dl, valid_dl)
out:
0 0.21190375366210937
1 0.18018000435829162
总结
我们现在得到了一个通用的数据加载和模型训练方法,我们可以在pytorch种用这种方法训练大多的模型。想知道训练一个模型有多简单,回顾一下本次的代码便可以了。
当然,除此之外本篇内容还有很多需求没有讲到,比如数据增强,超参调试,数据监控(monitoring training),迁移学习等。这些特点都以与本篇教程相似的设计方法包含于 fastai库中。
本篇教程开头我们承诺将会通过例程解释 torch.nn
torch.optim
Dataset
DataLoader
等模块,下面我们就这些模型进行总结。
- torch.nn
- Module: 创建一个可以像函数一样调用地对象,包含了网络的各种状态,可以使用
parameter
方便地获取模型地参数,并有清零梯度,循环更新参数等功能。 - Parameter: 将模型中需要更新的参数全部打包,方便反向传播过程中进行更新。有
requires_grad
属性的参数才会被更新。 - functional:通常导入为
F
,包含了许多激活函数,损失函数等。
- Module: 创建一个可以像函数一样调用地对象,包含了网络的各种状态,可以使用
- torch.optim: 包含了很多诸如
SGD
一样的优化器,用来在反向传播中跟新参数 - Dataset: 一个带有
__len__
__getitem__
等函数的抽象接口。里面包含了TensorDataset
等类。 - DataLoader: 输入任意的
Dataset
并按批(batch)迭代输出数据。
附录
完整代码下载地址