配置对象把所有「会变的东西」集中到一处,路径是第一号。 依赖注入听起来像术语,其实就一句:要用的东西从参数进来,不要从外面抓。 scaler 必须跟模型一起存——只存模型不存 mu、sd,是这类项目最常见的静默错误。
这一篇对应教材 17.5.3:重新構建。 上一篇把脚本拆成了六个函数,这一篇把它们搬进包,并解决一个会直接导致部署失败的问题: 写死的路径。
一、包的形状
mlapi/
__init__.py create_app
config.py 配置对象
data.py 读文件、切分
features.py 标准化
model.py 训练、存盘、加载、预测
views/predict.py 路由
wsgi.py 入口
和 17.2 那张目录图一样,只是这次真造出来了。包的形状定了,填肉的时候才不会乱。
data.py 和 features.py 这一篇先没拆开——读文件和标准化加起来不到三十行,
拆成两个文件反而要来回跳。拆不拆看体量,不看「应该有这个文件」。
二、配置对象:会变的东西集中到一处
from dataclasses import dataclass
@dataclass
class Config:
data_path: str = "data.csv"
model_path: str = "model.pkl"
test_ratio: float = 0.7
alpha: float = 1.0
n_features: int = 10
五个配置项:data_path、model_path、test_ratio、alpha、n_features。
data_path 这条是关键。上一篇那个脚本把 data.csv 写死在代码里,
这里改成「从一个东西里取」——那个东西可以是默认值、环境变量、或者测试时传进来的字典。
用 dataclass 而不是字典,好处是拼错字段名会立刻报错:cfg.datp_path 会在启动时炸,
而 cfg["datp_path"] 要到运行时才 KeyError。
再加一个 from_env() 类方法,就能让「线上改配置」变成改环境变量,不用改代码、不用重新构建镜像。
第 15 章那个 Dockerfile 里的 ENV 就是干这个的。
三、依赖注入:从参数进来,不要从外面抓
def train(cfg):
X, y = load_csv(cfg.data_path)
Xtr, ytr, Xte, yte = split(X, y, cfg.test_ratio)
Xtr, Xte, mu, sd = scale(Xtr, Xte)
model = Ridge(alpha=cfg.alpha).fit(Xtr, ytr)
...
with open(cfg.model_path, "wb") as f:
pickle.dump(bundle, f)
注意 train(cfg):路径从 cfg 里取,函数自己不知道文件在哪。
这叫依赖注入。
听起来像术语,其实就一句话:要用的东西从参数进来,不要从外面抓。「从外面抓」指的是 global、
模块级变量、或者直接读 os.environ。
好处有三个,都不抽象:测试时能传临时路径、同一进程能跑两份配置、换个环境不用改代码。
第三条就是能上线的门槛。
还有一个细节值得看:predict 里把 mu、sd 跟模型一起存进 bundle。
def predict(bundle, feats):
z = (np.asarray(feats, dtype=float) - bundle["mu"]) / bundle["sd"]
return float(bundle["model"].predict(z.reshape(1, -1))[0])
只存模型不存 scaler,是这类项目最常见的一处静默错误。预测的时候忘了标准化,或者用了另一套 mu、 sd,结果是错的,但一个错都不报——这正是最难查的那类 bug。
四、真跑一条链:训练、存盘、加载、预测
| 步骤 | 实测 |
|---|---|
| 训练 | 训练 309 条、测试 133 条,RMSE 56.7371 |
| 存盘 | 模型文件 0.8 KB |
| 加载 | 读回来 RMSE 字段还是 56.7371 |
| 预测第一条 | 205.30(真值 151.0) |
这条链就是「重新构建」要的东西:训练和预测是两个入口,共用一份配置和一份模型文件。
服务启动时只 load 不 train——训练是离线的事,几秒钟能跑完;预测是线上每秒几十次的事。
这两件事的触发时机完全不同,代码也就该分开。
预测第一条差得挺远(205 对 151),这是正常的:
RMSE 56 意味着单条预测浮动几十很常见。模型给的是「平均来看偏多少」,不是「这一条错多少」。
部署之后如果发现某一条预测离谱,先别怀疑模型——看它的特征是不是离群。 下一篇那个「数值超范围」的校验就是为这个准备的。
五、换目录、换文件,代码零改动
| 场景 | 数据量 | 结果 |
|---|---|---|
| 原来的目录 | 442 条 | 训练 309 条,RMSE 56.7371 |
| 另一个目录 + 新文件 | 300 条 | 训练 210 条,RMSE 56.0250 |
| 代码改动行数 | — | 0 |
上一篇那个脚本做不到这件事:它 open("data.csv"),换目录就找不到文件。
「换个环境就跑不了」的原因,十次里有八次是硬编码的路径。剩下两次通常是写死的端口和写死的密钥。
配置从外面给还有个副作用:测试变容易了。测的时候传一个临时的 Config,
不用去动真实的配置文件——第 11 章那些 fixture 就是这个套路。
六、什么变了,什么没变
| 东西 | 变了没有 | 为什么 |
|---|---|---|
| 算法 | 没变 | 还是 Ridge,alpha 也一样 |
| 函数体 | 基本没变 | 拆出来的逻辑原样搬过来 |
| 路径 | 变了 | 从写死改成从 cfg 取 |
| 输出 | 变了 | print 改成 return 字典 |
| 入口 | 变了 | train 与 predict 分开 |
教材 17.5.3 讲「重新構建」,难点不在写得出来,在忍住不乱改。 看到一段代码觉得别扭,顺手改了,重构就变成了「重构加改需求」,出了问题查不出来。 这一条上一节刚说过,因为它是这一章最常见的翻车方式。 判断标准很朴素:这一次改动,能不能用一句话说清?「把路径参数化」是一句话; 「把路径参数化顺便把算法换成随机森林」就不是了。
七、这一篇之后
现在的 mlapi 能训练、能预测、能换配置,但外面还调不到。
下一篇补上最后两块:schema.py 和 views/predict.py,把 17.2 那张契约变成真接口。
那条线到这儿已经很清楚:脚本、函数、包、服务。每一步只做一件事,每一步都能单独验证。
八、动手 Demo:重新构建:把函数搬进包,把路径从代码里赶出去
这个 Demo 真造一个 mlapi 包,用配置对象把路径参数化,然后跑完整的训练、存盘、加载、预测。 第五节换个目录、换个文件再跑一次,代码零改动——这是这一篇最想证明的一件事。
@dataclass
class Config:
data_path: str = "data.csv"
model_path: str = "model.pkl"
test_ratio: float = 0.7
alpha: float = 1.0
n_features: int = 10
@classmethod
def from_env(cls):
return cls(data_path=os.environ.get("DATA_PATH", "data.csv"))
实跑输出:
--------------------------------------------------------------------
重新构建:把函数搬进包,把路径从代码里赶出去
--------------------------------------------------------------------
① 重建后的样子
--------------------------------------------------------------------
mlapi/
├── __init__.py
├── config.py
├── data.py
├── features.py
├── model.py
└── views/
├── __init__.py
└── predict.py
! 和上一篇那张目录图一样,只是这次真造出来了。包的形状定了,填肉的时候才不会乱。
② 配置对象:所有「会变的东西」集中到一处
--------------------------------------------------------------------
配置项 5 个:data_path、model_path、test_ratio、alpha、rows_expected
! data_path 这条是关键。
上一篇那个脚本把 data.csv 写死在代码里,这里改成「从一个东西里取」——那个东西可以是默认值、
环境变量、或者测试时传进来的字典。
! from_env 这个方法让「线上改配置」变成改环境变量,不用改代码、不用重新构建镜像。
第 15 章那个 Dockerfile 里的 ENV 就是干这个的。
③ 依赖注入:把 config 传进去,不 import 全局
--------------------------------------------------------------------
! 注意 train(cfg):路径从 cfg 里取,函数自己不知道文件在哪。
这叫依赖注入,听起来像术语,其实就一句话:要用的东西从参数进来,不要从外面抓。
! predict 里把 mu、sd 跟模型一起存下来,是为了保证训练和预测用同一套缩放参数。
只存模型不存 scaler,是这类项目最常见的一处静默错误——预测结果不对,但一个错都不报。
④ 真跑一条链:训练、存盘、加载、预测
--------------------------------------------------------------------
训练完 -> 训练 309 条,测试 133 条,RMSE 56.7371
存盘 -> 0.8 KB
读回来 -> RMSE 字段还是 56.7371
预测第一条 -> 205.30(真值 151.0)
! 这条链就是第 17.5.3 讲「重新构建」要的东西:训练和预测是两个入口,共用一份配置和一份模型文件。
! 服务启动时只 load 不 train——训练是离线的事,几秒钟能跑完;预测是线上每秒几十次的事。
这两件事的触发时机完全不同,代码也就该分开。
⑤ 换个目录、换个文件,代码一个字不改
--------------------------------------------------------------------
另一个目录、300 条数据 -> 训练 210 条,RMSE 56.0250
代码改动行数 -> 0
! 上一篇那个脚本做不到这件事:它 open("data.csv"),换目录就找不到文件。
「换个环境就跑不了」的原因,十次里有八次是硬编码的路径。
! 配置从外面给,还有个副作用是测试变容易了:测的时候传一个临时的 Config,
不用去动真实的配置文件。第 11 章那些 fixture 就是这个套路。
⑥ 重新构建:什么变了,什么没变
--------------------------------------------------------------------
| 东西 | 变了没有 | 为什么 |
|---|---|---|
| 算法 | 没变 | 还是 Ridge,alpha 也一样 |
| 函数体 | 基本没变 | 拆出来的逻辑原样搬过来 |
| 路径 | 变了 | 从写死改成从 `cfg` 取 |
| 输出 | 变了 | print 改成 return 字典 |
| 入口 | 变了 | `train` 与 `predict` 分开 |
! 教材 17.5.3 讲「重新构建」,难点不在写得出来,在忍住不乱改。
看到一段代码觉得别扭,顺手改了,重构就变成了「重构加改需求」,出了问题查不出来。
! 判断标准很朴素:这一次改动,能不能用一句话说清?「把路径参数化」是一句话;
「把路径参数化顺便把算法换成随机森林」就不是了。
⑦ 还剩最后一块:把它接成 API
--------------------------------------------------------------------
现在的 mlapi 能训练、能预测,但外面调不到
下一篇补上 schema.py 和 views/predict.py,把契约变成真接口
! 三篇下来,这条线是清楚的:脚本 -> 函数 -> 包 -> 服务。每一步都只做一件事,每一步都能单独验证。
! 这也是第 17 章想说的:机器学习项目里,模型训练那部分代码最少,剩下全是为了让它能被人用。
第四节那个 205.30 对 151.0 单条差得远,正常——RMSE 56 说的是整体平均。 第五节换了目录和文件,训练 210 条、RMSE 56.0250,代码一行没改。 第六节那张两列对照,是这一篇的验收标准:只该变该变的。
30 秒实验
- 给
Config加一个from_env(),用环境变量改DATA_PATH再跑一次。 - 把
mu、sd从bundle里删掉,看预测结果会错成什么样。 - 想一想:为什么依赖注入能让测试变容易?
常见误区
- 路径写死在代码里:换目录、进容器立刻 FileNotFoundError。
- 只存模型不存 scaler:预测结果错,但一个错都不报。
- 重构时顺手改算法:出问题分不清是哪一步引起的。
- 启动时训练模型:冷启动从几十毫秒变成几秒。
- 配置用字典:拼错键名要到运行时才炸。
自测三问(点开对答案)
1. 依赖注入一句话是什么? —— 要用的东西从参数进来,不要从外面抓(全局变量、模块级变量、环境变量)。 2. 为什么 mu、sd 要跟模型一起存? —— 保证训练和预测用同一套缩放参数;只存模型会导致预测错但不报错。 3. 「重新构建」最容易犯的错是什么? —— 顺手改需求。判断标准是这次改动能不能用一句话说清。最后一步:把这些接成真接口,加上校验、日志,然后回头看整本书。
- ☐ (本篇完,下一篇 17.5 建 API:路由、错误码、请求校验与 MLOps)