Vanilla RNN

Vanilla RNN

传统神经网络的局限性

  1. 输入输出固定
  2. 计算步骤固定
    • 层数固定,每次处理都走一遍计算图
    • 不具备处理任意长度输入序列的能力

引入 RNN 一个有趣案例

如何设计一个循环网络检测重复的 1

输入序列:X = [0, 1, 0, 1, 1, 1, 0, 1, 1] 目标输出:Y = [0, 0, 0, 0, 1, 1, 0, 0, 1] image.png

具体设计要点

  1. 隐藏状态 ht 设计为:

  1. 每一步计算需要记录当前输入到 current, 上一时间步的 current 输入到 previous, 偏置为 1

于是

Why = [1, 1, −1]

然后 Whyht 相乘再经过 ReLU 变换,即可实现要求

代码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import numpy as np

h_0 = np.array([[0], [0], [1]])

w_xh = np.array([[1], [0], [0]])

w_hh = np.array([
[0, 0, 0],
[1, 0, 0],
[0, 0, 1]
])

w_yh = np.array([1, 1, -1])

x_in = [0, 1, 0, 1, 1, 1, 0, 1, 1, 0, 1, 1, 0, 1, 0, 0, 0, 0, 1, 1]

x_seq = np.array([[x] for x in x_in]) # Format properly for matmul ops

h_t_prev = h_0

def relu(x):
return np.maximum(0, x)

y_seq = []
for t, x in enumerate(x_seq):
h_t = relu(w_hh @ h_t_prev + (w_xh @ x).reshape(3,1))
y_t = relu(w_yh @ h_t)
y_seq.append(y_t)
h_t_prev = h_t

print("Inputs: ", [int(x) for x in x_seq.flatten()])
print("Outputs:", [int(y[0]) for y in y_seq])


RNN 的优势

  1. 能处理变长的序列(输入输出均可变长)
    • 一个时间步一个计算单元,不是增加网络层,而是增加时间步
  2. 每一步的计算共享权重,可无限展开
    • 相当于用同一个“程序块”在时间上不断迭代执行
    • 时间维度的”深度”是动态的,而不是结构上死板的堆层数
  3. 从函数式学习走向“可微程序设计”

If training vanilla neural nets is optimization over functions, training recurrent nets is optimization over programs.


RNN 的结构

运作原理

RNN 主要由输入层、隐藏层和输出层组成,其中隐藏层会在每个时间步(t)维护一个隐藏状态 ht

ht = f(Whht − 1 + Wxxt + b)

yt = g(Wyht + by)

(注意这是简单循环网络,也称 Elman 网络, 还有 jordan 网络)

image.png

pytorch 前向传播代码

注意 pytorch 将循环步骤封装到了 nn. RNN 模块里了,具体实现参照原码和 cs231n 作业

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import torch
import torch.nn as nn

# RNN model
class SimpleRNN(nn.Module):
def __init__(self, input_size, hidden_size, output_size):
super(SimpleRNN, self).__init__()
self.rnn = nn.RNN(input_size, hidden_size, batch_first=True)
self.fc = nn.Linear(hidden_size, output_size)

def forward(self, x):
out, _ = self.rnn(x)
out = self.fc(out[:, -1, :]) # 取最后一个时间步的输出
return out

input_size = 10
hidden_size = 20
output_size = 1

model = SimpleRNN(input_size, hidden_size, output_size)
x = torch.randn(5, 3, 10) # (batch_size=5, seq_len=3, input_size=10)
output = model(x)
print(output.shape) # 输出 (5, 1)



种类

many to many

image.png
应用 输入序列 输出序列 例子
语音识别 音频帧序列 字符或单词序列 语音 → 文本
机器翻译 源语言句子 目标语言句子 English → 中文
视频字幕生成 视频帧序列 描述词语序列 视频 → 字幕
音乐生成 前面几个音符 后面音符 连续旋律生成

大致流程:训练+推理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
import torch
import torch.nn as nn
import torch.nn.functional as F

class RNNLanguageModel(nn.Module):
def __init__(self, vocab_size, embed_size, hidden_size):
super().__init__()
self.embedding = nn.Embedding(vocab_size, embed_size)
self.rnn = nn.RNN(embed_size, hidden_size, batch_first=True)
self.fc = nn.Linear(hidden_size, vocab_size)
self.hidden_size = hidden_size

