欢迎您访问365答案网,请分享给你的朋友!
生活常识 学习资料

从零实现深度学习框架——神经元与常见激活函数

时间:2023-08-20
引言

本着“凡我不能创造的,我就不能理解”的思想,本系列文章会基于纯Python以及NumPy从零创建自己的深度学习框架,该框架类似PyTorch能实现自动求导。

要深入理解深度学习,从零开始创建的经验非常重要,从自己可以理解的角度出发,尽量不使用外部完备的框架前提下,实现我们想要的模型。本系列文章的宗旨就是通过这样的过程,让大家切实掌握深度学习底层实现,而不是仅做一个调包侠。
本系列文章首发于微信公众号:JavaNLP

我们已经了解了线性回归和逻辑回归,本文来学习深度学习中神经网络的基础构建——神经元,以及常见的激活函数。

神经元

神经网络和逻辑回归很像,但神经网络更强大。而神经网络是由很多个神经元(Neuron)组成的。一个神经元将实数集作为输入,然后应用某种运算,产生一个实数输出。

在神经元内部,如上图所示,神经元首先计算输入的加权和 ∑ i w i x i sum_i w_i x_i ∑i​wi​xi​,然后加上偏置项 b b b。给定输入 x 1 , ⋯   , x n x_1,cdots,x_n x1​,⋯,xn​,每个输入对应一个权重,得到加权和 z z z:
z = b + ∑ i w i x i (1) z = b + sum_i w_i x_i tag 1 z=b+i∑​wi​xi​(1)
通常使用向量的形式描述更加方便。这样 z z z由向量 w w w和标量 b b b,以及输入向量 x x x来描述:
z = w ⋅ x + b (2) z =w cdot x +b tag 2 z=w⋅x+b(2)
注意这里得到的 z z z只是一个实数(标量)。

最后,我们不是直接使用 z z z作为输出,神经元内部应用一个非线性函数 f f f到 z z z​上:
y = a = f ( z ) y = a = f(z) y=a=f(z)
这里的非线性函数称为激活函数,该函数的输出值称为激活值 a a a,我们已经见过的一种激活函数是Sigmoid函数:
y = σ ( z ) = 1 1 + e − z (3) y = sigma(z) = frac{1}{1 + e^{-z}} tag 3 y=σ(z)=1+e−z1​(3)
这里神经元的输出 y y y和激活值 a a a相同,但在神经网络中,我们通常用 y y y​表示整个网络最终的输出。把 ( 2 ) (2) (2)代入 ( 3 ) (3) (3),得到神经元的输出:
y = σ ( w ⋅ x + b ) = 1 1 + exp ⁡ ( − ( w ⋅ x + b ) ) (4) y = sigma(wcdot x + b) = frac{1}{1 + exp(-(wcdot x + b))} tag 4 y=σ(w⋅x+b)=1+exp(−(w⋅x+b))1​(4)
除了Sigmoid之外,还有很多其他比较常见的激活函数。

常见激活函数

激活函数(activation function)通过计算加权和并加上偏置来确定神经元是否应该被激活,大多数激活函数都是非线性的。所有

ReLU

最常用的激活函数是修正线性单元(Rectified linear unit,ReLU),提供了一种非常简单的非线性变换。给定元素 x x x,ReLU函数被定义为该元素与 0 0 0的最大值:
ReLU ( x ) = max ⁡ ( 0 , x ) (5) text{ReLU}(x) = max(0, x) tag 5 ReLU(x)=max(0,x)(5)
ReLU函数通过将相应的激活值设为 0 0 0,仅保留正元素并丢弃所有负元素。我们可以画出函数的图形感受一下:

from metagrad.functions import *from metagrad.utils import plotif __name__ == '__main__': x = Tensor.arange(-8.0, 8.0, 0.1, requires_grad=True) y = relu(x) plot(x.numpy(), y.numpy(), 'x', 'relu(x)', random_fname=True, figsize=(5, 2.5))

