Neural Network in Practice

文章目录
  1. 数据预处理
  2. 参数初始化
  3. 批量标准化BN
  4. 正则化
  5. 随机失活Dropout
  6. 损失函数
    1. 分类问题
    2. 回归问题
  7. 梯度检查
  8. 模型检查
    1. 训练前
    2. 训练中
  9. 参数更新
    1. Nesterov动量更新
    2. 学习率退火
    3. 逐参数适应学习率
  10. 超参数调优
  11. 模型集成
  12. 拓展阅读

上一篇介绍了神经网络的基本知识, 这篇文章将cs231n课程中关于神经网络的 course notes2course notes3的有关知识记录下来, 介绍了构建神经网络的整个过程, 包括数据的预处理,参数初始化, 正则化和损失函数, 以及神经网络训练和搜索最优超参数等知识。

在神经元模型中, 计算模型将输入向量$x$和权重向量$w$进行內积运算然后通过非线性激活函数映射输出, 这些神经元通过层级嵌套的方式共同组成了神经网络模型, 神经网络本质上就是进行了一系列的线性映射与非线性激活函数交织的运算。

数据预处理

  1. 数据零中心化

    数据零中心化可以理解为让数据在每个特征维度上均值为$0$, 在实际操作中减去每一维的均值就可以了, 但是在图像处理中通常有两种方式, 一种是将所有像素都减去同一个均值(比如AlexNet), 另一种是在每一个通道(RGB)上进行零中心化操作, 即每个通道的数据减去该通道的均值(比如VGGNet)

  2. 数据归一化

    数据归一化可以理解为让数据在每一维上的数值范围近似相等, 通常有两种处理方式, 一种是在数据零中心化之后, 除以每个维度的标准差, 也就是对每个维度上的数据做标准化, 另一种是采用极值归一化使得每一维的数据的最大值和最小值是1和-1

    这个预处理操作只有在确信不同的输入特征有不同的数值范围(或计量单位)时才有意义, 因为数据预处理的目的就是消除量纲的影响, 但是在图像处理中像素的数值范围几乎是一致的(都在0-255之间),所以进行这个额外的预处理步骤并不是很必要。

    • 上图就是一个二维数据标准化的过程, 首先将数据云移动到了原点(数据零中心化), 然后将数据进行缩放到近似相同的范围(除以标准差), 图中红色的线指出了数据各维度的数值范围
  3. PCA(Principal Component Analysis)

    我们知道在图像领域数据一般维数是非常高的, 而数据之间的相关性又比较大, 这就可以让一部分的特征数据来近似地代表全部的特征数据, 主成分分析法(PCA)通过线性变换将原始数据变换为一组各维度线性无关的表示,可用于提取数据的主要特征分量,常用于高维数据的降维, 通常使用PCA降维过的数据训练线性分类器和神经网络会达到非常好的性能效果,同时还能节省时间和存储器空间。

    • 假设输入的数据矩阵$X$大小为[N x D], 我们来看一下PCA的过程:
    1
    2
    3
    4
    5
    6
    # 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
    U,S,V = np.linalg.svd(cov) #compute the SVD
    Xrot = np.dot(X, U) # decorrelate the data
    Xrot_reduced = np.dot(X, U[:,:100]) # Xrot_reduced becomes [N x 100]
    • 协方差矩阵中的元素$cov_{i j}$表示第$i$个维度和第$j$个维度的协方差, 矩阵的对角线上的元素是方差, 由于协方差矩阵是对称和半正定的, 我们可以对协方差矩阵进行奇异值分解(SVD)得到$U,S$

    • $U$的列是特征向量, U的列是标准正交向量的集合, 即列向量的模为1,列向量之间标准正交,所以可以把它们看做标准正交基向量

    • $S$是装有奇异值的1维数组, S中元素是特征值的平方

    • 然后将已经零中心化处理过的原始数据投影到特征基准上去除数据之间的相关性, 相当于对$X$中的数据进行旋转,旋转产生的结果就是新的特征向量。

    • np.linalg.svd的一个良好性质是在它的返回值$U$中,特征向量是按照特征值的大小排列的, 我们可以利用这个性质来对数据降维,只使用前面的小部分特征向量

    经过上面的操作,将原始的数据集的大小由[N x D]降到了[N x 100],留下了数据中包含最大特征值的100个维度。

  4. 白化

    白化操作的输入是特征基准上的数据,然后对每个维度除以其特征值来对数值范围进行归一化。该变换的几何解释是:如果数据服从多变量的高斯分布,那么经过白化后,数据的分布将会是一个均值为零,且协方差相等的矩阵。

    1
    2
    3
    # whiten the data:
    # divide by the eigenvalues (which are square roots of the singular values)
    Xwhite = Xrot / np.sqrt(S + 1e-5)
  • 在代码中加入1e-5为了防止分母为零, 但是在变换的过程中可能会夸大数据中的噪声,这是因为它将所有维度都拉伸到相同的数值范围,这些维度中也包含了方差小的维度, 而这些维度大多是噪声, 在实际操作中可以用更强的平滑来解决, 例如采用比1e-5更大的值。

    上图展示了PCA降维和白化的过程

  • 通过PCA将原始数据变换到了原始数据的协方差矩阵的基向量上, 这样就消除了数据间的相关性, 在线性代数的角度来看, 协方差矩阵变成对角阵矩阵

  • 通过白化, 每个维度都被特征值调整到一个相同数值范围以内,将数据协方差矩阵变为单位矩阵, 从几何上看,就是对数据在各个方向上拉伸压缩,使之变成服从高斯分布的数据点分布。

    使用CIFAR-10数据将这些变化可视化PCA和白化:

  • CIFAR-10训练集的大小是50000x3072,其中每张图片都可以拉伸为3072维的行向量, 然后计算[3072 x 3072]的协方差矩阵进行奇异值分解

  • 左1图 : 含49张图片的样本集

  • 左2图 : 3072个特征值向量中的前144个特征向量

  • 左3图 : 49张样本经过了PCA降维处理的图片,展示了144个特征向量, 展示原始图像用了3072维的向量,向量中的元素是图片上某个位置的像素在某个颜色通道中的亮度值, 而现在每张图片只使用了一个144维的向量,其中每个元素表示了特征向量对于组成这张图片的贡献值

  • 左4图 : 将“白化”后的数据进行显示, 其中144个维度中的方差都被压缩到了相同的数值范围内

