写 YOLO26 改进,最怕的不是找不到新模块,而是找到了一堆模块,却不知道它到底该放在哪里、解决了什么问题、能不能在低算力设备上跑起来。
CVPR 2025 上出现的 nnWNet,以及它里面被反复提到的 LSB 模块,正好踩中了这个痛点。LSB 模块的核心思路如果用一句话来讲,就是:在深层网络传递特征时,把局部细节“连续”地往前送,而不是让每一层都重新提取一遍。这种“局部特征连续传输”的思路,恰恰是 YOLO 这类单阶段检测器在 neck 特征融合阶段最需要的东西。
这篇文章不打算给你贴一大堆看不懂的论文公式,而是把 LSB 模块拆开,讲清楚它为什么对 YOLO26 有改进价值,然后提供一个可以直接接入的 PyTorch 示例,再从数据训练、低光检测到 RK3588 端侧部署,完整走一遍落地流程。
先说明一点:截至发文时,nnWNet 的官方源码还没有完整公开,因此本文的 LSB 模块是基于“局部特征连续传输”的原理复现的一个可运行版本,用来验证思路,不是论文原版代码。如果你手上已经有官方实现,可以在阅读完原理后直接替换。
1. 为什么 YOLO26 也需要改进模块
很多同学会有疑问:YOLO26 都出到第 26 版了,网络结构已经足够强,为什么还要加模块?
这种想法只对了一半。YOLO26 在主干特征提取、训练收敛速度和部署友好性上确实做了大量优化,但单阶段检测器有一个长期存在的结构性问题没有完全解决:深层网络在逐层下采样和上采样的过程中,局部细节特征会不断被稀释。
通俗解释一下。一张 640×640 的图,经过 backbone 逐层下采样后,到 neck 阶段特征图已经缩小到 20×20 左右。这时候小目标、遮挡目标、低光环境下的弱纹理目标,响应已经被压缩得特别弱。传统 FPN 做的事情是把浅层特征和深层特征拼在一起,但拼接只发生在一两个节点上,信息传递路径是“断”的——浅层细节没有一条连续通道可以跟着深层语义一起往前走。
LSB 模块要解决的,就是这个“断点”问题。它把局部特征提取和跨层连续传输拆成两个分支:一个分支负责在当前层继续做局部特征精炼,另一个分支专门把上一层或者前面几层的特征“接力棒”式地传下来,两个分支融合后再回到主网络。
这套思路对 YOLO26 的意义很大。因为 YOLO26 本身的训练效率已经很高,你并不需要把一个模块塞到每一层里去,只需要在 neck 的关键融合位置加入 LSB 模块,让低级细节和高级语义之间形成一条连续的传递通道,检测精度就能看到比较明显的变化。
这不是一个“骗参数”的模块。它增加的计算量很小,核心开销只有两个卷积和一个拼接操作,非常适合在 GPU 或者 RK3588 这种端侧设备上做轻量化改进。
2. nnWNet 与 LSB 模块的改进原理
先简单说下 nnWNet。从论文标题和摘要来看,nnWNet 的研究点放在“局部特征连续传输”这一方向,LSB 模块(Local Sequential Block,这里按字面理解为局部连续传输模块)是它提出的核心组件。论文本身做的是医学图像分割方向,但这类跨层特征连续传输的思想完全可以迁移到目标检测里来。
为什么说它能迁移?目标检测和图像分割共享同一个底层需求:网络既要知道“这是什么东西”(深层语义),又要知道“这个东西精确在哪”(浅层空间细节)。分割对像素级边界要求更高,检测对目标框精度要求更高,两者面对的困难是一致的。
LSB 模块的设计逻辑可以拆成三个关键词:
第一是“局部”。模块在当前特征图上做局部卷积,不改变特征图的分辨率,尽量减少信息损耗。这样做的好处是模块对输入尺寸不敏感,可以插在 YOLO26 的任意一个 neck 节点上。
第二是“连续”。模块接收上一个节点传来的特征,做一次轻量映射后和当前特征融合。这个“上一个节点”可以是相邻层,也可以是相隔几层的特征。连续传输的核心是让信息不是一次性拼接,而是像接力棒一样逐级传下去。
第三是“即插即用”。模块输入输出通道数一致,分辨率一致,意味着接进 YOLO26 后,不需要额外修改 loss、不需要改 anchor、不需要改前后处理,只需要改模型结构定义。
如果只看表面,LSB 模块似乎只是在特征拼接前多加了一个残差结构。但实际上,它的关键差异在于传输方向。普通 FPN 是自顶向下单向传递,LSB 的连续传输强调的是“局部”与“整体”之间的双向互补:当前层既要用上一层的特征来补充语义,也要把当前层的局部细节继续传给下一层。
这种机制在低光环境下尤其有效。低光图像中的目标边缘本身就很弱,如果只在某一层做一次融合,细节很容易在上采样过程中丢失。而 LSB 模块逐级传递局部细节,相当于给浅层特征修了一条“高速通道”。
3. 复现一个 LSB 风格模块
在没有官方源码的情况下,我们可以根据“局部特征连续传输”的原理,先写一个最小可用的 PyTorch 模块。这里要注意,我给的版本是一个原理复现,用于跑通 YOLO26 的接入流程,不保证和论文里的结构完全一致。
建议单独建一个文件lsb_module.py,代码如下:
# 文件路径:lsb_module.py import torch import torch.nn as nn class LSBBlock(nn.Module): """ LSB 风格模块:局部特征精炼 + 跨层连续传输 输入 x:当前层特征 输入 x_prev:上一层(或前面某一层)特征 输出:与 x 形状相同 """ def __init__(self, in_channels, hidden_channels=None): super().__init__() hidden_channels = hidden_channels or in_channels # 局部特征精炼分支 self.local_branch = nn.Sequential( nn.Conv2d(in_channels, hidden_channels, 3, 1, 1, bias=False), nn.BatchNorm2d(hidden_channels), nn.SiLU(inplace=True), nn.Conv2d(hidden_channels, in_channels, 3, 1, 1, bias=False), nn.BatchNorm2d(in_channels), ) # 连续传输分支:把前序特征映射到当前尺度 self.continuity_branch = nn.Sequential( nn.Conv2d(in_channels, hidden_channels, 1, bias=False), nn.BatchNorm2d(hidden_channels), nn.SiLU(inplace=True), ) self.act = nn.SiLU(inplace=True) def forward(self, x, x_prev=None): local_out = self.local_branch(x) if x_prev is not None: if x_prev.shape[-2:] != x.shape[-2:]: x_prev = nn.functional.interpolate( x_prev, size=x.shape[-2:], mode="bilinear", align_corners=False ) continuity_out = self.continuity_branch(x_prev) # 局部信息与连续传输信息拼接融合 fused = torch.cat([local_out, continuity_out], dim=1) else: fused = local_out # 简单残差连接,保证输入输出形状一致 out = x + fused[:, : x.shape[1]] return self.act(out)这段代码有几个关键点需要说明。
local_branch是两个 3×3 卷积组成的瓶颈结构,负责在当前特征图上做局部细节精炼。第一个卷积先把通道数扩大到hidden_channels,第二个卷积再恢复到in_channels,目的是让模块有足够的表达能力,同时保持输入输出通道一致。
continuity_branch是 LSB 模块与普通残差网络最大的区别。它接收“前面的特征”x_prev,做一个 1×1 卷积映射,再和当前层的局部特征拼接。如果输入特征图尺寸不一致,就用双线性插值对齐。这样即使你把相隔两三层、分辨率不同的特征传进来,也能正常工作。
最后一个残差连接保证了模块是即插即用的。无论特征图是 20×20 还是 80×80,经过 LSBBlock 后,形状都不会变。所以你可以把它放在 YOLO26 的任意一个 neck 节点之后,而不用去改动 loss 和前处理。
这里要特别提醒:上面的实现只是一个“可运行的最小示例”。如果你从其他渠道拿到了 nnWNet 的原始代码,请以原始代码为准。不同版本的 LSB 模块在通道数、归一化方式、连接方式上可能有很大差异,不要照抄这个示例就当作论文复现。
4. 接入 YOLO26 的配置修改
现在进入正题:把 LSB 模块接进 YOLO26。
不同版本的 YOLO26 源码结构可能不一样,但无论是 Ultralytics 风格还是其他第三方仓库,接入新模块的基本流程是一样的:
第一步,把 LSBBlock 注册到模型的模块列表中。通常在 YOLO 系列源码里会有一个类似modules.py或者nn/modules.py的文件,里面定义了Conv、C2f、SPPF等基础模块。我们需要在文件末尾加入 LSBBlock 的导入。
第二步,在模型配置文件(一般是以.yaml结尾的结构文件)里添加 LSB 模块节点。
第三步,在代码里注册模块名。在 Ultralytics 风格的仓库里,通常会有一个parse_model函数,把 YAML 中的字符串映射到具体的类。
以一个典型的 YOLO neck 结构为例,修改后的 YAML 配置大致如下:
# 文件路径:yolo26_lsb.yaml(YAML 片段,仅展示 neck 部分) # 假设原 neck 中 P4 特征通过 C2f 之后是 P5 分支 # 我们在关键融合节点之后插入 LSB 模块 neck: - [-1, 6, Conv, [512, 3, 2]] # 下采样 - [-1, 1, C2f, [512, True]] - [-1, 1, LSBBlock, [512]] # 插入 LSB 模块,输入输出都是 512 通道 - [-1, 1, Conv, [256, 1, 1]] # 继续后续融合注意:上面这份 YAML 只是为了演示接入思路,不是某个具体 YOLO26 版本的完整配置。不同仓库的 YAML 关键字、参数位置可能不同,你需要打开自己下载的 yolo26 结构文件,把LSBBlock这一行插入到你希望增强的特征融合节点后面。
接下来还要注册模块类。在parse_model函数中,通常会有一行类似下面的判断代码:
# 在 parse_model 的模块映射中加上: elif m in {LSBBlock}: c1 = c2 = ch[f] args = [c1]这段代码的意思是:当读到 YAML 里的LSBBlock时,把上一层输出的通道数c1当作 LSB 模块的输入通道,同时模块输出通道也等于c1,这样后续层不用改。
完成这两步后,建议先做一个前向验证,用随机张量跑一遍模型,确认没有维度错误:
import torch from lsn_module import LSBBlock x = torch.randn(1, 512, 20, 20) x_prev = torch.randn(1, 512, 40, 40) block = LSBBlock(in_channels=512) out = block(x, x_prev) print(out.shape) # 期望输出 torch.Size([1, 512, 20, 20])这一步很重要。很多同学把模块写完后直接拿去训练,结果第一天就报 shape mismatch,浪费一整天。用随机张量前向验证一下,基本能排除 80% 的接入问题。
5. 训练自定义数据集与效果验证
模块接好了,接下来就是训练自己的数据集。这里以 YOLO 格式为例,不管你是用 Ultralytics 官方仓库还是第三方 YOLO26 仓库,数据格式大同小异。
先准备好dataset.yaml:
# 文件路径:dataset.yaml path: ./datasets/mydata train: images/train val: images/val nc: 3 names: 0: person 1: helmet 2: vehicle要注意几点:
path指向数据集根目录。train和val可以写绝对路径,也可以写相对path的路径。nc和names必须和你的标签保持一致。- 如果类别名称带空格,建议用下划线代替,例如
hard_hat而不是hard hat。
接下来启动训练。假如你使用的是 YOLO26 源码仓库,训练命令一般是:
yolo train model=yolo26n.pt data=dataset.yaml \ epochs=100 imgsz=640 batch=16 device=0如果yolo26n.pt这个权重文件名和实际下载的不一致,以你下载到的文件名为准。有些仓库可能叫yolo26s.pt、yolo26m.pt,不影响训练逻辑。
训练过程中要重点观察几个指标:
box_loss、cls_loss、dfl_loss是否平稳下降mAP50和mAP50-95是否持续上升- 训练后期验证集 loss 是否回升,判断是否过拟合
等训练结束后,加入 LSB 模块的模型会和 baseline 模型做对比。对比方式有两种:
第一种是直接比指标。把mAP50、mAP50-95、参数量、GFLOPs 拉成一张表,看 LSB 模块带来的收益。
第二种是可视化对比。用训练好的两个模型分别跑同一张验证图片,导出预测结果图,看小目标、密集目标的检测差异。很多情况下,指标可能只提升零点几个点,但可视化图里能明显看到小目标的召回率提高了。
我建议两种都做。因为 LSB 模块这种连续传输机制,最明显的改进往往不是整体 mAP 的大幅上涨,而是某些困难场景(小目标、遮挡、低光)的漏检减少。
这里还涉及一个判断:LSB 模块不一定每次都能带来收益。如果你训练的数据集本身目标很大、场景很干净,那连续传输的增益可能非常微小。如果训练后发现 mAP 反而下降了,也不要慌,先检查是不是学习率、epoch 数、数据增强等超参数不一致导致的。
6. 低光环境检测中的 LSB 适配
YOLO26 在低光环境下的检测是最近讨论度很高的一个方向。很多实际场景,比如夜间安防、地下车库、无人机夜间作业,图像整体对比度低、细节弱,普通 YOLO 模型的表现会明显下降。
LSB 模块的低光适配思路不在于“把图像变亮”,而在于“把微弱细节传得更远”。
低光图像经过 backbone 提取后,浅层还能保留一些边缘信息,但到深层时这些边缘已经被池化和卷积操作消耗殆尽。加入 LSB 模块后,浅层边缘可以通过连续传输分支一直传递到检测头部,相当于在弱信号环境中给网络搭了一条保真度更高的旁路。
如果你在低光数据集上训练,建议配合以下策略:
第一,数据增强中增加随机亮度调整和对比度调整。你不必专门去采集大量的夜间图像,只需要在白天图像上模拟低光效果,让模型学习到特征连续传输的泛化能力。Ultralytics 仓库里可以通过修改augment参数或自定义增强函数实现。
第二,训练时优先使用较小的imgsz。低光环境下目标本来就很模糊,如果强行用 1280 的大分辨率训练,网络可能需要更多的参数来拟合噪声,反而容易过拟合。建议从 640 开始,确认 LSB 模块有效后再考虑提升。
第三,部署前做一次轻量图像增强。这指的是在推理端做预处理,例如直方图均衡化或自适应伽马校正。LSB 模块解决的是网络内部的特征传递问题,而预处理解决的是输入图像质量的问题,两者是互补关系,不要混为一谈。
简单测试代码如下:
import cv2 import numpy as np def low_light_enhance(img, gamma=1.5): # 轻量级伽马校正,适合低光输入 table = np.array([ ((i / 255.0) ** (1.0 / gamma)) * 255.0 for i in range(256) ]).astype("uint8") return cv2.LUT(img, table)要注意:不是所有低光场景都适合伽马校正。如果图像本身噪声很大,盲目提亮会把噪声一起放大,反而影响检测效果。你需要在验证集上多试几组参数,选择 mAP 最高的组合。
7. RK3588 端侧部署:从 PyTorch 到 RKNN
模型训练完成后,下一步是把带 LSB 模块的 YOLO26 部署到 RK3588 上。RK3588 是瑞芯微推出的高性能 AIoT 芯片,NPU 算力能够支撑 YOLO 系列的端侧推理,但流程上有一个关键点:RK3588 的 NPU 不直接支持 PyTorch 模型,需要先导出 ONNX,再转换为 RKNN 格式。
第一步,导出 ONNX。使用 YOLO 仓库自带的导出命令:
yolo export model=runs/train/exp/weights/best.pt format=onnx imgsz=640 opset=12如果仓库没有直接支持导出,可以自己写一次 torch.onnx.export。导出时注意固定输入尺寸,尽量不用动态维度。RKNN 工具链对动态 shape 的支持比较有限,动态维度很容易在转换时报错。
第二步,安装 RKNN-Toolkit2。建议新建一个 Python 虚拟环境:
conda create -n rknn python=3.8 conda activate rknn pip install rknn-toolkit2-x.x.x-cp38-cp38-linux_x86_64.whl具体版本号请以瑞芯微官方发布的 whl 为准。安装完成后可以用python -c "from rknn.api import RKNN; print('ok')"验证。
第三步,ONNX 转 RKNN。这里给一个最小转换脚本:
# 文件路径:convert_to_rknn.py from rknn.api import RKNN rknn = RKNN() # 配置输入输出参数 rknn.config( mean_values=[[0, 0, 0]], std_values=[[255, 255, 255]], target_platform="rk3588", ) # 加载 ONNX 模型 rknn.load_onnx(model="yolo26n.onnx") # 构建 RKNN 模型,开启量化以降低显存和算力需求 rknn.build(do_quantization=True, dataset="dataset.txt") # 导出 RKNN 文件 rknn.export_rknn("yolo26n.rknn") rknn.release()其中dataset.txt里是用于量化校准的图片路径列表,每行一个路径,建议从验证集中随机抽取 200 张左右,覆盖不同亮度场景:
# 文件路径:dataset.txt ./samples/0001.jpg ./samples/0002.jpg ./samples/0003.jpg量化这一步非常关键。RKNN 默认把模型转成 INT8 推理,如果校准集选得不好,精度可能下降两三个点。低光检测场景下,建议在 dataset.txt 里混入一些夜间图像,让量化过程看到低光分布。
第四步,在 RK3588 板卡上推理。常见的做法是把 RKNN 模型放到板卡上,用 Python 的 RKNN Lite 接口加载,或者通过 C++ API 集成到业务代码里。C++ 部署时要注意 NPU 输入格式,有些 RKNN 版本要求 NHWC 布局,需要在预处理里做一次 transpose。
// 伪代码:RKNN C++ 端推理流程 // 1. rknn_init() 加载 .rknn 模型 // 2. rknn_query() 查询输入输出属性 // 3. 读取图像并 resize 到 640x640 // 4. rknn_run() 执行推理 // 5. rknn_outputs_get() 获取检测输出 // 6. 对输出做 NMS 后处理整个部署的坑主要集中在两点:一是导出 ONNX 时如果模型里有动态尺寸的 interpolate 操作,转换到 RKNN 时可能失败,解决办法是在导出时固定imgsz=640;二是量化后小目标检测精度下降明显,如果损失过大,可以尝试do_quantization=False保留 FP16,代价是推理速度变慢。
8. 常见问题与排查思路
把 LSB 模块接入 YOLO26 并部署到 RK3588 的过程中,有几个问题是高频出现的。整理如下:
| 问题现象 | 可能原因 | 排查方式 | 解决方案 |
|---|---|---|---|
| 训练时直接报维度错误 | YAML 中层与层之间的通道数不匹配 | 打印每一层输出的 shape | 检查 parse_model 中 LSB 的 c1/c2 设置 |
| LSBBlock 没有被识别 | 没有在 parse_model 中注册模块名 | 查看模型配置文件命名是否一致 | 在模块映射中添加 LSBBlock |
| 加入 LSB 后 mAP 下降 | 超参数未调整或模块插入位置不合适 | 对比 baseline 训练曲线 | 尝试换插入位置,或减少模块数量 |
| 导出 ONNX 失败 | 模型中有动态 shape 操作 | 查看报错节点与输出尺寸 | 固定 imgsz,关闭动态 shape |
| RKNN 转换失败 | 算子不被 RKNN 支持 | 查看 rknn.build 日志 | 更换部分激活函数,或用官方算子替换 |
| RK3588 推理耗时过高 | 量化关闭或 NPU 使用不足 | 检查是否真的使用 RKNN 推理 | 开启量化,检查预处理耗时 |
| 低光下漏检严重 | 输入图像过暗,模型未见低光分布 | 查看输入图像亮度统计 | 增加低光增强预处理和低光训练样本 |
排查顺序一般遵循“先模型、后数据、再部署”的原则。模型维度错误优先看 parse_model;mAP 下降优先看训练曲线和数据增强;部署问题优先看 ONNX 导出日志和 RKNN 转换日志。
9. 最佳实践与工程建议
最后给几个实战层面的建议,都是踩过坑之后总结出来的。
第一,不要把所有模块都堆在 backbone 上。LSB 这种连续传输模块,最适合的是 neck 的关键融合节点,尤其是 P4 到 P5 之间的过渡位置。如果你在 backbone 每一层后面都加,训练速度和显存都会明显上升,而精度提升可能趋近于零。
第二,做消融实验时,保持训练条件完全一致。这里指的是同一种数据增强、同一个 epoch 数、同一个 batch size、同一个优化器参数。很多同学改完模块后直接加 50 个 epoch 再对比,结果模块本身没有效果,却也因为训练轮数不同看不出真实差距。
第三,优先使用小模型验证思路。先用 yolo26n 这种轻量版本,在小型数据集上快速跑通 LSB 模块的接入和效果验证。如果小模型上都看不到提升,那就不要急着在大模型上尝试。这个原则能省下大量实验时间。
第四,RK3588 部署时,要把量化校准集当成训练集一样重视。校准集必须覆盖目标场景的真实分布。白天训练的模型直接转换后部署到夜间环境,量化精度一定会出问题。建议单独从低光场景中抽一组图片加入 dataset.txt。
第五,把模型文件和配置文件的版本管理做好。改进类项目最怕的是“我记得我这里改过”,结果过了一周根本不知道哪个 yaml 配的是哪个权重。建议在每次实验前把配置文件名改成有含义的名字,比如yolo26_lsb_p4.yaml、yolo26_lsb_p5.yaml,同时把训练日志一起归档。
第六,关注算力边界。即插即用模块不是越多越好。RK3588 的 NPU 算力是有限的,如果你加入了 LSB 模块后发现帧率降得太多,可以在连续传输分支里把中间通道数hidden_channels调小一倍,比如从in_channels改成in_channels // 2,通常能换回不少速度,同时保持大部分精度收益。
YOLO26 的结构还在持续迭代,LSB 模块也只代表了 CVPR 2025 年“特征连续传输”这一方向的一个具体方案。真正有用的东西不是记住某个模块的代码,而是理解它为什么能改善特征流动,然后能够针对自己的数据集和设备去调整。建议你把上面的流程跑一遍,在 mAP、速度和低光场景三项指标上分别记录对比数据,再判断要不要把它引入自己的项目。
如果你在接入过程中遇到和文章里不一样的问题,优先去看模型配置文件和 RKNN 转换日志,这两处通常能给出最直接的线索。