Skip to main content
Milestone #07Not StartedPhase 4Intermediate → Advanced12–16 hours · 1–2 weeks12 required tasks

Character Language Model

Train an autoregressive character-level language model and generate text

PythonNumPyPyTorch
Build Progress0 / 12 tasks (0%)
0%(0/12 steps)Not Started
Project Specification & Architecture

What You Will Build

Autoregressive language modeling—predicting the probability distribution of the next token given the preceding context `P(x_t | x_{<t})`—is the foundational objective behind GPT-4, Claude, and Llama. Before scaling to subword BPE vocabularies and multi-layer Transformers, you can experience the entire lifecycle of generative language modeling at the character level (`vocab_size ≈ 65`). Inspired by Andrej Karpathy's `makemore`, you will prepare a text corpus, encode characters to integer IDs, slice `(X, Y)` context windows, build a count-based bigram baseline, train a neural character language model (embedding + MLP or RNN/GRU) using Cross-Entropy loss, evaluate validation perplexity `exp(loss)`, and generate text using multinomial temperature and Top-k sampling.

Why This Project Matters in the AI/LLM Stack

Every data formatting, loss computation, and sampling concept in this project—input/target shifting by 1 position, `nn.Embedding`, cross-entropy over vocabulary logits, perplexity, and temperature-scaled `torch.multinomial`—carries over unchanged to Project 10 (Mini GPT) and Project 11 (Pretrained Mini LLM).

Learning Objectives

  • Build a bidirectional character vocabulary (`stoi` and `itos`) and construct shifted `(context, target)` training tensors.
  • Implement a count-based or lookup bigram baseline to establish a concrete validation loss benchmark.
  • Build and train a neural character language model with `nn.Embedding` that beats the bigram baseline on held-out validation loss.
  • Implement autoregressive text generation comparing greedy decoding, temperature scaling (`τ = 0.5, 0.8, 1.2`), and Top-k sampling.

Prerequisites

  • Project 6 (PyTorch training loops, `nn.Module`, `CrossEntropyLoss`)
  • Phase 4 Module 4.1 & 4.3 (Tokenization, Embeddings, and Autoregressive Language Modeling)

Required Mathematical Concepts

  • Chain rule of probability for sequences: P(x₁,…,x_T) = ∏_{t=1}^T P(x_t | x₁,…,x_{t-1})
  • Negative Log-Likelihood (Cross-Entropy) loss: L = -(1/T) ∑_t log P_θ(x_t | x_{<t})
  • Perplexity: PPL = exp(L) (effective branching factor per character)
  • Temperature-scaled Softmax: P_i = exp(z_i / τ) / ∑_j exp(z_j / τ)
Interactive Engineering Checklist

Step-by-Step Implementation Tasks (12 Steps)

Check off each engineering task as your code passes its acceptance criteria. Progress is saved automatically in your browser.

Stage 1 of 30/4 tasks complete

Corpus Preparation, Vocabulary & Sequence Slicing

Load a text dataset, build `stoi`/`itos` mappings, and construct shifted input-target sequences.

Core

A cohesive public-domain text file (such as TinyShakespeare ~1MB or a list of 32,000 names) has clear orthographic and structural patterns that a small character model can learn quickly.

Implementation: Load a clean UTF-8 text corpus (`data/input.txt`), inspect its total character length, and split it into 90% training and 10% validation text.
Expected Outcome: Contiguous `train_text` and `val_text` splits.
Acceptance Check: Corpus is split before creating sliding windows so adjacent overlapping windows do not leak across train and val.
Core

Every unique character in the corpus gets a deterministic integer index `0 .. vocab_size - 1`.

Implementation: Extract `chars = sorted(list(set(text)))`, build `stoi = {ch: i for i, ch in enumerate(chars)}` and `itos = {i: ch for i, ch in enumerate(chars)}`, and record `vocab_size = len(chars)`.
Expected Outcome: Deterministic `stoi` and `itos` dictionaries covering all characters in the corpus.
Acceptance Check: `len(stoi) == len(itos) == vocab_size`.
Core

Neural networks ingest integer token ID tensors (`torch.long`), not Python strings.

Implementation: Implement `encode(s: str) -> list[int]` and `decode(ids: list[int]) -> str` and convert `train_text` and `val_text` into 1D `torch.long` tensors.
Expected Outcome: `decode(encode('hello')) == 'hello'` and `train_data` is a `torch.int64` tensor.
Acceptance Check: Round-trip `decode(encode(s))` is 100% lossless for any string composed of vocabulary characters.
Core

In next-token prediction with context length `block_size`, a single chunk of `block_size + 1` tokens provides `block_size` supervised training examples simultaneously.

Implementation: Implement `get_batch(data, block_size, batch_size)` (or a sliding-window builder) where `x = data[i : i + block_size]` and `y = data[i + 1 : i + block_size + 1]` (or `y = data[i + block_size]` for fixed-window MLP).
Expected Outcome: Batch tensors `X` and `Y` where every target `Y` is the immediate next character after the context in `X`.
Acceptance Check: Unit test verifies that for every position `t`, `Y` equals the character immediately following `X` in the source corpus.
Stage 2 of 30/3 tasks complete

