深度学习中的激活函数
众所周知神经网络单元是由线性单元和非线性单元组成的,而非线性单元就是我们今天要介绍的–激活函数,不同的激活函数得出的结果也是不同的。他们也各有各的优缺点,虽然激活函数有自己的发展历史,不断的优化,但是如何在众多激活函数中做出选择依然要看我们所实现深度学习实验的效果。
这篇博客会分为上下两篇,上篇介绍一些常用的激活函数(Sigmoid、tanh、ReLU、LeakyReLU、maxout)。下篇介绍一下不常用的激活函数(PRelu、ELU、SELU)。
sigmoid
sigmoid激活函数将输入映射到(0,1)之间,他的数学函数为:
$$\sigma (z)=\frac{1}{1+e^{-z}}$$
历史上sigmoid非常常用,但是由于他的两个缺点,实际很少用了,现在看到sigmoid激活函数,都是在新手教程中做一些简单的实验。
优点
它能够把输入的连续实值变换为0和1之间的输出,特别的,如果是非常大的负数,那么输出就是0;如果是非常大的正数,输出就是1.
缺点
函数饱和使梯度消失
我们先看sigmoid激活函数的导数图像,
如果我们初始化神经网络的权值为$[0,1]$之间的随机值,由反向传播算法的数学推导可知,梯度从后向前传播时,每传递一层梯度值都会减小为原来的0.25倍,如果神经网络隐层特别多,那么梯度在穿过多层后将变得非常小接近于0,即出现梯度消失现象;当网络权值初始化为$(1,+\infty )$区间内的值,则会出现梯度爆炸情况。
不是原点中心对称
输出不是0均值(既zero-centerde),这会导致后一层的神经元将得到上一层输出的非均值的输入。产生的结果就是:如$x>0,f=W^Tx_b$,那么对w求局部梯度则都为正,这样在反向传播的过程中w要么都往正方向更新,要么都往负方向更新,导致有一种捆绑的效果,使得收敛缓慢。
这个特性会导致后面网络层的输入不是零中心的,如果输入都是正数的话(如 x>0 中 ),那么关于$W$的梯度在反向传播过程中,权重要么全往正方向更新,要么全往负方向更新,这样很可能导致陷入局部最小值。当然了,如果按batch去训练,那么那个batch可能得到不同的信号,所以这个问题还是可以缓解一下的。
运算量大:
解析式中含有幂运算,计算机求解时相对来讲比较耗时。对于规模比较大的深度网络,这会较大地增加训练时间。
def sigmoid(x): return 1.0 / (1.0 + np.exp(-x))
View Code
tanh
tanh函数它的输出是zero-centered的,但是它同样存在梯度消失和幂指数问题。数学函数为:
$$f(z)=tanh(z)=\frac{e^{z}-e^{-z}}{e^z}+e^{-z}$$
def tanh(x): return (np.exp(x) - np.exp(-x)) / (np.exp(x) + np.exp(-x))
View Code
tanh函数相比于Sigmoid函数往往更具有优越性,这主要是因为Sigmoid函数在输入处于[-1,1]之间时,函数值变化敏感,一旦接近或者超出区间就失去敏感性,处于饱和状态。
ReLU
这才是一个目前主流论文中非常常用的激活函数,它的数学公式为:
$$f(x)=max(0,x)$$
def relu(x): return np.where(x<0,0,x)
View Code
优点
- ReLU的计算量小,收敛速度很快,因为sigmoid和tanh,ReLU有指数运算
- 在正区间(x>0)解决了梯度消失问题。
缺点:
- ReLU的输出不是zero-centered
- RuLU在训练的时候很容易导致神经元“死掉”
死掉:一个非常大的梯度经过一个 ReLU 神经元,更新过参数之后,这个神经元再也不会被任何数据激活相应的权重永远不会更新。有两种原因导致这种情况:1、非常不幸的初始化。2、学习率设置的太高导致在训练过程中参数更新太大,解决方法是使用Xavier初始化方法,合理设置学习率,会降低这种情况的发生概率。或使用Adam等自动调节学习率的算法。
补充:ReLU相比sigmoid和tanh的一个缺点是没有对上界设限,在实际使用中,可以设置一个上限,如ReLU6经验函数: f(x)=min(6,max(0,x))
LeakyReLU
LeakyReLU也有人称为PReLU,但是还是不太一样的,LeakyReLU中的斜率a是自定义的,pReLU中的a是通过训练学习得到的,LeakyReLU是为了解决“ReLU死亡”问题的尝试
$$f(x)=\left\{\begin{matrix}
x&&x>0\\
0.01x&&其他
\end{matrix}\right.$$
ReLU 中当 x<0 时,函数值为 0 。而 Leaky ReLU 则是给出一个很小的负数梯度值,比如 0.01 。
有些研究者的论文指出这个激活函数表现很不错,但是其效果并不是很稳定。
def prelu(x,a): return np.where(x<0,a*x,x)
View Code
虽然Leaky ReLU修复了ReLU的神经元死亡问题,但是在实际的使用并没有完全证明Leaky ReLU完全优于ReLU。
softmax
softmax用于多分类神经网络输出,如果某一个$a_i$打过其他z,那这个映射的分量就逼近1,其他就逼近0,主要应用于“分类”。
$$SOFTMAX:a_i=\sigma_i(z)=\frac{e^{z_i}}{\sum_{j=1}^{m}e^{z_j}},z_i=w_ix+b$$
作用:把神经元中线性部分输出的得分值(score),转换为概率值。softmax输出的是(归一化)概率,
含有softmax激活函数的网络层有这样一个性质:$\sum_{i=1}^{j}\sigma _i(z)=1$,可以解释为每个节点的输出值小于等于1。softmax激励函数通常在神经网络的最后一层作为分类器的输出,输出值(概率)最大的即为分类结果。
$$猫:\begin{pmatrix}0.05\\ 0.05\\ 0.7\\ 0.2\end{pmatrix} 狗:\begin{pmatrix}0.8\\ 0.06\\ 0.01\\ 0.04\end{pmatrix}$$
PReLU
在RReLU中,负值的斜率$a_i$在训练中是随机的,$a_i$是可学习的,如果$a_i=0$,那么 PReLU 退化为ReLU;如果$a_i$是一个很小的固定值(如$a_i=0.01$),则 PReLU 退化为 Leaky ReLU。
$a_i$在之后的测试中就变成了固定的了。RReLU的亮点在于,在训练环节中,$a_i$是从一个均匀的分布$U(I,u)$中随机抽取的数值。形式上来说,我们能得到以下数学表达式:
$$f(x)=\left\{\begin{matrix}
x&&x>0\\
a_ix&&x\leqslant 0
\end{matrix}\right.$$
其中$$a_i\sim U(x,y),区间(x,y)上的均匀分布;x,y\in [0,1]$$
优点
(1)PReLU只增加了极少量的参数,也就意味着网络的计算量以及过拟合的危险性都只增加了一点点。特别的,当不同channels使用相同的$a$时,参数就更少了。
(2)BP更新$a$时,采用的是带动量的更新方式,如下:
$$\Delta a_i=\mu \Delta a_i+\epsilon \frac{\partial \varepsilon }{\partial a_i}$$
ELU
ELU也是为了解决ReLU存在的问题而提出的,ELU有ReLU的基本所有优点,以及不会有Dead ReLU问题,和输出的均值接近0(zero-certered),它的一个小问题在于计算量稍大。类似于Leaky ReLU,理论上虽然好于ReLU,但在实际使用中目前并没有好的证据ELU总是优于ReLU。
$$f(x)=\left\{\begin{matrix}
x&&x>0\\
\alpha (e^x-1)&&x\leq 0
\end{matrix}\right.$$
$$f'(x)=\left\{\begin{matrix}
1&&x>0\\
f(x)+a&&x\leq 0
\end{matrix}\right.$$
def elu(x, a): return np.where(x < 0, a*(np.exp(x)-1), a*x)
View Code
其中$\alpha$是一个可调整的参数,它控制着ELU负值部分在何时饱和。右侧线性部分使得ELU能够缓解梯度消失,而左侧软饱能够让ELU对输入变化或噪声更鲁棒。ELU的输出均值接近于零,所以收敛速度更快
SELU
$$SELU(x)=\lambda \left\{\begin{matrix}
x&&x>0\\
\alpha e^x-\alpha &&x\leq 0
\end{matrix}\right.$$
经过该激活函数后使得样本分布自动归一化到0均值和单位方差(自归一化,保证训练过程中梯度不会爆炸或消失,效果比Batch Normalization 要好)
其实就是ELU乘了个$\alpha$,关键在于这个$\alpha$是大于1的。以前relu,prelu,elu这些激活函数,都是在负半轴坡度平缓,这样在激活函数的方差过大的时候可以让它减小,防止了梯度爆炸,但是正半轴坡度简单的设成了1。而selu的正半轴大于1,在方差过小的的时候可以让它增大,同时防止了梯度消失。这样激活函数就有一个不动点,网络深了以后每一层的输出都是均值为0方差为1。
def selu(x): alpha = 1.6732632423543772848170429916717 scale = 1.0507009873554804934193349852946 return scale*np.where(x>=0.0, x, alpha*(np.exp(x)-1))
View Code
其中超参 α
和 λ
的值是 证明得到 的(而非训练学习得到):
α
= 1.6732632423543772848170429916717λ
= 1.0507009873554804934193349852946
即:
- 不存在死区
- 存在饱和区(负无穷时, 趋于 –
αλ
) - 输入大于零时,激活输出对输入进行了放大
如何选择合适的激活函数
这个问题目前没有确定的方法,凭一些经验吧。
1)深度学习往往需要大量时间来处理大量数据,模型的收敛速度是尤为重要的。所以,总体上来讲,训练深度学习网络尽量使用zero-centered数据 (可以经过数据预处理实现) 和zero-centered输出。所以要尽量选择输出具有zero-centered特点的激活函数以加快模型的收敛速度。
2)如果使用 ReLU,那么一定要小心设置 learning rate,而且要注意不要让网络出现很多 “dead” 神经元,如果这个问题不好解决,那么可以试试 Leaky ReLU、PReLU。
3)最好不要用 sigmoid,你可以试试 tanh,不过可以预期它的效果会比不上 ReLU 和 Maxout.
最后来一张全家照
import math import matplotlib.pyplot as plt import numpy as np import matplotlib as mpl plt.rcParams['font.sans-serif']=['SimHei'] # 指定默认字体 plt.rcParams['axes.unicode_minus']=False # 用来正常显示符号 def sigmoid(x): return 1.0 / (1.0 + np.exp(-x)) def tanh(x): return (np.exp(x) - np.exp(-x)) / (np.exp(x) + np.exp(-x)) def relu(x): return np.where(x<0,0,x) def prelu(x,a): return np.where(x<0,a*x,x) def elu(x, a): return np.where(x < 0, a*(np.exp(x)-1), a*x) def selu(x): alpha = 1.6732632423543772848170429916717 scale = 1.0507009873554804934193349852946 return scale*np.where(x>=0.0, x, alpha*(np.exp(x)-1)) fig = plt.figure(figsize=(6,4)) ax = fig.add_subplot(111) x = np.linspace(-10, 10) y_sigmoid = sigmoid(x) y_tanh = tanh(x) y_relu = relu(x) y_LeakyReLU = prelu(x, 0.05) y_elu = elu(x, 0.25) y_selu = selu(x) plt.xlim(-11,11) plt.ylim(-1.1,1.1) ax.spines['top'].set_color('none') ax.spines['right'].set_color('none') ax.xaxis.set_ticks_position('bottom') ax.spines['bottom'].set_position(('data',0)) ax.set_xticks([-10,-5,0,5,10]) ax.yaxis.set_ticks_position('left') ax.spines['left'].set_position(('data',0)) ax.set_yticks([-1,-0.5,0.5,1]) plt.plot(x,y_sigmoid,label="Sigmoid",color = "blue") # 蓝色 plt.plot(2*x,y_tanh,label="tanh", color = "red") # 红色 plt.plot(2*x,y_relu,label="relu", color = "c") # 青色 plt.plot(2*x,y_LeakyReLU, '-.', label="LeakyReLU", color = "Violet") # 紫色 plt.plot(2*x,y_elu, ":", label="elu", color = "green") # 绿色 plt.plot(2*x,y_selu, "--", label="selu", color = "k") # 黑色 plt.legend() plt.show()
View Code
参考文献
SELU论文地址:【Self-Normalizing Neural Networks】.
StevenSun2014的CSDN博客:常用激活函数总结