In-Context Learning Emergence › Studying ICL Emergence Empirically

Michael BrenndoerferNovember 6, 20256 min read
Language AI HandbookAll chapters
Part 30 of 67Emergent Capabilities
Chapter 215In-Context Learning Emergence › Studying ICL Emergence Empirically
Reading section 6 of 10 · Studying ICL Emergence Empirically · 6 minSection 6 of 10 · 6 min

Book contents

Section 6 of 10 · 6 min read

In-Context Learning Emergence › Studying ICL Emergence EmpiricallyOne section · Sections in this chapter6 / 10 ↓
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

Researchers have developed various experimental approaches to understand ICL emergence. Let's implement some key analyses that reveal how ICL develops with scale.

In[28]:
Code
import numpy as np
import torch


def measure_icl_ability(model_name, task_examples, test_cases, device="cpu"):
    """
    Measure a model's in-context learning ability on a simple task.

    Returns accuracy improvement from adding demonstrations.
    """
    tokenizer = AutoTokenizer.from_pretrained(model_name)
    model = AutoModelForCausalLM.from_pretrained(model_name).to(device)
    model.eval()

    if tokenizer.pad_token is None:
        tokenizer.pad_token = tokenizer.eos_token

    def get_prediction(prompt, choices):
        """Get model's preferred choice."""
        choice_probs = []

        for choice in choices:
            full_text = prompt + choice
            inputs = tokenizer(full_text, return_tensors="pt").to(device)

            with torch.no_grad():
                outputs = model(**inputs)
                # Get probability of last token
                logits = outputs.logits[0, -1, :]
                choice_token_id = tokenizer.encode(
                    choice, add_special_tokens=False
                )[0]
                prob = torch.softmax(logits, dim=-1)[choice_token_id].item()
                choice_probs.append(prob)

        return choices[np.argmax(choice_probs)]

    # Format demonstrations
    demo_text = "\n".join(
        [
            f"Input: {ex['input']}\nOutput: {ex['output']}"
            for ex in task_examples
        ]
    )

    zero_shot_correct = 0
    few_shot_correct = 0

    for test in test_cases:
        choices = test["choices"]
        correct = test["correct"]

        # Zero-shot
        zero_shot_prompt = f"Input: {test['input']}\nOutput: "
        zero_shot_pred = get_prediction(zero_shot_prompt, choices)
        if zero_shot_pred == correct:
            zero_shot_correct += 1

        # Few-shot
        few_shot_prompt = demo_text + f"\nInput: {test['input']}\nOutput: "
        few_shot_pred = get_prediction(few_shot_prompt, choices)
        if few_shot_pred == correct:
            few_shot_correct += 1

    n = len(test_cases)
    return {
        "zero_shot_acc": zero_shot_correct / n,
        "few_shot_acc": few_shot_correct / n,
        "icl_gain": (few_shot_correct - zero_shot_correct) / n,
    }
In[29]:
Code
# Define a simple sentiment classification task
task_examples = [
    {"input": "This movie was fantastic", "output": "positive"},
    {"input": "I hated every minute", "output": "negative"},
    {"input": "Best experience ever", "output": "positive"},
    {"input": "Terrible waste of time", "output": "negative"},
]

test_cases = [
    {
        "input": "Absolutely wonderful",
        "choices": ["positive", "negative"],
        "correct": "positive",
    },
    {
        "input": "Disappointing result",
        "choices": ["positive", "negative"],
        "correct": "negative",
    },
    {
        "input": "I loved it",
        "choices": ["positive", "negative"],
        "correct": "positive",
    },
    {
        "input": "Would not recommend",
        "choices": ["positive", "negative"],
        "correct": "negative",
    },
]
Out[30]:
Console
Simulated ICL Emergence Study Results:
==================================================

Model Size (B) | Zero-Shot | Few-Shot | ICL Gain
--------------------------------------------------
           0.1 |      0.48 |     0.50 | +0.02
           0.3 |      0.51 |     0.55 | +0.04
           1.0 |      0.54 |     0.65 | +0.11
           3.0 |      0.58 |     0.78 | +0.20
           7.0 |      0.62 |     0.85 | +0.23

Key observation: ICL gain increases sharply between 0.3B and 3B parameters,
showing the characteristic emergence pattern.
Out[31]:
Visualization
Line plot comparing zero-shot and few-shot accuracy across model sizes.
Zero-shot vs few-shot performance across model scales showing the growing gap.
Line plot showing ICL gain with emergence zone highlighted.
ICL gain showing sharp increase between 0.3B and 3B parameters.

Probing for Internal ICL Mechanisms

Researchers have developed probing techniques to understand what happens inside models during ICL. These methods examine model internals during the forward pass to identify when and how task information is encoded.

In[32]:
Code
import numpy as np
import torch


def analyze_icl_representations(model, tokenizer, demonstrations, query):
    """
    Analyze how representations change through the layers during ICL.

    This probes whether task information accumulates in specific layers.
    """
    # Prepare input
    demo_text = "\n".join([f"Q: {d['q']}\nA: {d['a']}" for d in demonstrations])
    full_prompt = demo_text + f"\nQ: {query}\nA:"

    inputs = tokenizer(full_prompt, return_tensors="pt")

    # Get all hidden states
    with torch.no_grad():
        outputs = model(**inputs, output_hidden_states=True)

    hidden_states = (
        outputs.hidden_states
    )  # Tuple of (batch, seq_len, hidden_dim)

    # Analyze the final position (where prediction happens)
    final_position_states = [h[0, -1, :].numpy() for h in hidden_states]

    # Compute change in representation across layers
    layer_changes = []
    for i in range(1, len(final_position_states)):
        change = np.linalg.norm(
            final_position_states[i] - final_position_states[i - 1]
        )
        layer_changes.append(change)

    return {
        "final_states": final_position_states,
        "layer_changes": layer_changes,
    }
Out[33]:
Visualization
Bar plot of representational change magnitude by layer number.
Representational change across layers during ICL. Middle layers show the largest changes, suggesting task-relevant processing occurs there.
Peak processing occurs around layer 14 (of 24 total).
Maximum representational change at layer 14: 2.13

The analysis reveals that middle-to-late layers show the largest representational changes during ICL, suggesting that task-relevant processing concentrates in these layers rather than being distributed uniformly. This aligns with research showing that different layers serve different functions. Early layers handle basic pattern recognition, middle layers perform task-specific processing, and late layers prepare the final output.