动手学深度学习8.5. 循环神经网络的从零开始实现-笔记&练习(PyTorch)

本节课程地址:从零开始实现_哔哩哔哩_bilibili

本节教材地址:8.5. 循环神经网络的从零开始实现 --- 动手学深度学习 2.0.0 documentation (d2l.ai)

本节开源代码:...>d2l-zh>pytorch>chapter_multilayer-perceptrons>rnn-scratch.ipynb


循环神经网络的从零开始实现

本节将根据 8.4节 中的描述, 从头开始基于循环神经网络实现字符级语言模型。 这样的模型将在H.G.Wells的时光机器数据集上训练。 和前面 8.3节 中介绍过的一样, 我们先读取数据集。

%matplotlib inline
import math
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)

[独热编码]

回想一下,在train_iter中,每个词元都表示为一个数字索引, 将这些索引直接输入神经网络可能会使学习变得困难。 我们通常将每个词元表示为更具表现力的特征向量。 最简单的表示称为独热编码 (one-hot encoding), 它在 3.4.1节 中介绍过。

简言之,将每个索引映射为相互不同的单位向量: 假设词表中不同词元的数目为 N(即len(vocab)), 词元索引的范围为 0 到 N-1。 如果词元的索引是整数 i , 那么我们将创建一个长度为 N 的全 0 向量, 并将第 i 处的元素设置为 1 。 此向量是原始词元的一个独热向量。 索引为 0 和 2 的独热向量如下所示:

F.one_hot(torch.tensor([0, 2]), len(vocab))
tensor([[1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
         0, 0, 0, 0],
        [0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
         0, 0, 0, 0]])

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

X = torch.arange(10).reshape((2, 5))
F.one_hot(X.T, 28).shape
# 转置的目的是为了将时间步放在第一个维度,后两个维度就代表x_T
torch.Size([5, 2, 28])

初始化模型参数

接下来,我们[初始化循环神经网络模型的模型参数 ]。 隐藏单元数num_hiddens是一个可调的超参数。 当训练语言模型时,输入和输出来自相同的词表。 因此,它们具有相同的维度,即词表的大小。

def get_params(vocab_size, num_hiddens, device):
    num_inputs = num_outputs = vocab_size

    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

循环神经网络模型

为了定义循环神经网络模型, 我们首先需要[一个init_rnn_state函数在初始化时返回隐状态]。 这个函数的返回是一个张量,张量全用0填充, 形状为(批量大小,隐藏单元数)。 在后面的章节中我们将会遇到隐状态包含多个变量的情况, 而使用元组可以更容易地处理些。

# 用于在0时刻是给定一个初始化的隐状态
def init_rnn_state(batch_size, num_hiddens, device):
    return (torch.zeros((batch_size, num_hiddens), device=device), )

[下面的rnn函数定义了如何在一个时间步内计算隐状态和输出。 ] 循环神经网络模型通过inputs最外层的维度实现循环, 以便逐时间步更新小批量数据的隐状态H。 此外,这里使用 函数作为激活函数。 如 4.1节 所述, 当元素在实数上满足均匀分布时, 函数的平均值为0。

# state是初始化的隐状态
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:
        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)
    # torch.cat(outputs, dim=0)的列数不变,仍为词表大小,行数变为批量大小×时间步
    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):
        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())
Y, new_state = net(X.to(d2l.try_gpu()), state)
Y.shape, len(new_state), new_state[0].shape

输出结果:

(torch.Size([10, 28]), 1, torch.Size([2, 512]))

我们可以看到输出形状是(时间步数 × 批量大小,词表大小), 而隐状态形状保持不变,即(批量大小,隐藏单元数)。

预测

让我们[首先定义预测函数来生成prefix之后的新字符 ], 其中的prefix是一个用户提供的包含多个字符的字符串。 在循环遍历prefix中的开始字符时, 我们不断地将隐状态传递到下一个时间步,但是不生成任何输出。 这被称为预热(warm-up)期, 因为在此期间模型会自我更新(例如,更新隐状态), 但不会进行预测。 预热期结束后,隐状态的值通常比刚开始的初始值更适合预测, 从而预测字符并输出它们。

