Bài 11

MLOps & Triển khai

Huấn luyện được một mô hình có accuracy cao mới chỉ là một nửa công việc. Nửa còn lại — thường khó hơn nhiều — là đưa mô hình đó vào production một cách ổn định, có thể tái lập, giám sát được và cập nhật liên tục. MLOps (Machine Learning Operations) là tập hợp các nguyên tắc và công cụ để làm việc đó. Bài này đi từ vòng đời ML end-to-end, cách lưu & version mô hình, các kiểu phục vụ, đóng gói REST API bằng FastAPI, experiment tracking với MLflow, feature store, CI/CD cho ML và giám sát drift trong production.

MLOps là gì & vì sao mô hình chết trên bàn thí nghiệm
Vòng đời ML end-to-end

MLOps là sự giao thoa giữa Machine Learning, DevOps và Data Engineering: áp dụng các thực hành kỹ thuật phần mềm (versioning, automation, testing, CI/CD, monitoring) vào toàn bộ vòng đời của mô hình học máy. Mục tiêu là biến việc đưa mô hình ra production từ một sự kiện thủ công, mong manh, "chỉ chạy trên máy của tôi" thành một quy trình lặp lại được, tự động và đáng tin cậy.

Một con số hay được trích dẫn trong ngành: khoảng 87% dự án ML không bao giờ lên được production (theo các khảo sát của VentureBeat, Gartner...). Lý do hầu như không phải vì thuật toán kém, mà vì các vấn đề kỹ thuật vận hành:

Vòng đời ML end-to-end (ML lifecycle) là một vòng lặp liên tục chứ không phải đường thẳng:

Giai đoạnNội dung
1. Business & data understandingXác định bài toán, metric thành công, thu thập & khám phá dữ liệu (EDA).
2. Data preparationLàm sạch, feature engineering, tách train/val/test, versioning dữ liệu.
3. Model developmentThử nghiệm thuật toán, tuning, tracking experiment (params/metrics/artifacts).
4. EvaluationĐánh giá offline trên test set, so sánh với baseline, kiểm tra fairness/robustness.
5. DeploymentĐóng gói, chọn kiểu phục vụ (batch/online/edge), release có kiểm soát (canary/shadow).
6. MonitoringTheo dõi data drift, concept drift, performance, latency; cảnh báo & quyết định retrain.
Cốt lõi: MLOps không phải một công cụ duy nhất mà là văn hoá + tự động hoá. Trưởng thành theo cấp độ: Level 0 (thủ công) → Level 1 (tự động hoá pipeline train) → Level 2 (CI/CD đầy đủ cho cả code lẫn mô hình).
Lưu & version mô hình
Pickle · joblib · ONNX · Model Registry

Sau khi train xong, mô hình (bao gồm các tham số đã học) cần được serialize ra file để tải lại lúc phục vụ. Các lựa chọn phổ biến trong hệ sinh thái Python:

Định dạngDùng khi nàoƯu / nhược
pickleSerialize bất kỳ object Python nào, mặc định của Python.Tiện, nhưng gắn chặt với phiên bản Python/thư viện; không an toàn khi load file từ nguồn không tin cậy (có thể chạy mã tuỳ ý).
joblibChuẩn de-facto cho mô hình scikit-learn (nhiều mảng NumPy lớn).Nhanh & nén tốt hơn pickle với numpy array; vẫn cùng hạn chế về tương thích phiên bản.
ONNXCần chạy mô hình ở môi trường/ngôn ngữ khác (C++, Java, mobile), tối ưu tốc độ inference.Định dạng trung lập giữa các framework; runtime tối ưu (ONNX Runtime); tách khỏi Python.
Framework-nativeTensorFlow SavedModel, PyTorch state_dict/TorchScript cho deep learning.Giữ đầy đủ đồ thị tính toán; hợp với serving chuyên dụng (TF Serving, TorchServe).
# Lưu & nạp mô hình scikit-learn bằng joblib
import joblib
joblib.dump(model, "model_v3.joblib")      # lưu
model = joblib.load("model_v3.joblib")     # nạp lại

# Xuất sang ONNX (từ sklearn)
from skl2onnx import to_onnx
onx = to_onnx(model, X_train[:1].astype("float32"))
with open("model.onnx", "wb") as f:
    f.write(onx.SerializeToString())

Model Registry là kho tập trung để quản lý các mô hình đã đăng ký: mỗi mô hình có nhiều version, mỗi version gắn stage (Staging, Production, Archived), kèm metadata (ai train, metric, data version, commit code). Nó cho phép promote/rollback mô hình mà không đổi code phía serving — service chỉ trỏ tới "mô hình ở stage Production". MLflow, SageMaker và Vertex AI đều có registry.

