工具介绍:使用 Optuna 进行超参数调优

机器学习与数学

共 7209字,需浏览 15分钟

 · 2022-01-19

在机器学习中,除了一般可学习的参数外,还有一些参数需要预先设置,称为超参数。

超参数的值对于模型的性能至关重要,寻找在验证集上性能最佳的超参数称为超参数优化,然而这并不是一项简单的任务。

可以通过搜索方法来选择比较好的超参数。比如,Grid Search 和 Random Search 之类的方法。后者貌似带有一定随机性,但往往表现得比前者好。因为它以不均匀的间隔搜索超参数空间,避免了 Grid Search 的很多冗余操作。

除了这种搜索方式,我们还可以使用像 Optuna 这样的更加灵活强大的工具来应对这项任务。先安装它,

pip install optuna

1流程及简例

一个典型的 Optuna 的优化程序中只有三个最核心的概念,

  • objective,负责定义待优化的目标函数并指定参数的范围。

  • trial,对应目标函数 objective 的单次试验。

  • study,负责管理整个优化过程,决定优化的方式、总试验的次数、试验结果的记录等功能。

.简例 .

例如,在定义域 内估计函数 的最小值及其对应的

我们就用这个例子来练练手,代码如下。

import optuna
 
def objective(trial):
    x = trial.suggest_uniform('x'01)
    y = trial.suggest_uniform('y'01)
    return (x + y) ** 2
 
study = optuna.create_study(direction='minimize')
study.optimize(objective, n_trials=100)
 
print(study.best_params)
print(study.best_value)
{'x': 0.00136012038661543, 'y': 0.0003168904600867363}
2.8123653799567168e-06
  • 首先,定义一个 objective 函数,即 ,其参数 采样自两个均匀分布。

  • 然后,Optuna 创建了一个 study,指定了优化的方式为最小化并且最大实验次数为 100,然后将目标函数传入其中,开始优化过程。

  • 最后,输出在 100 次试验中找到的最佳参数组合。

注意,上面例子是拿来演示它的使用流程,并不是拿它去解一般的最优化问题。

.可视化 .

study 结束,下一步就是查看结果,可以使用 Optuna 的内置可视化函数来查看 study 的各项进度。

下面用 plotly 来作多个方式的可视化。如果没有安装过,则安装它 pip install plotly

下图显示了模型在多次迭代中的性能演化。预期的行为是模型性能随着搜索次数的增加而提高。

study.optimize 执行结束以后,调用 plot_contour,并将 study 和需要可视化的参数传入该方法,Optuna 将返回一张等高线图。

例如,当在上面的例子中,我们想要查看参数 的关系以及它们对于函数值的共同贡献,只需要下面的语句即可:

optuna.visualization.plot_contour(study, params=['x''y'])

还有其他形式的一些图,如通过展示超参数重要性可以了解到哪些超参数对模型的性能影响较大。

optuna.visualization.plot_param_importances(study)

2CNN 超参数优化例子

本文接下来结合 PyTorchOptuna,来实验 CNN 模型在 MNIST 数据集上的超参数优化。

import matplotlib.pyplot as plt
import numpy as np

import torch
import torch.nn as nn
import torch.optim as optim
from torch.utils.data import Dataset, DataLoader,TensorDataset,random_split,SubsetRandomSampler, ConcatDataset
from torch.nn import functional as F
import torchvision
from torchvision import datasets,transforms
import torchvision.transforms as transforms
import optuna
import os

DEVICE = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
CLASSES = 10
DIR = os.getcwd()
EPOCHS = 10
LOG_INTERVAL = 10
train_dataset = torchvision.datasets.MNIST('classifier_data', train=True, download=True)
m=len(train_dataset)

transform = torchvision.transforms.Compose([
    torchvision.transforms.ToTensor()
])

train_dataset.transform=transform

定义具体的卷积神经网络,并增加参数 trial 来设置要采样的超参数。

class ConvNet(nn.Module):
    def __init__(self, trial):
        # We optimize dropout rate in a convolutional neural network.
        super(ConvNet, self).__init__()

        self.conv1 = nn.Conv2d(in_channels=1, out_channels=16, kernel_size=5, stride=1, padding=2)

        self.conv2 = nn.Conv2d(in_channels=16, out_channels=32, kernel_size=5, stride=1, padding=2)
        dropout_rate = trial.suggest_float("dropout_rate"00.5,step=0.1)
        self.drop1=nn.Dropout2d(p=dropout_rate)   
        
        fc2_input_dim = trial.suggest_int("fc2_input_dim"32128,32)
        self.fc1 = nn.Linear(32  7  7, fc2_input_dim)
        dropout_rate2 = trial.suggest_float("dropout_rate2"00.3,step=0.1)
        self.drop2=nn.Dropout2d(p=dropout_rate2)

        self.fc2 = nn.Linear(fc2_input_dim, 10)

    def forward(self, x):
        x = F.relu(F.max_pool2d(self.conv1(x),kernel_size = 2))

        x = F.relu(F.max_pool2d(self.conv2(x),kernel_size = 2))
        x = self.drop1(x)

        x = x.view(x.size(0),-1)

        x = F.relu(self.fc1(x))
        x = self.drop2(x)

        x = self.fc2(x)

        return x

