2026/9/2 8:57:48

YOLO26改进:LSB模块实现局部特征连续传输与RK3588部署

YOLO26改进:LSB模块实现局部特征连续传输与RK3588部署 写 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_channelsNone): super().__init__() hidden_channels hidden_channels or in_channels # 局部特征精炼分支 self.local_branch nn.Sequential( nn.Conv2d(in_channels, hidden_channels, 3, 1, 1, biasFalse), nn.BatchNorm2d(hidden_channels), nn.SiLU(inplaceTrue), nn.Conv2d(hidden_channels, in_channels, 3, 1, 1, biasFalse), nn.BatchNorm2d(in_channels), ) # 连续传输分支把前序特征映射到当前尺度 self.continuity_branch nn.Sequential( nn.Conv2d(in_channels, hidden_channels, 1, biasFalse), nn.BatchNorm2d(hidden_channels), nn.SiLU(inplaceTrue), ) self.act nn.SiLU(inplaceTrue) def forward(self, x, x_prevNone): 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, sizex.shape[-2:], modebilinear, align_cornersFalse ) continuity_out self.continuity_branch(x_prev) # 局部信息与连续传输信息拼接融合 fused torch.cat([local_out, continuity_out], dim1) 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.yamlYAML 片段仅展示 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_channels512) 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 modelyolo26n.pt datadataset.yaml \ epochs100 imgsz640 batch16 device0如果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, gamma1.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 modelruns/train/exp/weights/best.pt formatonnx imgsz640 opset12如果仓库没有直接支持导出可以自己写一次 torch.onnx.export。导出时注意固定输入尺寸尽量不用动态维度。RKNN 工具链对动态 shape 的支持比较有限动态维度很容易在转换时报错。第二步安装 RKNN-Toolkit2。建议新建一个 Python 虚拟环境conda create -n rknn python3.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_platformrk3588, ) # 加载 ONNX 模型 rknn.load_onnx(modelyolo26n.onnx) # 构建 RKNN 模型开启量化以降低显存和算力需求 rknn.build(do_quantizationTrue, datasetdataset.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 时可能失败解决办法是在导出时固定imgsz640二是量化后小目标检测精度下降明显如果损失过大可以尝试do_quantizationFalse保留 FP16代价是推理速度变慢。8. 常见问题与排查思路把 LSB 模块接入 YOLO26 并部署到 RK3588 的过程中有几个问题是高频出现的。整理如下问题现象可能原因排查方式解决方案训练时直接报维度错误YAML 中层与层之间的通道数不匹配打印每一层输出的 shape检查 parse_model 中 LSB 的 c1/c2 设置LSBBlock 没有被识别没有在 parse_model 中注册模块名查看模型配置文件命名是否一致在模块映射中添加 LSBBlock加入 LSB 后 mAP 下降超参数未调整或模块插入位置不合适对比 baseline 训练曲线尝试换插入位置或减少模块数量导出 ONNX 失败模型中有动态 shape 操作查看报错节点与输出尺寸固定 imgsz关闭动态 shapeRKNN 转换失败算子不被 RKNN 支持查看 rknn.build 日志更换部分激活函数或用官方算子替换RK3588 推理耗时过高量化关闭或 NPU 使用不足检查是否真的使用 RKNN 推理开启量化检查预处理耗时低光下漏检严重输入图像过暗模型未见低光分布查看输入图像亮度统计增加低光增强预处理和低光训练样本排查顺序一般遵循“先模型、后数据、再部署”的原则。模型维度错误优先看 parse_modelmAP 下降优先看训练曲线和数据增强部署问题优先看 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 转换日志这两处通常能给出最直接的线索。