lstm-scratch.md 9.5 KB
Newer Older
A
Aston Zhang 已提交
1
# 长短期记忆(LSTM)——从零开始
A
Aston Zhang 已提交
2

A
Aston Zhang 已提交
3
[上一节](bptt.md)中,我们介绍了循环神经网络中的梯度计算方法。我们发现,循环神经网络的隐含层变量梯度可能会出现衰减或爆炸。虽然[梯度裁剪](rnn-scratch.md)可以应对梯度爆炸,但无法解决梯度衰减的问题。因此,给定一个时间序列,例如文本序列,循环神经网络在实际中其实较难捕捉两个时刻距离较大的文本元素(字或词)之间的依赖关系。
A
Aston Zhang 已提交
4

A
Aston Zhang 已提交
5
为了更好地捕捉时序数据中间隔较大的依赖关系,我们介绍了一种常用的门控循环神经网络,叫做[门控循环单元](gru-scratch.md)。本节将介绍另一种常用的门控循环神经网络,长短期记忆(long short-term memory,简称LSTM)。它由Hochreiter和Schmidhuber在1997年被提出。事实上,它比门控循环单元的结构稍微更复杂一点。
A
Aston Zhang 已提交
6 7


A
Aston Zhang 已提交
8
## 长短期记忆
A
Aston Zhang 已提交
9

A
Aston Zhang 已提交
10
我们先介绍长短期记忆的构造。长短期记忆的隐含状态包括隐含层变量$\boldsymbol{H}$和细胞$\boldsymbol{C}$(也称记忆细胞)。它们形状相同。
A
Aston Zhang 已提交
11 12


A
Aston Zhang 已提交
13
### 输入门、遗忘门和输出门
A
Aston Zhang 已提交
14 15


A
Aston Zhang 已提交
16
假定隐含状态长度为$h$,给定时刻$t$的一个样本数为$n$特征向量维度为$x$的批量数据$\boldsymbol{X}_t \in \mathbb{R}^{n \times x}$和上一时刻隐含状态$\boldsymbol{H}_{t-1} \in \mathbb{R}^{n \times h}$,输入门(input gate)$\boldsymbol{I}_t \in \mathbb{R}^{n \times h}$、遗忘门(forget gate)$\boldsymbol{F}_t \in \mathbb{R}^{n \times h}$和输出门(output gate)$\boldsymbol{O}_t \in \mathbb{R}^{n \times h}$的定义如下:
A
Aston Zhang 已提交
17

A
Aston Zhang 已提交
18
$$\boldsymbol{I}_t = \sigma(\boldsymbol{X}_t \boldsymbol{W}_{xi} + \boldsymbol{H}_{t-1} \boldsymbol{W}_{hi} + \boldsymbol{b}_i)$$
A
Aston Zhang 已提交
19

A
Aston Zhang 已提交
20
$$\boldsymbol{F}_t = \sigma(\boldsymbol{X}_t \boldsymbol{W}_{xf} + \boldsymbol{H}_{t-1} \boldsymbol{W}_{hf} + \boldsymbol{b}_f)$$
A
Aston Zhang 已提交
21

A
Aston Zhang 已提交
22
$$\boldsymbol{O}_t = \sigma(\boldsymbol{X}_t \boldsymbol{W}_{xo} + \boldsymbol{H}_{t-1} \boldsymbol{W}_{ho} + \boldsymbol{b}_o)$$
A
Aston Zhang 已提交
23

A
Aston Zhang 已提交
24
其中的$\boldsymbol{W}_{xi}, \boldsymbol{W}_{xf}, \boldsymbol{W}_{xo} \in \mathbb{R}^{x \times h}$和$\boldsymbol{W}_{hi}, \boldsymbol{W}_{hf}, \boldsymbol{W}_{ho} \in \mathbb{R}^{h \times h}$是可学习的权重参数,$\boldsymbol{b}_i, \boldsymbol{b}_f, \boldsymbol{b}_o \in \mathbb{R}^{1 \times h}$是可学习的偏移参数。函数$\sigma$自变量中的三项相加使用了[广播](../chapter_crashcourse/ndarray.md)
A
Aston Zhang 已提交
25

