CI: modelni tekshiradigan darvoza
Juma kuni soat 17:50
Hamkasbingiz xususiyat kodiga kichik o'zgarish kiritdi — narx_m2 hisobida bitta qavs.
Testlar o'tdi. Kod ko'rib chiqildi. Birlashtirildi. Hamma uyga ketdi.
Dushanba kuni hisobotda broker aniqlash F1 0.86 dan 0.71 ga tushgani ko'rinadi.
Endi savol: nega hech kim to'xtatmadi?
Chunki CI faqat kodni tekshirgan edi. pytest yashil, ruff toza, mypy rozi. Kod
mukammal ishlayapti — u shunchaki boshqa narsani hisoblaydi.
25-bobdagi tarozini eslang: u yonadi, raqam ko'rsatadi, xato bermaydi. Faqat raqam noto'g'ri.
CI nima va nega u yetarli emas
CI (Continuous Integration) — har o'zgarishda avtomatik tekshiruv. Siz kod yuborasiz, server uni oladi, testlarni chopadi, natijani aytadi.
Oddiy dasturda bu yetarli: kod to'g'ri bo'lsa, dastur to'g'ri ishlaydi.
ML da esa 1-bobdagi uchlik bor — kod, ma'lumot, model. CI odatda faqat birinchisini ko'radi.
import pandas as pd show(pd.DataFrame([ {"Tekshiruv": "ruff / mypy", "Nimani ushlaydi": "Uslub, tur xatosi", "F1 tushishini ushlaydimi": "Yo'q"}, {"Tekshiruv": "pytest (kod)", "Nimani ushlaydi": "Funksiya noto'g'ri qiymat qaytarsa", "F1 tushishini ushlaydimi": "Yo'q"}, {"Tekshiruv": "Ma'lumot sxemasi (6-bob)", "Nimani ushlaydi": "Buzuq kirish", "F1 tushishini ushlaydimi": "Ba'zan"}, {"Tekshiruv": "Model testlari (25-bob)", "Nimani ushlaydi": "Mantiqsiz xulq", "F1 tushishini ushlaydimi": "Ba'zan"}, {"Tekshiruv": "**Sifat darvozasi**", "Nimani ushlaydi": "F1 belgilangan chegaradan past", "F1 tushishini ushlaydimi": "**Ha**"}, ]))
| Tekshiruv | Nimani ushlaydi | F1 tushishini ushlaydimi |
|---|---|---|
| ruff / mypy | Uslub, tur xatosi | Yo'q |
| pytest (kod) | Funksiya noto'g'ri qiymat qaytarsa | Yo'q |
| Ma'lumot sxemasi (6-bob) | Buzuq kirish | Ba'zan |
| Model testlari (25-bob) | Mantiqsiz xulq | Ba'zan |
| **Sifat darvozasi** | F1 belgilangan chegaradan past | **Ha** |
Oxirgi qator — bu bobning mavzusi. Qolgan to'rttasi ham kerak, lekin ularning hech biri "model yomonlashdi" degan savolga javob bermaydi.
Darvoza qanday ishlaydi
24-bobda quvurga sifat sharti qo'ygandik: F1 chegaradan past bo'lsa, model registrga chiqmaydi.
CI da ham aynan shu, faqat boshqa joyda: model registrga emas, kod asosiy tarmoqqa chiqmaydi.
diagramma( { "O'zgarish\n(pull request)": (0, 1.8, 2.3, 1.0, "manba"), "Kod tekshiruvi\n(ruff, mypy, pytest)": (3.1, 1.8, 2.6, 1.0, "qayta"), "Model o'qitish\n(kichik namuna)": (6.3, 1.8, 2.4, 1.0, "qayta"), "SIFAT DARVOZASI\nF1 >= chegara": (9.3, 1.8, 2.5, 1.0, "kuzatuv"), "Birlashtirish\nmumkin": (12.4, 3.1, 2.2, 0.9, "xizmat"), "RAD ETILDI": (12.4, 0.5, 2.2, 0.9, "manba"), }, [ ("O'zgarish\n(pull request)", "Kod tekshiruvi\n(ruff, mypy, pytest)"), ("Kod tekshiruvi\n(ruff, mypy, pytest)", "Model o'qitish\n(kichik namuna)"), ("Model o'qitish\n(kichik namuna)", "SIFAT DARVOZASI\nF1 >= chegara"), ("SIFAT DARVOZASI\nF1 >= chegara", "Birlashtirish\nmumkin", "o'tdi"), ("SIFAT DARVOZASI\nF1 >= chegara", "RAD ETILDI", "past"), ], nom="ci_darvoza.png", kenglik=15, izoh="Kod tekshiruvi birinchi (arzon), model o'qitish keyin (qimmat).", )

