Mode

Key idea

Treat the assistant as a fast, well-read junior who never says "I don't know". It has read every tutorial, so it produces idiomatic-looking PyTorch in seconds. But it can't run your code, doesn't know your data, and will invent an API rather than admit uncertainty. The speed is real; the correctness is yours. The winning move is to use it where mistakes are cheap and loud, and to verify hard where they're silent — which in ML is most places.

Where it shines. Boilerplate you'd resent writing — Dataset/DataLoader scaffolding, argparse/config plumbing, plotting, docstrings, a training-loop skeleton, converting a notebook into a module. Explaining an unfamiliar error or library. Translating maths you already understand into a first-draft implementation you will then check.

Where it bites. The parts that make ML ML: tensor shapes and broadcasting, whether a .detach() belongs there, whether that preprocessing step leaks the test set, whether the loss matches the task, whether the numbers are stable. Bugs here don't crash — they train to a slightly-worse number and you never notice.

The mental model. It's autocomplete for code, not for judgement. It optimises for "looks like what usually comes next", which is exactly right for syntax and exactly wrong for the load-bearing decisions. You stay the engineer; it holds the keyboard.

Reach for it when

  • Writing boilerplate: data loaders, configs, CLI, plots
  • Drafting an implementation of maths you already understand
  • Explaining a stack trace or an unfamiliar library
  • Refactoring, renaming, adding type hints and docstrings
  • Writing the first version of a test you'll then sharpen

Verify hard (or do it yourself) when

  • It touches tensor shapes, indexing, or broadcasting
  • It adds a preprocessing/split step — data-leakage risk
  • It picks a loss, metric, or evaluation protocol
  • It reaches for an API you can't immediately confirm exists
  • The stakes are a silent accuracy drop, not a crash

# A weak prompt gets confident, generic, often-wrong code:
"write a training loop for my model"

# A strong prompt pins down everything the model can't guess:
"PyTorch 2.3. Model is a classifier: input (B, 128) float, output
(B, 10) logits. Data is a TensorDataset already on CPU. Write a
training loop with AdamW, cosine LR, gradient clipping at 1.0, and
an overfit-one-batch sanity check I can run first. Don't add any
preprocessing — the data is already normalised."
Want the prompting patterns and the tight-loop workflow?

The loop that works

Small step → run → read the real output → correct → repeat. The assistant is most useful in tight iterations where each change is verifiable in seconds. Ask for a diff, not a rewrite. Run it. Paste back the actual error or the actual shape mismatch — never the paraphrase. The failure mode to avoid is accepting 200 lines you haven't executed; by the time it's wrong you can no longer tell which of the twelve plausible changes caused it.

Give it the context it cannot guess. Framework and versions, exact input/output shapes and dtypes, the real traceback, the two or three files it will touch, and your constraint ("data is already split — don't re-split"). Most bad generations are under-specified prompts, not model failure. Shapes and versions alone remove the majority of hallucinated-API and broadcasting bugs.

Ask for the sanity check up front. "…and give me an overfit-one-batch test I can run first." A model that can't drive the loss to ~0 on two examples has a wiring bug, and the assistant will have handed you the means to catch its own mistake. Pair with shape asserts on the forward pass.

Prefer small, reviewable diffs. "Change only the optimiser and LR schedule; leave the rest." Large rewrites are hard to review and bury the one line that matters. If you can't read every changed line, you can't own the result.

Make it show its work. Ask it to explain why a step is there before you accept it — especially any .detach(), .reshape(), with torch.no_grad(), or normalisation. If the justification is vague, that's your leak or your stopped-gradient.

Pin versions and confirm the API. Assistants blend APIs across library versions — a sklearn argument that was renamed, a torch function that moved. When it uses something you don't recognise, check the installed version's docs before trusting it.

Keep it out of the load-bearing decisions. Loss choice, evaluation protocol, train/val/test discipline, metric selection — decide these yourself, then have it implement your decision. It's an excellent typist for a plan you own; a poor author of the plan.

# The two guards to demand in (or add to) any generated model code.
import torch

