Model testlari: kod testi yetarli emas
Har doim 70 kilogramm ko'rsatadigan tarozi
Do'konda tarozi bor. Uni tekshiramiz:
- Yoqilganda ishlaydimi? Ha.
- Ekranda raqam chiqadimi? Ha.
- Raqam sonmi, matn emasmi? Ha.
- Manfiy chiqadimi? Yo'q.
Hamma test o'tdi. Tarozi soz.
Endi unga 10 kilogramm olma qo'yamiz. Ekranda: 70.
Bolani qo'yamiz. 70. Hech narsa qo'ymaymiz. 70.
Tarozi buzuq emas — u ishlayapti, raqam beryapti, xato bermayapti. U faqat noto'g'ri.
Kod testi va model testi
Mana ML dagi eng muhim ajratish.
Python muhandisligi kitobida kod testlarini yozgandik: funksiya to'g'ri natija qaytaradimi, xato holatni ushlaydimi. Bular tarozining yonishini tekshiradi.
Model uchun bu yetarli emas, chunki kod to'g'ri, model esa yomon bo'lishi mumkin. Kod hech qachon xato bermaydi — u har doim 70 qaytaradi.
Demak bizga boshqa turdagi test kerak: "javob mantiqiymi?" degan savolga javob beradigan test. Masalan: telefon raqami ko'proq e'londa takrorlansa, broker ehtimoli oshishi kerak. Agar tushsa — kod ishlayapti, lekin model buzuq.
Uch xil test
Python muhandisligi kitobida kod testlarini yozgandik: funksiya to'g'ri natija qaytaradimi. ML da bu yetarli emas, chunki kod to'g'ri, model esa yomon bo'lishi mumkin.
import pandas as pd show(pd.DataFrame([ {"tur": "Kod testi", "savol": "Funksiya to'g'ri hisoblaydimi?", "misol": "tayyorla() 13 ta ustun qaytaradimi", "qachon buziladi": "Refaktoring"}, {"tur": "Ma'lumot testi", "savol": "Kirish ma'lumot to'g'rimi?", "misol": "maydon > 0 (6-bob)", "qachon buziladi": "Manba o'zgarganda"}, {"tur": "Model testi", "savol": "Model MANTIQIY xulq qiladimi?", "misol": "Telefon ko'p takrorlansa, broker ehtimoli oshishi kerak", "qachon buziladi": "Model qayta o'qitilganda"}, ]))
| tur | savol | misol | qachon buziladi |
|---|---|---|---|
| Kod testi | Funksiya to'g'ri hisoblaydimi? | tayyorla() 13 ta ustun qaytaradimi | Refaktoring |
| Ma'lumot testi | Kirish ma'lumot to'g'rimi? | maydon > 0 (6-bob) | Manba o'zgarganda |
| Model testi | Model MANTIQIY xulq qiladimi? | Telefon ko'p takrorlansa, broker ehtimoli oshishi kerak | Model qayta o'qitilganda |
Uchinchisi eng kam yoziladi va eng ko'p kerak bo'ladi.
To'rt xil model testi
diagramma( { "MINIMAL\nAniq holatlar": (0, 0, 2.6, 1.1, "manba"), "YO'NALISH\nX oshsa, Y oshsin": (3.4, 0, 2.6, 1.1, "qayta"), "O'ZGARMASLIK\nAhamiyatsiz belgi": (6.8, 0, 2.6, 1.1, "saqlash"), "REGRESSIYA\nEskidan yomon emas": (10.2, 0, 2.6, 1.1, "kuzatuv"), "Model chiqarilsin": (5.1, 2.2, 3.0, 1.1, "xizmat"), }, [ ("MINIMAL\nAniq holatlar", "Model chiqarilsin"), ("YO'NALISH\nX oshsa, Y oshsin", "Model chiqarilsin"), ("O'ZGARMASLIK\nAhamiyatsiz belgi", "Model chiqarilsin"), ("REGRESSIYA\nEskidan yomon emas", "Model chiqarilsin"), ], nom="model_testlari.png", kenglik=13, izoh="To'rttasi ham o'tsagina model chiqariladi. Har biri boshqa turdagi nosozlikni ushlaydi.", )

