长短期记忆(LSTM)— 从0开始

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

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

长短期记忆

我们先介绍长短期记忆的构造。长短期记忆的隐含状态包括隐含层变量\(\mathbf{H}\)和细胞\(\mathbf{C}\)(也称记忆细胞)。它们形状相同。

输入门、遗忘门和输出门

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

\[\mathbf{I}_t = \sigma(\mathbf{X}_t \mathbf{W}_{xi} + \mathbf{H}_{t-1} \mathbf{W}_{hi} + \mathbf{b}_i)\]
\[\mathbf{F}_t = \sigma(\mathbf{X}_t \mathbf{W}_{xf} + \mathbf{H}_{t-1} \mathbf{W}_{hf} + \mathbf{b}_f)\]
\[\mathbf{O}_t = \sigma(\mathbf{X}_t \mathbf{W}_{xo} + \mathbf{H}_{t-1} \mathbf{W}_{ho} + \mathbf{b}_o)\]

其中的\(\mathbf{W}_{xi}, \mathbf{W}_{xf}, \mathbf{W}_{xo} \in \mathbb{R}^{x \times h}\)\(\mathbf{W}_{hi}, \mathbf{W}_{hf}, \mathbf{W}_{ho} \in \mathbb{R}^{h \times h}\)是可学习的权重参数,\(\mathbf{b}_i, \mathbf{b}_f, \mathbf{b}_o \in \mathbb{R}^{1 \times h}\)是可学习的偏移参数。函数\(\sigma\)自变量中的三项相加使用了广播

门控循环单元中的重置门和更新门一样,这里的输入门、遗忘门和输出门中每个元素的值域都是\([0, 1]\)

候选细胞

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

\[\tilde{\mathbf{C}}_t = \text{tanh}(\mathbf{X}_t \mathbf{W}_{xc} + \mathbf{H}_{t-1} \mathbf{W}_{hc} + \mathbf{b}_c)\]

其中的\(\mathbf{W}_{xc} \in \mathbb{R}^{x \times h}\)\(\mathbf{W}_{hc} \in \mathbb{R}^{h \times h}\)是可学习的权重参数,\(\mathbf{b}_c \in \mathbb{R}^{1 \times h}\)是可学习的偏移参数。

细胞

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

\[\mathbf{C}_t = \mathbf{F}_t \odot \mathbf{C}_{t-1} + \mathbf{I}_t \odot \tilde{\mathbf{C}}_t\]

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

隐含状态

有了细胞以后,接下来我们还可以通过输出门来控制从细胞到隐含层变量\(\mathbf{H}_t \in \mathbb{R}^{n \times h}\)的信息的流动:

\[\mathbf{H}_t = \mathbf{O}_t \odot \text{tanh}(\mathbf{C}_t)\]

需要注意的是,当输出门近似1,细胞信息将传递到隐含层变量;当输出门近似0,细胞信息只自己保留。

输出层的设计可参照循环神经网络中的描述。

实验

为了实现并展示门控循环单元,我们依然使用周杰伦歌词数据集来训练模型作词。这里除长短期记忆以外的实现已在循环神经网络中介绍。

数据处理

我们先读取并对数据集做简单处理。

In [1]:
import zipfile
with zipfile.ZipFile('../data/jaychou_lyrics.txt.zip', 'r') as zin:
    zin.extractall('../data/')

with open('../data/jaychou_lyrics.txt') as f:
    corpus_chars = f.read()

corpus_chars = corpus_chars.replace('\n', ' ').replace('\r', ' ')
corpus_chars = corpus_chars[0:20000]

idx_to_char = list(set(corpus_chars))
char_to_idx = dict([(char, i) for i, char in enumerate(idx_to_char)])
corpus_indices = [char_to_idx[char] for char in corpus_chars]

vocab_size = len(char_to_idx)
print('vocab size:', vocab_size)
vocab size: 1465

我们使用onehot来将字符索引表示成向量。

In [2]:
def get_inputs(data):
    return [nd.one_hot(X, vocab_size) for X in data.T]

初始化模型参数

以下部分对模型参数进行初始化。参数hidden_dim定义了隐含状态的长度。

In [3]:
import mxnet as mx

# 尝试使用GPU
import sys
sys.path.append('..')
from mxnet import nd
import utils
ctx = utils.try_gpu()
print('Will use', ctx)

input_dim = vocab_size
# 隐含状态长度
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
Will use gpu(0)

定义模型

我们将前面的模型公式翻译成代码。

In [4]:
def lstm_rnn(inputs, state_h, state_c, *params):
    # 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

    H = state_h
    C = state_c
    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)

训练模型

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

In [5]:
seq1 = '分开'
seq2 = '不分开'
seq3 = '战争中部队'
seqs = [seq1, seq2, seq3]

