Asosiy qismga o'tish

Hyperparameter tuning

24-bob 13 kod bloki 3220 so'z

Bu bobdagi kod shu fayllar bilan ishlaydio'z kompyuteringizda ishga tushiring

Sahna: sukut sozlamalari yaxshimi?

Hozirgacha biz modellarni sukut bo'yicha ishlatdik: RandomForestClassifier(), SVC(), XGBClassifier().

Lekin ularning ichida o'nlab sozlama bor. RandomForest da max_depth, min_samples_leaf, max_features. SVC da C va gamma. XGBoost da yigirmadan ko'p.

Savol: sukut qiymatlar yaxshimi? Va sozlash qancha foyda beradi?

Javob ko'pchilik o'ylaganidan kamroq — buni o'lchab ko'rsataman.

Protokoldagi joyi

Bu — 12-qadam dagi variantlardan biri: "Hyperparameter tuning — model yaxshi, lekin sozlanmagan".

Diqqat: u oxirgi variantlardan. 10-bobda aytganmiz — xato tahlili qilmasdan tuning ga o'tish eng ko'p uchraydigan vaqt yo'qotish.

Parametr va hyperparametr

1-bobdan eslatma, chunki bu yerda hayotiy:

python8 qator
import pandas as pd
import numpy as np
show(pd.DataFrame([
    {"": "Parametr (weight)", "Kim topadi": "MODEL — fit() paytida",
     "Misol": "Chiziq koeffitsiyenti, daraxt chegarasi"},
    {"": "Hyperparametr", "Kim topadi": "SIZ — fit() dan OLDIN",
     "Misol": "max_depth, C, learning_rate, n_neighbors"},
]))
Kim topadiMisol
Parametr (weight)MODEL — fit() paytidaChiziq koeffitsiyenti, daraxt chegarasi
HyperparametrSIZ — fit() dan OLDINmax_depth, C, learning_rate, n_neighbors

Sukut qiymatlar qanchalik yaxshi

Avval bazani o'lchaymiz — sozlanmagan modellar.

python33 qator
from sklearn.model_selection import train_test_split, cross_val_score, GridSearchCV, RandomizedSearchCV
from sklearn.pipeline import Pipeline
from sklearn.compose import ColumnTransformer
from sklearn.preprocessing import StandardScaler, OneHotEncoder
from sklearn.impute import SimpleImputer
from sklearn.ensemble import RandomForestClassifier
from sklearn.linear_model import LogisticRegression
from sklearn.svm import SVC

df = pd.read_csv(DATA / "uy_ijara.csv")
son = ["xonalar", "maydon_m2", "qavat", "qavatlar_soni", "metro_km", "mebel", "qurilgan_yil"]
matn = ["tuman"]
X, y = df[son + matn], df["ijaraga_berildi_30k"]
X_tr, X_te, y_tr, y_te = train_test_split(X, y, test_size=0.3, random_state=42, stratify=y)

def quvur(model):
    return Pipeline([
        ("t", ColumnTransformer([
            ("son", Pipeline([("i", SimpleImputer(strategy="median")), ("sc", StandardScaler())]), son),
            ("matn", OneHotEncoder(handle_unknown="ignore"), matn),
        ])),
        ("m", model),
    ])

print(f"{'model (sukut sozlamalar)':<34} {'CV F1':>9} {'±':>8}")
print("-" * 54)
sukut = {}
for nom, m in [("LogisticRegression", LogisticRegression(max_iter=3000)),
               ("RandomForest", RandomForestClassifier(random_state=42)),
               ("SVC", SVC(random_state=42))]:
    b = cross_val_score(quvur(m), X, y, cv=5, scoring="f1")
    sukut[nom] = b
    print(f"{nom:<34} {b.mean():>9.4f} {b.std():>8.4f}")
Chiqish
model (sukut sozlamalar)               CV F1        ±
------------------------------------------------------
LogisticRegression                    0.7273   0.0118
RandomForest                          0.7032   0.0145
SVC                                   0.7263   0.0134

