我见过太多这样的场景:Notebook 里把模型调准确率调到了 92%,看起来顺风顺水,结果要把这套东西交付出去的时候,对方拿到的还是一个 .ipynb 文件,里面依赖没写清楚、路径是写死的、跑到某个 cell 还要手动点一下按钮。诚然,Notebook 做探索性实验是无敌的,但一旦数据每天更新、团队要协作、结果要可复现,你就需要一条真正的数据流水线。
这篇文章的思路很简单:以 GCP 为底座,讲讲怎么从 Notebook 里的“能跑”,一步步走到流水线里的“能自动跑、能重复跑、能追踪产物”。适合那些已经在 Jupyter/Notebook 里调过模型,但还没碰过 Cloud Composer、Vertex AI Pipelines 这类编排工具的人。我会把中间最容易被忽视的几个坑也一并放在里面,这些坑我基本都踩过。
1. 先在 Notebook 里跑通模型,再回答“要不要上流水线”
1.1 为什么我宁愿把机器学习实验放在 GCP,而不是本地
最早我做实验其实是本地跑的,公司配的机器还算不错,但一旦要跑稍微大一点的数据,CPU 直接打满,风扇声跟飞机起飞一样,更别提同时开好几个实验的时候了。后来我切到 GCP 上的 Notebook 实例,一个很现实的变化是:实验环境跟数据放到了同一个地方。
GCP 里数据一般在 Cloud Storage(GCS)或者 BigQuery 里,你本地写代码去连这些服务,网络延迟是一回事,权限配置又是一回事。把 Notebook 放在 GCP 上的优势在于,它跟 GCS、BigQuery 在同一个内网,你只需要在服务账号上配好 IAM 权限,读写数据的耗时能少一个量级。另外 GPU 是按分钟计费的,训练完把实例停掉,成本可控,不用一直供着一台吃灰的机器。
对于个人学习和中小型项目来说,n1-standard-4 这类 CPU 实例基本够用,先用 CPU 跑通逻辑,再临时加 GPU 跑大模型,是比较正常的节奏。我自己一般先开一个不带 GPU 的实例做数据探索和代码调试,确认没问题再单独开一个带 GPU 的实例做训练,省钱的习惯就是从这里养成的。
1.2 Notebook 实例创建:一份不会出错的初始化清单
GCP 的 AI Platform Notebooks 现在已经整合到 Vertex AI Workbench 里了,界面创建实例的流程很傻瓜,选区域、选机型、选镜像,点创建。但有几个细节值得单独说一下:
- 区域尽量跟你的数据所在区域一致。数据在
us-central1,实例开在asia-east1,跨区域访问 GCS 虽然不报错,但会产生网络费用和延迟,没必要。 - 镜像建议选带预装 PyTorch 或 TensorFlow 的 Deep Learning 镜像,省去后面装框架的时间。镜像选完之后,系统会默认创建一个
jupyter用户,Python 环境很干净。 - 重点来了:很多人在创建实例之后会用终端手动
pip install一堆包,觉得环境已经装好了。但实际上一旦实例停止再启动,部分系统盘环境会被重置,你手动装的东西可能会丢。更稳妥的做法是写一个启动脚本,在实例每次启动时自动执行安装,或者干脆用 conda 建一个独立环境来装依赖。
我给你一个可以直接用的启动脚本示例:
bash复制#!/bin/bash
set -e
# 这里放到 Notebook 实例的“自定义启动脚本”里
source /opt/conda/etc/profile.d/conda.sh
conda activate base
pip install --quiet \
pandas-gbq \
scikit-learn \
lightgbm \
google-cloud-storage \
gcsfs \
kfp==2.*
这段脚本做的事情很简单:每次实例启动时,确保这些常用包是存在的。看似多此一举,却能在“实例重启后环境丢失”这种问题上给你省下大量排查时间。
1.3 什么时候才真正需要数据流水线
这是很现实的问题。不是所有机器学习项目都需要上流水线,有些人只是拿 Notebook 做一次性的分析,那确实没必要折腾。但如果你遇到下面这些情况之一,就应该认真考虑流水线了:
- 数据每天或每周更新,模型需要定期重跑;
- 训练数据来源不止一个,需要先合并、清洗再建模;
- 有队友需要复现你的实验,但他不可能把你 Notebok 里的 cell 一个个手工执行;
- 你需要向别人证明某次模型指标是哪个数据集、哪份代码、哪个超参数跑出来的;
- 训练完成之后还要自动部署模型或者自动产出报告。
当你发现自己开始做“每天早上手动跑一遍同一个 Notebook”这种毫无技术含量的事情,就该转向流水线了。流水线真正要替代的,不是模型训练本身,而是那些重复、易错、靠人来保证的中间流程。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. Notebook 实验环境的几个坑:从 IAM 到依赖冲突
2.1 权限问题:为什么你连 GCS 里的文件都读不到
Notebook 实例创建好之后,默认会带一个 Compute Engine 默认服务账号,多数情况下访问 GCS 是没问题的。但有个常见翻车现场:你在 GCS 的某个 bucket 里只给某个子账号开了权限,Notebook 用的是默认服务账号,两边对不上,读取数据就 403。
最好的做法是单独创建一个服务于机器学习任务的服务账号,而不是用默认账号一把梭。比如:
bash复制gcloud iam service-accounts create ml-notebook-sa \
--display-name="ML Notebook SA"
gcloud projects add-iam-policy-binding my-project \
--member="serviceAccount:ml-notebook-sa@my-project.iam.gserviceaccount.com" \
--role="roles/storage.objectViewer"
然后创建 Notebook 实例时,在服务账号参数里指定这个账号:
bash复制gcloud notebooks instances create ml-workbench \
--location=$REGION \
--machine-type=n1-standard-4 \
--service-account=ml-notebook-sa@my-project.iam.gserviceaccount.com \
--vm-image-project=deeplearning-platform-release \
--vm-image-family=common-cpu \
--metadata=startup-script='...'
权限的粒度也值得注意:只读数据用 objectViewer,需要写入模型文件时用 objectAdmin,别一上来就授予 owner。GCP 的权限体系虽然灵活,但给大了容易出事,给小了倒是好解决,加一条 binding 就行。建议从最小权限开始,缺什么再补什么。
另一个好习惯是:如果在 GCS 上做实验,可以把 gcsfuse 或者 gcsfs 装好,直接用 gs:// 路径读写文件。gcsfs 这个包在 Python 里用起来非常顺手。
python复制import pandas as pd
df = pd.read_csv("gs://your-bucket/data/train.csv")
这种方式省去了下载和上传文件的步骤,也让代码在 Notebook 和流水线之间迁移时改动更小。
2.2 依赖冲突:一个 Python 环境里装十个包的下场
Jupyter Notebook 安装依赖遇到的问题,热词里能看到一堆,比如 subprocess-exited-with-error。这种报错十有八九是当前的 Python 环境里某个包的编译版本冲突了。
我记得有一次装 paddlepaddle,pip 直接报 subprocess-exited-with-error,最后发现是 conda 环境里的 Python 版本是 3.11,而那个版本的 paddle 还没提供预编译的 wheel,pip 试图源码编译,然后编译环境缺依赖,就挂了。
这类问题最好的解法不是硬刚,而是隔离。在 Notebook 里为每个项目建一个独立的 conda 环境:
bash复制conda create -n ml-proj python=3.10 -y
conda activate ml-proj
pip install ...
在 Jupyter 里使用这个环境,还需要把这个环境注册到 kernel:
bash复制python -m ipykernel install --user --name=ml-proj --display-name="Python (ml-proj)"
之后在 Notebook 右上角切换 kernel 就行。这样不同项目之间的依赖互相隔离,新项目折腾坏了也不影响别的。
2.3 Jupyter 内核挂掉或无法打开——先看这些地方
Notebook 打不开、代码跑一半内核死掉,这类问题我在群里看到过无数次。大部分时候排查顺序是这样:
- 是不是内存不够了。
n1-standard-4只有 15GB 内存,如果你的训练数据或中间结果把内存打满,内核会直接被杀。打开dmesg或者监控面板看有没有 OOM kill,如果有,要么换大内存机型,要么把数据分块处理。 - 是不是磁盘满了。Notebook 实例默认会挂载一块磁盘,容器镜像、conda 环境、Notebook 文件都往里塞,很容易满。实例创建时直接把启动盘和磁盘容量调大一点,比如 100GB,能省去很多后续麻烦。
- 是不是 kernel 跟 Python 环境对不上。你在终端装了包,但 Jupyter kernel 用的是另一个环境,导致 import 失败。解决办法是显式指定内核对应的环境。
3. 从 Notebook 到流水线:需要改掉的三个心智习惯
3.1 不再依赖“执行顺序”
Notebook 最大的优点是交互式探索,最大的坑也是这一条:整个 notebook 的状态是隐式共享的。你可能在第一个 cell 里加载了数据,跳了十几个 cell 之后,又在某个不起眼的 cell 里调用了前面那个变量,自己都忘了这个 cell 单独跑会报错。
但流水线不是一个有记忆的环境。每一个组件都是独立运行的,跑完就结束,变量不复存在。组件之间唯一能传递的东西,是你显式声明的输入和输出:文件路径、参数、产物。这是从 Notebook 到流水线最本质的思维转变:从“过程依赖”变成“产物依赖”。想通了这一点,写流水线组件的时候就不会觉得别扭了。
3.2 不再依赖“手工触发”
在 Notebook 里跑流程,每一步都在你的掌控之中:数据加载好了,看一眼;模型训练完,点个按钮。流水线里没有“我看看”“我确认一下”这个环节。每个步骤要么成功执行完,要么失败并给出日志,没有中间态。
这就意味着你的组件代码必须能够完全自动运行,不能有任何 input() 或者等待人工确认的地方。如果有哪一步需要人工判断,你得把判断逻辑做成一个显式的门控任务,而不是让人盯着屏幕操作。
3.3 不再只让结果“留在屏幕上”
Notebook 里你训练完一个模型,在 cell 下面打印了 accuracy 值,然后保存了一堆 model.pkl、metrics.json 散落在工作目录里。过两天你回来看,都不知道这份结果是用哪个数据集跑的。
流水线里每跑一次,都会产生一组可追踪的记录:用了什么代码版本、输入了哪个数据集、输出了哪个模型文件、各项指标是多少。你要做的是把这些东西清晰、显式地写出来,而不是等跑完之后再回忆。这也是为什么流水线代码更“啰嗦”的原因,但这种啰嗦,恰恰是它能被复现的保障。
4. 选型:Cloud Composer 还是 Vertex AI Pipelines?
4.1 Cloud Composer(Apache Airflow):稳定但重
Cloud Composer 是 GCP 托管的 Apache Airflow 服务。它在 GCP 生态里是个老大哥,很多数据团队本身就有 Airflow 的底子,所以选择它非常自然。
Airflow 适合做复杂的任务编排,比如一条数据流水线里既有数据抽取、又有模型训练、还有结果发送到 BigQuery。它的调度能力很强,支持 cron 表达式,也可以做复杂的依赖关系。如果你的团队里已经有数据工程师在用 Airflow,让机器学习任务并入同一条编排体系,运维视角会更统一。
但它的问题也很明显:Cloud Composer 默认会拉起一套包含至少三个节点的 Kubernetes 集群,哪怕你只是跑几个小任务,账面上最低成本也在每月上百美元级别。如果你的任务并不复杂,只是为了“让模型定时训练”就上 Composer,会感觉性价比很低。
4.2 Vertex AI Pipelines(KFP):和机器学习贴合更紧
Vertex AI Pipelines 是以 Kubeflow Pipelines(KFP)为基础的托管服务。它跟 Vertex AI 的其余组件无缝集成,比如模型训练、模型注册、端点部署。你可以在同一个 UI 里看数据流水线的运行状态、看模型评估曲线、甚至直接部署模型,这种体验对数据科学家来说非常友好。
KFP 的核心思想是“组件化”:把每一步抽象成一个组件,组件有明确的输入和输出,然后把这些组件串成一个管道。它的执行成本按实际运行的任务资源计费,没有 Composer 那种“必须养着一套环境”的开销。任务跑完,资源自动释放。对中小型团队和实验性质的项目来说,这个成本模型比 Composer 灵活太多。
4.3 我的选型建议
我自己的习惯是这样判断的:
| 判断维度 | Cloud Composer | Vertex AI Pipelines |
|---|---|---|
| 适合人群 | 数据工程师、已有 Airflow 生态团队 | 数据科学家、ML 工程师 |
| 学习成本 | 需要学 Airflow 概念,相对陡峭 | 和 Notebook 开发思维接近,上手快 |
| 调度能力 | Cron 表达式,非常灵活 | 支持定时调度,但以事件/手动触发为主 |
| 成本 | 常驻环境,费用偏高 | 按执行计费,跑多少算多少 |
| 与 Vertex AI 生态集成 | 一般 | 原生,无缝衔接训练/部署 |
如果你的核心诉求是“把机器学习实验工程化、自动化”,并且团队里没有专门的数据工程师,我建议直接用 Vertex AI Pipelines。如果你的团队已经用 Airflow 管理了一套复杂的数据流程,机器学习任务是其中一环,那 Cloud Composer 更合适。这两种方案没有绝对的优劣,关键看你的场景更像“机器学习项目”还是更像“数据平台里的子任务”。
5. 搭建一条最小可用数据流水线:一个真实可抄的作业
下面我用一套很常见的流程做演示:从 GCS 读取 CSV 数据,做简单的数据准备,训练一个逻辑回归模型,记录准确率指标。这里我用 Vertex AI Pipelines + KFP 2.x 的轻量组件来实现,注意生产环境建议用容器组件,这个区别我后面会单独说。
5.1 把训练代码改造成一个“函数化的组件”
Notebook 里的代码往往是这种风格:
python复制df = pd.read_csv("...")
X = df.drop("target", axis=1)
...
流水线组件要改成“无状态函数”,输入和输出都通过参数显式声明。以数据准备为例:
python复制# components.py
from kfp.dsl import component, Input, Output, Dataset, Model, Metrics
@component(
base_image="python:3.10",
packages_to_install=["pandas", "gcsfs"]
)
def prepare_data(uri: str, data: Output[Dataset]):
import pandas as pd
df = pd.read_csv(uri)
df.to_csv(data.path, index=False)
这里的关键在于 Output[Dataset]。KFP 会为这个输出自动生成一个临时文件路径,你往 data.path 写入文件,这个文件会被托管上传到 GCS 的某一个固定位置,后续组件通过输入参数拿到这个路径。你不用自己去管文件上传细节,框架帮你做了。
5.2 数据准备、训练、评估三个组件串成一条流水线
继续写训练组件:
python复制@component(
base_image="python:3.10",
packages_to_install=["pandas", "scikit-learn", "joblib"]
)
def train_model(
data: Input[Dataset],
model: Output[Model],
metrics: Output[Metrics]
):
import pandas as pd
from sklearn.model_selection import train_test_split
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import accuracy_score
from joblib import dump
df = pd.read_csv(data.path)
X = df.drop(columns=["target"])
y = df["target"]
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, random_state=42
)
clf = LogisticRegression(max_iter=1000)
clf.fit(X_train, y_train)
acc = accuracy_score(y_test, clf.predict(X_test))
dump(clf, model.path)
metrics.log_metric("accuracy", float(acc))
注意 metrics.log_metric 会把指标写到 KFP 的追踪系统里,这样你在运行记录里就能直接看到准确率,而不只是日志里的文本输出。
最后把它们串起来:
python复制from kfp.dsl import pipeline
@pipeline(name="iris-training-pipeline")
def ml_pipeline(data_uri: str = "gs://your-bucket/iris.csv"):
prep = prepare_data(uri=data_uri)
train = train_model(data=prep.outputs["data"])
数据集用经典 Iris 也好,换成自己的 CSV 也行,只要列里有 target 字段。把这段代码存成 pipeline.py,然后编译:
bash复制kfp compiler compile --py pipeline.py --output pipeline.yaml
如果你用的是 kfp 3.x,编译命令也可以写成 kfp compiler compile --py pipeline.py --output pipeline.yaml,语法差异不大。组件装饰器在 kfp 3.x 里被统一到了 @dsl.component,但函数体内部逻辑完全一致。
5.3 提交到 Vertex AI Pipelines 并查看运行记录
编译出 pipeline.yaml 之后,提交运行有两种方式。第一种是用 gcloud:
bash复制PROJECT_ID=your-project-id
REGION=us-central1
gcloud ai pipelines run \
--project=$PROJECT_ID \
--region=$REGION \
--display-name=ml-pipeline-demo \
--pipeline-package-path=pipeline.yaml \
--parameter="data_uri=gs://your-bucket/iris.csv"
第二种是我更常用的方式,用 Python SDK 在 Notebook 里直接跑:
python复制from google.cloud import aiplatform
PROJECT_ID = "your-project-id"
REGION = "us-central1"
aiplatform.init(project=PROJECT_ID, location=REGION)
job = aiplatform.PipelineJob(
display_name="ml-pipeline-demo",
template_path="pipeline.yaml",
parameter_values={"data_uri": "gs://your-bucket/iris.csv"},
)
job.run()
提交之后,在 console 的 Vertex AI Pipelines 页面能看到一条运行记录,点进去能看到组件的依赖图,每个步骤的状态、日志、产物都能点开看。这一步的体验和 Notebook 完全不同,但也确实是生产环境该有的样子。
5.4 为什么生产环境要换成容器组件
轻量组件用起来舒服,是因为它把你的 Python 函数和依赖列表打包到了一个临时镜像里,方便是方便,但每次跑都要重新构建镜像,而且镜像环境里的系统依赖不好控制,比如有些模型库依赖一些底层 C 库。这时候容器组件的价值就体现出来了。
容器组件的做法是:把每个步骤写成一个独立的 Python 脚本,用 Dockerfile 把环境和脚本一起打成镜像,推到 Artifact Registry,然后在 KFP 里直接指定镜像地址运行。同样一个训练组件,代码主体不用大改,只要把传参方式从“函数参数”变成“命令行参数”就行:
bash复制python train.py \
--data-path <GCS路径> \
--model-path <GCS路径> \
--metric-path <GCS路径>
对应的 Dockerfile:
dockerfile复制FROM python:3.10-slim
WORKDIR /app
COPY train.py /app/train.py
RUN pip install pandas scikit-learn joblib google-cloud-storage gcsfs
ENTRYPOINT ["python", "/app/train.py"]
构建推送到 Artifact Registry:
bash复制gcloud auth configure-docker $REGION-docker.pkg.dev
docker build -t $REGION-docker.pkg.dev/$PROJECT_ID/ml-repo/train:latest .
docker push $REGION-docker.pkg.dev/$PROJECT_ID/ml-repo/train:latest
之后在流水线定义里直接用这个镜像地址启动容器即可。这样做的好处是:镜像一旦构建,环境就是完全固定的,不会出现“本地好好的,流水线里环境不一致”这种典型问题。
6. 流水线跑起来之后:监控、成本与踩坑清单
6.1 日志和指标怎么看
流水线跑完不是终点,长期维护才是。Vertex AI Pipelines 的运行详情页已经给你把日志集中好了。组件失败时,点进对应的组件,能看到标准输出和标准错误。我自己遇到流水线失败,第一步永远是先看日志尾部,多数情况是路径写错、权限不足、或者是依赖包版本不对。
自定义指标在“Metrics”标签页可以看到。类似前面代码里的 metrics.log_metric("accuracy", ...),它会以键值对的形式展示。如果你跑的模型评估比较复杂,也可以把 ROC 曲线这类图像存成 artifact,在 UI 里预览。
6.2 成本控制:别让训练完的实例继续烧钱
GCP 的计费是按秒的,这对用好的人来说是省钱,对“创建了实例忘了关”的人来说就是烧钱。我现在的基本习惯是:Notebook 实例不用了就停掉,长期不用的直接删掉。流水线里的训练任务用完即走,资源自动释放,没有这个问题。
之前带团队的时候,组里有个人创建了一台带 GPU 的 Notebook 实例,跑完实验后忘了关,连续烧了好几天,账单出来的时候整个人都不好了。从那以后我就给项目设置了预算告警,每天检查一次用量。你也可以在 Billing 里设置预算和告警阈值,一旦接近预警线就会收到邮件通知,这是防止账单爆炸最后一道防线。
6.3 一张避坑速查表
下面这张表是我在从 Notebook 到流水线的迁移过程中整理的常见问题,基本都能对号入座:
| 现象 | 常见原因 | 建议处理方式 |
|---|---|---|
| 组件报 403,读不到文件 | 服务账号缺少 GCS 权限 | 为任务单独建服务账号并授予最小必要权限 |
| 组件报 ModuleNotFoundError | 镜像里没有安装对应包 | 轻量组件检查 packages_to_install;容器组件检查依赖列表 |
| Notebook 实例重启后环境丢失 | 手动安装的包未持久化 | 使用启动脚本安装依赖,或者使用自定义容器镜像 |
| Notebook 内核跑一半被杀 | 内存或磁盘不足 | 查看 OOM 日志,扩大实例规格或分块处理数据 |
| 流水线触发任务比预期费用高 | 流水线运行过于频繁 | 调整调度频率,合并不必要的步骤 |
| 重跑流水线结果和上次不一致 | 代码或依赖版本未锁定 | 锁定镜像版本,固定随机种子 |
6.4 怎么让流水线定时跑起来
流水线搭好了,定时任务也是同一件事。Vertex AI Pipelines 支持把一条 pipeline 创建成 schedule,用 cron 表达式控制执行频率。在作业详情页的 “Schedule” 标签里创建一个新调度,填上 cron 表达式,比如每天凌晨 2 点跑一次:
code复制0 2 * * *
这样就不需要你在 Notebook 里手动提交运行了。新增数据之后模型会自动更新,产出的模型 artifact 会累积在 GCS 里,配合模型注册中心还能进一步做自动部署。
我在实际项目里是这么用的:原始数据每天由上游任务写入 GCS,凌晨流水线自动拉取新数据,完成训练和评估,如果指标相比上一版没有明显下降,就自动把模型注册到 Vertex AI Model Registry,供线上服务拉取。整条链路不需要人盯着,除非指标异常,会触发告警通知。
从这个项目里我体会最深的一点是:机器学习代码不是写完就完的,它更像是流水线上的一道工序,数据进来,模型出去,过程中每一步都要能被观察、被复现。Notebook 是探索的工具,流水线才是交付的载体。先适应这种“函数化、无状态、显式传参”的思维方式,再去看 KFP、Vertex AI Pipelines 这些东西,会发现一切都很顺理成章。如果你也正卡在“Notebook 里已经跑通了,但不知道怎么自动化”的阶段,希望这条从实例创建到流水线提交的路线能帮你少走几步弯路。
