这是一个造轮子的过程,但是从头构建 LSTM 能够使我们对体系结构进行更加了解,并将我们的研究带入下一个层次。

    LSTM 单元是递归神经网络深度学习研究领域中最有趣的结构之一:它不仅使模型能够从长序列中学习,而且还为长、短期记忆创建了一个数值抽象,可以在需要时相互替换。

    LSTM:原理及其实现 - 图1

    在这篇文章中,我们不仅将介绍 LSTM 单元的体系结构,还将通过 PyTorch 手工实现它。

    最后但最不重要的是,我们将展示如何对我们的实现做一些小的调整,以实现一些新的想法,这些想法确实出现在 LSTM 研究领域,如 peephole。

    LSTM 被称为门结构:一些数学运算的组合,这些运算使信息流动或从计算图的那里保留下来。因此,它能够 “决定” 其长期和短期记忆,并输出对序列数据的可靠预测:

    LSTM:原理及其实现 - 图2

    LSTM 单元中的预测序列。注意,它不仅会传递预测值,而且还会传递一个 c,c 是长期记忆的代表

    遗忘门(forget gate)是输入信息与候选者一起操作的门,作为长期记忆。请注意,在输入、隐藏状态和偏差的第一个线性组合上,应用一个 sigmoid 函数:

    LSTM:原理及其实现 - 图3

    sigmoid 将遗忘门的输出 “缩放” 到 0-1 之间,然后,通过将其与候选者相乘,我们可以将其设置为 0,表示长期记忆中的 “遗忘”,或者将其设置为更大的数字,表示我们从长期记忆中记住的 “多少”。

    输入门是将包含在输入和隐藏状态中的信息组合起来,然后与候选和部分候选 c’’u t 一起操作的地方:

    LSTM:原理及其实现 - 图4

    在这些操作中,决定了多少新信息将被引入到内存中,如何改变——这就是为什么我们使用 tanh 函数(从 - 1 到 1)。我们将短期记忆和长期记忆中的部分候选组合起来,并将其设置为候选。

    之后,我们可以收集 ot 作为 LSTM 单元的输出门,然后将其乘以候选单元(长期存储器)的 tanh,后者已经用正确的操作进行了更新。网络输出为 ht。

    LSTM:原理及其实现 - 图5

    LSTM:原理及其实现 - 图6

    1. import math
    2. import torch
    3. import torch.nn as nn

    我们现在将通过继承 nn.Module,然后还将引用其参数和权重初始化,如下所示(请注意,其形状由网络的输入大小和输出大小决定):

    1. class NaiveCustomLSTM(nn.Module):
    2. def __init__(self, input_sz: int, hidden_sz: int):
    3. super().__init__()
    4. self.input_size = input_sz
    5. self.hidden_size = hidden_sz #i_t
    6. self.U_i = nn.Parameter(torch.Tensor(input_sz, hidden_sz))
    7. self.V_i = nn.Parameter(torch.Tensor(hidden_sz, hidden_sz))
    8. self.b_i = nn.Parameter(torch.Tensor(hidden_sz))
    9. #f_t
    10. self.U_f = nn.Parameter(torch.Tensor(input_sz, hidden_sz))
    11. self.V_f = nn.Parameter(torch.Tensor(hidden_sz, hidden_sz))
    12. self.b_f = nn.Parameter(torch.Tensor(hidden_sz))
    13. #c_t
    14. self.U_c = nn.Parameter(torch.Tensor(input_sz, hidden_sz))
    15. self.V_c = nn.Parameter(torch.Tensor(hidden_sz, hidden_sz))
    16. self.b_c = nn.Parameter(torch.Tensor(hidden_sz))
    17. #o_t
    18. self.U_o = nn.Parameter(torch.Tensor(input_sz, hidden_sz))
    19. self.V_o = nn.Parameter(torch.Tensor(hidden_sz, hidden_sz))
    20. self.b_o = nn.Parameter(torch.Tensor(hidden_sz))
    21. self.init_weights()

    要了解每个操作的形状,请看:

    矩阵的输入形状是(批量大小、序列长度、特征长度),因此将序列的每个元素相乘的权重矩阵必须具有该形状(特征长度、输出长度)。

    序列上每个元素的隐藏状态(也称为输出)都具有形状(批大小、输出大小),这将在序列处理结束时产生输出形状(批大小、序列长度、输出大小)。- 因此,将其相乘的权重矩阵必须具有与单元格的参数 hiddensz 相对应的形状(outputsize,output_size)。

    这里是权重初始化,我们将其用作 PyTorch 默认值中的权重初始化 nn.Module:

    1. def init_weights(self):
    2. stdv = 1.0 / math.sqrt(self.hidden_size)
    3. for weight in self.parameters():
    4. weight.data.uniform_(-stdv, stdv)

    前馈操作接收 initstates 参数,该参数是上面方程的(ht,ct)参数的元组,如果不引入,则设置为零。然后,我们对每个保留(ht,c_t)的序列元素执行 LSTM 方程的前馈,并将其作为序列下一个元素的状态引入。

    最后,我们返回预测和最后一个状态元组。让我们看看它是如何发生的:

    1. def forward(self,x,init_states=None):
    2. """
    3. assumes x.shape represents (batch_size, sequence_size, input_size)
    4. """
    5. bs, seq_sz, _ = x.size()
    6. hidden_seq = []
    7. if init_states is None:
    8. h_t, c_t = (
    9. torch.zeros(bs, self.hidden_size).to(x.device),
    10. torch.zeros(bs, self.hidden_size).to(x.device),
    11. )
    12. else:
    13. h_t, c_t = init_states
    14. for t in range(seq_sz):
    15. x_t = x[:, t, :]
    16. i_t = torch.sigmoid(x_t @ self.U_i + h_t @ self.V_i + self.b_i)
    17. f_t = torch.sigmoid(x_t @ self.U_f + h_t @ self.V_f + self.b_f)
    18. g_t = torch.tanh(x_t @ self.U_c + h_t @ self.V_c + self.b_c)
    19. o_t = torch.sigmoid(x_t @ self.U_o + h_t @ self.V_o + self.b_o)
    20. c_t = f_t * c_t + i_t * g_t
    21. h_t = o_t * torch.tanh(c_t)
    22. hidden_seq.append(h_t.unsqueeze(0))
    23. #reshape hidden_seq p/ retornar
    24. hidden_seq = torch.cat(hidden_seq, dim=0)
    25. hidden_seq = hidden_seq.transpose(0, 1).contiguous()
    26. return hidden_seq, (h_t, c_t)

    这个 LSTM 在运算上是正确的,但在计算时间上没有进行优化:我们分别执行 8 个矩阵乘法,这比矢量化的方式慢得多。我们现在将演示如何通过将其减少到 2 个矩阵乘法来完成,这将使它更快。

    为此,我们设置了两个矩阵 U 和 V,它们的权重包含在 4 个矩阵乘法上。然后,我们对已经通过线性组合 + 偏置操作的矩阵执行选通操作。

    通过矢量化操作,LSTM 单元的方程式为:

    LSTM:原理及其实现 - 图7

    1. class CustomLSTM(nn.Module):
    2. def __init__(self, input_sz, hidden_sz):
    3. super().__init__()
    4. self.input_sz = input_sz
    5. self.hidden_size = hidden_sz
    6. self.W = nn.Parameter(torch.Tensor(input_sz, hidden_sz * 4))
    7. self.U = nn.Parameter(torch.Tensor(hidden_sz, hidden_sz * 4))
    8. self.bias = nn.Parameter(torch.Tensor(hidden_sz * 4))
    9. self.init_weights()
    10. def init_weights(self):
    11. stdv = 1.0 / math.sqrt(self.hidden_size)
    12. for weight in self.parameters():
    13. weight.data.uniform_(-stdv, stdv)
    14. def forward(self, x,
    15. init_states=None):
    16. """Assumes x is of shape (batch, sequence, feature)"""
    17. bs, seq_sz, _ = x.size()
    18. hidden_seq = []
    19. if init_states is None:
    20. h_t, c_t = (torch.zeros(bs, self.hidden_size).to(x.device),
    21. torch.zeros(bs, self.hidden_size).to(x.device))
    22. else:
    23. h_t, c_t = init_states
    24. HS = self.hidden_size
    25. for t in range(seq_sz):
    26. x_t = x[:, t, :]
    27. # batch the computations into a single matrix multiplication
    28. gates = x_t @ self.W + h_t @ self.U + self.bias
    29. i_t, f_t, g_t, o_t = (
    30. torch.sigmoid(gates[:, :HS]), # input
    31. torch.sigmoid(gates[:, HS:HS*2]), # forget
    32. torch.tanh(gates[:, HS*2:HS*3]),
    33. torch.sigmoid(gates[:, HS*3:]), # output
    34. )
    35. c_t = f_t * c_t + i_t * g_t
    36. h_t = o_t * torch.tanh(c_t)
    37. hidden_seq.append(h_t.unsqueeze(0))
    38. hidden_seq = torch.cat(hidden_seq, dim=0)
    39. # reshape from shape (sequence, batch, feature) to (batch, sequence, feature)
    40. hidden_seq = hidden_seq.transpose(0, 1).contiguous()
    41. return hidden_seq, (h_t, c_t)

    最后但并非最不重要的是,我们可以展示如何优化,以使用 LSTM peephole connections。

    LSTM peephole 对其前馈操作进行了细微调整,从而将其更改为优化的情况:

    LSTM:原理及其实现 - 图8

    如果 LSTM 实现得很好并经过优化,我们可以添加 peephole 选项,并对其进行一些小的调整:

    1. class CustomLSTM(nn.Module):
    2. def __init__(self, input_sz, hidden_sz, peephole=False):
    3. super().__init__()
    4. self.input_sz = input_sz
    5. self.hidden_size = hidden_sz
    6. self.peephole = peephole
    7. self.W = nn.Parameter(torch.Tensor(input_sz, hidden_sz * 4))
    8. self.U = nn.Parameter(torch.Tensor(hidden_sz, hidden_sz * 4))
    9. self.bias = nn.Parameter(torch.Tensor(hidden_sz * 4))
    10. self.init_weights()
    11. def init_weights(self):
    12. stdv = 1.0 / math.sqrt(self.hidden_size)
    13. for weight in self.parameters():
    14. weight.data.uniform_(-stdv, stdv)
    15. def forward(self, x,
    16. init_states=None):
    17. """Assumes x is of shape (batch, sequence, feature)"""
    18. bs, seq_sz, _ = x.size()
    19. hidden_seq = []
    20. if init_states is None:
    21. h_t, c_t = (torch.zeros(bs, self.hidden_size).to(x.device),
    22. torch.zeros(bs, self.hidden_size).to(x.device))
    23. else:
    24. h_t, c_t = init_states
    25. HS = self.hidden_size
    26. for t in range(seq_sz):
    27. x_t = x[:, t, :]
    28. # batch the computations into a single matrix multiplication
    29. if self.peephole:
    30. gates = x_t @ U + c_t @ V + bias
    31. else:
    32. gates = x_t @ U + h_t @ V + bias
    33. g_t = torch.tanh(gates[:, HS*2:HS*3])
    34. i_t, f_t, o_t = (
    35. torch.sigmoid(gates[:, :HS]), # input
    36. torch.sigmoid(gates[:, HS:HS*2]), # forget
    37. torch.sigmoid(gates[:, HS*3:]), # output
    38. )
    39. if self.peephole:
    40. c_t = f_t * c_t + i_t * torch.sigmoid(x_t @ U + bias)[:, HS*2:HS*3]
    41. h_t = torch.tanh(o_t * c_t)
    42. else:
    43. c_t = f_t * c_t + i_t * g_t
    44. h_t = o_t * torch.tanh(c_t)
    45. hidden_seq.append(h_t.unsqueeze(0))
    46. hidden_seq = torch.cat(hidden_seq, dim=0)
    47. # reshape from shape (sequence, batch, feature) to (batch, sequence, feature)
    48. hidden_seq = hidden_seq.transpose(0, 1).contiguous()
    49. return hidden_seq, (h_t, c_t)

    我们的 LSTM 就这样结束了。如果有兴趣大家可以将他与 torch LSTM 内置层进行比较。

    代码 github 地址:
    piEsposito/pytorch-lstm-by-hand

    作者:Piero Esposito
    https://www.toutiao.com/i6831689529854788110/