Asosiy qismga o'tish

CI: modelni tekshiradigan darvoza

31-bob 9 kod bloki 2436 so'z

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

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.

python13 qator
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**"},
]))
TekshiruvNimani ushlaydiF1 tushishini ushlaydimi
ruff / mypyUslub, tur xatosiYo'q
pytest (kod)Funksiya noto'g'ri qiymat qaytarsaYo'q
Ma'lumot sxemasi (6-bob)Buzuq kirishBa'zan
Model testlari (25-bob)Mantiqsiz xulqBa'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.

python19 qator
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).

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

python50 qator
"""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

python46 qator
"""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

matn3 qator
python3 ci_tekshir.py
echo ""
echo "chiqish kodi: $?"
Chiqish (qaytish kodi 0, 1599 ms)
==============================================
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".

python30 qator
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)
"
Chiqish (qaytish kodi 0, 1641 ms)
==============================================
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).

python27 qator
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')
"
Chiqish (qaytish kodi 0, 2013 ms)
==============================================
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

yaml40 qator
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

python8 qator
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"},
]))
MuammoCI ushlaydimi
Xususiyat hisobida xatoHa — F1 tushadi
Ma'lumot manbasi buzildiHa — sxema yoki F1
Model sekinlashdiYo'q — tezlik o'lchanmayapti
Ishlab chiqarishda driftYo'q — 35-bob
Yangi ma'lumot kelmadiYo'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

  1. eng_kop_tushish ni 0.001 qiling. Yaxshi model ham rad etiladimi? Nega bu yomon?
  2. Darvozaga tezlik sharti qo'shing (29-bob): p99 belgilangan chegaradan past bo'lsin.
  3. ci_asos.json ni o'chiring va skriptni qayta ishga tushiring. Nisbiy tekshiruv nima qildi?
  4. CI ni 10% namunada ishlatadigan qiling. F1 qancha o'zgardi va chegarani qanday moslash kerak?
  5. 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.

Telegramda yozish Narxlar