长短期记忆(LSTM)网络已被广泛用于解决各种顺序任务。让我们了解这些网络如何工作以及如何实施它们。

就像我们一样,循环神经网络(RNN)也可能很健忘。这种与短期记忆的斗争导致 RNN 在大多数任务中失去有效性。不过,不用担心,长短期记忆网络 (LSTM) 具有出色的记忆力,可以记住普通 RNN 无法记住的信息!

LSTM 是 RNN 的一种特殊变体,因此掌握 RNN 相关的概念将极大地帮助您理解本文中的 LSTM。我在上一篇文章中介绍了 RNN 的机制。

RNN 快速回顾

RNN 以顺序方式处理输入,其中在计算当前步骤的输出时考虑先前输入的上下文。这允许神经网络在不同的时间步长上携带信息,而不是保持所有输入彼此独立。

pytorch lstm双向填充处理代码_深度学习


然而,困扰典型 RNN 的一个重大缺点是梯度消失/爆炸问题。在训练过程中通过 RNN 反向传播时会出现此问题,特别是对于具有更深层的网络。由于链式法则,梯度在反向传播过程中必须经过连续的矩阵乘法,导致梯度要么呈指数收缩(消失),要么呈指数爆炸(爆炸)。梯度太小会阻碍权重的更新和学习,而梯度太大会导致模型不稳定。

由于这些问题,RNN 无法处理较长的序列并保持长期依赖性,从而使它们遭受“短期记忆”的困扰。

什么是 LSTM

虽然 LSTM 是一种 RNN,其功能与传统 RNN 类似,但它的门控机制使其与众不同。该功能解决了 RNN 的“短期记忆”问题。

pytorch lstm双向填充处理代码_人工智能_02


从图中我们可以看出,差异主要在于 LSTM 保存长期记忆的能力。这在大多数自然语言处理 (NLP) 或时间序列和顺序任务中尤其重要。例如,假设我们有一个网络根据给我们的一些输入生成文本。文章开头提到作者有一只“名叫克里夫的狗”。在其他几个没有提到宠物或狗的句子之后,作者再次提到了他的宠物,模型必须生成下一个单词“但是,克里夫,我的宠物____”。由于单词 pet 出现在空白之前,RNN 可以推断出下一个单词可能是可以作为宠物饲养的动物。

pytorch lstm双向填充处理代码_lstm_03


然而,由于短期记忆的原因,典型的 RNN 只能使用最后几句话中出现的文本中的上下文信息——这根本没有用处。RNN 不知道宠物可能是什么动物,因为文本开头的相关信息已经丢失。另一方面,LSTM 可以保留作者有一只宠物狗的早期信息,这将有助于模型在生成文本时根据大量上下文信息选择“狗” 。较早的时间步长。

pytorch lstm双向填充处理代码_人工智能_04

LSTM 的内部工作原理

LSTM 的秘密在于每个 LSTM 单元内的门控机制。在普通 RNN 单元中,某个时间步的输入和前一个时间步的隐藏状态通过tanh激活函数来获得新的隐藏状态和输出。

pytorch lstm双向填充处理代码_深度学习_05


另一方面,LSTM 的结构稍微复杂一些。在每个时间步,LSTM 单元都会接收 3 条不同的信息:当前输入数据、前一个单元的短期记忆(类似于 RNN 中的隐藏状态)以及最后的长期记忆。

短期记忆通常称为隐藏状态,长期记忆通常称为细胞状态。

然后,在将长期和短期信息传递到下一个单元之前,单元使用门来调节每个时间步要保留或丢弃的信息。

这些门可以看作是水过滤器。理想情况下,这些门的作用应该是选择性地去除任何不相关的信息,类似于水过滤器如何防止杂质通过。同时,只有水和有益的营养物质才能通过这些过滤器,就像大门只保留有用的信息一样。当然,这些门需要经过训练才能准确过滤有用的内容和无用的内容。

pytorch lstm双向填充处理代码_pytorch_06

