小Cの已经记不起来的博客

机器学习入门:热更新 数据预处理

最近在折腾一个小模型服务,模型热更新倒是能用了,结果数据预处理这块卡住了。线上字段加了一个类别,预处理代码里的映射是训练时 fit 出来的,服务重启一次还能忍,但每次改映射都重启就太蠢了。于是想着能不能热更新预处理。一开始以为 watch 一下 yaml 重新加载就完事,结果发现这玩意儿坑不少,可能是我没找对姿势吧。

先说结论:预处理热更新不是 reload 一下代码

数据预处理跟模型不一样。模型权重是参数,预处理里也有一半是“参数”:均值方差、缺失值填充值、类别映射、特征顺序、分箱边界。这些必须在训练集上 fit,推理时只能 transform。热更新时如果直接把新配置读进来,很容易出现特征顺序错、类别编码对不上、标准化用了新均值但模型还是旧模型,预测直接飘。所以第一件事不是写监听,是把预处理拆成“配置 + 固定变换代码”。配置里只放 transform 需要的参数,不放训练逻辑。

把预处理配置拆出来

我现在的做法是一个 yaml 管推理预处理,类似这样:

version: 20240521-1
model_version: yolo-nas-s-v3
features:
  - name: age
    type: numeric
    fill: 30
    mean: 35.2
    std: 8.1
  - name: city
    type: category
    fill: "__UNK__"
    mapping:
      beijing: 0
      shanghai: 1
      "__UNK__": 2

推理时只按这个文件做 transform,fit 的结果在训练脚本里导出,不在这里现算。这样热更新就是换配置,不碰训练数据。需要加字段就加字段,但注意模型输入维度别乱变,除非模型也一起换。

监听文件并原子切换

监听用 watchdog 就行,别自己写轮询。关键不是“文件变了就 reload”,而是“读完、校验、再切换”。我一开始直接在回调里 open 文件,结果编辑器保存时先清空再写入,读到半个 json,服务直接报错。后来改成先读临时变量,校验通过再赋值,并且用读写锁包一下。最小代码大概这样:

import json
import threading
import time
from watchdog.observers import Observer
from watchdog.events import FileSystemEventHandler


class PreprocessState:
    def __init__(self, path):
        self.path = path
        self.lock = threading.RLock()
        self.config = self._load()
        self.version = self.config["version"]

    def _load(self):
        with open(self.path, "r", encoding="utf-8") as f:
            cfg = json.load(f)
        assert "version" in cfg and "features" in cfg
        return cfg

    def reload(self):
        new_cfg = self._load()
        with self.lock:
            self.config = new_cfg
            self.version = new_cfg["version"]

    def transform(self, row):
        with self.lock:
            cfg = self.config
        out = []
        for feat in cfg["features"]:
            v = row.get(feat["name"], feat.get("fill"))
            if feat["type"] == "numeric":
                v = (float(v) - feat["mean"]) / feat["std"]
            else:
                v = feat["mapping"].get(v, feat["mapping"]["__UNK__"])
            out.append(v)
        return out


class Handler(FileSystemEventHandler):
    def __init__(self, state):
        self.state = state

    def on_modified(self, event):
        if event.src_path.endswith("preprocess.json"):
            time.sleep(0.2)
            self.state.reload()

这里 time.sleep(0.2) 很土,但能躲过一部分半写入。更稳的是写临时文件再 os.replace 原子替换,生产环境建议直接上配置中心,别跟文件系统较劲。

预处理代码本身怎么热更新

如果连 transform 逻辑都要热更新,那就不是改配置了,是改代码。Python 里 reload 模块听着简单,实际坑很多:旧对象还引用老函数、类定义不一致、线程里跑着旧逻辑。入门阶段我不建议一上来搞代码热更新,先把参数热更新做稳。真要改逻辑,用策略注册表,把每个预处理版本注册成函数,配置里写 pipeline: v2,切换时只换函数引用。新函数先离线跑一批样本对比,别直接上。

踩过的坑

第一个坑是版本绑定。模型文件和预处理配置得一起换,不然新模型用旧均值,loss 看着正常,线上指标就是不对。我现在在配置里写 model_version,加载时校验,不匹配就拒绝切换。第二个坑是缓存。有些服务会缓存 transform 结果,热更新后缓存没清,等于没更新。第三个坑是并发,一边读配置一边替换,轻则读到旧值,重则读到半截。锁不贵,别省。

怎么验证

热更新完别只看日志说“reload success”。拿几条固定样本,旧配置和新配置各跑一遍,看输出差异是不是符合预期;再拿模型做一次预测,确认输入维度、特征顺序没变。最后留一个回滚按钮,配置文件保留上一版,出问题切回去。机器学习入门阶段,热更新不是炫技,是让预处理别成为每次改字段都要重启的理由。先跑起来,再慢慢补监控和回滚,不然一开始就想搞大而全,基本会卡在配置上。

评论

还没有评论。

发表评论

提交后评论将经过自动审核,审核通过后公开展示。

未在播放