定义函数来获取训练集中不同 batch_size 大小的批次数据。它将 train_datasetbatch_size 作为输入,并返回训练和验证数据加载器对象。

def get_mnist(train_dataset,batch_size):

    train_data, val_data = random_split(train_dataset, [int(m-m0.2), int(m0.2)])
    # The dataloaders handle shuffling, batching, etc...
    train_loader = torch.utils.data.DataLoader(train_data, batch_size=batch_size)
    valid_loader = torch.utils.data.DataLoader(val_data, batch_size=batch_size)
    return train_loader, valid_loader

接下来是定义目标函数,它通过采样程序来选择每次试验的超参数值,并返回在该试验中验证集上的准确度。

def objective(trial):

    # Generate the model.
    model = ConvNet(trial).to(DEVICE)

    # Generate the optimizers.

    # try RMSprop and SGD
    '''
    optimizer_name = trial.suggest_categorical("optimizer", ["RMSprop", "SGD"])
    momentum = trial.suggest_float("momentum", 0.0, 1.0)
    lr = trial.suggest_float("lr", 1e-5, 1e-1, log=True)
    optimizer = getattr(optim, optimizer_name)(model.parameters(), lr=lr,momentum=momentum)
    '''

    #try Adam, AdaDelta adn Adagrad
    
    optimizer_name = trial.suggest_categorical("optimizer", ["Adam""Adadelta","Adagrad"])
    lr = trial.suggest_float("lr"1e-51e-1,log=True)
    optimizer = getattr(optim, optimizer_name)(model.parameters(), lr=lr)
    batch_size=trial.suggest_int("batch_size"64256,step=64)

    criterion=nn.CrossEntropyLoss()

    # Get the MNIST imagesset.
    train_loader, valid_loader = get_mnist(train_dataset,batch_size)
    
    # Training of the model.
    for epoch in range(EPOCHS):
        model.train()
       
        for batch_idx, (images, labels) in enumerate(train_loader):
            # Limiting training images for faster epochs.
            #if batch_idx * BATCHSIZE >= N_TRAIN_EXAMPLES:
            #    break

            images, labels = images.to(DEVICE), labels.to(DEVICE)

            optimizer.zero_grad()
            output = model(images)
            loss = criterion(output, labels)
            loss.backward()
            optimizer.step()

        # Validation of the model.
        model.eval()
        correct = 0
        with torch.no_grad():
            for batch_idx, (images, labels) in enumerate(valid_loader):
                # Limiting validation images.
               # if batch_idx * BATCHSIZE >= N_VALID_EXAMPLES:
                #    break
                images, labels = images.to(DEVICE), labels.to(DEVICE)
                output = model(images)
                # Get the index of the max log-probability.
                pred = output.argmax(dim=1, keepdim=True)
                correct += pred.eq(labels.view_as(pred)).sum().item()

        accuracy = correct / len(valid_loader.dataset)

        trial.report(accuracy, epoch)

        # Handle pruning based on the intermediate value.
        if trial.should_prune():
            raise optuna.exceptions.TrialPruned()

    return accuracy

接着,创建一个 study 对象来最大化目标函数,然后使用 optimize 来展开试验,实验次数设为 20 次。

study = optuna.create_study(direction='maximize')
study.optimize(objective, n_trials=20)

trial = study.best_trial

print('Accuracy: {}'.format(trial.value))
print("Best hyperparameters: {}".format(trial.params))
Accuracy: 0.98925
Best hyperparameters: {'dropout_rate': 0.0, 'fc2_input_dim': 64, 'dropout_rate2': 0.1, 'optimizer': 'Adam', 'lr': 0.006891576863485639, 'batch_size': 256}
df = study.trials_dataframe().drop(['state','datetime_start','datetime_complete','duration','number'], axis=1)
df.tail(5)

3可视化 study

先通过下面代码看一下各超参数的总体优化进展情况。

optuna.visualization.plot_optimization_history(study)

以及还可以查看不同超参数组合的等值线图,下图中只关注批次大小和学习率。

.超参数重要性 .

查看各个超参数对目标值的影响大小。

optuna.visualization.plot_param_importances(study)

.单个超参数切片 .

查看不同的单个超参数在多次试验中的变化情况,颜色对应试验次数。

optuna.visualization.plot_slice(study, params=['dropout_rate''batch_size''lr'])

4小结

Optuna 基于贝叶斯方法来进行超参数优化,提供了一种有效的搜索结构化,为模型超参数的选取提供了一种较为理想的解决方案,大家不妨在实践中增加对它的理解。

参考资料



浏览 180
点赞
评论
收藏
分享

手机扫一扫分享

举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

举报