🗺️ AI 學習與考證地圖
中級科目三程式實戰 · 超參數搜尋

三個參數、五折交叉驗證:
GridSearchCV 到底 fit 了幾次?🔍 試看版

這一題不用跑程式,也不用懂紅酒。你只需要抓對一件事:GridSearchCV 把 param_grid 攤開成幾種組合、每種組合又各被訓練幾次——以及 refit=False 那一行,有沒有偷偷多送你一次 fit。乘法抓對,答案就出來了;乘錯一步,就會掉進另外三個選項。

閱讀模式

00題目

Wine 分類任務以 RandomForestClassifier 進行網格搜尋,程式如下;本題不計搜尋完成後另行重訓,因此設定 refit=False交叉驗證期間總共會執行多少次模型 fit?

from sklearn.datasets import load_wine               # 從 sklearn 的資料集抽屜,拿出「載入紅酒資料」的工具
from sklearn.ensemble import RandomForestClassifier  # 拿出隨機森林分類器
from sklearn.model_selection import GridSearchCV     # 拿出網格搜尋工具(本題主角)
X_wine, y_wine = load_wine(return_X_y=True)          # 載入資料:X 是特徵、y 是答案
param_grid = {                                       # 開一本「候選值菜單」(字典)
    "n_estimators": [100, 200, 400],                # 樹的數量:3 個候選值
    "max_depth": [None, 5, 10, 20],                  # 樹的深度上限:4 個候選值
    "max_features": ["sqrt", "log2"]                 # 分岔時看幾個特徵:2 個候選值
}                                                    # 菜單寫完,大括號收起來
search = GridSearchCV(                               # 組一台搜尋機,取名 search
    RandomForestClassifier(random_state=42),         # 要被反覆訓練的模型
    param_grid=param_grid,                           # 把菜單交給它
    cv=5,                                            # 每種組合都做 5 折交叉驗證
    refit=False                                      # 搜完不自動重訓最後一次
)                                                    # 設定收尾——還沒開始跑
search.fit(X_wine, y_wine)                           # 按下開關:fit 從這裡開始算

先說「網格搜尋」是什麼

你幫全公司訂手搖飲,想找出最好喝的一杯:茶底有幾種、甜度有幾種、冰塊有幾種。最老實的辦法就是把每一種搭配都實際做一杯出來喝喝看,一種都不跳過——這就是網格搜尋(Grid Search)。「網格」的意思是:把每個選項的候選值排成一格一格的表,然後逐格全部試過

機器學習裡,「茶底、甜度、冰塊」對應的是模型的超參數——像隨機森林要種幾棵樹(n_estimators)、每棵樹最多長幾層(max_depth)。這些數字模型自己學不出來,只能由你先決定;而「哪一組最好」很難用猜的,所以 sklearn 提供 GridSearchCV:你開出菜單(param_grid),它幫你逐一「試喝」。

那要怎麼「試喝」才公平?每一種搭配都不能只喝一口就下結論——GridSearchCV 的做法是對每一種組合都做一輪完整的交叉驗證(名字尾巴的 CV 就是 Cross-Validation):把資料切成 k 折,輪流留一折當考題、其餘拿去訓練。所以總共要訓練幾次,就是「組合數」和「折數」的乘法——這正是本題要你算的東西。

先點開看:GridSearchCV 是什麼?超參數跟參數差在哪?fit 一次是指什麼?

不熟 AI 專有名詞?你需要先認識下列名詞

點擊後出現漸進式說明:白話說明 → 說清楚一點 → 常見錯誤與考點。

這題在做的事
GridSearchCV超參數 hyperparameterparam_grid組合數(笛卡兒積)fit 到底是什麼
交叉驗證那一乘
交叉驗證 cross-validationK 折 KFold分層 StratifiedKFoldscoring 評分方式
被搜尋的模型與資料
隨機森林 RandomForestn_estimatorsmax_depthmax_featuresrandom_stateWine 資料集X 與 y
搜尋完之後
refitbest_params_ 一家人cv_results_ 成績單
更省力的替代方案
RandomizedSearchCVHalvingGridSearchCVn_jobs 平行化

不熟 Python?你需要先認識下列名詞

點擊後出現漸進式說明

