NLP (三): RNN (Recurrent Neural Network, 循环神经网络)

本文为《深度学习进阶: 自然语言处理》的读书笔记

前馈型神经网络 (feedforward neural network, FNN)

  • 前馈型神经网络是指网络的传播方向是单向的。具体地说,先将输入信号传给下一层(隐藏层),接收到信号的层也同样传给下一层,然后再传给下一层……
  • 虽然前馈网络结构简单、易于理解,但是可以应用于许多任务中。不过,这种网络存在一个大问题,就是不能很好地处理时间序列数据 (时序数据)。更确切地说,单纯的前馈网络无法充分学习时序数据的模式。于是,RNN 便应运而生

概率和语言模型 (language model)

语言模型 (language model)

  • 语言模型给出了单词序列发生的概率. e.g.,对于 “you say goodbye” 这一单词序列,语言模型给出高概率;对于 “you say good die” 这一单词序列,模型则给出低概率
    • 语言模型可以应用于多种应用,典型的例子有机器翻译语音识别。比如,语音识别系统会根据人的发言生成多个句子作为候选。此时,使用语言模型,可以按照 “作为句子是否自然” 这一基准对候选句子进行排序
    • 语言模型也可以用于生成新的句子。因为语言模型可以使用概率来评价单词序列的自然程度,所以它可以根据这一概率分布采样单词

语言模型 - 联合概率

在这里插入图片描述

为了简化数学式,这里记 P ( w 1 ) P(w_1) P(w1) P ( w 1 ∣ w 0 ) P(w_1 | w_0) P(w1w0)

  • 如上所示,联合概率可以由后验概率的乘积表示,因此如果能计算出后验概率 P ( w y ∣ w 1 , . . . , w t − 1 ) P(w_y|w_1,...,w_{t-1}) P(wyw1,...,wt1),就能求得语言模型的联合概率 P ( w 1 , . . . , w m ) P(w_1,... , w_m) P(w1,...,wm)
  • Markov Assumption ( k k k-order Markov):
    P ( w 1 , . . . , w m ) = ∏ t = 1 m P ( w t ∣ w t − 1 . . . w t − k ) P(w_1,...,w_m)=\prod_{t=1}^mP(w_t|w_{t-1}...w_{t-k}) P(w1,...,wm)=t=1mP(wtwt1...wtk)Unigram model (Simplest case)
    P ( w 1 , . . . , w m ) = ∏ t = 1 m P ( w t ) P(w_1,...,w_m)=\prod_{t=1}^mP(w_t) P(w1,...,wm)=t=1mP(wt)Bigram model ( 1 1 1-order Markov)
    P ( w 1 , . . . , w m ) = ∏ t = 1 m P ( w t ∣ w t − 1 ) P(w_1,...,w_m)=\prod_{t=1}^mP(w_t|w_{t-1}) P(w1,...,wm)=t=1mP(wtwt1) N N N-gram models
    P ( w 1 , . . . , w m ) = ∏ t = 1 m P ( w t ∣ w t − 1 . . . w t − N ) P(w_1,...,w_m)=\prod_{t=1}^mP(w_t|w_{t-1}...w_{t-N}) P(w1,...,wm)=t=1mP(wtwt1...wtN)

将 CBOW 模型用作语言模型?

  • 如果要强行把 CBOW 模型用作语言模型,可以通过将上下文的大小限制在某个值来近似实现 (i.e. 假设语言模型满足 n n n 阶马尔可夫链). 下式以使用 2 个单词作为上下文为例,此时 CBOW 模型的任务就变成了由前两个单词预测后一个单词:
    在这里插入图片描述
  • 显然,如果上下文窗口过小,模型将无法很好地捕捉语义信息。但把窗口值调得很大就能解决问题吗?
    • 实际上还是不行! 这是因为 CBOW 模型忽视了上下文中单词顺序. 如图 5-5 的左图所示,在 CBOW 模型的中间层求单词向量的和,因此上下文的单词顺序会被忽视
    • 我们想要的是考虑了上下文中单词顺序的模型。为此,可以像图 5-5 中的右图那样,在中间层 “拼接” 上下文的单词向量 (A Neural Probabilistic Language Model)。但如果采用拼接的方法,权重参数的数量将与上下文大小成比例地增加。显然,这是我们不愿意看到的
      在这里插入图片描述

实际上 word2vec 是在 RNN 之后提出的,主要目的是为了应对词汇量的增加,提高分布式表示的质量

简单 RNN / Elman

RNN

RNN 层

  • RNN 的特征就在于拥有一个环路。这个环路可以使数据不断循环。通过数据的循环,RNN 一边记住过去的数据,一边更新到最新的数据

下面我们将 RNN 中使用的层称为 “RNN 层


RNN 层

在这里插入图片描述

  • 设时刻 t t t 的输入是 x t x_t xt ( x t x_t xt 可以是单词的分布式表示),则时序数据 ( x 0 , x 1 , , , , , x t , . . . ) (x_0, x_1,,,, , x_t, ...) (x0,x1,,,,,xt,...) 会被输入到层中。然后,以与输入对应的形式,输出 ( h 0 , h 1 , , , , , h t , . . . ) (h_0, h_1,,,, , h_t, ...) (h0,h1,,,,,ht,...)。同时注意到,输出有两个分叉,这意味着同一个东西被复制了。输出中的一个分叉将成为其自身的输入

展开循环

在这里插入图片描述

  • 可以看出,各个时刻的 RNN 层接收传给该层的输入和前一个 RNN 层的输出,然后据此计算当前时刻的输出,此时进行的计算可以用下式表示:
    在这里插入图片描述其中, W x W_x Wx, W h W_h Wh, b b b 为 RNN 层的权重与偏置参数, h t h_t ht, x t x_t xt 均为行向量
    • RNN 的输出 h t h_t ht 称为隐藏状态 (hidden state)隐藏状态向量 (hidden state vector)。RNN 可以看作具有 “状态” h h h,并以式 (5.9) 的形式被更新。这就是说 RNN 层是 “具有状态的层” 或“具有存储 (记忆) 的层” 的原因
  • RNN 层的输出 h t h_t ht 一方面向上输出到另一个层,另一方面向右输出到下一个 RNN 层 (自身)

Backpropagation Through Time (BPTT)

  • 将 RNN 层展开后,就可以视为在水平方向上延伸的神经网络,因此 RNN 的学习可以用与普通神经网络的学习相同的方式进行
    在这里插入图片描述因为这里的误差反向传播法是 “按时间顺序展开的神经网络的误差反向传播法”,所以称为 Backpropagation Through Time
  • 通过该 BPTT,RNN 的学习似乎可以进行了,但是在这之前还有一个必须解决的问题,那就是学习长时序数据的问题。因为随着时序数据的时间跨度的增大,BPTT 消耗的计算机资源也会成比例地增大 (要基于 BPTT 求梯度,必须在内存中保存各个时刻的 RNN 层的中间数据)。另外,反向传播的梯度也会变得不稳定 (梯度消失 / 梯度爆炸) (e.g. 如果时序数据长度为 1000,水平方向上就变成了一个 1000 层的深度神经网络)

Truncated BPTT