任何预处理策略(比如数据均值)都只能在训练集数据上进行计算,算法训练完毕后再应用到验证集或者测试集上。例如,如果先计算整个数据集图像的平均值然后每张图片都减去平均值,最后将整个数据集分成训练/验证/测试集,那么这个做法是错误的。应该怎么做呢?应该先分成训练/验证/测试集,只是从训练集中求图片平均值,然后各个集(训练/验证/测试集)中的图像再减去这个平均值。 —– 智能单元

参数初始化

在真正开始训练网络之前, 有一个非常tricky的步骤就是参数初始化, 注意一定不能将所有参数设为0, 因为如果网络中的每个神经元都计算出同样的输出,然后它们就会在反向传播中计算出同样的梯度,从而进行同样的参数更新, 从代价函数的角度来说, 参数初始化又不能太大, 所以我们希望权重初始值要非常接近0又不能等于0, 并不是小数值一定会得到好的结果, 例如一个神经网络的层中的权重值很小,那么在反向传播的时候就会计算出非常小的梯度, 就会出现梯度消失问题

小随机数初始化

如果神经元刚开始的时候是随机且不相等的,那么它们将计算出不同的更新,并将自身变成整个网络的不同部分

  • 小随机数权重初始化的实现方法是 :W = 0.01 * np.random.randn(D,H)

  • 其中randn函数是基于零均值和标准差的一个高斯分布, 每个神经元的权重向量都被初始化为一个随机向量,而这些随机向量又服从一个多变量高斯分布,这样在输入空间中,所有的神经元的指向是随机的, 也就是说所有神经元是具有差异的不对称的, 那么它们就会在反向传播中计算出不同的梯度

    使用1/sqrt(n)校准方差 : 随着输入数据量的增长, 随机初始化的神经元的输出数据的分布中的方差也在增大, 除以输入数据量的平方根来调整其数值范围, 具体实现方法是 : w = np.random.randn(n) / np.sqrt(n), 这样就保证了网络中所有神经元起始时有近似同样的输出分布。实践经验证明,这样做可以提高收敛的速度, 理论证明如下 :

  • 假设权重向量为$w$和输入向量为$x$, 其线性运算结果为$s = \sum_i^n w_i x_i$, 由于我们是(0,1)正态分布的随机初始化, 所以输入和权重的平均值都是0, 即$E[x_i] = E[w_i] = 0$, 我们假设$w_i, x_i$同分布

    我们可以计算$s$的方差 :

    $$
    \begin{align}
    \text{Var}(s) &= \text{Var}(\sum_i^n w_ix_i) \\
    &= \sum_i^n \text{Var}(w_ix_i) \\
    &= \sum_i^n [E(w_i)]^2\text{Var}(x_i) + E[(x_i)]^2\text{Var}(w_i) + \text{Var}(x_i)\text{Var}(w_i) \\
    &= \sum_i^n \text{Var}(x_i)\text{Var}(w_i) \\
    &= \left( n \text{Var}(w) \right) \text{Var}(x)
    \end{align}
    $$

    由结果可以看出, 随着$n$的增大, $s$的方差越来越大, 如果想要$s$有和输入$x$一样的方差,那么在初始化的时候必须保证每个权重$w$的方差是1/n, 那么在初始化的时候, 由于$\text{Var}(aX) = a^2\text{Var}(X)$, 只要在权重前面乘上系数$a = \sqrt{1/n}$就可以了

  • Glorot等在论文Understanding the difficulty of training deep feedforward neural networks推荐$Var(w) = 2/(n_{in} + n_{out})$, 其中$n_{in} + n_{out}$是前一层和后一层的神经元个数

  • Delving Deep into Rectifiers: Surpassing Human-Level Performance on ImageNet Classification指出ReLU神经元的特殊初始化,并给出结论:网络中神经元的方差应该是2.0/n, 代码实现为w = np.random.randn(n) * np.sqrt(2.0/n), 这个形式是神经网络算法使用ReLU神经元时的当前最佳推荐。

稀疏初始化

为了处理方差问题, 可以将所有权重矩阵设为0,但是为了打破对称性导致的反向梯度相同问题,每个神经元都同下一层固定数目的神经元随机连接, 其权重数值由一个小的高斯分布生成, 一般连接数目是10个。

偏置(biases)的初始化

我们可以将偏置项初始化为0, 因为我们在初始化权重矩阵时候已经打破了对称性,

  • 对于ReLU非线性激活函数,有研究人员喜欢使用如0.01这样的小数值常量作为所有偏置的初始值,这是因为这样做能让所有的ReLU单元一开始就激活,就能保存并传播一些梯度, 但是这样做是不是总是能提高算法性能并不清楚, 所以通常还是使用0来初始化偏置参数, 推荐是使用ReLU激活函数,并且使用w = np.random.randn(n) * np.sqrt(2.0/n)来进行权重初始化, 参考文献

