资讯详情

Swish与Hard-Swish激活函数:从原理到移动端部署实践

📅 2026/9/17 20:05:00 | 华诺云谱 👁 阅读
Swish与Hard-Swish激活函数:从原理到移动端部署实践
1. 先从激活函数说起为什么 ReLU 不够用1.1 激活函数的本质做深度学习的人几乎每天都会跟激活函数打交道但说实话很多人对它的理解停留在“加一个非线性”这个层面。神经网络如果只有卷积、全连接这类线性操作那不管堆多少层整体依然是一个线性变换。你可以想象成一张纸无论怎么折叠它还是一个平面只有引入非线性纸张才能真正变成有起伏的立体结构。这个让网络“弯”起来的角色就是激活函数。早期主流是 Sigmoid 和 tanh。Sigmoid 把任意实数压到 (0,1) 区间天然适合做概率输出但问题也很明显输入绝对值一大函数就进入饱和区梯度趋近于 0反向传播时误差信号越传越弱深层网络基本训不动。tanh 把输出中心改成了 0收敛比 Sigmoid 好一些但饱和区的梯度消失问题依然在。所以在 ResNet 出现前后ReLU 迅速取代了它们成为默认选择。ReLU 的公式是 y max(0, x)正半轴梯度恒为 1计算几乎不花代价这让训练速度和稳定性都有了质的提升。但 ReLU 不是没有短板。负半轴直接截断意味着一旦某个神经元的输入长期落在负区间它的输出会一直是 0梯度也永远是 0这个神经元就“死”了。虽然实际训练中 BN、较小的学习率可以缓解这个问题但它始终是悬在头上的刀。另外 ReLU 在 x0 处不可导理论上看总归不够优雅。1.2 深度网络对激活函数的新要求当网络从十几层发展到上百层激活函数的影响会被急剧放大。尤其是 MobileNet 这类轻量级模型本身参数就不多每一层的表达效率都要被尽量榨干激活函数一旦选错精度掉得比大模型明显得多。现代深度网络对激活函数其实有几个隐含要求。第一函数最好单调且有下界、无上界。有下界能避免输出在负方向无限漂移无上界则保证深层响应不会被压死。第二最好能兼顾稀疏性和梯度流动性。ReLU 的稀疏性很好但负半轴太“死”Sigmoid 梯度平滑但会饱和。新的激活函数往往试图在这两者之间找平衡。第三计算要便宜。这里说的便宜不是 GPU 上的浮点运算次数而是移动端 NPU、DSP 上的实现成本一次指数运算可能就让延迟预算破功。Swish 和 Hard-Swish 正是在这种需求下被推上前台的。2. Swish 的来历与设计原理2.1 自门控把“门”做到激活函数里Swish 是 Google Brain 团队在 2017 年提出的论文标题是 Searching for Activation Functions。它不是某个研究员凭经验拍脑袋写的公式而是通过强化学习在候选算子空间里搜索出来的。搜索得到的最终形式非常简单f(x) x · sigmoid(β · x)当 β 1 时就是我们最常见的 Swish-1PyTorch 里也把它叫做 SiLU。这个公式最大的特点就是“乘”。x 自己经过 sigmoid 生成一个 0 到 1 之间的门控系数再乘回自己。等于说每个神经元都有一个软开关开关的闭合程度由输入自己决定输入越大门开得越大输入越小门关得越紧。这就是自门控self-gating的核心思路。这个设计的好处体现在负半轴。ReLU 对所有负输入一律输出 0Swish 在输入接近于 0 的负区间会输出一个很小的负值而不是直接截断。梯度因此可以在负半轴继续流动神经元死亡的概率大幅下降。而且 Swish 的整个函数曲线是光滑的处处可导对梯度优化更友好。2.2 与 GELU 和 Sigmoid 的家族关系如果你看到 GELU 的公式 x · Φ(x)其中 Φ 是标准正态分布的累积分布函数会发现它和 Swish 长得几乎一样。本质上两者都是“输入乘上一个值域在 (0,1) 之间的门控”。GELU 用的是正态分布 CDFSwish 用的是 sigmoid曲线形状高度重合在很多任务上表现也极其接近。Transformer 里普遍使用 GELU而近年来很多大模型又重新用回 SiLU也就是 β1 的 Swish说明这个函数族已经成了现代网络的基础设施。你也可以把 Swish 理解成一种平滑化的 ReLU。ReLU 在原点附近从 0 直接跳到一次函数Swish 则是一个缓慢爬升的过渡曲线。这个平滑性带来的实际收益是输入在小范围内抖动时输出不会剧烈变化模型的局部敏感度更低。这一点在训练初期权重还没稳定时尤为重要。H-Swish 的“硬”版本就是在这种曲线的基础上做了一次分段线性近似。用分段函数去模拟原曲线再把指数运算彻底去掉。如果你看过 Transformer 里的近似 softmax、某些硬件加速器里的激活函数查表实现会发现这个思路在不断重复保留函数的几何形状去掉昂贵的计算原语。3. Hard-Swish 诞生移动端部署的妥协与智慧3.1 为什么移动端不喜欢 SigmoidGPU 上有专门的指数运算硬件单元计算 sigmoid 几乎是免费的所以 Swish 在训练阶段跑得飞快。但手机 NPU、DSP、以及很多嵌入式芯片完全不同它们对指数运算没有硬件加速一个 sigmoid 可能需要调用软件库的泰勒展开或查表实现延迟一下就上去了。MobileNetV3 论文里专门提过这个问题swish 里的 sigmoid 在移动端设备上实现开销过高尤其是低精度量化场景sigmoid 对数值范围太敏感均匀量化会引入不小的误差。于是 Hard-Swish 被正式引入到网络设计中。它的公式可以写成hard_swish(x) x · ReLU6(x 3) / 6也可以改成更工程化的写法hard_swish(x) x · clip((x 3) / 6, 0, 1)两种形式本质一样都是让门控值落在 [0,1] 区间且完全不需要指数运算操作只有加法、裁剪、乘法。这个函数在移动端可以拆解成几个极其高效的底层算子非常符合 NPU 的执行模型。3.2 Hard-Swish 的数学形式与近似质量ReLU6 就是 y min(max(0, x), 6)。把输入 x 加上 3通过 ReLU6 后取值范围是 [0,6]再除以 6就得到了 [0,1] 的门控系数。这个系数和 sigmoid(x) 在 x 取 -3 到 3 之间非常接近。超出这个区间Swish 的门控会无限逼近 0 或 1Hard-Swish 则直接截断到边界。这种近似会引入两个不可导点分别在 x-3 和 x3 处。理论上这两个点的梯度不完全连续但实际训练中几乎不会带来问题。MobileNetV3 在 ImageNet 上的实验显示用 Hard-Swish 替换 Swish准确率基本持平但推理延迟有可感知的下降。对于追求极致的移动端模型来说这就是把省下的开销用在了刀刃上。对比项SwishHard-Swish公式x · sigmoid(x)x · ReLU6(x3) / 6计算复杂度指数 乘法 加法加法 裁剪 乘法 除法量化友好性较差sigmoid 区间敏感好分段线性易于量化梯度平滑性全区间光滑在 x-3 和 x3 处有一个小折点移动端速度较慢快适合 NPU/DSP 部署4. 实操在 PyTorch 与 TensorFlow 里落地 Swish 和 Hard-Swish4.1 PyTorch 实现与集成PyTorch 目前没有把 Swish 直接放进 torch.nn 的顶层 API 里Hard-Swish 反而有就是 torch.nn.Hardswish。如果你做研究或者自定义模型时想用 Swish通常需要自己封装一个模块代码并不多import torch import torch.nn as nn import torch.nn.functional as F class Swish(nn.Module): def __init__(self, beta1.0): super().__init__() self.beta beta def forward(self, x): return x * torch.sigmoid(self.beta * x) class HardSwish(nn.Module): def forward(self, x): return x * F.relu6(x 3) / 6实际用的时候我强烈建议把激活函数做成一个可配置的模块而不是直接写死在每个 Block 里。因为你大概率会在 Swish、Hard-Swish、ReLU 之间来回切换做对比实验。我之前吃过这个亏结构里到处是 nn.ReLU()后面想改成 Swish只能全局搜索替换非常被动。4.2 TensorFlow / Keras 实现与导出注意点TensorFlow 的 Keras 接口里直接提供了 hard_swish 和 swish 激活函数使用起来更省事。不过自定义 beta 的 Swish 还是得自己来import tensorflow as tf def swish(x, beta1.0): return x * tf.sigmoid(beta * x) inputs tf.keras.layers.Input(shape(32,)) x tf.keras.layers.Dense(64)(inputs) x tf.keras.layers.Activation(swish)(x)需要注意Keras 内置的 tf.keras.activations.swish 实际对应的是 tf.nn.silu也就是固定 β1 的 Swish。如果想用可训练 β就得写自定义层。模型导出到 TFLite 或 ONNX 时自定义激活函数很可能会变成不支持的算子。所以在做部署转换前建议先把这些激活函数替换成框架原生支持的形式然后再导出。4.3 在 MobileNetV3 中的使用位置Hard-Swish 最出名的应用场景就是 MobileNetV3。在 MobileNetV3 的 Bottleneck 结构里Hard-Swish 并不是被放在所有卷积层后面而是有选择地放在最后几个 stage 的逐点卷积之后。前几个 stage 仍然使用 ReLU。这个设计透露出一个重要的工程思想不是越复杂的激活函数就越该到处用。前几层处理的是纹理、边缘这类低层特征ReLU 的稀疏性和低成本优势更大。到了高层语义特征提取阶段网络需要更强的非线性表达能力这时再用 Hard-Swish而它的额外计算量只作用于较少的通道和较小的特征图延迟影响可以控制在合理范围。如果你在自定义网络里用 Hard-Swish我建议也遵循这种“靠后放置”的原则而不是无脑替换所有激活。5. 训练细节与调参心得为什么 Swish 能刷精度却容易翻车5.1 BN 顺序与学习率的敏感度把网络中的 ReLU 直接替换成 Swish最容易遇到的问题就是训练变慢而且不是慢一点点。原因是 ReLU 会强制把负半轴清零激活值稀疏梯度流动路径干净Swish 几乎所有输入都有非零梯度梯度信号的方差更大网络需要更长时间来稳定。我最早做这个替换时在一个图像分类模型上保持初始学习率不动训练几个 epoch 后发现 loss 不降反升。把学习率从 0.1 降到 0.01 以后训练才恢复稳定。所以如果你的基线网络是围绕 ReLU 调好的切换到 Swish 之后一定要重新扫描学习率通常需要降低 2 到 5 倍。BatchNorm 的位置也不能忽略。我的推荐顺序是 Conv - BN - Swish。BN 先把卷积输出拉到一个相对稳定的分布然后 Swish 再引入非线性这样训练时会稳定很多。如果网络结构特殊需要把激活放在 BN 前面务必用实验验证不要想当然。5.2 β 参数到底要不要训练原版 Swish 允许 β 作为可训练参数这样网络能自己决定门控曲线的形状。β 小Swish 接近线性β 大Swish 接近 ReLU。从表达能力上说可训练 β 确实更有潜力但收益并不稳定。我自己做过一组对比在相同参数量的分类模型上固定 β1 和可训练 β 的精度差距在 0.1% 以内远小于不同随机种子带来的波动。而可训练 β 引入了额外的优化难度有时 β 还会收敛到负值导致激活曲线变得很奇怪。再加上部署时可训练 β 会变成一个额外的动态参数不利于模型转换。除非你专门研究激活函数否则直接用固定 β1 就好。5.3 数值稳定性与初始化Swish 的输出不像 ReLU 那样会截断负值所以在没有 BatchNorm 的网络上它对权重初始化更敏感。如果初始权重过大深层激活值可能持续累积最终导致数值溢出或梯度爆炸。尤其是在 FP16 混合精度训练下sigmoid 输入一旦很大就会饱和到 0 或 1梯度变成 0训练瞬间失效。一个稳妥的做法是切换 Swish 时把卷积和全连接层的初始化尺度调小一些。比如使用 Xavier 初始化时把 gain 设为 0.5而不是默认的 1.0。另一个更省心的办法是直接用 Hard-Swish它的值域更加可控对低精度训练天然友好。如果你的部署目标是移动端训练阶段直接用 Hard-Swish 从头训能省去很多后期适配的麻烦。6. 常见问题与排查技巧实录6.1 模型从 Swish 换成 Hard-Swish 后准确率下降这个问题我遇到过好几次。训练好的 Swish 模型直接替换成 Hard-Swish 进行推理准确率有明显下降。原因并不复杂Hard-Swish 只是 Swish 的近似两者的函数曲线在两端和原点附近都有细微差异特征分布已经发生了偏移。正确的做法是“用哪个函数训练就用哪个函数推理”。如果最终要部署 Hard-Swish那在训练时就用 Hard-Swish或者至少在训练的最后阶段做若干轮 finetune。MobileNetV3 的官方实现就是从头训练时就使用 Hard-Swish并不是训完 Swish 再换过去。这一点经常被忽略但它决定了精度能不能保住。6.2 FP16 训练时出现 NaNFP16 的表示范围比 FP32 小得多sigmoid 在输入绝对值较大的情况下很容易饱和。如果网络比较深FP16 反向传播时梯度会出现 NaN。排查时先看 loss 曲线如果前几步就跳到 NaN基本可以断定是数值溢出。我常用的解决方案有三个一是在关键层插入 BN把激活输入拉回安全范围二是把 Swish 替换成 Hard-Swish三是如果必须保留 Swish就对 sigmoid 的输入做 clamp限制在 [-15, 15] 范围。这样既不会完全失去梯度也能避免溢出。这个方法我在多个模型上验证过精度损失可以忽略但训练稳定性提升明显。6.3 部署到移动端算子不支持很多移动端 NPU 对 sigmoid 的支持非常保守因为硬件上没有对应的指令运行时只能退回到 CPU 实现延迟会一下子涨上去。如果模型导出成 ONNX 或 TFLite 时包含自定义 Swish轻则出现警告重则直接导出失败。我的建议是写一个脚本在导出前把所有 Swish 模块批量替换成 HardSwishdef replace_swish_with_hardswish(model): for name, module in model.named_children(): if isinstance(module, Swish): setattr(model, name, nn.Hardswish()) else: replace_swish_with_hardswish(module) return model替换之后务必跑一遍精度对比。如果发现单算子输出差异大于 1e-3需要检查 Hard-Swish 的实现版本不同框架在边界位置的舍入方式可能不同量化模型里这种细小的差异会被放大最终影响精度。7. Swish 家族在非卷积结构里的表现7.1 Transformer 与 MLP 里的 SiLUSwish 并不只属于卷积网络。Transformer 系列模型中前馈网络 FFN 的激活函数普遍选择 GELU但近年来很多开源大模型用了 SiLU也就是 β1 的 Swish。比如 LLaMA 系列中就在 MLP 结构里用 SiLU。原因之一是 SiLU 的梯度特性比 GELU 更平滑另一个原因是它在很多深度学习框架里已经有高效实现计算开销不比 GELU 高。在一些多模态模型里我也见过把 Hard-Swish 放在 token 融合层后面的做法。这类层通常处理的是高维特征需要激活函数有较强的非线性表达能力同时又要控制推理延迟Hard-Swish 正好适合。7.2 从激活函数的演进看模型优化激活函数的发展过程本质上是一个“表达能力和计算代价博弈”的过程。Sigmoid 提供了光滑非线性但代价是梯度消失ReLU 用稀疏性和零计算成本赢得了训练效率但牺牲了负半轴的信息Swish 在这两者之间找到了一个巧妙的折中用一次乘法换来了更好的梯度流动性Hard-Swish 又用分段线性近似把乘法里的指数部分省掉。这种取舍思路可以用到整个模型优化中理解每个算子的真实成本而不是只看论文里的 FLOPs。FLOPs 只是理论计算量实际部署耗时还取决于算子是否被硬件原生支持、是否能被融合、是否能量化。MobileNetV3 选择 Hard-Swish本质上是“用可量化的分段线性去替代难量化的指数函数”。如果你在做模型压缩这个思路非常值得借鉴。最后再分享一个我自己的经验。做模型优化时间久了会发现很多结构上的巧思最后都会落到工程细节上。Swish 和 Hard-Swish 这对组合恰好给了我们一个完整的观察窗口一个用精妙的曲线换效果一个用粗糙的直线换速度两者并不冲突各取所需。GPU 上训练时你可以放心享受 Swish 的平滑梯度但如果目标是手机、嵌入式设备那 Hard-Swish 基本就是更理性的选择。不要觉得它“不够高级”在 100ms 延迟预算面前少算一次指数比什么都实在。下次做模型设计时建议你把这两种函数都跑一遍记录一下每层激活值的分布再做决定。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。