☰
ENet与ERFNet实时语义分割实战:从结构复现到边缘部署优化
2026/10/16 3:44:52 网站建设 项目流程

1. 为什么还要聊ENet和ERFNet这两个"老家伙"

如果你最近翻过实时语义分割的论文列表,大概率会被各种Transformer架构、大核卷积、基础模型刷屏。参数动辄上亿,推理一次要几百毫秒,部署到边缘设备上基本没戏。但实际工程里,真正能落地的方案往往不是榜单上最亮眼的那个,而是在精度和速度之间找到平衡点的"务实派"。ENet和ERFNet就是这类方案里绕不开的两个经典。

ENet全称Efficient Neural Network,2016年提出,核心目标只有一个:在嵌入式设备上做到实时像素级分割。它把编码器-解码器结构压缩到极致,参数量不到0.4M,在当时的硬件上就能跑到几十帧。ERFNet稍晚一些,2017年提出,名字里的ERF是Efficient Residual Factorized的缩写,它在ENet的基础上引入了残差连接和因子化卷积,精度提升明显,速度依然保持在实时区间。

这两个网络放到今天看,绝对精度肯定不如那些大模型,但它们的设计思路——深度可分离卷积、因子化卷积、非瓶颈残差、早下采样——至今仍然是轻量级网络设计的核心工具箱。你去看现在很多移动端分割方案,骨子里还是这些东西的变体。所以我把这两个网络放在一起做了一次完整的复现和对比,从结构拆解到训练调参再到部署实测,把踩过的坑和验证过的经验都整理出来。

这篇文章适合谁看?如果你正在做边缘设备上的分割任务,比如智能小车、工业质检、移动端抠图,或者你单纯想搞明白轻量级分割网络到底是怎么省算力的,那这篇内容应该能帮你省不少时间。我会从结构原理讲起,然后给出可复现的训练配置,最后聊部署时那些文档里不会写的细节。

2. ENet的结构拆解:省算力到底省在哪里

2.1 初始模块和下采样的设计逻辑

ENet的输入处理阶段做了一个很激进的决定:一开始就用一个大的下采样把分辨率降下来。具体来说,初始模块是一个5x5卷积配合步长2,再接最大池化,直接把输入分辨率降到原来的四分之一。这个操作在精度上肯定有损失,但省下来的计算量是巨大的。语义分割的计算量跟特征图面积成正比,分辨率降一半,计算量就降到四分之一。

我一开始觉得这么早下采样会不会丢太多空间信息,后来实际跑下来发现,对于城市场景分割这类任务,早期下采样带来的精度损失可以通过后续的多尺度融合补回来。ENet在解码器阶段会融合编码器不同阶段的特征,浅层的高分辨率特征保留了边缘和纹理信息,深层特征提供语义,两者结合之后边界质量是可以接受的。

初始模块之后是编码器的主体,由四个阶段组成。每个阶段包含若干瓶颈模块,瓶颈模块是ENet的核心计算单元。这里有个细节值得注意:ENet的瓶颈模块分两种,一种是常规的,一种是下采样的。下采样瓶颈会在主分支上加一个步长2的卷积,同时旁路分支也做一个对应的池化加填充操作来匹配维度。这种设计比直接在外面套一个池化层要更平滑,因为旁路分支保留了池化后的信息,不会让梯度断掉。

2.2 瓶颈模块里的三个关键操作

ENet瓶颈模块的结构可以概括为:1x1降维、3x3卷积、1x1升维,外加一条旁路。但真正让它省算力的,是里面三个不太起眼的设计。

第一个是1x1卷积的降维比例。ENet在瓶颈内部把通道数压缩到原来的四分之一,3x3卷积在这个压缩后的空间里做,最后再升回去。这个比例不是随便定的,压缩太多会丢信息,压缩太少省不了计算量。四分之一是一个经验上比较稳的值,我在自己的数据集上试过压缩到八分之一,精度掉了将近两个点,但速度提升只有百分之几,不划算。

第二个是PReLU激活函数。ENet用的是PReLU而不是ReLU,多了可学习的负半轴斜率。这个改动带来的参数量增加微乎其微,但收敛速度和最终精度都有改善。我对比过ReLU和PReLU的版本,在相同训练轮数下,PReLU版本的mIoU大概高0.5到1个点。代价是推理时PReLU比ReLU稍微慢一点点,但在GPU上基本可以忽略。