# num_preds是需要预测的词数/字符数
def predict_ch8(prefix, num_preds, net, vocab, device):  #@save
    """在prefix后面生成新字符"""
    state = net.begin_state(batch_size=1, device=device)
    # 初始outputs是prfix第一个字符对应的vocab索引值
    outputs = [vocab[prefix[0]]]
    # 把最近预测的词outpus[-1]作为输入,批量大小=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]) # outputs添加真实值,没有误差
    for _ in range(num_preds):  # 预测num_preds步,存储输出,也即预测值
        y, state = net(get_input(), state)
        # y.argmax(dim=1)指按词表大小,去除最大值的坐标值,也即预测到的字符索引值
        outputs.append(int(y.argmax(dim=1).reshape(1)))
        # 将outputs存储的预测索引值转成字符/词
    return ''.join([vocab.idx_to_token[i] for i in outputs])

现在我们可以测试predict_ch8函数。 我们将前缀指定为time traveller, 并基于这个前缀生成10个后续字符。 鉴于我们还没有训练网络,它会生成荒谬的预测结果。

predict_ch8('time traveller ', 10, net, vocab, d2l.try_gpu())

输出结果:

'time traveller woiensxjqn'

[梯度裁剪]

对于长度为 的序列,我们在迭代中计算这 个时间步上的梯度, 将会在反向传播过程中产生长度为 O(T) 的矩阵乘法链。 如 4.8节 所述, 当 较大时,它可能导致数值不稳定, 例如可能导致梯度爆炸或梯度消失。 因此,循环神经网络模型往往需要额外的方式来支持稳定训练。

一般来说,当解决优化问题时,我们对模型参数采用更新步骤。 假定在向量形式的 中, 或者在小批量数据的负梯度 方向上。 例如,使用 作为学习率时,在一次迭代中, 我们将 更新为 。 如果我们进一步假设目标函数 表现良好, 即函数 在常数 下是利普希茨连续的 (Lipschitz continuous)。 也就是说,对于任意 我们有:

在这种情况下,我们可以安全地假设: 如果我们通过 更新参数向量,则

这意味着我们不会观察到超过 的变化。 这既是坏事也是好事。 坏的方面,它限制了取得进展的速度; 好的方面,它限制了事情变糟的程度,尤其当我们朝着错误的方向前进时。

有时梯度可能很大,从而优化算法可能无法收敛。 我们可以通过降低 的学习率来解决这个问题。 但是如果我们很少得到大的梯度呢? 在这种情况下,这种做法似乎毫无道理。 一个流行的替代方案是通过将梯度 投影回给定半径 (例如 )的球来裁剪梯度 。 如下式:

通过这样做,我们知道梯度范数永远不会超过 , 并且更新后的梯度完全与 的原始方向对齐。 它还有一个值得拥有的副作用, 即限制任何给定的小批量数据(以及其中任何给定的样本)对参数向量的影响, 这赋予了模型一定程度的稳定性。 梯度裁剪提供了一个快速修复梯度爆炸的方法, 虽然它并不能完全解决问题,但它是众多有效的技术之一。

下面我们定义一个函数来裁剪模型的梯度, 模型是从零开始实现的模型或由高级API构建的模型。 我们在此计算了所有模型参数的梯度的范数。

def grad_clipping(net, theta):  #@save
    """裁剪梯度"""
    if isinstance(net, nn.Module): # 如果是用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))
    # 如果范数超过theta,将所有参数的梯度 × theta / norm
    if norm > theta:
        for param in params:
            param.grad[:] *= theta / norm

训练

在训练模型之前,让我们[定义一个函数在一个迭代周期内训练模型 ]。 它与我们训练 3.6节 模型的方式有三个不同之处。

  1. 序列数据的不同采样方法(随机采样和顺序分区)将导致隐状态初始化的差异。
  2. 我们在更新模型参数之前裁剪梯度。 这样的操作的目的是,即使训练过程中某个点上发生了梯度爆炸,也能保证模型不会发散。
  3. 我们用困惑度来评价模型。如 8.4.4节 所述,这样的度量确保了不同长度的序列具有可比性。

