自然处理语言NLP:RNN、RNN基本结构、输入输出关系、RNNCell、单层单向和双向RNN、BPTT

RNN

一、为什么需要 RNN?

在自然语言处理、时间序列预测等任务中,数据往往以序列形式存在(如句子是单词的序列,股票价格是时间点的序列),且序列中前后元素存在时序依赖关系(如 “下雨天要带伞” 中,“下雨” 与 “伞” 存在逻辑关联)。

传统的前馈神经网络(如全连接网络)存在两个致命缺陷

  1. 无法处理变长序列:输入长度固定,而实际序列(如句子)长度不固定;
  2. 没有 “记忆” 能力:每个输入的处理独立于历史信息,无法捕捉时序依赖。

RNN(循环神经网络)通过循环结构引入 “记忆” 机制,让模型能:

  1. 捕捉时序依赖:传统前馈神经网络假设输入独立,而RNN通过隐藏状态(Hidden State)传递历史信息,建模序列中前后元素的关系。
  2. 处理变长输入:RNN可以处理任意长度的序列(通过时间步展开),适用于动态长度的输入输出。
  3. 共享参数:RNN在不同时间步共享权重参数( W h h , W x h {W}_{hh}, {W}_{xh} Whh,Wxh),减少参数量并提升泛化能力。

二、RNN 的原理

RNN(循环神经网络,Recurrent Neural Network)是一类专门用于处理序列数据的神经网络,其核心特点是通过 “循环结构” 引入 “记忆” 机制,能够捕捉序列中元素的时序依赖关系,在自然语言处理、时间序列分析等领域有着广泛应用。

RNN 的核心是隐藏状态(Hidden State) 的循环传递,它通过将当前输入与前一时刻的隐藏状态结合,生成新的隐藏状态。这个过程使得RNN能够记住序列中的历史信息,并在后续时间步中使用这些信息。隐藏状态 S t {S}_{t} St不仅作为当前时间步的输出特征,还传递给下一时刻的RNN单元,形成时间上的循环连接。


三、RNN 模型框架与内部结构

常见的RNN架构如下图两种:

在这里插入图片描述

左图可以理解为,先将每一层的网络简化,再将网络旋转90度得到的简化图

而右边两种类型,可以理解为,再将左图继续进行简化

1. RNN 的基本结构

RNN(循环神经网络)的核心设计是通过隐藏状态(Hidden State)传递历史信息,从而建模序列数据的时序依赖性。其结构主要包括以下三部分:

在这里插入图片描述

(1) 输入层(Input Layer)

  • 功能:接收序列数据的每个时间步的输入 x t {x}_{t} xt(如文本中的单词、时间序列中的数值)。
  • 输入形状:通常为 (batch_size, sequence_length, input_size) ,其中:
    • batch_size:批量大小。
    • sequence_length:序列长度(时间步数)。
    • input_size:每个时间步的特征维度。

(2) 隐藏层(Hidden Layer)

  • 核心:隐藏状态 S t {S}_{t} St 是 RNN 的记忆单元,用于存储序列的历史信息。

  • 递归计算:每个时间步的隐藏状态由当前输入 x t {x}_{t} xt 和前一时刻的隐藏状态 S t − 1 {S}_{t-1} St1 共同决定。

  • 数学公式
    s t = f ( U ⋅ x t + W ⋅ s t − 1 ) s_t=f(\mathbf{U}·x_t+\mathbf{W}·s_{t-1}) st=f(Uxt+Wst1)

    在这个公式中, S t {S}_{t} St表示在时间步t的隐藏状态, x t {x}_{t} xt是当前时间步的输入,UW分别是输入到隐藏状态和隐藏状态到隐藏状态的权重矩阵。

