AI 互動教室 ‹ MLOps 自動化技術
下載 .py 開啟實戰 notebook ↗ 留言回報
MLFLOW · MODELS & REGISTRY · 02

MLflow Models 與 Model Registry:
從最好的 run 到線上那一版

上一課找到了最好的 run。但「最好的 run」離「線上那一版」還差三件事:模型要能被別人載入、要有大家都認得的名字與版本、 上線那一版要能一行切換。先玩最後這件:服務程式永遠載 models:/churn-clf@champion, 按晉升或回滾,看它拿到的模型與預測怎麼變——

@championchurn-clf · version 1LogisticRegression
roc_auc 0.951 · recall 0.860 · f1 0.885
@championchurn-clf · version 2RandomForest(depth 8)
roc_auc 0.968 · recall 0.913 · f1 0.920

機率與指標都是 notebook 的實測數字(同一組亂數種子)。notebook 裡的 alias 是真的 Registry: set_registered_model_alias 一行,服務端的載入程式一個字不改。

01 · MLFLOW MODEL

模型+規格+環境,一個資料夾

from mlflow.models import infer_signature signature = infer_signature(X_train, model.predict_proba(X_train)[:, 1]) # 輸入 12 欄 double → 輸出 double with mlflow.start_run(run_name="v1-logreg"): info = mlflow.sklearn.log_model(model, name="churn_model", signature=signature, input_example=X_train.head(3)) info.model_uri # models:/m-4052… (MLflow 3 的 LoggedModel,有自己的 id) info.flavors # ['python_function', 'sklearn']

log_model 不是存 pickle,而是產生一個資料夾:MLmodel 說明書(YAML:有哪些 flavor、signature、Python 與套件版本)、 模型本體(model.skops)、requirements.txtpython_env.yamlconda.yaml 重建環境用、 input_example.json 一筆範例輸入——實測資料夾裡共 8 個檔案,notebook 會把 MLmodel 印給你看。

flavor 是「可以用哪些方式載入」:sklearn flavor 載回原生物件(有 feature_importances_predict_proba); python_function(pyfunc)是統一介面——不管底層是 sklearn、PyTorch、XGBoost,都是 load_model(uri).predict(df)。 部署工具只認 pyfunc,所以每個 flavor 都附帶它。

到 notebook 的 1️⃣ 節:log_model、資料夾內容、MLmodel 說明書
02 · SIGNATURE

合約:載回來推論,餵錯資料會怎樣

pyfunc = mlflow.pyfunc.load_model(info.model_uri) pyfunc.predict(X_test.head(3)) # [1 0 1] pyfunc.predict(X_test.head(3).drop(columns=["f11"])) # ✗ Model is missing inputs ['f11']. pyfunc.predict(X_test.head(3).assign(f1=["a","b","c"])) # ✗ Failed to convert column f1 from type object to DataType.double. pyfunc.predict(X_test.head(3).assign(extra=1.0)) # 多一欄:靜靜忽略

有 signature,MLflow 在呼叫模型之前就把關(schema enforcement):少一欄、型別錯都直接拒絕並說清楚是哪欄;多一欄則忽略。 沒有 signature 的模型什麼都吃,錯誤延後到 scikit-learn 內部才爆,訊息難懂得多。 所以 log_model 一定要給 signature——它是模型與呼叫端之間的合約。

到 notebook 的 2️⃣ 節:三種錯誤輸入的實際反應
03 · REGISTRY

名字、版本、alias:晉升與回滾各一行

mv1 = mlflow.register_model(v1_info.model_uri, "churn-clf") # → version 1 mv2 = mlflow.register_model(v2_info.model_uri, "churn-clf") # → version 2 client = MlflowClient() client.set_registered_model_alias("churn-clf", "champion", mv1.version) # 線上:v1 client.set_registered_model_alias("churn-clf", "challenger", mv2.version) # 候選:v2 model = mlflow.pyfunc.load_model("models:/churn-clf@champion") # 服務端永遠這一行 client.set_registered_model_alias("churn-clf", "champion", mv2.version) # 晉升(回滾就指回 1)

Registry 是「有名字的模型」的目錄:一個 registered model(churn-clf)底下多個 version,每個 version 指向一個 LoggedModel, 可以掛 description 與 tag(例如 validated=true)。alias 是貼在 version 上的可移動標籤,一個 alias 同時只指一個 version; 指到不存在的 alias 會報 Registered model alias nope not found. 舊版的 stage(Staging/Production)已 deprecated,現在用 alias,名字自己取。

