Mô Hình ML Chạy Hoàn Hảo, Nhưng Chưa Hữu Dụng: Hành Trình Biến Model Thành API Service

Công nghệ03 tháng 9, 2026·9 phút đọc

Bài viết chia sẻ trải nghiệm thực tế của một nhà phân tích dữ liệu khi chuyển từ xây dựng mô hình dự đoán khách hàng rời bỏ (churn prediction) sang việc triển khai nó thành một dịch vụ API với FastAPI. Tác giả nhấn mạnh sự khác biệt giữa việc có một mô hình 'chạy được' trong notebook và một mô hình thực sự 'hữu dụng' cho các hệ thống phần mềm khác. Qua đó, bài viết chỉ ra các vấn đề kỹ thuật quan trọng như chia sẻ tiền xử lý dữ liệu, thiết kế schema API, và kiểm thử phần mềm.

Mô Hình ML Chạy Hoàn Hảo, Nhưng Chưa Hữu Dụng: Hành Trình Biến Model Thành API Service

Mô Hình ML Chạy Hoàn Hảo, Nhưng Chưa Hữu Dụng: Hành Trình Biến Model Thành API Service

Việc xây dựng một mô hình machine learning có độ chính xác cao trong notebook mới chỉ là một nửa chặng đường. Để mô hình thực sự có giá trị, nó cần được đóng gói thành một dịch vụ mà các phần mềm khác có thể gọi đến. Bài viết dưới đây sẽ mô tả chi tiết quá trình biến một mô hình dự đoán khách hàng rời bỏ thành một API service với FastAPI, cùng những bài toán kỹ thuật thú vị phía sau.

Cách đây vài tháng, tôi thử thách bản thân chuyển từ nền tảng phân tích dữ liệu sang kỹ thuật dữ liệu (data engineering). Tôi đã xây dựng được hai dự án thực tế có ảnh hưởng, bao gồm một pipeline ETL cho GitHub và một pipeline RSS với Kestra. Tuy nhiên, tôi luôn bị cuốn hút bởi machine learning và cuối cùng đã thử sức với việc xây dựng mô hình dự đoán churn (khách hàng rời bỏ) cho một công ty viễn thông giả định tên là Northline Mobile.

Mô hình của tôi đạt độ chính xác 81% trên dữ liệu kiểm tra, một kết quả đáng khích lệ. Nhưng rồi tôi nhận ra một vấn đề lớn: mô hình tồn tại nhưng không thực sự hữu dụng. Nếu một nhân viên cần dự đoán, họ sẽ phải nhờ tôi mở Jupyter, chạy các ô lệnh theo thứ tự và gọi hàm predict_churn() một cách thủ công. Không ai khác có thể tự động gửi dữ liệu khách hàng và nhận kết quả, và mô hình cũng không thể kết nối với các ứng dụng khác.

Ranh Giới Thực Sự Cần Xây Dựng

Câu hỏi quan trọng không phải là "làm sao để đặt FastAPI quanh mô hình", mà là "ranh giới giữa phần mềm của tôi và mô hình nên trông như thế nào". Tôi quyết định chọn phương án toàn vẹn dữ liệu: API chấp nhận mọi trường dữ liệu thô mà hệ thống của Northline có về khách hàng, giống hệt các cột trong tập dữ liệu gốc.

Một request điển hình sẽ có dạng JSON đầy đủ thông tin về giới tính, tuổi, tình trạng hợp đồng, thời gian sử dụng, phí hàng tháng, v.v. Kết quả trả về được thiết kế tối giản: xác suất rời bỏ, dự đoán nhị phân và mức độ rủi ro (Thấp/Trung bình/Cao). Điều thú vị là mức độ rủi ro không phải do mô hình tạo ra — nó chỉ xuất ra xác suất thô. Tôi thêm một bước phân loại thủ công để giúp nhân viên giữ chân khách hàng dễ dàng hành động hơn.

Mô hình cần được chuẩn bị để sống bên ngoài môi trường notebook.

Kiến trúc tổng quan của hệ thốngKiến trúc tổng quan của hệ thống

Tiền Xử Lý Dữ Liệu: Kẻ Thù Thầm Lặng

