第10课:深度学习之长短时记忆网络(LSTM)

长短时记忆网络是啥


原始RNN的隐藏层只有一个状态,即h,它对于短期的输入非常敏感。那么,假如我们再增加一个状态,即c,让它来保存长期的状态,那么问题不就解决了么?如下图所示:


1.png


新增加的状态c,称为单元状态(cell state)。我们把上图按照时间维度展开:


2.png


LSTM的关键,就是怎样控制长期状态c。在这里,LSTM的思路是使用三个控制开关。第一个开关,负责控制继续保存长期状态c;第二个开关,负责控制把即时状态输入到长期状态c;第三个开关,负责控制是否把长期状态c作为当前的LSTM的输出。三个开关的作用如下图所示:


3.png


长短时记忆网络的前向计算


4.png


长短时记忆网络的训练


熟悉我们这个系列文章的同学都清楚,训练部分往往比前向计算部分复杂多了。LSTM的前向计算都这么复杂,那么,可想而知,它的训练算法一定是非常非常复杂的。现在只有做几次深呼吸,再一头扎进公式海洋吧。

LSTM训练算法框架

LSTM的训练算法仍然是反向传播算法,对于这个算法,我们已经非常熟悉了。主要有下面三个步骤:

  1. 前向计算每个神经元的输出值,对于LSTM来说,即五个向量的值。计算方法已经在上一节中描述过了。

  2. 反向计算每个神经元的误差项值。与循环神经网络一样,LSTM误差项的反向传播也是包括两个方向:一个是沿时间的反向传播,即从当前t时刻开始,计算每个时刻的误差项;一个是将误差项向上一层传播。

  3. 根据相应的误差项,计算每个权重的梯度。

关于公式和符号的说明

首先,我们对推导中用到的一些公式、符号做一下必要的说明。

接下来的推导中,我们设定gate的激活函数为sigmoid函数,输出的激活函数为tanh函数。他们的导数分别为:


5.png


长短时记忆网络的实现


在下面的实现中,LSTMLayer的参数包括输入维度、输出维度、隐藏层维度,单元状态维度等于隐藏层维度。gate的激活函数为sigmoid函数,输出的激活函数为tanh。

激活函数的实现


我们先实现两个激活函数:sigmoid和tanh。


  1. class SigmoidActivator(object):

  2.    def forward(self, weighted_input):

  3.        return 1.0 / (1.0 + np.exp(-weighted_input))


  4.    def backward(self, output):

  5.        return output * (1 - output)



  6. class TanhActivator(object):

  7.    def forward(self, weighted_input):

  8.        return 2.0 / (1.0 + np.exp(-2 * weighted_input)) - 1.0


  9.    def backward(self, output):

  10.        return 1 - output * output

LSTM初始化


和前两篇文章代码架构一样,我们把LSTM的实现放在LstmLayer类中。


在构造函数的初始化中,只初始化了与forward计算相关的变量,与backward相关的变量没有初始化。这是因为构造LSTM对象的时候,我们还不知道它未来是用于训练(既有forward又有backward)还是推理(只有forward)。


  1. class LstmLayer(object):

  2.    def __init__(self, input_width, state_width,

  3.                 learning_rate):

  4.        self.input_width = input_width

  5.        self.state_width = state_width

  6.        self.learning_rate = learning_rate

  7.        # 门的激活函数

  8.        self.gate_activator = SigmoidActivator()

  9.        # 输出的激活函数

  10.        self.output_activator = TanhActivator()

  11.        # 当前时刻初始化为t0

  12.        self.times = 0      

  13.        # 各个时刻的单元状态向量c

  14.        self.c_list = self.init_state_vec()

  15.        # 各个时刻的输出向量h

  16.        self.h_list = self.init_state_vec()

  17.        # 各个时刻的遗忘门f

  18.        self.f_list = self.init_state_vec()

  19.        # 各个时刻的输入门i

  20.        self.i_list = self.init_state_vec()

  21.        # 各个时刻的输出门o

  22.        self.o_list = self.init_state_vec()

  23.        # 各个时刻的即时状态c~

  24.        self.ct_list = self.init_state_vec()

  25.        # 遗忘门权重矩阵Wfh, Wfx, 偏置项bf

  26.        self.Wfh, self.Wfx, self.bf = (

  27.            self.init_weight_mat())

  28.        # 输入门权重矩阵Wfh, Wfx, 偏置项bf

  29.        self.Wih, self.Wix, self.bi = (

  30.            self.init_weight_mat())

  31.        # 输出门权重矩阵Wfh, Wfx, 偏置项bf

  32.        self.Woh, self.Wox, self.bo = (

  33.            self.init_weight_mat())

  34.        # 单元状态权重矩阵Wfh, Wfx, 偏置项bf

  35.        self.Wch, self.Wcx, self.bc = (

  36.            self.init_weight_mat())


  37.    def init_state_vec(self):

  38.        '''

  39.        初始化保存状态的向量

  40.        '''

  41.        state_vec_list = []

  42.        state_vec_list.append(np.zeros(

  43.            (self.state_width, 1)))

  44.        return state_vec_list


  45.    def init_weight_mat(self):

  46.        '''

  47.        初始化权重矩阵

  48.        '''

  49.        Wh = np.random.uniform(-1e-4, 1e-4,

  50.            (self.state_width, self.state_width))

  51.        Wx = np.random.uniform(-1e-4, 1e-4,

  52.            (self.state_width, self.input_width))

  53.        b = np.zeros((self.state_width, 1))

  54.        return Wh, Wx, b