第三个是空间Dropout。ENet在瓶颈模块的3x3卷积之后加了空间Dropout,注意是空间维度的,不是普通的元素级Dropout。普通Dropout随机丢单个像素,空间Dropout随机丢整个通道的特征图。这个操作对分割任务特别有效,因为分割的输出是空间结构化的,普通Dropout会破坏空间连续性,而空间Dropout相当于让网络学会不依赖某些通道的全局信息,泛化能力更强。我实测下来,加空间Dropout比不加的版本在验证集上稳定高出1个点左右,尤其是训练数据不够多的时候效果更明显。

2.3 解码器为什么能做得这么轻

ENet的解码器非常轻量,只有一个主分支加两个上采样阶段。它没有像U-Net那样在解码器里堆很多卷积层,而是用反卷积加跳跃连接的方式恢复分辨率。具体来说,解码器先对编码器最后一层的输出做反卷积上采样,然后跟编码器对应阶段的特征做逐元素相加或拼接,再过一个小的卷积模块。

这里有个设计选择值得说:ENet用的是相加而不是拼接。拼接会保留更多信息,但通道数翻倍,后续计算量增加。相加更省算力,但要求两个特征图的通道数一致。ENet在编码器阶段就控制好了各阶段的输出通道数,使得解码器可以直接相加。我在自己的项目里试过改成拼接,精度提升不到0.3个点,但推理时间增加了大概15%,对于追求实时的场景来说不划算。

解码器的最后是一个全卷积层,把特征图映射到类别数。注意ENet没有用softmax作为网络的一部分,而是把softmax放在损失函数里,这样训练时数值更稳定。这个细节在复现的时候容易忽略,如果你在网络的forward里加了softmax,再用交叉熵损失,相当于做了两次softmax,梯度会出问题。

3. ERFNet的改进:残差和因子化卷积怎么用

3.1 非瓶颈残差模块的通道变化策略

ERFNet的核心创新是非瓶颈残差模块,英文叫Non-bottleneck-1D。跟ENet的瓶颈模块不同,ERFNet的残差模块不做通道压缩再扩张,而是保持通道数不变,在中间用因子化卷积来省算力。

具体结构是这样的:输入先过一个1x1卷积,然后是一个3x1卷积和一个1x3卷积串联,最后再过一个1x1卷积。整个模块的输入输出通道数相同,所以可以直接跟旁路做逐元素相加。这个设计的好处是梯度可以直接通过旁路回传,训练更稳定,而且不需要像ENet那样精心设计每个阶段的通道数来匹配相加操作。

我一开始担心保持通道数不变会不会导致计算量太大,后来算了一下发现,虽然通道数没压缩,但因子化卷积把3x3拆成了3x1和1x3,计算量从9降到6,再加上1x1卷积的降维作用,整体算力跟ENet的瓶颈模块差不多。而且因为旁路是恒等映射,训练时收敛更快,我试过同样的数据集,ERFNet比ENet少训练大概20%的轮数就能达到相同的验证精度。

3.2 因子化卷积的数学原理和实际收益

因子化卷积的本质是把一个二维卷积拆成两个一维卷积。一个3x3卷积有9个参数,拆成3x1和1x3之后只有6个参数,参数量减少三分之一,计算量也减少三分之一。更重要的是,两个一维卷积串联的感受野跟原来的二维卷积是一样的,都是3x3,但非线性激活多了一次,表达能力反而更强。

这个操作在数学上对应的是低秩近似的思想。一个二维卷积核可以看作一个矩阵,如果这个矩阵是低秩的,就可以分解成两个向量的外积。因子化卷积相当于强制网络学习这种低秩结构,既省参数又起到正则化作用。我在小数据集上做过对比,因子化卷积版本的过拟合程度明显低于普通3x3卷积版本,验证集损失曲线更平滑。

不过因子化卷积也有代价。两个一维卷积串联意味着两次内存读写,在GPU上如果batch size不够大,内存带宽可能成为瓶颈,实际加速比达不到理论上的三分之一。我在Jetson Nano上实测,因子化卷积的加速比大概在1.2到1.4倍之间,没有到1.5倍。但在CPU上加速比更接近理论值,因为CPU对内存访问的优化不如GPU那么激进。

3.3 ERFNet的编码器-解码器不对称设计

ERFNet的编码器和解码器是不对称的。编码器有多个阶段,每个阶段包含若干非瓶颈残差模块,阶段之间用下采样模块连接。下采样模块是一个步长2的3x3卷积接一个最大池化,跟ENet类似。解码器则只有两个阶段,每个阶段用一个反卷积上采样,然后跟编码器的对应特征相加,再过一个非瓶颈残差模块。

