本章涵盖:
- 设计模块化的训练组件
- 在追踪框架中捕获指标与产物(artifacts)
- 将模型训练与验证组件加入流水线(pipelines)
- 使用不同方法访问训练与评估数据
构建可靠的机器学习系统,不仅仅需要准确的模型——还要求可复现的训练流程与健壮的验证策略。在本章中,我们将在数据准备流水线的基础上,创建可用于生产的训练与验证组件(图 8.1)。
图 8.1 当前的心智地图:我们正聚焦于流水线的第 4 与第 5 个步骤/组件——模型训练(4)与评估(5)
通过使用 You Only Look Once(YOLO) 目标检测的动手示例,你将学会开发可从实验扩展到生产的模块化训练组件。我们会深入探讨真实场景中的模型验证细节,看看如何有效捕获与追踪指标以改进模型,并掌握将这些组件无缝集成进 ML 流水线的技巧。
把这些实践先用于我们的身份证检测系统,之后再应用到电影推荐系统中,你将获得构建稳健训练工作流的实战经验——而这些工作流正是生产级 ML 系统的骨架。
8.1 训练一个目标检测模型
在构建生产 ML 系统时,模型训练需要可复现、模块化,并且能与流水线基础设施良好集成。虽然我们可以在 Jupyter Notebook 里训练模型,但这种方式很难扩展到生产用例。相反,我们会把训练流程设计成组件,使其能够:
- 在源码管理中进行版本化与追踪
- 在不同环境中一致执行
- 与数据准备流水线无缝集成
- 被系统化地监控与评估
我们将使用 YOLO 来实现身份证检测系统。训练组件会承担三项关键职责:
- 将训练、验证与测试图像(以及对应标签)下载到合适的目录
- 配置数据集:设置数据集路径、各分割(split)的图像路径,并将数据与对应标签匹配起来
- 执行模型的实际训练与评估
我们所使用的 Ultralytics YOLO 库会替我们完成不少训练与评估的“重活”,你很快就会看到。不过这并不是本章的核心重点;需要强调的是,你可以按需以任何方式自定义代码。我们先从理解如何为自定义数据集配置 YOLO 开始。
8.1.1 在自定义数据集上训练 YOLO
YOLO 默认并不会检测身份证,但别担心!在 YOLO 上训练自定义数据集并不难。首先,我们需要创建一个配置文件,包含图像与标签所在的位置;我们还会在其中加入希望 YOLO 学习的唯一标签。之后就可以开始训练模型。让我们直接开始。
为了让 YOLO 使用我们的数据集,我们需要创建一个 YAML 文件。好在它的结构很直观。在把配置写成 YAML 之前,我们先从一个字典开始,键和值如清单所示。
清单 8.1 YOLO 期望的数据配置
data = {
'path': '/dataset/',
'train': 'train/images',
'val': 'val/images',
'test': 'test/images',
'names': {
0: 'id_card' #1
}
}
#1 定义标签
(空)
对我们来说,只需要一个标签:'id_card'。如果想检测其他对象,就把更多类别加到这个列表里。注意:在 YOLOv8 中,我们只需要指定图像路径。
对于 YOLOv8,某个阶段(stage)的标签应位于与对应图像相邻的目录中。例如,训练集的标签应放在 train/labels 中。定义完数据配置后,我们需要把它写出为 YAML 文件,如下所示。
清单 8.2 将数据配置写入 YAML
file_path = 'custom_data.yaml'
try:
with open(file_path, 'w') as file:
yaml.dump(data, file) #1
print("YAML file has been written successfully.")
except Exception as e:
print(f"Error writing YAML file: {e}")
#1 将字典转换为 YAML 文件
(空)
有了这个数据配置 YAML 文件,我们就可以继续训练模型了!
8.1.2 训练模型
生产环境中的模型训练与实验场景差异很大。我们当然可以手动调每一个超参数,但生产系统需要一套鲁棒的默认配置,能够在不同场景下都表现良好。对于身份证检测系统,我们会在三点之间取得平衡:
- 训练效率——使用合适的 batch size 与学习率
- 资源利用——选择与计算约束匹配的模型架构
- 性能要求——满足准确率需求,同时保持可接受的训练时间
我们会在训练组件里实现这种平衡。这里就是让 YOLO 模型真正“干活”的地方。下面的代码片段使用 Ultralytics 库来训练我们的自定义 YOLO 模型。
清单 8.3 使用 Ultralytics 训练自定义 YOLO 模型(原文标题处疑似重复/笔误)
from ultralytics import YOLO
model = YOLO('yolov8n.pt') #1
results = model.train( #2
data='custom_data.yaml',
imgsz=640,
epochs=epochs,
batch=batch,
name='yolov8n_custom',
project=project_path) #3
#1 初始化(并下载)预训练模型
#2 执行模型训练循环
#3 在模型训练期间收集所有产物
(空 空 空)
在这段清单里,我们使用 Ultralytics 的 YOLO 类初始化并下载一个预训练模型,然后调用 train 方法执行训练循环。
train 方法包含几个重要参数:
data——自定义数据配置文件的路径,用于定义训练数据集imgsz——训练时会把图像缩放到这个正方形尺寸(例如 640 表示 640×640)。非正方形图像会进行 letterbox(填充)以保持纵横比。该值越大可能提升准确率,但会增加显存占用与训练时间。epochs——从流水线定义中传入的变量,控制训练数据将被迭代多少轮batch——同样来自流水线定义的变量,决定每次迭代使用的样本数name——训练出的模型名称project——训练期间生成的所有产物的收集位置。作为 Kubeflow 组件运行时,该路径用于存储模型权重、混淆矩阵图、召回/精确率曲线,以及训练/验证集上的推理样本等。
随着训练进行,模型会在每个 epoch 生成各种推理结果,并自动捕获并存入 project 路径。这使我们能够监控训练进展,并在训练的每个阶段评估性能。图 8.2 给出了一个示例。
图 8.2 训练过程中自动捕获的推理示例
YOLOv8 提供了一系列预训练模型,每个模型都针对不同用例设计。像 YOLOv8n 这样的更小模型,由于处理速度更快、内存需求更低,非常适合边缘设备或实时应用;更大的模型通常更准确,但也需要更多算力资源。
为什么模型架构很重要
为项目选模型时,必须考虑性能与资源约束之间的权衡。选对 YOLOv8 模型,你就能在速度、内存占用与准确率之间做优化。比如:
- 如果你在开发用于监控的 AI 摄像头,可能更看重更快的处理速度与更低的内存需求。
- 如果你在构建自动驾驶系统,即便计算需求更高,也可能更偏向更准确的结果。
无论哪种情况,理解 YOLOv8 模型的特性,都能帮助你对选型与性能优化做出更明智的决策(图 8.3)。
图 8.3 YOLOv8 的不同模型尺寸及其性能指标
针对我们的用例,我们先从 Nano 版本开始。它有几个优势:
- 节省算力资源——尽量降低训练与推理所需的计算能力,这在硬件或预算受限时尤其重要
- 验证可行性——用更小的模型快速判断是否能在不投入太多时间与资源的情况下取得不错结果
- 迭代与优化——如果 Nano 版本达不到预期,可以用本次实验的洞察指导更大模型的开发,以更好满足需求
YOLO 的超参数
使用 YOLOv8(或任何模型)时,理解可用超参数很重要。虽然这里我们只关注其中一部分设置,但你仍需要掌握每个参数的作用以及(如适用)其默认值。最终在训练组件中暴露哪些参数,取决于你的具体需求与约束。表 8.1 提供了一个有用的起点。
表 8.1 YOLO 超参数
| Key | Value | Description | Adjustment considerations |
|---|---|---|---|
| model | None | 模型文件路径,例如 yolov8n.pt、yolov8n.yaml | 按用例选择合适的模型架构与权重 |
| data | None | 数据文件路径,例如 coco128.yaml | 确保数据集与问题相关、标注良好且规模足够 |
| epochs | 20 | 训练轮数 | 结合算力与精度目标调整;更多 epoch 往往更准但更耗时 |
| batch | 4 | 每个 batch 的图像数(-1 为 AutoBatch) | 增大 batch 可提升吞吐,但注意 GPU 显存;过大 batch 也可能因梯度更新减少而影响效果 |
| imgsz | 640 | 输入图像尺寸(整数) | 在检测精度与计算效率之间权衡(例如高分辨率有利于更精确定位) |
| save | True | 保存训练检查点与预测结果 | — |
| project | None | 项目名 | — |
| name | None | 实验名 | — |
| exist_ok | False | 是否覆盖已有实验 | 除非有很好的理由,否则这是一个安全默认值 |
| pretrained | True | (bool 或 str)是否使用预训练(bool)或从指定权重加载(str) | 根据用例与算力决定从预训练开始还是从零训练;预训练通常是更好的起点但可能需要调整 |
| optimizer | 'auto' | 优化器:[SGD, Adam, Adamax, AdamW, NAdam, RAdam, RMSProp, auto] | 选择兼顾收敛速度与稳定性的优化器;auto 会基于总迭代次数动态选择:>10,000 次用 SGD,否则用 AdamW |
| verbose | False | 是否输出详细日志 | — |
| val | True | 训练期间进行验证/测试 | — |
8.1.3 用容器组件处理系统级依赖
预构建组件在大多数情况下够用,但有时你需要更多控制力——尤其是当你遇到 Python 包无法安装的系统级依赖时。这里就轮到容器组件登场,它专门解决这个问题。
Ultralytics 的 Docker 镜像通常开箱即用,但也可能遇到 ImportError: libGL.so.1: cannot open shared object file。修复方式需要用 apt-get 安装 libgl1-mesa-glx——而 @dsl.component 做不到这一点,因为它只能处理 Python 包。
这时就该用 dsl.ContainerSpec 和 @dsl.container_component。它们允许你运行任意 Docker 命令(包括系统包安装),同时仍保持一切在 Python 代码中完成(不需要手动构建 Dockerfile)。代价是组件定义会更啰嗦。接下来小节将给出分步方法。
步骤 1:创建训练脚本
先从 main() 方法开始:添加 import,然后调用一个尚未实现的 parse_args()。这很关键,因为训练脚本会传入一堆参数。这部分代码你应该比较熟悉:我们为 YOLOv8 准备配置文件,如下所示。
清单 8.4 创建包含路径与身份证类别的 YOLO data.yaml
import os
import yaml
import shutil
import argparse
from ultralytics import YOLO
def main():
args = parse_args()
data = {
'train': os.path.join(args.train_path, "images"), #1
'val': os.path.join(args.val_path, "images"), #1
'test': os.path.join(args.test_path, "images"), #1
'nc': 1, #2
'names': {
0: 'id_card' #2
}
}
data_yaml_path = os.path.join(
args.data_yaml, "data.yaml") #4
os.makedirs( #5
os.path.dirname(data_yaml_path), exist_ok=True) #5
print(f"Writing data configuration to {data_yaml_path}")
print("Data YAML contents:")
print(yaml.dump(data))
with open(data_yaml_path, 'w') as file: #6
yaml.dump(data, file)
#1 构建 train/val/test 的图像目录绝对路径
#2 定义单类别配置:身份证检测
#4 创建 data.yaml 配置文件路径
#5 确保输出目录存在
#6 将 YAML 配置写入文件
(空 空 空 空 空 空)
接下来加入模型训练逻辑,下面的清单给出了必要代码。
清单 8.5 创建/训练 YOLO 数据集并保存最佳权重
def main():
model = YOLO('yolov8n.pt') #1
results = model.train(
data=data_yaml_path, #2
imgsz=640, #3
epochs=args.epochs,
batch=args.batch,
project=os.path.dirname(args.model_output),
name=args.model_name #4
) #4
best_model_source = os.path.join(
os.path.dirname(args.model_output)
args.model_name, "weights", "best.pt") #5
best_model_dest = os.path.join(
args.model_output, "best.pt") #6
os.makedirs(os.path.dirname(best_model_dest), exist_ok=True)
shutil.copy2(best_model_source, best_model_dest) #7
#1 初始化 YOLOv8 nano 模型
#2 使用生成的 data.yaml 配置数据集
#3 设置输入图像尺寸为 640×640
#4 配置输出项目目录与 run 名称
#5 训练输出中最佳权重的路径
#6 保存最佳模型的目标路径
#7 将最佳模型复制到输出位置
(空 空 空 空 空 空 空)
当我们训练了若干个 epoch 后,会选择最佳模型(Ultralytics 会帮我们做这件事)并把它保存到 args.model_output。说到这里,就该定义 parse_args 了,如下所示。
清单 8.6 解析 YOLO 训练配置的命令行参数
def parse_args():
parser = argparse.ArgumentParser(description='Train YOLO model')
parser.add_argument('--train-path',
required=True, help='Path to training dataset')
parser.add_argument('--val-path',
required=True, help='Path to validation dataset')
parser.add_argument('--test-path',
required=True, help='Path to test dataset')
parser.add_argument('--epochs', type=int,
required=True, help='Number of epochs')
parser.add_argument('--batch', type=int,
required=True, help='Batch size')
parser.add_argument('--model-name',
required=True, help='Name of the model')
parser.add_argument('--model-output',
required=True, help='Path to save the model')
parser.add_argument('--data-yaml',
required=True, help='Path to save data.yaml')
return parser.parse_args()
最后,在训练脚本底部加上下面这一段,让脚本执行时会调用 main():
if __name__ == "__main__":
main()
OK,步骤 1 完成。这个脚本可以独立执行。接下来我们看看如何自定义依赖。
步骤 2:用容器组件自定义依赖
要运行 apt-get 这类任意命令,我们需要用 @dsl.container_component 而不是标准的 @dsl.component。这个装饰器允许我们直接指定 Docker 镜像、shell 命令与参数。我们会逐步构建它——先写函数签名,再写容器规格,如清单所示。
清单 8.7 YOLO 训练流水线的 Kubeflow Pipelines 组件
from kfp import dsl
from kfp.dsl import Input, Output, Dataset, Model, Artifact, Metrics
@dsl.container_component #1
def train_model(
epochs: int, #2
batch: int, #2
yolo_model_name: str, #2
train_dataset: Input[Dataset], #3
validation_dataset: Input[Dataset], #3
test_dataset: Input[Dataset], #3
model_output: Output[Model], #4
data_yaml: Output[Artifact] #4
):
return dsl.ContainerSpec(...)
#1 将该函数定义为基于容器的 KFP 组件的装饰器
#2 用于配置训练过程的基础参数输入
#3 以 KFP Dataset 类型封装的 train/val/test 输入数据集
#4 输出产物:训练后的模型与 YAML 配置文件
(空 空 空 空 空)
@dsl.container_component 将其标记为容器化的流水线步骤,接收基础训练参数(epochs、batch、yolo_model_name)以及 KFP 的三份 Dataset 输入(训练/验证/测试)。组件产生两个输出:训练后的模型产物(model_output)与 YAML 配置文件(data_yaml),都通过 KFP 的类型系统封装,便于流水线产物管理。
还记得我们需要安装 libgl1-mesa-glx 等依赖吗?下面的清单展示了如何用 dsl.ContainerSpec 完成。
清单 8.8 配置包含 YOLO 训练依赖的容器
@dsl.container_component
def train_model(...):
return dsl.ContainerSpec(
image='python:3.11-slim',
command=['bash', '-c'],
args=[
f'''
apt-get update && \
apt-get install -y --no-install-recommends \
libgl1-mesa-glx \
libglib2.0-0 \
&& rm -rf /var/lib/apt/lists/* && \
pip install --no-cache-dir \
ultralytics \
torch \
opencv-python-headless==4.8.1.78 \
minio \
tqdm \
pyyaml
'''
]
)
如果你熟悉自定义 Docker 镜像,这些内容应该不陌生。dsl.ContainerSpec 允许我们像写 Dockerfile 一样加入任意命令。OK,那训练脚本怎么包含进来?你可能已经猜到了:像写 Docker 命令一样把它加进去。我们接下来就看看怎么做。
步骤 3:将训练脚本集成到组件中
下面的代码展示了一种独特的 KFP 组件创建方式:把一整段训练脚本直接嵌入到容器规格(container specification)里。它使用 heredoc 语法(cat << 'EOF'),先把 YOLO 训练脚本写入文件,再带上合适的命令行参数执行该脚本(见清单 8.9)。
这种方法在保留 KFP 容器化便利性的同时,实现了动态注入脚本与参数传递,不过它用可读性换取了灵活性。组件通过 Bash 的位置参数($0、$1 等),把输入参数(如 train_dataset.path、epochs 等)映射到脚本参数上,从而在 KFP 的类型系统与训练脚本的命令行接口(CLI)之间搭起桥梁。
清单 8.9 配置容器并注入训练脚本与参数
@dsl.container_component
def train_model(...):
return dsl.ContainerSpec(
image='python:3.11-slim',
command=['bash', '-c'],
args=[
f'''
apt-get update && \
apt-get install -y --no-install-recommends \
libgl1-mesa-glx \
libglib2.0-0 \
&& rm -rf /var/lib/apt/lists/* && \
pip install --no-cache-dir \
ultralytics \
torch \
opencv-python-headless==4.8.1.78 \
minio \
tqdm \
pyyaml && \
cat << 'EOF' > /train.py \ #1
{TRAINING_SCRIPT} #2
EOF
python3 /train.py \ #3
--train-path "$0" \ #4
--val-path "$1" \
--test-path "$2" \
--epochs "$3" \ #5
--batch "$4" \
--model-name "$5" \
--model-output "$6" \ #6
--data-yaml "$7"
''',
train_dataset.path, #7
validation_dataset.path,
test_dataset.path,
epochs,
batch,
yolo_model_name,
model_output.path,
data_yaml.path
]
)
#1 使用 heredoc 将脚本写到 /train.py
#2 从 TRAINING_SCRIPT 变量注入训练脚本内容
#3 启动 Python 脚本并传入参数
#4 将组件输入中的数据集路径映射到脚本参数
#5 传入训练配置参数
#6 指定产物(artifact)输出位置
#7 将组件参数与脚本参数一一关联
(空 空 空 空 空 空 空)
在下面的清单中,TRAINING_SCRIPT 就是我们在步骤 1 中创建的训练脚本。
清单 8.10 用 YOLO 生成训练脚本
TRAINING_SCRIPT = '''
import os
import yaml
import shutil
import argparse
from ultralytics import YOLO
def parse_args():
...
def main():
...
if __name__ == "__main__":
main()
'''
步骤 4:使用该组件
使用 @dsl.container_component 注解的组件,和使用 @dsl.component 注解的组件,调用方式完全相同,如下所示。
清单 8.11 为数据集准备与训练定义 KFP 流水线
@dsl.pipeline(
name="YOLO Object Detection Pipeline",
description="YOLO Object Detection Pipeline" #1
)
def pipeline(
epochs: int = 1,
batch: int = 8, #2
random_state: int = 42,
yolo_model_name: str = "yolov8n_custom"
):
download_op = download_dataset() #3
split_op = split_dataset(
random_state=random_state,
input_dataset=download_op.outputs[
"output_dataset"] #4
)
train_op = train_model(
epochs=epochs,
batch=batch,
yolo_model_name=yolo_model_name, #5
train_dataset=split_op.outputs["train_dataset"],
validation_dataset=split_op.outputs[
"validation_dataset"],
test_dataset=split_op.outputs["test_dataset"] #6
)
#1 为 Kubeflow 定义流水线元数据
#2 设置训练的默认超参数
#3 第一步:下载原始数据集
#4 第二步:基于上一步输出进行数据切分
#5 第三步:配置模型训练参数
#6 将切分后的数据集传给训练算子
(空 空 空 空 空 空)
让我们用验证组件把本章补齐,这样就能看到完整的训练/验证流水线了。
8.1.4 创建验证组件
生产 ML 系统中的验证不仅仅是测量准确率,它还承担更多目的。一个设计良好的验证组件能帮助你:
- 尽早捕获模型退化
- 理解在不同数据分段上的表现
- 对模型部署做出有依据的决策
- 维护历史性能记录
在我们的实现中,我们会使用 Kubeflow 的指标追踪能力,把这些洞察系统化地捕获下来。该方法既能与流水线基础设施集成,也保留了在需求演进时加入自定义验证逻辑的灵活性。
这里有一个关键概念是要掌握的:输出指标(outputting metrics) ,这是 KFP 提供的功能。利用这些设施,我们可以获得模型性能的关键洞察,并做出数据驱动的优化决策。指标可以以如下形式呈现:
- Markdown
- 图表(plots)
- 原始数值(raw values)
这次我们就不再“折磨自己”用 dsl.container_component 的方式来创建组件了,而是复用我们的老朋友 dsl.component。
作为开发者,出于肌肉记忆,我们常常想通过 requirements.txt 安装所有依赖来简化流程。但这种做法可能导致“依赖地狱(dependency hell)”——复杂的版本依赖网会带来挫败感并拖慢开发,这在 ML 库里尤其常见。比如某些依赖需要 TensorFlow,而另一个依赖又需要特定版本的 PyTorch,进而与已安装的 OpenCV 版本冲突——你懂的。
为了避免这些问题,值得考虑使用预打包的 Docker 容器,比如 Ultralytics 的基础镜像。使用这类预构建镜像,我们就不必手动安装依赖,从而节省时间与精力,也降低错误与版本冲突的概率。这样我们可以专注于构建项目,而不用担心底层基础设施。
因此,为了让我们本来就复杂的生活稍微简单一点,下面的清单中我们会使用 base_image 指定 Ultralytics Docker 镜像,并通过 packages_to_install 补齐剩余依赖。
清单 8.12 定义 YOLO 模型验证的 KFP 组件
@dsl.component(
base_image="ultralytics/ultralytics:8.0.194-cpu",
packages_to_install=["minio", "tqdm"]
)
def validate_model(
data_yaml: Input[Artifact], #1
model: Input[Model], #2
validation_dataset: Input[Dataset], #3
metrics: Output[Metrics] #4
):
# ...
#1 作为通用 KFP Artifact 的配置文件
#2 使用 KFP Model 类型的训练后模型
#3 用于验证数据的 Dataset 类型
#4 用于存储评估结果的 Metrics 类型
(空 空 空 空)
这个验证组件展示了四种不同的产物类型:用于配置数据的通用 Artifact、用于训练权重的专用 Model、用于验证数据的 Dataset,以及用于捕获评估结果的 Metrics。这些类型使 KFP 能够管理流水线组件之间的数据流与依赖关系。清单 8.13 展示了如何加载训练好的 YOLO 模型并在测试数据集上进行验证,同时通过 KFP 的指标日志接口捕获标准的 Common Objects in Context(COCO)指标(平均精度均值 mean Average Precision,即 mAP)。
清单 8.13 加载 YOLO 模型并计算验证指标
@dsl.component(...)
def validate_model(...):
from ultralytics import YOLO
import os
model_path = os.path.join(model.path, "best.pt") #1
model = YOLO(model_path)
val_results = model.val( #2
data=os.path.join(data_yaml.path, "data.yaml"),
imgsz=640,
batch=1,
verbose=True
)
metrics.log_metric("map50-95", val_results.box.map) #3
metrics.log_metric("map50", val_results.box.map50) #3
metrics.log_metric("map75", val_results.box.map75) #3
#1 从组件输入中加载最佳模型权重
#2 使用 YOLO 的 val 方法验证模型
#3 将标准 COCO 指标写入 KFP 的 metrics 产物
(空 空 空)
使用 Ultralytics YOLO 库时,模型验证只需要一行代码:model.val()。虽然并非所有库都提供这种便利,但一定要记住:收集并分析验证指标对做出模型决策至关重要。这样你才能真正理解模型表现,并识别改进空间。现在我们已经备齐全部“食材”,是时候把它们缝合成一条流水线了。
8.1.5 创建流水线
你之前已经见过这个流水线的一个版本,只不过现在我们加入了验证组件。所需代码如下。
清单 8.14 该工作流的 YOLO 训练-验证流水线
@dsl.pipeline(
name="YOLO Object Detection Pipeline",
description="YOLO Object Detection Pipeline"
)
def pipeline(
epochs: int = 1,
batch: int = 8,
random_state: int = 42,
yolo_model_name: str = "yolov8n_custom"
):
download_op = download_dataset()
split_op = split_dataset(
random_state=random_state,
input_dataset=download_op.outputs["output_dataset"]
)
train_op = train_model(
epochs=epochs,
batch=batch,
yolo_model_name=yolo_model_name,
train_dataset=split_op.outputs["train_dataset"],
validation_dataset=split_op.outputs[
"validation_dataset"], #1
test_dataset=split_op.outputs["test_dataset"]
)
validate_op = validate_model(
data_yaml=train_op.outputs["data_yaml"], #2
model=train_op.outputs["model_output"], #2
validation_dataset=split_op.outputs[
"validation_dataset"], #1
)
if __name__ == '__main__':
from kfp import compiler
compiler.Compiler().compile(
pipeline_func=pipeline,
package_path='training_and_validation_pipeline.yaml'
)
#1 使用数据准备阶段的 validation 切分
#2 传入训练步骤的输出
(空 空 空)
接下来就是编译、上传并执行流水线。
8.1.6 执行流水线
到这里,你应该已经知道如何创建一次 KFP run 了。注意:run 的参数会基于你前面定义的默认流水线参数自动预填(图 8.4)。
图 8.4 Run 参数会根据流水线参数自动填充。
流水线最终的拓扑结构会突出显示组件之间的数据依赖关系(图 8.5)。
图 8.5 新上传流水线的拓扑结构
如果你使用提供的数据集,在我们的 Kubernetes 集群上跑 1 个 epoch 大约需要 1 小时。你当然可以跑更多 epoch,但为了学习目的,1 个 epoch 就够了。具体耗时会因环境而异,不过对耗时有个直观感受总是好事。Ultralytics 库会在训练过程中输出大量信息,例如:
- 模型架构信息,包括包含总层数与参数量的模型摘要
- 训练设置信息,例如是否使用预训练权重、使用哪种优化器
- 数据集信息,例如图像数量与损坏图像数量
- 训练进度信息,例如当前 epoch、GPU 显存使用量、loss 指标,以及 mAP 等评估指标
- 训练速度信息,例如每个训练步骤耗时与预计剩余时间
图 8.6 是我们一直在等的东西——所有组件都成功执行的一次运行结果。再次注意数据依赖的表示:split-dataset 组件有一个参数被 validate-model 使用,所以两者之间会有连接。
图 8.6 含训练与验证组件的成功流水线运行
点击 Run 的 output 标签页可以看到指标,如图 8.7 所示。
图 8.7 指标会作为 Run output 标签页的一部分显示在 UI 中。
你也可以在 Output artifacts 区域看到这些指标的原始值(图 8.8)。
图 8.8 同样的指标也可以在 Output artifacts 下以原始形式查看。
说到 output artifacts,现在正是把它们用起来的好时机!在下一节中,你将下载训练期间生成的 artifacts,并在本地加载训练好的 YOLO 模型权重,或者用于部署(在后续章节)。最后,如果你进入 Pipelines Overview 页面,会看到这些指标已经被列在历史运行列表中了(图 8.9)。
图 8.9 指标现在会随之前的所有流水线运行一起展示。
在下一节中,你将学习保存模型 artifacts 为什么非常有用。其中一个用例,就是把(在 Kubeflow 中训练好的)模型权重加载到本地机器上。
8.1.7 验证模型产物
虽然我们流水线中的自动化验证至关重要,但在开发与调试阶段,能够手动检查模型行为同样能提供非常有价值的洞察。这种做法可以帮助你做到以下几点:
- 在特定测试用例上验证模型行为。
- 调试意料之外的模型输出。
- 向干系人展示模型能力。
- 建立对训练流水线的信心。
说服自己模型确实能用非常重要,所以一定要记住那句谚语——信任,但要验证(trust, but verify)。我们经历过很多次:别人把模型交给我们去上线运行,但直到很久以后才发现它其实并不好用。
我们先试着用一个没有训练过身份证检测的模型来跑推理。也就是说,我们会使用通用的预训练 YOLO 模型所学习到的那些对象类别。
下面的清单使用默认的 YOLO 模型。文件名 yolov8n 里的 “n” 表示我们使用的是 Nano 版本,它与我们训练的模型采用相同的架构。我们会创建一个名为 samples 的文件夹,放入一些样例图片,最好是模型没有见过的图像——就像我们构建任何测试集时那样。对目录中的每一张图片,我们把它喂给模型并显示结果。按任意键切换到下一张图片(前提是目录里不止一张)。
清单 8.15 使用默认 Nano 模型运行推理
import glob
import cv2
from ultralytics import YOLO
model = YOLO("yolov8n.pt") #1
for file in glob.glob("samples/**.jpg"): #2
result = model(cv2.imread(file))
res_plotted = result[0].plot() #3
cv2.imshow("result", res_plotted) #4
cv2.waitKey(0)
#1 用默认(nano)权重初始化模型
#2 遍历目录中的所有 JPEG 文件
#3 将检测结果转换为 cv2 可读取的格式
#4 在窗口中显示图像
(空 空 空 空)
运行这段代码后,你会看到类似图 8.10 的结果。
图 8.10 未经过训练、使用默认 YOLO 模型的检测结果
你立刻可以观察到两点:
- 模型能够检测到人脸,并将其分类为 person。
- 模型把身份证识别成了 book,且置信度较低。
在查看目标检测模型输出时,你会注意到每个检测到的对象旁边都有数值。这些数值代表该检测的置信度分数,范围是 0 到 1。越接近 1 代表越自信,越接近 0 则表示越不确定。
例如,在驾驶证的图片中,我们看到一个 Book 检测的置信度为 0.28,这表明模型对该判断并不太确定。相比之下,更大的 Person 检测置信度高达 0.79,说明模型对该识别相当确信。这些置信度分数对于理解每个检测的可靠性非常关键;在你的具体应用中,如有需要,也可以用它们来过滤掉不够确定的预测。
尽管它把卡片误分类为 book,但依然能把边界框(bounding box)框得比较正确。现在,我们把模型权重切换成我们用 KFP 训练得到的权重。当你有一次成功的流水线运行后,到 Output artifacts 区域下载 project 的 tar 压缩包(图 8.11)。
图 8.11 Project 会作为一个输出产物出现,其中包含模型训练产物。
解压该压缩包,找到 best.pt 文件,并把它复制到一个方便的位置。把权重路径替换成 best.pt 的位置后重新运行代码,如下所示。
清单 8.16 使用最佳训练权重运行
import glob
import cv2
from ultralytics import YOLO
model = YOLO("weights/best.pt") #1
for file in glob.glob("samples/**.png"):
result = model(cv2.imread(file))
res_plotted = result[0].plot()
cv2.imshow("result", res_plotted)
cv2.waitKey(0)
#1 将权重位置改为指向 best.pt
(空)
为了证明模型训练确实有效,图 8.12 展示了仅训练 1 个 epoch 的结果。
图 8.12 仅训练一个 epoch 也能得到不错的结果
虽然结果并不完美,但确实很有希望!边界框还不够理想,但至少包住了卡片的大部分。更重要的是,它也把身份(类别)识别对了!
接着,如果再多训练一点(大约 5 个 epoch),我们就能得到更好的结果!不仅边界框能完整覆盖身份证,而且置信度也还不错(见图 8.13)。这是因为更多训练会通过微调权重与偏置来进一步提升模型表现。
图 8.13 多训练一点得到更好的检测;现在整张身份证都被选中了
在训练过程中增加更多 epoch,我们通常能在几个关键方面看到提升:
- 模型准确率——更多训练循环让模型更擅长识别模式并给出准确预测。
- 置信度——模型见到更多目标类别(这里是身份证)的样本后,会对检测更有把握。
- 鲁棒性——通过接触更大的数据集,模型更能适应不同条件,例如不同光照或角度。
在这个案例里,仅增加到 5 个 epoch 就带来了显著改进。不过,具体需要多少 epoch,取决于你的数据集复杂度以及项目需求。
你可能会想:到底该设置多少 epoch?Ultralytics 库会追踪验证表现,并将表现最好的权重保存为 best.pt——即使你训练了 100 个 epoch,best.pt 也可能来自第 4 个 epoch(因为性能可能在某个点后进入平台期甚至变差)。关键在于把 epoch 设得足够:如果你只训练 3 个 epoch,那么 best.pt 只是这 3 个里最好的一个,很可能仍然训练不足。
一旦你确认模型确实可用,就应该开始思考如何把验证自动化。从一开始你就应该考虑:如何以编程方式确保某个数据集上的置信度高于某个阈值,同时边界框坐标也合理。你无法为它们指定一个绝对精确的值,因此需要设置一定的容忍度(tolerance)。
到这里,我们已经走过了训练模型并尝试推理的大部分基础流程。超参数的选择与调优是一个迭代过程,所以我们通常从尽可能少的一组参数开始。
在实验过程中,我们可以通过调整之前训练任务中的超参数来发现让模型更贴合用例的优化空间。例如,如果你第一次训练的模型看起来没有收敛,你可以调整学习率、batch size 或 epoch 数来提升性能。随着用例与需求演进,这种方法也允许我们逐步扩展可配置参数的列表。
虽然我们已经为模型训练与验证打下了很强的基础,但生产 ML 系统需要的不只是训练良好的模型——它们还需要健壮的数据管理与全面的实验追踪。在第 9 章,我们会通过引入 Kubernetes Persistent Volumes 来实现更高效的数据处理,并集成 MLflow、TensorBoard 等高级追踪工具。它们会帮助我们构建更可扩展的训练工作流,并让我们对模型开发过程有更好的可见性。我们也会把这些概念应用到电影推荐系统上,展示这些模式如何适配不同的 ML 问题。
小结
- 将训练数据送入模型有多种方式。无论选择哪种方法,都必须支持版本控制与血缘(lineage)。
- Kubeflow 提供了多种访问下载结果的方式,本章讨论过其中一些,包括直接下载。
- 在决定模型架构时,必须始终牢记最终模型的需求与可接受的限制。公共指标再好,如果你不理解它与需求之间的取舍,后续一定会遇到问题。
- 编写训练代码时,要重视数据访问与传递。创建需要部署到自动化流水线中的训练代码库时,应始终假设“本地访问”不是最好的方案。
- 指标,以及更重要的——指标的可见性,是大型组织和/或大规模使用训练流水线的团队成功的关键。像 Kubeflow dashboard 这样能快速可视化流水线指标的地方,能显著缩短反馈回路并促进协作。
- 自动化中一个经常被忽视的方面,是能够快速对某个模型的输出权重进行 sanity test。在设计数据流与模型存储策略时把这点考虑进去,能在后续调试与仪表盘工作流中省下大量麻烦。