[ML101] Chương 2: Dự án Machine Learning đầu tiên
Hướng dẫn từng bước xây dựng dự án Machine Learning đầu tiên với Python
Bài viết có tham khảo, sử dụng và sửa đổi tài nguyên từ kho lưu trữ handson-mlp, tuân thủ giấy phép Apache‑2.0. Chúng tôi chân thành cảm ơn tác giả Aurélien Géron (@aureliengeron) vì sự chia sẻ kiến thức tuyệt vời và những đóng góp quý giá cho cộng đồng.
Chào mừng bạn đến với chương thực hành đầu tiên. Trong chương này, chúng ta sẽ đi qua toàn bộ quy trình của một dự án Machine Learning thực tế, từ việc thu thập dữ liệu, xử lý dữ liệu, chọn mô hình, tinh chỉnh tham số cho đến khi triển khai.
Mục tiêu của chúng ta là xây dựng một mô hình dự đoán giá nhà trung bình tại các quận (districts) thuộc bang California, dựa trên các đặc trưng dữ liệu từ cuộc điều tra dân số.
Bạn có thể chạy trực tiếp các đoạn mã code tại: Google Colab.
1. Thiết lập Môi trường
Trước khi bắt đầu, chúng ta nên đảm bảo môi trường Python đáp ứng các yêu cầu về phiên bản của ngôn ngữ và các thư viện cần thiết như Scikit-Learn.
Đầu tiên, hãy gửi lời chào đến thế giới Machine Learning!
print("Welcome to Machine Learning!")
Welcome to Machine Learning!
Dự án này yêu cầu Python phiên bản 3.10 trở lên để đảm bảo tính tương thích của các cú pháp mới.
import sys
# Kiểm tra phiên bản Python hiện tại.
# Nếu version < 3.10, chương trình sẽ báo lỗi AssertionError.
assert sys.version_info >= (3, 10)
Chúng ta cũng cần thư viện Scikit-Learn phiên bản 1.6.1 trở lên. Scikit-Learn là thư viện quan trọng nhất cho các thuật toán Machine Learning cổ điển trong Python.
from packaging.version import Version
import sklearn
# Kiểm tra phiên bản sklearn.
assert Version(sklearn.__version__) >= Version("1.6.1")
2. Thu thập Dữ liệu
Nhiệm vụ của bạn ở ví dụ này là dự đoán giá nhà trung vị (median house value) tại các quận của California, dựa trên các đặc trưng như dân số, thu nhập trung vị, v.v.
2.1. Tải dữ liệu
Trong thực tế, dữ liệu thường được lưu trữ trong các cơ sở dữ liệu quan hệ (relational databases) hoặc các kho dữ liệu. Tuy nhiên, để đơn giản hóa việc học tập, chúng ta sẽ tải một file nén (tarball) chứa dữ liệu CSV từ GitHub.
Đoạn mã dưới đây định nghĩa hàm load_housing_data để tự động hóa việc tải xuống và giải nén dữ liệu nếu nó chưa tồn tại trên máy của bạn.
from pathlib import Path
import pandas as pd
import tarfile
import urllib.request
def load_housing_data():
# Định nghĩa đường dẫn file nén
tarball_path = Path("datasets/housing.tgz")
# Nếu file chưa tồn tại, tiến hành tải về
if not tarball_path.is_file():
# Tạo thư mục datasets nếu chưa có
Path("datasets").mkdir(parents=True, exist_ok=True)
url = "https://github.com/ageron/data/raw/main/housing.tgz"
# Tải file từ URL về đường dẫn local
urllib.request.urlretrieve(url, tarball_path)
# Giải nén file
with tarfile.open(tarball_path) as housing_tarball:
housing_tarball.extractall(path="datasets", filter="data")
# Đọc file CSV vào Pandas DataFrame
return pd.read_csv(Path("datasets/housing/housing.csv"))
# Gọi hàm để lấy dữ liệu
housing_full = load_housing_data()
2.2. Khám phá cấu trúc dữ liệu
Sau khi tải dữ liệu, bước đầu tiên quan trọng là hiểu cấu trúc của nó. Chúng ta sử dụng phương thức head() để xem 5 dòng đầu tiên.
housing_full.head()
longitude latitude housing_median_age total_rooms total_bedrooms \
0 -122.23 37.88 41.0 880.0 129.0
1 -122.22 37.86 21.0 7099.0 1106.0
2 -122.24 37.85 52.0 1467.0 190.0
3 -122.25 37.85 52.0 1274.0 235.0
4 -122.25 37.85 52.0 1627.0 280.0
population households median_income median_house_value ocean_proximity
0 322.0 126.0 8.3252 452600.0 NEAR BAY
1 2401.0 1138.0 8.3014 358500.0 NEAR BAY
2 496.0 177.0 7.2574 352100.0 NEAR BAY
3 558.0 219.0 5.6431 341300.0 NEAR BAY
4 565.0 259.0 3.8462 342200.0 NEAR BAY
Tiếp theo, phương thức info() cung cấp thông tin tổng quan về dữ liệu, bao gồm tổng số dòng, kiểu dữ liệu của từng cột và số lượng giá trị khác rỗng (non-null). Điều này giúp ta phát hiện nhanh các dữ liệu bị thiếu.
housing_full.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 20640 entries, 0 to 20639
Data columns (total 10 columns):
# Column Non-Null Count Dtype
--- ------ -------------- -----
0 longitude 20640 non-null float64
1 latitude 20640 non-null float64
2 housing_median_age 20640 non-null float64
3 total_rooms 20640 non-null float64
4 total_bedrooms 20433 non-null float64
5 population 20640 non-null float64
6 households 20640 non-null float64
7 median_income 20640 non-null float64
8 median_house_value 20640 non-null float64
9 ocean_proximity 20640 non-null object
dtypes: float64(9), object(1)
memory usage: 1.6+ MB
Quan sát kết quả info(), ta thấy cột ocean_proximity có kiểu dữ liệu là object, trong khi các cột khác là số (float64). Vì đây là file CSV, object thường ám chỉ dữ liệu dạng văn bản (text). Ta có thể kiểm tra xem có bao nhiêu danh mục (categories) trong cột này bằng phương thức value_counts().
housing_full["ocean_proximity"].value_counts()
| count | |
|---|---|
| ocean_proximity | |
| <1H OCEAN | 9136 |
| INLAND | 6551 |
| NEAR OCEAN | 2658 |
| NEAR BAY | 2290 |
| ISLAND | 5 |
Để hiểu rõ hơn về các thuộc tính số (numerical attributes), phương thức describe() sẽ tính toán các thống kê mô tả như trung bình (mean), độ lệch chuẩn (std), min, max và các phân vị (percentiles).
housing_full.describe()
longitude latitude housing_median_age total_rooms \
count 20640.000000 20640.000000 20640.000000 20640.000000
mean -119.569704 35.631861 28.639486 2635.763081
std 2.003532 2.135952 12.585558 2181.615252
min -124.350000 32.540000 1.000000 2.000000
25% -121.800000 33.930000 18.000000 1447.750000
50% -118.490000 34.260000 29.000000 2127.000000
75% -118.010000 37.710000 37.000000 3148.000000
max -114.310000 41.950000 52.000000 39320.000000
total_bedrooms population households median_income \
count 20433.000000 20640.000000 20640.000000 20640.000000
mean 537.870553 1425.476744 499.539680 3.870671
std 421.385070 1132.462122 382.329753 1.899822
min 1.000000 3.000000 1.000000 0.499900
25% 296.000000 787.000000 280.000000 2.563400
50% 435.000000 1166.000000 409.000000 3.534800
75% 647.000000 1725.000000 605.000000 4.743250
max 6445.000000 35682.000000 6082.000000 15.000100
median_house_value
count 20640.000000
mean 206855.816909
std 115395.615874
min 14999.000000
25% 119600.000000
50% 179700.000000
75% 264725.000000
max 500001.000000
Một cách khác để cảm nhận dữ liệu là vẽ biểu đồ histogram cho từng thuộc tính số. Histogram hiển thị số lượng các mẫu dữ liệu (trục tung) nằm trong một khoảng giá trị nhất định (trục hoành).
import matplotlib.pyplot as plt
# extra code – các dòng sau thiết lập kích thước font chữ mặc định cho biểu đồ
plt.rc('font', size=14)
plt.rc('axes', labelsize=14, titlesize=14)
plt.rc('legend', fontsize=14)
plt.rc('xtick', labelsize=10)
plt.rc('ytick', labelsize=10)
# Vẽ histogram cho toàn bộ các cột số
housing_full.hist(bins=50, figsize=(12, 8))
plt.show()

3. Tạo tập kiểm tra (Test Set)
Một nguyên tắc vàng trong Machine Learning là: Không bao giờ được nhìn vào tập kiểm tra (Test Set) cho đến khi mô hình đã hoàn thiện. Nếu bạn khám phá tập kiểm tra quá sớm, não bộ của bạn có thể vô tình phát hiện ra các mẫu (patterns) trong đó và lựa chọn mô hình thiên vị theo các mẫu này. Hiện tượng này gọi là Thiên kiến dòm ngó dữ liệu (Data Snooping Bias).
3.1. Lấy mẫu ngẫu nhiên (Random Sampling)
Cách đơn giản nhất là chọn ngẫu nhiên 20% dữ liệu làm tập kiểm tra.
import numpy as np
def shuffle_and_split_data(data, test_ratio, rng):
# Hoán vị ngẫu nhiên các chỉ số (index)
shuffled_indices = rng.permutation(len(data))
# Tính kích thước tập test
test_set_size = int(len(data) * test_ratio)
# Tách chỉ số cho test và train
test_indices = shuffled_indices[:test_set_size]
train_indices = shuffled_indices[test_set_size:]
# Trả về 2 DataFrame tương ứng
return data.iloc[train_indices], data.iloc[test_indices]
Để đảm bảo kết quả có thể tái lập (reproducible), chúng ta cần thiết lập một hạt giống ngẫu nhiên (random seed).
# Tạo bộ sinh số ngẫu nhiên với seed cố định là 42
rng = np.random.default_rng(seed=42)
train_set, test_set = shuffle_and_split_data(housing_full, 0.2, rng)
# Kiểm tra kích thước tập train
len(train_set)
16512
# Kiểm tra kích thước tập test
len(test_set)
4128
Lưu ý về tính nhất quán: Ngay cả khi đặt seed, nếu dữ liệu được cập nhật (thêm dòng mới), việc xáo trộn lại có thể khiến các mẫu trong tập test cũ lọt vào tập train mới. Để giải quyết, ta có thể dùng định danh (identifier) của mỗi mẫu để quyết định nó thuộc tập nào (ví dụ: tính hash của ID).
from zlib import crc32
def is_id_in_test_set(identifier, test_ratio):
# Kiểm tra xem mã hash của ID có nhỏ hơn ngưỡng tỉ lệ hay không
return crc32(np.int64(identifier)) < test_ratio * 2**32
def split_data_with_id_hash(data, test_ratio, id_column):
ids = data[id_column]
# Áp dụng hàm kiểm tra cho từng ID
in_test_set = ids.apply(lambda id_: is_id_in_test_set(id_, test_ratio))
return data.loc[~in_test_set], data.loc[in_test_set]
Dữ liệu nhà ở không có cột ID, ta có thể dùng chỉ số dòng (row index) làm ID:
housing_with_id = housing_full.reset_index() # thêm cột `index`
train_set, test_set = split_data_with_id_hash(housing_with_id, 0.2, "index")
Tuy nhiên, nếu thứ tự dòng thay đổi, cách trên sẽ sai. Giải pháp tốt hơn là tạo một ID ổn định từ kinh độ và vĩ độ:
# Tạo ID từ kinh độ và vĩ độ
housing_with_id["id"] = (housing_full["longitude"] * 1000
+ housing_full["latitude"])
train_set, test_set = split_data_with_id_hash(housing_with_id, 0.2, "id")
Trong Scikit-Learn, hàm train_test_split thực hiện việc chia ngẫu nhiên rất tiện lợi:
from sklearn.model_selection import train_test_split
train_set, test_set = train_test_split(housing_full, test_size=0.2,
random_state=42)
Kiểm tra dữ liệu bị thiếu trong tập test:
test_set["total_bedrooms"].isnull().sum()
np.int64(44)
3.2. Lấy mẫu phân tầng (Stratified Sampling)
Lấy mẫu ngẫu nhiên hoạt động tốt nếu tập dữ liệu đủ lớn. Nếu không, ta có nguy cơ gặp lỗi lấy mẫu (sampling bias). Ví dụ, nếu thu nhập trung vị (median income) là một yếu tố quan trọng để dự đoán giá nhà, ta cần đảm bảo tập kiểm tra có phân phối thu nhập tương tự như tổng thể. Đây gọi là lấy mẫu phân tầng.
Chúng ta sẽ tạo một cột phân loại thu nhập income_cat:
# Đoạn mã mô phỏng xác suất (lý thuyết xác suất thống kê)
# extra code – tính xác suất mẫu xấu (10.7%)
from scipy.stats import binom
sample_size = 1000
ratio_female = 0.516
proba_too_small = binom(sample_size, ratio_female).cdf(490 - 1)
proba_too_large = 1 - binom(sample_size, ratio_female).cdf(540)
print(proba_too_small + proba_too_large)
0.10727422667455615
# extra code – một cách khác để ước lượng xác suất bằng mô phỏng
rng = np.random.default_rng(seed=42)
samples = (rng.random((100_000, sample_size)) < ratio_female).sum(axis=1)
((samples < 490) | (samples > 540)).mean()
np.float64(0.1077)
Tạo các nhóm thu nhập (income categories) để thực hiện phân tầng:
# Chia median_income thành 5 nhóm (bins)
housing_full["income_cat"] = pd.cut(housing_full["median_income"],
bins=[0., 1.5, 3.0, 4.5, 6., np.inf],
labels=[1, 2, 3, 4, 5])
Vẽ biểu đồ phân phối các nhóm thu nhập:
cat_counts = housing_full["income_cat"].value_counts().sort_index()
cat_counts.plot.bar(rot=0, grid=True)
plt.xlabel("Income category")
plt.ylabel("Number of districts")
plt.show()

Bây giờ ta dùng StratifiedShuffleSplit hoặc train_test_split với tham số stratify:
from sklearn.model_selection import StratifiedShuffleSplit
splitter = StratifiedShuffleSplit(n_splits=10, test_size=0.2, random_state=42)
strat_splits = []
for train_index, test_index in splitter.split(housing_full,
housing_full["income_cat"]):
strat_train_set_n = housing_full.iloc[train_index]
strat_test_set_n = housing_full.iloc[test_index]
strat_splits.append([strat_train_set_n, strat_test_set_n])
strat_train_set, strat_test_set = strat_splits[0]
Cách viết ngắn gọn hơn:
strat_train_set, strat_test_set = train_test_split(
housing_full, test_size=0.2, stratify=housing_full["income_cat"],
random_state=42)
Kiểm tra tỷ lệ các nhóm trong tập test phân tầng:
strat_test_set["income_cat"].value_counts() / len(strat_test_set)
| count | |
|---|---|
| income_cat | |
| 3 | 0.350533 |
| 2 | 0.318798 |
| 4 | 0.176357 |
| 5 | 0.114341 |
| 1 | 0.039971 |
So sánh tỷ lệ sai số giữa lấy mẫu ngẫu nhiên và lấy mẫu phân tầng so với tổng thể:
# extra code – tính toán dữ liệu cho Hình 2–10
def income_cat_proportions(data):
return data["income_cat"].value_counts() / len(data)
train_set, test_set = train_test_split(housing_full, test_size=0.2,
random_state=42)
compare_props = pd.DataFrame({
"Overall %": income_cat_proportions(housing_full),
"Stratified %": income_cat_proportions(strat_test_set),
"Random %": income_cat_proportions(test_set),
}).sort_index()
compare_props.index.name = "Income Category"
compare_props["Strat. Error %"] = (compare_props["Stratified %"] /
compare_props["Overall %"] - 1)
compare_props["Rand. Error %"] = (compare_props["Random %"] /
compare_props["Overall %"] - 1)
(compare_props * 100).round(2)
Overall % Stratified % Random % Strat. Error % \
Income Category
1 3.98 4.00 4.24 0.36
2 31.88 31.88 30.74 -0.02
3 35.06 35.05 34.52 -0.01
4 17.63 17.64 18.41 0.03
5 11.44 11.43 12.09 -0.08
Rand. Error %
Income Category
1 6.45
2 -3.59
3 -1.53
4 4.42
5 5.63
Sau khi chia xong, ta cần xóa cột income_cat để trả dữ liệu về trạng thái ban đầu:
for set_ in (strat_train_set, strat_test_set):
set_.drop("income_cat", axis=1, inplace=True)
4. Khám phá và Trực quan hóa Dữ liệu (Discover and Visualize the Data)
Bây giờ chúng ta sẽ chỉ làm việc trên tập huấn luyện (strat_train_set) để tránh làm rò rỉ thông tin từ tập test.
housing = strat_train_set.copy()
4.1. Trực quan hóa Dữ liệu Địa lý
Vì dữ liệu có kinh độ và vĩ độ, biểu đồ phân tán (scatter plot) là lựa chọn tốt nhất để hình dung vị trí địa lý.
# Biểu đồ cơ bản
housing.plot(kind="scatter", x="longitude", y="latitude", grid=True)
plt.show()