實測:v1 LogisticRegression AUC 0.9508、v2 RandomForest AUC 0.9684。晉升前後同一行載入程式拿到的 run id 不同、預測也換成 v2 的—— 注意 Registry 需要資料庫後端(sqlite 或 server);MLflow 3.15 起純資料夾模式 ./mlruns 已進維護模式,預設直接報錯。

到 notebook 的 3️⃣ 節:註冊、alias、晉升、壞 alias
04 · EVALUATE

晉升不憑感覺:一行評估,兩版並排

with mlflow.start_run(run_name="eval-v2-rf"): res = mlflow.models.evaluate(v2_info.model_uri, eval_df, targets="label", model_type="classifier") res.metrics["roc_auc"] # 0.968 res.artifacts # roc_curve_plot, precision_recall_curve_plot, lift_curve_plot, calibration_curve_plot, confusion_matrix
metric(同一份 test)v1 logregv2 rf
accuracy0.8820.916
precision / recall0.912 / 0.8600.927 / 0.913
f10.8850.920
log_loss0.2880.270
roc_auc / pr_auc0.951 / 0.9560.968 / 0.975

指標與 5 張圖全部記進當前 run,混淆矩陣可以直接讀回來畫。第 5 課會把「roc_auc 必須高於目前 champion」變成管線裡的自動閘門。

到 notebook 的 4️⃣ 節:evaluate 兩個版本、讀回混淆矩陣
05 · 自訂 PYFUNC 與資料版本

把前後處理跟模型包在一起;讓 run 記得用哪份資料

class ChurnWrapper(mlflow.pyfunc.PythonModel): def load_context(self, context): # 載入時讀回打包的模型檔 self.model = pickle.load(open(context.artifacts["sk_model"], "rb")) def predict(self, context, model_input, params=None): # 前處理 → 模型 → 後處理 thr = (params or {}).get("threshold", 0.5) proba = self.model.predict_proba(model_input.fillna(0.0))[:, 1] return pd.DataFrame({"prob": proba, "churn": (proba >= thr).astype(int)}) mlflow.pyfunc.log_model(name="churn_model", python_model=ChurnWrapper(), artifacts={"sk_model": "rf.pkl"}, signature=sig_with_params) wrapper.predict(X, params={"threshold": 0.9}) # 門檻由呼叫端決定

真實模型很少「餵 DataFrame 就出機率」:前面要清資料、後面要用門檻轉成決策。這些邏輯散在服務程式裡,換模型就對不上。 繼承 PythonModel 把它們包進同一個部署單位;signature 的 params 段宣告可調參數。 實測同樣 4 筆客戶:門檻 0.5 判流失 2 筆,threshold=0.9 剩 0 筆(機率 0.746/0.071/0.843/0.174)。

資料版本:mlflow.data.from_pandas(df, name=..., targets=...) 自動算內容指紋(digest), mlflow.log_input(dataset, context="training") 記進 run——兩個 run 指標不同時,先看 digest 是不是變了。

到 notebook 的 5️⃣–7️⃣ 節:ChurnWrapper、log_input、切版本拉門檻
06 · 實戰

換你動手

LEVEL 1

mlflow.sklearn.load_model("models:/churn-clf/2") 以原生 flavor 載回 v2,印出 feature_importances_ 最高的三個特徵——pyfunc 做不到,想想為什麼還需要它。

LEVEL 2

訓一個 GradientBoosting 當 v3 註冊,evaluate 之後寫「自動晉升」:只有 roc_auc 高於目前 champion 才移 alias,否則貼 rejected=true

LEVEL 3

ChurnWrapper 改成能吃缺欄位、多欄位、亂序的輸入,仍保有 signature。驗證:三種輸入的 prob 都跟原始輸入一致。

卡住了?每一題在 notebook 末節都有折疊解答——先自己做,再打開對照。

07 · 驗收

情境測驗

離開前試試看:下面的情境都真的會遇到。每題選一個你認為的最佳做法,選了馬上看得到解釋。

Q1 情境題

線上服務程式寫死 load_model("models:/churn-clf/2")。現在 v3 驗證通過要上線,而且以後每週都會有新版。最佳做法是?