把工具拿進來
importfrom模組 module
存東西、取名字
= 指派a, b = ... 一次接兩個變數與命名
兩種括號
{ } 字典 dict鍵與值 key / value[ ] list 串列
呼叫與設定
( ) 呼叫括號關鍵字引數方法 method. 點運算子為什麼可以換行寫
特別的值
None"sqrt" 字串

01逐行拆解:第 1 行到第 16 行

十六行,一行一行走完。右上角的「看位置」可以把這一行放回完整程式裡看。

先認識兩種括號:{ } 與 [ ]

這題的程式一半都在跟括號打交道,先把三種括號的分工弄清楚,後面會順很多。

{ "n_estimators": [100, 200, 400] }   大括號 { } = 字典:「名字 → 內容」的對照表
[100, 200, 400]                       中括號 [ ] = 串列:一排照順序放的東西
GridSearchCV(...)                     小括號 ( ) = 呼叫:叫這個工具開始做事

想深入的話點這裡:字典是什麼?串列是什麼?呼叫括號括號裡的「名字=值」

第 1 行從資料集抽屜,拿出「載入紅酒資料」的工具
from sklearn.datasets import load_wine   # 只拿 load_wine 這一支工具出來

from A import B 的意思是「去 A 那個抽屜裡,只把 B 拿出來」。sklearn.datasets 是 scikit-learn 附的內建資料集抽屜,裡面放著練習用的小資料:紅酒(load_wine)、鳶尾花(load_iris)、乳癌(load_breast_cancer)、手寫數字(load_digits)⋯⋯

為什麼教學都愛用內建資料集?因為不用下載、不用清理、一行就能開始。它們都很小(本題的紅酒只有 178 筆),跑起來快,適合拿來練「流程」而不是練「資料工程」。

注意這一行只是把工具拿出來放桌上,還沒有真的載入資料——真正動手是第 4 行呼叫它的時候。

相關名詞:fromimport模組Wine 資料集

第 2 行拿出隨機森林分類器
from sklearn.ensemble import RandomForestClassifier   # 拿出「隨機森林」這個模型

sklearn.ensemble 是「集成方法」的抽屜——把很多個小模型合起來投票的那一類演算法都住在這裡。隨機森林就是最有名的一個:種一大群決策樹,每棵樹各自判斷,最後多數決

名字直接拆開讀就懂:Random(隨機)=每棵樹拿到的資料和特徵都刻意隨機打亂,讓樹長得不一樣;Forest(森林)=樹很多;Classifier(分類器)=做的是分類題。字尾如果是 Regressor,就是做預測數值的迴歸題。

之所以要介紹它,是因為待會 param_grid 裡的三個參數,全部都是這座森林的「造林規格」:種幾棵樹、每棵長多高、分岔時看幾個特徵。

相關名詞:隨機森林n_estimatorsmax_depth

第 3 行拿出網格搜尋工具——名字就是答案的地圖
from sklearn.model_selection import GridSearchCV   # 拿出網格搜尋工具

sklearn.model_selection 是「模型挑選」的抽屜:切訓練測試集(train_test_split)、交叉驗證(cross_val_scoreStratifiedKFold)、搜參數(GridSearchCVRandomizedSearchCV)都在這裡。

把 GridSearchCV 這個名字拆成三段,本題的解法其實已經寫在裡面

Grid    網格  → 把 param_grid 攤開成一格一格的候選組合
Search  搜尋  → 每一格都老實試過,一格都不跳
CV      交叉驗證 → 每一格都用 k 折來評分(本題 k = 5)

也就是說,看到 GridSearchCV 這個名字,就要反射性想到「格子數 × 折數」這個乘法——這正是本題唯一要考的東西。

相關名詞:GridSearchCV交叉驗證RandomizedSearchCV

第 4 行載入資料,一次接住 X 和 y
X_wine, y_wine = load_wine(return_X_y=True)   # 178 瓶酒的特徵與答案,分兩包接住

這一行真的去載入資料了。return_X_y=True 的意思是「直接回傳(X, y)兩包,不要包成一個大物件」;等號左邊用逗號寫了兩個名字,Python 就會照順序一次接住兩包——第一包給 X_wine、第二包給 y_wine

X 是題目、y 是答案:X_wine 是一張 178 列 × 13 欄的大表格,每一列是一瓶酒、每一欄是一個化學檢測值;y_wine 是 178 個答案(0、1、2),代表這瓶酒來自三個品種中的哪一個。

