IlmHamroh
Data Science va sun'iy intellekt/Daraxtlar va ansambllar13/14-dars20 daqiqa
Mundarija (22)

15.13-dars: Ansambllarni ishlab chiqarishga chiqarish

15-QISM — DARAXTLAR VA ANSAMBLLAR · 13-dars


1. Kirish va motivatsiya

Model CV da 0.85 AUC berdi — bu ishning yarmi. Qolgan yarmi: modelni saqlash, yuklash, bashorat kechikishini o'lchash, hajmini cheklash, driftni kuzatish va qachon qayta o'qitishni hal qilish.

Ansambllar bu yerda maxsus muammolar tug'diradi: 500 daraxtli Random Forest yuzlab megabayt bo'lishi mumkin, gradient boosting bashorati ketma-ket hisoblanadi, va model versiyasi bilan kutubxona versiyasi mos kelmasa — yuklash umuman ishlamaydi.

Bu darsda: joblib bilan saqlash, model hajmini kamaytirish, bashorat kechikishi, Pipeline ni to'liq saqlash, drift ni aniqlash, qayta o'qitish siyosati va nazorat ro'yxati.

Real vaziyat. Tavsiya tizimida 1000 daraxtli Random Forest 340 MB joy egalladi va har so'rov 45 ms oldi — SLA 20 ms edi. min_samples_leaf=20 va n_estimators=300 bilan model 18 MB va 9 ms ga tushdi, AUC esa atigi 0.004 ga pasaydi. Sifatning kichik qismini narxning katta qismiga almashtirish — ishlab chiqarishning odatiy savdosi.

Bu darsda ansambllarni ishlab chiqarishga chiqarishni o'rganamiz.

Bu darsda:

  • Saqlash va yuklash
  • Hajm va kechikish
  • Pipeline ni to'liq saqlash
  • Drift
  • Qayta o'qitish siyosati
  • Nazorat ro'yxati
  • Tuzoqlar
  • Amaliy: to'liq oqim

ℹ Misollar real numpy/pandas/sklearn bilan (Python 3.14).


2. Nazariya — chuqur tushuntirish

2.1. Saqlash va yuklash

python
import joblib

joblib.dump(quvur, "model.joblib", compress=3)
quvur = joblib.load("model.joblib")

MUHIM: pickle/joblib VERSIYAGA bog'liq
  sklearn versiyasi o'zgarsa - ogohlantirish yoki xato
  Python versiyasi o'zgarsa - muammo bo'lishi mumkin

Shuning uchun SAQLANG:
  - sklearn/numpy/python versiyalari
  - o'quv ma'lumotining xesh yoki sanasi
  - belgilar ro'yxati va tartibi
  - o'quv metrikalari

Versiya mosligini yozib qo'ying: joblib.load boshqa sklearn versiyasida InconsistentVersionWarning beradi yoki umuman ishlamaydi. Model faylini metama'lumot bilan birga saqlang.

2.2. Hajm va kechikish

text
Random Forest hajmi ~ n_estimators * o'rtacha tugunlar soni

