长短期记忆(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.860099
 -  分开 我我的 我不的 我不的 我我的 我不的 我不的 我我的 我不的 我不的 我我的 我不的 我不的 我我的 我不的 我不的 我我的 我不的 我不的 我我的 我不的 我不的 我我的 我不的 我不的 我我的
 -  不分开 我我的 我不的 我不的 我我的 我不的 我不的 我我的 我不的 我不的 我我的 我不的 我不的 我我的 我不的 我不的 我我的 我不的 我不的 我我的 我不的 我不的 我我的 我不的 我不的 我我的
 -  战争中部队 我我的 我不的 我不的 我我的 我不的 我不的 我我的 我不的 我不的 我我的 我不的 我不的 我我的 我不的 我不的 我我的 我不的 我不的 我我的 我不的 我不的 我我的 我不的 我不的 我我的

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

Epoch 60. Training perplexity 73.238507
 -  分开 我想你再想 我的天界 我想了这了我 这样 我想我要你的可爱女人 我不能这不 我的世界 你不了我的快 我想好觉 我的世界 的灵魂 我想 我想 我不了我的天 我有你 我想我的可快 我想好打 我想了这球 我
 -  不分开 我不是我的天 我想想你的天 我不会这样 我的让我有你的可爱 我想你说你 我不会再想 我不着我 我不要这样 我不不觉 我不了这样 我不了这样 我不会这样 我不会这样 我不会这样 我不会这样 我不会这样
 -  战争中部队 我想你的爱 我一种味道叫 一直在直 我有一种  一直在空  一直用空 的一种 有一种 一直 一步 xi xi xi xi xi xi xi xi xi xi xi xi xi xi xi xi xi

Epoch 80. Training perplexity 29.642317
 -  分开 我不要这想我 这不了没不 我想想要的想活 不不是我 我不要再想 我不好这样 我不好好不活 不不不觉 我不了这生我 不知后觉 我的没有好活 我想好好 我不要这爱我的爱你就好 不不不受 我想的一条 听 不
 -  不分开 我不要 爱你有我的想快有 这样在我想了我的怒 我想 你想我很想着 你 在!!!单单 说 在我的想想 说你的这样 我想了这样的没有 我说不想 我想 我不  不不是我不要 不不是这样我的快知 我想想想想的
 -  战争中部队 我想你的爱你 让你在在不不 我想 你不 我不 我不 我不再这不么我 不不不再想开的怒知 我想想说你 我的定 别不  单不么停样 说不在我想我的想 我想 你不再 我不了这不是我 不不不再 我不了这不我

Epoch 100. Training perplexity 11.348488
 -  分开 我有能这想开 是这样的太情 我会会好好 我不能 爱不走 我不了这样活 后知不觉 我该了好生活 后知好觉 我该好好生活 我知好好生活 不知不觉 我已经离开开 不知不觉 我跟了这生活 我知好好生活 不知不
 -  不分开的可人 我想揍你的经 我想不能 你不能 我不 我不能 爱情走的太快就像龙卷风 不能承受我已已可可 我不要 你不再我 想不要的样样 不知不觉 我不了这生活 我知好觉 我知了好生活 我知好好生活 不知不觉
 -  战争中部队就) 这真到我不已 让你着着你不到 不会不要 你已经难难球 不知不觉 我跟了这节活 后知后觉 我该好好生活 我知好好生活 不知不觉 我已经离开开 不知不觉 我该了这生活 我知好好生活 不知不觉 我已经离

Epoch 120. Training perplexity 5.575671
 -  分开 我有经这口堡开 想想你 说远我 我想你 我不怎简!! 我该该 你爱你 说不是我不想 不知不是我我 说你  你想简考了我 说散 我想很了我活 说你 你想很久了吧? 我给你的黑色幽幽  想 我想很睡了我
 -  不分开的甜戏 我想要你的子人 你的那你已已离的想 想要像你在我想要 回不躲 你的眼眼睛的想想 (发抖惚三三的民实  我有你在经着一定的想空 和点的黑我 说你的对里 我想你已见的的非明明明 温柔的让我心疼的可爱
 -  战争中部队就了 这那我这的世知意 安被一直在三过 也么就有一起 我的天界被被的雨 想亲像老的国 我的天美主义 雨彻过 一场两好三步步望望著天 看星星 一颗两颗三颗四颗 连成线背著背默默许下心愿 看远方的星是一听的

Epoch 140. Training perplexity 3.069970
 -  分开 这天在没不要 家色看着我们你 他入了证的防防 随你变旁的画人 不会说说把人 让我们回起 没人着人后 没人有美你的回色有你 你说我的想 你想的美 听不能 你不了难我有难 我说好多手的让  说我在眼不得得
 -  不分开你的手样好 我想知的爱息 你没着你不得 如说 你说你不了我 说散的话里么快说 别怪我多多违的痛 周默去底里里来找现 在一个 不不会多多要我 当我去没没堡  说穿了其实我的愿望就怎么小 就怎么每天祈祷我的
 -  战争中部队就) 我真的这有你就封记 你才ㄟ你会子往 我对你每分的小 小亲么眼里已得 我有一起来好堡 就经么远远离开 我用会慢慢走开 是什么我连分开都迁就着你 我真的没有天份 安静的没这么快 我会学着放弃你 是因为

Epoch 160. Training perplexity 2.131113
 -  分开了天的手 走轻再过去 我就抱事里离是 想要我再见 我不是逃忆 我的天事主你离过 没留帮着你走才快乐 爷时的笑 将将名 一多的公的画道 一待英雄的风 一本名在半岛铁铁 放在床 边堆好多 第一页第六页第七页
 -  不分开就走 想要走慢放开开下下的天 沉点的喊叫天默安静的那 公好了说来 不来我就就走 我想要再的模 我不能好你 我已没人 你不要的些笑 我该会觉生活 你的没你 你不离 分球我的起球就了 等是 泪离让我的吧实
 -  战争中部队堂) 想回到过去 试着抱事 挡着它动 蜥有一空中 你一种空 全面了空 木慢中空 不再再动 一一中中 不再再动 恨没没人 不想放动 恨没没有 一一放中 不再再同 全不放 一一放 木炭放痛 恨止止动 没有放

Epoch 180. Training perplexity 1.632437
 -  分开 这面开的梦铜我 何色 看又 三子一人在我的地道 等暗小暗离物黄黄 花起的全 如果时觉的画画  山朝的父静欣口 在静的爹的早征 不会从扫把的胖女 还要走白念过 让我带起国不到 没有你有话烧偎一种悲剧 也
 -  不分开就走 这手慢慢交给我 放下心中的困惑 雨点从两旁划过 割开两种精神的我 经过老伯的家 篮框变得好高 爬过的那棵树 又何时变得渺小 这样也好 开始没人注意到我 等雨变强之前 我将将会分化软 弱时 过去黄口
 -  战争中部队就旁 想可你么怎 那一种应在黑暗家乡道 你说你的想防我的想叫现了这道 为你么回去 一天应故事继续 至少不再让你我我而去 分散时间的注意 这次会抱得更紧 这样挽留不知还来不来得及 想回到过去 试默不断跃继

Epoch 200. Training perplexity 1.372898
 -  分开 这面在那眼铜 我就想起生生 这样么风队友 我透到任远 强天着一个友 我的水里主义的风 什么透眼眼 我的手界被被摧毁 至记不与觉违 让我们失忆不开 没有了人明依偎 江越着河南神的快找 在我在天的想  那
 -  不分开就走 把手慢慢交给我 放下心中的困惑 雨点从两旁划过 割开两种精神的我 经过老伯的家 篮框变得好高 爬过的那棵树 又何时变得渺小 这样也好 开始没人注意到你我 等雨变强之前 我们将会分化软弱 趁时间没发
 -  战争中部队就了 它那云樟 我试著努努打你奔跑多多的难定  过过下权来真的我回边 我给你的爱写在西元前 深埋在美索不达米亚平原 我一个世字后下土土远 那板上的字迹依然清晰可见 我给你的爱写在西元前 深埋在美索不达米

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

结论

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

练习

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

吐槽和讨论欢迎点这里