第一章 RNN介绍

1.1 RNN概念

RNN(Recurrent Neural Network,循环神经网络)是一种专门用来处理"有顺序的数据"的神经网络。说得通俗一点,它就像一个有"记忆"的人,在阅读一段话时,能记住前面看到的内容,从而更好地理解当前的内容。

为什么普通神经网络不够用呢? 让我们用一个例子来理解:

例子:“我吃了一个苹果,它很好吃”

普通神经网络处理这句话时,会把每个字当作独立的输入。它看到"它"这个字时,不知道"它"指的是什么。但 RNN 不一样,它记得前面看到了"苹果",所以能推断出"它"指的就是"苹果"。

💡 简单来说,RNN 的核心特点是:它在处理当前数据时,会同时考虑之前的信息。

用一句话概括 RNN 的工作原理:

“循环"意味着网络会反复做同样的操作,每一步都把"上一步的记忆"和"当前的输入"结合起来,产生"新的记忆"和"当前的输出”。这个过程就像滚雪球一样,越滚越大,信息不断累积。

RNN的三个关键概念

  1. 时间步(Time Step)
    RNN 是按"时间步"一步一步处理数据的。比如处理一句话"我爱你",第1步处理"我",第2步处理"爱",第3步处理"你"。每一步都会接收上一步传来的"记忆"。

  2. 隐藏状态(Hidden State)
    “隐藏状态"就是 RNN 的"记忆”。它是一个向量(一组数字),保存了到目前为止所有的历史信息。每一步,这个"记忆"都会被更新。

  3. 共享权重(Shared Weights)
    RNN 在每个时间步使用的是同一组参数(权重)。这就好比你用同一套规则去理解每一个字,而不是每个字都用不同的规则。这样的好处是:可以处理任意长度的序列,而且参数量不会随序列长度增加。

1.2 RNN应用场景

RNN 的强项是处理"序列数据",也就是那些前后顺序很重要的数据。以下是 RNN 的几个典型应用场景:

  1. 文本生成
    比如智能写作、聊天机器人。RNN 可以根据前面写的内容,预测下一个字应该是什么。例如,你输入"今天天气真",RNN 可能会预测下一个字是"好"。

  2. 机器翻译
    将一种语言翻译成另一种语言。翻译时需要考虑整句话的语境,而不是简单地逐字翻译。RNN 能够记住前面的内容,从而做出更准确的翻译。

  3. 情感分析
    判断一段文字是正面还是负面的。例如,"这部电影真棒!"是正面的,"这部电影太难受了"是负面的。RNN 可以通过分析整句话的语境来判断情感。

  4. 语音识别
    将说话的声音信号转化为文字。声音是一个时间序列,RNN 可以逐帧分析声音信号,结合上下文识别出具体的字词。

  5. 股票价格预测
    股票价格是一个典型的时间序列数据。RNN 可以利用历史价格数据来预测未来的价格走势。

💡 总结一下:只要数据有"前后顺序"的关系,就适合用 RNN 来处理。

1.3 自然语言处理概述

自然语言处理(Natural Language Processing,简称 NLP)是人工智能的一个重要分支,它的目标是让计算机能够理解、生成和处理人类语言。

为什么 NLP 很难? 因为人类语言非常复杂:

  1. 一词多义
    同一个词在不同语境下有不同的含义。比如"苹果"可以是水果,也可以是手机品牌。只有结合上下文才能确定它的含义。

  2. 语序很重要
    "我打你"和"你打我"用的词完全一样,但意思完全相反。这就是为什么处理语言需要考虑顺序信息——也正是 RNN 的强项。

  3. 语言的灵活性
    人类可以用很多种方式表达同一个意思,甚至可以说"话"不在字面上。这种灵活性给计算机处理带来了很大的挑战。

第二章 词嵌入层

2.1 词嵌入层的作用

计算机不能直接理解中文、英文这样的文字。在让神经网络处理文本之前,我们首先需要把文字转换成数字——这就是词嵌入层的工作。

为什么不能直接用数字编号?
你可能会想,给每个词编个号不就行了吗?比如:“苹果”=1,“香蕉”=2,“池子”=3。

问题在于:这样的编号会让网络误以为"苹果"和"香蕉"比较近(因为1和2很接近),而"苹果"和"池子"很远。但实际上,"苹果"和"香蕉"确实比较近(都是水果),但"苹果"和"池子"的"距离"不一定比"苹果"和"香蕉"远。简单的数字编号无法表达词与词之间的真实关系。

