005 《循环神经网络 (Recurrent Neural Networks - RNNs) 深度解析与实践》


作者Lou Xiao, gemini创建时间2025-04-22 22:16:10更新时间2025-04-22 22:16:10

🌟🌟🌟本文由Gemini 2.5 Flash Preview 04-17生成,用来辅助学习。🌟🌟🌟

书籍大纲

▮▮ 1. 绪论:序列数据与深度学习的挑战
▮▮▮▮ 1.1 什么是序列数据?
▮▮▮▮ 1.2 序列数据的特性:顺序与依赖
▮▮▮▮ 1.3 传统神经网络处理序列数据的局限性
▮▮▮▮ 1.4 循环神经网络 (RNN) 的概念与优势
▮▮▮▮ 1.5 本书结构与内容概览
▮▮ 2. 循环神经网络 (RNN) 的基础原理
▮▮▮▮ 2.1 RNN 的基本结构与计算图 (Computation Graph)
▮▮▮▮ 2.2 隐藏状态 (Hidden State) 的作用
▮▮▮▮ 2.3 数学模型:前向传播 (Forward Propagation)
▮▮▮▮ 2.4 不同类型的 RNN 架构 (Many-to-One, One-to-Many, Many-to-Many)
▮▮ 3. 训练循环神经网络:反向传播通过时间 (BPTT)
▮▮▮▮ 3.1 损失函数 (Loss Function) 与优化目标
▮▮▮▮ 3.2 BPTT 算法的推导
▮▮▮▮▮▮ 3.2.1 沿着时间反向传播
▮▮▮▮▮▮ 3.2.2 参数更新
▮▮▮▮ 3.3 BPTT 的计算复杂性
▮▮ 4. 标准 RNN 的局限性:梯度消失与梯度爆炸
▮▮▮▮ 4.1 梯度消失问题 (Vanishing Gradient Problem)
▮▮▮▮ 4.2 梯度爆炸问题 (Exploding Gradient Problem)
▮▮▮▮ 4.3 原因分析:激活函数与权重矩阵
▮▮▮▮ 4.4 应对梯度爆炸的方法:梯度裁剪 (Gradient Clipping)
▮▮ 5. 长短期记忆网络 (LSTM)
▮▮▮▮ 5.1 LSTM 的核心思想:引入门控单元
▮▮▮▮ 5.2 LSTM 单元结构详解
▮▮▮▮ 5.3 门控机制的数学表示与工作原理
▮▮▮▮ 5.4 LSTM 的前向传播与反向传播
▮▮▮▮ 5.5 窥视孔连接 (Peephole Connections)
▮▮ 6. 门控循环单元 (GRU)
▮▮▮▮ 6.1 GRU 的核心思想:简化门控机制
▮▮▮▮ 6.2 GRU 单元结构详解
▮▮▮▮ 6.3 门控机制的数学表示与工作原理
▮▮▮▮ 6.4 GRU 与 LSTM 的对比
▮▮ 7. 高级 RNN 架构与变体
▮▮▮▮ 7.1 双向循环神经网络 (Bidirectional RNN - BiRNN)
▮▮▮▮ 7.2 深度循环神经网络 (Deep RNN / Stacked RNN)
▮▮▮▮ 7.3 编码器-解码器模型 (Encoder-Decoder Model)
▮▮▮▮▮▮ 7.3.1 编码器 (Encoder)
▮▮▮▮▮▮ 7.3.2 解码器 (Decoder)
▮▮▮▮ 7.4 注意力机制 (Attention Mechanism)
▮▮▮▮ 7.5 其他 RNN 变体简述
▮▮ 8. 循环神经网络的应用实践
▮▮▮▮ 8.1 自然语言处理 (NLP)
▮▮▮▮▮▮ 8.1.1 语言模型 (Language Modeling)
▮▮▮▮▮▮ 8.1.2 机器翻译 (Machine Translation)
▮▮▮▮▮▮ 8.1.3 文本生成 (Text Generation)
▮▮▮▮▮▮ 8.1.4 情感分析 (Sentiment Analysis)
▮▮▮▮▮▮ 8.1.5 命名实体识别 (Named Entity Recognition - NER)
▮▮▮▮ 8.2 时间序列分析 (Time Series Analysis)
▮▮▮▮▮▮ 8.2.1 时间序列预测 (Time Series Forecasting)
▮▮▮▮▮▮ 8.2.2 异常检测 (Anomaly Detection)
▮▮▮▮ 8.3 语音识别 (Speech Recognition)
▮▮▮▮ 8.4 其他应用领域
▮▮ 9. RNNs 的训练技巧与实践
▮▮▮▮ 9.1 数据预处理 (Data Preprocessing)
▮▮▮▮ 9.2 优化器选择与学习率调度
▮▮▮▮ 9.3 正则化 (Regularization) 技术
▮▮▮▮ 9.4 批量处理 (Batching) 策略
▮▮▮▮ 9.5 模型评估指标
▮▮▮▮ 9.6 使用深度学习框架实现 RNNs
▮▮ 10. RNNs 的最新进展与替代方案
▮▮▮▮ 10.1 Attention 机制的进一步发展
▮▮▮▮ 10.2 Transformer 模型概述
▮▮▮▮ 10.3 卷积神经网络 (CNN) 在序列建模中的应用
▮▮▮▮ 10.4 RNNs 的未来与展望
▮▮ 附录A: 数学基础回顾
▮▮ 附录B: BPTT 梯度计算详细推导
▮▮ 附录C: 常用激活函数及其导数
▮▮ 附录D: RNN 模型实现代码示例
▮▮ 附录E: 超参数调优指南
▮▮ 附录F: 参考文献
▮▮ 附录G: 术语表 (Glossary)


1. 绪论:序列数据与深度学习的挑战

欢迎来到循环神经网络 (Recurrent Neural Networks - RNNs) 的世界!在开启这段深入探索之旅前,让我们先来了解一下本书的起点——序列数据,以及为什么传统的机器学习甚至前馈神经网络在处理这类数据时会遇到挑战。正是这些挑战,催生了 RNN 这种强大的模型架构。我们将从现实世界中无处不在的序列数据谈起,分析其独有特性,再剖析传统方法的不足,最终自然而然地引出 RNN 的核心思想,并简要介绍本书的整体结构,为您规划好接下来的学习路径。🚀

1.1 什么是序列数据?

在数字世界中,数据并非总是孤立存在的点。很多时候,数据是按照特定的顺序排列的,而且一个数据点往往与其前后的数据点存在紧密的关联。这类数据,我们称之为序列数据(Sequence Data)

序列数据最显著的特点在于其元素的排列顺序至关重要。改变元素的顺序,可能会彻底改变数据的含义。

让我们看几个例子:

自然语言(Natural Language)
▮▮▮▮这可能是最直观的序列数据例子。一个句子是由一系列词语组成的,词语的顺序决定了句子的语法和语义。
▮▮▮▮例如,“我 爱 机器学习”与“机器学习 爱 我”表达的意思截然不同。词语在序列中的位置(时步)包含了关键信息。
▮▮▮▮一篇文章、一本书,本质上都是更长的词语或字符序列。

时间序列数据(Time Series Data)
▮▮▮▮这类数据按照时间顺序收集。例如:
▮▮▮▮▮▮▮▮❶ 股票价格随时间波动的数据。
▮▮▮▮▮▮▮▮❷ 气温、湿度等气象数据随时间变化的数据。
▮▮▮▮▮▮▮▮❸ 用户的网络浏览记录(点击流数据)。
▮▮▮▮在时间序列中,当前的数据点往往与过去的数据点紧密相关(如今天的气温与昨天的气温)。预测未来的数据点需要理解历史趋势和模式。

音频信号(Audio Signals)
▮▮▮▮一段音频可以看作是声音信号强度随时间变化的序列。构成声音的声波是连续的,采样后形成离散的时间点序列。语音识别、音乐生成等任务都涉及处理音频序列。

视频数据(Video Data)
▮▮▮▮视频是一系列连续的图像帧。每一帧都与前一帧和后一帧高度相关。理解视频内容需要分析帧之间的时序关系和运动信息。

基因序列(Genetic Sequences)
▮▮▮▮DNA、RNA 是由核苷酸(A, T/U, C, G)组成的序列。核苷酸的排列顺序决定了基因的功能。分析基因序列需要识别序列中的模式和结构。

传感器数据(Sensor Data)
▮▮▮▮物联网设备、工业生产线上的传感器持续产生数据,形成时间序列。分析这些数据可以用于故障预测、状态监控等。

从这些例子可以看出,序列数据在我们的世界中无处不在,对它们的有效处理是人工智能和数据科学领域许多核心任务的基础。

1.2 序列数据的特性:顺序与依赖

处理序列数据之所以具有挑战性,根本原因在于其固有的两个关键特性:顺序性(Order)依赖性(Dependency)

顺序性(Order)
▮▮▮▮序列数据的元素是按照特定的先后次序排列的。这种顺序本身就携带着信息。
▮▮▮▮例如,在自然语言中,“前”和“后”这两个词语的位置决定了时间顺序;在时间序列中,数据的采集时间决定了它们的排列。
▮▮▮▮传统的机器学习模型,如逻辑回归或支持向量机 (Support Vector Machine - SVM),通常假设输入数据是独立同分布的(Independent and Identically Distributed - I.I.D.),忽略了数据之间的顺序关系。

依赖性(Dependency)
▮▮▮▮序列中的一个元素往往与其前面(有时也与后面)的元素存在依赖关系。这种依赖可能是短期的,也可能是长期的。
▮▮▮▮例如,在句子中,一个词的含义可能取决于前面的几个词;在时间序列中,今天的股票价格可能受到昨天乃至上周价格的影响。
▮▮▮▮这种依赖关系使得序列数据不能被简单地视为孤立的数据点集合。我们需要能够捕捉并建模这些时序依赖(Temporal Dependencies)。

理解了序列数据的这两个特性,我们就能够更好地理解为什么传统的机器学习方法在处理这类数据时会显得力不从心。

1.3 传统神经网络处理序列数据的局限性

传统的前馈神经网络 (Feedforward Neural Network),例如多层感知机 (Multilayer Perceptron - MLP),在处理非序列数据(如图像分类、结构化数据分类)方面取得了巨大成功。然而,当面对序列数据时,它们暴露出明显的局限性:

输入/输出长度固定(Fixed Input/Output Size)
▮▮▮▮标准的前馈神经网络通常要求输入层具有固定的尺寸,输出层也通常是固定的。
▮▮▮▮而序列数据,如句子或时间序列,其长度往往是可变的。例如,不同句子的词数不同,不同音频片段的时长不同。
▮▮▮▮为了将变长序列输入到前馈网络,我们可能需要进行填充 (Padding) 或截断 (Truncating),但这可能会引入噪音或丢失信息。对于输出也是变长序列的任务(如机器翻译、文本生成),前馈网络更是难以直接处理。

无法共享参数(No Parameter Sharing Across Time)
▮▮▮▮前馈网络的每一层中的神经元都有独立的权重。当处理序列数据时,如果将整个序列展平作为输入(比如将一个句子所有词的向量拼接起来),则网络无法学习到在不同时间步(序列位置)上共享的特征提取器。
▮▮▮▮例如,判断一个词在句子中的词性,无论这个词出现在句子的开头、中间还是结尾,我们都希望使用相同的规则或特征提取方法。前馈网络难以自然地实现这种参数共享。

无法捕捉时序依赖(Cannot Capture Temporal Dependencies)
▮▮▮▮前馈网络的计算是“无记忆”的。对于每一个输入,其计算过程是独立的,不受之前输入的任何影响。
▮▮▮▮这意味着前馈网络无法记住序列中过去的信息,也无法利用这些信息来理解当前的数据点或预测未来的数据点。它无法建立元素之间的时序联系。例如,在前馈网络看来,"我 爱 机器学习" 和 "机器学习 爱 我" 经过词向量表示并简单拼接后,可能看不出本质区别,因为它没有考虑词语出现的顺序。

难以处理长期依赖(Difficulty with Long-Term Dependencies)
▮▮▮▮即使强行将序列输入到前馈网络(例如,通过滑动窗口),网络也难以捕捉序列中相隔较远的元素之间的依赖关系。依赖的距离越长,前馈网络越难学习到这种关系。

这些局限性表明,我们需要一种全新的神经网络架构,能够专门为处理序列数据而设计,能够有效地捕捉其顺序性和依赖性。

1.4 循环神经网络 (RNN) 的概念与优势

为了克服前馈神经网络在处理序列数据上的不足,循环神经网络(Recurrent Neural Network - RNN)应运而生。其核心思想是:引入“记忆”或“状态”

不同于前馈网络中层与层之间的单向连接,RNN 引入了指向自身的“循环”连接。这种循环连接允许信息在网络内部随时间步传递。具体来说,RNN 在处理序列中的当前元素时,不仅考虑当前的输入,还会考虑网络在处理前一个元素时产生的隐藏状态(Hidden State)记忆(Memory)。这个隐藏状态就像一个压缩的历史信息摘要,它包含了网络对前面序列元素的理解。

每一次处理一个新的序列元素(例如,句子中的下一个词),RNN 单元会同时接收当前的输入和前一个时间步的隐藏状态,然后产生当前的输出以及一个新的、更新后的隐藏状态。这个新的隐藏状态会被传递给下一个时间步。通过这种方式,信息可以在序列中流动和积累。

RNN 的主要优势在于:

能够处理变长序列(Handles Variable-Length Sequences)
▮▮▮▮RNN 通过在每个时间步处理一个序列元素,可以自然地适应不同长度的输入序列。

参数共享(Parameter Sharing)
▮▮▮▮RNN 在所有时间步共享相同的循环权重和偏置。这意味着网络学习到的特征提取或状态更新机制是通用的,可以应用于序列中的任何位置,这符合我们对序列规律的直观认识(例如,词语语法规则与位置无关)。

能够捕捉时序依赖(Captures Temporal Dependencies)
▮▮榜榜通过隐藏状态传递历史信息,RNN 可以建立序列元素之间的依赖关系,从而理解上下文。

适用于序列到序列任务(Suitable for Sequence-to-Sequence Tasks)
▮▮榜榜通过编码器-解码器结构等,RNN 可以将一个序列(如英文句子)映射到另一个序列(如中文句子),非常适合机器翻译等任务。

简而言之,RNN 通过在内部引入一个随时间演变的“状态”或“记忆”,赋予了神经网络处理序列信息的能力。这使得它成为处理自然语言、时间序列等序列数据的强大工具。

1.5 本书结构与内容概览

本书旨在提供一个全面、系统、深入的循环神经网络学习路径,无论您是初学者、有一定基础的开发者,还是希望深入理解理论的专家,都能从中获益。本书的结构安排如下:

第一部分:基础理论 (Chapters 1-4)
▮▮榜榜涵盖序列数据特性、传统方法局限性、标准 RNN 的基本原理、结构、前向传播、以及核心的训练算法——反向传播通过时间 (BPTT)。
▮▮榜榜深入分析标准 RNN 面临的关键问题:梯度消失和梯度爆炸,为后续改进模型做铺垫。

第二部分:改进模型 (Chapters 5-6)
▮▮榜榜详细讲解解决长期依赖问题的两大里程碑式模型:长短期记忆网络 (LSTM) 和门控循环单元 (GRU)。阐述它们的设计理念、门控机制及其如何缓解梯度问题。

第三部分:高级架构 (Chapter 7)
▮▮榜榜介绍在基础 RNN 之上构建的更复杂、更强大的架构,如双向 RNN、深度 RNN、以及重要的编码器-解码器模型及其与注意力机制 (Attention Mechanism) 的结合。

第四部分:应用与实践 (Chapters 8-9)
▮▮榜榜通过丰富的案例,展示 RNNs 在自然语言处理(语言模型、机器翻译、文本生成、情感分析、NER)、时间序列分析(预测、异常检测)、语音识别等领域的应用。
▮▮榜榜提供实用的训练技巧和实践经验,包括数据预处理、优化器、正则化、批量处理、评估指标以及如何使用主流深度学习框架实现 RNNs。

第五部分:最新进展与展望 (Chapter 10)
▮▮榜榜探讨 Attention 机制的进一步发展以及更先进的序列模型(如 Transformer),对比 RNNs 与这些新模型的优劣,并展望 RNNs 技术未来的发展方向和与其他技术的融合。

附录 (Appendices A-G)
▮▮榜榜提供必要的数学基础回顾、详细的梯度计算推导、常用激活函数、代码示例、超参数调优指南、参考文献和术语表,作为正文内容的补充和参考。

通过这个结构,本书将带您从序列数据的基本概念出发,逐步深入到 RNN 的核心机制、训练算法,掌握 LSTM 和 GRU 等重要变体,了解高级架构和广泛应用,最终触及最新的发展前沿。希望本书能帮助您全面理解 RNNs,并能自信地将其应用于实际问题中。

2. 循环神经网络 (RNN) 的基础原理

2.1 RNN 的基本结构与计算图 (Computation Graph)

欢迎来到本书的第二章!在本章中,我们将深入探索循环神经网络(Recurrent Neural Networks - RNNs)的核心构造。与传统的前馈神经网络(Feedforward Neural Networks - FFNs)不同,RNNs 的设计天生就适用于处理序列数据。这是因为它们引入了一个非常关键的机制:循环连接 (Recurrent Connection),允许信息在时间步之间传递。

2.1.1 单个 RNN 单元的结构

想象一个基本的 RNN 单元,它可以接收当前的输入,并且还接收来自上一个时间步的信息。
⚝ 输入 (Input): 在时间步 \(t\),单元接收当前的输入向量 \(x_t\)。这个输入可以是词向量、时间序列中的一个数值、或者其他任何形式的特征向量。
⚝ 隐藏状态 (Hidden State): 这是 RNN 的“记忆”或“状态”。在时间步 \(t\),单元会计算一个新的隐藏状态 \(h_t\)。这个 \(h_t\) 的计算不仅依赖于当前的输入 \(x_t\),还依赖于上一个时间步的隐藏状态 \(h_{t-1}\)。
⚝ 输出 (Output): 在时间步 \(t\),单元可以产生一个输出向量 \(y_t\)。这个输出通常基于当前的隐藏状态 \(h_t\)。

可以将单个 RNN 单元想象成一个带有内部状态的黑箱,这个状态会随着时间步的输入而更新。其核心是一个循环连接,将当前时间步计算出的隐藏状态传递给下一个时间步作为输入的一部分。

2.1.2 计算图的展开 (Unfolding Computation Graph)

循环连接使得网络看起来像是在同一个单元上重复执行计算。为了更好地理解信息流和梯度传播,我们通常会将 RNN 的计算图沿时间轴展开 (Unfold)

假设我们有一个长度为 \(T\) 的输入序列 \(x_1, x_2, \dots, x_T\)。将 RNN 展开后,它看起来就像一个非常深的前馈神经网络,其中每一层对应于序列中的一个时间步,并且所有这些“层”共享相同的权重参数。

在一个展开的计算图中:
① 在时间步 1,网络接收输入 \(x_1\) 和一个初始的隐藏状态 \(h_0\)(通常初始化为零向量或一个可学习的参数)。计算得到 \(h_1\) 和可选的输出 \(y_1\)。
② 在时间步 2,网络接收输入 \(x_2\) 和时间步 1 的隐藏状态 \(h_1\)。计算得到 \(h_2\) 和可选的输出 \(y_2\)。
③ 这个过程一直持续到时间步 \(T\),网络接收 \(x_T\) 和 \(h_{T-1}\),计算得到 \(h_T\) 和可选的输出 \(y_T\)。

下图(此处无法直接绘制,请读者自行想象或参考附录中的图示)展示了 RNN 展开的计算图:
一个圆圈代表一个 RNN 单元。
方块代表输入 (\(x_t\)) 和输出 (\(y_t\))。
箭头代表信息流的方向。
从 \(h_{t-1}\) 到 \(h_t\) 的箭头代表了循环连接在展开图中的体现。

关键点: 尽管展开后有 \(T\) 个单元,它们实际上是同一个 RNN 单元在不同时间步的实例化。这意味着用于计算 \(h_t\) 和 \(y_t\) 的权重矩阵和偏置向量在所有时间步都是共享 (Shared) 的。这种权重共享是 RNN 的核心特性之一,它使得网络能够处理变长序列,并且在学习时序依赖性时更有效率。

通过计算图的展开,我们可以清晰地看到信息是如何一步一步向前传递,隐藏状态是如何累积历史信息,以及网络参数是如何被多个时间步的计算所共享的。这为理解 RNN 的前向传播和后续的训练算法(反向传播通过时间 - BPTT)奠定了基础。

2.2 隐藏状态 (Hidden State) 的作用

隐藏状态 \(h_t\) 是循环神经网络的核心所在,它赋予了 RNN 处理序列数据和捕捉序列中依赖关系的能力。我们可以将隐藏状态形象地理解为网络的“记忆”或“上下文向量”。

以下是隐藏状态的关键作用:

存储历史信息 (Storing Historical Information): 在每个时间步 \(t\),隐藏状态 \(h_t\) 是基于当前输入 \(x_t\) 和上一个时间步的隐藏状态 \(h_{t-1}\) 计算得出的。这意味着 \(h_t\) 编码了从序列开始到时间步 \(t\) 为止的所有输入信息(至少是理论上如此)。随着序列的进行,隐藏状态会不断地更新和积累信息,从而在处理当前时间步的输入时,网络能够参考到之前的上下文。

捕捉时序依赖 (Capturing Temporal Dependencies): 序列数据中的元素往往不是独立的,它们之间存在先后顺序和相互影响的关系(例如,自然语言中的词语顺序决定句子的含义,时间序列中前一个时刻的数值会影响后一个时刻的数值)。隐藏状态通过将前一个时间步的状态融入当前时间步的计算,使得网络能够建模这些时序依赖关系。网络学习到的权重参数会决定如何将旧的状态信息与新的输入结合,从而有效地捕捉到序列中的模式和依赖。

影响当前及未来的计算 (Influencing Current and Future Calculations):
▮▮▮▮⚝ 当前时间步的输出 \(y_t\) 通常是基于当前的隐藏状态 \(h_t\) 计算得出的。因此,隐藏状态中包含的历史信息直接影响了当前时刻网络的预测或决策。
▮▮▮▮⚝ 当前时间步计算出的隐藏状态 \(h_t\) 会被传递给下一个时间步 \(t+1\),成为计算 \(h_{t+1}\) 的一部分。这样,当前的信息又会影响到未来时间步的状态更新和输出计算。

简而言之,隐藏状态就像一个“记忆带”,每一步都在上面刻录新的信息,同时读取上一刻的信息。网络通过学习如何有效地更新和利用这个记忆带上的信息,来理解和处理整个序列。一个设计良好的隐藏状态能够有效地压缩并保留序列中的关键信息,即使输入序列很长,也能记住早期出现的但对当前任务重要的信息。然而,标准的 RNN 在处理长序列时,隐藏状态很难有效地保留远距离的历史信息,这导致了著名的“长期依赖问题 (Long-Term Dependencies Problem)”,我们将在后续章节详细讨论。

2.3 数学模型:前向传播 (Forward Propagation)

理解 RNN 工作原理的关键在于掌握其数学模型。在前向传播阶段,我们根据当前的输入和上一个时间步的隐藏状态来计算当前的隐藏状态和输出。对于一个标准的(或称为 Vanilla)RNN 单元,其计算过程相对简单,并且在所有时间步共享同一组权重矩阵和偏置向量。

假设我们的 RNN 单元有以下参数:
⚝ \(W_{xh}\): 输入层到隐藏层的权重矩阵 (Input-to-Hidden Weight Matrix)。
⚝ \(W_{hh}\): 隐藏层到隐藏层的权重矩阵 (Hidden-to-Hidden Weight Matrix),也称为循环权重矩阵 (Recurrent Weight Matrix)。
⚝ \(b_h\): 隐藏层的偏置向量 (Hidden Bias Vector)。
⚝ \(W_{hy}\): 隐藏层到输出层的权重矩阵 (Hidden-to-Output Weight Matrix)。
⚝ \(b_y\): 输出层的偏置向量 (Output Bias Vector)。

在时间步 \(t\),给定输入向量 \(x_t\) 和上一个时间步的隐藏状态向量 \(h_{t-1}\),当前时间步的隐藏状态 \(h_t\) 和输出向量 \(y_t\) 的计算公式如下:

1. 计算当前隐藏状态 \(h_t\):
首先,我们将当前输入 \(x_t\) 通过 \(W_{xh}\) 线性变换,并与上一个隐藏状态 \(h_{t-1}\) 通过 \(W_{hh}\) 线性变换的结果相加,再加上偏置 \(b_h\)。然后,将结果通过一个激活函数 \(f\)(常用的有 Tanh 或 ReLU)进行非线性变换,得到当前的隐藏状态 \(h_t\)。
\[ h_t = f(W_{hh} h_{t-1} + W_{xh} x_t + b_h) \]
▮▮▮▮⚝ \(h_t\): 时间步 \(t\) 的隐藏状态向量。
▮▮▮▮⚝ \(h_{t-1}\): 时间步 \(t-1\) 的隐藏状态向量。对于第一个时间步 \(t=1\),\(h_0\) 是一个初始状态向量,通常设置为零向量或一个可学习的参数。
▮▮▮▮⚝ \(x_t\): 时间步 \(t\) 的输入向量。
▮▮▮▮⚝ \(W_{hh}\), \(W_{xh}\): 权重矩阵,它们在所有时间步共享。
▮▮▮▮⚝ \(b_h\): 偏置向量,在所有时间步共享。
▮▮▮▮⚝ \(f\): 激活函数,如 Tanh (\(\text{tanh}\)) 或 ReLU (\(\text{ReLU}\))。Tanh 是标准 RNN 中常用的选择,因为它将输出值限制在 \([-1, 1]\) 之间。

2. 计算当前输出 \(y_t\):
然后,我们将当前的隐藏状态 \(h_t\) 通过 \(W_{hy}\) 线性变换,并加上偏置 \(b_y\)。接着,将结果通过一个激活函数 \(g\) 得到最终的输出 \(y_t\)。激活函数 \(g\) 的选择取决于具体的任务。例如,对于分类任务,可以使用 Softmax 函数来得到类别概率;对于回归任务,可以使用线性激活函数(即不做非线性变换)。
\[ y_t = g(W_{hy} h_t + b_y) \]
▮▮▮▮⚝ \(y_t\): 时间步 \(t\) 的输出向量。
▮▮▮▮⚝ \(h_t\): 时间步 \(t\) 的隐藏状态向量。
▮▮▮▮⚝ \(W_{hy}\): 权重矩阵,在所有时间步共享。
▮▮▮▮⚝ \(b_y\): 偏置向量,在所有时间步共享。
▮▮▮▮⚝ \(g\): 激活函数,如 Softmax、Sigmoid 或线性激活。

前向传播过程 (Forward Propagation Process):

整个前向传播过程就是按照时间顺序,从 \(t=1\) 到 \(T\) 依次执行上述计算:
① 初始化 \(h_0\)。
② For \(t = 1, 2, \dots, T\):
▮▮▮▮ⓒ 计算 \(h_t = f(W_{hh} h_{t-1} + W_{xh} x_t + b_h)\)
▮▮▮▮ⓓ 计算 \(y_t = g(W_{hy} h_t + b_y)\)
⑤ 最终得到输出序列 \(y_1, y_2, \dots, y_T\)(或者根据任务需要只取最后一个输出 \(y_T\),或者只使用隐藏状态 \(h_T\))。

这个数学模型清晰地展示了 RNN 如何在每个时间步接收新输入,更新其内部状态(隐藏状态),并基于当前状态产生输出。隐藏状态的更新公式尤其重要,它体现了当前状态如何结合历史信息(\(h_{t-1}\))和当前输入(\(x_t\))。

2.4 不同类型的 RNN 架构 (Many-to-One, One-to-Many, Many-to-Many)

尽管核心的 RNN 单元是相同的,但根据输入序列和输出序列的模式,我们可以构建不同类型的 RNN 架构来适应各种序列任务。这些架构通常被称为序列模式 (Sequence Patterns)RNN 架构类型 (RNN Architecture Types)。以下是几种常见的类型:

2.4.1 一对多 (One-to-Many)

模式描述: 输入是一个单一的数据点,但输出是一个序列。
结构: 网络接收一个输入 \(x\),然后基于这个输入和一个初始隐藏状态 \(h_0\) 生成第一个输出 \(y_1\) 和第一个隐藏状态 \(h_1\)。接着,将 \(h_1\) 作为输入的一部分(或者只使用 \(h_1\)),并结合 \(y_1\)(通常将其嵌入成向量作为下一个时间步的“输入”)来生成 \(y_2\) 和 \(h_2\),以此类推,直到生成完整的输出序列。在某些变体中,第一个输入只用于产生初始隐藏状态,后续时间步的输入都是前一个时间步的输出(或其嵌入)。
应用示例:
▮▮▮▮⚝ 图像标注 (Image Captioning): 输入是一张图片(通过 CNN 等处理后得到一个特征向量),输出是描述这张图片的文字序列。
▮▮▮▮⚝ 音乐生成 (Music Generation): 输入可能是一个种子音符、风格或情绪,输出是一个音符序列构成乐曲。
▮▮▮▮⚝ 视频生成 (Video Generation): 输入是一个初始帧或描述,输出是一个视频帧序列。

2.4.2 多对一 (Many-to-One)

模式描述: 输入是一个序列,但输出是一个单一的数据点(通常在序列的最后一个时间步产生)。
结构: 网络按顺序处理输入序列 \(x_1, x_2, \dots, x_T\)。在处理完最后一个输入 \(x_T\) 并计算出最终的隐藏状态 \(h_T\) 后,只使用 \(h_T\) 来产生最终的输出 \(y\)。
应用示例:
▮▮▮▮⚝ 情感分析 (Sentiment Analysis): 输入是一句文本或一段评论(词序列),输出是表示情感(如积极、消极)的单一标签。
▮▮▮▮⚝ 文本分类 (Text Classification): 输入是一篇文档(词序列),输出是文档所属的类别。
▮▮▮▮⚝ 垃圾邮件检测 (Spam Detection): 输入是一封邮件的文本(词序列),输出是一个二元判断(是垃圾邮件/不是垃圾邮件)。

2.4.3 多对多 (Many-to-Many)

模式描述: 输入是一个序列,输出也是一个序列。这种模式又可以进一步细分为两种子类型:

子类型 1: 同步多对多 (Synchronous Many-to-Many)
模式描述: 输入序列的每个元素对应一个输出序列的元素。输入和输出的长度通常是相同的。
结构: 网络在处理输入序列 \(x_1, x_2, \dots, x_T\) 的每个时间步 \(t\) 时,都会产生一个对应的输出 \(y_t\)。
应用示例:
▮▮▮▮⚝ 词性标注 (Part-of-Speech Tagging): 输入是一个单词序列,输出是每个单词对应的词性标签序列。
▮▮▮▮⚝ 命名实体识别 (Named Entity Recognition - NER): 输入是一个单词序列,输出是每个单词是否是命名实体及其类型的标签序列。
▮▮▮▮⚝ 视频帧分类 (Video Frame Classification): 输入是视频帧序列,输出是每个帧对应的分类标签序列。

子类型 2: 异步多对多 (Asynchronous Many-to-Many / Encoder-Decoder)
模式描述: 输入序列和输出序列的长度可能不同,并且输出是在处理完整个输入序列后才开始生成的。这种架构通常包含一个编码器 (Encoder) 和一个解码器 (Decoder)
结构:
▮▮▮▮ⓐ 编码器 (Encoder): 一个 RNN 读取整个输入序列 \(x_1, x_2, \dots, x_T\),并将序列的信息压缩到一个固定长度的向量中,通常是最后一个隐藏状态 \(h_T\)。这个向量可以视为输入序列的“上下文向量 (Context Vector)”。
▮▮▮▮ⓑ 解码器 (Decoder): 另一个 RNN(或同一个 RNN 的另一个实例)接收编码器输出的上下文向量作为其初始隐藏状态(或其他形式的初始输入),然后开始生成输出序列 \(y_1, y_2, \dots, y_{T'}\)。在生成每个输出 \(y_t\) 时,解码器通常也会将上一步生成的输出 \(y_{t-1}\)(或其嵌入)作为当前时间步的输入。
应用示例:
▮▮▮▮⚝ 机器翻译 (Machine Translation): 输入是源语言句子序列,输出是目标语言句子序列。
▮▮▮▮⚝ 文本摘要 (Text Summarization): 输入是原文序列,输出是摘要序列。
▮▮▮▮⚝ 语音识别 (Speech Recognition): 输入是语音信号序列(声学特征),输出是文字序列。

这些不同的架构类型展示了 RNN 强大的建模能力,使其能够处理各种各样的序列到序列、序列到单个输出、或单个输入到序列的任务。在实际应用中,我们选择合适的架构取决于待解决问题的输入输出形式。

3. 训练循环神经网络:反向传播通过时间 (BPTT)

在本章中,我们将深入探讨如何训练循环神经网络 (RNNs)。与前馈神经网络 (Feedforward Neural Networks) 类似,训练 RNN 的核心也是使用反向传播 (Backpropagation) 算法来计算模型参数的梯度,并利用这些梯度通过优化算法(如梯度下降)来更新参数,以最小化损失函数 (Loss Function)。然而,由于 RNN 在时间维度上共享参数并具有循环连接,其反向传播过程需要考虑时间步之间的依赖关系,因此被称为反向传播通过时间 (Backpropagation Through Time - BPTT)。本章将详细讲解 BPTT 算法的原理、推导过程以及其计算特点和面临的挑战。

3.1 损失函数 (Loss Function) 与优化目标

在训练神经网络时,损失函数 (Loss Function) 的作用是衡量模型预测输出与真实目标之间的差异。我们的优化目标 (Optimization Objective) 就是找到一组模型参数,使得在整个训练数据集上的总损失最小化。对于序列任务,常见的损失函数取决于具体的任务类型。

① 分类任务:例如语言模型 (Language Modeling) 中预测序列的下一个词,或者对文本进行情感分类 (Sentiment Analysis)。
▮▮▮▮⚝ 交叉熵损失 (Cross-Entropy Loss):这是分类任务中最常用的损失函数。对于一个长度为 \( T \) 的序列,假设在时间步 \( t \) 的真实目标是独热编码向量 \( y_t \),模型输出的在各个类别上的概率分布是 \( \hat{y}_t \),则在时间步 \( t \) 的损失 \( L_t \) 通常定义为:
\[ L_t = - \sum_{i} y_{t,i} \log(\hat{y}_{t,i}) \]
其中 \( i \) 遍历所有可能的类别。对于独热编码的目标 \( y_t \),这个公式可以简化为 \( L_t = - \log(\hat{y}_{t,c}) \),其中 \( c \) 是真实类别的索引。
▮▮▮▮模型的总损失 \( L \) 是各个时间步损失的累加(或平均):
\[ L = \sum_{t=1}^T L_t \]
我们的目标是最小化这个总损失 \( L \)。

② 回归任务:例如时间序列预测 (Time Series Forecasting) 中预测股票价格或传感器读数等连续数值。
▮▮▮▮⚝ 均方误差 (Mean Squared Error - MSE):这是回归任务中常用的损失函数。对于时间步 \( t \),如果真实目标是 \( y_t \)(一个数值或向量),模型输出是 \( \hat{y}_t \),则在时间步 \( t \) 的损失 \( L_t \) 通常定义为:
\[ L_t = \frac{1}{N} \sum_{i=1}^N (y_{t,i} - \hat{y}_{t,i})^2 \]
其中 \( N \) 是输出向量的维度。
▮▮▮▮同样,模型的总损失 \( L \) 是各个时间步损失的累加(或平均):
\[ L = \sum_{t=1}^T L_t \]
我们需要最小化这个总损失 \( L \)。

总而言之,无论使用哪种损失函数,我们的核心任务都是计算总损失 \( L \) 关于模型参数(权重矩阵 \( W \) 和偏置向量 \( b \),如 \( W_{hh}, W_{xh}, W_{hy}, b_h, b_y \))的梯度 \( \nabla L \),然后使用梯度下降或其变体(如 Adam, RMSprop)来迭代更新参数:
\[ \theta \leftarrow \theta - \alpha \nabla L(\theta) \]
其中 \( \theta \) 代表模型参数集合,\( \alpha \) 是学习率 (Learning Rate)。

3.2 BPTT 算法的推导

反向传播 (Backpropagation) 的本质是利用链式法则 (Chain Rule) 来计算损失函数相对于模型参数的梯度。对于前馈神经网络,计算图是无环的,梯度计算相对直接。然而,RNN 的计算图在时间维度上是展开的,形成一个有向无环图 (Directed Acyclic Graph),但由于参数在所有时间步共享,这种共享引入了跨时间步的依赖,使得梯度计算需要特别处理,即 BPTT。

考虑一个简单的标准 RNN 单元的前向传播 (Forward Propagation) 过程:
在时间步 \( t \):
隐藏状态:\( h_t = f(W_{hh} h_{t-1} + W_{xh} x_t + b_h) \)
输出:\( y_t = g(W_{hy} h_t + b_y) \)
其中 \( x_t \) 是时间步 \( t \) 的输入,\( h_{t-1} \) 是时间步 \( t-1 \) 的隐藏状态,\( f \) 和 \( g \) 是激活函数 (Activation Function),\( W_{hh}, W_{xh}, W_{hy} \) 是权重矩阵,\( b_h, b_y \) 是偏置向量。请注意,\( W_{hh}, W_{xh}, W_{hy}, b_h, b_y \) 在所有时间步都是共享的。

训练时,我们通常有一个输入序列 \( x_1, x_2, \dots, x_T \) 和对应的目标输出序列 \( y_1^*, y_2^*, \dots, y_T^* \)。模型在时间步 \( t \) 产生输出 \( y_t \),并计算损失 \( L_t = \text{Loss}(y_t, y_t^*) \)。总损失为 \( L = \sum_{t=1}^T L_t \)。我们的目标是计算 \( \frac{\partial L}{\partial W_{hh}}, \frac{\partial L}{\partial W_{xh}}, \frac{\partial L}{\partial W_{hy}}, \frac{\partial L}{\partial b_h}, \frac{\partial L}{\partial b_y} \)。

以 \( W_{hh} \) 为例。由于 \( W_{hh} \) 在每个时间步 \( t \) 的计算 \( W_{hh} h_{t-1} \) 中都被使用,对总损失 \( L \) 的贡献来自所有时间步。根据链式法则,\( \frac{\partial L}{\partial W_{hh}} \) 是 \( W_{hh} \) 在每个时间步对总损失贡献的累加:
\[ \frac{\partial L}{\partial W_{hh}} = \sum_{t=1}^T \frac{\partial L}{\partial W_{hh}}|_{t} \]
其中 \( \frac{\partial L}{\partial W_{hh}}|_{t} \) 表示 \( W_{hh} \) 在时间步 \( t \) 通过隐藏状态 \( h_t, h_{t+1}, \dots, h_T \) 和输出 \( y_t, y_{t+1}, \dots, y_T \) 对总损失 \( L \) 的影响。

关键在于理解 \( \frac{\partial L}{\partial h_t} \) 如何计算,因为它影响了所有依赖于 \( h_t \) 的后续计算,并最终影响 \( L \)。

3.2.1 沿着时间反向传播

我们从最后一个时间步 \( T \) 开始反向传播。
在时间步 \( T \),总损失 \( L \) 只直接依赖于 \( y_T \) 和 \( h_T \)。
首先,计算 \( \frac{\partial L}{\partial y_T} \),这取决于所使用的损失函数 \( L_T \)。
然后,通过链式法则计算 \( \frac{\partial L}{\partial h_T} \):
\[ \frac{\partial L}{\partial h_T} = \frac{\partial L}{\partial y_T} \frac{\partial y_T}{\partial h_T} \]
其中 \( \frac{\partial y_T}{\partial h_T} = \frac{\partial g(W_{hy} h_T + b_y)}{\partial h_T} = \text{diag}(g'(W_{hy} h_T + b_y)) W_{hy}^T \)。

现在考虑时间步 \( t < T \)。总损失 \( L \) 依赖于 \( y_t \)(如果存在输出)以及通过 \( h_t \) 影响的后续隐藏状态 \( h_{t+1} \) 及其后续计算。因此,\( \frac{\partial L}{\partial h_t} \) 的计算需要考虑这两条“路径”:一条通向当前时间步的输出 \( y_t \),另一条通向下一个时间步的隐藏状态 \( h_{t+1} \)。
\[ \frac{\partial L}{\partial h_t} = \frac{\partial L}{\partial y_t} \frac{\partial y_t}{\partial h_t} + \frac{\partial L}{\partial h_{t+1}} \frac{\partial h_{t+1}}{\partial h_t} \]
这是一个递归关系!它表明当前时间步的梯度 \( \frac{\partial L}{\partial h_t} \) 来自于:
① 当前时间步的输出 \( y_t \) 对总损失的贡献,反传到 \( h_t \)。
② 下一个时间步的隐藏状态 \( h_{t+1} \) 对总损失的贡献,反传到 \( h_t \)。

其中的项可以通过链式法则展开:
⚝ \( \frac{\partial L}{\partial y_t} \) 取决于时间步 \( t \) 的损失函数 \( L_t \),如果时间步 \( t \) 没有输出,则此项为 0。
⚝ \( \frac{\partial y_t}{\partial h_t} = \frac{\partial g(W_{hy} h_t + b_y)}{\partial h_t} = \text{diag}(g'(W_{hy} h_t + b_y)) W_{hy}^T \)
⚝ \( \frac{\partial h_{t+1}}{\partial h_t} = \frac{\partial f(W_{hh} h_t + W_{xh} x_{t+1} + b_h)}{\partial h_t} = \text{diag}(f'(W_{hh} h_t + W_{xh} x_{t+1} + b_h)) W_{hh}^T \)

将这些代入递归公式,我们得到:
\[ \frac{\partial L}{\partial h_t} = \frac{\partial L}{\partial y_t} \frac{\partial y_t}{\partial h_t} + \frac{\partial L}{\partial h_{t+1}} \text{diag}(f'(W_{hh} h_t + W_{xh} x_{t+1} + b_h)) W_{hh}^T \]
或者更简洁地表示(将 \( \frac{\partial L}{\partial y_t} \frac{\partial y_t}{\partial h_t} \) 记作 \( \delta_t^{out} \),将 \( \text{diag}(f'(\dots)) \) 记作 \( \text{diag}(f'_t) \)):
\[ \frac{\partial L}{\partial h_t} = \delta_t^{out} + \frac{\partial L}{\partial h_{t+1}} \text{diag}(f'_t) W_{hh}^T \]

我们可以从 \( t=T \) 开始, iteratively 计算 \( \frac{\partial L}{\partial h_T}, \frac{\partial L}{\partial h_{T-1}}, \dots, \frac{\partial L}{\partial h_1} \)。

计算出 \( \frac{\partial L}{\partial h_t} \) 后,我们就可以计算参数在时间步 \( t \) 对总损失的贡献:
⚝ \( \frac{\partial L}{\partial W_{hh}}|_{t} = \frac{\partial L}{\partial h_t} \frac{\partial h_t}{\partial W_{hh}} = \frac{\partial L}{\partial h_t} \text{diag}(f'(W_{hh} h_{t-1} + W_{xh} x_t + b_h)) (h_{t-1})^T \)
⚝ \( \frac{\partial L}{\partial W_{xh}}|_{t} = \frac{\partial L}{\partial h_t} \frac{\partial h_t}{\partial W_{xh}} = \frac{\partial L}{\partial h_t} \text{diag}(f'(W_{hh} h_{t-1} + W_{xh} x_t + b_h)) x_t^T \)
⚝ \( \frac{\partial L}{\partial W_{hy}}|_{t} = \frac{\partial L}{\partial y_t} \frac{\partial y_t}{\partial W_{hy}} = \frac{\partial L}{\partial y_t} \text{diag}(g'(W_{hy} h_t + b_y)) h_t^T \)
⚝ \( \frac{\partial L}{\partial b_h}|_{t} = \frac{\partial L}{\partial h_t} \frac{\partial h_t}{\partial b_h} = \frac{\partial L}{\partial h_t} \text{diag}(f'(W_{hh} h_{t-1} + W_{xh} x_t + b_h)) \)
⚝ \( \frac{\partial L}{\partial b_y}|_{t} = \frac{\partial L}{\partial y_t} \frac{\partial y_t}{\partial b_y} = \frac{\partial L}{\partial y_t} \text{diag}(g'(W_{hy} h_t + b_y)) \)

注意:上面的 \( \text{diag}(\cdot) \) 表示将向量的导数作为对角线元素的对角矩阵,与后面的向量/矩阵相乘。更准确的写法涉及张量乘法或矩阵向量求导规则,但为了概念清晰,上面的形式可以帮助理解。例如,对于 \( \frac{\partial h_t}{\partial W_{hh}} \),如果 \( h_t = f(z_t) \) 且 \( z_t = W_{hh} h_{t-1} + \dots \),那么 \( \frac{\partial h_t}{\partial z_t} \) 是一个对角矩阵 \( \text{diag}(f'(z_t)) \),而 \( \frac{\partial z_t}{\partial W_{hh}} \) 是一个张量。最终结果 \( \frac{\partial L}{\partial W_{hh}}|_{t} \) 是一个与 \( W_{hh} \) 同形的矩阵。更严谨的推导请参考附录 B。

从上述 \( \frac{\partial L}{\partial h_t} \) 的递归公式可以看出,梯度 \( \frac{\partial L}{\partial h_t} \) 是通过连续乘以矩阵 \( \text{diag}(f'_i) W_{hh}^T \) 从 \( \frac{\partial L}{\partial h_{t+1}} \) 反传回来的。具体来说,\( \frac{\partial L}{\partial h_t} \) 包含一个项,其形式为 \( \frac{\partial L}{\partial h_T} \left( \prod_{i=t}^{T-1} \text{diag}(f'_{i+1}) W_{hh}^T \right) \text{diag}(f'_t) \)。这个连乘项 \( \prod_{i=t}^{T-1} \text{diag}(f'_{i+1}) W_{hh}^T \) 是导致梯度消失 (Vanishing Gradient) 和梯度爆炸 (Exploding Gradient) 问题的根本原因,我们将在下一章详细讨论。

3.2.2 参数更新

计算完每个时间步对参数的梯度贡献后,我们就可以计算共享参数的总梯度。
对于任何共享参数 \( \theta \in \{ W_{hh}, W_{xh}, W_{hy}, b_h, b_y \} \),其总梯度为:
\[ \frac{\partial L}{\partial \theta} = \sum_{t=1}^T \frac{\partial L}{\partial \theta}|_{t} \]
其中 \( \frac{\partial L}{\partial \theta}|_{t} \) 是参数 \( \theta \) 在时间步 \( t \) 对总损失 \( L \) 的贡献梯度,其计算依赖于 \( \frac{\partial L}{\partial h_t} \) 和 \( \frac{\partial L}{\partial y_t} \)。

在计算出总梯度 \( \frac{\partial L}{\partial \theta} \) 后,就可以使用优化器(如 SGD, Adam, RMSprop)来更新参数:
\[ \theta \leftarrow \theta - \alpha \frac{\partial L}{\partial \theta} \]
这个过程在每个训练迭代 (Training Iteration) 中进行,直到模型收敛。

对于非常长的序列,完全的 BPTT (Full BPTT) 需要沿着整个序列反向传播,这可能导致巨大的计算量和内存消耗。在实践中,常常使用截断反向传播通过时间 (Truncated Backpropagation Through Time - TBPTT)。TBPTT 将长序列分割成固定长度的子序列,并在每个子序列上独立执行 BPTT。在前向传播时,隐藏状态可以跨子序列传递;但在反向传播时,梯度被“截断”,只在一个子序列内部传播,不会传播到前一个子序列。这牺牲了一部分长期依赖的梯度信息,但显著降低了计算复杂度和内存需求,使其在实际应用中可行。

3.3 BPTT 的计算复杂性

理解 BPTT 的计算复杂性对于高效地训练 RNNs 至关重要。

时间复杂度 (Time Complexity)
前向传播:对于一个长度为 \( T \) 的序列,每个时间步的计算(矩阵乘法和激活函数)涉及 \( W_{hh} \in \mathbb{R}^{H \times H}, W_{xh} \in \mathbb{R}^{H \times D}, W_{hy} \in \mathbb{R}^{O \times H} \) 等(其中 \( H \) 是隐藏状态大小,\( D \) 是输入特征维度,\( O \) 是输出维度)。每个时间步的计算复杂度大约是 \( O(H^2 + HD + OH) \)。总的前向传播时间复杂度为 \( O(T \cdot (H^2 + HD + OH)) \)。
反向传播:BPTT 需要沿着展开的计算图反向计算梯度。计算 \( \frac{\partial L}{\partial h_t} \) 涉及矩阵乘法 \( \frac{\partial L}{\partial h_{t+1}} \) 和 \( W_{hh}^T \),复杂度为 \( O(H^2) \)。计算参数在时间步 \( t \) 的梯度(例如 \( \frac{\partial L}{\partial W_{hh}}|_t \))涉及外积或矩阵乘法,复杂度大约是 \( O(H^2 + HD + OH) \)。由于这些计算发生在每个时间步,并且 \( \frac{\partial L}{\partial h_t} \) 的计算需要从 \( t=T \) 反传到 \( t=1 \),总的反向传播时间复杂度与前向传播类似,也是 \( O(T \cdot (H^2 + HD + OH)) \)。
总体而言,标准 BPTT 的时间复杂度与序列长度 \( T \) 成线性关系。对于固定模型大小,它是 \( O(T) \)。

空间复杂度 (Space Complexity) / 内存需求 (Memory Requirement)
在反向传播过程中,为了计算梯度,需要存储前向传播过程中所有时间步的中间激活值(输入 \( x_t \)、隐藏状态 \( h_t \)、以及激活函数输入等)。对于一个长度为 \( T \) 的序列,需要存储 \( T \) 个时间步的隐藏状态 \( h_t \),每个状态大小为 \( H \),以及输入 \( x_t \) 等。总的内存需求大约是 \( O(T \cdot H + T \cdot D + T \cdot O) \)。
总体而言,标准 BPTT 的内存需求也与序列长度 \( T \) 成线性关系。对于固定模型大小,它是 \( O(T) \)。

这种与序列长度呈线性的时间复杂度和内存需求是标准 BPTT 的一个显著挑战。当序列非常长时(例如,处理一本小说或长时间的音频信号),完全的 BPTT 会变得计算量巨大且内存不可承受。截断 BPTT (TBPTT) 通过限制反向传播的跨度(例如,只反传 \( k \) 个时间步)来降低内存需求到 \( O(k \cdot H) \) 和时间复杂度,但这可能会影响模型学习长距离依赖的能力。这是 RNNs 在处理超长序列时面临的实际问题,也是引入 LSTM 和 GRU 等门控机制以及更晚的 Transformer 模型的部分动因。

下一章,我们将深入探讨标准 RNN 在 BPTT 过程中遇到的两大难题:梯度消失和梯度爆炸,正是这些问题催生了 LSTM 和 GRU 等更高级的 RNN 变体。

4. 标准 RNN 的局限性:梯度消失与梯度爆炸

亲爱的同学们:👋 欢迎来到本书的第四章。在前面的章节中,我们深入探讨了循环神经网络(RNN)如何通过引入隐藏状态(Hidden State)来处理序列数据,并学习了其核心的前向传播(Forward Propagation)计算以及如何使用反向传播通过时间(Backpropagation Through Time - BPTT)算法进行训练。RNN 的这种设计在处理短序列时表现良好,但在面对较长的序列时,我们很快就会发现它存在一些根本性的局限性。本章将聚焦于这些关键的挑战,特别是深度学习中常见的梯度消失(Vanishing Gradient)和梯度爆炸(Exploding Gradient)问题,以及它们在标准 RNN 中如何发生,为何会严重影响模型学习长期依赖(Long-Term Dependencies)的能力。理解这些问题是理解后续高级 RNN 架构(如 LSTM 和 GRU)为何诞生的基础。让我们一起揭开这些“幕后”的训练难题。

4.1 梯度消失问题 (Vanishing Gradient Problem)

要理解梯度消失问题,我们首先需要回顾 BPTT 算法。在 BPTT 中,模型参数(尤其是循环权重矩阵 \(W_{hh}\))的梯度是通过链式法则(Chain Rule)沿着时间步反向传播计算得到的。考虑在时间步 \(t\) 计算的损失 \(L_t\) 对较早时间步 \(t-k\) 的隐藏状态 \(h_{t-k}\) 或参数的梯度。这个梯度计算会涉及到一系列导数的乘积,其中最核心的部分是隐藏状态关于前一个时间步隐藏状态的导数链:

\[ \frac{\partial h_t}{\partial h_{t-k}} = \frac{\partial h_t}{\partial h_{t-1}} \frac{\partial h_{t-1}}{\partial h_{t-2}} \cdots \frac{\partial h_{t-k+1}}{\partial h_{t-k}} \]

这是一个包含 \(k\) 个项的连乘。每一项 \(\frac{\partial h_i}{\partial h_{i-1}}\) 都反映了时间步 \(i-1\) 的隐藏状态如何影响时间步 \(i\) 的隐藏状态。

梯度消失问题指的是,当序列长度 \(k\) 变得很大时,这个连乘的结果可能变得非常小,趋近于零。

定义:梯度消失(Vanishing Gradient)是指在通过多层网络(或在 RNN 中通过多个时间步)反向传播时,梯度值变得越来越小,甚至趋近于零的现象。

影响
① 当计算损失函数对早期时间步(即距离当前时间步较远)的隐藏状态或参数的梯度时,由于梯度消失,这些梯度会变得非常小。
② 这意味着早期时间步的输入或事件对当前时间步的输出或隐藏状态的影响很难通过梯度信号有效地传播回模型。
③ 最终结果是,模型难以学习到序列中相隔较远元素之间的依赖关系,即无法捕捉长期依赖(Long-Term Dependencies)。

例如,在一个很长的句子中,一个词语的含义可能取决于句子开头的一个词(例如,主语决定了动词的单复数形式)。如果模型存在梯度消失问题,那么句首的词语对句末动词预测的贡献,在反向传播计算梯度时就会被“稀释”掉,导致模型无法正确学习这种依赖关系。

4.2 梯度爆炸问题 (Exploding Gradient Problem)

与梯度消失相反,梯度爆炸是另一种发生在 BPTT 过程中的问题。

定义:梯度爆炸(Exploding Gradient)是指在通过多层网络(或在 RNN 中通过多个时间步)反向传播时,梯度值变得非常大,甚至发散(diverge)的现象。

同样地,回顾梯度传播中的连乘项 \(\frac{\partial h_t}{\partial h_{t-k}} = \frac{\partial h_t}{\partial h_{t-1}} \frac{\partial h_{t-1}}{\partial h_{t-2}} \cdots \frac{\partial h_{t-k+1}}{\partial h_{t-k}}\)。

影响
① 当这个连乘的结果变得非常大时,计算得到的梯度值也会非常大。
② 使用这些巨大的梯度来更新模型参数时,会导致参数发生剧烈的变化。
③ 这使得训练过程变得极不稳定,学习率需要设置得非常小才能避免模型权重在训练过程中发散到无穷大(即 NaN 或 Inf)。即使使用很小的学习率,模型也可能难以收敛到最优解。

想象一下训练过程中的损失函数曲面,如果梯度爆炸发生,模型参数的更新步长会像喝醉了酒一样,在曲面上跳跃,根本无法平稳地找到最低点。

与梯度消失相比,梯度爆炸通常更容易发现,因为模型的损失会迅速变为无穷大(NaN),或者训练过程变得非常不稳定。

4.3 原因分析:激活函数与权重矩阵

梯度消失和梯度爆炸的根本原因在于 BPTT 中梯度计算涉及的连乘结构,以及这些连乘项(\(\frac{\partial h_i}{\partial h_{i-1}}\))的性质。

让我们更详细地分析 \(\frac{\partial h_i}{\partial h_{i-1}}\) 这一项。在标准 RNN 中,隐藏状态的计算通常如下:

\[ h_i = f(W_{hh} h_{i-1} + W_{xh} x_i + b_h) \]

其中 \(f\) 是激活函数(如 Sigmoid 或 Tanh)。

根据链式法则,我们可以计算 \(\frac{\partial h_i}{\partial h_{i-1}}\):

\[ \frac{\partial h_i}{\partial h_{i-1}} = \text{diag}(f'(W_{hh} h_{i-1} + W_{xh} x_i + b_h)) W_{hh} \]

这是一个矩阵,称为 Jacobian 矩阵(雅可比矩阵)。整个梯度传播链 \(\frac{\partial h_t}{\partial h_{t-k}}\) 实际上是多个这样的 Jacobian 矩阵的乘积。

\[ \frac{\partial h_t}{\partial h_{t-k}} = \left( \text{diag}(f'(\cdots)) W_{hh} \right)_t \cdot \left( \text{diag}(f'(\cdots)) W_{hh} \right)_{t-1} \cdots \left( \text{diag}(f'(\cdots)) W_{hh} \right)_{t-k+1} \]

这里的核心问题在于这个连乘的矩阵序列。根据线性代数的知识,连续矩阵乘积的范数增长或衰减与这些矩阵的谱半径(Specular Radius)或特征值(Eigenvalues)有关。在这里,关键因素是循环权重矩阵 \(W_{hh}\) 和激活函数导数 \(f'\) 的性质。

激活函数 \(f'\) 的影响
① 常用的激活函数如 Sigmoid 或 Tanh,它们的导数 \(f'(z)\) 的最大值是有限的,且通常小于或等于 1(Sigmoid 的导数最大值为 0.25,Tanh 的导数最大值为 1)。
② 特别是在输入 \(z\) 很大或很小(即远离 0)时,Sigmoid 和 Tanh 函数会趋于饱和(Saturate),其导数 \(f'(z)\) 会趋近于 0。
③ 当激活函数频繁处于饱和区时,矩阵 \(\text{diag}(f'(\cdots))\) 的对角线元素会非常小,导致整个 Jacobian 矩阵的范数变得很小。
④ 在长序列的连乘中,多个小于 1 的数相乘会迅速趋近于 0,从而导致梯度消失。

循环权重矩阵 \(W_{hh}\) 的影响
① 如果 \(W_{hh}\) 的特征值(绝对值)都小于 1,那么即使 \(f'\) 不饱和,多次乘以 \(W_{hh}\) 也会导致结果趋向于 0(类似于一个小于 1 的数自乘多次)。这加剧了梯度消失的可能性。
② 如果 \(W_{hh}\) 的某个特征值(绝对值)大于 1,那么多次乘以 \(W_{hh}\) 会导致结果呈指数级增长(类似于一个大于 1 的数自乘多次)。这可能导致梯度爆炸。

总结来说:
梯度消失:主要由于激活函数的饱和性(导致 \(f'\) 接近 0)和/或循环权重矩阵 \(W_{hh}\) 的谱半径小于 1。在长序列中,这些小于 1 的项多次相乘,使得梯度呈指数级衰减。
梯度爆炸:主要由于循环权重矩阵 \(W_{hh}\) 的谱半径大于 1。在长序列中,这些大于 1 的项多次相乘,使得梯度呈指数级增长。

虽然这两种问题都源于 BPTT 的连乘结构,但梯度消失是标准 RNN 在处理长期依赖时更普遍和更棘手的问题,而梯度爆炸相对容易检测和处理。

4.4 应对梯度爆炸的方法:梯度裁剪 (Gradient Clipping)

幸运的是,应对梯度爆炸有一个相对简单且有效的方法:梯度裁剪(Gradient Clipping)。

核心思想:当计算出的梯度向量的范数(norm)超过一个预设的阈值时,对其进行等比例缩放(scale down),使其范数等于该阈值。

工作原理
① 在计算出所有参数的梯度后,计算这些梯度的总范数(通常是 L2 范数)。
② 如果梯度范数 \(||g||\) 大于设定的阈值 \(\text{threshold}\),则将梯度向量 \(g\) 缩放为 \(g' = g \cdot \frac{\text{threshold}}{||g||}\)。
③ 如果梯度范数 \(||g||\) 小于等于阈值,则不进行缩放,使用原始梯度 \(g' = g\)。

这个过程可以表示为:

\[ \text{if } ||g|| > \text{threshold} \text{ then } g \leftarrow g \cdot \frac{\text{threshold}}{||g||} \]

优点
① 实现简单。
② 能有效防止梯度爆炸导致训练过程发散。

局限性
① 梯度裁剪仅仅是防止了梯度变得无限大,它并不能解决梯度消失问题。
② 裁剪阈值的选择需要经验。

在实际应用中,梯度裁剪是训练 RNN,特别是标准 RNN,时常用的技巧。然而,它并不能解决核心的长期依赖学习问题,因为这通常是由梯度消失引起的,而梯度裁剪并不能阻止梯度变小。

在本章中,我们深入分析了标准 RNN 在处理长序列时遇到的主要挑战:梯度消失和梯度爆炸。我们了解到,这些问题源于 BPTT 中梯度计算的连乘结构,并受到激活函数导数和循环权重矩阵性质的影响。我们还学习了应对梯度爆炸的常用方法:梯度裁剪。理解这些局限性,将引导我们进入下一章,去探索那些旨在克服这些挑战、更强大的循环神经网络架构,如 LSTM 和 GRU。💪

5. 长短期记忆网络 (Long Short-Term Memory Network - LSTM)

本章详细讲解解决标准循环神经网络 (Standard Recurrent Neural Network - RNN) 长期依赖 (Long-Term Dependencies) 问题的开创性架构:长短期记忆网络 (Long Short-Term Memory Network - LSTM),包括其门控机制 (Gating Mechanism) 和结构。LSTM 由 Sepp Hochreiter 和 Jürgen Schmidhuber 于 1997 年提出,是 RNN 领域最重要的突破之一,极大地提高了模型处理和记忆长序列信息的能力。尽管近期 Transformer 等新模型在许多任务上取得了更好的表现,但理解 LSTM 的原理对于掌握序列建模和深度学习的发展历程至关重要。

5.1. LSTM 的核心思想:引入门控单元

在第 4 章中,我们讨论了标准 RNN 在处理长序列时面临的梯度消失 (Vanishing Gradient) 和梯度爆炸 (Exploding Gradient) 问题。特别是梯度消失问题,使得 RNN 难以学习到输入序列中相距较远的时间步之间的依赖关系,即长期依赖。例如,在处理一篇长文本时,理解当前句子的含义可能需要回顾之前很远的上下文信息。标准 RNN 的隐藏状态 (Hidden State) 倾向于“遗忘”早期信息,因为它不断地被新输入的信息覆盖,并且梯度在反向传播时会衰减。

LSTM 的核心思想是为了解决这个问题而设计的。它引入了一种特殊的结构,称为门控单元 (Gating Unit)门 (Gate),来精确地控制信息在网络中的流动。与标准 RNN 简单地通过一个激活函数处理输入和前一个隐藏状态来更新当前隐藏状态不同,LSTM 单元内部有专门的机制来决定哪些信息应该被保留(记忆),哪些应该被丢弃(遗忘),以及哪些信息应该被输出。

可以把 LSTM 单元想象成一个具有更复杂的内部结构和控制开关(门)的神经元。这些门由神经网络层实现,并使用 Sigmoid 激活函数输出 0 到 1 之间的值。这个值代表了允许多少信息通过。例如,一个值为 0 的门表示完全阻止信息通过,而一个值为 1 的门则表示允许所有信息通过。通过这种门控机制,LSTM 能够有选择地记忆和遗忘信息,从而有效地捕捉和利用长期依赖。

引入门控单元的主要动机包括:

⚝ 更好的控制信息流:门控机制允许网络选择性地更新其内部状态,而不是无条件地用新信息覆盖旧信息。
⚝ 缓解梯度消失:通过引入细胞状态 (Cell State) 和特殊的门控结构,梯度可以在细胞状态这条“高速公路”上传播,而不像标准 RNN 那样在每个时间步都必须经过非线性的激活函数和权重矩阵的乘积,从而缓解了梯度消失问题。
⚝ 捕捉长期依赖:通过门控对信息的精确控制,LSTM 可以在细胞状态中长时间保留重要的历史信息,即使这些信息来自序列的早期部分。

简单来说,LSTM 通过在 RNN 单元内添加“记忆单元”和控制这个记忆单元的“门”,使得网络能够更聪明地决定何时记住、何时遗忘历史信息,从而克服了标准 RNN 在处理长序列时的主要障碍。

5.2. LSTM 单元结构详解

一个标准的 LSTM 单元在每个时间步 \( t \) 接收三个输入:当前时间步的输入 \( x_t \)、前一个时间步的隐藏状态 \( h_{t-1} \) 和前一个时间步的细胞状态 \( c_{t-1} \)。它输出当前时间步的隐藏状态 \( h_t \) 和细胞状态 \( c_t \)。

LSTM 单元的核心是细胞状态 (Cell State),表示为 \( c_t \)。细胞状态类似于一条贯穿整个序列处理过程的“传送带”,它携带信息在时间步之间传递。与隐藏状态 \( h_t \) 不同,细胞状态 \( c_t \) 的更新是通过简单的加法和乘法操作来控制的,这使得梯度更容易在时间步之间传递,从而缓解了梯度消失问题。

为了控制信息如何进入、离开或保留在细胞状态中,LSTM 引入了三个关键的门:

遗忘门 (Forget Gate) \( f_t \):
▮▮▮▮⚝ 作用:决定从细胞状态 \( c_{t-1} \) 中丢弃什么信息。
▮▮▮▮⚝ 计算:接收 \( x_t \) 和 \( h_{t-1} \) 作为输入,通过一个 Sigmoid 函数输出一个介于 0 和 1 之间的向量。
▮▮▮▮⚝ \( f_t = \sigma(W_f \cdot [h_{t-1}, x_t] + b_f) \)
▮▮▮▮⚝ 输出的向量 \( f_t \) 中的每个元素对应于细胞状态中的一个元素。如果 \( f_t \) 中的某个元素接近 0,表示遗忘对应的细胞状态元素;如果接近 1,表示保留。

输入门 (Input Gate) \( i_t \):
▮▮▮▮⚝ 作用:决定将哪些新信息存储到细胞状态中。
▮▮▮▮⚝ 这个门有两个部分:
▮▮▮▮ⓐ 输入门本身 \( i_t \):通过一个 Sigmoid 函数决定哪些值需要被更新。
▮▮▮▮▮▮▮▮⚝ \( i_t = \sigma(W_i \cdot [h_{t-1}, x_t] + b_i) \)
▮▮▮▮ⓑ 候选细胞状态 \( \tilde{c}_t \):通过一个 Tanh 函数创建一个新的候选值向量,准备加入到细胞状态中。
▮▮▮▮▮▮▮▮⚝ \( \tilde{c}_t = \tanh(W_c \cdot [h_{t-1}, x_t] + b_c) \)
▮▮▮▮⚝ 输入门 \( i_t \) 的输出与候选细胞状态 \( \tilde{c}_t \) 的输出进行逐元素相乘 \( i_t \odot \tilde{c}_t \),这决定了哪些候选信息以及多少被添加到细胞状态中。

输出门 (Output Gate) \( o_t \):
▮▮▮▮⚝ 作用:决定输出什么值(即当前时间步的隐藏状态 \( h_t \))。
▮▮▮▮⚝ 计算:接收 \( x_t \) 和 \( h_{t-1} \) 作为输入,通过一个 Sigmoid 函数输出一个介于 0 和 1 之间的向量。
▮▮▮▮⚝ \( o_t = \sigma(W_o \cdot [h_{t-1}, x_t] + b_o) \)
▮▮▮▮⚝ 为了得到最终的隐藏状态 \( h_t \),首先将当前的细胞状态 \( c_t \) 通过一个 Tanh 函数进行变换,得到一个在 \([-1, 1]\) 之间的值;然后将这个结果与输出门 \( o_t \) 的输出进行逐元素相乘 \( o_t \odot \tanh(c_t) \)。这相当于对经过 Tanh 变换的细胞状态进行“过滤”,只输出门控允许的部分。
▮▮▮▮⚝ \( h_t = o_t \odot \tanh(c_t) \)

细胞状态的更新 (Cell State Update)
在计算出遗忘门 \( f_t \) 和输入门 \( i_t \odot \tilde{c}_t \) 后,细胞状态 \( c_t \) 的更新分两步完成:
首先,将前一个时间步的细胞状态 \( c_{t-1} \) 与遗忘门 \( f_t \) 进行逐元素相乘 \( f_t \odot c_{t-1} \)。这相当于“遗忘” \( c_{t-1} \) 中不重要的信息。
然后,将上一步的结果与输入门处理后的新信息 \( i_t \odot \tilde{c}_t \) 进行逐元素相加 \( f_t \odot c_{t-1} + i_t \odot \tilde{c}_t \)。这相当于将重要的新信息“添加”到细胞状态中。
这个加法操作是 LSTM 缓解梯度消失的关键。因为在加法操作中,梯度可以比较容易地流过(导数是 1),这使得信息(和梯度)更容易在细胞状态这条链上传递。

所以,细胞状态的更新公式是:
\[ c_t = f_t \odot c_{t-1} + i_t \odot \tilde{c}_t \]

总结 LSTM 单元的主要组成部分:

细胞状态 (Cell State) \( c \): 贯穿单元的核心,用于长时间存储信息。
遗忘门 (Forget Gate) \( f \): 控制从细胞状态中遗忘哪些信息。
输入门 (Input Gate) \( i \): 控制将哪些新信息加入到细胞状态中。
候选细胞状态 \( \tilde{c} \): 待加入到细胞状态的新信息候选项。
输出门 (Output Gate) \( o \): 控制根据细胞状态输出什么信息作为当前隐藏状态。
隐藏状态 (Hidden State) \( h \): 当前时间步的输出,也作为下一个时间步的输入。

这些门和状态通过一系列向量操作(矩阵乘法、向量加法、逐元素乘法、激活函数)相互作用,共同决定了信息在 LSTM 单元内的流动和存储。

5.3. 门控机制的数学表示与工作原理

本节将给出 LSTM 单元在时间步 \( t \) 的详细数学公式,并解释每个公式的功能和各部分如何协同工作。

假设输入向量 \( x_t \) 的维度是 \( d_x \),隐藏状态 \( h_{t-1} \) 的维度是 \( d_h \)。拼接后的向量 \( [h_{t-1}, x_t] \) 的维度是 \( d_h + d_x \)。细胞状态 \( c_{t-1} \) 和 \( c_t \) 的维度通常与隐藏状态相同,即 \( d_h \)。

门控单元和候选细胞状态的计算:

遗忘门 \( f_t \):
决定保留多少来自 \( c_{t-1} \) 的信息。它将 \( h_{t-1} \) 和 \( x_t \) 作为输入,通过一个线性变换后应用 Sigmoid 激活函数。Sigmoid 函数将输出压缩到 \((0, 1)\) 区间,代表“遗忘”的程度。
\[ f_t = \sigma(W_{xf} x_t + W_{hf} h_{t-1} + b_f) \]
或者使用拼接表示:
\[ f_t = \sigma(W_f [h_{t-1}, x_t] + b_f) \]
其中 \( W_f \) 是遗忘门的权重矩阵,\( b_f \) 是偏置向量,\( \sigma \) 是 Sigmoid 激活函数。

输入门 \( i_t \):
决定哪些新信息将用于更新细胞状态。同样将 \( h_{t-1} \) 和 \( x_t \) 作为输入,通过 Sigmoid 函数。
\[ i_t = \sigma(W_{xi} x_t + W_{hi} h_{t-1} + b_i) \]
或者使用拼接表示:
\[ i_t = \sigma(W_i [h_{t-1}, x_t] + b_i) \]
其中 \( W_i \) 是输入门的权重矩阵,\( b_i \) 是偏置向量。

候选细胞状态 \( \tilde{c}_t \):
生成新的候选值向量,准备更新细胞状态。使用 Tanh 激活函数将值压缩到 \((-1, 1)\) 区间。
\[ \tilde{c}_t = \tanh(W_{xc} x_t + W_{hc} h_{t-1} + b_c) \]
或者使用拼接表示:
\[ \tilde{c}_t = \tanh(W_c [h_{t-1}, x_t] + b_c) \]
其中 \( W_c \) 是候选细胞状态的权重矩阵,\( b_c \) 是偏置向量。

细胞状态更新 \( c_t \):
根据遗忘门 \( f_t \) 和输入门 \( i_t \) 来更新细胞状态 \( c_t \)。
\[ c_t = f_t \odot c_{t-1} + i_t \odot \tilde{c}_t \]
这里 \( \odot \) 表示逐元素相乘 (Hadamard product)。遗忘门 \( f_t \) 控制着 \( c_{t-1} \) 中保留的信息量,输入门 \( i_t \) 控制着 \( \tilde{c}_t \) 中有多少新信息被添加到 \( c_t \)。

输出门 \( o_t \):
决定基于更新后的细胞状态 \( c_t \) 输出什么。
\[ o_t = \sigma(W_{xo} x_t + W_{ho} h_{t-1} + b_o) \]
或者使用拼接表示:
\[ o_t = \sigma(W_o [h_{t-1}, x_t] + b_o) \]
其中 \( W_o \) 是输出门的权重矩阵,\( b_o \) 是偏置向量。

隐藏状态输出 \( h_t \):
最终的隐藏状态 \( h_t \) 是通过将更新后的细胞状态 \( c_t \) 应用 Tanh 激活函数,然后与输出门 \( o_t \) 逐元素相乘得到的。
\[ h_t = o_t \odot \tanh(c_t) \]
Tanh 激活函数将 \( c_t \) 的值缩放到 \((-1, 1)\) 之间,而输出门 \( o_t \) 则像一个过滤器,决定 \( \tanh(c_t) \) 的哪些部分最终构成隐藏状态 \( h_t \)。隐藏状态 \( h_t \) 既是当前时间步的输出,也是下一个时间步的输入。

工作原理总结:

在每个时间步 \( t \),LSTM 单元执行以下步骤:

  1. 遗忘(Forget): 查看输入 \( x_t \) 和上一时刻的隐藏状态 \( h_{t-1} \),计算遗忘门 \( f_t \),决定从上一时刻的细胞状态 \( c_{t-1} \) 中“遗忘”多少信息。
  2. 输入(Input): 查看输入 \( x_t \) 和上一时刻的隐藏状态 \( h_{t-1} \),计算输入门 \( i_t \)(决定哪些信息更新)和候选细胞状态 \( \tilde{c}_t \)(新的信息候选项)。
  3. 更新细胞状态(Update Cell State): 结合遗忘门 \( f_t \) 和输入门 \( i_t \odot \tilde{c}_t \),更新细胞状态从 \( c_{t-1} \) 到 \( c_t \)。这是通过遗忘旧信息 \( f_t \odot c_{t-1} \) 并添加新信息 \( i_t \odot \tilde{c}_t \) 来实现的。
  4. 输出(Output): 查看输入 \( x_t \) 和上一时刻的隐藏状态 \( h_{t-1} \),计算输出门 \( o_t \)。然后将更新后的细胞状态 \( c_t \) 通过 Tanh 激活函数,并与输出门 \( o_t \) 相乘,得到当前时间步的隐藏状态 \( h_t \)。

通过这三个门的协调控制,LSTM 能够在细胞状态中长期保留重要信息,并且能够根据当前的输入和上下文选择性地更新和输出信息,从而有效地处理长期依赖问题。

5.4. LSTM 的前向传播与反向传播

5.4.1. 前向传播 (Forward Propagation)

LSTM 单元的前向传播过程就是按照上一节给出的数学公式,依次计算每个时间步的门控值、候选细胞状态、细胞状态和隐藏状态。

对于一个长度为 \( T \) 的序列,前向传播从时间步 \( t=1 \) 开始,到 \( t=T \) 结束。在时间步 \( t \),计算依赖于当前输入 \( x_t \) 和前一个时间步的状态 \( h_{t-1}, c_{t-1} \)。

假设初始状态 \( h_0 \) 和 \( c_0 \) 通常初始化为零向量(或可学习的参数)。

循环过程如下:

① 对于 \( t = 1, 2, \dots, T \):
② 计算遗忘门 \( f_t \):\( f_t = \sigma(W_f [h_{t-1}, x_t] + b_f) \)
③ 计算输入门 \( i_t \):\( i_t = \sigma(W_i [h_{t-1}, x_t] + b_i) \)
④ 计算候选细胞状态 \( \tilde{c}_t \):\( \tilde{c}_t = \tanh(W_c [h_{t-1}, x_t] + b_c) \)
⑤ 更新细胞状态 \( c_t \):\( c_t = f_t \odot c_{t-1} + i_t \odot \tilde{c}_t \)
⑥ 计算输出门 \( o_t \):\( o_t = \sigma(W_o [h_{t-1}, x_t] + b_o) \)
⑦ 计算当前隐藏状态 \( h_t \):\( h_t = o_t \odot \tanh(c_t) \)

这个过程沿着时间步一步步进行,每个时间步的计算都依赖于前一个时间步的结果,并为下一个时间步准备状态。最终的输出(如果任务需要序列到序列的输出)可以是从每个时间步的 \( h_t \) 计算得出的,或者只使用最后一个时间步的 \( h_T \)(例如在分类任务中)。

5.4.2. 反向传播 (Backward Propagation)

训练 LSTM 的核心算法仍然是反向传播通过时间 (Backpropagation Through Time - BPTT)。然而,与标准 RNN 不同的是,LSTM 的结构,特别是细胞状态的线性更新路径和门控机制,显著改善了梯度的传播。

回顾标准 RNN 中梯度消失的主要原因之一是梯度在通过每个时间步的隐藏状态时,不断乘以一个权重矩阵的转置和一个激活函数的导数,如果这些值都很小(例如 Sigmoid/Tanh 在两端的导数接近 0),梯度就会指数级衰减。

在 LSTM 中,细胞状态的更新公式是 \( c_t = f_t \odot c_{t-1} + i_t \odot \tilde{c}_t \)。在反向传播计算损失函数 \( L \) 关于 \( c_{t-1} \) 的梯度时,应用链式法则,梯度会经过 \( c_t \) 到 \( c_{t-1} \) 的路径。这条路径上的导数是 \( \frac{\partial c_t}{\partial c_{t-1}} = f_t \). 这里的 \( f_t \) 是遗忘门的输出,它是一个向量,其元素在 (0, 1) 之间。当 \( f_t \) 的元素接近 1 时,梯度可以通过 \( c_{t-1} \) 几乎无衰减地传播。这与标准 RNN 中通过 \( h_{t-1} \) 传播时必须乘以 \( \tanh'(\dots) W \) 不同,LSTM 提供了一条更直观的、由遗忘门控制的梯度通路。

虽然梯度在细胞状态链上传播时仍然会受到 \( f_t \) 的影响,并且还会受到 \( i_t \odot \tilde{c}_t \) 这一项的梯度影响(这部分梯度会涉及到非线性的输入门和候选状态计算),但细胞状态的加法更新和遗忘门的控制共同作用,使得 LSTM 在实践中能够显著缓解梯度消失问题,更好地学习长期依赖。

详细的 BPTT 过程涉及计算损失函数对每个时间步的门控单元参数 \( W_f, b_f, W_i, b_i, W_c, b_c, W_o, b_o \) 的梯度,以及对初始状态 \( h_0, c_0 \) 的梯度(如果它们是可学习的)。这个过程需要应用链式法则,沿着时间步从后往前计算梯度,并将每个时间步的梯度贡献累加起来。由于涉及多个门和细胞状态,完整的推导比标准 RNN 更复杂(本书附录 B 提供了详细推导)。但核心思想是利用细胞状态的特殊结构,使得梯度信息更容易在时间步之间流动。

与标准 RNN 类似,LSTM 在反向传播时仍然可能遇到梯度爆炸问题。处理梯度爆炸的常用方法是梯度裁剪 (Gradient Clipping),即在梯度向量的范数 (norm) 超过某个阈值时对其进行缩放。这在第 4 章中已经介绍过,同样适用于 LSTM 和 GRU 等模型。

5.5. 窥视孔连接 (Peephole Connections)

窥视孔连接 (Peephole Connections),也称为窥视孔 LSTM (Peephole LSTM),是 LSTM 的一个常见变体,由 Felix Gers 及其同事在 2000 年提出。标准 LSTM 中的门控单元(遗忘门、输入门、输出门)只接收当前输入 \( x_t \) 和上一时间步的隐藏状态 \( h_{t-1} \) 作为输入来计算其激活值。窥视孔连接的思想是允许门控单元也能“看到”或使用细胞状态 (Cell State)作为额外输入。

具体来说,窥视孔连接会修改门控单元的计算公式,将 \( c_{t-1} \)(对于遗忘门和输入门)或 \( c_t \)(对于输出门)添加到线性变换的输入中。修改后的公式如下:

遗忘门 \( f_t \) (带窥视孔连接):
\[ f_t = \sigma(W_{xf} x_t + W_{hf} h_{t-1} + W_{cf} c_{t-1} + b_f) \]
或使用拼接表示,但需要额外的 \( W_{cf} \) 矩阵与 \( c_{t-1} \) 相乘:
\[ f_t = \sigma(W_f [h_{t-1}, x_t] + W_{cf} c_{t-1} + b_f) \]
这里 \( W_{cf} \) 是与 \( c_{t-1} \) 相关的权重矩阵。遗忘门现在可以根据前一个时间步的细胞状态来决定遗忘什么。

输入门 \( i_t \) (带窥视孔连接):
\[ i_t = \sigma(W_{xi} x_t + W_{hi} h_{t-1} + W_{ci} c_{t-1} + b_i) \]
或使用拼接表示:
\[ i_t = \sigma(W_i [h_{t-1}, x_t] + W_{ci} c_{t-1} + b_i) \]
其中 \( W_{ci} \) 是与 \( c_{t-1} \) 相关的权重矩阵。输入门现在可以根据前一个时间步的细胞状态来决定接收哪些新信息。

输出门 \( o_t \) (带窥视孔连接):
\[ o_t = \sigma(W_{xo} x_t + W_{ho} h_{t-1} + W_{co} c_{t} + b_o) \]
或使用拼接表示:
\[ o_t = \sigma(W_o [h_{t-1}, x_t] + W_{co} c_{t} + b_o) \]
其中 \( W_{co} \) 是与 \( c_t \) 相关的权重矩阵。输出门现在可以根据当前的细胞状态来决定输出什么。

候选细胞状态 \( \tilde{c}_t \)细胞状态更新 \( c_t \) 的公式保持不变,它们不直接接收细胞状态作为输入来计算其内部值,而是根据门的输出来更新细胞状态。

窥视孔连接的引入使得门控单元能够直接感知细胞状态的内容,这可能有助于模型做出更明智的门控决策。例如,遗忘门可以根据前一时刻的细胞状态来决定是否遗忘某些内容,而不仅仅是依赖于隐藏状态和当前输入。理论上,这可能提高了模型的性能,尤其是在需要精确控制信息流动的任务中。然而,在实际应用中,带窥视孔连接的 LSTM 相对于标准 LSTM 带来的性能提升并不总是非常显著,并且会增加一些额外的参数。许多主流的深度学习框架(如 TensorFlow, PyTorch)都提供了实现带窥视孔连接的 LSTM 的选项。

6. 门控循环单元 (GRU)

本章讲解另一种流行的门控循环神经网络 (Gated Recurrent Unit - GRU) 架构。GRU 是 LSTM 的一个变体,旨在在保持处理长期依赖能力的同时,简化模型结构和参数数量。通过本章的学习,读者将了解 GRU 的核心思想、内部结构、数学表示以及它与 LSTM 的主要异同。

6.1 GRU 的核心思想:简化门控机制

本节介绍 GRU 设计的动机,即在保证性能的同时简化 LSTM 的结构和参数数量。

正如我们在第 4 章中讨论的,标准循环神经网络 (Vanilla RNN) 在处理长序列时面临梯度消失 (Vanishing Gradient) 和梯度爆炸 (Exploding Gradient) 的问题,这使得它们难以学习序列中的长期依赖 (Long-Term Dependencies)。长短期记忆网络 (LSTM) 通过引入复杂的门控机制(遗忘门、输入门、输出门)和细胞状态 (Cell State) 成功地缓解了这些问题,极大地提升了模型在序列任务上的表现。然而,LSTM 单元内部包含较多的参数,计算相对复杂。

门控循环单元 (GRU) 由 Cho 等人于 2014 年提出,其核心思想是在保留门控机制精髓的基础上,对 LSTM 进行了简化。GRU 合并了 LSTM 的遗忘门 (Forget Gate) 和输入门 (Input Gate) 为一个更新门 (Update Gate),并且将细胞状态 (Cell State) 和隐藏状态 (Hidden State) 合并为一个状态向量。这种简化减少了模型的参数数量,可能有助于加速训练,并在某些任务上达到与 LSTM 相当甚至更好的性能。

GRU 的设计哲学是:用更少的门来控制信息的流动,使得模型更容易训练,同时仍然能够有效地捕捉不同时间步之间的依赖关系,包括长期依赖。这种简化体现在门的功能合并和状态向量的统一上。

6.2 GRU 单元结构详解

本节详细拆解 GRU 单元的内部结构,包括更新门 (Update Gate) 和重置门 (Reset Gate)。

GRU 单元包含两个主要的门:

更新门 (Update Gate):决定当前时间步的信息有多少应该被保留到下一个隐藏状态,以及前一个隐藏状态有多少应该被遗忘。可以理解为它同时控制了“遗忘”和“输入”的过程。
重置门 (Reset Gate):决定过去的信息有多少应该被忽略,以便计算当前时间步的候选隐藏状态。如果重置门的值接近于零,意味着模型可以完全忽略掉之前的历史信息。

与 LSTM 不同,GRU 没有独立的细胞状态 (Cell State)。它只维护一个隐藏状态 (Hidden State),该状态在时间步之间传递,并同时充当“记忆”和输出的基础。

GRU 单元在时间步 \(t\) 的计算流程如下:
⚝ 接收当前时间步的输入 \(x_t\) 和前一个时间步的隐藏状态 \(h_{t-1}\)。
⚝ 使用 \(x_t\) 和 \(h_{t-1}\) 计算更新门 \(z_t\) 和重置门 \(r_t\)。
⚝ 使用重置门 \(r_t\)、\(x_t\) 和 \(h_{t-1}\) 计算候选隐藏状态 \(\tilde{h}_t\)。重置门在这里决定了 \(h_{t-1}\) 有多少被用于计算 \(\tilde{h}_t\)。
⚝ 使用更新门 \(z_t\) 将前一个隐藏状态 \(h_{t-1}\) 和当前候选隐藏状态 \(\tilde{h}_t\) 进行加权平均,得到当前时间步的隐藏状态 \(h_t\)。更新门决定了从 \(h_{t-1}\) 中保留多少信息()1-z_t) 部分),以及从 \(\tilde{h}_t\) 中获取多少新信息(\(z_t\) 部分)。

这个流程可以用一个简化的图示表示(省略了权重和偏置项):

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 graph LR
2 X[xt] --> UpdateGate
3 H_prev[h_t-1] --> UpdateGate
4 UpdateGate(Update Gate: z_t) --> H_t[h_t]
5
6 X --> ResetGate
7 H_prev --> ResetGate
8 ResetGate(Reset Gate: r_t) --> CandidateState_mult
9 H_prev --> CandidateState_mult(Multiply)
10 CandidateState_mult --> CandidateState
11 X --> CandidateState(Candidate State: ~h_t) --> H_t
12
13 CandidateState --> H_t
14 H_prev --> H_t
15 style UpdateGate fill:#f9f,stroke:#333,stroke-width:2px
16 style ResetGate fill:#f9f,stroke:#333,stroke-width:2px
17 style CandidateState fill:#9cf,stroke:#333,stroke-width:2px
18 style H_t fill:#afa,stroke:#333,stroke-width:2px

通过这种设计,GRU 可以在不引入单独细胞状态的情况下,有效地控制信息的流动和遗忘,从而学习到跨时间步的依赖关系。

6.3 门控机制的数学表示与工作原理

本节给出 GRU 门控单元的数学公式,并解释它们如何控制信息的更新和重置。

在时间步 \(t\),给定输入向量 \(x_t\) 和前一个时间步的隐藏状态 \(h_{t-1}\),GRU 单元的计算步骤如下:

计算更新门 (Update Gate) \(z_t\):
\[ z_t = \sigma(W_z x_t + U_z h_{t-1} + b_z) \]
▮▮▮▮⚝ \(W_z\) 和 \(U_z\) 是更新门对应的权重矩阵。
▮▮▮▮⚝ \(b_z\) 是更新门对应的偏置向量。
▮▮▮▮⚝ \(\sigma\) 是 Sigmoid 激活函数,其输出值在 0 到 1 之间,表示门打开的程度。
▮▮▮▮⚝ 更新门 \(z_t\) 决定了在更新隐藏状态时,有多少来自新的候选状态 \(\tilde{h}_t\) 的信息(比例为 \(z_t\)),有多少来自前一个隐藏状态 \(h_{t-1}\) 的信息(比例为 \(1-z_t\))。

计算重置门 (Reset Gate) \(r_t\):
\[ r_t = \sigma(W_r x_t + U_r h_{t-1} + b_r) \]
▮▮▮▮⚝ \(W_r\) 和 \(U_r\) 是重置门对应的权重矩阵。
▮▮▮▮⚝ \(b_r\) 是重置门对应的偏置向量。
▮▮▮▮⚝ \(\sigma\) 是 Sigmoid 激活函数。
▮▮▮▮⚝ 重置门 \(r_t\) 决定了前一个隐藏状态 \(h_{t-1}\) 有多少信息被用于计算当前的候选隐藏状态 \(\tilde{h}_t\)。如果 \(r_t\) 接近 0,则前一个隐藏状态几乎被完全忽略。

计算候选隐藏状态 (Candidate Hidden State) \(\tilde{h}_t\):
\[ \tilde{h}_t = \tanh(W_h x_t + U_h (r_t \odot h_{t-1}) + b_h) \]
▮▮▮▮⚝ \(W_h\) 和 \(U_h\) 是候选隐藏状态计算对应的权重矩阵。
▮▮▮▮⚝ \(b_h\) 是候选隐藏状态计算对应的偏置向量。
▮▮▮▮⚝ \(\tanh\) 是双曲正切激活函数,其输出值在 -1 到 1 之间。
▮▮▮▮⚝ \(\odot\) 表示逐元素乘法 (Element-wise Multiplication)。
▮▮▮▮⚝ 注意 \(U_h\) 乘以的是 \(r_t \odot h_{t-1}\),这意味着重置门 \(r_t\) 控制了 \(h_{t-1}\) 中哪些信息被“重置”或忽略,只保留与当前输入 \(x_t\) 和 \(r_t\) 相乘后的 \(h_{t-1}\) 相关的信息来计算候选隐藏状态。

计算当前隐藏状态 (Current Hidden State) \(h_t\):
\[ h_t = (1 - z_t) \odot h_{t-1} + z_t \odot \tilde{h}_t \]
▮▮▮▮⚝ 这是 GRU 的核心更新步骤。
▮▮▮▮⚝ \(1 - z_t\) 控制了前一个隐藏状态 \(h_{t-1}\) 的信息保留量。如果 \(z_t\) 接近 0,保留大部分 \(h_{t-1}\) 信息;如果 \(z_t\) 接近 1,保留少量 \(h_{t-1}\) 信息(即遗忘)。
▮▮▮▮⚝ \(z_t\) 控制了当前候选隐藏状态 \(\tilde{h}_t\) 的信息加入量。如果 \(z_t\) 接近 1,加入大部分 \(\tilde{h}_t\) 信息;如果 \(z_t\) 接近 0,加入少量 \(\tilde{h}_t\) 信息。
▮▮▮▮⚝ 这个公式将前一个隐藏状态和当前候选状态进行了加权平均,权重由更新门 \(z_t\) 控制。

总而言之,重置门 \(r_t\) 用于决定如何结合当前输入和前一个隐藏状态来计算新的候选状态,而更新门 \(z_t\) 用于决定保留多少旧状态信息以及加入多少新候选状态信息来得到最终的当前隐藏状态。

这种门控机制允许 GRU 选择性地更新和保留信息,使得梯度可以在时间步上传播得更远,从而有效地捕捉长期依赖,就像 LSTM 那样。

6.4 GRU 与 LSTM 的对比

本节从结构、参数数量、计算效率和性能等方面对比 GRU 和 LSTM。

GRU 和 LSTM 都是为了解决标准 RNN 的长期依赖问题而设计的门控 RNN 架构。它们都通过门控机制来控制信息在时间步之间的流动。然而,它们在结构和复杂性上存在一些显著差异:

门的数量与类型
▮▮▮▮⚝ LSTM 有三个门:遗忘门 (Forget Gate)、输入门 (Input Gate) 和输出门 (Output Gate)。
▮▮▮▮⚝ GRU 有两个门:更新门 (Update Gate) 和重置门 (Reset Gate)。更新门结合了 LSTM 的遗忘门和输入门的功能。

状态向量
▮▮▮▮⚝ LSTM 维护两个状态向量:细胞状态 (Cell State) \(c_t\) 和隐藏状态 (Hidden State) \(h_t\)。细胞状态负责存储长期信息,隐藏状态是基于细胞状态和输出门计算得出的,通常也作为当前时间步的输出。
▮▮▮▮⚝ GRU 只维护一个状态向量,即隐藏状态 (Hidden State) \(h_t\)。这个状态向量同时承担了 LSTM 中细胞状态和隐藏状态的功能。

参数数量
▮▮▮▮⚝ 由于 GRU 门和状态向量更少,其参数数量通常少于具有相同隐藏单元维度的 LSTM。
▮▮▮▮⚝ 对于 LSTM,需要计算遗忘门、输入门、候选细胞状态、输出门和最终隐藏状态,每个计算都涉及与输入和前一个隐藏状态的线性变换,例如遗忘门:\(f_t = \sigma(W_f x_t + U_f h_{t-1} + b_f)\)。共有四个这样的主要计算。
▮▮▮▮⚝ 对于 GRU,需要计算更新门、重置门和候选隐藏状态,共有三个这样的主要计算。
▮▮▮▮⚝ 因此,GRU 通常比 LSTM 更“轻量级”。

计算效率
▮▮▮▮⚝ 由于参数较少且结构更简单,GRU 的计算通常比 LSTM 快。这在处理大规模数据集或需要快速训练时是一个优势。

性能表现
▮▮▮▮⚝ 在许多任务上,GRU 和 LSTM 的性能非常接近,甚至不相上下。一些研究表明,对于某些数据集,GRU 可能表现稍好,而对于另一些数据集,LSTM 可能更优。
▮▮▮▮⚝ 选择使用 GRU 还是 LSTM 通常取决于具体的任务和数据集。GRU 的简化结构有时可以使其更容易收敛,尤其是在数据量不是非常大的情况下。

理解与解释
▮▮▮▮⚝ GRU 的结构相对简单,可能比 LSTM 更容易理解。

总结一下 GRU 和 LSTM 的主要区别:

特性LSTMGRU
门的数量3 (遗忘门、输入门、输出门)2 (更新门、重置门)
状态数量2 (细胞状态 \(c_t\), 隐藏状态 \(h_t\))1 (隐藏状态 \(h_t\))
参数数量较多较少
计算效率较低较高
复杂性较高较低
性能通常与 GRU 相当,或略有不同通常与 LSTM 相当,或略有不同
细胞状态有独立的细胞状态无独立的细胞状态,隐藏状态承担该功能
输出计算隐藏状态 \(h_t\) 由细胞状态 \(c_t\) 计算隐藏状态 \(h_t\) 直接更新并作为输出状态

在实际应用中,GRU 和 LSTM 都是处理序列数据的强大工具。选择哪一个通常需要通过实验来确定,或者基于对计算资源和模型复杂性的考虑。对于新手入门,GRU 由于其相对简单的结构,可能是一个不错的起点。

7. 高级 RNN 架构与变体

总结: 本章介绍在基础循环神经网络 (RNN)、长短期记忆网络 (LSTM)、门控循环单元 (GRU) 之上的更复杂或更高效的 RNN 架构,以应对不同的任务需求。我们将探讨如何通过结构改进来增强模型处理序列信息的能力。

7.1 双向循环神经网络 (Bidirectional RNN - BiRNN)

总结: 本节介绍双向循环神经网络 (BiRNN) 如何通过结合正向和反向处理来捕捉序列中的上下文信息。

在很多序列任务中,仅仅考虑当前时间步及之前的信息是不够的。例如,在命名实体识别 (Named Entity Recognition - NER) 任务中,判断一个词是否是人名,可能需要知道它后面的词。考虑句子 "Washington is the capital of the United States." 要确定 "Washington" 是指人名还是地名,后面的词 "capital" 提供了重要的上下文信息。

标准的 RNN (包括 LSTM 和 GRU) 只能处理历史信息,将输入序列从左到右或从前向后依次处理。它们在时间 \(t\) 的隐藏状态 \(h_t\) 仅依赖于时间 \(t\) 的输入 \(x_t\) 以及时间 \(t-1\) 的隐藏状态 \(h_{t-1}\)。这种单向的处理方式无法利用未来时间步的信息。

双向循环神经网络 (BiRNN) 应运而生,它通过引入一个额外的反向 RNN 层来解决这个问题。BiRNN 由两个独立的 RNN 层组成:

① 一个正向层,按照时间顺序 \(t=1, 2, \dots, T\) 处理输入序列,计算隐藏状态 \(\vec{h}_t\)。
② 一个反向层,按照时间逆序 \(t=T, T-1, \dots, 1\) 处理输入序列,计算隐藏状态 \(\overleftarrow{h}_t\)。

在任何一个时间步 \(t\),BiRNN 的输出或用于后续层的表示是将正向隐藏状态 \(\vec{h}_t\) 和反向隐藏状态 \(\overleftarrow{h}_t\) 进行拼接(Concatenation)或者求和等操作得到。通常是拼接:

\[ h_t = [\vec{h}_t; \overleftarrow{h}_t] \]

这里的 \([\cdot; \cdot]\) 表示向量拼接操作。最终的输出 \(y_t\) 可以基于 \(h_t\) 计算。

BiRNN 的计算过程可以概括为:

① 正向层计算:
\[ \vec{h}_t = f(\vec{W}_{hh}\vec{h}_{t-1} + \vec{W}_{xh}x_t + \vec{b}_h) \]
其中 \(f\) 是激活函数 (例如 Tanh 或 ReLU),\(\vec{W}_{hh}\) 是隐藏层到隐藏层的权重,\(\vec{W}_{xh}\) 是输入到隐藏层的权重,\(\vec{b}_h\) 是偏置项。

② 反向层计算:
\[ \overleftarrow{h}_t = f(\overleftarrow{W}_{hh}\overleftarrow{h}_{t+1} + \overleftarrow{W}_{xh}x_t + \overleftarrow{b}_h) \]
注意反向层的计算依赖于时间 \(t+1\) 的隐藏状态 \(\overleftarrow{h}_{t+1}\) 和时间 \(t\) 的输入 \(x_t\)。反向层的权重矩阵 \(\overleftarrow{W}_{hh}, \overleftarrow{W}_{xh}\) 和偏置 \(\overleftarrow{b}_h\) 与正向层的权重 \(\vec{W}_{hh}, \vec{W}_{xh}\) 和偏置 \(\vec{b}_h\) 是独立的,它们是两套不同的参数。

③ 合并隐藏状态:
\[ h_t = [\vec{h}_t; \overleftarrow{h}_t] \]

④ 输出层计算:
\[ y_t = g(W_{hy}h_t + b_y) \]
其中 \(g\) 是输出层的激活函数,\(W_{hy}\) 是隐藏层到输出层的权重,\(b_y\) 是偏置项。对于分类任务,\(g\) 可能是 Softmax;对于回归任务,可能是线性函数。

BiRNN 的主要优势在于它能够在每个时间步同时利用过去和未来的信息,这对于需要理解完整上下文才能做出预测或决策的任务至关重要,比如序列标注 (Sequence Tagging) 或机器翻译 (Machine Translation) 中的编码器部分。

然而,BiRNN 也存在一些局限性。最显著的是,在处理需要在线实时预测(例如语音识别中的实时转录)的任务时,BiRNN 需要等待整个序列输入完毕才能计算反向层的状态,这会导致延迟。

7.2 深度循环神经网络 (Deep RNN / Stacked RNN)

总结: 本节讲解如何堆叠多个 RNN 层来构建更深层次的模型,以学习更复杂的特征表示。

类似于前馈神经网络可以通过堆叠多个隐藏层来增加模型的容量和学习更抽象的特征,循环神经网络也可以通过堆叠多个 RNN 层来构建深度模型,也称为堆叠循环神经网络 (Stacked RNN)。

在一个深度 RNN 中,第 \(l\) 层的隐藏状态计算依赖于当前时间步的输入(如果是第一层)或下一层的输入(如果是更高层),以及同一层前一时间步的隐藏状态。具体来说,对于第 \(l\) 层 (其中 \(l > 1\)) 在时间步 \(t\) 的计算:

\[ h_t^{(l)} = f(W_{h^{(l)}h^{(l)}}h_{t-1}^{(l)} + W_{x^{(l)}h^{(l)}}h_t^{(l-1)} + b_h^{(l)}) \]

这里:
⚝ \(h_t^{(l)}\) 是第 \(l\) 层在时间步 \(t\) 的隐藏状态。
⚝ \(h_{t-1}^{(l)}\) 是第 \(l\) 层在时间步 \(t-1\) 的隐藏状态。
⚝ \(h_t^{(l-1)}\) 是第 \(l-1\) 层(即下一层)在时间步 \(t\) 的隐藏状态。在堆叠 RNN 中,第 \(l-1\) 层的输出 (通常是其隐藏状态) 成为第 \(l\) 层的输入。
⚝ \(W_{h^{(l)}h^{(l)}}\)、\(W_{x^{(l)}h^{(l)}}\)、\(b_h^{(l)}\) 是第 \(l\) 层的参数。
⚝ \(f\) 是激活函数。

对于第一层 (\(l=1\)),其输入是原始的输入序列 \(x_t\),所以计算公式为:
\[ h_t^{(1)} = f(W_{h^{(1)}h^{(1)}}h_{t-1}^{(1)} + W_{xh^{(1)}}x_t + b_h^{(1)}) \]

然后,第 1 层的输出 \(h_t^{(1)}\) 作为第 2 层在时间步 \(t\) 的输入,依此类推,直到最后一层。最后一层的隐藏状态或输出用于最终的任务预测。

堆叠 RNN 的优势在于:

增强表示能力: 堆叠多层使得网络能够学习到输入序列在不同抽象层次上的特征表示。例如,在文本处理中,底层可能学习词汇或短语的局部模式,而高层可能学习句子或段落的更宏观的结构。
提升模型容量: 增加层数增加了模型的总参数数量,从而提高了模型的容量,使其能够学习更复杂的映射关系。

然而,堆叠 RNN 也面临一些挑战:

训练难度增加: 随着层数的增加,训练过程可能变得更加困难,更容易出现梯度问题 (尽管使用 LSTM 或 GRU 单元可以在一定程度上缓解)。
计算成本增加: 更多的层意味着更多的计算,训练和推断时间都会增加。

在实践中,堆叠的 LSTM 或 GRU 网络比堆叠的 vanilla RNN 更常见,因为 LSTM 和 GRU 更善于处理长期依赖并缓解梯度问题。例如,一个常见的架构可能是堆叠 2-4 层的 LSTM 或 GRU。

7.3 编码器-解码器模型 (Encoder-Decoder Model)

总结: 本节介绍编码器-解码器 (Encoder-Decoder) 架构,它常用于序列到序列 (Sequence-to-Sequence) 任务,其中一个 RNN 编码输入序列,另一个 RNN 解码输出序列。

很多实际问题涉及将一个序列转换成另一个序列,而且输入序列和输出序列的长度可能不同。这类任务被称为序列到序列 (Sequence-to-Sequence, Seq2Seq) 任务,典型的例子包括:

机器翻译 (Machine Translation): 将一种语言的句子翻译成另一种语言的句子。输入是源语言句子,输出是目标语言句子。
文本摘要 (Text Summarization): 将长篇文本压缩成简短摘要。输入是长文本序列,输出是摘要序列。
语音识别 (Speech Recognition): 将声学信号序列转换成文本序列。输入是语音信号序列,输出是文本序列。
对话系统 (Dialogue Systems): 输入是用户的话语序列,输出是系统的回复序列。

对于这类输入和输出长度可变的任务,传统的单向或双向 RNN 模型难以直接应用。编码器-解码器模型是解决这类问题的经典框架。它由两个主要的组成部分构成:一个编码器 (Encoder) 和一个解码器 (Decoder)。两者通常都是循环神经网络 (RNN),可以是 vanilla RNN、LSTM 或 GRU。

7.3.1 编码器 (Encoder)

总结: 讲解编码器的作用:将变长输入序列压缩成固定长度的上下文向量。

编码器 RNN 负责处理输入序列 \(x = (x_1, x_2, \dots, x_{T_x})\),其中 \(T_x\) 是输入序列的长度。它按照时间步依次读取输入序列的元素,并更新其隐藏状态。编码器处理完整个输入序列后,其最终的隐藏状态(或者有时是所有时间步隐藏状态的某种组合)被认为是整个输入序列的一个“编码”或“上下文向量 (Context Vector)”。

计算过程如下:
⚝ 在时间步 \(t\),编码器接收输入 \(x_t\) 和前一时间步的隐藏状态 \(h_{t-1}^{\text{enc}}\)。
⚝ 计算当前时间步的隐藏状态 \(h_t^{\text{enc}}\):
\[ h_t^{\text{enc}} = f_{\text{enc}}(h_{t-1}^{\text{enc}}, x_t) \]
这里的 \(f_{\text{enc}}\) 是编码器 RNN 单元的非线性函数(例如,LSTM 或 GRU 的内部计算)。
⚝ 这个过程重复进行,直到处理完最后一个输入元素 \(x_{T_x}\)。
⚝ 编码器的最终隐藏状态 \(h_{T_x}^{\text{enc}}\) 通常被用作输入序列的上下文向量 \(c\)。

\[ c = h_{T_x}^{\text{enc}} \]

这个上下文向量 \(c\) 是一个固定维度的向量,它试图捕获整个输入序列的信息。编码器的目标是将变长的输入序列压缩成这个固定长度的向量表示。

7.3.2 解码器 (Decoder)

总结: 讲解解码器的作用:根据上下文向量和之前的输出生成输出序列。

解码器 RNN 负责根据编码器生成的上下文向量 \(c\) 来生成目标序列 \(y = (y_1, y_2, \dots, y_{T_y})\),其中 \(T_y\) 是输出序列的长度,它可能与 \(T_x\) 不同。解码器是一个条件生成模型,它在生成当前时间步的输出时,会考虑上下文向量 \(c\) 以及之前已经生成的输出 \(y_1, y_2, \dots, y_{t-1}\)。

解码器的计算过程如下:
⚝ 解码器的初始隐藏状态 \(h_0^{\text{dec}}\) 通常由编码器的最终隐藏状态 \(c\) 初始化。
⚝ 在时间步 \(t\) (从 1 开始),解码器接收前一时间步生成的输出 \(y_{t-1}\) (在第一个时间步,通常是一个特殊的起始标记,例如 <SOS> (Start Of Sequence)) 和前一时间步的隐藏状态 \(h_{t-1}^{\text{dec}}\)。
⚝ 解码器计算当前时间步的隐藏状态 \(h_t^{\text{dec}}\):
\[ h_t^{\text{dec}} = f_{\text{dec}}(h_{t-1}^{\text{dec}}, y_{t-1}, c) \]
这里的 \(f_{\text{dec}}\) 是解码器 RNN 单元的非线性函数。注意,上下文向量 \(c\) 通常会作为输入或通过其他方式参与解码器每个时间步的计算。
⚝ 基于当前时间步的隐藏状态 \(h_t^{\text{dec}}\),解码器预测当前时间步的输出 \(y_t\)。这通常涉及一个线性层和一个 Softmax 层来得到输出词汇表上的概率分布:
\[ P(y_t | y_{ 这里的 \(g_{\text{dec}}\) 是一个转换函数,输出一个概率分布。\(y_{ ⚝ 在训练时,\(y_{t-1}\) 通常是真实的上一个词 (Teacher Forcing)。在推理时,\(y_{t-1}\) 是模型在前一步预测的词。
⚝ 这个过程重复进行,直到生成一个特殊的结束标记 (例如 <EOS> (End Of Sequence)) 或者达到预设的最大长度。

编码器-解码器模型通过将一个序列压缩成固定长度的上下文向量来连接两个 RNN,这在一定程度上是有效的。然而,这种架构的一个主要瓶颈在于,不论输入序列有多长,它都被强制压缩成一个固定大小的向量 \(c\)。对于非常长的输入序列,这个向量可能无法完全捕捉序列中的所有重要信息,导致在解码长序列时性能下降,特别是早期的输入信息可能会在多次非线性变换后丢失。这被称为上下文向量瓶颈 (Context Vector Bottleneck) 问题。

7.4 注意力机制 (Attention Mechanism)

总结: 本节引入注意力机制 (Attention Mechanism),解释它是如何解决 Encoder-Decoder 模型在处理长序列时上下文向量瓶颈的问题,并允许模型关注输入序列的不同部分。

为了解决 Encoder-Decoder 模型中固定长度上下文向量的瓶颈问题,注意力机制被提出并迅速成为序列建模领域的关键技术。注意力机制的核心思想是,在生成输出序列的每个元素时,解码器不是仅仅依赖于一个固定的上下文向量,而是能够“关注”或“加权”输入序列中不同位置的信息。

具体来说,当解码器在生成第 \(t\) 个输出 \(y_t\) 时,它会回顾输入序列的编码器隐藏状态集合 \((h_1^{\text{enc}}, h_2^{\text{enc}}, \dots, h_{T_x}^{\text{enc}})\),并计算一个关于这些状态的加权平均。这个加权平均构成了当前时间步 \(t\) 的动态上下文向量 \(c_t\)。权重(即“注意力权重”)取决于当前解码器的状态 \(h_t^{\text{dec}}\) 与每个编码器状态 \(h_j^{\text{enc}}\) 之间的相关性或相似性。

注意力机制的计算过程如下:

① 计算注意力分数 (Attention Score / Alignment Score):
对于解码器在时间步 \(t\) 的状态 \(h_t^{\text{dec}}\) 和编码器在时间步 \(j\) 的状态 \(h_j^{\text{enc}}\),计算一个分数 \(e_{tj}\),表示生成 \(y_t\) 时,输入 \(x_j\) (由 \(h_j^{\text{enc}}\) 代表) 的重要程度。分数函数 \(score(h_t^{\text{dec}}, h_j^{\text{enc}})\) 可以有多种形式,例如:

点积 (Dot Product): \(e_{tj} = (h_t^{\text{dec}})^T h_j^{\text{enc}}\)
通用点积 (General Dot Product): \(e_{tj} = (h_t^{\text{dec}})^T W_a h_j^{\text{enc}}\) (引入一个可学习的权重矩阵 \(W_a\))
连接 (Concatenation): \(e_{tj} = v_a^T \text{tanh}(W_b [h_t^{\text{dec}}; h_j^{\text{enc}}])\) (引入可学习的参数 \(W_b\) 和 \(v_a\),称为 Additive Attention 或 Bahdanau Attention)

② 计算注意力权重 (Attention Weights):
对计算出的分数进行 Softmax 归一化,得到注意力权重 \(\alpha_{tj}\)。这些权重是非负的,并且对于每个解码时间步 \(t\),所有输入时间步 \(j\) 对应的权重之和为 1:

\[ \alpha_{tj} = \frac{\exp(e_{tj})}{\sum_{k=1}^{T_x} \exp(e_{tk})} \]

③ 计算当前时间步的上下文向量 (Context Vector):
使用注意力权重对编码器隐藏状态进行加权平均,得到时间步 \(t\) 的上下文向量 \(c_t\):

\[ c_t = \sum_{j=1}^{T_x} \alpha_{tj} h_j^{\text{enc}} \]

④ 解码器生成输出:
解码器在生成 \(y_t\) 时,会结合当前隐藏状态 \(h_t^{\text{dec}}\) 和动态计算出的上下文向量 \(c_t\)。具体的结合方式 vary,一种常见的方式是将两者拼接后经过一个线性层和 Softmax 层:

\[ y_t \sim P(y_t | y_{

通过注意力机制,解码器在生成每个输出词时,都可以回顾整个输入序列,并根据当前的解码状态,有选择性地关注输入序列中最重要的部分。这极大地缓解了固定长度上下文向量的瓶颈问题,使得模型能够更好地处理长序列,并在机器翻译等任务上取得了显著的性能提升。注意力机制使得模型变得更加可解释,因为我们可以通过可视化注意力权重 \(\alpha_{tj}\) 来观察模型在生成某个输出词时,关注的是输入序列中的哪些词。例如,在机器翻译中,可以将源语言词和目标语言词之间的注意力权重绘制成图,通常可以看到对角线上的高权重,表明模型正在对齐源语言和目标语言的对应词。

注意力机制是一个非常强大的概念,它不仅应用于 RNNs 中,后来更是成为了 Transformer 模型 (将在后续章节讨论) 的核心组成部分。

7.5 其他 RNN 变体简述

总结: 简要介绍其他一些 RNN 相关的架构或改进,如 Clockwork RNN、Echo State Network 等。

除了 BiRNN, Deep RNN, Encoder-Decoder with Attention 这些主流和重要的变体外,研究人员还提出了许多其他的 RNN 架构或改进,以解决特定的问题或探索不同的计算范式。本节简要介绍其中几个:

Clockwork RNN (CW-RNN):
CW-RNN 是一种尝试解决标准 RNN 长期依赖问题同时保持计算效率的模型。它的核心思想是将隐藏层神经元分成不同的组 (模块),每个组以不同的时钟频率更新。频率高的组处理更快的变化,频率低的组处理更慢的变化或长期信息。不同组之间只有高频组可以连接到低频组,低频组不能直接连接到高频组,这形成了一个层级结构。这种结构旨在通过强制不同的时间尺度来更好地捕捉多尺度的时序信息。

Echo State Network (ESN):
ESN 是一种水库计算 (Reservoir Computing) 模型。与传统 RNN 需要训练所有权重不同,ESN 的大部分循环连接权重 (称为“水库”) 是随机生成且固定的。只有输出层的权重是可训练的。水库的内部随机连接和非线性激活使得输入序列在水库中产生复杂的、高维的动态响应(即“回声状态”)。训练时,只需将这些回声状态映射到期望的输出。ESN 的主要优势在于训练速度快,因为它只需要训练一个简单的线性输出层,但其表示能力受到水库固定结构的限制。

Independently Recurrent Neural Network (IndRNN):
IndRNN 旨在解决传统 RNN 中梯度消失/爆炸问题,并允许构建更深的循环网络。它通过让每个神经元独立地接收其自身的循环输入,而不是像传统 RNN 那样将所有神经元的输出通过一个共享的权重矩阵进行转换。循环连接被解耦,每个神经元有一个独立的循环权重。这使得梯度可以在时间步上更稳定地传播。同时,IndRNN 可以在层间进行全连接或卷积连接,从而构建深层模型。

Recurrent Highway Network (RHN):
RHN 将 Highway Networks 的思想引入到 RNN 中。Highway Networks 使用门控机制来控制信息在层间的流动,允许信息直接通过 (类似残差连接)。RHN 将这种门控机制应用于循环连接,允许信息在时间步之间“畅通无阻”地传递,从而有助于学习长期依赖。

这些变体各有特点,有的侧重于解决梯度问题,有的侧重于提高计算效率,有的则探索了不同的网络结构或训练范式。虽然在大型序列任务中,LSTM、GRU 以及后来的 Transformer 成为了主流,但对这些变体的研究也推动了我们对循环网络的理解和发展。

8. 循环神经网络的应用实践

本章总结: 本章将深入探讨循环神经网络(RNNs)在解决实际问题中的应用,特别是其在自然语言处理(NLP)和时间序列分析(Time Series Analysis)领域的强大能力。通过具体的案例研究和应用场景,本章旨在帮助读者理解如何将前几章学到的 RNN 理论知识应用于实践,并展示 RNNs 在处理序列数据方面的广泛性和有效性。我们将覆盖语言模型、机器翻译、文本生成、情感分析、命名实体识别、时间序列预测、异常检测以及语音识别等经典应用,并简要提及其他相关领域。

8.1 自然语言处理 (NLP)

本节总结: 自然语言处理(NLP)是 RNNs 最重要的应用领域之一。由于自然语言天然具备序列结构,词语或字符的含义和语法依赖于其在句子或段落中的上下文,RNNs 凭借其处理序列信息的能力,在 NLP 任务中取得了巨大的成功。本节将详细介绍 RNNs 在几种核心 NLP 任务中的应用。

8.1.1 语言模型 (Language Modeling)

本小节总结: 语言模型(Language Modeling)的目标是计算一个词序列的概率,或给定前一个词序列,预测下一个词出现的概率。这是一个典型的序列预测任务,RNNs 非常适合解决这类问题。

语言模型是许多其他 NLP 任务的基础,例如机器翻译、语音识别和文本生成。一个好的语言模型能够捕捉语言的语法和语义结构。

使用 RNN 构建语言模型的思想是:在时间步 \( t \),给定之前的词 \( w_1, w_2, \dots, w_{t-1} \),模型预测下一个词 \( w_t \) 的概率分布 \( P(w_t | w_1, \dots, w_{t-1}) \)。

具体实现时,通常将每个词 \( w_i \) 表示为一个向量(例如,通过词嵌入(Word Embedding))。在时间步 \( t-1 \),RNN 接收词向量 \( x_{t-1} \) 和前一个隐藏状态 \( h_{t-2} \),计算得到当前隐藏状态 \( h_{t-1} \)。然后,通过一个线性层和 Softmax 函数,将 \( h_{t-1} \) 映射到词汇表(Vocabulary)大小的向量,表示下一个词 \( w_t \) 在词汇表中每个词上的概率分布。

\[ P(w_t | w_1, \dots, w_{t-1}) = \text{softmax}(W_y h_{t-1} + b_y) \]

其中 \( W_y \) 和 \( b_y \) 是输出层的权重和偏置。

训练语言模型时,输入通常是文本语料库中的一个词序列。损失函数(Loss Function)通常使用交叉熵(Cross-Entropy),衡量模型预测的概率分布与真实下一个词的独热向量(One-Hot Vector)之间的差异。目标是最小化所有时间步上的交叉熵损失之和。

评估语言模型的常用指标是困惑度(Perplexity)。困惑度衡量的是模型预测下一个词的不确定性,困惑度越低,模型性能越好。困惑度可以理解为每个词平均有多少个“可能性”的下一个词。

RNNs(尤其是 LSTM 和 GRU)由于能够有效捕捉长距离依赖(Long-Distance Dependencies),在语言模型任务上表现出色,显著优于传统的 \( N \)-gram 模型。

8.1.2 机器翻译 (Machine Translation)

本小节总结: 机器翻译(Machine Translation)是将一种语言(源语言,Source Language)的句子翻译成另一种语言(目标语言,Target Language)的句子。这是一个经典的序列到序列(Sequence-to-Sequence, Seq2Seq)任务,Encoder-Decoder 模型是解决这类问题的常用架构,而 RNNs 是构建 Encoder-Decoder 模型的核心组件。

Encoder-Decoder 模型通常由两个 RNN 组成:
⚝ 编码器(Encoder):读取源语言句子,逐步处理每个词,并将整个句子的信息压缩成一个固定长度的向量,称为上下文向量(Context Vector)或隐藏状态(Hidden State)。这个向量被认为是源句子的语义表示。
⚝ 解码器(Decoder):接收编码器输出的上下文向量作为初始状态或输入,然后逐步生成目标语言句子。在生成每个词时,解码器通常会考虑之前已经生成的词和编码器提供的上下文信息。

原始的 Encoder-Decoder 模型中,编码器将整个输入序列压缩成一个固定长度的向量,这对于长序列来说是一个信息瓶颈(Information Bottleneck),解码器可能难以从这一个向量中获取生成长输出序列所需的全部信息。

为了解决这个问题,注意力机制(Attention Mechanism)被引入到 Encoder-Decoder 模型中。Attention 机制允许解码器在生成目标序列的每个词时,动态地“关注”到源序列的不同部分。它通过计算源序列每个隐藏状态与当前解码器状态之间的相关性得分,然后对源序列的隐藏状态进行加权求和,得到一个动态的上下文向量。这个动态上下文向量与解码器的当前状态一起用于预测下一个目标词。Attention 机制极大地提升了机器翻译模型的性能,特别是对于长句子。

尽管后来 Transformer 模型在机器翻译任务上取得了更好的结果,但基于 RNN 的 Encoder-Decoder 和 Attention 模型是序列到序列学习领域的一个里程碑,为后续模型奠定了基础。

8.1.3 文本生成 (Text Generation)

本小节总结: 文本生成(Text Generation)任务旨在让模型根据给定的输入(可以是文本片段、主题、风格等)生成新的、连贯且有意义的文本序列。RNNs,特别是 LSTM 和 GRU,在文本生成方面非常有效。

训练 RNN 进行文本生成通常采用语言模型的训练方式:给定一个文本语料库,模型学习预测序列中的下一个字符或下一个词。训练完成后,可以使用模型来生成新的文本。

生成过程通常是迭代的:
① 提供一个起始的输入(例如,一个起始词或一个特殊的开始标记)。
② 将当前输入喂给训练好的 RNN 模型。
③ 模型输出一个预测的概率分布,表示下一个词或字符在词汇表上的概率。
④ 从这个概率分布中采样(Sampling)或选择(例如,选择概率最高的词,即贪婪搜索(Greedy Search),或使用束搜索(Beam Search))一个词或字符作为下一个输出。
⑤ 将生成的词或字符作为新的输入,重复步骤 ②-④,直到生成结束标记(End-of-Sequence Token)或达到预设的最大长度。

通过调整采样策略(例如,温度采样(Temperature Sampling)),可以控制生成文本的随机性和创造性。较低的温度值会使模型更倾向于选择高概率的词,生成更保守的文本;较高的温度值会增加低概率词被选中的机会,生成更多样化甚至“奇特”的文本。

RNNs 能够学习文本的结构、语法甚至一定的风格,生成的故事、诗歌、代码等都展示了其强大的序列生成能力。

8.1.4 情感分析 (Sentiment Analysis)

本小节总结: 情感分析(Sentiment Analysis)是一种文本分类任务,目标是判断一段文本(如评论、推文)所表达的情感倾向,例如积极、消极或中立。

对于情感分析这类任务,RNNs 通常被用作特征提取器。模型读取整个输入文本序列,并在处理完最后一个词后,使用最终的隐藏状态(或者所有隐藏状态的某种聚合)作为整个文本序列的表示向量。然后,将这个向量输入到一个全连接层(Fully Connected Layer)和 Softmax 层,输出文本属于各个情感类别的概率。

这种架构是一种典型的 Many-to-One(多对一)RNN 应用:输入是一个序列,输出是一个单一的类别标签。

使用 LSTM 或 GRU 可以帮助模型更好地捕捉文本中的长距离依赖,例如否定词对情感的修饰作用可能出现在距离情感词较远的位置。例如,“这本书 怎么样”,否定词“不”显著改变了对“怎么样”的情感判断。

8.1.5 命名实体识别 (Named Entity Recognition - NER)

本小节总结: 命名实体识别(Named Entity Recognition, NER)是信息提取(Information Extraction)的一个子任务,旨在识别文本中具有特定意义的实体,例如人名、地名、组织机构名、日期等,并标注出它们的类别。这是一个序列标注(Sequence Labeling)任务,即对输入序列中的每一个元素(词或字符)分配一个标签。

对于 NER 任务,通常采用 Many-to-Many(多对多)的 RNN 架构,其中输入序列中的每个词对应输出序列中的一个标签。一个常用的标注体系是 IOB 格式(Inside, Outside, Beginning),例如,“B-PER”表示一个人名实体的开始,“I-PER”表示一个人名实体的内部,“O”表示非实体。

由于命名实体可能跨越多个词,并且实体的识别往往需要考虑词语前后的上下文信息,双向循环神经网络(Bidirectional RNN, BiRNN)在这里显得特别有用。BiRNN 包含一个正向 RNN,按顺序处理输入序列,和一个反向 RNN,按逆序处理输入序列。在每个时间步,两个方向的隐藏状态被拼接起来,形成一个包含当前词前向和后向上下文信息的综合表示。然后,将这个综合表示输入到一个分类器(如一个线性层和 Softmax 层),预测当前词的标签。

\[ h_t = [ \overrightarrow{h_t} ; \overleftarrow{h_t} ] \]
\[ P(label_t | w_1, \dots, w_n) = \text{softmax}(W_y h_t + b_y) \]

其中 \( \overrightarrow{h_t} \) 是正向 RNN 在时间步 \( t \) 的隐藏状态,\( \overleftarrow{h_t} \) 是反向 RNN 在时间步 \( t \) 的隐藏状态,\( [ ; ] \) 表示向量拼接。

BiLSTM 或 BiGRU 是 NER 任务中的标准模型,它们能够有效地利用上下文信息进行精确的实体边界划分和类别判断。

8.2 时间序列分析 (Time Series Analysis)

本节总结: 时间序列分析(Time Series Analysis)涉及对按时间顺序排列的数据点进行分析,以理解其内在模式、趋势、季节性等,并常用于预测未来的值或检测异常。时间序列数据与自然语言一样,具有天然的序列依赖性,因此 RNNs 在该领域也有广泛应用。

8.2.1 时间序列预测 (Time Series Forecasting)

本小节总结: 时间序列预测(Time Series Forecasting)是根据过去观测到的时间序列数据来预测未来一个或多个时间步的值。例如,预测股票价格、天气、销量、流量等。

RNNs 可以很自然地应用于时间序列预测。模型接收过去一系列时间步的观测值作为输入序列,然后预测下一个时间步的值。这可以是一个 Many-to-One(预测单个未来值,使用最后一个隐藏状态进行回归)或 Many-to-Many(预测未来一系列值,解码器结构)的任务。

对于单步预测,RNN 在时间步 \( t \) 接收当前输入 \( x_t \) 和前一时间步的隐藏状态 \( h_{t-1} \),计算得到 \( h_t \)。然后通过一个线性层将 \( h_t \) 映射到预测值 \( \hat{y}_t \)。

\[ \hat{y}_t = W_y h_t + b_y \]

训练时,使用均方误差(Mean Squared Error, MSE)或其他回归任务的损失函数。

对于多步预测,可以使用几种策略:
① 单步预测的迭代应用:预测时间步 \( t+1 \) 的值 \( \hat{y}_{t+1} \),然后将 \( \hat{y}_{t+1} \) 作为时间步 \( t+1 \) 的输入来预测 \( \hat{y}_{t+2} \),依此类推。这种方法可能存在误差累积的问题。
② 使用 Encoder-Decoder 模型:编码器处理输入历史序列,生成上下文向量;解码器接收上下文向量并逐步生成未来序列。这与机器翻译的架构类似。
③ 使用特定的 RNN 架构,例如 Sequence-to-Sequence with Attention,可以进一步提高长序列预测的准确性。

LSTM 和 GRU 在时间序列预测中表现出色,因为它们能够捕获不同时间尺度上的复杂模式和长期依赖关系,例如季节性(Seasonality)和趋势(Trend)。

8.2.2 异常检测 (Anomaly Detection)

本小节总结: 时间序列异常检测(Time Series Anomaly Detection)是指识别时间序列数据中那些与正常模式显著偏离的数据点、子序列或整个序列。这些异常可能表示系统故障、欺诈行为、罕见事件等。

RNNs 可以通过学习时间序列的正常演化模式来检测异常。一种常见的方法是训练一个 RNN 模型来预测时间序列中的下一个值。在预测过程中,如果某个时间步的实际观测值与模型的预测值之间的误差(Prediction Error)非常大,就可能表明该点是一个异常。

具体步骤可以是:
① 使用历史正常数据训练一个 RNN 模型(通常是 LSTM 或 GRU)进行单步或多步预测。
② 在检测阶段,使用训练好的模型对新的时间序列数据进行预测。
③ 计算每个时间步的预测误差,例如 \( e_t = |y_t - \hat{y}_t| \) 或 \( e_t = (y_t - \hat{y}_t)^2 \)。
④ 设定一个阈值(Threshold),如果某个时间步的误差超过这个阈值,则将该点标记为异常。阈值可以根据训练数据上的误差分布来确定。

这种方法基于一个假设:模型在正常数据上能够做出准确预测,但在遇到异常数据时,其预测能力会显著下降,导致误差增大。

RNNs 也能用于学习正常序列的概率分布,然后检测低概率的序列作为异常。例如,训练一个 RNN 语言模型来学习正常的时间序列“语言”,然后计算新序列的困惑度,高困惑度可能指示异常。

8.3 语音识别 (Speech Recognition)

本节总结: 语音识别(Speech Recognition)是将人类的语音信号转换为文本序列的任务。这是一个复杂的序列转换问题,因为语音信号是连续的且长度变化,而对应的文本是离散的字符或词序列。RNNs 在传统的语音识别系统中扮演了重要角色,尤其是在声学模型(Acoustic Model)部分。

传统的语音识别系统通常包含特征提取(如梅尔频率倒谱系数,Mel-Frequency Cepstral Coefficients, MFCCs)、声学模型、发音词典(Pronunciation Dictionary)和语言模型。声学模型负责将声学特征序列映射到音素(Phonemes)或次词单元(Sub-word Units)的概率序列。

RNNs(尤其是 LSTM 和 GRU)由于其处理序列数据的能力,被广泛用于构建声学模型。它们能够捕捉语音信号中的时序依赖和长距离上下文信息,这对于区分相似发音但含义不同的词语至关重要。

一种常用的基于 RNN 的声学模型是使用连接主义时间分类(Connectionist Temporal Classification, CTC)。CTC 允许 RNN 直接预测一个比输入声学特征序列更短的输出标签序列(如音素序列),而无需显式的输入-输出对齐。RNN 处理输入声学特征序列,输出每个时间步属于各个音素的概率分布。CTC 损失函数则通过动态规划(Dynamic Programming)有效地计算给定输入序列下,所有可能的标签序列的概率之和,并优化模型参数以最大化正确标签序列的概率。

另一个方法是使用基于 Attention 的 Encoder-Decoder 模型,直接将声学特征序列映射到文本序列,这通常被称为端到端(End-to-End)语音识别。

虽然 Transformer 和基于卷积的模型如 WaveNet 等在语音识别领域取得了显著进展,但 RNNs(特别是 LSTM 和 GRU)仍然是许多现代语音识别系统中不可或缺的组件,尤其是在处理长语音序列时。

8.4 其他应用领域

本节总结: 除了自然语言处理、时间序列分析和语音识别,RNNs 还被应用于许多其他涉及序列数据的领域,展现了其处理各种类型序列数据的通用性。

⚝ 视频处理(Video Processing):视频可以看作是一系列图像帧的序列。RNNs 可以用于视频理解任务,如视频分类(Video Classification)、行为识别(Action Recognition)和视频描述生成(Video Captioning)。例如,将每一帧的特征输入 RNN,然后使用最终状态或注意力机制来对整个视频进行分类或生成描述。
⚝ 基因序列分析(Gene Sequence Analysis):DNA、RNA 和蛋白质序列都是生物序列。RNNs 可以用于预测基因的功能、识别基因组中的特定区域、分析蛋白质结构等。
⚝ 音乐生成(Music Generation):类似于文本生成,RNNs 可以学习音乐的结构和风格,生成新的旋律或乐曲。输入可以是音符序列、MIDI 数据等。
⚝ 手写识别(Handwriting Recognition):手写轨迹可以表示为一系列坐标点的序列。RNNs 可以学习这些轨迹模式,将其转换为文本。
⚝ 推荐系统(Recommendation Systems):在处理用户的行为序列(如浏览历史、购买记录)时,RNNs 可以用来捕捉用户的兴趣演变,进行更精准的序列推荐。

这些多样的应用充分说明了 RNNs 在处理一切具有时序或顺序结构的数据方面的强大潜力。理解和掌握 RNNs,为解决许多现实世界的复杂问题提供了有力的工具。

9. RNNs 的训练技巧与实践

本章将深入探讨在训练循环神经网络 (Recurrent Neural Networks - RNNs) 模型时不可或缺的各种技术和实践经验。正如任何复杂的模型一样,RNNs 的性能和泛鲁棒性 (robustness) 很大程度上取决于有效的训练策略。从数据的准备到模型的优化和正则化,再到最终的评估,每一个环节都有其独特的挑战和相应的解决方案。本章旨在为读者提供一套实用的工具箱,帮助他们在面对序列数据任务时,能够有效地构建、训练并调优 RNN 模型,从而获得更好的性能和更稳定的训练过程。我们将从基础的数据处理讲起,逐步深入到优化算法、正则化技术,并讨论批量处理的策略以及模型评估的常用指标,最后简要介绍如何在主流深度学习框架中实现这些技术。

9.1 数据预处理 (Data Preprocessing)

在将序列数据输入到 RNN 模型之前,必须进行适当的预处理。序列数据通常具有变长特性,这给模型的批量处理和计算带来了挑战。此外,原始数据需要被转换成模型可以理解的数值形式。本节将介绍针对序列数据的常用预处理方法。

9.1.1 序列填充 (Padding) 与截断 (Truncating)

处理变长序列最常见的方法是填充 (Padding) 和截断 (Truncating)。

填充 (Padding)
▮▮▮▮当训练一个批次 (batch) 的序列数据时,为了使批次中的所有序列具有相同的长度,通常会将较短的序列填充到与批次中最长序列相同的长度,或者填充到一个预设的最大长度。
▮▮▮▮填充通常使用一个特定的值(例如 0)来完成,这个值不应与实际数据中的任何有效值混淆。
▮▮▮▮填充可以在序列的开头(左填充 - Left Padding)或结尾(右填充 - Right Padding)进行。在处理文本数据时,如果 RNN 是从左到右处理序列,右填充更常见;如果使用了双向 RNN,左填充或右填充都可以,但需要注意填充位置与模型结构的一致性。
▮▮▮▮填充后的序列在计算损失时需要进行处理,以避免填充部分对梯度产生影响。这通常通过引入掩码 (Masking) 机制来实现。掩码会告诉模型哪些部分是填充的,不应该参与损失计算或反向传播。

截断 (Truncating)
▮▮▮▮对于长度超过预设最大长度的序列,通常会进行截断 (Truncating),只保留序列的开头或结尾部分。
▮▮▮▮截断的目的是限制序列的最大长度,从而控制模型的计算复杂度和内存消耗。选择保留开头还是结尾取决于任务的需求。例如,对于需要捕获最新信息的任务(如股票预测),可能保留序列的结尾更重要;对于需要理解完整句子的任务,保留开头部分可能更合适,或者直接放弃过长的句子。

9.1.2 向量化 (Vectorization)

机器学习模型处理的是数值,因此原始序列数据需要被转换成数值向量或张量 (tensor)。这个过程称为向量化。

文本数据的向量化
▮▮▮▮对于文本数据,向量化通常包括以下步骤:
▮▮▮▮ⓐ 分词 (Tokenization):将文本分解成独立的单元,如单词、子词或字符。
▮▮▮▮ⓑ 构建词汇表 (Vocabulary):收集数据集中所有不重复的词元 (token),构建一个词汇表,并为每个词元分配一个唯一的整数 ID。
▮▮▮▮ⓒ 整数编码 (Integer Encoding):将文本序列中的每个词元替换为其对应的整数 ID。
▮▮▮▮ⓓ 嵌入 (Embedding):将每个整数 ID 映射到一个低维度的实数向量。这个映射关系可以通过预训练的词嵌入模型(如 Word2Vec, GloVe)获得,或者在训练 RNN 模型的同时学习。嵌入层 (Embedding Layer) 的输出是一个形状为 (batch_size, sequence_length, embedding_dim) 的张量,可以直接作为 RNN 层的输入。

时间序列数据的向量化
▮▮▮▮时间序列数据通常已经是数值形式,但可能需要进行标准化 (Standardization) 或归一化 (Normalization) 以便模型更好地学习。
▮▮▮▮可以将单变量时间序列看作是一个数值序列。多变量时间序列可以看作是每个时间步都有一个特征向量的序列。

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 # 伪代码示例:序列填充和向量化
2 # 假设有一个文本序列列表
3 sequences = [
4 "这是一个短句子",
5 "这是一个非常长的句子,需要被截断或填充"
6 ]
7
8 # 1. 分词和构建词汇表 (简化)
9 vocab = {"<PAD>": 0, "<UNK>": 1, "这": 2, "是": 3, "一": 4, "个": 5, "短": 6, "句": 7, "子": 8, "非": 9, "常": 10, "长": 11, "的": 12, ",": 13, "需": 14, "要": 15, "被": 16, "截": 17, "断": 18, "或": 19, "填": 20, "充": 21}
10 token_sequences = [[word for word in seq] for seq in sequences] # 简单按字符分词
11
12 # 2. 整数编码
13 id_sequences = [[vocab.get(token, vocab["<UNK>"]) for token in tokens] for tokens in token_sequences]
14
15 # 3. 填充 (假设最大长度为 10,右填充)
16 max_len = 10
17 padded_sequences = []
18 for seq in id_sequences:
19 if len(seq) > max_len:
20 # 截断
21 padded_seq = seq[:max_len]
22 else:
23 # 填充
24 padded_seq = seq + [vocab["<PAD>"]] * (max_len - len(seq))
25 padded_sequences.append(padded_seq)
26
27 print("原始序列 IDs:", id_sequences)
28 print("填充/截断后的序列 IDs:", padded_sequences)
29
30 # 4. 嵌入 (概念上,实际实现需要嵌入层)
31 # embedding_dim = 32
32 # embedding_layer = nn.Embedding(len(vocab), embedding_dim)
33 # embedded_input = embedding_layer(torch.tensor(padded_sequences))
34 # print("嵌入后的输入形状:", embedded_input.shape) # 示例形状:(batch_size, max_len, embedding_dim)

9.2 优化器选择与学习率调度

选择合适的优化算法和学习率调度策略对于 RNN 模型的有效训练至关重要。RNN 的训练过程,特别是标准 RNN,由于梯度在时间步上的传播可能导致不稳定的梯度,因此对优化器和学习率比较敏感。

9.2.1 优化算法 (Optimization Algorithms)

传统的随机梯度下降 (Stochastic Gradient Descent - SGD) 及其动量 (Momentum) 和 Nesterov 动量变体是常用的优化器。然而,对于 RNNs,自适应学习率方法通常表现更好。

自适应学习率方法
▮▮▮▮这类方法根据参数的历史梯度信息动态调整每个参数的学习率。
▮▮▮▮ⓐ Adagrad (Adaptive Gradient):根据参数的历史平方梯度累积量来缩放学习率。对于不频繁的参数,学习率较大;对于频繁的参数,学习率较小。这对于处理稀疏数据(如文本中的词嵌入)非常有效。但其缺点是累积平方梯度会单调递增,导致学习率最终变得非常小。
▮▮▮▮ⓑ RMSprop (Root Mean Square Propagation):改进了 Adagrad,使用指数加权移动平均 (Exponentially Weighted Moving Average) 来累积平方梯度,从而避免学习率过快下降。这是Hinton在Coursera课程中提出的一种方法,非常适合 RNNs。
\[ \text{mean\_square}(t) = \beta \cdot \text{mean\_square}(t-1) + (1-\beta) \cdot g_t^2 \\ \theta_{t+1} = \theta_t - \frac{\eta}{\sqrt{\text{mean\_square}(t) + \epsilon}} g_t \]
▮▮▮▮其中 \( g_t \) 是当前时间步的梯度,\( \beta \) 是衰减率,\( \eta \) 是全局学习率,\( \epsilon \) 是为了数值稳定而添加的小常数。
▮▮▮▮ⓒ Adam (Adaptive Moment Estimation):结合了 Momentum 和 RMSprop 的思想。它同时计算梯度的一阶矩(均值)和二阶矩(方差),并对它们进行偏差修正。Adam 通常是训练深度学习模型(包括 RNNs)的首选优化器,因为它在实践中表现良好且易于使用。
\[ m_t = \beta_1 m_{t-1} + (1-\beta_1) g_t \\ v_t = \beta_2 v_{t-1} + (1-\beta_2) g_t^2 \\ \hat{m}_t = m_t / (1 - \beta_1^t) \quad (\text{Bias correction for first moment}) \\ \hat{v}_t = v_t / (1 - \beta_2^t) \quad (\text{Bias correction for second moment}) \\ \theta_{t+1} = \theta_t - \frac{\eta}{\sqrt{\hat{v}_t} + \epsilon} \hat{m}_t \]
▮▮▮▮其中 \( m_t \) 和 \( v_t \) 分别是梯度的一阶矩和二阶矩的指数加权移动平均,\( \beta_1 \) 和 \( \beta_2 \) 是衰减率,\( \eta \) 是学习率,\( \epsilon \) 是小常数,\( t \) 是时间步(或迭代次数)。

在实践中,Adam 和 RMSprop 是训练 RNNs 最常用的优化器。它们能够更好地处理 RNN 训练过程中可能出现的复杂梯度模式。

9.2.2 学习率调度 (Learning Rate Scheduling)

学习率调度是指在训练过程中动态调整学习率的策略。在训练初期使用较高的学习率可以加速收敛,而在训练后期逐渐降低学习率有助于模型更好地在损失景观 (loss landscape) 的局部最优解附近收敛,并提高模型的稳定性。

常见的学习率调度策略
▮▮▮▮ⓑ 步进衰减 (Step Decay):每经过固定的 epoch (周期) 数量或训练步数,学习率乘以一个固定的衰减因子(例如 0.1)。这是最简单也常用的一种策略。
▮▮▮▮ⓒ 指数衰减 (Exponential Decay):学习率按照指数曲线衰减。例如,学习率 \( \eta_t = \eta_0 \cdot \gamma^t \),其中 \( \eta_0 \) 是初始学习率,\( \gamma \) 是衰减率(\( 0 < \gamma < 1 \)),\( t \) 是训练步数。
▮▮▮▮ⓓ 余弦退火 (Cosine Annealing):学习率按照余弦函数周期性地衰减和回升。这种策略有时能帮助模型跳出局部最优解,找到更好的解。
▮▮▮▮ⓔ 基于性能的调度 (ReduceLROnPlateau):当监控指标(如验证集上的损失或准确率)在一定数量的 epoch 内没有改善时,学习率就会降低。这种策略非常实用,因为它直接基于模型的表现来调整学习率。

选择哪种调度策略以及具体的参数(如初始学习率、衰减因子、衰减间隔)通常需要通过实验来确定。

9.3 正则化 (Regularization) 技术

正则化是防止模型在训练数据上过拟合 (Overfitting) 的重要手段,特别是在模型参数较多或训练数据较少的情况下。对于 RNNs 而言,过拟合可能导致模型在未见过的时间步或序列上表现不佳。

9.3.1 L1/L2 正则化

L1 和 L2 正则化通过在损失函数中添加模型参数的范数 (norm) 来惩罚过大的权重。
\[ \text{Total Loss} = \text{Original Loss} + \lambda \cdot R(W) \]
▮▮▮▮其中 \( R(W) \) 可以是参数 \( W \) 的 L1 范数(\( \sum |w_i| \),促使权重稀疏)或 L2 范数(\( \sum w_i^2 \),促使权重接近于零但不完全为零)。\( \lambda \) 是正则化强度超参数。
▮▮▮▮在 RNN 中,L1/L2 正则化可以应用于输入到隐藏层、隐藏层到隐藏层(循环连接)以及隐藏层到输出层的权重矩阵。

9.3.2 Dropout (丢弃法)

Dropout 是一种简单但非常有效的正则化技术。在训练过程中,它以概率 \( p \) 随机地“丢弃”(即将其激活值设为零)隐藏层或可见层中的神经元。这可以被视为在每次训练迭代时训练一个不同的子网络,从而使模型对神经元的特定组合不那么敏感,增强了模型的鲁棒性。

在 RNN 中的 Dropout 应用
▮▮▮▮将 Dropout 应用于 RNN 需要更谨慎,因为它会影响序列信息的流动。
▮▮▮▮ⓐ 输入 Dropout:在将输入序列传递给 RNN 层之前,对输入序列的每个时间步应用相同的 Dropout 掩码。这会随机丢弃输入特征,帮助模型对输入噪声更鲁棒。
▮▮▮▮ⓑ 输出 Dropout:在 RNN 层的输出传递给下一层(或其他任务相关的层,如分类层)之前,应用 Dropout。
▮▮▮▮ⓒ 循环连接 Dropout (Recurrent Dropout):将 Dropout 应用于 RNN 单元内部的循环连接。这是防止 RNN 过拟合的关键部分。然而,直接在每个时间步的循环连接上独立应用 Dropout 会破坏 RNN 的记忆能力,因为随机丢弃的模式在时间步之间变化,引入了噪声。
▮▮▮▮为了解决这个问题,提出了变分 Dropout (Variational Dropout) 或称为循环 Dropout (Recurrent Dropout) 的改进方法。这种方法在整个序列的处理过程中,对同一个 RNN 层(特别是循环连接)使用相同的 Dropout 掩码。这意味着如果一个神经元在序列的第一个时间步被丢弃,它在所有后续时间步的循环计算中也会被丢弃(对于该特定序列)。这保留了 RNN 的记忆能力,同时提供了有效的正则化。现代深度学习框架中的 RNN 层实现通常会内置或支持这种循环 Dropout。

9.3.3 其他正则化方法

还有一些其他正则化技术也可以用于 RNNs:
提前停止 (Early Stopping):在训练过程中监控模型在验证集上的性能。如果在连续几个 epoch 内验证集性能不再提升,则停止训练。这是最简单实用的正则化手段之一。
权重衰减 (Weight Decay):通常与 L2 正则化等价,在优化器更新参数时,根据权重的大小按比例减小权重。

结合使用多种正则化技术通常能获得更好的效果。选择哪种正则化技术及其超参数(如 Dropout 比率、正则化强度)需要通过实验和交叉验证来确定。

9.4 批量处理 (Batching) 策略

批量处理是利用硬件并行计算能力、提高训练效率的标准方法。然而,序列数据的变长特性使得批量处理变得复杂。

9.4.1 静态批量 (Static Batching)

原理
▮▮▮▮静态批量是最直接的批量处理方法。在一个批次中,所有序列都被填充到该批次中最长序列的长度,或者填充到整个数据集中所有序列的最大长度。
▮▮▮▮然后将填充后的张量输入到模型进行并行计算。

优点
▮▮▮▮实现简单。

缺点
▮▮▮▮如果批次中序列长度差异很大,会导致大量的填充,造成计算资源的浪费和效率低下。填充部分也会增加计算,尽管通常会通过掩码机制避免其影响损失和梯度。

9.4.2 动态批量 (Dynamic Batching) 或分桶 (Bucketing)

原理
▮▮▮▮动态批量或分桶策略旨在解决静态批量中填充过多导致效率低下的问题。
▮▮▮▮首先,根据序列长度将数据集划分为多个“桶” (buckets)。
▮▮▮▮在训练时,从同一个桶中采样数据来组成批次。这样,一个批次内的序列长度差异就会比较小,需要的填充量也更少。
▮▮▮▮或者,在每个训练步动态地构建批次,将长度相似的序列组合在一起。

优点
▮▮▮▮显著减少填充量,提高计算效率,尤其是在序列长度分布范围很大的情况下。
▮▮▮▮减少内存消耗。

缺点
▮▮▮▮实现相对复杂,需要额外的数据组织和管理逻辑。
▮▮▮▮需要仔细设计分桶策略,以平衡桶的数量和每个桶内长度的差异。

在实际应用中,动态批量是处理变长序列、提高 RNN 训练效率的常用且推荐的方法。许多深度学习框架和数据加载库提供了实现动态批量的工具或示例。

9.5 模型评估指标

模型评估是衡量模型性能和比较不同模型效果的关键步骤。对于 RNNs 应用于各种序列任务,需要使用不同的评估指标。

9.5.1 语言模型 (Language Modeling)

困惑度 (Perplexity)
▮▮▮▮困惑度是衡量语言模型好坏的常用指标,尤其是在评估模型的生成能力时。
▮▮▮▮它的定义是模型在测试集上预测下一个词时的平均分支系数 (average branching factor)。直观地说,困惑度越低,表示模型对测试数据的建模能力越强,或者说在预测下一个词时越“不困惑”。
\[ \text{Perplexity} = P(w_1, w_2, \dots, w_N)^{-\frac{1}{N}} = \sqrt[N]{\frac{1}{P(w_1, w_2, \dots, w_N)}} \]
▮▮▮▮假设模型预测下一个词是基于之前的词,即 \( P(w_1, w_2, \dots, w_N) = \prod_{i=1}^N P(w_i | w_1, \dots, w_{i-1}) \)。
\[ \text{Perplexity} = \sqrt[N]{\frac{1}{\prod_{i=1}^N P(w_i | w_1, \dots, w_{i-1})}} = \exp\left( -\frac{1}{N} \sum_{i=1}^N \log P(w_i | w_1, \dots, w_{i-1}) \right) \]
▮▮▮▮实际上,这等价于交叉熵 (Cross-Entropy) 损失的指数。因此,优化交叉熵损失就是在降低困惑度。

9.5.2 机器翻译 (Machine Translation) 与文本生成 (Text Generation)

BLEU (Bilingual Evaluation Understudy)
▮▮▮▮BLEU 是一种衡量机器翻译质量的自动评估指标,也可以用于评估其他文本生成任务。
▮▮▮▮它通过计算机器翻译的译文与一个或多个人工参考译文之间的 n-gram (连续的 n 个词) 重叠程度来评估翻译质量。
▮▮▮▮BLEU 的计算考虑了精确率 (Precision):机器译文中有多少 n-gram 出现在了参考译文中。为了惩罚过短的译文,BLEU 还引入了简短惩罚 (Brevity Penalty)
▮▮▮▮BLEU 的取值范围通常在 0 到 1 之间,值越高表示翻译质量越好。虽然 BLEU 是基于表层文本匹配,无法完全捕捉语义和流畅度,但它与人工评估有较好的相关性,并且易于计算,因此被广泛使用。

ROUGE (Recall-Oriented Understudy for Gisting Evaluation)
▮▮▮▮ROUGE 是一组用于评估自动文本摘要和机器翻译的指标,它主要衡量生成的文本与参考文本之间的召回率 (Recall),即参考文本中有多少 n-gram 出现在了生成的文本中。
▮▮▮▮ROUGE 有不同的变体,如 ROUGE-N (基于 N-gram 重叠)、ROUGE-L (基于最长公共子序列 Longest Common Subsequence) 和 ROUGE-S (基于跳跃双词 Skip-bigram)。
▮▮▮▮ROUGE-L 特别适合评估摘要,因为它关注句子层面的结构匹配。ROUGE 通常用于评估摘要任务,而 BLEU 更常用于机器翻译。

9.5.3 序列标注 (Sequence Labeling)

准确率 (Accuracy), 精确率 (Precision), 召回率 (Recall), F1-score
▮▮▮▮对于命名实体识别 (NER) 或词性标注 (POS tagging) 等序列标注任务,每个词元都需要被分配一个标签。评估这些任务时,可以将它们视为一个逐词元的分类问题,并使用分类任务常用的指标。
▮▮▮▮ⓐ 准确率:正确预测的词元数量占总词元数量的比例。
▮▮▮▮ⓑ 精确率:模型预测为正类的样本中,实际是正类的比例。
▮▮▮▮ⓒ 召回率:所有实际为正类的样本中,被模型预测为正类的比例。
▮▮▮▮ⓓ F1 分数:精确率和召回率的调和平均值 \( F1 = 2 \cdot \frac{\text{Precision} \cdot \text{Recall}}{\text{Precision} + \text{Recall}} \),是衡量分类器性能的综合指标,在高不平衡数据集上比准确率更有意义。

▮▮▮▮在序列标注中,这些指标可以基于词元 (token-level) 计算,也可以基于实体 (entity-level) 计算。实体级别的评估更严格,例如,只有当一个实体的所有词元都被正确标记时,才算一个正确的实体预测。

9.5.4 时间序列预测 (Time Series Forecasting)

均方误差 (Mean Squared Error - MSE)
\[ \text{MSE} = \frac{1}{N} \sum_{i=1}^N (y_i - \hat{y}_i)^2 \]
▮▮▮▮衡量预测值 \( \hat{y}_i \) 与真实值 \( y_i \) 之间差的平方的平均值。对较大的误差给予更大的惩罚。

均方根误差 (Root Mean Squared Error - RMSE)
\[ \text{RMSE} = \sqrt{\text{MSE}} = \sqrt{\frac{1}{N} \sum_{i=1}^N (y_i - \hat{y}_i)^2} \]
▮▮▮▮RMSE 是 MSE 的平方根,与原始数据的单位相同,更容易解释。

平均绝对误差 (Mean Absolute Error - MAE)
\[ \text{MAE} = \frac{1}{N} \sum_{i=1}^N |y_i - \hat{y}_i| \]
▮▮▮▮衡量预测值与真实值之间差的绝对值的平均值。对所有误差给予相同的权重,不像 MSE/RMSE 那样惩罚较大的误差。

平均绝对百分比误差 (Mean Absolute Percentage Error - MAPE)
\[ \text{MAPE} = \frac{1}{N} \sum_{i=1}^N \left| \frac{y_i - \hat{y}_i}{y_i} \right| \times 100\% \]
▮▮▮▮衡量预测误差相对于真实值的百分比。当真实值接近零时,MAPE 可能变得非常大或无定义。

选择哪种时间序列评估指标取决于具体的应用场景和对误差的容忍度。

9.6 使用深度学习框架实现 RNNs

现代深度学习框架(如 TensorFlow, PyTorch)极大地简化了 RNN 模型的构建和训练过程,提供了高性能且易于使用的 API。本节简要介绍如何在这些框架中构建和训练 RNN 模型。

深度学习框架通常提供了各种 RNN 单元和层的实现,包括:

⚝ 标准 RNN 层 (e.g., tf.keras.layers.SimpleRNN, torch.nn.RNN)
⚝ LSTM 层 (e.g., tf.keras.layers.LSTM, torch.nn.LSTM)
⚝ GRU 层 (e.g., tf.keras.layers.GRU, torch.nn.GRU)
⚝ 双向 RNN 层 (通过设置参数 bidirectional=True)
⚝ 多层堆叠 RNN (通过创建 Sequential 模型或堆叠多个 RNN 层)

这些层通常需要指定隐藏单元的数量 (unitshidden_size)、层数 (num_layers)、是否返回完整的序列输出 (return_sequences=True) 或只返回最后一个时间步的输出 (return_sequences=False) 等参数。

训练 RNN 模型的基本流程如下:

数据准备:使用框架提供的数据加载和预处理工具(如 tf.data, torch.utils.data)加载、批量处理和预处理数据,包括填充、截断和向量化。确保使用适当的掩码处理填充部分。

模型构建:使用框架提供的层 API 构建 RNN 模型。例如,可以使用 tf.keras.Sequentialtorch.nn.Module 来定义模型架构,包括嵌入层、RNN 层、全连接层 (Dense Layer) 等。

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 # 伪代码示例:使用框架构建一个简单的 LSTM 模型
2 # import tensorflow as tf
3 # from tensorflow.keras.models import Sequential
4 # from tensorflow.keras.layers import Embedding, LSTM, Dense
5
6 # vocab_size = 10000 # 词汇表大小
7 # embedding_dim = 64 # 嵌入维度
8 # lstm_units = 128 # LSTM 隐藏单元数
9 # max_len = 100 # 序列最大长度
10 # num_classes = 10 # 分类任务的类别数
11
12 # model = Sequential([
13 # Embedding(vocab_size, embedding_dim, input_length=max_len), # 嵌入层
14 # LSTM(lstm_units, return_sequences=False), # LSTM 层,只返回最后一个时间步的输出
15 # Dense(num_classes, activation='softmax') # 输出层,用于分类任务
16 # ])
17
18 # model.summary()

编译模型:选择合适的损失函数(如分类任务的交叉熵 Cross-Entropy,回归任务的均方误差 MSE)、优化器(如 Adam, RMSprop)和评估指标。

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 # 伪代码示例:编译模型
2 # optimizer = tf.keras.optimizers.Adam(learning_rate=0.001)
3 # loss_fn = tf.keras.losses.SparseCategoricalCrossentropy() # 或 CategoricalCrossentropy()
4 # model.compile(optimizer=optimizer, loss=loss_fn, metrics=['accuracy'])

训练模型:将准备好的训练数据输入到模型中进行训练。在训练循环中,通常会迭代处理数据批次,执行前向传播、计算损失、执行反向传播计算梯度、使用优化器更新模型参数。框架通常提供了高级的 fit 方法或训练管理器来简化这个过程。

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 # 伪代码示例:训练模型
2 # history = model.fit(
3 # train_dataset, # 训练数据集
4 # epochs=10, # 训练轮数
5 # validation_data=val_dataset # 验证数据集,用于监控性能和早期停止
6 # )

评估与推理:使用测试数据集评估模型的性能,并使用训练好的模型对新的数据进行推理 (inference)。

框架会负责处理底层的计算细节,包括 BPTT 算法的实现、梯度的计算与传播,以及在 GPU 等硬件上的高效运行。通过利用这些框架,开发者可以专注于模型架构的设计和训练策略的调优。

掌握这些训练技巧和实践经验,对于成功应用 RNNs 解决实际问题至关重要。它们能够帮助我们训练出更稳定、性能更优越、泛化能力更强的序列模型。

10. 循环神经网络 (RNNs) 的最新进展与替代方案

在本书前面的章节中,我们已经全面深入地探讨了循环神经网络 (RNNs) 及其主要的改进模型,如长短期记忆网络 (LSTM) 和门控循环单元 (GRU)。这些模型在处理序列数据方面展现了强大的能力,并在自然语言处理 (NLP)、语音识别、时间序列分析等众多领域取得了显著成就。然而,标准 RNNs 及其门控变体在处理极长序列时仍然面临挑战,例如计算并行性的限制以及对长期依赖关系的建模效率问题。

随着深度学习技术的飞速发展,研究人员不断探索更有效、更高效的序列建模方法。本章将带领大家了解 RNNs 领域的一些最新进展,特别是 Attention 机制的进一步演化,并重点介绍在许多序列任务中表现卓越、并已在很大程度上取代传统 RNNs 的新型序列模型——Transformer。此外,我们还将简要回顾卷积神经网络 (CNN) 在序列建模中的应用,并最后探讨 RNNs 在当前技术格局下的地位、未来发展方向和与其他模型的结合可能性。

10.1 Attention 机制的进一步发展

在第 7 章中,我们初识了 Attention 机制,了解了它如何帮助编码器-解码器模型解决固定长度上下文向量的瓶颈问题,使得解码器在生成输出时能够“关注”输入序列中相关的部分。Attention 的核心思想是为输入序列的不同部分分配不同的权重,这些权重通常是动态计算的,取决于当前的解码状态和输入元素的关联程度。

Attention 机制的强大之处在于它提供了一种灵活的方式来捕捉输入和输出序列之间的对应关系,尤其是在输入和输出序列长度不同或对齐关系不明显的情况下。然而,基础的 Attention 机制(如加性 Attention 或简单的点积 Attention)在处理复杂任务时,可能不足以捕捉数据中丰富多样的关联模式。因此,研究人员在此基础上提出了更复杂的 Attention 变体,其中最重要且影响最深远的是多头注意力 (Multi-Head Attention)。

10.1.1 回顾 Attention 机制

让我们快速回顾一下 Attention 的基本思想。假设我们有一个查询向量 (Query) \(q\) 和一组键值对 (Key-Value pairs) \((k_1, v_1), (k_2, v_2), \dots, (k_m, v_m)\)。Attention 的目标是计算一个加权平均的 वैल्यू (Value) 向量,权值由 查询 (Query) 与各个 键 (Key) 的相似度决定。常用的 点积注意力 (Dot-Product Attention) 计算方式如下:
① 计算 查询 \(q\) 与每个 键 \(k_i\) 的点积相似度:\(s_i = q \cdot k_i\)。
② 对相似度进行 softmax 归一化,得到权重:\( \alpha_i = \text{softmax}(s_1, \dots, s_m)_i = \frac{\exp(s_i)}{\sum_{j=1}^m \exp(s_j)} \)。
③ 计算加权平均的 值 向量:\( \text{Attention}(q, K, V) = \sum_{i=1}^m \alpha_i v_i \)。
其中,\(K\) 是由所有 键 \(k_i\) 组成的矩阵,\(V\) 是由所有 值 \(v_i\) 组成的矩阵。

在实践中,为了提高计算效率和稳定性,通常使用的是 缩放点积注意力 (Scaled Dot-Product Attention):
\[ \text{Attention}(Q, K, V) = \text{softmax}\left(\frac{QK^T}{\sqrt{d_k}}\right)V \]
这里,\(Q\) 是 查询 矩阵,\(K\) 是 键 矩阵,\(V\) 是 值 矩阵,\(d_k\) 是 键向量 的维度。缩放因子 \(\sqrt{d_k}\) 用于防止点积结果过大,导致 softmax 函数的梯度过小。

10.1.2 多头注意力 (Multi-Head Attention)

多头注意力 (Multi-Head Attention) 是在缩放点积注意力基础上的一种重要创新,它允许模型在不同的“表示子空间”(Representation Subspaces) 中并行地学习并融合来自不同位置的信息。

核心思想:
不是只执行一次 Attention 函数,而是并行地运行 \(h\) 次 Attention 函数,每一次都在 查询、键、值 的一个线性投影版本上执行。然后将这 \(h\) 次 Attention 的结果拼接 (concatenate) 起来,再进行一次线性变换,得到最终的结果。

具体步骤:
① 对输入的 查询 (Query) \(Q\), 键 (Key) \(K\), 值 (Value) \(V\) 分别进行 \(h\) 次不同的线性投影。对于第 \(i\) 个头 (\(i=1, \dots, h\)),使用不同的权重矩阵 \(W^Q_i, W^K_i, W^V_i\) 进行投影:
\( Q_i = Q W^Q_i \)
\( K_i = K W^K_i \)
\( V_i = V W^V_i \)
这里 \(W^Q_i \in \mathbb{R}^{d_{model} \times d_k}\), \(W^K_i \in \mathbb{R}^{d_{model} \times d_k}\), \(W^V_i \in \mathbb{R}^{d_{model} \times d_v}\)。通常选择 \( d_k = d_v = d_{model} / h \),以保持总的计算量与单头 Attention 相似。
② 在每个投影后的 \(Q_i, K_i, V_i\) 上并行计算 Attention:
\( head_i = \text{Attention}(Q_i, K_i, V_i) = \text{softmax}\left(\frac{Q_i K_i^T}{\sqrt{d_k}}\right)V_i \)
③ 将所有 \(h\) 个 Attention 头的结果 \(head_1, \dots, head_h\) 拼接起来:
\( \text{Concatenate}(head_1, \dots, head_h) \)
④ 对拼接后的结果进行最终的线性变换,得到多头注意力的输出:
\( \text{MultiHead}(Q, K, V) = \text{Concatenate}(head_1, \dots, head_h) W^O \)
其中 \(W^O \in \mathbb{R}^{(h \cdot d_v) \times d_{model}}\)。

多头注意力的优势:
捕捉不同的关系模式: 每个“头”学习关注输入序列中不同方面的信息。例如,在一个句子中,一个头可能关注语法结构,另一个头可能关注语义关联。
关注不同的位置范围: 不同的头可能学习关注不同距离的依赖关系,有的关注局部联系,有的关注全局联系。
提高模型的表达能力和鲁棒性: 通过组合多个头的输出,模型可以获得更全面的信息表示,从而提高在复杂任务上的性能。

多头注意力是 Transformer 模型的核心组成部分,极大地增强了模型处理序列数据的能力。

10.2 Transformer 模型概述

Transformer 模型由 Vaswani 等人于 2017 年在论文《Attention Is All You Need》中提出。正如论文题目所示,Transformer 完全依赖于 Attention 机制,彻底抛弃了循环和卷积结构,这在当时的序列建模领域是一个革命性的变化。

10.2.1 为什么需要 Transformer?

尽管 LSTM 和 GRU 在处理序列数据方面取得了巨大成功,但它们固有的顺序性计算结构限制了模型的并行化能力。在训练过程中,处理一个时间步的数据必须等待前一个时间步的计算完成,这使得在 GPU 等并行计算硬件上效率不高,尤其是在处理长序列时。此外,捕捉远距离依赖仍然是一个挑战,虽然门控机制有所缓解,但信息仍然需要通过一系列时间步传递。

Transformer 的出现正是为了解决这些问题。它利用 Attention 机制实现了序列中任意两个位置之间的直接连接,大大提高了捕捉长距离依赖的能力,并且由于其非顺序性的计算特点,可以高度并行化,显著提升了训练效率。

10.2.2 模型架构

原始的 Transformer 模型采用经典的 编码器-解码器 (Encoder-Decoder) 结构,常用于 序列到序列 (Sequence-to-Sequence) 任务,如机器翻译。

Encoder (编码器):
编码器由 \(N\) 个相同的层堆叠而成。每一层有两个主要子层:
多头自注意力机制 (Multi-Head Self-Attention): 允许模型在编码输入序列时,让序列中的每一个位置都能“关注”输入序列中的所有其他位置,并从中提取有用的信息。这里的 查询、键、值 都来自同一输入序列的上一个层(或输入层)。
前馈网络 (Feed-Forward Network): 一个简单的全连接前馈网络,应用于每个位置独立地(参数共享)。通常包含两个线性变换,中间有一个激活函数(如 ReLU)。
每个子层都伴随着 残差连接 (Residual Connection) 和 层归一化 (Layer Normalization)。 残差连接 (也称 Add & Norm) 有助于解决深度网络的梯度消失问题,而 层归一化 有助于稳定训练过程。
输出:\( \text{LayerNorm}(x + \text{Sublayer}(x)) \)

Decoder (解码器):
解码器也由 \(N\) 个相同的层堆叠而成。每一层有三个主要子层:
带掩码的多头自注意力机制 (Masked Multi-Head Self-Attention): 与编码器中的自注意力类似,但增加了一个掩码 (Masking) 机制。在训练过程中生成序列时,模型只能看到当前位置及之前的信息,不能“偷看”未来的信息,以确保模型的自回归 (Autoregressive) 性质。
多头交叉注意力机制 (Multi-Head Cross-Attention): 这里的 查询 来自解码器的前一个子层,而 键 和 值 来自编码器的输出。这使得解码器的每个位置都可以关注到输入序列(由编码器表示)的所有位置,这是传统的 Encoder-Decoder 模型的 Attention 机制的泛化。
前馈网络 (Feed-Forward Network): 同编码器中的前馈网络。
同样,每个子层都伴随着 残差连接 和 层归一化。

10.2.3 关键组成部分

① 自注意力 (Self-Attention):
自注意力是一种特殊的 Attention 机制,其中 查询、键、值 都来源于同一个输入序列。这意味着模型可以计算序列中每个元素与其他所有元素(包括其自身)之间的关联度。通过自注意力,模型可以直接建立序列中长距离的依赖关系,而不像 RNN 那样需要信息逐步传递。

② 位置编码 (Positional Encoding):
Transformer 模型不像 RNN 那样按顺序处理序列,它并行地处理整个序列。这意味着模型本身没有对序列中元素位置信息的感知能力。为了解决这个问题,Transformer 在输入嵌入 (Input Embeddings) 中加入了 位置编码 (Positional Encoding)。位置编码是与输入嵌入维度相同的向量,它们包含元素在序列中的绝对或相对位置信息。位置编码和词嵌入 (Word Embeddings) 相加后作为模型的输入。原始 Transformer 使用固定的正弦和余弦函数来生成位置编码:
\( PE_{(pos, 2i)} = \sin(pos / 10000^{2i/d_{model}}) \)
\( PE_{(pos, 2i+1)} = \cos(pos / 10000^{2i/d_{model}}) \)
其中 \(pos\) 是位置,\(i\) 是维度的索引,\(d_{model}\) 是嵌入维度。这种编码方式的好处是可以通过线性变换表示相对位置。

10.2.4 Transformer 的优势

与 RNNs (包括 LSTM/GRU) 相比,Transformer 主要有以下优势:
并行化 (Parallelization): Transformer 的计算不依赖于前一个时间步的隐藏状态,每个位置的计算可以并行进行,极大地加快了训练速度,尤其是在长序列上。
长距离依赖建模 (Modeling Long-Term Dependencies): 自注意力机制允许序列中的任意两个位置直接交互,无需经过许多中间步骤,因此在捕捉长距离依赖方面通常比 RNNs 更有效。
更强的表达能力 (Stronger Representational Capacity): 多头注意力和多层堆叠使得 Transformer 能够学习到更复杂、更丰富的特征表示。

10.2.5 Transformer 的影响

Transformer 架构的提出对深度学习领域产生了深远影响。它不仅在机器翻译等 Seq2Seq 任务上取得了突破性进展,更成为了许多后续大型预训练模型(如 BERT, GPT 系列, T5 等)的基础架构。这些基于 Transformer 的模型在自然语言处理的各种任务上刷新了记录,成为当前 NLP 领域的主流。虽然最初为 NLP 设计,但 Transformer 的思想和变体也被成功应用于计算机视觉 (Vision Transformer - ViT)、语音、推荐系统等其他领域。

10.3 卷积神经网络 (CNN) 在序列建模中的应用

虽然循环神经网络长期以来是处理序列数据的主流模型,而卷积神经网络 (CNN) 主要以其在图像处理领域的巨大成功而闻名,但 CNNs 同样可以有效地应用于序列建模任务。

10.3.1 基本的 CNN 序列建模

将 CNN 应用于序列数据(如文本、时间序列)通常使用 一维卷积 (1D Convolution)。一维卷积核 (Filter) 在序列上滑动,提取局部特征。
输入: 序列数据通常表示为一个二维矩阵,维度是 (sequence length, embedding dimension)。
卷积层: 使用多个一维卷积核。每个卷积核在一个小的窗口内(核大小,Kernel Size)对输入序列进行卷积操作,生成一个特征图 (Feature Map)。这个操作类似于在文本中捕捉 N-gram 特征。
池化层: 通常在卷积层后使用 池化 (Pooling) 层(如最大池化 Max Pooling 或平均池化 Average Pooling)来降低维度并引入平移不变性(尽管在序列中平移不变性可能不是总是有益的)。对于分类任务,通常在时间维度上进行全局池化 (Global Pooling),得到一个固定大小的向量表示整个序列。

基本的 1D CNN 对于捕捉序列中的局部模式非常有效,例如在文本分类中识别关键词或短语。然而,其固有的问题在于感受野 (Receptive Field) 有限,即单个卷积操作只能看到输入序列的一个局部区域。要捕捉长距离依赖,需要堆叠多层卷积或使用非常大的卷积核,但这会带来计算量增加、参数增多或梯度问题。

10.3.2 扩张卷积 (Dilated Convolutions)

为了克服标准 CNN 在序列建模中感受野有限的问题,研究人员引入了 扩张卷积 (Dilated Convolutions),也称为 带孔卷积 (Atrous Convolutions)。

扩张卷积的操作与标准卷积类似,但在卷积核的元素之间引入了间隔(称为 扩张率,Dilation Rate)。一个扩张率为 \(r\) 的 1D 扩张卷积核,其作用范围相当于一个大小为 \(k\) 的标准卷积核,但覆盖的输入区域大小为 \(k + (k-1)(r-1)\)。当 \(r=1\) 时,扩张卷积退化为标准卷积。

\[ \text{Output}[i] = \sum_{j=0}^{k-1} \text{Input}[i - r \cdot j] \cdot \text{Kernel}[j] \]

其中 \(i\) 是输出位置,\(j\) 是卷积核索引,\(k\) 是核大小,\(r\) 是扩张率。

扩张卷积的优势:
扩大感受野: 在不增加模型参数和计算量的情况下,通过指数级地增加扩张率(例如,在连续层中使用扩张率 1, 2, 4, 8, ...),可以迅速扩大模型的感受野,使其能够覆盖整个输入序列,从而捕捉长距离依赖。
保留分辨率: 与池化不同,扩张卷积不会丢失序列的内部分辨率,每个输出元素仍然对应输入序列中的一个特定位置,这对于序列标注等任务很重要。

基于扩张卷积的序列模型,如 WaveNet(用于语音生成)和 时间卷积网络 (Temporal Convolutional Network - TCN),在一些序列任务上取得了与 RNNs 甚至 Transformer 竞争或超越的性能,尤其是在需要大感受野和并行计算的场景。

总的来说,CNNs,特别是结合了扩张卷积的 CNNs,是 RNNs 和 Transformer 之外另一种强大的序列建模替代方案,它们在并行性、计算效率和捕捉局部模式方面具有优势。

10.4 RNNs 的未来与展望

在 Transformer 及其变体主导当前序列建模领域的背景下,RNNs 的地位似乎有所下降。然而,断言 RNNs 已经“死亡”为时尚早。RNNs 凭借其独特的循环结构,在某些特定场景和任务中仍然具有不可替代的优势或应用的便利性。

处理无限长序列或流式数据: RNNs 的特性使其天然适合处理持续到来的流式数据或理论上无限长的序列。它们在每个时间步更新内部状态,而无需回顾整个历史。相比之下,标准 Transformer 的自注意力机制计算复杂性随序列长度平方增长 (\(O(L^2)\)),对于无限长或超长序列难以直接应用。尽管已有 Lighweight Attention、Linear Attention 等变体尝试降低 Attention 的复杂度,以及 Transformer-XL、Reformer 等模型试图处理长序列,但在纯粹的流式处理和内存效率方面,RNNs 仍然有其独到之处。

内存效率: 标准 RNN 在每个时间步的计算通常只需要存储前一个时间步的隐藏状态,其内存开销相对于序列长度是线性的 (\(O(L)\)) 或常数的 (\(O(1)\))(取决于如何计算梯度)。而 Transformer 的 Attention 矩阵需要 \(O(L^2)\) 的内存,对于内存受限的设备(如移动端、嵌入式系统)或需要处理极长序列的场景,RNNs 可能是更可行的选择。门控 RNNs(如 LSTM, GRU)虽然参数更多,但其每个时间步的计算和内存复杂度仍然远低于处理整个序列的 Transformer。

解释性: 相比于高度并行的 Transformer,RNNs 的顺序处理流程有时更符合人类对某些序列生成过程的直觉,可能在一定程度上更易于理解(虽然深度网络的内部机制普遍难以完全解释)。

特定任务的匹配度: 对于一些本身就具有强顺序依赖或马尔可夫性质(尽管可能是高阶马尔可夫)的任务,RNNs 的结构可能与问题更匹配,从而在数据量有限或需要更简单模型时表现良好。

混合模型: 未来的发展趋势之一是将不同模型的优点结合起来。例如,可以将 Attention 机制引入 RNNs,使其能够选择性地关注历史信息;或者将 RNNs 作为 Transformer 中的局部特征提取器。许多成功的模型架构本身就是多种思想的结合。

研究与教学价值: 即使在实际应用中被 Transformer 部分取代,RNNs 及其门控变体仍然是理解序列建模基本原理、梯度传播问题以及门控机制思想的重要基石。学习 RNNs 对于深入理解深度学习模型处理时序信息的挑战与方法仍然具有重要的教学和研究价值。

总而言之,虽然 Transformer 在许多基准任务上表现出色,但 RNNs 并非过时。它们在特定场景下(如流式处理、内存受限环境)仍然具有优势,并且其核心思想和变体可能继续在未来的混合模型或新型架构中发挥作用。序列建模是一个持续发展的领域,各种模型架构都在不断演进和融合。

Appendix A: 数学基础回顾

欢迎来到《循环神经网络 (Recurrent Neural Networks - RNNs) 深度解析与实践》的附录 A。在深入学习 RNNs 之前,扎实的数学基础是至关重要的。本附录旨在帮助读者回顾和巩固理解本书核心内容所需的数学知识,主要涵盖线性代数 (Linear Algebra)、微积分 (Calculus)(特别是链式法则 (Chain Rule))和概率论 (Probability Theory) 的基础概念。对于已经熟悉这些内容的读者,可以快速浏览本附录;对于需要复习的读者,本附录将提供必要的背景知识。💪

Appendix A1: 线性代数基础 (Linear Algebra Basics)

线性代数是理解神经网络,包括 RNNs,如何处理和变换数据的基石。数据在神经网络中常常以向量和矩阵的形式表示。

标量 (Scalar):一个单独的数值。
向量 (Vector):一个有序的数值列表。在深度学习中,向量常用于表示数据的特征,例如一个词的词嵌入 (Word Embedding) 或者一个时间步的输入。
▮▮▮▮⚝ 行向量 (Row Vector):形如 \([x_1, x_2, ..., x_n]\)。
▮▮▮▮⚝ 列向量 (Column Vector):形如
\[ \begin{pmatrix} x_1 \\ x_2 \\ \vdots \\ x_n \end{pmatrix} \]
本书中,如无特别说明,向量默认为列向量。
矩阵 (Matrix):一个二维的数值数组,包含 \(m\) 行和 \(n\) 列。矩阵常用于表示数据集(每行一个样本,每列一个特征)或神经网络的权重 (Weights) 参数。
\[ \mathbf{A} = \begin{pmatrix} a_{11} & a_{12} & \cdots & a_{1n} \\ a_{21} & a_{22} & \cdots & a_{2n} \\ \vdots & \vdots & \ddots & \vdots \\ a_{m1} & a_{m2} & \cdots & a_{mn} \end{pmatrix} \]
张量 (Tensor):矩阵是二维张量,向量是一维张量,标量是零维张量。张量是多维数组的总称。在深度学习中,我们经常处理更高维度的张量,例如图像数据(高度、宽度、颜色通道)或批量处理的序列数据(批量大小、时间步、特征维度)。

理解以下线性代数运算对于理解神经网络的前向传播 (Forward Propagation) 至关重要:

向量加法 (Vector Addition):两个相同维度的向量相加,对应元素相加。
\[ \mathbf{u} + \mathbf{v} = \begin{pmatrix} u_1 \\ \vdots \\ u_n \end{pmatrix} + \begin{pmatrix} v_1 \\ \vdots \\ v_n \end{pmatrix} = \begin{pmatrix} u_1 + v_1 \\ \vdots \\ u_n + v_n \end{pmatrix} \]
标量与向量/矩阵相乘 (Scalar-Vector/Matrix Multiplication):标量与向量或矩阵中的每个元素相乘。
\[ c\mathbf{v} = c\begin{pmatrix} v_1 \\ \vdots \\ v_n \end{pmatrix} = \begin{pmatrix} cv_1 \\ \vdots \\ cv_n \end{pmatrix}, \quad c\mathbf{A} = \begin{pmatrix} ca_{11} & \cdots & ca_{1n} \\ \vdots & \ddots & \vdots \\ ca_{m1} & \cdots & ca_{mn} \end{pmatrix} \]
矩阵加法 (Matrix Addition):两个相同维度的矩阵相加,对应元素相加。
\[ \mathbf{A} + \mathbf{B} = \begin{pmatrix} a_{11}+b_{11} & \cdots & a_{1n}+b_{1n} \\ \vdots & \ddots & \vdots \\ a_{m1}+b_{m1} & \cdots & a_{mn}+b_{mn} \end{pmatrix} \]
矩阵乘法 (Matrix Multiplication):设矩阵 \(\mathbf{A}\) 的维度是 \(m \times n\),矩阵 \(\mathbf{B}\) 的维度是 \(n \times p\)。它们的乘积 \(\mathbf{C} = \mathbf{AB}\) 是一个 \(m \times p\) 的矩阵,其中元素 \(c_{ij}\) 是 \(\mathbf{A}\) 的第 \(i\) 行与 \(\mathbf{B}\) 的第 \(j\) 列的点积 (Dot Product)。
\[ c_{ij} = \sum_{k=1}^n a_{ik} b_{kj} \]
重要提示:矩阵乘法不满足交换律 (\(\mathbf{AB} \neq \mathbf{BA}\) 通常)。矩阵乘法的维度匹配是 \( (m \times n) \times (n \times p) = (m \times p) \)。这是神经网络中权重矩阵与输入向量/矩阵相乘的核心运算。

转置 (Transpose):矩阵 \(\mathbf{A}\) 的转置 \(\mathbf{A}^\top\) 是将其行和列互换得到的新矩阵。如果 \(\mathbf{A}\) 是 \(m \times n\) 矩阵,则 \(\mathbf{A}^\top\) 是 \(n \times m\) 矩阵,其中 \((\mathbf{A}^\top)_{ij} = a_{ji}\)。转置在反向传播 (Backpropagation) 中用于计算梯度。

在 RNNs 中,输入、隐藏状态 (Hidden State)、输出以及它们之间的权重都是向量或矩阵,前向传播的过程就是一系列的矩阵乘法和向量加法,再通过激活函数 (Activation Function) 进行非线性变换。例如,一个基本的 RNN 单元的计算就包括输入向量与输入权重矩阵的乘积,以及前一个时间步的隐藏状态向量与循环权重矩阵的乘积。

Appendix A2: 微积分基础 (Calculus Basics)

微积分是理解神经网络训练过程,特别是反向传播算法 (Backpropagation Algorithm),的关键。反向传播本质上是通过计算损失函数 (Loss Function) 相对于模型参数的梯度 (Gradient),然后沿着梯度的反方向更新参数以最小化损失。

导数 (Derivative):衡量函数 \(f(x)\) 随其输入 \(x\) 变化的速率,记作 \(f'(x)\) 或 \(\frac{df}{dx}\)。在优化中,导数表示函数在该点斜率。
偏导数 (Partial Derivative):对于一个多元函数 \(f(x_1, x_2, ..., x_n)\),偏导数是函数相对于其中一个变量的导数,同时保持其他变量不变,记作 \(\frac{\partial f}{\partial x_i}\)。
梯度 (Gradient):一个多元函数 \(f(x_1, ..., x_n)\) 的梯度是一个向量,其分量是函数对各个变量的偏导数。梯度指向函数值增长最快的方向。
\[ \nabla f(\mathbf{x}) = \begin{pmatrix} \frac{\partial f}{\partial x_1} \\ \frac{\partial f}{\partial x_2} \\ \vdots \\ \frac{\partial f}{\partial x_n} \end{pmatrix} \]
在深度学习中,我们需要计算损失函数对模型权重和偏置 (Bias) 的梯度,以便进行梯度下降 (Gradient Descent) 优化。

链式法则 (Chain Rule):这是反向传播算法的核心。链式法则用于计算复合函数的导数。如果 \(y = f(u)\) 且 \(u = g(x)\),那么 \(y\) 对 \(x\) 的导数是 \(y\) 对 \(u\) 的导数乘以 \(u\) 对 \(x\) 的导数:
\[ \frac{dy}{dx} = \frac{dy}{du} \cdot \frac{du}{dx} \]
对于多元函数,链式法则的形式更为复杂。例如,如果 \(y = f(u, v)\),其中 \(u = g(x)\) 且 \(v = h(x)\),则
\[ \frac{dy}{dx} = \frac{\partial y}{\partial u} \frac{du}{dx} + \frac{\partial y}{\partial v} \frac{dv}{dx} \]
如果 \(y = f(u_1, ..., u_n)\),其中每个 \(u_i = g_i(x)\),则
\[ \frac{dy}{dx} = \sum_{i=1}^n \frac{\partial y}{\partial u_i} \frac{du_i}{dx} \]
在神经网络中,损失函数是层层嵌套的函数的复合。链式法则使得我们可以从输出层开始,逐层向前(即向后)计算损失函数对每一层参数的梯度。反向传播通过时间 (Backpropagation Through Time - BPTT) 算法正是链式法则在时间序列上的应用。

激活函数及其导数 (Activation Functions and their Derivatives):神经网络的非线性能力来自于激活函数。在反向传播中,我们需要计算损失相对于激活函数输入的梯度,这需要知道激活函数的导数。附录 C 将详细列出常用的激活函数及其导数。例如:
▮▮▮▮⚝ Sigmoid 函数:\(\sigma(x) = \frac{1}{1 + e^{-x}}\),其导数是 \(\sigma'(x) = \sigma(x)(1 - \sigma(x))\)。
▮▮▮▮⚝ Tanh 函数:\(\tanh(x) = \frac{e^x - e^{-x}}{e^x + e^{-x}}\),其导数是 \(\tanh'(x) = 1 - \tanh^2(x)\)。
▮▮▮▮⚝ ReLU 函数:\(ReLU(x) = \max(0, x)\),其导数是 \(ReLU'(x) = \begin{cases} 1 & x > 0 \\ 0 & x \le 0 \end{cases}\)(在 \(x=0\) 处通常定义为 0 或不存在,但在实践中通常忽略不影响)。

Appendix A3: 概率论基础 (Probability Theory Basics)

概率论为理解模型的输出、损失函数以及模型的评估提供了数学框架。

概率 (Probability):衡量某个事件发生的可能性,其值介于 0 和 1 之间。
随机变量 (Random Variable):其取值是随机事件结果的变量。
概率分布 (Probability Distribution):描述随机变量所有可能取值及其对应概率或概率密度的函数。
▮▮▮▮⚝ 离散概率分布 (Discrete Probability Distribution):如伯努利分布 (Bernoulli Distribution)、分类分布 (Categorical Distribution)(用于多分类问题)。
▮▮▮▮⚝ 连续概率分布 (Continuous Probability Distribution):如正态分布 (Normal Distribution)。
在分类任务中,神经网络的输出层通常会通过 Softmax 函数将输出转化为一个概率分布,表示输入属于各个类别的可能性。

条件概率 (Conditional Probability):在已知事件 B 发生的条件下,事件 A 发生的概率,记作 \(P(A|B) = \frac{P(A \cap B)}{P(B)}\)。
在序列建模中,我们经常关心在给定前面所有词的情况下,下一个词出现的概率,这就是一个条件概率问题。语言模型 (Language Model) 的目标就是学习这种条件概率分布。

最大似然估计 (Maximum Likelihood Estimation - MLE):在已知观测数据的情况下,选择使得这些数据出现的概率最大的模型参数。在训练神经网络时,最小化交叉熵 (Cross-Entropy) 损失函数等价于最大化观测数据的对数似然 (Log-Likelihood),这正是 MLE 的应用。

损失函数 (Loss Function) / 目标函数 (Objective Function):衡量模型预测结果与真实结果之间差距的函数。训练过程的目标通常是最小化损失函数。常用的损失函数(如交叉熵、均方误差)与概率论概念紧密相关。例如,交叉熵损失来源于信息论,用于衡量两个概率分布之间的差异。

理解这些数学基础概念将极大地帮助你理解本书后续章节中 RNNs 的内部工作原理、训练算法(特别是 BPTT)以及各种应用场景的模型设计和优化过程。📘🚀

Appendix B: BPTT 梯度计算详细推导

本附录旨在为读者提供标准循环神经网络 (Standard Recurrent Neural Network - Standard RNN)、长短期记忆网络 (Long Short-Term Memory - LSTM) 和门控循环单元 (Gated Recurrent Unit - GRU) 中反向传播通过时间 (Backpropagation Through Time - BPTT) 算法的详细数学推导过程。理解 BPTT 是理解如何训练这些序列模型的关键,尤其对于深入分析其训练过程中的梯度问题至关重要。我们将从标准 RNN 开始,因为它最简单,然后扩展到更复杂的 LSTM 和 GRU。

Appendix B1: 标准循环神经网络 (Standard RNN) 的 BPTT 推导

标准 RNN 的核心思想是在每个时间步 \(t\) 接收当前输入 \(x_t\),并结合前一个时间步的隐藏状态 \(h_{t-1}\) 来计算当前时间步的隐藏状态 \(h_t\) 和输出 \(y_t\)。其前向传播公式如下:

隐藏状态更新:
\[ h_t = f(W_{hh} h_{t-1} + W_{ih} x_t + b_h) \]
输出计算:
\[ y_t = g(W_{ho} h_t + b_o) \]
其中:
⚝ \(x_t\) 是时间步 \(t\) 的输入向量。
⚝ \(h_t\) 是时间步 \(t\) 的隐藏状态向量。
⚝ \(y_t\) 是时间步 \(t\) 的输出向量。
⚝ \(W_{ih}\) 是输入到隐藏层的权重矩阵。
⚝ \(W_{hh}\) 是隐藏层到隐藏层的循环权重矩阵。
⚝ \(W_{ho}\) 是隐藏层到输出层的权重矩阵。
⚝ \(b_h\) 是隐藏层的偏置向量。
⚝ \(b_o\) 是输出层的偏置向量。
⚝ \(f\) 是隐藏层的激活函数(如 Tanh 或 ReLU)。
⚝ \(g\) 是输出层的激活函数(如 Softmax 或 Identity)。

假设总序列长度为 \(T\),整个序列的损失函数 \(L\) 是每个时间步的损失 \(L_t\) 的总和,即 \(L = \sum_{t=1}^T L_t\),其中 \(L_t\) 通常取决于 \(y_t\) 和对应的目标输出 \(target_t\)。BPTT 的目标是计算损失 \(L\) 对所有模型参数 (\(W_{ih}, W_{hh}, W_{ho}, b_h, b_o\)) 的梯度。

根据链式法则,对任意参数 \(\theta\),其梯度为 \(\frac{\partial L}{\partial \theta} = \sum_{t=1}^T \frac{\partial L_t}{\partial \theta}\)。我们需要计算每个时间步的损失对参数的贡献,然后求和。

Appendix B1.1: 对输出层参数 \(W_{ho}\) 和 \(b_o\) 的梯度

这些参数的梯度计算相对直接,因为它们只影响当前时间步的输出 \(y_t\) 和损失 \(L_t\),不涉及跨时间步的依赖。

对于 \(W_{ho}\):
\[ \frac{\partial L}{\partial W_{ho}} = \sum_{t=1}^T \frac{\partial L_t}{\partial W_{ho}} \]
对于每个时间步 \(t\),根据链式法则:
\[ \frac{\partial L_t}{\partial W_{ho}} = \frac{\partial L_t}{\partial y_t} \frac{\partial y_t}{\partial W_{ho}} \]
令 \(\delta_t^o = \frac{\partial L_t}{\partial y_t}\) 为输出层的误差项。
\[ \frac{\partial y_t}{\partial W_{ho}} = \frac{\partial g(W_{ho} h_t + b_o)}{\partial W_{ho}} = \text{diag}(g'(W_{ho} h_t + b_o)) \cdot h_t^T \]
这里,我们假设 \(y_t\) 是向量,\(g\) 是逐元素应用的激活函数,\(g'\) 是其导数。如果 \(y_t\) 是标量,则 \(\frac{\partial y_t}{\partial W_{ho}}\) 就是 \(h_t^T \cdot g'(W_{ho} h_t + b_o)\)。对于向量输出和矩阵乘法,需要仔细处理维度。更一般的形式是利用雅可比矩阵:
\[ \frac{\partial L_t}{\partial W_{ho}} = \left( \frac{\partial L_t}{\partial y_t} \right)^T \frac{\partial y_t}{\partial W_{ho}} \]
对于 \(y_t = g(z_t)\) 其中 \(z_t = W_{ho} h_t + b_o\),我们可以写成:
\[ \frac{\partial L_t}{\partial W_{ho}} = \left( \frac{\partial L_t}{\partial z_t} \right)^T \frac{\partial z_t}{\partial W_{ho}} \]
其中 \(\frac{\partial L_t}{\partial z_t} = \frac{\partial L_t}{\partial y_t} \odot g'(z_t)\) (\(\odot\) 表示逐元素乘积)。
\[ \frac{\partial z_t}{\partial W_{ho}} = h_t^T \]
因此,
\[ \frac{\partial L_t}{\partial W_{ho}} = \left( \frac{\partial L_t}{\partial z_t} \right) h_t^T \]
总梯度:
\[ \frac{\partial L}{\partial W_{ho}} = \sum_{t=1}^T \left( \frac{\partial L_t}{\partial z_t} \right) h_t^T \]
对于 \(b_o\):
\[ \frac{\partial L}{\partial b_o} = \sum_{t=1}^T \frac{\partial L_t}{\partial b_o} \]
对于每个时间步 \(t\):
\[ \frac{\partial L_t}{\partial b_o} = \left( \frac{\partial L_t}{\partial z_t} \right) \cdot \frac{\partial z_t}{\partial b_o} = \left( \frac{\partial L_t}{\partial z_t} \right) \cdot 1 \]
总梯度:
\[ \frac{\partial L}{\partial b_o} = \sum_{t=1}^T \left( \frac{\partial L_t}{\partial z_t} \right) \]

Appendix B1.2: 对隐藏层参数 \(W_{ih}, W_{hh}, b_h\) 的梯度

这些参数的梯度计算是 BPTT 的核心,因为 \(h_t\) 依赖于 \(h_{t-1}\),这意味着当前时间步 \(t\) 的损失 \(L_t\) 不仅通过 \(h_t\) 影响这些参数,未来时间步 \(k > t\) 的损失 \(L_k\) 也会通过 \(h_k, h_{k-1}, \dots, h_t\) 间接影响这些参数。

我们需要计算 \(\frac{\partial L}{\partial h_t}\) (即时间步 \(t\) 的隐藏状态对总损失的贡献),这个梯度会从未来时间步反向传播回来。
\[ \frac{\partial L}{\partial h_t} = \frac{\partial (\sum_{k=1}^T L_k)}{\partial h_t} = \sum_{k=t}^T \frac{\partial L_k}{\partial h_t} \]
注意到 \(L_k\) 只通过 \(h_k, h_{k+1}, \dots\) 间接依赖于 \(h_t\).
所以,对于 \(k > t\),通过链式法则,\(\frac{\partial L_k}{\partial h_t} = \frac{\partial L_k}{\partial h_k} \frac{\partial h_k}{\partial h_t}\).
对于 \(k = t\),\(\frac{\partial L_t}{\partial h_t}\) 直接通过 \(y_t\) 产生影响。
\[ \frac{\partial L}{\partial h_t} = \frac{\partial L_t}{\partial h_t} + \frac{\partial L_{t+1}}{\partial h_t} + \frac{\partial L_{t+2}}{\partial h_t} + \dots + \frac{\partial L_T}{\partial h_t} \]
利用链式法则,我们看到 \(\frac{\partial L_k}{\partial h_t}\) 对于 \(k > t\) 可以写成:
\[ \frac{\partial L_k}{\partial h_t} = \frac{\partial L_k}{\partial h_k} \frac{\partial h_k}{\partial h_{k-1}} \dots \frac{\partial h_{t+1}}{\partial h_t} \]
这正是“反向传播通过时间”的体现:梯度从 \(L_k\) 传播到 \(h_k\),然后沿着时间步 \(k \to k-1 \to \dots \to t\) 传播到 \(h_t\)。
定义 \(\delta_t^h = \frac{\partial L}{\partial h_t}\). 我们可以建立一个递归关系:
\[ \delta_t^h = \frac{\partial L_t}{\partial h_t} + \frac{\partial L}{\partial h_{t+1}} \frac{\partial h_{t+1}}{\partial h_t} \]
其中,\(\frac{\partial L_t}{\partial h_t}\) 是当前时间步的损失直接对 \(h_t\) 的梯度,它通过 \(y_t\) 影响 \(L_t\)。
\[ \frac{\partial L_t}{\partial h_t} = \left( \frac{\partial L_t}{\partial z_t} \right)^T \frac{\partial z_t}{\partial h_t} = \left( \frac{\partial L_t}{\partial z_t} \right)^T W_{ho} \]
而 \(\frac{\partial h_{t+1}}{\partial h_t}\) 描述了隐藏状态从 \(t\) 到 \(t+1\) 的依赖关系。
\(h_{t+1} = f(W_{hh} h_t + W_{ih} x_{t+1} + b_h)\). 令 \(a_{t+1} = W_{hh} h_t + W_{ih} x_{t+1} + b_h\).
\[ \frac{\partial h_{t+1}}{\partial h_t} = \text{diag}(f'(a_{t+1})) W_{hh} \]
因此,递归关系变为:
\[ \delta_t^h = \left( \frac{\partial L_t}{\partial z_t} \right)^T W_{ho} + \delta_{t+1}^h \text{diag}(f'(a_{t+1})) W_{hh} \]
边界条件是 \(\delta_T^h = \left( \frac{\partial L_T}{\partial z_T} \right)^T W_{ho}\) (假设 \(t=T+1\) 时没有后续损失贡献)。
这个递归关系允许我们从 \(t=T\) 开始,反向计算 \(\delta_T^h, \delta_{T-1}^h, \dots, \delta_1^h\).

一旦计算出每个时间步的 \(\delta_t^h\),我们就可以计算对 \(W_{ih}, W_{hh}, b_h\) 的梯度。
同样,这些参数的总梯度是每个时间步贡献的总和。
\[ \frac{\partial L}{\partial W_{ih}} = \sum_{t=1}^T \frac{\partial L}{\partial h_t} \frac{\partial h_t}{\partial W_{ih}} = \sum_{t=1}^T \delta_t^h \frac{\partial h_t}{\partial W_{ih}} \]
\[ \frac{\partial L}{\partial W_{hh}} = \sum_{t=1}^T \frac{\partial L}{\partial h_t} \frac{\partial h_t}{\partial W_{hh}} = \sum_{t=1}^T \delta_t^h \frac{\partial h_t}{\partial W_{hh}} \]
\[ \frac{\partial L}{\partial b_h} = \sum_{t=1}^T \frac{\partial L}{\partial h_t} \frac{\partial h_t}{\partial b_h} = \sum_{t=1}^T \delta_t^h \frac{\partial h_t}{\partial b_h} \]
令 \(a_t = W_{hh} h_{t-1} + W_{ih} x_t + b_h\). \(h_t = f(a_t)\).
\[ \frac{\partial h_t}{\partial a_t} = \text{diag}(f'(a_t)) \]
\[ \frac{\partial a_t}{\partial W_{ih}} = x_t^T \]
\[ \frac{\partial a_t}{\partial W_{hh}} = h_{t-1}^T \]
\[ \frac{\partial a_t}{\partial b_h} = 1 \]
则每个时间步 \(t\) 对参数的梯度贡献为:
\[ \frac{\partial L}{\partial h_t} \frac{\partial h_t}{\partial W_{ih}} = \delta_t^h \text{diag}(f'(a_t)) x_t^T \]
\[ \frac{\partial L}{\partial h_t} \frac{\partial h_t}{\partial W_{hh}} = \delta_t^h \text{diag}(f'(a_t)) h_{t-1}^T \]
\[ \frac{\partial L}{\partial h_t} \frac{\partial h_t}{\partial b_h} = \delta_t^h \text{diag}(f'(a_t)) \cdot 1 \]
注意:在实际计算中,\(\delta_t^h\) 的维度是 \(1 \times \text{hidden\_size}\),\(\text{diag}(f'(a_t))\) 是 \(\text{hidden\_size} \times \text{hidden\_size}\) 的对角矩阵,\(x_t^T\) 是 \(\text{input\_size} \times 1\),\(h_{t-1}^T\) 是 \(\text{hidden\_size} \times 1\)。为了得到与权重矩阵维度匹配的梯度矩阵,我们需要进行外积或适当的矩阵乘法。\[ \frac{\partial L}{\partial W_{ih}} = \sum_{t=1}^T (\delta_t^h \odot f'(a_t))^T x_t^T \]
\[ \frac{\partial L}{\partial W_{hh}} = \sum_{t=1}^T (\delta_t^h \odot f'(a_t))^T h_{t-1}^T \]
\[ \frac{\partial L}{\partial b_h} = \sum_{t=1}^T (\delta_t^h \odot f'(a_t)) \]
其中 \(\odot\) 表示逐元素乘积 (Hadamard product)。

Appendix B1.3: 梯度消失与梯度爆炸的分析

从递归关系 \(\delta_t^h = \left( \frac{\partial L_t}{\partial z_t} \right)^T W_{ho} + \delta_{t+1}^h \text{diag}(f'(a_{t+1})) W_{hh}\) 可以看出,梯度 \(\delta_t^h\) 是由当前损失贡献和未来时间步的梯度贡献两部分组成的。未来时间步的贡献通过 \(\text{diag}(f'(a_{t+1})) W_{hh}\) 传播到当前时间步。考虑 \(\delta_1^h\),它接收来自 \(\delta_T^h\) 的贡献,该贡献项是 \(\delta_T^h\) 乘以一系列形如 \(\text{diag}(f'(a_k)) W_{hh}\) 的矩阵的乘积(对于 \(k = T, T-1, \dots, 2\))。

如果激活函数的导数 \(f'(a_k)\) 大部分时间都远小于 1 (如 Sigmoid 或 Tanh 在饱和区),并且 \(W_{hh}\) 的谱半径(spectral radius,最大奇异值)小于 1,那么随着时间步长 \(T-t\) 的增加,这一系列矩阵的乘积将指数级地衰减到零,导致 \(\delta_t^h\) 主要由当前损失贡献决定,无法有效地捕捉长期依赖信息。这就是梯度消失 (Vanishing Gradient) 问题。

反之,如果 \(f'(a_k)\) 和 \(W_{hh}\) 的乘积在某些方向上大于 1,那么这一系列矩阵的乘积可能会指数级增长,导致 \(\delta_t^h\) 变得非常大。这就是梯度爆炸 (Exploding Gradient) 问题。梯度爆炸相对容易检测和处理(如梯度裁剪 Gradient Clipping),但梯度消失更根本地限制了标准 RNN 学习长期依赖的能力。

Appendix B2: 长短期记忆网络 (LSTM) 的 BPTT 推导

LSTM 通过引入门控机制 (Gating Mechanism) 和细胞状态 (Cell State) 来解决标准 RNN 的梯度消失问题,使其能够更好地学习和记忆长期依赖。LSTM 单元的前向传播涉及多个步骤和门:

遗忘门 (Forget Gate): \(f_t = \sigma(W_{xf} x_t + W_{hf} h_{t-1} + b_f)\)
输入门 (Input Gate): \(i_t = \sigma(W_{xi} x_t + W_{hi} h_{t-1} + b_i)\)
候选细胞状态 (Candidate Cell State): \(\tilde{c}_t = \tanh(W_{xc} x_t + W_{hc} h_{t-1} + b_c)\)
细胞状态更新 (Cell State Update): \(c_t = f_t \odot c_{t-1} + i_t \odot \tilde{c}_t\)
输出门 (Output Gate): \(o_t = \sigma(W_{xo} x_t + W_{ho} h_{t-1} + b_o)\)
隐藏状态更新 (Hidden State Update): \(h_t = o_t \odot \tanh(c_t)\)

其中:
⚝ \(x_t, h_t, c_t\) 分别是输入、隐藏状态和细胞状态向量。
⚝ \(W\) 和 \(b\) 是对应的权重矩阵和偏置向量(下标表示来源和目标,如 \(W_{xf}\) 是输入到遗忘门的权重)。
⚝ \(\sigma\) 是 Sigmoid 激活函数(用于门控)。
⚝ \(\tanh\) 是 Tanh 激活函数。
⚝ \(\odot\) 是逐元素乘积。

LSTM 的 BPTT 目标是计算损失 \(L = \sum_{t=1}^T L_t\) 对所有权重矩阵和偏置向量的梯度。与标准 RNN 类似,总梯度是每个时间步贡献的总和。关键在于计算 \(\frac{\partial L}{\partial c_t}\) 和 \(\frac{\partial L}{\partial h_t}\),并利用它们来计算对参数的梯度。

Appendix B2.1: 细胞状态梯度 \(\frac{\partial L}{\partial c_t}\) 的推导

细胞状态 \(c_t\) 是 LSTM 的核心,它通过简单的加法和乘法与 \(c_{t-1}\) 相连,这使得梯度更容易在时间步上传播。
\(\frac{\partial L}{\partial c_t}\) 接收来自两个地方的梯度:
① 来自当前时间步的隐藏状态 \(h_t\) (\(h_t\) 依赖于 \(c_t\))。
② 来自下一个时间步的细胞状态 \(c_{t+1}\) (\(c_{t+1}\) 依赖于 \(c_t\)).

\[ \frac{\partial L}{\partial c_t} = \frac{\partial L}{\partial h_t} \frac{\partial h_t}{\partial c_t} + \frac{\partial L}{\partial c_{t+1}} \frac{\partial c_{t+1}}{\partial c_t} \]
注意这里的 \(\frac{\partial L}{\partial h_t}\) 实际上是 \(\sum_{k=t}^T \frac{\partial L_k}{\partial h_t}\),但为了推导的递归形式,我们关注从 \(t+1\) 传来的梯度。更准确地说,我们定义 \(\delta_t^h = \frac{\partial L}{\partial h_t}\) 和 \(\delta_t^c = \frac{\partial L}{\partial c_t}\).
\[ \delta_t^c = \frac{\partial L}{\partial h_t} \frac{\partial h_t}{\partial c_t} + \frac{\partial L}{\partial c_{t+1}} \frac{\partial c_{t+1}}{\partial c_t} \]
从 \(h_t = o_t \odot \tanh(c_t)\),我们有:
\[ \frac{\partial h_t}{\partial c_t} = o_t \odot \tanh'(c_t) = o_t \odot (1 - \tanh^2(c_t)) \]
从 \(c_{t+1} = f_{t+1} \odot c_t + i_{t+1} \odot \tilde{c}_{t+1}\),我们有:
\[ \frac{\partial c_{t+1}}{\partial c_t} = f_{t+1} \]
注意,这里的 \(\frac{\partial c_{t+1}}{\partial c_t}\) 只考虑 \(c_t\) 对 \(c_{t+1}\) 的直接影响,不考虑 \(c_t\) 通过 \(h_t\) 影响 \(f_{t+1}, i_{t+1}, \tilde{c}_{t+1}\) 进而影响 \(c_{t+1}\) 的间接路径。这是 BPTT 链式法则应用的关键点之一。

所以,细胞状态的梯度递归关系为:
\[ \delta_t^c = \delta_t^h \odot (o_t \odot (1 - \tanh^2(c_t))) + \delta_{t+1}^c \odot f_{t+1} \]
边界条件:\(\delta_T^c = \delta_T^h \odot (o_T \odot (1 - \tanh^2(c_T)))\).
这里的 \(\delta_t^h\) 是时间步 \(t\) 的隐藏状态对总损失的梯度,它接收来自当前时间步输出的梯度以及来自 \(t+1\) 时间步隐藏状态的梯度。
\[ \delta_t^h = \left( \frac{\partial L_t}{\partial y_t} \right)^T W_{ho} + \frac{\partial L}{\partial h_{t+1}} \frac{\partial h_{t+1}}{\partial h_t} \]
这个 \(\frac{\partial h_{t+1}}{\partial h_t}\) 涉及 \(h_{t+1}\) 对 \(h_t\) 的依赖,通过门 \(f_{t+1}, i_{t+1}, o_{t+1}\) 和候选状态 \(\tilde{c}_{t+1}\) 实现,而这些门和候选状态都依赖于 \(h_t\)。这是一个复杂的依赖,使得完整的 \(\frac{\partial L}{\partial h_t}\) 递归关系比标准 RNN 更复杂。然而,在实践中,我们通常首先计算 \(\delta_t^c\) 和 \(\delta_t^h\) (从 \(t=T\) 到 \(t=1\) 反向计算),然后用这些梯度来计算参数梯度。

我们通常从输出层误差开始反向传播:
定义 \(\frac{\partial L_t}{\partial h_t}\) 为时间步 \(t\) 的损失 \(L_t\) 直接对 \(h_t\) 的梯度(通过 \(y_t\))。
\[ \frac{\partial L_t}{\partial h_t} = \left( \frac{\partial L_t}{\partial z_t^o} \right)^T W_{ho} \]
其中 \(z_t^o = W_{ho} h_t + b_o\), \(\frac{\partial L_t}{\partial z_t^o} = \frac{\partial L_t}{\partial y_t} \odot g'(z_t^o)\).

现在考虑总损失 \(L\) 对 \(h_t\) 的梯度 \(\delta_t^h = \frac{\partial L}{\partial h_t}\). 它包含来自当前时间步输出的贡献和来自下一时间步 \(h_{t+1}\) 的贡献。
\[ \delta_t^h = \frac{\partial L_t}{\partial h_t} + \frac{\partial L}{\partial h_{t+1}} \frac{\partial h_{t+1}}{\partial h_t} \]
这里的 \(\frac{\partial L}{\partial h_{t+1}}\) 就是 \(\delta_{t+1}^h\).
\[ \delta_t^h = \left( \frac{\partial L_t}{\partial z_t^o} \right)^T W_{ho} + \delta_{t+1}^h \frac{\partial h_{t+1}}{\partial h_t} \]
\(\frac{\partial h_{t+1}}{\partial h_t}\) 是 \(h_{t+1} = o_{t+1} \odot \tanh(c_{t+1})\) 对 \(h_t\) 的偏导。\(c_{t+1}, o_{t+1}\) 都依赖于 \(h_t\).
\[ \frac{\partial h_{t+1}}{\partial h_t} = \frac{\partial o_{t+1}}{\partial h_t} \odot \tanh(c_{t+1}) + o_{t+1} \odot \tanh'(c_{t+1}) \odot \frac{\partial c_{t+1}}{\partial h_t} \]
这个偏导计算很复杂。一个更实用的方法是直接计算 \(\delta_t^c\) 和 \(\delta_t^h\) 的关系。
我们已经有 \(\delta_t^c = \delta_t^h \odot (o_t \odot (1 - \tanh^2(c_t))) + \delta_{t+1}^c \odot f_{t+1}\).
我们还需要 \(\delta_t^h\) 与 \(\delta_t^c\) 的关系。注意到 \(h_t\) 依赖于 \(c_t\) 和 \(o_t\),而 \(o_t\) 又依赖于 \(h_{t-1}\) 和 \(x_t\)。
\[ \delta_t^h = \frac{\partial L}{\partial h_t} \]
这个梯度会反向传播到 \(c_t\) 和 \(o_t\),进而影响 \(h_{t-1}\) 和 \(x_t\).
\[ \frac{\partial L}{\partial o_t} = \delta_t^h \odot \tanh(c_t) \]
\[ \frac{\partial L}{\partial c_t} = \delta_t^h \odot o_t \odot (1 - \tanh^2(c_t)) \quad (\text{from } h_t = o_t \odot \tanh(c_t)) \]
然后 \(\frac{\partial L}{\partial c_t}\) 进一步反向传播到 \(f_t, i_t, \tilde{c}_t\) 和 \(c_{t-1}\).
\[ \frac{\partial L}{\partial f_t} = \frac{\partial L}{\partial c_t} \odot c_{t-1} \]
\[ \frac{\partial L}{\partial i_t} = \frac{\partial L}{\partial c_t} \odot \tilde{c}_t \]
\[ \frac{\partial L}{\partial \tilde{c}_t} = \frac{\partial L}{\partial c_t} \odot i_t \]
\[ \frac{\partial L}{\partial c_{t-1}} = \frac{\partial L}{\partial c_t} \odot f_t \]
这里,\(\frac{\partial L}{\partial c_{t-1}}\) 就是 \(\delta_{t-1}^c\) 从时间步 \(t\) 传回的贡献。总的 \(\delta_{t-1}^c\) 还需要加上从 \(h_{t-1}\) 来的贡献。
但是,从 \(c_{t+1}\) 到 \(c_t\) 的梯度传播 \(\frac{\partial L}{\partial c_t} = \frac{\partial L}{\partial c_{t+1}} \odot f_{t+1}\) 是 LSTM 缓解梯度消失的关键。即使 \(f_{t+1}\) 的某些元素接近 1,梯度也可以通过加法路径 \(i_{t+1} \odot \tilde{c}_{t+1}\) 绕过乘法链,或者通过 \(f_{t+1} \odot c_t\) 路径以接近线性的方式传播(如果 \(f_{t+1}\) 接近 1)。

Appendix B2.2: 对门控单元参数的梯度

有了 \(\frac{\partial L}{\partial f_t}, \frac{\partial L}{\partial i_t}, \frac{\partial L}{\partial \tilde{c}_t}, \frac{\partial L}{\partial o_t}\),我们可以计算它们对各自输入(通过 Sigmoid 或 Tanh 之前的激活值)的梯度,然后计算对权重的梯度。

令 \(z_t^f = W_{xf} x_t + W_{hf} h_{t-1} + b_f\), \(f_t = \sigma(z_t^f)\).
\[ \frac{\partial L}{\partial z_t^f} = \frac{\partial L}{\partial f_t} \odot \sigma'(z_t^f) = \frac{\partial L}{\partial f_t} \odot f_t \odot (1 - f_t) \]
梯度对参数:
\[ \frac{\partial L}{\partial W_{xf}} = \sum_{t=1}^T (\frac{\partial L}{\partial z_t^f})^T x_t^T \]
\[ \frac{\partial L}{\partial W_{hf}} = \sum_{t=1}^T (\frac{\partial L}{\partial z_t^f})^T h_{t-1}^T \]
\[ \frac{\partial L}{\partial b_f} = \sum_{t=1}^T \frac{\partial L}{\partial z_t^f} \]
类似地,对于输入门 \(i_t = \sigma(z_t^i)\),其中 \(z_t^i = W_{xi} x_t + W_{hi} h_{t-1} + b_i\):
\[ \frac{\partial L}{\partial z_t^i} = \frac{\partial L}{\partial i_t} \odot \sigma'(z_t^i) = \frac{\partial L}{\partial i_t} \odot i_t \odot (1 - i_t) \]
\[ \frac{\partial L}{\partial W_{xi}} = \sum_{t=1}^T (\frac{\partial L}{\partial z_t^i})^T x_t^T \]
\[ \frac{\partial L}{\partial W_{hi}} = \sum_{t=1}^T (\frac{\partial L}{\partial z_t^i})^T h_{t-1}^T \]
\[ \frac{\partial L}{\partial b_i} = \sum_{t=1}^T \frac{\partial L}{\partial z_t^i} \]
对于候选细胞状态 \(\tilde{c}_t = \tanh(z_t^c)\),其中 \(z_t^c = W_{xc} x_t + W_{hc} h_{t-1} + b_c\):
\[ \frac{\partial L}{\partial z_t^c} = \frac{\partial L}{\partial \tilde{c}_t} \odot \tanh'(z_t^c) = \frac{\partial L}{\partial \tilde{c}_t} \odot (1 - \tilde{c}_t^2) \]
\[ \frac{\partial L}{\partial W_{xc}} = \sum_{t=1}^T (\frac{\partial L}{\partial z_t^c})^T x_t^T \]
\[ \frac{\partial L}{\partial W_{hc}} = \sum_{t=1}^T (\frac{\partial L}{\partial z_t^c})^T h_{t-1}^T \]
\[ \frac{\partial L}{\partial b_c} = \sum_{t=1}^T \frac{\partial L}{\partial z_t^c} \]
对于输出门 \(o_t = \sigma(z_t^o)\),其中 \(z_t^o = W_{xo} x_t + W_{ho} h_{t-1} + b_o\):
\[ \frac{\partial L}{\partial z_t^o} = \frac{\partial L}{\partial o_t} \odot \sigma'(z_t^o) = \frac{\partial L}{\partial o_t} \odot o_t \odot (1 - o_t) \]
\[ \frac{\partial L}{\partial W_{xo}} = \sum_{t=1}^T (\frac{\partial L}{\partial z_t^o})^T x_t^T \]
\[ \frac{\partial L}{\partial W_{ho}} = \sum_{t=1}^T (\frac{\partial L}{\partial z_t^o})^T h_{t-1}^T \]
\[ \frac{\partial L}{\partial b_o} = \sum_{t=1}^T \frac{\partial L}{\partial z_t^o} \]

Appendix B2.3: 对隐藏层输入 \(h_{t-1}\) 的梯度

最后,我们需要计算 \(\frac{\partial L}{\partial h_{t-1}}\),这是下一时间步 \(t\) 的计算对 \(h_{t-1}\) 的贡献,用于计算 \(\delta_{t-1}^h\)。
\[ \frac{\partial L}{\partial h_{t-1}} = \frac{\partial L}{\partial z_t^f} \frac{\partial z_t^f}{\partial h_{t-1}} + \frac{\partial L}{\partial z_t^i} \frac{\partial z_t^i}{\partial h_{t-1}} + \frac{\partial L}{\partial z_t^c} \frac{\partial z_t^c}{\partial h_{t-1}} + \frac{\partial L}{\partial z_t^o} \frac{\partial z_t^o}{\partial h_{t-1}} + \frac{\partial L}{\partial c_t} \frac{\partial c_t}{\partial h_{t-1}} \]
这里,\(\frac{\partial L}{\partial c_t}\) 的一项 \(\frac{\partial L}{\partial c_t} \odot f_t\) 已经通过细胞状态路径回传到 \(c_{t-1}\),但 \(c_t\) 也依赖于 \(\tilde{c}_t\),而 \(\tilde{c}_t\) 依赖于 \(h_{t-1}\)。所以 \(\frac{\partial c_t}{\partial h_{t-1}} = i_t \odot \frac{\partial \tilde{c}_t}{\partial h_{t-1}}\). 考虑到 \(\tilde{c}_t = \tanh(z_t^c)\) 和 \(z_t^c = W_{xc} x_t + W_{hc} h_{t-1} + b_c\),
\[ \frac{\partial \tilde{c}_t}{\partial h_{t-1}} = \tanh'(z_t^c) \odot W_{hc} \]
因此 \(\frac{\partial c_t}{\partial h_{t-1}} = i_t \odot \tanh'(z_t^c) \odot W_{hc}\).

各项偏导为:
\[ \frac{\partial z_t^f}{\partial h_{t-1}} = W_{hf} \]
\[ \frac{\partial z_t^i}{\partial h_{t-1}} = W_{hi} \]
\[ \frac{\partial z_t^c}{\partial h_{t-1}} = W_{hc} \]
\[ \frac{\partial z_t^o}{\partial h_{t-1}} = W_{ho} \]
将这些代入并求和:
\[ \frac{\partial L}{\partial h_{t-1}} = (\frac{\partial L}{\partial z_t^f})^T W_{hf} + (\frac{\partial L}{\partial z_t^i})^T W_{hi} + (\frac{\partial L}{\partial z_t^c})^T W_{hc} + (\frac{\partial L}{\partial z_t^o})^T W_{ho} \]
再加上细胞状态通过 \(\tilde{c}_t\) 对 \(h_{t-1}\) 的贡献(这部分已包含在 \(\frac{\partial L}{\partial z_t^c}\) 中):
\[ \frac{\partial L}{\partial h_{t-1}} = \left[ (\frac{\partial L}{\partial z_t^f})^T W_{hf} + (\frac{\partial L}{\partial z_t^i})^T W_{hi} + (\frac{\partial L}{\partial z_t^c})^T W_{hc} + (\frac{\partial L}{\partial z_t^o})^T W_{ho} \right] \]
这个 \(\frac{\partial L}{\partial h_{t-1}}\) 是时间步 \(t\) 的计算对 \(h_{t-1}\) 的贡献。总的 \(\delta_{t-1}^h\) 是这个贡献加上来自 \(L_{t-1}\) 的直接贡献(通过 \(y_{t-1}\))以及从 \(h_{t-1}\) 传回的 \(\delta_{t-1}^c\) 产生的贡献(即 \(\frac{\partial L}{\partial c_{t-1}} \frac{\partial c_{t-1}}{\partial h_{t-1}}\) 这一项,但这个路径已被 \(\delta_{t-1}^c\) 包含)。

更清晰地,\(\delta_{t-1}^h\) 是由两部分组成:
1. 来自 \(L_{t-1}\) 直接通过 \(y_{t-1}\) 传播到 \(h_{t-1}\) 的梯度:\(\left( \frac{\partial L_{t-1}}{\partial z_{t-1}^o} \right)^T W_{ho}\)
2. 来自时间步 \(t\) 的计算通过 \(h_{t-1}\) 反传回来的梯度:\(\left[ (\frac{\partial L}{\partial z_t^f})^T W_{hf} + (\frac{\partial L}{\partial z_t^i})^T W_{hi} + (\frac{\partial L}{\partial z_t^c})^T W_{hc} + (\frac{\partial L}{\partial z_t^o})^T W_{ho} \right]\)

所以,\(\delta_{t-1}^h\) 的递归关系是:
\[ \delta_{t-1}^h = \left( \frac{\partial L_{t-1}}{\partial z_{t-1}^o} \right)^T W_{ho} + \left[ (\frac{\partial L}{\partial z_t^f})^T W_{hf} + (\frac{\partial L}{\partial z_t^i})^T W_{hi} + (\frac{\partial L}{\partial z_t^c})^T W_{hc} + (\frac{\partial L}{\partial z_t^o})^T W_{ho} \right] \]
其中 \(\frac{\partial L}{\partial z_t^{(\cdot)}}\) 是之前计算的中间梯度。例如 \(\frac{\partial L}{\partial z_t^f} = \frac{\partial L}{\partial f_t} \odot \sigma'(z_t^f)\),而 \(\frac{\partial L}{\partial f_t} = \frac{\partial L}{\partial c_t} \odot c_{t-1}\).

完整的 LSTM BPTT 实现通常从 \(t=T\) 开始反向计算 \(\delta_t^h\) 和 \(\delta_t^c\).
\[ \delta_T^h = \left( \frac{\partial L_T}{\partial z_T^o} \right)^T W_{ho} \]
\[ \delta_T^c = \delta_T^h \odot o_T \odot (1 - \tanh^2(c_T)) \]
然后对于 \(t = T-1, \dots, 1\):
首先计算 \(\delta_t^c\) (它依赖于 \(\delta_{t+1}^c\)):
\[ \delta_t^c = \delta_t^h \odot o_t \odot (1 - \tanh^2(c_t)) + \delta_{t+1}^c \odot f_{t+1} \]
注意这里的 \(\delta_t^h\) 是总损失 \(L\) 对 \(h_t\) 的梯度。它由当前输出损失 \(L_t\) 对 \(h_t\) 的直接梯度以及来自未来时间步的梯度组成。未来的梯度通过 \(h_{t+1}\) (依赖 \(h_t\)) 和 \(c_{t+1}\) (依赖 \(c_t\)) 传播。
完整的 \(\delta_t^h\) 推导比较繁琐,因为它涉及 \(\delta_{t+1}^h\) 通过 \(h_t\) 对 \(f_{t+1}, i_{t+1}, \tilde{c}_{t+1}, o_{t+1}\) 的影响。
在实际编程实现中,通常是计算出 \(\delta_t^c\) 和 \(\delta_t^h\) (来自输出层和下一时间步回传的总梯度)后,再计算门控单元的梯度。

更实用的反向传播计算流程(从 \(t=T\) 到 1):
① 计算 \(\frac{\partial L_t}{\partial z_t^o} = \frac{\partial L_t}{\partial y_t} \odot g'(z_t^o)\)
② 计算输出层参数梯度:\(\frac{\partial L_t}{\partial W_{ho}}\) 和 \(\frac{\partial L_t}{\partial b_o}\) (如 Appendix B1.1)
③ 计算当前隐藏状态梯度 \(\delta_t^h\). 它由两部分组成:来自当前输出损失 (\(\frac{\partial L_t}{\partial h_t} = (\frac{\partial L_t}{\partial z_t^o})^T W_{ho}\)) 和来自下一时间步的总梯度 \(\delta_{t+1}^h\) 传播回来的部分。
从 \(t=T\) 开始,\(\delta_T^h = (\frac{\partial L_T}{\partial z_T^o})^T W_{ho}\).
对于 \(t < T\),\(\delta_t^h = (\frac{\partial L_t}{\partial z_t^o})^T W_{ho} + \text{gradient contributed from } h_{t+1}\).
这个来自 \(h_{t+1}\) 的贡献是 \(\delta_{t+1}^h \frac{\partial h_{t+1}}{\partial h_t}\). 就像标准 RNN 一样,但 \(\frac{\partial h_{t+1}}{\partial h_t}\) 对 LSTM 更复杂。
实际上,我们可以先计算 \(\delta_t^c\) 和 \(\delta_t^o\),然后通过它们计算 \(\delta_t^h\) 从当前时间步对前一个时间步 \(h_{t-1}\) 的贡献。
\[ \frac{\partial L}{\partial h_{t-1}} = \sum_{\text{paths through } t \text{ and } h_t} \dots \]
一个更常见的实现方式是计算时间步 \(t\) 对 \(h_{t-1}\) 的总梯度 \(\frac{\partial L}{\partial h_{t-1}}\) 并将其作为 \(\delta_{t-1}^h\) 的一部分。
\(\delta_t^c\) 的递归:\(\delta_t^c = (\frac{\partial L}{\partial h_t} \odot o_t \odot (1 - \tanh^2(c_t))) + \delta_{t+1}^c \odot f_{t+1}\).
这里的 \(\frac{\partial L}{\partial h_t}\) 是总梯度 \(\delta_t^h\).
\[ \delta_t^c = \delta_t^h \odot o_t \odot (1 - \tanh^2(c_t)) + \delta_{t+1}^c \odot f_{t+1} \]
\(\delta_t^h\) 是当前时间步的输出梯度 \(\left( \frac{\partial L_t}{\partial z_t^o} \right)^T W_{ho}\) 加上从 \(t+1\) 传回的梯度。从 \(t+1\) 传回的梯度是通过 \(h_{t+1}\) 和 \(c_{t+1}\) 传回来的。
\[ \delta_t^h = \left( \frac{\partial L_t}{\partial z_t^o} \right)^T W_{ho} + \frac{\partial L}{\partial h_{t+1}} \frac{\partial h_{t+1}}{\partial h_t} + \frac{\partial L}{\partial c_{t+1}} \frac{\partial c_{t+1}}{\partial h_t} \]
\[ \delta_t^h = \left( \frac{\partial L_t}{\partial z_t^o} \right)^T W_{ho} + \delta_{t+1}^h \frac{\partial h_{t+1}}{\partial h_t} + \delta_{t+1}^c \frac{\partial c_{t+1}}{\partial h_t} \]
这再次涉及 \(\frac{\partial h_{t+1}}{\partial h_t}\) 和 \(\frac{\partial c_{t+1}}{\partial h_t}\) 的复杂计算。

一个更简洁且常用的计算顺序:
从 \(t=T\) 到 1 反向循环:
① 计算当前时间步输出误差 \(\epsilon_t^o = \frac{\partial L_t}{\partial z_t^o} = \frac{\partial L_t}{\partial y_t} \odot g'(z_t^o)\).
② 计算当前时间步对 \(h_t\) 的直接梯度 \(\delta_t^{h, \text{output}} = (\epsilon_t^o)^T W_{ho}\).
③ 计算从 \(t+1\) 传回的对 \(h_t\) 和 \(c_t\) 的梯度。令 \(\delta_{t+1}^h\) 和 \(\delta_{t+1}^c\) 为从 \(t+1\) 及以后时间步传回的总梯度。
\(\delta_t^{\text{from\_next\_h}} = \delta_{t+1}^h \frac{\partial h_{t+1}}{\partial h_t}\).
\(\delta_t^{\text{from\_next\_c}} = \delta_{t+1}^c \frac{\partial c_{t+1}}{\partial c_t}\).
\(\delta_t^c\) 的总梯度:\(\delta_t^c = \delta_t^{\text{from\_next\_c}} + \delta_t^h \odot o_t \odot (1 - \tanh^2(c_t))\). (注意这里 \(\delta_t^h\) 是总梯度)
\(\delta_t^h\) 的总梯度:\(\delta_t^h = \delta_t^{h, \text{output}} + \delta_t^{\text{from\_next\_h}}\).
这依然是相互依赖的递归。

Let's refine the step-by-step process for BPTT in practice, calculating gradients for \(h_{t-1}\) and \(c_{t-1}\) from time step \(t\).
Assume we have \(\frac{\partial L}{\partial h_t}\) and \(\frac{\partial L}{\partial c_t}\) (representing the total gradient from \(t\) and all future steps).
\[ \frac{\partial L}{\partial o_t} = \frac{\partial L}{\partial h_t} \odot \tanh(c_t) \]
\[ \frac{\partial L}{\partial c_t} = \frac{\partial L}{\partial c_t} \quad \text{(this is the total gradient passed to } c_t) \]
Propagate gradient from \(c_t\) backwards:
\[ \frac{\partial L}{\partial c_{t-1}} = \frac{\partial L}{\partial c_t} \odot f_t \]
\[ \frac{\partial L}{\partial f_t} = \frac{\partial L}{\partial c_t} \odot c_{t-1} \]
\[ \frac{\partial L}{\partial i_t} = \frac{\partial L}{\partial c_t} \odot \tilde{c}_t \]
\[ \frac{\partial L}{\partial \tilde{c}_t} = \frac{\partial L}{\partial c_t} \odot i_t \]
Now we have gradients w.r.t. gate outputs and candidate state. Propagate to their inputs (before activation):
\[ \frac{\partial L}{\partial z_t^f} = \frac{\partial L}{\partial f_t} \odot \sigma'(z_t^f) \]
\[ \frac{\partial L}{\partial z_t^i} = \frac{\partial L}{\partial i_t} \odot \sigma'(z_t^i) \]
\[ \frac{\partial L}{\partial z_t^c} = \frac{\partial L}{\partial \tilde{c}_t} \odot \tanh'(z_t^c) \]
\[ \frac{\partial L}{\partial z_t^o} = \frac{\partial L}{\partial o_t} \odot \sigma'(z_t^o) \]
From these, calculate gradients w.r.t. \(h_{t-1}\) and \(x_t\):
\[ \frac{\partial L}{\partial h_{t-1}} \text{ from time } t = (\frac{\partial L}{\partial z_t^f})^T W_{hf} + (\frac{\partial L}{\partial z_t^i})^T W_{hi} + (\frac{\partial L}{\partial z_t^c})^T W_{hc} + (\frac{\partial L}{\partial z_t^o})^T W_{ho} \]
\[ \frac{\partial L}{\partial x_t} \text{ from time } t = (\frac{\partial L}{\partial z_t^f})^T W_{xf} + (\frac{\partial L}{\partial z_t^i})^T W_{xi} + (\frac{\partial L}{\partial z_t^c})^T W_{xc} + (\frac{\partial L}{\partial z_t^o})^T W_{xo} \]

The total gradient \(\delta_{t-1}^h = \frac{\partial L}{\partial h_{t-1}}\) is the sum of contributions from \(L_{t-1}\) (via \(y_{t-1}\)) and from time step \(t\) (calculated above).
\[ \delta_{t-1}^h = \left( \frac{\partial L_{t-1}}{\partial z_{t-1}^o} \right)^T W_{ho} + \left[ (\frac{\partial L}{\partial z_t^f})^T W_{hf} + (\frac{\partial L}{\partial z_t^i})^T W_{hi} + (\frac{\partial L}{\partial z_t^c})^T W_{hc} + (\frac{\partial L}{\partial z_t^o})^T W_{ho} \right] \]
And \(\delta_{t-1}^c = \frac{\partial L}{\partial c_{t-1}}\) is just the gradient propagated from \(c_t\).

This iterative calculation of \(\delta_t^h\) and \(\delta_t^c\) from \(t=T\) down to \(t=1\) allows us to collect the gradients \(\frac{\partial L}{\partial z_t^f}, \frac{\partial L}{\partial z_t^i}, \frac{\partial L}{\partial z_t^c}, \frac{\partial L}{\partial z_t^o}\) at each time step, which are then summed up over time to get the total gradient for the parameters.

Appendix B2.4: 窥视孔连接 (Peephole Connections) 的 BPTT

如果 LSTM 包含窥视孔连接,例如门的计算也依赖于细胞状态:
\(f_t = \sigma(W_{xf} x_t + W_{hf} h_{t-1} + W_{cf} c_{t-1} + b_f)\)
\(i_t = \sigma(W_{xi} x_t + W_{hi} h_{t-1} + W_{ci} c_{t-1} + b_i)\)
\(o_t = \sigma(W_{xo} x_t + W_{ho} h_{t-1} + W_{co} c_t + b_o)\)
(注意:窥视孔连接形式多样,这里是一种常见形式,即遗忘门和输入门看 \(c_{t-1}\),输出门看 \(c_t\))

梯度计算需要将细胞状态对门的输入的影响也纳入链式法则。
例如,对于 \(W_{cf}\):
\[ \frac{\partial L}{\partial W_{cf}} = \sum_{t=1}^T \frac{\partial L}{\partial W_{cf}} \text{ at time } t \]
\[ \frac{\partial L}{\partial W_{cf}} \text{ at time } t = \frac{\partial L}{\partial z_t^f} \frac{\partial z_t^f}{\partial W_{cf}} = (\frac{\partial L}{\partial z_t^f})^T c_{t-1}^T \]
对于 \(\frac{\partial L}{\partial c_{t-1}}\),除了通过 \(c_t\) 和 \(h_t\) 传回的梯度,现在还需要加上通过门传回的梯度:
\[ \frac{\partial L}{\partial c_{t-1}} = \frac{\partial L}{\partial c_t} \odot f_t \quad (\text{from } c_t) \]
\[ + \frac{\partial L}{\partial z_t^f} \frac{\partial z_t^f}{\partial c_{t-1}} + \frac{\partial L}{\partial z_t^i} \frac{\partial z_t^i}{\partial c_{t-1}} \quad (\text{from } f_t, i_t) \]
\[ + \frac{\partial L}{\partial z_{t-1}^o} \frac{\partial z_{t-1}^o}{\partial c_{t-1}} \quad (\text{if } o_{t-1} \text{ sees } c_{t-1}) \]
\[ + \frac{\partial L}{\partial z_{t}^o} \frac{\partial z_{t}^o}{\partial c_{t}} \frac{\partial c_t}{\partial c_{t-1}} + \frac{\partial L}{\partial z_{t}^o} \frac{\partial z_{t}^o}{\partial c_{t-1}} \quad (\text{if } o_{t} \text{ sees } c_{t} \text{ and/or } c_{t-1}) \]
\[ \frac{\partial z_t^f}{\partial c_{t-1}} = W_{cf} \]
\[ \frac{\partial z_t^i}{\partial c_{t-1}} = W_{ci} \]
\[ \frac{\partial z_t^o}{\partial c_{t}} = W_{co} \]
因此,\(\delta_{t-1}^c = \frac{\partial L}{\partial c_{t-1}}\) 的推导会更加复杂,需要仔细跟踪所有依赖路径。

Appendix B3: 门控循环单元 (GRU) 的 BPTT 推导

GRU 是 LSTM 的简化版本,它将遗忘门和输入门合并为一个更新门 (Update Gate),并结合了隐藏状态和细胞状态。GRU 的结构更简单,参数更少,但在许多任务上性能与 LSTM 相当。

GRU 单元的前向传播:
更新门 (Update Gate): \(z_t = \sigma(W_{xz} x_t + W_{hz} h_{t-1} + b_z)\)
重置门 (Reset Gate): \(r_t = \sigma(W_{xr} x_t + W_{hr} h_{t-1} + b_r)\)
候选隐藏状态 (Candidate Hidden State): \(\tilde{h}_t = \tanh(W_{xh} x_t + W_{hh} (r_t \odot h_{t-1}) + b_h)\)
隐藏状态更新 (Hidden State Update): \(h_t = (1 - z_t) \odot h_{t-1} + z_t \odot \tilde{h}_t\)

其中:
⚝ \(x_t, h_t\) 分别是输入和隐藏状态向量。
⚝ \(W\) 和 \(b\) 是对应的权重矩阵和偏置向量。
⚝ \(\sigma\) 是 Sigmoid 激活函数。
⚝ \(\tanh\) 是 Tanh 激活函数。
⚝ \(\odot\) 是逐元素乘积。

GRU 的 BPTT 推导与 LSTM 类似,但更简单,因为它只有一个隐藏状态 \(h_t\) 在时间步上传播,没有独立的细胞状态。

目标是计算损失 \(L = \sum_{t=1}^T L_t\) 对所有权重矩阵和偏置向量的梯度。关键是计算 \(\frac{\partial L}{\partial h_t}\),并利用它来计算对参数的梯度。

Appendix B3.1: 隐藏状态梯度 \(\frac{\partial L}{\partial h_t}\) 的推导

\(\frac{\partial L}{\partial h_t}\) 接收来自两个地方的梯度:
① 来自当前时间步的输出 \(y_t\) (\(y_t\) 依赖于 \(h_t\)).
② 来自下一个时间步的隐藏状态 \(h_{t+1}\) (\(h_{t+1}\) 依赖于 \(h_t\)).

同样定义 \(\delta_t^h = \frac{\partial L}{\partial h_t}\).
\[ \delta_t^h = \frac{\partial L_t}{\partial h_t} + \frac{\partial L}{\partial h_{t+1}} \frac{\partial h_{t+1}}{\partial h_t} \]
其中,\(\frac{\partial L_t}{\partial h_t}\) 是当前时间步的损失 \(L_t\) 直接对 \(h_t\) 的梯度,通过 \(y_t\) 产生影响:
\[ \frac{\partial L_t}{\partial h_t} = \left( \frac{\partial L_t}{\partial z_t^o} \right)^T W_{ho} \]
\(\frac{\partial h_{t+1}}{\partial h_t}\) 描述了隐藏状态从 \(t\) 到 \(t+1\) 的依赖关系。
\(h_{t+1} = (1 - z_{t+1}) \odot h_t + z_{t+1} \odot \tilde{h}_{t+1}\).
\(\tilde{h}_{t+1} = \tanh(W_{xh} x_{t+1} + W_{hh} (r_{t+1} \odot h_t) + b_h)\).\(z_{t+1} = \sigma(W_{xz} x_{t+1} + W_{hz} h_t + b_z)\).
\(r_{t+1} = \sigma(W_{xr} x_{t+1} + W_{hr} h_t + b_r)\).

\(\frac{\partial h_{t+1}}{\partial h_t}\) 的计算需要考虑 \(h_t\) 对 \(z_{t+1}, r_{t+1}, \tilde{h}_{t+1}\) 的影响,以及其在 \(h_{t+1}\) 更新公式中的直接项。
\[ \frac{\partial h_{t+1}}{\partial h_t} = \frac{\partial}{\partial h_t} [(1 - z_{t+1}) \odot h_t + z_{t+1} \odot \tilde{h}_{t+1}] \]
\[ = (1 - z_{t+1}) \odot \mathbf{I} \quad (\text{Identity matrix}) \]
\[ + \frac{\partial z_{t+1}}{\partial h_t} \odot (-h_t + \tilde{h}_{t+1}) \quad (\text{product rule on } (1-z_{t+1}) \odot h_t \text{ and } z_{t+1} \odot \tilde{h}_{t+1}) \]
\[ + z_{t+1} \odot \frac{\partial \tilde{h}_{t+1}}{\partial h_t} \]
其中:
\[ \frac{\partial z_{t+1}}{\partial h_t} = \sigma'(z_{t+1}^z) \odot W_{hz} \quad (\text{where } z_{t+1}^z = W_{xz} x_{t+1} + W_{hz} h_t + b_z) \]
\[ \frac{\partial r_{t+1}}{\partial h_t} = \sigma'(z_{t+1}^r) \odot W_{hr} \quad (\text{where } z_{t+1}^r = W_{xr} x_{t+1} + W_{hr} h_t + b_r) \]
\[ \frac{\partial \tilde{h}_{t+1}}{\partial h_t} = \tanh'(z_{t+1}^h) \odot W_{hh} \odot r_{t+1} \quad (\text{direct dependency on } h_t \text{ via } r_{t+1} \odot h_t) \]
\[ + \tanh'(z_{t+1}^h) \odot (W_{hh} h_t) \odot \frac{\partial r_{t+1}}{\partial h_t} \quad (\text{dependency on } h_t \text{ via } r_{t+1}) \]
\[ \text{where } z_{t+1}^h = W_{xh} x_{t+1} + W_{hh} (r_{t+1} \odot h_t) + b_h \]

将这些代入 \(\frac{\partial h_{t+1}}{\partial h_t}\) 会得到一个相当复杂的表达式。
所以,\(\delta_t^h\) 的递归关系是:
\[ \delta_t^h = \left( \frac{\partial L_t}{\partial z_t^o} \right)^T W_{ho} + \delta_{t+1}^h \frac{\partial h_{t+1}}{\partial h_t} \]
边界条件是 \(\delta_T^h = \left( \frac{\partial L_T}{\partial z_T^o} \right)^T W_{ho}\).

Appendix B3.2: 对门控单元参数的梯度

一旦我们计算出每个时间步的 \(\delta_t^h\),我们可以计算其对门控单元输入的影响,进而计算对参数的梯度。
\[ \frac{\partial L}{\partial h_t} = \delta_t^h \]
从 \(h_t = (1 - z_t) \odot h_{t-1} + z_t \odot \tilde{h}_t\),计算 \(\frac{\partial L}{\partial z_t}\) 和 \(\frac{\partial L}{\partial \tilde{h}_t}\).
\[ \frac{\partial L}{\partial z_t} = \delta_t^h \odot (-h_{t-1} + \tilde{h}_t) \]
\[ \frac{\partial L}{\partial \tilde{h}_t} = \delta_t^h \odot z_t \]
从 \(\tilde{h}_t = \tanh(z_t^h)\) where \(z_t^h = W_{xh} x_t + W_{hh} (r_t \odot h_{t-1}) + b_h\), calculate \(\frac{\partial L}{\partial z_t^h}\).
\[ \frac{\partial L}{\partial z_t^h} = \frac{\partial L}{\partial \tilde{h}_t} \odot \tanh'(z_t^h) = (\delta_t^h \odot z_t) \odot (1 - \tilde{h}_t^2) \]
This gradient \(\frac{\partial L}{\partial z_t^h}\) is needed to calculate gradients w.r.t. \(W_{xh}, W_{hh}, b_h\) and propagate back to \(r_t\) and \(h_{t-1}\).

From \(z_t = \sigma(z_t^z)\) where \(z_t^z = W_{xz} x_t + W_{hz} h_{t-1} + b_z\), calculate \(\frac{\partial L}{\partial z_t^z}\).
\[ \frac{\partial L}{\partial z_t^z} = \frac{\partial L}{\partial z_t} \odot \sigma'(z_t^z) = (\delta_t^h \odot (\tilde{h}_t - h_{t-1})) \odot z_t \odot (1 - z_t) \]
From \(r_t = \sigma(z_t^r)\) where \(z_t^r = W_{xr} x_t + W_{hr} h_{t-1} + b_r\), calculate \(\frac{\partial L}{\partial z_t^r}\). Note that \(r_t\) affects \(L\) via \(\tilde{h}_t\).
\[ \frac{\partial L}{\partial r_t} = \frac{\partial L}{\partial \tilde{h}_t} \frac{\partial \tilde{h}_t}{\partial r_t} = (\delta_t^h \odot z_t) \odot \tanh'(z_t^h) \odot (W_{hh} h_{t-1}) \]
\[ \frac{\partial L}{\partial z_t^r} = \frac{\partial L}{\partial r_t} \odot \sigma'(z_t^r) = [(\delta_t^h \odot z_t) \odot (1 - \tilde{h}_t^2) \odot (W_{hh} h_{t-1})] \odot r_t \odot (1 - r_t) \]

Now we can compute parameter gradients by summing over time:
For \(W_{xz}, W_{hz}, b_z\) (update gate):
\[ \frac{\partial L}{\partial W_{xz}} = \sum_{t=1}^T (\frac{\partial L}{\partial z_t^z})^T x_t^T \]
\[ \frac{\partial L}{\partial W_{hz}} = \sum_{t=1}^T (\frac{\partial L}{\partial z_t^z})^T h_{t-1}^T \]
\[ \frac{\partial L}{\partial b_z} = \sum_{t=1}^T \frac{\partial L}{\partial z_t^z} \]
For \(W_{xr}, W_{hr}, b_r\) (reset gate):
\[ \frac{\partial L}{\partial W_{xr}} = \sum_{t=1}^T (\frac{\partial L}{\partial z_t^r})^T x_t^T \]
\[ \frac{\partial L}{\partial W_{hr}} = \sum_{t=1}^T (\frac{\partial L}{\partial z_t^r})^T h_{t-1}^T \]
\[ \frac{\partial L}{\partial b_r} = \sum_{t=1}^T \frac{\partial L}{\partial z_t^r} \]
For \(W_{xh}, W_{hh}, b_h\) (candidate hidden state):
\[ \frac{\partial L}{\partial W_{xh}} = \sum_{t=1}^T (\frac{\partial L}{\partial z_t^h})^T x_t^T \]
\[ \frac{\partial L}{\partial W_{hh}} = \sum_{t=1}^T (\frac{\partial L}{\partial z_t^h})^T (r_t \odot h_{t-1})^T \]
\[ \frac{\partial L}{\partial b_h} = \sum_{t=1}^T \frac{\partial L}{\partial z_t^h} \]

Appendix B3.3: 对隐藏层输入 \(h_{t-1}\) 的梯度

最后,计算 \(\frac{\partial L}{\partial h_{t-1}}\). 它接收来自时间步 \(t\) 通过 \(h_t\) 传回的梯度。
\[ \frac{\partial L}{\partial h_{t-1}} = \frac{\partial L}{\partial h_t} \frac{\partial h_t}{\partial h_{t-1}} \]
\[ \frac{\partial h_t}{\partial h_{t-1}} = \frac{\partial}{\partial h_{t-1}} [(1 - z_t) \odot h_{t-1} + z_t \odot \tilde{h}_t] \]
\[ = (1 - z_t) \odot \mathbf{I} \quad (\text{Identity matrix}) \]
\[ + \frac{\partial z_t}{\partial h_{t-1}} \odot (-h_{t-1} + \tilde{h}_t) \]
\[ + z_t \odot \frac{\partial \tilde{h}_t}{\partial h_{t-1}} \]
其中:
\[ \frac{\partial z_t}{\partial h_{t-1}} = \sigma'(z_t^z) \odot W_{hz} \]
\[ \frac{\partial \tilde{h}_t}{\partial h_{t-1}} = \tanh'(z_t^h) \odot W_{hh} \odot r_t \quad (\text{direct dependency on } h_{t-1}) \]
\[ + \tanh'(z_t^h) \odot (W_{hh} h_{t-1}) \odot \frac{\partial r_t}{\partial h_{t-1}} \quad (\text{dependency on } h_{t-1} \text{ via } r_t) \]
\[ \frac{\partial r_t}{\partial h_{t-1}} = \sigma'(z_t^r) \odot W_{hr} \]

将这些代入并结合 \(\delta_t^h = \frac{\partial L}{\partial h_t}\),我们可以得到 \(\delta_{t-1}^h = \frac{\partial L}{\partial h_{t-1}}\).
\[ \delta_{t-1}^h = \left( \frac{\partial L_{t-1}}{\partial z_{t-1}^o} \right)^T W_{ho} \quad (\text{from output loss at } t-1) \]
\[ + \delta_t^h \left[ (1 - z_t) \odot \mathbf{I} + (\sigma'(z_t^z) \odot W_{hz}) \odot (\tilde{h}_t - h_{t-1}) + z_t \odot (\tanh'(z_t^h) \odot W_{hh} \odot r_t + \tanh'(z_t^h) \odot (W_{hh} h_{t-1}) \odot (\sigma'(z_t^r) \odot W_{hr})) \right] \]
这是一个复杂的表达式,但在编程中可以逐项计算。

GRU 缓解梯度消失/爆炸的能力来源于其门控机制,特别是更新门 \(z_t\)。它控制了有多少旧的隐藏状态 \(h_{t-1}\) 被保留,以及有多少新的候选隐藏状态 \(\tilde{h}_t\) 被纳入。通过 \(h_t = (1 - z_t) \odot h_{t-1} + z_t \odot \tilde{h}_t\),梯度可以通过 \((1 - z_t)\) 路径直接从 \(h_t\) 流向 \(h_{t-1}\)。如果 \(z_t\) 的某些元素接近 0,那么相应的梯度流通过 \((1 - z_t) \odot \mathbf{I}\) 将接近 1,有助于梯度传播,类似于 LSTM 细胞状态的加法路径。

Appendix B4: 总结与实践考虑

本附录详细推导了标准 RNN、LSTM 和 GRU 的 BPTT 算法。核心思想是利用链式法则,将总损失对参数的梯度表示为每个时间步损失对参数梯度之和,并且在计算隐藏层参数和隐藏状态的梯度时,考虑梯度沿着时间步反向传播的路径。

标准 RNN 中的梯度消失和爆炸问题源于反复乘以循环权重矩阵和激活函数导数。LSTM 通过引入细胞状态和门控机制,特别是细胞状态的加法更新路径 (\(c_t = f_t \odot c_{t-1} + i_t \odot \tilde{c}_t\)) 和门控的乘法控制,更有效地管理了梯度流,缓解了梯度消失。GRU 通过简化的门控结构也达到了类似的效果。

在实际应用中,长序列的 BPTT 计算量非常大。因此,常常使用截断的反向传播通过时间 (Truncated Backpropagation Through Time - TBPTT)。TBPTT 将序列分割成固定长度的子序列,并在每个子序列上独立执行 BPTT。在前向传播时,隐藏状态在子序列之间传递;但在反向传播时,梯度只在子序列内部传播,不会跨越子序列边界回传到更早的时间步。这大大减少了计算和内存需求,但也限制了模型学习超过子序列长度的长期依赖。

现代深度学习框架 (如 TensorFlow, PyTorch) 提供了自动微分功能,用户只需要定义模型的前向传播,框架会自动计算所有参数的梯度,极大地简化了 RNNs 的训练过程。然而,理解 BPTT 的工作原理对于诊断训练问题、设计新的循环架构以及优化模型性能仍然至关重要。

Appendix C: 常用激活函数及其导数

本附录旨在回顾循环神经网络(RNNs)中常用的激活函数(Activation Functions)及其数学上的导数(Derivative),并深入分析它们对梯度(Gradient)在训练过程中传播(Propagation)的影响,特别是与第4章讨论的梯度消失(Vanishing Gradient)和梯度爆炸(Exploding Gradient)问题之间的关联。理解这些激活函数的特性对于理解 RNNs 的工作原理和训练挑战至关重要。🚀

Appendix C.1: Sigmoid 函数(Sigmoid Function)

Sigmoid 函数是一个经典的激活函数,在早期的神经网络中被广泛使用,尤其是在 RNNs 的门控机制(如 LSTM 和 GRU)中仍然扮演着重要角色。

Appendix C.1.1: 函数定义与特性

Sigmoid 函数将任何实数值映射到 (0, 1) 的范围内。这使得它非常适合用作输出层,当需要预测概率或在二分类任务中输出时。

数学表达式为:
\[ \sigma(x) = \frac{1}{1 + e^{-x}} \]

其图形呈现一个平滑的 S 形曲线。

⚝ 特性:
▮▮▮▮⚝ 输出范围严格限制在 (0, 1) 之间。
▮▮▮▮⚝ 曲线平滑,处处可导。
▮▮▮▮⚝ 在输入 \(x\) 趋近于正无穷时,输出趋近于 1;趋近于负无穷时,输出趋近于 0。
▮▮▮▮⚝ 在 \(x=0\) 附近,曲线的斜率(即导数)最大。

Appendix C.1.2: 导数推导与分析

Sigmoid 函数的导数可以用函数本身来表示,这使得计算非常方便。

导数表达式为:
\[ \sigma'(x) = \sigma(x)(1 - \sigma(x)) \]

导数的图形呈现一个钟形曲线(Bell Curve),其最大值在 \(x=0\) 处取得,最大值为 \(0.25 = 1/4\)。

⚝ 导数特性:
▮▮▮▮⚝ 导数范围严格限制在 (0, 0.25] 之间。
▮▮▮▮⚝ 只有当输入 \(x\) 接近 0 时,导数才相对较大。
▮▮▮▮⚝ 当 \(x\) 的绝对值较大(即 \(x\) 趋近正无穷或负无穷)时,导数趋近于 0。

Appendix C.1.3: 对梯度传播的影响

在 RNNs 的反向传播通过时间(BPTT)过程中,梯度需要沿着时间步反向传播,这涉及到链式法则(Chain Rule)中的乘法运算。当使用 Sigmoid 作为激活函数时,每经过一个 Sigmoid 神经元,梯度都会乘以该神经元输出的导数 \(\sigma'(x)\)。

由于 Sigmoid 函数的导数最大只有 0.25,并且在 \(x\) 的绝对值较大时导数趋近于 0。这意味着:
① 当输入 \(x\) 较大或较小时(即 Sigmoid 输出接近 1 或 0,处于饱和区),导数非常小。
② 梯度在反向传播时,每经过一个 Sigmoid 层就会乘以一个小于等于 0.25 的数。
③ 在处理长序列时,梯度需要跨越多个时间步进行反向传播,这相当于将多个小于等于 0.25 的数连乘。例如,经过 \(T\) 个时间步,梯度可能被乘以 \((0.25)^T\)。

这种连乘效应导致梯度随着时间步的增加呈指数级衰减,即梯度值迅速变得非常小,接近于零。这就是梯度消失问题的根本原因之一。梯度消失使得网络难以学习到序列中早期时间步的信息,从而无法捕捉长期依赖(Long-Term Dependencies)。😥

Sigmoid 在门控单元(Gate Unit)中的应用(如遗忘门、输入门、输出门)是合理的,因为门的作用就是将信息限制在 (0, 1) 范围内,用来“控制”信息的流动或比例,而不是直接作为主要的隐藏状态非线性变换。在这些地方,其输出范围的特性比其导数对主通路梯度的影响更重要。

Appendix C.2: Tanh 函数(Hyperbolic Tangent Function)

Tanh 函数是 Sigmoid 函数的另一种形式,它将任何实数值映射到 (-1, 1) 的范围内。

Appendix C.2.1: 函数定义与特性

Tanh 函数是双曲正切函数,其输出范围为 (-1, 1)。

数学表达式为:
\[ \tanh(x) = \frac{e^x - e^{-x}}{e^x + e^{-x}} \]
或者与 Sigmoid 的关系:
\[ \tanh(x) = 2\sigma(2x) - 1 \]

其图形也呈现一个平滑的 S 形曲线,但中心在 (0, 0)。

⚝ 特性:
▮▮▮▮⚝ 输出范围在 (-1, 1) 之间,是零中心化的(Zero-Centered)。零中心化的输出在一些情况下有助于后续层的学习。
▮▮▮▮⚝ 曲线平滑,处处可导。
▮▮▮▮⚝ 在输入 \(x\) 趋近于正无穷时,输出趋近于 1;趋近于负无穷时,输出趋近于 -1。
▮▮▮▮⚝ 在 \(x=0\) 处,曲线的斜率最大。

Appendix C.2.2: 导数推导与分析

Tanh 函数的导数同样可以用函数本身表示。

导数表达式为:
\[ \tanh'(x) = 1 - \tanh^2(x) \]

导数的图形也是一个钟形曲线,其最大值在 \(x=0\) 处取得,最大值为 \(1 - \tanh^2(0) = 1 - 0^2 = 1\)。

⚝ 导数特性:
▮▮▮▮⚝ 导数范围严格限制在 (0, 1] 之间。
▮▮▮▮⚝ 只有当输入 \(x\) 接近 0 时,导数才接近最大值 1。
▮▮▮▮⚝ 当 \(x\) 的绝对值较大时(即 \(x\) 趋近正无穷或负无穷,Tanh 输出接近 1 或 -1,处于饱和区),导数趋近于 \(1 - (\pm 1)^2 = 0\)。

Appendix C.2.3: 对梯度传播的影响

与 Sigmoid 类似,当 Tanh 用作隐藏状态的激活函数时,梯度在反向传播时会乘以 Tanh 函数的导数 \(\tanh'(x)\)。

尽管 Tanh 的导数最大值为 1 (在 \(x=0\) 处),但其导数范围是 (0, 1]。在大多数情况下,导数会小于 1 (除非输入恰好为 0)。

⚝ 影响:
① 当输入 \(x\) 较大或较小时(即 Tanh 输出接近 1 或 -1,处于饱和区),导数趋近于 0。
② 梯度在反向传播时,每经过一个 Tanh 层就会乘以一个小于等于 1 的数。
③ 在处理长序列时,梯度需要跨越多个时间步进行反向传播,这相当于将多个小于等于 1 的数连乘。例如,经过 \(T\) 个时间步,梯度可能被乘以 \(\prod_{t=1}^T \tanh'(x_t)\)。

与 Sigmoid 相比,Tanh 的导数最大值是 1,高于 Sigmoid 的 0.25,这使得 Tanh 在一定程度上缓解了 Sigmoid 的梯度消失问题,因为它在非饱和区的导数值相对更大。然而,当输入进入饱和区时,Tanh 的导数仍然会趋近于 0,因此梯度消失问题在标准 RNN 中使用 Tanh 时依然存在,尤其是在处理非常长的序列时。🤦‍♀️

尽管如此,由于其零中心化的输出和相对较大的非饱和区导数,Tanh 通常在标准 RNN 的隐藏层中比 Sigmoid 表现更好。在 LSTM 和 GRU 中,Tanh 经常被用于计算候选隐藏状态 \(\tilde{h}_t\),而 Sigmoid 用于门控。

Appendix C.3: ReLU 函数(Rectified Linear Unit)及其变体

ReLU 函数在现代深度学习中非常流行,因为它在一定程度上解决了梯度消失问题,并加速了训练。虽然标准 ReLU 在 RNNs 的循环层中不如 Sigmoid 或 Tanh 常见(因为其非负性可能导致一些问题),但其变体或在网络的非循环层中使用仍然具有价值,而且其原理对于理解为何能缓解梯度消失非常重要。

Appendix C.3.1: 函数定义与特性

标准 ReLU 函数对于正输入直接输出输入值,对于负输入输出 0。

数学表达式为:
\[ \text{ReLU}(x) = \max(0, x) \]

其图形是一个在 \(x=0\) 处带有拐点的折线。

⚝ 特性:
▮▮▮▮⚝ 计算简单,激活过程非常快。
▮▮▮▮⚝ 对于正输入,激活函数不会导致梯度衰减。
▮▮▮▮⚝ 可能导致“死亡 ReLU”(Dying ReLU)问题:如果一个神经元在训练过程中输入的恒小于 0,那么它的输出永远是 0,梯度永远是 0,该神经元将不再更新权重。
▮▮▮▮⚝ 输出是非零中心化的(输出恒 \(\ge 0\))。

Appendix C.3.2: 导数推导与分析

ReLU 函数的导数非常简单,但在 \(x=0\) 处不可导(通常约定导数为 0 或 1)。

导数表达式为:
\[ \text{ReLU}'(x) = \begin{cases} 1 & \text{if } x > 0 \\ 0 & \text{if } x < 0 \\ \text{undefined (or 0/1)} & \text{if } x = 0 \end{cases} \]

⚝ 导数特性:
▮▮▮▮⚝ 对于正输入,导数恒为 1。
▮▮▮▮⚝ 对于负输入,导数恒为 0。

Appendix C.3.3: 对梯度传播的影响

当 ReLU 用作激活函数时:

⚝ 影响:
① 对于输入 \(x > 0\) 的神经元,其导数是 1。在反向传播时,梯度会乘以 1,不会衰减。这极大地缓解了梯度消失问题,使得梯度能够有效地传播很长的距离。👍
② 对于输入 \(x < 0\) 的神经元,其导数是 0。一旦某个时间步的隐藏状态经过 ReLU 后导致后续输入为负,梯度就会在这一点被“截断”,无法继续向前传播。这虽然在一定程度上导致了信息的丢失和潜在的“死亡 ReLU”问题,但相比于 Sigmoid/Tanh 在饱和区普遍存在的微小导数,ReLU 在非饱和区 (即 \(x>0\)) 能够保证梯度不衰减,这是其核心优势。

在标准 RNN 或 LSTM/GRU 的循环连接中直接使用 ReLU 可能会有问题,因为隐藏状态需要能够包含负值信息,并且 ReLU 的非负性可能限制模型的表达能力,或者导致隐藏状态在某些情况下变为全零向量。然而,在一些 RNN 变体、前馈连接层,或者结合其他机制(如 Leaky ReLU 或其他变体)时,ReLU 的思想和优点仍然可以被利用。例如,一些研究探索了使用 ReLU 或其变体构建的 RNN 单元。

Appendix C.3.4: ReLU 的变体

为了解决标准 ReLU 的一些问题(如死亡 ReLU 和非零均值输出),出现了一些变体:

① Leaky ReLU:
\[ \text{Leaky ReLU}(x) = \begin{cases} x & \text{if } x > 0 \\ \alpha x & \text{if } x \le 0 \end{cases} \]
其中 \(\alpha\) 是一个小的正数(如 0.01)。这样对于负输入,导数不再是 0,而是 \(\alpha\),避免了死亡神经元。

② Parametric ReLU (PReLU):Leaky ReLU 的 \(\alpha\) 变成了可学习的参数。

③ Exponential Linear Unit (ELU):
\[ \text{ELU}(x) = \begin{cases} x & \text{if } x > 0 \\ \alpha (e^x - 1) & \text{if } x \le 0 \end{cases} \]
ELU 试图使激活函数的均值接近零,同时对于负输入也有非零导数。

这些变体在某些任务和网络结构中可能比标准 ReLU 表现更好,但它们在 RNN 单元内部循环连接中的适用性也需要具体分析。

Appendix C.4: 激活函数对梯度传播的总结

下表总结了 Sigmoid、Tanh 和 ReLU 对梯度传播的主要影响:

激活函数输出范围导数范围最大导数对梯度传播的影响适用场景 (RNNs)
Sigmoid(0, 1)(0, 0.25]0.25严重导致梯度消失,尤其在输入绝对值较大时。门控单元(如 LSTM/GRU 中的遗忘门、输入门、输出门)
Tanh(-1, 1)(0, 1]1相对 Sigmoid 缓解梯度消失,但在输入绝对值较大时仍消失。标准 RNN 的隐藏层,LSTM/GRU 的候选隐藏状态。
ReLU[0, \(\infty\)){0, 1} (\(x=0\) 除外)1对于正输入避免梯度消失;对于负输入梯度为 0 (死亡 ReLU)。RNN 变体,非循环层,结合其他机制。

从表中可以看出,Sigmoid 和 Tanh 的导数在输入值较大或较小时会趋近于零,这是标准 RNN 难以学习长期依赖、出现梯度消失问题的核心原因之一。ReLU 通过让正输入的导数恒为 1,有效缓解了这一问题,但也引入了其他挑战。因此,LSTM 和 GRU 等改进型 RNN 架构并未直接在主信息流中使用标准的 Sigmoid 或 Tanh 作为隐藏状态的激活函数(而是通过门控机制来管理信息流),或者在计算候选状态时使用 Tanh。理解这些激活函数的数学特性及其对梯度流的影响,是深入理解 RNNs 工作原理和局限性的关键。🔑

Appendix D: RNN 模型实现代码示例

本附录旨在通过具体的代码示例,帮助读者理解如何在主流的深度学习框架(TensorFlow 和 PyTorch)中构建、训练和使用循环神经网络 (RNN) 模型,包括简单的 RNN、长短期记忆网络 (LSTM) 和门控循环单元 (GRU)。这些示例代码是基础性的,用于展示模型的构建过程,读者可以根据实际任务需求进行修改和扩展。

Appendix D1: 使用 TensorFlow 实现

TensorFlow 是一个由 Google 开发的开源机器学习框架,提供了 Keras 高级 API,使得构建神经网络模型变得非常便捷。本节将使用 TensorFlow 和 Keras 来实现各种 RNN 模型。

Appendix D1.1: 准备数据 (Data Preparation)

在构建模型之前,我们需要准备适合 RNN 输入格式的序列数据。通常,输入数据是一个三维张量 (Tensor),形状为 (batch_size, sequence_length, input_feature_size)。这里我们使用一个简单的例子,模拟一个字符级别的序列预测任务。

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 import numpy as np
2 import tensorflow as tf
3 from tensorflow.keras.models import Sequential
4 from tensorflow.keras.layers import SimpleRNN, LSTM, GRU, Dense, Embedding
5 from tensorflow.keras.preprocessing.sequence import pad_sequences
6
7 # 模拟一些序列数据
8 # 假设词汇表大小为 10,序列长度为 5
9 vocab_size = 10
10 sequence_length = 5
11 embedding_dim = 16 # 词嵌入维度
12
13 # 随机生成一些序列数据 (整数索引)
14 num_samples = 100
15 # 每个样本是一个长度为 sequence_length 的整数序列
16 sequences = np.random.randint(1, vocab_size, size=(num_samples, sequence_length))
17
18 # 模拟标签,例如预测序列中的下一个词 (shifted sequence)
19 # 注意:实际任务中,标签的形状和内容取决于具体的任务类型
20 labels = np.roll(sequences, shift=-1, axis=1) # 简单地将序列向左移动一位作为标签
21 labels[:, -1] = 0 # 最后一个位置的标签设为 0 (或特殊标记)
22
23 print("原始序列示例:\n", sequences[:3])
24 print("对应标签示例:\n", labels[:3])
25
26 # RNN 通常需要输入是浮点数,如果输入是整数索引,需要先通过 Embedding 层转换为向量
27 # Embedding 层将在模型内部处理,所以模型的输入形状是 (batch_size, sequence_length)
28 # 实际数据预处理可能更复杂,例如 tokenization, building vocabulary 等。
29
30 # 对于分类任务 (如预测下一个词),标签通常是下一个词的索引
31 # 如果是 Many-to-Many 任务,标签形状与输入类似
32 # 如果是 Many-to-One 任务 (如序列分类),标签形状是 (batch_size,)
33 # 这里我们假设是 Many-to-Many 预测下一个词,标签仍然是整数索引,损失函数使用 SparseCategoricalCrossentropy

Appendix D1.2: 构建并训练简单 RNN 模型 (Build and Train Simple RNN Model)

简单 RNN (Simple RNN) 是最基本的循环神经网络类型。

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 # 构建简单 RNN 模型 (Many-to-Many 架构,预测序列中的下一个词)
2 model_rnn = Sequential([
3 # Embedding 层将输入的整数索引转换为密集向量
4 Embedding(input_dim=vocab_size, output_dim=embedding_dim, input_length=sequence_length),
5 # SimpleRNN 层,返回每个时间步的输出
6 # return_sequences=True 表示在每个时间步都有输出,适用于 Many-to-Many
7 SimpleRNN(units=32, return_sequences=True), # 32 是隐藏单元的数量
8 # Dense 层用于将 RNN 的输出映射到词汇表大小,进行分类预测
9 Dense(vocab_size, activation='softmax')
10 ])
11
12 # 编译模型
13 model_rnn.compile(optimizer='adam',
14 loss='sparse_categorical_crossentropy', # 标签是整数索引,使用这个损失函数
15 metrics=['accuracy'])
16
17 # 查看模型结构
18 model_rnn.summary()
19
20 # 训练模型
21 # 注意:这个例子使用模拟数据,训练效果没有实际意义
22 print("\n开始训练简单 RNN 模型...")
23 history_rnn = model_rnn.fit(sequences, labels, epochs=10, batch_size=32, validation_split=0.2)
24 print("简单 RNN 模型训练完成。")
25
26 # 预测示例
27 # 假设输入一个序列 (batch_size=1)
28 sample_input = np.array([[1, 2, 3, 4, 5]])
29 predictions_rnn = model_rnn.predict(sample_input)
30
31 # predictions_rnn 的形状是 (batch_size, sequence_length, vocab_size)
32 # 对于序列中的每个位置,输出一个概率分布
33 print("\n简单 RNN 预测结果形状:", predictions_rnn.shape)
34 # print("简单 RNN 预测结果:\n", predictions_rnn) # 打印原始概率分布可能太长
35
36 # 选取每个时间步概率最高的词索引作为预测结果
37 predicted_indices_rnn = np.argmax(predictions_rnn, axis=-1)
38 print("简单 RNN 预测的词索引序列:\n", predicted_indices_rnn)

Appendix D1.3: 构建并训练 LSTM 模型 (Build and Train LSTM Model)

长短期记忆网络 (LSTM) 是 RNN 的一种改进,能有效解决梯度消失问题,更好地捕捉长期依赖。

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 # 构建 LSTM 模型 (Many-to-Many 架构)
2 model_lstm = Sequential([
3 Embedding(input_dim=vocab_size, output_dim=embedding_dim, input_length=sequence_length),
4 LSTM(units=32, return_sequences=True), # LSTM 层,return_sequences=True
5 Dense(vocab_size, activation='softmax')
6 ])
7
8 # 编译模型
9 model_lstm.compile(optimizer='adam',
10 loss='sparse_categorical_crossentropy',
11 metrics=['accuracy'])
12
13 # 查看模型结构
14 model_lstm.summary()
15
16 # 训练模型
17 print("\n开始训练 LSTM 模型...")
18 history_lstm = model_lstm.fit(sequences, labels, epochs=10, batch_size=32, validation_split=0.2)
19 print("LSTM 模型训练完成。")
20
21 # 预测示例
22 predictions_lstm = model_lstm.predict(sample_input)
23 print("\nLSTM 预测结果形状:", predictions_lstm.shape)
24 predicted_indices_lstm = np.argmax(predictions_lstm, axis=-1)
25 print("LSTM 预测的词索引序列:\n", predicted_indices_lstm)

Appendix D1.4: 构建并训练 GRU 模型 (Build and Train GRU Model)

门控循环单元 (GRU) 是 LSTM 的一个简化版本,参数更少,计算更快,在许多任务上性能与 LSTM 相近。

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 # 构建 GRU 模型 (Many-to-Many 架构)
2 model_gru = Sequential([
3 Embedding(input_dim=vocab_size, output_dim=embedding_dim, input_length=sequence_length),
4 GRU(units=32, return_sequences=True), # GRU 层,return_sequences=True
5 Dense(vocab_size, activation='softmax')
6 ])
7
8 # 编译模型
9 model_gru.compile(optimizer='adam',
10 loss='sparse_categorical_crossentropy',
11 metrics=['accuracy'])
12
13 # 查看模型结构
14 model_gru.summary()
15
16 # 训练模型
17 print("\n开始训练 GRU 模型...")
18 history_gru = model_gru.fit(sequences, labels, epochs=10, batch_size=32, validation_split=0.2)
19 print("GRU 模型训练完成。")
20
21 # 预测示例
22 predictions_gru = model_gru.predict(sample_input)
23 print("\nGRU 预测结果形状:", predictions_gru.shape)
24 predicted_indices_gru = np.argmax(predictions_gru, axis=-1)
25 print("GRU 预测的词索引序列:\n", predicted_indices_gru)

Appendix D1.5: 其他架构示例 (Other Architecture Examples)

Many-to-One 架构 (如序列分类): 在 RNN/LSTM/GRU 层后不加 return_sequences=True,只取最后一个时间步的输出。

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 # Many-to-One 模型示例 (假设进行序列二分类)
2 model_many_to_one = Sequential([
3 Embedding(input_dim=vocab_size, output_dim=embedding_dim, input_length=sequence_length),
4 LSTM(units=32), # return_sequences=False (default)
5 Dense(1, activation='sigmoid') # Sigmoid 用于二分类
6 ])
7 model_many_to_one.compile(optimizer='adam', loss='binary_crossentropy', metrics=['accuracy'])
8 model_many_to_one.summary()
9 # 需要准备形状为 (num_samples,) 的二分类标签进行训练
10 # dummy_labels_binary = np.random.randint(0, 2, size=(num_samples,))
11 # model_many_to_one.fit(sequences, dummy_labels_binary, epochs=10, batch_size=32)

堆叠 RNN (Stacked RNN): 堆叠多个 RNN/LSTM/GRU 层。除最后一层外,前面的层需要 return_sequences=True

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 # 堆叠 LSTM 模型示例 (2层)
2 model_stacked_lstm = Sequential([
3 Embedding(input_dim=vocab_size, output_dim=embedding_dim, input_length=sequence_length),
4 LSTM(units=32, return_sequences=True), # 第一层返回序列
5 LSTM(units=32, return_sequences=True), # 第二层也返回序列 (如果最后一层不是输出层)
6 # 如果是 Many-to-One,最后一层 LSTM 不需要 return_sequences=True
7 # LSTM(units=32), # 最后一层 LSTM (Many-to-One)
8 Dense(vocab_size, activation='softmax')
9 ])
10 model_stacked_lstm.compile(optimizer='adam', loss='sparse_categorical_crossentropy', metrics=['accuracy'])
11 model_stacked_lstm.summary()

双向 RNN (Bidirectional RNN): 使用 Bidirectional 包装器包裹 RNN 层,同时处理正向和反向序列信息。

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 from tensorflow.keras.layers import Bidirectional
2
3 # 双向 LSTM 模型示例
4 model_bidirectional_lstm = Sequential([
5 Embedding(input_dim=vocab_size, output_dim=embedding_dim, input_length=sequence_length),
6 Bidirectional(LSTM(units=32, return_sequences=True)), # Bidirectional 包裹 LSTM
7 Dense(vocab_size, activation='softmax')
8 ])
9 model_bidirectional_lstm.compile(optimizer='adam', loss='sparse_categorical_crossentropy', metrics=['accuracy'])
10 model_bidirectional_lstm.summary()

Appendix D2: 使用 PyTorch 实现

PyTorch 是另一个广泛使用的开源深度学习框架,由 Facebook (现 Meta) 开发。与 TensorFlow 的 Keras API 风格略有不同,PyTorch 更偏向于命令式编程和动态计算图。本节将使用 PyTorch 来实现各种 RNN 模型。

Appendix D2.1: 准备数据 (Data Preparation)

在 PyTorch 中,数据通常表示为 torch.Tensor。输入到 RNN 模块的张量形状通常是 (sequence_length, batch_size, input_feature_size),这与 TensorFlow/Keras 的默认顺序 (batch_size, sequence_length, input_feature_size) 不同。PyTorch 的 RNN 模块可以通过设置 batch_first=True 来适应 (batch_size, sequence_length, input_feature_size) 的形状。这里我们使用 batch_first=True 来与 TensorFlow 示例保持一致。

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 import torch
2 import torch.nn as nn
3 import torch.optim as optim
4 from torch.utils.data import Dataset, DataLoader
5
6 # 模拟一些序列数据
7 vocab_size = 10
8 sequence_length = 5
9 embedding_dim = 16 # 词嵌入维度
10 hidden_size = 32 # 隐藏单元数量
11
12 num_samples = 100
13 # 随机生成一些序列数据 (整数索引),形状 (num_samples, sequence_length)
14 sequences_pt = torch.randint(1, vocab_size, size=(num_samples, sequence_length), dtype=torch.long)
15
16 # 模拟标签,例如预测序列中的下一个词 (shifted sequence)
17 # 标签仍然是整数索引,形状与 sequences_pt 相同
18 labels_pt = torch.roll(sequences_pt, shifts=-1, dims=1)
19 labels_pt[:, -1] = 0 # 最后一个位置的标签设为 0
20
21 print("原始序列示例 (PyTorch):\n", sequences_pt[:3])
22 print("对应标签示例 (PyTorch):\n", labels_pt[:3])
23
24 # 创建 PyTorch DataLoader
25 class SequenceDataset(Dataset):
26 def __init__(self, sequences, labels):
27 self.sequences = sequences
28 self.labels = labels
29
30 def __len__(self):
31 return len(self.sequences)
32
33 def __getitem__(self, idx):
34 return self.sequences[idx], self.labels[idx]
35
36 dataset = SequenceDataset(sequences_pt, labels_pt)
37 dataloader = DataLoader(dataset, batch_size=32, shuffle=True)

Appendix D2.2: 构建并训练简单 RNN 模型 (Build and Train Simple RNN Model)

在 PyTorch 中,我们通常继承 torch.nn.Module 类来定义模型。

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 # 构建简单 RNN 模型 (Many-to-Many 架构)
2 class SimpleRNNModel(nn.Module):
3 def __init__(self, vocab_size, embedding_dim, hidden_size, output_size):
4 super(SimpleRNNModel, self).__init__()
5 self.embedding = nn.Embedding(vocab_size, embedding_dim)
6 # batch_first=True 表示输入形状是 (batch_size, sequence_length, feature)
7 self.rnn = nn.RNN(embedding_dim, hidden_size, batch_first=True)
8 self.fc = nn.Linear(hidden_size, output_size) # 全连接层映射到输出大小
9
10 def forward(self, x):
11 # x 的形状: (batch_size, sequence_length)
12 embedded = self.embedding(x) # 形状: (batch_size, sequence_length, embedding_dim)
13 # rnn 的输入形状: (batch_size, sequence_length, input_size)
14 # output 的形状: (batch_size, sequence_length, hidden_size * num_directions)
15 # hidden 的形状: (num_layers * num_directions, batch_size, hidden_size)
16 output, hidden = self.rnn(embedded)
17 # output 包含每个时间步的隐藏状态,用于 Many-to-Many
18 # 将 output reshape 进行全连接层计算
19 # 形状从 (batch_size, sequence_length, hidden_size) 变为 (batch_size * sequence_length, hidden_size)
20 output = output.contiguous().view(-1, hidden_size)
21 output = self.fc(output) # 形状: (batch_size * sequence_length, output_size)
22 # reshape 回 (batch_size, sequence_length, output_size)
23 output = output.view(x.size(0), x.size(1), -1)
24 return output
25
26 # 实例化模型、定义损失函数和优化器
27 model_rnn_pt = SimpleRNNModel(vocab_size, embedding_dim, hidden_size, vocab_size)
28 criterion = nn.CrossEntropyLoss() # 适用于分类任务,标签是类别索引
29 optimizer_rnn_pt = optim.Adam(model_rnn_pt.parameters(), lr=0.001)
30
31 print("简单 RNN 模型结构 (PyTorch):")
32 print(model_rnn_pt)
33
34 # 训练模型
35 print("\n开始训练简单 RNN 模型 (PyTorch)...")
36 num_epochs = 10
37 for epoch in range(num_epochs):
38 for inputs, targets in dataloader:
39 # targets 的形状是 (batch_size, sequence_length)
40 # CrossEntropyLoss 期望输入形状 (N, C, ...) 和目标形状 (N, ...) 或 (N, C, ...)
41 # 如果目标是类别索引 (N, ...),则输入形状应为 (N, C, ...)
42 # 这里我们的模型输出形状是 (batch_size, sequence_length, vocab_size)
43 # targets 形状是 (batch_size, sequence_length)
44 # 需要将 output 和 targets reshape 以匹配 CrossEntropyLoss 的期望
45 # output 形状变为 (batch_size * sequence_length, vocab_size)
46 # targets 形状变为 (batch_size * sequence_length)
47 output = model_rnn_pt(inputs)
48 loss = criterion(output.view(-1, vocab_size), targets.view(-1))
49
50 optimizer_rnn_pt.zero_grad()
51 loss.backward()
52 optimizer_rnn_pt.step()
53
54 print(f'Epoch [{epoch+1}/{num_epochs}], Loss: {loss.item():.4f}')
55 print("简单 RNN 模型训练完成 (PyTorch)。")
56
57 # 预测示例
58 model_rnn_pt.eval() # 设置模型为评估模式
59 with torch.no_grad(): # 禁用梯度计算
60 sample_input_pt = torch.tensor([[1, 2, 3, 4, 5]], dtype=torch.long)
61 predictions_rnn_pt = model_rnn_pt(sample_input_pt)
62
63 # predictions_rnn_pt 的形状是 (batch_size, sequence_length, vocab_size)
64 print("\n简单 RNN 预测结果形状 (PyTorch):", predictions_rnn_pt.shape)
65
66 # 选取每个时间步概率最高的词索引作为预测结果
67 predicted_indices_rnn_pt = torch.argmax(predictions_rnn_pt, dim=-1)
68 print("简单 RNN 预测的词索引序列 (PyTorch):\n", predicted_indices_rnn_pt)

Appendix D2.3: 构建并训练 LSTM 模型 (Build and Train LSTM Model)

在 PyTorch 中,使用 nn.LSTM 模块构建 LSTM 模型。

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 # 构建 LSTM 模型 (Many-to-Many 架构)
2 class LSTMModel(nn.Module):
3 def __init__(self, vocab_size, embedding_dim, hidden_size, output_size):
4 super(LSTMModel, self).__init__()
5 self.embedding = nn.Embedding(vocab_size, embedding_dim)
6 self.lstm = nn.LSTM(embedding_dim, hidden_size, batch_first=True)
7 self.fc = nn.Linear(hidden_size, output_size)
8
9 def forward(self, x):
10 embedded = self.embedding(x)
11 # LSTM 返回 output, (hidden_state, cell_state)
12 # output 形状: (batch_size, sequence_length, hidden_size * num_directions)
13 output, (hidden, cell) = self.lstm(embedded)
14 output = output.contiguous().view(-1, hidden_size)
15 output = self.fc(output)
16 output = output.view(x.size(0), x.size(1), -1)
17 return output
18
19 # 实例化模型、定义损失函数和优化器
20 model_lstm_pt = LSTMModel(vocab_size, embedding_dim, hidden_size, vocab_size)
21 criterion = nn.CrossEntropyLoss()
22 optimizer_lstm_pt = optim.Adam(model_lstm_pt.parameters(), lr=0.001)
23
24 print("\nLSTM 模型结构 (PyTorch):")
25 print(model_lstm_pt)
26
27 # 训练模型 (过程与 SimpleRNN 类似)
28 print("\n开始训练 LSTM 模型 (PyTorch)...")
29 for epoch in range(num_epochs):
30 for inputs, targets in dataloader:
31 output = model_lstm_pt(inputs)
32 loss = criterion(output.view(-1, vocab_size), targets.view(-1))
33
34 optimizer_lstm_pt.zero_grad()
35 loss.backward()
36 optimizer_lstm_pt.step()
37
38 print(f'Epoch [{epoch+1}/{num_epochs}], Loss: {loss.item():.4f}')
39 print("LSTM 模型训练完成 (PyTorch)。")
40
41 # 预测示例
42 model_lstm_pt.eval()
43 with torch.no_grad():
44 predictions_lstm_pt = model_lstm_pt(sample_input_pt)
45 print("\nLSTM 预测结果形状 (PyTorch):", predictions_lstm_pt.shape)
46 predicted_indices_lstm_pt = torch.argmax(predictions_lstm_pt, dim=-1)
47 print("LSTM 预测的词索引序列 (PyTorch):\n", predicted_indices_lstm_pt)

Appendix D2.4: 构建并训练 GRU 模型 (Build and Train GRU Model)

在 PyTorch 中,使用 nn.GRU 模块构建 GRU 模型。

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 # 构建 GRU 模型 (Many-to-Many 架构)
2 class GRUModel(nn.Module):
3 def __init__(self, vocab_size, embedding_dim, hidden_size, output_size):
4 super(GRUModel, self).__init__()
5 self.embedding = nn.Embedding(vocab_size, embedding_dim)
6 self.gru = nn.GRU(embedding_dim, hidden_size, batch_first=True)
7 self.fc = nn.Linear(hidden_size, output_size)
8
9 def forward(self, x):
10 embedded = self.embedding(x)
11 # GRU 返回 output, hidden_state
12 # output 形状: (batch_size, sequence_length, hidden_size * num_directions)
13 output, hidden = self.gru(embedded)
14 output = output.contiguous().view(-1, hidden_size)
15 output = self.fc(output)
16 output = output.view(x.size(0), x.size(1), -1)
17 return output
18
19 # 实例化模型、定义损失函数和优化器
20 model_gru_pt = GRUModel(vocab_size, embedding_dim, hidden_size, vocab_size)
21 criterion = nn.CrossEntropyLoss()
22 optimizer_gru_pt = optim.Adam(model_gru_pt.parameters(), lr=0.001)
23
24 print("\nGRU 模型结构 (PyTorch):")
25 print(model_gru_pt)
26
27 # 训练模型 (过程与 SimpleRNN 类似)
28 print("\n开始训练 GRU 模型 (PyTorch)...")
29 for epoch in range(num_epochs):
30 for inputs, targets in dataloader:
31 output = model_gru_pt(inputs)
32 loss = criterion(output.view(-1, vocab_size), targets.view(-1))
33
34 optimizer_gru_pt.zero_grad()
35 loss.backward()
36 optimizer_gru_pt.step()
37
38 print(f'Epoch [{epoch+1}/{num_epochs}], Loss: {loss.item():.4f}')
39 print("GRU 模型训练完成 (PyTorch)。")
40
41 # 预测示例
42 model_gru_pt.eval()
43 with torch.no_grad():
44 predictions_gru_pt = model_gru_pt(sample_input_pt)
45 print("\nGRU 预测结果形状 (PyTorch):", predictions_gru_pt.shape)
46 predicted_indices_gru_pt = torch.argmax(predictions_gru_pt, dim=-1)
47 print("GRU 预测的词索引序列 (PyTorch):\n", predicted_indices_gru_pt)

Appendix D2.5: 其他架构示例 (Other Architecture Examples)

Many-to-One 架构: 在 PyTorch 的 RNN/LSTM/GRU 模块中,可以通过只取 output 的最后一个时间步或者利用返回的 hidden 状态(对于单层 RNN/GRU,hidden 形状是 (num_directions, batch_size, hidden_size);对于 LSTM 是 (num_directions, batch_size, hidden_size) 的元组)。

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 # Many-to-One LSTM 模型示例 (假设进行序列二分类)
2 class LSTMModelManyToOne(nn.Module):
3 def __init__(self, vocab_size, embedding_dim, hidden_size, num_classes):
4 super(LSTMModelManyToOne, self).__init__()
5 self.embedding = nn.Embedding(vocab_size, embedding_dim)
6 # LSTM 最后一层通常 batch_first=True
7 self.lstm = nn.LSTM(embedding_dim, hidden_size, batch_first=True)
8 self.fc = nn.Linear(hidden_size, num_classes) # 输出 num_classes
9
10 def forward(self, x):
11 embedded = self.embedding(x)
12 # LSTM 返回 output, (hidden_state, cell_state)
13 # output 形状: (batch_size, sequence_length, hidden_size * num_directions)
14 # hidden_state 形状: (num_layers * num_directions, batch_size, hidden_size)
15 output, (hidden, cell) = self.lstm(embedded)
16 # 取最后一个时间步的输出 (当 batch_first=True 时)
17 last_time_step_output = output[:, -1, AlBeRt63EiNsTeIn (batch_size, hidden_size)
18 # 或者使用最后一个隐藏状态 (对于单层 LSTM/GRU)
19 # last_hidden_state = hidden.squeeze(0) # 形状: (batch_size, hidden_size)
20
21 output = self.fc(last_time_step_output) # 形状: (batch_size, num_classes)
22 return output
23
24 # 实例化并编译/训练 (需要准备形状为 (num_samples,) 的分类标签)
25 # model_lstm_mto_pt = LSTMModelManyToOne(vocab_size, embedding_dim, hidden_size, 2)
26 # criterion_mto = nn.CrossEntropyLoss() # 如果是多分类
27 # criterion_mto = nn.BCEWithLogitsLoss() # 如果是二分类
28 # optimizer_mto = optim.Adam(model_lstm_mto_pt.parameters(), lr=0.001)
29 # ... 训练 ...

堆叠 RNN: 在 RNN/LSTM/GRU 模块中设置 num_layers 参数。

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 # 堆叠 LSTM 模型示例 (2层)
2 class StackedLSTMModel(nn.Module):
3 def __init__(self, vocab_size, embedding_dim, hidden_size, output_size, num_layers=2):
4 super(StackedLSTMModel, self).__init__()
5 self.embedding = nn.Embedding(vocab_size, embedding_dim)
6 # num_layers 设置堆叠层数
7 self.lstm = nn.LSTM(embedding_dim, hidden_size, num_layers=num_layers, batch_first=True)
8 self.fc = nn.Linear(hidden_size, output_size)
9
10 def forward(self, x):
11 embedded = self.embedding(x)
12 # output 形状: (batch_size, sequence_length, hidden_size * num_directions)
13 # hidden 形状: (num_layers * num_directions, batch_size, hidden_size)
14 output, (hidden, cell) = self.lstm(embedded)
15 # Many-to-Many 任务,取所有时间步的输出
16 output = output.contiguous().view(-1, hidden_size)
17 output = self.fc(output)
18 output = output.view(x.size(0), x.size(1), -1)
19 return output
20
21 # 实例化并编译/训练
22 # model_stacked_lstm_pt = StackedLSTMModel(vocab_size, embedding_dim, hidden_size, vocab_size, num_layers=2)
23 # ... 编译/训练 ...

双向 RNN: 在 RNN/LSTM/GRU 模块中设置 bidirectional=True 参数。注意,双向 RNN 的 hidden_size 会加倍(因为前向和反向各输出 hidden_size)。

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 # 双向 LSTM 模型示例
2 class BidirectionalLSTMModel(nn.Module):
3 def __init__(self, vocab_size, embedding_dim, hidden_size, output_size):
4 super(BidirectionalLSTMModel, self).__init__()
5 self.embedding = nn.Embedding(vocab_size, embedding_dim)
6 # bidirectional=True
7 self.lstm = nn.LSTM(embedding_dim, hidden_size, batch_first=True, bidirectional=True)
8 # 全连接层的输入维度是 hidden_size * 2 (前向和反向隐藏状态的拼接)
9 self.fc = nn.Linear(hidden_size * 2, output_size)
10
11 def forward(self, x):
12 embedded = self.embedding(x)
13 # output 形状: (batch_size, sequence_length, hidden_size * 2)
14 # hidden 形状: (num_layers * 2, batch_size, hidden_size)
15 output, (hidden, cell) = self.lstm(embedded)
16 output = output.contiguous().view(-1, hidden_size * 2)
17 output = self.fc(output)
18 output = output.view(x.size(0), x.size(1), -1)
19 return output
20
21 # 实例化并编译/训练
22 # model_bi_lstm_pt = BidirectionalLSTMModel(vocab_size, embedding_dim, hidden_size, vocab_size)
23 # ... 编译/训练 ...

通过这些基础示例,读者可以了解如何在 TensorFlow 和 PyTorch 中搭建不同类型的 RNN 模型。在实际应用中,还需要考虑更复杂的数据处理、模型调优、正则化以及使用预训练词嵌入等技术。

Appendix E: 超参数调优指南(Hyperparameter Tuning Guide)

循环神经网络(RNNs)及其变体(如 LSTM 和 GRU)虽然强大,但它们的性能很大程度上依赖于选择合适的超参数(Hyperparameters)。超参数是在训练过程开始之前设置的参数,而不是通过训练从数据中学习到的参数。精心调优超参数是获得高性能模型 Kritische Schritte(关键步骤)。本附录旨在为读者提供关于 RNN 模型超参数调优的系统性指南,涵盖常用的超参数、调优策略以及实践中的建议。

Appendix E1: 理解超参数的作用

超参数对模型的学习过程、收敛速度、泛化能力以及最终性能有着 profound effect(深远影响)。不同的超参数组合可能导致模型表现天壤之别。例如,学习率(Learning Rate)过高可能导致训练不稳定甚至发散,过低则可能导致收敛缓慢;隐藏单元数量(Number of Hidden Units)不足可能限制模型的表达能力,过多则可能导致过拟合(Overfitting)和计算成本增加。因此,理解每个超参数的作用是有效调优的基础。

Appendix E2: 重要的 RNN 超参数

以下是一些在训练 RNN 模型时最常见的、需要调优的超参数:

学习率(Learning Rate)
▮▮▮▮⚝ 定义:学习率控制了模型参数在每次更新时朝着梯度方向移动的步长。它是优化器(Optimizer)最重要的超参数之一。
▮▮▮▮⚝ 影响:
▮▮▮▮▮▮▮▮⚝ 高学习率:可能导致训练过程震荡甚至发散,无法收敛到最优解。
▮▮▮▮▮▮▮▮⚝ 低学习率:训练过程可能非常缓慢,容易陷入局部最优(Local Optima)。
▮▮▮▮⚝ 调优建议:通常从一个相对较大的值开始(如 0.01 或 0.001),然后逐渐降低。可以使用学习率调度(Learning Rate Scheduling)技术,在训练过程中动态调整学习率。

优化器(Optimizer)
▮▮▮▮⚝ 定义:优化器是用于更新模型参数以最小化损失函数(Loss Function)的算法。常见的有随机梯度下降(SGD)、Adam、RMSprop、Adagrad 等。
▮▮▮▮⚝ 影响:不同的优化器有不同的参数更新机制和收敛特性,对训练速度和最终性能有显著影响。例如,Adam 和 RMSprop 通常收敛更快,但可能更容易陷入次优解;SGD 加动量(Momentum)有时能获得更好的泛化能力。
▮▮▮▮⚝ 调优建议:Adam 或 RMSprop 通常是 RNNs 的良好起点,它们对学习率的敏感度相对较低。尝试不同的优化器并对比其性能。

隐藏单元数量(Number of Hidden Units)
▮▮▮▮⚝ 定义:RNN 层中隐藏状态向量的维度。
▮▮▮▮⚝ 影响:
▮▮▮▮▮▮▮▮⚝ 隐藏单元数量少:模型容量不足,难以学习复杂的模式,可能导致欠拟合(Underfitting)。
▮▮▮▮▮▮▮▮⚝ 隐藏单元数量多:模型容量过大,容易过拟合训练数据,计算成本和内存消耗增加。
▮▮▮▮⚝ 调优建议:没有普适的规则,通常需要根据数据集大小和任务复杂度进行实验。可以从几十或几百个单元开始,根据验证集(Validation Set)性能调整。

层数(Number of Layers)/ 深度(Depth)
▮▮▮▮⚝ 定义:堆叠的 RNN 层数量,形成深度 RNN(Deep RNN)。
▮▮▮▮⚝ 影响:增加层数可以提高模型的抽象能力,学习更复杂的层次化特征。然而,层数过多会增加训练难度(特别是梯度问题),计算成本急剧上升,且容易过拟合。
▮▮▮▮⚝ 调优建议:对于大多数任务,2-4 层已经足够。更深的层可能需要更复杂的训练技巧(如残差连接,但 RNNs 中不常用)。

Dropout 比率(Dropout Rate)
▮▮▮▮⚝ 定义:在训练过程中,随机地“关闭”(即将其输出设置为零)一定比例的神经元。循环 Dropout(Recurrent Dropout)则应用于循环连接(Recurrent Connections)。
▮▮▮▮⚝ 影响:Dropout 是一种有效的正则化(Regularization)技术,有助于防止过拟合,提高模型的泛化能力。比率过高可能导致欠拟合,比率过低则正则化效果不明显。
▮▮▮▮⚝ 调优建议:对于非循环连接(如输入到隐藏层、隐藏层到输出层),常用的 Dropout 比率在 0.2 到 0.5 之间。对于循环连接,如果框架支持循环 Dropout,通常比率设置得更低(如 0.05 到 0.1),因为对循环状态进行 Dropout 更敏感。

批量大小(Batch Size)
▮▮▮▮⚝ 定义:每次梯度更新使用的训练样本数量。
▮▮▮▮⚝ 影响:
▮▮▮▮▮▮▮▮⚝ 大批量:计算的梯度更准确(近似整体数据集梯度),可以更好地利用硬件并行性,训练可能更稳定,但可能收敛到尖锐的局部最优(泛化能力可能稍差),需要更多内存。
▮▮▮▮▮▮▮▮⚝ 小批量:引入更多噪声,可能有助于跳出局部最优(泛化能力可能更好),对内存需求较低,但并行性利用不充分,训练过程可能更震荡。
▮▮▮▮⚝ 调优建议:通常在几十到几百之间。受硬件内存限制较大。从小批量开始尝试,逐渐增大,观察性能变化。对于序列数据,批量处理还需要考虑序列长度。

序列长度(Sequence Length)/ BPTT 截断长度(Truncated BPTT Length)
▮▮▮▮⚝ 定义:
▮▮▮▮ⓐ 对于定长序列或通过 Padding/Truncating 统一长度的数据集,序列长度就是输入到模型的每个样本的时间步长。
▮▮▮▮ⓑ 对于非常长的序列(如文本或音频),为了控制计算复杂度和内存消耗,通常会使用截断的反向传播通过时间(Truncated Backpropagation Through Time - TBPTT),此时需要设置截断长度。
▮▮▮▮⚝ 影响:
▮▮▮▮ⓐ 如果序列中有长距离依赖,而截断长度太短,模型可能无法捕捉这些依赖。
▮▮▮▮ⓑ 序列长度或截断长度过长会显著增加计算时间和内存消耗。
▮▮▮▮⚝ 调优建议:取决于任务需求和计算资源。尽量覆盖重要的依赖范围,同时保持计算可行性。

词嵌入维度(Embedding Dimension)
▮▮▮▮⚝ 定义:将离散的词汇(或其他离散序列单元)映射到连续向量空间的维度。
▮▮▮▮⚝ 影响:嵌入维度影响了词汇表示的能力。维度过低可能无法充分捕捉语义信息;维度过高会增加模型参数,容易过拟合。
▮▮▮▮⚝ 调优建议:常见的值在 50 到 300 之间,取决于词汇量大小和数据集规模。可以考虑使用预训练的词嵌入(如 Word2Vec, GloVe, FastText)作为初始化或固定。

激活函数(Activation Function)
▮▮▮▮⚝ 定义:非线性函数,应用于隐藏层的输出。在 RNNs 中,tanh 和 ReLU 是常用的选择。
▮▮▮▮⚝ 影响:激活函数引入非线性,使网络能够学习复杂模式。Sigmoid/tanh 容易导致梯度消失(尤其在 Vanilla RNNs 中),而 ReLU 及其变体(如 Leaky ReLU)可以缓解梯度消失,但可能遇到死亡 ReLU(Dying ReLU)问题。在 LSTM/GRU 的门控机制中通常使用 Sigmoid 或 Hard Sigmoid。
▮▮▮▮⚝ 调优建议:对于隐藏状态的更新,tanh 曾经是标准,但 ReLU 或其变体在实践中往往表现更好。对于门控,坚持使用 Sigmoid 通常是安全的。

梯度裁剪阈值(Gradient Clipping Threshold)
▮▮▮▮⚝ 定义:为了应对梯度爆炸(Exploding Gradient)问题,当梯度的范数(Norm)超过某个阈值时,按比例缩小梯度。
▮▮▮▮⚝ 影响:有效的梯度裁剪可以稳定训练过程,防止参数更新步长过大。阈值设置过低可能限制模型学习能力;过高则裁剪无效。
▮▮▮▮⚝ 调优建议:通常通过观察训练过程中梯度的范数来确定一个合适的阈值。常用的范数类型是 L2 范数。阈值可以是一个固定的数值,或者基于批量梯度的统计量(如平均范数)来设置。

Appendix E3: 超参数调优策略(Hyperparameter Tuning Strategies)

手动调优超参数非常耗时且低效。以下是一些常用的自动化或半自动化调优策略:

网格搜索(Grid Search)
▮▮▮▮⚝ 原理:为每个待调超参数定义一个离散的取值范围(或列表),然后尝试所有可能的组合。对于每种组合,训练并评估模型在验证集上的性能。
▮▮▮▮⚝ 优缺点:
▮▮▮▮▮▮▮▮⚝ 优点:简单易懂,如果最优解在一个格点上,可以找到最优解。
▮▮▮▮▮▮▮▮⚝ 缺点:计算量巨大,随着超参数数量和每个参数取值数量的增加呈指数级增长。难以处理连续或大范围的超参数空间。容易遗漏网格点之间的最优解。
▮▮▮▮⚝ 适用场景:超参数数量较少,且每个参数的取值范围可以相对精确地确定。

随机搜索(Random Search)
▮▮▮▮⚝ 原理:不像网格搜索尝试所有组合,随机搜索在每个超参数预定义的连续或离散分布中随机采样固定数量的组合。
▮▮▮▮⚝ 优缺点:
▮▮▮▮▮▮▮▮⚝ 优点:相比网格搜索,通常能更快地找到接近最优的超参数组合,特别是在某些超参数对最终结果影响远大于其他参数时。计算效率更高。
▮▮▮▮▮▮▮▮⚝ 缺点:不能保证找到全局最优解,结果依赖于随机采样的次数。
▮▮▮▮⚝ 适用场景:超参数数量较多,或者对哪些超参数最重要不确定时。通常比网格搜索更有效。

贝叶斯优化(Bayesian Optimization)
▮▮▮▮⚝ 原理:贝叶斯优化是一种基于概率模型的顺序搜索方法。它建立一个关于目标函数(如验证集性能)的概率模型(常用高斯过程),该模型描述了超参数组合与性能之间的关系以及不确定性。然后使用采集函数(Acquisition Function)来决定下一个要评估的超参数组合,该组合通常是在模型预测性能高或不确定性大的区域。
▮▮▮▮⚝ 优缺点:
▮▮▮▮▮▮▮▮⚝ 优点:在评估目标函数(即训练模型)成本较高时非常高效,因为它尝试用最少的试验次数找到最优解。能够利用之前试验的结果来指导后续搜索。
▮▮▮▮▮▮▮▮⚝ 缺点:实现和理解比网格搜索和随机搜索复杂。初始阶段可能需要一些试验来构建初步模型。不适用于超高维度的超参数空间。
▮▮▮▮⚝ 适用场景:训练时间较长,希望用较少的尝试找到好的超参数组合。

早停法(Early Stopping)
▮▮▮▮⚝ 原理:虽然早停法本身不是一种超参数搜索方法,但它是超参数调优过程中非常重要的一环。在训练过程中,监控模型在验证集上的性能(如损失或准确率)。如果验证集性能在连续的若干个 Epoch(轮次)内没有改善,则提前停止训练。
▮▮▮▮⚝ 影响:防止模型在训练集上过拟合,节省训练时间。
▮▮▮▮⚝ 调优建议:需要设定一个“耐心”(Patience)值,即在性能停止改善后等待多少个 Epoch 再停止。耐心值需要根据数据集大小和模型复杂度调整。

Appendix E4: 实践中的调优技巧与建议(Tips and Best Practices)

始终使用验证集(Validation Set):绝不能使用测试集(Test Set)进行超参数调优。测试集只能用于最终模型的评估。将数据划分为训练集、验证集和测试集是标准做法。

从小规模数据或简化模型开始:在完整数据集上训练大型模型进行超参数搜索非常耗时。可以先在数据的一个子集上或使用一个更简单的模型架构上进行初步的超参数范围探索。

先调影响大的超参数:通常,学习率、优化器、隐藏单元数量和 Dropout 比率对模型性能影响最大,可以优先调优这些参数。

迭代调优:不要试图一次性调好所有超参数。可以采取迭代的方式:先粗略搜索几个关键参数的最佳范围,然后在这个范围内进行更精细的搜索,同时逐步引入和调优其他参数。

利用社区经验和文献:查阅与你的任务或模型类似的论文、博客或代码库,了解其他人通常使用的超参数值范围或调优方法。

记录每次试验结果:详细记录每次试验使用的超参数组合、训练过程中的性能曲线(训练集和验证集损失/指标)以及最终结果。这有助于分析不同参数的影响并避免重复劳动。可以使用工具如 TensorBoard, MLflow, Weights & Biases 等来追踪实验。

考虑计算资源:超参数调优通常需要大量的计算资源进行多次模型训练。根据可用的 GPU/CPU 资源选择合适的调优策略和并行度。

注意随机性:神经网络训练受到权重初始化、数据顺序等多种随机因素的影响。对于同一个超参数组合,重复训练几次并记录平均性能,可以获得更可靠的评估结果。

可视化训练过程:绘制训练集和验证集的损失曲线、准确率曲线等,可以直观地了解模型的训练状态,判断是否存在欠拟合或过拟合,从而指导超参数调整。例如,如果训练集损失下降但验证集损失很快停止下降甚至上升,说明可能过拟合,需要增加正则化(如 Dropout)或减少模型容量。

理解超参数之间的相互作用:超参数不是独立的,它们之间存在复杂的相互作用。例如,学习率与优化器类型、批量大小、模型深度等都有关联。调优时需要考虑这些相互作用。

超参数调优是一个 iterative(迭代)、empirical(经验性)的过程,没有一劳永逸的公式。需要结合理论知识、实践经验以及系统性的实验方法。通过本附录的指南,希望读者能够更有效地进行 RNN 模型的超参数调优, building robust and high-performing models(构建健壮且高性能的模型)。

Appendix F: 参考文献

本附录旨在为读者提供进一步深入学习循环神经网络 (Recurrent Neural Networks - RNNs) 及相关领域的参考资料。这些文献包括该领域的经典论文、权威著作以及重要的技术报告,它们构成了本书知识体系的基础,也代表了相关研究的前沿方向。对于希望在理论上深挖或在实践中应用 RNNs 的读者,这些资源将是宝贵的起点。

这些参考文献涵盖了从基础的 RNN 模型、训练算法(如反向传播通过时间 - BPTT)到更高级的架构(如长短期记忆网络 - LSTM、门控循环单元 - GRU)、重要的技术(如注意力机制 - Attention Mechanism)以及相关的现代序列模型(如 Transformer)。

Appendix F1: 重要书籍

《深度学习》 (Deep Learning)
▮▮▮▮👤 作者:Ian Goodfellow, Yoshua Bengio, Aaron Courville
▮▮▮▮🏛️ 出版社:MIT Press
▮▮▮▮📅 出版年份:2016
▮▮▮▮📝 简介:本书是深度学习领域的经典著作,涵盖了从基础数学知识到各种深度学习模型的全面内容。其中有专门的章节讨论序列模型,包括 RNNs、LSTMs 和 GRUs,提供了坚实的理论基础和深入的解释。

《神经网络与深度学习》 (Neural Networks and Deep Learning)
▮▮▮▮👤 作者:Michael Nielsen
▮▮▮▮🌐 网站:http://neuralnetworksanddeeplearning.com/
▮▮▮▮📝 简介:这是一本优秀的在线免费书籍,以清晰易懂的方式讲解神经网络的基础知识,包括反向传播算法。虽然不是完全专注于 RNNs,但其对基础概念的讲解对于理解深度学习模型的训练过程非常有帮助。

《模式识别与机器学习》 (Pattern Recognition and Machine Learning)
▮▮▮▮👤 作者:Christopher M. Bishop
▮▮▮▮🏛️ 出版社:Springer
▮▮▮▮📅 出版年份:2006
▮▮▮▮📝 简介:尽管本书出版年份较早,但它是机器学习领域的基石性著作,提供了概率论、统计学和机器学习算法(包括经典的神经网络章节)的坚实数学基础。理解这些基础对于深入理解任何复杂的深度学习模型都至关重要。

《自然语言处理入门》 (Speech and Language Processing)
▮▮▮▮👤 作者:Daniel Jurafsky, James H. Martin
▮▮▮▮🏛️ 出版社:Prentice Hall (当前为第三版草稿在线更新)
▮▮▮▮📝 简介:这是自然语言处理 (NLP) 领域的权威教材。本书详细介绍了 NLP 的各种任务和技术,其中包含了大量使用 RNNs (尤其是 LSTMs 和 GRUs) 处理文本、语音等序列数据的章节,提供了丰富的应用案例和技术细节。

Appendix F2: 经典与重要论文

RNN 的早期工作与反向传播通过时间 (BPTT)
▮▮▮▮ⓑ Finding Structure in Time
▮▮▮▮▮▮▮▮👤 作者:Jeffrey L. Elman
▮▮▮▮▮▮▮▮📰 发表:Cognitive Science, 1990
▮▮▮▮▮▮▮▮📝 简介:这篇论文被认为是现代 RNN (特别是 Simple RNN 或 Elman Network) 的开创性工作之一,展示了网络如何通过内部状态来处理和预测序列数据中的结构。

▮▮▮▮ⓑ Backpropagation Through Time: Training Neural Networks with Finite Duration Responses
▮▮▮▮▮▮▮▮👤 作者:Paul J. Werbos
▮▮▮▮▮▮▮▮📰 发表:Proceedings of the IEEE, 1990
▮▮▮▮▮▮▮▮📝 简介:这篇论文详细阐述了如何将反向传播算法应用于具有反馈连接的网络(即 RNNs)以训练它们处理序列数据,首次系统地提出了反向传播通过时间 (BPTT) 算法。

长短期记忆网络 (LSTM)
▮▮▮▮ⓑ Long Short-Term Memory
▮▮▮▮▮▮▮▮👤 作者:Sepp Hochreiter, Jürgen Schmidhuber
▮▮▮▮▮▮▮▮📰 发表:Neural Computation, 1997
▮▮▮▮▮▮▮▮📝 简介:这篇具有里程碑意义的论文首次提出了 LSTM 架构,通过引入细胞状态 (Cell State) 和门控机制 (Gate Mechanism) 有效解决了标准 RNN 在学习长期依赖 (Long-Term Dependencies) 时的梯度消失问题。

门控循环单元 (GRU)
▮▮▮▮ⓑ On the Properties of Neural Machine Translation: Encoder-Decoder Approaches from the Perspective of the Decoder
▮▮▮▮▮▮▮▮👤 作者:Kyunghyun Cho, Bart van Merrienboer, Dzmitry Bahdanau, Fethi Bougares, Holger Schwenk, Yoshua Bengio
▮▮▮▮▮▮▮▮📰 发表:arXiv preprint arXiv:1409.1259, 2014 (被 NIPS 2014 接收)
▮▮▮▮▮▮▮▮📝 简介:这篇论文在探讨神经机器翻译的 Encoder-Decoder 模型时,提出了一种新的循环单元,即门控循环单元 (GRU),它简化了 LSTM 的结构,并表现出相似的性能。

序列到序列模型与注意力机制 (Attention Mechanism)
▮▮▮▮ⓑ Sequence to Sequence Learning with Neural Networks
▮▮▮▮▮▮▮▮👤 作者:Ilya Sutskever, Oriol Vinyals, Quoc V. Le
▮▮▮▮▮▮▮▮📰 发表:NIPS 2014
▮▮▮▮▮▮▮▮📝 简介:这篇论文提出了使用两个 LSTM 网络构建 Encoder-Decoder 模型进行序列到序列学习的框架,并在机器翻译任务上取得了显著成果,为后续的序列转换模型奠定了基础。

▮▮▮▮ⓑ Neural Machine Translation by Jointly Learning to Align and Translate
▮▮▮▮▮▮▮▮👤 作者:Dzmitry Bahdanau, Kyunghyun Cho, Yoshua Bengio
▮▮▮▮▮▮▮▮📰 发表:ICLR 2015
▮▮▮▮▮▮▮▮📝 简介:这篇论文首次将注意力机制 (Attention Mechanism) 应用于神经机器翻译的 Encoder-Decoder 模型中,解决了传统 Encoder-Decoder 模型在处理长序列时依赖于固定长度上下文向量的瓶颈问题,允许模型在生成输出时“关注”输入序列的不同部分。

Transformer 模型 (作为 RNN 的重要替代方案)
▮▮▮▮ⓑ Attention Is All You Need
▮▮▮▮▮▮▮▮👤 作者:Ashish Vaswani, Noam Shazeer, Niki Parmar, Jakob Uszkoreit, Llion Jones, Aidan N. Gomez, Łukasz Kaiser, Illia Polosukhin
▮▮▮▮▮▮▮▮📰 发表:NIPS 2017
▮▮▮▮▮▮▮▮📝 简介:这篇论文提出了完全基于注意力机制 (Self-Attention) 而不使用循环或卷积结构的 Transformer 模型。尽管本书主要关注 RNNs,但 Transformer 是序列建模领域极其重要的进展,在许多任务上超越了 RNNs,理解它有助于更全面地把握序列模型的发展脉络。

Appendix F3: 其他资源与综述

循环神经网络教程与博客文章
▮▮▮▮📝 简介:网上有大量优秀的循环神经网络教程和博客文章,许多提供了直观的解释和代码示例。例如,Chris Olah 的博客文章 "Understanding LSTM Networks" 对 LSTM 的工作原理进行了非常清晰的可视化解释,对于初学者理解 LSTM 的内部机制非常有帮助。

深度学习框架文档
▮▮▮▮📝 简介:TensorFlow、PyTorch 等主流深度学习框架都提供了丰富的 RNN、LSTM、GRU 等模块的 API 文档和使用示例。阅读这些文档并动手实践是掌握如何在实际中应用这些模型的重要途径。

相关领域的综述论文
▮▮▮▮📝 简介:针对自然语言处理、时间序列分析等领域,经常会有关于深度学习方法,特别是 RNNs 及其变体的综述论文发表。这些综述可以帮助读者快速了解特定应用领域的研究现状和常用技术。

本附录仅列出了一部分基础且重要的参考文献。循环神经网络及其相关领域是一个快速发展的研究方向,读者应持续关注最新的研究进展和技术动态。

Appendix G: 术语表 (Glossary)

本附录汇集了本书中使用的关键术语及其简洁定义,旨在帮助读者回顾和巩固对循环神经网络 (RNNs) 及其相关概念的理解。

① 序列数据 (Sequence Data)
▮▮▮▮指元素之间存在先后顺序和依赖关系的数据,例如文本、时间序列、语音、DNA序列等。

② 循环神经网络 (Recurrent Neural Network - RNN)
▮▮▮▮一类专门用于处理序列数据的神经网络。与传统前馈网络不同,RNN 在其内部节点之间具有指向自身的连接,允许信息在不同时间步之间传递,形成“记忆”。

③ 深度学习 (Deep Learning)
▮▮▮▮机器学习的一个子领域,其核心是使用包含多个处理层的神经网络(即深度神经网络)来从数据中学习表示。

④ 序列模型 (Sequence Model)
▮▮▮▮用于处理或生成序列数据的模型,RNNs、LSTM、GRU 和 Transformer 等都属于序列模型。

⑤ 前馈神经网络 (Feedforward Neural Network)
▮▮▮▮一种基本类型的神经网络,信息仅单向从输入层经过隐藏层流向输出层,没有循环连接。

⑥ 隐藏状态 (Hidden State)
▮▮▮▮在 RNNs 或其他序列模型中,表示模型在当前时间步之前处理过的序列信息的内部“记忆”或状态向量。它捕捉了序列的历史信息。

⑦ 计算图 (Computation Graph)
▮▮▮▮一种有向无环图 (Directed Acyclic Graph - DAG),用于表示计算过程。在神经网络中,它描述了输入数据如何通过各层进行计算以得到输出,以及如何计算梯度。

⑧ 前向传播 (Forward Propagation)
▮▮▮▮在神经网络中,从输入层开始,按照计算图的顺序,通过网络的各层计算,最终得到网络输出的过程。

⑨ 反向传播 (Backpropagation)
▮▮▮▮用于训练神经网络的一种算法,通过计算损失函数相对于模型参数的梯度,并将梯度从输出层向输入层反向传播,从而更新参数。

⑩ 反向传播通过时间 (Backpropagation Through Time - BPTT)
▮▮▮▮用于训练循环神经网络 (RNNs) 的一种特殊形式的反向传播算法。它将 RNN 在时间维度上展开,然后应用标准的反向传播算法来计算沿时间步传播的梯度。

⑪ 损失函数 (Loss Function)
▮▮▮▮衡量模型预测输出与真实目标之间差异的函数。训练模型的目的是最小化损失函数。

⑫ 优化目标 (Optimization Objective)
▮▮▮▮训练模型时试图最小化的量,通常就是损失函数。

⑬ 梯度 (Gradient)
▮▮▮▮损失函数相对于模型参数的偏导数向量。梯度指示了损失函数值增加最快的方向,其反方向则指示了损失函数值下降最快的方向。

⑭ 梯度消失问题 (Vanishing Gradient Problem)
▮▮▮▮在训练深度网络或 RNNs 处理长序列时,梯度在反向传播过程中变得非常小,趋近于零,导致模型难以学习到距离较远的输入信息(即长期依赖)。

⑮ 梯度爆炸问题 (Exploding Gradient Problem)
▮▮▮▮在训练深度网络或 RNNs 时,梯度在反向传播过程中变得非常大,导致模型参数更新过大,训练过程不稳定,甚至无法收敛。

⑯ 长期依赖 (Long-Term Dependencies)
▮▮▮▮在序列数据中,当前时刻的输出或状态不仅依赖于最近的输入,还依赖于序列中较早时刻的输入信息。

⑰ 激活函数 (Activation Function)
▮▮▮▮神经网络中位于神经元输出端的非线性函数,用于引入非线性,使网络能够学习复杂的模式。常见的激活函数有 Sigmoid, Tanh, ReLU 等。

⑱ 权重矩阵 (Weight Matrix)
▮▮▮▮神经网络中用于连接神经元并存储模型学习到的权重的矩阵。在 RNN 中,通常包括输入到隐藏层、隐藏层到隐藏层以及隐藏层到输出层的权重矩阵。

⑲ 梯度裁剪 (Gradient Clipping)
▮▮▮▮一种应对梯度爆炸问题的技术。在模型训练过程中,如果计算出的梯度范数超过预设的阈值,则按比例缩小梯度,防止其过大。

⑳ 长短期记忆网络 (Long Short-Term Memory - LSTM)
▮▮▮▮一种特殊的 RNN 架构,通过引入门控机制(遗忘门、输入门、输出门)和细胞状态 (Cell State) 来有效缓解梯度消失问题,从而更好地学习和记忆长期依赖。

㉑ 门控单元 (Gate Unit)
▮▮▮▮在 LSTM 和 GRU 中使用的特殊结构,通常由一个 Sigmoid 激活函数和一个逐元素乘法操作组成。门控单元控制信息的流动,决定哪些信息可以通过。

㉒ 细胞状态 (Cell State)
▮▮▮▮ LSTM 单元中的一个重要组成部分,可以看作是一条在整个链上流动的“传送带”,信息可以轻松地添加到其上或从中移除,用于传递长期记忆。

㉓ 遗忘门 (Forget Gate)
▮▮▮▮ LSTM 中的一个门控单元,决定从细胞状态中丢弃哪些信息。

㉔ 输入门 (Input Gate)
▮▮▮▮ LSTM 中的一个门控单元,决定将当前输入中的哪些新信息存储到细胞状态中。它通常与一个由 Tanh 激活函数计算出的候选值一起工作。

㉕ 输出门 (Output Gate)
▮▮▮▮ LSTM 中的一个门控单元,决定基于当前输入、隐藏状态和细胞状态输出什么值。

㉖ 窥视孔连接 (Peephole Connections)
▮▮▮▮ LSTM 的一个变体,允许门控单元(遗忘门、输入门、输出门)直接“看到”细胞状态的值,从而增强门控机制对细胞状态的控制。

㉗ 门控循环单元 (Gated Recurrent Unit - GRU)
▮▮▮▮一种比 LSTM 结构更简单的门控 RNN 架构。它将 LSTM 的遗忘门和输入门合并为一个更新门 (Update Gate),并引入重置门 (Reset Gate),同时将细胞状态和隐藏状态合并。

㉘ 更新门 (Update Gate)
▮▮▮▮ GRU 中的一个门控单元,控制前一时间步的隐藏状态信息有多少保留到当前时间步的隐藏状态中,以及当前时间步的新信息有多少加入到隐藏状态中。

㉙ 重置门 (Reset Gate)
▮▮▮▮ GRU 中的一个门控单元,控制前一时间步的隐藏状态有多少被忽略,用于计算当前时间步的候选隐藏状态。

㉚ 双向循环神经网络 (Bidirectional RNN - BiRNN)
▮▮▮▮由两个方向相反的 RNN 组成,一个处理正向序列(从头到尾),另一个处理反向序列(从尾到头)。通过结合两个方向的隐藏状态,BiRNN 可以捕捉序列中任意位置的上下文信息。

㉛ 深度循环神经网络 (Deep RNN / Stacked RNN)
▮▮▮▮通过堆叠多个 RNN 层构建的网络。每一层的输入是前一层的隐藏状态,这使得网络能够学习更抽象或更高层次的序列表示。

㉜ 编码器-解码器模型 (Encoder-Decoder Model)
▮▮▮▮一种常用于序列到序列 (Sequence-to-Sequence) 任务的架构。编码器将输入序列处理并压缩成一个固定长度的上下文向量,解码器则根据该向量和之前生成的输出来生成目标序列。

㉝ 编码器 (Encoder)
▮▮▮▮在 Encoder-Decoder 模型中,负责处理输入序列并将其转换为一个固定长度的上下文向量或一组表示的组件。

㉞ 解码器 (Decoder)
▮▮▮▮在 Encoder-Decoder 模型中,负责接收编码器的输出(如上下文向量)并根据任务生成目标序列的组件。

㉟ 上下文向量 (Context Vector)
▮▮▮▮在经典的 Encoder-Decoder 模型中,编码器将整个输入序列压缩成的一个固定大小的向量,被认为是输入序列的语义表示,传递给解码器。在引入注意力机制后,上下文向量的概念有所扩展。

㊱ 序列到序列 (Sequence-to-Sequence - Seq2Seq)
▮▮▮▮一类机器学习任务,其输入和输出都是变长序列,例如机器翻译、文本摘要等。

㊲ 注意力机制 (Attention Mechanism)
▮▮▮▮一种技术,允许模型在处理当前序列元素时,有选择地关注输入序列中的不同部分,并分配不同的“注意力”权重。它解决了传统 Encoder-Decoder 模型在处理长序列时上下文向量瓶颈的问题。

㊳ Transformer
▮▮▮▮一种完全基于注意力机制(特别是自注意力机制,Self-Attention)的序列模型架构,在许多序列任务上取得了优于 RNNs 和 LSTMs 的性能。

㊴ Clockwork RNN
▮▮▮▮一种 RNN 变体,将隐藏层分成多个组,每个组以不同的时钟速度更新,旨在更好地处理不同时间尺度的依赖关系。

㊵ Echo State Network (ESN)
▮▮▮▮一种简化训练的 RNN 变体,其循环隐藏层(称为“储备池”,Reservoir)的权重是随机生成并保持固定的,只有输出层的权重是可训练的。

㊶ 自然语言处理 (Natural Language Processing - NLP)
▮▮▮▮计算机科学、人工智能和语言学领域的一个交叉学科,研究如何使计算机理解、处理和生成人类语言。RNNs 在 NLP 中有广泛应用。

㊷ 语言模型 (Language Modeling)
▮▮▮▮预测序列中下一个词或字符的概率分布的模型。常用于文本生成、语音识别等。

㊸ 机器翻译 (Machine Translation)
▮▮▮▮将一种自然语言的文本自动翻译成另一种自然语言的任务。Encoder-Decoder 模型加 Attention 是经典方法。

㊹ 文本生成 (Text Generation)
▮▮▮▮使用模型生成符合语法和语义规则的新文本序列的任务。

㊺ 情感分析 (Sentiment Analysis)
▮▮▮▮分析文本内容以确定作者的情感倾向(如积极、消极、中性)的任务。

㊻ 命名实体识别 (Named Entity Recognition - NER)
▮▮▮▮在文本中识别和分类具有特定意义的实体,如人名、地名、组织名、日期等。通常被视为一个序列标注任务。

㊼ 时间序列分析 (Time Series Analysis)
▮▮▮▮分析按时间顺序排列的数据,以识别模式、趋势、周期性,并进行预测或检测异常。

㊽ 时间序列预测 (Time Series Forecasting)
▮▮▮▮基于过去的时间序列数据,预测未来时间步数值的任务。

㊾ 异常检测 (Anomaly Detection)
▮▮▮▮识别时间序列或其他数据中不符合预期模式或行为的点或模式。

㊿ 语音识别 (Speech Recognition)
▮▮▮▮将人类语音转换为文本的任务。

⑤① 声学模型 (Acoustic Model)
▮▮▮▮在语音识别系统中,负责将声学特征映射到音素、音节或词汇的模型。RNNs 曾是常用的声学模型组件。

⑤② 数据预处理 (Data Preprocessing)
▮▮▮▮在将原始数据输入模型之前进行的清洗、转换、格式化等操作,以使其适合模型训练。

⑤③ 序列填充 (Padding)
▮▮▮▮在处理变长序列时,为了使同一批次 (Batch) 中的序列长度一致,在较短序列的末尾(或开头)填充特定值(如零)的操作。

⑤④ 截断 (Truncating)
▮▮▮▮在处理过长序列时,为了控制计算量,截断序列的末尾(或开头)的操作。

⑤⑤ 向量化 (Vectorization)
▮▮▮▮将文本、类别等非数值数据转换为数值向量表示的过程,例如词嵌入 (Word Embeddings)。

⑤⑥ 优化器 (Optimizer)
▮▮▮▮用于更新模型参数以最小化损失函数的算法,例如随机梯度下降 (SGD)、Adam、RMSprop 等。

⑤⑦ 学习率调度 (Learning Rate Scheduling)
▮▮▮▮在模型训练过程中,根据预设的策略(如根据 epoch 数、损失值变化等)动态调整学习率的方法。

⑤⑧ 正则化 (Regularization)
▮▮▮▮用于防止模型过拟合的技术,通过向损失函数添加惩罚项或修改网络结构来实现,例如 L1/L2 正则化、Dropout。

⑤⑨ Dropout
▮▮▮▮一种正则化技术,在训练过程中随机地“关闭”神经网络中的一部分神经元,以减少神经元之间的相互依赖,提高模型的泛化能力。

⑥⓪ 循环 Dropout (Recurrent Dropout)
▮▮▮▮一种专门用于 RNNs 的 Dropout 变体,通常应用于循环连接或输入到循环层的连接,且在同一序列的所有时间步使用相同的 Dropout 掩码。

⑥① L1/L2 正则化 (L1/L2 Regularization)
▮▮▮▮通过向损失函数添加模型权重绝对值之和(L1)或平方和(L2)的惩罚项来约束模型权重大小,防止过拟合。

⑥② 批量处理 (Batching)
▮▮▮▮在训练过程中,将多个数据样本组织成一个批次 (Batch),一次性输入到模型中进行计算和梯度更新,以提高计算效率。

⑥③ 模型评估指标 (Model Evaluation Metrics)
▮▮▮▮用于衡量模型在特定任务上性能的标准或度量,例如准确率 (Accuracy)、精确率 (Precision)、召回率 (Recall)、F1 分数、BLEU、ROUGE、困惑度等。

⑥④ 困惑度 (Perplexity)
▮▮▮▮在语言模型中常用的评估指标,衡量模型预测下一个词的不确定性或困惑程度,值越低表示模型性能越好。

⑥⑤ BLEU (Bilingual Evaluation Understudy)
▮▮▮▮机器翻译中常用的评估指标,基于 N-gram 的匹配来衡量机器翻译结果与参考译文的相似度。

⑥⑥ ROUGE (Recall-Oriented Understudy for Gisting Evaluation)
▮▮▮▮文本摘要和机器翻译中常用的评估指标,基于 N-gram 或序列匹配来衡量生成文本与参考文本的召回率。

⑥⑦ TensorFlow
▮▮▮▮一个由 Google 开发的开源深度学习框架,提供了构建和训练各种神经网络模型的工具和库。

⑥⑧ PyTorch
▮▮▮▮一个由 Facebook 开发的开源深度学习框架,以其灵活性和易用性受到欢迎。

⑥⑨ 多头注意力 (Multi-Head Attention)
▮▮▮▮Attention 机制的一种扩展,通过并行地执行多次注意力计算(使用不同的线性投影),并将结果拼接和再次投影来捕捉来自不同表示子空间的注意力信息。

⑦⓪ 扩张卷积 (Dilated Convolution)
▮▮▮▮一种卷积操作,在不增加参数和计算量的情况下扩大感受野 (Receptive Field)。在序列模型中,通过使用扩张卷积可以捕获长距离依赖,是某些 CNN 序列模型(如 WaveNet)的关键组成部分。