DL:循环神经网络的基本原理与 PyTorch 实现

循环神经网络(Recurrent Neural Network,RNN)是深度学习中专门用于处理序列数据的一类神经网络。与前馈神经网络不同,RNN 不只是把输入从前向后逐层传递,而是在处理序列时引入"隐藏状态",让模型能够把前面时间步的信息传递到后面时间步。

在很多任务中,数据并不是彼此独立的静态样本,而是按照时间、位置或顺序排列的序列。

例如:

• 一句话中的词语有前后顺序

• 一段语音由连续声音帧组成

• 一支股票的价格随时间变化

• 一个用户的行为记录具有先后关系

• 一段传感器数据由连续采样点组成

这类数据的核心特点是:当前输入的含义往往依赖前面的上下文。

循环神经网络正是为这类序列数据设计的基础模型。它通过在时间步之间传递隐藏状态,使模型能够在处理当前输入时"记住"前面已经看到的信息。

一、为什么需要循环神经网络

普通前馈神经网络(Feedforward Neural Network,FNN)通常假设输入是固定长度向量,并且一次性完成从输入到输出的映射。

例如,一个前馈网络可以写为:

其中:

• x 表示输入样本

• ŷ 表示模型预测结果

• f 表示由多层计算组成的函数

• θ 表示模型中的可训练参数

这种结构适合处理固定长度、整体输入的任务,例如表格分类、图像分类中的分类头等。

但是,序列数据通常具有两个特点。

第一,序列长度可能不固定。

一句话可能有 5 个词,也可能有 50 个词;一段时间序列可能有几十个时间点,也可能有几千个时间点。

第二,序列中的元素具有顺序依赖。

同一个词出现在不同上下文中,含义可能不同;同一个数值在不同时间趋势中,也可能代表不同含义。

例如,句子:

go 复制代码
这部电影一点也不好看

如果只看"好看"两个字,可能会误判为正面评价;但结合前面的"一点也不",整体语义实际上是负面评价。

普通前馈网络如果直接把句子或时间序列展平成固定向量,通常难以自然表示这种前后依赖关系。

图 1:从前馈神经网络到循环神经网络

循环神经网络的基本思想是:按时间步依次读取序列,并在每个时间步更新隐藏状态,使模型能够把历史信息传递到后续计算中。

可以简单理解为:

• 前馈神经网络:一次性处理一个整体输入

• 循环神经网络:按顺序处理输入,并不断更新"记忆"

这里的"记忆"并不是人工写入的规则,而是模型在训练过程中学习到的隐藏状态表示。

二、RNN 的基本结构

图 2:RNN 的基本循环结构

循环神经网络的核心结构是循环单元。它在每个时间步接收两个输入:

• 当前时间步的输入 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)来理解。

图 3:RNN 按时间展开的结构

假设输入序列为:

RNN 会依次计算:

一直到:

其中:

• T 表示序列长度

• h₀ 表示初始隐藏状态,通常可以设为零向量

• h₁、h₂、...、hₜ 表示不同时间步的隐藏状态

从这个过程可以看出,hₜ 不只依赖当前输入 xₜ,也间接依赖前面所有时间步的信息。

例如:

• h₃ 依赖 x₃ 和 h₂

• h₂ 依赖 x₂ 和 h₁

• h₁ 依赖 x₁ 和 h₀

因此,h₃ 实际上包含了 x₁、x₂、x₃ 的历史信息。

这就是 RNN 能够处理序列数据的原因。

四、RNN 的输入、输出与常见任务形式

RNN 可以根据任务需要设计不同的输入输出形式。常见形式包括一对一、一对多、多对一和多对多。

图 4:RNN 的常见输入输出形式

1、一对一:普通非序列任务

一对一形式类似普通前馈网络:

一个输入 → 一个输出

例如:

• 图像分类

• 表格分类

• 单样本回归

这类任务本身不一定需要 RNN。

2、一对多:从一个输入生成一个序列

一对多形式是:

一个输入 → 多个输出

例如:

• 图像生成文字描述

• 给定主题生成文本

• 给定初始条件生成时间序列

这类任务需要模型从一个初始信息开始,逐步生成序列输出。

3、多对一:序列分类

多对一形式是:

多个输入 → 一个输出

例如:

• 文本情感分类

