跳到主要内容

在 MLflow 中利用子运行进行超参数调优

下载此笔记本

在机器学习领域,超参数调优是模型优化的核心任务。 这个过程包括使用不同的参数执行多次运行,以确定最有效的组合,最终提高模型性能。 但是,这可能会导致大量的运行,使得有效地跟踪、组织和比较这些实验变得具有挑战性。

MLflow 结合了简化大数据量问题的能力,提供了一种结构化的方法来管理这种复杂性。 在此笔记本中,我们将探讨 MLflow 中父运行和子运行的概念,该功能提供了一个分层结构来组织运行。 这种层次结构允许我们将一组运行捆绑在一个父运行下,使得分析和比较不同超参数组合的结果更加易于管理和直观。 这种结构被证明在理解和可视化超参数调整过程的结果方面特别有益。

在本笔记本中,我们将

  • 了解 MLflow 中父运行和子运行的用法和好处。
  • 通过一个实际示例演示如何组织没有和有子运行的运行。
  • 观察子运行如何帮助有效地跟踪和比较不同参数组合的结果。
  • 演示进一步的改进,让父运行保持来自子运行迭代的最佳条件的状态。

从没有子运行开始

在深入研究父运行和子运行的结构化世界之前,让我们首先观察一下在 MLflow 中不使用子运行的情况。 在本节中,我们使用不同的参数和指标执行多次运行,而无需将它们关联为父运行的子运行。

以下是执行五个超参数调整运行的代码。 这些运行未组织为子运行,因此,每次运行都被视为 MLflow 中的独立实体。 我们将观察这种方法在跟踪和比较运行方面带来的挑战,为后续章节中子运行的引入奠定基础。

运行上述代码后,您可以继续访问 MLflow UI 以查看记录的运行。 观察这些运行的组织(或缺乏组织)将有助于理解使用子运行提供的结构化方法,我们将在本笔记本的后续部分中进行探讨。

import random
from functools import partial
from itertools import starmap

from more_itertools import consume

import mlflow


# Define a function to log parameters and metrics
def log_run(run_name, test_no):
with mlflow.start_run(run_name=run_name):
mlflow.log_param("param1", random.choice(["a", "b", "c"]))
mlflow.log_param("param2", random.choice(["d", "e", "f"]))
mlflow.log_metric("metric1", random.uniform(0, 1))
mlflow.log_metric("metric2", abs(random.gauss(5, 2.5)))


# Generate run names
def generate_run_names(test_no, num_runs=5):
return (f"run_{i}_test_{test_no}" for i in range(num_runs))


# Execute tuning function
def execute_tuning(test_no):
# Partial application of the log_run function
log_current_run = partial(log_run, test_no=test_no)
# Generate run names and apply log_current_run function to each run name
runs = starmap(log_current_run, ((run_name,) for run_name in generate_run_names(test_no)))
# Consume the iterator to execute the runs
consume(runs)


# Set the tracking uri and experiment
mlflow.set_tracking_uri("https://:8080")
mlflow.set_experiment("No Child Runs")

# Execute 5 hyperparameter tuning runs
consume(starmap(execute_tuning, ((x,) for x in range(5))))

迭代开发模拟

很少有调整运行会孤立地进行。 通常,我们将运行许多参数组合的迭代,优化我们的搜索空间,以便在最短的执行时间内获得最佳的潜在结果。

为了获得这个有限的参数范围和条件选择集,我们将执行许多这样的测试。

# What if we need to run this again?
consume(starmap(execute_tuning, ((x,) for x in range(5))))

使用子运行改进组织

当我们继续时,焦点现在转移到 MLflow 中的子运行 的使用。 此功能带来了一种有组织的结构,从本质上解决了我们在上一节中观察到的挑战。 子运行整齐地嵌套在父运行下,提供了所有运行的清晰分层视图,从而非常方便地分析和比较结果。

使用子运行的优势:

  • 结构化视图: 在父运行下分组的子运行在 MLflow UI 中提供了干净且结构化的视图。
  • 高效筛选: 分层组织有助于高效筛选和选择,从而增强了 MLflow UI 和搜索 API 的可用性。
  • 独特命名: 对运行使用视觉上独特的命名有助于在 UI 中轻松识别和选择。

在本节中,代码经过增强,可以使用子运行。 每个 execute_tuning 函数调用都会创建一个父运行,多个子运行嵌套在该父运行下。 这些子运行使用不同的参数和指标执行。 此外,我们还加入了标签以进一步增强 MLflow 中的搜索和筛选功能。

请注意 mlflow.start_run() 函数中包含的 nested=True 参数,表明创建了子运行。 使用 mlflow.set_tag() 函数添加标签提供了额外的信息层,可用于有效地筛选和搜索运行。