具体来说,当使用顺序分区时, 我们只在每个迭代周期的开始位置初始化隐状态。 由于下一个小批量数据中的第i个子序列样本 与当前第i个子序列样本相邻, 因此当前小批量数据最后一个样本的隐状态, 将用于初始化下一个小批量数据第一个样本的隐状态。 这样,存储在隐状态中的序列的历史信息 可以在一个迭代周期内流经相邻的子序列。 然而,在任何一点隐状态的计算, 都依赖于同一迭代周期中前面所有的小批量数据, 这使得梯度计算变得复杂。 为了降低计算量,在处理任何一个小批量数据之前, 我们先分离梯度,使得隐状态的梯度计算总是限制在一个小批量数据的时间步内。

当使用随机抽样时,因为每个样本都是在一个随机位置抽样的, 因此需要为每个迭代周期重新初始化隐状态。 与 3.6节 中的 train_epoch_ch3函数相同, updater是更新模型参数的常用函数。 它既可以是从头开始实现的d2l.sgd函数, 也可以是深度学习框架中内置的优化函数。

#@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_() 
                # 读取新的iter后,将隐状态从计算图中分离出来,以避免不必要的梯度计算,从而提高效率和减少内存使用
            else:
                # state对于nn.LSTM或对于我们从零开始实现的模型是个元组,每个元素是一个张量,需要遍历每个元素
                for s in state:
                    s.detach_()
        # 把Y的形状转为(时间步数×批量大小,词表大小)
        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()
        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()

[循环神经网络模型的训练函数既支持从零开始实现, 也可以使用高级API来实现。]

#@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'))

[现在,我们训练循环神经网络模型。] 因为我们在数据集中只使用了10000个词元, 所以模型需要更多的迭代周期来更好地收敛。

num_epochs, lr = 500, 1
train_ch8(net, train_iter, vocab, lr, num_epochs, d2l.try_gpu())

输出结果:

困惑度 1.0, 18788.2 词元/秒 cpu

time travelleryou can show black is white by argument said filby

travelleryou can show black is white by argument said filby

[最后,让我们检查一下使用随机抽样方法的结果。]

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)

输出结果:

困惑度 1.4, 19979.5 词元/秒 cpu

time travellerit s against reason said filbywhat uxurious for in

travellerit s against reason said filbywhat uxurious for in

从零开始实现上述循环神经网络模型, 虽然有指导意义,但是并不方便。 在下一节中,我们将学习如何改进循环神经网络模型。 例如,如何使其实现地更容易,且运行速度更快。

小结

  • 我们可以训练一个基于循环神经网络的字符级语言模型,根据用户提供的文本的前缀生成后续文本。
  • 一个简单的循环神经网络语言模型包括输入编码、循环神经网络模型和输出生成。
  • 循环神经网络模型在训练以前需要初始化状态,不过随机抽样和顺序划分使用初始化方法不同。
  • 当使用顺序划分时,我们需要分离梯度以减少计算量。
  • 在进行任何预测之前,模型通过预热期进行自我更新(例如,获得比初始值更好的隐状态)。
  • 梯度裁剪可以防止梯度爆炸,但不能应对梯度消失。

练习

  1. 尝试说明独热编码等价于为每个对象选择不同的嵌入表示。
    解:
    嵌入表示是将每个对象映射到一个密集的低维向量空间中。这些向量是通过学习得到的,能够捕捉对象之间的语义关系。
    而独热编码是为每个类别值创建了一个唯一的二进制向量,除了表示该类别的一个位置是1以外,其余位置都是0,仅表示类别存在与否,不包含任何语义信息。
    但独热编码等价于为每个对象选择不同的嵌入表示,在于独热编码为每个类别值分配了一个唯一的N维嵌入向量(N为类别数),相当于为每个对象创建了一个N维空间中的点,即选择不同的嵌入表示。
  2. 通过调整超参数(如迭代周期数、隐藏单元数、小批量数据的时间步数、学习率等)来改善困惑度。
  • 困惑度可以降到多少?
  • 用可学习的嵌入表示替换独热编码,是否会带来更好的表现?
  • 如果用H.G.Wells的其他书作为数据集时效果如何, 例如*世界大战*?

