Swish与Hardswish激活函数深度解析:从原理到端侧部署实践
2026/9/17 5:47:38 网站建设 项目流程

这周在调一个轻量级分类模型,顺手把最后几个block的ReLU换成了Hardswish,top-1直接涨了0.6个点,端上单线程推理延迟几乎没变。说“顺手”其实是装轻松,换之前我把Swish和Hardswish的数学性质、梯度行为、量化特性重新捋了一遍,也踩了几个坑才敢下手。这两个激活函数在工业模型里出镜率极高:MobileNetV3、EfficientNet、YOLOv5的C3模块、还有各种注意力机制后面,都能看到它们的身影。国内技术博客聊激活函数,大多停在ReLU到GELU的科普,Swish和Hardswish经常被一笔带过。这篇想系统聊清楚它们到底是什么、为什么有效、工程上怎么替换、有哪些坑,给正在做模型提点或端侧部署的读者一个可落地的参考。

1. 为什么我们需要重新审视激活函数:从ReLU的“硬伤”说起

1.1 ReLU的硬伤与Swish出现的背景

ReLU大概是深度学习里最广为人知的激活函数,公式极其简单:f(x) = max(0, x)。它解决了早期Sigmoid/Tanh的梯度消失问题,让深度网络在2012年之后得以大规模训练起来。但用了这么多年,ReLU身上的问题其实大家心知肚明,只是惯性太大,很少有人愿意换。

第一个问题就是“神经元死亡”。ReLU在负区间梯度恒为0,一旦某个神经元在训练过程中被大量负输入打到,它就会一直输出0,梯度也一直是0,参数再也不会更新,相当于这个神经元永久“关机”了。在实际训练里,尤其是学习率设置偏高的时候,一个网络里可能会有百分之十几甚至更多的神经元处于这种死亡状态。这个问题不是理论上的杞人忧天,是会在训练日志里真实出现的事。

第二个问题是输出非零中心。ReLU的输出要么是0要么是正数,这会使得网络下一层的输入永远是非负的。非零中心的输入会对梯度传播产生一个偏置效果,虽然BN(Batch Normalization)的出现缓解了这个矛盾,但它本身仍然是一个模型表达上的限制。

第三个问题在端侧表现得特别明显:ReLU在负区间那个硬拐点,会让输出分布变得“尖锐”,后面接量化的时候,同样的bit数往往要花更多精力去校准分布。

Swish就是在这些困境里被搜出来的。2017年Google Brain的Ramachandran等人用神经架构搜索(NAS)的方式,在一个受限的候选空间里去搜“最有效的激活函数”,最后找出来的冠军函数形式是 f(x) = x · sigmoid(x),也就是我们现在说的Swish。它跟ReLU不一样,曲线平滑、有下界无上界、非单调,最重要的是它在负区间保留了一个小的“负向凹陷”,让神经元即使拿到负输入,也还能有微弱的梯度往外传,从根上缓解了神经元死亡的问题。

1.2 Swish到底“新”在哪里

Swish的结构其实不复杂:sigmoid(x) 大家早就知道,乘上一个x而已。但它有几个性质在数学上很值得玩味。

先看单调性。ReLU、Leaky ReLU、ELU这些主流激活函数都是单调的——输入越大,输出一定越大。但Swish不是单调函数,它在负区间有一个极小值点,大概在x≈-1.28的地方,函数值约为-0.28。这意味着它对“负输入的响应”不是一味压到0,而是保留了一个柔和的非线性“谷”。这个谷让网络在反向传播的时候,即使输入是负的,神经元也能拿到一个方向正确的梯度去更新权重。

再看平滑性。Swish的一阶导数在定义域内处处连续可导,ReLU则是在x=0处突然拐弯。这里插入一个很多人忽略的点:ReLU在x=0处虽然当作不可导处理,实际工程里一般取0或1,但它的二阶导是冲激函数,这个不连续会对优化过程产生微妙影响。而Swish的导数是一个光滑函数,梯度更新过程中不会出现突然的跳变,这让训练曲线通常更稳。我在实际训练里也观察到,用Swish作为激活函数时,loss曲线的震荡幅度往往比ReLU小一些。

