《神经网络的梯度推导与代码验证》之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)

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

 with tf.GradientTape(persistent=True) as t:
# -----hidden l1-------------
# DNN layer1
l1 = tf.keras.layers.Dense(4) # 输入经过DNN layer1得到输出z_l1
z_l1 = l1(x)
# 跟踪变量z_l1,用于随后计算其梯度
t.watch([z_l1])
# DNN layer1的输出再经过激活函数sigmoid
a_l1 = tf.math.sigmoid(z_l1)
# 跟踪变量a_l1 ,用于随后计算其梯度
t.watch([a_l1])
# ------hidden l2------------
l2 = tf.keras.layers.Dense(3) z_l2 = l2(a_l1)
t.watch([z_l2])
a_l2 = tf.math.sigmoid(z_l2)
t.watch([a_l2])
# -------计算loss-----------
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行),方便计算梯度的时候好做分解。

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

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

 # 注意的是,在tensorflow里,W变量矩阵和数学推导的是互为转置的关系,所以在验证的时候,要注意转置关系的处理
# ------dl_da2------ sigmoid(x)的导数 = sigmoid(x) * (1-sigmoid(x))
dl_da2 = t.gradient(loss, a_l2)
my_dl_da2 = (a_l2 - y_true)
# ------dl_dz2---------
dl_dz2 = t.gradient(loss, z_l2)
my_dl_dz2 = my_dl_da2 * d_sigmoid(z_l2)
# -------dl_dW2--------
dl_dW2 = t.gradient(loss, l2.kernel)
my_dl_W2 = np.matmul(a_l1.numpy().transpose(), my_dl_dz2)
# -------dl_db2--------
dl_db2 = t.gradient(loss, l2.bias)
my_dl_db2 = my_dl_dz2
# -------dl_dz1---------
dl_dz1 = t.gradient(loss, z_l1)
my_dl_dz1 = np.matmul(my_dl_dz2, l2.weights[0].numpy().transpose()) * d_sigmoid(z_l1)
# -------dl_dW1---------
dl_dW1 = t.gradient(loss, l1.kernel)
my_dl_dW1 = np.matmul(x.transpose(), my_dl_dz1)
# -------dl_db1----------
dl_db1 = t.gradient(loss, l1.bias)
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}$这种方式做前传计算的,即输入x是一个行向量而非列向量

而我们在数学推导上,习惯写成$\boldsymbol{o}\boldsymbol{u}\boldsymbol{t}\boldsymbol{p}\boldsymbol{u}\boldsymbol{t} = \boldsymbol{W}^{\boldsymbol{T}}\boldsymbol{x}\boldsymbol{~} + \boldsymbol{~}\boldsymbol{b}$。于是在做验证的时候,需要对weights进行一下转置操作,即上面的 .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 vs 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)的前向传播和反向梯度推导中的得到的公式自行验证。


(欢迎转载,转载请注明出处。欢迎留言或沟通交流: lxwalyw@gmail.com)

