前言
之前写了篇文章《手撕神经网络(1)——神经网络的基本组件》介绍了手撕神经网络的各个层。但是疫情和学习原因,导致其后续一直没有机会写,这两天难得空闲,终于有机会继续分享。
概要
在文章《手撕神经网络(1)——神经网络的基本组件》一文中,我们将神经网络的各个组件,通过python类的方式定义完毕,而在这篇文章中,我们就来使用这些积木,搭建成我们的神经网络。
网络结构
为双层神经网络,即含有:
- 一个输入层
- 一个隐藏层
- 一个输出层
我们仍然是使用python类来定义网络的结构
初始化
超参数
显然,对于一个双层神经网络,其有哪些超参数?—— 最基本的就是:各个层的神经元个数,分别设置为:input_size,hidden_size,output_size
网络参数
权重
对于一个双层神经网络,其参数为第一层的权重 w 1 , b 1 w_1,b1 w1,b1,第二层的权重 w 2 , b 2 w_2,b_2 w2,b2。我们将这些参数协程该类的属性:
class TwoLayerNet:
def __init__(self, input_size,hidden_size,output_size,weight_init_std=0.01):
# 初始化权重
self.params={}
self.params['W1']=weight_init_std*np.random.randn(input_size,hidden_size)
self.params['b1']=np.zeros(hidden_size)
self.params['W2']=weight_init_std*np.random.randn(hidden_size,output_size)
self.params['b2']=np.zeros(output_size)
这里,为了方便对第一层的权重 w 1 , b 1 w_1,b1 w1,b1,第二层的权重 w 2 , b 2 w_2,b_2 w2,b2的管理,我们使用一个容器(字典)来包装它们。
层
你可能会发现,我们本文开头提到的搭建积木的事,直到现在都没有提起。也没有用到积木。不着急,在使用这些积木搭建我们的模型之前,我们先来想一下我们的神经网络的功能:
- 预测:通过正向传播,将输入转化为输出。即我们的神经网络类需要有
forward
方法。 - 学习:通过反向传播,通过计算实际输出与正确标签之间的误差,计算误差的梯度,在通过梯度下降法完成网络参数的更新。即我们的神经网络类需要有
backward
方法。
现在,你可以回到文章《手撕神经网络(1)——神经网络的基本组件》中看一看,我们的积木——也就是每一个层的类,都定义了什么方法?—— 正好是我们整个神经网络所需要的forward
方法和backward
方法!
所以,讲到这里,你脑海里一定闪过一个聪明的想法:要实现整个神经网络的正向(反向)传播,只需要用一个for loop遍历所有的层(也就是我们的积木),将上一个层的正向(反向)传播所得到的结果传递给下一层不就行了吗!
——是的,这也是链式法则的思想,也是我们接下来要做的事情。但是,先别着急,在做这些之前,我们还需要将这些层(积木)作为属性传递给我们的神经网络类,因为我们无论是在forward
方法还是backward
方法中,都会用到!
# 各个层
self.layers=OrderedDict()
self.layers['Affine1']=Affine(self.params['W1'],self.params['b1'])
self.layers['ReLU1']=Relu()
self.layers['Affine2']=Affine(self.params['W2'],self.params['b2'])
self.lastLayer=SoftmaxWithLoss()
注意,这里出于同样地目的,我是用了字典这个容器来包装各个层。但是这个字典不是一个普通的字典,而是一个有序字典(普通的哈希表是无序的,这一点你是知道的)。这是因为我们希望各个层在进行正向和反向传播时都能保持前后顺序。
讲到这里,实际上我们的搭积木过程也完成了。—— 你可能会疑惑:嗯?你真的搭积木了吗,我怎么没有发现?——嗯,我真的搭建了,因为搭积木不就是把我们的组件(层)按照一定的顺序组合起来吗。我们的有序字典已经完成了这件事情!
完整初始化代码
from collections import OrderedDict
class TwoLayerNet:
def __init__(self, input_size,hidden_size,output_size):
# 初始化权重
self.params={}
self.params['W1']=np.random.randn(input_size,hidden_size)
self.params['b1']=np.zeros(hidden_size)
self.params['W2']=np.random.randn(hidden_size,output_size)
self.params['b2']=np.zeros(output_size)
# 各个层
self.layers=OrderedDict()
self.layers['Affine1']=Affine(self.params['W1'],self.params['b1'])
self.layers['ReLU1']=Relu()
self.layers['Affine2']=Affine(self.params['W2'],self.params['b2'])
self.lastLayer=SoftmaxWithLoss()
前向传播
我们的积木已经搭建完成,但是距离最终的任务还很遥远,因为我们现在只是有一个框架,我们所搭建的模型还没有一些实用的功能。神经网络的一个最基本的功能应该是——正向传播。其实现方法之前已经介绍过,通过for loop即可:
def predict(self,x):
for layer in self.layers.values():
x=layer.forward(x) # 逐层前向传播
return x
反向传播
想一下,我们反向传播,传播的是什么?——是误差(对参数的梯度),是损失(对参数的梯度)。所以,我们在实现反向传播的方法之前,需要先实现一个误差计算的方法。
def loss(self,x,t):
y=self.predict(x)
return self.lastLayer.forward(y,t)
下面,需要实现反向传播,我们的方法要完成这样的功能:输入数据对 ( X , y ) (X,y) (X,y),计算误差loss并计算loss对网络参数的梯度,然后将梯度返回。
def backward(self,x,t):
self.loss(x,t) # 前向传播
dout=1 # 最后一层的反向输入,为1
dout=self.lastLayer.backward(dout)
layers=list(self.layers.values()) # 将所有的层按顺序放入列表中
layers.reverse() # 反向传播,需要从最后一层开始,一直向前传播
for layer in layers: # 反向传播
dout=layer.backward(dout)
# 获取梯度
grads={}
grads['W1']=self.layers['Affine1'].dW
grads['b1']=self.layers['Affine1'].db
grads['W2']=self.layers['Affine2'].dW
grads['b2']=self.layers['Affine2'].db
return grads
在上面的代码中,有一点需要注意,那就是反向传播的过程完成以后,各个Affine层的梯度属性自动被更新,我们直接取各Affine个层的梯度属性即可。
其他方法
为了使神经网络的功能更加完善,我们再给神经网络添加一个方法,比如计算精度的方法:
def accuracy(self,x,t):
y=self.predict(x)
y=np.argmax(y,axis=1)
if t.ndim != 1:
t=np.argmax(t,axis=1)
accuracy=np.sum(y==t)/float(x.shape[0])
return accuracy
到目前为止,整个神经网络的搭建已经完成! 下一篇文章中,我将给出训练的过程。
代码
https://github.com/HanggeAi/numpy-neural-network