Copy Mechanism › The Pointer-Generator Network

Michael BrenndoerferUpdated May 19, 20257 min read
Language AI HandbookAll chapters
Part 12 of 67Sequence-to-Sequence
Chapter 89Copy Mechanism › The Pointer-Generator Network
Reading section 6 of 14 · The Pointer-Generator Network · 7 minSection 6 of 14 · 7 min

Book contents

Section 6 of 14 · 7 min read

Copy Mechanism › The Pointer-Generator NetworkOne section · Sections in this chapter6 / 14 ↓
Reading tools Definitions & inline links

Choose your expertise level to adjust how many terms are explained. Beginners see more tooltips, experts see fewer to maintain reading flow. Hover over underlined terms for instant definitions.

Article links

Make inline references clickable

The pointer-generator network, introduced by See et al. (2017) for abstractive summarization, combines the ideas above into a cohesive architecture. It extends the standard attention-based seq2seq model with a copy mechanism, enabling it to both generate words from the vocabulary and copy words from the source document.

The 2017 paper by Abigail See, Peter Liu, and Christopher Manning was a landmark in neural summarization research. Their model was evaluated on the CNN/DailyMail dataset, a standard benchmark where news articles are summarized using bullet-point highlights. The pointer-generator model substantially outperformed prior neural approaches on ROUGE metrics, and more importantly, it produced summaries with dramatically fewer factual errors because it could copy names, numbers, and entities directly from the source. The paper also introduced the coverage mechanism discussed later in this chapter, addressing the separate problem of repetitive output. Together, these two contributions made the pointer-generator the go-to architecture for neural summarization for several years.

The architecture is elegant precisely because it adds so little to the baseline. The encoder is a standard bidirectional LSTM that processes the source document. The decoder is a standard LSTM with attention. The entire copy mechanism adds only two components: the copy switch (a small linear layer plus sigmoid) and the distribution combination step. The extended vocabulary and OOV tracking add some bookkeeping complexity, but no additional trainable parameters beyond those in the switch. This efficiency was a significant practical advantage when the alternative was large, complex architectures that were difficult to train.

Out[14]:
Visualization
Diagram showing encoder-decoder architecture with copy mechanism, including attention, copy switch, and combined output distribution.
Architecture of the pointer-generator network. The encoder processes the source document, and the decoder generates the summary. At each step, the copy switch determines whether to generate from the vocabulary or copy from the source. The final distribution combines both possibilities, with attention weights serving as copy probabilities.

Let's implement a complete pointer-generator decoder step:

In[15]:
Code
class PointerGeneratorDecoder(nn.Module):
    """
    Single decoding step of a pointer-generator network.
    """

    def __init__(self, vocab_size, embed_dim, hidden_dim):
        super().__init__()
        self.vocab_size = vocab_size
        self.hidden_dim = hidden_dim

        # Embedding and LSTM
        self.embedding = nn.Embedding(vocab_size, embed_dim)
        self.lstm = nn.LSTMCell(embed_dim + hidden_dim, hidden_dim)

        # Attention
        self.attention = PointerAttention(hidden_dim)

        # Copy switch
        self.copy_switch = CopySwitch(hidden_dim, embed_dim)

        # Vocabulary projection
        self.vocab_proj = nn.Linear(hidden_dim * 2, vocab_size)

    def forward(
        self,
        input_token,
        prev_hidden,
        prev_cell,
        encoder_outputs,
        source_ids,
        encoder_mask=None,
    ):
        """
        Perform one decoding step.

        Args:
            input_token: (batch,) - previous output token
            prev_hidden: (batch, hidden) - previous hidden state
            prev_cell: (batch, hidden) - previous cell state
            encoder_outputs: (batch, src_len, hidden) - encoder outputs
            source_ids: (batch, src_len) - source token IDs
            encoder_mask: (batch, src_len) - valid source positions

        Returns:
            final_dist: (batch, extended_vocab) - output distribution
            hidden: (batch, hidden) - new hidden state
            cell: (batch, hidden) - new cell state
            attn_weights: (batch, src_len) - attention weights
        """
        batch_size = input_token.shape[0]

        # Embed input
        embedded = self.embedding(input_token)  # (batch, embed)

        # Compute attention over encoder outputs
        attn_weights = self.attention(
            prev_hidden, encoder_outputs, encoder_mask
        )

        # Context vector
        context = torch.bmm(attn_weights.unsqueeze(1), encoder_outputs).squeeze(
            1
        )

        # LSTM input: concatenate embedding and context
        lstm_input = torch.cat([embedded, context], dim=-1)

        # LSTM step
        hidden, cell = self.lstm(lstm_input, (prev_hidden, prev_cell))

        # Vocabulary distribution
        vocab_input = torch.cat([hidden, context], dim=-1)
        vocab_logits = self.vocab_proj(vocab_input)
        vocab_dist = F.softmax(vocab_logits, dim=-1)

        # Copy probability
        p_gen = self.copy_switch(context, hidden, embedded)

        # Combine distributions
        final_dist = self._combine_distributions(
            p_gen, vocab_dist, attn_weights, source_ids
        )

        return final_dist, hidden, cell, attn_weights

    def _combine_distributions(
        self, p_gen, vocab_dist, attn_weights, source_ids
    ):
        """Combine generation and copy distributions."""
        batch_size, src_len = source_ids.shape

        # For simplicity, assume no OOV (would need extended vocab otherwise)
        final_dist = p_gen * vocab_dist

        # Add copy probabilities
        p_copy = 1 - p_gen

        # Use scatter_add for efficiency
        copy_dist = torch.zeros_like(vocab_dist)
        copy_dist.scatter_add_(1, source_ids, attn_weights * p_copy)

        final_dist = final_dist + copy_dist

        return final_dist


# Test the decoder
vocab_size = 1000
embed_dim = 64
hidden_dim = 128
batch_size = 2
src_len = 10

decoder = PointerGeneratorDecoder(vocab_size, embed_dim, hidden_dim)

# Dummy inputs
input_token = torch.randint(0, vocab_size, (batch_size,))
prev_hidden = torch.zeros(batch_size, hidden_dim)
prev_cell = torch.zeros(batch_size, hidden_dim)
encoder_outputs = torch.randn(batch_size, src_len, hidden_dim)
source_ids = torch.randint(0, vocab_size, (batch_size, src_len))

final_dist, hidden, cell, attn = decoder(
    input_token, prev_hidden, prev_cell, encoder_outputs, source_ids
)
Out[16]:
Console
Pointer-Generator Decoder Output
==================================================

Output distribution shape: torch.Size([2, 1000])
Hidden state shape: torch.Size([2, 128])
Attention weights shape: torch.Size([2, 10])

Top 5 predicted tokens (batch 0):
  Token 361: 0.0683
  Token 101: 0.0669
  Token 621: 0.0636
  Token 86: 0.0592
  Token 645: 0.0568

Distribution sum: 1.0000

The output distribution sums to 1.0, confirming it's a valid probability distribution. The decoder produces probabilities over all tokens in the vocabulary, and the top predictions show which tokens are most likely at this step. In practice, these probabilities would be used either for greedy decoding (selecting the argmax) or for beam search (maintaining multiple hypotheses).

The implementation above uses scatter_add_ for the copy distribution, which is an efficient tensor operation that accumulates copy probabilities into the right vocabulary positions in a single pass. This is important for performance: the naive loop over source positions that the compute_final_distribution function earlier used works for illustration purposes but would be far too slow for real-world documents with hundreds or thousands of tokens. The scatter operation achieves the same result in vectorized form, making it suitable for batched training.