在维持正向传播的连接的同时,以块为单位应用误差反向传播法

  • 在处理长时序数据时,通常的做法是将时间轴方向上过长的网络在合适的位置进行截断,从而创建多个小型网络,然后以截出来的小型网络为单位执行误差反向传播法
    • 注意:只是反向传播的连接被截断,正向传播的连接依然被维持

在这里插入图片描述


在 RNN 执行 Truncated BPTT 时,数据需要按顺序输入

一般将语料库中多个串联起来的句子当作一个时序数据

  • 现在,我们考虑使用 Truncated BPTT 来学习 RNN。我们首先要做的是,将第 1 个块的输入数据 ( x 0 , . . . , x 9 ) (x_0,..., x_9) (x0,...,x9) 输入 RNN 层,按顺序进行正向传播和反向传播
    在这里插入图片描述
  • 接着,对下一个块的输入数据 ( x 10 , x 11 , . . . , x 19 ) (x_{10}, x_{11},... , x_{19}) (x10,x11,...,x19) 执行误差反向传播法 (由于正向传播时需要用到第一个块最后的隐藏状态 h 9 h_9 h9,因此必须等第一个块处理完才能处理第二个块)
    在这里插入图片描述
  • 用同样的方法,继续学习第 3 个块。像这样,在 RNN 的学习中,通过将数据按顺序输入,从而继承隐藏状态进行学习 (如下图所示,Truncated BPTT 的学习方式可以看作一个多级流水线,如果支持多线程处理的话,是可以并行的)
    在这里插入图片描述

Truncated BPTT 的 mini-batch 学习

  • 如果将语料库中所有句子串联成一个很长的时序数据,那么就相当于是在做批大小为 1 的 mini-batch 学习。如果想要考虑批数据,那么就可以平移各批次输入数据的开始位置,按顺序输入。此外,如果在按顺序输入数据的过程中遇到了结尾,则需要设法返回头部
    • e.g. 对长度为 1000 的时序数据,以时间长度 10 为单位进行截断。如果批大小设为 2,那么第 1 笔样本数据从头开始按顺序输入,第 2 笔数据从第 500 个数据开始按顺序输入
      在这里插入图片描述

RNN 的实现

Time RNN 层

  • 考虑到基于 Truncated BPTT 的学习,我们只需要创建一个在水平方向上长度固定的网络序列即可。考虑到模块化,可以将水平方向上延伸的神经网络实现为 “一个层”,称这种一次处理 T T T 步的层为 Time RNN 层
    在这里插入图片描述

下面,我们首先实现进行 RNN 单步处理的 RNN 类;然后,利用这个 RNN 类,完成一次进行 T T T 步处理的 TimeRNN 类

RNN 层的实现

在这里插入图片描述

class RNN:
    def __init__(self, Wx, Wh, b):
    	# shape: Wx (D x H), Wh (H x H), b (1 x H)
    	# 	其中,D 为词向量的维数
        self.params = [Wx, Wh, b]
        self.grads = [np.zeros_like(Wx), np.zeros_like(Wh), np.zeros_like(b)]
        self.cache = None

    def forward(self, x, h_prev):
        Wx, Wh, b = self.params
        t = np.dot(h_prev, Wh) + np.dot(x, Wx) + b
        h_next = np.tanh(t)

        self.cache = (x, h_prev, h_next)
        return h_next

    def backward(self, dh_next):
        Wx, Wh, b = self.params
        x, h_prev, h_next = self.cache

        dt = dh_next * (1 - h_next ** 2)
        db = np.sum(dt, axis=0)
        dWh = np.dot(h_prev.T, dt)
        dh_prev = np.dot(dt, Wh.T)
        dWx = np.dot(x.T, dt)
        dx = np.dot(dt, Wx.T)

        self.grads[0][...] = dWx
        self.grads[1][...] = dWh
        self.grads[2][...] = db

        return dx, dh_prev

Time RNN 层的实现

在这里插入图片描述


  • 实现时可以将 RNN 层的隐藏状态 h h h 保存在成员变量中,在进行隐藏状态的 “继承” 时会用到它
    在这里插入图片描述同理,反向传播的梯度 d h dh dh 也保存在成员变量中:
    在这里插入图片描述
class TimeRNN:
    def __init__(self, Wx, Wh, b, stateful=False):
        self.params = [Wx, Wh, b]
        self.grads = [np.zeros_like(Wx), np.zeros_like(Wh), np.zeros_like(b)]
        self.layers = None	# 保存多个 RNN 层

        self.h = None	# 保存调用 forward() 方法时的最后一个 RNN 层的隐藏状态
        self.dh = None	# 保存流向上一时刻的隐藏状态的梯度 (在 seq2seq 中会用到它)
        self.stateful = stateful	# 控制是否继承隐藏状态 (有状态 / 无状态)

    def forward(self, xs):
        Wx, Wh, b = self.params
        N, T, D = xs.shape
        D, H = Wx.shape

        self.layers = []
        hs = np.empty((N, T, H), dtype='f')

        if not self.stateful or self.h is None:
        	# 不继承上一个块的隐藏状态
            self.h = np.zeros((N, H), dtype='f')

		# self.h 中存放最后一个 RNN 层的隐藏状态。在 stateful 为 True 的情况下,
		# 下一次调用 forward() 方法时,刚才的 self.h 将被继续使用
        for t in range(T):
            layer = RNN(*self.params)
            self.h = layer.forward(xs[:, t, :], self.h)
            hs[:, t, :] = self.h
            self.layers.append(layer)

        return hs

    def backward(self, dhs):
        Wx, Wh, b = self.params
        N, T, H = dhs.shape
        D, H = Wx.shape

        dxs = np.empty((N, T, D), dtype='f')
        dh = 0
        grads = [0, 0, 0]
        for t in reversed(range(T)):
            layer = self.layers[t]
            dx, dh = layer.backward(dhs[:, t, :] + dh)
            dxs[:, t, :] = dx

            for i, grad in enumerate(layer.grads):
                grads[i] += grad

        for i, grad in enumerate(grads):
            self.grads[i][...] = grad
        self.dh = dh

        return dxs

    def set_state(self, h):
        self.h = h

    def reset_state(self):
        self.h = None

RNNLM (RNN Language Model)

RNNLM 的网络架构

在这里插入图片描述

  • Embedding 层: 将单词 ID 转化为单词的分布式表示
  • RNN 层: 将单词向量转化为隐藏状态,同时也向下一时刻的 RNN 层输出隐藏状态
  • Affine + Softmax 层: 输入隐藏状态,输出下一个单词的概率分布

在这里插入图片描述

Time 层的实现

  • 之前我们将整体处理时序数据的层实现为了 Time RNN 层,这里也同样使用 Time Embedding 层、Time Affine 层等来实现整体处理时序数据的层。一旦创建了这些 Time 层,我们的目标神经网络就可以像下图这样实现:
    在这里插入图片描述
Time Affine 层

在这里插入图片描述

