循环神经网络(RNN)深度学习笔记

循环神经网络(RNN)深度学习笔记

目录

  1. 动机:为什么需要RNN
  2. 数学基础
  3. 优化算法
  4. 工程方法
  5. 批判性思维技能
  6. 附录:完整代码示例

1. 动机:为什么需要RNN

1.1 问题背景

在现实世界中,我们经常遇到序列数据

  • 自然语言处理:一句话是单词的序列,前面的词会影响后面词的理解
  • 时间序列预测:股票价格、天气变化、传感器读数
  • 语音识别:音频信号是时间序列
  • 视频分析:视频是图像帧的序列
  • 音乐生成:音符的序列构成旋律

核心问题 :传统的前馈神经网络(Feedforward Neural Network)有一个致命缺陷------无法处理可变长度的序列,且无法保留历史信息

1.2 具体场景分析

场景1:情感分析
复制代码
输入: "这部电影的前半部分很无聊,但结局出人意料地精彩"
期望输出: 正面评价

如果使用传统神经网络:

  • 需要固定输入长度
  • "无聊"和"精彩"会被独立处理,无法理解"但"的转折关系
  • 无法捕捉词序信息
场景2:机器翻译
复制代码
输入: "I love deep learning"
期望输出: "我爱深度学习"

挑战:

  • 不同语言的词序可能不同
  • 需要理解整个句子的语境
  • 输入和输出长度可能不同

1.3 RNN的核心思想

RNN通过引入"记忆"机制解决上述问题

复制代码
当前时刻的输出 = f(当前输入, 过去的记忆)

关键特性:

  1. 参数共享:处理每个时间步使用相同的参数
  2. 循环连接:隐藏状态从一个时间步传递到下一个时间步
  3. 可变长度:可以处理任意长度的序列

类比:RNN就像一个人在阅读句子,每读一个词,都会根据之前读到的内容(记忆)来理解当前这个词。


2. 数学基础

2.1 RNN的基本结构

2.1.1 核心公式

对于时间步 t t t,RNN的计算过程如下:

h t = tanh ⁡ ( W h h h t − 1 + W x h x t + b h ) y t = W h y h t + b y \begin{aligned} h_t &= \tanh(W_{hh} h_{t-1} + W_{xh} x_t + b_h) \\ y_t &= W_{hy} h_t + b_y \end{aligned} htyt=tanh(Whhht−1+Wxhxt+bh)=Whyht+by

其中:

  • x t ∈ R d x_t \in \mathbb{R}^{d} xt∈Rd:时间步 t t t 的输入向量(维度为 d d d)
  • h t ∈ R h h_t \in \mathbb{R}^{h} ht∈Rh:时间步 t t t 的隐藏状态(维度为 h h h)
  • h t − 1 ∈ R h h_{t-1} \in \mathbb{R}^{h} ht−1∈Rh:上一时间步的隐藏状态
  • y t ∈ R o y_t \in \mathbb{R}^{o} yt∈Ro:时间步 t t t 的输出(维度为 o o o)
  • W x h ∈ R h × d W_{xh} \in \mathbb{R}^{h \times d} Wxh∈Rh×d:输入到隐藏层的权重矩阵
  • W h h ∈ R h × h W_{hh} \in \mathbb{R}^{h \times h} Whh∈Rh×h:隐藏层到隐藏层的权重矩阵(循环权重)
  • W h y ∈ R o × h W_{hy} \in \mathbb{R}^{o \times h} Why∈Ro×h:隐藏层到输出的权重矩阵
  • b h ∈ R h b_h \in \mathbb{R}^{h} bh∈Rh:隐藏层偏置
  • b y ∈ R o b_y \in \mathbb{R}^{o} by∈Ro:输出层偏置
2.1.2 展开的计算图

让我们看一个具体例子,假设输入序列长度为3:

复制代码
时间步:     t=0         t=1         t=2
输入:       x_0   →     x_1   →     x_2
           ↓           ↓           ↓
隐藏层:    h_0   →     h_1   →     h_2
           ↓           ↓           ↓
输出:      y_0         y_1         y_2

数学展开:

复制代码
h_0 = tanh(W_hh * h_{-1} + W_xh * x_0 + b_h)  # h_{-1} 通常初始化为0
h_1 = tanh(W_hh * h_0 + W_xh * x_1 + b_h)
h_2 = tanh(W_hh * h_1 + W_xh * x_2 + b_h)

y_0 = W_hy * h_0 + b_y
y_1 = W_hy * h_1 + b_y
y_2 = W_hy * h_2 + b_y

2.2 详细的维度变化分析

这是理解RNN的关键!让我们用一个具体例子追踪每一步的维度变化。

2.2.1 问题设定

假设我们有以下配置:

  • 输入维度 d = 10 d = 10 d=10(例如,词嵌入维度)
  • 隐藏层维度 h = 20 h = 20 h=20
  • 输出维度 o = 5 o = 5 o=5(例如,5个类别的分类)
  • 序列长度 T = 3 T = 3 T=3
  • 批次大小 B = 2 B = 2 B=2(同时处理2个序列)
2.2.2 单个时间步的维度变化

输入数据形状

复制代码
x_t: (B, d) = (2, 10)

表示2个样本,每个样本是10维向量

权重矩阵形状

复制代码
W_xh: (h, d) = (20, 10)  # 输入到隐藏层
W_hh: (h, h) = (20, 20)  # 隐藏层到隐藏层(循环)
W_hy: (o, h) = (5, 20)   # 隐藏层到输出
b_h:  (h,)   = (20,)     # 隐藏层偏置
b_y:  (o,)   = (5,)      # 输出层偏置

前一时刻隐藏状态形状

复制代码
h_{t-1}: (B, h) = (2, 20)

计算过程与维度变化

python 复制代码
# 步骤1: W_xh @ x_t^T
# (20, 10) @ (10, 2) = (20, 2)
# 转置后: (2, 20)
term1 = x_t @ W_xh.T  # (2, 10) @ (10, 20) = (2, 20)

# 步骤2: W_hh @ h_{t-1}^T
# (20, 20) @ (20, 2) = (20, 2)
# 转置后: (2, 20)
term2 = h_{t-1} @ W_hh.T  # (2, 20) @ (20, 20) = (2, 20)

# 步骤3: 相加并加偏置
# (2, 20) + (2, 20) + (20,) = (2, 20)
# 广播机制: (20,) 自动扩展为 (2, 20)
h_t = tanh(term1 + term2 + b_h)  # (2, 20)