前向计算的实现


forward方法实现了LSTM的前向计算:


  1.    def forward(self, x):

  2.        '''

  3.        根据式1-式6进行前向计算

  4.        '''

  5.        self.times += 1

  6.        # 遗忘门

  7.        fg = self.calc_gate(x, self.Wfx, self.Wfh,

  8.            self.bf, self.gate_activator)

  9.        self.f_list.append(fg)

  10.        # 输入门

  11.        ig = self.calc_gate(x, self.Wix, self.Wih,

  12.            self.bi, self.gate_activator)

  13.        self.i_list.append(ig)

  14.        # 输出门

  15.        og = self.calc_gate(x, self.Wox, self.Woh,

  16.            self.bo, self.gate_activator)

  17.        self.o_list.append(og)

  18.        # 即时状态

  19.        ct = self.calc_gate(x, self.Wcx, self.Wch,

  20.            self.bc, self.output_activator)

  21.        self.ct_list.append(ct)

  22.        # 单元状态

  23.        c = fg * self.c_list[self.times - 1] + ig * ct

  24.        self.c_list.append(c)

  25.        # 输出

  26.        h = og * self.output_activator.forward(c)

  27.        self.h_list.append(h)


  28.    def calc_gate(self, x, Wx, Wh, b, activator):

  29.        '''

  30.        计算门

  31.        '''

  32.        h = self.h_list[self.times - 1] # 上次的LSTM输出

  33.        net = np.dot(Wh, h) + np.dot(Wx, x) + b

  34.        gate = activator.forward(net)

  35.        return gate



反向传播算法的实现


backward方法实现了LSTM的反向传播算法。需要注意的是,与backword相关的内部状态变量是在调用backward方法之后才初始化的。这种延迟初始化的一个好处是,如果LSTM只是用来推理,那么就不需要初始化这些变量,节省了很多内存。


