Bỏ qua để đến nội dung

Xử lý dữ liệu, tăng cường dữ liệu, dữ liệu mất cân bằng

Sau bài này, bạn sẽ:

  • Xử lý được ba vấn đề hay gặp của bảng dữ liệu thật: giá trị thiếu (missing value), biến phân loại (categorical variable) và đặc trưng lệch thang đo, bằng SimpleImputer, OneHotEncoder, StandardScaler gộp trong ColumnTransformer.
  • Hiểu vì sao mọi bước xử lý phải học chỉ từ tập train, và nhận ra rò rỉ dữ liệu (data leakage) khi bước xử lý được khớp trên toàn bộ dữ liệu.
  • Biết tăng cường dữ liệu (data augmentation) là gì và khi nào dùng được.
  • Xử lý được dữ liệu mất cân bằng (imbalanced data): đo bằng precision, recall, F1, chia bằng stratify, và chọn giữa trọng số lớp, lấy mẫu lại, dời ngưỡng.

Bạn muốn đoán giá thuê căn hộ. Lần này dữ liệu không phải bảng gọn gàng như bài Hồi quy tuyến tính, mà là 8 căn hộ do người thuê tự điền qua một biểu mẫu:

CănDiện tích (m²)TầngQuậnBan công
1305Cầu Giấycó
245trốngBa Đìnhkhông
3trống12Hoàng Maicó
4603Cầu Giấytrống
5528Hoàng Maikhông
6382Ba Đìnhcó
77515trốngcó
8416Cầu Giấykhông

Bảng này có ba chỗ mà mô hình chưa nuốt được:

  • Ô trống. Mô hình tuyến tính tính w⊤x+b\mathbf{w}^\top\mathbf{x} + b. Một ô trống trong x\mathbf{x} làm phép tính đó vô nghĩa, và scikit-learn sẽ báo lỗi.
  • Cột chữ. “Cầu Giấy” không nhân được với trọng số. Phải đổi chữ thành số, nhưng đổi thế nào cho khỏi bịa ra quan hệ không có thật?
  • Thang đo lệch nhau. Diện tích lên tới hàng chục, tầng chỉ vài đơn vị, còn cột “có/không” sẽ thành 0 và 1. Bài Gradient Descent đã cho thấy thang đo lệch làm GD chậm hẳn.

Một bài toán khác: đoán giao dịch thẻ nào là gian lận. Cứ 100 giao dịch chỉ có khoảng 5 gian lận. Một mô hình lười, luôn nói “không gian lận”, đúng tới 95%. Như bài Đánh giá mô hình: Accuracy, Precision, Recall, F1 đã chỉ ra, accuracy cao mà mô hình vô dụng, vì nó không bắt được giao dịch gian lận nào.

Còn khi dữ liệu quá ít, ví dụ vài trăm ảnh, bạn có thể tự tạo thêm mẫu: một tấm ảnh mèo lật ngang vẫn là ảnh mèo.

Bài toán của bài này là: biến một bảng dữ liệu thật (có ô trống, có cột chữ, thang đo lệch, lớp hiếm) thành đầu vào mà mô hình học được, mà không để thông tin của tập kiểm tra lọt vào quá trình học.

Bỏ đi. Cách đơn giản nhất là bỏ mọi dòng có ô trống (df.dropna()). Ở bảng trên, bốn ô trống nằm ở bốn căn khác nhau, nên bỏ dòng là mất một nửa dữ liệu. Bỏ cả cột chỉ hợp lý khi cột đó thiếu gần hết. Bỏ dòng còn nguy hiểm ở chỗ khác: nếu người thuê căn rẻ hay bỏ trống ô diện tích, phần còn lại không còn đại diện cho dữ liệu thật.