# 步骤4: 计算输出
# (2, 20) @ (20, 5) = (2, 5)
y_t = h_t @ W_hy.T + b_y  # (2, 5)

总结单个时间步

复制代码
输入:  x_t    (2, 10)
      h_{t-1} (2, 20)
      ↓
输出:  h_t    (2, 20)
      y_t    (2, 5)
2.2.3 完整序列的维度变化

对于整个序列(T=3个时间步):

python 复制代码
# 输入序列
X: (B, T, d) = (2, 3, 10)
# 可以理解为: 2个样本,每个样本有3个时间步,每个时间步是10维向量

# 初始化隐藏状态
h_0: (B, h) = (2, 20)  # 通常初始化为全0

# 时间步 t=0
x_0 = X[:, 0, :]  # (2, 10) - 取所有样本的第0个时间步
h_0 = tanh(x_0 @ W_xh.T + h_{-1} @ W_hh.T + b_h)  # (2, 20)
y_0 = h_0 @ W_hy.T + b_y  # (2, 5)

# 时间步 t=1
x_1 = X[:, 1, :]  # (2, 10) - 取所有样本的第1个时间步
h_1 = tanh(x_1 @ W_xh.T + h_0 @ W_hh.T + b_h)  # (2, 20)
y_1 = h_1 @ W_hy.T + b_y  # (2, 5)

# 时间步 t=2
x_2 = X[:, 2, :]  # (2, 10) - 取所有样本的第2个时间步
h_2 = tanh(x_2 @ W_xh.T + h_1 @ W_hh.T + b_h)  # (2, 20)
y_2 = h_2 @ W_hy.T + b_y  # (2, 5)

# 收集所有输出
Y = stack([y_0, y_1, y_2], dim=1)  # (2, 3, 5)
# 形状: (批次大小, 序列长度, 输出维度)

可视化维度流动

复制代码
时间维度展开:

t=0:  X[:, 0, :] (2, 10) ──→ h_0 (2, 20) ──→ y_0 (2, 5)
                               ↓
t=1:  X[:, 1, :] (2, 10) ──→ h_1 (2, 20) ──→ y_1 (2, 5)
                               ↓
t=2:  X[:, 2, :] (2, 10) ──→ h_2 (2, 20) ──→ y_2 (2, 5)

最终输出: Y (2, 3, 5)
2.2.4 实际数据流示例

让我们用一个文本分类的具体例子:

任务:情感分类(正面/负面)

输入句子

复制代码
样本1: "I love this movie"  → [I, love, this, movie]
样本2: "Great film"         → [Great, film, <PAD>, <PAD>]

数据准备

python 复制代码
# 词汇表
vocab = {"I": 0, "love": 1, "this": 2, "movie": 3, "Great": 4, "film": 5, "<PAD>": 6}

# 转换为索引
sequence1 = [0, 1, 2, 3]  # "I love this movie"
sequence2 = [4, 5, 6, 6]  # "Great film <PAD> <PAD>" (填充到相同长度)

# 词嵌入层会将索引转换为向量
# 假设embedding_dim = 10
# sequence1 → (4, 10) 的矩阵
# sequence2 → (4, 10) 的矩阵

# 批次处理
X = stack([sequence1_embedded, sequence2_embedded])
# X: (2, 4, 10)  # 2个样本,4个时间步,10维嵌入

RNN处理流程

python 复制代码
# 配置
B = 2 (批次大小)
T = 4 (序列长度)
d = 10 (嵌入维度)
h = 20 (隐藏层维度)
o = 2 (输出维度: 正面/负面)

# 初始隐藏状态
h_init = zeros(2, 20)  # (B, h)

# 逐时间步处理
for t in range(4):
    x_t = X[:, t, :]  # (2, 10) - 当前时间步的输入
    h_t = rnn_cell(x_t, h_prev)  # (2, 20) - 更新隐藏状态
    h_prev = h_t
    
# 最后一个隐藏状态用于分类
# h_4: (2, 20)
logits = h_4 @ W_hy.T + b_y  # (2, 2)
probabilities = softmax(logits)  # (2, 2)
# [[0.8, 0.2],   # 样本1: 80%正面, 20%负面
#  [0.9, 0.1]]   # 样本2: 90%正面, 10%负面

2.3 损失函数

2.3.1 序列到序列任务(Sequence-to-Sequence)

对于每个时间步都有输出的任务(如语言模型):

L = 1 T ∑ t = 1 T L t ( y t , y ^ t ) \mathcal{L} = \frac{1}{T} \sum_{t=1}^{T} \mathcal{L}_t(y_t, \hat{y}_t) L=T1t=1∑TLt(yt,y^t)

交叉熵损失 (用于分类):
L t = − ∑ c = 1 C y t ( c ) log ⁡ ( y ^ t ( c ) ) \mathcal{L}t = -\sum{c=1}^{C} y_t^{(c)} \log(\hat{y}_t^{(c)}) Lt=−c=1∑Cyt(c)log(y^t(c))

其中 C C C 是类别数, y t ( c ) y_t^{(c)} yt(c) 是真实标签的one-hot编码。

2.3.2 序列到单一输出任务(Sequence-to-One)

对于只需要最后输出的任务(如情感分类):

L = L ( y T , y ^ T ) \mathcal{L} = \mathcal{L}(y_T, \hat{y}_T) L=L(yT,y^T)

只计算最后一个时间步的损失。

2.3.3 具体例子:语言模型

任务:给定前面的词,预测下一个词

复制代码
输入序列: "I love deep"
目标序列: "love deep learning"

时间步 t=0: 输入 "I"     → 预测 "love"
时间步 t=1: 输入 "love"  → 预测 "deep"
时间步 t=2: 输入 "deep"  → 预测 "learning"

损失计算

python 复制代码
# 假设词汇表大小 V = 10000
# 每个时间步的输出是 (B, V) 的概率分布

# y_0 是 "love" 的one-hot编码
# y_1 是 "deep" 的one-hot编码
# y_2 是 "learning" 的one-hot编码

# 总损失
loss = (CrossEntropy(pred_0, y_0) + 
        CrossEntropy(pred_1, y_1) + 
        CrossEntropy(pred_2, y_2)) / 3

2.4 通过时间反向传播(BPTT)

2.4.1 基本思想

由于RNN在时间上展开,反向传播需要沿着时间链传递梯度。

前向传播(已知):

复制代码
h_0 → h_1 → h_2 → ... → h_T → Loss

