如何通过梯度检验帮助实现反向传播

本文介绍了如何使用梯度检验方法确认反向传播代码是否准确。

在《Coding Neural Network - Forward Propagation and Backpropagation》一文中,我们借助 numpy 实现了前向传播和反向传播算法。但从头开始实现反向传播很容易遇到 bug 或者报错。因此,在训练数据上运行神经网络之前,必须检验反向传播的实现是否正确。不过首先,我们先复习一下反向传播的概念:从最后的节点开始,沿着拓扑排序的反方向遍历所有节点,计算每个边的尾节点相对于损失函数导数。换言之,计算损失函数对所有参数导数:∂J/∂θ,其中θ表示模型中的参数

我们通过计算数值梯度并比较数值梯度和根据反向传播求出的梯度(解析梯度)间的差异,来测试我们的实现代码。这里有两种数值梯度的计算方法:

  • 右边形式:

  •  [J(θ+ϵ)−J(θ)]/ϵ

  • 双边形式(见图 2):

  •  [J(θ+ϵ)−J(θ−ϵ)]/2ϵ

图 2:双边数值梯度

逼近导数的双边形式比右边形式更接近真实值。我们以 f(x)=x^2 为例,在 x=3 处计算导数

  • 解析导数:∇_xf(x)=2x ⇒∇_xf(3)=6

  • 双边数值导数:[(3+1e−2)^2−(3−1e−2)^2]/[2∗1e−2]=5.999999999999872

  • 右边数值导数:[(3+1e−2)^2−3^2]/[1e−2]=6.009999999999849

可以看到,解析梯度和双边数值梯度之间的差值几乎为零;而和右边形式的数值梯度之间的差值为 0.01。因此在下文中,我们使用双边形式计算数值梯度。

另外,我们使用下式对数值梯度和解析梯度间的差值进行标准化。

如果差值≤10^−7,可以认为反向传播的实现代码没有问题;否则,就需要回去检查代码,因为一定有什么地方出错了。

以下是完成梯度检验的步骤:

1. 随机从训练集中抽取一些样本,用来计算数值梯度和解析梯度(不要使用所有训练样本,因为梯度检验运行会很慢)。

2. 初始化参数

3. 计算前向传播和交叉熵损失。

4. 利用写好的反向传播的实现代码计算梯度(解析梯度)。

5. 计算双边形式的数值梯度。

6. 计算数值梯度和解析解梯度的差值。

这里,我们使用《Coding Neural Network - Forward Propagation and Backpropagation》中所写的函数来实现参数初始化、前向传播、反向传播以及交叉熵损失的计算。

导入数据。

# Loading packages
import sys

import h5py
import matplotlib.pyplot as plt
import numpy as np
from numpy.linalg import norm
import seaborn as sns

sys.path.append("../scripts/")
from coding_neural_network_from_scratch import (initialize_parameters,
                                                L_model_forward,
                                                L_model_backward,
                                                compute_cost)
# Import the data
train_dataset = h5py.File("../data/train_catvnoncat.h5")
X_train = np.array(train_dataset["train_set_x"]).T
y_train = np.array(train_dataset["train_set_y"]).T
X_train = X_train.reshape(-1, 209)
y_train = y_train.reshape(-1, 209)

X_train.shape, y_train.shape
((12288, 209), (1, 209))

编写 helper 函数,帮助实现参数和梯度词典(gradients dictionary)到向量的相互转换。

