❤️ 用“我喜欢你”讲解你的 RNN 代码全过程(超大白话)

📌 你需要知道的前提(非常简单)

在 RNN 里,“我喜欢你”不是直接输入文字,而是变成三个“向量”:

  • “我” → 一个 5 维向量(input_size=5)
  • “喜欢” → 一个 5 维向量
  • “你” → 一个 5 维向量

为什么是 5 维?
因为你的 RNN 是 nn.RNN(5, 6, 1),规定每个词的向量必须是长度 5。

然后 RNN 会按顺序,一个一个词地读


✨ 故事开始:RNN 是一个“小记事本机器人”

这个机器人叫 RNN 小明,它的特点:

  • 每次只能看一个词(input_size=5 的向量)
  • 它有一个“脑袋记忆”(hidden_size=6)
  • 每看一个词,就会更新一次记忆

就像你让小明读一段话,他会边看边记:

1
我 → 喜欢 → 你

🧩 对应到你的 RNN 代码

第一步:创建 RNN

1
rnn = nn.RNN(5, 6, 1)

翻译成大白话:

“小明每次读一个 5 维向量,他的脑容量是 6 维,他只有一层大脑。”


第二步:准备输入(“我喜欢你”)

你的代码里是这样:

1
input = torch.randn(1, 3, 5)

虽然是随机数,但其实真实意义就是:

1
2
3
sequence_length = 1
batch_size = 3
input_size = 5

如果换成“我喜欢你”:

1
2
3
sequence_length = 3  (三个词)
batch_size = 1 (一次只读一句话)
input_size = 5 (每个词是 5 维)

真实输入应该像这样:

5 维向量(示例)
[0.2, 0.8, -0.3, 0.1, 0.5]
喜欢 [-0.4, 0.9, 0.1, -0.2, 0.7]
[0.5, -0.6, 0.8, 0.3, 0.1]

💡 RNN 是怎么“读句子”的?

RNN 小明开始读句子


⭐ 第 1 步:读到“我”

输入:

1
2
x1 = “我”的向量
h0 = 初始记忆(你的代码里的 h0)

小明做的事情:

  1. 看“我”的向量
  2. 把向量和自己的初始记忆混合
  3. 得出新的记忆 h1

就像:

小明心想:“哦,对方说的是‘我’。记下来。”

输出(output[0])也是 h1。


⭐ 第 2 步:读到“喜欢”

输入:

1
2
x2 = “喜欢”的向量
h1 = 上一步的记忆

小明做的事情:

  1. 看当前词“喜欢”
  2. 打开第 1 步的记忆(他还记着“我”)
  3. 综合两者,更新记忆变成 h2

就像:

“我……喜欢……嗯,可能在表达感情。”

输出(output[1])就是 h2。


⭐ 第 3 步:读到“你”

输入:

1
2
x3 = “你”的向量
h2 = 上一步的记忆

小明:

  1. 看“你”
  2. 想起之前读过“我喜欢”
  3. 把它们综合成最终记忆 h3

就像:

“我……喜欢……你!哦,我懂了!”

最终输出(output[2])就是 h3。


🏁 最后两个结果:output 和 hn

当读完全部句子后:

✔ output(每一步的结果):

1
2
3
4
5
[
h1, # 读完“我”时的理解
h2, # 读完“喜欢”时的理解
h3 # 读完“你”时的理解
]

✔ hn(最终的记忆)

1
hn = h3

也就是整句话读完后,小明的最终理解。


📌 对照你的代码结果

你输入的是 1 个时间步(seq_len=1),所以 output == hn
如果你输入“我喜欢你”(seq_len=3),就会变成:

1
2
output = [h1, h2, h3]
hn = h3

🎉 总结一句超级大白话

RNN 就像让一个小机器人读句子,他每读一个词,就把这个词和之前的记忆混合,更新自己的理解,一直到把整句话读完。

整个代码如下:

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
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
# -*- coding:utf-8 -*-
import torch
import torch.nn as nn

def dm1_rnn_base():
# 1.实例化模型
# RNN的参数说明:
# 第一个参数input_size:输入的词嵌入维度
# 第二个参数hidden_size:RNN单元输出的隐藏层张量的维度
# 第三个参数num_layers:有几层RNN单元(有几个隐藏层)
input_size = 5
hidden_size = 6
num_layers = 1
model = nn.RNN(input_size, hidden_size, num_layers)

# 2. 获取x0输入
# x0的参数说明
# 第一个参数sequence_len:每个样本的长度(单词的个数)(因为RNN模型batch_first=False, seq_len放在第一位置)
# 第二个参数batch_size:一个批次送入几个样本
# 第三个参数input_size:输入的词嵌入维度
sequence_len = 1
batch_size = 3
x0 = torch.randn(sequence_len, batch_size, input_size)

# 3.获取h0输入
# h0的参数说明
# 第一个参数num_layers:有几层RNN单元(有几个隐藏层)
# 第二个参数batch_size:一个批次送入几个样本
# 第三个参数hidden_size:RNN单元输出的隐藏层张量的维度

