【中文】【吴恩达课后编程作业】Course 5 - 序列模型 - 第一周作业

【中文】【吴恩达课后编程作业】Course 5 - 序列模型 - 第一周作业 - 搭建循环神经网络及其应用


上一篇: 【课程5 - 第一周测验】※※※※※ 【回到目录】※※※※※下一篇: 【课程5 - 第二周测验】

资料下载

  • 本文所使用的资料已上传到百度网盘【点击下载(16.7MB)】,提取码:7vx7 ,请在开始之前下载好所需资料,底部不提供代码。

【博主使用的python版本:3.6.2】


恶心的大纲预览:

一步步搭建循环神经网络

 欢迎来到课程5的第一个作业,在这个作业中,你将在numpy中实现一个循环神经网络。

 递归神经网络(RNN)对于自然语言处理和其他序列任务非常有效,因为它们具有“记忆”功能。 它们可以一次读取一个输入 x t x^{⟨t⟩} (如单词),并且通过隐藏层激活从一个时间步传递到下一个时间步来记住一些信息/上下文,这允许单向RNN从过去获取信息来处理后面的输入,双向RNN可以从过去和未来中获取上下文。

有些东西需要声明:

  • 上标 [ l ] [l] 表示第 l l

    • 举例: a [ 4 ] a^{[4]} 表示第 4 4 层的激活值, W [ 5 ] W^{[5]} b [ 5 ] b^{[5]} 是第 5 5 层的参数。
  • 上标 ( i ) (i) 表示第 i i 个样本

    • 举例: x ( i ) x^{(i)} 表示第 i i 个输入的样本。
  • 上标 < t > <t> 表示第 t t 个时间步

    • 举例: x < t > x^{<t>} 表示输入 x x 的第 t t 个时间步, x ( i ) < t > x^{(i)<t>} 表示输入 x x 的第 i i 个样本的第 t t 个时间步。
  • 下标 i i 表示向量的第 i i

    • 举例: a i [ l ] a^{[l]}_{i} 表示 l l 层中的第 i i 个项的激活值。

 我们先来加载所需要的库:

import numpy as np
import rnn_utils

1 - 循环神经网络的前向传播

 我们来看一下下面的循环神经网络的图,在这里使用的是 T x = T y T_x = T_y ,我们来实现它。

扫描二维码关注公众号,回复: 10960568 查看本文章

rnn.png

**图 1**: 基本的RNN模型

我们怎么才能实现它呢?有以下步骤:

  1. 实现RNN的一个时间步所需要计算的东西。
  2. T x T_x 时间步上实现一个循环,以便一次处理所有输入。

1.1 - RNN单元

 循环神经网络可以看作是单元的重复,首先要实现单个时间步的计算,下图描述了RNN单元的单个时间步的操作。

rnn_step_forward.png

**图 2**: 基本的RNN单元. 输入 $x^{\langle t \rangle}$ (当前输入) 与 $a^{\langle t - 1\rangle}$ (包含过去信息的上一隐藏层的激活值), 输出 $a^{\langle t \rangle}$ 给下一个RNN单元,也用于预测 $y^{\langle t \rangle}$

 现在我们要根据图2来实现一个RNN单元,这需要由以下几步完成:

  1. 使用tanh函数计算隐藏单元的激活值: a t = tanh ( W a a a t 1 + W a x x t + b a ) a^{\langle t \rangle} = \tanh(W_{aa}a^{\langle t - 1 \rangle} + W_{ax}x^{\langle t \rangle} + b_a)

  2. 使用 a t a^{\langle t \rangle} 计算 y ^ t = s o f t m a x ( W y a a t + b y ) \hat y ^{\langle t \rangle} = softmax( W_{ya}a^{\langle t \rangle} + b_y ) softmax在rnn_utils内。

  3. ( a t , a t 1 , x t , p a r a m e t e r s ) ( a^{\langle t \rangle},a^{\langle t \rangle - 1},x^{\langle t \rangle},parameters) 存储到cache中。

  4. 返回 a t , y t a^{\langle t \rangle},y^{\langle t \rangle} 与cache。

 我们将向量化 m m 个样本,因此, x t x^{\langle t \rangle} 的维度为 ( n x , m ) (n_x,m) a t a^{\langle t \rangle} 的维度为 ( n a , m ) (n_a,m)

def rnn_cell_forward(xt, a_prev, parameters):
    """
    根据图2实现RNN单元的单步前向传播
    
    参数:
        xt -- 时间步“t”输入的数据,维度为(n_x, m)
        a_prev -- 时间步“t - 1”的隐藏隐藏状态,维度为(n_a, m)
        parameters -- 字典,包含了以下内容:
                        Wax -- 矩阵,输入乘以权重,维度为(n_a, n_x)
                        Waa -- 矩阵,隐藏状态乘以权重,维度为(n_a, n_a)
                        Wya -- 矩阵,隐藏状态与输出相关的权重矩阵,维度为(n_y, n_a)
                        ba  -- 偏置,维度为(n_a, 1)
                        by  -- 偏置,隐藏状态与输出相关的偏置,维度为(n_y, 1)
    
    返回:
        a_next -- 下一个隐藏状态,维度为(n_a, m)
        yt_pred -- 在时间步“t”的预测,维度为(n_y, m)
        cache -- 反向传播需要的元组,包含了(a_next, a_prev, xt, parameters)
    """
    
    # 从“parameters”获取参数
    Wax = parameters["Wax"]
    Waa = parameters["Waa"]
    Wya = parameters["Wya"]
    ba = parameters["ba"]
    by = parameters["by"]
    
    # 使用上面的公式计算下一个激活值
    a_next = np.tanh(np.dot(Waa, a_prev) + np.dot(Wax, xt) + ba)
    
    # 使用上面的公式计算当前单元的输出
    yt_pred = rnn_utils.softmax(np.dot(Wya, a_next) + by)
    
    # 保存反向传播需要的值
    cache = (a_next, a_prev, xt, parameters)
    
    return a_next, yt_pred, cache
    

我们来测试一下:

np.random.seed(1)
xt = np.random.randn(3,10)
a_prev = np.random.randn(5,10)
Waa = np.random.randn(5,5)
Wax = np.random.randn(5,3)
Wya = np.random.randn(2,5)
ba = np.random.randn(5,1)
by = np.random.randn(2,1)
parameters = {"Waa": Waa, "Wax": Wax, "Wya": Wya, "ba": ba, "by": by}

a_next, yt_pred, cache = rnn_cell_forward(xt, a_prev, parameters)
print("a_next[4] = ", a_next[4])
print("a_next.shape = ", a_next.shape)
print("yt_pred[1] =", yt_pred[1])
print("yt_pred.shape = ", yt_pred.shape)

测试结果:

a_next[4] =  [ 0.59584544  0.18141802  0.61311866  0.99808218  0.85016201  0.99980978
 -0.18887155  0.99815551  0.6531151   0.82872037]
a_next.shape =  (5, 10)
yt_pred[1] = [ 0.9888161   0.01682021  0.21140899  0.36817467  0.98988387  0.88945212
  0.36920224  0.9966312   0.9982559   0.17746526]
yt_pred.shape =  (2, 10)

1.2 - RNN的前向传播

 可以看到的是RNN是刚刚构建的单元格的重复连接,如果输入的数据序列经过10个时间步,那么将复制RNN单元10次,每个单元将前一个单元中的隐藏状态( a t 1 a^{\langle t-1 \rangle} )和当前时间步的输入数据( x t x^{\langle t \rangle} )作为输入。 它为此时间步输出隐藏状态( a t a^{\langle t \rangle} )和预测( y t y^{\langle t \rangle} )。

cell_rnn.png

**图 3**: 基本的RNN,序列 $x = (x^{\langle 1 \rangle}, x^{\langle 2 \rangle}, ..., x^{\langle T_x \rangle})$ 输入$T_x$时间步,输出 $y = (y^{\langle 1 \rangle}, y^{\langle 2 \rangle}, ..., y^{\langle T_x \rangle})$.

 我们要根据图3来实现前向传播的代码,它由以下几步构成:

  1. 创建0向量zeros(a),它将保存RNN计算的所有的隐藏状态。

  2. 使用“ a 0 a_0 ”初始化“next”隐藏状态。

  3. 循环所有时间步:

    • 使用rnn_cell_forward函数来更新“next”隐藏状态与cache。

    • 使用 a a 来保存“next”隐藏状态(第 t t )个位置。

    • 使用 y y 来保存预测值。

    • 把cache保存到“caches”列表中。

  4. 返回 a , y a, y 与caches。

def rnn_forward(x, a0, parameters):
    """
    根据图3来实现循环神经网络的前向传播
    
    参数:
        x -- 输入的全部数据,维度为(n_x, m, T_x)
        a0 -- 初始化隐藏状态,维度为 (n_a, m)
        parameters -- 字典,包含了以下内容:
                        Wax -- 矩阵,输入乘以权重,维度为(n_a, n_x)
                        Waa -- 矩阵,隐藏状态乘以权重,维度为(n_a, n_a)
                        Wya -- 矩阵,隐藏状态与输出相关的权重矩阵,维度为(n_y, n_a)
                        ba  -- 偏置,维度为(n_a, 1)
                        by  -- 偏置,隐藏状态与输出相关的偏置,维度为(n_y, 1)
    
    返回:
        a -- 所有时间步的隐藏状态,维度为(n_a, m, T_x)
        y_pred -- 所有时间步的预测,维度为(n_y, m, T_x)
        caches -- 为反向传播的保存的元组,维度为(【列表类型】cache, x))
    """
    
    # 初始化“caches”,它将以列表类型包含所有的cache
    caches = []
    
    # 获取 x 与 Wya 的维度信息
    n_x, m, T_x = x.shape
    n_y, n_a = parameters["Wya"].shape
    
    # 使用0来初始化“a” 与“y”
    a = np.zeros([n_a, m, T_x])
    y_pred = np.zeros([n_y, m, T_x])
    
    # 初始化“next”
    a_next = a0
    
    # 遍历所有时间步
    for t in range(T_x):
        ## 1.使用rnn_cell_forward函数来更新“next”隐藏状态与cache。
        a_next, yt_pred, cache = rnn_cell_forward(x[:, :, t], a_next, parameters)
        
        ## 2.使用 a 来保存“next”隐藏状态(第 t )个位置。
        a[:, :, t] = a_next
        
        ## 3.使用 y 来保存预测值。
        y_pred[:, :, t] = yt_pred
        
        ## 4.把cache保存到“caches”列表中。
        caches.append(cache)
    
    # 保存反向传播所需要的参数
    caches = (caches, x)
    
    return a, y_pred, caches

我们来测试一下:

np.random.seed(1)
x = np.random.randn(3,10,4)
a0 = np.random.randn(5,10)
Waa = np.random.randn(5,5)
Wax = np.random.randn(5,3)
Wya = np.random.randn(2,5)
ba = np.random.randn(5,1)
by = np.random.randn(2,1)
parameters = {"Waa": Waa, "Wax": Wax, "Wya": Wya, "ba": ba, "by": by}

a, y_pred, caches = rnn_forward(x, a0, parameters)
print("a[4][1] = ", a[4][1])
print("a.shape = ", a.shape)
print("y_pred[1][3] =", y_pred[1][3])
print("y_pred.shape = ", y_pred.shape)
print("caches[1][1][3] =", caches[1][1][3])
print("len(caches) = ", len(caches))

测试结果:

a[4][1] =  [-0.99999375  0.77911235 -0.99861469 -0.99833267]
a.shape =  (5, 10, 4)
y_pred[1][3] = [ 0.79560373  0.86224861  0.11118257  0.81515947]
y_pred.shape =  (2, 10, 4)
caches[1][1][3] = [-1.1425182  -0.34934272 -0.20889423  0.58662319]
len(caches) =  2

 我们构建了循环神经网络的前向传播函数,这对于某些应用程序来说已经足够好了,但是它还存在梯度消失的问题。当每个输出 y t y^{\langle t \rangle} 是根据局部的上下文来进行预测的时候,它的效果是比较好的(意思是输入的是 x t x^{\langle t' \rangle} ,其中 t t' t t 相隔不是太远)。

 接下来我们要构建一个更加复杂的LSTM模型,它可以更好地解决梯度消失的问题,LSTM能够更好地记住一条信息,并且可以在很多时间步中保存。

2 - 长短时记忆(Long Short-Term Memory (LSTM))网络

 下图是LSTM模块:

LSTM.png

**图 4**: LSTM单元. 它跟踪与更新每个时间步上的“单元状态”或记忆变量$c^{\langle t \rangle}$ ,这与 $a^{\langle t \rangle}$不同。

 与上面的RNN例子相类似,我们先来实现一个LSTM单元,只执行一个时间步,然后在循环中调用,以处理所有输入数据。

“门”的介绍

2.0.1 - 遗忘门

 假设我们正在阅读文本中的单词,并希望使用LSTM来跟踪语法结构,比如主语是单数还是复数。如果主语从单数变为复数,我们需要找到一种方法来摆脱我们先前存储的单复数状态的记忆值。在LSTM中,遗忘门是这样做的:

Γ f t = σ ( W f [ a t 1 , x t ] + b f ) (1) \Gamma_f^{\langle t \rangle} = \sigma(W_f[a^{\langle t-1 \rangle}, x^{\langle t \rangle}] + b_f) \tag{1}

 其中, W f W_f 是控制遗忘门的权值,我们把 a t 1 , x t a^{\langle t-1 \rangle}, x^{\langle t \rangle} 连接起来变为 [ a t 1 , x t ] [a^{\langle t-1 \rangle}, x^{\langle t \rangle}] ,然后乘以 W f W_f ,结果就是得到了一个矢量 Γ f t \Gamma_f^{\langle t \rangle} ,其值在 0 1 0与1 之间。这个遗忘门向量将与前一个单元状态 c t 1 c^{\langle t-1 \rangle} 相乘,因此,如果 Γ f t \Gamma_f^{\langle t \rangle} 的一个值是 0 0 (或者 0 \approx 0 ),则意味着LSTM应该删除对应的信息,如果其中有为 1 1 的值,那么LSTM将保留该信息。

2.0.2 - 更新门

 一旦我们“忘记”所讨论的过去的主题是单数,我们需要找到一种方法来更新它,以反映新的主题现在是复数。这里是更新门的公式:
Γ u t = σ ( W u [ a t 1 , x t ] + b u ) (2) \Gamma_u^{\langle t \rangle} = \sigma(W_u [a^{\langle t-1 \rangle}, x^{\langle t \rangle}] + b_u) \tag{2}

 与遗忘门相似, Γ u t \Gamma_u^{\langle t \rangle} 向量的值是在 0 0 1 1 之间,为了计算 c t c^{\langle t \rangle} ,它会与 c ~ t \tilde c^{\langle t \rangle} 相乘。

2.0.3 - 更新单元

 为了要更新主题,我们需要创建一个新的向量,我们可以将其添加到之前的单元状态中。我们使用的公式是:
c ~ t = tanh ( W c [ a t 1 , x t ] + b c ) (3) \tilde c^{\langle t \rangle} = \tanh(W_c[a^{\langle t-1 \rangle}, x^{\langle t \rangle}] + b_c) \tag{3}

最后,单元的新状态是:
c t = Γ f t c t 1 + Γ u t c ~ t (4) c^{\langle t \rangle} = \Gamma_f^{\langle t \rangle}\ast c^{\langle t - 1 \rangle} + \Gamma_u^{\langle t \rangle} \ast \tilde c^{\langle t \rangle} \tag{4}

2.0.4 - 输出门

 为了决定我们将使用哪种输出,我们将使用以下两个公式:
Γ o t = σ ( W o [ a t 1 , x t ] + b o ) (5) \Gamma_o^{\langle t \rangle}= \sigma(W_o[a^{\langle t-1 \rangle}, x^{\langle t \rangle}] + b_o)\tag{5}
a t = Γ o t tanh ( c t ) (6) a^{\langle t \rangle} = \Gamma_o^{\langle t \rangle}* \tanh(c^{\langle t \rangle})\tag{6}

2.1 - LSTM单元

 我们根据图4来实现一个LSTM单元,步骤如下:

  1. a t 1 , x t a^{\langle t-1 \rangle}, x^{\langle t \rangle} 连接起来变为一个矩阵: c o n t a c t = [ a t 1 x t ] contact = \begin{bmatrix} a^{\langle t -1 \rangle} \\ x^{\langle t \rangle} \\ \end{bmatrix}

  2. 计算公式1-6,我们可以使用sigmoid()(在rnn_utils内)与np.tanh()

  3. 计算预测 y t y^{\langle t \rangle} ,我们可以使用softmax()(在rnn_utils内)。

def lstm_cell_forward(xt, a_prev, c_prev, parameters):
    """
    根据图4实现一个LSTM单元的前向传播。
    
    参数:
        xt -- 在时间步“t”输入的数据,维度为(n_x, m)
        a_prev -- 上一个时间步“t-1”的隐藏状态,维度为(n_a, m)
        c_prev -- 上一个时间步“t-1”的记忆状态,维度为(n_a, m)
        parameters -- 字典类型的变量,包含了:
                        Wf -- 遗忘门的权值,维度为(n_a, n_a + n_x)
                        bf -- 遗忘门的偏置,维度为(n_a, 1)
                        Wi -- 更新门的权值,维度为(n_a, n_a + n_x)
                        bi -- 更新门的偏置,维度为(n_a, 1)
                        Wc -- 第一个“tanh”的权值,维度为(n_a, n_a + n_x)
                        bc -- 第一个“tanh”的偏置,维度为(n_a, n_a + n_x)
                        Wo -- 输出门的权值,维度为(n_a, n_a + n_x)
                        bo -- 输出门的偏置,维度为(n_a, 1)
                        Wy -- 隐藏状态与输出相关的权值,维度为(n_y, n_a)
                        by -- 隐藏状态与输出相关的偏置,维度为(n_y, 1)
    返回:
        a_next -- 下一个隐藏状态,维度为(n_a, m)
        c_next -- 下一个记忆状态,维度为(n_a, m)
        yt_pred -- 在时间步“t”的预测,维度为(n_y, m)
        cache -- 包含了反向传播所需要的参数,包含了(a_next, c_next, a_prev, c_prev, xt, parameters)
        
    注意:
        ft/it/ot表示遗忘/更新/输出门,cct表示候选值(c tilda),c表示记忆值。
    """
    
    # 从“parameters”中获取相关值
    Wf = parameters["Wf"]
    bf = parameters["bf"]
    Wi = parameters["Wi"]
    bi = parameters["bi"]
    Wc = parameters["Wc"]
    bc = parameters["bc"]
    Wo = parameters["Wo"]
    bo = parameters["bo"]
    Wy = parameters["Wy"]
    by = parameters["by"]
    
    # 获取 xt 与 Wy 的维度信息
    n_x, m = xt.shape
    n_y, n_a = Wy.shape
    
    # 1.连接 a_prev 与 xt
    contact = np.zeros([n_a + n_x, m])
    contact[: n_a, :] = a_prev
    contact[n_a :, :] = xt
    
    # 2.根据公式计算ft、it、cct、c_next、ot、a_next
    
    ## 遗忘门,公式1
    ft = rnn_utils.sigmoid(np.dot(Wf, contact) + bf)
    
    ## 更新门,公式2
    it = rnn_utils.sigmoid(np.dot(Wi, contact) + bi)
    
    ## 更新单元,公式3
    cct = np.tanh(np.dot(Wc, contact) + bc)
    
    ## 更新单元,公式4
    #c_next = np.multiply(ft, c_prev) + np.multiply(it, cct)
    c_next = ft * c_prev + it * cct
    ## 输出门,公式5
    ot = rnn_utils.sigmoid(np.dot(Wo, contact) + bo)
    
    ## 输出门,公式6
    #a_next = np.multiply(ot, np.tan(c_next))
    a_next = ot * np.tanh(c_next)
    # 3.计算LSTM单元的预测值
    yt_pred = rnn_utils.softmax(np.dot(Wy, a_next) + by)
    
    # 保存包含了反向传播所需要的参数
    cache = (a_next, c_next, a_prev, c_prev, ft, it, cct, ot, xt, parameters)
    
    return a_next, c_next, yt_pred, cache

我们来测试一下:

np.random.seed(1)
xt = np.random.randn(3,10)
a_prev = np.random.randn(5,10)
c_prev = np.random.randn(5,10)
Wf = np.random.randn(5, 5+3)
bf = np.random.randn(5,1)
Wi = np.random.randn(5, 5+3)
bi = np.random.randn(5,1)
Wo = np.random.randn(5, 5+3)
bo = np.random.randn(5,1)
Wc = np.random.randn(5, 5+3)
bc = np.random.randn(5,1)
Wy = np.random.randn(2,5)
by = np.random.randn(2,1)

parameters = {"Wf": Wf, "Wi": Wi, "Wo": Wo, "Wc": Wc, "Wy": Wy, "bf": bf, "bi": bi, "bo": bo, "bc": bc, "by": by}

a_next, c_next, yt, cache = lstm_cell_forward(xt, a_prev, c_prev, parameters)
print("a_next[4] = ", a_next[4])
print("a_next.shape = ", c_next.shape)
print("c_next[2] = ", c_next[2])
print("c_next.shape = ", c_next.shape)
print("yt[1] =", yt[1])
print("yt.shape = ", yt.shape)
print("cache[1][3] =", cache[1][3])
print("len(cache) = ", len(cache))

测试结果:

a_next[4] =  [-0.66408471  0.0036921   0.02088357  0.22834167 -0.85575339  0.00138482
  0.76566531  0.34631421 -0.00215674  0.43827275]
a_next.shape =  (5, 10)
c_next[2] =  [ 0.63267805  1.00570849  0.35504474  0.20690913 -1.64566718  0.11832942
  0.76449811 -0.0981561  -0.74348425 -0.26810932]
c_next.shape =  (5, 10)
yt[1] = [ 0.79913913  0.15986619  0.22412122  0.15606108  0.97057211  0.31146381
  0.00943007  0.12666353  0.39380172  0.07828381]
yt.shape =  (2, 10)
cache[1][3] = [-0.16263996  1.03729328  0.72938082 -0.54101719  0.02752074 -0.30821874
  0.07651101 -1.03752894  1.41219977 -0.37647422]
len(cache) =  10

2.2 - LSTM的前向传播

 我们已经实现了LSTM单元的一个时间步的前向传播,现在我们要对LSTM网络进行前向传播进行计算

LSTM_rnn.pn

**图 5**: 所有时间步的LSTM的计算

我们来实现lstm_forward(),然后运行 T x T_x 个时间步。

注意: c 0 c^{\langle 0 \rangle} 使用0来初始化。

def lstm_forward(x, a0, parameters):
    """
    根据图5来实现LSTM单元组成的的循环神经网络
    
    参数:
        x -- 所有时间步的输入数据,维度为(n_x, m, T_x)
        a0 -- 初始化隐藏状态,维度为(n_a, m)
        parameters -- python字典,包含了以下参数:
                        Wf -- 遗忘门的权值,维度为(n_a, n_a + n_x)
                        bf -- 遗忘门的偏置,维度为(n_a, 1)
                        Wi -- 更新门的权值,维度为(n_a, n_a + n_x)
                        bi -- 更新门的偏置,维度为(n_a, 1)
                        Wc -- 第一个“tanh”的权值,维度为(n_a, n_a + n_x)
                        bc -- 第一个“tanh”的偏置,维度为(n_a, n_a + n_x)
                        Wo -- 输出门的权值,维度为(n_a, n_a + n_x)
                        bo -- 输出门的偏置,维度为(n_a, 1)
                        Wy -- 隐藏状态与输出相关的权值,维度为(n_y, n_a)
                        by -- 隐藏状态与输出相关的偏置,维度为(n_y, 1)
        
    返回:
        a -- 所有时间步的隐藏状态,维度为(n_a, m, T_x)
        y -- 所有时间步的预测值,维度为(n_y, m, T_x)
        caches -- 为反向传播的保存的元组,维度为(【列表类型】cache, x))
    """
    
    # 初始化“caches”
    caches = []
    
    # 获取 xt 与 Wy 的维度信息
    n_x, m, T_x = x.shape
    n_y, n_a = parameters["Wy"].shape
    
    # 使用0来初始化“a”、“c”、“y”
    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”、“c_next”
    a_next = a0
    c_next = np.zeros([n_a, m])
    
    # 遍历所有的时间步
    for t in range(T_x):
        # 更新下一个隐藏状态,下一个记忆状态,计算预测值,获取cache
        a_next, c_next, yt_pred, cache = lstm_cell_forward(x[:,:,t], a_next, c_next, parameters)
        
        # 保存新的下一个隐藏状态到变量a中
        a[:, :, t] = a_next
        
        # 保存预测值到变量y中
        y[:, :, t] = yt_pred
        
        # 保存下一个单元状态到变量c中
        c[:, :, t] = c_next
        
        # 把cache添加到caches中
        caches.append(cache)
    
    # 保存反向传播需要的参数
    caches = (caches, x)
    
    return a, y, c, caches

我们来测试一下:

np.random.seed(1)
x = np.random.randn(3,10,7)
a0 = np.random.randn(5,10)
Wf = np.random.randn(5, 5+3)
bf = np.random.randn(5,1)
Wi = np.random.randn(5, 5+3)
bi = np.random.randn(5,1)
Wo = np.random.randn(5, 5+3)
bo = np.random.randn(5,1)
Wc = np.random.randn(5, 5+3)
bc = np.random.randn(5,1)
Wy = np.random.randn(2,5)
by = np.random.randn(2,1)

parameters = {"Wf": Wf, "Wi": Wi, "Wo": Wo, "Wc": Wc, "Wy": Wy, "bf": bf, "bi": bi, "bo": bo, "bc": bc, "by": by}

a, y, c, caches = lstm_forward(x, a0, parameters)
print("a[4][3][6] = ", a[4][3][6])
print("a.shape = ", a.shape)
print("y[1][4][3] =", y[1][4][3])
print("y.shape = ", y.shape)
print("caches[1][1[1]] =", caches[1][1][1])
print("c[1][2][1]", c[1][2][1])
print("len(caches) = ", len(caches))

测试结果:

a[4][3][6] =  0.172117767533
a.shape =  (5, 10, 7)
y[1][4][3] = 0.95087346185
y.shape =  (2, 10, 7)
caches[1][1[1]] = [ 0.82797464  0.23009474  0.76201118 -0.22232814 -0.20075807  0.18656139
  0.41005165]
c[1][2][1] -0.855544916718
len(caches) =  2

3 - 循环神经网络的反向传播(选学

 在现代深度学习框架中,我们只需要实现前向传播,框架负责反向传播,因此大多数深度学习工程师不需要为反向传播的细节而烦恼。 但是,如果您是微积分方面的专家并希望在RNN中查看反向传播的详细信息,则可以学习这个选学部分。

 在前面课程中,我们实现了一个简单(完全连接)的神经网络,我们使用反向传播来计算与更新参数的成本相关的导数。类似地,在循环神经网络中,我们可以计算与成本相关的导数,以便更新参数。反向传播的方程非常复杂,我们没有在视频中推导它们,但是,我们将在下面简要介绍它们。

3.1 - 基本的RNN网络的反向传播

 我们将开始计算基本的RNN单元的反向传播,我们先来看一下下面的图:

rnn_cell_backprop.png

**图 6**: RNN单元的反向传播。就像在全连接的神经网络中,代价函数$J$的导数通过遵循链式规则从RNN进行反向传播,链式法则也用于计算$(\frac{\partial J}{\partial W_{ax}},\frac{\partial J}{\partial W_{aa}},\frac{\partial J}{\partial b})$来更新参数$(W_{ax}, W_{aa}, b_a)$。

 单步反向传播的推导:为了计算rnn_cell_backward,我们需要计算下面的公式:

tanh \tanh 的导数是 1 tanh ( x ) 2 1 - \tanh(x)^2 ,类似的,对于 ( J W a x , J W a a , J b ) (\frac{\partial J}{\partial W_{ax}},\frac{\partial J}{\partial W_{aa}},\frac{\partial J}{\partial b}) 而言, tanh ( u ) \tanh(u) 的导数是 tanh ( u ) \tanh(u) is ( 1 tanh ( u ) 2 ) d u (1-\tanh(u)^2)du 。最后两个方程也遵循相同的规则,并用 tanh \tanh 导数导出。请注意,这样做是为了获得相同尺寸的匹配。

def rnn_cell_backward(da_next, cache):
    """
    实现基本的RNN单元的单步反向传播
    
    参数:
        da_next -- 关于下一个隐藏状态的损失的梯度。
        cache -- 字典类型,rnn_step_forward()的输出
        
    返回:
        gradients -- 字典,包含了以下参数:
                        dx -- 输入数据的梯度,维度为(n_x, m)
                        da_prev -- 上一隐藏层的隐藏状态,维度为(n_a, m)
                        dWax -- 输入到隐藏状态的权重的梯度,维度为(n_a, n_x)
                        dWaa -- 隐藏状态到隐藏状态的权重的梯度,维度为(n_a, n_a)
                        dba -- 偏置向量的梯度,维度为(n_a, 1)
    """
    # 获取cache 的值
    a_next, a_prev, xt, parameters = cache
    
    # 从 parameters 中获取参数
    Wax = parameters["Wax"]
    Waa = parameters["Waa"]
    Wya = parameters["Wya"]
    ba = parameters["ba"]
    by = parameters["by"]
    
    # 计算tanh相对于a_next的梯度.
    dtanh = (1 - np.square(a_next)) * da_next
    
    # 计算关于Wax损失的梯度
    dxt = np.dot(Wax.T,dtanh)
    dWax = np.dot(dtanh, xt.T)
    
    # 计算关于Waa损失的梯度
    da_prev = np.dot(Waa.T,dtanh)
    dWaa = np.dot(dtanh, a_prev.T)
    
    # 计算关于b损失的梯度
    dba = np.sum(dtanh, keepdims=True, axis=-1)
    
    # 保存这些梯度到字典内
    gradients = {"dxt": dxt, "da_prev": da_prev, "dWax": dWax, "dWaa": dWaa, "dba": dba}
    
    return gradients

我们来测试一下:

np.random.seed(1)
xt = np.random.randn(3,10)
a_prev = np.random.randn(5,10)
Wax = np.random.randn(5,3)
Waa = np.random.randn(5,5)
Wya = np.random.randn(2,5)
b = np.random.randn(5,1)
by = np.random.randn(2,1)
parameters = {"Wax": Wax, "Waa": Waa, "Wya": Wya, "ba": ba, "by": by}

a_next, yt, cache = rnn_cell_forward(xt, a_prev, parameters)

da_next = np.random.randn(5,10)
gradients = rnn_cell_backward(da_next, cache)
print("gradients[\"dxt\"][1][2] =", gradients["dxt"][1][2])
print("gradients[\"dxt\"].shape =", gradients["dxt"].shape)
print("gradients[\"da_prev\"][2][3] =", gradients["da_prev"][2][3])
print("gradients[\"da_prev\"].shape =", gradients["da_prev"].shape)
print("gradients[\"dWax\"][3][1] =", gradients["dWax"][3][1])
print("gradients[\"dWax\"].shape =", gradients["dWax"].shape)
print("gradients[\"dWaa\"][1][2] =", gradients["dWaa"][1][2])
print("gradients[\"dWaa\"].shape =", gradients["dWaa"].shape)
print("gradients[\"dba\"][4] =", gradients["dba"][4])
print("gradients[\"dba\"].shape =", gradients["dba"].shape)

测试结果:

gradients["dxt"][1][2] = -0.460564103059
gradients["dxt"].shape = (3, 10)
gradients["da_prev"][2][3] = 0.0842968653807
gradients["da_prev"].shape = (5, 10)
gradients["dWax"][3][1] = 0.393081873922
gradients["dWax"].shape = (5, 3)
gradients["dWaa"][1][2] = -0.28483955787
gradients["dWaa"].shape = (5, 5)
gradients["dba"][4] = [ 0.80517166]
gradients["dba"].shape = (5, 1)

 单步反向传播已经实现了,我们接下来就实现整个循环神经网络的反向传播:

def rnn_backward(da, caches):
    """
    在整个输入数据序列上实现RNN的反向传播
    
    参数:
        da -- 所有隐藏状态的梯度,维度为(n_a, m, T_x)
        caches -- 包含向前传播的信息的元组
    
    返回:    
        gradients -- 包含了梯度的字典:
                        dx -- 关于输入数据的梯度,维度为(n_x, m, T_x)
                        da0 -- 关于初始化隐藏状态的梯度,维度为(n_a, m)
                        dWax -- 关于输入权重的梯度,维度为(n_a, n_x)
                        dWaa -- 关于隐藏状态的权值的梯度,维度为(n_a, n_a)
                        dba -- 关于偏置的梯度,维度为(n_a, 1)
    """
    # 从caches中获取第一个cache(t=1)的值
    caches, x = caches
    a1, a0, x1, parameters = caches[0]
    
    # 获取da与x1的维度信息
    n_a, m, T_x = da.shape
    n_x, m = x1.shape
    
    # 初始化梯度
    dx = np.zeros([n_x, m, T_x])
    dWax = np.zeros([n_a, n_x])
    dWaa = np.zeros([n_a, n_a])
    dba = np.zeros([n_a, 1])
    da0 = np.zeros([n_a, m])
    da_prevt = np.zeros([n_a, m])
    
    # 处理所有时间步
    for t in reversed(range(T_x)):
        # 计算时间步“t”时的梯度
        gradients = rnn_cell_backward(da[:, :, t] + da_prevt, caches[t])
        
        #从梯度中获取导数
        dxt, da_prevt, dWaxt, dWaat, dbat = gradients["dxt"], gradients["da_prev"], gradients["dWax"], gradients["dWaa"], gradients["dba"]
        
        # 通过在时间步t添加它们的导数来增加关于全局导数的参数
        dx[:, :, t] = dxt
        dWax += dWaxt
        dWaa += dWaat
        dba += dbat
        
    #将 da0设置为a的梯度,该梯度已通过所有时间步骤进行反向传播
    da0 = da_prevt
    
    #保存这些梯度到字典内
    gradients = {"dx": dx, "da0": da0, "dWax": dWax, "dWaa": dWaa,"dba": dba}
    
    return gradients

我们来测试一下:

np.random.seed(1)
x = np.random.randn(3,10,4)
a0 = np.random.randn(5,10)
Wax = np.random.randn(5,3)
Waa = np.random.randn(5,5)
Wya = np.random.randn(2,5)
ba = np.random.randn(5,1)
by = np.random.randn(2,1)
parameters = {"Wax": Wax, "Waa": Waa, "Wya": Wya, "ba": ba, "by": by}
a, y, caches = rnn_forward(x, a0, parameters)
da = np.random.randn(5, 10, 4)
gradients = rnn_backward(da, caches)

print("gradients[\"dx\"][1][2] =", gradients["dx"][1][2])
print("gradients[\"dx\"].shape =", gradients["dx"].shape)
print("gradients[\"da0\"][2][3] =", gradients["da0"][2][3])
print("gradients[\"da0\"].shape =", gradients["da0"].shape)
print("gradients[\"dWax\"][3][1] =", gradients["dWax"][3][1])
print("gradients[\"dWax\"].shape =", gradients["dWax"].shape)
print("gradients[\"dWaa\"][1][2] =", gradients["dWaa"][1][2])
print("gradients[\"dWaa\"].shape =", gradients["dWaa"].shape)
print("gradients[\"dba\"][4] =", gradients["dba"][4])
print("gradients[\"dba\"].shape =", gradients["dba"].shape)

测试结果:

gradients["dx"][1][2] = [-2.07101689 -0.59255627  0.02466855  0.01483317]
gradients["dx"].shape = (3, 10, 4)
gradients["da0"][2][3] = -0.314942375127
gradients["da0"].shape = (5, 10)
gradients["dWax"][3][1] = 11.2641044965
gradients["dWax"].shape = (5, 3)
gradients["dWaa"][1][2] = 2.30333312658
gradients["dWaa"].shape = (5, 5)
gradients["dba"][4] = [-0.74747722]
gradients["dba"].shape = (5, 1)

3.2 - LSTM反向传播

3.2.1 - 单步反向传播

 LSTM反向传播比前向传播更复杂一些。我们已经提供了下面LSTM反向传播的所有方程。(如果你喜欢微积分的练习,你可以自己尝试去推导这些。)

3.2.2 门的导数

d Γ o t = d a n e x t tanh ( c n e x t ) Γ o t ( 1 Γ o t ) (7) d \Gamma_o^{\langle t \rangle} = da_{next}*\tanh(c_{next}) * \Gamma_o^{\langle t \rangle}*(1-\Gamma_o^{\langle t \rangle})\tag{7}

d c ~ t = d c n e x t Γ i t + Γ o t ( 1 tanh ( c n e x t ) 2 ) i t d a n e x t c ~ t ( 1 tanh ( c ~ ) 2 ) (8) d\tilde c^{\langle t \rangle} = dc_{next}*\Gamma_i^{\langle t \rangle}+ \Gamma_o^{\langle t \rangle} (1-\tanh(c_{next})^2) * i_t * da_{next} * \tilde c^{\langle t \rangle} * (1-\tanh(\tilde c)^2) \tag{8}

d Γ u t = d c n e x t c ~ t + Γ o t ( 1 tanh ( c n e x t ) 2 ) c ~ t d a n e x t Γ u t ( 1 Γ u t ) (9) d\Gamma_u^{\langle t \rangle} = dc_{next}*\tilde c^{\langle t \rangle} + \Gamma_o^{\langle t \rangle} (1-\tanh(c_{next})^2) * \tilde c^{\langle t \rangle} * da_{next}*\Gamma_u^{\langle t \rangle}*(1-\Gamma_u^{\langle t \rangle})\tag{9}

d Γ f t = d c n e x t c ~ p r e v + Γ o t ( 1 tanh ( c n e x t ) 2 ) c p r e v d a n e x t Γ f t ( 1 Γ f t ) (10) d\Gamma_f^{\langle t \rangle} = dc_{next}*\tilde c_{prev} + \Gamma_o^{\langle t \rangle} (1-\tanh(c_{next})^2) * c_{prev} * da_{next}*\Gamma_f^{\langle t \rangle}*(1-\Gamma_f^{\langle t \rangle})\tag{10}

3.2.3参数的导数

d W f = d Γ f t ( a p r e v x t ) T (11) dW_f = d\Gamma_f^{\langle t \rangle} * \begin{pmatrix} a_{prev} \\ x_t\end{pmatrix}^T \tag{11}
d W u = d Γ u t ( a p r e v x t ) T (12) dW_u = d\Gamma_u^{\langle t \rangle} * \begin{pmatrix} a_{prev} \\ x_t\end{pmatrix}^T \tag{12}
d W c = d c ~ t ( a p r e v x t ) T (13) dW_c = d\tilde c^{\langle t \rangle} * \begin{pmatrix} a_{prev} \\ x_t\end{pmatrix}^T \tag{13}
d W o = d Γ o t ( a p r e v x t ) T (14) dW_o = d\Gamma_o^{\langle t \rangle} * \begin{pmatrix} a_{prev} \\ x_t\end{pmatrix}^T \tag{14}

为了计算 d b f , d b u , d b c , d b o db_f, db_u, db_c, db_o 我们需要在 d Γ f t , d Γ u t , d c ~ t , d Γ o t d\Gamma_f^{\langle t \rangle}, d\Gamma_u^{\langle t \rangle}, d\tilde c^{\langle t \rangle}, d\Gamma_o^{\langle t \rangle} 上使用(axis = 1)来进行求和,需要注意的事情是要使用keep_dims = True
我们将计算关于先前隐藏状态、先前记忆状态和输入的导数。

d a p r e v = W f T d Γ f t + W u T d Γ u t + W c T d c ~ t + W o T d Γ o t (15) da_{prev} = W_f^T*d\Gamma_f^{\langle t \rangle} + W_u^T * d\Gamma_u^{\langle t \rangle}+ W_c^T * d\tilde c^{\langle t \rangle} + W_o^T * d\Gamma_o^{\langle t \rangle} \tag{15}
这里,方程13的权重是第一个 n_a, (比如 W f = W f [ : n a , : ] W_f = W_f[:n_a,:] 等等)

d c p r e v = d c n e x t Γ f t + Γ o t ( 1 tanh ( c n e x t ) 2 ) Γ f t d a n e x t (16) dc_{prev} = dc_{next}\Gamma_f^{\langle t \rangle} + \Gamma_o^{\langle t \rangle} * (1- \tanh(c_{next})^2)*\Gamma_f^{\langle t \rangle}*da_{next} \tag{16}

d x t = W f T d Γ f t + W u T d Γ u t + W c T d c ~ t + W o T d Γ o t (17) dx^{\langle t \rangle} = W_f^T*d\Gamma_f^{\langle t \rangle} + W_u^T * d\Gamma_u^{\langle t \rangle}+ W_c^T * d\tilde c_t + W_o^T * d\Gamma_o^{\langle t \rangle}\tag{17}
方程15的权值从n_a到结尾, (比如 W f = W f [ n a : , : ] W_f = W_f[n_a:,:] 等等)

def lstm_cell_backward(da_next, dc_next, cache):
    """
    实现LSTM的单步反向传播
    
    参数:
        da_next -- 下一个隐藏状态的梯度,维度为(n_a, m)
        dc_next -- 下一个单元状态的梯度,维度为(n_a, m)
        cache -- 来自前向传播的一些参数
        
    返回:
        gradients -- 包含了梯度信息的字典:
                        dxt -- 输入数据的梯度,维度为(n_x, m)
                        da_prev -- 先前的隐藏状态的梯度,维度为(n_a, m)
                        dc_prev -- 前的记忆状态的梯度,维度为(n_a, m, T_x)
                        dWf -- 遗忘门的权值的梯度,维度为(n_a, n_a + n_x)
                        dbf -- 遗忘门的偏置的梯度,维度为(n_a, 1)
                        dWi -- 更新门的权值的梯度,维度为(n_a, n_a + n_x)
                        dbi -- 更新门的偏置的梯度,维度为(n_a, 1)
                        dWc -- 第一个“tanh”的权值的梯度,维度为(n_a, n_a + n_x)
                        dbc -- 第一个“tanh”的偏置的梯度,维度为(n_a, n_a + n_x)
                        dWo -- 输出门的权值的梯度,维度为(n_a, n_a + n_x)
                        dbo -- 输出门的偏置的梯度,维度为(n_a, 1)
    """
    # 从cache中获取信息
    (a_next, c_next, a_prev, c_prev, ft, it, cct, ot, xt, parameters) = cache
    
    # 获取xt与a_next的维度信息
    n_x, m = xt.shape
    n_a, m = a_next.shape
    
    # 根据公式7-10来计算门的导数
    dot = da_next * np.tanh(c_next) * ot * (1 - ot)
    dcct = (dc_next * it + ot * (1 - np.square(np.tanh(c_next))) * it * da_next) * (1 - np.square(cct))
    dit = (dc_next * cct + ot * (1 - np.square(np.tanh(c_next))) * cct * da_next) * it * (1 - it)
    dft = (dc_next * c_prev + ot * (1 - np.square(np.tanh(c_next))) * c_prev * da_next) * ft * (1 - ft)
    
    # 根据公式11-14计算参数的导数
    concat = np.concatenate((a_prev, xt), axis=0).T
    dWf = np.dot(dft, concat)
    dWi = np.dot(dit, concat)
    dWc = np.dot(dcct, concat)
    dWo = np.dot(dot, concat)
    dbf = np.sum(dft,axis=1,keepdims=True)
    dbi = np.sum(dit,axis=1,keepdims=True)
    dbc = np.sum(dcct,axis=1,keepdims=True)
    dbo = np.sum(dot,axis=1,keepdims=True)
    
    
    # 使用公式15-17计算洗起来了隐藏状态、先前记忆状态、输入的导数。
    da_prev = np.dot(parameters["Wf"][:, :n_a].T, dft) + np.dot(parameters["Wc"][:, :n_a].T, dcct) +  np.dot(parameters["Wi"][:, :n_a].T, dit) + np.dot(parameters["Wo"][:, :n_a].T, dot)
        
    dc_prev = dc_next * ft + ot * (1 - np.square(np.tanh(c_next))) * ft * da_next
    
    dxt = np.dot(parameters["Wf"][:, n_a:].T, dft) + np.dot(parameters["Wc"][:, n_a:].T, dcct) +  np.dot(parameters["Wi"][:, n_a:].T, dit) + np.dot(parameters["Wo"][:, n_a:].T, dot)
    
    # 保存梯度信息到字典
    gradients = {"dxt": dxt, "da_prev": da_prev, "dc_prev": dc_prev, "dWf": dWf,"dbf": dbf, "dWi": dWi,"dbi": dbi,
                "dWc": dWc,"dbc": dbc, "dWo": dWo,"dbo": dbo}
    
    return gradients

我们来测试一下:

np.random.seed(1)
xt = np.random.randn(3,10)
a_prev = np.random.randn(5,10)
c_prev = np.random.randn(5,10)
Wf = np.random.randn(5, 5+3)
bf = np.random.randn(5,1)
Wi = np.random.randn(5, 5+3)
bi = np.random.randn(5,1)
Wo = np.random.randn(5, 5+3)
bo = np.random.randn(5,1)
Wc = np.random.randn(5, 5+3)
bc = np.random.randn(5,1)
Wy = np.random.randn(2,5)
by = np.random.randn(2,1)

parameters = {"Wf": Wf, "Wi": Wi, "Wo": Wo, "Wc": Wc, "Wy": Wy, "bf": bf, "bi": bi, "bo": bo, "bc": bc, "by": by}

a_next, c_next, yt, cache = lstm_cell_forward(xt, a_prev, c_prev, parameters)

da_next = np.random.randn(5,10)
dc_next = np.random.randn(5,10)
gradients = lstm_cell_backward(da_next, dc_next, cache)
print("gradients[\"dxt\"][1][2] =", gradients["dxt"][1][2])
print("gradients[\"dxt\"].shape =", gradients["dxt"].shape)
print("gradients[\"da_prev\"][2][3] =", gradients["da_prev"][2][3])
print("gradients[\"da_prev\"].shape =", gradients["da_prev"].shape)
print("gradients[\"dc_prev\"][2][3] =", gradients["dc_prev"][2][3])
print("gradients[\"dc_prev\"].shape =", gradients["dc_prev"].shape)
print("gradients[\"dWf\"][3][1] =", gradients["dWf"][3][1])
print("gradients[\"dWf\"].shape =", gradients["dWf"].shape)
print("gradients[\"dWi\"][1][2] =", gradients["dWi"][1][2])
print("gradients[\"dWi\"].shape =", gradients["dWi"].shape)
print("gradients[\"dWc\"][3][1] =", gradients["dWc"][3][1])
print("gradients[\"dWc\"].shape =", gradients["dWc"].shape)
print("gradients[\"dWo\"][1][2] =", gradients["dWo"][1][2])
print("gradients[\"dWo\"].shape =", gradients["dWo"].shape)
print("gradients[\"dbf\"][4] =", gradients["dbf"][4])
print("gradients[\"dbf\"].shape =", gradients["dbf"].shape)
print("gradients[\"dbi\"][4] =", gradients["dbi"][4])
print("gradients[\"dbi\"].shape =", gradients["dbi"].shape)
print("gradients[\"dbc\"][4] =", gradients["dbc"][4])
print("gradients[\"dbc\"].shape =", gradients["dbc"].shape)
print("gradients[\"dbo\"][4] =", gradients["dbo"][4])
print("gradients[\"dbo\"].shape =", gradients["dbo"].shape)

测试结果:

gradients["dxt"][1][2] = 3.23055911511
gradients["dxt"].shape = (3, 10)
gradients["da_prev"][2][3] = -0.0639621419711
gradients["da_prev"].shape = (5, 10)
gradients["dc_prev"][2][3] = 0.797522038797
gradients["dc_prev"].shape = (5, 10)
gradients["dWf"][3][1] = -0.147954838164
gradients["dWf"].shape = (5, 8)
gradients["dWi"][1][2] = 1.05749805523
gradients["dWi"].shape = (5, 8)
gradients["dWc"][3][1] = 2.30456216369
gradients["dWc"].shape = (5, 8)
gradients["dWo"][1][2] = 0.331311595289
gradients["dWo"].shape = (5, 8)
gradients["dbf"][4] = [ 0.18864637]
gradients["dbf"].shape = (5, 1)
gradients["dbi"][4] = [-0.40142491]
gradients["dbi"].shape = (5, 1)
gradients["dbc"][4] = [ 0.25587763]
gradients["dbc"].shape = (5, 1)
gradients["dbo"][4] = [ 0.13893342]
gradients["dbo"].shape = (5, 1)

3.3 - LSTM网络的反向传播

 这部分与我们在上面实现的rnn_backward函数非常相似。我们将首先创建与返回变量相同维度的变量。然后将遍历从结束到开始的所有时间步,并调用在每次迭代时为LSTM实现的单步反向传播功能。然后我们将通过单独求和来更新参数,最后返回一个带有新梯度的字典。

def lstm_backward(da, caches):
    
    """
    实现LSTM网络的反向传播
    
    参数:
        da -- 关于隐藏状态的梯度,维度为(n_a, m, T_x)
        cachses -- 前向传播保存的信息
    
    返回:
        gradients -- 包含了梯度信息的字典:
                        dx -- 输入数据的梯度,维度为(n_x, m,T_x)
                        da0 -- 先前的隐藏状态的梯度,维度为(n_a, m)
                        dWf -- 遗忘门的权值的梯度,维度为(n_a, n_a + n_x)
                        dbf -- 遗忘门的偏置的梯度,维度为(n_a, 1)
                        dWi -- 更新门的权值的梯度,维度为(n_a, n_a + n_x)
                        dbi -- 更新门的偏置的梯度,维度为(n_a, 1)
                        dWc -- 第一个“tanh”的权值的梯度,维度为(n_a, n_a + n_x)
                        dbc -- 第一个“tanh”的偏置的梯度,维度为(n_a, n_a + n_x)
                        dWo -- 输出门的权值的梯度,维度为(n_a, n_a + n_x)
                        dbo -- 输出门的偏置的梯度,维度为(n_a, 1)
        
    """

    # 从caches中获取第一个cache(t=1)的值
    caches, x = caches
    (a1, c1, a0, c0, f1, i1, cc1, o1, x1, parameters) = caches[0]
    
    # 获取da与x1的维度信息
    n_a, m, T_x = da.shape
    n_x, m = x1.shape
    
    # 初始化梯度
    dx = np.zeros([n_x, m, T_x])
    da0 = np.zeros([n_a, m])
    da_prevt = np.zeros([n_a, m])
    dc_prevt = np.zeros([n_a, m])
    dWf = np.zeros([n_a, n_a + n_x])
    dWi = np.zeros([n_a, n_a + n_x])
    dWc = np.zeros([n_a, n_a + n_x])
    dWo = np.zeros([n_a, n_a + n_x])
    dbf = np.zeros([n_a, 1])
    dbi = np.zeros([n_a, 1])
    dbc = np.zeros([n_a, 1])
    dbo = np.zeros([n_a, 1])
    
    # 处理所有时间步
    for t in reversed(range(T_x)):
        # 使用lstm_cell_backward函数计算所有梯度
        gradients = lstm_cell_backward(da[:,:,t],dc_prevt,caches[t])
        # 保存相关参数
        dx[:,:,t] = gradients['dxt']
        dWf = dWf+gradients['dWf']
        dWi = dWi+gradients['dWi']
        dWc = dWc+gradients['dWc']
        dWo = dWo+gradients['dWo']
        dbf = dbf+gradients['dbf']
        dbi = dbi+gradients['dbi']
        dbc = dbc+gradients['dbc']
        dbo = dbo+gradients['dbo']
    # 将第一个激活的梯度设置为反向传播的梯度da_prev。
    da0 = gradients['da_prev']

    # 保存所有梯度到字典变量内
    gradients = {"dx": dx, "da0": da0, "dWf": dWf,"dbf": dbf, "dWi": dWi,"dbi": dbi,
                "dWc": dWc,"dbc": dbc, "dWo": dWo,"dbo": dbo}
    
    return gradients

我们来测试一下:

np.random.seed(1)
x = np.random.randn(3,10,7)
a0 = np.random.randn(5,10)
Wf = np.random.randn(5, 5+3)
bf = np.random.randn(5,1)
Wi = np.random.randn(5, 5+3)
bi = np.random.randn(5,1)
Wo = np.random.randn(5, 5+3)
bo = np.random.randn(5,1)
Wc = np.random.randn(5, 5+3)
bc = np.random.randn(5,1)

parameters = {"Wf": Wf, "Wi": Wi, "Wo": Wo, "Wc": Wc, "Wy": Wy, "bf": bf, "bi": bi, "bo": bo, "bc": bc, "by": by}

a, y, c, caches = lstm_forward(x, a0, parameters)

da = np.random.randn(5, 10, 4)
gradients = lstm_backward(da, caches)

print("gradients[\"dx\"][1][2] =", gradients["dx"][1][2])
print("gradients[\"dx\"].shape =", gradients["dx"].shape)
print("gradients[\"da0\"][2][3] =", gradients["da0"][2][3])
print("gradients[\"da0\"].shape =", gradients["da0"].shape)
print("gradients[\"dWf\"][3][1] =", gradients["dWf"][3][1])
print("gradients[\"dWf\"].shape =", gradients["dWf"].shape)
print("gradients[\"dWi\"][1][2] =", gradients["dWi"][1][2])
print("gradients[\"dWi\"].shape =", gradients["dWi"].shape)
print("gradients[\"dWc\"][3][1] =", gradients["dWc"][3][1])
print("gradients[\"dWc\"].shape =", gradients["dWc"].shape)
print("gradients[\"dWo\"][1][2] =", gradients["dWo"][1][2])
print("gradients[\"dWo\"].shape =", gradients["dWo"].shape)
print("gradients[\"dbf\"][4] =", gradients["dbf"][4])
print("gradients[\"dbf\"].shape =", gradients["dbf"].shape)
print("gradients[\"dbi\"][4] =", gradients["dbi"][4])
print("gradients[\"dbi\"].shape =", gradients["dbi"].shape)
print("gradients[\"dbc\"][4] =", gradients["dbc"][4])
print("gradients[\"dbc\"].shape =", gradients["dbc"].shape)
print("gradients[\"dbo\"][4] =", gradients["dbo"][4])
print("gradients[\"dbo\"].shape =", gradients["dbo"].shape)

测试结果:

radients["dx"][1][2] = [-0.00173313  0.08287442 -0.30545663 -0.43281115]
gradients["dx"].shape = (3, 10, 4)
gradients["da0"][2][3] = -0.095911501954
gradients["da0"].shape = (5, 10)
gradients["dWf"][3][1] = -0.0698198561274
gradients["dWf"].shape = (5, 8)
gradients["dWi"][1][2] = 0.102371820249
gradients["dWi"].shape = (5, 8)
gradients["dWc"][3][1] = -0.0624983794927
gradients["dWc"].shape = (5, 8)
gradients["dWo"][1][2] = 0.0484389131444
gradients["dWo"].shape = (5, 8)
gradients["dbf"][4] = [-0.0565788]
gradients["dbf"].shape = (5, 1)
gradients["dbi"][4] = [-0.15399065]
gradients["dbi"].shape = (5, 1)
gradients["dbc"][4] = [-0.29691142]
gradients["dbc"].shape = (5, 1)
gradients["dbo"][4] = [-0.29798344]
gradients["dbo"].shape = (5, 1)

 最基础的东西算是搭建好了,我们进行下一部分:


字符级语言模型 - 恐龙岛

 欢迎来到恐龙岛,恐龙生活于在6500万年前,现在研究人员在试着复活恐龙,而你的任务就是给恐龙命名,如果一只恐龙不喜欢它的名字,它可能会狂躁不安,所以你要谨慎选择。

 你的助手已经收集了他们能够找到的所有恐龙名字,并编入了这个数据集,为了构建字符级语言模型来生成新的名称,你的模型将学习不同的名称模式,并随机生成新的名字。希望这个算法能让你和你的团队远离恐龙的愤怒。

 在这里你将学习到:

  • 如何存储文本数据以便使用RNN进行处理。

  • 如何合成数据,通过每次采样预测,并将其传递给下一个rnn单元。

  • 如何构建字符级文本生成循环神经网络。

  • 为什么梯度修剪很重要?

 我们将首先加载我们在rnn_utils中提供的一些函数。具体地说,我们可以使用rnn_forwardrnn_backward等函数,这些函数与前面实现的函数相同。

import numpy as np
import random
import time
import cllm_utils

1 - 问题描述

1.1 - 数据集与预处理

 我们先来读取恐龙名称的数据集,创建一个唯一字符列表(如AZ),并计算数据集和词汇量大小。

# 获取名称
data = open("dinos.txt", "r").read()

# 转化为小写字符
data = data.lower()

# 转化为无序且不重复的元素列表
chars = list(set(data))

# 获取大小信息
data_size, vocab_size = len(data), len(chars)

print(chars)
print("共计有%d个字符,唯一字符有%d个"%(data_size,vocab_size))

执行结果:

['o', 'n', 'm', 'j', 's', 'q', 'f', 'k', 'e', 't', 'z', 'c', 'a', '\n', 'i', 'u', 'l', 'h', 'b', 'w', 'v', 'y', 'r', 'p', 'x', 'g', 'd']
共计有19909个字符,唯一字符有27个

 这些字符是a-z(26个英文字符)加上“\n”(换行字符),在这里换行字符起到了在视频中类似的EOS(句子结尾)的作用,这里表示了名字的结束而不是句子的结尾。下面我们将创建一个字典,每个字符映射到0-26的索引,然后再创建一个字典,它将该字典将每个索引映射回相应的字符字符,它会帮助我们找出softmax层的概率分布输出中的字符。我们来创建char_to_ixix_to_char字典。

char_to_ix = {ch:i for i, ch in enumerate(sorted(chars))}
ix_to_char = {i:ch for i, ch in enumerate(sorted(chars))}

print(char_to_ix)
print(ix_to_char)

执行结果:

{'\n': 0, 'a': 1, 'b': 2, 'c': 3, 'd': 4, 'e': 5, 'f': 6, 'g': 7, 'h': 8, 'i': 9, 'j': 10, 'k': 11, 'l': 12, 'm': 13, 'n': 14, 'o': 15, 'p': 16, 'q': 17, 'r': 18, 's': 19, 't': 20, 'u': 21, 'v': 22, 'w': 23, 'x': 24, 'y': 25, 'z': 26}

{0: '\n', 1: 'a', 2: 'b', 3: 'c', 4: 'd', 5: 'e', 6: 'f', 7: 'g', 8: 'h', 9: 'i', 10: 'j', 11: 'k', 12: 'l', 13: 'm', 14: 'n', 15: 'o', 16: 'p', 17: 'q', 18: 'r', 19: 's', 20: 't', 21: 'u', 22: 'v', 23: 'w', 24: 'x', 25: 'y', 26: 'z'}

1.2 - 模型回顾

 模型的结构如下:

  • 初始化参数

  • 循环:

    • 前向传播计算损失

    • 反向传播计算关于损失的梯度

    • 修剪梯度以免梯度爆炸

    • 用梯度下降更新规则更新参数。

  • 返回学习后了的参数

rnn.png

**图 1**: 循环神经网络, 与之前的“一步步搭建神经网络”的图是一样的。

 在每个时间步,RNN会预测给定字符的下一个字符是什么。数据集 X = ( x 1 , x 2 , . . . , x T x ) X = (x^{\langle 1 \rangle}, x^{\langle 2 \rangle}, ..., x^{\langle T_x \rangle}) 是一个列表类型的字符训练集,同时 Y = ( y 1 , y 2 , . . . , y T x ) Y = (y^{\langle 1 \rangle}, y^{\langle 2 \rangle}, ..., y^{\langle T_x \rangle}) 在每个时间步 t t 亦是如此,因此, y t = x t + 1 y^{\langle t \rangle} = x^{\langle t+1 \rangle}

2 - 构建模型中的模块

 在这部分,我们将来构建整个模型中的两个重要的模块:

  • 梯度修剪:避免梯度爆炸

  • 取样:一种用来产生字符的技术

2.1 梯度修剪

 在这里,我们将实现在优化循环中调用的clip函数。回想一下,整个循环结构通常包括前向传播、成本计算、反向传播和参数更新。在更新参数之前,我们将在需要时执行梯度修剪,以确保我们的梯度不是“爆炸”的。

 接下来我们将实现一个修剪函数,该函数输入一个梯度字典输出一个已经修剪过了的梯度。有很多的方法来修剪梯度,我们在这里使用一个比较简单的方法。梯度向量的每一个元素都被限制在 [ N N ] [-N,N] 的范围,通俗的说,有一个maxValue(比如10),如果梯度的任何值大于10,那么它将被设置为10,如果梯度的任何值小于-10,那么它将被设置为-10,如果它在-10与10之间,那么它将不变。

clip.png

**图 2**:在网络进入轻微的“梯度爆炸”的问题的情况下,使用梯度修剪和无梯度修剪的可视化图。

 我们来实现下面的函数来返回一个修剪过后的梯度字典,函数接受最大阈值,并返回修剪后的梯度。

def clip(gradients, maxValue):
    """
    使用maxValue来修剪梯度
    
    参数:
        gradients -- 字典类型,包含了以下参数:"dWaa", "dWax", "dWya", "db", "dby"
        maxValue -- 阈值,把梯度值限制在[-maxValue, maxValue]内
        
    返回:
        gradients -- 修剪后的梯度
    """
    # 获取参数
    dWaa, dWax, dWya, db, dby = gradients['dWaa'], gradients['dWax'], gradients['dWya'], gradients['db'], gradients['dby']
    
    # 梯度修剪
    for gradient in [dWaa, dWax, dWya, db, dby]:
        np.clip(gradient, -maxValue, maxValue, out=gradient)

    gradients = {"dWaa": dWaa, "dWax": dWax, "dWya": dWya, "db": db, "dby": dby}
    
    return gradients

我们来测试一下:

np.random.seed(3)
dWax = np.random.randn(5,3)*10
dWaa = np.random.randn(5,5)*10
dWya = np.random.randn(2,5)*10
db = np.random.randn(5,1)*10
dby = np.random.randn(2,1)*10
gradients = {"dWax": dWax, "dWaa": dWaa, "dWya": dWya, "db": db, "dby": dby}
gradients = clip(gradients, 10)
print("gradients[\"dWaa\"][1][2] =", gradients["dWaa"][1][2])
print("gradients[\"dWax\"][3][1] =", gradients["dWax"][3][1])
print("gradients[\"dWya\"][1][2] =", gradients["dWya"][1][2])
print("gradients[\"db\"][4] =", gradients["db"][4])
print("gradients[\"dby\"][1] =", gradients["dby"][1])

测试结果:

gradients["dWaa"][1][2] = 10.0
gradients["dWax"][3][1] = -10.0
gradients["dWya"][1][2] = 0.29713815361
gradients["db"][4] = [ 10.]
gradients["dby"][1] = [ 8.45833407]

2.2 - 采样

 现在假设我们的模型已经训练过了,我们希望生成新的文本,生成的过程如下图:

dinos3.png

**图 3**: 在这幅图中,我们假设模型已经经过了训练。我们在第一步传入$x^{\langle 1\rangle} = \vec{0}$,然后让网络一次对一个字符进行采样。

 现在我们来实现sample函数,它由以下四步构成:

  • 步骤1:网络的第一个“虚”输入 x 1 = 0 x^{\langle 1 \rangle} = \vec{0} (零向量),这是在生成字符之前的默认输入,同时我们设置 a 0 = 0 a^{\langle 0 \rangle} = \vec{0}

  • 步骤2:运行一次前向传播,然后得到 a 1 a^{\langle 1 \rangle} y ^ 1 \hat{y}^{\langle 1 \rangle} . 公式如下:

a t + 1 = tanh ( W a x x t + W a a a t + b ) (1) a^{\langle t+1 \rangle} = \tanh(W_{ax} x^{\langle t \rangle } + W_{aa} a^{\langle t \rangle } + b)\tag{1}

z t + 1 = W y a a t + 1 + b y (2) z^{\langle t + 1 \rangle } = W_{ya} a^{\langle t + 1 \rangle } + b_y \tag{2}

y ^ t + 1 = s o f t m a x ( z t + 1 ) (3) \hat{y}^{\langle t+1 \rangle } = softmax(z^{\langle t + 1 \rangle })\tag{3}

 需要注意的是 y ^ t + 1 \hat{y}^{\langle t+1 \rangle} 是一个softmax概率向量(其下的值在0到1之间,总和为1), y ^ i t + 1 \hat{y}^{\langle t+1 \rangle}_i 表示索引“i”的字符是下一个字符的概率。

  • 步骤3:采样:根据 y ^ t + 1 \hat{y}^{\langle t+1 \rangle} 指定的概率分布选择下一个字符的索引,假如 y ^ i t + 1 = 0.16 \hat{y}^{\langle t+1 \rangle}_i = 0.16 ,那么选择索引“i”的概率为 16 16% ,为了实现它,我们可以使用np.random.choice函数,下面是np.random.choice()使用的例子:
np.random.seed(0)
p = np.array([0.1, 0.0, 0.7, 0.2])
index = np.random.choice([0, 1, 2, 3], p = p.ravel())

这意味着你将根据分布选择索引:
P ( i n d e x = 0 ) = 0.1 , P ( i n d e x = 1 ) = 0.0 , P ( i n d e x = 2 ) = 0.7 , P ( i n d e x = 3 ) = 0.2 P(index = 0) = 0.1, P(index = 1) = 0.0, P(index = 2) = 0.7, P(index = 3) = 0.2 .

  • 步骤4:在sample()中实现的最后一步是用 x t + 1 x^{\langle t + 1 \rangle } 的值覆盖变量x(当前存储 x t x^{\langle t \rangle } )。我们将通过创建一个与我们所选择的字符相对应的一个独热向量来表示 x t + 1 x^{\langle t + 1 \rangle } 。然后在步骤1中向前传播 x t + 1 x^{\langle t + 1 \rangle } ,并不断重复这个过程,直到得到一个“\n”字符,表明已经到达恐龙名称的末尾。
def sample(parameters, char_to_is, seed):
    """
    根据RNN输出的概率分布序列对字符序列进行采样
    
    参数:
        parameters -- 包含了Waa, Wax, Wya, by, b的字典
        char_to_ix -- 字符映射到索引的字典
        seed -- 随机种子
        
    返回:
        indices -- 包含采样字符索引的长度为n的列表。
    """
    
    # 从parameters 中获取参数
    Waa, Wax, Wya, by, b = parameters['Waa'], parameters['Wax'], parameters['Wya'], parameters['by'], parameters['b']
    vocab_size = by.shape[0]
    n_a = Waa.shape[1]
    
    # 步骤1 
    ## 创建独热向量x
    x = np.zeros((vocab_size,1))
    
    ## 使用0初始化a_prev
    a_prev = np.zeros((n_a,1))
    
    # 创建索引的空列表,这是包含要生成的字符的索引的列表。
    indices = []
    
    # IDX是检测换行符的标志,我们将其初始化为-1。
    idx = -1
    
    # 循环遍历时间步骤t。在每个时间步中,从概率分布中抽取一个字符,
    # 并将其索引附加到“indices”上,如果我们达到50个字符,
    #(我们应该不太可能有一个训练好的模型),我们将停止循环,这有助于调试并防止进入无限循环
    counter = 0
    newline_character = char_to_ix["\n"]
    
    while (idx != newline_character and counter < 50):
        # 步骤2:使用公式1、2、3进行前向传播
        a = np.tanh(np.dot(Wax, x) + np.dot(Waa, a_prev) + b)
        z = np.dot(Wya, a) + by
        y = cllm_utils.softmax(z)
        
        # 设定随机种子
        np.random.seed(counter + seed)
        
        # 步骤3:从概率分布y中抽取词汇表中字符的索引
        idx = np.random.choice(list(range(vocab_size)), p=y.ravel())
        
        # 添加到索引中
        indices.append(idx)
        
        # 步骤4:将输入字符重写为与采样索引对应的字符。
        x = np.zeros((vocab_size,1))
        x[idx] = 1
        
        # 更新a_prev为a
        a_prev = a 
        
        # 累加器
        seed += 1
        counter +=1
    
    if(counter == 50):
        indices.append(char_to_ix["\n"])
    
    return indices
        

我们来测试一下:

np.random.seed(2)
_, n_a = 20, 100
Wax, Waa, Wya = np.random.randn(n_a, vocab_size), np.random.randn(n_a, n_a), np.random.randn(vocab_size, n_a)
b, by = np.random.randn(n_a, 1), np.random.randn(vocab_size, 1)
parameters = {"Wax": Wax, "Waa": Waa, "Wya": Wya, "b": b, "by": by}


indices = sample(parameters, char_to_ix, 0)
print("Sampling:")
print("list of sampled indices:", indices)
print("list of sampled characters:", [ix_to_char[i] for i in indices])

测试结果:

Sampling:
list of sampled indices: [12, 17, 24, 14, 13, 9, 10, 22, 24, 6, 13, 11, 12, 6, 21, 15, 21, 14, 3, 2, 1, 21, 18, 24, 7, 25, 6, 25, 18, 10, 16, 2, 3, 8, 15, 12, 11, 7, 1, 12, 10, 2, 7, 7, 11, 17, 24, 12, 12, 0, 0]
list of sampled characters: ['l', 'q', 'x', 'n', 'm', 'i', 'j', 'v', 'x', 'f', 'm', 'k', 'l', 'f', 'u', 'o', 'u', 'n', 'c', 'b', 'a', 'u', 'r', 'x', 'g', 'y', 'f', 'y', 'r', 'j', 'p', 'b', 'c', 'h', 'o', 'l', 'k', 'g', 'a', 'l', 'j', 'b', 'g', 'g', 'k', 'q', 'x', 'l', 'l', '\n', '\n']

3 - 构建语言模型

3.1 - 梯度下降

 在这里,我们将实现一个执行随机梯度下降的一个步骤的函数(带有梯度修剪)。我们将一次训练一个样本,所以优化算法将是随机梯度下降,这里是RNN的一个通用的优化循环的步骤:

  • 前向传播计算损失

  • 反向传播计算关于参数的梯度损失

  • 修剪梯度

  • 使用梯度下降更新参数

 我们来实现这一优化过程(单步随机梯度下降),这里我们提供了一些函数:

# 示例,请勿执行。
def rnn_forward(X, Y, a_prev, parameters):
    """
    通过RNN进行前向传播,计算交叉熵损失。

    它返回损失的值以及存储在反向传播中使用的“缓存”值。
    """
    ....
    return loss, cache
    
def rnn_backward(X, Y, parameters, cache):
    """ 
    通过时间进行反向传播,计算相对于参数的梯度损失。它还返回所有隐藏的状态
    """
    ...
    return gradients, a

def update_parameters(parameters, gradients, learning_rate):
    """
    Updates parameters using the Gradient Descent Update Rule
    """
    ...
    return parameters

我们来构建优化函数:

def optimize(X, Y, a_prev, parameters, learning_rate = 0.01):
    """
    执行训练模型的单步优化。
    
    参数:
        X -- 整数列表,其中每个整数映射到词汇表中的字符。
        Y -- 整数列表,与X完全相同,但向左移动了一个索引。
        a_prev -- 上一个隐藏状态
        parameters -- 字典,包含了以下参数:
                        Wax -- 权重矩阵乘以输入,维度为(n_a, n_x)
                        Waa -- 权重矩阵乘以隐藏状态,维度为(n_a, n_a)
                        Wya -- 隐藏状态与输出相关的权重矩阵,维度为(n_y, n_a)
                        b -- 偏置,维度为(n_a, 1)
                        by -- 隐藏状态与输出相关的权重偏置,维度为(n_y, 1)
        learning_rate -- 模型学习的速率
    
    返回:
        loss -- 损失函数的值(交叉熵损失)
        gradients -- 字典,包含了以下参数:
                        dWax -- 输入到隐藏的权值的梯度,维度为(n_a, n_x)
                        dWaa -- 隐藏到隐藏的权值的梯度,维度为(n_a, n_a)
                        dWya -- 隐藏到输出的权值的梯度,维度为(n_y, n_a)
                        db -- 偏置的梯度,维度为(n_a, 1)
                        dby -- 输出偏置向量的梯度,维度为(n_y, 1)
        a[len(X)-1] -- 最后的隐藏状态,维度为(n_a, 1)
    """
    
    # 前向传播
    loss, cache = cllm_utils.rnn_forward(X, Y, a_prev, parameters)
    
    # 反向传播
    gradients, a = cllm_utils.rnn_backward(X, Y, parameters, cache)
    
    # 梯度修剪,[-5 , 5]
    gradients = clip(gradients,5)
    
    # 更新参数
    parameters = cllm_utils.update_parameters(parameters,gradients,learning_rate)
    
    return loss, gradients, a[len(X)-1]
    

我们来测试一下:

np.random.seed(1)
vocab_size, n_a = 27, 100
a_prev = np.random.randn(n_a, 1)
Wax, Waa, Wya = np.random.randn(n_a, vocab_size), np.random.randn(n_a, n_a), np.random.randn(vocab_size, n_a)
b, by = np.random.randn(n_a, 1), np.random.randn(vocab_size, 1)
parameters = {"Wax": Wax, "Waa": Waa, "Wya": Wya, "b": b, "by": by}
X = [12,3,5,11,22,3]
Y = [4,14,11,22,25, 26]

loss, gradients, a_last = optimize(X, Y, a_prev, parameters, learning_rate = 0.01)
print("Loss =", loss)
print("gradients[\"dWaa\"][1][2] =", gradients["dWaa"][1][2])
print("np.argmax(gradients[\"dWax\"]) =", np.argmax(gradients["dWax"]))
print("gradients[\"dWya\"][1][2] =", gradients["dWya"][1][2])
print("gradients[\"db\"][4] =", gradients["db"][4])
print("gradients[\"dby\"][1] =", gradients["dby"][1])
print("a_last[4] =", a_last[4])

测试结果:

Loss = 126.503975722
gradients["dWaa"][1][2] = 0.194709315347
np.argmax(gradients["dWax"]) = 93
gradients["dWya"][1][2] = -0.007773876032
gradients["db"][4] = [-0.06809825]
gradients["dby"][1] = [ 0.01538192]
a_last[4] = [-1.]

3.2 - 训练模型

 给定恐龙名称的数据集,我们使用数据集的每一行(一个名称)作为一个训练样本。每100步随机梯度下降,你将抽样10个随机选择的名字,看看算法是怎么做的。记住要打乱数据集,以便随机梯度下降以随机顺序访问样本。当examples[index]包含一个恐龙名称(String)时,为了创建一个样本(X,Y),你可以使用这个:

        index = j % len(examples)
        X = [None] + [char_to_ix[ch] for ch in examples[index]] 
        Y = X[1:] + [char_to_ix["\n"]]

 需要注意的是我们使用了index= j % len(examples),其中= 1....num_iterations,为了确保examples[index]总是有效的(index小于len(examples)),rnn_forward()会将X的第一个值None解释为 x 0 = 0 x^{\langle 0 \rangle} = \vec{0} 。此外,为了确保Y等于X,会向左移动一步,并添加一个附加的“\n”以表示恐龙名称的结束。

def model(data, ix_to_char, char_to_ix, num_iterations=3500, 
          n_a=50, dino_names=7,vocab_size=27):
    """
    训练模型并生成恐龙名字
    
    参数:
        data -- 语料库
        ix_to_char -- 索引映射字符字典
        char_to_ix -- 字符映射索引字典
        num_iterations -- 迭代次数
        n_a -- RNN单元数量
        dino_names -- 每次迭代中采样的数量
        vocab_size -- 在文本中的唯一字符的数量
    
    返回:
        parameters -- 学习后了的参数
    """
    
    # 从vocab_size中获取n_x、n_y
    n_x, n_y = vocab_size, vocab_size
    
    # 初始化参数
    parameters = cllm_utils.initialize_parameters(n_a, n_x, n_y)
    
    # 初始化损失
    loss = cllm_utils.get_initial_loss(vocab_size, dino_names)
    
    # 构建恐龙名称列表
    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"]]
        
        # 执行单步优化:前向传播 -> 反向传播 -> 梯度修剪 -> 更新参数
        # 选择学习率为0.01
        curr_loss, gradients, a_prev = optimize(X, Y, a_prev, parameters)
        
        # 使用延迟来保持损失平滑,这是为了加速训练。
        loss = cllm_utils.smooth(loss, curr_loss)
        
        # 每2000次迭代,通过sample()生成“\n”字符,检查模型是否学习正确
        if j % 2000 == 0:
            print("第" + str(j+1) + "次迭代,损失值为:" + str(loss))
            
            seed = 0
            for name in range(dino_names):
                # 采样
                sampled_indices = sample(parameters, char_to_ix, seed)
                cllm_utils.print_sample(sampled_indices, ix_to_char)
                
                # 为了得到相同的效果,随机种子+1
                seed += 1
            
            print("\n")
    return parameters

我们来训练一下:

#开始时间
start_time = time.clock()

#开始训练
parameters = model(data, ix_to_char, char_to_ix, num_iterations=3500)

#结束时间
end_time = time.clock()

#计算时差
minium = end_time - start_time

print("执行了:" + str(int(minium / 60)) + "分" + str(int(minium%60)) + "秒")

执行结果:

第1次迭代,损失值为:23.0873360855
Nkzxwtdmfqoeyhsqwasjkjvu
Kneb
Kzxwtdmfqoeyhsqwasjkjvu
Neb
Zxwtdmfqoeyhsqwasjkjvu
Eb
Xwtdmfqoeyhsqwasjkjvu


第2001次迭代,损失值为:27.8841604914
Liusskeomnolxeros
Hmdaairus
Hytroligoraurus
Lecalosapaus
Xusicikoraurus
Abalpsamantisaurus
Tpraneronxeros


执行了:0分3秒

4 - 写出莎士比亚风格的文字(选学

 这部分是可选的,类似的(但更复杂的)任务是产生莎士比亚诗歌。不用学习恐龙名字的数据集,你可以使用莎士比亚诗集。使用LSTM单元,我们可以学习跨越文本中许多字符的较长时间的依赖关系,例如,出现在某个序列的某个字符会影响在该序列后面的不同字符。由于恐龙名字很短,这些长期的依赖性与恐龙名字并不那么重要。我们用Keras实现了莎士比亚诗歌生成器,我们先来加载所需的包和模型,这可能需要几分钟。
shakespeare.jpg

让我们成为诗人!
#开始时间
start_time = time.clock()

from keras.callbacks import LambdaCallback
from keras.models import Model, load_model, Sequential
from keras.layers import Dense, Activation, Dropout, Input, Masking
from keras.layers import LSTM
from keras.utils.data_utils import get_file
from keras.preprocessing.sequence import pad_sequences
from shakespeare_utils import *
import sys
import io

#结束时间
end_time = time.clock()

#计算时差
minium = end_time - start_time

print("执行了:" + str(int(minium / 60)) + "分" + str(int(minium%60)) + "秒")

执行结果:

Loading text data...
Creating training set...
number of training examples: 31412
Vectorizing training set...
Loading model...
执行了:0分8秒

 为了节省时间,我们已经为莎士比亚诗集《十四行诗》模型训练了1000代,让我们再训练一下这个模型。当它完成了一代的训练——这也需要几分钟——你可以运行generate_output,它会提示你输入(小于40个字符)。这首诗将从你的句子开始,我们的RNN-Shakespeare将为你完成这首诗的其余部分!例如,是试着输入“Forsooth this maketh no sense”(不要输入引号)。取决于最后是否包含空格,您的结果也可能有所不同——您可以同时尝试这两种方式,也可以尝试其他输入。

print_callback = LambdaCallback(on_epoch_end=on_epoch_end)

model.fit(x, y, batch_size=128, epochs=1, callbacks=[print_callback])

执行结果:

Epoch 1/1
31412/31412 [==============================] - 27s 846us/step - loss: 2.7274
# 运行此代码尝试不同的输入,而不必重新训练模型。
generate_output() #博主在这里输入hello

执行结果:

Write the beginning of your poem, the Shakespeare machine will complete it. Your input is: hello 


Here is your poem: 

hello s ate.

how wile is seyse to vimither part,
no love and do pros of eyes for show,
thy bourt duse dore bes in his band
awharces pery arey apery denired,
hibfers beer bespeck new shome is dide in shill,
to ars' thy loth le's all my vide berend,
that delencly pap steloted and'ing chimthof shoud,
grot, with meauty's whether on my epsiy to drase.
our the with make i prouy my drere.
for my canvere's med

想不想看看模型细节呢?

#------------用于绘制模型细节,可选--------------#
from IPython.display import SVG
from keras.utils.vis_utils import model_to_dot
from keras.utils import plot_model

%matplotlib inline
plot_model(model, to_file='shakespeare.png')
SVG(model_to_dot(model).create(prog='dot', format='svg'))
#------------------------------------------------#

执行结果:
shakespeare.png

RNN莎士比亚模型与你为恐龙名字建立的模型非常相似。唯一的主要区别是:

  • LSTM代替基本的RNN捕获更长的距离依赖关系

  • 该模型是一个更深层的LSTM模型(2层)。

  • 使用keras而不是Python来简化代码

这一部分算是告一段落,下面我们来生成音乐:


用LSTM网络即兴独奏爵士乐

 在这里我们要实现使用LSTM生成音乐的模型,你甚至可以在结束时听你自己的音乐,接下来你将会学习到:

  • 使用LSTM生成音乐

  • 使用深度学习生成你自己的爵士乐

现在我们先来加载库,其中,music21可能不在你的环境内,你需要在命令行中执行pip install msgpack以及pip install music21来获取。

from keras.models import load_model, Model
from keras.layers import Dense, Activation, Dropout, Input, LSTM, Reshape, Lambda, RepeatVector
from keras.initializers import glorot_uniform
from keras.utils import to_categorical
from keras.optimizers import Adam
from keras import backend as K
import numpy as np
import IPython
import sys
from music21 import *
from grammar import *
from qa import *
from preprocess import * 
from music_utils import *
from data_utils import *

1 - 问题描述

 你想为朋友的生日创作一段爵士乐,然而,你不会任何乐器、乐理,幸运的是,你了解深度学习并将使用LSTM网络来解决这个问题。
jazz.jpg

1.1 - 数据集

你将在爵士乐语料库上训练模型,运行下面的代码,听一下训练集的音频片段:

IPython.display.Audio('./data/30s_seq.mp3')

执行结果:因CSDN限制,无法播放音乐,您可以下载资料包自行聆听。

 我们已经对音乐数据进行了预处理,以根据音乐“值”来呈现它。你可以非正式地将每个“值”视为一个音符,其中包括振幅和持续时间。举个例子,如果你按下特定的钢琴键0.5秒,那么你刚刚弹奏了一个音符。在音乐理论中,“值”实际上比这更复杂 - 具体而言,它还收集了同时播放多个音符所需的信息。例如,在播放乐曲时,你可以同时按下两个钢琴键(同时播放多个音符会产生所谓的“和弦”)。但是我们不需要担心音乐理论的细节。出于此目的,你需要知道的是我们将获得值的数据集,并将学习RNN模型以生成值序列。

 我们的音乐生成系统将使用78个独特的值。运行以下代码以加载原始音乐数据并将其预处理为值。这可能需要几分钟时间。

X, Y, n_values, indices_values = load_music_utils()
print('shape of X:', X.shape)
print('number of training examples:', X.shape[0])
print('Tx (length of sequence):', X.shape[1])
print('total # of unique values:', n_values)
print('Shape of Y:', Y.shape)

执行结果:

shape of X: (60, 30, 78)
number of training examples: 60
Tx (length of sequence): 30
total # of unique values: 78
Shape of Y: (30, 60, 78)

简单解释下数据集:

  • X:这是一个维度为(m, T x T_x , 78)的数组,我们有 m m 个样本,每个样本被分割为 T x = 30 T_x = 30 的音乐值,在每个时间步,输入的是78个不同的可能值之一,表示为一个独热向量,因此, X [ i , t , : ] X[i,t,:] 是表示时间t的第i个样本的值的一个独热向量。

  • Y:这与“X”基本相同,但向左移动了一步。类似于恐龙命名,我们感兴趣的是使用先前的值来预测下一个值的网络,所以我们的序列模型在给定 x 1 , , x t x^{\langle 1\rangle}, \ldots, x^{\langle t \rangle} 时将尝试预测 y t y^{\langle t \rangle}

  • n_values:此数据集中唯一值的数量,这应该是78。

  • indices_values:字典,0-77的数值映射到音乐值的字典。

1.2 - 模型预览

 这是我们将要使用的模型的架构,这类似于你在之前实现的恐龙命名模型,我们将在Keras中实现它。体系结构图如下:

music_generation.png

 我们将从一段较长的音乐片段中随机抽取30个值来训练模型。因此,我们不会设置第一个输入 x 1 = 0 x^{\langle 1 \rangle} = \vec{0} ,这是我们先前所做的来表示恐龙名字的开始,因为现在大部分的音频片段都在一个音乐的中间开始。我们设置每个片段具有相同的长度 T x = 30 T_x = 30 以使向量化变得更容易。

2 - 构建模型

 在这里,我们将构建和训练一个学习音乐的模型,输入的是维度为 ( m , T x , 78 ) (m, T_x, 78) 的X、维度为 ( T y , m , 78 ) (T_y, m, 78) 的Y,我们使用的是64维隐藏状态的LSTM模块,所以n_a = 64

n_a = 64

 以下是如何创建具有多个输入和输出的Keras模型。如果你正在构建一个RNN网络,即使在测试时也预先给出了整个输入序列 x 1 , x 2 , , x T x x^{\langle 1 \rangle}, x^{\langle 2 \rangle}, \ldots, x^{\langle T_x \rangle} ,例如,如果输入是单词,输出是一个标签,那么Keras有简单的内置函数来构建模型。但是,对于序列生成,在测试时我们并不提前知道 x t x^{\langle t\rangle} 的所有值;相反,我们使用 x t = y t 1 x^{\langle t\rangle} = y^{\langle t-1 \rangle} 一次生成一个。所以代码会有点复杂,你需要实现自己的for循环来迭代不同的时间步。

 函数djmodel()将使用for循环调用LSTM层 T x T_x 次,并且重要的是所有 T x T_x 副本具有相同的权重。也就是说,它不应该每次都重新初始化权重 - T x T_x 步骤应该具有共同的权重,在Keras中实现具有可共享权重的层的关键步骤是:

  1. 定义图层对象(我们将使用全局变量)。

  2. 在输入时调用这些对象。

 我们将需要的层对象定义为全局变量,请运行下面的代码创建它们,但是你要看一下Reshape()以及LSTM()Dense()以确保知道它做了什么。

reshapor = Reshape((1, 78))                        #2.B
LSTM_cell = LSTM(n_a, return_state = True)        #2.C
densor = Dense(n_values, activation='softmax')    #2.D

 每个reshaporLSTM_celldensor都是层对象,我们可以使用它们来实现djmodel()。为了将Keras张量对象X通过其中一个层传播,如果需要多个输入,可以使用layer_object(X)(或layer_object([X,Y])。例如,reshapor(X)将通过上面定义的Reshape(1,78)层来传播X。

 现在,我么要来实现djmodel()函数,我们需要做以下两步:

  1. 创建一个空的outputs来保存LSTM的所有时间步的输出。

  2. t 1 , , T x t \in 1, \ldots, T_x 中循环:

    A:从X中选择第“t”个时间步向量,选择的向量的维度应该是(78,),使用下面的语句在Keras中创建一个Lambda层

    x = Lambda(lambda x: X[:,t,:])(X)
    

     查看Keras文档,了解它的作用。 它正在创建一个“临时”或“未命名”函数(这就是Lambda函数所用的函数),它提取出适当的独热向量,并使该函数成为Keras Layer对象以应用于X.

    B:把x重构为(1,78),你可以使用reshapor()层(将在下面定义)

    C:使用x在LSTM_cell中执行单步传播,要记得使用之前隐藏状态 a a 和单元状态 c c 初始化LSTM_cell,你可以使用下面的语句来格式化:

    a, _, c = LSTM_cell(input_x, initial_state=[previous hidden state, previous cell state])
    

    D:使用densor通过dense+softmax层传播LSTM的输出激活值。

    E:把预测值添加到"outputs"列表中。

  3. 创建模型实体

def djmodel(Tx, n_a, n_values):
    """
    实现这个模型
    
    参数:
        Tx -- 语料库的长度
        n_a -- 激活值的数量
        n_values -- 音乐数据中唯一数据的数量
        
    返回:
        model -- Keras模型实体
    """
    # 定义输入数据的维度
    X = Input((Tx, n_values))
    
    # 定义a0, 初始化隐藏状态
    a0 = Input(shape=(n_a,),name="a0")
    c0 = Input(shape=(n_a,),name="c0")
    a = a0
    c = c0
    
    # 第一步:创建一个空的outputs列表来保存LSTM的所有时间步的输出。
    outputs = []
    
    # 第二步:循环
    for t in range(Tx):
        ## 2.A:从X中选择第“t”个时间步向量
        x = Lambda(lambda x:X[:, t, :])(X)
        
        ## 2.B:使用reshapor来对x进行重构为(1, n_values)
        x = reshapor(x)
        
        ## 2.C:单步传播
        a, _, c = LSTM_cell(x, initial_state=[a, c])
        
        ## 2.D:使用densor()应用于LSTM_Cell的隐藏状态输出
        out = densor(a)
        
        ## 2.E:把预测值添加到"outputs"列表中
        outputs.append(out)
        
    # 第三步:创建模型实体
    model = Model(inputs=[X, a0, c0], outputs=outputs)
    
    return model

模型定义好了,我们来执行一下:

# 获取模型,这里Tx=30, n_a=64,n_values=78
model = djmodel(Tx = 30 , n_a = 64, n_values = 78)

# 编译模型,我们使用Adam优化器与分类熵损失。
opt = Adam(lr=0.01, beta_1=0.9, beta_2=0.999, decay=0.01)
model.compile(optimizer=opt, loss='categorical_crossentropy', metrics=['accuracy'])

# 初始化a0和c0,使LSTM的初始状态为零。
m = 60
a0 = np.zeros((m, n_a))
c0 = np.zeros((m, n_a))

 现在让我们来拟合模型,在这样做之前,我们将把Y转换成一个列表,因为成本函数期望Y以这种格式提供(每一个时间步的一个列表项)。所以list(Y)是一个有30个条目的列表,其中每个列表项都是维度为(60,78)矩阵,让我们训练100代。这需要几分钟。

import time

#开始时间
start_time = time.clock()

#开始拟合
model.fit([X, a0, c0], list(Y), epochs=100)

#结束时间
end_time = time.clock()

#计算时差
minium = end_time - start_time

print("执行了:" + str(int(minium / 60)) + "分" + str(int(minium%60)) + "秒")

执行结果:

Epoch 1/100
60/60 [==============================] - 29s 491ms/step - loss: 125.9595 - dense_1_loss: 0.0000e+00 - dense_1_acc: 0.0000e+00 - dense_1_acc_1: 0.0333 - dense_1_acc_2: 0.0167 - dense_1_acc_3: 0.0333 - dense_1_acc_4: 0.0167 - dense_1_acc_5: 0.0333 - dense_1_acc_6: 0.0500 - dense_1_acc_7: 0.0667 - dense_1_acc_8: 0.0333 - dense_1_acc_9: 0.0667 - dense_1_acc_10: 0.1167 - dense_1_acc_11: 0.0500 - dense_1_acc_12: 0.0500 - dense_1_acc_13: 0.1000 - dense_1_acc_14: 0.0333 - dense_1_acc_15: 0.1000 - dense_1_acc_16: 0.0500 - dense_1_acc_17: 0.0500 - dense_1_acc_18: 0.0667 - dense_1_acc_19: 0.0167 - dense_1_acc_20: 0.0500 - dense_1_acc_21: 0.0500 - dense_1_acc_22: 0.0333 - dense_1_acc_23: 0.0167 - dense_1_acc_24: 0.1000 - dense_1_acc_25: 0.0500 - dense_1_acc_26: 0.0000e+00 - dense_1_acc_27: 0.0667 - dense_1_acc_28: 0.0167 - dense_1_acc_29: 0.0167  

......

Epoch 100/100
60/60 [==============================] - 0s 4ms/step - loss: 6.0661 - dense_1_loss: 0.0000e+00 - dense_1_acc: 0.0667 - dense_1_acc_1: 0.6833 - dense_1_acc_2: 0.9333 - dense_1_acc_3: 1.0000 - dense_1_acc_4: 1.0000 - dense_1_acc_5: 1.0000 - dense_1_acc_6: 1.0000 - dense_1_acc_7: 1.0000 - dense_1_acc_8: 1.0000 - dense_1_acc_9: 1.0000 - dense_1_acc_10: 1.0000 - dense_1_acc_11: 1.0000 - dense_1_acc_12: 1.0000 - dense_1_acc_13: 1.0000 - dense_1_acc_14: 1.0000 - dense_1_acc_15: 1.0000 - dense_1_acc_16: 1.0000 - dense_1_acc_17: 1.0000 - dense_1_acc_18: 1.0000 - dense_1_acc_19: 1.0000 - dense_1_acc_20: 1.0000 - dense_1_acc_21: 1.0000 - dense_1_acc_22: 1.0000 - dense_1_acc_23: 1.0000 - dense_1_acc_24: 1.0000 - dense_1_acc_25: 1.0000 - dense_1_acc_26: 1.0000 - dense_1_acc_27: 1.0000 - dense_1_acc_28: 1.0000 - dense_1_acc_29: 0.0000e+00

执行了:1分13秒

 可以看到模型损失下降,现在我们已经训练了一个模型,让我们更进一步生成一些音乐。

3 - 生成音乐

 现在让我们用这个模型来合成新的音乐。

3.1 - 预测与采样

music_gen.png

 在每个采样步骤中,我们将从LSTM的前一个状态输入激活a和单元状态c,向前传播一步,获得一个新的输出激活和单元状态。然后可以使用新的激活a来生成输出,就像以前一样使用densor来生成。

 首先,我们将初始化x0以及LSTM激活,并将单元值a0c0初始化为0。实现下面的函数来采样一个序列的音乐值,以下是在生成 T y T_y 输出字符的for循环中需要实现的一些关键步骤:

  • 步骤2.A:使用LSTM_Cell,输入上一步的ca,生成当前时间步的ca

  • 步骤2.B:使用densor(前面定义的)来计算softmax,以获得当前步骤的输出a

  • 步骤2.C:将刚刚生成的输出附加到outputs中。

  • 步骤2.D:样本x到“out”的预测的一个独热向量,你可以把它传递给下一个LSTM时间步,我们已经提供了这一行代码,它使用Lambda函数:

    x = Lambda(one_hot)(out) 
    

    提示:这行代码不是根据输出的概率随机抽取一个值,而是使用argmax在每个步骤中选择最可能的一个值。

def music_inference_model(LSTM_cell, densor, n_values = 78, n_a = 64, Ty = 100):
    """
    参数:
        LSTM_cell -- 来自model()的训练过后的LSTM单元,是keras层对象。
        densor -- 来自model()的训练过后的"densor",是keras层对象
        n_values -- 整数,唯一值的数量
        n_a -- LSTM单元的数量
        Ty -- 整数,生成的是时间步的数量
        
    返回:
        inference_model -- Kears模型实体
    """
    
    # 定义模型输入的维度
    x0 = Input(shape=(1,n_values))
    
    # 定义s0,初始化隐藏状态
    a0 = Input(shape=(n_a,),name="a0")
    c0 = Input(shape=(n_a,),name="c0")
    a = a0
    c = c0
    x = x0
    
    # 步骤1:创建一个空的outputs列表来保存预测值。
    outputs = []
    
    # 步骤2:遍历Ty,生成所有时间步的输出
    for t in range(Ty):
        
        # 步骤2.A:在LSTM中单步传播
        a, _, c = LSTM_cell(x, initial_state=[a, c])
        
        # 步骤2.B:使用densor()应用于LSTM_Cell的隐藏状态输出
        out = densor(a)
        
        # 步骤2.C:预测值添加到"outputs"列表中
        outputs.append(out)
        
        # 根据“out”选择下一个值,并将“x”设置为所选值的一个独热编码,
        # 该值将在下一步作为输入传递给LSTM_cell。我们已经提供了执行此操作所需的代码
        x = Lambda(one_hot)(out)
        
    # 创建模型实体
    inference_model = Model(inputs=[x0, a0, c0], outputs=outputs)
    
    return inference_model
# 获取模型实体,模型被硬编码以产生50个值
inference_model = music_inference_model(LSTM_cell, densor, n_values = 78, n_a = 64, Ty = 50)

#创建用于初始化x和LSTM状态变量a和c的零向量。
x_initializer = np.zeros((1, 1, 78))
a_initializer = np.zeros((1, n_a))
c_initializer = np.zeros((1, n_a))

 实现predict_and_sample()函数,它会接受很多的参数 [x_initializer, a_initializer, c_initializer] ,为了预测该输入对应的输出,需要进行3个步骤:

  1. 用你的模型来预测给定输入集的输出,输出pred应该是长度为20的列表,其中每个元素都是一个维度为( T y T_y , n_values)的numpy数组。

  2. pred转换为一个数字的 T y T_y 索引数组,每个索引都通过获取pred列表中的元素的argmax进行计算。

  3. 将索引转换为它们的一个独热编码。

def predict_and_sample(inference_model, x_initializer = x_initializer, a_initializer = a_initializer, 
                       c_initializer = c_initializer):
    """
    使用模型预测当前值的下一个值。
    
    参数:
        inference_model -- keras的实体模型
        x_initializer -- 初始化的独热编码,维度为(1, 1, 78)
        a_initializer -- LSTM单元的隐藏状态初始化,维度为(1, n_a)
        c_initializer -- LSTM单元的状态初始化,维度为(1, n_a)
    
    返回:
        results -- 生成值的独热编码向量,维度为(Ty, 78)
        indices -- 所生成值的索引矩阵,维度为(Ty, 1)
    """
    # 步骤1:模型来预测给定x_initializer, a_initializer and c_initializer的输出序列
    pred = inference_model.predict([x_initializer, a_initializer, c_initializer])
    
    # 步骤2:将“pred”转换为具有最大概率的索引数组np.array()。
    indices = np.argmax(pred, axis=-1)
    
    # 步骤3:将索引转换为它们的一个独热编码。
    results = to_categorical(indices, num_classes=78)
    
    return results, indices

我们来执行一下:

results, indices = predict_and_sample(inference_model, x_initializer, a_initializer, c_initializer)
print("np.argmax(results[12]) =", np.argmax(results[12]))
print("np.argmax(results[17]) =", np.argmax(results[17]))
print("list(indices[12:18]) =", list(indices[12:18]))

执行结果:

np.argmax(results[12]) = 71
np.argmax(results[17]) = 54
list(indices[12:18]) = [array([71], dtype=int64), array([12], dtype=int64), array([24], dtype=int64), array([57], dtype=int64), array([64], dtype=int64), array([54], dtype=int64)]

 你的结果可能与这里**【不同】**,因为Keras的结果不是完全可预测的。但是,如果你用上面所描述的100代来训练LSTM网络,很可能会观察到一系列不完全相同的索引

3.3 - 生成音乐

 RNN生成一个值序列。下面的代码通过首先调用predict_and_sample()函数来生成音乐,然后这些值被后期处理成音乐和弦(意味着可以同时演奏多个值或音符)。大多数计算音乐算法使用一些后期处理,因为很难在没有这种后期处理的情况下生成听起来不错的音乐。后期处理的工作包括清理生成的音频,确保相同的声音不会重复太多次,两个连续的音符在音高上的距离不会太远,等等。有人可能会说,这些后期处理步骤中有很多都是很困难的;另外,很多的音乐创作都集中在手工制作后期处理软件上,很多的输出质量取决于后期处理的质量,而不仅仅是RNN的质量。但是这种后期处理确实有很大的不同,所以让我们在实现中也使用它。

 运行下面的代码来生成音乐并将其记录到out_stream中,这可能需要几分钟。

out_stream = generate_music(inference_model)

执行结果:

Predicting new values for different set of chords.
Generated 51 sounds using the predicted values for the set of chords ("1") and after pruning
Generated 50 sounds using the predicted values for the set of chords ("2") and after pruning
Generated 51 sounds using the predicted values for the set of chords ("3") and after pruning
Generated 51 sounds using the predicted values for the set of chords ("4") and after pruning
Generated 50 sounds using the predicted values for the set of chords ("5") and after pruning
Your generated music is saved in output/my_music.midi

 这里音乐已经生成到了output/my_music.mid下,你可以下载一个midi转MP3的软件,也可以在线转换(使用百度:midi转mp3),作为参考,这里是我们使用该算法生成的30s音频剪辑:

IPython.display.Audio('./data/30s_trained_model.mp3')

执行结果:因CSDN限制,无法播放音乐,您可以下载资料包自行聆听。

需要记住的是:

  • 序列模型可用于生成音乐值,然后再处理成midi音乐。

  • 类似的模型可以用来生成恐龙名字或生成音乐,主要的区别是给模型的输入是什么。

  • 在Keras中,序列生成涉及定义具有共享权重的层,然后在不同的时间步 1 , , T x 1, \ldots, T_x 中重复这些层。

发布了57 篇原创文章 · 获赞 1690 · 访问量 76万+

猜你喜欢

转载自blog.csdn.net/u013733326/article/details/80890454