本站實跑(python3 + scikit-learn 1.8):X_wine.shape = (178, 13)、三個類別各有 59 / 71 / 48 瓶。13 個特徵是酒精濃度、蘋果酸、灰分、鎂、總酚、類黃酮、色澤強度、脯胺酸⋯⋯等化學檢測值。13 這個數字待會有戲份——第 8 行的 "sqrt""log2" 要用它來換算。

相關名詞:一次接兩個X 與 yWine 資料集關鍵字引數

第 5–9 行param_grid:開出你的候選值菜單
param_grid = {                        # 大括號=字典:開一本菜單
    "n_estimators": [100, 200, 400],     # 森林要種幾棵樹:3 個候選
    "max_depth": [None, 5, 10, 20],       # 每棵樹最多長幾層:4 個候選
    "max_features": ["sqrt", "log2"]      # 分岔時抽幾個特徵來比:2 個候選
}                                     # 菜單寫完

這五行做出一個字典,取名 param_grid。字典的每一筆都是「參數名(字串)→ 候選值(串列)」:

第 6 行 "n_estimators":森林要種幾棵樹。候選 [100, 200, 400]3 個值
第 7 行 "max_depth":每棵樹最多長幾層。候選 [None, 5, 10, 20]4 個值——None 也算一個正式的候選值,意思是「不設上限,讓樹自然長到底」。
第 8 行 "max_features":每次分岔時,隨機抽幾個特徵出來比較。候選 ["sqrt", "log2"]2 個值,分別代表「取特徵數的平方根」與「取特徵數的以 2 為底對數」。

兩個常見的陷阱:
字典的鍵一定要跟模型參數同名,而且是字串。打錯字(例如 "n_estimator" 少個 s)不會默默被忽略,fit 時會直接噴 ValueError: Invalid parameter——這其實是好事,錯誤越早爆越好。
None 是 Python 的正式值,不是「沒有填」。它在候選名單裡佔一個名額,算組合數時要數進去——很多人第 7 行只數到 3 個,就是把 None 漏掉了。

讀法示範:param_grid["max_depth"] 唸作「param_grid 這本菜單裡,max_depth 那一頁」,翻開會看到 [None, 5, 10, 20] 四個候選。

相關名詞:param_grid字典鍵與值串列n_estimatorsmax_depthmax_features

第 10–11 行組搜尋機:先放進要被反覆訓練的模型
search = GridSearchCV(                      # 開始組搜尋機(括號先不關)
    RandomForestClassifier(random_state=42),   # 第一個材料:要被反覆訓練的模型

第 10 行開始呼叫 GridSearchCV(...) 並把成品存進變數 search。括號裡的第一個材料(第 11 行)是要被反覆訓練的模型本體——一台設定好 random_state=42 的隨機森林。

注意這台森林「沒有」寫 n_estimators、max_depth、max_features。這不是漏寫——這三個位子就是故意空下來的,待會搜尋時,GridSearchCV 會把菜單裡的候選值一組一組填進去。而且它每次都會先 clone 出一台全新的、乾淨的模型再填再訓練,不會沿用上一輪的訓練結果

random_state=42 是「固定隨機種子」:隨機森林裡有很多抽籤動作(抽資料、抽特徵),固定種子後每次重跑抽出來的都一樣,結果才能重現。42 沒有魔力,只是個大家愛用的哏。

第 10 行行尾的括號沒有關起來——Python 規定括號沒關完就可以一直換行寫下去,所以第 10 到 15 行其實是「同一句話」,只是排版排得比較好讀。

相關名詞:隨機森林random_state為什麼可以換行= 指派

第 12 行把菜單交給搜尋機
    param_grid=param_grid,   # 左邊是參數名、右邊是我們第 5 行做的字典

這一行長得有點繞口:param_grid=param_grid。其實是兩個不同的東西剛好同名——等號左邊是 GridSearchCV 的關鍵字引數名稱(它規定這個欄位就叫 param_grid);等號右邊是我們自己在第 5 行取名的那本字典。

像寄包裹:包裹單上有一格印好的欄位叫「品名」,你在格子裡填上自己包裹的名字。欄位名和包裹名剛好一樣,只是巧合(也是慣例——大家都把那本字典取名叫 param_grid)。把第 5 行的字典改名叫 menu,這一行寫成 param_grid=menu 也完全合法。

相關名詞:關鍵字引數param_grid變數與命名

第 13 行cv=5:每種組合都要過五關
    cv=5,   # 每種組合都做 5 折交叉驗證

cv=5 指定每一種參數組合都要做 5 折交叉驗證:把 178 瓶酒切成 5 份,輪流留 1 份當考題、其餘 4 份拿去訓練——每一輪都是一次全新的 fit,然後在留下的那份上打分數。5 輪的平均分,就是這個組合的成績。

對分類問題傳整數給 cv,sklearn 會自動改用分層版 StratifiedKFold:每一折裡三個品種的比例都跟整體幾乎一樣,不會出現「某一折剛好都是同一種酒」的不公平考題。

本站實跑:178 瓶酒切 5 折,各折考題數是 36 / 36 / 36 / 35 / 35,對應的訓練筆數是 142 或 143;每折考題裡三個品種大約都是 12 / 14 / 10 瓶——分層有確實發生。所以「一次 fit」用的不是 178 筆,是 142~143 筆,這在第 03 節的實驗室可以親眼看到。

相關名詞:交叉驗證K 折分層 StratifiedKFold成績怎麼算

第 14–15 行refit=False:搜完就停,不重訓最後一次
    refit=False   # 搜完不自動用最佳參數重訓一次
)                # 括號關起來,search 組裝完成