最重要的是,Swish保持了“无上界”这个特性。上界如果太小,比如tanh的1.0,会限制神经元对大数值输入的响应,加深网络后容易产生饱和现象;无上界则能保证深层模型在大动态范围输入下依然有足够的表达能力。

其实大家都知道,Swish并不是2017年才被“发明”的,很多做RNN的人早就用过类似的形态。但NAS把它从海量候选里捞出来,并且证明了它在ImageNet这种大规模任务上有稳定提升,这才让它进入主流视野。从研究者的角度,这有点像用穷举法给“人工设计激活函数”这件事交了一份答卷:你设计得再精巧,可能还不如机器在一个权衡空间里搜出来的组合有效。

1.3 Hardswish的登场:移动端的故事

Swish精度虽好,但有一个很现实的问题:它比ReLU贵。ReLU在底层实现就是一个比较操作,一两条指令就完了;而Swish需要先算sigmoid,sigmoid里面有指数函数exp,指数函数在CPU上可能是几十条指令,在移动端甚至需要查近似表。对一个包含了上百万次激活计算的卷积网络来说,这个开销是实打实的。

2019年Howard等人发布MobileNetV3的时候,给了一个思路:你不是嫌sigmoid贵吗?我找一个分段线性函数去近似sigmoid,用它替换Swish里的sigmoid,精度尽量不掉,计算量降到接近ReLU。这个近似函数就是ReLU6(x+3)/6,代入之后得到Hardswish:

f(x) = x * ReLU6(x + 3) / 6

这个函数在x小于等于-3时输出0,在x大于等于3时输出x,中间用一条二次曲线连接。整条曲线跟Swish的形状非常接近,但所有运算都可以用加减乘除、比较和clip操作完成,没有任何指数运算。对于量化部署来说,它更是天然友好——整条曲线都落在有限的整数范围内,不需要像sigmoid那样做浮点查表。MobileNetV3的论文里提到,用Hardswish替换Swish之后,在ImageNet上的精度几乎无损,但推理速度在部分移动端硬件上有明显提升。这句话在当时被很多工程团队当成“免检报告”,但真正替换的时候,还是会遇到一些麻烦,后文我会专门讲。

2. Swish与Hardswish的核心原理拆解

2.1 Swish的数学定义与梯度推导

Swish的原始定义带一个可学习参数β,全称叫Swish-β:

f(x) = x · sigmoid(βx)

当β=1时,就是最常用的形态。这里有个容易混淆的点:很多框架里把β=1的Swish直接叫SiLU(Sigmoid Linear Unit),PyTorch里的nn.SiLU()就是它,nn.Swish()反而没有。严格说,Swish和SiLU在数学形式上一致,只是历史命名路径不同,工程上把它们当同一个函数用没有毛病。

反向传播时需要用到它的导数,推导过程不复杂。设σ表示sigmoid函数,σ' = σ(1 - σ),那么:

f'(x) = σ(x) + x · σ'(x) = σ(x) + x · σ(x) · (1 - σ(x))

这个导函数有一个很优雅的性质:它的最小值不为0,且在负区间有一个平滑的“下限”。当x趋于负无穷时,σ(x)趋近于0,但x·σ(x)趋近于0,整体梯度趋近于0但不等于0;而在x=-1到-3这个区间,梯度保留了一个小台阶。这个“小台阶”就是Swish对抗神经元死亡的关键。

如果你在训练中打印过各层激活值的分布,会发现使用Swish的网络,负区间并不是一片死寂,而是有一些接近0但不为0的小值分布。这些值对应的神经元虽然在当前batch里输出很小,但它们保留了梯度通道,一旦后续权重更新让输入往正方向移一点,神经元就能“复活”。这在长时间训练时尤其重要,特别是训练后期学习率降低、网络容量被压缩的情况下,ReLU里死掉的神经元无法恢复,而Swish网络还能持续微调。