h0 = torch.randn(num_layers, batch_size, hidden_size)

# 4.将输入送给RNN模型得到下一时间步的输出结果
output, hn = model(x0, h0)

print(f'output--》{output}')
print('*'*80)
print(f'hn--》{hn}')


def dm2_rnn_len():
# 修改样本的长度
# 1.实例化模型
# RNN的参数说明:
# 第一个参数input_size:输入的词嵌入维度
# 第二个参数hidden_size:RNN单元输出的隐藏层张量的维度
# 第三个参数num_layers:有几层RNN单元(有几个隐藏层)
input_size = 5
hidden_size = 6
num_layers = 1
model = nn.RNN(input_size, hidden_size, num_layers)

# 2. 获取x0输入
# x0的参数说明
# 第一个参数sequence_len:每个样本的长度(单词的个数)(因为RNN模型batch_first=False, seq_len放在第一位置)
# 第二个参数batch_size:一个批次送入几个样本
# 第三个参数input_size:输入的词嵌入维度
# 比如“我喜欢你”可以切分成为“我 喜欢 你”,这时候sequence_len = 3(我 喜欢 你 这是三个单词) batch_size = 1(因为一次只送入一个样本)
# | 词 | 5 维向量(示例) |
# | -- | --------------------------- |
# | 我 | [0.2, 0.8, -0.3, 0.1, 0.5] |
# | 喜欢 | [-0.4, 0.9, 0.1, -0.2, 0.7] |
# | 你 | [0.5, -0.6, 0.8, 0.3, 0.1] |

sequence_len = 4
batch_size = 3
x0 = torch.randn(sequence_len, batch_size, input_size)
print(f'x0 -->{x0.shape}')

# 3.获取h0输入
# h0的参数说明
# 第一个参数num_layers:有几层RNN单元(有几个隐藏层)
# 第二个参数batch_size:一个批次送入几个样本
# 第三个参数hidden_size:RNN单元输出的隐藏层张量的维度

h0 = torch.randn(num_layers, batch_size, hidden_size)
print(f'h0--》{h0.shape}')

# 4.将输入送给RNN模型得到下一时间步的输出结果(一次性送入模型)
output, hn = model(x0, h0)
print("一次性送入模型")
print(f'output--》{output}')
print('*'*80)
print(f'hn--》{hn}')
print('&'*80)
# 5. 将一个token一个token往RNN模型里面去送
# x0.size(0) = 4代表sequence_len
# x0-->[4, 1, 5]
# print(f'x0-->{x0}')
# print(f'一个token一个token往RNN模型里面去送')
# for idx in range(x0.size(0)):
# # print(x0[idx])
# temp = x0[idx].unsqueeze(dim=0)
# # print(f'temp--》{temp}')
# # break
# output, h0 = model(temp, h0)
# print(f'output--》{output}')
# print(f'h0--》{h0}')
# print('*'*80)

def dm3_rnn_batch():
# batch的位置放到第一位
# 1.实例化模型
# RNN的参数说明:
# 第一个参数input_size:输入的词嵌入维度
# 第二个参数hidden_size:RNN单元输出的隐藏层张量的维度
# 第三个参数num_layers:有几层RNN单元(有几个隐藏层)
# batch_first默认为False,但是如果设置为True,那么input第一个参数是batch_size
input_size = 5
hidden_size = 6
num_layers = 1
model = nn.RNN(input_size, hidden_size, num_layers, batch_first=True)

# 2. 获取x0输入
# x0的参数说明
# 第一个参数sequence_len:每个样本的长度(单词的个数)(因为RNN模型batch_first=False, seq_len放在第一位置)
# 第二个参数batch_size:一个批次送入几个样本
# 第三个参数input_size:输入的词嵌入维度
sequence_len = 4
batch_size = 3
x0 = torch.randn(batch_size, sequence_len, input_size)
print(f'x0 -->{x0.shape}')

# 3.获取h0输入
# h0的参数说明
# 第一个参数num_layers:有几层RNN单元(有几个隐藏层)
# 第二个参数batch_size:一个批次送入几个样本
# 第三个参数hidden_size:RNN单元输出的隐藏层张量的维度

h0 = torch.randn(num_layers, batch_size, hidden_size)
print(f'h0--》{h0.shape}')

# 4.将输入送给RNN模型得到下一时间步的输出结果(一次性送入模型)
output, hn = model(x0, h0)
print("一次性送入模型")
print(f'output--》{output}')
print('*'*80)
print(f'hn--》{hn}')
print('&'*80)

def dm4_rnn_numlayers():
# 多个RNN单元
# 1.实例化模型
# RNN的参数说明:
# 第一个参数input_size:输入的词嵌入维度
# 第二个参数hidden_size:RNN单元输出的隐藏层张量的维度
# 第三个参数num_layers:有几层RNN单元(有几个隐藏层)
# batch_first默认为False,但是如果设置为True,那么input第一个参数是batch_size
input_size = 5
hidden_size = 6
num_layers = 2
model = nn.RNN(input_size, hidden_size, num_layers=2)

