基于PaddlePaddle2.0实现门控循环单元GRU组件

PaddlePaddle 同时被 3 个专栏收录
35 篇文章 2 订阅
41 篇文章 1 订阅
46 篇文章 0 订阅

使用基础的Linear等组件拼接出GRU组件

参考资料:

一、门控循环单元GRU简介

GRU 论文:https://arxiv.org/pdf/1406.1078v3.pdf

门控循环单元(GRU)是一种简单的循环神经网络,其门控机制及结构如图所示:

图中 x t x_t xt表示输入数据, h t − 1 h_{t-1} ht1表示历史状态, r t r_t rt为重置门, h t ~ \widetilde{h_t} ht 表示候选状态, z t z_t zt为更新门, h t h_t ht表示当前状态。

GRU 有两个有两个门,即一个重置门(reset gate)和一个更新门(update gate)。从直观上来说,重置门决定了如何将新的输入信息与前面的记忆相结合,更新门定义了前面记忆保存到当前时间步的量。

1.重置门和更新门

门控循环单元中的重置门和更新门的输入均为当前时间步输入 X t \boldsymbol{X}_t Xt与上一时间步隐藏状态 H t − 1 \boldsymbol{H}_{t-1} Ht1,输出由激活函数为sigmoid函数的全连接层计算得到。

具体来说,假设隐藏单元个数为h,给定时间步t的小批量输入 X t ∈ R n × d \boldsymbol{X}_t \in \mathbb{R}^{n \times d} XtRn×d(样本数为n,输入个数为d)和上一时间步隐藏状态 H t − 1 ∈ R n × h \boldsymbol{H}_{t-1} \in \mathbb{R}^{n \times h} Ht1Rn×h。重置门 R t ∈ R n × h \boldsymbol{R}_t \in \mathbb{R}^{n \times h} RtRn×h和更新门 Z t ∈ R n × h \boldsymbol{Z}_t \in \mathbb{R}^{n \times h} ZtRn×h的计算如下:

r t = σ ( W r x t + U r h t − 1 + b r ) r_{t}=\sigma\left(W_{r} x_{t}+U_{r} h_{t-1}+b_{r}\right) rt=σ(Wrxt+Urht1+br)

z t = σ ( W z x t + U z h t − 1 + b z ) z_{t}=\sigma\left(W_{z} x_{t}+U_{z} h_{t-1}+b_{z}\right) zt=σ(Wzxt+Uzht1+bz)

  • W x r , W x z ∈ R d × h 和 W h r , W h z ∈ R h × h \boldsymbol{W}_{xr}, \boldsymbol{W}_{xz} \in \mathbb{R}^{d \times h}和\boldsymbol{W}_{hr}, \boldsymbol{W}_{hz} \in \mathbb{R}^{h \times h} Wxr,WxzRd×hWhr,WhzRh×h是权重参数;
  • b r , b z ∈ R 1 × h \boldsymbol{b}_r, \boldsymbol{b}_z \in \mathbb{R}^{1 \times h} br,bzR1×h是偏差参数。

sigmoid函数可以将元素的值变换到0和1之间。因此,重置门 R t \boldsymbol{R}_t Rt和更新门 Z t \boldsymbol{Z}_t Zt中每个元素的值域都是 [ 0 , 1 ] [0, 1] [0,1]

2.候选隐藏状态

接下来,门控循环单元将计算候选隐藏状态来辅助稍后的隐藏状态计算。如图6.5所示,我们将当前时间步重置门的输出与上一时间步隐藏状态做按元素乘法(符号为 ⊙ \odot )。如果重置门中元素值接近0,那么意味着重置对应隐藏状态元素为0,即丢弃上一时间步的隐藏状态。如果元素值接近1,那么表示保留上一时间步的隐藏状态。然后,将按元素乘法的结果与当前时间步的输入连结,再通过含激活函数tanh的全连接层计算出候选隐藏状态,其所有元素的值域为 [ − 1 , 1 ] [-1, 1] [1,1]

具体来说,时间步t的候选隐藏状态 H ~ t ∈ R n × h \tilde{\boldsymbol{H}}_t \in \mathbb{R}^{n \times h} H~tRn×h的计算为