Kod tekshiruvi birinchi (arzon), model o'qitish keyin (qimmat).
Tartib muhim: arzon tekshiruv oldin. ruff bir soniya, model o'qitish bir necha
daqiqa. Kod uslubi buzuq bo'lsa, model o'qitishga vaqt sarflash ma'nosiz.
Darvoza kodi
shu faylni yaratingsrc/quvur/darvoza.py
"""CI uchun sifat darvozasi: model chegaradan past bo'lsa, chiqish kodi 1.""" import json from dataclasses import dataclass, asdict from pathlib import Path from typing import Any @dataclass(frozen=True, slots=True) class Chegara: """CI shartlari. Qiymatlar kodda emas, faylda saqlanadi.""" eng_kam_f1: float = 0.82 eng_kam_aniqlik: float = 0.80 eng_kop_tushish: float = 0.02 # avvalgi modeldan qancha past bo'lishi mumkin def asos_oqi(yol: str | Path) -> dict[str, float] | None: """Avvalgi model natijasi. Birinchi marta bo'lsa — None.""" p = Path(yol) return json.loads(p.read_text()) if p.exists() else None def darvoza(natija: dict[str, float], ch: Chegara, asos: dict[str, float] | None = None) -> tuple[bool, list[str]]: """Tekshiradi va SABABLARNI qaytaradi. Faqat True/False qaytarish yetarli emas — CI jurnalida NEGA rad etilgani yozilishi kerak. """ sabablar = [] if natija["f1"] < ch.eng_kam_f1: sabablar.append(f"F1 {natija['f1']:.4f} < chegara {ch.eng_kam_f1}") if natija["aniqlik"] < ch.eng_kam_aniqlik: sabablar.append(f"aniqlik {natija['aniqlik']:.4f} < {ch.eng_kam_aniqlik}") if asos is not None: tushish = asos["f1"] - natija["f1"] if tushish > ch.eng_kop_tushish: sabablar.append( f"avvalgidan {tushish:.4f} past (ruxsat {ch.eng_kop_tushish})") return len(sabablar) == 0, sabablar def hisobot(natija: dict[str, float], ok: bool, sabablar: list[str]) -> str: """CI jurnaliga tushadigan matn.""" q = ["=" * 46, "SIFAT DARVOZASI", "=" * 46] for k, v in natija.items(): q.append(f" {k:12s}: {v:.4f}") q.append("-" * 46) q.append(" NATIJA: O'TDI" if ok else " NATIJA: RAD ETILDI") q.extend(f" - {s}" for s in sabablar) return "\n".join(q)
CI skripti
shu faylni yaratingci_tekshir.py
"""CI da ishlaydigan skript. Chiqish kodi 0 = o'tdi, 1 = rad etildi.""" import json import sys from pathlib import Path sys.path.insert(0, "src") import pandas as pd from sklearn.ensemble import RandomForestClassifier from sklearn.metrics import f1_score, precision_score, recall_score from sklearn.model_selection import train_test_split from quvur.darvoza import Chegara, asos_oqi, darvoza, hisobot from quvur.xususiyat import tayyorla ASOS = "ci_asos.json" def oqit_va_bahola(csv: str, namuna: int | None = None) -> dict[str, float]: d = pd.read_csv(csv) if namuna: d = d.sample(n=min(namuna, len(d)), random_state=42) x, y = tayyorla(d), d["broker"] Xtr, Xte, ytr, yte = train_test_split( x, y, test_size=0.25, stratify=y, random_state=42) m = RandomForestClassifier(n_estimators=100, min_samples_leaf=3, random_state=42, n_jobs=1).fit(Xtr, ytr) p = m.predict(Xte) return { "f1": float(f1_score(yte, p)), "aniqlik": float(precision_score(yte, p)), "toliqlik": float(recall_score(yte, p)), } def main() -> int: natija = oqit_va_bahola("../datasets/ijara.csv") ok, sabablar = darvoza(natija, Chegara(), asos_oqi(ASOS)) print(hisobot(natija, ok, sabablar)) if ok: Path(ASOS).write_text(json.dumps(natija)) return 0 if ok else 1 if __name__ == "__main__": sys.exit(main())
Eng muhim qator — oxirgisi: sys.exit(0 yoki 1).
CI tizimi sizning matningizni o'qimaydi. U faqat chiqish kodiga qaraydi. 0 — o'tdi, boshqa har qanday son — xato.
Yaxshi model: darvoza o'tkazadi
python3 ci_tekshir.py echo "" echo "chiqish kodi: $?"
============================================== SIFAT DARVOZASI ============================================== f1 : 0.8603 aniqlik : 0.8947 toliqlik : 0.8285 ---------------------------------------------- NATIJA: O'TDI chiqish kodi: 0
Buzilgan o'zgarish: darvoza to'xtatadi
Endi bob boshidagi holatni takrorlaymiz — kimdir xususiyat kodini "yaxshilaydi".
python3 -c " import sys, json; sys.path.insert(0, 'src') import pandas as pd from sklearn.ensemble import RandomForestClassifier from sklearn.metrics import f1_score, precision_score, recall_score from sklearn.model_selection import train_test_split from quvur.darvoza import Chegara, asos_oqi, darvoza, hisobot from quvur.xususiyat import XOM_USTUNLAR # BUZILGAN tayyorlash: narx_m2 da xato (1000 ga bo'lish unutildi) def buzuq_tayyorla(d): x = d[XOM_USTUNLAR].copy() x['narx_m2'] = x['narx_mln'] / x['maydon'] # 1000 YO'Q x['qavat_nisbati'] = x['qavat'] / x['jami_qavat'] x['rasm_tavsif'] = x['rasmlar_soni'] * x['tavsif_uzunligi'] return x d = pd.read_csv('../datasets/ijara.csv') x, y = buzuq_tayyorla(d), d['broker'] Xtr, Xte, ytr, yte = train_test_split(x, y, test_size=0.25, stratify=y, random_state=42) m = RandomForestClassifier(n_estimators=100, min_samples_leaf=3, random_state=42, n_jobs=1).fit(Xtr, ytr) p = m.predict(Xte) natija = {'f1': float(f1_score(yte,p)), 'aniqlik': float(precision_score(yte,p)), 'toliqlik': float(recall_score(yte,p))} ok, sabablar = darvoza(natija, Chegara(), asos_oqi('ci_asos.json')) print(hisobot(natija, ok, sabablar)) print() print('CI chiqish kodi boladi:', 0 if ok else 1) "
============================================== SIFAT DARVOZASI ============================================== f1 : 0.8573 aniqlik : 0.8892 toliqlik : 0.8276 ---------------------------------------------- NATIJA: O'TDI CI chiqish kodi boladi: 0
Natijaga qarang. Ikki holat bo'lishi mumkin va ikkalasi ham foydali:
Agar F1 chegaradan pastga tushgan bo'lsa — darvoza rad etdi, ish tugadi.
Agar F1 deyarli o'zgarmagan bo'lsa — bu ham ma'lumot: demak o'sha xususiyat modelga deyarli ta'sir qilmasdi. 2-bobdagi saboq qaytadi: siz "yaxshilagan" narsa haqiqatan foyda berayaptimi?
Haqiqiy buzilish: darvoza ishga tushadi
Yuqoridagi xato yetarlicha katta emas edi. Endi haqiqatan buzilgan holatni ko'ramiz — ma'lumot manbasi aralashib ketgan (yorliqlar boshqa qatorga tushgan).
python3 -c " import sys; sys.path.insert(0, 'src') import numpy as np, pandas as pd from sklearn.ensemble import RandomForestClassifier from sklearn.metrics import f1_score, precision_score, recall_score from sklearn.model_selection import train_test_split from quvur.darvoza import Chegara, asos_oqi, darvoza, hisobot from quvur.xususiyat import tayyorla d = pd.read_csv('../datasets/ijara.csv') x = tayyorla(d) # BUZILISH: yorliqlar aralashib ketdi rng = np.random.default_rng(42) y = pd.Series(rng.permutation(d['broker'].values), index=d.index) Xtr, Xte, ytr, yte = train_test_split(x, y, test_size=0.25, stratify=y, random_state=42) m = RandomForestClassifier(n_estimators=100, min_samples_leaf=3, random_state=42, n_jobs=1).fit(Xtr, ytr) p = m.predict(Xte) natija = {'f1': float(f1_score(yte,p)), 'aniqlik': float(precision_score(yte,p)), 'toliqlik': float(recall_score(yte,p))} ok, sabablar = darvoza(natija, Chegara(), asos_oqi('ci_asos.json')) print(hisobot(natija, ok, sabablar)) print() print('CI chiqish kodi:', 0 if ok else 1) print('pull request:', 'birlashtiriladi' if ok else 'BLOKLANDI') "
==============================================
SIFAT DARVOZASI
==============================================
f1 : 0.0696
aniqlik : 0.3694
toliqlik : 0.0384
----------------------------------------------
NATIJA: RAD ETILDI
- F1 0.0696 < chegara 0.82
- aniqlik 0.3694 < 0.8
- avvalgidan 0.7907 past (ruxsat 0.02)
CI chiqish kodi: 1
pull request: BLOKLANDI
Mana darvoza ishlagani. Uchta narsaga e'tibor bering:
Chiqish kodi 1 — CI bu buildni qizil qiladi va pull request birlashtirilmaydi.
Sabablar yozilgan — "rad etildi" emas, balki qaysi shart, qanday qiymat bilan buzilgani. Ertalab jurnalga qaragan odam darhol tushunadi.
Hech kim qo'lda tekshirmadi. Juma kuni soat 17:50 da ham darvoza o'sha ishni qiladi.
Absolut chegara yetarli emas
f1 >= 0.82 yozish oson, lekin uning bitta kamchiligi bor.
Faraz qiling model asta-sekin yomonlashmoqda: 0.88 → 0.86 → 0.84 → 0.83. Har safar chegaradan yuqori, demak darvoza har safar o'tkazadi.
Olti oydan keyin siz 0.83 da turasiz va buni hech kim sezmagan — chunki hech bir qadam chegarani buzmagan.
Shuning uchun eng_kop_tushish ham bor: avvalgi model bilan taqqoslash. Bitta qadamda
0.02 dan ko'p tushsa — to'xtatadi, hatto chegaradan yuqori bo'lsa ham.
Ikkalasi birga kerak: absolut chegara qanchalik yomon ekanini, nisbiy chegara esa qancha yomonlashganini ushlaydi.
GitHub Actions fayli
shu faylni yarating.github/workflows/ci.yml
name: CI
on:
pull_request:
push:
branches: [main]
jobs:
tekshir:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.11"
cache: pip
- name: Bogliqliklar
run: pip install -r talablar.txt
# ARZON tekshiruvlar oldin
- name: Uslub va turlar
run: |
ruff check src/
mypy src/
- name: Kod testlari
run: pytest tests/ -q
# QIMMAT tekshiruv keyin
- name: Sifat darvozasi
run: python3 ci_tekshir.py
- name: Natijani saqlash
if: always()
uses: actions/upload-artifact@v4
with:
name: sifat-hisoboti
path: ci_asos.json
Uch narsaga e'tibor bering.
Tartib — ruff va pytest oldin, model o'qitish keyin. Arzon xato tez topiladi.
if: always() — hisobot darvoza rad etganda ham saqlanadi. Aks holda eng kerakli
paytda ma'lumot yo'qoladi.
cache: pip — 7-bobdagi qatlam keshi g'oyasi, faqat CI da. Bog'liqliklar har safar
qaytadan yuklanmaydi.
CI da model o'qitish — vaqt va pul
Har pull request da to'liq model o'qitish qimmatga tushadi. 12 ming qatorda bu bir necha soniya, 10 million qatorda esa yarim soat.
Amaliy yechimlar:
- Kichik namuna: CI da 10% ma'lumotda o'qiting. Chegarani ham shunga moslang — kichik namunada F1 tabiiy ravishda pastroq.
- Faqat kerakli paytda:
paths:filtri bilan — model kodi o'zgarmasa, darvozani o'tkazib yuboring. - Tungi to'liq tekshiruv: PR da tez, kechasi to'liq.
Muhimi: darvozani butunlay o'chirib qo'ymang. Sekin darvoza darvozasizlikdan yaxshiroq.
Nima ushlanadi, nima yo'q
import pandas as pd show(pd.DataFrame([ {"Muammo": "Xususiyat hisobida xato", "CI ushlaydimi": "Ha — F1 tushadi"}, {"Muammo": "Ma'lumot manbasi buzildi", "CI ushlaydimi": "Ha — sxema yoki F1"}, {"Muammo": "Model sekinlashdi", "CI ushlaydimi": "Yo'q — tezlik o'lchanmayapti"}, {"Muammo": "Ishlab chiqarishda drift", "CI ushlaydimi": "Yo'q — 35-bob"}, {"Muammo": "Yangi ma'lumot kelmadi", "CI ushlaydimi": "Yo'q — 34-bob"}, ]))
| Muammo | CI ushlaydimi |
|---|---|
| Xususiyat hisobida xato | Ha — F1 tushadi |
| Ma'lumot manbasi buzildi | Ha — sxema yoki F1 |
| Model sekinlashdi | Yo'q — tezlik o'lchanmayapti |
| Ishlab chiqarishda drift | Yo'q — 35-bob |
| Yangi ma'lumot kelmadi | Yo'q — 34-bob |
CI — bu chiqishdan oldingi to'siq. U kod bilan kelgan muammoni ushlaydi.
Ishlab chiqarishda o'z-o'zidan paydo bo'ladigan muammolarni esa (drift, manba to'xtashi) u ko'rmaydi. Ular uchun monitoring kerak — 34 va 35-boblar.
Xulosa
| Tushuncha | Asosiy fikr |
|---|---|
| Oddiy CI | Kodni tekshiradi; ML da bu yetarli emas |
| Sifat darvozasi | Model chegaradan past bo'lsa, birlashtirmaydi |
| Chiqish kodi | CI matnni emas, 0 yoki 1 ni o'qiydi |
| Sabablarni yozish | "Rad etildi" yetarli emas — nega kerak |
| Tartib | Arzon tekshiruv oldin, model o'qitish keyin |
| Absolut chegara | "Qanchalik yomon" ni ushlaydi |
| Nisbiy chegara | "Qancha yomonlashdi" ni ushlaydi — sekin pasayish uchun |
if: always() |
Hisobot rad etilganda ham saqlansin |
| CI ko'rmaydigan narsa | Drift va manba to'xtashi — monitoring ishi |
Talaba ishi
eng_kop_tushishni 0.001 qiling. Yaxshi model ham rad etiladimi? Nega bu yomon?- Darvozaga tezlik sharti qo'shing (29-bob): p99 belgilangan chegaradan past bo'lsin.
ci_asos.jsonni o'chiring va skriptni qayta ishga tushiring. Nisbiy tekshiruv nima qildi?- CI ni 10% namunada ishlatadigan qiling. F1 qancha o'zgardi va chegarani qanday moslash kerak?
- Model testlarini (25-bob) CI ga qo'shing. Ular darvozadan oldin turishi kerakmi yoki keyin? Nega?
Bu bobni bepul o'qidingiz.
Kitob o'rgatadi, lekin sizning kodingiz nega ishlamaganini ayta olmaydi. Kursda uy vazifangiz tekshiriladi.