另外有个数值稳定的问题值得记一下:直接算sigmoid(x)时,当x为很大的正数,exp(-x)可能下溢出为0,这没问题;当x为很大的负数,exp(-x)会上溢出为inf,导致sigmoid变成NaN。所以底层实现一般会把公式改成对x>=0和x<0分开计算,或者用clip限制输入范围。PyTorch和TensorFlow都在CUDA/cpu内核里处理过这个细节,自定义实现时一定要自己兜住,否则训练到一半loss变成NaN,排查起来非常头大。

2.2 从Swish到Hardswish:近似推导

Hardswish的动机在于用ReLU6(x+3)/6去近似sigmoid(x)。先看ReLU6(x+3)/6是什么:ReLU6把输入clip到[0, 6],加上3再除以6,相当于一个在[-3, 3]区间内斜率为1/6的线性函数,在x<=-3时输出0,x>=3时输出1。

把它跟sigmoid的曲线叠在一起看,sigmoid在0附近近似线性,在±3附近已经饱和到接近0和1。ReLU6(x+3)/6这条折线就是抓住这个特点,用一段直线去逼近sigmoid的“线性区”,用两端的常数去模拟“饱和区”。代入之后得到一个分段函数:

  • 当 x <= -3 时:f(x) = 0
  • 当 -3 < x < 3 时:f(x) = x(x+3)/6
  • 当 x >= 3 时:f(x) = x

这个分段函数在连接点处的值连续:x=-3时两部分都是0,x=3时两部分都是3。但要注意,导数并不连续,x趋近3时内部导数趋近(2·3+3)/6=1.5,而x>=3的区间导数是1。这个导数不连续会导致什么?理论上会使梯度在x=3附近有一个“突变跳变”,但在实际训练中,激活值落在3附近的样本比例并不高,且这个跳变幅度不大,对优化影响有限。MobileNetV3跑大量实验也证明工程上可以接受。

不过我这里想多说一句:Hardswish不是唯一的sigmoid近似方案。有人用H-sigmoid(就是ReLU6(x+3)/6本身)直接替换sigmoid用在门控结构里,也有人用更复杂的分段多项式去逼近。Hardswish能胜出,核心是它在“精度近似度”和“计算代价”之间取得了很好的平衡,所有运算都能落在低精度算子的覆盖范围里,这对端侧推理芯片来说非常关键。

2.3 梯度流动与网络表达能力的关系

很多人学激活函数,只记公式和图像,忽略了它对梯度流的影响。深度网络的训练本质上是在做信用分配:梯度要从loss端一层一层传回到输入层,中间每经过一个激活函数,梯度都会乘以一个缩放因子。ReLU在正区间的缩放因子是1,负区间是0。这意味着负区间的“信用”完全被切断。Swish的缩放因子是一个动态值:输入在0附近时,缩放因子接近0.5;输入变大时,缩放因子趋近于1;输入为负但不太负时,缩放因子是一个小的正值。这种动态缩放让网络能更精细地控制每一层的信息流动性,相当于给每层加了一个可学习的“软门控”。

再往深一点说,Swish的非单调性其实为网络提供了一种类似于“门控”的表达能力。在负区间那个凹陷,可以看作输入在负小值范围时,输出有一个微弱的反向响应。这个反向响应有助于网络建模那些“输入越负、输出反而应该增大”的特征模式。ReLU做不了这件事,Leaky ReLU虽然保留负区间斜率,但它的负区间是单调线性,表达不了凹陷这种非线性。

在注意力机制里这个性质特别有用。SE-Net、EfficientNet、MobileNetV3里的SE模块,本质是学习一组通道权重,这组权重就是对特征图做全局平均池化后再经过两个全连接层。如果把这两个全连接层之间的激活函数从ReLU换成Swish/Hardswish,通道权重分布会变得平滑很多,小的权重不会被硬截断成0,这能让注意力更细腻。我自己的实验观察到,用Hardswish的SE模块在剪枝之后效果更稳,可能就是因为平滑的激活让权重分布更连续,不容易在剪枝时出现剧烈退化。