H ~ t = tanh ( X t W x h + ( R t ⊙ H t − 1 ) W h h + b h ) \tilde{\boldsymbol{H}}_t = \text{tanh}(\boldsymbol{X}_t \boldsymbol{W}_{xh} + \left(\boldsymbol{R}_t \odot \boldsymbol{H}_{t-1}\right) \boldsymbol{W}_{hh} + \boldsymbol{b}_h) H~t=tanh(XtWxh+(RtHt1)Whh+bh)

  • W x h ∈ R d × h \boldsymbol{W}_{xh} \in \mathbb{R}^{d \times h} WxhRd×h W h h ∈ R h × h \boldsymbol{W}_{hh} \in \mathbb{R}^{h \times h} WhhRh×h是权重参数;
  • b h ∈ R 1 × h \boldsymbol{b}_h \in \mathbb{R}^{1 \times h} bhR1×h是偏差参数

从上面这个公式可以看出,重置门控制了上一时间步的隐藏状态如何流入当前时间步的候选隐藏状态。而上一时间步的隐藏状态可能包含了时间序列截至上一时间步的全部历史信息。因此,重置门可以用来丢弃与预测无关的历史信息。

3.隐藏状态

最后,时间步t的隐藏状态 H t ∈ R n × h \boldsymbol{H}_t \in \mathbb{R}^{n \times h} HtRn×h的计算使用当前时间步的更新门 Z t \boldsymbol{Z}_t Zt来对上一时间步的隐藏状态 H t − 1 \boldsymbol{H}_{t-1} Ht1和当前时间步的候选隐藏状态 H ~ t \tilde{\boldsymbol{H}}_t H~t做组合:

H t = Z t ⊙ H t − 1 + ( 1 − Z t ) ⊙ H ~ t \boldsymbol{H}_t = \boldsymbol{Z}_t \odot \boldsymbol{H}_{t-1} + (1 - \boldsymbol{Z}_t) \odot \tilde{\boldsymbol{H}}_t Ht=ZtHt1+(1Zt)H~t