批量标准化BN

Batch Normalizationloffe和Szegedy最近才提出的方法, 批量归一化可以理解为在网络的每一层之前都做预处理, 让激活数据在训练开始前进行标准化处理, 网络处理数据使其服从标准正态分布, 在神经网络的结构上来看, 就是将全连接层或者是卷积层与激活函数之间添加一个BatchNorm层, 使用了批量归一化的网络对于不好的初始值有更强的鲁棒性

  • 提高了整个网络的梯度流

  • 允许设置更高的学习率

  • 减少了对于初始化的依赖

  • 在某种程度上有类似正则化的作用, 可能不再需要Dropout

BN层的理解:

  • 由于BN操作也就是标准化是一个简单可求导的操作, 所以对于整个网络依然可以利用梯度下降法进行迭代优化

注意,

  • 在训练时候我们使用的均值和方差并不是基于选定的那批数据, 而是调整过的具有经验的单一均值, 比如在训练期间模型估计的均值
  • 在模型训练完成后, 一般是没有batch的概念, 在测试阶段一般输入的是一个测试样本, 又因为网络一旦训练完毕,参数都是固定的,这个时候即使是每批训练样本进入网络,那么BN层计算的均值、标准差都是固定不变的, 我们可以用训练时的均值、标准差来处理测试数据

Batch Normalization: Accelerating Deep Network Training by Reducing Internal Covariate Shift

正则化

L1正则化

L1正则化是对于每个$w$都向目标函数增加一个$\lambda|w|$, L1正则化会让权重向量在最优化的过程中变得稀疏, 即非常接近0, 也就是说,使用L1正则化的神经元最后使用的是它们最重要的输入数据的稀疏子集, 同时对于噪音输入则几乎是不变的

L2正则化

L2正则化可能是最常用的正则化方法, 可以通过惩罚目标函数中所有参数的平方将其实现, 即对于网络中的每个权重$w_i$, 向目标函数中增加一个$\frac{1}{2}\lambda w_i^2$, 其中$\lambda$是正则化强度, 加上$\frac{1}{2}$后,该式子关于$w_i$梯度就是$\lambda w_i$而不是$2\lambda w_i$, 在线性分类中讲过, L2正则化可以直观理解为它对于大数值的权重向量进行严厉惩罚,倾向于更加分散的权重向量, 这样就可以使得网络更倾向于使用所有输入特征, 而不是严重依赖输入特征中某些小部分特征

  • 注意, 在在梯度下降和参数更新的时候,使用L2正则化意味着所有的权重都以$W = W -\lambda W$趋向于0

  • 相较L1正则化,L2正则化中的权重向量大多是分散的小数字, 如果不是特别关注某些明确的特征选择,一般说来L2正则化都会比L1正则化效果好, L1和L2正则化也可以进行组合:$\lambda_1|w|+\lambda_2w^2$, 叫做 Elastic net regularization

最大范式约束(Max norm constraints)

最大范式约束(Max norm constraints)是给每个神经元中权重向量的量级设定上限,并使用投影梯度下降来确保这一约束。在实践中,与之对应的是参数更新方式不变,然后要求神经元中的权重向量$\overrightarrow{w}$必须满足$||\overrightarrow{w}||_2<c$的条件,一般c值为3或者4, 即使在学习率设置过高的时候,网络中也不会出现数值“爆炸”,这是因为它的参数更新始终是被限制着的

偏置正则化

对于偏置参数的正则化并不常见,因为它们在矩阵乘法中和输入数据并不产生反向梯度(常数项求导为0),使用了合理数据预处理的情况下,对偏置进行正则化也很少会导致算法性能变差, 这可能是因为相较于权重参数,偏置参数实在太少

随机失活Dropout

随机失活(Dropout)是一个简单又极其有效的正则化方法, 由Srivastava在论文 Dropout: A Simple Way to Prevent Neural Networks from Overfitting 中提出的, 与L1正则化,L2正则化和最大范式约束等方法互为补充。在训练的时候,随机失活的实现方法是让神经元以超参数p的概率被激活或者被设置为$0$

  • 在训练过程中,随机失活可以被认为是对完整的神经网络抽样出一些子集,每次基于输入数据只更新子网络的参数, 但是数量巨大的子网络们并不是相互独立的,因为它们都共享参数

  • 在训练过程中做多次的Dropout训练, 每次采用不同的Dropout mask, 相当于每次产生了一个子网络模型, 在测试过程中不使用随机失活,可以理解为是对数量巨大的子网络们做了模型集成(model ensemble), 以此来计算出一个平均的预测。

这里有个小问题, 如下图所示:

假设在$p=0.5$, 那么如上图所示, 训练阶段由于采用了Dropout导致期望$E(a)$比测试阶段的期望减少一半, 为了解决这个问题, 我们需要在测试阶段将权重矩阵$W$减半, 这样使得训练阶段和测试阶段的期望就一致了, 这一点非常重要

实现代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
""" Vanilla Dropout: Not recommended implementation (see notes below) """

p = 0.5 # probability of keeping a unit active. higher = less dropout

def train_step(X):
""" X contains the data """

# forward pass for example 3-layer neural network
H1 = np.maximum(0, np.dot(W1, X) + b1)
U1 = np.random.rand(*H1.shape) < p # first dropout mask
H1 *= U1 # drop!
H2 = np.maximum(0, np.dot(W2, H1) + b2)
U2 = np.random.rand(*H2.shape) < p # second dropout mask
H2 *= U2 # drop!
out = np.dot(W3, H2) + b3

