这里我们提供一些列案例,基于keras的深度学习框架对winequality-white
数据进行quanlity的预测。此章节的代码基于MLFlow官网GitHub链接。
本章节将详细解释如何基于MLFlow以及keras的端到端深度学习流程。相关主要代码参见train.py
文件。关于整个系列案例如何运行,请参见博客:MLOps极致细节:9. MLFlow 超参数调参案例综述(附代码)。相关代码参见此Gitee链接。
在MLOps极致细节:9. MLFlow 超参数调参案例综述(附代码)中,我们解释了三种运行此代码的方式:
git clone https://gitee.com/yichaoyyds/mlflow-ex-hyperparametertunning.git
。然后进入mlflow-ex-hyperparametertunning
文件夹,在terminal中运行py文件,比如python train.py
;git clone https://gitee.com/yichaoyyds/mlflow-ex-hyperparametertunning.git
。然后再按照下面章节的步骤运行mlrun
指令(比如mlflow run -e train --experiment-id ./mlflow-ex-hyperparametertunning/
);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=100
,batch_size=16
,learning_rate=1e-2
,momentum=0.9
。train_rmse
以及val_rmse
的长度是等于epoch的,或者说,等于epoch+1,但test_rmse
的数量是不固定的,因为代码中,只有当这次epoch运算出来的结果小于上一次,才会去计算test的结果,并且保存。
另外,我们可以通过ui看到每次训练/验证的结果:
可以看到,迭代并没有让梯度下降多少。
在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}"
所以,我们设定以下参数:
np.random.seed(seed)
关于MLFlow Projects
的详细介绍,请参见我之前的一篇博客。这里只简单介绍相关的两个最重要的文件:MLproject
以及conda.yaml
。
这个文档中包含了三个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.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是否更低,如果更低,那么当前循环模型就是最优模型。详细介绍请参见下面章节。
在此案例中,我们使用了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.
在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__
方法有三个参数val
,type
和trace
。这些参数在异常处理中相当有用。比如以下代码:
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__
函数,这个函数自带val
,type
和trace
这三个变量,我们可以把他们答应出来。结果如下:
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里。
接下来就是解读代码中的Callbacks
回调函数。回调函数是一组在训练的特定阶段被调用的函数集,你可以使用回调函数来观察训练过程中网络内部的状态和统计信息。通过传递回调函数列表到模型的.fit()
中,即可在给定的训练阶段调用该函数集中的函数。keras.callbacks.Callback()
是回调函数的抽象类,定义新的回调函数必须继承自该类。
被回调函数作为参数的logs
字典,它会含有于当前批量或训练轮相关数据的键。
目前Sequential
模型类的.fit()
方法会在传入到回调函数的logs
里面包含以下的数据:
on_epoch_end
: 包括acc
和loss
的日志,也可以选择性的包括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值保存下来,以及训练出来的模型。