class TimeAffine:
    def __init__(self, W, b):
        self.params = [W, b]
        self.grads = [np.zeros_like(W), np.zeros_like(b)]
        self.x = None

    def forward(self, x):
        N, T, D = x.shape
        W, b = self.params

		# 这里并不是单纯地使用 T 个 Affine 层,
		# 而是使用矩阵运算实现了高效的整体处理
        rx = x.reshape(N*T, -1)
        out = np.dot(rx, W) + b
        self.x = x
        return out.reshape(N, T, -1)

    def backward(self, dout):
        x = self.x
        N, T, D = x.shape
        W, b = self.params

        dout = dout.reshape(N*T, -1)
        rx = x.reshape(N*T, -1)

        db = np.sum(dout, axis=0)
        dW = np.dot(rx.T, dout)
        dx = np.dot(dout, W.T)
        dx = dx.reshape(*x.shape)

        self.grads[0][...] = dW
        self.grads[1][...] = db

        return dx
Time Embedding 层
  • Time Embedding 层由 T T T 个 Embedding 层组成
class TimeEmbedding:
    def __init__(self, W):
        self.params = [W]
        self.grads = [np.zeros_like(W)]
        self.layers = None
        self.W = W

    def forward(self, xs):
        N, T = xs.shape
        V, D = self.W.shape

        out = np.empty((N, T, D), dtype='f')
        self.layers = []

        for t in range(T):
            layer = Embedding(self.W)
            out[:, t, :] = layer.forward(xs[:, t])
            self.layers.append(layer)

        return out

    def backward(self, dout):
        N, T, D = dout.shape

        grad = 0
        for t in range(T):
            layer = self.layers[t]
            layer.backward(dout[:, t, :])
            grad += layer.grads[0]

        self.grads[0][...] = grad
        return None
Time Softmax with Loss 层

在这里插入图片描述

class TimeSoftmaxWithLoss:
    def __init__(self):
        self.params, self.grads = [], []
        self.cache = None
        self.ignore_label = -1

    def forward(self, xs, ts):
        N, T, V = xs.shape

        if ts.ndim == 3:  # 在监督标签为one-hot向量的情况下
            ts = ts.argmax(axis=2)

        mask = (ts != self.ignore_label)

        # 按批次大小和时序大小进行整理(reshape)
        xs = xs.reshape(N * T, V)
        ts = ts.reshape(N * T)
        mask = mask.reshape(N * T)

        ys = softmax(xs)
        ls = np.log(ys[np.arange(N * T), ts])
        ls *= mask  # 与ignore_label相应的数据将损失设为0
        loss = -np.sum(ls)
        loss /= mask.sum()

        self.cache = (ts, ys, mask, (N, T, V))
        return loss

    def backward(self, dout=1):
        ts, ys, mask, (N, T, V) = self.cache

        dx = ys
        dx[np.arange(N * T), ts] -= 1	# Softmax 反向传播: y - t
        dx *= dout
        dx /= mask.sum()				
        dx *= mask[:, np.newaxis]  		# 与 ignore_label 相应的数据将梯度设为 0

        dx = dx.reshape((N, T, V))

        return dx

RNNLM 的实现

在这里插入图片描述

# SimpleRnnlm 只需组合前面实现的 Time 层即可
class SimpleRnnlm:
    def __init__(self, vocab_size, wordvec_size, hidden_size):
        V, D, H = vocab_size, wordvec_size, hidden_size
        rn = np.random.randn

        # 初始化权重 (这是一个简略版的实现,原始论文中提出的权重初始值还考虑了下一层的节点数)
        # 使用 Xavier初始值:在上一层有 n 个节点的情况下,使用标准差为 1/sqrt(n) 的分布作为初始值
        # 在语言模型的相关研究中,也经常使用 0.01 * np.random.uniform(...) 这样的经过缩放的均匀分布
        embed_W = (rn(V, D) / 100).astype('f')
        rnn_Wx = (rn(D, H) / np.sqrt(D)).astype('f')
        rnn_Wh = (rn(H, H) / np.sqrt(H)).astype('f')
        rnn_b = np.zeros(H).astype('f')
        affine_W = (rn(H, V) / np.sqrt(H)).astype('f')
        affine_b = np.zeros(V).astype('f')

        # 生成层
        self.layers = [
            TimeEmbedding(embed_W),
            TimeRNN(rnn_Wx, rnn_Wh, rnn_b, stateful=True),
            TimeAffine(affine_W, affine_b)
        ]
        self.loss_layer = TimeSoftmaxWithLoss()
        self.rnn_layer = self.layers[1]

        # 将所有的权重和梯度整理到列表中
        self.params, self.grads = [], []
        for layer in self.layers:
            self.params += layer.params
            self.grads += layer.grads

    def forward(self, xs, ts):
        for layer in self.layers:
            xs = layer.forward(xs)
        loss = self.loss_layer.forward(xs, ts)
        return loss

    def backward(self, dout=1):
        dout = self.loss_layer.backward(dout)
        for layer in reversed(self.layers):
            dout = layer.backward(dout)
        return dout

    def reset_state(self):
        self.rnn_layer.reset_state()

语言模型的评价 - 困惑度 (perplexity)

  • 语言模型基于给定的已经出现的单词 (信息) 输出将要出现的单词的概率分布。困惑度 常被用作评价语言模型的预测性能的指标
    在这里插入图片描述由上式可知,困惑度越小越好,最小值为 1
    • N = 1 N=1 N=1,即数据量为 1 时,困惑度 = e − log ⁡ y k = 1 y k =e^{-\log y_k}=\frac{1}{y_k} =elogyk=yk1,也就是正确概率值的倒数。以 “you say goodbye and i say hello.” 这一语料库为例,下图中模型一的困惑度为 1 / 0.8 = 1.25 1/0.8=1.25 1/0.8=1.25,模型二的困惑度为 1 / 0.2 = 5 1/0.2=5 1/0.2=5. 那么,如何直观地解释值 1.25 和 5.0 呢?它们可以解释为 “分叉度”。所谓分叉度,是指下一个可以选择的选项的数量(下一个可能出现的单词的候选个数)。在刚才的例子中,好的预测模型的分叉度是 1.25,这意味着下一个要出现的单词的候选个数可以控制在 1 个左右。而在差的模型中,下一个单词的候选个数有 5 个
      在这里插入图片描述
    • 在信息论领域,困惑度也称为 “平均分叉度”。这可以解释为,数据量为 1 时的分叉度是数据量为 N N N 时的分叉度的平均值

RNNLM 的学习

  • 下面,我们使用 PTB 数据集进行学习,不过这里仅使用 PTB 数据集的前 1000 个单词。这是因为在本节实现的 RNNLM 中,即便使用所有的训练数据,也得不出好的结果。下一章我们将对它进行改进
# coding: utf-8
import sys
sys.path.append('..')
import matplotlib.pyplot as plt
import numpy as np
from common.optimizer import SGD
from dataset import ptb
from simple_rnnlm import SimpleRnnlm


# 设定超参数
batch_size = 10
wordvec_size = 100
hidden_size = 100
time_size = 5  # Truncated BPTT 的时间跨度大小
lr = 0.1
max_epoch = 100

# 读入训练数据(缩小了数据集)
corpus, word_to_id, id_to_word = ptb.load_data('train')
corpus_size = 1000
corpus = corpus[:corpus_size]
vocab_size = int(max(corpus) + 1)