让我们深入研究代码,并观察使用 MLflow 中的子运行所带来的无缝组织和增强的功能。

# Define a function to log parameters and metrics and add tag
# logging for search_runs functionality
def log_run(run_name, test_no, param1_choices, param2_choices, tag_ident):
with mlflow.start_run(run_name=run_name, nested=True):
mlflow.log_param("param1", random.choice(param1_choices))
mlflow.log_param("param2", random.choice(param2_choices))
mlflow.log_metric("metric1", random.uniform(0, 1))
mlflow.log_metric("metric2", abs(random.gauss(5, 2.5)))
mlflow.set_tag("test_identifier", tag_ident)


# Generate run names
def generate_run_names(test_no, num_runs=5):
return (f"run_{i}_test_{test_no}" for i in range(num_runs))


# Execute tuning function, allowing for param overrides,
# run_name disambiguation, and tagging support
def execute_tuning(
test_no, param1_choices=("a", "b", "c"), param2_choices=("d", "e", "f"), test_identifier=""
):
ident = "default" if not test_identifier else test_identifier
# Use a parent run to encapsulate the child runs
with mlflow.start_run(run_name=f"parent_run_test_{ident}_{test_no}"):
# Partial application of the log_run function
log_current_run = partial(
log_run,
test_no=test_no,
param1_choices=param1_choices,
param2_choices=param2_choices,
tag_ident=ident,
)
mlflow.set_tag("test_identifier", ident)
# Generate run names and apply log_current_run function to each run name
runs = starmap(log_current_run, ((run_name,) for run_name in generate_run_names(test_no)))
# Consume the iterator to execute the runs
consume(runs)


# Set the tracking uri and experiment
mlflow.set_tracking_uri("https://:8080")
mlflow.set_experiment("Nested Child Association")

# Define custom parameters
param_1_values = ["x", "y", "z"]
param_2_values = ["u", "v", "w"]

# Execute hyperparameter tuning runs with custom parameter choices
consume(starmap(execute_tuning, ((x, param_1_values, param_2_values) for x in range(5))))

定制超参数调优过程

在本段中,我们正在进一步采取超参数调整的迭代过程。 观察其他超参数调整运行的执行情况,我们在其中引入了 自定义参数选择 和用于标记的唯一标识符。

我们在做什么?

  • 自定义参数选择: 我们现在对运行采用不同的参数值(param_1_values 作为 ["x", "y", "z"]param_2_values 作为 ["u", "v", "w"])。
  • 用于标记的唯一标识符: 使用了用于标记的独特标识符 (ident),这提供了一种简单有效的方法来筛选和搜索 MLflow UI 中的这些特定运行。

它如何应用于超参数调优?

  • 参数敏感性分析: 此步骤允许我们分析模型对不同参数值的敏感性,从而有助于进行更明智和有效的调优过程。
  • 高效搜索和筛选: 使用用于标记的唯一标识符有助于在众多其他运行中高效且快速地搜索这些特定运行,从而增强了 MLflow UI 中的用户体验。

这种采用自定义参数和标记的方法增强了超参数调优过程的清晰度和效率,有助于构建更强大和优化的模型。

让我们执行代码的这一部分,并更深入地了解它在超参数调整过程中提供的见解和改进。

# Execute additional hyperparameter tuning runs with custom parameter choices
param_1_values = ["x", "y", "z"]
param_2_values = ["u", "v", "w"]
ident = "params_test_2"
consume(starmap(execute_tuning, ((x, param_1_values, param_2_values, ident) for x in range(5))))

优化超参数搜索空间

在此阶段,我们专注于 优化超参数搜索空间。 这是超参数调整过程中的关键步骤。 在对参数空间进行广泛探索之后,我们现在将搜索范围缩小到参数值的子集。

我们在做什么?

  • 子集参数值: 根据从先前运行中收集的见解,我们专注于更具体的参数值集(param_1_values 作为 ["b", "c"]param_2_values 作为 ["d", "f"])。
  • 标记运行: 使用用于标记的唯一标识符 (ident) 可确保在 MLflow UI 中轻松筛选和搜索这些运行。

它如何应用于超参数调优?

  • 集中搜索: 这种缩小的搜索范围使我们能够深入探索特定参数值集的交互和影响,从而可能产生更优化的模型。
  • 高效的资源利用: 通过将搜索重点放在参数空间中有希望的区域,它可以更有效地利用计算资源。

注意事项

虽然这种方法是超参数调整中的常用策略,但必须承认其含义。 将缩小的搜索空间中的结果与原始的、更广泛的搜索空间中的结果进行直接比较可能会产生误导。

