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

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.xdnf.cn/news/19644.html

相关文章:

  • 运动规划实战案例 | 基于行人社交模型的移动机器人动态避障(附ROS C++仿真)
  • 交互体验升级:Three.js在设备孪生体中的实时数据响应方案
  • LintCode第401题-排序矩阵中的从小到大第k个数
  • 大数据-湖仓一体
  • Pomian语言处理器研发笔记(三):使用组合子构建抽象语法树
  • SpringBoot的基础介绍,用法和配置
  • 解锁Git仓库瘦身秘籍,git-sizer真香警告
  • GitHub 宕机自救指南:应急解决方案与替代平台
  • 复刻elementUI的步骤条Steps
  • 机器翻译:python库translatepy的详细使用(集成了多种翻译服务)
  • Redis 核心概念解析:从渐进式遍历、数据库管理到客户端通信协议
  • 自由学习记录(91)
  • C++“类吸血鬼幸存者”游戏制作的要点学习
  • 计算机毕设推荐:基于python的农产品价格数据分析与预测的可视化系统的设计与实现 基于Python农产品管理系统【源码+文档+调试】
  • 前后端联合实现多个文件上传
  • Java全栈开发面试实录:从基础到微服务架构的深度解析
  • Python 基础综合与实践教案:密码验证、循环、分支条件、图形绘制
  • ReconDreamer++
  • Polkadot - ELVES
  • 你的数据是如何被保护的?
  • 解决浏览器的**混合内容安全策略**(Mixed Content Security Policy)带来的无法访问页面
  • 联合体Union
  • Backroom:信息代币化 AI 时代数据冗杂的解决方案
  • 【系统分析师】高分论文:论原型法及其在系统开发中的应用
  • 【Proteus仿真】按键控制系列仿真——LED灯表示按键状态/按键控制LED灯/4*4矩阵键盘控制LED
  • 部署在windows的docker中的dify知识库存储位置
  • NMOS概述
  • python---类.函数名(self) 和 self.函数名()的调用方式
  • 数据结构 二叉树
  • RocketMQ5.0+保姆级单点Docker部署教程