循环神经网络(RNN)Python实现详解
循环神经网络(Recurrent Neural Network, RNN)是一种专门处理序列数据的神经网络结构。本文将深入讲解RNN的Python实现,并在每段代码后提供相应的数学公式。
RNN基础理论
RNN的核心结构包含以下关键公式:
-
隐层状态更新公式:
st=g1(Uxt+Wst−1+ba)s_t = g_1(Ux_t + Ws_{t-1} + b_a)st=g1(Uxt+Wst−1+ba)
其中sts_tst是当前时刻隐层状态,xtx_txt是当前输入,st−1s_{t-1}st−1是前一时刻隐层状态,UUU和WWW是权重矩阵,bab_aba是偏置项。 -
输出层公式:
ot=g2(Vst+by)o_t = g_2(Vs_t + b_y)ot=g2(Vst+by)
其中oto_tot是当前时刻输出,VVV是输出权重矩阵,byb_yby是输出偏置项。 -
初始状态:
s0=0s_0 = 0s0=0
Python完整代码
import numpy as npdef softmax(x):e_x = np.exp(x-np.max(x))return e_x / e_x.sum(axis=0)def rnn_cell_forword(x_t, s_prev, parameters):"""单个cell的前向传播过程:param x_t: 当前时刻序列输入:param s_prev: 上一个cell的隐层状态输入:param parameters: cell的参数:return: 隐层输出,s_next, out_pred, cache"""# 取出参数U = parameters["U"]W = parameters["W"]V = parameters["V"]ba = parameters["ba"]by = parameters["by"]# 隐层输出计算s_next = np.tanh(np.dot(U, x_t) + np.dot(W, s_prev) +ba)# 计算cell的输出out_pred = softmax(np.dot(V, s_next) + by)# 记录每一层的值,用于反向传播使用cache = (s_next, s_prev, x_t, parameters)return s_next, out_pred, cachedef rnn_forword(x, s0 ,parameters):"""对于所有cell进行前向传播:param x: 输入序列,形状(m, 1, T),T为序列长度:param s0: 初始状态输入,0:param parameters: 所以cell共享的参数 U,W,V,ba,by:return: s, y, caches"""caches = []# 获取序列的长度,时刻数m, _, T =x.shape# 获取输入的Nm, n = parameters["V"].shape# 获取s0的值, 保存到S_next里面s_next = s0# 定义s,y保留所有cell的隐层状态以及输出s = np.zeros((n, 1 ,T))y = np.zeros((m, 1, T))# 循环对每一个cell进行前向传播计算for t in range(T):# 对于t时刻的cell进行输出s_next, out_pred, cache = rnn_cell_forword(x[:, :, t], s_next, parameters)#放入数组当中s[:, :, t] = s_nexty[:, :, t] = out_pred# 放入所有的缓存到列表当中caches.append(cache)return s, y, cachesdef rnn_cell_backward(ds_next, cache):"""每个cell的右边输入梯度:param ds_next: s_next的梯度值:param cache: 当前cell的缓存:return: gradients"""# 获取cache当中的缓存值以及参数(s_next, s_prev, x_t, parameters) = cacheU = parameters["U"]W = parameters["W"]V = parameters["V"]ba = parameters["ba"]by = parameters["by"]# 根据公式进行反向传播计算# 1. 计算tanh的导数dtanh = (1 - s_next ** 2) * ds_next# 2. 计算U的梯度值dU = np.dot(dtanh, x_t.T)# 3. 计算W的梯度值dW = np.dot(dtanh, s_prev.T)# 4.计算ba的梯度值dba = np.sum(dtanh, axis=1, keepdims=1)# 5.计算x_t的导数dx_t = np.dot(U.T, dtanh)# 6.计算s_prev的导数ds_prev = np.dot(W.T, dtanh)# 把所有的导数保存到字典中gradients = {"dtanh":dtanh, "dU":dU, "dW":dW, "dba":dba, "dx_t":dx_t, "ds_prev":ds_prev}return gradientsdef rnn_backward(ds, caches):"""所有的cell的反向传播过程:param ds: 每个时刻的损失对于s的梯度值:param caches: 每个cell的输出值:return:"""# 取出caches当中的值(s1, s0, x_1, parameters) = caches[0]# 获取输入数据的总序列长度n, _, T = ds.shapem, _ = x_1.shape# 存储所以一次更新后的参数的梯度dU = np.zeros((n, m))dW = np.zeros((n, n))dba = np.zeros((n, 1))# 初始化一个为0的s第二部分梯度值ds_prevt = np.zeros((n, 1))# 保存其他不需要更新的梯度dx = np.zeros((m, 1, T))#循环从后往前进行计算梯度for t in reversed(range(T)):# 从三时刻开始# 2,1,0时刻的s梯度由两个部分组成gradients = rnn_cell_backward(ds[:, :, t] + ds_prevt, caches[t])ds_prevt = gradients["ds_prev"]# 共享梯度相加dU += gradients["dU"]dW += gradients["dW"]dba += gradients["dba"]# 保存每一层的x_t,s_prev的梯度值dx[:, :, t] = gradients["dx_t"]# 返回所有更新参数的梯度以及其他变量的梯度值gradients = {"dU":dU, "dW":dW, "dba":dba, "dx":dx}return gradientsif __name__ == '__main__':np.random.seed(1)# 定义四个cell, 每一个形状(3,1)x = np.random.randn(3, 1, 4)s0 = np.random.randn(5, 1)U = np.random.randn(5, 3)W = np.random.randn(5, 5)V = np.random.randn(3, 5)ba = np.random.randn(5, 1)by = np.random.randn(3, 1)parameters = {"U":U, "W":W, "V":V, "ba":ba, "by":by}s, y, caches = rnn_forword(x, s0, parameters)# 随机给每4个cell的隐藏层输出的导数结果(实际需要计算)ds = np.random.randn(5, 1, 4)gradients = rnn_backward(ds, caches)print(gradients)
Python实现详解
1. Softmax函数实现
def softmax(x):e_x = np.exp(x - np.max(x))return e_x / e_x.sum(axis=0)
数学公式:
softmax(xi)=exi−max(x)∑jexj−max(x)\text{softmax}(x_i) = \frac{e^{x_i - \max(x)}}{\sum_j e^{x_j - \max(x)}}softmax(xi)=∑jexj−max(x)exi−max(x)
其中减去最大值是为了数值稳定性,防止指数计算溢出。
2. 单个RNN单元前向传播
def rnn_cell_forward(x_t, s_prev, parameters):U = parameters["U"]W = parameters["W"]V = parameters["V"]ba = parameters["ba"]by = parameters["by"]s_next = np.tanh(np.dot(U, x_t) + np.dot(W, s_prev) + ba)out_pred = softmax(np.dot(V, s_next) + by)cache = (s_next, s_prev, x_t, parameters)return s_next, out_pred, cache
数学公式:
st=tanh(Uxt+Wst−1+ba)s_t = \tanh(Ux_t + Ws_{t-1} + b_a)st=tanh(Uxt+Wst−1+ba)
ot=softmax(Vst+by)o_t = \text{softmax}(Vs_t + b_y)ot=softmax(Vst+by)
其中:
- tanh\tanhtanh是激活函数
- sts_tst是当前隐层状态
- oto_tot是当前输出预测
3. 完整RNN前向传播
def rnn_forward(x, s0, parameters):caches = []m, _, T = x.shapem, n = parameters["V"].shapes_next = s0s = np.zeros((n, 1, T))y = np.zeros((m, 1, T))for t in range(T):s_next, out_pred, cache = rnn_cell_forward(x[:, :, t], s_next, parameters)s[:, :, t] = s_nexty[:, :, t] = out_predcaches.append(cache)return s, y, caches
数学过程:
- 初始化隐层状态 s0s_0s0
- 对每个时间步 t∈[0,T−1]t \in [0, T-1]t∈[0,T−1]:
- 计算 st=tanh(Uxt+Wst−1+ba)s_t = \tanh(Ux_t + Ws_{t-1} + b_a)st=tanh(Uxt+Wst−1+ba)
- 计算 ot=softmax(Vst+by)o_t = \text{softmax}(Vs_t + b_y)ot=softmax(Vst+by)
- 存储所有时间步的隐层状态和输出
4. 单个RNN单元反向传播
def rnn_cell_backward(ds_next, cache):s_next, s_prev, x_t, parameters = cacheU, W, V, ba, by = parameters["U"], parameters["W"], parameters["V"], parameters["ba"], parameters["by"]dtanh = (1 - s_next**2) * ds_nextdU = np.dot(dtanh, x_t.T)dW = np.dot(dtanh, s_prev.T)dba = np.sum(dtanh, axis=1, keepdims=True)dx_t = np.dot(U.T, dtanh)ds_prev = np.dot(W.T, dtanh)gradients = {"dU": dU, "dW": dW, "dba": dba, "dx_t": dx_t, "ds_prev": ds_prev}return gradients
数学推导:
-
tanh\tanhtanh 激活函数的导数:
∂tanh(z)∂z=1−tanh2(z)\frac{\partial \tanh(z)}{\partial z} = 1 - \tanh^2(z)∂z∂tanh(z)=1−tanh2(z)
因此:
dtanh=(1−st2)⊙dsnextd_{\tanh} = (1 - s_t^2) \odot ds_{\text{next}}dtanh=(1−st2)⊙dsnext -
参数梯度计算:
∂L∂U=dtanh⋅xtT\frac{\partial L}{\partial U} = d_{\tanh} \cdot x_t^T∂U∂L=dtanh⋅xtT
∂L∂W=dtanh⋅st−1T\frac{\partial L}{\partial W} = d_{\tanh} \cdot s_{t-1}^T∂W∂L=dtanh⋅st−1T
∂L∂ba=∑dtanh(沿batch维度求和)\frac{\partial L}{\partial b_a} = \sum d_{\tanh} \quad (\text{沿batch维度求和})∂ba∂L=∑dtanh(沿batch维度求和) -
输入和前一状态梯度:
∂L∂xt=UT⋅dtanh\frac{\partial L}{\partial x_t} = U^T \cdot d_{\tanh}∂xt∂L=UT⋅dtanh
∂L∂st−1=WT⋅dtanh\frac{\partial L}{\partial s_{t-1}} = W^T \cdot d_{\tanh}∂st−1∂L=WT⋅dtanh
5. 完整RNN反向传播
def rnn_backward(ds, caches):s1, s0, x_1, parameters = caches[0]n, _, T = ds.shapem, _ = x_1.shapedU = np.zeros((n, m))dW = np.zeros((n, n))dba = np.zeros((n, 1))ds_prevt = np.zeros((n, 1))dx = np.zeros((m, 1, T))for t in reversed(range(T)):gradients = rnn_cell_backward(ds[:, :, t] + ds_prevt, caches[t])ds_prevt = gradients["ds_prev"]dU += gradients["dU"]dW += gradients["dW"]dba += gradients["dba"]dx[:, :, t] = gradients["dx_t"]gradients = {"dU": dU, "dW": dW, "dba": dba, "dx": dx}return gradients
数学过程:
- 初始化参数梯度为零
- 从最后一个时间步向前遍历(时间反向传播):
t=T−1,T−2,…,0t = T-1, T-2, \dots, 0t=T−1,T−2,…,0 - 每个时间步的梯度包含两部分:
- 当前时刻输出的梯度 dstds_tdst
- 下一时刻传递的梯度 dsprevtds_{\text{prevt}}dsprevt
- 参数梯度累加(参数共享):
∂L∂U=∑t=0T−1∂L∂Ut\frac{\partial L}{\partial U} = \sum_{t=0}^{T-1} \frac{\partial L}{\partial U_t}∂U∂L=t=0∑T−1∂Ut∂L
∂L∂W=∑t=0T−1∂L∂Wt\frac{\partial L}{\partial W} = \sum_{t=0}^{T-1} \frac{\partial L}{\partial W_t}∂W∂L=t=0∑T−1∂Wt∂L
∂L∂ba=∑t=0T−1∂L∂ba,t\frac{\partial L}{\partial b_a} = \sum_{t=0}^{T-1} \frac{\partial L}{\partial b_{a,t}}∂ba∂L=t=0∑T−1∂ba,t∂L
6. 测试代码
if __name__ == '__main__':np.random.seed(1)# 创建模拟数据:4个时间步,每个输入3维x = np.random.randn(3, 1, 4)s0 = np.random.randn(5, 1) # 初始隐层状态# 初始化参数U = np.random.randn(5, 3) # 输入到隐层权重W = np.random.randn(5, 5) # 隐层到隐层权重V = np.random.randn(3, 5) # 隐层到输出权重ba = np.random.randn(5, 1) # 隐层偏置by = np.random.randn(3, 1) # 输出偏置parameters = {"U": U, "W": W, "V": V, "ba": ba, "by": by}# 前向传播s, y, caches = rnn_forward(x, s0, parameters)# 模拟梯度(实际应用中来自损失函数)ds = np.random.randn(5, 1, 4)# 反向传播gradients = rnn_backward(ds, caches)print("梯度字典:")for key in gradients:print(f"{key}: {gradients[key].shape}")
RNN的特点与挑战
优点:
- 处理变长序列:适合文本、语音等序列数据
- 参数共享:所有时间步共享同一组参数
- 记忆能力:隐层状态可携带历史信息
挑战:
-
梯度消失/爆炸:长序列训练困难
∂st∂sk=∏i=kt−1∂si+1∂si\frac{\partial s_t}{\partial s_k} = \prod_{i=k}^{t-1} \frac{\partial s_{i+1}}{\partial s_i}∂sk∂st=i=k∏t−1∂si∂si+1
当序列较长时,梯度可能指数级衰减或爆炸 -
短期记忆限制:难以捕获长期依赖关系
总结
本文详细讲解了RNN的Python实现,包括:
- 前向传播过程(单个单元和完整序列)
- 反向传播过程(BPTT算法)
- 核心数学公式推导
- 完整可运行代码示例
RNN是处理序列数据的基础模型,理解其原理和实现对于学习更高级的序列模型(如LSTM、GRU)至关重要。实际应用中,我们通常使用这些改进型来解决标准RNN的梯度问题,但标准RNN仍然是理解循环神经网络的基础。