(3) 输出层(Output Layer)

  • 功能:根据隐藏状态生成当前时间步的输出
  • 数学公式 O t {O}_{t} Ot = g(V s t {s}_{t} st)
    • O t {O}_{t} Ot是时间步t的输出
    • V是从隐藏状态到输出层的权重矩阵
    • g是另一个非线性函数,常用于输出层

(4) 循环连接(Recurrent Connections)

  • 关键机制:隐藏状态 s t {s}_{t} st 在时间步间传递,形成链式结构(图1)。
  • 特点
    • 参数共享:所有时间步共享相同的权重
      • U是输入到隐藏层的权重矩阵
      • W是隐藏层到隐藏层的权重矩阵
      • V是隐藏层到输出的权重矩阵
    • 时序依赖:当前输出不仅依赖当前输入,还依赖所有历史信息(通过 s t − 1 {s}_{t-1} st1)。

2. RNN 的内部工作流程
  1. 初始化
    • 初始隐藏状态 S 0 {S}_{0} S0 通常设为零向量。
  2. 时间步循环
    • 对每个时间步 t = 1 {t} = 1 t=1 T {T} T
      • 计算隐藏状态 S t {S}_{t} St
      • 生成输出 y t {y}_{t} yt
  3. 输出方式
    • 序列到单值:仅返回最后一个时间步的输出 y T {y}_{T} yT(如分类任务)。
    • 序列到序列:返回所有时间步的输出 y 1 , y 2 , … , y T {y}_{1}, {y}_{2}, \dots, {y}_{T} y1,y2,,yT(如序列标注)。

在这里插入图片描述

在这里插入图片描述

3.RNN模型输入输出关系对应模式

在这里插入图片描述

通过改变RNN的结构,即调整其输入和输出的数量和形式,可以让它适应各种不同的任务。以下是几种常见的RNN结构调整示例,以及它们各自适用的任务类型:

  1. 一对多(One-to-Many):这种结构输入是单个样本(非序列),输出是变长序列这种模式常用于“看图说话”的任务,即给定一张图片(单个输入),RNN生成一段描述该图片的文本(一系列输出)。在这种情况下,RNN的结构被调整为首先对输入图片进行编码,然后根据这个编码连续生成文本序列中的词语。
  2. 多对一(Many-to-One):输入是变长序列,输出是单个结果,即 “用一串时序数据提炼一个总结性结论”,是 RNN 处理序列分类的最常用模式。这种结构适用于如文本分类和情感分析等任务,其中模型需要阅读和理解整个文本(一系列输入),然后决定文本属于哪个类别(单个输出)。在图片生成的上下文中,这种结构可以通过分析一系列的特征或指令来生成单个图片输出。
  3. 多对多(Many-to-Many):这种结构输入是变长序列,输出也是变长序列,是 RNN 处理 “时序到时序” 映射的核心模式。这在需要输入和输出均为序列的任务中非常有用,例如机器翻译,其中模型需要读取一个语言的文本(一系列输入),然后生成另一种语言的对应文本(一系列输出)。另一个例子是小说生成,其中RNN可以基于给定的开头或主题(一系列输入),连续生成故事的后续内容(一系列输出)。

4. PyTorch 实现示例(简单 RNN)
import torch
import torch.nn as nn

# 定义模型
input_size = 4       # 输入特征维度
hidden_size = 3      # 隐藏层维度
num_layers = 1       # 单层
batch_first = True   # 输入形状为 (batch, seq_len, input_size)

rnn = nn.RNN(input_size, hidden_size, num_layers, batch_first=True)

# 模拟输入 (batch_size=2, sequence_length=5)
input = torch.randn(2, 5, input_size)
h0 = torch.zeros(num_layers, 2, hidden_size)  # 初始隐藏状态

# 前向传播
output, hn = rnn(input, h0)
print("Output shape:", output.shape)  # (2, 5, 3)
print("Final hidden state:", hn.shape)  # (1, 2, 3)

四、RNN 实现代码

