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

devtools/2024/10/18 12:26:06/

本节我们通过使用MXnet,来从零开始的实现一个含有隐藏状态的循环神经网络。

前序工作

  1. 数据集预处理
  2. 进行采样

实现循环神经网络

完成前序工作后,即可开始实现循环神经网络。本文首先构建一个具有隐状态的循环神经网络。其结构如图所示:

接下来,我们一边讲解循环神经网络的结构,一边构建循环神经网络。

首先需要引入使用到的库,并读取数据集,设置批量大小为32,时间步为35,通过前文(前序工作中的第二项)的加载数据集的函数对数据集进行读取,并完成采样:

%matplotlib inline
import math
from mxnet import autograd, gluon, np, npx
from d2l import mxnet as d2lnpx.set_np()batch_size, num_steps = 32, 35
train_iter, vocab = d2l.load_data_time_machine(batch_size, num_steps)

独热编码

将数字索引直接进行训练会使模型训练十分困难,因此我们使用独热编码(one-hot encoding)将训练集的数据进行转换——假设词元表中不同的词元共有N个,则生成一个长度为N的数组,将数组的对应位置设为1,其他位置均为零,这样做可以更好的展现数据的特征。

每次采样的数据形状是(批量大小,时间步数),通过独热编码,我们希望我们构建的训练数据的形状为(时间步数,批量大小,词表大小),即num_steps个,批量大小x词表大小的二维数组

所以我们需要先对输入X进行转置操作,随后进行独热编码,具体操作如下:

npx.one_hot(X.T, len(vocab))

初始化模型参数

我们直接使用如下的get_params函数来初始化模型参数,其中vocab_size表示词表大小,num_hiddens为超参数,表示隐藏层的大小,device为规定使用cpu运算还是gpu运算。

注:经过作者检验,此处RNN的训练是否使用GPU运算不太重要,都很快!

def get_params(vocab_size, num_hiddens, device):num_inputs = num_outputs = vocab_sizedef normal(shape):return np.random.normal(scale=0.01, size=shape, ctx=device)# 隐藏层参数W_xh = normal((num_inputs, num_hiddens))W_hh = normal((num_hiddens, num_hiddens))b_h = np.zeros(num_hiddens, ctx=device)# 输出层参数W_hq = normal((num_hiddens, num_outputs))b_q = np.zeros(num_outputs, ctx=device)# 附加梯度params = [W_xh, W_hh, b_h, W_hq, b_q]for param in params:param.attach_grad()return params

 内部函数normal对所有参数进行随机的初始化,在隐蔽层参数中,W_{xh},W_{hh},b_{hh}分别为隐藏层的权重和偏差,W_{hq},b_{q}表示输出层的权重和偏差,将这些参数全部附上梯度。

循环神经网络模型

在定义循环神经网络模型之前,我们还需要定义一个函数来在初始化时返回隐状态。这里使用语法使得返回的隐状态是一个元组,隐状态的大小为(批量大小,隐藏单元个数)。

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

现在,我们来开始定义循环神经网络。

我们可以将循环神经网络看成是一个类似于单隐层多层感知机的结构,隐藏状态类似于多层感知机的隐层。先将输入X进行处理后形成一个新的隐藏状态,然后将输入的旧的隐藏状态进行处理产生另一个新的隐藏状态,将两个新的隐藏状态相加,得到当前时间步的隐藏状态。之后通过矩阵乘法处理隐藏状态进行输出。最后,将输出与状态进行连结。

def rnn(inputs, state, params):# inputs的形状:(时间步数量,批量大小,词表大小)W_xh, W_hh, b_h, W_hq, b_q = paramsH, = stateoutputs = []# X的形状:(批量大小,词表大小)for X in inputs:H = np.tanh(np.dot(X, W_xh) + np.dot(H, W_hh) + b_h)Y = np.dot(H, W_hq) + b_qoutputs.append(Y)return np.concatenate(outputs, axis=0), (H,)

注:在对这个代码块进行学习时,我曾产生一个疑惑,通过concatenate来连接隐状态和输出,岂不是隐状态的大小越来越大了吗?但是事实上我发现,H在for-each循环的第一步进行了一下更新,使得H相当于重新进行了初始化,这样做,每个时间步新输出的H将与输入的H具有同样大小。

