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$.
图1:简单的RNN模型
一个RNN网络可以看作是由RNN单元(RNN cell)的多个时间步连接而成的。所以要想实现RNN网络,我们先得实现RNN单元。
1.1 - RNN单元
下图就是RNN单元的一个时间步的计算图
**图 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次。
**图 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单元的计算流程
**图 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单元。
**图 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)
为者常成,行者常至
自由转载-非商用-非衍生-保持署名(创意共享3.0许可证)