动手学深度学习——循环神经网络的从零开始实现(原理解释+代码详解)

news2024/12/23 5:58:33

文章目录

    • 循环神经网络的从零开始实现
      • 1. 独热编码
      • 2. 初始化模型参数
      • 3. 循环神经网络模型
      • 4. 预测
      • 5. 梯度裁剪
      • 6. 训练

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

从头开始基于循环神经网络实现字符级语言模型。

# 读取数据集
%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)

1. 独热编码

每个词元都有一个对应的索引,表示为特征向量,即每个索引映射为相互不同的单位向量。

词元表不同词元个数为N,词元索引范围为0到N-1。词元的索引为整数,那么将创建一个长度为N的全0向量,并将第i处元素设置为1。则此向量是原始词元的一个独热编码。

假如有2个词元"cat"和"dog"

  • "cat"对应:[1, 0]
  • "dog"对应:[0, 1]

索引为0和2的独热向量

# 索引为0和2的独热向量
F.one_hot(torch.tensor([0, 2]), len(vocab))

在这里插入图片描述
采样的小批量数据形状为二维张量:(批量大小,时间步数),one_hot函数将其转换为三维张量:(时间步数,批量大小,词表大小)

# 采样的小批量数据形状为二维张量:(批量大小,时间步数)
# one_hot函数将其转换为三维张量:(时间步数,批量大小,词表大小)
# 方便我们通过最外层维度,一步一步更新小批量数据的隐状态
X = torch.arange(10).reshape((2, 5))
print(F.one_hot(X.T, 28).shape)
# 显示第一行
F.one_hot(X.T, 28)[0,:,:]

在这里插入图片描述

2. 初始化模型参数

隐藏单元数num_hiddens是一个可调的超参数

训练语言模型时,输入和输出来自相同的词表,具有相同的维度即词表大小

"""
初始化模型参数:
    1、隐藏层参数
    2、输出层参数
    3、附加梯度
"""
# (词表大小,隐藏层数,设备)
def get_params(vocab_size, num_hiddens, device):
    num_inputs = num_outputs = vocab_size
    
    # 定义函数normal(),初始化模型的参数
    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

3. 循环神经网络模型

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

# 定义init_rnn_state函数在初始化时返回隐状态
# 该函数的返回是一个张量,张量全用0填充,形状为(批量大小,隐藏单元数)
def init_rnn_state(batch_size, num_hiddens, device):
    return (torch.zeros((batch_size, num_hiddens), device=device), )

在这里插入图片描述
循环神经网络通过最外层的维度实现循环,以便时间步更新小批量数据的隐状态H

# 循环神经网络通过最外层的维度实现循环,以便时间步更新小批量数据的隐状态H
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:
        # 激活函数tanh,更新隐状态H
        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,)

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

"""
从零开始实现的循环神经网络模型:
1、定义网络模型的参数
2、对词表进行独热编码
3、初始化模型参数并返回隐状态
"""
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())
# 将X移到GPU上,并且返回输出Y和状态
Y, new_state = net(X.to(d2l.try_gpu()), state)
Y.shape, len(new_state), new_state[0].shape

在这里插入图片描述
可以看到输出形状是(时间步数x批量大小,词表大小), 而隐状态形状保持不变,即(批量大小,隐藏单元数)。

4. 预测

定义预测函数

"""
定义预测函数:
1、prefix是用户提供的字符串;
2、循环遍历prefix的开始字符时不输出,不断将隐状态传递给下一个时间步;
3、在此期间模型进行自我更新(隐状态),不进行预测;
4、2和3步骤称为预热期,预热期过后隐状态的值更适合预测,从而预测字符并输出。
"""
# prefix:前缀字符串
def predict_ch8(prefix, num_preds, net, vocab, device):  #@save
    """在prefix后面生成新字符"""
    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):  # 预测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个后续字符

# 测试predict_ch8函数。 我们将前缀指定为time traveller, 并基于这个前缀生成10个后续字符。
# 未训练模型,输出预测结果没有联系
predict_ch8('time traveller ', 10, net, vocab, d2l.try_gpu())

在这里插入图片描述

5. 梯度裁剪

为什么要梯度裁剪:
1、对于长度为T的序列,我们在迭代中计算T个时间步上的梯度,在反向传播过程中产生长度为T的矩阵乘法链;
2、T较大时,会导致数值不稳定,例如梯度消失或者梯度爆炸。

一个流行的替代方案是通过将梯度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:
        # 梯度的范数:对应图里作为分母的"||g||"
        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

6. 训练

在一个迭代周期内训练模型:
1、序列数据的不同采样方法(随机采样和顺序分区)将导致状态初始化的差异;
2、在更新模型参数之前裁剪梯度,这样可以保证训练过程中如果某点发生梯度爆炸,模型也不会发散;
3、用困惑度评价模型,使得不同长度的序列也有了可比性。

  • 顺序分区:只在每个迭代周期的开始位置初始化隐状态。
  • 随机抽样:每个样本都是在一个随机位置抽样的,因此需要在每个迭代周期重新初始化隐状态。
