【深度学习实验】网络优化与正则化(七):超参数优化方法——网格搜索、随机搜索、贝叶斯优化、动态资源分配、神经架构搜索

2024-07-30 09:12:34 浏览数 (2)

一、实验介绍

  深度神经网络在机器学习中应用时面临两类主要问题:优化问题和泛化问题。

  • 优化问题:深度神经网络的优化具有挑战性。
    • 神经网络的损失函数通常是非凸函数,因此找到全局最优解往往困难。
    • 深度神经网络的参数通常非常多,而训练数据也很大,因此使用计算代价较高的二阶优化方法不太可行,而一阶优化方法的训练效率通常较低。
    • 深度神经网络存在梯度消失梯度爆炸问题,导致基于梯度的优化方法经常失效。
  • 泛化问题:由于深度神经网络的复杂度较高且具有强大的拟合能力,很容易在训练集上产生过拟合现象。因此,在训练深度神经网络时需要采用一定的正则化方法来提高网络的泛化能力。

  目前,研究人员通过大量实践总结了一些经验方法,以在神经网络的表示能力、复杂度、学习效率和泛化能力之间取得良好的平衡,从而得到良好的网络模型。本系列文章将从网络优化和网络正则化两个方面来介绍如下方法:

  • 在网络优化方面,常用的方法包括优化算法的选择参数初始化方法数据预处理方法逐层归一化方法超参数优化方法
  • 在网络正则化方面,一些提高网络泛化能力的方法包括ℓ1和ℓ2正则化权重衰减提前停止丢弃法数据增强标签平滑等。

  本文将介绍神经网络优化的逐层归一化方法,包括批量归一化、层归一化、权重归一化(略)、局部响应归一化(略)等

二、实验环境

  本系列实验使用了PyTorch深度学习框架,相关操作如下:

1. 配置虚拟环境

代码语言:javascript复制
conda create -n DL python=3.7 
代码语言:javascript复制
conda activate DL
代码语言:javascript复制
pip install torch==1.8.1 cu102 torchvision==0.9.1 cu102 torchaudio==0.8.1 -f https://download.pytorch.org/whl/torch_stable.html
代码语言:javascript复制
conda install matplotlib
代码语言:javascript复制
 conda install scikit-learn

2. 库版本介绍

软件包

本实验版本

目前最新版

matplotlib

3.5.3

3.8.0

numpy

1.21.6

1.26.0

python

3.7.16

scikit-learn

0.22.1

1.3.0

torch

1.8.1 cu102

2.0.1

torchaudio

0.8.1

2.0.2

torchvision

0.9.1 cu102

0.15.2

三、优化算法

  神经网络的参数学习是一个非凸优化问题.当使用梯度下降法来进行优化网络参数时,参数初始值的选取十分关键,关系到网络的优化效率和泛化能力.参数初始化的方式通常有以下三种:

0. 导入必要的库

代码语言:javascript复制
from torch import nn

1. 随机梯度下降SGD算法

  随机梯度下降(Stochastic Gradient Descent,SGD)是一种常用的优化算法,用于训练深度神经网络。在每次迭代中,SGD通过随机均匀采样一个数据样本的索引,并计算该样本的梯度来更新网络参数。具体而言,SGD的更新步骤如下:

  1. 从训练数据中随机选择一个样本的索引。
  2. 使用选择的样本计算损失函数对于网络参数的梯度。
  3. 根据计算得到的梯度更新网络参数。
  4. 重复以上步骤,直到达到停止条件(如达到固定的迭代次数或损失函数收敛)。
a. PyTorch中的SGD优化器

Pytorch官方教程

代码语言:javascript复制
optimizer = torch.optim.SGD(model.parameters(), lr=0.2)
b. 使用SGD优化器的前馈神经网络

【深度学习实验】前馈神经网络(final):自定义鸢尾花分类前馈神经网络模型并进行训练及评价

2.随机梯度下降的改进方法

  传统的SGD在某些情况下可能存在一些问题,例如学习率选择困难和梯度的不稳定性。为了改进这些问题,提出了一些随机梯度下降的改进方法,其中包括学习率的调整和梯度的优化。

