全连接神经网络的概念我就不介绍了, 对这个不是很了解的朋友, 可以移步其他博主的关于神经网络的文章, 这里只介绍我使用基本工具实现全连接神经网络的方法.
所用工具:
numpy == 1.16.4
matplotlib 最新版
我的思路是定义一个 layer 类, 在这个类里边构建传播的前向传播的逻辑, 以及反向传播的逻辑, 然后在构建一个 model 类, 在 model 类里边, 将 layer 类中的对象拼接, 就可以得到我们想要的模型.
在 Layers 类的传播中, 在 Dense 层中, 我是按照公式 output = X*w+b, 来计算输出. X 是 (m,n)的矩阵, 表示有 m 行数据, 每一个数据是 n 维的向量. w 是一个 (n,1) 的矩阵, 是我们要优化的参数. b 是一个 (m,1) 的矩阵, 是偏置. 在 Activation 层中, 我是按照公式 output = f 激活 (X) 来计算输出的. f 激活是激活函数, 是逐元素函数. 将 Dense 层与 Activation 层叠加, 就能实现 output = f 激活(X*w+b) 的效果, 如果多次交替叠加, 就相当于在计算 output = f 激活( f 激活(f 激活(X*w+b)*w+b)*w+b), 这里只演示了三层, 实际上这个就是全连接神经网络的基本数学表达式.
构建这个模型的难点在于梯度的计算以及反向传播逻辑. 中间层的每一层的输出对输入以及偏置求导, 都是一个矩阵对另一个矩阵的求导. 而矩阵的求导不同于高数中所学的导数, 链式法则也有一些不同. 关于这部分内容可参考: 矩阵求导术(上) https://zhuanlan.zhihu.com/p/24709748 , 矩阵求导术(下) https://zhuanlan.zhihu.com/p/24863977 , 这里不再讲述. 笔者正是在参考了这两篇文章的前提下实现这个过程的.
导入工具包:
- import numpy as np
- import matplotlib.pyplot as plt
定义 Layer 类中的 Dense 类中类:(这里可以把 layers 类单独拿出来作为一个父类, 其余的层可以继承 layers, 然后钉子自己的反向传播逻辑, 可以减少重复代码, 这里为了方便展示, 没有那么做)
- class Layers:
- class Dense:
- '''
- 全连接层
- '''
- def __init__(self, output_category):
- '''
- 接收并初始化一个输出维度, 用于确定这一层 w 的维度, 以及用于梯度计算
- :param output_category:
- '''
- self.output_category = output_category
- def __call__(self, Input):
- '''
- 使用魔法方法, 实例化对象后, 随机的方式初始化 w 参数,
- 实例化输入数据, 计算本层前向传播方式
- :param Input:
- :return:
- '''
- w_shape = (Input.shape[1], self.output_category)
- b_shape = (Input.shape[0], self.output_category)
- self.w = np.mat(np.random.random(size=w_shape))
- self.b = np.mat(np.random.random(size=b_shape))
- self.Input = Input
- result = self.Input * self.w +self.b
- self.result = result
- return result
- def backpropagation(self, w_grad_from_next_layer=None, learning_rate=None, use_bias=False):
- '''
- 反向传播算法的数学描述, 公式参考
- https://zhuanlan.zhihu.com/p/24863977
- 公式 Y = X * w +b ,
- dY = dX * w + X * dw + db
- = I * dX * w + X * dw * I + I * db * I (I 是单位矩阵,
- 公式里每个 I 都不一样维度, 具体是多少要参考它与谁相乘)
- vec(dY) = np.kron(w,I)*vec(dX) + np.kron(I,w)*vec(dw) + np.kron(I,I)*vec(db)
- :param w_grad_from_next_layer: 从下一层传过来的梯度
- :param learning_rate: 学习率
- :return:
- '''
- mid_w_grad = np.mat(np.kron(np.eye(self.output_category), self.Input))
- self.w_grad = w_grad_from_next_layer * mid_w_grad
- mid_x_grad = np.kron(self.w.T, np.eye(self.Input.shape[0]))
- self.x_grad = w_grad_from_next_layer * mid_x_grad
- if use_bias == True:
- mid_b_grad = np.kron(np.eye(self.output_category), np.eye(self.Input.shape[0]))
- self.b_grad = w_grad_from_next_layer * mid_b_grad
- if learning_rate is not None:
- self.w = self.w - learning_rate * self.w_grad.T
- if use_bias == True:
- self.b = self.b - learning_rate * self.b_grad
- return self.x_grad
定义 Layers 中的 Activation 类:(这里初始化的时候必须传入一个激活函数, 这个激活函数应该是一个类, 应该有一个 call 方法来定义它的前向逻辑, 应该有一个 grad 方法来计算梯度)
- class Layers:
- class Activation:
- '''
- 激活层
- '''
- def __init__(self, activate_func):
- '''
- 传入激活方程类
- :param output_category:
- '''
- self.activate_func = activate_func
- def __call__(self, Input):
- '''
- 使用魔法方法, 实例化对象后, 随机的方式初始化 w 参数,
- 实例化输入数据, 计算本层前向传播方式
- :param Input:
- :return:
- '''
- self.Input = Input
- self.activate_func_obj = self.activate_func(self.Input)
- result = self.activate_func_obj.call()
- self.result = result
- return result
- def backpropagation(self, w_grad_from_next_layer=None, learning_rate=None, ):
- '''
- 反向传播算法的数学描述, 公式参考
- https://zhuanlan.zhihu.com/p/24863977
- 公式 Y = f(X) (f 是逐元素函数)
- dY = df(X)
- = f'(X) ⊙ dX (⊙表示出逐元素相乘, 也就是通缩意义上的对应位置相乘)
- 所以
- vec(dY) = np.diagflat(f'(X))* vec(dX)
- :param w_grad_from_next_layer: 从下一层传过来的梯度
- :param learning_rate: 学习率
- :return:
- '''
- mid_x_grad = self.activate_func_obj.grad()
- self.x_grad = w_grad_from_next_layer * mid_x_grad
- return self.x_grad
定义了 Activation 后, 但这个方程需要传入一个激活函数类, 现在我们顺从定义一个函数类, 用来给激活层提供激活函数类. 这个类有两个功能, 一是定义正向传播方法, 一种是定义梯度计算.
- class Funcs:
- '''
- 方程
- '''
- class Relu:
- '''
- Relu 函数
- '''
- def __init__(self,Input):
- self.Input = Input
- pass
- def call(self):
- '''
- 计算结果, 并返回
- :return:
- '''
- result = np.multiply((self.Input>0),self.Input)
- return result
- def grad(self):
- '''
- 梯度计算
- :return:
- '''
- mid_grad = (self.Input>0)*1.0 #乘 1 讲 bollen 类型转化为 float 类型
- result = np.diagflat(mid_grad)
- return result
类似的, 我们可以定义更多的种类的激活函数, 比如 selu,sigmoid 等等. 到目前为止, 如果我们可以定义一个常用的损失函数, 那么我们就具备了搭建简单的神经网络的基本要求了. 这里我们的定义一个平方差损失函数.
- class Funcs:
- '''
- 方程
- '''
- class SqureLossError:
- '''
- 平方误差类
- '''
- def __init__(self,y,pred):
- self.y=y
- self.pred = pred
- def call(self):
- '''
- 计算损失, 并返回
- :return:
- '''
- result = np.linalg.norm(self.y-self.pred)
- return result
- def grad(self):
- '''
- 梯度计算
- :return:
- '''
- result = np.mat(2*(self.pred-self.y).T)
- return result
到这里, 我们已经具备了搭建神经网络的基本板块了, 我们可以用这些基本模块来搭建一个简单的神经网络模型. 下一章, 我会继续详述一下这个过程.
来源: https://www.cnblogs.com/gfanqi/p/12079350.html