Skip to content

Latest commit

 

History

History
380 lines (297 loc) · 13.7 KB

01-quickstart-model.md

File metadata and controls

380 lines (297 loc) · 13.7 KB

快速上手:构建自己的模型

本教程教你如何使用 ComputeEngine + Layer + Model 三件套从零构建神经网络模型。


三件套概念

ComputeEngine  — 硬件抽象(CPU/GPU),提供计算原语
Layer          — 神经网络层,组合原语表达算法
Model          — 层容器,管理 Layer 的 forward/backward 链

关系:

nn::Model model(engine);              // Model 绑定一个 Engine
model.add<nn::Linear>(784, 128);      // 添加 Layer(add<T>(args...) 返回 Result<void>,
model.add<nn::ReLU>();                //   构造后自动 init(engine))
model.add<nn::Linear>(128, 10);

auto result = model.forward(input);   // engine 在 Model 内部自动传递

头文件怎么 include(两种方式)

// ① 聚合入口:一次拿齐全部模块(本篇示例都用它)
#include <neuralnet.cpp/nn.hpp>

// ② 按需加载:只拿要写的那部分(薄转发头,名字即用途)
#include <neuralnet.cpp/engine.hpp>      // ComputeEngine / CpuEngine / GpuEngine
#include <neuralnet.cpp/layer.hpp>       // Layer 基类 + 全部内置层 + 位置编码
#include <neuralnet.cpp/loss.hpp>        // Loss / MSELoss / CrossEntropyLoss
#include <neuralnet.cpp/optimizer.hpp>   // Optimizer + create_optimizer
#include <neuralnet.cpp/model.hpp>       // Model / ModelSpec / save_model / load_model
#include <neuralnet.cpp/train.hpp>       // 上面五者的合集(写训练循环够用)