值得注意的是,更新门可以控制隐藏状态应该如何被包含当前时间步信息的候选隐藏状态所更新,如上图所示。假设更新门在时间步t’到t(t’ < t)之间一直近似1。那么,在时间步t’到t之间的输入信息几乎没有流入时间步t的隐藏状态 H t \boldsymbol{H}_t Ht。实际上,这可以看作是较早时刻的隐藏状态 H t ′ − 1 \boldsymbol{H}_{t'-1} Ht1一直通过时间保存并传递至当前时间步t。这个设计可以应对循环神经网络中的梯度衰减问题,并更好地捕捉时间序列中时间步距离较大的依赖关系。

对门控循环单元的设计稍作总结:

  • 重置门有助于捕捉时间序列里短期的依赖关系;
  • 更新门有助于捕捉时间序列里长期的依赖关系。

二、门控循环单元GRU的由来

门控循环单元网络(GRU)根据输出序列和给定的初始状态计算返回输出序列和最终状态。在该网络中的每一层对应输入的step,每个step根据当前时刻输入 xt 和上一时刻状态 ht−1 计算当前时刻输出 yt 并更新状态 ht 。

总结上面的4个公式,可以得到门控循环单元的状态更新公式:

r t = σ ( W r x t + U r h t − 1 + b r ) r_{t}=\sigma\left(W_{r} x_{t}+U_{r} h_{t-1}+b_{r}\right) rt=σ(Wrxt+Urht1+br)

z t = σ ( W z x t + U z h t − 1 + b z ) z_{t}=\sigma\left(W_{z} x_{t}+U_{z} h_{t-1}+b_{z}\right) zt=σ(Wzxt+Uzht1+bz)

H ~ t = tanh ( X t W x h + ( R t ⊙ H t − 1 ) W h h + b h ) \tilde{\boldsymbol{H}}_t = \text{tanh}(\boldsymbol{X}_t \boldsymbol{W}_{xh} + \left(\boldsymbol{R}_t \odot \boldsymbol{H}_{t-1}\right) \boldsymbol{W}_{hh} + \boldsymbol{b}_h) H~t=tanh(XtWxh+(RtHt1)Whh+bh)

H t = Z t ⊙ H t − 1 + ( 1 − Z t ) ⊙ H ~ t \boldsymbol{H}_t = \boldsymbol{Z}_t \odot \boldsymbol{H}_{t-1} + (1 - \boldsymbol{Z}_t) \odot \tilde{\boldsymbol{H}}_t Ht=ZtHt1+(1Zt)H~t

为什么要引入门控循环单元GRU呢?门控是什么?请带着这个问题往下看。

1.传统的神经网络NN

比如图像分类的任务,一个输入只有一个输出,并且上一个输出结果并不会影响下一个输出结果。就像下面这张图:

但如果我的输入,其互相之间存在某种联系,就好像做饭的时候要先放油,不然可能会粘锅:

这时的问题就来了,很明显,传统的神经网络并不能帮我们解决数据的关联问题。那么,想想人类是怎么做的?

最简单的方式就是记住这些数据!心理学中也有这么一句话:“每个人都是过去经验的总和,你过去的经验造成了现在的你”。

神经网络也是能记住数据的,把第一次输出的结果给第二次输入就可以了,这时就变成了RNN

2.循环神经网络RNN

每次RNN运行结束后都会生成对当前状态的描述——State(t)

RNN在t时刻会产生S(t),在t+1时刻会产生S(t+1):

而Y(t+1)是由S(t)与S(t+1)共同决定的:

因此,普通的RNN是这个样子的:

不过RNN的结构其实是很灵活的,如果要用RNN做情感分类,那么我们只需要保留最后一个输出结果就可以了:

如果要用RNN做图片描述,又或者是给定关键词输出图片的任务,那么只需要一个输入即可:

除此之外,RNN在语言翻译上也能大显身手:

总的来说,RNN是在有序数据上进行学习的,但它就像一位老爷爷,有时候还挺健忘的:

对RNN的计算过程做一个可视化,可以画出下图:

图中左边是输入 x t x_t xt h t − 1 h_{t- 1} ht1、右边是输出 h t h_t ht。计算从左向右进行,整个运算包括三步:输入 X t X_t Xt h t − 1 h_{t- 1} ht1分别乘以 W x h W_{xh} Wxh W h n W_{hn} Whn、相加、经过tanh非线性变换。

我们可以认为 h t h_t ht储存了网络中的记忆(memory),RNN学习的目标是使得 h t h_t ht记录了在t时刻之前(含)的输入信息 X 1 , X 2 , . . . , X t X_1,X_2,...,X_t X1,X2,...,Xt。在新词Xt输入到网络之后,之前的隐状态向量 h t − 1 h_{t- 1} ht1就转换为和当前输入 X t X_t Xt有关的 h t h_t ht

RNN的学习过程

假设输入一句话:“今天要做红烧排骨,首先要准备排骨… … ”

我们需要让机器识别出今天要做的菜名,我们能很快的回答出:“红烧排骨”,但机器是怎么学到的呢?

输入的关键信息“红烧排骨”要经过长途跋涉,直到最后一个时间点才能输出,根据输出与期望值的误差做反向传播:

如果权重w小于1,那么误差会越来越小,此时就出现了梯度消失

而如果权重w大于1,误差便会越来越大,此时就出现了梯度爆炸

这就是普通RNN无法回忆久远记忆的原因,LSTM就是为了解决这个问题而产生的。

3.长短时记忆神经网络LSTM

LSTM在ENN的基础上多了3个门,分别是输入门、忘记门和输出门:

这里举一个玩游戏,或者是看电影的例子来解释LSTM的工作原理:

最左边的这条主线可以表示电影情节发展的大方向,假设剧终结果是男主和女主在一起了,那么,促成这一结果的分线剧情可能有很多,不相干的剧情可能也有不少,把能够促成剧终结果的分线剧情输出,而把不相干的分线剧情忘记。

将不重要的信息忘记,LSTM便可以记的更牢。

将LSTM可视化:

和RNN相同的是,网络接受两个输入,得到一个输出。其中使用了两个参数矩阵 W r c W_{rc} Wrc W h c W_{hc} Whc以及tanh激活函数。不同之处在于,LSTM中通过3个门]控单元 i t i_t it f t f_t ft 0 t 0_t 0t 来对的信息交互进行控制。当 i t = 1 i_t=1 it=1(开关闭合)、 f t = 0 f_t=0 ft=0(开关打开)、 O t = 1 O_t=1 Ot=1(开关闭台)时,LSTM退化为标准的RNN。

4.门控逻辑单元GRU

GRU是另一种十分主流的RNN衍生物。 RNN和LSTM都是在设计网络结构用于缓解梯度消失问题,只不过是网络结构有所不同。LSTM和GRU的关键是会选择性地忽略其中一些词,不让其参与到隐层状态向量的更新中,最后只保留相关的信息进行预测。

用电路图将GRU表述出来,左边是输入,右边是输出:

与LSTM相比,GRU将输入门 i t i_t it和遗忘门 f t f_t ft 融合成单一的更新门 Z t Z_t Zt,并且融合了细胞状态 C t C_t Ct和隐层单元 h t h_t ht。当 r t = 1 r_t=1 rt=1(开关闭合)、 z t = 0 z_t=0 zt=0(开关连通上面)GRU退化为标准的RNN。