xs = corpus[:-1]  	# 输入
ts = corpus[1:]  	# 输出(监督标签)
data_size = len(xs)
print('corpus size: %d, vocabulary size: %d' % (corpus_size, vocab_size))

# 学习用的参数
max_iters = data_size // (batch_size * time_size)	# n_itr for one epoch
time_idx = 0
total_loss = 0
loss_count = 0
ppl_list = []

# 生成模型
model = SimpleRnnlm(vocab_size, wordvec_size, hidden_size)
optimizer = SGD(lr)

# 计算读入 mini-batch 的各笔样本数据的开始位置
jump = (corpus_size - 1) // batch_size
offsets = [i * jump for i in range(batch_size)]

for epoch in range(max_epoch):
    for iter in range(max_iters):
        # 获取 mini-batch
        batch_x = np.empty((batch_size, time_size), dtype='i')
        batch_t = np.empty((batch_size, time_size), dtype='i')
        for t in range(time_size):
            for i, offset in enumerate(offsets):
                batch_x[i, t] = xs[(offset + time_idx) % data_size]
                batch_t[i, t] = ts[(offset + time_idx) % data_size]
            time_idx += 1

        # 计算梯度,更新参数
        loss = model.forward(batch_x, batch_t)
        model.backward()
        optimizer.update(model.params, model.grads)
        total_loss += loss
        loss_count += 1

    # 各个 epoch 的困惑度评价
    ppl = np.exp(total_loss / loss_count)
    print('| epoch %d | perplexity %.2f'
          % (epoch+1, ppl))
    ppl_list.append(float(ppl))
    total_loss, loss_count = 0, 0

# 绘制困惑度曲线
x = np.arange(len(ppl_list))
plt.plot(x, ppl_list, label='train')
plt.xlabel('epochs')
plt.ylabel('perplexity')
plt.show()

  • 可以将上面的流程封装在 RnnlmTrainer 类中,封装后代码如下:
# coding: utf-8
import sys
sys.path.append('..')
from common.optimizer import SGD
from common.trainer import RnnlmTrainer
from dataset import ptb
from simple_rnnlm import SimpleRnnlm


# 设定超参数
batch_size = 10
wordvec_size = 100
hidden_size = 100  # RNN的隐藏状态向量的元素个数
time_size = 5  # RNN的展开大小
lr = 0.1
max_epoch = 100

# 读入训练数据
corpus, word_to_id, id_to_word = ptb.load_data('train')
corpus_size = 1000  # 缩小测试用的数据集
corpus = corpus[:corpus_size]
vocab_size = int(max(corpus) + 1)
xs = corpus[:-1]  # 输入
ts = corpus[1:]  # 输出(监督标签)

# 生成模型
model = SimpleRnnlm(vocab_size, wordvec_size, hidden_size)
optimizer = SGD(lr)
trainer = RnnlmTrainer(model, optimizer)

trainer.fit(xs, ts, max_epoch, batch_size, time_size)
trainer.plot()

Gated RNN

  • 上面介绍的简单 RNN 的效果并不好。原因在于,许多情况下它都无法很好地学习到时序数据的长期依赖关系。现在,简单 RNN 经常被名为 LSTMGRU 的层所代替,LSTM 和 GRU 中增加了一种名为 “” 的结构。基于这个门,可以学习到时序数据的长期依赖关系

实际上,当我们说 RNN 时,更多的是指 LSTM 层,而不是之前介绍的简单 RNN

简单 RNN 的问题 - 梯度消失和梯度爆炸

在这里插入图片描述

  • 这里考虑长度为 T T T 的时序数据,关注从第 T T T 个正确解标签传递出的梯度如何变化。此时,关注时间方向上的梯度,可知反向传播的梯度流经 tanh、“+” 和 MatMul 运算。“+” 的反向传播将上游传来的梯度原样传给下游,因此我们重点考察 tanh 和 MatMul 运算的反向传播

tanh 运算的反向传播

  • y = t a n h ( x ) y = tanh(x) y=tanh(x) 时,它的导数是 d y d x = 1 − y 2 \frac{dy}{dx}=1-y^2 dxdy=1y2,如下图所示:
    在这里插入图片描述可见,导数值始终是小于 1 的,这意味着,当反向传播的梯度经过 tanh 节点时,它的值会越来越小

如果激活函数改为 ReLU,则有希望抑制梯度消失的问题 (paper: Improving performance of recurrent neural network with relu nonlinearity)

MatMul 运算的反向传播

在这里插入图片描述

  • 简单地分析一下上面的矩阵连乘:如果对 W h W_h Wh 作奇异值分解,即 W h = U Σ V T W_h=U\Sigma V^T Wh=UΣVT,则 W h   d h T = U Σ V T d h T W_h\ dh^T=U\Sigma V^Tdh^T Wh dhT=UΣVTdhT。又因为正交变换不改变向量长度,因此 ∣ ∣ W h   d h T ∣ ∣ = ∣ ∣ U Σ V T d h T ∣ ∣ ||W_h\ dh^T||=||U\Sigma V^Tdh^T|| ∣∣Wh dhT∣∣=∣∣UΣVTdhT∣∣ 就取决于 Σ \Sigma Σ,即 W h W_h Wh 的奇异值。如果 W h W_h Wh 的奇异值都是小于 1 的,那么很显然 d h T dh^T dhT 在经过线性变换 W h W_h Wh 后长度一定是变小的;而如果 W h W_h Wh 的最大奇异值 (谱范数) 大于 1,那么 d h T dh^T dhT 在经过线性变换 W h W_h Wh 后长度可能变大。也就是说,如果 W h W_h Wh 的最大奇异值大于 1,那么可能会产生梯度爆炸,反之则一定会产生梯度消失
  • On the difficulty of training Recurrent Neural Networks 中详细探讨了 RNN 的梯度消失和梯度爆炸问题
# coding: utf-8
import numpy as np
import matplotlib.pyplot as plt


N = 2  # mini-batch的大小
H = 3  # 隐藏状态向量的维数
T = 20  # 时序数据的长度

dh = np.ones((N, H))

np.random.seed(3)

Wh = np.random.randn(H, H)
#Wh = np.random.randn(H, H) * 0.5

norm_list = []
for t in range(T):
    dh = np.dot(dh, Wh.T)
    norm = np.sqrt(np.sum(dh**2)) / N	# 记录梯度的 L2 范数
    norm_list.append(norm)

print(norm_list)

# 绘制图形
plt.plot(np.arange(len(norm_list)), norm_list)
plt.xticks([0, 4, 9, 14, 19], [1, 5, 10, 15, 20])
plt.xlabel('time step')
plt.ylabel('norm')
plt.show()
  • 如果 W h W_h Wh 初始化为 np.random.randn(H, H),那么将发生梯度爆炸,最终就会导致溢出,出现 NaN 之类的值。如此一来,神经网络的学习将无法正确运行:
    在这里插入图片描述如果将 W h W_h Wh 初始化为 np.random.randn(H, H) * 0.5,那么将发生梯度消失。一旦梯度变小,权重梯度不能被更新,模型就会无法学习长期的依赖关系:
    在这里插入图片描述

