常见问题解答

我可以用 Optuna 和 X 一起使用吗?(其中 X 是你最喜欢的机器学习库)

Optuna 兼容大多数机器学习库,并且很容易与这些库一起使用 Optuna。请参考 示例

如何定义具有自身参数的目标函数?

有两种实现方式。

首先,可调用的类可以如下使用:

import optuna


class Objective:
    def __init__(self, min_x, max_x):
        # Hold this implementation specific arguments as the fields of the class.
        self.min_x = min_x
        self.max_x = max_x

    def __call__(self, trial):
        # Calculate an objective value by using the extra arguments.
        x = trial.suggest_float("x", self.min_x, self.max_x)
        return (x - 2) ** 2


# Execute an optimization by using an `Objective` instance.
study = optuna.create_study()
study.optimize(Objective(-100, 100), n_trials=100)

其次,你可以使用 lambdafunctools.partial 来创建包含额外参数的函数(闭包)。以下是使用 lambda 的示例:

import optuna

# Objective function that takes three arguments.
def objective(trial, min_x, max_x):
    x = trial.suggest_float("x", min_x, max_x)
    return (x - 2) ** 2


# Extra arguments.
min_x = -100
max_x = 100

# Execute an optimization by using the above objective function wrapped by `lambda`.
study = optuna.create_study()
study.optimize(lambda trial: objective(trial, min_x, max_x), n_trials=100)

请参阅 sklearn_additional_args.py 示例,该示例重用了数据集,而不是在每次试验执行时加载它。

我可以不使用远程RDB服务器来使用Optuna吗?

是的,这是可能的。

在最简单的形式中,Optuna 使用内存存储:

study = optuna.create_study()
study.optimize(objective)

如果你想保存和恢复学习进度,使用 SQLite 作为本地存储会很方便:

study = optuna.create_study(study_name="foo_study", storage="sqlite:///example.db")
study.optimize(objective)  # The state of `study` will be persisted to the local SQLite file.

详情请参阅 使用RDB后端保存/恢复学习

如何保存和恢复学习?

有两种持久化研究的方法,这取决于您是使用内存存储(默认)还是远程数据库(RDB)。内存中的研究可以使用 picklejoblib 像通常的 Python 对象一样保存和加载。例如,使用 joblib

study = optuna.create_study()
joblib.dump(study, "study.pkl")

并且继续学习:

study = joblib.load("study.pkl")
print("Best trial until now:")
print(" Value: ", study.best_trial.value)
print(" Params: ")
for key, value in study.best_trial.params.items():
    print(f"    {key}: {value}")

请注意,Optuna 不支持使用 pickle 在不同 Optuna 版本之间保存/重新加载。要在不同 Optuna 版本之间保存/重新加载研究,请使用 RDBs 并在必要时 升级存储模式。如果您使用的是 RDBs,请参阅 使用RDB后端保存/恢复学习 了解更多详情。

如何抑制 Optuna 的日志消息?

默认情况下,Optuna 在 optuna.logging.INFO 级别显示日志消息。您可以通过使用 optuna.logging.set_verbosity() 来更改日志级别。

例如,你可以如下方式停止显示每个试验结果:

optuna.logging.set_verbosity(optuna.logging.WARNING)

study = optuna.create_study()
study.optimize(objective)
# Logs like '[I 2020-07-21 13:41:45,627] Trial 0 finished with value:...' are disabled.

详情请参阅 optuna.logging

如何在目标函数中保存训练好的机器学习模型?

Optuna 将超参数值及其对应的目标值保存到存储中,但它会丢弃诸如机器学习模型和神经网络权重等中间对象。要保存模型或权重,请使用您所使用的机器学习库的功能。

We recommend saving optuna.trial.Trial.number with a model in order to identify its corresponding trial. For example, you can save SVM models trained in the objective function as follows:

def objective(trial):
    svc_c = trial.suggest_float("svc_c", 1e-10, 1e10, log=True)
    clf = sklearn.svm.SVC(C=svc_c)
    clf.fit(X_train, y_train)

    # Save a trained model to a file.
    with open("{}.pickle".format(trial.number), "wb") as fout:
        pickle.dump(clf, fout)
    return 1.0 - accuracy_score(y_valid, clf.predict(X_valid))


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

