各类神经网络学习:(三)RNN 循环神经网络(中集),同步多对多结构的详细解释
上一篇 | 下一篇 |
---|---|
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^)=∑tEt(yt,yt^)=−∑tyt⋅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∂JW=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):
"""
单个时刻的模型前向传播过程
:param
x_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_t
s_t = np.tanh(np.dot(U, x_t) + np.dot(W, s_prev) + bs)
# 计算公式二, 即输出o_t
o_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):
"""
整体模型前向传播过程
:param
x: 输入序列(分词矩阵), 尺寸为[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_0
for t in range(T):
s_t, o_t, cache = rnn_LMing_moment_forwad(x[:, :, t], s_t, parameters) # 生成的 s_t 在下一个循环中就是 s_prev
s_total[:, :, t] = s_t
o_total[:, :, t] = o_t
caches.append(cache) # 将中间参数元组添加进列表中
return s_total, o_total, caches
# 单个时刻的反向传播, 其中 ds_t、do_t 需要提前计算传入
def rnn_LMing_moment_backward(ds_t, do_t, cache):
"""
对单个时刻进行反向传播
:param
ds_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):
"""
对单个时刻进行反向传播
:param
ds: 所有时刻损失对隐层输出结果的导数
do: 所有时刻损失对模型输出结果的导数
cache: 当前时刻的缓存参数
:return:
gradients: 梯度字典
"""
# 获取第一个时刻的数据, 参数, 输入输出值
(s1, s0, x_1, parameters) = caches[0]
# 获取时刻数以及 m 和 n 的值
n, _, T = ds.shape
m, _ = 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_t
dU += dUt
dW += dWt
dbs += dbst
dV += dVt
dbo += dbot
ds0 = ds_prev
gradients = {"dU": dU, "dW": dW, "dbs": dbs, "dV": dV, "dbo": dbo, "dx": dx, "ds0": ds0}
return gradients