# 2. 获取x0输入
# x0的参数说明
# 第一个参数sequence_len:每个样本的长度(单词的个数)(因为RNN模型batch_first=False, seq_len放在第一位置)
# 第二个参数batch_size:一个批次送入几个样本
# 第三个参数input_size:输入的词嵌入维度
sequence_len = 4
batch_size = 3
x0 = torch.randn(sequence_len, batch_size, input_size)
print(f'x0 -->{x0.shape}')

# 3.获取h0输入
# h0的参数说明
# 第一个参数num_layers:有几层RNN单元(有几个隐藏层)
# 第二个参数batch_size:一个批次送入几个样本
# 第三个参数hidden_size:RNN单元输出的隐藏层张量的维度

h0 = torch.randn(num_layers, batch_size, hidden_size)
print(f'h0--》{h0.shape}')

# 4.将输入送给RNN模型得到下一时间步的输出结果(一次性送入模型)
output, hn = model(x0, h0)
print("一次性送入模型")
print(f'output--》{output}')
print('*'*80)
print(f'hn--》{hn}')
print('&'*80)

def dm5_rnn_1():
rnn = nn.RNN(5, 6)
x = torch.randn(3, 4, 5)
output, hn = rnn(x)
print(f'output--》{output}')
print(f'hn--》{hn}')
if __name__ == '__main__':
# dm1_rnn_base()
# dm2_rnn_len()
# dm3_rnn_batch()
# dm4_rnn_numlayers()
dm5_rnn_1()

已知:

现在我创建了一个 rnn,输入的参数是 input 和 h0。input 的形状是(a,b,c)。h0 的形状是(d,e,f)请问

$$output,hn=rnn(input,h0)$$

运行之后得到的两个参数的形状是什么

  • input 的形状 (a, b, c)
    • seq_len = a
    • batch = b
    • input_size = c
  • h0 的形状 (d, e, f)
    • num_layers = d
    • batch = e(必须等于 b
    • hidden_size = f

输出形状:

  1. output(每个时间步的输出):

$$\text{output.shape} = (seq_len, batch, hidden_size) = (a, b, f)$$

  1. hn(最后时间步的隐藏状态):

$$ \text{hn.shape} = (num_layers, batch, hidden_size) = (d, b, f)$$

注意:e 必须等于 b,否则会报错。


所以 单向 RNN 的输出形状总结为:

1
2
output.shape = (a, b, f)
hn.shape = (d, b, f)

对于下面这段代码来说:

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
# 1.实例化模型
# RNN的参数说明:
# 第一个参数input_size:输入的词嵌入维度
# 第二个参数hidden_size:RNN单元输出的隐藏层张量的维度
# 第三个参数num_layers:有几层RNN单元(有几个隐藏层)
input_size = 5
hidden_size = 6
num_layers = 1
model = nn.RNN(input_size, hidden_size, num_layers)

# 2. 获取x0输入
# x0的参数说明
# 第一个参数sequence_len:每个样本的长度(单词的个数)(因为RNN模型batch_first=False, seq_len放在第一位置)
# 第二个参数batch_size:一个批次送入几个样本
# 第三个参数input_size:输入的词嵌入维度
sequence_len = 1
batch_size = 3
x0 = torch.randn(sequence_len, batch_size, input_size)

# 3.获取h0输入
# h0的参数说明
# 第一个参数num_layers:有几层RNN单元(有几个隐藏层)
# 第二个参数batch_size:一个批次送入几个样本
# 第三个参数hidden_size:RNN单元输出的隐藏层张量的维度

h0 = torch.randn(num_layers, batch_size, hidden_size)

# 4.将输入送给RNN模型得到下一时间步的输出结果
output, hn = model(x0, h0)

print(f'output--》{output}')
print('*'*80)
print(f'hn--》{hn}')

已知参数:

  • input_size = 5
  • hidden_size = 6
  • num_layers = 1
  • sequence_len = 1
  • batch_size = 3

构建的 RNN:

1
model = nn.RNN(input_size, hidden_size, num_layers)

输入:

1
2
x0.shape = (sequence_len, batch_size, input_size) = (1, 3, 5)
h0.shape = (num_layers, batch_size, hidden_size) = (1, 3, 6)

输出规则(单向 RNN,batch_first=False):

  1. output 的形状:
1
output.shape = (seq_len, batch, hidden_size) = (1, 3, 6)
  1. hn 的形状:
1
hn.shape = (num_layers, batch, hidden_size) = (1, 3, 6)

✅ 用你的变量表示:

1
2
output.shape = (sequence_len, batch_size, hidden_size)  # (1, 3, 6)
hn.shape = (num_layers, batch_size, hidden_size) # (1, 3, 6)

所以在这段代码里,outputhn 的维度都是 (1, 3, 6),只是含义不同:

  • output:每个时间步的输出
  • hn:最后一个时间步的隐藏状态

总结一下就是: output 作为下一个输入,除了input_size的维度变成hidden_size(可以想象成为 CNN 的神经元从 input_size->hidden_size)以外其他的都不变,而 hn 直接就是都不变的。