1.核心参数
参数作用默认值示例
input_size输入特征的维度(每个时间步的输入特征数)。必须指定input_size=4 表示每个时间步有 4 个特征。
hidden_size隐藏状态的维度(每个时间步的隐藏层输出特征数)。必须指定hidden_size=3 表示隐藏层输出 3 个特征。
num_layersRNN 层数(堆叠的 RNN 单元数量)。1num_layers=2 表示堆叠两个 RNN 层。
nonlinearity非线性激活函数(仅适用于 RNN,可选 tanhrelu)。tanhnonlinearity='relu' 使用 ReLU 激活函数。
bias是否使用偏置(True 表示使用偏置项)。Truebias=False 表示不添加偏置。
batch_first输入/输出张量的维度顺序是否以 batch 为第一维(True 表示 [batch, seq, feature])。Falsebatch_first=True 时输入形状为 (batch_size, sequence_length, input_size)
dropout除最后一层外的 RNN 层之间的 dropout 概率(0 表示不使用)。0dropout=0.5 表示在每层之间随机丢弃 50% 的神经元。
bidirectional是否为双向 RNN(True 表示同时处理正向和反向序列)。Falsebidirectional=True 会生成双向 RNN,输出维度翻倍(2 * hidden_size)。

假设正在处理一个文本分类任务,每个单词已经被嵌入为一个100维的向量,我们的序列长度(sequence length)是50(即最长句子有50个单词),批量大小(batch size)是32(一次处理32个句子),我们设定的隐藏层大小(hidden size)是128。

1. 输入维度

  • 每个单词被嵌入为 100 维向量input_size = 100)。
  • 句子长度固定为 50 个单词sequence_length = 50)。
  • 批量大小32 个句子batch_size = 32)。
  • 输入张量形状(batch_size, sequence_length, input_size)(32, 50, 100)

2. 隐藏层计算

  • RNN 每个时间步处理一个单词的嵌入向量(100 维),并基于上一时间步的隐藏状态生成当前隐藏状态。
  • 隐藏状态维度设置为 128hidden_size = 128)。
  • 每个时间步的隐藏状态形状(batch_size, hidden_size)(32, 128)

3. 输出维度

  • RNN 输出包含所有时间步的隐藏状态,形状为:
    (batch_size, sequence_length, hidden_size)(32, 50, 128)
  • 分类任务通常仅使用 最后一个时间步的隐藏状态[32, 128])作为输入。
  • 全连接层(Dense Layer)将 128 维隐藏状态映射到分类数量(如二分类 → 2 维)。
  • 最终输出形状(batch_size, num_classes)(32, 2)(表示 32 个样本的分类概率分布)。

【示例代码】手动实现一个简单的RNN的前向传播过程

import numpy as np

# 初始化参数【s,h】
x = np.random.rand(3,2)

# 定义RNN参数
input_size = 2  # 词嵌入之后的结果
hidden_size = 3
output_size = 6 # 输出头 词表大小

# 初始化权重矩阵
U = np.random.rand(input_size,hidden_size)  # 输入权重矩阵
W = np.random.rand(hidden_size,hidden_size) # 隐藏层权重矩阵
V = np.random.rand(hidden_size,output_size) # 输出权重矩阵

# 偏置
b_h = np.zeros((hidden_size,))
b_o = np.zeros((output_size,))
def tanh(x):
    return np.tanh(x)

# 初始化隐藏层状态
H_prev = np.zeros((hidden_size,))

# 解码时间步 :每个单词
# 第一个单词
x1 = x[0,:]
H1 = tanh(np.dot(x1,U) + np.dot(H_prev,W) + b_h)
y1 = np.dot(H1,V) + b_o

# 时间步2
x2 = x[1,:]
H2 = tanh(np.dot(x2,U) + np.dot(H1,W) + b_h)
y2 = np.dot(H2,V) + b_o