A
Aston Zhang 已提交
26
[门控循环单元](gru-scratch.md)中的重置门和更新门一样,这里的输入门、遗忘门和输出门中每个元素的值域都是$[0, 1]$。
A
Aston Zhang 已提交
27 28


A
Aston Zhang 已提交
29
### 候选细胞
A
Aston Zhang 已提交
30

A
Aston Zhang 已提交
31
[门控循环单元](gru-scratch.md)中的候选隐含状态一样,长短期记忆中的候选细胞$\tilde{\boldsymbol{C}}_t \in \mathbb{R}^{n \times h}$也使用了值域在$[-1, 1]$的双曲正切函数tanh做激活函数:
A
Aston Zhang 已提交
32

A
Aston Zhang 已提交
33
$$\tilde{\boldsymbol{C}}_t = \text{tanh}(\boldsymbol{X}_t \boldsymbol{W}_{xc} + \boldsymbol{H}_{t-1} \boldsymbol{W}_{hc} + \boldsymbol{b}_c)$$
A
Aston Zhang 已提交
34

A
Aston Zhang 已提交
35
其中的$\boldsymbol{W}_{xc} \in \mathbb{R}^{x \times h}$和$\boldsymbol{W}_{hc} \in \mathbb{R}^{h \times h}$是可学习的权重参数,$\boldsymbol{b}_c \in \mathbb{R}^{1 \times h}$是可学习的偏移参数。
A
Aston Zhang 已提交
36 37


A
Aston Zhang 已提交
38
### 细胞
A
Aston Zhang 已提交
39

A
Aston Zhang 已提交
40
我们可以通过元素值域在$[0, 1]$的输入门、遗忘门和输出门来控制隐含状态中信息的流动:这通常可以应用按元素乘法符$\odot$。当前时刻细胞$\boldsymbol{C}_t \in \mathbb{R}^{n \times h}$的计算组合了上一时刻细胞和当前时刻候选细胞的信息,并通过遗忘门和输入门来控制信息的流动:
A
Aston Zhang 已提交
41

A
Aston Zhang 已提交
42
$$\boldsymbol{C}_t = \boldsymbol{F}_t \odot \boldsymbol{C}_{t-1} + \boldsymbol{I}_t \odot \tilde{\boldsymbol{C}}_t$$
A
Aston Zhang 已提交
43

A
Aston Zhang 已提交
44
需要注意的是,如果遗忘门一直近似1且输入门一直近似0,过去的细胞将一直通过时间保存并传递至当前时刻。这个设计可以应对循环神经网络中的梯度衰减问题,并更好地捕捉时序数据中间隔较大的依赖关系。
A
Aston Zhang 已提交
45 46


A
Aston Zhang 已提交
47
### 隐含状态
A
Aston Zhang 已提交
48

A
Aston Zhang 已提交
49
有了细胞以后,接下来我们还可以通过输出门来控制从细胞到隐含层变量$\boldsymbol{H}_t \in \mathbb{R}^{n \times h}$的信息的流动:
A
Aston Zhang 已提交
50

A
Aston Zhang 已提交
51
$$\boldsymbol{H}_t = \boldsymbol{O}_t \odot \text{tanh}(\boldsymbol{C}_t)$$
A
Aston Zhang 已提交
52

A
Aston Zhang 已提交
53
需要注意的是,当输出门近似1,细胞信息将传递到隐含层变量;当输出门近似0,细胞信息只自己保留。
A
Aston Zhang 已提交
54 55 56



A
Aston Zhang 已提交
57
输出层的设计可参照[循环神经网络](rnn-scratch.md)中的描述。
A
Aston Zhang 已提交
58 59


A
Aston Zhang 已提交
60
## 实验
A
Aston Zhang 已提交
61 62