GridSearchCV: hamma kombinatsiyani sinash

python26 qator
import time

panjara = {
    "m__n_estimators": [100, 300],
    "m__max_depth": [None, 6, 12],
    "m__min_samples_leaf": [1, 5, 20],
    "m__max_features": ["sqrt", 0.5],
}
jami = 1
for v in panjara.values():
    jami *= len(v)
print(f"Kombinatsiyalar soni : {jami}")
print(f"CV fold lari         : 5")
print(f"Jami o'qitish        : {jami * 5} marta")

t0 = time.perf_counter()
gs = GridSearchCV(quvur(RandomForestClassifier(random_state=42)), panjara,
                  cv=5, scoring="f1", n_jobs=-1)
gs.fit(X_tr, y_tr)
gs_vaqt = time.perf_counter() - t0
print()
print(f"Vaqt                 : {gs_vaqt:.1f}s")
print(f"Eng yaxshi CV F1     : {gs.best_score_:.4f}")
print("Eng yaxshi sozlamalar:")
for k, v in gs.best_params_.items():
    print(f"  {k.replace('m__', ''):20s} = {v}")
Chiqish
Kombinatsiyalar soni : 36
CV fold lari         : 5
Jami o'qitish        : 180 marta

Vaqt                 : 4.1s
Eng yaxshi CV F1     : 0.6997
Eng yaxshi sozlamalar:
  max_depth            = 6
  max_features         = sqrt
  min_samples_leaf     = 20
  n_estimators         = 100
python9 qator
print(f"Sukut sozlamalar CV F1 : {sukut['RandomForest'].mean():.4f} ±{sukut['RandomForest'].std():.4f}")
print(f"Sozlangandan keyin     : {gs.best_score_:.4f}")
print(f"Tuning dan foyda       : {gs.best_score_ - sukut['RandomForest'].mean():+.4f}")
print(f"CV tarqalishi          : ±{sukut['RandomForest'].std():.4f}")
print()
foyda = gs.best_score_ - sukut["RandomForest"].mean()
print("Hukm:", "foyda tarqalishdan KATTA — tuning arzigulik"
      if foyda > sukut["RandomForest"].std()
      else "foyda tarqalish ICHIDA — tuning deyarli hech narsa bermadi")
Chiqish
Sukut sozlamalar CV F1 : 0.7032 ±0.0145
Sozlangandan keyin     : 0.6997
Tuning dan foyda       : -0.0034
CV tarqalishi          : ±0.0145

Hukm: foyda tarqalish ICHIDA — tuning deyarli hech narsa bermadi

Tuning dan kutiladigan foyda — odatda kichik

Talabalar GridSearchCV ga katta umid bog'laydi. Amalda u odatda 1-3% beradi, ba'zan esa hech narsa.

Taqqoslash uchun tartib:

  1. Ma'lumot sifati — eng katta ta'sir (4-bob, 43-bob mashqi)
  2. To'g'ri metrika va chegara — katta ta'sir (7, 8-bob)
  3. Yangi xususiyat yoki yangi manba — o'rtacha (22-bob)
  4. Model turini almashtirish — o'rtacha
  5. Hyperparameter tuning — eng kichik

Shuning uchun tuning oxirida qilinadi. Avval ma'lumotni tuzatasiz.

Eng yaxshi natijalar jadvali

python13 qator
natijalar = pd.DataFrame(gs.cv_results_)
ustunlar = ["param_m__n_estimators", "param_m__max_depth",
            "param_m__min_samples_leaf", "param_m__max_features",
            "mean_test_score", "std_test_score"]