当输入为负时,ReLU函数的导数为 0 0 0,当输入为正时,ReLU函数的导数为 1 1 1。当输入为 0 0 0时,我们让其导数也为 0 0 0。

y.backward(Tensor.ones_like(x))plot(x.numpy(), x.grad.numpy(), 'x', 'grad of relu', figsize=(5, 2.5))

由于ReLU的简单性,没有包含 e x e^x ex,导致它的计算效率极高。同时它的梯度要么为 0 0 0,要么为 1 1 1,这使得优化变现得更好,减轻了困扰神经网络的梯度消息问题。

ReLU导数的函数图形如上图所示,我们可以看到,在 x < 0 x < 0 x<0的一侧,梯度值永远是 0 0 0。因此,在反向传播的过程中,可能有些神经元的权重不会被更新(因为导数为 0 0 0)。这可能会导致永不激活的死节点(神经元)。这个问题可以被ReLU的变种:Leaky ReLU解决。

Leaky ReLU

Leaky ReLU是ReLU的改进版本,主要用于解决上面跳到的死节点问题,通过给所有负值赋予一个小的正斜率来解决
Leaky ReLu ( x ) = m a x ( a x , x ) (6) text{Leaky ReLu}(x) = max(ax, x) tag 6 Leaky ReLu(x)=max(ax,x)(6)
通常这里的 a = 0.01 a=0.01 a=0.01,为了看出效果。在画图时让 a = 0.1 a=0.1 a=0.1,我们看一下它的图形:

y = leaky_relu(x)plot(x.numpy(), y.numpy(), 'x', 'leaky relu(x)', random_fname=True, figsize=(5, 2.5))

Leaky ReLU的优点与ReLU相同,同时对于负输入,其导数也变成了一个非零值(即 a a a)。

从上图可以看到,对于 x < 0 x < 0 x<0的一侧,它们也有非零的导数。不至于出现死节点,但是由于 a a a通常很小,导致在在此侧的模型参数学习缓慢。

除了Leaky ReLU外,类似地还有两种变体,分别是Parametric ReLU和Randomized Leaky ReLU。

Parametric ReLU称为参数化的ReLU,即令Leaky ReLU中的 a a a变成了一个可学习的参数。

而Randomized Leaky ReLU让 a a a取自一个连续均匀概率分布。

Exponential Linear Unit

ELU(Exponential Linear Unit)也是ReLU的一种变体,类似Leaky ReLU修改在 x < 0 x < 0 x<0侧的斜率,但在负区域不是一条直线,而是一条对数曲线。
ELU ( x ) = max ⁡ ( 0 , x ) + min ⁡ ( 0 , α ∗ ( exp ⁡ ( x ) − 1 ) ) (7) text{ELU}(x) = max(0,x) + min(0, alpha *(exp(x)-1)) tag 7 ELU(x)=max(0,x)+min(0,α∗(exp(x)−1))(7)
通常 α = 1 alpha=1 α=1,我们画出ELU的图像:

y = elu(x)plot(x.numpy(), y.numpy(), 'x', 'elu(x)', random_fname=True, figsize=(5, 2.5))

ELU在负值部分缓慢变得平滑,直到输出等于 − α -alpha −α,且 α α α是一个可调整的参数,它控制着ELU负值部分在何时饱和。但是引入了 e x e^x ex。其导数的图像为:

SoftPlus

SoftPlus函数与ReLU函数接近,但比较平滑,也是单边抑制的。
SoftPlus ( x ) = 1 β ∗ log ⁡ ( 1 + exp ⁡ ( β ∗ x ) ) (8) text{SoftPlus}(x) = frac{1}{beta} * log(1 + exp(beta * x)) tag 8 SoftPlus(x)=β1​∗log(1+exp(β∗x))(8)
其中 β beta β默认为 1 1 1,随着 β β β的增加,该函数越来越像ReLU。