根据这张图,我们再对GRU的各单元作用进行分析:

  • 重置门 r t r_t rt r t r_t rt用于控制前一时刻隐层单元 h t − 1 h_{t-1} ht1对当前词 X t X_t Xt的影响。如果 h t 1 h_{t_1} ht1 X t X_t Xt不重要,即从当前词 X t X_t Xt开始表述了新的意思,与上文无关。那么开关 r t r_t rt可以打开,使得 h t − 1 h_{t-1} ht1 X t X_t Xt不产生影响。
  • 更新门 z t z_t zt : t用于决定是否忽略当前词 X t X_t Xt。类似于LSTM中的输入门 i t i_t it Z t Z_t Zt可以判断当前词 X t X_t Xt对整体意思的表达是否重要。当t开关接通下面的支路时,我们将忽略当前词 x t x_t xt ,同时构成了从 h t − 1 h_{t- 1} ht1 h t h_t ht的短路连接,这使得梯度得已有效地反向传播。和LSTM相同,这种短路机制有效地缓解了梯度消失现象,这个机制于highway networks十分相似。

尽管RNN、LSTM、和GRU的网络结构差别很大,但是他们的基本计算单元是一致的,都是对 X t X_t Xt h t h_t ht做一个线性映射加tanh激活函数,见三个图的红色框部分。他们的区别在于如何设计额外的门控机制控制梯度信息传播用以缓解梯度消失现象。LSTM用 了3个门、GRU用了2个,那能不能再少呢? MGU (minimal gate unit)尝试对这个问题做出回答,它只有一个门控单元。

三、深入理解门控循环单元GRU的工作方式

递归神经网络(RNN)具有短期记忆。如果序列足够长,他们将很难将信息从较早的时间步骤传送到后面的步骤。因此,如果你正在尝试对一段文本进行预测,RNN可能会从一开始就遗漏掉重要信息。LSTM和GRU是作为短期记忆的解决方案而创建的。它们具有称为门的内部机制,可以调节信息流。

双曲正切tanh

激活函数tanh用于帮助调节流经网络的值。tanh函数将值限制与在-1和1之间。

S型函数Sigmoid

S形激活函数类似于tanh激活函数。但它不是在-1和1之间取值,而是在0和1之间取值。这有助于更新或忘记数据,因为任何数字乘以0都是0,这会导致值消失或被“遗忘”。任何数字乘以1是相同的值,因此值保持相同或“保持”。网络可以了解哪些数据不重要因此可能被遗忘或哪些数据很重要。

RNN的深入理解

让我们看看RNN的一个单元,看看它将如何计算隐藏状态。首先,将输入和先前隐藏状态组合以形成向量。该向量现在具有关于当前输入和先前输入的信息。向量经过tanh函数,输出是新的隐藏状态,或网络的内存。

tanh函数确保值保持在-1和1之间,从而调节神经网络的输出。如果没有tanh函数的帮助,某些值会爆炸并变得天文数字, 从而导致其他值看起来微不足道:

没有tanh函数的变换

使用tanh函数,你可以看到上面的相同值如何保持在tanh函数允许的边界之间:

矢量变换与tanh

这就是一个RNN,它内部的操作很少,但在适当的情况下工作得很好。RNN使用的计算资源比它的演化变体LSTM和GRU少得多。

LSTM的深入理解

LSTM具有与递归神经网络类似的控制流程,都是向前传播时处理传递信息的数据。有三个不同的门来调节LSTM单元中的信息流,分别是遗忘门、输入门和输出门。

遗忘门

遗忘门决定了应丢弃或保留哪些信息。来自先前隐藏状态的信息和来自当前输入的信息通过sigmoid函数传递。值介于0和1之间,越接近0意味着忘记,越接近1意味着要保持。

输入门

输入门要更新单元状态,首先,我们将先前的隐藏状态和当前输入传递给sigmoid函数。这决定了通过将值转换为0到1来更新哪些值:0表示不重要,1表示重要。接着你还要将隐藏状态和当前输入传递给tanh函数,以便在-1和1之间取值以帮助调节网络。然后将tanh输出与sigmoid输出相乘。sigmoid输出将决定哪些信息对于输出很重要。

细胞状态

现在我们应该有足够的信息来计算细胞状态。首先,细胞状态逐点乘以遗忘向量。如果它乘以接近0的值,则有可能在单元状态中丢弃。然后我们从输入门获取输出并进行逐点相加,将神经网络发现的新值更新为细胞状态中,这就给了我们新的细胞状态。