# Load the best model.
with open("{}.pickle".format(study.best_trial.number), "rb") as fin:
    best_clf = pickle.load(fin)
print(accuracy_score(y_valid, best_clf.predict(X_valid)))

如何获得可重复的优化结果?

为了使 Optuna 建议的参数可重现,您可以通过 samplers 实例的 seed 参数指定一个固定的随机种子,如下所示:

sampler = TPESampler(seed=10)  # Make the sampler behave in a deterministic way.
study = optuna.create_study(sampler=sampler)
study.optimize(objective)

然而,有两个注意事项。

首先,在分布式或并行模式下优化研究时,存在固有的不确定性。因此,在这种条件下很难重现相同的结果。如果您希望重现结果,我们建议按顺序执行研究的优化。

其次,如果你的目标函数表现出非确定性行为(即,即使建议相同的参数,它也不会返回相同的值),你无法重现优化过程。为了解决这个问题,如果你的优化目标(例如,一个机器学习库)提供了这样的选项,请设置一个选项(例如,随机种子)以使行为具有确定性。

试验中的异常是如何处理的?

Trials that raise exceptions without catching them will be treated as failures, i.e. with the FAIL status.

默认情况下,在目标函数中引发的所有异常(除了 TrialPruned)都会传播到 optimize() 的调用者。换句话说,当引发此类异常时,研究会中止。可能希望在剩余的试验中继续研究。为此,您可以在 optimize() 中使用 catch 参数指定要捕获的异常类型。这些类型的异常会在研究内部被捕获,并且不会进一步传播。

你可以在日志消息中找到失败的尝试。

[W 2018-12-07 16:38:36,889] Setting status of trial#0 as TrialState.FAIL because of \
the following error: ValueError('A sample error in objective.')

您还可以通过检查试验状态来找到失败的试验,如下所示:

study.trials_dataframe()

数字

状态

参数

系统属性

0

TrialState.FAIL

0

由于以下错误,将试验#0的状态设置为TrialState.FAIL:ValueError(‘目标中的测试错误。’)

toctree 是一个 reStructuredText 指令 ,这是一个非常多功能的标记。指令可以有参数、选项和内容。

TrialState.COMPLETE

1269

toctree 是一个 reStructuredText 指令 ,这是一个非常多功能的标记。指令可以有参数、选项和内容。

参见

optimize() 中的 catch 参数。

试验返回的NaN如何处理?

返回 NaN (float('nan')) 的试验被视为失败,但它们不会中止研究。

返回 NaN 的试验显示如下:

[W 2018-12-07 16:41:59,000] Setting status of trial#2 as TrialState.FAIL because the \
objective function returned nan.

当我动态改变搜索空间时会发生什么?

由于参数搜索空间是在每次调用建议API时指定的,例如 suggest_float()suggest_int(),因此可以在单个研究中,通过从不同搜索空间中采样参数来改变范围。改变时的行为由每个采样器单独定义。

备注

关于TPE采样器的讨论。 https://github.com/optuna/optuna/issues/822

如何同时使用两块GPU来评估两个试验?

如果你的优化目标支持GPU(CUDA)加速,并且你想在你的脚本 main.py 中指定使用哪个GPU,最简单的方法是设置 CUDA_VISIBLE_DEVICES 环境变量:

# On a terminal.
#
# Specify to use the first GPU, and run an optimization.
$ export CUDA_VISIBLE_DEVICES=0
$ python main.py

# On another terminal.
#
# Specify to use the second GPU, and run another optimization.
$ export CUDA_VISIBLE_DEVICES=1
$ python main.py

更多详情请参阅 CUDA C 编程指南

我如何测试我的目标函数?

当你测试目标函数时,你可能更喜欢固定的参数值而不是采样的参数值。在这种情况下,你可以使用 FixedTrial,它会根据给定的参数字典建议固定的参数值。例如,你可以将任意值的 \(x\)\(y\) 输入到目标函数 \(x + y\) 中,如下所示:

def objective(trial):
    x = trial.suggest_float("x", -1.0, 1.0)
    y = trial.suggest_int("y", -5, 5)
    return x + y