我们看一下默认情况下的函数图像:

y = softplus(x, beta=10)plot(x.numpy(), y.numpy(), 'x', 'softplus(x)', random_fname=True, figsize=(5, 2.5))

当 β = 10 beta=10 β=10时,我们看一下函数图像:

y = softplus(x, beta=10)plot(x.numpy(), y.numpy(), 'x', r'softplus(x) with $beta$ = 10', random_fname=True, figsize=(5, 2.5))

其导数为:
d d x Swish ( x ) = d d x ( 1 β log ⁡ ( 1 + exp ⁡ ( β x ) ) ) = 1 β exp ⁡ ( β x ) β 1 + exp ⁡ ( β x ) = 1 1 + exp ⁡ ( − β x ) = σ ( β x ) begin{aligned} frac{d}{dx}text{Swish}(x) &= frac{d}{dx} left(frac{1}{beta} log(1 + exp(beta x)) right) \ &= frac{1}{beta} frac{exp(beta x) beta}{1 + exp(beta x)}\ &= frac{1}{1 + exp(-beta x)} \ &= sigma(beta x) end{aligned} dxd​Swish(x)​=dxd​(β1​log(1+exp(βx)))=β1​1+exp(βx)exp(βx)β​=1+exp(−βx)1​=σ(βx)​
当 β = 1 beta=1 β=1,其导数就是 σ ( x ) sigma(x) σ(x)。我们来看下其导数图像:

Swish

Swish在更深层次的模型上显示出比ReLU更好的性能。Swish的输入从负无穷到正无穷。函数定义为
Swish = x ∗ σ ( x ) = x 1 + exp ⁡ ( − x ) (9) text{Swish} = x * sigma(x) = frac{x}{1 + exp(-x)} tag 9 Swish=x∗σ(x)=1+exp(−x)x​(9)
相当于是对输入 x x x进行了门控(通过 σ sigma σ函数),我们看一下它的图像:

y = swish(x)plot(x.numpy(), y.numpy(), 'x', 'swish(x)', random_fname=True, figsize=(5, 2.5))

它的曲线都是光滑的,且处处可导。当 x x x增大时,函数值趋于无穷大;当 x x x减小时,函数值趋于常数。

Swish函数的导数为下面的公式:
d d x Swish ( x ) = d d x x σ ( x ) = σ ( x ) + σ ( x ) ′ x = σ ( x ) + σ ( x ) ( 1 − σ ( x ) ) x begin{aligned} frac{d}{dx}text{Swish}(x) &= frac{d}{dx} x sigma(x) \ &= sigma(x) + sigma(x)^prime x \ &= sigma(x) + sigma(x)(1 - sigma(x)) x end{aligned} dxd​Swish(x)​=dxd​xσ(x)=σ(x)+σ(x)′x=σ(x)+σ(x)(1−σ(x))x​
其导数的图像为:

Swish的特性:

无上边界:不像sigmoid和tanh函数,Swish没有上边界的,因为它避免了在接近零的梯度中缓慢的训练时间——像sigmoid或tanh这样的函数是有界的,因此需要小心地初始化网络,以保持在这些函数的界限内。

曲线的平滑性:平滑性在泛化和优化中起着重要的作用。与ReLU不同,Swish是一个平滑的函数,这使得它对初始化权值和学习率不那么敏感。

有下边界:这有助于增强正则化效果( x x x左侧慢慢接近于 0 0 0,一定程度过滤掉一部分信息,起到正则化的效果)。

Sigmoid

Sigmoid函数将输入压缩为区间 ( 0 , 1 ) (0,1) (0,1)上的输出。因此,Sigmoid函数通常称为挤压函数(squashing function):
sigmoid ( x ) = 1 1 + exp ⁡ ( − x ) (10) text{sigmoid}(x) = frac{1}{1 + exp(-x)} tag {10} sigmoid(x)=1+exp(−x)1​(10)
当我们想要将输出看成二分类的概率时,sigmoid此时最常用。然而,sigmoid在隐藏层中较少使用,它通常被更简单、更容易训练的ReLU所取代。

