🏠 House Prices 房價預測:用演化找到最強模型的房價預測之路

更新 發佈閱讀 75 分鐘
問神、卜卦,欲知未來。
模型為籤,參數作卦。
此刻,數據低語天機。
不問因果,不解其意,
只看哪一組命中註定

🧭 前言

這是我第二個碰的Kaggle挑戰,也是機器學習剛出來的時候,大家很熱衷的事情,將機器學習拿來做未來的預測,而剛好這題是對房價的預測。

House Prices - Advanced Regression Techniques 是一道模擬美國愛荷華州艾姆斯市住宅數據的任務,藉由已知資訊預測房價,與 Titanic 一樣是入門經典。資料集早就公布多年,Leaderboard 前排也幾乎被「最佳解」佔滿。

這次要講的,是一個有點歪打正著的例子。剛接觸機器學習時,對特徵工程一竅不通,而這題有 79 個欄位一字排開,實在不知道該怎麼下手。於是我在思考,我能不能寫個他自己處理的方法。

因此我選擇了最直接的方法, 自動補值、建模,交給模型自己找路。最後用四個模型的堆疊取得初步分數,之後再回頭進行超參數最佳化,竟也得到了不錯的結果。

🧰 環境建立

這次的實作是在 Kaggle Notebook 上完成的。它的使用方式與 Google Colab 類似,都是不需要自己架設環境、直接在網頁上就能執行程式的工具,操作介面也與 Jupyter Notebook 十分相近,對初學者來說相當友好。

選擇 Kaggle Notebook 的原因,除了它內建的競賽資料可以直接載入之外,最主要是它提供的免費 GPU 使用時數遠多於 Colab,對於訓練像 XGBoost、CatBoost 這類計算量較大的模型來說,相當實用。這兩種模型也都支援 GPU 加速,在 Kaggle Notebook 上執行能省下不少等待時間。

(但也得說句實話,Kaggle Notebook 在中文輸入的相容性上,確實不如 Colab 順暢。)

通常你開一個新的 Kaggle Notebook Page,進入後就會自動有下面這段:

# This Python 3 environment comes with many helpful analytics libraries installed
# It is defined by the kaggle/python Docker image: <https://github.com/kaggle/docker-python>
# For example, here's several helpful packages to load

import numpy as np # linear algebra
import pandas as pd # data processing, CSV file I/O (e.g. pd.read_csv)

# Input data files are available in the read-only "../input/" directory
# For example, running this (by clicking run or pressing Shift+Enter) will list all files under the input directory

import os
for dirname, _, filenames in os.walk('/kaggle/input'):
for filename in filenames:
print(os.path.join(dirname, filename))

# You can write up to 20GB to the current directory (/kaggle/working/) that gets preserved as output when you create a version using "Save & Run All"
# You can also write temporary files to /kaggle/temp/, but they won't be saved outside of the current session

這段提醒你載入Input的位置,如果你有指定比賽項目,Input裡面就會自動有比賽的Data可以使用。
同樣也可以自己丟入資料,他提供20GB給你使用。
接下來,進入資料分析環節。

📊 資料初探

同樣,拿到一份資料還是要看看他到底是什麼,我們把他展開來看看。

首先,我們載入資料。

folder_path = "/kaggle/input/house-prices-advanced-regression-techniques/"
train_path = folder_path + "train.csv"
test_path = folder_path + "test.csv"
df_train = pd.read_csv(train_path)
df_test = pd.read_csv(test_path)

然後將train打開來看看。

df_train.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 1460 entries, 0 to 1459
Data columns (total 81 columns):
# Column Non-Null Count Dtype
--- ------ -------------- -----
0 Id 1460 non-null int64
1 MSSubClass 1460 non-null int64
2 MSZoning 1460 non-null object
3 LotFrontage 1201 non-null float64
4 LotArea 1460 non-null int64
5 Street 1460 non-null object
6 Alley 91 non-null object
7 LotShape 1460 non-null object
8 LandContour 1460 non-null object
9 Utilities 1460 non-null object
10 LotConfig 1460 non-null object
11 LandSlope 1460 non-null object
12 Neighborhood 1460 non-null object
13 Condition1 1460 non-null object
14 Condition2 1460 non-null object
15 BldgType 1460 non-null object
16 HouseStyle 1460 non-null object
17 OverallQual 1460 non-null int64
18 OverallCond 1460 non-null int64
19 YearBuilt 1460 non-null int64
20 YearRemodAdd 1460 non-null int64
21 RoofStyle 1460 non-null object
22 RoofMatl 1460 non-null object
23 Exterior1st 1460 non-null object
24 Exterior2nd 1460 non-null object
25 MasVnrType 588 non-null object
26 MasVnrArea 1452 non-null float64
27 ExterQual 1460 non-null object
28 ExterCond 1460 non-null object
29 Foundation 1460 non-null object
30 BsmtQual 1423 non-null object
31 BsmtCond 1423 non-null object
32 BsmtExposure 1422 non-null object
33 BsmtFinType1 1423 non-null object
34 BsmtFinSF1 1460 non-null int64
35 BsmtFinType2 1422 non-null object
36 BsmtFinSF2 1460 non-null int64
37 BsmtUnfSF 1460 non-null int64
38 TotalBsmtSF 1460 non-null int64
39 Heating 1460 non-null object
40 HeatingQC 1460 non-null object
41 CentralAir 1460 non-null object
42 Electrical 1459 non-null object
43 1stFlrSF 1460 non-null int64
44 2ndFlrSF 1460 non-null int64
45 LowQualFinSF 1460 non-null int64
46 GrLivArea 1460 non-null int64
47 BsmtFullBath 1460 non-null int64
48 BsmtHalfBath 1460 non-null int64
49 FullBath 1460 non-null int64
50 HalfBath 1460 non-null int64
51 BedroomAbvGr 1460 non-null int64
52 KitchenAbvGr 1460 non-null int64
53 KitchenQual 1460 non-null object
54 TotRmsAbvGrd 1460 non-null int64
55 Functional 1460 non-null object
56 Fireplaces 1460 non-null int64
57 FireplaceQu 770 non-null object
58 GarageType 1379 non-null object
59 GarageYrBlt 1379 non-null float64
60 GarageFinish 1379 non-null object
61 GarageCars 1460 non-null int64
62 GarageArea 1460 non-null int64
63 GarageQual 1379 non-null object
64 GarageCond 1379 non-null object
65 PavedDrive 1460 non-null object
66 WoodDeckSF 1460 non-null int64
67 OpenPorchSF 1460 non-null int64
68 EnclosedPorch 1460 non-null int64
69 3SsnPorch 1460 non-null int64
70 ScreenPorch 1460 non-null int64
71 PoolArea 1460 non-null int64
72 PoolQC 7 non-null object
73 Fence 281 non-null object
74 MiscFeature 54 non-null object
75 MiscVal 1460 non-null int64
76 MoSold 1460 non-null int64
77 YrSold 1460 non-null int64
78 SaleType 1460 non-null object
79 SaleCondition 1460 non-null object
80 SalePrice 1460 non-null int64
dtypes: float64(3), int64(35), object(43)
memory usage: 924.0+ KB