这些门称为输入门、遗忘门和输出门。这些门的名称有多种变体。然而,这些门的计算和工作原理大部分是相同的。

pytorch lstm双向填充处理代码_深度学习_07


让我们一一了解这些门的机制。

输入门

输入门决定哪些新信息将存储在长期记忆中。它仅适用于当前输入的信息和上一时间步的短期记忆。因此,它必须从这些变量中过滤掉无用的信息。

pytorch lstm双向填充处理代码_人工智能_08

从数学上讲,这是使用2 层来实现的。第一层可以看作是过滤器,它选择哪些信息可以通过它以及哪些信息被丢弃。为了创建这一层,我们将短期记忆和当前输入传递给sigmoid函数。sigmoid函数会将值转换为0到1之间,0表示部分信息不重要,而1表示该信息将被使用。这有助于决定要保留和使用的值,以及要丢弃的值。当该层通过反向传播进行训练时,sigmoid函数中的权重将被更新,以便它学会只让有用的特征通过,同时丢弃不太重要的特征。
pytorch lstm双向填充处理代码_AIGC_09
第二层也采用短期记忆和当前输入,并将其传递给激活函数(通常是 pytorch lstm双向填充处理代码_pytorch_10函数)来调节网络。
pytorch lstm双向填充处理代码_lstm_11
然后将这两层的输出相乘,最终结果表示要保存在长期记忆中并用作输出的信息。
pytorch lstm双向填充处理代码_AIGC_12

遗忘门

遗忘门决定应保留或丢弃长期记忆中的哪些信息。这是通过将传入的长期记忆乘以当前输入和传入的短期记忆生成的遗忘向量来完成的。

pytorch lstm双向填充处理代码_pytorch_13


就像输入门中的第一层一样,遗忘向量也是一个选择性过滤层。为了获得遗忘向量,短期记忆和当前输入通过sigmoid函数传递,类似于上面输入门中的第一层,但具有不同的权重。该向量将由 0 和 1 组成,并将与长期记忆相乘以选择要保留长期记忆的哪些部分。

pytorch lstm双向填充处理代码_pytorch_14

输入门和遗忘门的输出将进行逐点加法,以给出新版本的长期记忆,并将其传递到下一个单元。这个新的长期记忆也将用于最后一个门,即输出门。

pytorch lstm双向填充处理代码_pytorch_15

输出门

输出门将采用当前输入、先前的短期记忆和新计算的长期记忆来产生新的短期记忆/隐藏状态 ,该状态将在下一个时间步骤中传递到单元。当前时间步的输出也可以从这个隐藏状态中得出。

pytorch lstm双向填充处理代码_AIGC_16


首先,之前的短期记忆和当前输入将再次以不同的权重传递到 sigmoid 函数(是的,这是我们第三次这样做),以创建第三个也是最后一个过滤器。然后,我们将新的长期记忆通过激活 pytorch lstm双向填充处理代码_pytorch_10函数。这两个过程的输出将相乘以产生新的短期记忆。

pytorch lstm双向填充处理代码_人工智能_18

这些门产生的短期和长期记忆将被转移到下一个单元,以重复该过程。每个时间步的输出可以从短期记忆中获得,也称为隐藏状态。

这就是典型 LSTM 结构的全部机制。没那么难吧?

代码实现

对 LSTM 有了必要的理论了解后,让我们开始在代码中实现它。今天我们将使用 PyTorch 库。

在我们进入具有完整数据集的项目之前,让我们通过可视化输出来看看 PyTorch LSTM 层在实践中的实际工作原理。我们不需要实例化模型来查看该层如何工作。您可以使用下面的按钮在 FloydHub 上运行此程序LSTM_starter.ipynb。(这部分不需要在 GPU 上运行)

import torch
import torch.nn as nn

就像其他类型的层一样,我们可以实例化 LSTM 层并为其提供必要的参数。可以在此处找到已接受参数的完整文档。在此示例中,我们将仅定义输入维度、隐藏维度和层数。

  • 输入维度- 表示每个时间步输入的大小,例如维度 5 的输入将如下所示 [1, 3, 8, 2, 3]
  • 隐藏维度- 表示每个时间步隐藏状态和细胞状态的大小,例如,如果隐藏维度为 3,则隐藏状态和细胞状态都将具有 [3, 5, 4] 的形状
  • 层数 - 彼此堆叠的 LSTM 层数