解:
1)顺序分区的困惑度已经降到最低(1.0)了,尝试调整超参数来改善随机抽样的困惑度,困惑度降低到1.2,代码如下:

# 增加小批量数据的时间步数
batch_size, new_num_steps = 32, 70
train_iter, vocab = d2l.load_data_time_machine(batch_size, new_num_steps)
# 增加迭代周期数,降低学习率,隐藏单元数不变
num_epochs, lr = 1000, 0.5
net2 = RNNModelScratch(len(vocab), num_hiddens, d2l.try_gpu(), get_params,
                      init_rnn_state, rnn)
train_ch8(net2, train_iter, vocab, lr, num_epochs, d2l.try_gpu(),
          use_random_iter=True)

输出结果:
困惑度 1.2, 18029.6 词元/秒 cpu
time traveller came back andfilby s anecdote collapsedthe thing
traveller held in his hand was a glitteringmetallic framewo

2)用可学习的嵌入表示替换独热编码,随机抽样的困惑度收敛速度更快,运行速度也更快,并且困惑度进一步降低到1.1,代码如下:

class RNNModelScratch_Embedding:
    # embedding_dim是嵌入向量的维度,是一个超参数
    def __init__(self, vocab_size, num_hiddens, device,
                 get_params, init_state, forward_fn, embedding_dim):
        self.vocab_size, self.num_hiddens = vocab_size, num_hiddens
        # num_inputs要改为embedding_dim,以匹配嵌入表示
        self.params = get_params(embedding_dim, num_hiddens, device)
        self.init_state = init_state
        self.forward_fn = forward_fn
        # 添加嵌入层
        self.embedding = nn.Embedding(num_embeddings=vocab_size, embedding_dim=embedding_dim)

    def __call__(self, X, state):
        # 使用嵌入层代替独热编码
        # 输入形状为(批量大小,时间步数),输出形状为(批量大小,时间步数,嵌入向量的维度)
        X = self.embedding(X) 
        # 将X形状改为(时间步数,批量大小,嵌入向量的维度)
        X = X.permute(1, 0, 2) 
        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)
batch_size, new_num_steps = 32, 70
train_iter, vocab = d2l.load_data_time_machine(batch_size, new_num_steps)
num_epochs, lr, embedding_dim = 1000, 0.5, 300
net3 = RNNModelScratch_Embedding(len(vocab), num_hiddens, d2l.try_gpu(), get_params,
                      init_rnn_state, rnn, embedding_dim)
train_ch8(net3, train_iter, vocab, lr, num_epochs, d2l.try_gpu(),
          use_random_iter=True)

输出结果:
困惑度 1.1, 33282.1 词元/秒 cpu
time traveller held in his hand was a glitteringmetallic framewo
traveller held in his hand was a glitteringmetallic framewo

3)先下载《世界大战》的.txt文件: - 打开网址http://www.gutenberg.org/ebooks/36; - 找到文件列表最下方的"There may be more files related to this item.",点击"more files"; - 下载"36-0.zip",解压获得"36-0.txt"文件。 然后使用"36-0.txt"作为数据集进行训练和预测,代码如下:

# 文本预处理
import re

text = '.../chapter_recurrent-neural-networks/36-0.txt'

with open(text, 'r') as f:
    lines = f.readlines()
for line in lines:
    re.sub('[^A-Za-z]+', ' ', line).strip().lower() 

def load_corpus_the_war_of_the_worlds(max_tokens=-1): 
    tokens = d2l.tokenize(lines, 'char')
    vocab = d2l.Vocab(tokens)
    corpus = [vocab[token] for line in tokens for token in line]

    if max_tokens > 0: 
        corpus = corpus[:max_tokens]
    return corpus, vocab