# 时间步3
x3 = x[2,:]
H3 = tanh(np.dot(x3,U) + np.dot(H2,W) + b_h)
y3 = np.dot(H3,V) + b_o

print("时间步1的结果",H1,y1)
print("时间步2的结果",H2,y2)
print("时间步3的结果",H3,y3)

时间步1的结果 [0.15786976 0.14616117 0.19599538] [0.30847211 0.29100385 0.22966804 0.35643783 0.33077437 0.23346962]
时间步2的结果 [0.46865815 0.43445979 0.47375999] [0.81418741 0.80473318 0.65843277 0.96870814 0.92042698 0.65268124]
时间步3的结果 [0.79376069 0.70767975 0.75874627] [1.32934236 1.32428635 1.08278497 1.59445713 1.5166766  1.07724672]

2.RNNCell

RNNCell 是构成 RNN(循环神经网络)的基本单元,负责处理 单个时间步 的输入,并基于前一时间步的隐藏状态更新当前隐藏状态。两者是 “组件” 与 “整体” 的关系:RNN 本质上是多个 RNNCell 在时间维度上的串联,通过循环调用 RNNCell 实现对序列数据的处理。

RNNCell 的工作流程

  1. 初始化 - 隐藏状态 h 0 h_0 h0 初始化为全零向量,形状为 [ b a t c h _ s i z e , h i d d e n _ s i z e ] [batch\_size, hidden\_size] [batch_size,hidden_size]
  2. 逐时间步处理 - 对每个时间步 t t t,将输入 x t x_t xt 和上一时刻的隐藏状态 h t − 1 h_{t-1} ht1 输入 RNNCell,计算得到当前隐藏状态 h t h_t ht 和输出 y t y_t yt
  3. 循环展开 - RNNCell 被重复调用 s e q u e n c e _ l e n g t h sequence\_length sequence_length 次,形成完整的 RNN 层。

【RNNCell代码实现】

"""
    承上启下为后面rnn api做准备
"""
import torch
from torch import nn

class RNNCellBase(nn.Module):
    '''
    batch_first=True输入数据直接为 [batch_size, seq_len, input_size]
    '''
    def __init__(self,input_size,hidden_size,batch_first):
        super(RNNCellBase, self).__init__()
        self.rnncell = nn.RNNCell(input_size, hidden_size)
        self.input_size = input_size
        self.hidden_size = hidden_size
        # 判断数据格式的flag是否是RNN需要的【seq_len,batch_size,embedding_size】
        self.batch_first = batch_first
    '''
    如果 batch_first=False,
    输入格式为 [seq_len, batch_size, input_size]
    '''

    # 初始化隐藏状态
    def _initialize_hidden(self,batch_size):
        return torch.zeros(batch_size,self.hidden_size)

    def forward(self,input,p_rev=None):
        if self.batch_first:
            batch_size,seq_len,_ = input.size()
        else:
            seq_len,batch_size,_ = input.size()

        hiddens = [] # 初始化隐藏状态的结果
        if p_rev is None:
            p_rev = self._initialize_hidden(batch_size)

        hidden_t = p_rev

        # 时间步解码
        for t in range(seq_len):
            hidden_t = self.rnncell(input[:,t,:],hidden_t)
            hiddens.append(hidden_t)

        # 堆叠所有隐藏层状态结果成为一个新的张量
        hiddens = torch.stack(hiddens)

        # 数据转为【b,s,hidden_size】
        if self.batch_first:
            hiddens = hiddens.permute(1, 0, 2)
        return hiddens

if __name__ == '__main__':
        rnncell = RNNCellBase(10, 20, batch_first=True)
        # 模拟词嵌入之后的结果
        input = torch.randn(5, 3, 10)
        output = rnncell(input)
        print(output.shape)

nn.RNNCell 本质上只返回隐藏状态,它没有单独的输出结果。一般在 RNN 中,隐藏状态既可以被视为输出,也可以通过一个线性层将隐藏状态转化为实际的输出。

