4.4.循环神经网络RNN

循环神经网络

在这里插入图片描述

更新隐藏状态:
h t = ϕ ( W h h h t − 1 + W h x x t − 1 + b h ) h_t = \phi(W_{hh}h_{t-1}+W_{hx}x_{t-1}+b_h) ht=ϕ(Whhht1+Whxxt1+bh)
输出:
o t = ϕ ( W h o h t + b o ) o_t = \phi(W_{ho}h_t+b_o) ot=ϕ(Whoht+bo)
​ 容易注意到,去掉隐藏层后( W h h h t − 1 W_{hh}h_{t-1} Whhht1这一项),和MLP完全一样

困惑度(perplexity)

​ 需要量化预测结果的好坏。

​ 衡量一个语言模型的好坏可以用平均交叉熵(分类问题,有字典大小个类别的分类):
π = 1 n ∑ i = 1 n − l o g   p ( x t ∣ x t − 1 , ⋯   ) \pi = \frac 1 n\sum^n_{i=1} -log\ p(x_t|x_{t-1},\cdots) π=n1i=1nlog p(xtxt1,)
​ p是语言模型的预测概率, x t x_t xt是真实词。

​ 由于历史原因,NLP使用困惑度 e x p ( π ) exp(\pi) exp(π) ,1表示完美,无穷大是最差情况

梯度裁剪

​ 迭代中计算T个时间步上的梯度,在反向传播过程中产生长度为 O ( T ) O(T) O(T)的矩阵乘法链,导致数值不稳定。梯度裁剪能有效预防梯度爆炸:

​ 如果梯度长度超过 θ \theta θ,那么拖影回长度 θ \theta θ:
g ← m i n ( 1 , θ ∣ ∣ g ∣ ∣ ) g g\leftarrow min(1,\frac{\theta}{||g||})g gmin(1,∣∣g∣∣θ)g
​ 如果 ∣ ∣ g ∣ ∣ > θ ||g|| > \theta ∣∣g∣∣>θ,则会取到 θ ∣ ∣ g ∣ ∣ \frac{\theta}{||g||} ∣∣g∣∣θ

更多的RNN应用

在这里插入图片描述

RNN的实现

1.独立热编码

​ 在训练数据中,每个词元都表示为一个数字索引,将这些索引直接输入神经网络可能会使学习变得困难,我们通常将每个词元表示为更具表现力的特征向量,最简单的标识为独热编码。

​ 每次采样的小批量数据形状是二维张量: (批量大小,时间步数)。 one_hot函数将这样一个小批量数据转换成三维张量, 张量的最后一个维度等于词表大小(len(vocab))。 我们经常转换输入的维度,以便获得形状为 (时间步数,批量大小,词表大小)的输出。 这将使我们能够更方便地通过最外层的维度, 一步一步地更新小批量数据的隐状态。

'''独立热编码,vocab有28个,26个字母+空格+unk'''
print(F.one_hot(torch.tensor([0, 2]), len(vocab))) # 下标0和2为1
X = torch.arange(10).reshape((2, 5))
print(F.one_hot(X.T, 28).shape)

2.循环神经网络模型

'''初始化参数'''
def get_params(vocab_size, num_hiddens, device):
    num_inputs = num_outputs = vocab_size #独热编码后是one hot,则输入输出的维度都是词表的大小

    def normal(shape):
        return torch.randn(size=shape, device=device) * 0.01

    # 隐藏层参数
    W_xh = normal((num_inputs, num_hiddens))
    W_hh = normal((num_hiddens, num_hiddens))
    b_h = torch.zeros(num_hiddens, device=device)
    # 输出层参数
    W_hq = normal((num_hiddens, num_outputs))
    b_q = torch.zeros(num_outputs, device=device)
    # 附加梯度
    params = [W_xh, W_hh, b_h, W_hq, b_q]
    for param in params:
        param.requires_grad_(True)
    return params
 
