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

Written by

in

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.

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *