跳转至

5 Training Neural Networks

文本统计:约 6817 个字 • 62 行代码

关于训练神经网络,这是一个系统的工程,这篇笔记简单地介绍一些方法

One time setup

  • activation functions
  • preprocessing
  • weight initialization
  • regularization
  • gradient checking

Training dynamics

  • babysitting the learning process

  • parameter updates

  • hyperparameter optimization

Evaluation

  • model ensembles

Activation Functions

回忆一下激活方程

以及一些常见的激活方程

针对于 Sigmoid 激活方程, Sigmoid函数在过去非常流行,因为它可以很好地模拟生物神经元的“饱和”行为,即当输入信号足够强时,神经元的“激发率”趋于稳定。但是整个函数也有很多问题

  • 饱和神经元“杀死”梯度(Saturated neurons "kill" the gradients)

当输入值非常大或非常小时,Sigmoid函数的输出会接近于1或0,此时函数的导数(即梯度)几乎为零。这意味着在这些区域,函数对输入的变化变得不敏感。如果使用Sigmoid作为激活函数,那么在反向传播过程中,梯度会经过多层传递。由于每层的梯度都很小,最终传回前面层的梯度可能会变得极其微小,甚至接近于零。这种现象被称为“梯度消失”,它会导致模型难以训练,尤其是在深层网络中。

  • Sigmoid输出非零中心化

由于 Sigmoid 的输出全是正的,也就是除了第一层以外,后面所有层的 \(x\) 都是正的,而每个节点 \(W\) 的局部梯度是 \(x\), 也就是说所有分量的梯度相比于最后一个节点的梯度的符号都是同时变化的(可能比较难理解),实际上就是每次梯度下降的方向只有两个大方向,这样子就会导致在梯度下降的过程中会走折线

  • exp() 的计算有一些复杂

针对上述问题,我们也可以采用 \(\tanh(x)\) 作为激活函数,但是同样的也会在值很大或者值很小的时候饱和而杀死梯度。

而对于 \(\text{ReLU}\) 这个激活函数,它在正的部分并不会出现饱和的情况,同时计算也比 \(\text{Sigmoid}\)\(\tanh(x)\) 要快。但是仍有 Not Zero-centered output 的问题。当 \(x<0\) 的时候,ReLU 函数的梯度为 0。这意味着如果神经元的输入始终小于 0,那么该神经元将不会对后续层产生任何贡献,并且在反向传播过程中也不会更新权重。这种现象被称为 “Dead ReLU”

  • 数据云(Data Cloud):表示输入数据的分布情况。
  • 绿色箭头(active ReLU):表示一个活跃的ReLU神经元,其权重向量指向数据云中的某个区域。当输入数据落在这个区域内时,该ReLU神经元会被激活,即其输出大于0。
  • 红色箭头(dead ReLU):表示一个死亡的ReLU神经元,其权重向量指向数据云之外的区域。这意味着对于所有输入数据,该ReLU神经元的输入始终小于0,因此其输出恒为0。

出现 "Dead ReLU" 的原因

  • 如果ReLU神经元的初始权重设置不合理,可能会导致其输入始终小于0,从而一开始就进入非激活状态。

  • 过高的学习率可能导致权重更新过大,使得某些ReLU神经元的输入迅速变为负值,并且难以恢复到正值区域。

出现 "Dead ReLU" 的影响

  • 模型容量下降:死亡ReLU神经元无法参与模型的学习过程,相当于减少了模型的有效参数数量,降低了模型的表达能力和拟合能力。
  • 训练效率降低:由于部分神经元不参与计算和更新,模型的训练速度和收敛速度可能会受到影响。

针对上述的问题,我们可以对 \(\text{ReLU}\) 进行改进,使用 \(\text{Leaky ReLU}\) 的激活函数,这里 \(\alpha\) 也可以作为超参数进行调整

\[ f(x) = \max(\alpha x,x) \]

或者采用 \(\text{ELU}\) 作为激活函数

\[ f(x) = \begin{cases} x & \text{if } x > 0 \\ \alpha (\exp(x) - 1) & \text{if } x \leq 0 \end{cases} \]

比较 Leaky ReLU 和 ELU

负值区域的行为

  • Leaky ReLU:当输入 \(x<0\) 时,Leaky ReLU允许一个非常小的正斜率(通常为 \(α\),例如0.01),即输出为 \(f(x)=αx\)。这意味着即使对于负输入,Leaky ReLU也会产生非零输出,从而避免了ReLU中可能出现的“死亡神经元”问题。然而,这种固定的小斜率可能导致模型对噪声过于敏感,因为它始终以相同的比例缩放所有的负输入。
  • ELU:与Leaky ReLU不同的是,ELU在 \(x<0\) 时采用指数形式\(f(x)=α(exp⁡(x)−1)\)。随着xx趋向于负无穷大,ELU的输出趋于一个固定的负值 \(−α\)。这种特性使得ELU能够在面对较大的负输入时保持一定的响应能力,而不是像Leaky ReLU那样按固定比例缩小所有负输入。因此,在存在大量噪声的情况下,ELU能更有效地抑制过大的负输入的影响,减少噪声对模型的影响。

更接近零均值的激活分布

  • ELU倾向于生成比Leaky ReLU更接近零均值的激活值。这对于对抗输入中的噪声尤为重要,因为接近零均值的激活可以帮助梯度下降算法更有效地找到全局最小点,并减少梯度消失的问题。相比之下,Leaky ReLU虽然改进了ReLU在负区间的响应,但其输出仍偏向正值,可能造成数据分布不对称,影响训练效率和模型性能。

还有一种神经元被称为 Maxout "Neuron",可以被看做 ReLU 和 Leaky ReLU 的泛化版本

\[ \max(w_1^Tx+b_1, w_2^Tx+b_2) \]

Linear Regime! Does not saturate! Does not die!

在实际应用中,一般情况下多数使用 ReLU,但是要注意学习率的设置。可以尝试使用 Leaky ReLU / Maxout / ELU,也可以使用 \(\tanh\) 但是不要期待结果很好,不要使用 \(\text{Sigmoid}\)

Data Preprocessing

数据处理的方法有 zero-centered data, normalized data, decorrelated data, whitened data 等

零均值化的作用和意义在于:

  • 改善梯度方向:通过零均值化,可以避免梯度方向受限于某个特定区域,使得权重更新的方向更加灵活和多样化。
  • 加速收敛速度:优化路径变得更加直接,减少了不必要的“之”字形路径,从而加速模型的收敛速度。
  • 提高训练效率:通过改善梯度方向和加速收敛速度,可以显著提高模型的训练效率和最终性能。

关于数据预处理,一个更好的理解

未归一化前,即使权重有微小的变化,也可能导致分类边界发生显著偏移,从而影响分类结果。很难找到最优解

在实践过程中,通常只进行中心化操作,关于中心化的方法也有几种

  • AlexNet 采用的方法是计算整个训练集中所有图像的平均值,得到一个与单张图像相同尺寸的平均图像([32, 32, 3] 数组),然后从每张图像中减去这个平均图像。
  • VGGNet 采用的方法分别计算每个通道(红、绿、蓝)的平均值,得到三个数值,然后从每张图像的相应通道中减去对应的平均值。

Weight Initialization

权重的初始化的好坏会影响神经网络能否正确训练以及训练的速度。


假设我们将我们的权重都初始化为0会有什么问题?它会导致神经网络中所有神经元在每层产生相同的输出和梯度,无法打破对称性,致使模型无法有效学习和区分特征。

所以我们需要引入随机性,采用 gaussian with zero mean and 1e-2 standard deviation

W = 0.01 * np.random.randn(D,H)

上述初始化方法对于小的神经网络来说是可以的,但是对于更深的神经网络来说就不一定有效了

10-layer net with 500 neurons on each layer, using tanh non-linearities,采用上述的初始化方式

上左图为每层均值,上右图是每层的标准差,下图是直方图

