長短期記憶(LSTM)
LSTM 中引入了3個門,即
- 輸入門(input gate)
- 遺忘門(forget gate)
- 輸出門(output gate)
- 以及與隐藏狀态形狀相同的記憶細胞(某些文獻把記憶細胞當成一種特殊的隐藏狀态),進而記錄額外的資訊。
輸入門、遺忘門和輸出門
與門控循環單元中的重置門和更新門一樣,如下圖所示
- 長短期記憶的門的輸入均為目前時間步輸入 X t \boldsymbol{X}_t Xt與上一時間步隐藏狀态 H t − 1 \boldsymbol{H}_{t-1} Ht−1
- 輸出由激活函數為sigmoid函數的全連接配接層計算得到。
- 這3個門元素的值域均為 [ 0 , 1 ] [0,1] [0,1]。
假設
- 隐藏單元個數為 h h h
- 給定時間步 t t t的小批量輸入 X t ∈ R n × d \boldsymbol{X}_t \in \mathbb{R}^{n \times d} Xt∈Rn×d(樣本數為 n n n,輸入個數為 d d d)
- 上一時間步隐藏狀态 H t − 1 ∈ R n × h \boldsymbol{H}_{t-1} \in \mathbb{R}^{n \times h} Ht−1∈Rn×h。
時間步 t t t的輸入門 I t ∈ R n × h \boldsymbol{I}_t \in \mathbb{R}^{n \times h} It∈Rn×h、遺忘門 F t ∈ R n × h \boldsymbol{F}_t \in \mathbb{R}^{n \times h} Ft∈Rn×h和輸出門 O t ∈ R n × h \boldsymbol{O}_t \in \mathbb{R}^{n \times h} Ot∈Rn×h分别計算如下:
I t = σ ( X t W x i + H t − 1 W h i + b i ) , F t = σ ( X t W x f + H t − 1 W h f + b f ) , O t = σ ( X t W x o + H t − 1 W h o + b o ) , \begin{aligned} \boldsymbol{I}_t &= \sigma(\boldsymbol{X}_t \boldsymbol{W}_{xi} + \boldsymbol{H}_{t-1} \boldsymbol{W}_{hi} + \boldsymbol{b}_i),\\ \boldsymbol{F}_t &= \sigma(\boldsymbol{X}_t \boldsymbol{W}_{xf} + \boldsymbol{H}_{t-1} \boldsymbol{W}_{hf} + \boldsymbol{b}_f),\\ \boldsymbol{O}_t &= \sigma(\boldsymbol{X}_t \boldsymbol{W}_{xo} + \boldsymbol{H}_{t-1} \boldsymbol{W}_{ho} + \boldsymbol{b}_o), \end{aligned} ItFtOt=σ(XtWxi+Ht−1Whi+bi),=σ(XtWxf+Ht−1Whf+bf),=σ(XtWxo+Ht−1Who+bo),
其中
- W x i , W x f , W x o ∈ R d × h \boldsymbol{W}_{xi}, \boldsymbol{W}_{xf}, \boldsymbol{W}_{xo} \in \mathbb{R}^{d \times h} Wxi,Wxf,Wxo∈Rd×h和 W h i , W h f , W h o ∈ R h × h \boldsymbol{W}_{hi}, \boldsymbol{W}_{hf}, \boldsymbol{W}_{ho} \in \mathbb{R}^{h \times h} Whi,Whf,Who∈Rh×h是權重參數
- b i , b f , b o ∈ R 1 × h \boldsymbol{b}_i, \boldsymbol{b}_f, \boldsymbol{b}_o \in \mathbb{R}^{1 \times h} bi,bf,bo∈R1×h是偏差參數。
候選記憶細胞
接下來,長短期記憶需要計算候選記憶細胞 C ~ t \tilde{\boldsymbol{C}}_t C~t。它的計算與上面介紹的3個門類似,但使用了值域在 [ − 1 , 1 ] [-1, 1] [−1,1]的tanh函數作為激活函數,如下圖所示。
具體來說,時間步 t t t的候選記憶細胞 C ~ t ∈ R n × h \tilde{\boldsymbol{C}}_t \in \mathbb{R}^{n \times h} C~t∈Rn×h的計算為
C ~ t = tanh ( X t W x c + H t − 1 W h c + b c ) , \tilde{\boldsymbol{C}}_t = \text{tanh}(\boldsymbol{X}_t \boldsymbol{W}_{xc} + \boldsymbol{H}_{t-1} \boldsymbol{W}_{hc} + \boldsymbol{b}_c), C~t=tanh(XtWxc+Ht−1Whc+bc),
其中 W x c ∈ R d × h \boldsymbol{W}_{xc} \in \mathbb{R}^{d \times h} Wxc∈Rd×h和 W h c ∈ R h × h \boldsymbol{W}_{hc} \in \mathbb{R}^{h \times h} Whc∈Rh×h是權重參數, b c ∈ R 1 × h \boldsymbol{b}_c \in \mathbb{R}^{1 \times h} bc∈R1×h是偏差參數。
記憶細胞
可以通過元素值域在 [ 0 , 1 ] [0, 1] [0,1]的輸入門、遺忘門和輸出門來控制隐藏狀态中資訊的流動,這一般也是通過使用按元素乘法(符号為 ⊙ \odot ⊙)來實作的。目前時間步記憶細胞 C t ∈ R n × h \boldsymbol{C}_t \in \mathbb{R}^{n \times h} Ct∈Rn×h的計算組合了上一時間步記憶細胞和目前時間步候選記憶細胞的資訊,并通過遺忘門和輸入門來控制資訊的流動:
C t = F t ⊙ C t − 1 + I t ⊙ C ~ t . \boldsymbol{C}_t = \boldsymbol{F}_t \odot \boldsymbol{C}_{t-1} + \boldsymbol{I}_t \odot \tilde{\boldsymbol{C}}_t. Ct=Ft⊙Ct−1+It⊙C~t.
如下圖所示
- 遺忘門控制上一時間步的記憶細胞 C t − 1 \boldsymbol{C}_{t-1} Ct−1中的資訊是否傳遞到目前時間步,而輸入門則控制目前時間步的輸入 X t \boldsymbol{X}_t Xt通過候選記憶細胞 C ~ t \tilde{\boldsymbol{C}}_t C~t如何流入目前時間步的記憶細胞。
- 如果遺忘門一直近似1且輸入門一直近似0,過去的記憶細胞将一直通過時間儲存并傳遞至目前時間步。
- 這個設計可以應對循環神經網絡中的梯度衰減問題,并更好地捕捉時間序列中時間步距離較大的依賴關系。
隐藏狀态
有了記憶細胞以後,接下來我們還可以通過輸出門來控制從記憶細胞到隐藏狀态 H t ∈ R n × h \boldsymbol{H}_t \in \mathbb{R}^{n \times h} Ht∈Rn×h的資訊的流動:
H t = O t ⊙ tanh ( C t ) . \boldsymbol{H}_t = \boldsymbol{O}_t \odot \text{tanh}(\boldsymbol{C}_t). Ht=Ot⊙tanh(Ct).
- 這裡的tanh函數確定隐藏狀态元素值在-1到1之間。
- 當輸出門近似1時,記憶細胞資訊将傳遞到隐藏狀态供輸出層使用
- 當輸出門近似0時,記憶細胞資訊隻自己保留。
下圖展示了長短期記憶中隐藏狀态的計算。
實作LSTM網絡
讀取資料集
import numpy as np
import torch
from torch import nn, optim
import torch.nn.functional as F
import sys
sys.path.append("..")
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
def load_data_jay_lyrics():
"""加載周傑倫歌詞資料集"""
with zipfile.ZipFile('../../data/jaychou_lyrics.txt.zip') as zin:
with zin.open('jaychou_lyrics.txt') as f:
corpus_chars = f.read().decode('utf-8')
corpus_chars = corpus_chars.replace('\n', ' ').replace('\r', ' ')
corpus_chars = corpus_chars[0:10000]
idx_to_char = list(set(corpus_chars))
char_to_idx = dict([(char, i) for i, char in enumerate(idx_to_char)])
vocab_size = len(char_to_idx)
corpus_indices = [char_to_idx[char] for char in corpus_chars]
return corpus_indices, char_to_idx, idx_to_char, vocab_size
(corpus_indices, char_to_idx, idx_to_char, vocab_size) = load_data_jay_lyrics()
初始化模型參數
num_inputs, num_hiddens, num_outputs = vocab_size, 256, vocab_size
print('will use', device)
def get_params():
def _one(shape):
ts = torch.tensor(np.random.normal(0, 0.01, size=shape), device=device, dtype=torch.float32)
return torch.nn.Parameter(ts, requires_grad=True)
def _three():
return (_one((num_inputs, num_hiddens)),
_one((num_hiddens, num_hiddens)),
torch.nn.Parameter(torch.zeros(num_hiddens, device=device, dtype=torch.float32), requires_grad=True))
W_xi, W_hi, b_i = _three() # 輸入門參數
W_xf, W_hf, b_f = _three() # 遺忘門參數
W_xo, W_ho, b_o = _three() # 輸出門參數
W_xc, W_hc, b_c = _three() # 候選記憶細胞參數
# 輸出層參數
W_hq = _one((num_hiddens, num_outputs))
b_q = torch.nn.Parameter(torch.zeros(num_outputs, device=device, dtype=torch.float32), requires_grad=True)
return nn.ParameterList([W_xi, W_hi, b_i, W_xf, W_hf, b_f, W_xo, W_ho, b_o, W_xc, W_hc, b_c, W_hq, b_q])
定義模型
def init_lstm_state(batch_size, num_hiddens, device):
return (torch.zeros((batch_size, num_hiddens), device=device),
torch.zeros((batch_size, num_hiddens), device=device))
下面根據長短期記憶的計算表達式定義模型
- 隻有隐藏狀态會傳遞到輸出層,而記憶細胞不參與輸出層的計算。
def lstm(inputs, state, params):
[W_xi, W_hi, b_i, W_xf, W_hf, b_f, W_xo, W_ho, b_o, W_xc, W_hc, b_c, W_hq, b_q] = params
(H, C) = state
outputs = []
for X in inputs:
I = torch.sigmoid(torch.matmul(X, W_xi) + torch.matmul(H, W_hi) + b_i)
F = torch.sigmoid(torch.matmul(X, W_xf) + torch.matmul(H, W_hf) + b_f)
O = torch.sigmoid(torch.matmul(X, W_xo) + torch.matmul(H, W_ho) + b_o)
C_tilda = torch.tanh(torch.matmul(X, W_xc) + torch.matmul(H, W_hc) + b_c)
C = F * C + I * C_tilda
H = O * C.tanh()
Y = torch.matmul(H, W_hq) + b_q
outputs.append(Y)
return outputs, (H, C)
訓練模型并創作歌詞
num_epochs, num_steps, batch_size, lr, clipping_theta = 160, 35, 32, 1e2, 1e-2
pred_period, pred_len, prefixes = 40, 50, ['分開', '不分開']
def train_and_predict_rnn(rnn, get_params, init_rnn_state, num_hiddens,
vocab_size, device, corpus_indices, idx_to_char,
char_to_idx, is_random_iter, num_epochs, num_steps,
lr, clipping_theta, batch_size, pred_period,
pred_len, prefixes):
if is_random_iter:
data_iter_fn = data_iter_random
else:
data_iter_fn = data_iter_consecutive
params = get_params()
loss = nn.CrossEntropyLoss()
for epoch in range(num_epochs):
if not is_random_iter: # 如使用相鄰采樣,在epoch開始時初始化隐藏狀态
state = init_rnn_state(batch_size, num_hiddens, device)
l_sum, n, start = 0.0, 0, time.time()
data_iter = data_iter_fn(corpus_indices, batch_size, num_steps, device)
for X, Y in data_iter:
if is_random_iter: # 如使用随機采樣,在每個小批量更新前初始化隐藏狀态
state = init_rnn_state(batch_size, num_hiddens, device)
else:
# 否則需要使用detach函數從計算圖分離隐藏狀态, 這是為了
# 使模型參數的梯度計算隻依賴一次疊代讀取的小批量序列(防止梯度計算開銷太大)
for s in state:
s.detach_()
inputs = to_onehot(X, vocab_size)
# outputs有num_steps個形狀為(batch_size, vocab_size)的矩陣
(outputs, state) = rnn(inputs, state, params)
# 拼接之後形狀為(num_steps * batch_size, vocab_size)
outputs = torch.cat(outputs, dim=0)
# Y的形狀是(batch_size, num_steps),轉置後再變成長度為
# batch * num_steps 的向量,這樣跟輸出的行一一對應
y = torch.transpose(Y, 0, 1).contiguous().view(-1)
# 使用交叉熵損失計算平均分類誤差
l = loss(outputs, y.long())
# 梯度清0
if params[0].grad is not None:
for param in params:
param.grad.data.zero_()
l.backward()
grad_clipping(params, clipping_theta, device) # 裁剪梯度
sgd(params, lr, 1) # 因為誤差已經取過均值,梯度不用再做平均
l_sum += l.item() * y.shape[0]
n += y.shape[0]
if (epoch + 1) % pred_period == 0:
print('epoch %d, perplexity %f, time %.2f sec' % (
epoch + 1, math.exp(l_sum / n), time.time() - start))
for prefix in prefixes:
print(' -', predict_rnn(prefix, pred_len, rnn, params, init_rnn_state,
num_hiddens, vocab_size, device, idx_to_char, char_to_idx))
train_and_predict_rnn(lstm, get_params, init_lstm_state, num_hiddens,
vocab_size, device, corpus_indices, idx_to_char,
char_to_idx, False, num_epochs, num_steps, lr,
clipping_theta, batch_size, pred_period, pred_len,
prefixes)