To'rttasi ham o'tsagina model chiqariladi. Har biri boshqa turdagi nosozlikni ushlaydi.
Har birini yozamiz va haqiqiy modelda sinaymiz.
Sinov uchun yordamchi
shu faylni yaratingsrc/quvur/sinov_yordam.py
"""Model testlari uchun namuna yasovchi.""" from typing import Any import pandas as pd from .xususiyat import XOM_USTUNLAR ODATIY = { "xonalar": 2, "maydon": 55.0, "qavat": 3, "jami_qavat": 9, "narx_mln": 4.5, "tavsif_uzunligi": 300, "rasmlar_soni": 8, "telefon_takrorlanishi": 1, "kunlik_elonlar": 1, "egasi_tasdiqlangan": 1, } def namuna(**ozgarishlar: Any) -> pd.DataFrame: """Odatiy e'lon; berilgan maydonlar almashtiriladi.""" q = {**ODATIY, **ozgarishlar} return pd.DataFrame([{u: q[u] for u in XOM_USTUNLAR}]) def ehtimol(model, **ozgarishlar: Any) -> float: """Shu e'lon uchun broker ehtimoli.""" from .xususiyat import tayyorla return float(model.predict_proba(tayyorla(namuna(**ozgarishlar)))[0, 1])
1. Minimal funksionallik testi
Eng aniq holatlarni model to'g'ri tushunishi kerak:
python3 -c " import sys; sys.path.insert(0, 'src') import pandas as pd from sklearn.ensemble import RandomForestClassifier from sklearn.model_selection import train_test_split from quvur.xususiyat import XUSUSIYATLAR, tayyorla from quvur.sinov_yordam import ehtimol d = pd.read_csv('../datasets/ijara.csv') x, y = tayyorla(d), d['broker'] Xtr, _, ytr, _ = train_test_split(x, y, test_size=0.25, stratify=y, random_state=42) m = RandomForestClassifier(n_estimators=150, min_samples_leaf=3, random_state=42, n_jobs=1).fit(Xtr, ytr) import joblib; joblib.dump(m, 'sinov_model.joblib') aniq_ega = ehtimol(m, telefon_takrorlanishi=1, kunlik_elonlar=1, tavsif_uzunligi=600, rasmlar_soni=15, egasi_tasdiqlangan=1) aniq_broker = ehtimol(m, telefon_takrorlanishi=18, kunlik_elonlar=9, tavsif_uzunligi=60, rasmlar_soni=2, egasi_tasdiqlangan=0) print(f'aniq EGA -> broker ehtimoli {aniq_ega:.3f} (past bo\'lishi kerak)') print(f'aniq BROKER -> broker ehtimoli {aniq_broker:.3f} (yuqori bo\'lishi kerak)') print() print('o\'tdimi:', aniq_ega < 0.3 and aniq_broker > 0.7) "
aniq EGA -> broker ehtimoli 0.056 (past bo'lishi kerak) aniq BROKER -> broker ehtimoli 0.898 (yuqori bo'lishi kerak) o'tdimi: True
Bu test oddiy ko'rinadi, lekin u eng ko'p nosozlikni ushlaydi: ustun tartibi buzilgan (3-bob), model noto'g'ri yuklangan, yoki nishon teskari o'rgatilgan.
2. Yo'nalish testi
Ba'zi belgilarning ta'siri oldindan ma'lum. Telefon ko'p takrorlansa, broker ehtimoli oshishi kerak — kamaymasligi:
python3 -c " import sys; sys.path.insert(0, 'src') import joblib from quvur.sinov_yordam import ehtimol m = joblib.load('sinov_model.joblib') print('telefon_takrorlanishi oshgan sari:') oldingi = -1 buzildi = False for t in (1, 3, 6, 10, 15, 20): e = ehtimol(m, telefon_takrorlanishi=t) belgi = '' if e >= oldingi else ' <- KAMAYDI' if e < oldingi: buzildi = True print(f' telefon={t:3} ehtimol={e:.3f}{belgi}') oldingi = e print() print('monoton oshdimi:', not buzildi) "
telefon_takrorlanishi oshgan sari: telefon= 1 ehtimol=0.052 telefon= 3 ehtimol=0.040 <- KAMAYDI telefon= 6 ehtimol=0.460 telefon= 10 ehtimol=0.650 telefon= 15 ehtimol=0.645 <- KAMAYDI telefon= 20 ehtimol=0.645 monoton oshdimi: False
python3 -c " import sys; sys.path.insert(0, 'src') import joblib from quvur.sinov_yordam import ehtimol m = joblib.load('sinov_model.joblib') print('tavsif uzaygan sari (teskari yo\'nalish kutiladi):') for t in (50, 150, 300, 500, 800): print(f' tavsif={t:4} ehtimol={ehtimol(m, tavsif_uzunligi=t):.3f}') "
tavsif uzaygan sari (teskari yo'nalish kutiladi): tavsif= 50 ehtimol=0.091 tavsif= 150 ehtimol=0.056 tavsif= 300 ehtimol=0.052 tavsif= 500 ehtimol=0.042 tavsif= 800 ehtimol=0.053
Natijani diqqat bilan o'qing — u kutilganidek emas.
Telefon 1 dan 3 ga oshganda ehtimol kamaydi (0.052 → 0.040), 10 dan 15 ga oshganda yana kamaydi. Umumiy tendensiya to'g'ri (0.052 dan 0.645 ga), lekin yo'l monoton emas.
Tavsifda ham xuddi shunday: 500 dan 800 ga o'tganda ehtimol biroz oshdi (0.042 → 0.053).
Sabab: daraxt modeli monoton bo'lishga majbur emas. U ma'lumotni bo'laklarga bo'ladi va har bo'lakda o'z qaroriga keladi. Oraliqda kichik tebranish — normal hodisa.
Shuning uchun testni qadam-baqadam monotonlikka emas, umumiy tendensiyaga qurish kerak:
birinchi va oxirgi qiymatni solishtirish. Yuqoridagi yonalish funksiyasi aynan shunday yozilgan.
Agar umumiy tendensiya ham teskari bo'lsa — o'shanda model mantiqni emas, tasodifiy naqshni o'rgangan bo'ladi va bu haqiqiy muammo.
Yo'nalish testini to'g'ri e'londan boshlang
Yuqoridagi tavsif o'lchovida ehtimol 0.09 dan 0.05 gacha tushdi — atigi 0.04. Sabab: boshlang'ich e'lon allaqachon aniq ega (telefon=1, tasdiqlangan=1), ehtimol nolga yaqin va tushishga joy yo'q.
Yo'nalish testini shubhali e'londan boshlang — o'shanda belgining ta'siri ko'rinadi. Aks holda test modelni emas, boshlang'ich nuqtani sinaydi.
3. O'zgarmaslik testi
Ba'zi belgilar natijaga ta'sir qilmasligi kerak. Masalan qavat raqami brokerlikni belgilamaydi:
python3 -c " import sys; sys.path.insert(0, 'src') import joblib from quvur.sinov_yordam import ehtimol m = joblib.load('sinov_model.joblib') qiymatlar = [ehtimol(m, qavat=q, jami_qavat=12) for q in (1, 3, 5, 8, 11)] print('qavat o\'zgarganda ehtimol:', [round(v, 3) for v in qiymatlar]) tarqalish = max(qiymatlar) - min(qiymatlar) print(f'tarqalish: {tarqalish:.3f}') print('o\'zgarmaslik o\'tdimi (< 0.15):', tarqalish < 0.15) "
qavat o'zgarganda ehtimol: [0.08, 0.032, 0.027, 0.018, 0.043] tarqalish: 0.062 o'zgarmaslik o'tdimi (< 0.15): True
Agar qavat natijani keskin o'zgartirsa, model ahamiyatsiz belgiga tayanayapti — bu qayta moslashish (overfit) belgisi.
4. Regressiya testi
Eng muhimi: yangi model eskisidan yomon bo'lmasin.
Bu oddiy ko'rinadi — F1 ni solishtirasiz, tamom. Lekin bir tuzoq bor.
Tasavvur qiling, yangi model F1 ni 0.860 dan 0.865 ga ko'tardi. Yaxshi, chiqaramiz.
Lekin o'rtacha ko'tarilgani hamma joyda ko'tarilgan degani emas. Yangi model Yunusobodda yaxshiroq, Chilonzorda esa yomonroq bo'lishi mumkin. O'rtachada bu ko'rinmaydi.
Shifokor misolini eslang: "o'rtacha harorat normal" degan gap bitta bemor isitmalab, ikkinchisi muzlab yotganini yashiradi.
Shuning uchun regressiya testi ikki darajali bo'lishi kerak:
- Umumiy metrika tushmadimi?
- Alohida guruhlarda metrika tushmadimi?
Ikkinchisi muhimroq, chunki foydalanuvchi o'rtachada yashamaydi — u aniq bir tumanda yashaydi.
Va yana bir narsa: taqqoslash uchun bir xil test to'plami kerak. Har safar boshqa namuna olsangiz, farq modeldan emas, namunadan kelib chiqadi (2-bobdagi seed muammosi yana qaytadi).
shu faylni yaratingsrc/quvur/model_test.py
"""Model testlari — pytest bilan ishlatiladi.""" import json from pathlib import Path from typing import Any import pandas as pd from .sinov_yordam import ehtimol from .xususiyat import tayyorla def minimal_funksionallik(model) -> tuple[bool, str]: ega = ehtimol(model, telefon_takrorlanishi=1, kunlik_elonlar=1, tavsif_uzunligi=600, rasmlar_soni=15, egasi_tasdiqlangan=1) brok = ehtimol(model, telefon_takrorlanishi=18, kunlik_elonlar=9, tavsif_uzunligi=60, rasmlar_soni=2, egasi_tasdiqlangan=0) ok = ega < 0.3 and brok > 0.7 return ok, f"ega={ega:.3f} broker={brok:.3f}" def yonalish(model, belgi: str, qiymatlar: list, oshishi_kerak: bool, eng_kam_farq: float = 0.05, **asos: Any) -> tuple[bool, str]: """UMUMIY tendensiyani tekshiradi, qadam-baqadam monotonlikni EMAS. Daraxt modellari monoton bo'lishga majbur emas — oraliqda tebranish normal. Shuning uchun birinchi va oxirgi qiymat solishtiriladi. `asos` — boshlang'ich e'lonni o'zgartiradi. Bu MUHIM: agar e'lon allaqachon aniq ega bo'lsa, ehtimol 0.05 atrofida bo'ladi va tushishga JOY QOLMAYDI. """ e = [ehtimol(model, **{**asos, belgi: q}) for q in qiymatlar] farq = e[-1] - e[0] ok = farq >= eng_kam_farq if oshishi_kerak else farq <= -eng_kam_farq yonalish_matni = "oshdi" if farq > 0 else "kamaydi" return ok, f"{belgi}: {[round(v, 3) for v in e]} ({yonalish_matni} {abs(farq):.3f})" def ozgarmaslik(model, belgi: str, qiymatlar: list, chegara: float = 0.15) -> tuple[bool, str]: e = [ehtimol(model, **{belgi: q}) for q in qiymatlar] t = max(e) - min(e) return t < chegara, f"{belgi} tarqalishi={t:.3f}" def regressiya(yangi_metrikalar: dict[str, float], asos_yoli: str | Path, ruxsat: float = 0.01) -> tuple[bool, str]: """Yangi model asosdan `ruxsat` dan ko'proq yomon bo'lmasin.""" p = Path(asos_yoli) if not p.exists(): p.write_text(json.dumps(yangi_metrikalar, indent=2), encoding="utf-8") return True, "asos yaratildi (birinchi marta)" asos = json.loads(p.read_text(encoding="utf-8")) yomonlashgan = [] for k, eski in asos.items(): yangi = yangi_metrikalar.get(k) if yangi is not None and yangi < eski - ruxsat: yomonlashgan.append(f"{k}: {eski:.4f} -> {yangi:.4f}") return (not yomonlashgan), ("; ".join(yomonlashgan) or "hammasi asos darajasida")
python3 -c " import sys; sys.path.insert(0, 'src') import joblib, json from quvur.model_test import regressiya m = joblib.load('sinov_model.joblib') # asos yaratamiz print('1)', regressiya({'f1': 0.8595, 'toliqlik': 0.8285}, 'asos.json')) # bir xil natija — o'tadi print('2)', regressiya({'f1': 0.8595, 'toliqlik': 0.8285}, 'asos.json')) # biroz yaxshiroq — o'tadi print('3)', regressiya({'f1': 0.8700, 'toliqlik': 0.8400}, 'asos.json')) # sezilarli yomonroq — YIQILADI print('4)', regressiya({'f1': 0.8100, 'toliqlik': 0.7900}, 'asos.json')) "
1) (True, 'asos yaratildi (birinchi marta)') 2) (True, 'hammasi asos darajasida') 3) (True, 'hammasi asos darajasida') 4) (False, 'f1: 0.8595 -> 0.8100; toliqlik: 0.8285 -> 0.7900')
To'rtinchi holat yiqildi va qaysi metrika qancha tushganini aytdi. Bu CI da PR ni to'xtatadi (31-bob).
Hammasini pytest ga yig'amiz
shu faylni yaratingtests/test_model.py
"""Model testlari. `pytest tests/test_model.py -v` bilan ishlaydi.""" import sys from pathlib import Path import joblib import pytest sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src")) from quvur.model_test import minimal_funksionallik, ozgarmaslik, yonalish @pytest.fixture(scope="module") def model(): yol = Path(__file__).resolve().parents[1] / "sinov_model.joblib" if not yol.exists(): pytest.skip("model topilmadi — avval o'qitish quvurini ishga tushiring") return joblib.load(yol) def test_minimal_funksionallik(model): ok, tafsilot = minimal_funksionallik(model) assert ok, f"aniq holatlar noto'g'ri: {tafsilot}" def test_telefon_kop_bolsa_broker_ehtimoli_oshadi(model): ok, tafsilot = yonalish(model, "telefon_takrorlanishi", [1, 5, 10, 18], oshishi_kerak=True) assert ok, f"yo'nalish buzildi: {tafsilot}" def test_tavsif_uzun_bolsa_broker_ehtimoli_kamayadi(model): # Shubhali e'londan boshlaymiz: aniq egada ehtimol allaqachon ~0.05, # tushishga joy qolmaydi va test sababsiz yiqiladi. ok, tafsilot = yonalish(model, "tavsif_uzunligi", [60, 200, 400, 700], oshishi_kerak=False, telefon_takrorlanishi=7, egasi_tasdiqlangan=0, rasmlar_soni=3, kunlik_elonlar=4) assert ok, f"yo'nalish buzildi: {tafsilot}" def test_qavat_natijaga_tasir_qilmaydi(model): ok, tafsilot = ozgarmaslik(model, "qavat", [1, 4, 7, 10]) assert ok, f"ahamiyatsiz belgiga tayanmoqda: {tafsilot}" @pytest.mark.parametrize("belgi, qiymat", [ ("maydon", 30.0), ("maydon", 120.0), ("xonalar", 1), ("xonalar", 4), ]) def test_ehtimol_oraliqda(model, belgi, qiymat): from quvur.sinov_yordam import ehtimol e = ehtimol(model, **{belgi: qiymat}) assert 0.0 <= e <= 1.0
python3 -m pytest tests/test_model.py -v 2>&1 | tail -16
cachedir: .pytest_cache rootdir: /mnt/c/Users/Admin/Desktop/Course Material/MLOps_Kitob/ish plugins: cov-7.0.0, hydra-core-1.3.2, langsmith-0.5.1, asyncio-1.3.0, anyio-4.12.0, typeguard-4.4.4 asyncio: mode=strict, debug=False, asyncio_default_fixture_loop_scope=None, asyncio_default_test_loop_scope=function collecting ... collected 8 items tests/test_model.py::test_minimal_funksionallik PASSED [ 12%] tests/test_model.py::test_telefon_kop_bolsa_broker_ehtimoli_oshadi PASSED [ 25%] tests/test_model.py::test_tavsif_uzun_bolsa_broker_ehtimoli_kamayadi PASSED [ 37%] tests/test_model.py::test_qavat_natijaga_tasir_qilmaydi PASSED [ 50%] tests/test_model.py::test_ehtimol_oraliqda[maydon-30.0] PASSED [ 62%] tests/test_model.py::test_ehtimol_oraliqda[maydon-120.0] PASSED [ 75%] tests/test_model.py::test_ehtimol_oraliqda[xonalar-1] PASSED [ 87%] tests/test_model.py::test_ehtimol_oraliqda[xonalar-4] PASSED [100%] ============================== 8 passed in 1.24s ===============================
Nima ushlanadi, nima yo'q
import pandas as pd show(pd.DataFrame([ {"nosozlik": "Ustun tartibi buzildi (3-bob)", "qaysi test ushlaydi": "Minimal funksionallik"}, {"nosozlik": "Nishon teskari o'rgatildi", "qaysi test ushlaydi": "Minimal funksionallik"}, {"nosozlik": "Model ahamiyatsiz belgiga tayanadi", "qaysi test ushlaydi": "O'zgarmaslik"}, {"nosozlik": "Mantiq teskari o'rganildi", "qaysi test ushlaydi": "Yo'nalish"}, {"nosozlik": "Yangi model yomonroq", "qaysi test ushlaydi": "Regressiya"}, {"nosozlik": "Ma'lumot drifti", "qaysi test ushlaydi": "HECH BIRI — 35-bob kerak"}, ]))
| nosozlik | qaysi test ushlaydi |
|---|---|
| Ustun tartibi buzildi (3-bob) | Minimal funksionallik |
| Nishon teskari o'rgatildi | Minimal funksionallik |
| Model ahamiyatsiz belgiga tayanadi | O'zgarmaslik |
| Mantiq teskari o'rganildi | Yo'nalish |
| Yangi model yomonroq | Regressiya |
| Ma'lumot drifti | HECH BIRI — 35-bob kerak |
Oxirgi qator muhim: model testlari o'qitish paytida ishlaydi. Drift esa ishlab chiqarishda, oylar o'tib yuz beradi — uni faqat kuzatuv ushlaydi.
Yo'nalish testini juda qat'iy qilmang
Yuqorida qadam-baqadam monotonlik o'rniga umumiy tendensiya tekshiriladi. Sababi yuqorida o'lchandi: daraxt modelida telefon 1→3 da ehtimol kamaydi, garchi umumiy yo'nalish to'g'ri bo'lsa ham.
Juda qat'iy qilsangiz, test har qayta o'qitishda tasodifan yiqiladi va jamoa uni o'chirib qo'yadi — bu eng yomon natija. Test ishonchli bo'lishi, "mukammal" bo'lishidan muhimroq.
Xulosa
| Tushuncha | Asosiy fikr |
|---|---|
| Uch xil test | Kod, ma'lumot, model — uchinchisi eng kam yoziladi |
| Minimal funksionallik | Aniq holatlar; eng ko'p nosozlikni ushlaydi |
| Yo'nalish | Belgining ta'siri mantiqqa mosmi |
| O'zgarmaslik | Ahamiyatsiz belgi natijani o'zgartirmasin |
| Regressiya | Yangi model asosdan yomon bo'lmasin; qaysi metrika tushganini aytsin |
| Bag'rikenglik | +0.02 — beqaror test o'chirib tashlanadi |
| Chegara | Model testlari drift ni ushlamaydi (35-bob) |
Talaba ishi
tests/test_model.pyga yangi yo'nalish testi qo'shing:egasi_tasdiqlangan=1bo'lsa broker ehtimoli kamayishi kerak. O'tadimi?- Modelni ataylab buzing:
tayyorlada ikki ustunni almashtiring. Qaysi test birinchi yiqiladi? regressiyaga "ruxsat" ni 0.0 qiling. Testlar beqaror bo'lib qoladimi? Nega?ozgarmaslikchegarasini 0.05 qiling. O'tadimi? Agar yo'q bo'lsa — bu model muammosimi yoki test muammosimi?
Bu bobni bepul o'qidingiz.
Kitob o'rgatadi, lekin sizning kodingiz nega ishlamaganini ayta olmaydi. Kursda uy vazifangiz tekshiriladi.