前言
今天来写一下如何用Pytorch进行深度学习网络搭建,之所以写这个,是因为最近要跑一些网络代码,用到这些相关的知识,刚好做一个汇总,这篇文章参考的是Datawhale的这篇文章:[ 🔗 轻松看懂基于PyTorch的网络搭建 ]
搭建网络
pytorch网络搭建要比tensorflow简单,格式易理解。如果你想做一个网络,需定义一个该网络的Class,且该Class继承torch的网络类nn.Module(这个是必须的,可以先在代码头部引入torch.nn,import torch.nn as nn
,nn是torch中的工具箱,很好用),我们把该自定义的class命名为Net:
class Net(nn.Module):
这个类里主要写两个函数,一个是类初始化__init__
函数(类似于C++的构造函数),另一个是forward
函数。我们随便搭一个,如下:
import torch
import torch.nn as nn
import torch.nn.functional as F
class Net(nn.Module):
def __init__(self):
super().__init__()
self.conv1 = nn.Conv2d(1, 6, 5)
self.conv2 = nn.Conv2d(6, 16, 5)
def forward(self, x):
x = F.max_pool2d(F.relu(self.conv1(x)), 2)
x = F.max_pool2d(F.relu(self.conv2(x)), 2)
return x
__init__
初始化函数定义卷积层,super()
给父类的nn.Module
初始化一下;可以看到有两个卷积conv1
和conv2
,其中第一个Conv2d(1, 6, 5)
的含义是:把该卷积定义为输入1通道,输出6通道,卷积核 5 × 5 5\times5 5×5的一个卷积层,conv2
同理,这里就不说明了。「深度学习」主要就是学习卷积核里的参数,像别的不需要学习和改变的,就不用放进去。比如激活函数relu()
,你非要放进去也行,再给它起个名字叫myrelu,也是可以的。「forward
里面就是真正执行数据的流动」。
比如上面的代码,forward的参数输入的x
先经过定义的conv1(这个名字是你自己起的),再经过激活函数F.relu()
(这个就不是自己起的名字了,最开始先import torch.nn.functional as F
,F.relu()
是官方提供的函数。当然如果你在__init__
里面把relu定义成了我上面说的myrelu,那你这里直接第一句话就成了:
x = F.max_pool2d(myrelu(self.conv1(x)), 2)
下一步的F.max_pool2d
池化也是一样的,不多废话了。在一系列流动以后,最后把x
返回到外面去。这个Net的Class定义主要要注意两点:
- 是注意前后输出通道和输入通道的一致性。不能第一个卷积层输出4通道第二个输入6通道,这样就会报错。
- 它和我们常规的python的class还有一些不同,发现了没有?我们该怎么用这个Net呢?先定义一个Net的实例(毕竟Net只是一个类不能直接传参数,output=Net(input)当然不行)
net = Net()
这样我们就可以往里传 x 了,假设你已经有一个要往神经网络的输入的数据「input」(这个input应该定义成tensor类型,怎么定义tensor那就自己去看看书了,比如input = torch.rand([1, 3, 256, 256])
之类的)在传入的时候,是:
output = net(input)
看之前的定义:
def __init__(self):
# ..
def forward(self, x):
# ..
有点奇怪。好像常规 python 一般向class里面传入一个数据 x,在 class 的定义里面,应该是把这个 x 作为形参传入__init__
函数里的,而在上面的定义中,x 作为形参是传入 forward 函数里面的。其实也不矛盾,因为你定义 net 的时候,是net = Net()
,并没有往里面传入参数。如果你想初始化的时候按需传入,就把需要的传入进去。只是 x 是神经网络的输入,但是并非是初始化需要的,初始化一个网络,必须要有输入数据吗?未必吧。只是在传入网络时,会自动认为你这个 x 是喂给 forward 里面的。也就是说,先定义一个网络的实例net = Net()
, 这时调用output = net(input)
,可以理解为等同于调用output = net.forward(input)
, 这两者可以理解为一码事。在网络定义好以后,就涉及到传入参数,算误差,反向传播,更新权重等一系列操作,确实很容易记不住这些东西的格式和顺序。传入的方式上面已经介绍了,相当于一次正向传播,把一路上各层的输入 x 都算出来了。
想让神经网络输出的 output 跟你期望的 ground truth 差不多,那就是不断减小二者间的差异,这个差异是你自己定义的,也就是目标函数(object function)或者就是损失函数(loss function)。如果损失函数loss趋近于0,那么自然就达到目的了。损失函数loss基本上没法达到0,但是希望能让它达到最小值,那么就是希望它能按照梯度进行下降。关于梯度下降,我之前的文章又说到:[ 🔗 误差和梯度下降 ]
那神经网络能学习和决定什么呢?自然它只能决定每一层卷积层的权重。所以神经网络只能不停修改权重,比如y = wx + b
,x 是你给的,它只能改变 w、b,让最后的输出 y 尽可能接近你希望的 y 值,这样损失loss就越来越小。如果 loss 对于输入 x 的偏导数接近0了,不就意味着到达了一个极值吗?而在你的 loss 计算方式已经给定的情况下,loss对于输入 x 的偏导数的减小,其实只能通过更新参数卷积层参数 W 来实现(别的它决定不了啊,都是你输入和提供的)。所以,通过下述方式实现对W的更新:(注意这些编号,下面还要提)
- 先算 loss 对于输入 x 的偏导,(当然网络好几层,这个 x 指的是每一层的输入,而不是最开始的输入 input )
- 对 1. 的结果再乘以一个步长(这样就相当于是得到一个对参数 W 的修改量)
- 用 W 减掉这个修改量,完成一次对参数 W 的修改。
说的不太严谨,但是大致意思是这样的。这个过程你可以手动实现,但是大规模神经网络怎么手动实现?那是不可能的事情。所以我们要利用框架pytorch和工具箱torch.nn,要定义损失函数,以MSELoss
为例:
compute_loss = nn.MSELoss()
明显它也是个类,不能直接传入输入数据,所以直接loss = nn.MSEloss(target, output)
是不对的。需要把这个函数赋一个实例,叫成compute_loss
。之后就可以把你的神经网络的输出,和标准答案target
传入进去:
loss = compute_loss(target,output)
算出loss
,下一步就是反向传播,如果对反向传播理论不太理解,可参考我之前的这篇文章:[ 🔗 深度学习简介及反向传播 ]。
loss.backward()
这一步其实就是把更新 W 的第一步给算完了,得到对参数 W 一步的更新量,算是一次反向传播。这里就注意了,loss.backward()
是啥玩意?如果是自己的定义的 loss(比如你就自己定义了个def loss(x,y): return y-x
),这样直接backward
肯定会出错。所以应当用nn
里面提供的函数。
当然搞深度学习不可能只用官方提供的 loss 函数,所以如果你要想用自己的 loss 函数,必须也把 loss 定义成上面 Net 的样子(不然你的 loss 不能反向传播,这点要注意,注:这点是以前写的,很久之前的版本不行,现在都可以了,基本不太需要这样了)。也是继承nn.Module
,把传入的参数放进forward
里面,具体的 loss 在 forward 里面算,最后return loss
。__init__()
就空着,写个super().__init__
就行了。
在反向传播之后,更新 W 的第2和第3步怎么实现?就是通过优化器来实现。让优化器来自动实现对网络权重W的更新。所以在Net
定义完以后,需要写一个优化器的定义(选SGD
方式为例):
from torch import optim
optimizer = optim.SGD(net.parameters(), lr = 0.001, momentum = 0.9)
SGD
是随机梯度下降的意思,同样,优化器也是一个类,先定义一个实例optimizer
,然后之后会用。注意在optimizer
定义时,需要给SGD
传入了net
的参数parameters
,这样之后优化器就掌握了对网络参数的控制权,就能够对它进行修改了。传入的时候把学习率lr
也传入了。上面的momentum
是梯度下降时的「动量」,这个概念我在之前的文章 [ 🔗 神经网络训练不起来,怎么办? ] 中有介绍。在每次迭代之前,先把optimizer
里存的梯度清零一下(因为W已经更新过的“更新量”下一次就不需要用了):
optimizer.zero_grad()
在loss.backward()
反向传播以后,更新参数:
optimizer.step()
所以要使用Pytorch搭建一个深度学习网络框架,我们的顺序是:
- 先定义网络:写网络 Net 的 Class,声明网络的实例
net = Net()
- 定义优化器
optimizer = optim.xxx(net.parameters(), lr = xxx)
- 再定义损失函数(自己写 class 或者直接用官方的,
compute_loss = nn.MSELoss()
或者其他 - 在定义完之后,开始一次一次的循环:
- 先清空优化器里的梯度信息,
optimizer.zero_grad();
- 再将
input
传入,output = net(input)
,正向传播 - 计算损失,
loss = compute_loss(target, output)
,这里target
就是参考标准值Groud Truth
,需要自己准备,和之前传入的input
一一对应 - 误差反向传播,
loss.backward()
- 更新参数,
optimizer.step()
- 先清空优化器里的梯度信息,
这样就实现了一个基本的神经网络。大部分神经网络的训练都可以简化为这个过程,无非是传入的内容复杂,网络定义复杂,损失函数复杂,等等。