def forward(self, x, hidden=None):
"""
前向传播(用于训练):
x: (batch_size, seq_len) --> 输入词 id 序列
返回: (batch_size, seq_len, vocab_size)
"""
emb = self.embedding(x) # (B, T, embed_size)
if hidden is None:
hidden = torch.zeros(1, x.size(0), self.hidden_size, device=x.device)
out, _ = self.rnn(emb, hidden) # (B, T, hidden)
logits = self.fc(out) # (B, T, vocab_size)
return logits

def generate(self, start_token_id, end_token_id, max_len=20, device='cpu'):
"""
推理生成函数:逐步生成序列直到遇到 <END>
返回:生成的 token id 序列(包含 START,不包含 END)
"""
self.eval()
input_id = torch.tensor([[start_token_id]], device=device) # 初始输入 (1, 1)
hidden = torch.zeros(1, 1, self.hidden_size, device=device)
generated_ids = [start_token_id]

for _ in range(max_len):
emb = self.embedding(input_id) # (1, 1, embed_size)
out, hidden = self.rnn(emb, hidden) # (1, 1, hidden)
logits = self.fc(out.squeeze(1)) # (1, vocab_size)
probs = F.softmax(logits, dim=-1)
next_id = torch.argmax(probs, dim=-1).item()

if next_id == end_token_id:
break
generated_ids.append(next_id)
input_id = torch.tensor([[next_id]], device=device)

return generated_ids

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 假设词表
vocab = {"<START>": 0, "<END>": 1, "hello": 2, "world": 3}
inv_vocab = {v: k for k, v in vocab.items()}
vocab_size = len(vocab)

model = RNNLanguageModel(vocab_size, embed_size=32, hidden_size=64)

# 训练阶段
x = torch.tensor([[0, 2, 3]]) # <START> hello world
target = torch.tensor([[2, 3, 1]]) # hello world <END>
logits = model(x)
loss_fn = nn.CrossEntropyLoss()
loss = loss_fn(logits.view(-1, vocab_size), target.view(-1))
loss.backward()

# 推理阶段
generated_ids = model.generate(start_token_id=vocab["<START>"],
end_token_id=vocab["<END>"],
max_len=10)
generated_tokens = [inv_vocab[i] for i in generated_ids]
print("Generated:", generated_tokens)

many to one

  • image.png

应用

应用 输入序列 输出单值
情感分类 一段评论(多个词) 情绪是正向 / 负向
问题分类 一个问题的文本 问题类别
股票预测 一段时间的价格序列 下一日涨/跌
语音命令识别 音频帧序列 命令类型(如“打开灯”)

代码 与上面 many to many 类似,只用改一行 将

1
2
3
4
5
def forward(self, x):
out, _ = self.rnn(x)
logits = self.fc(out) # (B, T, vocab_size)
return logits

改成

1
2
3
4
5
6
def forward(self, x):
out, _ = self.rnn(x) # (B, T, hidden)
last = out[:, -1, :] # (B, hidden)
logits = self.fc(last) # (B, vocab_size)
return logits

注意可以用 flag 控制,选择不同 rnn 种类,类似 cs231n assignment2 那样

one to many

一般在推理时用,代码框架见下面的decoder.。训练阶段通常是 teacherforcing, 类似many-to-many image.png

场景 输入 输出
图像字幕生成 图像特征向量 单词序列描述
音频生成 起始音频帧或噪声向量 完整音频序列
文本生成(从 prompt) 一个初始 token 多个生成词
编码器-解码器中的解码器 上一步 hidden 或 多个 y

Seq2Seq (Many-to-One + One-to-Many)

image.png

应用场景 机器翻译,语音识别, 图像字幕生成, 对话系统 ,语音识别,文本生成(摘要/诗歌)

代码框架示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
import torch
import torch.nn as nn
import random

# Many-to-One
class Encoder(nn.Module):
def __init__(self, input_vocab_size, embed_size, hidden_size):
super().__init__()
self.embedding = nn.Embedding(input_vocab_size, embed_size)
self.rnn = nn.RNN(embed_size, hidden_size, batch_first=True)

