在 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 中轻松比较不同父运行中性能最佳的模型。
您的任务:
- 修改
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))
)
)