RNN
ht 与 ht-1 和 xt 相关(图片有问题)
隐变量状态是一个向量,如何更新向量。
import math
import torch
import d2l.torch
from torch import nn
from torch.nn import functional as F
batch_size,num_steps = 32,35
#train_iter为数据迭代器,vocab是Vocab类实例,包含把词元id转换成对应的词元,以及把词元转换成对应id等功能
train_iter,vocab = d2l.torch.load_data_time_machine(batch_size,num_steps)
#初始化权重参数
def get_params(vocab_size,num_hiddens,device):
input_size = output_size = vocab_size
def normal(shape):#生成均值为0,方差为1的形状为shape的矩阵
return torch.randn(size=shape,device=device)*0.01 #randn()范围是0-1
# 隐藏层参数
W_xh = normal(shape=(input_size,num_hiddens))#和xt
W_hh = normal(shape=(num_hiddens,num_hiddens))#和ht-1
b_h = torch.zeros(num_hiddens,device=device)
# 输出层参数
W_hq = normal(shape=(num_hiddens,output_size))
b_q = torch.zeros(output_size,device=device)
#设置参数梯度
params = [W_xh,W_hh,b_h,W_hq,b_q]
for param in params :
param.requires_grad_(True)
#requires_grad是参数的梯度属性,requires_grad_()函数是设置参数是否需要梯度
return params
def init_rnn_state(batch_size,num_hiddens,device):
#h0、h1....的参数值,刚开始都是0
return (torch.zeros(size=(batch_size,num_hiddens),device=device),)
#因为LSTM返回的隐状态有两个,为了更适配更多网络,因此采用了元组形式返回隐状态变量
#在1个时间步内,计算隐状态和输出
def rnn(inputs,state,params):
# inputs的形状:(时间步数量,批量大小,词表大小)
W_xh,W_hh,b_h,W_hq,b_q = params
H, = state#初始化的隐藏状态
outputs = []
for X in inputs:
# X的形状:(批量大小,词表大小),表示每一个时间步下数据形状是(批量大小,词表大小)
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:#@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
#forward函数是前向调用函数,在之后的步骤会定义forward函数为rnn函数
def __call__(self,X,state):
# X.T表示转置,将X形状从(批量大小,时间步数)转换成(时间步数,批量大小)
X = F.one_hot(X.T,self.vocab_size).type(torch.float32)
#one_hat独热编码:把每个索引映射为相互不同的单位向量
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.torch.try_gpu(),
get_params,init_rnn_state,rnn)
#rnn是RNNM类里的forward_fn参数
begin_state = net.begin_state(X.shape[0],d2l.torch.try_gpu())
#初始化一个状态(0时刻之前的状态)
Y,new_state = net(X.to(d2l.torch.try_gpu()),begin_state)
# net()调用了net类内置函数__call__()函数
"""在prefix后面生成新字符"""
def predict_ch8(prefix,num_preds,net,vocab,device):#@save
#state表示对看过的序列用一个状态量表示,刚开始没有看过任何序列,状态量初始化为0,然后逐渐按顺序看序列,然后更新状态量,使用状态量进行对看过的序列进行表示
state = net.begin_state(batch_size=1,device=device)
outputs = [vocab[prefix[0]]]#开始就是1个词
get_input = lambda:torch.tensor([outputs[-1]],device=device).reshape((1,1))
#把最近预测的词(outputs[-1])转化为矩阵,作为输入(批量大小1,时间步长1)
# 预热期
for i in prefix[1:]:#遍历句中的所有词
_,state = net(get_input(),state)
#把参数信息放进state里,net的前向是run,只有1步
outputs.append(vocab[i])#把真实词传递进去,避免累计误差
# 预测num_preds步
for _ in range(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])
#把编号转成词
"""裁剪梯度"""
def grad_clipping(net,theta):#@save
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
"""训练网络一个迭代周期"""
def train_epoch_ch8(net,train_iter,loss,optim,device,use_random_iter):#@save
state,timer = None,d2l.torch.Timer()
# 训练损失之和,词元数量
accumulator = d2l.torch.Accumulator(2)
for X,Y in train_iter:
X,Y = X.to(device),Y.to(device)
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是个张量
#在顺序划分数据中在任何一点隐状态的计算
#都依赖于同一迭代周期中前面所有的小批量数据, 这使得梯度计算变得复杂。
#为了降低计算量,在处理任何一个小批量数据之前
#我们先分离梯度,使得隐状态的梯度计算总是限制在一个小批量数据的时间步内。
state.detach_()
else:
# state对于nn.LSTM或对于我们从零开始实现的模型是一个tuple
for s in state:
s.detach_()
y = Y.T.reshape(-1)
y_hat,state = net(X,state)
l = loss(y_hat,y.long()).mean()
if isinstance(optim,torch.optim.Optimizer):
#Pytorch框架实现的优化函数optim()
optim.zero_grad()
#计算梯度
l.backward()
#梯度裁剪
grad_clipping(net,theta=1)
#通过学习率和计算出的梯度(经过梯度剪裁过的梯度)更新参数
optim.step()
else:
#手动实现的优化函数optim()
#计算梯度
l.backward()
#梯度裁剪
grad_clipping(net,theta=1)
#通过学习率和计算出的梯度(经过梯度剪裁过的梯度)更新参数
optim(batch_size=1)
# 因为前面l已经调用了mean函数,这里是对所有预测数据与label数据的loss求和
accumulator.add(l*y.numel(),y.numel())
return math.exp(accumulator[0]/accumulator[1]),accumulator[1]/timer.stop()
"""训练模型"""
def train_ch8(net,train_iter,vocab,lr,num_epoches,device,use_random_iter=False):#@save
loss = nn.CrossEntropyLoss()
animator = d2l.torch.Animator(xlabel='epoch',ylabel='perplexity',xlim=[10,num_epoches],legend=['train'])
# 初始化
if isinstance(net,nn.Module):
optim = torch.optim.SGD(net.parameters(),lr)
else:
optim = lambda batch_size:d2l.torch.sgd(net.params,lr,batch_size)
predict = lambda prefix:predict_ch8(prefix,50,net,vocab,device)
# 训练和预测
for epoch in range(num_epoches):
ppl,speed = train_epoch_ch8(net,train_iter,loss,optim,device,use_random_iter)
if (epoch+1)%10 == 0:
print(predict('time travller'))
animator.add(epoch+1,[ppl])
print('ppl = ',ppl,' speed = ',speed)
print(predict('time travller'))
print(predict('time '))
num_epoches,lr = 500,1
train_ch8(net,train_iter,vocab,lr,num_epoches,d2l.torch.try_gpu(),False)
调用pytorch框架
num_hiddens = 256
rnn_layer = nn.RNN(len(vocab),num_hiddens)
state = torch.zeros(size=(1,batch_size,num_hiddens))
X = torch.rand(size=(num_steps,batch_size,len(vocab)))
Y,new_state = rnn_layer(X,state)
"""循环神经网络模型"""
class RNNModel(nn.Module):
def __init__(self,rnn_layer,vocab_size):
super(RNNModel, self).__init__()
self.rnn = rnn_layer
self.vocab_size = vocab_size
self.num_hiddens = rnn_layer.hidden_size
# 如果RNN是双向的,num_directions应该是2,否则应该是1
if not self.rnn.bidirectional:
self.num_directions = 1
self.linear = nn.Linear(self.num_hiddens,self.vocab_size)
else:
self.num_directions = 2
self.linear = nn.Linear(self.num_hiddens*2,self.vocab_size)
def forward(self,inputs,state):
X = F.one_hot(inputs.T.long(),self.vocab_size)
X = X.to(torch.float32)
Y,state = self.rnn(X,state)
# 全连接层输出层首先将Y的形状改为(时间步数*批量大小,隐藏单元数)
# 它的输出形状是(时间步数*批量大小,词表大小)。
outputs = self.linear(Y.reshape((-1,Y.shape[-1])))
return outputs,state
def begin_state(self,device,batch_size=1):
if not isinstance(self.rnn,nn.LSTM):
# nn.GRU,nn.RNN以张量作为隐状态
return torch.zeros(
size=(self.num_directions*self.rnn.num_layers,batch_size,self.num_hiddens),device=device)
else:
# nn.LSTM以元组作为隐状态,LSTM有两个隐状态
return(torch.zeros(
size=(self.num_directions*self.rnn.num_layers,batch_size,self.num_hiddens),device=device),
torch.zeros(
size=(self.num_directions*self.rnn.num_layers,batch_size,self.num_hiddens),device=device))
device = d2l.torch.try_gpu()
net = RNNModel(rnn_layer,vocab_size=len(vocab))
net = net.to(device)
d2l.torch.predict_ch8('time traveller',10,net,vocab,device)
num_epochs,lr = 500,1
d2l.torch.train_ch8(net,train_iter,vocab,lr,num_epochs,device,use_random_iter=False)
门控制循环单元GRU
门控循环单元与普通的循环神经网络之间的关键区别在于: 前者支持隐状态的门控,这意味着模型有专门的机制来确定应该何时更新隐状态, 以及应该何时重置隐状态,这些机制是可学习的,并且能够解决了上面列出的问题。 例如如果第一个词元非常重要,模型将学会在第一次观测之后不更新隐状态,同样模型也可以学会跳过不相关的临时观测,最后模型还将学会在需要的时候重置隐状态
门:跟隐藏状态长度一样的向量
其实等价全连接层,多了几个权重参数
总体来说,就是隐状态h(t)是怎么从h(t-1)和x(t)推导出来的
没有用GRU的推导公式:
GRU的推导公式:
重置门:有助于捕获序列中的短期依赖关系
更新门:有助于捕获序列中的长期依赖关系
import torch
import d2l.torch
from torch import nn
batch_size,num_steps = 32,35
train_iter,vocab = d2l.torch.load_data_time_machine(batch_size,num_steps)
def get_params(vocab_size,num_hiddens,device):
#初始化参数
input_size = output_size = vocab_size
def normal(shape):
return torch.randn(size=shape,device=device)*0.01
#均值为0,方差为1的初始化权重矩阵
def three():#初始化
return (normal(shape=(input_size,num_hiddens)),
normal(shape=(num_hiddens,num_hiddens)),
torch.zeros(num_hiddens,device=device))
W_xr,W_hr,b_r = three() # 更新门参数
W_xz,W_hz,b_z = three() # 重置门参数
W_xh,W_hh,b_h = three() # 候选隐状态参数
# 输出层参数
W_hq = normal(shape=(num_hiddens,output_size))
b_q = torch.zeros(output_size,device=device)
# 设置参数梯度为True
params = [W_xr,W_hr,b_r,W_xz,W_hz,b_z,W_xh,W_hh,b_h,W_hq,b_q]
for param in params:
param.requires_grad_(True)
return params
#隐状态的初始化函数
def init_gru_state(batch_size,num_hiddens,device):
return (torch.zeros(size=(batch_size,num_hiddens),device=device),)
def gru(inputs,state,params):
W_xr,W_hr,b_r,W_xz,W_hz,b_z,W_xh,W_hh,b_h,W_hq,b_q = params
H, = state
outputs = []#每个时刻的输出
for X in inputs:
R = torch.sigmoid((X @ W_xr)+(H @ W_hr)+b_r)
# @是矩阵乘法
Z = torch.sigmoid((X @ W_xz)+(H @ W_hz)+b_z)
H_tilda = torch.tanh((X @ W_xh)+((R*H) @ W_hh)+b_h)
# *是元素乘法
H = Z*H+(1-Z)*H_tilda
Y = H @ W_hq+b_q
outputs.append(Y)
return torch.cat(outputs,dim=0),(H,)
vocab_size,num_hiddens,device = len(vocab),256,d2l.torch.try_gpu()
num_epochs,lr = 500,1
model = d2l.torch.RNNModelScratch(vocab_size,num_hiddens,device,get_params,init_gru_state,gru)
d2l.torch.train_ch8(model,train_iter,vocab,lr,num_epochs,device,use_random_iter=False)
LSTM
如果F(遗忘门)接近0,那么C(t)尽量不依赖C(t-1)
tanh保证C(t)在(-1,1)之间
LSTM和GRU的区别
- 结构复杂度:LSTM具有三个门控结构(输入门、遗忘门、输出门),而GRU只有两个门控结构(更新门、重置门)。因此,LSTM的结构相对复杂数一些。
- 参数数量:由于LSTM具有更多的门控结构,因此LSTM的参数数量也相对较多。
- 计算效率:GRU由于结构简单、参数较少,因此计算效率相对较高。
- 记忆能力:LSTM具有细胞状态,能够更好地保留长距离信息。而GRU将细胞状态与隐藏状态合并,可能在某些情况下损失一些长程信息。
LSTM和GRU是循环神经网络的两种重要变体,它们在处理序列数据时具有较强的记忆能力和长程依赖捕捉能力。LSTM通过引入门控机制和细胞状态解决了传统RNN的长程依赖问题,而GRU则是LSTM的简化版本,具有较高的计算效率。
在实际应用中,选择LSTM还是GRU需要根据任务的特点和数据的性质进行决策。LSTM适用于更复杂数的序列数据,而GRU由于参数较少、计算更高效,适用于计算资源有限的场景。
无论选择哪种模型,都需要进行合理的超参数调整和模型训练,以实现模型的优化。
import torch
import d2l.torch
from torch import nn
batch_size,num_steps = 32,35
train_iter,vocab = d2l.torch.load_data_time_machine(batch_size,num_steps)
#初始化参数
def get_lstm_params(vocab_size,num_hiddens,device):
input_size = output_size = vocab_size
def normal(shape):
return torch.randn(size=shape,device=device)*0.01 #生成均值为0,方差为0.01的权重参数
def three():
return (normal((input_size,num_hiddens)),
normal((num_hiddens,num_hiddens)),
torch.zeros(num_hiddens,device=device))
W_xi,W_hi,b_i = three() # 输入门参数
W_xf,W_hf,b_f = three() # 遗忘门参数
W_xo,W_ho,b_o = three() # 输出门参数
W_xc,W_hc,b_c = three() # 候选记忆元参数
# 输出层参数
W_hq = normal(shape=(num_hiddens,output_size))
b_q = torch.zeros(output_size,device=device)
# 设置模型权重参数梯度为True
params = [W_xi,W_hi,b_i,W_xf,W_hf,b_f,W_xo,W_ho,b_o,W_xc,W_hc,b_c,W_hq,b_q]
for param in params:
param.requires_grad_(True)
return params
#初始化状态
def init_lstm_state(batch_size,num_hiddens,device):
return (torch.zeros(size=(batch_size,num_hiddens),device=device),
torch.zeros(size=(batch_size,num_hiddens),device=device))
def lstm(inputs,state,params):
# inputs形状为(num_steps,batch_size,vocab_size)为一个批量样本数据
[W_xi,W_hi,b_i,W_xf,W_hf,b_f,W_xo,W_ho,b_o,W_xc,W_hc,b_c,W_hq,b_q] = params
(H,C) = state
outputs = []
for X in inputs:
# X代表每个时间步下的每个批量的数据
# I表示(决定如何)把到目前为止(到当前时间步)看到的数据序列信息有用的部分选择性提取
I = torch.sigmoid((X @ W_xi+H @ W_hi+b_i))
# F表示(决定如何)把到前一个时间步存储的序列数据信息无用的部分选择性遗忘
F = torch.sigmoid((X @ W_xf+H @ W_hf+b_f))
# O表示(决定如何)把从头到当前数据有用的信息选择性部分提取出来
O = torch.sigmoid((X @ W_xo+H @ W_ho+b_o))
#C_tilda表示根据前一个时间步计算出来的隐状态(存储过去时间步的序列信息)结合当前时间步数据信息得到
#到目前为止的数据序列整个看到的信息(有哪些联系)(表示把当前时间步的数据信息与过去序列数据结合起来表示存储从头到当前数据的整个信息)
C_tilda = torch.tanh((X @ W_xc+H @ W_hc+ b_c))
# F*C表示选择性对过去记忆的信息遗忘(对过去看到的序列数据无用的一部分信息选择性遗忘),I*C_tilda表示把从头到当前数据整个信息有用的一部分选择性提取出来,C表示存储从头到当前序列数据中的有用的信息
C = I * C_tilda + F * C
# H表示把从头到当前数据有用信息选择性部分提取出来,用于更新隐状态H,以及作为输入用于预测输出层的输出
H = O * torch.tanh(C)
Y = H @ W_hq + b_q
outputs.append(Y)
return torch.cat(outputs,dim=0),(H,C)
vocab_size,num_hiddens,device=len(vocab),256,d2l.torch.try_gpu()
num_epochs,lr = 500,1
model = d2l.torch.RNNModelScratcha(vocab_size,num_hiddens,device,get_lstm_params,init_lstm_state,lstm)
d2l.torch.train_ch8(model,train_iter,vocab,lr,num_epochs,device,use_random_iter=False)