3.单层单向 RNN

步骤 1:导入库

import torch
from torch import nn

步骤 2:定义模型参数

vocab_size = 10       # 词汇表大小(输入的索引范围)
input_size = 5        # 词嵌入维度(每个词转换为 input_size 维向量)
hidden_size = 20      # RNN 隐藏层维度
batch_first = True    # 输入/输出格式为 [batch_size, seq_len, ...]

步骤 3:定义 RNN 模型类

class RNN(nn.Module):
    def __init__(self, vocab_size, input_size, hidden_size, batch_first):
        super(RNN, self).__init__()
        # 1. 词嵌入层:将词汇索引转为向量
        self.embed = nn.Embedding(vocab_size, input_size)
        # 2. RNN 层:处理序列数据
        self.rnn = nn.RNN(input_size, hidden_size, batch_first=batch_first)
        # 3. 输出层:将隐藏状态映射回词汇表大小
        self.fn = nn.Linear(hidden_size, vocab_size)
  • Embedding:将离散的词汇索引(如 [1, 3, 5])转为连续向量。
  • RNN:处理序列输入,输出每个时间步的隐藏状态。
  • Linear:将隐藏状态转换为词汇表大小的输出(如预测下一个词)。

步骤 4:定义前向传播逻辑

    def forward(self, x):
        # 1. 词嵌入:[batch_size, seq_len] → [batch_size, seq_len, input_size]
        x = self.embed(x)
        # 2. RNN:[batch_size, seq_len, input_size] → [batch_size, seq_len, hidden_size]
        x, h = self.rnn(x)
        # 3. 输出层:[batch_size, seq_len, hidden_size] → [batch_size, seq_len, vocab_size]
        x = self.fn(x)
        return x

步骤 5:测试模型

if __name__ == '__main__':
    # 生成输入数据:[batch_size=4, seq_len=3](每个样本有3个词)
    x = torch.randint(9, (4, 3))
    # 实例化模型
    model = RNN(vocab_size, input_size, hidden_size, batch_first)
    # 前向传播
    out = model(x)
    # 输出形状:[4, 3, 10](batch_size × seq_len × vocab_size)
    print(out.shape)
    print(out)
  • 输出是每个时间步对下一个词的预测概率(共 10 个可能的词)。
4.单层双向RNN

双向RNN(Bidirectional RNN,Bi-RNN)是标准RNN的扩展,核心在于同时处理序列的正向和反向信息。与单向RNN仅依赖过去的时间步不同,Bi-RNN通过并行运行两个独立的RNN层——一个从前向后处理序列,另一个从后向前处理——最终将两者的隐藏状态融合,从而捕捉更全面的上下文依赖关系。

在这里插入图片描述

Bi-RNN的两个方向在计算时互不干扰,正向RNN基于当前输入和前一时间步的状态生成隐藏状态,反向RNN则基于当前输入和后一时间步的状态生成隐藏状态。在每一步,这两个方向的隐藏状态会被拼接或加权求和,形成最终的隐藏状态。例如,在自然语言处理中,判断某个词的含义时,Bi-RNN能同时参考其前后的词汇信息,而单向RNN只能依赖单侧上下文。


实现

