Your Model Isn't Done Until Someone Else Can Call It
Your Model Isn’t Done Until Someone Else Can Call It
你的模型还没完成,直到别人能调用它为止
To give you context, I got curious about what goes into building and deploying machine learning models. So, rather than learn theory. As always, I decided to learn by building, because I still believe this is the best way to learn. The first project I chose to tackle was a churn predictor api. It’s a small FastAPI app I built to predict whether a customer is about to churn. How it works is that, you feed it a customer’s data, it hands back a probability, a prediction, and a risk level. In my previous article, I trained the model, wrapped it in an endpoint, and tested it in Swagger UI on my own laptop. It worked and every request came back with a real answer. You can read the article here. At this point, I thought I was done with it. 为了让你了解背景,我开始对构建和部署机器学习模型的过程感到好奇。因此,我没有去学习理论,而是像往常一样决定通过实践来学习,因为我始终认为这是最好的学习方式。我选择的第一个项目是一个客户流失预测 API。这是一个我构建的小型 FastAPI 应用,用于预测客户是否即将流失。它的工作原理是:你输入客户数据,它会返回一个概率、一个预测结果和一个风险等级。在上一篇文章中,我训练了模型,将其封装在一个端点中,并在我自己的笔记本电脑上通过 Swagger UI 进行了测试。它运行良好,每个请求都能返回真实答案。你可以在这里阅读那篇文章。当时,我以为我已经完成了。
Not even close. It turns out that the model I built isn’t useful in any way. It’s just another toy only I can play around with. It couldn’t survive my laptop going to sleep, let alone being reachable by an actual stranger typing in a URL. So I’m going to be addressing this issue in this article. I’ll be going through the unglamorous work of making it reachable. So, I’ll be walking you through how I containerized it, and moved it to a real server. By the way, I also ran into three separate failures I didn’t see coming, which I’ll also point out in this article. 远非如此。事实证明,我构建的模型没有任何实际用途。它只是一个只有我能玩的玩具。它无法在我笔记本电脑休眠时存活,更不用说让真正的陌生人通过输入 URL 来访问了。因此,我将在本文中解决这个问题。我将完成那些枯燥乏味的工作,使其能够被外界访问。我将带你了解我是如何将其容器化并迁移到真实服务器上的。顺便提一下,我在此过程中遇到了三个意想不到的故障,我也会在文中指出。
Quick recap of where Part A left off: This project implements a FastAPI app churn-api. It provides a simple /predict endpoint that uses a pre-trained scikit-learn pipeline consisting of a scaler and a classifier. This is used to predict the likelihood of a given customer churning. If you are new to the project, I recommend starting with Part A, as this article continues from there.
第一部分回顾:本项目实现了一个名为 churn-api 的 FastAPI 应用。它提供了一个简单的 /predict 端点,使用了一个包含缩放器(scaler)和分类器(classifier)的预训练 scikit-learn 流水线。这用于预测给定客户流失的可能性。如果你是本项目的新手,建议先从第一部分开始,因为本文是基于该部分继续展开的。
Why Docker: So here is the problem. Sure the project works perfectly on my computer, the issue is that it has a specific python version, specific package versions, specific file paths which someone else’s computer on which it would be deployed on or an AWS server might not have. Docker solves this. Here is how: Instead of delivering just code which hopefully works after the machine is set correctly (which is an issue for another day), the deliverable now is a self-sufficient image that runs on any computer and on whose environment (python version, various libraries required, folder structure) no longer has an effect. This is because all of this ships with the image itself so now nothing is needed to be set by the receiver. 为什么选择 Docker:问题在于,虽然项目在我的电脑上运行完美,但它依赖于特定的 Python 版本、特定的包版本和特定的文件路径,而这些在部署的目标电脑或 AWS 服务器上可能并不具备。Docker 解决了这个问题。方法如下:与其只交付代码并祈祷在机器配置正确后能运行(那是另一个层面的问题),现在的交付物是一个自给自足的镜像,它可以在任何电脑上运行,且不受环境(Python 版本、所需的各种库、文件夹结构)的影响。这是因为所有这些都随镜像一起打包了,接收方无需进行任何额外配置。
Writing the Dockerfile and what broke: Before I even started writing the Dockerfile, I wanted to make sure I knew exactly what was installed in my local environment. The goal was to use the same package versions inside the container, rather than letting pip install whatever happened to be the latest version at the time. At first, I tried using conda list, but it wasn’t giving me the clean list of package versions I needed. So I switched to pip freeze, which gave me the exact versions of the packages I was actually working with.
编写 Dockerfile 及遇到的故障:在开始编写 Dockerfile 之前,我确保自己清楚本地环境中安装了什么。目标是在容器内使用相同的包版本,而不是让 pip 安装当时最新的版本。起初,我尝试使用 conda list,但它没有给我提供我需要的简洁版本列表。所以我改用了 pip freeze,它给出了我实际使用的包的精确版本。
(Package list omitted for brevity) (此处省略包列表)
And there was one more detail that turned out to matter: I was running Python 3.14.6 locally, which is pretty new. So I couldn’t just reach for the usual python:3.11-slim base image out of habit. If I wanted the container to match my local environment as closely as possible, the Dockerfile needed to use Python 3.14 as well.
还有一个细节至关重要:我在本地运行的是 Python 3.14.6,这是一个非常新的版本。所以我不能习惯性地直接使用常用的 python:3.11-slim 基础镜像。如果我想让容器尽可能贴近我的本地环境,Dockerfile 也必须使用 Python 3.14。
(Dockerfile and build process details omitted) (此处省略 Dockerfile 及构建过程细节)
The build itself succeeded and I got no errors. Although it took about 166 seconds, likely because some packages didn’t have prebuilt wheels yet for this new Python version and had to compile from source. Then I ran it, and got this: ModuleNotFoundError: No module named 'schemas'. This was one of those problems I only realised existed after I’d begun running the container. During the build nothing had suggested that such an error would arise.
构建本身成功了,没有报错。虽然耗时约 166 秒,这可能是因为某些包还没有针对这个新 Python 版本的预编译 wheel 文件,必须从源码编译。然后我运行它,得到了这个错误:ModuleNotFoundError: No module named 'schemas'。这是那种我只有在运行容器后才意识到的问题。在构建过程中,没有任何迹象表明会出现这种错误。
When I had run the app locally, I’d always found myself in the app/ folder, so running uvicorn main:app --reload wasn’t ever a problem: schemas.py was nearby, next to the main.py, easily found by Python. But the Dockerfile was subtly different. There my CMD was running uvicorn app.main:app in /code which led python to believe that the app was a package and that it should be being treated as such. In other words, it had a different working directory and that was enough to break it.
当我在本地运行应用时,我总是处于 app/ 文件夹中,所以运行 uvicorn main:app --reload 从来不是问题:schemas.py 就在附近,紧挨着 main.py,Python 可以轻松找到。但 Dockerfile 的情况略有不同。我的 CMD 在 /code 目录下运行 uvicorn app.main:app,这让 Python 认为 app 是一个包,并应按包的方式处理。换句话说,它处于不同的工作目录,这足以导致程序崩溃。
The fix was simple. Instead of altering the project structure, much simpler to just tell Uvicorn to use and load the application using the app folder as the root. This would mimic how I’d previously used it before.
修复方法很简单。与其修改项目结构,不如直接告诉 Uvicorn 使用 app 文件夹作为根目录来加载应用程序。这将模拟我之前的使用方式。
(Fixing the CMD command) (修复 CMD 命令)
After running this, I rebuilt the image and span the container, and this time, I got no issues. Armed with this; just as in Part A, I then sent off a dummy request to my FastAPI for the sole purpose of checking against my Part A numbers. This test came back with exactly what I’d hoped: same churn probability, same prediction, same risk level. I therefore knew my container hadn’t changed or altered how the application behaved. It ran the same way as it ran on my machine. So first checkpoint: done. 运行此命令后,我重新构建了镜像并启动了容器,这次没有出现任何问题。有了这个基础,就像第一部分一样,我向 FastAPI 发送了一个虚拟请求,目的仅仅是为了核对第一部分的数据。测试结果正如我所愿:相同的流失概率、相同的预测结果、相同的风险等级。因此我知道我的容器没有改变应用程序的行为方式。它的运行方式与在我机器上完全一致。所以,第一个检查点:完成。
Choosing AWS and standing up EC2: I picked EC2 again, just as with the rss-pipeline deployment. This is useful in maintaining consistency in my ideas about the architecture of projects; this project is a t3.micro instance using Ubuntu; this instance type is eligible for the free tier and is ideal for deploying a FastAPI application running on a single model. The setup process involved mostly navigating the options on the AWS console. I launched an instance and configured it with Ubuntu; I chose the t3.micro instance type and generated a key pair for the SSH connection. 选择 AWS 并搭建 EC2:我再次选择了 EC2,就像在 rss-pipeline 部署中一样。这有助于保持我对项目架构思路的一致性;本项目使用的是运行 Ubuntu 的 t3.micro 实例;该实例类型符合免费套餐资格,非常适合部署运行单个模型的 FastAPI 应用。设置过程主要是在 AWS 控制台上进行配置。我启动了一个实例并将其配置为 Ubuntu;我选择了 t3.micro 实例类型,并生成了一个用于 SSH 连接的密钥对。