Điền vào (impute). Thay ô trống bằng một giá trị tính từ cột đó:

  • Trung bình (mean) hoặc trung vị (median) cho cột số. Diện tích của 7 căn có dữ liệu có trung bình 341/7≈48.71341/7 \approx 48.71 và trung vị 4545. Trung vị ít bị kéo bởi điểm ngoại lai (outlier): thay căn 75 m² bằng một biệt thự 500 m², trung bình nhảy lên khoảng 109, còn trung vị vẫn là 45.
  • Giá trị hay gặp nhất (mode) cho cột phân loại. Cột quận có Cầu Giấy 3 lần, nên căn 7 được điền Cầu Giấy.

Thêm cờ đánh dấu thiếu. Bản thân việc thiếu có thể mang thông tin: có thể chủ nhà cố ý giấu diện tích của căn quá nhỏ. Vì vậy, ngoài việc điền, người ta thường thêm một cột 0/1 (missing indicator) cho biết ô đó vốn bị trống. Mô hình tự học xem cờ này có đáng tin không.

Cách nghĩ ngay ra là đánh số: Ba Đình = 0, Cầu Giấy = 1, Hoàng Mai = 2, gọi là mã hóa thứ tự (ordinal encoding). Với mô hình tuyến tính, phần đóng góp của cột quận vào giá là w⋅ma˜w \cdot \text{mã}:

Ba Đıˋnh: 0,Caˆˋu Giaˆˊy: w,Hoaˋng Mai: 2w\text{Ba Đình: } 0, \qquad \text{Cầu Giấy: } w, \qquad \text{Hoàng Mai: } 2w

Mô hình bị ép tin rằng ba quận cách đều nhau theo đúng thứ tự bạn tình cờ đặt, và Cầu Giấy luôn nằm “giữa” hai quận kia. Đổi thứ tự đánh số là đổi mô hình, dù dữ liệu không đổi gì. Đó là bẫy.

One-hot encoding thay một cột quận bằng ba cột 0/1, mỗi quận một cột. Mỗi căn có đúng một số 1:

CănQuậnquận Ba Đìnhquận Cầu Giấyquận Hoàng Mai
1Cầu Giấy010
2Ba Đình100
3Hoàng Mai001

Giờ mỗi quận có trọng số riêng wBĐw_{\text{BĐ}}, wCGw_{\text{CG}}, wHMw_{\text{HM}}, và mô hình tự học xem quận nào đắt hơn bao nhiêu. Giá phải trả là số cột: cột có 500 giá trị khác nhau thành 500 cột.

Mã hóa thứ tự chỉ đúng khi các giá trị có thứ tự thật, ví dụ nội thất “không, cơ bản, đầy đủ” (OrdinalEncoder). Cây quyết định (bài Decision Tree và Random Forest) cũng chịu được mã thứ tự tốt hơn mô hình tuyến tính, vì nó chỉ hỏi “mã có nhỏ hơn ngưỡng không”.

Một chi tiết khi dùng thật: tập test có thể có quận chưa từng xuất hiện ở tập train. OneHotEncoder(handle_unknown="ignore") khi đó cho cả ba cột bằng 0 thay vì báo lỗi.

Hai cách hay dùng, cho đặc trưng thứ jj:

z-score: zj=xj−μjσjmin-max: x~j=xj−min⁡jmax⁡j−min⁡j\text{z-score: } z_j = \frac{x_j - \mu_j}{\sigma_j} \qquad\qquad \text{min-max: } \tilde{x}_j = \frac{x_j - \min_j}{\max_j - \min_j}

Chuẩn hóa z-score (standardization, StandardScaler) trừ trung bình μj\mu_j và chia cho độ lệch chuẩn σj\sigma_j, nên cột mới có trung bình 0, độ lệch chuẩn 1. Chuẩn hóa min-max (MinMaxScaler) đưa cột về đoạn [0,1][0, 1], như bài Gradient Descent. Min-max nhạy với điểm ngoại lai hơn: một căn 500 m² làm mọi căn còn lại dồn về gần 0.

Mô hình nào cần co giãn?

  • KNN (bài K-Nearest Neighbors): khoảng cách cộng bình phương chênh lệch của từng cột, nên cột có số lớn nhất quyết định ai là láng giềng.
  • Gradient descent: thang đo lệch làm mặt mất mát thành thung lũng hẹp, GD chậm hoặc phân kỳ.
  • Mô hình có phạt (bài Ridge, Lasso, Elastic Net): khoản phạt đối xử mọi wjw_j như nhau, nên độ lớn của wjw_j không được phụ thuộc vào đơn vị.

