Week 09:PyTorch 基础——从 Tensor 到完整训练循环

公開日: 2026-07-26 09:00 3822文字 20 min read

この投稿は「日本語」では表示できません。元の投稿を表示しています。
Week 09:PyTorch 基础——从 Tensor 到完整训练循环。fish-first 终端教学,面向 CachyOS、VS Code、uv 和 Python 学习路线。
Oh My Pi / weekly tutorial / week09-pytorch-basics
miku@cachyos:~/Code/python-learning$ omp teach week09-pytorch-basics --fish-first --step-by-step
source516 行教学文档
weekWeek 09
shellfish-first 命令版
backlink20 周计划

Week 09:PyTorch 基础——从 Tensor 到完整训练循环

回到总路线:USTC 统计 AI / 量化 20 周成长计划
本周目标对应计划中的 Week 09:Tensor、Dataset / DataLoader、nn.Module、loss、optimizer、train / eval loop。

本教程默认你在 CachyOS + fish shell + VS Code + uv 环境下学习。所有命令默认都是 fish;如果你使用 Bash / Zsh,需要自己换成对应激活脚本。

0. 本周详细教学:语法、规范、验收

本节不是追加在尾部的复习,而是本周正文的入口。先读这里,再做后面的命令和项目。

0.1 本周真正要学会什么

维度要求
知识点Tensor、Dataset、DataLoader、nn.Module、loss、optimizer
代码语法能从空文件写出本周核心脚本,而不是只复制运行
程序规范函数拆分、路径清楚、输入输出明确、错误能解释
交付物src/pytorch_mlp.py
验收方式从 fish 终端运行命令,得到可复查的文件或指标

0.2 代码语法精讲

下面的代码不是最终答案,而是本周必须理解的最小骨架:

omppython
for epoch in range(epochs):
    model.train()
    for xb, yb in train_loader:
        logits = model(xb)
        loss = criterion(logits, yb)
        optimizer.zero_grad()
        loss.backward()
        optimizer.step()

plain model.eval() with torch.no_grad(): valid_logits = model(X_valid_tensor)

读代码时按四步检查:输入从哪里来;中间变量的类型和 shape 是什么;函数或脚本输出什么;哪些错误应该显式报出来。

0.3 本周程序规范

  • 所有路径用相对路径或 `pathlib.Path`,不要写死 `/home/miku/...`。
  • 核心逻辑进 `src/`,notebook 只做探索和解释。
  • 每个脚本能从 fish 终端运行,并在 README 写出命令。
  • 输出必须落盘到 `reports/`、`figures/` 或 `outputs/`,不能只在屏幕上看。

0.4 本周练习分层

层级任务不合格表现合格验收
最小练习手写上面的最小骨架只在 notebook 里运行终端运行成功
标准练习把逻辑拆成函数/模块一个大脚本从头写到尾至少 2 个函数,职责清楚
项目练习生成本周交付物 src/pytorch_mlp.py只有屏幕输出文件落盘,可复查
复盘练习写 3 个错误和修复只写“已解决”写清报错、原因、修复、预防

0.5 本周和主线的连接

1. 本周目标

完成后你应该能做到:

  1. 知道 PyTorch 的 Tensor 和 NumPy array 有什么相似与不同。
  2. 会把表格数据包装成 Dataset,再交给 DataLoader 分批训练。
  3. 会写一个最小但完整的 nn.Module
  4. 会写 CPU-first 的训练循环:model.train()、前向传播、loss、反向传播、optimizer step。
  5. 会写验证循环:model.eval()torch.no_grad()、accuracy。
  6. 留下可运行的 src/pytorch_mlp.py 和学习笔记。

本周不追求高精度,也不追求 GPU。重点是把深度学习训练的骨架写明白。

2. 前置条件

你需要已经具备:

  • VS Code 能打开项目目录。
  • 终端默认 shell 是 fish。
  • uv 可用。
  • 会运行普通 Python 脚本。
  • 知道监督学习中的 train / validation split。

先确认工具:

ompfish
python --version
uv --version
fish --version

如果这些命令都能输出版本号,就可以继续。

3. 建立 Week 09 项目

建议每周一个独立目录,避免依赖混在一起。