Bigram Baseline & Neural Language Model Architecture

Implement a simple character-level baseline, then build a neural language model with learned embeddings.

Core

A bigram model predicts the next character using only the single current character `P(x_t | x_{t-1})`. Measuring its validation loss gives you a concrete floor that your neural model must beat.

Implementation: Implement a `BigramBaseline` (either via a smoothed `vocab_size × vocab_size` count matrix or a single `nn.Embedding(vocab_size, vocab_size)` lookup table) and record its validation Cross-Entropy loss and perplexity.
Expected Outcome: Baseline validation loss recorded (e.g., `~2.45–2.50` nats on TinyShakespeare/names, far better than uniform random `-ln(1/65) ≈ 4.17`).
Acceptance Check: Also verifies that an untrained uniform model has loss `≈ ln(vocab_size)`.
Intermediate

Conditioning on `block_size > 1` previous characters via learned dense embeddings (`nn.Embedding(vocab_size, d_emb)`) and hidden non-linear layers allows the model to learn syllable structure, words, and punctuation.

Implementation: Implement `CharNeuralLM(nn.Module)` using `nn.Embedding(vocab_size, d_emb)` followed by either a concatenated multi-layer perceptron (`Linear -> Tanh/GELU -> Linear`) or a multi-layer `GRU`/`LSTM` projecting to `vocab_size` logits.
Expected Outcome: Forward pass accepts `(B, T)` token IDs and outputs next-character logits.
Acceptance Check: Initial output layer weights are scaled small so initial loss starts near `-ln(1/vocab_size)` rather than a high hockey-stick spike.
Core

Minimizing Cross-Entropy over the vocabulary logits maximizes the log-likelihood assigned to the true next character.

Implementation: Compute `F.cross_entropy(logits.view(-1, vocab_size), targets.view(-1))` and verify that initial loss at step 0 is close to `math.log(vocab_size)`.
Expected Outcome: Scalar Cross-Entropy loss ready for `.backward()`.
Acceptance Check: Correctly reshapes 3D `(B, T, V)` logits and 2D `(B, T)` targets when training across all sequence positions.
Stage 3 of 30/5 tasks complete

Training, Validation Perplexity & Autoregressive Generation

Train the model, track validation loss/perplexity, and implement autoregressive sampling strategies.

Core

Training with `AdamW` over several thousand mini-batches progressively lowers training and validation cross-entropy.

Implementation: Train `CharNeuralLM` using `torch.optim.AdamW` for 3,000–5,000 steps, periodically evaluating average loss over 100 training and validation batches.
Expected Outcome: Smooth decrease in training and validation loss clearly outperforming the bigram baseline.
Acceptance Check: Validation loss is strictly lower than the Task 5 bigram baseline.
Core

Perplexity `PPL = exp(cross_entropy_loss)` measures how many characters the model is effectively choosing among at each step.

Implementation: Compute and log both validation Cross-Entropy loss (in nats) and validation Perplexity `math.exp(val_loss)` for the uniform baseline, bigram baseline, and trained neural LM.
Expected Outcome: Quantitative comparison table of Loss and Perplexity across all 3 models.
Acceptance Check: Evaluated inside `with torch.no_grad():` and `model.eval()`.
Intermediate

Autoregressive generation feeds a context sequence into the model, takes the logits for the final step, converts them to probabilities via Softmax, samples 1 character ID, appends it to the sequence, and repeats.

Implementation: Implement `generate(model, prompt_ids, max_new_tokens, temperature=1.0, top_k=None)` that autoregressively samples `max_new_tokens` characters and decodes them to a string.
Expected Outcome: Model generates coherent word-like structures, line breaks, and punctuation matching the corpus style.
Acceptance Check: Crops the running context to the last `block_size` tokens at each step if using a fixed context window.
Core

Temperature `τ → 0` (greedy) often gets stuck in repetitive loops, `τ = 0.8` with `top_k = 10` balances coherence and diversity, and `τ = 1.5` produces chaotic spelling.

Implementation: Generate 200-character samples using greedy decoding, `temperature=0.5`, `temperature=0.8` (`top_k=10`), and `temperature=1.4` from the same seed prompt and compare them side-by-side.
Expected Outcome: Side-by-side generated samples illustrating the entropy-coherence tradeoff.
Acceptance Check: Includes deterministic greedy mode as well as temperature/top-k sampling.
Core

Character-level models must spend sequence length and model capacity spelling individual words letter-by-letter, resulting in short effective semantic context.

Implementation: Document your perplexity results, generated samples, and why subword tokenization (Project 8: BPE) and self-attention (Projects 9–10) are needed to scale further.
Expected Outcome: Comprehensive README with perplexity benchmarks and qualitative generation samples.
Acceptance Check: Explains the memory/compute tradeoff between character vocabularies (`V ≈ 65`, long `T`) and BPE vocabularies (`V ≈ 32k`, short `T`).
Reference Architecture & Verification Code