梯度爆炸的对策

梯度消失的对策

  • 为了解决梯度消失问题,需要从根本上改变 RNN 层的结构,为此人们已经提出了诸多 Gated RNN 框架,其中具有代表性的有 LSTMGRU,下面对 LSTM 进行说明

LSTM (Long Short-Term Memory)

长短期记忆: 意思是可以时间维持短期记忆

LSTM 层的结构

Ref: colah’s blog: Understanding LSTM Networks

LSTM 的接口

在这里插入图片描述

  • 如图 6-11 所示,LSTM 与 RNN 的接口的不同之处在于,LSTM 还有记忆单元 c c c,它仅在 LSTM 层内部接收和传递数据,不向其他层输出。而 LSTM 的隐藏状态 h h h 和 RNN 层相同,会被 (向上) 输出到其他层

记忆单元 c t c_t ct

  • 记忆单元 c t c_t ct 存储了时刻 t t t 时 LSTM 的记忆,可以认为其中保存了从过去到时刻 t t t 的所有必要信息。然后,隐藏状态 h t h_t ht 由记忆单元 c t c_t ct 计算得到,即 h t = t a n h ( c t ) h_t=tanh(c_t) ht=tanh(ct) (这意味着 h t h_t ht c t c_t ct 具有相同的维数):
    在这里插入图片描述

输出门 (output gate)

门 (gate)

  • Gate 就像将门打开或合上一样,控制数据的流动,它不但能控制门的开合,还能控制开合程度 (0.0 ~ 1.0)
  • 在 LSTM 中,门的开合程度也是自动从数据中学习到的。有专门的权重参数用于控制门的开合程度,这些权重参数通过学习被更新。另外,sigmoid 函数用于求门的开合程度
    在这里插入图片描述

输出门 (output gate)

  • 这里考虑对 t a n h ( c t ) tanh(c_t) tanh(ct) 施加门,也就是针对 t a n h ( c t ) tanh(c_t) tanh(ct) 的各个元素,调整它们作为下一时刻的隐藏状态的重要程度。由于这个门管理下一个隐藏状态 h t h_t ht 的输出,所以称为输出门
  • 输出门的开合程度根据输入 x t x_t xt 和上一个状态 h t − 1 h_{t−1} ht1 求出
    在这里插入图片描述其中,上标 o o o 表示 output, σ \sigma σ 为 sigmoid 函数。最后,输出 h t h_t ht
    h t = o ⊙ t a n h ( c t ) h_t=o\odot tanh(c_t) ht=otanh(ct)

在这里插入图片描述

遗忘门 (forget gate)

  • 只有放下包袱,才能轻装上路。接下来,我们要做的就是明确告诉记忆单元需要 “忘记什么”。这里,我们使用遗忘门来实现这一目标。下面将遗忘门添加到 LSTM 层。计算图如下:
    在这里插入图片描述其中,
    在这里插入图片描述

新的记忆单元

  • 遗忘门从上一时刻的记忆单元中删除了应该忘记的东西,但是这样一来,记忆单元只会忘记信息。现在我们还想向这个记忆单元添加一些应当记住的新信息,为此我们添加新的 tanh 节点
    在这里插入图片描述其中,
    在这里插入图片描述这个 tanh 节点的作用不是门,而是将新的信息添加到记忆单元中。因此,它不用 sigmoid 函数作为激活函数,而是使用 tanh 函数

输入门 (input gate)

  • 最后,我们 g g g 添加输入门,输入门判断新增信息 g g g 的各个元素的价值有多大,也就是对要添加的信息进行取舍。计算图如下图所示:
    在这里插入图片描述其中
    在这里插入图片描述

LSTM 有多个“变体”。这里说明的 LSTM 是最有代表性的 LSTM,也有许多在门的连接方式上稍微不同的其他 LSTM

为什么 LSTM 不会引起梯度消失?

  • 我们可以通过观察记忆单元 c c c 的反向传播来直观地解释这个问题。首先画出如下的计算图:
    在这里插入图片描述可以发现,记忆单元的反向传播仅流过 “+” 和 “×” 节点。“+” 节点将上游传来的梯度原样流出,所以梯度没有变化。而 “×” 节点的计算并不是矩阵乘积,而是对应元素的乘积,而且每次都会基于不同的门值进行对应元素的乘积计算,遗忘门认为 “应该忘记” 的记忆单元的元素,其梯度会变小;而遗忘门认为 “不能忘记” 的元素,其梯度在向过去的方向流动时不会退化。因此,可以期待记忆单元的梯度 (应该长期记住的信息) 能在不发生梯度消失的情况下传播

一个加速 LSTM 计算的 trick

在这里插入图片描述

  • 首先整理一下 LSTM 中进行的计算:
    在这里插入图片描述可以将上面的四个仿射变换合并成一个来加速计算
    在这里插入图片描述其中, W x W_x Wx, W h W_h Wh, b b b 为整合得到的新权重。得到的新的计算图如下:
    在这里插入图片描述其中,slice 节点负责将仿射变换的结果 (矩阵) 均等地分成 4 份,然后取出内容

LSTM 的实现

下面将进行单步处理的类实现为 LSTM 类,将整体处理 T T T 步的类实现为 TimeLSTM 类

LSTM 层的实现

slice 节点的反向传播

在这里插入图片描述

class LSTM:
    def __init__(self, Wx, Wh, b):
        '''

        Parameters
        ----------
        Wx: 输入`x`用的权重参数(整合了4个权重)
        Wh: 隐藏状态`h`用的权重参数(整合了4个权重)
        b: 偏置(整合了4个偏置)
        '''
        self.params = [Wx, Wh, b]
        self.grads = [np.zeros_like(Wx), np.zeros_like(Wh), np.zeros_like(b)]
        self.cache = None

    def forward(self, x, h_prev, c_prev):
    	# shape: x (N x D), h_prev (N x H), c_prev (N x H)
    	# 		 Wx (D x 4H), Wh (H x 4H), b (1 x 4H)
        Wx, Wh, b = self.params
        N, H = h_prev.shape

        A = np.dot(x, Wx) + np.dot(h_prev, Wh) + b

        f = A[:, :H]
        g = A[:, H:2*H]
        i = A[:, 2*H:3*H]
        o = A[:, 3*H:]

        f = sigmoid(f)
        g = np.tanh(g)
        i = sigmoid(i)
        o = sigmoid(o)

        c_next = f * c_prev + g * i
        h_next = o * np.tanh(c_next)

        self.cache = (x, h_prev, c_prev, i, f, g, o, c_next)
        return h_next, c_next

    def backward(self, dh_next, dc_next):
        Wx, Wh, b = self.params
        x, h_prev, c_prev, i, f, g, o, c_next = self.cache

        tanh_c_next = np.tanh(c_next)

        ds = dc_next + (dh_next * o) * (1 - tanh_c_next ** 2)

        dc_prev = ds * f

        di = ds * g
        df = ds * c_prev
        do = dh_next * tanh_c_next
        dg = ds * i

        di *= i * (1 - i)
        df *= f * (1 - f)
        do *= o * (1 - o)
        dg *= (1 - g ** 2)

        dA = np.hstack((df, dg, di, do))	# BP of slice node

        dWh = np.dot(h_prev.T, dA)
        dWx = np.dot(x.T, dA)
        db = dA.sum(axis=0)

        self.grads[0][...] = dWx
        self.grads[1][...] = dWh
        self.grads[2][...] = db

        dx = np.dot(dA, Wx.T)
        dh_prev = np.dot(dA, Wh.T)

        return dx, dh_prev, dc_prev

