跳到主要内容

数据截至 (上游 commit c187ef3271d5)

让模型走出脚本 — 全书的落点,和它自己撞上的那堵墙

这一章讲三件事。第一件,是一句听起来很扫兴、但完全成立的话: 你练得再好,模型留在脚本里等于零。 它现在只是硬盘上的一个文件 —— 别人的程序调不到它、你的用户点不着它。

第二件,是把它交出去的三步: 导成一份换个框架也能跑的通用格式 → 包成一个能被网络请求叫醒的服务 → 推上公网。 这三步是这本书区别于同类书的地方 —— 别的书讲到「练出来了」就收尾。

第三件,是这本书自己撞上的那堵墙,也是全书的落点。 第 04 章埋下的那个类别名错位,被原样抄进了线上服务。 拿一张松饼图去问它,它回答「吉娃娃 0.697」—— 书一字未评。 这一章把那个 0.697 的真实含义算给你看,结论比「它答错了」更别扭: 它其实答对了,只是「哪个数对应哪个类」那张对照表反了,于是被印成了错的。

在全书链条里的位置:这是最后一环,也是闭环合上的地方。 交给框架(第 13 章)→ 装上仪表(第 14 章)→ 送出去(这一章)。 而它想教的那句话,被它自己坐实了:能上线,不等于是对的。

1. 顶层全景:一个模型文件,走到公网上的一个地址

这一章拿的是第 04 章那个分松饼和吉娃娃的模型,一路送到公网1

第 04 章训出来的权重文件

├─① 导成通用格式 ────────→ 换个运行环境也能跑,不必装 PyTorch

├─② 包成一个服务 ────────→ 开两个地址:一个报活,一个收图片地址、返回判断

├─③ 本地起来、试一试 ────→ 本机 8000 端口;拿一张吉娃娃图问,答「吉娃娃 86%」
│ ← 看起来一切正常

├─④ 推上第一家云 ────────→ 五个文件,git push 就完事
│ (这一家叫 Heroku)

└─⑤ 推上第二家云 ────────→ 一条发布命令
(这一家叫 Microsoft Azure)
← 拿一张松饼图问,答「吉娃娃 0.697」

图说:这一章真正的落点在第 ③ 步和第 ⑤ 步之间的对照。 同一套代码、同一个模型,一次看起来对、一次露馅 —— 第 8 节把这两次连起来算一遍。

2. 训完之后,模型只是硬盘上的一个文件

先看现象。 第 13 章那三个检查点文件、第 14 章那个注册表里的第 2 版 —— 它们都在你的机器上躺着。

而这意味着:你的同事没法用它,你的用户更点不着它。

**书对「部署」这件事的定义,值得原样记住:把开发出来的东西 (比如一个新的深度学习模型)接进真实的业务流程,并且让别人能用2

书还借页首那句行里的老话点了它的性质 ——「周五不上线」。 它自己的解读是:这句话说的是部署意味着责任,同事和客户在依赖你2。 (这句话的字面理由也很实在:出了事没人愿意用整个周末去查。)

3. 用户点的东西,碰不到模型

先看现象: 你打开一个网站,输入点什么,得到一个结果。 中间到底隔着几层?

书画了一条链,而且强调了链上一个不许被绕过的规矩3:

用户 ──→ 浏览器 ──→ 前端服务器 ──→ 后端服务器 ──→ 数据库
└────────→ 第三方服务

规矩:后端服务器不直接和最终用户说话,一切都要经过前端服务器这个中间人。

图说:前端管画面,后端管算。 书举的例子是一个聊天机器人: 用户输入一段文字 → 前端把它转给后端 → 后端可能再去问一个第三方的语言模型, 或者去数据库里取点东西 → 结果原路返回3

后端上跑的那些各管一摊的服务,书给了个名字叫微服务3我们训出来的模型,就住在后端。

3.1 接口和端点

这两个词是这一章的入口,必须当场讲清。

一个程序要被另一个程序调用,得先伸出几个可以被叫到的地址 —— 这套「能被别的程序叫到」的东西,统称接口(第 10 章用它调过别人的语言模型, 那时候我们管它叫 API,是同一件事)。

而其中每一个具体的地址,叫一个端点。 书给的定义是: 客户端 —— 也就是发起请求的那一方,你的浏览器、或者另一个程序 —— 为了和这个接口打交道而把请求发过去的那个具体网址,就是端点; 它同时也是使用这台服务器的接入点4

书拿自己的网站举了一个极好的例子,一句话就说明白了5:

打开 gollnickdata.de → 走的是默认的那个端点,写作 "/"
点开网站上某一本书的介绍页 → 地址栏变了,变成了另一个端点

图说:你天天在浏览器地址栏里看到的那些路径,就是端点。

这一章的服务只开两个端点6:

端点干什么回什么
/报活 —— 让你确认这台服务还在一句「状态正常」,外加一句用法提示
/predict收一个图片地址,返回判断类别名,以及两个类各自的把握

4. 这一层用什么协议说话:三种选法

先看现象: 上一节那条链上,每一段都要「说话」。用什么语言说?

书列了三种最常见的,而且各自的取舍很清楚7:

选法怎么说话什么时候选它
REST拿网址加动词说话(取 / 建 / 改 / 删),数据用 JSON 那种格式最常用,几乎所有服务之间的通信都基于它;和浏览器这类客户端打交道最省事
gRPC走二进制,不是文本要快、要低延迟时 —— 传的数据更少,所以更快更省
GraphQL没有固定的端点,调用方自己声明「我要哪几个字段」数据结构复杂时 —— 一次请求就能从好几处取数,换成 REST 要发好几次

第一种是这一章用的。 书说它是这些协议里的头一号、 是互联网最重要的支柱之一7。它建立在网页那套请求方式上, 动作由「取 / 提交 / 更新 / 删除」这几个动词定义7

