Thomas Piacentini
← Workshop

The workshop · From text to Transformer

Build an LLM.

A small model, trained here. Six experiments to understand its computations, then build in Python.

01 / 06

Prepare the text

The model receives numbers. Give each character an ID, then check that you can recover the text.

100–20,000 tokens · 2–100 distinct characters · local computation

Text → IDs

IDs → text

Understand this step

A token is a unit of text. Here, one normalized Unicode character = one token. The book later uses BPE, where a token can contain multiple characters. Vocabulary is built from the whole corpus; only the first 80% is used for updates.

chars = sorted(set(text))
stoi = {c: i for i, c in enumerate(chars)}
ids = [stoi[c] for c in text]
assert ''.join(chars[i] for i in ids) == text
Raschka · chapter 2 ↗

02 / 06

Create a target

To learn, the model predicts the next character at every position. Move the window, then complete its last target.

Indices in the training split. ␠ = space; ↵ = line break.

Which character completes the last target?
Understand this step

Context is the maximum number of tokens read at once. Targets are the same sequence shifted by one token. Training and validation are split before making windows: no window crosses the boundary. Validation measures prediction on held-out text, not understanding.

train, validation = ids[:split], ids[split:]
x = train[start:start + context]
y = train[start + 1:start + context + 1]
assert x[1:] == y[:-1]
Raschka · chapter 2 ↗

03 / 06

Mask the future

A position can read its past, never its future. Choose a position and turn the mask off to observe the leak.

Position reading the context

Fixed Q/K, six-position example. Turn the mask off, then back on to check this step.

Understand this step

Queries Q and keys K produce similarity scores. Future scores are masked before softmax, which converts scores into weights summing to 1. Values V are then combined with these weights. This example uses fixed Q/K to isolate the mask; the model learns its Q/K/V.

scores = q @ k.transpose(-2, -1) / sqrt(head_dim)
scores = scores.masked_fill(future, float('-inf'))
weights = scores.softmax(dim=-1)
context = weights @ v
Raschka · chapter 3 ↗

04 / 06

Assemble the model

Connect the parts, then test the dimensions and mask on the actual model. Its weights start at random.

1 block · 2 heads · float32
  1. Tokens
  2. Embeddings
  3. Attention + MLP
  4. Logits
What the test checks

We change the second half of a real window. Logits for the first half must remain unchanged: no future information may leak. Parameter count and output shape are also checked. The numerical engine loads only at this step.

Understand this step

Embeddings map each token and its position to vectors. Two attention heads read different relationships. An MLP transforms the result; residual connections and LayerNorm stabilize computation. Logits are scores, one per possible token. We use one block and no dropout to keep the model small and reproducible.

x = token_embedding(ids) + position_embedding(positions)
x = x + attention(norm1(x))
x = x + mlp(norm2(x))
logits = output(final_norm(x))
Raschka · chapter 4 ↗

05 / 06

Learn and measure

Run 80 updates. The model corrects predictions on training text; validation text is held out for measurement.

0 / 400 maximum steps

Measurements will appear when you start.

Read the measurements
Mean loss on 8 fixed windows per split
StepTrainValidation

Computation runs in a worker to keep the page usable. You can stop between updates. This experiment is limited to 400 steps; continue in Python to go further.

Understand this step

Loss is the mean prediction error (cross entropy, in nats). Lower loss means better prediction of these targets. Gradients indicate how to change weights; Adam updates them at rate 0.003 with gradient norm clipped to 1. Measurements always use the same 8 windows per split. Falling training loss with rising validation loss indicates overfitting. The best checkpoint has the lowest measured validation loss.

loss = cross_entropy(logits.flatten(0, 1), targets.flatten())
optimizer.zero_grad()
loss.backward()
clip_grad_norm_(model.parameters(), 1.0)
optimizer.step()
Raschka · chapter 5 ↗

06 / 06

Produce a token

Give the trained model a prompt. Inspect probabilities, sample one token, then reuse the output as context.

Your text will appear here.

Understand this step

Temperature divides logits before softmax. Lower values concentrate the distribution; higher values spread it. Sampling uses the full vocabulary, without top-k. Displayed probabilities describe the last generation decision. The same seed and weights produce the same sequence. A few lines of text produce a demonstration model, not an assistant or reasoning capability.

logits = model(ids[:, -context:])[:, -1, :]
probabilities = (logits / temperature).softmax(-1)
next_id = multinomial(probabilities, 1)
ids = cat((ids, next_id), dim=1)
Raschka · chapter 5 ↗

Your next workbench

Build in Python.

Take your corpus, architecture and weights with you. The notebook guides you through implementing and testing the parts.

  1. Keep this experiment

    The JSON project restores the model in this page. Weights and measurements are included; the optimizer’s internal state starts fresh on resume.

  2. Code with tests

    Python 3.10+ and PyTorch 2.x. The notebook has six short exercises, tests and solutions to open when needed. Your weights can be loaded without retraining.

    Download notebook

    Without a built model: example corpus and random weights.

    Set up and troubleshoot
    python -m venv .venv
    # macOS / Linux
    source .venv/bin/activate
    # Windows
    # .venv\Scripts\activate
    python -m pip install torch jupyterlab
    python -m jupyter lab
    ModuleNotFoundError: torch
    Install torch in the environment of the kernel selected in Jupyter.
    GPU out of memory
    Set DEVICE = "cpu" in the configuration; keep d=16 and context=8.
    Rising validation loss
    Restore the best checkpoint. Add data before increasing model size.

Clear local project

Clears text, weights and progress on this device. Export the JSON to keep them.

After the small Transformer

Explore reasoning.

The second book and course start with pretrained Qwen3. Our miniature model illustrates pretraining; it does not replace that starting point. First measure accuracy on held-out problems.

1. Verify and vote · no weight updates

Handwritten example: 12 nautical miles in 3 hours. The verifier expects 4 knots; a majority vote can be wrong. Edit the answers (one per line).

Voting compares strings; the verifier accepts only finite decimals. No reasoning model is executed here.

Chapter 4 · self-consistency ↗
2. Reward · update weights with GRPO

Compare rewards within a group. A reward above the mean gives a positive advantage. If all rewards are equal, this signal is zero.

Rewards → normalized advantages

This calculation isolates advantages; it is not a full GRPO loop (loss, clipping and KL remain to be implemented).

Chapter 6 · GRPO ↗
3. Distill · learn from selected answers

A teacher model produces answers. After filtering, a student trains on these examples. Evaluate baseline, voting, GRPO and distillation on the same problems: accuracy, tokens and time. Chapter 7 of the first book introduces instruction tuning; chapter 8 of the reasoning repository develops distillation.

Chapter 8 · distillation ↗

Original explanations, exercises and code. PDFs are not redistributed. Checks record local experiments; they do not certify Python code or general competence.