可视化超参数作用机制(一):动画化激活函数
编者按:《纽约客》资深数据科学家Daniel Godoy以可视化的方式,简明清晰地介绍了训练神经网络的基础运动部件——超参数。
动机
深度学习完全关于超参数(hyper-parameters)!也许这有点夸张,但在训练深度神经网络时,实实在在地理解不同超参数的效应绝对会让你的生活变得更简单。
学习深度学习时,你多半会找到很多关于恰当设置网络超参数的重要性的信息:激活函数(activation function)、权重初始化(weight initializer)、优化(optimizer)、学习率(learning rate)、mini-batch大小,以及网络架构自身,比如隐藏层的数量和每层单元的数量。
所以,你学习所有的最佳实践,配置你的网络,定义超参数(或者直接使用默认值),开始训练,同时监测你的模型的损失(loss)和度量(metric)。
也许试验的进展不像你期望的那样好,因此你迭代(iterate)、调节(tweak)网络,直到你找到能够很好地解决手头特定问题的值的集合。
寻求深度理解
你是否曾经好奇底下到底发生了什么?我好奇过,结果发现一些简单的试验可能有助于阐明这一问题。
就拿本文的主题激活函数来说吧。我们知道激活函数的角色是引入非线性(non-linearity),否则,不管网络有多深,整个网络都可以直接替换为一个相应的仿射变换(affine transformation)(也就是一个线性变换(linear transformation),比如旋转、伸缩、偏斜,加上一个平移(translation))。
只有线性激活(linear activation)的神经网络等于没有激活函数,即使是像下面一样非常简单的分类问题,都会让这样的网络陷入困境。
在这个二维特征空间上,蓝线表示负面情形(y=0),绿线表示正面情形(y=1)
如果网络唯一能做的事就是进行仿射变换,那么它最终大概会得到这样一个解答:
线性边界——看起来不怎么好,是吧?
很明显,这甚至都说不上接近。一些好很多的解答的例子:
非线性是我们的救星!
这是非线性激活函数带来的三个良好解答!你能猜测下上面哪张图对应ReLU吗?
非线性边界(是这样吗?)
这些非线性边界是从何而来的呢?好吧,非线性的实际角色是大肆扭曲翻转特征空间以至于边界最终变成……线性!
好,现在事情变得越来越有趣了(至少,第一次在Chris Olah的博客文章Neural Networks, Manifolds, and Topology(神经网络、流形、拓扑学)中看到这一点时,我是这样想的。我写这篇文章也是受此启发。)所以,让我们进一步查看下!
下一步是创建尽可能简单的神经网络来处理这一特定分类问题。我们的特征空间(feature space)(x1和x2)有两个维度,而我们的网络有一个隐藏层,其中有两个单元,因此我们的隐藏层的输出(z1和z2)保留了维度的数目。
到此为止,我们始终在仿射变换的领地之内……所以,是时候看看我们的非线性激活函数了,图中用希腊字母西格玛表示,得到隐藏层的激活值(a1和a2)。
这些激活值表示我在本节第一段提到的扭曲翻转的激活空间。下图是使用sigmoid作为激活函数的预览:
扭曲翻转的二维特征空间
如同之前所承诺的,这一边界是线性的!顺便,上图对应上一节末尾3个解答示意图最左边的图形。
神经网络基础数学复习
下图展示了神经网络到隐藏层为止,应用激活函数之前所做的基本矩阵算术,也就是xW + b这样的仿射变换。
现在到了应用激活函数的时候了:
在仿射变换的结果上应用激活函数
万岁!我们从输入到达了隐藏层的激活值。
基于Keras实现网络
为了实现这一简单网络,我使用了Keras的序列模型(Sequential model)API。除了不同的激活函数外,每个模型都使用一样的超参数:
权重初始化: Glorot (He)正态分布初始化(隐藏层),随机正态分布初始化(输出层);
优化: 随机梯度下降;
学习率: 0.05;
mini-batch大小: 16;
隐藏层数目: 1;
(隐藏层)单元数目: 2.
由于这是一个二元分类任务(binary classification task),输出层使用一个单元,搭配sigmoid激活函数,损失函数使用二元交叉熵(binary cross-entropy)。
import numpy as np
from keras.models import Sequential
from keras.layers import Dense
from keras.optimizers import SGD
from keras.initializers import glorot_normal, normal
# ============ #
# 数据生成过程 #
# ============ #
# 特征x1,在-1和1之间均匀分布的1000个数据点
x1 = np.linspace(-1, 1, 1000)
# 两条曲线的特征x2
x2_blue = np.square(x1)
x2_green = np.square(x1) - .5
# 蓝线数据点坐标
blue_line = np.vstack([x1, x2_blue])
# 绿线数据点坐标
green_line = np.vstack([x1, x2_green])
# 记住,蓝线为负(0),绿线为正(1)
X = np.concatenate([blue_line, green_line], axis=1)
y = np.concatenate([np.zeros(1000), np.ones(1000)])
# 但我们不能给网络传入整齐划一的输入……
# 所以让我们用幸运数字13随机化一下!
np.random.seed(13)
shuffled = np.random.permutation(range(X.shape[1]))
X = X.transpose()[shuffled]
y = y[shuffled].reshape(-1, 1)
# ======== #
# 构建网络 #
# ======== #
# 为隐藏层选择一个激活函数:sigmoid、tanh、relu
activation = 'sigmoid'
# 隐藏层使用Glorot初始化(典型的种子值42)
glorot_initializer = glorot_normal(seed=42)
# 输出层使用正态分布初始化(同样的种子值)
normal_initializer = normal(seed=42)
# 使用随机梯度下降,学习率0.05
sgd = SGD(lr=0.05)
# 使用Keras的序列API
model = Sequential()
model.add(Dense(input_dim=2, # 输入层包含2个单元
units=2, # 隐藏层包含2个单元
kernel_initializer=glorot_initializer,
activation=activation))
# 输出层使用sigmoid激活进行二元分类
model.add(Dense(units=1,
kernel_initializer=normal_initializer,
activation='sigmoid'))
# 编译模型,使用二元交叉熵作为损失函数
model.compile(loss='binary_crossentropy',
optimizer=sgd,
metrics=['acc'])
# 拟合模型,mini-batch大小16,epoch数150
model.fit(X, y, epochs=150, batch_size=16)
运转中的激活函数
现在,到了关键部分——可视化网络训练过程中扭曲翻转的特征空间,每次使用不同的激活函数:sigmoid、tanh、ReLU。
除了显示特征空间的变化,动画同时包含:
预测概率的直方图,包括负面(蓝线)和正面(绿线)情形,误分类情形显示为红色条形(阈值0.5);
精确度和平均损失的曲线图;
损失直方图。
sigmoid
让我们从最传统的激活函数,sigmoid开始。尽管今时今日,它的使用场景主要限制在分类任务的输出层。
sigmoid激活函数及其梯度(红色曲线为梯度)
如上图所示,sigmoid激活函数将输入值“压入”区间(0, 1)(和概率值的区间一致,这正是它在输出层中用于分类任务的原因)。同时,别忘了给定层的激活值将是接下来一层的输入,由于sigmoid的区间在(0, 1),激活值将以0.5为中心,而不是以零为中心(正态分布输入通常以零为中心)。
另外,我们可以验证,其梯度(gradient)峰值为0.25(当z = 0时),而当|z|达到5时,它的值已经很接近零了。
那么,sigmoid激活函数在这一简单网络上是如何工作的呢?让我们看下动画:
https://v.qq.com/txp/iframe/player.html?vid=d1337m06w6e&width=500&height=375&auto=0
我们观察到一些东西:
epoch 15-40: 可以看到,典型的sigmoid“挤压”过程发生在横轴上;
epoch 40-65: 损失位于平原,纵轴上特征空间转换有一个“加宽”的过程;
epoch 65: 在这一点上,负面情形(蓝线)全部正确分类了,尽管相应的概率仍然分布在0.5左边;而边际的正面情形仍有错误分类;
epoch 65-100: 之前提到“加宽”过程愈演愈烈,以至于大部分特征空间都被重新覆盖,而损失稳定下降;
epoch 103: 感谢“加宽”过程,所有的正面情形都位于恰当的边界之内,尽管有些概率仅仅略微高于0.5阈值;
epoch 100-150: 现在,纵轴上也出现了一些“挤压”,损失进一步下降了些,看起来到了一个新平原,除了一些正面边际情形,网络对它所做的预测相当自信;
所以,sigmoid激活函数成功分开了两条曲线,不过损失下降得比较缓慢,训练时间有显著部分停留在高原(plateaus)上。
一个不同的激活函数能不做得更好呢?
tanh
tanh激活函数从sigmoid演进而来,和其前辈不同,其输出值的均值为零。
tanh激活函数及其梯度(红色曲线为梯度)
如上图所示,tanh激活函数“挤压”输入至区间(-1, 1)。因此,中心为零,(某种程度上)激活值已经是下一层的正态分布输入了。
至于梯度,它有一个大得多的峰值1.0(同样位于z = 0处),但它下降得更快,当|z|的值到达3时就已经接近零了。这是所谓梯度消失(vanishing gradients)问题背后的原因,这导致网络的训练进展变慢了。
现在,是使用tanh作为激活函数的对应动画:
https://v.qq.com/txp/iframe/player.html?vid=a1337iovec0&width=500&height=375&auto=0
我们观察到一些东西:
epoch 10-40: 横轴上发生了tanh“挤压”过程,不过它不如之前sigmoid那样明显,同时损失位于高原;
epoch 40-55: 损失仍然没有改善,不过在纵轴上出现了特征空间转换的“加宽”过程;
epoch 55: 在这一点上,负面情形(蓝线)全部正确分类了,尽管相应的概率仍然分布在0.5左边;而边际的正面情形仍有错误分类;
epoch 55-65: 之前提到的“加宽”过程愈演愈烈,以至于大部分特征空间都被重新覆盖,而损失突然下降;
epoch 69: 感谢“加宽”过程,所有的正面情形都位于恰当的边界之内,尽管有些概率仅仅略微高于0.5阈值;
epoch 65-90: 现在,纵轴上也出现了一些“挤压”,损失进一步下降,直到到达一个新平原,网络对所有的预测都高度自信;
epoch 90-150: 预测概率仅有微小的改善。
好,看起来要好一点……tanh激活函数以更快的速度达到了所有情形正确分类的点,而损失函数同样下降得更快(我说的是损失函数下降的时候),但它同样在高原上花了很多时间。
如果我们摆脱所有这些“挤压”,会怎么样?
ReLU
修正线性单元(Rectified Linear Units),简称ReLU,是今时今日寻常使用的激活函数。ReLU处理了它的两个前辈中常见的梯度消失问题,同时也是计算梯度最快的激活函数。
ReLU激活函数及其梯度(红色折线为梯度)
如上图所示,ReLU是一头完全不同的野兽:它并不“挤压”值至某一区间——它只是保留正值,并将所有负值转化为零。
使用ReLU的积极方面是它的梯度要么是1(正值),要么是0(负值)——再也没有梯度消失了!这一模式使网络更快收敛。
另一方面,这一表现导致所谓的“死亡神经元”问题,也就是输入持续为负的神经元激活值总是为零。
到了最后一个动画的时间了,由于ReLU激活函数中“挤压”的缺席,这个动画和之前两个动画很不一样:
https://v.qq.com/txp/iframe/player.html?vid=b1337209duo&width=500&height=375&auto=0
我们观察到一些东西:
epoch 0-10: 从一开始损失就稳定下降
epoch 10: 在这一点上,负面情形(蓝线)全部正确分类了,尽管相应的概率仍然分布在0.5左边;而边际的正面情形仍有错误分类;
epoch 10-60: 损失不断下降,直到到达一个平原,从epoch 52开始,所有情形已经正确分类了,而网络已经对所有的预测都高度自信;
epoch 60-150: 预测概率仅有微小的改善。
好吧,难怪ReLU成为今日激活函数的事实标准。损失从开始就保持稳定下降,直到接近零后才进入平原,花了大约75%的tanh训练时间就达成所有情形正确分类。
对决
动画很酷(好吧,我有偏见,这是我做的!),但不太方便比较每个不同的激活函数对特征空间的总体效果。所以,为了便于你进行比较,我制作了下图:
上为变换后的特征空间的线性边界;下为原特征空间的非线性边界
下图并排展示了精确度和损失曲线,以供比较训练速度:
结语
本文使用了尽可能简单的例子,动画刻画的模式试图让你对每个激活函数的底层机制有个大概的了解。
另外,我的初始化权重“运气不错”(也许使用42作为种子是个吉兆?!),所有三个网络都在150个epoch的训练中学习到了正确分类所有情形。事实上,训练对初始化高度敏感,但这将是后续文章的主题。
不管怎么说,我真心希望这篇文章和动画能在学习深度学习这一激动人心的话题时给你一些洞见,甚至是恍然大悟的时刻。
如果你有任何想法、评论或问题,请留言或在Twitter上联系我(dvgodoy)。
原文地址:https://towardsdatascience.com/hyper-parameters-in-action-a524bf5bf1c