算法主要分成两个部分,一部分使计算误差项:


  1.    def calc_delta(self, delta_h, activator):

  2.        # 初始化各个时刻的误差项

  3.        self.delta_h_list = self.init_delta()  # 输出误差项

  4.        self.delta_o_list = self.init_delta()  # 输出门误差项

  5.        self.delta_i_list = self.init_delta()  # 输入门误差项

  6.        self.delta_f_list = self.init_delta()  # 遗忘门误差项

  7.        self.delta_ct_list = self.init_delta() # 即时输出误差项


  8.        # 保存从上一层传递下来的当前时刻的误差项

  9.        self.delta_h_list[-1] = delta_h


  10.        # 迭代计算每个时刻的误差项

  11.        for k in range(self.times, 0, -1):

  12.            self.calc_delta_k(k)


  13.    def init_delta(self):

  14.        '''

  15.        初始化误差项

  16.        '''

  17.        delta_list = []

  18.        for i in range(self.times + 1):

  19.            delta_list.append(np.zeros(

  20.                (self.state_width, 1)))

  21.        return delta_list


  22.    def calc_delta_k(self, k):

  23.        '''

  24.        根据k时刻的delta_h,计算k时刻的delta_f、

  25.        delta_i、delta_o、delta_ct,以及k-1时刻的delta_h

  26.        '''

  27.        # 获得k时刻前向计算的值

  28.        ig = self.i_list[k]

  29.        og = self.o_list[k]

  30.        fg = self.f_list[k]

  31.        ct = self.ct_list[k]

  32.        c = self.c_list[k]

  33.        c_prev = self.c_list[k-1]

  34.        tanh_c = self.output_activator.forward(c)

  35.        delta_k = self.delta_h_list[k]


  36.        # 根据式9计算delta_o

  37.        delta_o = (delta_k * tanh_c *

  38.            self.gate_activator.backward(og))

  39.        delta_f = (delta_k * og *

  40.            (1 - tanh_c * tanh_c) * c_prev *

  41.            self.gate_activator.backward(fg))

  42.        delta_i = (delta_k * og *

  43.            (1 - tanh_c * tanh_c) * ct *

  44.            self.gate_activator.backward(ig))

  45.        delta_ct = (delta_k * og *

  46.            (1 - tanh_c * tanh_c) * ig *

  47.            self.output_activator.backward(ct))

  48.        delta_h_prev = (

  49.                np.dot(delta_o.transpose(), self.Woh) +

  50.                np.dot(delta_i.transpose(), self.Wih) +

  51.                np.dot(delta_f.transpose(), self.Wfh) +

  52.                np.dot(delta_ct.transpose(), self.Wch)

  53.            ).transpose()


  54.        # 保存全部delta值

  55.        self.delta_h_list[k-1] = delta_h_prev

  56.        self.delta_f_list[k] = delta_f

  57.        self.delta_i_list[k] = delta_i

  58.        self.delta_o_list[k] = delta_o

  59.        self.delta_ct_list[k] = delta_ct



另一部分是计算梯度:


  1.    def calc_gradient(self, x):

  2.        # 初始化遗忘门权重梯度矩阵和偏置项

  3.        self.Wfh_grad, self.Wfx_grad, self.bf_grad = (

  4.            self.init_weight_gradient_mat())

  5.        # 初始化输入门权重梯度矩阵和偏置项

  6.        self.Wih_grad, self.Wix_grad, self.bi_grad = (

  7.            self.init_weight_gradient_mat())

  8.        # 初始化输出门权重梯度矩阵和偏置项

  9.        self.Woh_grad, self.Wox_grad, self.bo_grad = (

  10.            self.init_weight_gradient_mat())

  11.        # 初始化单元状态权重梯度矩阵和偏置项

  12.        self.Wch_grad, self.Wcx_grad, self.bc_grad = (

  13.            self.init_weight_gradient_mat())


  14.       # 计算对上一次输出h的权重梯度

  15.        for t in range(self.times, 0, -1):

  16.            # 计算各个时刻的梯度

  17.            (Wfh_grad, bf_grad,

  18.            Wih_grad, bi_grad,

  19.            Woh_grad, bo_grad,

  20.            Wch_grad, bc_grad) = (

  21.                self.calc_gradient_t(t))

  22.            # 实际梯度是各时刻梯度之和

  23.            self.Wfh_grad += Wfh_grad

  24.            self.bf_grad += bf_grad

  25.            self.Wih_grad += Wih_grad

  26.            self.bi_grad += bi_grad

  27.            self.Woh_grad += Woh_grad

  28.            self.bo_grad += bo_grad

  29.            self.Wch_grad += Wch_grad

  30.            self.bc_grad += bc_grad

  31.            print '-----%d-----' % t

  32.            print Wfh_grad

  33.            print self.Wfh_grad


  34.        # 计算对本次输入x的权重梯度

  35.        xt = x.transpose()

  36.        self.Wfx_grad = np.dot(self.delta_f_list[-1], xt)

  37.        self.Wix_grad = np.dot(self.delta_i_list[-1], xt)

  38.        self.Wox_grad = np.dot(self.delta_o_list[-1], xt)

  39.        self.Wcx_grad = np.dot(self.delta_ct_list[-1], xt)


  40.    def init_weight_gradient_mat(self):

  41.        '''

  42.        初始化权重矩阵

  43.        '''

  44.        Wh_grad = np.zeros((self.state_width,

  45.            self.state_width))

  46.        Wx_grad = np.zeros((self.state_width,

  47.            self.input_width))

  48.        b_grad = np.zeros((self.state_width, 1))

  49.        return Wh_grad, Wx_grad, b_grad


  50.    def calc_gradient_t(self, t):

  51.        '''

  52.        计算每个时刻t权重的梯度

  53.        '''

  54.        h_prev = self.h_list[t-1].transpose()

  55.        Wfh_grad = np.dot(self.delta_f_list[t], h_prev)

  56.        bf_grad = self.delta_f_list[t]

  57.        Wih_grad = np.dot(self.delta_i_list[t], h_prev)

  58.        bi_grad = self.delta_f_list[t]

  59.        Woh_grad = np.dot(self.delta_o_list[t], h_prev)

  60.        bo_grad = self.delta_f_list[t]

  61.        Wch_grad = np.dot(self.delta_ct_list[t], h_prev)

  62.        bc_grad = self.delta_ct_list[t]

  63.        return Wfh_grad, bf_grad, Wih_grad, bi_grad, \

  64.               Woh_grad, bo_grad, Wch_grad, bc_grad



