Текст в основном знакомит с тем, как использовать PyTorch для воспроизведения Seq2Seq (с вниманием) для выполнения простых задач машинного перевода, пожалуйста, сначала прочитайте статьюNeural Machine Translation by Jointly Learning to Align and Translate, затем потратьте 15 минут на чтение этих двух моих статейSeq2Seq и механизм внимания,Графическое внимание, и, наконец, посмотрите на текст, чтобы добиться эффекта расширения прав и возможностей людей и получения вдвое большего результата при половинных усилиях.
предварительная обработка данных
Код для предобработки данных на самом деле вызывает различные API, я не хочу, чтобы читатели отвлекались на эти менее важные части, поэтому я не буду выкладывать здесь код, а просто опишу его.
Как показано на рисунке ниже, в этой статье используется набор данных «Немецкий → Английский», ввод осуществляется на немецком языке, и каждое введенное предложение имеет специальный идентификатор в начале и в конце. Вывод на английском языке, и каждое предложение вывода также начинается и заканчивается специальным идентификатором.
Будь то английский или немецкий, длина каждого предложения не фиксирована, поэтому для предложений в каждом пакете я добавляю [seq_len, batch_size]
Просто распечатайте часть данных и посмотрите форму инкапсуляции данных
При предварительной обработке данных необходимо построить словари отдельно от исходного предложения и целевого предложения, то есть построить тезаурус для немецкого языка и тезаурус для английского языка.
Encoder
Кодировщик использую однослойный двунаправленный GRU
Выход скрытого состояния двунаправленного GRU объединяется из двух векторов, например.,......Скрытое состояние последнего слоя всегда представляет собой вывод ГРУ.
Предположим, что это m-слойный ГРУ, тогдаСкрытые состояния во всех слоях в последний момент составляют окончательные скрытые состояния ГРУ.
в
так
Согласно статье или если вы читали мою графику Внимание, вы будете знать, что нам нужен вывод последнего скрытого слоя (как прямого, так и обратного), чтобы мы могли пройтиhidden[-2,:,:]
иhidden[-1,:,:]
Выньте скрытые состояния последнего слоя и соедините их вместе, как
Последняя деталь заключается в том, чтоРазмерность[batch_size, en_hid_dim*2]
, даже без механизма Внимание,Так как начальное скрытое состояние Декодера тоже неверно, т.к. размерности не совпадают, начальное скрытое состояние Декодера трехмерное, и теперь нашеявляется двумерным, поэтому необходимоРазмеры преобразуются в трехмерные, а также корректируются значения в каждом измерении. Сначала я передаю полносвязную нейронную сеть,измерение становится[batch_size, dec_hid_dim]
Деталей кодировщика очень много.Код прямо под ним.Мой стиль кода такой,комментарии вверху,а код внизу.
class Encoder(nn.Module):
def __init__(self, input_dim, emb_dim, enc_hid_dim, dec_hid_dim, dropout):
super().__init__()
self.embedding = nn.Embedding(input_dim, emb_dim)
self.rnn = nn.GRU(emb_dim, enc_hid_dim, bidirectional = True)
self.fc = nn.Linear(enc_hid_dim * 2, dec_hid_dim)
self.dropout = nn.Dropout(dropout)
def forward(self, src):
'''
src = [src_len, batch_size]
'''
src = src.transpose(0, 1) # src = [batch_size, src_len]
embedded = self.dropout(self.embedding(src)).transpose(0, 1) # embedded = [src_len, batch_size, emb_dim]
# enc_output = [src_len, batch_size, hid_dim * num_directions]
# enc_hidden = [n_layers * num_directions, batch_size, hid_dim]
enc_output, enc_hidden = self.rnn(embedded) # if h_0 is not give, it will be set 0 acquiescently
# enc_hidden is stacked [forward_1, backward_1, forward_2, backward_2, ...]
# enc_output are always from the last layer
# enc_hidden [-2, :, : ] is the last of the forwards RNN
# enc_hidden [-1, :, : ] is the last of the backwards RNN
# initial decoder hidden is final hidden state of the forwards and backwards
# encoder RNNs fed through a linear layer
# s = [batch_size, dec_hid_dim]
s = torch.tanh(self.fc(torch.cat((enc_hidden[-2,:,:], enc_hidden[-1,:,:]), dim = 1)))
return enc_output, s
Attention
внимание не что иное, как три формулы
вОтносится к переменной в Encoders
,Относится к переменной в Encoderenc_output
,По сути, это простая полносвязная нейронная сеть.
Мы можем сделать вывод, каковы размеры каждой переменной из последней формулы, или каковы требования к размерам
во-первыхРазмер должен быть[batch_size, src_len]
, это несомненно, тоРазмер также должен быть[batch_size, src_len]
,илиявляется трехмерным, но значение размерности равно 1, что может быть получено путемsqueeze()
стать двухмерным. Здесь мы предполагаемРазмерность[batch_size, src_len, 1]
, я объясню, почему это предположение сделано позже
Продолжайте отжиматься, переменнаяРазмер должен быть[?, 1]
,?
Указывает, что я не знаю, каким должно быть его значение в данный момент.Размер должен быть[batch_size, src_len, ?]
теперь известноРазмерность[batch_size, src_len, enc_hid_dim*2]
,Текущее измерение[batch_size, dec_hid_dim]
, эти две переменные нужно соединить и передать в полносвязную нейронную сеть, поэтому сначала нам нужноизмерение становится[batch_size, src_len, dec_hid_dim]
, размер после сращивания становится[batch_size, src_len, enc_hid_dim*2+dec_hid_dim]
,тогдаВходные и выходные значения этой функции также имеют
attn = nn.Linear(enc_hid_dim*2+dec_hid_dim, ?)
Пока, кроме?
Некоторые значения не ясны, все остальные размеры выведены. Теперь давайте вернемся и подумаем об этом?
Сколько установлено, кажется, что нет предела, поэтому мы можем установить?
для любого значения (в коде я установил?
заdec_hid_dim
)
Внимание подробностей так много, код приведен ниже
class Attention(nn.Module):
def __init__(self, enc_hid_dim, dec_hid_dim):
super().__init__()
self.attn = nn.Linear((enc_hid_dim * 2) + dec_hid_dim, dec_hid_dim, bias=False)
self.v = nn.Linear(dec_hid_dim, 1, bias = False)
def forward(self, s, enc_output):
# s = [batch_size, dec_hid_dim]
# enc_output = [src_len, batch_size, enc_hid_dim * 2]
batch_size = enc_output.shape[1]
src_len = enc_output.shape[0]
# repeat decoder hidden state src_len times
# s = [batch_size, src_len, dec_hid_dim]
# enc_output = [batch_size, src_len, enc_hid_dim * 2]
s = s.unsqueeze(1).repeat(1, src_len, 1)
enc_output = enc_output.transpose(0, 1)
# energy = [batch_size, src_len, dec_hid_dim]
energy = torch.tanh(self.attn(torch.cat((s, enc_output), dim = 2)))
# attention = [batch_size, src_len]
attention = self.v(energy).squeeze(2)
return F.softmax(attention, dim=1)
Seq2Seq(with Attention)
Позвольте мне изменить порядок, сначала поговорим о Seq2Seq, а затем поговорим о части декодера.
Традиционный Seq2Seq заключается в непосредственном вводе каждого слова в предложении в декодер для обучения.После введения механизма внимания мне нужно иметь возможность вручную управлять вводом каждого слова (поскольку ввод каждого слова в декодер требует некоторых операций), Итак, в коде вы увидите, что я использую цикл for для цикла trg_len-1 раз (
И в процессе обучения я использовал механизм под названием «Принуждение учителя», чтобы обеспечить скорость обучения и повысить надежность.Если вы не знаете о принуждении учителя, вы можете прочитать мою статью.статья
Подумайте, что нужно сделать в цикле for? Сначала передаем переменную в Декодер, так как расчет Внимания осуществляется внутри Декодера, мне нужно передатьdec_input
,s
,enc_output
Эти три переменные передаются в декодер, и декодер возвращаетdec_output
и новыйs
. Тогда по вероятностиdec_output
Просто сделай принуждение учителя
Деталей Seq2Seq так много, что код приведен ниже.
class Seq2Seq(nn.Module):
def __init__(self, encoder, decoder, device):
super().__init__()
self.encoder = encoder
self.decoder = decoder
self.device = device
def forward(self, src, trg, teacher_forcing_ratio = 0.5):
# src = [src_len, batch_size]
# trg = [trg_len, batch_size]
# teacher_forcing_ratio is probability to use teacher forcing
batch_size = src.shape[1]
trg_len = trg.shape[0]
trg_vocab_size = self.decoder.output_dim
# tensor to store decoder outputs
outputs = torch.zeros(trg_len, batch_size, trg_vocab_size).to(self.device)
# enc_output is all hidden states of the input sequence, back and forwards
# s is the final forward and backward hidden states, passed through a linear layer
enc_output, s = self.encoder(src)
# first input to the decoder is the <sos> tokens
dec_input = trg[0,:]
for t in range(1, trg_len):
# insert dec_input token embedding, previous hidden state and all encoder hidden states
# receive output tensor (predictions) and new hidden state
dec_output, s = self.decoder(dec_input, s, enc_output)
# place predictions in a tensor holding predictions for each token
outputs[t] = dec_output
# decide if we are going to use teacher forcing or not
teacher_force = random.random() < teacher_forcing_ratio
# get the highest predicted token from our predictions
top1 = dec_output.argmax(1)
# if teacher forcing, use actual next token as next input
# if not, use predicted token
dec_input = trg[t] if teacher_force else top1
return outputs
Decoder
Декодер использую односторонний однослойный ГРУ
Часть декодера на самом деле состоит из трех формул
Относится к переменной в Encoderenc_output
,значитdec_input
Результат, полученный после WordEmbedding,На самом деле функция предназначена для преобразования размеров, поскольку желаемый результатTRG_VOCAB_SIZE
размер. Одна из деталей заключается в том, что GRU имеет только два параметра, один вход и один вход скрытого слоя, но в приведенной выше формуле есть три переменные, поэтому мы должны выбрать одну в качестве входных данных скрытого слоя, а два других «интегрировать» в качестве входных данных.
Каковы размеры каждой переменной, которые мы выводим из первой формулы
Сначала вызовите внимание один раз в кодировщике, чтобы получить вес, его размерность[batch_size, src_len]
,иРазмерность[src_len, batch_size, enc_hid_dim*2]
, они должны быть перемножены вместе, и должны сохранятьсяbatch_size
это измерение, поэтому мы должны сначалаРасширьте одно измерение, затем транспонируйтепорядок размеров, а затемУмножить на партию (то есть умножить матрицы в одной партии)
a = a.unsqueeze(1) # [batch_size, 1, src_len]
H = H.transpose(0, 1) # [batch_size, src_len, enc_hid_dim*2]
c = torch.bmm(a, h) # [batch_size, 1, enc_hid_dim*2]
Как упоминалось ранее, поскольку ГРУ не нужны три переменные, их нужноисобрать это вместе,на самом деле находится в классе Seq2Seqdec_input
переменная, ее размерность[batch_size]
, так сначалаРазверните измерение, а затем передайте WordEmbedding, чтобы он стал[batch_size, 1, emb_dim]
. последняя параиконкат
y = y.unsqueeze(1) # [batch_size, 1]
emb_y = self.emb(y) # [batch_size, 1, emb_dim]
rnn_input = torch.cat((emb_y, c), dim=2) # [batch_size, 1, emb_dim+enc_hid_dim*2]
Размерность[batch_size, dec_hid_dim]
, поэтому его следует сначала расширить на одно измерение
rnn_input = rnn_input.transpose(0, 1) # [1, batch_size, emb_dim+enc_hid_dim*2]
s = s.unsqueeze(1) # [batch_size, 1, dec_hid_dim]
# dec_output = [1, batch_size, dec_hid_dim]
# dec_hidden = [1, batch_size, dec_hid_dim] = s (new, is not s previously)
dec_output, dec_hidden = self.rnn(rnn_input, s)
Последняя формула требует, чтобы все три переменные были объединены вместе, а затем для получения окончательного прогноза используется полносвязная нейронная сеть. Давайте сначала проанализируем размерности этих трех переменных,Размерность[batch_size, 1, emb_dim]
,Размерность[batch_size, 1, enc_hid_dim]
,Размерность[1, batch_size, dec_hid_dim]
, так что мы можем соединить их все вот так
emd_y = emb_y.squeeze(1) # [batch_size, emb_dim]
c = w.squeeze(1) # [batch_size, enc_hid_dim*2]
s = s.squeeze(0) # [batch_size, dec_hid_dim]
fc_input = torch.cat((emb_y, c, s), dim=1) # [batch_size, enc_hid_dim*2+dec_hid_dim+emb_hid]
Выше приведены сведения о части декодера, а код приведен ниже (выше приведены только примеры кодов, а имена переменных могут отличаться от приведенного ниже кода)
class Decoder(nn.Module):
def __init__(self, output_dim, emb_dim, enc_hid_dim, dec_hid_dim, dropout, attention):
super().__init__()
self.output_dim = output_dim
self.attention = attention
self.embedding = nn.Embedding(output_dim, emb_dim)
self.rnn = nn.GRU((enc_hid_dim * 2) + emb_dim, dec_hid_dim)
self.fc_out = nn.Linear((enc_hid_dim * 2) + dec_hid_dim + emb_dim, output_dim)
self.dropout = nn.Dropout(dropout)
def forward(self, dec_input, s, enc_output):
# dec_input = [batch_size]
# s = [batch_size, dec_hid_dim]
# enc_output = [src_len, batch_size, enc_hid_dim * 2]
dec_input = dec_input.unsqueeze(1) # dec_input = [batch_size, 1]
embedded = self.dropout(self.embedding(dec_input)).transpose(0, 1) # embedded = [1, batch_size, emb_dim]
# a = [batch_size, 1, src_len]
a = self.attention(s, enc_output).unsqueeze(1)
# enc_output = [batch_size, src_len, enc_hid_dim * 2]
enc_output = enc_output.transpose(0, 1)
# c = [1, batch_size, enc_hid_dim * 2]
c = torch.bmm(a, enc_output).transpose(0, 1)
# rnn_input = [1, batch_size, (enc_hid_dim * 2) + emb_dim]
rnn_input = torch.cat((embedded, c), dim = 2)
# dec_output = [src_len(=1), batch_size, dec_hid_dim]
# dec_hidden = [n_layers * num_directions, batch_size, dec_hid_dim]
dec_output, dec_hidden = self.rnn(rnn_input, s.unsqueeze(0))
# embedded = [batch_size, emb_dim]
# dec_output = [batch_size, dec_hid_dim]
# c = [batch_size, enc_hid_dim * 2]
embedded = embedded.squeeze(0)
dec_output = dec_output.squeeze(0)
c = c.squeeze(0)
# pred = [batch_size, output_dim]
pred = self.fc_out(torch.cat((dec_output, c, embedded), dim = 1))
return pred, dec_hidden.squeeze(0)
Определить модель
INPUT_DIM = len(SRC.vocab)
OUTPUT_DIM = len(TRG.vocab)
ENC_EMB_DIM = 256
DEC_EMB_DIM = 256
ENC_HID_DIM = 512
DEC_HID_DIM = 512
ENC_DROPOUT = 0.5
DEC_DROPOUT = 0.5
attn = Attention(ENC_HID_DIM, DEC_HID_DIM)
enc = Encoder(INPUT_DIM, ENC_EMB_DIM, ENC_HID_DIM, DEC_HID_DIM, ENC_DROPOUT)
dec = Decoder(OUTPUT_DIM, DEC_EMB_DIM, ENC_HID_DIM, DEC_HID_DIM, DEC_DROPOUT, attn)
model = Seq2Seq(enc, dec, device).to(device)
TRG_PAD_IDX = TRG.vocab.stoi[TRG.pad_token]
criterion = nn.CrossEntropyLoss(ignore_index = TRG_PAD_IDX).to(device)
optimizer = optim.Adam(model.parameters(), lr=1e-3)
предпоследний рядCrossEntropyLoss()
Параметры в редки,ignore_index=TRG_PAD_IDX
, функция этого параметра состоит в том, чтобы игнорировать определенную категорию, а не вычислять ее потери, но следует отметить, что категория в реальном значении игнорируется.Например, в следующем коде категория реального значения равна 1, и все прогнозируемое значение прогнозируется как 2 (нижний индекс начинается с 0), а функция потерь настроена на игнорирование первого типа потерь, и в это время будет напечатан 0
label = torch.tensor([1, 1, 1])
pred = torch.tensor([[0.1, 0.2, 0.6], [0.2, 0.1, 0.8], [0.1, 0.1, 0.9]])
loss_fn = nn.CrossEntropyLoss(ignore_index=1)
print(loss_fn(pred, label).item()) # 0
Если функция потерь настроена на игнорирование второй категории, потери в это время не будут равны 0.
label = torch.tensor([1, 1, 1])
pred = torch.tensor([[0.1, 0.2, 0.6], [0.2, 0.1, 0.8], [0.1, 0.1, 0.9]])
loss_fn = nn.CrossEntropyLoss(ignore_index=2)
print(loss_fn(pred, label).item()) # 1.359844
наконец даноСсылка на полный код (требуется сила науки)Адрес проекта на гитхабе:nlp-tutorial