#@save
"""
训练网络一个迭代周期:
1、初始化状态,将数据传到GPU上
2、计算损失,进行梯度裁剪并更新模型参数
"""
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是个张量
                # detach_()将张量从计算图中分离出来,不会影响到原始张量
                state.detach_()
            else:
                # state对于nn.LSTM或对于我们从零开始实现的模型是个张量
                for s in state:
                    s.detach_()
        # 将Y 进行转置并展平成一维向量
        y = Y.T.reshape(-1)
        # 将X,y移动到设备上,并且输入到模型中
        X, y = X.to(device), y.to(device)
        y_hat, state = net(X, state)
        l = loss(y_hat, y.long()).mean()
        # 如果更新器 updater 是 torch.optim.Optimizer 类型,则调用 updater.step() 方法进行参数更新;
        # 否则调用 updater(batch_size=1) 进行参数更新。
        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)
        # y.numel()计算y中元素数量
        metric.add(l * y.numel(), y.numel())
    # 使用指数损失函数计算累积平均困惑度 math.exp(metric[0] / metric[1]) 和训练速度 metric[1] / timer.stop()。
    return math.exp(metric[0] / metric[1]), metric[1] / timer.stop()
            
  • updater.zero_grad(): 这一行代码将模型参数的梯度置零,以便在每次迭代中计算新的梯度。
  • l.backward(): 这一行代码使用反向传播算法计算损失函数对模型参数的梯度。通过计算梯度,我们可以知道如何调整模型参数以最小化损失函数。
  • grad_clipping(net, 1): 这一行代码对模型的梯度进行裁剪,以防止梯度爆炸的问题。梯度爆炸可能会导致训练不稳定,裁剪梯度可以限制梯度的范围。
  • updater.step(): 这一行代码使用优化器(如SGD、Adam等)来更新模型的参数。优化器根据计算得到的梯度和预定义的学习率来更新模型参数,以使模型更好地拟合训练数据。

循环神经网络的训练函数也支持高级API实现

# 循环神经网络的训练函数也支持高级API实现
#@save
def train_ch8(net, train_iter, vocab, lr, num_epochs, device,
              use_random_iter=False):
    """训练模型(定义见第8章)"""
    loss = nn.CrossEntropyLoss()
    # 动画窗口:窗口显示一个图例,图例名称为 "train",x 轴的范围从 10 到 num_epochs
    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)
        # 每10个epoch,对输入字符串进行预测,并将预测结果添加到动画中
        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个词元, 所以模型需要更多的迭代周期来更好地收敛

# 在数据集中只使用了10000个词元, 所以模型需要更多的迭代周期来更好地收敛
num_epochs, lr = 500, 1
train_ch8(net, train_iter, vocab, lr, num_epochs, d2l.try_gpu())

在这里插入图片描述
检查一下随机抽样方法的结果

# 检查一下随机抽样方法的结果
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)

在这里插入图片描述

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

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

相关文章

shopee地区选品:深入了解选品工具——知虾,优化您的销售策略

作为一家社交电商平台,Shopee拥有庞大的用户群体和海量的交易数据,这使得该平台的选品数据对于卖家来说非常宝贵。在这方面,知虾工具是一款强大的分析工具,可以帮助卖家深入了解用户的消费习惯、喜好和需求,从而更准确…

深入解析:开发抖音酒店景区小程序的技术

抖音作为社交媒体平台的佼佼者,其独特的风格和用户基础吸引了无数开发者的目光。在本文中,我们将深入解析开发抖音酒店景区小程序的关键技术,为开发者提供实用指南。 1.抖音风格设计 在开发酒店景区小程序时,首先要注重界面设计…

强化学习各种符号含义解释

:状态 : 动作 : 奖励 : 奖励函数 : 非终结状态 : 全部状态,包括终结状态 : 动作集合 ℛ : 奖励集合 : 转移矩阵 : 离散时间步 : 回合内最终时间步 : 时间t的状态 : 时间t动作 : 时间t的奖励,通常为随机量,且由和决定 : 回报 : n步…

【C++】模板初阶 【 深入浅出理解 模板 】

模板初阶 前言:泛型编程一、函数模板(一)函数模板概念(二)函数模板格式(三)函数模板的原理(四)函数模板的实例化(五)模板参数的匹配原则 三、类模…

Java Fasn 带您谈谈——开源、闭源

✅作者简介:2022年博客新星 第八。热爱国学的Java后端开发者,修心和技术同步精进。 🍎个人主页:Java Fans的博客 🍊个人信条:不迁怒,不贰过。小知识,大智慧。 ✨特色专栏&#xff1a…

【C++】类与对象 III 【 深入浅出理解 类与对象 】

文章内容 前言 :新关键字explicit 的引入一、explicit关键字二、static成员(一)概念(二)特性 三、匿名对象四、友元前言:友元的引入(一)友元的概念友元分为:友元函数 和 …

黑马程序员 学成在线项目 第1章 项目介绍环境搭建v3.1

