Mục lục
- Mục tiêu bài học
- Vì sao cần feature scaling
- Model nào CẦN scaling
- Model nào KHÔNG cần scaling
- Công thức Min-Max Normalization
- sklearn MinMaxScaler — API cơ bản
- feature_range — đổi range mặc định
- Quy tắc vàng: fit trên train, transform cho cả 2
- inverse_transform — đưa về scale gốc
- MaxAbsScaler — variant cho sparse data
- Pitfall: outlier kéo lệch min / max
- Use case typical
- Code Python end-to-end
- Bài tập
- Tóm tắt
Mục tiêu bài học
Sau bài học, bạn sẽ:
- Giải thích được vì sao feature scaling là bước cần thiết trước khi train phần lớn model ML.
- Phân biệt nhóm model cần scaling (gradient / distance / margin-based) và nhóm không cần (tree-based).
- Nắm công thức Min-Max Normalization và biết kết quả nằm trong
[0, 1]. - Dùng
MinMaxScalercủa sklearn đúng API:fit_transformtrên train,transformtrên test. - Hiểu vì sao fit lại trên test là data leak và sai kết quả đánh giá.
- Đổi range bằng
feature_range; đảo ngược bằnginverse_transform. - Biết khi nào nên dùng
MaxAbsScalerthay choMinMaxScaler. - Nhận diện pitfall lớn nhất của Min-Max: nhạy cảm cực mạnh với outlier.
Vì sao cần feature scaling
Dataset thực tế hầu như không bao giờ có các feature cùng đơn vị, cùng độ lớn. Một bảng nhân sự điển hình:
age (năm) : 18 – 65 → range 47
salary (VND) : 5_000_000 – 100_000_000 → range 95_000_000
years_exp : 0 – 40 → range 40
Nếu đưa thẳng vào model, salary sẽ áp đảo 2 feature còn lại đơn giản vì giá trị tuyệt đối lớn hơn hàng triệu lần — không phải vì nó "quan trọng hơn". Hệ quả phụ thuộc loại model:
- Gradient descent (Linear / Logistic Regression, Neural Network): hàm loss có dạng "thung lũng" rất dài theo trục feature lớn, rất hẹp theo trục feature nhỏ. Gradient descent zigzag, mất nhiều epoch để hội tụ; learning rate phải nhỏ kẻo overshoot.
- Distance-based (KNN, K-Means): khoảng cách Euclid \( d(x, y) = \sqrt{\sum_i (x_i - y_i)^2} \) bị thống trị bởi feature có range lớn. Hai người chênh nhau 1 triệu VND lương sẽ "xa" hơn hai người chênh nhau 40 tuổi — vô lý.
- Margin-based (SVM): khoảng cách từ điểm tới hyperplane phụ thuộc trực tiếp độ lớn feature. Không scale → margin bị méo.
- Variance-based (PCA): PCA tìm hướng có variance lớn nhất. Feature có range lớn tự động có variance lớn, "ăn" hết các thành phần chính — kết quả PCA gần như chỉ phản ánh đơn vị đo.
Feature scaling đưa mọi feature về cùng một "sân chơi" — cùng range hoặc cùng phân phối — để model đánh giá tầm quan trọng dựa trên pattern thực, không phải đơn vị đo.
Model nào CẦN scaling
Các nhóm model dưới đây bắt buộc (hoặc gần như bắt buộc) scaling trước khi train:
- Linear Regression / Logistic Regression (khi train bằng gradient descent hoặc dùng regularization L1/L2). Sklearn dùng solver dạng đóng (closed-form) cho Linear Regression nên không bắt buộc, nhưng khi có L1/L2 (Ridge, Lasso — Bài 21) thì bắt buộc: penalty cộng trên hệ số, hệ số của feature scale lớn sẽ bị phạt bất công.
- SVM (Bài 31) — margin tính bằng khoảng cách, scale méo → margin méo, kernel RBF sẽ "chết" vì khoảng cách trong exponent quá lớn / quá nhỏ.
- KNN (Bài 27) — toàn bộ thuật toán là khoảng cách giữa các điểm.
- K-Means (Bài 32) — centroid và assignment đều dùng khoảng cách Euclid.
- PCA (Bài 36) — phân tích phương sai; không scale thì PCA chỉ phản ánh đơn vị đo.
- Neural Network (Series 3) — gradient descent + activation function (sigmoid / tanh) đều nhạy cảm độ lớn input. Mạng không scale thường không hội tụ hoặc hội tụ rất chậm.
Có 2 lựa chọn scaling chính dùng cho các model trên: Min-Max Normalization (bài này) và Standardization / Z-score (Bài 8). Bài 9 sẽ so sánh trực tiếp khi nào nên dùng cái nào.
Model nào KHÔNG cần scaling
Một nhóm model không thay đổi kết quả khi scale feature: các thuật toán dựa trên cây quyết định.
- Decision Tree (Bài 28)
- Random Forest (Bài 29)
- Gradient Boosting — XGBoost, LightGBM, CatBoost (Bài 30)
Lý do: mỗi node chỉ hỏi câu hỏi dạng "feature_i < threshold ?". Threshold được chọn từ chính các giá trị thực của feature đó. Nếu scale feature về [0, 1], threshold cũng tự động scale theo — split point đổi nhưng tập hợp các sample đi về mỗi nhánh không đổi. Cây sau scaling tương đương cây trước scaling.
Hệ quả thực dụng: với Random Forest / XGBoost, có thể bỏ qua bước scaling — không sai, không thừa. Nhưng nếu cùng dataset cần dùng nhiều model (so sánh Logistic Regression với XGBoost), ta vẫn để scaling trong pipeline cho thống nhất; nó chỉ "vô hại" với cây, không gây hại. Bài 9 sẽ deep hơn về phần này.
Công thức Min-Max Normalization
Min-Max scale một feature về đoạn [0, 1] bằng biến đổi affine — trừ đi min, chia cho range:
Trong đó \( x_{\min} \) và \( x_{\max} \) là min và max của feature đó trên tập train. Kết quả:
- Giá trị nhỏ nhất (
x = x_min) →x' = 0. - Giá trị lớn nhất (
x = x_max) →x' = 1. - Mọi giá trị giữa → tỉ lệ tuyến tính trong
[0, 1].
Tổng quát hơn — scale về một đoạn tuỳ ý [a, b]:
Đây chính là công thức sklearn dùng khi đặt feature_range=(a, b) (xem mục 7).
Tính chất quan trọng:
- Biến đổi tuyến tính — không thay đổi hình dạng phân phối, không thay đổi correlation giữa các feature.
- Bảo toàn thứ tự — nếu \( x_1 < x_2 \) thì \( x_1' < x_2' \).
- Đảo ngược được — biết \( x_{\min}, x_{\max} \) thì lấy lại \( x \) từ \( x' \) (mục 9).
sklearn MinMaxScaler — API cơ bản
Class MinMaxScaler nằm trong sklearn.preprocessing, theo đúng API fit / transform / fit_transform chung của sklearn.
from sklearn.preprocessing import MinMaxScaler
from sklearn.datasets import load_iris
from sklearn.model_selection import train_test_split
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, stratify=y
)
scaler = MinMaxScaler() # mặc định feature_range=(0, 1)
X_train_scaled = scaler.fit_transform(X_train)
X_test_scaled = scaler.transform(X_test) # KHÔNG fit lại trên test!
print("Train min :", X_train_scaled.min(axis=0)) # [0. 0. 0. 0.]
print("Train max :", X_train_scaled.max(axis=0)) # [1. 1. 1. 1.]
print("Test min :", X_test_scaled.min(axis=0)) # có thể < 0
print("Test max :", X_test_scaled.max(axis=0)) # có thể > 1
Sau khi fit, scaler nhớ min_ và scale_ đã học:
print("data_min_ :", scaler.data_min_) # min của từng feature trong train
print("data_max_ :", scaler.data_max_)
print("scale_ :", scaler.scale_) # = 1 / (data_max_ - data_min_)
print("min_ :", scaler.min_) # = -data_min_ * scale_
Công thức nội bộ của sklearn: X_scaled = X * scale_ + min_ — tương đương công thức ở mục 5 nhưng viết dưới dạng nhân + cộng để tận dụng vector hoá NumPy.
feature_range — đổi range mặc định
Tham số feature_range nhận một tuple (a, b) để chọn đoạn đích.
from sklearn.preprocessing import MinMaxScaler
scaler_pm1 = MinMaxScaler(feature_range=(-1, 1))
X_train_pm1 = scaler_pm1.fit_transform(X_train)
print(X_train_pm1.min(axis=0)) # [-1. -1. -1. -1.]
print(X_train_pm1.max(axis=0)) # [ 1. 1. 1. 1.]
Khi nào không dùng mặc định [0, 1]?
[-1, 1]— phù hợp với activationtanh(range output đúng[-1, 1]), hoặc khi muốn đối xứng quanh 0 mà không cần Standardization đầy đủ.[0, 255]— gần như không bao giờ dùng cho input model, nhưng có thể tiện cho visualization ảnh.[0, 1]mặc định — dùng cho 95% trường hợp; phù hợp vớisigmoidvà pixel ảnh.
Đa số bài thực tế chỉ dùng (0, 1) hoặc (-1, 1); các giá trị khác hiếm gặp.
Quy tắc vàng: fit trên train, transform cho cả 2
Quy tắc quan trọng nhất khi dùng bất kỳ scaler nào:
fit(hoặcfit_transform) chỉ chạy trên train.transformchạy trên cả train và test (và val), dùng cùng bộ tham số đã học từ train.
Vì sao? Test set là "tương lai" mà model chưa thấy. Nếu fit trên toàn bộ data (train + test) thì min và max đã chứa thông tin từ test — model đã "biết" về test trước khi predict. Đây là data leak: kết quả đánh giá trên test sẽ tốt hơn thực tế, và khi deploy gặp dữ liệu mới, model sẽ tệ hơn dự kiến.
Sai (data leak)
# SAI — fit trên toàn bộ data trước khi split
scaler = MinMaxScaler()
X_all_scaled = scaler.fit_transform(X) # leak!
X_train, X_test, y_train, y_test = train_test_split(X_all_scaled, y, test_size=0.2)
Sai (fit lại trên test)
# SAI — fit lại trên test → min/max khác giữa train và test
scaler = MinMaxScaler()
X_train_scaled = scaler.fit_transform(X_train)
X_test_scaled = scaler.fit_transform(X_test) # khác scaler!
Đúng
scaler = MinMaxScaler()
X_train_scaled = scaler.fit_transform(X_train)
X_test_scaled = scaler.transform(X_test) # dùng min/max của train
Lưu ý quan trọng: vì min và max lấy từ train, một giá trị trong test có thể lớn hơn x_max của train hoặc nhỏ hơn x_min → kết quả sau scaling sẽ vượt đoạn [0, 1]. Đây là hành vi đúng, không phải bug. Model vẫn predict được; nó chỉ phản ánh sự thật rằng test có sample "ngoài kinh nghiệm" của train.
Khi pipeline phức tạp hơn (nhiều bước preprocessing), nên dùng sklearn.pipeline.Pipeline (Bài 14) — pipeline tự đảm bảo mỗi fit chỉ chạy trên train, mỗi transform chạy đúng dữ liệu.
inverse_transform — đưa về scale gốc
Mọi scaler của sklearn đều có method inverse_transform — đảo ngược biến đổi để lấy lại scale gốc.
from sklearn.preprocessing import MinMaxScaler
import numpy as np
X = np.array([[1.0], [5.0], [10.0]])
scaler = MinMaxScaler()
X_scaled = scaler.fit_transform(X)
print(X_scaled.ravel()) # [0. 0.444 1. ]
X_back = scaler.inverse_transform(X_scaled)
print(X_back.ravel()) # [ 1. 5. 10.]
Use case chính của inverse_transform:
- Regression với scaled target. Khi train regression, đôi khi ta scale luôn
y(về[0, 1]) để loss số học ổn định. Model output là số trong[0, 1]— cầninverse_transformđể báo cáo kết quả theo đơn vị gốc (VND, mét, độ C…). - Hiển thị / debug. Sample scaled không trực quan; muốn in ra để con người đọc thì
inverse_transformrồi log. - Reverse cho production. Khi deploy: input đến từ user ở scale gốc → scaler.transform → model → inverse_transform output (nếu target được scale) → trả về user.
Nếu chỉ scale X (không scale y) — trường hợp phổ biến nhất — thì hiếm khi cần inverse_transform.
MaxAbsScaler — variant cho sparse data
MaxAbsScaler là biến thể của Min-Max, scale theo giá trị tuyệt đối lớn nhất:
Kết quả luôn nằm trong [-1, 1]. Khác biệt cốt lõi so với MinMaxScaler: không trừ đi giá trị nào, chỉ chia. Hệ quả: giá trị 0 trong input vẫn là 0 sau khi scale — giữ được sparsity.
from sklearn.preprocessing import MaxAbsScaler
import numpy as np
X = np.array([[ 0.0, 1.0],
[ 0.0, 5.0],
[ 0.0, -10.0]])
scaler = MaxAbsScaler()
X_scaled = scaler.fit_transform(X)
print(X_scaled)
# [[ 0. 0.1]
# [ 0. 0.5]
# [ 0. -1. ]]
# Cột 0 toàn 0 → giữ nguyên 0 sau scaling
Khi nào dùng MaxAbsScaler?
- Sparse matrix — TF-IDF, bag-of-words, one-hot encoded high-cardinality.
MinMaxScalersẽ phá sparsity (vì trừminbiến 0 thành số khác 0, khiến ma trận thành dense và tốn bộ nhớ). - Data đã centered quanh 0 (mean ≈ 0) — không cần dịch về 0 nữa.
Với data dense thông thường, dùng MinMaxScaler hoặc StandardScaler (Bài 8); chỉ chuyển sang MaxAbsScaler khi sparsity quan trọng.
Pitfall: outlier kéo lệch min / max
Điểm yếu lớn nhất của Min-Max: chỉ dùng 2 con số (min và max) để định nghĩa toàn bộ biến đổi. Một outlier duy nhất ở 2 đầu có thể phá toàn bộ scaling.
Ví dụ giá nhà trong một quận, 99% căn nằm trong khoảng 1–5 tỷ, nhưng có 1 biệt thự 100 tỷ:
import numpy as np
from sklearn.preprocessing import MinMaxScaler
prices = np.array([1, 2, 3, 4, 5, 100]).reshape(-1, 1) # tỷ VND
scaler = MinMaxScaler()
scaled = scaler.fit_transform(prices)
print(scaled.ravel())
# [0. 0.0101 0.0202 0.0303 0.0404 1. ]
99% data bị co dồn về khoảng [0, 0.04] — gần như không phân biệt được giữa căn 1 tỷ và căn 5 tỷ. Outlier 100 tỷ độc chiếm phần range còn lại. Với KNN hay K-Means, khoảng cách giữa các căn "thường" gần như bằng 0; model sẽ coi chúng đều như nhau.
Cách khắc phục:
- Xử lý outlier trước — Bài 12 (IQR / Z-score) sẽ học. Cắt hoặc loại bỏ giá trị bất thường rồi mới scale.
- Dùng Standardization (Z-score, Bài 8) — vẫn nhạy với outlier nhưng ít hơn vì dùng
meanvàstd(phân tán đều) thay vì 2 đầu cực. - Dùng RobustScaler — dùng median và IQR thay vì min/max và mean/std. Sẽ đề cập ở Bài 12.
- Log transform — với feature có phân phối lệch nặng (giá, thu nhập, dân số),
log(x)nén đuôi dài về kích thước hợp lý trước khi scale.
Quy tắc thực dụng: Min-Max chỉ dùng khi feature đã sạch outlier hoặc có range biết trước (pixel 0–255, percentage 0–100, score 0–1).
Use case typical
Các tình huống Min-Max là lựa chọn tự nhiên (hợp lý hơn Standardization):
- Image pixel — ảnh grayscale / RGB có range cố định
[0, 255]. Chia cho 255 (đúngMinMaxScalervớidata_min_ = 0, data_max_ = 255) đưa về[0, 1]— input chuẩn cho CNN. - Sigmoid / tanh activation — sigmoid output trong
(0, 1), tanh trong(-1, 1). Input scale về cùng range giữ activation trong vùng nhạy (gradient không bị saturated). - Feature có range biết trước — percentage (0–100), age sinh học (0–120), score game (0–10).
- Khi muốn giữ ý nghĩa "0 = nhỏ nhất, 1 = lớn nhất" — dễ giải thích cho người dùng, dễ visualize trên heatmap.
Ngược lại, khi feature có phân phối gần Gaussian, hoặc khi không biết range tối đa (ví dụ số lượt truy cập website / ngày), Standardization (Bài 8) thường an toàn hơn.
Code Python end-to-end
Scale một feature đơn giản
import numpy as np
from sklearn.preprocessing import MinMaxScaler
ages = np.array([18, 25, 30, 45, 65]).reshape(-1, 1)
scaler = MinMaxScaler()
ages_scaled = scaler.fit_transform(ages)
print(ages_scaled.ravel())
# [0. 0.149 0.255 0.574 1. ]
# 18 → 0, 65 → 1, các giá trị giữa nội suy tuyến tính
Scale ma trận nhiều feature
import numpy as np
from sklearn.preprocessing import MinMaxScaler
X = np.array([
[25, 50_000_000, 2],
[30, 75_000_000, 5],
[45, 30_000_000, 20],
[60, 90_000_000, 35],
]) # age, salary, years_exp
scaler = MinMaxScaler()
X_scaled = scaler.fit_transform(X)
print(X_scaled.round(3))
# [[0. 0.333 0. ]
# [0.143 0.75 0.091]
# [0.571 0. 0.545]
# [1. 1. 1. ]]
Demo inverse_transform
X_back = scaler.inverse_transform(X_scaled)
print(X_back.round(0))
# [[2.5e+01 5.0e+07 2.0e+00]
# [3.0e+01 7.5e+07 5.0e+00]
# [4.5e+01 3.0e+07 2.0e+01]
# [6.0e+01 9.0e+07 3.5e+01]]
# Khớp với X ban đầu
Demo bug khi fit lại trên test
import numpy as np
from sklearn.preprocessing import MinMaxScaler
X_train = np.array([[1.0], [2.0], [3.0], [4.0], [5.0]])
X_test = np.array([[2.0], [4.0], [6.0]]) # 6 vượt max train
# Đúng: dùng cùng scaler
scaler = MinMaxScaler()
scaler.fit(X_train)
print("train_min, train_max =", scaler.data_min_, scaler.data_max_)
print("test transform đúng :", scaler.transform(X_test).ravel())
# [0.25 0.75 1.25] ← 1.25 vượt 1, nhưng đúng — phản ánh test có giá trị mới
# Sai: fit lại trên test
scaler_bad = MinMaxScaler()
print("test transform sai :", scaler_bad.fit_transform(X_test).ravel())
# [0. 0.5 1. ] ← bị "ép" về [0,1] với min/max khác → train/test không cùng scale
Hai output cuối khác nhau dù cùng input. Khi đưa vào model đã train trên train scaled, version "sai" cho kết quả vô nghĩa vì giá trị test đã bị mapping bằng một hàm khác hẳn.
Bài tập
- Load iris bằng
load_iris(return_X_y=True). ÁpMinMaxScaler()cho toàn bộ X. VerifyX_scaled.min(axis=0)đều bằng 0 vàX_scaled.max(axis=0)đều bằng 1. - Lặp lại bài 1 nhưng dùng
MinMaxScaler(feature_range=(-1, 1)). Verify min/max của mỗi cột. - Chia iris thành train/test với
test_size=0.2, random_state=42. Fit scaler trên train, transform cả 2. InX_test_scaled.min(axis=0)vàX_test_scaled.max(axis=0)— quan sát có cột nào âm hoặc > 1 không, giải thích. - Cho
data = np.array([1, 2, 3, 4, 1000]).reshape(-1, 1). Scale bằngMinMaxScaler. Quan sát giá trị sau scaling của 4 số đầu — chúng có còn phân biệt được không? Liên hệ pitfall mục 11. - Scale lại data ở bài 4 sau khi loại bỏ outlier 1000 (dùng
data[data < 100]). So sánh khoảng cách giữa 1 và 4 trong 2 trường hợp.
Đáp án ngắn
X_scaled.min(axis=0)→[0. 0. 0. 0.],X_scaled.max(axis=0)→[1. 1. 1. 1.].- Min →
[-1. -1. -1. -1.], max →[1. 1. 1. 1.]. - Một số cột test có min < 0 hoặc max > 1 vì sample test chứa giá trị nhỏ hơn min hoặc lớn hơn max của train. Đây là hành vi đúng — phản ánh test có giá trị "ngoài kinh nghiệm" của train.
- Sau scaling:
[0.000, 0.001, 0.002, 0.003, 1.000]. 4 giá trị đầu gần như dính vào nhau, mất khả năng phân biệt — đúng pitfall outlier. - Loại 1000, scale
[1, 2, 3, 4]→[0, 0.333, 0.667, 1.0]. Khoảng cách giữa 1 và 4 là 1.0; ở bài 4 chỉ là 0.003. Sau khi loại outlier, model phân biệt được giữa các sample "thường".
Tóm tắt
- Feature scaling cần thiết khi các feature có range chênh lệch lớn — feature lớn sẽ áp đảo feature nhỏ trong gradient, khoảng cách, margin, variance.
- Cần scaling: Linear / Logistic Regression (với L1/L2), SVM, KNN, K-Means, PCA, Neural Network.
- Không cần scaling: Decision Tree, Random Forest, Gradient Boosting (XGBoost, LightGBM, CatBoost) — split dựa trên threshold, không nhạy với scale.
- Công thức Min-Max: \( x' = (x - x_{\min}) / (x_{\max} - x_{\min}) \). Kết quả \( x' \in [0, 1] \). Tổng quát về \([a, b]\): \( x' = a + (x - x_{\min})(b - a) / (x_{\max} - x_{\min}) \).
MinMaxScalertrongsklearn.preprocessing:fit_transformtrên train,transformtrên test.feature_rangeđổi đoạn đích.- Quy tắc vàng:
fitchỉ chạy trên train. Fit trên toàn bộ data hoặc fit lại trên test → data leak. Test có thể vượt[0, 1]sau scaling — bình thường. inverse_transformđảo ngược về scale gốc — hữu ích khi scale luôn target trong regression.MaxAbsScalerchia cho|x_max|, range[-1, 1], giữ sparsity (không trừ gì) — dùng cho TF-IDF, bag-of-words, ma trận thưa.- Pitfall lớn nhất: outlier kéo lệch min/max → 99% data co dồn về một góc nhỏ. Min-Max chỉ an toàn khi feature đã sạch outlier hoặc có range biết trước.
- Use case typical: image pixel (0–255 → 0–1), sigmoid/tanh activation, feature có range cố định. Khi không chắc chắn, cân nhắc Standardization (Bài 8).