總共有80個欄位,比起之前的鐵達尼號多出好多,其中 LotFrontage Alley MasVnrType MasVnrArea BsmtQual BsmtCond …等等有18個欄位缺值,再來看看內容長甚麼樣子。

df_train.head(5)
raw-image

可以看到中間被省略了一大段,因為欄位實在太多,預設情況下 Pandas 只會顯示部分欄位。為了完整顯示全部內容,我們加上以下設定:

pd.set_option('display.max_columns', None)
df_train.head(5)

再執行一次,就可以看到所有欄位囉:

raw-image

可以發現,欄位類型相當混雜,有數值、有文字、有分類、有缺值…… 處理起來肯定是一場漫長的工程。

🧩 缺失值處理:讓演算法自己去猜

這題的資料多達 80 個欄位,其中有不少欄位出現缺值,像是 LotFrontageGarageYrBltMasVnrType,甚至還有整列幾乎是空的(像 PoolQCMiscFeature)。

這些空值很容易造成困擾,該刪掉?該補成 0?還是直接忽略?

正常來講接下來我們就要進行一連串補值,特徵工程,如果各位已經看過我Titanic 生還預測那篇,就會知道這項工作需要仔細的處理,要花不少時間。

而這次我選擇的是更偷懶(或說更自動化)的方法:讓模型自己猜

我使用了兩種工具進行補值:

🧮 數值型欄位 → KNN Imputer:看誰跟你最像,就用他的值補你

KNN Imputer 是一種鄰居補值法,它會找出每筆資料最相似的幾個「鄰居」,再用他們的平均值來補上你缺的那一格。

簡單來說:你不知道這個房子的 LotFrontage,那就看看其他跟它差不多的房子,平均是多寬,就給它那個值。

🔤 類別型欄位 → Simple Imputer:大家都說是這個,那就用這個吧

至於文字(類別)欄位,我們就簡單粗暴地用眾數(出現最多次的那個值)去補。

這就是 SimpleImputer 的策略:你不知道,就聽大家說最多的是什麼。

這兩個的作法如下:

首先分開數值型與類別型的欄位

from sklearn.impute import KNNImputer, SimpleImputer
# 找出欄位
numeric_features = df_train.select_dtypes(include=[np.number]).columns.drop('SalePrice')
categorical_features = df_train.select_dtypes(include=['object']).columns
# train跟test都處理
train_num = df_train[numeric_features]
test_num = df_test[numeric_features]

train_cat = df_train[categorical_features]
test_cat = df_test[categorical_features]

然後我們用train資料集來跑這個規則,再把這個規則套到test上,以防資料洩漏。

其實在 Kaggle 上,大多數人不會這麼講究,直接將 train+test 一起補值的人也很多。 但這樣容易造成資料洩漏與過擬合。如果未來參加更嚴謹的比賽,或想建立真正能商轉的模型,模擬結果與實際表現的落差,可能就會非常明顯。

# 數值型補值:KNNImputer
imputer_num = KNNImputer(n_neighbors=5)
train_num_imputed = pd.DataFrame(imputer_num.fit_transform(train_num), columns=numeric_features)
test_num_imputed = pd.DataFrame(imputer_num.transform(test_num), columns=numeric_features)

# 類別型補值:SimpleImputer
imputer_cat = SimpleImputer(strategy='most_frequent')
train_cat_imputed = pd.DataFrame(imputer_cat.fit_transform(train_cat), columns=categorical_features)
test_cat_imputed = pd.DataFrame(imputer_cat.transform(test_cat), columns=categorical_features)

最後將數值合併回來。

# 合併補值後的資料
train_imputed = pd.concat([train_num_imputed, train_cat_imputed], axis=1)
test_imputed = pd.concat([test_num_imputed, test_cat_imputed], axis=1)

我們檢查一下還有沒有缺值。

print("Train 缺失值總數:", train_imputed.isnull().sum().sum())
print("Test 缺失值總數:", test_imputed.isnull().sum().sum())
Train 缺失值總數: 0
Test 缺失值總數: 0

這樣一來,所有的缺值就都被補完了。

因為我們接下來使用的模型不一定都能處理類別特徵(像 XGBoost 就不吃文字),

所以我直接對所有類別欄位做 one-hot encoding:

# 分別對 train 和 test 做 one-hot encoding(會自動轉換類別欄位)
X_train_encoded = pd.get_dummies(train_imputed, drop_first=True)
X_test_encoded = pd.get_dummies(test_imputed, drop_first=True)

# 對齊欄位(防止 test 少欄位 or 多欄位)
X_train_encoded, X_test_encoded = X_train_encoded.align(X_test_encoded, join='left', axis=1, fill_value=0)

這題的目標變數 SalePrice分布相當偏態,

所以我也對它做了一個 log 處理,讓模型學得更穩定:

y = np.log1p(df_train['SalePrice'])  

好,我們完成特徵的部分了。

沒有加入背景知識、沒有條件判斷,純粹讓演算法根據「相似性」與「群眾智慧」來決定該補什麼。

沒錯,就這麼簡單。

但簡單也是有代價的——我完全不了解每個特徵的意義,也不知道它們彼此之間的關聯。

我不知道 LotFrontage 應該填 0,還是該依照 Neighborhood 來推估;這些我都沒有碰。

