目录标题
-
-
- 一、概念
- 二、api的几个参数(就这几个参数搞死我了)
-
- [第一步:初始化 `torch.nn.RNN` 的参数](#第一步:初始化
torch.nn.RNN的参数) - 第二步:调用时传入的参数
- 看完这第一步和第二步解释,还是有很多疑问
-
- [疑问1 这里为啥还传hx,rnn不是定义了:隐藏层的层数num_layers和隐藏层的特征数量hidden_size,为啥又传一遍?batch_size在rnn倒是没有定义(虽然 nn.RNN 规定了隐藏层的特征数量是 16,但它不知道你这次要同时批量处理多少数据),所以hx是不是num_layers和hidden_size多余](#疑问1 这里为啥还传hx,rnn不是定义了:隐藏层的层数num_layers和隐藏层的特征数量hidden_size,为啥又传一遍?batch_size在rnn倒是没有定义(虽然 nn.RNN 规定了隐藏层的特征数量是 16,但它不知道你这次要同时批量处理多少数据),所以hx是不是num_layers和hidden_size多余)
- [疑问2 input的序列长度seq_len和input的输入数据特征数量input_size有啥关系?batch_size是控制批量处理数量,hx就是受到这个影响(你传入批量数据处理数量,hx就会记录这个批量数据数量)](#疑问2 input的序列长度seq_len和input的输入数据特征数量input_size有啥关系?batch_size是控制批量处理数量,hx就是受到这个影响(你传入批量数据处理数量,hx就会记录这个批量数据数量))
- [疑问3 output和hn是啥,分别干啥用的,他俩啥关系](#疑问3 output和hn是啥,分别干啥用的,他俩啥关系)
- [第一步:初始化 `torch.nn.RNN` 的参数](#第一步:初始化
- [三、数据经过nn.RNN,有8个输入特征值,输出是16个特征值,下一个隐藏层怎么接受上一层的16特征值数据继续处理啊(这里num_layers 如果是2层)](#三、数据经过nn.RNN,有8个输入特征值,输出是16个特征值,下一个隐藏层怎么接受上一层的16特征值数据继续处理啊(这里num_layers 如果是2层))
-
- [关键点一:`input_size=8` 只对谁负责?](#关键点一:
input_size=8只对谁负责?) - [关键点二:第二层怎么接受上一层的 16 特征值?(内部转换)](#关键点二:第二层怎么接受上一层的 16 特征值?(内部转换))
- [关键点一:`input_size=8` 只对谁负责?](#关键点一:
- 四、数据是怎么在隐藏层里变化的
- 五、代码api
-
一、概念
文本是连续的,具有序列特性。如果其序列被重排可能就会失去原有的意义。比如"狗咬人"这段文本具有序列关系,如果文字的序列颠倒可能就会表达不同的意思
目前我们接触的神经网络都是前馈型神经网络。前馈(feedforward)是指网络的传播方向是单向的。具体地说,将输入信号传给下一层,下一层接收到信号后传给下下一层,然后再传给下下下一层...像这样,信号仅在一个方向上传播。虽然前馈网络结构简单、易于理解,并且可以应用于许多任务中。不过,这种网络存在一个大问题,就是不能很好地处理时间序列数据。更确切地说,单纯的前馈网络无法充分学习时序数据的性质。于是,循环神经网络(Recurrent Neural Network,RNN)应运而生。
RNN层具有环路,通过环路数据可以在层内循环。向时序数据x0,x1...xt输入层中,相应的会输出h0,h1...ht。

RNN层的循环的展开:

由图可知,各个时刻的 RNN 层接收传给该层的输入 x t x_t xt 和前一个时刻 RNN 层的输出 h t − 1 h_{t-1} ht−1,据此计算当前时刻 RNN 层的输出 h t h_t ht:
h t = tanh ( h t − 1 W h + x t W x + b ) h_t = \tanh(h_{t-1}W_h + x_tW_x + b) ht=tanh(ht−1Wh+xtWx+b)
RNN 层有 2 个权重,分别是与输入 x t x_t xt 运算的权重 W x W_x Wx,和与前一时刻 RNN 层的输出 h t − 1 h_{t-1} ht−1(也叫隐藏状态,隐状态)运算的权重 W h W_h Wh。执行完乘积和求和运算之后使用 tanh \tanh tanh 函数转换,其结果就是时刻 t t t 的输出 h t h_t ht。
二、api的几个参数(就这几个参数搞死我了)
老师不先讲清楚这几个参数是干啥的,所以我们要先搞清楚这几个参数才能继续听尚硅谷老师的api
可使用torch.nn.RNN来初始化RNN层:
python
rnn = torch.nn.RNN(input_size, hidden_size, num_layers)
# input_size:输入数据的特征数量
# hidden_size:隐藏状态的特征数量
# num_layers:隐藏层的层数,如果设置多个层,前一个隐藏层的输出作为下一个隐藏层的输入
调用时需要传入2个参数:最终会返回两个参数
python
output, hn = rnn(input, hx)
# input:输入数据[seq_len序列长度, batch_size批量大小, input_size]
# hx:初始隐状态[num_layers, batch_size, hidden_size]
# output:输出数据[seq_len, batch_size, hidden_size]
# hn:隐状态[num_layers, batch_size, hidden_size]
第一步:初始化 torch.nn.RNN 的参数
rnn = torch.nn.RNN(input_size, hidden_size, num_layers)
这就好比你要建一个加工厂,在建厂的时候,你要定好厂房的规格:
input_size(输入数据特征数量):原料的尺寸。比如你要处理一句话,每个词变成了一个向量(比如长度为 8 的向量),那这个值就是 8。hidden_size(隐藏状态特征数量):大脑的记忆容量。也就是经过这个网络后,特征被转换成的维度大小。值越大,能记住的信息越多,但也越慢。假设我们定为 16。num_layers(隐藏层的层数):流水线的层数。如果是 1,就是单层 RNN;如果是 2,就是第一层 RNN 的输出,直接作为第二层 RNN 的输入。
👉 举个例子(建厂):
rnn = torch.nn.RNN(input_size=8, hidden_size=16, num_layers=1)
解释: 我建了一个单层的 RNN 工厂。工厂接收长度为 8 的原料,经过加工后,吐出长度为 16 的记忆信息。
rnn = torch.nn.RNN(input_size=8, hidden_size=16, num_layers=2)
解释: 我建了一个两层的 RNN 工厂。工厂接收长度为 8 的原料,经过加工后,吐出长度为 16 的记忆信息。
第二步:调用时传入的参数
截图代码:output, hn = rnn(input, hx)
工厂建好了,现在要把具体的货物送进去加工。这里有个非常大的难点,就是"数据的维度(形状)"。
input(输入数据) :形状必须是[seq_len, batch_size, input_size]seq_len(序列长度):一句话有几个词(或者时间序列有几个时刻)。比如"我爱吃苹果"有4个词,就是 4。batch_size(批量大小):一次同时处理几句话(几批货)。比如一次送 2 句话进去。input_size:和建厂时一样,每个词的特征长度,必须是 8。
hx(初始隐状态) :形状必须是[num_layers, batch_size, hidden_size]- 这就是 RNN 的"初始记忆"。如果是全 0,就相当于失忆状态开始算。如果不传,PyTorch 默认会给你生成全 0 的。形状里的
hidden_size必须是 16。
- 这就是 RNN 的"初始记忆"。如果是全 0,就相当于失忆状态开始算。如果不传,PyTorch 默认会给你生成全 0 的。形状里的
看完这第一步和第二步解释,还是有很多疑问
疑问1 这里为啥还传hx,rnn不是定义了:隐藏层的层数num_layers和隐藏层的特征数量hidden_size,为啥又传一遍?batch_size在rnn倒是没有定义(虽然 nn.RNN 规定了隐藏层的特征数量是 16,但它不知道你这次要同时批量处理多少数据),所以hx是不是num_layers和hidden_size多余
python
rnn = torch.nn.RNN(input_size, hidden_size, num_layers)
# input_size:输入数据的特征数量
# hidden_size:隐藏状态的特征数量
# num_layers:隐藏层的层数,如果设置多个层,前一个隐藏层的输出作为下一个隐藏层的输入
output, hn = rnn(input, hx),
# hx:初始隐状态[num_layers, batch_size, hidden_size]
hx 要记录每个RNN层,同时批量处理多少数据,还有每个数据在隐藏层处理后的特征值。所以格式一定会包含 num_layers和hidden_size,它是为了记录每次RNN输出结果,做记录。 和rnn初始化中num_layers和hidden_size肯定一样啊。 两个不存在什么重复,又传一遍,两个八竿子打不着,一点关系没有。不懂得时候瞎问两个啥关系,是不是重复(哈哈)
举个列子说明hx作用
为了让你秒懂,我们把"建厂"和"启动流水线"区分开来,用一个"背书"的例子来解释:
1. 建厂(初始化):nn.RNN(...)
你买的这台机器,出厂设置规定了:
- 它有几层大脑(
num_layers=2)。 - 每个大脑能装多少东西(
hidden_size=16)。
这就像你买了一本"空白的记事本",本子规定了有 2 页(2层),每页能写 16 个字(特征大小)。
2. 启动流水线(调用):rnn(input, hx)
当你真正要开始处理数据时,PyTorch 要求你告诉它:在开始读第一个词之前,这 2 层大脑里原本装的是什么?这就是 hx (Initial Hidden State,初始隐状态)的作用。
为什么一定要传?因为 RNN 是"连轴转"的,它必须知道起点在哪里。
原因一:为了处理"上下文的延续"(比如一篇文章分两次读)
假设你正在读一本小说。昨天你读了前 10 章,今天你要接着读第 11 章。
- 今天你调用
rnn处理第 11 章的数据(input)。 - 第 11 章的第一个字,不能凭空开始算,它必须承接昨天的记忆。所以,你需要把昨天读完第 10 章时的最终记忆作为
hx传进去。
原因二:为了默认的"失忆"状态(全零)
如果你今天刚刚拿到这本书,从第一章开始读,那你大脑里原本没有任何记忆。
在代码里怎么表示"失忆"?就是传一个全 0 的张量给 hx。
如果 PyTorch 允许你不传 hx,它其实就会在底层偷偷帮你生成一个和 input 维度匹配的全 0 张量。但为了灵活性,PyTorch 把选择权交给了你。
原因三:为了并行处理多个句子(batch_size 的体现)
这里的 batch_size=3 (一次处理 3 句话)。
虽然 nn.RNN 规定了大脑大小是 16,但它不知道你这次要同时喂进去 3 句话。
所以,你传入的 hx 形状是 [2, 3, 16]:
2:对应两层大脑(num_layers)。3:对应这批数据有 3 句话(batch_size),这是初始化时没有的信息!每一句话都需要自己独立的初始记忆,不能混在一起。16:每句话的记忆长度(hidden_size)。
灵魂拷问:既然传了,它到底怎么起作用?
你回头看上一张图里的公式:
h t = tanh ( h t − 1 ∗ W h + x t ∗ W x + b ) h_t = \tanh(h_{t-1} * W_h + x_t * W_x + b) ht=tanh(ht−1∗Wh+xt∗Wx+b)
这里的 h t − 1 h_{t-1} ht−1 就是上一刻的记忆。
- 当 t = 0 t = 0 t=0 时,公式里的 h t − 1 h_{t-1} ht−1 就是你传进去的
hx。 - 当 t = 1 t = 1 t=1 时,公式里的 h t − 1 h_{t-1} ht−1 就变成了 t = 0 t = 0 t=0 时计算出来的 h 0 h_0 h0(此时传进去的
hx就被覆盖掉了)。
疑问2 input的序列长度seq_len和input的输入数据特征数量input_size有啥关系?batch_size是控制批量处理数量,hx就是受到这个影响(你传入批量数据处理数量,hx就会记录这个批量数据数量)
python
rnn = torch.nn.RNN(input_size, hidden_size, num_layers)
# input_size:输入数据的特征数量
# hidden_size:隐藏状态的特征数量
# num_layers:隐藏层的层数,如果设置多个层,前一个隐藏层的输出作为下一个隐藏层的输入
output, hn = rnn(input, hx),
# input:输入数据[seq_len序列长度, batch_size批量大小, input_size]
核心区别:
seq_len(序列长度) :代表"时间 "或者"步骤"。也就是这段话有几个词,或者这个视频有几帧。input_size(特征数量) :代表"某一个时刻"里包含的信息量。也就是一个词有多大,或者一帧画面有多大。
举个"自然语言处理"的例子:
假设我们要让电脑读一句话:"我 爱 学习"(一共 3 个词)。
-
怎么把词变成数字?
电脑不认识字,我们需要把每个词变成一串数字(这就叫词向量 )。假设我们规定,每个词用 8 个数字来表示。
- "我" =
[0.1, 0.5, ...](共8个数字) - "爱" =
[0.8, 0.2, ...](共8个数字) - "学习" =
[0.4, 0.9, ...](共8个数字)
- "我" =
-
这时候两个参数是什么?
input_size = 8:因为每个词是用 8 个数字表示的。seq_len = 3:因为这句话有 3 个词(相当于 3 个时间步)。
如果按 [seq_len, batch_size, input_size] 排版,数据长这样:
bash
[
[ [0.1, 0.5, ...] ], <-- 第1个时刻(词"我"),长度是8
[ [0.8, 0.2, ...] ], <-- 第2个时刻(词"爱"),长度是8
[ [0.4, 0.9, ...] ] <-- 第3个时刻(词"学习"),长度是8
]
你看,seq_len(3)就像是排队的队伍有多长,input_size(8)是队伍里每个人的胖瘦(特征数)。
再举个完整得"自然语言处理"的例子(包含序列长度,批量大小,特征数量):
假设我们要让电脑读两句话:"我 爱 学习" (一共 3 个词), "我 喜欢 羽毛球"(一共 3 个词)。
-
怎么把词变成数字?
电脑不认识字,我们需要把每个词变成一串数字(这就叫词向量 )。假设我们规定,每个词用 8 个数字来表示。
- "我" =
[0.1, 0.5, ...](共8个数字) - "爱" =
[0.8, 0.2, ...](共8个数字) - "学习" =
[0.4, 0.9, ...](共8个数字) - "喜欢" =
[0.6, 0.6, ...](共8个数字) - "羽毛球" =
[0.1, 0.9, ...](共8个数字)
- "我" =
如果按 [seq_len, batch_size, input_size] 排版,数据长这样:
bash
[
[ [0.1, 0.5, ...],[0.1, 0.5, ...] ], <-- 第1个时刻 第一个批量数据(词"我"),长度是8,第二个批量数据(词"我"),长度是8
[ [0.8, 0.2, ...],[0.6, 0.6, ...] ], <-- 第2个时刻 第一个批量数据(词"爱"),长度是8,第二个批量数据(词"喜欢"),长度是8
[ [0.4, 0.9, ...],[0.1, 0.9, ...] ] <-- 第3个时刻 第一个批量数据(词"学习"),长度是8,第二个批量数据(词"羽毛球"),长度是8
]
疑问3 output和hn是啥,分别干啥用的,他俩啥关系
python
rnn = torch.nn.RNN(input_size, hidden_size, num_layers)
# input_size:输入数据的特征数量
# hidden_size:隐藏状态的特征数量
# num_layers:隐藏层的层数,如果设置多个层,前一个隐藏层的输出作为下一个隐藏层的输入
output, hn = rnn(input, hx)
# output:输出数据[seq_len, batch_size, hidden_size]
# hn:隐状态[num_layers, batch_size, hidden_size]
前置假设条件: rnn 是输入特征数量8,隐藏层特征数量16,隐藏层有2层
1. output 到底是啥?
output 是一个三维张量,形状是 [seq_len, batch_size, hidden_size](如果没有设置 batch_first=True 的话)。
大白话解释:
我们rnn有两层(num_layers=2)。output 里面装的,全是"最后一层(第2层)"的输出。
假设你的输入是一句话:"我 爱 学习"(一共3个词,seq_len=3,batch_size=1,hidden_size=16)。
RNN 处理的时候,是一个词一个词吃进去的:
- 吃进"我",第2层输出一个 16 维的向量 h 0 h_0 h0 -> 存入
output[0] - 吃进"爱",第2层输出一个 16 维的向量 h 1 h_1 h1 -> 存入
output[1] - 吃进"学习",第2层输出一个 16 维的向量 h 2 h_2 h2 -> 存入
output[2]
所以,output 的形状是 [3, batch_size=1, 16]。它记录了第2层在每一个时刻吐出来的东西。
(注:如果 num_layers=1,那就是那一层每个时刻的输出;如果有2层,PyTorch 默认只把最顶层的输出给你,因为底层输出已经被顶层消耗掉了。)
2. hn 到底是啥?
hn 也是一个三维张量,形状是 [num_layers, batch_size, hidden_size]。
在例子中,形状是 [2, batch_size=1, 16]。
大白话解释:
hn 也就是 hidden state (最后时刻的隐状态)。
hn[0]是第 1 层在读完最后一个词("学习")后,脑子里的记忆。hn[1]是第 2 层在读完最后一个词("学习")后,脑子里的记忆。
3. 它俩到底是什么关系?(核心来了!)
它们的关系非常简单:
对于最后一层(最高层)来说,hn 的最后一个切片,和 output 的最后一个切片,是一模一样的!
用伪代码表示就是:
output[-1] == hn[-1]
4. 总结
output和hn都是三维,
一个是时序当第三维度,里面记录最后一个cnn处理后的batch_size和hidden_size二维数据,
一个是cnn层数当第三维度,记录每一个cnn处理后的batch_size和hidden_size二维数据
- output:输出数据seq_len, batch_size, hidden_size
- output 是 受到 rnn的hidden_size影响 外加 输入input 的 seq_len和batch_size影响
- hn:隐状态num_layers, batch_size, hidden_size
- hn 是 受到 rnn的num_layers和hidden_size影响 外加 输入input的batch_size影响
三、数据经过nn.RNN,有8个输入特征值,输出是16个特征值,下一个隐藏层怎么接受上一层的16特征值数据继续处理啊(这里num_layers 如果是2层)
假设 input_size 是8, hidden_size是16,num_layers是2
rnn = torch.nn.RNN(input_size=8, hidden_size=16, num_layers=2)
关键点一:input_size=8 只对谁负责?
当你设置 input_size=8 时,这个限制仅仅针对整个网络的最底层(第一层) 。
意思是:"我的第一层,只负责接收长度为 8 的特征向量。"
关键点二:第二层怎么接受上一层的 16 特征值?(内部转换)
当 num_layers=2 时,PyTorch 其实在内部偷偷建了两个 RNN 单元:
- 底层 RNN(Layer 0):
- 它的
input_size是 8(你指定的)。 - 它的
hidden_size是 16(你指定的)。 - 所以,它吃进长度为 8 的数据,吐出长度为 16 的输出。
- 它的
- 顶层 RNN(Layer 1):
- 它的
input_size被 PyTorch 自动设置成了 16! (因为它紧跟在前一个hidden_size为 16 的层后面)。 - 它的
hidden_size依然是 16。 - 所以,它吃进长度为 16 的数据,再吐出长度为 16 的数据。
- 它的
大白话解释流水线:
你建了一个两层加工厂。你规定大门(第一层)必须吞进 8 寸的原料。第一层加工完,吐出了 16 寸的半成品。
第二层根本不需要你操心,PyTorch 会自动把第二层的进货口调成 16 寸,直接接上第一层的传送带。第二层加工完,最终还是吐出 16 寸的成品。
四、数据是怎么在隐藏层里变化的

h_t = tanh(h_{t-1} * W_h + x_t * W_x + b)
💡 场景设定:
- 我们有一句话:"我 爱 学习" (
seq_len = 3,被切分为3个词) - 每个字是一个长度为 8 的向量(
input_size = 8)。 - 记忆力(
hidden_size = 16)。 - 一次只处理这一句话(
batch_size = 1)
这是只有RNN只有一层num_layers的时候
就因为这里batch_size 是 1,后面得到的hn 就是 一个长度为16的向量,hn的形状是1,batch_size,hidden_size组成。
如果batch_size 是 2,后面得到的hn 就是 2 * 16的张量,hn的形状是1,batch_size,hidden_size组成。
这是只有RNN只有两层num_layers的时候
就因为这里batch_size 是 1,后面得到的hn 就是 一个长度为16的向量,hn的形状是2,batch_size,hidden_size组成。
如果batch_size 是 2,后面得到的hn 就是 2 * 16的张量,hn的形状是2,batch_size,hidden_size组成。
(我回头再详细讲这个 hn 这个返回值)
⚙️ 第一步:数据进入 RNN(在 t = 0 t = 0 t=0 时刻)
- 送进去第一个字 x 0 x_0 x0 ("我",长度为 8)。
- RNN 内部有两个矩阵 W x W_x Wx (给输入用的)和 W h W_h Wh (给上一次记忆用的)。
RNN内部有两个矩阵 - 因为没有上一次的记忆(初始 h − 1 h_{-1} h−1 为全 0),所以公式变成了
tanh(0 + x_0 * W_x + b)。 - 经过数学计算,输出了第一个时刻的记忆 h 0 h_0 h0 (长度为 16)。
- 此时系统产生一个输出
output[0]就是 h 0 h_0 h0。
⚙️ 第二步:记忆传递(在 t = 1 t = 1 t=1 时刻)
- 送进去第二个字 x 1 x_1 x1 ("爱",长度为 8)。
- 重点来了! 此时 h 0 h_0 h0 会像图中红线的那样,从左边"流"到右边。
- RNN 把 x 1 x_1 x1 和 h 0 h_0 h0 结合起来,套入公式:
tanh(h_0 * W_h + x_1 * W_x + b)。 - 经过计算,输出了第二个时刻的记忆 h 1 h_1 h1 (长度为 16)。
- 此时系统产生一个输出
output[1]就是 h 1 h_1 h1。
⚙️ 第三步:继续传递(在 t = 2 t = 2 t=2 时刻)
- 送进去第三个字 x 2 x_2 x2 ("学习",长度为 8)。
- 把 x 2 x_2 x2 和上一刻的记忆 h 1 h_1 h1 结合起来,套公式:
tanh(h_1 * W_h + x_2 * W_x + b)。 - 计算出 h 2 h_2 h2。
- 产生输出
output[2]就是 h 2 h_2 h2。
🎉 总结最终结果:
- 执行完这一句话,你得到的
output包含了[h_0, h_1, h_2],你可以用它们来做每个字的词性标注。 - 你得到的
hn就是最后的h_2。为什么只留 h 2 h_2 h2?因为 h 2 h_2 h2 经过了前面的传递,"记住"了前面所有的信息**(我、爱、学习)**,所以它代表整句话的最终语义。
五、代码api
python
import torch
#定义RNN层
rnn = torch.nn.RNN(input_size=8, hidden_size=16, num_layers=2)
#定义输入数据和初始隐状态
input = torch.rand(1, 3, 8)
hx = torch.randn(2, 3, 16)
#前向传播,得到输出和隐状态
output, hn = rnn(input=input, hx=hx)
print(output.shape) # torch.Size([1, 3, 16])
print(hn.shape) # torch.Size([2, 3, 16])