下面我们画出sigmoid函数。

y = sigmoid(x)plot(x.numpy(), y.numpy(), 'x', 'sigmoid(x)', random_fname=True, figsize=(5, 2.5))

其导数计算如下:
d d x sigmoid ( x ) = d d x 1 1 + e − x = 0 × ( 1 + e − x ) − 1 × ( 1 + e − x ) ′ ( 1 + e − x ) 2 = − ( − e − x ) ( 1 + e − x ) 2 = 1 + e − x − 1 ( 1 + e − x ) 2 = 1 1 + e − x − 1 ( 1 + e − x ) 2 = 1 1 + e − x ( 1 − 1 1 + e − x ) = σ ( x ) ( 1 − σ ( x ) ) begin{aligned} frac{d}{dx}text{sigmoid}(x) &= frac{d}{dx} frac{1}{1 + e^{-x}} \ &= frac{0 times (1+e^{-x}) - 1times (1+e^{-x})^prime}{(1 + e^{-x})^2} \ &= frac{- (-e^{-x})}{(1+e^{-x})^2} \ &= frac{1 + e^{-x} - 1}{(1+e^{-x})^2} \ &= frac{1}{1 + e^{-x}} - frac{1}{(1 + e^{-x})^2} \ &= frac{1}{1 + e^{-x}} left( 1 - frac{1}{1 + e^{-x}} right) \ &= sigma(x)(1 - sigma(x)) end{aligned} dxd​sigmoid(x)​=dxd​1+e−x1​=(1+e−x)20×(1+e−x)−1×(1+e−x)′​=(1+e−x)2−(−e−x)​=(1+e−x)21+e−x−1​=1+e−x1​−(1+e−x)21​=1+e−x1​(1−1+e−x1​)=σ(x)(1−σ(x))​
其导数的图像为:

在深层网络中,Sigmoid存在三个问题:

饱和的神经元会让梯度消失,即在较大的正数或负数作为输入的时候,梯度就会变成零,使得神经元基本不能更新。其输出不是以 0 0 0为中心的,而是 0.5 0.5 0.5。我们知道 d L d w = d L d z x frac{dL}{dw} = frac{dL}{dz}x dwdL​=dzdL​x,在深层网络中,由于上一层使用的是Sigmoid激活函数,导致该层的输入都是正数。即该层 w w w的梯度取决于 d L d z frac{dL}{dz} dzdL​,要么都是正的,要么都是负的,出现了zig zag问题。如下图所示,假设最佳更新路线是蓝线所示,由于zig zag问题,使其优化变成缓慢。指数计算耗时 Tanh

tanh是sigmoid函数的变种,它的函数值变成范围从 − 1 -1 −1到 + 1 +1 +1,即变成了以 0 0 0为中心的。
tanh ⁡ ( x ) = e x − e − x e x + e − x (11) tanh(x) = frac{e^x - e^{-x}}{e^x + e^{-x}} tag{11} tanh(x)=ex+e−xex−e−x​(11)
我们来画出该函数的图像:

y = tanh(x)plot(x.numpy(), y.numpy(), 'x', 'tanh(x)', random_fname=True, figsize=(5, 2.5))

tanh函数具有平滑可微性,和将离群值映射到均值的良好性质。

