神经网络基础(六)——LSTM与GRU

1. 引言

在神经网络基础——循环神经网络中提到,循环神经网络很难处理长距离的依赖。于是提出了一种改进的循环神经网络,长短时记忆网络(Long Short Term Memory Network, LSTM),它成功的解决了原始循环神经网络的缺陷,成为当前最流行的RNN,在语音识别、图片描述、自然语言处理等许多领域中成功应用。


2. LSTM的基本概念

2.1 实现思路

与RNN相同,在 t t t时刻,LSTM的输入有三个:当前时刻网络的输入值 x t \boldsymbol{x_t} xt、上一时刻LSTM的输出值 h t − 1 \boldsymbol{h_{t-1}} ht1、以及上一时刻的单元状态 c t − 1 \boldsymbol{c_{t-1}} ct1;LSTM的输出有两个:当前时刻LSTM输出值 h t \boldsymbol{h_{t}} ht、和当前时刻的单元状态 c t \boldsymbol{c_{t}} ct。注意 x t \boldsymbol{x_t} xt h t \boldsymbol{h_{t}} ht c t \boldsymbol{c_{t}} ct都是向量。

在原始的RNN神经网络中,隐藏节点中有一个功能称为记忆单元 c c c ,其作用为:记录前一个时刻的信息,所以对于短期的输入非常敏感,而在LSTM模型中,增加了三个门,每个门都有自己的任务:

  • 输入门 (input gate):负责控制把即时状态输入到长期状态
  • 输出门 (output gate):负责控制是否把长期状态作为当前的LSTM的输出。
  • 遗忘门 (forget gate):负责控制继续保存长期状态。

由于这三个门的存在,使得模型对于记忆单元存储的内容以及输出的内容更加丰富。

当下图中所有的开关都处于闭合状态时,LSTM模型等同于普通的RNN模型,由此也可以看出,LSTM模型相对于RNN最大的区别在于,增加了三个开关。
在这里插入图片描述


2.2 三个门的工作流程

假设一个LSTM隐藏节点中的单元结构示意图如下:
在这里插入图片描述
上图中,

  • z z z为该隐藏单元的输入, a a a为该隐藏单元的输出,即隐藏状态的值 h t h^t ht
  • ∫ f \int_f f ∫ g \int_g g ∫ h \int_h h代表三种不同的激活函数,通常情况下, ∫ f \int_f f一般使用 sigmoid 激活函数,sigmoid 函数的输出在(0,1)之间的值,代表了门的打开程度,如果sigmoid函数的值为 1,则说明门是处于打开状态,若为0,则说明门处于关闭状态
  • Z i Z_i Zi Z f Z_f Zf Z o Z_o Zo,分别代表输入信号遗忘信号输出信号,决定对应三个门的状态是开放还是闭合;
  • W z c W_{zc} Wzc W i c W_{ic} Wic W f c W_{fc} Wfc W o c W_{oc} Woc,分别代表输入数据对应的权重输入信号的权重遗忘信号的权重输出信号的权重,决定对应三个门的状态是开放还是闭合;
  • b x b_{x} bx b i b_{i} bi b f b_{f} bf b o b_{o} bo,分别代表输入数据对应的bias输入信号的bias遗忘信号的bias输出信号的bias

2.2.1 计算流程

2.2.1.1 单个神经元的工作流程
  • 输入数据与输入门控信号的计算
    • 输入数据 Z Z Z输入权重 W z c W_{zc} Wzc相乘得到 Z Z Z经过激活函数 ∫ g \int_g g 得到 g ( Z ) g(Z) g(Z)
    • 输入数据 Z Z Z输入门权重 W i c W_{ic} Wic相乘得到 Z i Z_i Zi经过激活函数 ∫ f \int_f f 得到 f ( Z i ) f(Z_i) f(Zi)
    • g ( Z ) g(Z) g(Z) f ( Z i ) f(Z_i) f(Zi)按元素相乘得到 g ( Z ) f ( Z i ) g(Z)f(Z_i) g(Z)f(Zi)

输入门公式:
f ( Z i ) = s i g m o i d ( W i c ⋅ [ Z ] + b i ) \mathbf{f}(Z_i)=sigmoid(W_{ic}\cdot[\mathbf{Z}]+\mathbf{b}_i) f(Zi)=sigmoid(Wic[Z]+bi)