输出门

最后是输出门,输出门决定下一个隐藏状态应该是什么。请记住,隐藏状态包含有关先前输入的信息,隐藏状态也可用于预测。首先,我们将先前的隐藏状态和当前输入传递给sigmoid函数。然后我们将新修改的单元状态传递给tanh函数。我们将tanh输出与sigmoid输出相乘,以确定隐藏状态应携带的信息,输出的是隐藏状态。然后将新的细胞状态和新的隐藏状态转移到下一个时间步。

GRU的深入理解

GRU是作为一种使用称为门的机制来缓解短期记忆的痛点而创建的。门结构可以调节流经序列链的信息流。

GRU与LSTM非常相似。GRU摆脱了细胞状态并使用隐藏状态来传输信息。它也只有两个门,一个重置门和一个更新门。

重置门

重置门决定忘记过去的信息量。

更新门

更新门的作用类似于LSTM的遗忘门和输入门。它决定了要丢弃哪些信息以及要添加的新信息。

下面,使用的输入尺寸为3 (绿色) 和输出尺寸为2的隐含单元(红色),batch size为1,来模拟一下GRU是怎么工作的:

四、使用Paddle2.0完成GRU组网

Paddle2.0有两种组网方式:

  • Sequential 组网:针对顺序的线性网络结构我们可以直接使用Sequential来快速完成组网,可以减少类的定义等代码编写
  • SubClass 组网:针对一些比较复杂的网络结构,就可以使用Layer子类定义的方式来进行模型代码编写

飞桨框架2.0中,组网相关的API都在paddle.nn目录下,可以通过 Sequential 或 SubClass的方式构建具体的模型。组网相关的API源码位于https://github.com/PaddlePaddle/Paddle/tree/develop/python/paddle/nn

在正式组网前,先来看看基础代码。

组网用到的基础API

激活函数层在神经网络中是最常见的一个网络结构层,该网络层是用来加入非线性因素的,为什么要加入非线性因素呢?这是由于因为线性模型的表达能力不够,通过加入非线性激活函数以后,神经网络的表达能力可以变的更加强大。常用的激活函数包括Sigmoid函数、tanh函数以及Relu函数等。

paddle.nn.Tanh()

Tanh函数的数学表达式如下所示:
T a n h ( x ) = e x − e − x e x + e − x Tanh(x) = \frac{e^{x} - e^{-x}}{e^{x} + e^{-x}} Tanh(x)=ex+exexex

PaddlePaddle已将该函数封装,其源代码位于:https://github.com/PaddlePaddle/Paddle/blob/release/2.0-rc1/python/paddle/nn/layer/activation.py#L229

import paddle

x = paddle.to_tensor([-0.4, 0, 2])
m = paddle.nn.Tanh()
out = m(x)
print(out)
Tensor(shape=[3], dtype=float32, place=CUDAPlace(0), stop_gradient=True,
       [-0.37994897,  0.        ,  0.96402758])

paddle.nn.Sigmoid()

Sigmoid函数的数学表达式如下所示:

S i g m o i d ( x ) = 1 1 + e − x Sigmoid(x) = \frac{1}{1 + e^{-x}} Sigmoid(x)=1+ex1

PaddlePaddle已将该函数封装,其源代码位于:https://github.com/PaddlePaddle/Paddle/blob/release/2.0-rc1/python/paddle/nn/layer/activation.py#L533

import paddle

m = paddle.nn.Sigmoid()
x = paddle.to_tensor([-0.4, 0, 4.0])
out = m(x)
print(out)
Tensor(shape=[3], dtype=float32, place=CUDAPlace(0), stop_gradient=True,
       [0.40131235, 0.50000000, 0.98201376])

paddle.nn.Linear

Linear是线性变换层 。对于每个输入Tensor X ,其计算公式为:

O u t = X W + b Out = XW + b Out=XW+b

其中, W 和 b 分别为权重和偏置。

PaddlePaddle已将该函数封装,其源代码位于:https://github.com/PaddlePaddle/Paddle/blob/release/2.0-rc1/python/paddle/nn/layer/common.py#L44

import paddle

# Define the linear layer.
weight_attr = paddle.ParamAttr(
    name="weight",
    initializer=paddle.nn.initializer.Constant(value=0.5))
bias_attr = paddle.ParamAttr(
    name="bias",
    initializer=paddle.nn.initializer.Constant(value=1.0))
linear = paddle.nn.Linear(2, 4, weight_attr=weight_attr, bias_attr=bias_attr)
print("weight:{}".format(linear.weight))
print("bias:{}".format(linear.bias))

