RNN的R是Recurrent的意思是循环复发的意思,另外还有一个叫RNN的R事Recursive的意思,是一种递归的神经网络,他和循环神经网路哦不是一个东西,是用于处理树结构的神经网络。
之前谈过cnn,cnn也可以进行序列数据比如文本的处理,只不过是将2d变成了1d。在大的概念上,rnn和cnn都是技术的一种,不一定要将他们归类为解决特定问题的方法。
RNN的核心是循环神经元,它就像是一个携带着记忆的时间旅行者。每当接收新的输入时,循环神经元会运用激活函数(通常是Tanh双曲正切函数),并将上一时刻的输入合并进学习数据。这就好比是一个输入自循环的过程,或者说是在时间轴上不断利用先前所有输入数据的演变。
循环神经元内部有一个称之为记忆单元的机制,它随着时间的推移将更早的输入影响逐渐稀释,而最近的输入影响则更为显著。这种机制使得RNN能够更好地处理序列数据,比如股票价格、机器翻译、语音、词序分析以及机器生成音乐等。
在理解循环神经网络(Recurrent Neural Network, RNN)的序列展开和时间反向传播之前,我们先了解一下RNN的基本结构和工作原理。
RNN是一种用于处理序列数据的神经网络结构,它在处理序列数据时具有记忆功能,可以利用之前的信息来影响当前的输出。RNN的基本结构包括一个循环连接,使得网络可以在序列数据上进行迭代操作。每个时间步的输入数据和上一时间步的隐藏状态会被输入到网络中,生成当前时间步的输出和隐藏状态,并在下一个时间步中继续被使用。
现在来解释序列展开和时间反向传播:
序列展开(Sequence Unrolling): 在训练RNN时,为了方便计算梯度和优化参数,通常会将网络在时间维度上展开成多个重复的网络结构。这个过程称为序列展开。展开后的网络会将序列数据按时间步展开成多个相同的网络结构,每个时间步都有相同的参数。这样做的好处是可以将RNN的计算过程转化为普通的前馈神经网络,利用反向传播算法来计算梯度并更新参数。
时间反向传播(Backpropagation Through Time, BPTT): 在展开的RNN网络中,每个时间步都有自己的损失函数,用于衡量当前时间步的预测输出与真实标签之间的差距。时间反向传播是指通过在展开的网络结构中沿着时间维度反向传播误差,计算每个时间步的参数的梯度。这个过程类似于普通的反向传播算法,但是由于网络在时间维度上展开,需要考虑到每个时间步的参数共享和梯度累积的情况。通过计算每个时间步的参数梯度,并根据梯度更新参数,可以训练RNN模型以最小化损失函数,提高预测性能。
这相当于在每一个时间步上,都有一个单独的层,所有层的权重相同。通过输入展开,要为每一个时间步计算不同损失。
序列展开和时间反向传播是用于训练循环神经网络的关键步骤,通过展开网络结构并在时间维度上进行反向传播,可以有效地计算梯度并更新参数,从而训练出适用于序列数据的神经网络模型。
梯度累计技巧的伪代码:
accumulate_gradient_steps = 2
: 设置了梯度累积的步数为2,每处理两个批次的数据后,才执行一次参数更新步骤。loss = criterion(predictions, targets)/accumulate_gradient_steps
: 计算预测结果与真实目标之间的损失,然后除以累积梯度步数,得到每个批次的平均损失。if counter % accumulate_gradient_steps ==0:
: 判断当前批次是否是累积梯度步数的倍数。如果是,表示已经累积了指定的梯度步数,此时执行参数更新步骤。通过这种方式,可以在梯度累积的过程中,控制每个参数更新步骤的频率,从而在内存有限或者计算资源受限的情况下,有效地利用梯度信息进行模型训练。
accumulate_gradient_steps = 2
for counter, data in enumerate(dataloader):
inputs, targets = data
predictions = model(inputs)
loss = criterion(predictions, targets)/accumulate_gradient_steps
loss.backward()
if counter % accumulate_gradient_steps ==0:
# 优化器更新参数
optimizer.step()
# 梯度清零,以便下次计算
optimizer.zero_grad()
尽管RNN在处理时间序列上表现出色,但训练过程中也面临着一系列问题:
梯度消失和梯度爆炸:
长短时记忆(LSTM):
为了解决梯度消失和梯度爆炸的问题,LSTM引入了长期和短期记忆的机制,适用于自然语言处理、时间序列预测等任务。
short term memory with hidden state:短期记忆内容是有权重更新的。
短期记忆的input经过权重处理和激活函数(sigmoid输出[0, 1])的计算,完成第一个stage的处理,这个处理出来的一个结果是一个百分比,这个百分比决定了有多少long-term-memory会被保留下来。这个stage的功能也被叫做forget gate遗忘门,虽然表达的是有多少被记忆,但是其实也是决定有多少被忘却。遗忘门包括一个权重矩阵和一个偏置向量,它们用于计算遗忘门的输出。
在第二个stage的处理中,继续对短期记忆的内容进行处理,得到潜在长期记忆(tanh激活函数)和潜在长期记忆百分比(sigmoid激活函数),将两者进行相乘,得到当前的短期记忆是否会成为长期记忆,以及有多少内容成为长期记忆。这个stage的功能也被叫做input gate记忆门。输入门包括两组参数,一组用于计算候选记忆值,另一组用于计算输入门的输出。
更新记忆状态将遗忘门的输出与输入门的输出相乘,并将结果与候选记忆值相加,从而更新记忆状态。该步骤不涉及额外的参数,而是利用遗忘门的输出和输入门的输出来更新记忆状态。
在长短期记忆(LSTM)模型中,细胞状态(cell state)C是一种主要的内部状态,用于存储模型在处理序列数据过程中学到的长期信息。
在第三个stage的处理中,使用之前使用了两次的sigmoid激活函数和tanh函数得到有多少的长期记忆会被转化为短期记忆的百分比,最终计算得到输出。这个stage又被称为output gate输出门。输出门包括一个权重矩阵和一个偏置向量,用于计算输出门的输出。
记忆单元(Memory Cell):LSTM引入了一个称为记忆单元的结构,该单元可以存储信息并在长时间跨度上保持这些信息。通过精心设计的门控结构,记忆单元可以决定何时记忆、读取或清除信息,从而有效地管理和控制信息的流动,有助于解决长期依赖问题。
门控结构:LSTM包含三种门:遗忘门(forget gate)、输入门(input gate)和输出门(output gate)。这些门控制着信息流的传递和遗忘,使得网络可以根据输入数据的特征自适应地选择性地记忆或遗忘信息。通过这种门控机制,LSTM可以有效地处理不同时间步长上的信息,从而更好地捕捉时间序列数据中的长期依赖关系。
梯度传播:由于LSTM中门控单元的存在,网络的误差可以在时间上更有效地传播。相比于普通的RNN,LSTM中的梯度可以更容易地在时间上保持稳定,避免了梯度消失或梯度爆炸问题,有助于提高网络的训练效率和性能。
灵活性:LSTM网络具有很高的灵活性,可以适应各种时间序列数据的特征和模式。通过调整记忆单元的大小、门控结构的参数等,可以根据具体任务的需求对网络进行调整和优化,从而获得更好的性能。
总的来说,LSTM之所以能够起作用,主要是因为它通过记忆单元和门控结构有效地解决了长期依赖问题,同时具有良好的梯度传播性质和灵活性,使得它在处理各种时间序列任务中表现出色。
另外,门控循环单位(GRU)是LSTM的简化版本,通过门控机制实现信息的更新,同样适用于各种序列处理任务。
在训练RNN时,选择合适的拓扑结构和参数至关重要。错误的选择可能导致网络无法收敛,成为一项艰难的任务。
上面讲的四个门控公式就是LSTM的主要流程,使用PyTorch将他们转化为代码。
import torch
from torch import nn
class My_LSTM_cell(torch.nn.Module):
"""
A simple LSTM cell network by PyTorch
"""
def __init__(self, input_length=10, hidden_length=20):
super(My_LSTM_cell, self).__init__()
# 输入长度和隐藏状态长度
self.input_length = input_length
self.hidden_length = hidden_length
# 遗忘门组件
self.linear_gate_w1 = nn.Linear(self.input_length, self.hidden_length, bias=True)
self.linear_gate_r1 = nn.Linear(self.hidden_length, self.hidden_length, bias=False)
self.sigmoid_forget = nn.Sigmoid()
# 输入门组件
self.linear_gate_w2 = nn.Linear(self.input_length, self.hidden_length, bias=True)
self.linear_gate_r2 = nn.Linear(self.hidden_length, self.hidden_length, bias=False)
self.sigmoid_gate = nn.Sigmoid()
# 细胞状态组件
self.linear_gate_w3 = nn.Linear(self.input_length, self.hidden_length, bias=True)
self.linear_gate_r3 = nn.Linear(self.hidden_length, self.hidden_length, bias=False)
self.activation_gate = nn.Tanh()
# 输出门组件
self.linear_gate_w4 = nn.Linear(self.input_length, self.hidden_length, bias=True)
self.linear_gate_r4 = nn.Linear(self.hidden_length, self.hidden_length, bias=False)
self.sigmoid_hidden_out = nn.Sigmoid()
# 最终输出
self.activation_final = nn.Tanh()
def forget_gate(self, x, h):
x = self.linear_gate_w1(x)
h = self.linear_gate_r1(h)
return self.sigmoid_gate(x + h)
def input_gate(self, x, h):
x_temp = self.linear_gate_w2(x)
h_temp = self.linear_gate_r2(h)
i = self.sigmoid_gate(x_temp + h_temp)
return i
def cell_memory_gate(self, i, f, x, h, c_prev):
'''
i : input gate
f : forget gate
x : input
h : hidden state
c_prev : last cell state
'''
x = self.linear_gate_w3(x)
h = self.linear_gate_r3(h)
# 使用激活函数,得到要注入新细胞中的新信息
# 使用输入门 i 控制门控向量,得到加权后的门控向量
k = self.activation_gate(x + h)
g = k * i
# 使用遗忘门 f 控制上一个细胞状态 c_prev 中的信息,得到遗忘后的旧细胞状态 c
c = f * c_prev
# 将遗忘后的旧细胞状态 c 与加权后的门控向量 g 相加
# 得到新的细胞状态 c_next,它即为当前时间步的细胞状态,即 LSTM 单元的输出
c_next = g + c
return c_next
def out_gate(self, x, h):
x = self.linear_gate_w4(x)
h = self.linear_gate_r4(h)
return self.sigmoid_hidden_out(x + h)
def forward(self, x, tuple_in):
(h, c_prev) = tuple_in
# 方程 1. 输入门
i = self.input_gate(x, h)
# 方程 2. 遗忘门
f = self.forget_gate(x, h)
# 方程 3. 更新记忆状态
c_next = self.cell_memory_gate(i, f, x, h,c_prev)
# 方程 4. 计算输出门
o = self.out_gate(x, h)
# 方程 5. 计算下一个隐藏输出
h_next = o * self.activation_final(c_next)
return h_next, c_next
PS:
在这个例子中,super()
函数的参数 My_LSTM_cell
是用来指定要调用的父类的名称。在 Python 中,通常情况下会将当前类的名称作为 super()
函数的第一个参数,这是一种常见的做法。
在 super(My_LSTM_cell, self).__init__()
中,My_LSTM_cell
是当前类的名称,self
是当前对象的实例。这样写的目的是确保在调用父类的构造函数时,Python 能够正确地确定要调用的父类是哪一个。即使子类被继承多次,也能够准确地找到正确的父类。
总之,这种写法是一种通用的做法,用于确保在多重继承等情况下,能够正确地调用父类的构造函数。
多复杂的构架,底层都很多线性计算,因为一维的线性是高维度的组成单元。第一性原理出发,所有的都要回到原点。
这个例子中复现了整个LSTM的公式流程。
即使这段代码对我来说已经很复杂了,但是其实它只实现了一个cell。
以上只是一个cell,当然我们使用PyTorch的话直接可以帮我们定义一个cell,我们只需要定义输入输出就可以了。这就像是记忆在时间和空间上的连接,在LSTM中这种连接非常灵活,比如 one-to-one,one-to-many,many-to-one,many-to-many等。
代码示例。
1,One-to-Many(一对多)
这个示例中使用一个输入序列生成多个输出序列。
forward
方法中,每个时间步的LSTM输出都经过线性层和softmax层,产生每个时间步的输出概率。import torch
import torch.nn as nn
class LSTMOneToMany(nn.Module):
def __init__(self, input_size, hidden_size, output_size):
super(LSTMOneToMany, self).__init__()
self.hidden_size = hidden_size
self.lstm = nn.LSTM(input_size, hidden_size)
self.linear = nn.Linear(hidden_size, output_size)
self.softmax = nn.Softmax(dim=1)
def forward(self, input_seq):
lstm_out, _ = self.lstm(input_seq)
output_seq = self.linear(lstm_out)
output_probs = self.softmax(output_seq)
return output_probs
# Example usage
input_size = 10
hidden_size = 20
output_size = 3
seq_length = 5
batch_size = 1
lstm_model = LSTMOneToMany(input_size, hidden_size, output_size)
input_seq = torch.randn(seq_length, batch_size, input_size)
output_probs = lstm_model(input_seq)
print("Output probabilities shape:", output_probs.shape) # 输出概率的形状为 (seq_length, batch_size, output_size)
2,Many-to-One(多对一)
在这个示例中使用多个输入序列生成一个输出序列。
forward
方法中,只使用最后一个时间步的LSTM输出,经过线性层和softmax层,产生最终的输出概率。class LSTMManyToOne(nn.Module):
def __init__(self, input_size, hidden_size, output_size):
super(LSTMManyToOne, self).__init__()
self.hidden_size = hidden_size
self.lstm = nn.LSTM(input_size, hidden_size)
self.linear = nn.Linear(hidden_size, output_size)
self.softmax = nn.Softmax(dim=1)
def forward(self, input_seq):
lstm_out, _ = self.lstm(input_seq)
last_output = lstm_out[-1] # 取最后一个时间步的输出
output_seq = self.linear(last_output)
output_probs = self.softmax(output_seq)
return output_probs
# Example usage
seq_length = 5
batch_size = 1
input_seq = torch.randn(seq_length, batch_size, input_size)
lstm_model = LSTMManyToOne(input_size, hidden_size, output_size)
output_probs = lstm_model(input_seq)
print("Output probabilities shape:", output_probs.shape) # 输出概率的形状为 (batch_size, output_size)
3,Many-to-Many(多对多)
在这个示例中使用多个输入序列生成多个输出序列。
forward
方法中,每个时间步的LSTM输出都经过线性层和softmax层,产生每个时间步的输出概率。class LSTMManyToMany(nn.Module):
def __init__(self, input_size, hidden_size, output_size):
super(LSTMManyToMany, self).__init__()
self.hidden_size = hidden_size
self.lstm = nn.LSTM(input_size, hidden_size)
self.linear = nn.Linear(hidden_size, output_size)
self.softmax = nn.Softmax(dim=1)
def forward(self, input_seq):
lstm_out, _ = self.lstm(input_seq)
output_seq = self.linear(lstm_out)
output_probs = self.softmax(output_seq)
return output_probs
# Example usage
seq_length = 5
batch_size = 1
input_seq = torch.randn(seq_length, batch_size, input_size)
lstm_model = LSTMManyToMany(input_size, hidden_size, output_size)
output_probs = lstm_model(input_seq)
print("Output probabilities shape:", output_probs.shape) # 输出概率的形状为 (seq_length, batch_size, output_size)
它只是一个基本的可玩代码,输出结果不是很好,但是可以通过自己调整和增加层,提高预测结果。
import random
import numpy as np
import torch
from torch import nn
import matplotlib
import matplotlib.pyplot as plt
%matplotlib inline
import torch.optim as optim
#set seed to be able to replicate the resutls
seed = 172
random.seed(seed)
torch.manual_seed(seed)
def generate_sin_wave_data():
T = 20
L = 1000
N = 200
x = np.empty((N, L), 'int64')
x[:] = np.array(range(L)) + np.random.randint(-4 * T, 4 * T, N).reshape(N, 1)
data = np.sin(x / 1.0 / T).astype('float64')
return data
class Sequence(nn.Module):
def __init__(self):
super(Sequence, self).__init__()
self.rnn1 = nn.LSTMCell(1, 51)
self.rnn2 = nn.LSTMCell(51, 51)
self.linear = nn.Linear(51, 1)
def forward(self, input, future=0):
outputs = []
h_t = torch.zeros(input.size(0), 51, dtype=torch.double)
c_t = torch.zeros(input.size(0), 51, dtype=torch.double)
h_t2 = torch.zeros(input.size(0), 51, dtype=torch.double)
c_t2 = torch.zeros(input.size(0), 51, dtype=torch.double)
for i, input_t in enumerate(input.chunk(input.size(1), dim=1)):
h_t, c_t = self.rnn1(input_t, (h_t, c_t))
h_t2, c_t2 = self.rnn2(h_t, (h_t2, c_t2))
output = self.linear(h_t2)
outputs += [output]
# if we should predict the future
for i in range(future):
h_t, c_t = self.rnn1(input_t, (h_t, c_t))
h_t2, c_t2 = self.rnn2(h_t, (h_t2, c_t2))
output = self.linear(h_t2)
outputs += [output]
outputs = torch.stack(outputs, 1).squeeze(2)
return outputs
def train():
# load data and make training set
data = generate_sin_wave_data()
input = torch.from_numpy(data[3:, :-1])
target = torch.from_numpy(data[3:, 1:])
test_input = torch.from_numpy(data[:3, :-1])
test_target = torch.from_numpy(data[:3, 1:])
seq = Sequence()
seq.double()
criterion = nn.MSELoss()
# use LBFGS as optimizer since we can load the whole data to train
optimizer = optim.LBFGS(seq.parameters(), lr=0.8)
# begin to train
for i in range(1):
print('STEP: ', i)
def closure():
optimizer.zero_grad()
out = seq(input)
loss = criterion(out, target)
print('loss:', loss.item())
loss.backward()
return loss
optimizer.step(closure)
# begin to predict, no need to track gradient here
with torch.no_grad():
future = 1000
pred = seq(test_input, future=future)
loss = criterion(pred[:, :-future], test_target)
print('test loss:', loss.item())
y = pred.detach().numpy()
# draw the result
plt.figure(figsize=(30, 10))
plt.title('Predict future values for time sequences\n(Dashlines are predicted values)', fontsize=30)
plt.xlabel('x', fontsize=20)
plt.ylabel('y', fontsize=20)
plt.xticks(fontsize=20)
plt.yticks(fontsize=20)
def draw(yi, color):
plt.plot(np.arange(input.size(1)), yi[:input.size(1)], color, linewidth=2.0)
plt.plot(np.arange(input.size(1), input.size(1) + future), yi[input.size(1):], color + ':', linewidth=2.0)
draw(y[0], 'r')
draw(y[1], 'g')
draw(y[2], 'b')
plt.show()
if __name__ == '__main__':
generate_sin_wave_data()
train()
当长短期记忆(LSTM)模型被提出后,其结构并不是一成不变的,而是经历了多次改进和演化。这些改进的目标通常是为了提高模型的性能、训练速度、泛化能力等方面。以下是一些常见的关于 LSTM 的研究和改进:
Gated Recurrent Unit (GRU):GRU 是对 LSTM 的一种简化版本,它合并了 LSTM 中的遗忘门和输入门,以及将细胞状态和隐藏状态合并为一个状态。这种简化的结构减少了参数数量,使得 GRU 更易于训练和优化。
Peephole Connections:在标准 LSTM 中,门控机制仅依赖于当前时间步的输入和上一个时间步的隐藏状态。Peephole Connections 将细胞状态的信息也引入到门控机制中,使得门的开启和关闭更加精细化,从而改善了 LSTM 的建模能力。
Clockwork RNN:这是一种将时间分解为多个时钟周期,每个周期拥有不同更新速度的 RNN 模型。Clockwork RNN 的基本思想是让不同部分的网络在不同的时间尺度上更新参数,这可以提高模型的灵活性和效率。
Attention Mechanism:虽然不是直接针对 LSTM 的改进,但是 Attention Mechanism 已经成为了处理序列数据的一个重要技术。它允许模型在处理序列时动态地关注序列中的不同部分,从而更有效地捕获长距离的依赖关系。
深度 LSTM:将多个 LSTM 层堆叠起来构成深度 LSTM 模型,以增加模型的表达能力和抽象能力。深度 LSTM 模型在一些任务中表现出更好的性能,例如语言建模和机器翻译。
这些改进和研究使得 LSTM 模型在处理序列数据时更加灵活、高效,并且在许多任务中取得了优异的性能。随着深度学习的发展,我们可以期待更多关于 LSTM 的改进和新的变种的出现,以应对不同领域的挑战。
通过开始对时间这个尺度的操作,增加了人们解决的问题的范畴。那就是序列问题。
序列问题指的是涉及到序列数据的任务或问题,其中数据是按照一定顺序排列的,每个数据点都与其前后的数据点有关联。序列数据可以是时间序列(例如股票价格、气温变化)、文本数据(例如句子、文章)、音频数据(例如语音信号)、视频数据(例如帧序列)等。
在序列问题中,数据的顺序和时间通常是重要的考虑因素。例如,在时间序列预测问题中,模型需要根据过去的数据来预测未来的数据;比如在自然语言处理中,文本数据的每个词语都按照一定的顺序排列,并且前面的词语通常会影响后面的词语的含义和语境。
我会联想到,回归问题也和时间有关,但是回归问题通常是指根据输入特征来预测一个连续的数值型输出变量。虽然回归问题中的数据也可能与时间相关,但回归问题通常不涉及到数据之间的顺序关系。例如,如果我们要预测房屋价格,虽然我们可以使用时间作为一个特征(如房屋建造年份),但我们通常不会考虑房屋价格与时间之间的序列关系。因此,尽管回归问题中的数据可能与时间有关,但如果问题的重点是预测一个连续的数值型输出变量而不是处理数据之间的顺序关系,通常不会将其归类为序列问题。相比之下,序列问题更侧重于处理数据的顺序关系和时间相关性,因此通常涉及到使用序列模型来建模和解决。