深度学习基础|基于pytorch的BP神经网络实现

对于一个神经网络,我们可以根据神经网络结构从头实现,例如一个BP神经网络,我们需要选择损失函数、激活函数,根据公式推导反向传递的梯度,并使用梯度下降更新参数,而卷积神经网络,还要写卷积、池化等函数,同时还要推导出卷积核等梯度......这样一来,便需要更多的时间用于梯度公式的推导与算法的实现。
为了避免每次推导梯度公式,我们可以采取自动微分的方法,写一个自动微分的类,之后每次反向传递,可以自动求出各个参数的梯度,从而用于梯度的更新。即便如此,我们还是需要用更多的时间去写激活函数、梯度下降、前向传递、自动微分等部分。所以使用其它的方法来实现神经网络模型就显得较为重要。兔兔在本文pytorch为例,使用pytorch实现一个简单的BP神经网络。
利用pytorch实现神经网络模型主要分为两种方法——一种是利用torch.nn中Sequential、RNN等方法来直接实现,该方法可以简化搭建过程;另一种是继承nn.Module父类,自己构建一个神经网络,该方法相对第一种较为复杂、灵活,但是十分常用,可以构建自己的神经网络模型,较多的深度学习论文中的模型都采用该方法来实现。
除了以上两种方法,其实也可以利用pytorch自动求导机制实现神经网络模型,即模型参数、前向传递、梯度下降等部分都手动去写,该方法在实际应用中不会用到,但是可以使读者更好地理解其中的细节,所以兔兔在最后以BP神经网络为例,采用自动求导机制来实现该方法。
关于BP神经网络的原理,可以参考兔兔前面的文章。
(一)torch.nn.Sequential搭建BP神经网络

import numpy as np import torch from torch import nn data=https://www.it610.com/article/np.random.normal(0,10,size=(10,3)) label=np.random.normal(0,10,size=(10,4)) data=torch.tensor(data,dtype=torch.float32) label=torch.tensor(label,dtype=torch.float32) bp=nn.Sequential(nn.Linear(3,10),nn.Sigmoid(),nn.Linear(10,6),nn.ReLU(),nn.Linear(6,4),nn.Sigmoid()) Loss=nn.MSELoss() #损失函数,这里选用MSE损失函数 optim=torch.optim.SGD(params=bp.parameters(),lr=0.1)#参数更新方法,这里选用随机梯度下降 for i in range(100): yp=bp(data) #前向传递的预测值 loss=Loss(yp,label) #预测值与实际值的差别 optim.zero_grad() loss.backward() #反向传递 optim.step() #更新参数

在该模型中,data每行表示每组数据,每列为数据的属性(指标);label每行表示每组数据,每列表示对应的类别。所以BP神经网络的输入层节点个数要与data的列数一致,输出层要与label列数一致(通常label每行是只有一个1,其余为0的一维数组,这里为了表述简便,使用随机生成的数组)。在输入模型前需要将data、label转成tensor张量。
之后用nn.Sequential()来写前馈神经网络模型,第一个nn.Linear()为神经网络输入层神经元,其中第一个参数表示输入数据特征数,第二个参数为输出特征数。所以根据data、label的维数确定神经元输入层与输出层的神经元个数。上述代码中是[3,10,6,4]四层神经元的神经网络,层与层之间分别使sigmoid、ReLU、sigmoid激活函数。之后选择损失函数与参数优化方法,损失函数在nn中,优化方法在torch.optim中,可根据实际需要来选择。优化器中需要传递模型参数params与学习率lr。
最后神经网络通过循环迭代来实现,每次用bp模型对数据进行预测,将预测值与实际值传递到损失函数Loss中,之后backward()反向传递并使用step()更新参数。
模型训练结束后,可以直接用bp(x)来预测x,也可以用bp.forward(x)。训练过程中,可以通过print(loss)或print(loss.data)查看损失函数值的变化。
(二)torch.nn.Module继承父类搭建BP神经网络
import torch from torch import nn class BP(nn.Module): '''BP神经网络模型''' def __init__(self,node,act): super().__init__() self.node=node #各层神将元数 self.n=len(node) #神经网络层数 self.act=act #各层之间激活函数 self.model=nn.Sequential() for i in range(self.n-1): self.model.append(nn.Linear(self.node[i],self.node[i+1])) self.model.append(self.act[i]) def forward(self,input): output=self.model(input) return output if __name__=='__main__': bp=BP(node=[2,2,3],act=[nn.ReLU(),nn.Sigmoid(),nn.ReLU()]) traindata=https://www.it610.com/article/torch.randint(0,2,size=(4,2),dtype=torch.float32) labeldata=torch.randint(0,2,size=(4,3),dtype=torch.float32) optimizer=torch.optim.Adam(params=bp.parameters(),lr=0.01) Loss=nn.MSELoss() for i in range(10): yp=bp(traindata) loss=Loss(yp,labeldata) optimizer.zero_grad() loss.backward() optimizer.step() print(loss)