objective(FixedTrial({"x": 1.0, "y": -1}))  # 0.0
objective(FixedTrial({"x": -1.0, "y": -4}))  # -5.0

使用 FixedTrial ,你可以如下编写单元测试:

# A test function of pytest
def test_objective():
    assert 1.0 == objective(FixedTrial({"x": 1.0, "y": 0}))
    assert -1.0 == objective(FixedTrial({"x": 0.0, "y": -1}))
    assert 0.0 == objective(FixedTrial({"x": -1.0, "y": 1}))

在优化研究时,如何避免内存不足(OOM)?

If the memory footprint increases as you run more trials, try to periodically run the garbage collector. Specify gc_after_trial to True when calling optimize() or call gc.collect() inside a callback.

def objective(trial):
    x = trial.suggest_float("x", -1.0, 1.0)
    y = trial.suggest_int("y", -5, 5)
    return x + y


study = optuna.create_study()
study.optimize(objective, n_trials=10, gc_after_trial=True)

# `gc_after_trial=True` is more or less identical to the following.
study.optimize(objective, n_trials=10, callbacks=[lambda study, trial: gc.collect()])

There is a performance trade-off for running the garbage collector, which could be non-negligible depending on how fast your objective function otherwise is. Therefore, gc_after_trial is False by default. Note that the above examples are similar to running the garbage collector inside the objective function, except for the fact that gc.collect() is called even when errors, including TrialPruned are raised.

备注

ChainerMNStudy 目前不提供 gc_after_trial 也不提供 optimize() 的回调。使用此类时,您需要在目标函数内部调用垃圾收集器。

我如何才能只在最佳值更新时输出日志?

以下是如何用你自己的日志回调函数替换optuna的日志功能。实现的回调函数可以传递给 optimize()。以下是一个示例:

import optuna


# Turn off optuna log notes.
optuna.logging.set_verbosity(optuna.logging.WARN)


def objective(trial):
    x = trial.suggest_float("x", 0, 1)
    return x ** 2


def logging_callback(study, frozen_trial):
    previous_best_value = study.user_attrs.get("previous_best_value", None)
    if previous_best_value != study.best_value:
        study.set_user_attr("previous_best_value", study.best_value)
        print(
            "Trial {} finished with best value: {} and parameters: {}. ".format(
            frozen_trial.number,
            frozen_trial.value,
            frozen_trial.params,
            )
        )


study = optuna.create_study()
study.optimize(objective, n_trials=100, callbacks=[logging_callback])

请注意,由于此回调函数在读写存储时容易发生竞态条件,因此在尝试使用 ``n_jobs!=1``(或其他形式的分布式优化)优化目标函数时,可能会显示不正确的值。

如何建议表示比例的变量,即符合狄利克雷分布的变量?

当你想要建议 \(n\) 个变量来表示比例,即 \(p[0], p[1], ..., p[n-1]\),它们满足 \(0 \le p[k] \le 1\) 对于任何 \(k\) 并且 \(p[0] + p[1] + ... + p[n-1] = 1\),可以尝试以下方法。例如,这些变量可以用作插值损失函数时的权重。这些变量符合平坦的 Dirichlet 分布

import numpy as np
import matplotlib.pyplot as plt
import optuna


def objective(trial):
    n = 5
    x = []
    for i in range(n):
        x.append(- np.log(trial.suggest_float(f"x_{i}", 0, 1)))

    p = []
    for i in range(n):
        p.append(x[i] / sum(x))

    for i in range(n):
        trial.set_user_attr(f"p_{i}", p[i])

    return 0

study = optuna.create_study(sampler=optuna.samplers.RandomSampler())
study.optimize(objective, n_trials=1000)

n = 5
p = []
for i in range(n):
    p.append([trial.user_attrs[f"p_{i}"] for trial in study.trials])
axes = plt.subplots(n, n, figsize=(20, 20))[1]

for i in range(n):
    for j in range(n):
        axes[j][i].scatter(p[i], p[j], marker=".")
        axes[j][i].set_xlim(0, 1)
        axes[j][i].set_ylim(0, 1)
        axes[j][i].set_xlabel(f"p_{i}")
        axes[j][i].set_ylabel(f"p_{j}")