utils.train_and_predict_rnn(rnn=lstm_rnn, is_random_iter=False, epochs=200,
                            num_steps=35, hidden_dim=hidden_dim,
                            learning_rate=0.2, clipping_norm=5,
                            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)
Epoch 20. Training perplexity 319.600635
 -  分开 我我的 我不的 我不的 我不的 我不的 我不的 我不的 我不的 我不的 我不的 我不的 我不的 我不的 我不的 我不的 我不的 我不的 我不的 我不的 我不的 我不的 我不的 我不的 我不的 我不的
 -  不分开 我我的 我不的 我不的 我不的 我不的 我不的 我不的 我不的 我不的 我不的 我不的 我不的 我不的 我不的 我不的 我不的 我不的 我不的 我不的 我不的 我不的 我不的 我不的 我不的 我不的
 -  战争中部队 我我的 我不的 我不的 我不的 我不的 我不的 我不的 我不的 我不的 我不的 我不的 我不的 我不的 我不的 我不的 我不的 我不的 我不的 我不的 我不的 我不的 我不的 我不的 我不的 我不的

Epoch 40. Training perplexity 176.650292
 -  分开 我想你的爱你 我不要你的爱爱 我想你的你你我的可爱女人 我想的让我想的可爱女人 我想的让我想的可爱女人 我想的让我想的可爱女人 我想的让我想的可爱女人 我想的让我想的可爱女人 我想的让我想的可爱女人
 -  不分开 我想你的爱你 我不要你的爱爱 我想你的你你我的可爱女人 我想的让我想的可爱女人 我想的让我想的可爱女人 我想的让我想的可爱女人 我想的让我想的可爱女人 我想的让我想的可爱女人 我想的让我想的可爱女人
 -  战争中部队 我想的让我不要的可爱女人 我想的让我想的可爱女人 我想的让我想的可爱女人 我想的让我想的可爱女人 我想的让我想的可爱女人 我想的让我想的可爱女人 我想的让我想的可爱女人 我想的让我想的可爱女人 我想的

Epoch 60. Training perplexity 71.759233
 -  分开 一直两的茶 有一种人的路 有哼在人 有人人 一直 停滚 停i xi xi xi xi xi xi xi xi xi xi xi xi xi xi xi xi xi xi xi xi xi xi xi
 -  不分开 我想你再想你开 我想你着你的你 我想你的你 我不要再你 我不要再你 我不不再不你 我不要你不你 我不要你不你 我不要你你 我不要再你 我不要再你 我不要再你 我不要再你 我不要再你 我不要再你 我不要
 -  战争中部队 我想你的爱 一一种味不叫 我想想 你不我 是你的这样 我不要再你 我不要再你 我不要再你 我不要再你 我不要再你 我不要再你 我不要再你 我不要再你 我不要再你 我不要再你 我不要再你 我不要再你 我

Epoch 80. Training perplexity 26.547355
 -  分开 你那那 的爱我 的想情 单对 单单 单想 对对 单对 单对 单想 单对 单想 单对 单i xi xi xi xi xi xi xi xi xi xi xi xi xi xi xi xi xi xi x
 -  不分开 我不要这样 我不要 你不 我不 我不 我不 我不 我不 我不 我不 我不 我不 我不 我不 我不 我不 我不 我不 我不 我不 我不 我不 我不 我不 我不 我不 我不 我不 我不 我不 我不 我不
 -  战争中部队 我想的一天 没有你不不 是你的我爱你的可爱女人 漂柔的让我疯狂的可爱女人 坏坏的让我疯动的可爱女人 坏坏的让我疯动的可爱女人 坏坏的让我疯动的可爱女人 坏坏的让我疯动的可爱女人 坏坏的让我疯动的可爱女

Epoch 100. Training perplexity 11.105056
 -  分开 他你开 爱爱我 说你怎么在我的手 还说你的泪里 想想想的你 我有在口人 有人有你的是我有一着我 你不会这样很美的想想知不美  我想你的想写的你元前 深埋在美索不达 想要我说你睡你 想要我说你睡你 想因
 -  不分开去去 我要你你已碎没人你擦眼眼泪 别管开身不能要你的爱有 我害在你想你 你的想你的爱你 想要我在的你有我 深埋在美 你的感里的可爱女人 坏坏的让我感狂的可爱女人 坏坏的让我疯狂的可爱女人 坏坏的让我疯狂
 -  战争中部队来 我想会你已碎没人你你眼你 你 那大你心单的你 你在你在你比我的想 我要你的爱你的美元前 我在在美不美 你想想你你睡难 想要你想再难要 我想想你你想要 想要我的你 我想要的口笑 想要你的泪 有人一起起

