> ## 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.

# 用 PyTorch 训练

> 把一个按类别分好目录的图片文件夹训练成分类器：数据形态、提交、调参、换成你自己的模型

你有一堆按类别分好目录的图片，想训一个模型判断「这张属于哪一类」。三条命令：

```bash theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
tp new my-cnn --method pytorch/image-classification
tp dataset push my-images v1 <你的图片目录>
tp submit my-cnn --profile <卡型>:1 --train-dataset <你的用户名>/my-images@v1
```

原生 PyTorch **2.14.0**，独立运行时。它是平台上唯一属于 `supervised` 类别的方法：
带标注的输入/目标对、从初始化开始训练，spec 里没有任何 base model。

可运行的完整例子在示例项目 `tuneplane-examples` 的
`experiments/pytorch-imgcls_simple-cnn_digit-images_v1`。

## 数据长什么样

**目录名就是类名。** 没有标注文件，没有 CSV：

```text theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
train/scratch/0001.png      这张图的标签是 scratch
train/dent/0002.png         这张图的标签是 dent
train/ok/0003.png
val/scratch/0101.png        验证集，同样的类名
val/ok/0102.png
```

这就是 `torchvision.datasets.ImageFolder` 的约定。类别数写进 `config.yaml` 的
`num_classes`，**代码一行都不用改**。

`val/` 是必须的，而且要和 `train/` 没有重叠 —— 平台报的准确率是在这批图上算的，
掺了训练图这个数就没有意义了。

## 创建

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

得到 `train.py` 和 `config.yaml`，两个都归你。`train.py` 里只有 `build_model` 和
`build_datasets` 是留给你改的；其余部分是平台契约 —— 参数怎么传进来、只在 rank 0 上报、
产物写在哪。

## 配置

`config.yaml` 是平铺的，没有继承。每个键都由 recipe 声明，所以拼错、越界在
`tp validate` 就报，不必等排到 GPU：

```yaml theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
input_pipeline: imagefolder   # 读 <data_dir>/{train,val}/<类名>/*
num_classes: 3
arch: simple_cnn              # 或任意 torchvision.models 构造器，如 resnet18
epochs: 5
batch_size: 64                # 每进程；多卡时全局 batch = 该值 × 卡数
learning_rate: 1.0e-3
optimizer: adamw              # adamw | sgd
lr_scheduler: cosine          # constant | step | cosine
amp: true
```

`--set` 覆盖同样这些键，走同一份声明校验：

```bash theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
tp submit my-cnn --profile <卡型>:1 --set epochs=20 --set arch=resnet18
```

全部可调参数与取值范围：`tp methods pytorch/image-classification`。

<Note>
  `arch` 用 torchvision 架构（`resnet18` 等）时输入必须是三通道。单通道灰度图会被
  明确拒绝并告诉你怎么改，而不是在第一个卷积上抛一个只提张量形状的错误。
</Note>

## 数据从哪来

图片语料是一个**目录**，所以和文本框架不同，这个方法拿到的是挂载点而不是文件。两条路：

**平台数据集** —— 不可变、带版本，训练时从共享缓存读、不出网：

```bash theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
tp dataset push my-images v1 <目录>
tp submit my-cnn --profile <卡型>:1 --train-dataset <你的用户名>/my-images@v1
```

**Volume** —— 就地维护的目录，没有版本，只读挂载：

```bash theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
tp volume push <目录> --name my-images
tp submit my-cnn --profile <卡型>:1 --volume <你的用户名>/my-images
```

```yaml theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
data_dir: ${VOLUMES_DIR}/my-images   # 路径由平台定，写名字让它展开，不要硬编码
```

**怎么选**：会变的、一直在加的原始素材走 Volume；要复现、要说清「训的是哪一版」的走数据集。
详见[数据集与 Volume](/zh-Hans/guides/datasets)。

两个都不声明时：`input_pipeline: imagefolder` 会去读工作目录下的 `data/{train,val}`（本地调试用），
而 `mnist` / `cifar10` 会尝试联网下载 —— 笔记本上没问题，集群节点通常没有出网路由。正式跑请声明其中一条。

## 拉起

adapter 按服务端计费的拓扑编译出 `torchrun`。单卡就够用；多卡时 `batch_size` 是每进程的。
两台及以上会编译成带 rendezvous 的进程组，每台机器一个 launcher 进程 —— 实验里不需要读卡数，
`train.py` 启动时 `RANK` / `LOCAL_RANK` / `WORLD_SIZE` 已经就位。

```bash theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
tp submit my-cnn --profile <卡型>:1     # 单卡
tp submit my-cnn --profile <卡型>:8     # 单机 8 卡
```

多机需要 kuberay 或 slurm 后端，和这里其它走进程组的框架一样。

## 训练之后

* 控制台曲线：`train/loss`、`validation/loss`、`validation/accuracy`、`train/lr`
* `checkpoints/best.pt`，以及每次评估的 epoch 快照
* 准确率是在 `val/` 上算的 `预测对的张数 / 总张数`

这个方法**没有** `tp export` / `tp eval`，这是明确声明不是遗漏：分类器没有任何一种
HuggingFace 导出是有意义的，平台也没有给它打分的基准 harness。`tp submit --then export`
会在编译期被拒。

## 换成你自己的模型

改 `train.py` 的两个函数就行：

```python theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
def build_model(config, in_channels):
    """返回任意 nn.Module。"""

def build_datasets(config, data_dir, *, may_download=True):
    """返回 (train_set, val_set, in_channels)。"""
```

其余部分不要动 —— 那是让控制台画得出曲线、产物登记找得到权重的原因。

## 为什么不是 custom/custom

shell 脚本也能调 `torchrun`，差别在这四点：

|      | `custom/custom`              | `pytorch/image-classification` |
| ---- | ---------------------------- | ------------------------------ |
| 多机   | 一个容器、没有协调者，各训各的              | 真正的 rendezvous，由计费拓扑编译出来       |
| 超参   | `--set` 落不到任何地方              | 声明式，进队列前校验类型与区间                |
| 镜像   | 每次提交都要传 `--image`            | 平台镜像，按 `runtime_id` 解析         |
| 可观测性 | `external`，自带 URL 且镜像里要装 SDK | `platform`，开箱有曲线               |

训练器确实不是这个形状时才用 [custom](/zh-Hans/guides/custom-training)。

## 镜像（运维一次性）

`pytorch-2.14.0` 由部署侧构建，catalog 不内嵌地址：

```bash theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
./deploy/docker/build-runtimes.sh pytorch --push
```

然后在控制台填：**设置 → 运行时 → 镜像 → 「PyTorch 默认镜像」**，给个 tag 即可，保存立即生效。
`TUNEPLANE_IMAGE_PYTORCH` 是同一项的环境变量形态，作为控制台值的基线。没配的话提交会明确报
`runtime_id='pytorch-2.14.0' has no built-in OCI source`。