refit 的預設值是 True:搜尋結束後,GridSearchCV 會自動拿「成績最好的那組參數」,用全部資料再訓練最後一次,把成品掛在 best_estimator_ 上讓你直接拿去預測。那一次不屬於交叉驗證,是搜尋結束後的加碼。

本題寫成 False,就是「搜完就停,只留成績單」。題目那句「本題不計搜尋完成後另行重訓」講的正是這件事——出題者把 refit 關掉,是為了讓「總 fit 次數」只剩交叉驗證這一段,答案乾乾淨淨。

第 15 行的 ) 把第 10 行開的括號關起來——這句長達六行的「組裝指令」到此結束。到目前為止還是一次 fit 都沒有發生,我們只是把機器組好了。

相關名詞:refitbest_params_ 一家人

第 16 行search.fit:按下開關,連環 fit 開始
search.fit(X_wine, y_wine)   # 本題問的「fit 次數」就是從這裡開始數

前面 15 行全部只是「設定」。這一行才是按下開關:GridSearchCV 接過全部資料,開始它的固定流程——把每一種參數組合,在每一折上各訓練一次、評分一次,全部做完才把成績整理進 cv_results_

有趣的是這裡呼叫的 .fit() 跟一般模型的 .fit() 長得一模一樣——sklearn 刻意讓「搜尋器」用起來像「一個模型」。差別在於:一般模型的 fit 訓練一次,search 的 fit 裡面裝著一大串 fit

本站實跑:verbose=1 打開重跑一次,GridSearchCV 自己印出了這一行——
Fitting 5 folds for each of 24 candidates, totalling 120 fits
它把本題的算式直接唸給你聽:5 折 × 24 個候選組合。我們另外用「會數數的隨機森林」實際攔截計數,得到的 fit 次數分毫不差。單執行緒總共跑了 47.5 秒。

相關名詞:fit 到底是什麼方法點運算子cv_results_

02第一個乘法:菜單會開出幾種組合?

GridSearchCV 拿到菜單後做的第一件事,是把它攤開成所有可能的搭配:從每個參數的候選裡各挑一個值,湊成一組完整的設定,這叫一個「候選組合(candidate)」。數學上這是三個串列的笛卡兒積,講白話就是連乘

n_estimators   有 3 種選法(100 / 200 / 400)
max_depth      有 4 種選法(None / 5 / 10 / 20)
max_features   有 2 種選法(sqrt / log2)

組合數 = 3 × 4 × 2 = 24 種

