GRU說白了就是加了兩個門,這兩個門控制最終隱藏狀態的輸出,其中還是那一套換湯不換葯。
R是重置門,決定上一個時間步\(h_{t-1}\)是否要被重置,如果R元素全為0,很顯然我們就丟掉了上一個時間步的h信息。
S是更新門,決定了這個時刻的候選隱藏狀態\(h_{t}^{\prime}\)應該怎么輸出。
注意,因為這是兩個閥門,閥門控制肯定取值只有(0~1)
,所以這個的激活函數是sigmod函數。
公式:
候選隱藏狀態
值得注意的是,這里因為R和Z都是起到了閥門的作用,所有很顯然它是直接做哈達瑪乘積的,即對應元素相乘。
可以看到,通過重置門,我們得到了候選隱藏狀態,這個做的好處是可以減少一萬狀態的影響。
更新隱藏狀態
通過更新門實現了對隱藏狀態的更新。
如果Z接近1,那么\(h_{t-1}\)就會被保留,而如果整個子序列的所有時間步的更新門,也就是 Z 都接近1,那么我們可以保留從序列起始時間步開始的所有隱藏狀態。
重置門有利於捕獲序列中的短期依賴關系。
更新門有助於補貨序列中的長期依賴關系。
從零開始實現
import torch
import torch.nn as nn
import torch.nn.functional as F
import math
class GRU(nn.Module):
def __init__(self,indim, hidim, outdim):
super(GRU, self).__init__()
self.indim = indim
self.hidim = hidim
self.outdim = outdim
self.W_zh, self.W_zx, self.b_z = self.get_three_parameters()
self.W_rh, self.W_rx, self.b_r = self.get_three_parameters()
self.W_hh, self.W_hx, self.b_h = self.get_three_parameters()
self.Linear = nn.Linear(hidim, outdim) # 全連接層做輸出
self.reset()
def forward(self, input, state):
input = input.type(torch.float32)
if torch.cuda.is_available():
input = input.cuda()
Y = []
h = state
h = h.cuda()
for x in input:
z = F.sigmoid(h @ self.W_zh + x @ self.W_zx + self.b_z)
r = F.sigmoid(h @ self.W_rh + x @ self.W_rx + self.b_r)
ht = F.tanh((h * r) @ self.W_hh + x @ self.W_hx + self.b_h)
h = (1 - z) * h + z * ht
y = self.Linear(h)
Y.append(y)
return torch.cat(Y, dim=0), h
def get_three_parameters(self):
indim, hidim, outdim = self.indim, self.hidim, self.outdim
return nn.Parameter(torch.FloatTensor(hidim, hidim)), \
nn.Parameter(torch.FloatTensor(indim, hidim)), \
nn.Parameter(torch.FloatTensor(hidim))
def reset(self):
stdv = 1.0 / math.sqrt(self.hidim)
for param in self.parameters():
nn.init.uniform_(param, -stdv, stdv)
就是按公式原原本本寫了一遍,沒什么特點,就像搭積木一樣。
框架實現
import torch
import torch.nn as nn
import torch.nn.functional as F
import math
from RNN import *
setup_seed(916)
class GRU(nn.Module):
def __init__(self, indim, hidim, outdim):
super(GRU, self).__init__()
self.GRU = nn.GRU(indim, hidim)
self.Linear = nn.Linear(hidim, outdim)
def forward(self, input, state):
input = input.type(torch.float32)
h = state.unsqueeze(0)
if torch.cuda.is_available():
input = input.cuda()
h = h.cuda()
y, state = self.GRU(input, h)
output = self.Linear(y.reshape(-1, y.shape[-1]))
return output, state
這里有個值得注意的點,由於框架它實際上是可以定義多層RNN的,所以它輸入和輸出張量的維度不一樣。
gru(input, h_0)
輸入 input 就是(time_step, batch_size, feature_dim)
,在模型初始化nn.GRU()
傳入參數batch_first
,那么input的shape就是(batch_size, time_step,feature_dim)
,這一點需要當心。
對於h_0,它的shape簡單(D* num_layers, N, H_{hidim})
, 這里的D是看我們在初始化的時候是否設置了bidirectional
, 如果true,代表我們要用雙向的rnn,於是D就為2.不過大部分情況下我們都只用單向的rnn,於是一般來說它的shape就是(num_layers, N, H_{hidim})
,如果不顯式地給出h0,框架會自動用全0來構造這個h0,如果只是訓練的話,是沒必要自己初始化一個h0的,當然預測肯定要傳入h0。
對於這個輸出的結果,我們需要更加注意。
(output,h_n) = ouputs
其中output的shape為(L, N, D * H_{out})
,如果設置了batch_first = True, 就顛倒一下,一個重要的點:如果我們設置了雙向的RNN,那么我們最后是將兩個隱藏層結果concat起來了,所以,最后一維就是D * H_{out}
這是一個需要留心的點。
h_n也需要注意,他是最后一個時間步的每一層的隱藏狀態(D * num_layers, N, H_{out})
,如果我們設置層數為1,並且不使用雙向的rnn,那么輸出的結果就是(1, N, h_out)
這些維度挺繞的,所以一定要留心一點。