• 用户流失预测

• 时间序列故障判断

• 语音片段分类

在这种任务中,模型读取完整序列后,根据最后的隐藏状态或聚合后的隐藏状态给出一个整体预测。

例如,文本情感分类可以理解为:

词语序列 → RNN → 最终隐藏状态 → 情感类别

4、多对多:序列标注或序列生成

多对多形式是:

多个输入 → 多个输出

例如:

• 词性标注

• 命名实体识别

• 机器翻译

• 语音识别

• 每个时间点的状态预测

如果输入序列和输出序列长度相同,可以在每个时间步输出一个预测结果。

如果输入序列和输出序列长度不同,则通常需要编码器---解码器结构。

五、隐藏状态、记忆能力与局限

RNN 的隐藏状态可以理解为模型对历史信息的压缩表示。

在第 t 个时间步,隐藏状态 hₜ 汇总了当前输入 xₜ 和此前隐藏状态 hₜ₋₁ 中的信息。这样,模型就能在处理后续输入时利用前面的上下文。

从直观角度看:

隐藏状态 = 当前输入 + 历史信息的综合表示

但是,普通 RNN 的记忆能力并不是无限的。

在较长序列中,早期信息需要经过很多时间步才能影响后面的输出。反向传播时,梯度也需要沿时间步不断向前传递。这会带来两个经典问题:

• 梯度消失

• 梯度爆炸

1、梯度消失

梯度消失(Vanishing Gradient)是指梯度在反向传播过程中逐渐变得非常小,导致较早时间步的参数难以有效更新。

在长序列任务中,这意味着模型很难学习远距离依赖。

例如,在句子:

go 复制代码
虽然这部电影前半段节奏很慢,但结尾非常精彩,所以我还是很喜欢

如果模型需要根据句子后面的"很喜欢"判断整体情感,就必须结合前面的转折结构。如果序列很长,普通 RNN 可能难以稳定保留远处信息。

2、梯度爆炸

梯度爆炸(Exploding Gradient)是指梯度在反向传播过程中不断放大,导致参数更新过大,训练不稳定,甚至出现损失变成异常值的情况。

常见缓解方法包括:

• 梯度裁剪

• 更合适的初始化

• 使用 LSTM 或 GRU

• 使用更稳定的优化器

在 PyTorch 中,梯度裁剪常用写法是:

apache 复制代码
torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)

3、普通 RNN 的主要局限

图 5:RNN 的隐藏状态与长距离依赖问题

普通 RNN 的主要局限包括:

• 难以建模长距离依赖

• 训练时可能出现梯度消失或梯度爆炸

• 序列计算难以完全并行

• 对较长文本或复杂上下文任务表现有限

因此,在很多实际任务中,普通 RNN 常被 LSTM、GRU、Transformer 等结构替代或扩展。

六、LSTM 与 GRU:对普通 RNN 的改进

为了解决普通 RNN 难以建模长距离依赖的问题,研究者提出了更复杂的循环结构,其中最经典的是 LSTM 和 GRU。

1、LSTM:加入门控机制与记忆单元

长短期记忆网络(Long Short-Term Memory,LSTM)在普通 RNN 的基础上引入了记忆单元和门控机制。

LSTM 的核心思想是:让模型自己学习哪些信息应该保留,哪些信息应该遗忘,哪些新信息应该写入。

它通常包含三个重要门控:

• 遗忘门:决定保留多少过去信息

• 输入门:决定写入多少当前信息

• 输出门:决定输出多少内部记忆

可以简单理解为:

• 普通 RNN:所有信息都混合在隐藏状态中

• LSTM:通过门控机制管理信息的保留、写入和输出

LSTM 更适合处理较长序列和复杂上下文关系。

2、GRU:更简化的门控循环单元

门控循环单元(Gated Recurrent Unit,GRU)可以看作一种更简化的门控 RNN。它通常包含:

• 更新门

• 重置门

GRU 的结构比 LSTM 更简单,参数更少,训练速度通常更快。在很多任务中,GRU 能取得接近 LSTM 的效果。

3、普通 RNN、LSTM 与 GRU 的关系

可以粗略理解为:

• RNN:基础循环结构,简单但长距离记忆能力有限

• LSTM:加入更完整的门控机制,适合长序列建模

