众所周知神经网络单元是由线性单元和非线性单元组成的, 而非线性单元就是我们今天要介绍的 -- 激活函数, 不同的激活函数得出的结果也是不同的. 他们也各有各的优缺点, 虽然激活函数有自己的发展历史, 不断的优化, 但是如何在众多激活函数中做出选择依然要看我们所实现深度学习实验的效果.
这篇博客会分为上下两篇, 上篇介绍一些常用的激活函数(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
参考文献
hn_ma 的 CSDN 博客
SELU 论文地址:[Self-Normalizing Neural Networks] .
StevenSun2014 的 CSDN 博客: 常用激活函数总结
来源: https://www.cnblogs.com/LXP-Never/p/9771869.html