第三种那个「没有固定端点」值得多说一句,因为它正好和上一节反着来: REST 是服务器规定好每个端点返回什么,GraphQL 是调用方自己点菜。 (这一句对照是我们补的,书分开说的两句。)

5. 上线不是手动传文件

先看现象: 上面讲的是「架构长什么样」。可代码是怎么从你手上跑到线上去的?

书把部署放进一个更大的流程里讲,这个流程叫 CI/CD8它是一串自动化的步骤,目标是让软件的改动又快又可靠地从开发者手上送到最终用户手上8

拆开看是两半:

前一半那两个字母,展开叫持续集成 —— 管的是「代码进库」这件事。 书说得很具体:把多个开发者写的代码,自动地并进一个大家共用的地方; 每次有人上传代码,就自动触发一套流程8

那个「大家共用的地方」有个名字叫版本库 —— 一个记录代码每一次改动、能回退到任意一次的仓库;这一章用的是最常见的那种(Git)8

这套流程可以包括两类动作8:

动作干什么
自动测试单元测试、集成测试
自动构建把代码打成一个能跑的东西

目的书说得极准:尽早发现问题,让流程停下来,别把有毛病的代码部署出去8

后一半(CD)其实是两个意思,差别只有一处:放行是不是自动的9:

说法改动怎么上线
持续交付先准备好,等人手动点一下才放行
持续部署直接上线,没有人工那一步

这就是「周五不上线」那句老话的落点: 全自动上线快, 但出了问题的时候,没人拦得住那一步。(这一句连接是我们做的,书两处分开说的。)

6. 换成通用格式:让别人不装 PyTorch 也能跑

它解决什么问题。 你存下来的那份权重,得用 PyTorch 才读得动。 可线上的运行环境未必装了 PyTorch —— 书说得很实在: 很多生产环境用的是经过优化的运行时,可能和训练用的框架并不兼容10

做法:导出成一份跨框架的开放格式,名字叫 ONNX。 书给的定义是:一种开放格式,用来改善不同深度学习框架之间的互通 —— 你可以用 PyTorch 训,再把它导到别的运行环境里去跑10

导出时有两处必须交代11:

要给什么这一章给的为什么要给
一个样例输入一个形状是 (1, 1, 32, 32) 的随机张量导出这一步要拿它把整张计算图走一遍,才知道图长什么样
格式的版本号12这个格式在演进,对方的运行时支持到哪一版,得对得上

那个 (1, 1, 32, 32) 正是第 04 章那个模型的输入形状 —— 一张图、一个通道(灰度)、32 × 32 像素。 这个形状此后被写死在了模型文件里:线上送进去的图,必须先被处理成这个形状。 (这一句是我们补的,它是下一节那个陷阱的根子。)

7. 包成服务:两个端点,和一条不能差半步的规矩

它解决什么问题: 有了模型文件,还得有个东西一直开着、等别人来问

这一章用的是三样小东西12:一个写接口的框架、一个跑它的服务器、 以及一个专门跑那份通用格式模型的运行时(注意:这里已经没有 PyTorch 了)。

这三样都有名字,而且你出门一定会撞见,所以现在就点出来12: 写接口的那个框架叫 FastAPI,跑它的那个服务器叫 uvicorn (它负责真正听端口、把请求交给你写的函数), 跑通用格式模型的那个运行时叫 onnxruntime —— 上一节导出的文件正是交给它。

第一个端点简单到只有三行:声明一下这个地址收「取」这个动作, 写一个函数,返回一句写死的话13书特意说:那个函数叫什么名字无所谓13 —— 起作用的是它上面那行声明。

第二个端点是正经活,四步14:

① 把图下载回来
── 图片常常只允许浏览器下载,所以要装成浏览器的样子去要
(代码里带了一整套浏览器的自我介绍)

② 预处理 ← 这一步是全章最容易翻车的地方,见下面

③ 送进模型跑一遍

④ 把结果包成 JSON 返回:类别名 + 两个类各自的把握

7.1 预处理必须和训练时一模一样,差一步就废

这一条是这一节真正的知识点,而且它是从第 04 章一路延续下来的规矩。

线上这四步预处理,每一步都对应第 04 章训练时做过的同一步14:

线上这一步对应训练时的哪一步
转成灰度第 04 章那个模型吃的就是单通道灰度图
缩到 32 × 32同上,模型的输入形状写死了
每个像素除以 255把 0–255 的亮度压到 0–1
再做 (x − 0.5) ÷ 0.5把 0–1 挪到 −1 到 +1(第 07 章解释过这一步为什么要做)
补两次维度(32, 32) 变成 (1, 1, 32, 32) —— 补出「一批」和「一个通道」

为什么差一步就废: 模型学到的规律是建立在「输入长这样」这个前提上的。 你训练时把像素挪到了 −1 到 +1,线上却只除了 255(0 到 1), 那么同一张图在模型眼里完全是另一张图。 (这一段因果是我们补的:书列了这四步代码,但没有解释为什么必须一致。)

7.2 这里书悄悄修好了第 03 章那条规矩

值得单记一笔,而且这是一条独立的线,和后面那个错误没有因果关系。

第 03 章立过一条规矩:用带 logits 的那把尺训出来的模型, 输出的是原始分数,不是概率;要卡阈值,得先压一压。 而第 04 章违反了它 —— 直接拿 0.5 去卡原始分数。

这一章的代码修好了:它先把模型吐出来的那个数过一遍 sigmoid 压成 0 到 1,再卡 0.515

但书没有说自己前面错过。 两处摆在一起看才知道这里改了口径 —— 读者对着第 04 章敲代码,不会知道那一步是错的。

8. 主走查:同一套代码,一次看起来对、一次露馅

这条走查带一次反转,请一步一步读。

第 ① 步,导出。 第 04 章那个模型 → 通用格式,输入形状 (1, 1, 32, 32)、版本 1211

第 ② 步,起服务。 本机 8000 端口,监听所有网络接口16

