在 MLflow 中利用子运行进行超参数调优
在机器学习领域,超参数调优是模型优化的核心任务。这个过程涉及使用不同的参数进行多次运行,以找出最有效的组合,最终提升模型性能。然而,这可能会产生大量的运行记录,使得跟踪、组织和有效地比较这些实验变得充满挑战。
MLflow 提供了一种结构化的方法来管理这种复杂性,从而简化了大量数据的问题。在本笔记本中,我们将探讨 MLflow 中的 父运行和子运行 概念,这项功能为组织运行提供了一种分层结构。这种层级结构允许我们将一组运行捆绑到一个父运行之下,使得分析和比较不同超参数组合的结果变得更加易于管理和直观。这种结构在理解和可视化超参数调优过程的结果方面尤其有益。
在本笔记本中,我们将
- 理解 MLflow 中父运行和子运行的用法和优势。
- 通过一个实际示例,演示在没有和有子运行的情况下组织运行。
- 观察子运行如何帮助有效地跟踪和比较不同参数组合的结果。
- 演示进一步的改进,即让父运行维护子运行迭代中的最佳条件的状y态。
开始时不使用子运行
在深入了解结构化的父运行和子运行世界之前,让我们先观察一下不使用 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 中比较不同父运行下的最佳性能模型。
您的任务:
- 修改
execute_tuning函数,使其为每个父运行记录在其所有子运行中找到的最佳(最小)metric1。 - 除了最佳
metric1之外,还要记录产生此最佳metric1的参数param1和param2。 - 确保
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))
)
)