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

1.从零开始写 RNN 循环神经网络

1.1 运行环境

  • pytorch1.9.1 GPU版
  • pycharm 2021
  • 李沐动手学深度学习 d2l 包

注:参考李沐书籍,这里只是做代码注释

1.2 代码及注释

# -*- coding: utf-8 -*-
# @Project: zc
# @Author: ZhangChu
# @File name: RNN-ZC
# @Create time: 2021/10/29 5:56


# 1.导入数据库
import math

import matplotlib.pyplot as plt
import torch
from d2l import torch as d2l
from torch.nn import functional as F
import torch.nn
from torch import nn

# 2. 将文本转换成词向量

batch_size = 32  # 批量大小
num_steps = 35  # 每次看多长的序列,时间维度 T

# vocab 作用:可以将一个整数的 index 转换成相对应的词
train_iter, vocab = d2l.load_data_time_machine(batch_size, num_steps)

X = torch.arange(10).reshape((2, 5))


# 注:F.one_hot(张量, 类别数 )
# 小批量数据形状是:(批量大小,时间步数);
# 为了方便后续的迭代计算,将时间放在前面,这样就可以以时间 X_t 为序号来处理每个样本
# 我们需要将此转置为:(时间步数, 批量大小,每个样本的特征长度)


# 3. 初始化循环神经网络模型的参数
# 功能:
# 		初始化参数:返回一些可以学习的参数
# 		RNN模型在训练时候,一般隐变量 num_hiddens 是可调超参数,
# 参数:input:
# 		vocab_size :词汇表的大小,输入和输出都是同一个词汇表,故相同
# 		num_hiddens : 隐单元的个数(超参数)
# 		device : 尽量选择网络模型在GPU
#
# 参数:output:
#		params = [W_xh,W_hh,b_h,W_hq,b_q]

def get_params(vocab_size, num_hiddens, device):
	num_inputs = num_outputs = vocab_size  # 词向量的大小等于输入,输出

	def normal(shape):  # 从一个均值为0*0.01,方差为1*0.01的正太分布中随机抽取给定大小的张量值
		return torch.randn(size=shape, device=device) * 0.01

	# 公式1: H_t = φ(X_t * W_xh + H_t-1 * W_hh + b_h )  隐藏层
	# 公式2; O_t = H_t * W_hq + b_q  输出层,注:无激活函数
	# 注: 如果把 W_hh 这行代码取消掉,就相当于具有一个单隐藏层的 MLP
	# RNN 与 MLP 的区别就仅仅加了一个 W_hh 这一层
	W_xh = normal((num_inputs, num_hiddens))  # 输入 X 映射到隐藏变量 H 的矩阵
	W_hh = normal((num_hiddens, num_hiddens))  # H_t-1 时刻的隐藏变量映射到下一个时刻 H_t
	b_h = torch.zeros(num_hiddens, device=device)  # 每个隐藏元的偏置 b_h 应该跟隐藏数大小一致
	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


# 4. 一个 init_rnn_state 函数在初始化时返回隐藏状态
# 为了定义循环神经⽹络模型,我们⾸先需要⼀个init_rnn_state函数在初始化时返回隐藏状态。函数的返
# 回是⼀个张量,张量全⽤0填充,形状为(批量⼤小, 隐藏单元数)。在后⾯的章节中将会遇到隐藏状态包含多
# 个变量的情况,而使⽤元组可以处理地更容易些。
# 在 t=0 时刻的时候,我们是不知道初始的隐藏单元状态,所以我们需要初始化为张量0

def init_rnn_state(batch_size, num_hiddens, device):
	"""

	:param batch_size: 批量大小
	:param num_hiddens: 隐藏单元数
	:param device: GPU设置
	:return: (值全为0的张量,)	注:一个元祖,RNN只有一个张量初始化,为了后续 LSTM 我们写成元祖更方便
	"""
	return (torch.zeros((batch_size, num_hiddens), device=device),)


# 5. 定义 RNN 的 forward 函数

