LSTM с вниманием

Я пытаюсь добавить механизм внимания к реализации составных LSTM https://github.com/salesforce/awd-lstm-lm

Во всех онлайн-примерах используется архитектура кодировщика-декодера, которую я не хочу использовать (нужно ли это для механизма внимания?).

В основном я использовал https://webcache.googleusercontent.com/search?q=cache:81Q7u36DRPIJ:https://github.com/zhedongzheng/finch/blob/master/nlp-models/pytorch/rnn_attn_text_clf.py+&cd=2&hl=en&ct=clnk&gl=uk

def __init__(self, rnn_type, ntoken, ninp, nhid, nlayers, dropout=0.5, dropouth=0.5, dropouti=0.5, dropoute=0.1, wdrop=0, tie_weights=False):
    super(RNNModel, self).__init__()
    self.encoder = nn.Embedding(ntoken, ninp)
    self.rnns = [torch.nn.LSTM(ninp if l == 0 else nhid, nhid if l != nlayers - 1 else (ninp if tie_weights else nhid), 1, dropout=0) for l in range(nlayers)]
    for rnn in self.rnns:
        rnn.linear = WeightDrop(rnn.linear, ['weight'], dropout=wdrop)
    self.rnns = torch.nn.ModuleList(self.rnns)
    self.attn_fc = torch.nn.Linear(ninp, 1)
    self.decoder = nn.Linear(nhid, ntoken)

    self.init_weights()

def attention(self, rnn_out, state):
    state = torch.transpose(state, 1,2)
    weights = torch.bmm(rnn_out, state)# torch.bmm(rnn_out, state)
    weights = torch.nn.functional.softmax(weights)#.squeeze(2)).unsqueeze(2)
    rnn_out_t = torch.transpose(rnn_out, 1, 2)
    bmmed = torch.bmm(rnn_out_t, weights)
    bmmed = bmmed.squeeze(2)
    return bmmed

def forward(self, input, hidden, return_h=False, decoder=False, encoder_outputs=None):
    emb = embedded_dropout(self.encoder, input, dropout=self.dropoute if self.training else 0)
    emb = self.lockdrop(emb, self.dropouti)

    new_hidden = []
    raw_outputs = []
    outputs = []
    for l, rnn in enumerate(self.rnns):
        temp = []
        for item in emb:
            item = item.unsqueeze(0)
            raw_output, new_h = rnn(item, hidden[l])

            raw_output = self.attention(raw_output, new_h[0])

            temp.append(raw_output)
        raw_output = torch.stack(temp)
        raw_output = raw_output.squeeze(1)

        new_hidden.append(new_h)
        raw_outputs.append(raw_output)
        if l != self.nlayers - 1:
            raw_output = self.lockdrop(raw_output, self.dropouth)
            outputs.append(raw_output)
    hidden = new_hidden

    output = self.lockdrop(raw_output, self.dropout)
    outputs.append(output)

    outputs = torch.stack(outputs).squeeze(0)
    outputs = torch.transpose(outputs, 2,1)
    output = output.transpose(2,1)
    output = output.contiguous()
    decoded = self.decoder(output.view(output.size(0)*output.size(1), output.size(2)))
    result = decoded.view(output.size(0), output.size(1), decoded.size(1))
    if return_h:
        return result, hidden, raw_outputs, outputs
    return result, hidden

введите здесь описание изображения

Эта модель обучающая, но моя потеря довольно высока по сравнению с моделью без модели внимания.


person Boris Mocialov    schedule 03.03.2018    source источник
comment
Можете ли вы кратко объяснить, что вам нужно, вместо того, чтобы показывать свой код? ваш вопрос немного сбивает с толку. Если вы можете сказать, в чем заключается ваша идея добавления механизма внимания поверх многослойной RNN, я смогу вам помочь. Кроме того, что вы имеете в виду, говоря о том, что внимание упускает одно измерение? зачем вам это дополнительное измерение? Кстати, вам не нужно использовать архитектуру кодировщика-декодера, чтобы привлечь внимание. если вы понимаете, что означает внимание, вы можете использовать его где угодно.   -  person Wasi Ahmad    schedule 07.03.2018
comment
@WasiAhmad Мне нужно изменить код из ссылки в вопросе (я использую его для моделирования языка), чтобы включить механизм внимания, чтобы я мог сравнивать качество обученных моделей. В исходном коде forward возвращает тензор 5x5x831 (batchesXlengthXdictionary). Если я обращаю внимание на свой вопрос, я получаю тензор размерности 5x831, в котором отсутствует одно измерение. Мне было интересно, могу ли я изменить функцию внимания, чтобы вернуть 3-е измерение, но я думаю, что вместо этого patapouf_ai предлагает уделять внимание каждому слову в тензоре 'emb'.   -  person Boris Mocialov    schedule 08.03.2018
comment
@WasiAhmad Я изменил функцию пересылки, чтобы передать слово за словом вниманию, но мои результаты хуже, чем у модели без внимания. Я отредактировал свой вопрос   -  person Boris Mocialov    schedule 09.03.2018


