numpy 实现反向传播学习笔记

本博客内容来源于网络以及其他书籍,结合自己学习的心得进行重编辑,因为看了很多文章不便一一标注引用,如图片文字等侵权,请告知删除。

传统2D计算机视觉学习笔记目录------->传送门
传统3D计算机视觉学习笔记目录------->传送门
深度学习学习笔记目录 ------------------->传送门

本文简介

本文的主要目的就是描述出怎么使用numpy实现一个简单的神经网络,通过反向传播完成训练的过程,正如题目一样。当然我们不会像成熟的深度学习框架一样内部实现自动求导,那就太麻烦了。通过自己手写这么一份代码,可以让自己加深深度神经网络到底是怎么运作的,以达到我们的目的,而不再是完完全全的黑箱了。

目前网上有很多相关的文章,我自己也通过那些文章得到很多的认识再最初学习的时候,但是总是感觉有一些不足,比如为了追求代码简洁,而失去了结构性,而我们使用的pytorch或者tensorflow有很好的面型对象的结构。所以本文实现的代码更注重结构性,和可拓展性,可以在此基础上在实现其他的一些简单的层。那么开始吧


图文无关

分步实现思路

首先我们知道神经网络是有一些layer(层)组成的的,我们目前主要关注隐藏层,因为神经网络的主要计算是在隐藏层。这些层分别可以进行前向推导,反向传播,参数更新,所以我们先写这些层的基类,为方便调试,我们在初始化类时,要给层一个名字。

class BaseLayer:
    def __init__(self,name):
        self.name = name
    def forward(self, input):            #前向推导
        pass
    def backward(self,grad):             #反向传播
        pass
    def update(self):                    #参数更新
        pass

接着我们要实现全连接层,激活函数,以及损失函数。激活函数我们实现简单的sigmoid激活函数,损失函数我们实现带有softmax的CrossEntropyLoss。有关简单的激活函数和损失函数我会在其他文章详细描述。

我们先实现sigmoid激活函数,由于sigmoid 中我们不需要更新任何的参数,所以不用重载参数更新函数。

class SigmoidLayer(BaseLayer):
    def __init__(self, name):
        super(SigmoidLayer,self).__init__(name)
    def forward(self,input):
        self.output = 1/(1+np.exp(-input))
        return self.output
    def backward(self,grad):
        grad = grad * self.output*(1-self.output)
        return grad

然后我们实现全连接层,在此我们将学习率简化为1,初始参数设置为正太分布随机参数,优化器也是最简单的批量梯度下降(BGD)

class LinearLayer(BaseLayer):
    def __init__(self,name,input_channels,output_channels):
        super(LinearLayer,self).__init__(name)
        self.weight = np.random.randn( input_channels,output_channels )
        self.bias = np.random.randn(1,output_channels)
    def forward(self,input):
        self.input = input
        self.output = np.dot(self.input,self.weight)+ self.bias          # y = wx +b
        return self.output
    def backward(self,grad):
        self.batch_size = grad.shape[0]
        self.grad_w = np.dot(self.input.T,grad )/self.batch_size     # δw = δg * x
        self.grad_b = np.sum( grad , axis=0,keepdims= True )/self.batch_size
        grad = np.dot(grad,self.weight.T)
        return grad
    def update(self):
        self.weight -= self.grad_w
        self.bias -= self.grad_b

然后我们来实现损失函数,以及softmax,我们可以将softmax的反向传播与CrossEntropy反向传播一起执行,可以简化整个过程。

class SoftMaxLayer(BaseLayer):
    def __init__(self, name):
        super(SoftMaxLayer,self).__init__(name)
    def forward(self,input):
        vec_max = np.max( input,axis=1 )[np.newaxis,:].T
        input -= vec_max
        exp = np.exp(input)
        output = exp / (np.sum(exp,axis=1)[np.newaxis,:].T)
        return output

