深度神经网络中常用的激活函数的优缺点分析
本文主要总结了深度神经网络中常用的激活函数,根据其数学特性分析它的优缺点。
在开始之前,我们先讨论一下什么是激活函数(激活函数的作用)?
如果将一个神经元的输出通过一个非线性函数,那么整个神经网络的模型也就不在是线性的了,这个非线性函数就是激活函数。 显然非线性函数具有更强的表达能力。引入非线性函数作为激励函数,这样深层神经网络表达能力(泛化能力)就更加强大(不再是输入的线性组合,而是几乎可以逼近任意函数)。
1. Sigmoid 函数
Sigmoid函数是传统的神经网络和深度学习领域开始时使用频率最高的激活函数。
Sigmoid函数的数学表达式如下:
f
(
x
)
=
1
1
+
e
−
x
f(x) = \frac {1} {1+e^{-x}}
f(x)=1+e−x1
令
σ
(
x
)
=
1
1
+
e
−
x
\sigma(x) = \frac 1{1+e^{-x}}
σ(x)=1+e−x1
则
f ( x ) = σ ( x ) f(x) = \sigma(x) f(x)=σ(x)
其导数为
f
′
(
x
)
=
σ
(
x
)
(
1
−
σ
(
x
)
)
f'(x) = \sigma(x)(1 - \sigma(x))
f′(x)=σ(x)(1−σ(x))
它的函数图像如下。
作为最早开始使用的激活函数之一, Sigmoid 函数具有如下优点。
- 连续,且平滑便于求导
但是缺点也同样明显。
-
none-zero-centered(非零均值, Sigmoid 函数 的输出值恒大于0),会使训练出现 zig-zagging dynamics 现象,使得收敛速度变慢。
-
梯度消失问题。 由于Sigmoid的导数总是小于1,所以当层数多了之后,会使回传的梯度越来越小,导致梯度消失问题。而且在前向传播的过程中,通过观察Sigmoid的函数图像,当 x 的值大于2 或者小于-2时,Sigmoid函数的输出趋于平滑,会使权重和偏置更新幅度非常小,导致学习缓慢甚至停滞。
-
计算量大。由于采用了幂计算。
建议:基于上面Sigmoid的性质,所以不建议在中间层使用Sigmoid激活函数,因为它会让梯度消失。
1.2 Hard Sigmoid
Hard sigmoid 是一种对于 sigmoid 的近似,主要优势是计算速度快,无需幂计算,所以当对对于速度要求高的情况下,hard sigmoid 是一种选择。在具体的实现形式上有多种方式,比如 pytorch 和 tensorflow 中的实现方式就有差别,但是总之都是对于 sigmoid 的一种近似。下图是 tensorflow 中的 hard sigmoid。
f ( x ) = { 0.2 ∗ x + 0.5 − 2.5 ≤ x ≤ 2.5 0 x < − 2.5 1 x > 2.5 f(x) = \left\{\begin{aligned} & 0.2 * x + 0.5 &-2.5 \leq x \leq 2.5 \\ & 0 \space\space\space & x < -2.5 \\ &1 \space\space\space & x > 2.5 \\ \end{aligned}\right. f(x)=⎩⎪⎨⎪⎧0.2∗x+0.50 1 −2.5≤x≤2.5x<−2.5x>2.5
1.3 SiLU( Sigmoid Linear Unit)
SiLU 也叫 Swish 激活函数,具体参考下面的 6.1 Swish 的内容。
2. Tanh 函数
tanh 函数的表达式为:
t a n h ( x ) = e x − e − x e x + e − x tanh(x) = \frac {e^x-e^{-x}}{e^x+e^{-x}} tanh(x)=ex+e−xex−e−x
其倒数为
t a n h ‘ ( x ) = 1 − t a n h 2 ( x ) tanh‘(x) = 1 - tanh^2(x) tanh‘(x)=1−tanh2(x)
它们的函数图像如下。
通过观察其函数图像,可以发现它的优点主要是解决了none-zero-centered 的问题,但是缺点依然是梯度消失,计算消耗大。但是如果和上面的 sigmoid 激活函数相比, tanh 的导数的取值范围为(0, 1), 而 sigmoid 的导数的取值范围为(0,1/4),显然sigmoid 会更容易出现梯度消失,所以 tanh 的收敛速度会比 sigmoid 快。
3. ReLU 系列
3.1 ReLU
ReLU 是 Hinton 大神于 2010 在 Rectified Linear Units Improve Restricted Boltzmann Machines 中提出, 更多关于 ReLU 的介绍可以参考 [ReLU_WiKi]
ReLU 的函数表达式为:
R e l u ( x ) = m a x { 0 , x } Relu(x) = max \lbrace 0,x \rbrace Relu(x)=max{0,x}
它的函数图像如下。
其优点如下
- x 大于0时,其导数恒为1,这样就不会存在梯度消失的问题
- 计算导数非常快,只需要判断 x 是大于0,还是小于0
- 收敛速度远远快于前面的 Sigmoid 和 Tanh函数
缺点
- none-zero-centered
- Dead ReLU Problem,指的是某些神经元可能永远不会被激活,导致相应的参数永远不能被更新。因为当x 小于等于0时输出恒为0,如果某个神经元的输出总是满足小于等于0 的话,那么它将无法进入计算。有两个主要原因可能导致这种情况产生:
- (1) 非常不幸的参数初始化,这种情况比较少见
- (2) learning rate太高导致在训练过程中参数更新太大,不幸使网络进入这种状态。解决方法是可以采用 MSRA 初始化方法,以及避免将learning rate设置太大或使用adagrad等自动调节learning rate的算法。
这个激活函数应该是在实际应用中最广泛的一个。
3.2 ReLU6
ReLU6 最早由 Alex(就是提出 AlexNet 的那个 Alex)于2010年提出,具体可以参考 Convolutional Deep Belief Networks on CIFAR-10 。
ReLU6 仅仅是在 ReLU 的基础上进行了 clip 操作,即限制了最大输出,比较典型的是在 mobile net v1 中有使用,可以参考MobileNet 进化史: 从 V1 到 V3(V1篇) 。
f
(
x
)
=
min
(
max
(
0
,
x
)
,
6
)
\begin{aligned} f(x) = \min(\max(0,x), 6) \\ \end{aligned}
f(x)=min(max(0,x),6)
转换为更清晰的形式为:
f
(
x
)
=
{
0
x
≤
−
3
6
x
≥
3
x
o
t
h
e
r
w
i
s
e
f(x) = \left\{\begin{aligned} & 0 & x\leq -3 \\ & 6 & x \geq 3 \\ & x & otherwise \\ \end{aligned}\right.
f(x)=⎩⎪⎨⎪⎧06xx≤−3x≥3otherwise
比较有意思的是,为什么是6,不是7或者其他呢?结合原文的说明,可能是作者尝试了多种,ReLU6 效果最好。
... First, we cap the units at 6, so our ReLU activation function is y = min(max(x, 0), 6). In our tests, this
encourages the model to learn sparse features earlier. In the formulation of [8], this is equivalent to imagining
that each ReLU unit consists of only 6 replicated bias-shifted Bernoulli units, rather than an infinute amount.
We will refer to ReLU units capped at n as ReLU-n units.
3.3 Leaky ReLU
Leaky ReLU 由 Andrew L. Maas 等人于 2014 年 在 Rectifier Nonlinearities Improve Neural Network Acoustic Models 中提出。
Leaky ReLU 函数表达式为
f ( x ) = m a x { 0.01 x , x } f(x) = max \lbrace 0.01x,x \rbrace f(x)=max{0.01x,x}
其函数图像为
Leaky ReLU 的提出主要是为了解决前面 Relu 提到的 Dead ReLUProblem 的问题,因为当 x 小于 0 时,其输出不再是 0.
而同时 Leaky ReLU 具有 ReLU 的所有优点。听上去貌似很好用,只是在实际操作中并没有完全证明好于 ReLU 函数。
这里其实还有一个小的变种 PReLU,其函数表达式如下:
f
(
x
)
=
m
a
x
{
α
x
,
x
}
f(x) = max \lbrace \alpha x,x \rbrace
f(x)=max{αx,x}
在实际的使用中PRelu使用的是比较多的。需要注意的是,在 pytorch/tensorflow等平台上提供的 leaky-relu 其实是这里的 PReLU 。
4 ELU(Exponential Lenear Unit)
ELU 是 Clevert, Djork-Arné 等人于 2015 年在 Fast and Accurate Deep Network Learning by Exponential Linear Units (ELUs) 中提出。
ELU 函数表达式为:
f ( x ) = { α ( e x − 1 ) , x ≤ 0 x , x > 0 f(x)=\left\{ \begin{aligned} \alpha(e^x -1), x \leq0\\ x, x >0\\ \end{aligned} \right. f(x)={α(ex−1),x≤0x,x>0
其函数图像为
ELU 主要是对小于等于 0 的部分进行了改进,使得函数在 x = 0 处可导, 而且 使得神经元的平均激活均值趋近为 0,对噪声更具有鲁棒性。
缺点是由于又引入了指数,计算量增大了。
5. SELU(Self_Normalizing Neural Networks)
SELU 函数表达式为:
f
(
x
)
=
λ
{
α
(
e
x
−
1
)
,
x
≤
0
x
,
x
>
0
f(x)=\lambda\left\{ \begin{aligned} \alpha(e^x -1), x \leq0\\ x, x >0\\ \end{aligned} \right.
f(x)=λ{α(ex−1),x≤0x,x>0
显然仅仅是在 ELU 的基础上加了一个系数。其目的据说(Self-Normalizing Neural Networks是为了使输入在经过一定层数之后变成固定的分布。
6.Swish 系列
6.1 Swish
Swish 也叫 SiLU 激活函数,其表达式很明了。
f
(
x
)
=
x
∗
σ
(
x
)
w
h
e
r
e
σ
i
s
s
i
g
m
o
i
d
a
c
t
i
v
a
t
i
o
n
f
u
n
c
t
i
o
n
f(x) = x*\sigma(x) \ \ where\ \sigma \ is\ sigmoid\ activation\ function
f(x)=x∗σ(x) where σ is sigmoid activation function
Swish 具备无上界有下界、平滑、非单调的特性,这些都在 Swish 和类似激活函数的性能中发挥有利影响。效果较 ReLU 要好,特别是在较深的网络中优势更明显。YOLOV5 1.0 中的激活函数就是 Swish。
6.2 HardSwish
Hard swish 是 mobilev3 和 YOLOV5 3.0 的激活函数。其表达式如下。
f
(
x
)
=
x
∗
R
e
l
u
6
(
x
+
3
)
/
6
f(x) = x * Relu6(x+3)/6
f(x)=x∗Relu6(x+3)/6
更具体的表达式如下。
f
(
x
)
=
{
0
x
≤
−
3
x
x
≥
3
x
∗
(
x
+
3
)
/
6
o
t
h
e
r
w
i
s
e
f(x) = \begin{cases} & 0 & x \leq -3 \\ & x & x \geq 3 \\ & x * (x + 3)/6 &otherwise \end{cases}
f(x)=⎩⎪⎨⎪⎧0xx∗(x+3)/6x≤−3x≥3otherwise
相比swish,非线性提高了精度,但是在嵌入式环境中,他的成本是非零的,因为在移动设备上计算sigmoid函数代价要大得多。
7. Mish
Mish 激活函数是 Diganta Misra 于 2019 年在 Mish: A Self Regularized Non-Monotonic Activation Function 中提出的。YOLOv4 中采用了 Mish 激活函数。Mish 的函数表达式如下:
f
(
x
)
=
x
∗
t
a
n
h
(
l
n
(
1
+
e
x
)
)
f(x) = x * tanh(ln(1+e^x))
f(x)=x∗tanh(ln(1+ex))
我们再来看看 Mish 激活 和其倒数的曲线图。
一看这图和 Swish 激活函数是不是很像,无上界有下界、平滑、非单调。论文中作者重点对比了 Mish 和 Swish,结论是 Mish 更牛逼。但是 YOLO v5 用的 Swish。
8. Softmax
SoftMax 函数表达式为:
S
o
f
t
m
a
x
(
x
)
=
e
x
i
∑
k
=
1
K
e
x
k
Softmax(x) = \frac {e^ {x_i}} {\sum_{k=1} ^K e^ {x_k}}
Softmax(x)=∑k=1Kexkexi
Softmax 函数可视为 Sigmoid 函数的泛化形式, 其本质就是将一个 K 维的任意实数向量压缩(映射)成另一个 K 维的实数向量, 其中向量中的每个元素的取值范围都介于 [ 0 , 1 ] [0,1] [0,1] 之间(可以理解为概率)。Softmax 常常是作为多分类神经网络的输出,比如 LeNet-5 中就用Softmax 映射到最后的输入结果,其表示1~10的概率。
引用
本文主要参考《深度学习之图像识别》和 常用激活函数(激励函数)理解与总结。
关于 Swish & Maxout 可以参考激活函数(ReLU, Swish, Maxout)。
附录
下面是绘制激活函数曲线的 python 代码
# -*- coding: utf-8 -*-
import numpy as np
import matplotlib.pyplot as plt
import math
def single_sigmoid(x):
return 1/(1 + np.e**(-x))
def sigmoid(x):
y = np.zeros_like(x)
for i, ele in enumerate(x):
y[i] = single_sigmoid(ele)
return y
def swish(x):
y = np.zeros_like(x)
for i, ele in enumerate(x):
y[i] = single_sigmoid(ele)*ele
return y
def swish_d1(x):
y = np.zeros_like(x)
for i, ele in enumerate(x):
temp = single_sigmoid(ele)
y[i] = temp + ele * temp * (1-temp)
return y
def hard_sigmoid(x):
y = np.zeros_like(x)
for i, ele in enumerate(x):
if ele < -2.5:
y[i] = 0
elif ele > 2.5:
y[i] = 1
else:
y[i] = 0.2 * ele + 0.5
return y
def hard_sigmoid_d1(x):
y = np.zeros_like(x)
for i, ele in enumerate(x):
if ele < -2.5:
y[i] = 0
elif ele > 2.5:
y[i] = 0
else:
y[i] = 0.2
return y
def relu6(x):
y = np.zeros_like(x)
for i, ele in enumerate(x):
if ele < 0:
y[i] = 0
elif ele > 6:
y[i] = 6
else:
y[i] = ele
return y
def relu6_d(x):
y = np.zeros_like(x)
for i, ele in enumerate(x):
if ele < 0:
y[i] = 0
elif ele > 6:
y[i] = 0
else:
y[i] = 1
return y
def hardswish(x):
y = np.zeros_like(x)
for i, ele in enumerate(x):
if ele < -3:
y[i] = 0
elif ele > 3:
y[i] = ele
else:
y[i] = ele*(ele+3)/6
return y
def hardswish_d(x):
y = np.zeros_like(x)
for i, ele in enumerate(x):
if ele < -3:
y[i] = 0
elif ele > 3:
y[i] = 1
else:
y[i] = ele/3 + 0.5
return y
def tanh(k):
a = np.e**(k)
b = np.e**(-k)
return (a - b)/(a+b)
def mish(x):
y = np.zeros_like(x)
for i, ele in enumerate(x):
y[i] = ele * tanh(math.log(1 + np.e**ele))
return y
def mish_d(x):
y = np.zeros_like(x)
for i, ele in enumerate(x):
n = np.e**ele
m = math.log(1 + n)
y[i] = ele * (1 - (tanh(m))**2) * n /(1 + n) + tanh(m)
return y
def draw(X, *args):
plt.figure(figsize=(8, 5), dpi=80)
ax = plt.subplot(111)
ax.spines['right'].set_color('none')
ax.spines['top'].set_color('none')
ax.xaxis.set_ticks_position('bottom')
ax.spines['bottom'].set_position(('data',0))
ax.yaxis.set_ticks_position('left')
ax.spines['left'].set_position(('data',0))
color = ['blue', 'red', 'green', 'yellow', 'brown']
for i, F in enumerate(args):
clr = color[i%6]
Y = F(X)
plt.plot(X, Y, color=clr, linewidth=2.5, linestyle="-", label=F.__name__)
plt.xlim(X.min()*1.1, X.max()*1.1)
plt.ylim(Y.min()*4, Y.max()*1.1)
plt.legend(loc='upper left', frameon=False)
#
# t = 2*np.pi/3
# plt.plot([t,t],[0,np.cos(t)],
# color ='blue', linewidth=1.5, linestyle="--")
# plt.scatter([t,],[np.cos(t),], 50, color ='blue')
# plt.annotate(r'$\sin(\frac{2\pi}{3})=\frac{\sqrt{3}}{2}$',
# xy=(t, np.sin(t)), xycoords='data',
# xytext=(+10, +30), textcoords='offset points', fontsize=16,
# arrowprops=dict(arrowstyle="->", connectionstyle="arc3,rad=.2"))
#
# plt.plot([t,t],[0,np.sin(t)],
# color ='red', linewidth=1.5, linestyle="--")
# plt.scatter([t,],[np.sin(t),], 50, color ='red')
# plt.annotate(r'$\cos(\frac{2\pi}{3})=-\frac{1}{2}$',
# xy=(t, np.cos(t)), xycoords='data',
# xytext=(-90, -50), textcoords='offset points', fontsize=16,
# arrowprops=dict(arrowstyle="->", connectionstyle="arc3,rad=.2"))
#
#
# plt.savefig("exercice_10.png",dpi=72)
plt.show()
if __name__ == '__main__':
x = np.linspace(-10, 10, 500, endpoint=True)
# draw(x, hard_sigmoid, hard_sigmoid_d1)
# draw(x, relu6, relu6_d)
# draw(x, hardswish)
draw(x, mish, mish_d)