从图中可以看出,随着网络深度的增加(即层数的增加),每一层的输出标准差逐渐减小,特别是在第3层之后,标准差迅速下降到接近于0。这意味着信号在通过多层传递时被严重削弱,最终导致深层神经元接收到的输入几乎为常数,无法有效学习

为什么每一层的标准差不断缩小

在每一层中,神经元的输入是前一层输出与权重的线性组合:

\[ z = w_1 x_1 + w_2 x_2 + \ldots + w_n x_n \]

对于线性组合,其方差可以通过以下公式计算:

\[ \text{Var}(z) = \sum_{i=1}^{n} \text{Var}(w_i x_i) \]

由于 \(w_i\)\(x_i\) 是独立的,可以进一步分解为:

\[ \text{Var}(w_i x_i) = \text{Var}(w_i) \cdot \text{Var}(x_i) \]

因此,总的方差为:

\[ \text{Var}(z) = \sum_{i=1}^{n} \text{Var}(w_i) \cdot \text{Var}(x_i) \]

如果所有权重 \(w_i\) 具有相同的方差 \(\sigma_w^2\),且所有输入 \(x_i\) 具有相同的方差 \(\sigma_x^2\),则:

\[ \text{Var}(z) = n \cdot \sigma_w^2 \cdot \sigma_x^2 \]

相反地,如果我们初始化的时候如果取的 weight_scale 特别大的话,那么几乎每一层的神经元都处于饱和状态,导致梯度消失的情况

那么一个好的初始化应该是怎么样的呢?下图采用 Xavier initialization 得到的结果,其中激活函数是线性的

但是当使用非线性激活函数 (ReLU) 的时候,Xavier 初始化仍然会出现梯度消失的问题。

W = np.random.randn(fan_in,fan_out)/np.sqrt(fan_in)

可以通过 He 初始化的方法解决这个问题

W = np.random.randn(fan_in,fan_out)/np.sqrt(fan_in/2)

Batch Normalization

输入与输出 - 输入: \(x\) 在一个mini-batch中的值: \(\mathcal{B} = \{x_1...x_m\}\) - 需要学习的参数: \(\gamma, \beta\) - 输出: \(\{y_i = BN_{\gamma,\beta}(x_i)\}\)

Batch normalization 的步骤

注意下面这些 \(x_i\) 都是向量,进行的都是向量运算

  • 计算mini-batch均值