class SMCrossEntropyLossLayer(BaseLayer):
    def __init__(self, name):
        super(SMCrossEntropyLossLayer,self).__init__(name)
    def forward(self,pred,real):
        self.softmax_p = SoftMaxLayer("softmax").forward(pred)
        self.real = real
        loss = 0
        for i in range(self.real.shape[0]):
            loss += -np.log( self.softmax_p[i,real[i]] )
        loss /= self.real.shape[0]
        return loss
    def backward(self):
        for i in range(self.real.shape[0]):
            self.softmax_p[i,self.real[i]] -= 1
        self.softmax_p = self.softmax_p / self.real.shape[0]
        return self.softmax_p

现在我们将神经网络的基本的几个层实现完了,现在我们要将这些隐层组建成一个网络。我们实现一个基本的网络框架,然后再通过新的子类继承基类,只需要该变隐层结构就可以了。由于准备训练一个mnist手写数字数据,所以第一层的输入的维度是784。

class NetBase:
    def __init__(self):
        self.layers = []
        
    def forward(self,input):
        for layer in self.layers:
            input = layer.forward(input)
        pred = SoftMaxLayer("softmax").forward(input)
        return input,pred
    def backward(self,grad):
        for layer in  reversed(self.layers):
            grad = layer.backward(grad)
            layer.update()

class SimpleNet(NetBase):
    def __init__(self):
        super(SimpleNet,self).__init__()
        self.layers = [
            LinearLayer(name="full1",input_channels= 784, output_channels= 512),
            SigmoidLayer(name="relu1"),
            LinearLayer(name="full2",input_channels=512,output_channels=128),
            SigmoidLayer(name="sigmoid2"),
            LinearLayer(name="full3",input_channels=128,output_channels=10)
        ]

整体代码

现在我们将网络结构的代码以及训练代码放到一起。

#BaseNet.py
import numpy as np
class BaseLayer:
    def __init__(self,name):
        self.name = name
    def forward(self, input):
        pass
    def backward(self,grad):
        pass
    def update(self):
        pass

class SigmoidLayer(BaseLayer):
    def __init__(self, name):
        super(SigmoidLayer,self).__init__(name)
    def forward(self,input):
        self.output = 1/(1+np.exp(-input))
        return self.output
    def backward(self,grad):
        grad = grad * self.output*(1-self.output)
        return grad

class LinearLayer(BaseLayer):
    def __init__(self,name,input_channels,output_channels):
        super(LinearLayer,self).__init__(name)
        self.weight = np.random.randn( input_channels,output_channels )
        self.bias = np.random.randn(1,output_channels)
    def forward(self,input):
        self.input = input
        self.output = np.dot(self.input,self.weight)+ self.bias
        return self.output
    def backward(self,grad):
        self.batch_size = grad.shape[0]
        self.grad_w = np.dot(self.input.T,grad )/self.batch_size 
        self.grad_b = np.sum( grad , axis=0,keepdims= True )/self.batch_size
        grad = np.dot(grad,self.weight.T)
        return grad
    def update(self):
        self.weight -= self.grad_w
        self.bias -= self.grad_b

class SoftMaxLayer(BaseLayer):
    def __init__(self, name):
        super(SoftMaxLayer,self).__init__(name)
    def forward(self,input):
        vec_max = np.max( input,axis=1 )[np.newaxis,:].T
        input -= vec_max
        exp = np.exp(input)
        output = exp / (np.sum(exp,axis=1)[np.newaxis,:].T)
        return output

class SMCrossEntropyLossLayer(BaseLayer):
    def __init__(self, name):
        super(SMCrossEntropyLossLayer,self).__init__(name)
    def forward(self,pred,real):
        self.softmax_p = SoftMaxLayer("softmax").forward(pred)
        self.real = real
        loss = 0
        for i in range(self.real.shape[0]):
            loss += -np.log( self.softmax_p[i,real[i]] )
        loss /= self.real.shape[0]
        return loss
    def backward(self):
        for i in range(self.real.shape[0]):
            self.softmax_p[i,self.real[i]] -= 1
        self.softmax_p = self.softmax_p / self.real.shape[0]
        return self.softmax_p