畢竟這篇主要是想介紹模型的堆疊與超參數調整。

也許未來有空,我會再回來把特徵理解補上,說不定,那時候能再把分數推高一點也說不定。

🧬 模型進化論:用遺傳演算法找到每個模型的最適姿態

在資料補完後,我並沒有急著進行特徵工程,因為這次的目標(當時也不知道怎麼做),正是看看不碰特徵,只靠模型本身與參數搜尋,能走到哪一步。

既然要讓模型自己發揮潛力,那就讓它們進化看看,這時候我想到了我的碩士論文:應用遺傳演算法最佳化三相感應電動機,當時我利用遺傳演算法的全域搜尋特性,將馬達設計參數化,成功的找到最佳設計值,那同樣的,這裡的超參數我想也能做這件事情。

🔁 簡單介紹一下遺傳演算法

遺傳演算法(Genetic Algorithm, GA)是一種模擬自然界演化過程的優化方法。

它的核心思想來自「達爾文的演化論」:適者生存,優勝劣汰,讓好的基因留下來,代代強化,慢慢進化出更好的解。

整個流程可以簡單想成這樣:

  1. 初始化族群:隨機生成一批參數組合,這些就像一個個候選者的 DNA。
  2. 適應度評估:把每一組參數丟進模型裡試試看,表現好不好就是它的「適應度」分數。
  3. 選擇與交配:挑出高分的幾組讓它們交配,也就是混合參數、生成下一代。
  4. 突變:為了避免卡在某個「局部最佳解」,還會對某些參數動點手腳,模擬基因突變。
  5. 迭代更新:每一代都重複這個流程,讓分數高的組合留下,繼續繁衍。

經過多代進化後,我們希望能找到某組參數,剛好就是那組讓模型表現最好的組合。

和 Grid Search 那種暴力窮舉不同,GA 不會每個都試一遍,而是從好的解出發,不斷修正改良。這種策略在參數空間很大時特別有效,尤其像這次要調四個不同模型、上百種組合,用 GA 就顯得聰明多了。

當然,它也有缺點。

一組基因就要跑一次訓練,代表族群大、維度高、世代多時,花費時間也會非常驚人。

而且 GA 對「適應度設計」非常敏感,如果你定義得不好,就很容易提早卡在局部最優,整個族群都在原地打轉。

🛠️ 設定遺傳演算法與模型個體

這邊我們先建立 GA 使用的 Fitness評分標準與 Individual 結構。

# 3. 定義遺傳演算法的適應度和個體類型
if 'FitnessMin' in creator.__dict__:
del creator.FitnessMin
creator.create("FitnessMin", base.Fitness, weights=(-1.0,))

if 'Individual' in creator.__dict__:
del creator.Individual
creator.create("Individual", list, fitness=creator.FitnessMin)

def create_individual_rf():
return creator.Individual([random.uniform(0.3, 0.7), # 相關性閾值
random.randint(3, 10), # 隨機森林最大深度
random.randint(50, 200)]) # 隨機森林樹的數量

def create_individual_xgb():
return creator.Individual([random.uniform(0.3, 0.7), # 相關性閾值
random.randint(50, 500), # XGB 最大樹數量
random.uniform(0.01, 0.3)]) # XGB 學習率

def create_individual_cat():
return creator.Individual([random.uniform(0.3, 0.7), # 相關性閾值
random.randint(100, 1000), # CatBoost 最大迭代次數
random.uniform(0.01, 0.3), # CatBoost 學習率
random.uniform(1, 10), # CatBoost L2 正則化系數
random.uniform(0, 1)]) # CatBoost bagging 溫度
def create_individual_extra():
return creator.Individual([random.uniform(0.3, 0.7), # 相關性閾值
random.randint(3, 10), # ExtraTrees 最大深度
random.randint(50, 200)]) # ExtraTrees 樹的數量

這邊我們有用幾個模型就要定義多少個範圍,因為每個模型能改變的參數不太一樣。

🧪 評估函數:每個模型都需要自己的適應度函數

為了讓 GA 正確評估每一組參數,我們針對每個模型寫一個 evaluate_xxx() 函數

這些函數的主要流程都是:

  1. 根據「相關性閾值」挑選特徵(用皮爾森相關係數判斷每個特徵相關度,大於設定值才用於預測)
  2. 建立對應模型
  3. 用 3-Fold Cross Validation 測試 RMSE
  4. 回傳平均 RMSE 作為「適應度」

首先是隨機森林:

def evaluate_rf(individual):
#將超參數拉入
corr_threshold, rf_max_depth, rf_n_estimators = individual
rf_max_depth = int(rf_max_depth)
rf_n_estimators = int(rf_n_estimators)
seed = 86
# 特徵選擇
corr = X_train_encoded.corrwith(y).abs()
selected_features = corr[corr > corr_threshold].index
X_selected = X_train_encoded[selected_features]
# 如果沒有選到特徵,自動給最差的適應值(通常不會出現,但防止Bug情況)
if X_selected.shape[1] == 0:
return 999999.,

# 模型與評分
model = RandomForestRegressor(
max_depth=rf_max_depth,
n_estimators=rf_n_estimators,
random_state=int(seed),
n_jobs=-1
)
pipeline = Pipeline([('model', model)])
kf = KFold(n_splits=3, shuffle=True, random_state=int(seed))
scores = cross_val_score(pipeline, X_selected, y, scoring='neg_root_mean_squared_error', cv=kf)
avg_rmse = -np.mean(scores)

return avg_rmse,

XGBoost

def evaluate_xgb(individual):
#將超參數拉入
corr_threshold, xgb_n_estimators, xgb_learning_rate = individual
xgb_n_estimators = int(xgb_n_estimators)
xgb_learning_rate = max(0.01, xgb_learning_rate)
seed = 42
# 特徵選擇
corr = X_train_encoded.corrwith(y).abs()
selected_features = corr[corr > corr_threshold].index
X_selected = X_train_encoded[selected_features]
# 如果沒有選到特徵,自動給最差的適應值(通常不會出現,但防止Bug情況)
if X_selected.shape[1] == 0:
return 999999.,

