跳至主要內容

    LSTM 與 GRU:怎麼選擇記住和忘記?

    從 cell state、hidden state 到兩種 reset 次序

    sunny 桑尼· · 37 MIN READ · 290 次瀏覽

    LSTM 與 GRU:怎麼選擇記住和忘記?
    本文目錄

    上一篇把 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。

    圖 1|同一個輸入 X_t 和前一個 hidden state H_(t−1) 分別進入三個 gate。來源為 D2L 英文版 LSTM 章節原圖,Aston Zhang、Zachary C. Lipton、Mu Li、Alexander J. Smola, CC BY-SA 4.0。完整原 SVG 轉為 1400px 白底 PNG;保留全圖和 Copy/Concatenate/全連接層圖例。此圖是現代教材的 LSTM gate 示意,不是 1997 年原始論文圖。

    對一個 hidden 向量來看,i/f/o 每個位置都可以有不同值。模型不是只選一個全域的「記住」按鈕,而是可對向量各維分別縮放。不過,從圖上看到可調係數,不能推論訓練後每一維就對應一個可命名的記憶概念。

    從候選內容,到 cell state 的寫入與保留

    input gate 需要一份候選內容,才能決定寫入什麼。圖 2 在 gate 計算旁加上 tanh input node。候選內容可正可負,而且會由目前輸入與前一個 hidden state 共同決定。

    圖 2|這是上一圖的延伸:新增的 C~_t 是候選內容,尚未乘上 input gate,也不是更新後的 C_t。來源同為 D2L 英文版 LSTM 章節與四位作者,CC BY-SA 4.0;完整 SVG 轉為白底 1400px PNG,保留全圖與原圖例,沒有改動 gate 順序或標籤。

    接著把候選乘上 i_t,把舊狀態乘上 f_t,再逐元素相加:

    c_t = f_t ⊙ c_(t−1) + i_t ⊙ g_t
    

    這條更新式說明兩種來源如何混成新的 cell state:舊狀態的保留量,以及新候選的寫入量。若某一維的 f_t 很小,該維舊 c 對這一步直接更新的貢獻會下降;若 i_t 很小,該維候選的直接貢獻會下降。這些只是運算上的路徑描述,不代表 gate 學會了「忘掉某件事」或「記住某個詞」。

    圖 3|完整圖在前,手機圖接著放大原圖中 cell-state 更新與 gate/operator 路徑;放大區裁掉了圖外狀態標籤,需以完整圖核對輸入與輸出端點。來源同為 D2L 英文版 LSTM 章節與四位作者,CC BY-SA 4.0;白底 1400px 原 SVG 及原座標裁切,沒有添線、符號或數字,衍生圖同採 CC BY-SA 4.0。這一階段尚未接上 hidden-state 輸出公式。

    用原型的第一個向量維度走一遍,就能看見各項如何影響結果。輸入 x=[0.6,−0.4]、前一個 hidden state h=[0.3,−0.2]、前一個 cell state c=[2.4,−1.8];所有權重和偏置是手設值。這一步實際算出:

    第 0 維的量數值代表的運算
    f0.527472舊 c 的縮放係數
    i0.615384候選 g 的縮放係數
    g0.019997tanh 候選內容
    o0.532454轉成 h 的縮放係數
    f × c_old1.265934保留的舊 cell 成分
    i × g0.012306寫入的候選成分
    c_new1.278240兩種 cell 成分相加
    h_new0.455789o × 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 則是經過限制與縮放後的輸出。

    圖 4|手機版先呈現完整圖,再用原圖區域放大 gate 與 cell-to-hidden 路徑;跨區連線、兩種狀態端點及全圖例以完整圖為準。來源:D2L 英文版 LSTM 章節原圖,Aston Zhang、Zachary C. Lipton、Mu Li、Alexander J. Smola,CC BY-SA 4.0。原 SVG 轉為 1400px 白底 PNG,手機版保留原區域並揭露裁切,沒有重畫箭頭;改作同採 CC BY-SA 4.0。完整圖亦等比置於白底 1600×900 封面,未加標題或圖形。

    這也是解讀梯度時要小心的地方。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與候選值。

    圖 5|來源:D2L 英文版 GRU 章節原圖,Aston Zhang、Zachary C. Lipton、Mu Li、Alexander J. Smola,CC BY-SA 4.0。完整原 SVG 轉為 1400px 白底 PNG,保留完整 gate 連接和全圖例;本圖只交代 r、z 如何取得輸入,尚未形成新 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 被清零。

    圖 6|手機版先列完整圖,再放大 reset gate 乘法與候選支路;放大圖省略了外圍狀態端點及圖例,須以完整圖為上下文。來源為 D2L 英文版 GRU 章節原圖,四位作者與 CC BY-SA 4.0 同前;轉為 1400px 白底 PNG,手機細節取自原座標,不添線或公式,衍生圖同採 CC BY-SA 4.0。此圖明確是 D2L reset-before 候選,不代表所有框架採相同慣例。

    最後,update gate 按來源的公式混合舊狀態和候選值:

    h_t = z_t ⊙ h_(t−1) + (1 − z_t) ⊙ h~_t
    

    依這個公式的定義,z_t 越接近 1,越偏向舊 hidden state;越接近 0,越偏向候選值。 因為不同文章對 gate 名稱的慣例可能相反,要先看更新式,不要只憑「update」這個名稱猜係數方向。

    圖 7|完整原圖先呈現 reset、candidate 與更新路徑;手機圖其後放大同一張原圖中的 gate 和加權相加運算,不改變門的次序或連線。跨區上下文以完整圖為準。來源:D2L 英文版 GRU 章節原圖,Aston Zhang、Zachary C. Lipton、Mu Li、Alexander J. Smola,CC BY-SA 4.0。白底 1400px SVG 轉製與原區域裁切均有披露,改作同採 CC BY-SA 4.0。

    因此即使 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 權重怎麼打包。

    模型/來源本篇符號圖上或方程順序PyTorch 2.3.1 packed row 順序

    LSTM
    本篇符號
    i input、f forget、g 候選、o output
    圖上或方程順序
    圖 1–4 依 gate/cell 漸進顯示;圖 1 閘列為 f、i、o
    PyTorch 2.3.1 packed row 順序
    i / f / g / o
    GRU
    本篇符號
    r reset、z update、n 候選
    圖上或方程順序
    圖 5–7 依 reset、候選、blend 漸進顯示
    PyTorch 2.3.1 packed row 順序
    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:

    設定不含輸出 head 的參數數量

    D2L LSTM:4 組 affine、單 bias
    4(dh+h²+h)
    PyTorch LSTM:4 組 affine、兩 bias
    4(dh+h²+2h)
    D2L GRU:3 組 affine、單 bias
    3(dh+h²+h)
    PyTorch GRU:3 組 affine、兩 bias
    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?
    LSTM 有兩個狀態;GRU 只傳 h。這是介面與更新結構差異,不代表 LSTM 必然保留更多有用語意。
    現有模型權重或框架實作採哪個 gate 次序與 reset 慣例?
    核對版本文件、權重形狀、bias 數量和候選式。尤其不能將 D2L reset-before 方程當成 PyTorch reset-after。
    哪個模型更適合你的資料、延遲與資源限制?
    在相同資料切分、前處理、hidden 維度選擇與評估指標下實測;本文單步原型沒有提供速度、準確率或語料比較。

    1997 年的原始 LSTM 工作、今日教材中的三門 LSTM,和某個框架 API 是相關但不完全相同的對象。讀公式、看權重、檢查資料狀態流,才能知道正在比較的是哪一個版本。門能調節資訊路徑,並不保證資訊一定被學會、保留或在預測時用對。

    來源

    留言0

    登入後就能留言。

    登入
    sunny 桑尼
    sunny 桑尼

    AI 資料科學 · 產品經理 · 蒙奇 D 桑尼

    專注於 AI 工具應用與資料科學教育,致力於讓每個人都能用 AI 提升工作效率。曾任職於科技業,現為全職教育創作者。

    讀更多文章 →想自己動手試?領免費 Starter Kit →