上一篇把 RNN 展開成時間步,說明每步如何把目前輸入和前一個 hidden state 一起計算。這次接著問:如果有些資訊值得留下,另一些應該淡出,模型能不能把「保留多少」放進狀態更新裡?
LSTM 和 GRU 都用 sigmoid gate 產生介於 0 與 1 的連續係數,逐元素調整狀態路徑。這不是人手寫「遇到某個詞就記住」的 if/else,也不保證訓練後的 gate 會符合我們對重要資訊的直覺。讀懂結構能幫你檢查模型到底提供哪些路徑;模型是否適合某項任務,仍要用合適的資料和驗證方式比較。
先分清兩種狀態:c 留在 cell,h 傳出去
LSTM 有兩個不同的狀態。c_t 是 memory cell 的內部狀態,沿時間傳遞;h_t 是 hidden state,會交給下一時間步,也能送到其他網路層。它們可能有相同的向量長度,但扮演不同角色。
先用 row-batch 的 D2L 記號寫出四組中間量。x_t 是本步輸入,h_(t−1) 和 c_(t−1) 是前一步兩種狀態;⊙ 表示逐元素相乘:
i_t = sigmoid(x_t W_xi + h_(t−1) W_hi + b_i)
f_t = sigmoid(x_t W_xf + h_(t−1) W_hf + b_f)
g_t = tanh (x_t W_xg + h_(t−1) W_hg + b_g)
o_t = sigmoid(x_t W_xo + h_(t−1) W_ho + b_o)
c_t = f_t ⊙ c_(t−1) + i_t ⊙ g_t
h_t = o_t ⊙ tanh(c_t)
圖上的字母用大寫 I/F/C~/O,下文公式為了易讀寫小寫。g_t 是 input node 的候選內容,有些教材寫成 C~_t;它不是完整 cell state。i_t 控制候選內容有多少寫入,f_t 控制舊 cell state 有多少保留,o_t 控制更新後的 cell state 有多少轉成外部可見的 h_t。
sigmoid 的輸出在 0 與 1 之間,所以「接近 0/接近 1」可以當作壓低/放大的直覺;它不是二元開關,input gate 和 forget gate 也不必相加等於 1。tanh 候選內容在 −1 與 1 之間,但 c_t 是多步累積與寫入的結果,沒有同樣的固定界限。在沒有 projection 的標準 LSTM 中,h_t=o_t⊙tanh(c_t) 才因 tanh 和 output gate 而落在 −1 與 1 之間。
先看第一張圖。它只拆出三個 sigmoid gates 從哪裡取得資訊,還沒有算 c_t 或 h_t。
對一個 hidden 向量來看,i/f/o 每個位置都可以有不同值。模型不是只選一個全域的「記住」按鈕,而是可對向量各維分別縮放。不過,從圖上看到可調係數,不能推論訓練後每一維就對應一個可命名的記憶概念。
從候選內容,到 cell state 的寫入與保留
input gate 需要一份候選內容,才能決定寫入什麼。圖 2 在 gate 計算旁加上 tanh input node。候選內容可正可負,而且會由目前輸入與前一個 hidden state 共同決定。
接著把候選乘上 i_t,把舊狀態乘上 f_t,再逐元素相加:
c_t = f_t ⊙ c_(t−1) + i_t ⊙ g_t
這條更新式說明兩種來源如何混成新的 cell state:舊狀態的保留量,以及新候選的寫入量。若某一維的 f_t 很小,該維舊 c 對這一步直接更新的貢獻會下降;若 i_t 很小,該維候選的直接貢獻會下降。這些只是運算上的路徑描述,不代表 gate 學會了「忘掉某件事」或「記住某個詞」。
用原型的第一個向量維度走一遍,就能看見各項如何影響結果。輸入 x=[0.6,−0.4]、前一個 hidden state h=[0.3,−0.2]、前一個 cell state c=[2.4,−1.8];所有權重和偏置是手設值。這一步實際算出:
| 第 0 維的量 | 數值 | 代表的運算 |
|---|---|---|
f | 0.527472 | 舊 c 的縮放係數 |
i | 0.615384 | 候選 g 的縮放係數 |
g | 0.019997 | tanh 候選內容 |
o | 0.532454 | 轉成 h 的縮放係數 |
f × c_old | 1.265934 | 保留的舊 cell 成分 |
i × g | 0.012306 | 寫入的候選成分 |
c_new | 1.278240 | 兩種 cell 成分相加 |
h_new | 0.455789 | o × tanh(c_new) |
c_new 大於 1 並不違反 LSTM 定義;候選 g 有界,不會把累積的 cell state 限制在同一範圍。第二維的 c_new 是 −1.097460,對應 h_new −0.409785。這些數字只回答「手設這個單步 cell 如何計算」,不是語料上學到的 gate 行為或模型成效。
圖 4 再加上 tanh(c_t) 與 output gate,形成可對外傳遞的 h_t。cell state 與 hidden state 在這裡分流:c_t 繼續沿 cell 路徑走,h_t 則是經過限制與縮放後的輸出。
這也是解讀梯度時要小心的地方。LSTM 的 cell-state 路徑提供可調整的保留係數,因此有助於緩解長序列訓練中的梯度消失問題;它不保證整個網路的梯度永遠等於 1,也不保證所有資訊都留得住。即使只看直接的 cell-state 路徑,固定 forget gate 時,連乘係數是 f_1 × f_2 × …,不必然等於 1。真實總梯度還會包含 gates、candidate、hidden state、輸出層及多個時間步的其他依賴路徑。
歷史上也要分清版本。Hochreiter 與 Schmidhuber 的 1997 年原始 LSTM 論文提出了 LSTM memory cell,但其原始形式沒有現代這條 learned forget gate;本文的三門結構是 D2L 教材呈現的現代形式。不要把現代方程或本文圖說成 1997 原始圖的逐式複製。1997 原論文與D2L LSTM 章節(本篇圖檔固定版本列於文末)可分別對照。
GRU:少一個獨立 state,兩道 gate 怎麼合作?
GRU 沒有 LSTM 那個另外傳遞的 c_t,只傳 hidden state h_t。它用 reset gate r_t 控制舊 hidden state 對候選值的影響,再用 update gate z_t 混合舊 hidden state與候選值。
以下沿用 D2L 的符號定義。reset gate 和 update gate 都由目前輸入與前一個 hidden state 經 sigmoid 計算:
r_t = sigmoid(x_t W_xr + h_(t−1) W_hr + b_r)
z_t = sigmoid(x_t W_xz + h_(t−1) W_hz + b_z)
接著 D2L 先用 r_t 逐元素縮放舊 hidden state,再做候選值計算:
h~_t = tanh(x_t W_xh + (r_t ⊙ h_(t−1)) W_hh + b_h)
這個順序叫 reset-before:先 reset,再乘 recurrent matrix。若某一維 r_t 接近 0,舊狀態該維對候選分支的直接輸入會減少;但最終 h_t 還有一條舊狀態分支,因此 r_t=0 不等於整個 GRU state 被清零。
最後,update gate 按來源的公式混合舊狀態和候選值:
h_t = z_t ⊙ h_(t−1) + (1 − z_t) ⊙ h~_t
依這個公式的定義,z_t 越接近 1,越偏向舊 hidden state;越接近 0,越偏向候選值。 因為不同文章對 gate 名稱的慣例可能相反,要先看更新式,不要只憑「update」這個名稱猜係數方向。
因此即使 r_t 很小,z_t⊙h_(t−1) 仍可能保留舊值;reset gate 作用在候選分支,不是整體重設指令。反過來,r_t 接近 1 也不等於最終狀態照抄舊狀態,因為還有 z_t 控制兩項的混合。
reset-before 和 reset-after 為什麼會算出不同結果?
上面的 D2L 版本,在 row-batch 記號下可寫作:
h~ = tanh(x W_xh + (r ⊙ h) W_hh + b_h)
PyTorch 2.3.1 的 nn.GRUCell 使用 reset-after 慣例。以欄向量表示,同一候選可寫成:
n = tanh(W_in x + b_in + r ⊙ (W_hn h + b_hn))
這裡 b_in 是輸入候選偏置,b_hn 是 recurrent 候選偏置;reset 乘在 recurrent 投影和 recurrent bias 的和外面。相比之下,D2L 將舊 hidden 先乘 reset 再做 recurrent 投影,並把候選 bias 放在該項外。對一般矩陣而言,先逐維縮放再投影,通常不等於先投影再逐維縮放;兩個偏置的位置也不同。
以下是手設參數的兩維計算,目的是揭示差異,並非語料上的模型比較。兩種版本使用相同的輸入、hidden state、reset gate、update gate、矩陣與數值;只改候選支路的運算次序:
| 量 | reset-before(D2L章節) | reset-after(PyTorch 2.3.1) |
|---|---|---|
| 候選 preactivation | [0.726794, −0.086156] | [0.678780, −0.021276] |
候選 h~/n | [0.621100, −0.085943] | [0.590726, −0.021273] |
經相同 z 混合後的 h_new | [0.460550, −0.146673] | [0.445363, −0.116437] |
最終 hidden state 最大分量差約 0.030236。這不是浮點誤差,也不代表其中一個實作錯誤,而是兩個公式在相同參數標籤下定義了不同候選計算。D2L 的候選式只有一個 bias;為了放進 PyTorch 的兩個 bias,本次映射把兩個候選 bias 相加後放在 reset 之外。此對照說明機制,不宣稱兩個模型完整訓練後必定有某種效能差異。
如果把 D2L 的公式直接照抄成 nn.GRUCell,又期待逐值相同,便忽略了這個慣例差異。PyTorch 2.3.1 的 GRU API 文件明列 reset-after 的候選式與 gate 排列。框架或文章若只說「GRU」,要再看它的具體方程、權重安排和版本。
gate 次序、參數數量與 API 要怎麼對照?
數學上的字母、圖上從左到右的順序、框架儲存參數的順序,是三件不同的事。尤其圖 1 和圖 5 不能用來推斷 PyTorch 將 gate 權重怎麼打包。
iinput、fforget、g候選、ooutput- 圖 1–4 依 gate/cell 漸進顯示;圖 1 閘列為 f、i、o
i / f / g / o
rreset、zupdate、n候選- 圖 5–7 依 reset、候選、blend 漸進顯示
r / z / n
g(有時寫 C~)是候選內容,不是第四種 sigmoid gate。PyTorch LSTM 的 g 有時也稱 candidate cell gate;比較時先對照公式,再看名字。D2L 用 row-batch 表示法,權重放在向量右側,例如 X W_xi;本文的程式將每個權重矩陣以輸出 feature 為列,用欄向量左乘。轉置後兩者等價,不能把一邊的矩陣形狀與另一邊的乘法次序直接拼在同一個式子裡。
再看參數數量。設輸入維度 d、hidden 維度 h,先不算輸出分類/預測 head;D2L scratch 式子每個 gate 用一組合併後的 bias,而 PyTorch 2.3.1 的 LSTM/GRU 各為 input 與 recurrent 部分保存 bias:
4(dh+h²+h)4(dh+h²+2h)3(dh+h²+h)3(dh+h²+2h)當 d=h=2,D2L/PyTorch 的 LSTM 分別為 40/48 個純量,GRU 則為 30/36 個純量。
例如 PyTorch LSTMCell(2,2) 有兩個 (8,2) 權重矩陣和兩個長度 8 的 bias:16+16+8+8=48。GRUCell(2,2) 則是兩個 (6,2) 矩陣加兩個長度 6 的 bias:12+12+6+6=36。上述數量不含 batch、序列長度或輸出 head;也不是運算時間的直接換算。門數較少會改變 affine 參數規模,但實際速度、記憶體和準確度仍受框架實作、裝置、batch、序列長度與任務影響,不能單憑公式宣布 GRU 一定更快或更準。
還要注意歷史界線:1997 年原始 LSTM 論文提出的 cell 與此處現代含 forget gate 的 D2L/PyTorch 形式並不完全相同;forget gate 不是可直接塞回 1997 原式的標籤。若要寫某個 gate 是哪一年由誰提出,需查對應的一手來源,本文不替未追查的年份背書。
用手設數值檢查機制,不把它說成訓練成果
下面的完整程式使用 torch.nn.LSTMCell 和 torch.nn.GRUCell,手設兩維輸入、狀態、權重及兩個 bias,逐項算出 gate、候選、cell 和 hidden。LSTM 的純 Python 計算會與 PyTorch cell 對照;GRU 會把 reset-before 及 PyTorch reset-after 兩種候選都算出,並跟 nn.GRUCell 對照。另有兩個小檢查:人工固定 f 的直接 cell 路徑,以及人工替換 z 的混合係數。
在已存在的環境中執行:
python3 docs/content/series/data-science-foundations/articles/ds43-lstm-gru-gates/practice/gated_cells.py
這段程式已在 Python 3.10.12、PyTorch 2.3.1+cu118 的既有環境執行,所有張量明確放在 CPU、使用 float64。它不下載資料、不訓練模型,也沒有做 GPU、速度、語料或準確率測試。requirements.txt 記錄這個已存在的 runtime;本次沒有在新 virtualenv 安裝 PyTorch 或驗證安裝流程。本機專案同資料夾的 stdout.json 和 verification.md 保留完整輸出及執行範圍;公開頁下方列出完整可複製的程式和主要數值。
原型完整內容如下;要重跑時可直接複製此區塊存成 gated_cells.py,或在本機專案執行上方路徑的檔案:
"""Hand-set CPU/float64 LSTM/GRU mechanisms; no training/benchmark.
Plain Python uses column-vector equations (matrix rows = output features).
PyTorch parameters stack gate rows: LSTM i,f,g,o; GRU r,z,n.
"""
import json
import math
import platform
import torch
def mv(matrix, vector):
return [sum(a*b for a, b in zip(row, vector)) for row in matrix]
def sigmoid(v):
return 1 / (1 + math.exp(-v))
def affine(x, h, p):
wx, wh = mv(p['W_i'], x), mv(p['W_h'], h)
return [wx[j] + p['b_i'][j] + wh[j] + p['b_h'][j] for j in range(2)]
def tensor(value):
return torch.tensor(value, dtype=torch.float64, device='cpu')
def load_cell(cell, gates, order):
with torch.no_grad():
for name, key in [('weight_ih', 'W_i'), ('weight_hh', 'W_h'),
('bias_ih', 'b_i'), ('bias_hh', 'b_h')]:
# Each matrix contributes2 rows; each bias contributes2 entries.
packed = [row for g in order for row in gates[g][key]]
getattr(cell, name).copy_(tensor(packed))
def main():
torch.manual_seed(43) # All randomized initialization is overwritten.
x, h, c = [0.6, -0.4], [0.3, -0.2], [2.4, -1.8]
lstm_params = {
'i': {'W_i': [[.4, -.2], [.1, .3]], 'W_h': [[.5, .15], [-.25, .4]],
'b_i': [.05, -.1], 'b_h': [-.02, .03]},
'f': {'W_i': [[-.3, .2], [.25, -.1]], 'W_h': [[.2, -.35], [.15, .3]],
'b_i': [.2, .1], 'b_h': [.04, -.06]},
'g': {'W_i': [[.2, .5], [-.4, .3]], 'W_h': [[.3, -.2], [.4, .1]],
'b_i': [-.1, .05], 'b_h': [.07, -.03]},
'o': {'W_i': [[.1, -.3], [.2, .4]], 'W_h': [[-.2, .25], [.3, -.15]],
'b_i': [.1, -.05], 'b_h': [-.04, .02]}}
pre = {name: affine(x, h, p) for name, p in lstm_params.items()}
gates = {name: [(math.tanh(v) if name == 'g' else sigmoid(v)) for v in values]
for name, values in pre.items()}
retained = [gates['f'][j] * c[j] for j in range(2)]
written = [gates['i'][j] * gates['g'][j] for j in range(2)]
c_new = [retained[j] + written[j] for j in range(2)]
h_new = [gates['o'][j] * math.tanh(c_new[j]) for j in range(2)]
cell = torch.nn.LSTMCell(2, 2, bias=True, device='cpu', dtype=torch.float64)
load_cell(cell, lstm_params, ['i', 'f', 'g', 'o'])
with torch.no_grad():
torch_h, torch_c = cell(tensor([x]), (tensor([h]), tensor([c])))
torch.testing.assert_close(torch_h, tensor([h_new]), rtol=0, atol=1e-14)
torch.testing.assert_close(torch_c, tensor([c_new]), rtol=0, atol=1e-14)
assert any(abs(v) > 1 for v in c_new) # C is not restricted to (-1,1).
assert all(abs(v) < 1 for v in h_new)
assert all(0 < v < 1 for name in ['i', 'f', 'o'] for v in gates[name])
assert all(abs(v) < 1 for v in gates['g'])
# Isolated direct C carry: fixed artificial f, no writes, gates independent of C.
# This is deliberately NOT the total derivative of a full recurrent LSTM.
decay = []
for f in [.9, .2]:
values = [1.0]
for _ in range(10):
values.append(f * values[-1])
c0 = tensor(1.0).requires_grad_()
propagated = c0
for _ in range(10):
propagated = f * propagated
derivative = torch.autograd.grad(propagated, c0)[0].item()
assert math.isclose(derivative, f**10, rel_tol=1e-14, abs_tol=1e-16)
decay.append({'fixed_f': f, 'c_t_from_t0_to_t10': values,
'direct_path_dc10_dc0': derivative, 'closed_form_f_power_10': f**10})
gru_params = {
'r': {'W_i': [[.4, -.3], [-.2, .5]], 'W_h': [[.2, .35], [-.4, .1]],
'b_i': [.1, -.15], 'b_h': [.05, -.02]},
'z': {'W_i': [[-.1, .2], [.3, .15]], 'W_h': [[.4, -.2], [.1, .3]],
'b_i': [-.05, .08], 'b_h': [.03, -.04]},
'n': {'W_i': [[.25, -.4], [.3, .2]], 'W_h': [[.6, -.5], [.4, .3]],
'b_i': [.07, -.09], 'b_h': [.2, -.15]}}
r = [sigmoid(v) for v in affine(x, h, gru_params['r'])]
z = [sigmoid(v) for v in affine(x, h, gru_params['z'])]
n_params = gru_params['n']
input_term = [a+b for a, b in zip(mv(n_params['W_i'], x), n_params['b_i'])]
h_linear = mv(n_params['W_h'], h)
reset_h_linear = mv(n_params['W_h'], [r[j]*h[j] for j in range(2)])
# D2L reset-before: combine biases into b_n=b_in+b_hn, outside reset.
before_a = [input_term[j] + reset_h_linear[j] + n_params['b_h'][j] for j in range(2)]
# PyTorch reset-after: r multiplies BOTH recurrent projection and its bias.
after_a = [input_term[j] + r[j]*(h_linear[j]+n_params['b_h'][j]) for j in range(2)]
n_before, n_after = [math.tanh(v) for v in before_a], [math.tanh(v) for v in after_a]
h_before = [(1-z[j])*n_before[j] + z[j]*h[j] for j in range(2)]
h_after = [(1-z[j])*n_after[j] + z[j]*h[j] for j in range(2)]
gru = torch.nn.GRUCell(2, 2, bias=True, device='cpu', dtype=torch.float64)
load_cell(gru, gru_params, ['r', 'z', 'n'])
with torch.no_grad():
builtin_gru = gru(tensor([x]), tensor([h]))
torch.testing.assert_close(builtin_gru, tensor([h_after]), rtol=0, atol=1e-14)
assert abs(r[0]-r[1]) > .05
assert all(n_params['W_h'][i][1-i] != 0 for i in range(2))
assert all(v != 0 for v in n_params['b_h'])
assert max(abs(a-b) for a, b in zip(n_before, n_after)) > .01
assert max(abs(a-b) for a, b in zip(h_before, h_after)) > .01
# Separate noncommuting matrix/reset term from recurrent-bias placement term.
matrix_difference = [r[j]*h_linear[j]-reset_h_linear[j] for j in range(2)]
bias_difference = [(r[j]-1)*n_params['b_h'][j] for j in range(2)]
for j in range(2):
assert math.isclose(after_a[j]-before_a[j], matrix_difference[j]+bias_difference[j], abs_tol=1e-14)
mix = []
for forced_z in [.99, .01]:
mixed = [(1-forced_z)*n_after[j]+forced_z*h[j] for j in range(2)]
mix.append({'artificial_fixed_z': forced_z, 'mixed_hidden': mixed,
'max_distance_to_old_hidden': max(abs(mixed[j]-h[j]) for j in range(2)),
'max_distance_to_candidate': max(abs(mixed[j]-n_after[j]) for j in range(2))})
assert mix[0]['max_distance_to_old_hidden'] < mix[1]['max_distance_to_old_hidden']
result = {
'scope': 'Hand-set mechanics only, CPU float64, no text corpus/training/benchmark or guaranteed memory quality.',
'environment': {'python': platform.python_version(), 'torch': torch.__version__, 'device': str(torch_h.device), 'dtype': str(torch_h.dtype)},
'inputs': {'x': x, 'previous_hidden': h, 'previous_cell_lstm': c, 'batched_shapes': {'x':[1,2], 'h':[1,2], 'c':[1,2]}},
'lstm': {'gate_stack_order': ['i','f','g','o'], 'parameters_column_orientation': lstm_params,
'preactivations': pre, 'gate_values': gates, 'retained_f_times_c': retained,
'written_i_times_g': written, 'manual_c_new': c_new, 'manual_h_new': h_new,
'builtin_c_new': torch_c.squeeze(0).tolist(), 'builtin_h_new': torch_h.squeeze(0).tolist(),
'max_abs_cell_error': (torch_c-tensor([c_new])).abs().max().item(),
'max_abs_hidden_error': (torch_h-tensor([h_new])).abs().max().item(),
'range_note': 'i/f/o in(0,1),g/h in(-1,1); C itself is not bounded to(-1,1).'},
'fixed_gate_direct_cell_path': {'scope': 'Artificial C_t=f*C_prev with writes zero and fixed gate independent of states. Direct-path derivative only; NOT total full-LSTM gradient or task quality.', 'series': decay},
'gru': {'gate_stack_order': ['r','z','n'], 'parameters_column_orientation': gru_params, 'reset_r': r, 'update_z': z,
'input_candidate_affine': input_term, 'recurrent_h_projection_without_bias': h_linear,
'reset_before_recurrent_projection': reset_h_linear,
'd2l_equivalent_single_candidate_bias': [n_params['b_i'][j]+n_params['b_h'][j] for j in range(2)],
'reset_before_candidate_preactivation': before_a, 'reset_after_candidate_preactivation': after_a,
'reset_before_candidate': n_before, 'reset_after_candidate': n_after,
'reset_before_hidden': h_before, 'manual_pytorch_hidden': h_after, 'builtin_pytorch_hidden': builtin_gru.squeeze(0).tolist(),
'max_abs_builtin_manual_error': (builtin_gru-tensor([h_after])).abs().max().item(),
'max_abs_variant_hidden_difference': max(abs(a-b) for a,b in zip(h_before,h_after)),
'preactivation_difference_matrix_noncommutation': matrix_difference,
'preactivation_difference_bias_placement': bias_difference,
'variant_note': 'Reset-before uses W_hn(r*h)+b_hn; PyTorch uses r*(W_hn*h+b_hn). Same r/z/weights/input; differences are matrix order AND bias placement, not precision error.'},
'fixed_z_interpolation': {'scope':'Artificial z substitution with same fixed old hidden/candidate; not a trained gate or probability of remembering.', 'cases':mix},
'checks': 'PASS: Python LSTM vs nn.LSTMCell; ranges; isolated fixed-f path autograd; GRU variants differ; Python reset-after vs nn.GRUCell; difference decomposition; z-near1 preserves old hidden.'}
print(json.dumps(result, ensure_ascii=False, indent=2, allow_nan=False))
if __name__ == '__main__':
main()
這個手設步驟算出 z=[0.500000, 0.532454]、r=[0.622459, 0.347511];它比較兩種方程,並沒有訓練 gate。程式也把矩陣次序和 recurrent bias 的差異分開輸出。最後幾個 z 替換值只是代數上的混合示例,不能解讀成模型對舊資訊的「記憶機率」。
應用上怎麼選?
先看你需要的狀態結構和實作限制,再用同一評估流程決定,而不是看模型名字猜輸贏:
c 與對外傳遞的 h?h。這是介面與更新結構差異,不代表 LSTM 必然保留更多有用語意。1997 年的原始 LSTM 工作、今日教材中的三門 LSTM,和某個框架 API 是相關但不完全相同的對象。讀公式、看權重、檢查資料狀態流,才能知道正在比較的是哪一個版本。門能調節資訊路徑,並不保證資訊一定被學會、保留或在預測時用對。
來源
- D2L, Long Short-Term Memory 與 Gated Recurrent Units,英文版固定版本 23d7a5aeccee;七張圖均由該版本圖檔轉製。作者與授權逐圖列於圖說。
- Sepp Hochreiter and Jürgen Schmidhuber, “Long Short-Term Memory” (1997), Neural Computation 9(8): 1735–1780;原論文 PDF(Stanford 課程保存)。
- Kyunghyun Cho et al., “Learning Phrase Representations using RNN Encoder–Decoder for Statistical Machine Translation” (2014). 用於核對 GRU reset/update 候選與混合方程;本文不引用其翻譯 benchmark 結果。
- PyTorch 2.3.1,
torch.nn.LSTM、torch.nn.GRU、torch.nn.LSTMCell、torch.nn.GRUCell。API 說明限於此版本的 packed gate 順序、state 介面、bias 參數與 reset-after 方程。 - 本篇的 CPU mechanics 原型已完整列於本文;本機
practice/gated_cells.py、stdout.json和verification.md保留程式、實際輸出及範圍紀錄。它以人工兩維資料核算,不含語料訓練或模型效能基準。
留言0
登入後就能留言。
登入