news 2026/9/7 1:47:11

RK3588端侧部署YOLOv8:单头改双头输出实战指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
RK3588端侧部署YOLOv8:单头改双头输出实战指南

在做 RK3588 端侧部署时,很多朋友会遇到一个很尴尬的情况:模型在电脑上跑得好好的,精度也够,一旦转到 RKNN 就出现各种算子不支持、后处理写法别扭、多任务输出没法拆分的问题。尤其是 YOLOv8 这种自带解耦头的模型,默认输出结构在边缘侧并不总是“最优解”。本篇文章就围绕 RK3588 + YOLOv8 这个组合,从源码层面拆解 YOLOv8 输出层的设计,再一步步把默认的单头输出改造成双头输出,并完整走一遍训练、导出、RKNN 转换和板端部署流程。

这篇文章适合已经跑通 RK3588 基础环境、正在做模型优化或想自定义 YOLOv8 检测头的开发者。零基础的朋友可以先补一下 RKNN Toolkit2 的基础用法,再回来看本文的改动思路。

1. 为什么要拆解输出层:从单头到双头的实际场景

1.1 输出层在 YOLOv8 中扮演的角色

YOLOv8 的网络结构可以粗分成三块:Backbone(主干网络)、Neck(特征融合)、Head(检测头)。Backbone 负责提取图像特征,Neck 把不同尺度的特征做融合,最后交给 Head 输出目标类别和边界框位置。

输出层,也就是 Head 部分,直接决定了“网络最终吐出什么”。默认情况下,YOLOv8 的 Head 会在三个尺度上分别输出预测结果。这三个尺度对应特征图的下采样倍数,通常是 8 倍、16 倍、32 倍。小幅度的特征图负责检测大目标,大幅度负责检测小目标。

在 YOLOv8 的源码里,这个 Head 是由Detect类实现的。它的核心逻辑不复杂:对每个尺度的特征图做卷积,预测出目标的类别概率和边界框坐标,最后把多尺度的结果拼到一起,形成最终输出。

如果只是做标准的目标检测,直接用默认 Head 没有任何问题。但一旦涉及到“检测 + 分类 + 关键点”这样的多任务场景,或者想把小目标检测单独拆出来做优化,默认的单一输出结构就不够灵活了。

1.2 单头与双头的本质区别

先解释一下本文语境下的“单头”和“双头”。

默认 YOLOv8 的Detect类虽然在内部把分类和回归分支分开了,但从外部看,最终只输出一个融合后的结果,这个叫单头输出。它的结构大致是这样的:

输入特征图 ├── 分类分支(Conv + Conv + Conv2d) └── 回归分支(Conv + Conv + Conv2d) ↓ 拼接成一个 Tensor 输出

所谓双头输出,就是把这个单一结果拆成两个独立的输出分支。比如:

输入特征图 ├── 输出头 A:负责类别预测 └── 输出头 B:负责边界框回归

还可以是另一种双头:在原本的检测头基础上,额外增加一个“辅助头”,比如一个专门输出小目标检测结果的浅层头,或者一个输出关键点坐标的头。

在 RK3588 上做双头改造,常见动机有三个:

  1. 多任务输出:一个头做检测,一个头做属性分类,或者一个头做检测,一个头做关键点。
  2. 小目标增强:在原有 P3/P4/P5 三个输出尺度的基础上,增加一个 P2 浅层输出头,让模型对小目标更敏感。
  3. NPU 部署优化:把输出拆开后,RKNN 可以分别处理不同尺度的结果,后处理代码更容易编写和维护。

1.3 RK3588 部署场景下的真实需求

RK3588 内置的 NPU 算力虽然不错,但和桌面级 GPU 不同,它对网络结构的兼容性有自己的脾气。很多在 PyTorch 里能跑的算子,转到 RKNN 时可能就不支持,或者需要特殊写法。