\[ \mu_{\mathcal{B}} \leftarrow \frac{1}{m} \sum_{i=1}^{m} x_i \quad // 小批次均值 \]
  • 计算mini-batch方差
\[ \sigma_{\mathcal{B}}^2 \leftarrow \frac{1}{m} \sum_{i=1}^{m} (x_i - \mu_{\mathcal{B}})^2 \quad // 小批次方差 \]
  • 标准化
\[ \hat{x}_i \leftarrow \frac{x_i - \mu_{\mathcal{B}}}{\sqrt{\sigma_{\mathcal{B}}^2 + \epsilon}} \quad // 标准化 \]
  • 缩放和平移
\[ y_i \leftarrow \gamma \hat{x}_i + \beta \equiv BN_{\gamma,\beta}(x_i) \quad // 缩放和平移 \]

进行 Batch normalization 的好处有哪些?

  1. Improves gradient flow through the network (改善网络中的梯度流)

BN解决了“内部协变量偏移”(Internal Covariate Shift)问题,并稳定了激活值的分布。

内部协变量偏移问题

在训练过程中,当网络前层的参数更新时,后一层的输入数据分布(均值和方差)也会随之改变。这种不断变化的输入分布迫使后层需要不断地去适应新的分布,这会减慢学习速度,甚至可能导致梯度变得非常小或非常大(即梯度消失或爆炸)。

BN通过将输入限制在激活函数的线性区域附近,确保了梯度不会因为饱和而消失。即使使用ReLU,过大的输入也可能导致梯度爆炸,BN也能起到抑制作用。由于输入分布稳定,反向传播时计算出的梯度也更加稳定和一致。这使得梯度能够更有效地从输出层“流动”回输入层,避免了深层网络中常见的梯度消失问题。

  1. Allows higher learning rates (允许使用更高的学习率)

在没有BN的网络中,如果学习率设置得太高,一次大的参数更新可能会导致某一层的输出分布发生剧烈变化(例如,均值大幅偏移或方差急剧增大)。这会使得下一层的神经元大量进入激活函数的饱和区,导致梯度消失。使用 BN,即使前一层的参数因为高学习率发生了剧烈变化,BN层也能迅速将这些变化的输出重新归一化到一个可控的范围内。这相当于为网络提供了一个“安全网”或“缓冲区”。

  1. Reduces the strong dependence on initialization (减少了对初始化的强烈依赖)

在深度网络中,糟糕的权重初始化(比如权重过大或过小)会导致信号在前向传播时迅速放大或缩小。例如,如果权重初始值太小,信号会逐层衰减,最终在深层几乎为零(梯度消失);如果太大,信号会逐层放大,最终导致梯度爆炸。这使得模型对初始化非常敏感,需要精心设计初始化方法(如Xavier、He初始化)。

BN在每一层都对输入进行归一化。无论初始权重导致第一层输出是大是小,BN都会将其调整到一个标准范围。这个标准化后的输出再作为下一层的输入,下一层的BN又会进行同样的操作。

  1. Acts as a form of regularization and slightly reduces the need for dropout (起到正则化作用,并略微减少对Dropout的需求)【这个部分后面会讲】

在测试阶段,BN层的行为与训练阶段不同

  • 测试时不再基于当前batch计算均值和方差,而是使用在整个训练过程中累积的固定经验均值和方差。
  • 这些经验均值和方差可以通过运行平均(running averages)的方式在训练过程中逐步估计出来。

  • 例如,在训练过程中,可以使用以下公式更新运行均值和方差:

\[ \text{running\_mean} \leftarrow \text{momentum} \times \text{running\_mean} + (1 - \text{momentum}) \times \mu_{\mathcal{B}} \]
\[ \text{running\_var} \leftarrow \text{momentum} \times \text{running\_var} + (1 - \text{momentum}) \times \sigma_{\mathcal{B}}^2 \]

其中 momentum 是一个小于1的常数(如0.9或0.99),用于控制新旧统计量的权重。

Babysitting the Learning Process

下面我们将展示一个学习的过程

首先我们可以先预处理一下数据

然后选择合适的架构

初始化相应的权重后进行 loss 的检查,由于权重是随机生成的,对于 CIFAR-10 来说一共有10个分类,采取 softmax 得到的结果应该是 \(\log10 \approx 2.3\)

加入 regularization 项,发现 loss 的结果提升了,基本没有问题

Make sure that you can overfit very small portion of the training data 选取小样本,看看模型能否做到过拟合

为什么需要过拟合一小部分数据

  • 在深度学习中,如果一个模型无法过拟合一小部分训练数据,那么它很可能存在一些问题,比如网络结构设计不合理、参数初始化不当、优化算法选择不合适等。
  • 如果模型能够在非常少的数据上达到100%的准确率(即完全过拟合),说明模型有足够的表达能力去学习这些数据中的模式。接下来,可以通过调整超参数、增加数据量等方式来提高模型在完整数据集上的泛化能力。
model = init_two_layer_model(32*32*3, 50, 10) # input size, hidden size, number of classes
trainer = ClassifierTrainer()
X_tiny = X_train[:20] # take 20 examples
y_tiny = y_train[:20]
best_model, stats = trainer.train(X_tiny, y_tiny, X_tiny, y_tiny,
                                 model, two_layer_net,
                                 num_epochs=200, reg=0.0,
                                 update='sgd', learning_rate_decay=1,
                                 sample_batches=False,
                                 learning_rate=1e-3, verbose=True)

上述代码从 CIFAR-10 中选取了20个样本,然后关闭了正则化项,使用简单的梯度下降法(SGD)

得到的结果很好,train accuracy 达到 1.00

然后我们就要选择合适的学习率,学习率过低的话就会导致 loss 基本不发生变化;学习率过高的话,会使得 loss 发生爆炸或者出现震荡现象。选择合适的学习率对于模型训练是很重要的,越深的神经网络对其更敏感。

Hyperparameter Optimization

选择超参数的时候,可以采用 Cross-validation strategy, 先粗略地找出参数的基本范围,然后再用更长的时间去细致地寻找更好的参数。

设置范围的时候可以采用在 log 域下随机采样的方式

The best cross-validation result is worrying. Why?

过拟合风险:即使验证准确率达到53%,这并不一定意味着模型在实际应用中会有良好的表现。

参数范围调整后的结果变化不大:在进行更精细的搜索时(即调整了正则化强度reg和学习率lr的范围),最佳验证准确率仍然停留在53%左右。这表明即使在更细致的参数搜索范围内,模型性能没有显著提升,可能暗示当前模型结构或超参数设置存在局限性。

对于多个超参数的设置,有两种搜索方式:random search 和 grid search

关于两者的区别和适用条件,在这篇文章中详细阐述了一下在机器学习中,Grid Search与Random Search的选择:哪个更适合大规模模型训练? - WEBKT


在设置超参数的时候,也可以通过 loss 函数随着时间的图像来判断当前参数设置的合理性

  • 学习率非常高的时候,每次更新的幅度过大,导致曲线很不稳定,同时也有可能直接越过最优解的区域导致曲线直接发散了
  • 学习率比较高的时候,有一定的收敛速度,但是在接近最优解的时候,容易产生震荡现象,达不到最优解
  • 学习率较低的时候,每次更新的幅度比较小,收敛速度较慢
  • 好的学习率使得 loss 在初期快速下降,并在整个训练过程中稳定下降

如果初始化不当,可能会导致梯度消失和梯度爆炸等问题,导致一开始损失曲线停滞不前


观察 train accuracy 和 validation accuracy。如果差距过大的话,那么可能出现过拟合的问题,可以增加正则化系数;如果两者没有什么差距的话,就可能出现欠拟合的问题,模型在训练集和验证集上表现的都不理想,可以增加模型容量(包括增加模型层数、增加每层的神经元数量、使用更复杂的模型结构等)以提高模型的表达能力和拟合能力。


Track the ratio of weight updates / weight magnitudes 每次更新的权重占整体权重的比值,尽可能处于一个合适的值

# 假设参数向量W及其梯度向量dW
param_scale = np.linalg.norm(W.ravel())  # 计算参数向量W的范数(即权重大小)
update = -learning_rate * dW  # 简单的SGD(随机梯度下降)更新公式
update_scale = np.linalg.norm(update.ravel())  # 计算更新向量的范数(即更新幅度)
W += update  # 实际执行参数更新
print(update_scale / param_scale)  # 输出更新幅度与权重大小的比率,期望该值接近1e-3

want this to be somewhere around 0.001 or so

Fancier Optimization

对于优化权重的方法,我们之前提到了梯度下降法 (SGD)。下面我们将介绍 SGD 算法的问题以及提出一些新的优化方法。


问题1:如果 loss function 在不同方向上变化速率相差很大,就有可能在梯度下降的过程中,在较缓的维度下变化缓慢,而在陡峭的维度下抖动。

如同下图所示的那样,椭圆的长短轴差异巨大就会出现问题。椭圆一圈一圈是等高线,每次下降的方向与当前等高线的切线方向垂直,那么梯度下降的方向就会出现下图震荡的情况。

High condition number

这种出现两个维度差异巨大的情况表明该 loss function 的 Hessian 矩阵有着较大的条件数(最大最小奇异值之比)


问题2:如果 loss function 存在局部最小值或者鞍点会出现什么情况?

局部最小值就是在一个区域内,如果一个点的函数值小于或等于其邻域内所有其他点的函数值。采用 SGD 的时候很容易被困在某一个局部极小值中,从而仅仅得到一个全局次优解

鞍点是在某个方向是局部最小值,在某个方向是局部最大值,该点的梯度为0。在鞍点附近梯度接近于0,比较平缓。

对于鞍点来讲,平稳段会减缓学习,平稳段是一块区域,其中导数长时间接近于0,如果你在此处,梯度会从曲面从从上向下下降,因为梯度等于或接近0,曲面很平坦,你得花上很长时间慢慢抵达平稳段的这个点。

在高维空间中鞍点比局部最优点更加普遍存在


问题3:由于我们的梯度并不是通过全部样本得到的,而是选取了一些小样本 (mini-batch) 所得到的,所以下降的路线可能充满噪声进而变得非常曲折。


为了解决上述问题,我们对 SGD 的方法进行改进,引入动量。这是一种很自然且巧妙的想法,一个球落入一个局部最优的时候,是有一定可能冲出整个 ”小坑“ 的。

放到代码中来说就是就是每一步的步长受到前一步的影响,上述的三个问题都得到了很好的解决

这种方法的问题在于在最优解附近可能出现过冲的现象

当然 SGD + Momentum 也有一些变式,如 Nesterov Momentum,其更新方式略有区别。

The original update equations are given by:

\[ \begin{aligned} &v_{t+1} = \rho v_t - \alpha \nabla f(x_t + \rho v_t)\\ &x_{t+1} = x_t + v_{t+1} \end{aligned} \]

Note: This formulation is annoying because we usually want updates in terms of \(x_t\) and \(\nabla f(x_t)\).

To simplify the equations, we introduce a change of variables:

\[ \tilde{x}_t = x_t + \rho v_t \]

After rearranging, the update equations become:

\[ v_{t+1} = \rho v_t - \alpha \nabla f(\tilde{x}_t) \]
\[ \begin{aligned} \tilde{x}_{t+1} &= \tilde{x}_t - \rho v_t + (1 + \rho) v_{t+1} \\ &= \tilde{x}_t + v_{t+1} + \rho(v_{t+1} - v_t) \end{aligned} \]

【稍微做一个换元,因为我们一般来说是给某个点得到这个点的梯度】

Nesterov 版本对未来的位置做了一定的预测,提供了更好的控制,减少了过冲的风险,通常能提供更快且更稳定的收敛速度。


接下来介绍另一种方法 AdaGrad

grad_squared = 0
while True:
    dx = compute_gradient(x)
    grad_squared += dx * dx
    x -= learning_rate * dx / (np.sqrt(grad_squared) +1e-7)

将每一步的学习率与之前的梯度大小产生联系,如果某个参数在过去迭代中经常有较大的梯度,那么它的学习率将会逐渐减小;反之,如果某个参数的梯度一直较小,它的学习率则会保持较大。

  • 优点:AdaGrad能有效处理稀疏数据和非平稳目标函数,因为它为每个参数提供了个性化的学习率调整策略。
  • 缺点:由于历史梯度平方和不断累积,AdaGrad的学习率会逐渐减小,可能导致后期训练时学习率过小,影响模型的进一步优化。

对 AdaGrad 方法有一个改进:RMSProp,解决了后期学习率过慢的问题

grad_squared = 0
while True:
    dx = compute_gradient(x)
    grad_squared  = decay_rate * grad_squared + (1 - decay_rate) * dx *dx
    x -= learning_rate * dx / (np.sqrt(grad_squared) +1e-7)

我们也可以将上述的两类方法结合起来,提出 Adam 的算法

first_moment = 0
second_moment = 0
while True:
    dx = compute_gradient(x)
    first_moment = beta1 * first_moment + (1 - beta1) * dx
    second_moment = beta2 * second_moment + (1 - beta2) * dx * dx
    x -= learning_rate * first_moment / (np.sqrt(second_moment) + 1e-7)

first_moment 像是结合了 momentum 的思想,second_moment 则是结合了 RMSProp 的思想

在实际应用中还需要一些偏差校正,因为在刚开始的时候两者都初始化为0,那么第一个时间的时候 first_moment 和 second_moment 为 (1 - beta1) * dx 和 (1 - beta2) * dx * dx,而 beta1 和 beta2 都很接近于1,所以初始的变化会非常小。为了消除这种初始偏差, Adam 算法引入偏差校正机制,对其进行相应的校正

first_moment = 0
second_moment = 0
for t in range(1, num_iterations):
    dx = compute_gradient(x)
    first_moment = beta1 * first_moment + (1 - beta1) * dx  # Momentum
    second_moment = beta2 * second_moment + (1 - beta2) * dx * dx  # AdaGrad / RMSProp
    first_unbias = first_moment / (1 - beta1 ** t)
    second_unbias = second_moment / (1 - beta2 ** t)
    x -= learning_rate * first_unbias / (np.sqrt(second_unbias) + 1e-7)

Usually, Adam with beta1 = 0.9, beta2 = 0.999, and learning_rate = 1e-3 or 5e-4 is a great starting point for many models!


对于学习率来说,我们可以选择让其随着时间而变小,每隔一段时间缩小一下学习率

  • 指数衰减 (Exponential Decay)
\[ \alpha = \alpha_0 e^{-kt} \]
  • 1/t 衰减 (1/t Decay)
\[ \alpha = \frac{\alpha_0}{1 + kt} \]


注意我们之前的梯度下降其实是在该点做线性拟合然后找到这段部分的最小值,我们也可以做二阶曲线的拟合,用二次函数对该店进行拟合,然后找到这个二阶曲线的最小值点

Second-order Taylor expansion: 对 loss function 在该点做泰勒展开,展开到二次项

\[ J(\theta) \approx J(\theta_0) + (\theta - \theta_0)^\top \nabla_\theta J(\theta_0) + \frac{1}{2} (\theta - \theta_0)^\top H(\theta - \theta_0) \]

Solving for the critical point we obtain the Newton parameter update:

\[ \theta^* = \theta_0 - H^{-1} \nabla_\theta J(\theta_0) \]

这样的更新方式不需要学习率这个超参数,但是计算量很大:海森矩阵有 \(O(N^2)\) 个参数,求逆需要 \(O(N^3)\) 的计算量。对此我们有一些优化的方法

  • Quasi-Newton Methods(拟牛顿法):在二阶优化方法中,牛顿法通过计算目标函数的海森矩阵 \(H\) 的逆来更新参数。然而,直接计算和存储海森矩阵及其逆的计算复杂度为 \(O(n^3)\),其中 \(n\) 是参数的数量。对于高维问题,这种计算成本非常高昂。为了克服这一问题,拟牛顿法(Quasi-Newton methods)被提出。这类方法中最著名的是Broyden-Fletcher-Goldfarb-Shanno算法(简称BFGS)。BFGS的核心思想是不直接计算海森矩阵的逆,而是通过一系列秩1更新(rank 1 updates)来近似海森矩阵的逆。每次迭代的计算复杂度为 \(O(n^2)\)

  • L-BFGS(有限内存BFGS)

尽管BFGS已经大大降低了计算复杂度,但在处理大规模问题时,存储和更新近似矩阵 \(B_k\) 仍然需要大量的内存。为了解决这个问题,L-BFGS(Limited-memory BFGS)被提出。L-BFGS的核心思想是不形成或存储完整的近似海森矩阵的逆,而是只保留最近几次迭代中的梯度差和参数差信息。具体来说,L-BFGS只存储最近 \(m\) 次迭代的信息,并利用这些信息来构建搜索方向。这样做的好处是,只需要 \(O(mn)\) 的内存空间,其中 \(m\) 是一个较小的常数,通常取值为5到20。

L-BFGS 方法通常在全批量(full batch)和确定性(deterministic)模式下表现出色。这意味着当你的目标函数 \(f(x)\) 是一个单一的、确定性的函数时,L-BFGS 很可能能够很好地工作。然而,L-BFGS 在小批量(mini-batch)设置中表现不佳,甚至可能导致较差的结果。小批量设置是指每次迭代只使用一小部分数据来计算梯度,而不是整个数据集。

Model Ensembles

模型集成是一种稳定提升正确率的技术,虽然提升的并不多。大致的思路是先训练多个独立的模型,然后在测试阶段平均各个模型得到的结果。

在实践过程中,有一些技巧,比如说使用单个模型在训练过程中的多个快照(snapshots)来进行集成。这种方法不仅能够提高模型的泛化能力,还能节省大量的计算资源。

可以参考这篇文章:使用余弦退火逃离局部最优点——快照集成(Snapshot Ensembles)在Keras上的应用 - 知乎


还有一种被称为 Polyak 平均化的方法

while True:
    data_batch = dataset.sample_data_batch()
    loss = network.forward(data_batch)
    dx = network.backward()
    x += - learning_rate * dx  # 更新当前参数向量x
    x_test = 0.995 * x_test + 0.005 * x  # 计算并更新移动平均参数向量x_test

通过计算参数向量在整个训练过程中的加权平均值来提高模型的泛化能力。具体来说,在每次迭代中,除了更新当前的参数向量外,还会更新一个累积的平均参数向量。这个平均参数向量在训练结束后用于测试和预测。

Regularization

当验证集的准确率比训练集的准确率要低很多的时候,说明我们对训练集过拟合了,利用之前的知识我们可以知道通过正则化的方式来解决(缓解?)这个问题。之前讲的正则化方式是增加一个正则化项以加入参数复杂化的代价,接下来我们将介绍其他的几种正则化的方式

Dropout

In each forward pass, randomly set some neurons to zero. Probability of dropping is a hyperparameter; 0.5 is common

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)

随机舍弃一些神经元以降低模型对某个样本的依赖

dropout 也可以被认为是一种模型集成,每一种神经元掩码都是一种模型。

在测试的时候我们对所有的情况取均值

\[ y = f(x) = E_z[f(x,z)] = \int p(z)f(x,z) \text{ d}z \]

Example

由上面这个例子可以看出只需要正常做然后乘以相应的概率即可。

在测试阶段做乘法可以改为在训练阶段对相应的值除以相应的概率,这样我们在测试阶段就可以少做一次乘法,略微加速了测试的速度。

Data Augmentation

我们可以对同一份数据进行一定的处理,使其于原先的数据产生区别,但是相应的类别并没有变化

一些常用的数据增强的方式

即在训练阶段框选多个位置进行采样;在测试阶段取多个地方进行测试然后平均一下

加入一些颜色的偏差

Random mix/combinations of :

- translation

- rotation

- stretching

- shearing,

- lens distortions, … (go crazy)

Others

DropConnect

  • 类似于Dropout,但在训练时随机丢弃连接权重,而不是神经元。
  • 在测试时使用所有连接权重,并将权重乘以保留概率,以模拟多个子网络的平均效果。

Fractional Max Pooling

  • 在训练时使用随机选择的池化窗口大小和步长,以增加模型的灵活性。
  • 在测试时通常使用固定的池化窗口大小和步长,以保持模型的一致性。

Stochastic Depth

  • 在训练时随机跳过某些层,以减少模型的深度和复杂度。
  • 在测试时使用完整的模型结构,以充分利用所有层的信息。

Transfer Learning

训练一个数据集一定需要很多数据吗?其实不一定需要,我们可以先在大规模数据集中先预训练一个模型,然后我们在一个小数据集上进行微调;当然如果数据集更多的情况下,我们可以训练微调更多层

微调的过程就是重新初始化某一层然后只针对这一层重新训练,将其他层进行固定。神经网络的顶层更加抽象,与具体的任务息息相关。

使用CNN进行迁移学习已经非常普遍,成为了一种常态而非例外。在实际应用中,人们通常会利用预训练的CNN模型作为起点,而不是从零开始训练一个全新的模型。

评论区

对你有帮助的话请给我个赞和 star => GitHub stars
欢迎跟我探讨!!!