1. 循环神经网络RNN循环神经网络通俗讲解前面几节我们已经把文字变成了数字、学会了怎么切训练数据。这一节终于要上真正的模型了——RNN循环神经网络。这是处理序列数据文本、语音、时间序列最经典的神经网络结构。我们先搞懂RNN到底解决了什么问题、核心思想是什么再逐段看代码。RNN要解决什么问题为什么普通神经网络不行回忆一下第一节的MLP多层感知机输入固定4个数字输出1个数字。但文本预测有个麻烦——句子长度是不固定的而且离得远的词也可能互相影响比如我出生在法国...我的母语是____要猜空里的词是法语得记住前面很远的法国这个信息。普通神经网络处理不了这种不定长需要记住历史的情况。RNN的解决办法是引入一个记忆隐藏状态 hidden state像滚雪球一样每处理一个新词就把这个词的信息和之前积累的记忆融合更新出新的记忆一直往后传递。核心类比想象你在读一本侦探小说读到第100页时你脑子里其实存着一个故事摘要记住了前面99页发生的关键情节。读到第100页新内容时你会把新内容和脑子里的故事摘要结合起来更新出一个新的故事摘要然后继续读第101页。这个不断更新的故事摘要就是RNN里的隐藏状态hidden state。第一部分独热编码One-hot EncodingF.one_hot(torch.tensor([0,2]), len(vocab))问题前面词表把每个字符变成了一个数字编号比如a5,b8。但这些数字编号本身是没有大小意义的——编号8不代表比编号5大或者更好它们只是标签。如果直接把编号5、8这样的数字喂给神经网络做矩阵运算网络会误以为这些数字之间有大小关系这是错的。解决办法独热编码。把每个编号转换成一个只有一个位置是1其他全是0的长向量。比如词表大小是28编号0就变成[1,0,0,0,...0]第0位是1其余27位是0编号2就变成[0,0,1,0,...0]第2位是1。类比就像给每个字符发一张专属身份证这张身份证是一长串灯泡只有属于这个字符的那一盏灯亮着其他灯都不亮。这样每个字符在网络眼里都是完全平等、独立的不会被误认为有大小/远近关系。F.one_hot(X.T, 28).shape # torch.Size([5, 2, 28])这里做了个转置X.T形状从(批量2,步数5)变成(步数5,批量2)再做独热编码得到形状(5, 2, 28)。为什么要把时间步数放在最前面因为RNN的计算逻辑是一个时间步一个时间步地处理把时间步维度放第一位方便后面直接用for X in inputs循环一步步取出每个时间步的数据来处理马上就会在rnn函数里看到。第二部分初始化模型参数get_paramsW_xh normal((num_inputs, num_hiddens)) # 输入 → 隐藏 W_hh normal((num_hiddens, num_hiddens)) # 隐藏 → 隐藏这是RNN的关键 b_h torch.zeros(num_hiddens) W_hq normal((num_hiddens, num_outputs)) # 隐藏 → 输出 b_q torch.zeros(num_outputs)这5个参数是RNN的全部学习对象对应3种变换W_xh输入→隐藏决定这一时刻新看到的字符该如何影响记忆W_hh隐藏→隐藏决定上一时刻的记忆该如何延续到这一时刻——这是RNN和普通网络最本质的区别普通网络没有这种自己传给自己的连接W_hq隐藏→输出决定当前的记忆该如何转换成预测下一个字符是什么的输出类比还是故事摘要的例子——W_xh相当于这一页新内容有多重要该怎么记下来W_hh相当于之前的摘要该保留多少、怎么和新内容融合W_hq相当于根据目前的摘要我该怎么猜接下来会发生什么。第三部分RNN的核心计算rnn函数这是全篇最关键的一段代码一定要吃透def rnn(inputs, state, params): W_xh, W_hh, b_h, W_hq, b_q 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, dim0), (H,)这里发生了什么inputs是整个序列比如35个时间步代码用for X in inputs一步一步遍历每一步只处理一个时间步的数据核心公式H_新 tanh(X_当前 · W_xh H_旧 · W_hh b_h)这一步在做什么拿当前时间步的输入 X和上一时间步传下来的记忆 H_旧各自做一次线性变换后加起来再加个偏置b_h最后套上tanh激活函数把结果压缩到-1到1之间防止数值爆炸也引入非线性得到更新后的记忆 H_新。然后Y H_新 · W_hq b_q用这个刚更新好的记忆去计算这一时间步的输出——也就是根据目前看到的所有内容模型猜下一个字符最可能是什么。关键点这个H隐藏状态在for循环里是不断被覆盖更新、并传递到下一次循环的——这正是记忆在时间步之间流动的具体实现每处理完一个字符H就更新一次带着到目前为止看过的所有内容的浓缩信息继续参与下一个字符的计算。类比整个循环想象一条传送带每个时间步传送带上过来一个新字符X工人RNN拿着手里的记忆笔记本H把新字符的信息和笔记本上原有的内容融合更新笔记本H_新同时根据更新后的笔记本写一个预测Y然后带着更新后的笔记本迎接下一个字符。torch.cat(outputs, dim0)把所有时间步产生的输出Y按顺序拼接成一个大张量返回方便后面统一计算损失。第四部分把这些函数包装成一个模型类RNNModelScratchclass RNNModelScratch: def __init__(self, ...): self.params get_params(...) # 初始化参数 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) # 再调用rnn函数计算 def begin_state(self, batch_size, device): return self.init_state(batch_size, self.num_hiddens, device) # 返回全零的初始记忆这只是一个整理箱把前面写好的get_params造参数、init_rnn_state造初始记忆全是0表示一开始什么都不记得、rnn真正的计算逻辑三个零件组装到一起做成一个能直接调用的模型对象。验证形状Y.shape, len(new_state), new_state[0].shape # (torch.Size([10, 28]), 1, torch.Size([2, 512]))Y.shape (10, 28)10 5个时间步 × 2个批量样本拼在一起了28是词表大小每个位置输出下一个字符是词表里每个字符的可能性得分new_state[0].shape (2, 512)2是批量大小512是隐藏单元数——这就是最后时刻的记忆第五部分预测函数predict_ch8—— 怎么用训练好的模型写句子def predict_ch8(prefix, num_preds, net, vocab, device): state net.begin_state(batch_size1, devicedevice) # 记忆清零从头开始 outputs [vocab[prefix[0]]] # 先把用户给的开头第一个字符记下来 get_input lambda: torch.tensor([outputs[-1]],devicedevice).reshape(1,1) for y in prefix[1:]: # 把prefix剩下的字符依次喂给模型更新记忆(但不用它的预测结果) _, state net(get_input(), state) outputs.append(vocab[y]) for _ in range(num_preds): # 开始真正的自由发挥生成 y, state net(get_input(), state) outputs.append(int(y.argmax(dim1).reshape(1))) # 取模型认为最可能的下一个字符 return .join([vocab.idx_to_token[i] for i in outputs])这个函数做两件事第一步热身阶段把用户提供的开头比如time traveller 逐字符喂给模型目的只是为了把这些已知信息灌进隐藏状态里让模型的记忆里已经装好了这段开头的内容这个阶段不需要用模型的输出因为这些字符是已知的不用猜。第二步自由生成阶段从这里开始模型每次根据当前记忆预测下一个字符最可能是什么y.argmax(dim1)就是找输出向量里得分最高的那个位置对应哪个字符然后把这个刚生成的字符当作下一步的新输入继续生成下一个字符如此反复就写出了一整段续写文字。类比就像手机输入法的联想功能——你打了今天天气输入法根据这几个字猜你接下来想打真好然后你如果接受了真好输入法又根据今天天气真好接着猜下一个词一步步往后联想。训练前的预测模型是瞎猜的predict_ch8(time traveller , 10, net, vocab, d2l.try_gpu()) time traveller uupwqaydrq因为参数还是刚初始化的随机值所以生成的后续内容是乱码完全没有意义——这验证了模型能跑起来但还没学到任何语言规律。第六部分训练技巧——梯度裁剪grad_clippingnorm torch.sqrt(sum(torch.sum((p.grad**2)) for p in params)) if norm theta: for param in params: param.grad[:] * theta / norm问题RNN因为要把记忆沿着时间步不断传递前面讲的for X in inputs循环在反向传播算梯度时梯度也要沿着这条链条一路传回去。如果序列很长这个梯度经过很多次连续相乘很容易变得极其巨大梯度爆炸或极其微小梯度消失。梯度一旦爆炸参数更新的步子会迈得特别大直接把训练带崩。解决办法算出所有参数梯度合在一起的总体大小norm就像算一个向量的长度如果这个总体大小超过了一个阈值theta就按比例把所有梯度整体缩小让它的总体大小刚好等于theta。类比就像开车时如果发现油门踩得太猛车速要失控了赶紧按比例把油门松一松让车速回到安全范围内但不改变往哪个方向开只缩小步子大小不改变梯度方向。这是训练RNN几乎必备的一个技巧。第七部分训练一轮的过程train_epoch_ch8for X, Y in train_iter: if state is None or use_random_iter: state net.begin_state(...) # 重新开始记忆(清零) else: state.detach_() # 保留记忆的数值但切断计算图的历史连接 y Y.T.reshape(-1) y_hat, state net(X, state) l loss(y_hat, y.long()).mean() ...这里有个容易让人懵的细节state.detach_()是干什么的回忆前面讲的顺序分区采样方法——相邻两个batch在原文里是紧挨着的所以我们希望隐藏状态记忆能延续到下一个batch不要每个batch都重新清零不然就白白浪费了顺序分区特意保留的连续性。但是如果直接把上一个batch算出来的state原封不动地传给下一个batchPyTorch会记得这个state是由第一个batch的所有计算一步步得到的反向传播时会试图把梯度一路传回第一个batch这样计算图会越滚越大占用内存爆炸、速度也越来越慢。detach_()的作用是保留state当前的数值记忆的内容还在但把它和之前是怎么计算出来的这段历史剪断让它看起来就像是一个全新的、凭空冒出来的初始值。这样下一个batch在反向传播时只需要往回传播这一个batch自己的计算不会牵扯到更早的历史。类比就像你读书读到第100页时脑子里记得故事内容数值保留但你不需要记得我是怎么一字一句读到这里的每一个细节过程计算过程剪断——你只需要带着当前的理解继续往下读不需要每次都从头回顾一遍阅读过程。第八部分损失函数与困惑度Perplexityloss nn.CrossEntropyLoss() ... return math.exp(metric[0]/metric[1]), ...交叉熵损失CrossEntropyLoss这是分类任务最常用的损失函数。这里本质上是把预测下一个字符看成一个分类问题——词表里有28个字符每次都要从这28个类别里猜哪个是正确答案交叉熵衡量的就是模型给正确答案打的概率分数有多低分数越低说明模型越自信且猜对了loss就越小。困惑度Perplexity, ppl代码里对loss取了个指数math.exp(...)这就是困惑度是语言模型专用的评价指标。怎么理解困惑度可以粗略地理解为模型在猜下一个字符时大概像在几个选项里随机选一样犹豫不定。困惑度1表示模型几乎100%确定下一个字符是什么完全不困惑困惑度越大表示模型越迷茫、猜测的不确定性越高。所以困惑度越低说明模型学得越好。训练完之后打印出困惑度 1.0或1.3说明模型已经把这本小说的字符规律学得相当好了几乎能确定地猜出下一个字符。第九部分训练完之后的效果对比顺序分区训练后time traveller for so it will be convenient to speak of himwas e这句话已经非常接近原文了time traveller for so it will be convenient to speak of him正是原文中真实出现过的句子说明模型确实学会了这段文本的字符级规律。随机采样训练后time travellerit s against reason said filbywhat thatlly i so di效果也不错但因为随机采样时batch之间没有连续性记忆经常被清零重来学习效率和最终效果通常会比顺序分区稍差一点点。第十部分用PyTorch框架简化实现nn.RNN前面手写的get_params、rnn函数本质上PyTorch已经内置好了直接调用nn.RNN就行不用自己写公式rnn_layer nn.RNN(len(vocab), num_hiddens)这一行就自动包含了前面手写的所有参数W_xh, W_hh, b_h等和计算逻辑tanh那个公式全部封装好了。Y, state_new rnn_layer(X, state) Y.shape, state_new.shape # (torch.Size([35, 32, 256]), torch.Size([1, 32, 256]))注意这里nn.RNN直接返回的Y形状是(35, 32, 256)——这里的256是隐藏层大小不是词表大小这说明nn.RNN这一层本身只负责计算记忆的演变还没有做记忆→输出预测这最后一步对应手写版里的W_hq那一步。所以还需要额外接一个nn.Linear层把隐藏状态转换成预测词表里每个字符的得分。这也是为什么下面要单独包装一个RNNModel类。第十一部分RNNModel类——把nn.RNN和输出层拼起来class RNNModel(nn.Module): def __init__(self, rnn_layer, vocab_size, **kwargs): super(RNNModel, self).__init__(**kwargs) self.rnn rnn_layer self.linear nn.Linear(self.num_hiddens, self.vocab_size) # 补上记忆→输出这一步 def forward(self, inputs, state): X F.one_hot(inputs.T.long(), self.vocab_size).to(torch.float32) # 独热编码 Y, state self.rnn(X, state) # 用框架自带的RNN算出记忆的演变 output self.linear(Y.reshape((-1, Y.shape[-1]))) # 再用线性层把记忆转换成预测 return output, state这和手写版本的逻辑完全一样只是把记忆的更新计算这部分交给PyTorch内置的nn.RNN速度更快、经过底层优化自己只需要额外补一个线性层做最后的输出转换。为什么要Y.reshape((-1, Y.shape[-1]))因为Y的形状是(时间步数, 批量大小, 隐藏层大小)而线性层需要的输入是一堆样本每个样本是一个向量所以把前两维时间步×批量压扁成一维变成(时间步数×批量大小, 隐藏层大小)这样才能批量地喂给nn.Linear统一计算。begin_state函数额外处理了LSTM的情况LSTM是RNN的一个更高级变种会在后面章节学到它需要两个状态而不是一个这里先不用深究只需要知道这是为了兼容以后要学的LSTM。整体串起来看这一节的核心独热编码把字符的编号转换成网络能安全处理的向量形式RNN的核心公式H_新 tanh(X·W_xh H_旧·W_hh b_h)这是全篇最重要的一行实现了记忆随时间步不断更新、传递的机制梯度裁剪防止训练时因为长序列梯度爆炸而崩溃顺序分区 detach让记忆能跨batch延续同时避免反向传播计算图无限增长困惑度衡量语言模型好坏的专用指标越低越好手写版 vs 框架版两者算法完全一致框架版只是把重复的底层公式用nn.RNN封装好了实际写论文/项目时通常直接用框架版即可但理解手写版有助于真正搞懂RNN内部在干什么如果之后要看LSTM或GRURNN的改良版用来解决长序列记不住早期信息的问题这一节的隐藏状态H的概念是完全通用的基础吃透了这里后面会轻松很多。1. RNN的实现(不用框架)%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)# 独热编码 # 打印词汇表的大小 print(len(vocab)) # 使用独热编码将 [0, 2] 表示的物体下标转换为独热向量其中0表示第一个元素2表示第3个元素 F.one_hot(torch.tensor([0,2]),len(vocab)) # [0,2] 表示物体下标0表示第一个元素2表示第3个元素28tensor([[1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]])# 小批量形状是(批量大小时间步数) # 创建一个张量形状为(2, 5)表示批量大小为2时间步数为5 X torch.arange(10).reshape((2,5)) # 对X的转置进行独热编码其中28表示编码长度返回独热编码后的形状 F.one_hot(X.T, 28).shapetorch.Size([5, 2, 28])# 初始化循环神经网络模型的模型参数 def get_params(vocab_size, num_hiddens, device): # 设置输入和输出的维度为词汇表大小 num_inputs num_outputs vocab_size # 定义normal函数用于生成服从正态分布的随机张量并乘以0.01进行缩放 def normal(shape): return torch.randn(sizeshape, devicedevice) * 0.01 # 初始化模型参数 # 输入到隐藏层的权重矩阵形状为(词汇表大小, 隐藏单元个数) W_xh normal((num_inputs, num_hiddens)) # 隐藏层到隐藏层的权重矩阵形状为(隐藏单元个数, 隐藏单元个数) W_hh normal((num_hiddens, num_hiddens)) # 隐藏层的偏置向量形状为(隐藏单元个数,) b_h torch.zeros(num_hiddens, devicedevice) # 隐藏层到输出层的权重矩阵形状为(隐藏单元个数, 词汇表大小) W_hq normal((num_hiddens, num_outputs)) # 输出层的偏置向量形状为(词汇表大小,) b_q torch.zeros(num_outputs, devicedevice) # 将所有参数放入列表中 params [W_xh, W_hh, b_h, W_hq, b_q] # 遍历所有参数 for param in params: # 设置参数的requires_grad为True用于梯度计算 param.requires_grad_(True) # 返回模型的参数 return params# 一个init_rnn_state函数在初始化时返回隐藏状态 def init_rnn_state(batch_size, num_hiddens, device): # 返回一个包含隐藏状态的元组元组中的唯一元素是一个形状为(批量大小, 隐藏单元个数)的全零张量 return (torch.zeros((batch_size, num_hiddens), devicedevice),)# 下面的rnn函数定义了如何在一个时间步计算隐藏状态和输出 def rnn(inputs, state, params): # 从参数元组中解包获取输入到隐藏层的权重矩阵 W_xh # 隐藏层到隐藏层的权重矩阵 W_hh # 隐藏层的偏置向量 b_h # 隐藏层到输出层的权重矩阵 W_hq # 输出层的偏置向量 b_q W_xh, W_hh, b_h, W_hq, b_q params # 从状态元组中解包获取隐藏状态 H # 注意这里使用逗号是为了确保 H 为一个元组 H, state # 创建一个空列表用于存储输出 outputs [] # 对于输入序列中的每个输入 X # 输入序列通常是一个时间步的数据可以是单个时间步的特征向量或者是嵌入向量 for X in inputs: # 计算新的隐藏状态 H使用双曲正切函数作为激活函数 # 根据当前输入 X、上一时间步的隐藏状态 H、以及权重矩阵和偏置向量来计算 H torch.tanh(torch.mm(X, W_xh) torch.mm(H, W_hh) b_h) # 计算输出 Y通过隐藏状态 H 与权重矩阵 W_hq 相乘并加上偏置向量 b_q 得到 Y torch.mm(H, W_hq) b_q # 将输出 Y 添加到输出列表中 outputs.append(Y) # 将输出列表中的输出张量沿着行维度进行拼接得到一个形状为 (时间步数 * 批量大小, 输出维度) 的张量 # 返回拼接后的输出张量和最后一个时间步的隐藏状态 H return torch.cat(outputs, dim0), (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 # 调用 get_params 函数初始化模型的参数并保存为类的属性 # 参数包括输入到隐藏层的权重矩阵、隐藏层到隐藏层的权重矩阵、隐藏层的偏置向量、隐藏层到输出层的权重矩阵、输出层的偏置向量 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)# 检查输出是否具有正确的形状 # 设置隐藏单元个数为 512 num_hiddens 512 # 创建一个 RNNModelScratch 的实例 net指定词汇表大小、隐藏单元个数、设备、获取参数函数、初始化隐藏状态函数和前向传播函数 net RNNModelScratch(len(vocab), num_hiddens, d2l.try_gpu(), get_params, init_rnn_state, rnn) # 获取模型的初始隐藏状态输入的批量大小为 X 的行数设备使用与 X 相同的设备 state net.begin_state(X.shape[0], d2l.try_gpu()) # 使用输入 X 和初始隐藏状态进行前向传播计算得到输出张量 Y 和更新后的隐藏状态 new_state # 将输入和状态都移动到与 X 相同的设备上进行计算 Y, new_state net(X.to(d2l.try_gpu()),state) # 输出 Y 的形状new_state 的长度即元素个数和 new_state 中第一个元素的形状 Y.shape, len(new_state), new_state[0].shape(torch.Size([10, 28]), 1, torch.Size([2, 512]))# 首先定义预测函数来生成用户提供的prefix之后的新字符 def predict_ch8(prefix, num_preds, net, vocab, device): 在 prefix 后面生成新字符。 # 获取模型的初始隐藏状态批量大小为 1设备为指定的设备 state net.begin_state(batch_size1, devicedevice) # 将 prefix 的第一个字符索引添加到输出列表中 outputs [vocab[prefix[0]]] # 定义一个函数 get_input用于获取输入序列的张量表示 # 输入序列只包含一个字符将该字符的索引转换为张量并进行形状调整为 (1, 1) get_input lambda: torch.tensor([outputs[-1]],devicedevice).reshape(1,1) # 对于 prefix 中除第一个字符之外的每个字符 y for y in prefix[1:]: # 使用当前输入字符和隐藏状态进行前向传播计算得到输出和更新后的隐藏状态 _, state net(get_input(), state) # 将字符 y 的索引添加到输出列表中 outputs.append(vocab[y]) # 生成指定数量的新字符 for _ in range(num_preds): # 使用当前输入字符和隐藏状态进行前向传播计算得到输出和更新后的隐藏状态 y, state net(get_input(), state) # 将输出张量中概率最大的字符索引添加到输出列表中 outputs.append(int(y.argmax(dim1).reshape(1))) # 将输出列表中的字符索引转换为对应的字符并拼接成一个字符串返回 return .join([vocab.idx_to_token[i] for i in outputs]) # 生成以 time traveller 为前缀的 10 个新字符 # 注意由于模型尚未训练这里的预测结果是随机初始化后的预测 predict_ch8(time traveller , 10, net, vocab, d2l.try_gpu())time traveller uupwqaydrq① 梯度裁剪def grad_clipping(net, theta): 裁剪梯度。 # 如果 net 是 nn.Module 的实例即使用 PyTorch 构建的模型 if isinstance(net, nn.Module): # 获取所有需要计算梯度的参数列表 params [p for p in net.parameters() if p.requires_grad] # 如果 net 是自定义的模型例如上述的 RNNModelScratch else: # 获取自定义模型的参数列表 params net.params # 计算参数梯度的范数即所有参数梯度平方和的平方根 norm torch.sqrt(sum(torch.sum((p.grad**2)) for p in params)) # 如果梯度范数超过指定阈值 theta if norm theta: # 对于每个参数 for param in params: # 将参数的梯度值裁剪至指定范围内保持梯度范数不超过 theta param.grad[:] * theta / norm# 定义一个函数来训练只有一个迭代周期的模型 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: # 初始化隐藏状态批量大小为 X 的行数设备为指定的设备 state net.begin_state(batch_sizeX.shape[0],devicedevice) else: # 如果 net 是 nn.Module 的实例且隐藏状态不是元组类型 if isinstance(net, nn.Module) and not isinstance(state, tuple): # 分离隐藏状态的计算图 state.detach_() else: # 对于隐藏状态中的每个元素 for s in state: # 分离隐藏状态的计算图用于减少内存占用和加速计算 s.detach_() # 将目标序列 Y 转置并展平为一维张量 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() # 如果使用 PyTorch 内置的优化器 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_size1) # 累加损失和样本数量 metric.add(l * y.numel(), y.numel()) # 计算平均损失和每秒处理的样本数返回平均损失的指数形式以 e 为底和每秒样本处理速度 return math.exp(metric[0]/metric[1]), metric[1]/timer.stop()# 训练函数支持从零开始或使用高级API实现的循环神经网络模型 def train_ch8(net, train_iter, vocab, lr, num_epochs, device, use_random_iterFalse): 训练模型 # 定义损失函数为交叉熵损失 loss nn.CrossEntropyLoss() # 创建动画对象用于可视化训练过程的损失变化 animator d2l.Animator(xlabelepoch, ylabelperplexity, legend[train],xlim[10,num_epochs]) # 如果模型是 nn.Module 的实例 if isinstance(net, nn.Module): # 使用 PyTorch 的优化器 SGD 进行参数更新 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) # 每隔 10 个迭代周期生成 if (epoch 1) % 10 0: # 打印以 time traveller 为前缀的新字符序列 print(predict(time traveller)) # 将当前迭代周期的困惑度添加到动画中进行可视化 animator.add(epoch 1, [ppl]) # 打印最终的困惑度和每秒样本处理速度 print(f困惑度 {ppl:.1f}, {speed:.1f} 标记/秒 {str(device)}) # 生成并打印以 time traveller 为前缀的新字符序列 print(predict(time traveller)) # 生成并打印以 traveller 为前缀的新字符序列 print(predict(traveller))# 现在我们可以训练循环神经网络模型 # 设置迭代周期数和学习率 num_epochs, lr 500, 1 # 调用训练函数进行模型训练使用训练数据迭代器、词汇表、学习率、迭代周期数和设备信息作为输入 train_ch8(net, train_iter, vocab, lr, num_epochs, d2l.try_gpu())困惑度 1.0, 74455.3 标记/秒 cuda:0 time traveller for so it will be convenient to speak of himwas e traveller with a slight accession ofcheerfulness really thi# 最后让我们检查一下使用随即抽样方法的结果 # 调用训练函数进行模型训练使用训练数据迭代器、词汇表、学习率、迭代周期数、设备信息和随机抽样标志位作为输入 # 设置 use_random_iter 参数为 True表示使用随机抽样方法进行训练 train_ch8(net, train_iter, vocab, lr, num_epochs, d2l.try_gpu(), use_random_iterTrue)困惑度 1.3, 69106.4 标记/秒 cuda:0 time travellerit s against reason said filbywhat thatlly i so di travellerit s against reason said filbywhat thatlly i so di2. RNN的实现(用框架)# 导入 PyTorch 库 import torch # 导入 nn 模块用于定义神经网络模型的基类 from torch import nn # 导入 functional 模块用于定义神经网络模型中的激活函数等功能 from torch.nn import functional as F # 导入 d2l.torch 模块包含了与深度学习相关的工具函数和类 from d2l import torch as d2l # 设置批量大小和时间步数 batch_size, num_steps 32, 35 # 调用 load_data_time_machine 函数加载时间机器数据集返回训练数据迭代器和词汇表 train_iter, vocab d2l.load_data_time_machine(batch_size, num_steps)# 定义模型 # 设置隐藏单元的数量为 256 num_hiddens 256 # 使用 nn.RNN 类定义一个循环神经网络层 # 输入大小为词汇表的大小隐藏单元数量为 num_hiddens # 将该循环神经网络层赋值给 rnn_layer rnn_layer nn.RNN(len(vocab), num_hiddens)# 使用张量来初始化隐藏状态 # 创建一个形状为 (1, batch_size, num_hiddens) 的张量用于初始化隐藏状态 # 全部元素初始化为 0 # 将该张量赋值给变量 state state torch.zeros((1, batch_size, num_hiddens)) # 打印隐藏状态张量的形状 state.shapetorch.Size([1, 32, 256])# 通过一个隐藏状态和一个输入我们可以用更新后的隐藏状态计算输出 # 创建一个形状为 (num_steps, batch_size, len(vocab)) 的随机张量 X # 用于表示输入的序列每个时间步的输入为一个词汇表大小的独热编码向量 X torch.rand(size(num_steps, batch_size, len(vocab))) # 将输入 X 和初始隐藏状态 state 作为输入传递给循环神经网络层 rnn_layer 进行前向计算 # 返回输出张量 Y 和更新后的隐藏状态 state_new Y, state_new rnn_layer(X, state) # 打印输出张量 Y 和更新后的隐藏状态 state_new 的形状 Y.shape, state_new.shape(torch.Size([35, 32, 256]), torch.Size([1, 32, 256]))# 我们为一个完整的循环神经网络模型定义一个RNNModel类 class RNNModel(nn.Module): 循环神经网络模型 # 初始化函数 def __init__(self, rnn_layer, vocab_size, **kwargs): # 调用父类的构造函数初始化继承的属性 super(RNNModel, self).__init__(**kwargs) # 循环神经网络层 self.rnn rnn_layer # 词汇表大小 self.vocab_size vocab_size # 隐藏状态的大小 self.num_hiddens self.rnn.hidden_size # 如果循环神经网络不是双向的 if not self.rnn.bidirectional: # 方向数量为1 self.num_directions 1 # 线性层的输入大小为隐藏状态大小输出大小为词汇表大小 self.linear nn.Linear(self.num_hiddens, self.vocab_size) # 如果循环神经网络是双向的 else: # 方向数量为2 self.num_directions 2 # 线性层的输入大小为隐藏状态大小的两倍输出大小为词汇表大小 self.linear nn.Linear(self.num_hiddens * 2, self.vocab_size) # 前项传播函数 def forward(self, inputs, state): # 将输入的索引序列转换为独热编码张量 X X F.one_hot(inputs.T.long(), self.vocab_size) # 将 X 转换为 float32 类型 X X.to(torch.float32) # 使用循环神经网络层 rnn 进行前向计算返回输出张量 Y 和更新后的隐藏状态 state Y, state self.rnn(X, state) # 将输出张量 Y 展平并通过线性层 linear 进行变换得到最终的输出 output self.linear(Y.reshape((-1, Y.shape[-1]))) # 返回最终输出和更新后的隐藏状态 return output, state # 创建循环神经网络的初始隐藏状态 def begin_state(self, device, batch_size1): # 如果循环神经网络不是LSTM类型 if not isinstance(self.rnn, nn.LSTM): # 创建全零的隐藏状态张量 return torch.zeros((self.num_directions * self.rnn.num_layers, batch_size, self.num_hiddens), devicedevice) # 如果循环神经网络是LSTM类型 else: # 创建全零的隐藏状态张量和记忆单元张量 # 第一个张量是全零的隐藏状态张量第二个张量是全零的记忆单元张量 return (torch.zeros((self.num_directions * self.rnn.num_layers, batch_size, self.num_hiddens), devicedevice), torch.zeros((self.num_directions * self.rnn.num_layers, batch_size, self.num_hiddens), devicedevice))# 用一个具有随即权重的模型进行预测 # 尝试使用GPU设备如果不可用则使用CPU device d2l.try_gpu() # 创建RNN模型实例 net RNNModel(rnn_layer, vocab_sizelen(vocab)) # 将模型移动到指定设备上 net net.to(device) # 对模型进行预测生成文本 d2l.predict_ch8(time traveller, 10, net, vocab, device)time travellerrrrrrrrrrr# 使用高级API训练模型 # 设置训练的迭代周期数和学习率 num_epochs, lr 500, 1 # 使用高级API训练模型传入模型、训练数据迭代器、词汇表、学习率和迭代周期数进行训练 d2l.train_ch8(net, train_iter, vocab, lr, num_epochs, device)perplexity 1.3, 270684.1 tokens/sec on cuda:0 time travelleryon oug heally a chifily exper and have at atw yo travelleryon o have e tern whis vewy cand scascolleredit to
