Katib
Katib 是一个用于自动化机器学习 (AutoML) 的 K8s 原生项目。Katib 支持超参数调整、提前停止和神经架构搜索 (NAS)。
Katib 是一个与机器学习 (ML) 框架无关的项目。它可以调整以用户选择的任何语言编写的应用程序的超参数,并原生支持许多 ML 框架,例如 TensorFlow、MXNet、PyTorch、XGBoost 等。
Katib 支持多种 AutoML 算法,例如 贝叶斯优化、 Parzen 估计器树、 随机搜索、 协方差矩阵自适应进化策略、 Hyperband、 高效神经架构搜索、 可微分架构搜索 等等。其他算法支持即将推出。
下面开始介绍我体验Katib的全过程。
Katib 原生方法
u1s1,Katib对新人上手并不友好:简略的文档,稀少的网络资料,以及难上手的使用方式:直接部署YAML文件。
简而言之,要使用Katib,我们首先必须得写个直接运行起来就可以训练模型的脚本;
构建训练脚本
# train.py
from __future__ import print_function
import argparse
import logging
import os
from torchvision import datasets, transforms
import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
class Net(nn.Module):
def __init__(self):
...
def forward(self, x):
...
def train(args, model, device, train_loader, optimizer, epoch):
model.train()
for batch_idx, (data, target) in enumerate(train_loader):
...
def test(args, model, device, test_loader, epoch, hpt):
model.eval()
test_loss = 0
correct = 0
with torch.no_grad():
for data, target in test_loader:
data, target = data.to(device), target.to(device)
output = model(data)
test_loss += F.nll_loss(output, target, reduction="sum").item()
pred = output.max(1, keepdim=True)[1]
correct += pred.eq(target.view_as(pred)).sum().item()
test_loss /= len(test_loader.dataset)
test_accuracy = float(correct) / len(test_loader.dataset)
logging.info("{{metricName: accuracy, metricValue: {:.4f}}};{{metricName: loss, metricValue: {:.4f}}}\n".format(
test_accuracy, test_loss)) #注意输出log信息
def main():
# Training settings
parser = argparse.ArgumentParser(description="PyTorch MNIST Example")
parser.add_argument("--batch-size", type=int, default=64, help=...)
parser.add_argument("--test-batch-size", type=int, default=1000, help=...)
parser.add_argument("--epochs", type=int, default=10, help=...)
parser.add_argument("--lr", type=float, default=0.01, help=...)
parser.add_argument("--momentum", type=float, default=0.5, help=...)
parser.add_argument("--no-cuda", action="store_true", default=False, help=...)
parser.add_argument("--log-path", type=str, default="", help=...)
parser.add_argument("--save-model", action="store_true", default=False, help=...)
args = parser.parse_args()
if args.log_path == " ":
logging.basicConfig(
format="%(asctime)s %(levelname)-8s %(message)s",
datefmt="%Y-%m-%dT%H:%M:%SZ",
level=logging.DEBUG)
else:
logging.basicConfig(
format="%(asctime)s %(levelname)-8s %(message)s",
datefmt="%Y-%m-%dT%H:%M:%SZ",
level=logging.DEBUG,
filename=args.log_path) #定义log信息的输出文件路径
use_cuda = not args.no_cuda and torch.cuda.is_available()
if use_cuda:
print("Using CUDA")
device = torch.device("cuda" if use_cuda else "cpu")
kwargs = {"num_workers": 1, "pin_memory": True} if use_cuda else {}
train_loader = torch.utils.data.DataLoader(
datasets.FashionMNIST("./data",
train=True,
download=True,
transform=transforms.Compose([
transforms.ToTensor()
])),
batch_size=args.batch_size, shuffle=True, **kwargs) #
test_loader = torch.utils.data.DataLoader(
datasets.FashionMNIST("./data",
train=False,
transform=transforms.Compose([
transforms.ToTensor()
])),
batch_size=args.test_batch_size, shuffle=False, **kwargs)
model = Net().to(device)
optimizer = optim.SGD(model.parameters(), lr=args.lr, momentum=args.momentum)
for epoch in range(1, args.epochs + 1):
train(args, model, device, train_loader, optimizer, epoch)
test(args, model, device, test_loader, epoch, hpt)
if (args.save_model):
torch.save(model.state_dict(), "mnist_cnn.pt")
if __name__ == "__main__":
main()
该脚本中,主要有三点值得注意:
- 我们将下载数据和模型训练放在一脚本中完成;换言之,只要直接运行该脚本,我们就可以得到一个训练好的模型;
- 我们自定义了一些argument变量,这些将成为Katib自调参时的对象;
- 在test函数中,我们用字典形式定义了log信息:
logging.info("{{metricName: accuracy, metricValue: {:.4f}}};{{metricName: loss, metricValue: {:.4f}}}\n".format( test_accuracy, test_loss)),这些log输出信息将被Katib用于评估调参的结果优劣;
接下来开始第二步,构建TMD的镜像;是的, Katib也希望我们把它做成一个镜像;(毕竟方便直接启动容器运行,可以理解)。
构建镜像
原文件夹下只包含train.py、requirements.txt和DockerFile。
DockerFile
FROM python:3.9
ADD . /opt/pytorch-mnist
WORKDIR /opt/pytorch-mnist
RUN mkdir /katib
RUN pip install --no-cache-dir -r requirements.txt
RUN chgrp -R 0 /opt/pytorch-mnist \
&& chmod -R g+rwX /opt/pytorch-mnist \
&& chgrp -R 0 /katib \
&& chmod -R g+rwX /katib
ENTRYPOINT ["python3", "/opt/pytorch-mnist/mnist.py"]
最难受的部分来了,开始写Yaml文件
编写Yaml文件
u1s1,Kubeflow提供的这个UI,简直聊胜于无。
反正我只用到了绿色画圈的部分:Edit,以及Create。
不过它上面那个12345的流程,最后倒是可以帮我们捋一捋YAML文件的结构。
先丢一个YAML文件让大家懵一懵:
---
apiVersion: kubeflow.org/v1beta1
kind: Experiment
metadata:
namespace: kubeflow
name: file-metrics-collector
spec:
objective:
type: maximize
goal: 0.99
objectiveMetricName: accuracy
additionalMetricNames:
- loss
metricsCollectorSpec:
source:
filter:
metricsFormat:
- "{metricName: ([\\w|-]+), metricValue: ((-?\\d+)(\\.\\d+)?)}"
fileSystemPath:
path: "/katib/mnist.log"
kind: File
collector:
kind: File
algorithm:
algorithmName: random
parallelTrialCount: 3
maxTrialCount: 12
maxFailedTrialCount: 3
parameters:
- name: lr
parameterType: double
feasibleSpace:
min: "0.01"
max: "0.03"
- name: momentum
parameterType: double
feasibleSpace:
min: "0.3"
max: "0.7"
trialTemplate:
primaryContainerName: training-container
trialParameters:
- name: learningRate
description: Learning rate for the training model
reference: lr
- name: momentum
description: Momentum for the training model
reference: momentum
trialSpec:
apiVersion: batch/v1
kind: Job
spec:
template:
spec:
containers:
- name: training-container
image: ...
command:
- "python3"
- "/opt/pytorch-mnist/train.py"
- "--epochs=1"
- "--log-path=/katib/mnist.log"
- "--lr=${trialParameters.learningRate}"
- "--momentum=${trialParameters.momentum}"
restartPolicy: Never
知道这是在干嘛吗?着实懵逼了很久。
现在,划分一下结构:
可以分为5个大部分:
- objective: 调参指标。这里的调参指标是:accuracy达到0.99;附加目标loss尽可能低;
- metricsCollectorSpec: 调参指标收集器:分别定义了收集路劲及收集格式;结合一下上文脚本log输出信息的格式和存放位置,应该很快就明白了;
- algorithm:调参使用算法
- parameters: 调参对象
- trailTemplate 每次调参实验的模板:确保调参代码以命令行的方式传入参数运行;其中的 trailParameters将在parameters和脚本的argument间建立对应关系;containers则定义了上文我们所构建镜像的具体运行方式;
附: algorithm 可用调参算法:
此外,还可以手动添加额外配置:
- parallelTrialCount: 并行实验个数
- maxTrialCount: 最大实验次数
- maxFailedTrialCount: 最多失败实验次数
- metricsCollectorSpec: 回收存储每次迭代实验结果的方式
编写完后,点击提交,就算完成啦。
Katib Python SDK
直接编写YAML文件,是不是也太那啥了? 事实上,不久后,就出现了Katib的Python SDK。
有了上文使用Katib原生方法的经验后,我们也对Katib的YAML文件结构有了一定认识,使用Python SDK我们也得心应手了不少;
首先导入包:
from kubeflow.katib import KatibClient
from kubernetes.client import V1ObjectMeta
from kubeflow.katib import V1beta1Experiment
from kubeflow.katib import V1beta1AlgorithmSpec
from kubeflow.katib import V1beta1ObjectiveSpec
from kubeflow.katib import V1beta1FeasibleSpace
from kubeflow.katib import V1beta1ExperimentSpec
from kubeflow.katib import V1beta1ObjectiveSpec
from kubeflow.katib import V1beta1ParameterSpec
from kubeflow.katib import V1beta1TrialTemplate
from kubeflow.katib import V1beta1TrialParameterSpec
定义各部分:
namespace = "kubeflow-user-example-com"
experiment_name = "cmaes-example"
metadata = V1ObjectMeta(
name=experiment_name,
namespace=namespace
)
调参算法:
algorithm_spec=V1beta1AlgorithmSpec(
algorithm_name="random"
)
调参指标;
objective_spec=V1beta1ObjectiveSpec(
type="maximize",
goal= 0.99,
objective_metric_name="Validation-accuracy",
additional_metric_names=["Train-accuracy"]
)
调参对象:
parameters=[
V1beta1ParameterSpec(
name="lr",
parameter_type="double",
feasible_space=V1beta1FeasibleSpace(
min="0.01",
max="0.06"
),
),
V1beta1ParameterSpec(
name="num-layers",
parameter_type="int",
feasible_space=V1beta1FeasibleSpace(
min="2",
max="5"
),
),
V1beta1ParameterSpec(
name="optimizer",
parameter_type="categorical",
feasible_space=V1beta1FeasibleSpace(
list=["sgd", "adam", "ftrl"]
),
),
]
等等。其他各部分定义可以查函数具体功能介绍:
定义完各部分后,我们便可以创建、提交实验:
并获取实验相关信息、状态等:
不过可以发现,所谓的Katib Python SDK并未提供更便携的构建方式,只是使YAML文件各部分可拆分,定义并赋值给变量,提高了各部分的复用性罢了。
还好,这个问题最近也开始得到解决:
社区中有人提出要在Katib中创建Tune API。
这不仅使我们从编写YAML文件中解放了出来,也使我们得以绕过构建镜像的步骤直接训练模型代码,使我们的AutoML低代码可视化开发成为了可能。
Katib SDK Tune API
u1s1,这个包确实来得晚,以至于出现了个很神奇的事,我pip install kubeflow-katib时,一开始甚至没有Tune API,然后我反复pip uninstall和install了很多次,忽然就莫名其妙又有了....
来个简单的例子:
在该tune API中,name指定了实验名称,objective参数指定了调参指标,parameters指定了调参对象,base_image定义了运行该模型训练函数的基础镜像。
提交该函数后,我们便可以在Katib UI中查看该实验了:
当然,这个Tune API的实现方式也算是蛮简单粗暴的? 看了下提交后的YAML文件:
结合Katib 的 Python SDK,我们已经可以观察到Katib的大部分可复用性是很强的,完全可以做成积木+填空;
然后,再观察它的containers部分:
它基于我们提供的镜像环境,执行了一段sh:将我们写的函数放入$program_path/ephemeral_objective.py,并执行该文件。
下周目标
- 构建私有Elyra镜像,并成功跑通流程;
- 将Katib结合进流程的一部分;并同样基于Elyra可视化编程实现。