ompfish
mkdir -p ~/Code/ustc-ai/week09-pytorch-basics
cd ~/Code/ustc-ai/week09-pytorch-basics
uv init --name week09-pytorch-basics
uv venv
source .venv/bin/activate.fish
uv add torch scikit-learn matplotlib
code .

逐句解释:

命令 作用
mkdir -p 创建项目目录;如果上级目录不存在就一起创建
cd 进入本周项目目录
uv init 生成 pyproject.toml 等项目文件
uv venv 创建当前项目专用的 .venv
source .venv/bin/activate.fish 用 fish 方式激活虚拟环境
uv add ... 安装本周需要的依赖
code . 用 VS Code 打开当前目录

检查 PyTorch 是否可用:

ompfish
python -c "import torch; print(torch.__version__); print('cuda:', torch.cuda.is_available())"

在 CachyOS 笔记本上,cuda: False 很正常。本教程所有代码都按 CPU 设计。

4. 推荐文件布局

创建本周目录结构:

ompfish
mkdir -p src notes outputs

目标布局:

ompprompt
week09-pytorch-basics/
├── .venv/
├── pyproject.toml
├── src/
│   ├── tensor_basics.py
│   └── pytorch_mlp.py
├── notes/
│   └── week09_pytorch.md
└── outputs/

文件用途:

文件 用途
src/tensor_basics.py 练习 Tensor、shape、dtype、autograd
src/pytorch_mlp.py 本周核心:Dataset、DataLoader、MLP、训练循环
notes/week09_pytorch.md 记录你理解的训练流程和错误
outputs/ 以后放日志、图、模型文件

5. Tensor 入门:先理解数据容器

在 VS Code 中创建 src/tensor_basics.py

omppython
import torch

x = torch.tensor([[1.0, 2.0], [3.0, 4.0]]) y = torch.ones((2, 2))

print(“x =”) print(x) print(“shape:”, x.shape) print(“dtype:”, x.dtype) print(“x + y =”) print(x + y) print(“x @ y =”) print(x @ y)

autograd: PyTorch 自动记录计算图,用于反向传播

w = torch.tensor([2.0], requires_grad=True) b = torch.tensor([1.0], requires_grad=True) feature = torch.tensor([3.0]) target = torch.tensor([10.0])

prediction = w * feature + b loss = (prediction - target).pow(2).mean() loss.backward()

print(“prediction:”, prediction.item()) print(“loss:”, loss.item()) print(“d loss / d w:”, w.grad.item()) print(“d loss / d b:”, b.grad.item())

运行:

ompfish
source .venv/bin/activate.fish
python src/tensor_basics.py

你需要重点观察:

  • shape:深度学习代码里最常见的错误来源。
  • dtype:模型通常使用 float32,标签分类通常使用 long
  • requires_grad=True:告诉 PyTorch 这个 Tensor 需要梯度。
  • loss.backward():从 loss 反向计算所有参数的梯度。

6. Dataset 和 DataLoader:把数据交给训练循环

PyTorch 不希望你在训练循环里手动切片全部数据。更常见的结构是:

ompprompt
原始数据 -> Dataset -> DataLoader -> 每个 batch 的 x, y -> model

概念区分:

名称 作用
Dataset 定义“第 i 条样本怎么取”
DataLoader 负责 batch、shuffle、迭代
batch_size 每次给模型多少条样本
shuffle=True 每个 epoch 打乱训练样本顺序

7. 写一个完整的 CPU-first MLP

创建 src/pytorch_mlp.py

omppython
from __future__ import annotations

import argparse import random from dataclasses import dataclass

import numpy as np import torch from sklearn.datasets import make_classification from sklearn.model_selection import train_test_split from sklearn.preprocessing import StandardScaler from torch import nn from torch.utils.data import DataLoader, Dataset

@dataclass(frozen=True) class TrainConfig: epochs: int batch_size: int lr: float seed: int

class TabularClassificationDataset(Dataset): def init(self, features: np.ndarray, labels: np.ndarray) -> None: self.features = torch.tensor(features, dtype=torch.float32) self.labels = torch.tensor(labels, dtype=torch.long)

plain def len(self) -> int: return len(self.labels)

plain def getitem(self, index: int) -> tuple[torch.Tensor, torch.Tensor]: return self.features[index], self.labels[index]

