Skip to content

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 a 1×1 or loss node
  • nn.gradients(loss, [p1, p2, ..., pn]) — returns [g1, g2, ..., gn] as nn.Constants, same order
  • parameter.update(direction, multiplier) — performs weights ← weights + direction · multiplier
    • direction: Node, same shape as parameter
    • multiplier: 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) — return nn.DotProduct(self.w, x) (an nn.DotProduct node, NOT a scalar)
  • get_prediction(self, x) — return 1 if nn.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): if get_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
  • 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) — return batch_size × 1 prediction 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) (or iterate_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)
  • 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)
  • 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 · Wxh1 = f_initial(x0)

  • Subsequent letters: zi = xi · Wx + hi · W_hidden via nn.Add(nn.Linear(x, Wx), nn.Linear(h, W_hidden))

  • f and f_initial share parameters (same Wx)

  • After the loop: hL (batch_size × d) → output layers → batch_size × 5

  • __init__(self) — pick hidden size d (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.gradients list and update every one
    • Stop on dataset.get_validation_accuracy() ≥ ~0.85 (buffer above 0.81 test threshold)
    • python autograder.py -q q4

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
  • update multiplier 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 .py files to Gradescope
  • Specify partner (if any) and verify both are associated with the submission