深度学习实践 7:手撕RNN网络

思路:

        这段代码实现了一个循环神经网络(RNN)模型,并使用训练数据对其进行训练。

代码流程:

        1.导入所需的库:

import math
import torch
from torch import nn
from torch.nn import functional as F
from main import load_data_time_machine
import matplotlib.pyplot as plt

         这部分代码导入了需要使用的库,包括数学库math、PyTorch库、PyTorch的nn模块、PyTorch的functional模块(用于定义激活函数)、自定义的load_data_time_machine函数和绘图库matplotlib.pyplot

        2.设置训练所需的超参数和加载训练数据,加载训练数据具体看:函数流程

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

        这部分代码调用load_data_time_machine函数加载《时间机器》数据集,并设置了批量大小(batch_size)和时间步数(num_steps)。同时,返回了训练数据迭代器和词汇表。

        3.定义梯度更新函数 sgd

def sgd(params, lr, batch_size):
    with torch.no_grad():
        for param in params:
            param -= lr * param.grad / batch_size
            param.grad.zero_()

        这个函数接受模型参数、学习率(lr)和批量大小(batch_size),并根据SGD算法更新模型参数。对于每个参数,使用梯度下降公式进行更新,并将梯度清零。 

        4.定义累加器类 Accumulator

class Accumulator:
    def __init__(self, n):
        self.data = [0.0] * n

    def add(self, *args):
        self.data = [a + float(b) for a, b in zip(self.data, args)]

    def reset(self):
        self.data = [0.0] * len(self.data)

    def __getitem__(self, idx):
        return self.data[idx]

         这个类用于在n个变量上进行累加操作。它具有add方法,可以将输入的参数与内部数据进行累加。同时,还提供了reset方法用于将累加的数据重置为零,并提供了__getitem__方法用于获取累加的结果。

        5.定义模型参数初始化函数 get_params

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

        这部分代码定义了一个函数get_params,用于初始化模型的参数。函数接受词汇表大小(vocab_size)、隐藏单元数量(num_hiddens)和设备(device)作为输入。

        在函数内部,首先将输入和输出的数量设置为词汇表大小(num_inputs = num_outputs = vocab_size)。

        然后定义了一个辅助函数normal,用于生成服从标准正态分布的随机数,并将其乘以0.01,以限制参数的初始值范围。

        接下来,使用normal函数生成了参数W_xhW_hhb_hW_hqb_q,它们分别表示输入到隐藏层的权重、隐藏层到隐藏层的权重、隐藏层的偏置、隐藏层到输出层的权重和输出层的偏置。这些参数都被初始化为随机值,并使用torch.zeros生成的全零张量作为偏置。

        最后,将这些参数存储在列表params中,并将它们的requires_grad属性设置为True,以便在训练过程中计算梯度。最后,将参数列表返回。

        6.定义 RNN 的初始状态函数 init_rnn_state 和 RNN 前向传播函数 rnn

def init_rnn_state(batch_size, num_hiddens, device):
    return (torch.zeros((batch_size, num_hiddens), device=device),)

def rnn(inputs, state, params):
    W_xh, W_hh, b_h, W_hq, b_q = params
    H, = state
    outputs = []
    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)
    return torch.cat(outputs, dim=0), (H,)

         

        这个函数接受批量大小(batch_size)、隐藏单元数量(num_hiddens)和设备(device)作为输入。

        在函数内部,使用torch.zeros生成一个形状为(batch_size, num_hiddens)的全零张量,表示初始化的隐藏状态。由于循环神经网络的隐藏状态是一个元组,所以将这个张量放入元组中并返回。

        RNN这个函数接受输入序列(inputs)、隐藏状态(state)和模型参数(params)作为输入。

        在函数内部,首先从参数列表中解包出各个参数,包括输入到隐藏层的权重W_xh、隐藏层到隐藏层的权重W_hh、隐藏层的偏置b_h、隐藏层到输出层的权重W_hq和输出层的偏置b_q。同时,从隐藏状态中解包出隐藏层的输出H

        然后,定义一个空列表outputs,用于存储每个时间步的输出结果。

        接下来,使用一个循环遍历输入序列中的每个时间步。对于每个时间步的输入X,通过以下步骤计算隐藏层的输出和输出层的输出:

  • 计算隐藏层的输出:使用torch.mm函数计算输入到隐藏层的线性变换,然后将其与隐藏层的权重W_hh相乘的结果进行相加,并加上隐藏层的偏置b_h,最后通过torch.tanh函数进行激活。这一步骤更新了隐藏状态。
  • 计算输出层的输出:将隐藏层的输出与输出层的权重W_hq相乘,并加上输出层的偏置b_q

        将输出结果Y添加到outputs列表中。

        最后,使用torch.cat函数将所有输出结果在维度0上进行拼接,并将更新后的隐藏状态返回。

        7.定义从零开始实现的 RNN 模型类 RNNModelScratch

class RNNModelScratch:
    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)

           

        这个类包含了模型的初始化、前向传播和初始化隐藏状态的方法。

        在__init__方法中,接受词汇表大小(vocab_size)、隐藏单元数量(num_hiddens)、设备(device)、获取参数的函数(get_params)、初始化隐藏状态的函数(init_state)和循环神经网络的前向传播函数(forward_fn)作为输入。在初始化过程中,调用get_params函数获取模型的参数,并将其存储在params属性中。

  __call__方法用于执行模型的前向传播。它接受输入序列X和隐藏状态state