Có một bước dễ bị đánh giá thấp giữa "request đến" và "dự đoán trả về": dữ liệu JSON thô trông không giống chút nào với những gì mô hình thực sự mong đợi. Mọi thao tác tiền xử lý (scaling, mã hóa one-hot) đã áp dụng trong quá trình huấn luyện đều phải được lặp lại giống hệt trong lúc suy luận (inference).

Nếu training chia tỷ lệ tenureMonthlyCharges theo một cách nào đó mà API lại chia tỷ lệ khác, hoặc quên chia tỷ lệ, mô hình sẽ nhận những con số chưa bao giờ thấy và âm thầm dự đoán sai. Để tránh điều này, tôi tạo một file preprocessing.py duy nhất, được import bởi cả script huấn luyện lẫn API.

Quá trình này đã giúp tôi phát hiện một bug cụ thể. Hàm mã hóa nhị phân trong lúc training có dòng df[col] = df[col].map({'Yes': 1, 'No': 0}) và duyệt qua danh sách cột bao gồm cả Churn — biến mục tiêu. Nhưng request thực tế không có cột Churn vì đó là thứ chúng ta đang cố dự đoán. Nếu chạy nguyên hàm này với dữ liệu request, nó sẽ báo lỗi vì thiếu cột. Cách khắc phục rất đơn giản: kiểm tra xem cột có tồn tại trước khi xử lý. Bug kiểu này chỉ xuất hiện khi bạn thử chạy code huấn luyện trong môi trường suy luận.

Kiến Trúc Dự Án và Nạp Mô Hình

Cấu trúc dự án được tổ chức rõ ràng với thư mục app/ chứa code phục vụ API, tách biệt với train.py ở thư mục gốc. Điểm đáng chú ý là train.py gọi vào app/preprocessing.py để tái sử dụng logic, chứ không phải ngược lại. Dịch vụ không cần biết cách mô hình được huấn luyện, chỉ cần cùng logic tiền xử lý.

Một quyết định quan trọng không kém là vị trí nạp mô hình. Cách sai là nạp bên trong hàm /predict để mỗi request đọc lại file .pkl từ đĩa — vừa chậm vừa lãng phí. Cách đúng là nạp một lần khi module được import:

# app/model.py
import joblib
from pathlib import Path

MODEL_DIR = Path(__file__).resolve().parent.parent / "models"

model = joblib.load(MODEL_DIR / "churn_pipeline.pkl")
scaler = joblib.load(MODEL_DIR / "scaler.pkl")
feature_columns = joblib.load(MODEL_DIR / "feature_columns.pkl")

Khi API bắt đầu phục vụ request, mô hình đã nằm sẵn trong bộ nhớ. Đây là chi tiết nhỏ nhưng tạo nên khác biệt lớn về hiệu suất.

Thiết Kế Endpoint /predict

Sau khi hoàn tất các bước chuẩn bị, endpoint thực tế khá nhỏ gọn:

# app/main.py
from fastapi import FastAPI
from schemas import CustomerRequest, ChurnPrediction
from model import predict_churn

app = FastAPI(title="Northline Mobile Churn API")

@app.post("/predict", response_model=ChurnPrediction)
def predict(customer: CustomerRequest):
    result = predict_churn(customer.model_dump())
    return result

Sự nhỏ gọn này có chủ đích. Toàn bộ logic nằm trong model.pypreprocessing.py, còn hàm route chỉ đảm nhận nhận request đã được xác thực và chuyển tiếp.

Một chi tiết khiến tôi mất một lúc để hiểu: với dữ liệu one-hot, dữ liệu training có thể tạo ra 4 cột giả cho PaymentMethod qua hàng ngàn dòng, nhưng một request chỉ có thể có một phương thức thanh toán. Trước khi dự đoán, tôi phải sắp xếp lại các cột của request theo danh sách chính xác mà mô hình đã học, điền giá trị 0 cho những cột thiếu:

df = df.reindex(columns=_feature_columns, fill_value=0)

Kiểm Thử Phần Mềm và Khi Nó Thực Sự Trở Thành "Software"

Quá trình xử lý request trong APIQuá trình xử lý request trong API

Việc nhận được 200 OK trong Swagger UI không phải là đích đến. Câu hỏi thú vị hơn là điều gì xảy ra khi input không "sạch". Tôi gửi request thiếu trường tenure và API từ chối trước khi mô hình nhìn thấy dữ liệu, kèm thông báo lỗi rõ ràng. Tôi gửi "gender": "female" viết thường thay vì "Female", và nhận lại phản hồi chính xác về lỗi literal.