Để thấy rõ mật độ các điểm dữ liệu, ta giảm độ mờ (alpha).
# Biểu đồ hiển thị mật độ
housing.plot(kind="scatter", x="longitude", y="latitude", grid=True, alpha=0.2)
plt.show()

Bây giờ ta thêm thông tin về dân số (kích thước hình tròn - s) và giá nhà (màu sắc - c). Bản đồ nhiệt (heatmap) cmap="jet" sẽ giúp phân biệt giá trị từ thấp (xanh) đến cao (đỏ).
housing.plot(kind="scatter", x="longitude", y="latitude", grid=True,
s=housing["population"] / 100, label="population",
c="median_house_value", cmap="jet", colorbar=True,
legend=True, sharex=False, figsize=(10, 7))
plt.show()

Dưới đây là phiên bản đẹp hơn của biểu đồ trên, được chồng lên bản đồ bang California (đoạn mã này dùng để tạo hình ảnh minh họa trong sách).
# extra code – tạo hình ảnh đầu tiên trong chương
# Tải hình ảnh bản đồ California
filename = "california.png"
filepath = Path(f"my_{filename}")
if not filepath.is_file():
homlp_root = "https://github.com/ageron/handson-mlp/raw/main/"
url = homlp_root + "images/end_to_end_project/" + filename
print("Downloading", filename)
urllib.request.urlretrieve(url, filepath)
housing_renamed = housing.rename(columns={
"latitude": "Latitude", "longitude": "Longitude",
"population": "Population",
"median_house_value": "Median house value (ᴜsᴅ)"})
housing_renamed.plot(
kind="scatter", x="Longitude", y="Latitude",
s=housing_renamed["Population"] / 100, label="Population",
c="Median house value (ᴜsᴅ)", cmap="jet", colorbar=True,
legend=True, sharex=False, figsize=(10, 7))
california_img = plt.imread(filepath)
axis = -124.55, -113.95, 32.45, 42.05
plt.axis(axis)
plt.imshow(california_img, extent=axis)
plt.show()
Downloading california.png

4.2. Tìm kiếm Tương quan (Looking for Correlations)
Hệ số tương quan Pearson () đo lường mối quan hệ tuyến tính giữa hai biến, giá trị từ -1 đến 1.
# Tính ma trận tương quan
corr_matrix = housing.corr(numeric_only=True)
Xem sự tương quan của các biến với mục tiêu median_house_value:
corr_matrix["median_house_value"].sort_values(ascending=False)
| median_house_value | |
|---|---|
| median_house_value | 1.000000 |
| median_income | 0.688380 |
| total_rooms | 0.137455 |
| housing_median_age | 0.102175 |
| households | 0.071426 |
| total_bedrooms | 0.054635 |
| population | -0.020153 |
| longitude | -0.050859 |
| latitude | -0.139584 |
Chúng ta cũng có thể dùng scatter_matrix của Pandas để vẽ biểu đồ tương quan giữa các cặp thuộc tính quan trọng.
from pandas.plotting import scatter_matrix
attributes = ["median_house_value", "median_income", "total_rooms",
"housing_median_age"]
scatter_matrix(housing[attributes], figsize=(12, 8))
plt.show()

Mối tương quan mạnh nhất là giữa thu nhập trung vị và giá nhà. Hãy phóng to biểu đồ này:
housing.plot(kind="scatter", x="median_income", y="median_house_value",
alpha=0.1, grid=True)
plt.show()

4.3. Thử nghiệm kết hợp thuộc tính (Attribute Combinations)
Đôi khi các thuộc tính gốc không hữu ích bằng các thuộc tính phái sinh. Ví dụ: tổng số phòng (total_rooms) không có ý nghĩa nhiều nếu ta không biết có bao nhiêu hộ gia đình (households). Ta sẽ tạo ra các thuộc tính mới:
housing["rooms_per_house"] = housing["total_rooms"] / housing["households"]
housing["bedrooms_ratio"] = housing["total_bedrooms"] / housing["total_rooms"]
housing["people_per_house"] = housing["population"] / housing["households"]
Kiểm tra lại ma trận tương quan:
corr_matrix = housing.corr(numeric_only=True)
corr_matrix["median_house_value"].sort_values(ascending=False)
| median_house_value | |
|---|---|
| median_house_value | 1.000000 |
| median_income | 0.688380 |
| rooms_per_house | 0.143663 |
| total_rooms | 0.137455 |
| housing_median_age | 0.102175 |
| households | 0.071426 |
| total_bedrooms | 0.054635 |
| population | -0.020153 |
| people_per_house | -0.038224 |
| longitude | -0.050859 |
| latitude | -0.139584 |
| bedrooms_ratio | -0.256397 |
5. Chuẩn bị Dữ liệu cho Thuật toán ML (Prepare the Data)
Đây là bước quan trọng nhất. Thay vì làm thủ công, chúng ta nên viết các hàm để có thể tái sử dụng và áp dụng cho dữ liệu mới.
Đầu tiên, tách biến mục tiêu (label) ra khỏi biến đầu vào (features).
housing = strat_train_set.drop("median_house_value", axis=1)
housing_labels = strat_train_set["median_house_value"].copy()
5.1. Làm sạch Dữ liệu (Data Cleaning)
Dữ liệu thực tế thường bị thiếu (missing values). Cột total_bedrooms có một số giá trị bị thiếu. Chúng ta có 3 lựa chọn cơ bản:
- Bỏ các dòng bị thiếu.
- Bỏ toàn bộ cột đó.
- Điền giá trị bị thiếu (imputation) bằng 0, trung bình, hoặc trung vị.
# Mô phỏng 3 phương án (không áp dụng trực tiếp lên biến `housing` gốc ngay)
null_rows_idx = housing.isnull().any(axis=1)
housing.loc[null_rows_idx].head()
# Option 1: Xóa dòng
housing_option1 = housing.copy()
housing_option1.dropna(subset=["total_bedrooms"], inplace=True)
housing_option1.loc[null_rows_idx].head()
# Option 2: Xóa cột
housing_option2 = housing.copy()
housing_option2.drop("total_bedrooms", axis=1, inplace=True)
housing_option2.loc[null_rows_idx].head()
# Option 3: Điền giá trị trung vị (Median)
housing_option3 = housing.copy()
median = housing["total_bedrooms"].median()
housing_option3["total_bedrooms"] = housing_option3["total_bedrooms"].fillna(median)
housing_option3.loc[null_rows_idx].head()
longitude latitude housing_median_age total_rooms total_bedrooms \
14452 -120.67 40.50 15.0 5343.0 434.0
18217 -117.96 34.03 35.0 2093.0 434.0
11889 -118.05 34.04 33.0 1348.0 434.0
20325 -118.88 34.17 15.0 4260.0 434.0
14360 -117.87 33.62 8.0 1266.0 434.0
population households median_income ocean_proximity
14452 2503.0 902.0 3.5962 INLAND
18217 1755.0 403.0 3.4115 <1H OCEAN
11889 1098.0 257.0 4.2917 <1H OCEAN
20325 1701.0 669.0 5.1033 <1H OCEAN
14360 375.0 183.0 9.8020 <1H OCEAN
Scikit-Learn cung cấp lớp SimpleImputer để xử lý việc điền dữ liệu thiếu một cách chuyên nghiệp.
from sklearn.impute import SimpleImputer
# Khởi tạo Imputer với chiến lược là điền số trung vị
imputer = SimpleImputer(strategy="median")
Vì trung vị chỉ tính được trên dữ liệu số, ta cần tạo một bản sao dữ liệu chỉ chứa các cột số:
housing_num = housing.select_dtypes(include=[np.number])
Huấn luyện (fit) imputer trên dữ liệu huấn luyện:
imputer.fit(housing_num)
SimpleImputer(strategy='median')
SimpleImputer(strategy='median')
Xem các giá trị trung vị đã tính được:
imputer.statistics_
array([-118.51 , 34.26 , 29. , 2125. , 434. , 1167. ,
408. , 3.5385])
So sánh với việc tính thủ công:
housing_num.median().values
array([-118.51 , 34.26 , 29. , 2125. , 434. , 1167. ,
408. , 3.5385])
Sử dụng imputer để biến đổi (transform) dữ liệu, thay thế các giá trị NaN bằng median:
X = imputer.transform(housing_num)
Kiểm tra lại các tên đặc trưng và chuyển đổi kết quả từ NumPy array về DataFrame:
imputer.feature_names_in_
housing_tr = pd.DataFrame(X, columns=housing_num.columns,
index=housing_num.index)
# Kiểm tra lại các dòng từng bị thiếu
housing_tr.loc[null_rows_idx].head()
longitude latitude housing_median_age total_rooms total_bedrooms \
14452 -120.67 40.50 15.0 5343.0 434.0
18217 -117.96 34.03 35.0 2093.0 434.0
11889 -118.05 34.04 33.0 1348.0 434.0
20325 -118.88 34.17 15.0 4260.0 434.0
14360 -117.87 33.62 8.0 1266.0 434.0
population households median_income
14452 2503.0 902.0 3.5962
18217 1755.0 403.0 3.4115
11889 1098.0 257.0 4.2917
20325 1701.0 669.0 5.1033
14360 375.0 183.0 9.8020
imputer.strategy
'median'
# Lặp lại để đảm bảo biến housing_tr có sẵn
housing_tr = pd.DataFrame(X, columns=housing_num.columns,
index=housing_num.index)
housing_tr.loc[null_rows_idx].head()
longitude latitude housing_median_age total_rooms total_bedrooms \
14452 -120.67 40.50 15.0 5343.0 434.0
18217 -117.96 34.03 35.0 2093.0 434.0
11889 -118.05 34.04 33.0 1348.0 434.0
20325 -118.88 34.17 15.0 4260.0 434.0
14360 -117.87 33.62 8.0 1266.0 434.0
population households median_income
14452 2503.0 902.0 3.5962
18217 1755.0 403.0 3.4115
11889 1098.0 257.0 4.2917
20325 1701.0 669.0 5.1033
14360 375.0 183.0 9.8020
# from sklearn import set_config
# set_config(transform_output="pandas") # scikit-learn >= 1.2 có thể xuất ra pandas trực tiếp
5.2. Xử lý Ngoại lai (Outliers)
Mặc dù trong chương này chúng ta chưa tập trung sâu, nhưng IsolationForest là một công cụ mạnh mẽ để phát hiện ngoại lai.
from sklearn.ensemble import IsolationForest
isolation_forest = IsolationForest(random_state=42)
outlier_pred = isolation_forest.fit_predict(X)
outlier_pred
array([-1, 1, 1, ..., 1, 1, 1])
Đoạn mã dưới đây (đã được comment) cho thấy cách lọc bỏ ngoại lai nếu muốn:
# housing = housing.iloc[outlier_pred == 1]
# housing_labels = housing_labels.iloc[outlier_pred == 1]
5.3. Xử lý Đặc trưng Văn bản và Phân loại (Categorical Attributes)
Cột ocean_proximity là dữ liệu dạng văn bản. Máy tính cần các con số.
housing_cat = housing[["ocean_proximity"]]
housing_cat.head(8)
ocean_proximity
13096 NEAR BAY
14973 <1H OCEAN
3785 INLAND
14689 INLAND
20507 NEAR OCEAN
1286 INLAND
18078 <1H OCEAN
4396 NEAR BAY
Ordinal Encoding: Gán mỗi loại một số nguyên (0, 1, 2…). Nhược điểm là mô hình có thể hiểu nhầm thứ tự (ví dụ: loại 0 gần loại 1 hơn loại 4, nhưng thực tế có thể không phải vậy).
from sklearn.preprocessing import OrdinalEncoder
ordinal_encoder = OrdinalEncoder()
housing_cat_encoded = ordinal_encoder.fit_transform(housing_cat)
housing_cat_encoded[:8]
array([[3.],
[0.],
[1.],
[1.],
[4.],
[1.],
[0.],
[3.]])
ordinal_encoder.categories_
[array(['<1H OCEAN', 'INLAND', 'ISLAND', 'NEAR BAY', 'NEAR OCEAN'],
dtype=object)]
One-Hot Encoding: Tạo ra các cột nhị phân cho từng loại (1 nếu thuộc loại đó, 0 nếu không). Đây là phương pháp phổ biến cho các biến định danh không có thứ tự.
from sklearn.preprocessing import OneHotEncoder
cat_encoder = OneHotEncoder()
housing_cat_1hot = cat_encoder.fit_transform(housing_cat)
housing_cat_1hot
<Compressed Sparse Row sparse matrix of dtype 'float64'
with 16512 stored elements and shape (16512, 5)>
Kết quả trả về là một ma trận thưa (sparse matrix) để tiết kiệm bộ nhớ. Ta có thể chuyển về dạng mảng đầy đủ (dense array):
housing_cat_1hot.toarray()
array([[0., 0., 0., 1., 0.],
[1., 0., 0., 0., 0.],
[0., 1., 0., 0., 0.],
...,
[0., 0., 0., 0., 1.],
[1., 0., 0., 0., 0.],
[0., 0., 0., 0., 1.]])
Hoặc thiết lập sparse_output=False ngay từ đầu:
cat_encoder = OneHotEncoder(sparse_output=False)
housing_cat_1hot = cat_encoder.fit_transform(housing_cat)
housing_cat_1hot
array([[0., 0., 0., 1., 0.],
[1., 0., 0., 0., 0.],
[0., 1., 0., 0., 0.],
...,
[0., 0., 0., 0., 1.],
[1., 0., 0., 0., 0.],
[0., 0., 0., 0., 1.]])
cat_encoder.categories_
[array(['<1H OCEAN', 'INLAND', 'ISLAND', 'NEAR BAY', 'NEAR OCEAN'],
dtype=object)]
Cách xử lý các biến loại mới không có trong tập huấn luyện (Unknown categories):
df_test = pd.DataFrame({"ocean_proximity": ["INLAND", "NEAR BAY"]})
pd.get_dummies(df_test)
ocean_proximity_INLAND ocean_proximity_NEAR BAY
0 True False
1 False True
cat_encoder.transform(df_test)
array([[0., 1., 0., 0., 0.],
[0., 0., 0., 1., 0.]])
df_test_unknown = pd.DataFrame({"ocean_proximity": ["<2H OCEAN", "ISLAND"]})
pd.get_dummies(df_test_unknown)
ocean_proximity_<2H OCEAN ocean_proximity_ISLAND
0 True False
1 False True
Nếu gặp loại lạ, ta có thể yêu cầu Encoder bỏ qua (ignore) thay vì báo lỗi:
cat_encoder.handle_unknown = "ignore"
cat_encoder.transform(df_test_unknown)
array([[0., 0., 0., 0., 0.],
[0., 0., 1., 0., 0.]])
cat_encoder.feature_names_in_
array(['ocean_proximity'], dtype=object)
cat_encoder.get_feature_names_out()
array(['ocean_proximity_<1H OCEAN', 'ocean_proximity_INLAND',
'ocean_proximity_ISLAND', 'ocean_proximity_NEAR BAY',
'ocean_proximity_NEAR OCEAN'], dtype=object)
df_output = pd.DataFrame(cat_encoder.transform(df_test_unknown),
columns=cat_encoder.get_feature_names_out(),
index=df_test_unknown.index)
df_output
ocean_proximity_<1H OCEAN ocean_proximity_INLAND ocean_proximity_ISLAND \
0 0.0 0.0 0.0
1 0.0 0.0 1.0
ocean_proximity_NEAR BAY ocean_proximity_NEAR OCEAN
0 0.0 0.0
1 0.0 0.0
5.4. Co giãn Đặc trưng (Feature Scaling)
Các thuật toán ML thường hoạt động kém nếu các đặc trưng có tỷ lệ khác nhau quá lớn (ví dụ: tuổi từ 0-100, thu nhập từ 0-100.000).
- Min-Max Scaling (Normalization): Đưa dữ liệu về khoảng [0, 1] hoặc [-1, 1].
- Standardization: Trừ đi trung bình và chia cho độ lệch chuẩn. Kết quả có trung bình 0 và phương sai 1. Cách này ít bị ảnh hưởng bởi ngoại lai hơn.
from sklearn.preprocessing import MinMaxScaler
min_max_scaler = MinMaxScaler(feature_range=(-1, 1))
housing_num_min_max_scaled = min_max_scaler.fit_transform(housing_num)
from sklearn.preprocessing import StandardScaler
std_scaler = StandardScaler()
housing_num_std_scaled = std_scaler.fit_transform(housing_num)
Khi dữ liệu bị lệch (như population), việc lấy logarit có thể giúp phân phối trở nên chuẩn hơn (hình chuông):
# extra code – tạo hình Figure 2–17
fig, axs = plt.subplots(1, 2, figsize=(8, 3), sharey=True)
housing["population"].hist(ax=axs[0], bins=50)
housing["population"].apply(np.log).hist(ax=axs[1], bins=50)
axs[0].set_xlabel("Population")
axs[1].set_xlabel("Log of population")
axs[0].set_ylabel("Number of districts")
plt.show()