我在实际项目中遇到的一个典型场景是:需要同时检测目标并判断目标朝向。原来的做法是单独训练一个 YOLOv8 检测模型和一个分类模型,部署时串行推理。这样做有两个问题:一是耗时翻倍,二是两套模型的预处理和后处理互相独立,工程上很繁琐。

改成双头输出后,检测和朝向分类在一个模型里完成,前处理只做一次,NPU 推理也只做一次,后处理从两个模型的结果里各自取数据即可。整体帧率提升很明显。

另一个常见场景是小目标检测。标准 YOLOv8 的三个输出尺度对密集小目标效果有限。很多做工厂缺陷检测、无人机航拍目标检测的朋友,都会尝试加一个 P2 输出头。这个改动其实也可以理解成一种“双头到多头”的扩展思路。

所以,学会从源码层面修改 YOLOv8 的输出层,是做端侧模型定制绕不开的基本功。

2. 读懂 YOLOv8 的检测头源码

2.1 YOLOv8 的 Decoupled Head 结构

YOLOv8 和 YOLOv5 一个很大的区别,就是 Head 从耦合头改成了解耦头。

YOLOv5 的检测头是直接在特征图上预测(4 + 1 + nc) * 3个通道,其中 4 是边界框坐标,1 是置信度,nc 是类别数,3 是 anchor 数量。这种方式把分类和回归耦合在同一个输出通道上。

YOLOv8 则把分类和回归拆成两个独立分支。在源码ultralytics/nn/modules/head.pyDetect类中,可以看到两个核心卷积模块:

  • cv2:回归分支,输出4 * reg_max个通道
  • cv3:分类分支,输出nc个通道

这里的reg_max是 YOLOv8 度量学习中 DFL(Distribution Focal Loss)模块的参数,默认值是 16。也就是说,每个边界框的每条边不是直接回归一个坐标值,而是回归 16 个离散概率,再通过 DFL 层加权求和得到最终坐标。

这种结构图大致如下:

输入特征图(P3/P4/P5) ├── cv2 分支:Conv -> Conv -> Conv2d(4 * reg_max) │ ↓ │ 4 * reg_max 通道的回归特征 └── cv3 分支:Conv -> Conv -> Conv2d(nc) ↓ nc 通道的分类特征

2.2 Detect 类的初始化与 Forward 流程

打开 YOLOv8 源码,Detect 类的初始化函数中比较关键的部分如下:

class Detect(nn.Module): def __init__(self, nc=80, ch=()): super().__init__() self.nc = nc self.nl = len(ch) self.reg_max = 16 self.no = nc + self.reg_max * 4 self.stride = torch.zeros(self.nl) c2, c3 = max((16, ch[0] // 4, self.reg_max * 4)), max(ch[0], min(self.nc, 100)) self.cv2 = nn.ModuleList( nn.Sequential(Conv(x, c2, 3), Conv(c2, c2, 3), nn.Conv2d(c2, 4 * self.reg_max, 1)) for x in ch ) self.cv3 = nn.ModuleList( nn.Sequential(Conv(x, c3, 3), Conv(c3, c3, 3), nn.Conv2d(c3, self.nc, 1)) for x in ch ) self.dfl = DFL(self.reg_max)

这里ch是 Neck 输出的三个特征图的通道数。cv2cv3都是 ModuleList,每个特征图对应一套自己的卷积参数。

forward方法做的事可以拆成下面几步:

  1. 遍历三个尺度的特征图,分别经过cv2cv3
  2. 对分类结果做 sigmoid 激活。
  3. 对回归结果通过 DFL 计算边界框坐标。
  4. 按尺度拼接,并统一转成推理格式输出。

核心代码如下:

def forward(self, x): shape = x[0].shape for i in range(self.nl): x[i] = torch.cat((self.cv2[i](x[i]), self.cv3[i](x[i])), 1) if self.training: return x ... # 推理模式:拼接所有尺度的输出并返回

注意这个torch.cat,它把回归分支和分类分支拼接在通道维度上。这就是“单头”输出的核心所在:外部调用模型时,拿到的是一个已经拼好的结果。

2.3 输出 Tensor 的形状与含义

推理模式下,YOLOv8 输出的格式是[batch, num_anchors, 4 + nc]

num_anchors是所有尺度特征图的锚点总数。以 640x640 输入为例:

  • P3 特征图是 80x80,有 6400 个锚点
  • P4 特征图是 40x40,有 1600 个锚点
  • P5 特征图是 20x20,有 400 个锚点

合计 8400 个锚点。每个锚点对应4 + nc个数值,前 4 个是边界框坐标(归一化的 x, y, w, h),后面 nc 个是类别概率。

了解这个形状很重要,因为改造成双头输出后,前 4 个数值和后面 nc 个数值会被拆到两个不同的 Tensor 中,后处理代码需要相应调整。

3. 环境准备与代码基线

3.1 硬件与软件环境

本文的环境如下,版本可以根据自己的项目情况调整:

项目版本
开发板RK3588(本文以正点原子 RK3588 为例)
NPU 工具链rknn-toolkit2
训练环境Python 3.8 + PyTorch 2.0
YOLOv8 源码ultralytics 8.x
模型导出格式ONNX -> RKNN

操作系统可以是 Ubuntu,也可以直接在 RK3588 的板端系统上做推理验证。训练和导出建议放在 PC 上完成,RK3588 只负责 RKNN 模型推理。

3.2 准备 YOLOv8 源码与预训练权重

首先克隆 ultralytics 源码。如果网络不方便,也可以直接在 PyPI 安装:

pip install ultralytics

不过,由于我们需要改源码里的Detect类,建议直接使用 GitHub 源码方式:

git clone https://github.com/ultralytics/ultralytics.git cd ultralytics pip install -e .

下载一个预训练权重作为基线,比如 YOLOv8s:

wget https://github.com/ultralytics/assets/releases/download/v8.1.0/yolov8s.pt

这个权重是 COCO 80 类预训练的,后面做双头改造时,我们可以先用它跑通流程,再用自己的数据集重新训练。

3.3 快速验证当前输出

写一个最简单的推理脚本,确认当前单头输出的形状:

import torch from ultralytics import YOLO model = YOLO("yolov8s.pt") model.eval() dummy_input = torch.randn(1, 3, 640, 640) with torch.no_grad(): result = model.predict(dummy_input) print(type(result))

如果要看更底层的模型输出形状,可以直接拿model.model的 forward 结果:

import torch from ultralytics.nn.tasks import DetectionModel model = DetectionModel("yolov8s.yaml") model.eval() dummy_input = torch.randn(1, 3, 640, 640) with torch.no_grad(): output = model(dummy_input) print(len(output)) for i, x in enumerate(output): print(i, x.shape)

这里输出的就是推理阶段模型拼接后的最终结果。一共一个 Tensor,形状是[1, 8400, 84],其中 84 = 4 + 80。

记住这个基线结果,后面改成双头输出后,比较差异就有依据了。

4. 核心改造:单头变双头输出

4.1 设计双头结构

在动手改代码之前,先把目标说清楚。

我们这次要实现的“双头”,是把默认的检测头从“一个融合输出”,改造成“两个独立输出”:

  • 输出头 A:边界框回归结果,形状为[batch, 8400, 4]
  • 输出头 B:分类结果,形状为[batch, 8400, nc]

后处理时,先从两个输出里分别取所需数据,再合并计算最终检测框。

这样做的好处是:

  1. 后处理代码更清晰,不需要在一大块连续内存里切分。
  2. 任务解耦后,可以单独对分类分支做蒸馏、量化或优化。
  3. 如果后续要扩展第三个头(比如关键点头),结构上也很方便。

4.2 修改 Detect 类

yolov8 的模型定义中,检测头是动态加载的。我们需要修改ultralytics/nn/modules/head.py里的Detect类。

核心思路是:在forward中不再把cv2cv3的结果cat在一起,而是分别返回。

下面给出修改后的核心代码:

class Detect(nn.Module): def __init__(self, nc=80, ch=()): super().__init__() self.nc = nc self.nl = len(ch) self.reg_max = 16 self.no = nc + self.reg_max * 4 self.stride = torch.zeros(self.nl) c2, c3 = max((16, ch[0] // 4, self.reg_max * 4)), max(ch[0], min(self.nc, 100)) self.cv2 = nn.ModuleList( nn.Sequential(Conv(x, c2, 3), Conv(c2, c2, 3), nn.Conv2d(c2, 4 * self.reg_max, 1)) for x in ch ) self.cv3 = nn.ModuleList( nn.Sequential(Conv(x, c3, 3), Conv(c3, c3, 3), nn.Conv2d(c3, self.nc, 1)) for x in ch ) self.dfl = DFL(self.reg_max) def forward(self, x): shape = x[0].shape dfl_out = [] cls_out = [] for i in range(self.nl): reg_branch = self.cv2[i](x[i]) cls_branch = self.cv3[i](x[i]) # 回归分支解码 b, c, h, w = reg_branch.shape reg_branch = reg_branch.view(b, 4, self.reg_max, h * w).transpose(2, 3).reshape(b, 4, h * w) dfl_out.append(self.dfl(reg_branch)) # 分类分支 sigmoid cls_out.append(torch.sigmoid(cls_branch.reshape(b, self.nc, h * w))) if self.training: # 训练阶段返回回归原始特征和分类特征,便于 loss 计算 return dfl_out, cls_out # 推理阶段:分尺度转换为 xywh 格式 anchors, strides = self.make_anchors(x, self.stride, 0.5) results = [] for i in range(self.nl): d = dfl_out[i].transpose(1, 2) c = cls_out[i].transpose(1, 2) results.append((d, c)) return results

这段代码里有几个关键点需要解释一下:

  • reg_branch要经过 DFL 解码才能得到边界框坐标。
  • cls_branch需要做 sigmoid,得到类别概率。
  • 训练阶段和推理阶段返回的格式不一样。训练阶段需要保留原始特征,方便 loss 计算;推理阶段输出解码后的坐标和类别概率,方便后处理直接使用。

当然,实际工程中,很多人不会直接在主源码里大改,而是继承Detect写一个DetectDual类。这样改动更小,也不容易影响原有逻辑。比如:

class DetectDual(Detect): def forward(self, x): # 重写 forward,拆分输出 ...

然后在模型的 yaml 配置里,把 head 的类型改成DetectDual

如果你用的是自定义数据集,还要注意nc改成自己的类别数。比如检测车辆和行人,nc=2

4.3 修改 Loss 计算逻辑

输出结构变了,loss 计算也必须跟着改。

默认情况下,YOLOv8 的 loss 变化在ultralytics/utils/loss.py文件里调用。原来的forward返回拼接结果,loss 会从拼接结果里切分分类分支和回归分支。

改成双头输出后,loss 函数需要直接接收两个分支的结果。

修改原则是:分类 loss 只依赖分类分支输出,回归 loss 只依赖回归分支输出。点开 v8DetectionLoss 类,把原先从pred中切分pred_distripred_scores的部分,改成直接接收双头结果。

大致逻辑如下:

class v8DetectionLoss: def __init__(self, model): # 原有初始化逻辑保持不变 ... def __call__(self, preds, batch): # preds 变成了 (dfl_out, cls_out) pred_distri, pred_scores = preds ...

需要注意的是,训练阶段我们返回的是未解码的回归特征,loss 内部会通过 DFL 计算分布损失。保持和官方一致的流程即可。

4.4 修改导出逻辑

模型训练完以后,要导出 ONNX。导出的逻辑在ultralytics/nn/tasks.pyModel.exportexporter.py里。

默认导出时,会把 forward 的输出拼接成一个维度输出。改双头后,导出时会输出两个节点,这符合我们的需求。

导出命令:

yolo export model=runs/train/exp/weights/best.pt format=onnx opset=12

或者直接用 Python API:

from ultralytics import YOLO model = YOLO("runs/train/exp/weights/best.pt") model.export(format="onnx", opset=12)

导出后,可以用onnxruntime验证输出节点数:

import onnxruntime as ort sess = ort.InferenceSession("best.onnx") for inp in sess.get_inputs(): print("input:", inp.name, inp.shape) for out in sess.get_outputs(): print("output:", out.name, out.shape)

如果修改成功,这里应该能看到两个输出节点,分别对应回归结果和分类结果。

4.5 训练验证

以 COCO128 这个小数据集为例,快速验证改造后的流程能不能跑通:

yolo detect train data=coco128.yaml model=yolov8s.yaml epochs=20 imgsz=640

如果数据集路径不对,可以先下载 COCO128:

from ultralytics.data import utils utils.download_dataset("https://ultralytics.com/assets/coco128.zip")

训练完成后,重点检查两点:

  1. 训练 loss 是否正常下降。
  2. 导出 ONNX 时输出节点是否为两个。

如果 loss 曲线明显震荡或不下降,优先检查是不是双头后的 loss 计算维度不匹配。

5. RK3588 部署:ONNX 导出与 RKNN 转换

5.1 ONNX 导出注意事项

在改完双头输出后,ONNX 导出有几个坑要注意。

第一个是 opset 版本。RKNN Toolkit2 对 ONNX opset 的兼容性有限,一般建议固定在 12 或 13。太高版本可能导致某些算子无法解析。

第二个是动态维度。因为 RKNN 转换时通常要指定固定输入尺寸,建议导出时就把 batch 和宽高固定:

model.export(format="onnx", opset=12, imgsz=[640, 640], batch=1)

第三个是多输出节点的命名。导出后最好确认一下输出的名称,因为后面 RKNN 转换时可能要用名称索引。

5.2 RKNN Toolkit2 转换配置

RK3588 对应的是 rknn-toolkit2,注意不是老版本 rknn-toolkit。

在 PC 上安装 rknn-toolkit2 后,写一个转换脚本:

from rknn.api import RKNN rknn = RKNN() # 配置量化与预处理的参数 rknn.config( mean_values=[[0, 0, 0]], std_values=[[255, 255, 255]], target_platform="rk3588" ) # 加载 ONNX 模型 ret = rknn.load_onnx(model="best.onnx") if ret != 0: print("load onnx failed") exit(1) # 构建 RKNN 模型 ret = rknn.build(do_quantization=True, dataset="dataset.txt") if ret != 0: print("build failed") exit(1) # 导出 RKNN 模型 ret = rknn.export_rknn("best.rknn") if ret != 0: print("export failed") exit(1) rknn.release()

其中dataset.txt是量化校准数据集列表,每行一张图片路径:

img_001.jpg img_002.jpg img_003.jpg

量化校准集的图片建议从训练集中挑选 200 到 500 张,覆盖不同光照和目标形态。

5.3 板端推理与后处理

best.rknn拷贝到 RK3588 板子上,使用 RKNN Python API 做推理。

核心代码如下:

import numpy as np from rknnlite.api import RKNNLite rknn = RKNNLite() ret = rknn.load_rknn("best.rknn") if ret != 0: print("load rknn failed") exit(1) ret = rknn.init_runtime() if ret != 0: print("init runtime failed") exit(1) # 读取并预处理图片 img = cv2.imread("test.jpg") img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) img = cv2.resize(img, (640, 640)) img = img.astype(np.float32) / 255.0 img = img.transpose(2, 0, 1)[None] # 推理,outputs 是一个列表 outputs = rknn.inference(inputs=[img]) # 第一个输出:回归结果 # 第二个输出:分类结果 reg_out = outputs[0] cls_out = outputs[1] print("reg_out shape:", reg_out.shape) print("cls_out shape:", cls_out.shape)

这时候后处理就变得非常直观:回归输出直接 reshape 成[1, 8400, 4],分类输出 reshape 成[1, 8400, nc]。然后做阈值过滤、NMS 就可以了。

NMS 部分可以沿用 YOLOv8 官方后处理思路,也可以集成一些端侧加速库。

6. 常见问题与排查

下面这些问题是双头改造和 RK3588 部署过程中比较常遇到的,整理成一个排查表格。

问题现象常见原因解决思路
训练 loss 不下降双头输出后 loss 计算维度不匹配打印两个分支的 shape,逐一核对
导出 ONNX 后只有一个输出节点forward 里还是默认 cat 逻辑检查 Detect 类 forward 是否真的改到位
ONNX 转 RKNN 失败opset 版本太高或不支持的算子把 opset 设为 12,检查是否有自定义算子
RKNN 推理输出 shape 不对输出节点索引和预期不一致用 Netron 打开 ONNX 查看节点顺序
量化后精度下降明显校准图片太少或不具代表性增加校准集图片,覆盖多种场景
后处理出框位置偏移输出坐标解码方式变了确认 DFL 解码和 make_anchors 坐标逻辑

6.1 训练 loss 不下降

比较隐蔽的原因是make_anchors不同的调用方式导致 offset 不对。YOLOv8 官方在Detect的 forward 里使用make_anchors(x, self.stride, 0.5),如果你的双头实现里缺少这一步,边界框坐标的解码结果就会乱掉。

排查时可以在训练脚本里把预测结果打印出来,看 DFL 解码后的坐标范围是否在合理区间内。如果坐标数值明显异常,基本可以确定是 anchor 生成的问题。

6.2 转 RKNN 报算子不支持

YOLOv8 本身算子比较简单,一般不会出现不支持的算子。但如果你的模型结构里加了自定义模块,比如注意力机制或其他复杂操作,转 RKNN 时可能报错。

解决思路有两种:

一是简化模型结构,把自定义算子用 RKNN 支持的常见算子重写。

二是使用 RKNN 的算子自定义扩展功能。不过这个开发成本较高,不建议在初期尝试。

6.3 量化精度问题

RK3588 上做 INT8 量化后,精度下降在 1% 到 3% 以内通常是可接受的。如果下降太多,优先做以下检查:

  1. 校准集是否和训练集来自同一分布。
  2. 预处理是否一致。YOLOv8 训练时的归一化方式是除以 255,RKNN 配置里的std_values要对应设置成 255。
  3. 是否有对量化特别不友好的层,比如输出的数值范围很大。可以尝试对部分层做混合精度量化。

7. 工程建议与最佳实践

7.1 结构设计建议

双头输出不是越多越好。每增加一个输出头,模型参数量和计算量都会增加,对 RK3588 这种嵌入式平台来说,推理耗时也会随之上升。在改造之前,先明确业务痛点:

  • 是多任务需求,还是小目标需求?
  • 是精度瓶颈,还是后处理复杂度瓶颈?

明确需求后再确定双头的具体形态,避免无意义的结构膨胀。

7.2 训练技巧

双头模型训练时,两个分支的重要程度可能不一样。建议给分类分支和回归分支设置不同的 loss 权重,防止一个分支的梯度过大,压过另一个分支。

例如在 loss 函数里:

loss_cls = self.bce(cls_preds, cls_labels) loss_reg = self.dfl(reg_preds, reg_labels) loss = loss_cls * 0.5 + loss_reg * 1.0

具体的权重系数需要根据数据集特点调整。类别不平衡严重时,分类 loss 的权重可以适当提高。

另外,双头模型对初始学习率比较敏感。建议在预训练权重的基础上做 fine-tune,初始学习率不要太高,默认的 0.01 通常没问题。

7.3 部署侧建议

在 RK3588 上部署双头模型时,有几个工程细节值得注意。

第一,输入分辨率尽量固定。虽然 RKNN 支持动态输入,但固定输入尺寸可以获得更好的 NPU 计算性能和更低的首帧延迟。

第二,多线程和零拷贝。如果做实时视频流检测,建议用RKNNLite的多实例模式,每个线程绑定一个核心,配合 RK3588 的六核 CPU 和 NPU 并行,性能上限会高很多。

第三,后处理尽量向量化。Python 环境下逐锚点遍历 8400 个结果会很慢,建议用 NumPy 的向量化操作,或者直接改写成 C 扩展。

7.4 代码维护与版本管理

这类对官方源码的修改,强烈建议用 Git 管理,并且把改动点集中在少数几个文件里。每次升级 ultralytics 版本时,改动点越少,合并成本越低。

保存模型配置时,把 yaml 文件和改过的 head.py 一起备份。RKNN 转换时,模型结构依赖 yaml,训练超参数依赖 yaml,数据集配置依赖 yaml。没有 yaml,后续复现和调试会很被动。

8. 总结与下一步

本文围绕 RK3588 + YOLOv8 的端侧部署场景,完整拆解了 YOLOv8 输出层的结构,解释了单头输出和双头输出的区别,并给出了从源码修改、训练、导出 ONNX、转 RKNN 到板端推理的完整流程。如果你也在做 RK3588 端侧目标检测,并且有自定义输出结构的需求,这套改造思路可以直接参考。

所谓单头变双头,本质上不是“两个检测框输出”,而是把模型的任务空间拆得更细,让后续的工程部署更加灵活。真正理解了Detect类的 forward 流程后,你还可以举一反三,改成第三个头、第四个头,或者在输出层加入后处理需要的附加信息。

下一步可以继续深入的方向:

  • 在双头基础上加一个关键点输出头,实现检测 + 姿态估计。
  • 把分类分支改成多标签分类,用于同时输出多个属性。
  • 尝试将双头模型和 DeepSORT 等跟踪算法结合,做实时多目标跟踪。

如果在改动的过程中遇到问题,欢迎在评论区交流。动手改一遍源码,比看十篇教程都管用。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/7 1:45:31

滑环与无线遥测信号质量实测对比:旋转机械测试到底选谁

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/7 1:44:20

STM32实战:Y01-3IN1空气质量传感器与OLED显示完整教程

1. 项目到底要做什么:Y01-3IN1和OLED的搭配逻辑手里有一块Y01-3IN1空气质量模块,又有一块0.96寸OLED,想把两者接到STM32上,让屏幕实时显示PM2.5、温湿度和甲醛浓度。这个需求听起来简单,但实际做的时候,串口…

作者头像 李华
网站建设 2026/9/7 1:43:50

大模型蒸馏实战指南:从原理到部署的关键技术解析

简介:大模型(LLMs)蒸馏面.pdf 是一份聚焦大模型知识蒸馏的面试梳理笔记,主要面向备战 AI 算法岗位的求职者,以及对模型压缩、高效部署感兴趣的工程师和研究者。全篇以问答形式整理知识蒸馏的核心脉络:教师-…

作者头像 李华
网站建设 2026/9/7 1:42:37

通信技术基础课件全解析:知识框架与PPTX使用指南

简介:《通信技术基础(第二版)》全书电子讲义以PPT课件形式,面向通信技术初学者、高职院校师生及备考人员,系统讲解通信系统从信号产生、传输到接收的核心知识,帮助读者理清模拟通信与数字通信、基带传输与频…

作者头像 李华
网站建设 2026/9/7 1:42:34

图像镜像处理工具部署与测试全攻略:从环境配置到批量应用

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华