MLOps极致细节:11. MLFlow 超参数调参案例: 随机搜索调参(附代码)

MLOps极致细节:11. MLFlow 超参数调参案例: 随机搜索调参(附代码)

本章节将详细解释如何通过随机搜索,基于MLFlow以及keras的端到端深度学习流程。相关主要代码参见search_random.py文件。关于整个系列案例如何运行,请参见博客:MLOps极致细节:9. MLFlow 超参数调参案例综述(附代码)。相关代码参见此Gitee链接



1 代码的运行结果

MLOps极致细节:9. MLFlow 超参数调参案例综述(附代码)中,我们解释了三种运行此代码的方式:

  1. 我们把此案例相关代码克隆到本地:git clone https://gitee.com/yichaoyyds/mlflow-ex-hyperparametertunning.git。然后进入mlflow-ex-hyperparametertunning文件夹,在terminal中运行py文件,比如python search_random.py
  2. 我们把此案例相关代码克隆到本地:git clone https://gitee.com/yichaoyyds/mlflow-ex-hyperparametertunning.git。然后再按照下面章节的步骤运行mlrun指令(比如mlflow run -e random --experiment-id <individual_runs_experiment_id> ./mlflow-ex-hyperparametertunning/);
  3. 不用把代码克隆到本地。mlflow也支持直接跑git上的代码(比如mlflow run -e random --experiment-id <individual_runs_experiment_id> https://gitee.com/yichaoyyds/mlflow-ex-hyperparametertunning.git)。

注意,如果你要运行起代码,请务必先参考上述文档的步骤。

假设我们已经顺利地运行代码。运行完后,我们会看到mlruns这个文件夹。我们在terminal输入mlflow ui,我们来看一下保存在mlruns里面的数据。

在这里插入图片描述

说明:

  1. 这里的new_run就是我们新建的experiment的名字。experiment id是1;
  2. 我们看到页面最下面有许多run的结果。这些run的行数应该等于我们这个程序随机搜索的次数(实际上还要加一,有一个run是初始化);
  3. 这些run的结果第一行是parent run,剩下的是child run;

当我们点击parent run(run的结果的第一行),出现如下图:

在这里插入图片描述

说明:

  1. run command一栏中,我们可以看到完整的指令:mlflow run https://gitee.com/yichaoyyds/mlflow-ex-hyperparametertunning.git -v 01722e9a616bd0a354e9af15c0bf030429969ede -e random -b local -P epochs=32 -P max_p=2 -P max_runs=8 -P metric=rmse -P seed=97531 -P training_data=http://archive.ics.uci.edu/ml/machine-learning-databases/wine-quality/winequality-white.csv。由于我们是省略了所有的参数手动输入,而选择默认值,所以我们输入的指令就简短了许多:mlflow run -e random --experiment-id <individual_runs_experiment_id> https://gitee.com/yichaoyyds/mlflow-ex-hyperparametertunning.git
  2. 点开tag,记录了best run的run id。具体怎么写进去的在之后的代码中有详细讲解。

现在,我们回到上一个界面,然后选择需要比较的child run结果,如下图

在这里插入图片描述

然后点击Compare按钮。我们能看到下图

在这里插入图片描述

有意思的地方是,MLFlow给了我们一些图标显示,这点非常不错。将页面往下拉,我们可以比较超参数lrtest_rmse的关系:

在这里插入图片描述

以及另一个超参数momentumtest_rmse的关系:

在这里插入图片描述

还有更有意思的。如果我们选择Parallel Coordinates Plot,然后选择如下图:

在这里插入图片描述

你能够非常直观地看到不同的lrmomentum值对train_rmsevalidate_rmsetest_rmse的影响。从中,我们找到有两个run其实都还可以。然后我们分别点进去,观察每一个echo下来train_rmsevalidate_rmsetest_rmse的值:

在这里插入图片描述

在这里插入图片描述

2 参数的设定

search_random.py中,参数相关的设定代码如下:

@click.command(help="Perform grid search over train (main entry point).")
@click.option("--max-runs", type=click.INT, default=32, help="Maximum number of runs to evaluate.")
@click.option("--max-p", type=click.INT, default=1, help="Maximum number of parallel runs.")
@click.option("--epochs", type=click.INT, default=50, help="Number of epochs")
@click.option("--metric", type=click.STRING, default="rmse", help="Metric to optimize on.")
@click.option("--seed", type=click.INT, default=97531, help="Seed for the random generator")
@click.option("--training-data", type=click.STRING, default="http://archive.ics.uci.edu/ml/machine-learning-databases/wine-quality/winequality-white.csv",\
    help="Input dataset link.")

关于click的使用说明,请参见我的博客:MLOps极致细节:10. MLFlow 超参数调参案例: 基于keras的端到端深度学习流程(附代码)

MLproject文件中,我们也有相关的参数赋值:

# Use random search to optimize hyperparams of the train entry_point.
  random:
    parameters:
      training_data: {type: string, default: "http://archive.ics.uci.edu/ml/machine-learning-databases/wine-quality/winequality-white.csv"}
      max_runs: {type: int, default: 8}
      max_p: {type: int, default: 2}
      epochs: {type: int, default: 32}
      metric: {type: string, default: "rmse"}
      seed: {type: int, default: 97531}
    command: "python search_random.py --training-data {training_data}
                                             --max-runs {max_runs}
                                             --max-p {max_p}
                                             --epochs {epochs}
                                             --metric {metric}
                                             --seed {seed}"

所以,我们设定以下参数:

  • training_data:训练数据的URI地址;
  • max_runs:run的次数。每一个run的循环,我们将随机给lrmomentum赋值;
  • max_p:多线程计算,每次循环可以并行计算几个线程;
  • epochs:我们需要训练多少个loop;
  • seed:一个随机数的选择,用于np.random.seed(seed)

3 MLFlow Projects相关文件的介绍

关于MLFlow Projects的详细介绍,请参见我之前的博客:MLOps极致细节:4. MLFlow Projects 案例介绍。关于此案例的MLproject以及conda.yaml文件介绍,请参见我的博客:MLOps极致细节:10. MLFlow 超参数调参案例: 基于keras的端到端深度学习流程(附代码)

4 整体逻辑

整体上,search_random.py的功能就是随机给lrmomentum赋值,然后再运行train.py来进行模型的训练与验证。

我们先新建一个parent run

with mlflow.start_run() as run:

随机给lrmomentum赋值,并把这些值放进一个list中。这个list的格式类似[(X,X),(X,X),...,(X,X)]. List的长度为max_runs

runs = [(np.random.uniform(1e-5, 1e-1), np.random.uniform(0, 1.0)) for _ in range(max_runs)]

然后我们打开多线程,对不同的lrmomentum值进行模型的训练:

with ThreadPoolExecutor(max_workers=max_p) as executor:
_ = executor.map(
    new_eval(epochs, experiment_id, null_train_loss, null_val_loss, null_test_loss),
    runs,
)

对于每一个训练,我们都会新建一个child run,然后运行train.py的程序:

with mlflow.start_run(nested=True) as child_run:
    p = mlflow.projects.run(
        run_id=child_run.info.run_id,
        uri=".",
        entry_point="train",
        parameters={
            "training_data": training_data,
            "epochs": str(nepochs),
            "learning_rate": str(lr),
            "momentum": str(momentum),
            "seed": str(seed),
        },
        experiment_id=experiment_id,
        synchronous=False,
    )

运行完后,我们会选择validation rmse值最低的那个run最为best run:

for r in runs:
    if r.data.metrics["val_rmse"] < best_val_valid:
        best_run = r
        best_val_train = r.data.metrics["train_rmse"]
        best_val_valid = r.data.metrics["val_rmse"]
        best_val_test = r.data.metrics["test_rmse"]

在代码中,我写了详细的注释,感兴趣的话可以过一遍代码。

5 ThreadPoolExecutor的使用

从Python3.2开始,标准库为我们提供了concurrent.futures模块,它提供了ThreadPoolExecutor(线程池)和ProcessPoolExecutor(进程池)两个类。

这里我们不对ThreadPoolExecutor本身做详细描述,感兴趣的朋友可以自行bing一下。这里列举一个ThreadPoolExecutormap的例子。

map(fn, *iterables, timeout=None)
fn: 第一个参数 fn 是需要线程执行的函数;
iterables:第二个参数接受一个可迭代对象;
timeout: 第三个参数 timeout 跟 wait() 的 timeout 一样,但由于 map 是返回线程执行的结果,如果 timeout小于线程执行时间会抛异常 TimeoutError。

map的用法:

import time
from concurrent.futures import ThreadPoolExecutor

def spider(page):
    time.sleep(page)
    return page

start = time.time()
executor = ThreadPoolExecutor(max_workers=4)

i = 1
for result in executor.map(spider, [2, 3, 1, 4]):
    print("task{}:{}".format(i, result))
    i += 1

返回

task1:2
task2:3
task3:1
task4:4

6 new_eval函数的解读

其实new_eval里面就一个eval函数,我们看到,new_eval返回的也是eval。这么写的原因,又要回到上个段落ThreadPoolExecutormap的写法:map(fn, *iterables, timeout=None)。我们epochs, experiment_id, null_train_loss, null_val_loss, null_test_loss这些参数都是不变的,变得是存在runs里面的两个超参数。所以其实每次map的对象是eval这个函数。以下为此函数的代码:

def eval(parms):
    lr, momentum = parms
    with mlflow.start_run(nested=True) as child_run:
        p = mlflow.projects.run(
            run_id=child_run.info.run_id,
            uri=".",
            entry_point="train",
            parameters={
                "training_data": training_data,
                "epochs": str(nepochs),
                "learning_rate": str(lr),
                "momentum": str(momentum),
                "seed": str(seed),
            },
            experiment_id=experiment_id,
            synchronous=False,
        )
        succeeded = p.wait()
        mlflow.log_params({"lr": lr, "momentum": momentum})
    if succeeded:
        training_run = tracking_client.get_run(p.run_id)
        metrics = training_run.data.metrics
        # cap the loss at the loss of the null model
        train_loss = min(null_train_loss, metrics[train_metric])
        val_loss = min(null_val_loss, metrics[val_metric])
        test_loss = min(null_test_loss, metrics[test_metric])
    else:
        # run failed => return null loss
        tracking_client.set_terminated(p.run_id, "FAILED")
        train_loss = null_train_loss
        val_loss = null_val_loss
        test_loss = null_test_loss
    mlflow.log_metrics(
        {
            "train_{}".format(metric): train_loss,
            "val_{}".format(metric): val_loss,
            "test_{}".format(metric): test_loss,
        }
    )
    return p.run_id, train_loss, val_loss, test_loss

首先,当我们每次运行这个函数的时候,一个新的child run会被新建with mlflow.start_run(nested=True) as child_run:,这也就是为什么我们运行完软件后,会在experiment id的文件夹下看到许多子文件夹的原因,每一个子文件夹都代表一个child run。

然后,系统就运行train.py这个文件了,当然,也是以mlflow project的形式:mlflow.projects.run。我们点进去看一下这个run函数的定义:

def run(
    uri,
    entry_point="main",
    version=None,
    parameters=None,
    docker_args=None,
    experiment_name=None,
    experiment_id=None,
    backend="local",
    backend_config=None,
    use_conda=True,
    storage_dir=None,
    synchronous=True,
    run_id=None,

需要主义的是,除了uri之外,其他的参数都有默认值。

这里需要注意uri的写法。

情况一:如果我们直接通过git来run这个MLFlow Project,即:mlflow run -e random --experiment-id <hyperparam_experiment_id> https://gitee.com/yichaoyyds/mlflow-ex-hyperparametertunning.git。这个情况下,设置 uri=".",否则会报错。

情况二:如果我们先把这个git clone下来,然后在本地运行这个MLFlow Project,即:mlflow run -e random --experiment-id <hyperparam_experiment_id> ./mlflow-ex-hyperparametertunning。这个情况下设置uri为"."会报错:stderr: 'fatal: 'XXX/mlflow-ex-hyperparametertunning' does not appear to be a git repository。这个情况下,设置 uri="https://gitee.com/yichaoyyds/mlflow-ex-hyperparametertunning.git"

如果run成功,那么最终函数返回p.run_id, train_loss, val_loss, test_loss

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

破浪会有时

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值