当前位置: 首页 > news >正文

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

本节根据8.4节中的描述,从零开始基于循环神经网络实现字符级语言模型。这样的模型将在时光机器数据集上训练。和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)

8.5.1 独热编码

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

简而言之,独热编码时将每个索引映射为相互不同的单位向量,假设词表中不同词元的数量为N,词元索引的范围为0~N-1 ,如果词元的索引时整数l,那么我们将创建一个长度为N的全0向量,并将第i个元素设置为1,此变量时原始词元的一个独热向量,索引为0和2的独热向量如下所示。

  1. .one_hot(torch.tensor([0,2])), len(vocab)
  2. 我们每次抽样的小批量数据性状个时二维张量,one_hot 函数将这样一个小批量数据转换成三维张量,张量的最后一个维度等于词表大小len(vocab)
  3. 我们经常转换输入的维度,以便获得形状为的输出,这将使我们能够更方便的通过最外层的维度,一步步的更新小批量数据的隐状态。
  4. X = torch.arange(10).reshape(2, 5)
  5. F.one_hot(X.T, 28).shape
  6. torch.Size([5,2,28])

8.5.2 初始化模型参数

我们初始化循环神经网络模型的参数,隐单元数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

8.5.3 循环神经网络模型

为了定义循环网络模型,首先需要一个init_rnn_state 函数在初始化时返回隐状态,这个函数的返回值是一个张量,张量完全0填充,形状为 批量大小,隐单元数

在后面的章节中我们将会遇到隐状态包含多个变量的情况,使用元组可以更容易处理。

def init_rnn_state(batch_size, num_hiddens, device)

return (torch.zeros(batch_szie, num_hiddens), device=device),

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

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)

return torch.cat(outputs, dim = 0), (H,)

定义了所需的函数之后,我们创建一个类来包装这些函数,并存储从零开始实现的循环神经网络模型的参数。

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)

我们检查输出具有正确形状状态的维数是否保持不变。

num_hiddens = 512

net = RNNModelSratch(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)

  1. shape, len(new_state), new_state[0].shape

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

我们可以看到输出形状状态保持不变

8.5.4 预测

我们首先定义预测函数生成prefix之后新字符prefix一个用户提供包含多个字符字符串,在循环遍历prefix初始字符我们不断状态传递到下一个时间但是不生成任何输出称为预热因为在此期间模型自动更新但是不会进行预测预热结束状态通常初始值更适合预测从而预测字符输出它们

def predict_ch8(prefix, num_preds, net, vocab, device):

#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_to_token[i] for i in outputs])

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

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

8.5.5 梯度截断

对于长度T序列我们在迭代计算T时间步梯度将会反向传播过程产生长度O矩阵乘法4.8所描述T较大时候可能导致数值不稳定可能导致梯度爆炸或者梯度消失循环神经网络模型往往需要额外方式支持稳定训练

解决优化问题我们对模型参数采用更新步骤假定向量形式x或者批量数据梯度g方向使用 n > 0作为学习再一次迭代我们x更新x - ng 如果我们进一步假设目标函数f表现良好函数fL利普希茨连续对于任意xy都有

|f(x) - f(y) <= L|x-y|

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

|f(x) - f(x-ng)| <= ln|g|

意味着我们不会观测超过ln|g|变化坏事也是好事限制了去的进展速度好的方面限制事情变糟糕程度我们朝着错误方向前进

梯度也有可能很大从而优化算法可能无法收敛可以通过降低学习n解决这个问题但是如果我们很少得到梯度这种情况下这种做法似乎毫无道理一个流行代替方案通过梯度g投影给定半径截断梯度g

g = min(1, Sigma/|g|) g

我们知道梯度范围不会超过sigma更新后梯度方向完全g原始方向一致还有一个拥有附带作用限制任何给定小批量数据参数向量影响赋予模型一定程度稳定性梯度截断提供了一个快速修复梯度爆炸方法并不能完全解决问题,是众多有效技术之一

我们定义一个函数截断模型梯度模型开始实现模型高级API构建模型计算所有模型参数梯度范数

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

8.5.6 训练

训练模型之前定义一个函数一轮训练模型我们训练3.6训练模型方式有所不同

1)序列数据不同抽样导致状态初始化差异

2)我们在更新模型参数之前截断梯度操作目的训练过程某个点上发生梯度爆炸模型不会发散

3)我们困惑评估模型8.4.4所描述 这样度量确保不同长度序列具有可比性

