《神经网络的梯度推导与代码验证》之FNN(DNN)的前向传播和反向梯度推导中,我们学习了FNN(DNN)的前向传播和反向梯度求导,但知识仍停留在纸面。本篇章将基于深度学习框架tensorflow验证我们所得结论的准确性,以便将抽象的数学符号和实际数据结合起来,将知识固化。更多相关内容请见《神经网络的梯度推导与代码验证》系列介绍

 


需要用到的库有tensorflow和numpy,其中tensorflow其实版本>=2.0.0就行

import tensorflow as tf
import numpy as np

然后是定义下面两个要用到的函数,一个是计算mse,另外一个是计算sigmoid的导数:

# mse
def get_mse(y_pred, y_true):
    return 0.5 * tf.reduce_sum((y_pred - y_true)**2)


# sigmoid的导数
def d_sigmoid(x):
    """
    sigmoid(x)的导数 = sigmoid(x) * (1-sigmoid(x))
    :param x:
    :return: sigmoid(x)的导数
    """
    return tf.math.sigmoid(x) * (1 - tf.math.sigmoid(x))

接着是随便产生一条样本数据:

x = np.array([[1, 2]]).astype(np.float32)
y_true = np.array([[0.3, 0.5, 0.2]]).astype(np.float32)

x的是2维的,输出y是3维的

x.shape
Out[5]: (1, 2)
y_true.shape
Out[6]: (1, 3)

有了一条样本之后,我们开始写前向传播的代码:

 1 with tf.GradientTape(persistent=True) as t:
 2     # -----hidden l1-------------
 3     # DNN layer1
 4     l1 = tf.keras.layers.Dense(4)
 5
 6     # 输入经过DNN layer1得到输出z_l1
 7     z_l1 = l1(x)
 8     # 跟踪变量z_l1,用于随后计算其梯度
 9     t.watch([z_l1])
10     # DNN layer1的输出再经过激活函数sigmoid
11     a_l1 = tf.math.sigmoid(z_l1)
12     # 跟踪变量a_l1 ,用于随后计算其梯度 
13     t.watch([a_l1])
14     # ------hidden l2------------
15     l2 = tf.keras.layers.Dense(3)
16
17     z_l2 = l2(a_l1)
18     t.watch([z_l2])
19     a_l2 = tf.math.sigmoid(z_l2)
20     t.watch([a_l2])
21     # -------计算loss-----------
22     loss = get_mse(a_l2, y_true)

上面是一个两层的FNN(DNN)网络,激活函数都是sigmoid

这里 tf.GradientTape(persistent=True) ,t.watch()是用于后面计算变量的导数用的,不太熟悉的可参考tensorflow官方给出的关于这部分的教程(自动微分)

这里为方便起见我就直接用tf.keras.layers.Dense()来创建DNN层了,tensorflow官方的教程也推荐用这种方法快速定义layer。

如果要看某一层内部的weights和bias也比较容易

l1.kernel
Out[7]:
<tf.Variable 'dense/kernel:0' shape=(2, 4) dtype=float32, numpy=
array([[-0.96988726, -0.84827805,  0.312042  ,  0.8871379 ],
       [ 0.6567688 , -0.29099226, -0.80029106, -0.15143871]],
      dtype=float32)>
l1.bias
Out[8]: <tf.Variable 'dense/bias:0' shape=(4,) dtype=float32, numpy=array([0., 0., 0., 0.], dtype=float32)>

----------前向传播的验证----------

下面来验证上面代码第7+11行代码是否符合DNN的前传规则:

tf.math.sigmoid(tf.matmul(x, l1.kernel) + l1.bias)
Out[14]: <tf.Tensor: shape=(1, 4), dtype=float32, numpy=array([[0.585077  , 0.19305778, 0.2161    , 0.64204717]], dtype=float32)>
a_l1
Out[15]: <tf.Tensor: shape=(1, 4), dtype=float32, numpy=array([[0.585077  , 0.19305778, 0.2161    , 0.64204717]], dtype=float32)>

 看来tf.keras.layers.Dense确实是实现了下面的计算公式:

$\left\lbrack \begin{array}{l} \begin{array}{l} a_{1}^{2} \\ a_{2}^{2} \\ \end{array} \\ a_{3}^{2} \\ a_{4}^{2} \\ \end{array} \right\rbrack = \sigma\left( {\left\lbrack \begin{array}{lll} \begin{array}{l} w_{11}^{2} \\ w_{21}^{2} \\ \end{array} & \begin{array}{l} w_{12}^{2} \\ w_{22}^{2} \\ \end{array} & \begin{array}{l} w_{13}^{2} \\ w_{23}^{2} \\ \end{array} \\ w_{31}^{2} & w_{32}^{2} & w_{33}^{2} \\ w_{41}^{2} & w_{42}^{2} & w_{43}^{2} \\ \end{array} \right\rbrack\left\lbrack \begin{array}{l} x_{1} \\ x_{2} \\ x_{3} \\ \end{array} \right\rbrack + \left\lbrack \begin{array}{l} \begin{array}{l} b_{1}^{2} \\ b_{2}^{2} \\ \end{array} \\ b_{3}^{2} \\ b_{4}^{2} \\ \end{array} \right\rbrack} \right)$

这里l1层激活函数默认是linear,sigmoid激活函数被我单独拿了出来(见前传部分的代码第11行),方便计算梯度的时候好做分解。

----------反向梯度计算的验证----------

接下来就是验证反向梯度求导公式的时候了:

 1 # 注意的是,在tensorflow里,W变量矩阵和数学推导的是互为转置的关系,所以在验证的时候,要注意转置关系的处理
 2 # ------dl_da2------ sigmoid(x)的导数 = sigmoid(x) * (1-sigmoid(x))
 3 dl_da2 = t.gradient(loss, a_l2)
 4 my_dl_da2 = (a_l2 - y_true)
 5 # ------dl_dz2---------
 6 dl_dz2 = t.gradient(loss, z_l2)
 7 my_dl_dz2 = my_dl_da2 * d_sigmoid(z_l2)
 8 # -------dl_dW2--------
 9 dl_dW2 = t.gradient(loss, l2.kernel)
10 my_dl_W2 = np.matmul(a_l1.numpy().transpose(), my_dl_dz2)
11 # -------dl_db2--------
12 dl_db2 = t.gradient(loss, l2.bias)
13 my_dl_db2 = my_dl_dz2
14 # -------dl_dz1---------
15 dl_dz1 = t.gradient(loss, z_l1)
16 my_dl_dz1 = np.matmul(my_dl_dz2, l2.weights[0].numpy().transpose()) * d_sigmoid(z_l1)
17 # -------dl_dW1---------
18 dl_dW1 = t.gradient(loss, l1.kernel)
19 my_dl_dW1 = np.matmul(x.transpose(), my_dl_dz1)
20 # -------dl_db1----------
21 dl_db1 = t.gradient(loss, l1.bias)
22 my_dl_db1 = my_dl_dz1

上面反向梯度计算的对象的顺序跟前先传播的顺序是正好相反的,因为这样方便进行梯度计算的时候,靠前的层的参数的梯度能够用得到靠后的层的梯度计算结果而不必从头开始计算,这也是反向梯度传播名字的由来,这点在上面代码中也能够体现出来。

注意:在tensorflow里,W变量矩阵和数学推导的是互为转置的关系,所以在验证的时候,要注意转置关系的处理。举个例子,l1.kernel的shape是(2, 4),即(input_dim, output_dim)这样的格式,说明在tensorflow是按照$\boldsymbol{o}\boldsymbol{u}\boldsymbol{t}\boldsymbol{p}\boldsymbol{u}\boldsymbol{t} = \boldsymbol{x}^{\boldsymbol{T}}\boldsymbol{W}\boldsymbol{~} + \boldsymbol{~}\boldsymbol{b}$这种方式做前传计算的,即输入$\boldsymbol{x}^{\boldsymbol{T}}$是一个行向量而非列向量