《神经网络的梯度推导与代码验证》之FNN(DNN)前向和反向过程的代码验证的更多相关文章

  1. 《神经网络的梯度推导与代码验证》之FNN(DNN)的前向传播和反向推导

    在<神经网络的梯度推导与代码验证>之数学基础篇:矩阵微分与求导中,我们总结了一些用于推导神经网络反向梯度求导的重要的数学技巧.此外,通过一个简单的demo,我们初步了解了使用矩阵求导来批量 ...

  2. 《神经网络的梯度推导与代码验证》之CNN的前向传播和反向梯度推导

    在FNN(DNN)的前向传播,反向梯度推导以及代码验证中,我们不仅总结了FNN(DNN)这种神经网络结构的前向传播和反向梯度求导公式,还通过tensorflow的自动求微分工具验证了其准确性.在本篇章 ...

  3. 《神经网络的梯度推导与代码验证》之CNN前向和反向传播过程的代码验证

    在<神经网络的梯度推导与代码验证>之CNN的前向传播和反向梯度推导 中,我们学习了CNN的前向传播和反向梯度求导,但知识仍停留在纸面.本篇章将基于深度学习框架tensorflow验证我们所 ...

  4. 《神经网络的梯度推导与代码验证》之vanilla RNN的前向传播和反向梯度推导

    在本篇章,我们将专门针对vanilla RNN,也就是所谓的原始RNN这种网络结构进行前向传播介绍和反向梯度推导.更多相关内容请见<神经网络的梯度推导与代码验证>系列介绍. 注意: 本系列 ...

  5. 《神经网络的梯度推导与代码验证》之vanilla RNN前向和反向传播的代码验证

    在<神经网络的梯度推导与代码验证>之vanilla RNN的前向传播和反向梯度推导中,我们学习了vanilla RNN的前向传播和反向梯度求导,但知识仍停留在纸面.本篇章将基于深度学习框架 ...

  6. 《神经网络的梯度推导与代码验证》之LSTM的前向传播和反向梯度推导

    前言 在本篇章,我们将专门针对LSTM这种网络结构进行前向传播介绍和反向梯度推导. 关于LSTM的梯度推导,这一块确实挺不好掌握,原因有: 一些经典的deep learning 教程,例如花书缺乏相关 ...

  7. 神经网络的BP推导过程

    神经网络的BP推导过程 下面我们从一个简单的例子入手考虑如何从数学上计算代价函数的梯度,考虑如下简单的神经网络,该神经网络有三层神经元,对应的两个权重矩阵,为了计算梯度我们只需要计算两个偏导数即可: ...

  8. [DeeplearningAI笔记]改善深层神经网络1.1_1.3深度学习使用层面_偏差/方差/欠拟合/过拟合/训练集/验证集/测试集

    觉得有用的话,欢迎一起讨论相互学习~Follow Me 1.1 训练/开发/测试集 对于一个数据集而言,可以将一个数据集分为三个部分,一部分作为训练集,一部分作为简单交叉验证集(dev)有时候也成为验 ...

  9. 再说表单验证,在Web Api中使用ModelState进行接口参数验证

    写在前面 上篇文章中说到了表单验证的问题,然后尝试了一下用扩展方法实现链式编程,评论区大家讨论的非常激烈也推荐了一些很强大的验证插件.其中一位园友提到了说可以使用MVC的ModelState,因为之前 ...

随机推荐

  1. Hadoop学习之NCDC天气数据获取

    期望目的 下载<Hadoop权威教程>里用到的NCDC天气数据,供后续在此数据基础上跑mapred程序. 操作过程 步骤一.编写简单的shell脚本,下载数据文件到本地文件系统 已知NCD ...

  2. JDK8的LocalDateTime用法

    参考资料:好好学Java  https://mp.weixin.qq.com/s/Dd_7yUh3lq3TqE2cjsYXvw JDK8新特性里提供了3个时间类:LocalDate.LocalTime ...

  3. Gradient Centralization: 简单的梯度中心化,一行代码加速训练并提升泛化能力 | ECCV 2020 Oral

    梯度中心化GC对权值梯度进行零均值化,能够使得网络的训练更加稳定,并且能提高网络的泛化能力,算法思路简单,论文的理论分析十分充分,能够很好地解释GC的作用原理   来源:晓飞的算法工程笔记 公众号 论 ...

  4. 【NOI2016】区间 题解(线段树+尺取法)

    题目链接 题目大意:给定$n$个区间$[l_i,r_i]$,选出$m$个区间使它们有一个共同的位置$x$,且使它们产生的费用最小.求最小费用.费用定义为最长的区间长度减去最短区间长度. ------- ...

  5. python 把多个list合并为dataframe并输出到csv文件

    import pandas as pd a = [1,2,3] b = ['a','b','c'] test = pd.DataFrame({'a_list':a,'b_list':b}) 将两个列表 ...

  6. 说说Spring中的 @RestController 和 @Controller

    Spring MVC执行流程已是JAVA面试中老生常谈的问题,相信各位小伙伴也是信手拈来.今天我们来谈谈另一个面试中必会必知的问题: @RestController和@Controller的区别? S ...

  7. 19、State 状态模式

    “人有悲欢离合,月有阴晴圆缺”,包括人在内,很多事物都具有多种状态,而且在不同状态下会具有不同的行为,这些状态在特定条件下还将发生相互转换.就像水,它可以凝固成冰,也可以受热蒸发后变成水蒸汽,水可以流 ...

  8. 7、Java 循环结构

    本章讲解一下Java中常见的三种循环结构,顺序结构的程序语句只能 被执行一次.使用循环可以解决我们多个常量或者变量的同一类的操作或者更加复杂的操作. 循环 循环结构有三大类: 1.for循环:确定循环 ...

  9. 网络协议: TCP/IP 和UDP/IP

    网络协议: TCP/IP 和UDP/IP TCP/IP TCP/IP(Transmission Control Protocol/Internet Protocol)是一种可靠的网络数据传输控制协议. ...

  10. Compilation failed (return status=1): g++.exe: error: CreateProcess: No such file or directory错误

    windows10上 theano安装之后出现的问题 >>> import theano You can find the C code in this temporary file ...