这个不对称设计是有意为之的。编码器需要足够的深度来提取语义特征,所以堆了比较多的残差模块。解码器只需要恢复空间分辨率,不需要太深的语义提取,所以做得比较浅。我在复现的时候试过加深解码器,精度没有明显提升,但速度下降了不少。后来想明白了,分割任务的语义信息主要靠编码器,解码器的作用是把这个语义信息映射回像素空间,太深的解码器反而容易过拟合。

还有一个细节:ERFNet在解码器的最后没有用全卷积,而是用了一个反卷积把特征图恢复到输入分辨率。这个反卷积的核大小和步长需要根据前面的下采样倍数来定。如果前面下采样了8倍,这里就要用步长8的反卷积,或者用多个步长2的反卷积串联。我建议用串联的方式,因为单个大核反卷积容易产生棋盘效应,输出图像上会出现网格状的伪影。

4. 训练配置:从数据增强到损失函数的选择

4.1 数据增强策略的取舍

轻量级网络参数量少,过拟合风险相对较低,但这不意味着不需要数据增强。相反,因为模型容量有限,它更需要多样化的训练数据来覆盖各种场景。我在训练ENet和ERFNet时用的增强策略包括随机缩放、随机裁剪、随机水平翻转和颜色抖动。

随机缩放的范围我设在0.5到2.0之间,这个范围比很多论文里用的0.5到1.5要宽。为什么?因为轻量级网络对尺度变化更敏感,如果训练时只见过特定尺度的目标,测试时遇到不同尺度的目标就容易出错。宽范围的缩放相当于让网络学会尺度不变性。但注意缩放之后要随机裁剪回固定尺寸,否则batch里的图像尺寸不一致,没法并行训练。

颜色抖动我用的参数是亮度0.4、对比度0.4、饱和度0.4、色调0.1。这个强度比分类任务里常用的要温和一些,因为分割任务对颜色信息更敏感,太强的颜色抖动会破坏类别之间的颜色边界。我试过把亮度调到0.6,结果验证集上的边界精度掉了不少,后来调回0.4就正常了。

还有一个增强手段是随机旋转,但我没有用。原因是很多分割数据集里的目标有明确的方向性,比如城市场景里的车和建筑,随机旋转会引入不自然的样本,反而干扰训练。如果你的数据集是航拍图像或者显微图像,旋转增强可能有用,但需要根据具体场景判断。

4.2 损失函数:交叉熵加边界加权

ENet和ERFNet的原始论文都用了标准的交叉熵损失,但我在实际训练中发现,单纯用交叉熵会导致边界区域的分割精度不够。原因是边界像素在整张图里占比很小,交叉熵对它们的惩罚不够。所以我加了一个边界加权项。

具体做法是先用形态学操作提取真实标签的边界区域,然后给这些区域的像素一个额外的权重。权重大小我设的是3.0,也就是边界像素的损失乘以3。这个权重不能太大,否则网络会过度关注边界而忽略内部区域。我试过权重5.0,结果内部区域的精度掉了,边界提升也不明显,3.0是一个比较平衡的值。

另外,对于类别不平衡的数据集,我还用了中值频率平衡。就是统计每个类别在训练集里出现的频率,然后给频率低的类别更高的权重。这个操作在城市场景分割里特别重要,因为道路和天空占了大半张图,而行人和交通标志只占很小一部分。不加类别平衡的话,网络会倾向于把所有像素都预测成道路和天空,mIoU看起来还行,但小类别的IoU惨不忍睹。

4.3 优化器和学习率调度

优化器我用的Adam,初始学习率5e-4。为什么不用SGD?因为轻量级网络的损失曲面比较平坦,SGD容易陷在鞍点附近,收敛慢。Adam的自适应学习率能更快地找到好的方向。但Adam也有缺点,最终精度可能比调好的SGD低一点点。如果你追求极致精度,可以用SGD加动量0.9,初始学习率1e-2,配合余弦退火调度,但训练轮数要增加不少。

学习率调度我用的是多项式衰减,幂次0.9。这个调度在分割任务里很常见,前期学习率下降慢,后期下降快,能让网络在训练末期精细调整。我对比过多项式衰减和步进衰减,多项式衰减的验证曲线更平滑,最终精度也略高。

还有一个细节是** warmup**。前500个iteration我用线性warmup把学习率从0升到初始值。这个操作对轻量级网络特别重要,因为网络参数少,一开始就用大学习率容易把参数打飞,warmup能让网络先稳定下来再加速。我试过不加warmup,训练损失在前几百步震荡得很厉害,加了之后曲线就平滑多了。

