Memory Augmentation › Memory Network Fundamentals

Michael BrenndoerferUpdated July 8, 20258 min read
Language AI HandbookAll chapters
Part 18 of 67Long Context
Chapter 131Memory Augmentation › Memory Network Fundamentals
Reading section 3 of 12 · Memory Network Fundamentals · 8 minSection 3 of 12 · 8 min

Book contents

Section 3 of 12 · 8 min read

Memory Augmentation › Memory Network FundamentalsOne section · Sections in this chapter3 / 12 ↓
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

Imagine you're reading a 500-page novel and someone asks you, "What color was the protagonist's childhood home?" You don't re-read the entire book. Instead, your mind somehow retrieves that specific detail from storage. Memory networks attempt to give neural networks this same capability: the ability to write information to an external store during processing and retrieve it on demand.

The Key Insight: Separate Storage from Computation

The fundamental innovation of memory networks is decoupling what the model remembers from how it processes information. In a standard neural network, "memory" exists implicitly in the hidden states, and information must survive through layer after layer of transformation. In a memory network, we provide an explicit storage mechanism that the model can query independently.

This separation immediately suggests two operations:

  1. Writing: The model encounters important information and stores it
  2. Reading: The model needs information and retrieves it from storage

But how should storage be organized? And how does the model know where to look when reading?

Content-Based Addressing: Finding Information by What It Contains

Consider how you might organize a library. You could assign each book a numbered shelf (positional addressing), or you could organize books by topic and keywords (content-based addressing). A positional system requires remembering exactly where you put each book. A content-based system lets you find books by describing what you're looking for.