Mô hình không cần: cây quyết định và rừng ngẫu nhiên. Cây chỉ chia mẫu theo câu hỏi kiểu “diện tích ≤50\le 50?”. Sau z-score, câu hỏi thành ”z≤(50−μ)/σz \le (50 - \mu)/\sigma?”, vẫn chia đúng những căn đó, nên cây không đổi.

Điểm ngoại lai. Trước khi xử lý, hãy xem điểm đó là lỗi nhập liệu (diện tích 4500 m² vì gõ thừa số 0) hay là thật (một căn penthouse). Lỗi thì sửa hoặc bỏ. Thật thì giữ, và cân nhắc trung vị khi điền, hay RobustScaler (co giãn theo trung vị và khoảng tứ phân vị) thay vì min-max. Bài Hồi quy tuyến tính có ví dụ một điểm ngoại lai kéo lệch cả đường thẳng.

Để ý rằng mọi bước ở trên đều học một thứ gì đó từ dữ liệu: trung vị để điền, danh sách quận để one-hot, μj\mu_j và σj\sigma_j để chuẩn hóa. Theo đúng tinh thần của bài Tổng quan về Học máy, những thứ đó phải học chỉ từ tập train. Quy tắc của scikit-learn:

  • Trên tập train: fit (học tham số xử lý) rồi transform, viết gọn là fit_transform.
  • Trên tập test và dữ liệu mới: chỉ transform, dùng lại đúng tham số đã học.

Nếu bạn fit bộ chuẩn hóa trên toàn bộ dữ liệu rồi mới chia, μj\mu_j đã “thấy” tập test. Với chuẩn hóa, sai lệch thường nhỏ. Nhưng với bước dùng cả nhãn, như chọn những đặc trưng tương quan với nhãn mạnh nhất, sai lệch có thể khổng lồ: ô code thứ hai cho thấy một mô hình đạt 85% trên dữ liệu hoàn toàn ngẫu nhiên.

Kiểm định chéo (cross-validation) làm việc này khó hơn, vì mỗi fold có tập train riêng, nên phải khớp lại bộ xử lý ở mỗi fold. Pipeline lo việc đó: nó nối các bước xử lý với mô hình thành một khối có fit và predict. Khi cross_val_score khớp pipeline trên một fold, mọi bước bên trong chỉ học từ phần train của fold ấy. ColumnTransformer cho phép mỗi nhóm cột một chuỗi xử lý riêng (cột số thì điền trung vị và chuẩn hóa, cột chữ thì điền mode và one-hot), rồi ghép kết quả lại.

Tăng cường dữ liệu là tạo thêm mẫu train bằng cách biến đổi mẫu có sẵn theo những cách không làm đổi nhãn. Mô hình học được rằng các biến đổi đó không quan trọng, nên ít học thuộc chi tiết vặt của từng mẫu.

  • Ảnh: lật ngang, xoay nhẹ, cắt một phần, đổi độ sáng. Phải chọn biến đổi hợp với bài toán: lật ngang ảnh mèo vẫn là mèo, nhưng xoay ngược chữ số 6 thành 9 thì nhãn sai. Phần Thị giác máy tính sẽ nói kỹ.
  • Dữ liệu bảng: cộng nhiễu nhỏ vào cột số, ví dụ xj+εx_j + \varepsilon với ε\varepsilon ngẫu nhiên nhỏ so với σj\sigma_j. Cách này thường kém hiệu quả hơn ở ảnh, vì khó biết biến đổi nào giữ nguyên nhãn.
  • Văn bản ngắn: thay vài từ bằng từ đồng nghĩa, hoặc dịch sang ngôn ngữ khác rồi dịch ngược lại (back-translation).

Chỉ tăng cường tập train, và chỉ sau khi đã chia. Bản biến đổi của một ảnh train mà lọt vào tập test thì gần như là mẫu trùng.