在上面代码中,兔兔在__init__()中直接使用了nn.Sequential()来实现构建模型,在一些复杂神经网络中经常会这样做。这里其实也可以由BP神经网络的基本原理来构建,代码如下所示。
import torch from torch import nn class BP(nn.Module): '''BP神经网络模型''' def __init__(self,node): super().__init__() self.node=node #各层神将元数 self.n=len(node) #神经网络层数 self.w=torch.nn.ParameterList([torch.randint(0,2,size=(self.node[i],self.node[i+1]),dtype=torch.float32) for i in range(self.n-1)]) #权重参数weight self.b=torch.nn.ParameterList([torch.randint(0,2,size=(1,self.node[i]),dtype=torch.float32) for i in range(1,self.n)]) #偏置参数bias def forward(self,input): x=torch.mm(input,self.w[0])+self.b[0] x=torch.relu(x) for i in range(1,self.n-1): x=torch.mm(x,self.w[i])+self.b[i] x=torch.relu(x) return x if __name__=='__main__': bp=BP(node=[2,2,3]) traindata=https://www.it610.com/article/torch.randint(0,2,size=(4,2),dtype=torch.float32) labeldata=torch.randint(0,2,size=(4,3),dtype=torch.float32) optimizer=torch.optim.Adam(params=bp.parameters(),lr=0.01) Loss=nn.MSELoss() for i in range(10): yp=bp(traindata) loss=Loss(yp,labeldata) optimizer.zero_grad() loss.backward() optimizer.step() print(loss)

采用该方法的关键是利用nn.ParameterList()在__init__()中来定义可训练参数,这样该模型才能传递parameters()给optimizer,并在训练过程中更新这些参数。nn.ParameterDict()也可以定义可训练参数,只是此时参数是字典形式,键为参数名,值为参数。在forward函数中需要自己去写前向传播算法。
(三)pytorch自动求导实现BP神经网络 兔兔在这里讲述的方法,主要是介绍pytorch自动求导方法的使用以及BP神经网络的原理,在实际应用中一般不会使用,感兴趣的同学可以阅读该部分。
import torch class BP: def __init__(self,traindata,labeldata,node,epoch,lr): self.traindata=https://www.it610.com/article/traindata self.labeldata=labeldata self.node=node #各层节点 self.n=len(node) #层数 self.epoch=epoch #训练次数 self.lr=lr #学习率learning rate w=[]; b=[] for i in range(self.n-1): exec(f'w{i}=torch.randint(0,1,size=(self.node[i],self.node[i+1]),dtype=torch.float32,requires_grad=True)') w.append(eval(f'w{i}')) cv=locals() for i in range(1,self.n): exec(f'b{i}=torch.randint(0,1,size=(1,self.node[i]),dtype=torch.float32,requires_grad=True)') b.append(eval(f'b{i}')) self.w=w self.b=b def forward(self,input): x = torch.mm(input, self.w[0]) + self.b[0] x = torch.relu(x) for i in range(1, self.n - 1): x = torch.mm(x, self.w[i]) + self.b[i] x = torch.relu(x) return x def loss(self,yp,y): '''损失函数''' return (yp-y).pow(2).sum() #均方损失函数 def updata(self): for i in range(self.epoch): #print('the {} epoch'.format(i+1)) yp=self.forward(self.traindata) loss=self.loss(yp,self.labeldata) loss.backward() for i in range(self.n-1): self.w[i].data-=self.lr*self.w[i].grad.data self.b[i].data-=self.lr*self.b[i].grad.data#梯度下降更新参数if __name__=='__main__': traindata = https://www.it610.com/article/torch.randint(0, 5, size=(10, 2), dtype=torch.float32) labeldata = torch.randint(0, 5, size=(10, 3), dtype=torch.float32) bp = BP(traindata=traindata,labeldata=labeldata,node=[2,2,3],epoch=10000,lr=0.01) bp.updata()

在pytorch的之前版本中,对需要求梯度的参数w、b,需要使用torch.autugrad.Variable(requires_grad=True),使w,b可以求导。新的版本中tensor类型即可求梯度,只需更改其中参数requires_grad=True即可。之后这些参数参与前向传播的计算,通过计算的预测值与实际值构造损失函数,使用loss.backward()反向传播计算各个参数梯度,通过w.grad.data、b.grad.data得到相应梯度值,最终利用梯度下降更新参数。这里的损失函数与梯度下降法兔兔选择了最为简单的一种。
在初始化可求梯度的参数时,兔兔采用了exec内置函数循环批量生成变量名,并将其添加到列表中。
在梯度下降更新过程中,计算图中的叶节点不能直接进行内置运算,所以不可以对self.w[i]或self.b[i]等直接进行更新,否则会出现"RuntimeError: a leaf Variable that requires grad is being used in an in-place operation."这样的错误,这里兔兔利用self.w[i].data更新参数。若不使用该方法,也可以在代码第37行前加上 "with tensor.no_grad:"。
总结 【深度学习基础|基于pytorch的BP神经网络实现】利用pytorch实现神经网络有多种方法,对于BP神经网络这样基础模型使用Sequential()方法较为合适,而对于复杂的神经网络通常采用继承nn.Module父类的方法来实现,一般的搭建流程是先构建一些小的部分或单层神经网络,这些小的部分同样继承Module父类,最终将这些部分组合一起形成完整的神经网络。

    推荐阅读