一次请求过五道手:收图、前处理、推理、后处理、返回。调试按这五步找。 存整个模型还是只存权重:后者小五倍,但加载时要先有结构。 这一课不装 torch。模型是黑盒,能把控的是前后处理和接口形状。
这一篇对应教材 13.1 到 13.2.2:物件侦测 API 的处理流程、安装 PyTorch、储存已学习模型。 第 12 章讲清了 HTTP 与四个方法,这一篇定规格:这个 API 到底长什么样。
一、五道手
| 步骤 | 输入 | 输出 |
|---|---|---|
| 1 收图 | HTTP 请求里的文件 | 临时文件路径 |
| 2 前处理 | 任意尺寸的图片 | 固定尺寸的张量 |
| 3 推理 | 张量 | 原始的框与类别 |
| 4 后处理 | 原始输出 | 筛选、还原坐标、转 dict |
| 5 返回 | dict 列表 | JSON 响应 |
这五步是第 3 篇的骨架。第 14 章那三次实作就是围着它转的。 分清它的用处很具体:结果不对的时候,先看 2 和 4,再看 3。 因为模型本身你改不了——它是别人训好的黑盒,能把控的只有喂进去什么、和出来以后怎么解读。 这也是为什么 YOLO 装不上不影响这一课:第 3 步换个替身,2 和 4 的原理一个字都不变。
二、为什么前处理必须有
模型只吃固定尺寸的输入。实测那张测试图是 640x480,但用户上传的可能是 3000x4000 的手机照片。 所以要缩放到模型要的尺寸。缩的时候要按比例,不能硬拉——硬拉会把猫拉成一条,模型照样认不出。 缩完还得记住缩放比。因为模型给的框是缩放后坐标系里的,要乘回去才是原图的坐标。 这个「乘回去」就是后处理的一半工作。
另一半是筛选:模型会给一堆框,置信度低的、重叠的(NMS 非极大值抑制)都要去掉。 教材这一段在 14.4.3,这一篇先知道有这么回事。
三、PyTorch:装不上,也不装
pip install torch torchvision
import torch
model = torch.hub.load("ultralytics/yolov5", "yolov5s")
实测 import torch 报 ModuleNotFoundError,pip show torch 输出为空。
这一课不打算装它,理由说清楚:torch 的 CPU 轮子两百多兆,YOLOv5 的权重还得从 GitHub 拉,
而且这一章要讲的不是模型本身——YOLO 是黑盒,你调不动它。
所以后面用 OpenCV 自带的检测器当替身:接口一样、能真跑、整条链路跑通。
换回 YOLO 的时候,只有 detect() 里面那几行要改。
这不是偷懒,是分工:真实项目里也是这样,做 Web 的人和训模型的人往往是两拨, 中间靠一个接口形状对接。
四、模型文件里存了什么
| 存法 | 实测大小 | 代价 |
|---|---|---|
| 整个对象 | 1058.5 KB | 换版本、改类名就打不开 |
| 只存权重 | 210.9 KB | 加载时要先有结构 |
实测一个 30 棵树的随机森林:整个对象 1058.5 KB,只存权重数组 210.9 KB。
差的那五分之四是类的定义:类的名字、它在哪个模块、它那一堆方法。
存整个对象时这些也一起进文件了。
torch.save(model.state_dict(), path) 对应的就是「只存权重」。
官方推荐它,因为换个版本、改个类名,整个对象那个文件就打不开了。
代价是加载时要先有结构:m = MyModel(); m.load_state_dict(torch.load(p))。
结构和权重分开存,两者都得对得上——对不上就报错,不会静默出错,这一点还好。
五、加载一次要多久
| 做法 | 实测最快 |
|---|---|
| 加载整个对象 | 2.05 ms |
| 加载权重 | 0.27 ms |
这个模型一千多 KB,已经能说明问题了,但它仍然是个玩具。
换成 YOLO 就不是这个量级:权重十四兆起,加载要几百毫秒到一两秒。
那个秒数这里测不出来——沙箱没有 torch,也不下权重。测不到的数不写。
但结论一样:加载必须放模块级只做一次。第 8 章那个 get_detector() 干的就是这个,
35.6 毫秒 vs 0.002 毫秒,一万倍。
一万倍这个数字值得记一下:如果每请求加载一次,一个请求光加载就要 35 毫秒, 一百个请求就是 3.5 秒纯等待。这是最容易改、收益最大的一处优化。
六、一次推理的时间花在哪
| 阶段 | 实测 | 占比 |
|---|---|---|
| 读图(imdecode) | 2.35 ms | 12.1% |
| 转灰度 + 阈值 | 2.90 ms | 14.9% |
| 找轮廓 + 取外接矩形 | 2.77 ms | 14.3% |
| 画框 + 存盘 | 11.41 ms | 58.7% |
| 合计 | 19.43 ms |
测试图 640x480,三个亮块,全部框出来了。 注意「读图」那一项:同一张图被读了四遍(每个计时的函数各读一次),所以这个百分比偏高。 真实代码里只读一次,把数组往下传。 这里为了单独计时才各读一遍——这就是「测出来的数要连测法一起说」的原因。脱离测法报一个数, 很容易被人拿去当另一个场景的结论。
真接上 YOLO 之后,「找轮廓」那一步会变成几百毫秒,把前后处理全压成个位数。 那时候读图这点开销完全可以忽略,现在它占了八分之一是因为分母太小。
七、规格表:先定,再写
| 项 | 定什么 | 本课的值 |
|---|---|---|
| 端点 | 路径与方法 | POST /detect |
| 入参 | 怎么传图 | multipart/form-data,字段名 file |
| 成功 | 状态码与结构 | 200 + count 与 tags |
| 没有文件 | 缺参 | 400 + error |
| 不是图片 | 类型不对 | 415 + error |
| 太大 | 超上限 | 413 |
| 标签 | 每条几项 | label、score、box 四项 |
这张表就是第 14 章要照着写的东西。先定表再写代码,顺序反了会写成「代码能跑,
但别人不知道怎么调」。
注意第四到第六行:出错也要返回 JSON。第 10 章那个 HTML 错误页是给浏览器看的;
API 的调用方解析不了 HTML,它会报「JSON 解析失败」,把真正的原因(比如文件太大)盖掉。
第 14 章要为这三个状态码各写一段 jsonify,还得注册「API 请求走 JSON、页面请求走 HTML」的分流。
box 用四项坐标 [x, y, w, h] 而不是两个角点,跟第 8 章存库那个格式一致。
能统一就统一,省得每次转换都出错。
八、动手 Demo:规格、模型与一次推理的时间都花在哪
这个 Demo 先把五道手摆出来,再照实检查 torch 装没装,然后把「模型文件存什么」量一遍。 第五节那张耗时表要连着测法一起看:同一个图读了四遍,所以前三项偏高。
p_state = os.path.join(d, "state.pkl")
with io.open(p_state, "wb") as f:
pickle.dump([est.tree_.value for est in m.estimators_], f)
# 对应的 torch 写法:
# torch.save(model.state_dict(), "model.pth")
# m = MyModel()
# m.load_state_dict(torch.load("model.pth"))
实跑输出:
--------------------------------------------------------------------
规格、模型与一次推理的时间都花在哪
--------------------------------------------------------------------
① 一次请求要过五道手
--------------------------------------------------------------------
| 步骤 | 输入 | 输出 |
|---|---|---|
| 1 收图 | HTTP 请求里的文件 | 临时文件路径 |
| 2 前处理 | 任意尺寸的图片 | 固定尺寸的张量 |
| 3 推理 | 张量 | 原始的框与类别 |
| 4 后处理 | 原始输出 | 筛选、还原坐标、转 dict |
| 5 返回 | dict 列表 | JSON 响应 |
! 这五步是这一篇的骨架。
第 14 章的三次实作分别对应:启动代码(0 步)、前处理与后处理(2、4 步)、模型执行与路由(1、3、
5 步)。
! 把这五步分清以后,调试就有方向了:结果不对,先看 2 和 4,再看 3。
因为模型本身你改不了,能把控的只有前后那两步。
② PyTorch 在这个沙箱里装不上
--------------------------------------------------------------------
[平台原样 · 本沙箱不执行]
| pip install torch torchvision
|
| import torch
| model = torch.hub.load("ultralytics/yolov5", "yolov5s")
教材 13.2.1 的安装与调用。torch 的 CPU 轮子两百多兆,权重还得从 GitHub 拉。
import torch -> ModuleNotFoundError
pip show torch 的输出 -> (空,说明没装)
! 没装,这一课也不打算装。理由是这一章要讲的不是模型:YOLO 是黑盒,你调不动它;
能把控的是前后处理和接口形状。
! 所以后面用 OpenCV 自带的检测器当替身:接口一样、能真跑、让整条链路跑通。
换回 YOLO 的时候,只有 detect() 里面那几行要改。
③ 模型文件里到底存了什么
--------------------------------------------------------------------
训练了一个小模型:2000 个样本、12 个特征、30 棵树、准确率 0.998
存整个对象 -> 1058.5 KB
只存权重 -> 210.9 KB
! 差了五倍。那五分之四是类的定义:RandomForestClassifier 这个名字、它在哪个模块、它那一堆方法。
存整个对象时这些也一起进文件了。
! torch.save(model.state_dict(), ...) 对应的就是「只存权重」那个做法。
官方推荐它,因为换个版本、改个类名,整个对象那个文件就打不开了。
! 代价是加载时要先有结构:m = MyModel(); m.load_state_dict(torch.load(p))。
结构和权重分开存,两者都得对得上。
④ 加载一次要多久
--------------------------------------------------------------------
加载整个对象 -> 最快 2.05 ms
加载权重 -> 最快 0.27 ms
! 这个模型一千多 KB,已经比刚才那个几十 KB 的能说明问题了,但它仍然是个玩具。
换成 YOLO 就不是这个量级:权重十四兆起,加载要几百毫秒到一两秒。
! 那个秒数这里测不出来——沙箱没有 torch,也不下权重。测不到的数不写。
! 但结论一样:加载必须放模块级只做一次。
第 8 章那个 get_detector() 就是干这个的,35.6 毫秒 vs 0.002 毫秒,一万倍。
⑤ 一次推理的时间构成
--------------------------------------------------------------------
测试图 -> 640x480,1 KB
读图(imdecode) -> 2.35 ms(12.1%)
转灰度 + 阈值 -> 2.90 ms(14.9%)
找轮廓 + 取外接矩形 -> 2.77 ms(14.3%)
画框 + 存盘 -> 11.41 ms(58.7%)
合计 -> 19.43 ms
! 这张图 640x480,三个亮块,全部框出来了。
注意「读图」这一项:同一张图要读四遍(每个函数都读一次),所以这个百分比是偏高的。
! 真实代码里只读一次,把数组往下传。
这里为了单独计时才各读一遍——这就是「测出来的数要连测法一起说」的原因。
! 真接 YOLO 之后,第 3 行会变成几百毫秒,把前后处理压成个位数。
那时候「读图」这点开销完全可以忽略。
⑥ 规格:这个 API 长什么样
--------------------------------------------------------------------
| 项 | 定什么 | 本课的值 |
|---|---|---|
| 端点 | 路径与方法 | `POST /detect` |
| 入参 | 怎么传图 | `multipart/form-data`,字段名 `file` |
| 成功返回 | 状态码与结构 | 200 + `{"count": n, "tags": [...]}` |
| 没有文件 | 缺参 | 400 + `{"error": ...}` |
| 不是图片 | 类型不对 | 415 + `{"error": ...}` |
| 太大 | 超上限 | 413 |
| 标签结构 | 每条标签几项 | `label`、`score`、`box` 四项坐标 |
! 这张表就是第 14 章要照着写的东西。
先定表,再写代码,顺序反了会写成「代码能跑,但别人不知道怎么调」。
! 注意第四到第六行:出错也要返回 JSON。
第 10 章那个 HTML 错误页是给浏览器看的,API 的调用方解析不了 HTML——它会报「JSON 解析失败」,
把真正的原因(比如文件太大)盖掉。
! box 用四项坐标 [x, y, w, h] 而不是两个角点,跟第 8 章存库那个格式一致。
能统一就统一,省得每次转换都出错。
第二节那两行空输出不是出错:一个是 ModuleNotFoundError,一个是 pip show 没东西。 第三节 1058.5 与 210.9,差的那部分是类的定义。 第五节 19.43 ms 里画框占了一多半——它是 IO,不是计算。
30 秒实验
- 把
pickle.dump换成joblib.dump,比一下文件大小和加载时间。 - 把测试图改成 1920x1080,再跑第五节,看四项耗时各涨多少。
- 想一想:为什么「出错也要返回 JSON」这件事这么重要?
常见误区
- 以为模型文件只有权重:存整个对象时,类的定义也在里面。
- 每请求加载一次模型:那一项开销比推理本身还大。
- 硬拉图片到模型尺寸:比例变了,模型认不出。
- API 出错时返回 HTML 错误页:调用方解析不了,真正的原因被盖掉。
自测三问(点开对答案)
1. 一次检测请求过哪五道手? —— 收图、前处理、推理、后处理、返回。 2. 存整个模型和只存权重各有什么代价? —— 整存换版本可能打不开;只存权重要先有结构才能加载。 3. 为什么 API 出错时不能返回 HTML? —— 调用方按 JSON 解析,HTML 会让它报解析错误,盖掉真正的原因。规格定完了,下一篇开始写代码:目录结构、启动模块、配置。
- ☐ (本篇完,下一篇 14.1 目录架构与启动代码)