Ответы (2)


Я понял ваш вопрос, но немного сложно следовать вашему коду и найти причину, по которой потери не уменьшаются. Кроме того, неясно, почему вы хотите сравнивать последнее скрытое состояние RNN со всеми скрытыми состояниями на каждом временном шаге.

Обратите внимание, что определенный трюк / механизм полезен, если вы используете его правильно. То, как вы пытаетесь использовать механизм внимания, я не уверен, правильно ли это. Так что не ожидайте, что если вы используете уловку внимания в своей модели, вы получите хорошие результаты !! Вы должны подумать, почему механизм внимания принесет пользу вашей желаемой задаче?


Вы четко не упомянули, на какую задачу вы нацелены? Поскольку вы указали репо, которое содержит код для моделирования языка, я предполагаю, что задача заключается в следующем: учитывая последовательность токенов, предсказать следующий токен.

Одна возможная проблема, которую я вижу в вашем коде, заключается в следующем: в цикле for item in emb: вы всегда будете использовать вложения в качестве входных данных для каждого уровня LSTM, поэтому для меня не имеет смысла иметь составной LSTM.


Теперь позвольте мне сначала ответить на ваш вопрос, а затем шаг за шагом показать, как вы можете построить желаемую архитектуру NN.

Нужно ли мне использовать архитектуру кодировщика-декодера для использования механизма внимания?

Архитектура кодер-декодер более известна как от последовательности к последовательности для обучения и широко используется во многих задачах генерации, например, при машинном переводе. Ответ на ваш вопрос: нет, для использования механизма внимания не требуется использовать какую-либо конкретную архитектуру нейронной сети.


Структура, представленная на рисунке, немного двусмысленна, но ее легко реализовать. Поскольку ваша реализация мне непонятна, я пытаюсь помочь вам найти лучший способ ее реализации. В следующем обсуждении я предполагаю, что мы имеем дело с вводом текста.

Скажем, у нас есть ввод формы 16 x 10, где 16 - это batch_size, а 10 - это seq_len. Мы можем предположить, что у нас есть 16 предложений в мини-пакете, а длина каждого предложения равна 10.

batch_size, vocab_size = 16, 100
mat = np.random.randint(vocab_size, size=(batch_size, 10))
input_var = Variable(torch.from_numpy(mat))

Здесь 100 можно рассматривать как размер словарного запаса. Важно отметить, что на протяжении всего приведенного мной примера я принимаю batch_size как первое измерение во всех соответствующих тензорах / переменных.

Теперь давайте вставим входную переменную.

embedding = nn.Embedding(100, 50)
embed = embedding(input_var)

После встраивания мы получили переменную формы 16 x 10 x 50, где 50 - размер встраивания.

Теперь давайте определим двухуровневый однонаправленный LSTM со 100 скрытыми модулями на каждом уровне.

rnns = nn.ModuleList()
nlayers, input_size, hidden_size = 2, 50, 100
for i in range(nlayers):
    input_size = input_size if i == 0 else hidden_size
    rnns.append(nn.LSTM(input_size, hidden_size, 1, batch_first=True))

Затем мы можем передать наш вход в этот двухуровневый LSTM, чтобы получить результат.

sent_variable = embed
outputs, hid = [], []
for i in range(nlayers):
    if i != 0:
        sent_variable = F.dropout(sent_variable, p=0.3, training=True)
    output, hidden = rnns[i](sent_variable)
    outputs.append(output)
    hid.append(hidden[0].squeeze(0))
    sent_variable = output

