常见问题

某某库可以和 Optuna 配合使用吗?(某某是你常用的机器学习库)

Optuna 和绝大多数机器学习库兼容,并且很容易同他们配合使用。参见 examples.

如何定义带有额外参数的目标函数?

有两种方法可以实现这类函数。

首先,如下例所示,可调用的 objective 类具有这个功能:

import optuna


class Objective(object):
    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)

其次,你可以用 lambda 或者 functools.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)

Please also refer to sklearn_addtitional_args.py example, which reuses the dataset instead of loading it in each trial execution.

没有远程 RDB 的情况下可以使用 Optuna 吗?

可以。

在最简单的情况下,Optuna 使用内存 (in-memory) 存储:

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

如果想保存和恢复 study 的话,你可以轻松地将 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 后端保存/恢复 Study .

如何保存和恢复 study?

有两种方法可以将 study 持久化。具体采用哪种取决于你是使用内存存储 (in-memory) 还是远程数据库存储 (RDB). 通过 pickle 或者 joblib, 采用了内存存储的 study 可以和普通的 Python 对象一样被存储和加载。比如用 joblib 的话:

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

恢复 study:

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}")

如果你用的是 RDB, 具体细节请参考 用 RDB 后端保存/恢复 Study.

如何禁用 Optuna 的日志信息?

默认情况下,Optuna 打印处于 optuna.logging.INFO 层级的日志信息。通过设置 optuna.logging.set_verbosity(), 你可以改变这个层级。

比如,下面的代码可以终止打印每一个trial的结果:

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 会保存超参数和对应的目标函数值,但是它不会存储诸如机器学习模型或者网络权重这样的中间数据。要保存模型或者权重的话,请利用你正在使用的机器学习库提供的对应功能。

在保存模型的时候,我们推荐将 optuna.trial.Trial.number 一同存储。这样易于之后确认对应的 trial.比如,你可以用以下方式在目标函数中保存训练好的 SVM 模型:

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 生成的参数可复现的话,你可以通过设置 RandomSampler 或者 TPESampler 中的参数 seed 来指定一个固定的随机数种子:

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

但是这么做的需要注意以下两点。

首先,如果一个 study 的优化过程本身是分布式的或者并行的,那么这个过程中存在着固有的不确定性。因此,在这种情况下我们很难复现出同样的结果。如果你想复现结果的话,我们建议用顺序执行的方式来优化你的 study.

其次,如果你的目标函数的行为本身就是不确定的(也就是说,即使送入同样的参数,其返回值也不是唯一的),那么你就无法复现这个优化过程。要解决这个问题的话,请设置一个选项(比如随机数种子)来让你的优化目标的行为变成确定性的,前提是你用的机器学习库支持这一功能。

Trial 是如何处理抛出异常的?

那些抛出异常却没有对应的捕获机制的 trial 会被视作失败的 trial, 也就是处于 FAIL 状态的 trial.

在默认情况下,除了目标函数中抛出的 TrialPruned, 其他所有异常都会被传回给调用函数 optimize().换句话说,当此类异常被抛出时,对应的 study 就会被终止。但有时候我们希望能用剩余的 trial 将该 study 继续下去。要这么做的话,你得通过 optimize() 函数中的 catch 参数来指定要捕获的异常类型。这样,此类异常就会在 study 内部被捕获,而不会继续向外层传递。

你可以在日志信息里找到失败的 trial.

[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.')

你也可以通过查看 trial 的状态来找到它们:

study.trials_dataframe()

number

state

value

params

system_attrs

0

TrialState.FAIL

0

Setting status of trial#0 as TrialState.FAIL because of the following error: ValueError(‘A test error in objective.’)

1

TrialState.COMPLETE

1269

1

参见

The catch argument in optimize().

Trial 返回的 NaN 是如何处理的?

返回 NaN 的 trial 被视为失败的 trial, 但是它们并不会导致 study 被终止。

这些返回 NaN 的 trial 在日志里长这样:

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

动态地改变搜索空间会导致怎样的结果?

Since parameters search spaces are specified in each call to the suggestion API, e.g. suggest_float() and suggest_int(), it is possible to, in a single study, alter the range by sampling parameters from different search spaces in different trials. The behavior when altered is defined by each sampler individually.

备注

关于 TPE sampler 的 讨论:https://github.com/optuna/optuna/issues/822

如何在两块 GPU 上同时跑两个 trial?

如果你的优化目标支持 GPU (CUDA) 加速,你又想指定优化所用的 GPU 的话,设置 CUDA_VISIBLE_DEVICES 环境变量可能是实现这一目标最轻松的方式了:

# On a terminal.
#
# Specify to use the first GPU, and run an optimization.
$ export CUDA_VISIBLE_DEVICES=0
$ optuna study optimize foo.py objective --study-name foo --storage sqlite:///example.db

# On another terminal.
#
# Specify to use the second GPU, and run another optimization.
$ export CUDA_VISIBLE_DEVICES=1
$ optuna study optimize bar.py objective --study-name bar --storage sqlite:///example.db

更多细节见 CUDA C Programming Guide.

如何对目标函数进行测试?

在对目标函数的测试中,我们总倾向于使用固定的,而不是随机采样的参数。这时,你可以选择用 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}))

在优化时, 我该如何避免耗尽内存 (running out of memory, OOM)?

如果内存使用量随着你运行更多的 trial 而增长,请尝试定期运行垃圾回收器。可在调用 optimize() 时指定 gc_after_trialTrue 或在回调函数中调用 gc.collect().

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()])

运行垃圾回收器是有性能损失的,取决于你的目标函数运行的速度,这种损失可能是不能忽略的。因此,gc_after_trial 在默认情况下是 False. 注意,上面这个例子类似于在目标函数内运行垃圾回收器,不同之处在于 gc.collect() 哪怕在出现包括 TrialPruned 这样的错误时也会被调用。

备注

ChainerMNStudy 目前并不提供 gc_after_trial 和用于 optimize() 的回调接口。在使用该类时,你只能在目标函数内部调用垃圾回收器。