深度学习中的卷积和反卷积(四)——卷积和反卷积的梯度
本系列已完结,全部文章地址为:
深度学习中的卷积和反卷积(一)——卷积的介绍
深度学习中的卷积和反卷积(二)——反卷积的介绍
深度学习中的卷积和反卷积(三)——卷积和反卷积的计算
深度学习中的卷积和反卷积(四)——卷积和反卷积的梯度
1 卷积的梯度计算
1.1 Tensorflow中矩阵梯度运算的说明
请注意,计算y对x的梯度时,如果y、x都是矩阵,梯度理应是每一个y对每一个x求偏导的结果。但在Tensorflow中,gradient是返回了总和的梯度。如果想求出每个分量的梯度,应该使用Jacobian矩阵。这一点困扰了笔者很久,直到翻到文档才恍然大悟。文档地址:梯度和自动微分简介 | TensorFlow Core
import tensorflow as tf
x = tf.Variable(2.0)
# 求gradient,结果为7
with tf.GradientTape() as tape:
y = x * [3., 4.]
print(tape.gradient(y, x).numpy())
# 求gradient,结果为[3. 4.]
with tf.GradientTape() as tape:
y = x * [3., 4.]
print(tape.jacobian(y, x).numpy())
1.2 卷积对input的梯度
沿用上一篇的例子如下图:
数值例子为:
输入是3*3的维度,因此梯度维度也是3*3,表示对每一个a中的元素求梯度的结果
观察卷积输出的结果,例如,参与了的计算,系数是,因此梯度为。同理,所有的y对所有的输入都可以计算梯度。以为例:
| | |
| | |
| | |
在Tensorflow中验证如下
@tf.function
def compute_gradient(x, filters):
with tf.GradientTape() as tape:
tape.watch(x) # 监视x
y = tf.nn.conv2d(x, filters, [1, 1, 1, 1], "VALID")
return tape.jacobian(y, x) # 计算y相对于x的梯度
print(compute_gradient(x, filters).numpy().reshape([4, 3, 3]))
输出如下,的输出结果与上文中表格一致,其余y分量不再赘述。
[[[1. 2. 0.]
[3. 4. 0.]
[0. 0. 0.]]
[[0. 1. 2.]
[0. 3. 4.]
[0. 0. 0.]]
[[0. 0. 0.]
[1. 2. 0.]
[3. 4. 0.]]
[[0. 0. 0.]
[0. 1. 2.]
[0. 3. 4.]]]
1.3 卷积对kernel的梯度
对卷积核计算的梯度,就是每一个y对每一个k求梯度,例如每一个y对于的梯度,就是下图红框中的部分,分别是1、2、4、5。
还是以为例,在Tensorflow中验证如下
@tf.function
def compute_kernel_gradient(x, filters):
with tf.GradientTape() as tape:
tape.watch(filters) # 监视x
y = tf.nn.conv2d(x, filters, [1, 1, 1, 1], "VALID")
return tape.jacobian(y, filters)
print(compute_kernel_gradient(x, filters).numpy().reshape([4, 2, 2]))
输出如下,符合预期。
[[[1. 2.]
[4. 5.]]
[[2. 3.]
[5. 6.]]
[[4. 5.]
[7. 8.]]
[[5. 6.]
[8. 9.]]]
2 反卷积的梯度计算
由于反卷积的计算相当于对矩阵先做填充再做卷积,因此反卷积的梯度等价于对填充后的输入矩阵做卷积的梯度。
2.1 反卷积对input的梯度
以前文的数据为例,首先对输入矩阵填充0,然后翻转卷积核,得到4*4的输出。
我们计算每一个输出对每一个输入的梯度,输出是4*4,输入是3*3,因此算梯度的Jacobian矩阵维度是4*4*3*3。
我们以对的梯度为例,先看是怎么算出来的
因此对的梯度为
Tensorflow中验证如下:
import numpy as np
import tensorflow as tf
def conv2d_transpose(x, filters):
return tf.nn.conv2d_transpose(x, filters, [1, 4, 4, 1], strides=1, padding="VALID")
@tf.function
def compute_conv2d_transpose_i_gradient(x, filters):
with tf.GradientTape() as tape:
tape.watch(x)
y = conv2d_transpose(x, filters)
return tape.jacobian(y, x)
x = tf.constant(np.arange(1, 10).reshape([1, 3, 3, 1]), dtype=tf.float32)
filters = tf.constant(np.arange(1, 5).reshape(2, 2, 1, 1), dtype=tf.float32)
print(compute_conv2d_transpose_i_gradient(x, filters).numpy().reshape([4, 4, 3, 3])[2][1][1][1]) # 注意下标是从0开始的,[2][1]代表y32,[1][1]代表a22
输出为3,与手算结果一致。
3.0
2.2 反卷积对kernel的梯度
与反卷积对input梯度类似,也等价于对填充后的输入矩阵做卷积时计算梯度。同样用数值例子验证。
计算 对的梯度,结合上节的表达式,得到梯度为
Tensorflow中验证如下:
import numpy as np
import tensorflow as tf
def conv2d_transpose(x, filters):
return tf.nn.conv2d_transpose(x, filters, [1, 4, 4, 1], strides=1, padding="VALID")
@tf.function
def compute_conv2d_transpose_k_gradient(x, filters):
with tf.GradientTape() as tape:
tape.watch(filters)
y = conv2d_transpose(x, filters)
return tape.jacobian(y, filters)
x = tf.constant(np.arange(1, 10).reshape([1, 3, 3, 1]), dtype=tf.float32)
filters = tf.constant(np.arange(1, 5).reshape(2, 2, 1, 1), dtype=tf.float32)
print(compute_conv2d_transpose_k_gradient(x, filters).numpy().reshape([4, 4, 2, 2])[2][1][1][0])
输出为5,与手算结果一致。
5.0
3 反卷积等价于误差反向传播
https://zhuanlan.zhihu.com/p/338780702
下图是Tensorflow中反卷积函数的源码,可以看出反卷积等价于将input作为卷积下层误差反向传播,本节进行推导。
@tf_export("nn.conv2d_transpose", v1=[])
@dispatch.add_dispatch_support
def conv2d_transpose_v2(
input, # pylint: disable=redefined-builtin
filters, # pylint: disable=redefined-builtin
output_shape,
strides,
padding="SAME",
data_format="NHWC",
dilations=None,
name=None):
......
return gen_nn_ops.conv2d_backprop_input(
input_sizes=output_shape,
filter=filters,
out_backprop=input,
strides=strides,
padding=padding,
explicit_paddings=explicit_paddings,
data_format=data_format,
dilations=dilations,
name=name)
3.1 全连接网络的误差反向传播
卷积可视作特殊的全连接网络。全连接网络中每一个输出与每一个输入都使用权重边相连,输出是各输入的加权求和。对于卷积而言,输出只与某些输入有关,但可以理解为所有输出与所有输入相连,只是其中有些权重边固定为0而已。因此,本节先回顾全连接网络的误差反向传播过程,随后推广到卷积的误差反向传播。
如下图所示,我们构造了一个全连接神经网络,忽略偏置。
符号表示如下:
符号 | 含义 |
| L层第i个输入。A指activation,表示L-1层激活后传递给L层的输入 |
| L层第i个输入连接到第j个输出的权重 |
| L层第i个输出 |
| L层偏置 |
损失函数 | |
| 最终的误差对于L层第i个输出的梯度,表示反向传播过来的误差 |
对于L层来说,误差反向传播需要做两件事情:(1)计算误差对本层权重的梯度,从而更新权重;(2)将误差反向传播到上一层,从而更新上层的权重。
3.1.1 误差对本层权重的梯度
根据链式法则,有:
根据的定义,前一项即为。后一项比较简单,因为Z是由W加权求和而来,因此该项等于。在卷积中,卷积核其实就是特殊的权重,因此该项即对应前文讨论的卷积对kernel的梯度。
表示误差传播到这个节点的误差,表示节点对于最后误差负的责任,注意这里的是激活之前的输出。可以继续分解为最终误差对激活后的结果A的梯度乘A对于Z的梯度,如果是最后一层,则代表损失函数的梯度。后者代表激活函数的梯度。
求出梯度后,根据神经网络的学习规则,权重根据学习率、梯度动态更新。
3.1.2 误差反向传播到上一层
本层需要求出,从而使得下一层根据此结果更新权重。以L-1层第j个输出为例
注意乘号后一项就是激活函数的导数,下面分析乘号前一项。注意L层的Aj会参与到多个输出Z,因此需要将所有输出Zi都考虑在内。
此式后一项即为卷积中对input的梯度。可进一步化简,结果为,因此损失函数对L层输入的梯度可表示为与权重相乘后求和。
3.2 卷积的误差反向传播
误差反向传播,对应前文中“误差反向传播到上一层”这一小节。在卷积中,卷积核其实就是特殊的稀疏权重,其中有很多权重为0。
还是以前文卷积计算为例,我们列出损失函数对所有输入的梯度。代入上式,得到:
注意这里符号表示有所调整,因为全连接网络是把输入和输出展开的,卷积这里输入和输出是二维的,因此下标用二维坐标表示。同时此处只讨论第L层网络情况,不再保留上标。
可以看到由于a22参与了所有的输出,所以表达式有4项,其他输入只参与了1或2项输出,相当于对剩余输出的权重为0,因此表达式只有1、2项。
这种计算结果等价于对下式求卷积:
可以发现,这正好是反卷积的计算过程。
3.3 Tensorflow的验证
在Tensorflow中验证如下:
def conv2d_transpose(x, filters):
return tf.nn.conv2d_transpose(x, filters, [1, 4, 4, 1], strides=1, padding="VALID")
x = tf.constant(np.arange(1, 10).reshape([1, 3, 3, 1]), dtype=tf.float32)
filters = tf.constant(np.arange(1, 5).reshape(2, 2, 1, 1), dtype=tf.float32)
print("反卷积结果:")
print(conv2d_transpose(x, filters).numpy().reshape([4, 4]))
# 卷积反向传播
x = tf.constant(np.array([[0, 0, 0, 0, 0],
[0, 1, 2, 3, 0],
[0, 4, 5, 6, 0],
[0, 7, 8, 9, 0],
[0, 0, 0, 0, 0]]).reshape([1, 5, 5, 1]), dtype=tf.float32)
filters = tf.constant(np.array([[4, 3],
[2, 1]]).reshape(2, 2, 1, 1), dtype=tf.float32)
print("卷积反向传播结果:")
print(tf.nn.conv2d(x, filters, [1, 1, 1, 1], "VALID").numpy().reshape(4, 4))
输出如下图所示,二者一致。
反卷积结果:
[[ 1. 4. 7. 6.]
[ 7. 23. 33. 24.]
[19. 53. 63. 42.]
[21. 52. 59. 36.]]
卷积反向传播结果:
[[ 1. 4. 7. 6.]
[ 7. 23. 33. 24.]
[19. 53. 63. 42.]
[21. 52. 59. 36.]]
参考资料
《卷积神经网络(CNN)反向传播算法详细解析》
《反向传播算法中的权重更新是如何进行的?》