用户定义的采样器 (Sampler)

你可以用用户定义的 sampler 来实现:

  • 试验你自己的采样算法,

  • 实现具体任务对应的算法来改进优化性能,或者

  • 将其他的优化框架包装起来,整合进 Optuna 的流水线中 (比如 SkoptSampler ).

本节将介绍 sampler 类的内部行为,并展示一个实现用户自定义 sampler 的例子。

Sampler 概述

Sampler 负责产生要被用在 trial 中求值的参数值。在目标函数内,当一个 suggest API (比如 suggest_uniform()) 被调用时,一个对应的分布对象 (比如 UniformDistribution) 就会从内部被创建。Sampler 则从该分布汇总采样一个参数值。该采样值会被返回给 suggest API 的调用者,进而在目标函数内被求值。

为了创建一个新的 sampler, 你所定义的类需继承 BaseSampler.该基类提供三个抽象方法:infer_relative_search_space(), sample_relative()sample_independent().

从这些方法名可以看出,Optuna 支持两种类型的采样过程:一种是 relative sampling, 它考虑了单个 trial 内参数之间的相关性, 另一种是 independent sampling, 它对各个参数的采样是彼此独立的。

在一个 trial 刚开始时,infer_relative_search_space() 会被调用,它向该 trial 提供一个相对搜索空间。之后, sample_relative() 会被触发,它从该搜索空间中对相对参数进行采样。在目标函数的执行过程中,sample_independent() 用于对不属于该相对搜索空间的参数进行采样。

注解

更多细节参见 BaseSampler 的文档。

案例: 实现模拟退火 Sampler (SimulatedAnnealingSampler)

下面的代码根据 Simulated Annealing (SA) 定义类一个 sampler:

import numpy as np
import optuna


class SimulatedAnnealingSampler(optuna.samplers.BaseSampler):
    def __init__(self, temperature=100):
        self._rng = np.random.RandomState()
        self._temperature = temperature  # Current temperature.
        self._current_trial = None  # Current state.

    def sample_relative(self, study, trial, search_space):
        if search_space == {}:
            return {}

        #
        # An implementation of SA algorithm.
        #

        # Calculate transition probability.
        prev_trial = study.trials[-2]
        if self._current_trial is None or prev_trial.value <= self._current_trial.value:
            probability = 1.0
        else:
            probability = np.exp((self._current_trial.value - prev_trial.value) / self._temperature)
        self._temperature *= 0.9  # Decrease temperature.

        # Transit the current state if the previous result is accepted.
        if self._rng.uniform(0, 1) < probability:
            self._current_trial = prev_trial

        # Sample parameters from the neighborhood of the current point.
        #
        # The sampled parameters will be used during the next execution of
        # the objective function passed to the study.
        params = {}
        for param_name, param_distribution in search_space.items():
            if not isinstance(param_distribution, optuna.distributions.UniformDistribution):
                raise NotImplementedError('Only suggest_uniform() is supported')

            current_value = self._current_trial.params[param_name]
            width = (param_distribution.high - param_distribution.low) * 0.1
            neighbor_low = max(current_value - width, param_distribution.low)
            neighbor_high = min(current_value + width, param_distribution.high)
            params[param_name] = self._rng.uniform(neighbor_low, neighbor_high)

        return params

    #
    # The rest is boilerplate code and unrelated to SA algorithm.
    #
    def infer_relative_search_space(self, study, trial):
        return optuna.samplers.intersection_search_space(study)

    def sample_independent(self, study, trial, param_name, param_distribution):
        independent_sampler = optuna.samplers.RandomSampler()
        return independent_sampler.sample_independent(study, trial, param_name, param_distribution)

注解

为了代码的简洁性,上面的实现没有支持一些特性 (比如 maximization). 如果你对如何实现这些特性感兴趣,请看 examples/samplers/simulated_annealing.py.

你可以像使用内置的 sampler 一样使用 SimulatedAnnealingSampler:

def objective(trial):
    x = trial.suggest_uniform('x', -10, 10)
    y = trial.suggest_uniform('y', -5, 5)
    return x**2 + y

sampler = SimulatedAnnealingSampler()
study = optuna.create_study(sampler=sampler)
study.optimize(objective, n_trials=100)

在上面这个优化过程中,参数 xy 的值都是由 SimulatedAnnealingSampler.sample_relative 方法采样得出的。

注解

严格意义上说,在第一个 trial 中,SimulatedAnnealingSampler.sample_independent 用于采样参数值。因为,如果没有已经完成的 trial 的话, SimulatedAnnealingSampler.infer_relative_search_space 中的 intersection_search_space() 是无法对搜索空间进行推断的。