d2l-循环神经网络

1. 序列模型

之前讨论的表格数据和图像数据,我们都默认数据来自于某种分布,并且所有样本都是独立同分布的(independently and identically distributed, i.i.d.).
然而,大部分数据并非如此。例如:文章中的单词、视频中的图像帧、对话中的音频信号、网站上的浏览行为
都是有顺序的。因此针对此类数据设计特定模型,可能效果会更好。

同时,我们不仅需要可以接收一个序列作为输入,还希望能够继续预测序列的后续。例如预测股市的波动、患者的体温曲线。

  • 卷积神经网络 -> 空间信息
  • 循环神经网络 -> 序列信息
  1. 自回归模型:使用自身过去数据来预测未来
  2. 马尔可夫模型:假设当前只跟最近少数数据(\(\tau\)个)相关,从而简化模型
    • \(p(x_t | x_1, ..., x_{t-1}) = p(x_t | x_{t-\tau}, ..., x_{t-1}) = p(x_t | f(x_{t-\tau}, ..., x_{t-1}))\)
  3. 潜变量模型:使用潜变量\(h_t\)来概括历史信息
    • \(h_t = f(x_1, ..., x_{t-1})\)
    • \(x_t = p(x_t | h_t)\)

img

潜变量(latent variable)和隐变量(hidden variable):通常认为

  • 隐变量(hidden variable)是现实生活中存在的,但是没有被观察到。
  • 潜变量(latent variable)包含隐变量,潜变量可能在现实中不存在,例如标签信息。
  • 在神经网络领域,两者有时会混用。

2. 文本预处理

文本预处理的常见步骤:

  1. 读取数据集:将文本作为字符串加载进内存
  2. 词元化:将字符串拆分为词元 token(如单词或字符)
  3. 词表 vocabulary:将词元映射到从0开始的数字索引上
    • 对训练集中的文档合并,对唯一词元进行统计,统计结果称为语料 corpus
    • 根据词元出现的频率,分配数字索引
    • 很少出现的词元会被移除,用<unk>记录
  4. 将文本转化为数字索引序列,方便模型操作

3. 语言模型

自然语言统计

  • 单词的频率满足 齐普夫定律 (Zipf's law),第\(i\)个最常用单词的频率\(n_i\)为:$ n_i \propto \frac{1}{i^\alpha}$
  • n元组也遵循齐普夫定律。
  • n元组的数量没有那么大,说明语言中存在相当多的结构。
    img

读取长序列数据

数据与标签:对于语言建模,目标时基于目前为止看到的词元来预测下一个词元,因此标签是移位了一个词元的原始序列

  1. 随机采样:确定num_steps,每次随机丢弃开头的0 - num_steps-1个token
    • 相邻两个batch在原始序列上不一定相邻
X:  tensor([[13, 14, 15, 16, 17],
      [28, 29, 30, 31, 32]])
Y: tensor([[14, 15, 16, 17, 18],
      [29, 30, 31, 32, 33]])
X:  tensor([[ 3,  4,  5,  6,  7],
      [18, 19, 20, 21, 22]])
Y: tensor([[ 4,  5,  6,  7,  8],
      [19, 20, 21, 22, 23]])
X:  tensor([[ 8,  9, 10, 11, 12],
      [23, 24, 25, 26, 27]])
Y: tensor([[ 9, 10, 11, 12, 13],
      [24, 25, 26, 27, 28]])
  1. 顺序采样:同样是随机丢弃开头的0 - num_steps-1个token
    • 相邻两个batch在原始序列上是相邻的。
X:  tensor([[ 0,  1,  2,  3,  4],
        [17, 18, 19, 20, 21]])
Y: tensor([[ 1,  2,  3,  4,  5],
        [18, 19, 20, 21, 22]])
X:  tensor([[ 5,  6,  7,  8,  9],
        [22, 23, 24, 25, 26]])
Y: tensor([[ 6,  7,  8,  9, 10],
        [23, 24, 25, 26, 27]])
X:  tensor([[10, 11, 12, 13, 14],
        [27, 28, 29, 30, 31]])
Y: tensor([[11, 12, 13, 14, 15],
        [28, 29, 30, 31, 32]])

4. 循环神经网络 RNN

img

  • \(o_t\)是根据\(h_t\)得到的,而\(h_t\)依赖的是\(x_{t-1}\)及之前的输入
  • 即输出\(o_t\)之前,不能看到\(x_t\)
  • 通过\(o_t\)\(x_t\)之间的误差,计算损失函数
  • RNN是通过\(W_{hh}\)来存储时序信息的(即,隐变量的横向箭头)

img

困惑度 perplexity

语言模型可以视为一个多分类任务(预测词汇,有len(vocab)个类别)。
衡量一个语言模型的好坏可以用平均交叉熵

\[\pi = \frac{1}{n} \sum_{i=1}^n\log p(x_t|x_{t-1}, ...) \]

其中,\(p\)是语言模型的预测模型,\(x_t\)是真实词。

由于历史原因,NLP使用困惑度 \(exp(\pi)\) 来衡量,其中

  • 1表示完美
  • 无穷大表示最差情况

李普希茨连续 Lipschitz continuous

假设在向量形式的\(\mathbf{x}\)中,使用\(\eta > 0\)作为学习率,我们将\(\mathbf{x}\)更新为\(\mathbf{x}-\eta \mathbf{g}\)
如果我们进一步假设目标函数\(f\)表现良好,即函数\(f\)在常数\(L\)下是李普希茨连续的

\[|f(\mathbf{x}) - f(\mathbf{x} - \eta \mathbf{g})| \leq L\eta||\mathbf{g}|| \]

这意味着我们不会观察到超过\(L\eta||\mathbf{g}||\)的变化。这既是坏事也是好事。

  • 坏的方面:限制了取得进展的速度。
  • 好的方面:限制了事情变遭的程度,尤其当我们朝着错误的方向前进时。

梯度剪裁 gradient clipping

梯度剪裁能够有效防止梯度爆炸。
如果梯度范数超过\(\theta\),则将梯度投影回半径为\(\theta\)的球。

\[\mathbf{g} \leftarrow min(1, \frac{\theta}{||\mathbf{g}||})\mathbf{g} \]

RNN的Pytorch实现

  • Y是每个时间步的隐状态,这些隐状态可以作为后续输出层(Linear)的输入
  • state,state_new分别是更新前后的隐状态
import torch
from torch import nn
from torch.nn import functional as F
from d2l import torch as d2l

# 批量大小,时间步长
batch_size, num_steps = 32, 35
train_iter, vocab = d2l.load_data_time_machine(batch_size, num_steps)

# 构造一个具有256个隐藏单元的单隐藏层
num_hiddens = 256
rnn_layer = nn.RNN(len(vocab), num_hiddens)

# 初始化隐状态h
state = torch.zeros((1, batch_size, num_hiddens))
# 形状为(隐藏层数,批量大小,隐藏单元数)
# state.shape
# torch.Size([1, 32, 256])


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]))
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))
posted @ 2025-02-11 13:15  Frank23  阅读(39)  评论(0)    收藏  举报