# 给定一个初始的隐藏状态
# 使用该函数在初始化时返回隐状态,返回是一个全0张量,形状为(批量大小,隐藏单元数)
def init_rnn_state(batch_size, num_hiddens, device):
    return (torch.zeros((batch_size, num_hiddens), device=device),)

# 做计算
# state 是隐藏状态,params是学习的参数
def rnn(inputs, state, params):
    # inputs的形状:(时间步数量,批量大小,词表大小)
    W_xh, W_hh, b_h, W_hq, b_q = params
    H, = state
    outputs = []
    # X的形状:(批量大小,词表大小)
    for X in inputs:#按时刻进行遍历,所以one_hot时做个转置,将时间维度放在第一维度,方便遍历
      # mm做矩阵乘法,H是前一个隐藏状态,使用tanh做激活函数
        H = torch.tanh(torch.mm(X, W_xh) + torch.mm(H, W_hh) + b_h)
        Y = torch.mm(H, W_hq) + b_q
        outputs.append(Y)
        # 输出还要加上当前的隐藏状态,因为可能还要使用
        # 在维度0拼接,拼接成一个二维的矩阵,列数没别(vocb_size),行数变为批量大小乘时间长度
    return torch.cat(outputs, dim=0), (H,)

'''包装成一个类'''
class RNNModelScratch: #@save
    """从零开始实现的循环神经网络模型"""
    def __init__(self, vocab_size, num_hiddens, device,
                 get_params, init_state, forward_fn):
        self.vocab_size, self.num_hiddens = vocab_size, num_hiddens
        self.params = get_params(vocab_size, num_hiddens, device)
        self.init_state, self.forward_fn = init_state, forward_fn

    def __call__(self, X, state):
      	# one_hot出来是个整型,变换为浮点,才能做前向传播
        X = F.one_hot(X.T, self.vocab_size).type(torch.float32)
        return self.forward_fn(X, state, self.params)

    def begin_state(self, batch_size, device):
        return self.init_state(batch_size, self.num_hiddens, device)
      
      
num_hiddens = 512
net = RNNModelScratch(len(vocab), num_hiddens, d2l.try_gpu(), get_params,
                      init_rnn_state, rnn)
state = net.begin_state(X.shape[0], d2l.try_gpu())
# X是2*5的矩阵,即10个词,那么Y就是(10,28)的矩阵,对每个词都有28分类
# new_state 是长为1的元组

Y, new_state = net(X.to(d2l.try_gpu()), state)
Y.shape, len(new_state), new_state[0].shape

3.预测

def predict_ch8(prefix, num_preds, net, vocab, device):  #@save
    """在prefix后面生成新字符,num_preds就是预测多少个词"""
    state = net.begin_state(batch_size=1, device=device)
    outputs = [vocab[prefix[0]]]
    # input函数就是将最近预测的一个值作为输入,批量大小为1,时间步长为1
    get_input = lambda: torch.tensor([outputs[-1]], device=device).reshape((1, 1))
    for y in prefix[1:]:  # 预热期,不需要进行预测,只是用于初始化状态
        _, state = net(get_input(), state)
        outputs.append(vocab[y])#直接用真实值
    for _ in range(num_preds):  # 预测num_preds步
        y, state = net(get_input(), state)
        #argmax就是softmax,找到最大可能的类别,reshape成一个标量,即下标
        outputs.append(int(y.argmax(dim=1).reshape(1)))
    return ''.join([vocab.idx_to_token[i] for i in outputs])
  
predict_ch8('time traveller ', 10, net, vocab, d2l.try_gpu())

4.梯度裁剪

​ 前文中提过,如果在迭代中计算T个时间步上的梯度,将会在反向传播中产生长度为 O ( T ) O(T) O(T)的矩阵乘法链,大道至梯度爆炸或者梯度消失,所以需要用额外的方式来支持稳定训练

​ 一般来说,假定在向量形式的 x x x中,或者在小批量数据的负梯度 g g g方向上,用 η \eta η作为学习率是,在一次迭代中,我们将 x x x更新为 x − η g x-\eta g xηg,如果我们进一步假设目标函数 f f f表现良好,即函数 f f f在常数 L L L下是利普希茨连续的。

