Tag: transformers

  • Pytorch implementation of paper: Effective Approaches to Attention-based Neural Machine Translation

    The paper “Effective Approaches to Attention-based Neural Machine Translation” (Luong et al., 2015) introduced what is now commonly known as Luong Attention. It differs from Bahdanau attention by being “top-down”—calculating attention after the decoder’s RNN step—and by offering different scoring functions (Dot, General, and Concat).

    1. The Global Attention Mechanism

    The core of Luong’s approach is the calculation of a variable-length context vector $c_t$ that is derived from the encoder hidden states $H$. The paper proposes three ways to calculate the alignment score between the current decoder hidden state $s_t$ and an encoder hidden state $h_s$:

    $$score(s_t, h_s) =

    \begin{cases}

    s_t^\top h_s & \text{dot} \

    s_t^\top W_a h_s & \text{general} \

    v_a^\top \tanh(W_a [s_t; h_s]) & \text{concat}

    \end{cases}$$

    Python

    import torch
    import torch.nn as nn
    import torch.nn.functional as F
    
    class LuongAttention(nn.Module):
        def __init__(self, method, hidden_dim):
            super(LuongAttention, self).__init__()
            self.method = method
            self.hidden_dim = hidden_dim
    
            if self.method == 'general':
                self.attn = nn.Linear(hidden_dim, hidden_dim)
            elif self.method == 'concat':
                self.attn = nn.Linear(hidden_dim * 2, hidden_dim)
                self.v = nn.Parameter(torch.FloatTensor(hidden_dim))
    
        def forward(self, decoder_hidden, encoder_outputs):
            # decoder_hidden: [1, batch_size, hidden_dim]
            # encoder_outputs: [src_len, batch_size, hidden_dim]
            
            src_len = encoder_outputs.size(0)
            batch_size = encoder_outputs.size(1)
    
            # Calculate alignment scores
            if self.method == 'dot':
                # dot product between decoder_hidden and encoder_outputs
                attn_energies = torch.sum(decoder_hidden * encoder_outputs, dim=2) # [src_len, batch_size]
            
            elif self.method == 'general':
                energy = self.attn(encoder_outputs) # [src_len, batch_size, hidden_dim]
                attn_energies = torch.sum(decoder_hidden * energy, dim=2)
                
            elif self.method == 'concat':
                # Concatenate decoder hidden and encoder outputs
                combined = torch.cat((decoder_hidden.expand(src_len, -1, -1), encoder_outputs), dim=2)
                energy = torch.tanh(self.attn(combined)) # [src_len, batch_size, hidden_dim]
                attn_energies = torch.sum(self.v * energy, dim=2)
    
            # Normalize scores to probabilities (softmax over src_len)
            return F.softmax(attn_energies.t(), dim=1).unsqueeze(1) # [batch_size, 1, src_len]
    
    

    2. The Attentional Decoder

    In Luong attention, the sequence is: RNN Step $\rightarrow$ Attention $\rightarrow$ Context Vector $\rightarrow$ Hidden-to-Output.

    Python

    class LuongDecoder(nn.Module):
        def __init__(self, output_dim, emb_dim, hidden_dim, dropout, attention):
            super().__init__()
            self.attention = attention
            self.embedding = nn.Embedding(output_dim, emb_dim)
            self.rnn = nn.LSTM(emb_dim, hidden_dim, dropout=dropout)
            
            # Combined layer to produce 'attentional hidden state' tilde{h}_t
            self.concat = nn.Linear(hidden_dim * 2, hidden_dim)
            self.out = nn.Linear(hidden_dim, output_dim)
            self.dropout = nn.Dropout(dropout)
    
        def forward(self, input, hidden, cell, encoder_outputs):
            # input: [batch_size]
            input = input.unsqueeze(0)
            embedded = self.dropout(self.embedding(input))
    
            # 1. Standard RNN step
            # luong uses s_t (current) to calculate attention, unlike bahdanau (s_{t-1})
            output, (hidden, cell) = self.rnn(embedded, (hidden, cell))
    
            # 2. Calculate attention weights and context vector
            a = self.attention(output, encoder_outputs) # [batch_size, 1, src_len]
            encoder_outputs = encoder_outputs.permute(1, 0, 2) # [batch_size, src_len, hidden_dim]
            
            # context vector is weighted sum of encoder outputs
            context = torch.bmm(a, encoder_outputs) # [batch_size, 1, hidden_dim]
    
            # 3. Concatenate RNN output and context vector
            output = output.squeeze(0) # [batch_size, hidden_dim]
            context = context.squeeze(1) # [batch_size, hidden_dim]
            
            # Eq. 5 from paper: h_tilde = tanh(W_c [c_t; h_t])
            concat_input = torch.cat((output, context), 1)
            concat_output = torch.tanh(self.concat(concat_input))
    
            # 4. Final prediction
            prediction = self.out(concat_output)
            
            return prediction, hidden, cell
    
    

    Key Differences from Bahdanau (2015)

    FeatureBahdanau (NMT)Luong (Effective NMT)
    RNN StructureBidirectional GRUUnidirectional LSTM (often multi-layer)
    Attention TimingBefore RNN step (uses $s_{t-1}$)After RNN step (uses $s_t$)
    Alignment ScoreConcat onlyDot, General, or Concat
    Context PathFed into next RNN stepFed into a final hidden-to-output layer

    Summary of “Local” Attention

    The paper also introduced Local Attention, which only looks at a small window of the source sentence centered around a predicted position $p_t$. This is more computationally efficient for very long sequences but is less commonly used today than the Global Attention implemented above.