a. 学习率调整
  • 学习率衰减(Learning Rate Decay):随着训练的进行,逐渐降低学习率。常见的学习率衰减方法有固定衰减、按照指数衰减、按照时间表衰减等。
  • Adagrad:自适应地调整学习率。Adagrad根据参数在训练过程中的历史梯度进行调整,对于稀疏梯度较大的参数,降低学习率;对于稀疏梯度较小的参数,增加学习率。这样可以在不同参数上采用不同的学习率,提高收敛速度。
  • Adadelta:与Adagrad类似,但进一步解决了Adagrad学习率递减过快的问题。Adadelta不仅考虑了历史梯度,还引入了一个累积的平方梯度的衰减平均,以动态调整学习率。
  • RMSprop:也是一种自适应学习率的方法,通过使用梯度的指数加权移动平均来调整学习率。RMSprop结合了Adagrad的思想,但使用了衰减平均来减缓学习率的累积效果,从而更加稳定。
b. 梯度估计修正
  • Momentum:使用梯度的“加权移动平均”作为参数的更新方向。Momentum方法引入了一个动量项,用于加速梯度下降的过程。通过积累之前的梯度信息,可以在更新参数时保持一定的惯性,有助于跳出局部最优解、加快收敛速度。
  • Nesterov accelerated gradient:Nesterov加速梯度(NAG)是Momentum的一种变体。与Momentum不同的是,NAG会先根据当前的梯度估计出一个未来位置,然后在该位置计算梯度。这样可以更准确地估计当前位置的梯度,并且在参数更新时更加稳定。
  • 梯度截断(Gradient Clipping):为了应对梯度爆炸或梯度消失的问题,梯度截断的方法被提出。梯度截断通过限制梯度的范围,将梯度控制在一个合理的范围内。常见的梯度截断方法有阈值截断和梯度缩放。

3. 梯度估计修正:动量法Momentum

【深度学习实验】网络优化与正则化(一):优化算法:使用动量优化的随机梯度下降算法(Stochastic Gradient Descent with Momentum)

4. 自适应学习率

【深度学习实验】网络优化与正则化(二):基于自适应学习率的优化算法详解:Adagrad、Adadelta、RMSprop

5. Adam算法

Adam算法(Adaptive Moment Estimation Algorithm)[Kingma et al., 2015]可以看作动量法和 RMSprop 算法的结合,不但使用动量作为参数更新方向,而且可以自适应调整学习率。 【深度学习实验】网络优化与正则化(三):随机梯度下降的改进——Adam算法详解(Adam≈梯度方向优化Momentum 自适应学习率RMSprop)~入选综合热榜

四、参数初始化

【深度学习实验】网络优化与正则化(四):参数初始化及其Pytorch实现——基于固定方差的初始化(高斯、均匀分布),基于方差缩放的初始化(Xavier、He),正交初始化

五、数据预处理

【深度学习实验】网络优化与正则化(五):数据预处理详解——标准化、归一化、白化、去除异常值、处理缺失值~入选综合热榜

六、逐层归一化

【深度学习实验】网络优化与正则化(六):逐层归一化方法——批量归一化、层归一化、权重归一化、局部响应归一化

七、超参数优化

0. 前期准备

a. 神经网络模型
代码语言:javascript复制
class SimpleNN(nn.Module):
    def __init__(self, input_size, hidden_size, output_size):
        super(SimpleNN, self).__init__()
        self.fc1 = nn.Linear(input_size, hidden_size)
        self.relu = nn.ReLU()
        self.fc2 = nn.Linear(hidden_size, output_size)
        self.softmax = nn.Softmax(dim=1)

    def forward(self, x):
        x = self.fc1(x)
        x = self.relu(x)
        x = self.fc2(x)
        x = self.softmax(x)
        return x
b. 训练和评估函数
代码语言:javascript复制
def train_and_evaluate(model, X_train, y_train, X_val, y_val, criterion, optimizer, epochs=10):
    for epoch in range(epochs):
        model.train()
        optimizer.zero_grad()
        outputs = model(X_train)
        loss = criterion(outputs, y_train)
        loss.backward()
        optimizer.step()

    model.eval()
    val_outputs = model(X_val)
    _, predictions = torch.max(val_outputs, 1)
    val_accuracy = accuracy_score(y_val.numpy(), predictions.numpy())
    return val_accuracy