Bẫy phổ biến: luôn ghi lại phiên bản thư viện (scikit-learn, numpy) đi kèm file mô hình. Load một pickle sklearn cũ bằng phiên bản mới có thể lỗi hoặc — tệ hơn — chạy được nhưng cho kết quả sai lệch âm thầm.
Các kiểu phục vụ mô hình
Batch · Online/Real-time · Streaming · Edge

Không phải mô hình nào cũng cần một API real-time. Chọn kiểu phục vụ theo yêu cầu độ trễ và cách tiêu thụ dự đoán:

KiểuCách hoạt độngĐộ trễHợp cho
Batch predictionChạy định kỳ (cron/Airflow) trên một khối dữ liệu lớn, ghi kết quả vào DB/warehouse để dùng sau.Phút → giờChấm điểm churn hằng đêm, gợi ý email hằng ngày, scoring danh sách khách hàng.
Online / Real-time APIService (REST/gRPC) nhận request và trả dự đoán ngay lập tức, đồng bộ.Mili-giâyDuyệt gian lận lúc thanh toán, gợi ý khi user đang lướt, chấm điểm tín dụng tức thời.
StreamingTiêu thụ sự kiện liên tục từ Kafka/Kinesis, dự đoán theo luồng và phát kết quả ra topic khác.Cận real-timePhát hiện bất thường IoT, giám sát giao dịch liên tục, cập nhật feature real-time.
Edge / On-deviceMô hình chạy ngay trên thiết bị (mobile, camera, IoT) không cần gọi server.Cực thấp, offline đượcNhận diện khuôn mặt trên điện thoại, xe tự lái, khi mạng không ổn định/riêng tư.
Mẹo chọn: hỏi hai câu — (1) dự đoán có cần ngay khi user hành động không? Nếu không, batch là đủ và rẻ nhất. (2) Có ràng buộc mạng/riêng tư/độ trễ khắc nghiệt không? Nếu có, cân nhắc edge. Đa số bài toán "phân tích" thực ra chỉ cần batch.
Đóng gói REST API với FastAPI
Load model · endpoint /predict · Docker

FastAPI là framework Python phổ biến nhất để bọc mô hình thành REST API online: hiệu năng cao (dựa trên ASGI/uvicorn), validate input tự động qua Pydantic, và sinh docs OpenAPI/Swagger sẵn tại /docs. Mẫu chuẩn: nạp mô hình một lần lúc khởi động (không nạp trong mỗi request), rồi định nghĩa endpoint /predict:

# app.py
from fastapi import FastAPI
from pydantic import BaseModel, Field
import joblib, numpy as np

# Nạp mô hình MỘT LẦN khi service khởi động
model = joblib.load("model_v3.joblib")
app = FastAPI(title="Churn Predictor", version="3.0")

# Schema input: Pydantic validate kiểu & ràng buộc tự động
class Customer(BaseModel):
    tenure: int = Field(..., ge=0, description="Số tháng gắn bó")
    monthly_charges: float = Field(..., ge=0)
    total_charges: float
    contract_type: int  # đã encode 0/1/2

class Prediction(BaseModel):
    churn: bool
    probability: float

@app.get("/health")
def health():
    return {"status": "ok"}

@app.post("/predict", response_model=Prediction)
def predict(c: Customer):
    x = np.array([[c.tenure, c.monthly_charges,
                   c.total_charges, c.contract_type]])
    prob = float(model.predict_proba(x)[0, 1])
    return Prediction(churn=prob > 0.5, probability=round(prob, 4))

Chạy local: uvicorn app:app --host 0.0.0.0 --port 8000. Test nhanh:

curl -X POST http://localhost:8000/predict \
  -H "Content-Type: application/json" \
  -d '{"tenure":5,"monthly_charges":80.5,"total_charges":402.5,"contract_type":0}'
# => {"churn": true, "probability": 0.7213}

Docker hoá để đảm bảo môi trường đồng nhất giữa dev và production:

# Dockerfile
FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY app.py model_v3.joblib ./
EXPOSE 8000
CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "8000"]
docker build -t churn-api:3.0 .
docker run -p 8000:8000 churn-api:3.0
Production tips: thêm /health cho readiness probe của Kubernetes; chạy nhiều worker (--workers 4) hoặc dùng Gunicorn quản uvicorn; log mỗi request & dự đoán để phục vụ monitoring; giới hạn kích thước input để tránh abuse.
Experiment Tracking & Reproducibility
MLflow · versioning data (DVC)