eng_yaxshi = natijalar[ustunlar].sort_values("mean_test_score", ascending=False).head(8).copy()
eng_yaxshi.columns = ["n_est", "depth", "min_leaf", "max_feat", "CV F1", "±"]
eng_yaxshi["CV F1"] = eng_yaxshi["CV F1"].round(4)
eng_yaxshi["±"] = eng_yaxshi["±"].round(4)
show(eng_yaxshi)
print()
eng = natijalar.mean_test_score.max()
yaqin = int((natijalar.mean_test_score > eng - natijalar.std_test_score.max()).sum())
print(f"Eng baland balldan tarqalish ichida bo'lgan kombinatsiyalar: {yaqin} / {len(natijalar)}")
Chiqish
Eng baland balldan tarqalish ichida bo'lgan kombinatsiyalar: 36 / 36
n_estdepthmin_leafmax_featCV F1±
100620sqrt0.70.014
10061sqrt0.70.024
1001220sqrt0.6980.013
100None20sqrt0.6980.013
30061sqrt0.6980.023
30065sqrt0.6980.025
10065sqrt0.6970.025
300620sqrt0.6960.013

Oxirgi qatorga diqqat qiling, chunki natija keskin: 36 kombinatsiyadan 36 tasi eng baland balldan bir tarqalish ichida.

Ya'ni bu panjarada birorta sozlama ham boshqasidan ishonchli yaxshi emas. GridSearchCV ularning birini "eng yaxshi" deb tanladi, lekin bu tanlov amalda tasodif.

Shuning uchun best_params_ ni muqaddas deb bilmang. Uning yonidagi std_test_score ustuniga qarang: agar farqlar tarqalish ichida bo'lsa, siz eng oddiy yoki eng tez sozlamani tanlashingiz mumkin — natija bir xil bo'ladi.

RandomizedSearchCV: tasodifiy, lekin tezroq

Panjara kattalashganda GridSearchCV portlaydi. Yechim: hamma kombinatsiyani emas, tasodifiy N tasini sinash.

python22 qator
from scipy.stats import randint, uniform

katta_panjara = {
    "m__n_estimators": randint(50, 500),
    "m__max_depth": [None, 4, 6, 8, 12, 20],
    "m__min_samples_leaf": randint(1, 40),
    "m__min_samples_split": randint(2, 40),
    "m__max_features": ["sqrt", "log2", 0.3, 0.5, 0.8],
}
print("Agar GridSearch bilan qilsak (taqriban):")
print(f"  450 x 6 x 39 x 38 x 5 = {450*6*39*38*5:,} kombinatsiya".replace(",", " "))
print("  -> amalda imkonsiz")
print()
t0 = time.perf_counter()
rs = RandomizedSearchCV(quvur(RandomForestClassifier(random_state=42)), katta_panjara,
                        n_iter=40, cv=5, scoring="f1", random_state=42, n_jobs=-1)
rs.fit(X_tr, y_tr)
rs_vaqt = time.perf_counter() - t0
print(f"RandomizedSearchCV (40 urinish): {rs_vaqt:.1f}s")
print(f"  eng yaxshi CV F1: {rs.best_score_:.4f}")
print(f"GridSearchCV ({jami} kombinatsiya): {gs_vaqt:.1f}s")
print(f"  eng yaxshi CV F1: {gs.best_score_:.4f}")
Chiqish
Agar GridSearch bilan qilsak (taqriban):
  450 x 6 x 39 x 38 x 5 = 20 007 000 kombinatsiya
  -> amalda imkonsiz

RandomizedSearchCV (40 urinish): 5.3s
  eng yaxshi CV F1: 0.6985
GridSearchCV (36 kombinatsiya): 4.1s
  eng yaxshi CV F1: 0.6997

Amalda RandomizedSearchCV dan boshlang

Sababi: ko'p hyperparametr natijaga deyarli ta'sir qilmaydi. Grid search o'sha ahamiyatsiz parametrlarga ham teng vaqt sarflaydi.

Random search esa muhim parametrning ko'proq xil qiymatini ko'rib chiqadi. Shuning uchun bir xil vaqt ichida ko'pincha yaxshiroq natija topadi.

Tartib: RandomizedSearchCV bilan keng qidiring -> eng yaxshi zonani toping -> o'sha zonada kichik GridSearchCV.