为输入序列X和隐藏状态state作为输入。

        在方法内部,首先使用F.one_hot函数将输入序列X进行独热编码,将其转换为形状为(序列长度,词汇表大小)的浮点张量。这是为了将输入转换为适合模型计算的格式。

        然后,调用forward_fn函数执行循环神经网络的前向传播。传入独热编码后的输入序列X、隐藏状态state和模型的参数paramsforward_fn函数在前面的代码中定义为rnn函数。

        最后,将前向传播的结果返回。begin_state方法用于初始化隐藏状态。它接受批量大小(batch_size)和设备(device)作为输入。

        在方法内部,调用init_state函数初始化隐藏状态,并将其返回。

        这样,RNNModelScratch类封装了从零开始实现的循环神经网络模型,并提供了初始化、前向传播和初始化隐藏状态的方法,方便使用和调用。

        8.创建 RNN 模型实例 net

num_hiddens = 512
device = torch.device('cuda')
net = RNNModelScratch(len(vocab), num_hiddens, device, get_params,
                      init_rnn_state, rnn)

         

        9.定义生成预测结果的函数 predict_ch8

def predict_ch8(prefix, num_preds, net, vocab, device):
    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)
        outputs.append(int(y.argmax(dim=1).reshape(1)))
    return ''.join([vocab.idx_to_token[i] for i in outputs])

      predict_ch8函数用于生成给定前缀的新字符。它接受前缀(prefix)、要生成的字符数量(num_preds)、模型(net)、词汇表(vocab)和设备(device)作为输入。首先,它使用net.begin_state方法生成隐藏状态。然后,它使用给定的前缀进行预测,将最近预测的值作为输入,并将预测结果添加到输出列表中。最后,它将输出列表中的索引转换为对应的字符,并将它们连接起来形成一个字符串,作为最终的预测结果。            

        10.定义梯度裁剪函数 grad_clipping

def grad_clipping(net, theta):
    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

         grad_clipping函数用于裁剪梯度。它接受模型(net)和裁剪阈值(theta)作为输入。如果模型是nn.Module的实例,它会获取模型的参数;否则,它会获取模型的params属性中的参数。然后,它计算所有参数梯度的范数,并将其与阈值进行比较。如果范数大于阈值,则将所有参数的梯度进行缩放,使范数不超过阈值。

        11.定义训练一个迭代周期的函数 train_epoch_ch8

def train_epoch_ch8(net, train_iter, loss, updater, device, use_random_iter):
    """训练模型一个迭代周期(定义见第8章)。"""
    state = None
    metric = 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()
            grad_clipping(net, 1)  # 梯度剪裁
            updater.step()
        else:
            l.backward()
            grad_clipping(net, 1)
            updater(batch_size=1)
        metric.add(l * y.numel(), y.numel())
    return math.exp(metric[0] / metric[1]), metric[1]

         train_epoch_ch8函数用于训练模型的一个迭代周期。它接受模型(net)、训练数据迭代器(train_iter)、损失函数(loss)、更新器(updater)、设备(device)和是否使用随机迭代器(use_random_iter)作为输入。在每个迭代步骤中,它首先初始化隐藏状态。然后,将输入数据和标签转移到设备上。接下来,它通过调用模型进行前向传播,得到预测结果和更新后的隐藏状态。然后,计算损失并进行反向传播。如果更新器是torch.optim.Optimizer的实例,它将执行优化器的相关操作;否则,它将调用自定义的更新器函数。最后,它计算并返回困惑度(perplexity)和标记数量。

        12.定义训练模型的函数 train_ch8

def train_ch8(net, train_iter, vocab, lr, num_epochs, device,
              use_random_iter=False):
    """训练模型(定义见第8章)。"""
    loss = nn.CrossEntropyLoss()
    plt.xlabel('epoch')
    plt.ylabel('perplexity')
    plt.xlim(10, num_epochs)
    if isinstance(net, nn.Module):
        updater = torch.optim.SGD(net.parameters(), lr)
    else:
        updater = lambda batch_size: 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'))
            plt.plot(epoch + 1, ppl, 'bo')
    # print(f'困惑度 {ppl:.1f}, {speed:.1f} 标记/秒 {str(device)}')
    print(predict('time traveller'))
    print(predict('traveller'))

        train_ch8函数用于训练模型。它接受模型(net)、训练数据迭代器(train_iter)、词汇表(vocab)、学习率(lr)、迭代周期数(num_epochs)、设备(device)和是否使用随机迭代器(use_random_iter)作为输入。在函数内部,它定义了损失函数(交叉熵损失)和更新器(使用随机梯度下降)。然后,它使用循环迭代指定的周期数,在每个周期中调用train_epoch_ch8函数进行模型训练,并记录困惑度和速度。如果当前周期是10的倍数,它会打印生成给定前缀的预测结果,并将困惑度绘制到图表中。最后,它打印最终的困惑度、速度和预测结果。

13.设置训练的迭代次数和学习率,并调用 train_ch8 函数开始训练:

num_epochs, lr = 500, 1
train_ch8(net, train_iter, vocab, lr, num_epochs, device, use_random_iter=True)

最后一部分代码调用了train_ch8函数来训练模型。它传入了模型(net)、训练数据迭代器(train_iter)、词汇表(vocab)、学习率(lr)、迭代周期数(num_epochs)、设备(device)和是否使用随机迭代器(use_random_iter=True)。这将开始模型的训练过程,并输出每个周期的困惑度、速度和预测结果。

结果:

        1.使用顺序提取:

        

困惑度 1.0, 8960.0 标记/秒 cuda
time traveller for so it will be convenient to speak of him was 
traveller with a slight accession of cheerfulness really th

        困惑度为1,说明此时效果已经达到最好,但受模型本身的限制,无法达到更高的水平

        2.使用随机提取:

困惑度 1.4, 8960.0 标记/秒 cuda
time traveller smiled are you sure we can move freely in space r
traveller came back and filby s anecdote collapsedthe thing

        随机提取效果稍微要差一些,但随机性更高,不容易过拟合。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值