而我们在数学推导上,习惯写成$\boldsymbol{o}\boldsymbol{u}\boldsymbol{t}\boldsymbol{p}\boldsymbol{u}\boldsymbol{t} = \boldsymbol{W}^{\boldsymbol{T}}\boldsymbol{x}\boldsymbol{~} + \boldsymbol{~}\boldsymbol{b}$,其中$\boldsymbol{x}$默认是列向量而非行向量。于是在做验证的时候,需要对一些变量进行一下转置操作,即上面的 .transpose()操作,但大体并不影响公式的验证。

 

回到上面的代码部分。dl_da2 = t.gradient(loss, a_l2)表示用tensorflow微分工具求得的$\frac{\partial l}{\partial a2}$;而带my_前缀的则是根据《神经网络的梯度推导与代码验证》之FNN(DNN)的前向传播和反向梯度推导中得到的结论手动计算出来的结果。我们依次对比一下是否有区别。

关于$\frac{\partial l}{\partial a2}$,根据我们得到的公式,它满足:

$\frac{\partial l}{\partial\boldsymbol{a}^{\boldsymbol{L}}} = \boldsymbol{a}^{\boldsymbol{L}} - \boldsymbol{y}$

代码验证的结果是:

t.gradient(loss, a_l2)
Out[17]: <tf.Tensor: shape=(1, 3), dtype=float32, numpy=array([[ 0.1497804 , -0.05124322,  0.23775901]], dtype=float32)>
a_l2 - y_true
Out[18]: <tf.Tensor: shape=(1, 3), dtype=float32, numpy=array([[ 0.1497804 , -0.05124322,  0.23775901]], dtype=float32)>

没有问题,下一个是$\frac{\partial l}{\partial z2}$,根据公式,它满足:

$d\boldsymbol{a}^{\boldsymbol{L}} = d\sigma\left( \boldsymbol{z}^{L} \right) = \sigma^{'}\left( \boldsymbol{z}^{L} \right) \odot d\boldsymbol{z}^{L} = diag\left( {\sigma^{'}\left( \boldsymbol{z}^{L} \right)} \right)d\boldsymbol{z}^{L}$

代码验证的结果是:

t.gradient(loss, z_l2)
Out[21]: <tf.Tensor: shape=(1, 3), dtype=float32, numpy=array([[ 0.03706735, -0.01267625,  0.05851868]], dtype=float32)>
my_dl_da2 * d_sigmoid(z_l2)
Out[22]: <tf.Tensor: shape=(1, 3), dtype=float32, numpy=array([[ 0.03706735, -0.01267625,  0.05851869]], dtype=float32)>

也没有问题,其中d_sigmoid()函数是前面定义好的,用来求sigmoid导数的。至于上面0.05851868 v.s. 0.05851869的问题,我觉得单纯只是两种代码实现过程中调用的底层方式不同导致的不同而已。

接下来是$\frac{\partial l}{\partial W2}$,根据公式,它满足:

$\frac{\partial l}{\partial\boldsymbol{W}^{\boldsymbol{L}}} = \frac{\partial l}{\partial\boldsymbol{z}^{\boldsymbol{L}}}\left( \boldsymbol{a}^{\boldsymbol{L} - 1} \right)^{T}$

代码验证的结果是:

t.gradient(loss, l2.kernel)
Out[23]:
<tf.Tensor: shape=(4, 3), dtype=float32, numpy=
array([[ 0.02168725, -0.00741658,  0.03423793],
       [ 0.00715614, -0.00244725,  0.01129749],
       [ 0.00801025, -0.00273934,  0.01264589],
       [ 0.02379899, -0.00813875,  0.03757175]], dtype=float32)>
np.matmul(a_l1.numpy().transpose(), my_dl_dz2)
Out[24]:
array([[ 0.02168725, -0.00741658,  0.03423794],
       [ 0.00715614, -0.00244725,  0.01129749],
       [ 0.00801025, -0.00273934,  0.01264589],
       [ 0.02379899, -0.00813875,  0.03757175]], dtype=float32)

也没有问题。剩下的大家可自行对照着《神经网络的梯度推导与代码验证》之FNN(DNN)的前向传播和反向梯度推导中的得到的公式自行验证。

 


(欢迎转载,转载请注明出处。欢迎留言或沟通交流: [email protected]

09-03 10:03