Lab 12 — Fine-tune a model and read the loss curves

Scaffold: 2/5. The datasets, the job launch, and the download are written. You write the part that turns two CSVs into a decision.

Cost, up front. Part A runs a real Amazon Bedrock customization job. On Nova Micro with the 300-record dataset here, the training charge is small (billed per token processed, multiplied by the epoch count) but it is not zero, and the resulting custom model is billed for storage every month until you delete it. The job runs for hours, not minutes. Part B stands the model up for inference: on-demand deployment costs per token and is cheap, one Provisioned Throughput model unit costs tens of US dollars an hour from creation to deletion. Rates move; check the Amazon Bedrock pricing page before you run anything.

There is a free path. ./scripts/test.sh runs the whole curve-reading exercise against two shipped sample runs, with no AWS account and no spend. If you only want the skill, that is the whole lab.

The scenario

A support team has a few hundred prompt-and-completion pairs that capture how their replies should sound, and a smaller set they kept back. They want a model that answers in the house style without being reminded of it in every prompt, so they can stop paying for a long few-shot preamble on every call.

The first run used the default hyperparameters and produced a model that sounded like the base. The second run cranked the passes right up and produced a model that parrots the training replies. Somewhere in between is the model they want, and nobody wants to find it by launching twenty jobs and reading the output.

The knobs are epochs, learning rate, and (on some base models) batch size. The signal is the pair of loss curves the job writes to S3 when it finishes. This lab is the mechanics of both.

The requirement

Launch a customization job with hyperparameters you chose, fetch the two metrics files it wrote, and produce a chart plus a one-paragraph reading that names which of three pictures the run is: underfitting, healthy, or overfitting. On an overfitting run, name the step where the run should have stopped.

What’s provided

The house style

Every reply in the dataset does the same four things: opens with the subscriber’s first name, states the fact in one plain sentence, gives the next action, and closes with “Any trouble, just reply to this email.” Every detail a reply repeats appears in the message it is replying to, so the model is learning a shape rather than learning to invent facts. A style this mechanical is one you can grade by eye, which is what makes the Part B comparison readable without a judge model.

Your task

Implement two functions in src/plot_curves.py:

  1. render_svg(training, validation, title) — return an SVG string plotting both curves against step number, with a marker at the lowest validation point. _bounds(), _points() and _STYLE are written; this is assembly.
  2. verdict(training, validation) — return one short paragraph naming the picture:
    • underfitting: training loss barely moved from where it started;
    • overfitting: validation loss bottomed out early and climbed after, while training loss kept falling;
    • healthy: both fell together and validation has not turned up.

    On an overfitting run, say the step the validation low sits at. That is the best model the run produced, and where the epoch count should have ended.

Training loss alone always looks like progress, because a model can always fit its own training data harder. The validation curve is the one that tells you when that progress stopped being real, and verdict() is where you encode that.

Run it

The free path (no AWS account, no spend)

./scripts/test.sh              # runs your src/plot_curves.py
SRC=solution ./scripts/test.sh # runs the reference answer

Writes build/healthy.svg and build/overfit.svg from the shipped samples and prints a verdict for each.

Part A: train a model and fetch its curves (paid, hours)

./scripts/deploy.sh        # bucket, role, datasets uploaded. Cents.
./scripts/train.sh         # launches the job. Confirms first. Hours.
./scripts/fetch-metrics.sh # downloads the CSVs and plots them

Hyperparameters are environment variables, so you can rerun with a different shape:

EPOCH_COUNT=5 LEARNING_RATE=0.00005 ./scripts/train.sh

Amazon Nova Understanding models expose exactly three, with the defaults and ranges from the Bedrock user guide’s Custom model hyperparameters:

Hyperparameter (API) Min Max Default Script variable
epochCount 1 5 2 EPOCH_COUNT
learningRate 1e-6 1e-4 1e-5 LEARNING_RATE
learningRateWarmupSteps 0 100 10 LR_WARMUP_STEPS

There is no batchSize and no learningRateMultiplier on Nova. Those exist on other base model families (Anthropic Claude 3 exposes batchSize, learningRateMultiplier, earlyStoppingThreshold and earlyStoppingPatience; Cohere Command exposes batchSize and early stopping; Meta Llama exposes batchSize fixed at 1). The set of knobs is a property of the base model, not of fine-tuning, which is worth knowing before you go looking for a dial that is not there. Warmup is recommended at roughly dataset size divided by 640 for Nova Micro, so 300 records wants a very small number; the default is 2 here.

Part B: serve it and compare (optional, paid, gated)

./scripts/serve-and-compare.sh              # on-demand deployment, per token
MODE=provisioned ./scripts/serve-and-compare.sh  # 1 no-commitment PT model unit

It prints what it will cost, refuses to move until you type a confirmation, and deletes the serving capacity on exit, on Ctrl-C, and on error. It runs held-out messages through the base model and the custom model side by side, which is the comparison that decides whether the model ships. The loss curve says the run was healthy; only this says the model is better.

Teardown

./scripts/teardown.sh

Deletes deployments and Provisioned Throughputs first (they meter), then the custom model, then the bucket contents and the stack. The custom model is not a CloudFormation resource, so deleting the stack does not touch it; it goes through aws bedrock delete-custom-model --model-identifier <name-or-arn>, and teardown does that for you unless you pass KEEP_MODEL=1.

What success looks like

./scripts/test.sh writes two SVGs and prints two readings. The healthy run should read as healthy: both curves fall together and level off. The overfitting run should name its turning point, somewhere around step 320 in epoch 3, and say the epoch count should have ended near there. Open the two SVGs side by side; the shape difference is the whole lesson.

On Part A, fetch-metrics.sh should find both CSVs under the output prefix and plot your own run. Every job writes to the same output/ prefix under its own model-customization-job-<id>/ folder, so once you have trained twice there are two sets there. The script plots the job in build/job-arn.txt, the one you launched last, and prints which id it used; set JOB_ID=<id> to plot an earlier run instead.

If it fails

Reveal the solution

SRC=solution ./scripts/test.sh

What you just learned

Next

Lab 13 — Generate the week’s creative from the box manifest. Fine-tuning taught the model to sound like Greenbox; the next lab teaches the pipeline to draw like it, wiring Stable Image Core and Luma Ray 2 onto the weekly box manifest.