跳到主要内容

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

下载此 Notebook

在机器学习领域,超参数调优是模型优化的核心任务。这个过程涉及执行多次具有不同参数的运行,以找到最有效的组合,最终提升模型性能。然而,这可能导致大量的运行,使得有效追踪、组织和比较这些实验变得困难。

MLflow 通过提供结构化的方法来管理这种复杂性,从而简化了大数据量的问题。在本 Notebook 中,我们将探讨 MLflow 中的父子运行概念,这是一个提供分层结构来组织运行的特性。这种层级结构允许我们将一组运行捆绑在一个父运行下,使得分析和比较不同超参数组合的结果更加易于管理和直观。这种结构对于理解和可视化超参数调优过程的结果尤其有益。

在本 Notebook 中,我们将:

  • 理解 MLflow 中父子运行的用法和益处。
  • 通过一个实际示例演示不使用子运行和使用子运行来组织运行。
  • 观察子运行如何帮助有效追踪和比较不同参数组合的结果。
  • 展示通过让父运行维护子运行迭代中的最佳条件状态来实现进一步优化。

不使用子运行开始

在深入探讨父子运行的结构化世界之前,让我们首先观察一下在 MLflow 中不使用子运行的场景。在本节中,我们执行多次具有不同参数和指标的运行,但不对它们进行关联,不将它们作为父运行的子运行。

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

运行上述代码后,您可以前往 MLflow UI 查看记录的运行。观察这些运行的组织方式(或缺乏组织方式)将有助于欣赏使用子运行提供的结构化方法,我们将在本 Notebook 的后续章节中探讨这一点。

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("http://localhost: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("http://localhost: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("http://localhost: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))
)
)