alias 就是為了這件事存在:服務端永遠載同一個 URI,「線上是哪一版」由 Registry 裡的一個可移動標籤決定,晉升與回滾都是一行、不重新部署。A 能動但每次都要改程式碼+部署,回滾也一樣慢;C 是舊做法,stage 已標記 deprecated,而且只有固定的幾個名字;D 是災難:版本 2 的內容被偷換,紀錄與實際不符,永遠回不去。

Q2 錯誤診斷

模型上線第一天,服務端呼叫 pyfunc.predict(df) 就炸。最可能的原因與正確修法?

MlflowException: Failed to enforce schema of data '...' with schema '['f0': double (required), ..., 'f11': double (required)]'. Error: Model is missing inputs ['f11'].

訊息說得很清楚:模型要 12 欄、輸入缺 f11。signature 在這裡發揮了它的功能——在進到模型之前就擋下,並指名缺哪欄。A 只是把錯誤延後到 scikit-learn 內部(會變成一個難懂的 shape 錯誤,甚至默默算錯);B 症狀不符,版本錯不會產生 missing inputs;D 跟 A 一樣是拆掉安全帶,且 RandomForest 對欄位順序敏感,少一欄照樣炸或算錯。

Q3 情境題

業務說「機率 ≥ 0.7 才算高風險客戶,而且這個數字每季會調」。你要把這條規則跟模型一起交付。最佳做法是?

門檻是業務規則,不是模型的一部分,但又必須跟模型一起版本化與交付——自訂 pyfunc 正是為此:模型與後處理同一個部署單位,params 讓呼叫端調整而不改包裝(notebook 實測 0.5 → 0.9,判流失從 2 筆變 0 筆)。B 把可調的規則烙進模型,每季要重訓;C 能動但規則與模型分家,換模型時容易對不上、改一次要部署一次;D 想法對但用錯地方——tag 是中繼資料,服務端每次推論去查 Registry 既慢又脆弱。

Q4 錯誤診斷

同事照舊教學寫 mlflow.set_tracking_uri("./mlruns"),第一個 run 就炸出下面這段。怎麼修最對?

MlflowException: The filesystem tracking backend (e.g., './mlruns') is in maintenance mode and will not receive further updates. Please migrate to a database backend (e.g., 'sqlite:///mlflow.db') ... set `MLFLOW_ALLOW_FILE_STORE=true` to opt out of this exception.

訊息本身就給了答案:檔案後端進入維護模式,請改用資料庫後端。SQLite 是一個檔案、零安裝,跟資料夾模式一樣方便,卻多了 Model Registry(純檔案模式從來不支援註冊與 alias)。A 讀錯訊息,這不是權限錯誤;B 為了舊教學鎖死版本,之後所有新功能都用不到;C 是訊息提供的暫時逃生口——課堂上救急可以,但它明說「不再更新」,而且照樣沒有 Registry。

Q5 情境題

同一份訓練程式、同樣的 params、同樣的隨機種子,上週的 run AUC 0.968,今天重跑只有 0.951。第一件該查的事是?

重現 = 同樣的程式+設定+資料。程式與設定都有紀錄且相同,剩下的變數就是資料;log_input 記的 digest 是內容指紋,任何一格改了指紋就不同——先看它,一秒排除或確認。A 種子相同的話 sklearn 是決定性的,平均不會揭露原因;C 在沒搞清楚原因前換模型,只是把問題藏起來;D 逃避問題,如果資料真的變了(例如上游欄位定義改了),線上那版可能也已經不適用。

HANDS-ON · MOLAB

實作在 molab 跑(免費)

molab 的登入狀態進不了內嵌框架(瀏覽器的跨站 cookie 保護), 所以 notebook 要在新分頁執行——把它跟本頁並排開,左邊教學照樣對照。

  1. 登入 molab(GitHub / Google)
  2. 開啟課程 notebook,Fork 成自己的副本即可編輯
  3. 從第一格往下全部執行(首次安裝套件約 1–2 分鐘)——免費 CPU 環境即可,不需要 GPU;Registry 就是本機一個 SQLite 檔

不想用 molab?下載 mlflow-registry_ext.py 後在自己電腦 uvx marimo edit --sandbox mlflow-registry_ext.py,依賴會自動安裝。

molab 的線上編輯器在手機上體驗有限——動手這一段建議用電腦進行。