第1章 项目介绍&环境搭建v3.1 1.项目背景 1.1 在线教育市场环境 以下内容摘自艾瑞:2020年在线教育行业洞察:To B赛道篇_网络服务_艾瑞网 在线教育行业是一个有着极强的广度和深度的行业,从校内到校外;从早幼教到职业培训&…

计算机指令的流水线执行与流水线冒险

目录 计算机指令流水线 流水线冒险 结构冒险 数据冒险 控制冒险 计算机指令流水线 流水线方式的洗衣房可以以并行的方式提高性能 计算机执行指令,同样可以以流水线的方式并行 MIPS 流水化的数据通路 流水线冒险 下一周期不能按时执行下一条指令 结构冒险…

Selenium安装WebDriver(含116/117/118/119)

1、确认浏览器的版本 在浏览器的地址栏,输入chrome://version/,回车后即可查看到对应版本 2、找到对应的chromedriver版本 2.1 114及之前的版本可以通过点击下载chromedriver,根据版本号(只看大版本)下载对应文件 2.2 116版…

3D建模基础教程:编辑样条线【子层级】

了解子层级编辑样条线 在3D建模中,样条线是创建各种形状和曲线的重要工具。而编辑样条线是3D建模过程中不可或缺的一部分。今天,我们将一起学习如何编辑样条线,以及了解其子层级的相关知识。 样条线的子层级介绍 样条线的子层级包括&#xff…

[C国演义] 第二十一章

第二十一章 最长公共子序列不相交的线 最长公共子序列 力扣链接 单个数组的子序列问题 – dp[i] -- 以nums[i] 为结尾的所有子序列中, xxx xxx. 然后状态转移方程根据 最后一个位置的归属问题进行讨论 两个数组的子序列问题 – 以小见大, 分别分析nums1中的一个区间 和 nums…

Linux:firewalled服务常规操作汇总

一、firewalled防火墙工作原理 firewalled的内部结构,可以简单的看做下图,有两个集合,一个集合管理关闭的端口,另一个集合管理放开的端口。 二、常用操作 1、开启和关闭防火墙 临时性配置: systemctl [start | stop …

​分享mfc140u.dll丢失的解决方法,针对原因解决mfc140u.dll丢失的问题

作为电脑小白,如果电脑中出现了mfc140u.dll丢失的问题,肯定会比较的慌乱。但是出现mfc140u.dll丢失的问题,其实也有很简单的办法,所以大家不用慌张,接下来就教大家解决办法,能够有效的解决mfc140u.dll丢失的…

某app c++层3处魔改md5详解

hello everybody,本期是安卓逆向so层魔改md5教学,干货满满,可以细细品味,重点介绍的是so层魔改md5的处理. 常见的魔改md5有: 1:明文加密前处理 2:改初始化魔数 3:改k表中的值 4:改循环左移的次数 本期遇到的是124.且循环左移的次数是动态的,需要前面的加密结果处理生成 目录…

Java项目实战《苍穹外卖》 二、项目搭建

当我痛苦地站在你的面前 你不能说我一无所有 你不能说我两手空空 系列文章目录 苍穹外卖是黑马程序员2023年的Java实战项目,作为业余练手用,需要源码或者课程的可以找我,无偿分享 Java项目实战《苍穹外卖》 一、项目概述Java项目实战《苍穹外…

c# webapi 处理跨源问题

利用cors中间件处理跨源问题。 首先,什么是跨域(跨源)问题: 是指不同站点之间,使用ajax无法相互调用的问题。跨域问题本质是浏览器的一种保护机制,它的初衷是为了保证用户的安全,防止恶意网站窃…

【C语言数据结构】单链表

目录 分析顺序表和链表实现单链表打印链表动态申请一个节点尾插头插尾删头删查找值函数单链表在pos位置之前插入x单链表在pos位置之后插入x删除pos位置单链表删除pos位置之后的值释放内存空间 分析顺序表和链表 如下图可以看出顺序表的优点 1.尾插尾删足够快 2.下标随机访问和修…

鸿蒙开发|开启鸿蒙开发之旅-发工具下载安装、项目创建和预览

文章目录 一、鸿蒙开发使用语言二、下载开发工具三、安装开发工具四、新建项目五、项目启动 一、鸿蒙开发使用语言 鸿蒙OS开发支持多种编程语言,开发者可以根据自身技术背景和项目需求选择合适的语言进行开发。目前鸿蒙OS主要支持以下几种语言: Java&am…

计算机msvcp120.dll丢失?msvcp120.dll丢失5种简单的解决方法分享

你们是否在电脑操作过程中常看到一段类似“msvcp120.dll缺失或损坏”的报错信息?这可能会干扰大家的日常应用程序使用,怎么办呢?别担心,接下来就是一篇详细的步骤来教你如何应对这种情况,让你们的电脑运作如初&#xf…

二叉树的遍历(非递归版)

文章目录 二叉树的前序遍历二叉树的中序遍历二叉树的后序遍历 正文开始前给大家推荐个网站,前些天发现了一个巨牛的 人工智能学习网站, 通俗易懂,风趣幽默,忍不住分享一下给大家。 点击跳转到网站。 二叉树的前序遍历 用递归实…