为什么说tanh函数是sigmoid函数的变种呢?我们来推导一下:
tanh ⁡ ( x ) = e x − e − x e x + e − x = e x + e − x − e − x − e − x e x + e − x = 1 + − 2 e − x e x + e − x = 1 − 2 e 2 x + 1 = 1 − 2 σ ( − 2 x ) = 1 − 2 ( 1 − σ ( 2 x ) ) = 1 − 2 + 2 σ ( 2 x ) = 2 σ ( 2 x ) − 1 begin{aligned} tanh(x) &= frac{e^x - e^{-x}}{e^x + e^{-x}} \ &= frac{e^x + e^{-x} - e^{-x} - e^{-x} }{e^x + e^{-x}} \ &= 1 + frac{-2e^{-x}}{e^x + e^{-x}} \ &= 1 - frac{2}{e^{2x}+ 1}\ &= 1 - 2sigma(-2x) \ &= 1 - 2(1 - sigma(2x)) \ &= 1 - 2 + 2sigma(2x) \ &= 2sigma(2x) - 1 end{aligned} tanh(x)​=ex+e−xex−e−x​=ex+e−xex+e−x−e−x−e−x​=1+ex+e−x−2e−x​=1−e2x+12​=1−2σ(−2x)=1−2(1−σ(2x))=1−2+2σ(2x)=2σ(2x)−1​

因此,我们可以看到tanh只是sigmoid函数的缩放版本。

tanh函数的导数是:
d d x tanh ⁡ ( x ) = d d x e x − e − x e x + e − x = ( e x − e − x ) ′ ( e x + e − x ) − ( e x − e − x ) ( e x + e − x ) ′ ( e x + e − x ) 2 = ( e x + e − x ) 2 − ( e x − e − x ) 2 ( e x + e − x ) 2 = 1 − ( e x − e − x e x + e − x ) 2 = 1 − tanh ⁡ 2 ( x ) begin{aligned} frac{d}{dx}tanh(x) &= frac{d}{dx} frac{e^x - e^{-x}}{e^x + e^{-x}} \ &= frac{(e^x - e^{-x})^prime(e^x + e^{-x}) -(e^x - e^{-x})(e^x + e^{-x})^prime}{(e^x + e^{-x})^2} \ &= frac{(e^x + e^{-x})^2 - (e^x - e^{-x})^2}{(e^x + e^{-x})^2} \ &= 1 - left ( frac{e^x - e^{-x}}{e^x + e^{-x}} right)^2 \ &= 1 - tanh^2(x) end{aligned} dxd​tanh(x)​=dxd​ex+e−xex−e−x​=(ex+e−x)2(ex−e−x)′(ex+e−x)−(ex−e−x)(ex+e−x)′​=(ex+e−x)2(ex+e−x)2−(ex−e−x)2​=1−(ex+e−xex−e−x​)2=1−tanh2(x)​
其中 d d x e x = e x frac{d}{dx} e^x = e^x dxd​ex=ex; d d x e − x = − e − x frac{d}{dx}e^{-x} = - e^{-x} dxd​e−x=−e−x

其导数图像如下所示:

可以看到,当输入接近于 0 0 0时,tanh函数的导数接近于最大值 1 1 1。而输入在任一方向上越远离 0 0 0点,导数越接近 0 0 0。

Tanh的缺点类似Sigmoid,不过它是以 0 0 0为中心的,避免了zig zag问题。

如何选择激活函数

我们看了这么多激活函数,到底要如何选择呢?

在深层网络中,首先要尝试ReLU,它具有速度快的优点,如果效果欠佳;

那么尝试Leaky ReLU;

或者tanh这种以零为中心的;

另外,在RNN中常用sigmoid或tanh,作为门控或概率值。

完整代码

完整代码笔者上传到了程序员最大交友网站上去了,地址:  https://github.com/nlp-greyfoss/metagrad

References DIVE INTO DEEP LEARNINGDelving Deep into Rectifiers: Surpassing Human-Level Performance on ImageNet Classification

Copyright © 2016-2020 www.365daan.com All Rights Reserved. 365答案网 版权所有 备案号:

部分内容来自互联网,版权归原作者所有,如有冒犯请联系我们,我们将在三个工作时内妥善处理。