class MLP(nn.Module): def init(self, input_dim: int, hidden_dim: int, num_classes: int) -> None: super().init() self.net = nn.Sequential( nn.Linear(input_dim, hidden_dim), nn.ReLU(), nn.Linear(hidden_dim, hidden_dim), nn.ReLU(), nn.Linear(hidden_dim, num_classes), )

plain def forward(self, x: torch.Tensor) -> torch.Tensor: return self.net(x)

def set_seed(seed: int) -> None: random.seed(seed) np.random.seed(seed) torch.manual_seed(seed)

def build_loaders(config: TrainConfig) -> tuple[DataLoader, DataLoader, int, int]: x, y = make_classification( n_samples=1200, n_features=20, n_informative=10, n_redundant=4, n_classes=2, random_state=config.seed, ) x_train, x_valid, y_train, y_valid = train_test_split( x, y, test_size=0.2, stratify=y, random_state=config.seed, )

plain scaler = StandardScaler() x_train = scaler.fit_transform(x_train) x_valid = scaler.transform(x_valid)

plain train_dataset = TabularClassificationDataset(x_train, y_train) valid_dataset = TabularClassificationDataset(x_valid, y_valid)

plain train_loader = DataLoader( train_dataset, batch_size=config.batch_size, shuffle=True, ) valid_loader = DataLoader( valid_dataset, batch_size=config.batch_size, shuffle=False, ) return train_loader, valid_loader, x_train.shape[1], len(np.unique(y))

def train_one_epoch( model: nn.Module, loader: DataLoader, criterion: nn.Module, optimizer: torch.optim.Optimizer, device: torch.device, ) -> float: model.train() total_loss = 0.0 total_examples = 0

plain for features, labels in loader: features = features.to(device) labels = labels.to(device)

plain logits = model(features) loss = criterion(logits, labels)

plain optimizer.zero_grad() loss.backward() optimizer.step()

    total_loss += loss.item() * len(labels)
    total_examples += len(labels)

return total_loss / total_examples

def evaluate(model: nn.Module, loader: DataLoader, criterion: nn.Module, device: torch.device) -> tuple[float, float]: model.eval() total_loss = 0.0 total_correct = 0 total_examples = 0

plain with torch.no_grad(): for features, labels in loader: features = features.to(device) labels = labels.to(device)

plain logits = model(features) loss = criterion(logits, labels) predictions = logits.argmax(dim=1)

plain total_loss += loss.item() * len(labels) total_correct += (predictions == labels).sum().item() total_examples += len(labels)

return total_loss / total_examples, total_correct / total_examples

def parse_args() -> TrainConfig: parser = argparse.ArgumentParser() parser.add_argument(“—epochs”, type=int, default=20) parser.add_argument(“—batch-size”, type=int, default=32) parser.add_argument(“—lr”, type=float, default=0.01) parser.add_argument(“—seed”, type=int, default=42) args = parser.parse_args() return TrainConfig( epochs=args.epochs, batch_size=args.batch_size, lr=args.lr, seed=args.seed, )

def main() -> None: config = parse_args() set_seed(config.seed)

plain device = torch.device(“cpu”) train_loader, valid_loader, input_dim, num_classes = build_loaders(config) model = MLP(input_dim=input_dim, hidden_dim=32, num_classes=num_classes).to(device) criterion = nn.CrossEntropyLoss() optimizer = torch.optim.Adam(model.parameters(), lr=config.lr)

plain for epoch in range(1, config.epochs + 1): train_loss = train_one_epoch(model, train_loader, criterion, optimizer, device) valid_loss, valid_acc = evaluate(model, valid_loader, criterion, device) print( f”epoch={epoch

} ” f”train_loss={train_loss:.4f} ” f”valid_loss={valid_loss:.4f} ” f”valid_acc={valid_acc:.3f}” )

if name == “main”: main()

运行一个短训练:

ompfish
source .venv/bin/activate.fish
python src/pytorch_mlp.py --epochs 5 --batch-size 32 --lr 0.01

如果你看到类似下面的输出,说明训练循环已经跑通:

ompprompt
epoch=01 train_loss=0.6200 valid_loss=0.5100 valid_acc=0.760
epoch=02 train_loss=0.4200 valid_loss=0.3500 valid_acc=0.850