词嵌入的解决方案

词嵌入(Word Embedding)把每个词表示为一个**“向量”**(一组数字),而不是一个简单的编号。这个向量能够捕捉词语的语义信息。

举个例子,假设我们用三个数字来表示每个词:

词语向量表示
国王[0.9, 0.1, 0.1] → 偏向"皇室"特征
皇后[0.8, 0.1, 0.3] → 也偏向"皇室"特征
苹果[0.1, 0.9, 0.2] → 偏向"水果"特征
香蕉[0.1, 0.8, 0.2] → 也偏向"水果"特征

可以看到,"国王"和"皇后"的向量很接近,"苹果"和"香蕉"的向量也很接近。这样,网络就能"理解"哪些词的含义更相似。

💡 词嵌入层的核心作用:把离散的词语符号转换为密集的数值向量,使得语义相似的词在向量空间中也相似。

2.2 词嵌入层工作流程

词嵌入层的工作流程可以分为以下几个步骤:

步骤1:构建词汇表
首先,我们需要统计所有出现过的词,建立一个"词汇表"(Vocabulary)。比如我们有一篇文章,里面共出现了 5000 个不同的词,那么词汇表的大小就是 5000。

步骤2:词语编号
给词汇表中的每个词分配一个唯一的数字编号。比如:“的”=0,“我”=1,“是”=2,…。这个编号只是一个"索引",用来查找对应的嵌入向量。

步骤3:查表获取向量
词嵌入层内部维护一个"查找表"(嵌入矩阵)。当输入一个词的编号时,它会在这个表中查找对应的向量并输出。这就像查字典一样:你知道词的编号,查一下就能得到它的向量表示。

步骤4:输出嵌入向量
最终,每个词都被转换成了一个固定长度的向量(比如 128 维或 256 维)。这些向量就是 RNN 网络的实际输入。

💡 用一句话总结:文本 → 分词 → 编号 → 查嵌入表 → 向量。词嵌入层就是负责最后两步的。

2.3 分词器的使用

在使用词嵌入层之前,我们需要先把文本"切开"——这就是分词器的工作。

什么是分词器?

分词器(Tokenizer)的作用是把一段连续的文本切分成一个个的"词"或"字符"。就像小学生学课文时用铅笔划分词一样。

中文分词的特殊性
英文分词很简单,因为单词之间有空格分隔。但中文没有空格来分隔词语,所以需要专门的分词工具。例如:

"我爱人工智能"["我", "爱", "人工智能"]
注意,"人工智能"被作为一个整体词保留了下来,而不是被拆成单个字。这是因为分词器能识别常用词组。

常用的分词方法

  1. 字符级分词
    把每个字都作为一个独立的单元。比如"人工智能"→[“人”, “工”, “智”, “能”]。这种方法简单,但词汇表很大,而且丢失了词的完整含义。

  2. 词级分词
    把每个完整的词作为一个单元。比如"我爱人工智能"→[“我”, “爱”, “人工智能”]。这种方法更合理,但需要专门的分词工具。

  3. 子词分词(BPE)
    这是目前最常用的方法,它在字级和词级之间取了个平衡。常见的词保持完整,不常见的词被拆成子词。比如"不可思议"可能被拆成[“不”, “可”, “思”, “议”]。

实际使用中的特殊符号
在分词时,我们通常会添加一些特殊符号:

特殊符号说明
PAD填充符号,用于将不等长的序列补齐到相同长度
UNK未知词符号,用于替代词汇表中不存在的词
BOS序列开始符号,标记序列的起点
EOS序列结束符号,标记序列的终点
💡 分词器是词嵌入层的"前置工作",只有先把文本切分成词,才能进行嵌入操作。

2.4 词嵌入层的使用

在 PyTorch 中,使用词嵌入层非常简单。PyTorch 提供了 nn.Embedding 类来实现这个功能。

基本用法
创建一个词嵌入层只需要两个参数:

参数1:词汇表大小(num_embeddings)
你的词汇表中一共有多少个词。比如你有 10000 个不同的词,那么这个值就设为 10000。

参数2:嵌入维度(embedding_dim)
你希望每个词用多少个数字来表示。常见的选择有 128、256、512 等。维度越大,能表达的信息越多,但计算量也越大。

代码示例
以下是一个简单的使用示例:

import torch
import torch.nn as nn

# 创建词嵌入层:词汇表大小为10000,嵌入维度为128
embedding = nn.Embedding(num_embeddings=10000, embedding_dim=128)