corpus, vocab = load_corpus_the_war_of_the_worlds()
len(corpus), len(vocab)
(356616, 92)
# 构建加载世界大战数据集的迭代器
class SeqDataLoader_for_the_war_of_the_worlds: 
    def __init__(self, batch_size, num_steps, use_random_iter, max_tokens):
        if use_random_iter:
            self.data_iter_fn = d2l.seq_data_iter_random
        else:
            self.data_iter_fn = d2l.seq_data_iter_sequential
        self.corpus, self.vocab = load_corpus_the_war_of_the_worlds(max_tokens)
        self.batch_size, self.num_steps = batch_size, num_steps

    def __iter__(self):
        return self.data_iter_fn(self.corpus, self.batch_size, self.num_steps)

# 返回世界大战数据集的迭代器和词表
def load_data_the_war_of_the_worlds(batch_size, num_steps, 
                                    use_random_iter=False, max_tokens=10000):
    data_iter = SeqDataLoader_for_the_war_of_the_worlds(
        batch_size, num_steps, use_random_iter, max_tokens)
    return data_iter, data_iter.vocab
# 训练与预测
batch_size, new_num_steps = 32, 70
train_iter, vocab = load_data_the_war_of_the_worlds(batch_size, new_num_steps)

def train_ch8_2_3(net, train_iter, vocab, lr, num_epochs, device,
              use_random_iter=False):
    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('the war of the worlds'))
            animator.add(epoch + 1, [ppl])
    print(f'困惑度 {ppl:.1f}, {speed:.1f} 词元/秒 {str(device)}')
    print(predict('the war of the worlds'))
    print(predict('the war'))

num_epochs, lr, embedding_dim = 1000, 0.5, 300
net4 = RNNModelScratch_Embedding(len(vocab), num_hiddens, d2l.try_gpu(), get_params,
                      init_rnn_state, rnn, embedding_dim)
train_ch8_2_3(net4, train_iter, vocab, lr, num_epochs, d2l.try_gpu(), 
              use_random_iter=True)

输出结果:
困惑度 1.1, 33616.9 词元/秒 cpu
the war of the worlds

Author: H. G. Wells

Release Date: July 1992 [eB
the warden to
themselves and ready to welcome a missionar

  1. 修改预测函数,例如使用采样,而不是选择最有可能的下一个字符。
  • 会发生什么?
  • 调整模型使之偏向更可能的输出,例如,当 α>1 ,从 q(xt∣xt−1,...,x1)∝P(xt∣xt−1,...,x1)α 中采样。

解:
1)修改预测函数以使用采样而不是贪婪地选择最有可能的下一个字符,可能发生的变化:

  • 多样性增加:采样方法允许模型生成更加多样化的文本,因为它不是总是选择概率最高的字符,而是从概率分布中随机选择。
  • 减少重复:贪婪选择最高概率的字符可能会导致模型陷入重复的模式。采样可以帮助打破这种模式,生成更丰富的文本。
  • 可能引入错误:采样可能会引入一些错误,因为不是每个预测的字符都是基于最高概率的选择。
  • 降低生成文本的连贯性:使用采样可能会降低文本的连贯性,因为模型不再总是选择最可能的下一个词。
  • 性能影响:采样可能会影响模型的性能评估,因为它引入了随机性。这可能使模型的输出更难以预测和评估。

2)可以通过调整温度参数来控制采样的随机性。较高的温度值会增加随机性,而较低的温度值会使采样接近贪婪选择。

α 代表温度参数的逆,即 。 当 时,即 ,分布 c 会变得更加尖锐,这意味着更高概率的输出会被赋予更多权重,而低概率的输出则几乎不会被采样到。这导致模型在生成文本时更加保守,更可能选择概率最高的字符,接近于贪婪选择。
代码如下:

# 增加参数alpha
def predict_ch8_adjusted_sampling(prefix, num_preds, net, vocab, device, alpha=1.0):
    state = net.begin_state(batch_size=1, device=device)
    outputs = [vocab[prefix[0]]]
    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):
        y, state = net(get_input(), state)
        probs = F.softmax(y * alpha, dim=1)
        # 采样一个字符索引
        sample = torch.multinomial(probs, 1)
        outputs.append(int(sample.item()))

    return ''.join([vocab.idx_to_token[i] for i in outputs])
