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以及深度学习技术也颇感兴趣,欢迎加我微信或留言交流相关工作。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,607评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,047评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,496评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,405评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,400评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,479评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,883评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,535评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,743评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,544评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,612评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,309评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,881评论 3 306
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,891评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,136评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,783评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,316评论 2 342

推荐阅读更多精彩内容