TimeLSTM 层的实现 (Truncated BPTT)

在这里插入图片描述

  • 实现思想与 TimeRNN 类似,为了维持正向传播的数据流,将隐藏状态和记忆单元保存在成员变量中
    在这里插入图片描述
class TimeLSTM:
    def __init__(self, Wx, Wh, b, stateful=False):
        self.params = [Wx, Wh, b]
        self.grads = [np.zeros_like(Wx), np.zeros_like(Wh), np.zeros_like(b)]
        self.layers = None

        self.h, self.c = None, None
        self.dh = None
        self.stateful = stateful

    def forward(self, xs):
        Wx, Wh, b = self.params
        N, T, D = xs.shape
        H = Wh.shape[0]

        self.layers = []
        hs = np.empty((N, T, H), dtype='f')

        if not self.stateful or self.h is None:
            self.h = np.zeros((N, H), dtype='f')
        if not self.stateful or self.c is None:
            self.c = np.zeros((N, H), dtype='f')

        for t in range(T):
            layer = LSTM(*self.params)
            self.h, self.c = layer.forward(xs[:, t, :], self.h, self.c)
            hs[:, t, :] = self.h

            self.layers.append(layer)

        return hs

    def backward(self, dhs):
        Wx, Wh, b = self.params
        N, T, H = dhs.shape
        D = Wx.shape[0]

        dxs = np.empty((N, T, D), dtype='f')
        dh, dc = 0, 0

        grads = [0, 0, 0]
        for t in reversed(range(T)):
            layer = self.layers[t]
            dx, dh, dc = layer.backward(dhs[:, t, :] + dh, dc)
            dxs[:, t, :] = dx
            for i, grad in enumerate(layer.grads):
                grads[i] += grad

        for i, grad in enumerate(grads):
            self.grads[i][...] = grad
        self.dh = dh
        return dxs

    def set_state(self, h, c=None):
        self.h, self.c = h, c

    def reset_state(self):
        self.h, self.c = None, None

使用 LSTM 的语言模型 (RNNLM)

在这里插入图片描述

代码实现

# coding: utf-8
import sys
sys.path.append('..')
from common.time_layers import *
from common.base_model import BaseModel

#  BaseModel 类实现了 save_ params() 和 load_params() 方法
# 另外,BaseModel 类的实现还进行了优化,以支持 GPU 和进行缩位 (使用 16 位浮点数存储)
class Rnnlm(BaseModel):
    def __init__(self, vocab_size=10000, wordvec_size=100, hidden_size=100):
        V, D, H = vocab_size, wordvec_size, hidden_size
        rn = np.random.randn

        # 初始化权重
        embed_W = (rn(V, D) / 100).astype('f')
        lstm_Wx = (rn(D, 4 * H) / np.sqrt(D)).astype('f')
        lstm_Wh = (rn(H, 4 * H) / np.sqrt(H)).astype('f')
        lstm_b = np.zeros(4 * H).astype('f')
        affine_W = (rn(H, V) / np.sqrt(H)).astype('f')
        affine_b = np.zeros(V).astype('f')

        # 生成层
        self.layers = [
            TimeEmbedding(embed_W),
            TimeLSTM(lstm_Wx, lstm_Wh, lstm_b, stateful=True),
            TimeAffine(affine_W, affine_b)
        ]
        self.loss_layer = TimeSoftmaxWithLoss()
        self.lstm_layer = self.layers[1]

        # 将所有的权重和梯度整理到列表中
        self.params, self.grads = [], []
        for layer in self.layers:
            self.params += layer.params
            self.grads += layer.grads

    def predict(self, xs):
        for layer in self.layers:
            xs = layer.forward(xs)
        return xs

    def forward(self, xs, ts):
        score = self.predict(xs)
        loss = self.loss_layer.forward(score, ts)
        return loss

    def backward(self, dout=1):
        dout = self.loss_layer.backward(dout)
        for layer in reversed(self.layers):
            dout = layer.backward(dout)
        return dout

    def reset_state(self):
        self.lstm_layer.reset_state()

模型学习

  • 下面使用 PTB 数据集的所有训练数据进行学习
# coding: utf-8
import sys
sys.path.append('..')
from common.optimizer import SGD
from common.trainer import RnnlmTrainer
from common.util import eval_perplexity
from dataset import ptb
from rnnlm import Rnnlm


# 设定超参数
batch_size = 20
wordvec_size = 100
hidden_size = 100  # RNN的隐藏状态向量的元素个数
time_size = 35  # RNN的展开大小
lr = 20.0
max_epoch = 4
max_grad = 0.25

# 读入训练数据
corpus, word_to_id, id_to_word = ptb.load_data('train')
corpus_test, _, _ = ptb.load_data('test')
vocab_size = len(word_to_id)
xs = corpus[:-1]
ts = corpus[1:]

# 生成模型
model = Rnnlm(vocab_size, wordvec_size, hidden_size)
optimizer = SGD(lr)
trainer = RnnlmTrainer(model, optimizer)

# 应用梯度裁剪进行学习
trainer.fit(xs, ts, max_epoch, batch_size, time_size, max_grad,
            eval_interval=20)	# 每 20 次迭代对困惑度进行 1 次评价
trainer.plot(ylim=(0, 500))

# 基于测试数据进行评价
model.reset_state()	# 需要先重置模型的状态 (LSTM 的隐藏状态和记忆单元)
ppl_test = eval_perplexity(model, corpus_test)
print('test perplexity: ', ppl_test)

# 保存参数
model.save_params()

在这里插入图片描述

进一步改进 RNNLM

LSTM 层的多层化

  • 在使用 RNNLM 创建高精度模型时,加深 LSTM 层(叠加多个 LSTM层)的方法往往很有效。之前我们只用了一个 LSTM 层,通过叠加多个层,可以提高语言模型的精度
    在这里插入图片描述

在 PTB 数据集上学习语言模型的情况下,当 LSTM 的层数为 2 ~ 4 时,可以获得比较好的结果

基于 Dropout 抑制过拟合

  • 叠加 LSTM 层虽然有助于学习到时序数据的复杂依赖关系,但却往往会发生过拟合。更糟糕的是,RNN 比常规的前馈神经网络更容易发生过拟合,因此 RNN 的过拟合对策非常重要

常用对策

  • (1) 增加训练数据
  • (2) 降低模型的复杂度 (L2 正则化, Dropout …)

下面主要介绍如何在 RNN 上加入 Dropout