c. 分类数据
代码语言:javascript复制
在这里插入代码片

1. 网格搜索

  网格搜索(Grid Search)是一种穷举搜索方法,它尝试在预定义的超参数空间中的所有可能组合中找到最佳配置。具体来说,如果总共有

K

个超参数,每个超参数可以取

m_k

个不同的值,那么网格搜索将尝试

m_1 × m_2 × ... × m_K

个不同的超参数组合。   在网格搜索中,如果某些超参数是连续的,而不是离散的,可以将其离散化为几个"经验"值。这样做的目的是为了限制搜索空间,以便更有效地寻找最佳配置。虽然这种方法可能会在某些情况下忽略超参数的细微变化,但在实践中,它可以帮助减少搜索的复杂性。   网格搜索是一种简单但有效的方法,特别适用于超参数空间较小的情况。然而,对于超参数空间较大或高维的情况,随机搜索、贝叶斯优化或演化算法等方法可能更具优势,它们可以更灵活地探索超参数空间,而不会受到穷举搜索的限制。

a. 基本步骤
  1. 定义超参数空间: 首先,需要明确定义每个超参数的可能取值。这可以通过指定每个超参数的候选值范围来完成。
  2. 创建参数网格: 对于每个超参数,选择一组候选值。将这些候选值组合成一个网格,即每个超参数的所有可能组合。如果超参数的取值空间是离散的,那么可以使用所有可能的离散值;如果是连续的,可以选择一些合适的离散化值。
  3. 设置评估指标: 定义一个评估指标,用于度量每个超参数组合的性能。这通常是在验证集或开发集上的性能表现,如准确率、误差率等。目标是最小化或最大化该指标,具体取决于任务类型。
  4. 训练和评估模型: 对于每个超参数组合,在训练集上训练模型,并在验证集上评估性能。使用定义的评估指标来度量每个模型的性能。
  5. 选择最佳超参数组合: 通过比较所有超参数组合的性能,选择具有最佳性能的超参数组合。
  6. 可视化和分析: 可以通过可视化方法,如学习曲线或热力图,来进一步分析超参数的影响。这有助于了解模型在超参数空间中的表现。
  7. 验证和测试: 最终,使用选定的最佳超参数组合在测试集上验证模型的性能,确保所选超参数对未见数据的泛化效果。
b. 代码实现
代码语言:javascript复制
hidden_sizes = [64, 128, 256]
learning_rates = [0.001, 0.01, 0.1]

best_accuracy = 0
best_params = {}

for hidden_size in hidden_sizes:
    for learning_rate in learning_rates:
        model = SimpleNN(input_size=20, hidden_size=hidden_size, output_size=2)
        optimizer = SGD(model.parameters(), lr=learning_rate)
        criterion = nn.CrossEntropyLoss()

        accuracy = train_and_evaluate(model, torch.FloatTensor(X_train), torch.LongTensor(y_train),
                                      torch.FloatTensor(X_test), torch.LongTensor(y_test), criterion, optimizer)

        if accuracy > best_accuracy:
            best_accuracy = accuracy
            best_params = {'hidden_size': hidden_size, 'learning_rate': learning_rate}

print("Grid Search - Best Parameters:", best_params)
print("Grid Search - Best Accuracy:", best_accuracy)

2. 随机搜索

  随机搜索是一种更灵活的超参数优化方法,相较于网格搜索,它不受先验定义的超参数网格的限制。通过在超参数空间中进行随机采样,随机搜索能够更有效地探索可能的超参数组合,特别是当某些超参数对模型性能的影响相对较小或难以预测时。   随机搜索的主要优势在于它避免了网格搜索中的过度尝试不重要的超参数组合。对于那些对性能有较大影响的超参数,随机搜索有更大的可能性在更早的阶段找到优秀的配置,而不受网格搜索的较粗略采样的限制。