x = paddle.randn((3, 2), dtype="float32")
y = linear(x)
print("x:{}".format(x))
print("y:{}".format(y))
weight:Parameter containing:
Tensor(shape=[2, 4], dtype=float32, place=CUDAPlace(0), stop_gradient=False,
       [[0.50000000, 0.50000000, 0.50000000, 0.50000000],
        [0.50000000, 0.50000000, 0.50000000, 0.50000000]])
bias:Parameter containing:
Tensor(shape=[4], dtype=float32, place=CUDAPlace(0), stop_gradient=False,
       [1., 1., 1., 1.])
x:Tensor(shape=[3, 2], dtype=float32, place=CUDAPlace(0), stop_gradient=True,
       [[ 0.30493721, -1.89157832],
        [-1.51025200, -0.68703473],
        [-1.10733140,  1.64044058]])
y:Tensor(shape=[3, 4], dtype=float32, place=CUDAPlace(0), stop_gradient=False,
       [[ 0.20667946,  0.20667946,  0.20667946,  0.20667946],
        [-0.09864330, -0.09864330, -0.09864330, -0.09864330],
        [ 1.26655459,  1.26655459,  1.26655459,  1.26655459]])

使用SubClass 组网

针对一些比较复杂的网络结构,就可以使用Layer子类定义的方式来进行模型代码编写,在__init__构造函数中进行组网Layer的声明,在forward中使用声明的Layer变量进行前向计算。子类组网方式也可以实现sublayer的复用,针对相同的layer可以在构造函数中一次性定义,在forward中多次调用。

# Layer类继承方式组网
class Mnist(paddle.nn.Layer):
    def __init__(self):
        super(Mnist, self).__init__()

        self.flatten = paddle.nn.Flatten()
        self.linear_1 = paddle.nn.Linear(784, 512)
        self.linear_2 = paddle.nn.Linear(512, 10)
        self.relu = paddle.nn.ReLU()
        self.dropout = paddle.nn.Dropout(0.2)

    def forward(self, inputs):
        y = self.flatten(inputs)
        y = self.linear_1(y)
        y = self.relu(y)
        y = self.dropout(y)
        y = self.linear_2(y)

        return y

mnist = Mnist()

r t = σ ( W i r x t + b i r + W h r h t − 1 + b h r ) r_{t} = \sigma(W_{ir}x_{t} + b_{ir} + W_{hr}h_{t-1} + b_{hr}) rt=σ(Wirxt+bir+Whrht1+bhr)

z t = σ ( W i z x t + b i z + W h z h t − 1 + b h z ) z_{t} = \sigma(W_{iz}x_{t} + b_{iz} + W_{hz}h_{t-1} + b_{hz}) zt=σ(Wizxt+biz+Whzht1+bhz)

h ~ t = tanh ⁡ ( W i c x t + b i c + r t ∗ ( W h c h t − 1 + b h c ) ) \widetilde{h}_{t} = \tanh(W_{ic}x_{t} + b_{ic} + r_{t} * (W_{hc}h_{t-1} + b_{hc})) h t=tanh(Wicxt+bic+rt(Whcht1+bhc))

h t = z t ∗ h t − 1 + ( 1 − z t ) ∗ h ~ t h_{t} = z_{t} * h_{t-1} + (1 - z_{t}) * \widetilde{h}_{t} ht=ztht1+(1zt)h t

y t = h t y_{t} = h_{t} yt=ht

下面,根据GRU的数学公式写出它的代码。

导入PaddlePaddle

import paddle

组网Layer的声明

self.Linear_ir = paddle.nn.Linear(10, 10)
self.Linear_hr = paddle.nn.Linear(10, 10)
self.Linear_iz = paddle.nn.Linear(10, 10)
self.Linear_hz = paddle.nn.Linear(10, 10)
self.Sigmoid = paddle.nn.Sigmoid()
self.Tanh = paddle.nn.Tanh()
self.Linear_ic = paddle.nn.Linear(10, 10)
self.Linear_hc = paddle.nn.Linear(10, 10)

前向计算

xt = inputs[0]
ht_1 = inputs[1]

ir = self.Linear_ir(xt)
hr = self.Linear_hr(ht_1)
rt = self.Sigmoid(ir + hr)

iz = self.Linear_ir(xt)
hz = self.Linear_hr(ht_1)
zt = self.Sigmoid(iz + hz)

ic = self.Linear_ic(xt)
hc = self.Linear_hc(ht_1)
h_tilda = self.Tanh(ic + rt * hc)

