本教程教你如何使用 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 <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);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 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 是所有数据的统一容器:
// 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 实现自己的层:
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);// ❌ 错误:(batch, feature) — 行主序 batch-major
nn::Tensor input = engine.create_tensor(32, 784);
// ✅ 正确:(feature, batch) — 列主序 batch-major
nn::Tensor input = engine.create_tensor(784, 32);// ❌ 错误: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);// ❌ 错误:直接摸 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);// ❌ 错误:忘记清零梯度
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 模型 |