# 训练与预测
batch_size, num_steps = 32, 35
train_iter, vocab = d2l.load_data_time_machine(batch_size, num_steps)

def train_ch8_3_2(net, train_iter, vocab, lr, num_epochs, device,
              use_random_iter=False, alpha=1.0):
    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_adjusted_sampling(prefix, 50, net, vocab, device, alpha)
    # 训练和预测
    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
net5 = RNNModelScratch(len(vocab), num_hiddens, d2l.try_gpu(), get_params,
                      init_rnn_state, rnn)
# alpha=0.5,采样更倾向于随机的字符,困惑度较高
train_ch8_3_2(net5, train_iter, vocab, lr, num_epochs, d2l.try_gpu(), 
              use_random_iter=True, alpha=0.5)

输出结果:
困惑度 1.4, 18378.5 词元/秒 cpu
time travellerit s abovelock insaigatixepqyit or its bacontcatle
travelleryou can showpeleshe dam spisa doz nos tousari if n

net5 = RNNModelScratch(len(vocab), num_hiddens, d2l.try_gpu(), get_params,
                      init_rnn_state, rnn)
# alpha=2.0,采样更倾向于概率最高的字符,困惑度较低
train_ch8_3_2(net5, train_iter, vocab, lr, num_epochs, d2l.try_gpu(), 
              use_random_iter=True, alpha=2.0)

输出结果:
困惑度 1.3, 20347.7 词元/秒 cpu
time travellerit s against reason said filbywhat they have to sa
traveller for so it will be convenient to speak of himwas e

  1. 在不裁剪梯度的情况下运行本节中的代码会发生什么?

解:
不裁剪梯度会出现梯度爆炸、数值不稳定的情况,困惑度曲线陡峭不平衡。代码如下:

def train_epoch_ch8_without_clipping(net, train_iter, loss, updater, device, use_random_iter):
    state, timer = None, d2l.Timer()
    metric = d2l.Accumulator(2) 
    for X, Y in train_iter:
        if state is None or use_random_iter:
            state = net.begin_state(batch_size=X.shape[0], device=device)
        else:
            if isinstance(net, nn.Module) and not isinstance(state, tuple):
                state.detach_()
            else:
                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()
        if isinstance(updater, torch.optim.Optimizer):
            updater.zero_grad()
            l.backward()
            updater.step()
        else:
            l.backward()
            updater(batch_size=1)
        metric.add(l * y.numel(), y.numel())
    return math.exp(metric[0] / metric[1]), metric[1] / timer.stop()