# backward pass: compute gradients... (not shown)
# perform parameter update... (not shown)

def predict(X):
# ensembled forward pass
H1 = np.maximum(0, np.dot(W1, X) + b1) * p # NOTE: scale the activations
H2 = np.maximum(0, np.dot(W2, H1) + b2) * p # NOTE: scale the activations
out = np.dot(W3, H2) + b3

上述实现不推荐是因为必须在测试时对激活数据要按照$p$进行数值范围调整, 实际更倾向使用反向随机失活(inverted dropout),它是在训练时就进行数值范围调整,从而让前向传播在测试时模型期望保持不变, 无论你决定是否使用随机失活,预测方法的代码都可以保持不变

代码实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
""" 
Inverted Dropout: Recommended implementation example.
We drop and scale at train time and don't do anything at test time.
"""

p = 0.5 # probability of keeping a unit active. higher = less dropout

def train_step(X):
# forward pass for example 3-layer neural network
H1 = np.maximum(0, np.dot(W1, X) + b1)
U1 = (np.random.rand(*H1.shape) < p) / p # first dropout mask. Notice /p!
H1 *= U1 # drop!
H2 = np.maximum(0, np.dot(W2, H1) + b2)
U2 = (np.random.rand(*H2.shape) < p) / p # second dropout mask. Notice /p!
H2 *= U2 # drop!
out = np.dot(W3, H2) + b3

# backward pass: compute gradients... (not shown)
# perform parameter update... (not shown)

def predict(X):
# ensembled forward pass
H1 = np.maximum(0, np.dot(W1, X) + b1) # no scaling necessary
H2 = np.maximum(0, np.dot(W2, H1) + b2)
out = np.dot(W3, H2) + b3
  • 通过随机失活我们可以将看作是将一个完整的网络拆分成多个子网络, 随机选择不同子网络进行前向传播, 最后对它们取平均将噪声数据边缘化

另一种方法是DropConnect, 在前向传播的时候,一系列权重被随机设置为0, 卷积神经网络同样会吸取这类方法的优点,比如随机汇合(stochastic pooling),分级汇合(fractional pooling),数据增长(data augmentation)。

Dropout的另一种解释

  • 在我们看来, 图片信息是有冗余的, 就像人类一样, 可能只需要尾巴和爪子的信息就可以判定是猫了, 所以Dropout在信息冗余的角度来说可以看作是一个去冗余的过程

损失函数

损失函数是由数据损失和正则损失组成的, 正则损失在正则化部分已经详细讨论过了, 下面我们看看数据损失部分, 用于衡量分类算法的预测结果和真实标签结果之间的差距。数据损失是对所有样本的数据损失求平均, $L=\frac{1}{N}\sum_iL_i$中,$N$是训练集数据的样本数, 我们把神经网络中输出层的激活函数简写为$f=f(x_i;W)$

分类问题

在分类问题当中, 每个样本数据具有唯一的真实标签, 最常见的损失函数就是SVM(是Weston Watkins 公式):

$$L_i = \sum_{j\neq y_i} \max(0, f_j - f_{y_i} + 1)$$

当然, 有些学者的论文中指出平方折叶损失$max(0,f_j-f_{y_i}+1)^2$的结果会更好

第二个常用的损失函数是Softmax分类器使用交叉熵损失:

$$L_i = -\log\left(\frac{e^{f_{y_i}}}{ \sum_j e^{f_j} }\right)$$

  • 当类别的数目非常庞大的时候, 就需要使用 Hierarchical Softmax , Hierarchical Softmax将标签分解成一个树, 每个标签都表示成这个树上的一个路径,这个树的每个节点处都训练一个Softmax分类器来决策左树还是右树, 树的结构对于算法的最终结果影响很大,而且一般需要具体问题具体分析

属性分类

上面两个损失公式的前提,都是假设每个样本只有一个正确的标签$y_i$, 但是如果$y_i$是一个多值向量,每个样本可能有,也可能没有某个属性,比如在Instagram上的图片,就可以看成是被一个巨大的标签集合中的某个子集打上标签,一张图片上可能有多个标签, 在这种情况下一般会为每个属性创建一个独立的二分类的分类器, 针对每个分类的二分类器会采用下面的公式:

$$L_i = \sum_j \max(0, 1 - y_{ij} f_j)$$

在上式中,

  • $y_{ij}$表示第$j$个类别的第$i$个属性的真实值, $y_{ij}$的值为1或者-1

  • 当该类别被正确预测并展示的时候,分值向量$f_j$为正,其余情况为负

  • 当一个正样本的得分小于+1,或者一个负样本得分大于-1的时候就会累计损失值

另一种方法是对每种属性训练一个独立的逻辑回归分类器

二分类的逻辑回归分类器只有两个分类(0,1),其中对于分类1的概率计算为:

$$ P(y = 1 \mid x; w, b) = \frac{1}{1 + e^{-(w^Tx +b)}} = \sigma (w^Tx + b) $$

那么类别为0的概率为

$$P(y = 0 \mid x; w, b) = 1 - P(y = 1 \mid x; w,b)$$

如果$\sigma(w^Tx+b)>0.5$(等价于$w^Tx+b>0$),那么样本就要被分类成为正样本(y=1)。

逻辑回归二分类器的损失函数为 :

$$ L_i = \sum_j y_{ij} \log(\sigma(f_j)) + (1 - y_{ij}) \log(1 - \sigma(f_j)) $$

  • 上式中标签$y_{ij}$非0即1, $\sigma(x)$是sigmoid函数, 在用梯度下降法优化模型的时候计算梯度非常简单 : $\partial{L_i} / \partial{f_j} = y_{ij} - \sigma(f_j)$

