基于时间的反向传播算法BPTT(Backpropagation through time)

来源:互联网 发布:研如寓 知乎 编辑:程序博客网 时间:2024/05/16 08:56

本文是读“Recurrent Neural Networks Tutorial, Part 3 – Backpropagation Through Time and Vanishing Gradients”的读书笔记,加入了自己的一些理解,有兴趣可以直接阅读原文。

1. 算法介绍

这里引用原文中的网络结构图
RNN
其中x为输入,s为隐藏层状态,o为输出,按时间展开
这里写图片描述
为了与文献中的表示一致,我们用y^来代替o,则

st=tanh(Uxt+Wst1)y^=softmat(Vst)

使用交叉熵(cross entropy)作为损失函数
Et(y,y^)=ytlogy^E(y,y^)=tEt(yt,y^t)=tytlogy^

我们使用链式法则来计算后向传播时的梯度,以网络的输出E3为例,
y^3=ez3ieziE3=y3logy^3=y3(z3logiezi)z3=Vs3s3=tanh(Ux3+Ws2)

因此可以求V的梯度
E3V=E3z^3z3V=y3(y^31)s3

这里求导时将y^3带入消去了,求导更直观,这里给出的是标量形式,改成向量形式应该是y^13,也就是输出概率矩阵中,对应结果的那个概率-1,其他不变,而输入y恰好可以认为是对应结果的概率是1,其他是0,因此原文中写作
E3V=(y^3y3)s3

相对V的梯度,因为st是W,U的函数,而且含有的st1在 求导时,不能简单的认为是一个常数,因此在求导时,如果不加限制,需要对从t到0的所有状态进行回溯,在实际中一般按照场景和精度要求进行截断。
E3W=E3z^3z3s3s3skskW

其中s3对W的求导是一个分部求导
stW=(1st)2(st1+Wst1sW)

U的梯度类似
stU=(1st)2(xt+Wst1sU)

2. 代码分析

首先我们给出作者自己实现的完整的BPTT,再各部分分析

def bptt(self, x, y):    T = len(y)    # Perform forward propagation    o, s = self.forward_propagation(x)    # We accumulate the gradients in these variables    dLdU = np.zeros(self.U.shape)    dLdV = np.zeros(self.V.shape)    dLdW = np.zeros(self.W.shape)    delta_o = o    delta_o[np.arange(len(y)), y] -= 1.    # For each output backwards...    for t in np.arange(T)[::-1]:        dLdV += np.outer(delta_o[t], s[t].T)        # Initial delta calculation: dL/dz        delta_t = self.V.T.dot(delta_o[t]) * (1 - (s[t] ** 2))        # Backpropagation through time (for at most self.bptt_truncate steps)        for bptt_step in np.arange(max(0, t-self.bptt_truncate), t+1)[::-1]:            # print "Backpropagation step t=%d bptt step=%d " % (t, bptt_step)            # Add to gradients at each previous step            dLdW += np.outer(delta_t, s[bptt_step-1])                          dLdU[:,x[bptt_step]] += delta_t            # Update delta for next step dL/dz at t-1            delta_t = self.W.T.dot(delta_t) * (1 - s[bptt_step-1] ** 2)    return [dLdU, dLdV, dLdW]

2.1. 初始化

结合完整的代码,我们可知梯度的维度

#100*8000dLdU = np.zeros(self.U.shape)#8000*100dLdV = np.zeros(self.V.shape)#100*100dLdW = np.zeros(self.W.shape)

2.2. 公共部分

对照上面的理论可知,无论是V,还是U,W,都有E3z^3,这部分可以预先计算出来,也就是代码中的delta_o

#o是forward的输出,T(句子的实际长度)*8000维,每一行是8000维的,就是词表中所有词作为输入x中每一个词的后一个词的概率delta_o = o#[]中是索引操作,对y中的词对应的索引的概率-1delta_o[np.arange(len(y)), y] -= 1.

2.3. V的梯度

s[t].T是取s[t]的转置,numpy.outer是将第一个参数和第二个参数中的所有元素分别按行展开,然后拿第一个参数中的数因此乘以第二个参数的每一行,例如a=[a0,a1,...,aM], b=[b0,b1,...,bN],则相乘后变成

[[a0b0a0b1...a0bN][a1b0a1b1...a1bN]...[aMb0aMb1...aMbN]]

结果是M*N维的

#delta_o1*8000维向量,s[t]是1*100的向量,转不转置对outer并没有什么区别,其实和delta_o[t].T * s[t]等价,*是矩阵相乘,结果是8000*100维的矩阵dLdV += np.outer(delta_o[t], s[t].T)

2.4. W和U的梯度

对比W和U的梯度公式,我们可以看到,两者+号的第二部分前面的系数是一样的,也就是(1st)2W,这部分可以存起来减少计算量,也就是代码中的delta_t

delta_t = self.V.T.dot(delta_o[t]) * (1 - (s[t] ** 2)) # Backpropagation through time (for at most self.bptt_truncate steps)#截断for bptt_step in np.arange(max(0, t-self.bptt_truncate), t+1)[::-1]:    # print "Backpropagation step t=%d bptt step=%d " % (t, bptt_step)    # Add to gradients at each previous step    #计算+号的第一部分,第二部分本次还没得到,下次累加进来    dLdW += np.outer(delta_t, s[bptt_step-1])    #x为单词的位置向量,与delta_t相乘相当于dLdU按x取索引(对应的词向量)直接与delta_t相加                                      dLdU[:,x[bptt_step]] += delta_t    # Update delta for next step dL/dz at t-1     #更新第二部分系数    delta_t = self.W.T.dot(delta_t) * (1 - s[bptt_step-1] ** 2)