def dictionary_to_vector(params_dict):"""
    Roll a dictionary into a single vector.

    Arguments
    ---------
    params_dict : dict
        learned parameters.

    Returns
    -------
    params_vector : array
        vector of all parameters concatenated.
    """count = 0for key in params_dict.keys():new_vector = np.reshape(params_dict[key], (-1, 1))if count == 0:theta_vector = new_vectorelse:theta_vector = np.concatenate((theta_vector, new_vector))count += 1return theta_vectordef vector_to_dictionary(vector, layers_dims):"""
    Unroll parameters vector to dictionary using layers dimensions.

    Arguments
    ---------
    vector : array
        parameters vector.
    layers_dims : list or array_like
        dimensions of each layer in the network.

    Returns
    -------
    parameters : dict
        dictionary storing all parameters.
    """L = len(layers_dims)parameters = {}k = 0for l in range(1, L):# Create temp variable to store dimension used on each layerw_dim = layers_dims[l] * layers_dims[l - 1]b_dim = layers_dims[l]# Create temp var to be used in slicing parameters vectortemp_dim = k + w_dim# add parameters to the dictionaryparameters["W" + str(l)] = vector[k:temp_dim].reshape(layers_dims[l], layers_dims[l - 1])parameters["b" + str(l)] = vector[temp_dim:temp_dim + b_dim].reshape(b_dim, 1)k += w_dim + b_dimreturn parametersdef gradients_to_vector(gradients):"""
    Roll all gradients into a single vector containing only dW and db.

    Arguments
    ---------
    gradients : dict
        storing gradients of weights and biases for all layers: dA, dW, db.

    Returns
    -------
    new_grads : array
        vector of only dW and db gradients.
    """# Get the number of indices for the gradients to iterate overvalid_grads = [key for key in gradients.keys()if not key.startswith("dA")]L = len(valid_grads)// 2count = 0# Iterate over all gradients and append them to new_grads listfor l in range(1, L + 1):if count == 0:new_grads = gradients["dW" + str(l)].reshape(-1, 1)new_grads = np.concatenate((new_grads, gradients["db" + str(l)].reshape(-1, 1)))else:new_grads = np.concatenate((new_grads, gradients["dW" + str(l)].reshape(-1, 1)))new_grads = np.concatenate((new_grads, gradients["db" + str(l)].reshape(-1, 1)))count += 1return new_grads

最后,编写梯度检验函数,利用此函数计算解析梯度和数值梯度之间的差值,并借此判断反向传播的实现代码是否正确。我们随机抽取 1 个样本来计算差值:

def forward_prop_cost(X, parameters, Y, hidden_layers_activation_fn="tanh"):"""
    Implements the forward propagation and computes the cost.

    Arguments
    ---------
    X : 2d-array
        input data, shape: number of features x number of examples.
    parameters : dict
        parameters to use in forward prop.
    Y : array
        true "label", shape: 1 x number of examples.
    hidden_layers_activation_fn : str
        activation function to be used on hidden layers: "tanh", "relu".

    Returns
    -------
    cost : float
        cross-entropy cost.
    """# Compute forward propAL, _ = L_model_forward(X, parameters, hidden_layers_activation_fn)# Compute costcost = compute_cost(AL, Y)return costdef gradient_check(parameters, gradients, X, Y, layers_dims, epsilon=1e-7,hidden_layers_activation_fn="tanh"):"""
    Checks if back_prop computes correctly the gradient of the cost output by
    forward_prop.

    Arguments
    ---------
    parameters : dict
        storing all parameters to use in forward prop.
    gradients : dict
        gradients of weights and biases for all layers: dA, dW, db.
    X : 2d-array
        input data, shape: number of features x number of examples.
    Y : array
        true "label", shape: 1 x number of examples.
    epsilon : 
        tiny shift to the input to compute approximate gradient.
    layers_dims : list or array_like
        dimensions of each layer in the network.

    Returns
    -------
    difference : float
        difference between approx gradient and back_prop gradient
    """# Roll out parameters and gradients dictionariesparameters_vector = dictionary_to_vector(parameters)gradients_vector = gradients_to_vector(gradients)# Create vector of zeros to be used with epsilongrads_approx = np.zeros_like(parameters_vector)for i in range(len(parameters_vector)):# Compute cost of theta + epsilontheta_plus = np.copy(parameters_vector)theta_plus[i] = theta_plus[i] + epsilonj_plus = forward_prop_cost(X, vector_to_dictionary(theta_plus, layers_dims), Y,hidden_layers_activation_fn)# Compute cost of theta - epsilontheta_minus = np.copy(parameters_vector)theta_minus[i] = theta_minus[i] - epsilonj_minus = forward_prop_cost(X, vector_to_dictionary(theta_minus, layers_dims), Y,hidden_layers_activation_fn)# Compute numerical gradientsgrads_approx[i] = (j_plus - j_minus) / (2 * epsilon)# Compute the difference of numerical and analytical gradientsnumerator = norm(gradients_vector - grads_approx)denominator = norm(grads_approx) + norm(gradients_vector)difference = numerator / denominatorif difference > 10e-7:print ("\033[31mThere is a mistake in back-propagation " +\
               "implementation. The difference is: {}".format(difference))else:print ("\033[32mThere implementation of back-propagation is fine! "+\
               "The difference is: {}".format(difference))return difference
# Set up neural network architecture
layers_dims = [X_train.shape[0], 5, 5, 1]

# Initialize parameters
parameters = initialize_parameters(layers_dims)

# Randomly selecting 1 example from training data
perms = np.random.permutation(X_train.shape[1])
index = perms[:1]

# Compute forward propagation
AL, caches = L_model_forward(X_train[:, index], parameters, "tanh")

# Compute analytical gradients
gradients = L_model_backward(AL, y_train[:, index], caches, "tanh")

# Compute difference of numerical and analytical gradients
difference = gradient_check(parameters, gradients, X_train[:, index], y_train[:, index], layers_dims)

反向传播的实现是 OK 的!这里的差值是 3.0220555297630148e-09

结论

以下是一些关键点:

  • 双边形式的数值梯度在逼近解析梯度时效果比单边形式的数值梯度更好。

  • 由于梯度检验的运行很慢,因此:

  •  进行梯度检验时,只使用一个或少数样本;

  •  在确认反向传播的实现代码无误后,训练神经网络时记得取消梯度检验函数的调用。

  • 如果使用了 drop-out 策略,(直接进行)梯度检验会失效。可以在进行梯度检验时,将 keep-prob 设置为 1,训练神经网络时,再进行修改。

  • 通常采用 e=10e-7 作为检查解析梯度和数值梯度间差值的基准。如果差值小于 10e-7,则反向传播的实现代码没有问题。

  • 幸运的是,在诸如 TensorFlow、PyTorch 等深度学习框架中,我们几乎不需要自己实现反向传播,因为这些框架已经帮我们计算好梯度了;但是,在成为一个深度学习工作者之前,动手实现这些算法是很好的练习,可以帮助我们理解其中的原理。

源代码地址:https://github.com/ImadDabbura/blog-posts/blob/master/notebooks/Coding-Neural-Network-Gradient-Checking.ipynb

工程最优化反向传播梯度下降
1
相关数据
深度学习技术

深度学习(deep learning)是机器学习的分支,是一种试图使用包含复杂结构或由多重非线性变换构成的多个处理层对数据进行高层抽象的算法。 深度学习是机器学习中一种基于对数据进行表征学习的算法,至今已有数种深度学习框架,如卷积神经网络和深度置信网络和递归神经网络等已被应用在计算机视觉、语音识别、自然语言处理、音频识别与生物信息学等领域并获取了极好的效果。

交叉熵技术

交叉熵(Cross Entropy)是Loss函数的一种(也称为损失函数或代价函数),用于描述模型预测值与真实值的差距大小

基准技术

一种简单的模型或启发法,用作比较模型效果时的参考点。基准有助于模型开发者针对特定问题量化最低预期效果。

参数技术

在数学和统计学裡,参数(英语:parameter)是使用通用变量来建立函数和变量之间关系(当这种关系很难用方程来阐述时)的一个数量。

损失函数技术

在数学优化,统计学,计量经济学,决策理论,机器学习和计算神经科学等领域,损失函数或成本函数是将一或多个变量的一个事件或值映射为可以直观地表示某种与之相关“成本”的实数的函数。

TensorFlow技术

TensorFlow是一个开源软件库,用于各种感知和语言理解任务的机器学习。目前被50个团队用于研究和生产许多Google商业产品,如语音识别、Gmail、Google 相册和搜索,其中许多产品曾使用过其前任软件DistBelief。

导数技术

导数(Derivative)是微积分中的重要基础概念。当函数y=f(x)的自变量x在一点x_0上产生一个增量Δx时,函数输出值的增量Δy与自变量增量Δx的比值在Δx趋于0时的极限a如果存在,a即为在x0处的导数,记作f'(x_0) 或 df(x_0)/dx。

张量技术

张量是一个可用来表示在一些矢量、标量和其他张量之间的线性关系的多线性函数,这些线性关系的基本例子有内积、外积、线性映射以及笛卡儿积。其坐标在 维空间内,有 个分量的一种量,其中每个分量都是坐标的函数,而在坐标变换时,这些分量也依照某些规则作线性变换。称为该张量的秩或阶(与矩阵的秩和阶均无关系)。 在数学里,张量是一种几何实体,或者说广义上的“数量”。张量概念包括标量、矢量和线性算子。张量可以用坐标系统来表达,记作标量的数组,但它是定义为“不依赖于参照系的选择的”。张量在物理和工程学中很重要。例如在扩散张量成像中,表达器官对于水的在各个方向的微分透性的张量可以用来产生大脑的扫描图。工程上最重要的例子可能就是应力张量和应变张量了,它们都是二阶张量,对于一般线性材料他们之间的关系由一个四阶弹性张量来决定。

神经网络技术

(人工)神经网络是一种起源于 20 世纪 50 年代的监督式机器学习模型,那时候研究者构想了「感知器(perceptron)」的想法。这一领域的研究者通常被称为「联结主义者(Connectionist)」,因为这种模型模拟了人脑的功能。神经网络模型通常是通过反向传播算法应用梯度下降训练的。目前神经网络有两大主要类型,它们都是前馈神经网络:卷积神经网络(CNN)和循环神经网络(RNN),其中 RNN 又包含长短期记忆(LSTM)、门控循环单元(GRU)等等。深度学习是一种主要应用于神经网络帮助其取得更好结果的技术。尽管神经网络主要用于监督学习,但也有一些为无监督学习设计的变体,比如自动编码器和生成对抗网络(GAN)。

反向传播算法技术

反向传播(英语:Backpropagation,缩写为BP)是“误差反向传播”的简称,是一种与最优化方法(如梯度下降法)结合使用的,用来训练人工神经网络的常见方法。该方法计算对网络中所有权重计算损失函数的梯度。这个梯度会反馈给最优化方法,用来更新权值以最小化损失函数。 在神经网络上执行梯度下降法的主要算法。该算法会先按前向传播方式计算(并缓存)每个节点的输出值,然后再按反向传播遍历图的方式计算损失函数值相对于每个参数的偏导数。

暂无评论
暂无评论~