在代码实现上,Bi-RNN的关键参数是bidirectional=True,这会使得RNN层的输出维度翻倍(如hidden_size*2

self.birnn = nn.RNN(input_size, hidden_size, batch_first=batch_first,bidirectional= True)
        self.fn = nn.Linear(hidden_size*2, vacab_size)

例如,使用PyTorch的nn.RNN时,若设置双向模式,输出形状会从[batch_size, seq_len, hidden_size]变为[batch_size, seq_len, 2*hidden_size]。此外,Bi-RNN的隐藏状态通常需要手动拼接(如正向和反向的最后一个时间步状态),或通过全局池化(如平均池化)压缩序列长度,以适配多对一的任务需求。

output = torch.mean(out, dim=1)

接下来是完整实现步骤:

步骤 1:导入库与定义参数

import torch
import torch.nn as nn

# 模型参数
vocab_size = 10      # 词表大小(输入的索引范围)
input_size = 5       # 词嵌入维度
hidden_size = 20     # RNN 隐藏层维度
batch_first = True   # 输入/输出格式为 [batch_size, seq_len, ...]
  • 双向 RNN 特点:与单向 RNN 不同,双向 RNN 同时处理序列的正向和反向信息。bidirectional=True 是关键参数,会将输出扩展为 2 * hidden_size

步骤 2:定义模型结构

class BIRNN(nn.Module):
    def __init__(self, vocab_size, input_size, hidden_size, batch_first):
        super(BIRNN, self).__init__()
        # 1. 词嵌入层:将词汇索引转为向量
        self.embedding = nn.Embedding(vocab_size, input_size)
        # 2. 双向 RNN 层:处理序列,输出正向和反向隐藏状态
        self.birnn = nn.RNN(input_size, hidden_size, batch_first=batch_first, bidirectional=True)
        # 3. 输出层:将双向隐藏状态拼接后映射回词表大小
        self.fn = nn.Linear(hidden_size * 2, vocab_size)  # 双向输出需乘以 2

与单向 RNN 的区别

  • 单向 RNN 的输出维度是 [batch_size, seq_len, hidden_size]
  • 双向 RNN 的输出维度是 [batch_size, seq_len, 2 * hidden_size],因为正向和反向隐藏状态被拼接。

步骤 3:前向传播逻辑

    def forward(self, x):
        x = self.embedding(x)        # [4,3] → [4,3,5](词嵌入)
        out, h_n = self.birnn(x)     # [4,3,5] → [4,3,40](双向 RNN 输出)
        output = torch.mean(out, dim=1)  # [4,3,40] → [4,40](取时间步的平均值)
        out = self.fn(output)        # [4,40] → [4,10](映射回词表大小)
        return out
  • 双向 RNN 输出out 包含所有时间步的正向和反向隐藏状态。例如,输入长度为 3 的序列,输出形状为 [4,3,40]2 * hidden_size = 40)。
  • 全局池化:使用 torch.mean(out, dim=1) 将时间步维度压缩,得到固定长度的表示([4,40]),适合“多对一”任务(如分类)。
  • 单向 RNN 对比:单向 RNN 通常直接取最后一个时间步的隐藏状态( s n {s}_{n} sn),而双向 RNN 需要额外处理正向和反向的 s n {s}_{n} sn(例如拼接)。

步骤 4:测试模型

if __name__ == '__main__':
    # 生成输入数据:[batch_size=4, seq_len=3](每个样本有 3 个词)
    x = torch.randint(9, (4, 3))
    model = BIRNN(vocab_size, input_size, hidden_size, batch_first)
    out = model(x)
    print(out.shape)  # 输出形状: [4, 10](4 个样本 × 10 个词预测)
    print(out)

输出解释

  • 双向 RNN 的最终输出是 [batch_size, vocab_size],每个样本对应一个类别预测(如分类任务)。
  • 单向 RNN 在“多对一”任务中通常只取最后一个时间步的隐藏状态( s n {s}_{n} sn),而双向 RNN 需要结合正向和反向的 s n {s}_{n} sn(例如拼接后输入线性层)。

完整代码展示:

import  torch
import torch.nn as nn


# 模型参数
vacab_size = 10
input_size = 5
hidden_size = 20
batch_first = True