input_dim = 5
hidden_dim = 10
n_layers = 1

lstm_layer = nn.LSTM(input_dim, hidden_dim, n_layers, batch_first=True)

让我们创建一些虚拟数据来查看该层如何接收输入。由于我们的输入维度是5,我们必须创建一个形状为 ( 1, 1, 5 ) 的张量,它表示(批量大小、序列长度、输入维度)。

此外,我们必须初始化 LSTM 的隐藏状态和单元状态,因为这是第一个单元。隐藏状态和单元状态存储在格式为 ( hidden_state , cell_state ) 的元组中。

batch_size = 1
seq_len = 1

inp = torch.randn(batch_size, seq_len, input_dim)
hidden_state = torch.randn(n_layers, batch_size, hidden_dim)
cell_state = torch.randn(n_layers, batch_size, hidden_dim)
hidden = (hidden_state, cell_state)
[Out]:
Input shape: (1, 1, 5)
Hidden shape: ((1, 1, 10), (1, 1, 10))

接下来,我们将提供输入和隐藏状态,看看我们会从中得到什么。

out, hidden = lstm_layer(inp, hidden)
print("Output shape: ", out.shape)
print("Hidden: ", hidden)
[Out]: Output shape: torch.size([1, 1, 10])
	 Hidden: (tensor([[[ 0.1749,  0.0099, -0.3004,  0.2846, -0.2262, -0.5257,  0.2925, -0.1894,  0.1166, -0.1197]]], grad_fn=<StackBackward>), tensor([[[ 0.4167,  0.0385, -0.4982,  0.6955, -0.9213, -1.0072,  0.4426,
          -0.3691,  0.2020, -0.2242]]], grad_fn=<StackBackward>))

在上面的过程中,我们看到了 LSTM 单元如何在每个时间步处理输入和隐藏状态。然而,在大多数情况下,我们将以大序列处理输入数据。LSTM 还可以接收可变长度的序列并在每个时间步产生输出。这次我们尝试改变序列长度。

seq_len = 3
inp = torch.randn(batch_size, seq_len, input_dim)
out, hidden = lstm_layer(inp, hidden)
print(out.shape)
[Out]: torch.Size([1, 3, 10])

这次,输出的第二维是 3,表明 LSTM 给出了 3 个输出。这对应于我们输入序列的长度。对于我们需要在每个时间步(多对多)输出的用例,例如文本生成,每个时间步的输出可以直接从第二维提取并输入到完全连接的层中。对于文本分类任务(多对一),例如情感分析,可以将最后的输出输入到分类器中。

pytorch lstm双向填充处理代码_pytorch_19

# Obtaining the last output
out = out.squeeze()[-1, :]
print(out.shape)
[Out]: torch.Size([10])

项目:亚马逊评论情绪分析

对于此项目,我们将使用 Amazon 客户评论数据集,该数据集可以在Kaggle上找到。该数据集总共包含 400 万条评论,每条评论都标记为正面或负面情绪, 可以在此处找到 GitHub 存储库的链接。

我们实施此项目时的目标是创建一个 LSTM 模型,能够准确分类和区分评论的情绪。为此,我们必须从一些数据预处理、定义和训练模型开始,然后评估模型。

我们的实现流程如下所示。

pytorch lstm双向填充处理代码_AIGC_20


我们在此实现中仅使用 100 万条评论来加快速度,但是,如果您有时间和计算能力,请随意使用整个数据集自行运行它。

对于我们的数据预处理步骤,我们将使用regex、Numpy和NLTK(自然语言工具包)库来实现一些简单的 NLP 辅助函数。由于数据以bz2格式压缩,因此我们将使用 Python bz2模块来读取数据。