ht = zt * ht_1 + (1.0 - zt) * h_tilda

y = ht
return y

整理成GRU网络

import paddle

class GRU(paddle.nn.Layer):
    def __init__(self):
        super(GRU, self).__init__()

        self.Linear_ir = paddle.nn.Linear(10, 10)
        self.Linear_hr = paddle.nn.Linear(10, 10)
        self.Linear_iz = paddle.nn.Linear(10, 10)
        self.Linear_hz = paddle.nn.Linear(10, 10)
        self.Sigmoid = paddle.nn.Sigmoid()
        self.Tanh = paddle.nn.Tanh()
        self.Linear_ic = paddle.nn.Linear(10, 10)
        self.Linear_hc = paddle.nn.Linear(10, 10)

    def forward(self, inputs): 
        xt = inputs[0]
        ht_1 = inputs[1]

        ir = self.Linear_ir(xt)
        hr = self.Linear_hr(ht_1)
        rt = self.Sigmoid(ir + hr)

        iz = self.Linear_ir(xt)
        hz = self.Linear_hr(ht_1)
        zt = self.Sigmoid(iz + hz)

        ic = self.Linear_ic(xt)
        hc = self.Linear_hc(ht_1)
        h_tilda = self.Tanh(ic + rt * hc)

        ht = zt * ht_1 + (1.0 - zt) * h_tilda

        y = ht

        return y

gru = GRU()

GRU代码测试

x = paddle.randn((2, 10, 10))
y = gru(x)
print("x:{}".format(x))
### GRU代码测试


