My Model Worked Perfectly. Then I Tried to Make It Useful.
My Model Worked Perfectly. Then I Tried to Make It Useful.
我的模型运行完美,直到我试图让它变得“有用”
A few months ago, I tried to challenge myself to undertake a journey to transition from a data analytics background to data engineering. So far I’ve built a total of two impactful real-world projects that actually taught me something useful. I built a GitHub ETL pipeline that extracts GitHub repositories and loads them into a SQLite database — this ran on a schedule using GitHub Actions. I also built an RSS pipeline that extracts articles from RSS feeds and stores them into a Kestra database — orchestrated by Kestra to run on an hourly schedule.
几个月前,我挑战自己,开启了一段从数据分析背景转型为数据工程的旅程。到目前为止,我总共完成了两个具有实际影响力的项目,并从中获益良多。我构建了一个 GitHub ETL 流水线,用于提取 GitHub 仓库并将其加载到 SQLite 数据库中——该流水线通过 GitHub Actions 定时运行。我还构建了一个 RSS 流水线,用于从 RSS 源提取文章并存储到 Kestra 数据库中——由 Kestra 编排,每小时运行一次。
Now that I’ve understood ETL to a point, I wanted to try something new. I wanted to keep practicing all I’ve learned in the past whilst building something new. I had always been fascinated by the field of machine learning, but never actually had the courage to step in because I thought it had complex math. Not anymore. Recently, I built a churn prediction model for a fictional telecom company I am calling Northline Mobile (P.S. I’m using a fictional company because I understand things best with real-world scenarios). I provided it with data from 7043 customers, telling it whether they had signed up for a 1-year contract or month-to-month plans, length of customer, monthly charges, add-ons, etc. Furthermore, I told it who eventually left Northline Mobile. I cross-validated the model with customers it had not seen yet.
既然我已经对 ETL 有了一定了解,我想尝试一些新的东西。我希望在构建新项目的同时,继续练习过去所学的一切。我一直对机器学习领域很着迷,但从未有勇气涉足,因为我以为它涉及复杂的数学。现在不再是这样了。最近,我为一家名为 Northline Mobile 的虚构电信公司构建了一个流失预测模型(注:我使用虚构公司是因为我通过真实场景能更好地理解事物)。我向模型提供了 7043 名客户的数据,包括他们是签署了 1 年合约还是月度计划、客户时长、月费、附加服务等。此外,我还告诉了它哪些客户最终离开了 Northline Mobile。我使用模型未见过的数据对模型进行了交叉验证。
It achieved 81% accuracy. This taught me everything that goes into building a model. Obviously I didn’t understand all the complex code, because I prefer intuitive drag and drop interfaces rather than complex code. But I understood the essential building blocks of building a model; I’ll explain further below with a simplified architecture. So building this model felt like a win from a machine learning perspective; my model worked. But there was still one problem: it was still not really useful.
它的准确率达到了 81%。这让我了解了构建模型所需的一切。显然,我并没有理解所有复杂的代码,因为我更喜欢直观的拖拽式界面,而不是复杂的代码。但我理解了构建模型的基本要素;我将在下文通过一个简化的架构进一步解释。从机器学习的角度来看,构建这个模型感觉像是一次胜利;我的模型成功运行了。但仍然存在一个问题:它还不够“有用”。
Assuming a Northline employee who needed a prediction comes to me, I would have to open Jupyter, load the right notebook, run the cells in the correct order and make a manual call to predict_churn(). Yeah, the model exists, but I was the only one that knows how to use it. No one else at Northline could just send information on a customer to the model and retrieve a prediction and it could not speak to any other application either. This article will be covering this.
假设 Northline 的一名员工需要预测结果来找我,我必须打开 Jupyter,加载正确的笔记本,按正确顺序运行单元格,并手动调用 predict_churn() 函数。是的,模型确实存在,但只有我知道如何使用它。Northline 的其他人无法直接将客户信息发送给模型并获取预测结果,它也无法与其他任何应用程序进行交互。本文将探讨如何解决这个问题。
I recently learned that there is a difference between having a model and having a service. If a model just sits in a notebook only the person who built it can use it. But making it a service makes it possible for everyone to use it, other teams, apps, dashboards and systems that do not need to know or care how the prediction is made. It turns out building the machine learning model was the easiest part, but making it useful is another crucial element worth exploring.
我最近了解到,拥有一个模型和拥有一个服务是有区别的。如果模型只是放在笔记本里,那么只有构建它的人才能使用它。但将其转化为服务,就能让所有人——包括其他团队、应用程序、仪表板和系统——都能使用它,而无需了解或关心预测是如何做出的。事实证明,构建机器学习模型是最简单的部分,但使其变得有用是另一个值得探索的关键要素。
What “Done” Meant Before the API 在 API 出现之前,“完成”意味着什么
Here’s roughly what building the model looked like: That’s pretty much it. Nothing too fancy. By the end of that, I had a trained churn classifier, a preprocessing pipeline that cleaned and encoded the raw data, and evaluation numbers I was comfortable with (more on those numbers shortly, they’re not perfect and I’m not going to pretend they are). But like I said. Assuming Northline’s retention team builds a dashboard, and they want it to flag at-risk customers automatically. Their dashboard can’t reasonably open my Jupyter notebook and run my cells. It needs something else entirely. Something like this:
构建模型的过程大致如下:基本就是这样,没什么特别复杂的。到此为止,我拥有了一个训练好的流失分类器、一个用于清洗和编码原始数据的预处理流水线,以及我所满意的评估数据(稍后会详细说明这些数字,它们并不完美,我也不会假装它们完美)。但正如我所说,假设 Northline 的留存团队构建了一个仪表板,他们希望它能自动标记高风险客户。他们的仪表板不可能去打开我的 Jupyter 笔记本并运行我的单元格。它需要完全不同的东西,类似于这样:
That’s the shift this article covers. One quick disclaimer, though: this isn’t a FastAPI tutorial. FastAPI is simply the tool I happened to use to expose the model as a service. The interesting part, at least for me, was figuring out what that service should actually look like.
这就是本文要涵盖的转变。不过,先做一个简短的免责声明:这不是 FastAPI 教程。FastAPI 只是我恰好用来将模型作为服务公开的工具。对我而言,有趣的部分在于弄清楚这个服务到底应该是什么样子的。
The Boundary I Actually Needed 我真正需要的边界
The real question wasn’t “how do I put FastAPI around my model.” It was “what should the boundary between my software and my model actually look like.” I had two options. Full fidelity or something more simplified that only involves training the model on a handful of data. I settled on full fidelity: this means that the API accepts every raw field Northline’s other systems would realistically have about a customer, the same columns as the original dataset, not some simplified subset.
真正的问题不是“如何用 FastAPI 封装我的模型”,而是“我的软件和模型之间的边界应该是什么样的”。我有两个选择:全保真(Full fidelity)或仅涉及少量数据训练的简化方案。我选择了全保真:这意味着 API 接受 Northline 其他系统关于客户的所有原始字段,即与原始数据集相同的列,而不是简化的子集。
A request would typically look like this: 请求通常如下所示:
{
"gender": "Female",
"SeniorCitizen": 0,
"Partner": "Yes",
"Dependents": "No",
"tenure": 12,
"PhoneService": "Yes",
"MultipleLines": "No",
"InternetService": "Fiber optic",
"OnlineSecurity": "No",
"OnlineBackup": "Yes",
"DeviceProtection": "No",
"TechSupport": "No",
"StreamingTV": "Yes",
"StreamingMovies": "No",
"Contract": "Month-to-month",
"PaperlessBilling": "Yes",
"PaymentMethod": "Electronic check",
"MonthlyCharges": 75.5,
"TotalCharges": 890.5
}
And the response is deliberately small: 响应则特意保持简洁:
{
"churn_probability": 0.3136,
"prediction": 0,
"risk_level": "Medium"
}
I have to point out something real quick though. That risk_level field isn’t something the model produces. The model only outputs a raw probability. But a raw 0.31 isn’t something a retention rep can act on at a glance, so I added a simple bucket: below 0.3 is Low, 0.3 to 0.6 is Medium, above that is High. Those thresholds are a starting guess, not something I derived statistically, and I want to be upfront about that rather than pretend they’re more rigorous than they are. This is the boundary. Input schema, output schema, what’s required, what’s rejected. Once I’d actually thought this through, the endpoint itself was almost the easy part.
不过,我必须快速指出一点。那个 risk_level 字段并不是模型生成的。模型只输出原始概率。但 0.31 这样的原始数值并不是留存专员一眼就能采取行动的指标,所以我添加了一个简单的分级:低于 0.3 为“低”,0.3 到 0.6 为“中”,高于此为“高”。这些阈值只是初步猜测,并非通过统计推导得出,我希望坦诚这一点,而不是假装它们比实际更严谨。这就是边界:输入模式、输出模式、哪些是必需的、哪些是被拒绝的。一旦我想通了这一点,端点本身的实现几乎是最简单的部分。
Preparing the Model for Life Outside the Notebook 为笔记本之外的模型生命周期做准备
There’s a step between “request arrives” and “prediction comes back” that’s easy to underestimate: the raw JSON coming in looks nothing like what the model actually expects. Here’s what the journey typically looks like: It’s worth keeping in mind that the API can’t invent its own version of preprocessing. Whatever happened to the data during training has to happen, identically, at inference time.
在“请求到达”和“预测返回”之间有一个容易被低估的步骤:传入的原始 JSON 与模型实际期望的数据格式完全不同。这个过程通常如下:值得记住的是,API 不能发明自己的预处理版本。训练期间对数据所做的任何处理,在推理时必须完全相同地执行。