3. 效果到底怎么样:实测数据与适用场景

3.1 图像分类任务中的收益:从NAS到MobileNetV3

Swish原论文里比较了不同深度网络上Swish和ReLU的表现。在ImageNet这种大规模分类数据集上,用NAS搜索得到的最优激活函数(Swish)比ReLU的top-1准确率普遍高出0.6到1个百分点左右。这里的口径不是某个网络换了个激活函数就涨一个点,而是在Wide ResNet等基础架构上做控制变量实验,并且配合超参数调优之后的结果。

MobileNetV3论文里对Hardswish和Swish也做了直接对比:把原来的swish替换成hardswish,精度损失在0.2个百分点以内,有些任务甚至没有损失。考虑到hardswish在移动端推理速度上有优势,这个精度换速度的性价比非常高。

我自己的实际经历可以补充一个数据:拿一个基于MobileNetV2改造的轻量分类模型,训练集大概30万张图,把最后几个下采样block里的ReLU6换成Hardswish,保持其他超参数不变,val acc从89.4%涨到90.0%。这个提升在我的实验里不是偶然,重复跑了三次都稳定涨0.5个点左右。换成Swish(即SiLU)也能涨,大约0.4个点,但推理端到端耗时增加了约8%,所以最终线上选择的是Hardswish。

3.2 目标检测与注意力模块中的表现

在检测任务里,Swish和Hardswish同样有应用价值。YOLOv5从某个版本开始,C3模块的Bottleneck里就默认用了SiLU(也就是Swish)。很多复现YOLO系算法的同学会发现,用ReLU替换掉SiLU后,同样的训练配置mAP会掉0.5到1个点左右。原因在于检测头需要处理多尺度特征,激活函数的平滑性对特征图上的小目标响应更友好,硬截断的ReLU容易把偏暗小目标的特征响应直接清零。

SE模块和注意力类网络是把Swish/Hardswish当“标配”的重灾区。EfficientNet全系列大量使用Swish激活,MobileNetV3也把Hardswish用在SE模块中。这些工作之所以敢大面积使用,除了精度原因,还因为这些模块计算量占比不高,激活函数引入的开销被控制在了可接受范围内。

3.3 什么时候不值得换

说了这么多好处,也要泼点冷水。Swish/Hardswish并不是“万金油”,我在几个场景下试过发现收益很小甚至为负。

第一个场景是超浅层网络,比如只有两三个卷积层的线性分类器。这种网络表达能力有限,Swish的非线性优势发挥不出来,反而因为引入了更复杂的激活计算,训练时需要更多迭代次数才能收敛。换过去之后精度没变,训练时间还长了。

第二个场景是训练数据量非常小的任务。Swish的动态门控特性依赖足够的统计量来学习合适的梯度流,如果数据集只有几千张图,很容易过拟合,激活函数的平滑性反而让模型更难“记住”训练集里的细节。这时候ReLU的硬截断更像一个天然的正则化器,效果反而好。

第三个场景是推理框架不支持的场景。这个太重要了。很多端侧推理引擎,尤其是芯片厂商自研的SDK,算子列表里根本没有Hardswish,编译器会把Hardswish拆成片段的elementwise算子组合,比如clip、add、mul这些。如果拆得好,性能还不错;拆得不好,可能是把整个激活过程拆成了四五个核函数的逐个调用,每个核函数都要单独读写一遍数据,memory traffic飙升,延迟比ReLU慢两三倍都有可能。如果遇到这种情况,换Hardswish前一定要先确认目标推理引擎的算子支持情况,别只看论文收益就盲目替换。

4. 工程落地:框架支持、参数配置与训练技巧

4.1 PyTorch和TensorFlow里的直接使用

PyTorch从1.7开始,nn.SiLU()nn.Hardswish()都已经是官方API,直接用就行,不需要自己手写。很多教程说“PyTorch没有Swish”,这是误解,nn.SiLU()就是beta=1的Swish,另外torch.nn.functional.silutorch.nn.functional.hardswish这两个函数式接口也都存在。

