MLOps极致细节:10. MLFlow 超参数调参案例: 基于keras的端到端深度学习流程(附代码)

MLOps极致细节:10. MLFlow 超参数调参案例: 基于keras的端到端深度学习流程(附代码)

这里我们提供一些列案例,基于keras的深度学习框架对winequality-white数据进行quanlity的预测。此章节的代码基于MLFlow官网GitHub链接。

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


文章目录

  • MLOps极致细节:10. MLFlow 超参数调参案例: 基于keras的端到端深度学习流程(附代码)
    • 代码的运行结果
    • 参数的设定
    • MLFlow Projects相关文件的介绍
      • MLproject
      • conda.yaml
    • 整体逻辑
    • Python Click的使用
    • Python with语句的使用
    • Keras中的回调函数Callbacks


代码的运行结果

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

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

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

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

首先一个,我们发现,train_rmse以及val_rmse有101个值,但test_rmse只有16个值。这是因为我们参数的设置:epochs=100batch_size=16learning_rate=1e-2momentum=0.9train_rmse以及val_rmse的长度是等于epoch的,或者说,等于epoch+1,但test_rmse的数量是不固定的,因为代码中,只有当这次epoch运算出来的结果小于上一次,才会去计算test的结果,并且保存。

另外,我们可以通过ui看到每次训练/验证的结果:

MLOps极致细节:10. MLFlow 超参数调参案例: 基于keras的端到端深度学习流程(附代码)_第1张图片

可以看到,迭代并没有让梯度下降多少。

参数的设定

train.py中我们可以看到:

@click.command(
    help="Trains an Keras model on wine-quality dataset."
    "The input is expected in csv format."
    "The model and its metrics are logged with mlflow."
)
@click.option("--epochs", type=click.INT, default=100, help="Maximum number of epochs to evaluate.")
@click.option(
    "--batch-size", type=click.INT, default=16, help="Batch size passed to the learning algo."
)
@click.option("--learning-rate", type=click.FLOAT, default=1e-2, help="Learning rate.")
@click.option("--momentum", type=click.FLOAT, default=0.9, help="SGD momentum.")
@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的用法,这里我们只专注于哪些参数是我们这个文件需要设定的。另外,在MLproject文件中,

train:
    parameters:
      training_data: {type: string, default: "http://archive.ics.uci.edu/ml/machine-learning-databases/wine-quality/winequality-white.csv"}
      epochs: {type: int, default: 32}
      batch_size: {type: int, default: 16}
      learning_rate: {type: float, default: 1e-1}
      momentum: {type: float, default: .0}
      seed: {type: int, default: 97531}
    command: "python train.py --training-data {training_data}
                                    --batch-size {batch_size}
                                    --epochs {epochs}
                                    --learning-rate {learning_rate}
                                    --momentum {momentum}"

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

  • training_data:训练数据的URI地址;
  • epochs:我们需要训练多少个loop;
  • batch_size:训练的时候每个batch的大小;
  • learning_rate:第一个超参数;
  • momentum:第二个超参数;
  • seed:一个随机数的选择,用于np.random.seed(seed)

MLFlow Projects相关文件的介绍

关于MLFlow Projects的详细介绍,请参见我之前的一篇博客。这里只简单介绍相关的两个最重要的文件:MLproject以及conda.yaml

MLproject

这个文档中包含了三个entry points: train, random, hyperopt。对应的是我们这边的三个例子。这里我们只需要看train这部分。上面已经复制了对应的代码。

当我们在terminal中输入mlflow run指令的时候,实际上,我们运行的就是MLproject文件对应的某一部分。比如我们运行

mlflow run -e train --experiment-id <individual_runs_experiment_id> https://gitee.com/yichaoyyds/mlflow-ex-hyperparametertunning.git

关于mlflow run的具体细节,请参见此文档。

需要注意的是几个后缀,比如,-e后面需要跟entry point。这也是为什么上面指令写的是train的原因。另外,-P 后面跟的是参数,此外还有诸如--experiment-id--experiment-name等参数。

conda.yaml

这个文件里面包含了这个项目需要的环境依赖。由于一些依赖需要通过国内的源才能够顺利下载,所以我们对原文件做了修改,conda.yamlbk则是原文件。

这里需要注意一点,就是conda.yaml中tensorflow version的选择。在terminal中,我们输入conda search --info tensorflow,查看最新版本的tensorflow:

tensorflow 2.6.0 mkl_py39h31650da_0
-----------------------------------
file name   : tensorflow-2.6.0-mkl_py39h31650da_0.conda
name        : tensorflow
version     : 2.6.0
build       : mkl_py39h31650da_0
build number: 0
size        : 4 KB
license     : Apache 2.0
subdir      : win-64
url         : https://mirrors.tuna.tsinghua.edu.cn/anaconda/pkgs/main/win-64/tensorflow-2.6.0-mkl_py39h31650da_0.conda
md5         : f7d33568d142730388cb1508ab11e0b5
timestamp   : 2021-09-09 18:10:13 UTC
dependencies:
  - _tflow_select 2.3.0 mkl
  - python 3.9.*
  - tensorboard >=2.6.0
  - tensorflow-base 2.6.0 mkl_py39h9201259_0
  - tensorflow-estimator >=2.6.0

