DL:循环神经网络的基本原理与 PyTorch 实现
循环神经网络Recurrent Neural NetworkRNN是深度学习中专门用于处理序列数据的一类神经网络。与前馈神经网络不同RNN 不只是把输入从前向后逐层传递而是在处理序列时引入“隐藏状态”让模型能够把前面时间步的信息传递到后面时间步。在很多任务中数据并不是彼此独立的静态样本而是按照时间、位置或顺序排列的序列。例如• 一句话中的词语有前后顺序• 一段语音由连续声音帧组成• 一支股票的价格随时间变化• 一个用户的行为记录具有先后关系• 一段传感器数据由连续采样点组成这类数据的核心特点是当前输入的含义往往依赖前面的上下文。循环神经网络正是为这类序列数据设计的基础模型。它通过在时间步之间传递隐藏状态使模型能够在处理当前输入时“记住”前面已经看到的信息。一、为什么需要循环神经网络普通前馈神经网络Feedforward Neural NetworkFNN通常假设输入是固定长度向量并且一次性完成从输入到输出的映射。例如一个前馈网络可以写为其中• x 表示输入样本• ŷ 表示模型预测结果• f 表示由多层计算组成的函数• θ 表示模型中的可训练参数这种结构适合处理固定长度、整体输入的任务例如表格分类、图像分类中的分类头等。但是序列数据通常具有两个特点。第一序列长度可能不固定。一句话可能有 5 个词也可能有 50 个词一段时间序列可能有几十个时间点也可能有几千个时间点。第二序列中的元素具有顺序依赖。同一个词出现在不同上下文中含义可能不同同一个数值在不同时间趋势中也可能代表不同含义。例如句子这部电影一点也不好看如果只看“好看”两个字可能会误判为正面评价但结合前面的“一点也不”整体语义实际上是负面评价。普通前馈网络如果直接把句子或时间序列展平成固定向量通常难以自然表示这种前后依赖关系。图 1从前馈神经网络到循环神经网络循环神经网络的基本思想是按时间步依次读取序列并在每个时间步更新隐藏状态使模型能够把历史信息传递到后续计算中。可以简单理解为• 前馈神经网络一次性处理一个整体输入• 循环神经网络按顺序处理输入并不断更新“记忆”这里的“记忆”并不是人工写入的规则而是模型在训练过程中学习到的隐藏状态表示。二、RNN 的基本结构图 2RNN 的基本循环结构循环神经网络的核心结构是循环单元。它在每个时间步接收两个输入• 当前时间步的输入 xₜ• 上一个时间步的隐藏状态 hₜ₋₁然后计算当前时间步的隐藏状态 hₜ其中• xₜ 表示第 t 个时间步的输入• hₜ₋₁ 表示上一个时间步的隐藏状态• hₜ 表示当前时间步的隐藏状态• Wₓ 表示输入到隐藏状态的权重矩阵• Wₕ 表示隐藏状态到隐藏状态的权重矩阵• b 表示偏置向量• φ 表示激活函数常见为 tanh 或 ReLU如果需要在每个时间步输出结果可以继续写为其中• yₜ 表示第 t 个时间步的输出• Wᵧ 表示隐藏状态到输出的权重矩阵• c 表示输出层偏置• g 表示输出层变换从结构上看RNN 的关键不是某一层有多复杂而是它在时间维度上反复使用同一组参数。也就是说第 1 个时间步、第 2 个时间步、……、第 T 个时间步使用的是同一个 RNN 单元和同一组参数。这类似于 CNN 中的“权重共享”CNN 在空间位置上共享卷积核参数RNN 在时间步上共享循环单元参数。三、按时间展开理解隐藏状态如何传递RNN 的循环结构可以按时间展开Unroll来理解。图 3RNN 按时间展开的结构假设输入序列为RNN 会依次计算一直到其中• T 表示序列长度• h₀ 表示初始隐藏状态通常可以设为零向量• h₁、h₂、…、hₜ 表示不同时间步的隐藏状态从这个过程可以看出hₜ 不只依赖当前输入 xₜ也间接依赖前面所有时间步的信息。例如• h₃ 依赖 x₃ 和 h₂• h₂ 依赖 x₂ 和 h₁• h₁ 依赖 x₁ 和 h₀因此h₃ 实际上包含了 x₁、x₂、x₃ 的历史信息。这就是 RNN 能够处理序列数据的原因。四、RNN 的输入、输出与常见任务形式RNN 可以根据任务需要设计不同的输入输出形式。常见形式包括一对一、一对多、多对一和多对多。图 4RNN 的常见输入输出形式1、一对一普通非序列任务一对一形式类似普通前馈网络一个输入 → 一个输出例如• 图像分类• 表格分类• 单样本回归这类任务本身不一定需要 RNN。2、一对多从一个输入生成一个序列一对多形式是一个输入 → 多个输出例如• 图像生成文字描述• 给定主题生成文本• 给定初始条件生成时间序列这类任务需要模型从一个初始信息开始逐步生成序列输出。3、多对一序列分类多对一形式是多个输入 → 一个输出例如• 文本情感分类• 用户流失预测• 时间序列故障判断• 语音片段分类在这种任务中模型读取完整序列后根据最后的隐藏状态或聚合后的隐藏状态给出一个整体预测。例如文本情感分类可以理解为词语序列 → RNN → 最终隐藏状态 → 情感类别4、多对多序列标注或序列生成多对多形式是多个输入 → 多个输出例如• 词性标注• 命名实体识别• 机器翻译• 语音识别• 每个时间点的状态预测如果输入序列和输出序列长度相同可以在每个时间步输出一个预测结果。如果输入序列和输出序列长度不同则通常需要编码器—解码器结构。五、隐藏状态、记忆能力与局限RNN 的隐藏状态可以理解为模型对历史信息的压缩表示。在第 t 个时间步隐藏状态 hₜ 汇总了当前输入 xₜ 和此前隐藏状态 hₜ₋₁ 中的信息。这样模型就能在处理后续输入时利用前面的上下文。从直观角度看隐藏状态 当前输入 历史信息的综合表示但是普通 RNN 的记忆能力并不是无限的。在较长序列中早期信息需要经过很多时间步才能影响后面的输出。反向传播时梯度也需要沿时间步不断向前传递。这会带来两个经典问题• 梯度消失• 梯度爆炸1、梯度消失梯度消失Vanishing Gradient是指梯度在反向传播过程中逐渐变得非常小导致较早时间步的参数难以有效更新。在长序列任务中这意味着模型很难学习远距离依赖。例如在句子虽然这部电影前半段节奏很慢但结尾非常精彩所以我还是很喜欢如果模型需要根据句子后面的“很喜欢”判断整体情感就必须结合前面的转折结构。如果序列很长普通 RNN 可能难以稳定保留远处信息。2、梯度爆炸梯度爆炸Exploding Gradient是指梯度在反向传播过程中不断放大导致参数更新过大训练不稳定甚至出现损失变成异常值的情况。常见缓解方法包括• 梯度裁剪• 更合适的初始化• 使用 LSTM 或 GRU• 使用更稳定的优化器在 PyTorch 中梯度裁剪常用写法是torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm1.0)3、普通 RNN 的主要局限图 5RNN 的隐藏状态与长距离依赖问题普通 RNN 的主要局限包括• 难以建模长距离依赖• 训练时可能出现梯度消失或梯度爆炸• 序列计算难以完全并行• 对较长文本或复杂上下文任务表现有限因此在很多实际任务中普通 RNN 常被 LSTM、GRU、Transformer 等结构替代或扩展。六、LSTM 与 GRU对普通 RNN 的改进为了解决普通 RNN 难以建模长距离依赖的问题研究者提出了更复杂的循环结构其中最经典的是 LSTM 和 GRU。1、LSTM加入门控机制与记忆单元长短期记忆网络Long Short-Term MemoryLSTM在普通 RNN 的基础上引入了记忆单元和门控机制。LSTM 的核心思想是让模型自己学习哪些信息应该保留哪些信息应该遗忘哪些新信息应该写入。它通常包含三个重要门控• 遗忘门决定保留多少过去信息• 输入门决定写入多少当前信息• 输出门决定输出多少内部记忆可以简单理解为• 普通 RNN所有信息都混合在隐藏状态中• LSTM通过门控机制管理信息的保留、写入和输出LSTM 更适合处理较长序列和复杂上下文关系。2、GRU更简化的门控循环单元门控循环单元Gated Recurrent UnitGRU可以看作一种更简化的门控 RNN。它通常包含• 更新门• 重置门GRU 的结构比 LSTM 更简单参数更少训练速度通常更快。在很多任务中GRU 能取得接近 LSTM 的效果。3、普通 RNN、LSTM 与 GRU 的关系可以粗略理解为• RNN基础循环结构简单但长距离记忆能力有限• LSTM加入更完整的门控机制适合长序列建模• GRU结构更简洁的门控循环网络在效率和效果之间折中图 6RNN、LSTM 与 GRU 的结构对比在学习路径上普通 RNN 适合帮助理解序列建模的基本思想LSTM 和 GRU 则是更常见的实用循环网络结构。七、PyTorch 实现使用 RNN 进行文本情感分类下面使用 PyTorch 构建一个简单 RNN用于演示文本情感分类的基本流程。图 7RNN 文本分类的训练与预测流程为了避免依赖外部大型数据集示例使用一个小型中文样例数据集。这个示例的目标不是训练高性能模型而是帮助理解 RNN 的基本输入、隐藏状态和训练闭环。任务是根据一句中文短句判断它是正面情感还是负面情感。1、导入库并准备数据import torchimport torch.nn as nnfrom torch.utils.data import Dataset, DataLoader构造一个简单数据集# 定义样本数据(文本, 标签) 其中标签1表示正面0表示负面samples [ (《主角》这部电视连续剧非常好看, 1), (演员表演非常精彩, 1), (剧情紧凑我很喜欢, 1), (这家餐厅味道不错, 1), (服务态度很好, 1), (这部电影太难看了, 0), (剧情拖沓让人失望, 0), (演员表演很糟糕, 0), (这家餐厅味道很差, 0), (服务态度不好, 0),]为了简化演示这里按“单字”构建词表。真实任务中通常会使用分词器、子词模型或预训练词向量。2、构建字符表与编码函数# 构建字符表将所有文本中的字符去重后排序chars sorted(set(.join(text for text, _ in samples))) # 为每个字符分配一个唯一ID从1开始0留作填充符char_to_id { char: idx 1 for idx, char in enumerate(chars)} # 填充符ID为0pad_id 0 # 编码函数将文本转为ID序列def encode_text(text): return [char_to_id[char] for char in text]其中• char_to_id 用于把字符转换为整数编号• pad_id0 表示填充符• encode_text() 把文本转换为编号序列例如print(encode_text(电影好看))输出可能类似[33, 22, 16, 34]3、定义数据集与填充函数因为不同句子的长度可能不同所以需要在一个批次中把它们填充到相同长度。class SentimentDataset(Dataset): 情感分析数据集将文本转为ID序列 def __init__(self, samples): self.samples samples # 存储原始样本列表 (text, label) def __len__(self): return len(self.samples) # 返回数据集大小 def __getitem__(self, index): text, label self.samples[index] # 获取一个样本 ids encode_text(text) # 将文本编码为ID列表 # 返回ID张量和标签张量类型均为long return torch.tensor(ids, dtypetorch.long), torch.tensor(label, dtypetorch.long) def collate_fn(batch): 批处理函数将多个样本填充到相同长度并返回长度信息 sequences, labels zip(*batch) # 分离ID序列和标签 # 记录每个序列的原始长度 lengths torch.tensor( [len(seq) for seq in sequences], dtypetorch.long ) # 使用pad_sequence将序列填充到同一长度右侧填充batch_firstTrue输出形状 (B, T) padded_sequences nn.utils.rnn.pad_sequence( sequences, batch_firstTrue, padding_valuepad_id # 使用预设的填充符ID (0) ) labels torch.stack(labels) # 将标签堆叠成一维张量 return padded_sequences, lengths, labels # 返回填充后的序列、原始长度、标签其中• Dataset 用于定义样本读取方式• collate_fn 用于处理一个 batch 中不同长度的序列• pad_sequence 用于把序列填充到相同长度• batch_firstTrue 表示输出形状为 batch_size × sequence_length构建 DataLoader# 创建情感分析数据集实例传入样本列表dataset SentimentDataset(samples) # 创建DataLoader用于批量加载数据train_loader DataLoader( dataset, # 数据集对象 batch_size4, # 每批4个样本 shuffleTrue, # 每个epoch打乱数据顺序 collate_fncollate_fn # 自定义批处理函数负责填充和对齐序列)4、定义 RNN 文本分类模型文本输入需要先经过嵌入层Embedding Layer把整数编号转换为稠密向量。class RNNTextClassifier(nn.Module): 基于RNN的文本分类器 def __init__(self, vocab_size, embed_dim, hidden_size, num_classes): super().__init__() # 词嵌入层将词ID映射为稠密向量 self.embedding nn.Embedding( num_embeddingsvocab_size, # 词汇表大小 embedding_dimembed_dim, # 嵌入维度 padding_idxpad_id # 填充词ID不参与梯度更新 ) # RNN层简单循环神经网络 self.rnn nn.RNN( input_sizeembed_dim, # 输入特征维度嵌入维 hidden_sizehidden_size, # 隐藏状态维度 batch_firstTrue # 输入形状为 (batch, seq, feature) ) # 全连接分类层将最后一层隐藏状态映射到类别logits self.fc nn.Linear(hidden_size, num_classes) def forward(self, x, lengths): # x: 填充后的序列 (batch, seq_len) # lengths: 各序列原始长度本例未直接用于RNN仅作接口保留 # 嵌入层输出 (batch, seq_len, embed_dim) embedded self.embedding(x) # RNN前向传播 # output: 所有时间步的隐藏状态 (batch, seq_len, hidden_size) # hidden: 最后一层各时间步的隐藏状态 (num_layers, batch, hidden_size) output, hidden self.rnn(embedded) # 取最后一层的最后一个时间步即RNN的最终隐藏状态作为文档表示 last_hidden hidden[-1] # 形状 (batch, hidden_size) # 分类输出 logits self.fc(last_hidden) # 形状 (batch, num_classes) return logits这个模型包含三部分Embedding → RNN → Linear其中• Embedding 把字符编号转换为向量• RNN 按顺序读取字符向量并更新隐藏状态• Linear 根据最终隐藏状态输出类别得分需要注意• x 的形状是 batch_size × sequence_length• embedded 的形状是 batch_size × sequence_length × embed_dim• hidden 的形状是 num_layers × batch_size × hidden_size• last_hidden 表示最后一层 RNN 的最终隐藏状态• logits 的形状是 batch_size × num_classes5、定义模型、损失函数和优化器# 词汇表大小字符ID数从1开始 1填充符0vocab_size len(char_to_id) 1embed_dim 16 # 嵌入向量维度hidden_size 32 # RNN隐藏层维度num_classes 2 # 二分类正面/负面 # 实例化RNN文本分类器model RNNTextClassifier( vocab_sizevocab_size, embed_dimembed_dim, hidden_sizehidden_size, num_classesnum_classes) # 损失函数交叉熵适用于多分类此处二分类criterion nn.CrossEntropyLoss()# 优化器Adam学习率0.01optimizer torch.optim.Adam(model.parameters(), lr0.01)其中• vocab_size 表示词表大小• embed_dim 表示字符向量维度• hidden_size 表示隐藏状态维度• num_classes2 表示二分类• CrossEntropyLoss 用于多类分类包括二分类的两个类别输出• Adam 用于更新模型参数这里模型最后输出的是 logits不需要手动添加 Softmax。6、训练模型num_epochs 30 # 训练轮数 for epoch in range(num_epochs): model.train() # 设置为训练模式 total_loss 0.0 # 累计本轮所有样本损失 # 遍历DataLoader每次获取一个batch for batch_x, lengths, batch_y in train_loader: # 前向传播计算logits logits model(batch_x, lengths) # 计算交叉熵损失 loss criterion(logits, batch_y) # 反向传播清空梯度、计算梯度、更新参数 optimizer.zero_grad() loss.backward() optimizer.step() # 累加该batch的总损失乘以batch内样本数 total_loss loss.item() * batch_x.size(0) # 计算本轮平均损失总损失 / 总样本数 avg_loss total_loss / len(dataset) # 每10轮输出一次平均损失 if (epoch 1) % 10 0: print(fEpoch [{epoch 1}/{num_epochs}], Loss: {avg_loss:.4f})训练流程仍然是深度学习中的标准闭环前向传播 → 计算损失 → 清空旧梯度 → 反向传播 → 更新参数其中• logits model(batch_x, lengths) 表示前向传播• loss criterion(logits, batch_y) 表示计算分类损失• optimizer.zero_grad() 清空旧梯度• loss.backward() 自动计算新梯度• optimizer.step() 根据梯度更新参数7、预测新句子def predict_sentiment(text): 预测单条文本的情感倾向正面/负面 model.eval() # 切换到推理模式关闭Dropout等 # 将文本编码为ID张量并添加batch维度形状: 1, seq_len ids torch.tensor( encode_text(text), dtypetorch.long ).unsqueeze(0) # 计算序列长度此处为全部有效长度无填充 lengths torch.tensor([ids.size(1)]) # 无梯度环境下前向传播 with torch.no_grad(): logits model(ids, lengths) # 输出形状 (1, num_classes) pred logits.argmax(dim1).item() # 取最大概率的类别索引 # 将类别索引0或1转换为文字 return 正面 if pred 1 else 负面测试预测print(predict_sentiment(电影非常精彩))print(predict_sentiment(服务太差了))可能输出正面负面由于这里的数据集非常小模型只是演示 RNN 的基本流程不能代表真实中文情感分析系统的效果。真实任务通常需要更大数据集、更可靠的分词方法、更复杂的模型结构以及验证集和测试集评估。8、查看张量形状可以通过打印形状来理解 RNN 的输入输出# 取一个batch的数据用于观察中间张量形状batch_x, lengths, batch_y next(iter(train_loader)) # 通过模型的嵌入层将ID序列转为词向量序列embedded model.embedding(batch_x)# 通过RNN层得到所有时间步的输出和最后一个时间步的隐藏状态output, hidden model.rnn(embedded) # 打印各阶段张量形状print(输入编号形状, batch_x.shape) # (batch, seq_len)print(嵌入后形状, embedded.shape) # (batch, seq_len, embed_dim)print(RNN 输出形状, output.shape) # (batch, seq_len, hidden_size)print(最终隐藏状态形状, hidden.shape) # (num_layers, batch, hidden_size)可能看到类似结果输入编号形状 torch.Size([4, 8])嵌入后形状 torch.Size([4, 8, 16])RNN 输出形状 torch.Size([4, 8, 32])最终隐藏状态形状 torch.Size([1, 4, 32])其中• 4 表示 batch_size• 8 表示当前批次中填充后的序列长度• 16 表示嵌入向量维度• 32 表示隐藏状态维度• 1 表示 RNN 层数八、RNN 的适用场景、局限与扩展方向RNN 是序列建模中的基础模型。它虽然在许多现代任务中已经被 LSTM、GRU 或 Transformer 替代但仍然是理解序列神经网络的重要入口。图 8RNN 的适用场景、局限与扩展方向1、适用场景RNN 适合处理具有顺序关系的数据例如• 文本分类• 时间序列预测• 语音识别• 序列标注• 用户行为建模• 传感器数据分析• 简单序列生成任务这些任务的共同特点是数据元素之间存在顺序依赖。2、主要优势RNN 的主要优势包括• 能处理变长序列• 能通过隐藏状态传递历史信息• 参数在时间步之间共享• 适合解释序列建模的基本思想• 可以扩展为 LSTM、GRU、编码器—解码器结构与前馈神经网络相比RNN 更适合处理“前后相关”的数据。3、主要局限普通 RNN 也有明显局限• 长距离依赖建模能力有限• 容易出现梯度消失或梯度爆炸• 时间步之间存在依赖难以完全并行计算• 对长文本和复杂上下文任务表现有限• 实际应用中常被 LSTM、GRU 或 Transformer 替代这些局限并不意味着 RNN 不重要。相反RNN 是理解序列建模、隐藏状态和时间展开的重要基础。4、扩展方向从普通 RNN 出发可以继续学习• LSTM通过门控机制增强长期记忆能力• GRU更简洁的门控循环网络• 双向 RNN同时利用前向和后向上下文• 编码器—解码器结构用于序列到序列任务• 注意力机制帮助模型直接关注重要时间步• Transformer用自注意力机制替代循环结构提升并行能力和长距离建模能力这些模型虽然结构更复杂但都可以从 RNN 的基本问题出发理解如何让模型有效利用序列中的上下文信息。 小结循环神经网络通过隐藏状态在时间步之间传递历史信息使模型能够处理文本、语音、时间序列等顺序数据。普通 RNN 适合理解序列建模的基本思想但在长距离依赖上存在梯度消失和训练效率问题。LSTM、GRU 和 Transformer 等结构正是在此基础上进一步改进而来。“点赞有美意赞赏是鼓励”