ggml 实战:用 YOLOv3-tiny 与 GGUF 权重实现 CPU/GPU 目标检测
2026/9/14 9:02:37 网站建设 项目流程

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.namesCOCO 80 类类别名(每行一个,如dogcar
examples/yolo/data/labels/绘制检测标签文字用的位图字体(8 种字号 × 96 个 ASCII 字符)
examples/yolo/CMakeLists.txt定义可执行目标yolov3-tiny

从构建配置看,yolov3-tiny目标由 examples/yolo/CMakeLists.txt 定义,仅链接ggml与示例公共库common,两个源文件yolov3-tiny.cppyolo-image.cpp即构成全部实现。与gpt-2mnist等其他示例不同,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.weights

2. 转换为 GGUF

$ ./convert-yolov3-tiny.py yolov3-tiny.weights yolov3-tiny.weights converted to yolov3-tiny.gguf

脚本依赖仓库requirements.txt中的ggufnumpy包。转换完成后可跳过下载原始权重,直接获取社区已转换好的yolov3-tiny.gguf(HuggingFace 上 rgerganov/yolo-gguf 仓库提供同一文件)。

转换脚本做了什么:examples/yolo/convert-yolov3-tiny.py 按 Darknet 二进制的线性内存布局依次读取每一层卷积的参数。对每个卷积层,save_conv2d_layer()先后读取:

  • biases(偏置,长度 = 输出通道数);
  • scalesrolling_meanrolling_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 个卷积层l0l12硬编码了输入/输出通道数(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.jpg

README 中记录的完整运行输出(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--threadsCPU 后端线程数硬件并发数 / 2(至少 1)
-th--thresh检测阈值,取值范围(0, 1],越严召回越低0.5
-m--modelGGUF 模型路径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 中“从文件到设备”的标准加载范式:

  1. gguf_init_from_file()在临时上下文中解析 GGUF 元数据与张量;
  2. 新建目标上下文,逐张量ggml_dup_tensor()复制结构(元数据在 CPU 端,no_alloc=true);
  3. ggml_backend_alloc_ctx_tensors()一次性为全部权重张量在后端 buffer 中分配内存;
  4. 遍历张量,用ggml_backend_tensor_set()把 GGUF 里的数据逐块拷贝到设备(README 输出中的load_model: using CUDA backend即来自此阶段的create_backend()日志);
  5. 按层号l{i}_weights / l{i}_biases / ...把张量挂到conv2d_layer结构体上,并按 YOLOv3-tiny 的网络约定配置特殊层:l7/l9/l10/l12padding = 0l9/l12关闭 BatchNorm 与激活(线性输出)。

conv2d_layer结构(yolov3-tiny.cpp#L23-L32)同时持有weights / biases / scales / rolling_mean / rolling_variance五组张量和三个行为开关(paddingbatch_normalizeactivate),这为下面统一的图构建函数打下了基础。

前向计算图: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 号(小目标)。关键后处理步骤:

  1. Sigmoid 激活apply_yolo):每个锚框只对“前 2 个偏移量 tx/ty”(2*w*h个值)与“objectness + 80 类概率”((1+classes)*w*h个值)做 logistic 激活;宽高的 log 尺度保持未激活状态;
  2. 框解码get_yolo_box,L208-L217):中心点(i + tx)/lw, (j + ty)/lh,宽高exp(tw)*anchor_w / wexp(th)*anchor_h / h
  3. letterbox 逆变换correct_yolo_box,L219-L234):把 416×416 归一化坐标反推回原图坐标,扣除灰边偏移并按缩放比例放大宽高;
  4. 阈值过滤get_yolo_detections):objectness 低于-th阈值(默认 0.5)的网格点直接丢弃,类别概率取objectness × class_prob,低于阈值的类置零;
  5. NMSdo_nms_sort,L300-L328):逐类别按概率降序排序,对 IoU 超过0.45detect()中硬编码传入)的重复框清零;
  6. 可视化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),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询