為什麼是「乘」不是「加」?——因為每一種樹數量,都要配上每一種深度,
再配上每一種特徵抽法。就像 3 種茶底 × 4 種甜度 × 2 種冰塊 = 24 種飲料。
檢查自己的算法:搜尋跑完後,len(search.cv_results_["params"]) 會告訴你候選組合數——本站實跑回傳 24。成績單 cv_results_ 就是一張 24 列的表,一列一個組合。注意 24 還不是答案——它只是第一個乘法,每個組合接下來還要各考 5 次試。
互動實驗室:組合檢視器點掉或加回候選值,看組合數怎麼跳
候選組合數
24
cv_results_ 的列數
× 交叉驗證折數
5
本題固定 cv=5
= 交叉驗證的 fit 次數
120
refit=False,就到此為止
下面每一張卡就是一個候選組合。點一張卡,看它在整場搜尋裡佔了哪幾次 fit。
試一件事:按「n_estimators 多加一個 800」——只多寫一個數字,組合數就從 24 跳到 32、fit 次數從 120 跳到 160在網格裡加候選值是乘法,不是加法,這正是第 05 節「組合爆炸」的預告。

03第二個乘法:每種組合都要走完 5 折

24 種組合排好隊之後,GridSearchCV 對每一種都執行同一套儀式——5 折交叉驗證

一個組合的 5 折之旅(以 178 瓶酒、cv=5 為例):

  第 1 折: 用第 2~5 份共 142 瓶 fit 一次 → 拿第 1 份 36 瓶考試打分
  第 2 折: 用其餘 142 瓶       fit 一次 → 拿第 2 份 36 瓶考試打分
  第 3 折: 用其餘 142 瓶       fit 一次 → 拿第 3 份 36 瓶考試打分
  第 4 折: 用其餘 143 瓶       fit 一次 → 拿第 4 份 35 瓶考試打分
  第 5 折: 用其餘 143 瓶       fit 一次 → 拿第 5 份 35 瓶考試打分

  → 一個組合吃掉 5 次 fit;5 個分數平均,就是它的總成績

重點有三個。第一,每一折都是從零開始的全新訓練——sklearn 每次都先 clone 一台乾淨的模型,絕不沿用上一折學到的東西,所以 5 折就是紮紮實實的 5 次 fit。第二,每次 fit 用的是 142~143 瓶(五分之四),不是全部 178 瓶。第三,24 種組合各自獨立地做完這套,於是:

24 種組合 × 5 折 = 120 次 fit
本站實跑(python3 + scikit-learn 1.8):我們把 RandomForestClassifier 包了一層「每被 fit 一次就計數一次」的外衣再丟進 GridSearchCV——計數器最後停在 120,跟 verbose 印出的 totalling 120 fits 一致。單執行緒跑了 47.5 秒;順帶一提,這 120 次 fit 裡森林總共種出了 28,000 棵決策樹(每個 n_estimators 值各出現在 8 個組合 × 5 折裡:5 × 8 × (100+200+400))。
別把「fit 次數」跟「樹的棵數」搞混。一次 RandomForest 的 fit,內部會種出 n_estimators 棵樹——但那是一次 fit 裡面的事。題目問的是「模型 fit 幾次」,數的是 .fit() 被呼叫幾次,不是森林裡有幾棵樹。
互動實驗室:五折之旅親眼看 120 次 fit 一次一次發生
第幾個組合#1
這個組合的第幾折第 1 折
這次 fit 拿去訓練這折留下來考試一格=一瓶酒,共 178 格
整場搜尋的第幾次 fit
1
共 120 次
這次 fit 的訓練筆數
142
考題 36 瓶
進度
0.8%
120 次裡完成的比例

04refit 那一行:到底要不要「加一」?

交叉驗證做完,GridSearchCV 手上只有一張成績單——它知道哪一組參數平均分最高,但還沒有任何一台「用最佳參數訓練好、可以直接拿去用」的模型。因為剛剛那 120 台模型,每一台都只看過五分之四的資料,而且考完就丟了。

這就是 refit 存在的理由:

refit=True(預設)refit=False(本題)
搜尋結束後自動用最佳參數+全部 178 瓶再訓練一次就地解散,只留成績單
總 fit 次數120 + 1 = 121120
多的那一次算交叉驗證嗎不算——它用全部資料、也不打分數
本站實跑:同一段程式只把 refit 改成 True,計數器停在 121;攔截到的最後一次 fit 用了 178 瓶(全部資料),參數正是成績單上的最佳組合。改回 False,計數器停在 120。題目那句「本題不計搜尋完成後另行重訓,因此設定 refit=False」,就是出題者在幫你把這個「+1」明確排除掉。

