引言
从零开始搭建一种可灵活调整每层神经元数量、激活函数以及损失函数等参数的神经网络(框架)。
目标
本文的目标在于设计一种,可以用于拟合,也可以用于分类的神经网络。该神经网络可以灵活地调整每一层的神经元数量和各种参数。
本文对于神经网络的基本原理不会详细讲解,只会在适当的地方点一下,如有想学习基础原理的,请另寻他处。
设计思路
设计这种有点像小框架的东西,很需要思路。现有的思路有两种,一种是类似于Keras的,一种是类似于Tensorflow的。
我选了类似于Keras的,将神经网络作为一个实例,往实例里面添加层,在每一层里面设置相应的参数。然后往实例里面加入数据进行训练,就完成了一个模型。
整体架构
整体设计思路采用面向对象的方法,所以首先得有一个神经网络类,还需要一个神经层类。每个类有相应的方法。
神经网络类
神经网络类里面的方法包括如下这些:
- 构造函数。存放一些全局变量。
- 神经层添加函数。用于给神经网络添加神经层。
- 训练函数。控制训练流程。
- 前向传播函数。计算前向传播。
- 反向传播函数。计算反向传播。
- 预测函数。用于预测新的数据。
- 保存模型。
- 加载模型。
神经层类
神经层类的方法包括这些:
- 构造函数。
- 前向计算。
- 计算误差。
- 计算神经元梯度。
- 更新权值。
- 计算激活函数梯度。
通用函数
这里面还包括一些通用函数:
- sigmoid函数。
- sigmoid梯度函数。
- relu函数。
- relu梯度函数。
- softmax函数。
- softmax与交叉熵函数。
- softmax与误差函数。
通用函数
万事开头难,那我们就从简单的开始。
sigmoid函数及其梯度函数
1 | def sigmoid(Z): |
relu函数及其梯度函数
1 | def relu(Z): |
softmax函数及其反向传播相关函数
1 | def softmax(z): |
这里尤其要说明一下这个softmax
。
softmax
可以作为中间层的激活函数(不推荐),也可以搭配交叉熵最为最后的激活函数进行多分类(强烈推荐)。但是用于分类的时候就有一点迷惑。
为什么要写softmax
和softmax与交叉熵
两个函数,而没有单独写交叉熵损失函数呢。(因为不好写XD!)其实是没必要写,因为反向传播的时候不单独算交叉熵的误差和梯度(这个确实是因为不好算),而是和softmax
一起算,跳过了中间的步骤。公式推导可以看这里。
那又为啥要单独写一个softmax
函数呢。因为前向传播分为两种(此处参考了这篇文章):
- 推理预测时的前向传播。因为此时不需要计算loss,也不需要反向传播,就不需要用到softmax, 只用logits就可以做出预测,可以减少计算量(所以用softmax概率化后的得分也是可以的)。
- 训练时的前向传播。此时需要记录中间变量z和a,用于反向传播,所以需要进行softmax计算。
神经层
构造函数
1 | class Layer: |
前向计算
1 | def _forward(self, X, Y): |
计算本层误差
1 | def count_err(self, front_err, Y): |
依据的公式为如下两个,第一个是最后一层的误差:
中间层的误差:
解释一下上面的式子。记每一层输入到神经元的输入记为z
,也就是w*x
,其中的x
为上一层的输出,w
为这一层的权值。
先看最后一层,最后一层的误差等于损失函数的梯度乘以这一层激活函数的梯度。
然后对于中间层,这一层的误差等于本层激活函数的梯度乘以(后一层的w
乘以后一层的误差)。
如果将损失函数也看做一个神经层,最后一层和损失函数之间的权值w=1
,那么损失函数的梯度就可以看做是最后一层的后一层的误差。就可以将上面的汇总成一个。
计算本层权值的梯度
1 | def count_gradient(self, al_1): |
计算权值梯度就是用前一层的a
乘以自己这一层的误差。a
表示一层神经元经过激活函数后的输出。
更新权值
1 | def update_parameters(self, rate, n): |
本层激活函数的梯度
1 | def act_gradient(self, Z): |
小结
将不同的激活函数、不同的层都集中到一个方法里面,是为了更好的实现面向对象编程,实现方法复用。
神经网络
构造函数
1 | class NeuralNetwork: |
用于存放神经层的。
添加神经层
1 | def add_layer(self, layers_dims, activation='relu'): |
前向传播
1 | def forward(self, X, Y, mission): |
反向传播
1 | def backward(self, out, x, y, rate, mission): |
在这里要特别注意,因为反向传播是从后往前算,所以我在最开始将神经层逆序了,在最后又逆序回来了。然后拟合的损失函数用的平方损失函数,就没有单独拿出来了,有兴趣的同学可以自己改,改成不同的损失函数。
训练
1 | def train(self, x, y, mission, epochs=1000, batch_size=50, learning_rate=0.01, data_rate=0.6, target=None, print_cost=False): |
训练的话就没什么特别需要注意的了,都是老生常谈。只是里面的load_data
和load_batch
两个函数我没有在这里给出,详情可以看我的github,在最后给出链接。
预测
1 | def predict(self, x, y, mission): |
预测也没什么好说,就是前向传播,然后分了回归和分类两个任务。
保存和加载
1 | def save_model(self, name): |
用的是Python自带的pickle
库,还是挺好使的。
总结
中间很多都是一笔带过,其实只是涉及实现的话,我觉得难点在于以下几点:
- 将分类和回归的正反传播结合起来。
- 弄懂神经网络最后一层和损失函数之间该怎么反向传播,特别是分类的时候。Tips:我觉得这里可以将损失函数也理解成一层,但是和最后一层的输出是直连,中间的权值为1。
- 弄懂
softmax
和交叉熵怎么一起算反向传播。
后记
更详细更全面的代码,可以看我的github。欢迎Star~