基于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=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=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=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=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 = 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神经网络这样基础模型使用Sequential()方法较为合适,而对于复杂的神经网络通常采用继承nn.Module父类的方法来实现,一般的搭建流程是先构建一些小的部分或单层神经网络,这些小的部分同样继承Module父类,最终将这些部分组合一起形成完整的神经网络。

猜你喜欢

转载自blog.csdn.net/weixin_60737527/article/details/126439288