搭建自己的迷你框架 本节摘要:你已经造好了神经元、层、网络、反向传播、激活函数、损失函数、优化器、正则化、初始化、学习率调度——全是散落的零件。现在把它们焊成一个框架。不是 PyTorch,不是 TensorFlow,是你的。前 9 节里,这些积木散落在不同文件:这边一个 类,那边一个训练循环,权重初始化又在另一个文件,学习率调度还在别处。要训一个网络,你得从五节里复制粘贴、手工连线。框架解决的就是这件事:PyTorch 给你 、 、 、 和一个把它们串起来的训练循环模式;TensorFlow 给你 、 、 。它们不是魔法,只是让你不必每次重造管道的组织模式。
本节摘要:你已经造好了神经元、层、网络、反向传播、激活函数、损失函数、优化器、正则化、初始化、学习率调度——全是散落的零件。现在把它们焊成一个框架。不是 PyTorch,不是 TensorFlow,是你的。前 9 节里,这些积木散落在不同文件:这边一个
Value类,那边一个训练循环,权重初始化又在另一个文件,学习率调度还在别处。要训一个网络,你得从五节里复制粘贴、手工连线。框架解决的就是这件事:PyTorch 给你nn.Module、nn.Sequential、optim.Adam、DataLoader和一个把它们串起来的训练循环模式;TensorFlow 给你keras.Layer、keras.Sequential、keras.optimizers.Adam。它们不是魔法,只是让你不必每次重造管道的组织模式。本节你将用约 500 行 Python(不用 numpy、无外部依赖)造出同样的东西——一个能定义任意前馈网络、用 SGD 或 Adam 训练、批数据、施加 dropout 与批归一化、用任意激活、调度学习率的框架。完成后,你会精确理解写下model = nn.Sequential(...)时发生了什么、model.train()与model.eval()为何存在、optimizer.zero_grad()为何是独立调用——因为你全都亲手造过。
阅读完本节,你应当能够:
你有 9 节课的搭建积木,散落在不同文件:这边一个 Value 类,那边一个训练循环,权重初始化又在另一个文件,学习率调度还在别处。要训一个网络,你得从五节里复制粘贴、手工连线。
框架解决的就是这件事。PyTorch 给你 nn.Module、nn.Sequential、optim.Adam、DataLoader 和一个把它们串起来的训练循环模式;TensorFlow 给你 keras.Layer、keras.Sequential、keras.optimizers.Adam。它们不是魔法,只是让你不必每次重造管道的组织模式。
你将用约 500 行 Python 造出同样的东西——不用 numpy、无外部依赖。一个能定义任意前馈网络、用 SGD 或 Adam 训练、批数据、施加 dropout 与批归一化、用任意激活、调度学习率的框架。完成后,你会精确理解写下 model = nn.Sequential(...) 时发生了什么,理解 model.train() 与 model.eval() 为何存在,理解 optimizer.zero_grad() 为何是独立调用——因为你全都亲手造过。
PyTorch 里每个层都继承自 nn.Module。一个 Module 有三项职责:
Linear 层是 Module,ReLU 激活是 Module,Dropout 层是 Module,BatchNorm 层是 Module,它们都有同一套接口。
nn.Sequential 把 Module 串起来。前向:数据依次过 Module 1、Module 2、Module 3;反向:倒过来。容器本身也是 Module——它有 forward()、parameters()、backward()。这就是组合模式:一串 Module 本身也是 Module。
Dropout 训练时随机置零神经元,评估时全部放行;BatchNorm 训练时用批统计,评估时用滑动平均。train() 与 eval() 切换这种行为,每个 Module 都有一个 training 标志。
优化器用梯度更新参数:SGD 是 param -= lr * grad,Adam 维护动量与方差估计再更新。优化器不关心网络架构,它只看到一个扁平的参数列表和它们的梯度。
批处理重要有两个原因:一是大问题整数据集放不进内存,二是小批量梯度下降提供有助于跳出局部最小的噪声。DataLoader 把数据切成 batch,可选地在轮次间打乱。
完整代码见原课程 phases/03-deep-learning-core/10-mini-framework/code/ 相应文件。下面给出关键骨架。
每个层都实现的抽象接口。
class Module: def __init__(self): self.training = True def forward(self, x): raise NotImplementedError def backward(self, grad): raise NotImplementedError def parameters(self): return [] def train(self): self.training = True def eval(self): self.training = False
基本积木:存权重和偏置,前向算 Wx + b,反向算权重/输入梯度。权重用 Kaiming 初始化(std = sqrt(2/fan_in)),梯度缓存为 2D/1D 列表以便优化器更新。
ReLU、Sigmoid、Tanh 都做成 Module,各自缓存反向所需信息(ReLU 缓存掩码、Sigmoid/Tanh 缓存输出)。
class ReLU(Module): def forward(self, x): self.mask = [1.0 if v > 0 else 0.0 for v in x] return [max(0.0, v) for v in x] def backward(self, grad): return [g * m for g, m in zip(grad, self.mask)]
训练时随机置零,剩余元素乘 1/(1−p) 保持期望不变,评估时不动。掩码同时用于反向。
按特征跨批归一化为零均值单位方差,维护推理用的滑动统计,gamma/beta 可学习。训练用当前批统计并更新滑动均值/方差,推理用滑动统计。
串联模块:前向从左到右,反向从右到左。parameters() 收集所有子模块参数,train()/eval() 递归切换全部子模块的 training 标志。
class Sequential(Module): def __init__(self, *modules): super().__init__() self.modules = list(modules) def forward(self, x): for module in self.modules: x = module.forward(x) return x def backward(self, grad): for module in reversed(self.modules): grad = module.backward(grad) return grad def train(self): self.training = True for module in self.modules: module.train() def eval(self): self.training = False for module in self.modules: module.eval()
MSE 与二分类交叉熵,各自 __call__ 返回损失值,backward() 返回对预测的梯度。
两者都接收参数列表、用梯度更新权重。step() 按规则更新,zero_grad() 清零缓存梯度。
class Adam: def __init__(self, parameters, lr=0.001, beta1=0.9, beta2=0.999, eps=1e-8): self.params = parameters self.lr = lr self.beta1, self.beta2, self.eps = beta1, beta2, eps self.t = 0 self.m = [0.0] * len(parameters) self.v = [0.0] * len(parameters) def step(self): self.t += 1 for idx, (container, i, j, grad_container) in enumerate(self.params): g = grad_container[i][j] if j is not None else grad_container[i] self.m[idx] = self.beta1 * self.m[idx] + (1 - self.beta1) * g self.v[idx] = self.beta2 * self.v[idx] + (1 - self.beta2) * g * g m_hat = self.m[idx] / (1 - self.beta1 ** self.t) v_hat = self.v[idx] / (1 - self.beta2 ** self.t) update = self.lr * m_hat / (math.sqrt(v_hat) + self.eps) if j is not None: container[i][j] -= update else: container[i] -= update
把数据切成 batch,可选每轮打乱。
class DataLoader: def __init__(self, data, batch_size=32, shuffle=True): self.data = data self.batch_size = batch_size self.shuffle = shuffle def __iter__(self): indices = list(range(len(self.data))) if self.shuffle: random.shuffle(indices) for start in range(0, len(indices), self.batch_size): batch = [self.data[i] for i in indices[start:start + self.batch_size]] yield [item[0] for item in batch], [item[1] for item in batch]
把一切接起来:定义模型、选损失、选优化器、跑训练循环。
def train(): random.seed(42) model = Sequential( Linear(2, 16), ReLU(), Linear(16, 16), ReLU(), Linear(16, 8), ReLU(), Linear(8, 1), Sigmoid(), ) criterion = BCELoss() optimizer = Adam(model.parameters(), lr=0.01) data = make_circle_data(500) split = int(len(data) * 0.8) train_data, test_data = data[:split], data[split:] loader = DataLoader(train_data, batch_size=16, shuffle=True) model.train() for epoch in range(100): for batch_inputs, batch_targets in loader: for x, t in zip(batch_inputs, batch_targets): pred = model.forward(x) loss = criterion(pred, t) optimizer.zero_grad() grad = criterion.backward() model.backward(grad) optimizer.step() model.eval() # ... 评估测试精度 ...
PyTorch 的等价实现:
import torch import torch.nn as nn from torch.utils.data import DataLoader, TensorDataset model = nn.Sequential( nn.Linear(2, 16), nn.ReLU(), nn.Linear(16, 16), nn.ReLU(), nn.Linear(16, 8), nn.ReLU(), nn.Linear(8, 1), nn.Sigmoid(), ) criterion = nn.BCELoss() optimizer = torch.optim.Adam(model.parameters(), lr=0.01) for epoch in range(100): model.train() for inputs, targets in dataloader: optimizer.zero_grad() predictions = model(inputs) loss = criterion(predictions, targets) loss.backward() optimizer.step() model.eval() with torch.no_grad(): test_predictions = model(test_inputs)
结构一模一样:Sequential、Linear、ReLU、Sigmoid、BCELoss、Adam、zero_grad、backward、step、train、eval。每个概念一一对应。差别在于 PyTorch 自动处理 autograd(不必在每个模块里手写 backward())、跑在 GPU 上、并被优化了多年——但骨头相同。现在你看 PyTorch 代码,每一行都知道在发生什么。这份理解,正是本节全部的意义。
本节产出(位于原课程 outputs/):
prompt-framework-architect.md:一个提示,用框架抽象帮你设计神经网络架构。SoftmaxCrossEntropyLoss 类用于多分类——softmax 预测、算交叉熵、处理合并的反向传播,在 3 类螺旋数据上测试。set_lr() 方法,接上第 09 节的余弦调度,在圆形分类上比较「预热+余弦」与常数 LR。save() 与 load(),把所有权重序列化到 JSON 并读回,验证加载的模型预测与原模型一致。param -= lr × grad,Adam 维护矩估计。下一节,我们正式进入 PyTorch——把你亲手造的引擎换成大家真正在开的那台,讲清张量、autograd、nn.Module 与标准训练循环。