rnn_out = torch.cat(outputs, 2)
hid = torch.cat(hid, 1)

Теперь вы можете просто использовать hid, чтобы предсказать следующее слово. Я бы посоветовал вам это сделать. Здесь форма hid равна batch_size x (num_layers*hidden_size).

Но поскольку вы хотите использовать внимание для вычисления показателя мягкого выравнивания между последними скрытыми состояниями с каждым скрытым состоянием, созданным слоями LSTM, давайте сделаем это.

sent_variable = embed
hid, con = [], []
for i in range(nlayers):
    if i != 0:
        sent_variable = F.dropout(sent_variable, p=0.3, training=True)
    output, hidden = rnns[i](sent_variable)
    sent_variable = output

    hidden = hidden[0].squeeze(0) # batch_size x hidden_size
    hid.append(hidden)
    weights = torch.bmm(output[:, 0:-1, :], hidden.unsqueeze(2)).squeeze(2)  
    soft_weights = F.softmax(weights, 1)  # batch_size x seq_len
    context = torch.bmm(output[:, 0:-1, :].transpose(1, 2), soft_weights.unsqueeze(2)).squeeze(2)
    con.append(context)

hid, con = torch.cat(hid, 1), torch.cat(con, 1)
combined = torch.cat((hid, con), 1)

Здесь мы вычисляем оценку мягкого выравнивания между последним состоянием со всеми состояниями каждого временного шага. Затем мы вычисляем вектор контекста, который представляет собой просто линейную комбинацию всех скрытых состояний. Мы объединяем их в единое представление.

Обратите внимание, я удалил последние скрытые состояния из output: output[:, 0:-1, :], так как вы сравниваете с самим последним скрытым состоянием.

Окончательное combined представление хранит последние скрытые состояния и векторы контекста, созданные на каждом уровне. Вы можете напрямую использовать это представление для предсказания следующего слова.

Предсказать следующее слово очень просто, и, поскольку вы используете простой линейный слой, это нормально.


Изменить: мы можем сделать следующее, чтобы предсказать следующее слово.

decoder = nn.Linear(nlayers * hidden_size * 2, vocab_size)
dec_out = decoder(combined)

Здесь форма dec_out равна batch_size x vocab_size. Теперь мы можем вычислить отрицательную потерю логарифмической вероятности, которая будет использована для обратного распространения позже.

Перед вычислением отрицательной потери логарифма правдоподобия нам нужно применить log_softmax к выходному сигналу декодера.

dec_out = F.log_softmax(dec_out, 1)
target = np.random.randint(vocab_size, size=(batch_size))
target = Variable(torch.from_numpy(target))

И мы также определили цель, которая требуется для расчета потерь. Подробнее см. NLLLoss. Итак, теперь мы можем вычислить потери следующим образом.

criterion = nn.NLLLoss()
loss = criterion(dec_out, target)
print(loss)

Напечатанная величина потерь:

Variable containing:
 4.6278
[torch.FloatTensor of size 1]

Надеюсь, все объяснение вам поможет !!

