4
$\begingroup$

Please see the update, below.

I am working with embeddings and wanted to see how feasible it is to predict some scores attached to some sequences of words. The details of the scores are not important.

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

I have been following this tutorial which tries to predict part-of-speech tags of such input. In such case, the output of the system is a distribution of all possible tags for all tokens in the sequence, e.g. for three possible POS classes {'DET': 0, 'NN': 1, 'V': 2}, the output for ('the', 'dog', 'ate', 'the', 'apple') could be

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

Each row is a token, the index of the highest value in a token is the best predicted POS tag.

I understand this example relatively well, so I wanted to adapt it to a regression problem. The full code is below, but I am trying to make sense of the output.

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)

The output is:

# 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]])

But I don't understand why. I was expecting a single output. The size of the tensor is the same as the number of tokens of the input. I would, then, guess that for each step in the input, the hidden state is given. Is that correct? Does that mean that the last item in the tensor (tensor[-1], or is it the first tensor[0]?) is the final prediction? Why are all outputs given? Or lies my misunderstanding earlier in the forward-pass? Perhaps I should only feed the last item of the LSTM layer to the linear layer?

I am also interested to know how this extrapolates to bidirectional LSTMs and multilayer LSTMs, and even how this would work with GRUs (bidirectional or not).

The bounty will be given to the person who can explain why we would use the last output or the last hidden state or what the difference means from a goal-directed perspective. In addition, some information about multilayer architectures and bidirectional RNNs is welcome. For instance, is it common practice to sum or concatenate the output and hidden state of bidirectional LSTM/GRU to get your data into sensible shape? If so, how do you do it?


Update 18 February, 2019

I went ahead, and testing a lot of stuff out and I came up with this network which seems to work alright as far as I have tested it

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

Are there any obvious mistakes that I made. Some other questions remain, too. Is my understanding of LSTMs and the last output correct? Am I right in using only the output of the last item in the sequence? And what are the consequences of summing vs. concatenating the output of the two LSTMs?

$\endgroup$

bumped to the homepage by Community 2 days ago

This question has answers that may be good or bad; the system has marked it active so that they can be reviewed.

0
$\begingroup$

Part of the confusion could be because POS tagging is not a regression problem. POS tagging is a classification problem. Classification is predicting the most likely category from a set of discrete categories. Regression is predicting a numeric output.

POS tagging is multi-class classification (e.g., DET, NN, V, ...). The last layer in a neural network for a multi-class classification should be a softmax function. A softmax function will convert the activation of the nodes for each category into a probability. The largest probability will be the predicted category, in this case a POS tag.

$\endgroup$
  • $\begingroup$ I know that, I am not confused about that. The confusion or question is about the output of a (bidirectional) LSTM and how that can be fed into a final linear layer that predicts a number. $\endgroup$ – Bram Vanroy Feb 15 at 21:09
0
$\begingroup$

I think what you are missing here is a clear understanding of LSTMs. I will answer in parts.

"why we would use the last output or the last hidden state": The last hidden state and the last output are not the same. The last output is generated from the last hidden state by passing it through a linear layer, such as softmax.

"what the difference means from a goal-directed perspective": The last hidden state is simply a set of weights, while the last output is a prediction based on those weights.

"is it common practice to sum or concatenate the output and hidden state of bidirectional LSTM/GRU to get your data into sensible shape": It is not impossible to concatenate the output and the hidden state, because, well, they are tensors. But, it is inadvisable to do so. Also, it makes no practical sense. The output is generated by using the hidden state as one of the inputs in LSTMs.

Regarding the main question, the final tensor you are getting is simply logits. If you want a single value out of it, like the other answer states, you should stack a softmax layer on top of your existing architecture.

$\endgroup$
  • $\begingroup$ It seems you may have misinterpreted my questions. I understand the difference between hidden state, cell state, and output. What I don't understand is when it makes sense to use the hidden state vs. when to use the output. It seems that for an encoder/decoder scenario (e.g. seq2seq, translation) the last hidden state is used as the first input to the decoder. In other cases, the output is used. I'm not sure about my case. Also, I meant: concatenating or adding the outputs OR the hidden states of the two GRUs (output+output, hidden+hidden), and if that would make sense. $\endgroup$ – Bram Vanroy Feb 18 at 17:48
  • $\begingroup$ Also, a softmax returns a probability distribution over some output classes. Why would that be used for regression? $\endgroup$ – Bram Vanroy Feb 18 at 17:48

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.