refit=False 的代價:有些東西會拿不到

沒有那最後一次重訓,就沒有「訓練好的最佳模型」。本站把每個屬性實際摸過一遍:

搜尋完之後想拿⋯⋯refit=False(本題)refit=True
cv_results_ 完整成績單拿得到實跑確認拿得到
best_params_ 最佳參數拿得到(單一 scoring 時)實跑確認拿得到
best_score_ 最佳平均分拿得到(單一 scoring 時)實跑確認拿得到
best_estimator_ 訓練好的最佳模型AttributeError實跑確認拿得到
search.predict() / search.score()AttributeError實跑確認可以直接用
實跑的錯誤訊息長這樣(值得看一眼,考題很愛考):

search.predict(X_wine)
→ AttributeError: This 'GridSearchCV' has no attribute 'predict'

search.score(X_wine, y_wine)
→ AttributeError: This GridSearchCV instance was initialized with
  `refit=False`. score is available only after refitting ...
一個容易記錯的細節:refit=False 時 best_params_best_score_ 仍然拿得到(因為它們只是「讀成績單」,不需要訓練好的模型)——但這只限 scoring 只有一個指標的情況。如果 scoring 傳了多個指標(例如同時看 accuracy 和 f1),sklearn 不知道該用哪個指標定義「最佳」,此時 refit 必須明確指定指標名稱,否則連 best_params_ 都沒有。
互動實驗室:refit 開關切切看,多的那一次 fit 從哪裡冒出來
把 refit 設成:
交叉驗證的 fit
120
24 組合 × 5 折
搜尋後的重訓
0
refit=False:不重訓
總 fit 次數
120
本題問的數字

05通用公式,以及網格為什麼會爆炸

把前面兩節收成一條公式,以後看到同型題直接套:

\[ \text{總 fit 次數} \;=\; \underbrace{\Big(\prod_{i} n_i\Big)}_{\text{各參數候選數連乘}} \times \; k \;+\; \underbrace{\big[\,\text{refit=True 則加 } 1\,\big]}_{\text{用全部資料重訓}} \]

其中 \(n_i\) 是第 \(i\) 個參數的候選值個數、\(k\) 是交叉驗證折數。本題:\(3 \times 4 \times 2 \times 5 + 0 = 120\)。

公式裡最兇的是那個連乘。每多一個參數就多乘一個數字、每多一個候選值就把其中一個數字加一——總次數是指數式長大的。本題的網格 24 種組合很秀氣;若五個參數各放 6 個候選值,就是 65 = 7,776 種組合、cv=5 之下 38,880 次 fit。這叫組合爆炸
互動實驗室:搜尋成本計算機拖拖看,網格多快變成災難
參數 1 候選數3
參數 2 候選數4
參數 3 候選數2
cv 折數5
refit
每次 fit 平均秒數0.40s
組合數
24
候選值連乘
總 fit 次數
120
× 折數(refit=False)
預估耗時
48 秒
次數 × 每次秒數
GridSearchCV120 次
RandomizedSearchCV
n_iter=10
50 次
n_iter(隨機抽幾組)10

次數太多怎麼辦:三條路

做法fit 次數特性
GridSearchCV組合數 × k(+refit)地毯式全搜,保證試過菜單上每一格;網格一大就爆炸
RandomizedSearchCVn_iter × k(+refit)——跟網格大小無關從菜單裡隨機抽 n_iter 組來試;預算固定、可搜連續分布,大網格的首選
HalvingGridSearchCV介於中間(逐輪淘汰)先用少量資料讓全部組合初賽,贏家才能用更多資料複賽;sklearn 的逐次減半搜尋
n_jobs=-1次數完全不變只是「同時開好幾個爐子」——平行化改變的是等待時間,不是 fit 次數
本站實跑:把本題改成 RandomizedSearchCV(..., n_iter=10, cv=5, refit=False),verbose 印出 Fitting 5 folds for each of 10 candidates, totalling 50 fits,計數器同樣停在 50——它只抽了 24 種組合裡的 10 種。另外,本題的網格開 n_jobs=-1 全核心平行後,還是 120 次 fit,但時間從 47.5 秒縮到 27.2 秒

