《动手学深度学习 Pytorch版》 8.5 循环神经网络的从零开始实现

news2024/11/25 2:53:09
%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)  # 仍然使用时间机器数据集

8.5.1 独热编码

采样的小批量数据形状是二维张量:(批量大小,时间步数)。one_hot 函数将这样一个小批量数据转换成形状为(时间步数,批量大小,词表大小)的输出。这将使我们能够更方便地通过最外层的维度, 一步一步地更新小批量数据的隐状态。

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]])
X = torch.arange(10).reshape((2, 5))
F.one_hot(X.T, 28).shape  # 转置一下把时间放前面
torch.Size([5, 2, 28])

8.5.2 初始化模型参数

def get_params(vocab_size, num_hiddens, device):
    num_inputs = num_outputs = vocab_size  # 输入是独热编码所以长度是vocab_size;输出是在vocab里面预测所以长度也是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

8.5.3 循环神经网络

init_rnn_state 函数在初始化时返回隐状态。这个函数返回的是一个全用 0 填充的张量,张量形状为(批量大小,隐藏单元数)。

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

rnn 函数在一个时间步内计算隐状态和输出。这里使用 tanh 函数作为激活函数。如前节所述,当元素在实数上满足均匀分布时,tanh 函数的平均值为0。

def rnn(inputs, state, params):  # inputs的形状:(时间步数量,批量大小,词表大小)
    W_xh, W_hh, b_h, W_hq, b_q = params
    H, = state
    outputs = []
    for X in inputs:  # X的形状:(批量大小,词表大小) 前面转置是为了这里遍历
        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,)  # 沿时间步拼接

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

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]))

8.5.4 预测

首先定义预测函数来生成 prefix (用户提供的多字符的字符串)之后的新字符。

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

def predict_ch8(prefix, num_preds, net, vocab, device):  #@save
    """在prefix后面生成新字符"""
    state = net.begin_state(batch_size=1, device=device)
    outputs = [vocab[prefix[0]]]  # 调用 vocab 类的 __getitem__ 方法
    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)
        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())  # 没有训练过,会生成荒谬的结果
'time traveller gnwvr gnwv'

8.5.5 梯度裁剪

对于长度为 T T T 的序列,我们在迭代中计算 T T T 这个时间步上的梯度,将会在反向传播过程中产生长度为 O ( T ) O(T) O(T) 的矩阵乘法链。当 T T T 较大时,数值不稳定可能导致梯度爆炸或梯度消失。

一个流行的替代方案是通过将梯度 g g g 投影回给定半径 θ \theta θ 的球来裁剪梯度:

g ← min ⁡ ( 1 , θ ∣ ∣ g ∣ ∣ ) g g\gets\min\left(1,\frac{\theta}{||g||}\right)g gmin(1,∣∣g∣∣θ)g

def grad_clipping(net, theta):  #@save
    """裁剪梯度"""
    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

8.5.6 训练

与之前训练模型的方式有三个不同之处:

  • 序列数据的不同采样方法将导致隐状态初始化的差异。

    • 当使用顺序分区时, 我们只在每个迭代周期的开始位置初始化隐状态。

      由于下一个小批量数据中的序列样本 与当前子序列样本相邻,因此当前小批量数据最后一个样本的隐状态将用于初始化下一个小批量数据第一个样本的隐状态。

    • 当使用随机抽样时,因为每个样本都是在一个随机位置抽样的,因此需要为每个迭代周期重新初始化隐状态。

  • 在更新模型参数之前裁剪梯度。即使训练过程中某个点上发生了梯度爆炸,也能保证模型不会发散。

    • 在任何一点隐状态的计算,都依赖于同一迭代周期中前面所有的小批量数据,这使得梯度计算变得复杂。为了降低计算量,在处理任何一个小批量数据之前,我们先分离梯度,使得隐状态的梯度计算总是限制在一个小批量数据的时间步内。
  • 我们用困惑度来评价模型,确保了不同长度的序列具有可比性。

