CS 188 Project 5 — Machine Learning Checklist
Due: Friday, August 7, 11:59 PM PT (today) File to edit: models.py only. Never touch nn.py, backend.py, autograder.py.
Setup
-
conda activate [env] -
pip install numpy -
pip install matplotlib -
python autograder.py --check-dependencies→ spinning line segment window appears
API Reference (what you're allowed to use)
Node constructors
| Node | Usage | Shapes |
|---|---|---|
nn.Constant |
provided to you (never construct) | inputs x: batch_size × num_features, labels y: batch_size × num_outputs |
nn.Parameter(n, m) |
trainable weights/biases | must be 2D, shape n × m |
nn.DotProduct(x, y) |
perceptron ONLY | returns 1×1 node |
nn.Add(x, y) |
element-wise add | both batch_size × num_features |
nn.AddBias(features, bias) |
broadcast bias to every row | features batch_size × num_features, bias 1 × num_features |
nn.Linear(features, weights) |
matrix multiply | features batch_size × i, weights i × o → output batch_size × o |
nn.ReLU(features) |
element-wise max(x, 0) | same shape as input |
nn.SquareLoss(a, b) |
regression loss (Q2) | both batch_size × num_outputs |
nn.SoftmaxLoss(logits, labels) |
classification loss (Q3, Q4) | both batch_size × num_classes. Order matters: logits first |
Functions / methods
-
nn.as_scalar(node)— extract Python float from a1×1or loss node -
nn.gradients(loss, [p1, p2, ..., pn])— returns[g1, g2, ..., gn]asnn.Constants, same order -
parameter.update(direction, multiplier)— performsweights ← weights + direction · multiplierdirection: Node, same shape as parametermultiplier: Python scalar. For gradient descent, multiplier = negative learning rate (e.g.-0.05)
-
dataset.iterate_once(batch_size)— one pass, yields(x, y)pairs -
dataset.iterate_forever(batch_size)— infinite batches -
dataset.get_validation_accuracy()— Q3/Q4 stopping condition
Q1: Perceptron (6 pts) — PerceptronModel
self.w already initialized as a 1 × dimensions Parameter. Bias is baked into x — no separate bias param. Labels y are 1 or -1.
-
run(self, x)— returnnn.DotProduct(self.w, x)(an nn.DotProduct node, NOT a scalar) -
get_prediction(self, x)— return1ifnn.as_scalar(self.run(x)) >= 0, else-1 -
train(self)- Loop: repeat full passes over dataset with
dataset.iterate_once(1)(batch_size = 1) - For each
(x, y): ifget_prediction(x) != nn.as_scalar(y)→self.w.update(x, nn.as_scalar(y))(direction = x, multiplier = label ±1) - Track a mistake flag per pass; terminate when a full pass completes with zero mistakes
- Loop: repeat full passes over dataset with
-
python autograder.py -q q1— should finish in ≤ ~20 sec. Longer = bug.
Pseudocode
run(self, x):
return nn.DotProduct(self.w, x)
get_prediction(self, x):
score = nn.as_scalar(self.run(x))
if score >= 0:
return 1
else:
return -1
train(self):
converged = False
while not converged:
converged = True # assume clean pass
for x, y in dataset.iterate_once(1):
prediction = self.get_prediction(x)
label = nn.as_scalar(y) # +1 or -1
if prediction != label:
self.w.update(x, label) # w <- w + label * x
converged = False # mistake made, need another pass
Q2: Non-linear Regression (6 pts) — RegressionModel
Approximate sin(x) on [−2π, 2π]. Loss: nn.SquareLoss. Pass threshold: avg loss ≤ 0.02.
Suggested hyperparameters (staff-verified):
| Hyperparam | Value |
|---|---|
| Hidden layer size | 512 |
| Batch size | 200 |
| Learning rate | 0.05 |
| Hidden layers | 1 (2 linear layers total) |
-
__init__(self)— create parameters:-
self.W1 = nn.Parameter(1, 512)(input dim is 1) -
self.b1 = nn.Parameter(1, 512) -
self.W2 = nn.Parameter(512, 1) -
self.b2 = nn.Parameter(1, 1) - Store learning rate / batch size as instance vars
-
-
run(self, x)— returnbatch_size × 1prediction node:-
h1 = nn.ReLU(nn.AddBias(nn.Linear(x, self.W1), self.b1)) -
return nn.AddBias(nn.Linear(h1, self.W2), self.b2)— no ReLU on output (need negatives)
-
-
get_loss(self, x, y)—return nn.SquareLoss(self.run(x), y) -
train(self)- Loop over
dataset.iterate_once(200)(oriterate_forever) - Per batch:
loss = self.get_loss(x, y)→grads = nn.gradients(loss, [W1, b1, W2, b2])→param.update(grad, -lr)for each pair in order - Stop when
nn.as_scalar(loss)< 0.02 (check per pass or per batch; aim a bit under, e.g. 0.015)
- Loop over
-
python autograder.py -q q2— takes a few minutes to train
Pseudocode
__init__(self):
self.lr = 0.05
self.batch_size = 200
self.W1 = nn.Parameter(1, 512)
self.b1 = nn.Parameter(1, 512)
self.W2 = nn.Parameter(512, 1)
self.b2 = nn.Parameter(1, 1)
run(self, x):
z1 = nn.AddBias(nn.Linear(x, self.W1), self.b1)
h1 = nn.ReLU(z1)
output = nn.AddBias(nn.Linear(h1, self.W2), self.b2) # no ReLU here
return output
get_loss(self, x, y):
return nn.SquareLoss(self.run(x), y)
train(self):
params = [self.W1, self.b1, self.W2, self.b2]
loss_value = infinity
while loss_value > 0.015: # buffer under 0.02
for x, y in dataset.iterate_once(self.batch_size):
loss = self.get_loss(x, y)
grads = nn.gradients(loss, params)
for i in range(len(params)):
params[i].update(grads[i], -self.lr) # NEGATIVE lr
loss_value = nn.as_scalar(loss)
Q3: Digit Classification (6 pts) — DigitClassificationModel
MNIST. Input: batch_size × 784. Output: batch_size × 10 scores/logits. Loss: nn.SoftmaxLoss. Pass threshold: ≥ 97% test accuracy (you only see validation — stop at 97.5–98% validation to be safe).
Suggested hyperparameters:
| Hyperparam | Value |
|---|---|
| Hidden layer size | 200 |
| Batch size | 100 |
| Learning rate | 0.5 |
| Hidden layers | 1 (2 linear layers total) |
-
__init__(self)-
self.W1 = nn.Parameter(784, 200) -
self.b1 = nn.Parameter(1, 200) -
self.W2 = nn.Parameter(200, 10) -
self.b2 = nn.Parameter(1, 10)
-
-
run(self, x)— same shape as Q2 but 784 → 200 → 10. NO ReLU after the last linear layer (output raw logits) -
get_loss(self, x, y)—return nn.SoftmaxLoss(self.run(x), y)— logits first, labels second -
train(self)- Loop epochs over
dataset.iterate_once(100) - Same gradient update pattern as Q2 with
-lr - After each epoch: check
dataset.get_validation_accuracy(); stop at ≥ 0.975 (or 0.98)
- Loop epochs over
-
python autograder.py -q q3— staff hits 98% validation in ~5 epochs
Pseudocode
__init__(self):
self.lr = 0.5
self.batch_size = 100
self.W1 = nn.Parameter(784, 200)
self.b1 = nn.Parameter(1, 200)
self.W2 = nn.Parameter(200, 10)
self.b2 = nn.Parameter(1, 10)
run(self, x):
z1 = nn.AddBias(nn.Linear(x, self.W1), self.b1)
h1 = nn.ReLU(z1)
logits = nn.AddBias(nn.Linear(h1, self.W2), self.b2) # raw scores, no ReLU
return logits
get_loss(self, x, y):
return nn.SoftmaxLoss(self.run(x), y) # logits FIRST, labels SECOND
train(self):
params = [self.W1, self.b1, self.W2, self.b2]
while dataset.get_validation_accuracy() < 0.975:
for x, y in dataset.iterate_once(self.batch_size):
loss = self.get_loss(x, y)
grads = nn.gradients(loss, params)
for i in range(len(params)):
params[i].update(grads[i], -self.lr)
Q4: Language Identification (7 pts) — LanguageIDModel
RNN over characters. 5 languages. Variable-length words → input is a list xs = [x0, x1, ..., xL-1], each batch_size × 47 (num chars). Output: batch_size × 5 logits. Loss: nn.SoftmaxLoss. Pass threshold: ≥ 81% test accuracy. Staff ref: ~89% validation, 10–20 epochs.
Architecture (required construction from spec):
-
First letter:
z0 = x0 · Wx→h1 = f_initial(x0) -
Subsequent letters:
zi = xi · Wx + hi · W_hiddenviann.Add(nn.Linear(x, Wx), nn.Linear(h, W_hidden)) -
fandf_initialshare parameters (sameWx) -
After the loop:
hL(batch_size × d) → output layers →batch_size × 5 -
__init__(self)— pick hidden sized(sufficiently large, e.g. 200–400):-
self.Wx = nn.Parameter(47, d)— input-to-hidden -
self.W_hidden = nn.Parameter(d, d)— hidden-to-hidden - Bias for the recurrent layer:
nn.Parameter(1, d)(optional but helps) - Output layer(s): e.g.
self.W_out = nn.Parameter(d, 5),self.b_out = nn.Parameter(1, 5) -
run(self, xs) -
Explicit for loop with index over
xs -
i == 0:h = nn.ReLU(nn.Linear(xs[0], self.Wx))(add bias if you made one) -
i > 0:h = nn.ReLU(nn.Add(nn.Linear(xs[i], self.Wx), nn.Linear(h, self.W_hidden))) - After loop:
return nn.AddBias(nn.Linear(h, self.W_out), self.b_out)— no ReLU on final output -
get_loss(self, xs, y)—return nn.SoftmaxLoss(self.run(xs), y)
-
-
train(self)- Same epoch loop + gradient update pattern; include ALL parameters in the
nn.gradientslist and update every one - Stop on
dataset.get_validation_accuracy()≥ ~0.85 (buffer above 0.81 test threshold) -
python autograder.py -q q4
- Same epoch loop + gradient update pattern; include ALL parameters in the
Pseudocode
__init__(self):
self.d = 300 # hidden size, tune 200-400
self.lr = 0.1 # tune this first
self.batch_size = 100
self.Wx = nn.Parameter(47, self.d) # 47 = num chars
self.W_hidden = nn.Parameter(self.d, self.d)
self.b_hidden = nn.Parameter(1, self.d)
self.W_out = nn.Parameter(self.d, 5) # 5 languages
self.b_out = nn.Parameter(1, 5)
run(self, xs):
# xs is a list of L nodes, each batch_size x 47
for i in range(len(xs)):
if i == 0:
z = nn.Linear(xs[0], self.Wx) # f_initial
else:
z = nn.Add(nn.Linear(xs[i], self.Wx), # shared Wx
nn.Linear(h, self.W_hidden)) # recurrent term
z = nn.AddBias(z, self.b_hidden)
h = nn.ReLU(z)
logits = nn.AddBias(nn.Linear(h, self.W_out), self.b_out) # no ReLU
return logits
get_loss(self, xs, y):
return nn.SoftmaxLoss(self.run(xs), y)
train(self):
params = [self.Wx, self.W_hidden, self.b_hidden, self.W_out, self.b_out]
while dataset.get_validation_accuracy() < 0.85: # buffer over 0.81
for xs, y in dataset.iterate_once(self.batch_size):
loss = self.get_loss(xs, y)
grads = nn.gradients(loss, params)
for i in range(len(params)):
params[i].update(grads[i], -self.lr)
Design tips if failing:
- Tune learning rate FIRST — wrong lr invalidates everything else
- Start shallow (1 non-linearity), then deepen
- Loss → Inf/NaN = learning rate too high
- Smaller batches need lower learning rates
- Keep a log of every architecture + hyperparams + result
Global Gotchas
-
nn.DotProduct— perceptron only, nowhere else -
nn.SoftmaxLoss(logits, labels)— never swap argument order - No ReLU on final output layer (Q2, Q3, Q4)
- All
nn.Parameters must be 2D -
updatemultiplier must be negative lr for descent (Q2–Q4); positive label for perceptron (Q1) - Q2/Q3: dataset size must be evenly divisible by batch size
- Randomness: failing once can happen; failing twice in a row = change architecture
- Full autograder should run in 2–12 min
Submission
-
python autograder.py(full run, all questions pass) - Upload all
.pyfiles to Gradescope - Specify partner (if any) and verify both are associated with the submission