person Wasi Ahmad    schedule 12.03.2018
comment
Спасибо за развернутый ответ. Теперь я пробую ваше предложение, и у меня возникла та же проблема, что и у меня изначально: моя вставка имеет размеры 5x80x400, в то время как скрытое состояние rnn имеет размеры 80x400, где 5 - количество слов в качестве ввода (это следующая задача предсказания слова ), 80 - размер партии, а 400 - размер вложения. Следовательно, weights = torch.bmm (...) не может быть вычислено, поскольку для этого требуются такие же размеры партий. Вот почему я попробовал предложение patapouf_ai - итерация через вставку - person Boris Mocialov; 12.03.2018
comment
Чтобы использовать bmm, мне нужно иметь rnn_output = (batch, seq_len, cell_size) и hidden = (batch, cell_size, 1). В моем случае rnn_output batch! = Скрытая партия - person Boris Mocialov; 12.03.2018
comment
похоже, что добавление batch_first = True в определение LSTM решает проблему ... - person Boris Mocialov; 12.03.2018
comment
Думаю, мне понадобится помощь с расчетом убытков. github.com/salesforce/awd-lstm-lm/ blob / master / model.py # L94 передает вывод rnn через линейный декодер, а затем преобразует вывод в (batch, batch_size, dictionary_size). И hid, и con имеют измерения (пакеты, embedding_dimension), накопленные за все проходы rnn. Честно говоря, я заблудился. - person Boris Mocialov; 12.03.2018
comment
Единственный способ, которым я могу это видеть, - это создать линейный слой из пакетов размера batch_size dictionary_size, а затем изменить его форму, имеет ли это смысл? - person Boris Mocialov; 12.03.2018
comment
Я обновил свой пост. Пожалуйста, взгляните. И да, в приведенном мной примере я предполагал, что вы используете batch_first=True всякий раз, когда используете RNN. - person Wasi Ahmad; 12.03.2018
comment
Проблема, с которой я столкнулся сейчас, - это несоответствие размерностей. Убыток от первоначального репо предполагает 400 целей с прогнозом 400x8967. В вашем коде декодер выводит Vocab_size, что приводит к тензору 5xvocab_size, но для соответствия исходному репо мне нужен тензор 400xvocab_size. Я могу заставить декодер выводить Vocab_size * (400/5) тензор, но тогда у меня заканчивается память. Альтернативой является уменьшение размера партии, я полагаю - person Boris Mocialov; 13.03.2018
comment
@MocialovBoris, пожалуйста, задайте другой вопрос по проблеме, с которой вы столкнулись. в одном посте вы должны сосредоточиться только на одной важной проблеме. - person Wasi Ahmad; 13.03.2018

Вся суть внимания заключается в том, что порядок слов в разных языках различается, и поэтому при декодировании 5-го слова на целевом языке вам может потребоваться обратить внимание на 3-е слово (или кодировку 3-го слова) на исходном языке, потому что они слова, которые соответствуют друг другу. Вот почему вы чаще всего видите, что внимание уделяется структуре декодера кодировщика.

Если я правильно понял, вы делаете предсказание следующего слова? В этом случае все еще может иметь смысл использовать внимание, потому что следующее слово может сильно зависеть от слова «4 шага» в прошлом.

Итак, в основном вам нужно:

rnn: который принимает input формы MBxninp и hidden формы MBxnhid и выводит h формы MBxnhid.

h, next_hidden = rnn(input, hidden)

внимание: которое занимает последовательность h, а последний h_last решает, насколько важен каждый из них, присваивая каждому вес w.

w = attention(hs, h_last)

где w имеет форму seq_len x MB x 1, hs имеет форму seq_len x MB x nhid, а h_last имеет форму MB x nhid.

Теперь вы взвешиваете hs на w:

h_att = torch.sum(w*hs, dim=0) #shape MB x n_hid

Теперь дело в том, что вам нужно делать это для каждого временного шага:

h_att_list = []
h_list = []
hidden = hidden_init
for word in embedded_words:
    h, hidden = rnn(word, hidden)
    h_list.append(h)
    h_att = attention(torch.stack(h_list), h)
    h_att_list.append(h_att)

А затем вы можете применить декодер (который может быть MLP, а не просто линейным преобразованием) на h_att_list.

person patapouf_ai    schedule 06.03.2018
comment
Я воспользовался вашим предложением, чтобы пословно вводить в слой внимания, но производительность модели довольно низка. Я обновил свой вопрос новыми результатами - person Boris Mocialov; 09.03.2018
comment
хотя, в моем подходе, я передаю только один вывод rnn на уровень внимания, а не список, как вы показываете .. У меня была бы проблема размерности, если бы я передал bmm в моем внимании, если бы я передал список - person Boris Mocialov; 09.03.2018
comment
Вся суть внимания в том, что ему нужно выбрать, на какие слова нужно обратить внимание сейчас. Затем он выводит веса для каждого слова, чтобы сказать, насколько они важны для следующего шага. Если вы сосредоточили внимание на одном слове, это совершенно бессмысленно. - person patapouf_ai; 09.03.2018
comment
Я рекомендую вам попробовать реализовать эту архитектуру: arxiv.org/abs/1706.03762, где ваша выходная последовательность - это ваша входная последовательность (так что вы в основном делаете предсказание следующего шага). Приведенный выше ответ - это простейшая модификация вашего текущего кода, которая, я думаю, имеет смысл, но этот информационный документ должен дать лучшие результаты. - person patapouf_ai; 09.03.2018