深入浅出 PyTorch 系列 — 激活函数(上)

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第11天,点击查看活动详情

之前自己对于激活函数研究并不深入,只是知道激活函数为神经网络增加了非线性。那么当问到这些激活函数之间差别以及如何选择激活函数时,自己也不知道应该如何回答,那么激活函数作为神经网络的一部分,对于不同任务或者网络结构,可能不同激活函数起的作用可能也不相同

import os
import json
import math
import numpy as np

## Imports for plotting
import matplotlib.pyplot as plt
%matplotlib inline
from IPython.display import set_matplotlib_formats
set_matplotlib_formats('svg', 'pdf') # For export
import seaborn as sns
sns.set()

## Progress bar
from tqdm.notebook import tqdm

## PyTorch
import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.utils.data as data
import torch.optim as optim
复制代码
# 用于存放数据集
DATASET_PATH = "data"
# 用于存放预训练模型
CHECKPOINT_PATH = "saved_models/tut3"

#设置随机种子
def set_seed(seed):
    #设置 numpy 和 torch 的随机种子
    np.random.seed(seed)
    torch.manual_seed(seed)
    #设置 GPU 操作随机种子,需要单独设置
    if torch.cuda.is_available():
        torch.cuda.manual_seed(seed)
        torch.cuda.manual_seed_all(seed)
set_seed(42)

复制代码

为什么使用相同的网络结构,跑出来的效果完全不同,用的学习率,迭代次数,batch size 都是一样?固定随机数种子是非常重要的。但是如果你使用的是PyTorch等框架,还要看一下框架的种子是否固定了。还有,如果你用了cuda,别忘了cuda的随机数种子。这里还需要用到torch.backends.cudnn.deterministic.

torch.backends.cudnn.deterministic = True
torch.backends.cudnn.benchmark = False

# 遍历出可以使用的设备(GPU 或者 CPU)
device = torch.device("cpu") if not torch.cuda.is_available() else torch.device("cuda:0")
print("Using device", device)
复制代码

torch.backends.cudnn.deterministic是啥?顾名思义,将这个 flag 置为True的话,每次返回的卷积算法将是确定的,即默认算法。如果配合上设置 Torch 的随机种子为固定值的话,应该可以保证每次运行网络的时候相同输入的输出是固定的,代码大致这样

定义损失函数

可以通过实现一些常见的激活函数,来深入理解激活函数。还是那句话只有自己实现动手实现一篇,才能理解真正理解。这些常用激活函数都可以在 torch.nn 包中找到。可以参见官方文档或者借助源码来查看 Pytorch 中是如何实现的。

class ActivationFunction(nn.Module):

    def __init__(self):
        super().__init__()
        self.name = self.__class__.__name__
        self.config = {"name": self.name}
复制代码

为了对比这些激活函数的差异性,这里创建了一个基类ActivationFunction,让定义激活函数类都继承于这个基类。 ActivationFunction 继承了 nn.Module 类,就很容易集成到网络里,类中包含 config 为字典类型属性用于存储参数。

接下来我们去实现两个相对比较古老的激活函数 sigmoid 和 tanh。在 PyTorch 也提供这两个激活函数实现,可以 torch.sigmoidtorch.tanh 或者 nn.Sigmoidnn.Tanh 找到这两个函数的实现的方法和类

在 Pytorch 中,函数的函数名都是以小写单词开头,而对于模块都是一个大写字母开头

激活函数一些性质

  • 非线性
  • 可微分
  • 单调性

Sigmoid

σ ( x ) = 1 1 + e x \sigma(x) = \frac{1}{1 + e^{-x}}
class Sigmoid(ActivationFunction):

    def forward(self, x):
        return 1 / (1 + torch.exp(-x))
复制代码

Sigmoid 存在问题就是只有输入在一个范围内,才有梯度

Tanh

双曲正切函数

σ ( x ) = e x e x e x + e x \sigma(x) = \frac{e^x - e^{-x}}{e^x + e^{-x}}
class Tanh(ActivationFunction):

    def forward(self, x):
        x_exp, neg_x_exp = torch.exp(x), torch.exp(-x)
        return (x_exp - neg_x_exp) / (x_exp + neg_x_exp)
复制代码

activation_001.png

其实以上 Sigmoid 和 Tanh 都存在问题,就是容易发生梯度消失和梯度爆炸,特别对于深层网络。

这 Sigmoid 和 Tanh 输入范围分别为实数范围,而输出都分别是 (0,1) 和(-1,1) 范围,这个也是激活函数的性质,需要输出在一定范围内

ReLU

其实到今天虽然接触 ReLU 激活函数很长时间,关于 ReLU 的全称还是不知道,所以特意去查了一下 Rectified Linear Unit(ReLU), 那么翻译过来就是线性整流单元函数,从字面上无法对其理解,看形状这个函数比较简单

那么我们通过一个特点来重新描述一个 ReLU

  • 线性
  • 单侧抑制: 当输入值小于 0 ,神经网元处于抑制状态
  • 宽广的激活范围:
  • 稀疏性: 整流器是个真正优点就是可以输出一个真正的 0 值

ReLU激活函数的提出 就是为了解决梯度消失问题,ReLU 的梯度只可以取两个值,分别是 0 或 1,当输入小于 0 时梯度为0;当输入大于 0 时,梯度为 1,如果梯度为 1 这保持梯度保持不变。

我们知道一个激活函数需要满足可导,虽然 ReLU 作为激活函数在 0 并不可导,不过可以为在 0 处导数为 0 这个并不是一个大问题,而且大多数情况下都工作的很好,小于 0 数值

