上一篇 | 下一篇 |
---|---|
RNN(上集) | RNN(下集) |
同步多對多結構
1)結構詳解
①圖解:
②參數含義:
- x t x_t xt? :表示每一個時刻的輸入;
- o t o_t ot? :表示每一個時刻的輸出;
- s t s_t st? :表示每一個隱藏層的狀態輸出;
- 右側小圓圈代表隱藏層的一個單元;
- U 、 V 、 W U、V、W U、V、W 參數共享,即所有的隱藏層都共用這三個參數。
③通用公式:
- s 0 = 0 s_0=0 s0?=0 (實際計算中是 0 0 0 列向量)。
- s t = g 1 ( U ? x t + W ? s t ? 1 + b s ) s_t=g1(U·x_t+W·s_{t-1}+b_s) st?=g1(U?xt?+W?st?1?+bs?) 。 g 1 ( ) g1() g1() 是激活函數, b s b_s bs? 是偏置。
- o t = g 2 ( V ? s t + b o ) o_t=g2(V·s_t+b_o) ot?=g2(V?st?+bo?) 。 g 2 ( ) g2() g2() 是激活函數, b o b_o bo? 是偏置。
通過將公式分解,可以發現: o t o_t ot? 的值和前面每個時刻的輸入都有關系(展開式形似 累乘 )。
④激活函數的選擇
總結:多分類使用 t a n h tanh tanh + s o f t m a x softmax softmax ;單分類使用 t a n h tanh tanh + s i g m o i d sigmoid sigmoid 。
-
激活函數 g 1 ( ) g1() g1() 一般選用 t a n h tanh tanh 。不用其他的函數的原因如下:
-
梯度消失問題(相比于 s i g m o i d sigmoid sigmoid ):
s i g m o i d sigmoid sigmoid 函數的導數范圍是 ( 0 , 0.25 ] (0,0.25] (0,0.25] , t a n h tanh tanh 函數的導數是 ( 0 , 1 ] (0,1] (0,1] 。由于 R N N RNN RNN 中會執行很多累乘,小于 1 1 1 的小數累乘會導致梯度越來越接近于 0 0 0 ,從而導致梯度消失現象。 t a n h tanh tanh 與 s i g m o i d sigmoid sigmoid 相比,梯度收斂速度更快并且相對不易出現梯度消失問題。
-
梯度爆炸問題(相比于 r e l u relu relu ):
雖然 r e l u relu relu 函數能有效緩解梯度消失,但是由于 r e l u relu relu 的導數不是 0 0 0 就是 1 1 1 ,恒為 1 1 1 的導數容易導致梯度爆炸,尤其是在會執行很多累乘的 R N N RNN RNN 中。
-
對稱問題:
t a n h tanh tanh 的輸出范圍為 [ ? 1 , 1 ] [?1,1] [?1,1] ,這使得它能夠將輸入值映射到一個對稱的區間,有助于梯度的傳播和模型的收斂。
-
-
激活函數 g 2 ( ) g2() g2() :
- 對于多分類問題,使用 s o f t m a x softmax softmax ;
- 對于單分類問題,使用 s i g m o i d sigmoid sigmoid 。
2)在同步多對多RNN網絡中,句子如何作為輸入
①首先將句子進行分詞
分詞就是將一個句子拆分成單詞或字符,例如 “我喜歡打籃球” ,會被分詞為 “我、喜歡、打、籃球” 。
-
對于英文,可用
NLTK
的word_tokenize
和sent_tokenize
等工具;對于中文,可用Jieba
等工具。 -
分詞后,通常會過濾掉語料庫中出現頻率較低的詞,以減少詞匯表的規模并提高模型訓練效率,低頻詞通常會被替換為
unknown_token
。 -
為了幫助模型識別句子的開始和結束,通常會在句首和句尾添加標識符,如開始符
sentence_start
和結束符sentence_end
。
②將分詞結果映射為向量
分詞后的單詞或字符需要被映射為向量表示。這通常通過構建一個詞匯表,將每個單詞或字符映射到一個唯一的索引,然后將這些索引轉換為向量。例如,使用 O n e ? H o t One-Hot One?Hot 編碼或嵌入層( E m b e d d i n g L a y e r Embedding Layer EmbeddingLayer)將單詞表示為向量( o n e ? h o t one-hot one?hot 編碼上網一搜便知,這里不做額外解釋)。
將詞典通過上述方法轉換之后,就會得到右邊的一個高維、稀疏的向量組( m m m 個詞組成的向量組為 m m m 行, m m m 列)(稀疏指的是絕大部分元素都是 0 0 0 )。
之前提到的開始和結束標志也會在此向量組里,比如用 [1,0,0,...]
表示開始符, [...,0,0,1]
表示結束符。
③將分詞后的結果按照時刻依次輸入模型
以“我喜歡打籃球”→“我、喜歡、打、籃球”為例
模型的輸出后續會給出圖解。
3)模型訓練過程中的矩陣運算是怎樣的
僅代表模型在 t t t 時刻的矩陣運算
回顧通用公式:
- s 0 = 0 s_0=0 s0?=0 (實際計算中是 0 0 0 列向量)。
- s t = g 1 ( U ? x t + W ? s t ? 1 ) s_t=g1(U·x_t+W·s_{t-1}) st?=g1(U?xt?+W?st?1?) 偏置 b s b_s bs? 先省略。
- o t = g 2 ( V ? s t ) o_t=g2(V·s_t) ot?=g2(V?st?) 偏置 b o b_o bo? 先省略。
先確定輸出向量的維度: o t o_t ot? 為 [ m , 1 ] [m,1] [m,1] (維度和 x t x_t xt? 一樣), s t s_t st? 為 [ n , 1 ] [n,1] [n,1] ( n n n 可自定義)。公式展開如下:
[ s t 1 : s t n ] = g 1 ( U ? [ x t 1 ┇ x t m ] + W ? [ s t ? 1 1 : s t ? 1 n ] ) ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? [ o t 1 ┇ o t m ] = g 2 ( V ? [ s t 1 : s t n ] ) \large\left[ \begin{matrix} s^1_t\\ :\\ s^n_t\\ \end{matrix} \right]=g1(U·\left[ \begin{matrix} x^1_t\\ ┇\\ x^m_t\\ \end{matrix} \right]+W·\left[ \begin{matrix} s^1_{t-1}\\ :\\ s^n_{t-1}\\ \end{matrix} \right])\\ -----------------------\\ \large\left[ \begin{matrix} o^1_t\\ ┇\\ o^m_t\\ \end{matrix} \right]=g2(V·\left[ \begin{matrix} s^1_t\\ :\\ s^n_t\\ \end{matrix} \right])\\ ?st1?:stn?? ?=g1(U? ?xt1?┇xtm?? ?+W? ?st?11?:st?1n?? ?)??????????????????????? ?ot1?┇otm?? ?=g2(V? ?st1?:stn?? ?)
注意其中的 n n n 和 m m m 。參數矩陣 U 、 W 、 V U、W、V U、W、V 可由此確定維度( U U U 為 [ n , m ] [n,m] [n,m] , W W W 為 [ n , n ] [n,n] [n,n] , V V V 為 [ m , n ] [m,n] [m,n] )。
不難推出參數 b s b_s bs? 為 [ n , 1 ] [n,1] [n,1] , b o bo bo 為 [ m , 1 ] [m,1] [m,1] 。
4)在同步多對多RNN網絡中,模型的輸出是什么
①以語言建模為例:輸出激活函數使用softmax,即多分類
語言建模:輸入一個句子并輸出句子中每個詞在下一個時刻最有可能的詞,例如,給定句子 “The cat is on the”,RNN 會預測下一個詞可能是 “mat”、“roof” 等,并給出每個詞的概率。這個過程可以逐詞進行,直到生成完整的句子或序列。
每一個時刻的輸出是一條概率向量,表示下一個最可能的詞。
概率向量構成:由預測概率組成的向量,長度為 N N N 。形如: [0.00001,...,0.018,...,0.00023,...]
。這些概率都由 s o f t m a x softmax softmax 函數計算得出。
例如下方圖解:
模型的完整輸入輸出為(輸入仍以“我、喜歡、打、籃球”為例):
(輸出結果可以是其他的,但是這里圖方便,就正好假設輸出也是“我、喜歡、打、籃球”。)
舉一個語言建模逐詞進行的例子:
以句子 “The cat is on the” 為例,RNN 會逐詞預測下一個詞,并給出每個詞的概率分布。以下是假設的逐詞生成過程:
- 輸入“The”:
RNN 會基于 “The” 預測下一個詞,可能的輸出概率分布為:- cat: 0.8
- dog: 0.1
- bird: 0.05
- …
- 輸入“The cat”:
RNN 會基于 “The cat” 預測下一個詞,可能的輸出概率分布為:- is: 0.7
- was: 0.2
- jumped: 0.05
- …
- 輸入“The cat is”:
RNN 會基于 “The cat is” 預測下一個詞,可能的輸出概率分布為:- on: 0.6
- under: 0.2
- sleeping: 0.1
- …
- 輸入“The cat is on”:
RNN 會基于 “The cat is on” 預測下一個詞,可能的輸出概率分布為:- the: 0.7
- a: 0.2
- my: 0.05
- …
- 輸入“The cat is on the”:
RNN 會基于 “The cat is on the” 預測下一個詞,可能的輸出概率分布為:- mat: 0.5
- roof: 0.3
- table: 0.1
- …
最終,RNN 可能會生成完整的句子,例如 “The cat is on the mat” 或 “The cat is on the roof” ,具體取決于概率分布和模型訓練數據。
②輸出激活函數使用sigmoid,即單分類
(這里就不展開了,隨便找 D e e p S e e k DeepSeek DeepSeek 寫了個例子):
假設我們有一個 R N N RNN RNN 模型用于預測某個事件是否會在每個時間步發生。模型的輸入是一個時間序列數據,輸出是一個與輸入序列長度相同的二進制序列
( 0 0 0 表示不會, 1 1 1 表示會)。
具體步驟:
- 輸入處理:輸入序列被逐個時間步輸入到 R N N RNN RNN 中。
- 隱藏狀態更新: R N N RNN RNN 在每個時間步更新其隱藏狀態,基于當前輸入和前一隱藏狀態。
- 輸出生成:在每個時間步, R N N RNN RNN 的輸出層使用 S i g m o i d Sigmoid Sigmoid 激活函數生成一個輸出值,表示當前時間步的事件發生概率(概率 ≥ 0.5 ≥0.5 ≥0.5 時判為 1 1 1,否則為 0 0 0 )。
- 序列輸出:最終,模型輸出一個與輸入序列長度相同的二進制序列,每個值表示對應時間步的事件是否會發生。
總結:在同步多對多結構的 R N N RNN RNN 中,使用 S i g m o i d Sigmoid Sigmoid 作為輸出層的激活函數,可以生成一個二進制序列(一開始是概率序列,經過閾值判別后變成二進制序列),適用于二分類問題或概率預測任務。
5)模型訓練中的損失
一般采用交叉熵損失函數。
一整個序列(句子)作為一個輸入樣本時,其損失為各個時刻詞的損失之和。
損失計算公式定義:
- 時刻 t t t 的損失: E t ( y t , y t ^ ) = ? y t ? l o g ( y t ^ ) \large E_t(y_t,\hat{y_t}) =-y_t·log(\hat{y_t}) Et?(yt?,yt?^?)=?yt??log(yt?^?) ;
- 各時刻損失之和: E ( y , y ^ ) = ∑ t E t ( y t , y t ^ ) = ? ∑ t y t ? l o g ( y t ^ ) \large E(y,\hat{y})=\sum_t E_t(y_t,\hat{y_t}) =-\sum_ty_t·log(\hat{y_t}) E(y,y^?)=∑t?Et?(yt?,yt?^?)=?∑t?yt??log(yt?^?) 。
這里和 C N N CNN CNN 的交叉熵損失函數有所不同,這里的 y t y_t yt? 代表時刻 t t t 上正確詞的向量, y t ^ \hat{y_t} yt?^? 代表預測詞的向量。
單個時刻的損失對模型輸出 o t o_t ot? 的導數為 ? J t ? o t = o t ? y t \large \frac{\partial J_t}{\partial o_t} = o_t - y_t ?ot??Jt??=ot??yt? 。
單個時刻的損失對隱層狀態輸出 s t s_t st? 的導數為 ? J t ? s t = V T ? ( o t ? y t ) \large \frac{\partial J_t}{\partial s_t} = V^T * (o_t - y_t) ?st??Jt??=VT?(ot??yt?) 。
6)時序反向傳播算法(BPTT)
BPTT,Back Propagation Through Time,對 RNN 來說,梯度是沿時間通道反向傳播的。
看看過程,理解一下就行,實際寫代碼不用親自寫,直接模塊化調用就行。
再次回顧前向傳播通用公式,并帶入激活函數:
- 公式一: s t = t a n h ( z t ) = t a n h ( U ? x t + W ? s t ? 1 + b s ) \large s_t=tanh(z_t)=tanh(U·x_t+W·s_{t-1}+b_s) st?=tanh(zt?)=tanh(U?xt?+W?st?1?+bs?)
- 公式二: o t = s o f t m a x ( V ? s t + b o ) \large o_t=softmax(V·s_t+b_o) ot?=softmax(V?st?+bo?)
令損失為 J J J 。
①梯度計算要求及規則:
-
目標 是:計算損失 J J J 關于參數 U 、 V 、 W 、 b s 、 b o U、V、W、b_s、b_o U、V、W、bs?、bo? 的梯度;
-
因為上述五個參數在每個時刻都是共享的,所以每個時刻的梯度都得計算出來,求出所有時刻梯度之和,才可用于參數更新;
圖解如下:
-
每個時刻的梯度計算 規則:
-
最后一個時刻:
- 第一步:依據交叉熵計算公式和公式二計算出: ? J t ? s t 、 ? J t ? V ( √ ) 、 ? J t ? b o ( √ ) \Large \frac{\partial J_t}{\partial s_t}、\frac{\partial J_t}{\partial V}(√)、\frac{\partial J_t}{\partial b_o}(√) ?st??Jt??、?V?Jt??(√)、?bo??Jt??(√) ;
- 第二步:依據公式一可以計算出: ? s t ? z t 、 ? s t ? U 、 ? s t ? W 、 ? s t ? b s 、 ? s t ? x t \Large \frac{\partial s_t}{\partial z_t}、\frac{\partial s_t}{\partial U}、\frac{\partial s_t}{\partial W}、\frac{\partial s_t}{\partial b_s}、\frac{\partial s_t}{\partial x_t} ?zt??st??、?U?st??、?W?st??、?bs??st??、?xt??st?? ;
- 第三步:依據 鏈式法則 ,繼續求出: ? J t ? U ( √ ) 、 ? J t ? W ( √ ) 、 ? J t ? b s ( √ ) \Large \frac{\partial J_t}{\partial U}(√)、\frac{\partial J_t}{\partial W}(√)、\frac{\partial J_t}{\partial b_s}(√) ?U?Jt??(√)、?W?Jt??(√)、?bs??Jt??(√) 。
-
非最后一個時刻:
-
第一步:依據交叉熵計算公式、公式二計算出: ? J t ? s t 、 ? J t ? V 、 ? J t ? b o \Large \frac{\partial J_t}{\partial s_t}、\frac{\partial J_t}{\partial V}、\frac{\partial J_t}{\partial b_o} ?st??Jt??、?V?Jt??、?bo??Jt?? ;
-
第二步( 不同點 ):在下一個時刻 t + 1 t+1 t+1 (反向傳播時,其實應稱為上一個時刻)時,要求出 ? s t + 1 ? s t \Large \frac{\partial s_{t+1}}{\partial s_t} ?st??st+1?? ,進一步求出 ? J t + 1 ? s t \Large \frac{\partial J_{t+1}}{\partial s_{t}} ?st??Jt+1?? ,則此刻的 ? J t ? s t \Large \frac{\partial J_t}{\partial s_t} ?st??Jt?? 應再加上一部分: ? J t ? s t ( ☆ ) = ? J t ? s t + ? J t + 1 ? s t \Large \frac{\partial J_t}{\partial s_t}(☆)=\frac{\partial J_t}{\partial s_t}+\frac{\partial J_{t+1}}{\partial s_{t}} ?st??Jt??(☆)=?st??Jt??+?st??Jt+1?? ;
即:當前時刻損失對于隱層狀態輸出值 s t s_t st? 的梯度要再加上下一時刻損失對 s t s_{t} st? 的導數。
-
第三步:照搬 “最后一個時刻” 的第二步;
-
第四步:照搬 “最后一個時刻” 的第三步。
-
-
單個時刻的反向傳播可以借鑒此視頻中的公式(引自《85.09_手寫RNN案例:單個cell的反向傳播_嗶哩嗶哩_bilibili》),內部公式推導不需要記住。
↑ ↑ ↑ 但是其中的 x x x (不是指 x t x^t xt )要改寫成 z t z_t zt? , ? x \partial x ?x (不是指 ? x t \partial x^t ?xt )要改寫成 ? z t \partial z_t ?zt? ,右側第六行的 ? W \partial W ?W 改成 ? b s \partial bs ?bs 。
補充(其中 y t y_t yt? 為預測真實值):
? J t ? o t = o t ? y t ? o t ? s t = V T ? J t ? s t = V T ? ( o t ? y t ) ? J t ? V = ∑ t = 1 T ( o t ? y t ) ? s t T ? J t ? b o = ∑ t = 1 T ( o t ? y t ) \begin{align*} \frac{\partial J_t}{\partial o_t} &= o_t - y_t\\ \frac{\partial o_t}{\partial s_t} &= V^T\\ \frac{\partial J_t}{\partial s_t} &= V^T * (o_t - y_t)\\ \frac{\partial J_t}{\partial V} &= \sum^T_{t=1} (o_t - y_t) * s_t^T\\ \frac{\partial J_t}{\partial b_o} &= \sum^T_{t=1} (o_t - y_t) \end{align*} ?ot??Jt???st??ot???st??Jt???V?Jt???bo??Jt???=ot??yt?=VT=VT?(ot??yt?)=t=1∑T?(ot??yt?)?stT?=t=1∑T?(ot??yt?)?
②參數更新
當每個時刻的參數梯度都計算出來,求和得出最終的梯度 ? J ? U 、 ? J ? W 、 ? J ? b s 、 ? J ? V 、 ? J ? b o \Large \frac{\partial J}{\partial U}、\frac{\partial J}{\partial W}、\frac{\partial J}{\partial b_s}、\frac{\partial J}{\partial V}、\frac{\partial J}{\partial b_o} ?U?J?、?W?J?、?bs??J?、?V?J?、?bo??J? 時,可按照下方公式進行更新:
U = U ? α ? ? J ? U W = W ? α ? ? J ? W ? ? ? b o = b o ? α ? ? J ? b o U=U-\alpha·\frac{\partial J}{\partial U}\\ W=W-\alpha·\frac{\partial J}{\partial W}\\ ···\\ b_o=b_o-\alpha·\frac{\partial J}{\partial b_o}\\ U=U?α??U?J?W=W?α??W?J????bo?=bo??α??bo??J?
其中 α \alpha α 為學習率。
7)相關理解性代碼
下面的代碼僅供參考,并且實際使用pytorch的時候,都是模塊化的,可直接調用的。
這里就僅僅給讀者看看,方便理解前向傳播和反向傳播的代碼邏輯。
import numpy as np# Language modeling: 語言建模(簡寫為 LMing ), 本質上是多分類任務# ***************************** 語言建模 *****************************# softmax自定義函數
def softmax(x):e_x = np.exp(x - np.max(x))return e_x / e_x.sum(axis=0)# 單個時刻的模型前向傳播過程
def rnn_LMing_moment_forwad(x_t, s_prev, parameters):"""單個時刻的模型前向傳播過程:paramx_t: 當前時刻的分詞輸入s_prev: 上一個時刻的隱層狀態輸出parameters: 字典形式的其他參數,包括 U、W、V、bs、bo:return:當前隱層狀態 s_t,輸出 o_t,緩存參數 cache 元組"""U = parameters['U']W = parameters['W']V = parameters['V']bs = parameters['bs']bo = parameters['bo']# 計算公式一, 即當前隱層狀態s_ts_t = np.tanh(np.dot(U, x_t) + np.dot(W, s_prev) + bs)# 計算公式二, 即輸出o_to_t = softmax(np.dot(V, s_t) + bo)# 記錄此時刻的一些參數(在反向傳播中會用到)cache = (s_t, s_prev, x_t, parameters)return s_t, o_t, cache# 整體模型前向傳播
def rnn_LMing_forwad(x, s_0, parameters):"""整體模型前向傳播過程:paramx: 輸入序列(分詞矩陣), 尺寸為[m,1,T], T為該序列中分詞個數s_0: 初始化隱層狀態輸入, 尺寸為[n,1]parameters: 字典形式的其他參數,包括 U、W、V、bs、bo:return:全部時刻的隱層狀態輸出 s_total,尺寸為[n,1,T];全部時刻的輸出 o_total,尺寸為[m,1,T];全部緩存參數元組, 并整合進一個列表 caches 中"""m, _, T = x.shape # 獲取分詞的向量形式長度、時刻數(分詞數)n, _ = parameters['U'].shape # 獲取單個時刻隱層狀態的向量形式長度# 初始化全部隱層狀態輸出矩陣、全部時刻輸出、s_total = np.zeros((n, 1, T))o_total = np.zeros((m, 1, T))caches = []s_t = s_0for t in range(T):s_t, o_t, cache = rnn_LMing_moment_forwad(x[:, :, t], s_t, parameters) # 生成的 s_t 在下一個循環中就是 s_prevs_total[:, :, t] = s_to_total[:, :, t] = o_tcaches.append(cache) # 將中間參數元組添加進列表中return s_total, o_total, caches# 單個時刻的反向傳播, 其中 ds_t、do_t 需要提前計算傳入
def rnn_LMing_moment_backward(ds_t, do_t, cache):"""對單個時刻進行反向傳播:paramds_t: 當前時刻損失對隱層輸出結果的導數do_t: 當前時刻損失對模型輸出結果的導數(do_t = o_t - y_t), y_t 為真實值cache: 當前時刻的緩存參數:return:gradients: 梯度字典"""# 獲取緩存(s_t, s_prev, x_t, parameters) = cache# 獲取參數U = parameters['U']W = parameters['W']V = parameters['V']bs = parameters['bs']bo = parameters['bo']dz_t = (1 - s_t ** 2) * ds_t # 計算 z_t 的梯度dx_t = np.dot(U.T, dz_t) # 計算 x_t 的梯度值dUt = np.dot(dz_t, x_t.T) # 計算 U 的梯度值ds_prev = np.dot(W.T, dz_t) # 計算 s_prev 的梯度值, s_prev 就是 s_t_1, ds_prev的含義是當前損失對前一時刻隱層狀態輸出的導數dWt = np.dot(dz_t, s_prev.T) # 計算 W 的梯度值dbst = np.sum(dz_t, axis=1, keepdims=True) # 計算 bs 的梯度dVt = np.dot(do_t, s_t.T) # 計算 V 的梯度值dbot = np.sum(do_t, axis=1, keepdims=True) # 計算 bo 的梯度值# 將所求梯度存進字典gradient = {"dz_t": dz_t, "dx_t": dx_t, "ds_prev": ds_prev, "dUt": dUt, "dWt": dWt, "dbst": dbst, "dVt": dVt,"dbot": dbot}return gradient# 整體反向傳播, 其中 ds 為所有 ds_t 的集合, do 為所有 do_t 的集合, 兩者需要提前計算傳入
def rnn_LMing_backward(ds, do, caches):"""對單個時刻進行反向傳播:paramds: 所有時刻損失對隱層輸出結果的導數do: 所有時刻損失對模型輸出結果的導數cache: 當前時刻的緩存參數:return:gradients: 梯度字典"""# 獲取第一個時刻的數據, 參數, 輸入輸出值(s1, s0, x_1, parameters) = caches[0]# 獲取時刻數以及 m 和 n 的值n, _, T = ds.shapem, _ = x_1.shape# 初始化梯度值dx = np.zeros((m, 1, T))dU = np.zeros((n, m))dW = np.zeros((n, n))dbs = np.zeros((n, 1))dV = np.zeros((m, n))dbo = np.zeros((m, 1))ds_prev = np.zeros((n, 1))# 循環從后往前進行反向傳播for t in reversed(range(T)):# 根據時間 T 的 s 梯度,以及緩存計算當前時刻的反向傳播梯度gradient = rnn_LMing_moment_backward((ds[:, :, t] + ds_prev), do[:, :, t], caches[t])# 獲取梯度準備進行更新dx_t, ds_prev, dUt, dWt, dbst, dVt, dbot = gradient["dx_t"], gradient["ds_prev"], gradient["dUt"], gradient["dWt"], gradient["dbst"], gradient["dVt"], gradient["dbot"]# 進行每次 t 時間上的梯度接過相加, 作為最終更新的梯度dx[:, :, t] = dx_tdU += dUtdW += dWtdbs += dbstdV += dVtdbo += dbotds0 = ds_prevgradients = {"dU": dU, "dW": dW, "dbs": dbs, "dV": dV, "dbo": dbo, "dx": dx, "ds0": ds0}return gradients