相關名詞:RandomizedSearchCVHalvingGridSearchCVn_jobs組合爆炸

06實跑彩蛋:24 個組合,其實只有 12 種模型

算完次數,本站把這場搜尋真的跑完,結果成績單藏了一個超展開——先看兩個換算:

Wine 有 13 個特徵:

max_features="sqrt" → 取 √13 ≈ 3.61 → 無條件捨去 → 每次分岔抽 3 個特徵
max_features="log2" → 取 log₂13 ≈ 3.70 → 無條件捨去 → 每次分岔抽 3 個特徵
                                                              ↑
                                  兩個候選值,算出來是同一個數字

也就是說,在 13 個特徵的資料上,"sqrt""log2"同一件事的兩種寫法。加上 random_state=42 固定了抽籤順序,每一對「只差 max_features」的組合訓練出來的森林一模一樣——本站實跑驗證:兩邊在驗證折上的預測一瓶都不差。24 個組合,實際上只有 12 種不同的模型

成績單攤開更妙(本站實跑 cv_results_ 全 24 列):整張成績單只出現兩種平均分——
n_estimators=100 的 8 個組合全是 0.9721(並列第 1 名);n_estimators=200 和 400 的 16 個組合全是 0.9665(並列第 9 名)。
max_depth 的四個候選(None / 5 / 10 / 20)在這份資料上完全沒有影響——因為樹自然只長到 3~7 層(實測平均 4.9 層),上限根本沒被碰到;我們把四種深度分別訓練後對答案,驗證折上的預測同樣一瓶不差。於是 120 次 fit,實際上只比較出「100 棵樹 vs 200/400 棵樹」這一件事,而兩者的差距只是其中一折多對或少對一瓶酒(0.9722 vs 0.9444),完全在雜訊範圍內。

這個彩蛋有三個實戰教訓:

  1. GridSearchCV 不會幫你去重。它老實得可怕:你開 24 格它就跑 24 格,即使其中 12 格跟另外 12 格完全相同——fit 次數照算 120 次,一次都不會少。設計網格前先想清楚「這些候選值真的會做出不一樣的模型嗎」。
  2. 並列第一時,best_params_ 只回報一個。8 個組合同分並列 rank 1,sklearn 取成績單順序最前面的那個(本題是 {'max_depth': None, 'max_features': 'sqrt', 'n_estimators': 100})。看到 best_params_ 別急著寫報告說「最佳深度是 None」——先翻 rank_test_score 看看有幾個並列。
  3. 178 筆的小資料,分數差 0.006 沒有意義。那只是一折裡的一瓶酒。要比較超參數,資料量和折數都得夠,或改看分數的標準差(本題 0.018~0.021,比差距本身還大三倍)。

相關名詞:max_featuresmax_depthbest_params_cv_results_

07四個選項逐一拆解

四個數字,各自對應一條「乘法走歪」的路。把歪路認出來,下次看到變形題也不會踩。

A24 次只做了第一個乘法

24 是候選組合數(3 × 4 × 2),也是成績單 cv_results_ 的列數——但它不是 fit 次數。每一個組合都還要在 5 折上各訓練一次,選 A 等於以為「每種組合只試了一口」。verbose 的原話已經把兩個數字分開講了:24 candidates120 fits

B60 次漏數了一個參數

60 = 3 × 4 × 5——把 max_features 那個「× 2」弄丟了。最常見的失手方式有兩種:只顧著看數字候選、忽略 ["sqrt", "log2"] 這種字串候選也要算;或是把第 7 行的 None 當成「沒有值」漏數(那樣會得到 3 × 3 × 2 × 5 = 90,連選項都對不上)。param_grid 有幾個鍵,就要乘幾個數字,一個都不能少。

C120 次正確

兩個乘法都做齊:組合數 3 × 4 × 2 = 24,再乘折數 24 × 5 = 120refit=False,所以沒有搜尋後的加碼重訓,就停在 120。本站實跑雙重驗證:verbose 印出 totalling 120 fits,計數器攔截到的 fit 也是 120 次,每次用 142~143 瓶訓練。

D240 次多乘了一個 2

