T06_循环神经网络
文字编码
具有先后顺序的数据一般叫作序列(Sequence),比如随时间而变化的商品价格数据就是非常典型的序列。商品 A 在 1 月到 6 月之间的价格变化趋势可以用一个一维向量表示:[x1,x2,x3,x4,x5,x6]\left[ x_1,x_2,x_3,x_4,x_5,x_6 \right][x1,x2,x3,x4,x5,x6],考虑到多个商品,可以用shape为[b,s]的二维向量表示,其中b为商品种类,s为1 月到 6 月之间的价格。
序列信号表示起来并不麻烦,但对于很多信号并不能直接用一个标量数值表示,比如每个时间戳产生长度为𝑛的特征向量,则需要 shape 为[b, s, n]的张量才能表示。考虑更复杂的文本数据:句子。它在每个时间戳上面产生的单词是一个字符,并不是数值,不能直接用某个标量表示。
单词的一种简单表示方法就是One-hot编码,只考虑最常用的 5000 个汉字,一个汉字可以用长度为 5000 的 One-hot 向量表示,这种编码时高纬度且极稀疏的,不利于网络训练,One-hot 编码还有一个严重的问题,它忽略了单词先天具有的语义相关性,不能很好地体现原有文字的语义相关度。因此 One-hot 编码具有明显的缺陷。
在自然语言处理领域,有专门的一个研究方向在探索如何学习到单词的表示向量(Word Vector),使得语义层面的相关性能够很好地通过 Word Vector 体现出来。一个衡量词向量之间相关度的方法就是余弦相关度(Cosine similarity)
similarity(a,b)≜cos(θ)=a∗b∣a∣∗∣b∣similarity(a,b)\triangleq cos(\theta)=\frac{a*b}{|a|*|b|}similarity(a,b)≜cos(θ)=∣a∣∗∣b∣a∗b
Embedding
文字编码为数值的过程叫作 Word Embedding。
在神经网络中,单词的表示向量可以直接通过训练的方式得到,我们把单词的表示层叫作 Embedding 层。Embedding 层负责把单词编码为某个词向量𝒗,它接受的是采用数字编码的单词编号𝑖,如 2 表示“I”,3 表示“me”等,系统总单词数量记为NvocabN_{vocab}Nvocab,输出长度为𝑛的向量𝒗:
v=fθ(i∣Nvocab,n)v = f_{\theta}(i|N_{vocab},n)v=fθ(i∣Nvocab,n)
Embedding 层实现起来非常简单,构建一个 shape 为[Nvocab,n][N_{vocab},n][Nvocab,n]的查询表对象 table,对于任意的单词编号𝑖,只需要查询到对应位置上的向量并返回即可:
v=table[i]v=table[i]v=table[i]
Embedding 层是可训练的,它可放置在神经网络之前,完成单词到向量的转换,得到的表示向量可以继续通过神经网络完成后续任务,并计算误差L\mathcal{L}L,采用梯度下降算法来实现端到端(end-to-end)的训练。
可以通过 layers.Embedding(NvocabN_{vocab}Nvocab,n)来定义一个 Word Embedding层,其中NvocabN_{vocab}Nvocab参数指定词汇数量,𝑛指定单词向量的长度。
x = tf.range(10)
x = tf.random.shuffle(x)
net = keras.layers.Embeding(10,4) # 建共 10 个单词,每个单词用长度为 4 的向量表示的层
out = net(x) # 获取词向量
net.embeddings # 查询表 table
预训练的词向量
目前应用的比较广泛的预训练模型有 Word2Vec 和 GloVe 等。它们已经在海量语料库训练得到了较好的词向量表示方法,并可以直接导出学习到的词向量表,方便迁移到其它任务。
# 从预训练模型中加载词向量表
embed_glove = load_embed('glove.6B.50d.txt')
# 直接利用预训练的词向量表初始化 Embedding 层
net.set_weights([embed_glove])
经过预训练的词向量模型初始化的 Embedding 层可以设置为不参与训练:net.trainable= False,那么预训练的词向量就直接应用到此特定任务上;如果希望能够学到区别于预训练词向量模型不同的表示方法,那么可以把 Embedding 层包含进反向传播算法中去,利用梯度下降来微调单词表示方法。
循环神经网络
在每个时间戳ttt,网络层接受当前时间戳的输入xtx_txt和上一个时间戳的网络状态向量ht−1h_{t-1}ht−1,经过ht=fθ(ht−1,xt)h_t = f_{\theta}(h_{t-1},x_t)ht=fθ(ht−1,xt)交换后得到当前时间戳的新状态向量hth_tht,并写入内存状态中,其中fθf_{\theta}fθ代表了网络的运算逻辑,θ\thetaθ为网络参数集。在每一个时间戳上,网络层均有输出产生oto_tot,ot=gϕ(ht)o_t= g_{\phi }(h_t)ot=gϕ(ht),即将网络的状态向量变换后输出。
上述网络结构在时间戳上折叠,如下图所示,网络循环接受序列的每个特征向量xtx_txt,并刷新内部状态向量hth_tht,同时形成输出oto_tot。对于这种网络结构,称之为循环网络结构(Recurrent Neural Network,简称RNN)
如果使用张量WxhW_{xh}Wxh、WhhW_{hh}Whh和偏执bbb来参数化fθf_{\theta}fθ网络,并按照:ht=σ(Wxhxt+Whhht−1+b)h_t = \sigma(W_{xh}x_t+W_{hh}h_{t-1}+b)ht=σ(Wxhxt+Whhht−1+b)方式更新内存状态,把这种网络叫做基本的循环神经网络,一般说的循环神经网络即指这种实现。在循环神经网络中,激活函数更多地采用 tanh 函数。
可以选择不使用偏执bbb来进一步减少参数量,状态向量hth_tht可以直接用作输出,即ot=hto_t = h_tot=ht,也可以对hth_tht做一个简单的线性变换ot=Whohto_t = W_{ho}h_tot=Whoht后得到每个时间戳上的网络输出oto_tot。
RNN
在 TensorFlow 中,可以通过 layers.SimpleRNNCell 来完成σ(Wxhxt+Whhht−1+b)\sigma(W_{xh}x_t+W_{hh}h_{t-1}+b)σ(Wxhxt+Whhht−1+b)计算。需要注意的是,在TensorFlow 中,RNN 表示通用意义上的循环神经网络。
SimpleRNNCell
以某输入特征长度𝑛 = 4,Cell 状态向量特征长度ℎ = 3为例,首先新建一个SimpleRNNCell,不需要指定序列长度𝑠
cell = keras.layers.SimpleRNNCell(3) # 创建 RNN Cell,内存向量长度为 3
cell.bulid(input_shape=(None,4)) #输出特征长度 n=4
cell.trainable_variables # 打印 wxh, whh, b 张量
SimpleRNNCell 内部维护了 3 个张量,kernel变量即WxhW_{xh}Wxh张量,recurrent_kernel变量即WhhW_{hh}Whh张量,bias变量即偏置bbb向量。但是 RNN 的 Memory 向量hhh并不由SimpleRNNCell 维护,需要用户自行初始化向量hoh_oho并记录每个时间戳上的hth_tht。
通过调用 Cell 实例即可完成前向运算:ot,[ht]=Cell(xt,[ht−1])o_t,[h_t]=Cell(x_t,[h_{t-1}])ot,[ht]=Cell(xt,[ht−1])
神经网络的初始化阶段,状态向量h0h_0h0一般初始化为全 0 向量,例如:
h0 = [tf.zero([4,64])] # 初始化状态向量,用列表包裹,这么设置是为了与 LSTM、GRU 等 RNN 变种格式统一
x = tf.random.normal([4,80,100]) # # 生成输入张量,4 个 80 单词的句子
xt = x[:,0,:]
cell = keras.layers.SimpleRNNCell(64) # 建输入特征 n=100,序列长度 s=80,状态长度=64 的 Cell
out,h1 = cell(xt,h0) # 前向计算
print(out.shape, h1[0].shape)
对于长度为𝑠的训练来说,需要循环通过Cell 类𝑠次才算完成一次网络层的前向运算。
h = h0
# 在序列长度的维度解开输入,得到 xt:[b,n]
for xt in tf.unstack(x,axis=1):out,h = cell(xt,h) # 前向计算,out 和 h 均被覆盖
out=out # 最终输出可以聚合每个时间戳上的输出,也可以只取最后时间戳的输出
最后一个时间戳的输出变量 out 将作为网络的最终输出。实际上,也可以将每个时间戳上的输出保存,然后求和或者均值,将其作为网络的最终输出。
多层SimpleRNNCell网络
循环神经网络很容易出现梯度弥散和梯度爆炸到现象,深层的循环神经网络训练起来非常困难,目前常见的循环神经网络模型层数一般控制在十层以内。
以两层的循环神经网络为例:
x = tf.random.normal([4,80,100])
xt = x[:,0,:]
cell0 = keras.layers.SimpleRNNCell(64)
cell1 = keras.layers.SimpleRNNCell(64)
h0 = [tf.zero([4,64])] # cell0 的初始状态向量
h1 = [tf.zero([4,64])] # cell1 的初始状态向量
在时间轴上面循环计算多次来实现整个网络的前向运算,每个时间戳上的输入 xt 首先通过第一层,得到输出 out0,再通过第二层,得到输出 out1
for xt in tf.unstack(x,axis=1):out0,h0 = cell0(xt,h0)out1,h1 = cell1(out0,h1)
也可以先完成输入在第一层上所有时间戳的计算,并保存第一层在所有时间戳上的输出列表,再计算第二层、第三层等的传播。
middle_sequences=[]
for xt in tf.unstack(x,axis=1):out0,h0=cell0(xt,h0)middle_sequences.append(out0)for xt in middle_sequences:out1,h1=cell(xt,h1)
SimpleRNN
通过 SimpleRNN层高层接口可以非常方便实现多层SimpleRNNCell网络的功能。
要完成单层循环神经网络的前向运算:
layer = keras.layers.SimpleRNN(64) # 创建状态向量长度为 64 的 SimpleRNN 层
x = tf.random.normal([4,80,100])
out = layer(x)# 和普通卷积网络一样,一行代码即可获得输出
out.shape
默认返回最后一个时间戳上的输出,如果希望返回所有时间戳上的输出列表,可以设置 return_sequences=True 参数
layer = keras.layers.SimpleRNN(64,return_sequences=True)
out = layer(x)
out # 输出,自动进行了 concat 操作
对于多层循环神经网络,可以通过堆叠多个 SimpleRNN 实现,如两层的网络,用法和普通的网络类似
net = keras.Sequential([# 除最末层外,都需要返回所有时间戳的输出,用作下一层的输入keras.layers.SimpleRNN(64,return_sequences=True),keras.layers.SimpleRNN(64)
])
pout = net(x)
梯度弥散和梯度爆炸
循环神经网络的训练并不稳定,网络的深度也不能任意的加深。原因在于梯度推导中的关键表达式:
∂ht∂hi=∏j=it−1diag(σ′(Wxhxj+i)+Whhhj+b))Whh\frac{\partial h_t}{\partial h_i}=\prod_{j=i}^{t-1}diag(\sigma^\prime(W_{xh}x_{j+i})+W_{hh}h_j+b))W_{hh}∂hi∂ht=∏j=it−1diag(σ′(Wxhxj+i)+Whhhj+b))Whh
也就是说,从时间戳iii到时间戳ttt的梯度∂ht∂hi\frac{\partial h_t}{\partial h_i}∂hi∂ht包含了WhhW_{hh}Whh的连乘运算。当WhhW_{hh}Whh的最大特征值小于1时,多次连乘运算会导致∂ht∂hi\frac{\partial h_t}{\partial h_i}∂hi∂ht的元素值接近于零;当WhhW_{hh}Whh的值大于1时,多次连乘运算会导致∂ht∂hi\frac{\partial h_t}{\partial h_i}∂hi∂ht的元素值爆炸式增长。
梯度值接近于 0 的现象叫做梯度弥散(Gradient Vanishing),把梯度值远大于 1 的现象叫做梯度爆炸(Gradient Exploding)。梯度弥散和梯度爆炸是神经网络优化过程中间比较容易出现的两种情况,不利于网络训练的。
梯度裁剪
梯度爆炸可以通过梯度裁剪(Gradient Clipping)的方式在一定程度上的解决。梯度裁剪与张量限幅非常类似,也是通过将梯度张量的数值或者范数限制在某个较小的区间内,从而将远大于 1 的梯度值减少,避免出现梯度爆炸。
对张量的数值限幅
直接对张量的数值进行限幅,使得张量𝑾的所有元素wij∈[min,max]w_{ij} \in [min,max]wij∈[min,max],可以通过 tf.clip_by_value()函数来实现。
a = tf.random.uniform([2,2])
tf.clip_by_value(a,0.4,0.6) # # 梯度值裁剪
限制梯度张量𝑾的范数
通过限制梯度张量𝑾的范数来实现梯度裁剪。比如对𝑾的二范数∣∣W∣∣2||W||_2∣∣W∣∣2约束在[0,max]之间,如果∣∣W∣∣2||W||_2∣∣W∣∣2大于max值,则按照W′=W∣∣W∣∣2⋅maxW^\prime=\frac{W}{||W||_2} \cdot maxW′=∣∣W∣∣2W⋅max的方式将∣∣W∣∣2||W||_2∣∣W∣∣2约束在max内。可以通过 tf.clip_by_norm 函数方便的实现梯度张量𝑾裁剪。
a = tf.random.uniform([2,2])*5
b = tf.clip_by_norm(a,5) # 按范数方式裁剪
# 裁剪前和裁剪后的张量范数
tf.norm(a),tf.norm(b)
全局范数裁剪
神经网络的更新方向是由所有参数的梯度张量𝑾共同表示的,前两种方式只考虑单个梯度张量的限幅,会出现网络更新方向发生变动的情况。
在 TensorFlow 中,可以通过 tf.clip_by_global_norm 函数快捷地缩放整体网络梯度𝑾的范数,这样能够处理所有参数的梯度𝑾的范数,实现等比例的缩放,很好地限制网络的梯度值,同时不改变网络的更新方向。
令W(i)W^{(i)}W(i)表示网络参数的第iii个梯度张量,首先通过global_norm=∑il∥z∥22global\_norm = \sqrt{\sum_il{\Vert z \Vert_2}^2}global_norm=∑il∥z∥22计算网络的总范数global_norm,对第i个参数W(i)W^{(i)}W(i),通过W(i)=W(i)⋅max_normmax(globalnorm,maxnorm)W^{(i)} = \frac{W^{(i)}\cdot max\_norm}{max(global_norm,max_norm)}W(i)=max(globalnorm,maxnorm)W(i)⋅max_norm进行裁剪,其中max_norm是用户指定的全局最大范数值
w1 = tf.random.normal([3,3])# 创建梯度张量 1
w2 = tf.random.normal([3,3])# 创建梯度张量 2# 计算global norm
global_norm = tf.math.sqrt(tf.norm(w1)**2+tf.norm(w2)**2)
# 根据global norm和max norm=2裁剪
(ww1,ww2),global_norm = tf.clip_by_global_norm([w1,w2],2)
# 计算裁剪后的张量组的 global norm
global_norm2 = tf.math.sqrt(tf.norm(ww1)**2+tf.norm(ww2)**2)
print(global_norm, global_norm2)# 剪前的全局范数和裁剪后的全局范数
tf.clip_by_global_norm 返回裁剪后的张量 List 和 global_norm 这两个对象,其中global_norm 表示裁剪前的梯度总范数和。
在网络训练时,梯度裁剪一般在计算出梯度后,梯度更新之前进行:
with tf.GradientTape() as tape:logits = model(x) # 前向传播loss = criteon(y,logits) # 误差计算
# 计算梯度值
grads = tape.gradient(loss,model.trainable_variables)
grads,tmp = tf.clip_by_global_norm(grads,25) # # 全局梯度裁剪
optimizer.apply_gradients(zip(graps,model.trainable_variables)) # 利用裁剪后的梯度张量更新参数
梯度弥散
对于梯度弥散现象,可以通过增大学习率、减少网络深度、添加 Skip Connection 等一系列的措施抑制。
LSTM
受RNN的短时记忆影响,基础的RNN网络不能很好的理解长句子。LSTM 相对于基础的 RNN 网络来说,记忆能力更强,更擅长处理较长的序列信号数据,LSTM 提出后,被广泛应用在序列预测、自然语言处理等任务中,几乎取代了基础的 RNN 模型。
在LSTM 中,有两个状态向量ccc和 ,其中hhh作为 LSTM 的内部状态向量,可以理解为LSTM 的内存状态向量 Memory,而 表示 LSTM 的输出向量。相对于基础的 RNN 来说,LSTM 把内部 Memory 和输出分开为两个变量,同时利用三个门控:输入门(Input Gate)、遗忘门(Forget Gate)和输出门(Output Gate)来控制内部信息的流动。
遗忘门
遗忘门作用于 LSTM 状态向量ccc上面,用于控制上一个时间戳的记忆ct−1c_{t-1}ct−1对当前时间戳的影响。
遗忘门控制变量gfg_fgf由:gf=σ(Wf[ht−1,xt]+bf)g_f = \sigma(W_f[h_{t-1},x_t]+b_f)gf=σ(Wf[ht−1,xt]+bf)产生。其中WfW_fWf和bfb_fbf为遗忘门的参数张量,可由反向传播算法自动优化,σ\sigmaσ为激活函数,一般使用 Sigmoid 函数。当门控gf=1g_f = 1gf=1时,遗忘门全部打开,LSTM 接受上一个状态ct−1c_{t-1}ct−1的所有信息;当门控gf=0g_f = 0gf=0时,遗忘门关闭,LSTM 直接忽略ct−1c_{t-1}ct−1,输出为 0的向量。
经过遗忘门后,LSTM 的状态向量变为gfct−1g_fc_{t-1}gfct−1。
输入门
输入门用于控制 LSTM 对输入的接收程度。首先通过对当前时间戳的输入xtx_txt和上一个时间戳的输出ht−1h_{t-1}ht−1非线性变换得到新的输入向量ct~\tilde{c_{t}}ct~:ct~=tanh(Wc[ht−1,xc]+bc)\tilde{c_{t}} = tanh(W_c[h_{t-1},x_c]+b_c)ct~=tanh(Wc[ht−1,xc]+bc),其中WcW_cWc和bcb_cbc为输入门的参数,需要通过反向传播算法自动优化,tanh 为激活函数,用于将输入标准化到[−1,1]区间。ct~\tilde{c_{t}}ct~并不会全部刷新进LSTM的Memory,而是通过输入门控制接受输入的量。输入门的控制变量同样来自于输入xtx_txt和输出ht−1h_{t-1}ht−1:gi=σ(Wi[ht−1,xt]+bi)g_i = \sigma(W_i[h_{t-1},x_t]+b_i)gi=σ(Wi[ht−1,xt]+bi)。其中WiW_iWi和bib_ibi为输入门的参数,需要通过反向传播算法自动优化,σ\sigmaσ为激活函数,一般使用Sigmoid 函数。当gi=0g_i = 0gi=0时,LSTM 不接受任何的新输入ct~\tilde{c_{t}}ct~;当gi=1g_i = 1gi=1时,LSTM 全部接受新输入ct~\tilde{c_{t}}ct~。
在遗忘门和输入门的控制下,LSTM 有选择地读取了上一个时间戳的记忆ct−1c_{t-1}ct−1和当前时间戳的新输入ct~\tilde{c_{t}}ct~,状态向量ctc_tct的刷新方式为:ct=gict~+gfct−1c_t = g_i\tilde{c_{t}} + g_fc_{t-1}ct=gict~+gfct−1。得到的新状态向量ctc_tct即为当前时间戳的状态向量。
输入门控 | 遗忘门控 | LSTM行为 |
---|---|---|
0 | 1 | 只用作记忆 |
1 | 1 | 综合输入和记忆 |
0 | 0 | 清零记忆 |
1 | 0 | 输入覆盖记忆 |
输出门
LSTM 的内部状态向量ctc_tct并不会直接用于输出,基础的RNN网络的状态向量hhh既用于记忆,又用于输出(基础的 RNN 可以理解为状态向量ccc和输出向量hhh是同一个对象)。在 LSTM 内部,状态向量并不会全部输出,而是在输出门的作用下有选择地输出。输出门的门控变量gog_ogo为:go=σ(Wo[ht−1,xt]+bo)g_o = \sigma(W_o[h_{t-1},x_t]+b_o)go=σ(Wo[ht−1,xt]+bo),其中WoW_oWo和bob_obo为输出门的参数,同样需要通过反向传播算法自动优化,σ\sigmaσ为激活函数,一般使用Sigmoid函数。当输出门go=0g_o=0go=0时,输出关闭,LSTM的内部记忆被全部隔断,无法用作输出,此时输出为0的向量;当输出门go=1g_o=1go=1,输出完全打开,LSTM 的状态向量ctc_tct全部用于输出。LSTM 的输出由:ht=go⋅thanh(ct)h_t = g_o \cdot thanh(c_t)ht=go⋅thanh(ct)产生(由于go∈[0,1]mtanh(ct)∈[−1,1]g_o \in [0,1]mtanh(c_t) \in [-1,1]go∈[0,1]mtanh(ct)∈[−1,1],则ht∈[−1,1]h_t\in[-1,1]ht∈[−1,1])。
LSTM层使用
在 TensorFlow 中,同样有两种方式实现 LSTM 网络。既可以使用 LSTMCell 来手动完成时间戳上面的循环运算,也可以通过 LSTM 层方式一步完成前向运算。
LSTMCell
LSTMCell 的用法和 SimpleRNNCell 基本一致,区别在于 LSTM 的状态变量 List 有两个,即[ht,ct][h_t,c_t][ht,ct],需要分别初始化。调用 cell完成前向运算时,返回两个元素,第一个元素为 cell 的输出,也就是hth_tht,第二个元素为cell 的更新后的状态 List:[ht,ct][h_t,c_t][ht,ct]。
x = tf.random.normal([2,80,100])
xt = x[:,0,:] # 得到一个时间戳的输入
cell = keras.layers.LSTMCell(64)
state = [tf.zeros[2,64],tf.zeros[2,64]]
out,state = cell(xt,state)
id(out),id(state[0]),id(state[1]) # id(out),id(state[0])是相同的
在序列长度维度上解开,循环送入 LSTM Cell 单元
for xt in tf.unstack(x,axis=1):out,state=cell(xt,state)
LSTM
通过 layers.LSTM 层可以方便的一次完成整个序列的运算
layer = keras.layers.LSTM(64)
out = layer(x) # 序列通过 LSTM 层,默认返回最后一个时间戳的输出
如果需要返回每个时间戳上面的输出,需要设置 return_sequences=True 标志
layer = keras.layers.LSTM(64,return_sequences=True)
out = layer(x) # 前向计算,每个时间戳上的输出自动进行了 concat,拼成一个张量
对于多层神经网络,可以通过 Sequential 容器包裹多层 LSTM 层,并设置所有非末层网络 return_sequences=True,这是因为非末层的 LSTM 层需要上一层在所有时间戳的输出作为输入
net = keras.Sequential([keras.layers.LTSM(64,return_sequences=True),keras.layers.LTSM(64)
])
out = layer(x) # 一次通过网络模型,即可得到最末层、最后一个时间戳的输出
GRU
LSTM 具有更长的记忆能力,在大部分序列任务上面都取得了比基础的 RNN 模型更好的性能表现,更重要的是,LSTM 不容易出现梯度弥散现象。但是 LSTM 结构相对较复杂,计算代价较高,模型参数量较大。科学家们尝试简化 LSTM 内部的计算流程,特别是减少门控数量。
门控循环网络(Gated Recurrent Unit,简称 GRU)是应用最广泛的 RNN 变种之一,GRU把内部状态向量和输出向量合并,统一为状态向量hhh,门控数量也减少到 2 个:复位门(Reset Gate)和更新门(Update Gate)。
复位门
复位门用于控制上一个时间戳的状态ht−1h_{t-1}ht−1进入 GRU 的量。门控向量grg_rgr由当前时间戳输入xtx_txt和上一时间戳状态ht−1h_{t-1}ht−1变换得到,关系如下:gr=σ(Wr[ht−1,xt]+br)g_r = \sigma(W_r[h_{t-1},x_t]+b_r)gr=σ(Wr[ht−1,xt]+br)。其中WrW_rWr和brb_rbr为复位门的参数,由反向传播算法自动优化,𝜎为激活函数,一般使用Sigmoid 函数。门控向量grg_rgr只控制状态ht−1h_{t-1}ht−1,而不会控制输入xtx_txt:ht~=tanh(Wh[grht−1]+bh)\tilde{h_{t}}=tanh(W_h[g_rh_{t-1}]+b_h)ht~=tanh(Wh[grht−1]+bh),当gr=0g_r = 0gr=0时,新输入ht~\tilde{h_t}ht~全部来自输入xtx_txt,不接受ht−1h_{t-1}ht−1,此时相当于复位ht−1h_{t-1}ht−1。当gr=1g_r=1gr=1时,ht−1h_{t-1}ht−1和输入xtx_txt共同产生新的输入ht~\tilde{h_t}ht~
更新门
更新门用于控制上一时间戳状态ht−1h_{t-1}ht−1和新输入ht−1h_{t-1}ht−1对新状态向量ht~\tilde{h_t}ht~的影响程度。更新门控向量gzg_zgz由gz=σ(Wt[ht−1,xt]+bz)g_z = \sigma(W_t[h_{t-1},x_t]+b_z)gz=σ(Wt[ht−1,xt]+bz)得到,其中WzW_zWz和bzb_zbz为更新门的参数,由反向传播算法自动优化,𝜎为激活函数,一般使用Sigmoid 函数。gzg_zgz用于控制新输入ht~\tilde{h_t}ht~信号,1−gz1-g_z1−gz用于控制状态ht−1h_{t-1}ht−1信号:ht=(1−gz)ht−1+gzht~h_t = (1-g_z)h_{t-1}+g_z\tilde{h_t}ht=(1−gz)ht−1+gzht~,ht~\tilde{h_t}ht~和ht−1h_{t-1}ht−1对hth_tht的更新处于相互竞争、此消彼长的状态。当更新门gz=0g_z = 0gz=0时,hth_tht全部来自上一时间戳状态ht−1h_{t-1}ht−1;当更新门gz=1g_z = 1gz=1时, hth_tht全部来自新输入ht~\tilde{h_t}ht~。
GRU使用方法
同样地,在 TensorFlow 中,也有 Cell 方式和层方式实现 GRU 网络。GRUCell 和 GRU层的使用方法和之前的 SimpleRNNCell、LSTMCell、SimpleRNN 和 LSTM 非常类似。
h = [tf.zeros[2,64]]
cell = layers.GRUCell(64) # 新建 GRU Cell,向量长度为 64
for xt in tf.unstack(x,axis=1):out,h = cell(x)
通过 layers.GRU 类可以方便创建一层 GRU 网络层,通过 Sequential 容器可以堆叠多层 GRU 层的网络。
net = keras.Sequential([layers.GRU(64, return_sequences=True),layers.GRU(64)
])
out = net(x)