一只菜鸟的PyTorch笔记!
1 前言
如今主流的学习框架有两个。其一是Google推出的TensorFlow
,其二是由Facebook推出的PyTorch
。相较之下后者更容易上手,且其动态图的设计也十分利于了解和调节神经网络结构,这也是其异军突起的原因。
笔者建议使用Jupyter Lab
来学习。因其即时交互和支持Markdown
等优点,许多Tutorial都是用采用的.ipynb
来记录的。初次学习需要对Python
有基础的了解。此文参考的是PyTorch官方教程。此外,多多查阅PyTorch官方文档会让你更快地上手。
所依赖的环境如下:
- Python 3.6.6
- PyTorch 0.4.1
- TorchVision 0.2.1
2 构建一个神经网络
构建一个神经网络的主要步骤为:
- 定义网络结构。
- 定义损失函数。
- 定义优化器。
2.1 网络结构
定义网络时,需要继承父类nn.Module
,调用父类的初始化函数__init__
,并实现它的forward
函数,把网络中具有可学习参数的层放在初始化函数__init__
中。如果某一层(如ReLU
)不具有可学习的参数,则既可以放在初始化函数中,也可以不放,建议是不放在其中,而在forward
中使用nn.functional
代替。
只要在nn.Module
的子类中定义了forward
函数,backward
函数就会自动被实现(利用autograd
)。在forward
函数中可使用任何tensor
支持的函数,还可以使用if
和for
循环,print
、log
等Python
语法,写法和标准的Python
写法一致。
定义一个简单的卷积神经网络LeNet,如下:
import torch
import torch.nn as nn
import torch.nn.functional as F
class Net(nn.Module): # 定义一个子类Net,并继承父类nn.module。
def __init__(self): # 定义初始化方法__init__。
super(Net, self).__init__() # 等价于nn.Module.__init__(self)
# 调用父类的初始化方法__init__。
# 卷积层
self.conv1 = nn.Conv2d(1, 6, 5) # 输入通道为1,输出通道为6(即分别与六个滤波器做卷积),滤波器大小为5*5)
self.conv2 = nn.Conv2d(6, 16, 5) # conv2d表示输入为二维的卷积层。注意:torch中不支持一维的卷积运算。
# 全连接层/仿射层,y = Wx + b。
self.fc1 = nn.Linear(16*5*5, 120) # 定义fc1(fullconnect)全连接函数1为线性函数:y = Wx + b,并将16*5*5个节点连接到120个节点上。
self.fc2 = nn.Linear(120, 84)
self.fc3 = nn.Linear(84, 10)
def forward(self, x): # 定义前向传播。
x = F.max_pool2d(F.relu(self.conv1(x)), (2, 2)) # 输入x经过卷积conv1之后,经过激活函数ReLU,使用2x2的窗口进行最大池化Max_pool2d,然后更新到x。
x = F.max_pool2d(F.relu(self.conv2(x)), 2) # 输入x经过卷积conv2之后,经过激活函数ReLU,使用2*2的窗口进行最大池化Max_pool2d,然后更新到x。
x = x.view(x.size()[0], -1) # view函数将池化后的[in_chanel,6,5,5]四维张量x,变形成根据in_chanel大小的[x.size()[0],-1]二维形式,其中-1表示自适应。总特征数并不改变,为接下来的全连接作准备。
x = F.relu(self.fc1(x)) # 输入x经过全连接fc1后,经过激活函数ReLU更新到x。
x = F.relu(self.fc2(x)) # 输入x经过全连接fc2后,经过激活函数ReLU更新到x。
x = self.fc3(x) # 输入x经过全连接fc3后,更新到x。
return x # 返回值x。
net = Net() # 创建一个Net的实例net。
print(net)
"""Out:
Net(
(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)
)
"""
网络的可学习参数通过net.parameters()
返回:
params = list(net.parameters())
print(len(params))
"""Out:
10
"""
net.named_parameters
可同时返回可学习的参数及名称:
for name,parameters in net.named_parameters(): # 可将其看作字典
print(name,':',parameters.size())
"""Out:
conv1.weight : torch.Size([6, 1, 5, 5])
conv1.bias : torch.Size([6])
conv2.weight : torch.Size([16, 6, 5, 5])
conv2.bias : torch.Size([16])
fc1.weight : torch.Size([120, 400])
fc1.bias : torch.Size([120])
fc2.weight : torch.Size([84, 120])
fc2.bias : torch.Size([84])
fc3.weight : torch.Size([10, 84])
fc3.bias : torch.Size([10])
"""
需要注意的是,torch.nn
只支持mini-batches
,不支持一次只输入一个样本,即一次必须是一个batch
。但如果只想输入一个样本,则用input.unsqueeze(0)
将batch_size
设为1。例如nn.Conv2d
输入必须是4维张量,形如$nSamples \times nChannels \times Height \times Width$。可将nSample
设为1,即$1 \times nChannels \times Height \times Width$。
2.2 损失函数
输入和输出:
input = torch.randn(1, 1, 32, 32) # 输入一个随机生成的样本。
out = net(input) # 输出
input.size(),out.size() #输入和输出的都是Tensor。
"""Out:
(torch.Size([1, 1, 32, 32]), torch.Size([1, 10]))
"""
前向传播,计算损失:
output = net(input)
target = torch.randn(10) # a dummy target, for example.随机生成一个目标,可理解为label值。
target = target.view(1, -1) # 转换为[1, n]的二维张量,-1表示自适应。
criterion = nn.MSELoss() # 定义均方误差损失函数。
loss = criterion(output, target)
loss # lost是个scalar
"""Out:
tensor(0.7358, grad_fn=<MseLossBackward>)
"""
反向传播,计算梯度:
net.zero_grad() # 把net中所有可学习参数的梯度清零
print('反向传播之前 conv1.bias的梯度')
print(net.conv1.bias.grad)
loss.backward()
print('反向传播之后 conv1.bias的梯度')
print(net.conv1.bias.grad)
"""Out:
反向传播之前 conv1.bias的梯度
None
反向传播之后 conv1.bias的梯度
tensor([-0.0030, -0.0066, -0.0036, 0.0317, 0.0085, 0.0023])
"""
2.3 优化器
在反向传播计算完所有参数的梯度后,还需要使用优化方法来更新网络的权重和参数,例如随机梯度下降法(SGD)的更新策略如下:
手动实现如下:
learning_rate = 0.01
for f in net.parameters():
f.data.sub_(f.grad.data * learning_rate) # inplace 减法
torch.optim
中实现了深度学习中绝大多数的优化方法,例如RMSProp
、Adam
、SGD
等,更便于使用,因此大多数时候并不需要手动写上述代码。
调用优化器进行操作如下:
import torch.optim as optim
#新建一个优化器,指定要调整的参数和学习率
optimizer = optim.SGD(net.parameters(), lr = 0.01)
# 在训练过程中
# 先梯度清零(与net.zero_grad()效果一样)
optimizer.zero_grad()
# 前向传播,计算损失。
output = net(input)
loss = criterion(output, target)
#反向传播,计算梯度。
loss.backward()
#更新参数。
optimizer.step()
就这样这样反复迭代下去,直到收敛,即可得到最终的模型。之后便是测试过程了。
3 小试牛刀:CIFAR-10分类
CIFAR-10是一个常用的彩色图片数据集,它有10个类别: ‘airplane’, ‘automobile’, ‘bird’, ‘cat’, ‘deer’, ‘dog’, ‘frog’, ‘horse’, ‘ship’, ‘truck’。每张图片都是3×32×32
,也即3-通道彩色图片,分辨率为32×32。 通过百度网盘下载到本地会更有效率。
实现对CIFAR-10分类的步骤如下:
- 数据处理。
- 构建网络。
- 训练网络。
- 测试网络。
3.1 数据处理
加载并预处理CIFAR-10数据集,如下:
import torch
import torchvision as tv
import torchvision.transforms as transforms
from torchvision.transforms import ToPILImage
# 第一次运行程序torchvision会自动下载CIFAR-10数据集。
# 大约163M,需花费一定的时间。
# 如果已经下载有CIFAR-10,可通过root参数指定。
# 定义对数据的预处理方式
transform = transforms.Compose([
transforms.ToTensor(), # 把一个取值范围是[0,255]的PIL.Image或者shape为(H,W,C)的numpy.ndarray,转换成形状为[C,H,W],取值范围是[0,1.0]的torch.FloadTensor。
transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5)), # 归一化。Normalize(mean,std):这里有三个通道,每个通道减去均值后除以标准差。
])
# 训练集
trainset = tv.datasets.CIFAR10(
root = 'H:/Data/CIFAR-10/',
train = True,
download = True,
transform = transform)
#################参数说明#################
# root : cifar-10-batches-py 的根目录。
# train : True = 训练集; False = 测试集 。
# download : True = 从互联上下载数据,并将其放在root目录下。如果数据集已经下载,什么都不干。
# transform : A function/transform = 将PIL(python image library) image进行转换。
# target_transform : A function/transform = 对target(即标签)进行转换。
#########################################
trainloader = torch.utils.data.DataLoader( #数据加载器。组合数据集和采样器,并在数据集上提供单进程或多进程迭代器。
trainset,
batch_size = 4,
shuffle = True,
num_workers = 2)
#################参数说明#################
# dataset (Dataset) – 加载数据的数据集。
# batch_size (int, optional) – 每个batch加载多少个样本。(默认: 1)
# shuffle (bool, optional) – 设置为True时会在每个epoch(整个训练集被遍历的次数)重新打乱数据。(默认: False)
# sampler (Sampler, optional) – 定义从数据集中提取样本的策略。如果指定,则忽略shuffle参数。
# num_workers (int, optional) – 用多少个子进程加载数据。0表示数据将在主进程中加载。(默认: 0)
# collate_fn (callable, optional) – 合并一个示例列表以形成一个Mini-batch。
# pin_memory (bool, optional) – 如果为True,数据加载器将把张量复制到CUDA固定内存中,然后返回它们。
# drop_last (bool, optional) – 如果数据集大小不能被batch size整除,则设置为True后可删除最后一个不完整的batch。如果设为False并且数据集的大小不能被batch size整除,则最后一个batch将更小。(默认: False)
#########################################
# 测试集
testset = tv.datasets.CIFAR10(
'H:/Data/CIFAR-10/',
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') # classes是一个元组,对应dataset的标签。
Dataset对象是一个数据集,可以按下标访问,返回形如(data, label)的数据。可尝试输出一张图片及其标签:
(data, label) = trainset[100] print(classes[label]) # 打印对应label的标签。 show = ToPILImage() # 可以把Tensor转成PIL.Image,方便可视化 show((data + 1) / 2).resize((100, 100)) # (data + 1) / 2是为了还原被归一化的数据。
Dataloader是一个可迭代的对象,它将dataset返回的每一条数据拼接成一个batch,并提供多线程加速优化和数据打乱等操作。当程序对dataset的所有数据遍历完一遍之后,相应的对Dataloader也完成了一次迭代。可尝试输出一个batch的图片及其标签:
dataiter = iter(trainloader) # 因trainloader的shuffle参数设置为True,所以每次迭代时每个batch的内容被打乱。 images, labels = dataiter.next() # 返回4张图片及标签 print(' '.join('%11s'%classes[labels[j]] for j in range(4))) show(tv.utils.make_grid((images+1)/2)).resize((400,100))
3.2 构建网络
拷贝前文中构建的LeNet网络,因CIFAR-10是3通道彩图,修改self.conv1第一个参数为3通道即可。
import torch.nn as nn
import torch.nn.functional as F
class Net(nn.Module):
def __init__(self):
super(Net, self).__init__()
self.conv1 = nn.Conv2d(3, 6, 5)
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)
def forward(self, x):
x = F.max_pool2d(F.relu(self.conv1(x)), (2, 2))
x = F.max_pool2d(F.relu(self.conv2(x)), 2)
x = x.view(x.size()[0], -1)
x = F.relu(self.fc1(x))
x = F.relu(self.fc2(x))
x = self.fc3(x)
return x
net = Net()
use_cuda = False # 控制全局是否使用GPU。
if use_cuda:
net = net.cuda()
print(net)
# 损失函数和优化器。
from torch import optim
criterion = nn.CrossEntropyLoss() # 交叉熵损失函数
optimizer = optim.SGD(net.parameters(), lr=0.001, momentum=0.9) # 随机梯度下降优化器。
############ 优化tricks之动量法参数momentum ############
# 一般,神经网络在更新权值时,采用公式为: w = w - learning_rate * dw
# 引入momentum后,采用公式为: w = momentum * w - learning_rate * dw
# 其在平坦的区域收敛会加快。
######################################################
"""Out:
Net(
(conv1): Conv2d(3, 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)
)
"""
3.3 训练网络
使用处理好的数据分批训练:
torch.set_num_threads(8) # 获得用于并行化CPU操作的OpenMP线程数
for epoch in range(2): #训练两个epoch。即两次遍历训练集。
running_loss = 0.0
for i, data in enumerate(trainloader, 0):
# 输入数据
inputs, labels = data # 数据加载器生成的data为一个list,其包含两个元素,第一个是inputs值,第二个是labels值
if use_cuda: # 是否使用GPU。
inputs, labels = inputs.cuda(), labels.cuda()
# 梯度清零
optimizer.zero_grad()
# forward + backward
outputs = net(inputs)
loss = criterion(outputs, labels)
loss.backward()
# 更新参数
optimizer.step()
# 打印log信息
# loss 是一个scalar,需要使用loss.item()来获取数值,不能使用loss[0],否则返回的是一个tensor
running_loss += loss.item()
if i % 2000 == 1999: # 每2000个batch打印一下训练状态
print('[%d, %5d] loss: %.3f' % (epoch+1, i+1, running_loss / 2000)) # 每两千次训练的平均损失。
running_loss = 0.0
print('Finished Training')
"""Out:
[1, 2000] loss: 2.205
[1, 4000] loss: 1.810
[1, 6000] loss: 1.674
[1, 8000] loss: 1.564
[1, 10000] loss: 1.480
[1, 12000] loss: 1.469
[2, 2000] loss: 1.385
[2, 4000] loss: 1.365
[2, 6000] loss: 1.350
[2, 8000] loss: 1.289
[2, 10000] loss: 1.289
[2, 12000] loss: 1.270
Finished Training
"""
3.4 测试网络
首先尝试一个batch中四张图片:
dataiter = iter(testloader)
images, labels = dataiter.next() # 一个batch返回4张图片
print('实际的label: ' '\n' , ' '.join('%10s' % classes[labels[j]] for j in range(4)))
if use_cuda: # 是否使用GPU。
images, labels = images.cuda(), labels.cuda()
show(tv.utils.make_grid((images.to('cpu')+1) / 2)).resize((400,100)) # 用.to('cpu')将cuda Tensor转换为普通的Tensor来画图
"""Out:
实际的label:
cat ship ship plane
"""
然后查看测试结果:
# 计算图片在每个类别上的分数
outputs = net(images)
# 得分最高的那个类
_, predicted = torch.max(outputs.data, 1) # torch.max(Tensor, dimension(optional))输出Tensor中指定维度最大值及其索引
print('预测结果: ' '\n', ' '.join('%10s'% classes[predicted[j]] for j in range(4)))
"""Out:
预测结果:
frog car ship plane
"""
计算全局准确率:
correct = 0 # 预测正确的图片数
total = 0 # 总共的图片数
# 由于测试的时候不需要求导,可以暂时关闭autograd,提高速度,节约内存。
with torch.no_grad():
for data in testloader:
images, labels = data
if use_cuda: # 是否使用GPU。
images, labels = images.cuda(), labels.cuda()
outputs = net(images)
_, predicted = torch.max(outputs, 1)
total += labels.size(0)
correct += (predicted == labels).sum() # (predicted == labels)是一个Tensor,为真时值为1,假时值为0。
print('10000张测试集中的准确率为: %d %%' % (100 * correct / total))
"""Out:
10000张测试集中的准确率为: 55 %
"""
比随机猜测的准确率百分之十要好,说明算法是有效的。
3.5 GPU加速
如使用GPU加速,则所需添加环境:
- CUDA Toolkit V9.0
- cuDNN v7.3
具体可参考笔者的前篇博文。
在上述的代码中,只需设置use_cuda = Ture
即可。如自行设置GPU加速,需注意:
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu") # device命名。如果多个CPU有待补充,TODO。
net.to(device) # 将网络存入显存
inputs = inputs.to(device) # 将训练时的输入存入显存
labels = labels.to(device) # 将标签存入显存
images = images.to(device) # 将测试时的输入存入显存
4 后记
恭喜你已经初步掌握了PyTorch
,之后便是独自修行的过程了。望诸位早日成为一名合格的“炼丹师”!