在使用 RNN 的模型中,应该将 Dropout 层插入哪里呢?

  • 首先可以想到的是插入在 LSTM 层的时序方向上,如下图所示。不过答案是,这并不是一个好的插入方式
    在这里插入图片描述如果在时序方向上插入 Dropout,那么当模型学习时,随着时间的推移,信息会渐渐丢失。也就是说,因 Dropout 产生的噪声会随时间成比例地积累。考虑到噪声的积累,最好不要在时间轴方向上插入 Dropout
  • 因此,如图 6-33 所示,我们在深度方向(垂直方向)上插入 Dropout 层,这样一来,无论沿时间方向(水平方向)前进多少,信息都不会丢失。 Dropout 与时间轴独立,仅在深度方向(垂直方向)上起作用
    在这里插入图片描述

变分 Dropout (variational dropout)

  • paper: A Theoretically Grounded Application of Dropout in Recurrent Neural Networks
  • 如前所述,“常规的 Dropout” 不适合用在时间方向上。但是也有研究提出了多种方法来实现时间方向上的 RNN 正则化. e.g. variational dropout
  • 除了深度方向,变分 Dropout 也能用在时间方向上,从而进一步提高语言模型的精度。如图 6-34 所示,它的机制是同一层的 Dropout 使用相同的 mask。这里所说的 mask 是指决定是否传递数据的随机布尔值。如此一来,信息的损失方式也被 “固定”,所以可以避免常规 Dropout 发生的指数级信息损失
    在这里插入图片描述

权重共享 (weight tying)

直译为 “权重绑定”


  • 改进语言模型有一个非常简单的技巧,那就是权重共享。如下图所示,通过在 Embedding 层和 Affine 层之间共享权重 (Embedding 层权重形状为 V × H V\times H V×H,Affine 层权重形状为 H × V H\times V H×V),可以大大减少学习的参数数量。尽管如此,它仍能提高精度 (直观上,共享权重有抑制过拟合的效果。论文 Tying Word Vectors and Word Classifiers: A Loss Framework for Language Modeling理论上描述了权重共享为什么有用)
    在这里插入图片描述

更好的 RNNLM 的实现 (BetterRnnlm)

在这里插入图片描述

class BetterRnnlm(BaseModel):
    '''
     利用2个LSTM层并在各层使用Dropout的模型
     基于[1]提出的模型,利用weight tying[2][3]

     [1] Recurrent Neural Network Regularization (https://arxiv.org/abs/1409.2329)
     [2] Using the Output Embedding to Improve Language Models (https://arxiv.org/abs/1608.05859)
     [3] Tying Word Vectors and Word Classifiers (https://arxiv.org/pdf/1611.01462.pdf)
    '''
    def __init__(self, vocab_size=10000, wordvec_size=650,
                 hidden_size=650, dropout_ratio=0.5):
        V, D, H = vocab_size, wordvec_size, hidden_size
        rn = np.random.randn

        embed_W = (rn(V, D) / 100).astype('f')
        lstm_Wx1 = (rn(D, 4 * H) / np.sqrt(D)).astype('f')
        lstm_Wh1 = (rn(H, 4 * H) / np.sqrt(H)).astype('f')
        lstm_b1 = np.zeros(4 * H).astype('f')
        lstm_Wx2 = (rn(H, 4 * H) / np.sqrt(H)).astype('f')
        lstm_Wh2 = (rn(H, 4 * H) / np.sqrt(H)).astype('f')
        lstm_b2 = np.zeros(4 * H).astype('f')
        affine_b = np.zeros(V).astype('f')

        self.layers = [
            TimeEmbedding(embed_W),
            TimeDropout(dropout_ratio),
            TimeLSTM(lstm_Wx1, lstm_Wh1, lstm_b1, stateful=True),
            TimeDropout(dropout_ratio),
            TimeLSTM(lstm_Wx2, lstm_Wh2, lstm_b2, stateful=True),
            TimeDropout(dropout_ratio),
            TimeAffine(embed_W.T, affine_b)  # weight tying!!
        ]
        self.loss_layer = TimeSoftmaxWithLoss()
        self.lstm_layers = [self.layers[2], self.layers[4]]
        self.drop_layers = [self.layers[1], self.layers[3], self.layers[5]]

        self.params, self.grads = [], []
        for layer in self.layers:
            self.params += layer.params
            self.grads += layer.grads

    def predict(self, xs, train_flg=False):
        for layer in self.drop_layers:
            layer.train_flg = train_flg

        for layer in self.layers:
            xs = layer.forward(xs)
        return xs

    def forward(self, xs, ts, train_flg=True):
        score = self.predict(xs, train_flg)
        loss = self.loss_layer.forward(score, ts)
        return loss

    def backward(self, dout=1):
        dout = self.loss_layer.backward(dout)
        for layer in reversed(self.layers):
            dout = layer.backward(dout)
        return dout

    def reset_state(self):
        for layer in self.lstm_layers:
            layer.reset_state()

BetterRnnlm 的学习

  • 学习代码中的一个改动是,针对每个 epoch 使用验证数据评价困惑度,在值变差时,降低学习率。这是一种在实践中经常用到的技巧,并且往往能有好的结果
# coding: utf-8
import sys
sys.path.append('..')
from common import config
# 在用GPU运行时,请打开下面的注释(需要cupy)
# ==============================================
# config.GPU = True
# ==============================================
from common.optimizer import SGD
from common.trainer import RnnlmTrainer
from common.util import eval_perplexity, to_gpu
from dataset import ptb
from better_rnnlm import BetterRnnlm


# 设定超参数
wordvec_size = 650
hidden_size = 650
time_size = 35
lr = 20.0
max_epoch = 40
max_grad = 0.25
dropout = 0.5

# 读入训练数据
corpus, word_to_id, id_to_word = ptb.load_data('train')
corpus_val, _, _ = ptb.load_data('val')
corpus_test, _, _ = ptb.load_data('test')

if config.GPU:
    corpus = to_gpu(corpus)
    corpus_val = to_gpu(corpus_val)
    corpus_test = to_gpu(corpus_test)

vocab_size = len(word_to_id)
xs = corpus[:-1]
ts = corpus[1:]

model = BetterRnnlm(vocab_size, wordvec_size, hidden_size, dropout)
optimizer = SGD(lr)
trainer = RnnlmTrainer(model, optimizer)

best_ppl = float('inf')
for epoch in range(max_epoch):
    trainer.fit(xs, ts, max_epoch=1, batch_size=batch_size,
                time_size=time_size, max_grad=max_grad)

    model.reset_state()
    ppl = eval_perplexity(model, corpus_val)
    print('valid perplexity: ', ppl)

    if best_ppl > ppl:
        best_ppl = ppl
        model.save_params()
    else:
    	# 这里针对每个 epoch 使用验证数据评价困惑度,当它比之前的困惑度低时,将学习率乘以 1/4
        lr /= 4.0
        optimizer.lr = lr

    model.reset_state()
    print('-' * 50)


# 基于验证数据进行评价
model.reset_state()
ppl_test = eval_perplexity(model, corpus_test)
print('test perplexity: ', ppl_test)
改进前改进后
困惑度136.0775.76