import bz2
from collections import Counter
import re
import nltk
import numpy as np
nltk.download('punkt')

train_file = bz2.BZ2File('../input/amazon_reviews/train.ft.txt.bz2')
test_file = bz2.BZ2File('../input/amazon_reviews/test.ft.txt.bz2')

train_file = train_file.readlines()
test_file = test_file.readlines()

Number of training reviews: 3600000
Number of test reviews: 400000

该数据集总共包含 400 万条评论,其中 360 万条用于训练,40 万条用于测试。我们将仅使用 800k 进行训练,200k 进行测试——这仍然是大量数据。

num_train = 800000  # We're training on the first 800,000 reviews in the dataset
num_test = 200000  # Using 200,000 reviews from test set

train_file = [x.decode('utf-8') for x in train_file[:num_train]]
test_file = [x.decode('utf-8') for x in test_file[:num_test]]

句子的格式如下:

__label__2 Stunning even for the non-gamer: This soundtrack was beautiful! It paints the scenery in your mind so well I would recommend it even to people who hate vid. game music! I have played the game Chrono Cross but out of all of the games I have ever played it has the best music! It backs away from crude keyboarding and takes a fresher step with great guitars and soulful orchestras. It would impress anyone who cares to listen! _

我们必须从句子中提取标签。数据是格式__label__1/2 ,因此我们可以轻松地相应地分割它。积极情绪标签存储为 1,消极情绪标签存储为 0。

我们还将所有URL更改为标准,<url>因为在大多数情况下,确切的URL与情绪无关。

# Extracting labels from sentences
train_labels = [0 if x.split(' ')[0] == '__label__1' else 1 for x in train_file]
train_sentences = [x.split(' ', 1)[1][:-1].lower() for x in train_file]

test_labels = [0 if x.split(' ')[0] == '__label__1' else 1 for x in test_file]
test_sentences = [x.split(' ', 1)[1][:-1].lower() for x in test_file]

# Some simple cleaning of data
for i in range(len(train_sentences)):
    train_sentences[i] = re.sub('\d','0',train_sentences[i])

for i in range(len(test_sentences)):
    test_sentences[i] = re.sub('\d','0',test_sentences[i])

# Modify URLs to <url>
for i in range(len(train_sentences)):
    if 'www.' in train_sentences[i] or 'http:' in train_sentences[i] or 'https:' in train_sentences[i] or '.com' in train_sentences[i]:
        train_sentences[i] = re.sub(r"([^ ]+(?<=\.[a-z]{3}))", "<url>", train_sentences[i])
        
for i in range(len(test_sentences)):
    if 'www.' in test_sentences[i] or 'http:' in test_sentences[i] or 'https:' in test_sentences[i] or '.com' in test_sentences[i]:
        test_sentences[i] = re.sub(r"([^ ]+(?<=\.[a-z]{3}))", "<url>", test_sentences[i])

快速清理数据后,我们将对句子进行标记化,这是标准的 NLP 任务。

标记化是将句子分割成单个标记的任务,这些标记可以是单词或标点符号等。

有许多 NLP 库可以做到这一点,例如spaCy或Scikit-learn,但我们将在这里使用NLTK,因为它具有更快的分词器之一。
然后,这些单词将被存储在字典中,将单词映射到其出现次数。这些词将成为我们的词汇。

words = Counter()  # Dictionary that will map a word to the number of times it appeared in all the training sentences
for i, sentence in enumerate(train_sentences):
    # The sentences will be stored as a list of words/tokens
    train_sentences[i] = []
    for word in nltk.word_tokenize(sentence):  # Tokenizing the words
        words.update([word.lower()])  # Converting all the words to lowercase
        train_sentences[i].append(word)
    if i%20000 == 0:
        print(str((i*100)/num_train) + "% done")
print("100% done")
  • 为了删除可能不存在的拼写错误和单词,我们将从词汇表中删除仅出现一次的所有单词。
  • 为了解决未知单词和填充问题,我们还必须将它们添加到我们的词汇表中。然后,词汇表中的每个单词将被分配一个整数索引,然后映射到该整数。
