简介:个人学习分享,如有错误,欢迎批评指正。
一. 核心理念
循环神经网络(Recurrent Neural Network,RNN)是一类专门用于处理序列数据
的神经网络架构。其独特之处在于能够处理输入序列中元素的时序关系
,通过在网络中引入循环连接,使得信息能够在时间步之间传递和积累。
时间序列数据是指在不同时间点上收集到的数据,这类数据反映了某一事物、现象等随时间的变化状态或程度。当然序列数据也可以不是时间,比如文字序列,但总归序列数据有一个特点——后面的数据跟前面的数据有关系
。
为什么要发明RNN
我们先来看一个NLP很常见的问题,命名实体识别,举个例子,现在有两句话:
第一句话:I like eating apple!(我喜欢吃苹果!)
第二句话:The Apple is a great company!(苹果真是一家很棒的公司!)
现在的任务是要给apple打Label,我们都知道第一个apple是一种水果,第二个apple是苹果公司,假设我们现在有大量的已经标记好的数据以供训练模型,当我们使用全连接的神经网络时,我们做法是把apple这个单词的特征向量输入到我们的模型中(如上图),在输出结果时,让我们的label里,正确的label概率最大,来训练模型,但我们的语料库中,有的apple的label是水果,有的label是公司,这将导致模型在训练的过程中,预测的准确程度,取决于训练集中哪个label多一些
,这样的模型对于我们来说完全没有作用。问题就出在了我们没有结合上下文去训练模型,而是单独的在训练apple这个单词的label,这也是全连接神经网络模型所不能做到的,于是就有了我们的循环神经网络。
二. RNN的基本网络结构
1 网络组成
标准的RNN由以下几个主要部分组成:
- 输入层 (Input Layer):接收序列数据的
每一个时间步的输入向量
。 - 隐藏层 (Hidden Layer):包含循环连接,
用于储存和传递时间步之间的信息
。 - 输出层 (Output Layer):生成每个时间步的输出。
2 RNN基础结构
单层网络结构如下图所示:
输入是 x x x,经过变换 W x + b Wx + b Wx+b 和 激活函数 f f f,得到输出 y y y。
假设这里的激活函数 f f f 是 sigmoid 函数:
f ( x ) = 1 1 + e − x f(x) = \frac{1}{1 + e^{-x}} f(x)=1+e−x1
则进一步得到:
y = 1 1 + e − ( W x + b ) y = \frac{1}{1 + e^{-(Wx + b)}} y=1+e−(Wx+b)1
在实际应用中,我们还会遇到很多序列形的数据。
为了建模序列问题,RNN 引入了 隐藏状态 h h h (hidden state) 的概念,隐藏状态 h h h 可以对序列形的数据提取特征,接着再转换为输出。
先从 h 1 h_1 h1 的计算开始看:
图示中记号的含义是:
a) 圆圈或方块表示的是向量。
b) 一个箭头就表示对该向量做一次变换
。如上图中 h 0 h_0 h0 和 x 1 x_1 x1 分别有一个箭头连接,就表示对 h 0 h_0 h0 和 x 1 x_1 x1 各做了一次变换。
h 2 h_2 h2 的计算和 h 1 h_1 h1 类似,但有两点需要注意:
-
在计算时,
每一步使用的参数
U , W , b U, W, b U,W,b都是一样的,也就是说每个步骤的参数都是共享的
,这是 RNN 的重要特点。(虽然说 X [ t − 1 ] , X [ t ] , X [ t + 1 ] X[t-1], X[t], X[t+1] X[t−1],X[t],X[t+1] 是表示不同时间刻的输入,但是真正输入到 RNN 网络中的时候,并不是作为单独的向量一个一个输入,而是组合在一起形成一个矩阵输入,然后这个矩阵再通过权重矩阵 U U U 的变化。其实是同一时刻输入地,只是计算的先后顺序不同
。因此同一个隐藏层中,不同时刻的输入他们的 W , V , U W, V, U W,V,U 参数是共享的。) -
LSTM 中的权值则不共享,
因为它是在两个不同的向量中
。而 RNN 的权值为何共享呢?很简单,因为RNN 的权值是在同一个向量中,只是不同时刻而已
。
依次计算剩下来的(使用相同的参数 U , W , b U, W, b U,W,b):
为了方便起见,只画出序列长度为4的情况,实际上,这个计算过程可以无限地持续下去。
我们目前的RNN还没有输出,得到输出值的方法就是直接通过 h h h 进行计算:
正如之前所说,一个箭头就表示对对应的向量做一次类似于 f ( W x + b ) f(Wx + b) f(Wx+b) 的变换,这里的这个箭头就表示对 h 1 h_1 h1 进行一次变换,得到输出 y 1 y_1 y1。
剩下的输出类似进行(使用和 h 1 h_1 h1 同样的参数 V V V 和 c c c):
这就是最经典的 RNN 结构,是 x 1 , x 2 , … , x n x_1, x_2, \dots, x_n x1,x2,…,xn,输出为 y 1 , y 2 , … , y n y_1, y_2, \dots, y_n y1,y2,…,yn。也就是说,输入和输出序列必须是等长的。
三. RNN的基本工作流程
1 序列数据的处理
RNN主要用于处理序列数据,如文本、语言、时间序列等。假设输入序列为 X = ( x 1 , x 2 , x 3 , … , x T ) X = (x_1, x_2, x_3, \ldots, x_T) X=(x1,x2,x3,…,xT),RNN逐步处理序列中的每一个时间步 t = 1 , 2 , … , T t = 1, 2, \ldots, T t=1,2,…,T。
2 时间步之间的信息传递
在每一个时间步 t t t,RNN接收当前输入 x t x_t xt 和前一时间步的隐藏状态 h t − 1 h_{t-1} ht−1,通过非线性变换计算当前时间步的隐藏状态 h t h_t ht,并根据 h t h_t ht 生成输出 y t y_t yt。这种机制允许网络在处理当前输入时,结合之前的历史信息
。
3 信息流动图示
四. 数学基础
1 隐藏状态的计算
每个时间步的隐藏状态 h t h_t ht 由当前输入 x t x_t xt 和前一隐藏状态 h t − 1 h_{t-1} ht−1 共同决定,计算公式如下:
h t = tanh ( W x h x t + W h h h t − 1 + b h ) h_t = \tanh(W_{xh}x_t + W_{hh}h_{t-1} + b_h) ht=tanh(Wxhxt+Whhht−1+bh)
- W x h W_{xh} Wxh:输入到隐藏层的权重矩阵
- W h h W_{hh} Whh:隐藏层到隐藏层的权重矩阵(循环连接)
- b h b_h bh:隐藏层的偏置向量
- tanh \tanh tanh:激活函数,常用双曲正切函数以引入非线性
2 输出的计算
基于当前隐藏状态 h t h_t ht,计算输出 y t y_t yt:
y t = W h y h t + b y y_t = W_{hy}h_t + b_y yt=Whyht+by
- W h y W_{hy} Why:隐藏层到输出层的权重矩阵
- b y b_y by:输出层的偏置向量
3 参数共享
RNN在所有时间步之间共享权重矩阵 W x h W_{xh} Wxh、 W h h W_{hh} Whh 和 W h y W_{hy} Why。这种参数共享不仅减少了模型的参数数量,还使得RNN能够处理可变长度的序列输入
。
五. 信息流动与记忆机制
1 前向传播
在前向传播过程中,RNN按时间步依次处理输入序列的每个元素
。具体步骤如下:
-
初始化:设定初始隐藏状态 h 0 h_0 h0(通常为零向量)。
-
时间步 t t t:
- 接收当前输入 x t x_t xt 和前一隐藏状态 h t − 1 h_{t-1} ht−1。
- 计算当前隐藏状态 h t h_t ht。
- 生成当前输出 y t y_t yt。
整个过程可以表示为:
h t = tanh ( W x h x t + W h h h t − 1 + b h ) y t = W h y h t + b y \begin{aligned} h_t &= \tanh(W_{xh}x_t + W_{hh}h_{t-1} + b_h) \\ y_t &= W_{hy}h_t + b_y \end{aligned} htyt=tanh(Wxhxt+Whhht−1+bh)=Whyht+by
2 隐藏状态的记忆能力
通过循环连接,隐藏状态 h t h_t ht 不仅包含当前时间步的信息,还积累了前面时间步的历史信息
。这使得RNN能够在处理当前输入时,参考和利用之前的信息,从而捕捉序列中的时序依赖关系
。
六. 学习机制:通过时间反向传播(BPTT)
1 损失函数
对于一个序列任务,通常定义一个整体损失函数,如对所有时间步的输出损失进行累加:
L = ∑ t = 1 T L ( y t , y ^ t ) L = \sum_{t=1}^{T} L(y_t, \hat{y}_t) L=t=1∑TL(yt,y^t)
其中, L ( y t , y ^ t ) L(y_t, \hat{y}_t) L(yt,y^t) 表示在时间步 t t t 的损失。
2 反向传播过程
由于RNN的隐藏状态在时间步之间共享传递,传统的反向传播方法无法直接应用。取而代之的是通过时间反向传播(Backpropagation Through Time,BPTT)
,其步骤如下:
-
展开网络:将RNN在时间维度上
展开成一个深层网络,每个时间步对应网络中的一层
。 -
计算损失:
在每个时间步计算输出的损失,并累加得到整个序列的总损失
。 -
反向传播:
- 从最后一个时间步 T T T 开始,
逐步向前计算每个时间步的梯度
。 于隐藏状态在时间步之间共享,梯度会在时间步之间传播,影响每个时间步的权重更新
。
- 从最后一个时间步 T T T 开始,
-
更新参数:根据损失的梯度,通过优化算法(如SGD, Adam)更新权重参数 W x h W_{xh} Wxh, W h h W_{hh} Whh, W h y W_{hy} Why 及偏置 b h b_h bh, b y b_y by。
七. RNN的具体工作机制
1 初始化
在开始处理序列之前,RNN需要初始化隐藏状态 h 0 h_0 h0。通常, h 0 h_0 h0 被设定为零向量
,但也可以根据具体任务进行初始化。
2 前向传播详细过程
对于每一个时间步 t t t,RNN执行以下操作:
- 输入获取:接收当前时间步的输入 x t x_t xt 和前一时间步的隐藏状态 h t − 1 h_{t-1} ht−1。
- 隐藏状态更新:计算当前隐藏状态 h t h_t ht,结合 x t x_t xt 和 h t − 1 h_{t-1} ht−1。
- 输出生成:基于当前隐藏状态 h t h_t ht,生成输出 y t y_t yt。
这种逐步处理的方式使得RNN能够在处理当前输入时,参考并利用之前的信息,实现对序列的动态建模。
3 反向传播详细过程
通过时间反向传播(BPTT)的步骤如下:
- 展开网络:将RNN在时间维度上展开为一个多层的前馈网络,每个时间步对应网络中的一层。
- 损失计算:在每个时间步计算输出的损失,并累加得到整体损失。
- 梯度计算:
从最后一个时间步开始,逐步向前计算每个时间步的梯度
。每个时间步的梯度不仅受当前时间步的损失影响,还受后续时间步梯度的影响,因为隐藏状态在时间步之间共享
。
- 权重更新:根据累积的梯度,通过优化算法更新权重参数。
4 参数更新
RNN的参数更新通常采用梯度下降法及其变种,如:
- 随机梯度下降(SGD):逐步更新参数。
- Adam优化:结合动量项和自适应学习率,提升收敛速度和稳定性。
- RMSProp:适应性平滑动量,动态调整学习率。
八. RNN的优势与局限
1 优势
- 序列建模能力强:能够捕捉和利用序列中的
时序依赖关系
,适用于多种序列任务。 - 参数共享:
权重
在时间步之间共享
,减少了模型的参数数量,提高了泛化能力。 - 处理可变长度序列:能够处理
长短不同的输入和输出序列
,具备高度的灵活性。
2 局限
- 梯度消失与爆炸:在
长序列
训练中,在反向传播过程中,梯度容易消失或爆炸(因为RNN在时序上共享参数,梯度在反向传播过程中,不断连乘,数值不是越来越大就是越来越小)
,影响模型的学习效果。 - 计算效率低:由于时间步之间存在依赖关系,
难以进行并行计算(RNN的计算过程具有依赖性,因此难以有效地并行化,无法充分利用GPU等硬件加速器的计算能力)
,训练速度较慢。 - 难以捕捉长距离依赖:尽管LSTM和GRU在一定程度上缓解了这一问题,但对于非常长的序列,仍然存在挑战。
九. RNN的优化与改进
1 梯度裁剪(Gradient Clipping)
在训练过程中,当梯度的范数超过预设阈值
时,将其缩放至阈值范围内,防止梯度爆炸。
if ∥ ∇ ∥ > 阈值 ⇒ ∇ ← 阈值 ∥ ∇ ∥ × ∇ \text{if} \ \|\nabla\| > \text{阈值} \ \Rightarrow \ \nabla \leftarrow \frac{\text{阈值}}{\|\nabla\|} \times \nabla if ∥∇∥>阈值 ⇒ ∇←∥∇∥阈值×∇
2 正则化技术
- Dropout:在RNN中应用Dropout需要特殊处理,如在每个时间步共享Dropout掩码,防止时间步之间的信息泄露。
- L2正则化:通过在损失函数中添加权重的平方和,防止权重过大,减少过拟合。
3 优化算法
采用先进的优化算法,如Adam、RMSProp,提升训练的收敛速度和稳定性。
4 权重初始化
使用适当的权重初始化方法,如Xavier初始化
或He初始化
,确保网络在开始训练时处于良好的状态,避免梯度消失或爆炸。
5 使用更稳定的激活函数
采用ReLU等更稳定的激活函数,替代传统的tanh或sigmoid,可以在一定程度上缓解梯度消失问题。
十. 高级RNN结构
1 双向RNN(Bidirectional RNN)
双向RNN通过同时考虑序列的正向和反向信息
,提升模型对上下文的理解能力
。其结构包括两个独立的RNN,一个按时间正序处理输入,另一个按时间逆序处理输入,最终将两个方向的隐藏状态进行合并。
数学表示
h t f o r w a r d = RNN_Cell ( h t − 1 f o r w a r d , x t ) h t b a c k w a r d = RNN_Cell ( h t + 1 b a c k w a r d , x t ) h t = [ h t f o r w a r d , h t b a c k w a r d ] h_t^{forward} = \text{RNN\_Cell}(h_{t-1}^{forward}, x_t) \\ h_t^{backward} = \text{RNN\_Cell}(h_{t+1}^{backward}, x_t) \\ h_t = [h_t^{forward}, h_t^{backward}] htforward=RNN_Cell(ht−1forward,xt)htbackward=RNN_Cell(ht+1backward,xt)ht=[htforward,htbackward]
2 深度RNN(Deep RNN)
深度RNN通过堆叠多个RNN层,增加网络的深度
,从而提升模型的表达能力。每一层RNN的输出作为下一层RNN的输入。
结构示意
输入层 → RNN层1 → RNN层2 → ... → RNN层L → 输出层
3 注意力机制RNN(Attention-based RNN)
注意力机制通过赋予输入序列中不同部分不同的权重
,提升模型对重要信息的关注度。特别是在机器翻译等任务中,注意力机制显著提升了模型性能。
数学表示
在计算输出时,引入一个注意力权重 α t , i \alpha_{t,i} αt,i,用于加权输入序列中不同时间步的隐藏状态:
α t , i = exp ( e t , i ) ∑ k = 1 T exp ( e t , k ) \alpha_{t,i} = \frac{\exp(e_{t,i})}{\sum_{k=1}^{T} \exp(e_{t,k})} αt,i=∑k=1Texp(et,k)exp(et,i)
e t , i = score ( h t , h i encoder ) e_{t,i} = \text{score}(h_t, h_i^{\text{encoder}}) et,i=score(ht,hiencoder)
context t = ∑ i = 1 T α t , i h i encoder \text{context}_t = \sum_{i=1}^{T} \alpha_{t,i} h_i^{\text{encoder}} contextt=i=1∑Tαt,ihiencoder
y t = Output ( h t , context t ) y_t = \text{Output}(h_t, \text{context}_t) yt=Output(ht,contextt)
其中, e t , i e_{t,i} et,i 是一个评分函数,用于计算当前隐藏状态 h t h_t ht 与编码器隐藏状态 h i encoder h_i^{\text{encoder}} hiencoder 的相关性。
十一. RNN与其他模型的对比
1 RNN与前馈神经网络(FNN)
- 序列处理能力:RNN适用于处理
序列数据
,而FNN主要处理固定大小的输入
。 - 记忆能力:RNN具有
记忆前一时间步信息
的能力,FNN缺乏这种能力
。
2 RNN与卷积神经网络(CNN)
- 结构差异:CNN主要用于处理具有
局部特征的二维数据
(如图像),而RNN用于处理一维序列数据
。 - 应用领域:CNN广泛应用于计算机视觉,RNN则主要用于自然语言处理、语言识别等领域。
3 RNN与Transformer
- 并行化能力:Transformer利用自注意力机制,能够更好地
并行化计算
,提升训练效率。 - 捕捉依赖关系:Transformer在
捕捉长距离依赖关系上表现优异
,而RNN在这方面存在梯度消失问题
。 - 应用趋势:近年来,Transformer逐渐取代RNN成为序列建模的主流架构,但
RNN在资源受限或需要极长序列的场景中仍具有优势
。
十二. 案例和python代码
案例概述
任务:基于给定的文本数据(例如莎士比亚的作品),训练一个标准 RNN 模型,以生成具有相似风格的新文本。
模型架构:
- 字符级编码:将每个字符转换为整数索引,并进行独热编码。
- 嵌入层(可选):将字符索引转换为密集向量表示。
- 多层标准 RNN (SimpleRNN):捕捉字符序列中的模式和依赖关系。
- 全连接层 (Dense Layer):预测下一个字符的概率分布。
- Dropout层:防止过拟合。
- 批归一化层(可选):加强训练并稳定模型。
所需库
首先,确保你已经安装了必要的库:
pip install tensorflow numpy
Python代码实现
以下是完整的Python代码,实现了字符级文本生成的标准RNN模型:
import tensorflow as tf
import numpy as np
import os
import sys
import requestsfrom tensorflow.keras.models import Sequential
from tensorflow.keras.layers import Dense, SimpleRNN, Dropout, Embedding, BatchNormalization
from tensorflow.keras.optimizers import Adam
from tensorflow.keras.callbacks import ModelCheckpoint, EarlyStopping# 下载示例文本数据(莎士比亚的《哈姆雷特》)
def download_text():url = "https://www.gutenberg.org/files/2264/2264-0.txt"response = requests.get(url)text = response.textreturn text# 数据预处理
def preprocess_text(text, max_vocab=1000):# 保留前max_vocab个字符unique_chars = sorted(list(set(text)))if len(unique_chars) > max_vocab:unique_chars = unique_chars[:max_vocab]char_to_int = {c: i for i, c in enumerate(unique_chars)}int_to_char = {i: c for i, c in enumerate(unique_chars)}encoded_text = [char_to_int[c] for c in text if c in char_to_int]return encoded_text, char_to_int, int_to_char, len(unique_chars)# 创建数据集
def create_dataset(encoded_text, seq_length):X = []y = []for i in range(0, len(encoded_text) - seq_length):X.append(encoded_text[i:i + seq_length])y.append(encoded_text[i + seq_length])X = np.array(X)y = np.array(y)return X, y# 超参数
SEQ_LENGTH = 100 # 输入序列长度
BATCH_SIZE = 128
EPOCHS = 50
EMBEDDING_DIM = 64
RNN_UNITS = 256
DROPOUT_RATE = 0.2
LEARNING_RATE = 0.001
MAX_VOCAB = 1000 # 最大字符集大小# 下载并预处理文本数据
print("下载文本数据...")
text = download_text()
print(f"文本长度: {len(text)}")print("预处理文本数据...")
encoded_text, char_to_int, int_to_char, vocab_size = preprocess_text(text, max_vocab=MAX_VOCAB)
print(f"字符集大小: {vocab_size}")# 创建数据集
print("创建数据集...")
X, y = create_dataset(encoded_text, SEQ_LENGTH)
print(f"输入数据形状: {X.shape}")
print(f"目标数据形状: {y.shape}")# 分割训练和验证集
split = int(0.9 * len(X))
X_train, X_val = X[:split], X[split:]
y_train, y_val = y[:split], y[split:]# 构建模型
print("构建模型...")
model = Sequential()# 嵌入层
model.add(Embedding(input_dim=vocab_size, output_dim=EMBEDDING_DIM, input_length=SEQ_LENGTH))# 第一层SimpleRNN
model.add(SimpleRNN(RNN_UNITS, return_sequences=True))
model.add(Dropout(DROPOUT_RATE))
model.add(BatchNormalization())# 第二层SimpleRNN
model.add(SimpleRNN(RNN_UNITS))
model.add(Dropout(DROPOUT_RATE))
model.add(BatchNormalization())# 全连接层
model.add(Dense(256, activation='relu'))
model.add(Dropout(DROPOUT_RATE))# 输出层
model.add(Dense(vocab_size, activation='softmax'))# 编译模型
optimizer = Adam(learning_rate=LEARNING_RATE)
model.compile(loss='sparse_categorical_crossentropy', optimizer=optimizer, metrics=['accuracy'])# 模型概述
model.summary()# 设置回调
checkpoint_filepath = 'model_checkpoint.h5'
checkpoint = ModelCheckpoint(filepath=checkpoint_filepath, monitor='val_loss', save_best_only=True, verbose=1)
early_stopping = EarlyStopping(monitor='val_loss', patience=5, restore_best_weights=True, verbose=1)# 训练模型
print("开始训练模型...")
history = model.fit(X_train, y_train,validation_data=(X_val, y_val),epochs=EPOCHS,batch_size=BATCH_SIZE,callbacks=[checkpoint, early_stopping])# 加载最佳模型
model.load_weights(checkpoint_filepath)# 生成文本
def generate_text(model, int_to_char, char_to_int, seed_text, length=100):generated = seed_textencoded = [char_to_int[c] for c in seed_text if c in char_to_int]for _ in range(length):if len(encoded) < SEQ_LENGTH:input_seq = [0] * (SEQ_LENGTH - len(encoded)) + encodedelse:input_seq = encoded[-SEQ_LENGTH:]input_seq = np.array([input_seq])preds = model.predict(input_seq, verbose=0)[0]next_index = np.argmax(preds)next_char = int_to_char.get(next_index, '')generated += next_charencoded.append(next_index)return generated# 选择一个随机种子文本
start_index = np.random.randint(0, len(encoded_text) - SEQ_LENGTH - 1)
seed_encoded = encoded_text[start_index:start_index + SEQ_LENGTH]
seed_text = ''.join([int_to_char.get(i, '') for i in seed_encoded])
print("生成文本:")
print(generate_text(model, int_to_char, char_to_int, seed_text, length=1000))
总结
循环神经网络(RNN)通过其独特的循环连接和隐藏状态机制,能够有效处理和建模序列数据中的时序依赖关系。尽管标准RNN在训练过程中面临梯度消失和爆炸的挑战,但通过引入LSTM、GRU等变种,显著提升了其在捕捉长期依赖方面的能力。同时,RNN在多种实际应用中展现了强大的潜力。然而,随着Transformer等更高效模型的出现,RNN的应用场景逐渐被拓展和细化。未来,RNN与其他模型的结合,如混合神经网络,可能进一步推动其在人工智能领域的发展和应用。
参考文献:
如何从RNN起步,一步一步通俗理解LSTM_rnn lstm-CSDN博客
【精选】通俗易懂的RNN-CSDN博客
史上最小白之RNN详解-CSDN博客
结~~~