在 MLflow 中利用子运行进行超参数调优
在机器学习的世界里,超参数调优是模型优化的核心任务。这个过程涉及使用不同的参数进行多次运行,以找到最有效的组合,最终提升模型性能。然而,这可能导致产生大量的运行记录,使得跟踪、组织和有效比较这些实验变得具有挑战性。
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("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))
)
)