def rnn(inputs, state, params):
	"""
	注:input 的形状 : (时间步数量,批量大小,词表大小)
	功能: 给定一个小批量,将里面的时间步都计算一遍,得到输出定义了如何在一个时间步内计算隐藏状态和输出
	:param inputs: X_0 到 X_t 所有的输入
	:param state: 提供过来的初始化隐藏状态
	:param params: 可学习的参数
	:return: 1.所有时刻的状态输出,二维矩阵:(批量大小*时间长度,vocab_size)2.H_t+1的隐状态
	"""
	W_xh, W_hh, b_h, W_hq, b_q = params
	H, = state
	output = []
	# x 的形状为:(批量大小,词表大小)
	# 公式1: H_t = φ(X_t * W_xh + H_t-1 * W_hh + b_h )  隐藏层
	# 公式2; O_t = H_t * W_hq + b_q  输出层,注:无激活函数

	for x in inputs:  # 以时间步为序号进行提取,得到 X_t 时刻的(批量大小,词表大小)
		H = torch.tanh(torch.mm(x, W_xh) + torch.mm(H, W_hh) + b_h)
		Y = torch.mm(H, W_hq) + b_q
		output.append(Y)
	return torch.cat(output, dim=0), (H,)


# 6. 定义 RNN 的类,将之前的函数封装到一个类里面

class RNNModelScratch:
	"""从零开始实现的循环神经网络"""
	"""
	:param vocab_size : 词表大小
	:param num_hiddens :隐藏单元数
	:param device: GPU设置
	:param get_params : 获取参数
	:param init_state : 初始化隐藏单元状态
	:param forward_fn : 定义好的RNN函数,forward 函数
	"""

	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 =(批量大小,时间步数(句子的长度))
		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)


# 7. 我们检查输出是否具有正确的形状,例如,是否保证了隐藏状态的维数保持不变。


num_hiddens = 512
# 设置隐藏单元数大小为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)


# 8. 定义预测函数来生成 prefix 之后的新字符
def predict_ch8(prefix, num_preds, net, vocab, device):
	"""
	功能:在 prefix 后面生成新字符

	:param prefix:	给定一段词
	:param num_preds:	预测多少个词, 这里我们只预测一个字符
	:param net:		训练好的模型
	:param vocab:	预测值可以映射到真实的字符
	:param device:	GPU设置
	:return:
	"""
	# state 生成初始化的状态
	state = net.begin_state(batch_size=1, device=d2l.try_gpu())
	outputs = [vocab[prefix[0]]]  # 将prefix的第一个词拿出来,放到 vocab 中,得到相对应的下标
	get_input = lambda: torch.tensor([outputs[-1]], device=device).reshape((1, 1))
	# 将输出(outputs)中最后的一个词放入到输入(inputs)中
	for y in prefix[1:]:  # 此循环是为了初始化
		_, state = net(get_input(), state)  # 初始化状态即可,前面一个返回值忽略掉
		outputs.append(vocab[y])  # 此时 y 是真是的值,不是预测的值

	for _ in range(num_preds):  # 此循环是为了预测,每次就 get_input 输入,得到输出预测值 y 和 state状态值
		y, state = net(get_input(), state)  # y = (1,vocab_size)的向量
		outputs.append(int(y.argmax(dim=1).reshape(1)))
	return ''.join([vocab.idx_to_token[i] for i in outputs])


# 9. 梯度剪裁,防止梯度爆炸的有效方式
def grad_clipping(net, theta):
	"""
	裁剪梯度: g <- min(1, theta/|g|)g
	:param net:		网络模型
	:param theta:	参数
	:return:
	"""
	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))
	# 1. 将单层里面的参数的梯度求平方,求和
	# 2. 将所有的层的梯度平方和来求和后再开根号得到范数
	# 3. 如果梯度大于给定的 theta , 那么我们就将梯度直接改写 param.grad[:] *= theta / norm
	if norm > theta:
		for param in params:
			param.grad[:] *= theta / norm


# 10.训练模型一个迭代周期
def train_epoch_ch8(net, train_iter, loss, updater, device, use_random_iter):
	"""
	训练模型一个迭代周期
	:param net:	训练的模型
	:param train_iter: 训练的迭代
	:param loss: 损失函数
	:param updater: 优化器
	:param device: GPU
	:param use_random_iter: 判断是否是随机采样还是顺序采样
	:return:
	"""
	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()


# 11. 定义训练模型函数
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 = 600, 1
train_ch8(net, train_iter, vocab, lr, num_epochs, d2l.try_gpu(), use_random_iter=True)
plt.figure()
plt.show()

1.3 运行结果

困惑度 1.4, 115179.0 词元/秒 cuda:0
time travellerit s against reason said filbywan a lubu foce time
traveller after the pauserequired for the proper assimilati

在这里插入图片描述

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值