2019-06-07
上一篇我们已经聊过 `CosineConv2D` 这种更偏卷积算子层面的新玩法,这一篇换个方向,看几个不用大改骨架、但很有意思的“小招式”。
这类东西最迷人的点不在于名字新,而在于它们通常都有一个共同特点:
所以它们特别适合拿来做实验。你不一定非得一把梭全网替换,但很适合在自己的模型里先试一层、两层,看看味道对不对。
从公开摘要里可以确认,这篇“其他”主要点到了两类很值得留意的思路:
SinReLUSwish它们的共性是:都在想办法让传统激活函数别那么“硬”,让网络在表达能力和训练行为上多一点回旋空间。
如果用一句话概括这篇的气质,大概就是:
不是推翻 CNN,而是想把它常用的小零件再打磨一下。
ReLU 之所以流行,是因为简单、直接、好训。
但它也有自己的脾气:
SinReLU 这类思路,可以粗略理解成:
在 ReLU 的门控逻辑之外,再给它一点正弦式的起伏感。
你不用一上来就死背公式,先抓直觉就够了:
这种设计为什么会让人觉得有潜力?
对于卷积网络来说,激活函数的形状一旦变得更灵活,网络对局部模式、细小变化、复杂响应的表达,就有机会跟着变活一点。
当然,这类方法是不是一定比 ReLU 强,不存在绝对答案。更准确的说法是:
它给了你一个值得试的方向。
如果说 SinReLU 更像是在 ReLU 的基础上加一点波动感,那么 Swish 的气质就更偏“平滑门控”。
它最常见的写法是:
f(x) = x * sigmoid(beta * x)
这个式子看起来很短,但味道很足。
它不像 ReLU 那样:
而是给输入乘上一个 sigmoid(beta * x) 形成的软权重。于是:
从使用体验上看,Swish 最讨喜的地方就在这里:
所以很多人第一次看到它时都会有一种感觉:
这玩意儿,好像真的挺适合直接塞进现有网络里试试。
很多 CNN 新招并不是在 backbone 上狠狠干一刀,而是专挑“小但关键”的位置下手。
比如:
这类位置有个特点:
它们虽然小,但几乎贯穿全网。
所以你改一个激活函数,影响的并不是一层两层,而是整张网络的非线性表达方式。
这也是为什么很多看起来只是“小修小补”的论文,最后却能带来挺像样的提升。
很多人看新模块时,第一反应都是:
那它到底涨了几点?
这个问题当然重要,但还不够。
像 SinReLU、Swish 这种东西,真正有价值的地方通常还包括:
尤其是 Swish 这一类,优点就在于它非常像一个“替换成本不高”的升级件。对工程实践来说,这种东西的吸引力其实很大。
我的建议很简单:别一上来全网换血。
先从小规模替换开始更稳:
这样你能更快判断:
import tensorflow as tf
from tensorflow import keras
def swish(x, beta=1.0):
return x * tf.sigmoid(beta * x)
inputs = keras.Input(shape=(32, 32, 3))
x = keras.layers.Conv2D(32, 3, padding="same")(inputs)
x = keras.layers.BatchNormalization()(x)
x = keras.layers.Activation(swish)(x)
x = keras.layers.GlobalAveragePooling2D()(x)
outputs = keras.layers.Dense(10, activation="softmax")(x)
model = keras.Model(inputs, outputs)
model.summary()
这类实验的核心不是代码多复杂,而是你能不能把变量控制住。
import torch
import torch.nn as nn
class Swish(nn.Module):
def __init__(self, beta=1.0):
super().__init__()
self.beta = beta
def forward(self, x):
return x * torch.sigmoid(self.beta * x)
x = torch.randn(4, 64, 32, 32)
act = Swish(beta=1.0)
y = act(x)
print(x.shape, y.shape)
顺手提一句,在 PyTorch 里你也可以直接看看 SiLU,它和 Swish 非常接近:
import torch.nn as nn
act = nn.SiLU()
如果你只是想快速做实验,SiLU 会很顺手。
严格实现要看你采用的具体定义,但如果只是为了体验“ReLU + 周期扰动”的思路,可以先写一个玩具版本:
import torch
import torch.nn as nn
class ToySinReLU(nn.Module):
def __init__(self, alpha=0.1):
super().__init__()
self.alpha = alpha
def forward(self, x):
return torch.relu(x) + self.alpha * torch.sin(x)
x = torch.linspace(-4, 4, 9)
layer = ToySinReLU(alpha=0.05)
print(layer(x))
这个版本不是论文标准实现,但足够帮你理解那种“在 ReLU 基础上加一点波动感”的直觉。
如果你属于下面这几类人,这种“新姿势”很值得看:
它不一定每次都能带来压倒性收益,但很适合拿来做:
这篇“其他”看起来像是在随手记几个新点子,其实很有代表性。
它传达出来的意思大概是:
CNN 不只是 backbone 在进化,连最基础的激活函数和局部计算方式,也一直有人在认真折腾。
其中:
SinReLU 更像是在 ReLU 的表达形状上加活力Swish 更像是在平滑门控上做优化它们未必能在所有场景里都赢,但绝对属于那种值得你放进实验清单里的东西。
说白了,很多模型提升并不是一拳把房子拆了重盖,而是把原来那些“默认值”一个个重新审视。激活函数这件事,恰好就是最典型的一项。