AI 学习之使用纯 Python 构建 RNN 与 LSTM

基本概念

我们人类理解事物都是基于上下文的,一句话只有在明确的上下文中才能有确定含义。而传统的神经网络不能实现,因为神经网络是一对一的,我们输入一个input,网络输出一个 output,而多个input 之间却没有联系。就好比我们我们输入一部电影的一帧,让它预测接下来将会发生什么,显然没有先前剧情的支撑神经网络不可能做出好的预测。

RNN

Recurrent neural networks(RNN)正是为了解决此类问题诞生的。RNN 网络连接中存在循环,并且能够存储信息。

纯python构建RNN

因为循环神经网络Recurrent Neural Networks(RNN)有记忆能力,所以被广泛用于自然语言处理Natural Language Processing(NLP)和其它一些序列任务中。单向循环神经网络Uni-directional RNN(URNN)可以记住前面的时间步信息,双向循环神经网络Bidirection RNN(BRNN)可以记住前面以及后面的时间步信息。我们在前面的文章中已经学习了很多关于RNN的知识了。但是,仅仅是从文字的层面上学习了而已,大家可能对RNN还是朦朦胧胧的。所以,本次的实战编程就是带领大家从代码的层面来学习RNN。通过使用代码将RNN实现一遍后,大家对它的理解就会更透彻了!本次我们不使用TF和keras框架,而是使用纯python代码,这样我们才能接触更多RNN的逻辑细节。

import numpy as np
from rnn_utils import *

1 - RNN的前向传播

下图一个输入和输出长度相等的RNN,也就是$T_x = T_y$.

file

图1:简单的RNN模型

一个RNN网络可以看作是由RNN单元(RNN cell)的多个时间步连接而成的。所以要想实现RNN网络,我们先得实现RNN单元。

1.1 - RNN单元

下图就是RNN单元的一个时间步的计算图

file

**图 2**: 简单的RNN单元

# 实现上图展示的RNN单元的前向传播计算(也就是一个时间步的前向传播计算)

def rnn_cell_forward(xt, a_prev, parameters):
    """
    参数:
    xt -- 时间步"t"的输入x,就是本时间步的输入x, 维度是(n_x, m),m是样本个数,n_x是输入特征数量
    a_prev -- 时间步"t-1"的激活值,就是上一个时间步计算得到的激活值, 维度是(n_a, m),n_a是RNN单元中的神经元个数。
    parameters -- 是一个包含下列参数的字典:
                  注意,下面的参数维度中都没有包含时间步t,这是因为所有时间步都共用一组参数。
                        Wax -- 与输入x关联的权重,维度是(n_a, n_x)。
                        Waa -- 与输入激活值(也即是上一步的激活值)关联的权重,维度是(n_a, n_a)
                        Wya -- 与预测值y关联的权重,维度是(n_y, n_a),n_y是预测值个数
                        ba --  与激活值关联的阈值,维度是(n_a, 1)
                        by -- 与预测值关联的阈值,维度是 (n_y, 1)
    返回值:
    a_next -- 输出激活值,即输出到时间步"t+1"的激活值,维度是(n_a, m)
    yt_pred -- 本时间步的预测值,维度是(n_y, m)
    cache -- 用于计算反向传播的缓存,包含了 (a_next, a_prev, xt, parameters)
    """

    Wax = parameters["Wax"]
    Waa = parameters["Waa"]
    Wya = parameters["Wya"]
    ba = parameters["ba"]
    by = parameters["by"]

    # 计算激活值
    a_next = np.tanh(np.dot(Wax, xt) + np.dot(Waa, a_prev) + ba)
    # 计算预测值
    yt_pred = 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单元重复连接起来就构成一个RNN了。假设输入数据需要10个时间步来处理(例如句子中有10个单词),那么我们就要重复调用上面那个RNN单元10次。

file

**图 3**

# 实现上图的RNN前向传播