最后,是梯度检查的代码:


  1. def data_set():

  2.    x = [np.array([[1], [2], [3]]),

  3.         np.array([[2], [3], [4]])]

  4.    d = np.array([[1], [2]])

  5.    return x, d


  6. def gradient_check():

  7.    '''

  8.    梯度检查

  9.    '''

  10.    # 设计一个误差函数,取所有节点输出项之和

  11.    error_function = lambda o: o.sum()


  12.    lstm = LstmLayer(3, 2, 1e-3)


  13.    # 计算forward值

  14.    x, d = data_set()

  15.    lstm.forward(x[0])

  16.    lstm.forward(x[1])


  17.    # 求取sensitivity map

  18.    sensitivity_array = np.ones(lstm.h_list[-1].shape,

  19.                                dtype=np.float64)

  20.    # 计算梯度

  21.    lstm.backward(x[1], sensitivity_array, IdentityActivator())


  22.    # 检查梯度

  23.    epsilon = 10e-4

  24.    for i in range(lstm.Wfh.shape[0]):

  25.        for j in range(lstm.Wfh.shape[1]):

  26.            lstm.Wfh[i,j] += epsilon

  27.            lstm.reset_state()

  28.            lstm.forward(x[0])

  29.            lstm.forward(x[1])

  30.            err1 = error_function(lstm.h_list[-1])

  31.            lstm.Wfh[i,j] -= 2*epsilon

  32.            lstm.reset_state()

  33.            lstm.forward(x[0])

  34.            lstm.forward(x[1])

  35.            err2 = error_function(lstm.h_list[-1])

  36.            expect_grad = (err1 - err2) / (2 * epsilon)

  37.            lstm.Wfh[i,j] += epsilon

  38.            print 'weights(%d,%d): expected - actural %.4e - %.4e' % (

  39.                i, j, expect_grad, lstm.Wfh_grad[i,j])

  40.    return lstm



GRU


前面我们讲了一种普通的LSTM,事实上LSTM存在很多变体,许多论文中的LSTM都或多或少的不太一样。在众多的LSTM变体中,GRU (Gated Recurrent Unit)也许是最成功的一种。它对LSTM做了很多简化,同时却保持着和LSTM相同的效果。因此,GRU最近变得越来越流行。


小结


至此,LSTM——也许是结构最复杂的一类神经网络——就讲完了,相信拿下前几篇文章的读者们搞定这篇文章也不在话下吧!现在我们已经了解循环神经网络和它最流行的变体——LSTM,它们都可以用来处理序列。但是,有时候仅仅拥有处理序列的能力还不够,还需要处理比序列更为复杂的结构(比如树结构),这时候就需要用到另外一类网络:递归神经网络(Recursive Neural Network),巧合的是,它的缩写也是RNN。在下一篇文章中,我们将介绍递归神经网络和它的训练算法。