a. 基本步骤
  1. 定义超参数空间: 确定每个超参数的可能取值范围。这可以是一个离散的集合,也可以是一个连续的区间。
  2. 选择随机超参数组合: 对于每次迭代,从超参数空间中随机选择一个超参数组合。这可以通过在每个超参数的取值范围内进行均匀或非均匀的随机采样来完成。
  3. 训练和评估模型: 使用所选的超参数组合,在训练集上训练模型,并在验证集或开发集上评估性能。这通常涉及训练模型直到收敛或达到预定义的迭代次数。
  4. 更新最佳配置: 比较当前超参数组合的性能与已知的最佳性能,如果性能更好,则更新最佳配置。
  5. 重复迭代: 重复以上步骤,直到达到预定的迭代次数或计算资源限制。
b. 优缺点
  • 相对于网格搜索,随机搜索的主要优点在于:
    • 灵活性: 不受先验定义的网格限制,能够在超参数空间中更灵活地进行探索。
    • 高效性: 特别适用于超参数空间较大的情况,避免了网格搜索中不必要的尝试。
  • 然而,随机搜索也有一些局限性:
    • 不保证最优解: 由于是随机选择,不保证找到全局最优的超参数配置。
    • 不充分利用超参数之间的相关性: 与贝叶斯优化等方法相比,随机搜索不利用不同超参数之间的相关性,可能在搜索过程中浪费一些资源。
c. 代码实现
代码语言:javascript复制
num_trials = 10

best_accuracy = 0
best_params = {}

for _ in range(num_trials):
    hidden_size = random.choice([64, 128, 256])
    learning_rate = random.choice([0.001, 0.01, 0.1])

    model = SimpleNN(input_size=20, hidden_size=hidden_size, output_size=2)
    optimizer = SGD(model.parameters(), lr=learning_rate)
    criterion = nn.CrossEntropyLoss()

    accuracy = train_and_evaluate(model, torch.FloatTensor(X_train), torch.LongTensor(y_train),
                                  torch.FloatTensor(X_test), torch.LongTensor(y_test), criterion, optimizer)

    if accuracy > best_accuracy:
        best_accuracy = accuracy
        best_params = {'hidden_size': hidden_size, 'learning_rate': learning_rate}

print("Random Search - Best Parameters:", best_params)
print("Random Search - Best Accuracy:", best_accuracy)

3. 贝叶斯优化

  贝叶斯优化是一种基于贝叶斯统计的自适应超参数优化方法,它通过在搜索空间中建立一个目标函数的概率模型,来智能地选择下一组待试验的超参数。这种方法相对于随机搜索和网格搜索更加高效,特别适用于计算资源受限的情况下。

a.基本步骤
  1. 定义超参数空间: 和其他优化方法一样,首先需要定义每个超参数的可能取值范围。
  2. 选择初始样本点: 选择一组初始的超参数样本点,通常是通过随机选择或者根据先验知识选择的。
  3. 建立概率模型: 使用已有的样本点,建立一个对目标函数的概率模型。常用的模型包括高斯过程(Gaussian Process)和随机森林。
  4. 选择下一个样本点: 基于当前的概率模型,选择下一个超参数样本点,这个选择通常是基于对目标函数的不确定性的评估。一种常见的策略是使用“概率提升(Probability of Improvement)”或“置信区间(Expected Improvement)”等指标来评估每个点的潜在收益。
  5. 采样和评估: 在选择的超参数点处进行模型的训练和评估,得到目标函数的值。
  6. 更新概率模型: 将新的样本点加入已有的样本,然后更新概率模型,以更准确地表示目标函数。
  7. 重复迭代: 重复上述步骤,直到达到预定的迭代次数或满足其他停止准则。

  贝叶斯优化的优势在于它能够根据已有样本来预测目标函数的形状,从而更聪明地选择下一个样本点,尤其在高维空间和计算资源有限的情况下表现得更为明显。时序模型优化(Sequential Model-Based Optimization,SMBO)是一种基于序列的贝叶斯优化方法,其中的“时序”指的是通过不断地迭代来逐步改善模型。

b. 代码实现
代码语言:javascript复制
def objective(trial):
    hidden_size = trial.suggest_categorical('hidden_size', [64, 128, 256])
    learning_rate = trial.suggest_loguniform('learning_rate', 0.001, 0.1)

    model = SimpleNN(input_size=20, hidden_size=hidden_size, output_size=2)
    optimizer = SGD(model.parameters(), lr=learning_rate)
    criterion = nn.CrossEntropyLoss()

    accuracy = train_and_evaluate(model, torch.FloatTensor(X_train), torch.LongTensor(y_train),
                                  torch.FloatTensor(X_test), torch.LongTensor(y_test), criterion, optimizer)

    return -accuracy  # Optuna minimizes the objective function, so we use negative accuracy.

