模型轻量化策略
模型尺寸选择
| 模型 | 参数 (M) | mAP | CPU 推理 | 适用场景 |
|---|
| YOLO26n | 2.8 | 38.9 | 最快 | 边缘设备、嵌入式 |
| YOLO26s | 9.4 | 48.2 | 很快 | 移动端、Web |
| YOLO26m | 21.8 | 53.1 | 中等 | 服务器、高性能 |
| YOLO11n | 2.6 | 39.6 | 快 | 轻量部署 |
| YOLOv8n | 3.2 | 37.3 | 基准 | 通用 |
知识蒸馏
1
2
3
4
5
6
7
8
9
10
| # 大模型作为教师,小模型作为学生
teacher = YOLO("yolo26x.pt")
student = YOLO("yolo26n.yaml")
# 蒸馏训练(Ultralytics内置支持)
student.train(
data="data.yaml",
distill="yolo26x.pt", # 教师模型
distill_ratio=0.5, # 蒸馏损失比例
)
|
模型剪枝
结构化剪枝 vs 非结构化剪枝
| 类型 | 方法 | 稀疏模式 | 硬件加速 | 压缩率 |
|---|
| 非结构化 | 权重剪枝 | 随机稀疏 | 困难(需专用硬件) | 高 |
| 结构化 | 通道剪枝 | 规整稀疏 | 原生加速 | 中等 |
Torch Prune 通道剪枝示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
| import torch
import torch.nn.utils.prune as prune
# 对卷积层进行 L1 非结构化剪枝
model = YOLO("yolo26n.pt")
for name, module in model.model.named_modules():
if isinstance(module, torch.nn.Conv2d):
prune.l1_unstructured(module, name="weight", amount=0.3)
prune.remove(module, "weight") # 使剪枝永久化
# 通道剪枝使用 torch-pruning 库
# pip install torch-pruning
import torch_pruning as tp
model = YOLO("yolo26n.pt").model
DG = tp.DependencyGraph()
DG.build_dependency(model, example_inputs=torch.randn(1, 3, 640, 640))
# 按 L1 范数剪枝 20% 通道
pruning_plan = DG.get_pruning_plan(
model.model[4], tp.prune_conv,
pruning_dim=0, # 输出通道维度
idxs=list(range(0, 64, 5)) # 每 5 个通道保留一个
)
pruning_plan.exec()
|
剪枝比例指南
| 模型 | 安全剪枝比例 | 激进剪枝比例 | mAP 损失 |
|---|
| YOLO26n | ≤20% | 20-40% | <1% / 2-5% |
| YOLO26s | ≤30% | 30-50% | <1% / 3-6% |
| YOLO26m | ≤40% | 40-60% | <1% / 3-8% |
| YOLOv8n | ≤20% | 20-35% | <1% / 2-4% |
模型剪枝与量化
导出时量化
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| model = YOLO("yolo26n.pt")
# INT8量化(需要校准数据)
model.export(
format="engine", # TensorRT
int8=True,
data="data.yaml", # 校准数据集
batch=8,
)
# ONNX动态量化
model.export(
format="onnx",
dynamic=True,
simplify=True,
)
|
TensorRT INT8 校准流程详解
校准数据集准备
INT8 量化需要代表性校准数据来确定激活值的动态范围:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| import tensorrt as trt
from ctypes import c_size_t
class YOLOCalibrator(trt.IInt8EntropyCalibrator2):
def __init__(self, data_loader, cache_file="calibration.cache"):
trt.IInt8EntropyCalibrator2.__init__(self)
self.data_loader = data_loader
self.cache_file = cache_file
self.buffer_size = 0
def get_batch_size(self):
return 8
def get_batch(self, names):
try:
batch = next(self.data_loader)
batch = batch.cpu().numpy()
return [batch.astype(np.float32).ctypes.data_as(c_size_t)]
except StopIteration:
return None
|
INT8 vs FP16 vs FP32 对比
| 精度 | 存储大小 | 推理速度 (GPU) | mAP 损失 | 适用场景 |
|---|
| FP32 | 100% (基准) | 1× | 0% | 训练、精度优先 |
| FP16 | 50% | 1.5-2× | <0.5% | 通用推理 |
| INT8 | 25% | 2-4× | 1-3% | 边缘部署、实时 |
校准算法选择
TensorRT 提供两种校准算法:
- Entropy (IInt8EntropyCalibrator2):基于 KL 散度,最小化量化前后的信息损失。推荐用于大多数视觉模型,包括 YOLO。
- Min-Max (IInt8MinMaxCalibrator):基于绝对值范围,简单快速,但对异常值敏感。适合权重分布对称的模型。
经验建议:使用 Entropy 校准器 + 500-1000 张校准图片,batch size=8-16,覆盖各类场景。
延迟与精度权衡
| 校准集大小 | INT8 mAP | FP16 mAP | 延迟 (ms) | 加速比 |
|---|
| 100 张 | 51.2 | 52.8 | 3.2 | 3.1× |
| 500 张 | 52.1 | 52.8 | 3.2 | 3.1× |
| 1000 张 | 52.5 | 52.8 | 3.2 | 3.1× |
| 2000 张 | 52.7 | 52.8 | 3.2 | 3.1× |
ONNX 量化详细指南
动态量化 vs 静态量化
| 方式 | 校准数据 | 权重精度 | 激活精度 | 加速比 | 适用场景 |
|---|
| 动态量化 | 不需要 | INT8 | FP32 (动态计算) | 1.5-2× | CPU、NLP 模型 |
| 静态量化 | 需要 | INT8 | INT8 | 2-3× | CPU、视觉模型 |
QDQ (Quantize-Dequantize) 节点
ONNX 静态量化通过在计算图中插入 QDQ 节点来模拟量化效果:
1
2
| 输入 → QuantizeLinear → Conv → DequantizeLinear → ... → 输出
scale, zp scale, zp
|
ONNX Runtime 在推理时会自动折叠 QDQ 节点为高效的 INT8 内核。
ONNX Runtime 量化工具
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
| from onnxruntime.quantization import quantize_dynamic, quantize_static
from onnxruntime.quantization import CalibrationMethod, QuantType, QuantFormat
# 动态量化(无需校准数据)
quantize_dynamic(
"yolo26n.onnx",
"yolo26n_dynamic.onnx",
weight_type=QuantType.QInt8,
)
# 静态量化(需要校准数据)
class YOLOCalibDataReader:
def __init__(self, image_paths, batch_size=8):
self.images = [self.preprocess(img) for img in image_paths]
self.batch_size = batch_size
self.index = 0
def get_next(self):
if self.index >= len(self.images):
return None
batch = self.images[self.index:self.index + self.batch_size]
self.index += self.batch_size
return {"images": np.stack(batch)}
quantize_static(
"yolo26n.onnx",
"yolo26n_static.onnx",
calibration_data_reader=YOLOCalibDataReader(calibration_images),
quant_format=QuantFormat.QDQ, # QDQ 格式,支持更多优化
per_channel=True, # 逐通道量化精度更高
activation_type=QuantType.QInt8,
weight_type=QuantType.QInt8,
calibrate_method=CalibrationMethod.Entropy,
)
|
逐张量 vs 逐通道量化
| 方式 | 粒度 | 精度 | 计算开销 | 推荐场景 |
|---|
| 逐张量 (Per-Tensor) | 每个权重张量一个 scale | 较低 | 无 | 小型模型、快速部署 |
| 逐通道 (Per-Channel) | 每个输出通道一个 scale | 较高 | 轻微 | 精度敏感、YOLO 等检测模型 |
对于 YOLO 模型,强烈推荐逐通道量化,因为检测头部的通道差异性较大。
NCNN 移动端优化
NCNN 简介
NCNN 是腾讯开发的移动端神经网络推理框架,专为手机端 CPU/Vulkan 优化。相比于 TensorRT(仅限 NVIDIA GPU)和 ONNX Runtime(全平台),NCNN 在 ARM CPU 和 Adreno GPU 上有显著的性能优势。
Vulkan GPU 加速
1
2
3
4
5
6
| # 将 ONNX 转换为 NCNN 格式
onnx2ncnn yolo26n.onnx yolo26n.param yolo26n.bin
# 使用 Vulkan GPU 加速推理
ncnnoptimize yolo26n.param yolo26n.bin yolo26n_opt.param yolo26n_opt.bin 1
# 最后一个参数: 0=CPU, 1=Vulkan
|
FP16 存储
NCNN 默认使用 FP16 存储权重,减少 50% 模型体积:
1
2
3
4
5
6
7
| # FP16 存储模式
ncnn2table --param=yolo26n.param --bin=yolo26n.bin \
--input=calibration_images --output=table.bfp32 \
--mean=0,0,0 --norm=255,255,255 --size=640,640
ncnn2int8 yolo26n.param yolo26n.bin yolo26n_int8.param \
yolo26n_int8.bin table.bfp32
|
NCNN 算子融合
NCNN 自动执行以下算子融合优化:
- Conv + BN + ReLU → ConvReLU
- Conv + BN → Conv
- Conv + ReLU → ConvReLU
- 相邻的 1×1 Conv 合并
无需手动操作,ncnnoptimize 工具自动完成。
ARM CPU 优化(汇编内核)
NCNN 对 ARM CPU 提供了深度优化的汇编内核:
| CPU 架构 | 指令集 | 加速比 | 适用设备 |
|---|
| ARMv7 | NEON | 2-3× | 老旧手机 |
| ARMv8 | NEON | 3-4× | 主流 Android |
| ARMv8.2 | SVE | 4-6× | 旗舰手机、Apple M 系列 |
| ARMv9 | SVE2 | 5-8× | 最新旗舰 |
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| // C++ 集成示例
#include "net.h"
ncnn::Net yolo;
yolo.load_param("yolo26n_opt.param");
yolo.load_model("yolo26n_opt.bin");
ncnn::Mat in = ncnn::Mat::from_pixels_resize(
image_data, ncnn::Mat::PIXEL_BGR,
w, h, 640, 640
);
ncnn::Extractor ex = yolo.create_extractor();
ex.input("images", in);
ncnn::Mat out;
ex.extract("output", out);
|
精度提升技巧
多尺度训练与测试
1
2
3
4
5
6
7
8
9
10
11
12
| # 训练时多尺度
model.train(
imgsz=640,
multi_scale=True, # 自动在 ±50% 范围内变化
)
# 推理时多尺度增强
results = model(
"test.jpg",
imgsz=[640, 800, 1024], # 多尺度融合
augment=True, # TTA测试时增强
)
|
类别不平衡处理
方法 1:损失权重调整
1
2
3
4
5
| model.train(
box=7.5, # 框回归权重
cls=0.5, # 分类权重(少类别时降低)
dfl=1.5,
)
|
方法 2:Focal Loss 集成
YOLO 默认使用 BCE 损失,对于极端类别不平衡的场景可替换为 Focal Loss:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| import torch.nn.functional as F
class FocalLoss(torch.nn.Module):
def __init__(self, alpha=0.25, gamma=2.0):
super().__init__()
self.alpha = alpha
self.gamma = gamma
def forward(self, pred, target):
ce_loss = F.binary_cross_entropy_with_logits(
pred, target, reduction="none"
)
pt = torch.exp(-ce_loss)
focal_loss = self.alpha * (1 - pt) ** self.gamma * ce_loss
return focal_loss.mean()
# 在训练回调中替换损失函数
# Ultralytics 自定义损失:继承 DetectionLoss 并重写 bcecls
|
方法 3:类别权重自动计算
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
| import numpy as np
def compute_class_weights(labels_path, num_classes=80):
"""根据标注频率计算类别权重"""
class_counts = np.zeros(num_classes)
# 统计每个类别的标注框数量
for label_file in labels_path.glob("*.txt"):
boxes = np.loadtxt(label_file) # [class_id, x, y, w, h]
if boxes.ndim == 1:
boxes = boxes.reshape(1, -1)
classes = boxes[:, 0].astype(int)
for c in classes:
class_counts[c] += 1
# 使用中位数频率平衡 (Median Frequency Balancing)
median_freq = np.median(class_counts[class_counts > 0])
class_weights = median_freq / (class_counts + 1e-6)
class_weights = np.clip(class_weights, 0.1, 10.0) # 限制范围
return class_weights
# weights = compute_class_weights("datasets/coco/labels/train/")
# 将 weights 传入损失函数
|
方法 4:重采样策略
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
| # 重采样:在 data.yaml 中设置采样权重
# 或者使用 torch 的 WeightedRandomSampler
from torch.utils.data import WeightedRandomSampler
from collections import Counter
def create_balanced_sampler(labels_path, num_classes=80):
class_counts = Counter()
for label_file in labels_path.glob("*.txt"):
boxes = np.loadtxt(label_file)
if boxes.ndim == 1:
boxes = boxes.reshape(1, -1)
for c in boxes[:, 0].astype(int):
class_counts[c] += 1
# 样本权重 = 1 / 类别频率
weights = [1.0 / class_counts[c] for c in range(num_classes)]
# 每张图片的权重为其所有标注类别的平均权重
sample_weights = []
for label_file in labels_path.glob("*.txt"):
boxes = np.loadtxt(label_file)
if boxes.ndim == 0:
sample_weights.append(1.0)
elif boxes.ndim == 1:
sample_weights.append(weights[int(boxes[0])])
else:
sample_weights.append(np.mean(
[weights[int(c)] for c in boxes[:, 0]]
))
return WeightedRandomSampler(
sample_weights, len(sample_weights), replacement=True
)
|
各类方法对比
| 方法 | 实现难度 | 训练稳定性 | 效果 | 推荐场景 |
|---|
| 损失权重调整 | ★☆☆☆☆ | 稳定 | 一般 | 简单不平衡 |
| Focal Loss | ★★★☆☆ | 需调参 | 优秀 | 极度不平衡 |
| 类别权重 | ★★☆☆☆ | 较稳定 | 良好 | 长尾分布 |
| 重采样 | ★★☆☆☆ | 易过拟合 | 良好 | 数据充足时 |
消融实验方法论
什么是消融实验
消融实验(Ablation Study)是系统性地移除模型中某个组件或功能,观察其对最终性能的影响。在 YOLO 优化中,消融实验帮助我们回答以下问题:
- 某个优化模块是否真正有效?
- 不同优化组合是否存在正/负交互?
- 每个模块的边际收益是多少?
消融实验设计原则
- 单变量原则:每次只移除一个优化,保持其他不变
- 基线可重复:所有实验从相同的基线模型开始
- 控制变量:保持训练超参数、数据增强、种子一致
- 量化指标:至少报告 mAP、延迟、模型大小三个维度
YOLO 优化消融实验结果
| 优化组合 | mAP@50 | mAP@50:95 | 参数量 (M) | 延迟 (ms) | 模型大小 (MB) |
|---|
| Baseline | 52.8 | 38.9 | 2.8 | 4.2 | 5.6 |
| + KD | 54.1 (+1.3) | 40.5 (+1.6) | 2.8 | 4.2 | 5.6 |
| + INT8 | 52.5 (-0.3) | 38.2 (-0.7) | 2.8 | 1.5 (2.8×) | 1.6 |
| + Multi-scale | 53.4 (+0.6) | 39.8 (+0.9) | 2.8 | 4.3 | 5.6 |
| + TTA | 54.6 (+1.8) | 41.2 (+2.3) | 2.8 | 12.6 (3×) | 5.6 |
| + Pruning | 52.2 (-0.6) | 38.0 (-0.9) | 1.9 | 3.4 (1.2×) | 4.0 |
| All Combined | 56.3 (+3.5) | 42.8 (+3.9) | 1.9 | 1.5 (2.8×) | 1.6 |
如何解读消融结果
- 正收益组件:知识蒸馏 (+1.3 mAP) 和多尺度训练 (+0.6 mAP) 是明确的增益来源
- 加速组件:INT8 量化虽然 mAP 有轻微下降 (-0.3),但延迟降低 2.8×,是部署场景的必备
- 权衡组件:TTA 提升最大 (+1.8 mAP),但延迟增加 3×,适合精度优先场景
- 组合效应:全部组合后 mAP 提升 3.5(超过各组件单独之和的简单叠加),说明 KD + 量化 + 多尺度存在正交互
- 边际递减:当叠加 4 个以上优化后,每个新组件的边际收益逐渐降低
性能分析与基准测试
关键性能指标
| 指标 | 单位 | 说明 | 测量方法 |
|---|
| 延迟 (Latency) | ms | 单张图片推理时间 | Warmup + 平均 100 次 |
| 吞吐量 (Throughput) | FPS | 每秒处理图片数 | Batch 推理 |
| 内存占用 | MB | GPU/CPU 峰值内存 | nvidia-smi / psutil |
| 计算量 | GFLOPs | 浮点运算次数 | thop / ptflops |
| 模型大小 | MB | 磁盘存储占用 | os.path.getsize |
延迟测量:Warmup + 多次平均
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
| import time
import numpy as np
import torch
def benchmark_latency(model, input_tensor, num_warmup=50, num_runs=200):
# Warmup
for _ in range(num_warmup):
_ = model(input_tensor)
# 正式测量
if torch.cuda.is_available():
torch.cuda.synchronize()
timings = []
for _ in range(num_runs):
start = time.perf_counter()
_ = model(input_tensor)
if torch.cuda.is_available():
torch.cuda.synchronize()
timings.append((time.perf_counter() - start) * 1000) # ms
return {
"mean": np.mean(timings),
"std": np.std(timings),
"p50": np.percentile(timings, 50),
"p95": np.percentile(timings, 95),
"p99": np.percentile(timings, 99),
}
|
吞吐量测量
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
| def benchmark_throughput(
model, batch_size=32, input_size=(3, 640, 640), num_batches=100
):
dummy_input = torch.randn(batch_size, *input_size).cuda()
# Warmup
for _ in range(10):
_ = model(dummy_input)
torch.cuda.synchronize()
start = time.perf_counter()
for _ in range(num_batches):
_ = model(dummy_input)
torch.cuda.synchronize()
total_time = time.perf_counter() - start
total_images = batch_size * num_batches
throughput = total_images / total_time
return {
"throughput": throughput,
"total_time": total_time,
"batch_size": batch_size,
}
|
Torch Profiler 使用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| from torch.profiler import profile, record_function, ProfilerActivity
with profile(
activities=[ProfilerActivity.CPU, ProfilerActivity.CUDA],
record_shapes=True,
profile_memory=True,
with_stack=True,
) as prof:
with record_function("model_inference"):
output = model(input_tensor)
# 打印耗时排名
print(prof.key_averages().table(
sort_by="cuda_time_total", row_limit=20
))
# 导出为 Chrome Trace 可视化
prof.export_chrome_trace("trace.json")
# 在 chrome://tracing/ 中打开
|
ONNX Runtime 性能分析
1
2
3
4
5
6
7
8
9
10
11
12
13
| import onnxruntime as ort
# 启用会话 profiling
sess_options = ort.SessionOptions()
sess_options.enable_profiling = True
sess = ort.InferenceSession("yolo26n.onnx", sess_options)
# 运行推理
outputs = sess.run(None, {"images": np_input})
# 获取 profile 数据
prof_file = sess.end_profiling()
# 输出 profiling_*.json 文件,可在 chrome://tracing/ 中查看
|
NVIDIA Nsight Systems 分析
1
2
3
4
5
6
| # 安装: https://developer.nvidia.com/nsight-systems
# 命令行 profiling
nsys profile -o yolo_profile -t cuda,nvtx python inference.py
# 可视化结果
# 在 Nsight Systems GUI 中打开 yolo_profile.qdrep
|
不同部署方案基准对比
| 方案 | 延迟 (ms) | 吞吐量 (FPS) | 内存 (MB) | 模型大小 (MB) | 设置难度 |
|---|
| PyTorch (FP32) | 4.2 | 238 | 850 | 5.6 | 最低 |
| ONNX (FP32) | 3.1 | 322 | 620 | 5.5 | 低 |
| ONNX (INT8) | 2.0 | 500 | 480 | 1.5 | 中等 |
| TensorRT (FP16) | 2.5 | 400 | 420 | 2.8 | 中等 |
| TensorRT (INT8) | 1.5 | 666 | 380 | 1.6 | 较高 |
| NCNN (FP16) | 8.5 (CPU) | 117 | 320 | 2.8 | 中等 |
| NCNN (INT8) | 6.0 (CPU) | 166 | 280 | 1.0 | 较高 |
各版本优化策略对比
| 优化方向 | YOLOv8 | YOLO11 | YOLO26 |
|---|
| 架构优化 | C2f | C2f 优化 | 全新简化架构 |
| 训练优化 | SGD | SGD | MuSGD |
| 损失函数 | DFL+CIoU | DFL+CIoU | ProgLoss+STAL |
| 部署友好 | 良好 | 良好 | 最佳 (无 NMS) |
| CPU 优化 | 基准 | +25% | +43% |