简介
前面学习了卷积神经网络(CNN
),卷积神经网络适合图片这类具有空间局部相关性的数据,而在新的一节中开始学习循环神经网络(RNN
),循环神经网络适合具有时间序列的数据,如股票、语音对话、阅读的文本等。
序列表示
序列是指具有先后顺序的一组数据。
序列信号使用一个 shape
为 [b,s]
的张量即可表示,其中 b
表示序列数量,s
表示序列长度。
对于一句含有 n
个单词的句子,单词的表示方法就是之前说过的 one-hot
编码,这种将文字编码为数值的过程叫做 Word Embedding
。
但在此过程中会破坏文字之间的相关性,因此可以通过余弦相关度衡量词向量之间的相关度。
$$ similarity(a, b) = cos(\theta) = {\frac {a * b} {|a| * |b|}} $$
Embedding
层
在神经网络总,单词的表示向量可以直接通过训练的方式得到,将单词的表示层称为 Embedding
层。Embedding
层负责将单词编码为某个词向量 v
,其接受的是采用数字编码的单词编号 i
,系统总单词量记为 N
,输出长度为 n
的向量 v
。Embedding
层实现非常简单,构建一个 shape
为 [N,n]
的查询表对象 table
,对于任意的单词编号 i
,只需要查询到对应位置上的向量并返回即可。
Embedding
层是可训练的,其放置在神经网络之前,完成单词到向量的转换,得到的表示向量继续通过神经网络完成后续任务,并计算误差 L
,采用梯度下降算法来实现端到端(end-to-end
)的训练。
通过 layers.Embedding(N, n)
来定义一个 Word Embedding
层,其中 N
表示制定词汇数量,n
指定单词向量长度。
1 | x = tf.range(10) # 生成10个单词的数字编码 |
预训练词向量
Embedding
层的查询表是随机初始化的,需要从零开始训练,但实际上可以使用预训练的 Word Embedding
模型得到更好的单词表示方法。
目前较为广泛的预训练模型为 Word2Vec
和 GloVe
等。
1 | # load embedding as a dict |
经过预训练的词向量模型初始化的 Embedding
层可以设置为不参与训练:net.trainable = False
,其预训练的词向量就可以直接应用到此特定任务上。
循环神经网络
原理
如何让网络具有整体理解序列信号的能力?
此时想到了内存(Memory
)机制,网络提供一个单独的内存变量,每次提取词向量的特征并刷新内存变量,直至最后一个输入完成,此时的内存变量即存储了所有序列的语义特征,并且由于输入序列之间的先后顺序,使得内存变量内容与序列顺序具有关联。
定义状态向量 $h_0$ 为初始的内存状态,经过 s
个词向量的输入后得到网络最终的状态张量 $h_s$,其很好地代表了句子的全局语义信息,基于 $h_s$ 通过某个全连接层分类器即可完成任务。
循环神经网络
为了解决上述问题,提出了一种新的网络结构。在每个时间戳 $t$,网络层接受当前时间戳的输入 $x$ 和上一个时间戳的网络状态向量 $h$,经过 $h_t = f_{\theta}(h_{t-1}, x_t)$ 变换后得到当前时间戳的新状态向量 $h_t$,并写入内存状态中。
其中 $f_{\theta}$ 表示网络的运算逻辑,在每个时间戳伤网络层均有输出产生 $o = g(h)$,即将网络的状态向量变换后输出。
将上述网络结构进行折叠,便可得到网络循环接受序列的每个特征向量 $x_t$,并刷新内部状态向量 $h_t$,同时得到输出 $o_t$,对于这种网络结构将其称为循环网络结构(Recurrent Neural Network, RNN
)。
使用 $W_{xh}, W_{hh}, b$ 来参数化 $f$ 网络,并按照
$$ f_{\theta} = \sigma (W_{xh} x_t + W_{hh} h_{t-1} + b) $$
方式更新内存,将这种网络称为基本的循环神经网络。
在循环神经网络中,激活函数一般更多地采用 tanh
函数,并且可以选择不使用偏置 $b$ 来进一步减少参数量。状态向量 $h_t$ 可以直接用作输出,即 $o_t=h_t$,也可以对 $h_t$ 做简单线性变换 $o_t = W_{ho} h_t$ 后得到每个时间戳上的网络输出 $o_t$。
梯度传播
通过循环神经网络的更新表达式可以看出输出对张量 $W_{xh}$、$W_{hh}$ 和偏置 $b$ 均是可导的,其均可以利用自动梯度求导算法计算。
考虑梯度 ${\frac {\delta L} {\delta W_{hh}}}$,由于 $W_{hh}$ 被每个时间戳 i
上权值共享,因此在计算 ${\frac {\delta L} {\delta W_{hh}}}$ 时需要讲每个中间时间戳 i
上的梯度求和,利用链式法则展开为:
$$ {\frac {\delta L} {\delta W_{hh}}} = sum_{i=1}^t {\frac {\delta L} {\delta o_t}} {\frac {\delta o_t} {\delta h_t}} {\frac {\delta h_t} {\delta h_i}} {\frac {\delta^+ h_i} {\delta W_{hh}}} $$
其中 ${\frac {\delta L} {\delta o_t}}$ 可以基于损失函数求得,${\frac {\delta o_t} {\delta h_t}}$ 在 $o_t = h_t$ 时
$${\frac {\delta o_t} {\delta h_t}} = I$$
${\frac {\delta^+ h_i} {\delta W_{hh}}}$ 的梯度将 $h_i$ 展开后也可以求得:
$$ {\frac {\delta^+ h_i} {\delta W_{hh}}} = {\frac {\delta \sigma (W_{xh} x_t + W_{hh} h_{t-1} + b)} {\delta W_{hh}}} $$
其中 ${\frac {\delta^+ h_i} {\delta W_{hh}}}$ 只考虑到一个时间戳的梯度传播,与 ${\frac {\delta L} {\delta W_{hh}}}$ 考虑所有时间戳的偏导数不同,因此只需要推导出 ${\frac {\delta h_t} {\delta h_i}}$ 的表达式即可完成循环神经网络的梯度推导。
利用链式法则将 ${\frac {\delta h_t} {\delta h_i}}$ 拆分为连续时间戳的梯度表达式:
$$ {\frac {\delta h_t} {\delta h_i}} = {\frac {\delta h_t} {\delta h_{t-1}}} {\frac {\delta h_{t-1}} {\delta h_{t-2}}} … {\frac {\delta h_{i+1}} {\delta h_i}} = sum_{k=i}^{t-1} {\frac {\delta h_{k+1}} {\delta h_k}}$$
由于
$$ h_{k+1} = \sigma (W_{xh} x_{k+1} + W_{hh} h_k + b) $$
因此
$$ {\frac {\delta h_{k+1}} {\delta h_k}} = W_{hh}^T diag(\sigma^‘ (W_{xh} x_{k+1} + W_{hh} h_k + b)) = W_{hh}^T diag(\sigma^‘(h_{k+1})) $$
其中 $diag(x)$ 把向量 $x$ 的每个元素作为矩阵的对角元素,得到其他元素全为 0
的对角矩阵。
最终
$$ {\frac {\delta h_t} {\delta h_i}} = sum_{j=i}^{t-1} diag(\sigma^‘(W_{xh} x_{j+1} + W_{hh} h_j + b)) W_{hh} $$
至此 ${\frac {\delta L} {\delta W_{hh}}}$ 梯度推导完成。
梯度爆炸和梯度弥散
循环神经网络的训练并不稳定,其深度并不能随意加深,通过回顾 ${\frac {\delta h_t} {\delta h_i}} = sum_{j=i}^{t-1} diag(\sigma^‘(W_{xh} x_{j+1} + W_{hh} h_j + b)) W_{hh}$ 可以发现其内部包含 $W_{hh}$ 的连乘运算。
- 当 $W_{hh}$ 的最大值连续大于
1
时,多次相乘会使得 ${\frac {\delta h_t} {\delta h_i}}$ 结果爆炸式增大。 - 当 $W_{hh}$ 的最大值连续小于
1
时,多次相乘会使得 ${\frac {\delta h_t} {\delta h_i}}$ 结果趋近于零。
这种梯度值接近于 0
的现象叫做梯度弥散(Gradient Vanishing
),而把梯度值远大于 1
的现象叫做梯度爆炸(Gradient Exploding
)。梯度爆炸和梯度弥散都是神经网络优化过程中很容易出现的情况。
梯度爆炸
梯度爆炸可以通过梯度剪裁(Gradient Clipping
)的方式在一定程度上解决。梯度剪裁通过将梯度张量的数值或范数限制在某个较小的区间内,从而将远大于 1
的梯度值减少,避免出现梯度爆炸。
在深度学习中,梯度剪裁常用以下三种方式:
- 张量的数值限幅
- 限制梯度张量的范数
- 全局范数剪裁
梯度弥散
对于梯度弥散现象,可以通过以下措施进行抑制:
- 增大学习率
- 减少网络深度
- 添加
Skip Connection
RNN
层
layers.SimpleRNNCell()
和 layers.SimpleRNN()
,其中带 Cell
的层仅完成一个时间戳的前向计算,不带 Cell
的层是基于 Cell
层实现,内部完成多个时间戳的循环计算。
SimpleRNNCell
1 | # 特征长度为4,Cell 状态向量特征长度 h=3 |
RNN
内部维护 3
个变量,kernel
变量即 $W_{xh}$,recurrent_kernel
变量即 $W_{hh}$,bias
即偏置变量 $b$。
多层 SimpleRNNCell
1 | x = tf.random.normal([4,80,100]) |
目前常见的循环神经网络的深度都在10 层以内,因为其很容易出现梯度弥散和梯度爆炸现象。
SimpleRNN
1 | layer = layers.SimpleRNN(64) # 创建状态向量长度为64 的SimpleRNN 层 |
如果希望获得所有时间戳上的输出列表,设置 return_sequences=True
参数
对于多层循环神经网络,可以通过堆叠多个 SimpleRNN
来实现。
1 | # 构建两层RNN。出最外层外,均需要返回所有时间戳的输出,用于下一层的输入 |
RNN
短时记忆
循环神经网络在处理较长的句子时,仅能够理解有限长度内的信息,而对于较长范围内的信息往往不能很好利用,这种现象被叫做短时记忆。
那该如何延长短时记忆?就提出了长短时记忆网络(Long Short-Term Memory, LSTM
) ,相比 RNN
其记忆能力更强,更擅长处理较长的序列信号数据。
LSTM
相比 RNN
网络只有一个状态向量 $h_t$,LSTM
新增了一个状态向量 $C_t$,同时引入门控(Gate
)机制,可通过门控单元来控制信息的遗忘和刷新。
在 LSTM
中有两个状态向量 c
和 h
,其中 c
作为 LSTM
的内部状态向量,可以理解为 LSTM
的内存状态向量 Memory
;而 h
表示 LSTM
的输出向量。同时 LSTM
将内部 Memory
和输出分开为两个变量,利用以下三个门控来控制内部信息的流动:
- 输入门(
Input Gate
) - 遗忘门(
Forget Gate
) - 输出门(
Output Gate
)
门控机制
门控机制可以简单理解为控制数据流通量的一种手段。
在 LSTM
中阀门的开合程度通过门控值向量 g
表示,通过 $\sigma_g$ 激活函数将门控制压缩到 [0,1]
之间区间。
- 当 $\sigma_g=0$ 时,门控值全部关闭,输出
o=0
。 - 当 $\sigma_g=1$ 时,门控全部打开,输出
o=x
。
通过门控机制可以较好地控制数据的流量程度。
输入门
输入门用于控制 LSTM
对输入的接收程度。
首先通过对当前时间戳的输入 $x_t$ 和上一个时间戳的输出 $h_{t-1}$ 做非线性变换得到新的输入变量 $\hat{c_t}$
$$\hat{c_t} = tanh(W_c[h_{t-1}, x_t] + b_c)$$
其中 $W_c$ 和 $b_c$ 为输入门的参数,需要通过反向传播算法自动优化,tanh
为激活函数,用于将输入标准化到 [-1,1]
区间。
$\hat{c_t}$ 并不会全部刷新进入 LSTM
的 Memory
,而是通过输入门控制接受输入的量。
输入门的控制变量同样来自于输入 $x_t$ 和输出 $h_{t-1}$:
$$g_i = \sigma(W_i[h_{t-1},x_t] + b_i)$$
其中 $W_i$ 和 $b_i$ 为输入门的参数,需要通过反向传播算法自动优化,$\sigma$ 为激活函数,一般使用 Sigmoid
函数。
输入门控制变量 $g_i$ 决定了 LSTM
对当前时间戳的新输入 $\hat{c_t}$ 的接受程度:
- 当 $g_i=0$ 时,
LSTM
不接受任何的新输入 $\hat{c_t}$。 - 当 $g_i=1$ 时,
LSTM
全部接受新输入 $\hat{c_t}$。
经过输入门后,待写入 Memory
的向量为 $g_i * \hat{c_t}$。
遗忘门
遗忘门作用于 LSTM
状态向量 c
上,用于控制上一个时间戳的记忆 $c_{t-1}$ 对当前时间戳的影响。
遗忘门的控制变量 $g_f$
$$g_f = \sigma(W_f[h_{t-1}, x_t] + b_f)$$
其中 $W_f$ 和 $b_f$ 为遗忘门的参数张量,可由反向传播算法自动优化,$\sigma$ 为激活函数,一般使用 Sigmoid
函数。
- 当门控 $g_f=1$ 时,遗忘门全部打开,
LSTM
接受上一个状态 $c_{t-1}$ 的所有信息; - 当门控 $g_f=0$ 时,遗忘门关闭,
LSTM
直接忽略 $c_{t-1}$,输出为0
的向量。
经过遗忘门后,LSTM
的状态向量变为 $g_f * c_{t-1}$。
输出门
LSTM
的内部状态向量 $c_t$ 并不会直接用于输出,这一点与 RNN
不一样。RNN
网络的状态向量 h
即用于记忆,也用于输出,因此 RNN
可以理解为状态向量 c
和输出向量 h
是同一个对象。
LSTM
中状态向量并不会全部输出,而是在输出门作用下选择性地输出。输出门的门控变量 $g_o$ 为
$$g_o = \sigma(W_o[h_{t-1},x_t] + b_o)$$
其中 $W_o$ 和 $b_o$ 为输出门的参数,通过需要通过反向传播算法自动优化,$\sigma$ 为激活函数,一般使用 Sigmoid
函数。
- 当输出门 $g_o=0$ 时,输出关闭,
LSTM
的内部记忆完全被隔断,无法用作输出,此时输出向量为0
。 - 当输出门 $g_o=1$ 时,输出完全打开,
LSTM
的状态向量 $c_t$ 全部用于输出。
LSTM
的输出
$$h_t = g_o*tanh(c_t)$$
即内存向量 $c_t$ 经过 tanh
激活函数后与输入门作用得到 LSTM
的输出。$g_o \in [0,1]$ 同时 $tanh \in [-1,1]$,因此 LSTM
的输出 $h_t \in [-1,1]$。
刷新 Memory
在遗忘门和输入门的控制下,LSTM
有选择地读取上一个时间戳的记忆 $c_{t-1}$ 和当前时间戳的新输入 $\hat{c_t}$,状态向量 $c_t$ 的刷新方式为:
$$c_t = g_i * \hat{c_t} + g_f * c_{t-1}$$
得到新的状态向量 $c_t$ 即为当前时间戳的状态向量。
小结
LSTM
虽然状态向量和门控数量较多,计算流程复杂,但将典型的门控列表列举出来,即可解释 LSTM
的行为。
输入门控 | 遗忘门控 | LSTM 行为 |
---|---|---|
0 |
1 |
只使用记忆 |
1 |
1 |
综合输入和记忆 |
0 |
0 |
清零记忆 |
1 |
0 |
输入覆盖记忆 |
LSTM
层
在 TensorFLow
中同样有两种方式实现 LSTM
网络。既可以使用 LSTMCell
来手动完成时间戳上面的循环运算,也可以通过 LSTM
层方式一步完成前向计算。
LSTMCell
新建一个状态向量长度为 64
的 LSTMCell
,其中状态向量 $c_t$ 和输出向量 $h_t$ 的长度均为 h
。
1 | x = tf.random.normal([2,80,100]) |
LSTM
通过 layers.LSTM
层可以方便地一次完成整个序列的运算。
1 | layer = layers.LSTM(64) # 创建一层 LSTM 层,内存向量长度为 64 |
默认返回最后一个时间戳的输出,如果需要返回所有时间戳的输出,需要设置 return_sequences=True
标志
对于多层神经网络,可以通过 Sequential
容器包裹多层 LSTM
层,并设置所有非模型网络 return_sequences=True
,这是因为非末层需要上一层在所有时间戳的输出作为输入。
1 | net = Sequential([ |
GRU
LSTM
由于其门控机制可以在大部分序列任务取得较好的性能表现,但结构相对复杂、计算代价高、模型参数量较大等问题则有了门控循环网络(Gated Recurrent Unit, GRU
),其可以理解为是 LSTM
的简化版本。GRU
通过将内部状态向量和输出向量合并,统一为状态向量 h
,同时门控数量也减少到两个:
- 复位门(
Reset Gate
) - 更新门(
Update Gate
)
复位门
复位门用于控制上一个时间戳的状态 $h_{t-1}$ 进入 GRU
的量。
门控向量 $g_r$ 由当前时间戳的输入 $x_t$ 和上一个时间戳状态 $h_{t-1}$ 变换而得
$$g_r=\sigma(W_r[h_{t-1}, x_t] + b_r)$$
其中 $W_r$ 和 $b_r$ 为复位门的参数,由反向传播算法自动优化,$\sigma$ 为激活函数,一般使用 Sigmoid
函数。
门控向量 $g_r$ 仅控制状态 $h_{t-1}$,而不会控制输入 $x_t$
$$\hat{h_t} = tanh(W_h[g_r h_{t-1}, x_t] + b_h)$$
- 当 $g_r=0$ 时,新输入 $\hat{h_t}$ 全部来源于输入 $x_t$,不接受 $h_{t-1}$,此时相当于复位。
- 当 $g_r=1$ 时,$h_{t-1}$ 和 $x_t$ 共同作用产生新输入 $\hat{h_t}$。
更新门
更新门控制上一个时间戳转台 $h_{t-1}$ 和新输入 $\hat{h_t}$ 对新状态向量 $h_t$ 的影响程度。更新门控向量 $g_z$
$$g_z = \sigma(W_z[h_{t-1}, x_t] + b_z)$$
其中 $W_z$ 和 $b_z$ 为更新门的参数,由反向传播算法自动优化,$\sigma$ 为激活函数,一般使用 Sigmoid
函数。
$g_z$ 用于控制新输入 $\hat{h_t}$,$1-g_z$ 用于控制状态 $h_{t-1}$:
$$ h_t = (1-g_z) * h_{t-1} + g_z * \hat{h_t} $$
由上面可以看出 $h_{t-1}$ 与 $\hat{h_t}$ 处于此消彼长的状态,相互竞争。
- 当更新门 $g_z=0$ 时,$h_t$ 全部来自上一个时间戳状态 $h_{t-1}$。
- 当更新门 $g_z=1$ 时,$h_t$ 全部来自新输入 $\hat{h_t}$。
GRU
层
GRUCell
同样地也有 Cell
方式和层方式实现 GRU
网络。
1 | h = [tf.zeros([2,64])] # 初始化状态向量,GRU 只有一个 |
GRU
使用 Sequential
容器堆叠多层 GRU
层的网络。
1 | net = Sequential([ |
实践
本次实践使用最基础的 RNN
来实现情感分类。RNN
网络共两层,循环提取序列信号的语义特征,利用第二层 RNN
层的最后时间戳的状态向量 $h_s$ 作为句子的全局语义特征表示,之后送入全连接层构成的分类网络,得到样本为 $x$ 为积极情感的概率 $P$。
SimpleRNN
模型
引入依赖
1 | import tensorflow as tf |
加载数据
1 | # 数据集 |
预处理数据
1 | # 数字编码表 |
模型
1 | # 网络模型 |
训练模型并计算准确率
1 | # 训练与测试 |
在经历 20
轮次训练后,其在测试集上的准确率可以轻松达到 79.02%
。
LSTM/GRU
模型
得益于 TensorFlow
在循环神经网络相关接口的统一,原有代码仅需修改少部分即可升级到 LSTM
模型或 GRU
模型。
LSTM
1 | # 网络模型 |
GRU
1 | # 网络模型 |
对比 RNN
、LSTM
、GRU
三者在测试集上的准确率,可以发现每次技术上的优化都会带来实实在在的提升。
总结
所有事物都是基于已有旧事务之上优化、演进而来,对于计算机技术也是如此!
最近新发布了 Test Time Training(TTT): RNNs with Expressive Hidden States
,有兴趣的可以去研究下。
引用
个人备注
此博客内容均为作者学习《TensorFlow深度学习》所做笔记,侵删!
若转作其他用途,请注明来源!