跳至主要內容

    調參與公平比較:如何選出值得部署的模型?

    把搜尋空間、交叉驗證與最終測試分開,分數才有比較意義

    sunny 桑尼· · 21 MIN READ · 160 次瀏覽

    調參與公平比較:如何選出值得部署的模型?
    本文目錄

    模型A有0.91、模型B有0.92,該換成B嗎?如果A只試一次,B試了兩百組參數,而且0.92來自挑中B的那份資料,這兩個數字就還不是公平的部署依據。

    學過基準模型與交叉驗證後,調參的下一步是讓比較條件可檢查:使用相同的資料切分與指標,記錄搜尋了什麼、花了多少資源,並保留尚未參與選擇的評估。這篇用一個小型SVM搜尋把流程跑完;原始配圖則補充Nested CV及逐次淘汰的判讀。

    超參數是選擇流程的一部分

    線性模型從資料估出的權重是模型參數;由你決定的正則化強度、樹深度、近鄰數等是超參數。調參不是在學習之外免費加上的最後一步,它同樣會根據資料做選擇。

    一個可檢查的安排是先保留test,剩餘資料稱dev。在dev裡用交叉驗證比較候選;選好之後用全部dev重新訓練,再對test做最後評估。

    圖1|Dataset分出Training與Test;參數候選經CV選出Best parameters,再以訓練資料建立Retrained model,與Test一起進行Final evaluation。此圖沒有畫每個fold的前處理,也沒有保證任意切分皆適用。來源:The scikit-learn developers,Cross-validation,BSD-3-Clause;透明底合成白底,內容未改。

    如果每看一次test就改參數、改欄位或換模型,test就逐漸參與了選擇。檔案名稱還叫test,並不會讓它恢復獨立。要再估計新流程的泛化,需要新的未參與資料,或事先設計合適的外層驗證。

    資料切分也必須對應任務。同一客戶反覆出現、同一次事件切成多列、預測未來日期,都可能需要群組或時間切分。以下範例是獨立行的合成資料,才能使用一般分層隨機切分;不能直接把它搬到所有業務資料。

    搜尋不只包含模型,也包含前處理

    縮放、缺失值填補、特徵選擇或target encoding若先用全dev fit,再做交叉驗證,驗證fold就提前影響了處理規則。沒有直接讀取y,也不等於所有前處理都可以在切分外完成。

    圖2|整份資料先保留右側test,左側training內輪替五折的藍色驗證塊,其他綠色為該輪訓練。原圖沒有畫Scaler或Pipeline;本文程式補上fold內前處理。來源同圖1,BSD-3-Clause;透明底合成白底,手機另排完整圖與五折區放大。

    用Pipeline把StandardScaler與SVC接在一起,搜尋器在每個訓練fold上fit縮放器,才把相同規則套用到該次驗證fold。最後refit時,才會在全部dev上重新學縮放規則與分類器。

    這讓比較更接近你真正要使用的流程。它仍不會自動修好錯誤的時間切分,也不會阻止你把答案欄位放進X;工具能管理步驟,資料定義仍要由你確認。

    Grid Search列完組合,Random Search抽取候選

    假設C候選為0.1、1、10,gamma候選為0.01、0.1。網格有3×2=6組,每組都做相同5折,候選訓練共6×5=30次;若refit=True,選好之後通常再用全dev fit一次,合計31次。

    若RandomizedSearchCV只抽4組,相同5折就是20次候選fit,再加1次refit。這個例子所有參數都以清單提供,工具會從有限組合中不重複抽樣;若使用機率分布,抽樣規則不同,不能把「不重複」說成所有設定的保證。

    方法這次的小範例做什麼容易忽略什麼
    Grid試完列出的6組列出的區域以外完全沒搜尋
    Random從這6組抽4組可能剛好抽中好組合,也可能錯過
    兩者共同條件相同5折、相同AUC、相同Pipelinefit次數不代表相同秒數或算力

    當很多參數中只有少數很重要,隨機搜尋可以把預算用來探索不同位置,不必把所有維度都排成細密網格。但這是搜尋設計的理由,並非每次random都比grid好。範圍太窄、尺度不合適或根本漏掉重要參數,兩者都可能找到一個有限候選中的好答案,卻錯過真正需要的設定。

    跨多個數量級的正值參數,通常值得考慮log尺度。例如0.001、0.01、0.1、1、10各代表十倍變化;這跟在0到10之間均勻切幾格是不同的假設。抽樣分布本身也是你帶進搜尋的偏好,應記錄而不是藏在「自動調參」四個字裡。

    逐次淘汰:先少量資源,再讓部分候選繼續

    圖3|來源400筆、12特徵合成分類,RandomForest每個模型20棵樹、factor=2,以樣本資源n_samples逐輪增加。圖標資源20/40/80/160/320、候選20/10/5/3/2;資源值不是每個fold實際訓練筆數,train與CV-valid會再按比例取樣;mean test score是CV平均accuracy,不是保留測試集。來源:The scikit-learn developers,Successive Halving Iterations,BSD-3-Clause;原圖未改、未在本站重跑。

    Successive halving先給許多候選較少資源,保留部分表現較好的候選,再增加資源。資源可能是訓練樣本數,也可能是符合介面要求的模型參數,不一定是相同種類的成本。

    官方迭代圖的每條線是一組候選,橫軸同時列出iteration、資源量與候選數。線中途停止,表示該候選未進入下一輪;不是普通Grid Search中的訓練曲線。y軸的mean_test_score是搜尋器的CV驗證分數,不是事先保留的test。

    圖4|來源1,000筆合成SVC、7個gamma乘6個C。左圖格內數字為最後參與iteration,顏色才是當時的平均CV accuracy;右側為完整Grid。1.802與6.462秒只屬來源實驗,並非本站計時或通用加速比。來源:The scikit-learn developers,Comparison between grid search and successive halving,BSD-3-Clause;原圖未改,手機左右兩面板上下排,色標隨右圖保留、兩圖共同使用;兩圖gamma列相同,右圖沿用左圖列刻度。

    另一張官方熱圖比較SVC的42組參數:6個C、7個gamma。左圖格子上的數字是候選最後參與的iteration,顏色才是該候選對應的CV分數。較早淘汰者使用較少資源,其顏色和最後存活者並不是相同資料量下的完整重跑。

    這類策略的風險在於:小資源下看起來不好的候選,可能在更大資源下改善,卻已經被淘汰。圖上的計時也只屬於來源那次資料、參數與硬體,不能拿來承諾你的訓練一定快幾倍。sklearn 1.5系列的halving搜尋仍需明確匯入experimental enable;它不是下方普通GridSearchCV的默認流程。

    最高CV值已經被挑過,不能當沒有選擇的估計

    就算每組參數的CV都有雜訊,挑其中最高的一組,也容易挑到雜訊對它比較有利的結果。候選越多,越值得警惕這個選擇效果。best_score_仍然有用,它表示本次選中候選的平均CV分數;問題在於把這個數字當成沒有參與挑選的最終效能。

    圖5|來源30個trial、Iris RBF SVC、內外各4折。上方紅non-nested/藍nested為accuracy,下方為紅減藍;差值可正可負。它不是本篇自有AUC實跑,也不是Grid與Random抽樣圖。來源:The scikit-learn developers,Nested versus non-nested cross-validation,BSD-3-Clause;原圖未改,本站沒有重跑這組來源實驗。

    Nested CV把搜尋放進內層,外層只評估整個選擇流程。每個外層訓練集合各自做內層搜尋,再對那次外層保留資料評估;不是先用全部資料挑好參數,最後才補畫一個外層框。

    官方Iris例子使用RBF SVC、6組C/gamma、內外各4折,重複30個切分設定。上方比較non-nested與nested的accuracy分數,下方是兩者差值;與本篇自有範例使用的AUC不同。有些個別差值可為負,不能把「存在選擇偏差」說成每次non-nested一定較高。這張來源圖沒有由本站重跑。

    保留test與Nested CV是在安排不同形式的外部評估。沒有必要為了看起來完整,將兩者在所有任務上重複堆疊;但你必須知道目前回報的是選中候選的內層CV,還是選擇流程在未參與資料上的表現。

    實跑:把縮放放進Pipeline,共用五折

    這次用1,000筆合成二元分類資料,800筆dev、200筆test。正負類比例約3:7,目標帶少量隨機翻轉。不是實際客戶成效,也不是不同模型的排行榜。

    Python 3.10.12、NumPy 1.26.3、scikit-learn 1.5.1於WSL實跑。搜尋指標選AUC,直接使用SVC的decision score,不需要先開啟機率估計。兩個搜尋器共用實際存下來的fold索引,減少不同切分干擾;前處理在每個fold內fit。

    將下列程式存成search_comparison.py,在已安裝固定版本的Python環境執行:

    python -m pip install numpy==1.26.3 scikit-learn==1.5.1
    python search_comparison.py
    
    import numpy as np
    import sklearn
    from sklearn.datasets import make_classification
    from sklearn.model_selection import (train_test_split, StratifiedKFold,
                                         GridSearchCV, RandomizedSearchCV)
    from sklearn.pipeline import Pipeline
    from sklearn.preprocessing import StandardScaler
    from sklearn.svm import SVC
    from sklearn.metrics import roc_auc_score, balanced_accuracy_score
    
    X, y = make_classification(n_samples=1000, n_features=10,
                               n_informative=5, n_redundant=2,
                               weights=[0.7, 0.3], flip_y=0.03,
                               random_state=42)
    X_dev, X_test, y_dev, y_test = train_test_split(
        X, y, test_size=0.2, stratify=y, random_state=42)
    folds = list(StratifiedKFold(n_splits=5, shuffle=True,
                               random_state=42).split(X_dev, y_dev))
    pipe = Pipeline([("scale", StandardScaler()),
                     ("svc", SVC(kernel="rbf"))])
    space = {"svc__C": [0.1, 1.0, 10.0],
             "svc__gamma": [0.01, 0.1]}
    searches = {
        "grid": GridSearchCV(pipe, space, scoring="roc_auc", cv=folds,
                             n_jobs=1, refit=True),
        "random": RandomizedSearchCV(pipe, space, n_iter=4,
                                     scoring="roc_auc", cv=folds,
                                     n_jobs=1, refit=True, random_state=42),
    }
    print(f"numpy={np.__version__} sklearn={sklearn.__version__}")
    print(f"split dev={len(y_dev)} test={len(y_test)} folds={len(folds)}")
    for name, search in searches.items():
        search.fit(X_dev, y_dev)
        print(f"{name} candidates={len(search.cv_results_['params'])} "
              f"CV_fits={len(search.cv_results_['params']) * len(folds)} refit=1")
        for params, score in zip(search.cv_results_["params"],
                                 search.cv_results_["mean_test_score"]):
            print(f"  C={params['svc__C']:.1f} gamma={params['svc__gamma']:.2f} "
                  f"mean_fold_auc={score:.6f}")
        print(f"{name} selected={search.best_params_} "
              f"selected_CV_auc={search.best_score_:.6f}")
    
    # Pick a procedure from dev CV results. Ties use the predeclared grid order.
    chosen = max(searches, key=lambda name: searches[name].best_score_)
    model = searches[chosen].best_estimator_
    test_score = model.decision_function(X_test)
    test_pred = model.predict(X_test)
    print(f"chosen_from_dev={chosen}")
    print(f"final_test_auc={roc_auc_score(y_test, test_score):.6f}")
    print(f"final_test_balanced_accuracy={balanced_accuracy_score(y_test, test_pred):.6f}")
    

    實跑網格6組、30次CV fit加1次refit;隨機4組、20次CV fit加1次refit。這次固定seed的隨機搜尋剛好抽中網格的最佳組合,所以兩者都選C=10、gamma=0.1,平均fold AUC為0.961481。

    搜尋候選數候選CV fitrefit選中CV AUC
    Grid63010.961481
    Random42010.961481

    程式事先規定CV平手選grid,從dev結果選定流程後才回報test:AUC 0.964286、balanced accuracy 0.921429。後者是各類recall的平均,與普通accuracy不是相同指標;此處用SVC原本的分類規則,沒有在test上調閾值。

    這次的結果只能說,在這個小空間、這次固定抽樣,random少試兩組也找到了同一組;不能推出少抽一定夠,也不是random與grid在同算力下的排名。兩個搜尋總共已花52次fit,不應把其中一個流程的21或31次誤寫成整個探索工作的總成本。

    比較也要看資料量與成本怎樣一起變

    圖6|來源Digits、GaussianNB與RBF SVC(gamma=.001),50次ShuffleSplit、test_size=.2、五個訓練量。藍線Training、橘線Test,這裡Test是CV每折保留部分的accuracy;陰影是各次分數的標準差,不是保證區間。來源:The scikit-learn developers,Plotting Learning Curves and Checking Models’ Scalability,BSD-3-Clause;原圖未改,手機依左NB、右SVC順序排成單欄,將原圖左側共用y軸刻度原樣複用到右圖旁,保留圖例與曲線。

    這張圖讓我們把「模型差異」與「資料量差異」分開看。在來源這個例子,SVC的驗證表現隨資料增加而改善,後段趨於平緩;NB的曲線則在另一個範圍靠近。不能只看某個訓練量就推成所有資料規模下的結論,也不能把這個例子的平臺當成未來再收任何資料都沒用。新的樣本可能有不同分布、品質或稀有情況。

    圖7|上排Fit time、下排Score time,左GaussianNB、右SVC;橫軸皆為訓練樣本數,縱軸秒數與尺度各自不同。陰影為來源重複切分的計時平均±標準差,可能延伸到0以下,不表示觀察到負耗時。來源及授權同圖6;手機依左上、右上、左下、右下排四面板,完整保留各面板y刻度,模型名稱與順序由本圖說對應。本站未重跑此計時,不把它當預測單筆延遲或跨硬體速度保證。

    較高分的模型是否值得使用,取決於額外改善與資源成本。這些來源圖分別畫accuracy與秒,不能把Score time誤讀成分類分數,也不能直接拿不同y軸刻度的線高低判斷幾倍快。正式決策仍應在你的資料、硬體與批次大小上量測。

    部署決定還需要分數之外的條件

    選出較高AUC後,仍要檢查模型檔大小、推論延遲、記憶體、未知類別、缺失欄位與錯誤切片。差異很小時,部署成本較低、行為較穩定的方案可能更合適;要用實際量測與任務成本支持這個判斷。

    比較時留下的紀錄為什麼需要

    資料版本、切分索引或規則
    確認雙方真的面對同樣問題
    指標、方向、閾值選擇方式
    AUC、log loss與分類成本不可互換
    搜尋空間、抽樣分布、候選數與seed
    看清模型得到了多少嘗試機會
    CV、最終test與誤差切片
    分清用來挑選與用來評估的證據
    實際時間、資源與推論條件
    fit數目相同也可能成本差很多

    若用sklearn的負損失scorer,例如負RMSE,分數仍採越大越好;回報RMSE時要轉回正值。平均各fold的RMSE與把所有out-of-fold平方誤差合併後再開根,也不一定相同,不能只因名字相近就混用。

    值得部署的模型,必須在可比較的條件下兌現任務需求。把搜尋與評估分開留下證據,才有辦法判斷一次分數進步,是較好的選擇,還是更多次嘗試剛好換來的高點。

    原始來源與配圖

    依據sklearn超參數搜尋、GridSearchCV、RandomizedSearchCV、Nested versus non-nested CV、Successive halving iterations及Grid與halving熱圖原例。1.5系列文件目前頁面標示1.5.2,本篇實跑1.5.1;不將線上頁面版本冒充實測版本。查核日2026-10-31。

    圖1–7皆為The scikit-learn developers的官方原圖,依BSD-3-Clause採用,沒有以Jake教材的程式授權代替其受限圖像條款,也沒有重刊未確認權利的2012論文截圖。Grid/Random的六組算例與實跑程式為本站自有教學設計;來源圖的資料、指標與計時和本站實跑分開。

    封面採圖4完整等比縮放加白邊;手機圖2保留全圖加五折區放大,圖4左右面板改上下排、共同色標隨右圖,圖6兩模型改單欄,原共用y刻度複用於SVC圖旁,圖7四面板按列優先改單欄。皆不重畫資料。圖1–2合成白底;其他桌機圖保留原檔。上述修改不代表來源作者背書。

    另見Cross-validation原文與Learning curves完整原例。

    scikit-learn圖像原始BSD-3-Clause完整聲明

    BSD 3-Clause License
    
    Copyright (c) 2007-2026 The scikit-learn developers.
    All rights reserved.
    
    Redistribution and use in source and binary forms, with or without
    modification, are permitted provided that the following conditions are met:
    
    * Redistributions of source code must retain the above copyright notice, this
      list of conditions and the following disclaimer.
    
    * Redistributions in binary form must reproduce the above copyright notice,
      this list of conditions and the following disclaimer in the documentation
      and/or other materials provided with the distribution.
    
    * Neither the name of the copyright holder nor the names of its
      contributors may be used to endorse or promote products derived from
      this software without specific prior written permission.
    
    THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
    AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
    IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
    DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
    FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
    DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
    SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
    CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
    OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
    OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
    

    留言0

    登入後就能留言。

    登入
    sunny 桑尼
    sunny 桑尼

    AI 資料科學 · 產品經理 · 蒙奇 D 桑尼

    專注於 AI 工具應用與資料科學教育,致力於讓每個人都能用 AI 提升工作效率。曾任職於科技業,現為全職教育創作者。

    讀更多文章 →想自己動手試?領免費 Starter Kit →