plt.savefig("sampled_ps.png")

这种方法的合理性如下:首先,如果我们将变换 \(x = - \log (u)\) 应用于从均匀分布 \(Uni(0, 1)\) 在区间 \([0, 1]\) 中采样的变量 \(u\),变量 \(x\) 将服从尺度参数为 \(1\) 的指数分布 \(Exp(1)\)。此外,对于独立地服从尺度参数为 \(1\) 的指数分布的 \(n\) 个变量 \(x[0], ..., x[n-1]\),通过 \(p[i] = x[i] / \sum_i x[i]\) 对其进行归一化,向量 \(p\) 服从尺度参数为 \(\alpha = (1, ..., 1)\) 的狄利克雷分布 \(Dir(\alpha)\)。你可以通过计算雅可比行列式的元素来验证这一变换。

如何使用一些约束条件优化模型?

当你想要优化一个带有约束的模型时,可以使用以下类:TPESamplerNSGAIISamplerBoTorchSampler。以下示例是使用 NSGAIISampler 对 Binh 和 Korn 函数(一个多目标优化问题)进行约束优化的基准测试。该问题有两个约束条件 \(c_0 = (x-5)^2 + y^2 - 25 \le 0\)\(c_1 = -(x - 8)^2 - (y + 3)^2 + 7.7 \le 0\),并找到满足这些约束条件的最优解。

import optuna


def objective(trial):
    # Binh and Korn function with constraints.
    x = trial.suggest_float("x", -15, 30)
    y = trial.suggest_float("y", -15, 30)

    # Constraints which are considered feasible if less than or equal to zero.
    # The feasible region is basically the intersection of a circle centered at (x=5, y=0)
    # and the complement to a circle centered at (x=8, y=-3).
    c0 = (x - 5) ** 2 + y ** 2 - 25
    c1 = -((x - 8) ** 2) - (y + 3) ** 2 + 7.7

    # Store the constraints as user attributes so that they can be restored after optimization.
    trial.set_user_attr("constraint", (c0, c1))

    v0 = 4 * x ** 2 + 4 * y ** 2
    v1 = (x - 5) ** 2 + (y - 5) ** 2

    return v0, v1


def constraints(trial):
    return trial.user_attrs["constraint"]


sampler = optuna.samplers.NSGAIISampler(constraints_func=constraints)
study = optuna.create_study(
    directions=["minimize", "minimize"],
    sampler=sampler,
)
study.optimize(objective, n_trials=32, timeout=600)

print("Number of finished trials: ", len(study.trials))

print("Pareto front:")

trials = sorted(study.best_trials, key=lambda t: t.values)

for trial in trials:
    print("  Trial#{}".format(trial.number))
    print(
        "    Values: Values={}, Constraint={}".format(
            trial.values, trial.user_attrs["constraint"][0]
        )
    )
    print("    Params: {}".format(trial.params))

如果你对 BoTorchSampler 的示例感兴趣,请参考 这个示例代码

有两种约束优化,一种是软约束,另一种是硬约束。软约束不必满足,但如果它们未被满足,目标函数会受到惩罚。另一方面,硬约束必须被满足。

Optuna 采用了软约束,**不**支持硬约束。换句话说,Optuna **不**内置硬约束的采样器。

如何并行化优化?

并行化的变体包括以下三种情况。

  1. 单节点多线程并行化

  2. 单节点多进程并行化

  3. 多节点多进程并行化

1. 单节点多线程并行化

可以通过设置参数 n_jobsoptuna.study.Study.optimize() 中实现并行化。然而,由于 GIL 的存在,Python 代码不会因此变得更快,因为 optuna.study.Study.optimize()n_jobs!=1 时使用多线程。

在优化过程中,它在某些有限的情况下会更快,例如等待其他服务器请求或使用 numpy 进行 C/C++ 处理等,但在其他情况下不会更快。

有关1.的更多信息,请参阅APIReference

2. 单节点多进程并行化

这可以通过使用 JournalFileBackend 或客户端/服务器 RDBs(如 PostgreSQL 和 MySQL)来实现。

有关第2点的更多信息,请参见 TutorialEasyParallelization