# 假设我们有一个句子,分词后的编号为:
# ["我", "爱", "人工", "智能"]
word_indices = torch.tensor([42, 156, 789, 1023])

# 通过嵌入层获取向量
vectors = embedding(word_indices)

# 输出形状:[4, 128]
# 含义:4个词,每个词用128个数字表示
print(vectors.shape)  # torch.Size([4, 128])

从上面的代码可以看出,词嵌入层的使用非常简单:传入词的编号,就能得到对应的向量表示。

嵌入矩阵的理解

词嵌入层的内部实际上是一个大的"查找表"(矩阵),它的形状是 [num_embeddings, embedding_dim]。对于上面的例子,这个矩阵的大小是 [10000, 128],共有 10000 × 128 = 1,280,000 个参数。

这些参数在训练过程中会不断更新,最终学习到能够很好地表示词语语义的向量。训练初始时,这些向量是随机的;训练后,语义相似的词会**“靠得更近”**。

💡 词嵌入层的参数是可以训练的!它会随着整个模型一起训练,自动学习最佳的词语表示。

第三章 循环网络层【重点】

3.1 RNN网络层原理

这一章是整个 RNN 学习的核心。我们将用尽可能通俗的方式来解释 RNN 的内部原理。

从一个生活例子开始
想象你在看一部电视剧,每看完一集,你都会记住前面的剧情,然后用这些记忆来理解下一集。RNN 的工作方式就是这样的:

第1集:看完第1集,你有了初始的记忆。
第2集:结合"第1集的记忆"和"第2集的内容",产生新的记忆。
第3集:结合"第2集的记忆"和"第3集的内容",产生更新的记忆。
…以此类推。
RNN的内部结构
RNN 单元内部主要有以下几个部分:

  1. 输入(Input)
    当前时间步的输入数据。在 NLP 任务中,这通常是词嵌入层输出的向量。比如处理"我爱你"这句话,第一步的输入就是"我"的嵌入向量。

  2. 隐藏状态(Hidden State)
    这是 RNN 的**“记忆”**。它是一个向量,保存了到目前为止所有的历史信息。每一步,隐藏状态都会被更新。

  3. 输出(Output)
    当前时间步的输出。它是根据当前输入和隐藏状态计算得来的。

RNN的数学公式

RNN 的核心计算可以用两个简单的公式来表示:

ht​=tanh(Wh​⋅ht−1​+Wx​⋅xt​+b)
让我们逐个解释这个公式中的每个部分:

符号含义
h(t)当前时间步的隐藏状态(新的记忆)
x(t)当前时间步的输入
h(t-1)上一个时间步的隐藏状态(旧的记忆)
W_x输入权重矩阵,用于处理当前输入
W_h隐藏权重矩阵,用于处理旧的记忆
b偏置项(bias),类似于一元一次方程中的 b
tanh激活函数,把结果压缩到 -1 到 1 之间

用更通俗的话来说:

新记忆 = 处理过的(当前输入 + 旧记忆)

其中,W_x 和 W_h 就是"处理"过程中用到的参数(权重),tanh 是一个"调整器",确保记忆值不会越来越大或越来越小。

RNN的展开图解释

把 RNN 按时间步展开,就像一条链子一样:

时间步计算公式
t=1h(1)=tanh⁡(Wx⋅x(1)+Wh⋅h(0)+b)h(1) = \tanh(W_x \cdot x(1) + W_h \cdot h(0) + b)h(1)=tanh(Wxx(1)+Whh(0)+b)
t=2h(2)=tanh⁡(Wx⋅x(2)+Wh⋅h(1)+b)h(2) = \tanh(W_x \cdot x(2) + W_h \cdot h(1) + b)h(2)=tanh(Wxx(2)+Whh(1)+b)
t=3h(3)=tanh⁡(Wx⋅x(3)+Wh⋅h(2)+b)h(3) = \tanh(W_x \cdot x(3) + W_h \cdot h(2) + b)h(3)=tanh(Wxx(3)+Whh(2)+b)
可以看到,每一步的输出都依赖于上一步的输出。信息就这样一步一步地传递下去,就像一个**“循环”**。

RNN的前向传播
在实际使用中,RNN 的计算过程称为**“前向传播”**(Forward Propagation)。它的流程如下:

步骤1:初始化
设置初始隐藏状态 h(0) 为全零向量。这就像开始看电视剧之前,你的记忆是空的。