```python
x = paddle.randn((2, 10, 10))
y = gru(x)
print("x:{}".format(x))
print("y:{}".format(y))
x:Tensor(shape=[2, 10, 10], dtype=float32, place=CUDAPlace(0), stop_gradient=True,
       [[[-1.23688149, -0.17335074,  1.12128758,  0.01654052, -2.25986099, -1.05273819,  0.49860737,  1.33913815,  1.41709363,  1.11617875],
         [ 1.20773053,  0.25190699,  0.63645649, -0.15178812,  0.38558376,  1.06525695,  1.35773337, -0.87815106, -1.02234280, -0.26863816],
         [ 0.83202833,  0.35741627,  0.27798858,  0.42471856, -1.47742105, -0.20523828,  0.60823894,  0.59238470,  0.69570953, -0.23501527],
         [-1.66190076,  1.09829438, -0.49272355, -0.52626604, -2.57263446,  0.23813504, -0.32548672, -0.18374123,  1.56514835,  1.38403678],
         [-1.13899028, -0.03577895,  0.35591760,  0.07019860, -0.74009484, -1.69030738,  0.21631148,  0.94210184,  0.60481238,  0.93007690],
         [-0.49875101,  0.38129321, -1.35123706,  0.84165609, -0.60629928, -0.41080648, -0.12057021, -0.66971582, -1.30057406,  1.33788550],
         [-1.45253122, -0.45265883, -0.67711097,  0.19281828,  0.79509574,  0.89497662,  0.17031211, -0.07180623,  1.23113942,  1.45276558],
         [-0.68787557, -0.38812304,  0.73213565, -1.56900048, -0.37708279, -0.33701321, -0.49931258, -1.28467560,  0.49102911,  0.79802299],
         [ 1.08501363, -0.33913976,  0.33938006,  0.63373303,  0.17953457, -0.31802577,  1.06707370,  0.61715007,  1.75313854, -0.41244328],
         [-0.02342343, -0.08053148, -0.37812191,  0.62420833, -1.48849773, -0.29437870, -0.32350472,  0.35529548, -0.57857150, -0.38771328]],

        [[-1.35947621, -0.06499371,  0.75499320,  1.37854171, -0.34539723,  2.19889164,  1.83039057, -0.52771395,  0.63588542, -0.10901656],
         [-0.57654977, -0.52028888, -0.30034900, -0.32340103,  0.08255346, -0.11118763,  0.59790570,  0.91402942, -0.54359454,  0.81970066],
         [ 0.98965907, -1.06375396,  0.02803453, -0.52549404,  1.66569328, -0.16494410, -0.10158475,  0.27044448,  0.66196555, -0.44405726],
         [-1.62280238, -0.42000467,  1.76581538, -0.62819356,  1.03418708, -1.74727595, -1.49724281, -1.15750849, -1.89843619, -1.09346831],
         [ 0.44047543, -0.26046458,  0.78908837, -0.58589929, -0.71597725, -0.36707914, -1.14292264,  2.25984526,  0.95518368, -0.34489620],
         [-1.43644631, -0.11850964,  0.38669378,  0.51886952,  0.63058609,  1.60239077,  0.15217087,  0.34746289, -0.05915897,  1.40110803],
         [ 0.83382082, -1.75101805, -0.10334164,  1.65824771, -0.24990967,  0.08048306,  0.57705009,  1.05695045,  0.88633364,  0.31165561],
         [-0.24762402, -0.81814468,  0.22250116, -0.88491410,  1.02073526, -0.72528249, -1.07479751, -0.14184393, -1.31006885,  1.33685100],
         [ 1.45489252, -0.04815796, -0.09124713,  0.69996679,  0.44647151, -1.62952816,  0.89259708,  0.96805191, -0.16721791, -0.61006635],
         [-1.70082438, -0.05379014, -1.06003237, -0.54283625, -0.27114618, -0.74802172, -0.50707072, -0.08983559,  0.95293242,  1.09397388]]])
y:Tensor(shape=[10, 10], dtype=float32, place=CUDAPlace(0), stop_gradient=False,
       [[ 0.14520603,  0.67404151, -0.00817549,  0.34321442, -0.78182596,  1.32726717,  1.35381448, -0.53631097,  0.36024374, -0.40964308],
        [-0.82716328, -0.63635063, -0.33055073, -0.06007849,  0.17795920, -0.14964885,  0.56933248,  0.76825643, -0.29830980,  0.03376311],
        [ 0.38745636, -0.29905555,  0.02182700, -0.04701963,  0.05946112, -0.42361289,  0.48355022,  0.26989233, -0.03224674, -0.61322331],
        [-1.43023038, -0.71561587,  0.86268783, -0.53602755, -0.08432311, -0.87605667, -0.63385916, -0.82205725, -0.91702652, -1.05017066],
        [ 0.47761607,  0.18874769,  0.77323550, -0.56867629, -0.86212790, -0.40293929, -0.88074285,  1.14270127,  0.05252013, -0.42765886],
        [-0.89775705, -0.82425386,  0.44143695,  0.56845534,  0.32671759,  0.61694491, -0.08547843,  0.42542931, -0.25654337,  1.11901224],
        [ 0.76882017, -1.25476789,  0.15704733,  1.24849272, -0.68049622,  0.18308198,  0.20473978,  0.64086723,  0.89720410,  0.10967828],
        [-0.25140968, -0.75052178,  0.05700951, -0.78863275,  0.35841396, -0.11416039, -0.66218102, -0.32322094, -1.09448612,  0.96195966],
        [ 0.05477545, -0.13403758,  0.13885610,  0.11034974,  0.06454031, -1.41611338,  0.76666969,  0.70874524,  0.20816121, -0.73370075],
        [ 0.17110205,  0.43432760, -0.97928166, -0.05102953, -0.30350113, -0.60969806, -0.06897119,  0.11675366, -0.25055820,  0.60119200]])

五、总结与升华

  • 我之前专注于应用层面,学会使用PaddlePaddle开发项目,但是没有关注底层的网络结构,因此刚开始做这个任务时,还有一点吃力,所以我把大部分时间用于学习GRU网络的结构以及数学公式上,事实证明,这一步也非常重要。
  • 我个人认为,只要会Python,那么PaddlePaddle用起来还是非常简单的,有不明白的去看看源码就明白了,根据GRU的数学公式写出代码,基于Layer类继承方式能快速地将GRU网络搭建起来,但是将自己写的代码对照GitHub上的源码,还是有很大差距。
  • 在看了源码后,我想尝试直接使用Sequential来快速完成组网,但是GRU网络不是简单的线性结构,它存在一个并行计算,而Sequential好像无法解决这一问题,目前我还没有想好使用Sequential组网GRU的方法

个人简介

北京联合大学 机器人学院 自动化专业 2018级 本科生 郑博培

百度飞桨开发者技术专家 PPDE

百度飞桨官方帮帮团、答疑团成员

深圳柴火创客空间 认证会员

百度大脑 智能对话训练师

我在AI Studio上获得至尊等级,点亮8个徽章,来互关呀

https://aistudio.baidu.com/aistudio/personalcenter/thirdview/147378

  • 1
    点赞
  • 0
    评论
  • 4
    收藏
  • 一键三连
    一键三连
  • 扫一扫,分享海报

相关推荐
©️2020 CSDN 皮肤主题: 博客之星2020 设计师:CY__ 返回首页
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值