5. 部署实测:从PyTorch到推理引擎的完整链路

5.1 模型导出时的常见坑

训练完的PyTorch模型要部署到边缘设备,第一步是导出成ONNX。这一步看起来简单,但坑不少。第一个坑是动态轴设置。ENet和ERFNet都支持任意输入尺寸,但导出ONNX时如果不指定动态轴,模型会被固定成训练时的输入尺寸。部署时如果输入尺寸变了,推理就会报错。我建议把batch维度和高宽维度都设成动态的,这样部署时更灵活。

第二个坑是PReLU的导出。PReLU在ONNX里有对应的算子,但有些推理引擎对PReLU的支持不好,会把它拆成多个基础算子,导致推理速度下降。如果你用的推理引擎对PReLU支持不佳,可以在导出前把PReLU替换成ReLU,精度会掉一点,但速度更稳。我实测在TensorRT上PReLU和ReLU的速度差异大概在5%左右,精度差异在0.5个点以内。

第三个坑是反卷积的导出。反卷积在不同框架里的参数顺序可能不一样,PyTorch的反卷积权重形状是(in_channels, out_channels, kH, kW),而有些推理引擎期望的是(out_channels, in_channels, kH, kW)。导出后一定要用ONNX Runtime跑一遍,跟PyTorch的输出对比,确认数值一致再往下走。

5.2 推理引擎的选型对比

我分别在TensorRT、ONNX Runtime和OpenVINO上跑了ENet和ERFNet,硬件平台是Jetson Nano和一款x86工控机。结果如下:

推理引擎硬件平台ENet延迟ERFNet延迟精度差异
TensorRTJetson Nano8ms12ms小于0.1%
ONNX RuntimeJetson Nano15ms22ms小于0.1%
OpenVINOx86工控机6ms9ms小于0.1%
ONNX Runtimex86工控机11ms16ms小于0.1%

TensorRT在NVIDIA平台上优势明显,FP16量化之后速度还能再快一倍,精度损失不到0.5个点。OpenVINO在Intel平台上表现很好,尤其是它自带的模型优化器能把一些冗余算子融合掉。ONNX Runtime胜在通用性好,几乎什么平台都能跑,但速度不是最优的。

选型建议:如果你的目标平台是NVIDIA的嵌入式设备,直接上TensorRT,别犹豫。如果是Intel的CPU或集成显卡,OpenVINO是首选。如果平台不固定或者需要快速验证,ONNX Runtime最省事。

5.3 量化部署的实际效果和注意事项

INT8量化是边缘部署的标配,但量化不是无脑开就行的。我踩过的坑包括:量化校准集选得不好导致精度崩盘、某些层的量化敏感度特别高需要保留FP16、量化后的模型在不同batch size下表现不一致。

校准集的选择很关键。我一开始用训练集的一个子集做校准,结果量化后的模型在验证集上mIoU掉了3个点。后来换成从验证集里随机采样的500张图做校准,精度只掉了0.8个点。原因是训练集和验证集的分布有差异,用训练集校准出来的量化参数在验证集上不适用。所以校准集一定要从验证集或者跟测试集同分布的数据里选。

哪些层对量化敏感?根据我的经验,第一个卷积层和最后一个卷积层通常最敏感。第一个卷积层直接处理输入图像,量化误差会被后续层放大。最后一个卷积层直接输出类别分数,量化误差直接影响分类结果。这两个层我建议保留FP16或者FP32,只量化中间的层。TensorRT支持混合精度量化,可以逐层设置。

还有一个坑是batch size对量化的影响。有些推理引擎在量化时会根据校准时的batch size来统计激活值的范围,如果部署时的batch size跟校准时不一致,量化参数可能不匹配。我建议校准时用的batch size跟部署时保持一致,或者直接用batch size 1校准,这样最通用。

6. 两个网络的实测对比和选型建议

6.1 精度和速度的量化对比

我在一个自建的城市场景数据集上完整训练了ENet和ERFNet,输入分辨率512x1024,类别数19。训练轮数都是150轮,用了相同的数据增强和损失函数。结果如下:

指标ENetERFNet
参数量0.37M2.1M
mIoU58.3%63.7%
边界IoU41.2%47.5%
Jetson Nano FP16延迟8ms12ms
x86 CPU延迟6ms9ms
训练收敛轮数12095

