BP(Back Propagation)神经网络是1986年由Rumelhart和McCelland为首的科学家小组提出,是一种==按偏差逆传播算法训练的多层前馈网络==,是目前应用最普遍的神经网络模型之一。BP网络能学习和存贮大量的==输入-输出模式映射关系==,而无需事前揭示描述这种映射关系的数学方程。它的学习规则是使用梯度降低法,经过反向传播来不断调整网络的权值和阈值,使==网络的偏差平方和最小==。BP神经网络模型拓扑结构包括输入层(input)、隐层(hidden layer)和输出层(output layer)。python
注:本文将包含大量用 Python 编写的代码片断。但愿读起来不会太无聊。web
Keras、TensorFlow、PyTorch 等高级框架能够帮助咱们快速构建复杂模型。深刻研究并理解其中的理念颇有价值。下面尝试只使用 NumPy 构建一个全运算的神经网络,经过解决简单的分类问题来测试模型,并将其与 Keras 构建的神经网络进行性能比较。面试
密集神经网络架构算法
在开始编程以前,须要先整理一个基本的路线图。咱们的目标是建立一个程序,该程序能建立一个拥有特定架构(层的数量和大小以及激活函数都是肯定的)的密集链接神经网络。图 1 给出了网络的示例。最重要的是,网络必须可训练且能进行预测。编程
神经网络框图
上图显示了在训练神经网络时须要执行的操做。它还显示了在单次迭代的不一样阶段,须要更新和读取多少参数。构建正确的数据结构并熟练地管理其状态是任务中最困难的部分之一。数组
l 层的权值矩阵 W 和偏置向量 b 的维数。网络
首先初始化每一层的权值矩阵 W 和偏置向量 b。在图 3 中。先准备一个为系数分配适当维数的清单。上标 [l] 表示当前层的索引 (从 1 数起),值 n 表示给定层中的单位数。假设描述 NN 架构的信息将以相似 Snippet 1 的列表形式传递到程序中,列表的每一项是一个描述单个网络层基本参数的字典:input_dim 是输入层信号向量的大小,output_dim 是输出层激活向量的大小,activation 是在内层使用的激活函数。数据结构
nn_architecture = [ {"input_dim": 2, "output_dim": 4, "activation": "relu"}, {"input_dim": 4, "output_dim": 6, "activation": "relu"}, {"input_dim": 6, "output_dim": 6, "activation": "relu"}, {"input_dim": 6, "output_dim": 4, "activation": "relu"}, {"input_dim": 4, "output_dim": 1, "activation": "sigmoid"}, ]
Snippet 1:包含描述特定神经网络参数的列表。该列表对应图 1 所示的 NN。架构
若是你对这个话题很熟悉,你可能已经在脑海中听到一个焦虑的声音:「嘿,嘿!这里有问题!有些领域是没必要要的……」是的,此次你心里的声音是对的。前一层输出的向量是下一层的输入,因此实际上只知道一个向量的大小就足够了。但我特地使用如下符号来保持全部层之间目标的一致性,使那些刚接触这一课题的人更容易理解代码。app
def init_layers(nn_architecture, seed = 99): np.random.seed(seed) number_of_layers = len(nn_architecture) params_values = {} for idx, layer in enumerate(nn_architecture): layer_idx = idx + 1 layer_input_size = layer["input_dim"] layer_output_size = layer["output_dim"] params_values['W' + str(layer_idx)] = np.random.randn( layer_output_size, layer_input_size) * 0.1 params_values['b' + str(layer_idx)] = np.random.randn( layer_output_size, 1) * 0.1 return params_values
Snippet 2:初始化权值矩阵和偏置向量值的函数。
最后是这一部分最主要的任务——层参数初始化。看过 Snippet 2 上的代码并对 NumPy 有必定经验的人会发现,矩阵 W 和向量 b 被小的随机数填充。这种作法并不是偶然。权值不能用相同的数字初始化,否则会出现「对称问题」。若是全部权值同样,无论输入 X 是多少,隐藏层中的全部单位都相同。在某种程度上,咱们在初始阶段就会陷入死循环,不管训练模型时间多长、网络多深都没法逃脱。线性代数是不会被抵消的。
在第一次迭代中,使用较小的数值能够提升算法效率。经过图 4 所示的 sigmoid 函数图能够看到,对于较大数值,它几乎是平的,这十分影响 NN 的学习速度。总之,使用小随机数进行参数初始化是一种简单的方法,能保证咱们的算法有足够好的起点。准备好的参数值存储在带有惟一标定其父层的 python 字典中。字典在函数末尾返回,所以算法的下一步是访问它的内容。
算法中使用的激活函数
咱们将使用的函数中,有几个函数很是简单但功能强大。激活函数能够写在一行代码中,但却能使神经网络表现出自身所需的非线性性能和可表达性。「没有它们,咱们的神经网络就会变成由多个线性函数组合而成的线性函数。」可选激活函数不少,但在这个项目中,我决定使用这两种——sigmoid 和 ReLU。为了可以获得完整循环并同时进行前向和反向传播,咱们还须要求导。
def sigmoid(Z): return 1/(1+np.exp(-Z)) def relu(Z): return np.maximum(0,Z) def sigmoid_backward(dA, Z): sig = sigmoid(Z) return dA * sig * (1 - sig) def relu_backward(dA, Z): dZ = np.array(dA, copy = True) dZ[Z <= 0] = 0; return dZ;
Snippet 3:ReLU 和 Sigmoid 激活函数及其导数。
设计好的神经网络有一个简单的架构。信息以 X 矩阵的形式沿一个方向传递,穿过隐藏的单元,从而获得预测向量 Y_hat。为了便于阅读,我将前向传播分解为两个单独的函数——对单个层进行前向传播和对整个 NN 进行前向传播。
def single_layer_forward_propagation(A_prev, W_curr, b_curr, activation="relu"): Z_curr = np.dot(W_curr, A_prev) + b_curr if activation is "relu": activation_func = relu elif activation is "sigmoid": activation_func = sigmoid else: raise Exception('Non-supported activation function') return activation_func(Z_curr), Z_curr
Snippet 4:单层前向传播步骤
这部分代码多是最容易理解的。给定上一层的输入信号,咱们计算仿射变换 Z,而后应用选定的激活函数。经过使用 NumPy,咱们能够利用向量化——一次性对整个层和整批示例执行矩阵运算。这减小了迭代次数,大大加快了计算速度。除了计算矩阵 A,咱们的函数还返回一个中间值 Z。做用是什么呢?答案如图 2 所示。咱们须要在反向传播中用到 Z。
在前向传播中使用的单个矩阵的维数
使用预设好的一层前向函数后,就能够轻松地构建整个前向传播。这个函数稍显复杂,它的做用不只是预测,还要管理中间值的集合。它返回 Python 字典,其中包含为特定层计算的 A 和 Z 值。
def full_forward_propagation(X, params_values, nn_architecture): memory = {} A_curr = X for idx, layer in enumerate(nn_architecture): layer_idx = idx + 1 A_prev = A_curr activ_function_curr = layer["activation"] W_curr = params_values["W" + str(layer_idx)] b_curr = params_values["b" + str(layer_idx)] A_curr, Z_curr = single_layer_forward_propagation(A_prev, W_curr, b_curr, activ_function_curr) memory["A" + str(idx)] = A_prev memory["Z" + str(layer_idx)] = Z_curr return A_curr, memory
Snippnet 5:完整前向传播步骤
为了观察进度,保证正确方向,咱们一般须要计算损失函数的值。「通常来讲,损失函数用来表征咱们与『理想』解决方案的距离。」咱们根据要解决的问题来选择损失函数,像 Keras 这样的框架会有多种选择。由于我计划测试咱们的 NN 在两类点上的分类,因此选择二进制交叉熵,它定义以下。为了得到更多学习过程的信息,我决定引入一个计算准确率的函数。
Snippnet 6:损失函数和准确率计算
许多缺少经验的深度学习爱好者认为反向传播是一种难以理解的算法。微积分和线性代数的结合经常使缺少数学基础的人望而却步。因此若是你没法立刻理解,也不要担忧。相信我,咱们都经历过这个过程。
def single_layer_backward_propagation(dA_curr, W_curr, b_curr, Z_curr, A_prev, activation="relu"): m = A_prev.shape[1] if activation is "relu": backward_activation_func = relu_backward elif activation is "sigmoid": backward_activation_func = sigmoid_backward else: raise Exception('Non-supported activation function') dZ_curr = backward_activation_func(dA_curr, Z_curr) dW_curr = np.dot(dZ_curr, A_prev.T) / m db_curr = np.sum(dZ_curr, axis=1, keepdims=True) / m dA_prev = np.dot(W_curr.T, dZ_curr) return dA_prev, dW_curr, db_curr
Snippnet 7:单层反向传播步骤
人们经常混淆反向传播与梯度降低,但实际上这是两个独立的问题。前者的目的是有效地计算梯度,然后者是利用计算获得的梯度进行优化。在 NN 中,咱们计算关于参数的代价函数梯度(以前讨论过),可是反向传播能够用来计算任何函数的导数。这个算法的本质是在已知各个函数的导数后,利用微分学中的链式法则计算出结合成的函数的导数。对于一层网络,这个过程可用下面的公式描述。本文主要关注的是实际实现,故省略推导过程。经过公式能够看出,预先记住中间层的 A 矩阵和 Z 矩阵的值是十分必要的。
一层中的前向和反向传播
就像前向传播同样,我决定将计算分为两个独立的函数。第一个函数(Snippnet7)侧重一个单独的层,能够归结为用 NumPy 重写上面的公式。第二个表示彻底反向传播,主要在三个字典中读取和更新值。而后计算预测向量(前向传播结果)的代价函数导数。这很简单,它只是重述了下面的公式。而后从末端开始遍历网络层,并根据图 6 所示的图计算全部参数的导数。最后,函数返回 python 字典,其中就有咱们想求的梯度。
def full_backward_propagation(Y_hat, Y, memory, params_values, nn_architecture): grads_values = {} m = Y.shape[1] Y = Y.reshape(Y_hat.shape) dA_prev = - (np.divide(Y, Y_hat) - np.divide(1 - Y, 1 - Y_hat)); for layer_idx_prev, layer in reversed(list(enumerate(nn_architecture))): layer_idx_curr = layer_idx_prev + 1 activ_function_curr = layer["activation"] dA_curr = dA_prev A_prev = memory["A" + str(layer_idx_prev)] Z_curr = memory["Z" + str(layer_idx_curr)] W_curr = params_values["W" + str(layer_idx_curr)] b_curr = params_values["b" + str(layer_idx_curr)] dA_prev, dW_curr, db_curr = single_layer_backward_propagation( dA_curr, W_curr, b_curr, Z_curr, A_prev, activ_function_curr) grads_values["dW" + str(layer_idx_curr)] = dW_curr grads_values["db" + str(layer_idx_curr)] = db_curr return grads_values
Snippnet 8:全反向传播步骤
该方法的目标是利用梯度优化来更新网络参数,以使目标函数更接近最小值。为了实现这项任务,咱们使用两个字典做为函数参数:params_values 存储参数的当前值;grads_values 存储根据参数计算出的代价函数导数。虽然该优化算法很是简单,只需对每一层应用下面的方程便可,但它能够做为更高级优化器的一个良好起点,因此我决定使用它,这也多是我下一篇文章的主题。
def update(params_values, grads_values, nn_architecture, learning_rate): for layer_idx, layer in enumerate(nn_architecture): params_values["W" + str(layer_idx)] -= learning_rate * grads_values["dW" + str(layer_idx)] params_values["b" + str(layer_idx)] -= learning_rate * grads_values["db" + str(layer_idx)] return params_values;
Snippnet 9:利用梯度降低更新参数值
任务中最困难的部分已通过去了,咱们已经准备好了全部必要的函数,如今只需把它们按正确的顺序组合便可。为了更好地理解操做顺序,须要对照图 2 的表。该函数通过训练和期间的权值变化返回了最优权重。只须要使用接收到的权重矩阵和一组测试数据便可运行完整的前向传播,从而进行预测。
def train(X, Y, nn_architecture, epochs, learning_rate): params_values = init_layers(nn_architecture, 2) cost_history = [] accuracy_history = [] for i in range(epochs): Y_hat, cashe = full_forward_propagation(X, params_values, nn_architecture) cost = get_cost_value(Y_hat, Y) cost_history.append(cost) accuracy = get_accuracy_value(Y_hat, Y) accuracy_history.append(accuracy) grads_values = full_backward_propagation(Y_hat, Y, cashe, params_values, nn_architecture) params_values = update(params_values, grads_values, nn_architecture, learning_rate) return params_values, cost_history, accuracy_history
Snippnet 10:训练模型
若是对Python编程、网络爬虫、机器学习、数据挖掘、web开发、人工智能、面试经验交流。感兴趣能够519970686,群内会有不按期的发放免费的资料连接,这些资料都是从各个技术网站搜集、整理出来的,若是你有好的学习资料能够私聊发我,我会注明出处以后分享给你们。
如今能够检验咱们的模型在简单的分类问题上的表现了。我生成了一个由两类点组成的数据集,如图 7 所示。而后让模型学习对两类点分类。为了便于比较,我还在高级框架中编写了 Keras 模型。两种模型具备相同的架构和学习速率。尽管如此,这样对比仍是稍有不公,由于咱们准备的测试太过于简单。最终,NumPy 模型和 Keras 模型在测试集上的准确率都达到了 95%,可是咱们的模型须要多花几十倍的时间才能达到这样的准确率。在我看来,这种状态主要是因为缺少适当的优化。
测试数据集
两种模型实现的分类边界可视化