两者取其一即可(都带 #pragma once,混用无副作用)。转发头是整洁性入口, 不省编译时间 —— 各头的依赖闭包本就相互牵连,真正省 parse 是另一个专项 (docs/todo/18-roadmap.md)。模型工厂(build_gpt_model 等)与分词器 仍在 domain_*.hpp,名字已足够清楚,不另起转发头。


第一步:选择引擎

#include <neuralnet.cpp/nn.hpp>

// CPU 引擎(默认,无需额外依赖)
auto engine_owner = nn::CpuEngine::create();
nn::CpuEngine& engine = *engine_owner;

// GPU 引擎(需要 Vulkan SDK,条件编译 NN_HAS_VULKAN)
// auto gpu_backend = nn::GpuBackend::shared();
// auto gpu_engine = nn::GpuEngine::create(gpu_backend);

第二步:构建 Model

方式一:模板 add(推荐,适合 MLP)

nn::Model model(engine);

// add 返回 Result<void>([[nodiscard]])——直接丢弃在 -Werror 下编译失败,
// 用 NN_UNWRAP 解掉:失败时打印 message + file:line 后终止
NN_UNWRAP(model.add<nn::Linear>(784, 256));   // 输入层: 784 → 256
NN_UNWRAP(model.add<nn::ReLU>());             // 激活函数
NN_UNWRAP(model.add<nn::Linear>(256, 128));   // 隐藏层: 256 → 128
NN_UNWRAP(model.add<nn::ReLU>());
NN_UNWRAP(model.add<nn::Linear>(128, 10));    // 输出层: 128 → 10

add<LayerType>(args...) 返回 Result<void>(层构造后自动调用 Layer::init(engine), 失败经 Result 上抛);它不返回 *this,不能点链调用——逐行添加并 NN_UNWRAP(...) 解掉;要把错误往上传(函数本身返回 Result)则用 NN_TRY(r, ...)。

方式二:使用工厂函数(MNIST/GPT 预设)

// MNIST MLP:784 → 512 → 256 → 128 → 64 → 10
nn::Model model = NN_UNWRAP(nn::build_mnist_mlp_model(engine));

// MNIST Transformer (ViT):28×28 图像,patch_size=7
// 默认含编码器末端 final norm(NormPlace::Final,原版 ViT 的 ln_f);
// 块内 pre-norm 默认 LayerNorm,可用 NormType 参数换 RMSNorm/BatchNorm
nn::Model vit = NN_UNWRAP(nn::build_mnist_transformer_model(engine));

// GPT 语言模型
nn::Model gpt = NN_UNWRAP(nn::build_gpt_model(
    engine,
    10000,  // vocab_size
    128,    // d_model
    256,    // seq_len
    4,      // num_heads
    512,    // d_ff
    4       // num_layers
));

⚠ 不要写 std::move(*model_result) 而不先判断——失败态解引用是未定义行为。 NN_UNWRAP(...) 把"检查 + 解值"合成一步。

方式三:添加其他内置层

nn::Model model(engine);
NN_UNWRAP(model.add<nn::GeLU>());                  // QuickGeLU 激活
NN_UNWRAP(model.add<nn::LayerNorm>(256));          // 层归一化
NN_UNWRAP(model.add<nn::Softmax>());               // Softmax
NN_UNWRAP(model.add<nn::PositionalEncoding>(128, 1024)); // 位置编码
NN_UNWRAP(model.add<nn::FeedForward>(128, 512));   // FFN
NN_UNWRAP(model.add<nn::TransformerEncoderLayer>(128, 4, 512, 256)); // Transformer 编码器层
NN_UNWRAP(model.add<nn::CausalSelfAttention>(128, 4, 1024));  // 因果自注意力
NN_UNWRAP(model.add<nn::GPTBlock>(128, 4, 512, 1024));        // GPT 块

Tensor 的使用

Tensor 是所有数据的统一容器:

// M1(docs/todo/17 §4.1)起:创建/读写张量一律经 Engine——
// Tensor 的静态工厂与存储访问器已私有化,绕过引擎 = 编译错误。

// 通过 Engine 创建(统一入口:自动分配、出生绑定到引擎)
nn::Tensor t1 = engine.create_tensor(784, 32);  // 纯分配
nn::Tensor t3 = engine.create_tensor(784, 32);
(void)engine.zero(t3);  // 清零(分配后手动)

// M2(docs/todo/17 §4.4):声明式初始化——层算分布参数,引擎填数;
// 分布类 seed 必填(同 seed 不同张量由引擎混流,不撞流)
nn::Tensor t_w = engine.create_tensor(32, 10, nn::Precision::F32,
                                      nn::InitSpec::uniform(-0.1f, 0.1f, nn::kInitSeed));
nn::Tensor t_b = engine.create_tensor(10, 1, nn::Precision::F32,
                                      nn::InitSpec::zero());

// 从 Matrix 上传(Matrix = 宿主 I/O 载体)
nn::Matrix m(784, 32);
auto t_result = engine.from_matrix(m);
nn::Tensor t2 = std::move(*t_result);

// 转换为 Matrix(GPU 张量会先等待在飞命令完成,再下载回 CPU)
auto m_result = engine.to_matrix(t3);
nn::Matrix m2 = std::move(*m_result);

张量布局(重要!):

Matrix/Tensor 行主序:  (rows, cols)
  data_[row * cols + col]

神经网络约定(列主序 batch-major):
  输入: (feature_dim, batch_size)    — 每列一个样本
  输出: (out_dim, batch_size)
  权重: (out_features, in_features)

完整前向传播示例

#include <neuralnet.cpp/nn.hpp>

int main() {
    auto engine_owner = nn::CpuEngine::create();
    nn::CpuEngine& engine = *engine_owner;

    // 1. 构建模型(add 返回 Result<void>,[[nodiscard]] 不能直接丢弃;
    //    NN_UNWRAP = 失败时打印 message + file:line 后终止)
    nn::Model model(engine);
    NN_UNWRAP(model.add<nn::Linear>(784, 256));
    NN_UNWRAP(model.add<nn::ReLU>());
    NN_UNWRAP(model.add<nn::Linear>(256, 10));

    // 2. 准备输入 (784 像素, 32 样本)
    nn::Tensor input = engine.create_tensor(784, 32);
    // ... 填充数据 ...

    // 3. 前向传播:NN_UNWRAP 直接解出值,免写 if (!r) { …; return 1; }
    nn::Tensor output = NN_UNWRAP(model.forward(input));
    // output: (10, 32) — 每列一个样本的 10 类 logits

    // 4. 下载到 CPU 做 argmax
    nn::Matrix m = NN_UNWRAP(engine.to_matrix(output));
    for (std::size_t b = 0; b < 32; ++b) {
        std::size_t best = 0;
        for (std::size_t c = 1; c < 10; ++c) {
            if (m.at(c, b) > m.at(best, b))
                best = c;
        }
        std::cout << "Sample " << b << ": predicted " << best << "\n";
    }
}

三种错误处理语义怎么选(完整定义见 core_errors.hpp 宏注释 / AGENTS.md 铁律 #1):

  • 函数返回 Result<T> 要往上传 → NN_TRY(decl, expr)(展开与手写逐字等价); 要把调用点语境也带上去 → NN_TRY_MSG(decl, "语境", expr);
  • 任意上下文(main / void / 测试)解包并继续,失败即终止 → NN_UNWRAP(expr[, "语境"]) (别名 NN_UNWRAP_1;也接受 bool 条件,等于"始终启用的断言");
  • CLI/参数错误要保住退出码 → NN_EXIT(expr, code[, "语境"])。

自定义 Layer

如果内置层不够用,可以继承 Layer 实现自己的层:

class MyLayer final : public nn::Layer {
private:
    nn::Tensor weight_;
    nn::Tensor grad_weight_;
    nn::Tensor input_cache_;
    std::size_t in_dim_;
    std::size_t out_dim_;

public:
    // 构造函数只存形状(Model::add 构造后会自动调用 init(engine))
    MyLayer(std::size_t in_dim, std::size_t out_dim)
        : in_dim_(in_dim), out_dim_(out_dim) {}

    // 权重初始化:M2 声明式——层算分布参数(Xavier limit),引擎填数
    // M6 段 C 起公共 init 是 NVI:子类覆写的是 init_impl(不是 init)
    [[nodiscard]] nn::Result<void> init_impl(nn::ComputeEngine& engine) override
    {
        const nn::Scalar limit =
            std::sqrt(6.0f / static_cast<nn::Scalar>(in_dim_ + out_dim_));
        weight_ = engine.create_tensor(out_dim_, in_dim_, nn::Precision::F32,
                                       nn::InitSpec::uniform(-limit, limit, nn::kInitSeed));
        if (!weight_.valid())
            NN_FAIL("MyLayer: 权重初始化失败");

        grad_weight_ = engine.create_tensor(out_dim_, in_dim_, nn::Precision::F32,
                                            nn::InitSpec::zero());
        if (!grad_weight_.valid())
            NN_FAIL("MyLayer: 梯度缓冲初始化失败");
        return {};
    }

    // 参数访问(供 Optimizer 使用):TensorRef = reference_wrapper<Tensor>
    std::vector<nn::TensorRef> parameters() override {
        return {weight_};
    }
    std::vector<nn::TensorRef> param_gradients() override {
        return {grad_weight_};
    }

    // 前向传播:M6 段 C 起不带 engine 形参(engine 在 init 时已绑定到 engine_,
    // 方法体里经 engine_ref() 取回,未 init 就调用会 fail-fast 并打印调用点)
    [[nodiscard]] nn::Result<nn::Tensor> forward(const nn::Tensor& input) override
    {
        nn::ComputeEngine& engine = engine_ref();
        input_cache_ = input;
        // 组合原语表达算法
        return engine.matmul(weight_, input);
    }

    // 反向传播
    [[nodiscard]] nn::Result<nn::Tensor> backward(const nn::Tensor& grad_output) override
    {
        nn::ComputeEngine& engine = engine_ref();

        // grad_input = weight^T × grad_output
        NN_TRY(grad_input, engine.matmul(weight_, grad_output, /*transA=*/true));

        // grad_weight += grad_output × input^T(NN_TRY = 声明 + 检查 + 传播)
        NN_TRY(gw, engine.matmul(grad_output, input_cache_, /*transA=*/false, /*transB=*/true));
        NN_TRY(r, engine.add_inplace(grad_weight_, *gw));

        return grad_input;
    }
};

错误写法对照:NN_TRY(decl, expr) 只用于声明表达式结果(内部求值一次); NN_TRY_CHECK(x) 只用于已声明的变量——它在失败分支会再取一次 x.error(), 对带副作用的表达式写成 NN_TRY_CHECK(engine.foo()) 会重复执行 foo()。

添加到 Model:

nn::Model model(engine);
model.add<MyLayer>(784, 256);
model.add<nn::ReLU>();
model.add<MyLayer>(256, 10);

常见陷阱

1. Tensor 布局错误

// ❌ 错误:(batch, feature) — 行主序 batch-major
nn::Tensor input = engine.create_tensor(32, 784);

// ✅ 正确:(feature, batch) — 列主序 batch-major
nn::Tensor input = engine.create_tensor(784, 32);

2. 忘记绑定 Engine

// ❌ 错误:Model 未绑定 Engine
nn::Model model;                    // 默认构造
model.add<nn::Linear>(784, 10);     // NN_ASSERT(engine_, "Model: engine not bound") 断言失败

// ✅ 正确
auto engine_owner = nn::CpuEngine::create();
nn::CpuEngine& engine = *engine_owner;
nn::Model model(engine);
model.add<nn::Linear>(784, 10);

3. 跨设备操作绕过 Engine

// ❌ 错误:直接摸 GPU 张量的 CPU 存储(M1 起存储访问器已私有 → 这行编译不过)
auto gpu_backend = nn::GpuBackend::shared();             // 需 NN_HAS_VULKAN
auto gpu_engine = nn::GpuEngine::create(gpu_backend);
nn::Tensor t = gpu_engine->create_tensor(10, 10);
auto& m = t.cpu_matrix();                   // 编译错误:cpu_matrix 是私有成员

// ✅ 正确:跨设备读写统一走 engine(to_matrix 下载,GPU 会等待在飞命令完成)
auto m_result = gpu_engine->to_matrix(t);

4. 梯度清零遗漏

// ❌ 错误:忘记清零梯度
auto out = model.forward(input);
auto loss = loss_fn.forward(engine, *out, target);
auto grad = loss_fn.backward();
model.backward(*grad);
optimizer->step();  // 梯度累积!

// ✅ 正确:每步清零
optimizer->zero_grad();   // 或 model.zero_grad()(两者都不带 engine 参数)
auto out = model.forward(input);
auto loss = loss_fn.forward(engine, *out, target);
auto grad = loss_fn.backward();
model.backward(*grad);
optimizer->step();

可用层速查表

层 构造参数 用途
Linear(in, out) 输入/输出维度 全连接层
ReLU() 无 ReLU 激活
GeLU() 无 QuickGeLU 激活
LayerNorm(shape, eps) 归一化维度 + epsilon 层归一化
Softmax() 无 行级 Softmax
PositionalEncoding(d_model, max_len) 模型维度 + 最大长度 正弦位置编码
FeedForward(d_model, d_ff) 模型维度 + FFN 中间维度 FFN = Linear₂(GeLU(Linear₁(x)))
TransformerEncoderLayer(d_model, heads, d_ff, seq_len) 全部 Pre-Norm 编码器层
TransformerEncoder(d_model, heads, d_ff, layers, patches) 全部 ViT 编码器(含池化)
PatchEmbedding(img, patch, d_model) 图像/patch/模型维度 图像 patch 嵌入
CausalSelfAttention(d_model, heads, max_len) 全部 因果自注意力
GPTBlock(d_model, heads, d_ff, max_len) 全部 Pre-Norm 解码器块
GPTModel(vocab, d_model, seq, heads, d_ff, layers) 全部 完整 GPT 模型