HalvingGridSearchCV: tezroq variant

python13 qator
from sklearn.experimental import enable_halving_search_cv  # noqa: F401
from sklearn.model_selection import HalvingGridSearchCV

t0 = time.perf_counter()
hs = HalvingGridSearchCV(quvur(RandomForestClassifier(random_state=42)), panjara,
                         cv=5, scoring="f1", random_state=42, n_jobs=-1, factor=3)
hs.fit(X_tr, y_tr)
hs_vaqt = time.perf_counter() - t0
print(f"{'usul':<24} {'vaqt':>8} {'eng yaxshi CV F1':>18}")
print("-" * 52)
print(f"{'GridSearchCV':<24} {gs_vaqt:>7.1f}s {gs.best_score_:>18.4f}")
print(f"{'RandomizedSearchCV':<24} {rs_vaqt:>7.1f}s {rs.best_score_:>18.4f}")
print(f"{'HalvingGridSearchCV':<24} {hs_vaqt:>7.1f}s {hs.best_score_:>18.4f}")
Chiqish
usul                         vaqt   eng yaxshi CV F1
----------------------------------------------------
GridSearchCV                 4.1s             0.6997
RandomizedSearchCV           5.3s             0.6985
HalvingGridSearchCV          3.8s             0.6948

Halving g'oyasi: birinchi bosqichda hamma kombinatsiyani kichik ma'lumotda sinaydi, yomonlarini tashlaydi, qolganlarini kattaroq ma'lumotda sinaydi. Sport turniri kabi.

Eng muhim xato: test to'plamida tuning

python16 qator
print("NOTO'G'RI yo'l — eng yaxshi sozlamani TEST da tanlash:")
from sklearn.metrics import f1_score
eng_test, eng_sozlama = -1, None
for d in [None, 4, 6, 8, 12, 20]:
    for msl in [1, 5, 10, 20, 40]:
        m = quvur(RandomForestClassifier(max_depth=d, min_samples_leaf=msl, random_state=42))
        m.fit(X_tr, y_tr)
        ball = f1_score(y_te, m.predict(X_te))
        if ball > eng_test:
            eng_test, eng_sozlama = ball, (d, msl)
print(f"  30 kombinatsiya TEST da sinaldi")
print(f"  eng yaxshi test F1: {eng_test:.4f}  (max_depth={eng_sozlama[0]}, min_samples_leaf={eng_sozlama[1]})")
print()
print("TO'G'RI yo'l — CV da tanlash, keyin test da BIR MARTA tekshirish:")
print(f"  CV da tanlangan eng yaxshi: {gs.best_score_:.4f}")
print(f"  o'sha model test da       : {f1_score(y_te, gs.best_estimator_.predict(X_te)):.4f}")
Chiqish
NOTO'G'RI yo'l — eng yaxshi sozlamani TEST da tanlash:
  30 kombinatsiya TEST da sinaldi
  eng yaxshi test F1: 0.7958  (max_depth=None, min_samples_leaf=40)

TO'G'RI yo'l — CV da tanlash, keyin test da BIR MARTA tekshirish:
  CV da tanlangan eng yaxshi: 0.6997
  o'sha model test da       : 0.7842

30 marta test ga qarasangiz, test endi test emas

Yuqoridagi birinchi usulda men 30 ta sozlamani test to'plamida sinab, eng yaxshisini tanladim.

Natija chiroyli chiqadi — lekin u yolg'on. Men test to'plamiga 30 marta qaradim va unga moslashgan sozlamani tanladim. Bu — 5-bobdagi leakage ning boshqa shakli.

Haqiqiy loyihada bu shunday ko'rinadi: modelni chiqarasiz, serverda natija kutganingizdan past bo'ladi, va nega ekanini tushunmaysiz.

Qoida: sozlama CV da tanlanadi, test faqat yakuniy hisobot uchun bir marta ochiladi.

Nested CV: eng halol baho