A
Aston Zhang 已提交
63
为了实现并展示门控循环单元,我们依然使用周杰伦歌词数据集来训练模型作词。这里除长短期记忆以外的实现已在[循环神经网络](rnn-scratch.md)中介绍。
A
Aston Zhang 已提交
64 65


A
Aston Zhang 已提交
66
### 数据处理
A
Aston Zhang 已提交
67

A
Aston Zhang 已提交
68
我们先读取并对数据集做简单处理。
A
Aston Zhang 已提交
69

A
Aston Zhang 已提交
70 71 72 73
```{.python .input  n=1}
import zipfile
with zipfile.ZipFile('../data/jaychou_lyrics.txt.zip', 'r') as zin:
    zin.extractall('../data/')
A
Aston Zhang 已提交
74

A
Aston Zhang 已提交
75 76
with open('../data/jaychou_lyrics.txt') as f:
    corpus_chars = f.read()
A
Aston Zhang 已提交
77

A
Aston Zhang 已提交
78 79
corpus_chars = corpus_chars.replace('\n', ' ').replace('\r', ' ')
corpus_chars = corpus_chars[0:20000]
A
Aston Zhang 已提交
80

A
Aston Zhang 已提交
81 82
idx_to_char = list(set(corpus_chars))
char_to_idx = dict([(char, i) for i, char in enumerate(idx_to_char)])
A
Aston Zhang 已提交
83 84
corpus_indices = [char_to_idx[char] for char in corpus_chars]

A
Aston Zhang 已提交
85 86
vocab_size = len(char_to_idx)
print('vocab size:', vocab_size)
A
Aston Zhang 已提交
87 88
```

A
Aston Zhang 已提交
89
我们使用onehot来将字符索引表示成向量。
A
Aston Zhang 已提交
90

A
Aston Zhang 已提交
91
```{.python .input  n=2}
A
Aston Zhang 已提交
92 93 94 95
def get_inputs(data):
    return [nd.one_hot(X, vocab_size) for X in data.T]
```

A
Aston Zhang 已提交
96
### 初始化模型参数
A
Aston Zhang 已提交
97

A
Aston Zhang 已提交
98
以下部分对模型参数进行初始化。参数`hidden_dim`定义了隐含状态的长度。
A
Aston Zhang 已提交
99

A
Aston Zhang 已提交
100
```{.python .input  n=3}
A
Aston Zhang 已提交
101 102 103 104 105
import mxnet as mx

# 尝试使用GPU
import sys
sys.path.append('..')
A
Aston Zhang 已提交
106
from mxnet import nd
A
Aston Zhang 已提交
107 108 109 110 111
import utils
ctx = utils.try_gpu()
print('Will use', ctx)

input_dim = vocab_size
A
Aston Zhang 已提交
112
# 隐含状态长度
A
Aston Zhang 已提交
113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150
hidden_dim = 256
output_dim = vocab_size
std = .01

def get_params():
    # 输入门参数
    W_xi = nd.random_normal(scale=std, shape=(input_dim, hidden_dim), ctx=ctx)
    W_hi = nd.random_normal(scale=std, shape=(hidden_dim, hidden_dim), ctx=ctx)
    b_i = nd.zeros(hidden_dim, ctx=ctx)
    
    # 遗忘门参数
    W_xf = nd.random_normal(scale=std, shape=(input_dim, hidden_dim), ctx=ctx)
    W_hf = nd.random_normal(scale=std, shape=(hidden_dim, hidden_dim), ctx=ctx)
    b_f = nd.zeros(hidden_dim, ctx=ctx)
    
    # 输出门参数
    W_xo = nd.random_normal(scale=std, shape=(input_dim, hidden_dim), ctx=ctx)
    W_ho = nd.random_normal(scale=std, shape=(hidden_dim, hidden_dim), ctx=ctx)
    b_o = nd.zeros(hidden_dim, ctx=ctx)

    # 候选细胞参数
    W_xc = nd.random_normal(scale=std, shape=(input_dim, hidden_dim), ctx=ctx)
    W_hc = nd.random_normal(scale=std, shape=(hidden_dim, hidden_dim), ctx=ctx)
    b_c = nd.zeros(hidden_dim, ctx=ctx)

    # 输出层
    W_hy = nd.random_normal(scale=std, shape=(hidden_dim, output_dim), ctx=ctx)
    b_y = nd.zeros(output_dim, 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_hy, b_y]
    for param in params:
        param.attach_grad()
    return params
```

