常见问题

我可以将 Optuna 与 X 一起使用吗? (其中 X 是您喜欢的 ML 库)

Optuna 与大多数 ML 库兼容,并且很容易与之一起使用 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 与 InMemoryStorage 一起工作

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 后端保存/恢复研究

如何保存和恢复研究?

有两种方法来持久化研究,这取决于您是使用 InMemoryStorage(默认)还是远程数据库 (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 版本之间保存/重新加载研究,请使用 RDB 并根据需要升级存储模式。如果您正在使用 RDB,请参阅 使用 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 将超参数值及其对应的目标值保存到存储中,但它会丢弃中间对象,例如机器学习模型和神经网络权重。

要保存模型或权重,我们建议利用 Optuna 内置的 ArtifactStore。例如,您可以按如下方式使用 upload_artifact()

base_path = "./artifacts"
os.makedirs(base_path, exist_ok=True)
artifact_store = optuna.artifacts.FileSystemArtifactStore(base_path=base_path)

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 the model using ArtifactStore
    with open("model.pickle", "wb") as fout:
        pickle.dump(clf, fout)
    artifact_id = optuna.artifacts.upload_artifact(
        artifact_store=artifact_store,
        file_path="model.pickle",
        study_or_trial=trial.study,
    )
    trial.set_user_attr("artifact_id", artifact_id)
    return 1.0 - accuracy_score(y_valid, clf.predict(X_valid))

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

要检索模型或权重,您可以使用 get_all_artifact_meta()download_artifact() 列出并下载它们,如下所示

# List all models
for artifact_meta in optuna.artifacts.get_all_artifact_meta(study_or_trial=study):
    print(artifact_meta)
# Download the best model
trial = study.best_trial
best_artifact_id = trial.user_attrs["artifact_id"]
optuna.artifacts.download_artifact(
    artifact_store=artifact_store,
    file_path='best_model.pickle',
    artifact_id=best_artifact_id,
)

有关更全面的指南,请参阅 ArtifactStore 教程

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

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

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

为了使 HyperbandPruner 的剪枝可重现,除了 seed 参数外,请指定 Study 的固定 study_name

然而,有两个注意事项。

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

其次,如果您的目标函数以非确定性方式运行(即,即使建议了相同的参数,它也不会返回相同的值),则无法重现优化。为了解决这个问题,如果您的优化目标(例如,一个 ML 库)提供了使行为确定性的选项(例如,随机种子),请设置该选项。

如何处理试验中的异常?

未能捕获异常而引发异常的试验将被视为失败,即状态为 FAIL

默认情况下,除了目标函数中引发的 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(“目标中的测试错误。”)

1

TrialState.COMPLETE

1269

1

另请参阅

optimize() 中的 catch 参数。

如何处理试验返回的 NaNs?

返回 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.

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

由于参数搜索空间在每次调用 suggestion 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)?

如果随着运行更多试验而内存占用增加,请尝试定期运行垃圾收集器。在调用 optimize() 时将 gc_after_trial 指定为 True,或者在回调函数内部调用 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。请注意,上述示例类似于在目标函数内部运行垃圾收集器,区别在于即使引发了错误(包括 TrialPruned),也会调用 gc.collect()

注意

ChainerMNStudy 目前不提供 gc_after_trialoptimize() 的回调函数。使用此类时,您必须在目标函数内部调用垃圾收集器。

如何仅在最佳值更新时输出日志?

下面是如何将 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(或分布式优化的其他形式)优化目标函数时,此回调可能会显示不正确的值,因为它对存储的读写容易发生竞态条件。

如何建议代表比例(即符合 Dirichlet 分布)的变量?

当您想建议代表比例的 \(n\) 个变量,即满足对于任意 \(k\) 都有 \(0 \le p[k] \le 1\)\(p[0] + p[1] + ... + p[n-1] = 1\)\(p[0], p[1], ..., p[n-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")

这种方法是合理的,原因如下:首先,如果我们对从区间 \([0, 1]\) 上的均匀分布 \(Uni(0, 1)\) 中采样的变量 \(u\) 应用变换 \(x = - \log (u)\),则变量 \(x\) 将遵循尺度参数为 \(1\) 的指数分布 \(Exp(1)\)。此外,对于独立地遵循尺度参数为 \(1\) 的指数分布的 \(n\) 个变量 \(x[0], ..., x[n-1]\),通过 \(p[i] = x[i] / \sum_i x[i]\) 对它们进行归一化,向量 \(p\) 将遵循尺度参数为 \(\alpha = (1, ..., 1)\) 的 Dirichlet 分布 \(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. 单节点的双进程并行化

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

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

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

2. 单节点的多进程并行化

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

有关第 2 项的更多信息,请参阅 TutorialEasyParallelization

3. 多节点的多进程并行化

这可以通过使用客户端/服务器 RDB(如 PostgreSQL 和 MySQL)来实现。但是,如果您所处的环境无法安装客户端/服务器 RDB,则无法运行多节点的多进程并行化。

有关第 3 项的更多信息,请参阅 TutorialEasyParallelization

如何解决使用 SQLite3 进行并行优化时发生的错误?

我们绝不推荐 SQLite3 用于并行优化,原因如下。

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

  • 正如 SQLAlchemy 文档 中所述,SQLite3(和 pysqlite 驱动程序)不支持高级别的并发。您可能会遇到“数据库被锁定”的错误,这发生在当一个线程或进程对数据库连接(实际上是文件句柄)持有独占锁,而另一个线程超时等待锁释放时。不过,您可以通过类似 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 未来可能会改变。

运行试验的进程可能会意外终止,通常是由集群环境中的作业调度器引起的。如果试验意外终止,它们将以 RUNNING 状态留在存储中,直到我们手动删除或更新其状态。对于这种情况,Optuna 支持使用心跳机制监控试验。使用心跳机制,如果运行试验的进程意外终止,Optuna 将自动将该进程上正在运行的试验状态从 RUNNING 更改为 FAIL

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 码的概念。

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

Lehmer 码不仅提供了将排列唯一编码为整数空间的方法,而且还具有一些理想的属性。例如,Lehmer 码条目之和等于将相应排列变换为恒等排列所需的最小相邻转置次数。此外,两个排列的编码的字典序与原始序列的字典序相同。因此,Lehmer 码在某种程度上保留了排列之间的“接近性”,这对于优化算法很重要。以下是解决 Euclid TSP 的 Optuna 实现示例

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)