#@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或对于我们从零开始实现的模型是个张量
                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)
            # 因为已经调用了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  # 因为只使用了10000个词元,所以模型需要更多的迭代周期来更好地收敛
train_ch8(net, train_iter, vocab, lr, num_epochs, d2l.try_gpu())  # 先来一波默认的顺序分区
困惑度 1.0, 84837.9 词元/秒 cuda:0
time traveller for so it will be convenient to speak of himwas e
travelleryou can show black is white by argument said filby

在这里插入图片描述

困惑度 1.0,属于是把书背下来了。

net1 = 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.5, 79291.4 词元/秒 cuda:0
time traveller held in his hand was a glitteringmetallic framewo
travellerit would be remarkably convenient for the historia

在这里插入图片描述

练习

(1)尝试说明独热编码等价于为每个对象选择不同的嵌入表示。

不会,略


(2)通过调整超参数(如轮数、隐藏单元数、小批量数据的时间步数、学习率等)来改善困惑度。

a. 困惑度可以降到多少?

b. 用可学习的嵌入表示替换独热编码,是否会带来更好的表现?

c. 如果用H.G.Wells的其他书作为数据集时效果如何, 例如世界大战?

能降,b c太麻烦了,略。

batch_size, num_steps2 = 32, 64
train_iter2, vocab = d2l.load_data_time_machine(batch_size, num_steps2)

num_hiddens2 = 1024
net2 = RNNModelScratch(len(vocab), num_hiddens2, d2l.try_gpu(), get_params,
                      init_rnn_state, rnn)

num_epochs2, lr2 = 1000, 0.5
train_ch8(net2, train_iter2, vocab, lr2, num_epochs2, d2l.try_gpu(),
          use_random_iter=True)  # 使用随机抽样方法,顺序分区已经降无可降了
困惑度 1.2, 58441.2 词元/秒 cuda:0
time traveller for so it will be convenient to speak of himwas e
traveller with a slight accession ofcheerfulness really thi

在这里插入图片描述


(3)修改预测函数,例如使用抽样,而不是选择最有可能的下一个字符。

a. 会发生什么?

b. 调整模型使之偏向更可能的输出,例如,当 α > 1 \alpha >1 α>1,从 q ( x t ∣ x t − 1 , … , x 1 ) ∝ P ( x t ∣ x t − 1 , … , x 1 ) α q(x_t|x_{t-1},\dots,x_1)\propto P(x_t|x_{t-1},\dots,x_1)^\alpha q(xtxt1,,x1)P(xtxt1,,x1)α 中采样。

不会,略。


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

略。