TensorFlow这边,tf.keras.activations.swish是老牌API,tf.keras.activations.hard_swish在TensorFlow 2.x里也提供。如果你在用TensorFlow Lite做移动端部署,Hardswish的TFLite算子已经原生支持,转换时不需要额外注册自定义算子,这点比PyTorch生态里很多自定义算子方便。

需要自己手写时,核心就两段逻辑。Swish的PyTorch实现:

import torch import torch.nn as nn class Swish(nn.Module): def forward(self, x): return x * torch.sigmoid(x)

Hardswish的分段实现:

import torch import torch.nn as nn class Hardswish(nn.Module): def forward(self, x): return x * torch.clamp(x + 3.0, min=0.0, max=6.0) / 6.0

注意这里一定要用torch.clamp(x + 3.0, min=0.0, max=6.0),不要写成torch.relu6(x + 3.0),虽然结果等价,但relu6在某些老版本PyTorch里导数计算有边界问题,实测下来不如clamp稳。

4.2 超参与网络结构配合的经验

换用Swish或Hardswish之后,我总结出几条训练配置上的经验,供大家参考。

第一,BatchNorm不能省。Swish的输出是非零中心的,在无BN的网络里,激活值均值会缓慢漂移,导致深层网络不稳定。几乎所有用Swish登上论文榜的模型,都在激活前后搭配了BN层。BN让激活值分布重新回到零均值、单位方差附近,极大缓解了非零中心带来的问题。

第二,学习率策略可能需要微调。我发现用Swish族激活函数时,初始学习率可以比ReLU略高一点,因为Swish的梯度永远不会为0,不会出现ReLU那种一片神经元集体死亡的“假死”现象,所以更大的学习率不太容易把网络训崩。在ImageNet规模的训练上,初始学习率从0.1调到0.12到0.15都能正常收敛。但要注意,这只是我的经验,具体数值跟batch size、优化器、正则化强度都耦合,最好在小数据集上先打个表。

第三,对Hardswish而言,权重初始化建议沿用MobileNetV3的做法,对激活值做一个缩放。V3论文里明确提到,为了让Sigmoid类激活在深层的初始输出分布更合理,他们会在激活之前对上一个卷积层的输出做固定缩放。实操上,我会在初始化阶段把卷积层权重乘以一个小系数(比如0.5),并观察第一轮epoch的loss基准值。如果第一轮loss跟ReLU模型差异太大,就说明初始化尺度有问题,需要调回到1.0附近。

4.3 量化部署:Hardswish为什么比Swish“香”

端侧部署是模型轻量化绕不开的话题。现在主流端侧推理芯片对INT8量化的支持已经非常普遍,但量化对激活函数的“友好程度”差异很大。ReLU量化很简单,负区间直接截断到0,正区间均匀分段即可。Swish麻烦的地方在于sigmoid的形状是指数型曲线,量化后在靠近0和靠近1的地方,分辨率严重不足,容易造成精度损失。而且sigmoid的输出范围是(0, 1),这个范围在量化时需要分配额外的bit来表征小数部分,进一步挤压了有效量化精度。

Hardswish的分段线性(严格说是分段二次)结构,让它的量化误差分布更均匀。在[-3, 3]区间,曲线被均匀切分后,每个量化步长内的误差基本可控;在[-3, 3]之外的区间,直接退化成ReLU的行为,量化完全精准。MobileNetV3论文里专门提过,在INT8量化下Hardswish的精度损失比Swish小。这一点在做TFLite部署时表现得很明显,我实测TFLite转换INT8模型,Hardswish版本比Swish版本在ImageNet验证集上的量化精度损失少0.3到0.5个百分点。

顺带一提,如果你在自研推理引擎里手写Hardswish的INT8算子,核心思路是先对输入做clip到[-3, 3],再把结果映射到INT8的整数区间,最后用整数乘法和位移来实现乘x除以6的操作。这个过程不需要查表,也不需要恢复浮点,非常干净。

