前4个博客,已经介绍了什么是Tensor,构建模型所需的基本类有哪些,这些类有哪些工作。下面,结合这些内容,通过简单的例子进行使用说明。
文章目录
1 构造模块化的神经网络
下面具体以一个由两个全连接层组成的感知机为例, 介绍如何使用nn.Module构造模块化的神经网络。
import torch
from torch import nn
# 实现了 y = x*w + b 的一个方法,wb是未知参数,x是输入,y是对应的输出
class Linear(nn.Module):
# 输入维度,in_dim为x的维度,out_dim为y的维度
def __init__(self, in_dim, out_dim):
super(Linear, self).__init__()
# 定义参数wb,默认是需要求导的
# 结合公式我们知道w是一个in_dim * out_dim的矩阵
self.w = nn.Parameter(torch.randn(in_dim, out_dim))
self.b = nn.Parameter(torch.randn(out_dim))
# 输入x,计算y = wx + b
def forward(self, x):
x = x.matmul(self.w) # 计算wx
y = x + self.b.expand_as(x) # 再加上b
return y
class Perception(nn.Module):
def __init__(self, in_dim, hid_dim, out_dim):
super(Perception, self).__init__()
# 这里定义了两层,注意第一层的输出维度要跟第二维的输入维度相同
self.layer1 = Linear(in_dim, hid_dim)
self.layer2 = Linear(hid_dim, out_dim)
def forward(self, x):
x = self.layer1(x)
y = torch.sigmoid(x) # 激活函数
y = self.layer2(y)
y = torch.sigmoid(y)
return y
2 损失函数
在深度学习中, 损失反映模型最后预测结果与实际真值之间的差距, 可以用来分析训练过程的好坏、 模型是否收敛等, 例如均方损失、 交叉熵损失等。 在PyTorch中, 损失函数可以看做是网络的某一层而放到模型定义中, 但在实际使用时更偏向于作为功能函数而放到前向传播过程中。
PyTorch在torch.nn及torch.nn.functional中都提供了各种损失函数, 通常来讲, 由于损失函数不含有可学习的参数, 因此这两者在功能上基本没有区别。
在上一行代码的基础上,我们补充下面代码计算损失,损失函数两种输出结果都是一样的。在使用pycharm开发时,输入nn.已经不会弹出functional,所以我觉得未来趋势,functional里面的函数将会被取消,所以安全起见,尽量使用nn自带的函数吧。
model = Perception(100, 1000, 10)
input = torch.randn(100) # 输入一个100维向量
label = torch.randn(10) # 设置输出标签
output = model(input)
# 使用nn.CrossEntropyLoss
criterion = nn.L1Loss()
loss_nn = criterion(output, label)
# 使用nn.functional.l1_loss
loss_functional = nn.functional.l1_loss(output, label)
下面给出nn里面包含的损失函数。
函数定义
torch.nn.L1Loss(size_average=None, reduce=None, reduction='mean')
L1Loss就是输入 x x x和目标 y y y的平均绝对值误差(mean absolute error,MAE)。在深度学习中,一般 x x x就是预测输出的一个向量, y y y是对应的标签,那么绝对值误差就是 ∣ x − y ∣ |x-y| ∣x−y∣,在深度学习中,batchsize一般不为1,定义 L = { l 1 , … , l N } T , l n = ∣ x n − y n ∣ L = \{l_1,\dots,l_N\}^T, l_n = \left| x_n - y_n \right| L={l1,…,lN}T,ln=∣xn−yn∣, N N N为对应的batch的个数,那么对应的MAE如下所示,两种误差的区别就是是否求均值。
ℓ ( x , y ) = { m e a n ( L ) r e d u c t i o n = ′ m e a n ′ s u m ( L ) r e d u c t i o n = ′ s u m ′ } \ell(x, y) = \left\{\begin{array}{cc}mean(L) & reduction = 'mean' \\ sum(L) & reduction='sum'\end{array}\right\} ℓ(x,y)={mean(L)sum(L)reduction=′mean′reduction=′sum′}
如果想返回一个batch中每个输出的误差,设置reduction = 'none'
,那么返回的就是一个向量
L
=
{
l
1
,
…
,
l
N
}
T
,
l
n
=
∣
x
n
−
y
n
∣
L = \{l_1,\dots,l_N\}^T, l_n = \left| x_n - y_n \right|
L={l1,…,lN}T,ln=∣xn−yn∣
size_average和reduce官方已经不赞成使用了,选择输出误差的格式仅使用reduction配置即可(三个参数,mean,sum,none)
下面是这个函数的简单用法。
loss = nn.L1Loss()
input = torch.randn(3, 5)
target = torch.randn(3, 5)
output = loss(input, target)
3 训练方法
在上述介绍中, nn.Module模块提供了网络骨架,使用了nn.L1Loss计算了输出与标签的差异,也就是损失。这时还缺少一个如何进行模型优化、 加速收敛的模块,nn.optim就是干这事的。
nn.optim中包含了各种常见的优化算法, 包括随机梯度下降算法SGD(Stochastic Gradient Descent, 随机梯度下降) 、Adam(Adaptive Moment Estimation) 、 Adagrad、 RMSProp, 这里仅对常用的SGD与Adam两种算法进行介绍。
3.1 SGD方法
梯度下降(Gradient Descent) 是迭代法中的一种, 是指沿着梯度下降的方向求解极小值, 一般可用于求解最小二乘问题。 在深度学习中, 当前更常用的是SGD算法, 以一个小批次(Mini Batch) 的数据为单位, 计算一个批次的梯度, 然后反向传播优化, 并更新参数。
优点:
- 分担训练压力: 当前数据集通常数量较多, 尺度较大, 使用较大的数据同时训练显然不现实, SGD则提供了小批量训练并优化网络的方法, 有效分担了GPU等计算硬件的压力。
- 加快收敛: 由于SGD一次只采用少量的数据, 这意味着会有更多次的梯度更新, 在某些数据集中,其收敛速度会更快。
缺点:
- 初始学习率难以确定: SGD算法依赖于一个较好的初始学习率, 但设置初始学习率并不直观, 并且对于不同的任务, 其初始值也不固定。
- 容易陷入局部最优: SGD虽然采用了小步快走的思想, 但是容易陷入局部的最优解, 难以跳出。
有效解决局部最优的通常做法是增加动量momentum,SGD算法中可以通过设置momentum参数解决局部最优问题(这就是调参)
3.2 Adam方法
Adam利用了梯度的一阶矩与二阶矩动态地估计调整每一个参数的学习率, 是一种学习率自适应算法。Adam的优点在于, 经过调整后, 每一次迭代的学习率都在一个确定范围内, 使得参数更新更加平稳。此外, Adam算法可以使模型更快收敛, 尤其适用于一些深层网络, 或者神经网络较为复杂的场景。
3.3 简单用法
承接之前创建的模型,给出一个简单训练的示例
model = Perception(100, 1000, 10)
optimizer = torch.optim.SGD(params = model.parameters(), lr=0.01)
input = torch.randn(20, 100) # 输入一个10个100维向量
output = model(input) # 输出是一个20*10的矩阵
label = torch.randn(20, 10) # 给出一个对应维度的标签
# 计算loss
criterion = nn.L1Loss()
loss_l1 = criterion(output, label)
optimizer.zero_grad() # 清空梯度, 在每次优化前都需要进行此操作
loss_l1.backward() # 损失的反向传播
optimizer.step() # 利用优化器进行梯度更新
不同参数层分配不同的学习率: 优化器也可以很方便地实现将不同的网络层分配成不同的学习率, 即对于特殊的层单独赋予学习率, 其余的保持默认的整体学习率, 具体示例如下:
# 对于model中需要单独赋予学习率的层, 如special层, 则使用'lr'关键字单独赋予
optimizer = optim.SGD(
[{'params': model.special.parameters(), 'lr': 0.001},
{'params': model.base.parameters()}, lr=0.0001)
4 网络模型库 torchvision.models
对于深度学习, torchvision.models库提供了众多经典的网络结构与预训练模型, 例如VGG、 ResNet和Inception等, 利用这些模型可以快速搭建物体检测网络, 不需要逐层手动实现。
以VGG模型为例, 在torchvision.models中, VGG模型的特征层与分类层分别用vgg.features与vgg.classifier来表示, 每个部分是一个nn.Sequential结构, 可以方便地使用与修改。 下面简单进行使用说明。先导入包from torchvision import models
。
输入vgg = models.vgg16()
即可快速的获得一个VGG16模型,VGG16的特征层包括13个卷积、 13个激活函数ReLU、 5个池化, 一共31层,输入print(len(vgg.features))
即可发现返回是31。VGG16的分类层包括3个全连接、 2个ReLU、 2个Dropout, 一共7层,输入print(len(vgg.classifier))
返回值为7。
模型返回类型也是个pytorch的Module类型,查看源代码发现vgg继承Module类,class VGG(nn.Module),所以在pytorch中直接使用即可。
vgg.features这是一个Sequence,可以通过矩阵角标访问网络结构,比如输入vgg.classifier[-1]
直接返回最后一层信息。
Linear(in_features=4096, out_features=1000, bias=True)
5 模型加载与保存
5.1 模型加载
对于计算机视觉的任务, 包括物体检测, 我们通常很难拿到很大的数据集, 在这种情况下重新训练一个新的模型是比较复杂的, 并且不容易调整, 因此, Fine-tune(微调) 是一个常用的选择。 所Finetune是指利用别人在一些数据集上训练好的预训练模型, 在自己的数据集上训练自己的模型。
下面提供两种模型加载方法。
5.1.1 加载自带的预训练模型
直接利用torchvision.models中自带的预训练模型, 只需要在使用时赋予pretrained参数为True即可。
from torchvision import models
vgg = models.vgg16(pretrained=True)
5.1.2 加载本地的训练模型
如果想要使用自己的本地预训练模型, 或者之前训练过的模型, 则可以通过model.load_state_dict()函数操作, 具体如下:
from torchvision import models
vgg = models.vgg16()
state_dict = torch.load("训练模型路径")
vgg.load_state_dict({k:v for k, v in state_dict_items() if k in vgg.state_dict()})
通常来讲, 对于不同的检测任务, 卷积网络的前两三层的作用是非常类似的, 都是提取图像的边缘信息等, 因此为了保证模型训练中能够更加稳定, 一般会固定预训练网络的前两三个卷积层而不进行参数的学习。 例如VGG模型, 可以设置前三个卷积模组不进行参数学习, 设置方式如下:
for layer in range(10):
for p in vgg[layer].parameters():
p.requires_grad = False
5.2 模型保存
在PyTorch中, 参数的保存通过torch.save()函数实现, 可保存对象包括网络模型、 优化器等, 而这些对象的当前状态数据可以通过自身的state_dict()函数获取。
torch.save({'model': model.state_dict(), 'optimizer': optimizer.state_dict(), 'model_path.pth')
6 GPU加速
PyTorch为数据在GPU上运行提供了非常便利的操作。 首先可以使用torch.cuda.is_available()来判断当前环境下GPU是否可用, 其次是对于Tensor和模型, 可以直接调用cuda()方法将数据转移到GPU上运行, 并且可以输入数字来指定具体转移到哪块GPU上运行。
import torch
from torchvision import models
a = torch.randn(3,3)
b = models.vgg16()
# 判断当前GPU是否可用
if torch.cuda.is_available():
a = a.cuda()
# 指定将b转移到编号为1的GPU上
b = b.cuda(1)
# 使用torch.device()来指定使用哪一个GPU
device = torch.device("cuda: 1")
c = torch.randn(3, 3, device = device, requires_grad = True)
在脚本中利用函数指定使用哪一块GPU
import torch
torch.cuda.set_device(1)
在工程应用中, 通常使用torch.nn.DataParallel(module,device_ids)函数来处理多GPU并行计算的问题。 示例如下:
model_gpu = nn.DataParallel(model, device_ids=[0,1])
output = model_gpu(input)
多GPU处理的实现方式是, 首先将模型加载到主GPU上, 然后复制模型到各个指定的GPU上, 将输入数据按batch的维度进行划分,分配到每个GPU上独立进行前向计算, 再将得到的损失求和并反向传播更新单个GPU上的参数, 最后将更新后的参数复制到各个GPU上。
总结
网络基本的模型构建,计算损失,训练方法都说明差不多了,后面可以开展关于网络层、损失函数等具体内容的细致分析,也会穿插介绍一些网络的构建方法。