• GRU:结构更简洁的门控循环网络,在效率和效果之间折中

图 6:RNN、LSTM 与 GRU 的结构对比

在学习路径上,普通 RNN 适合帮助理解序列建模的基本思想;LSTM 和 GRU 则是更常见的实用循环网络结构。

七、PyTorch 实现:使用 RNN 进行文本情感分类

下面使用 PyTorch 构建一个简单 RNN,用于演示文本情感分类的基本流程。

图 7:RNN 文本分类的训练与预测流程

为了避免依赖外部大型数据集,示例使用一个小型中文样例数据集。这个示例的目标不是训练高性能模型,而是帮助理解 RNN 的基本输入、隐藏状态和训练闭环。

任务是:根据一句中文短句,判断它是正面情感还是负面情感。

1、导入库并准备数据

javascript 复制代码
import torchimport torch.nn as nnfrom torch.utils.data import Dataset, DataLoader

构造一个简单数据集:

ini 复制代码
# 定义样本数据:(文本, 标签) 其中标签1表示正面,0表示负面samples = [    ("《主角》这部电视连续剧非常好看", 1),    ("演员表演非常精彩", 1),    ("剧情紧凑我很喜欢", 1),    ("这家餐厅味道不错", 1),    ("服务态度很好", 1),    ("这部电影太难看了", 0),    ("剧情拖沓让人失望", 0),    ("演员表演很糟糕", 0),    ("这家餐厅味道很差", 0),    ("服务态度不好", 0),]

为了简化演示,这里按"单字"构建词表。真实任务中通常会使用分词器、子词模型或预训练词向量。

2、构建字符表与编码函数

cs 复制代码
# 构建字符表:将所有文本中的字符去重后排序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_id=0 表示填充符

• encode_text() 把文本转换为编号序列

例如:

bash 复制代码
print(encode_text("电影好看"))

输出可能类似:

cs 复制代码
[33, 22, 16, 34]

3、定义数据集与填充函数

因为不同句子的长度可能不同,所以需要在一个批次中把它们填充到相同长度。

python 复制代码
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, dtype=torch.long), torch.tensor(label, dtype=torch.long)

def collate_fn(batch):    """批处理函数:将多个样本填充到相同长度,并返回长度信息"""    sequences, labels = zip(*batch)             # 分离ID序列和标签
    # 记录每个序列的原始长度    lengths = torch.tensor(        [len(seq) for seq in sequences],        dtype=torch.long    )
    # 使用pad_sequence将序列填充到同一长度(右侧填充),batch_first=True输出形状 (B, T)    padded_sequences = nn.utils.rnn.pad_sequence(        sequences,        batch_first=True,        padding_value=pad_id        # 使用预设的填充符ID (0)    )
    labels = torch.stack(labels)    # 将标签堆叠成一维张量
    return padded_sequences, lengths, labels   # 返回填充后的序列、原始长度、标签

其中:

• Dataset 用于定义样本读取方式

• collate_fn 用于处理一个 batch 中不同长度的序列

• pad_sequence 用于把序列填充到相同长度

• batch_first=True 表示输出形状为 batch_size × sequence_length

构建 DataLoader:

makefile 复制代码
# 创建情感分析数据集实例,传入样本列表dataset = SentimentDataset(samples)
# 创建DataLoader,用于批量加载数据train_loader = DataLoader(    dataset,               # 数据集对象    batch_size=4,          # 每批4个样本    shuffle=True,          # 每个epoch打乱数据顺序    collate_fn=collate_fn  # 自定义批处理函数,负责填充和对齐序列)

4、定义 RNN 文本分类模型

文本输入需要先经过嵌入层(Embedding Layer),把整数编号转换为稠密向量。

python 复制代码
class RNNTextClassifier(nn.Module):    """基于RNN的文本分类器"""    def __init__(self, vocab_size, embed_dim, hidden_size, num_classes):        super().__init__()
        # 词嵌入层:将词ID映射为稠密向量        self.embedding = nn.Embedding(            num_embeddings=vocab_size,   # 词汇表大小            embedding_dim=embed_dim,     # 嵌入维度            padding_idx=pad_id            # 填充词ID不参与梯度更新        )
        # RNN层(简单循环神经网络)        self.rnn = nn.RNN(            input_size=embed_dim,         # 输入特征维度(嵌入维)            hidden_size=hidden_size,      # 隐藏状态维度            batch_first=True              # 输入形状为 (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_classes