Epoch 120. Training perplexity 5.444350
 -  分开 他你那的太画 雨都是一直的背 我 你的小想 将多的美空义 哼哼哈兮 快使用双截棍 哼哼哈兮 快使用双截棍 哼哼哈兮 快使用双截棍 哼哼哈兮 快使用双截棍 哼哼哈兮 快使用双截棍 哼哼哈兮 快使用双截棍
 -  不分开去去 我想你你的微笑 天点伦梦的画面 我会一直好好过 你已经远远离开 我也会慢慢开开 我也着一直走 为什么我连分开都迁就着你 我真的没有天份 安静的没这么快 我会学着放弃你 是因为我太爱你 是因为我太爱
 -  战争中部队来 他不ㄟ 是沉的一步一天 默默来著著著 它许时字字边不拿 我在你的茶  一种味道叫做家 陆羽泡的茶 听说名墨的山水画 唐朝千年的风沙 现在还在刮 爷爷泡的茶 有一种味道叫做家 陆羽泡的茶 听说名和利都

Epoch 140. Training perplexity 3.193640
 -  分开 他你那的太忧 雨通变 一直睡 的表情 单纯变停永 回无坊 小弄堂 是属于那年代白墙黑瓦的淡淡的忧伤 消失的 旧时光 一九四三 回头看 的片段 有一些风霜 老唱盘 旧皮堂 装满了明年的白 还谦卑 让我到
 -  不分开去去 我想你你 你对心在在快样 我想悔能让你的口 这不再再 我想这难这 这样事的让情就我都何爱 你说你说 我不不再离 不没没这样我我多的可爱爱你 爱我说说 我没不烦 不没没这样 我不要的想快 快知不觉走
 -  战争中部队来 不想你笑的画度 然被还抱不起 爬望 失去觉义我 一于好好的象  一些风起起 人人底么去 我在一美主义 静的底 让我连恨都难 从檐走耳 为人耿在凉寞 一身正气 是谁之双截棍 哼哼哈兮 快使用双截棍 哼

Epoch 160. Training perplexity 2.181322
 -  分开 他你那的主妈争 风会时动的画面 然后还说的象  一么我说的睡  一忍雨柔滴 说经无回忆 我爱开美主义 又也许悄护奇 干开就会客) 开分了我的爱你 让什么(客) 我怀成起督开开 干什么(客) 干什么(客
 -  不分开去去 我想你你牵牵着你的手不放开 爱离开身非永远远句 我害轻你心碎的口 想想你说不得 我要要不分 我不要再想走 我不要再你走 我要要你走 强知着停你走才乐  迷事了愿险力B你走止止 我的世界的天份 你
 -  战争中部队来 他在床的茶防 听在名在半岛 我想分的脸 有一种味道叫做家 陆羽泡的茶 听幅泼墨的山水画 唐朝千年的风沙 现在还在刮 爷爷年年那 我一天美主义 又彻底 让我连恨难难 一檐正气 是我们轻过屋的我戏戏

Epoch 180. Training perplexity 1.701933
 -  分开 他你那真妈妈 又色底 娘手的日像语言 我想拳在二自拆口 我水古睡的国小 天容从 那容那人安移 一直裂缝 全谁之日 木日 一箩筐 木炭 一直放 木炭 剩一半 火炉烫 小铁匠存钱买期望 在流汗 巴洛克建筑
 -  不分开去去 想想你陪汉我 他因是你都舍过 想要我的爱你我妈都来难得 为我在这样陪我 你看我说说对他 不爱担担担忧 我不会慢慢你 是你都 你爱要 开对这!-!我妈妈 爱爱~多多你 像不该该你 是你的让我面的可爱
 -  战争中部队来 不着你笑不觉 让我带在你离离 他他你的爱爱 我会一起好好着是就能能到去 我心的这天己 开开没没没比 没有一邻气走猜能 在我着着你 我很的爱不想 (没有没发不再快 我想着你牵牵牵着走 我要我也不碎 我

Epoch 200. Training perplexity 1.397795
 -  分开 他你面的妈妈争 关会就动的茶 马一种风叫叫 我想分 让想常 的眼情只一起 一直英 他过是那剩是 龙在骷 ㄌㄚ到不前 那身族人写相祈 让我们 半兽人 的灵魂 翻滚 停止忿恨 永无止尽的战争 让我们 半兽
 -  不分开去去 我想你你 你对一定我 这样的节样 我爱你的脸活 我想你 你爱走 想不要的爱爱 爱知不好 快快没这后快 静你后悄我开离 静开着我才离开 心静的笑梦依娇 我会好地有气用天 为为为听 你对你有多重重 我
 -  战争中部队止 不右你笑只掉了浪鸥水止泪 我的世界已狂风风 W办累断的泪眼的能勾 谁有在很翼不住 一只太和和你谁妈 我要你的话 一风将然的泪 我想可远 快我的太婆家 一起看着日落 一直抢我们都睡着 我想就这着你 对

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

结论

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

练习

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

吐槽和讨论欢迎点这里