iT邦幫忙

2026 iThome 鐵人賽

DAY 9
0
AI Engineering

從 4,343 筆職缺到 AI Engineer:MLOps × GenAI Engineering 雙主軸實戰系列 第 9

Day 09:資料與特徵——DVC、資料驗證與訓練—服務偏斜

  • 分享至 

  • xImage
  •  

昨天的成熟度診斷中,其中「訓練前自動檢查資料」與「重現三個月前的訓練」應是最多團隊答「否」的兩題。今天就處理這兩題。

Day 06 說過可重現需要五根柱子,今天說明資料版本隨機種子這根支柱,也順便處理一個棘手的問題。

今天要解決的問題

「我照著三個月前的 commit 重跑,結果不一樣。」

程式碼一樣、套件版本一樣、seed 也固定了,為什麼結果不同?因為 data/train.csv 這個檔案在這三個月內被人更新過兩次,而 Git 裡沒有它的任何紀錄——它被 .gitignore 擋掉了(Day 05 我們才剛這樣做)。

「模型離線 AUC 0.91,上線後只有 0.78。」

這個更麻煩,因為它不會報錯。等到有人發現時,通常已經跑了幾個月。

這兩個問題,就是今天的主題。

📊 職缺訊號

「訓練管線與 CI/CD」出現在 42.6% 的 MLOps 職缺中,而資料驗證是這條管線的第一道關卡。實務上的比例更誇張:Day 06 那張圖顯示資料相關工作佔專案總工作量近三成,是模型程式碼的六倍。


一、現象:資料問題的三種樣貌

image

圖 9-1:資料問題的三種來源(示意流程)。問題 1、2 來自上游變動,問題 3 來自訓練與服務兩條路徑的實作差異。

  • 問題 1:schema 變更。 上游多了一個欄位、改了型別、或把 NULL 改成空字串。訓練時沒發現,因為 pandas 很包容。
  • 問題 2:分布漂移。 資料格式沒變,但內容變了(新市場、新活動、疫情)。這是 Day 14 的主題。
  • 問題 3:訓練—服務偏斜(Training-Serving Skew)。 同一個特徵,在訓練和線上用不同的程式碼算出來,於是算出不同的值。

第三個最陰險,因為它完全不會報錯。經典案例:訓練時「近 30 日消費總額」用完整月結資料算,線上取的卻是尚未結算的即時累計值——同一個特徵名,兩個意思。

二、原理:用 DVC 把資料納入版本控制

DVC 的設計很聰明:Git 管小檔、遠端儲存管大檔,兩者用雜湊對上。

image

圖 9-2:DVC 的運作方式(示意流程)。Git 只存指標檔,實際資料存在遠端;git checkoutdvc pull 就能還原任一時間點的「程式碼+資料」組合。

關鍵在於:.dvc 檔案(只有幾百 bytes,內含資料的 md5 雜湊)進 Git。所以當你 git checkout 到三個月前的 commit,再 dvc pull,拿到的就是當時那份資料

三、動手:資料版本 + 資料驗證

3.1 DVC 三分鐘上手

uv add dvc dvc-s3          # 或 dvc-gs / dvc-azure,依你的儲存而定
dvc init                    # 會建立 .dvc/ 並自動處理 .gitignore

# 把資料交給 DVC 管
dvc add data/train.csv      # 產生 data/train.csv.dvc,並把原檔加進 .gitignore
git add data/train.csv.dvc data/.gitignore
git commit -m "資料集 v1:2026-01 快照"

# 設定遠端儲存並推送
dvc remote add -d storage s3://my-bucket/dvc-store
dvc push

# ── 三個月後要重現當時的結果 ──
git checkout <當時的 commit>
dvc pull                    # 資料自動還原成當時那一份

💡 搭配 Git tag 使用

每次資料集有意義的更新就打 tag(git tag data-v2 && git push --tags),日後「用 data-v2 重跑」就是一句話的事。這比在檔名寫 train_final_v2_真的最終版.csv 可靠得多。

3.2 資料驗證:擋在訓練之前

驗證的重點不是「檢查所有東西」,而是把你的假設寫成程式碼。用 pandera 示範:

"""validate.py —— 訓練前的資料契約檢查。放在管線的第一步。"""
import pandera.pandas as pa
from pandera import Column, Check

schema = pa.DataFrameSchema({
    # 型別與值域:抓「上游改了型別」與「出現不可能的值」
    "user_id":      Column(int, nullable=False, unique=True),
    "age":          Column(int, Check.in_range(0, 120), nullable=True),
    "amount_30d":   Column(float, Check.ge(0), nullable=False),
    "channel":      Column(str, Check.isin(["web", "app", "store", "line"])),
    "label":        Column(int, Check.isin([0, 1])),
}, strict=True)          # strict=True:出現預期外的欄位也要報錯

def validate(df, ref_stats=None):
    df = schema.validate(df, lazy=True)        # lazy:一次回報所有錯誤,而非第一個

    # 契約之外,再加三個「統計層」的守門條件
    null_rate = df["amount_30d"].isna().mean()
    assert null_rate < 0.05, f"amount_30d 缺值率 {null_rate:.1%} 超過 5%"

    pos_rate = df["label"].mean()
    assert 0.01 < pos_rate < 0.5, f"正樣本比例 {pos_rate:.1%} 異常,疑似標籤產生錯誤"

    if ref_stats:   # 與上一版比較,抓「格式沒變但內容變了」
        drift = abs(df["amount_30d"].mean() - ref_stats["amount_30d_mean"])
        assert drift / ref_stats["amount_30d_mean"] < 0.3, "amount_30d 均值偏移超過 30%"
    return df