model = XGBRegressor(
objective='reg:squarederror',
n_estimators=xgb_n_estimators,
learning_rate=xgb_learning_rate,
random_state=seed,
tree_method='hist', # ✅ GPU 加速
device ='cuda', # ✅ GPU 推論
)

pipeline = Pipeline([('model', model)])
kf = KFold(n_splits=3, shuffle=True, random_state=seed)
scores = cross_val_score(pipeline, X_selected, y, scoring='neg_root_mean_squared_error', cv=kf)
avg_rmse = -np.mean(scores)

return avg_rmse,

Extratree

def evaluate_extra(individual):
#將超參數拉入
corr_threshold, extra_max_depth, extra_n_estimators = individual
extra_n_estimators = int(extra_n_estimators)
extra_max_depth = int(extra_max_depth)
seed=42
# 特徵選擇
corr = X_train_encoded.corrwith(y).abs()
selected_features = corr[corr > corr_threshold].index
X_selected = X_train_encoded[selected_features]
# 如果沒有選到特徵,自動給最差的適應值(通常不會出現,但防止Bug情況)
if X_selected.shape[1] == 0:
return 999999.,

# 模型與評分
model = ExtraTreesRegressor(
max_depth=extra_max_depth,
n_estimators=extra_n_estimators,
random_state=int(seed),
n_jobs=-1
)
pipeline = Pipeline([('model', model)])
kf = KFold(n_splits=3, shuffle=True, random_state=int(seed))
scores = cross_val_score(pipeline, X_selected, y, scoring='neg_root_mean_squared_error', cv=kf)
avg_rmse = -np.mean(scores)

return avg_rmse,

CatBoost

def evaluate_cat(individual):
#將超參數拉入
corr_threshold, cat_iterations, cat_learning_rate, cat_l2_leaf_reg, cat_bagging_temperature = individual
cat_iterations = int(max(100, cat_iterations))
cat_learning_rate = max(0.01, cat_learning_rate)
cat_l2_leaf_reg = max(0, cat_l2_leaf_reg)
cat_bagging_temperature = max(0, cat_bagging_temperature)
seed=42
# 特徵選擇
corr = X_train_encoded.corrwith(y).abs()
selected_features = corr[corr > corr_threshold].index
X_selected = X_train_encoded[selected_features]
# 如果沒有選到特徵,自動給最差的適應值(通常不會出現,但防止Bug情況)
if X_selected.shape[1] == 0:
return 999999.,

# 模型與評分
model = CatBoostRegressor(
iterations=cat_iterations,
learning_rate=cat_learning_rate,
l2_leaf_reg=cat_l2_leaf_reg,
bagging_temperature=cat_bagging_temperature,
verbose=0,
random_state=int(seed),
task_type='GPU',
thread_count=-1
)
pipeline = Pipeline([('model', model)])
kf = KFold(n_splits=3, shuffle=True, random_state=int(seed))
scores = cross_val_score(pipeline, X_selected, y, scoring='neg_root_mean_squared_error', cv=kf)
avg_rmse = -np.mean(scores)

return avg_rmse,

這樣四個模型的跑分都設定完了,其實他們是能夠寫在一起的,但我這次將他分開寫,方便我們針對某個模型做調整。

🧰 初始化 toolbox:定義每個模型的進化行為

然後我們初始化poo,以及將工具箱放進去。