GRU (Gated Recurrent Unit, 门控循环单元)

  • GRU 保留了 LSTM 使用门的理念,但是减少了参数,缩短了计算时间。现在,我们来看一下 GRU 的内部结构

  • LSTM 的重点是使用门,因此学习时梯度的流动平稳,梯度消失得以缓解。GRU 也继承了这一想法,但与 LSTM 不同的是,GRU 没有记忆单元,只有一个隐藏状态 h h h 在时间方向上传播
    在这里插入图片描述在这里插入图片描述这里使用 r r r z z z 共两个门, r r r 称为 reset 门, z z z 称为 update 门。reset 门决定在多大程度上 “忽略” 过去的隐藏状态,如果 r r r 是 0,则新的隐藏状态 h ~ \tilde h h~ 仅取决于输入 x t x_t xt,此时过去的隐藏状态将完全被忽略。而 update 门是更新隐藏状态的门,它扮演了 LSTM 的 forget 门和 input 门两个角色。 ( 1 − z ) ⊙ h t − 1 (1 − z)\odot h_{t−1} (1z)ht1 部分充当 forget 门的功能,从过去的隐藏状态中删除应该被遗忘的信息, z ⊙ h ~ z\odot\tilde h zh~ 的部分充当 input 门的功能,对新增的信息进行加权
  • 综上,GRU 是简化的 LSTM 架构,与 LSTM 相比,可以减少计算成本和参数

LSTM:在这里插入图片描述

双向 RNN

  • 我们在输出某一时刻的隐藏状态向量时,不仅想让它包含左侧时序数据的信息,还想让它包含右侧时序数据的信息。为此可以采用双向 LSTM
    在这里插入图片描述也就是在之前的 LSTM 层上添加了一个反方向处理的 LSTM 层。然后,拼接各个时刻的两个 LSTM 层的隐藏状态,将其作为最后的隐藏状态向量(除了拼接之外,也可以 “求和” 或者 “取平均” 等)。通过这样的双向处理,各个单词对应的隐藏状态向量可以从左右两个方向聚集信息。这样一来,这些向量就编码了更均衡的信息
  • 双向 LSTM 的实现非常简单。一种实现方式是准备两个 LSTM 层,并调整输入各个层的单词的排列。具体而言,其中一个层的输入语句与之前相同,这相当于从左向右处理输入语句的常规的 LSTM 层。而另一个 LSTM 层的输入语句则按照从右到左的顺序输入。之后,只需要拼接这两个 LSTM 层的输出,就可以创建双向 LSTM 层
class TimeBiLSTM:
    def __init__(self, Wx1, Wh1, b1,
                 Wx2, Wh2, b2, stateful=False):
        self.forward_lstm = TimeLSTM(Wx1, Wh1, b1, stateful)
        self.backward_lstm = TimeLSTM(Wx2, Wh2, b2, stateful)
        self.params = self.forward_lstm.params + self.backward_lstm.params
        self.grads = self.forward_lstm.grads + self.backward_lstm.grads

    def forward(self, xs):
        o1 = self.forward_lstm.forward(xs)
        o2 = self.backward_lstm.forward(xs[:, ::-1])
        o2 = o2[:, ::-1]

        out = np.concatenate((o1, o2), axis=2)
        return out

    def backward(self, dhs):
        H = dhs.shape[2] // 2
        do1 = dhs[:, :, :H]
        do2 = dhs[:, :, H:]

        dxs1 = self.forward_lstm.backward(do1)
        do2 = do2[:, ::-1]
        dxs2 = self.backward_lstm.backward(do2)
        dxs2 = dxs2[:, ::-1]
        dxs = dxs1 + dxs2
        return dxs

使用语言模型生成文本

使用 RNN 生成文本的步骤

  • 语言模型根据已经出现的单词输出下一个出现的单词的概率分布,如果想要生成下一个单词,可以直接选择概率最高的单词,也可以根据概率分布对单词进行采样。为了让每次生成的文本有所不同,这里选择第二种方法
    在这里插入图片描述假设采样得到的单词为 “say”,那么就再将 “say” 输入语言模型,获得单词的概率分布,然后再根据这个概率分布采样下一个出现的单词
    在这里插入图片描述不断重复上述过程,直至生成 <eos> 这一结尾记号为止

文本生成的实现

class BetterRnnlmGen(BetterRnnlm):
	# 主要在 BetterRnnlmGen 类的基础上添加 generate 方法
    def generate(self, start_id, skip_ids=None, sample_size=100):
    	# start_id 是第 1 个单词 ID
    	# sample_size 表示要采样的单词数量
    	# skip_ids 是单词 ID 列表,它指定的单词将不被采样。这个参数用于排除 PTB 数据集中的 <unk>、N 等被预处理过的单词 
    	# 	(PTB 数据集对原始文本进行了预处理,稀有单词被 <unk> 替换,数字被 N 替换。另外,我们用 <eos> 作为文本的分隔符)
        word_ids = [start_id]

        x = start_id
        while len(word_ids) < sample_size:
            x = np.array(x).reshape(1, 1)
            score = self.predict(x).flatten()
            p = softmax(score).flatten()

            sampled = np.random.choice(len(p), size=1, p=p)
            if (skip_ids is None) or (sampled not in skip_ids):
                x = sampled
                word_ids.append(int(x))

        return word_ids

    def get_state(self):
        states = []
        for layer in self.lstm_layers:
            states.append((layer.h, layer.c))
        return states

    def set_state(self, states):
        for layer, state in zip(self.lstm_layers, states):
            layer.set_state(*state)

加载训练好的 RNN 网络参数进行文本生成

# coding: utf-8
import sys
sys.path.append('..')
from common.np import *
from rnnlm_gen import BetterRnnlmGen
from dataset import ptb


corpus, word_to_id, id_to_word = ptb.load_data('train')
vocab_size = len(word_to_id)
corpus_size = len(corpus)


model = BetterRnnlmGen()
model.load_params('../ch06/BetterRnnlm.pkl')

# 设定 start 字符和 skip 字符
start_word = 'you'
start_id = word_to_id[start_word]
skip_words = ['N', '<unk>', '$']	# 设置不参与采样的单词
skip_ids = [word_to_id[w] for w in skip_words]
# 文本生成
word_ids = model.generate(start_id, skip_ids)
txt = ' '.join([id_to_word[i] for i in word_ids])
txt = txt.replace(' <eos>', '.\n')

print(txt)

结果:

you 've seen two families and the women and two other women of students.
the principles of investors that prompted a bipartisan rule of which had a withdrawn target of black men or legislators interfere with the number of plants can do to carry it together.
the appeal was to deny steady increases in the operation of dna and educational damage in the 1950s.

  • 最后,我们尝试给语言模型输入 “the meaning of life is”,让它生成后续的单词
# 根据给定的一句话生成文本
model.reset_state()

start_words = 'the meaning of life is'
start_ids = [word_to_id[w] for w in start_words.split(' ')]

for x in start_ids[:-1]:
    x = np.array(x).reshape(1, 1)
    model.predict(x)

word_ids = model.generate(start_ids[-1], skip_ids)
word_ids = start_ids[:-1] + word_ids
txt = ' '.join([id_to_word[i] for i in word_ids])
txt = txt.replace(' <eos>', '.\n')
print('-' * 50)
print(txt)

结果:

the meaning of life is not a good version of paintings.
  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值