Phase 05 / Neural network training
Train a CNN on a small dataset
Revision v0.7.0 · Updated since your last visit. Review the changes; your progress is saved.
Lesson · about 110 minutes · CPU practice
Train a classifier with a fixed data split and report held-out metrics.
Phase study guide: preparation, milestones and project evidence
Before you start
Before this: A compiled train and evaluation step
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/05-networks/04-train-a-cnn-on-a-small-dataset/code/main.py.\.venv\Scripts\python.exe phases/05-networks/04-train-a-cnn-on-a-small-dataset/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 networks-04.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: Held-out accuracy is at least for the fixed fixture; confusion counts total . The runnable script prints the actual measured loss and counts.
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
- Generate images with an explicit label rule
- Derive the convolution output shape
- Pool spatial responses and retain class scores
- Read a confusion matrix rather than a success label
The problem
Can a small model tell horizontal bars from vertical bars in noisy images? We’ll create the images locally and train a convolutional classifier on CPU. You’ll then test images from a different random seed and read a confusion matrix. The task is small enough to inspect without downloading a dataset.
The idea
A convolution combines a local input patch with weights reused across spatial positions. Layout, stride and padding decide which pixels contribute to each output. Trace one position first; the full feature map repeats that same rule.
Trace one convolution output
In a separate shape example, a three-by-three kernel sliding over a five-by-five image with stride one and no padding has three valid positions along each axis. It produces a three-by-three output per filter.
For one output location, multiply the selected patch by the kernel and sum over spatial and input-channel axes, then add the filter bias. Moving to another location changes the patch while reusing the filter. A new padding or stride convention changes the indexing contract.
The confusion matrix answers a later question about classification errors. Pick one off-diagonal cell, identify its true/predicted labels, and inspect an image from that cell. Correct synthetic-shape classification is separate from recognition of natural photographs.
Pixels behind one convolution output
Predict: What stays shared when the kernel moves?
The purple square selects one three-by-three patch within a five-by-five input. Each selected cell contributes to one output under the declared convolution. Moving the patch reuses weights. This separate shape example has three valid positions per axis at stride one without padding; it does not display learned weights.
Generate images with an explicit label rule
Our images have height eight, width eight, and one channel. Class zero contains a horizontal bright bar; class one contains a vertical bright bar. The bar position varies among interior rows or columns, and independent Gaussian noise perturbs every pixel. Alternate labels so both splits are balanced. Generate training and held-out sets from different seeds; identical seeds would reproduce images and create leakage. Every observation follows a known rule, so inspect a few images and labels before training. This synthetic distribution contains no occlusion, perspective, or multiple objects and should never be described as realistic vision evaluation.
Derive the convolution output shape
NNX Conv accepts channels-last arrays: batch, height, width, channels. A filter with stride one and VALID padding slides only where the whole patch fits. An image therefore yields spatial outputs. Four output channels mean four independently learned filters; the kernel shape is , followed by four biases. Each output is a patch dot product plus a bias. Deep-learning convolution uses cross-correlation here: do not reverse the filter in the independent NumPy reference. We check one interior output explicitly before fitting, which catches axis or padding misunderstandings.
(B,8,8,1) → Conv3×3/VALID → (B,6,6,4) → ReLU → spatial mean → (B,4) → Linear → (B,2)Pool spatial responses and retain class scores
ReLU replaces negative filter responses with zero. Averaging axes one and two summarizes how strongly each learned pattern appears anywhere in the remaining image. Pooling discards exact position, which suits orientation classification but would hurt a task asking for bar location. The classifier head emits two logits per image. softmax_cross_entropy_with_integer_labels consumes those scores and integer labels directly; do not apply softmax beforehand. A stable independent NumPy reference subtracts the largest score before computing log-sum-exp. The objective is averaged over observations, not over classes.
Read a confusion matrix rather than a success label
Use rows for true classes and columns for predicted classes. The diagonal counts correct classifications; off-diagonal cells identify orientation-specific mistakes. A balanced 24-image held-out set contains examples per class, so a model always predicting horizontal has accuracy . After training, print both held-out mean loss and accuracy, along with all four confusion counts. Check that the matrix sums to and that its trace divided by equals accuracy. This independent accounting prevents a reduction-axis error from looking like good performance. Keep the split and hyperparameters fixed; a new data distribution is a new experiment, not another chance to select the reported test result.
See the image task and its classification errors
Predict before looking: Which axis of the confusion matrix is the actual label?
Read the figure
The upper panel is one held-out image, with pixel columns across and rows down. Its dark horizontal stripe is near row ; the faint variation elsewhere is added noise. This color bar measures pixel value, so the picture is an input to the classifier.
The lower panel summarizes all held-out predictions. Rows are actual classes and columns are predicted classes. The two diagonal cells contain each; both off-diagonal cells contain zero. The lower color bar measures example count, which is a different quantity from pixel intensity.
Connect it to the computation
The upper-left confusion cell counts horizontal images classified as horizontal; the lower-right counts vertical images classified as vertical. Off-diagonal counts would identify the two kinds of confusion. Here the held-out images are all classified correctly.
Use the sample image to understand the task and the confusion matrix to understand the observed errors. The single image is distinct from accuracy, and a zero in the matrix means no observed mistakes of that kind in this set, not that such a mistake is impossible. This is a small synthetic stripe task, not evidence of general image recognition.
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/05-networks/04-train-a-cnn-on-a-small-dataset/code/main.py (or downloaded exercises/networks-04.py / exercises/networks-04.ipynb) and execute from the repository root with python3 phases/05-networks/04-train-a-cnn-on-a-small-dataset/code/main.py.
1 Generate two disjoint synthetic splits
Create main.py. Print a training image as an array if you want to inspect the task before fitting.
# Step 1 — 1. Generate two disjoint synthetic splits: Two seeds produce separate examples.
from flax import nnx
import jax
import jax.numpy as jnp
import numpy as np
import optax
def bars(seed,n,noise=.08):
# Draw pseudorandom samples for `rng` using the explicit RNG state.
rng=np.random.default_rng(seed)
# Allocate initialized array `images` with the specified shape and dtype.
images=np.zeros((n,8,8,1),np.float32)
labels=np.arange(n,dtype=np.int32)%2
for i,label in enumerate(labels):
# Draw pseudorandom samples for `position` using the explicit RNG state.
position=int(rng.integers(2,6))
if label==0:images[i,position,:,0]=1.
else:images[i,:,position,0]=1.
# Accumulate the next contribution into `images`.
images+=rng.normal(0,noise,images.shape).astype(np.float32)
return jnp.array(images),jnp.array(labels)
train_x,train_y=bars(20,48)
test_x,test_y=bars(21,24)
# Check tensor shape invariant: `train_x.shape==(48,8,8,1) and test_x.shape==(24,8,8,1)`
assert train_x.shape==(48,8,8,1) and test_x.shape==(24,8,8,1)
assert not np.array_equal(np.asarray(train_x[:24]),np.asarray(test_x))Code building blocks in this step
jnp.array(values, dtype=...)— Constructs an immutable device-backed JAX array from Python/NumPy values.
Two seeds produce separate examples. Both labels are balanced by construction; noise can make orientation harder as its scale grows.
2 Build and independently inspect the convolution
Append the small CNN and the patch dot-product check before defining an optimizer.
# Step 2 — 2. Build and independently inspect the convolution: The first output at (0,0) consumes exactly the top-left 3\times3...
# Define `BarCNN` module / container with explicit state and forward pass:
class BarCNN(nnx.Module):
def __init__(self):
rngs=nnx.Rngs(0)
# Combine or mask array elements to form `self.conv`.
self.conv=nnx.Conv(1,4,(3,3),padding='VALID',rngs=rngs)
self.head=nnx.Linear(4,2,rngs=rngs)
def __call__(self,x):
# Apply nonlinear activation or probability normalization to compute `features`.
features=jnp.mean(nnx.relu(self.conv(x)),axis=(1,2))
return self.head(features)
model=BarCNN()
raw=model.conv(train_x[:1])
# Check tensor shape invariant: `raw.shape==(1,6,6,4)`
assert raw.shape==(1,6,6,4)
# Convert `patch` to a host NumPy array for inspection or verification.
patch=np.asarray(train_x[0,:3,:3,:])
# Convert `kernel` to a host NumPy array for inspection or verification.
kernel=np.asarray(model.conv.kernel[...])
# Aggregate array values to compute `expected`.
expected=np.sum(patch*kernel[:,:,:,0])+np.asarray(model.conv.bias[...])[0]
np.testing.assert_allclose(raw[0,0,0,0],expected,rtol=1e-5,atol=1e-6)
def loss(m,x,y):return jnp.mean(optax.softmax_cross_entropy_with_integer_labels(m(x),y))
# Configure or step the Optax optimizer state (`optimizer`).
optimizer=nnx.Optimizer(model,optax.adam(.03),wrt=nnx.Param)
@nnx.jit
def train_step(m,o,x,y):
# Evaluate both scalar loss and parameter gradients in one pass (`(value, grads)`).
value,grads=nnx.value_and_grad(loss)(m,x,y)
# Update state in place with the new values.
o.update(m,grads)
return valueCode 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).jnp.allclose(actual, expected, rtol=..., atol=...)— Checks that two arrays match elementwise within floating-point tolerance.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.
The first output at consumes exactly the top-left patch. The NumPy result validates filter orientation and layout.
3 Fit, then measure the held-out split once
Append full-batch updates, stable NumPy loss, and confusion accounting. Every update uses only train_x/train_y.
# Step 3 — 3. Fit, then measure the held-out split once: The output is a measured CPU result for synthetic bars.
initial=float(loss(model,train_x,train_y))
# Execute `for _ in range(80):train_step(model,optimizer,train_x,train_y)`.
for _ in range(80):train_step(model,optimizer,train_x,train_y)
# Convert `scores` to a host NumPy array for inspection or verification.
# Convert `labels` to a host NumPy array for inspection or verification.
scores=np.asarray(model(test_x))
labels=np.asarray(test_y)
predictions=scores.argmax(axis=-1)
# Reduce along axis=-1 to compute `shifted`.
shifted=scores-scores.max(axis=-1,keepdims=True)
# Reduce along axis=-1 to compute `reference_loss`.
reference_loss=np.mean(np.log(np.exp(shifted).sum(axis=-1))-shifted[np.arange(len(labels)),labels])
np.testing.assert_allclose(loss(model,test_x,test_y),reference_loss,rtol=1e-5,atol=1e-6)
# Allocate initialized array `confusion` with the specified shape and dtype.
confusion=np.zeros((2,2),dtype=int)
np.add.at(confusion,(labels,predictions),1)
# Aggregate array values to compute `accuracy`.
accuracy=np.mean(predictions==labels)
assert confusion.sum()==24 and np.isclose(np.trace(confusion)/24,accuracy)
assert accuracy>=.9 and float(loss(model,train_x,train_y))<initial*.3
print('Held-out loss:',reference_loss,'accuracy:',accuracy,'confusion:\n',confusion)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).jnp.allclose(actual, expected, rtol=..., atol=...)— Checks that two arrays match elementwise within floating-point tolerance.
The output is a measured CPU result for synthetic bars. No MNIST, TPU, or throughput claim is implied.
View the cumulative build in main.py
These blocks append in order to the same file.
# Step 1 — 1. Generate two disjoint synthetic splits: Two seeds produce separate examples.
from flax import nnx
import jax
import jax.numpy as jnp
import numpy as np
import optax
def bars(seed,n,noise=.08):
# Draw pseudorandom samples for `rng` using the explicit RNG state.
rng=np.random.default_rng(seed)
# Allocate initialized array `images` with the specified shape and dtype.
images=np.zeros((n,8,8,1),np.float32)
labels=np.arange(n,dtype=np.int32)%2
for i,label in enumerate(labels):
# Draw pseudorandom samples for `position` using the explicit RNG state.
position=int(rng.integers(2,6))
if label==0:images[i,position,:,0]=1.
else:images[i,:,position,0]=1.
# Accumulate the next contribution into `images`.
images+=rng.normal(0,noise,images.shape).astype(np.float32)
return jnp.array(images),jnp.array(labels)
train_x,train_y=bars(20,48)
test_x,test_y=bars(21,24)
# Check tensor shape invariant: `train_x.shape==(48,8,8,1) and test_x.shape==(24,8,8,1)`
assert train_x.shape==(48,8,8,1) and test_x.shape==(24,8,8,1)
assert not np.array_equal(np.asarray(train_x[:24]),np.asarray(test_x))
# Step 2 — 2. Build and independently inspect the convolution: The first output at (0,0) consumes exactly the top-left 3\times3...
# Define `BarCNN` module / container with explicit state and forward pass:
class BarCNN(nnx.Module):
def __init__(self):
rngs=nnx.Rngs(0)
# Combine or mask array elements to form `self.conv`.
self.conv=nnx.Conv(1,4,(3,3),padding='VALID',rngs=rngs)
self.head=nnx.Linear(4,2,rngs=rngs)
def __call__(self,x):
# Apply nonlinear activation or probability normalization to compute `features`.
features=jnp.mean(nnx.relu(self.conv(x)),axis=(1,2))
return self.head(features)
model=BarCNN()
raw=model.conv(train_x[:1])
# Check tensor shape invariant: `raw.shape==(1,6,6,4)`
assert raw.shape==(1,6,6,4)
# Convert `patch` to a host NumPy array for inspection or verification.
patch=np.asarray(train_x[0,:3,:3,:])
# Convert `kernel` to a host NumPy array for inspection or verification.
kernel=np.asarray(model.conv.kernel[...])
# Aggregate array values to compute `expected`.
expected=np.sum(patch*kernel[:,:,:,0])+np.asarray(model.conv.bias[...])[0]
np.testing.assert_allclose(raw[0,0,0,0],expected,rtol=1e-5,atol=1e-6)
def loss(m,x,y):return jnp.mean(optax.softmax_cross_entropy_with_integer_labels(m(x),y))
# Configure or step the Optax optimizer state (`optimizer`).
optimizer=nnx.Optimizer(model,optax.adam(.03),wrt=nnx.Param)
@nnx.jit
def train_step(m,o,x,y):
# Evaluate both scalar loss and parameter gradients in one pass (`(value, grads)`).
value,grads=nnx.value_and_grad(loss)(m,x,y)
# Update state in place with the new values.
o.update(m,grads)
return value
# Step 3 — 3. Fit, then measure the held-out split once: The output is a measured CPU result for synthetic bars.
initial=float(loss(model,train_x,train_y))
# Execute `for _ in range(80):train_step(model,optimizer,train_x,train_y)`.
for _ in range(80):train_step(model,optimizer,train_x,train_y)
# Convert `scores` to a host NumPy array for inspection or verification.
# Convert `labels` to a host NumPy array for inspection or verification.
scores=np.asarray(model(test_x))
labels=np.asarray(test_y)
predictions=scores.argmax(axis=-1)
# Reduce along axis=-1 to compute `shifted`.
shifted=scores-scores.max(axis=-1,keepdims=True)
# Reduce along axis=-1 to compute `reference_loss`.
reference_loss=np.mean(np.log(np.exp(shifted).sum(axis=-1))-shifted[np.arange(len(labels)),labels])
np.testing.assert_allclose(loss(model,test_x,test_y),reference_loss,rtol=1e-5,atol=1e-6)
# Allocate initialized array `confusion` with the specified shape and dtype.
confusion=np.zeros((2,2),dtype=int)
np.add.at(confusion,(labels,predictions),1)
# Aggregate array values to compute `accuracy`.
accuracy=np.mean(predictions==labels)
assert confusion.sum()==24 and np.isclose(np.trace(confusion)/24,accuracy)
assert accuracy>=.9 and float(loss(model,train_x,train_y))<initial*.3
print('Held-out loss:',reference_loss,'accuracy:',accuracy,'confusion:\n',confusion)Run the example
python3 phases/05-networks/04-train-a-cnn-on-a-small-dataset/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. Generate two disjoint synthetic splits: Two seeds produce separate examples.
from flax import nnx
import jax
import jax.numpy as jnp
import numpy as np
import optax
def bars(seed,n,noise=.08):
# Draw pseudorandom samples for `rng` using the explicit RNG state.
rng=np.random.default_rng(seed)
# Allocate initialized array `images` with the specified shape and dtype.
images=np.zeros((n,8,8,1),np.float32)
labels=np.arange(n,dtype=np.int32)%2
for i,label in enumerate(labels):
# Draw pseudorandom samples for `position` using the explicit RNG state.
position=int(rng.integers(2,6))
if label==0:images[i,position,:,0]=1.
else:images[i,:,position,0]=1.
# Accumulate the next contribution into `images`.
images+=rng.normal(0,noise,images.shape).astype(np.float32)
return jnp.array(images),jnp.array(labels)
train_x,train_y=bars(20,48)
test_x,test_y=bars(21,24)
# Check tensor shape invariant: `train_x.shape==(48,8,8,1) and test_x.shape==(24,8,8,1)`
assert train_x.shape==(48,8,8,1) and test_x.shape==(24,8,8,1)
assert not np.array_equal(np.asarray(train_x[:24]),np.asarray(test_x))
# Step 2 — 2. Build and independently inspect the convolution: The first output at (0,0) consumes exactly the top-left 3\times3...
# Define `BarCNN` module / container with explicit state and forward pass:
class BarCNN(nnx.Module):
def __init__(self):
rngs=nnx.Rngs(0)
# Combine or mask array elements to form `self.conv`.
self.conv=nnx.Conv(1,4,(3,3),padding='VALID',rngs=rngs)
self.head=nnx.Linear(4,2,rngs=rngs)
def __call__(self,x):
# Apply nonlinear activation or probability normalization to compute `features`.
features=jnp.mean(nnx.relu(self.conv(x)),axis=(1,2))
return self.head(features)
model=BarCNN()
raw=model.conv(train_x[:1])
# Check tensor shape invariant: `raw.shape==(1,6,6,4)`
assert raw.shape==(1,6,6,4)
# Convert `patch` to a host NumPy array for inspection or verification.
patch=np.asarray(train_x[0,:3,:3,:])
# Convert `kernel` to a host NumPy array for inspection or verification.
kernel=np.asarray(model.conv.kernel[...])
# Aggregate array values to compute `expected`.
expected=np.sum(patch*kernel[:,:,:,0])+np.asarray(model.conv.bias[...])[0]
np.testing.assert_allclose(raw[0,0,0,0],expected,rtol=1e-5,atol=1e-6)
def loss(m,x,y):return jnp.mean(optax.softmax_cross_entropy_with_integer_labels(m(x),y))
# Configure or step the Optax optimizer state (`optimizer`).
optimizer=nnx.Optimizer(model,optax.adam(.03),wrt=nnx.Param)
@nnx.jit
def train_step(m,o,x,y):
# Evaluate both scalar loss and parameter gradients in one pass (`(value, grads)`).
value,grads=nnx.value_and_grad(loss)(m,x,y)
# Update state in place with the new values.
o.update(m,grads)
return value
# Step 3 — 3. Fit, then measure the held-out split once: The output is a measured CPU result for synthetic bars.
initial=float(loss(model,train_x,train_y))
# Execute `for _ in range(80):train_step(model,optimizer,train_x,train_y)`.
for _ in range(80):train_step(model,optimizer,train_x,train_y)
# Convert `scores` to a host NumPy array for inspection or verification.
# Convert `labels` to a host NumPy array for inspection or verification.
scores=np.asarray(model(test_x))
labels=np.asarray(test_y)
predictions=scores.argmax(axis=-1)
# Reduce along axis=-1 to compute `shifted`.
shifted=scores-scores.max(axis=-1,keepdims=True)
# Reduce along axis=-1 to compute `reference_loss`.
reference_loss=np.mean(np.log(np.exp(shifted).sum(axis=-1))-shifted[np.arange(len(labels)),labels])
np.testing.assert_allclose(loss(model,test_x,test_y),reference_loss,rtol=1e-5,atol=1e-6)
# Allocate initialized array `confusion` with the specified shape and dtype.
confusion=np.zeros((2,2),dtype=int)
np.add.at(confusion,(labels,predictions),1)
# Aggregate array values to compute `accuracy`.
accuracy=np.mean(predictions==labels)
assert confusion.sum()==24 and np.isclose(np.trace(confusion)/24,accuracy)
assert accuracy>=.9 and float(loss(model,train_x,train_y))<initial*.3
print('Held-out loss:',reference_loss,'accuracy:',accuracy,'confusion:\n',confusion)Expected result: Held-out accuracy is at least for the fixed fixture; confusion counts total . The runnable script prints the actual measured loss and counts.
Recorded reference execution
Reference output captured from executing the complete lesson script (code/main.py) and its assertions in the course CPU environment.
Held-out loss: 0.015700625 accuracy: 1.0 confusion:
[[12 0]
[ 0 12]]
Held-out loss: 0.015700625 accuracy: 1.0 confusion:
[[12 0]
[ 0 12]]
True-class counts: [12 12] row-normalized confusion: [[1. 0.]
[0. 1.]]
Absent class recall: undefined, not zero.
PASS: networks-04
Download execution record · The notebook includes executed cells and figure outputs. Compare your run with these values; timings depend on the machine.
Trace a second patch and a second filter
# Experiment — Trace a second patch and a second filter: The same learned filter is reused at each valid location;...
patch=np.asarray(test_x[0,2:5,3:6,:])
# Convert `weights` to a host NumPy array for inspection or verification.
weights=np.asarray(model.conv.kernel[...])[:,:,:,2]
# Aggregate array values to compute `expected`.
expected=np.sum(patch*weights)+np.asarray(model.conv.bias[...])[2]
np.testing.assert_allclose(model.conv(test_x[:1])[0,2,3,2],expected,rtol=1e-5,atol=1e-6)Expected: The output uses rows – and columns – of the input.
The same learned filter is reused at each valid location; changing the output coordinate changes the patch, not the filter.
Predict the chance classifier
# Experiment — Predict the chance classifier: A majority or constant-label baseline is part of evaluation.
always_zero=np.zeros_like(labels)
# Aggregate array values to compute `baseline`.
baseline=np.mean(always_zero==labels)
assert baseline==.5
# Allocate initialized array `baseline_confusion` with the specified shape and dtype.
baseline_confusion=np.zeros((2,2),dtype=int)
np.add.at(baseline_confusion,(labels,always_zero),1)
# Convert `` to a host NumPy array for inspection or verification.
np.testing.assert_array_equal(baseline_confusion,np.array([[12,0],[12,0]]))Expected: Accuracy is with confusion ,.
A majority or constant-label baseline is part of evaluation. A good-looking class-specific count alone can hide a useless predictor.
Make it yours
Where to put your code: Append your solution at the bottom of phases/05-networks/04-train-a-cnn-on-a-small-dataset/code/main.py (or inside the Make it yours cell of exercises/networks-04.ipynb) and run python3 phases/05-networks/04-train-a-cnn-on-a-small-dataset/code/main.py to verify your prediction before opening the reference solution.
Apply the trained model to one image. Predict the intermediate activation shape and total parameter count before executing, then verify both assertions.
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.zeros / jnp.ones(shape, dtype=...)— Allocates a tensor of the givenshapeinitialized with constants.nnx.Module / nnx.Linear / nnx.Optimizer— Flax NNX stateful module and optimizer containers with explicit RNG streams (nnx.Rngs) and traced graph updates.
2. Step-by-step implementation plan:
- Check tensor shape invariant:
model.conv(larger).shape==(1,8,8,4) - Check tensor shape invariant:
model(larger).shape==(1,2)
# Exercise solution: Apply the trained model to one 10\times10 image.
larger = jnp.zeros(...) # TODO: compute larger
larger = ... # TODO: compute larger
# Check tensor shape invariant: `model.conv(larger).shape==(1,8,8,4)`
assert model.conv(larger).shape # TODO: complete assertion check
# Check tensor shape invariant: `model(larger).shape==(1,2)`
assert model(larger).shape # TODO: complete assertion check
assert sum(a.size for a # TODO: complete assertion check
Reveal the reference solution
# Exercise solution: Apply the trained model to one 10\times10 image.
larger=jnp.zeros((1,10,10,1))
larger=larger.at[0,4,:,0].set(1.)
# Check tensor shape invariant: `model.conv(larger).shape==(1,8,8,4)`
assert model.conv(larger).shape==(1,8,8,4)
# Check tensor shape invariant: `model(larger).shape==(1,2)`
assert model(larger).shape==(1,2)
assert sum(a.size for a in jax.tree.leaves(nnx.state(model,nnx.Param)))==50Read a confusion matrix as conditional rates
Transfer
Normalize each confusion-matrix row by its true-class count. Explain the diagonal and verify that weighting class recalls by class counts reproduces overall accuracy. Then remove a class in a constructed case and report its recall as undefined.
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.
2. Step-by-step implementation plan:
- Reduce along axis=1 to compute
counts. - Combine or mask array elements to form
rates. - Allocate initialized array
with the specified shape and dtype. - Execute
np.testing.assert_allclose((np.diag(rates)*counts).sum()/counts.sum(),accuracy). - Reduce along axis=1 to compute
n.
# Read a confusion matrix as conditional rates (Transfer): Each row asks where examples of one true class went.
# Reduce along axis=1 to compute `counts`.
counts = confusion.sum(...) # TODO: compute counts
# Combine or mask array elements to form `rates`.
rates = np.divide(...) # TODO: compute rates
# Allocate initialized array `` with the specified shape and dtype.
np.testing.assert_allclose(rates.sum(axis = ... # TODO: compute np.testing.assert_allclose(rates.sum(axis
# Execute `np.testing.assert_allclose((np.diag(rates)*counts).sum()/counts.sum(),accuracy)`.
np.testing.assert_allclose((np.diag(rates)*counts).sum()/counts.sum(),accuracy)
missing = np.array(...) # TODO: compute missing
# Reduce along axis=1 to compute `n`.
n = missing.sum(...) # TODO: compute n
# Combine or mask array elements to form `missing_rates`.
missing_rates = np.divide(...) # TODO: compute missing_rates
# Ensure all array elements remain finite: `np.isnan(missing_rates[1]).all()`
assert np.isnan(missing_rates[1]).all() # TODO: complete assertion check
print('True-class counts:',counts,'row-normalized confusion:',rates)
print('Absent class recall: undefined, not zero.')
Show a hint
Keep the integer counts; a normalized row without a count hides how much evidence supports it.
Reveal reference solution and reasoning
# Read a confusion matrix as conditional rates (Transfer): Each row asks where examples of one true class went.
# Reduce along axis=1 to compute `counts`.
counts=confusion.sum(axis=1)
# Combine or mask array elements to form `rates`.
rates=np.divide(confusion,counts[:,None],out=np.full(confusion.shape,np.nan),where=counts[:,None]!=0)
# Allocate initialized array `` with the specified shape and dtype.
np.testing.assert_allclose(rates.sum(axis=1),np.ones(2))
# Execute `np.testing.assert_allclose((np.diag(rates)*counts).sum()/counts.sum(),accuracy)`.
np.testing.assert_allclose((np.diag(rates)*counts).sum()/counts.sum(),accuracy)
missing=np.array([[3,1],[0,0]])
# Reduce along axis=1 to compute `n`.
n=missing.sum(axis=1)
# Combine or mask array elements to form `missing_rates`.
missing_rates=np.divide(missing,n[:,None],out=np.full(missing.shape,np.nan),where=n[:,None]!=0)
# Ensure all array elements remain finite: `np.isnan(missing_rates[1]).all()`
assert np.isnan(missing_rates[1]).all()
print('True-class counts:',counts,'row-normalized confusion:',rates)
print('Absent class recall: undefined, not zero.')Each row asks where examples of one true class went. A missing class has no observed recall; assigning zero confuses missing evidence with demonstrated failure.
Catch a channel-order mistake
Intermediate
Transpose NHWC images to NCHW and show why this convolution rejects them. Repair the layout and compare predictions.
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:
jnp.allclose(actual, expected, rtol=..., atol=...)— Checks that two arrays match elementwise within floating-point tolerance.
2. Step-by-step implementation plan:
- Catch a channel-order mistake (Intermediate): The mistaken array has eight channels where the layer...
- Run the boundary check and catch the expected exception:
- Rearrange tensor axes to match the required layout for
repaired.
# Catch a channel-order mistake (Intermediate): The mistaken array has eight channels where the layer...
wrong = jnp.transpose(...) # TODO: compute wrong
caught = ... # TODO: compute caught
# Run the boundary check and catch the expected exception:
try:model(wrong)
except (ValueError,TypeError):caught=True
assert caught # TODO: complete assertion check
# Rearrange tensor axes to match the required layout for `repaired`.
repaired = jnp.transpose(...) # TODO: compute repaired
np.testing.assert_allclose(model(repaired),model(test_x[:2]),rtol=1e-6)
Show a hint
The final axis is the input-channel dimension for NNX Conv.
Reveal reference solution and reasoning
# Catch a channel-order mistake (Intermediate): The mistaken array has eight channels where the layer...
wrong=jnp.transpose(test_x[:2],(0,3,1,2))
caught=False
# Run the boundary check and catch the expected exception:
try:model(wrong)
except (ValueError,TypeError):caught=True
assert caught
# Rearrange tensor axes to match the required layout for `repaired`.
repaired=jnp.transpose(wrong,(0,2,3,1))
np.testing.assert_allclose(model(repaired),model(test_x[:2]),rtol=1e-6)The mistaken array has eight channels where the layer expects one. Explicit layout conversion is the repair; changing the declared channel count would silently change the model task.
Diagnose the result
Before tuning, inspect sample labels and input layout. Validate a convolution patch numerically, inspect prediction counts, and compare the constant baseline. If all predictions choose one class, examine class balance, score ranges, and per-class errors. If held-out samples equal training samples, repair the split before reporting metrics.
Carry forward
- Generate images with an explicit label rule
- Derive the convolution output shape
- Pool spatial responses and retain class scores
- Read a confusion matrix rather than a success label
Keep your evidence
Keep the 8-by-8-to-6-by-6 shape diagram, NumPy patch dot product, seeds 20/21, independent held-out cross-entropy, 24-observation confusion matrix, constant-class baseline, and layout failure repair.
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).