那么接下来,我们创建一个类对上述函数进行包装。包装完成后,即可来定义一个该类的神经网络net,后面将利用net来完成前向计算。

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_hiddensself.params = get_params(vocab_size, num_hiddens, device)self.init_state, self.forward_fn = init_state, forward_fndef __call__(self, X, state):X = npx.one_hot(X.T, self.vocab_size)return self.forward_fn(X, state, self.params)def begin_state(self, batch_size, ctx):return self.init_state(batch_size, self.num_hiddens, ctx)num_hiddens = 512
net = RNNModelScratch(len(vocab), num_hiddens, d2l.try_gpu(), get_params,init_rnn_state, rnn)

预测

使用predict_ch8这一函数来生成prefix之后的新字符,prefix为用户输入的一个具有若干连续字符的字符串,通过这一阶段来对隐状态H进行一定程度的更新,因此,这一状态被称为预热期(warm-up)

def predict_ch8(prefix, num_preds, net, vocab, device):  #@save"""在prefix后面生成新字符"""state = net.begin_state(batch_size=1, ctx=device)outputs = [vocab[prefix[0]]]get_input = lambda: np.array([outputs[-1]], ctx=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(axis=1).reshape(1)))return ''.join([vocab.idx_to_token[i] for i in outputs])

梯度裁剪

使用梯度裁剪主要是为了缓解梯度爆炸或梯度消失,关于梯度裁剪的具体数学原理,这里暂时不做讨论,但使用梯度裁剪在RNN中是必不可少的。在更新模型参数之前裁剪梯度,即使训练过程中某个点上发生了梯度爆炸,也能保证模型不会发散。

def grad_clipping(net, theta):  #@saveif isinstance(net, gluon.Block):params = [p.data() for p in net.collect_params().values()]else:params = net.paramsnorm = math.sqrt(sum((p.grad ** 2).sum() for p in params))if norm > theta:for param in params:param.grad[:] *= theta / norm

训练

经过预热后,我们现在可以开始训练循环神经网络模型了。在代码之前,我们首先对使用到的各种信息进行说明,便于大家进行了解。

参数说明

  • train_iter:训练集的迭代器,返回每个训练样本。
  • vocab:词表,返回用到的词及其对应的编号id。
  • lr:学习率,一个超参数。
  • num_epochs:学习轮数,一个超参数。
  • device:使用的计算设备。

方法说明

  • SoftmaxCrossEntropyLoss:gluon自带的交叉熵损失函数。
  • Animator:使训练结果更加可视化的方法。