第 ③ 步,先确认它活着。 浏览器打开本机 8000 端口的根地址, 返回「状态正常」和一句用法提示 —— 服务通了17

第 ④ 步,拿一张吉娃娃的照片去问它。 用的是维基百科上一张吉娃娃图。返回:类别「吉娃娃」,把握 86%18

书到这里的评语是:服务器正确地、以很高的把握预测出了吉娃娃18看起来一切正常。

第 ⑤ 步 —— 停下来,把那个 0.86 到底是什么算一遍。

回到第 04 章那三条事实,它们书自己都印过19:

① 类别顺序是 ['chihuahua', 'muffin']
→ 吉娃娃是第 0 号,松饼是第 1 号 (书自己用文字确认过)

② 训练用的尺是「带 logits 的二分类」那一把
→ 它教模型的是:输出越大,越像第 1 号

③ 第 1 号是松饼
→ 所以 sigmoid 压出来的那个数 = 「是松饼」的把握

把这三条串起来:那个 0.86,含义是「这张图有 86% 的把握是松饼」。

而这张图是吉娃娃。

判断(我们的,不是书里的):第 ④ 步那个「答对了」是假象 —— 模型答错了, 只是被一张反过来的对照表掩盖成了对的。 「哪个数对应哪个类」的这张对照表,行话叫映射。 代码里那张映射写的是「压出来的数大于 0.5 就叫吉娃娃」20, 而这个数越大越是松饼 —— 反了,于是「86% 是松饼」被印成了「吉娃娃 86%」。 两次颠倒抵消,于是错误看起来像正确。 如果错,会错在: 如果加载数据时给出的类别顺序不是书打印的那样, 或者训练时用的标签不是这套编号,这条判断就不成立。 判据是可查的:书自己在第 04 章印出了那个类别列表, 并且在下一段用文字确认了「吉娃娃在最上面、排第 0 号,松饼排第 1 号」19

第 ⑥ 步,推上第一家云。 五个文件,git 推上去(细节在第 9 节)21

第 ⑦ 步,推上第二家云。 一条发布命令,拿到一个公网地址(细节在第 9 节)22

第 ⑧ 步 —— 反转在这里。这一次拿一张松饼图去问它。

书在本地测第二家云那套代码时,用命令行发了一次请求, 图片地址明明白白是一张松饼;而返回是23:

{"label": "chihuahua",
"prob_chihuahua": 0.6972862305017932,
"prob_muffin": 0.30271376949820683}

书对这个结果一个字都没有评论,直接说「不管你用哪种方法,都能看到服务器成功响应了请求」23

判断(我们的,不是书里的):这一次的真相和上一次正好相反 —— 模型其实答对了,是那层反着的映射把它印成了错的。 按第 ⑤ 步算出来的口径,0.697 的含义是「有 69.7% 的把握是松饼」 —— 而这张图正是松饼。模型判对了。 可代码里那句「大于 0.5 就叫吉娃娃」把它翻了过来,于是印出来的标签是错的。 更要命的是那两个字段名也跟着反了: prob_chihuahua 那一格里装的是「是松饼」的把握, prob_muffin 那一格里装的是「是吉娃娃」的把握 —— 两个名字都贴反了。 如果错,会错在: 同上一条 —— 如果类别顺序或训练标签不是我们推断的那样,整条不成立。 另外要说清:我们没有跑过这个模型,只是按书自己印出来的类别顺序、 损失函数和那段代码推的;这三样书里都有行号。

第 ⑨ 步,把这条线收网 —— 这是全书的落点。

第 03 章:立规矩「输出是原始分数,不是概率」

├─ 第 04 章 A:违反了它(拿 0.5 卡原始分数)
│ └→ 第 15 章:**悄悄修好了**(先 sigmoid 再卡 0.5),但书没说自己错过

└─ 第 04 章 B:类别名对错了位置
└→ 第 15 章:**原样抄到了线上,而且书一字未评**

图说:这是两条互不相干的线,不许说成一条因果链。 第一条到这一章结束了(被修好了);第二条到这一章才露出全貌。

这本书想教的最后一句话,被它自己坐实了:能上线,不等于是对的。 它花了整整一章讲怎么把模型送出去 —— 服务起来了、云上跑起来了、请求也回来了, 每一步都「成功」。 而那个从第 04 章带来的错误,一路穿过导出、穿过服务、 穿过两家云,没有任何一步会拦住它。 因为没有一个环节在检查「答案对不对」,它们只检查「有没有返回」。

9. 推上公网:两家云,两种做法

书选了两家,而且明说了各自适合谁24:第一家适合小项目和创业团队, 因为它直白、极容易落地;第二家适合大公司、或者要做很复杂的流程。 这两家的名字是 Heroku 和 Microsoft Azure24

9.1 第一家(Heroku):五个文件,git push 就完事

它的卖点是「你只管代码,基础设施它管」25 —— 服务器怎么搭、怎么扩容,不用你操心。 这一家叫 Heroku;书还提醒了一句实在的:每次跑只花几美分,但仍然要先绑一张卡25

要准备五个文件,而且后三个的文件名是固定的、不能随便起21:

文件文件名干什么
服务代码随便起第 7 节写的那个
模型文件随便起那份通用格式的模型
一份启动说明Procfile告诉它「用哪条命令把这个应用跑起来」
一份依赖清单requirements.txt要装哪些包,最好连版本号一起钉死
一份运行时说明runtime.txt用哪个版本的 Python(可选,但能避免版本冲突)

那份启动说明里有一个细节值得记:端口不是写死的,而是取一个环境变量 —— 那个变量由平台在运行时动态塞给你26写死端口,上去就跑不通。

推上去是五条 git 命令27:建一个本地版本库 → 把远端指向平台给你的那个库 → 把文件加进暂存区 → 提交 → 推上去。 推上去之后平台自动做三件事:把文件复制过去、照依赖清单装好环境、照启动说明把服务拉起来27