每個模型都需要建立一組對應的 toolbox,裡面會設定:

  • "individual":使用哪個個體生成器
  • "population":一開始的族群要怎麼生出來
  • "mate":交配策略(這邊用 cxBlend
  • "mutate":突變策略(使用 mutPolynomialBounded
  • "select":挑選高分基因的方式(selTournament
  • "evaluate":對應的評估函數

這裡是四個 toolbox 設定的範例:

# 5. 設置遺傳演算法
# 初始化 pool 並將其應用於每個工具箱
toolbox_extra = base.Toolbox()
toolbox_extra.register("individual", create_individual_extra)
toolbox_extra.register("population", tools.initRepeat, list, toolbox_extra.individual)
toolbox_extra.register("mate", tools.cxBlend, alpha=0.5)
toolbox_extra.register("mutate", tools.mutPolynomialBounded, low=[0, 3, 50],
up=[0.7, 10, 200], eta=1.0, indpb=0.2)
toolbox_extra.register("select", tools.selTournament, tournsize=3)
toolbox_extra.register("evaluate", evaluate_extra)

toolbox_rf = base.Toolbox()
toolbox_rf.register("individual", create_individual_rf)
toolbox_rf.register("population", tools.initRepeat, list, toolbox_rf.individual)
toolbox_rf.register("mate", tools.cxBlend, alpha=0.5)
toolbox_rf.register("mutate", tools.mutPolynomialBounded, low=[0, 3, 50],
up=[0.7, 10, 200], eta=1.0, indpb=0.2)
toolbox_rf.register("select", tools.selTournament, tournsize=3)
toolbox_rf.register("evaluate", evaluate_rf)

toolbox_xgb = base.Toolbox()
toolbox_xgb.register("individual", create_individual_xgb)
toolbox_xgb.register("population", tools.initRepeat, list, toolbox_xgb.individual)
toolbox_xgb.register("mate", tools.cxBlend, alpha=0.5)
toolbox_xgb.register("mutate", tools.mutPolynomialBounded, low=[0, 50, 0.01],
up=[0.7, 500, 0.3], eta=1.0, indpb=0.2)
toolbox_xgb.register("select", tools.selTournament, tournsize=3)
toolbox_xgb.register("evaluate", evaluate_xgb)

toolbox_cat = base.Toolbox()
toolbox_cat.register("individual", create_individual_cat)
toolbox_cat.register("population", tools.initRepeat, list, toolbox_cat.individual)
toolbox_cat.register("mate", tools.cxBlend, alpha=0.5)
toolbox_cat.register("mutate", tools.mutPolynomialBounded, low=[0, 100, 0.01, 1, 0],
up=[0.7, 1000, 0.3, 10, 1], eta=1.0, indpb=0.2)
toolbox_cat.register("select", tools.selTournament, tournsize=3)
toolbox_cat.register("evaluate", evaluate_cat)

🚀 執行遺傳演算法 + 加入菁英政策

最後,我們將所有設定串接起來,並建立主流程函式 run_ga() 來執行遺傳演算法。

這裡我加入了 菁英政策(Elite),確保每一代都保留目前最優解,避免突變或交配破壞優良基因。

這裡的族群數與代數需要重複嘗試,可以先建立一個初始值,再看輸出結果調整,另外因為我有設定菁英政策,故我將突變機率提高,使他能更廣域的搜尋。

# 執行遺傳演算法
def run_ga(toolbox, model_name):
population = toolbox.population(n=25) # 將初始個體數量設定為 25
ngen = 40 # 將演化代數設定為 40
cxpb, mutpb = 0.5, 0.3 # 交叉和突變的機率

hof = tools.HallOfFame(1) #菁英政策

stats = tools.Statistics(lambda ind: ind.fitness.values)
stats.register("avg", np.mean)
stats.register("std", np.std)
stats.register("min", np.min)
stats.register("max", np.max)

logbook = tools.Logbook()
logbook.header = ["gen", "nevals"] + stats.fields

population, logbook = algorithms.eaSimple(population, toolbox, cxpb=cxpb, mutpb=mutpb, ngen=ngen,
stats=stats, halloffame=hof, verbose=True)

# 保存 logbook 信息,以便追蹤遺傳演算法的進度,這有助於分析和改進演算法的性能
with open(f"logbook_{model_name}.txt", "w") as f:
for record in logbook:
f.write(str(record) + "\\n")

# ✅ 改為回傳真正的最優個體(來自 hof 而非最後一代)
best_individual = hof[0]
return best_individual

🧪 執行 GA,尋找四個模型的最佳參數

我們依序對四個模型執行 run_ga(),並記錄過程:

best_individual_rf = run_ga(toolbox_rf, "RandomForest")
best_individual_xgb = run_ga(toolbox_xgb, "XGB")
best_individual_cat = run_ga(toolbox_cat, "CatBoost")
best_individual_extra = run_ga(toolbox_extra, "ExtraTrees")

輸出結果會包含每一代的演化紀錄,像是這樣:

gen	nevals	avg     	std      	min     	max     
0 25 0.166168 0.0172626 0.146548 0.202589
1 17 0.154374 0.00884052 0.146208 0.184599
2 21 0.151973 0.0105041 0.145413 0.195039
3 16 0.147273 0.00203819 0.145413 0.153698
4 16 0.146344 0.00108279 0.145413 0.149782
5 23 0.146223 0.00101877 0.145413 0.148961
6 11 0.147419 0.00549678 0.145413 0.168971
7 14 0.145644 0.00055895 0.145413 0.147751
8 14 0.145483 0.000247765 0.145377 0.146499
9 20 0.145413 1.71711e-05 0.145377 0.145482
10 13 0.145662 0.000691196 0.145377 0.14809
11 16 0.14602 0.00274486 0.145377 0.159392
12 15 0.14542 0.000132421 0.145377 0.146062

可以看到隨著代數,逐漸向小收斂(這次的題目越小越好),如果發現不收斂,最小值沒辦法固定在一個值,代表我們再遺傳演算法設定需要修改,或著下的限制過於嚴苛。

也推薦各位初期可以多跑幾次最佳化,如果發現演化結束而數字還沒有收斂到底,可以適當提升演化代數以及族群大小。

🏁 最佳參數輸出

最後,我們可以把四個模型進化後的最佳參數列印出來,方便之後建模使用:


# 打印最佳參數
print(f'===============ExtraTrees================')
print(f'最佳 ExtraTrees 相關性閾值: {best_individual_extra[0]}')
print(f'最佳 ExtraTrees 最大深度: {best_individual_extra[1]}')
print(f'最佳 ExtraTrees 樹的數量: {best_individual_extra[2]}')
print(f'===============RandomForest================')
print(f'最佳 RandomForest 相關性閾值: {best_individual_rf[0]}')
print(f'最佳 RandomForest 最大深度: {best_individual_rf[1]}')
print(f'最佳 RandomForest 樹的數量: {best_individual_rf[2]}')
print(f'===============XGBoost================')
print(f'最佳 XGB 相關性閾值: {best_individual_xgb[0]}')
print(f'最佳 XGB 最大樹數量: {best_individual_xgb[1]}')
print(f'最佳 XGB 學習率: {best_individual_xgb[2]}')
print(f'===============CatBoost================')
print(f'最佳 CatBoost 相關性閾值: {best_individual_cat[0]}')
print(f'最佳 CatBoost 最大迭代次數: {best_individual_cat[1]}')
print(f'最佳 CatBoost 學習率: {best_individual_cat[2]}')
print(f'最佳 CatBoost L2 正則化系數: {best_individual_cat[3]}')
print(f'最佳 CatBoost bagging 溫度: {best_individual_cat[4]}')
===============ExtraTrees================
最佳 ExtraTrees 相關性閾值: 0.17995882514659048
最佳 ExtraTrees 最大深度: 10
最佳 ExtraTrees 樹的數量: 154.08737464080576
===============RandomForest================
最佳 RandomForest 相關性閾值: 0.025171051537216535
最佳 RandomForest 最大深度: 10.178117014545315
最佳 RandomForest 樹的數量: 198.42087919681785
===============XGBoost================
最佳 XGB 相關性閾值: 0.011123061372732123
最佳 XGB 最大樹數量: 191.32257416062868
最佳 XGB 學習率: 0.10129660651593869
===============CatBoost================
最佳 CatBoost 相關性閾值: -0.03480750135703117
最佳 CatBoost 最大迭代次數: 484.96733184495145
最佳 CatBoost 學習率: 0.07824769933482238
最佳 CatBoost L2 正則化系數: 2.25089427214617
最佳 CatBoost bagging 溫度: 0.7096159592835822

這邊可以把這些數字記下來,這樣就不用每次都重跑一次最佳化。

另外這裡發現一個奇怪狀況,相關性閾值低於0,這在我的設定上不應該出現,原因來自Deap的突變設定並不是這麼嚴謹,雖然有設定上下限,但在一些情況他還是會超過一點點,我們可以在4個 def evaluate_OO(individual): 裡增加一段,這裡已隨機森林舉例

def evaluate_rf(individual):
corr_threshold, rf_max_depth, rf_n_estimators = individual
#增加限制合理範圍的clip
corr_threshold = float(np.clip(corr_threshold, 0.0, 0.7))
rf_max_depth = int(np.clip(rf_max_depth, 3, 15))
rf_n_estimators = int(np.clip(rf_n_estimators, 50, 500))
seed = 86
# 特徵選擇
#以下與隨機森林code後半段一樣
#(............)

不過Bug沒有影響這次的結果,因為我本來就設定最低是0,也就是全特徵進入使用,負值與0是一樣的意思,但不是每一次都能適用這個情況,建議還是加上限制。

🧪 模型訓練

單模型評估

有了前面透過遺傳演算法所找出的最佳超參數,我們就可以套用到各模型進行訓練。首先來看四個單一模型在交叉驗證下的表現分數:

def select_features_by_corr(X, y, threshold):
corr = X.corrwith(y).abs()
return X[corr[corr > threshold].index]

#相關性閥值設定
X_et = select_features_by_corr(X_train_encoded, y, best_individual_extra[0])
X_rf = select_features_by_corr(X_train_encoded, y, best_individual_rf[0])
X_xgb = select_features_by_corr(X_train_encoded, y, best_individual_xgb[0])
X_cat = select_features_by_corr(X_train_encoded, y, best_individual_cat[0])

X_test_et = X_test_encoded[X_et.columns]
X_test_rf = X_test_encoded[X_rf.columns]
X_test_xgb = X_test_encoded[X_xgb.columns]
X_test_cat = X_test_encoded[X_cat.columns]

models = {
'ExtraTrees': (
ExtraTreesRegressor(
max_depth=int(best_individual_extra[1]),
n_estimators=int(best_individual_extra[2]),
random_state=86
),
X_et, X_test_et
),
'RandomForest': (
RandomForestRegressor(
max_depth=int(best_individual_rf[1]),
n_estimators=int(best_individual_rf[2]),
random_state=42
),
X_rf, X_test_rf
),
'XGBRegressor': (
XGBRegressor(
objective='reg:squarederror',
n_estimators=int(best_individual_xgb[1]),
learning_rate=best_individual_xgb[2],
random_state=42,
tree_method='hist',
device='cuda'
),
X_xgb, X_test_xgb
),
'CatBoostRegressor': (
CatBoostRegressor(
iterations=int(best_individual_cat[1]),
learning_rate=best_individual_cat[2],
l2_leaf_reg=best_individual_cat[3],
bagging_temperature=best_individual_cat[4],
verbose=0,
random_state=42,
task_type='GPU'
),
X_cat, X_test_cat
)
}

results = {}
for name, (model, X_sel, _) in models.items():
steps = [('model', model)] if name in ['RandomForest', 'XGBRegressor', 'CatBoostRegressor'] else [('scaler', StandardScaler()), ('model', model)]
pipeline = Pipeline(steps)

kf = KFold(n_splits=5, shuffle=True, random_state=42)
scores = cross_val_score(pipeline, X_sel, y, scoring='neg_mean_squared_error', cv=kf)
rmse_scores = np.sqrt(-scores)

results[name] = rmse_scores.mean()
print(f'{name} 模型的平均 RMSE: {rmse_scores.mean():.6f}')

# 找出最佳模型名稱與其元件
best_model_name = min(results, key=results.get)
print(f'\\n最好的模型是: {best_model_name},其平均 RMSE: {results[best_model_name]:.6f}')

best_model, X_sel, X_test_sel = models[best_model_name]
ExtraTrees 模型的平均 RMSE: 0.145187
RandomForest 模型的平均 RMSE: 0.145201
XGBRegressor 模型的平均 RMSE: 0.143630
CatBoostRegressor 模型的平均 RMSE: 0.128703

最好的模型是: CatBoostRegressor,其平均 RMSE: 0.128703

看起來 CatBoost 的表現明顯優於其他模型,取得了 0.1287 的 RMSE。我們將這組預測結果提交到 Kaggle leaderboard:

raw-image

模型融合:Stacking

雖然單一的 CatBoost 表現已經非常出色,但我們仍嘗試透過 Stacking 將不同模型的預測進行融合,期待在多樣性與偏誤互補的幫助下,進一步強化模型在不同樣本情境下的穩定度與泛化能力。

以下是我們實作 Stacking 模型的過程。

import numpy as np
from sklearn.model_selection import KFold
from sklearn.ensemble import GradientBoostingRegressor
from sklearn.metrics import mean_squared_error
from sklearn.linear_model import RidgeCV

def select_features_by_corr(X, y, threshold):
corr = X.corrwith(y).abs()
return X[corr[corr > threshold].index]

# Step 1:為每個模型選擇特徵
X_et = select_features_by_corr(X_train_encoded, y, best_individual_extra[0])
X_rf = select_features_by_corr(X_train_encoded, y, best_individual_rf[0])
X_xgb = select_features_by_corr(X_train_encoded, y, best_individual_xgb[0])
X_cat = select_features_by_corr(X_train_encoded, y, best_individual_cat[0])

X_test_et = X_test_encoded[X_et.columns]
X_test_rf = X_test_encoded[X_rf.columns]
X_test_xgb = X_test_encoded[X_xgb.columns]
X_test_cat = X_test_encoded[X_cat.columns]

# Step 2:定義模型與特徵
models = {
'ExtraTrees': (
ExtraTreesRegressor(
max_depth=int(best_individual_extra[1]),
n_estimators=int(best_individual_extra[2]),
random_state=42
),
X_et, X_test_et
),
'RandomForest': (
RandomForestRegressor(
max_depth=int(best_individual_rf[1]),
n_estimators=int(best_individual_rf[2]),
random_state=42
),
X_rf, X_test_rf
),
'XGBRegressor': (
XGBRegressor(
objective='reg:squarederror',
n_estimators=int(best_individual_xgb[1]),
learning_rate=best_individual_xgb[2],
random_state=42,
tree_method='hist',
device='cuda'
),
X_xgb, X_test_xgb
),
'CatBoostRegressor': (
CatBoostRegressor(
iterations=int(best_individual_cat[1]),
learning_rate=best_individual_cat[2],
l2_leaf_reg=best_individual_cat[3],
bagging_temperature=best_individual_cat[4],
verbose=0,
random_state=42,
task_type='GPU'
),
X_cat, X_test_cat
)
}

kf = KFold(n_splits=5, shuffle=True, random_state=42)
meta_train = np.zeros((X_train_encoded.shape[0], len(models)))
meta_test = np.zeros((X_test_encoded.shape[0], len(models)))

for idx, (name, (model, X_feat, X_test_feat)) in enumerate(models.items()):
print(f'⏳ 執行模型:{name}')
oof_preds = np.zeros(X_feat.shape[0])
test_preds = np.zeros((X_test_feat.shape[0], kf.get_n_splits()))

for i, (train_idx, val_idx) in enumerate(kf.split(X_feat)):
X_tr, X_val = X_feat.iloc[train_idx], X_feat.iloc[val_idx]
y_tr, y_val = y.iloc[train_idx], y.iloc[val_idx]

model.fit(X_tr, y_tr)
oof_preds[val_idx] = model.predict(X_val)
test_preds[:, i] = model.predict(X_test_feat)

rmse = mean_squared_error(y, oof_preds, squared=False)
print(f'✅ {name} OOF RMSE(log): {rmse:.5f}')

meta_train[:, idx] = oof_preds
meta_test[:, idx] = test_preds.mean(axis=1)

# Step 4:Stacking meta-model
print('\\n🚀 訓練 Stacking 模型(RidgeCV)...')
stacking_model = RidgeCV(alphas=[0.1, 1.0, 10.0])
stacking_model.fit(meta_train, y)

# Step 5:評估
stacking_oof_preds = stacking_model.predict(meta_train)
rmse_log = mean_squared_error(y, stacking_oof_preds, squared=False)
print(f"✅ Stacking 模型的 RMSE(log(y)): {rmse_log:.6f}")

# Step 6:測試集預測
stacking_preds = stacking_model.predict(meta_test)
stacking_preds = np.exp(stacking_preds)

前面的單模型輸出可知,CatBoost相較其他模型強上不少,因此相對於一般 LinearRegression,RidgeCV 在模型權重學習時加入 L2 正則化,能避免 CatBoost 權重完全壓制其他模型,並保留其他模型可能在特定樣本下的貢獻。

⏳ 執行模型:ExtraTrees
⏳ 執行模型:RandomForest
⏳ 執行模型:XGBRegressor
⏳ 執行模型:CatBoostRegressor

🚀 訓練 Stacking 模型(RidgeCV)...
✅ Stacking 模型的 RMSE: 0.129273

乍看之下,Stacking 的 log(RMSE) 分數略高於單一 CatBoost,但為了驗證其泛化能力,我們將此結果提交至 Kaggle:

raw-image

提交結果為 0.12533,略優於單模型的 0.1287,證明 Stacking 雖然提升有限,但確實有助於降低泛化誤差。

此外,考慮到 CatBoost 在訓練集可能過擬合的潛在風險,Stacking 模型中的 RidgeCV 透過 L2 正則化能夠壓制其過度影響,讓其他模型也能適當發揮,這一點從 leaderboard 的穩定提升也能略見端倪。

🧾 結語:

這次的挑戰原本只是想試試看「單純靠模型本身」可以達到什麼樣的程度,沒想到在未進行深入特徵工程的情況下,分數表現竟然也還算令人滿意。

從超參數最佳化的過程可以看出,這次的模型傾向保留所有特徵,即使是低相關性的欄位,也可能在樹模型的分裂中發揮了一點貢獻,這也再次驗證了非線性模型在捕捉複雜關係上的彈性與優勢。

最終模型在 Kaggle 排名約 700 名左右,約落在前 15%,若排除部分疑似使用外部資訊的帳號,實際可能更接近前 10%。這樣的結果,對於未經大量特徵清理的 baseline 而言,是一個值得肯定的起點。

未來若有時間,我會回頭強化這題的特徵工程,重新檢視哪些欄位有實質意義、哪些只是噪音,也許還能一舉將 RMSE 壓進 0.1 以下。畢竟,真正的泛化能力,不只來自模型堆疊,而是每一次對資料的理解與詮釋。

Code

https://github.com/Noah-incoding/House-Prices---Advanced-Regression-Techniques.git

留言
avatar-img
留言分享你的想法!
avatar-img
夕月之下
0會員
8內容數
在模型尚未收斂前,記下語言的提示與意圖。觀察者、語言與語言模型的交界。
夕月之下的其他內容
2025/10/01
這篇是Titanic 生還預測:Machine Learning from Disaster原先後面有的補充資料,因為字數限制另外開到這篇寫。透過 Optuna,我們可以讓模型自主尋找最佳的特徵組合和參數設定,大幅提升實驗效率。
2025/10/01
這篇是Titanic 生還預測:Machine Learning from Disaster原先後面有的補充資料,因為字數限制另外開到這篇寫。透過 Optuna,我們可以讓模型自主尋找最佳的特徵組合和參數設定,大幅提升實驗效率。
2025/10/01
這篇文章記錄了我第一次進行鐵達尼號比賽,以及後來又再度認真的玩這個比賽的過程,會介紹一下我最終的code以及最一開始到最終的心路歷程,分享給大家做參考。
Thumbnail
2025/10/01
這篇文章記錄了我第一次進行鐵達尼號比賽,以及後來又再度認真的玩這個比賽的過程,會介紹一下我最終的code以及最一開始到最終的心路歷程,分享給大家做參考。
Thumbnail
2025/09/04
本文簡單地介紹線性模型、樹模型和神經網路三大類機器學習模型,並結合Kaggle比賽實戰經驗,說明模型選擇、超參數調整、模型融合和模型解釋等關鍵技術。
Thumbnail
2025/09/04
本文簡單地介紹線性模型、樹模型和神經網路三大類機器學習模型,並結合Kaggle比賽實戰經驗,說明模型選擇、超參數調整、模型融合和模型解釋等關鍵技術。
Thumbnail
看更多
你可能也想看
Thumbnail
試聞 Sunkronizo的香氛後,我才發現:原來不是我在挑香,而是香氣更早知道我是誰。原本以為自己最像溫柔的 1 號,真正試香後卻被成熟、冷靜的 3 號選中。其他七瓶香,也意外喚醒我生命中不同階段的八種角色。香氣讓我明白——人生不只直線前進,也能橫向展開,切換更多樣的自己。
Thumbnail
試聞 Sunkronizo的香氛後,我才發現:原來不是我在挑香,而是香氣更早知道我是誰。原本以為自己最像溫柔的 1 號,真正試香後卻被成熟、冷靜的 3 號選中。其他七瓶香,也意外喚醒我生命中不同階段的八種角色。香氣讓我明白——人生不只直線前進,也能橫向展開,切換更多樣的自己。
Thumbnail
前面介紹了幾種基於規則的策略 https://vocus.cc/article/685d463dfd89780001f3d7fd 並驗證了它們在比特幣上的表現 其實多數都是虧錢的 這一章開始用隨機森林來測試機器學習有沒有幫助 一樣沿用之前的 定義一個隨機森林策略 準備資料 注意機器學
Thumbnail
前面介紹了幾種基於規則的策略 https://vocus.cc/article/685d463dfd89780001f3d7fd 並驗證了它們在比特幣上的表現 其實多數都是虧錢的 這一章開始用隨機森林來測試機器學習有沒有幫助 一樣沿用之前的 定義一個隨機森林策略 準備資料 注意機器學
Thumbnail
AutoML 透過自動特徵工程、模型搜尋與超參數調校,把需要資深數據科學家耗時完成的工作交給系統自動化執行。它能在時間與算力內快速比較演算法組合、挑出最優方案,並自動生成易於部署的程式碼與報告,大幅降低 AI 專案門檻,讓中小企業、政府與教育單位都能用少量資料與人力驗證商業構想,加速 AI 普及。
Thumbnail
AutoML 透過自動特徵工程、模型搜尋與超參數調校,把需要資深數據科學家耗時完成的工作交給系統自動化執行。它能在時間與算力內快速比較演算法組合、挑出最優方案,並自動生成易於部署的程式碼與報告,大幅降低 AI 專案門檻,讓中小企業、政府與教育單位都能用少量資料與人力驗證商業構想,加速 AI 普及。
Thumbnail
透過 Docker,可將模型、環境與依賴完整封裝,避免開發與生產環境不一致的災難。搭配 RESTful API 與 GPU 加速,實現快速部署、跨平台一致性與大規模擴展。無論是在電商高流量推薦系統,或是醫療內部部署診斷模型,Docker 都能大幅提升彈性與效率,是 AI 工程化、商業化的強大後盾!
Thumbnail
透過 Docker,可將模型、環境與依賴完整封裝,避免開發與生產環境不一致的災難。搭配 RESTful API 與 GPU 加速,實現快速部署、跨平台一致性與大規模擴展。無論是在電商高流量推薦系統,或是醫療內部部署診斷模型,Docker 都能大幅提升彈性與效率,是 AI 工程化、商業化的強大後盾!
Thumbnail
想成為實戰型 AI 工程師?那就上 Kaggle 吧!Kaggle 是資料科學界的黃金武道場,結合真實世界資料集、高強度競賽與全球高手解法,是學習資料處理、建模、特徵工程與模型優化的絕佳平台。不僅能累積作品集與面試亮點,還能透過競賽磨練實戰思維、掌握業界最佳實踐。
Thumbnail
想成為實戰型 AI 工程師?那就上 Kaggle 吧!Kaggle 是資料科學界的黃金武道場,結合真實世界資料集、高強度競賽與全球高手解法,是學習資料處理、建模、特徵工程與模型優化的絕佳平台。不僅能累積作品集與面試亮點,還能透過競賽磨練實戰思維、掌握業界最佳實踐。
Thumbnail
XGBoost(eXtreme Gradient Boosting)是一種基於梯度提升框架的機器學習算法,專注於高效的分類與迴歸問題。它廣泛應用於數據分析和競賽中,因其出色的模型訓練能力。本文探討 XGBoost 實際中的實作,適合希望掌握此技術的讀者,並對模型調參提供有價值的技巧與建議。
Thumbnail
XGBoost(eXtreme Gradient Boosting)是一種基於梯度提升框架的機器學習算法,專注於高效的分類與迴歸問題。它廣泛應用於數據分析和競賽中,因其出色的模型訓練能力。本文探討 XGBoost 實際中的實作,適合希望掌握此技術的讀者,並對模型調參提供有價值的技巧與建議。
Thumbnail
*本文章為參考李弘毅2021年機器學習課程後的筆記。 在訓練模型的時候,常常會遇到訓練上的問題,像是Loss值太大,或是Test出來的結果不如預期,但我們又不知道模型中到底發生了甚麼事,就跟黑盒子一樣。 因此,感謝李弘毅教授傳授了一套SOP來幫助我們判斷模型是哪裡出了問題,應該要怎麼解決!!
Thumbnail
*本文章為參考李弘毅2021年機器學習課程後的筆記。 在訓練模型的時候,常常會遇到訓練上的問題,像是Loss值太大,或是Test出來的結果不如預期,但我們又不知道模型中到底發生了甚麼事,就跟黑盒子一樣。 因此,感謝李弘毅教授傳授了一套SOP來幫助我們判斷模型是哪裡出了問題,應該要怎麼解決!!
Thumbnail
在機器學習中,超參數的設定對模型的性能至關重要。本文介紹了主要的超參數調整方法,包括網格搜索、隨機搜索、貝葉斯優化、交叉驗證以及自適應搜索算法。每種方法的優缺點詳細說明,幫助讀者選擇最合適的調整策略。透過這些技術,可以有效提高模型的泛化能力與性能,並實現更好的機器學習效果。
Thumbnail
在機器學習中,超參數的設定對模型的性能至關重要。本文介紹了主要的超參數調整方法,包括網格搜索、隨機搜索、貝葉斯優化、交叉驗證以及自適應搜索算法。每種方法的優缺點詳細說明,幫助讀者選擇最合適的調整策略。透過這些技術,可以有效提高模型的泛化能力與性能,並實現更好的機器學習效果。
追蹤感興趣的內容從 Google News 追蹤更多 vocus 的最新精選內容追蹤 Google News