Đo cho đúng. Accuracy bị lớp đông lấn át, nên hãy nhìn precision, recall và F1 của lớp hiếm (bài Đánh giá mô hình: Accuracy, Precision, Recall, F1). Khi chia train/test, dùng train_test_split(..., stratify=y) để hai tập giữ đúng tỉ lệ lớp. Không có stratify, với 100 mẫu chỉ 5 mẫu hiếm, tập test có thể rơi vào cảnh chỉ có 0 hay 1 mẫu hiếm.

Mô hình học từ dữ liệu lệch thường đẩy lớp hiếm sang một góc: sai cả 5 mẫu hiếm vẫn rẻ hơn sai vài trong 95 mẫu đông. Có ba nhóm cách chữa.

1. Trọng số lớp (class weight). Nhân mất mát của mỗi mẫu với trọng số của lớp nó. Gọi ℓ(i)\ell^{(i)} là mất mát của mẫu thứ ii, scs_c là trọng số của lớp cc:

L=1m∑i=1msy(i) ℓ(i),sc=mK mc\mathcal{L} = \frac{1}{m}\sum_{i=1}^{m} s_{y^{(i)}}\,\ell^{(i)}, \qquad s_c = \frac{m}{K\,m_c}

với KK là số lớp và mcm_c là số mẫu của lớp cc. Công thức của scs_c là chế độ class_weight="balanced" của scikit-learn. Nhân scs_c với mcm_c được m/Km/K cho mọi lớp: mỗi lớp góp cùng một tổng trọng số, dù đông hay hiếm. Ở ô code thứ ba, tập train có m=1400m = 1400, m1=74m_1 = 74, m0=1326m_0 = 1326, nên:

s1=14002⋅74≈9.46,s0=14002⋅1326≈0.53s_1 = \frac{1400}{2 \cdot 74} \approx 9.46, \qquad s_0 = \frac{1400}{2 \cdot 1326} \approx 0.53

Mỗi giao dịch gian lận nặng gần 18 lần một giao dịch thường, đúng bằng 1326/741326/74.

2. Lấy mẫu lại (resampling) tập train.

  • Oversampling ngẫu nhiên: bốc lặp lại (có hoàn lại) các mẫu lớp hiếm cho tới khi đủ số. Một mẫu xuất hiện kk lần góp k ℓ(i)k\,\ell^{(i)} vào tổng mất mát, giống hệt trọng số kk. Vì vậy oversampling ngẫu nhiên gần như tương đương trọng số lớp, chỉ tốn bộ nhớ hơn.
  • Undersampling: bỏ bớt mẫu lớp đông. Nhanh, nhưng vứt đi dữ liệu thật; hợp khi lớp đông có hàng triệu mẫu.
  • SMOTE (Chawla và cộng sự, 2002): thay vì chép nguyên, tạo mẫu hiếm mới nằm giữa một mẫu hiếm x(i)\mathbf{x}^{(i)} và một láng giềng gần của nó (cũng thuộc lớp hiếm) x(z)\mathbf{x}^{(z)}:
xmới=x(i)+u(x(z)−x(i)),u∈[0,1] ngaˆ˜u nhieˆn\mathbf{x}_{\text{mới}} = \mathbf{x}^{(i)} + u\left(\mathbf{x}^{(z)} - \mathbf{x}^{(i)}\right), \qquad u \in [0, 1] \text{ ngẫu nhiên}

Với x(i)=(1,2)\mathbf{x}^{(i)} = (1, 2), x(z)=(3,6)\mathbf{x}^{(z)} = (3, 6) và u=0.5u = 0.5, mẫu mới là (2,4)(2, 4), trung điểm của đoạn nối hai mẫu. SMOTE có trong thư viện imbalanced-learn; thư viện này không có sẵn trong bộ gói Pyodide mà trang dùng, nên ô code của bài tự viết oversampling ngẫu nhiên bằng NumPy.