def train_ch8_without_clipping(net, train_iter, vocab, lr, num_epochs, device,
              use_random_iter=False):
    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_without_clipping(
            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
net6 = RNNModelScratch(len(vocab), num_hiddens, d2l.try_gpu(), get_params,
                      init_rnn_state, rnn)
train_ch8_without_clipping(net6, train_iter, vocab, lr, num_epochs, d2l.try_gpu(),
          use_random_iter=True)
困惑度 13434806621595245141149819618422986733015421973844048478208.0, 17603.2 词元/秒 cpu
time travellereeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
travellereeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
  1. 更改顺序划分,使其不会从计算图中分离隐状态。运行时间会有变化吗?困惑度呢?

解:
运行时间缩短,但困惑度增加。代码如下:

def train_epoch_ch8_without_detach(net, train_iter, loss, updater, device, use_random_iter):
    state, timer = None, d2l.Timer()
    metric = d2l.Accumulator(2) 
    for X, Y in train_iter:
        if use_random_iter:
            state = net.begin_state(batch_size=X.shape[0], device=device)
        else:
        # 顺序划分时,先对输入和标签进行设备变换和形状变换,再进行前向计算和反向传播,避免隐状态从计算图中分离。
            y = Y.T.reshape(-1)
            X, y = X.to(device), y.to(device)
            state = net.begin_state(batch_size=X.shape[0], device=device)
            y_hat, state = net(X, state)
            l = loss(y_hat, y.long()).mean()
        if isinstance(updater, torch.optim.Optimizer):
            updater.zero_grad()
            l.backward(retain_graph=True)
            grad_clipping(net, 1)
            updater.step()
        else:
            l.backward(retain_graph=True)
            grad_clipping(net, 1)
            updater(batch_size=1)
        metric.add(l * y.numel(), y.numel())
    return math.exp(metric[0] / metric[1]), metric[1] / timer.stop()
def train_ch8_without_detach(net, train_iter, vocab, lr, num_epochs, device,
              use_random_iter=False):
    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_without_detach(
            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
net6 = RNNModelScratch(len(vocab), num_hiddens, d2l.try_gpu(), get_params,
                      init_rnn_state, rnn)
train_ch8_without_detach(net6, train_iter, vocab, lr, num_epochs, d2l.try_gpu())

输出结果:
困惑度 1.4, 19898.7 词元/秒 cpu
time travellerit s against reason said filbycan a cube that does
travellerit s against reason said filbycan a cube that does

  1. 用ReLU替换本节中使用的激活函数,并重复本节中的实验。我们还需要梯度裁剪吗?为什么?

解:
使用ReLU作为激活函数,不需要梯度裁剪,因为梯度在输入为正时恒为1,输入为负时恒为0,因此在反向传播时通常不会出现数值不稳定现象。代码如下:

def rnn_relu(inputs, state, params):
    # inputs的形状:(时间步数量,批量大小,词表大小)
    W_xh, W_hh, b_h, W_hq, b_q = params
    H, = state
    outputs = []
    # X的形状:(批量大小,词表大小)
    for X in inputs:
        H = torch.relu(torch.mm(X, W_xh) + torch.mm(H, W_hh) + b_h)
        Y = torch.mm(H, W_hq) + b_q
        outputs.append(Y)
    return torch.cat(outputs, dim=0), (H,)
net7 = RNNModelScratch(len(vocab), num_hiddens, d2l.try_gpu(), get_params,
                      init_rnn_state, rnn_relu)
train_ch8(net7, train_iter, vocab, lr, num_epochs, d2l.try_gpu())

输出结果:
困惑度 1.0, 17932.4 词元/秒 cpu
time traveller for so it will be convenient to speak of himwhs e
traveller with a slight accession ofcheerfulness really thi

# 不使用梯度剪裁
net7 = RNNModelScratch(len(vocab), num_hiddens, d2l.try_gpu(), get_params,
                      init_rnn_state, rnn_relu)
train_ch8_without_clipping(net7, train_iter, vocab, lr, num_epochs, d2l.try_gpu())

输出结果:
困惑度 1.0, 18880.9 词元/秒 cpu
time travelleryou can show black is white by argument said filby
travelleryou can show black is white by argument said filby

相关推荐
小二·9 分钟前
java基础面试题笔记(基础篇)
java·笔记·python
wusong9993 小时前
mongoDB回顾笔记(一)
数据库·笔记·mongodb
猫爪笔记3 小时前
前端:HTML (学习笔记)【1】
前端·笔记·学习·html
Resurgence033 小时前
【计组笔记】习题
笔记
pq113_64 小时前
ftdi_sio应用学习笔记 3 - GPIO
笔记·学习·ftdi_sio
老艾的AI世界4 小时前
AI翻唱神器,一键用你喜欢的歌手翻唱他人的曲目(附下载链接)
人工智能·深度学习·神经网络·机器学习·ai·ai翻唱·ai唱歌·ai歌曲
爱米的前端小笔记4 小时前
前端八股自学笔记分享—页面布局(二)
前端·笔记·学习·面试·求职招聘
寒笙LED7 小时前
C++详细笔记(六)string库
开发语言·c++·笔记
sp_fyf_20247 小时前
【大语言模型】ACL2024论文-19 SportsMetrics: 融合文本和数值数据以理解大型语言模型中的信息融合
人工智能·深度学习·神经网络·机器学习·语言模型·自然语言处理
CoderIsArt7 小时前
基于 BP 神经网络整定的 PID 控制
人工智能·深度学习·神经网络