具体来说使用顺序分区只在每轮起始位置初始化状态下一个小批量数据i序列样本当前i子序列样本相邻因此小批量数据最后一个样本状态将用于初始化下一个小批量数据第一个样本状态这样存储状态中的序列历史信息可以一轮经过相邻子序列任何一点状态计算都依赖统一论前面所有的小批量数据使得梯度计算变得复杂位了减少计算在处理任务一个小批量数据之前梯度分离使得隐藏状态梯度计算总是限制在一个小批量数据时间

使用随机抽样因为每个样本都是一个随机位置抽样所以需要每轮重新初始化状态3.6train_epoch_ch3函数相同updater 更新模型参数常用函数可以开始实现d2l.sgd函数可以深度学习内置优化函数

def train_epoch_ch8(net, train_iter, loss, updater, device, use_random_iter):

#训来呢网络一轮

state, timer = None, d2l.TIme*(

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是一个张量

else:

state对于nn.LSTM或者对于我们从零开始实现模型是一个由于张量组成元祖

for s in state:

s.detch_()

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, l)

updater.step()

else:

l.backward()

grad_clipping(net, l)

#因为已经调用mean函数

updater(batch_size=1)

metric.add(l * y.numel(), y.numel())

return math.exp(metric[0] / metric[l], metric[l]/timer_.stop())

循环神经网络模型训练函数支持从零开始实现可以使用高级API实现

def train_ch8(net, train_iter, vocab, lr, num_epochs, device, use_random_iter = False):

训练模型

loss = nn.CrossEntropyLoss()

animator = d2l.Animator(xlabel = 'epoch', ylabel='preplexity', 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:

animator.add(epoch + 1, [ppl])

我们训练神经网络模型因为我们数据集使用1万个词元所以模型需要更多轮数更好收敛

num_epochs, lr = 500

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)

从零开始实现上述循环神经网络模型虽然指导意义但是不方便8.6节中我们学习如何改进循环神经网络模型如何使其更容易实现运行速度更快

小结

我们可以训练一个基于循环神经网络字符级语言模型根据用户提供文本前缀生成后续文本

一个简单循环神经网络语言模型包括输入编码循环神经网络模型输出生成

循环神经网络模型训练以前需要初始化状态不过随机抽样顺序分区使用初始化方法不同

当使用顺序分区我们需要分离梯度减少计算量

在进行任何预测之前模型通过预热自行更新

梯度截断可以防止梯度爆炸但是不能应对梯度消失

http://www.dtcms.com/a/362310.html

相关文章:

  • 二维元胞自动机:从生命游戏到自复制系统的计算宇宙
  • AI 安全与伦理:当大模型拥有 “决策能力”,我们该如何建立技术边界与监管框架?
  • Spring Cloud ------ Gateway
  • h5实现内嵌微信小程序支付宝 --截图保存海报分享功能
  • vmware中linux虚拟机提示磁盘空间不足
  • JavaScript 异步编程:Callback、Promise、async/await
  • 知识表示与处理1
  • 【光照】Unity中的[光照模型]概念辨析
  • 精确率、召回率、漏检率、误判率
  • 基于单片机倒车雷达/超声波测距设计
  • 《零基础入门AI:YOLOv3、YOLOv4详解》
  • React中纯 localStorage 与 Context + useReducer + localStorage对比
  • 【笔记】大模型训练(一)单卡训练的分析与优化策略
  • 微信小程序开发-day1
  • 一次诡异的报错排查:为什么时间戳变成了 ١٧٥٦٦٣٢٧٨
  • 9.1日IO作业
  • 大模型RAG项目实战:文本向量模型>Embedding模型、Reranker模型以及ColBERT模型
  • nCode 后处理常见问题汇总
  • 生成知识图谱与技能树的工具指南:PlantUML、Mermaid 和 D3.js
  • 过拟合 正则化(L1,L2,Dropout)
  • linux内核 - 文件系统相关的几个概念介绍
  • Ceres学习笔记
  • 从理论到RTL,实战实现高可靠ECC校验(附完整开源代码/脚本)(3) RTL实现实战
  • 智慧班牌系统基于Java+Vue技术栈构建,实现教育信息化综合管理。
  • ES6手录01-let与const
  • 2024 年 AI 技术全景图:大模型轻量化、多模态融合如何重塑产业边界?
  • c#:抽象类中的方法
  • Windows 使用 Compass 访问MongoDb
  • 笔记:现代操作系统:原理与实现(1)
  • 利用本地电脑上的MobaXterm连接虚拟机上的Ubuntu