3. Dời ngưỡng quyết định. Giữ nguyên mô hình, chỉ hạ ngưỡng tt: dự đoán lớp hiếm khi xác suất p≥tp \ge t, với tt nhỏ hơn 0.5. Recall tăng, precision thường giảm. Ngưỡng là siêu tham số, chọn trên tập kiểm định như bài Đánh giá mô hình: Accuracy, Precision, Recall, F1.

Như tăng cường dữ liệu, mọi cách lấy mẫu lại chỉ áp dụng trên tập train. Tập test phải giữ đúng tỉ lệ thật, vì đó là tỉ lệ mô hình sẽ gặp khi dùng.

  • Không có công thức xử lý chung. Điền trung vị hay thêm cờ, giữ hay bỏ điểm ngoại lai: tùy dữ liệu và mô hình. Hãy so các lựa chọn bằng kiểm định chéo, với mọi bước nằm trong pipeline. Bài Dự án ML từ đầu đến cuối làm điều này trên một bộ dữ liệu trọn vẹn.
  • Không cách chữa mất cân bằng nào tạo ra thông tin mới. Trọng số lớp, oversampling hay dời ngưỡng chủ yếu đổi chỗ đánh đổi giữa precision và recall (ô code thứ ba). Muốn tốt hơn thật thì cần thêm dữ liệu lớp hiếm hoặc đặc trưng tốt hơn.
  • Tăng cường dữ liệu dựa vào hiểu biết về bài toán. Biến đổi sai làm đổi nhãn, và mô hình học điều sai.

Bảng dưới gom mọi bước của bài: mỗi bước học gì từ tập train, dùng lớp nào của scikit-learn, và mô hình nào cần nó. Cột thứ hai là danh sách những thứ không được học từ tập test.

BướcHọc gì từ tập trainscikit-learnAi cần
Điền cột sốtrung bình hoặc trung vịSimpleImputer(strategy="median")mọi mô hình không nhận NaN
Cờ đánh dấu thiếucột nào có ô trốngMissingIndicator, hoặc add_indicator=Truekhi việc thiếu có thể mang thông tin
Điền cột phân loạigiá trị hay gặp nhấtSimpleImputer(strategy="most_frequent")mọi mô hình
One-hotdanh sách giá trị của cộtOneHotEncoder(handle_unknown="ignore")mô hình tuyến tính, KNN, mạng nơ-ron
Chuẩn hóaμj\mu_j, σj\sigma_j (hoặc min, max)StandardScaler, MinMaxScalerKNN, GD, mô hình có phạt; cây thì không
Chọn đặc trưngđặc trưng nào liên quan tới nhãnSelectKBestkhi có rất nhiều đặc trưng
Trọng số lớpsố mẫu mỗi lớpclass_weight="balanced"dữ liệu mất cân bằng
Lấy mẫu lại, tăng cườngmẫu nào để nhân bản, biến đổitự viết, hoặc imbalanced-learndữ liệu mất cân bằng, dữ liệu ít

Ba ô dưới đây chạy độc lập. Ô chạy đầu tiên lâu hơn, vì trình duyệt phải tải thêm scikit-learn (và pandas ở ô thứ nhất).

Ô đầu tiên xử lý đúng bảng ở mục Trực giác (tên quận viết không dấu). Cột số được điền trung vị rồi chuẩn hóa z-score; hai cột cờ đánh dấu ô số nào bị trống; cột phân loại được điền mode rồi one-hot.

Điền, one-hot, chuẩn hóa

