本文正在参加「Python主题月」,详情查看活动链接
上周分享关于卷积神经网的实现,不过只是实现前向传播,虽然卷积神经网络看似要复杂一些,但是实现起来可能没有想象那么难,其实难的东西都在今天内容里,我尝试去给大家解释,也尽量将一个推导公式给大家详细列出来。
![009.jpeg](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/2a7f171efd1d4f6890fe001d610afd27~tplv-k3u1fbpfcp-watermark.awebp)
训练模型
重点是训练环节,也就是在训练环节如何计算梯度和然后用梯度来更新参数,在训练过程中,通常包括 2 个阶段,前向传播和后项传播
- 前向传播阶段: 输入数据经神经网网络一层一层向前传递,这个过程就是前向传播
- 后向传播阶段: 在整个网络反向逐层更新梯度
在训练 CNN 过程中也包含前向传播和反向传播两个阶段,以及如何具体将其实现
- 在前向传播过程中,会将数据(例如输入数据和中间变量)缓存起来起来,以备在反向传播过程使用。就意味着每个反向传播一定会存储与其对应的前向传播
- 在反向传播过程中,神经网络每一层都接受一个梯度,计算后返回一个梯度。这里
∂out∂L 来表示接受的梯度而用
∂in∂l 表示返回的梯度
基于以上 2 个思路来实现代码,可以保证代码整洁和层次感,大多时候我们会说先思考然后再去 coding,不过想象这是对一些已经积累一些经验程序员而言,如果对于经验不多人我们还是先动手然后再去。所以我们的 CNN 代码看起来应该是类似下面代码的样子
gradient = np.zeros(10)
gradient = softmax.backprop(gradient)
gradient = pool.backprop(gradient)
gradient = conv.backprop(gradient)
复制代码
链式法则: 例如对
f[g(h(x))] 求导可以先对
g[h(x)] 求导得到
g′[h(x)]h′(x) 在得到
f[g(h(x))]=f′[g(h(x))]g′[h(x)]h′(x) 也就是 \frac{dy}{dx} = \frac{dy}{du} \frac{du}{dv} \frac{dv}{dx}
反向传播: Softmax
与前向传播相反,当前向传播完成后,就开始反向传播组成传递梯度,接下来我们就来看看在反向求导是如何进行的。首先来看的 cross-entropy loss(交叉熵损失函数)
L=−ln(pc)
公式里的
pc 是模型对于数据属于正确的类别 c (标注类别),给出预测概率值。首先来计算输入到 Softmax 层的反向传播,也就是
∂outs(i)∂L→0ifi=c−pi1ifi=c
这里 c 表示该样本图片属于类别,所以交叉熵计算损失函数只会考虑在正确类别上模型给出概率值,所以其他类别不会考虑
在 softmax 的前向传播(forward) 需要对 3 个变量进行缓存,分别是
input
是未展平前的形状
input
经过展平后
totals
表示传入到 softmax 激活函数前的值
在前向传播做好准备后,我们就可以开始反向传播。因为在交叉熵损失函数仅对真实标签所对应的,
,
首先,计算
outs(c) 的梯度,,
ti 表示所有类别 i ,这样便可以将
outs(c) 表示为下面式子
outs(c)=∑ietietc=SetcS=i∑eti
首先考虑类别 k 满足条件
k=c 类别的
outs(c)=etcS−1
∂tk∂outs(c)=∂S∂outs(c)(∂tk∂S)−etcS−2(∂tk∂S)=−etcS−2(etk)=S2−etcetk
∂tc∂outs(c)=S2Setc−etc∂tc∂S=S2Setc−etcetc=S2etc(S−etc)
如果上面两个公式看起来不算很好理解,我通过一个具体例子给大家一步一步推导
![006.png](https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/674df6f9ac1c4ced9240f467f96fecfc~tplv-k3u1fbpfcp-watermark.awebp)
∂w2,1∂L=∂a1∂L∂z1∂a1∂w2,1∂z1+∂a2∂L∂z1∂a2∂w2,1∂z1
这里我们以更新
w2,1 参数为例,看一看首先我们看这个权重一共有几条路径可以到底损失函数,这里有 2 条路径,也就是
w1,2 对损失值影响一共分为两个部分,然后将路径结点中变量求偏导一一列出分别是
∂a1∂L 、
z1∂a1 等等一一列出整理出上面公式,然后我们这些偏导一一求解再对号入座
∂a1∂L=∂a1∂[j∑h−yiln(aj)]∂a1∂[j∑h−y1ln(a1)]a1y
∂z1∂a1=z1∂[∑jnezjez1]
这个求导看似负责,我们用
f(x)=eez1
g(x)=∑j=1nezj
g(x)f(x)=[g(x)]2g(x)f′(x)−f(x)g′(x)
根据这个公式我们来计算上面偏导
[∑j=1nezj]2∑j=1nezj∂z1∂[ez1]−ez1∂z1∂[∑j=1nezj]=[∑j=1nezj]2[∑j=1nezj]ez1−ez1ez1]=[∑j=1nezj]2z1([∑j=1nezj]−ez1)=[∑j=1nezj]ez1[∑j=1nezj]∑j=1nezj]a1(1−a1)
因为推导过程比较详细,所以这里就不做过多解释。另一条路线大家自己去尝试一下给出答案是
−a1a2
class Softmax:
def backprop(self, d_L_d_out):
'''
Performs a backward pass of the softmax layer.
Returns the loss gradient for this layer's inputs.
- d_L_d_out is the loss gradient for this layer's outputs.
'''
for i, gradient in enumerate(d_L_d_out):
if gradient == 0:
continue
t_exp = np.exp(self.last_totals)
S = np.sum(t_exp)
d_out_d_t = -t_exp[i] * t_exp / (S ** 2)
d_out_d_t[i] = t_exp[i] * (S - t_exp[i]) / (S ** 2)
复制代码
因为在cross-entropy 在损失函数只考虑模型预测中对应真实标签类别 c 那一个预测值,所以只需要考虑 d_L_d_out
梯度不为 0 就可以。一旦计算梯度
∂t∂outs(i) 分为两种情况
ifk=c∂t∂outs(k)=S2−etcetkifk=c∂t∂outs(k)=S2etc(S−etc)
接下来就是计算权重、偏置和输入的梯度
- 计算权重梯度
∂w∂L 来更新下层的权重
- 计算偏置的梯度
∂b∂L 来更新层的偏置
- 在反向求导中将返回变量梯度作为前一层的梯度输入
复制代码
d_t_d_w = self.last_input
d_t_d_b = 1
d_t_d_inputs = self.weights
d_L_d_t = gradient * d_out_d_t
d_L_d_w = d_t_d_w[np.newaxis].T @ d_L_d_t[np.newaxis]
d_L_d_b = d_L_d_t * d_t_d_b
d_L_d_inputs = d_t_d_inputs @ d_L_d_t
复制代码
要计算权重、偏置和输入对于损失的梯度,下面这个公式可以将这些变量和上面我们计算 t 变量建立关系,t 就是这些变量的函数
t=w∗input+b
∂w∂t=input∂b∂t=1∂input∂t=w
接下来根据链式法则将权重、偏置和输入梯度进行整理
∂w∂L=∂out∂L∂t∂out∂w∂t∂b∂t=∂out∂L∂t∂out∂b∂t∂input∂t=∂out∂L∂t∂out∂input∂t
首先,可以预计算 d_L_d_t
这是因为在计算权重、偏置和输入的梯度时候都会用到。
class Softmax
# ...
def backprop(self, d_L_d_out, learn_rate):
for i, gradient in enumerate(d_L_d_out):
if gradient == 0:
continue
t_exp = np.exp(self.last_totals)
S = np.sum(t_exp)
d_out_d_t = -t_exp[i] * t_exp / (S ** 2)
d_out_d_t[i] = t_exp[i] * (S - t_exp[i]) / (S ** 2)
d_t_d_w = self.last_input
d_t_d_b = 1
d_t_d_inputs = self.weights
d_L_d_t = gradient * d_out_d_t
d_L_d_w = d_t_d_w[np.newaxis].T @ d_L_d_t[np.newaxis]
d_L_d_b = d_L_d_t * d_t_d_b
d_L_d_inputs = d_t_d_inputs @ d_L_d_t
self.weights -= learn_rate * d_L_d_w
self.biases -= learn_rate * d_L_d_b
return d_L_d_inputs.reshape(self.last_input_shape)
复制代码
由于这部比较难,自己可能分享不够透彻,如果感觉有疑问地方还希望大家多多留言以便共同讨论。
近期评论