5、定义模型、损失函数和优化器

makefile 复制代码
# 词汇表大小:字符ID数(从1开始) + 1(填充符0)vocab_size = len(char_to_id) + 1embed_dim = 16          # 嵌入向量维度hidden_size = 32        # RNN隐藏层维度num_classes = 2         # 二分类(正面/负面)
# 实例化RNN文本分类器model = RNNTextClassifier(    vocab_size=vocab_size,    embed_dim=embed_dim,    hidden_size=hidden_size,    num_classes=num_classes)
# 损失函数:交叉熵(适用于多分类,此处二分类)criterion = nn.CrossEntropyLoss()# 优化器:Adam,学习率0.01optimizer = torch.optim.Adam(model.parameters(), lr=0.01)

其中:

• vocab_size 表示词表大小

• embed_dim 表示字符向量维度

• hidden_size 表示隐藏状态维度

• num_classes=2 表示二分类

• CrossEntropyLoss 用于多类分类,包括二分类的两个类别输出

• Adam 用于更新模型参数

这里模型最后输出的是 logits,不需要手动添加 Softmax。

6、训练模型

python 复制代码
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(f"Epoch [{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、预测新句子

python 复制代码
def predict_sentiment(text):    """预测单条文本的情感倾向(正面/负面)"""    model.eval()          # 切换到推理模式(关闭Dropout等)
    # 将文本编码为ID张量,并添加batch维度(形状: 1, seq_len)    ids = torch.tensor(        encode_text(text),        dtype=torch.long    ).unsqueeze(0)
    # 计算序列长度(此处为全部有效长度,无填充)    lengths = torch.tensor([ids.size(1)])
    # 无梯度环境下前向传播    with torch.no_grad():        logits = model(ids, lengths)          # 输出形状 (1, num_classes)        pred = logits.argmax(dim=1).item()    # 取最大概率的类别索引
    # 将类别索引(0或1)转换为文字    return "正面" if pred == 1 else "负面"

测试预测:

bash 复制代码
print(predict_sentiment("电影非常精彩"))print(predict_sentiment("服务太差了"))

可能输出:

go 复制代码
正面负面

由于这里的数据集非常小,模型只是演示 RNN 的基本流程,不能代表真实中文情感分析系统的效果。真实任务通常需要更大数据集、更可靠的分词方法、更复杂的模型结构,以及验证集和测试集评估。

8、查看张量形状

可以通过打印形状来理解 RNN 的输入输出:

python 复制代码
# 取一个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)

可能看到类似结果:

css 复制代码
输入编号形状: 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 替代,但仍然是理解序列神经网络的重要入口。

图 8:RNN 的适用场景、局限与扩展方向

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 等结构,正是在此基础上进一步改进而来。

"点赞有美意,赞赏是鼓励"

相关推荐
ganbingfenxiang1 分钟前
山西干冰零售
大数据·人工智能·python·零售
薛定猫AI5 分钟前
【深度解析】GLM-5.2 与 Z-Code:AI 编程智能体的原理拆解与 Python 调用实战
开发语言·人工智能·python
weixin_4000056013 分钟前
RL-frenet-trajectory-planning-in-CARLA
人工智能·深度学习·算法·机器学习·自动驾驶
白白白飘17 分钟前
【3】大语言模型的核心原理
人工智能·语言模型·自然语言处理
ZeekerLin33 分钟前
AI时代,企业核心业务中台化建设思考
大数据·人工智能·企业ai转型
刘海东刘海东35 分钟前
完成了一二三四章
人工智能
甲维斯35 分钟前
普通人慎入!Fable5原创游戏“死线求生”第一版!
人工智能·ai编程·游戏开发
北鹤M36 分钟前
ai模特画图高效实现换装,批量生图工具实测体验
人工智能·aigc
蒟蒻的贤37 分钟前
python搭建MiniDL
开发语言·python·深度学习
大C聊AI42 分钟前
通用办公 AI 难以适配招投标专业场景,垂直行业 AI 工具落地实践|智标领航技术选型分析
人工智能·效率提升·ai 工具·招投标·智标领航·ai 辅助办公