def forward(self, src):
embedded = self.embedding(src) # (B, T_src, E)
_, hidden = self.rnn(embedded) # hidden: (1, B, H)
return hidden


# One-to-Many
class Decoder(nn.Module):
def __init__(self, output_vocab_size, embed_size, hidden_size):
super().__init__()
self.embedding = nn.Embedding(output_vocab_size, embed_size)
self.rnn = nn.RNN(embed_size, hidden_size, batch_first=True)
self.fc = nn.Linear(hidden_size, output_vocab_size)

def forward(self, input_token, hidden):
# input_token: (B, 1), hidden: (1, B, H)
embedded = self.embedding(input_token) # (B, 1, E)
output, hidden = self.rnn(embedded, hidden)
logits = self.fc(output) # (B, 1, V)
return logits, hidden


# Seq2Seq
class Seq2Seq(nn.Module):
def __init__(self, encoder, decoder, device):
super().__init__()
self.encoder = encoder
self.decoder = decoder
self.device = device

def forward(self, src, tgt, teacher_forcing_ratio=0.5):
"""
src: (B, T_src), tgt: (B, T_tgt)
返回: logits: (B, T_tgt, vocab_size)
"""
batch_size, tgt_len = tgt.shape
vocab_size = self.decoder.fc.out_features
logits = torch.zeros(batch_size, tgt_len, vocab_size).to(self.device)

hidden = self.encoder(src) # 初始 hidden 来自 encoder 最后隐藏状态
input_token = tgt[:, 0].unsqueeze(1) # 通常是 <START> token

for t in range(1, tgt_len):
output, hidden = self.decoder(input_token, hidden)
logits[:, t] = output.squeeze(1)
#teacher-forcing
teacher_force = random.random() < teacher_forcing_ratio
top1 = output.argmax(2) # (B, 1)
input_token = tgt[:, t].unsqueeze(1) if teacher_force else top1

return logits

def generate(self, src, start_token, end_token, max_len=20):
self.eval()
hidden = self.encoder(src.to(self.device))
input_token = torch.tensor([[start_token]], device=self.device)
output_tokens = []

for _ in range(max_len):
output, hidden = self.decoder(input_token, hidden)
next_token = output.argmax(2).item()
if next_token == end_token:
break
output_tokens.append(next_token)
input_token = torch.tensor([[next_token]], device=self.device)

return output_tokens

调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# 参数
INPUT_VOCAB_SIZE = 1000
OUTPUT_VOCAB_SIZE = 1000
EMBED_SIZE = 128
HIDDEN_SIZE = 256
DEVICE = "cuda" if torch.cuda.is_available() else "cpu"
# initial
encoder = Encoder(INPUT_VOCAB_SIZE, EMBED_SIZE, HIDDEN_SIZE)
decoder = Decoder(OUTPUT_VOCAB_SIZE, EMBED_SIZE, HIDDEN_SIZE)
model = Seq2Seq(encoder, decoder, device=DEVICE).to(DEVICE)

# 示例训练输入
src = torch.randint(0, 1000, (32, 10)).to(DEVICE)
tgt = torch.randint(0, 1000, (32, 12)).to(DEVICE)

# 训练调用
logits = model(src, tgt, teacher_forcing_ratio=0.7)

# 推理调用
start_token = 2
end_token = 3
src_one = torch.randint(0, 1000, (1, 10)).to(DEVICE)
result = model.generate(src_one, start_token, end_token)
print(result)


采样

RNN 是字符级语言模型(Character-level Language Model),推理过程中进行采样,增加输出多样性

如图,这里并没按照最大得分作为输出,而是进行了采样

image.png
阶段 是否采样 是否可微 用途
训练 不采样,用真实标签 可微分 用于学习参数
测试/生成 采样(或 argmax) 不可微 用于生成新文本
高级方法(Gumbel、REINFORCE) 近似采样 近似可微 特殊任务下尝试采样训练

测试阶段是否采样取决于任务的类型,若是翻译类型,则要求准确性;若为生成更丰富和多样化的文本时,比如文本生成(如 GPT 生成对话)、诗歌创作、自动写作等任务, 则要采样

