认真学习,佛系更博。
上一章简单介绍了如何实现数据的读取功能,本章将详细介绍如何实现神经网络最基础的层:全连接层。
全连接层的原理想必很多读者都接触过很多资料,比如链式法则,反向传播,梯度下降法等等。说来惭愧,博主也早早地接触过,确一直没有仔细推敲其中的原理,以至于一直对该网络层困惑了很久,其实静下心来仔细去研究一下,会发现内部原理也很简单直观,我们先来了解一下链式法则:
我们知道,神经网络的很多操作都可以当作一个个独立的层,比如卷积层、全连接层、sigmoid激活层等,其原因在于,这些操作都可以当作一系列对应的函数映射,比如全连接层可以表示为:
我们进行一下封装,表示为,其中f表示矩阵运算,如果在后面再接一个激活层,比如sigmoid,可以表示为:
可以封装为,其中g表示sigmoid运算;
前向运算很好理解,直接带入x就可以计算到y,那么反向传播该怎样计算呢?
我们先来了解一个叫gradient_check的思想,对于每个网络层,其参数(W,b)的梯度定义为结果误差变化相对于变量变化的程度,比如某一层的W,给予其一个极小的变化,误差函数发生变化的值就是该W的梯度,基于该思想,我们可以对网络模型的每个参数依次施加微小的变化,从而计算每个参数的梯度,最后运用梯度下降法更新参数。
上诉方法被验证为一种可行的方法,但也存在明显的缺点,比如效率极低,因此,引出了链式法则的思想:
我们知道,反向传播就是求y对x的导数,复合函数求导满足下图(截自百度知道),这是在高中或大学的知识:
神经网络层就相当于上面的f、p、g操作,若我们想对某一层的参数进行求导,则可以利用该求导公式,该计算也被称为链式法则;
于是乎,我们可以想象,在反向传播过程中,对于某一个全连接层,先接收来自上层的梯度,然后对本层参数进行求导,最后计算针对输入数据的梯度,作为前一层的更新梯度,这便是全连接层(卷积层同理)的参数更新过程;
我们先定义一个父类网络层,实现一些基本操作,在enet下新建一个新的模块layers,并新建文件bas_layer.py:
import numpy as np
class Layer(object):
"""
基础网络层
"""
def __init__(self, layer_type="layer"):
self.input_shape = None
self.output_shape = None
self.weight_shape = None
self.layer_type = layer_type
self.activation = None
self.name = None
self.cache = None
@staticmethod
def add_weight(shape=None,
dtype=np.float,
initializer="normal",
node_num=None):
"""
初始化网络参数
:param node_num: 上一层神经网络节点的数量
:param shape: 参数的shape
:param dtype: 参数的dtype
:param initializer: 初始化方法
:return: 初始化后的数据
"""
if initializer == "zero":
return np.zeros(shape=shape, dtype=dtype)
if initializer == "normal":
return np.random.normal(size=shape) * np.sqrt(1 / np.prod(node_num))
# return np.random.normal(size=shape)
raise TypeError("initializer must be normal or zero")
def build(self, *args, **k_args):
"""
编译网络层
:param k_args:
:return:
"""
pass
def forward(self, *args, **k_args):
"""
前向运算
:param k_args:
:return:
"""
pass
def backward(self, *args, **k_args):
"""
反向传播,只计算梯度而不更新参数
:param k_args:
:return:
"""
pass
def update(self, *args, **k_args):
"""
更新参数
:param k_args:
:return:
"""
pass
def get_input_shape(self):
"""
获取网络输入形状
:return:
"""
return self.input_shape
def get_output_shape(self):
"""
获取网络输出形状
:return:
"""
return self.output_shape
def get_layer_type(self):
"""
获取网络类型
:return:
"""
return self.layer_type
def get_activation_layer(self):
"""
获取激活函数类型
:return:
"""
return self.activation
def get_weight_shape(self):
"""
获取权重形状
:return:
"""
return self.weight_shape
def get_name(self):
"""
获取网络层名
:return:
"""
return self.name
def set_name(self, name):
"""
设置网络层名字
:param name: 名字
:return:
"""
if not self.name:
self.name = name
定义了一些公公变量,这些变量在以后会用到,另外添加了一个add_weight方法,该方法用于初始化网络的参数;
然后新建dense.py文件,dense的实现稍微复杂,因为我们要考虑神经单元的个数,是否使用激活函数等;另外,我们这里做一下说明,一般的神经网络框架都单独把优化器提出来作为统一的控制,但是神经网络的每层参数都可以使用不同的优化方法,adam、momentum,因此,我们为每个层建立一个优化控制器;
下面为dense的代码,将在下面做详细说明:
from enet.layers.base_layer import Layer
import numpy as np
from enet.optimizer import optimizer_dict
class Dense(Layer):
"""
全连接神经网络类
"""
def __init__(self, kernel_size=None, activation=None, input_shape=None, optimizer="sgd", name=None, **k_args):
"""
:param kernel_size: 神经元个数
:param activation: 激活函数
:param input_shape: 输入shape,只在输入层有效;
:param optimizer: 优化器;
:param name: 网络层名字;
"""
super(Dense, self).__init__(layer_type="dense")
assert activation in {None, "sigmoid", "relu", "softmax"}
assert optimizer in {"sgd", "momentum", "adagrad", "adam", "rmsprop"}
self.output_shape = kernel_size
self.activation = activation
self.name = name
# 该处的input_shape只在输入层有效,input_shape样式为(784,)
if input_shape:
self.input_shape = input_shape[0]
self.weight = None
self.bias = None
# self.use_bias = use_bias
self.optimizer = optimizer_dict[optimizer](**k_args)
def build(self, input_shape):
"""
根据input_shape来构建网络模型参数
:param input_shape: 输入形状
:return: 无返回值
"""
last_dim = input_shape
self.input_shape = input_shape
shape = (last_dim, self.output_shape)
self.weight_shape = shape
self.weight = self.add_weight(shape=shape, initializer="normal", node_num=input_shape)
self.bias = self.add_weight(shape=(self.output_shape,), initializer="zero")
def forward(self, input_signal, *args, **k_args):
"""
前向传播
:param input_signal: 输入信息
:return: 输出信号
"""
self.cache = input_signal
return np.dot(input_signal, self.weight) + self.bias
def backward(self, delta):
"""
反向传播
:param delta: 输入梯度
:return: 误差回传
"""
# if self.use_bias:
# delta_b = np.mean(delta, axis=0)
# else:
# delta_b = 0
delta_b = np.sum(delta, axis=0)
delta_w = np.dot(self.cache.transpose(), delta)
self.optimizer.grand(delta_w=delta_w, delta_b=delta_b)
# 回传给前一层的梯度
return np.dot(delta, self.weight.transpose())
def update(self, lr):
"""
更新参数
:param lr: 学习率
:return:
"""
delta_w, delta_b = self.optimizer.get_delta_and_reset(lr, "delta_w", "delta_b")
self.weight += delta_w
self.bias += delta_b
首先在初始化阶段,我们定义了关键参数,kernel_size,该参数表示该层神经元的个数,另外定义了优化器,激活函数等,这部分代码看不懂的不要着急,在后面实现优化器部分将详细介绍;
build函数完成了函数的初始化,将在网络模型的compile函数进行调用,我们需要关注的是forward和backward函数,实现了网络层的前向传播和反向传播;
forward容易理解,直接用即可计算结果,难点在于backward,我们先来弄清楚矩阵的反向传播公式;
假设矩阵运算为:
上层关于y的求导为,则关于X,W, b的求导为:
关于公式的推导,这里不做过多证明,可以直接拿来用,感兴趣的可以自己展开矩阵推导下,过程并不难,或者用几个小矩阵模拟一下,很容易得出答案;
于是乎,我们对于权重和偏执的求导便可以写成如下关键部分:
delta_b = np.sum(delta, axis=0)
delta_w = np.dot(self.cache.transpose(), delta)
注意,这里使用sum而不是mean,原因是我们计算误差梯度时已经做了平均的操作,下一章将会介绍;
回传给上一层的梯度为:
# 回传给前一层的梯度
return np.dot(delta, self.weight.transpose())
然后更新参数,回传梯度即可;
好了,本篇文章先写到这里,下一篇将介绍如何实现优化器类,以及激活函数的实现;
整个代码的github网址为:https://github.com/darkwhale/neural_network,不断更新中;