相关文章:
10 秒文章速览
本文主要论述矩阵计算中,如何对其求导
x@w+b对w求导:
x@w+b对x求导:
前言
本文是对于《手搓神经网络——BP反向传播》一文中高级的 BP 反向传播部分的详细论述
import numpy as np
import tensorflow as tf
输入,权重与偏置
输入,权重与偏置的属性
先来聊聊输入(x)、权重(w)、偏置(b)。无需再废话的是,这三者的类型都是数组numpy.ndarray
输入(x)
输入(x)的形状为(b, n),b表示 batch_size(批大小),n表示输入训练数据长度。下图表示 batch_size 为b的每个训练数据形状为(1, n)的训练集
输入矩阵中,x 上标表示此训练数据在一个批大小中的位置,x 下标表示第b个训练数据的第n个参数
权重(w)
权重(w)的形状为(n, m),n表示输入数据数量,m表示该层输出数据数量(即该层的神经元数量)
在全连接层中,每层的各个神经元相互交织连接。权重矩阵中,w 上标表示的是该层的第 m 个神经元,w 下标表示的是该层第 m 个神经元与第 n 个输入的联系
偏置(b)
偏置(b)的形状为(1, m),m表示该层输出数据数量(即该层的神经元数量),图中的 b1、b2、b3...分别表示神经元1、神经元2、神经元3...的偏置
输入,权重与偏置的计算
关于计算不多赘述,就是 ,在本文中@表示矩阵叉乘,用numpy表示即为x@w+b或者np.dot(x, w)+b,在将其计算结果带入激活函数,得到的即为神经网络层的输出,笔者认为前向运算的本质就是矩阵计算,反向传播的本质就是矩阵求导
矩阵求导
在输出层计算部分之前,单独论述下矩阵求导。主要论述x@w+b 对 w 求导与激活函数对 x@w+b 的求导
x@w+b 对 w 求导
下面有一个x@w+b表达式,嚯!看着挺复杂的,实则是笔者这里写的详细了些,与其说复杂倒不如是繁琐
接下来,加一点魔法🪄,让x@w+b叉乘✖一个形状为(m, 1)的全一矩阵(矩阵里全是一①Ⅰ壹),m与权重(w)中m的含义相同。将这个矩阵记作 。哈哈,好像高中写数学题,要化简时经常要用到神奇的1
这样子,得出的结果看着就简洁多了。然后如法炮制,只不过是让一个形状为(1, b)的全一矩阵叉乘✖ ,将这个矩阵记作 。哈哈哈,汗流浃背了吧,兄弟😅,你八成是看傻了,我两成是写傻了
经过一坨又一坨的变换,最终以一个标量的表达式来表示最初的x@w+b( 与 是为了化简成上方的形式而存在的,之所以要化简成上方形式是为了使表达式更简洁,也是为了便于对w求导)
计算 对 的导数
自此长篇大论得出的结论,x@w+b对w的导数表示成矩阵形式为
可以看得出x@w+b 对 w 的导数为 x 矩阵转置的行之和,且重复扩展成原形状(不是哥,你要问我怎么看出来是转置的?熟读并抄写10遍上文,还不晓得?熟读全文并背诵)
上面的话似乎有些难以理解,来点魔法。将形状为(b, m)全一矩阵记作 ,用数学的语言表达如下
的应用实现了两个功能,即上文所述的求行之和与重复扩展
- 求行之和:在矩阵叉乘中,可视为 的行与 的列的点乘求和
- 具体来说, 的结果是一个列向量,其第 i 个元素是 的每一行的元素与 的第 i 列的元素相乘并求和的结果。由于 的每一列全是1,这意味着每一行的所有元素都会被加起来。因此, 的结果实际上是 的每一行的元素之和
- 重复扩展:在矩阵叉乘中,可视为 的行与 的列运算
- 由于 的形状是
(n, b), 的形状是(b, m),这意味着在两个矩阵运算后,得到的的结果为一个形状为(n, m)的矩阵,这与 的形状一致
- 由于 的形状是
最后一句话收尾:
使用 Python 实现
tf.random.set_seed(123) # 批大小 batch_size = 64 # 输入神经元数 inputs_num = 2 # 输出神经元数 outputs_num = 4 x = tf.random.uniform((batch_size, inputs_num)) w = tf.random.uniform((inputs_num, outputs_num)) b = tf.random.uniform((1, outputs_num)) # 这就是那个 α3 α = tf.ones((batch_size, outputs_num)) with tf.GradientTape() as tape: tape.watch(w) y = x @ w + b print('tensorflow x@w+b 对 w 求导') diff = tape.gradient(y, w).numpy() print(diff) print('numpy x@w+b 对 w 求导') x = x.numpy() α = α.numpy() print(x.T @ α) ============================== 输出: tensorflow x@w+b 对 w 求导 [[31.709307 31.709307 31.709307 31.709307] [34.111023 34.111023 34.111023 34.111023]] numpy x@w+b 对 w 求导 [[31.709307 31.709307 31.709307 31.709307] [34.111023 34.111023 34.111023 34.111023]]也可以用以下方式对
w求导# x@w+b 对 w 的导数为 x 矩阵转置的行之和 print(x.T.sum(axis=-1, keepdims=True)) # 重复扩展成原形状 print(x.T.sum(axis=-1, keepdims=True).repeat(repeats=4, axis=1)) ============================== 输出: [[31.709307] [34.111027]] [[31.709307 31.709307 31.709307 31.709307] [34.111027 34.111027 34.111027 34.111027]]
激活函数对 w 的求导
根据链式法则,将激活函数(以sigmoid为例)对 w 进行求导。但在这里可以用 代替 ,因为 的形状与 一致,所以所起的功能也一样
可得结论:
使用 Python 实现
tf.random.set_seed(123) batch_size = 3 inputs_num = 2 outputs_num = 4 x = tf.random.uniform((batch_size, inputs_num)) w = tf.random.uniform((inputs_num, outputs_num)) b = tf.random.uniform((1, outputs_num)) with tf.GradientTape() as tape: tape.watch(w) y = tf.nn.sigmoid(x @ w + b) print('tensorflow') diff = tape.gradient(y, w).numpy() print(diff) print('numpy') x = x.numpy() y = y.numpy() print(x.T @ (y*(1-y))) ============================== 输出: tensorflow sigmoid(x@w+b) 对 w 求导 [[0.17910826 0.2533782 0.19129974 0.17476654] [0.32348415 0.4245056 0.34193107 0.3116489 ]] numpy sigmoid(x@w+b) 对 w 求导 [[0.17910826 0.2533782 0.19129974 0.17476654] [0.32348415 0.4245056 0.34193107 0.3116489 ]]
x@w+b 对 x 求导
loss对w、b求导是为了更新权重(w)、偏置(b),可why这里还要来对x求导。都说BP反向传播,哎,是吧?对x求导就是为了实现“传播”,具体的到误差传播部分在和你逼逼赖赖
回到正题,这个怎么弄,具体的就不多言了。与上面的方法大差不差,不过是照本宣科罢了。写的一坨一坨的都,是吧?反正也没人看。那就直接下结论了
使用 Python 实现
tf.random.set_seed(123) batch_size = 3 inputs_num = 2 outputs_num = 4 x = tf.random.uniform((batch_size, inputs_num)) w = tf.random.uniform((inputs_num, outputs_num)) b = tf.random.uniform((1, outputs_num)) α = tf.ones((batch_size, outputs_num)) with tf.GradientTape() as tape: tape.watch(x) y = x @ w + b print('tensorflow x@w+b 对 x 求导') diff = tape.gradient(y, x).numpy() print(diff) print('numpy x@w+b 对 x 求导') w = w.numpy() α = α.numpy() print(α @ w.T) # print(w.T.sum(axis=0, keepdims=True).repeat(repeats=3, axis=0)) ============================== 输出: tensorflow x@w+b 对 x 求导 [[2.4701815 2.0973797] [2.4701815 2.0973797] [2.4701815 2.0973797]] numpy x@w+b 对 x 求导 [[2.4701815 2.0973797] [2.4701815 2.0973797] [2.4701815 2.0973797]]
激活函数 对 x 求导
这儿也是,直接结论了
使用 Python 实现
tf.random.set_seed(123) batch_size = 3 inputs_num = 2 outputs_num = 4 x = tf.random.uniform((batch_size, inputs_num)) w = tf.random.uniform((inputs_num, outputs_num)) b = tf.random.uniform((1, outputs_num)) with tf.GradientTape() as tape: tape.watch(x) y = tf.nn.sigmoid(x @ w + b) print('tensorflow sigmoid(x@w+b) 对 x 求导') diff = tape.gradient(y, x).numpy() print(diff) print('numpy sigmoid(x@w+b) 对 x 求导') w = w.numpy() y = y.numpy() print((y*(1-y)) @ w.T) ============================== 输出: tensorflow sigmoid(x@w+b) 对 x 求导 [[0.492706 0.41777146 ] [0.4669016 0.3959195 ] [0.35548916 0.3006734]] numpy sigmoid(x@w+b) 对 x 求导 [[0.492706 0.4177715 ] [0.46690163 0.3959195] [0.35548916 0.3006734]]
再回首——高级的 BP 反向传播
既然是对《手搓神经网络——BP反向传播》一文中高级的 BP 反向传播部分的详细论述。那就再回到这一部分,来瞅瞅这里边的计算过程
这里构建的是一个极为简单的神经网络
- 输入层:1个神经元
- 隐藏层:3个神经元
- 输出层:1个神经元
模型图如下
前向计算
👉关于前向计算,这里忽略了激活函数(因为作图时忘了激活函数,不想重作)。但实际上,激活函数还是存在的,在后面的反向求导部分中,仍然会考虑激活函数的计算👈
隐藏层计算
在这里输入(x)的 batch_size 为1,以下分别是隐藏层内的计算过程的可视化图与算式
这里的隐藏层输出的形状实际应为(1, 3),考虑美观这里故画作(3, 1)
输出层计算
以下分别是输出层内的计算过程的可视化图与算式
反向求导
关于反向求导,这里说白了就是计算损失loss对其权重(w)、偏置(b)的导数。但在前文矩阵求导中已经细细讲解过,现在是实际应用的时候了
为了简化后面的式子,这里先提前作规定。 与 的含义相同,只是别名罢了,这样是为了使后期式子的表达更简洁
输出层求导
至此,上面瞎逼乱讲的扯了这么多,再回到最初,来看看 怎么对 求导。先来瞧瞧输出层中的计算过程
上面是输出层中的计算过程,下面是各个计算过程的导数。其实就是运用链式法则啦
将下面的各个求导结果相乘 就是 对 的导数
two 个为什么
- 为什么 与 之间是叉乘 ?
- 因为 所以
- 为什么 与 之间是点乘 ?
- 因为在 与 的计算过程中,不涉及矩阵的叉乘。通俗点说。就是不涉及
@运算符,所以在用链式法则,可当作标量处理
- 因为在 与 的计算过程中,不涉及矩阵的叉乘。通俗点说。就是不涉及
损失 对 求导的数学表达式如下
哈哈哈,不是吧阿sir,这个还需要再讲吗?嗯哼???😛😛😛只要上面看得懂,这里确实没啥好说的了,就是求导的对象不同而已啦(此对象非彼对象😡💢💢)
根据导数的加法计算法则,有 ,所以 对 的导数为1
更新参数
回到《手搓神经网络——BP反向传播》的结论
计算误差对参数(权重w and 偏置b)的导数,根据其导数的正负即可得出参数更新的方向(是加➕ or 是减🗡️)
参数更新的计算方式:参数自身减去
lr乘❎误差对参数的求导结果
得到 的更新计算方式
得到 的更新计算方式
在《手搓神经网络——BP反向传播》- 高级的 BP 反向传播中,故
w2更新方式:w2 -= lr * h.T @ sigmoid.diff(pred) * mse.diff(true, pred)b2更新方式:b2 -= lr * sigmoid.diff(pred) * mse.diff(true, pred)
误差传播
既然是误差反向传播,这里仅仅是更新了输出层的参数。而误差还需要继续向前传播,传至隐藏层,以实现隐藏层的参数更新。上文通篇在讲如何对权重(w)求导,而这里所谓的“传播”其实质是对要求导数的参数的某个计算部分中存在关联的参数求导,利用这一计算结果,可以对神经网络前部分的参数继续求导,以实现参数的更新。诶,😞这么说着都好拗口,只恨肚里没墨水,文笔不行呐!😥
下图是完整的神经网络模型的计算过程,链式法则的精髓是对y求导时,y对x导数为x形成y的各个计算部分的导数的乘积(但在矩阵求导中,“乘积”二字变得不太适用,上文内容已经体现出了)
为了对 求导,根据链式法则,需要先对 、、、 这些计算部分求导。其中, 很特殊啊,因为里头的 与 存在关联,也就是说 是 进行一定运算后的一个计算部分。前面不是有提到“y对x导数为x形成y的各个计算部分的导数”嘛,也正是因此,要对 求导就必须先对 求导
再来理解理解“对要求导数的参数的某个计算部分中存在关联的参数求导”
- 要求导数的参数就是
- 某个计算部分就是
- 存在关联的参数就是
这句话其实就是对应了“y对x导数为x形成y的各个计算部分的导数”。好比在上图中,求各个计算部分的导数,就是为了便于求某一参数的导数
隐藏层求导
对 的求导明显变得复杂了,甚至多了括号。why?因为这可是在矩阵求导,不是标量求导
反正公式在这里了 ,所以下面的式子中, 会出现在最后面,这都与矩阵叉乘的计算方式有关
这个更没必要多说了吧
更新参数
的更新计算方式
的更新计算方式
在《手搓神经网络——BP反向传播》- 高级的 BP 反向传播中,故
w1更新方式:w1 -= lr * x.T @ (sigmoid.diff(h) * ((sigmoid.diff(pred) * mse.diff(true, pred)) @ w2.T))b1更新方式:b1 -= lr * (sigmoid.diff(h) * ((sigmoid.diff(pred) * mse.diff(true, pred)) @ w2.T))
In the End
写的又长又臭的,欸哟喂,我的天呐!!!