240 = 120 × 2,通常來自兩種誤會。①「每折要訓練一次+驗證一次,所以乘 2」——不對,驗證用的是同一台剛訓練好的模型拿去 predict/score,評分不會再 fit 一次②「refit 會把整套再跑一遍」——也不對,refit=True 只是在最後多 1 次(用全部資料訓練最佳組合),是 121,不是 240。

一句話記法:組合數=各參數候選數連乘;fit 數=組合數 × 折數;refit=True 才要 +1,而那一次用的是全部資料。

回到題目:現在再作答一次

看完之後再看一次同一段程式,三個數字應該會自己跳出來。

Wine 分類任務以 RandomForestClassifier 進行網格搜尋,程式如下;本題不計搜尋完成後另行重訓,因此設定 refit=False交叉驗證期間總共會執行多少次模型 fit?

param_grid = {                        # 候選值菜單
    "n_estimators": [100, 200, 400],     # 3 個候選
    "max_depth": [None, 5, 10, 20],       # 4 個候選(None 也算一個)
    "max_features": ["sqrt", "log2"]      # 2 個候選
}                                     # 菜單寫完
search = GridSearchCV(                # 組搜尋機
    RandomForestClassifier(random_state=42),   # 被反覆訓練的模型
    param_grid=param_grid,            # 交出菜單
    cv=5,                             # 每個組合都做 5 折
    refit=False                       # 搜完不重訓
)                                     # 組裝完成
search.fit(X_wine, y_wine)            # 開始跑

08自我檢測

八題,全部都是本題的變形。答錯會直接告訴你錯在哪。

09重點整理

  1. 大括號 { }字典(名字 → 內容的菜單)、中括號 [ ]串列(一排候選值)、小括號 ( )呼叫。param_grid 就是「字典裡包著串列」。
  2. 第 10 到 15 行是同一句話——括號沒關完就能一直換行。到第 15 行為止一次 fit 都沒發生,第 16 行 search.fit() 才按下開關
  3. 組合數=各參數候選數連乘:3 × 4 × 2 = 24。None 是正式候選值要數進去;字串候選("sqrt"、"log2")也要數。
  4. fit 次數=組合數 × 折數:24 × 5 = 120。每一折都是 clone 出來的全新模型從零訓練,驗證(打分數)不會再多一次 fit。
  5. 每次 fit 用的是五分之四的資料(178 瓶 → 142~143 瓶),不是全部;分類問題傳整數給 cv 會自動用 StratifiedKFold 分層(實測各折品種比例幾乎一致)。
  6. refit=True(預設)會在搜尋後用最佳參數+全部資料多訓練 1 次(本題會變 121);refit=False 就停在 120。那 +1 不屬於交叉驗證。
  7. refit=False 拿不到 best_estimator_、不能 search.predict()(實跑:AttributeError);但單一 scoring 時 best_params_best_score_cv_results_ 都還在。多指標 scoring 時 refit 必須指名指標,否則連 best_params_ 都沒有。
  8. RandomizedSearchCV 的次數是 n_iter × 折數,跟網格大小無關(實跑 n_iter=10、cv=5 → 50 次);n_jobs=-1 平行化不改變次數,只縮短時間(47.5 秒 → 27.2 秒)。
  9. 網格是乘法:多一個候選值就整排放大(本題加一個 n_estimators 值 → 120 變 160)。大網格先用 RandomizedSearchCV 圈範圍,再開小網格精修。
  10. 別把 fit 次數跟樹的棵數搞混:一次隨機森林 fit 內部會種 n_estimators 棵樹(本題 120 次 fit 共種了 28,000 棵),但題目數的是 .fit() 的呼叫次數。
  11. 實跑彩蛋:13 個特徵時 "sqrt""log2" 都換算成 3,24 個組合其實只有 12 種模型;GridSearchCV 不會去重,120 次照跑。設計網格前先確認候選值真的會改變模型。
  12. 正確答案 C3 × 4 × 2 = 24 種組合,× 5 折 = 120 次 fit;refit=False 不加一。verbose 原話:Fitting 5 folds for each of 24 candidates, totalling 120 fits
完整程式碼

這是程式逐行解析 35 題中的 1 題

每一題都是:題目 → 程式逐行拆解 → 實跑數字對帳 → 互動實驗室 → 觀念延伸。完整版還有:

💳 信用卡直接購買🛒 前往蝦皮賣場 📖 看完整介紹 🗺️ 回學習順序總覽