对于序列模型,使用传统的神经网络效果并不好。原因是输入输出数据的长度可能不同,另外这种神经网络结果不能共享从文本不同位置所学习到的特征。循环神经则不存在这两个缺点。在每一个时间步中,循环神经网络会传递一个激活值到下一个时间步中,用于下一时间步的计算。
对于RNN,不同的问题需要不同的输入输出结构。各个RNN结构如下所示:
语言模型
使用RNN构建语言模型:
- 训练集:语言文本语料库;
- Tokenize:将句子使用字典库标记化,未出现在字典库中的词用“UNK”表示;
- 第一步:使用零向量预测第一个单词是某个单词的可能性;
- 第二步:通过前面的输入,逐步预测后面单词出现的概率;
- 训练网络:使用Softmax损失函数计算损失,进行参数更新。
新序列采样
在完成一个序列模型的训练之后,如果我们想要了解这个模型学到了什么,其中一种方法就是进行新序列采样。我们需要进行以下几个步骤:
- 输入$ x^{<1>} = 0$,$a^{<0>} = 0 $,在第一个时间步,经过Softmax得到所有可能的概率,根据分布进行随机采样,获取第一个单词 $ \hat y^{<1>} $;
- 继续下一个时间步,以刚刚的 $ \hat y^{<1>}$ 作为下一个时间步的输入,进而预测下一个输出$ \hat y^{<2>} $,依次类推;
- 如果字典中有结束标志如“EOS”,那么输出该符号时表示结束;若没有这种标志,可自行设置结束时间步。
梯度消失
RNN存在一个很大的缺陷,就是梯度消失问题。普通的深度神经网络结构类似,梯度很难通过反向传播再传播回去,RNN也存在同样的问题,并更难解决。梯度爆炸虽然也可能会出现,但这一问题很容易被发现,并可以用梯度修剪解决。
GRU
门控循环单元(Gated Recurrent Unit, GRU)改变了RNN的隐藏层,使其能够更好地捕捉深层次连接,并改善了梯度消失的问题。以时间步从左到右计算时,GRU单元存在一个新的变量c,作为“记忆细胞”,其提供了长期的记忆能力。
- $ c^{t} = a^{t} $ ,t时间步上的激活值a和记忆细胞输出值相等;
- $\widetilde c^{t} = \tanh (W_{c}[c^{t-1}, x^{t}] + b_{c})$,每个时间步的候选值$\widetilde c^{t}$,用以替代原本的记忆细胞值$c^{t}$;
- 更新门的值在0-1之间,用以决定是否更新记忆细胞值;
- 记忆细胞的更新公式能够有效缓解梯度消失问题。
LSTM
长短期记忆(long short-term memory, LSTM)对捕捉序列中更深层次的联系要比GRU更加有效。
LSTM中使用更新门${\Gamma _u}$ 、遗忘门${\Gamma _f}$以及输出门${\Gamma _o}$ ,以下以LSTM的向前传播代码展示RNN迭代过程:
def lstm_forward(x, a0, parameters):
caches = []
n_x, m, T_x = x.shape
n_y, n_a = parameters['Wy'].shape
# 初始化
a = np.zeros((n_a, m, T_x))
c = np.zeros((n_a, m, T_x))
y = np.zeros((n_y, m, T_x))
a_next = a0
c_next = np.zeros(a_next.shape)
# 遍历所有时间步
for t in range(T_x):
# 计算下一个隐藏层、记忆值、预测值
a_next, c_next, yt, cache = lstm_cell_forward(x[:,:,t], a_next, c_next, parameters)
# 保存数据
a[:,:,t] = a_next
y[:,:,t] = yt
c[:,:,t] = c_next
caches.append(cache)
# 保存以便反向传输计算
caches = (caches, x)
return a, y, c, caches
双向RNN
一般的循环神经网络,每个预测输出仅使用了前面的输入信息,而没有使用后面的信息。双向RNN(bidirectional RNNs)模型可以解决这种缺点。BRNN不仅有从左向右的前向连接层,还存在从右向左的反向连接层。
实例
以下展示一个生成恐龙名称的模型的核心代码,以字符为基础,使用LSTM模型:
def model(data, ix_to_char, char_to_ix, num_iterations = 35000, n_a = 50, dino_names = 7, vocab_size = 27):
n_x, n_y = vocab_size, vocab_size
# 初始化
parameters = initialize_parameters(n_a, n_x, n_y)
loss = get_initial_loss(vocab_size, dino_names)
# dinos.txt中包含各种已有的恐龙名称单词,转成列表作为训练数据
with open("dinos.txt") as f:
examples = f.readlines()
examples = [x.lower().strip() for x in examples]
# 将数据随机打乱
np.random.seed(0)
np.random.shuffle(examples)
# 初始化LSTM隐藏层
a_prev = np.zeros((n_a, 1))
# 迭代优化
for j in range(num_iterations):
# 每个单词为一个训练数据,将单词分解为字符,建立从字符到索引和索引到字符的对应
index = j % len(examples)
X = [None] + [char_to_ix[ch] for ch in examples[index]] # 整数列表,映射字符
Y = X[1:] + [char_to_ix["\n"]] # 整数列表,与X完全相同但向左移动一个索引
# 优化:前向传播 -> 反向传播 -> 梯度修剪 -> 更新参数
curr_loss, gradients, a_prev = optimize(X, Y, a_prev, parameters)
# 平滑损失,加速训练
loss = smooth(loss, curr_loss)
# …… 略去输出内容
return parameters
以下展示一个音乐生成模型的核心代码:
def music_inference_model(LSTM_cell, densor, n_values = 78, n_a = 64, Ty = 100):
# 定义输入
x0 = Input(shape=(1, n_values))
# 定义s0, 解码器LSTM的初始状态
a0 = Input(shape=(n_a,), name='a0')
c0 = Input(shape=(n_a,), name='c0')
a = a0
c = c0
x = x0
outputs = []
# 迭代,于每个时间步生成一个值
for t in range(Ty):
# 执行LSTM模块
a, _, c = LSTM_cell(x, initial_state=[a, c])
out = densor(a)
outputs.append(out)
# 根据“out”选择下一次迭代的x值,在下一次迭代作为输入传递给LSTM模块。
x = Lambda(one_hot)(out)
# 使用正确的“输入”和“输出”创建模型实例
inference_model = Model(inputs=[x0, a0, c0], outputs=outputs)
return inference_model