def train_ch8(net, train_iter, vocab, lr, num_epochs, device,  #@saveuse_random_iter=False):loss = gluon.loss.SoftmaxCrossEntropyLoss()animator = d2l.Animator(xlabel='epoch', ylabel='perplexity',legend=['train'], xlim=[10, num_epochs])# 初始化if isinstance(net, gluon.Block):net.initialize(ctx=device, force_reinit=True,init=init.Normal(0.01))trainer = gluon.Trainer(net.collect_params(),'sgd', {'learning_rate': lr})updater = lambda batch_size: trainer.step(batch_size)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])print(f'困惑度 {ppl:.1f}, {speed:.1f} 词元/秒 {str(device)}')print(predict('time traveller'))print(predict('traveller'))

其中的train_epoch_ch8()函数为每一轮的训练过程。先进行前向计算得到预测值,之后通过反向计算来更新权重、偏差,这些参数值。

def train_epoch_ch8(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:# 在第一次迭代或使用随机抽样时初始化statestate = net.begin_state(batch_size=X.shape[0], ctx=device)else:for s in state:s.detach()y = Y.T.reshape(-1)X, y = X.as_in_ctx(device), y.as_in_ctx(device)with autograd.record():y_hat, state = net(X, state)l = loss(y_hat, y).mean()l.backward()grad_clipping(net, 1)updater(batch_size=1)  # 因为已经调用了mean函数metric.add(l * d2l.size(y), d2l.size(y))return math.exp(metric[0] / metric[1]), metric[1] / timer.stop()

通过以下示例来观察训练的结果:

num_epochs, lr = 500, 1
train_ch8(net, train_iter, vocab, lr, num_epochs, d2l.try_gpu())
困惑度 1.0, 23960.0 词元/秒 gpu(0)
time traveller for so it will be convenient to speak of himwas e travelleryou can show black is white by argument said filby

简洁实现循环神经网络

这里,我们直接使用mxnet内的RNN类,用这些高级api完成RNN的计算。如果对RNN的原理不感兴趣,只是需要使用RNN的话,可以直接阅读和使用这一部分的代码。

num_hiddens = 256
rnn_layer = rnn.RNN(num_hiddens)
rnn_layer.initialize()
state = rnn_layer.begin_state(batch_size=batch_size)
class RNNModel(nn.Block):def __init__(self, rnn_layer, vocab_size, **kwargs):super(RNNModel, self).__init__(**kwargs)self.rnn = rnn_layerself.vocab_size = vocab_sizeself.dense = nn.Dense(vocab_size)def forward(self, inputs, state):X = npx.one_hot(inputs.T, self.vocab_size)Y, state = self.rnn(X, state)output = self.dense(Y.reshape(-1, Y.shape[-1]))return output, statedef begin_state(self, *args, **kwargs):return self.rnn.begin_state(*args, **kwargs)device = d2l.try_gpu()
net = RNNModel(rnn_layer, len(vocab))
net.initialize(force_reinit=True, ctx=device)
num_epochs, lr = 500, 1
d2l.train_ch8(net, train_iter, vocab, lr, num_epochs, device)

训练结果如下:

perplexity 1.2, 144941.9 tokens/sec on gpu(0)
time travellerit s against reason said filby of course a solid b
travelleryou can show black is whine be move the endled the


http://www.ppmy.cn/devtools/93946.html

相关文章

C语言指针详解-包过系列(二)目录版

C语言指针详解-包过系列(二)目录版 1、数组名的深入理解1.1、数组名的本质1.2、数组名本质的两个例外1.2.1、sizeof(数组名)1.2.2、&数组名 2、使用指针访问数组3、一维数组传参本质4、二级指针4.1、二级指针介绍4.2、二级指针…

亿达科创亮相智造数字科技大会

8月8日,IMC2024第七届智造数字科技大会在京启幕。大会以“乘‘数’而上”为题,邀请300智能制造行业数字化转型技术大咖、领军者及实践者共聚一堂,解读智造行业转型进程。亿达科创受邀参会,分享企业前沿数字技术、解决方案与创新实…

零知识证明中PLONKish和AIR的区别

这里写自定义目录标题 介绍AIRPLONKish 介绍 首先我们讲一下什么是算数化,为什么零知识证明当中经常提到算术化。算术化是指将一个计算问题(通常是我想要证明的问题,非代数过程)转化为一组代数方程。使得这些问题可以用多项式计算…

智慧图书馆:构建高效视频智能管理方案,提升图书馆个性化服务

一、背景分析 随着信息技术的飞速发展,智慧图书馆作为现代公共文化服务的重要载体,正逐步从传统的纸质阅读空间向数字化、智能化方向转型。其中,视频智能管理方案作为智慧图书馆安全管理体系的重要组成部分,不仅能够有效提升图书…

记2024-08原生微信小程序开发

继2024.08 最近需要开发一个微信小程序的一个功能模块,但是之前在学的时候都是好几年前的东东了,然后重新快速过了一遍b站大学的教程,这篇文章就是基于教程进行的一些总结,和自己开发过程当中使用到的一些点和一些技巧什么的吧。 …

Kotlin 值类(Value Class)

在 Java 中,像 Integer、Double 等都是 包装类,都需要创建对象 装箱 数值。 很显然 创建对象是 消耗额外内存的,而 对于优化这种问题,Kotlin 引入了 value class,尽量避免装箱和脱箱。 一、声明语法 JvmInline value…

私域高转化的秘诀

精准定位用户需求:深入了解目标用户的痛点和需求,提供与之匹配的产品或服务。 建立信任关系:通过内容营销、互动交流等方式,与用户建立良好的沟通和信任关系。 优质内容输出:持续产出有价值、有吸引力的内容&#xf…

C语言:字符串函数strcpy

该函数用于字符串的拷贝。 使用方法如下&#xff1a; #include<stdio.h> #include<string.h>int main() {char str[10];char* str1 "abcd";//strcpy(str, str1);//把str1复制到str&#xff0c;但此函数不安全所以用strcpy_sstrcpy_s(str, 10, str1);/…