Điểm mấu chốt là những request hỏng không bao giờ chạm tới predict_churn(). Schema không chỉ là tài liệu — nó là một cánh cổng thực sự. Input sai nhận lỗi cụ thể, không phải một lỗi mô hình mơ hồ nằm sâu bên trong, và cũng không phải dự đoán sai thầm lặng vì mô hình cố hiểu thứ nó chưa từng thấy.

Trước đây, để có dự đoán, tôi phải chạy model.predict(data) với Jupyter mở, các ô lệnh chạy đúng thứ tự. Bây giờ, chỉ cần gửi POST đến /predict từ bất kỳ đâu — một câu lệnh curl, một request từ ứng dụng khác. Người chưa từng xem code của tôi, chưa cài pandas hay nghe về scikit-learn vẫn có thể nhận dự đoán churn. Mô hình không trở nên thông minh hơn, nhưng nó đã trở thành thứ mà phần mềm khác có thể sử dụng.

Những Vấn Đề Chưa Được Giải Quyết

Nói thật, mô hình và API hoạt động tốt nhưng chỉ trên laptop của tôi. Nếu một kỹ sư khác chạy dự án này, không có gì đảm bảo nó sẽ hoạt động: phiên bản Python khác, Anaconda không được cài giống cách của tôi, hoặc xung đột phiên bản package. Chưa có cách nào chạy trên cloud, API không thể truy cập từ bên ngoài mạng nội bộ, và nếu laptop tắt, API cũng tắt.

Ví dụ về phản hồi lỗi từ APIVí dụ về phản hồi lỗi từ API

Đây sẽ là chủ đề của bài viết tiếp theo: đóng gói Docker và triển khai lên AWS. Việc xây dựng vững chắc ứng dụng lõi trước sẽ giúp việc thêm hạ tầng phía trên dễ dàng hơn rất nhiều — một nền móng vững chắc giúp việc gỡ lỗi sau này đơn giản hơn nhiều.

Bài Học Kinh Nghiệm

Mô hình hoạt động tốt không tự động trở thành mô hình có thể sử dụng. Chỉ số đánh giá tốt cho biết mô hình học được điều hữu ích, nhưng không đảm bảo người khác có thể gửi dữ liệu và nhận dự đoán. Biến mô hình thành thứ người dùng có thể dùng đòi hỏi một lớp kỹ thuật riêng.

Kết quả chạy API qua Swagger UIKết quả chạy API qua Swagger UI

API là một hợp đồng, không chỉ là lớp bọc. Ban đầu, tôi nghĩ API chỉ là một lớp mỏng quanh mô hình. Thực tế, việc quyết định request nên chứa gì, nên từ chối gì và trả về gì cũng quan trọng như gọi mô hình.

Suy luận (inference) là một bài toán kỹ thuật riêng. Dự đoán trong notebook tương đối dễ dàng. Dự đoán qua API đáng tin cậy đặt ra các vấn đề khác: tiền xử lý phải khớp hoàn toàn với huấn luyện, mô hình cần nạp một lần thay vì mỗi request, và hình dạng của một request đơn lẻ có thể tạo ra khác biệt.

Ranh giới rõ ràng giúp việc triển khai sau này dễ dàng hơn. Tôi không biết chính xác phần 2 sẽ mang đến điều gì khi triển khai, nhưng tôi biết ứng dụng giờ đã có hình dạng rõ ràng: dữ liệu vào → xác thực và chuẩn bị → mô hình dự đoán → phản hồi có cấu trúc ra. Dù điều gì hỏng tiếp theo, ít nhất cũng không phải vì tôi chưa từng định nghĩa API nên làm gì.

Mô hình chạy tốt chỉ là khởi đầu. Biến nó thành một service mà các hệ thống khác có thể gọi đến mới là lúc nó thực sự tạo ra giá trị. Nhưng điều đó lại mở ra một vấn đề mới: làm sao để dịch vụ này chạy được ở bất kỳ đâu, trên bất kỳ máy nào, và trong môi trường production thực sự. Đó sẽ là thử thách tiếp theo.

Chia sẻ:FacebookX
Nội dung tổng hợp bằng AI, mang tính tham khảo. Xem bài gốc ↗