pytorch学习笔记(19)

# 循环神经网络

循环神经网络(recurrent neural networks,RNNs) 是具有隐状态的神经网络。隐藏层和隐状态指的是两个截然不同的概念,隐藏层是在从输入到输出的路径上(以观测角度来理解)的隐藏的层, 而隐状态则是在给定步骤所做的任何事情(以技术角度来定义)的输入, 并且这些状态只能通过先前时间步的数据来计算。

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

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)


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  # 生成指定形状 shape 的随机数张量

    # 隐藏层参数
    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]  # 所有参数都被添加到 params 列表中。
    for param in params:  # 通过循环,所有参数的 requires_grad 属性被设置为 True,这表示这些参数将用于计算梯度,以便进行反向传播和训练神经网络。
        param.requires_grad_(True)
    return params

初始化循环神经网络(RNN)的隐藏状态。

def init_rnn_state(batch_size, num_hiddens, device):
    return (torch.zeros((batch_size, num_hiddens), device=device), )  # 张量的形状是 (batch_size, num_hiddens),其中每一行表示一个样本的隐藏状态。
def rnn(inputs, state, params):   # inputs 是输入数据的一个序列,其形状为 (时间步数量,批量大小,词表大小)。
    # 在时间序列中,每个时间步都有一个输入,时间步数量 表示时间序列的长度,批量大小 表示同时处理的样本数量,词表大小 表示词汇表的大小。
    # state 是一个包含一个张量的元组 (H,),其中 H 是隐藏状态张量。params 是包含RNN模型参数的列表
    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)  # 更新隐藏状态 H
        Y = torch.mm(H, W_hq) + b_q  # 计算输出 Y
        outputs.append(Y)
    return torch.cat(outputs, dim=0), (H,)  # 执行RNN模型的前向传播,从输入数据中生成输出序列,并更新隐藏状态以在时间步之间传递信息。、

建一个类来包装这些函数, 并存储从零开始实现的循环神经网络模型的参数。

class RNNModelScratch:
    def __init__(self, vocab_size, num_hiddens, device,  # vocab_size:词汇表的大小,表示输入和输出的词汇表大小。num_hiddens:隐藏层中的隐藏单元数量。
                 get_params, init_state, forward_fn):  # get_params:一个函数,用于获取 RNN 模型的参数。init_state:一个函数,用于初始化 RNN 模型的隐藏状态。forward_fn:一个函数,用于执行 RNN 模型的前向传播。
        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 和 RNN 模型的当前状态 state,并返回 RNN 模型的输出结果。
        X = F.one_hot(X.T, self.vocab_size).type(torch.float32)  #  X 被独热编码成浮点数类型,然后传递给 forward_fn 来执行前向传播。
        return self.forward_fn(X, state, self.params)  # 然后传递给 forward_fn 来执行前向传播。

    def begin_state(self, batch_size, device):  # 用于初始化 RNN 模型的隐藏状态。
        return self.init_state(batch_size, self.num_hiddens, device)

# 预测

def predict_ch8(prefix, num_preds, net, vocab, device):  # prefix 是输入的前缀字符串,用于初始化生成过程。num_preds 是要生成的字符的数量。vocab 包含词汇表信息,允许将模型输出的整数转换回字符。
    """在prefix后面生成新字符"""
    state = net.begin_state(batch_size=1, device=device)  # 初始化 RNN 模型的隐藏状态 state。这里使用批处理大小为 1,因为我们一次生成一个字符
    outputs = [vocab[prefix[0]]]  # outputs 是一个列表,用于存储生成的字符序列。初始时,它包含前缀字符串的第一个字符,即 vocab[prefix[0]]。
    get_input = lambda: torch.tensor([outputs[-1]], device=device).reshape((1, 1))  # get_input 是一个函数,用于获取下一个时间步的输入。它返回一个形状为 (1, 1) 的张量,其中 1 表示批处理大小和序列长度。
    for y in prefix[1:]:  # 预热期  我们通过将每个字符作为输入传递给 RNN 模型来更新隐藏状态,但不存储输出。这有助于模型适应前缀字符串。
        _, state = net(get_input(), state)
        outputs.append(vocab[y])
    for _ in range(num_preds):  # 预测num_preds步
        y, state = net(get_input(), state)  # 使用 get_input() 获取下一个输入字符
        outputs.append(int(y.argmax(dim=1).reshape(1)))  # 择输出中概率最高的字符作为预测字符,并将其整数索引添加到 outputs 列表中。
    return ''.join([vocab.idx_to_token[i] for i in outputs])  #  将生成的整数字符序列转换回字符,并使用 join 函数将它们连接起来,形成生成的字符串。