Một kỹ thuật khác là thay thế giá trị bằng phân vị (percentile) của nó:
# extra code – minh họa phân phối đồng nhất (uniform) sau khi đổi sang phân vị
percentiles = [np.percentile(housing["median_income"], p)
for p in range(1, 100)]
flattened_median_income = pd.cut(housing["median_income"],
bins=[-np.inf] + percentiles + [np.inf],
labels=range(1, 100 + 1))
flattened_median_income.hist(bins=50)
plt.xlabel("Median income percentile")
plt.ylabel("Number of districts")
plt.show()

Sử dụng hàm Gaussian RBF (Radial Basis Function) để đo độ tương đồng:
from sklearn.metrics.pairwise import rbf_kernel
age_simil_35 = rbf_kernel(housing[["housing_median_age"]], [[35]], gamma=0.1)
Đoạn mã dưới đây minh họa tác động của việc dùng RBF kernel:
# extra code – tạo Hình 2–18
ages = np.linspace(housing["housing_median_age"].min(),
housing["housing_median_age"].max(),
500).reshape(-1, 1)
gamma1 = 0.1
gamma2 = 0.03
rbf1 = rbf_kernel(ages, [[35]], gamma=gamma1)
rbf2 = rbf_kernel(ages, [[35]], gamma=gamma2)
fig, ax1 = plt.subplots()
ax1.set_xlabel("Housing median age")
ax1.set_ylabel("Number of districts")
ax1.hist(housing["housing_median_age"], bins=50)
ax2 = ax1.twinx() # tạo trục y thứ hai dùng chung trục x
color = "blue"
ax2.plot(ages, rbf1, color=color, label="gamma = 0.10")
ax2.plot(ages, rbf2, color=color, label="gamma = 0.03", linestyle="--")
ax2.tick_params(axis='y', labelcolor=color)
ax2.set_ylabel("Age similarity", color=color)
plt.legend(loc="upper left")
plt.show()

Đôi khi ta cần co giãn cả biến mục tiêu (target values), ví dụ dùng StandardScaler cho biến y, sau đó dùng TransformedTargetRegressor để tự động biến đổi ngược lại khi dự đoán.
from sklearn.linear_model import LinearRegression
target_scaler = StandardScaler()
scaled_labels = target_scaler.fit_transform(housing_labels.to_frame())
model = LinearRegression()
model.fit(housing[["median_income"]], scaled_labels)
some_new_data = housing[["median_income"]].iloc[:5]
scaled_predictions = model.predict(some_new_data)
predictions = target_scaler.inverse_transform(scaled_predictions)
predictions
array([[131997.15275877],
[299359.35844434],
[146023.37185694],
[138840.33653057],
[192016.61557639]])
from sklearn.compose import TransformedTargetRegressor
model = TransformedTargetRegressor(LinearRegression(),
transformer=StandardScaler())
model.fit(housing[["median_income"]], housing_labels)
predictions = model.predict(some_new_data)
predictions
array([131997.15275877, 299359.35844434, 146023.37185694, 138840.33653057,
192016.61557639])
5.5. Bộ biến đổi tùy chỉnh (Custom Transformers)
Để tích hợp các bước xử lý riêng vào Pipeline của Scikit-Learn, ta có thể viết các Transformer tùy chỉnh.
Sử dụng FunctionTransformer cho các hàm đơn giản (như log):
from sklearn.preprocessing import FunctionTransformer
log_transformer = FunctionTransformer(np.log, inverse_func=np.exp)
log_pop = log_transformer.transform(housing[["population"]])
rbf_transformer = FunctionTransformer(rbf_kernel,
kw_args=dict(Y=[[35.]], gamma=0.1))
age_simil_35 = rbf_transformer.transform(housing[["housing_median_age"]])
age_simil_35
array([[2.81118530e-13],
[8.20849986e-02],
[6.70320046e-01],
...,
[9.55316054e-22],
[6.70320046e-01],
[3.03539138e-04]])
sf_coords = 37.7749, -122.41
sf_transformer = FunctionTransformer(rbf_kernel,
kw_args=dict(Y=[sf_coords], gamma=0.1))
sf_simil = sf_transformer.transform(housing[["latitude", "longitude"]])
sf_simil
array([[0.999927 ],
[0.05258419],
[0.94864161],
...,
[0.00388525],
[0.05038518],
[0.99868067]])
ratio_transformer = FunctionTransformer(lambda X: X[:, [0]] / X[:, [1]])
ratio_transformer.transform(np.array([[1., 2.], [3., 4.]]))
array([[0.5 ],
[0.75]])
Nếu cần các Transformer phức tạp có khả năng “học” (có hàm fit), ta kế thừa từ BaseEstimator và TransformerMixin. Ví dụ dưới đây tái tạo lại StandardScaler:
from sklearn.base import BaseEstimator, TransformerMixin
from sklearn.utils.validation import check_array, check_is_fitted
class StandardScalerClone(BaseEstimator, TransformerMixin):
def __init__(self, with_mean=True): # không dùng *args hay **kwargs!
self.with_mean = with_mean
def fit(self, X, y=None): # y là bắt buộc dù không dùng
X = check_array(X) # kiểm tra X là mảng hợp lệ
self.mean_ = X.mean(axis=0)
self.scale_ = X.std(axis=0)
self.n_features_in_ = X.shape[1]
return self # luôn trả về self!
def transform(self, X):
check_is_fitted(self) # kiểm tra xem đã fit chưa
X = check_array(X)
assert self.n_features_in_ == X.shape[1]
if self.with_mean:
X = X - self.mean_
return X / self.scale_
Một ví dụ khác: ClusterSimilarity dùng K-Means để tạo đặc trưng mới dựa trên khoảng cách đến các tâm cụm (cluster centers).
from sklearn.cluster import KMeans
class ClusterSimilarity(BaseEstimator, TransformerMixin):
def __init__(self, n_clusters=10, gamma=1.0, random_state=None):
self.n_clusters = n_clusters
self.gamma = gamma
self.random_state = random_state
def fit(self, X, y=None, sample_weight=None):
self.kmeans_ = KMeans(self.n_clusters, random_state=self.random_state)
self.kmeans_.fit(X, sample_weight=sample_weight)
return self
def transform(self, X):
return rbf_kernel(X, self.kmeans_.cluster_centers_, gamma=self.gamma)
def get_feature_names_out(self, names=None):
return [f"Cluster {i} similarity" for i in range(self.n_clusters)]
Áp dụng ClusterSimilarity cho tọa độ địa lý:
cluster_simil = ClusterSimilarity(n_clusters=10, gamma=1., random_state=42)
similarities = cluster_simil.fit_transform(housing[["latitude", "longitude"]])
similarities[:3].round(2)
array([[0.46, 0. , 0.08, 0. , 0. , 0. , 0. , 0.98, 0. , 0. ],
[0. , 0.96, 0. , 0.03, 0.04, 0. , 0. , 0. , 0.11, 0.35],
[0.34, 0. , 0.45, 0. , 0. , 0. , 0.01, 0.73, 0. , 0. ]])
Trực quan hóa các cụm tìm được trên bản đồ:
# extra code – tạo Hình 2–19
housing_renamed = housing.rename(columns={
"latitude": "Latitude", "longitude": "Longitude",
"population": "Population",
"median_house_value": "Median house value (ᴜsᴅ)"})
housing_renamed["Max cluster similarity"] = similarities.max(axis=1)
housing_renamed.plot(kind="scatter", x="Longitude", y="Latitude", grid=True,
s=housing_renamed["Population"] / 100, label="Population",
c="Max cluster similarity",
cmap="jet", colorbar=True,
legend=True, sharex=False, figsize=(10, 7))
plt.plot(cluster_simil.kmeans_.cluster_centers_[:, 1],
cluster_simil.kmeans_.cluster_centers_[:, 0],
linestyle="", color="black", marker="X", markersize=20,
label="Cluster centers")
plt.legend(loc="upper right")
plt.show()