3. 多节点多进程并行化

这可以通过使用客户端/服务器关系数据库(如 PostgreSQL 和 MySQL)来实现。然而,如果你处于无法安装客户端/服务器关系数据库的环境中,你将无法在多个节点上运行多进程并行化。

有关第3点的更多信息,请参见 TutorialEasyParallelization

如何在执行SQLite3的并行优化时解决出现的错误?

基于以下原因,我们绝不推荐在并行优化中使用SQLite3。

  • 为了并发评估由 enqueue_trial() 入队的试验,RDBStorage 使用了 SELECT … FOR UPDATE 语法,这在 SQLite3 中是不支持的。

  • SQLAlchemy 文档 所述,SQLite3(及 pysqlite 驱动)不支持高并发。您可能会遇到“数据库被锁定”的错误,这是由于一个线程或进程对数据库连接(实际上是一个文件句柄)持有独占锁,而另一个线程在等待锁释放时超时。您可以增加默认的 timeout 值,例如 optuna.storages.RDBStorage(“sqlite:///example.db”, engine_kwargs={“connect_args”: {“timeout”: 20.0}})

  • 对于通过NFS进行的分布式优化,SQLite3不能像在 sqlite.org的FAQ部分 中描述的那样工作。

如果你想在这些场景中使用基于文件的Optuna存储,请考虑使用 JournalFileBackend

import optuna
from optuna.storages import JournalStorage
from optuna.storages.journal import JournalFileBackend

storage = JournalStorage(JournalFileBackend("optuna_journal_storage.log"))

study = optuna.create_study(storage=storage)
...

详情请参见 Medium 博客文章

我可以监控试验并在它们意外终止时自动标记为失败吗?

备注

心跳机制是实验性的。API 将在未来发生变化。

A process running a trial could be killed unexpectedly, typically by a job scheduler in a cluster environment. If trials are killed unexpectedly, they will be left on the storage with their states RUNNING until we remove them or update their state manually. For such a case, Optuna supports monitoring trials using heartbeat mechanism. Using heartbeat, if a process running a trial is killed unexpectedly, Optuna will automatically change the state of the trial that was running on that process to FAIL from RUNNING.

import optuna

def objective(trial):
    (Very time-consuming computation)

# Recording heartbeats every 60 seconds.
# Other processes' trials where more than 120 seconds have passed
# since the last heartbeat was recorded will be automatically failed.
storage = optuna.storages.RDBStorage(url="sqlite:///:memory:", heartbeat_interval=60, grace_period=120)
study = optuna.create_study(storage=storage)
study.optimize(objective, n_trials=100)

备注

心跳应与 optimize() 一起使用。如果你使用 ask()tell(),请通过显式调用 tell() 来更改已终止试验的状态。

你还可以执行一个回调函数来处理失败的试验。Optuna 提供了一个回调来重试失败的试验,即 RetryFailedTrialCallback。请注意,回调在每次试验开始时被调用,这意味着 RetryFailedTrialCallback 将在新试验开始评估时重试失败的试验。

import optuna
from optuna.storages import RetryFailedTrialCallback

storage = optuna.storages.RDBStorage(
    url="sqlite:///:memory:",
    heartbeat_interval=60,
    grace_period=120,
    failed_trial_callback=RetryFailedTrialCallback(max_retry=3),
)

study = optuna.create_study(storage=storage)

如何处理排列作为参数?

尽管使用现有的API处理像排列这样的组合搜索空间并不直接,但存在一种方便的技术来处理它们。它涉及将 \(n\) 个项目的排列搜索空间重新参数化为独立的 \(n\) 维整数搜索空间。这种技术基于 Lehmer code 的概念。

一个序列的 Lehmer 码是相同大小的整数序列,其第 \(i\) 个条目表示排列的第 \(i\) 个条目在其后有多少个逆序对。换句话说,Lehmer 码的第 \(i\) 个条目表示在原始序列中位于第 \(i\) 个条目之后且小于第 \(i\) 个条目的条目数量。例如,排列 \((3, 1, 4, 2, 0)\) 的 Lehmer 码是 \((3, 1, 2, 1, 0)\)

Lehmer 码不仅提供了一种将排列唯一编码为整数空间的方法,而且它还具有一些理想的性质。例如,Lehmer 码条目的总和等于将相应排列转换为恒等排列所需的最小相邻转置次数。此外,两个排列的编码的字典顺序与原始序列的顺序相同。因此,在某种意义上,Lehmer 码保持了排列之间的“接近性”,这对于优化算法非常重要。以下是使用 Optuna 解决欧几里得旅行商问题(Euclid TSP)的实现示例:

import numpy as np

import optuna


def decode(lehmer_code: list[int]) -> list[int]:
    """Decode Lehmer code to permutation.

    This function decodes Lehmer code represented as a list of integers to a permutation.
    """
    all_indices = list(range(n))
    output = []
    for k in lehmer_code:
        value = all_indices[k]
        output.append(value)
        all_indices.remove(value)
    return output


# Euclidean coordinates of cities for TSP.
city_coordinates = np.array(
    [[0.0, 0.0], [1.0, 0.0], [0.0, 1.0], [1.0, 1.0], [2.0, 2.0], [-1.0, -1.0]]
)
n = len(city_coordinates)


def objective(trial: optuna.Trial) -> float:
    # Suggest a permutation in the Lehmer code representation.
    lehmer_code = [trial.suggest_int(f"x{i}", 0, n - i - 1) for i in range(n)]
    permutation = decode(lehmer_code)

    # Calculate the total distance of the suggested path.
    total_distance = 0.0
    for i in range(n):
        total_distance += np.linalg.norm(
            city_coordinates[permutation[i]] - city_coordinates[np.roll(permutation, 1)[i]]
        )
    return total_distance


study = optuna.create_study()
study.optimize(objective, n_trials=10)
lehmer_code = study.best_params.values()
print(decode(lehmer_code))

如何忽略重复的样本?

Optuna 有时可能会建议过去评估过的参数,如果您想避免这个问题,可以尝试以下解决方法:

import optuna
from optuna.trial import TrialState


def objective(trial):
    # Sample parameters.
    x = trial.suggest_int("x", -5, 5)
    y = trial.suggest_int("y", -5, 5)
    # Fetch all the trials to consider.
    # In this example, we use only completed trials, but users can specify other states
    # such as TrialState.PRUNED and TrialState.FAIL.
    states_to_consider = (TrialState.COMPLETE,)
    trials_to_consider = trial.study.get_trials(deepcopy=False, states=states_to_consider)
    # Check whether we already evaluated the sampled `(x, y)`.
    for t in reversed(trials_to_consider):
        if trial.params == t.params:
            # Use the existing value as trial duplicated the parameters.
            return t.value

    # Compute the objective function if the parameters are not duplicated.
    # We use the 2D sphere function in this example.
    return x ** 2 + y ** 2


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

如何删除上传到研究中的所有工件?

Optuna 支持 artifacts 用于优化期间的大数据存储。在进行大量实验后,您可能希望删除优化期间存储的工件。

我们强烈建议为每个研究创建一个新的目录或存储桶,以便通过删除目录或存储桶可以完全移除与研究相关的所有工件。

然而,如果需要从Python脚本中移除工件,用户可以使用以下代码:

警告

add_trial()copy_study() 不会复制与 StudyTrial 关联的工件文件。请确保 不要 从源研究或试验中删除这些工件。未能这样做可能会导致意外行为,因为当用户从外部调用 remove() 时,Optuna 不保证预期的行为。由于 Optuna 软件设计的原因,很难正式支持删除功能,我们也不计划在未来支持此功能。

from optuna.artifacts import get_all_artifact_meta


def remove_artifacts(study, artifact_store):
    # NOTE: ``artifact_store.remove`` is discouraged to use because it is an internal feature.
    storage = study._storage
    for trial in study.trials:
        for artifact_meta in get_all_artifact_meta(trial, storage=storage):
            # For each trial, remove the artifacts uploaded to ``base_path``.
            artifact_store.remove(artifact_meta.artifact_id)

    for artifact_meta in get_all_artifact_meta(study):
        # Remove the artifacts uploaded to ``base_path``.
        artifact_store.remove(artifact_meta.artifact_id)