Khi thử hàng chục cấu hình mô hình, bạn cần ghi lại một cách có hệ thống: tham số nào cho metric nào, artifact (mô hình, biểu đồ) ở đâu. MLflow là công cụ mã nguồn mở phổ biến nhất, gồm bốn thành phần: Tracking (log params/metrics/artifacts), Projects (đóng gói code tái lập), Models (định dạng chuẩn để deploy), và Model Registry (quản lý version + stage).

import mlflow, mlflow.sklearn
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import f1_score, roc_auc_score

mlflow.set_experiment("churn-prediction")

with mlflow.start_run(run_name="rf-baseline"):
    params = {"n_estimators": 300, "max_depth": 12}
    model = RandomForestClassifier(**params).fit(X_train, y_train)

    proba = model.predict_proba(X_test)[:, 1]
    pred  = (proba > 0.5)

    # 1) Log tham số
    mlflow.log_params(params)
    # 2) Log metric
    mlflow.log_metric("f1", f1_score(y_test, pred))
    mlflow.log_metric("auc", roc_auc_score(y_test, proba))
    # 3) Log artifact + mô hình, đăng ký vào Registry
    mlflow.log_artifact("feature_importance.png")
    mlflow.sklearn.log_model(model, "model",
                             registered_model_name="churn-rf")

Mở UI bằng mlflow ui để so sánh các run cạnh nhau. Sau đó có thể promote một version lên Production trong Registry và service serving load qua URI models:/churn-rf/Production.

Versioning dữ liệu với DVC: Git version code rất tốt nhưng không hợp để lưu file dữ liệu lớn. DVC (Data Version Control) lưu con trỏ nhỏ (hash) trong Git còn dữ liệu thật ở remote storage (S3, GCS), cho phép git checkout một commit rồi dvc checkout để lấy đúng phiên bản dữ liệu tương ứng — chìa khoá của reproducibility.

dvc init
dvc add data/train.csv        # tạo train.csv.dvc (con trỏ), thêm vào .gitignore
git add data/train.csv.dvc .gitignore
dvc remote add -d storage s3://my-bucket/dvcstore
dvc push                      # đẩy dữ liệu lên remote
Reproducibility = code + data + môi trường + tham số + seed. Ghi lại đủ 5 yếu tố này (Git commit, DVC hash, requirements.txt/Docker image, MLflow params, random_state) thì bất kỳ ai cũng tái lập được kết quả của bạn.
Feature Store
Nơi tính, lưu & chia sẻ feature

Trong tổ chức lớn, cùng một feature (vd "tổng chi tiêu 30 ngày của khách hàng") được nhiều mô hình dùng chung, và phải nhất quán giữa lúc train và lúc serve. Feature Store (Feast, Tecton, Databricks Feature Store...) là hệ thống trung tâm để định nghĩa, tính toán, lưu trữ và phục vụ feature một cách chuẩn hoá. Nó giải quyết đúng vấn đề training-serving skew.

Thành phầnVai trò
Offline storeLưu lịch sử feature (warehouse/parquet) để tạo training set và tính "point-in-time correct" — tránh data leakage từ tương lai.
Online storeKho low-latency (Redis, DynamoDB) chứa giá trị feature mới nhất để serving real-time đọc trong vài mili-giây.
Feature registryDanh mục định nghĩa feature (tên, kiểu, nguồn, cách tính) để tái sử dụng & khám phá giữa các team.
MaterializationQuá trình đồng bộ feature đã tính từ offline sang online store theo lịch.
Khi nào cần: feature store là hạ tầng đáng kể. Nhóm nhỏ với vài mô hình thường chưa cần — chỉ cần đảm bảo dùng chung một hàm feature engineering giữa train và serve. Cân nhắc khi có nhiều team, nhiều mô hình dùng chung feature real-time.
CI/CD cho ML
Automated pipeline & testing mô hình

CI/CD trong ML phức tạp hơn phần mềm thông thường vì có thêm hai "tài sản" thay đổi: dữ liệumô hình, không chỉ code. Người ta hay chia thành ba đường ống:

Các loại test đặc thù cho ML (ngoài unit test code thông thường):

