首先需要知道为什么要引入激活函数:激活函数是用来加入非线性因素的,因为线性模型的表达能力不够。引入非线性激活函数,可使深层神经网络的表达能力更加强大。
在深度学习中,线性模型只需要简单的全连接层就能实现,神经网络中每一层的输入输出都是一个线性求和的过程,下一层的输出只是承接了上一层输入函数的线性变换,所以如果没有激活函数,那么无论你构造的神经网络多么复杂,有多少层,最后的输出都是输入的线性组合,纯粹的线性组合并不能够解决更为复杂的问题,哪怕有100个全连接层叠加,也只能表示出线性关系:
h ( x ) = w n T ( ⋯ ( w 2 T ( w 1 T x + b 1 ) + b 2 ) ⋯ ) + b n = w n T w n − 1 T ⋯ w 2 T w 1 T x + ( w n T w n − 1 T ⋯ w 2 T b 1 + w n T w n − 1 T ⋯ w 3 T b 2 + ⋯ + b 3 ) \begin{aligned}h(x)&=w_n^T\Bigl(\cdots\bigl(w_2^T(w_1^Tx+b_1)+b_2\bigr)\cdots\Bigr)+b_n\\&=w_n^Tw_{n-1}^T\cdots w_2^Tw_1^Tx+(w_n^Tw_{n-1}^T\cdots w_2^Tb_1+w_n^Tw_{n-1}^T\cdots w_3^Tb_2+\cdots+b_3)\end{aligned} h(x)=wnT(⋯(w2T(w1Tx+b1)+b2)⋯)+bn=wnTwn−1T⋯w2Tw1Tx+(wnTwn−1T⋯w2Tb1+wnTwn−1T⋯w3Tb2+⋯+b3)
令 W = w 1 w 2 ⋯ w n , B = w n T w n − 1 T ⋯ w 2 T b 1 + w n T w n − 1 T ⋯ w 3 T b 2 + ⋯ + b 3 W=w_1w_2\cdots w_n,B=w_n^Tw_{n-1}^T\cdots w_2^Tb_1+w_n^Tw_{n-1}^T\cdots w_3^Tb_2+\cdots+b_3 W=w1w2⋯wn,B=wnTwn−1T⋯w2Tb1+wnTwn−1T⋯w3Tb2+⋯+b3,
则 h ( x ) = W T x + B h(x)=W^Tx+B h(x)=WTx+B 仍是线性关系
线性模型所能拟合的变量关系十分受限,甚至连最简单的二次函数关系式 y = x 2 y=x^2 y=x2也是非线性的,无法用线性模型的叠加去拟合,因此引入非线性的激活函数用以拟合更复杂的非线性关系
激活函数需要满足如下条件:
-
非线性:Nonlinear,激活函数非线性时,多层神经网络可逼近所有函数
-
连续可微性:Continuously differentiable,优化器大多用梯度下降更新参数,不可微则无法求梯度
-
单调性:Monotonic,当激活函数是单调的,能保证单层网络的损失函数是凸函数
-
原点近似恒等性:Approximates identity near the origin,若激活函数有这一特性,神经网络在随机初始化较小的权重时学习更高效。若激活函数不具备这一特性,初始化权重时必须特别小心。
这一点比较抽象哈,我个人的理解是,激活函数在原点附近几乎是个恒等映射,也就是说,当激活函数的输入值接近原点时,其输出值约等于输入值
那么此时激活函数是个恒等映射有什么好处?
首先我们需要知道,神经网络中每一层(hidden layer)的输入输出都是一个线性关系(全连接层),当参数初始化较小时,输入值经过某个hidden layer与其参数相乘,得到的输出值自然也较小,此时在经过激活函数时,很有可能直接将输出判定为0,如ReLU在原点左侧,那么反向传播时传回该hidden layer的梯度值就为0,参数就不会得到更新
而当激活函数具有原点近似恒等性时,当参数初始化较小,相当于不存在激活函数(因为是恒等映射关系),那么该hidden layer的输出就等价于一个线性变换,此时反向传播就相当于直接对全连接层进行梯度下降,自然不存在梯度消失的情况
常见的激活函数如Sigmoid、Tanh、ReLU、Leaky ReLU、Softmax等,我们逐一认识并分析其优缺点
sigmoid
Sigmoid函数也叫Logistic函数,也就是机器学习中逻辑回归所使用的函数:
f ( x ) = Sigmoid ( x ) = 1 1 + e − x f(x)=\text{Sigmoid}(x)=\frac{1}{1+e^{-x}} f(x)=Sigmoid(x)=1+e−x1
图像是一个S型曲线,值域为 ( 0 , 1 ) (0,1) (0,1),将一个实数映射到 ( 0 , 1 ) (0,1) (0,1)的区间,当 x = 0 x=0 x=0时 y = 1 2 y=\frac{1}{2} y=21
- 优点:
- 函数单调连续可微,这意味着可以找到任意两个点的 sigmoid 曲线的斜率
- 出值限定在 0 到1,因此它对每个神经元的输出进行了归一化
- 可用于最终输出是个概率值的模型。因为概率的取值范围也是 0 到 1,因此 Sigmoid 函数非常合适
- 缺点:
- 非中心化,当 x = 0 x=0 x=0时 y = 1 2 y=\frac{1}{2} y=21,输出恒大于0,非零中心化的输出会使得其后一层的神经元的输入发生偏置偏移(Bias Shift),并进一步使得梯度下降的收敛速度变慢
- 计算复杂,涉及 e x e^x ex
- 梯度消失(这也是很多激活函数的通病),注意看函数图像,在 x ∈ ( − 5 , 5 ) x\in(-5,5) x∈(−5,5)时,图像斜率较大,此时的梯度也相应大,但是在 x ∈ ( − ∞ , − 5 ) ∪ ( 5 , + ∞ ) x\in(-\infty,-5)\cup (5,+\infty) x∈(−∞,−5)∪(5,+∞)时,图像斜率很小,Sigmoid函数在大部分取值范围内梯度趋近于 0,神经网络使用 Sigmoid 激活函数进行反向传播时,输出接近 0 或 1 的神经元其梯度趋近于 0。这些神经元叫作饱和神经元。因此,这些神经元的权重不会更新。此外,与此类神经元相连的神经元的权重也更新得很慢。该问题叫作梯度消失。因此,想象一下,如果一个大型神经网络包含 Sigmoid 神经元,而其中很多个都处于饱和状态,那么该网络无法执行反向传播。
Tanh
也叫双曲正切函数(hyperbolic tangent activation function):
f ( x ) = tanh ( x ) = e x − e − x e x + e − x = 2 1 + e − 2 x − 1 f(x)=\text{tanh}(x)=\frac{e^x-e^{-x}}{e^x+e^{-x}}=\frac{2}{1+e^{-2x}}-1 f(x)=tanh(x)=ex+e−xex−e−x=1+e−2x2−1
我们可以发现tanh函数可以看作放大并平移的sigmoid函数,二者的关系是:
tanh ( x ) = 2 ⋅ sigmoid ( 2 x ) − 1 \text{tanh}(x)=2\cdot \text{sigmoid}(2x)-1 tanh(x)=2⋅sigmoid(2x)−1
与sigmoid相似,tanh图像也是一个S型曲线,但是值域为 ( − 1 , 1 ) (-1,1) (−1,1):
从图像可以清楚的看出,相比于sigmoid,tanh显然是中心化的,不会发生偏置偏移(负数输入被当作负值,零输入值的映射接近零,正数输入被当作正值)
同时,tanh图像中斜率较大的范围仍局限于 ( − 2.5 , 2.5 ) (-2.5,2.5) (−2.5,2.5),其余值处的斜率均较小,所以和sigmoid类似,Tanh 函数也会有梯度消失的问题,因此在饱和时(x很大或很小时)也会「杀死」梯度。
这里要注意,既然tanh和sigmoid如此相似,那么它们到底在功能上有什么区别呢?可以互换使用吗?
在一般的二元分类问题中,tanh 函数用于隐藏层,而 sigmoid 函数用于输出层,但这并不是固定的,需要根据特定问题进行调整。
ReLU、Leaky ReLU
ReLU是Rectified Linear Unit的缩写,意为修正线性单元,是一种分段线性函数,其弥补了sigmoid函数以及tanh函数的梯度消失问题,在目前的深度神经网络中被广泛使用:
f ( x ) = ReLU ( x ) = max ( 0 , x ) = { x , x ≥ 0 0 , x < 0 f(x)=\text{ReLU}(x)=\max(0,x)=\begin{cases}x&,x\geq0\\0&,x<0\end{cases} f(x)=ReLU(x)=max(0,x)={x0,x≥0,x<0
- ReLU 函数是深度学习中较为流行的一种激活函数,相比于 sigmoid 函数和 tanh 函数,它具有如下优点:
- 当输入为正时,导数为1,一定程度上改善了梯度消失问题,加速梯度下降的收敛速度
- 计算速度快得多。ReLU 函数中只存在线性关系,因此它的计算速度比 sigmoid 和 tanh 更快
- 被认为具有生物学合理性(Biological Plausibility),比如单侧抑制、宽兴奋边界(即兴奋程度可以非常高)
- 缺点:
- Dead ReLU 问题。当输入为负时,ReLU 完全失效,在正向传播过程中,这不是问题。有些区域很敏感,有些则不敏感。但是在反向传播过程中,如果输入负数,则梯度将完全为零
- 非中心化,和 Sigmoid 激活函数类似,ReLU 函数的输出不以零为中心,ReLU 函数的输出为 0 或正数,给后一层的神经网络引入偏置偏移,会影响梯度下降的效率。
为了解决ReLU激活函数中负值的梯度消失问题,当 x < 0 x<0 x<0时,我们使用Leaky ReLU:
f ( x ) = Leaky-ReLU ( x ) = max ( 0 , x ) + γ min ( 0 , x ) = max ( x , γ x ) = { x , x ≥ 0 γ x , x < 0 f(x)=\text{Leaky-ReLU}(x)=\max(0,x)+\gamma\min(0,x)=\max(x,\gamma x)=\begin{cases}x&,x\geq0\\\gamma x&,x<0\end{cases} f(x)=Leaky-ReLU(x)=max(0,x)+γmin(0,x)=max(x,γx)={xγx,x≥0,x<0
其中 γ \gamma γ是一个小于1的值,例如 γ = 0.1 \gamma=0.1 γ=0.1(下图),保证在 x < 0 x<0 x<0时不会出现梯度消失
为什么使用Leaky ReLU会比ReLU效果要好呢?
- Leaky ReLU 通过把 x 的非常小的线性分量给予负输入来调整负值的零梯度(zero gradients)问题,当 x < 0 时,它得到 0.1 倍的正梯度。该函数一定程度上缓解了 dead ReLU 问题
- 扩大 ReLU 函数的范围,使其值域扩大到 ( − ∞ , + ∞ ) (-\infty,+\infty) (−∞,+∞)
尽管Leaky ReLU具备 ReLU 激活函数的所有特征(如计算高效、快速收敛、在正区域内不会饱和),但并不能完全证明在实际操作中Leaky ReLU 总是比 ReLU 更好。
Softmax
Softmax是用于多分类问题的激活函数,对于一个任意的 n n n维实向量,Softmax可以将其压缩为各元素值在 [ 0 , 1 ] [0,1] [0,1]之间且和为1的 n n n维实向量:
f ( x i ) = Softmax ( x i ) = e x i ∑ j e x j f(x_i)=\text{Softmax}(x_i)=\frac{e^{x_i}}{\sum_je^{x_j}} f(xi)=Softmax(xi)=∑jexjexi
相当于对输出值做归一化,它的作用是对神经网络全连接层输出进行变换,使其服从概率分布,即每个值都位于 [ 0 , 1 ] [0,1] [0,1]区间且和为1
可以将它和sigmoid的作用进行联系,区别在于sigmoid用于二分类,softmax用于多分类,最终结果的呈现形式都是以概率值输出
Softmax 与正常的 max 函数不同:max 函数仅输出最大值,但 Softmax 确保较小的值具有较小的概率,并且不会直接丢弃。我们可以认为它是 argmax 函数的概率版本或「soft」版本。
Softmax 函数的分母结合了原始输出值的所有因子,这意味着 Softmax 函数输出的各种概率彼此相关。
然而,softmax激活函数在零点不可微,负输入的梯度为零,这意味着对于该区域的激活,权重不会在反向传播期间更新,因此会产生永不激活的死亡神经元。
附上激活函数图像的画图代码:
import numpy as np
from matplotlib import pyplot as plt
x = np.linspace(-10, 10, 500)
sigmoid = 1 / (1 + np.exp(-x))
tanh = (2 / (1 + np.exp(-2 * x))) - 1
relu = [max(0, i) for i in x]
leaky_relu = [max(0.1 * i, i) for i in x]
fig = plt.figure(figsize=(17.2, 9.6), layout='tight') # 画布
ax1 = fig.add_subplot(2, 2, 1) # 画布上添加第一行第一列的第一张子图像
ax1.plot(x, sigmoid, color='#9AC8E2')
ax1.axvline(0, color='k', linestyle='--') # 画出x=0这条垂直线(虚线)
ax1.axhline(0.5, color='k', linestyle='--') # 画出y=0.5这条水平线(虚线)
ax1.set_title('Sigmoid')
ax2 = fig.add_subplot(2, 2, 2)
ax2.plot(x, tanh, color='#9AC8E2')
ax2.axvline(0, color='k', linestyle='--')
ax2.axhline(0, color='k', linestyle='--')
ax2.set_title('Tanh')
ax3 = fig.add_subplot(2, 2, 3)
ax3.plot(x, relu, color='#9AC8E2')
ax3.axvline(0, color='k', linestyle='--')
ax3.axhline(0, color='k', linestyle='--')
ax3.set_title('ReLU')
ax4 = fig.add_subplot(2, 2, 4)
ax4.plot(x, leaky_relu, color='#9AC8E2')
ax4.axvline(0, color='k', linestyle='--')
ax4.axhline(0, color='k', linestyle='--')
ax4.set_title('Leaky ReLU')
# 单独保存各个子图
for idx, ax in enumerate([ax1, ax2, ax3, ax4]):
extent = ax.get_window_extent().transformed(fig.dpi_scale_trans.inverted())
fig.savefig(f'fig{idx+1}.png', bbox_inches=extent.expanded(1.2, 1.25))
plt.show()