对于序列模型,使用传统的神经网络效果并不好。原因是输入输出数据的长度可能不同,另外这种神经网络结果不能共享从文本不同位置所学习到的特征。循环神经则不存在这两个缺点。在每一个时间步中,循环神经网络会传递一个激活值到下一个时间步中,用于下一时间步的计算。

对于RNN,不同的问题需要不同的输入输出结构。各个RNN结构如下所示:

语言模型

使用RNN构建语言模型:

新序列采样

在完成一个序列模型的训练之后,如果我们想要了解这个模型学到了什么,其中一种方法就是进行新序列采样。我们需要进行以下几个步骤:

梯度消失

RNN存在一个很大的缺陷,就是梯度消失问题。普通的深度神经网络结构类似,梯度很难通过反向传播再传播回去,RNN也存在同样的问题,并更难解决。梯度爆炸虽然也可能会出现,但这一问题很容易被发现,并可以用梯度修剪解决。

GRU

门控循环单元(Gated Recurrent Unit, GRU)改变了RNN的隐藏层,使其能够更好地捕捉深层次连接,并改善了梯度消失的问题。以时间步从左到右计算时,GRU单元存在一个新的变量c,作为“记忆细胞”,其提供了长期的记忆能力。

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