f ( Z i ) f(Z_i) f(Zi)其实起到了一个控制输入的作用,如果 f ( Z i ) f(Z_i) f(Zi)为0,则 g ( Z ) f ( Z i ) g(Z)f(Z_i) g(Z)f(Zi)为0,说明没有任何的输入。
在这里插入图片描述

  • 记忆单元和遗忘门控制信号的计算
    • 输入数据 Z Z Z输入门权重 W f c W_{fc} Wfc相乘得到 Z f Z_f Zf经过激活函数 ∫ f \int_f f 得到 f ( Z f ) f(Z_f) f(Zf)
    • 将记忆单元中的原始值记为 c c c,令其与 f ( Z f ) f(Z_f) f(Zf)按元素相乘,得到 c f ( Z f ) cf(Z_f) cf(Zf)
    • g ( Z ) f ( Z i ) g(Z)f(Z_i) g(Z)f(Zi) c f ( Z f ) cf(Z_f) cf(Zf)的和为 c ′ c' c记忆单元中的新的值,即:
      c ′ = c f ( Z f ) + g ( Z ) f ( Z i ) c'=cf(Z_f)+g(Z)f(Z_i) c=cf(Zf)+g(Z)f(Zi)

遗忘门公式:
f ( Z f ) = s i g m o i d ( W f c ⋅ [ Z ] + b f ) \mathbf{f}(Z_f)=sigmoid(W_{fc}\cdot[\mathbf{Z}]+\mathbf{b}_f) f(Zf)=sigmoid(Wfc[Z]+bf)

由此可见, f ( Z f ) f(Z_f) f(Zf)决定了要不要把保留原先记忆单元中的数据,如果 f ( Z f ) f(Z_f) f(Zf)为1,说明遗忘门打开,则 c ′ c' c的内容为当前输入与原来记忆单元的和,否则,记忆单元的值更新为当前输入。

在这里插入图片描述

  • 输出数据与输出门信号的控制
    • 输入数据 Z Z Z输入门权重 W o c W_{oc} Woc相乘得到 Z f Z_f Zf经过激活函数 ∫ f \int_f f 得到 f ( Z o ) f(Z_o) f(Zo)
    • c ′ c' c通过激活函数 ∫ h \int_h h得到 h ( c ′ ) h(c') h(c),再将 h ( c ′ ) h(c') h(c) f ( Z o ) f(Z_o) f(Zo)按元素相乘得到隐含状态的值 a a a,也就是 h t h^t ht h t = f ( Z o ) t ∘ tanh ⁡ ( c ′ ) h_t=f(Z_o)_t\circ \tanh(c') ht=f(Zo)ttanh(c)

输出门公式:
f ( Z o ) = σ ( W o c ⋅ [ Z ] + b o ) \mathbf{f}(Z_o)=\sigma(W_{oc}\cdot[\mathbf{Z}]+\mathbf{b}_o) f(Zo)=σ(Woc[Z]+bo)

如果 f ( Z o ) = 0 f(Z_o)=0 f(Zo)=0,说明神经元的值无法输出,否则则输出值为 a a a

总结一下规律:更新后记忆单元中的值 c t c^t ct其实是一个变换比较慢的数据,而 h t h^t ht则是一个变化较快的值

2.2.1.2 输入变量是个啥?

首先明确一点,输入变量 Z Z Z的值可以为一个向量,也可以为一个矩阵