利普希茨连续(Lipschitz continuous),对于任意 x , y x,y x,y有:
∣ f ( x ) − f ( y ) ∣ ≤ L ∣ ∣ x − y ∣ ∣ |f(x)-f(y)|\le L||x-y|| f(x)f(y)L∣∣xy∣∣

​ 在这种情况下,可以假设:如果我们通过 η g \eta g ηg更新参数向量,则:
∣ f ( x ) − f ( x − η g ) ∣ ≤ L η ∣ ∣ g ∣ ∣ |f(x)-f(x-\eta g)|\le L\eta||g|| f(x)f(xηg)Lη∣∣g∣∣
​ 这意味着我们不悔观察到超过$ L\eta||g||$的变化,这有好有坏,它限制了取得进展的素的,也限制了变坏的程度,尤其是朝着错误的方向前进时。

​ 有时梯度可能很大,从而优化算法无法收敛,我们可以通过降低 η \eta η的学习率来解决这个问题,但我们可能很少得到大的梯度,所以一个流行的替代方案是通过将梯度 g g g投影回给定半径( 例如 θ 例如\theta 例如θ)的球来裁剪梯度 g g g,如下式:
g ← m i n ( 1 , θ ∣ ∣ g ∣ ∣ ) g g\leftarrow min(1,\frac{\theta}{||g||})g gmin(1,∣∣g∣∣θ)g
​ 通过这样做,梯度范数将永远不会超过 θ \theta θ,并且更新后的梯度完全与原始方向对齐。它还有一个好处,即限制任何给定的小批量数据(以及其中任何给定的样本)对参数向量的影响,这赋予了模型一定程度的稳定性

def grad_clipping(net, theta):  #@save
    """裁剪梯度,params是全局的"""
    if isinstance(net, nn.Module):
        params = [p for p in net.parameters() if p.requires_grad]
    else:
        params = net.params
    norm = torch.sqrt(sum(torch.sum((p.grad ** 2)) for p in params))
    if norm > theta:
        for param in params:
            param.grad[:] *= theta / norm

5.训练

#@save
def train_epoch_ch8(net, train_iter, loss, updater, device, use_random_iter):
    """训练网络一个迭代周期(定义见第8章)"""
    state, timer = None, d2l.Timer()
    metric = d2l.Accumulator(2)  # 训练损失之和,词元数量
    for X, Y in train_iter:
        if state is None or use_random_iter:
            # 在第一次迭代或使用随机抽样时初始化state
            state = net.begin_state(batch_size=X.shape[0], device=device)
        else:
            if isinstance(net, nn.Module) and not isinstance(state, tuple):
                # state对于nn.GRU是个张量,一个张柳,直接分离出来
                state.detach_() # 连续的,将上一个批量的状态传递一下
            else:
                # state对于nn.LSTM或对于我们从零开始实现的模型是个张量
                # state是个元组,多个张量,分别分离出来,不做梯度运算
                for s in state:
                    s.detach_()
        y = Y.T.reshape(-1) # 转置把时间步放到第一维度
        X, y = X.to(device), y.to(device)
        y_hat, state = net(X, state)
        l = loss(y_hat, y.long()).mean() 
        # loss就是把他拉成一维向量,那么第一维度是时间步数就很关键
        if isinstance(updater, torch.optim.Optimizer):
            updater.zero_grad()
            l.backward()
            grad_clipping(net, 1) #梯度裁剪
            updater.step()
        else:
            l.backward()
            grad_clipping(net, 1)
            # 因为已经调用了mean函数
            updater(batch_size=1)
        metric.add(l * y.numel(), y.numel())
    return math.exp(metric[0] / metric[1]), metric[1] / timer.stop()
  