class BIRNN(nn.Module):
    def __init__(self,vacab_size,input_size,hidden_size,batch_first):
        super(BIRNN, self).__init__()
        # 1.词嵌入层
        self.embedding = nn.Embedding(vacab_size, input_size)
        # 2.RNN
        self.birnn = nn.RNN(input_size, hidden_size, batch_first=batch_first,bidirectional= True)
        self.fn = nn.Linear(hidden_size*2, vacab_size)
    def forward(self, x):
        x = self.embedding(x)        # [4,3] -> [4,3,5]
        out, h_n = self.birnn(x)     # [4,3,5] -> [4,3,40]
        output = torch.mean(out, dim=1)  # [4,3,40] -> [4,40]
        out = self.fn(output)        # [4,40] -> [4,10]
        return out


if __name__ == '__main__':
    # 初始数据
    # 生成一个形状为 [4, 3] 的张量,
    # 其中每个元素是从 0 到 8 的随机整数(不包括 9)
    x = torch.randint(9,(4,3))

    model = BIRNN(vacab_size,input_size,hidden_size,batch_first)
    out = model(x)
    print(out.shape)
    print(out)

注意事项

  1. 双向 RNN 的隐藏状态拼接

    • s n {s}_{n} sn 的形状是 [2, batch_size, hidden_size]2 表示正向和反向)。若需要直接使用 s n {s}_{n} sn,需手动拼接:

      h_n = torch.cat([h_n[0], h_n[1]], dim=1)  # [batch_size, 2 * hidden_size]
      
    • 本例通过全局池化简化了处理,避免手动拼接。

  2. 双向 RNN 的训练效率

    • 双向 RNN 会翻倍计算量,但能捕捉更全面的上下文信息(正向和反向依赖)。
  3. 任务适配性

    • “多对一”任务(如分类)需将序列压缩为固定长度表示(如平均池化)。
    • “多对多”任务(如序列标注)需保留所有时间步的输出(如 NER 任务)。

五、RNN训练方法—BPTT

BPTT的核心思想

BPTT(Backpropagation Through Time)是训练循环神经网络(RNN)的核心算法,其本质是将RNN在时间维度上展开为一个等效的深层前馈网络,然后应用反向传播计算梯度并更新参数。
给定长度为 T {T} T 的输入序列 x 1 , x 2 , … , x T {x}_{1}, {x}_{2}, \dots, {x}_{T} x1,x2,,xT,RNN在每个时间步共享参数 U {U} U W {W} W V {V} V,并通过隐藏状态 s t {s}_{t} st 传递历史信息。BPTT从最后一个时间步 t = T {t} = {T} t=T 开始,将误差沿时间轴反向传播,计算损失对所有参数的梯度,最终完成参数更新。

在这里插入图片描述

BPTT的计算流程
  • 前向传播

在每个时间步 t {t} t,RNN依次计算隐藏状态和输出:

s t = tanh ⁡ ( U x t + W s t − 1 + b ) {s}_{t} = \tanh({U} {x}_{t} + {W} {s}_{t-1} + {b}) st=tanh(Uxt+Wst1+b)

o t = V s t + c {o}_{t} = {V} {s}_{t} + {c} ot=Vst+c

若用于分类任务,输出层通常接 softmax 函数得到预测分布 y ^ t = s o f t m a x ( o t ) {\hat{y}}_{t} = \mathrm{softmax}({o}_{t}) y^t=softmax(ot)。初始状态 s 0 {s}_{0} s0 一般设为零向量。

  • 损失计算

    总损失为各时间步损失之和:

L = ∑ t = 1 T L t {L} = \sum_{t=1}^{T} {\mathcal{L}}_{t} L=t=1TLt,其中 L t {\mathcal{L}}_{t} Lt 可为交叉熵损失 − ∑ y t log ⁡ y ^ t -\sum {y}_{t} \log {\hat{y}}_{t} ytlogy^t

  • 反向传播

