1. 常用的激活函数
1.1 Sigmoid函数
Sigmoid函数的数学表达形式为:,它接受一个实数并将其压缩到0-1的范围内,其中大的负数将会变成0,而大的正数将会变成1。在历史上,sigmoid函数被经常使用到,因为它对神经元的激活有很好的解释性:从完全未激活的状态0,到完全饱和的激活状态1。实际上,sigmoid函数正在逐渐退出舞台,因为它有以下两个主要的缺点:
-
导致梯度饱和或消失: 使用了sigmod函数的神经元有一个非常不期望的性质就是当神经元的状态处于无限接近0或1的时候,它的梯度基本为0。而在反向传播的时候,这个局部梯度将会和上游传来的梯度进行相乘。因此,如果局部梯度非常小,那么它将“杀死”梯度,使得几乎没有信号从这个神经元中传出。此外,初始化的时候也需要保持谨慎,以防止一开始的时候神经元就出现梯度消失的问题。
-
值域不是零均值化的(zero-centered): 神经网络中的神经元接受来自前面层的非零均值化的输入是不愿被看到的。这对梯度下降期间的灵活性有影响,因为如果输入的数据一直保持正的,那么在反向传播时,权值 w 的梯度将会全部为正或全部为负(取决于上游传来的梯度正负)。这就导致根据梯度更新参数时产生Z字抖动,如下图,即只能朝着两个特定的方向移动,不能朝着正确的方向移动。因此,这是一种低效率的现象,但与上面的问题相比,它的严重后果较小。
1.2 Tanh函数
Tanh函数将输入的数压缩到 [-1,1] 内,这改善了sigmoid的第二个缺点,因为它的输出是零均值的,这就使得梯度可正可负,但任然不能解决梯度消失的问题。值得注意的是,tanh其实就是被缩放了的sigmoid,因为
1.3 ReLU函数
ReLU全称为Rectified Linear Unit,整流线性单元,在最近几年非常流行。它的公式为 ,换句话说,激活与否的门槛被简单得设置成了0,ReLu函数有以下几个优劣:
- 大大加速收敛速度: 与Tanh和Sigmoid相比,ReLU在随机梯度下降时的收敛速度更快,有人认为这是由于其一半线性一半不饱和形式所致。
- 更高效: 与Sigmoid和Tanh相比,ReLU函数的计算速度更快。
- 较为脆弱: 不幸的是,使用ReLU的神经元在训练的时候比较脆弱,容易“死亡”。比如说,如果有一个很大的梯度通过ReLU神经元,而如果你的学习率设置的也非常大,那么这就会导致你的权重被更新过头了,变成一个很小的负数。由于权重为负,那么如果输入的值为正数(一般都为正数),那么输出也为负数,经过ReLU函数后就变成了0,此时,反向传播时会发现该神经元的梯度为0,那么这个神经元的权值永远不会被更新。这种情况发生,那么该神经元的梯度将永远变为0。也就是说,在训练过程中,某些神经元可能会不可逆转地死亡。比如说,如果学习率设置过高,你会发现有大约40%的神经元死亡,当然用恰当的学习率可以使得这一情况很少发生。
1.4 Leaky ReLU
Leaky ReLU函数是对解决ReLU神经元死亡的一种尝试。与ReLU对负数全变为0不同,leaky ReLU给负半边一个很小的正斜率,也就是说,表达式变成 ,其中的α是一个很小的常数。有些人报告了这种激活函数的成功,但结果并不总是一致的。此外,负半边区域中的斜率 α 也可以成为每个神经元的一个参数进行学习得来,这就是PReLU。
1.5 Maxout
还有一种激活函数的使用形式不是 ,而是在参数和输入数据进行点乘的时候使用。比如说Maxout激活函数,它实际上是个一种ReLU和Leaky ReLU的一般化版本。Maxout使用
来计算输出。可以发现,ReLU和Leaky ReLU都是这种形式的一种特殊形式(比如说ReLU就是w1=0,b1=0的时候)。因此,Maxout在拥有ReLU和Leaky ReLU优点的同时没有两者的缺点。唯一不足之处在于,它需要两倍的参数。
以上就是最常见的神经元类型及其激活功能的讨论,我们很少在同一个神经网络中混合使用不同的激活函数,虽然这样没有任何问题。
1.6 Tips: 如何挑选激活函数呢?
使用ReLU函数,并小心选择学习率,时刻关注神经网络中死亡的神经元。如果这无法优化, 尝试Leaky ReLU或者Maxout,永远不要使用sigmoid,可以尝试Tanh,但一般不会有更好的效果。
2. 数据预处理
对于一个数据矩阵 X ,假设数据的大小为 [N*D],N 表示数据的个数,D 表示数据的维度,一共有三种数据预处理的方法。
1.1 Mean subtraction 平均减法
这是一种最常见的预处理形式。它的做法是,对于数据的每个特征属性,减去这个属性的平均值。其集合解释是,沿着每个维度将数据云放在以原点为中心的周围,也就是将数据进行零中心化(zero-centered)。在numpy中,这个操作的实现方式为: X -= np.mean(X, axis = 0)
。对于图片数据,为了方便,也可以对每个像素直接减去整个像素的均值(X -= np.mean(X)
),或者也可以单独对每个颜色通道进行操作。
1.2 Normalization 归一化
归一化是指对数据维度进行归一化,使其具有大致相同的尺度。有两种常见的方法来实现这种归一化。一种是将每个维度除以其标准差:X /= np.std(X, axis = 0)
。这种预处理的另一种形式是对每个维度进行归一化处理,使其在该维度下的取值为[-1,1]。只有当你确信,不同的输入数据具有不同的尺度(或单位),但应该在模型学习时对这些尺度上的差异一视同仁时,应用这种预处理才有意义。在输入数据为图像时,像素的相对尺度已经大致相等(在0到255的范围内),所以严格来说,没有必要执行这个额外的预处理步骤。
如下图左是原始数据,中间是 Mean subtraction后的结果,数据呈现零中心化,右边是进行归一化的结果,在每个维度上,数据的尺度相同。
1.3 PCA和白化
这是另外一种形式的预处理。在这个过程中,首先如上所述对数据进行居中处理。然后,我们可以计算出协方差矩阵,告诉我们数据中的相关结构。
# Assume input data matrix X of size [N x D]
X -= np.mean(X, axis = 0) # zero-center the data (important)
cov = np.dot(X.T, X) / X.shape[0] # get the data covariance matrix
协方差矩阵 cov
的第(i,j)个元素包含输入数据的第 i 维和第 j 维之间的协方差。特别的,这个矩阵的对角线包含了某个维度的方差。此外,协方差矩阵是对称的和正半定的。我们可以计算出协方差矩阵的SVD因子。
U,S,V = np.linalg.svd(cov)
其中 U
的每列是一个特征向量,S
代表奇异值的一维向量。我们将原始(但经过零中心化处理后)数据投射到特征基中。
Xrot = np.dot(X, U) # decorrelate the data
请注意,U
的列是一组正态向量(范数为1,并且相互正交),所以它们可以被看作是基向量。因此,投影相当于对 X
中的数据进行旋转,这样新的轴就是特征向量。如果我们计算 Xrot
的协方差矩阵,我们会发现它现在是对角线的。np.linalg.svd
的一个很好的特性是,在其返回值 U
中,特征向量列是按其特征值排序的。我们可以利用这一点来降低数据的维度,只使用前几个特征向量,而放弃数据没有差异的维度。这有时也被称为PCA降维:
Xrot_reduced = np.dot(X, U[:,:100]) # Xrot_reduced becomes [N x 100]
经过这样的操作,我们将把大小为 [NxD] 的原始数据集减少到大小为 [Nx100] 的数据集,保留了包含差异最大的100个维度的数据。通常情况下,你可以通过在PCA减少的数据集上训练线性分类器或神经网络来获得非常好的性能的同时节约存储空间和训练时间。
你在实践中可能会看到的最后一种数据预处理是白化。白化操作是将投射到特征基后的数据的每个维度除以该维度对应的特征值,以实现每个维度尺度的归一化。这种预处理的几何解释是,如果输入数据是一个多变量的高斯分布,那么白化后的数据将是一个均值为零、协方差矩阵相同的高斯分布,即使PCA映射后的数据的每个维度尺度相同,代码如下:
# whiten the data: # divide by the eigenvalues (which are square roots of the singular values) Xwhite = Xrot / np.sqrt(S + 1e-5)
Warining:噪音的放大。
请注意,在上述代码中加入了1e-5(或一个小常数)以防止被零除。这种做法的一个缺点是它会大大夸大数据中的噪声,因为它把所有的维度都拉长到相同大小。这在实践中可以通过更强的平滑化来缓解(即增加1e-5成为一个更大的数字)。
如下图,从左到右分别为原数据,PCA降维之后的数据,经过白化后的数据。
我们也可以尝试用CIFAR-10数据集将这些转换可视化。CIFAR-10的训练集大小为 50,000 x 3072,其中每个图像都被拉伸成 3072 维的行向量。然后我们可以计算[3072 x 3072] 大小的协方差矩阵并计算其SVD分解。计算出的特征向量可视化后效果如下图所示:
左图: 49张图片的示例。
左起第二张图:3072个特征向量中的前144个。靠前的特征向量占了数据中的大部分差异,我们可以看到它们对应于图像中的低频率。
右起第二张: 用PCA降维后的49幅图像,使用了前144个特征向量。
也就是说,与其将每张图像表示为3072维向量,其中每个元素都是某个位置和通道上的特定像素的亮度,不如用144维的向量来表示上面的每张图像,其中向量中的每个元素衡量每个特征向量占据了图像的多少,从而构成了这个图像。为了直观地了解这144个数字中保留了哪些图像信息,我们必须将这144个数字重新转换回3072个像素值。这可以通过乘以 U.transpose()[:144,:]
来实现,然后将得到的3072个数字可视化为图像。你可以看到图像稍显模糊,反映了顶部特征向量捕捉了较低频率的事实。然而,大部分信息仍然被保留下来。
最右边的图:白化后数据的可视化,其中144个维度中的每一个维度的方差都被压缩到同等长度。这里,通过乘以 U.transpose()[:144,:]
,将144个白化后的数据逆转回图像的像素。
可以发现较低的频率(占大部分方差)现在可以忽略不计,而较高的频率(原来占相对较少的方差)则变得夸张了。
为了预处理方法介绍的完整性,我们在这提到了PCA和白化,但这些转换并不用于卷积网络。不过,对数据进行零中心化是非常重要的,对每个像素进行归一化也是很常见的。
常见的误区:关于预处理的一个重要观点是,任何预处理的统计数据(如数据平均值)必须只在训练数据上计算,然后再应用于验证/测试数据。例如,计算整个数据集的平均值并将整个数据集中的每个像素都减去它,然后将数据分割成训练集、验证集、测试集,这是一个错误的做法。相反,应当只计算训练集中数据的平均值,然后在使用到验证集和测试集时减去训练集的平均值。
3. 权重初始化
我们已经介绍了如何构建一个神经网络架构,以及如何对数据进行预处理。在我们开始训练网络之前,我们必须初始化其参数。
3.1 错误的做法
将所有权重初始化为0。虽然我们不知道训练后的网络中每个权重的最终值是什么,但是通过适当的数据归一化,我们可以合理地假设大约一半的权重是正的,一半是负的。那么,一个听起来合理的想法可能是将所有的初始权重设置为零,我们期望这将是 "最佳猜测"。事实证明这是一个错误,因为如果网络中的每个神经元权重初始化为0,那么它们将计算得到相同的输出,那么它们在反向传播过程中也都会计算得到相同的梯度,并经历完全相同的参数更新。换句话说,如果神经元的权重被初始化为相同,每个神经元将没有差异了,神经元将具有对称性。
3.2 小的随机数
因此,虽然我们希望权重非常接近于零,但正如我们上面所论证的,不能完全为零。作为一种解决方案,通常将神经元的权重初始化为小数字,并将这样做称为对称性的破坏。这个想法是,神经元在开始时都是随机和唯一的,所以它们将获得不同的更新,并将自己整合成为整个网络中的不同部分。一个权重矩阵的初始化可以实现为:W = 0.01* np.random.randn(D,H)
,其中 randn
从均值为零、单位标准差的高斯分布中获得。通过这种形式,每个神经元的权重向量都被初始化为一个从多维高斯分布中采样得到的随机向量,因此神经元在输入空间中指向随机方向。也可以使用从均匀分布中抽取的小数字,但在实践中,这对最终的性能影响相对较小。
Warning:严格说来,并不是数字越小效果就越好。例如,一个权重非常小的神经网络层在反向传播过程中会在其数据上计算出非常小的梯度(因为这个梯度与权重的值成正比)。这可能会大大减少通过网络向后流动的梯度强度,并可能成为深层网络的一个问题。
3.3 用 1/sqrt(n)
来改变方差
上述方法的一个问题是,随机初始化的神经元的输出分布有一个方差,这个方差随着输入的数量而增长。事实证明,我们可以通过将每个神经元的权重向量按其扇入 fan-in(即输入数量)的平方根缩放来将其输出方差归一。也就是说,推荐的启发式方法是将每个神经元的权重向量初始化为:w = np.random.randn(n) / sqrt(n)
,其中 n
是其输入的数量。这确保了网络中的所有神经元最初具有大致相同的输出分布,并提高了收敛率。
3.4 稀疏的初始化
另一种解决未校准方差问题的方法是将所有权重矩阵设置为零,但为了打破对称性,每个神经元都随机连接(以一个从一个小的高斯分布中取样得到的权重)到它下面固定数量的神经元。连接的神经元的典型数量可能10个左右。
3.5 初始化偏置项
将偏差初始化为零是可能的,也是常见的,因为神经元的不对称性的打破依靠的是权重中的小随机数。对于ReLU的非线性,有些人喜欢对所有的偏置使用小的常数,如0.01,因为这可以确保所有的ReLU单元在开始时就生效,从而获得并传播一些梯度。然而,并不清楚这种改进结果的一致性(事实上,一些结果似乎表明,这表现得更糟),更常见的是简单地使用0初始化偏置项。
在实践中,目前的建议是使用 ReLU作为激活函数,并使用 w = np.random.randn(n) * sqrt(2.0/n)
进行权重初始化。