S'S ALGORITHM

循环神经网络RNN和LSTM


1 - 混淆的一个概念的澄清

RNN的R是Recurrent的意思是循环复发的意思,另外还有一个叫RNN的R事Recursive的意思,是一种递归的神经网络,他和循环神经网路哦不是一个东西,是用于处理树结构的神经网络。

之前谈过cnn,cnn也可以进行序列数据比如文本的处理,只不过是将2d变成了1d。在大的概念上,rnn和cnn都是技术的一种,不一定要将他们归类为解决特定问题的方法。

2 - 循环神经元:cell

RNN的核心是循环神经元,它就像是一个携带着记忆的时间旅行者。每当接收新的输入时,循环神经元会运用激活函数(通常是Tanh双曲正切函数),并将上一时刻的输入合并进学习数据。这就好比是一个输入自循环的过程,或者说是在时间轴上不断利用先前所有输入数据的演变。

循环神经元内部有一个称之为记忆单元的机制,它随着时间的推移将更早的输入影响逐渐稀释,而最近的输入影响则更为显著。这种机制使得RNN能够更好地处理序列数据,比如股票价格、机器翻译、语音、词序分析以及机器生成音乐等。

3 - RNN的拓扑结构应用场景

4 - 序列展开和时间反向传播

在理解循环神经网络(Recurrent Neural Network, RNN)的序列展开和时间反向传播之前,我们先了解一下RNN的基本结构和工作原理。

RNN是一种用于处理序列数据的神经网络结构,它在处理序列数据时具有记忆功能,可以利用之前的信息来影响当前的输出。RNN的基本结构包括一个循环连接,使得网络可以在序列数据上进行迭代操作。每个时间步的输入数据和上一时间步的隐藏状态会被输入到网络中,生成当前时间步的输出和隐藏状态,并在下一个时间步中继续被使用。

现在来解释序列展开和时间反向传播:

  1. 序列展开(Sequence Unrolling): 在训练RNN时,为了方便计算梯度和优化参数,通常会将网络在时间维度上展开成多个重复的网络结构。这个过程称为序列展开。展开后的网络会将序列数据按时间步展开成多个相同的网络结构,每个时间步都有相同的参数。这样做的好处是可以将RNN的计算过程转化为普通的前馈神经网络,利用反向传播算法来计算梯度并更新参数。

  2. 时间反向传播(Backpropagation Through Time, BPTT): 在展开的RNN网络中,每个时间步都有自己的损失函数,用于衡量当前时间步的预测输出与真实标签之间的差距。时间反向传播是指通过在展开的网络结构中沿着时间维度反向传播误差,计算每个时间步的参数的梯度。这个过程类似于普通的反向传播算法,但是由于网络在时间维度上展开,需要考虑到每个时间步的参数共享和梯度累积的情况。通过计算每个时间步的参数梯度,并根据梯度更新参数,可以训练RNN模型以最小化损失函数,提高预测性能。

这相当于在每一个时间步上,都有一个单独的层,所有层的权重相同。通过输入展开,要为每一个时间步计算不同损失。

序列展开和时间反向传播是用于训练循环神经网络的关键步骤,通过展开网络结构并在时间维度上进行反向传播,可以有效地计算梯度并更新参数,从而训练出适用于序列数据的神经网络模型。

梯度累计技巧的伪代码:

通过这种方式,可以在梯度累积的过程中,控制每个参数更新步骤的频率,从而在内存有限或者计算资源受限的情况下,有效地利用梯度信息进行模型训练。

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()  

5 - LSTM

尽管RNN在处理时间序列上表现出色,但训练过程中也面临着一系列问题:

梯度消失和梯度爆炸:

长短时记忆(LSTM):

为了解决梯度消失和梯度爆炸的问题,LSTM引入了长期和短期记忆的机制,适用于自然语言处理、时间序列预测等任务。

另外,门控循环单位(GRU)是LSTM的简化版本,通过门控机制实现信息的更新,同样适用于各种序列处理任务。

在训练RNN时,选择合适的拓扑结构和参数至关重要。错误的选择可能导致网络无法收敛,成为一项艰难的任务。

6 - 用PyTorch组装一个LSTM的cell