书还留了一条很实在的收尾提醒:不用了就把应用停掉、删掉,免得白花钱28

9.2 第二家(Microsoft Azure):先分组,再一条命令发布

这一家的路径长一些,因为它先要你把「东西放在哪、谁付钱」这件事理清楚。 它的名字叫 Microsoft Azure24

先说清一个它反复用的词:资源。 在这家云上,你建出来的每一样东西都叫一个资源 —— 跑你代码的那个应用是一个,它用的存储是一个,数据库也是一个。

第一件要理清的是「谁付钱」:那个既管计费、又装着你所有资源的容器,就叫订阅。

订阅之下还有一级,书把两级都讲得很清楚29:

这一级是什么为什么要有
订阅一个计费单位,同时也是资源的逻辑容器一个账号下可以有好几个,比如按项目分开计费;还能设月度预算上限
资源组一个项目相关资源的容器好管理、好监控

「订阅」「资源组」这两个词照抄它界面上的原话 —— 你照着做的时候会一个个撞见, 所以这里不换成别的说法。

资源组要选一个地区,而选哪儿有三条互相打架的依据30:

  1. 离用户近 —— 延迟低;
  2. 法规要求 —— 数据能不能出境;
  3. 价钱 —— 同样的服务在不同地区价格不一样。

然后建一个「函数应用」(界面上写作 Function App,它就是提供函数执行环境的那个主资源) —— 这一章的配置是31: 按实际用量计费512 MB(书说这个小模型够用了,而且费用是随规格涨的)、 Python 3.12、地区选在德国中西部

代码这边要多写三个小文件32:一份全局配置、一份这个函数的配置 (谁能调它 —— 这里设成谁都能调、不用登录;哪些动作允许;所有路径都转给这个函数)、 以及一个把这个函数和你的服务接起来的入口文件。

本地先跑一遍(7071 端口),再一条命令发布上去 —— 它会自己打包、上传、装环境、启动,最后把公网地址打印出来33

10. 书里的立场与证据

书里给了证据的:

  • 本地那次请求的完整返回 —— 状态码和 JSON 都有截图;
  • 命令行那次请求的完整返回 —— 类别名和两个概率值,连小数位都印全了(正是这一条让我们能把第 8 节那笔账算出来);
  • 发布命令的完整输出 —— 上传体积、耗时、公网地址;
  • 依赖清单里的七个包和各自的版本号 —— 原样印了。

作者的经验判断(书里没给证据):

  • 「第一家适合小项目,第二家适合大公司」 —— 一个合理的分类,但没有给成本或性能对照;
  • 「512 MB 对这个小模型够用」 —— 没有测过;
  • 三种协议的取舍 —— 定性描述,没有任何一组关于快慢的实测数字。

书里没交代来历的: 那份通用格式是谁提的、什么时候出现的; 三种协议各自的规范在哪儿 —— 全书一字未提。这一章仍然是零文献。

书里出问题的地方: 那个类别映射,以及对着结果一字未评 —— 见第 8 节的两个判断块。

书里悄悄改了口径的地方: 阈值那一处 —— 见第 7.2 节。

11. 边界与局限

这本书对的地方先说清:这一章是全书最有价值的一章,也是它区别于同类书的唯一理由。 从「模型只是硬盘上一个文件」这个处境讲起,到端点、到协议选型、到导出格式、 到两家云的完整操作,每一步都能照着做出来

「端点」那个用自己网站地址栏举的例子,是我们见过讲这个概念最省事的一次; 预处理那四步和训练时逐一对应,也做得很规矩。

但有三处要当心:

  1. 类别名对错了位置,一路带到线上,而且书对错误的输出一字未评 —— 见第 8 节的两个判断块。这是全书最严重的一处。
  2. 阈值那条规矩被悄悄修好了,但书没说自己前面错过 —— 见第 7.2 节。 对着第 04 章敲代码的读者,不会知道那一步要改。
  3. 服务的接入被设成了「谁都能调、不用登录」 —— 书给的理由是「为了简单」32这在演示里没问题,但它是一个公网地址 —— 书没有提醒这一点。

这一章没覆盖的:

  • 没有任何一步在检查「答案对不对」。 整章的测试只检查「有没有返回、状态码对不对」—— 而这正是那个错误能一路穿过去的原因;
  • 模型上线之后怎么盯,没有讲。 第 14 章的英文标题里有「监控」,可那一章讲的是训练期间; 上线之后请求量、延迟、以及模型表现会不会随时间变差,两章都零命中;
  • 怎么更新一个已经上线的模型,没有讲。 第 14 章刚讲完模型注册表有版本号, 可这一章的部署里,模型是一个被复制进去的文件,和注册表没有任何关系 —— 两章之间断了;
  • 打包成容器只提了一个名字。 CI/CD 那一节提了一句「把代码打成一个能跑的东西」, 举例时提到了容器这个词,但全书没有展开;
  • 同时来很多请求会怎样、怎么扩容、成本怎么估,都没有。 一个请求要跑多久,零命中;
  • 鉴权、限流、输入校验都没有。 除了「设成谁都能调」那一句,安全侧完全空白。

12. 可带走的