相比于传统的神经网络激活函数,例如 sigmoid 和 tanh 等,线性整流函数有着以下几方面的优势:

  • 仿生物学原理: 相关大脑方面的研究表明生物神经元的信息编码通常是比较分散及稀疏的。通常情况下,大脑中在同一时间大概只有 1%-4% 的神经元处于活跃状态。使用线性修正以及正则化(regularization) 可以对机器神经网络中神经元的活跃度(即输出为正值)进行调试;相比之下,逻辑函数在输入为 0 时到达 1 2 \frac{1}{2} ,即已经是半饱和的稳定状态,不够符合实际生物学对模拟神经网络的期望。不过需要指出的是,一般情况下,在一个使用修正线性单元(即线性整流)的神经网络中大概有50%的神经元处于激活态。

  • 更加有效率的梯度下降以及反向传播:避免了梯度爆炸和梯度消失问题

  • 简化计算过程:没有了其他复杂激活函数中诸如指数函数的影响;同时活跃度的分散性使得神经网络整体计算成本下降

另一个允许训练更深层次网络的流行激活函数是整流线性单元(ReLU)。尽管它是一个简单的片状线性函数,但与sigmoid和tanh相比,ReLU有一个主要的好处:在很大的数值范围内有一个强大、稳定的梯度。基于这个想法,ReLU的许多变体已经被提出,我们将实现以下三种。LeakyReLU, ELU, 和Swish。LeakyReLU用一个较小的斜率取代了负数部分的零设置,以使梯度也能在输入的这一部分流动。同样地,ELU用一个指数衰减来代替负数部分。第三个,也是最近提出的激活函数是Swish,它实际上是一个大型实验的结果,目的是寻找 "最佳 "激活函数。与其他激活函数相比,Swish既平滑又非单调(即包含梯度的符号变化)。这已被证明可以防止像标准ReLU激活中的死神经元,特别是对于深度网络。如果有兴趣,可以在这篇论文[1]中找到关于Swish的好处的更详细的讨论。

让我们来实现下面的四个激活函数。

class ReLU(ActivationFunction):

    def forward(self, x):
        return x * (x > 0).float()
复制代码

LeakyReLU

class LeakyReLU(ActivationFunction):

    def __init__(self, alpha=0.1):
        super().__init__()
        self.config["alpha"] = alpha

    def forward(self, x):
        return torch.where(x > 0, x, self.config["alpha"] * x)
复制代码

ELU(Exponential Linear Unit)

x    i f    x 0 α ( e x 1 )    i f    x < 0 x \; if \; x \ge 0\\ \alpha(e^x - 1) \; if \; x < 0
  • 从公式上来看融合了 Sigmoid 和 ReLU,在坐标轴左侧具有软饱和性,而右侧无饱和性,右侧和 ReLU 保持了一致
  • 通常在 ELU 中的超参数 α \alpha 取值为 1
  • 满足分布为均值为 0
  • 激活函数是单侧饱和
class ELU(ActivationFunction):

    def forward(self, x):
        return torch.where(x > 0, x, torch.exp(x)-1)
复制代码

Swish

这个激活函数是由 google 提出的,

σ ( x ) = x 1 + e x \sigma(x) = \frac{x}{1 + e^{-x}}

和 ReLU 一样,Swish 是有下界无上届激活函数,有平滑非线性的特性

class Swish(ActivationFunction):

    def forward(self, x):
        return x * torch.sigmoid(x)
复制代码

activation_002.png

为了便于随后调用这些激活函数,我们来定义字典来将这些类进行注册 TODO: 随后用注解形式来进行注册

act_fn_by_name = {
    "sigmoid": Sigmoid,
    "tanh": Tanh,
    "relu": ReLU,
    "leakyrelu": LeakyReLU,
    "elu": ELU,
    "swish": Swish
}
复制代码

可视化激活函数

为了更加直观了解激活函数,接下里绘制出激活函数的曲线以及激活函数对应梯度的曲线形状,这样也便于我们更好了解激活函数实际作用。

def get_grads(act_fn, x):
    """
    
    计算指定位置激活函数的梯度

    Inputs:
        act_fn - 实现了 forward 方法的 "ActivationFunction" 类的对象
        x - 1维 tensor.
    Output:
        计算激活函数在 x 的梯度
    """
    #
    x = x.clone().requires_grad_() # Mark the input as tensor for which we want to store gradients
    out = act_fn(x)
    out.sum().backward() # Summing results in an equal gradient flow to each element in x
    return x.grad # Accessing the gradients of x by "x.grad"
复制代码

def vis_act_fn(act_fn, ax, x):
    # Run activation function
    y = act_fn(x)
    y_grads = get_grads(act_fn, x)
    # Push x, y and gradients back to cpu for plotting
    x, y, y_grads = x.cpu().numpy(), y.cpu().numpy(), y_grads.cpu().numpy()
    ## Plotting
    ax.plot(x, y, linewidth=2, label="ActFn")
    ax.plot(x, y_grads, linewidth=2, label="Gradient")
    ax.set_title(act_fn.name)
    ax.legend()
    ax.set_ylim(-1.5, x.max())

# 遍历所有激活函数
act_fns = [act_fn() for act_fn in act_fn_by_name.values()]
# 在 -5 到 5 均匀采用 1000 点用于可视化激活函数
x = torch.linspace(-5, 5, 1000) 
## 绘制
# 绘制 2 列,也就是一行放置 2 个激活函数图像
rows = math.ceil(len(act_fns)/2.0)
# 创建图像网格
fig, ax = plt.subplots(rows, 2, figsize=(8, rows*4))
for i, act_fn in enumerate(act_fns):
    vis_act_fn(act_fn, ax[divmod(i,2)], x)
    
# 
fig.subplots_adjust(hspace=0.3)
plt.show()
复制代码

猜你喜欢

转载自juejin.im/post/7128755558202474503