上面讲的四个门控公式就是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。

7 - cell之间的连接

以上只是一个cell,当然我们使用PyTorch的话直接可以帮我们定义一个cell,我们只需要定义输入输出就可以了。这就像是记忆在时间和空间上的连接,在LSTM中这种连接非常灵活,比如 one-to-one,one-to-many,many-to-one,many-to-many等。

代码示例。

1,One-to-Many(一对多)

这个示例中使用一个输入序列生成多个输出序列。

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(多对一)

在这个示例中使用多个输入序列生成一个输出序列。

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(多对多)

在这个示例中使用多个输入序列生成多个输出序列。

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)

8 - 一些使用PyTorch可以玩的代码(预测sin曲线)

它只是一个基本的可玩代码,输出结果不是很好,但是可以通过自己调整和增加层,提高预测结果。

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()

9 - LSTM的进化和改进

当长短期记忆(LSTM)模型被提出后,其结构并不是一成不变的,而是经历了多次改进和演化。这些改进的目标通常是为了提高模型的性能、训练速度、泛化能力等方面。以下是一些常见的关于 LSTM 的研究和改进:

  1. Gated Recurrent Unit (GRU):GRU 是对 LSTM 的一种简化版本,它合并了 LSTM 中的遗忘门和输入门,以及将细胞状态和隐藏状态合并为一个状态。这种简化的结构减少了参数数量,使得 GRU 更易于训练和优化。

  2. Peephole Connections:在标准 LSTM 中,门控机制仅依赖于当前时间步的输入和上一个时间步的隐藏状态。Peephole Connections 将细胞状态的信息也引入到门控机制中,使得门的开启和关闭更加精细化,从而改善了 LSTM 的建模能力。

  3. Clockwork RNN:这是一种将时间分解为多个时钟周期,每个周期拥有不同更新速度的 RNN 模型。Clockwork RNN 的基本思想是让不同部分的网络在不同的时间尺度上更新参数,这可以提高模型的灵活性和效率。

  4. Attention Mechanism:虽然不是直接针对 LSTM 的改进,但是 Attention Mechanism 已经成为了处理序列数据的一个重要技术。它允许模型在处理序列时动态地关注序列中的不同部分,从而更有效地捕获长距离的依赖关系。

  5. 深度 LSTM:将多个 LSTM 层堆叠起来构成深度 LSTM 模型,以增加模型的表达能力和抽象能力。深度 LSTM 模型在一些任务中表现出更好的性能,例如语言建模和机器翻译。

这些改进和研究使得 LSTM 模型在处理序列数据时更加灵活、高效,并且在许多任务中取得了优异的性能。随着深度学习的发展,我们可以期待更多关于 LSTM 的改进和新的变种的出现,以应对不同领域的挑战。

10 - 一些思考

通过开始对时间这个尺度的操作,增加了人们解决的问题的范畴。那就是序列问题。

序列问题指的是涉及到序列数据的任务或问题,其中数据是按照一定顺序排列的,每个数据点都与其前后的数据点有关联。序列数据可以是时间序列(例如股票价格、气温变化)、文本数据(例如句子、文章)、音频数据(例如语音信号)、视频数据(例如帧序列)等。

在序列问题中,数据的顺序和时间通常是重要的考虑因素。例如,在时间序列预测问题中,模型需要根据过去的数据来预测未来的数据;比如在自然语言处理中,文本数据的每个词语都按照一定的顺序排列,并且前面的词语通常会影响后面的词语的含义和语境。

我会联想到,回归问题也和时间有关,但是回归问题通常是指根据输入特征来预测一个连续的数值型输出变量。虽然回归问题中的数据也可能与时间相关,但回归问题通常不涉及到数据之间的顺序关系。例如,如果我们要预测房屋价格,虽然我们可以使用时间作为一个特征(如房屋建造年份),但我们通常不会考虑房屋价格与时间之间的序列关系。因此,尽管回归问题中的数据可能与时间有关,但如果问题的重点是预测一个连续的数值型输出变量而不是处理数据之间的顺序关系,通常不会将其归类为序列问题。相比之下,序列问题更侧重于处理数据的顺序关系和时间相关性,因此通常涉及到使用序列模型来建模和解决。