Loại testKiểm tra gì
Data validationSchema, kiểu dữ liệu, khoảng giá trị, tỉ lệ null, số lượng category (dùng Great Expectations, pandera).
Model quality gateMetric trên test set phải ≥ ngưỡng và không tệ hơn mô hình production hiện tại.
Behavioral testInvariance (đổi feature không liên quan thì dự đoán không đổi), directional (tăng thu nhập thì xác suất duyệt vay tăng).
Serving testAPI trả đúng schema, latency dưới ngưỡng, load test, kiểm tra parity giữa batch & online.
Công cụ orchestration: GitHub Actions/GitLab CI cho phần code; Airflow, Kubeflow Pipelines, Vertex AI Pipelines hoặc Metaflow cho pipeline train/deploy nhiều bước.
Giám sát mô hình trong Production
Data drift · Concept drift · Retrain

Không giống phần mềm thông thường (đúng thì cứ đúng mãi), mô hình ML xuống cấp theo thời gian vì thế giới thay đổi trong khi mô hình đứng yên. Có hai loại "trôi" cần phân biệt:

Hiện tượngĐịnh nghĩaVí dụ
Data drift (covariate shift)Phân phối input P(X) thay đổi, quan hệ X→y vẫn giữ.Nhóm khách hàng mới trẻ hơn hẳn tập train; giá cả lạm phát dịch chuyển.
Concept driftQuan hệ P(y|X) thay đổi — cùng input nhưng nhãn đúng đã khác.Hành vi gian lận thay đổi; sau COVID thói quen mua sắm khác hẳn.
Label/prediction driftPhân phối đầu ra P(y_hat) lệch so với lúc train.Tỉ lệ dự đoán "churn" đột ngột tăng gấp đôi.

Cách phát hiện drift trên từng feature/đầu ra:

# PSI đơn giản giữa phân phối tham chiếu và hiện tại
import numpy as np
def psi(ref, cur, bins=10):
    edges = np.percentile(ref, np.linspace(0, 100, bins + 1))
    edges[0], edges[-1] = -np.inf, np.inf
    r = np.histogram(ref, edges)[0] / len(ref)
    c = np.histogram(cur, edges)[0] / len(cur)
    r, c = np.clip(r, 1e-6, None), np.clip(c, 1e-6, None)
    return np.sum((c - r) * np.log(c / r))

print(psi(ref_scores, live_scores))   # > 0.25 => cảnh báo drift

Khi nào retrain? Có ba chiến lược, thường kết hợp: (1) theo lịch — retrain định kỳ (hằng tuần/tháng) đơn giản, dễ vận hành; (2) theo trigger — retrain khi PSI/KS vượt ngưỡng hoặc metric tụt dưới SLA; (3) online/continual — cập nhật liên tục, phức tạp và rủi ro nhất. Kèm theo là alerting: đẩy metric drift & performance lên Prometheus/Grafana hoặc công cụ chuyên dụng (Evidently, WhyLabs, Arize) và bắn cảnh báo qua Slack/PagerDuty khi vượt ngưỡng.

Nên giám sát 4 nhóm: (1) chất lượng dữ liệu đầu vào (null, schema), (2) drift của feature & prediction, (3) performance khi có ground truth, (4) sức khoẻ vận hành (latency, throughput, tỉ lệ lỗi). Đừng chỉ nhìn accuracy — nó thường đến muộn nhất.
Cạm bẫy & lời khuyên khởi đầu
Training-serving skew · bắt đầu đơn giản
Training-serving skew — sát thủ thầm lặng: khi logic xử lý feature lúc train (thường viết bằng pandas trong notebook) khác logic lúc serve (viết lại tay trong service). Ví dụ: lúc train bạn chuẩn hoá bằng mean/std của toàn bộ tập, nhưng lúc serve lại tính trên từng request; hoặc thứ tự cột, cách xử lý missing/category khác nhau. Hậu quả: mô hình offline rất tốt nhưng online dự đoán sai mà không báo lỗi. Cách phòng: dùng chung một pipeline/hàm feature (đóng gói scikit-learn Pipeline hoặc feature store) cho cả train lẫn serve, và viết test parity so sánh output hai phía trên cùng input.
Bắt đầu đơn giản trước: đừng dựng Kubernetes + feature store + streaming ngay từ đầu. Với mô hình đầu tiên, một batch job chạy bằng cron (đọc dữ liệu → load model joblib → ghi dự đoán vào DB) đáp ứng phần lớn nhu cầu, dễ debug, dễ tin cậy. Chỉ nâng cấp lên online API, rồi CI/CD, rồi monitoring tự động, rồi feature store khi thực sự có nhu cầu. MLOps là hành trình trưởng thành dần, không phải mua trọn gói ngày một.
← Quay lại
Bài 10: Deep Learning