主走查一行写完: 第 04 章那个模型 → 导成通用格式 ONNX(输入 (1,1,32,32)、版本 12) → 用 FastAPI 包成一个服务、开 //predict 两个端点 → uvicorn 在本机 8000 端口起来 → 拿吉娃娃图问,答「吉娃娃 86%」把 0.86 的真实含义算出来:那是「86% 是松饼」,模型答错了 → 推上 Heroku 和 Microsoft Azure 两家云 → 拿松饼图问,答「吉娃娃 0.697」0.697 的真实含义是「69.7% 是松饼」,模型这次答对了,是标签被印反了。

  1. 模型留在脚本里等于零 —— 部署就是把它接进真实流程、让别人能用;
  2. 后端不直接和用户说话,一切经过前端这个中间人;模型住在后端;
  3. 接口是「能被别的程序叫到」的那一套,端点是其中一个具体地址;
  4. 地址栏里那些路径就是端点 —— 这一章只开两个:报活的和干活的;
  5. 三种协议:REST 最常用(网址 + 动词 + JSON)、gRPC 走二进制所以快、 GraphQL 让调用方自己点菜;
  6. CI 管「代码进库就自动测、自动构建」,目的是尽早让流程停下来;
  7. CD 有两个意思,差别只在放行是不是自动的 —— 这就是「周五不上线」的落点;
  8. 导成通用格式,是为了让不装 PyTorch 的运行环境也能跑;
  9. 导出时要给一个样例输入(拿它把整张图走一遍)和一个格式版本号;
  10. 线上的预处理必须和训练时一模一样,差一步就废 —— 灰度、32×32、除 255、挪到 −1~+1、补两维;
  11. 启动说明(Procfile)里的端口要取环境变量,不能写死 —— 平台在运行时才塞给你;
  12. 两级容器:订阅是计费单位(可设月度预算),资源组是项目的容器;
  13. 选地区有三条打架的依据:离用户近 / 法规 / 价钱;
  14. 那个从第 04 章带来的类别映射错误,穿过导出、服务、两家云,没有一步拦得住 —— 因为没有一个环节在检查答案对不对;
  15. 「吉娃娃 86%」其实是「86% 是松饼」,模型答错了; 「吉娃娃 0.697」其实是「69.7% 是松饼」,模型答对了,是标签印反了;
  16. 能上线,不等于是对的 —— 这本书想教的那句话,被它自己坐实了。

13. 原文地图

