PyTorch реализация Seq2Seq (Внимание)

NLP

Видео объяснение станции B

Текст в основном знакомит с тем, как использовать PyTorch для воспроизведения Seq2Seq (с вниманием) для выполнения простых задач машинного перевода, пожалуйста, сначала прочитайте статьюNeural Machine Translation by Jointly Learning to Align and Translate, затем потратьте 15 минут на чтение этих двух моих статейSeq2Seq и механизм внимания,Графическое внимание, и, наконец, посмотрите на текст, чтобы добиться эффекта расширения прав и возможностей людей и получения вдвое большего результата при половинных усилиях.

предварительная обработка данных

Код для предобработки данных на самом деле вызывает различные API, я не хочу, чтобы читатели отвлекались на эти менее важные части, поэтому я не буду выкладывать здесь код, а просто опишу его.

Как показано на рисунке ниже, в этой статье используется набор данных «Немецкий → Английский», ввод осуществляется на немецком языке, и каждое введенное предложение имеет специальный идентификатор в начале и в конце. Вывод на английском языке, и каждое предложение вывода также начинается и заканчивается специальным идентификатором.

Будь то английский или немецкий, длина каждого предложения не фиксирована, поэтому для предложений в каждом пакете я добавляю , чтобы их длины были одинаковыми, то естьПредложения в пакете имеют одинаковую длину, предложения в разных пакетах не обязательно имеют одинаковую длину.. Размеры, показанные на рисунке ниже,[seq_len, batch_size]

Просто распечатайте часть данных и посмотрите форму инкапсуляции данных

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

Encoder

Кодировщик использую однослойный двунаправленный GRU

Выход скрытого состояния двунаправленного GRU объединяется из двух векторов, например.h1=[h1;hT]h_1=[\overrightarrow{h_1};\overleftarrow{h_T}],h2=[h2;hT1]h_2=[\overrightarrow{h_2};\overleftarrow{h}_{T-1}]......Скрытое состояние последнего слоя всегда представляет собой вывод ГРУ.

output={h1,h2,...hT}output=\{h_1,h_2,...h_T\}

Предположим, что это m-слойный ГРУ, тогдаСкрытые состояния во всех слоях в последний момент составляют окончательные скрытые состояния ГРУ.

hidden={hT1,hT2,...,hTm}hidden=\{h^1_T,h^2_T,...,h^m_T\}

в

hTi=[hTi;h1i]h^i_T=[\overrightarrow{h^i_T};\overleftarrow{h^i_1}]

так

hidden={[hT1;h11],[hT2;h12],...,[hTm;h1m]}hidden=\{[\overrightarrow{h^1_T};\overleftarrow{h^1_1}],[\overrightarrow{h^2_T};\overleftarrow{h^2_1}],...,[\overrightarrow{h^m_T};\overleftarrow{h^m_1}]\}

Согласно статье или если вы читали мою графику Внимание, вы будете знать, что нам нужен вывод последнего скрытого слоя (как прямого, так и обратного), чтобы мы могли пройтиhidden[-2,:,:]иhidden[-1,:,:]Выньте скрытые состояния последнего слоя и соедините их вместе, какs0s_0

Последняя деталь заключается в том, чтоs0s_0Размерность[batch_size, en_hid_dim*2], даже без механизма Внимание,s0s_0Так как начальное скрытое состояние Декодера тоже неверно, т.к. размерности не совпадают, начальное скрытое состояние Декодера трехмерное, и теперь нашеs0s_0является двумерным, поэтому необходимоs0s_0Размеры преобразуются в трехмерные, а также корректируются значения в каждом измерении. Сначала я передаю полносвязную нейронную сеть,s0s_0измерение становится[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

внимание не что иное, как три формулы

Et=tanh(attn(st1,H))at~=vEtat=softmax(at~)E_t=tanh(attn(s_{t-1},H))\\ \tilde{a_t}=vE_t\\ {a_t}=softmax(\tilde{a_t})

вst1s_{t-1}Относится к переменной в Encoders,HHОтносится к переменной в Encoderenc_output,attn()attn()По сути, это простая полносвязная нейронная сеть.

Мы можем сделать вывод, каковы размеры каждой переменной из последней формулы, или каковы требования к размерам

во-первыхata_tРазмер должен быть[batch_size, src_len], это несомненно, тоat~\tilde{a_t}Размер также должен быть[batch_size, src_len],илиat~\tilde{a_t}является трехмерным, но значение размерности равно 1, что может быть получено путемsqueeze()стать двухмерным. Здесь мы предполагаемat~\tilde{a_t}Размерность[batch_size, src_len, 1], я объясню, почему это предположение сделано позже

Продолжайте отжиматься, переменнаяvvРазмер должен быть[?, 1],?Указывает, что я не знаю, каким должно быть его значение в данный момент.EtE_tРазмер должен быть[batch_size, src_len, ?]

теперь известноHHРазмерность[batch_size, src_len, enc_hid_dim*2],st1s_{t-1}Текущее измерение[batch_size, dec_hid_dim], эти две переменные нужно соединить и передать в полносвязную нейронную сеть, поэтому сначала нам нужноst1s_{t-1}измерение становится[batch_size, src_len, dec_hid_dim], размер после сращивания становится[batch_size, src_len, enc_hid_dim*2+dec_hid_dim],тогдаattn()attn()Входные и выходные значения этой функции также имеют

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

Декодер использую односторонний однослойный ГРУ

Часть декодера на самом деле состоит из трех формул

c=atHst=GRU(emb(yt),c,st1)yt^=f(emb(yt),c,st)c=a_tH\\ s_t=GRU(emb(y_t), c, s_{t-1})\\ \hat{y_t}=f(emb(y_t), c, s_t)

HHОтносится к переменной в Encoderenc_output,emb(yt)emb(y_t)значитdec_inputРезультат, полученный после WordEmbedding,f()f()На самом деле функция предназначена для преобразования размеров, поскольку желаемый результатTRG_VOCAB_SIZEразмер. Одна из деталей заключается в том, что GRU имеет только два параметра, один вход и один вход скрытого слоя, но в приведенной выше формуле есть три переменные, поэтому мы должны выбрать одну в качестве входных данных скрытого слоя, а два других «интегрировать» в качестве входных данных.

Каковы размеры каждой переменной, которые мы выводим из первой формулы

Сначала вызовите внимание один раз в кодировщике, чтобы получить весata_t, его размерность[batch_size, src_len]HHРазмерность[src_len, batch_size, enc_hid_dim*2], они должны быть перемножены вместе, и должны сохранятьсяbatch_sizeэто измерение, поэтому мы должны сначалаata_tРасширьте одно измерение, затем транспонируйтеHHпорядок размеров, а затемУмножить на партию (то есть умножить матрицы в одной партии)

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]

Как упоминалось ранее, поскольку ГРУ не нужны три переменные, их нужноemb(yt)emb(y_t)иccсобрать это вместе,yty_tна самом деле находится в классе Seq2Seqdec_inputпеременная, ее размерность[batch_size], так сначалаyty_tРазверните измерение, а затем передайте WordEmbedding, чтобы он стал[batch_size, 1, emb_dim]. последняя параccиemb(yt)emb(y_t)конкат

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]

st1s_{t-1}Размерность[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)

Последняя формула требует, чтобы все три переменные были объединены вместе, а затем для получения окончательного прогноза используется полносвязная нейронная сеть. Давайте сначала проанализируем размерности этих трех переменных,emb(yt)emb(y_t)Размерность[batch_size, 1, emb_dim],ccРазмерность[batch_size, 1, enc_hid_dim],sts_tРазмерность[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