#@save
def train_ch8(net, train_iter, vocab, lr, num_epochs, device,
              use_random_iter=False):
    """训练模型(定义见第8章)"""
    loss = nn.CrossEntropyLoss()
    animator = d2l.Animator(xlabel='epoch', ylabel='perplexity',
                            legend=['train'], xlim=[10, num_epochs])
    # 初始化
    if isinstance(net, nn.Module):
        updater = torch.optim.SGD(net.parameters(), lr)
    else:
        updater = lambda batch_size: d2l.sgd(net.params, lr, batch_size)
    predict = lambda prefix: predict_ch8(prefix, 50, net, vocab, device)
    # 训练和预测
    for epoch in range(num_epochs):
        ppl, speed = train_epoch_ch8(
            net, train_iter, loss, updater, device, use_random_iter)
        if (epoch + 1) % 10 == 0:
            print(predict('time traveller'))
            animator.add(epoch + 1, [ppl])
    print(f'困惑度 {ppl:.1f}, {speed:.1f} 词元/秒 {str(device)}')
    print(predict('time traveller'))
    print(predict('traveller'))
    
num_epochs, lr = 500, 1
train_ch8(net, train_iter, vocab, lr, num_epochs, d2l.try_gpu()) 

net = RNNModelScratch(len(vocab), num_hiddens, d2l.try_gpu(), get_params,
                      init_rnn_state, rnn)
train_ch8(net, train_iter, vocab, lr, num_epochs, d2l.try_gpu(),
          use_random_iter=True)

在这里插入图片描述

​ 词还是对的,但没什么联系。。。(困惑度和是随机取样的方式)

6.简洁实现

​ 库要快一些,因为从零实现做了很多次小矩阵乘法,而pytorch的rnn库里将小矩阵连接起来,形成一个大矩阵,只做一次大矩阵乘法,要快3倍左右。

'''简洁实现'''

batch_size, num_steps = 32, 35
train_iter, vocab = d2l.load_data_time_machine(batch_size, num_steps)

num_hiddens = 256
rnn_layer = nn.RNN(len(vocab), num_hiddens)

state = torch.zeros((1, batch_size, num_hiddens))
state.shape

X = torch.rand(size=(num_steps, batch_size, len(vocab)))
Y, state_new = rnn_layer(X, state)
Y.shape, state_new.shape # 时间,批量大小,隐藏层数量
#(torch.Size([35, 32, 256]), torch.Size([1, 32, 256]))

#pytorch的rnn类只有隐藏层,没有输出层

#@save
class RNNModel(nn.Module):
    """循环神经网络模型"""
    def __init__(self, rnn_layer, vocab_size, **kwargs):
        super(RNNModel, self).__init__(**kwargs)
        self.rnn = rnn_layer
        self.vocab_size = vocab_size
        self.num_hiddens = self.rnn.hidden_size
        # 如果RNN是双向的(之后将介绍),num_directions应该是2,否则应该是1
        if not self.rnn.bidirectional:
            self.num_directions = 1
            self.linear = nn.Linear(self.num_hiddens, self.vocab_size)
        else:
            self.num_directions = 2
            self.linear = nn.Linear(self.num_hiddens * 2, self.vocab_size)

    def forward(self, inputs, state):
        X = F.one_hot(inputs.T.long(), self.vocab_size)
        X = X.to(torch.float32)
        Y, state = self.rnn(X, state)
        # 全连接层首先将Y的形状改为(时间步数*批量大小,隐藏单元数)
        # 它的输出形状是(时间步数*批量大小,词表大小)。
        output = self.linear(Y.reshape((-1, Y.shape[-1])))
        return output, state

    def begin_state(self, device, batch_size=1):
        if not isinstance(self.rnn, nn.LSTM):
            # nn.GRU以张量作为隐状态
            return  torch.zeros((self.num_directions * self.rnn.num_layers,
                                 batch_size, self.num_hiddens),
                                device=device)
        else:
            # nn.LSTM以元组作为隐状态
            return (torch.zeros((
                self.num_directions * self.rnn.num_layers,
                batch_size, self.num_hiddens), device=device),
                    torch.zeros((
                        self.num_directions * self.rnn.num_layers,
                        batch_size, self.num_hiddens), device=device))
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值