主题原书章原文位置
部署是什么、周五不上线Chapter 13text/15-ch13-chapter-13.txt:7(搜「deployment means responsibility」)
CI/CDChapter 13text/15-ch13-chapter-13.txt:24(搜「continuous delivery (CI/CD)」) · text/15-ch13-chapter-13.txt:29(搜「shared repository」) · text/15-ch13-chapter-13.txt:34(搜「CD can have two meanings」)
前端与后端Chapter 13text/15-ch13-chapter-13.txt:37(搜「data flow of an internet service」) · text/15-ch13-chapter-13.txt:52(搜「microservices」) · text/15-ch13-chapter-13.txt:54(搜「don't communicate」)
三种协议Chapter 13text/15-ch13-chapter-13.txt:60(搜「REST API」) · text/15-ch13-chapter-13.txt:69(搜「gRPC」) · text/15-ch13-chapter-13.txt:74(搜「GraphQL」)
端点的定义与例子Chapter 13text/15-ch13-chapter-13.txt:85(搜「listens to requests at different endpoints」) · text/15-ch13-chapter-13.txt:103(搜「it will implicitly call」) · text/15-ch13-chapter-13.txt:107(搜「two endpoints」)
通用格式与导出Chapter 13text/15-ch13-chapter-13.txt:117(搜「Open Neural Network Exchange」) · text/15-ch13-chapter-13.txt:124(搜「dummy_input = torch.randn」)
服务的三样零件与第一个端点Chapter 13text/15-ch13-chapter-13.txt:139(搜「fastapi package」) · text/15-ch13-chapter-13.txt:170(搜「name of the function」)
第二个端点的四步与预处理Chapter 13text/15-ch13-chapter-13.txt:180(搜「implementation of the」) · text/15-ch13-chapter-13.txt:215(搜「Image preprocessing」) · text/15-ch13-chapter-13.txt:229(搜「logits = session.run」)
本地起服务与测试Chapter 13text/15-ch13-chapter-13.txt:240(搜「uvicorn.run」) · text/15-ch13-chapter-13.txt:268(搜「accessibility of the server itself」) · text/15-ch13-chapter-13.txt:295(搜「200 OK」)
落点 类别映射的三条事实Chapter 4text/06-ch04-chapter-4.txt:735(搜「Chihuahua」) · text/06-ch04-chapter-4.txt:741(搜「'chihuahua', 'muffin'」)
落点 线上那次松饼测试Chapter 13text/15-ch13-chapter-13.txt:232(搜「label = 」) · text/15-ch13-chapter-13.txt:645(搜「Muffin_NIH.jpg」) · text/15-ch13-chapter-13.txt:647(搜「0.6972862305017932」)
第一家云:五个文件与 gitChapter 13text/15-ch13-chapter-13.txt:305(搜「simplifies the deployment」) · text/15-ch13-chapter-13.txt:330(搜「required files」) · text/15-ch13-chapter-13.txt:354(搜「Procfile」) · text/15-ch13-chapter-13.txt:436(搜「push the local changes」)
第一家云:停掉与删掉Chapter 13text/15-ch13-chapter-13.txt:471(搜「avoid unnecessary costs」)
第二家云:订阅与资源组Chapter 13text/15-ch13-chapter-13.txt:504(搜「billing unit」) · text/15-ch13-chapter-13.txt:514(搜「resource group」) · text/15-ch13-chapter-13.txt:518(搜「choice of region」)
第二家云:三个文件与函数应用Chapter 13text/15-ch13-chapter-13.txt:544(搜「host.json」) · text/15-ch13-chapter-13.txt:573(搜「authLevel」) · text/15-ch13-chapter-13.txt:662(搜「Flex Consumption」) · text/15-ch13-chapter-13.txt:673(搜「Germany West Central」)
第二家云:本地跑与发布Chapter 13text/15-ch13-chapter-13.txt:619(搜「func start」) · text/15-ch13-chapter-13.txt:685(搜「publish」)

Footnotes

  1. 出处:「Chapter 13」第 110-114 段(text/15-ch13-chapter-13.txt:111,搜「binary classification model from Chapter 4」)。原文:我们要部署第 4 章那个把图像分成「松饼」和「吉娃娃」两类的二分类模型;为此必须把训练好的模型导出来。

  2. 出处:「Chapter 13」第 2-9 段(text/15-ch13-chapter-13.txt:7,搜「deployment means responsibility」)。原文:这句简单的话透露了部署的风险,因为没人想用一个周末去找错;但这句话的核心还在于,部署意味着责任 —— 你的同事和客户在依赖你;部署讲的是把开发成果(比如一个新的深度学习模型)接进真实的业务流程,让模型能被别人使用。书页首那句行内老话是「周五不上线」。 2

  3. 出处:「Chapter 13」第 37-56 段(text/15-ch13-chapter-13.txt:37,搜「data flow of an internet service」、text/15-ch13-chapter-13.txt:52,搜「microservices」、text/15-ch13-chapter-13.txt:54,搜「don't communicate」)。原文:最终用户通过浏览器访问服务,浏览器隐式地与前端服务器通信,前端服务器负责正确显示网站;前端服务器一般不是静态的,而是对用户输入作出反应 —— 以聊天机器人为例,用户输入的文字被转给后端服务器处理,后端服务器上跑着各种被称为微服务的服务;在这个例子里文字会被转给第三方提供的语言模型,也可能从数据库取数据;关键在于后端服务器不直接与最终用户通信,通信只通过前端服务器这个中间人进行。 2 3

  4. 出处:「Chapter 13」第 84-87 段(text/15-ch13-chapter-13.txt:85,搜「listens to requests at different endpoints」)。原文:这一节我们要造一个响应请求的 REST API 服务;一个 REST API 服务在不同的端点上监听请求 —— 端点是客户端为了与这个 REST API 交互而把请求发过去的那个具体网址,它也是使用这台服务器的接入点。

  5. 出处:「Chapter 13」第 103-107 段(text/15-ch13-chapter-13.txt:103,搜「it will implicitly call」)。原文:如果你在浏览器里打开作者的网站,它隐式地调用的是默认的「/」端点;如果你在网站上打开某本书的介绍,你会在地址栏里看到端点变了。原文由此引出:我们要造一个带两个端点的 REST API —— 「/」和「/predict」。

  6. 出处:「Chapter 13」第 88-101 段(text/15-ch13-chapter-13.txt:90,搜「status」)与第 174-176 段(text/15-ch13-chapter-13.txt:174,搜「@app.get(」)。原文那张图给出两个端点各自的返回:根端点返回状态与一句「请用 GET /predict?image_url=…」的提示;预测端点返回标签和两个类别各自的概率。

  7. 出处:「Chapter 13」第 57-79 段(text/15-ch13-chapter-13.txt:60,搜「REST API」、text/15-ch13-chapter-13.txt:69,搜「gRPC」、text/15-ch13-chapter-13.txt:74,搜「GraphQL」)。原文三条:REST API 在通信协议里排在首位,是互联网最重要的支柱之一,因为几乎所有服务之间的通信都基于它;它建立在 HTTP 协议上,动作由 GET、POST、PUT、DELETE 这些 HTTP 方法定义,简单、通用,用标准的 JSON 格式交换数据,与浏览器这类客户端交互非常直接。gRPC 是 Google 开发的现代框架,与交换文本数据的 REST 不同,它用专门的缓冲做二进制通信,因此更快更高效 —— 要高速和低延迟时选它。GraphQL 是 Facebook(现 Meta)开发的 API 查询语言,它没有 REST 那样固定的端点,客户端可以精确请求自己需要的数据,一次请求就能从多个资源取数,而 REST 需要多次请求,所以它适合数据结构复杂的应用。 2 3

  8. 出处:「Chapter 13」第 20-33 段(text/15-ch13-chapter-13.txt:24,搜「continuous delivery (CI/CD)」与 text/15-ch13-chapter-13.txt:29,搜「shared repository」)。原文:部署是 CI/CD 这个更全面的流程里不可分割的一部分,它是现代软件开发的一项核心实践 —— 一串自动化流程,目标是让软件改动又快又可靠地从开发者送到最终用户手里;这个语境下常说 CI/CD 流水线,也就是每次代码改动都会跑一遍的自动化步骤链;CI 关注的是把多个开发者的代码自动地并进一个共享的版本库(通常是一个 Git 版本库),每次上传代码都会触发一个自动流程,这个流程可以包括自动测试(单元测试与集成测试)或自动构建(把代码合成一个可执行文件或一个容器);目的是尽可能快地发现问题,让流程停下来,不把有缺陷的代码部署出去。「Git 版本库 = 记录代码每一次改动、能回退到任意一次的仓库」这句括号是我们补的(来自通用知识)。 2 3 4 5 6

  9. 出处:「Chapter 13」第 34-36 段(text/15-ch13-chapter-13.txt:34,搜「CD can have two meanings」)。原文:CD 可以有两个意思 —— 持续交付或持续部署,取决于改动是被直接部署(持续部署),还是只是先准备好、等待人工放行(持续交付)。

  10. 出处:「Chapter 13」第 116-122 段(text/15-ch13-chapter-13.txt:117,搜「Open Neural Network Exchange」)。原文的信息框:ONNX 是一种开放格式,改善 PyTorch、TensorFlow、Keras 等不同深度学习框架之间的互通;有了它,你可以用 PyTorch 训练模型,再把它导出到另一个运行时环境;这对部署尤其有用,因为许多生产环境使用的是经过优化的运行时,而这些运行时可能与训练所用的框架并不兼容。 2

  11. 出处:「Chapter 13」第 124-131 段(text/15-ch13-chapter-13.txt:124,搜「dummy_input = torch.randn」)。原文的导出调用里,样例输入是形状 (1, 1, 32, 32) 的随机张量,导出的文件名是 bin_class_model.onnx,格式版本设为 12。「导出这一步要拿样例输入把整张计算图走一遍」这句解释书里没有,是我们补的(来自通用知识)。 2

  12. 出处:「Chapter 13」第 138-167 段(text/15-ch13-chapter-13.txt:139,搜「fastapi package」、text/15-ch13-chapter-13.txt:146,搜「import onnxruntime as ort」与 text/15-ch13-chapter-13.txt:159,搜「inference session」)。原文:用 fastapi 这个包造 REST API,用 pydantic 做类型安全,用 PIL 处理图片;REST API 由 uvicorn 启动,uvicorn 是执行这个应用的异步服务器。导入清单里另有 onnxruntime(简写成 ort);加载模型时用它创建一个推理会话,把模型文件路径传进去,并用一个参数强制在 CPU 上执行。 2

  13. 出处:「Chapter 13」第 169-178 段(text/15-ch13-chapter-13.txt:170,搜「name of the function」)。原文:默认端点用一个装饰器定义成对「/」路径的 GET 路由;然后定义函数 —— 这个函数叫什么名字无关紧要,可以随便起;它不接收任何参数,返回一个写死的字典。 2

  14. 出处:「Chapter 13」第 180-234 段(text/15-ch13-chapter-13.txt:180,搜「implementation of the」与 text/15-ch13-chapter-13.txt:215,搜「Image preprocessing」)。原文的四步:① 把图片地址作为参数传进来,下载这个地址下的图片 —— 由于图片常常只能通过浏览器下载,必须用一个头部对象「模拟」成浏览器,再转成正确维度的数组;② 预处理下载来的图片 —— 缩到 32×32 并转成灰度;③ 让图片走一遍模型的正向传播做出预测,并根据概率值给出一个标签;④ 把结果作为 JSON 返回,含标签与两个类别各自的概率。代码里的预处理依次是:转灰度、缩到 (32,32)、除以 255、再做 (x − 0.5) / 0.5、两次补维度。 2

  15. 出处:「Chapter 13」第 229-232 段(text/15-ch13-chapter-13.txt:229,搜「logits = session.run」)。代码里先把模型输出取出来(变量名就叫 logit),再手动过一遍 sigmoid 得到 prob,然后拿 0.5 卡这个 prob这和第 04 章直接拿 0.5 卡原始分数(text/06-ch04-chapter-4.txt:743,搜「still probabilities」)不是一回事 —— 这一处修好了,但书没有说明自己前面错过。

  16. 出处:「Chapter 13」第 239-246 段(text/15-ch13-chapter-13.txt:240,搜「uvicorn.run」)。原文:这段代码只在直接运行这个文件时才执行;通过 uvicorn 启动服务器,并设置主机参数,让它在所有网络接口上监听 8000 端口的请求。

  17. 出处:「Chapter 13」第 266-271 段(text/15-ch13-chapter-13.txt:268,搜「accessibility of the server itself」)。原文:先在浏览器里访问本机 8000 端口检查服务器本身是否可达;这个请求走的是默认端点,返回一条状态为 ok 的消息,以及一句提示我们该用预测端点的说明。

  18. 出处:「Chapter 13」第 287-300 段(text/15-ch13-chapter-13.txt:295,搜「200 OK」)。原文:用 Postman 发一个 GET 请求,地址指向本机的预测端点,参数里给一个图片地址 —— 测试用的是维基百科上的一张吉娃娃图;屏幕下方能看到服务器的响应,200 OK 表示请求成功;服务器的实际响应是 JSON,而且服务器以很高的概率(86%)正确地预测出了吉娃娃。 2

  19. 出处:「Chapter 4」第 735-751 段(text/06-ch04-chapter-4.txt:735,搜「Chihuahua」与 text/06-ch04-chapter-4.txt:741,搜「'chihuahua', 'muffin'」)。原文:现在必须检查哪个类别对应哪个数字 —— 用类别属性可以查到原始类别,发现「Chihuahua」排在列表最上面因此索引为 0,而「Muffin」是 1;打印结果是 ['chihuahua', 'muffin']。而那个模型训练时用的损失函数是 BCEWithLogitsLoss(text/06-ch04-chapter-4.txt:605,搜「binary cross entropy nn.BCEWithLogitsLoss」),这把尺教模型的是「输出越大越像索引 1 那一类」,也就是松饼。 2

  20. 出处:「Chapter 13」第 232 段(text/15-ch13-chapter-13.txt:232,搜「label = 」)。代码里那一行判定写的是:概率大于 0.5 就叫 chihuahua,否则叫 muffin;返回的字典里 prob_chihuahua 装的是这个概率、prob_muffin 装的是 1 减去它。这与第 04 章那一行(text/06-ch04-chapter-4.txt:749,搜「'chihuahua' if float(i[0]) > threshold」)是同一套映射,原样搬了过来。

  21. 出处:「Chapter 13」第 326-352 段(text/15-ch13-chapter-13.txt:330,搜「required files」与 text/15-ch13-chapter-13.txt:337,搜「Procfile」)。原文列出的五个文件:服务代码、模型文件、一份告诉平台该用哪些命令启动应用的文件、一份汇总所有依赖包的清单、一份指定 Python 具体版本的文本文件(可选,但能避免版本差异带来的兼容问题)。原文的文件清单里,后三个的名字分别是 Procfilerequirements.txtruntime.txt。依赖清单里钉了七个包的版本号,运行时文件里写的是 python-3.12.5 2

  22. 出处:「Chapter 13」第 680-705 段(text/15-ch13-chapter-13.txt:685,搜「publish」)。原文:有一条命令行命令用于部署,它读取当前工作目录里的代码并「填充」应用容器,然后准备环境、启动应用 —— 这些用一条命令就能全部完成;输出里会给出可调用的公网地址,它就是监听请求的那个端点。

  23. 出处:「Chapter 13」第 632-651 段(text/15-ch13-chapter-13.txt:645,搜「Muffin_NIH.jpg」与 text/15-ch13-chapter-13.txt:647,搜「0.6972862305017932」)。原文在本地测试那一节列了三种测法,第三种是在终端里用 curl 发请求 —— 请求里的图片地址是一张松饼图,而打印出来的返回是 {"label":"chihuahua","prob_chihuahua":0.6972862305017932,"prob_muffin":0.30271376949820683} 紧接着的一句是:不管你选哪种方法,都应该看到服务器成功响应了请求。书对这个把松饼判成吉娃娃的结果没有任何评论。 2

  24. 出处:「Chapter 13」第 15-18 段(text/15-ch13-chapter-13.txt:15,搜「practical for small projects」)。原文:第 13.3 节讲用 Heroku 部署,它对小项目和创业团队很实用,因为它直白、非常容易落地;第 13.4 节讲用 Microsoft Azure 部署,如果你在大公司工作、或者要映射一个非常复杂的工作流,它可能更合适。 2 3

  25. 出处:「Chapter 13」第 304-309 段(text/15-ch13-chapter-13.txt:305,搜「simplifies the deployment」)。原文:Heroku 是一个简化应用部署与管理的流行服务,它以对开发者友好的界面和与 Git 的轻松集成著称;它让开发者以最小的运维投入完成部署 —— 你部署代码,服务负责服务器搭建、扩容这类基础设施上的复杂问题,于是你可以专注在开发应用上。原文还提到:每次应用运行只花几美分,但仍然需要添加支付方式。 2

  26. 出处:「Chapter 13」第 354-366 段(text/15-ch13-chapter-13.txt:354,搜「Procfile」)。原文:这个文件里是让平台正确启动应用所需的确切命令;web 这个进程类型告诉平台这是一个应该响应 HTTP 请求的网页服务;uvicorn 是运行 FastAPI 这类异步 Python 应用所需的异步服务器网关接口;模块路径的前半截指文件名、后半截是文件里那个应用实例的名字;主机参数让服务器监听所有网络接口,而端口参数引用的是一个环境变量 —— 它由平台动态设置。

  27. 出处:「Chapter 13」第 406-447 段(text/15-ch13-chapter-13.txt:436,搜「push the local changes」)。原文的几条命令依次是:初始化一个空的 Git 版本库;把本地版本库连到平台为你的应用提供的那个专门版本库(要填上一步用的应用名);把要发布的文件加进暂存区(原文说明:这一步还没把文件复制到平台,只是放进一个叫暂存区的本地子目录);提交并写一条消息;如果默认分支叫 master,要改名成 main,因为平台要求文件在 main 分支上;最后把本地 main 分支推上去。原文说明:这会启动供给流程、把所有文件复制过去,然后照依赖清单搭好环境,并照那份启动说明启动服务器。 2

  28. 出处:「Chapter 13」第 470-483 段(text/15-ch13-chapter-13.txt:471,搜「avoid unnecessary costs」)。原文:为了避免不必要的花费,不再需要时应该把应用停掉,这个选项在「Resources」页里,那里也显示按小时产生的费用;永久删除应用的选项在「Settings」页最下面。

  29. 出处:「Chapter 13」第 499-516 段(text/15-ch13-chapter-13.txt:504,搜「billing unit」与 text/15-ch13-chapter-13.txt:514,搜「resource group」)。原文:创建账户之后下一步是创建订阅 —— 订阅既是一个计费单位,也是你所创建资源的逻辑容器;一个账户下可以有多个订阅(比如为了把不同项目分开管理和计费);创建订阅时需要一个用来承担费用的计费账户,而且可以在预算页设置每月允许的上限,以免在费用上遇到不愉快的意外。为了组织资源,还应当创建一个资源组 —— 一个把某个应用或项目相关的资源放在一起的逻辑容器,这让管理和监控更容易。

  30. 出处:「Chapter 13」第 517-520 段(text/15-ch13-chapter-13.txt:518,搜「choice of region」)。原文:资源组总是隶属于某个订阅,并且有一个唯一的名字;地区的选择取决于多个影响因素 —— 比如离应用用户近可能很重要,以保证低延迟;但法规要求也可能起作用;还要注意同样的服务在不同地区价格可能不同。

  31. 出处:「Chapter 13」第 655-676 段(text/15-ch13-chapter-13.txt:662,搜「Flex Consumption」与 text/15-ch13-chapter-13.txt:673,搜「Germany West Central」)。原文:函数应用是提供函数执行环境的主要资源;创建时可以选托管方式,这里选按实际使用量计费的那一种;然后选订阅和资源组、填应用名(这里是 pytorchmodel);地区设为德国中西部,运行时选 Python 3.12;最后定实例规格 —— 用最小的 512MB,因为对这个小模型已经够了,而且费用是随规格上涨的。

  32. 出处:「Chapter 13」第 527-614 段(text/15-ch13-chapter-13.txt:544,搜「host.json」与 text/15-ch13-chapter-13.txt:573,搜「authLevel」)。原文:要部署并运行一个函数应用,需要几个必备文件 —— 一份全局配置(它设定的东西对项目里所有函数都生效,典型设置涉及日志或扩展)、一份这个函数的配置文件、以及一个入口文件;此外还要模型文件和实际的程序逻辑。那份函数配置里:一个参数指明被调用时执行哪个 Python 文件;权限级别参数定义谁可以调用这个函数,为了简单起见设成了匿名,于是任何人都能在不登录的情况下调用;函数响应 HTTP 请求,可以定义允许的方法,而路由参数指定把所有 URL 路径都转给这个函数。入口文件把这个函数的 HTTP 触发器和网页应用接起来,而且主函数必须是异步的。 2

  33. 出处:「Chapter 13」第 617-630 段(text/15-ch13-chapter-13.txt:619,搜「func start」)与第 688-701 段(text/15-ch13-chapter-13.txt:688,搜「functionapp publish」)。原文:一条命令在本地启动应用,输出里显示它监听在本机 7071 端口的所有路径上;发布时的输出依次是「取站点发布信息 → 开始部署 → 打包当前目录 → 远程构建 → 上传 148.98 KB → 部署成功」,最后打印出可调用的公网地址。