贪心搜索

  • 原理:每一步都选择概率最高的词(argmax)。
1
next_token = torch.argmax(logits, dim=-1)
  • 优点:简单,确定性强。
  • 缺点:容易陷入平庸、重复,缺乏多样性。

随机采样

  • 原理:直接根据 softmax 概率分布随机采样
1
2
probs = torch.softmax(logits, dim=-1)
next_token = torch.multinomial(probs, num_samples=1)
  • 优点:多样性高。
  • 缺点:可能采样出不合理的词,导致生成质量差。

温度采样

在实际应用中,采样时可以引入温度参数(temperature)来控制采样的随机性:

  • 高温度 T > 1:增加探索性,生成更加随机和多样的文本。
  • 低温度 T < 1:减少随机性,更接近 argmax,但仍保留一定的随机性。
  • T=0.1 → 选择最高概率的单词(接近 argmax)
  • T=1.0 → 典型的采样
  • T=2.0 → 允许更多低概率的单词被选中(更加随机)
1
2
3
4
temperature = 0.7
probs = torch.softmax(logits / temperature, dim=-1)
next_token = torch.multinomial(probs, num_samples=1)

Top-k 采样

  • 原理:每次只在概率最高的 k 个词中进行随机采样。
1
2
3
4
5
k = 10
topk_probs, topk_indices = torch.topk(logits, k)
probs = torch.softmax(topk_probs, dim=-1)
next_token = topk_indices.gather(-1, torch.multinomial(probs, 1))

  • 优点:避免随机性过大,平衡多样性和合理性。
  • 缺点:固定 k 可能忽略尾部重要词。

Top-p 采样(Nucleus Sampling)

  • 原理:动态选择累计概率 ≥ p 的词汇子集进行采样。
  • 常用 p 值:0.8 ~ 0.95
1
2
3
4
5
6
7
8
9
10
11
12
13
p = 0.9
sorted_logits, sorted_indices = torch.sort(logits, descending=True)
cumulative_probs = torch.softmax(sorted_logits, dim=-1).cumsum(dim=-1)
# 保留累计概率 <= p 的词
mask = cumulative_probs > p
mask[..., 1:] = mask[..., :-1].clone()
mask[..., 0] = 0
# 将被mask的位置赋为负无穷,避免被采样
filtered_logits = sorted_logits.masked_fill(mask, float('-inf'))
probs = torch.softmax(filtered_logits, dim=-1)
# 采样
next_token = sorted_indices.gather(-1, torch.multinomial(probs, 1))

  • 优点:自适应词汇量,兼顾多样性和合理性,效果优于 Top-k。

传统one-hot 编码 VS Embedding

one-hot 编码

  • 通过矩阵乘法将 one-hot 映射到隐藏层

特点:

  • 高维稀疏:浪费内存和计算资源。
  • 只是一种索引方式,没法表达词与词之间的语义关系。
  • 适合小词表。 image.png
1
2
3
4
5
6
7
8
9
10
11
12
13
14
import torch

vocab_size = 5 # 假设词表大小为5
hidden_size = 4 # 隐藏层维度

# one-hot编码
one_hot_input = torch.tensor([[0, 1, 0, 0, 0]], dtype=torch.float) # (1, vocab_size)

# 权重矩阵 W_xh
W_xh = torch.randn(vocab_size, hidden_size) # (vocab_size, hidden_size)

# 通过矩阵乘法映射
hidden = one_hot_input @ W_xh # (1, hidden_size)

加入 embedding

image.png

原理:

  • 每个词对应一个低维稠密向量(embedding lookup 查表机制)。
  • 用 nn.Embedding 自动完成映射,相当于查表操作。
  • 维度 = 自定义的 embedding size(远小于词表大小)。

内部原理

1
2
embedding_weight = torch.randn(vocab_size, embed_size)
output = embedding_weight[index]

示例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
import torch
import torch.nn as nn

vocab_size = 5 # 词表大小
embed_size = 3 # 嵌入维度
embedding = nn.Embedding(vocab_size, embed_size)

# 输入:词的索引(而不是one-hot)
input_idx = torch.tensor([1]) # 表示词表中第2个词

# 查表得到embedding向量
embed_vector = embedding(input_idx) # shape: (1, embed_size)


