前几天在 ModelArts 上把一个 ResNet50 图像分类任务从训练到部署完整跑了一遍,从写训练脚本、配数据路径,到最后发布成在线服务,中间确实没少踩坑。趁记忆还新鲜,把这份实操笔记整理出来,给正在用华为云 ModelArts 做模型训练和部署的同学做个参考。这个平台最大的好处是省心——训练资源按需申请,不跑训练就不收费,部署服务也能一键拉起,比较适合想快速验证模型效果,又不想维护一堆 GPU 服务器底层环境的人。
这篇笔记面向两类读者:一类是刚接触 ModelArts、想把本地训练好的代码搬上云跑一跑的新手;另一类是已经在用 Notebook 或训练作业,但对“训练产物怎么变成可调用的 API”“部署后老出错怎么排查”还不太清楚的伙伴。我会按实际操作的顺序来写,先准备数据,再讲训练作业怎么配,然后说模型怎么部署,最后列一排错误排查记录。所有内容都以我这次实操的 ResNet 分类任务为线索,你可以根据自己的模型替换对应部分。
1. 环境准备与数据工程
1.1 账号开通与资源规划
华为云账号是第一步,这没什么好说的,注册后实名认证就行。真正要花心思的是“区域”的选择。ModelArts 和 OBS 存储桶必须在同一个区域,否则训练作业读取数据时会出现跨区域访问的问题,网络延迟和计费都会变得很难看。我这次选的是“华东-上海一”,原因很简单——我的数据都放在这个区域的 OBS 桶里,而且 ModelArts 的公共资源和昇腾、GPU 规格在这个区域都比较全。
资源规划上,我建议你先评估两个量:数据总量和训练时长。数据总量影响 OBS 存储费用和训练时的读取效率,训练时长影响计算资源账单。ModelArts 是按时长计费的,一个p3.2xlarge.8(对应 V100 16G 约 8 卡?实际是单卡 16G)类型的资源,一小时大约十几块钱,几个月下来也是一笔不小的开销。所以我习惯先把训练作业里加上“自动停止”策略,设成半小时无运行就自动释放,防止深夜跑完忘了关,白白扣费。
另外,ModelArts 的工作空间最好单独建一个,别和日常资源全堆在同一个 default 工作空间里。工作空间可以用来隔离项目、管理权限和配额,如果是团队协作,这一步越早做越好。个人学习的话可以跳过,但如果后期要对接企业账号,建议从一开始就保留清晰的项目边界。
1.2 OBS 桶创建与数据上传的 3 个注意点
OBS(对象存储服务)是 ModelArts 训练和部署的“数据中转站”。训练作业本身是无状态的,计算节点跑完就销毁,所以数据必须放在 OBS 里,训练时让 ModelArts 去拉;训练产生的模型文件也要回写到 OBS,后续部署再从桶里读取。这就决定了 OBS 的目录结构从一开始就要设计好,不然后面找文件会像在乱抽屉里翻东西。
我的建议是桶内按这个结构组织:
dataset/:存放原始训练数据,可包含图片、标注文件、划分好的 train/val/test 子目录src/:训练脚本、依赖文件、配置文件output/:训练作业输出目录,里面会自动保存模型权重、日志和评估结果model/:手工整理后的部署模型目录,包含推理脚本、配置文件和权重
上传数据可以用网页直传,也可以用obsutil命令行工具。小文件、少量数据用网页没问题,但我这次数据量有几千张图片,还是用obsutil更方便。命令大概长这样:
# 先配置 obsutil 的 AK/SK 和 endpoint obsutil config -i=你的AK -k=你的SK -e=https://obs.cn-east-3.myhuaweicloud.com # 上传整个目录到指定桶路径 obsutil cp ./dataset obs://my-modelarts-bucket/dataset/ -r -f三个注意点这里记一下:第一,OBS 的桶名全局唯一,不要用容易被占用的名字;第二,上传大文件时可以开分段上传,并发线程数调成 10 左右,速度会快很多;第三,上传完成后一定要用obsutil ls检查一下目录层级是否和预期一致,不要想当然。很多人训练作业一启动就报“No such file or directory”,大多都是路径写错了,根本没有到脚本执行那一步。
1.3 数据格式与标注准备
训练前必须先确认数据格式是否符合模型输入要求。以我这次用的 ResNet50 图像分类为例,数据预处理要包括:统一缩放到 224x224、随机翻转增强、归一化等。这些一般都在训练脚本里用 PyTorch 的torchvision.transforms完成,线上数据只保证是普通 JPG/PNG 图片即可。
如果你的数据需要标注,ModelArts 也有标注功能,可以创建标注任务、多人协作标注。不过我个人建议,如果数据集比较小,用开源标注工具先处理好,导出成 COCO 或 VOC 格式,再传到 OBS。ModelArts 自带的标注界面适合团队协作和线上管理,但在个人实验阶段反而添乱。我这次的数据是之前就标注好的 CSV 格式(文件名 + 类别标签),直接上传后,在训练脚本里通过pandas读取标注信息,方便灵活。
2. 训练作业的设计与脚本改造
2.1 训练框架与入口规范
ModelArts 训练作业支持“预置框架”和“自定义镜像”两种模型。预置框架就是平台打包好的 PyTorch、TensorFlow、MindSpore 环境,选完版本就能直接用,省去装依赖的麻烦。我这次用的是 PyTorch 1.10 + Python 3.7 的预置框架,原因很简单:官方对预置框架的适配做得比较顺,日志采集和 GPU 驱动都不用自己去操心。
有一点必须搞清楚:ModelArts 训练作业启动时,会把你的代码“灌”到运行环境的/home/work/user-job-dir/目录下,然后执行你在“训练命令”里填的启动命令。所以训练脚本里所有相对路径都必须基于这个根目录,或者直接用绝对路径。我第一次没搞明白,写了个os.path.realpath(__file__)去定位脚本位置,结果因为系统路径的映射关系和本地不一致,导致读取数据时找不到路径。后面改成这样:
import os BASE_DIR = "/home/work/user-job-dir" SRC_DIR = os.path.join(BASE_DIR, "src") # 代码目录 DATA_DIR = os.path.join(BASE_DIR, "dataset") # 数据目录(需通过 mapping 映射) OUTPUT_DIR = os.path.join(BASE_DIR, "output")注意数据目录并不是直接把 OBS 路径塞进来,而是通过训练作业里“数据路径映射”功能,把 OBS 路径映射到本地路径。比如/obs/data/映射到$DATA_SRC环境变量或指定目录,这样脚本里就只看到本地路径,训练作业不需要关心数据到底放在哪个桶里。这也是最推荐的做法,安全性和灵活性都高。
2.2 超参数注入与数据集路径映射
训练作业的“超参数”可以让你在创建作业时直接传参,不需要改代码。我用了argparse来解析这些参数。常见做法是写一个config.py把所有默认值放进去,又在训练命令里通过python train.py --epochs 50 --batch-size 32 --lr 0.001覆盖默认值。这样每次调参、做实验都不用改脚本,只要修改训练作业配置,非常方便。
batch-size的选择要结合 GPU 显存和实际显存占用去考虑。我的 ResNet50 在 16G 显存下,batch-size=64很稳,再往上就会 OOM。如果显存不够,就降到 32,同时把数据加载线程数num_workers调成 8。这里有一个经验:初始学习率可以按“lr = base_lr * batch_size / 256”来折算,比如base_lr=0.01,你的 batch 是 64,那么实际 lr 设为 0.0025,收敛效果比拍脑袋设的 0.01 好很多。
数据路径映射还有一个重点:ModelArts 训练作业中会提供几个内置的环境变量,包括DATA_URL指向数据源路径,TRAIN_URL指向输出路径。但这两个变量在预置框架里的行为不一定和网上教程完全一致,所以我更推荐直接使用“数据路径映射”功能,而不是依赖这几个环境变量。映射可以在创建训练作业时配置,把 OBS 的dataset/路径挂载到容器里的/home/work/user-job-dir/dataset,并在训练命令里手动指定数据路径:
python train.py --epochs 80 --batch-size 64 --lr 0.0025 --data-dir /home/work/user-job-dir/dataset --output-dir /home/work/user-job-dir/output这种方式的好处是脚本内部逻辑一目了然,日志里的报错也能准确对齐到本地路径,排查起来轻松得多。
2.3 使用预训练模型微调
这次要做的是识别猫狗这类细分类别,全量训练一个 ResNet50 肯定没必要,所以在 ImageNet 上预训练的权重基础上做迁移学习,是最高效的路子。ModelArts 的预置算法中本来也有 ResNet 相关的算法,但我没有直接用它的现成包,而是在自己的训练脚本里通过torchvision加载预训练模型:
import torch import torch.nn as nn import torchvision.models as models def build_model(num_classes=5): model = models.resnet50(weights=models.ResNet50_Weights.IMAGENET1K_V2) in_features = model.fc.in_features model.fc = nn.Linear(in_features, num_classes) return model注意:如果平台版本较老,weights参数可能需要改成pretrained=True,否则会报参数错误。同时要提醒一点:加载预训练权重可能从外网下载,但 ModelArts 节点通常无法访问外部下载源,这可能会导致启动失败。我遇到的情况是在本地先下载好权重文件,传到 OBS,然后训练命令加上--pretrained=/home/work/user-job-dir/pretrained/resnet50_weights.pth,加载这个本地路径。
微调时可以把 backbone 的层设成不更新参数,只训练 FC 层,这样可以让正常显存降到很低,训练速度也更快。但这样对大数据集的精度可能有瓶颈。我的做法是前 10 个 epoch 冻结 backbone,只更新 FC 层,再解冻最后几层 block4 做联合微调,学习率再减小一个量级。这个策略在小数据集上很有用,不容易过拟合,而且收敛稳定。
训练脚本中还需要加入模型保存逻辑,比如每个 epoch 结束后根据验证集准确率保存最优模型,同时在训练结束时保存pytorch_model.pth和config.json等推理所需文件到输出目录。
2.4 日志查看与训练监控
ModelArts 的训练日志在控制台的“日志”页签里能实时看到。我这次遇到一个坑:模型训练的print打出来的日志有时候不刷新,一直卡在一个地方,后来发现是日志缓冲问题。解决方法是把训练脚本里的print改成了flush=True,并且尽量使用logging模块并指定stream=sys.stdout,否则输出可能被系统缓存掉,导致控制台界面迟迟不显示。
另一个比较隐蔽的问题是训练作业“运行中”但不进步,loss 掉不下去,准确率一直停在某个水平。这种情况要到日志里看是否早期就出现了 loss=0 或者模型没有正确进入训练模式。我这次有一次把model.eval()和model.train()的顺序写反了,导致所有 epoch 都在验证模式下跑,BN 层不更新,结果训练了大半个小时后准确率纹丝不动。排查以后发现日志里的 “model: train” 根本没打出来,才意识到是脚本里在训练循环外面多调了一次eval()。
为了更直观地监控训练过程,我习惯在脚本里定期往输出目录写一个metrics.json,记录每个 epoch 的 loss 和 acc,这样训练结束后可以直接用工具绘图,也可以在训练期间用最小代价查看趋势。ModelArts 本身也有可视化功能,但个人用还是自己搞个 JSON 简单直接。
3. 模型转换、评估与在线部署
3.1 训练产物整理与 AI 应用创建
训练作业结束以后,OBS 的输出目录里通常会有model.pt、checkpoint.pth、日志文件等。但部署在线服务时,ModelArts 要求一个结构化的“模型包”,所以需要把这些产物整理成一个标准格式。我的目录结构如下:
model/ ├── config.json # 模型配置,包含 framework、model_processor 等 ├── resnet50.pth # 训练好的权重 ├── classes.txt # 类别标签 ├── custom_service.py # 模型推理脚本 └── datasource/ # 可选依赖包或预置代码整理好模型包后,把它上传到 OBS 的model/目录。然后在 ModelArts 控制台进入“AI 应用”管理页面,选择“从 OBS 导入模型”,填写模型名称、版本,选择模型框架和推理脚本类型。这里有一个关键点:模型框架选择要和训练一致,我训练用 PyTorch,这里就选 PyTorch,否则后面推理配置会出一堆莫名其妙的“算子执行失败”。
AI 应用创建完成后,系统会为这个应用分配一个模型 ID,后续部署服务时直接引用。如果待遇不明确,可以先在 ModelArts 的“模型评估”里跑一下测试集,使用推理脚本单测一下,确认预测结果基本正确后,再进行在线部署。这一步能省去在线服务部署后发现问题又来回调试的大量时间。
3.2 推理脚本与服务化配置
ModelArts 在线服务需要在模型包中提供自定义推理逻辑。在 PyTorch 框架下,自定义推理脚本一般要继承内置的BaseModelService类并实现_preprocess、_inference、_postprocess三个方法。我这次写了一个custom_service.py,核心逻辑是:
from model_service.model_service import BaseModelService from PIL import Image import torch import torchvision.transforms as transforms import numpy as np class MyService(BaseModelService): def _preprocess(self, data): # data 是请求体中的字节流,需要转成 PIL Image image = Image.open(data["image"]).convert("RGB") transform = transforms.Compose([ transforms.Resize((224, 224)), transforms.ToTensor(), transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]) ]) return transform(image).unsqueeze(0) def _inference(self, tensor): # 加载模型、执行推理 self.model.eval() with torch.no_grad(): outputs = self.model(tensor) return torch.softmax(outputs, dim=1).cpu().numpy() def _postprocess(self, result): # 将概率映射到标签 classes = ["cat", "dog", "bird", "flower", "other"] score = float(result[0][0]) return {"predicted": classes[np.argmax(result[0])], "confidence": score}注意BaseModelService在初始化时有没有自动加载模型,要根据推理脚本类型做选择。对于 PyTorch,如果模型包中只有一个 arhitecture 和权重,你需要自己在__init__里完成网络构建和权重加载,再把self.model赋值好。不过如果平台支持加载pytorch_model.pth并自动初始化,这里就不需要重复建模型了。稳妥起见,我是在_inference前先判断self.model是否为空,为空则执行加载逻辑。
配置文件config.json也别忘了写,否则系统不知道你用哪个模型处理。我的config.json大致长这样:
{ "model_algorithm": "image_classification", "model_type": "PyTorch", "execution_command": "custom_service.py", "infer_type": "TensorFlow"? }实际上,不同版本要求不一样,最好的做法是先用“AI 应用”导入后,看系统自动生成的配置模板,再手动补齐。不要凭记忆写infer_type,容易报no module named 'tensorflow'。
3.3 部署在线服务与调用测试
AI 应用创建好以后,就可以部署在线服务了。在 ModelArts “部署上线 -> 在线服务”页面选择“部署”,指定 AI 应用和资源池规模。这里要注意:如果模型很小,可以选择 CPU 规格;如果模型较大,或对推理延迟有要求,则选 GPU 规格。GPU 规格按秒计费,比 CPU 贵很多,但如果暂时只测试,我建议先用 CPU 跑一遍,确认推理脚本和模型兼容性没问题,再切到 GPU 做正式服务。
部署过程需要几分钟,期间会显示“部署中”。此时不要反复点击“部署”按钮,容易创建多个实例,产生费用。等状态变为“运行中”后,就可以通过页面上的“预测”按钮上传图片测试。我这次上传了一张猫的图片,返回结果自信地分到了 cat 类,心里一块石头落了地。
如果要通过代码调用线上 API,ModelArts 支持使用 AK/SK 签名认证,也可以使用公开 API Key。最简单的例子是用第三方库requests,照下面的方式构造鉴权头:
import requests import time import hashlib import hmac import base64 # 这里简写签名逻辑,实际推荐使用 SDK url = "https://modelarts.cn-east-3.myhuaweicloud.com/v1/infers/xxxx" headers = { "Content-Type": "application/json", "Authorization": "Bearer your-token" } resp = requests.post(url, json={"image": base64.b64encode(open("test.jpg","rb").read()).decode()}, headers=headers) print(resp.json())在实际项目中,建议使用官方 SDK,它已经封装好了 AK/SK 签名,不需要自己手动算 HS256。个人测试时用网页“预测”按钮是最快的方式,别一上来就搞 API 调用,没必要的复杂性。
3.4 轻量模型边缘部署尝试
在线服务适合需要一个稳定 API 的场景,但若模型要跑在手机、摄像头或边缘盒子上,就需要 ModelArts 的“模型转换与边缘部署”能力。对 PyTorch 模型,可以转换到 ONNX 或 MindSpore Lite 格式,再部署到昇腾或 HiLens 等设备上。
我这次没有做完整的边缘部署,只做了到 ONNX 的转换验证。转换时注意:ONNX 导出需要固定输入尺寸,动态 batch 支持不充分,所以导出要指定batch_size=1。另外,某些算子如F.interpolate在导出的低版本 ONNX 中可能不兼容,需要对模型做简化或替换。转换完成后,可以用onnxruntime在本地加载这个 ONNX 文件做一个推理验证,确认输出结果和 PyTorch 模型一致再上线。
边缘部署的好处是离线推理、响应快,但调试难度也高——设备环境、运行库、算子支持都可能出问题。如果是新手,我建议先走在线服务,把部署全流程跑通后再考虑边缘侧,别一上来就奔着 PAP 边缘方案去,容易劝退。
4. 常见问题与性能调优实录
4.1 训练 loss 变成 NaN 怎么办
这次训练到第 12 个 epoch 的时候,loss 突然变成nan,所有参数也崩了。排查原因时先看日志里的警告,再确认学习率是不是太大。我的初始学习率用 0.0025,理论上不算激进,但因为我用了冻结 backbone 再解冻的策略,解冻后网络层权重变化较大,同时学习率没有跟着降,所以发散。我加了学习率衰减:
scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=20)每次 epoch 结束后执行scheduler.step(),保证后期学习率缓慢下降。另外一个常见原因是图像数据里出现了异常值(比如纯黑图片或损坏图片),导致 loss 计算出现除零或 log(0)。我在数据加载时加了一个过滤:如果 PIL 打开失败,直接跳过这张图。效果立竿见影,再没出现 nan。
4.2 数据读取慢导致训练空转
最让我恼火的问题不是训练算不动,而是 GPU 利用率只有 20%,明明模型很小,epoch 却跑得特别慢。查了日志发现是数据读取瓶颈。原因是我在创建训练作业时用了“直接 copy 数据到本地”模式,每次跑训练都要把整个 OBS 数据集都复制到计算节点,几 GB 数据光拷贝就要好几分钟,epoch 也没跑多快。
后来我改成了 ModelArts 的“.obs_file_mount”或“数据并行挂载”模式,让训练作业直接以对象存储方式读取 OBS 里的数据,省去了 copy 这一步。然后在训练脚本里调高num_workers为 8,开启pin_memory=True,GPU 利用率才跑到 85% 以上。如果数据量特别大,还可以考虑先用obsutil在 OBS 上做数据分片,训练时读取多个小文件,避免一个超大数据文件反复被拉取。
4.3 显存溢出与小技巧
用 V100 16G 训练 ResNet50 的 batch=64 是够用的,但我第一次跑的时候还是 OOM 了。原因是我在脚本里保存了很多中间变量,例如把每个 epoch 的预测结果保留到一个 list 里,epoch 结束后再计算指标,导致显存占得非常大。换成长度固定的输出缓冲区,或者边算边丢弃,OOM 就消失了。
另外,PyTorch 默认会缓存显存,测试阶段如果报 “CUDA out of memory”,可以先试试加上:
torch.cuda.empty_cache()但这只是释放缓存。真正 OOM 时要先看是训练还是推理阶段,再分别调 batch-size 或图像分辨率。我的策略是:batch-size 直接减半,如果还不行,再把图片从 224 缩到 192,一般都能解决。
4.4 部署后预测结果不对的排查
有一次在线服务部署成功了,但测试图片总是返回同一个类别,而且概率接近 1。排查下来发现是推理脚本中_preprocess的数据类型搞错了,输入从请求里拿到的 base64 字符串没有正确解码成 PIL Image,结果模型看到的是一堆随机噪声。后来我用加try-except的方式输出预处理后的图像尺寸,确认[1, 3, 224, 224]正常,再计算推理输出。
另外一个坑是transforms.Normalize的参数一定要和训练时完全一致。我训练时用了均值[0.485, 0.456, 0.406],标准差[0.229, 0.224, 0.225],但部署脚本里不小心写成了[0.5, 0.5, 0.5],结果测试集正确率直接从 92% 跌到 60% 以下。这个问题极其隐蔽,因为推理本身不会报错,只有比对大量结果时才发现。所以一定把数据预处理逻辑单独抽成一个函数,在训练和推理脚本里共用同一个实现。
4.5 成本控制与自动停止
最后唠一点花钱的经验。ModelArts 训练作业、Notebook、在线服务都是按资源类型和时长计费的,最怕的就是作业跑完忘了释放。创建训练作业时,高级选项里有“自动停止”,我一般会设置 10 分钟的缓冲,万一作业结束前没有手动关闭,自动停止也能兜底。Notebook 也是一样,建议默认开启“空闲自动停止”,设成 15 ~ 30 分钟。
在线服务的成本比训练低一些,但由于 7x24 小时常驻,积累起来也很可观。如果只是做一次性验证,测完就把服务删掉;如果是测试阶段的 API,可以把它部署为“共享资源池”或“自动停止”模式,避免浪费。我在实际测试完以后,直接把在线服务删了,第二天需要再重新部署,反正模型包还在 OBS,重新拉起也就几分钟,完全没必要一直开着。
提示:ModelArts 控制台有时会出现费用看板延迟,建议自己设一个消费提醒,超过预算就邮件通知。否则月底账单能让你瞬间清醒。
最后再分享一个我在实际使用中的体会:ModelArts 的优势不在于“免代码”,而在于把云资源和训练环境的管理成本收敛到可控范围。真正决定项目质量的,还是训练脚本本身的严谨程度,比如数据路径是否只用相对路径、超参是否写死、脚本有没有错误分支。如果你只是把本地脚本简单“搬运”上来,一定会遇到各种奇怪的云环境兼容问题。我会先把训练脚本从本地迁移到 Notbook 里跑通一个小数据集,确认没问题后再开正式训练作业。这个习惯帮我避开了至少三次因依赖版本或路径映射导致的失败。另外,训练作业的输出文件建议定期下载到本地备份,别把 OBS 当唯一存档,万一桶被误删,至少还有本地救命数据。希望这份实操笔记能帮你少走几步弯路,后面再做类似训练部署时,能顺畅不少。