回归问题

回归问题是预测实数的值的问题,比如预测房价等, 对于这种问题, 通常是计算预测值和真实值之间的损失, 然后用L2平方范式或L1范式度量差异。

对于某个样本,L2范式计算如下:

$$ L_i = \Vert f - y_i \Vert_2^2 $$

L1范式是要将每个维度上的绝对值加起来:

$$ L_i = \Vert f - y_i \Vert_1 = \sum_j \mid f_j - (y_i)_j \mid $$

在上式中是将真实样本的每一维上与预测值的差异求和

注意, $L2$损失要比Softmax损失优化起来困难很多, 因为L2要预测一个真实的确切值, 而Softmax是一种概率意义上的预测, 还有一点就是L2对于异常值来说会导致很大的局部梯度, 所以在回归问题中, 我们依然优先考虑可否转化为分类问题去解决, 比如如果对一个产品的星级进行预测,使用5个独立的分类器来对1-5星进行打分的效果一般比使用一个回归损失要好很多。分类还有一个额外优点,就是能给出关于回归的输出的分布,而不是一个简单的毫无把握的输出值。如果确信分类不适用,那么使用L2损失吧,但是一定要谨慎

拓展 : 结构化预测(structured prediction)

结构化损失是指标签可以是任意的结构,例如图表、树或者其他复杂物体的情况。通常这种情况还会假设结构空间非常巨大,不容易进行遍历。结构化SVM背后的基本思想就是在正确的结构和得分最高的非正确结构之间画出一个边界。解决这类问题,并不是像解决一个简单无限制的最优化问题那样使用梯度下降就可以了,而是需要设计一些特殊的解决方案,这样可以有效利用对于结构空间的特殊简化假设。

梯度检查

理论上将进行梯度检查很简单,就是简单地把解析梯度和数值计算梯度进行比较, 但是在实际过程中十分复杂, 下面是一些注意事项:

  1. 使用中心化公式

    微分的定义公式是

    $$\frac{df(x)}{dx} = \frac{f(x + h) - f(x)}{h} \hspace{0.1in} \text{(bad, do not use)}$$

    注意, 上式中$h$是一个很小的值, 使用中心化公式更好, 如下:

    $$\frac{df(x)}{dx} = \frac{f(x + h) - f(x - h)}{2h} \hspace{0.1in} \text{(use instead)}$$

    • 在检查梯度的每个维度的时候,会计算两次损失函数, 所以计算资源的耗费也是两倍, 但是梯度的近似值会准确很多。这是因为对f(x+h)和f(x-h)使用泰勒展开,第一个公式的误差近似$O(h)$,第二个公式的误差近似$O(h^2)$, 这是个二阶近似, 因为$h<0$平方之后更小
  2. 使用相对误差来比较数值梯度和解析梯度

    为什么要使用相对误差来比较呢? 假设数值梯度为$f’_n$, 解析梯度为$f’_a$, 那么假如他们之间的误差绝对值$\mid f’_a - f’_n \mid$为1e-4, 如果$f’_n$和$f’_a$本身就是1左右的值, 我们可以认为梯度正确, 但是如果$f’_n$和$f’_a$本身有一个是1e-5左右的值, 那么1e-4就是很大的误差了, 所以我们必须引入$f’_n$或者$f’_a$的大小来作参考, 用相对误差去判断梯度是否正确:

    $$\frac{\mid f’_a - f’_n \mid}{\max(\mid f’_a \mid, \mid f’_n \mid)}$$

    上式考虑了差值占两个梯度绝对值的比例, 注意通常相对误差公式只包含任意一个梯度就可以,但是我更倾向取两个式子的最大值或者取两个式子的和。这样做是为了防止在其中一个式子为0时,公式分母为0的这种情况,在ReLU中是经常发生的,还必须注意两个式子都为零且通过梯度检查的情况。

    代码中的参考值设置:

    • 相对误差>1e-2:通常就意味着梯度可能出错

    • 1e-2>相对误差>1e-4 : 具体情况具体分析

    • 1e-4>相对误差 : 相对误差对于有不可导点的目标函数是OK的。但如果目标函数中没有使用tanh和softmax,那么相对误差值还是太高

    • 1e-7或者更小:梯度计算正确

    网络的深度越深,相对误差就越高。所以如果你是在对一个10层网络的输入数据做梯度检查,那么1e-2的相对误差值可能就OK了,因为误差一直在累积。相反,如果一个可微函数的相对误差值是1e-2,那么通常说明梯度实现不正确。

  3. 使用双精度进行梯度检查

  4. 保持在浮点数有效的范围

    “What Every Computer Scientist Should Know About Floating-Point Arithmetic”, 在神经网络中,在一个批量的数据上对损失函数进行归一化是很常见的。但是,如果每个数据点的梯度很小,然后又用数据点的数量去除,就使得数值更小,这反过来会导致更多的数值问题, 如果实在过小,可以使用一个常数暂时将损失函数的数值范围扩展到一个更大的范围,在这个范围中浮点数变得更加致密, 比较理想的是1.0的数量级上,即当浮点数指数为0时。

  5. 目标函数的不可导点

    不可导点是指目标函数不可导的部分,由$ReLU$等函数,或SVM损失,Maxout神经元等引入的, 当$x=-1e6$时,对ReLU函数进行梯度检查。因为$x<0$时,解析梯度在该点的梯度为0, 然而在这里数值梯度计算出一个非零的梯度值,因为可能越过了不可导点导致了一个非零的结果

  6. 使用少量数据点

    含有不可导点的损失函数(使用了ReLU或者边缘损失)的数据点越少,不可导点就越少,所以在计算有限差值近似时越过不可导点的几率就越小, 如果你的梯度检查对2-3个数据点都有效,那么基本上对整个批量数据进行梯度检查也是没问题的

  7. 谨慎设置步长h

    在实践中h并不是越小越好,因为当h特别小的时候,就可能就会遇到数值精度问题。有时候如果梯度检查无法进行,可以试试将h调到1e-4或者1e-6,然后突然梯度检查可能就恢复正常。

  8. 在特定的训练阶段进行梯度检查

    梯度检查是在参数空间中的一个随机的特定点进行的, 即使是在该点上梯度检查成功了,也不能马上确保全局上梯度的实现都是正确的, 一个随机的初始化可能不是参数空间最优代表性的点,这可能导致梯度看起来是正确实现了,实际上并不正确, 一个不正确的梯度也许依然能够产生看起来正确的模型,但是模型的泛化能力很差, 所以先让网络训练到某种程度, 等到损失函数开始下降的之后再进行梯度检查

  9. 不要让正则化损失掩盖数据损失

    正则化损失可能吞没掉数据损失,在这种情况下梯度主要正则化部分决定, 这样就掩盖了数据部分的梯度导致无法判定数据损失部分的梯度的正确性, 所以先关掉正则化对数据损失做单独检查,然后对正则化做单独检查。

  10. 记得关闭随机失活(dropout)和数据扩张(augmentation)

    在进行梯度检查时,记得关闭网络中随机失活,随机数据扩展等导致模型不确定的操作, 不然它们会在计算数值梯度的时候导致巨大误差。关闭这些操作不好的一点是无法对它们进行梯度检查, 例如随机失活的反向传播实现可能有错误, 更好的解决方案就是在计算$f(x+h)$和$f(x-h)$前强制增加一个特定的随机种子,在计算解析梯度时也同样如此。

  11. 检查少量的维度

    在实际中,梯度可以有上百万的参数,在这种情况下只能检查其中一些维度然后假设其他维度是正确的, 注意:确认在所有不同的参数中都抽取一部分来梯度检查, 有时候可能偏置项只占整个参数的一小部分, 就不能进行随机抽取, 在我看来可以采用分层抽样