Backward

BPTT(Backpropagation Through Time)

image.png
image.png
  • 总损失:

  • 梯度跟新

因为 WhhWxhWhy 在所有时间步共享,每个时间步的梯度都会累加到同一套参数上

即先积累梯度

再统一进行一次参数更新

对应该步骤的代码

1
2
3
4
5
6
7
8
optimizer = optim.Adam(model.parameters(), lr=0.01)
criterion = nn.CrossEntropyLoss()
loss = criterion(outputs.view(-1, output_size), target.view(-1))
optimizer.zero_grad()
loss.backward()#BPTT,pytor会沿时间维度,把梯度从最后一个时间步反向传播回每个时间步,累积梯度到共享的参数上
optimizer.step()#在完成整个序列的反向传播后,统一更新一次参数。即RNN 的参数在每个时间步是共享的,虽然展开了多个时间步,但实际上只更新一套权重。


问题 - 计算量大:长序列下效率低,显存消耗巨大。 - 容易出现 梯度消失 / 爆炸 问题。

Truncated BPTT

原理: 假设序列总长 T,我们选择一个截断长度 k,那么:

  • 每次前向传播:处理 k 个时间步
  • 每次反向传播:只计算这 k 步的梯度
  • 隐藏状态 h 会继续传递,但计算图会被“截断”
  • 隐藏状态 ht 在时间上传递,模型依然可以“记住”长期信息
image.png

计算流程:

  1. 对于第一个块:t = 1 到 k

更新权重。
  1. 然后继续下一个块:t = k+1 到 2k

再次更新权重。

相关的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
seq_len = 100
truncation_len = 20  # TBPTT的截断长度

for i in range(0, seq_len, truncation_len):
    # 取当前块
    x_chunk = data[:, i:i+truncation_len, :]
    y_chunk = target[:, i:i+truncation_len, :]

    # forward
    output, hidden = model(x_chunk, hidden)

    # loss
    loss = criterion(output, y_chunk)

    # BP
    optimizer.zero_grad()
    loss.backward()
   
    # 截断计算图
    hidden = hidden.detach()
       
    optimizer.step()
    print(f"Step {i//truncation_len + 1}, Loss: {loss.item():.4f}")



测试阶段 softmax 向量还是one-hot 向量

  1. 训练 vs. 测试的一致性
  • 训练时输入的是 one-hot 向量(稀疏的),而 softmax 向量是密集的概率分布
  • 模型在训练时并没有学习如何处理 softmax 向量输入
  • 输入分布的变化可能导致网络行为不可预测,输出垃圾结果
  1. 计算复杂性

假设词汇量很大,比如 10,000 个单词,如果我们在测试时输入 softmax 概率向量:

  • 每一步输入是一个长度 10,000 的 dense(密集)向量,而不是一个one-hot 稀疏向量(其中只有一个非零值)。
  • 计算负担:神经网络的权重矩阵需要和这个 dense 向量相乘,这会显著增加计算复杂性。
  • 存储问题:神经网络通常使用稀疏矩阵优化(sparse tensor operations),one-hot 只需索引操作(见上面 Embedding),而 softmax 向量则需要完整存储和计算。

计算效率和内存管理也是测试时仍然使用 one-hot 关键原因。

  1. 使用 softmax 向量场景

尽管一般来说测试时使用 one-hot,但在一些特定的情况下,输入 softmax 向量可能有用

  1. 温度采样(Temperature Sampling)
  • 通过 softmax 概率对字符进行采样,而不是总是选择最高概率的字符(argmax)。
  • 这样可以生成更加多样化的文本,而不是固定模式的输出。
  1. 连续 softmax 分布输入(Soft Sampling):
  • 在一些高级模型(如自回归变分自动编码器,Auto-Regressive VAE)中,可能使用 softmax 向量的插值,以产生更平滑的序列转换。
  1. 神经机器翻译中的 Beam Search
  • 可能在候选词之间进行概率混合,但最终仍然会选择一个确定的词,而不是直接将 softmax 向量输入到下一个时间步。

图像字幕生成(Image Captioning) 任务的架构

课程中的方法

image.png

更多细节见作业

改进的 RNN 公式如下:

h = tanh (Wxh ⋅ x + Whh ⋅ h + Wih ⋅ v)

每一个时间步引入图像特征, Wih 主要有两个作用:

(1) 让图像信息在整个文本生成过程中持续发挥作用

(2) 让图像信息有独立的学习路径

  • 文本输入 x 和图像信息 v 具有不同的特征空间
  • 文本是离散数据(词嵌入),通常来自 NLP 任务(如 Word2Vec、BERT)。
  • 图像是连续数据,通常来自 CNN(如 ResNet、VGG)。
  • 如果直接用同一个权重矩阵 Wxh 处理两者,可能会导致信息混杂,学习效果下降
  • 引入单独的 Wih 可以让模型独立学习图像信息对文本生成的影响,提高文本的连贯性和语义一致性。

后来的改进

  • CNN 负责提取图像特征(VGG、ResNet),并通过 Wih 结合 RNN。
  • RNN 负责生成文本,在每个时间步使用 Wih ⋅ v 让图像信息持续影响生成过程。
  • 改进方法:后续引入了 注意力机制(Attention),动态调整不同时间步对图像区域的关注程度,而不是使用全局 Wih

VQA (视觉问答)

也是经典的 cnn+rnn 架构

image.png

多模态应用

视觉问答(VQA)多模态学习(Multimodal Learning) 中,模型需要处理不同类型的数据,例如:

  • 图像(Image):用 CNN 提取的图像特征向量。
  • 文本(Text):用 RNN / Transformer 编码的问题向量。

为了让模型能够同时利用这两种信息,我们需要将它们组合起来,然后输入到后续的神经网络层进行处理。

组合不同输入的方法?

(1) 直接连接(Concatenation)

最简单的方法是直接拼接(Concatenate):

z = [v; q]

其中:

  • v 是图像特征向量(由 CNN 提取)。
  • q 是文本问题向量(由 RNN/Transformer 编码)。
  • z 是拼接后的新向量,表示组合后的输入。
1
2
3
4
5
6
7
import torch

v = torch.randn(1, 512) # CNN 提取的图像特征
q = torch.randn(1, 256) # RNN 提取的问题向量

# 直接拼接(Concatenation)
z = torch.cat([v, q], dim=1) # 组合后得到 (1, 768) 维向量

(2) 乘法交互(Multiplicative Interaction)

除了简单拼接,有时候我们希望让两个向量之间产生更复杂的关系,可以使用乘法交互,例如:

  1. 逐元素乘法(Element-wise Multiplication)

z = v ⊙ q

  • 这样可以增强输入之间的特定特征交互。
  • 但是要求 v q 具有相同的维度,否则不能直接相乘。
  1. 双线性变换(Bilinear Transformation)

z = vTWq

  • 这里 W 是一个学习的权重矩阵,它让两个向量进行更复杂的交互。
  • 优点:可以捕捉更复杂的关系。
  • 缺点:计算量较大,可能导致过拟合。

示例代码(PyTorch):

1
2
3
4
5
6
# 逐元素乘法
z_mult = v * q # 需要 v 和 q 维度相同

# 双线性变换
W = torch.randn(512, 256) # 需要学习的权重矩阵
z_bilinear = v @ W @ q.T # 结果是一个标量

RNN 反向传播的问题

ht = tanh (Whht − 1 + Wxxt)

梯度流

image.png
  • 梯度的反向传播,相当于不断地左乘 WhT

image.png 不断左乘 WhT 也可能梯度爆炸或消失

或者这样看容易梯度消失

image.png Almost always < 1

解决方案

  1. 梯度裁剪(Gradient Clipping)
  • 在每次梯度更新时,将梯度限制在一个最大值(如 5)。
image.png
1
torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=5)
  1. 使用正则化
  • 可以在训练过程中对权重矩阵 Wh 施加约束,比如:
  • L2 正则化(权重衰减):避免权重值过大。
  • Spectral Normalization(谱归一化):约束最大奇异值,使其接近 1。
  1. 使用 LSTM / GRU
  • LSTM 和 GRU 通过门控机制(gates),让梯度可以更好地控制,不会完全消失或爆炸。
  • LSTM 的长期记忆单元通过加法(不是乘法)存储信息,使其更稳定。

References