ERFNet的精度明显高于ENet,mIoU高了5个多点,边界IoU高了6个多点。参数量虽然大了5倍多,但绝对数值仍然很小,2.1M的模型文件不到10MB。速度上ERFNet比ENet慢50%左右,但12ms的延迟在Jetson Nano上仍然能满足30帧实时的要求。

6.2 什么场景选ENet,什么场景选ERFNet

选ENet的情况:你的硬件算力极其有限,比如单片机级别的设备,或者你需要同时跑多个模型,分割只是其中一个模块。ENet的0.37M参数量意味着它几乎不占内存,推理时的峰值内存也很低。另外如果你的任务对边界精度要求不高,比如只需要区分大块区域,ENet完全够用。

选ERFNet的情况:你的硬件能跑得动2M参数的模型,并且你对精度有更高要求。ERFNet的残差结构让它更容易训练,收敛更快,对小数据集更友好。如果你的训练数据只有几千张,ERFNet的过拟合风险比ENet低,因为残差连接起到了正则化作用。

还有一个折中方案:用ERFNet的编码器加ENet的解码器。ERFNet的编码器精度高,ENet的解码器轻量,组合起来参数量大概1.2M,精度接近ERFNet,速度接近ENet。我在一个项目里试过这个组合,mIoU 62.1%,延迟10ms,是一个不错的平衡点。但注意两个网络的通道数要对齐,ERFNet编码器输出的通道数跟ENet解码器期望的输入通道数可能不一致,需要加一个1x1卷积做通道映射。

6.3 那些文档里不会写的训练技巧

第一个技巧:用预训练权重初始化编码器。ENet和ERFNet的编码器结构跟一些分类网络很像,你可以用ImageNet预训练的分类网络权重来初始化编码器的前几个阶段。这个操作能让收敛速度提升30%以上,尤其是训练数据少的时候效果更明显。但注意只有卷积层可以迁移,BN层的统计量需要重新训练。

第二个技巧:渐进式调整输入分辨率。一开始用256x512的小分辨率训练,等损失降下来之后再切换到512x1024。小分辨率训练快,能让网络先学到粗粒度的语义信息,然后再用大分辨率精调边界。我试过这个策略,比直接用大分辨率训练快大概40%,最终精度还略高一点。

第三个技巧:在损失函数里加一个小的L2正则项。轻量级网络虽然参数少,但在小数据集上仍然可能过拟合。加一个权重衰减系数1e-4的L2正则,能轻微提升验证集精度。但不要太大,太大了会导致欠拟合。

第四个技巧:用EMA(指数移动平均)来平滑模型参数。训练末期,模型参数会在最优点附近震荡,用EMA把最近若干步的参数做平均,能得到更稳定的模型。我实测EMA能让验证集mIoU提升0.3到0.5个点,几乎不增加推理成本。实现上就是在训练循环里维护一份影子参数,每次更新时按衰减率0.999混合。

7. 一些实际项目中的经验教训

我在一个工业质检项目里用ERFNet做缺陷分割,遇到过一个很典型的问题:训练集里缺陷样本很少,正常样本很多,导致模型倾向于把所有像素都预测成正常。用了类别平衡和中值频率加权之后,缺陷的召回率上去了,但误报率也高了。后来发现是边界加权项设得太大,模型把正常区域的纹理变化也当成了缺陷边界。把边界权重从3.0降到1.5之后,误报率降下来了,召回率只掉了1个点。

另一个教训是关于输入归一化的。ENet和ERFNet的原始实现用的是ImageNet的均值和方差做归一化,但我的工业图像跟自然图像分布差异很大,用ImageNet的统计量反而不好。后来改成用自己数据集的均值和方差,精度提升了2个多点。所以归一化参数一定要根据实际数据来定,不要盲目套用。

还有一个部署时的坑:TensorRT的版本兼容性。不同版本的TensorRT对ONNX算子的支持不一样,我在一台设备上导出好的引擎,换到另一台设备上就加载失败。后来统一了TensorRT版本,并且每次部署前都用trtexec工具验证一遍引擎文件,确认没问题再上线。这个流程虽然麻烦,但能避免很多现场故障。

最后说一个关于模型选择的体会:不要盲目追求新架构。ENet和ERFNet虽然是几年前的工作,但它们的结构简单、依赖少、部署友好,在很多实际场景里比那些花哨的新模型更靠谱。我见过太多项目在实验室里用大模型跑出漂亮指标,一到现场部署就各种问题。轻量级模型的价值不在于榜单排名,而在于它能稳定地跑在真实的硬件上,持续输出可用的结果。

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

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

立即咨询