Từ notebook đến API trong hạ tầng khách: huấn luyện model scikit-learn, lưu an toàn và đóng container
Model đạt độ chính xác cao trên laptop vẫn có thể không load nổi trên server của khách hàng; bài thực hành này đi qua từng bước để tránh đúng tình huống đó.
- 1Huấn luyện pipelineGói scaler và model vào một pipeline. Rủi ro: lưu thiếu bước chuẩn bị dữ liệu
- 2Lưu bằng skops.ioRủi ro: artifact tin nhầm có thể chạy code. Chỉ load các type đã duyệt bằng mắt
- 3Pin môi trườngRủi ro: lệch version scikit-learn. Chạy pip freeze trong venv sạch, ghi version ==
- 4Bọc bằng FastAPILoad model một lần với danh sách trusted đã duyệt, kiểm tra input, trả về /predict
- 5Đóng containerCopy requirements trước, CMD dạng exec, model và image luôn đi cùng một lần build
- 6Chạy ở hạ tầng kháchDocker trên máy ảo; trên Kubernetes: một process mỗi container, hoặc KServe
Phần lớn rủi ro không nằm ở bước train mà ở cách lưu, pin version và đóng gói model.
Đồ hoạ: FDE Times
Tóm tắt nhanh
- scikit-learn không hỗ trợ load model đã huấn luyện bằng phiên bản khác, nên môi trường serving phải pin đúng package và version lúc train.
- Không bao giờ load file pickle từ nguồn không tin cậy; skops.io chỉ load các type đã được tin cậy.
- Đóng container FastAPI với CMD dạng exec; trên Kubernetes, chạy một process Uvicorn mỗi container và để cluster lo replication.
Tài liệu của scikit-learn viết một câu mà FDE nào cũng nên học thuộc: không có cách nào được hỗ trợ để load một model huấn luyện bằng phiên bản scikit-learn khác. Model của bạn chạy hoàn hảo trên laptop, nhưng server của khách đang dùng một version khác. Khi đó, không có gì bảo đảm model sẽ load được, hay chạy đúng nếu load được.
Đó là khoảng cách giữa một data scientist và một Forward Deployed Engineer. Người thứ nhất bàn giao một file model. Người thứ hai bàn giao một service chạy được trong hạ tầng khách, với môi trường được pin, artifact an toàn và cách khởi động mà đội vận hành của khách hiểu được.
Bài này đi qua toàn bộ con đường đó với một ví dụ nhỏ có chủ đích: model phân loại hoa iris bằng scikit-learn, bọc trong FastAPI, đóng thành container. Ví dụ đơn giản để bạn tập trung vào phần khó thật sự, là đưa model ra khỏi laptop.
Bạn sẽ build gì, cần chuẩn bị gì?
Kết quả cuối cùng là một Docker image có endpoint POST /predict, nhận bốn số đặc trưng và trả về nhãn dự đoán. Bạn cần Python, Docker, và các gói scikit-learn, skops, fastapi, uvicorn. Nếu muốn làm lại với PyTorch, lưu ý bản stable mới nhất yêu cầu Python 3.10 trở lên, nên chọn luôn Python từ 3.10 để hai hướng dùng chung một nền.
Hãy làm mọi thứ trong một virtualenv sạch, tạo riêng cho dự án này. Lý do sẽ rõ ở bước 3.
python -m venv .venv
source .venv/bin/activate
pip install scikit-learn skops fastapi uvicorn
Nên sắp thư mục như sau: train.py ở gốc, app/main.py cho API, model/ chứa artifact, cùng requirements.txt và Dockerfile.
Bước 1: huấn luyện như một pipeline, không phải các ô notebook rời
IBM mô tả quy trình huấn luyện gồm chọn model, thu thập và chuẩn bị dữ liệu, chọn hyperparameter, tính loss, tối ưu tham số và kết thúc bằng đánh giá. Với scikit-learn, cách gọn nhất là gói bước chuẩn bị dữ liệu và model vào cùng một pipeline, để API sau này không phải tự tái tạo bước scale.
# train.py — ví dụ rút gọn
from sklearn.datasets import load_iris
from sklearn.model_selection import train_test_split
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.linear_model import LogisticRegression
import skops.io as sio
X, y = load_iris(return_X_y=True)
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, random_state=42)
model = make_pipeline(StandardScaler(), LogisticRegression(max_iter=1000))
model.fit(X_train, y_train)
print("accuracy:", model.score(X_test, y_test))
sio.dump(model, "model/model.skops")
Kiểm tra: chạy python train.py, thấy dòng accuracy in ra và file model/model.skops xuất hiện. Nếu bạn chỉ lưu model mà không lưu scaler, đó là lỗi kinh điển: API sẽ nhận dữ liệu thô và dự đoán sai mà không báo lỗi gì.
Bước 2: vì sao không dùng pickle?
Tài liệu scikit-learn nói thẳng: không bao giờ load file pickle từ nguồn không tin cậy, cũng như không bao giờ chạy code từ nguồn không tin cậy. Ở site khách hàng, câu này rất thực tế. Artifact có thể đi qua ổ chia sẻ, bucket hoặc email của nhiều người trước khi tới server.
skops.io tránh pickle và chỉ load các file có type và tham chiếu hàm được tin cậy, mặc định hoặc do người dùng chỉ định. Trước khi load, bạn liệt kê những type chưa được tin cậy, tự đọc lại từng dòng, và chỉ khi đã duyệt xong mới ghi danh sách đó ra một file đi kèm model:
# review_types.py — ví dụ rút gọn
from pathlib import Path
import skops.io as sio
unknown = sio.get_untrusted_types(file="model/model.skops")
print(unknown) # đọc kỹ từng dòng trước khi tin
# chỉ chạy dòng dưới sau khi bạn đã duyệt danh sách trên
Path("model/trusted_types.txt").write_text("\n".join(unknown))
Đoạn trên được rút gọn để minh họa; hãy đối chiếu chữ ký hàm với version skops bạn cài. Điểm cốt lõi là danh sách trusted phải là thứ bạn đã duyệt bằng mắt, không phải thứ truyền thẳng vào cho xong. File trusted_types.txt trở thành một phần của artifact: ai review model cũng nhìn thấy bạn đã tin những gì.
Bước 3: pin môi trường trước khi viết API
Tài liệu scikit-learn yêu cầu môi trường serving có cùng package và cùng version với môi trường huấn luyện. Vì thế, ghi lại version ngay sau khi train, khi bạn còn chắc chắn model được tạo ra bằng gì:
python -c "import sklearn; print(sklearn.__version__)"
pip freeze > requirements.txt
Chạy pip freeze bên trong virtualenv sạch đã tạo từ đầu, không phải môi trường Python chung của máy. Nếu không, requirements.txt sẽ kéo theo mọi thư viện bạn từng cài cho dự án khác, làm image nặng hơn và khiến đội của khách khó biết model thật sự cần gì.
Kiểm tra: mở requirements.txt, tìm dòng scikit-learn== có version cụ thể, và danh sách chỉ nên gồm các gói đã cài ở trên cùng phụ thuộc của chúng. Nếu thấy scikit-learn không kèm số, hoặc dấu >=, bạn đang mở cửa cho đúng lỗi ở đầu bài.
Bước 4: bọc model bằng FastAPI
API chỉ nên làm ba việc: load model một lần khi khởi động, kiểm tra input, gọi predict. Danh sách trusted được đọc từ file đã duyệt ở bước 2; nếu bạn để trống trong khi file model chứa type chưa được tin cậy mặc định, việc load sẽ thất bại ngay khi service khởi động.
# app/main.py — ví dụ rút gọn
from pathlib import Path
from fastapi import FastAPI
from pydantic import BaseModel
import skops.io as sio
# các type đã duyệt ở bước 2
TRUSTED = Path("model/trusted_types.txt").read_text().split()
model = sio.load("model/model.skops", trusted=TRUSTED)
app = FastAPI()
class Features(BaseModel):
values: list[float]
@app.post("/predict")
def predict(f: Features):
pred = model.predict([f.values])
return {"prediction": int(pred[0])}
Chạy thử uvicorn app.main:app --port 8000 rồi gọi:
curl -X POST localhost:8000/predict \
-H "Content-Type: application/json" \
-d '{"values": [5.1, 3.5, 1.4, 0.2]}'
Kiểm tra: nhận về JSON có trường prediction. Thử gửi ba số thay vì bốn để xem API phản ứng thế nào; ở site khách, dữ liệu sai định dạng là chuyện hằng ngày, và bạn nên quyết định trước thông báo lỗi trông ra sao.
Bước 5: đóng container sao cho khách chạy lại được
FastAPI khuyến nghị container vì tính bảo mật, khả năng tái lập và sự đơn giản. Hai chi tiết trong tài liệu đáng làm đúng ngay từ đầu: copy requirements.txt trước để tận dụng cache layer, và luôn dùng CMD dạng exec để FastAPI tắt êm và các lifespan event được kích hoạt.
FROM python:3.11
WORKDIR /code
COPY ./requirements.txt /code/requirements.txt
RUN pip install --no-cache-dir -r /code/requirements.txt
COPY ./app /code/app
COPY ./model /code/model
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "80"]
Image Python ở dòng đầu nên trùng phiên bản bạn dùng khi train. Thư mục model/ mang theo cả model.skops lẫn trusted_types.txt. Build và chạy:
docker build -t iris-api .
docker run -p 8000:80 iris-api
Kiểm tra: lặp lại lệnh curl ở bước 4, kết quả phải giống hệt. Nếu khác, môi trường trong container đã lệch khỏi môi trường train.
Những lỗi khiến bạn mất một ngày ở site khách
Một lỗi dễ mắc là viết CMD dạng shell, kiểu CMD uvicorn app.main:app. Tài liệu FastAPI nhấn mạnh phải luôn dùng dạng exec để ứng dụng tắt êm và lifespan event được kích hoạt; bỏ dạng exec là bạn mất sự bảo đảm đó đúng lúc Kubernetes hay đội vận hành dừng container.
Lỗi tiếp theo là nhồi nhiều worker Uvicorn vào một container chạy trên Kubernetes. Tài liệu FastAPI khuyên chạy một process Uvicorn mỗi container khi cluster đã lo replication; để cluster scale số pod thay vì tự scale bên trong.
Lỗi cuối là retrain trên laptop sau khi nâng cấp thư viện, rồi chỉ gửi file model mới cho khách. Model và image phải luôn đi thành cặp, cùng một lần build.
Kỹ năng này hiện ra thế nào khi làm việc với khách?
Ở buổi làm việc kỹ thuật đầu tiên, câu hỏi nên đặt ra không phải “model nào” mà là “service sẽ chạy ở đâu”. Khách chạy Docker trên vài máy ảo thì image ở bước 5 gần như đủ. Khách đã có Kubernetes thì bạn chuẩn bị một container một process và hỏi đội platform về cách họ scale.
Nếu khách đã chạy Kubernetes, KServe là một lựa chọn nên cân nhắc: đây là nền tảng inference phân tán cho cả model dự đoán lẫn generative, và là dự án incubating của CNCF. Với model PyTorch, trang chủ PyTorch giới thiệu TorchScript để chuyển từ eager mode sang graph mode và TorchServe để rút ngắn đường lên production.
Dù chọn nền tảng nào, nguyên tắc pin version và đối xử artifact như code vẫn giữ nguyên.
Khi đọc JD FDE, hãy để ý các cụm như “deploy into customer environments”, “on-prem” hay “Kubernetes”. Đó là tín hiệu vị trí cần đúng chuỗi kỹ năng trong bài. Trong CV, đừng viết “biết machine learning”; hãy viết bạn đã đóng gói model thành API trong container, pin môi trường và thay pickle bằng định dạng an toàn hơn, kèm link repo có Dockerfile.
Model chính xác giúp bạn thắng buổi demo. Một image khởi động được ngay lần đầu trên server của khách mới giúp bạn được mời quay lại.