def rnn_forward(x, a0, parameters):
    """
    参数:
    x -- 输入x,维度是 (n_x, m, T_x)。T_x是指x里面有多少个序列,例如一个句子中有10个单词,那么T_x就等于10
    a0 -- 激活值,维度是(n_a, m)。n_a是RNN单元中的神经元个数
    parameters -- 是一个包含下列参数的字典:
                  注意,下面的参数维度中都没有包含时间步t,这是因为所有时间步都共用一组参数。
                        Wax -- 与输入x关联的权重,维度是(n_a, n_x)。
                        Waa -- 与输入激活值(也即是上一步的激活值)关联的权重,维度是(n_a, n_a)
                        Wya -- 与预测值y关联的权重,维度是(n_y, n_a),n_y是预测值个数
                        ba --  与激活值关联的阈值,维度是(n_a, 1)
                        by -- 与预测值关联的阈值,维度是 (n_y, 1)

    返回值:
    a -- 每一个时间步的激活值,维度是(n_a, m, T_x)
    y_pred -- 每一个时间步的预测值,维度是(n_y, m, T_x)
    caches -- 用于计算反向传播的缓存
    """

    caches = []

    n_x, m, T_x = x.shape
    n_y, n_a = parameters["Wya"].shape

    a = np.zeros((n_a, m, T_x))
    y_pred = np.zeros((n_y, m, T_x))

    a_next = a0

    # 遍历所有时间步
    for t in range(T_x):
        # 调用前面实现的rnn_cell_forward来处理当前的时间步
        a_next, yt_pred, cache = rnn_cell_forward(x[:,:,t], a_next, parameters)
        # 保存当前时间步的激活值
        a[:,:,t] = a_next
        # 保存当前时间步的预测值
        y_pred[:,:,t] = yt_pred
        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

恭喜,当前你已经使用纯python代码实现了一个RNN的前向传播。但是这个RNN会有梯度消失的问题,导致RNN的记性不好。所以我们需要将RNN单元改造成LSTM。使RNN可以记住更多时间步的信息。

2 - Long Short-Term Memory (LSTM)

下图展示了LSTM单元的计算流程

file

**图 4**: LSTM-单元.

与普通的RNN一样,首先我们要实现LSTM单元,然后再重复的为每一个时间步来调用这个LSTM单元。

2.1 - LSTM单元

# 实现时间步"t"
def lstm_cell_forward(xt, a_prev, c_prev, parameters):
    """
    参数:
    xt -- 时间步"t"的输入x, 维度是(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, 1)
                        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 -- 产生的预测值,维度是(n_y, m)
    cache -- 缓存,包含了(a_next, c_next, a_prev, c_prev, xt, 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"]

    n_x, m = xt.shape
    n_y, n_a = Wy.shape

    # 将输入x和a结合为一个大矩阵
    concat = np.zeros((n_a + n_x, m))
    concat[: n_a, :] = a_prev
    concat[n_a :, :] = xt

    # 实现前面图中列出的六个公式
    # ft/it/ot分别表示遗忘门/更新门/输出门
    ft = sigmoid(np.dot(Wf, concat) + bf)
    it = sigmoid(np.dot(Wi, concat) + bi)
    cct = np.tanh(np.dot(Wc, concat) + bc)
    c_next = ft * c_prev + it * cct
    ot = sigmoid(np.dot(Wo, concat) + bo)
    a_next = ot * np.tanh(c_next)

    # 计算预测值
    yt_pred = 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

与普通RNN一样,接下来就是为每一个时间步执行一次LSTM单元。
file

**图 5**

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
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.17211776753291672
a.shape =  (5, 10, 7)
y[1][4][3] = 0.9508734618501101
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.8555449167181981
len(caches) =  2

我们已经知道了如何实现普通RNN和LSTM的前向传播。我们知道,使用TF和Keras等深度学习框架时,只需要实现前向传播就可以了,框架会为我们实现反向传播。因为对于绝大多数AI工程师来说,都不需要我们自己实现反向传播,而且RNN的反向传播是需要很复杂的数学计算的,所以我们就不说反向传播了。如果你的数学很棒,并且你很感兴趣,那么你自己可以慢慢推导它的反向传播计算。


相关文章:
循环神经网络(Recurrent Neural Network)

为者常成,行者常至