MLOps 3 — Feature Store
MLOps 3 — Feature Store
Ở bài 2 — Theo dõi thí nghiệm & tái lập ta đã ghim được code, params, môi trường của một mô hình. Nhưng còn một biến số nữa quyết định kết quả mà chưa được xử lý triệt để: feature — chính những con số đầu vào mà mô hình ăn. Feature được tính bằng SQL/Spark rải rác trong nhiều pipeline, mỗi team một kiểu, và tệ nhất là feature lúc train khác feature lúc serve. Đây là đất sống của Feature Store — hạ tầng chuyên trách quản lý vòng đời của feature.
Ba nỗi đau Feature Store giải quyết
1. Training-serving skew (lệch huấn luyện–phục vụ)
Đây là lỗi âm thầm nhưng chết người nhất. Lúc train, data scientist tính feature avg_txn_amount_30d (giá trị giao dịch trung bình 30 ngày) bằng một query SQL trên warehouse, chạy batch qua đêm. Lúc serve (chấm điểm real-time khi khách quẹt thẻ), engineer viết lại logic đó bằng Java trong service online — và viết hơi khác: một bên tính "30 ngày gần nhất tính đến nửa đêm hôm qua", một bên tính "30 ngày tính đến đúng lúc giao dịch"; một bên loại giao dịch hoàn tiền (kind = 'refund'), một bên quên loại.
Kết quả: cùng một khách hàng, feature lúc train và lúc serve ra hai con số khác nhau. Mô hình được học trên phân phối A nhưng chạy trên phân phối B → skew. AUC offline đẹp 0.85 nhưng online rơi xuống 0.72 mà không ai hiểu tại sao, vì bug không nằm ở mô hình mà nằm ở việc feature được định nghĩa hai lần bằng hai đoạn code khác nhau.
Feature Store chữa gốc: feature được định nghĩa MỘT lần, và cả đường train lẫn đường serve đều lấy từ cùng một định nghĩa đó. Không còn hai bản logic để lệch.
2. Trùng lặp công sức
Trong một ngân hàng, so_du_trung_binh_90d, so_giao_dich_thang, tuoi_tai_khoan được đội fraud tính, đội credit tính lại, đội marketing tính lần thứ ba — mỗi đội một query, một tên cột, một chỗ lưu. Không ai biết đội kia đã có. Feature giá trị cao (đắt để tính, khó làm đúng) bị làm đi làm lại. Feature Store cung cấp một registry (sổ đăng ký) để feature được publish, tìm kiếm và tái sử dụng — tính một lần, cả tổ chức dùng.
3. Point-in-time correctness (rò rỉ nhãn tương lai)
Đây là cái bẫy tinh vi nhất khi tạo tập train. Giả sử ta gán nhãn: giao dịch T xảy ra lúc 2026-03-15 10:00 là gian lận hay không. Để train, ta cần feature của tài khoản đó tại đúng thời điểm 10:00 ngày 15/03 — ví dụ "tổng giao dịch 30 ngày trước 10:00 ngày 15/03".
Nếu ta lười và tính feature theo giá trị hiện tại (hôm nay), ta vô tình nhét vào feature các giao dịch xảy ra sau thời điểm gán nhãn — kể cả chính giao dịch gian lận đó và hậu quả của nó. Đó là rò rỉ nhãn tương lai (label leakage / lookahead): mô hình "nhìn trộm tương lai". Offline nó đạt AUC 0.97 tuyệt vời, nhưng online sập thảm hại vì lúc serve không có tương lai để nhìn.
Cách làm đúng gọi là as-of join (join theo mốc thời gian): với mỗi dòng nhãn có timestamp event_ts, lấy giá trị feature đúng như nó tồn tại tại event_ts, không dùng bất kỳ dữ liệu nào có timestamp lớn hơn. Feature Store làm việc này tự động khi bạn gọi get_historical_features — đây chính là giá trị lớn nhất của nó.
Feature Store là gì
Là một hệ thống có bốn thành phần cốt lõi:
| Thành phần | Vai trò | Công nghệ điển hình |
|---|---|---|
| Feature Registry | Định nghĩa & metadata: feature tên gì, thuộc entity nào, kiểu dữ liệu, ai sở hữu, nguồn dữ liệu | Metadata store (Postgres/file) |
| Offline Store | Dữ liệu feature lịch sử (có timestamp), dùng cho training & backfill | Warehouse: BigQuery, Snowflake, S3/Parquet |
| Online Store | Giá trị feature mới nhất, độ trễ thấp (< vài ms), dùng cho serving real-time | Redis, DynamoDB, Cassandra |
| Materialization + Retrieval | Đẩy dữ liệu offline→online; API lấy feature cho train và cho serve | Feast, Tecton, Hopsworks |
Hai kho — offline và online — là điểm mấu chốt kiến trúc. Chúng có cùng định nghĩa feature nhưng khác nhau về mục đích:
- Offline store lưu toàn bộ lịch sử có gắn timestamp, tối ưu cho quét lớn (đọc hàng triệu dòng để tạo tập train). Chấp nhận độ trễ cao.
- Online store chỉ lưu giá trị mới nhất mỗi entity (ví dụ: account 123 →
avg_txn_30d = 4.2tr), là key-value tra cứu bằng entity key, độ trễ mili-giây. Đây thường là một kho NoSQL như Redis vì mẫu truy cập là point-lookup.
Materialization là quá trình định kỳ (hoặc streaming) tính feature từ nguồn, ghi giá trị mới nhất vào online store để serving luôn có dữ liệu tươi. Retrieval có hai đường:
get_historical_features(entity_df, features)— dùng lúc train, chạy as-of join trên offline store, đảm bảo point-in-time.get_online_features(entity_rows, features)— dùng lúc serve, tra cứu online store bằng entity key, trả về vector feature trong vài ms.
Vì cả hai đường đọc từ cùng một định nghĩa feature → triệt tiêu training-serving skew về mặt định nghĩa.
Kiến trúc
Chú ý: mũi tên từ Registry chạy tới cả hai kho — đó là hiện thân của nguyên tắc "định nghĩa một lần". Đường train ăn offline, đường serve ăn online, nhưng feature là một.
Feast — ví dụ cụ thể
Feast là feature store mã nguồn mở phổ biến, nhẹ, phù hợp minh hoạ. Ba khái niệm chính (đây là code Python minh hoạ, không phải SQL chạy được):
Entity — thực thể mà feature gắn vào, có join key:
account = Entity(name="account", join_keys=["account_id"])
Feature View — một nhóm feature cùng nguồn, cùng entity, có timestamp:
account_txn_stats = FeatureView(
name="account_txn_stats",
entities=[account],
ttl=timedelta(days=90),
schema=[
Field(name="txn_sum_30d", dtype=Float64),
Field(name="txn_count_30d", dtype=Int64),
Field(name="balance", dtype=Float64),
],
source=txn_source, # trỏ vào offline store (bảng/parquet có event_timestamp)
)
Feature Service — gói các feature một mô hình cần dùng, để train và serve tham chiếu cùng một danh sách:
fraud_v1 = FeatureService(
name="fraud_scoring_v1",
features=[account_txn_stats[["txn_sum_30d", "txn_count_30d", "balance"]]],
)
Lúc train:
training_df = store.get_historical_features(
entity_df=labels_df, # có cột account_id + event_timestamp + nhãn
features=fraud_v1,
).to_df()
Feast tự as-of join: mỗi dòng trong labels_df chỉ được ghép feature có timestamp <= event_timestamp của dòng đó. Không rò rỉ.
Lúc serve:
features = store.get_online_features(
entity_rows=[{"account_id": 123}],
features=fraud_v1,
).to_dict() # trả về txn_sum_30d, txn_count_30d, balance mới nhất — vài ms
Cùng fraud_v1, cùng danh sách feature, hai đường khác nhau. ttl=90 days nghĩa là feature quá 90 ngày bị coi là hết hạn (không dùng), phục vụ feature freshness.
Point-in-time bằng SQL: as-of feature engineering
Để hiểu điều Feature Store làm bên dưới, ta viết tay logic tính feature "tổng và số giao dịch 30 ngày của mỗi account tính đến một thời điểm mốc X" — chính là feature dùng chung cho mô hình fraud/credit. Sandbox là PostgreSQL read-only với các bảng transactions(id, account_id, amount, kind, created_at) và accounts(id, customer_id, account_no, balance, currency).
Query 1 — Feature 30 ngày tính đến mốc X (giả lập offline as-of): với mốc X = 2025-06-15, chỉ lấy giao dịch trong cửa sổ [X-30 ngày, X), tuyệt đối không dùng giao dịch sau X (không nhìn tương lai):
-- ▶ Chạy được
SELECT
a.id AS account_id,
a.account_no,
COUNT(t.id) AS txn_count_30d,
ROUND(COALESCE(SUM(t.amount), 0)::numeric, 2) AS txn_sum_30d,
ROUND(COALESCE(AVG(t.amount), 0)::numeric, 2) AS txn_avg_30d
FROM accounts a
LEFT JOIN transactions t
ON t.account_id = a.id
AND t.created_at >= TIMESTAMP '2025-06-15' - INTERVAL '30 days'
AND t.created_at < TIMESTAMP '2025-06-15'
GROUP BY a.id, a.account_no
ORDER BY txn_sum_30d DESC
LIMIT 20;
Điểm cốt lõi là điều kiện t.created_at < TIMESTAMP '2025-06-15': nó chặn dữ liệu tương lai. Nếu tạo tập train cho nhiều account với nhiều mốc X khác nhau (mỗi dòng nhãn một mốc), ta thay hằng số bằng cột event_ts của từng dòng nhãn — đó chính là as-of join mà Feature Store tự động hoá.
Query 2 — Feature "as-of" đúng nghĩa với nhiều mốc thời gian khác nhau cho mỗi account: giả lập labels bằng CTE (mỗi account có một event_ts riêng), rồi tính tổng giao dịch 30 ngày trước event_ts của chính dòng đó. Đây là bản SQL của get_historical_features:
-- ▶ Chạy được
WITH labels AS (
SELECT DISTINCT ON (account_id)
account_id,
created_at AS event_ts
FROM transactions
ORDER BY account_id, created_at DESC -- mỗi account lấy giao dịch mới nhất làm "sự kiện cần chấm điểm"
)
SELECT
l.account_id,
l.event_ts,
COUNT(t.id) AS txn_count_30d_asof,
ROUND(COALESCE(SUM(t.amount), 0)::numeric, 2) AS txn_sum_30d_asof
FROM labels l
LEFT JOIN transactions t
ON t.account_id = l.account_id
AND t.created_at >= l.event_ts - INTERVAL '30 days'
AND t.created_at < l.event_ts -- chỉ dữ liệu TRƯỚC mốc của chính dòng đó → không rò rỉ
GROUP BY l.account_id, l.event_ts
ORDER BY txn_sum_30d_asof DESC
LIMIT 20;
Chú ý t.created_at < l.event_ts dùng mốc riêng của từng dòng l.event_ts, không phải một hằng số chung. Đây đúng là bản chất point-in-time correctness: feature của mỗi dòng train được "đóng băng" tại thời điểm sự kiện của nó. (Cả hai query đều ép ::numeric trước ROUND vì SUM/AVG trên cột NUMERIC cần kiểu rõ ràng cho hàm ROUND(numeric, int).)
Những query như thế này là offline feature computation. Trong Feature Store thật, kết quả của chúng vừa được lưu vào offline store (giữ timestamp cho as-of) vừa được materialize giá trị mới nhất vào online store (Redis) để serve — cùng một logic, không viết lại.
Tái sử dụng, chia sẻ & freshness
Tái sử dụng: khi account_txn_stats đã đăng ký, đội credit dựng scorecard (bài scoring & scorecard) và đội fraud giám sát giao dịch (bài transaction monitoring) cùng gọi feature đó qua get_historical_features/get_online_features. Không đội nào viết lại SQL. Feature được kiểm định một lần thì mọi mô hình hưởng lợi.
Feature freshness & monitoring: feature chỉ có giá trị nếu tươi. Online store phải được materialize đủ thường xuyên; nếu job materialize trễ, txn_sum_30d online sẽ cũ hơn thực tế → mô hình chấm điểm bằng dữ liệu ôi. Vì vậy cần giám sát:
- Freshness lag — khoảng cách giữa
now()và timestamp giá trị mới nhất trong online store; cảnh báo nếu vượt ngưỡng (ví dụ > 1 giờ). - Null/coverage — tỉ lệ entity thiếu feature (account mới chưa có lịch sử → null, cần chiến lược default).
- Online-offline consistency — lấy mẫu vài entity, so giá trị online với giá trị offline tính lại; lệch lớn là dấu hiệu skew quay lại. Đây là mắt xích nối sang giám sát & drift.
Use case thực tế
Bối cảnh: NCB triển khai chấm điểm gian lận real-time cho giao dịch thẻ. Yêu cầu: khi khách quẹt, hệ thống phải trả điểm rủi ro trong < 100 ms, dùng feature giống hệt lúc train.
Trước Feature Store:
- Đội DS train mô hình với feature
txn_sum_30d,txn_count_30dtính bằng SQL trên warehouse (batch đêm). - Đội backend viết lại logic tính hai feature đó bằng Java trong service online, nhưng dùng cửa sổ "30 ngày đến nửa đêm hôm qua" thay vì "30 ngày đến đúng thời điểm giao dịch", và không loại
kind = 'refund'. - Kết quả: AUC offline 0.86, online tụt còn 0.71. Mất 3 tuần điều tra mới phát hiện skew ở định nghĩa feature.
Sau khi dựng Feast:
- Định nghĩa
account_txn_stats(feature view) một lần từ bảngtransactions, gói vào feature servicefraud_scoring_v1. - Train:
get_historical_featuresvớientity_dfgồm 2 triệu giao dịch đã gán nhãn (mỗi dòng cóevent_timestamp) → as-of join tự động, không rò rỉ. AUC 0.86. - Materialize: job chạy mỗi 15 phút, đẩy giá trị
txn_sum_30d/count_30dmới nhất của mọi account vào Redis. - Serve: service gọi
get_online_features(account_id)→ nhận vector feature trong ~4 ms, chấm điểm trong ngân sách 100 ms. - Monitor: cảnh báo nếu freshness lag > 20 phút; job kiểm tra consistency online-offline hằng đêm trên 500 account mẫu.
Kết quả: online AUC lên 0.85 (khớp offline trong sai số), thời gian đưa feature mới ra production giảm từ ~2 tuần xuống ~2 ngày vì không phải code lại hai lần. Đội credit sau đó tái dùng chính account_txn_stats cho scorecard mà không tốn công tính lại.
Chi tiết deploy phần serving nằm ở MLOps 5 — Model Registry & Deploy và ML — Deployment & MLOps; phần pipeline điều phối materialize/train ở MLOps 4 — Pipelines.
Ghi nhớ
- Ba nỗi đau: (1) training-serving skew — feature định nghĩa hai lần bằng hai đoạn code lệch nhau; (2) trùng lặp — mỗi team tính lại cùng feature; (3) rò rỉ nhãn tương lai — tính feature không point-in-time.
- Điểm chữa gốc của skew: định nghĩa feature một lần, cả train và serve đọc cùng định nghĩa.
- Bốn thành phần: Registry (định nghĩa/metadata), Offline store (lịch sử có timestamp, cho train — warehouse), Online store (giá trị mới nhất, độ trễ thấp, cho serve — thường Redis/NoSQL), Materialization + Retrieval.
- Point-in-time correctness = as-of join: feature của mỗi dòng train lấy đúng giá trị tại
event_tscủa dòng đó, chỉ dùng dữ liệu có timestamp< event_ts. Trong SQL: điều kiệnt.created_at < event_tsvà mốc phải riêng theo từng dòng, không dùng hằng số chung. - Feast: Entity (join key) → Feature View (nhóm feature + nguồn + ttl) → Feature Service (gói cho một mô hình).
get_historical_featurescho train (as-of),get_online_featurescho serve. - Materialization đẩy offline→online định kỳ/streaming; freshness phải được giám sát (lag, coverage, online-offline consistency) nếu không mô hình chấm bằng dữ liệu ôi.
- Tái sử dụng là lợi ích tổ chức: feature kiểm định một lần, nhiều đội (fraud, credit, marketing) dùng chung.
Bài viết liên quan
Phân biệt AI/ML/DL, các kiểu học máy, quy trình ML end-to-end và thuật ngữ nền tảng.
Chất lượng dữ liệu, xử lý thiếu/ngoại lai, mã hoá, chuẩn hoá, tạo đặc trưng và tránh data leakage.
Mô hình ngôn ngữ lớn hoạt động ra sao, token, context window, tham số sinh, và kỹ thuật viết prompt.
Retrieval-Augmented Generation, vector database, tool calling, agent và MCP để xây ứng dụng LLM thực tế.