Kamaytirish:
  1. min_samples_leaf oshirish (eng samarali)
  2. n_estimators kamaytirish (to'yinishdan keyin)
  3. max_depth cheklash
  4. compress=3 (joblib) - 2-4x
  5. Boosting ga o'tish (odatda ancha kichik)

Kechikish:
  RF: daraxtlar PARALLEL (n_jobs) lekin har biri to'liq
  GB: KETMA-KET, lekin daraxtlar sayoz -> ko'pincha tezroq
  Bitta so'rov uchun n_jobs FOYDA BERMAYDI (ustama xarajat)

Bitta so'rov uchun n_jobs=-1 ko'pincha sekinroq: parallellashtirish ustama xarajati foydadan katta. Paketli (batch) bashoratda esa u foydali.

2.3. Pipeline ni to'liq saqlash

python
quvur = Pipeline([("tayyor", ColumnTransformer(...)),
                  ("model", RandomForestClassifier(...))])
quvur.fit(Xtr, ytr)
joblib.dump(quvur, "quvur.joblib")

NEGA butun Pipeline:
  - tayyorlash va model BIRGA versiyalanadi
  - bashoratda bir xil transformatsiya kafolatlanadi
  - leakage xavfi kamayadi (12.9)
  - kirish - asl DataFrame, tayyorlash kerak emas

Faqat modelni saqlash — keng tarqalgan xato: tayyorlash bosqichi (scaler, encoder, imputer) alohida qolib ketadi va ishlab chiqarishda boshqacha qo'llaniladi. Har doim butun Pipeline ni saqlang.

2.4. Drift

text
Ikki xil drift:

1. KIRISH drifti (covariate shift): P(X) o'zgardi
   - belgilar taqsimoti siljidi
   - aniqlash: KS-test, PSI, o'rtacha/kvantil kuzatuvi
   - model hali ishlashi mumkin

2. TUSHUNCHA drifti (concept drift): P(y|X) o'zgardi
   - bog'liqlik o'zgardi
   - aniqlash: faqat haqiqiy yorliq kelganda
   - model ishlamay qoladi

PSI (Population Stability Index):
  < 0.1  - barqaror
  0.1-0.25 - kuzatuv kerak
  > 0.25 - jiddiy siljish

Kirish drifti yorliqsiz aniqlanadi — bu uning asosiy qiymati: yorliqlar kechikib kelsa ham (masalan kredit defolti 12 oydan keyin ma'lum bo'ladi), kirish taqsimotini darhol kuzatish mumkin.

2.5. Qayta o'qitish siyosati

text
Uch yondashuv:

1. JADVAL bo'yicha: har hafta/oy
   + sodda, bashoratli
   - keraksiz o'qitish yoki kech qolish

2. DRIFT bo'yicha: PSI yoki metrika chegarasi oshsa
   + resurs tejaladi
   - kuzatuv infratuzilmasi kerak

3. ONLINE: har yangi ma'lumotda yangilash
   + tez moslashadi
   - daraxt ansambllarida qiyin (qayta o'qitish kerak)

Amalda: jadval + drift signali (ikkalasi)

Har qayta o'qitishda: yangi modelni eski bilan TAQQOSLANG
  yomonroq bo'lsa - chiqarmang

Yangi model avtomatik yaxshiroq emas: har qayta o'qitishdan keyin uni eski model bilan bir xil test to'plamida solishtiring va faqat yaxshiroq bo'lsa almashtiring. Aks holda bitta buzilgan ma'lumot to'plami ishlab chiqarishga tushadi.

2.6. Nazorat ro'yxati

text
CHIQARISHDAN OLDIN:
  [ ] Pipeline to'liq saqlangan (tayyorlash + model)
  [ ] Versiyalar yozib qo'yilgan
  [ ] Belgilar ro'yxati va tartibi qayd etilgan
  [ ] Test to'plamidagi metrika hujjatlashtirilgan
  [ ] Kechikish va hajm SLA ga mos
  [ ] Yo'qolgan qiymatlar/yangi kategoriyalar sinalgan
  [ ] Chegara (threshold) tanlangan va asoslangan
  [ ] Kalibrlash tekshirilgan (kerak bo'lsa)
  [ ] Orqaga qaytarish (rollback) rejasi bor

CHIQARGANDAN KEYIN:
  [ ] Kirish taqsimoti kuzatilmoqda (PSI)
  [ ] Bashorat taqsimoti kuzatilmoqda
  [ ] Yorliqlar kelganda metrika hisoblanmoqda
  [ ] Ogohlantirish chegaralari o'rnatilgan

Yangi kategoriya va yo'qolgan qiymat — ishlab chiqarishdagi eng tez-tez uchraydigan nosozlik sababi. OneHotEncoder(handle_unknown="ignore") va imputer ni oldindan sinab ko'ring.

2.7. Tuzoqlar

Asosiy tuzoqlar: faqat modelni saqlash (Pipeline emas); versiyalarni yozmaslik; belgilar tartibini tekshirmaslik; bitta so'rovda n_jobs=-1; drift ni faqat yorliq kelganda ko'rish; yangi modelni eski bilan solishtirmaslik; chegarani ishlab chiqarishda o'zgartirib, hujjatlashtirmaslik; model hajmini oxirida tekshirish (kech).

2.8. Ishlab chiqarish — ishning yarmi

Modelni butun Pipeline sifatida saqlang (joblib.dump(quvur, ..., compress=3)) va versiyalarni yozib qo'ying. Hajm va kechikishni SLA ga moslang: min_samples_leaf oshirish eng samarali usul. Kirish driftini PSI bilan yorliqsiz kuzating, tushuncha driftini esa yorliq kelganda. Qayta o'qitishni jadval + drift signali bo'yicha qiling va yangi modelni eski bilan albatta taqqoslang. Keyingi dars — amaliyot.


3. Tez ma'lumotnoma

python
import joblib
import sklearn

joblib.dump({"quvur": quvur, "versiyalar": {"sklearn": sklearn.__version__},
             "belgilar": list(X.columns), "sana": "2026-09-20",
             "metrika": {"test_auc": 0.842}}, "model.joblib", compress=3)

paket = joblib.load("model.joblib")
assert list(Xyangi.columns) == paket["belgilar"]     # tartibni tekshiring

# PSI
def psi(eski, yangi, savatlar=10):
    kes = np.quantile(eski, np.linspace(0, 1, savatlar + 1))
    kes[0], kes[-1] = -np.inf, np.inf
    a = np.histogram(eski, kes)[0] / len(eski)
    b = np.histogram(yangi, kes)[0] / len(yangi)
    a, b = np.clip(a, 1e-6, None), np.clip(b, 1e-6, None)
    return float(((b - a) * np.log(b / a)).sum())
QOIDA: Pipeline ni saqla · versiyani yoz · belgilar tartibini tekshir ·
       PSI ni kuzat

Ishlab chiqarish xulosasi

joblib.dump(butun Pipeline, compress=3) + metama'lumot
Hajm: min_samples_leaf > n_estimators > max_depth
Kechikish: bitta so'rovda n_jobs=1
PSI < 0.1 barqaror, > 0.25 jiddiy siljish
Qayta o'qitish: jadval + drift; yangi modelni eski bilan taqqosla

4. Batafsil misollar

Misollar real numpy/pandas/sklearn bilan (Python 3.14).

Misol 1 — Hajm va kechikish

python
"""Model hajmini va bashorat narxini o'lchash (real numpy/sklearn/joblib)."""

import io
import pickle

import numpy as np
from sklearn.ensemble import (HistGradientBoostingClassifier,
                              RandomForestClassifier)
from sklearn.metrics import roc_auc_score
from sklearn.model_selection import train_test_split


def yarat(seed: int = 4, n: int = 8000, p: int = 12, shovqin: float = 0.12):
    rng = np.random.default_rng(seed)
    X = rng.normal(0, 1, (n, p))
    qoida = (((X[:, 0] > 0.25) & (X[:, 1] < 0.25))
             | ((X[:, 2] > 0.4) & (X[:, 3] > 0.0))
             | (X[:, 4] < -1.0))
    y = qoida.astype(int)
    alm = rng.random(n) < shovqin
    y[alm] = 1 - y[alm]
    return X, y


def hajm_kb(model) -> float:
    """Serializatsiya qilingan model hajmi."""
    bufer = io.BytesIO()
    pickle.dump(model, bufer, protocol=pickle.HIGHEST_PROTOCOL)
    return bufer.tell() / 1024


def main() -> None:
    X, y = yarat()
    Xtr, Xte, ytr, yte = train_test_split(X, y, test_size=0.3, random_state=0,
                                          stratify=y)

    print("=== 1. n_estimators va hajm ===")
    print(f"  {'n_est':>7} {'hajm KB':>10} {'tugunlar':>11} {'test AUC':>10}")
    for ne in [50, 100, 200, 400]:
        o = RandomForestClassifier(n_estimators=ne, random_state=0,
                                   n_jobs=1).fit(Xtr, ytr)
        tugunlar = sum(e.tree_.node_count for e in o.estimators_)
        print(f"  {ne:>7} {hajm_kb(o):>10.0f} {tugunlar:>11,} "
              f"{roc_auc_score(yte, o.predict_proba(Xte)[:, 1]):>10.4f}")

    print("\n=== 2. min_samples_leaf va hajm ===")
    print(f"  {'msl':>5} {'hajm KB':>10} {'tugunlar':>11} {'test AUC':>10}")
    for msl in [1, 5, 20, 50, 200]:
        o = RandomForestClassifier(n_estimators=150, min_samples_leaf=msl,
                                   random_state=0, n_jobs=1).fit(Xtr, ytr)
        tugunlar = sum(e.tree_.node_count for e in o.estimators_)
        print(f"  {msl:>5} {hajm_kb(o):>10.0f} {tugunlar:>11,} "
              f"{roc_auc_score(yte, o.predict_proba(Xte)[:, 1]):>10.4f}")

    print("\n=== 3. RF va boosting hajmi ===")
    rf = RandomForestClassifier(n_estimators=150, min_samples_leaf=20,
                                random_state=0, n_jobs=1).fit(Xtr, ytr)
    gb = HistGradientBoostingClassifier(learning_rate=0.1, max_iter=250,
                                        early_stopping=True,
                                        validation_fraction=0.15,
                                        n_iter_no_change=20,
                                        random_state=0).fit(Xtr, ytr)
    print(f"  RF(150, msl=20): {hajm_kb(rf):>8.0f} KB, AUC "
          f"{roc_auc_score(yte, rf.predict_proba(Xte)[:, 1]):.4f}")
    print(f"  HistGB({gb.n_iter_} qadam): {hajm_kb(gb):>8.0f} KB, AUC "
          f"{roc_auc_score(yte, gb.predict_proba(Xte)[:, 1]):.4f}")

    print("\n=== 4. Bashorat hisoblash hajmi ===")
    print("  (bitta bashorat uchun ko'rib chiqiladigan tugunlar tartibi)")
    toliq = RandomForestClassifier(n_estimators=150, random_state=0,
                                   n_jobs=1).fit(Xtr, ytr)
    for nom, model in [("RF(150, msl=1)", toliq),
                       ("RF(150, msl=20)", rf)]:
        chuqurliklar = [e.get_depth() for e in model.estimators_]
        print(f"  {nom:<18}: {len(model.estimators_)} daraxt x "
              f"o'rtacha chuqurlik {np.mean(chuqurliklar):.1f} = "
              f"~{len(model.estimators_) * np.mean(chuqurliklar):.0f} qadam")
    print(f"  {'HistGB':<18}: {gb.n_iter_} daraxt x max_leaf_nodes "
          f"{gb.max_leaf_nodes} -> ancha kam")
    print("  ⭐ min_samples_leaf - hajmni kamaytirishning eng samarali yo'li")


if __name__ == "__main__":
    main()

Natijaning muhim qismi:

text
=== 1. n_estimators va hajm ===
    n_est    hajm KB    tugunlar   test AUC
       50       4898      62,498     0.8747
      100       9776     124,758     0.8731
      200      19454     248,272     0.8735
      400      39037     498,206     0.8724

=== 2. min_samples_leaf va hajm ===
    msl    hajm KB    tugunlar   test AUC
      1      14646     186,904     0.8733
      5       7848      99,904     0.8726
     20       3063      38,664     0.8730
     50       1309      16,212     0.8745
    200        345       3,880     0.8779

=== 3. RF va boosting hajmi ===
  RF(150, msl=20):     3063 KB, AUC 0.8730
  HistGB(47 qadam):      189 KB, AUC 0.8712

=== 4. Bashorat hisoblash hajmi ===
  (bitta bashorat uchun ko'rib chiqiladigan tugunlar tartibi)
  RF(150, msl=1)    : 150 daraxt x o'rtacha chuqurlik 25.8 = ~3877 qadam
  RF(150, msl=20)   : 150 daraxt x o'rtacha chuqurlik 14.7 = ~2210 qadam
  HistGB            : 47 daraxt x max_leaf_nodes 31 -> ancha kam
  ⭐ min_samples_leaf - hajmni kamaytirishning eng samarali yo'li

Nima ko'rsatdi: 2.2-bo'lim.

Misol 2 — Pipeline ni saqlash

python
"""Butun quvurni metama'lumot bilan saqlash (real pandas/sklearn/joblib)."""

import os
import tempfile

import joblib
import numpy as np
import pandas as pd
import sklearn
from sklearn.compose import ColumnTransformer
from sklearn.ensemble import RandomForestClassifier
from sklearn.impute import SimpleImputer
from sklearn.metrics import roc_auc_score
from sklearn.model_selection import train_test_split
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import OneHotEncoder


def yarat(seed: int = 6, n: int = 8000) -> pd.DataFrame:
    rng = np.random.default_rng(seed)
    hudud = rng.choice(["toshkent", "samarqand", "buxoro"], n,
                       p=[0.5, 0.3, 0.2])
    tur = rng.choice(["oddiy", "tezkor"], n, p=[0.7, 0.3])
    masofa = rng.gamma(3, 60, n)
    ogirlik = rng.gamma(2, 4, n)
    hq = pd.Series(hudud).map({"toshkent": 0.0, "samarqand": 0.5,
                               "buxoro": 0.9}).to_numpy()
    kuch = -2.5 + 0.006 * masofa + 0.05 * ogirlik + hq
    y = (rng.random(n) < 1 / (1 + np.exp(-kuch))).astype(int)
    df = pd.DataFrame({"hudud": hudud, "tur": tur, "masofa": masofa,
                       "ogirlik": ogirlik, "kechikdi": y})
    df.loc[rng.random(n) < 0.04, "ogirlik"] = np.nan
    return df


def main() -> None:
    df = yarat()
    X = df.drop(columns="kechikdi")
    y = df["kechikdi"]
    Xtr, Xte, ytr, yte = train_test_split(X, y, test_size=0.3, random_state=0,
                                          stratify=y)

    kategoriya = ["hudud", "tur"]
    sonli = ["masofa", "ogirlik"]
    quvur = Pipeline([
        ("tayyor", ColumnTransformer([
            ("k", OneHotEncoder(handle_unknown="ignore"), kategoriya),
            ("s", SimpleImputer(strategy="median"), sonli)])),
        ("model", RandomForestClassifier(n_estimators=200, min_samples_leaf=10,
                                         random_state=0, n_jobs=1))])
    quvur.fit(Xtr, ytr)
    auc = roc_auc_score(yte, quvur.predict_proba(Xte)[:, 1])

    print("=== 1. Quvur o'qitildi ===")
    print(f"  bosqichlar: {[nom for nom, _ in quvur.steps]}")
    print(f"  test ROC AUC: {auc:.4f}")

    print("\n=== 2. Metama'lumot bilan saqlash ===")
    paket = {
        "quvur": quvur,
        "belgilar": list(X.columns),
        "versiyalar": {"sklearn": sklearn.__version__,
                       "numpy": np.__version__,
                       "pandas": pd.__version__},
        "metrika": {"test_roc_auc": round(float(auc), 4),
                    "o_quv_hajmi": len(Xtr)},
        "chegara": 0.5,
    }
    jild = tempfile.mkdtemp()
    yol = os.path.join(jild, "model.joblib")
    joblib.dump(paket, yol, compress=3)
    print(f"  fayl hajmi: {os.path.getsize(yol) / 1024:.0f} KB")
    yol2 = os.path.join(jild, "model_siqilmagan.joblib")
    joblib.dump(paket, yol2)
    print(f"  siqilmagan:  {os.path.getsize(yol2) / 1024:.0f} KB")
    print(f"  siqish nisbati: "
          f"{os.path.getsize(yol2) / os.path.getsize(yol):.1f}x")

    print("\n=== 3. Yuklash va tekshirish ===")
    yuklangan = joblib.load(yol)
    print(f"  belgilar: {yuklangan['belgilar']}")
    print(f"  versiyalar: {yuklangan['versiyalar']}")
    print(f"  saqlangan metrika: {yuklangan['metrika']}")
    q2 = yuklangan["quvur"]
    yangi_auc = roc_auc_score(yte, q2.predict_proba(Xte)[:, 1])
    print(f"  yuklangandan keyin AUC: {yangi_auc:.4f} "
          f"(farq {abs(yangi_auc - auc):.6f})")

    print("\n=== 4. Ishlab chiqarishdagi qiyin holatlar ===")
    # yangi kategoriya
    yangi = Xte.iloc[:3].copy()
    yangi.loc[:, "hudud"] = "andijon"              # o'quvda yo'q edi
    print(f"  yangi kategoriya: bashorat "
          f"{q2.predict_proba(yangi)[:, 1].round(4).tolist()}")
    # yo'qolgan qiymat
    yoq = Xte.iloc[:3].copy()
    yoq.loc[:, "ogirlik"] = np.nan
    print(f"  NaN ogirlik: bashorat "
          f"{q2.predict_proba(yoq)[:, 1].round(4).tolist()}")
    # belgilar tartibi o'zgargan
    teskari = Xte.iloc[:3][list(reversed(list(X.columns)))]
    print(f"  teskari tartib: bashorat "
          f"{q2.predict_proba(teskari)[:, 1].round(4).tolist()}")
    print(f"  (ColumnTransformer nom bo'yicha ishlaydi - tartib muhim emas)")
    print("  ⭐ Butun Pipeline saqlansa, bu holatlar boshqariladi")
    for f in [yol, yol2]:
        os.remove(f)
    os.rmdir(jild)


if __name__ == "__main__":
    main()

Natijaning muhim qismi:

text
=== 1. Quvur o'qitildi ===
  bosqichlar: ['tayyor', 'model']
  test ROC AUC: 0.6441

=== 2. Metama'lumot bilan saqlash ===
  fayl hajmi: 2525 KB
  siqilmagan:  8449 KB
  siqish nisbati: 3.3x

=== 3. Yuklash va tekshirish ===
  belgilar: ['hudud', 'tur', 'masofa', 'ogirlik']
  versiyalar: {'sklearn': '1.9.1', 'numpy': '2.5.3', 'pandas': '3.0.6'}
  saqlangan metrika: {'test_roc_auc': 0.6441, 'o_quv_hajmi': 5600}
  yuklangandan keyin AUC: 0.6441 (farq 0.000000)

=== 4. Ishlab chiqarishdagi qiyin holatlar ===
  yangi kategoriya: bashorat [0.3078, 0.3143, 0.5927]
  NaN ogirlik: bashorat [0.2621, 0.3326, 0.2462]
  teskari tartib: bashorat [0.4844, 0.395, 0.5653]
  (ColumnTransformer nom bo'yicha ishlaydi - tartib muhim emas)
  ⭐ Butun Pipeline saqlansa, bu holatlar boshqariladi

Nima ko'rsatdi: 2.1, 2.3-bo'limlar.

Misol 3 — Drift ni aniqlash

python
"""PSI va bashorat taqsimoti kuzatuvi (real numpy/sklearn)."""

import numpy as np
from sklearn.ensemble import HistGradientBoostingClassifier
from sklearn.metrics import roc_auc_score
from sklearn.model_selection import train_test_split


def yarat(seed: int, n: int = 4000, siljish: float = 0.0,
          tushuncha: float = 0.0):
    """siljish - kirish drifti; tushuncha - bog'liqlik drifti."""
    rng = np.random.default_rng(seed)
    X = rng.normal(0, 1, (n, 8))
    X[:, 0] += siljish
    kuch = ((1.4 - tushuncha) * X[:, 0] - 1.0 * X[:, 1]
            + (0.3 + tushuncha) * X[:, 2] + 0.8 * (X[:, 3] > 0.5))
    y = (rng.random(n) < 1 / (1 + np.exp(-kuch))).astype(int)
    return X, y


def psi(eski, yangi, savatlar: int = 10) -> float:
    kes = np.quantile(eski, np.linspace(0, 1, savatlar + 1))
    kes[0], kes[-1] = -np.inf, np.inf
    a = np.histogram(eski, kes)[0] / len(eski)
    b = np.histogram(yangi, kes)[0] / len(yangi)
    a = np.clip(a, 1e-6, None)
    b = np.clip(b, 1e-6, None)
    return float(((b - a) * np.log(b / a)).sum())


def main() -> None:
    X, y = yarat(1)
    Xtr, Xte, ytr, yte = train_test_split(X, y, test_size=0.3, random_state=0,
                                          stratify=y)
    model = HistGradientBoostingClassifier(learning_rate=0.1, max_iter=300,
                                           early_stopping=True,
                                           validation_fraction=0.15,
                                           n_iter_no_change=20,
                                           random_state=0).fit(Xtr, ytr)
    asosiy = roc_auc_score(yte, model.predict_proba(Xte)[:, 1])
    asosiy_p = model.predict_proba(Xte)[:, 1]
    print("=== 1. Bazaviy model ===")
    print(f"  test AUC: {asosiy:.4f}, o'rtacha bashorat "
          f"{asosiy_p.mean():.4f}")

    print("\n=== 2. Kirish drifti (covariate shift) ===")
    print(f"  {'siljish':>8} {'PSI(x0)':>9} {'PSI(bashorat)':>15} "
          f"{'AUC':>8} {'o_rt p':>8}")
    for s in [0.0, 0.25, 0.5, 1.0, 2.0]:
        Xn, yn = yarat(50, siljish=s)
        p = model.predict_proba(Xn)[:, 1]
        print(f"  {s:>8.2f} {psi(Xte[:, 0], Xn[:, 0]):>9.4f} "
              f"{psi(asosiy_p, p):>15.4f} "
              f"{roc_auc_score(yn, p):>8.4f} {p.mean():>8.4f}")

    print("\n=== 3. Tushuncha drifti (concept shift) ===")
    print(f"  {'daraja':>8} {'PSI(x0)':>9} {'PSI(bashorat)':>15} "
          f"{'AUC':>8}")
    for t in [0.0, 0.4, 0.8, 1.2]:
        Xn, yn = yarat(50, tushuncha=t)
        p = model.predict_proba(Xn)[:, 1]
        print(f"  {t:>8.2f} {psi(Xte[:, 0], Xn[:, 0]):>9.4f} "
              f"{psi(asosiy_p, p):>15.4f} {roc_auc_score(yn, p):>8.4f}")
    print("  (kirish o'zgarmadi, lekin sifat tushdi - PSI ni ko'rmaydi)")

    print("\n=== 4. Barcha belgilar bo'yicha PSI ===")
    Xn, yn = yarat(50, siljish=0.8)
    print(f"  {'belgi':>8} {'PSI':>9} {'holat':<20}")
    for i in range(X.shape[1]):
        v = psi(Xte[:, i], Xn[:, i])
        holat = ("barqaror" if v < 0.1
                 else "kuzatuv kerak" if v < 0.25 else "JIDDIY SILJISH")
        print(f"  x{i:<7} {v:>9.4f} {holat:<20}")
    print("  ⭐ Kirish drifti yorliqsiz aniqlanadi, tushuncha drifti - yo'q")


if __name__ == "__main__":
    main()

Natijaning muhim qismi:

text
=== 1. Bazaviy model ===
  test AUC: 0.8144, o'rtacha bashorat 0.5479

=== 2. Kirish drifti (covariate shift) ===
   siljish   PSI(x0)   PSI(bashorat)      AUC   o_rt p
      0.00    0.0133          0.0068   0.8167   0.5627
      0.25    0.0924          0.0633   0.8192   0.6129
      0.50    0.3024          0.1800   0.8217   0.6604
      1.00    1.0555          0.6127   0.8139   0.7510
      2.00    3.5096          2.0615   0.8198   0.8721

=== 3. Tushuncha drifti (concept shift) ===
    daraja   PSI(x0)   PSI(bashorat)      AUC
      0.00    0.0133          0.0068   0.8167
      0.40    0.0133          0.0068   0.7914
      0.80    0.0133          0.0068   0.7421
      1.20    0.0133          0.0068   0.6801
  (kirish o'zgarmadi, lekin sifat tushdi - PSI ni ko'rmaydi)

=== 4. Barcha belgilar bo'yicha PSI ===
     belgi       PSI holat
  x0          0.7015 JIDDIY SILJISH
  x1          0.0050 barqaror
  x2          0.0189 barqaror
  x3          0.0070 barqaror
  x4          0.0082 barqaror
  x5          0.0078 barqaror
  x6          0.0057 barqaror
  x7          0.0140 barqaror
  ⭐ Kirish drifti yorliqsiz aniqlanadi, tushuncha drifti - yo'q

Nima ko'rsatdi: 2.4-bo'lim.

Misol 4 — Qayta o'qitish siyosati

python
"""Jadval, drift va taqqoslash (real numpy/sklearn)."""

import numpy as np
from sklearn.ensemble import HistGradientBoostingClassifier
from sklearn.metrics import roc_auc_score


def davr(nomer: int, n: int = 2500):
    """Vaqt o'tishi bilan sekin o'zgaradigan jarayon."""
    rng = np.random.default_rng(100 + nomer)
    X = rng.normal(0, 1, (n, 8))
    X[:, 0] += 0.12 * nomer                       # sekin siljish
    w0 = 1.4 - 0.05 * nomer                       # bog'liqlik ham o'zgaradi
    w2 = 0.3 + 0.05 * nomer
    kuch = w0 * X[:, 0] - 1.0 * X[:, 1] + w2 * X[:, 2] + 0.8 * (X[:, 3] > 0.5)
    y = (rng.random(n) < 1 / (1 + np.exp(-kuch))).astype(int)
    return X, y


def oqit(X, y):
    return HistGradientBoostingClassifier(learning_rate=0.1, max_iter=150,
                                          early_stopping=True,
                                          validation_fraction=0.15,
                                          n_iter_no_change=20,
                                          random_state=0).fit(X, y)


def main() -> None:
    X0, y0 = davr(0)
    model = oqit(X0, y0)

    print("=== 1. Hech qachon qayta o'qitmaslik ===")
    print(f"  {'davr':>5} {'AUC':>8}")
    hech = []
    for d in range(1, 9):
        Xd, yd = davr(d)
        a = roc_auc_score(yd, model.predict_proba(Xd)[:, 1])
        hech.append(a)
        if d in [1, 3, 5, 8]:
            print(f"  {d:>5} {a:>8.4f}")

    print("\n=== 2. Har davr qayta o'qitish ===")
    m = oqit(X0, y0)
    har = []
    for d in range(1, 9):
        Xd, yd = davr(d)
        a = roc_auc_score(yd, m.predict_proba(Xd)[:, 1])
        har.append(a)
        m = oqit(Xd, yd)                     # keyingi davr uchun yangilash
    print(f"  o'rtacha AUC: {np.mean(har):.4f} "
          f"(qayta o'qitishsiz {np.mean(hech):.4f})")
    print(f"  oxirgi davr: {har[-1]:.4f} (qayta o'qitishsiz {hech[-1]:.4f})")

    print("\n=== 3. Drift signali bo'yicha ===")

    def psi(eski, yangi, savatlar: int = 10) -> float:
        kes = np.quantile(eski, np.linspace(0, 1, savatlar + 1))
        kes[0], kes[-1] = -np.inf, np.inf
        a = np.clip(np.histogram(eski, kes)[0] / len(eski), 1e-6, None)
        b = np.clip(np.histogram(yangi, kes)[0] / len(yangi), 1e-6, None)
        return float(((b - a) * np.log(b / a)).sum())

    m = oqit(X0, y0)
    asos_X = X0
    signal, oqitishlar = [], 0
    for d in range(1, 9):
        Xd, yd = davr(d)
        a = roc_auc_score(yd, m.predict_proba(Xd)[:, 1])
        signal.append(a)
        eng_psi = max(psi(asos_X[:, i], Xd[:, i]) for i in range(Xd.shape[1]))
        if eng_psi > 0.25:
            m = oqit(Xd, yd)
            asos_X = Xd
            oqitishlar += 1
    print(f"  qayta o'qitishlar soni: {oqitishlar} (8 davrda)")
    print(f"  o'rtacha AUC: {np.mean(signal):.4f}")
    print(f"  oxirgi davr: {signal[-1]:.4f}")

    print("\n=== 4. Yangi modelni eski bilan taqqoslash ===")
    Xs, ys = davr(5)
    eski = oqit(X0, y0)
    # "buzilgan" yangi ma'lumot: yorliqlarning 40% i tasodifiy
    rng = np.random.default_rng(0)
    yb = ys.copy()
    buzuq = rng.random(len(yb)) < 0.4
    yb[buzuq] = rng.integers(0, 2, buzuq.sum())
    yangi = oqit(Xs, yb)
    Xv, yv = davr(6)
    a_eski = roc_auc_score(yv, eski.predict_proba(Xv)[:, 1])
    a_yangi = roc_auc_score(yv, yangi.predict_proba(Xv)[:, 1])
    print(f"  eski model:  {a_eski:.4f}")
    print(f"  yangi model: {a_yangi:.4f}")
    print(f"  qaror: {'YANGI chiqariladi' if a_yangi > a_eski else 'ESKI qoladi'}")
    print("  ⭐ Har qayta o'qitishdan keyin taqqoslash majburiy")


if __name__ == "__main__":
    main()

Natijaning muhim qismi:

text
=== 1. Hech qachon qayta o'qitmaslik ===
   davr      AUC
      1   0.8030
      3   0.8052
      5   0.7986
      8   0.7631

=== 2. Har davr qayta o'qitish ===
  o'rtacha AUC: 0.8059 (qayta o'qitishsiz 0.7929)
  oxirgi davr: 0.7851 (qayta o'qitishsiz 0.7631)

=== 3. Drift signali bo'yicha ===
  qayta o'qitishlar soni: 1 (8 davrda)
  o'rtacha AUC: 0.8039
  oxirgi davr: 0.8009

=== 4. Yangi modelni eski bilan taqqoslash ===
  eski model:  0.7993
  yangi model: 0.7528
  qaror: ESKI qoladi
  ⭐ Har qayta o'qitishdan keyin taqqoslash majburiy

Nima ko'rsatdi: 2.5-bo'lim.


5. To'g'ri va noto'g'ri tushunishlar

Noto'g'ri fikr To'g'risi
"Modelni saqlash yetarli" Butun Pipeline
"joblib versiyaga bog'liq emas" Juda bog'liq
"n_jobs=-1 har doim tezroq" Bitta so'rovda emas
"PSI barcha driftni ko'radi" Faqat kirish driftini
"Yangi model yaxshiroq" Taqqoslash kerak
"Hajm muhim emas" SLA va xarajat
"RF va GB hajmi o'xshash" GB ancha kichik
"Drift sekin keladi" Birdan ham bo'ladi

6. Keng tarqalgan xatolar va yechimlari

1. Faqat modelni saqlash

python
joblib.dump(model, "model.joblib")        # scaler qayerda?       # ⚠️
joblib.dump(quvur, "quvur.joblib")        # butun Pipeline        # ✅

2. Versiyalarni yozmaslik

python
joblib.dump(quvur, "model.joblib")                                # ⚠️
joblib.dump({"quvur": quvur, "versiyalar": {...}}, "model.joblib") # ✅

3. Bitta so'rovda n_jobs=-1

python
model.set_params(n_jobs=-1); model.predict(x_bitta)               # ⚠️
model.set_params(n_jobs=1);  model.predict(x_bitta)               # ✅

4. Belgilar tartibini tekshirmaslik

python
model.predict(yangi_df.values)                                    # ⚠️
assert list(yangi_df.columns) == paket["belgilar"]                # ✅

5. Faqat yorliq bilan kuzatuv

python
# metrika yorliq kelguncha hisoblanmaydi (12 oy)                  # ⚠️
# PSI va bashorat taqsimotini darhol kuzating                     # ✅

6. Taqqoslashsiz almashtirish

python
yangi = oqit(yangi_malumot); chiqar(yangi)                        # ⚠️
if baho(yangi) > baho(eski): chiqar(yangi)                        # ✅

7. Hajmni oxirida tekshirish

python
# sozlash tugagach 400 MB ekanini bilib qolish                    # ⚠️
# SLA ni sozlash setkasiga cheklov sifatida qo'ying               # ✅

7. Integratsiya — bu bilim qayerda kerak bo'ladi

  • 15.5-dars (o'tilgan): Random Forest
  • 15.10-dars (o'tilgan): XGBoost va LightGBM
  • 12.9-dars (o'tilgan): Leakage va Pipeline
  • 14.10-dars (o'tilgan): Kalibrlash
  • 15.14-dars: Amaliyot

8. Eng yaxshi amaliyotlar

  1. Butun Pipeline ni saqlang.

  2. Metama'lumot qo'shing.

  3. Belgilar tartibini tekshiring.

  4. Hajm va kechikishni o'lchang.

  5. PSI ni kuzating.

  6. Jadval + drift siyosati.

  7. Har doim taqqoslang.

  8. Rollback rejasi tayyorlang.


9. Amaliy topshiriq

Vazifa 1: Bashorat qiling

python
1.  # nima saqlanadi?
2.  # metama'lumotda nima bo'lsin?
3.  # hajmni kamaytirishning eng samarali yo'li?
4.  # bitta so'rovda n_jobs?
5.  # RF va GB hajmi?
6.  # kirish drifti nima?
7.  # tushuncha drifti?
8.  # PSI chegaralari?
9.  # PSI qaysi driftni ko'radi?
10. # qayta o'qitish siyosatlari?
11. # yangi model chiqarilsinmi?
12. # yangi kategoriya uchun?
Javoblar
  1. Butun Pipeline
  2. Versiyalar, belgilar, metrika
  3. min_samples_leaf
  4. 1
  5. GB ancha kichik
  6. P(X) o'zgarishi
  7. P(y|X) o'zgarishi
  8. 0.1 / 0.25
  9. Faqat kirish
  10. Jadval, drift, online
  11. Faqat taqqoslashdan keyin
  12. handle_unknown="ignore"

Vazifa 2: Xatolarni tuzating

python
1.  joblib.dump(model, "model.joblib")   # scaler alohida

2.  joblib.dump(quvur, "model.joblib")   # versiya yo'q

3.  model.set_params(n_jobs=-1); model.predict(bitta_qator)

4.  model.predict(yangi_df.values)

5.  yangi = oqit(yangi_malumot); chiqar(yangi)
Javoblar
python
1.  joblib.dump(quvur, "quvur.joblib")

2.  joblib.dump({"quvur": quvur, "versiyalar": {...}}, "model.joblib")

3.  model.set_params(n_jobs=1); model.predict(bitta_qator)

4.  assert list(yangi_df.columns) == paket["belgilar"]

5.  if baho(yangi) > baho(eski): chiqar(yangi)

Vazifa 3: Hajm

Modellang:

  1. n_estimators
  2. min_samples_leaf
  3. RF va GB
  4. Hisoblash hajmi

Vazifa 4: Saqlash

Modellang:

  1. Quvur
  2. Metama'lumot
  3. Yuklash
  4. Qiyin holatlar

Vazifa 5: Drift

Modellang:

  1. Bazaviy
  2. Kirish drifti
  3. Tushuncha drifti
  4. Barcha belgilar

Vazifa 6: Qayta o'qitish

Modellang:

  1. Hech qachon
  2. Har davr
  3. Drift bo'yicha
  4. Taqqoslash

Vazifa 7: O'ylash

Model ishlab chiqarishda 6 oy muvaffaqiyatli ishladi, keyin metrikalar tusha boshladi. PSI barqaror. Nima bo'lgan va nima qilish kerak?

Javob

Qisqa javob: PSI barqaror bo'lsa-yu metrika tushsa, bu tushuncha drifti (P(y|X) o'zgargan) yoki o'lchov muammosiga ishora qiladi. Birinchi qadam — sabab aniqlash, qayta o'qitish emas.

1. Mumkin sabablar

Sabab Tekshiruv
Tushuncha drifti Yangi ma'lumotda qayta o'qitib, sifatni solishtirish
Yorliqlar kechikishi Yorliq yig'ish jarayonini tekshirish
Yangi segment Segmentlar bo'yicha metrikani bo'lib ko'rish
Quvurdagi buzilish Kirish qiymatlarining oraliqlarini tekshirish
Fikr-mulohaza sikli Model qarorlari ma'lumotga ta'sir qilyaptimi?
Metrika ta'rifi o'zgardi Hisoblash kodini solishtirish

2. Fikr-mulohaza sikli (feedback loop)

Eng nozik sabab: model o'zi kuzatayotgan ma'lumotni o'zgartiradi.

  • Model "xavfli" degan arizalar rad etiladi -> ularning natijasi ma'lum bo'lmaydi
  • O'quv ma'lumoti faqat tasdiqlanganlardan iborat bo'lib qoladi
  • Model asta-sekin tor taqsimotga moslashadi

Yechim: tasodifiy nazorat guruhi (kichik ulushda modelni chetlab o'tish).

3. Segment tahlili

  1. Metrikani hudud, kanal, mijoz turi bo'yicha bo'ling
  2. Ko'pincha muammo bitta segmentda bo'ladi
  3. Umumiy PSI uni yashiradi (segment ulushi kichik)
  4. Segment bo'yicha PSI ni ham hisoblang

4. Amaliy tartib

  1. Metrika hisoblash kodini tekshiring (eng arzon)
  2. Segmentlarga bo'ling
  3. Yangi ma'lumotda qayta o'qitib, eski bilan solishtiring
  4. Yaxshilansa — tushuncha drifti tasdiqlandi
  5. Yaxshilanmasa — muammo ma'lumot quvurida yoki metrikada

5. Xulosa

  1. PSI faqat kirishni ko'radi
  2. Tushuncha drifti yorliq talab qiladi
  3. Avval sabab, keyin qayta o'qitish
  4. Segment tahlili ko'pincha javobni beradi

Nimani mustahkamlaydi: 2.4, 2.5-bo'limlar.


Xulosa

Bu darsda ansambllarni ishlab chiqarishga chiqarishni o'rgandik.

Eng muhim uch fikr:

  1. Butun Pipeline ni metama'lumot bilan saqlang. joblib.dump({"quvur": ..., "versiyalar": ..., "belgilar": ..., "metrika": ...}, ..., compress=3). Faqat modelni saqlash — keng tarqalgan xato: tayyorlash bosqichi alohida qolib ketadi. joblib versiyaga bog'liq, shuning uchun sklearn/numpy/Python versiyalarini albatta yozib qo'ying.

  2. Hajm va kechikish — SLA cheklovi. Random Forest yuzlab megabayt bo'lishi mumkin. Kamaytirishning eng samarali yo'li — min_samples_leaf ni oshirish (sifat deyarli tushmaydi), keyin n_estimators. Gradient boosting odatda ancha kichik. Bitta so'rovda n_jobs=1 qo'ying — parallellashtirish ustama xarajati foydadan katta.

  3. Drift ni yorliqsiz kuzating. PSI kirish taqsimoti siljishini darhol ko'rsatadi (< 0.1 barqaror, > 0.25 jiddiy), lekin tushuncha driftini (P(y|X) o'zgarishi) ko'rmaydi — uning uchun haqiqiy yorliq kerak. Qayta o'qitishni jadval + drift signali bo'yicha qiling va yangi modelni eski bilan bir xil to'plamda albatta taqqoslang.

Keyingi darsda amaliyot: butun 15-qism bo'yicha to'liq loyiha.

Ulashish:Telegram'da

Izohlar (0)

Izoh yozish uchun kiring.

  • Hozircha izoh yo'q. Birinchi bo'ling!
15.13-dars: Ansambllarni ishlab chiqarishga chiqarish — IlmHamroh