Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Python 前端

引言

前面的章节已经在 C++ 中实现了张量、自动微分、算子和参数更新。这些能力适合承担数值计算,却不一定适合直接描述模型和训练流程。C++ 需要显式处理类型、所有权和编译过程;Python 则更适合快速组合网络层、准备输入并控制训练循环。

训练代码经常需要反复调整模型结构、损失函数、学习率和数据处理方式。如果这些变化都写在 C++ 中,每次修改后都要重新编译,再运行程序观察结果。Python 可以直接执行新的模型与训练逻辑,也便于在运行过程中打印张量形状、损失和梯度,缩短发现问题、修改代码和重新验证之间的周期。只有底层算子或绑定发生变化时,才需要重新编译 C++ 扩展。

Python 前端并不是重新实现一套训练框架,而是在 C++ 核心之上提供另一种表达方式。两层之间的关系可以概括为:

Python:模型组合与训练流程
              ↓
PyBind11:类型与调用转换
              ↓
C++:Tensor、Autograd 与 Operator

Python 创建张量并调用算子,实际的数值计算和计算图仍由 C++ 完成。这样既保留底层实现的性能和统一语义,又让上层训练代码保持简洁。

实现

Python 前端需要解决两个问题:首先将 C++ 的张量和算子安全地暴露给 Python,然后用这些基础接口组织模型与参数更新。绑定层只负责跨越语言边界,ModuleLinearSGD 则使用普通 Python 代码完成组合。

PyBind11

Zero 使用 PyBind11 将 C++ 类型封装为 _zero 扩展模块。绑定层暴露 TensorParameter 和常用算子,并负责在两种语言之间转换参数与返回值:

py::class_<Tensor>(m, "Tensor")
    .def("backward", &Tensor::Backward)
    .def_property_readonly("grad", [](const Tensor& tensor) {
        return tensor.Grad();
    })
    .def("__matmul__", [](const Tensor& lhs, const Tensor& rhs) {
        return lhs.MatMul(rhs);
    });

当 Python 执行 loss.backward() 时,调用会直接进入 C++ 的 Tensor::Backward()。矩阵乘法、加减乘、转置、变形、ReLU 和交叉熵也沿用同一套 C++ 算子,因此绑定前后共享相同的前向与反向规则。

Python 列表创建张量时,绑定层先检查嵌套列表是否具有规则形状,再将数值展开为连续数组。张量返回 Python 时,则根据 shape 重新构造嵌套列表。这个边界只负责数据表示的转换,不参与数值计算。

Module

Python 层的 Module 保持得很小。__call__ 将调用转发给 forwardparameters() 则递归收集对象属性中的 Parameter 和子模块:

class Module:
    def parameters(self):
        result = []
        for value in vars(self).values():
            if isinstance(value, Parameter):
                result.append(value)
            elif isinstance(value, Module):
                result.extend(value.parameters())
        return result

    def __call__(self, *args, **kwargs):
        return self.forward(*args, **kwargs)

Linear

Linear 由参数和已有张量运算组成:

class Linear(Module):
    def __init__(self, in_features, out_features):
        scale = math.sqrt(2.0 / in_features)
        self.weight = Parameter(
            randn([out_features, in_features]) * scale
        )
        self.bias = Parameter(randn([out_features]) * 0.0)

    def forward(self, x):
        return x @ self.weight.T + self.bias

前向过程不需要特殊的模型执行器。每一次 Python 运算都会调用对应的 C++ 算子,并动态建立计算图。

SGD

Python 版 SGD 保存 model.parameters() 返回的参数。zero_grad() 清除上一轮梯度,step() 按学习率生成更新量:

def step(self):
    for parameter in self.parameters:
        if parameter.grad is not None:
            parameter.update(parameter.grad * self.lr)

Parameter.update() 最终仍在 C++ 中完成无梯度的减法。这样,Python 可以控制训练循环,而参数存储、梯度和更新语义仍由底层统一管理。

示例

下面的线性回归同时经过 Python 模型、C++ 算子、自动微分和参数更新:

import zero

model = zero.nn.Linear(2, 1)
optimizer = zero.optim.SGD(model.parameters(), lr=0.05)

x = zero.tensor([[1.0, 2.0], [2.0, 1.0]])
target = zero.tensor([[5.0], [4.0]])

for _ in range(100):
    prediction = model(x)
    difference = prediction - target
    loss = (difference * difference).mean()

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

model(x) 从 Python 进入绑定层,并调用 C++ 的矩阵乘法、转置和加法;loss.backward() 沿 C++ 计算图生成参数梯度;optimizer.step() 再通过 Parameter.update() 修改底层参数。循环仍由 Python 控制,但每一步张量计算都复用了同一套 C++ 实现。

Python 前端由此展示了训练框架的一条边界:底层提供稳定的计算能力,上层提供灵活的表达方式。接口不需要覆盖完整的 PyTorch,只要足以把前面实现的组件组合成一次可运行的训练即可。