了解 LSTM 的回归输出

数据挖掘 神经网络 回归 lstm rnn 词嵌入
2021-10-02 21:43:17

我正在使用嵌入并想看看预测附加到某些单词序列的某些分数是多么可行。分数的细节并不重要。

Input (tokenized sentence): ('the', 'dog', 'ate', 'the', 'apple')
Output (float): 0.25

我一直在关注本教程,该教程试图预测此类输入的词性标签。在这种情况下,系统的输出是序列中所有标记的所有可能标签的分布,例如对于三个可能的 POS 类{'DET': 0, 'NN': 1, 'V': 2},输出('the', 'dog', 'ate', 'the', 'apple')可能是

tensor([[-0.0858, -2.9355, -3.5374],
        [-5.2313, -0.0234, -4.0314],
        [-3.9098, -4.1279, -0.0368],
        [-0.0187, -4.7809, -4.5960],
        [-5.8170, -0.0183, -4.1879]])

每行是一个token,token中最高值的索引是最好的预测POS标签。

我对这个例子理解得比较好,所以我想把它改成回归问题。完整的代码如下,但我试图理解输出。

import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim

torch.manual_seed(1)


class LSTMRegressor(nn.Module):
    def __init__(self, embedding_dim, hidden_dim, vocab_size):
        super(LSTMRegressor, self).__init__()
        self.hidden_dim = hidden_dim

        self.word_embeddings = nn.Embedding(vocab_size, embedding_dim)

        # The LSTM takes word embeddings as inputs, and outputs hidden states
        # with dimensionality hidden_dim.
        self.lstm = nn.LSTM(embedding_dim, hidden_dim)

        # The linear layer that maps from hidden state space to a single output
        self.linear = nn.Linear(hidden_dim, 1)
        self.hidden = self.init_hidden()

    def init_hidden(self):
        # Before we've done anything, we dont have any hidden state.
        # Refer to the Pytorch documentation to see exactly
        # why they have this dimensionality.
        # The axes semantics are (num_layers, minibatch_size, hidden_dim)
        return (torch.zeros(1, 1, self.hidden_dim),
                torch.zeros(1, 1, self.hidden_dim))

    def forward(self, sentence):
        embeds = self.word_embeddings(sentence)

        lstm_out, self.hidden = self.lstm(embeds.view(len(sentence), 1, -1), self.hidden)
        regression = F.relu(self.linear(lstm_out.view(len(sentence), -1)))

        return regression


def prepare_sequence(seq, to_ix):
    idxs = [to_ix[w] for w in seq]
    return torch.tensor(idxs, dtype=torch.long)

# ================================================

training_data = [
    ("the dog ate the apple".split(), 0.25),
    ("everybody read that book".split(), 0.78)
]

word_to_ix = {}
for sent, tags in training_data:
    for word in sent:
        if word not in word_to_ix:
            word_to_ix[word] = len(word_to_ix)

tag_to_ix = {"DET": 0, "NN": 1, "V": 2}

# ================================================

EMBEDDING_DIM = 6
HIDDEN_DIM = 6

model = LSTMRegressor(EMBEDDING_DIM, HIDDEN_DIM, len(word_to_ix))
loss_function = nn.MSELoss()
optimizer = optim.Adam(filter(lambda p: p.requires_grad, model.parameters()))

# See what the results are before training
with torch.no_grad():
    inputs = prepare_sequence(training_data[0][0], word_to_ix)
    regr = model(inputs)

    print(regr)

for epoch in range(100):  # again, normally you would NOT do 300 epochs, it is toy data
    for sentence, target in training_data:
        # Step 1. Remember that Pytorch accumulates gradients.
        # We need to clear them out before each instance
        model.zero_grad()

        # Also, we need to clear out the hidden state of the LSTM,
        # detaching it from its history on the last instance.
        model.hidden = model.init_hidden()

        # Step 2. Get our inputs ready for the network, that is, turn them into
        # Tensors of word indices.
        sentence_in = prepare_sequence(sentence, word_to_ix)
        target = torch.tensor(target, dtype=torch.float)

        # Step 3. Run our forward pass.
        score = model(sentence_in)

        # Step 4. Compute the loss, gradients, and update the parameters by
        #  calling optimizer.step()
        loss = loss_function(score, target)
        loss.backward()
        optimizer.step()

# See what the results are after training
with torch.no_grad():
    inputs = prepare_sequence(training_data[0][0], word_to_ix)
    regr = model(inputs)

    print(regr)

输出是:

# Before training
tensor([[0.0000],
        [0.0752],
        [0.1033],
        [0.0088],
        [0.1178]])
# After training
tensor([[0.6181],
        [0.4987],
        [0.3784],
        [0.4052],
        [0.4311]])

但我不明白为什么。我期待一个单一的输出。张量的大小与输入的标记数相同。然后,我会猜测,对于输入中的每一步,都会给出隐藏状态。那是对的吗?这是否意味着张量中的最后一项 ( tensor[-1],或者它是第一项tensor[0]?) 是最终预测?为什么给出所有输出?还是我在前传中的误解?也许我应该只将 LSTM 层的最后一项提供给线性层?

我也很想知道这如何外推到双向 LSTM 和多层 LSTM,甚至如何与 GRU(双向或非双向)一起工作。

