CSE 10124 — Building ChatGPT: Lab 01¶
Learning from Numbers with MNIST (10 points)¶
- Name:
- NETID:
- Due: Sunday, September 13, 2026 at 11:59 PM Eastern
Overview¶
Before a neural network can work with language, it must learn to work with numbers. In this lab, you will train a small neural network on MNIST, a dataset of handwritten digits. Each image is already represented by numbers: 784 pixel intensities and one label from 0 through 9.
This gives us a concrete first model without yet worrying about how text becomes numbers. In Lab 02, we will make that transition by turning text into token IDs and embedding vectors.
From concept to implementation¶
In Homework 01, you learned how data becomes numerical features and targets, and why scale matters. In Homework 02, you traced an MNIST image through pixels, logits, loss, gradients, and parameter updates. In this lab, you will turn that conceptual pipeline into working PyTorch code.
- Homework 01: representation and scaling → Tasks 01–02
- Homework 02: architecture and logits → Task 03
- Homework 02: loss, backpropagation, and optimization → Task 04
- Homework 02: generalization → Task 05
What you will build¶
You will load and inspect MNIST, scale its pixel values to the range [0, 1], define a two-layer classifier, train it, and measure its accuracy on images the model did not see during training.
Learning objectives¶
By the end of this lab, you should be able to:
- Explain how an image is represented as a tensor of numerical features.
- Use a
DataLoaderto process examples in shuffled batches. - Define a small classifier with PyTorch layers and activation functions.
- Identify the forward pass, loss calculation, backpropagation, and parameter update in a training loop.
- Use held-out test data to measure generalization.
| Task ID | Description | Points |
|---|---|---|
| 01 | Load and inspect numerical data | 1.0 |
| 02 | Prepare tensors and data loaders | 2.0 |
| 03 | Define a neural network | 3.0 |
| 04 | Train the model | 2.0 |
| 05 | Evaluate and interpret predictions | 2.0 |
| 06 | Generate submission | 0 |
| Total | 10.0 |
Work from top to bottom. Complete only the sections marked TODO; do not rename the requested variables because later cells use them. The line counts are guides, not grading requirements. Run each cell after completing it and compare its output with the stated sanity check.
Key Terms¶
- Feature — one measured input property. Here, each pixel intensity at a fixed position is one numerical feature.
- Target — the output a model is trying to predict. Here, the target is the digit class.
- Label — the known correct target value attached to one example. Each MNIST label is an integer from 0 through 9.
- Batch — a group of examples processed together before the model's parameters are updated.
- Parameter — a numerical value learned during training, including a model's weights and biases.
- Logit — one of the model's raw, unnormalized output scores, with one logit for each possible class.
- Loss — a single numerical measure of how well the model's prediction matches the expected answer.
- Gradient — a value indicating how much and in which direction the loss would change if a parameter changed.
- Backpropagation — an algorithm that uses the chain rule to compute the gradient of the loss with respect to each parameter.
- Optimizer — an update rule that uses gradients to change parameters in a direction expected to reduce loss.
- Epoch — one complete pass over the entire training dataset.
- Generalization — a model's ability to perform well on new examples that were not used to update its parameters.
Task 00: Setup (0 pts.)¶
Run this cell first. It imports the libraries, fixes the random seeds, enables deterministic PyTorch operations, and downloads MNIST into the Colab runtime. The first download may take a minute. You should see 60000 training images and 10000 test images. This lab intentionally uses the CPU so that a fresh top-to-bottom run produces the same reference values for everyone.
import random
import numpy as np
import torch
from torch import nn
from torch.utils.data import DataLoader
from torchvision import datasets, transforms
import matplotlib.pyplot as plt
SEED = 10124
random.seed(SEED)
np.random.seed(SEED)
torch.manual_seed(SEED)
# Keep Colab's CPU math in a fixed order so results reproduce exactly.
torch.use_deterministic_algorithms(True)
torch.set_num_threads(1)
torch.set_num_interop_threads(1)
torch.backends.mkldnn.enabled = False
device = "cpu"
print("Using:", device)
BATCH_SIZE = 128
LEARNING_RATE = 0.001
EPOCHS = 3
# ToTensor converts each image to a tensor and scales pixel values to [0, 1].
to_tensor = transforms.ToTensor()
train_data = datasets.MNIST("data", train=True, download=True, transform=to_tensor)
test_data = datasets.MNIST("data", train=False, download=True, transform=to_tensor)
print(len(train_data), "training images")
print(len(test_data), "test images")
Task 00: Expected Output (0 pts.)¶
Using: cpu
60000 training images
10000 test images
These three lines should match exactly. The lab uses the CPU intentionally for reproducibility.
Task 01: Load and Inspect Numerical Data (1 pt.)¶
Connect this to Homework 01: one image is one data point, its 784 pixel intensities are features, and the digit class is the target. The supplied digit for a particular image is its label. ToTensor() has already scaled the raw pixel intensities to the range [0, 1].
Get the first (image, label) pair from train_data. Store them in variables named image and label. Print the image shape, label, minimum pixel value, and maximum pixel value. Then display the image in grayscale with its label as the title.
Sanity check: the image shape should be torch.Size([1, 28, 28]), the first label should be 5, and the pixel values should fall between 0.0 and 1.0.
Grading (1.0): retrieve and inspect the example (0.5); display it correctly (0.5).
LINES: about 6
# TODO: get the first image and label.
# TODO: print its shape, label, minimum, and maximum.
# TODO: display the 28 x 28 image in grayscale with the label as its title.
# LINES: ~6
Task 01: Expected Output (0 pts.)¶
shape: torch.Size([1, 28, 28])
label: 5
pixel range: 0.0 to 1.0
You should also see a grayscale image of a handwritten 5. Your print labels may differ, but the shape, digit, and range should match.
Task 02: Prepare Tensors and Data Loaders (2 pts.)¶
Create train_loader and test_loader using BATCH_SIZE. Shuffle the training data so the model does not see examples in the same order every epoch; do not shuffle the test data. Pull one batch from the training loader into variables named X_batch and y_batch, then print both shapes.
Sanity check: the image batch should have shape torch.Size([128, 1, 28, 28]); the label batch should have shape torch.Size([128]).
Grading (2.0): configure both data loaders correctly (1.0); retrieve and inspect one batch (1.0).
LINES: about 5
# TODO: create train_loader and test_loader using BATCH_SIZE.
# TODO: shuffle only the training data.
# TODO: print the image-batch and label-batch shapes.
# LINES: ~5
Task 02: Expected Output (0 pts.)¶
images: torch.Size([128, 1, 28, 28])
labels: torch.Size([128])
The wording may differ, but both shapes should match exactly.
Task 03: Define a Neural Network (3 pts.)¶
Define MNISTClassifier with this exact path:
- Flatten each image from
1 × 28 × 28into 784 numbers. - Apply a linear layer from 784 inputs to 128 hidden values.
- Apply ReLU.
- Apply a linear layer from 128 hidden values to 10 logits.
Store the layers in self.network, implement forward, and instantiate the classifier as model on device. Do not add a softmax layer: CrossEntropyLoss expects raw logits.
Sanity check: passing X_batch through the model should produce shape torch.Size([128, 10]).
Grading (3.0): correct architecture (2.0); working forward method, device placement, and output shape (1.0).
LINES: about 10
class MNISTClassifier(nn.Module):
def __init__(self):
super().__init__()
# TODO: store a Flatten -> Linear(784,128) -> ReLU -> Linear(128,10) network.
# LINES: ~6
def forward(self, x):
# TODO: return the network output.
# LINES: 1
pass
# TODO: instantiate MNISTClassifier as model on device and print one output shape.
# LINES: ~2
Task 03: Expected Output (0 pts.)¶
torch.Size([128, 10])
The first dimension is the batch size and the second is one logit for each of the 10 digit classes.
Task 04: Train the Model (2 pts.)¶
Use cross-entropy loss and the Adam optimizer with LEARNING_RATE. Train for EPOCHS epochs. For each batch: move data to device, clear old gradients, compute logits and loss, run backpropagation, update the parameters, and add the scalar batch loss to total_loss.
Sanity check: the code should print one mean loss per epoch, and the loss should generally decrease. Exact values will vary.
Grading (2.0): complete the training steps correctly (1.5); accumulate and report mean loss (0.5).
LINES: about 16
loss_fn = nn.CrossEntropyLoss()
optimizer = torch.optim.Adam(model.parameters(), lr=LEARNING_RATE)
for epoch in range(EPOCHS):
model.train()
total_loss = 0.0
for X, y in train_loader:
# TODO: move X and y to device.
# TODO: clear gradients, compute logits and loss, backpropagate, and update.
# TODO: add this batch's loss to total_loss.
# LINES: ~8
pass
print(f"epoch {epoch + 1}: loss = {total_loss / len(train_loader):.4f}")
Task 04: Expected Output (0 pts.)¶
epoch 1: loss = 0.4138
epoch 2: loss = 0.1901
epoch 3: loss = 0.1381
With the provided seed and a fresh top-to-bottom run, these values should match. If you rerun individual cells out of order, restart the session and run all cells again before troubleshooting.
Task 05: Evaluate and Interpret Predictions (2 pts.)¶
Switch the model to evaluation mode. Inside torch.no_grad(), count correct predictions across the complete test set. Store the final fraction in a variable named accuracy and print it as a percentage. Then display the first 8 test images with predicted and true labels.
Sanity check: total should equal 10000, and a correctly trained model should normally exceed 95% test accuracy. If yours does not, check Tasks 03 and 04 before submitting.
Finally, answer the reflection in the Markdown cell below. Your response should use the idea of generalization and distinguish learning a pattern from memorizing examples.
Grading (2.0): complete test-set evaluation (1.0); prediction visualization (0.5); reflection (0.5).
LINES: about 18, plus 2–3 sentences
model.eval()
correct = 0
total = 0
with torch.no_grad():
for X, y in test_loader:
# TODO: compute predictions and update correct and total.
# LINES: ~5
pass
accuracy = correct / total
print(f"test accuracy: {accuracy:.2%}")
# TODO: display the first 8 test images with predicted and true labels.
# LINES: ~10
Task 05: Expected Output (0 pts.)¶
test accuracy: 96.13%
With the provided seed and a fresh top-to-bottom run, the accuracy should match. You should also see eight test images, each labeled with its predicted (p) and true (y) digit.
Task 05 Reflection¶
Why do we evaluate on images that were not used for training? What would a high training accuracy but low test accuracy tell you?
TODO: Write 2–3 sentences here.
Task 06: Generate Submission (0 pts.)¶
First run every cell above and confirm that your outputs are visible. Then replace YOUR_NETID in the code cell below, uncomment the final function call, and run the cell. Colab will convert the current executed notebook into lab01_NETID.html and download it to your computer.
Submit the generated HTML file on Canvas. The HTML preserves your code, written responses, and outputs in one file that the grader can open without rerunning the notebook.
import json
import subprocess
from pathlib import Path
NETID = "YOUR_NETID" # Replace with your Notre Dame NetID.
def export_notebook(netid):
netid = netid.strip()
if not netid or netid == "YOUR_NETID" or not netid.isalnum():
raise ValueError("Replace YOUR_NETID with your Notre Dame NetID before exporting.")
try:
from google.colab import _message, files
except ImportError as exc:
raise RuntimeError("Run this export cell in Google Colab.") from exc
base_name = f"lab01_{netid}"
ipynb_path = Path("/content") / f"{base_name}.ipynb"
html_path = ipynb_path.with_suffix(".html")
notebook = _message.blocking_request("get_ipynb", timeout_sec=10)["ipynb"]
with ipynb_path.open("w", encoding="utf-8") as f:
json.dump(notebook, f)
subprocess.run(
["jupyter", "nbconvert", "--to", "html", str(ipynb_path)],
check=True,
)
files.download(str(html_path))
# Uncomment this line after replacing YOUR_NETID above.
# export_notebook(NETID)
Task 06: Expected Output (0 pts.)¶
Colab should begin downloading a file named lab01_NETID.html, with your actual NetID in place of NETID. The conversion log may mention nbconvert; its exact wording and file size can vary.
Submission Checklist¶
Before submitting:
- Enter your name and NETID at the top of the notebook.
- Complete every
TODO, including the Markdown reflection. - In Colab, choose Runtime → Run all and confirm that every graded cell finishes without an error.
- Confirm that all three epoch losses are visible and test accuracy is at least 95%.
- Keep the executed outputs in the notebook so the grader can see your results.
- In Task 06, replace
YOUR_NETID, uncommentexport_notebook(NETID), and run that cell. - Confirm that Colab downloads
lab01_NETID.htmlwith your actual NetID in the filename. - Upload that
.htmlfile to the Lab 01 assignment on Canvas by the deadline above. Do not submit the.ipynb, a PDF, orsubmission.json.