ggml 实战:用 YOLOv3-tiny 与 GGUF 权重实现 CPU/GPU 目标检测
【免费下载链接】ggmlTensor library for machine learning项目地址: https://gitcode.com/GitHub_Trending/gg/ggml
本篇基于 ggml 官方示例 examples/yolo/README.md 展开,讲解如何用 ggml 张量库加载预训练的 YOLOv3-tiny 权重、完成 Darknet.weights到 GGUF 格式的转换,并跑通一次完整的单图目标检测流程。读完本文,你既能照做复现出带检测框的predictions.jpg结果,也能从源码层面理解该示例中计算图的构建方式、卷积-归一化-激活的算子编排,以及 YOLO 后处理(锚框解码、letterbox 校正、NMS)在 examples/yolo/yolov3-tiny.cpp 中的具体实现。
示例定位与目录结构
YOLO 示例位于examples/yolo/目录,是 ggml 仓库中用“现成权重 + 少量 C++ 代码”搭起完整 CNN 推理管线的典型样本,目录内容如下:
| 文件 | 作用 |
|---|---|
| examples/yolo/yolov3-tiny.cpp | 主程序:加载模型、构建计算图、前向推理、后处理与可视化 |
| examples/yolo/convert-yolov3-tiny.py | 将 Darknet 原始.weights二进制转换为 GGUF 文件 |
| examples/yolo/yolo-image.cpp / examples/yolo/yolo-image.h | 图像读写(stb_image)、letterbox 预处理、画框与标签绘制 |
| examples/yolo/data/coco.names | COCO 80 类类别名(每行一个,如dog、car) |
| examples/yolo/data/labels/ | 绘制检测标签文字用的位图字体(8 种字号 × 96 个 ASCII 字符) |
| examples/yolo/CMakeLists.txt | 定义可执行目标yolov3-tiny |
从构建配置看,yolov3-tiny目标由 examples/yolo/CMakeLists.txt 定义,仅链接ggml与示例公共库common,两个源文件yolov3-tiny.cpp与yolo-image.cpp即构成全部实现。与gpt-2、mnist等其他示例不同,yolo 子目录在 examples/CMakeLists.txt 中是无条件加入构建的(不依赖GGML_BACKEND_DL开关),单独构建整个仓库(standalone 模式)时示例默认开启。
构建方式:
# 配置(standalone 模式下默认 GGML_BUILD_EXAMPLES=ON,可执行文件输出到 build/bin) cmake -B build # 只构建 yolo 目标 cmake --build build --target yolov3-tiny -j产物位于build/bin/yolov3-tiny。主程序入口会先调用ggml_backend_load_all()动态加载所有已编译后端(CPU 默认开启,CUDA/Metal/Vulkan 等需编译期通过对应GGML_*选项开启),随后按“优先 GPU、回退 CPU”的策略选择设备,详见 examples/yolo/yolov3-tiny.cpp#L558-L597。
获取权重并转换为 GGUF 格式
README 给出的标准流程分三步:下载原始权重、校验、转换。
1. 下载 YOLOv3-tiny 预训练权重(YOLOv3 原作者公开的 Darknet 格式文件,并可用 SHA1 校验完整性):
$ wget https://pjreddie.com/media/files/yolov3-tiny.weights $ sha1sum yolov3-tiny.weights 40f3c11883bef62fd850213bc14266632ed4414f yolov3-tiny.weights2. 转换为 GGUF:
$ ./convert-yolov3-tiny.py yolov3-tiny.weights yolov3-tiny.weights converted to yolov3-tiny.gguf脚本依赖仓库requirements.txt中的gguf与numpy包。转换完成后可跳过下载原始权重,直接获取社区已转换好的yolov3-tiny.gguf(HuggingFace 上 rgerganov/yolo-gguf 仓库提供同一文件)。
转换脚本做了什么:examples/yolo/convert-yolov3-tiny.py 按 Darknet 二进制的线性内存布局依次读取每一层卷积的参数。对每个卷积层,save_conv2d_layer()先后读取:
biases(偏置,长度 = 输出通道数);scales、rolling_mean、rolling_variance(BatchNorm 的仿射缩放与运行统计量,以(1, filters, 1, 1)的形状写入 GGUF);- 卷积核
weights,形状为(filters, inp_c, size, size)。
值得注意的是脚本中的一行注释(convert-yolov3-tiny.py#L20-L21):ggml doesn't support f32 convolution yet, use f16 instead—— 卷积核在写入前被强制转换为float16,即该示例模型中的卷积权重以 F16 存储,输入张量则为 F32,计算时由 ggml 后端负责按需转换。这一设计也解释了为什么 README 的运行时输出中出现 CUDA 初始化信息:F16 权重与 GPU 后端配合是该示例的推荐路径。
脚本对 13 个卷积层l0~l12硬编码了输入/输出通道数(3→16→32→64→128→256→512→1024→256→512→255 / 256→128→256→255),其中两个检测头l9(1×1 卷积,512→255)与l12(256→255)传入了batch_normalize=False,即不写 BN 参数——这与 YOLOv3-tiny 网络图中“最后一个卷积直接输出回归量、不做归一化”的结构一致。文件开头跳过 20 字节(Darknet 的major/minor/revision头),最后通过gguf_writer.write_header_to_file() / write_kv_data_to_file() / write_tensors_to_file()三步写出标准 GGUF 文件,与仓库核心实现 src/gguf.cpp 及格式说明 docs/gguf.md 相对应。
运行目标检测
准备一张测试图片(README 使用 Darknet 仓库自带的dog.jpg,内含狗、车、自行车等目标),在能访问data/目录的位置(通常即examples/yolo/)执行:
$ ./yolov3-tiny -m yolov3-tiny.gguf -i dog.jpgREADME 中记录的完整运行输出(CUDA 后端):
load_model: using CUDA backend ggml_cuda_init: GGML_CUDA_FORCE_MMQ: no ggml_cuda_init: GGML_CUDA_FORCE_CUBLAS: no ggml_cuda_init: found 1 CUDA devices: Device 0: NVIDIA T1200 Laptop GPU, compute capability 7.5, VMM: yes Layer 0 output shape: 416 x 416 x 16 x 1 Layer 1 output shape: 208 x 208 x 16 x 1 Layer 2 output shape: 208 x 208 x 32 x 1 Layer 3 output shape: 104 x 104 x 32 x 1 Layer 4 output shape: 104 x 104 x 64 x 1 Layer 5 output shape: 52 x 52 x 64 x 1 Layer 6 output shape: 52 x 52 x 128 x 1 Layer 7 output shape: 26 x 26 x 128 x 1 Layer 8 output shape: 26 x 26 x 256 x 1 Layer 9 output shape: 13 x 13 x 256 x 1 Layer 10 output shape: 13 x 13 x 512 x 1 Layer 11 output shape: 13 x 13 x 512 x 1 Layer 12 output shape: 13 x 13 x 1024 x 1 Layer 13 output shape: 13 x 13 x 256 x 1 Layer 14 output shape: 13 x 13 x 512 x 1 Layer 15 output shape: 13 x 13 x 255 x 1 Layer 18 output shape: 13 x 13 x 128 x 1 Layer 19 output shape: 26 x 26 x 128 x 1 Layer 20 output shape: 26 x 26 x 384 x 1 Layer 21 output shape: 26 x 26 x 256 x 1 Layer 22 output shape: 26 x 26 x 255 x 1 dog: 57% car: 52% truck: 56% car: 62% bicycle: 59% Detected objects saved in 'predictions.jpg' (time: 0.057000 sec.)输出的每一行Layer N output shape来自 examples/yolo/yolov3-tiny.cpp#L389-L392 的print_shape(),格式为w x h x channels x batch:行号沿用 Darknet 网络图的层编号(16/17是池化/跳接连线,故直接跳号),255 = 3 × (4 + 80 + 1)对应每个网格点 3 个锚框的(4 个框参数 + 80 个类别 + 1 个 objectness)回归量。检测结果逐行打印类别与置信度,最终带框结果写入predictions.jpg。
命令行参数(源码中的定义见 examples/yolo/yolov3-tiny.cpp#L482-L489 与yolo_print_usage()):
| 参数 | 长选项 | 说明 | 默认值 |
|---|---|---|---|
-h | --help | 打印帮助并退出 | - |
-d | --device | 指定设备名;未知设备名会列出所有可用设备及其显存 | 空(GPU 优先,回退 CPU) |
-t | --threads | CPU 后端线程数 | 硬件并发数 / 2(至少 1) |
-th | --thresh | 检测阈值,取值范围(0, 1],越严召回越低 | 0.5 |
-m | --model | GGUF 模型路径 | yolov3-tiny.gguf |
-i | --inp | 输入图片路径 | input.jpg |
-o | --out | 输出图片路径 | predictions.jpg |
注意两个隐含的运行时依赖:程序会固定读取相对路径data/coco.names(并要求恰好 80 行,源码中以GGML_ASSERT(labels.size() == 80)强制校验)以及data/labels/<size>_<char>.png字体图(8 个字号 × ASCII 32~126),因此直接以源码目录布局运行即可,无需额外拷贝。
模型加载:GGUF 权重如何进入后端显存
load_model()(examples/yolo/yolov3-tiny.cpp#L77-L139)展示了 ggml 中“从文件到设备”的标准加载范式:
- 用
gguf_init_from_file()在临时上下文中解析 GGUF 元数据与张量; - 新建目标上下文,逐张量
ggml_dup_tensor()复制结构(元数据在 CPU 端,no_alloc=true); ggml_backend_alloc_ctx_tensors()一次性为全部权重张量在后端 buffer 中分配内存;- 遍历张量,用
ggml_backend_tensor_set()把 GGUF 里的数据逐块拷贝到设备(README 输出中的load_model: using CUDA backend即来自此阶段的create_backend()日志); - 按层号
l{i}_weights / l{i}_biases / ...把张量挂到conv2d_layer结构体上,并按 YOLOv3-tiny 的网络约定配置特殊层:l7/l9/l10/l12的padding = 0,l9/l12关闭 BatchNorm 与激活(线性输出)。
conv2d_layer结构(yolov3-tiny.cpp#L23-L32)同时持有weights / biases / scales / rolling_mean / rolling_variance五组张量和三个行为开关(padding、batch_normalize、activate),这为下面统一的图构建函数打下了基础。
前向计算图:13 个卷积层如何拼成 YOLOv3-tiny
核心在build_graph()(examples/yolo/yolov3-tiny.cpp#L394-L454)。它先在图上下文中声明一个416 x 416 x 3的 F32 输入张量input(对应转换时写死的网络分辨率model.width = model.height = 416),然后按 Darknet 的网络定义串起算子:
// 单个卷积层的展开方式(apply_conv2d) struct ggml_tensor * result = ggml_conv_2d(ctx, layer.weights, input, 1, 1, pad, pad, 1, 1); if (layer.batch_normalize) { result = ggml_sub(ctx, result, ggml_repeat(ctx, layer.rolling_mean, result)); result = ggml_div(ctx, result, ggml_sqrt(ctx, ggml_repeat(ctx, layer.rolling_variance, result))); result = ggml_mul(ctx, result, ggml_repeat(ctx, layer.scales, result)); } result = ggml_add(ctx, result, ggml_repeat(ctx, layer.biases, result)); if (layer.activate) { result = ggml_leaky_relu(ctx, result, 0.1f, true); }(见 examples/yolo/yolov3-tiny.cpp#L171-L184)
从源码结构看,这里有几个值得学习的 ggml 用法:
- BatchNorm 推理展开:不单独调用归一化算子,而是把
(x - mean) / sqrt(var) * scale + bias展开为ggml_sub / ggml_div / ggml_sqrt / ggml_mul / ggml_add的组合,并用ggml_repeat把(1, C, 1, 1)的统计量广播到整个特征图。对 LLM 类模型而言类似的“把权重变换折叠成线性算子”是常见技巧; - Leaky ReLU 斜率 0.1:与 Darknet 中 YOLO 卷积的标准配置一致;
- 两个检测头:
l9(13×13 特征图)与l12(26×26 特征图)输出均被ggml_set_output()标记并命名为layer_15/layer_22,最后分别通过ggml_build_forward_expand()展开进同一张ggml_cgraph,一次ggml_backend_graph_compute()即可同时拿到两路预测。
整体拓扑(与运行输出逐行对应):
input 416x416x3 └─ l0 conv(3→16) → pool → l1 conv(16→32) → pool → l2 conv(32→64) → pool → l3 conv(64→128) → pool → l4 conv(128→256) ← layer_8 (26x26x256) → pool → l5 conv(256→512) → pool(保持13) → l6 conv(512→1024) → l7 conv1x1(1024→256) ← layer_13 (13x13x256) → l8 conv(256→512) → l9 conv1x1(512→255) ⇒ 输出 layer_15 (13x13x255) layer_13 → l10 conv1x1(256→128) → upscale 2x(最近邻) → concat(layer_8) (26x26x384) → l11 conv(384→256) → l12 conv1x1(256→255) ⇒ 输出 layer_22 (26x26x255)图构建完成后,main()使用ggml_gallocr(来自 include/ggml-alloc.h)做计算图内存分配:ggml_gallocr_new(ggml_backend_get_default_buffer_type(backend))按后端默认 buffer 类型创建分配器并ggml_gallocr_alloc_graph(),这是 ggml 官方推荐的图内存管理方式。
图像预处理:letterbox 与张量布局
detect()(examples/yolo/yolov3-tiny.cpp#L456-L461)每帧先把原图送入letterbox_image(img, 416, 416),再把结果写入图输入张量:
yolo_image sized = letterbox_image(img, model.width, model.height); struct ggml_tensor * input = ggml_graph_get_tensor(gf, "input"); ggml_backend_tensor_set(input, sized.data.data(), 0, ggml_nbytes(input));letterbox_image()(examples/yolo/yolo-image.cpp#L144-L160)的实现要点:
- 按比例缩放至 416×416 内接矩形(保持长宽比),其余区域以灰色 0.5 填充,避免拉伸形变;
- 缩放采用双线性插值(
resize_image()的横向先行 + 纵向混合); - 图像数据以CHW布局(
data[c*w*h + y*w + x])存储、像素值归一化到[0, 1],与ggml_new_tensor_4d(ctx, GGML_TYPE_F32, w, h, 3, 1)的(ne0=x, ne1=y, ne2=channel, ne3=batch)内存序严格对应——这是ggml_backend_tensor_set()能直接整块拷贝的前提。
读取图片用 stb_image(examples/yolo/yolo-image.cpp#L66-L89),固定转 3 通道。
后处理:锚框解码、letterbox 校正与 NMS
推理结束后,两路输出进入yolo_layer(yolov3-tiny.cpp#L43-L65):构造时用ggml_backend_tensor_get()把设备端预测拉回主机内存。两个检测头的配置(yolov3-tiny.cpp#L468-L476):
yolo_layer yolo16{ 80, {3, 4, 5}, {10,14, 23,27, 37,58, 81,82, 135,169, 344,319}, layer_15}; // 13x13 粗网格 yolo_layer yolo23{ 80, {0, 1, 2}, {10,14, 23,27, 37,58, 81,82, 135,169, 344,319}, layer_22}; // 26x26 细网格即 12 个锚框按 mask 拆分:粗网格用第 3/4/5 号(大目标),细网格用第 0/1/2 号(小目标)。关键后处理步骤:
- Sigmoid 激活(
apply_yolo):每个锚框只对“前 2 个偏移量 tx/ty”(2*w*h个值)与“objectness + 80 类概率”((1+classes)*w*h个值)做 logistic 激活;宽高的 log 尺度保持未激活状态; - 框解码(
get_yolo_box,L208-L217):中心点(i + tx)/lw, (j + ty)/lh,宽高exp(tw)*anchor_w / w、exp(th)*anchor_h / h; - letterbox 逆变换(
correct_yolo_box,L219-L234):把 416×416 归一化坐标反推回原图坐标,扣除灰边偏移并按缩放比例放大宽高; - 阈值过滤(
get_yolo_detections):objectness 低于-th阈值(默认 0.5)的网格点直接丢弃,类别概率取objectness × class_prob,低于阈值的类置零; - NMS(
do_nms_sort,L300-L328):逐类别按概率降序排序,对 IoU 超过0.45(detect()中硬编码传入)的重复框清零; - 可视化(
draw_detections):每个检测框用“类别索引 → 6 色调色板插值”得到 RGB 颜色,draw_box_width()按im.h * 0.006的线宽画空心矩形,标签文字由data/labels/位图字体逐字符拼贴(get_label()),最终以 JPEG 质量 80 保存(save_image(img, ..., 80))。
entry_index()(L60-L64)体现了 YOLO 输出张量的内存布局:anchor * w*h*(4+classes+1) + entry*w*h + loc,即“按锚框分块、每块内含 4+1+80 个连续特征平面”,这也是255通道数的来源。
结果解读与注意事项
- README 示例输出中 5 行“类别: 百分比”即
draw_detections()的printf日志,最后一行Detected objects saved in 'predictions.jpg' (time: 0.057000 sec.)的耗时由ggml_time_ms()围绕detect()计时(L646-L653),包含 letterbox、图执行与全部后处理; - 无 GPU 环境(如未开启
GGML_CUDA/GGML_METAL/GGML_VULKAN的纯 CPU 构建)下,程序会自动回退到 CPU 后端运行,行为一致,只是速度差异较大;也可用-d显式指定设备、-t调整线程数(默认取一半逻辑核); -th阈值是召回/精度的主要调节旋钮:调低(如 0.3)会输出更多低置信度框,调高则更保守;NMS 的 0.45 IoU 阈值与两头的锚框 mask 均在源码中固定,属该示例的简化处理;- 该示例模型分辨率固定为 416×416、类别固定为 COCO 80 类(
data/coco.names),输入/输出文件名与模型名均可通过命令行覆盖,但网络结构本身由build_graph()硬编码,不打算支持其它 YOLO 变体。
关键文件索引
| 内容 | 路径 |
|---|---|
| 示例说明(下载/转换/运行) | examples/yolo/README.md |
| 推理主程序(模型加载、构图、后处理) | examples/yolo/yolov3-tiny.cpp |
| Darknet → GGUF 转换脚本 | examples/yolo/convert-yolov3-tiny.py |
| 图像 I/O、letterbox、绘制 | examples/yolo/yolo-image.cpp |
| 类别名 / 字体数据 | examples/yolo/data/coco.names、examples/yolo/data/labels/ |
| 构建定义 | examples/yolo/CMakeLists.txt、examples/CMakeLists.txt |
| 相关核心库 | include/ggml.h、include/ggml-alloc.h、include/ggml-backend.h、include/gguf.h |
【免费下载链接】ggmlTensor library for machine learning项目地址: https://gitcode.com/GitHub_Trending/gg/ggml
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考