模型检查

训练前

在进行费时费力的训练之前, 最好进行一些合理性的检查:

  1. 确定特定的正确损失值

    在使用小参数进行初始化时,确保得到的损失值与预想的期望一致, 让正则化强度为0, 先单独检查数据损失, 比如基于数据集CIFAR-10的Softmax分类器,一般期望它的初始损失值是2.302,这是因为初始时预计每个类别(共10类)的概率是0.1,然后Softmax损失值正确分类的负对数概率:-ln(0.1)=2.302 , 如果没看到这些损失值,那么初始化中就可能有问题

  2. 提高正则化强度时导致损失值变大

  3. 对小数据子集过拟合

    最重要的一步,在整个数据集进行训练之前,尝试在一个很小的数据集上进行训练,然后确保能到达0的损失值, 最好让正则化强度为0,不然不会得到0损失, 除非能通过这一个正常性检查,不然进行整个数据集训练是没有意义的。

训练中

在训练神经网络的时候,应该跟踪多个重要数值。这些数值输出的图表是观察训练进程的一扇窗口,是直观理解不同的超参数设置效果的工具,从而知道如何修改超参数以获得更高效的学习过程。

  1. 可视化损失函数

    在上图中可知,

    • x轴通常都是表示周期(epochs)单位,该单位衡量了在训练中每个样本数据都被观察过次数的期望, 一个周期意味着每个样本数据都被观察过了一次, 也就是这批数据对于模型的一次完整的训练。相较于迭代次数(iterations),一般更倾向跟踪周期,这是因为迭代次数与数据的批尺寸(batchsize)有关,而批尺寸的设置又可以是任意的。

    • 随着模型的训练, 过低的学习率会导致模型的改进几乎是线性的, 即模型优化的比较慢

    • 过高的学习率会使得损失值下降很快, 但是最终的损失值却相对较高; 更高的学习率就会导致损失值上升, 因为导致了参数随机震荡

    • 合适的学习率会使得损失值以一个比较恰当的速度下降, 最终的损失值也相对较低

    • 上图是一个经典的随时间变化的损失函数值,在CIFAR-10数据集上面训练了一个小的网络,这个损失函数值曲线看起来比较合理, 由于损失值的噪音很大, 推断出批数据的数量设置可能有点太小。

    • 损失值的震荡程度和批尺寸(batch size)有关,当批尺寸为1,震荡会相对较大, 当批尺寸就是整个数据集时震荡就会最小,因为每个梯度更新都是单调地优化损失函数, 也就是说在一个周期单位内, 损失函数是单调的

    • 用对数域对损失函数值作图, 学习过程一般都是采用指数型的形状, 图表就会看起来更像是能够直观理解的直线,而不是呈曲线状

  2. 训练集和验证集准确率

    在训练分类器的时候,需要跟踪的第二重要的数值是验证集和训练集的准确率, 如下图所示:

    在其他文章中我们分析过 过拟合问题 , 由上图所示, 在训练集准确率和验证集准确率中间的空隙指明了模型过拟合的程度, 红色和蓝色曲线说明了模型过拟合(训练准确率很高,测试准确率很低), 应该增大正则化强度, 更多的随机失活等方法或者收集更多的数据, 另一种情况是验证集曲线和训练集曲线重合度很高, 说明模型欠拟合, 应该增大参数数量使得模型更加复杂一些

  3. 每层的激活数据及梯度分布

    一个不正确的初始化可能让学习过程变慢,甚至彻底停止, 这个问题可以比较简单地诊断出来, 一个方法是输出网络中所有层的激活数据和梯度分布的柱状图。直观地说,就是如果看到任何奇怪的分布情况,那都不是好兆头。比如,对于使用tanh的神经元,我们应该看到激活数据的值在整个[-1,1]区间中都有分布, 如果看到神经元的输出全部是0,或者全都饱和了都是-1和1,那肯定就是有问题

  4. 在图像问题中第一层可视化

    • 图中的特征充满了噪音,这暗示了网络可能出现了问题:网络没有收敛,学习率设置不恰当,正则化惩罚的权重过低

    • 图中的特征不错,平滑,干净而且种类繁多,说明训练过程进行良好
  5. 权重更新比例

    权重中更新值的数量和全部权重的数量之间的比例, 需要对每个参数集的更新比例进行单独的计算和跟踪, 一个经验性的结论是这个比例应该在1e-3左右, 如果更低,说明学习率可能太小,如果更高,说明学习率可能太高。代码实现如下:

    1
    2
    3
    4
    5
    6
    # assume parameter vector W and its gradient vector dW
    param_scale = np.linalg.norm(W.ravel())
    update = -learning_rate*dW # simple SGD update
    update_scale = np.linalg.norm(update.ravel())
    W += update # the actual update
    print update_scale / param_scale # want ~1e-3