Starter & Verification Code Examples

Autoregressive Sampling Loop with Temperature & Top-k Filtering

Shows how to sample tokens one step at a time with temperature scaling and optional Top-k logit masking.

import torch
import torch.nn.functional as F

@torch.no_grad()
def generate_characters(
    model: torch.nn.Module,
    idx: torch.Tensor,
    max_new_tokens: int,
    block_size: int,
    temperature: float = 1.0,
    top_k: int | None = None,
) -> torch.Tensor:
    model.eval()
    for _ in range(max_new_tokens):
        idx_cond = idx[:, -block_size:]
        logits = model(idx_cond)          # Shape: (B, T, V) or (B, V)
        if logits.ndim == 3:
            logits = logits[:, -1, :]     # Take final time-step: (B, V)
        logits = logits / max(temperature, 1e-5)

        if top_k is not None:
            v, _ = torch.topk(logits, min(top_k, logits.size(-1)))
            logits[logits < v[:, [-1]]] = -float("Inf")

        probs = F.softmax(logits, dim=-1)
        idx_next = torch.multinomial(probs, num_samples=1)
        idx = torch.cat((idx, idx_next), dim=1)
    return idx
Verification & Acceptance Suite

How to Test Your Implementation

Verify lossless encode/decode round-trips, target sequence alignment, initial uniform loss check, and generation output lengths.

$ pytest tests/test_char_lm.py -vTest Runner

Shifted Context-Target Alignment Test

Ensure `y[b, t]` is always the exact next token `x[b, t + 1]` in the raw stream.

Steps: Construct a batch `(x, y)` from `[10, 20, 30, 40, 50]` with `block_size=4`.

Pass Criteria: `x` is `[10, 20, 30, 40]` and `y` is `[20, 30, 40, 50]`.

Autoregressive Generation Length & Temperature Test

Confirm `generate()` appends exactly `max_new_tokens` valid token IDs without index errors.

Steps: Pass a 3-character prompt with `max_new_tokens=25`, `temperature=0.8`, `top_k=5`.

Pass Criteria: Output tensor has length `28` and decodes to a valid 28-character string.

Overall Project Acceptance Criteria

  • Bidirectional character vocabulary and shifted `(X, Y)` sequence slicing implemented and tested.
  • Bigram baseline validation loss and perplexity recorded.
  • Neural character language model trained to achieve lower validation perplexity than the bigram baseline.
  • Autoregressive text generation implemented with greedy, temperature, and Top-k sampling comparisons.
Progressive Spoiler-Free Guidance

Expandable Hints & Debugging Tips

Try implementing each stage on your own first. Expand a hint below if you want to verify tensor shapes, mathematical identities, or common edge cases.

Optional Bonus Extensions

Optional Stretch Challenges

Optional · Does Not Block Completion

2D Character Embedding Visualization (t-SNE / PCA)

Project the learned `vocab_size × d_emb` character embedding matrix down to 2D and plot where vowels, consonants, digits, and punctuation cluster.

What You Learn: How distributional similarity emerges automatically in learned embedding tables.

WaveNet-Style Hierarchical Dilated Convolutions

Fuse pairs of adjacent characters hierarchically across layers (following Karpathy's `makemore` Part 3/5) to expand context length to `16` or `32` characters.

What You Learn: How hierarchical sequence composition bridges simple MLPs and full self-attention.

Expected Deliverable

What Your Completed Repository Should Contain

A functioning character-level language model.

Deliverable Artifacts

  • Character vocabulary encoder/decoder and shifted batch loader
  • Bigram baseline and `CharNeuralLM` PyTorch implementation
  • Validation perplexity comparison and multi-temperature generation showcase

After Completing This Project, You Should Be Able To...

  • Formulate text generation as autoregressive next-token probability estimation trained with Cross-Entropy loss.
  • Construct shifted `(context, target)` sequences and evaluate language models using validation Perplexity.
  • Build and train an embedding-based neural language model that outperforms a count-based bigram baseline.
  • Implement autoregressive decoding with greedy search, temperature scaling, and Top-k sampling.
Portfolio & Repository Tracking

GitHub Repository Submission

Link your public or private GitHub repository URL to keep a structured portfolio of your from-scratch AI builds. Stored locally in your browser (self-reported; no GitHub tokens or remote code execution).

Status: Not Linked

Suggested Repository: character-language-model-pytorch

Recommended Directory Layout:

char_lm/dataset.py
char_lm/model.py
char_lm/generate.py
tests/test_char_lm.py
README.md

Recommended README Documentation Checklist

  • Loss & Perplexity comparison table (Uniform vs Bigram vs Neural LM)
  • Generated text samples across temperatures τ = 0.5, 0.8, 1.2
  • Analysis of character-level context limitations
Local Engineering Notebook

Personal Implementation Notes