LSTM
Chris Olah's LSTM post
Edwin Chen's LSTM post
Andrej Karpathy's lecture on RNNs and LSTMs from CS231n
LSTM结构包括几个门:遗忘门,学习门,记忆门,使用门
这些门的基本工作原理如下:
- 长期记忆进入遗忘门,忘记它认为没有用处的一切;
- 短期记忆和事件在学习门里合并到一起,,并移除掉一切不必要的信息,作为学到的新信息。
- 还没遗忘的长期记忆和刚学到的新信息会在记忆门里合并到一起,这个门把这两者放到一起,由于它叫记忆门,所以它会输出更新后的长期记忆
- 最后 使用门会决定要从之前知道的信息以及刚学到的信息中挑出什么来使用,从而作出预测 ,所以它也接受长期记忆和新信息的输入,把它们合并到一起并决定要输出什么。所以输出就包括了预测和新的短期记忆
短期记忆输入进来 我们把它们分别叫做 LTM 和 STM,如下图所示:
LSTM 结构
LSTMs in Pytorch
In PyTorch an LSTM can be defined as: lstm = nn.LSTM(input_size=input_dim, hidden_size=hidden_dim, num_layers=n_layers)
.
在PyTorch中,LSTM期望其所有输入都是3D张量,其尺寸定义如下:
-
input_dim
=输入数量(20的维度可代表20个输入) -
hidden_dim
=隐藏状态的大小; 每个LSTM单元在每个时间步产生的输出数。 -
n_layers
=隐藏LSTM图层的数量; 通常是1到3之间的值; 值为1表示每个LSTM单元具有一个隐藏状态。 其默认值为1。
隐藏状态
一旦LSTM输入和隐藏层维度,就可以调用它并在每个时间步骤检索输出和隐藏状态:
out, hidden = lstm(input.view(1, 1, -1), (h0, c0))
对于LSTM输入为:(input, (h0, c0))
.
-
input
= 输入序列中Tensor; (seq_len,batch,input_size) -
h0
= Tensor,包含批处理中每个元素的初始隐藏状态 -
c0
= 批次中每个元素的初始单元格内存的Tensor
h0和c0默认为0,如果未指定。 它们的尺寸为:(n_layers,batch,hidden_dim)。
PyTorch LSTM tutorial.
Example
该LSTM旨在查看4个值的序列,并生成3个值作为输出。
import torch
import torch.nn as nn
import matplotlib.pyplot as plt
%matplotlib inline
torch.manual_seed(2) # so that random variables will be consistent and repeatable for testing
定义一个简单的LSTM
除非定义自己的LSTM并通过在网络的末尾添加线性层(例如,fc=nn.Linear(hidden_dim,output_dim)来改变输出的数量,否则输出的“hidden_dim”和输出大小将是相同的。
from torch.autograd import Variable
# define an LSTM with an input dim of 4 and hidden dim of 3
# this expects to see 4 values as input and generates 3 values as output
input_dim = 4
hidden_dim = 3
lstm = nn.LSTM(input_size=input_dim, hidden_size=hidden_dim)
# make 5 input sequences of 4 random values each
inputs_list = [torch.randn(1, input_dim) for _ in range(5)]
print('inputs: \n', inputs_list)
print('\n')
# initialize the hidden state
# (1 layer, 1 batch_size, 3 outputs)
# first tensor is the hidden state, h0
# second tensor initializes the cell memory, c0
h0 = torch.randn(1, 1, hidden_dim)
c0 = torch.randn(1, 1, hidden_dim)
h0 = Variable(h0)
c0 = Variable(c0)
# step through the sequence one element at a time.
for i in inputs_list:
# wrap in Variable
i = Variable(i)
# after each step, hidden contains the hidden state
out, hidden = lstm(i.view(1, 1, -1), (h0, c0))
print('out: \n', out)
print('hidden: \n', hidden)
输出张量和隐藏张量长度总为3,这是我们在定义LSTM hidden_dim的时指定的
对于以上的输出,对于大型数据序列,for循环不是非常有效,所以我们也可以同时处理这些输入。
1.使用定义的batch_size将所有输入序列连接成一个大张量
2.定义隐藏状态的形状
3.获取输出和最近的隐藏状态(在序列中的最后一个单词之后创建)
# turn inputs into a tensor with 5 rows of data
# add the extra 2nd dimension (1) for batch_size
inputs = torch.cat(inputs_list).view(len(inputs_list), 1, -1)
# print out our inputs and their shape
# you should see (number of sequences, batch size, input_dim)
print('inputs size: \n', inputs.size())
print('\n')
print('inputs: \n', inputs)
print('\n')
# initialize the hidden state
h0 = torch.randn(1, 1, hidden_dim)
c0 = torch.randn(1, 1, hidden_dim)
# wrap everything in Variable
inputs = Variable(inputs)
h0 = Variable(h0)
c0 = Variable(c0)
# get the outputs and hidden state
out, hidden = lstm(inputs, (h0, c0))
print('out: \n', out)
print('hidden: \n', hidden)
学习门(The learn gate)
学习门要做的是,取短期记忆和事件将两者合并 。实际上 ,它做的不止这些:它会接受短期记忆和事件,将两者合并起来然后忽略其中的一部分,只保留重要的部分。
所以这里它忘了有棵树,只记得我们最近看到了一只松鼠和一条狗/一只狼。
数学原理
我们有短期记忆 STMt-1,和事件 Et ,学习门会把它们合并起来:也就是把两个向量放到一起,然后乘以一个矩阵 ,再加一个偏差 ,最后将所得结果代入 tanh 激活函数里。然后新信息 Nt 就产生了。
那我们要怎么忽略其中一部分呢?
可以将其乘以一个遗忘因子 it,遗忘因子 it实际上是一个向量 但它可以像元素一样进行乘法运算。
怎么计算it呢?
这就要用到短期记忆和事件,再次创建一个小型神经网络,其输入为短期记忆和事件。把它们代入一个小型线性函数里,在函数里乘以一个新矩阵,再加一个新偏差 ,把所得结果代入 sigmoid 函数 使其值保持在 0 和 1 之间
遗忘门(The forget Gate)
其工作原理是 遗忘门会接受一个长期记忆并决定要保留和遗忘记忆的哪个部分
数学原理
把 t-1 时的长期记忆输入进来乘以一个遗忘因子ft.
怎么计算遗忘因子:
短期记忆 STM 以及事件信息来计算 ft。用一个一层小型神经网络 将线性函数与sigmoid 函数结合起来 从而算出这个遗忘因子。
记忆门(remember gate)
接受从遗忘门输出的长期记忆,以及从学习门输出的短期记忆 然后直接把两者合并起来
数学原理
把遗忘门的输出结果和学习门的输出结果加起来
使用门/输出门(The Use Gate)
使用来自遗忘门的长期记忆,和来自学习门的短期记忆,找出新的短期记忆和输出。
1.遗忘门的输出上 使用双曲正切 (tanh) 激活函数,应用较小的神经网络;
2.短期记忆和使用 sigmoid 激活函数的事件上应用另一个较小的神经网络
3.把两个相乘 得到新的输出也是新的短期记忆
总结
- 遗忘门
会接受长期记忆并遗忘掉一部分 - 学习门
把短期记忆和事件放到一起 作为我们最近学到的信息 - 记忆门
记忆门则把还没有遗忘的长期记忆和刚学到的新信息放到一起,以便更新长期记忆并将其输出 - 使用门
把我们刚学到的信息和还没遗忘的长期记忆放到一起从而作出预测并更新短期记忆
LSTM初学者指南
词性标注的LSTM
神经网络在输入单词方面做得不好,所以我门的第一步是准备我们的训练数据,并将每个单词映射到一个数值。
首先创建一小组训练数据,这是几个简单的句子,它们被分解成一个单词列表和相应的单词标注。注意,使用low()
将句子转换为小写单词,然后使用split()
将句子分割为单独的单词,通过空格字符分隔句子。
然后根据这些训练数据,我们创建一个字典,将词汇表中的每个独特单词映射到一个数值:唯一索引idx。对每个单词标记都做同样的处理,例如:一个名词将由数字1表示。
准备数据
# import resources
import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
import matplotlib.pyplot as plt
%matplotlib inline
# training sentences and their corresponding word-tags
training_data = [
("The cat ate the cheese".lower().split(), ["DET", "NN", "V", "DET", "NN"]),
("She read that book".lower().split(), ["NN", "V", "DET", "NN"]),
("The dog loves art".lower().split(), ["DET", "NN", "V", "NN"]),
("The elephant answers the phone".lower().split(), ["DET", "NN", "V", "DET", "NN"])
]
# create a dictionary that maps words to indices
word2idx = {}
for sent, tags in training_data:
for word in sent:
if word not in word2idx:
word2idx[word] = len(word2idx)
# create a dictionary that maps tags to indices
tag2idx = {"DET": 0, "NN": 1, "V": 2}
# print out the created dictionary
print(word2idx)
import numpy as np
# a helper function for converting a sequence of words to a Tensor of numerical values
# will be used later in training
def prepare_sequence(seq, to_idx):
'''This function takes in a sequence of words and returns a
corresponding Tensor of numerical values (indices for each word).'''
idxs = [to_idx[w] for w in seq]
idxs = np.array(idxs)
return torch.from_numpy(idxs)
# check out what prepare_sequence does for one of our training sentences:
example_input = prepare_sequence("The dog answers the phone".lower().split(), word2idx)
print(example_input)
创建模型
模型假设以下几点:
- 输入被分解成一些列的单词[W1,W2,...]
- 这些单词来自我们已经知道的更多单词列表(词汇表)
- 我们有一组有限的标签,[NN,V,DET],分别表示:名词,动词和决定因素(像“the”或“that”这样的词)
-
我们想要为每个输入词预测*一个标签
为了进行预测,我们将在测试语句上传递LSTM,并将softmax函数应用于LSTM的隐藏状态;结果是标记分数的向量,根据该向量,我们可以基于标记分数分布的最大值来获得单词的预测标记。
词嵌入 Word embeddings
我们知道LSTM接受预期的输入大小和hidden_dim,但是句子很少具有一致的大小,那么我们如何定义LSTM的输入呢?
在这个网络的最开始,我们将创建一个“Embedding”层,它接受我们词汇表的大小,并为输入的单词序列中的每个单词返回指定大小的矢量embedding_dim
。
重要的是,这是该网络的第一层。 您可以在PyTorch文档.中阅读有关此嵌入层的更多信息
class LSTMTagger(nn.Module):
def __init__(self, embedding_dim, hidden_dim, vocab_size, tagset_size):
''' Initialize the layers of this model.'''
super(LSTMTagger, self).__init__()
self.hidden_dim = hidden_dim
# embedding layer that turns words into a vector of a specified size
self.word_embeddings = nn.Embedding(vocab_size, embedding_dim)
# the LSTM takes embedded word vectors (of a specified size) as inputs
# and outputs hidden states of size hidden_dim
self.lstm = nn.LSTM(embedding_dim, hidden_dim)
# the linear layer that maps the hidden state output dimension
# to the number of tags we want as output, tagset_size (in this case this is 3 tags)
self.hidden2tag = nn.Linear(hidden_dim, tagset_size)
# initialize the hidden state (see code below)
self.hidden = self.init_hidden()
def init_hidden(self):
''' At the start of training, we need to initialize a hidden state;
there will be none because the hidden state is formed based on perviously seen data.
So, this function defines a hidden state with all zeroes and of a specified size.'''
# The axes dimensions are (n_layers, batch_size, hidden_dim)
return (torch.zeros(1, 1, self.hidden_dim),
torch.zeros(1, 1, self.hidden_dim))
def forward(self, sentence):
''' Define the feedforward behavior of the model.'''
# create embedded word vectors for each word in a sentence
embeds = self.word_embeddings(sentence)
# get the output and hidden state by passing the lstm over our word embeddings
# the lstm takes in our embeddings and hiddent state
lstm_out, self.hidden = self.lstm(
embeds.view(len(sentence), 1, -1), self.hidden)
# get the scores for the most likely tag for a word
tag_outputs = self.hidden2tag(lstm_out.view(len(sentence), -1))
tag_scores = F.log_softmax(tag_outputs, dim=1)
return tag_scores
训练
首先,我们定义单词嵌入的大小。 EMBEDDING_DIM为我们简单的词汇和训练集定义了单词向量的大小; 我们将它们保持在较小的位置,以便我们可以看到当我们训练时重量如何变化
注意:复杂数据集的嵌入维度通常会大得多,大约为64,128或256维。
由于我们的LSTM输出了一系列带有softmax图层的标记分数,我们将使用NLLLoss。 与softmax层一起,NLL Loss创建了我们通常用于分析类分数分布的交叉熵损失。 我们将使用标准梯度下降优化,但我们鼓励您与其他优化器一起使用!
# the embedding dimension defines the size of our word vectors
# for our simple vocabulary and training set, we will keep these small
EMBEDDING_DIM = 6
HIDDEN_DIM = 6
# instantiate our model
model = LSTMTagger(EMBEDDING_DIM, HIDDEN_DIM, len(word2idx), len(tag2idx))
# define our loss and optimizer
loss_function = nn.NLLLoss()
optimizer = optim.SGD(model.parameters(), lr=0.1)
为了检查我们的模型是否已经学到了什么,让我们先看一下样本测试句的分数,然后再训练我们的模型。 请注意,测试句必须由我们词汇表中的单词组成,否则其单词不能转换为索引。
分数应为长度为3的张量(对于我们的每个标签),并且输入句子中的每个单词应该有分数。
对于测试句,“The cheese loves the elephant”,我们知道它有标签(DET,NN,V,DET,NN)或[0,1,2,0,1],但我们的网络还没有 知道这个。 实际上,在这种情况下,我们的模型以隐藏的全零状态开始,因此所有分数和预测标记应该是低的,随机的,以及您对尚未训练的网络的期望!
test_sentence = "The cheese loves the elephant".lower().split()
# see what the scores are before training
# element [i,j] of the output is the *score* for tag j for word i.
# to check the initial accuracy of our model, we don't need to train, so we use model.eval()
inputs = prepare_sequence(test_sentence, word2idx)
inputs = inputs
tag_scores = model(inputs)
print(tag_scores)
# tag_scores outputs a vector of tag scores for each word in an inpit sentence
# to get the most likely tag index, we grab the index with the maximum score!
# recall that these numbers correspond to tag2idx = {"DET": 0, "NN": 1, "V": 2}
_, predicted_tags = torch.max(tag_scores, 1)
print('\n')
print('Predicted tags: \n',predicted_tags)
循环遍历多个时期的所有训练数据:
1.通过调整渐变来准备我们的训练模型
2.初始化LSTM的隐藏状态
3.准备我们的数据进行培训
4.在输入上运行前向传递以获取tag_scores
5.计算tag_scores和真实标记之间的损失
6.使用反向传播更新模型的权重
在这个例子中,我们打印出每20个epoch的平均epoch损失; 你应该看到它随着时间的推移而减少
# normally these epochs take a lot longer
# but with our toy data (only 3 sentences), we can do many epochs in a short time
n_epochs = 300
for epoch in range(n_epochs):
epoch_loss = 0.0
# get all sentences and corresponding tags in the training data
for sentence, tags in training_data:
# zero the gradients
model.zero_grad()
# zero the hidden state of the LSTM, this detaches it from its history
model.hidden = model.init_hidden()
# prepare the inputs for processing by out network,
# turn all sentences and targets into Tensors of numerical indices
sentence_in = prepare_sequence(sentence, word2idx)
targets = prepare_sequence(tags, tag2idx)
# forward pass to get tag scores
tag_scores = model(sentence_in)
# compute the loss, and gradients
loss = loss_function(tag_scores, targets)
epoch_loss += loss.item()
loss.backward()
# update the model parameters with optimizer.step()
optimizer.step()
# print out avg loss per 20 epochs
if(epoch%20 == 19):
print("Epoch: %d, loss: %1.5f" % (epoch+1, epoch_loss/len(training_data)))
同样,对于测试句“the cheese loves the elephant”,我们知道这里有标记(DET, NN, V, DET, NN)或“[0,1,2,0,1]”。让我们看看我们的模型是否学会了找到这些标签!
test_sentence = "The cheese loves the elephant".lower().split()
# see what the scores are after training
inputs = prepare_sequence(test_sentence, word2idx)
inputs = inputs
tag_scores = model(inputs)
print(tag_scores)
# print the most likely tag index, by grabbing the index with the maximum score!
# recall that these numbers correspond to tag2idx = {"DET": 0, "NN": 1, "V": 2}
_, predicted_tags = torch.max(tag_scores, 1)
print('\n')
print('Predicted tags: \n',predicted_tags)