Memory networks use content-based addressing. Each memory entry consists of two parts:

  • A key ki\mathbf{k}_i: describes what the entry contains (like a book's title and keywords)
  • A value vi\mathbf{v}_i: the actual stored content (like the book itself)

When the model wants to retrieve information, it provides a query q\mathbf{q} describing what it's looking for. The memory system compares this query against all stored keys and returns the values whose keys match best.

The Mathematics of Matching: Similarity and Softmax

How do we measure whether a query matches a key? The most natural choice is the dot product. Given a query vector q\mathbf{q} and a key vector ki\mathbf{k}_i, their similarity is:

si=q⋅ki=∑j=1dqj⋅kijs_i = \mathbf{q} \cdot \mathbf{k}_i = \sum_{j=1}^{d} q_j \cdot k_{ij}

where:

  • q∈Rd\mathbf{q} \in \mathbb{R}^d is the query vector with dimension dd
  • ki∈Rd\mathbf{k}_i \in \mathbb{R}^d is the key for memory slot ii
  • sis_i is the scalar similarity score

This score tells us how well the query matches each key. Larger positive values indicate better matches. But we need to convert these raw scores into a probability distribution, a set of weights that sum to 1, telling us how much to trust each memory entry.

The softmax function accomplishes this. For nn memory slots with similarity scores s1,…,sns_1, \ldots, s_n:

αi=esi∑j=1nesj\alpha_i = \frac{e^{s_i}}{\sum_{j=1}^{n} e^{s_j}}

The exponential esie^{s_i} ensures all weights are positive (since ex>0e^x > 0 for all xx). The denominator normalizes so that ∑iαi=1\sum_i \alpha_i = 1. The weight αi\alpha_i can be interpreted as "the probability that memory slot ii contains what we're looking for."

Scaling for Stable Gradients

In practice, we add one refinement. When the dimension dd is large, dot products can become very large in magnitude. This pushes softmax outputs toward extreme values (nearly 0 or nearly 1), which causes gradient problems during training. The solution is scaled dot-product attention:

si=q⋅kids_i = \frac{\mathbf{q} \cdot \mathbf{k}_i}{\sqrt{d}}

Why d\sqrt{d}? If each component of q\mathbf{q} and k\mathbf{k} has variance 1, then the dot product has variance dd (since we're summing dd independent products). Dividing by d\sqrt{d} restores unit variance, keeping the scores in a well-behaved range regardless of dimension.

Retrieving the Answer: Weighted Combination

Once we have attention weights αi\alpha_i over all memory slots, we retrieve information by taking a weighted sum of the stored values:

r=∑i=1nαi⋅vi\mathbf{r} = \sum_{i=1}^{n} \alpha_i \cdot \mathbf{v}_i

where:

  • r∈Rdv\mathbf{r} \in \mathbb{R}^{d_v} is the retrieved vector (output)
  • vi∈Rdv\mathbf{v}_i \in \mathbb{R}^{d_v} is the value stored in memory slot ii
  • αi\alpha_i is the attention weight computed above

This weighted sum is fully differentiable, meaning gradients can flow back through the retrieval operation during training. The model learns which keys to associate with which values by receiving gradient signals about whether the retrieved information helped with the task.

Putting It Together: The Complete Memory Read

The full memory read operation combines all these steps:

r=MemoryRead(q,K,V)=∑i=1nsoftmax(qKTd)i⋅vi\mathbf{r} = \text{MemoryRead}(\mathbf{q}, \mathbf{K}, \mathbf{V}) = \sum_{i=1}^{n} \text{softmax}\left(\frac{\mathbf{q} \mathbf{K}^T}{\sqrt{d}}\right)_i \cdot \mathbf{v}_i

where:

  • K∈Rn×d\mathbf{K} \in \mathbb{R}^{n \times d} is the matrix of all nn keys stacked as rows
  • V∈Rn×dv\mathbf{V} \in \mathbb{R}^{n \times d_v} is the matrix of all nn values stacked as rows
  • qKT\mathbf{q} \mathbf{K}^T computes all nn similarity scores in parallel

This is identical to the attention mechanism used in transformers. The difference is conceptual: in standard self-attention, keys and values come from the input sequence; in a memory network, they come from an external storage that persists across inputs.

Implementation: A Basic Memory Network

Let's translate these mathematical concepts into working code. The implementation directly mirrors the formulas above:

In[6]:
Code
import torch
import torch.nn as nn
import torch.nn.functional as F


class BasicMemoryNetwork(nn.Module):
    """
    A basic memory network demonstrating core memory operations.

    The memory stores key-value pairs. Reading uses content-based
    attention over keys to retrieve values. Writing adds new entries.
    """

    def __init__(self, memory_size, key_dim, value_dim):
        super().__init__()
        self.memory_size = memory_size
        self.key_dim = key_dim
        self.value_dim = value_dim

        # Initialize memory as learnable parameters
        # K ∈ R^(n × d): matrix of n keys, each with dimension d
        self.memory_keys = nn.Parameter(torch.randn(memory_size, key_dim) * 0.1)
        # V ∈ R^(n × d_v): matrix of n values, each with dimension d_v
        self.memory_values = nn.Parameter(
            torch.randn(memory_size, value_dim) * 0.1
        )

        # Query projection: transforms raw query into key space
        self.query_proj = nn.Linear(key_dim, key_dim)

    def read(self, query):
        """
        Read from memory using content-based attention.

        This implements: r = Σ_i softmax(q·K^T / √d)_i · v_i

        Args:
            query: (batch, key_dim) - what we're looking for

        Returns:
            retrieved: (batch, value_dim) - weighted sum of memory values
            attention: (batch, memory_size) - attention weights α over memory
        """
        # Project query into key space
        query = self.query_proj(query)

        # Step 1: Compute similarity scores s_i = q · k_i for all keys
        # Matrix form: scores = q @ K^T gives all n scores at once
        scores = torch.matmul(query, self.memory_keys.T)  # (batch, n)

        # Step 2: Scale by √d for numerical stability
        scores = scores / np.sqrt(self.key_dim)

        # Step 3: Convert to attention weights α_i via softmax
        attention = F.softmax(scores, dim=-1)  # (batch, n), sums to 1

        # Step 4: Retrieve weighted sum r = Σ_i α_i · v_i
        retrieved = torch.matmul(attention, self.memory_values)  # (batch, d_v)

        return retrieved, attention

    def forward(self, query):
        return self.read(query)
In[7]:
Code
# Demonstrate basic memory operations
memory_net = BasicMemoryNetwork(memory_size=64, key_dim=128, value_dim=256)

# Create a batch of queries
batch_size = 4
queries = torch.randn(batch_size, 128)

# Read from memory
retrieved_values, attention_weights = memory_net(queries)
Out[8]:
Console
Basic Memory Network Demonstration:
  Memory size: 64 slots
  Key dimension: 128
  Value dimension: 256

Query batch shape: torch.Size([4, 128])
Retrieved values shape: torch.Size([4, 256])
Attention weights shape: torch.Size([4, 64])

Attention statistics (first query):
  Top-5 slots: [57, 21, 46, 60, 7]
  Top-5 weights: ['0.019', '0.018', '0.017', '0.017', '0.017']
  Sum of top-5: 0.089

The output reveals how attention weights distribute across memory slots. Even with random initialization (where keys and queries have no learned relationship), the softmax produces a sparse pattern. The top 5 slots capture a clear fraction of total attention mass. This shows the inherent focusing behavior of the exponential function. After training, these weights would reflect content similarity, because the model would learn to store related information under similar keys.

Out[9]:
Visualization
Heatmap with 4 rows (queries) and 64 columns (memory slots), showing sparse attention patterns with each query attending to different slots.
Attention weight heatmap for 4 queries over 64 memory slots. Each row shows one query's attention distribution. Darker cells indicate higher attention weights. Note how each query focuses on different memory locations. This shows content-based addressing.