# Removing the words that only appear once
words = {k:v for k,v in words.items() if v>1}
# Sorting the words according to the number of appearances, with the most common word being first
words = sorted(words, key=words.get, reverse=True)
# Adding padding and unknown to our vocabulary so that they will be assigned an index
words = ['_PAD','_UNK'] + words
# Dictionaries to store the word to index mappings and vice versa
word2idx = {o:i for i,o in enumerate(words)}
idx2word = {i:o for i,o in enumerate(words)}

通过映射,我们将句子中的单词转换为其相应的索引。

for i, sentence in enumerate(train_sentences):
    # Looking up the mapping dictionary and assigning the index to the respective words
    train_sentences[i] = [word2idx[word] if word in word2idx else 0 for word in sentence]

for i, sentence in enumerate(test_sentences):
    # For test sentences, we have to tokenize the sentences as well
    test_sentences[i] = [word2idx[word.lower()] if word.lower() in word2idx else 0 for word in nltk.word_tokenize(sentence)]

在最后的预处理步骤中,我们将用 0 填充句子并缩短冗长的句子,以便可以批量训练数据以加快速度。

# Defining a function that either shortens sentences or pads sentences with 0 to a fixed length
def pad_input(sentences, seq_len):
    features = np.zeros((len(sentences), seq_len),dtype=int)
    for ii, review in enumerate(sentences):
        if len(review) != 0:
            features[ii, -len(review):] = np.array(review)[:seq_len]
    return features

seq_len = 200  # The length that the sentences will be padded/shortened to

train_sentences = pad_input(train_sentences, seq_len)
test_sentences = pad_input(test_sentences, seq_len)

# Converting our labels into numpy arrays
train_labels = np.array(train_labels)
test_labels = np.array(test_labels)

填充的句子看起来像这样,其中 0 代表填充:

array([    0,     0,     0,     0,     0,     0,     0,     0,     0,
           0,     0,     0,     0,     0,     0,     0,     0,     0,
           0,     0,     0,     0,     0,     0,     0,     0,     0,
           0,     0,     0,     0,     0,     0,     0,     0,     0,
           0,     0,     0,     0,     0,     0,     0,     0,     0,
           0,     0,     0,     0,     0,     0,     0,     0,     0,
           0,     0,     0,     0,     0,     0,     0,     0,     0,
           0,     0,     0,     0,     0,     0,     0,     0,     0,
           0,     0,    44,   125,    13,    28,  1701,  5144,    60,
          31,    10,     3,    44,  2052,    10,    84,  2131,     2,
           5,    27,  1336,     8,    11,   125,    17,   153,     6,
           5,   146,   103,     9,     2,    64,     5,   117,    14,
           7,    42,  1680,     9,   194,    56,   230,   107,     2,
           7,   128,  1680,    52, 31073,    41,  3243,    14,     3,
        3674,     2,    11,   125,    52, 10669,   156,     2,  1103,
          29,     0,     0,     6,   917,    52,  1366,     2,    31,
          10,   156,    23,  2071,  3574,     2,    11,    12,     7,
        2954,  9926,   125,    14,    28,    21,     2,   180,    95,
         132,   147,     9,   220,    12,    52,   718,    56,     2,
        2339,     5,   272,    11,     4,    72,   695,   562,     4,
         722,     4,   425,     4,   163,     4,  1491,     4,  1132,
        1829,   520,    31,   169,    34,    77,    18,    16,  1107,
          69,    33])

我们的数据集已经分为训练数据和测试数据。然而,我们在训练过程中仍然需要一组数据进行验证。因此,我们将测试数据分成两半,分为验证集和测试集。可以在此处找到数据集拆分的详细说明。

split_frac = 0.5 # 50% validation, 50% test
split_id = int(split_frac * len(test_sentences))
val_sentences, test_sentences = test_sentences[:split_id], test_sentences[split_id:]
val_labels, test_labels = test_labels[:split_id], test_labels[split_id:]

接下来,我们将开始使用 PyTorch 库。我们首先从句子和标签定义数据集,然后将它们加载到数据加载器中。我们将批量大小设置为 256。这可以根据您的需要进行调整。

import torch
from torch.utils.data import TensorDataset, DataLoader
import torch.nn as nn

train_data = TensorDataset(torch.from_numpy(train_sentences), torch.from_numpy(train_labels))
val_data = TensorDataset(torch.from_numpy(val_sentences), torch.from_numpy(val_labels))
test_data = TensorDataset(torch.from_numpy(test_sentences), torch.from_numpy(test_labels))

batch_size = 400

train_loader = DataLoader(train_data, shuffle=True, batch_size=batch_size)
val_loader = DataLoader(val_data, shuffle=True, batch_size=batch_size)
test_loader = DataLoader(test_data, shuffle=True, batch_size=batch_size)

我们还可以检查是否有 GPU 可以将训练时间加快很多倍。如果您使用带有 GPU 的 FloydHub 来运行此代码,训练时间将显着减少。

# torch.cuda.is_available() checks and returns a Boolean True if a GPU is available, else it'll return False
is_cuda = torch.cuda.is_available()

# If we have a GPU available, we'll set our device to GPU. We'll use this device variable later in our code.
if is_cuda:
    device = torch.device("cuda")
else:
    device = torch.device("cpu")

此时,我们将定义模型的架构。在此阶段,我们可以创建具有深层或大量相互堆叠的 LSTM 层的神经网络。然而,像下面这样的简单模型,只有一个 LSTM 和一个全连接层,效果很好,并且需要的训练时间要少得多。在将句子输入 LSTM 层之前,我们将在第一层训练我们自己的词嵌入。

pytorch lstm双向填充处理代码_人工智能_21


最后一层是一个全连接层,具有 sigmoid 函数,用于对评论是否具有积极/消极情绪进行分类。

class SentimentNet(nn.Module):
    def __init__(self, vocab_size, output_size, embedding_dim, hidden_dim, n_layers, drop_prob=0.5):
        super(SentimentNet, self).__init__()
        self.output_size = output_size
        self.n_layers = n_layers
        self.hidden_dim = hidden_dim
        
        self.embedding = nn.Embedding(vocab_size, embedding_dim)
        self.lstm = nn.LSTM(embedding_dim, hidden_dim, n_layers, dropout=drop_prob, batch_first=True)
        self.dropout = nn.Dropout(drop_prob)
        self.fc = nn.Linear(hidden_dim, output_size)
        self.sigmoid = nn.Sigmoid()
        
    def forward(self, x, hidden):
        batch_size = x.size(0)
        x = x.long()
        embeds = self.embedding(x)
        lstm_out, hidden = self.lstm(embeds, hidden)
        lstm_out = lstm_out.contiguous().view(-1, self.hidden_dim)
        out = self.dropout(lstm_out)
        out = self.fc(out)
        out = self.sigmoid(out)
        
        out = out.view(batch_size, -1)
        out = out[:,-1]
        return out, hidden
    
    def init_hidden(self, batch_size):
        weight = next(self.parameters()).data
        hidden = (weight.new(self.n_layers, batch_size, self.hidden_dim).zero_().to(device),
                      weight.new(self.n_layers, batch_size, self.hidden_dim).zero_().to(device))
        return hidden

请注意,我们实际上可以加载预先训练的词嵌入,例如GloVe或fastText,这可以提高模型的准确性并减少训练时间。

这样,我们就可以在定义参数后实例化我们的模型。输出维度仅为 1,因为它只需要输出 1 或 0。还定义了学习率、损失函数和优化器。

vocab_size = len(word2idx) + 1
output_size = 1
embedding_dim = 400
hidden_dim = 512
n_layers = 2

model = SentimentNet(vocab_size, output_size, embedding_dim, hidden_dim, n_layers)
model.to(device)

lr=0.005
criterion = nn.BCELoss()
optimizer = torch.optim.Adam(model.parameters(), lr=lr)