整体逻辑

首先,我们导入数据,并拆分成训练集,验证集以及测试集,还有归一化等的预处理。

warnings.filterwarnings("ignore")
# Data input and split into train, validate and test dataset
data = pd.read_csv(training_data, sep=";")
# Split the data into training and test sets. (0.75, 0.25) split.
train, test = train_test_split(data, random_state=seed)
train, valid = train_test_split(train, random_state=seed)
# The predicted column is "quality" which is a scalar from [3, 9]
train_x = train.drop(["quality"], axis=1).astype("float32").values
train_y = train[["quality"]].astype("float32").values
valid_x = valid.drop(["quality"], axis=1).astype("float32").values
valid_y = valid[["quality"]].astype("float32").values
test_x = test.drop(["quality"], axis=1).astype("float32").values
test_y = test[["quality"]].astype("float32").values

然后开启一个mlflow run(with mlflow.start_run()),开启循环进行模型训练。

if epochs == 0: 
    # Calculate RMSE value of train/validate/test dataset. score null model.
    eval_and_log_metrics("train", train_y, np.ones(len(train_y)) * np.mean(train_y), epoch=-1)
    eval_and_log_metrics("val", valid_y, np.ones(len(valid_y)) * np.mean(valid_y), epoch=-1)
    eval_and_log_metrics("test", test_y, np.ones(len(test_y)) * np.mean(test_y), epoch=-1)
else:
    with MLflowCheckpoint(test_x, test_y) as mlflow_logger:
        model = Sequential()
        model.add(Lambda(get_standardize_f(train_x)))
        model.add(
            Dense(
                train_x.shape[1],
                activation="relu",
                kernel_initializer="normal",
                input_shape=(train_x.shape[1],),
            )
        )
        model.add(Dense(16, activation="relu", kernel_initializer="normal"))
        model.add(Dense(16, activation="relu", kernel_initializer="normal"))
        model.add(Dense(1, kernel_initializer="normal", activation="linear"))
        model.compile(
            loss="mean_squared_error",
            optimizer=SGD(lr=learning_rate, momentum=momentum),
            metrics=[],
        )

        model.fit(
            train_x,
            train_y,
            batch_size=batch_size,
            epochs=epochs,
            verbose=1,
            validation_data=(valid_x, valid_y),
            callbacks=[mlflow_logger],
        )

最后每一个循环(echo)会产生RMSE结果(on_epoch_end中),我们会比较当前循环模型训练结果,是否比上一个循环好,即RMSE是否更低,如果更低,那么当前循环模型就是最优模型。详细介绍请参见下面章节。

Python Click的使用

在此案例中,我们使用了Click命令行工具。这里附上一些Click命令行工具的简介:

  • @click.command。使用其修饰main函数(这里的main函数指的是run函数),就可以让它成为一条命令,目前它只能接受参数–help。

  • @click.option。指定参数,有2种比较常用的写法:

    • 指定参数缩写、完整名称、自定义解析后的变量名、设置默认值和参数解释:

      @click.option('-p', '--param-name', 'custom_param_name', default='magic', help='Parameter introduce.')
      
    • 只指定完整参数名(对应变量名将按约定方式命名),指定参数类型(默认为str),未指定参数时提示用户输入:

      @click.option('--count', type=int, prompt='Input greet times', help='The times to greet.')
      

      需要注意的是,如果没有定义变量名(比如param_name),而只是定义了参数名(比如param-name),click会将参数名去掉前缀,横线改下划线传递给main函数。

  • @click.argument。一般的写法就是:@click.argument('user-name'),后面不加默认值,而且在运行.py文件的时候,必须加这个参数,比如python train.py ABB。否则会报错。另外就是,当我们使用了argument之后,help命令是不会显示对应的user-name信息的。

介绍完click的基本用法,我们可以直接输入python .\train.py --help来看一下结果。Terminal会如下显示:

Usage: train.py [OPTIONS]
  Trains an Keras model on wine-quality dataset.The input is expected in csv
  format.The model and its metrics are logged with mlflow.

Options:
  --epochs INTEGER       Maximum number of epochs to evaluate.
  --batch-size INTEGER   Batch size passed to the learning algo.
  --learning-rate FLOAT  Learning rate.
  --momentum FLOAT       SGD momentum.
  --seed INTEGER         Seed for the random generator.
  --training-data TEXT   Input dataset link.
  --help                 Show this message and exit.

Python with语句的使用

train.py中,一个我觉得写得很好的地方就是MLflowCheckpoint类的使用,它继承了keras的Callback类。下一个章节我会详细介绍这个Callback类的使用,但这里,我们先具体解释with MLflowCheckpoint(test_x, test_y) as mlflow_logger:中的with语句,以及MLflowCheckpoint__enter____exit__函数的使用。

有一些任务,可能事先需要设置,事后做清理工作。对于这种场景,Python的with语句提供了一种非常方便的处理方式。举个例子,读一个txt文件的代码,可以这么写:

file = open("/tmp/foo.txt")
data = file.read()
file.close()

当然,我们也可以这么写:

file = open("/tmp/foo.txt")
try:
    data = file.read()
finally:
    file.close()

这么写的好处在于,防止读取文件数据发生异常。这也是我们比较常用的一种方式。除此之外,我们还可以这么写:

with open("/tmp /foo.txt") as file:
    data = file.read()

这两句话就完事儿了。当然,with的美丽还不仅与此,因为with自带__enter____exit__这两个函数,一个用于事先设置,一个用于事后清理。不仅如此,__exit__方法有三个参数valtypetrace。这些参数在异常处理中相当有用。比如以下代码:

class Sample:
    def __enter__(self):
        return self
    def __exit__(self, type, value, trace):
        print "type:", type
        print "value:", value
        print "trace:", trace
    def do_something(self):
        bar = 1/0
        return bar + 10
with Sample() as sample:
    sample.do_something()

相当于,先实例化sample=Sample(),然后运行sample.do_something()。运行时,先运行__enter__,啥都没干,因为返回了自己。然后运行do_something函数,1/0运算会出错,所以无法返回bar+10,最后运行__exit__函数,这个函数自带valtypetrace这三个变量,我们可以把他们答应出来。结果如下:

type: 
value: integer division or modulo by zero
trace: 
Traceback (most recent call last):
  File "./with_example02.py", line 19, in 
    sample.do_somet hing()
  File "./with_example02.py", line 15, in do_something
    bar = 1/0
ZeroDivisionError: integer division or modulo by zero

开发库时,清理资源,关闭文件等等操作,都可以放在__exit__方法当中。

在我们这个train.py中,当系统运行到with MLflowCheckpoint(test_x, test_y) as mlflow_logger:的时候,相当于mlflow_logger=MLflowCheckpoint(test_x, test_y),这个时候,我们实例化了MLflowCheckpoint类,这个类对应的__enter____exit__函数如下:

def __enter__(self):
    '''
    自动调用,最先调用
    '''
    return self

def __exit__(self, exc_type, exc_val, exc_tb):
    """
    自动调用,最后调用
    Log the best model at the end of the training run.
    """
    if not self._best_model:
        raise Exception("Failed to build any model")
    mlflow.log_metric(self.train_loss, self._best_train_loss, step=self._next_step)
    mlflow.log_metric(self.val_loss, self._best_val_loss, step=self._next_step)
    mlflow.keras.log_model(self._best_model, "model")

也就是说,每一次epoch之后,系统就会把_best_train_loss_best_val_loss,以及_best_model放进MLFlow的log里。

Keras中的回调函数Callbacks

接下来就是解读代码中的Callbacks回调函数。回调函数是一组在训练的特定阶段被调用的函数集,你可以使用回调函数来观察训练过程中网络内部的状态和统计信息。通过传递回调函数列表到模型的.fit()中,即可在给定的训练阶段调用该函数集中的函数。keras.callbacks.Callback()是回调函数的抽象类,定义新的回调函数必须继承自该类。

被回调函数作为参数的logs字典,它会含有于当前批量或训练轮相关数据的键。

目前Sequential模型类的.fit()方法会在传入到回调函数的logs里面包含以下的数据:

  • on_epoch_end: 包括accloss的日志,也可以选择性的包括val_loss(如果在fit中启用验证),和val_acc(如果启用验证和监测精确值)。
  • on_batch_begin: 包括size的日志,在当前批量内的样本数量。
  • on_batch_end: 包括loss的日志,也可以选择性的包括acc(如果启用监测精确值)。

在我们这个案例中,我们定义了MLflowCheckpoint这个类,以继承Callback类。这个类里面有一个on_epoch_end函数,在每个epoch结束后被调用,详细代码如下:

def on_epoch_end(self, epoch, logs=None):
    """
    Log Keras metrics with MLflow. If model improved on the validation data, evaluate it on
    a test set and store it as the best model.
    """
    if not logs:
        return
    self._next_step = epoch + 1
    train_loss = logs["loss"]
    val_loss = logs["val_loss"]
    mlflow.log_metrics({self.train_loss: train_loss, self.val_loss: val_loss}, step=epoch)

    if val_loss < self._best_val_loss:
        # The result improved in the validation set.
        # Log the model with mlflow and also evaluate and log on test set.
        self._best_train_loss = train_loss
        self._best_val_loss = val_loss
        self._best_model = keras.models.clone_model(self.model)
        self._best_model.set_weights([x.copy() for x in self.model.get_weights()])
        preds = self._best_model.predict(self._test_x)
        eval_and_log_metrics("test", self._test_y, preds, epoch)

在这个函数中,我们获得每个epoch结束后的loss值,然后将她们记录下来(mlflow.log_metrics)。此外,我们把这个epoch的结果和之前的做对比,如果这次的validation loss值比之前的要低,那么就说明这次训练的效果要比之前的好,那么我们就把loss值保存下来,以及训练出来的模型。

你可能感兴趣的:(mlops,deep,learning,深度学习,python,devops)