第一章、RNN(循环神经网络)
1 循环神经网络(RNN)
展开表示该模型可以表示为:
注意其中的RNN中的参数共用同一套参数。 那直观的理解这个网络的作用实际上就是结合当前的信息和历史中有用的信息综合输出相应的结果,及结合当前信息后的总历史信息。
那么这个网络能干嘛呢?跟CNN或者全连接神经网络有什么不同呢?
例1:假设我们现在有一个时间序列的预测问题,以股票为例(不考虑环境政策等问题,仅从数据出发为理解RNN),我们想从过去的数据中估计下一天股票会涨还是跌。
该问题可以用数学描述为,已知某股票过去 t t t天的交易量为 [ x 1 , x 2 , x 3 , x 4 , ⋯ , x t ] [x_1,x_2,x_3,x_4,\cdots ,x_t] [x1,x2,x3,x4,⋯,xt],及对应的涨跌情况为 [ y 1 , y 2 , y 3 , y 4 , ⋯ , y t ] [y_1,y_2,y_3,y_4,\cdots,y_t] [y1,y2,y3,y4,⋯,yt]那么已知第 t + 1 t+1 t+1天的股票交易量为 x t + 1 x_{t+1} xt+1则该天股票会涨还是跌?
假设你已经有前连接神经网络的知识,那你就会这样解决问题:模型输入为 t t t天的交易量,输出为 t t t天的涨跌情况。根据收集的数据训练一个全连接网络(代估计参数为 w w w),则 t + 1 t+1 t+1天的涨跌情况为 y ^ t + 1 = f ( w ∗ x t + 1 + b ) \hat{y}_{t+1}=f(w*x_{t+1}+b) y^t+1=f(w∗xt+1+b)。
在这个问题中我们利用 x t + 1 x_{t+1} xt+1估计 y t + 1 y_{t+1} yt+1。但是有一个问题就产生了,股票不仅仅跟当天的交易量相关它实际上与之前的交易量也相关,上面的估计实际上没有充分利用已知信息,那么怎么利用这些已知信息呢?重新整里我们的问题
当前信息 | 历史信息 | 目标 |
---|---|---|
x 1 x_1 x1 | - | y 1 y_1 y1 |
x 2 x_2 x2 | [ x 1 ] [x_1] [x1] | y 2 y_2 y2 |
x 3 x_3 x3 | [ x 1 , x 2 ] [x_1,x_2] [x1,x2] | y 3 y_3 y3 |
x 4 x_4 x4 | [ x 1 , x 2 , x 3 ] [x_1,x_2,x_3] [x1,x2,x3] | y 4 y_4 y4 |
⋯ \cdots ⋯ | ⋯ \cdots ⋯ | ⋯ \cdots ⋯ |
x t x_t xt | [ x 1 , x 2 , x 3 , ⋯ , x t − 1 ] [x_1,x_2,x_3,\cdots,x_{t-1}] [x1,x2,x3,⋯,xt−1] | y t y_t yt |
所以问题变成 t t t时刻的输入是 [ x 1 , x 2 , x 3 , ⋯ , x t − 1 , x t ] [x_1,x_2,x_3,\cdots,x_{t-1},x_t] [x1,x2,x3,⋯,xt−1,xt]输出是 y t y_t yt。那这么一个问题能不能用全连接解决答案是不行,因为 t t t时刻的输入是 [ x 1 , x 2 , x 3 , ⋯ , x t − 1 , x t ] [x_1,x_2,x_3,\cdots,x_{t-1},x_t] [x1,x2,x3,⋯,xt−1,xt]的长度在不停的变化,而我们之前学习的卷积也好全连接也好都是固定输入维度的,那怎么解决。
现在的问题是历史信息的维度在变化,如果我们能综合历史信息把它提取成一个长度不变的向量就好了,RNN本质上就是在做这样的事情。上述问题变成:
当前信息 | 历史信息 | RNN处理后的历史信息 | 目标 |
---|---|---|---|
x 1 x_1 x1 | - | h 0 h_0 h0 | y 1 y_1 y1 |
x 2 x_2 x2 | [ x 1 ] [x_1] [x1] | h 1 h_1 h1 | y 2 y_2 y2 |
x 3 x_3 x3 | [ x 1 , x 2 ] [x_1,x_2] [x1,x2] | h 2 h_2 h2 | y 3 y_3 y3 |
x 4 x_4 x4 | [ x 1 , x 2 , x 3 ] [x_1,x_2,x_3] [x1,x2,x3] | h 3 h_3 h3 | y 4 y_4 y4 |
⋯ \cdots ⋯ | ⋯ \cdots ⋯ | ⋯ \cdots ⋯ | ⋯ \cdots ⋯ |
x t x_t xt | [ x 1 , x 2 , x 3 , ⋯ , x t − 1 ] [x_1,x_2,x_3,\cdots,x_{t-1}] [x1,x2,x3,⋯,xt−1] | h t − 1 h_{t-1} ht−1 | y t y_t yt |
所以RNN适用与结果跟一序列变量有关且序列长度不等的情况。例如:一段句子是正面的还是负面的,一段语句是高兴还是悲伤,甚至与文字翻译都可以用到RNN。
用折叠的RNN模型可以表示为如下图所示的形式:
显而易见对于 t t t 时刻而言,网络的输入是 x t , h t − 1 x_t,h_{t-1} xt,ht−1;输出是 o t , h t o_t,h_{t} ot,ht。需要注意的是 h t − 1 h_{t-1} ht−1实际上结合了前 t − 1 t-1 t−1时刻的信息。
2 RNN参数学习
结合之前的内容,我们可以用以下数学表达式来表示RNN:
h t = f ( w 1 x t + w 2 h t − 1 + b ) h_t=f(w_1x_t+w_2h_{t-1}+b) ht=f(w1xt+w2ht−1+b)
o t = g ( w 3 h t ) o_t=g(w_3h_t) ot=g(w3ht)
误差: L t = l ( o t − y t ) L_t=l(o_t-y_t) Lt=l(ot−yt)其中 y t y_t yt是真实值。
待学习的参数即 w 1 , w 2 , w 3 w_1,w_2,w_3 w1,w2,w3
梯度: δ L δ w 1 \frac{\delta L}{\delta w_1} δw1δL、 δ L δ w 2 \frac{\delta L}{\delta w_2} δw2δL、 δ L δ w 3 \frac{\delta L}{\delta w_3} δw3δL
具体计算过程不再叙述,可以通过梯度下降优化参数。
唯一需要强调的是RNN 是在整个序列完成后更新参数。
3 pytorch实现
官方网址:https://docs.pytorch.org/docs/stable/generated/torch.nn.RNN.html#torch.nn.RNN
# 高效实现等效于双向=False的RNN前向传播
def forward(x, hx=None):# 如果输入格式是批次优先(batch_first=True),需要转置为序列优先# PyTorch内部处理使用序列优先格式 (seq_len, batch_size, features)if batch_first:x = x.transpose(0, 1) # 从(batch, seq, features)转为(seq, batch, features)# 获取输入张量的维度信息seq_len, batch_size, _ = x.size() # 序列长度, 批次大小, 输入特征维度# 如果没有提供初始隐藏状态,初始化为全零if hx is None:hx = torch.zeros(num_layers, batch_size, hidden_size) # 形状(层数, 批次大小, 隐藏大小)# 初始化隐藏状态变量h_t_minus_1 = hx # 上一时间步的隐藏状态 (t-1时刻)h_t = hx # 当前时间步的隐藏状态 (t时刻)# 存储每个时间步的输出output = []# 遍历序列中的每个时间步for t in range(seq_len):# 遍历每个RNN层for layer in range(num_layers):# 计算当前层在当前时间步的隐藏状态# 公式: h_t = tanh(W_ih * x_t + b_ih + W_hh * h_{t-1} + b_hh)h_t[layer] = torch.tanh(# 输入到隐藏的变换:x[t] @ weight_ih[layer].T + bias_ih[layer] # 当前时间步的输入# 隐藏到隐藏的变换:+ h_t_minus_1[layer] @ weight_hh[layer].T + bias_hh[layer] # 上一时间步的隐藏状态)# 只保存最后一层的输出作为该时间步的输出# 注意: 在多层RNN中,通常只使用最后一层的输出output.append(h_t[-1]) # 形状(batch_size, hidden_size)# 为下一个时间步准备隐藏状态h_t_minus_1 = h_t # 当前状态变为下一个时间步的"上一个状态"# 将所有时间步的输出堆叠成张量output = torch.stack(output) # 形状(seq_len, batch_size, hidden_size)# 如果原始输入是批次优先,将输出转回相同格式if batch_first:output = output.transpose(0, 1) # 从(seq, batch, hidden)转回(batch, seq, hidden)# 返回:# output: 所有时间步最后一层的输出 (根据batch_first决定形状)# h_t: 最后一个时间步所有层的隐藏状态 (num_layers, batch_size, hidden_size)return output, h_t
应用只需要4行
# 创建RNN模型,指定模型的输入维度是10,隐藏维度是20,层数为2
rnn = nn.RNN(10, 20, 2)
# 输入【序列长度,batch_size,输入维度】
input = torch.randn(5, 3, 10)
# 隐藏输入(实际上就是前面说的历史信息,初始时候全为0)【层数,batch_size,隐藏维度】
h0 = torch.randn(2, 3, 20)
# forward向前推理
output, hn = rnn(input, h0)