5. 常见问题与踩坑实录

5.1 训练过程loss变成NaN或梯度爆炸

Swish在正区间无上界,如果权重初始化不合理或者学习率过大,激活值可能变得非常大,导致梯度也变大,最终在反向传播过程中出现梯度爆炸。排查思路是把模型里每一层的激活值分布打出来,看看是哪一层开始“放飞”的。常见解决办法是降低学习率,或者在激活前面加一层LayerNorm/BN,再或者把权重初始化尺度降一个量级。另外,如果用的是自定义Swish实现,检查一下sigmoid有没有做数值稳定处理,前面已经说过,exp溢出是NaN的高发区。

5.2 换Hardswish后精度反而下降

这通常有三个原因。一是网络本身太浅,非线性优势发挥不出来,前文已讨论过。二是学习率没有重新调,ReLU下合适的学习率在Hardswish下可能偏大,导致负区间小梯度放大后引发震荡,把loss搞得不收敛。三是BN没有跟上,尤其是从别人的代码库里替换激活函数时,如果原模型某些位置没有BN层,替换后激活值分布偏移,精度会掉。判断方法是打印一下各层输出的均值方差,如果某层均值明显偏离0,优先在这一层后面补BN再试。

5.3 端侧推理速度没有提升甚至变慢

一个常见误区是“Hardswish比Swish快,所以端上也快”。实际上,如果你是直接部署PyTorch导出的ONNX模型,很多runtime对Hardswish都没有融合算子,会把它拆成若干个基础算子执行,内存访问次数成倍增加。这时候你看到的延迟反而比Swish还慢。解决思路有两个:一是换用量化工具链更成熟的推理框架,二是让模型转换工具把Hardswish的多个子算子融合成一个自定义算子。实际项目中,我遇到过TFLite比ONNX Runtime快一倍的情况,就是因为TFLite原生支持Hardswish算子融合。

5.4 常见问题速查表

场景现象排查方向解决方案
训练初期loss为NaN梯度爆炸/exp溢出调低学习率、检查数值稳定实现
训练中期精度上升很慢学习率不适配尝试提高初始学习率
小数据集换后过拟合网络容量过大回退到ReLU或加dropout
端侧部署延迟反而升高算子拆解过多检查推理引擎是否融合Hardswish
量化精度损失大sigmoid量化脆弱优先用Hardswish替代Swish
剪枝后效果退化激活分布改变检查BN层和激活的配合

5.5 几个容易被忽略的细节

最后分享三个操作性很强的细节。第一,在PyTorch里想省显存时,可以把nn.SiLU()换成自定义的x*sigmoid(x)后用torch.utils.checkpoint做梯度检查点,这样在长序列模型里能明显减少中间激活的存储。第二,如果你在分布式训练里用了混合精度AMP,Hardswish的half精度计算在某些GPU上会比float32慢,因为half没有原生指数运算优化,所以AMP场景下可以考虑保留float32计算。第三,做模型剪枝时,Swish/Hardswish的平滑曲线会让通道重要性判定更稳定,但阈值选取不能沿用ReLU时代的经验,建议对激活值分布重新做统计分析。我自己是把每层激活值的百分位数打印出来,再根据剪枝率反向调整sparsity ratio,比拍脑袋靠谱得多。

6. 结尾心得

我自己做了这么多年模型优化,最大的体会是:激活函数这个看似只有一行公式的东西,恰恰是端侧模型精度和速度博弈的缩影。Swish赢在表达,Hardswish赢在工程,二者本质上都是对“非线性能力”和“计算代价”的再权衡。如果你只记住一个结论,我希望是这句话:别再把ReLU当默认选项,尤其在轻量级模型里,花半小时把末尾几个block换成Hardswish,配合量化评估一下,大概率能白捡零点几个点的精度。试过之后,你大概也会跟我一样,从此工具箱里多一个顺手又靠谱的选项。

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

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

立即咨询