import numpy as np
import pandas as pd
from sklearn.compose import ColumnTransformer
from sklearn.impute import MissingIndicator, SimpleImputer
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import OneHotEncoder, StandardScaler
# 8 căn hộ tự tạo; np.nan và None là ô bị thiếu.
df = pd.DataFrame({
"area": [30, 45, np.nan, 60, 52, 38, 75, 41],
"floor": [5, np.nan, 12, 3, 8, 2, 15, 6],
"district": ["Cau Giay", "Ba Dinh", "Hoang Mai", "Cau Giay",
"Hoang Mai", "Ba Dinh", None, "Cau Giay"],
"balcony": ["yes", "no", "yes", None, "no", "yes", "yes", "no"],
})
print(df.isna().sum().to_dict()) # số ô thiếu mỗi cột
# Cột số: điền trung vị rồi chuẩn hóa z-score.
num = make_pipeline(SimpleImputer(strategy="median"), StandardScaler())
# Cột phân loại: điền giá trị hay gặp nhất (mode) rồi one-hot.
cat = make_pipeline(SimpleImputer(strategy="most_frequent"),
OneHotEncoder(handle_unknown="ignore"))
prep = ColumnTransformer([
("num", num, ["area", "floor"]),
("miss", MissingIndicator(), ["area", "floor"]), # cờ 1 = ô bị thiếu
("cat", cat, ["district", "balcony"]),
], verbose_feature_names_out=False)
X = prep.fit_transform(df) # học trung vị, mode, mu, sigma rồi biến đổi
out = pd.DataFrame(X, columns=prep.get_feature_names_out())
out.columns = [c.replace("missingindicator_", "miss_") for c in out]
pd.set_option("display.width", 200)
pd.set_option("display.max_columns", None)
print(out.round(2))
sc = prep.named_transformers_["num"][-1]
print("mu =", sc.mean_.round(2), " sigma =", sc.scale_.round(2))

Lần chạy đầu cần tải Python, khoảng 10 MB.

Mỗi cột thiếu đúng một ô. Bảng sau xử lý có 9 cột, toàn số, không còn ô trống:

  • Căn 3 (dòng có chỉ số 2) bị trống diện tích, được điền trung vị 45 m², nên có area =−0.25= -0.25, giống hệt căn 2, căn thật sự rộng 45 m². Chỉ cột miss_area =1= 1 phân biệt hai căn.
  • Căn 7 (dòng 6) thiếu quận, được điền Cầu Giấy, giá trị hay gặp nhất. Căn 4 thiếu ban công, được điền “có”.
  • Dòng cuối là tham số chuẩn hóa: μ=48.25\mu = 48.25 m² và 7.127.12 tầng, σ=13.13\sigma = 13.13 và 4.144.14. μ\mu của diện tích tính sau khi điền, nên là (341+45)/8=48.25(341 + 45)/8 = 48.25, không phải 48.7148.71.

Đây chính là các tham số mà prep.transform(df_test) sẽ dùng lại cho căn hộ mới.

Rò rỉ dữ liệu: chọn đặc trưng trước khi chia

Phần tiêu đề “Rò rỉ dữ liệu: chọn đặc trưng trước khi chia”

Ô thứ hai dựng một bộ dữ liệu không có gì để học: 100 mẫu, 5000 đặc trưng ngẫu nhiên, nhãn tung đồng xu. Mô hình trung thực chỉ đoán đúng khoảng 50%. Ô so hai cách chọn 20 đặc trưng tương quan với nhãn mạnh nhất (SelectKBest), rồi đo accuracy bằng kiểm định chéo 5 fold.

Chọn đặc trưng ngoài và trong Pipeline

import numpy as np
from sklearn.feature_selection import SelectKBest, f_classif
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import cross_val_score
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import StandardScaler
# Nhiễu thuần: 100 mẫu, 5000 đặc trưng ngẫu nhiên, nhãn tung đồng xu.
# Không có quy luật nào để học: mô hình trung thực chỉ đúng khoảng 50%.
rng = np.random.default_rng(0)
X = rng.normal(size=(100, 5000))
y = rng.integers(0, 2, size=100)
# SAI: chọn 20 đặc trưng "liên quan nhất" trên TOÀN BỘ dữ liệu, rồi mới
# kiểm định chéo. Bước chọn đã nhìn thấy nhãn của các fold kiểm định.
X_sel = SelectKBest(f_classif, k=20).fit_transform(X, y)
model = make_pipeline(StandardScaler(), LogisticRegression())
leak = cross_val_score(model, X_sel, y, cv=5)
print(f"select before split: {leak.mean():.3f}")
# ĐÚNG: đặt bước chọn vào Pipeline; mỗi fold chỉ chọn trên phần train.
pipe = make_pipeline(SelectKBest(f_classif, k=20), StandardScaler(),
LogisticRegression())
honest = cross_val_score(pipe, X, y, cv=5)
print(f"select inside pipeline: {honest.mean():.3f}")