def sanity_check(model, make_batch, steps=200):
    """A wiring bug can't overfit two examples. This catches most of them."""
    x, y = make_batch(n=2)
    opt = torch.optim.Adam(model.parameters(), lr=1e-2)
    for _ in range(steps):
        opt.zero_grad()
        out = model(x)
        assert out.shape[0] == x.shape[0], (out.shape, x.shape)  # shape guard
        loss = torch.nn.functional.cross_entropy(out, y)
        loss.backward(); opt.step()
    assert loss.item() < 0.05, f"can't overfit 2 samples — wiring bug: {loss.item()}"
Want the ML-specific failure modes and the verification discipline?

The silent-failure catalogue

Every ML-specific bug an assistant introduces shares one trait: the program still runs. No exception, no red text — just a number that's quietly worse than it should be. Ordinary software has a compiler and a crash to tell you you're wrong; ML training mostly doesn't. So the discipline isn't "read the code and it looks fine" — it's building the checks that make silent wrongness loud.

Data leakage via "helpful" preprocessing. The classic: it fits the scaler/imputer/encoder on the whole dataset before splitting, or leaks target statistics into features. The code is clean, the metric goes up, the model is worthless in production. Any time it adds a fit/fit_transform, check it happens strictly inside the training fold. See Data Leakage.

Silent shape and broadcasting bugs. A (B, 1) where a (B,) was meant, an accidental broadcast in the loss, a view that should have been a reshape, a mask applied on the wrong axis. It runs; it just computes the wrong thing. Assert shapes at boundaries; don't trust the eyeball.

Hallucinated or version-drifted APIs. Arguments that don't exist, functions from a different major version, a made-up utility that "should" be there. Crashes if you're lucky; silently no-ops (e.g. an ignored keyword) if you're not. Confirm against the installed version.

Wrong loss / metric for the task. BCEWithLogitsLoss vs CrossEntropyLoss vs applying a sigmoid twice; accuracy on an imbalanced problem; a metric averaged the wrong way. It'll pick the popular default, which is often not yours. You choose; it implements.

Numerical instability. log(softmax(x)) instead of log_softmax, missing epsilon in a denominator, exp before a max-subtract. Fine on the toy batch in the chat, NaN on real data three epochs in.

Stopped or leaking gradients. A stray .detach(), .item(), or with torch.no_grad() in the training path silently kills learning for part of the graph; missing them where you did want a stop inflates memory or backprops through your metric.

Fabricated evaluation. Asked to "add evaluation", it may write a plausible loop that computes on the training set, forgets model.eval(), or leaves dropout on. A confident number from a broken harness is worse than no number.

Non-determinism it papers over. Seeds set in one place but not the loader, shuffle=True in the val loader, a seed that doesn't cover CUDA. Your "reproducible" run isn't. See Reproducibility.

The verification stack. Overfit one batch → assert shapes and dtypes at boundaries → confirm the split/preprocessing order → diff-review every changed line → pin and check API versions → run the real data past the toy batch. This is the same discipline as Testing ML Code — the assistant just makes it more necessary, because it produces plausible code faster than you can eyeball it.

Current limitations, honestly. It has a knowledge cutoff (newest library releases may be wrong), no execution and no access to your data (so it guesses shapes and distributions), a limited context window (it forgets constraints from earlier in a long session — restate them), and a strong bias toward the most common pattern (which quietly ignores your unusual-but-correct setup). None of these are fatal; all of them are reasons the human stays in the loop.

# Leakage review: the single most common silent bug in generated pipelines.
from sklearn.preprocessing import StandardScaler
from sklearn.model_selection import train_test_split

# ✗ What an assistant often writes — scaler sees the test set:
# X = StandardScaler().fit_transform(X)
# X_tr, X_te, y_tr, y_te = train_test_split(X, y)

# ✓ Fit on train only, transform the rest:
X_tr, X_te, y_tr, y_te = train_test_split(X, y, random_state=0)
scaler = StandardScaler().fit(X_tr)          # statistics from train alone
X_tr, X_te = scaler.transform(X_tr), scaler.transform(X_te)
# In a real pipeline, wrap this in a sklearn Pipeline so CV can't leak either.
Too dense?