class NetBase:
    def __init__(self):
        self.layers = []
        
    def forward(self,input):
        for layer in self.layers:
            input = layer.forward(input)
        pred = SoftMaxLayer("softmax").forward(input)
        return input,pred
    def backward(self,grad):
        for layer in  reversed(self.layers):
            grad = layer.backward(grad)
            layer.update()

class SimpleNet(NetBase):
    def __init__(self):
        super(SimpleNet,self).__init__()
        self.layers = [
            LinearLayer(name="full1",input_channels= 784, output_channels= 512),
            SigmoidLayer(name="relu1"),
            LinearLayer(name="full2",input_channels=512,output_channels=128),
            SigmoidLayer(name="sigmoid2"),
            LinearLayer(name="full3",input_channels=128,output_channels=10)
        ]

训练部分代码,由于numpy没有使用gpu来进行训练,训练整体还是比较慢的,所以我们只训练了 前100个数据,通过观察loss 就可以验证我们的网络是否进行工作。

#train.py
import BaseNet
import numpy as np
import matplotlib.pyplot as plt
import os

training_set_inputs  = []
training_set_outputs   = []

def read_mnist(mnist_image_file, mnist_label_file):
    if 'train' in os.path.basename(mnist_image_file):
        num_file = 60000
    else:
        num_file = 10000
    with open(mnist_image_file, 'rb') as f1:
        image_file = f1.read()
    with open(mnist_label_file, 'rb') as f2:
        label_file = f2.read()
    image_file = image_file[16:]
    label_file = label_file[8:]
    for i in range(num_file):
        label = int(label_file[i])
        image_list = [int(item) for item in image_file[i*784:i*784+784]]
        image_np = np.array(image_list, dtype=np.uint8).reshape(28*28)
        training_set_outputs.append([label])
        training_set_inputs.append( image_np )

train_image_file = '/home/eric/data/mnist/train-images-idx3-ubyte'
train_label_file = '/home/eric/data/mnist/train-labels-idx1-ubyte'
read_mnist(train_image_file, train_label_file)
training_set_inputs = np.array( training_set_inputs )
training_set_outputs = np.array( training_set_outputs )

training_set_inputs = training_set_inputs[:100,:]
training_set_outputs = training_set_outputs[:100,:]

net  = BaseNet.SimpleNet()
loss = BaseNet.SMCrossEntropyLossLayer("loss")

x = []
y=[]
for i in range(10000):
    input = training_set_inputs
    output,pred = net.forward(input)
    loss_value = np.squeeze(loss.forward(output,training_set_outputs))
    print(i,loss_value,np.sum( (np.equal(pred.argmax(axis = 1),training_set_outputs.T)))/ training_set_outputs.shape[0] )
    x.append(i)
    y.append(loss_value)

    delta = loss.backward()
    net.backward(delta)

plt.plot(x,y,'r--')
plt.title('loss')
plt.show()

总结

写完这篇文章,才发现代码太多,没有太多的文字叙述,感觉要是一点点解释,怕是累死我,估计没有人像我这么笨吧。自己认为学习的过程还是需要自己用手就敲一遍,观察一下每个状态的输出,才能更好的理解。虽然代码很多但是其实也可以压缩成十几行,但是对初学者就太不友好了。


重要的事情说三遍:

如果我的文章对您有所帮助,那就点赞加个关注呗 ( * ^ __ ^ * )

如果我的文章对您有所帮助,那就点赞加个关注呗 ( * ^ __ ^ * )

如果我的文章对您有所帮助,那就点赞加个关注呗 ( * ^ __ ^ * )

传统2D计算机视觉学习笔记目录------->传送门
传统3D计算机视觉学习笔记目录------->传送门
深度学习学习笔记目录 ------------------->传送门

任何人或团体、机构全部转载或者部分转载、摘录,请保留本博客链接或标注来源。博客地址:开飞机的乔巴

作者简介:开飞机的乔巴(WeChat:zhangzheng-thu),现主要从事机器人抓取视觉系统以及三维重建等3D视觉相关方面,另外对slam以及深度学习技术也颇感兴趣,欢迎加我微信或留言交流相关工作。

你可能感兴趣的:(numpy 实现反向传播学习笔记)