Lần chạy đầu cần tải Python, khoảng 10 MB.

Chọn trên toàn bộ dữ liệu rồi mới kiểm định chéo cho accuracy 0.850, trên dữ liệu ngẫu nhiên. Lý do: trong 5000 cột ngẫu nhiên, thế nào cũng có những cột tình cờ khớp với 100 nhãn. Bước chọn đã nhìn thấy nhãn của cả các fold kiểm định, nên giữ lại đúng những cột khớp tình cờ với chúng. Đặt bước chọn vào Pipeline, mỗi fold tự chọn trên phần train của mình, và những cột đó không còn khớp với phần kiểm định: accuracy về 0.480, đúng mức đoán bừa.

Ví dụ này là kinh điển (sách The Elements of Statistical Learning gọi nó là cách sai và cách đúng để kiểm định chéo). Nó hay xảy ra với dữ liệu ít mẫu mà nhiều đặc trưng, như dữ liệu gen.

Mất cân bằng: bốn cách trên cùng dữ liệu

Phần tiêu đề “Mất cân bằng: bốn cách trên cùng dữ liệu”

Ô thứ ba sinh 2000 mẫu bằng make_classification, lớp 1 (coi như giao dịch gian lận) chiếm khoảng 5%. Chia 70/30 có stratify, rồi so bốn cách dùng hồi quy logistic (bài Hồi quy Logistic): mặc định; mặc định nhưng hạ ngưỡng xuống 0.2; class_weight="balanced"; và oversampling ngẫu nhiên tự viết. Mọi chỉ số tính trên tập test, cho lớp 1.

Trọng số lớp, oversampling, dời ngưỡng

import numpy as np
from sklearn.datasets import make_classification
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import accuracy_score, precision_recall_fscore_support
from sklearn.model_selection import train_test_split
# 2000 mẫu, lớp 1 (thiểu số) chỉ chiếm khoảng 5%.
X, y = make_classification(n_samples=2000, n_features=5, n_informative=2,
n_redundant=0, weights=[0.95, 0.05],
random_state=2)
# stratify=y: tập train và test giữ cùng tỉ lệ hai lớp.
X_tr, X_te, y_tr, y_te = train_test_split(X, y, test_size=0.3,
stratify=y, random_state=0)
print(f"positives: train {y_tr.sum()}/{len(y_tr)}, "
f"test {y_te.sum()}/{len(y_te)}")
# Oversampling ngẫu nhiên, CHỈ trên tập train: bốc lặp lại các mẫu
# lớp 1 cho tới khi bằng số mẫu lớp 0.
rng = np.random.default_rng(0)
pos, neg = np.where(y_tr == 1)[0], np.where(y_tr == 0)[0]
extra = rng.choice(pos, size=len(neg) - len(pos), replace=True)
idx = np.concatenate([neg, pos, extra])
X_os, y_os = X_tr[idx], y_tr[idx]
def report(name, model, X_fit, y_fit, threshold=0.5):
model.fit(X_fit, y_fit)
p = model.predict_proba(X_te)[:, 1] # xác suất thuộc lớp 1
y_hat = (p >= threshold).astype(int)
pr, rc, f1, _ = precision_recall_fscore_support(
y_te, y_hat, average="binary", zero_division=0)
acc = accuracy_score(y_te, y_hat)
print(f"{name:<14} acc={acc:.3f} P={pr:.3f} R={rc:.3f} F1={f1:.3f}")
# Ngưỡng 0.2 đặt sẵn để minh họa; làm thật thì chọn trên tập kiểm định.
report("plain", LogisticRegression(), X_tr, y_tr)
report("threshold 0.2", LogisticRegression(), X_tr, y_tr, threshold=0.2)
report("balanced", LogisticRegression(class_weight="balanced"), X_tr, y_tr)
report("oversample", LogisticRegression(), X_os, y_os)

