从0开始实现RNN模型
F.one_hot(torch.tensor([0,2]),len(vocab))
对于一个one-hont编码理解
他会返回一个 字典长度的向量,每一个元素表示一个词,如果是这个词,就表示为1
torch.arange(10).reshape(2,5)
F.one_hot(X.T,28)
X的转置,表示,这个minibatch是一个5句话(5个样本点),每一个样本点有两个词,需要的就是把这两个词映射成one-hot编码
def get_params(vocab_size,num_hiddens,device):
# 获得参数
def normal(shape):
return torch.randn(size=shape,device=deivce) * 0.01
#隐藏层参数
W_xh = normal((num_inputs,num_hiddens))
W_hh = normal((num_hiddens,numhiddens)) #这一行就RNN最牛逼的地方,记录了上一层的隐藏变量
b_h = torch.zeros(num_hiddens,device=device)
#输出参数
W_hq = normal((num_hiddens,num_outputs))
b_q = otrch.zeros(num_outputs,device=deivce)
#附加梯度
params = [W_xh,W_hh,b_h,W_hq,b_q]
for param in params:
param.requires_grad_(True)
return params
def init_rnn_state(batch_size,num_hiddens,device):
return (torch.zeros((batcch_size,num_hiddens),device=device),)
这个函数还是帮助我们初始化RNN的参数的
定义的normal函数就是一个辅助函数,给定shape,就给出一个shape
num_inputs = num_outputs = vocab_size,我们理解一下,对于我们的输入虽然是一个句子,但是都被one-hot映射成了vocab size大小的向量,而且我们的输出也是预测,这个词是字典中的某个词,做一个多分类的问题,所以我的输出维度还是一个长度为vocab_size的向量
如果光看W_xh和 b_h,那么这个就算是一个最最最简单,最最最基础的多层感知机,什么都不是,RNN不一样的地方就是加上了W_hh,这个是记录隐藏层的信息
W_hq 输出层的参数 b_q
对于输出层,可以理解为一个全连接层一个Linear,输入输出的维度是(vocabsize,vocabsize)
最后对于参数都要开启梯度记录,用于反向传播
def init_rnn_state(batch_size,num_hiddens,device):
return (torch.zeros((batcch_size,num_hiddens),device=device),)
初始化一个隐藏状态
def rnn(inputs,state,params):
# input的形状(时间步数量,批量大小,vocab的大小)
W_xh,W_hh,b_h,W_hq,b_q = get_params
H, = state
outputs = []
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,)
进行一个rnn的函数
第一步,先把参数展开
第二步,获得一个初始状态,这个状态记录了之前的时序信息
对于这个循环,然后你你看一看到,我们是先更新H
在这里得出一个结论 Yn = output(Hn),就是我发现 Yn 和 Hn竟然是这样的关系,两者的差距就是隔了一个全连接层,啊这
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) #定义call () 会自己调用这个函数,就是线转换成one-hot编码,然后再用RNN
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)
为什么要X的转置?
拿本例来说,我们的输入是一个 2528的矩阵。2是batchsize可以理解为有几句话,5是timestep可以理解为有几个词,28是vocab_size
再进入rnn这个函数里面看
如果不转置(首先我们先忽略H0是一个228的矩阵)
528 点乘 28* 28 + H0*Whh = H1,我们可以看到这个H1记录的是第一句话的信息,但是第一句话和第二句话是独立的,这个和我们的需求是违背的。我们的需求是,拿一句话的第n个词预测以及第n个词之前的历史信息,预测n+1个词。
如果转置 2 * 28,那记录的就是词于词的关系而不是句子与句子的关系了
初始化的没什么好说的
begin_state会初始化隐藏层的状态
def predict_ch8(prefix,num_preds,net,vocab,device):
# prefix 预测的起始句
# num_preds 需要预测字符的数量
# net 传入我们的网络
# vocab 字典的长度
# device 就是装置
state = net.begin_state(batch_size=1,device=device)
outputs = [vocab[prefix[0]]] #有待商榷
print([outputs[-1]])
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_token[i] for i in outputs])
prefix 预测的起始句
num_preds 需要预测字符的数量
net 传入我们的网络
vocab 字典的长度
device 就是装置
第一步更新隐藏状态
第二步装入待预测字符的第一个
第三步,这是一个匿名函数,自动获取output中最后一个字符
第四步,进行一个预热期,这个预热期主要预热的是隐藏状态,让隐藏状态达到一个更好的标准
第五步开始预测,每次传进去一个字符,然后预测下一个字符
最后把output中的索引转换成字符串
def train_epoch_ch8(net, train_iter, loss, updater, device, use_random_iter):
"""训练模型一个迭代周期(定义见第8章)。"""
state, timer = None, d2l.Timer() # 其实状态和这个叫计时器更准确?
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`是个张量
state.detach_() #它的核心思想就是这一部分不做梯度的更新,因为这个是之前的隐藏状态,不许要参与梯度的更新
else:
# `state`对于`nn.LSTM`或对于我们从零开始实现的模型是个张量
for s in state:
s.detach_()
y = Y.T.reshape(-1) # 把y拉成一条向量
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)
# 因为已经调用了`mean`函数
updater(batch_size=1) #没什么说的之后就是迭代
metric.add(l * y.numel(), y.numel())
return math.exp(metric[0] / metric[1]), metric[1] / timer.stop()
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'))
RNN的简洁实现
class RNNModel(nn.Module):
#循环神经网络模型
def __init__(self,rnn_layer,vocab_size,**kwargs):
super().__init__(**kwargs)
self.rnn = rnn_layer
self.vocab_size = vocab_size
self.num_hiddens = self.rnn.hidden_size
# 如果RNN是双向的(之后将介绍),`num_directions`应该是2,否则应该是1。
if not self.rnn.bidirectional: #这一块是为了RNN是不是双向设计的
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)
output = self.linear(Y.reshape((-1,Y.shape[-1])))
return output,state
def begin_state(self,device,batch_size=1):
if not isinstance(self.rnn,nn.LSTM):
# nn.GRU 以张量作为隐藏状态
return torch.zeros((self.num_directions * self.rnn.num_layers,
batch_size, self.num_hiddens),
device=device)
else:
return (torch.zeros((self.num_directions * self.rnn.num_layers,atch_size, self.num_hiddens), device=device),torch.zeros((
self.num_directions * self.rnn.num_layers,
batch_size, self.num_hiddens), device=device))