這段程式碼的價值不在語法,在於那三個 assert。 它們是把「我以為的資料長這樣」變成可執行的契約。每次線上事故後,就往這裡加一條——這份檔案會慢慢變成團隊對資料理解的活文件。

3.3 消滅訓練—服務偏斜:一份定義、兩處供應

根本解法是讓訓練與線上共用同一份特徵計算邏輯。最小可行做法不需要導入 Feature Store:

# features.py —— 訓練與線上服務都 import 這一個檔案,不准各寫一份
from datetime import timedelta

def amount_30d(txns, as_of):
    """近 30 日消費總額。

    as_of 一定要由呼叫端傳入——訓練時傳「當時的時間點」,線上傳「現在」。
    這個參數就是防止時間洩漏的關鍵:訓練時絕不能用 as_of 之後的交易。
    """
    window = txns[(txns.ts <= as_of) & (txns.ts > as_of - timedelta(days=30))]
    return float(window.amount.sum())

📌 重點在 as_of 這個參數

多數偏斜與時間洩漏,都源自「訓練時不小心用了未來的資料」。把時間點變成顯式參數,讓兩邊都必須明講「以什麼時間為準」,問題就消失一大半。這比導入任何工具都有效。

當團隊規模成長、特徵開始被多個模型共用時,才值得導入特徵存放區(Feature Store):

特徵存放區的線上/離線雙路徑

圖 9-3:特徵存放區的線上/離線雙路徑(示意架構)。核心價值是「一次定義、兩處供應」:離線路徑供訓練批次讀取、線上路徑供推論低延遲查詢,兩者由同一份定義產出。

四、取捨:什麼時候該用什麼

需求 最小方案 完整方案 什麼時候升級
資料版本 Git tag + 檔案雜湊記錄 DVC + 遠端儲存 資料超過 100MB 或需要多人協作
資料驗證 幾行 assert pandera / Great Expectations 資料來源超過 3 個或曾出過事故
特徵一致性 共用 features.py Feature Store(Feast 等) 特徵被 3 個以上模型共用
資料血緣 README 寫清楚 資料目錄工具 稽核或法遵有要求

4.1 台灣情境的兩個特別考量

其一:資料落地與內網限制。 半導體、製造與金融業的資料經常不允許離開內網,這讓「把資料放雲端物件儲存」的標準做法行不通。可行的替代是把 DVC 的遠端指向內部的 MinIO 或 NAS——DVC 對後端是抽象的,換一個 remote 設定就好,這也是我建議用 DVC 而非綁定特定雲端服務的原因之一。

其二:個資與去識別化的時間點。 很多團隊在訓練前才做去識別化,但這代表原始個資已經被複製到分析環境了。正確的做法是在資料進入分析環境之前就完成遮罩或假名化,讓分析環境從頭到尾都不持有可識別資料。這件事在 Day 27 談治理時會再細講,但要在建資料管線的第一天就決定,因為事後補救等於重建整條管線。

4.2 資料工作最容易被忽略的一件事:紀錄「為什麼」

技術上的版本控制解決「用了哪份資料」,但解決不了「為什麼當初決定這樣處理」。例如:為什麼把年齡超過 100 的樣本刪掉?為什麼某個通路的資料從三月起才納入?這些決策當下都有理由,三個月後全部忘記,於是下一個人要嘛不敢動,要嘛動了之後出事。

實務做法很簡單:在資料集的 .dvc 檔旁邊放一份 dataset-card.md,記錄資料來源、涵蓋期間、已知問題、處理決策與其理由。這份文件的維護成本一次不到十分鐘,但它是新人接手時唯一能救命的東西。

⚠️ 不要一開始就上 Feature Store

我看過團隊在只有一個模型時就導入完整的特徵平台,結果維護成本高於解決的問題。Feature Store 解決的是「多模型、多團隊共用特徵」的問題;只有一個模型時,一份共用的 features.py 就夠了。

反過來說,資料驗證從第一天就該有,因為它的成本只有幾行 assert,擋掉的卻是最常見的線上事故。


今日小結

  • 資料問題有三種:schema 變更分布漂移(Day 14)、訓練—服務偏斜。第三種最陰險,因為它不報錯。
  • DVC 的原理是「Git 管指標檔、遠端管大檔」,讓 git checkoutdvc pull 就能還原任一時間點的程式碼與資料組合。
  • 資料驗證的價值不在語法,在於把你對資料的假設寫成可執行的契約;每次事故後往裡面加一條。
  • 消滅偏斜的最小可行解不是導入工具,是共用同一份特徵計算程式碼,並把 as_of 時間點變成顯式參數。
  • 工具要分階段導入:資料驗證第一天就做,Feature Store 等到多模型共用時再說

明天預告

資料有版本了,明天輪到實驗與模型。我們會用 MLflow 把「這次訓練用了什麼參數、跑出什麼指標、產出哪個模型」全部記下來,並用模型註冊表的 champion/challenger 別名,做到換模型不用改程式碼、回滾只要一行指令

延伸閱讀

  • 📄 DVC 官方文件:Data Versioning——先看 dvc adddvc pull 兩節即可上手
  • 📄 Google, Data Validation for Machine Learning(TFX 團隊)——資料驗證的系統性方法,本文的三個 assert 是它的最小可行版本

上一篇
Day 08:MLOps 成熟度——你的團隊在 Level 幾?
下一篇
Day 10:實驗追蹤與模型註冊—MLflow 與 champion/challenger
系列文
從 4,343 筆職缺到 AI Engineer:MLOps × GenAI Engineering 雙主軸實戰10
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言