最后,我们可以开始训练模型。每 1000 个步骤,我们将根据验证数据集检查模型的输出,如果模型的表现比前一次更好,则保存模型。

state_dict是 PyTorch 中模型的权重,可以在单独的时间或脚本中加载到具有相同架构的模型中。

epochs = 2
counter = 0
print_every = 1000
clip = 5
valid_loss_min = np.Inf

model.train()
for i in range(epochs):
    h = model.init_hidden(batch_size)
    
    for inputs, labels in train_loader:
        counter += 1
        h = tuple([e.data for e in h])
        inputs, labels = inputs.to(device), labels.to(device)
        model.zero_grad()
        output, h = model(inputs, h)
        loss = criterion(output.squeeze(), labels.float())
        loss.backward()
        nn.utils.clip_grad_norm_(model.parameters(), clip)
        optimizer.step()
        
        if counter%print_every == 0:
            val_h = model.init_hidden(batch_size)
            val_losses = []
            model.eval()
            for inp, lab in val_loader:
                val_h = tuple([each.data for each in val_h])
                inp, lab = inp.to(device), lab.to(device)
                out, val_h = model(inp, val_h)
                val_loss = criterion(out.squeeze(), lab.float())
                val_losses.append(val_loss.item())
                
            model.train()
            print("Epoch: {}/{}...".format(i+1, epochs),
                  "Step: {}...".format(counter),
                  "Loss: {:.6f}...".format(loss.item()),
                  "Val Loss: {:.6f}".format(np.mean(val_losses)))
            if np.mean(val_losses) <= valid_loss_min:
                torch.save(model.state_dict(), './state_dict.pt')
                print('Validation loss decreased ({:.6f} --> {:.6f}).  Saving model ...'.format(valid_loss_min,np.mean(val_losses)))
                valid_loss_min = np.mean(val_losses)

完成训练后,是时候在以前从未见过的数据集(我们的测试数据集)上测试我们的模型了。我们首先从验证损失最低的点加载模型权重。

我们可以计算模型的准确性,看看我们的模型的预测有多准确。

# Loading the best model
model.load_state_dict(torch.load('./state_dict.pt'))

test_losses = []
num_correct = 0
h = model.init_hidden(batch_size)

model.eval()
for inputs, labels in test_loader:
    h = tuple([each.data for each in h])
    inputs, labels = inputs.to(device), labels.to(device)
    output, h = model(inputs, h)
    test_loss = criterion(output.squeeze(), labels.float())
    test_losses.append(test_loss.item())
    pred = torch.round(output.squeeze())  # Rounds the output to 0/1
    correct_tensor = pred.eq(labels.float().view_as(pred))
    correct = np.squeeze(correct_tensor.cpu().numpy())
    num_correct += np.sum(correct)

print("Test loss: {:.3f}".format(np.mean(test_losses)))
test_acc = num_correct/len(test_loader.dataset)
print("Test accuracy: {:.3f}%".format(test_acc*100))
[Out]: Test loss: 0.161
       Test accuracy: 93.906%

通过这个简单的 LSTM 模型,我们成功实现了93.8%的准确率!这显示了 LSTM 在处理此类顺序任务方面的有效性。

这个结果是通过几个简单的层实现的,并且没有任何超参数调整。可以进行许多其他改进来提高模型的有效性,并且您可以自由地尝试通过实施这些改进来超越此准确性!

一些改进建议如下:

  • 运行超参数搜索来优化您的配置。可以在此处找到技术指南
  • 增加模型复杂性
  • 添加更多层/使用双向 LSTM 使用预先训练的词嵌入,例如GloVe嵌入

超越 LSTM

多年来,LSTM 在 NLP 任务方面一直是最先进的。然而,基于注意力的模型和Transformer 的最新进展产生了更好的结果。随着 Google 的 BERT 和 OpenAI 的 GPT 等预训练 Transformer 模型的发布,LSTM 的使用量一直在下降。尽管如此,理解 RNN 和 LSTM 背后的概念肯定还是有用的,谁知道,也许有一天 LSTM 会卷土重来呢?