sampler = TPESampler(seed=42)
study = optuna.create_study(sampler=sampler)
study.optimize(objective, n_trials=10)

print("Bayesian Optimization - Best Parameters:", study.best_params)
print("Bayesian Optimization - Best Accuracy:", -study.best_value)  # Convert back to positive accuracy

4. 动态资源分配

  动态资源分配是一种在超参数优化中更加智能地分配有限资源的方法。它的核心思想是通过早期停止和逐次减半等策略,在训练过程中识别哪些超参数组合可能不会带来较好的性能,从而及时中止这些配置的评估,将资源更多地留给其他有潜力的配置。以下是动态资源分配的一般步骤,特别是逐次减半方法:

  1. 定义超参数空间和总资源预算: 和其他超参数优化方法一样,首先需要定义每个超参数的可能取值范围,并确定可用的总资源预算(例如,摇臂的次数)。
  2. 初始化超参数配置: 随机选择一组初始的超参数配置,并开始评估它们的性能。
  3. 逐次减半: 将总资源预算分配给一组超参数配置,并在每一轮中选择性能较好的一半进行下一轮的评估。这个过程会重复进行,逐次减半资源分配,直到达到预定的轮数或资源用尽。
  4. 早期停止策略: 对于正在评估的每个超参数配置,可以通过监测学习曲线的形状,比如早期停止来判断是否中止当前训练。如果学习曲线不收敛或者收敛较差,可以中止当前训练,将资源留给其他配置。
  5. 选择最佳超参数配置: 根据逐次减半的过程,选择性能最好的超参数配置作为最终的结果。

  逐次减半方法通过在每一轮中聚焦于性能较好的超参数配置,更有可能找到全局最优或局部最优的配置。这种方法尤其适用于计算资源受限的情况,可以在较短时间内找到性能较好的超参数配置。

5. 神经架构搜索

  神经架构搜索(Neural Architecture Search,NAS)是一种探索神经网络结构的自动化方法。与传统的由人类专家手动设计神经网络结构不同,NAS旨在通过使用机器学习技术来搜索神经网络的结构,以提高性能。基本上,神经架构搜索的目标是找到一个最优的神经网络结构,使得在给定任务上的性能达到最佳。这可以通过定义一个搜索空间,其中包含各种可能的网络结构来实现。每个网络结构都可以用一个参数化的描述来表示,通常是一个变长的字符串。这个描述包含了网络的层次结构、每一层的类型、连接方式等信息。

  神经架构搜索通常采用元学习的思想。这意味着有一个控制器,负责生成神经网络结构的描述。这个控制器本身可以是一个循环神经网络(RNN),它学会生成有效的网络结构描述。控制器的训练过程通常使用强化学习来完成。奖励信号一般是由生成的子网络在开发集或验证集上的性能,例如准确率。整个神经架构搜索的流程如下:

  1. 定义搜索空间: 确定神经网络结构的参数化表示,并定义一个搜索空间,包含各种可能的网络结构。
  2. 设计控制器: 创建一个控制器,通常是一个循环神经网络(RNN),负责生成神经网络结构的描述。
  3. 初始化控制器: 初始化控制器的参数。
  4. 强化学习训练: 通过强化学习算法,如REINFORCE,训练控制器。在每一轮训练中,生成一个网络结构描述,训练该结构的子网络,然后使用性能作为奖励信号来更新控制器的参数。
  5. 搜索过程: 通过不断迭代上述过程,搜索最佳的神经网络结构描述。
  6. 评估最优结构: 使用测试集评估最终选择的最优神经网络结构的性能。

  神经架构搜索的优势在于它可以自动发现复杂的网络结构,而不需要人类专家的介入。这使得神经网络设计更具有普适性和适应性,能够更好地适应不同的任务和数据。然而,NAS也面临着计算资源消耗大、搜索空间巨大等挑战。近年来,许多改进的方法和算法被提出,以提高神经架构搜索的效率。

0 人点赞