搭建自己的迷你框架


文档摘要

搭建自己的迷你框架 本节摘要:你已经造好了神经元、层、网络、反向传播、激活函数、损失函数、优化器、正则化、初始化、学习率调度——全是散落的零件。现在把它们焊成一个框架。不是 PyTorch,不是 TensorFlow,是你的。前 9 节里,这些积木散落在不同文件:这边一个 类,那边一个训练循环,权重初始化又在另一个文件,学习率调度还在别处。要训一个网络,你得从五节里复制粘贴、手工连线。框架解决的就是这件事:PyTorch 给你 、 、 、 和一个把它们串起来的训练循环模式;TensorFlow 给你 、 、 。它们不是魔法,只是让你不必每次重造管道的组织模式。

搭建自己的迷你框架

本节摘要:你已经造好了神经元、层、网络、反向传播、激活函数、损失函数、优化器、正则化、初始化、学习率调度——全是散落的零件。现在把它们焊成一个框架。不是 PyTorch,不是 TensorFlow,是你的。前 9 节里,这些积木散落在不同文件:这边一个 Value 类,那边一个训练循环,权重初始化又在另一个文件,学习率调度还在别处。要训一个网络,你得从五节里复制粘贴、手工连线。框架解决的就是这件事:PyTorch 给你 nn.Modulenn.Sequentialoptim.AdamDataLoader 和一个把它们串起来的训练循环模式;TensorFlow 给你 keras.Layerkeras.Sequentialkeras.optimizers.Adam。它们不是魔法,只是让你不必每次重造管道的组织模式。本节你将用约 500 行 Python(不用 numpy、无外部依赖)造出同样的东西——一个能定义任意前馈网络、用 SGD 或 Adam 训练、批数据、施加 dropout 与批归一化、用任意激活、调度学习率的框架。完成后,你会精确理解写下 model = nn.Sequential(...) 时发生了什么、model.train()model.eval() 为何存在、optimizer.zero_grad() 为何是独立调用——因为你全都亲手造过。

学习目标

阅读完本节,你应当能够:

  1. 搭建一个完整的深度学习框架(约 500 行),含 Module、Linear、ReLU、Sigmoid、Dropout、BatchNorm、Sequential、损失函数、优化器、DataLoader。
  2. 解释 Module 抽象(forward、backward、parameters)以及为何需要 train/eval 模式切换。
  3. 把全部零件接进一个可工作的训练循环,在圆形分类上训练一个 4 层网络
  4. 把你框架的每个组件映射到 PyTorch 等价物(nn.Module、nn.Sequential、optim.Adam、DataLoader)。

一、问题与直觉

你有 9 节课的搭建积木,散落在不同文件:这边一个 Value 类,那边一个训练循环,权重初始化又在另一个文件,学习率调度还在别处。要训一个网络,你得从五节里复制粘贴、手工连线。

框架解决的就是这件事。PyTorch 给你 nn.Modulenn.Sequentialoptim.AdamDataLoader 和一个把它们串起来的训练循环模式;TensorFlow 给你 keras.Layerkeras.Sequentialkeras.optimizers.Adam。它们不是魔法,只是让你不必每次重造管道的组织模式。

你将用约 500 行 Python 造出同样的东西——不用 numpy、无外部依赖。一个能定义任意前馈网络、用 SGD 或 Adam 训练、批数据、施加 dropout 与批归一化、用任意激活、调度学习率的框架。完成后,你会精确理解写下 model = nn.Sequential(...) 时发生了什么,理解 model.train()model.eval() 为何存在,理解 optimizer.zero_grad() 为何是独立调用——因为你全都亲手造过。

Module 抽象

PyTorch 里每个层都继承自 nn.Module。一个 Module 有三项职责:

  1. forward()——给定输入算输出;
  2. parameters()——返回所有可训练权重;
  3. backward()——算梯度(PyTorch 由 autograd 接管,我们的需手写)。

Linear 层是 Module,ReLU 激活是 Module,Dropout 层是 Module,BatchNorm 层是 Module,它们都有同一套接口。

Sequential 容器

nn.Sequential 把 Module 串起来。前向:数据依次过 Module 1、Module 2、Module 3;反向:倒过来。容器本身也是 Module——它有 forward()、parameters()、backward()。这就是组合模式:一串 Module 本身也是 Module。

训练 vs 评估模式