参数更新

  1. 基本更新方式

    前面讲过最基本的参数更新方式为x += - learning_rate * dx , learning_rate是一个超参数, 只要学习率足够低那么总能够使得模型的损失值降低, 使得模型优化

  2. 动量更新

    动量更新(Momentum update)可以看成是从物理角度上对于最优化问题得到的启发, 损失值可以理解为是山的高度$h$, 物理质点在此高度上的势能为$U = mgh$, $mg$是一个固定常量, 所以势能和高度是正相关的$U \propto h$, 降低势能也就是降低高度, 等价于降低损失函数, 用随机数字初始化参数等同于在某个位置给质点设定初始速度为0, 这样最优化过程可以看做是模拟参数向量(即质点)在地形上滚动的过程, 也就是说, 我们要根据速度的更新使得山的高度下降

    动量更新代码实现如下:

    1
    2
    3
    # Momentum update
    v = mu * v - learning_rate * dx # integrate velocity
    x += v # integrate position
  • 梯度不再直接影响原参数值, 而是通过速度v(初始化为0)来影响原参数

  • mu是一个超参数, 可以理解为速度的一个衰减系数, 这个变量有效地抑制了速度,降低了系统的动能,不然质点在山底永远不会停下来, 取值在(0.5,0.99)之间, 典型的做法是刚开始将动量mu设为0.5而在后面的多个周期(epoch)中慢慢提升到0.99。 通过交叉验证,这个参数通常设为[0.5,0.9,0.95,0.99]中的一个。

Nesterov动量更新

Nesterov动量的核心思路是,当参数向量位于某个位置x时,由动量更新公式可以发现,动量部分$mu*v$会通过$mu$超参数稍微改变参数向量, 如果要计算梯度, 那么可以将未来的近似位置$x + mu * v$看做是“向前看”了一些位置, 因此计算$x + mu * v$的梯度而不是“旧”位置$x$的梯度

  • Nesterov动量将会把我们带到绿色箭头指向的点,我们就不在原点(红色点)那里计算梯度, 而是在这个“向前看”的地方计算梯度

    实现代码如下:

    1
    2
    3
    4
    x_ahead = x + mu * v
    # evaluate dx_ahead (the gradient at x_ahead instead of at x)
    v = mu * v - learning_rate * dx_ahead
    x += v

相关参考文献:

学习率退火

如果学习率很高,系统的动能就过大,参数向量就会无规律地跳动,不能够稳定到损失函数更深更窄的部分去, 比如我们的参数优化接近于最优点的时候, 我们希望学习率越来越小, 因为这样就能更加接近最优点而不是在最优点附近跳来跳去, 参考 梯度下降法 部分, 知道什么时候开始衰减学习率是有技巧的:慢慢减小它,可能在很长时间内只能是浪费计算资源地看着它混沌地跳动,实际进展很少。但如果快速地减少它,系统可能过快地失去能量,不能到达原本可以到达的最好位置。

实现学习率退火有3种方式:

  1. 随步数衰减

    每训练几个周期就根据一些模型表现来降低学习率, 典型的值是每过5个周期就将学习率减少一半, 或者每20个周期减少到之前的0.1, 但是这些数值的设定是严重依赖具体问题和模型的选择的。在实践中有一种经验做法:使用一个固定的学习率来进行训练的同时观察验证集错误率,每当验证集错误率停止下降,就乘以一个常数(比如0.5)来降低学习率

  2. 指数衰减

    学习率的退火公式为$\alpha = \alpha_0 e^{-k t}$, 其中$\alpha_0, k$都是超参数, $t$是迭代次数或者以周期为单位

  3. 1/t衰减

    学习率的退火公式为$\alpha = \alpha_0 / (1 + k t )$, 其中$\alpha_0, k$都是超参数, $t$是迭代次数或者以周期为单位

    在实践中,随步数衰减的随机失活(dropout)更受欢迎,因为它使用的超参数(衰减系数和以周期为时间单位的步数)比k更有解释性。

逐参数适应学习率

前面讨论的所有方法都是对学习率进行全局地操作,并且对所有的参数都是一样的操作, 下面我们介绍一些自适应调整学习率的方法, 这些方法依然会引入一些超参数的设置, 但是这些方法确实有更好的表现