Agar siz "tuning dan keyin modelim qanchalik yaxshi?" degan savolga xolis javob bermoqchi bo'lsangiz, bitta CV yetarli emas — chunki u sozlama tanlashda ishlatilgan.

python15 qator
from sklearn.model_selection import KFold, cross_val_score as cvs

kichik_panjara = {"m__max_depth": [None, 6, 12], "m__min_samples_leaf": [1, 5, 20]}

# oddiy CV — sozlama tanlash uchun ishlatilgan CV ning o'zi
oddiy = GridSearchCV(quvur(RandomForestClassifier(random_state=42)), kichik_panjara,
                     cv=5, scoring="f1", n_jobs=-1).fit(X, y)
print(f"GridSearchCV ning best_score_ (optimistik): {oddiy.best_score_:.4f}")

# nested CV — tashqi halqa sozlash jarayonini BUTUNLAY ko'rmaydi
ichki = GridSearchCV(quvur(RandomForestClassifier(random_state=42)), kichik_panjara,
                     cv=3, scoring="f1", n_jobs=-1)
nested = cvs(ichki, X, y, cv=KFold(5, shuffle=True, random_state=42), scoring="f1")
print(f"Nested CV (xolis baho)                   : {nested.mean():.4f} ±{nested.std():.4f}")
print(f"Farq                                     : {oddiy.best_score_ - nested.mean():+.4f}")
Chiqish
GridSearchCV ning best_score_ (optimistik): 0.7392
Nested CV (xolis baho)                   : 0.7245 ±0.0219
Farq                                     : +0.0147

best_score_ har doim bir oz optimistik bo'ladi, chunki u eng yaxshi kombinatsiyani o'sha CV da tanlagan. Nested CV esa sozlash jarayonini ham sinovdan o'tkazadi.

Nested CV qachon kerak

U qimmat (CV ichida CV). Shuning uchun:

  • Maqola yoki ilmiy ish yozsangiz — nested CV majburiy
  • Ishlab chiqarish loyihasida — oddiy usul yetarli: CV da sozlang, keyin alohida test to'plamida bir marta tekshiring

Ikkinchi yo'l amalda keng tarqalgan va yetarlicha halol.

Qaysi parametrlarni sozlash kerak

Hamma parametrni sozlash — vaqtni yo'qotish. Modelga qarab muhim 2-4 tasi bor.