# 梯度裁剪

def grad_clipping(net, theta):  # net 是输入的神经网络模型,theta 是梯度裁剪的阈值,用于限制梯度的范数。
    if isinstance(net, nn.Module):  # 如果 net 是 nn.Module 类型,它将获取模型中所有具有梯度的参数,并将其存储在 params 列表中。如果 net 不是 nn.Module 类型,它将使用 net.params 来获取参数。
        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))  # 算所有参数的梯度的范数 norm,这是所有参数梯度的二范数之和。
    if norm > theta:  # 如果 norm 大于指定的阈值 theta,则梯度裁剪将被执行。对于每个参数,函数将梯度值按比例缩放,以确保梯度的范数不超过阈值 theta。
        for param in params:
            param.grad[:] *= theta / norm

# 训练

def train_epoch_ch8(net, train_iter, loss, updater, device, use_random_iter):
    # train_iter 是用于训练的数据迭代器,它产生输入数据 X 和目标数据 Y。loss 是损失函数,用于衡量模型的预测与实际目标的差距。updater 是用于更新模型参数的优化器。
    # use_random_iter 是一个布尔值,指示是否使用随机迭代。如果设置为 True,则在每个迭代周期的第一个迭代中将初始化隐藏状态,否则将在每个迭代中使用先前迭代的最终隐藏状态。
    """训练网络一个迭代周期(定义见第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 的类型,以确保它是可以进行梯度计算的张量
                # state对于nn.GRU是个张量
                state.detach_()
            else:
                # state对于nn.LSTM或对于我们从零开始实现的模型是个张量
                for s in state:
                    s.detach_()
        y = Y.T.reshape(-1)  # 将目标数据 Y 转置后展平为一维张量 y。
        X, y = X.to(device), y.to(device)
        y_hat, state = net(X, state)  # 使用神经网络模型 net 对输入数据进行前向传播,得到预测结果 y_hat 和更新后的隐藏状态 state。
        l = loss(y_hat, y.long()).mean()
        if isinstance(updater, torch.optim.Optimizer):  # 如果 updater 是 PyTorch 的优化器,清零优化器的梯度,然后执行反向传播和参数更新。否则,直接执行梯度裁剪和自定义的参数更新操作。
            updater.zero_grad()
            l.backward()
            grad_clipping(net, 1)  # 函数执行梯度裁剪,将梯度限制在阈值 1 内。
            updater.step() # 调用 updater.step() 来更新模型的参数。
        else:
            l.backward()
            grad_clipping(net, 1)
            # 因为已经调用了mean函数
            updater(batch_size=1)
        metric.add(l * y.numel(), y.numel())  # 使用累加器 metric 更新训练损失之和和处理的词元数量
    return math.exp(metric[0] / metric[1]), metric[1] / timer.stop()


def train_ch8(net, train_iter, vocab, lr, num_epochs, device,  # 包含词汇表信息,用于生成文本。
              use_random_iter=False):
    """训练模型(定义见第8章)"""
    loss = nn.CrossEntropyLoss()  # 定义了交叉熵损失函数 loss,用于度量模型的预测与实际目标的差距。
    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)  # 用于生成文本预测。这个函数接受一个前缀字符串,并生成模型生成的文本序列,长度为 50 个字符。
    # 训练和预测
    for epoch in range(num_epochs):
        ppl, speed = train_epoch_ch8(  # 调用 train_epoch_ch8 函数,用于训练一个迭代周期,并获取每个词元上的指数损失 ppl 和处理速度 speed。
            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())

# 循环神经网络的简洁实现

构造一个具有256个隐藏单元的单隐藏层的循环神经网络层rnn_layer

num_hiddens = 256  # len(vocab):这是词汇表的大小,也就是词汇表中不同字符的数量。RNN层的输入维度将等于词汇表的大小,因为它将处理字符级别的文本数据。
rnn_layer = nn.RNN(len(vocab), num_hiddens)  # RNN层配置为具有256个隐藏单元。

使用张量来初始化隐状态,它的形状是(隐藏层数,批量大小,隐藏单元数)。

rnn_layer的“输出”(Y)不涉及输出层的计算: 它是指每个时间步的隐状态,这些隐状态可以用作后续输出层的输入。

X = torch.rand(size=(num_steps, batch_size, len(vocab)))  #  X 的随机张量,其形状为 (num_steps, batch_size, len(vocab))。这个张量用于表示输入数据,num_steps 是时间步的数量,表示模型将在多少个时间步内处理输入数据。batch_size 是批量大小,表示在每个时间步处理多少个样本。len(vocab) 是词汇表的大小,表示输入数据的词表大小。在这里,它代表了字符级别的词汇表的大小。
Y, state_new = rnn_layer(X, state)  # Y 是输出张量,它包含 RNN 层在每个时间步上生成的输出。state_new 是更新后的 RNN 状态,它包含 RNN 层在处理完输入数据后的最终状态。
Y.shape, state_new.shape

 接下来创建一个RNNModel。

#@save
class RNNModel(nn.Module):
    """循环神经网络模型"""
    def __init__(self, rnn_layer, vocab_size, **kwargs):  # rnn_layer:RNN层,它定义了模型的循环结构。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层是否是双向的来选择线性变换层的输出维度。
        if not self.rnn.bidirectional:
            self.num_directions = 1  # 如果RNN不是双向的,则设置 self.num_directions 为 1,然后创建一个线性层 self.linear,将隐藏状态映射到词汇表的大小。
            self.linear = nn.Linear(self.num_hiddens, self.vocab_size)
        else:
            self.num_directions = 2  # 如果RNN是双向的,则 self.num_directions 设置为 2,并且线性层 self.linear 的输入维度为 self.num_hiddens * 2,以将双向RNN的隐藏状态映射到词汇表的大小。
            self.linear = nn.Linear(self.num_hiddens * 2, self.vocab_size)

    def forward(self, inputs, state):  # inputs:输入数据,通常是文本序列。state:RNN的隐藏状态,用于存储和传递RNN的内部状态。
        X = F.one_hot(inputs.T.long(), self.vocab_size)  # 首先将输入转置,self.vocab_size 表示词汇表的大小。然后将其转化为独热编码。
        X = X.to(torch.float32)
        Y, state = self.rnn(X, state)  # 将转换后的输入数据 X 和隐藏状态 state 传递给RNN层 self.rnn 进行前向传播。Y 包含RNN层在每个时间步上生成的输出,state 存储更新后的RNN状态。
        
        output = self.linear(Y.reshape((-1, Y.shape[-1])))  # Y 的形状被调整为 (时间步数*批量大小, 隐藏单元数),然后通过线性变换映射到 (时间步数*批量大小, 词汇表大小) 的输出。
        return output, state

    def begin_state(self, device, batch_size=1):  # 创建RNN的初始状态。
        if not isinstance(self.rnn, nn.LSTM):  # 首先检查RNN层的类型,如果RNN层不是LSTM类型,则创建一个全零的张量作为隐藏状态,并根据 self.num_directions 和 self.rnn.num_layers 确定张量的形状。
            # 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))

然后我们进行训练,训练的代码即是上面的train_ch8

与之前从零开始实现相比,我们的困惑度下降了,花费的时间也减少了

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值