步骤2:逐步计算
从第一个时间步开始,依次计算每个时间步的隐藏状态和输出。每一步都会用到上一步的隐藏状态。

步骤3:获取最终输出
根据任务类型的不同,最终输出可能来自最后一个时间步的隐藏状态,也可能是所有时间步输出的组合。

RNN的不同类型
根据输入和输出的不同,RNN 可以分为以下几种类型:

  1. 一对一(One-to-One)
    最简单的类型,一个输入对应一个输出。其实就是普通的前馈神经网络。
  2. 多对一(Many-to-One)
    多个输入序列,只有一个输出。典型应用:情感分析、文本分类。比如输入一段话,输出"正面"或"负面"。
  3. 一对多(One-to-Many)
    一个输入,多个输出序列。典型应用:图片描述生成。比如输入一张图片,输出一段描述文字。
  4. 多对多(Many-to-Many)
    多个输入,多个输出。典型应用:机器翻译、文本生成。比如输入一段中文,输出对应的英文。

RNN的梯度与反向传播(BPTT)

和普通神经网络一样,RNN 也需要通过反向传播来训练。但 RNN 的反向传播有一个特殊的名字:“随时间反向传播”(Backpropagation Through Time,简称 BPTT)。

它的核心思想是:先按时间顺序计算所有时间步的输出,然后从最后一个时间步开始,反向计算梯度。因为每个时间步都共享同一组权重,所以梯度会沿着时间反向累积。

RNN的常见问题:梯度消失与梯度爆炸

RNN 有一个著名的缺点:当序列很长时,它会**“忘记"早期的信息。这就是所谓的"梯度消失”**问题。

用电视剧的例子来说:如果你看了 100 集电视剧,你可能已经忘记了第 1 集的细节。RNN 也一样,当序列很长时,早期的信息在传递过程中会逐渐衰减。

解决这个问题的方案是使用 RNN 的改进版本,如 LSTM(长短记忆网络)和 GRU(门控循环单元)。它们通过引入"门"的机制来控制信息的流动,从而能够更好地保留长期信息。

💡 小结:RNN 的核心思想是"循环"——每一步都结合旧记忆和新输入来产生新记忆。它的主要缺点是难以记住很长的序列信息,这个问题由 LSTM 和 GRU 来解决。

3.2 PyTorch RNN层的使用

PyTorch 提供了 nn.RNN 类来实现循环神经网络层。下面我们来学习如何使用它。

nn.RNN的主要参数

参数含义示例
input_size输入特征的维度128(嵌入维度)
hidden_size隐藏状态的维度256
num_layersRNN层的堆叠层数2(双层RNN)
batch_first输入是否为 [batch, seq, feature] 格式True
bidirectional是否使用双向RNNTrue / False

基本使用示例
以下是一个完整的使用示例:

import torch
import torch.nn as nn

# 创建RNN层
rnn = nn.RNN(
    input_size=128,      # 输入维度(词嵌入维度)
    hidden_size=256,     # 隐藏状态维度
    num_layers=2,        # 堆叠2层RNN
    batch_first=True,    # 输入格式:[batch, seq_len, input_size]
    bidirectional=False  # 单向RNN
)

# 准备输入数据
# batch_size=32, seq_len=10, input_size=128
input_data = torch.randn(32, 10, 128)

# 初始化隐藏状态
# h_0的形状:[num_layers * num_directions, batch, hidden_size]
h_0 = torch.zeros(2, 32, 256)

# 前向传播
output, h_n = rnn(input_data, h_0)

# 查看输出形状
print(output.shape)  # [32, 10, 256] - 每个时间步的输出
print(h_n.shape)     # [2, 32, 256]  - 最后一步的隐藏状态

输入和输出详解
让我们详细解释上面代码中的输入和输出:

输入:input_data
形状为 [batch_size, seq_len, input_size],即 [32, 10, 128]。这表示 32 个样本,每个样本是一个长度为 10 的序列,每个时间步的输入是 128 维的向量。

输出:output
形状为 [batch_size, seq_len, hidden_size],即 [32, 10, 256]。这是每个时间步的输出。如果你需要每个时间步都有输出(比如序列标注任务),就用这个输出。

输出:h_n
形状为 [num_layers, batch_size, hidden_size],即 [2, 32, 256]。这是每一层最后一个时间步的隐藏状态。如果你只需要最终的输出(比如文本分类任务),就用这个输出。

完整的文本分类示例
下面是一个完整的使用 RNN 进行文本分类的示例:

import torch
import torch.nn as nn

class TextClassifier(nn.Module):
    def __init__(self, vocab_size, embed_dim, hidden_size,
                 num_classes, num_layers=1):
        super().__init__()
        # 1. 词嵌入层
        self.embedding = nn.Embedding(vocab_size, embed_dim)
        # 2. RNN循环层
        self.rnn = nn.RNN(
            input_size=embed_dim,
            hidden_size=hidden_size,
            num_layers=num_layers,
            batch_first=True
        )
        # 3. 全连接层(分类头)
        self.fc = nn.Linear(hidden_size, num_classes)

    def forward(self, x):
        # x形状: [batch_size, seq_len]
        embedded = self.embedding(x)     # [batch, seq_len, embed_dim]
        output, h_n = self.rnn(embedded) # output: [batch, seq, hidden]
                                       # h_n: [layers, batch, hidden]
        # 取最后一层的最后一个时间步的隐藏状态
        final_hidden = h_n[-1]           # [batch, hidden_size]
        # 通过全连接层得到分类结果
        logits = self.fc(final_hidden)    # [batch, num_classes]
        return logits

# 使用示例
model = TextClassifier(
    vocab_size=10000,   # 词汇表大小
    embed_dim=128,      # 嵌入维度
    hidden_size=256,    # 隐藏维度
    num_classes=2,      # 二分类(正面/负面)
    num_layers=2        # 双层RNN
)

# 模拟输入:一批次 32 个句子,每个句子长度为 20
input_text = torch.randint(0, 10000, (32, 20))

# 前向传播
output = model(input_text)
print(output.shape)  # [32, 2] - 每个样本得到2个类别的得分

双向RNN(Bidirectional RNN)

普通的 RNN 只从前往后处理序列,但有时候我们也需要考虑**“后面的内容”**。例如:

“我吃了一个苹果,它很好吃”

如果从后往前看,看到"它"时,后面的"苹果"就在前面,也能帮助理解。

双向 RNN 就是同时从前往后和从后往前处理序列,然后把两个方向的结果合并。在 PyTorch 中,只需要设置 bidirectional=True 即可:

rnn = nn.RNN(
    input_size=128,
    hidden_size=256,
    num_layers=1,
    batch_first=True,
    bidirectional=True  # 启用双向RNN
)

# 注意:双向RNN的隐藏状态维度会变成 2 * hidden_size
# 因为前向后向各有一个隐藏状态
input_data = torch.randn(32, 10, 128)
output, h_n = rnn(input_data)

print(output.shape)  # [32, 10, 512]  (注意:256*2=512)
print(h_n.shape)     # [2, 32, 256]   (注意:第一个是前向,第二个是后向)

RNN与LSTM、GRU的对比

虽然本章主要讲 RNN,但在实际工作中,我们更常用 RNN 的改进版本——LSTM 和 GRU。以下是它们的对比:

特性RNNLSTMGRU
结构复杂度简单复杂中等
长期记忆
训练速度中等
参数量中等
适用场景短序列任务长序列任务中等序列任务

在 PyTorch 中,将 RNN 替换为 LSTM 或 GRU 非常简单,只需要把 nn.RNN 换成 nn.LSTM 或 nn.GRU 即可,其他代码基本不需要改动。

💡 实践建议:如果序列较短(少于20步),用 RNN 就够了;如果序列较长,建议使用 LSTM 或 GRU。

总结

通过本文的学习,我们从三个方面全面了解了 RNN 循环神经网络:

  1. RNN基础认知
    RNN 是一种具有"记忆"的神经网络,专门用于处理序列数据。它通过隐藏状态保存历史信息,通过共享权重处理任意长度的序列。它在 NLP、语音识别、时间序列预测等领域有着广泛的应用。
  2. 词嵌入层
    词嵌入层是连接文本和神经网络的桥梁。它把离散的词语符号转换为密集的数值向量,使得语义相似的词在向量空间中也相似。分词器是其前置工具,负责将连续文本切分为可处理的单元。
  3. 循环网络层
    RNN 的核心是"循环"机制:每一步都结合旧记忆和新输入来更新记忆。它的主要缺点是梯度消失问题,可以通过 LSTM 和 GRU 来解决。在 PyTorch 中,使用 nn.RNN 可以很方便地构建 RNN 模型。
Logo

北京人形旗下天工造物具身智能开源社区,聚焦具身天工与慧思开物两大平台

更多推荐