数字不需要完全一致,因为初始化和环境可能不同。你要看的是:脚本能运行,loss 大体下降,验证集 accuracy 高于随机猜测。

8. 训练循环逐句解释

核心结构是:

omppython
for epoch in range(num_epochs):
    model.train()
    for x, y in train_loader:
        pred = model(x)
        loss = criterion(pred, y)
        optimizer.zero_grad()
        loss.backward()
        optimizer.step()

逐句解释:

代码 含义
model.train() 切换到训练模式;以后遇到 Dropout / BatchNorm 时很重要
for x, y in train_loader 从 DataLoader 取一个 batch
pred = model(x) 前向传播,得到 logits 或预测值
loss = criterion(pred, y) 计算当前 batch 的损失
optimizer.zero_grad() 清空上一轮梯度;否则梯度会累加
loss.backward() 反向传播,计算每个参数的梯度
optimizer.step() 根据梯度更新参数

验证循环必须不同:

omppython
model.eval()
with torch.no_grad():
    ...

原因:验证时不更新参数,也不需要保存计算图。这样更快,也更不容易写错。

9. 写学习笔记

创建 notes/week09_pytorch.md,至少回答这些问题:

ompprompt
# Week 09 PyTorch Notes

Tensor

  • Tensor 和 NumPy array 的共同点:
  • Tensor 多出来的重要能力:

Dataset / DataLoader

  • Dataset 负责:
  • DataLoader 负责:

Train Loop

  • 为什么要 zero_grad:
  • 为什么 train 和 eval 要分开:

Errors I Met

  • 错误信息:
  • 原因:
  • 修复方式:

10. 练习

  1. hidden_dim=32 改成 hidden_dim=64,观察验证集 accuracy 是否变化。
  2. lr=0.01 改成 lr=0.001,比较 loss 下降速度。
  3. batch_size=32 改成 batch_size=128,观察每个 epoch 是否更快。
  4. MLP 里增加一层 nn.Dropout(0.2),再比较训练集和验证集表现。
  5. 故意删掉 optimizer.zero_grad(),观察训练是否变得不稳定,然后恢复。

每次只改一个因素,否则你无法判断到底是什么导致结果变化。

11. 验收检查

在项目根目录执行:

ompfish
source .venv/bin/activate.fish
test -f src/pytorch_mlp.py; and test -f notes/week09_pytorch.md
python src/tensor_basics.py
python src/pytorch_mlp.py --epochs 3 --batch-size 32 --lr 0.01

通过标准:

  • tensor_basics.py 能打印 Tensor、shape、dtype 和梯度。
  • pytorch_mlp.py 能完成至少 3 个 epoch。
  • 输出中包含 train_lossvalid_lossvalid_acc
  • notes/week09_pytorch.md 里有你自己的解释,而不是只复制代码。

12. 常见错误

12.1 fish 激活脚本用错

错误做法是执行 Bash / Zsh 版激活脚本。fish 里应该执行:

ompfish
source .venv/bin/activate.fish

12.2 标签 dtype 不对

nn.CrossEntropyLoss() 要求分类标签是整数类别,并且 dtype 通常是 torch.long。如果你把标签做成 float32,可能会报 dtype 错误。

12.3 忘记 optimizer.zero_grad()

PyTorch 默认会累加梯度。训练循环里每个 batch 更新前都要清空上一轮梯度。

12.4 训练和验证没有分开

训练时用:

omppython
model.train()

验证时用:

omppython
model.eval()
with torch.no_grad():
    ...

不要在验证集上调用 optimizer.step()

12.5 shape 对不上

如果看到矩阵乘法 shape 错误,先打印:

omppython
print(features.shape)
print(logits.shape)
print(labels.shape)

分类任务中,logits 常见 shape 是 [batch_size, num_classes]labels 常见 shape 是 [batch_size]

13. 下一步

进入 Week 10:把这个最小训练脚本升级成一个小型深度学习项目。你会加入 MLP / CNN baseline、checkpoint、loss 曲线、验证集评估和独立的 evaluate.py

plain

気に入ったならばコメントを残してくださいね~

© 2026 江无没有月 @miku
Powered by theme astro-koharu · Inspired by Shoka