首页 > 应用开发 > Python > 你的模型只有在别人能调用它时才算完成
为流失预测构建一个 FastAPI 端点,以及从“它能运行”到“它上线了”之间所有出问题的地方

给你一些背景,我很好奇构建和部署机器学习模型到底涉及什么。所以,与其学习理论,和往常一样,我决定通过构建来学习,因为我仍然相信这是最好的学习方式。我选择的第一个项目是一个流失预测器 API(应用程序编程接口)。
这是我构建的一个小型 FastAPI 应用,用来预测客户是否即将流失。它的工作方式是,你输入客户数据,它返回一个概率、一个预测和一个风险等级。在我上一篇文章中,我训练了模型,把它包装成一个端点,并在我自己的笔记本电脑上的 Swagger UI 中进行了测试。它运行正常,每个请求都返回了真实答案。你可以在这里阅读那篇文章。此时,我以为我已经完成了它。
远没有。事实证明,我构建的模型在任何方面都没有用。它只是另一个只有我能玩的玩具。它无法在我笔记本电脑休眠后继续存活,更不用说被一个真正在浏览器中输入 URL 的陌生人访问了。
所以我将在本文中解决这个问题。我会经历那些不那么光鲜的工作,让它变得可访问。所以,我会带你了解我如何将它容器化,并迁移到一台真实服务器上。顺便说一句,我还遇到了三个我未曾预料到的独立故障,我也会在本文中指出。
这个项目实现了一个 FastAPI 应用 churn-api。它提供了一个简单的 /predict 端点,使用一个预训练的 scikit-learn 管道,该管道由缩放器和分类器组成。它用于预测给定客户流失的可能性。如果你是第一次接触这个项目,我建议从 Part A 开始,因为本文是从那里继续的。
问题就在这里。当然,这个项目在我的电脑上运行得很好,问题在于它有一个特定的 python 版本、特定的包版本、特定的文件路径,而部署它的其他人的电脑或 AWS 服务器可能没有这些。
Docker 解决了这个问题。方式如下:不再只是交付代码,并寄希望于机器配置正确后它能运行(这是另一个问题),现在的交付物是一个自给自足的镜像,它可以在任何电脑上运行,并且其环境(python 版本、所需的各种库、文件夹结构)不再产生影响。这是因为所有这些都随镜像本身一起发布,所以现在接收方不需要设置任何东西。
在我开始编写 Dockerfile 之前,我就想确保自己清楚本地环境中到底安装了哪些东西。目标是在容器内使用相同的包版本,而不是让 pip 安装当时碰巧是最新的版本。
起初,我尝试使用 conda list,但它并没有给出我需要的干净的包版本列表。于是我改用 pip freeze,它给出了我实际使用的包的精确版本:
fastapi==0.141.1 uvicorn[standard]==0.40.0 scikit-learn==1.9.0 pandas==3.0.3 numpy==2.4.6 pydantic==2.13.4 joblib==1.5.3
还有一个细节最终证明很重要:我本地运行的是 Python 3.14.6,这相当新。所以我不能出于习惯就直接使用常见的 python:3.11-slim 基础镜像。如果我希望容器尽可能贴近我的本地环境,Dockerfile 也需要使用 Python 3.14。
FROM python:3.14-slim WORKDIR /code COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt COPY ./app ./app COPY ./models ./models EXPOSE 8000 CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
构建本身成功了,我没有遇到任何错误。虽然花了大约 166 秒,可能是因为一些包还没有针对这个较新的 Python 版本提供预构建的 wheel,不得不从源码编译。
然后我运行了它,得到了这个:
ModuleNotFoundError: No module named 'schemas'
这是那种只有在我开始运行容器之后才意识到存在的问题。在构建过程中,没有任何迹象表明会出现这样的错误。
当我在本地运行应用时,我总是位于 app/ 文件夹中,所以运行 uvicorn main:app --reload 从来不是问题:schemas.py 就在附近,紧挨着 main.py,Python 很容易就能找到。
但 Dockerfile 有细微的不同。在那里,我的 CMD 是在 /code 中运行 uvicorn app.main:app,这让 Python 认为 app 是一个包,并且应该被当作包来对待。换句话说,它有不同的工作目录,而这足以让它出问题。
修复很简单。与其修改项目结构,不如直接告诉 Uvicorn 以 app 文件夹作为根来加载应用程序。这样可以模拟我之前使用它的方式。
CMD ["uvicorn", "main:app", "--app-dir", "app", "--host", "0.0.0.0", "--port", "8000"]
运行这个之后,我重新构建了镜像并启动了容器,这一次,我没有遇到任何问题。
有了这个;就像在 Part A 中一样,我随后向我的 FastAPI 发送了一个虚拟请求,唯一目的是对照 Part A 的数字进行检查。这个测试返回的结果正是我所希望的:相同的流失概率,相同的预测,相同的风险等级。
有了这个;就像在 Part A 中一样,我随后向我的 FastAPI 发送了一个虚拟请求,唯一目的是对照 Part A 的数字进行检查。这个测试返回的结果正是我所希望的:相同的流失概率,相同的预测,相同的风险等级。
因此,我知道我的容器没有改变或更改应用程序的行为方式。它的运行方式与在我的机器上运行时相同。所以第一个检查点:完成。
我再次选择了 EC2,就像 rss-pipeline 部署时一样。这有助于保持我对项目架构想法的一致性;这个项目使用的是一个 t3.micro 实例,运行 Ubuntu;该实例类型符合免费套餐资格,非常适合部署运行单个模型的 FastAPI 应用程序。
设置过程主要涉及在 AWS 控制台上浏览各种选项。我启动了一个实例并为其配置了 Ubuntu;我选择了 t3.micro 实例类型,并生成了一个用于 SSH 连接的密钥对。安全组的配置需要更加谨慎:我选择了两条入站规则。第一条应用于端口 22 上的 SSH,仅允许来自我的 IP 的流量,而第二条涉及端口 8000 上的自定义 TCP 规则。它适用于所有人,因为希望允许通过此端口进行 API 访问。
最后一点:我遵循了使用 IAM 用户而非根 AWS 账户的习惯。当从根 AWS 账户执行操作时,对该账户的访问不受任何边界限制。执行诸如设置额外实例之类的工作会使用此账户。然而,这风险更大,因为即使出现意外情况的可能性极小,也会造成大量破坏。使用具有权限的 IAM 用户将像根账户执行任务一样运作,并且在出现问题时影响范围更小,理应如此。
通过 SSH 连接到实例后,在服务器上安装了 Docker:
sudo apt update sudo apt install -y docker.io sudo systemctl start docker sudo systemctl enable docker sudo usermod -aG docker $USER
我决定直接在实例上使用 Dockerfile 重新构建 Docker 镜像,而不是将其添加到 Amazon ECR。对于这样的小规模项目,复制本地测试数据似乎更容易——这为我节省了一些 AWS 学习成本。
然后我使用 scp 将项目文件传输到实例上,
scp -i "churn-api-key.pem" -r . ubuntu@:/home/ubuntu/churn-api
然后我遇到了一个障碍:
ssh: connect to hostport 22: Connection timed out
我的第一个想法不可避免地是我在实例上做错了什么。然而,实际上并非如此。相反,问题出在我的安全组防火墙规则上,该规则将 SSH 访问限制为仅我的 IP 地址。结果,当我下次尝试 SCP 时,我的 IP 已经变了。更新后,我再次连接成功,文件也成功传输了。
这可能是个小问题,但值得指出的一个常见小故障。
解决这个问题后,复制顺利完成,在实例本身上构建镜像的命令与本地相同:
docker build -t churn-api .
它的运行速度比在我笔记本电脑上正常运行时慢得多,这是因为 t3.micro 可用于编译软件包的 CPU 较少。但构建成功运行了,这对我来说才是最重要的。
docker run -d -p 8000:8000 churn-api
这里的 -d 很重要。它的作用是让容器在后台以分离模式持续运行,因此即使我关闭 SSH 会话,它仍会继续运行。如果没有它,容器会在我断开连接的瞬间停止,这就违背了整个初衷。
然后,从我自己的笔记本电脑(而不是服务器)上,我打开浏览器并访问:
http://
同样的 Swagger UI 加载出来了。然后我再次通过 /predict 发送了同一个测试客户数据,得到了与本地和本地容器中相同的返回值。现在,任何地方的任何人,只要拥有那个 IP 地址,就可以调用我的模型并获得真实的答案。
我最初想忽略这个阶段,但我觉得为了确保这个项目达到生产就绪状态,保留它是值得的。你看,EC2 的默认公有 IP 并不保证保持不变,尤其是当实例停止并重新启动时。附加弹性 IP 可以解决这个问题,确保 IP 地址保持不变,并且每当我重新加载服务器时都不会改变。设置起来并没有花太长时间。
这能工作,但它还不是生产级方案,我更愿意坦诚地说明剩余问题,而不是掩盖它们:
没有 HTTPS。目前,所有通信都通过端口 8000 上的纯 HTTP 进行,这对于学习项目来说完全可以接受,但在处理真实客户数据时就不可持续了。
端点缺乏身份验证。任何拥有该 IP 的人都可以调用 /predict。没有 API 密钥,没有速率限制,没有任何防止滥用的措施。
如果实例重启,容器不会自动重启。如果 AWS 重启底层硬件,我的 API 将在无人察觉的情况下静默停用,直到我发现并手动重启它。
没有 HTTPS。目前,所有通信都通过端口 8000 上的纯 HTTP 进行,这对于学习项目来说完全可以接受,但在处理实际客户数据时就不可持续了。
该端点缺乏身份验证。任何拥有 IP 的人都可以调用 /predict。没有 API 密钥,没有速率限制,没有任何阻止滥用的措施。
如果实例重启,容器不会自动重启。如果 AWS 重启底层硬件,我的 API 将被静默停用,直到我注意到并手动重启它。
这些问题并不难解决,然而它们尚未被解决,我更愿意指出它们,而不是让文章给人留下一种误导性的完成印象。
该模型从 notebook 演变而来,然后到本地 API,再到容器化 API,最终达到互联网上任何人都可以访问的状态。这一进展概括了本系列的主要目标,并且是与传统代码编写最显著的区别;事实上,遇到的大多数问题并非由于我代码中的逻辑错误,而是源于环境不匹配或基础设施资源的细微变化。
这段旅程的下一阶段是构建一个功能完整的 ML 应用并将其部署到云端。这将需要部署关于使用不同环境和变化的基础设施的相同见解,并将其应用于比简单的单端点服务更大的模型。
转载声明:本文为转载整理,原文版权归原作者及原站点所有。
原文来源:Towards Data Science
原创地址:https://towardsdatascience.com/your-model-isnt-done-until-someone-else-can-call-it/
原文作者:Ibrahim Salami;如有侵权,请联系管理员删除。