反向传播

复制代码
Loss → ∂L/∂h_T → ∂L/∂h_{T-1} → ... → ∂L/∂h_1 → ∂L/∂h_0
2.4.2 梯度推导

对于时间步 t t t,计算损失对隐藏状态的梯度:

∂ L ∂ h t = ∂ L ∂ y t ∂ y t ∂ h t + ∂ L ∂ h t + 1 ∂ h t + 1 ∂ h t \frac{\partial \mathcal{L}}{\partial h_t} = \frac{\partial \mathcal{L}}{\partial y_t} \frac{\partial y_t}{\partial h_t} + \frac{\partial \mathcal{L}}{\partial h_{t+1}} \frac{\partial h_{t+1}}{\partial h_t} ∂ht∂L=∂yt∂L∂ht∂yt+∂ht+1∂L∂ht∂ht+1

第一项:当前时间步的直接贡献

第二项:未来时间步的间接贡献(通过链式法则)

权重梯度累积

∂ L ∂ W h h = ∑ t = 1 T ∂ L ∂ h t ∂ h t ∂ W h h \frac{\partial \mathcal{L}}{\partial W_{hh}} = \sum_{t=1}^{T} \frac{\partial \mathcal{L}}{\partial h_t} \frac{\partial h_t}{\partial W_{hh}} ∂Whh∂L=t=1∑T∂ht∂L∂Whh∂ht

注意:由于参数共享,每个时间步都对权重梯度有贡献。

2.4.3 梯度消失和梯度爆炸

问题的数学根源

考虑梯度从时间步 T T T 传播到时间步 t t t:

∂ h T ∂ h t = ∏ k = t + 1 T ∂ h k ∂ h k − 1 = ∏ k = t + 1 T W h h T ⋅ diag ( tanh ⁡ ′ ( a k − 1 ) ) \frac{\partial h_T}{\partial h_t} = \prod_{k=t+1}^{T} \frac{\partial h_k}{\partial h_{k-1}} = \prod_{k=t+1}^{T} W_{hh}^T \cdot \text{diag}(\tanh'(a_{k-1})) ∂ht∂hT=k=t+1∏T∂hk−1∂hk=k=t+1∏TWhhT⋅diag(tanh′(ak−1))

其中 a k − 1 = W h h h k − 2 + W x h x k − 1 + b h a_{k-1} = W_{hh} h_{k-2} + W_{xh} x_{k-1} + b_h ak−1=Whhhk−2+Wxhxk−1+bh

梯度消失

  • 如果 ∣ W h h ∣ < 1 |W_{hh}| < 1 ∣Whh∣<1 且 ∣ tanh ⁡ ′ ∣ < 1 |\tanh'| < 1 ∣tanh′∣<1,连乘会导致梯度指数衰减
  • 结果:长距离依赖无法学习

梯度爆炸

  • 如果 ∣ W h h ∣ > 1 |W_{hh}| > 1 ∣Whh∣>1,连乘会导致梯度指数增长
  • 结果:训练不稳定,权重更新过大

直观理解

复制代码
假设每一步梯度都乘以 0.5(小于1):
1步后: 梯度 = 0.5
2步后: 梯度 = 0.25
10步后: 梯度 ≈ 0.001
50步后: 梯度 ≈ 0(几乎消失)

3. 优化算法

3.1 梯度下降及其变体

3.1.1 标准梯度下降(SGD)

更新规则
θ t + 1 = θ t − η ∇ θ L ( θ t ) \theta_{t+1} = \theta_t - \eta \nabla_\theta \mathcal{L}(\theta_t) θt+1=θt−η∇θL(θt)

其中 η \eta η 是学习率。

应用于RNN

python 复制代码
# 伪代码
for epoch in range(num_epochs):
    for batch in dataloader:
        # 前向传播
        outputs = rnn(batch.inputs)
        loss = criterion(outputs, batch.targets)
        
        # 反向传播(BPTT)
        loss.backward()
        
        # 参数更新
        for param in rnn.parameters():
            param.data -= learning_rate * param.grad
        
        # 清零梯度
        rnn.zero_grad()
3.1.2 梯度裁剪(Gradient Clipping)

目的:防止梯度爆炸

方法1:按值裁剪
g clipped = max ⁡ ( min ⁡ ( g , clip_value ) , − clip_value ) g_{\text{clipped}} = \max(\min(g, \text{clip\_value}), -\text{clip\_value}) gclipped=max(min(g,clip_value),−clip_value)

方法2:按范数裁剪 (更常用)
g clipped = { clip_norm ∥ g ∥ g if ∥ g ∥ > clip_norm g otherwise g_{\text{clipped}} = \begin{cases} \frac{\text{clip\_norm}}{\|g\|} g & \text{if } \|g\| > \text{clip\_norm} \\ g & \text{otherwise} \end{cases} gclipped={∥g∥clip_normggif ∥g∥>clip_normotherwise

PyTorch实现

python 复制代码
import torch.nn.utils as nn_utils

# 前向和反向传播
loss.backward()

# 梯度裁剪
nn_utils.clip_grad_norm_(rnn.parameters(), max_norm=5.0)

# 参数更新
optimizer.step()

为什么有效

复制代码
原始梯度: [100, -200, 50]  → 范数 ≈ 229
裁剪到 max_norm=5:
新梯度: [2.18, -4.36, 1.09]  → 范数 = 5

保持了梯度方向,但限制了幅度
3.1.3 Adam优化器

核心思想

  1. 动量(Momentum):利用历史梯度信息
  2. 自适应学习率:每个参数有独立的学习率

数学公式

m t = β 1 m t − 1 + ( 1 − β 1 ) g t v t = β 2 v t − 1 + ( 1 − β 2 ) g t 2 m ^ t = m t 1 − β 1 t v ^ t = v t 1 − β 2 t θ t + 1 = θ t − η v ^ t + ϵ m ^ t \begin{aligned} 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 &= \frac{m_t}{1 - \beta_1^t} \\ \hat{v}t &= \frac{v_t}{1 - \beta_2^t} \\ \theta{t+1} &= \theta_t - \frac{\eta}{\sqrt{\hat{v}_t} + \epsilon} \hat{m}_t \end{aligned} mtvtm^tv^tθt+1=β1mt−1+(1−β1)gt=β2vt−1+(1−β2)gt2=1−β1tmt=1−β2tvt=θt−v^t +ϵηm^t

其中:

  • m t m_t mt:一阶矩估计(梯度的移动平均)
  • v t v_t vt:二阶矩估计(梯度平方的移动平均)
  • β 1 = 0.9 \beta_1 = 0.9 β1=0.9, β 2 = 0.999 \beta_2 = 0.999 β2=0.999(默认值)
  • ϵ = 1 0 − 8 \epsilon = 10^{-8} ϵ=10−8(数值稳定性)

PyTorch实现

python 复制代码
optimizer = torch.optim.Adam(rnn.parameters(), lr=0.001)

for epoch in range(num_epochs):
    for batch in dataloader:
        optimizer.zero_grad()
        
        outputs = rnn(batch.inputs)
        loss = criterion(outputs, batch.targets)
        
        loss.backward()
        optimizer.step()  # Adam自动处理参数更新

为什么Adam适合RNN

  1. 自适应学习率能应对RNN中不同参数的不同梯度规模
  2. 动量机制有助于穿越平坦区域
  3. 对学习率不太敏感

3.2 截断BPTT(Truncated BPTT)

问题:对于很长的序列,BPTT计算成本太高

解决方案:将长序列截断成小块

算法

python 复制代码
seq_length = 1000  # 原始序列很长
chunk_size = 50    # 截断长度

for start in range(0, seq_length, chunk_size):
    end = min(start + chunk_size, seq_length)
    
    # 只对这一块做BPTT
    chunk_input = full_sequence[start:end]
    chunk_target = full_targets[start:end]
    
    # 前向传播
    h = rnn(chunk_input, h_prev.detach())  # detach切断梯度流
    loss = criterion(h, chunk_target)
    
    # 反向传播(只在这个chunk内)
    loss.backward()
    optimizer.step()
    
    # 保留隐藏状态用于下一个chunk(但不保留梯度)
    h_prev = h.detach()

关键点

  • 隐藏状态在chunk间传递(保持序列连续性)
  • 梯度不在chunk间传递(降低计算成本)
  • 平衡:chunk太小损失性能,太大增加计算

4. 工程方法

4.1 高效训练技巧

4.1.1 批处理与填充

问题:不同序列长度不同,如何批处理?

解决方案:填充(Padding)+ 掩码(Masking)

python 复制代码
import torch
from torch.nn.utils.rnn import pad_sequence, pack_padded_sequence, pad_packed_sequence

# 原始序列(长度不同)
sequences = [
    torch.tensor([1, 2, 3, 4, 5]),      # 长度 5
    torch.tensor([6, 7]),                # 长度 2
    torch.tensor([8, 9, 10, 11])         # 长度 4
]

# 方法1: 简单填充
padded = pad_sequence(sequences, batch_first=True, padding_value=0)
# 结果: 
# [[1, 2, 3, 4, 5],
#  [6, 7, 0, 0, 0],
#  [8, 9, 10, 11, 0]]
# 形状: (3, 5)  # 3个序列,最大长度5

# 方法2: PackedSequence(更高效)
lengths = torch.tensor([5, 2, 4])  # 记录真实长度
sorted_lengths, sorted_idx = lengths.sort(descending=True)
sorted_sequences = [sequences[i] for i in sorted_idx]

padded_sorted = pad_sequence(sorted_sequences, batch_first=True)
packed = pack_padded_sequence(padded_sorted, sorted_lengths, batch_first=True)

# RNN处理
output, hidden = rnn(packed)

# 解包
unpacked, _ = pad_packed_sequence(output, batch_first=True)

为什么PackedSequence更高效

  • 避免对填充部分做无用计算
  • 内存占用更少
  • 训练速度更快
4.1.2 双向RNN(Bidirectional RNN)

动机:有些任务需要同时考虑过去和未来的信息

结构

复制代码
前向RNN:  h_0 → h_1 → h_2 → h_3
                  ↓    ↓    ↓
后向RNN:  h_0 ← h_1 ← h_2 ← h_3

最终输出: [h_forward; h_backward] 拼接

数学公式
h → t = RNN forward ( x t , h → t − 1 ) h ← t = RNN backward ( x t , h ← t + 1 ) h t = [ h → t ; h ← t ] \begin{aligned} \overrightarrow{h}t &= \text{RNN}{\text{forward}}(x_t, \overrightarrow{h}{t-1}) \\ \overleftarrow{h}t &= \text{RNN}{\text{backward}}(x_t, \overleftarrow{h}{t+1}) \\ h_t &= [\overrightarrow{h}_t; \overleftarrow{h}_t] \end{aligned} h th tht=RNNforward(xt,h t−1)=RNNbackward(xt,h t+1)=[h t;h t]

维度变化

python 复制代码
# 单向RNN
input: (B, T, d) = (2, 3, 10)
hidden: (B, h) = (2, 20)
output: (B, T, h) = (2, 3, 20)

# 双向RNN
input: (B, T, d) = (2, 3, 10)
forward_hidden: (B, h) = (2, 20)
backward_hidden: (B, h) = (2, 20)
output: (B, T, 2*h) = (2, 3, 40)  # 拼接后维度翻倍

PyTorch实现

python 复制代码
rnn = nn.RNN(input_size=10, hidden_size=20, 
             num_layers=1, bidirectional=True, batch_first=True)

# 输入: (B, T, d)
output, hidden = rnn(input)

# output: (B, T, 2*hidden_size)
# hidden: (2, B, hidden_size)  # 2表示前向和后向
4.1.3 多层RNN(Stacked RNN)

结构

复制代码
第2层:     h2_0 → h2_1 → h2_2
             ↑      ↑      ↑
第1层:     h1_0 → h1_1 → h1_2
             ↑      ↑      ↑
输入:       x_0    x_1    x_2

维度变化(2层RNN):

python 复制代码
# 配置
num_layers = 2
input_size = 10
hidden_size = 20
batch_size = 2
seq_length = 3

# 第1层
layer1_input: (2, 3, 10)   # (B, T, input_size)
layer1_output: (2, 3, 20)  # (B, T, hidden_size)

# 第2层(第1层的输出作为输入)
layer2_input: (2, 3, 20)   # 等于layer1_output
layer2_output: (2, 3, 20)  # (B, T, hidden_size)

# 最终hidden state: (num_layers, B, hidden_size) = (2, 2, 20)

PyTorch实现

python 复制代码
rnn = nn.RNN(input_size=10, hidden_size=20, 
             num_layers=2, batch_first=True)

input = torch.randn(2, 3, 10)  # (B, T, input_size)
h0 = torch.zeros(2, 2, 20)     # (num_layers, B, hidden_size)

output, hidden = rnn(input, h0)
# output: (2, 3, 20)  # 只输出最后一层的结果
# hidden: (2, 2, 20)  # 所有层的最终隐藏状态

4.2 数值稳定性

4.2.1 权重初始化

Xavier初始化 (适用于tanh激活):
W ∼ U ( − 6 n in + n out , 6 n in + n out ) W \sim \mathcal{U}\left(-\sqrt{\frac{6}{n_{\text{in}} + n_{\text{out}}}}, \sqrt{\frac{6}{n_{\text{in}} + n_{\text{out}}}}\right) W∼U(−nin+nout6 ,nin+nout6 )

PyTorch实现

python 复制代码
for name, param in rnn.named_parameters():
    if 'weight_ih' in name:  # 输入到隐藏层的权重
        nn.init.xavier_uniform_(param)
    elif 'weight_hh' in name:  # 隐藏层到隐藏层的权重
        nn.init.orthogonal_(param)  # 正交初始化有助于缓解梯度消失
    elif 'bias' in name:
        nn.init.zeros_(param)
4.2.2 Dropout正则化

在RNN中应用Dropout

python 复制代码
class RNNWithDropout(nn.Module):
    def __init__(self, input_size, hidden_size, dropout=0.5):
        super().__init__()
        self.rnn = nn.RNN(input_size, hidden_size, batch_first=True)
        self.dropout = nn.Dropout(dropout)
        
    def forward(self, x):
        # 方法1: 在RNN输出后应用dropout
        output, hidden = self.rnn(x)
        output = self.dropout(output)
        return output, hidden

注意

  • 不要在隐藏状态的循环连接上使用dropout
  • 通常在RNN的输出上或多层RNN的层间使用dropout

4.3 硬件加速

4.3.1 GPU优化
python 复制代码
import torch

# 检查GPU可用性
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')

# 模型和数据转移到GPU
rnn = rnn.to(device)
input_data = input_data.to(device)

# cuDNN加速(PyTorch自动启用)
torch.backends.cudnn.enabled = True
torch.backends.cudnn.benchmark = True  # 自动寻找最优算法
4.3.2 混合精度训练

使用FP16加速训练

python 复制代码
from torch.cuda.amp import autocast, GradScaler

scaler = GradScaler()

for batch in dataloader:
    optimizer.zero_grad()
    
    # 前向传播使用FP16
    with autocast():
        output = rnn(batch.input)
        loss = criterion(output, batch.target)
    
    # 反向传播时自动缩放梯度
    scaler.scale(loss).backward()
    
    # 梯度裁剪
    scaler.unscale_(optimizer)
    torch.nn.utils.clip_grad_norm_(rnn.parameters(), max_norm=1.0)
    
    # 参数更新
    scaler.step(optimizer)
    scaler.update()

优势

  • 训练速度提升2-3倍
  • 显存占用减少约50%
  • 在现代GPU上效果显著

5. 批判性思维技能

5.1 RNN的局限性

5.1.1 长期依赖问题

问题描述

尽管理论上RNN可以捕捉长距离依赖,但实际上由于梯度消失,很难学习距离超过10-20步的依赖关系。

实验验证

python 复制代码
# 创建一个简单的复制任务
# 任务: 记住序列开始的符号,在很久之后输出

def create_copy_task(seq_length, delay):
    """
    seq_length: 序列长度
    delay: 需要记忆的时间步数
    """
    # 输入: [3, 7, 0, 0, 0, ..., 0, 9]
    #       ↑              delay步      ↑
    #     记住这个                    在这里输出
    
    input_seq = [random.randint(1, 8)]  # 需要记住的符号
    input_seq += [0] * delay  # 填充
    input_seq += [9]  # 触发输出的信号
    
    target_seq = [0] * delay + [input_seq[0]]  # 最后才输出记住的符号
    
    return input_seq, target_seq

# 测试标准RNN
delays = [5, 10, 20, 50, 100]
for delay in delays:
    rnn = SimpleRNN(input_size=10, hidden_size=50)
    accuracy = train_and_test(rnn, delay)
    print(f"Delay={delay}, Accuracy={accuracy}")

# 预期结果:
# Delay=5,   Accuracy=0.95   ✓ 效果好
# Delay=10,  Accuracy=0.87   ✓ 还可以
# Delay=20,  Accuracy=0.45   ✗ 开始失败
# Delay=50,  Accuracy=0.10   ✗ 完全失败
# Delay=100, Accuracy=0.10   ✗ 完全失败

结论:标准RNN难以处理长期依赖 → 需要LSTM/GRU

5.1.2 并行化困难

问题

RNN的计算是串行的,时间步 t t t 必须等待时间步 t − 1 t-1 t−1 完成。

python 复制代码
# RNN: 必须串行
h_0 = f(x_0, h_init)
h_1 = f(x_1, h_0)      # 必须等h_0算完
h_2 = f(x_2, h_1)      # 必须等h_1算完
...

# CNN或Transformer: 可以并行
# 所有位置可以同时计算

影响

  • 训练速度慢
  • 无法充分利用现代GPU的并行能力
  • 对长序列尤其慢

解决方向

  1. Transformer架构(完全并行)
  2. 并行RNN变体(如Quasi-RNN)

5.2 何时使用RNN

决策树
复制代码
你的任务是什么?
│
├─ 序列建模?
│  │
│  ├─ 序列很长(>100)?
│  │  ├─ 是 → 考虑Transformer
│  │  └─ 否 → 继续
│  │
│  ├─ 需要处理实时流数据?
│  │  ├─ 是 → RNN/LSTM/GRU(保持隐藏状态)
│  │  └─ 否 → 继续
│  │
│  ├─ 计算资源有限?
│  │  ├─ 是 → GRU(参数少)
│  │  └─ 否 → LSTM(效果更好)
│  │
│  └─ 需要双向信息?
│     ├─ 是 → Bidirectional RNN
│     └─ 否 → 单向RNN
│
└─ 非序列任务 → 不要用RNN,考虑CNN/Transformer
5.2.1 RNN适用场景

✓ 适合使用RNN的情况

  1. 实时序列处理:语音识别、在线手写识别
  2. 中等长度序列:情感分析(句子级别)
  3. 时间序列预测:股票价格、天气预报
  4. 序列生成:音乐生成、文本生成
  5. 资源受限环境:移动设备、嵌入式系统

✗ 不适合使用RNN的情况

  1. 很长序列(>500词):文档分类 → 用Transformer
  2. 完全并行任务:图像分类 → 用CNN
  3. 不关心顺序:词袋模型任务 → 用MLP
  4. 需要全局关注:机器翻译 → 用Transformer

5.3 调试技巧

5.3.1 检查梯度
python 复制代码
# 检查梯度是否消失/爆炸
def check_gradients(model):
    total_norm = 0
    for name, param in model.named_parameters():
        if param.grad is not None:
            param_norm = param.grad.data.norm(2)
            total_norm += param_norm.item() ** 2
            print(f"{name}: {param_norm.item():.6f}")
    
    total_norm = total_norm ** 0.5
    print(f"Total gradient norm: {total_norm:.6f}")
    
    if total_norm < 1e-5:
        print("⚠️  警告: 梯度消失!")
    elif total_norm > 100:
        print("⚠️  警告: 梯度爆炸!")
    
# 使用
loss.backward()
check_gradients(rnn)
5.3.2 可视化隐藏状态
python 复制代码
import matplotlib.pyplot as plt

def visualize_hidden_states(rnn, input_seq):
    """可视化RNN的隐藏状态演化"""
    hidden_states = []
    h = torch.zeros(1, rnn.hidden_size)
    
    for x_t in input_seq:
        h = rnn.step(x_t, h)
        hidden_states.append(h.detach().numpy())
    
    hidden_states = np.array(hidden_states).squeeze()
    
    # 绘制热图
    plt.figure(figsize=(12, 6))
    plt.imshow(hidden_states.T, aspect='auto', cmap='viridis')
    plt.colorbar(label='Activation')
    plt.xlabel('Time Step')
    plt.ylabel('Hidden Unit')
    plt.title('RNN Hidden State Evolution')
    plt.show()

# 观察模式:
# - 横条纹: 某些单元持续激活(好)
# - 快速变化: 响应输入(好)
# - 全白/全黑: 饱和或死亡(坏)
5.3.3 过拟合单个batch
python 复制代码
# 调试技巧: 先确保模型能过拟合单个样本
single_batch = next(iter(dataloader))

for i in range(1000):
    optimizer.zero_grad()
    output = rnn(single_batch.input)
    loss = criterion(output, single_batch.target)
    loss.backward()
    optimizer.step()
    
    if i % 100 == 0:
        print(f"Step {i}, Loss: {loss.item():.6f}")

# 期望: loss应该降到接近0
# 如果不能 → 模型有bug或容量不足

5.4 理论与实践的差距

5.4.1 理论能力 vs 实际表现

理论:RNN可以表示任意序列函数(图灵完备)

实践

  • 受梯度消失限制
  • 需要大量数据
  • 训练困难

启示

"理论上可能 ≠ 实际上可行"

需要LSTM/GRU等改进架构

5.4.2 超参数的影响

实验:固定架构,改变超参数

python 复制代码
# 测试不同隐藏层大小
hidden_sizes = [10, 20, 50, 100, 200]
results = []

for h in hidden_sizes:
    rnn = SimpleRNN(input_size=50, hidden_size=h)
    test_acc = train(rnn)
    results.append(test_acc)

# 观察:
# h=10:  underfitting  (63% acc)
# h=20:  still low     (71% acc)
# h=50:  good          (87% acc)  ← sweet spot
# h=100: good          (88% acc)
# h=200: overfitting   (85% acc)  ← 太大反而下降

经验法则

  • 隐藏层大小: 通常在输入维度的1-4倍
  • 学习率: 从0.001开始尝试
  • 批次大小: 32-256(取决于内存)
  • 梯度裁剪: 1.0-5.0

6. 附录:完整代码示例

6.1 从零实现RNN单元

python 复制代码
import numpy as np

class SimpleRNNCell:
    """从零实现的RNN单元"""
    
    def __init__(self, input_size, hidden_size):
        # Xavier初始化
        self.W_xh = np.random.randn(hidden_size, input_size) * np.sqrt(2.0 / input_size)
        self.W_hh = np.random.randn(hidden_size, hidden_size) * np.sqrt(2.0 / hidden_size)
        self.b_h = np.zeros(hidden_size)
        
        # 输出层(假设二分类)
        self.W_hy = np.random.randn(2, hidden_size) * np.sqrt(2.0 / hidden_size)
        self.b_y = np.zeros(2)
        
    def forward(self, x, h_prev):
        """
        前向传播
        x: (input_size,)
        h_prev: (hidden_size,)
        """
        # h_t = tanh(W_xh @ x + W_hh @ h_prev + b_h)
        h_next = np.tanh(
            self.W_xh @ x + self.W_hh @ h_prev + self.b_h
        )
        
        # y_t = W_hy @ h_t + b_y
        y = self.W_hy @ h_next + self.b_y
        
        # 保存中间值用于反向传播
        self.cache = (x, h_prev, h_next)
        
        return h_next, y
    
    def backward(self, dh_next, dy):
        """
        反向传播
        dh_next: 从下一时间步传来的梯度
        dy: 当前时间步输出的梯度
        """
        x, h_prev, h = self.cache
        
        # 输出层梯度
        dW_hy = dy.reshape(-1, 1) @ h.reshape(1, -1)
        db_y = dy
        dh = self.W_hy.T @ dy + dh_next
        
        # tanh的导数
        dtanh = (1 - h ** 2) * dh
        
        # 参数梯度
        dW_xh = dtanh.reshape(-1, 1) @ x.reshape(1, -1)
        dW_hh = dtanh.reshape(-1, 1) @ h_prev.reshape(1, -1)
        db_h = dtanh
        
        # 传递给前一时间步的梯度
        dh_prev = self.W_hh.T @ dtanh
        
        return dh_prev, {
            'W_xh': dW_xh, 'W_hh': dW_hh, 'b_h': db_h,
            'W_hy': dW_hy, 'b_y': db_y
        }

# 使用示例
input_size, hidden_size = 10, 20
rnn_cell = SimpleRNNCell(input_size, hidden_size)

# 处理序列
h = np.zeros(hidden_size)  # 初始隐藏状态
sequence = [np.random.randn(input_size) for _ in range(5)]

for x_t in sequence:
    h, y = rnn_cell.forward(x_t, h)
    print(f"Hidden state shape: {h.shape}, Output shape: {y.shape}")

6.2 PyTorch完整训练示例

python 复制代码
import torch
import torch.nn as nn
import torch.optim as optim
from torch.utils.data import Dataset, DataLoader

# ========== 1. 定义数据集 ==========
class TextDataset(Dataset):
    """简单的文本分类数据集"""
    
    def __init__(self, texts, labels, vocab, max_length=50):
        self.texts = texts
        self.labels = labels
        self.vocab = vocab
        self.max_length = max_length
    
    def __len__(self):
        return len(self.texts)
    
    def __getitem__(self, idx):
        text = self.texts[idx]
        label = self.labels[idx]
        
        # 文本转索引
        indices = [self.vocab.get(word, 0) for word in text.split()]
        
        # 截断或填充
        if len(indices) > self.max_length:
            indices = indices[:self.max_length]
        else:
            indices += [0] * (self.max_length - len(indices))
        
        return torch.tensor(indices), torch.tensor(label)

# ========== 2. 定义RNN模型 ==========
class TextRNN(nn.Module):
    def __init__(self, vocab_size, embed_dim, hidden_dim, output_dim, 
                 num_layers=1, bidirectional=False, dropout=0.5):
        super(TextRNN, self).__init__()
        
        # 词嵌入层
        self.embedding = nn.Embedding(vocab_size, embed_dim, padding_idx=0)
        
        # RNN层
        self.rnn = nn.RNN(
            input_size=embed_dim,
            hidden_size=hidden_dim,
            num_layers=num_layers,
            bidirectional=bidirectional,
            batch_first=True,
            dropout=dropout if num_layers > 1 else 0
        )
        
        # 全连接层
        fc_input_dim = hidden_dim * 2 if bidirectional else hidden_dim
        self.fc = nn.Linear(fc_input_dim, output_dim)
        
        # Dropout
        self.dropout = nn.Dropout(dropout)
        
    def forward(self, text):
        # text: (batch_size, seq_length)
        
        # 嵌入: (batch_size, seq_length, embed_dim)
        embedded = self.dropout(self.embedding(text))
        
        # RNN: output (batch_size, seq_length, hidden_dim * num_directions)
        #      hidden (num_layers * num_directions, batch_size, hidden_dim)
        output, hidden = self.rnn(embedded)
        
        # 取最后一个时间步的输出用于分类
        # 如果是双向,需要拼接前向和后向的最后隐藏状态
        if self.rnn.bidirectional:
            # hidden[-2, :, :] 是前向最后一层
            # hidden[-1, :, :] 是后向最后一层
            hidden = torch.cat([hidden[-2, :, :], hidden[-1, :, :]], dim=1)
        else:
            hidden = hidden[-1, :, :]
        
        # 全连接: (batch_size, output_dim)
        output = self.fc(self.dropout(hidden))
        
        return output

# ========== 3. 训练函数 ==========
def train_epoch(model, dataloader, criterion, optimizer, device):
    model.train()
    total_loss = 0
    correct = 0
    total = 0
    
    for batch_idx, (texts, labels) in enumerate(dataloader):
        texts, labels = texts.to(device), labels.to(device)
        
        # 前向传播
        optimizer.zero_grad()
        outputs = model(texts)
        loss = criterion(outputs, labels)
        
        # 反向传播
        loss.backward()
        
        # 梯度裁剪
        torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)
        
        # 参数更新
        optimizer.step()
        
        # 统计
        total_loss += loss.item()
        _, predicted = outputs.max(1)
        total += labels.size(0)
        correct += predicted.eq(labels).sum().item()
        
        if (batch_idx + 1) % 10 == 0:
            print(f'Batch {batch_idx+1}/{len(dataloader)}, '
                  f'Loss: {loss.item():.4f}, '
                  f'Acc: {100.*correct/total:.2f}%')
    
    return total_loss / len(dataloader), 100. * correct / total

# ========== 4. 评估函数 ==========
def evaluate(model, dataloader, criterion, device):
    model.eval()
    total_loss = 0
    correct = 0
    total = 0
    
    with torch.no_grad():
        for texts, labels in dataloader:
            texts, labels = texts.to(device), labels.to(device)
            
            outputs = model(texts)
            loss = criterion(outputs, labels)
            
            total_loss += loss.item()
            _, predicted = outputs.max(1)
            total += labels.size(0)
            correct += predicted.eq(labels).sum().item()
    
    return total_loss / len(dataloader), 100. * correct / total

# ========== 5. 主训练流程 ==========
def main():
    # 设置设备
    device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
    print(f'Using device: {device}')
    
    # 超参数
    VOCAB_SIZE = 10000
    EMBED_DIM = 100
    HIDDEN_DIM = 256
    OUTPUT_DIM = 2  # 二分类
    NUM_LAYERS = 2
    BIDIRECTIONAL = True
    DROPOUT = 0.5
    BATCH_SIZE = 64
    LEARNING_RATE = 0.001
    NUM_EPOCHS = 10
    
    # 创建模型
    model = TextRNN(
        vocab_size=VOCAB_SIZE,
        embed_dim=EMBED_DIM,
        hidden_dim=HIDDEN_DIM,
        output_dim=OUTPUT_DIM,
        num_layers=NUM_LAYERS,
        bidirectional=BIDIRECTIONAL,
        dropout=DROPOUT
    ).to(device)
    
    print(f'模型参数量: {sum(p.numel() for p in model.parameters()):,}')
    
    # 损失函数和优化器
    criterion = nn.CrossEntropyLoss()
    optimizer = optim.Adam(model.parameters(), lr=LEARNING_RATE)
    
    # 学习率调度器
    scheduler = optim.lr_scheduler.StepLR(optimizer, step_size=5, gamma=0.1)
    
    # 假设我们有训练和验证数据
    # train_loader = DataLoader(train_dataset, batch_size=BATCH_SIZE, shuffle=True)
    # val_loader = DataLoader(val_dataset, batch_size=BATCH_SIZE)
    
    # 训练循环
    best_val_acc = 0
    for epoch in range(NUM_EPOCHS):
        print(f'\n{"="*50}')
        print(f'Epoch {epoch+1}/{NUM_EPOCHS}')
        print(f'{"="*50}')
        
        # 训练
        train_loss, train_acc = train_epoch(
            model, train_loader, criterion, optimizer, device
        )
        
        # 验证
        val_loss, val_acc = evaluate(
            model, val_loader, criterion, device
        )
        
        # 更新学习率
        scheduler.step()
        
        print(f'\nTrain Loss: {train_loss:.4f}, Train Acc: {train_acc:.2f}%')
        print(f'Val Loss: {val_loss:.4f}, Val Acc: {val_acc:.2f}%')
        
        # 保存最佳模型
        if val_acc > best_val_acc:
            best_val_acc = val_acc
            torch.save(model.state_dict(), 'best_rnn_model.pth')
            print('✓ Saved best model')
    
    print(f'\n训练完成! 最佳验证准确率: {best_val_acc:.2f}%')

# if __name__ == '__main__':
#     main()

6.3 维度追踪工具

python 复制代码
class DimensionTracker:
    """辅助追踪RNN中的维度变化"""
    
    @staticmethod
    def print_shape(tensor, name):
        """打印张量形状"""
        if isinstance(tensor, torch.Tensor):
            print(f"{name:20s}: {str(tuple(tensor.shape)):20s} dtype={tensor.dtype}")
        else:
            print(f"{name:20s}: {tensor}")
    
    @staticmethod
    def trace_rnn_forward(rnn_module, input_tensor, hidden=None):
        """追踪RNN前向传播的所有维度"""
        print("\n" + "="*60)
        print("RNN Forward Pass Dimension Trace")
        print("="*60)
        
        # 输入
        DimensionTracker.print_shape(input_tensor, "Input")
        
        # 模型参数
        print("\nModel Parameters:")
        for name, param in rnn_module.named_parameters():
            DimensionTracker.print_shape(param, name)
        
        # 前向传播
        print("\nForward Propagation:")
        if hidden is not None:
            DimensionTracker.print_shape(hidden, "Initial Hidden")
            output, hidden = rnn_module(input_tensor, hidden)
        else:
            output, hidden = rnn_module(input_tensor)
        
        # 输出
        print("\nOutputs:")
        DimensionTracker.print_shape(output, "Output")
        DimensionTracker.print_shape(hidden, "Final Hidden")
        
        print("="*60 + "\n")
        
        return output, hidden

# 使用示例
rnn = nn.RNN(input_size=10, hidden_size=20, num_layers=2, 
             bidirectional=True, batch_first=True)
input_tensor = torch.randn(3, 5, 10)  # (batch, seq_len, input_size)

output, hidden = DimensionTracker.trace_rnn_forward(rnn, input_tensor)

# 输出示例:
# ============================================================
# RNN Forward Pass Dimension Trace
# ============================================================
# Input               : (3, 5, 10)           dtype=torch.float32
#
# Model Parameters:
# weight_ih_l0        : (20, 10)             dtype=torch.float32
# weight_hh_l0        : (20, 20)             dtype=torch.float32
# bias_ih_l0          : (20,)                dtype=torch.float32
# bias_hh_l0          : (20,)                dtype=torch.float32
# ...
#
# Forward Propagation:
#
# Outputs:
# Output              : (3, 5, 40)           dtype=torch.float32
# Final Hidden        : (4, 3, 20)           dtype=torch.float32
# ============================================================

6.4 可视化工具

python 复制代码
import matplotlib.pyplot as plt
import seaborn as sns

def visualize_attention_weights(attention_weights, input_words, output_words):
    """可视化注意力权重(适用于seq2seq with attention)"""
    plt.figure(figsize=(10, 8))
    sns.heatmap(attention_weights, 
                xticklabels=input_words,
                yticklabels=output_words,
                cmap='Blues',
                annot=True,
                fmt='.2f')
    plt.xlabel('Input Words')
    plt.ylabel('Output Words')
    plt.title('Attention Weights Visualization')
    plt.tight_layout()
    plt.show()

def plot_training_curves(train_losses, val_losses, train_accs, val_accs):
    """绘制训练曲线"""
    fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(15, 5))
    
    # 损失曲线
    ax1.plot(train_losses, label='Train Loss', marker='o')
    ax1.plot(val_losses, label='Val Loss', marker='s')
    ax1.set_xlabel('Epoch')
    ax1.set_ylabel('Loss')
    ax1.set_title('Training and Validation Loss')
    ax1.legend()
    ax1.grid(True)
    
    # 准确率曲线
    ax2.plot(train_accs, label='Train Acc', marker='o')
    ax2.plot(val_accs, label='Val Acc', marker='s')
    ax2.set_xlabel('Epoch')
    ax2.set_ylabel('Accuracy (%)')
    ax2.set_title('Training and Validation Accuracy')
    ax2.legend()
    ax2.grid(True)
    
    plt.tight_layout()
    plt.show()

总结

  1. 动机: RNN通过引入"记忆"机制处理序列数据,解决了传统神经网络无法捕捉时序依赖的问题

  2. 数学基础:

    • 核心公式: h t = tanh ⁡ ( W h h h t − 1 + W x h x t + b h ) h_t = \tanh(W_{hh} h_{t-1} + W_{xh} x_t + b_h) ht=tanh(Whhht−1+Wxhxt+bh)
    • 关键是理解维度变化和数据流动
    • BPTT算法用于训练
  3. 优化算法:

    • 梯度裁剪防止梯度爆炸
    • Adam优化器适合RNN
    • 截断BPTT处理长序列
  4. 工程方法:

    • PackedSequence高效处理变长序列
    • 双向RNN和多层RNN提升能力
    • 混合精度训练加速
  5. 局限性:

    • 长期依赖问题 → LSTM/GRU
    • 并行化困难 → Transformer
相关推荐
V1ncent Chen6 小时前
机器是如何理解语义的?:循环神经网络
人工智能·rnn·深度学习
代码游侠6 小时前
应用--Minishell实现
linux·运维·笔记·学习·算法
白茶三许6 小时前
【江鸟中原】集光鸿蒙项目开发
pytorch·深度学习·harmonyos
摘星观月7 小时前
【深度学习6】多层感知机2
人工智能·深度学习
zore_c7 小时前
【C语言】Win 32 API——一部分内容详解!!!
c语言·开发语言·c++·经验分享·笔记
重生之我在番茄自学网安拯救世界7 小时前
网络安全中级阶段学习笔记(七):Web 安全之文件上传漏洞笔记1(包含upload-labs-master靶场前三关实战)
笔记·学习·web安全·文件上传漏洞·网安基础
走在路上的菜鸟7 小时前
Android学Dart学习笔记第十五节 类
android·笔记·学习·flutter
xian_wwq7 小时前
【学习笔记】AI赋能安全运营中心典型场景
人工智能·笔记·学习
d111111111d7 小时前
嵌入式面试问题:STM32中指针和数组的本质区别是什么,常用数组存储什么数据?
java·笔记·stm32·单片机·嵌入式硬件·学习