Adagrad

1
2
3
# Assume the gradient dx and parameter vector x
cache += dx**2
x += - learning_rate * dx / (np.sqrt(cache) + eps)
  • 变量$cache$的尺寸和梯度矩阵的尺寸是一样的,还跟踪了每个参数的梯度的平方和

  • 通过$cache$进行逐元素的归一化, 而且接收到高梯度值的权重更新的效果被减弱,而接收到低梯度值的权重的更新效果将会增强

  • $eps$一般设为1e-4到1e-8之间, 是防止出现除以0的情况出现

  • Adagrad 的一个缺点是在深度学习中单调的学习率通常过于激进且过早停止学习

    RMSprop

    RMSprop仍然是基于梯度的大小来对每个权重的学习率进行修改, 但该方法没有公开发表成论文, 每个使用这个方法的人在他们的论文中都引用自Geoff Hinton的Coursera课程的 第六课的第29页PPT, 这个方法克服了Adagrad方法的缺点

    1
    2
    cache = decay_rate * cache + (1 - decay_rate) * dx**2
    x += - learning_rate * dx / (np.sqrt(cache) + eps)
  • decay_rate是一个超参数,常用的值是[0.9,0.99,0.999]

    Adam

    Adam 像是RMSProp的动量版, 除了使用的是平滑版的梯度$m$,而不是用的原始梯度向量$dx$, 论文中推荐的参数值$eps=1e-8, beta1=0.9, beta2=0.999$, 在实际操作中推荐Adam作为默认的算法,一般而言跑起来比RMSProp要好一点, 但是也可以试试SGD+Nesterov动量

    1
    2
    3
    m = beta1*m + (1-beta1)*dx
    v = beta2*v + (1-beta2)*(dx**2)
    x += - learning_rate * m / (np.sqrt(v) + eps)
  • 完整的Adam更新算法包含了一个偏置(bias)矫正机制,因为m,v两个矩阵初始为0,在没有完全训练之前存在偏差, 需要采取一些补偿措施

    了解更多: Unit Tests for Stochastic Optimization 介绍了随机最优化的测试

  • 上图是一个是一个损失函数的等高线图, 展示了不同的优化算法的优化路径

  • 上图展示了一个马鞍状的最优化地形,其中对于不同维度它的曲率不同

  • SGD很难突破对称性,一直卡在顶部, 而类似RMSProp的方法能够发现马鞍方向上的低梯度, 因为在RMSProp更新方法中的分母项提高了在该方向的有效学习率

超参数调优

神经网络常见的超参数有:

  • 初始学习率

  • 学习率衰减方式

  • 正则化强度

    介绍一些额外的调参要点和技巧:

  1. 在程序实现过程中, 要在代码中设计从程序去记录点中有各种各样的训练统计数据, 在文件名中最好包含验证集的算法表现,这样就能方便地查找和排序

  2. 比起交叉验证最好使用一个验证集

  3. 超参数范围, 在对数尺度上进行超参数搜索, 因为学习率和正则化强度都对于训练的动态进程有乘的效果, 但是有一些参数(比如随机失活)还是在原始尺度上进行搜索

  4. 随机选择比网格化的选择更加有效, 参考文献

  5. 对于边界上的最优值要小心, 这种情况一般发生在你在一个不好的范围内搜索超参数, 一旦我们得到一个比较好的值,一定要确认你的值不是出于这个范围的边界上,不然你可能错过更好的其他搜索范围。

  6. 从粗到细地分阶段搜索, 在实践中,先进行初略范围搜索,然后根据好的结果出现的地方,缩小范围进行搜索。进行粗搜索的时候,让模型训练一个周期就可以了, 第二个阶段就是对一个更小的范围进行搜索,这时可以让模型运行5个周期,而最后一个阶段就在最终的范围内进行仔细搜索,运行很多次周期。

  7. 贝叶斯超参数最优化 是一整个研究领域,主要是研究在超参数空间中更高效的导航算法。其核心的思路是在不同超参数设置下查看算法性能时,要在探索和使用中进行合理的权衡。参考 Spearmint , SMAC , Hyperopt

模型集成

在训练的时候训练几个独立的模型,然后在测试的时候平均它们预测结果。集成的模型数量增加,算法的结果也单调提升(但提升效果越来越少)。还有模型之间的差异度越大,提升效果可能越好。进行集成有以下几种方法:

  1. 同一个模型,不同的初始化。使用交叉验证来得到最好的超参数,然后用最好的参数来训练不同初始化条件的模型, 这种方法的风险在于模型的多样性只来自于不同的初始化条件。

  2. 在交叉验证中发现最好的模型。使用交叉验证来得到最好的超参数,然后取其中最好的几个模型来进行集成。这样就提高了集成的多样性,但风险在于可能会包含不够理想的模型。

  3. 一个模型设置多个记录点。如果训练非常耗时,那就在不同的训练时间对网络留下记录点(比如每个周期结束),然后用它们来进行模型集成

  4. 在训练的时候跑参数的平均值, 在训练过程中,如果损失值相较于前一次权重出现指数下降时,就在内存中对网络的权重进行一个备份。这样你就对前几次循环中的网络状态进行了平均。你会发现这个“平滑”过的版本的权重总是能得到更少的误差。直观的理解就是目标函数是一个碗状的,你的网络在这个周围跳跃,所以对它们平均一下,就更可能跳到中心去。

Geoff Hinton on “Dark Knowledge” inspiring, where the idea is to “distill” a good ensemble back to a single model by incorporating the ensemble log likelihoods into a modified objective.

拓展阅读

文中部分内容引自智能单元