## 定义模型

A
Aston Zhang 已提交
151
我们将前面的模型公式翻译成代码。
A
Aston Zhang 已提交
152

A
Aston Zhang 已提交
153
```{.python .input  n=4}
A
Aston Zhang 已提交
154
def lstm_rnn(inputs, state_h, state_c, *params):
A
Aston Zhang 已提交
155 156 157 158 159 160
    # inputs: num_steps 个尺寸为 batch_size * vocab_size 矩阵
    # H: 尺寸为 batch_size * hidden_dim 矩阵
    # 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_hy, b_y] = params

A
Aston Zhang 已提交
161 162
    H = state_h
    C = state_c
A
Aston Zhang 已提交
163 164 165 166 167 168 169 170 171 172 173 174 175
    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 = F * C + I * C_tilda
        H = O * nd.tanh(C)
        Y = nd.dot(H, W_hy) + b_y
        outputs.append(Y)
    return (outputs, H, C)
```

A
Aston Zhang 已提交
176
### 训练模型
A
Aston Zhang 已提交
177

A
Aston Zhang 已提交
178
下面我们开始训练模型。我们假定谱写歌词的前缀分别为“分开”、“不分开”和“战争中部队”。这里采用的是相邻批量采样实验门控循环单元谱写歌词。
A
Aston Zhang 已提交
179

A
Aston Zhang 已提交
180
```{.python .input  n=5}
A
Aston Zhang 已提交
181 182 183
seq1 = '分开'
seq2 = '不分开'
seq3 = '战争中部队'
A
Aston Zhang 已提交
184
seqs = [seq1, seq2, seq3]
A
Aston Zhang 已提交
185

A
Aston Zhang 已提交
186 187
utils.train_and_predict_rnn(rnn=lstm_rnn, is_random_iter=False, epochs=200,
                            num_steps=35, hidden_dim=hidden_dim, 
188
                            learning_rate=0.2, clipping_norm=5,
A
Aston Zhang 已提交
189 190 191 192 193 194
                            batch_size=32, pred_period=20, pred_len=100,
                            seqs=seqs, get_params=get_params,
                            get_inputs=get_inputs, ctx=ctx,
                            corpus_indices=corpus_indices,
                            idx_to_char=idx_to_char, char_to_idx=char_to_idx,
                            is_lstm=True)
A
Aston Zhang 已提交
195 196 197 198
```

可以看到一开始学到简单的字符,然后简单的词,接着是复杂点的词,然后看上去似乎像个句子了。

A
Aston Zhang 已提交
199
## 小结
A
Aston Zhang 已提交
200

A
Aston Zhang 已提交
201 202 203
* 长短期记忆的提出是为了更好地捕捉时序数据中间隔较大的依赖关系。
* 长短期记忆的结构比门控循环单元的结构较复杂。

A
Aston Zhang 已提交
204 205 206

## 练习

A
Aston Zhang 已提交
207
* 调调参数(例如数据集大小、序列长度、隐含状态长度和学习率),看看对运行时间、perplexity和预测的结果造成的影响。
A
Aston Zhang 已提交
208
* 在相同条件下,比较长短期记忆和门控循环单元以及循环神经网络的运行效率。
A
Aston Zhang 已提交
209

A
Aston Zhang 已提交
210 211 212
## 扫码直达[讨论区](https://discuss.gluon.ai/t/topic/4042)

![](../img/qr_lstm-scratch.svg)