def train_epoch_ch8_4(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()
            # 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] / timer.stop()

def train_ch8_4(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_4(
            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'))
net3 = RNNModelScratch(len(vocab), num_hiddens, d2l.try_gpu(), get_params,
                      init_rnn_state, rnn)
num_epochs, lr = 500, 1
train_ch8_4(net3, train_iter, vocab, lr, num_epochs, d2l.try_gpu())  # 去掉梯度裁剪直接爆炸
困惑度 195745164855533848843693558201405885920387170281738976926429980299285757160792307487577948624519168.0, 92924.4 词元/秒 cuda:0
time travellertttttttttttttttttttttttttttttttttttttttttttttttttt
travellertttttttttttttttttttttttttttttttttttttttttttttttttt

在这里插入图片描述


(5)更改顺序划分,使其不会从计算图中分离隐状态。运行时间会有变化吗?困惑度呢?

略。


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

def rnn_6(inputs, state, params):
    W_xh, W_hh, b_h, W_hq, b_q = params
    H, = state
    outputs = []
    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,)
net4 = RNNModelScratch(len(vocab), num_hiddens, d2l.try_gpu(), get_params,
                      init_rnn_state, rnn_6)
num_epochs, lr = 500, 1
train_ch8_4(net4, train_iter, vocab, lr, num_epochs, d2l.try_gpu())  # 用 Relu 好像不用裁剪也行哇,收敛更快了
困惑度 1.0, 83541.7 词元/秒 cuda:0
time traveller for so it will be convenient to speak of himwas e
traveller with a slight accession ofcheerfulness really thi

在这里插入图片描述

net5 = RNNModelScratch(len(vocab), num_hiddens, d2l.try_gpu(), get_params,
                      init_rnn_state, rnn_6)
num_epochs, lr = 500, 1
train_ch8(net5, train_iter, vocab, lr, num_epochs, d2l.try_gpu())  # 换了 Relu 好像用不用裁剪没差
困惑度 1.0, 88798.8 词元/秒 cuda:0
time traveller with a slight accession ofcheerfulness really thi
traveller with a slight accession ofcheerfulness really thi

在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1090825.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

深度学习验证码项目

项目代码: GitHub - kerlomz/captcha_trainer: [验证码识别-训练] This project is based on CNN/ResNet/DenseNetGRU/LSTMCTC/CrossEntropy to realize verification code identification. This project is only for training the model. GitHub - Python3WebSpi…

锂电池行业新技术,RFID技术赋能生产、溯源

随着新能源汽车用动力电池的快速扩大,对锂电池的发展高安全性、高一致性、高合格率和低制造成本提出了更高的要求。而RFID技术被广泛应用在锂电池行业,为锂电池的生产、管理、溯源等生产管理方面提供了极大地便利,提升了生产效率、产品质量和…

【工具软件】mediamtx——网页、vue3项目中播放 rtsp 视频流(支持265转码)

声明 本文只做 mediamtx 的使用实操,请务必参考下面的博客,,我也参考下面的大佬博客,感谢唯一602的无私分享: 在web页面中直接播放rtsp视频流,重点推荐:mediamtx,不仅仅是rtsp mediamtx 介绍 …

Vue的学习补充

1.Vue路由-404 作用:当路径找不到匹配时,给个提示页面 位置:配在路由最后 语法:path:*(任意路径)-前面不匹配就命中最后这个 2.Vue路由-模式设置 hash路由(默认) 例如&#xff…

ubuntu mmdetection配置

mmdetection配置最重要的是版本匹配,特别是cuda,torch与mmcv-full 本项目以mmdetection v2.28.2为例介绍 1.查看显卡算力 因为gpu的算力需要与Pytorch依赖的CUDA算力匹配,低版本GPU可在相对高的CUDA版本下运行,相反则不行 算力…

【多线程】Thread类的基本用法

文章目录 线程创建线程中断线程等待线程休眠 线程创建 方法一:用一个类 继承Thread 重写run方法 //创建一个类 继承Thread class MyThread extends Thread {//run方法是线程的入口Overridepublic void run() {while (true){System.out.println("hello Thread…

Unity 3D基础——缓动效果

1.在场景中新建两个 Cube 立方体,在 Scene 视图中将两个 Cude的位置错开。 2.新建 C# 脚本 MoveToTarget.cs(写完记得保存) using System.Collections; using System.Collections.Generic; using UnityEngine;public class MoveToTarget : M…

代理和多级代理

文章目录 代理使用场景代理过程实验演示多级代理 代理使用场景 1、拿下远程 web 服务器 2、webshell 链接不稳定,需要使用稳定的木马程序 3、远程服务器无法直接链接攻击者电脑 4、需要借助公网vps转发来自失陷服务器的木马流量 5、借助frp服务端(vps)和客户端(内网…

AI为锚,创新为帆,谱写数实融合发展新篇章

云聚园区,智享未来。9月27日,在苏州工业园区管理委员会、华为云计算技术有限公司的指导下,由SISPARK(苏州国际科技园)、华为(苏州)人工智能创新中心联合主办,东北大学工业智能与系统…

2.用Flask框架创建一个简单的Web程序

怎么安装Flask框架 在终端输入以下命令: pip install flask 验证flask安装: flask --version 编写app.py文件 app文件py如下: #导入flask框架中的两个模块 #Flask允许创建一个Flask应用实例,处理路由、请求和响应等功能 #render…

【交叉编译】tslib库交叉编译

tslib 是一个捕捉触屏事件的工具。qt 库在交叉编译的时候,提供了 -tslib 选项,使用该选项需要提前对 tslib 库进行交叉编译。 目录 1、源码下载 2、安装依赖 3、创建编译脚本 4、开始编译 1、源码下载 tslib 源码下载地址: https://github.com/lib…

oracle 与mysql兼容日期(格式:YYYY年MM月DD日)

日期类型:date 查询sql: select concat(concat(concat(to_char(END_DATE,YYYY),年),concat(to_char(END_DATE,MM),月)),concat(to_char(END_DATE,DD),日)) AS dateInfo from test显示结果:

python jieba 词性标注 中文词性分类 nlp jieba.posseg

参考:https://blog.csdn.net/yellow_python/article/details/83991967 from jieba.posseg import dt dt.word_tag_tab[好看] >>> vflag_en2cn { ‘a’: ‘形容词’, ‘ad’: ‘副形词’, ‘ag’: ‘形语素’, ‘an’: ‘名形词’, ‘b’: ‘区别词’, ‘…

2023年工业大麻行业研究报告

第一章 行业概况 1.1 定义 工业大麻行业是一个多面向且快速发展的领域,涵盖了从种植、加工到分销各个环节。与休闲大麻不同,工业大麻主要用于制造和商业用途。工业大麻的种植重点放在产生纤维、籽和生物质等有价值的产品上,而非产生高含量的…

Rt-Thread 移植3--临界段保护(KF32)

1.什么是临界段 执行下的时候不能被中断的代码段。 系统调度和外部中断都会打断,系统调度本质是产生PendSV中断。NMI FAULST和Hard FAULE除外2.代码实现 contex_gcc.c中添加 rt_base_t __attribute__((noinline)) rt_hw_interrupt_disable(void) {asm volatile(…

阿里云服务器内存型20个实例规格性能特点和适用场景汇总

阿里云服务器ECS内存型规格族属于独享型云服务器,在高负载不会出现计算资源争夺现象,因为每一个vCPU都对应一个Intel Xeon 处理器核心的超线程,内存型实例的云服务器其CPU处理器与内存配比通常为1:8,最高比约为1:20。本文介绍阿里…

Seata入门系列【6】分布式事务之TCC模式简介

1 历史背景 关于TCC的概念,最早是由Pat Helland于2007年发表的一篇名为《Life beyond Distributed Transactions:an Apostate’s Opinion》的论文提出。在该论文中,TCC还是以Tentative-Confirmation-Cancellation命名。正式以Try-Confirm-Cancel作为名称…

【运维笔记】swow源码编译安装

swow的github网址 https://github.com/swow/swow 从github中拉取源码 git pull https://github.com/swow/swow.git 编译安装 github中readme文件讲述了安装方法 这里整理了命令,进入拉取项目的目录后依次执行命令即可 #pwd 确保自己在swow目录中,如…

linux中搭建c语言环境并编译

安装gcc 安装 yum install gcc 检查 gcc --version 编译文件 1.编写test.c vim test.c #include <stdio.h>int main() {printf(" ***** \n");printf(" * o o * \n");printf("* ^ *\n");printf("* - *\n");printf…

VUE树结构实现

实现效果: 数据库表结构如下: 要求:需要有parentId,id。parentId就是父记录的id 表数据要求:一定不要让一条记录的parentid和id相同 前端代码: 注意:el-table标签里面需要加上属性,才可以有下拉箭头的样式 <el-table v-loading="listLoading" :data