> ## Documentation Index
> Fetch the complete documentation index at: https://docs.tuneplane.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Train an image classifier

> Turn a folder of labelled images into a classifier: the data shape, submitting, tuning, and swapping in your own model.

```bash theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
tp new my-cnn --method pytorch/image-classification
tp dataset push my-images v1 ./images
tp submit my-cnn --profile h100:1 --train-dataset alice/my-images@v1
```

A folder of images sorted into class directories becomes a model that says which class a
picture belongs to.

Native PyTorch at catalogue version `2.14.0`, its own runtime. It is the platform's only
`supervised` method: labelled pairs trained from initialisation, with no base model anywhere in
the spec.

## What the data looks like

The directory name is the class name. No label file, no CSV:

```text theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
train/scratch/0001.png      this image is labelled scratch
train/dent/0002.png
train/ok/0003.png
val/scratch/0101.png        validation set, same class names
val/ok/0102.png
```

That is the `torchvision.datasets.ImageFolder` convention. Put the class count in `config.yaml`
as `num_classes` and no code changes.

`val/` is required and must not overlap `train/`. The accuracy the platform reports is measured
on it, and mixing training images in makes that number mean nothing.

## Create

```bash theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
tp new my-cnn --method pytorch/image-classification
```

You get `train.py` and `config.yaml`, both yours. In `train.py` only `build_model` and
`build_datasets` are meant to be edited. The rest is the platform contract: how arguments
arrive, reporting from rank zero, where artifacts are written.

## Configure

`config.yaml` is flat, with no inheritance. Every key is declared by the recipe, so a typo or an
out-of-range value fails at `tp validate` rather than after a queue wait:

```yaml theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
input_pipeline: imagefolder   # reads <data_dir>/{train,val}/<class>/*
num_classes: 3
arch: simple_cnn              # or any torchvision.models constructor, e.g. resnet18
epochs: 5
batch_size: 64                # per process; global batch is this times the GPU count
learning_rate: 1.0e-3
optimizer: adamw              # adamw | sgd
lr_scheduler: cosine          # constant | step | cosine
amp: true
```

`--set` overrides the same keys against the same declaration:

```bash theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
tp submit my-cnn --profile h100:1 -s learning_rate=5e-4 -s epochs=10
```

## Get the images to the cluster

**A dataset**

Versioned and immutable. Use this when you need to say which version a run trained on.

```bash theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
tp dataset push my-images v1 ./images
tp submit my-cnn --profile h100:1 --train-dataset alice/my-images@v1
```

**A volume**

A directory maintained in place, with no versions, mounted read-only. Use this for material
that keeps changing and growing.

```bash theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
tp volume push ./images --name my-images
tp submit my-cnn --profile h100:1 --volume alice/my-images
```

```yaml theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
data_dir: ${VOLUMES_DIR}/my-images   # name it, never hard-code a path
```

Declare neither and `input_pipeline: imagefolder` reads `data/{train,val}` under the working
directory, which is for local debugging. `mnist` and `cifar10` try to download instead — fine
locally, usually no route out of a cluster node. Declare one of the two for a real run. See
[datasets](https://docs.tuneplane.ai/en/guides/datasets.md) and [volumes](https://docs.tuneplane.ai/en/guides/volumes.md).

## Scale up

```bash theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
tp submit my-cnn --profile h100:1     # one card
tp submit my-cnn --profile h100:8     # eight on one node
```

The adapter compiles `torchrun` from the topology the server assigned. With several cards
`batch_size` is per process. Two machines or more become a process group with a rendezvous, one
launcher per machine — your experiment never reads the GPU count, because `RANK`, `LOCAL_RANK`
and `WORLD_SIZE` are already set when `train.py` starts.

Multi-node needs a `node`, `kubernetes` or `slurm` Fleet, the same as every other process-group framework
here.

## Check it worked

The Charts tab shows `train/loss`, `validation/loss`, `validation/accuracy` and `train/lr`. The
accuracy is `correct / total` over `val/`. Artifacts hold `checkpoints/best.pt` plus a snapshot
of every evaluated epoch.

This method has no `tp export` and no `tp eval`, and that is stated rather than omitted: a
classifier has no HuggingFace export that would mean anything, and there is no benchmark harness
here to score it against. `tp submit --then export` is refused at compile time.

## Bring your own model

Two functions in `train.py`:

```python theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
def build_model(config, in_channels):
    """Return any nn.Module."""

def build_datasets(config, data_dir, *, may_download=True):
    """Return (train_set, val_set, in_channels)."""
```

Leave the rest alone. It is what lets the console draw the curves and the artifact registry find
your weights.

## Why not custom/custom

A shell script can call `torchrun` too, so the difference is worth naming:

| | `custom/custom` | `pytorch/image-classification` |
| - | - | - |
| Multi-node | One container, no coordinator. Each node trains alone | A real rendezvous, compiled from the assigned topology |
| Hyperparameters | `--set` reaches nothing | Declared, type- and range-checked before the queue |
| Image | `--image` on every submit | The platform's, resolved by `runtime_id` |
| Observability | `external`: bring a URL and install the SDK | `platform`: curves out of the box |

Reach for [custom training](https://docs.tuneplane.ai/en/guides/custom-training.md) when your trainer is genuinely not this
shape.

> **Publishing the image (an administrator does this once)**
>
> The catalogue embeds no address for `pytorch-2.14.0`. Build it and name it:
>
> ```bash theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
> ./deploy/docker/build-runtimes.sh pytorch --push
> ```
>
> Then **Settings → Runtime → Images → Default PyTorch image**. A tag is enough and it takes
> effect without a restart. `TUNEPLANE_IMAGE_PYTORCH` sets the same field from the environment
> and is the baseline a saved value overrides.
>
> Without it, submission fails with `runtime_id='pytorch-2.14.0' has no built-in OCI source`.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.