Dropout 训练时随机置零神经元,评估时全部放行;BatchNorm 训练时用批统计,评估时用滑动平均。train()eval() 切换这种行为,每个 Module 都有一个 training 标志。

优化器

优化器用梯度更新参数:SGD 是 param -= lr * grad,Adam 维护动量与方差估计再更新。优化器不关心网络架构,它只看到一个扁平的参数列表和它们的梯度。

DataLoader

批处理重要有两个原因:一是大问题整数据集放不进内存,二是小批量梯度下降提供有助于跳出局部最小的噪声。DataLoader 把数据切成 batch,可选地在轮次间打乱。

框架架构

训练循环

Module 层次

二、从零实现

完整代码见原课程 phases/03-deep-learning-core/10-mini-framework/code/ 相应文件。下面给出关键骨架。

Step 1:Module 基类

每个层都实现的抽象接口。

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

Step 2:Linear 层

基本积木:存权重和偏置,前向算 Wx + b,反向算权重/输入梯度。权重用 Kaiming 初始化(std = sqrt(2/fan_in)),梯度缓存为 2D/1D 列表以便优化器更新。

Step 3:激活 Module

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)]

Step 4:Dropout Module

训练时随机置零,剩余元素乘 1/(1−p) 保持期望不变,评估时不动。掩码同时用于反向。

Step 5:BatchNorm Module

按特征跨批归一化为零均值单位方差,维护推理用的滑动统计,gamma/beta 可学习。训练用当前批统计并更新滑动均值/方差,推理用滑动统计。

Step 6:Sequential 容器

串联模块:前向从左到右,反向从右到左。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()

Step 7:损失函数

MSE 与二分类交叉熵,各自 __call__ 返回损失值,backward() 返回对预测的梯度。

Step 8:SGD 与 Adam 优化器

两者都接收参数列表、用梯度更新权重。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

Step 9:DataLoader

把数据切成 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]

Step 10:在圆形分类上训练一个 4 层网络

把一切接起来:定义模型、选损失、选优化器、跑训练循环。

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)

结构一模一样:SequentialLinearReLUSigmoidBCELossAdamzero_gradbackwardsteptraineval。每个概念一一对应。差别在于 PyTorch 自动处理 autograd(不必在每个模块里手写 backward())、跑在 GPU 上、并被优化了多年——但骨头相同。现在你看 PyTorch 代码,每一行都知道在发生什么。这份理解,正是本节全部的意义。

四、可复用产物

本节产出(位于原课程 outputs/):

  • prompt-framework-architect.md:一个提示,用框架抽象帮你设计神经网络架构。

五、练习

  1. Easy:加一个 SoftmaxCrossEntropyLoss 类用于多分类——softmax 预测、算交叉熵、处理合并的反向传播,在 3 类螺旋数据上测试。
  2. Medium:在优化器里实现学习率调度——加一个 set_lr() 方法,接上第 09 节的余弦调度,在圆形分类上比较「预热+余弦」与常数 LR。
  3. Hard:给 Sequential 加 save()load(),把所有权重序列化到 JSON 并读回,验证加载的模型预测与原模型一致。

本节要点回顾

  1. 框架是组织模式而非魔法:把散落的零件焊成可复用的管道,PyTorch/TensorFlow 给的就是这种组织。
  2. Module 抽象三职责:forward() 算输出、parameters() 返可训练权重、backward() 算梯度,所有层共享同一接口。
  3. Sequential 是组合模式:一串 Module 本身也是 Module,前向从左到右、反向从右到左。
  4. train/eval 模式切换必需:Dropout 与 BatchNorm 在两种模式下行为不同,每个 Module 都带 training 标志,Sequential 递归切换。
  5. 优化器只见扁平参数列表:不关心架构,SGD 是 param -= lr × grad,Adam 维护矩估计。
  6. DataLoader 切批 + 打乱:小批量噪声助跳出局部最小,大问题整数据集放不进内存。
  7. 训练循环五步:zero_grad → forward → loss → backward → step,这个顺序要背下来。
  8. 你的框架与 PyTorch 一一对应:每个概念都能映射,差别只在 autograd 自动化、GPU 加速、多年优化——骨头完全相同。

下一节,我们正式进入 PyTorch——把你亲手造的引擎换成大家真正在开的那台,讲清张量、autograd、nn.Module 与标准训练循环。


发布者: 作者: Rohit Gupta 转发
评论区 (0)
U