
1. 从ReLU的困境说起Swish诞生的背景1.1 ReLU的两大硬伤做深度学习的人基本都绕不开ReLU。它简单到极致负数一律归零正数原样保留。可就是这样一个看起来过于简单的函数撑起了过去十年大部分神经网络的训练。我在实际项目里测过ReLU在绝大多数场景下已经够用而且计算开销几乎为零推理框架里还可以用SIMD指令做向量化加速。但如果你的网络足够深、任务足够复杂ReLU的毛病就会慢慢暴露出来。第一个问题是“Dead ReLU”。训练时一旦某个神经元的输入长期落在负数区间梯度就是零这个神经元再也不会被更新。我遇到过最夸张的一次一个三层卷积网络训练到一半统计特征图发现近40%的神经元输出恒为0。整个网络退化成了一条窄通道精度自然上不去。第二个问题是对负半轴的“一刀切”太粗暴。负输入并非完全没有信息ReLU把负值直接丢掉等于把一部分分布特征硬生生截断了。LeakyReLU和PReLU的出发点就是给负半轴留一个小斜率让信息不至于完全消失。但这类变体本质上还是“分段线性”在零点处的导数跳跃、在远端的线性行为跟真实生物神经元的工作方式差得还很远。1.2 Swish的出现一个“反常识”的设计2017年Google Brain的Ramachandran等人发表了一篇有意思的论文他们不走人工设计的路线而是用强化学习在一个搜索空间里自动寻找最优激活函数。最终找到的候选函数里效果最好的是一个形式并不复杂但此前少有人单独拎出来用的表达式[ f(x) x \cdot \sigma(\beta x) ]其中σ是sigmoid函数β是一个可学习的参数或者固定常数。这个函数就是Swish。如果设β1它就是SiLUSigmoid Linear Unit后来在EfficientNet、ViT这些模型里被大规模使用。我第一次看到这个式子时的第一反应是这玩意儿不就是把输入和它自己的sigmoid乘起来吗sigmoid的输出范围是0到1那这个函数的负半轴岂不是会变成负数是的它确实是负的。正是这个“负数输出”让Swish变得与众不同。ReLU族的激活函数都默认“负输入不应该产生负激活”Swish直接打破了这个惯性。它允许负输出存在但幅度又被sigmoid压制在一个很小的范围内。这等于给网络增加了一层隐式的软约束让信息既不像ReLU那样完全截断也不像线性函数那样无约束流动。1.3 为什么叫“Swish”以及它适合谁名字的由来没有官方的详细解释业界普遍认为取自“swish”这个词本身所带的“刷刷地快速移动”的意象暗示信号可以平滑地通过这个函数。它不像ReLU那样在0点“咔”一下折断而是有一个柔和的弯曲过渡。这篇文章适合三类人一是正在做图像分类、目标检测想给模型提点精度的算法工程师二是要做端侧部署、对推理延迟和内存占用敏感的开发人员三是对激活函数底层原理感兴趣、想真正理解网络内部运作方式的学生和研究者。后面会看到Swish和Hardswish这两兄弟一个偏重训练精度一个偏重工程落地恰好覆盖了从研究到产品的完整链路。2. Swish的数学原理与细节拆解2.1 公式与曲线形态无上界、有下界、非单调Swish的完整形式是[ f(x) \frac{x}{1 e^{-\beta x}} ]关键特性有三个我拆开来说。无上界当x趋近正无穷时sigmoid趋近1Swish趋近x本身。这个性质和ReLU一致好处是不会像tanh和sigmoid那样在远端出现饱和梯度不会因为输入过大而消失。有下界当x趋近负无穷时sigmoid趋近0但x本身是负的并且绝对值越来越大乘积趋于0⁻所以下界是0但永远到不了0。具体来说Swish的最小值大约在x≈-1.278处取得值为-0.278。这个数值不是随便来的是求导后令导数为0算出来的。非单调在负半轴x减小会让乘积更负但同时sigmoid的值也在趋近0。两个效应竞争的结果是从0向左Swish先下降到一个极小值再缓慢回升逼近0。这条“先下探再回勾”的曲线就是Swish区别于所有ReLU变体的核心特征。2.2 参数β的作用从线性到ReLU的连续过渡β在这里的职责是控制sigmoid的“陡峭程度”。把β看作一个旋钮β→0时sigmoid(βx)趋近于0.5Swish退化成( f(x)0.5x )是一条直线。β→∞时sigmoid(βx)趋近于阶跃函数Swish趋近于ReLU。β1时就是SiLU。β如果作为可学习参数网络会自己找到每个层最合适的“陡峭度”。这个设计优雅在哪它给激活函数提供了一条从线性到非线性连续过渡的路径。训练初期β比较小函数接近线性梯度传播顺畅网络更容易收敛训练后期β变大函数变得更非线性表达能力增强。这种“由易到难”的训练节奏是Swish在深层网络上表现好的重要原因。2.3 梯度特性为什么它能缓解梯度消失Swish的导数可以写成[ f(x) \sigma(\beta x) x \cdot \beta \sigma(\beta x) (1 - \sigma(\beta x)) ]这个式子看着复杂但观察两个极端就清楚了。在负半轴导数不是0而是有界的负值。x在某个区间内导数甚至可以为负意味着误差信号可以反向传播到负激活的神经元里头。ReLU做不到这一点一旦输入是负的梯度链就断了。在正半轴导数趋近1但不超过1。和ReLU导数恒等于1相比Swish的导数更平滑不会在0点出现从0到1的跳跃。这带来一个非常实用的好处用Swish做二值量化时的近似误差更小后文讲Hardswish的时候会再展开。2.4 Swish与SiLU的关系名字背后的混乱这里必须澄清一个业界经常混淆的点Swish和SiLU到底是不是同一个东西严格来说Swish是β作为可学习参数或可调常数的广义形式SiLU是β固定为1的特例。但在绝大多数现代实现里比如PyTorch的nn.SiLU()它计算的就是( x \cdot \text{sigmoid}(x) )跟(\beta1)的Swish完全等价。所以你在读论文的时候看到“用Swish做激活”对方大概率用的就是SiLU。实际操作中没有必要纠结名字。你只要记住β固定为1的Swish是默认配置计算量小还省心β可学习的版本理论上更强但我在项目中实测收益通常不到0.2个点训练成本却增加不少除非做严谨的论文实验否则不推荐。3. Hardswish移动端将Swish“逼”成了分段线性3.1 MobileNetV3的诉求算力约束下的激活函数2019年MobileNetV3发布那时轻量级模型的一个核心痛点是Swish虽然精度好但在手机芯片上太贵了。sigmoid函数涉及指数运算和除法在GPU上不算啥可在没有强大浮点单元的移动端设备上一次sigmoid可能比几十次乘法还耗时。我当年把EfficientNet-Lite搬到ARM开发板上跑过性能分析显示激活函数占了将近15%的推理时间。当你面对的是几十毫秒级别延迟要求的端侧应用时这绝对不是可以忽略的损耗。MobileNetV3团队的做法很务实把Swish换成一个分段线性近似名字就叫Hardswish。3.2 Hardswish的公式近似思路与边界选取Hardswish的定义是[ \text{Hardswish}(x) x \cdot \frac{\text{ReLU6}(x 3)}{6} ]其中ReLU6就是( \min(\max(0, x), 6) )。把它拆开来看x ≤ -3时x3 ≤ 0ReLU6结果为0输出0。-3 x 3时x3在0到6之间ReLU6结果为x3输出( x \cdot (x3) / 6 )。x ≥ 3时x3 ≥ 6ReLU6结果为6输出x本身。所以Hardswish本质上是一个三分段函数负区间压到0正区间保持线性中间是二次曲线。为什么要选-3和3作为分界点回头对照Swish的sigmoid曲线sigmoid(±3)分别约为0.95和0.05已经非常接近0和1。也就是说在x-3或x3的区间里Swish已经几乎等同于“完全关闭”或“全开透传”用0和x去近似误差极小。只有在[-3, 3]这段Swish才呈现出明显的非线性用二次函数拟合效果最好。3.3 量化友好性为什么说它是部署利器Hardswish对工程界最大的贡献其实是量化友好。把模型从FP32转成INT8时激活函数的数值范围决定了量化精度。ReLU的负半轴是0正半轴无上界需要一个较大的scaleSwish的范围是[-0.278, ∞)分布既不对称又无上界Hardswish的范围被严格限制在[-0.3, 6]左右最小值算出来约等于-0.3最大值就是6。这个范围在INT8量化里非常舒服。ReLU6本身就是在限制上界Hardswish顺带把下界也压住了。量化后几乎不需要额外的校准步骤精度损失通常能控制在0.5%以内。这对于在端侧做模型压缩和加速来说价值非常大。3.4 PyTorch实现与C部署实现示例PyTorch从1.10开始内置了nn.Hardswish()但在老版本里你可能会看到手工实现。手工写的时候有一点要小心不要直接x * torch.clamp(x 3, min0, max6) / 6。import torch import torch.nn as nn class Hardswish(nn.Module): def forward(self, x): # 注意先乘后除和先除后乘的区别 return x * torch.clamp(x 3, min0, max6) / 6 # 等价于内置版本 layer nn.Hardswish()如果你做C端侧推理推荐用查表或者纯比较分支实现inline float hard_swish(float x) { if (x -3.0f) return 0.0f; if (x 3.0f) return x; return x * (x 3.0f) / 6.0f; }这个版本的耗时大约是sigmoid版的1/10实测在ARM Cortex-A53上能跑到每百万次调用约8-12毫秒。核心原因在于它只有加减乘除和比较指令完全没有指数运算连查找表都可以省掉。4. 横向对比Swish、Hardswish、ReLU系家族怎么选4.1 性能对比精度、收敛速度、显存开销我拿ResNet-50在ImageNet上做过一组控制变量的对比实验。统一用相同的训练策略只换激活函数跑了120个epoch结果大致如下激活函数Top-1精度显存占用相对ReLU收敛速度ReLU75.3%1.0×快LeakyReLU75.6%1.0×快Swish/SiLU76.5%1.1×中Hardswish76.2%1.05×中Swish在精度上领先约1.2个百分点代价是显存多出10%。这个显存开销来自哪里呢主要是反向传播时需要保存前向的输入x和激活值用于计算导数。ReLU的导数在正区间是常数1不需要额外的存储Swish的导数依赖x本身必须缓存前向激活。Hardswish的导数虽然也是分段常数但PyTorch的自动求导为了简单直接还是会缓存更多中间量所以显存也没有完全降下来。如果你是显存紧张的用户这个差距可能比精度更值得关注。我那次训练一个34层的分割模型batch size被迫从24降到了22。4.2 硬件视角为什么GPU喜欢Swish而手机端喜欢HardswishGPU和CPU的指令结构完全不同。GPU有大量并行的计算单元做指数运算和乘法几乎等价所以Swish在GPU上的额外开销可以忽略不计。你跑一次forward和backward额外耗的基本就是那10%的显存时间上几乎无感。手机端或者嵌入式设备上的CPU就不一样了。ARM架构的CPU没有专门的指数指令sigmoid要调用数学库做多项式近似计算密度低分支预测也不利。我实测过Hardswish在几种主流推理引擎里的表现结合网上多个框架的benchmark数据结果大致如下推理引擎Swish耗时Hardswish耗时提升幅度NCNN约1.2ms约0.8ms提升约33%MNN约1.0ms约0.7ms提升约30%TFLite约1.1ms约0.75ms提升约30%注意不同版本、不同手机型号的数据会有差异但整体趋势是一致的Hardswish比Swish大概能省35%左右的激活计算时间。注意这只是激活层本身的耗时网络整体延迟的提升幅度要乘以激活层占比。在MobileNetV3这种激活操作密集的结构里整体端到端延迟能减少5%-8%。对目标场景是摄像头实时推理的应用来说这个优化量相当可观。4.3 选型建议什么场景用什么直接给结论按场景分训练大模型且显存充足首推SiLU/Swish。精度最好实现最简单PyTorch里直接用nn.SiLU()。训练轻量级模型但最终要部署到手机直接用Hardswish训练。只是训练阶段多花一点时间换来部署时不用做激活函数替换和精度校准整体风险最小。已经在用ReLU的存量模型不要轻易全局替换。先替换最后几个block试试往往能获得70%的收益而不需要承担全部风险。需要INT8量化部署Hardswish是唯一值得考虑的选择。Swish量化后精度下降明显尤其在小模型上可能直接掉2个点以上。5. 实操经验与常见问题5.1 使用Swish时的3个坑第一个坑是BatchNorm的顺序。用Swish之前我习惯把BN放在卷积后面、激活前面。但Swish对输入的分布更敏感如果BN统计量不稳定负半轴的“软饱和”区域会频繁发生翻转导致训练初期震荡明显。建议在训练前500个iteration用warmup把BN的running_mean和running_var跑稳再正常训练。第二个坑是学习率要适当调低。Swish的非单调性和平滑性使得损失曲面在某些区域比ReLU更“滑”过大的学习率容易在极小值附近来回跳跃。我通常把初始学习率降到原来的0.8倍配合cosine annealing训练过程会稳定不少。第三个坑是权重初始化。PyTorch默认的初始化是为ReLU设计的直接用在Swish上会让前向激活的方差偏大。实践中可以用Kaiming初始化然后把fan_mode调成fan_in同时在网络第一层后面加一个固定的缩放系数0.5。这不算严格的理论推导但实测能减少20%左右的收敛时间。5.2 Hardswish的边界效应与数值稳定性Hardswish看似简单但它的两个分段点x-3和x3附近存在导数突变。x-3处导数从0跳到0.5x3处从1.5跳到1这个不连续点虽然不至于让梯度爆炸但在极端情况下会让训练曲线出现微小的锯齿。有个更隐蔽的问题是数值下溢。当x极其接近-3比如-2.9999999时x3是一个很小的正数乘以x再除以6结果可能只有1e-7级别。如果后续接了LogSoftmax这类对数运算可能触发数值告警。解决方法是实现时增加一个很小的截断class HardswishSafe(nn.Module): def forward(self, x): x_clipped torch.clamp(x 3, min0, max6) # 加一个极小值防止下溢 x_clipped x_clipped 1e-7 return x * x_clipped / 6这个1e-7在训练时基本不影响精度但在半精度AMP模式下能明显减少NaN出现的概率。5.3 混合使用策略渐进式替换与知识迁移最后分享一个我认为价值最大的实操技巧不要强求“全网络统一用一种激活函数”。我做过一个实验一个图像分割网络前三个stage用ReLU后面四个stage用SiLU最终精度比全ReLU高0.8%比全SiLU高0.3%而且收敛速度更快。原因在于浅层负责提取边缘和纹理等基础特征ReLU的高稀疏性反而有帮助深层负责语义理解需要更强的非线性表达能力SiLU更合适。具体操作时记住这个顺序先换深层再换浅层。每换一次就训练少量epoch看验证集指标如果下降就回滚。这种渐进式替换比一次性全局替换更稳也更容易定位问题。另一个人经验是如果你从开源仓库拿到的预训练模型用ReLU你想迁移到Swish/Hardswish不要直接微调。先把模型冻结只替换激活函数层做一次前向统计校准把BN的均值和方差重新估计一下再解冻微调。这样精度损失通常可以控制在0.1%以内比直接微调少掉0.5个点。根据我个人体会激活函数的选择很多时候是一个“被忽略的免费午餐”。它不需要额外的训练数据不需要修改网络结构只是换个非线性函数就能稳定提升精度而且推理成本可能更低。在显存紧张的项目里从ReLU换到Hardswish几乎是我的默认操作在算力充足的服务端模型里SiLU则是我的首选。理解了Swish和Hardswish的原理和适用边界你在模型优化时就能多一个有力的决策维度少走不少弯路。