python9 qator
show(pd.DataFrame([
    {"Model": "LogisticRegression", "Muhim": "C, penalty", "Sozlamaslik mumkin": "solver, max_iter"},
    {"Model": "RandomForest", "Muhim": "max_depth, min_samples_leaf, max_features",
     "Sozlamaslik mumkin": "n_estimators (katta qo'y, 18-bob)"},
    {"Model": "XGBoost / LightGBM", "Muhim": "learning_rate, max_depth, subsample, colsample",
     "Sozlamaslik mumkin": "n_estimators (early_stopping, 19-bob)"},
    {"Model": "SVC", "Muhim": "C, gamma (BIRGA — 21-bob)", "Sozlamaslik mumkin": "kernel (rbf dan boshlang)"},
    {"Model": "KNN", "Muhim": "n_neighbors, weights", "Sozlamaslik mumkin": "algorithm"},
]))
ModelMuhimSozlamaslik mumkin
LogisticRegressionC, penaltysolver, max_iter
RandomForestmax_depth, min_samples_leaf, max_featuresn_estimators (katta qo'y, 18-bob)
XGBoost / LightGBMlearning_rate, max_depth, subsample, colsamplen_estimators (early_stopping, 19-bob)
SVCC, gamma (BIRGA — 21-bob)kernel (rbf dan boshlang)
KNNn_neighbors, weightsalgorithm
python7 qator
print("Logistic regression uchun C ni sozlash:")
gs_lr = GridSearchCV(quvur(LogisticRegression(max_iter=3000)),
                     {"m__C": [0.001, 0.01, 0.1, 1, 10, 100]},
                     cv=5, scoring="f1", n_jobs=-1).fit(X, y)
print(f"  sukut (C=1)      : {sukut['LogisticRegression'].mean():.4f}")
print(f"  eng yaxshi C={gs_lr.best_params_['m__C']:<6}: {gs_lr.best_score_:.4f}")
print(f"  foyda            : {gs_lr.best_score_ - sukut['LogisticRegression'].mean():+.4f}")
Chiqish
Logistic regression uchun C ni sozlash:
  sukut (C=1)      : 0.7273
  eng yaxshi C=0.001 : 0.7315
  foyda            : +0.0042

Yakuniy taqqoslash

python12 qator
yakuniy = pd.DataFrame([
    {"Model": "LogisticRegression", "Sukut": round(sukut["LogisticRegression"].mean(), 4),
     "Sozlangan": round(gs_lr.best_score_, 4),
     "Foyda": round(gs_lr.best_score_ - sukut["LogisticRegression"].mean(), 4)},
    {"Model": "RandomForest", "Sukut": round(sukut["RandomForest"].mean(), 4),
     "Sozlangan": round(gs.best_score_, 4),
     "Foyda": round(gs.best_score_ - sukut["RandomForest"].mean(), 4)},
])
show(yakuniy)
print()
print(f"Eng katta CV tarqalishi: ±{max(b.std() for b in sukut.values()):.4f}")
print("Foydalarni shu tarqalish bilan solishtiring (9-bob qoidasi).")
Chiqish
Eng katta CV tarqalishi: ±0.0145
Foydalarni shu tarqalish bilan solishtiring (9-bob qoidasi).
ModelSukutSozlanganFoyda
LogisticRegression0.7270.7320.004
RandomForest0.7030.7-0.003

Bugun nima o'rgandik

python10 qator
show(pd.DataFrame([
    {"Tushuncha": "Hyperparametr", "Bitta jumla": "fit() dan OLDIN siz qo'yadigan sozlama"},
    {"Tushuncha": "GridSearchCV", "Bitta jumla": "Hamma kombinatsiya — kichik panjara uchun"},
    {"Tushuncha": "RandomizedSearchCV", "Bitta jumla": "Tasodifiy N ta — katta panjara uchun, amalda birinchi tanlov"},
    {"Tushuncha": "HalvingGridSearchCV", "Bitta jumla": "Turnir usuli — yomonlarini erta tashlaydi"},
    {"Tushuncha": "Tuning dan foyda", "Bitta jumla": "Odatda 1-3%, ba'zan nol — oxirida qiling"},
    {"Tushuncha": "Eng katta xato", "Bitta jumla": "Sozlamani TEST da tanlash — leakage"},
    {"Tushuncha": "best_score_", "Bitta jumla": "Bir oz optimistik — nested CV xolisroq"},
    {"Tushuncha": "Nechta parametr", "Bitta jumla": "Har model uchun muhim 2-4 tasi bor, qolgani vaqt yo'qotish"},
]))
TushunchaBitta jumla
Hyperparametrfit() dan OLDIN siz qo'yadigan sozlama
GridSearchCVHamma kombinatsiya — kichik panjara uchun
RandomizedSearchCVTasodifiy N ta — katta panjara uchun, amalda birinchi tanlov
HalvingGridSearchCVTurnir usuli — yomonlarini erta tashlaydi
Tuning dan foydaOdatda 1-3%, ba'zan nol — oxirida qiling
Eng katta xatoSozlamani TEST da tanlash — leakage
best_score_Bir oz optimistik — nested CV xolisroq
Nechta parametrHar model uchun muhim 2-4 tasi bor, qolgani vaqt yo'qotish

Keyingi bobda: bitta model o'rniga bir nechtasini birlashtiramiz.

Bu bobni bepul o'qidingiz.

Kitob o'rgatadi, lekin sizning kodingiz nega ishlamaganini ayta olmaydi. Kursda uy vazifangiz tekshiriladi.

Telegramda yozish Narxlar