赏金将提供给能够解释为什么我们会使用最后一个输出或最后一个隐藏状态或从目标导向的角度来看差异意味着什么的人。此外,欢迎提供有关多层架构和双向 RNN 的一些信息。例如,将双向 LSTM/GRU 的输出和隐藏状态相加或连接以使您的数据形成合理的形状是常见的做法吗?如果是这样,你怎么做?

4个回答

部分混淆可能是因为 POS 标记不是回归问题。词性标注是一个分类问题。分类是从一组离散类别中预测最可能的类别。回归是预测数字输出。

词性标注是多类分类(例如,DET、NN、V、...)。用于多类分类的神经网络的最后一层应该是softmax 函数softmax 函数会将每个类别的节点激活转换为概率。最大概率将是预测的类别,在这种情况下是 POS 标签。

我认为您在这里缺少的是对 LSTM 的清晰理解。我将分部分回答。

“为什么我们要使用最后一个输出或最后一个隐藏状态”:最后一个隐藏状态和最后一个输出不一样。最后一个输出是从最后一个隐藏状态通过线性层(例如 softmax)生成的。

“从目标导向的角度来看差异意味着什么”:最后一个隐藏状态只是一组权重,而最后一个输出是基于这些权重的预测。

“将双向 LSTM/GRU 的输出和隐藏状态求和或连接以使您的数据形成合理的形状是常见的做法吗?”:连接输出和隐藏状态并非不可能,因为它们是张量。但是,这样做是不可取的。此外,它没有实际意义。输出是通过使用隐藏状态作为 LSTM 中的输入之一生成的。

关于主要问题,您得到的最终张量只是 logits。如果你想要一个单一的值,就像其他答案状态一样,你应该在现有架构的顶部堆叠一个 softmax 层。

非常好的问题;在时间序列预测领域,LSTM 使用序列预测LSTM 网络可以学习预测与时间相关的给定输入序列旁边的值。

请记住,LSTM 网络的输入应该事先进行归一化,因此具有 y1、y2、y3、...、yn 的单个向量,LSTM 将尝试学习、记忆和忘记向量中实例内的关系;记忆和遗忘率在 LSTM 模型中是可配置的,并且通过具有神经元层,LSTM 可以创建复杂的关系结构,以预测下一个值,从而最大限度地减少序列预测的误差。

现在,如果您的场景中有外生变量,LSTM 可以通过查看额外信息(即提供的变量向量)来了解更多信息。另一点是,如果你的时间序列中没有大量历史点对应于大量的值序列,通常 LSTM 的结果不如数学模型。

在这里,我提供论文 [1] 中的正式描述:LSTM 网络由一系列单元组成,而每个 LSTM 单元主要由四个门配置:输入门、输入调制门、遗忘门和输出门。输入门从外部获取一个新的输入点并处理新来的数据。记忆单元输入门在最后一次迭代中从 LSTM 单元的输出中获取输入。遗忘门决定何时忘记输出结果,从而为输入序列选择最佳时间延迟输出门将计算出的所有结果生成输出。对于时间序列输入X=(x1,x2,...,xT), 隐藏状态单元为 H=(h1,h2,...,hT) 和输出序列为 Y=(y1,y2,...,yT).

为了 t=1,...,T LSTM 计算:

ht=H(Whyxt+Whhht1+bh)
yt=Whyht+by

[1]:长期短期记忆 Sepp Hochreiter 和 Jürgen Schmidhuber 于 2006 年 3 月 13 日在线发布,https://www.mitpressjournals.org/doi/abs/10.1162/neco.1997.9.8.1735

我继续进行,并测试了很多东西,我想出了这个网络,就我测试它而言,它似乎工作正常

def __init__(self, hidden_dim, ms_dim, embeddings):
        super(LSTMRegressor, self).__init__()
        self.hidden_dim = hidden_dim

        # load pretrained embeddings, freeze them
        self.word_embeddings = nn.Embedding.from_pretrained(embeddings)
        embed_size = embeddings.shape[1]
        self.word_embeddings.weight.requires_grad = False

        self.w2v_lstm = nn.LSTM(embed_size, hidden_dim, bidirectional=True)

        self.ms_lstm = nn.LSTM(ms_dim, hidden_dim, bidirectional=True)

        self.linear = nn.Linear(hidden_dim, 1)
        self.relu = nn.LeakyReLU()

    def forward(self, batch_size, sentence_input, ms_input):
        # 1. Embeddings
        embeds = self.word_embeddings(sentence_input)
        w2v_out, _ = self.w2v_lstm(embeds.view(-1, batch_size, embeds.size(2)))

        # separate bidirectional output into first/last, then sum them
        w2v_first_bi = w2v_out[:, :, :self.hidden_dim]
        w2v_last_bi = w2v_out[:, :, self.hidden_dim:]
        w2v_sum_bi = w2v_first_bi + w2v_last_bi

        # 2. Other features
        ms_out, _ = self.ms_lstm(ms_input.view(-1, batch_size, ms_input.size(1)))

        ms_first_bi = ms_out[:, :, :self.hidden_dim]
        ms_last_bi = ms_out[:, :, self.hidden_dim:]
        ms_sum_bi = ms_first_bi + ms_last_bi

        # 3. Concatenate LSTM outputs
        summed = torch.cat((w2v_sum_bi, ms_sum_bi))

        # 4. Only use the last item of the sequence's output
        summed = summed[-1, :, :]

        # 5. Send output to linear layer, then ReLU
        regression = self.linear(summed)
        regression = self.relu(regression)

        return regression