[关闭]
@Feiteng 2015-07-11T21:40:59.000000Z 字数 6543 阅读 19406

分分钟推导神经网络

Feiteng Email:lifeiteng0422@gmail.com
原文发在[分分钟推导神经网络],但网站不能解析Latex,转到这里。

一、快速矩阵、向量求导

这一节展示对矩阵、向量求导过程使用链式法则、转置、组合等技巧来快速完成求导
一个原则维数相容,这是我发明的词汇,实质是多元微分基本知识:

维数相容就是,如果xRm×n,f(x)R1,那么f(x)xRm×n;
如果xRm×1,f(x)Rn,那么f(x)xRn×m.

举例:

J=(Xwy)T(Xwy)=||Xwy||2,XRm×n,wRn×1,yRm×1,
JXJwJy

解:

对于矩阵、向量求导:

二、神经网络求导

神经网络的训练过程:反向传播求得“各层”参数W和b的导数之后,做梯度下降[一阶的GD、SGD,二阶的LBFGS、共轭梯度]。
反向传播是求导的链式法则和相容原则的完美体现,对每一层的参数求导都利用上一层的中间结果来完成。
这里的标号,参考UFLDL教程
前向传播记号(公式1):

z(l+1)=W(l)a(l)+b(l)a(l+1)=f(z(l+1))

z(l) 为第l层的中间计算结果,a(l) 为第l层的激活值,其中第l+1层包含元素[输入a(l),参数W(l)b(l),激活函数f(),中间结果z(l+1),输出a(l+1)]
设整个MLP的损失函数为J(W,b),根据求导的链式法则有:
W(l)J(W,b)=J(W,b)z(l+1)z(l+1)W(l)=δ(l+1)(a(l))Tb(l)J(W,b)=J(W,b)z(l+1)z(l+1)b(l)=δ(l+1)

这里我们记 J(W,b)z(l+1)=δ(l+1), z(l+1)W(l)=a(l)z(l+1)b(l)=1都可以由公式(1)得出
我们看到 a(l)加了转置符号(a(l))T,根据维数相容原理作出的调整
如何递推求 δ(l)=J(W,b)z(l) ?可使用如下递推[根据维数相容原理作出了调整]
δ(l)=Jz(l)=Jz(l+1)z(l+1)a(l)a(l)z(l)=((W(l))Tδ(l+1))f(z(l))

其中Jz(l+1)=δ(l+1)z(l+1)a(l)=W(l)a(l)z(l)=f(z(l))
那么我们可以从最顶层逐层往下,就可以递推求得每一层的δ(l)=J(W,b)z(l)
注意:a(l)z(l)=f(z(l))是逐维求导,在递推中是“·”乘的形式
反向传播的整个流程如下:
1) 进行前向传播计算,利用前向传播公式,得到隐藏层和输出层 的激活值。
2) 对输出层(第nl层),计算残差:
δ(nl)=J(W,b)z(nl)

3) 对于l=nl1,nl2,...,2的隐藏层,计算:
δ(l)=Jz(l)=Jz(l+1)z(l+1)a(l)a(l)z(l)=((W(l))Tδ(l+1))f(z(l))

4) 计算各层参数W(l)b(l)的偏导数:
W(l)J(W,b)=J(W,b)z(l+1)z(l+1)W(l)=δ(l+1)(a(l))Tb(l)J(W,b)=J(W,b)z(l+1)z(l+1)b(l)=δ(l+1)

三、编程实现的问题

很多DL的opensoure(caffe,Kaldi/src/nnet)实现并不是按照上面的层[W(l)b(l)、激活函数f()]来做的,通常把[W(l)b(l)]作为一个layer、[激活函数f()]作为一个layer(sigmoid,relu,softplus等以及softmax),
各层在BP的时候偏导数的时候,分清楚该层的输入、输出即能正确编程实现,如:

z(l+1)=W(l)a(l)+b(l)(1)a(l+1)=f(z(l+1))(2)

我们可以把(1)式作为一个AffineTransform层[W,b],以下是伪代码:

  1. class AffineTransform
  2. { //a(l)=in(列向量), z(l+1)=out(列向量)
  3. void Forward(const Vector &in, Vector *out)
  4. {
  5. out = W*in + b;
  6. }
  7. void Backward(const Vector &in, const Vector &out, const Vector &out_diff, const Vector *in_diff)
  8. {
  9. in_diff = W^T * out_diff; //(注)
  10. }
  11. void Update(const Vector &out_diff, const Vector& in)
  12. {
  13. W_diff = out_diff * in^T;
  14. b_diff = out_diff;
  15. W = W - learn_rate*W_diff;
  16. b = b - learn_rate*b_diff;
  17. }
  18. private:
  19. Matrix W;
  20. Matrix W_diff;
  21. Vector b;
  22. Vector b_diff;
  23. }

(注) out_diff =Jz(l+1)已经求得,

in_diff=Ja(l)=Jz(l+1)z(l+1)a(l)=WTout_diffW_diff=Jz(l+1)z(l+1)W(l)=out_diffinTb_diff=Jz(l+1)z(l+1)b(l)=out_diff1

我们可以把(2)式作为一个Sigmoid层

  1. class Sigmoid
  2. { //a(l)=in(列向量), z(l+1)=out(列向量)
  3. void Forward(const Vector &in, Vector *out)
  4. {
  5. out = sigmoid(in);//y = 1/(1+e^-x)
  6. }
  7. void Backward(const Vector &in, const Vector &out, const Vector &out_diff, const Vector *in_diff)
  8. {
  9. in_diff = out.*(1-out).*out_diff; //dy = y(1-y)dx
  10. }
  11. void Update(const Vector &out_diff, const Vector& in)=0
  12. private:
  13. }

(注) out_diff =Ja(l+1)已经求得,

in_diff=Jz(l+1)=Ja(l+1)a(l+1)z(l+1)=out_diff.out.(1out)

在实际编程实现时,in、out可能是矩阵(以一行存储一个输入向量,矩阵的行数就是batch_size),那么上面的C++代码就要做出变化(改变前后顺序、转置,把函数参数的Vector换成Matrix,此时Matrix out_diff 每一行就要存储对应一个Vector的diff,在update的时候就要做这个batch的加和,这个加和可以通过矩阵相乘out_diff*input[适当的转置]得到,如果你熟悉SVD分解的过程,逆过来就可以轻松理解这种通过乘积来做加和的技巧)。

四、卷积神经网络卷积层的求导

卷积怎么求导呢?实际上卷积可以通过矩阵乘法来实现[caffe里面是不是有image2col()],也可以使用FFT在频率域做加法,通常后者更快一些。
那么既然通过矩阵乘法,我们上面的秘密武器仍然可以运用,但卷积层求导这块还是要比MLP复杂一些,要做些累加的操作。具体怎么做还要看编程时选择什么样的策略、数据结构。

掌握了理论推导,就会觉得工程经验才是干货!

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注