1. 这不是“云上跑个模型”那么简单为什么 SageMaker 是我过去三年反复验证后最稳的 ML 生产落地方案你可能已经听过太多次“SageMaker 很强大”“SageMaker 很全”这类泛泛而谈。但作为在金融风控、电商推荐、工业质检三个垂直领域用 SageMaker 落地过 17 个线上模型的从业者我想先说句实在话SageMaker 的价值从来不在它功能列表有多长而在于它把“从数据到 API”这条链路上所有容易踩坑、容易扯皮、容易半夜被叫醒的环节都做了有迹可循的工程化封装。它不是教你怎么写算法而是教你怎么让算法真正活下来、跑得稳、改得快、查得清。我第一次用 SageMaker 是在 2021 年底一个银行客户要求把一个 XGBoost 风控模型从本地服务器迁移到云上同时要满足监管对模型可解释性、版本追溯和数据隔离的硬性要求。当时我们试了三种方案纯 EC2 自建环境、Kubeflow on EKS、以及 SageMaker。前两个方案在两周内都卡在了同一个地方——模型上线后当业务方提出“把上周三那个版本的模型再跑一遍历史数据做回溯分析”时EC2 环境里找不到当时的训练镜像和依赖包Kubeflow 的 pipeline 版本管理混乱得像一锅粥。而 SageMaker 的 Model Registry 里那个版本的模型、训练参数、输入数据 S3 路径、甚至当时用的 Spot 实例类型全都清清楚楚列在一行记录里点两下就复现。那一刻我才真正理解所谓“端到端”不是流程图上画个箭头而是每个环节的输出都天然带着“身份证”。这篇文章就是我把这三年踩过的坑、攒下的经验、压箱底的配置模板全部拆开揉碎给你讲透。它不讲概念不堆术语只讲你明天打开 AWS 控制台就能照着做的实操。核心关键词是SageMaker Studio、SKLearn Estimator、自定义训练脚本、Spot 训练成本控制、超参调优实战、Model Monitor 数据漂移预警、以及最关键的——如何避免“模型训出来了却部署不上线”的经典困局。适合两类人一类是刚接触云原生 ML 的数据科学家想跳过环境搭建的泥潭直接聚焦模型本身另一类是负责模型交付的工程师需要一套能经受住生产环境拷问的标准化流程。下面所有内容都是我在真实项目中反复验证、删减、优化后的结果没有一句是纸上谈兵。2. 整体设计思路为什么 SageMaker 不是“另一个 Jupyter Notebook”2.1 重新理解 SageMaker 的三层架构Studio、Training、Hosting很多新手一上来就在 SageMaker Studio 里写代码、跑训练觉得这就是 SageMaker 的全部。这是最大的误解。SageMaker 的本质是一个分层解耦、职责清晰的机器学习操作系统。它由三个逻辑上独立、物理上可分离的模块组成SageMaker Studio这是你的“驾驶舱”一个基于 Web 的、全功能的集成开发环境IDE。它底层是运行在 EC2 上的 JupyterLab 实例但它的价值远不止于此。Studio 的核心能力是统一元数据管理——你在里面创建的所有 notebook、启动的所有终端、甚至上传的每一个文件都会被自动打上标签、记录访问日志、关联到你的 IAM 角色。这意味着当你和同事协作时不需要再发邮件说“代码在 A 目录数据在 B 桶”因为整个工作空间的状态本身就是可追溯、可共享的。我习惯把它看作一个“带版本控制的实验室笔记本”而不是一个计算资源。SageMaker Training这才是 SageMaker 的“肌肉”。当你调用estimator.fit()时Studio 只是发出了一个指令真正的训练任务是在完全独立的、按需启动的计算集群上执行的。这个集群可以是 CPU 实例如ml.c5.xlarge也可以是 GPU 实例如ml.p3.2xlarge甚至可以是 Spot 实例。关键在于训练环境与开发环境彻底隔离。你在 Studio 里装的 pandas 版本和训练集群里用的 scikit-learn 版本互不影响。这种隔离是保证“本地能跑线上也能跑”这一基本诉求的基石。我见过太多项目因为开发机和训练机的 numpy 版本差了一个小数点导致模型预测结果出现毫秒级偏差最后排查了三天。SageMaker Hosting这是 SageMaker 的“出口”。训练好的模型最终要变成一个能被业务系统调用的 API。SageMaker 提供两种方式实时推理端点Real-time Endpoint和批量转换Batch Transform。前者适合低延迟、高并发的在线服务比如用户点击商品时实时返回推荐分数后者适合处理海量离线数据比如每天凌晨跑一次全量用户画像更新。Hosting 层的核心价值是自动扩缩容和内置监控。你不需要自己写 Kubernetes 的 HPAHorizontal Pod AutoscalerSageMaker 会根据请求 QPS 自动增减实例数量你也不需要自己搭 PrometheusGrafanaSageMaker Model Monitor 会自动采集延迟、错误率、CPU 利用率等指标并在 CloudWatch 里生成仪表盘。这三层架构的设计哲学就是把“写代码”、“算模型”、“供服务”这三个不同专业领域的工作用清晰的边界划分开来。你作为数据科学家只需要关心 Studio 里的 notebook 和 Training 里的脚本运维工程师则只需关注 Hosting 层的端点配置和告警策略。这种分工极大降低了团队协作的沟通成本。2.2 为什么必须写“自定义训练脚本”而不是直接在 notebook 里 train()这是新手最容易犯的第二个错误。看到 notebook 里model.fit(X_train, y_train)能跑通就以为万事大吉。但请记住SageMaker 的训练作业本质上是一个 Docker 容器的生命周期管理。当你调用estimator.fit()时SageMaker 做了三件事根据你指定的framework_version如0.23-1拉取一个预构建的、包含 scikit-learn 环境的 Docker 镜像将你写的script.py文件复制进这个镜像的/opt/ml/code/目录在这个容器里执行命令python /opt/ml/code/script.py --n-estimators 100 --train s3://my-bucket/train/ ...。这个过程和你在本地 terminal 里执行python script.py是完全不同的。本地执行时你的脚本可以随意 import 当前目录下的.py文件可以读取当前路径下的config.json甚至可以os.system(wget ...)。但在 SageMaker 的容器里这些操作要么失败要么行为不可控。所以写一个合格的 SageMaker 训练脚本核心原则就一条一切外部依赖必须通过命令行参数显式声明。这包括数据路径不能写死pd.read_csv(data/train.csv)必须写成pd.read_csv(os.path.join(args.train, args.train_file))。因为args.train的值是由 SageMaker 在容器启动时通过环境变量SM_CHANNEL_TRAIN注入的它指向的是你上传到 S3 的训练数据路径。模型保存路径不能写joblib.dump(model, model.joblib)必须写joblib.dump(model, os.path.join(args.model_dir, model.joblib))。args.model_dir同样由SM_MODEL_DIR注入SageMaker 会确保这个目录下的所有文件在训练结束后被自动打包上传到你指定的 S3 桶里成为后续部署的“模型工件”。超参数不能写RandomForestClassifier(n_estimators100)必须写RandomForestClassifier(n_estimatorsargs.n_estimators)。这样你才能在estimator的hyperparameters字典里动态修改它实现超参调优。我曾经在一个项目里因为脚本里漏写了--test-file参数的默认值导致调优时所有试验都只用了训练集做验证最终选出来的“最优”模型在测试集上惨不忍睹。这个教训让我养成了一个习惯每次写完脚本第一件事就是在本地模拟 SageMaker 的环境用python script.py --train ./data --test ./data --model-dir ./output这样的命令跑一遍确保它能在任何路径下独立工作。这一步省下的调试时间远超你写脚本的时间。2.3 成本控制不是“选个便宜机器”Spot 实例的正确打开方式AWS 的定价策略是很多团队望而却步的原因。但 SageMaker 的成本优势恰恰体现在它对 Spot 实例的深度集成上。Spot 实例简单说就是 AWS 把自己多余的、未被预约的计算资源以远低于按需实例On-Demand的价格通常低 60%-90%卖给你。它的代价是当 AWS 自己需要这些资源时会提前 2 分钟通知你然后终止你的实例。很多人一听“会被中断”就觉得 Spot 不可靠只敢用在离线批处理上。但在 SageMaker 里Spot 是训练场景的“默认选项”原因在于 SageMaker 的训练作业本身就是一个有明确起止点、状态可持久化的任务。它不像一个 Web 服务器需要 7x24 小时在线。一次训练从开始到结束就是一个完整的、可重入的流程。SageMaker 对 Spot 的支持体现在两个关键参数上use_spot_instancesTrue告诉 SageMaker这次训练请尽量使用 Spot 实例。max_wait7200设置最长等待时间秒。如果 2 小时内都抢不到合适的 Spot 实例SageMaker 会自动 fallback 到按需实例确保你的任务不会无限期挂起。max_run3600设置最长运行时间秒。这是为了防止 Spot 实例真的被中断后任务从头再来一遍。SageMaker 会定期将训练的中间状态checkpoints保存到 S3。一旦实例中断它会在新的 Spot 实例上从最近的 checkpoint 恢复训练而不是从零开始。我在一个图像分类项目中用ml.p3.16xlarge8 块 V100 GPU训练 ResNet50按需价格是 $24.48/小时。开启 Spot 后实际花费是 $3.21/小时总训练时间 4.5 小时成本从 $110 降到了 $14.5。更重要的是整个过程我完全不用操心中断问题SageMaker 全权负责了状态恢复。所以我的建议是除非你的训练任务小于 5 分钟否则默认开启 Spot。把max_wait设为 1 小时max_run设为你预估训练时间的 1.5 倍这是一个非常稳健的组合。3. 核心细节解析从数据准备到模型部署的每一步避坑指南3.1 数据上传与 S3 桶设计别让“桶名冲突”毁掉你的一天S3 是 SageMaker 的“数据粮仓”但它的设计远比想象中重要。很多团队在项目初期随便起了个my-first-model-bucket结果随着项目增多不同团队、不同环境dev/staging/prod的数据混在一起权限管理一团糟最后不得不花一周时间做数据迁移。我的经验是S3 桶的命名和目录结构应该遵循“环境-项目-用途”三级原则桶名company-name-env-project-name-region。例如acme-ml-dev-dry-bean-us-east-1。这样桶名本身就包含了环境dev、项目dry-bean、地域us-east-1信息全局唯一且一目了然。目录结构在桶内按sagemaker/project-name/stage/组织。例如s3://acme-ml-dev-dry-bean-us-east-1/ ├── sagemaker/ │ └── dry-bean/ │ ├── data/ # 原始数据、清洗后数据 │ │ ├── raw/ │ │ └── processed/ │ ├── models/ # 训练好的模型工件 │ │ ├── v1.0.0/ │ │ └── v1.0.1/ │ ├── notebooks/ # Studio 中使用的 notebook │ └── scripts/ # 训练/评估/部署脚本这种结构的好处是你可以用 IAM 策略精确控制权限。比如给数据科学家的 IAM 角色只允许s3:GetObject和s3:PutObject权限且仅限于s3://acme-ml-dev-dry-bean-us-east-1/sagemaker/dry-bean/data/*这个前缀。这样他们就无法误删模型或覆盖别人的 notebook。上传数据时sess.upload_data()是最便捷的方式但它有一个隐藏的坑它默认会递归上传整个目录下的所有文件包括.ipynb_checkpoints这种隐藏文件。这些文件虽然小但会污染你的 S3 桶更严重的是如果你的训练脚本里写了os.listdir(args.train)来遍历所有文件它可能会把.ipynb_checkpoints也当成数据文件读进来导致报错。解决方案很简单在上传前先清理一下import os import shutil # 清理本地临时文件 for root, dirs, files in os.walk(./data): for dir_name in dirs: if dir_name .ipynb_checkpoints: shutil.rmtree(os.path.join(root, dir_name)) # 再上传 trainpath sess.upload_data( path./data/train, bucketBUCKET_NAME, key_prefixsagemaker/dry-bean/data/train )3.2 数据探索EDA在 SageMaker Studio 里做 EDA 的独特优势在本地做 EDA你可能习惯用matplotlib画一堆图然后保存成 PNG 发给同事。在 SageMaker Studio 里这个过程可以更高效、更协作。Studio 的 notebook 有一个隐藏功能它可以直接渲染 Plotly、Bokeh 等交互式图表库。这意味着你画的散点图矩阵pairplot不再是静态的 PNG而是一个可以缩放、拖拽、悬停查看具体数值的动态图表。这对于发现数据中的异常模式比如某个类别的样本在某个特征维度上明显聚集非常有帮助。更重要的是Studio 的 EDA 结果是“可执行的”。举个例子你发现Area和Perimeter这两个特征高度相关相关系数 0.95你想快速验证去掉其中一个会不会影响模型效果。在本地你得改代码、重新跑训练。在 Studio 里你可以在同一个 notebook 里紧接着 EDA 的 cell 下面直接写一个简短的训练脚本只用Area特征然后调用sklearn_estimator.fit()。整个过程数据、代码、结果都在一个上下文里无需切换窗口、无需复制粘贴。我还有一个小技巧在做 EDA 时我会习惯性地把关键统计信息用print()打印出来并加上注释。比如print(fDataset shape: {dry_bean.shape}) # 总共 13611 行17 列 print(fClass distribution:\n{dry_bean[Class].value_counts()}) # 确认是否平衡这里各豆类数量接近无需过采样 print(fMissing values:\n{dry_bean.isnull().sum()}) # 确认无缺失值省去 fillna 步骤这些print输出会成为你后续写训练脚本时的“需求说明书”。当你在写script.py时看到# 确认无缺失值这行注释就会立刻知道脚本里不需要写数据清洗逻辑可以直奔模型训练。3.3 特征工程与预处理为什么“在训练脚本里做”比“在 notebook 里做”更安全这个问题的答案关乎模型的可复现性。很多团队喜欢在 notebook 里用StandardScaler对数据做标准化然后把scaler对象和X_train_scaled一起保存。这看起来很完美但埋下了巨大的隐患。隐患在于标准化的参数均值、标准差是在训练集上计算出来的。当模型部署上线后对新来的单条数据做预测时你必须用完全相同的均值和标准差来对它进行标准化。如果这个 scaler 对象只存在 notebook 的内存里或者只保存在本地磁盘上那么当训练脚本在 SageMaker 的容器里运行时它根本找不到这个 scaler。正确的做法是把特征工程的逻辑完整地封装进训练脚本里。这样每次训练脚本都会从头开始计算 scaler并和模型一起保存。我的script.py里就有这样一段# 在 fit() 之前先做特征工程 from sklearn.preprocessing import StandardScaler # 只对数值特征做标准化跳过目标列 Class numeric_features X_train.select_dtypes(include[np.number]).columns.tolist() scaler StandardScaler() X_train_scaled pd.DataFrame( scaler.fit_transform(X_train[numeric_features]), columnsnumeric_features, indexX_train.index ) # 将标准化后的特征与非数值特征如果有合并 X_train_final pd.concat([X_train_scaled, X_train.drop(columnsnumeric_features)], axis1)然后在模型保存时把 scaler 也一起存进去# Persist both model and scaler joblib.dump(model, os.path.join(args.model_dir, model.joblib)) joblib.dump(scaler, os.path.join(args.model_dir, scaler.joblib))这样当模型被部署为端点时推理脚本inference.py就可以从 S3 下载model.joblib和scaler.joblib先用 scaler 对输入数据做标准化再用 model 做预测。整个流程环环相扣没有任何外部依赖。3.4 模型部署从“能跑通”到“能扛住”的关键配置模型训练成功只是万里长征第一步。部署成端点才是真正的考验。SageMaker 的estimator.deploy()方法看似简单但背后有大量可调参数决定了你的端点是“玩具”还是“生产级”。最关键的三个参数是initial_instance_count初始启动的实例数量。对于一个新上线的端点我通常设为1。不要贪多先让一个实例稳定运行观察指标。instance_type实例类型。ml.c5.large2 vCPU, 4 GiB RAM对于大多数 tabular 数据模型如 Random Forest, XGBoost绰绰有余。只有当你部署的是大型深度学习模型如 BERT时才需要考虑ml.g4dn.xlarge或更高配的 GPU 实例。endpoint_name端点名称。强烈建议加上时间戳和版本号例如dry-bean-rf-v1-20240604。这样当你需要回滚到旧版本时可以轻松地delete_endpoint旧的再deploy新的而不会因为重名导致冲突。部署完成后端点并不会立刻进入InService状态。它会经历Creating-Updating-InService三个阶段。这个过程通常需要 5-10 分钟。在这期间SageMaker 会下载模型工件、启动容器、加载模型到内存、并进行健康检查。一个常被忽略的细节是端点的健康检查Health Check是通过一个名为ping的 API 实现的。你可以在自己的inference.py里自定义这个逻辑。默认情况下它只是返回一个{status: ok}。但你可以让它更智能比如检查模型文件是否完整、检查 GPU 显存是否充足。这能让你在端点真正对外提供服务前就发现潜在问题。最后也是最重要的永远不要忘记为端点配置 CloudWatch 告警。至少要设置两个Invocations调用量如果连续 5 分钟为 0说明业务方没调用或者调用地址错了需要告警。5XXError服务端错误如果错误率超过 1%说明模型或推理脚本有问题需要立即介入。这些告警是你和线上模型之间的“哨兵”。它们不会帮你写代码但会在你睡觉时把你从梦中叫醒告诉你“嘿你的模型出事了。”4. 实操过程详解手把手带你完成 Dry Bean 分类项目的全流程4.1 环境初始化从零开始创建一个可复用的 Studio 域SageMaker Studio 的入口是“Domain”域。一个 Domain 就是一个独立的、多用户协作的 Studio 工作空间。创建 Domain是整个流程的第一步也是奠定安全和协作基础的关键一步。登录 AWS 控制台进入 SageMaker 服务点击左侧菜单的 “Domains”然后点击 “Create domain”。这里有几个必填项需要特别注意Domain name给你的域起个名字比如acme-ml-domain。这个名字会出现在你访问 Studio 的 URL 里https://domain-id.studio.region.sagemaker.aws。Authentication mode选择 “AWS Identity and Access Management (IAM) Identity Center (successor to AWS Single Sign-On)”。这是最安全、最符合企业规范的方式。它允许你通过公司统一的 SSO 账号登录而不是为每个用户单独创建 AWS IAM 用户。Default execution role这是最关键的安全配置。点击 “Create a new role”SageMaker 会为你创建一个名为AmazonSageMaker-ExecutionRole-timestamp的 IAM 角色。这个角色就是你的 Studio 用户在执行所有操作读 S3、启动训练、创建端点时所使用的身份。你需要在这个角色的权限策略里显式地添加对你的 S3 桶的访问权限。SageMaker 不会自动给你加这是必须手动完成的步骤。创建 Domain 后你需要邀请用户。点击 “Users” - “Add user”输入用户的邮箱必须是公司邮箱且已注册 IAM Identity Center。SageMaker 会向该邮箱发送一个邀请链接。用户点击链接用公司 SSO 登录后就会看到一个干净的 Studio 界面。提示首次登录时Studio 会引导你创建一个 “User profile”。在这里你可以选择默认的 “JupyterLab 3” 环境或者更轻量的 “JupyterLab 4”。我推荐 JupyterLab 3因为它的插件生态更成熟特别是对 Git 集成的支持更好。4.2 数据准备Dry Bean 数据集的本地处理与 S3 上传Dry Bean 数据集来自 UCI 机器学习库是一个经典的多分类数据集。它包含 13611 个样本每个样本有 16 个数值型特征如 Area, Perimeter和一个类别标签Class共 7 个类别。下载 ZIP 包后解压你会得到一个 Excel 文件Dry_Bean_Dataset.xlsx。我们需要先把它转成 CSV因为 SageMaker 的训练容器对 Excel 支持并不友好而 CSV 是通用标准。在 Studio 的 notebook 里执行以下代码import pandas as pd from pathlib import Path # 获取当前工作目录 cwd Path.cwd() # 构建数据文件路径 data_path cwd / data / Dry_Bean_Dataset.xlsx # 读取 Excel 并保存为 CSV beans pd.read_excel(data_path) beans.to_csv(cwd / data / dry_bean.csv, indexFalse)这一步完成后dry_bean.csv就会出现在你的 Studio 文件浏览器里。接下来创建 S3 桶。回到 AWS 控制台进入 S3 服务点击 “Create bucket”。桶名必须全局唯一所以不能叫dry-bean-bucket。我建议用acme-ml-dev-dry-bean-us-east-1这样的格式。创建完成后点击进入桶点击 “Upload”选择你刚刚生成的dry_bean.csv文件上传。注意上传时不要勾选 “Enable S3 Object Lock”除非你的合规要求强制需要。这个功能会增加复杂性对于开发环境完全没有必要。4.3 编写与调试训练脚本script.py的逐行解析现在我们来编写核心的script.py。在 Studio 的 notebook 里新建一个 cell输入%%writefile script.py import argparse import os import joblib import numpy as np import pandas as pd from sklearn.ensemble import RandomForestClassifier from sklearn.metrics import balanced_accuracy_score from sklearn.preprocessing import StandardScaler if __name__ __main__: print( Step 1: Parsing command-line arguments ) parser argparse.ArgumentParser() # 超参数 parser.add_argument(--n-estimators, typeint, default10) parser.add_argument(--min-samples-leaf, typeint, default3) # 路径参数 parser.add_argument(--model-dir, typestr, defaultos.environ.get(SM_MODEL_DIR)) parser.add_argument(--train, typestr, defaultos.environ.get(SM_CHANNEL_TRAIN)) parser.add_argument(--test, typestr, defaultos.environ.get(SM_CHANNEL_TEST)) parser.add_argument(--train-file, typestr, defaultdry-bean-train.csv) parser.add_argument(--test-file, typestr, defaultdry-bean-test.csv) args, _ parser.parse_known_args() print(fTrain data path: {os.path.join(args.train, args.train_file)}) print(fTest data path: {os.path.join(args.test, args.test_file)}) print( Step 2: Loading and preprocessing data ) # 读取数据 train_df pd.read_csv(os.path.join(args.train, args.train_file)) test_df pd.read_csv(os.path.join(args.test, args.test_file)) # 特征工程标准化 numeric_features train_df.select_dtypes(include[np.number]).columns.tolist() # 跳过目标列 if Class in numeric_features: numeric_features.remove(Class) scaler StandardScaler() X_train pd.DataFrame( scaler.fit_transform(train_df[numeric_features]), columnsnumeric_features, indextrain_df.index ) X_test pd.DataFrame( scaler.transform(test_df[numeric_features]), columnsnumeric_features, indextest_df.index ) # 准备目标变量 y_train train_df[Class] y_test test_df[Class] print( Step 3: Training the model ) model RandomForestClassifier( n_estimatorsargs.n_estimators, min_samples_leafargs.min_samples_leaf, n_jobs-1, random_state42 # 固定随机种子保证可复现 ) model.fit(X_train, y_train) print( Step 4: Evaluating the model ) train_acc balanced_accuracy_score(y_train, model.predict(X_train)) test_acc balanced_accuracy_score(y_test, model.predict(X_test)) print(fTrain Balanced Accuracy: {train_acc:.4f}) print(fTest Balanced Accuracy: {test_acc:.4f}) print( Step 5: Saving model and scaler ) # 保存模型 model_path os.path.join(args.model_dir, model.joblib) joblib.dump(model, model_path) print(fModel saved to {model_path}) # 保存 scaler scaler_path os.path.join(args.model_dir, scaler.joblib) joblib.dump(scaler, scaler_path) print(fScaler saved to {scaler_path})这段脚本我已经加入了详细的注释和分步打印print( Step X: ... )。这样当你在本地调试时可以清晰地看到每一步的执行情况。运行!python script.py --train ./data --test ./data --model-dir ./output如果看到所有 Step X 都顺利打印出来并且最后显示Model saved to ./output/model.joblib那就说明脚本完全正确。4.4 启动训练作业从fit()到 S3 模型工件的完整旅程脚本调试通过后我们就可以在 notebook 里正式启动 SageMaker 的训练作业了。首先导入必要的库并初始化import boto3 import sagemaker from sagemaker.sklearn.estimator import SKLearn from sagemaker import get_execution_role # 创建 SageMaker session sess sagemaker.Session() # 获取执行角色 role get_execution_role() # 定义框架版本务必与你的脚本兼容 FRAMEWORK_VERSION 0.23-1 # 创建 Estimator sklearn_estimator SKLearn( entry_pointscript.py, # 你的训练脚本 rolerole, # 执行角色 instance_count1, # 使用 1 台机器 instance_typeml.c5.xlarge, # CPU 实例性价比之选 framework_versionFRAMEWORK_VERSION, base_job_namedry-bean-rf, # 训练作业的基础名称 hyperparameters{ n-estimators: 100, min-samples-leaf: 3 }, # 开启 Spot 实例 use_spot_instancesTrue, max_wait3600, # 最多等 1 小时 max_run1800 # 最多运行 30 分钟 )然后就是激动人心的fit()时刻# 启动训练 training_job sklearn_estimator.fit( inputs{ train: trainpath, # 指向 S3 中训练数据的路径 test: testpath # 指向 S3 中测试数据的路径 }, waitTrue # 设置为 True会阻塞直到训练完成 )执行这行代码后你会在 notebook 的输出区域看到 SageMaker 实时打印的日志和你之前看到的完全一样INFO:sagemaker:Creating training-job with name: dry-bean-rf-2024-06-04-15-22-11-456 2024-06-04 15:22:11 Starting - Starting the training job... 2024-06-04 15:22:26 Starting - Preparing the instances for training... ... 2024-06-04 15:23:45 Completed - Training job completed训练完成后你可以在 S3 桶里找到一个名为sagemaker/dry-bean-rf-2024-06-04-15-22-11-456/output/model.tar.gz的文件。这就是 SageMaker 自动生成的模型工件包。它是一个压缩包解压后里面就包含了model.joblib和scaler.joblib这两个文件。这个.tar.gz文件就是你后续部署端点的“原材料”。4.5 模型部署与端点测试用 Python SDK 发送第一个预测请求模型工件有了下一步就是把它变成一个 API。在 notebook 里执行# 部署模型为实时端点 predictor sklearn_estimator.deploy( initial_instance_count1, instance_typeml.c5.large, endpoint_namedry-bean-rf-v1-20240604 ) # 测试端点 import json # 构造一个测试样本从测试集中随机取一行 sample test_df.iloc[0].to_dict() # 移除目标列 sample.pop(Class, None) # 将字典转为 JSON 字符串 payload json.dumps(sample) # 发送预测请求 response predictor.predict(payload) print(fPredicted class: {response})如果一切顺利你会看到类似Predicted class: b{predictions: [Bombay]}的输出。这说明你的模型端点已经成功上线并且可以正常工作了。注意predictor.predict()方法的输入必须是 JSON 格式的字符串。如果你传入的是 Python 字典它会报错。这是新手最常见的错误之一。5. 常见问题与排查技巧实录那些文档里不会写的“血泪史”5.1 问题速查表高频报错与终极解决方案报错信息根本原因排查步骤终极解决方案ClientError: An error occurred (ValidationException) when calling the CreateTrainingJob operation: No module named sklearn训练脚本中 import 的库在 SageMaker 的预构建镜像中不存在1. 检查framework_version是否与你脚本中用的库版本匹配。2. 查看 SageMaker 官方文档中该框架版本支持的库列表。更换framework_version或使用requirements.txt自定义依赖。例如在script.py同目录下创建requirements.txt写入scikit-learn1.0.2然后在SKLearn构造函数中添加source_dir.和dependencies[requirements.txt]。ModelError: Unable to load model: No module named joblib模型保存时用了joblib但推理容器里没有安装joblib1. 检查requirements.txt是否包含了joblib。2. 检查inference.py中的 import 语句是否正确。在
SageMaker端到端机器学习实战:从训练到部署的工程化避坑指南
发布时间:2026/6/18 4:18:57
1. 这不是“云上跑个模型”那么简单为什么 SageMaker 是我过去三年反复验证后最稳的 ML 生产落地方案你可能已经听过太多次“SageMaker 很强大”“SageMaker 很全”这类泛泛而谈。但作为在金融风控、电商推荐、工业质检三个垂直领域用 SageMaker 落地过 17 个线上模型的从业者我想先说句实在话SageMaker 的价值从来不在它功能列表有多长而在于它把“从数据到 API”这条链路上所有容易踩坑、容易扯皮、容易半夜被叫醒的环节都做了有迹可循的工程化封装。它不是教你怎么写算法而是教你怎么让算法真正活下来、跑得稳、改得快、查得清。我第一次用 SageMaker 是在 2021 年底一个银行客户要求把一个 XGBoost 风控模型从本地服务器迁移到云上同时要满足监管对模型可解释性、版本追溯和数据隔离的硬性要求。当时我们试了三种方案纯 EC2 自建环境、Kubeflow on EKS、以及 SageMaker。前两个方案在两周内都卡在了同一个地方——模型上线后当业务方提出“把上周三那个版本的模型再跑一遍历史数据做回溯分析”时EC2 环境里找不到当时的训练镜像和依赖包Kubeflow 的 pipeline 版本管理混乱得像一锅粥。而 SageMaker 的 Model Registry 里那个版本的模型、训练参数、输入数据 S3 路径、甚至当时用的 Spot 实例类型全都清清楚楚列在一行记录里点两下就复现。那一刻我才真正理解所谓“端到端”不是流程图上画个箭头而是每个环节的输出都天然带着“身份证”。这篇文章就是我把这三年踩过的坑、攒下的经验、压箱底的配置模板全部拆开揉碎给你讲透。它不讲概念不堆术语只讲你明天打开 AWS 控制台就能照着做的实操。核心关键词是SageMaker Studio、SKLearn Estimator、自定义训练脚本、Spot 训练成本控制、超参调优实战、Model Monitor 数据漂移预警、以及最关键的——如何避免“模型训出来了却部署不上线”的经典困局。适合两类人一类是刚接触云原生 ML 的数据科学家想跳过环境搭建的泥潭直接聚焦模型本身另一类是负责模型交付的工程师需要一套能经受住生产环境拷问的标准化流程。下面所有内容都是我在真实项目中反复验证、删减、优化后的结果没有一句是纸上谈兵。2. 整体设计思路为什么 SageMaker 不是“另一个 Jupyter Notebook”2.1 重新理解 SageMaker 的三层架构Studio、Training、Hosting很多新手一上来就在 SageMaker Studio 里写代码、跑训练觉得这就是 SageMaker 的全部。这是最大的误解。SageMaker 的本质是一个分层解耦、职责清晰的机器学习操作系统。它由三个逻辑上独立、物理上可分离的模块组成SageMaker Studio这是你的“驾驶舱”一个基于 Web 的、全功能的集成开发环境IDE。它底层是运行在 EC2 上的 JupyterLab 实例但它的价值远不止于此。Studio 的核心能力是统一元数据管理——你在里面创建的所有 notebook、启动的所有终端、甚至上传的每一个文件都会被自动打上标签、记录访问日志、关联到你的 IAM 角色。这意味着当你和同事协作时不需要再发邮件说“代码在 A 目录数据在 B 桶”因为整个工作空间的状态本身就是可追溯、可共享的。我习惯把它看作一个“带版本控制的实验室笔记本”而不是一个计算资源。SageMaker Training这才是 SageMaker 的“肌肉”。当你调用estimator.fit()时Studio 只是发出了一个指令真正的训练任务是在完全独立的、按需启动的计算集群上执行的。这个集群可以是 CPU 实例如ml.c5.xlarge也可以是 GPU 实例如ml.p3.2xlarge甚至可以是 Spot 实例。关键在于训练环境与开发环境彻底隔离。你在 Studio 里装的 pandas 版本和训练集群里用的 scikit-learn 版本互不影响。这种隔离是保证“本地能跑线上也能跑”这一基本诉求的基石。我见过太多项目因为开发机和训练机的 numpy 版本差了一个小数点导致模型预测结果出现毫秒级偏差最后排查了三天。SageMaker Hosting这是 SageMaker 的“出口”。训练好的模型最终要变成一个能被业务系统调用的 API。SageMaker 提供两种方式实时推理端点Real-time Endpoint和批量转换Batch Transform。前者适合低延迟、高并发的在线服务比如用户点击商品时实时返回推荐分数后者适合处理海量离线数据比如每天凌晨跑一次全量用户画像更新。Hosting 层的核心价值是自动扩缩容和内置监控。你不需要自己写 Kubernetes 的 HPAHorizontal Pod AutoscalerSageMaker 会根据请求 QPS 自动增减实例数量你也不需要自己搭 PrometheusGrafanaSageMaker Model Monitor 会自动采集延迟、错误率、CPU 利用率等指标并在 CloudWatch 里生成仪表盘。这三层架构的设计哲学就是把“写代码”、“算模型”、“供服务”这三个不同专业领域的工作用清晰的边界划分开来。你作为数据科学家只需要关心 Studio 里的 notebook 和 Training 里的脚本运维工程师则只需关注 Hosting 层的端点配置和告警策略。这种分工极大降低了团队协作的沟通成本。2.2 为什么必须写“自定义训练脚本”而不是直接在 notebook 里 train()这是新手最容易犯的第二个错误。看到 notebook 里model.fit(X_train, y_train)能跑通就以为万事大吉。但请记住SageMaker 的训练作业本质上是一个 Docker 容器的生命周期管理。当你调用estimator.fit()时SageMaker 做了三件事根据你指定的framework_version如0.23-1拉取一个预构建的、包含 scikit-learn 环境的 Docker 镜像将你写的script.py文件复制进这个镜像的/opt/ml/code/目录在这个容器里执行命令python /opt/ml/code/script.py --n-estimators 100 --train s3://my-bucket/train/ ...。这个过程和你在本地 terminal 里执行python script.py是完全不同的。本地执行时你的脚本可以随意 import 当前目录下的.py文件可以读取当前路径下的config.json甚至可以os.system(wget ...)。但在 SageMaker 的容器里这些操作要么失败要么行为不可控。所以写一个合格的 SageMaker 训练脚本核心原则就一条一切外部依赖必须通过命令行参数显式声明。这包括数据路径不能写死pd.read_csv(data/train.csv)必须写成pd.read_csv(os.path.join(args.train, args.train_file))。因为args.train的值是由 SageMaker 在容器启动时通过环境变量SM_CHANNEL_TRAIN注入的它指向的是你上传到 S3 的训练数据路径。模型保存路径不能写joblib.dump(model, model.joblib)必须写joblib.dump(model, os.path.join(args.model_dir, model.joblib))。args.model_dir同样由SM_MODEL_DIR注入SageMaker 会确保这个目录下的所有文件在训练结束后被自动打包上传到你指定的 S3 桶里成为后续部署的“模型工件”。超参数不能写RandomForestClassifier(n_estimators100)必须写RandomForestClassifier(n_estimatorsargs.n_estimators)。这样你才能在estimator的hyperparameters字典里动态修改它实现超参调优。我曾经在一个项目里因为脚本里漏写了--test-file参数的默认值导致调优时所有试验都只用了训练集做验证最终选出来的“最优”模型在测试集上惨不忍睹。这个教训让我养成了一个习惯每次写完脚本第一件事就是在本地模拟 SageMaker 的环境用python script.py --train ./data --test ./data --model-dir ./output这样的命令跑一遍确保它能在任何路径下独立工作。这一步省下的调试时间远超你写脚本的时间。2.3 成本控制不是“选个便宜机器”Spot 实例的正确打开方式AWS 的定价策略是很多团队望而却步的原因。但 SageMaker 的成本优势恰恰体现在它对 Spot 实例的深度集成上。Spot 实例简单说就是 AWS 把自己多余的、未被预约的计算资源以远低于按需实例On-Demand的价格通常低 60%-90%卖给你。它的代价是当 AWS 自己需要这些资源时会提前 2 分钟通知你然后终止你的实例。很多人一听“会被中断”就觉得 Spot 不可靠只敢用在离线批处理上。但在 SageMaker 里Spot 是训练场景的“默认选项”原因在于 SageMaker 的训练作业本身就是一个有明确起止点、状态可持久化的任务。它不像一个 Web 服务器需要 7x24 小时在线。一次训练从开始到结束就是一个完整的、可重入的流程。SageMaker 对 Spot 的支持体现在两个关键参数上use_spot_instancesTrue告诉 SageMaker这次训练请尽量使用 Spot 实例。max_wait7200设置最长等待时间秒。如果 2 小时内都抢不到合适的 Spot 实例SageMaker 会自动 fallback 到按需实例确保你的任务不会无限期挂起。max_run3600设置最长运行时间秒。这是为了防止 Spot 实例真的被中断后任务从头再来一遍。SageMaker 会定期将训练的中间状态checkpoints保存到 S3。一旦实例中断它会在新的 Spot 实例上从最近的 checkpoint 恢复训练而不是从零开始。我在一个图像分类项目中用ml.p3.16xlarge8 块 V100 GPU训练 ResNet50按需价格是 $24.48/小时。开启 Spot 后实际花费是 $3.21/小时总训练时间 4.5 小时成本从 $110 降到了 $14.5。更重要的是整个过程我完全不用操心中断问题SageMaker 全权负责了状态恢复。所以我的建议是除非你的训练任务小于 5 分钟否则默认开启 Spot。把max_wait设为 1 小时max_run设为你预估训练时间的 1.5 倍这是一个非常稳健的组合。3. 核心细节解析从数据准备到模型部署的每一步避坑指南3.1 数据上传与 S3 桶设计别让“桶名冲突”毁掉你的一天S3 是 SageMaker 的“数据粮仓”但它的设计远比想象中重要。很多团队在项目初期随便起了个my-first-model-bucket结果随着项目增多不同团队、不同环境dev/staging/prod的数据混在一起权限管理一团糟最后不得不花一周时间做数据迁移。我的经验是S3 桶的命名和目录结构应该遵循“环境-项目-用途”三级原则桶名company-name-env-project-name-region。例如acme-ml-dev-dry-bean-us-east-1。这样桶名本身就包含了环境dev、项目dry-bean、地域us-east-1信息全局唯一且一目了然。目录结构在桶内按sagemaker/project-name/stage/组织。例如s3://acme-ml-dev-dry-bean-us-east-1/ ├── sagemaker/ │ └── dry-bean/ │ ├── data/ # 原始数据、清洗后数据 │ │ ├── raw/ │ │ └── processed/ │ ├── models/ # 训练好的模型工件 │ │ ├── v1.0.0/ │ │ └── v1.0.1/ │ ├── notebooks/ # Studio 中使用的 notebook │ └── scripts/ # 训练/评估/部署脚本这种结构的好处是你可以用 IAM 策略精确控制权限。比如给数据科学家的 IAM 角色只允许s3:GetObject和s3:PutObject权限且仅限于s3://acme-ml-dev-dry-bean-us-east-1/sagemaker/dry-bean/data/*这个前缀。这样他们就无法误删模型或覆盖别人的 notebook。上传数据时sess.upload_data()是最便捷的方式但它有一个隐藏的坑它默认会递归上传整个目录下的所有文件包括.ipynb_checkpoints这种隐藏文件。这些文件虽然小但会污染你的 S3 桶更严重的是如果你的训练脚本里写了os.listdir(args.train)来遍历所有文件它可能会把.ipynb_checkpoints也当成数据文件读进来导致报错。解决方案很简单在上传前先清理一下import os import shutil # 清理本地临时文件 for root, dirs, files in os.walk(./data): for dir_name in dirs: if dir_name .ipynb_checkpoints: shutil.rmtree(os.path.join(root, dir_name)) # 再上传 trainpath sess.upload_data( path./data/train, bucketBUCKET_NAME, key_prefixsagemaker/dry-bean/data/train )3.2 数据探索EDA在 SageMaker Studio 里做 EDA 的独特优势在本地做 EDA你可能习惯用matplotlib画一堆图然后保存成 PNG 发给同事。在 SageMaker Studio 里这个过程可以更高效、更协作。Studio 的 notebook 有一个隐藏功能它可以直接渲染 Plotly、Bokeh 等交互式图表库。这意味着你画的散点图矩阵pairplot不再是静态的 PNG而是一个可以缩放、拖拽、悬停查看具体数值的动态图表。这对于发现数据中的异常模式比如某个类别的样本在某个特征维度上明显聚集非常有帮助。更重要的是Studio 的 EDA 结果是“可执行的”。举个例子你发现Area和Perimeter这两个特征高度相关相关系数 0.95你想快速验证去掉其中一个会不会影响模型效果。在本地你得改代码、重新跑训练。在 Studio 里你可以在同一个 notebook 里紧接着 EDA 的 cell 下面直接写一个简短的训练脚本只用Area特征然后调用sklearn_estimator.fit()。整个过程数据、代码、结果都在一个上下文里无需切换窗口、无需复制粘贴。我还有一个小技巧在做 EDA 时我会习惯性地把关键统计信息用print()打印出来并加上注释。比如print(fDataset shape: {dry_bean.shape}) # 总共 13611 行17 列 print(fClass distribution:\n{dry_bean[Class].value_counts()}) # 确认是否平衡这里各豆类数量接近无需过采样 print(fMissing values:\n{dry_bean.isnull().sum()}) # 确认无缺失值省去 fillna 步骤这些print输出会成为你后续写训练脚本时的“需求说明书”。当你在写script.py时看到# 确认无缺失值这行注释就会立刻知道脚本里不需要写数据清洗逻辑可以直奔模型训练。3.3 特征工程与预处理为什么“在训练脚本里做”比“在 notebook 里做”更安全这个问题的答案关乎模型的可复现性。很多团队喜欢在 notebook 里用StandardScaler对数据做标准化然后把scaler对象和X_train_scaled一起保存。这看起来很完美但埋下了巨大的隐患。隐患在于标准化的参数均值、标准差是在训练集上计算出来的。当模型部署上线后对新来的单条数据做预测时你必须用完全相同的均值和标准差来对它进行标准化。如果这个 scaler 对象只存在 notebook 的内存里或者只保存在本地磁盘上那么当训练脚本在 SageMaker 的容器里运行时它根本找不到这个 scaler。正确的做法是把特征工程的逻辑完整地封装进训练脚本里。这样每次训练脚本都会从头开始计算 scaler并和模型一起保存。我的script.py里就有这样一段# 在 fit() 之前先做特征工程 from sklearn.preprocessing import StandardScaler # 只对数值特征做标准化跳过目标列 Class numeric_features X_train.select_dtypes(include[np.number]).columns.tolist() scaler StandardScaler() X_train_scaled pd.DataFrame( scaler.fit_transform(X_train[numeric_features]), columnsnumeric_features, indexX_train.index ) # 将标准化后的特征与非数值特征如果有合并 X_train_final pd.concat([X_train_scaled, X_train.drop(columnsnumeric_features)], axis1)然后在模型保存时把 scaler 也一起存进去# Persist both model and scaler joblib.dump(model, os.path.join(args.model_dir, model.joblib)) joblib.dump(scaler, os.path.join(args.model_dir, scaler.joblib))这样当模型被部署为端点时推理脚本inference.py就可以从 S3 下载model.joblib和scaler.joblib先用 scaler 对输入数据做标准化再用 model 做预测。整个流程环环相扣没有任何外部依赖。3.4 模型部署从“能跑通”到“能扛住”的关键配置模型训练成功只是万里长征第一步。部署成端点才是真正的考验。SageMaker 的estimator.deploy()方法看似简单但背后有大量可调参数决定了你的端点是“玩具”还是“生产级”。最关键的三个参数是initial_instance_count初始启动的实例数量。对于一个新上线的端点我通常设为1。不要贪多先让一个实例稳定运行观察指标。instance_type实例类型。ml.c5.large2 vCPU, 4 GiB RAM对于大多数 tabular 数据模型如 Random Forest, XGBoost绰绰有余。只有当你部署的是大型深度学习模型如 BERT时才需要考虑ml.g4dn.xlarge或更高配的 GPU 实例。endpoint_name端点名称。强烈建议加上时间戳和版本号例如dry-bean-rf-v1-20240604。这样当你需要回滚到旧版本时可以轻松地delete_endpoint旧的再deploy新的而不会因为重名导致冲突。部署完成后端点并不会立刻进入InService状态。它会经历Creating-Updating-InService三个阶段。这个过程通常需要 5-10 分钟。在这期间SageMaker 会下载模型工件、启动容器、加载模型到内存、并进行健康检查。一个常被忽略的细节是端点的健康检查Health Check是通过一个名为ping的 API 实现的。你可以在自己的inference.py里自定义这个逻辑。默认情况下它只是返回一个{status: ok}。但你可以让它更智能比如检查模型文件是否完整、检查 GPU 显存是否充足。这能让你在端点真正对外提供服务前就发现潜在问题。最后也是最重要的永远不要忘记为端点配置 CloudWatch 告警。至少要设置两个Invocations调用量如果连续 5 分钟为 0说明业务方没调用或者调用地址错了需要告警。5XXError服务端错误如果错误率超过 1%说明模型或推理脚本有问题需要立即介入。这些告警是你和线上模型之间的“哨兵”。它们不会帮你写代码但会在你睡觉时把你从梦中叫醒告诉你“嘿你的模型出事了。”4. 实操过程详解手把手带你完成 Dry Bean 分类项目的全流程4.1 环境初始化从零开始创建一个可复用的 Studio 域SageMaker Studio 的入口是“Domain”域。一个 Domain 就是一个独立的、多用户协作的 Studio 工作空间。创建 Domain是整个流程的第一步也是奠定安全和协作基础的关键一步。登录 AWS 控制台进入 SageMaker 服务点击左侧菜单的 “Domains”然后点击 “Create domain”。这里有几个必填项需要特别注意Domain name给你的域起个名字比如acme-ml-domain。这个名字会出现在你访问 Studio 的 URL 里https://domain-id.studio.region.sagemaker.aws。Authentication mode选择 “AWS Identity and Access Management (IAM) Identity Center (successor to AWS Single Sign-On)”。这是最安全、最符合企业规范的方式。它允许你通过公司统一的 SSO 账号登录而不是为每个用户单独创建 AWS IAM 用户。Default execution role这是最关键的安全配置。点击 “Create a new role”SageMaker 会为你创建一个名为AmazonSageMaker-ExecutionRole-timestamp的 IAM 角色。这个角色就是你的 Studio 用户在执行所有操作读 S3、启动训练、创建端点时所使用的身份。你需要在这个角色的权限策略里显式地添加对你的 S3 桶的访问权限。SageMaker 不会自动给你加这是必须手动完成的步骤。创建 Domain 后你需要邀请用户。点击 “Users” - “Add user”输入用户的邮箱必须是公司邮箱且已注册 IAM Identity Center。SageMaker 会向该邮箱发送一个邀请链接。用户点击链接用公司 SSO 登录后就会看到一个干净的 Studio 界面。提示首次登录时Studio 会引导你创建一个 “User profile”。在这里你可以选择默认的 “JupyterLab 3” 环境或者更轻量的 “JupyterLab 4”。我推荐 JupyterLab 3因为它的插件生态更成熟特别是对 Git 集成的支持更好。4.2 数据准备Dry Bean 数据集的本地处理与 S3 上传Dry Bean 数据集来自 UCI 机器学习库是一个经典的多分类数据集。它包含 13611 个样本每个样本有 16 个数值型特征如 Area, Perimeter和一个类别标签Class共 7 个类别。下载 ZIP 包后解压你会得到一个 Excel 文件Dry_Bean_Dataset.xlsx。我们需要先把它转成 CSV因为 SageMaker 的训练容器对 Excel 支持并不友好而 CSV 是通用标准。在 Studio 的 notebook 里执行以下代码import pandas as pd from pathlib import Path # 获取当前工作目录 cwd Path.cwd() # 构建数据文件路径 data_path cwd / data / Dry_Bean_Dataset.xlsx # 读取 Excel 并保存为 CSV beans pd.read_excel(data_path) beans.to_csv(cwd / data / dry_bean.csv, indexFalse)这一步完成后dry_bean.csv就会出现在你的 Studio 文件浏览器里。接下来创建 S3 桶。回到 AWS 控制台进入 S3 服务点击 “Create bucket”。桶名必须全局唯一所以不能叫dry-bean-bucket。我建议用acme-ml-dev-dry-bean-us-east-1这样的格式。创建完成后点击进入桶点击 “Upload”选择你刚刚生成的dry_bean.csv文件上传。注意上传时不要勾选 “Enable S3 Object Lock”除非你的合规要求强制需要。这个功能会增加复杂性对于开发环境完全没有必要。4.3 编写与调试训练脚本script.py的逐行解析现在我们来编写核心的script.py。在 Studio 的 notebook 里新建一个 cell输入%%writefile script.py import argparse import os import joblib import numpy as np import pandas as pd from sklearn.ensemble import RandomForestClassifier from sklearn.metrics import balanced_accuracy_score from sklearn.preprocessing import StandardScaler if __name__ __main__: print( Step 1: Parsing command-line arguments ) parser argparse.ArgumentParser() # 超参数 parser.add_argument(--n-estimators, typeint, default10) parser.add_argument(--min-samples-leaf, typeint, default3) # 路径参数 parser.add_argument(--model-dir, typestr, defaultos.environ.get(SM_MODEL_DIR)) parser.add_argument(--train, typestr, defaultos.environ.get(SM_CHANNEL_TRAIN)) parser.add_argument(--test, typestr, defaultos.environ.get(SM_CHANNEL_TEST)) parser.add_argument(--train-file, typestr, defaultdry-bean-train.csv) parser.add_argument(--test-file, typestr, defaultdry-bean-test.csv) args, _ parser.parse_known_args() print(fTrain data path: {os.path.join(args.train, args.train_file)}) print(fTest data path: {os.path.join(args.test, args.test_file)}) print( Step 2: Loading and preprocessing data ) # 读取数据 train_df pd.read_csv(os.path.join(args.train, args.train_file)) test_df pd.read_csv(os.path.join(args.test, args.test_file)) # 特征工程标准化 numeric_features train_df.select_dtypes(include[np.number]).columns.tolist() # 跳过目标列 if Class in numeric_features: numeric_features.remove(Class) scaler StandardScaler() X_train pd.DataFrame( scaler.fit_transform(train_df[numeric_features]), columnsnumeric_features, indextrain_df.index ) X_test pd.DataFrame( scaler.transform(test_df[numeric_features]), columnsnumeric_features, indextest_df.index ) # 准备目标变量 y_train train_df[Class] y_test test_df[Class] print( Step 3: Training the model ) model RandomForestClassifier( n_estimatorsargs.n_estimators, min_samples_leafargs.min_samples_leaf, n_jobs-1, random_state42 # 固定随机种子保证可复现 ) model.fit(X_train, y_train) print( Step 4: Evaluating the model ) train_acc balanced_accuracy_score(y_train, model.predict(X_train)) test_acc balanced_accuracy_score(y_test, model.predict(X_test)) print(fTrain Balanced Accuracy: {train_acc:.4f}) print(fTest Balanced Accuracy: {test_acc:.4f}) print( Step 5: Saving model and scaler ) # 保存模型 model_path os.path.join(args.model_dir, model.joblib) joblib.dump(model, model_path) print(fModel saved to {model_path}) # 保存 scaler scaler_path os.path.join(args.model_dir, scaler.joblib) joblib.dump(scaler, scaler_path) print(fScaler saved to {scaler_path})这段脚本我已经加入了详细的注释和分步打印print( Step X: ... )。这样当你在本地调试时可以清晰地看到每一步的执行情况。运行!python script.py --train ./data --test ./data --model-dir ./output如果看到所有 Step X 都顺利打印出来并且最后显示Model saved to ./output/model.joblib那就说明脚本完全正确。4.4 启动训练作业从fit()到 S3 模型工件的完整旅程脚本调试通过后我们就可以在 notebook 里正式启动 SageMaker 的训练作业了。首先导入必要的库并初始化import boto3 import sagemaker from sagemaker.sklearn.estimator import SKLearn from sagemaker import get_execution_role # 创建 SageMaker session sess sagemaker.Session() # 获取执行角色 role get_execution_role() # 定义框架版本务必与你的脚本兼容 FRAMEWORK_VERSION 0.23-1 # 创建 Estimator sklearn_estimator SKLearn( entry_pointscript.py, # 你的训练脚本 rolerole, # 执行角色 instance_count1, # 使用 1 台机器 instance_typeml.c5.xlarge, # CPU 实例性价比之选 framework_versionFRAMEWORK_VERSION, base_job_namedry-bean-rf, # 训练作业的基础名称 hyperparameters{ n-estimators: 100, min-samples-leaf: 3 }, # 开启 Spot 实例 use_spot_instancesTrue, max_wait3600, # 最多等 1 小时 max_run1800 # 最多运行 30 分钟 )然后就是激动人心的fit()时刻# 启动训练 training_job sklearn_estimator.fit( inputs{ train: trainpath, # 指向 S3 中训练数据的路径 test: testpath # 指向 S3 中测试数据的路径 }, waitTrue # 设置为 True会阻塞直到训练完成 )执行这行代码后你会在 notebook 的输出区域看到 SageMaker 实时打印的日志和你之前看到的完全一样INFO:sagemaker:Creating training-job with name: dry-bean-rf-2024-06-04-15-22-11-456 2024-06-04 15:22:11 Starting - Starting the training job... 2024-06-04 15:22:26 Starting - Preparing the instances for training... ... 2024-06-04 15:23:45 Completed - Training job completed训练完成后你可以在 S3 桶里找到一个名为sagemaker/dry-bean-rf-2024-06-04-15-22-11-456/output/model.tar.gz的文件。这就是 SageMaker 自动生成的模型工件包。它是一个压缩包解压后里面就包含了model.joblib和scaler.joblib这两个文件。这个.tar.gz文件就是你后续部署端点的“原材料”。4.5 模型部署与端点测试用 Python SDK 发送第一个预测请求模型工件有了下一步就是把它变成一个 API。在 notebook 里执行# 部署模型为实时端点 predictor sklearn_estimator.deploy( initial_instance_count1, instance_typeml.c5.large, endpoint_namedry-bean-rf-v1-20240604 ) # 测试端点 import json # 构造一个测试样本从测试集中随机取一行 sample test_df.iloc[0].to_dict() # 移除目标列 sample.pop(Class, None) # 将字典转为 JSON 字符串 payload json.dumps(sample) # 发送预测请求 response predictor.predict(payload) print(fPredicted class: {response})如果一切顺利你会看到类似Predicted class: b{predictions: [Bombay]}的输出。这说明你的模型端点已经成功上线并且可以正常工作了。注意predictor.predict()方法的输入必须是 JSON 格式的字符串。如果你传入的是 Python 字典它会报错。这是新手最常见的错误之一。5. 常见问题与排查技巧实录那些文档里不会写的“血泪史”5.1 问题速查表高频报错与终极解决方案报错信息根本原因排查步骤终极解决方案ClientError: An error occurred (ValidationException) when calling the CreateTrainingJob operation: No module named sklearn训练脚本中 import 的库在 SageMaker 的预构建镜像中不存在1. 检查framework_version是否与你脚本中用的库版本匹配。2. 查看 SageMaker 官方文档中该框架版本支持的库列表。更换framework_version或使用requirements.txt自定义依赖。例如在script.py同目录下创建requirements.txt写入scikit-learn1.0.2然后在SKLearn构造函数中添加source_dir.和dependencies[requirements.txt]。ModelError: Unable to load model: No module named joblib模型保存时用了joblib但推理容器里没有安装joblib1. 检查requirements.txt是否包含了joblib。2. 检查inference.py中的 import 语句是否正确。在