一、循环神经网络(RNN)的发展历史
1986年,Elman等人提出了用于处理序列数据的循环神经网络。如同卷积神经网络是专门用于处理二维数据信息(如图像)的神经网络,循环神经网络是专用于处理序列信息的神经网络。循环网络可以扩展到更长的序列,大多数循环神经网络可以处理可变长度的序列,循环神经网络的诞生解决了传统神经网络在处理序列信息方面的局限性。
1997年,Hochreiter和Schmidhuber提出了长短时记忆单元(Long Short-Term Memory, LSTM) 用于解决标准循环神经网络时间维度的梯度消失问题(vanishing gradient problem)。标准的循环神经网络结构存储的上下文信息的范围有限,限制了RNN的应用。LSTM型RNN用LSTM单元替换标准结构中的神经元节点,LSTM单元使用输入门、输出门和遗忘门控制序列信息的传输,从而实现较大范围的上下文信息的保存与传输。
1998年,Williams和Zipser提出名为“随时间反向传播(Backpropagation Through Time, BPTT)”的循环神经网络训练算法。BPTT算法的本质是按照时间序列将循环神经网络展开,展开后的网络包含N(时间步长)个隐含单元和一个输出单元,然后采用反向误差传播方式对神经网络的连接权值进行更新。
2001年,Gers和Schmidhuber提出了具有重大意义的LSTM型RNN优化模型,在传统的LSTM单元中加入了窥视孔连接(peephole connections)。具有窥视孔连接的LSTM型RNN模型是循环神经网络最流行的模型之一,窥视孔连接进一步提高了LSTM单元对具有长时间间隔相关性特点的序列信息的处理能力。2005年,Graves成功将LSTM型RNN应用于语音处理;2007年,Hochreiter将LSTM型RNN应用于生物信息学研究。
二、什么是循环神经网络(RNN)
循环神经网络(Recurrent Neural Network,RNN)是一种深度学习模型,专门设计用于处理序列数据,RNN对具有序列特性的数据非常有效,它能挖掘数据中的时序信息以及语义信息,例如时间序列、自然语言文本等。RNN 具有一种记忆功能,可以在处理序列数据时考虑先前的信息。
那么什么又是序列特性呢?
序列特性指的是数据按照一定的顺序排列,并且相邻元素之间存在关联或依赖关系。序列数据在时间序列、自然语言文本、DNA序列等领域中非常常见,具有动态演变的性质。
例如:
-
时间序列数据: 股票价格、气温、销售额等数据都是时间序列数据。在这些数据中,前一个时间点的值通常会影响到后一个时间点的值,因此具有序列特性。
-
自然语言处理(NLP): 文本是一个典型的序列数据。句子中的单词按照一定的顺序排列,并且前面的单词通常会影响后面的理解。例如,对于一个句子 "猫追逐老鼠",单词的顺序对于理解动作的进行是至关重要的。
-
DNA序列: 生物学中的DNA序列是由四种碱基(腺嘌呤、胸腺嘧啶、鸟嘌呤、胞嘧啶)组成的序列。这些碱基的排列顺序对于生物体的遗传特性具有关键作用。
-
音频信号: 在语音处理领域,音频信号是一个连续的序列,语音中的音素和语音信号的频谱分布都具有序列特性。
-
推荐系统: 用户的行为历史可以看作是一个序列,例如用户在电商网站上的购买记录。基于用户历史行为的推荐系统可以利用序列特性预测用户可能感兴趣的产品。
三、为什么要用循环神经网络(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)的原理和结构
让我们来看一个简单的RNN,如图,它由输入层、一个隐藏层和一个输出层组成:
乍一看是不是觉得有点懵,那就让我们一点一点看。其实如果去掉右边这个w,这就是一个简单的全连接神经网络,X是一个向量,也就是某个字或词的特征向量,U是输入层到隐藏层的参数矩阵,S是隐藏层的向量,V是隐藏层到输出层的参数矩阵,O是输出层的向量。知道这个结构之后,那么右边的w到底表示什么意思呢?下面我们把上面的图继续展开,如下图所示:
再一看,更懵了!其实举个例子就很明白:有一句话是 I love you,那么在利用RNN做一些事情时,比如命名实体识别,上图中的 代表的就是I这个单词的向量,代表的是love这个单词的向量, 代表的是you这个单词的向量,以此类推,我们注意到,上图展开后,W一直没有变,W其实是每个时间点之间的权重矩阵,我们注意到,RNN之所以可以解决序列问题,是因为它可以记住每一时刻的信息,每一时刻的隐藏层不仅由该时刻的输入层决定,还由上一时刻的隐藏层决定,公式如下,其中 代表t时刻的输出, 代表t时刻的隐藏层的值:
我们给出上面抽象图的具体图:结合上面所描述的是不是就恍然大悟了
切记:w一直不发生变化
举个例子说明RNN输入到输出的过程:(以下图全部来自史上最详细循环神经网络讲解(RNN/LSTM/GRU) - 知乎 (zhihu.com),可以去看看这个,讲的很清楚)
假设现在我们已经训练好了一个RNN,如图,我们假设每个单词的特征向量是二维的,也就是输入层的维度是二维,且隐藏层也假设是二维,输出也假设是二维,所有权重的值都为1且没有偏差且所有激活函数都是线性函数,现在输入一个序列,到该模型中,我们来一步步求解出输出序列:
W在实际的计算中可以想象上一时刻的隐藏层的值是被存起来,等下一时刻的隐藏层进来时,上一时刻的隐藏层的值通过与权重相乘,两者相加便得到了下一时刻真正的隐藏层,如图 可以看做每一时刻存下来的值,当然初始时是没有存值的,因此初始值为0:
当我们输入第一个序列,【1,1】,如下图,其中隐藏层的值,也就是绿色神经元,是通过公式 计算得到的,因为所有权重都是1,所以也就是 1∗1+1∗1+1∗0+1∗0=2 ,输出层的值4是通过公式 计算得到的,也就是 2∗1+2∗1=4 ,得到输出向量【4,4】:
当【1,1】输入过后, 已经不是0了,而是把这一时刻的隐藏状态放在里面,即变成了2,如图,输入下一个向量【1,1】,隐藏层的值通过公式得到, 1∗1+1∗1+1∗2+1∗2=6 ,输出层的值通过公式,得到 6∗1+6∗1=12 ,最终得到输出向量【12,12】:
同理,a1,a2继续更新,然后得到下图:
最后我们就得到了一个新的序列
五、示例
示例1:预测正弦函数
import torch
import torch.nn as nn
import numpy as np
import matplotlib.pyplot as plt
# 生成正弦函数序列作为输入
sequence_length = 100
time_steps = np.linspace(0, 10, sequence_length)
input_sequence = np.sin(time_steps)
# 转换为 PyTorch 的 Tensor
input_sequence = torch.FloatTensor(input_sequence).view(-1, 1, 1)
# 定义简单的循环神经网络模型
class SimpleRNN(nn.Module):
def __init__(self, input_size, hidden_size, output_size):
super(SimpleRNN, self).__init__()
self.rnn = nn.RNN(input_size, hidden_size, batch_first=True)
self.fc = nn.Linear(hidden_size, output_size)
def forward(self, x):
out, _ = self.rnn(x)
out = self.fc(out[:, -1, :]) # 取最后一个时间步的输出
return out
# 设置模型参数
input_size = 1
hidden_size = 32
output_size = 1
# 创建模型实例
model = SimpleRNN(input_size, hidden_size, output_size)
# 定义损失函数和优化器
criterion = nn.MSELoss()
optimizer = torch.optim.Adam(model.parameters(), lr=0.01)
# 训练模型
num_epochs = 1000
for epoch in range(num_epochs):
outputs = model(input_sequence)
loss = criterion(outputs, input_sequence.view(-1, 1))
optimizer.zero_grad()
loss.backward()
optimizer.step()
if (epoch + 1) % 100 == 0:
print(f'Epoch [{epoch + 1}/{num_epochs}], Loss: {loss.item():.4f}')
# 测试模型
model.eval()
with torch.no_grad():
future = 100
pred_sequence = input_sequence.clone()
for _ in range(future):
pred = model(pred_sequence)
pred_sequence = torch.cat([pred_sequence, pred.unsqueeze(1)], dim=1)
# 将结果转换为 numpy 数组
pred_sequence = pred_sequence.view(-1).numpy()
# 绘制原始序列和预测序列
plt.figure(figsize=(12, 6))
plt.title('RNN Sequence Prediction')
plt.plot(time_steps, input_sequence.view(-1).numpy(), label='Original Sequence', color='blue')
plt.plot(np.linspace(10, 12, future), pred_sequence[-future:], label='Predicted Sequence', color='red',
linestyle='dashed')
plt.legend()
plt.show()
示例2:预测莎士比亚戏剧文本
import torch
import torch.nn as nn
import numpy as np
# 读取莎士比亚的戏剧文本(或者你可以选择其他文本)
with open(r'E:\project\测试.txt', 'r', encoding='utf-8') as file:
text = file.read()
# 建立字符到索引和索引到字符的映射
chars = sorted(list(set(text)))
char_to_index = {char: i for i, char in enumerate(chars)}
index_to_char = {i: char for i, char in enumerate(chars)}
# 将文本转换为索引序列
text_as_indices = [char_to_index[char] for char in text]
# 定义字符级别的循环神经网络模型
class CharRNN(nn.Module):
def __init__(self, input_size, hidden_size, output_size, num_layers=1):
super(CharRNN, self).__init__()
self.embedding = nn.Embedding(input_size, hidden_size)
self.rnn = nn.GRU(hidden_size, hidden_size, num_layers, batch_first=True)
self.fc = nn.Linear(hidden_size, output_size)
def forward(self, x, hidden):
x = self.embedding(x)
out, hidden = self.rnn(x, hidden)
out = self.fc(out)
return out, hidden
# 设置模型参数
input_size = len(chars)
hidden_size = 256
output_size = len(chars)
num_layers = 2
# 创建模型实例
model = CharRNN(input_size, hidden_size, output_size, num_layers)
# 定义损失函数和优化器
criterion = nn.CrossEntropyLoss()
optimizer = torch.optim.Adam(model.parameters(), lr=0.001)
# 转换为 PyTorch 的 Tensor
text_as_indices = torch.tensor(text_as_indices, dtype=torch.long)
# 设置训练参数
seq_length = 100
num_epochs = 2000
learning_rate = 0.001
# 训练模型
for epoch in range(num_epochs):
for i in range(0, len(text_as_indices) - seq_length, seq_length):
inputs = text_as_indices[i:i+seq_length]
targets = text_as_indices[i+1:i+1+seq_length]
inputs = torch.unsqueeze(inputs, dim=0)
targets = torch.unsqueeze(targets, dim=0)
hidden = None
optimizer.zero_grad()
outputs, _ = model(inputs, hidden)
loss = criterion(outputs.view(-1, output_size), targets.view(-1))
loss.backward()
optimizer.step()
if (epoch+1) % 100 == 0:
print(f'Epoch [{epoch+1}/{num_epochs}], Loss: {loss.item():.4f}')
# 测试模型生成新文本
model.eval()
with torch.no_grad():
start_index = np.random.randint(0, len(text_as_indices) - seq_length)
seed_text = text_as_indices[start_index:start_index + seq_length]
generated_text = seed_text.tolist()
hidden = None
for _ in range(500): # 生成500个字符的文本
inputs = torch.unsqueeze(torch.tensor(seed_text[-seq_length:], dtype=torch.long), dim=0)
outputs, hidden = model(inputs, hidden)
probabilities = nn.functional.softmax(outputs[0, -1, :], dim=0).numpy()
next_index = np.random.choice(len(chars), p=probabilities)
generated_text.append(next_index)
# 将生成的索引序列转换为文本
generated_text = ''.join(index_to_char[index] for index in generated_text)
print(generated_text)
参考:史上最详细循环神经网络讲解(RNN/LSTM/GRU) - 知乎 (zhihu.com)一文搞懂RNN(循环神经网络)基础篇 - 知乎 (zhihu.com)
本文仅作为学习笔记使用