前言

今天来写一下如何用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初始化一下;可以看到有两个卷积conv1conv2,其中第一个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 FF.relu()是官方提供的函数。当然如果你在__init__里面把relu定义成了我上面说的myrelu,那你这里直接第一句话就成了:

x = F.max_pool2d(myrelu(self.conv1(x)), 2)

下一步的F.max_pool2d池化也是一样的,不多废话了。在一系列流动以后,最后把x返回到外面去。这个Net的Class定义主要要注意两点:

  1. 是注意前后输出通道和输入通道的一致性。不能第一个卷积层输出4通道第二个输入6通道,这样就会报错。
  2. 它和我们常规的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的更新:(注意这些编号,下面还要提)

  1. 先算 loss 对于输入 x 的偏导,(当然网络好几层,这个 x 指的是每一层的输入,而不是最开始的输入 input )
  2. 对 1. 的结果再乘以一个步长(这样就相当于是得到一个对参数 W 的修改量)
  3. 用 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搭建一个深度学习网络框架,我们的顺序是:

  1. 先定义网络:写网络 Net 的 Class,声明网络的实例net = Net()
  2. 定义优化器 optimizer = optim.xxx(net.parameters(), lr = xxx)
  3. 再定义损失函数(自己写 class 或者直接用官方的,compute_loss = nn.MSELoss()或者其他
  4. 在定义完之后,开始一次一次的循环:
    • 先清空优化器里的梯度信息,optimizer.zero_grad();
    • 再将input传入,output = net(input),正向传播
    • 计算损失,loss = compute_loss(target, output),这里target就是参考标准值Groud Truth,需要自己准备,和之前传入的input一一对应
    • 误差反向传播,loss.backward()
    • 更新参数,optimizer.step()

这样就实现了一个基本的神经网络。大部分神经网络的训练都可以简化为这个过程,无非是传入的内容复杂,网络定义复杂,损失函数复杂,等等。

12-14 06:57