Phase 17 / Self-supervised pretraining: masked and contrastive learning
Masked language modeling: predict hidden tokens
Revision v0.7.0 · Updated since your last visit. Review the changes; your progress is saved.
Lesson · about 105 minutes · CPU practice
Separate the clean targets from the corrupted input. Normalize over selected target positions.
Phase study guide: preparation, milestones and project evidence
Before you start
Before this: Transformers & language models
Hardware: CPU
Setup help: Python, notebook, or Cloud TPU VM
Use the tested CPU packages (the recorded environment uses Python 3.14.3). Other Python versions and accelerator environments require separate checks. The optional Cloud TPU specialization explains the target-specific execution boundaries. Logical CPU devices practice partitioning; they do not simulate TPU performance.
Download the repository ZIP or use your existing checkout. Extract it and open a terminal in the folder containing requirements-cpu.txt. The beginner ZIP is separate; these lessons use the full repository folder.
python3 -m venv .venv
source .venv/bin/activate
python -m pip install -r requirements-cpu.txtpy -3 -m venv .venv
.\.venv\Scripts\python.exe -m pip install -r requirements-cpu.txtAfter activation, run the complete worked example from that same folder:
python phases/17-pretraining/01-mlm/code/main.py.\.venv\Scripts\python.exe phases/17-pretraining/01-mlm/code/main.pyFor an incremental build, create main.py in your own practice folder, append each build block in order, and run python main.py with this environment active. Keep that file separate from the downloaded reference solution.
Download this lesson’s notebook. Open it with a Python kernel using the course environment. Run cells from the top after restarting the kernel; experiments depend on earlier definitions.
For local Jupyter, first install the course requirements using the Local Python steps, then use your environment’s Python to start Jupyter:
python -m pip install jupyterlab
python -m jupyter lab.\.venv\Scripts\python.exe -m pip install jupyterlab
.\.venv\Scripts\python.exe -m jupyter labSelect the downloaded pretraining-01.ipynb file in JupyterLab. The notebook contains reference solutions; make your prediction and attempt each practice task before opening them. Restart the kernel before changing logical device setup.
The worked reference for this lesson is CPU-validated. Some references explicitly configure logical CPU devices; setting JAX_PLATFORMS does not adapt them to a TPU. Follow the execution matrix and reviewed multi-host example before attempting a target run.
For accelerator preparation, study TPU setup, observed runtime receipts, checkpointed jobs and precision and retained traces. These target paths remain unqualified on physical TPU hardware until an actual receipt is recorded. Cloud provisioning is optional for CPU learning.
Check: Loss starts near 1.099 and ends below 0.02; an unseen mask position remains below 0.2.
Revisit the guided CPU setup · Open 19: TPU Setup. Package instructions are CPU instructions; Windows and accelerator environments need their own validation.
What you will be able to do
- Separate the clean targets from the corrupted input
- Normalize over selected target positions
- Verify the changed-condition exercise and explain the limits of this fixture.
The problem
A text model can see a sentence but one word is hidden. What prevents it from copying the answer, and which positions should contribute to its loss? We will train a small bidirectional attention model and audit those two boundaries.
The idea
Masked language modeling removes selected token information from the model's input and asks it to predict the original tokens. The clean targets remain available to the loss, while the encoder receives only the corrupted sequence.
Hide the input without hiding the teaching target
Draw a clean sequence splitting into two paths. One supplies labels; the other is corrupted before entering the encoder. Selected-position logits meet the clean labels only at the loss. An arrow from a hidden clean target directly into encoder input would leak the answer.
Bidirectional context means an unmasked token on either side can help prediction. It is separate from the loss-selection mask, which decides which outputs are scored. The current fixture masks selected positions; it is not an implementation of every corruption choice used in BERT.
The loss curve shows learning on the small repeated-token fixture. The probability panel shows which target receives mass at the selected positions. A low fixture loss is separate from robust embeddings or language understanding; changed masks and held-out data test different capabilities.
Keep clean targets out of encoder inputs
Predict: If all visible tokens reveal the same class, what limit does that place on the experiment?
Clean tokens fork into labels and a corrupted-input path. Only corrupted inputs enter the encoder. Selected logits meet clean targets at the loss. This makes supervision available without leaking the hidden answer directly into the input.
Before coding: follow one hidden token
You should be able to turn logits into probabilities and differentiate a scalar loss. Begin with the first clean sequence, . Selecting its second position produces input , while the target remains . The mask ID is an input symbol, not a fourth output class. The other visible zeros supply evidence about the missing token. Write down these three objects separately: clean targets, corrupted inputs and the Boolean selection mask. If you cannot say which one enters the encoder, stop before training.
The fixture repeats each token so you can inspect information flow without needing a tokenizer or a large corpus. This simplicity is also its main limitation: a model can solve repetition without learning syntax, word order or meaning.
Separate the clean targets from the corrupted input
Keep an immutable copy of the original token IDs. The selection mask identifies positions to predict; corruption changes the input tokens, not the targets. Here the extra token ID means hidden, while output classes are . Passing clean targets into the encoder would leak the answer. Check the corrupted arrays before the first update.
Normalize over selected target positions
Compute log-softmax over vocabulary, gather the log probability of each original target, then average only selected positions. For uniform predictions over three classes, the initial loss is , regardless of how many valid positions are selected. Reject an empty mask before entering the jitted step; silently dividing by zero creates an undefined objective.
Trace the axes and the gradient
The input IDs have shape . Looking them up in an embedding table with shape produces . Attention scores have shape : the last two axes are query and key positions, not vocabulary classes. The head maps the contextual vectors to logits with shape . Softmax over the final axis now means vocabulary probability. Mixing these two softmax axes gives plausible shapes with the wrong meaning.
Let be the selected-token count, the vocabulary probability and the original target ID. The derivative below is zero at an unselected output position. It does not say the visible input embeddings have zero gradient: they help predict selected positions through attention.
Work out unequal masks before batching
Imagine one sequence contributes one selected token with loss , and another contributes three selected tokens with losses . The selected-token mean is . Averaging the two sequence means gives . Both are well-defined objectives, but they assign different influence to the sequences. Accumulating microbatch losses requires sums and selected counts if you intend to reproduce the token-weighted objective.
Predict which answer appears before running the unequal-mask experiment. Then perturb only unselected target IDs. A correctly reduced objective stays fixed. Next perturb one selected target; the loss should respond unless the competing classes happen to have equal probability.
Use both sides without using the hidden value
The encoder forms attention scores across every input position. There is no causal triangle: a later visible token is useful evidence here. This tiny model has one attention head, no positional encoding and a linear vocabulary head. Repeated token sequences deliberately avoid word-order ambiguity; a realistic encoder also needs positions, padding handling, residual blocks and a tokenizer.
Distinguish selection from replacement policy
This lab always replaces selected tokens with the mask ID so information flow is easy to inspect. BERT-style training can mix mask replacement, random replacement and unchanged selected tokens; selection still determines the supervised positions. Keep special/padding tokens ineligible. Track a masking key and regenerate masks under a stated schedule rather than accidentally fixing one easy corruption forever.
Test a different corruption before claiming progress
The training curve uses one selected position. The code evaluates a second position without additional updates and checks that changing target labels changes the loss. This supports the bounded repeated-token task, not language understanding. A real corpus needs document-separated splits, a frozen tokenizer, mask provenance and downstream evaluation against random initialization.
Move from this fixture to a corpus deliberately
Keep the objective checker when replacing repeated IDs with tokenized documents. Split by document/source, freeze tokenizer and special-token IDs, exclude padding from selection, and keep an explicit random key for corruption. A full encoder also needs positional information and a tested padding mask. Do not infer these capabilities from this one-head model.
Evaluate a fixed held-out corpus with a documented mask schedule and an aggregate numerator/count. Compare random initialization and a trained checkpoint on the same downstream task. Save one failure case showing a missing dependency, not just an average loss. Moving a mask inside the same three sequences tests corruption transfer; it is not a held-out-language evaluation.
Masked language modeling: predict hidden tokens — recorded experiment
Predict before looking: Which cell should dominate each row of final masked-token probabilities, and what would a bright wrong cell tell you?
Read the figure
The horizontal axis is the number of updates already applied; the vertical axis is selected-token cross-entropy in nats. The first value is about . It falls to about on the fixed synthetic sequences. This is training loss, not masked-token accuracy on natural text.
The second panel shows final class probabilities at the hidden position: rows are the three examples and columns are vocabulary classes. Read each row as a distribution that sums to one. The largest cell should lie at that row’s target class. This panel uses the parameters after the last update; the loss curve ends just before that update. Confident diagonal predictions establish this repetition task, not general language understanding.
Connect it to the computation
The zero vocabulary head makes initial predictions uniform. Falling loss shows the trained attention/embedding/head system can use the remaining repeated tokens. A held mask position is checked separately; the plot does not show a held-out corpus or establish transfer quality.
Open full-size figureDownload data and provenance
The notebook contains the data experiment, plotting code, and recorded figure output. Rerun it to test your changes.
Where to put & run this code: Work in phases/17-pretraining/01-mlm/code/main.py (or downloaded exercises/pretraining-01.py / exercises/pretraining-01.ipynb) and execute from the repository root with python3 phases/17-pretraining/01-mlm/code/main.py.
1 Define corruption and the selected-token objective
Create main.py in your activated course environment. Paste this block, then run python main.py; function definitions alone print nothing.
# Step 1 — 1. Define corruption and the selected-token objective: The functions separate the input boundary from the differentiable...
import jax
import jax.numpy as jnp
import numpy as np
def masked_ce(logits, targets, selected):
# Caller validates a nonempty mask before a transformed training step.
logp = jax.nn.log_softmax(logits, axis=-1)
nll = -jnp.take_along_axis(logp, targets[..., None], axis=-1)[..., 0]
return jnp.sum(jnp.where(selected, nll, 0.0)) / jnp.sum(selected)
def validate_mask(selected, shape):
# Convert `a` to a host NumPy array for inspection or verification.
a = np.asarray(selected)
# Guard input contract (`a.shape != shape or a.dtype != np.bool_ or (not a.any())`) and fail fast if violated.
if a.shape != shape or a.dtype != np.bool_ or not a.any():
raise ValueError('a nonempty Boolean mask of the target shape is required')
def corrupt_tokens(tokens, selected, mask_id):
validate_mask(selected, tokens.shape)
return jnp.where(selected, mask_id, tokens)
def mlm_logits(p, corrupted):
h = p['embedding'][corrupted]
# Bidirectional single-head attention, deliberately no causal mask.
scores = h @ jnp.swapaxes(h, -1, -2) / jnp.sqrt(h.shape[-1])
# Perform matrix / vector contraction (`@`) to compute `context`.
context = jax.nn.softmax(scores, axis=-1) @ h
return context @ p['head']Code building blocks in this step
x.sum(axis=...) / x.mean(axis=..., keepdims=...)— Reduces values along the namedaxis(the axis you name is collapsed unlesskeepdims=True).
The functions separate the input boundary from the differentiable objective. Read masked_ce from vocabulary normalization to target gather to selected-count reduction.
2 Construct a batch whose answers you can inspect
Append this block to main.py and run python main.py again. Keep the earlier blocks above it.
# Step 2 — 2. Construct a batch whose answers you can inspect: Before continuing, inspect corrupted: each row must contain mask...
tokens = jnp.array([[0, 0, 0, 0], [1, 1, 1, 1], [2, 2, 2, 2]], jnp.int32)
selected = jnp.array([[False, True, False, False]] * 3)
corrupted = corrupt_tokens(tokens, selected, 3)
# Create or split explicit PRNG key(s) (`key`) for reproducible randomness.
key = jax.random.key(7)
# Sample deterministic random values into `p` using an explicit PRNG key.
p = {
'embedding': jax.random.normal(key, (4, 6)) * 0.2,
'head': jnp.zeros((6, 3)),
}
loss = lambda p: masked_ce(mlm_logits(p, corrupted), tokens, selected)
# Differentiate the objective to obtain `step` via automatic differentiation.
step = jax.jit(jax.value_and_grad(loss))
history = []Code building blocks in this step
jnp.array(values, dtype=...)— Constructs an immutable device-backed JAX array from Python/NumPy values.jnp.zeros / jnp.ones(shape, dtype=...)— Allocates a tensor of the givenshapeinitialized with constants.jax.value_and_grad(loss_fn)(params, ...)— Evaluates both the scalar loss and its gradient PyTree(loss_val, grads)in a single forward+backward pass.jax.jit(fn) / @jax.jit— Tracesfnwith abstract shapes and compiles a fused XLA executable cached by input shape and dtype.
Before continuing, inspect corrupted: each row must contain mask ID at the second position. The zero head makes the initial vocabulary distribution uniform.
3 Train, then change the corruption
Append this block to main.py and run python main.py again. Keep the earlier blocks above it.
# Step 3 — 3. Train, then change the corruption: The loop measures loss before each update.
for _ in range(100):
value, g = step(p)
history.append(float(value))
# Transform every leaf of the parameter PyTree (`p`).
p = jax.tree.map(lambda a, b: a - 0.4 * b, p, g)
assert history[-1] < history[0] * 0.15
np.testing.assert_allclose(history[0], np.log(3), atol=1e-6)
# Changing the clean target after constructing corrupted input cannot change the forward pass.
changed_targets = tokens.at[:, 1].set((tokens[:, 1] + 1) % 3)
assert not np.isclose(
float(masked_ce(mlm_logits(p, corrupted), changed_targets, selected)),
history[-1],
)
held_mask = jnp.array([[False, False, True, False]] * 3)
held_loss = float(
masked_ce(mlm_logits(p, corrupt_tokens(tokens, held_mask, 3)), tokens, held_mask)
)
assert held_loss < 0.2
print('MLM initial/final/changed-mask:', history[0], history[-1], held_loss)Code building blocks in this step
jnp.array(values, dtype=...)— Constructs an immutable device-backed JAX array from Python/NumPy values.jnp.allclose(actual, expected, rtol=..., atol=...)— Checks that two arrays match elementwise within floating-point tolerance.jax.tree.map(lambda p, g: ..., params, grads)— Applies a function leaf-by-leaf across matching PyTrees (such as updating every parameter tensor with its gradient).
The loop measures loss before each update. The final held-mask test uses the updated parameters; do not compare it as if it were the final recorded pre-update point.
View the cumulative build in main.py
These blocks append in order to the same file.
# Step 1 — 1. Define corruption and the selected-token objective: The functions separate the input boundary from the differentiable...
import jax
import jax.numpy as jnp
import numpy as np
def masked_ce(logits, targets, selected):
# Caller validates a nonempty mask before a transformed training step.
logp = jax.nn.log_softmax(logits, axis=-1)
nll = -jnp.take_along_axis(logp, targets[..., None], axis=-1)[..., 0]
return jnp.sum(jnp.where(selected, nll, 0.0)) / jnp.sum(selected)
def validate_mask(selected, shape):
# Convert `a` to a host NumPy array for inspection or verification.
a = np.asarray(selected)
# Guard input contract (`a.shape != shape or a.dtype != np.bool_ or (not a.any())`) and fail fast if violated.
if a.shape != shape or a.dtype != np.bool_ or not a.any():
raise ValueError('a nonempty Boolean mask of the target shape is required')
def corrupt_tokens(tokens, selected, mask_id):
validate_mask(selected, tokens.shape)
return jnp.where(selected, mask_id, tokens)
def mlm_logits(p, corrupted):
h = p['embedding'][corrupted]
# Bidirectional single-head attention, deliberately no causal mask.
scores = h @ jnp.swapaxes(h, -1, -2) / jnp.sqrt(h.shape[-1])
# Perform matrix / vector contraction (`@`) to compute `context`.
context = jax.nn.softmax(scores, axis=-1) @ h
return context @ p['head']
# Step 2 — 2. Construct a batch whose answers you can inspect: Before continuing, inspect corrupted: each row must contain mask...
tokens = jnp.array([[0, 0, 0, 0], [1, 1, 1, 1], [2, 2, 2, 2]], jnp.int32)
selected = jnp.array([[False, True, False, False]] * 3)
corrupted = corrupt_tokens(tokens, selected, 3)
# Create or split explicit PRNG key(s) (`key`) for reproducible randomness.
key = jax.random.key(7)
# Sample deterministic random values into `p` using an explicit PRNG key.
p = {
'embedding': jax.random.normal(key, (4, 6)) * 0.2,
'head': jnp.zeros((6, 3)),
}
loss = lambda p: masked_ce(mlm_logits(p, corrupted), tokens, selected)
# Differentiate the objective to obtain `step` via automatic differentiation.
step = jax.jit(jax.value_and_grad(loss))
history = []
# Step 3 — 3. Train, then change the corruption: The loop measures loss before each update.
for _ in range(100):
value, g = step(p)
history.append(float(value))
# Transform every leaf of the parameter PyTree (`p`).
p = jax.tree.map(lambda a, b: a - 0.4 * b, p, g)
assert history[-1] < history[0] * 0.15
np.testing.assert_allclose(history[0], np.log(3), atol=1e-6)
# Changing the clean target after constructing corrupted input cannot change the forward pass.
changed_targets = tokens.at[:, 1].set((tokens[:, 1] + 1) % 3)
assert not np.isclose(
float(masked_ce(mlm_logits(p, corrupted), changed_targets, selected)),
history[-1],
)
held_mask = jnp.array([[False, False, True, False]] * 3)
held_loss = float(
masked_ce(mlm_logits(p, corrupt_tokens(tokens, held_mask, 3)), tokens, held_mask)
)
assert held_loss < 0.2
print('MLM initial/final/changed-mask:', history[0], history[-1], held_loss)Run the example
python3 phases/17-pretraining/01-mlm/code/main.pyAbout this command
This shortcut uses an activated macOS/Linux course environment in the repository folder. For Windows, use the explicit interpreter command in “Setup help” above. The script includes worked examples and reference solutions.
Inspect the complete assembled example
# Step 1 — 1. Define corruption and the selected-token objective: The functions separate the input boundary from the differentiable...
import jax
import jax.numpy as jnp
import numpy as np
def masked_ce(logits, targets, selected):
# Caller validates a nonempty mask before a transformed training step.
logp = jax.nn.log_softmax(logits, axis=-1)
nll = -jnp.take_along_axis(logp, targets[..., None], axis=-1)[..., 0]
return jnp.sum(jnp.where(selected, nll, 0.0)) / jnp.sum(selected)
def validate_mask(selected, shape):
# Convert `a` to a host NumPy array for inspection or verification.
a = np.asarray(selected)
# Guard input contract (`a.shape != shape or a.dtype != np.bool_ or (not a.any())`) and fail fast if violated.
if a.shape != shape or a.dtype != np.bool_ or not a.any():
raise ValueError('a nonempty Boolean mask of the target shape is required')
def corrupt_tokens(tokens, selected, mask_id):
validate_mask(selected, tokens.shape)
return jnp.where(selected, mask_id, tokens)
def mlm_logits(p, corrupted):
h = p['embedding'][corrupted]
# Bidirectional single-head attention, deliberately no causal mask.
scores = h @ jnp.swapaxes(h, -1, -2) / jnp.sqrt(h.shape[-1])
# Perform matrix / vector contraction (`@`) to compute `context`.
context = jax.nn.softmax(scores, axis=-1) @ h
return context @ p['head']
# Step 2 — 2. Construct a batch whose answers you can inspect: Before continuing, inspect corrupted: each row must contain mask...
tokens = jnp.array([[0, 0, 0, 0], [1, 1, 1, 1], [2, 2, 2, 2]], jnp.int32)
selected = jnp.array([[False, True, False, False]] * 3)
corrupted = corrupt_tokens(tokens, selected, 3)
# Create or split explicit PRNG key(s) (`key`) for reproducible randomness.
key = jax.random.key(7)
# Sample deterministic random values into `p` using an explicit PRNG key.
p = {
'embedding': jax.random.normal(key, (4, 6)) * 0.2,
'head': jnp.zeros((6, 3)),
}
loss = lambda p: masked_ce(mlm_logits(p, corrupted), tokens, selected)
# Differentiate the objective to obtain `step` via automatic differentiation.
step = jax.jit(jax.value_and_grad(loss))
history = []
# Step 3 — 3. Train, then change the corruption: The loop measures loss before each update.
for _ in range(100):
value, g = step(p)
history.append(float(value))
# Transform every leaf of the parameter PyTree (`p`).
p = jax.tree.map(lambda a, b: a - 0.4 * b, p, g)
assert history[-1] < history[0] * 0.15
np.testing.assert_allclose(history[0], np.log(3), atol=1e-6)
# Changing the clean target after constructing corrupted input cannot change the forward pass.
changed_targets = tokens.at[:, 1].set((tokens[:, 1] + 1) % 3)
assert not np.isclose(
float(masked_ce(mlm_logits(p, corrupted), changed_targets, selected)),
history[-1],
)
held_mask = jnp.array([[False, False, True, False]] * 3)
held_loss = float(
masked_ce(mlm_logits(p, corrupt_tokens(tokens, held_mask, 3)), tokens, held_mask)
)
assert held_loss < 0.2
print('MLM initial/final/changed-mask:', history[0], history[-1], held_loss)Expected result: Loss starts near 1.099 and ends below 0.02; an unseen mask position remains below 0.2.
Recorded reference execution
Reference output captured from executing the complete lesson script (code/main.py) and its assertions in the course CPU environment.
MLM initial/final/changed-mask: 1.0986123085021973 0.010633035562932491 0.010457751341164112
MLM initial/final/changed-mask: 1.0986123085021973 0.010633035562932491 0.010457751341164112
Unselected output logits have zero direct loss gradient.
Token-weighted / sequence-weighted: 0.6577723026275635 0.51289606
Changed position loss: 0.010457751341164112
Empty mask rejected
Independent masked softmax gradient agrees.
PASS: pretraining-01
Download execution record · The notebook includes executed cells and figure outputs. Compare your run with these values; timings depend on the machine.
Show that unsupervised positions have no direct loss gradient
# Experiment — Show that unsupervised positions have no direct loss gradient: Visible input tokens can still influence selected predictions...
logits = mlm_logits(p, corrupted)
changed_logits = jnp.where(
selected[..., None], logits, logits + jnp.array([100.0, -50.0, 7.0])
)
# Execute `np.testing.assert_allclose(`
np.testing.assert_allclose(
masked_ce(changed_logits, tokens, selected),
masked_ce(logits, tokens, selected),
atol=1e-6,
)
# Differentiate the objective to obtain `grad_logits` via automatic differentiation.
grad_logits = jax.grad(masked_ce)(logits, tokens, selected)
# Convert `` to a host NumPy array for inspection or verification.
np.testing.assert_array_equal(np.asarray(grad_logits)[~np.asarray(selected)], 0.0)
print('Unselected output logits have zero direct loss gradient.')Expected: Unselected logits do not affect the masked loss and have zero direct gradient.
Visible input tokens can still influence selected predictions through attention. Zero direct gradient at an unselected output is not a claim that visible-token embeddings never learn. The perturbation changes class differences; adding one common scalar to every class would leave softmax unchanged even at supervised positions and would be a weak masking check.
Use unequal selection counts and a host probability calculation
# Experiment — Use unequal selection counts and a host probability calculation: The second sequence supplies three of four selected tokens.
audit_probs = jnp.array([
[[0.8, 0.1, 0.1], [0.2, 0.7, 0.1], [0.2, 0.3, 0.5]],
[[0.1, 0.6, 0.3], [0.5, 0.2, 0.3], [0.3, 0.4, 0.3]],
])
audit_targets = jnp.array([[0, 1, 2], [1, 0, 2]])
audit_mask = jnp.array([[True, False, False], [True, True, True]])
audit_logits = jnp.log(audit_probs)
host_losses = -np.log(
np.asarray(audit_probs)[
np.arange(2)[:, None], np.arange(3)[None, :], np.asarray(audit_targets)
]
)
# Aggregate array values to compute `expected`.
expected = host_losses[np.asarray(audit_mask)].mean()
observed = float(masked_ce(audit_logits, audit_targets, audit_mask))
np.testing.assert_allclose(observed, expected, atol=1e-6)
# Aggregate array values to compute `sequence_mean`.
sequence_mean = np.mean(
[host_losses[i][np.asarray(audit_mask[i])].mean() for i in range(2)]
)
assert not np.isclose(observed, sequence_mean)
print('Token-weighted / sequence-weighted:', observed, sequence_mean)Expected: The independent selected-token mean agrees; the sequence-weighted mean differs.
The second sequence supplies three of four selected tokens. Equal sequence weighting gives the first sequence half of the influence instead of one quarter.
Make it yours
Where to put your code: Append your solution at the bottom of phases/17-pretraining/01-mlm/code/main.py (or inside the Make it yours cell of exercises/pretraining-01.ipynb) and run python3 phases/17-pretraining/01-mlm/code/main.py to verify your prediction before opening the reference solution.
Select the first position instead of the second and evaluate the trained model without updating it.
Show a writing guide and starter scaffold
How to write this exercise code: step-by-step recipe & starter scaffold
1. Key functions & syntax to use:
jnp.array(values, dtype=...)— Constructs an immutable device-backed JAX array from Python/NumPy values.
2. Step-by-step implementation plan:
- Set up the changed input or configuration parameters requested in the prompt.
- Run the target function or transformation and assert that the output matches your prediction.
# Exercise solution: Select the first position instead of the second and evaluate the...
first_mask = jnp.array(...) # TODO: compute first_mask
first_loss = float(...) # TODO: compute first_loss
masked_ce(mlm_logits(p, corrupt_tokens(tokens, first_mask, 3)), tokens, first_mask)
)
assert first_loss # TODO: complete assertion check
print('Changed position loss:', first_loss)
Reveal the reference solution
# Exercise solution: Select the first position instead of the second and evaluate the...
first_mask = jnp.array([[True, False, False, False]] * 3)
first_loss = float(
masked_ce(mlm_logits(p, corrupt_tokens(tokens, first_mask, 3)), tokens, first_mask)
)
assert first_loss < 0.2
print('Changed position loss:', first_loss)Reject a missing learning signal
Transfer
Pass an empty selection mask through the public corruption boundary and require a clear failure.
Show a writing guide and starter scaffold
How to write this practice solution: step-by-step recipe & starter scaffold
1. Key functions & syntax to use:
signal(...)— Callsignalwith your updated parameters or inputs from this lesson's workspace.corrupt_tokens(...)— Callcorrupt_tokenswith your updated parameters or inputs from this lesson's workspace.
2. Step-by-step implementation plan:
- Run the boundary check and catch the expected exception:
# Reject a missing learning signal (Transfer): A skipped batch requires an explicit training policy.
# Run the boundary check and catch the expected exception:
try:
corrupt_tokens(tokens, jnp.zeros_like(tokens, dtype = ... # TODO: compute corrupt_tokens(tokens, jnp.zeros_like(tokens, dtype
except ValueError:
print('Empty mask rejected')
else:
raise AssertionError('empty mask accepted')
Show a hint
Validate outside the transformed objective before dividing by a count.
Reveal reference solution and reasoning
# Reject a missing learning signal (Transfer): A skipped batch requires an explicit training policy.
# Run the boundary check and catch the expected exception:
try:
corrupt_tokens(tokens, jnp.zeros_like(tokens, dtype=bool), 3)
except ValueError:
print('Empty mask rejected')
else:
raise AssertionError('empty mask accepted')A skipped batch requires an explicit training policy. It must not silently become a zero loss or a NaN update.
Derive the output gradient without autodiff
Challenge
Implement the probability-minus-one-hot formula for the unequal-mask fixture and compare every element with autodiff. Explain where visible context can still receive gradient.
Show a writing guide and starter scaffold
How to write this practice solution: step-by-step recipe & starter scaffold
1. Key functions & syntax to use:
x.sum(axis=...) / x.mean(axis=..., keepdims=...)— Reduces values along the namedaxis(the axis you name is collapsed unlesskeepdims=True).jnp.allclose(actual, expected, rtol=..., atol=...)— Checks that two arrays match elementwise within floating-point tolerance.jax.grad(loss_fn)(params, ...)— Transforms a scalar-output function into a function returning the gradient PyTree with the same structure asparams.
2. Step-by-step implementation plan:
- Reduce along axis=-1 to compute
analytic. - Differentiate the objective to obtain gradients
.
# Derive the output gradient without autodiff (Challenge): This verifies the entire derivative tensor, including zeros...
# Reduce along axis=-1 to compute `analytic`.
analytic = ... # TODO: compute analytic
(jax.nn.softmax(audit_logits, axis=-1) - jax.nn.one_hot(audit_targets, 3))
* audit_mask[..., None]
/ audit_mask.sum()
)
# Differentiate the objective to obtain gradients ``.
np.testing.assert_allclose(
jax.grad(masked_ce)(audit_logits, audit_targets, audit_mask),
analytic,
atol = ... # TODO: compute atol
)
print('Independent masked softmax gradient agrees.')
Show a hint
Broadcast the mask over vocabulary and divide by the selected count, not the batch size.
Reveal reference solution and reasoning
# Derive the output gradient without autodiff (Challenge): This verifies the entire derivative tensor, including zeros...
# Reduce along axis=-1 to compute `analytic`.
analytic = (
(jax.nn.softmax(audit_logits, axis=-1) - jax.nn.one_hot(audit_targets, 3))
* audit_mask[..., None]
/ audit_mask.sum()
)
# Differentiate the objective to obtain gradients ``.
np.testing.assert_allclose(
jax.grad(masked_ce)(audit_logits, audit_targets, audit_mask),
analytic,
atol=1e-6,
)
print('Independent masked softmax gradient agrees.')This verifies the entire derivative tensor, including zeros at unsupervised outputs. It says nothing by itself about intermediate embedding gradients.
Diagnose the result
If loss is suspiciously perfect at initialization, inspect target leakage and the selection count. If loss becomes NaN, reject empty masks and inspect vocabulary indices before changing the optimizer.
Carry forward
- Keep the input-corruption boundary separate from the loss mask: corrupted inputs with shape enter the encoder, while clean targets meet vocabulary logits of shape only inside the selected-token cross-entropy.
- Normalize masked loss by the count of selected tokens , reject empty masks before entering a jitted step, and carry numerator and selected-token counts explicitly when accumulating across unequal batches.
Keep your evidence
Keep corrupted inputs, selected-target counts, the log-vocabulary baseline, independent masked-loss checks, trained parameters and changed-mask results.
Save your modified code, its output, and an explanation of your prediction. Checkpoint answers and exercise evidence are tracked separately.
Saved in this browser. Export or back up your portfolio anytime in My learning.
Verified on local CPU. Stage to a Cloud TPU VM via Cloud TPU setup to record accelerator execution receipts (TODO: add automated Cloud TPU CI execution receipts).