t = T {t} = {T} t=T 开始,逐时间步反向计算梯度:

  • 输出误差: δ o t = ∂ L ∂ o t {\delta} {o}_{t} = \frac{\partial {L}}{\partial {o}_{t}} δot=otL

  • 隐藏层误差: δ s t = V ⊤ δ o t + W ⊤ δ s t + 1 ⊙ ( 1 − s t 2 ) {\delta} {s}_{t} = {V}^\top {\delta} {o}_{t} + {W}^\top {\delta} {s}_{t+1} \odot (1 - {s}_{t}^2) δst=Vδot+Wδst+1(1st2)

(注意: δ s T + 1 = 0 {\delta} {s}_{T+1} = 0 δsT+1=0

  • 参数梯度累加

    每个时间步对共享参数的梯度贡献如下:

∂ L ∂ U + = δ s t x t ⊤ {\frac{\partial {L}}{\partial {U}}} += {\delta} {s}_{t} {x}_{t}^\top UL+=δstxt

∂ L ∂ W + = δ s t s t − 1 ⊤ {\frac{\partial {L}}{\partial {W}}} += {\delta} {s}_{t} {s}_{t-1}^\top WL+=δstst1

∂ L ∂ V + = δ o t s t ⊤ {\frac{\partial {L}}{\partial {V}}} += {\delta} {o}_{t} {s}_{t}^\top VL+=δotst

  • 参数更新

使用梯度下降法更新参数:

U ← U − η ∂ L ∂ U {U} \leftarrow {U} - {\eta} {\frac{\partial {L}}{\partial {U}}} UUηUL

W ← W − η ∂ L ∂ W {W} \leftarrow {W} - {\eta} {\frac{\partial {L}}{\partial {W}}} WWηWL

V ← V − η ∂ L ∂ V {V} \leftarrow {V} - {\eta} {\frac{\partial {L}}{\partial {V}}} VVηVL

RNN 中的主要问题

a.梯度消失与梯度爆炸

RNN 通过 BPTT训练时,梯度需要沿时间轴多次连乘。若权重矩阵的特征值小于1,梯度会指数级衰减(梯度消失);若特征值大于1,则梯度会指数级爆炸(梯度爆炸)。这导致模型难以学习长期依赖关系。

  • 数学原理
    对于隐藏状态 s t = tanh ⁡ ( W s t − 1 + U x t ) {s}_{t} = \tanh({W} {s}_{t-1} + {U} {x}_{t}) st=tanh(Wst1+Uxt),其梯度计算涉及:
    ∂ L ∂ W = ∑ t = 1 T ∂ L ∂ s t ∂ s t ∂ W = ∑ t = 1 T ∏ k = 1 t ∂ s k ∂ s k − 1 ⋅ ∂ s 1 ∂ W {\frac{\partial {L}}{\partial {W}}} = \sum_{t=1}^{T} {\frac{\partial {L}}{\partial {s}_{t}}} {\frac{\partial {s}_{t}}{\partial {W}}} = \sum_{t=1}^{T} \prod_{k=1}^{t} {\frac{\partial {s}_{k}}{\partial {s}_{k-1}}} \cdot {\frac{\partial {s}_{1}}{\partial {W}}} WL=t=1TstLWst=t=1Tk=1tsk1skWs1
    ∂ s k ∂ s k − 1 < 1 {\frac{\partial {s}_{k}}{\partial {s}_{k-1}}} < 1 sk1sk<1,连乘效应会导致梯度消失;反之则爆炸。

b.长期依赖问题

RNN 理论上能处理任意长度的序列,但实际中难以捕捉远距离依赖。例如,在句子“只有在记住最前面的 student 是复数还是单数才能准确得到后面的结果”中,RNN 可能忽略开头信息,导致预测错误。

根本原因
梯度消失使得早期时间步的权重无法有效更新,导致模型无法学习到长期依赖。

c.计算资源消耗大

  • 训练速度慢:RNN 依赖序列顺序计算,无法并行化,导致长序列训练效率低。
  • 内存开销高:BPTT 需存储所有时间步的中间状态,长序列时占用大量内存。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值