5.6. Pipeline Chuyển đổi (Transformation Pipelines)
Pipeline giúp xâu chuỗi các bước xử lý dữ liệu để đảm bảo quy trình nhất quán và tránh rò rỉ dữ liệu.
from sklearn.pipeline import Pipeline
num_pipeline = Pipeline([
("impute", SimpleImputer(strategy="median")),
("standardize", StandardScaler()),
])
Dùng make_pipeline thì không cần đặt tên thủ công cho các bước:
from sklearn.pipeline import make_pipeline
num_pipeline = make_pipeline(SimpleImputer(strategy="median"), StandardScaler())
Hiển thị sơ đồ Pipeline:
from sklearn import set_config
set_config(display='diagram')
num_pipeline
Pipeline(steps=[('simpleimputer', SimpleImputer(strategy='median')),
('standardscaler', StandardScaler())])Pipeline(steps=[('simpleimputer', SimpleImputer(strategy='median')),
('standardscaler', StandardScaler())])SimpleImputer(strategy='median')
StandardScaler()
Thử nghiệm pipeline trên dữ liệu số:
housing_num_prepared = num_pipeline.fit_transform(housing_num)
housing_num_prepared[:2].round(2)
array([[-1.42, 1.01, 1.86, 0.31, 1.37, 0.14, 1.39, -0.94],
[ 0.6 , -0.7 , 0.91, -0.31, -0.44, -0.69, -0.37, 1.17]])
Lấy lại DataFrame từ kết quả:
df_housing_num_prepared = pd.DataFrame(
housing_num_prepared, columns=num_pipeline.get_feature_names_out(),
index=housing_num.index)
df_housing_num_prepared.head(2)
longitude latitude housing_median_age total_rooms total_bedrooms \
13096 -1.423037 1.013606 1.861119 0.311912 1.368167
14973 0.596394 -0.702103 0.907630 -0.308620 -0.435925
population households median_income
13096 0.137460 1.394812 -0.936491
14973 -0.693771 -0.373485 1.171942
Truy cập các bước trong Pipeline:
num_pipeline.steps
num_pipeline[1]
num_pipeline[:-1]
num_pipeline.named_steps["simpleimputer"]
num_pipeline.set_params(simpleimputer__strategy="median")
Pipeline(steps=[('simpleimputer', SimpleImputer(strategy='median')),
('standardscaler', StandardScaler())])Pipeline(steps=[('simpleimputer', SimpleImputer(strategy='median')),
('standardscaler', StandardScaler())])SimpleImputer(strategy='median')
StandardScaler()
ColumnTransformer cho phép áp dụng các pipeline khác nhau cho các cột khác nhau (số vs phân loại).
from sklearn.compose import ColumnTransformer
num_attribs = ["longitude", "latitude", "housing_median_age", "total_rooms",
"total_bedrooms", "population", "households", "median_income"]
cat_attribs = ["ocean_proximity"]
cat_pipeline = make_pipeline(
SimpleImputer(strategy="most_frequent"),
OneHotEncoder(handle_unknown="ignore"))
preprocessing = ColumnTransformer([
("num", num_pipeline, num_attribs),
("cat", cat_pipeline, cat_attribs),
])
Dùng make_column_selector để tự động chọn cột theo kiểu dữ liệu:
from sklearn.compose import make_column_selector, make_column_transformer
preprocessing = make_column_transformer(
(num_pipeline, make_column_selector(dtype_include=np.number)),
(cat_pipeline, make_column_selector(dtype_include=object)),
)
Áp dụng toàn bộ quy trình tiền xử lý lên dữ liệu:
housing_prepared = preprocessing.fit_transform(housing)
# extra code – chuyển về DataFrame
housing_prepared_fr = pd.DataFrame(
housing_prepared,
columns=preprocessing.get_feature_names_out(),
index=housing.index)
housing_prepared_fr.head(2)
pipeline-1__longitude pipeline-1__latitude \
13096 -1.423037 1.013606
14973 0.596394 -0.702103
pipeline-1__housing_median_age pipeline-1__total_rooms \
13096 1.861119 0.311912
14973 0.907630 -0.308620
pipeline-1__total_bedrooms pipeline-1__population \
13096 1.368167 0.137460
14973 -0.435925 -0.693771
pipeline-1__households pipeline-1__median_income \
13096 1.394812 -0.936491
14973 -0.373485 1.171942
pipeline-2__ocean_proximity_<1H OCEAN \
13096 0.0
14973 1.0
pipeline-2__ocean_proximity_INLAND pipeline-2__ocean_proximity_ISLAND \
13096 0.0 0.0
14973 0.0 0.0
pipeline-2__ocean_proximity_NEAR BAY \
13096 1.0
14973 0.0
pipeline-2__ocean_proximity_NEAR OCEAN
13096 0.0
14973 0.0
Bây giờ chúng ta sẽ lắp ráp một ColumnTransformer hoàn chỉnh bao gồm cả việc tạo đặc trưng mới (tỷ lệ phòng, log biến đổi, cụm địa lý…).
def column_ratio(X):
return X[:, [0]] / X[:, [1]]
def ratio_name(function_transformer, feature_names_in):
return ["ratio"]
def ratio_pipeline():
return make_pipeline(
SimpleImputer(strategy="median"),
FunctionTransformer(column_ratio, feature_names_out=ratio_name),
StandardScaler())
log_pipeline = make_pipeline(
SimpleImputer(strategy="median"),
FunctionTransformer(np.log, feature_names_out="one-to-one"),
StandardScaler())
cluster_simil = ClusterSimilarity(n_clusters=10, gamma=1., random_state=42)
default_num_pipeline = make_pipeline(SimpleImputer(strategy="median"),
StandardScaler())
preprocessing = ColumnTransformer([
("bedrooms", ratio_pipeline(), ["total_bedrooms", "total_rooms"]),
("rooms_per_house", ratio_pipeline(), ["total_rooms", "households"]),
("people_per_house", ratio_pipeline(), ["population", "households"]),
("log", log_pipeline, ["total_bedrooms", "total_rooms", "population",
"households", "median_income"]),
("geo", cluster_simil, ["latitude", "longitude"]),
("cat", cat_pipeline, make_column_selector(dtype_include=object)),
],
remainder=default_num_pipeline) # cột còn lại: housing_median_age
housing_prepared = preprocessing.fit_transform(housing)
housing_prepared.shape
(16512, 24)
preprocessing.get_feature_names_out()
array(['bedrooms__ratio', 'rooms_per_house__ratio',
'people_per_house__ratio', 'log__total_bedrooms',
'log__total_rooms', 'log__population', 'log__households',
'log__median_income', 'geo__Cluster 0 similarity',
'geo__Cluster 1 similarity', 'geo__Cluster 2 similarity',
'geo__Cluster 3 similarity', 'geo__Cluster 4 similarity',
'geo__Cluster 5 similarity', 'geo__Cluster 6 similarity',
'geo__Cluster 7 similarity', 'geo__Cluster 8 similarity',
'geo__Cluster 9 similarity', 'cat__ocean_proximity_<1H OCEAN',
'cat__ocean_proximity_INLAND', 'cat__ocean_proximity_ISLAND',
'cat__ocean_proximity_NEAR BAY', 'cat__ocean_proximity_NEAR OCEAN',
'remainder__housing_median_age'], dtype=object)
6. Lựa chọn và Huấn luyện Mô hình (Select and Train a Model)
6.1. Huấn luyện và Đánh giá trên Tập Huấn luyện
Bắt đầu với mô hình đơn giản nhất: Hồi quy Tuyến tính (Linear Regression).
from sklearn.linear_model import LinearRegression
lin_reg = make_pipeline(preprocessing, LinearRegression())
lin_reg.fit(housing, housing_labels)
Pipeline(steps=[('columntransformer',
ColumnTransformer(remainder=Pipeline(steps=[('simpleimputer',
SimpleImputer(strategy='median')),
('standardscaler',
StandardScaler())]),
transformers=[('bedrooms',
Pipeline(steps=[('simpleimputer',
SimpleImputer(strategy='median')),
('functiontransformer',
FunctionTransformer(feature_names_out=<function ratio_name at 0x7e2...
'median_income']),
('geo',
ClusterSimilarity(random_state=42),
['latitude', 'longitude']),
('cat',
Pipeline(steps=[('simpleimputer',
SimpleImputer(strategy='most_frequent')),
('onehotencoder',
OneHotEncoder(handle_unknown='ignore'))]),
<sklearn.compose._column_transformer.make_column_selector object at 0x7e2b87d49940>)])),
('linearregression', LinearRegression())])Pipeline(steps=[('columntransformer',
ColumnTransformer(remainder=Pipeline(steps=[('simpleimputer',
SimpleImputer(strategy='median')),
('standardscaler',
StandardScaler())]),
transformers=[('bedrooms',
Pipeline(steps=[('simpleimputer',
SimpleImputer(strategy='median')),
('functiontransformer',
FunctionTransformer(feature_names_out=<function ratio_name at 0x7e2...
'median_income']),
('geo',
ClusterSimilarity(random_state=42),
['latitude', 'longitude']),
('cat',
Pipeline(steps=[('simpleimputer',
SimpleImputer(strategy='most_frequent')),
('onehotencoder',
OneHotEncoder(handle_unknown='ignore'))]),
<sklearn.compose._column_transformer.make_column_selector object at 0x7e2b87d49940>)])),
('linearregression', LinearRegression())])ColumnTransformer(remainder=Pipeline(steps=[('simpleimputer',
SimpleImputer(strategy='median')),
('standardscaler',
StandardScaler())]),
transformers=[('bedrooms',
Pipeline(steps=[('simpleimputer',
SimpleImputer(strategy='median')),
('functiontransformer',
FunctionTransformer(feature_names_out=<function ratio_name at 0x7e2b87d589a0>,
func=<function column_ratio...
['total_bedrooms', 'total_rooms', 'population',
'households', 'median_income']),
('geo', ClusterSimilarity(random_state=42),
['latitude', 'longitude']),
('cat',
Pipeline(steps=[('simpleimputer',
SimpleImputer(strategy='most_frequent')),
('onehotencoder',
OneHotEncoder(handle_unknown='ignore'))]),
<sklearn.compose._column_transformer.make_column_selector object at 0x7e2b87d49940>)])['total_bedrooms', 'total_rooms']
SimpleImputer(strategy='median')
FunctionTransformer(feature_names_out=<function ratio_name at 0x7e2b87d589a0>,
func=<function column_ratio at 0x7e2b87d58900>)StandardScaler()
['total_rooms', 'households']
SimpleImputer(strategy='median')
FunctionTransformer(feature_names_out=<function ratio_name at 0x7e2b87d589a0>,
func=<function column_ratio at 0x7e2b87d58900>)StandardScaler()
['population', 'households']
SimpleImputer(strategy='median')
FunctionTransformer(feature_names_out=<function ratio_name at 0x7e2b87d589a0>,
func=<function column_ratio at 0x7e2b87d58900>)StandardScaler()
['total_bedrooms', 'total_rooms', 'population', 'households', 'median_income']
SimpleImputer(strategy='median')
FunctionTransformer(feature_names_out='one-to-one', func=<ufunc 'log'>)
StandardScaler()
['latitude', 'longitude']
ClusterSimilarity(random_state=42)
<sklearn.compose._column_transformer.make_column_selector object at 0x7e2b87d49940>
SimpleImputer(strategy='most_frequent')
OneHotEncoder(handle_unknown='ignore')
['housing_median_age']
SimpleImputer(strategy='median')
StandardScaler()
LinearRegression()
Thử dự đoán trên một vài mẫu:
housing_predictions = lin_reg.predict(housing)
housing_predictions[:5].round(-2)
array([246000., 372700., 135700., 91400., 330900.])
So sánh với nhãn thực tế:
housing_labels.iloc[:5].values
array([458300., 483800., 101700., 96100., 361800.])
Tính tỷ lệ lỗi:
# extra code – tính tỷ lệ lỗi
error_ratios = housing_predictions[:5].round(-2) / housing_labels.iloc[:5].values - 1
print(", ".join([f"{100 * ratio:.1f}%" for ratio in error_ratios]))
-46.3%, -23.0%, 33.4%, -4.9%, -8.5%
Đo lường sai số bằng RMSE (Root Mean Squared Error):
from sklearn.metrics import root_mean_squared_error
lin_rmse = root_mean_squared_error(housing_labels, housing_predictions)
lin_rmse
68972.88910758484
Kết quả RMSE khá lớn (68972 USD). Có vẻ mô hình đang bị Underfitting (kém khớp). Thử một mô hình phức tạp hơn: Cây quyết định (Decision Tree).
from sklearn.tree import DecisionTreeRegressor
tree_reg = make_pipeline(preprocessing, DecisionTreeRegressor(random_state=42))
tree_reg.fit(housing, housing_labels)
Pipeline(steps=[('columntransformer',
ColumnTransformer(remainder=Pipeline(steps=[('simpleimputer',
SimpleImputer(strategy='median')),
('standardscaler',
StandardScaler())]),
transformers=[('bedrooms',
Pipeline(steps=[('simpleimputer',
SimpleImputer(strategy='median')),
('functiontransformer',
FunctionTransformer(feature_names_out=<function ratio_name at 0x7e2...
ClusterSimilarity(random_state=42),
['latitude', 'longitude']),
('cat',
Pipeline(steps=[('simpleimputer',
SimpleImputer(strategy='most_frequent')),
('onehotencoder',
OneHotEncoder(handle_unknown='ignore'))]),
<sklearn.compose._column_transformer.make_column_selector object at 0x7e2b87d49940>)])),
('decisiontreeregressor',
DecisionTreeRegressor(random_state=42))])Pipeline(steps=[('columntransformer',
ColumnTransformer(remainder=Pipeline(steps=[('simpleimputer',
SimpleImputer(strategy='median')),
('standardscaler',
StandardScaler())]),
transformers=[('bedrooms',
Pipeline(steps=[('simpleimputer',
SimpleImputer(strategy='median')),
('functiontransformer',
FunctionTransformer(feature_names_out=<function ratio_name at 0x7e2...
ClusterSimilarity(random_state=42),
['latitude', 'longitude']),
('cat',
Pipeline(steps=[('simpleimputer',
SimpleImputer(strategy='most_frequent')),
('onehotencoder',
OneHotEncoder(handle_unknown='ignore'))]),
<sklearn.compose._column_transformer.make_column_selector object at 0x7e2b87d49940>)])),
('decisiontreeregressor',
DecisionTreeRegressor(random_state=42))])ColumnTransformer(remainder=Pipeline(steps=[('simpleimputer',
SimpleImputer(strategy='median')),
('standardscaler',
StandardScaler())]),
transformers=[('bedrooms',
Pipeline(steps=[('simpleimputer',
SimpleImputer(strategy='median')),
('functiontransformer',
FunctionTransformer(feature_names_out=<function ratio_name at 0x7e2b87d589a0>,
func=<function column_ratio...
['total_bedrooms', 'total_rooms', 'population',
'households', 'median_income']),
('geo', ClusterSimilarity(random_state=42),
['latitude', 'longitude']),
('cat',
Pipeline(steps=[('simpleimputer',
SimpleImputer(strategy='most_frequent')),
('onehotencoder',
OneHotEncoder(handle_unknown='ignore'))]),
<sklearn.compose._column_transformer.make_column_selector object at 0x7e2b87d49940>)])['total_bedrooms', 'total_rooms']
SimpleImputer(strategy='median')
FunctionTransformer(feature_names_out=<function ratio_name at 0x7e2b87d589a0>,
func=<function column_ratio at 0x7e2b87d58900>)StandardScaler()
['total_rooms', 'households']
SimpleImputer(strategy='median')
FunctionTransformer(feature_names_out=<function ratio_name at 0x7e2b87d589a0>,
func=<function column_ratio at 0x7e2b87d58900>)StandardScaler()
['population', 'households']
SimpleImputer(strategy='median')
FunctionTransformer(feature_names_out=<function ratio_name at 0x7e2b87d589a0>,
func=<function column_ratio at 0x7e2b87d58900>)StandardScaler()
['total_bedrooms', 'total_rooms', 'population', 'households', 'median_income']
SimpleImputer(strategy='median')
FunctionTransformer(feature_names_out='one-to-one', func=<ufunc 'log'>)
StandardScaler()
['latitude', 'longitude']
ClusterSimilarity(random_state=42)
<sklearn.compose._column_transformer.make_column_selector object at 0x7e2b87d49940>
SimpleImputer(strategy='most_frequent')
OneHotEncoder(handle_unknown='ignore')
['housing_median_age']
SimpleImputer(strategy='median')
StandardScaler()
DecisionTreeRegressor(random_state=42)
housing_predictions = tree_reg.predict(housing)
tree_rmse = root_mean_squared_error(housing_labels, housing_predictions)
tree_rmse
0.0
RMSE = 0.0? Điều này cực kỳ khả nghi. Có thể mô hình đã bị Overfitting (quá khớp) dữ liệu huấn luyện.
6.2. Đánh giá tốt hơn bằng Cross-Validation
Thay vì dùng tập test (điều cấm kỵ), ta dùng K-fold Cross-Validation: chia tập train thành 10 phần, huấn luyện trên 9 phần và kiểm tra trên 1 phần, lặp lại 10 lần.
from sklearn.model_selection import cross_val_score
tree_rmses = -cross_val_score(tree_reg, housing, housing_labels,
scoring="neg_root_mean_squared_error", cv=10)
pd.Series(tree_rmses).describe()
| 0 | |
|---|---|
| count | 10.000000 |
| mean | 66573.734600 |
| std | 1103.402323 |
| min | 64607.896046 |
| 25% | 66204.731788 |
| 50% | 66388.272499 |
| 75% | 66826.257468 |
| max | 68532.210664 |
Với Cross-Validation, Decision Tree có RMSE trung bình khoảng 66,573 USD, không tốt hơn Linear Regression là bao.
Kiểm tra Cross-Validation cho Linear Regression:
# extra code
lin_rmses = -cross_val_score(lin_reg, housing, housing_labels,
scoring="neg_root_mean_squared_error", cv=10)
pd.Series(lin_rmses).describe()
| 0 | |
|---|---|
| count | 10.000000 |
| mean | 70003.404818 |
| std | 4182.188328 |
| min | 65504.765753 |
| 25% | 68172.065831 |
| 50% | 68743.995249 |
| 75% | 70344.943988 |
| max | 81037.863741 |
Thử nghiệm với Random Forest Regressor (Rừng ngẫu nhiên), một mô hình mạnh mẽ hơn hoạt động bằng cách kết hợp nhiều cây quyết định.
Cảnh báo: Đoạn mã sau có thể chạy mất nhiều phút.
from sklearn.ensemble import RandomForestRegressor
forest_reg = make_pipeline(preprocessing,
RandomForestRegressor(random_state=42))
forest_rmses = -cross_val_score(forest_reg, housing, housing_labels,
scoring="neg_root_mean_squared_error", cv=10)
pd.Series(forest_rmses).describe()
| 0 | |
|---|---|
| count | 10.000000 |
| mean | 47038.092799 |
| std | 1021.491757 |
| min | 45495.976649 |
| 25% | 46510.418013 |
| 50% | 47118.719249 |
| 75% | 47480.519175 |
| max | 49140.832210 |
Random Forest cho kết quả tốt hơn hẳn (RMSE ~47,038). Tuy nhiên, hãy so sánh với lỗi trên tập huấn luyện:
forest_reg.fit(housing, housing_labels)
housing_predictions = forest_reg.predict(housing)
forest_rmse = root_mean_squared_error(housing_labels, housing_predictions)
forest_rmse
17551.2122500877
Lỗi trên tập train (17,551) thấp hơn nhiều so với lỗi validation (47,038), chứng tỏ mô hình vẫn đang bị Overfitting.
7. Tinh chỉnh Mô hình (Fine-Tune Your Model)
7.1. Tìm kiếm Lưới (Grid Search)
Thay vì thử thủ công từng tham số, ta dùng GridSearchCV để Scikit-Learn tự động thử tất cả các tổ hợp tham số mà ta định nghĩa.
Cảnh báo: Đoạn mã sau có thể chạy mất nhiều phút.
from sklearn.model_selection import GridSearchCV
full_pipeline = Pipeline([
("preprocessing", preprocessing),
("random_forest", RandomForestRegressor(random_state=42)),
])
param_grid = [
{'preprocessing__geo__n_clusters': [5, 8, 10],
'random_forest__max_features': [4, 6, 8]},
{'preprocessing__geo__n_clusters': [10, 15],
'random_forest__max_features': [6, 8, 10]},
]
grid_search = GridSearchCV(full_pipeline, param_grid, cv=3,
scoring='neg_root_mean_squared_error')
grid_search.fit(housing, housing_labels)
GridSearchCV(cv=3,
estimator=Pipeline(steps=[('preprocessing',
ColumnTransformer(remainder=Pipeline(steps=[('simpleimputer',
SimpleImputer(strategy='median')),
('standardscaler',
StandardScaler())]),
transformers=[('bedrooms',
Pipeline(steps=[('simpleimputer',
SimpleImputer(strategy='median')),
('functiontransformer',
FunctionTransformer(feature_names_out=<f...
<sklearn.compose._column_transformer.make_column_selector object at 0x7e2b87d49940>)])),
('random_forest',
RandomForestRegressor(random_state=42))]),
param_grid=[{'preprocessing__geo__n_clusters': [5, 8, 10],
'random_forest__max_features': [4, 6, 8]},
{'preprocessing__geo__n_clusters': [10, 15],
'random_forest__max_features': [6, 8, 10]}],
scoring='neg_root_mean_squared_error')GridSearchCV(cv=3,
estimator=Pipeline(steps=[('preprocessing',
ColumnTransformer(remainder=Pipeline(steps=[('simpleimputer',
SimpleImputer(strategy='median')),
('standardscaler',
StandardScaler())]),
transformers=[('bedrooms',
Pipeline(steps=[('simpleimputer',
SimpleImputer(strategy='median')),
('functiontransformer',
FunctionTransformer(feature_names_out=<f...
<sklearn.compose._column_transformer.make_column_selector object at 0x7e2b87d49940>)])),
('random_forest',
RandomForestRegressor(random_state=42))]),
param_grid=[{'preprocessing__geo__n_clusters': [5, 8, 10],
'random_forest__max_features': [4, 6, 8]},
{'preprocessing__geo__n_clusters': [10, 15],
'random_forest__max_features': [6, 8, 10]}],
scoring='neg_root_mean_squared_error')Pipeline(steps=[('preprocessing',
ColumnTransformer(remainder=Pipeline(steps=[('simpleimputer',
SimpleImputer(strategy='median')),
('standardscaler',
StandardScaler())]),
transformers=[('bedrooms',
Pipeline(steps=[('simpleimputer',
SimpleImputer(strategy='median')),
('functiontransformer',
FunctionTransformer(feature_names_out=<function ratio_name at 0x7e2b87d...
ClusterSimilarity(n_clusters=15,
random_state=42),
['latitude', 'longitude']),
('cat',
Pipeline(steps=[('simpleimputer',
SimpleImputer(strategy='most_frequent')),
('onehotencoder',
OneHotEncoder(handle_unknown='ignore'))]),
<sklearn.compose._column_transformer.make_column_selector object at 0x7e2b8a422270>)])),
('random_forest',
RandomForestRegressor(max_features=6, random_state=42))])ColumnTransformer(remainder=Pipeline(steps=[('simpleimputer',
SimpleImputer(strategy='median')),
('standardscaler',
StandardScaler())]),
transformers=[('bedrooms',
Pipeline(steps=[('simpleimputer',
SimpleImputer(strategy='median')),
('functiontransformer',
FunctionTransformer(feature_names_out=<function ratio_name at 0x7e2b87d589a0>,
func=<function column_ratio...
['total_bedrooms', 'total_rooms', 'population',
'households', 'median_income']),
('geo',
ClusterSimilarity(n_clusters=15,
random_state=42),
['latitude', 'longitude']),
('cat',
Pipeline(steps=[('simpleimputer',
SimpleImputer(strategy='most_frequent')),
('onehotencoder',
OneHotEncoder(handle_unknown='ignore'))]),
<sklearn.compose._column_transformer.make_column_selector object at 0x7e2b8a422270>)])['total_bedrooms', 'total_rooms']
SimpleImputer(strategy='median')
FunctionTransformer(feature_names_out=<function ratio_name at 0x7e2b87d589a0>,
func=<function column_ratio at 0x7e2b87d58900>)StandardScaler()
['total_rooms', 'households']
SimpleImputer(strategy='median')
FunctionTransformer(feature_names_out=<function ratio_name at 0x7e2b87d589a0>,
func=<function column_ratio at 0x7e2b87d58900>)StandardScaler()
['population', 'households']
SimpleImputer(strategy='median')
FunctionTransformer(feature_names_out=<function ratio_name at 0x7e2b87d589a0>,
func=<function column_ratio at 0x7e2b87d58900>)StandardScaler()
['total_bedrooms', 'total_rooms', 'population', 'households', 'median_income']
SimpleImputer(strategy='median')
FunctionTransformer(feature_names_out='one-to-one', func=<ufunc 'log'>)
StandardScaler()
['latitude', 'longitude']
ClusterSimilarity(n_clusters=15, random_state=42)
<sklearn.compose._column_transformer.make_column_selector object at 0x7e2b8a422270>
SimpleImputer(strategy='most_frequent')
OneHotEncoder(handle_unknown='ignore')
['housing_median_age']
SimpleImputer(strategy='median')
StandardScaler()
RandomForestRegressor(max_features=6, random_state=42)
Bạn có thể xem tên các tham số có thể tinh chỉnh bằng lệnh sau:
# extra code – hiển thị một phần output của get_params().keys()
print(str(full_pipeline.get_params().keys())[:1000] + "...")
dict_keys(['memory', 'steps', 'transform_input', 'verbose', 'preprocessing', 'random_forest', 'preprocessing__force_int_remainder_cols', 'preprocessing__n_jobs', 'preprocessing__remainder__memory', 'preprocessing__remainder__steps', 'preprocessing__remainder__transform_input', 'preprocessing__remainder__verbose', 'preprocessing__remainder__simpleimputer', 'preprocessing__remainder__standardscaler', 'preprocessing__remainder__simpleimputer__add_indicator', 'preprocessing__remainder__simpleimputer__copy', 'preprocessing__remainder__simpleimputer__fill_value', 'preprocessing__remainder__simpleimputer__keep_empty_features', 'preprocessing__remainder__simpleimputer__missing_values', 'preprocessing__remainder__simpleimputer__strategy', 'preprocessing__remainder__standardscaler__copy', 'preprocessing__remainder__standardscaler__with_mean', 'preprocessing__remainder__standardscaler__with_std', 'preprocessing__remainder', 'preprocessing__sparse_threshold', 'preprocessing__transformer_weights', ...
Các tham số tốt nhất tìm được:
grid_search.best_params_
{'preprocessing__geo__n_clusters': 15, 'random_forest__max_features': 6}
Mô hình tốt nhất:
grid_search.best_estimator_
Pipeline(steps=[('preprocessing',
ColumnTransformer(remainder=Pipeline(steps=[('simpleimputer',
SimpleImputer(strategy='median')),
('standardscaler',
StandardScaler())]),
transformers=[('bedrooms',
Pipeline(steps=[('simpleimputer',
SimpleImputer(strategy='median')),
('functiontransformer',
FunctionTransformer(feature_names_out=<function ratio_name at 0x7e2b87d...
ClusterSimilarity(n_clusters=15,
random_state=42),
['latitude', 'longitude']),
('cat',
Pipeline(steps=[('simpleimputer',
SimpleImputer(strategy='most_frequent')),
('onehotencoder',
OneHotEncoder(handle_unknown='ignore'))]),
<sklearn.compose._column_transformer.make_column_selector object at 0x7e2b8a422270>)])),
('random_forest',
RandomForestRegressor(max_features=6, random_state=42))])Pipeline(steps=[('preprocessing',
ColumnTransformer(remainder=Pipeline(steps=[('simpleimputer',
SimpleImputer(strategy='median')),
('standardscaler',
StandardScaler())]),
transformers=[('bedrooms',
Pipeline(steps=[('simpleimputer',
SimpleImputer(strategy='median')),
('functiontransformer',
FunctionTransformer(feature_names_out=<function ratio_name at 0x7e2b87d...
ClusterSimilarity(n_clusters=15,
random_state=42),
['latitude', 'longitude']),
('cat',
Pipeline(steps=[('simpleimputer',
SimpleImputer(strategy='most_frequent')),
('onehotencoder',
OneHotEncoder(handle_unknown='ignore'))]),
<sklearn.compose._column_transformer.make_column_selector object at 0x7e2b8a422270>)])),
('random_forest',
RandomForestRegressor(max_features=6, random_state=42))])ColumnTransformer(remainder=Pipeline(steps=[('simpleimputer',
SimpleImputer(strategy='median')),
('standardscaler',
StandardScaler())]),
transformers=[('bedrooms',
Pipeline(steps=[('simpleimputer',
SimpleImputer(strategy='median')),
('functiontransformer',
FunctionTransformer(feature_names_out=<function ratio_name at 0x7e2b87d589a0>,
func=<function column_ratio...
['total_bedrooms', 'total_rooms', 'population',
'households', 'median_income']),
('geo',
ClusterSimilarity(n_clusters=15,
random_state=42),
['latitude', 'longitude']),
('cat',
Pipeline(steps=[('simpleimputer',
SimpleImputer(strategy='most_frequent')),
('onehotencoder',
OneHotEncoder(handle_unknown='ignore'))]),
<sklearn.compose._column_transformer.make_column_selector object at 0x7e2b8a422270>)])['total_bedrooms', 'total_rooms']
SimpleImputer(strategy='median')
FunctionTransformer(feature_names_out=<function ratio_name at 0x7e2b87d589a0>,
func=<function column_ratio at 0x7e2b87d58900>)StandardScaler()
['total_rooms', 'households']
SimpleImputer(strategy='median')
FunctionTransformer(feature_names_out=<function ratio_name at 0x7e2b87d589a0>,
func=<function column_ratio at 0x7e2b87d58900>)StandardScaler()
['population', 'households']
SimpleImputer(strategy='median')
FunctionTransformer(feature_names_out=<function ratio_name at 0x7e2b87d589a0>,
func=<function column_ratio at 0x7e2b87d58900>)StandardScaler()
['total_bedrooms', 'total_rooms', 'population', 'households', 'median_income']
SimpleImputer(strategy='median')
FunctionTransformer(feature_names_out='one-to-one', func=<ufunc 'log'>)
StandardScaler()
['latitude', 'longitude']
ClusterSimilarity(n_clusters=15, random_state=42)
<sklearn.compose._column_transformer.make_column_selector object at 0x7e2b8a422270>
SimpleImputer(strategy='most_frequent')
OneHotEncoder(handle_unknown='ignore')
['housing_median_age']
SimpleImputer(strategy='median')
StandardScaler()
RandomForestRegressor(max_features=6, random_state=42)
Xem kết quả chi tiết của từng lần thử:
cv_res = pd.DataFrame(grid_search.cv_results_)
cv_res.sort_values(by="mean_test_score", ascending=False, inplace=True)
# extra code – làm đẹp bảng kết quả
cv_res = cv_res[["param_preprocessing__geo__n_clusters",
"param_random_forest__max_features", "split0_test_score",
"split1_test_score", "split2_test_score", "mean_test_score"]]
score_cols = ["split0", "split1", "split2", "mean_test_rmse"]
cv_res.columns = ["n_clusters", "max_features"] + score_cols
cv_res[score_cols] = -cv_res[score_cols].round().astype(np.int64)
cv_res.head()
n_clusters max_features split0 split1 split2 mean_test_rmse
12 15 6 42725 43708 44335 43590
13 15 8 43486 43820 44900 44069
6 10 4 43798 44036 44961 44265
9 10 6 43710 44163 44967 44280
7 10 6 43710 44163 44967 44280
7.2. Tìm kiếm Ngẫu nhiên (Randomized Search)
Khi không gian tham số quá lớn, GridSearchCV tốn quá nhiều thời gian. RandomizedSearchCV chọn ngẫu nhiên các tổ hợp tham số, giúp khám phá không gian rộng hơn hiệu quả hơn.
from sklearn.experimental import enable_halving_search_cv
from sklearn.model_selection import HalvingRandomSearchCV
Cảnh báo: Đoạn mã sau có thể chạy mất nhiều phút.
from sklearn.model_selection import RandomizedSearchCV
from scipy.stats import randint
param_distribs = {'preprocessing__geo__n_clusters': randint(low=3, high=50),
'random_forest__max_features': randint(low=2, high=20)}
rnd_search = RandomizedSearchCV(
full_pipeline, param_distributions=param_distribs, n_iter=10, cv=3,
scoring='neg_root_mean_squared_error', random_state=42)
rnd_search.fit(housing, housing_labels)
RandomizedSearchCV(cv=3,
estimator=Pipeline(steps=[('preprocessing',
ColumnTransformer(remainder=Pipeline(steps=[('simpleimputer',
SimpleImputer(strategy='median')),
('standardscaler',
StandardScaler())]),
transformers=[('bedrooms',
Pipeline(steps=[('simpleimputer',
SimpleImputer(strategy='median')),
('functiontransformer',
FunctionTransformer(feature_names_...
('random_forest',
RandomForestRegressor(random_state=42))]),
param_distributions={'preprocessing__geo__n_clusters': <scipy.stats._distn_infrastructure.rv_discrete_frozen object at 0x7e2b87d4ac60>,
'random_forest__max_features': <scipy.stats._distn_infrastructure.rv_discrete_frozen object at 0x7e2b8b770ce0>},
random_state=42, scoring='neg_root_mean_squared_error')RandomizedSearchCV(cv=3,
estimator=Pipeline(steps=[('preprocessing',
ColumnTransformer(remainder=Pipeline(steps=[('simpleimputer',
SimpleImputer(strategy='median')),
('standardscaler',
StandardScaler())]),
transformers=[('bedrooms',
Pipeline(steps=[('simpleimputer',
SimpleImputer(strategy='median')),
('functiontransformer',
FunctionTransformer(feature_names_...
('random_forest',
RandomForestRegressor(random_state=42))]),
param_distributions={'preprocessing__geo__n_clusters': <scipy.stats._distn_infrastructure.rv_discrete_frozen object at 0x7e2b87d4ac60>,
'random_forest__max_features': <scipy.stats._distn_infrastructure.rv_discrete_frozen object at 0x7e2b8b770ce0>},
random_state=42, scoring='neg_root_mean_squared_error')Pipeline(steps=[('preprocessing',
ColumnTransformer(remainder=Pipeline(steps=[('simpleimputer',
SimpleImputer(strategy='median')),
('standardscaler',
StandardScaler())]),
transformers=[('bedrooms',
Pipeline(steps=[('simpleimputer',
SimpleImputer(strategy='median')),
('functiontransformer',
FunctionTransformer(feature_names_out=<function ratio_name at 0x7e2b87d...
ClusterSimilarity(n_clusters=45,
random_state=42),
['latitude', 'longitude']),
('cat',
Pipeline(steps=[('simpleimputer',
SimpleImputer(strategy='most_frequent')),
('onehotencoder',
OneHotEncoder(handle_unknown='ignore'))]),
<sklearn.compose._column_transformer.make_column_selector object at 0x7e2b8a3e7f50>)])),
('random_forest',
RandomForestRegressor(max_features=9, random_state=42))])ColumnTransformer(remainder=Pipeline(steps=[('simpleimputer',
SimpleImputer(strategy='median')),
('standardscaler',
StandardScaler())]),
transformers=[('bedrooms',
Pipeline(steps=[('simpleimputer',
SimpleImputer(strategy='median')),
('functiontransformer',
FunctionTransformer(feature_names_out=<function ratio_name at 0x7e2b87d589a0>,
func=<function column_ratio...
['total_bedrooms', 'total_rooms', 'population',
'households', 'median_income']),
('geo',
ClusterSimilarity(n_clusters=45,
random_state=42),
['latitude', 'longitude']),
('cat',
Pipeline(steps=[('simpleimputer',
SimpleImputer(strategy='most_frequent')),
('onehotencoder',
OneHotEncoder(handle_unknown='ignore'))]),
<sklearn.compose._column_transformer.make_column_selector object at 0x7e2b8a3e7f50>)])['total_bedrooms', 'total_rooms']
SimpleImputer(strategy='median')
FunctionTransformer(feature_names_out=<function ratio_name at 0x7e2b87d589a0>,
func=<function column_ratio at 0x7e2b87d58900>)StandardScaler()
['total_rooms', 'households']
SimpleImputer(strategy='median')
FunctionTransformer(feature_names_out=<function ratio_name at 0x7e2b87d589a0>,
func=<function column_ratio at 0x7e2b87d58900>)StandardScaler()
['population', 'households']
SimpleImputer(strategy='median')
FunctionTransformer(feature_names_out=<function ratio_name at 0x7e2b87d589a0>,
func=<function column_ratio at 0x7e2b87d58900>)StandardScaler()
['total_bedrooms', 'total_rooms', 'population', 'households', 'median_income']
SimpleImputer(strategy='median')
FunctionTransformer(feature_names_out='one-to-one', func=<ufunc 'log'>)
StandardScaler()
['latitude', 'longitude']
ClusterSimilarity(n_clusters=45, random_state=42)
<sklearn.compose._column_transformer.make_column_selector object at 0x7e2b8a3e7f50>
SimpleImputer(strategy='most_frequent')
OneHotEncoder(handle_unknown='ignore')
['housing_median_age']
SimpleImputer(strategy='median')
StandardScaler()
RandomForestRegressor(max_features=9, random_state=42)
Xem kết quả tìm kiếm ngẫu nhiên:
# extra code – hiển thị kết quả random search
cv_res = pd.DataFrame(rnd_search.cv_results_)
cv_res.sort_values(by="mean_test_score", ascending=False, inplace=True)
cv_res = cv_res[["param_preprocessing__geo__n_clusters",
"param_random_forest__max_features", "split0_test_score",
"split1_test_score", "split2_test_score", "mean_test_score"]]
cv_res.columns = ["n_clusters", "max_features"] + score_cols
cv_res[score_cols] = -cv_res[score_cols].round().astype(np.int64)
cv_res.head()
n_clusters max_features split0 split1 split2 mean_test_rmse
1 45 9 41342 42242 43057 42214
8 32 7 41825 42275 43241 42447
0 41 16 42238 42938 43354 42843
5 42 4 41869 43362 43664 42965
2 23 8 42490 42928 43718 43046
Mẹo chọn phân phối xác suất cho tham số:
Sử dụng các hàm từ scipy.stats để định nghĩa cách chọn tham số (ví dụ: loguniform cho các tham số thay đổi theo cấp số nhân).
# extra code – vẽ biểu đồ minh họa các phân phối xác suất
from scipy.stats import randint, uniform, geom, expon
xs1 = np.arange(0, 7 + 1)
randint_distrib = randint(0, 7 + 1).pmf(xs1)
xs2 = np.linspace(0, 7, 500)
uniform_distrib = uniform(0, 7).pdf(xs2)
xs3 = np.arange(0, 7 + 1)
geom_distrib = geom(0.5).pmf(xs3)
xs4 = np.linspace(0, 7, 500)
expon_distrib = expon(scale=1).pdf(xs4)
plt.figure(figsize=(12, 7))
plt.subplot(2, 2, 1)
plt.bar(xs1, randint_distrib, label="scipy.randint(0, 7 + 1)")
plt.ylabel("Probability")
plt.legend()
plt.axis([-1, 8, 0, 0.2])
plt.subplot(2, 2, 2)
plt.fill_between(xs2, uniform_distrib, label="scipy.uniform(0, 7)")
plt.ylabel("PDF")
plt.legend()
plt.axis([-1, 8, 0, 0.2])
plt.subplot(2, 2, 3)
plt.bar(xs3, geom_distrib, label="scipy.geom(0.5)")
plt.xlabel("Hyperparameter value")
plt.ylabel("Probability")
plt.legend()
plt.axis([0, 7, 0, 1])
plt.subplot(2, 2, 4)
plt.fill_between(xs4, expon_distrib, label="scipy.expon(scale=1)")
plt.xlabel("Hyperparameter value")
plt.ylabel("PDF")
plt.legend()
plt.axis([0, 7, 0, 1])
plt.show()

Sự khác biệt giữa expon và loguniform:
# extra code – so sánh expon và loguniform
from scipy.stats import loguniform
xs1 = np.linspace(0, 7, 500)
expon_distrib = expon(scale=1).pdf(xs1)
log_xs2 = np.linspace(-5, 3, 500)
log_expon_distrib = np.exp(log_xs2 - np.exp(log_xs2))
xs3 = np.linspace(0.001, 1000, 500)
loguniform_distrib = loguniform(0.001, 1000).pdf(xs3)
log_xs4 = np.linspace(np.log(0.001), np.log(1000), 500)
log_loguniform_distrib = uniform(np.log(0.001), np.log(1000)).pdf(log_xs4)
plt.figure(figsize=(12, 7))
plt.subplot(2, 2, 1)
plt.fill_between(xs1, expon_distrib,
label="scipy.expon(scale=1)")
plt.ylabel("PDF")
plt.legend()
plt.axis([0, 7, 0, 1])
plt.subplot(2, 2, 2)
plt.fill_between(log_xs2, log_expon_distrib,
label="log(X) with X ~ expon")
plt.legend()
plt.axis([-5, 3, 0, 1])
plt.subplot(2, 2, 3)
plt.fill_between(xs3, loguniform_distrib,
label="scipy.loguniform(0.001, 1000)")
plt.xlabel("Hyperparameter value")
plt.ylabel("PDF")
plt.legend()
plt.axis([0.001, 1000, 0, 0.005])
plt.subplot(2, 2, 4)
plt.fill_between(log_xs4, log_loguniform_distrib,
label="log(X) with X ~ loguniform")
plt.xlabel("Log of hyperparameter value")
plt.legend()
plt.axis([-8, 1, 0, 0.2])
plt.show()

7.3. Phân tích Mô hình Tốt nhất
Chúng ta có thể xem xét tầm quan trọng của các đặc trưng (feature importance) để hiểu mô hình đang dựa vào đâu để dự đoán.
final_model = rnd_search.best_estimator_ # bao gồm cả preprocessing
feature_importances = final_model["random_forest"].feature_importances_
feature_importances.round(2)
array([0.07, 0.05, 0.05, 0.01, 0.01, 0.01, 0.01, 0.19, 0.01, 0.02, 0.01,
0.01, 0.01, 0. , 0.01, 0.02, 0.01, 0.02, 0.01, 0. , 0.01, 0.02,
0.01, 0.01, 0.01, 0. , 0.02, 0.01, 0.01, 0. , 0.01, 0.01, 0.01,
0.03, 0.01, 0.01, 0.01, 0.01, 0.04, 0.01, 0.02, 0.01, 0.02, 0.01,
0.02, 0.02, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0. , 0.07,
0. , 0. , 0. , 0.01])
Sắp xếp độ quan trọng giảm dần kèm tên đặc trưng:
sorted(zip(feature_importances,
final_model["preprocessing"].get_feature_names_out()),
reverse=True)
[(np.float64(0.18599734460509476), 'log__median_income'),
(np.float64(0.07338850855844489), 'cat__ocean_proximity_INLAND'),
(np.float64(0.06556941990883976), 'bedrooms__ratio'),
(np.float64(0.053648710076725316), 'rooms_per_house__ratio'),
(np.float64(0.04598870861894749), 'people_per_house__ratio'),
(np.float64(0.04175269214442519), 'geo__Cluster 30 similarity'),
(np.float64(0.025976797232869678), 'geo__Cluster 25 similarity'),
(np.float64(0.023595895886342255), 'geo__Cluster 36 similarity'),
(np.float64(0.02021056221732893), 'geo__Cluster 9 similarity'),
(np.float64(0.01860691707666145), 'geo__Cluster 34 similarity'),
(np.float64(0.018137988374628867), 'geo__Cluster 37 similarity'),
(np.float64(0.01740435316632675), 'geo__Cluster 18 similarity'),
(np.float64(0.016778386143844894), 'geo__Cluster 1 similarity'),
(np.float64(0.015459009666188978), 'geo__Cluster 7 similarity'),
(np.float64(0.015325731028175924), 'geo__Cluster 32 similarity'),
(np.float64(0.015073772015038348), 'geo__Cluster 13 similarity'),
(np.float64(0.014272160962173805), 'geo__Cluster 35 similarity'),
(np.float64(0.014180636461860479), 'geo__Cluster 0 similarity'),
(np.float64(0.013746364498238989), 'geo__Cluster 3 similarity'),
(np.float64(0.01357230570846952), 'geo__Cluster 28 similarity'),
(np.float64(0.01294034969422872), 'geo__Cluster 26 similarity'),
(np.float64(0.012738123746761944), 'geo__Cluster 31 similarity'),
(np.float64(0.011654237215152624), 'geo__Cluster 19 similarity'),
(np.float64(0.011628003598059723), 'geo__Cluster 6 similarity'),
(np.float64(0.011134113333125398), 'geo__Cluster 24 similarity'),
(np.float64(0.011042979326385049), 'remainder__housing_median_age'),
(np.float64(0.010907388443940418), 'geo__Cluster 43 similarity'),
(np.float64(0.010847192663592166), 'geo__Cluster 44 similarity'),
(np.float64(0.010592244492858267), 'geo__Cluster 10 similarity'),
(np.float64(0.010512467290844922), 'geo__Cluster 23 similarity'),
(np.float64(0.01045866561538645), 'geo__Cluster 41 similarity'),
(np.float64(0.010261910692851673), 'geo__Cluster 40 similarity'),
(np.float64(0.009757306983097491), 'geo__Cluster 2 similarity'),
(np.float64(0.00965993322211448), 'geo__Cluster 12 similarity'),
(np.float64(0.009574969190852869), 'geo__Cluster 14 similarity'),
(np.float64(0.008199144719918425), 'geo__Cluster 20 similarity'),
(np.float64(0.008141941480860806), 'geo__Cluster 33 similarity'),
(np.float64(0.007596761219964691), 'geo__Cluster 8 similarity'),
(np.float64(0.0075762980128490295), 'geo__Cluster 22 similarity'),
(np.float64(0.007346290789504319), 'geo__Cluster 39 similarity'),
(np.float64(0.006898774333063982), 'geo__Cluster 4 similarity'),
(np.float64(0.0067947318450798395), 'log__total_rooms'),
(np.float64(0.006514889773323568), 'log__population'),
(np.float64(0.006350528211987125), 'geo__Cluster 27 similarity'),
(np.float64(0.006337558749902337), 'geo__Cluster 16 similarity'),
(np.float64(0.006231053672395539), 'geo__Cluster 38 similarity'),
(np.float64(0.0061213483458714855), 'log__households'),
(np.float64(0.005849842001582111), 'log__total_bedrooms'),
(np.float64(0.0056783104666850125), 'geo__Cluster 15 similarity'),
(np.float64(0.005479729990673467), 'geo__Cluster 29 similarity'),
(np.float64(0.005348325088535128), 'geo__Cluster 42 similarity'),
(np.float64(0.004866251452445486), 'geo__Cluster 17 similarity'),
(np.float64(0.004495340541933027), 'geo__Cluster 11 similarity'),
(np.float64(0.004418821635620684), 'geo__Cluster 5 similarity'),
(np.float64(0.0035344732505291285), 'geo__Cluster 21 similarity'),
(np.float64(0.001832424657341851), 'cat__ocean_proximity_<1H OCEAN'),
(np.float64(0.0015282226447271795), 'cat__ocean_proximity_NEAR OCEAN'),
(np.float64(0.0004325970342247361), 'cat__ocean_proximity_NEAR BAY'),
(np.float64(3.0190221102670295e-05), 'cat__ocean_proximity_ISLAND')]
Kết quả cho thấy log__median_income và cat__ocean_proximity_INLAND là những yếu tố quan trọng nhất.
7.4. Đánh giá trên Tập Kiểm tra (Evaluate on Test Set)
Cuối cùng, ta đánh giá mô hình trên tập test. Nhớ chỉ dùng transform, không dùng fit trên tập test.
X_test = strat_test_set.drop("median_house_value", axis=1)
y_test = strat_test_set["median_house_value"].copy()
final_predictions = final_model.predict(X_test)
final_rmse = root_mean_squared_error(y_test, final_predictions)
print(final_rmse)
41445.533268606625
Tính khoảng tin cậy 95% cho RMSE:
from scipy.stats import bootstrap
def rmse(squared_errors):
return np.sqrt(np.mean(squared_errors))
confidence = 0.95
squared_errors = (final_predictions - y_test) ** 2
boot_result = bootstrap([squared_errors], rmse, confidence_level=confidence,
random_state=42)
rmse_lower, rmse_upper = boot_result.confidence_interval
print(f"95% CI for RMSE: ({rmse_lower:.4f}, {rmse_upper:.4f})")
95% CI for RMSE: (39520.9572, 43701.7681)
8. Lưu và Triển khai Mô hình
Sử dụng joblib để lưu mô hình ra file, giúp tái sử dụng sau này mà không cần huấn luyện lại.
import joblib
joblib.dump(final_model, "my_california_housing_model.pkl")
['my_california_housing_model.pkl']
Mô phỏng việc tải mô hình và dự đoán dữ liệu mới trong môi trường production:
import joblib
# extra code – định nghĩa lại các thành phần phụ thuộc nếu tải ở script khác
from sklearn.cluster import KMeans
from sklearn.base import BaseEstimator, TransformerMixin
from sklearn.metrics.pairwise import rbf_kernel
def column_ratio(X):
return X[:, [0]] / X[:, [1]]
# class ClusterSimilarity(BaseEstimator, TransformerMixin):
# [...]
final_model_reloaded = joblib.load("my_california_housing_model.pkl")
new_data = housing.iloc[:5] # giả sử đây là dữ liệu mới
predictions = final_model_reloaded.predict(new_data)
predictions
array([441046.12, 454713.09, 104832. , 101316. , 336181.05])
9. Bài tập Thực hành
Bài tập 1: Support Vector Machine (SVM)
Thử nghiệm với SVR (Support Vector Regressor) với các kernel khác nhau. Lưu ý SVM không mở rộng tốt với dữ liệu lớn, nên ta chỉ dùng 5,000 mẫu đầu tiên.
from sklearn.model_selection import GridSearchCV
from sklearn.svm import SVR
param_grid = [
{'svr__kernel': ['linear'], 'svr__C': [10., 30., 100., 300., 1000.,
3000., 10000., 30000.0]},
{'svr__kernel': ['rbf'], 'svr__C': [1.0, 3.0, 10., 30., 100., 300.,
1000.0],
'svr__gamma': [0.01, 0.03, 0.1, 0.3, 1.0, 3.0]},
]
svr_pipeline = Pipeline([("preprocessing", preprocessing), ("svr", SVR())])
grid_search = GridSearchCV(svr_pipeline, param_grid, cv=3,
scoring='neg_root_mean_squared_error')
grid_search.fit(housing.iloc[:5000], housing_labels.iloc[:5000])
GridSearchCV(cv=3,
estimator=Pipeline(steps=[('preprocessing',
ColumnTransformer(remainder=Pipeline(steps=[('simpleimputer',
SimpleImputer(strategy='median')),
('standardscaler',
StandardScaler())]),
transformers=[('bedrooms',
Pipeline(steps=[('simpleimputer',
SimpleImputer(strategy='median')),
('functiontransformer',
FunctionTransformer(feature_names_out=<f...
<sklearn.compose._column_transformer.make_column_selector object at 0x7e2b87d49940>)])),
('svr', SVR())]),
param_grid=[{'svr__C': [10.0, 30.0, 100.0, 300.0, 1000.0, 3000.0,
10000.0, 30000.0],
'svr__kernel': ['linear']},
{'svr__C': [1.0, 3.0, 10.0, 30.0, 100.0, 300.0,
1000.0],
'svr__gamma': [0.01, 0.03, 0.1, 0.3, 1.0, 3.0],
'svr__kernel': ['rbf']}],
scoring='neg_root_mean_squared_error')GridSearchCV(cv=3,
estimator=Pipeline(steps=[('preprocessing',
ColumnTransformer(remainder=Pipeline(steps=[('simpleimputer',
SimpleImputer(strategy='median')),
('standardscaler',
StandardScaler())]),
transformers=[('bedrooms',
Pipeline(steps=[('simpleimputer',
SimpleImputer(strategy='median')),
('functiontransformer',
FunctionTransformer(feature_names_out=<f...
<sklearn.compose._column_transformer.make_column_selector object at 0x7e2b87d49940>)])),
('svr', SVR())]),
param_grid=[{'svr__C': [10.0, 30.0, 100.0, 300.0, 1000.0, 3000.0,
10000.0, 30000.0],
'svr__kernel': ['linear']},
{'svr__C': [1.0, 3.0, 10.0, 30.0, 100.0, 300.0,
1000.0],
'svr__gamma': [0.01, 0.03, 0.1, 0.3, 1.0, 3.0],
'svr__kernel': ['rbf']}],
scoring='neg_root_mean_squared_error')Pipeline(steps=[('preprocessing',
ColumnTransformer(remainder=Pipeline(steps=[('simpleimputer',
SimpleImputer(strategy='median')),
('standardscaler',
StandardScaler())]),
transformers=[('bedrooms',
Pipeline(steps=[('simpleimputer',
SimpleImputer(strategy='median')),
('functiontransformer',
FunctionTransformer(feature_names_out=<function ratio_name at 0x7e2b87d...
'median_income']),
('geo',
ClusterSimilarity(random_state=42),
['latitude', 'longitude']),
('cat',
Pipeline(steps=[('simpleimputer',
SimpleImputer(strategy='most_frequent')),
('onehotencoder',
OneHotEncoder(handle_unknown='ignore'))]),
<sklearn.compose._column_transformer.make_column_selector object at 0x7e2b8b36da30>)])),
('svr', SVR(C=10000.0, kernel='linear'))])ColumnTransformer(remainder=Pipeline(steps=[('simpleimputer',
SimpleImputer(strategy='median')),
('standardscaler',
StandardScaler())]),
transformers=[('bedrooms',
Pipeline(steps=[('simpleimputer',
SimpleImputer(strategy='median')),
('functiontransformer',
FunctionTransformer(feature_names_out=<function ratio_name at 0x7e2b87d589a0>,
func=<function column_ratio...
['total_bedrooms', 'total_rooms', 'population',
'households', 'median_income']),
('geo', ClusterSimilarity(random_state=42),
['latitude', 'longitude']),
('cat',
Pipeline(steps=[('simpleimputer',
SimpleImputer(strategy='most_frequent')),
('onehotencoder',
OneHotEncoder(handle_unknown='ignore'))]),
<sklearn.compose._column_transformer.make_column_selector object at 0x7e2b8b36da30>)])['total_bedrooms', 'total_rooms']
SimpleImputer(strategy='median')
FunctionTransformer(feature_names_out=<function ratio_name at 0x7e2b87d589a0>,
func=<function column_ratio at 0x7e2b87d58900>)StandardScaler()
['total_rooms', 'households']
SimpleImputer(strategy='median')
FunctionTransformer(feature_names_out=<function ratio_name at 0x7e2b87d589a0>,
func=<function column_ratio at 0x7e2b87d58900>)StandardScaler()
['population', 'households']
SimpleImputer(strategy='median')
FunctionTransformer(feature_names_out=<function ratio_name at 0x7e2b87d589a0>,
func=<function column_ratio at 0x7e2b87d58900>)StandardScaler()
['total_bedrooms', 'total_rooms', 'population', 'households', 'median_income']
SimpleImputer(strategy='median')
FunctionTransformer(feature_names_out='one-to-one', func=<ufunc 'log'>)
StandardScaler()
['latitude', 'longitude']
ClusterSimilarity(random_state=42)
<sklearn.compose._column_transformer.make_column_selector object at 0x7e2b8b36da30>
SimpleImputer(strategy='most_frequent')
OneHotEncoder(handle_unknown='ignore')
['housing_median_age']
SimpleImputer(strategy='median')
StandardScaler()
SVR(C=10000.0, kernel='linear')
svr_grid_search_rmse = -grid_search.best_score_
svr_grid_search_rmse
np.float64(70059.9277356289)
grid_search.best_params_
{'svr__C': 10000.0, 'svr__kernel': 'linear'}
Kết quả cho thấy Kernel Linear tốt hơn RBF trong trường hợp này, và giá trị C tốt nhất nằm ở biên trên, gợi ý nên thử C lớn hơn nữa.
Bài tập 2: RandomizedSearchCV cho SVM
Thử thay thế Grid Search bằng Randomized Search.
from sklearn.model_selection import RandomizedSearchCV
from scipy.stats import expon, loguniform
# Note: gamma bị bỏ qua khi kernel là "linear"
param_distribs = {
'svr__kernel': ['linear', 'rbf'],
'svr__C': loguniform(20, 200_000),
'svr__gamma': expon(scale=1.0),
}
rnd_search = RandomizedSearchCV(svr_pipeline,
param_distributions=param_distribs,
n_iter=50, cv=3,
scoring='neg_root_mean_squared_error',
random_state=42)
rnd_search.fit(housing.iloc[:5000], housing_labels.iloc[:5000])
RandomizedSearchCV(cv=3,
estimator=Pipeline(steps=[('preprocessing',
ColumnTransformer(remainder=Pipeline(steps=[('simpleimputer',
SimpleImputer(strategy='median')),
('standardscaler',
StandardScaler())]),
transformers=[('bedrooms',
Pipeline(steps=[('simpleimputer',
SimpleImputer(strategy='median')),
('functiontransformer',
FunctionTransformer(feature_names_...
<sklearn.compose._column_transformer.make_column_selector object at 0x7e2b87d49940>)])),
('svr', SVR())]),
n_iter=50,
param_distributions={'svr__C': <scipy.stats._distn_infrastructure.rv_continuous_frozen object at 0x7e2b98296e70>,
'svr__gamma': <scipy.stats._distn_infrastructure.rv_continuous_frozen object at 0x7e2b91ebc260>,
'svr__kernel': ['linear', 'rbf']},
random_state=42, scoring='neg_root_mean_squared_error')RandomizedSearchCV(cv=3,
estimator=Pipeline(steps=[('preprocessing',
ColumnTransformer(remainder=Pipeline(steps=[('simpleimputer',
SimpleImputer(strategy='median')),
('standardscaler',
StandardScaler())]),
transformers=[('bedrooms',
Pipeline(steps=[('simpleimputer',
SimpleImputer(strategy='median')),
('functiontransformer',
FunctionTransformer(feature_names_...
<sklearn.compose._column_transformer.make_column_selector object at 0x7e2b87d49940>)])),
('svr', SVR())]),
n_iter=50,
param_distributions={'svr__C': <scipy.stats._distn_infrastructure.rv_continuous_frozen object at 0x7e2b98296e70>,
'svr__gamma': <scipy.stats._distn_infrastructure.rv_continuous_frozen object at 0x7e2b91ebc260>,
'svr__kernel': ['linear', 'rbf']},
random_state=42, scoring='neg_root_mean_squared_error')Pipeline(steps=[('preprocessing',
ColumnTransformer(remainder=Pipeline(steps=[('simpleimputer',
SimpleImputer(strategy='median')),
('standardscaler',
StandardScaler())]),
transformers=[('bedrooms',
Pipeline(steps=[('simpleimputer',
SimpleImputer(strategy='median')),
('functiontransformer',
FunctionTransformer(feature_names_out=<function ratio_name at 0x7e2b87d...
ClusterSimilarity(random_state=42),
['latitude', 'longitude']),
('cat',
Pipeline(steps=[('simpleimputer',
SimpleImputer(strategy='most_frequent')),
('onehotencoder',
OneHotEncoder(handle_unknown='ignore'))]),
<sklearn.compose._column_transformer.make_column_selector object at 0x7e2b8b3858b0>)])),
('svr',
SVR(C=np.float64(157055.10989448498),
gamma=np.float64(0.26497040005002437)))])ColumnTransformer(remainder=Pipeline(steps=[('simpleimputer',
SimpleImputer(strategy='median')),
('standardscaler',
StandardScaler())]),
transformers=[('bedrooms',
Pipeline(steps=[('simpleimputer',
SimpleImputer(strategy='median')),
('functiontransformer',
FunctionTransformer(feature_names_out=<function ratio_name at 0x7e2b87d589a0>,
func=<function column_ratio...
['total_bedrooms', 'total_rooms', 'population',
'households', 'median_income']),
('geo', ClusterSimilarity(random_state=42),
['latitude', 'longitude']),
('cat',
Pipeline(steps=[('simpleimputer',
SimpleImputer(strategy='most_frequent')),
('onehotencoder',
OneHotEncoder(handle_unknown='ignore'))]),
<sklearn.compose._column_transformer.make_column_selector object at 0x7e2b8b3858b0>)])['total_bedrooms', 'total_rooms']
SimpleImputer(strategy='median')
FunctionTransformer(feature_names_out=<function ratio_name at 0x7e2b87d589a0>,
func=<function column_ratio at 0x7e2b87d58900>)StandardScaler()
['total_rooms', 'households']
SimpleImputer(strategy='median')
FunctionTransformer(feature_names_out=<function ratio_name at 0x7e2b87d589a0>,
func=<function column_ratio at 0x7e2b87d58900>)StandardScaler()
['population', 'households']
SimpleImputer(strategy='median')
FunctionTransformer(feature_names_out=<function ratio_name at 0x7e2b87d589a0>,
func=<function column_ratio at 0x7e2b87d58900>)StandardScaler()
['total_bedrooms', 'total_rooms', 'population', 'households', 'median_income']
SimpleImputer(strategy='median')
FunctionTransformer(feature_names_out='one-to-one', func=<ufunc 'log'>)
StandardScaler()
['latitude', 'longitude']
ClusterSimilarity(random_state=42)
<sklearn.compose._column_transformer.make_column_selector object at 0x7e2b8b3858b0>
SimpleImputer(strategy='most_frequent')
OneHotEncoder(handle_unknown='ignore')
['housing_median_age']
SimpleImputer(strategy='median')
StandardScaler()
SVR(C=np.float64(157055.10989448498), gamma=np.float64(0.26497040005002437))
svr_rnd_search_rmse = -rnd_search.best_score_
svr_rnd_search_rmse
np.float64(56152.053691161)
rnd_search.best_params_
{'svr__C': np.float64(157055.10989448498),
'svr__gamma': np.float64(0.26497040005002437),
'svr__kernel': 'rbf'}
Randomized Search đã tìm ra bộ tham số tốt hơn hẳn cho RBF kernel, vượt qua kết quả của Grid Search trước đó.
# Kiểm tra phân phối của gamma
s = expon(scale=1).rvs(100_000, random_state=42)
((s > 0.105) & (s < 2.29)).sum() / 100_000
np.float64(0.80066)
Bài tập 3: Chọn lọc đặc trưng (SelectFromModel)
Sử dụng SelectFromModel để chỉ giữ lại các đặc trưng quan trọng nhất trước khi đưa vào mô hình dự đoán.
from sklearn.feature_selection import SelectFromModel
selector_pipeline = Pipeline([
('preprocessing', preprocessing),
('selector', SelectFromModel(RandomForestRegressor(random_state=42),
threshold=0.005)), # ngưỡng độ quan trọng tối thiểu
('svr', SVR(C=rnd_search.best_params_["svr__C"],
gamma=rnd_search.best_params_["svr__gamma"],
kernel=rnd_search.best_params_["svr__kernel"])),
])
selector_rmses = -cross_val_score(selector_pipeline,
housing.iloc[:5000],
housing_labels.iloc[:5000],
scoring="neg_root_mean_squared_error",
cv=3)
pd.Series(selector_rmses).describe()
| 0 | |
|---|---|
| count | 3.000000 |
| mean | 56622.643481 |
| std | 2272.666703 |
| min | 54243.242290 |
| 25% | 55548.509493 |
| 50% | 56853.776696 |
| 75% | 57812.344076 |
| max | 58770.911457 |
Kết quả không cải thiện nhiều, có thể do ngưỡng threshold chưa tối ưu.
Bài tập 4: Transformer tùy chỉnh dùng KNN
Tạo một transformer huấn luyện KNeighborsRegressor chỉ dựa trên tọa độ để dự đoán thu nhập trung vị (làm mượt nhiễu không gian).
from sklearn.base import MetaEstimatorMixin, clone
class FeatureFromRegressor(BaseEstimator, TransformerMixin, MetaEstimatorMixin):
def __init__(self, regressor, target_features):
self.regressor = regressor
self.target_features = target_features
def fit(self, X, y=None):
if hasattr(X, "columns"):
self.feature_names_in_ = list(X.columns)
X_df = X
else:
X_df = pd.DataFrame(X)
self.input_features_ = [c for c in X_df.columns
if c not in self.target_features]
self.regressor_ = clone(self.regressor)
self.regressor_.fit(X_df[self.input_features_],
X_df[self.target_features])
return self
def transform(self, X):
columns = X.columns if hasattr(X, "columns") else None
X_df = pd.DataFrame(X, columns=columns)
preds = self.regressor_.predict(X_df[self.input_features_])
if preds.ndim == 1:
preds = preds.reshape(-1, 1)
extra_columns = [f"pred_{t}" for t in self.target_features]
preds_df = pd.DataFrame(preds, columns=extra_columns, index=X_df.index)
return pd.concat([X_df, preds_df], axis=1)
def get_feature_names_out(self, input_features=None):
extra_columns = [f"pred_{t}" for t in self.target_features]
return self.feature_names_in_ + extra_columns
Thử nghiệm Transformer này:
from sklearn.neighbors import KNeighborsRegressor
knn_reg = KNeighborsRegressor(n_neighbors=3, weights="distance")
knn_transformer = FeatureFromRegressor(knn_reg, ["median_income"])
geo_features = housing[["latitude", "longitude", "median_income"]]
knn_transformer.fit_transform(geo_features, housing_labels)
latitude longitude median_income pred_median_income
13096 37.80 -122.42 2.0987 3.347233
14973 34.14 -118.38 6.0876 6.899400
3785 38.36 -121.98 2.4330 2.900900
14689 33.75 -117.11 2.2618 2.261800
20507 33.77 -118.15 3.5292 3.475633
... ... ... ... ...
14207 33.86 -118.40 4.7105 4.939100
13105 36.32 -119.31 2.5733 3.301550
19301 32.59 -117.06 4.0616 4.061600
19121 34.06 -118.40 4.1455 4.145500
19888 37.66 -122.41 3.2833 4.250667
[16512 rows x 4 columns]
knn_transformer.get_feature_names_out()
['latitude', 'longitude', 'median_income', 'pred_median_income']
Tích hợp vào Pipeline chính thay cho ClusterSimilarity:
from sklearn.base import clone
transformers = [(name, clone(transformer), columns)
for name, transformer, columns in preprocessing.transformers]
geo_index = [name for name, _, _ in transformers].index("geo")
transformers[geo_index] = ("geo", knn_transformer,
["latitude", "longitude", "median_income"])
new_geo_preprocessing = ColumnTransformer(transformers)
new_geo_pipeline = Pipeline([
('preprocessing', new_geo_preprocessing),
('svr', SVR(C=rnd_search.best_params_["svr__C"],
gamma=rnd_search.best_params_["svr__gamma"],
kernel=rnd_search.best_params_["svr__kernel"])),
])
new_pipe_rmses = -cross_val_score(new_geo_pipeline,
housing.iloc[:5000],
housing_labels.iloc[:5000],
scoring="neg_root_mean_squared_error",
cv=3)
pd.Series(new_pipe_rmses).describe()
| 0 | |
|---|---|
| count | 3.000000 |
| mean | 68782.438065 |
| std | 2458.334599 |
| min | 66161.322618 |
| 25% | 67655.268231 |
| 50% | 69149.213845 |
| 75% | 70092.995789 |
| max | 71036.777733 |
Kết quả tệ hơn so với dùng K-Means Cluster Similarity. Có thể cần tinh chỉnh tham số cho KNN.
Bài tập 5: Tự động khám phá với RandomSearchCV
Dùng Randomized Search để tìm tham số tối ưu cho cả bước tiền xử lý (KNN) và mô hình chính (SVR).
param_distribs = {
"preprocessing__geo__regressor__n_neighbors": range(1, 30),
"preprocessing__geo__regressor__weights": ["distance", "uniform"],
"svr__C": loguniform(20, 200_000),
"svr__gamma": expon(scale=1.0),
}
new_geo_rnd_search = RandomizedSearchCV(new_geo_pipeline,
param_distributions=param_distribs,
n_iter=50,
cv=3,
scoring='neg_root_mean_squared_error',
random_state=42)
new_geo_rnd_search.fit(housing.iloc[:5000], housing_labels.iloc[:5000])
RandomizedSearchCV(cv=3,
estimator=Pipeline(steps=[('preprocessing',
ColumnTransformer(transformers=[('bedrooms',
Pipeline(steps=[('simpleimputer',
SimpleImputer(strategy='median')),
('functiontransformer',
FunctionTransformer(feature_names_out=<function ratio_name at 0x7e2b87d589a0>,
func=<function column_ratio at 0x7e2b87d58900>)),
('standardscaler',
StandardSc...
param_distributions={'preprocessing__geo__regressor__n_neighbors': range(1, 30),
'preprocessing__geo__regressor__weights': ['distance',
'uniform'],
'svr__C': <scipy.stats._distn_infrastructure.rv_continuous_frozen object at 0x7e2b9803cd40>,
'svr__gamma': <scipy.stats._distn_infrastructure.rv_continuous_frozen object at 0x7e2b90149910>},
random_state=42, scoring='neg_root_mean_squared_error')RandomizedSearchCV(cv=3,
estimator=Pipeline(steps=[('preprocessing',
ColumnTransformer(transformers=[('bedrooms',
Pipeline(steps=[('simpleimputer',
SimpleImputer(strategy='median')),
('functiontransformer',
FunctionTransformer(feature_names_out=<function ratio_name at 0x7e2b87d589a0>,
func=<function column_ratio at 0x7e2b87d58900>)),
('standardscaler',
StandardSc...
param_distributions={'preprocessing__geo__regressor__n_neighbors': range(1, 30),
'preprocessing__geo__regressor__weights': ['distance',
'uniform'],
'svr__C': <scipy.stats._distn_infrastructure.rv_continuous_frozen object at 0x7e2b9803cd40>,
'svr__gamma': <scipy.stats._distn_infrastructure.rv_continuous_frozen object at 0x7e2b90149910>},
random_state=42, scoring='neg_root_mean_squared_error')Pipeline(steps=[('preprocessing',
ColumnTransformer(transformers=[('bedrooms',
Pipeline(steps=[('simpleimputer',
SimpleImputer(strategy='median')),
('functiontransformer',
FunctionTransformer(feature_names_out=<function ratio_name at 0x7e2b87d589a0>,
func=<function column_ratio at 0x7e2b87d58900>)),
('standardscaler',
StandardScaler())]),
['total_bedrooms',
'total...
['latitude', 'longitude',
'median_income']),
('cat',
Pipeline(steps=[('simpleimputer',
SimpleImputer(strategy='most_frequent')),
('onehotencoder',
OneHotEncoder(handle_unknown='ignore'))]),
<sklearn.compose._column_transformer.make_column_selector object at 0x7e2b91c686b0>)])),
('svr',
SVR(C=np.float64(55456.48365602121),
gamma=np.float64(0.006976409181650647)))])ColumnTransformer(transformers=[('bedrooms',
Pipeline(steps=[('simpleimputer',
SimpleImputer(strategy='median')),
('functiontransformer',
FunctionTransformer(feature_names_out=<function ratio_name at 0x7e2b87d589a0>,
func=<function column_ratio at 0x7e2b87d58900>)),
('standardscaler',
StandardScaler())]),
['total_bedrooms', 'total_rooms']),
('rooms_per_house',
Pipe...
FeatureFromRegressor(regressor=KNeighborsRegressor(n_neighbors=20,
weights='distance'),
target_features=['median_income']),
['latitude', 'longitude', 'median_income']),
('cat',
Pipeline(steps=[('simpleimputer',
SimpleImputer(strategy='most_frequent')),
('onehotencoder',
OneHotEncoder(handle_unknown='ignore'))]),
<sklearn.compose._column_transformer.make_column_selector object at 0x7e2b91c686b0>)])['total_bedrooms', 'total_rooms']
SimpleImputer(strategy='median')
FunctionTransformer(feature_names_out=<function ratio_name at 0x7e2b87d589a0>,
func=<function column_ratio at 0x7e2b87d58900>)StandardScaler()
['total_rooms', 'households']
SimpleImputer(strategy='median')
FunctionTransformer(feature_names_out=<function ratio_name at 0x7e2b87d589a0>,
func=<function column_ratio at 0x7e2b87d58900>)StandardScaler()
['population', 'households']
SimpleImputer(strategy='median')
FunctionTransformer(feature_names_out=<function ratio_name at 0x7e2b87d589a0>,
func=<function column_ratio at 0x7e2b87d58900>)StandardScaler()
['total_bedrooms', 'total_rooms', 'population', 'households', 'median_income']
SimpleImputer(strategy='median')
FunctionTransformer(feature_names_out='one-to-one', func=<ufunc 'log'>)
StandardScaler()
['latitude', 'longitude', 'median_income']
KNeighborsRegressor(n_neighbors=20, weights='distance')
KNeighborsRegressor(n_neighbors=20, weights='distance')
<sklearn.compose._column_transformer.make_column_selector object at 0x7e2b91c686b0>
SimpleImputer(strategy='most_frequent')
OneHotEncoder(handle_unknown='ignore')
SVR(C=np.float64(55456.48365602121), gamma=np.float64(0.006976409181650647))
new_geo_rnd_search_rmse = -new_geo_rnd_search.best_score_
new_geo_rnd_search_rmse
np.float64(64573.262757363635)
Kết quả có cải thiện nhưng vẫn chưa vượt qua phương pháp dùng K-Means.
Bài tập 6: Viết lại StandardScalerClone hoàn chỉnh
Cài đặt lại lớp StandardScaler với đầy đủ tính năng: inverse_transform và hỗ trợ tên đặc trưng (get_feature_names_out).
import numpy as np
from sklearn.base import BaseEstimator, TransformerMixin
from sklearn.utils.validation import check_is_fitted, validate_data
class StandardScalerClone(TransformerMixin, BaseEstimator):
def __init__(self, with_mean=True): # no *args or **kwargs!
self.with_mean = with_mean
def fit(self, X, y=None):
X = validate_data(self, X, ensure_2d=True)
self.n_features_in_ = X.shape[1]
if self.with_mean:
self.mean_ = np.mean(X, axis=0)
self.scale_ = np.std(X, axis=0, ddof=0)
self.scale_[self.scale_ == 0] = 1 # Tránh chia cho 0
return self
def transform(self, X):
check_is_fitted(self)
X = validate_data(self, X, ensure_2d=True, reset=False)
if self.with_mean:
X = X - self.mean_
return X / self.scale_
def inverse_transform(self, X):
check_is_fitted(self)
X = validate_data(self, X, ensure_2d=True, reset=False)
return X * self.scale_ + self.mean_
def get_feature_names_out(self, input_features=None):
if input_features is None:
return getattr(self, "feature_names_in_",
[f"x{i}" for i in range(self.n_features_in_)])
else:
if len(input_features) != self.n_features_in_:
raise ValueError("Invalid number of features")
if hasattr(self, "feature_names_in_") and not np.all(
self.feature_names_in_ == input_features
):
raise ValueError("input_features ≠ feature_names_in_")
return input_features
Kiểm tra tính tuân thủ API của Scikit-Learn:
from sklearn.utils.estimator_checks import check_estimator
check_estimator(StandardScalerClone())
[{'estimator': StandardScalerClone(),
'check_name': 'check_estimator_cloneable',
'exception': None,
'status': 'passed',
'expected_to_fail': False,
'expected_to_fail_reason': 'Check is not expected to fail'},
{'estimator': StandardScalerClone(),
'check_name': 'check_estimator_cloneable',
'exception': None,
'status': 'passed',
'expected_to_fail': False,
'expected_to_fail_reason': 'Check is not expected to fail'},
{'estimator': StandardScalerClone(),
'check_name': 'check_estimator_tags_renamed',
'exception': None,
'status': 'passed',
'expected_to_fail': False,
'expected_to_fail_reason': 'Check is not expected to fail'},
{'estimator': StandardScalerClone(),
'check_name': 'check_valid_tag_types',
'exception': None,
'status': 'passed',
'expected_to_fail': False,
'expected_to_fail_reason': 'Check is not expected to fail'},
{'estimator': StandardScalerClone(),
'check_name': 'check_estimator_repr',
'exception': None,
'status': 'passed',
'expected_to_fail': False,
'expected_to_fail_reason': 'Check is not expected to fail'},
{'estimator': StandardScalerClone(),
'check_name': 'check_no_attributes_set_in_init',
'exception': None,
'status': 'passed',
'expected_to_fail': False,
'expected_to_fail_reason': 'Check is not expected to fail'},
{'estimator': StandardScalerClone(),
'check_name': 'check_fit_score_takes_y',
'exception': None,
'status': 'passed',
'expected_to_fail': False,
'expected_to_fail_reason': 'Check is not expected to fail'},
{'estimator': StandardScalerClone(),
'check_name': 'check_estimators_overwrite_params',
'exception': None,
'status': 'passed',
'expected_to_fail': False,
'expected_to_fail_reason': 'Check is not expected to fail'},
{'estimator': StandardScalerClone(),
'check_name': 'check_dont_overwrite_parameters',
'exception': None,
'status': 'passed',
'expected_to_fail': False,
'expected_to_fail_reason': 'Check is not expected to fail'},
{'estimator': StandardScalerClone(),
'check_name': 'check_estimators_fit_returns_self',
'exception': None,
'status': 'passed',
'expected_to_fail': False,
'expected_to_fail_reason': 'Check is not expected to fail'},
{'estimator': StandardScalerClone(),
'check_name': 'check_readonly_memmap_input',
'exception': None,
'status': 'passed',
'expected_to_fail': False,
'expected_to_fail_reason': 'Check is not expected to fail'},
{'estimator': StandardScalerClone(),
'check_name': 'check_estimators_unfitted',
'exception': None,
'status': 'passed',
'expected_to_fail': False,
'expected_to_fail_reason': 'Check is not expected to fail'},
{'estimator': StandardScalerClone(),
'check_name': 'check_do_not_raise_errors_in_init_or_set_params',
'exception': None,
'status': 'passed',
'expected_to_fail': False,
'expected_to_fail_reason': 'Check is not expected to fail'},
{'estimator': StandardScalerClone(),
'check_name': 'check_n_features_in_after_fitting',
'exception': None,
'status': 'passed',
'expected_to_fail': False,
'expected_to_fail_reason': 'Check is not expected to fail'},
{'estimator': StandardScalerClone(),
'check_name': 'check_mixin_order',
'exception': None,
'status': 'passed',
'expected_to_fail': False,
'expected_to_fail_reason': 'Check is not expected to fail'},
{'estimator': StandardScalerClone(),
'check_name': 'check_positive_only_tag_during_fit',
'exception': None,
'status': 'passed',
'expected_to_fail': False,
'expected_to_fail_reason': 'Check is not expected to fail'},
{'estimator': StandardScalerClone(),
'check_name': 'check_estimators_dtypes',
'exception': None,
'status': 'passed',
'expected_to_fail': False,
'expected_to_fail_reason': 'Check is not expected to fail'},
{'estimator': StandardScalerClone(),
'check_name': 'check_complex_data',
'exception': None,
'status': 'passed',
'expected_to_fail': False,
'expected_to_fail_reason': 'Check is not expected to fail'},
{'estimator': StandardScalerClone(),
'check_name': 'check_dtype_object',
'exception': None,
'status': 'passed',
'expected_to_fail': False,
'expected_to_fail_reason': 'Check is not expected to fail'},
{'estimator': StandardScalerClone(),
'check_name': 'check_estimators_empty_data_messages',
'exception': None,
'status': 'passed',
'expected_to_fail': False,
'expected_to_fail_reason': 'Check is not expected to fail'},
{'estimator': StandardScalerClone(),
'check_name': 'check_pipeline_consistency',
'exception': None,
'status': 'passed',
'expected_to_fail': False,
'expected_to_fail_reason': 'Check is not expected to fail'},
{'estimator': StandardScalerClone(),
'check_name': 'check_estimators_nan_inf',
'exception': None,
'status': 'passed',
'expected_to_fail': False,
'expected_to_fail_reason': 'Check is not expected to fail'},
{'estimator': StandardScalerClone(),
'check_name': 'check_estimator_sparse_tag',
'exception': None,
'status': 'passed',
'expected_to_fail': False,
'expected_to_fail_reason': 'Check is not expected to fail'},
{'estimator': StandardScalerClone(),
'check_name': 'check_estimator_sparse_array',
'exception': None,
'status': 'passed',
'expected_to_fail': False,
'expected_to_fail_reason': 'Check is not expected to fail'},
{'estimator': StandardScalerClone(),
'check_name': 'check_estimator_sparse_matrix',
'exception': None,
'status': 'passed',
'expected_to_fail': False,
'expected_to_fail_reason': 'Check is not expected to fail'},
{'estimator': StandardScalerClone(),
'check_name': 'check_estimators_pickle',
'exception': None,
'status': 'passed',
'expected_to_fail': False,
'expected_to_fail_reason': 'Check is not expected to fail'},
{'estimator': StandardScalerClone(),
'check_name': 'check_estimators_pickle',
'exception': None,
'status': 'passed',
'expected_to_fail': False,
'expected_to_fail_reason': 'Check is not expected to fail'},
{'estimator': StandardScalerClone(),
'check_name': 'check_f_contiguous_array_estimator',
'exception': None,
'status': 'passed',
'expected_to_fail': False,
'expected_to_fail_reason': 'Check is not expected to fail'},
{'estimator': StandardScalerClone(),
'check_name': 'check_transformer_data_not_an_array',
'exception': None,
'status': 'passed',
'expected_to_fail': False,
'expected_to_fail_reason': 'Check is not expected to fail'},
{'estimator': StandardScalerClone(),
'check_name': 'check_transformer_general',
'exception': None,
'status': 'passed',
'expected_to_fail': False,
'expected_to_fail_reason': 'Check is not expected to fail'},
{'estimator': StandardScalerClone(),
'check_name': 'check_transformer_preserve_dtypes',
'exception': None,
'status': 'passed',
'expected_to_fail': False,
'expected_to_fail_reason': 'Check is not expected to fail'},
{'estimator': StandardScalerClone(),
'check_name': 'check_transformer_general',
'exception': None,
'status': 'passed',
'expected_to_fail': False,
'expected_to_fail_reason': 'Check is not expected to fail'},
{'estimator': StandardScalerClone(),
'check_name': 'check_transformers_unfitted',
'exception': None,
'status': 'passed',
'expected_to_fail': False,
'expected_to_fail_reason': 'Check is not expected to fail'},
{'estimator': StandardScalerClone(),
'check_name': 'check_transformer_n_iter',
'exception': None,
'status': 'passed',
'expected_to_fail': False,
'expected_to_fail_reason': 'Check is not expected to fail'},
{'estimator': StandardScalerClone(),
'check_name': 'check_parameters_default_constructible',
'exception': None,
'status': 'passed',
'expected_to_fail': False,
'expected_to_fail_reason': 'Check is not expected to fail'},
{'estimator': StandardScalerClone(),
'check_name': 'check_methods_sample_order_invariance',
'exception': None,
'status': 'passed',
'expected_to_fail': False,
'expected_to_fail_reason': 'Check is not expected to fail'},
{'estimator': StandardScalerClone(),
'check_name': 'check_methods_subset_invariance',
'exception': None,
'status': 'passed',
'expected_to_fail': False,
'expected_to_fail_reason': 'Check is not expected to fail'},
{'estimator': StandardScalerClone(),
'check_name': 'check_fit2d_1sample',
'exception': None,
'status': 'passed',
'expected_to_fail': False,
'expected_to_fail_reason': 'Check is not expected to fail'},
{'estimator': StandardScalerClone(),
'check_name': 'check_fit2d_1feature',
'exception': None,
'status': 'passed',
'expected_to_fail': False,
'expected_to_fail_reason': 'Check is not expected to fail'},
{'estimator': StandardScalerClone(),
'check_name': 'check_get_params_invariance',
'exception': None,
'status': 'passed',
'expected_to_fail': False,
'expected_to_fail_reason': 'Check is not expected to fail'},
{'estimator': StandardScalerClone(),
'check_name': 'check_set_params',
'exception': None,
'status': 'passed',
'expected_to_fail': False,
'expected_to_fail_reason': 'Check is not expected to fail'},
{'estimator': StandardScalerClone(),
'check_name': 'check_dict_unchanged',
'exception': None,
'status': 'passed',
'expected_to_fail': False,
'expected_to_fail_reason': 'Check is not expected to fail'},
{'estimator': StandardScalerClone(),
'check_name': 'check_fit_idempotent',
'exception': None,
'status': 'passed',
'expected_to_fail': False,
'expected_to_fail_reason': 'Check is not expected to fail'},
{'estimator': StandardScalerClone(),
'check_name': 'check_fit_check_is_fitted',
'exception': None,
'status': 'passed',
'expected_to_fail': False,
'expected_to_fail_reason': 'Check is not expected to fail'},
{'estimator': StandardScalerClone(),
'check_name': 'check_n_features_in',
'exception': None,
'status': 'passed',
'expected_to_fail': False,
'expected_to_fail_reason': 'Check is not expected to fail'},
{'estimator': StandardScalerClone(),
'check_name': 'check_fit1d',
'exception': None,
'status': 'passed',
'expected_to_fail': False,
'expected_to_fail_reason': 'Check is not expected to fail'},
{'estimator': StandardScalerClone(),
'check_name': 'check_fit2d_predict1d',
'exception': None,
'status': 'passed',
'expected_to_fail': False,
'expected_to_fail_reason': 'Check is not expected to fail'}]
Kiểm tra tính chính xác của thuật toán:
rng = np.random.default_rng(seed=42)
X = rng.random((1000, 3))
scaler = StandardScalerClone()
X_scaled = scaler.fit_transform(X)
# Kiểm tra công thức chuẩn hóa
assert np.allclose(X_scaled, (X - X.mean(axis=0)) / X.std(axis=0))
# Kiểm tra trường hợp không trừ mean
scaler = StandardScalerClone(with_mean=False)
X_scaled_uncentered = scaler.fit_transform(X)
assert np.allclose(X_scaled_uncentered, X / X.std(axis=0))
# Kiểm tra hàm nghịch đảo
scaler = StandardScalerClone()
X_back = scaler.inverse_transform(scaler.fit_transform(X))
assert np.allclose(X, X_back)
# Kiểm tra tên đặc trưng output
assert np.all(scaler.get_feature_names_out() == ["x0", "x1", "x2"])
assert np.all(scaler.get_feature_names_out(["a", "b", "c"]) == ["a", "b", "c"])
# Kiểm tra với DataFrame
df = pd.DataFrame({"a": rng.random(100), "b": rng.random(100)})
scaler = StandardScalerClone()
X_scaled = scaler.fit_transform(df)
assert np.all(scaler.feature_names_in_ == ["a", "b"])
assert np.all(scaler.get_feature_names_out() == ["a", "b"])
Mọi kiểm thử đều thông qua! Chúc mừng bạn đã hoàn thành Chương 2: Dự án Machine Learning đầu tiên. Bạn đã nắm vững quy trình từ xử lý dữ liệu thô đến tối ưu hóa mô hình phức tạp. Xin chào và hẹn gặp lại.