在2.2.1.1节中,我们并没有使用 X X X作为输入变量的符号,而是使用了 Z Z Z,下面来介绍一下,古圣先贤们是怎么样把X变成Z的。
首先给出公式
Z = t a n h ( W z c × Z ′ ) Z=tanh(W_{zc} \times Z') Z=tanh(Wzc×Z)
Z ′ Z' Z是一个分块矩阵,可能有以下几种情况:

1. 极简版——单层LSTM的工作流程

如下图所示, t t t时刻隐藏状态得到的结果,作为 t + 1 t+1 t+1时刻的记忆单元的值,参与 t + 1 t+1 t+1时刻的运算。
此时:

  • Z ′ Z' Z就是 X X X的shape: ( V B a t c h _ s i z e × V X ) (V_{Batch\_size} \times V_X) (VBatch_size×VX)
  • W z c W_{zc} Wzc W i c W_{ic} Wic W f c W_{fc} Wfc的shape就是 X X X的shape: ( V X × V C ) (V_X \times V_C) (VX×VC) V C V_C VC是LSTM神经元的数量。
  • W o c W_{oc} Woc的shae为: ( V C × V Y ) (V_C \times V_Y) (VC×VY)

在这里插入图片描述

2.2.1.3 标准版——单层LSTM的工作流程

除了在极简版中把 t t t时刻隐藏状态得到的结果,作为 t + 1 t+1 t+1时刻的记忆单元的值,参与 t + 1 t+1 t+1时刻的运算之外,还把 t t t时刻的输出、 t + 1 t+1 t+1时刻的特征值进行矩阵的拼接,然后一同作为输入。

此时:

  • Z ′ Z' Z就是 [ X , h ] [X, h] [X,h],也就是输入矩阵 X X X的shape: ( V B a t c h _ s i z e × V X ) (V_{Batch\_size} \times V_X) (VBatch_size×VX),输出矩阵 Y Y Y的shape: ( V B a t c h _ s i z e × V Y ) (V_{Batch\_size} \times V_Y) (VBatch_size×VY),最后拼成了一个 ( V B a t c h _ s i z e × ( V X + V Y ) ) (V_{Batch\_size} \times (V_X+V_Y)) (VBatch_size×(VX+VY))
  • W z c W_{zc} Wzc W i c W_{ic} Wic W f c W_{fc} Wfc的shape就是 X X X的shape: ( V X × V C ) (V_X \times V_C) (VX×VC) V C V_C VC是LSTM神经元的数量。
  • W o c W_{oc} Woc的shae为: ( V C × V Y ) (V_C \times V_Y) (VC×VY)

在这里插入图片描述

2.2.1.4 常用版——单层LSTM的工作流程

除了在极简版中把 t t t时刻隐藏状态得到的结果,作为 t + 1 t+1 t+1时刻的记忆单元的值,参与 t + 1 t+1 t+1时刻的运算之外,还把 t t t时刻的输出、 t t t时刻隐藏状态的值、 t + 1 t+1 t+1时刻的特征值进行矩阵的拼接,然后一同作为输入。

  • Z ′ Z' Z就是 [ X , h , c ] [X, h, c] [X,h,c],也就是输入矩阵 X X X的shape: ( V B a t c h _ s i z e × V X ) (V_{Batch\_size} \times V_X) (VBatch_size×VX),输出矩阵 Y Y Y的shape: ( V B a t c h _ s i z e × V Y ) (V_{Batch\_size} \times V_Y) (VBatch_size×VY),记忆单元输出 C C C的shape: ( V B a t c h _ s i z e × V Y ) (V_{Batch\_size} \times V_Y) (VBatch_size×VY),最后拼成了一个 ( V B a t c h _ s i z e × ( V X + V Y + V Y ) ) (V_{Batch\_size} \times (V_X+V_Y+V_Y)) (VBatch_size×(VX+VY+VY))
  • W z c W_{zc} Wzc W i c W_{ic} Wic W f c W_{fc} Wfc的shape就是 X X X的shape: ( ( V X + V Y + V Y ) × V C ) ( (V_X+V_Y+V_Y) \times V_C) ((VX+VY+VY)×VC) V C V_C VC是LSTM神经元的数量。
  • W o c W_{oc} Woc的shae为: ( V C × V Y ) (V_C \times V_Y) (VC×VY)

在这里插入图片描述

Note:在拼接 t t t时刻隐藏状态的值的时候,要求其所对应的权重是diagonal的,也就是W中的对应的分块矩阵部分。
在这里插入图片描述

2.2.1.5 常用版——多层LSTM的工作流程

在这里插入图片描述

2.2.2 输入维度与隐藏循环神经元数目的关系

因此,在只有两个时刻的输入变量以及两个循环神经元的情况下,LSTM的总架构如下:

在这里插入图片描述
Z的每一个维度都代表了操控LSTM的Memory cell。所以,Z的dimension都代表了LSTM隐藏单元的数目。


3. GRU

note:与LSTM不同的是,GRU神经网络只有两个门控单元,结构要比LSTM简单很多(但是理解就没那么容易了)。

3.1 基本概念

相对于LSTM神经网络:

  1. 将输入门、遗忘门、输出门变为两个门:更新门(Update Gate) Z t Z_t Zt和重置门(Reset Gate) r t r_t rt
  2. 将单元状态与输出合并为一个状态: h h h

GRU结构如下所示:
在这里插入图片描述
在上图中,各符号意义代表:

  • c t − 1 c^{t-1} ct1 t − 1 t-1 t1时刻隐藏状态的值。
  • h t − 1 h^{t-1} ht1 t − 1 t-1 t1时刻输出的值。
  • x t x^{t} xt t t t时刻输入的值。
  • y t y^{t} yt t t t时刻输出的值。
  • W r W_{r} Wr:重置门的权重。
  • W z W_{z} Wz:更新门的权重。

3.2 计算流程

  • 根据 t − 1 t-1 t1时刻的输出和 t t t时刻的输入,判断重置门是否开启。

    • t t t时刻输入数据 X t X^{t} Xt t − 1 t-1 t1时刻输出数据 h t − 1 h^{t-1} ht1拼接成矩阵, W z r W_{zr} Wzr相乘得到 Z Z Z经过激活函数 ∫ r \int_r r 得到 r ( Z ) r(Z) r(Z)
      在这里插入图片描述
  • 你重置也好,不重置也好,反正我 t t t时刻有输入,那我就算一下 t t t时刻有发生了哪些改变。
    在这里插入图片描述

  • 根据 t − 1 t-1 t1时刻的输出和 t t t时刻的输入,判断更新门是否开启。

    • t t t时刻输入数据 X t X^{t} Xt t − 1 t-1 t1时刻输出数据 h t − 1 h^{t-1} ht1拼接成矩阵, W z r W_{zr} Wzr相乘得到 Z Z Z经过激活函数 ∫ r \int_r r 得到 r ( Z ) r(Z) r(Z)
      在这里插入图片描述
  • 不管怎么样,反正我在 t t t时刻一定要输出,你看着办吧

    • 更新门打开:
      在这里插入图片描述
    • 更新门关闭:
      在这里插入图片描述

4. 它们的应用

4.1 Many to One

  • 情感分析:输入是一个Vector Sequence,输出是一个Vector
    在这里插入图片描述
  • 关键词提取
    在这里插入图片描述

4.2 Many to Many(Seq2Seq)

  • 语音识别(Input Sequence 长, Output Sequence 短):结合CTC(connectionlist Temporal Classification)
    在这里插入图片描述

  • 文本翻译
    在这里插入图片描述

5. 代码实现

5.1 自实现LSTM单元版本

本文实现的仅仅是标准版本的LSTM,其他的仅仅区别于矩阵的拼接问题。

至于代码改动部分,其实相对于神经网络基础(五)——循环神经网络一节,仅仅三个函数发生了改变:
get_params, init_rnn_state, rnn

获取参数函数:

def get_params(num_inputs, num_hiddens, num_outputs):
    '''
    初始化模型参数
    :return:
    '''
    W_xi = nd.random.normal(scale=0.01, shape=(num_inputs, num_hiddens), ctx=ctx)
    W_hi = nd.random.normal(scale=0.01, shape=(num_hiddens, num_hiddens), ctx=ctx)

    W_xf = nd.random.normal(scale=0.01, shape=(num_inputs, num_hiddens), ctx=ctx)
    W_hf = nd.random.normal(scale=0.01, shape=(num_hiddens, num_hiddens), ctx=ctx)

    W_xo = nd.random.normal(scale=0.01, shape=(num_inputs, num_hiddens), ctx=ctx)
    W_ho = nd.random.normal(scale=0.01, shape=(num_hiddens, num_hiddens), ctx=ctx)

    W_xc = nd.random.normal(scale=0.01, shape=(num_inputs, num_hiddens), ctx=ctx)
    W_hc = nd.random.normal(scale=0.01, shape=(num_hiddens, num_hiddens), ctx=ctx)

    b_i = nd.zeros(num_hiddens, ctx=ctx)
    b_f = nd.zeros(num_hiddens, ctx=ctx)
    b_o = nd.zeros(num_hiddens, ctx=ctx)
    b_c = nd.zeros(num_hiddens, ctx=ctx)

    W_hq = nd.random.normal(scale=0.01, shape=(num_hiddens, num_outputs), ctx=ctx)
    b_q = nd.zeros(num_outputs, ctx=ctx)

    params = [W_xi, W_hi, b_i, W_xf, W_hf, b_f, W_xo, W_ho, b_o, W_xc, W_hc,
              b_c, W_hq, b_q]
    for param in params:
        param.attach_grad()
    return params

初始化隐藏状态和输入函数:

def init_rnn_state(batch_size, num_hiddens):
    '''
    初始化隐藏单元的值
    :return:
    '''
    return (
        nd.zeros(shape=(batch_size, num_hiddens), ctx=ctx),
        nd.zeros(shape=(batch_size, num_hiddens), ctx=ctx),
    )

运行单次一次:

def rnn(inputs, state, params):
    '''
    运行一轮模型
    :return:
    '''
    # inputs和outputs皆为num_steps个形状为(batch_size, vocab_size)的矩阵
    W_xi, W_hi, b_i, W_xf, W_hf, b_f, W_xo, W_ho, b_o, W_xc, W_hc, b_c, W_hq, b_q = params
    (H, C) = state
    outputs = []
    for X in inputs:
        I = nd.sigmoid(nd.dot(X, W_xi) + nd.dot(H, W_hi) + b_i)
        F = nd.sigmoid(nd.dot(X, W_xf) + nd.dot(H, W_hf) + b_f)
        O = nd.sigmoid(nd.dot(X, W_xo) + nd.dot(H, W_ho) + b_o)

        C_tilda = nd.tanh(nd.dot(X, W_xc) + nd.dot(H, W_hc) + b_c)
        C = C_tilda * I + F * C
        H = O * nd.tanh(C)
        Y = nd.dot(H, W_hq) + b_q
        outputs.append(Y)
    return outputs, (H, C)

5.2 调用Pytorch库的版本

step1. 先调用库定义LSTM神经网络的基本架构:LSTM标准神经元的结构(三门)+有多少层网络。具体参数介绍:看这里

# 声明一个每个时间点具有10个特征,隐藏层深度为1(默认参数),隐藏神经元数量为20的LSTM神经网络。
lstm_layer = nn.LSTM(input_size=10, hidden_size=20)

step2. 自定义Model

model = RNNModel(lstm_layer, input_size)

RNNModel的实现如下:

class RNNModel(nn.Module):
    def __init__(self, rnn_layer, input_size):
        # 继承Pytorch本身的RNNModel
        super(RNNModel, self).__init__()
        # run_layer 就是第一步中定义好的网络结构
        self.rnn = rnn_layer
        # 从rnn_layer中获得隐藏单元的数量
        self.hidden_size = rnn_layer.hidden_size * (2 if rnn_layer.bidirectional else 1)
        self.input_size= input_size
		# 定义输出层:线性输出,输出的维度与输入维度一样
        self.dense = nn.Linear(self.hidden_size, vocab_size)
		# 假设LSTM的初始单元状态为None
        self.state = None

    def forward(self, inputs, state): # inputs: (batch, seq_len)
        # 获取one-hot向量表示,先看step 3
        X = to_onehot(inputs, self.vocab_size) # X是个list
        # 接下来就是计算LSTM神经网络的结果,返回值为
       		# 1. Y of shape (seq_len, batch, num_directions * hidden_size)
        	# 2. h_n of shape (num_layers * num_directions, batch, hidden_size) 
        	# 3. c_n of shape (num_layers * num_directions, batch, hidden_size)
        Y, self.state = self.rnn(torch.stack(X), state)
        # 全连接层会首先将Y的形状变成(num_steps * batch_size, num_hiddens),它的输出
        # 形状为(num_steps * batch_size, vocab_size)
        output = self.dense(Y.view(-1, Y.shape[-1]))
        return output, self.state

step2.5. 格式化输入
回归一下每一次训练值是啥:以一句话为例:

原始数据:一二三四五六七八九十

我们选择timestamps=4, batch_size=2,那么需要格式化为:

一二三四
五六七八

剩下的 “九十” 就被丢掉了。对于每一个字符,假设都用 400 × 1 400 \times 1 400×1的向量表示。

to_onehot函数的功能如下:把“一、五”用向量的形式表示。

def to_onehot(X, n_class):
    # X shape: (batch, seq_len), output: seq_len elements of (batch, n_class)
    return [one_hot(X[:, i], n_class) for i in range(X.shape[1])]
    
def one_hot(x, n_class, dtype=torch.float32):
    # X shape: (batch), output shape: (batch, n_class)
    x = x.long()
    res = torch.zeros(x.shape[0], n_class, dtype=dtype, device=x.device)
    res.scatter_(1, x.view(-1, 1), 1)
    return res

6. 参考文献

  • 《DEEP Learning》
  • 《西瓜书》
  • 《动手学深度学习》
  • 零基础入门深度学习
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值