Lần chạy đầu cần tải Python, khoảng 10 MB.

Tập train có 74 mẫu lớp 1 trên 1400, tập test 31 trên 600: cùng khoảng 5% nhờ stratify. Mô hình luôn đoán lớp 0 đã có accuracy 569/600≈0.948569/600 \approx 0.948, nên đừng nhìn cột acc.

  • Mặc định: accuracy 0.968 trông rất đẹp, nhưng recall chỉ 0.516: bỏ sót gần nửa số giao dịch gian lận (bắt được 16 trên 31).
  • class_weight="balanced": recall lên 0.903 (28 trên 31), nhưng precision rơi từ 0.800 xuống 0.509: mô hình báo 55 giao dịch, chỉ 28 là gian lận thật. F1 nhích từ 0.627 lên 0.651.
  • Oversampling ngẫu nhiên cho đúng cùng các số với balanced, như lý thuyết đã dự đoán: nhân bản mẫu cũng là nhân trọng số.
  • Hạ ngưỡng xuống 0.2 ở đây cho F1 tốt nhất, 0.788 (recall 0.839, precision 0.743). Trọng số balanced đẩy mô hình đi quá xa về phía recall; hạ ngưỡng vừa phải thì giữ được precision.

Không có cách nào thắng tuyệt đối: chọn cách nào tùy bỏ sót hay báo nhầm đắt hơn. Ngưỡng 0.2 được đặt sẵn cho ví dụ. Làm thật, bạn phải chọn ngưỡng trên tập kiểm định, không phải nhìn tập test như ở đây.

Câu 1/4 Bộ dữ liệu có 1000 giao dịch, 20 gian lận. Bạn nhân bản mỗi giao dịch gian lận 10 lần, rồi mới chia train/test. Recall trên tập test lên tới 0.98. Vì sao con số này đáng ngờ?
Câu 2/4 Bạn đổi cột diện tích từ m² sang dm² (nhân 100), không chuẩn hóa, và giữ nguyên các cột khác. Dự đoán của mô hình nào gần như chắc chắn thay đổi?
Câu 3/4 Cột "màu xe" có ba giá trị đỏ, xanh, trắng. Bạn mã hóa đỏ = 0, xanh = 1, trắng = 2 rồi đưa vào hồi quy tuyến tính. Mô hình đang ngầm giả định điều gì?
Câu 4/4 Tập train có 1000 mẫu, 20 mẫu lớp 1 và 980 mẫu lớp 0. Với class_weight='balanced', trọng số của mỗi mẫu lớp 1 gấp bao nhiêu lần mỗi mẫu lớp 0?

Ý chính:

  • Giá trị thiếu: bỏ dòng thì mất dữ liệu; điền trung vị (cột số) hoặc mode (cột phân loại), thêm cờ đánh dấu thiếu khi việc thiếu có thể mang thông tin. Biến phân loại không thứ tự dùng one-hot, không gán 0, 1, 2.
  • Chuẩn hóa z-score zj=(xj−μj)/σjz_j = (x_j - \mu_j)/\sigma_j hoặc min-max cho KNN, gradient descent, mô hình có phạt; cây quyết định không cần.
  • Mọi bước học từ dữ liệu (trung vị, danh sách giá trị, μj\mu_j, σj\sigma_j, đặc trưng được chọn) chỉ học từ tập train. Pipeline và ColumnTransformer bảo đảm điều đó, kể cả trong kiểm định chéo.
  • Dữ liệu mất cân bằng: đo bằng precision, recall, F1, chia có stratify. Trọng số lớp sc=m/(Kmc)s_c = m/(K m_c), oversampling (gần như tương đương trọng số lớp), undersampling, SMOTE, hay dời ngưỡng đều đổi chỗ đánh đổi precision–recall. Lấy mẫu lại và tăng cường dữ liệu chỉ làm trên tập train, sau khi chia.

Đọc thêm:

Nội dung theo giấy phép CC BY-NC-SA 4.0. Mã nguồn theo giấy phép MIT.Giới thiệu