为什么比较无效?

  • 贝叶斯调优算法的性质: 贝叶斯优化和其他调优算法通常依赖于对广泛参数空间的探索来做出明智的决策。 限制参数空间会影响这些算法的行为,从而导致有偏差或次优的结果。
  • 超参数选择值的交互: 不同的参数值对模型性能有不同的交互和影响。 缩小的搜索空间可能会错过捕获这些交互,从而导致不完整或有偏差的见解。

总之,虽然优化搜索空间对于有效且高效的超参数调整至关重要,但必须谨慎对待结果的比较,承认所涉及的复杂性和潜在偏差。

param_1_values = ["b", "c"]
param_2_values = ["d", "f"]
ident = "params_test_3"
consume(starmap(execute_tuning, ((x, param_1_values, param_2_values, ident) for x in range(5))))

挑战:记录最佳指标和参数

在真实的机器学习世界中,跟踪性能最佳的模型及其对应的参数对于轻松比较和重现至关重要。 您的挑战是增强 execute_tuning 函数,以记录每个父运行中子运行的最佳指标和参数。 这样,您可以轻松地比较 MLflow UI 中不同父运行中性能最佳的模型。

你的任务:

  1. 修改 execute_tuning 函数,以便对于每个父运行,它记录在其所有子运行中找到的最佳(最小值)metric1
  2. 除了最佳 metric1 之外,还要记录产生此最佳 metric1 的参数 param1param2
  3. 确保 execute_tuning 函数可以接受 num_child_runs 参数,以指定每个父运行要执行多少个子迭代。

这是一种常见的做法,可让您保持 MLflow 实验的组织性并易于检索,从而使模型选择过程更加顺畅和高效。

提示: 您可能希望从 log_run 函数返回值,并在 execute_tuning 函数中使用这些返回值来跟踪最佳指标和参数。

注意:

在继续下面的解决方案之前,自己尝试一下! 这是一个熟悉 MLflow 高级功能并提高 MLOps 技能的好机会。 如果您遇到困难或想比较您的解决方案,您可以向下滚动以查看可能的实现。

# Define a function to log parameters and metrics and add tag
# logging for search_runs functionality
def log_run(run_name, test_no, param1_choices, param2_choices, tag_ident):
with mlflow.start_run(run_name=run_name, nested=True) as run:
param1 = random.choice(param1_choices)
param2 = random.choice(param2_choices)
metric1 = random.uniform(0, 1)
metric2 = abs(random.gauss(5, 2.5))

mlflow.log_param("param1", param1)
mlflow.log_param("param2", param2)
mlflow.log_metric("metric1", metric1)
mlflow.log_metric("metric2", metric2)
mlflow.set_tag("test_identifier", tag_ident)

return run.info.run_id, metric1, param1, param2


# Generate run names
def generate_run_names(test_no, num_runs=5):
return (f"run_{i}_test_{test_no}" for i in range(num_runs))


# Execute tuning function, allowing for param overrides,
# run_name disambiguation, and tagging support
def execute_tuning(
test_no,
param1_choices=("a", "b", "c"),
param2_choices=("d", "e", "f"),
test_identifier="",
num_child_runs=5,
):
ident = "default" if not test_identifier else test_identifier
best_metric1 = float("inf")
best_params = None
# Use a parent run to encapsulate the child runs
with mlflow.start_run(run_name=f"parent_run_test_{ident}_{test_no}"):
# Partial application of the log_run function
log_current_run = partial(
log_run,
test_no=test_no,
param1_choices=param1_choices,
param2_choices=param2_choices,
tag_ident=ident,
)
mlflow.set_tag("test_identifier", ident)
# Generate run names and apply log_current_run function to each run name
results = list(
starmap(
log_current_run,
((run_name,) for run_name in generate_run_names(test_no, num_child_runs)),
)
)

for _, metric1, param1, param2 in results:
if metric1 < best_metric1:
best_metric1 = metric1
best_params = (param1, param2)

mlflow.log_metric("best_metric1", best_metric1)
mlflow.log_param("best_param1", best_params[0])
mlflow.log_param("best_param2", best_params[1])
# Consume the iterator to execute the runs
consume(results)


# Set the tracking uri and experiment
mlflow.set_tracking_uri("https://:8080")
mlflow.set_experiment("Parent Child Association Challenge")

param_1_values = ["a", "b"]
param_2_values = ["d", "f"]

# Execute hyperparameter tuning runs with custom parameter choices
consume(
starmap(
execute_tuning, ((x, param_1_values, param_2_values, "subset_test", 25) for x in range(5))
)
)