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}} ht−1、以及上一时刻的单元状态 c t − 1 \boldsymbol{c_{t-1}} ct−1;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)t∘tanh(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神经网络:
- 将输入门、遗忘门、输出门变为两个门:更新门(Update Gate) Z t Z_t Zt和重置门(Reset Gate) r t r_t rt。
- 将单元状态与输出合并为一个状态: h h h。
GRU结构如下所示:
在上图中,各符号意义代表:
- c t − 1 c^{t-1} ct−1: t − 1 t-1 t−1时刻隐藏状态的值。
- h t − 1 h^{t-1} ht−1: t − 1 t-1 t−1时刻输出的值。
- 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 t−1时刻的输出和 t t t时刻的输入,判断重置门是否开启。
-
t
t
t时刻输入数据
X
t
X^{t}
Xt与
t
−
1
t-1
t−1时刻输出数据
h
t
−
1
h^{t-1}
ht−1拼接成矩阵,
W
z
r
W_{zr}
Wzr相乘得到
Z
Z
Z经过激活函数
∫
r
\int_r
∫r 得到
r
(
Z
)
r(Z)
r(Z);
-
t
t
t时刻输入数据
X
t
X^{t}
Xt与
t
−
1
t-1
t−1时刻输出数据
h
t
−
1
h^{t-1}
ht−1拼接成矩阵,
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 t−1时刻的输出和 t t t时刻的输入,判断更新门是否开启。
-
t
t
t时刻输入数据
X
t
X^{t}
Xt与
t
−
1
t-1
t−1时刻输出数据
h
t
−
1
h^{t-1}
ht−1拼接成矩阵,
W
z
r
W_{zr}
Wzr相乘得到
Z
Z
Z经过激活函数
∫
r
\int_r
∫r 得到
r
(
Z
)
r(Z)
r(Z);
-
t
t
t时刻输入数据
X
t
X^{t}
Xt与
t
−
1
t-1
t−1时刻输出数据
h
t
−
1
h^{t-1}
ht−1拼接成矩阵,
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》
- 《西瓜书》
- 《动手学深度学习》
- 零基础入门深度学习