IlmHamroh
Data Science va sun'iy intellekt/MLOps va deploy5/14-dars41 daqiqa
Mundarija (25)

27.5-dars: Model versiyalash va registr

27-QISM — MLOPS VA DEPLOY · 5-dars


1. Kirish va motivatsiya

Oldingi darsda 27.4-bob har o'rgatish yurishini eksperiment kuzatuvi jurnaliga yozdik: run_id, parametrlar, metrikalar, artefaktlar. Bu savolga javob beradi: "qaysi tajriba eng yaxshi edi?". Lekin ishlab chiqarishda boshqa savollar tug'iladi:

  • Hozir mijozlarga qaysi model javob beryapti?
  • U qaysi ma'lumotda, qaysi kod bilan o'rgatilgan?
  • Uni kim, qachon va nima asosida ishlab chiqarishga chiqargan?
  • Yangi model muammo chiqarsa, bir buyruq bilan oldingisiga qaytish mumkinmi?

Bu savollarga javob beradigan joy — model registr (model registry). Eksperiment kuzatuvi — laboratoriya daftari: yuzlab tajriba, ko'pi tashlab yuboriladi. Registr — omborxona: faqat nomlangan, versiyalangan, tekshirilgan modellar, har biri aniq bosqichda (Staging, Production, Archived) va har bir o'tish jurnalda.

Registrning poydevori — versiyalash. Model uchta narsaning mahsuli: kod, ma'lumot va konfiguratsiya (muhit bilan birga — 27.2). Ulardan birortasi versiyalanmagan bo'lsa, model versiyasi yarim haqiqat: "v3" deymiz, lekin v3 ni qayta qura olmaymiz.

Real vaziyat. Kredit skoringi jamoasiga regulyator xat yubordi: "Uch oy oldin mijoz X ga kredit rad etilgan. Qaror qaysi model bilan chiqarilgan va u qanday ma'lumotda o'rgatilgan?". Jamoada s3://modellar/kredit/model_final.pkl bor edi — lekin u har oy ustiga yozilgan. Git da kod bor, lekin qaysi commit ishlab chiqarishda bo'lgani noma'lum; o'rgatish ma'lumoti esa ma'lumotlar omborida o'zgarib ketgan (kechikib kelgan to'lovlar qo'shilgan). Javob tayyorlash uchun ikki hafta kerak bo'ldi va natija baribir taxmin edi. Registr va ma'lumot xeshi bo'lganida bu — bitta SQL so'rov.

Bu darsda modelni kod + ma'lumot + model uchligi sifatida versiyalaymiz va noldan kichik, lekin to'liq model registr quramiz: bosqichlar, darvoza, orqaga qaytarish va audit.

Bu darsda:

  • Kod, ma'lumot va modelni birga versiyalash (xeshlar va manifest)
  • Ma'lumot xeshi va kanonik shakl
  • Semantik versiya modellar uchun
  • Model registr: nima saqlanadi, bosqichlar va o'tish qoidalari
  • Ishlab chiqarishga chiqarish darvozasi (promotion gate)
  • Orqaga qaytarish (rollback) va audit tarixi
  • DVC g'oyasi: kontent-manzilli saqlash
  • Model kartasi registr yozuvining bir qismi sifatida
  • Tuzoqlar

ℹ Misollar real numpy/pandas/sklearn va standart sqlite3, hashlib, yaml bilan (Python 3.14). MLflow va DVC mashinada yo'q — ularning asosiy g'oyasini noldan quramiz va har joyda "haqiqiy loyihada MLflow/DVC shu ishni qiladi" deb bog'laymiz.


2. Nazariya — chuqur tushuntirish

2.1. Versiyalash uchligi: kod, ma'lumot, model

text
                 +-----------+
                 |    KOD    |  git commit (a1b2c3d), toza ishchi daraxt
                 +-----+-----+
                       |
   +-----------+       v        +-------------+
   | MA'LUMOT  | ---> O'RGATISH <--- | KONFIG+MUHIT|  seed, giperparametrlar,
   | xesh 9f3e |       |        | requirements.lock 27.2-bob
   +-----------+       v        +-------------+
                 +-----------+
                 |   MODEL   |  artefakt xeshi, metrikalar
                 +-----------+

MODEL VERSIYASI = (kod versiyasi, ma'lumot xeshi, konfig, muhit) -> artefakt

QOIDA: uchlikdan bittasi noma'lum bo'lsa - model QAYTA QURILMAYDI

Git kodni yaxshi versiyalaydi, lekin ma'lumot va model uchun yaramaydi: fayllar katta (gigabaytlar), ikkilik (diff ma'noga ega emas), tez-tez o'zgaradi. Shuning uchun ma'lumot va model uchun xesh ishlatiladi: faylning mazmunidan hisoblangan qisqa "barmoq izi". Git esa faqat shu kichik barmoq izini saqlaydi.

Manifest — bitta model versiyasining pasporti:

text
{
  "model": "mijoz_ketishi",
  "versiya": "1.2.0",
  "kod_versiyasi": "a1b2c3d",          <- git commit
  "malumot_xeshi": "9f3e41c07b2a",     <- o'rgatish ma'lumoti
  "konfig": {"C": 1.0, "seed": 7},
  "muhit": "requirements.lock xeshi",
  "model_xeshi": "5d0c8e1a44f2",       <- artefakt fayli
  "metrikalar": {"auc": 0.81},
  "run_id": "run-0042"                 <- eksperiment kuzatuvi 27.4-bob
}

Model versiyasi — bitta fayl emas, uchlik: kod, ma'lumot va konfig versiyalari plus natija artefakti. Manifest shularni bog'laydi.

2.2. Ma'lumot xeshi va kanonik shakl

text
XOM XESH:  sha256(fayl baytlari)
  + oddiy, tez
  - qatorlar tartibi o'zgarsa  -> boshqa xesh (mazmun o'sha!)
  - float yozilishi 0.1 vs 0.10000000001 -> boshqa xesh
  - CSV da \r\n vs \n (Windows!)         -> boshqa xesh

KANONIK XESH:
  1. ustunlar nomi bo'yicha tartiblanadi
  2. qatorlar kalit (id) bo'yicha tartiblanadi
  3. sonlar bir xil formatda (%.6f)
  4. qator oxiri bir xil (\n)
  5. sha256 -> birinchi 12 belgi (ko'rsatish uchun)

QACHON QAYSI:
  fayl ombori (DVC)     -> xom xesh (fayl aynan saqlanadi)
  "mazmun bir xilmi?"   -> kanonik xesh

Kanonik xeshda "mazmun" ni o'zingiz belgilaysiz. Masalan, qatorlar tartibi model uchun ahamiyatsiz bo'lsa, tartib bo'yicha normallashtiring. Lekin qiymat o'zgarishi, hatto +0.01, albatta xeshni o'zgartirishi kerak — aks holda xesh o'z vazifasini bajarmaydi.

Ma'lumot xeshi — ma'lumot versiyasining nomi. "Iyun ma'lumoti" emas, 9f3e41c07b2a: kim qachon so'rasa ham aynan bir xil narsani bildiradi.

2.3. Semantik versiya modellar uchun

Dasturiy ta'minotdagi MAJOR.MINOR.PATCH qoidasini modelga moslaymiz. Mezon — iste'molchi (API mijozi, boshqa jamoa) uchun nima o'zgaradi:

text
MAJOR (1.x.x -> 2.0.0)  - SHARTNOMA buzildi, iste'molchi kodini o'zgartirishi kerak:
  yangi MAJBURIY kirish ustuni, ustun olib tashlandi
  chiqish ma'nosi o'zgardi (ehtimol -> sinf, yangi sinflar)
  birlik o'zgardi (so'm -> ming so'm)

MINOR (1.2.x -> 1.3.0)  - shartnoma o'sha, xatti-harakat o'zgardi:
  yangi ma'lumot bilan qayta o'qitish
  giperparametr yoki algoritm almashdi
  ixtiyoriy (sukut qiymatli) yangi ustun

PATCH (1.2.3 -> 1.2.4)  - bashoratlar o'zgarmaydi:
  metadata, model kartasi, hujjat tuzatildi
  paketlash tuzatildi (bir xil bashorat)

Registrlarda (MLflow ham) odatda yana bitta raqam bor — ro'yxat raqami: 1, 2, 3, …, har yangi yozuvga avtomatik beriladi. Ikkalasi birga yashaydi: registr raqami — "nechanchi yozuv", semantik versiya — "iste'molchi uchun nima o'zgardi".

MAJOR — shartnoma buzilganda. Iste'molchi sezmaydigan qayta o'qitish — MINOR; u kodini o'zgartirishi kerak bo'lsa — MAJOR.

2.4. Model registr: nima saqlanadi

text
REGISTR YOZUVI (nom, versiya):
  nom             "mijoz_ketishi"
  versiya         3                   (avtomatik, o'sib boradi)
  bosqich         None | Staging | Production | Archived
  metrikalar      {"auc": 0.84, "log_loss": 0.41}
  kelib chiqish (lineage):
    malumot_xeshi   "9f3e41c07b2a"
    kod_versiyasi   "a1b2c3d"
    run_id          "run-0042"        -> eksperiment kuzatuvidagi to'liq yozuv
  artefakt        ombordagi fayl + uning xeshi
  model kartasi   maqsad, cheklovlar, adolatlilik 26.10-bob

AUDIT JADVALI (hech qachon o'chirilmaydi, faqat qo'shiladi):
  soat | nom | versiya | eski bosqich | yangi bosqich | kim | sabab

Registrning ikki muhim xossasi:

  1. Artefakt o'chirilmaydi — Archived degani "ishlatilmaydi", "yo'q qilindi" emas. Aynan shuning uchun orqaga qaytarish bir necha soniya.
  2. Audit faqat qo'shiladi (append-only). Kim nimani qachon va nima uchun o'zgartirgani — regulyator, incident tahlili va jamoa xotirasi uchun.

2.5. Bosqichlar va o'tish qoidalari

text
            ro'yxatga olish
                  |
                  v
               [None] ---------------------+
                  |                        |
                  v                        v
             [Staging] ---- rad ----> [Archived]
                  |                     ^    |
          DARVOZA |                     |    | rollback
                  v                     |    | (maxsus yo'l)
           [Production] --- almashdi ---+    |
                  ^                          |
                  +--------------------------+

RUXSAT ETILGAN O'TISHLAR:
  None -> Staging, None -> Archived
  Staging -> Production (faqat DARVOZA orqali)
  Staging -> Archived
  Production -> Archived (yangisi chiqqanda avtomatik)
  Archived -> Staging (qayta ko'rib chiqish)
  Archived -> Production - FAQAT rollback, audit sababi bilan

TAQIQLANGAN:
  None -> Production  ("to'g'ridan-to'g'ri prodga")
  bitta nomda ikkita Production

Qoidalar kodda (registrda) yashashi kerak, jamoa kelishuvida emas. "Iltimos, to'g'ridan-to'g'ri prodga qo'ymang" degan kelishuv birinchi shoshilinch kechada buziladi; ValueError: ruxsat yo'q: None -> Production esa buzilmaydi.

2.6. Darvoza: Production ga qachon chiqariladi

Darvoza (promotion gate) — Staging -> Production o'tishidan oldin avtomatik tekshiruvlar. Ular natijani ko'rishdan oldin belgilanadi (18-qismdagi qoida: mezonni natijaga moslashtirmaslik).

text
DARVOZA (misoldagi):
  1. MUTLAQ CHEGARA:   AUC(nomzod) >= 0.70    (biznes bilan kelishilgan)
  2. NISBIY SHART:     nomzod joriy Production dan SEZILARLI YOMON EMAS

  2-shartni tekshirish - JUFTLASHGAN taqqoslash, bir xil validatsiya to'plamida:
    har namuna i uchun:  d_i = logloss_nomzod(i) - logloss_prod(i)
    o'rtacha d,  SE = std(d) / sqrt(n)
    o'rtacha d > 2*SE  -> nomzod sezilarli YOMON -> RAD
    aks holda          -> o'tadi

QO'SHIMCHA (haqiqiy loyihalarda):
  sxema mosligi 27.6-bob, kechikish byudjeti 27.8-bob, adolatlilik metrikalari,
  segmentlar bo'yicha (yangi mijozlar, hududlar) regressiya yo'qligi

Nega "sezilarli yaxshi" emas, "sezilarli yomon emas"? Qayta o'qitish ko'pincha yangi ma'lumotga moslashish uchun qilinadi — yangi model eskisidan metrikada sezilarli yaxshi bo'lmasligi mumkin, lekin u yangi taqsimotni ko'rgan. Darvoza regressiyani to'xtatadi; "arziydimi" savoli esa eng sodda munosib model qoidasi va biznes qarori.

Ikki shartning har biri o'z xatosini ushlaydi. Mutlaq chegara — "umuman yaroqsiz" modelni (buzilgan ma'lumot, xato join). Nisbiy shart — "o'zicha yomon emas, lekin hozirgisidan yomon" modelni. 3-misolda ikkalasi ham ishlaydi.

Darvoza — oldindan yozilgan, avtomatik, juftlashgan. Chegara natijadan oldin belgilanadi, taqqoslash bir xil to'plamda va shovqin (SE) hisobga olinadi.

2.7. Orqaga qaytarish va audit

text
ROLLBACK:
  1. joriy Production -> Archived  (sabab: "rollback: <nima bo'ldi>")
  2. OLDINGI Production -> Production
       "oldingi" = auditda oxirgi marta Production -> Archived bo'lgan
                   boshqa versiya
  3. ikkala qadam BITTA tranzaksiyada (yarim holat bo'lmaydi)

NEGA TEZ: artefakt registrda turibdi, qayta o'qitish shart emas
NEGA DARVOZASIZ: oldingi versiya allaqachon ishlab chiqarishda sinalgan
                 (lekin sabab va kim - auditga MAJBURIY)

AUDIT BERADIGAN JAVOBLAR:
  "hozir qaysi versiya?"              -> versiyalar jadvali
  "5-mart kuni qaysi versiya edi?"    -> audit, shu vaqtgacha oxirgi Production
  "bu ma'lumotdan qaysi modellar?"    -> lineage bo'yicha so'rov
  "kim prodga chiqargan va nega?"     -> audit.kim, audit.sabab

Orqaga qaytarish tugmasi faqat oldindan sinalgan bo'lsa ishonchli. Ko'p jamoalarda rollback birinchi marta haqiqiy incident paytida ishlatiladi — va ishlamaydi (artefakt o'chirilgan, konfig mos emas). Rollback ni oddiy jarayonning bir qismi sifatida muntazam sinang.

2.8. DVC g'oyasi: kontent-manzilli saqlash

Kontent-manzilli saqlash (content-addressable storage): fayl nomi bilan emas, mazmunining xeshi bilan saqlanadi.

text
ISHCHI PAPKA                     OMBOR (kesh / masofaviy)
  data/mijozlar.csv      --->      ab/12cd34...   (fayl xeshi = manzil)
  data/mijozlar.csv.dvc            5f/9e01aa...
     outs:
       - path: mijozlar.csv
         sha256: ab12cd34...       <- GIT FAQAT SHU KICHIK FAYLNI SAQLAYDI
         size: 81234

XOSSALAR:
  bir xil mazmun -> bir xil xesh -> BIR marta saqlanadi (dedup)
  mazmun o'zgarsa -> yangi xesh -> yangi obyekt, eskisi joyida
  "checkout" = ko'rsatkichdagi xesh bo'yicha fayl ombordan tiklanadi
  butunlik: ombordagi obyekt xeshi nomiga mos emas -> buzilgan
  gc: hech bir ko'rsatkich ishora qilmagan obyektlar o'chiriladi

Haqiqiy DVC bilan ish shunday ko'rinadi (bu mashinada DVC yo'q — faqat nazariya):

bash
dvc init
dvc add data/mijozlar.csv          # data/mijozlar.csv.dvc yaratiladi
git add data/mijozlar.csv.dvc data/.gitignore
git commit -m "ma'lumot v1"
dvc push                           # obyektlar masofaviy omborga
git checkout v1 && dvc checkout    # ma'lumot v1 holatiga qaytadi
dvc repro                          # dvc.yaml dagi quvurni kerak joyidan qayta ishlatadi

dvc.yaml quvur bosqichlarini 27.3-bob ularning kirish-chiqishi bilan tasvirlaydi — DVC xeshlar bo'yicha qaysi bosqich eskirganini aniqlaydi:

yaml
stages:
  tayyorla:
    cmd: python tayyorla.py
    deps: [tayyorla.py, data/mijozlar.csv]
    outs: [data/tayyor.csv]
  orgat:
    cmd: python orgat.py
    deps: [orgat.py, data/tayyor.csv]
    params: [orgat.C, orgat.seed]
    outs: [model/model.joblib]
    metrics: [natija/metrikalar.json]

Git — ko'rsatkichni, ombor — mazmunni saqlaydi. Kod commit i va ma'lumot ko'rsatkichi bir commit da bo'lsa, git checkout + dvc checkout ikkalasini birga qaytaradi.

2.9. MLflow Model Registry bilan bog'lash

Bu darsda noldan quradigan registr MLflow Model Registry ning soddalashtirilgan nusxasi. Haqiqiy loyihada kod taxminan shunday bo'ladi (mashinada mlflow yo'q — faqat tanishish uchun):

python
import mlflow
from mlflow import MlflowClient

natija = mlflow.register_model("runs:/run-0042/model", "mijoz_ketishi")
mijoz = MlflowClient()
mijoz.set_model_version_tag("mijoz_ketishi", natija.version,
                            "malumot_xeshi", "9f3e41c07b2a")
mijoz.set_registered_model_alias("mijoz_ketishi", "champion", natija.version)
model = mlflow.pyfunc.load_model("models:/mijoz_ketishi@champion")

MLflow ning yangi versiyalarida qat'iy bosqichlar (Staging/Production) o'rniga taxalluslar (alias: champion, challenger) tavsiya qilinadi — g'oya o'sha: nom bo'yicha "hozirgi ishlab chiqarish modeli" ga ishora, versiya esa almashtiriladi. Bosqichlar bilan ishlash tushunarliroq bo'lgani uchun darsda ular ishlatiladi; aniq API ni MLflow rasmiy hujjatidan tekshiring.

2.10. Model kartasi — registr yozuvining bir qismi

26.10-darsda generativ model uchun model kartasini yozdik. Registrda karta har versiyaga biriktiriladi, chunki u versiyaga xos:

text
MODEL KARTASI (qisqa):
  maqsad va iste'molchi      "ketish ehtimoli, marketing uchun, kredit qarori UCHUN EMAS"
  o'rgatish ma'lumoti        malumot_xeshi, davr, hajm, segmentlar
  metrikalar                 umumiy + segmentlar bo'yicha
  cheklovlar                 "yangi mijozlarda (oylar < 3) sinalmagan"
  adolatlilik                guruhlar bo'yicha farqlar
  egasi va aloqa             jamoa, navbatchi

Karta tuzatilsa (imlo, aniqlashtirish) — bu PATCH; kartadagi "cheklovlar" o'zgargan bo'lsa (masalan, yangi segmentda sinaldi) — odatda yangi o'qitish bilan birga MINOR.

2.11. Tuzoqlar

Asosiy tuzoqlar: model_final.pkl ni ustiga yozish (tarix yo'qoladi); ma'lumotni sana yoki fayl nomi bilan "versiyalash" (iyun.csv keyin o'zgaradi); xom fayl xeshini "mazmun" deb o'ylash (qator tartibi, \r\n); git holati toza bo'lmaganda o'rgatish (commit xeshi kodni to'liq ifodalamaydi); None -> Production ga ruxsat; bir nomda ikkita Production; darvoza chegarasini natijani ko'rgandan keyin tanlash; darvozada juftlashmagan yoki shovqinsiz taqqoslash; Archived modellarni o'chirish (rollback imkonsiz); auditni tahrirlash yoki o'chirish; rollback ni hech qachon sinamaslik.


3. Tez ma'lumotnoma

python
import hashlib
import json
import sqlite3


def malumot_xeshi(df, kalit="mijoz_id"):
    kanonik = df[sorted(df.columns)].sort_values(kalit)
    matn = kanonik.to_csv(index=False, float_format="%.6f", lineterminator="\n")
    return hashlib.sha256(matn.encode("utf-8")).hexdigest()[:12]


def keyingi_versiya(joriy, ozgarish):
    katta, orta, kichik = map(int, joriy.split("."))
    if ozgarish == "MAJOR":
        return f"{katta + 1}.0.0"
    if ozgarish == "MINOR":
        return f"{katta}.{orta + 1}.0"
    return f"{katta}.{orta}.{kichik + 1}"


RUXSAT = {("None", "Staging"), ("None", "Archived"), ("Staging", "Production"),
          ("Staging", "Archived"), ("Production", "Archived"),
          ("Archived", "Staging")}

db = sqlite3.connect(":memory:")
db.execute("CREATE TABLE audit (id INTEGER PRIMARY KEY, nom TEXT, versiya INT,"
           " eski TEXT, yangi TEXT, kim TEXT, sabab TEXT)")
with db:                                   # tranzaksiya: hammasi yoki hech narsa
    db.execute("INSERT INTO audit (nom, versiya, eski, yangi, kim, sabab)"
               " VALUES (?, ?, ?, ?, ?, ?)",
               ("mijoz_ketishi", 1, "Staging", "Production", "ali", "darvoza"))

Tuzilma xulosasi

model versiyasi = kod (git) + ma'lumot (xesh) + konfig/muhit -> artefakt (xesh)
semver: MAJOR shartnoma / MINOR xatti-harakat / PATCH bashorat o'zgarmaydi
registr: nom, versiya, bosqich, metrikalar, lineage, karta + audit (append-only)
None -> Staging -> [darvoza] -> Production -> Archived; rollback - maxsus yo'l
darvoza: mutlaq chegara + juftlashgan "sezilarli yomon emas" (2*SE)
DVC: fayl xeshi -> ombor manzili, git da faqat kichik ko'rsatkich

4. Batafsil misollar

Misollar real numpy/pandas/sklearn va sqlite3/hashlib/yaml bilan (Python 3.14). Hamma fayllar vaqtinchalik papkada.

Misol 1 — Kod, ma'lumot va modelni birga versiyalash

python
"""Kod + ma'lumot + model: xeshlar, manifest, qayta tiklash, semantik versiya."""

import hashlib
import inspect
import io
import json

import joblib
import numpy as np
import pandas as pd
from sklearn.compose import ColumnTransformer
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import roc_auc_score
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import OneHotEncoder, StandardScaler

SONLI = ["oylar", "tolov", "shikoyat"]
KATEGORIAL = ["tarif"]


def malumot(n, seed):
    rng = np.random.default_rng(seed)
    df = pd.DataFrame({
        "mijoz_id": np.arange(n),
        "oylar": rng.integers(1, 72, n),
        "tolov": rng.normal(60, 20, n).clip(10, 150).round(2),
        "shikoyat": rng.poisson(0.8, n),
        "tarif": rng.choice(["oylik", "yillik", "ikki_yillik"], n,
                            p=[0.55, 0.3, 0.15]),
    })
    logit = (-1.0 - 0.04 * df["oylar"] + 0.02 * (df["tolov"] - 60)
             + 0.5 * df["shikoyat"]
             + np.where(df["tarif"] == "oylik", 1.0, -0.5))
    df["ketdi"] = (rng.random(n) < 1 / (1 + np.exp(-logit))).astype(int)
    return df


def xesh(baytlar):
    return hashlib.sha256(baytlar).hexdigest()[:12]


def malumot_xeshi(df, kanonik=True):
    if kanonik:
        df = df[sorted(df.columns)].sort_values("mijoz_id")
    matn = df.to_csv(index=False, float_format="%.6f", lineterminator="\n")
    return xesh(matn.encode("utf-8"))


def orgat(df, konfig):
    pre = ColumnTransformer([
        ("son", StandardScaler(), SONLI),
        ("kat", OneHotEncoder(handle_unknown="ignore"), KATEGORIAL),
    ])
    model = Pipeline([("pre", pre),
                      ("clf", LogisticRegression(C=konfig["C"], max_iter=1000))])
    return model.fit(df[SONLI + KATEGORIAL], df["ketdi"])


def orgat_muvozanatli(df, konfig):
    pre = ColumnTransformer([
        ("son", StandardScaler(), SONLI),
        ("kat", OneHotEncoder(handle_unknown="ignore"), KATEGORIAL),
    ])
    model = Pipeline([("pre", pre),
                      ("clf", LogisticRegression(C=konfig["C"], max_iter=1000,
                                                 class_weight="balanced"))])
    return model.fit(df[SONLI + KATEGORIAL], df["ketdi"])


def kod_versiyasi(funksiya):
    # haqiqiy loyihada: git commit xeshi (toza ishchi daraxt bilan)
    return xesh(inspect.getsource(funksiya).encode("utf-8"))


def konfig_xeshi(konfig):
    return xesh(json.dumps(konfig, sort_keys=True).encode("utf-8"))


def model_xeshi(model):
    bufer = io.BytesIO()
    joblib.dump(model, bufer)
    return xesh(bufer.getvalue())


def keyingi_versiya(joriy, ozgarish):
    katta, orta, kichik = map(int, joriy.split("."))
    if ozgarish == "MAJOR":
        return f"{katta + 1}.0.0"
    if ozgarish == "MINOR":
        return f"{katta}.{orta + 1}.0"
    return f"{katta}.{orta}.{kichik + 1}"


def main() -> None:
    konfig = {"C": 1.0, "seed": 7}
    df = malumot(4000, seed=konfig["seed"])
    orgatish, sinov = df.iloc[:3000], df.iloc[3000:]

    print("=== 1. Ma'lumot xeshi ===")
    h1 = malumot_xeshi(orgatish)
    h2 = malumot_xeshi(malumot(4000, seed=7).iloc[:3000])
    aralash = orgatish.sample(frac=1.0, random_state=0)
    bitta = orgatish.copy()
    bitta.loc[bitta.index[5], "tolov"] += 0.01
    print(f"  asl ma'lumot:                  {h1}")
    print(f"  qayta generatsiya:             {h2}  teng: {h1 == h2}")
    print(f"  qatorlar aralashgan, xom:      "
          f"{malumot_xeshi(aralash, kanonik=False)}")
    print(f"  qatorlar aralashgan, kanonik:  {malumot_xeshi(aralash)}  "
          f"teng: {malumot_xeshi(aralash) == h1}")
    print(f"  bitta qiymat +0.01:            {malumot_xeshi(bitta)}  "
          f"teng: {malumot_xeshi(bitta) == h1}")

    print("\n=== 2. Manifest: uchlik bitta joyda ===")
    model = orgat(orgatish, konfig)
    p = model.predict_proba(sinov[SONLI + KATEGORIAL])[:, 1]
    manifest = {
        "model": "mijoz_ketishi",
        "versiya": "1.0.0",
        "malumot_xeshi": h1,
        "qatorlar": len(orgatish),
        "kod_versiyasi": kod_versiyasi(orgat),
        "konfig": konfig,
        "konfig_xeshi": konfig_xeshi(konfig),
        "model_xeshi": model_xeshi(model),
        "metrikalar": {"auc": round(float(roc_auc_score(sinov["ketdi"], p)), 4)},
    }
    for kalit, qiymat in manifest.items():
        print(f"  {kalit:<14} {qiymat}")

    print("\n=== 3. Manifestdan qayta qurish ===")
    qayta_df = malumot(4000, seed=manifest["konfig"]["seed"]).iloc[:3000]
    print(f"  ma'lumot xeshi mos: "
          f"{malumot_xeshi(qayta_df) == manifest['malumot_xeshi']}")
    qayta = orgat(qayta_df, manifest["konfig"])
    print(f"  kod versiyasi mos:  "
          f"{kod_versiyasi(orgat) == manifest['kod_versiyasi']}")
    print(f"  konfig xeshi mos:   "
          f"{konfig_xeshi(manifest['konfig']) == manifest['konfig_xeshi']}")
    print(f"  model xeshi mos:    "
          f"{model_xeshi(qayta) == manifest['model_xeshi']}")
    farq = np.abs(qayta.predict_proba(sinov[SONLI + KATEGORIAL])[:, 1] - p).max()
    print(f"  bashoratlar max farqi: {farq:.1e}")

    print("\n=== 4. Qaysi o'zgarish qaysi xeshni o'zgartiradi ===")
    yangi_df = malumot(3500, seed=8)
    variantlar = [
        ("hech narsa", orgat, orgatish, konfig),
        ("yangi ma'lumot (3500)", orgat, yangi_df, konfig),
        ("konfig: C=0.1", orgat, orgatish, {**konfig, "C": 0.1}),
        ("kod: class_weight", orgat_muvozanatli, orgatish, konfig),
    ]
    asos = (h1, manifest["kod_versiyasi"], manifest["konfig_xeshi"],
            manifest["model_xeshi"])
    sarlavha = "o'zgarish"
    print(f"  {sarlavha:<22} {'malumot':<9} {'kod':<9} {'konfig':<9} model")
    for nom, funksiya, d, k in variantlar:
        joriy = (malumot_xeshi(d), kod_versiyasi(funksiya), konfig_xeshi(k),
                 model_xeshi(funksiya(d, k)))
        b = ["o'zgardi" if x != y else "-" for x, y in zip(joriy, asos)]
        print(f"  {nom:<22} {b[0]:<9} {b[1]:<9} {b[2]:<9} {b[3]}")

    print("\n=== 5. Semantik versiya ===")
    stsenariylar = [
        ("MINOR", "yangi oy ma'lumoti bilan qayta o'qitish"),
        ("PATCH", "model kartasidagi imlo tuzatildi"),
        ("MINOR", "C giperparametri o'zgardi"),
        ("MAJOR", "yangi MAJBURIY kirish ustuni: hudud"),
        ("PATCH", "paket metadata si tuzatildi"),
        ("MAJOR", "chiqish: ehtimol o'rniga sinf"),
    ]
    versiya = manifest["versiya"]
    for ozgarish, tavsif in stsenariylar:
        yangi = keyingi_versiya(versiya, ozgarish)
        print(f"  {versiya:>6} -> {yangi:<6} {ozgarish:<6} {tavsif}")
        versiya = yangi
    print("  ⭐ Versiya = kod + ma'lumot + konfig; manifest ularni bog'laydi")


if __name__ == "__main__":
    main()

Natijaning muhim qismi:

text
=== 1. Ma'lumot xeshi ===
  asl ma'lumot:                  390d116cced9
  qayta generatsiya:             390d116cced9  teng: True
  qatorlar aralashgan, xom:      7e81c712e469
  qatorlar aralashgan, kanonik:  390d116cced9  teng: True
  bitta qiymat +0.01:            e9a117d03277  teng: False

=== 2. Manifest: uchlik bitta joyda ===
  model          mijoz_ketishi
  versiya        1.0.0
  malumot_xeshi  390d116cced9
  qatorlar       3000
  kod_versiyasi  2a9d7b89de90
  konfig         {'C': 1.0, 'seed': 7}
  konfig_xeshi   df1e7446ded1
  model_xeshi    23453b260206
  metrikalar     {'auc': 0.7721}

=== 3. Manifestdan qayta qurish ===
  ma'lumot xeshi mos: True
  kod versiyasi mos:  True
  konfig xeshi mos:   True
  model xeshi mos:    True
  bashoratlar max farqi: 0.0e+00

=== 4. Qaysi o'zgarish qaysi xeshni o'zgartiradi ===
  o'zgarish              malumot   kod       konfig    model
  hech narsa             -         -         -         -
  yangi ma'lumot (3500)  o'zgardi  -         -         o'zgardi
  konfig: C=0.1          -         -         o'zgardi  o'zgardi
  kod: class_weight      -         o'zgardi  -         o'zgardi

=== 5. Semantik versiya ===
   1.0.0 -> 1.1.0  MINOR  yangi oy ma'lumoti bilan qayta o'qitish
   1.1.0 -> 1.1.1  PATCH  model kartasidagi imlo tuzatildi
   1.1.1 -> 1.2.0  MINOR  C giperparametri o'zgardi
   1.2.0 -> 2.0.0  MAJOR  yangi MAJBURIY kirish ustuni: hudud
   2.0.0 -> 2.0.1  PATCH  paket metadata si tuzatildi
   2.0.1 -> 3.0.0  MAJOR  chiqish: ehtimol o'rniga sinf
  ⭐ Versiya = kod + ma'lumot + konfig; manifest ularni bog'laydi

Natija tahlili. 1-bo'limda ma'lumotni urug' bilan qayta generatsiya qilish aynan o'sha xeshni berdi — ma'lumot xeshi uning barqaror nomi. Qatorlar aralashtirilganda xom xesh butunlay boshqacha bo'ldi, kanonik xesh esa asl bilan teng qoldi: mazmun o'sha, faqat tartib boshqa. Bitta qiymatga +0.01 qo'shilganda esa kanonik xesh ham o'zgardi — shunday bo'lishi shart. 2-bo'limdagi manifest modelning pasporti: undagi ma'lumot xeshi, kod versiyasi va konfig bo'yicha 3-bo'limda model qayta qurildi va artefakt xeshi bitma-bit mos keldi. 4-bo'lim jadvali uchlik mantiqini ko'rsatadi: ma'lumot, konfig yoki kod — qaysi biri o'zgarsa, model xeshi ham o'zgaradi, lekin qaysi sababdan o'zgargani faqat qolgan ustunlarga qarab aniqlanadi. Model xeshining o'zi "nimadir o'zgardi" deydi, xolos; manifestdagi boshqa xeshlar "nima o'zgardi" ni aytadi. Misolda kod versiyasi o'rgatish funksiyasining matnidan hisoblandi; haqiqiy loyihada bu git commit xeshi, konfig esa alohida fayl (params.yaml). 5-bo'limda semantik versiya iste'molchi nuqtai nazaridan o'sdi: ikki MAJOR o'zgarish (yangi majburiy ustun, chiqish ma'nosi) API mijozlarini kod o'zgartirishga majbur qiladi.

Misol 2 — Model registr noldan (sqlite)

python
"""sqlite ustida model registr: versiyalar, bosqichlar, lineage, audit."""

import json
import sqlite3
import tempfile
from pathlib import Path

RUXSAT = {
    ("None", "Staging"), ("None", "Archived"),
    ("Staging", "Production"), ("Staging", "Archived"),
    ("Production", "Archived"), ("Archived", "Staging"),
}

SXEMA = """
CREATE TABLE IF NOT EXISTS versiyalar (
    nom TEXT NOT NULL,
    versiya INTEGER NOT NULL,
    bosqich TEXT NOT NULL,
    metrikalar TEXT NOT NULL,
    malumot_xeshi TEXT NOT NULL,
    kod_versiyasi TEXT NOT NULL,
    run_id TEXT NOT NULL,
    PRIMARY KEY (nom, versiya)
);
CREATE TABLE IF NOT EXISTS audit (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    soat INTEGER NOT NULL,
    nom TEXT NOT NULL,
    versiya INTEGER NOT NULL,
    eski TEXT,
    yangi TEXT NOT NULL,
    kim TEXT NOT NULL,
    sabab TEXT NOT NULL
);
"""


class Registr:
    def __init__(self, yol):
        self.db = sqlite3.connect(yol)
        self.db.executescript(SXEMA)
        # virtual soat: haqiqiy vaqt o'rniga o'sib boruvchi hisoblagich
        self.soat = self.db.execute(
            "SELECT COALESCE(MAX(soat), 0) FROM audit").fetchone()[0]

    def _audit(self, nom, versiya, eski, yangi, kim, sabab):
        self.soat += 1
        self.db.execute(
            "INSERT INTO audit (soat, nom, versiya, eski, yangi, kim, sabab)"
            " VALUES (?, ?, ?, ?, ?, ?, ?)",
            (self.soat, nom, versiya, eski, yangi, kim, sabab))

    def royxatga_ol(self, nom, metrikalar, malumot_xeshi, kod_versiyasi,
                    run_id, kim):
        with self.db:
            versiya = self.db.execute(
                "SELECT COALESCE(MAX(versiya), 0) + 1 FROM versiyalar"
                " WHERE nom = ?", (nom,)).fetchone()[0]
            self.db.execute(
                "INSERT INTO versiyalar VALUES (?, ?, ?, ?, ?, ?, ?)",
                (nom, versiya, "None", json.dumps(metrikalar, sort_keys=True),
                 malumot_xeshi, kod_versiyasi, run_id))
            self._audit(nom, versiya, None, "None", kim, "ro'yxatga olindi")
        return versiya

    def bosqich(self, nom, versiya):
        return self.db.execute(
            "SELECT bosqich FROM versiyalar WHERE nom = ? AND versiya = ?",
            (nom, versiya)).fetchone()[0]

    def otkaz(self, nom, versiya, yangi, kim, sabab):
        eski = self.bosqich(nom, versiya)
        if (eski, yangi) not in RUXSAT:
            raise ValueError(f"ruxsat yo'q: {eski} -> {yangi}")
        with self.db:
            if yangi == "Production":
                joriylar = self.db.execute(
                    "SELECT versiya FROM versiyalar WHERE nom = ?"
                    " AND bosqich = 'Production'", (nom,)).fetchall()
                for (joriy,) in joriylar:
                    self.db.execute(
                        "UPDATE versiyalar SET bosqich = 'Archived'"
                        " WHERE nom = ? AND versiya = ?", (nom, joriy))
                    self._audit(nom, joriy, "Production", "Archived", kim,
                                f"v{versiya} bilan almashtirildi")
            self.db.execute(
                "UPDATE versiyalar SET bosqich = ? WHERE nom = ? AND versiya = ?",
                (yangi, nom, versiya))
            self._audit(nom, versiya, eski, yangi, kim, sabab)

    def jadval(self, nom):
        return self.db.execute(
            "SELECT versiya, bosqich, metrikalar, malumot_xeshi, run_id"
            " FROM versiyalar WHERE nom = ? ORDER BY versiya", (nom,)).fetchall()


def main() -> None:
    with tempfile.TemporaryDirectory() as papka:
        yol = Path(papka) / "registr.db"
        reg = Registr(yol)

        print("=== 1. Ro'yxatga olish ===")
        yozuvlar = [
            ({"auc": 0.781, "log_loss": 0.482}, "9f3e41c07b2a", "a1b2c3d",
             "run-0007"),
            ({"auc": 0.804, "log_loss": 0.463}, "4c7d20e9b815", "a1b2c3d",
             "run-0012"),
            ({"auc": 0.809, "log_loss": 0.458}, "4c7d20e9b815", "e5f6a7b",
             "run-0019"),
        ]
        for metrikalar, mx, kv, run in yozuvlar:
            v = reg.royxatga_ol("mijoz_ketishi", metrikalar, mx, kv, run, "ali")
            print(f"  v{v}: run {run}, ma'lumot {mx}, kod {kv}")

        print("\n=== 2. Bosqich o'tishlari ===")
        reg.otkaz("mijoz_ketishi", 1, "Staging", "ali", "sinov uchun")
        reg.otkaz("mijoz_ketishi", 1, "Production", "vali", "birinchi chiqish")
        reg.otkaz("mijoz_ketishi", 2, "Staging", "ali", "yangi ma'lumot")
        reg.otkaz("mijoz_ketishi", 2, "Production", "vali", "auc yaxshiroq")
        for v in (1, 2, 3):
            print(f"  v{v}: {reg.bosqich('mijoz_ketishi', v)}")

        print("\n=== 3. Taqiqlangan o'tish ===")
        for v, yangi in [(3, "Production"), (1, "Production")]:
            try:
                reg.otkaz("mijoz_ketishi", v, yangi, "shoshqaloq", "tezroq")
                print(f"  v{v} -> {yangi}: bajarildi (kutilmagan)")
            except ValueError as xato:
                print(f"  v{v} -> {yangi}: ValueError - {xato}")
        reg.otkaz("mijoz_ketishi", 3, "Staging", "ali", "kod yangilandi")

        print("\n=== 4. Diskdan qayta ochish ===")
        reg.db.close()
        reg2 = Registr(yol)
        for v, bosq, metr, mx, run in reg2.jadval("mijoz_ketishi"):
            auc = json.loads(metr)["auc"]
            print(f"  v{v:<2} {bosq:<11} auc {auc:.3f}  {mx}  {run}")
        prod_soni = reg2.db.execute(
            "SELECT COUNT(*) FROM versiyalar WHERE bosqich = 'Production'"
        ).fetchone()[0]
        print(f"  Production soni: {prod_soni} (invariant: 1 dan oshmaydi)")

        print("\n=== 5. Lineage so'rovlari ===")
        v, mx, kv, run = reg2.db.execute(
            "SELECT versiya, malumot_xeshi, kod_versiyasi, run_id"
            " FROM versiyalar WHERE nom = 'mijoz_ketishi'"
            " AND bosqich = 'Production'").fetchone()
        print(f"  Production: v{v}, ma'lumot {mx}, kod {kv}, run {run}")
        tasirlangan = reg2.db.execute(
            "SELECT versiya, bosqich FROM versiyalar WHERE malumot_xeshi = ?"
            " ORDER BY versiya", ("4c7d20e9b815",)).fetchall()
        print(f"  ma'lumot 4c7d20e9b815 da muammo -> ta'sirlangan: "
              f"{[f'v{a} ({b})' for a, b in tasirlangan]}")

        print("\n=== 6. Audit tarixi ===")
        for soat, v, eski, yangi, kim, sabab in reg2.db.execute(
                "SELECT soat, versiya, eski, yangi, kim, sabab FROM audit"
                " ORDER BY id"):
            print(f"  t={soat:<2} v{v} {str(eski):<10} -> {yangi:<10} "
                  f"{kim:<5} {sabab}")
        reg2.db.close()
        print("  ⭐ Qoidalar registr kodida; audit faqat qo'shiladi")


if __name__ == "__main__":
    main()

Natijaning muhim qismi:

text
=== 1. Ro'yxatga olish ===
  v1: run run-0007, ma'lumot 9f3e41c07b2a, kod a1b2c3d
  v2: run run-0012, ma'lumot 4c7d20e9b815, kod a1b2c3d
  v3: run run-0019, ma'lumot 4c7d20e9b815, kod e5f6a7b

=== 2. Bosqich o'tishlari ===
  v1: Archived
  v2: Production
  v3: None

=== 3. Taqiqlangan o'tish ===
  v3 -> Production: ValueError - ruxsat yo'q: None -> Production
  v1 -> Production: ValueError - ruxsat yo'q: Archived -> Production

=== 4. Diskdan qayta ochish ===
  v1  Archived    auc 0.781  9f3e41c07b2a  run-0007
  v2  Production  auc 0.804  4c7d20e9b815  run-0012
  v3  Staging     auc 0.809  4c7d20e9b815  run-0019
  Production soni: 1 (invariant: 1 dan oshmaydi)

=== 5. Lineage so'rovlari ===
  Production: v2, ma'lumot 4c7d20e9b815, kod a1b2c3d, run run-0012
  ma'lumot 4c7d20e9b815 da muammo -> ta'sirlangan: ['v2 (Production)', 'v3 (Staging)']

=== 6. Audit tarixi ===
  t=1  v1 None       -> None       ali   ro'yxatga olindi
  t=2  v2 None       -> None       ali   ro'yxatga olindi
  t=3  v3 None       -> None       ali   ro'yxatga olindi
  t=4  v1 None       -> Staging    ali   sinov uchun
  t=5  v1 Staging    -> Production vali  birinchi chiqish
  t=6  v2 None       -> Staging    ali   yangi ma'lumot
  t=7  v1 Production -> Archived   vali  v2 bilan almashtirildi
  t=8  v2 Staging    -> Production vali  auc yaxshiroq
  t=9  v3 None       -> Staging    ali   kod yangilandi
  ⭐ Qoidalar registr kodida; audit faqat qo'shiladi

Natija tahlili. Registr uchta versiyani avtomatik raqamladi va har biriga lineage — ma'lumot xeshi, kod versiyasi va eksperiment run_id si — yozildi (metrikalar 27.4 dagi kuzatuvdan olingan deb faraz qilingan). 2-bo'limda v2 Production ga chiqqanda v1 avtomatik Archived bo'ldi — bitta nomda ikkita Production bo'lishi mumkin emas, va bu qoida jamoa xotirasida emas, otkaz metodida. 3-bo'limda ikki shoshqaloq urinish rad etildi: None -> Production ("to'g'ridan-to'g'ri prodga") va Archived -> Production (bu faqat rollback orqali, 3-misolda). 4-bo'limda ulanish yopilib, registr diskdan qayta ochildi — holat to'liq saqlangan, virtual soat ham auditdan tiklangan. 5-bo'lim registrning asosiy qadrini ko'rsatadi: "hozir prodda nima va u qayerdan?" — bitta so'rov; 4c7d20e9b815 ma'lumotida muammo (masalan, shaxsiy ma'lumot sizib chiqqan) topilsa, ta'sirlangan versiyalar ham bitta so'rov. 6-bo'limdagi audit har o'tishni kim va nima uchun qilganini saqlaydi — v1 ning arxivga o'tishi ham, garchi uni hech kim qo'lda qilmagan bo'lsa ham, sababi bilan yozilgan.

Misol 3 — Darvoza, orqaga qaytarish va audit

python
"""Production darvozasi (juftlashgan taqqoslash), rollback va audit."""

import sqlite3

import numpy as np
import pandas as pd
from sklearn.ensemble import HistGradientBoostingClassifier
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import roc_auc_score
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import StandardScaler

USTUNLAR = ["oylar", "tolov", "shikoyat", "oylik_tarif"]


def malumot(n, seed):
    rng = np.random.default_rng(seed)
    df = pd.DataFrame({
        "oylar": rng.integers(1, 72, n),
        "tolov": rng.normal(60, 20, n).clip(10, 150),
        "shikoyat": rng.poisson(0.8, n),
        "oylik_tarif": (rng.random(n) < 0.55).astype(int),
    })
    yangi_qimmat = (df["tolov"] > 75) & (df["oylar"] < 18)
    norozi_oylik = (df["shikoyat"] >= 3) & (df["oylik_tarif"] == 1)
    logit = (-1.5 - 0.02 * df["oylar"] + 0.5 * df["shikoyat"]
             + 1.0 * df["oylik_tarif"] + 3.0 * yangi_qimmat
             + 1.5 * norozi_oylik)
    df["ketdi"] = (rng.random(n) < 1 / (1 + np.exp(-logit))).astype(int)
    return df


def log_loss_har(y, p):
    p = np.clip(p, 1e-6, 1 - 1e-6)
    return -(y * np.log(p) + (1 - y) * np.log(1 - p))


def darvoza_yasa(X, y, min_auc):
    """Chegaralar natijadan OLDIN belgilanadi."""
    def darvoza(nomzod, prod):
        p = nomzod.predict_proba(X)[:, 1]
        auc = roc_auc_score(y, p)
        if auc < min_auc:
            return False, f"AUC {auc:.4f} < {min_auc}"
        if prod is None:
            return True, f"AUC {auc:.4f} >= {min_auc}, Production yo'q"
        d = log_loss_har(y, p) - log_loss_har(y, prod.predict_proba(X)[:, 1])
        ort, se = d.mean(), d.std(ddof=1) / np.sqrt(len(d))
        if ort > 2 * se:
            return False, f"log loss farqi {ort:+.4f} > 2*SE {2 * se:.4f}"
        return True, f"AUC {auc:.4f}, log loss farqi {ort:+.4f} (2*SE {2 * se:.4f})"
    return darvoza


class Registr:
    def __init__(self, darvoza):
        self.db = sqlite3.connect(":memory:")
        self.db.executescript(
            "CREATE TABLE versiyalar (nom TEXT, versiya INT, bosqich TEXT,"
            " izoh TEXT, PRIMARY KEY (nom, versiya));"
            "CREATE TABLE audit (id INTEGER PRIMARY KEY AUTOINCREMENT,"
            " soat INT, nom TEXT, versiya INT, eski TEXT, yangi TEXT,"
            " kim TEXT, sabab TEXT);")
        self.soat = 0
        self.artefakt = {}          # haqiqiyda: fayl ombori (4-misol)
        self.darvoza = darvoza

    def _audit(self, nom, v, eski, yangi, kim, sabab):
        self.soat += 1
        self.db.execute("INSERT INTO audit (soat, nom, versiya, eski, yangi,"
                        " kim, sabab) VALUES (?, ?, ?, ?, ?, ?, ?)",
                        (self.soat, nom, v, eski, yangi, kim, sabab))

    def _qoy(self, nom, v, yangi, kim, sabab):
        eski = self.bosqich(nom, v)
        self.db.execute("UPDATE versiyalar SET bosqich = ? WHERE nom = ?"
                        " AND versiya = ?", (yangi, nom, v))
        self._audit(nom, v, eski, yangi, kim, sabab)

    def bosqich(self, nom, v):
        return self.db.execute("SELECT bosqich FROM versiyalar WHERE nom = ?"
                               " AND versiya = ?", (nom, v)).fetchone()[0]

    def production(self, nom):
        r = self.db.execute("SELECT versiya FROM versiyalar WHERE nom = ?"
                            " AND bosqich = 'Production'", (nom,)).fetchone()
        return r[0] if r else None

    def royxatga_ol(self, nom, model, izoh, kim):
        with self.db:
            v = self.db.execute("SELECT COALESCE(MAX(versiya), 0) + 1 FROM"
                                " versiyalar WHERE nom = ?", (nom,)).fetchone()[0]
            self.db.execute("INSERT INTO versiyalar VALUES (?, ?, 'None', ?)",
                            (nom, v, izoh))
            self._audit(nom, v, None, "None", kim, izoh)
            self._qoy(nom, v, "Staging", kim, "sinovga")
        self.artefakt[(nom, v)] = model
        return v

    def kotar(self, nom, v, kim):
        joriy = self.production(nom)
        otdi, hisobot = self.darvoza(self.artefakt[(nom, v)],
                                     self.artefakt.get((nom, joriy)))
        with self.db:
            if not otdi:
                self._qoy(nom, v, "Archived", kim, "darvoza RAD: " + hisobot)
                return False, hisobot
            if joriy is not None:
                self._qoy(nom, joriy, "Archived", kim, f"v{v} bilan almashdi")
            self._qoy(nom, v, "Production", kim, "darvoza: " + hisobot)
        return True, hisobot

    def rollback(self, nom, kim, sabab):
        joriy = self.production(nom)
        r = self.db.execute(
            "SELECT versiya FROM audit WHERE nom = ? AND eski = 'Production'"
            " AND yangi = 'Archived' AND versiya != ? ORDER BY id DESC LIMIT 1",
            (nom, joriy)).fetchone()
        if r is None:
            raise RuntimeError("qaytish uchun oldingi Production yo'q")
        with self.db:                       # ikkala qadam - bitta tranzaksiya
            self._qoy(nom, joriy, "Archived", kim, "rollback: " + sabab)
            self._qoy(nom, r[0], "Production", kim, f"rollback: v{joriy} dan")
        return r[0]


def main() -> None:
    orgatish = malumot(4000, seed=1)
    val = malumot(3000, seed=2)
    Xo, yo = orgatish[USTUNLAR], orgatish["ketdi"]
    Xv, yv = val[USTUNLAR], val["ketdi"]

    buzilgan = orgatish.copy()
    buzilgan["oylar"] = buzilgan["oylar"].sample(
        frac=1.0, random_state=3).to_numpy()        # noto'g'ri join
    yangi = malumot(4000, seed=4)                   # keyingi oy ma'lumoti
    nomzodlar = {
        "logistik": make_pipeline(StandardScaler(),
                                  LogisticRegression()).fit(Xo, yo),
        "gradient boosting": HistGradientBoostingClassifier(
            max_iter=100, learning_rate=0.05, max_depth=3,
            random_state=0).fit(Xo, yo),
        "logistik (buzilgan join)": make_pipeline(
            StandardScaler(), LogisticRegression()).fit(buzilgan[USTUNLAR],
                                                        buzilgan["ketdi"]),
        "logistik (yangi oy)": make_pipeline(
            StandardScaler(), LogisticRegression()).fit(yangi[USTUNLAR],
                                                        yangi["ketdi"]),
    }

    print("=== 1. Nomzodlar validatsiyada ===")
    for nom, m in nomzodlar.items():
        p = m.predict_proba(Xv)[:, 1]
        print(f"  {nom:<26} AUC {roc_auc_score(yv, p):.4f}  "
              f"log loss {log_loss_har(yv.to_numpy(), p).mean():.4f}")

    reg = Registr(darvoza_yasa(Xv, yv.to_numpy(), min_auc=0.70))
    nom = "mijoz_ketishi"

    print("\n=== 2. Darvoza orqali chiqarish ===")
    for model_nomi, m in nomzodlar.items():
        v = reg.royxatga_ol(nom, m, model_nomi, "ali")
        otdi, hisobot = reg.kotar(nom, v, "ci")
        print(f"  v{v} {model_nomi:<26} {'OTDI' if otdi else 'RAD '}  {hisobot}")
    print(f"  hozirgi Production: v{reg.production(nom)}")

    print("\n=== 3. Incident va orqaga qaytarish ===")
    print(f"  v{reg.production(nom)} da muammo: yangi segmentda shikoyatlar")
    qaytgan = reg.rollback(nom, "navbatchi", "segment xatosi, tahlil kerak")
    print(f"  rollback -> v{qaytgan}, hozirgi Production: v{reg.production(nom)}")
    print(f"  qayta o'qitish kerak bo'ldimi: yo'q (artefakt registrda edi)")

    print("\n=== 4. Registr holati ===")
    for v, bosq, izoh in reg.db.execute(
            "SELECT versiya, bosqich, izoh FROM versiyalar ORDER BY versiya"):
        print(f"  v{v} {bosq:<11} {izoh}")

    print("\n=== 5. Audit tarixi ===")
    for soat, v, eski, yangi, kim, sabab in reg.db.execute(
            "SELECT soat, versiya, eski, yangi, kim, sabab FROM audit"
            " ORDER BY id"):
        print(f"  t={soat:<2} v{v} {str(eski):<10} -> {yangi:<10} {kim:<9} "
              f"{sabab[:44]}")

    print("\n=== 6. 'Soat t da qaysi versiya prodda edi?' ===")
    for soat in (4, 8, 15):
        r = reg.db.execute(
            "SELECT versiya FROM audit WHERE yangi = 'Production'"
            " AND soat <= ? ORDER BY soat DESC LIMIT 1", (soat,)).fetchone()
        print(f"  t={soat:<2} -> v{r[0]}")
    print("  javob auditdan, bitta so'rov bilan")
    print("  ⭐ Darvoza regressiyani to'xtatadi, rollback - bir qadam")


if __name__ == "__main__":
    main()

Natijaning muhim qismi:

text
=== 1. Nomzodlar validatsiyada ===
  logistik                   AUC 0.7334  log loss 0.5269
  gradient boosting          AUC 0.7568  log loss 0.5003
  logistik (buzilgan join)   AUC 0.6654  log loss 0.5623
  logistik (yangi oy)        AUC 0.7332  log loss 0.5265

=== 2. Darvoza orqali chiqarish ===
  v1 logistik                   OTDI  AUC 0.7334 >= 0.7, Production yo'q
  v2 gradient boosting          OTDI  AUC 0.7568, log loss farqi -0.0265 (2*SE 0.0080)
  v3 logistik (buzilgan join)   RAD   AUC 0.6654 < 0.7
  v4 logistik (yangi oy)        RAD   log loss farqi +0.0262 > 2*SE 0.0080
  hozirgi Production: v2

=== 3. Incident va orqaga qaytarish ===
  v2 da muammo: yangi segmentda shikoyatlar
  rollback -> v1, hozirgi Production: v1
  qayta o'qitish kerak bo'ldimi: yo'q (artefakt registrda edi)

=== 4. Registr holati ===
  v1 Production  logistik
  v2 Archived    gradient boosting
  v3 Archived    logistik (buzilgan join)
  v4 Archived    logistik (yangi oy)

=== 5. Audit tarixi ===
  t=1  v1 None       -> None       ali       logistik
  t=2  v1 None       -> Staging    ali       sinovga
  t=3  v1 Staging    -> Production ci        darvoza: AUC 0.7334 >= 0.7, Production yo'q
  t=4  v2 None       -> None       ali       gradient boosting
  t=5  v2 None       -> Staging    ali       sinovga
  t=6  v1 Production -> Archived   ci        v2 bilan almashdi
  t=7  v2 Staging    -> Production ci        darvoza: AUC 0.7568, log loss farqi -0.0265
  t=8  v3 None       -> None       ali       logistik (buzilgan join)
  t=9  v3 None       -> Staging    ali       sinovga
  t=10 v3 Staging    -> Archived   ci        darvoza RAD: AUC 0.6654 < 0.7
  t=11 v4 None       -> None       ali       logistik (yangi oy)
  t=12 v4 None       -> Staging    ali       sinovga
  t=13 v4 Staging    -> Archived   ci        darvoza RAD: log loss farqi +0.0262 > 2*SE 0
  t=14 v2 Production -> Archived   navbatchi rollback: segment xatosi, tahlil kerak
  t=15 v1 Archived   -> Production navbatchi rollback: v2 dan

=== 6. 'Soat t da qaysi versiya prodda edi?' ===
  t=4  -> v1
  t=8  -> v2
  t=15 -> v1
  javob auditdan, bitta so'rov bilan
  ⭐ Darvoza regressiyani to'xtatadi, rollback - bir qadam

Natija tahlili. 1-bo'limda to'rt nomzod bir xil validatsiya to'plamida baholandi: gradient boosting (AUC 0.7568) logistik regressiyadan (0.7334) yaxshi — ma'lumotda o'zaro ta'sirlar bor ("yangi va qimmat to'lovli" mijozlar), chiziqli model ularni ko'rmaydi. "Buzilgan join" — oylar ustuni boshqa mijozlarniki bilan aralashib ketgan — AUC ni 0.6654 gacha tushirdi. 2-bo'limda darvoza ikkala shartini ham ishlatdi. v1 birinchi model — faqat mutlaq chegara tekshirildi. v2 juftlashgan taqqoslashda v1 dan log loss bo'yicha 0.0265 ga yaxshi (manfiy farq) — o'tdi va v1 avtomatik arxivga ketdi. v3 mutlaq chegarada (0.6654 < 0.7) to'xtadi. v4 eng qiziq holat: u mutlaq chegaradan bemalol o'tadi (AUC 0.7332), lekin joriy Production (v2) dan log loss bo'yicha +0.0262 ga yomon, bu esa 2*SE = 0.0080 dan ancha katta — nisbiy shart uni to'xtatdi. Faqat mutlaq chegara bo'lganida, "yangi oy ma'lumoti bilan qayta o'qitilgan" oddiy model yaxshiroq modelni jimgina almashtirib yuborardi. 3-bo'limda incident simulyatsiya qilindi: v2 da oflayn metrikada ko'rinmaydigan muammo chiqdi, rollback bitta tranzaksiyada v2 ni arxivga, oldingi Production (v1) ni qaytardi — auditdan "oldingi Production kim edi" ni o'zi topdi. E'tibor bering: v1 ni darvoza orqali qaytarib bo'lmasdi (u v2 dan sezilarli yomon), shuning uchun rollback maxsus yo'l: darvozasiz, lekin sababi va bajaruvchisi auditga majburiy. 5-bo'limdagi audit to'liq tarixni saqlaydi, rad etilgan urinishlar ham sababi bilan; 6-bo'lim esa regulyator savoliga — "t paytda qaysi versiya ishlagan?" — bitta so'rov bilan javob beradi.

Misol 4 — DVC g'oyasi: kontent-manzilli saqlash noldan

python
"""Fayl xeshi -> ombor manzili: add, dedup, checkout, butunlik, gc."""

import hashlib
import tempfile
from pathlib import Path

import numpy as np
import pandas as pd
import yaml


class Ombor:
    def __init__(self, ildiz):
        self.ildiz = ildiz

    def _yol(self, xesh):
        return self.ildiz / xesh[:2] / xesh[2:]

    def qosh(self, fayl):
        """dvc add: faylni omborga, yoniga .dvc ko'rsatkich."""
        baytlar = fayl.read_bytes()
        xesh = hashlib.sha256(baytlar).hexdigest()
        yol = self._yol(xesh)
        yangi = not yol.exists()
        if yangi:
            yol.parent.mkdir(parents=True, exist_ok=True)
            yol.write_bytes(baytlar)
        korsatkich = {"outs": [{"path": fayl.name, "sha256": xesh,
                                "size": len(baytlar)}]}
        matn = yaml.safe_dump(korsatkich, sort_keys=False)
        (fayl.parent / (fayl.name + ".dvc")).write_text(matn, encoding="utf-8")
        return matn, yangi

    def chiqar(self, korsatkich_matni, papka):
        """dvc checkout: ko'rsatkich bo'yicha faylni tiklash (tekshirib)."""
        chiqish = yaml.safe_load(korsatkich_matni)["outs"][0]
        baytlar = self._yol(chiqish["sha256"]).read_bytes()
        if hashlib.sha256(baytlar).hexdigest() != chiqish["sha256"]:
            raise ValueError("ombordagi obyekt buzilgan")
        (papka / chiqish["path"]).write_bytes(baytlar)
        return chiqish["path"]

    def obyektlar(self):
        return sorted(p for p in self.ildiz.rglob("*") if p.is_file())

    def tekshir(self):
        buzilgan = []
        for p in self.obyektlar():
            kutilgan = p.parent.name + p.name
            if hashlib.sha256(p.read_bytes()).hexdigest() != kutilgan:
                buzilgan.append(kutilgan[:12])
        return buzilgan

    def gc(self, korsatkichlar):
        kerak = {yaml.safe_load(k)["outs"][0]["sha256"] for k in korsatkichlar}
        ochirildi = 0
        for p in self.obyektlar():
            if p.parent.name + p.name not in kerak:
                p.unlink()
                ochirildi += 1
        return ochirildi


def malumot(n, seed):
    rng = np.random.default_rng(seed)
    return pd.DataFrame({
        "mijoz_id": np.arange(n),
        "oylar": rng.integers(1, 72, n),
        "tolov": rng.normal(60, 20, n).round(2),
        "ketdi": rng.integers(0, 2, n),
    })


def holat(ombor):
    obyektlar = ombor.obyektlar()
    return len(obyektlar), sum(p.stat().st_size for p in obyektlar)


def main() -> None:
    with tempfile.TemporaryDirectory() as t:
        ish = Path(t) / "ish"
        ish.mkdir()
        ombor = Ombor(Path(t) / "ombor")
        fayl = ish / "mijozlar.csv"
        git_tarixi = []          # (commit xabari, ko'rsatkich matni)

        print("=== 1. dvc add: birinchi versiya ===")
        v1 = malumot(2000, seed=0)
        v1.to_csv(fayl, index=False, lineterminator="\n")
        matn, yangi = ombor.qosh(fayl)
        git_tarixi.append(("ma'lumot v1", matn))
        print("  mijozlar.csv.dvc:")
        for qator in matn.splitlines():
            if "sha256" in qator:
                qator = f"{qator[:22]}(+{len(qator) - 22} belgi)"
            print(f"    {qator}")
        n, hajm = holat(ombor)
        korsatkich_hajmi = (ish / "mijozlar.csv.dvc").stat().st_size
        print(f"  omborda: {n} obyekt, {hajm} bayt; git dagi ko'rsatkich: "
              f"{korsatkich_hajmi} bayt")

        print("\n=== 2. Dedup: bir xil mazmun - bitta obyekt ===")
        nusxa = ish / "mijozlar_nusxa.csv"
        nusxa.write_bytes(fayl.read_bytes())
        _, yangi = ombor.qosh(nusxa)
        print(f"  nusxa qo'shildi, yangi obyekt yaratildimi: {yangi}")
        print(f"  omborda: {holat(ombor)[0]} obyekt")
        nusxa.unlink()
        (ish / "mijozlar_nusxa.csv.dvc").unlink()

        print("\n=== 3. Ma'lumot o'zgardi: ikkinchi versiya ===")
        v2 = pd.concat([v1, malumot(500, seed=1).assign(
            mijoz_id=lambda d: d["mijoz_id"] + 2000)], ignore_index=True)
        v2.to_csv(fayl, index=False, lineterminator="\n")
        matn2, yangi = ombor.qosh(fayl)
        git_tarixi.append(("ma'lumot v2: +500 mijoz", matn2))
        n, hajm = holat(ombor)
        print(f"  yangi obyekt: {yangi}; omborda: {n} obyekt, {hajm} bayt")
        for xabar, k in git_tarixi:
            x = yaml.safe_load(k)["outs"][0]
            print(f"  commit '{xabar}': sha256 {x['sha256'][:12]}, "
                  f"{x['size']} bayt")

        print("\n=== 4. Checkout: v1 ga qaytish ===")
        ombor.chiqar(git_tarixi[0][1], ish)
        tiklangan = pd.read_csv(fayl)
        print(f"  ishchi fayl qatorlari: {len(tiklangan)}")
        print(f"  v1 bilan aynan teng: {tiklangan.equals(v1)}")
        ombor.chiqar(git_tarixi[1][1], ish)
        print(f"  yana v2 ga: {len(pd.read_csv(fayl))} qator")

        print("\n=== 5. gc: ishora qilinmagan obyektlar ===")
        (ish / "sinov.csv").write_text("a,b\n1,2\n", encoding="utf-8")
        ombor.qosh(ish / "sinov.csv")          # commit qilinmagan tajriba
        print(f"  gc dan oldin: {holat(ombor)[0]} obyekt")
        ochdi = ombor.gc([k for _, k in git_tarixi])
        print(f"  o'chirildi: {ochdi}; qoldi: {holat(ombor)[0]} obyekt")

        print("\n=== 6. Butunlik tekshiruvi ===")
        print(f"  buzilgan obyektlar: {ombor.tekshir()}")
        birinchi = ombor.obyektlar()[0]
        baytlar = bytearray(birinchi.read_bytes())
        baytlar[100] ^= 1                       # bitta bit o'zgardi
        birinchi.write_bytes(bytes(baytlar))
        print(f"  bitta bit o'zgargandan keyin: {ombor.tekshir()}")
        buzilgan_xesh = birinchi.parent.name + birinchi.name
        for xabar, k in git_tarixi:
            if yaml.safe_load(k)["outs"][0]["sha256"] == buzilgan_xesh:
                try:
                    ombor.chiqar(k, ish)
                except ValueError as xato:
                    print(f"  checkout '{xabar}': ValueError - {xato}")
        print("  ⭐ Manzil = mazmun xeshi: dedup, tiklash va butunlik bepul")


if __name__ == "__main__":
    main()

Natijaning muhim qismi:

text
=== 1. dvc add: birinchi versiya ===
  mijozlar.csv.dvc:
    outs:
    - path: mijozlar.csv
      sha256: 525cd44cd353(+52 belgi)
      size: 30524
  omborda: 1 obyekt, 30524 bayt; git dagi ko'rsatkich: 120 bayt

=== 2. Dedup: bir xil mazmun - bitta obyekt ===
  nusxa qo'shildi, yangi obyekt yaratildimi: False
  omborda: 1 obyekt

=== 3. Ma'lumot o'zgardi: ikkinchi versiya ===
  yangi obyekt: True; omborda: 2 obyekt, 68945 bayt
  commit 'ma'lumot v1': sha256 525cd44cd353, 30524 bayt
  commit 'ma'lumot v2: +500 mijoz': sha256 90707916b0aa, 38421 bayt

=== 4. Checkout: v1 ga qaytish ===
  ishchi fayl qatorlari: 2000
  v1 bilan aynan teng: True
  yana v2 ga: 2500 qator

=== 5. gc: ishora qilinmagan obyektlar ===
  gc dan oldin: 3 obyekt
  o'chirildi: 1; qoldi: 2 obyekt

=== 6. Butunlik tekshiruvi ===
  buzilgan obyektlar: []
  bitta bit o'zgargandan keyin: ['525cd44cd353']
  checkout 'ma'lumot v1': ValueError - ombordagi obyekt buzilgan
  ⭐ Manzil = mazmun xeshi: dedup, tiklash va butunlik bepul

Natija tahlili. 1-bo'limda dvc add ning mohiyati ko'rindi: 30524 baytli CSV omborga fayl xeshi nomi bilan joylandi, git ga esa faqat 120 baytli ko'rsatkich (path, sha256, size) tushadi. Ma'lumot 100 marta katta bo'lsa ham ko'rsatkich o'sha hajmda qoladi. 2-bo'limda aynan bir xil mazmunli nusxa qo'shildi — xesh bir xil, demak yangi obyekt yaratilmadi (dedup). 3-bo'limda ma'lumotga 500 mijoz qo'shildi — yangi xesh, yangi obyekt, eski obyekt esa joyida: ombor endi 2 obyekt, 68945 bayt, har commit o'z ko'rsatkichiga ega. 4-bo'limdagi "checkout" ko'rsatkichdagi xesh bo'yicha faylni tikladi va u v1 bilan aynan teng chiqdi — git checkout + dvc checkout juftligi shu. 5-bo'limda commit qilinmagan tajriba fayli ombordagi ortiqcha obyekt bo'lib qoldi; gc faqat git tarixidagi ko'rsatkichlar ishora qilgan obyektlarni saqladi va bittasini o'chirdi. 6-bo'lim kontent-manzilli saqlashning bepul bonusini ko'rsatadi: obyekt nomi — uning xeshi, shuning uchun bitta bit o'zgarishi ham darhol aniqlanadi va buzilgan ma'lumot ishchi papkaga tiklanmaydi. Haqiqiy DVC xuddi shu g'oyani katta hajmda, masofaviy omborlar (S3, GCS, SSH) va quvur keshi (dvc repro) bilan amalga oshiradi.


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

Noto'g'ri fikr To'g'risi
"Model versiyasi — bu .pkl fayl" Versiya — kod, ma'lumot, konfig va muhit uchligi; fayl — natija
"Ma'lumotni sana bilan nomlash yetarli" iyun.csv keyin o'zgaradi; mazmun xeshi o'zgarmas nom
"Xom fayl xeshi mazmunni aniqlaydi" Qator tartibi, \r\n, float formati — boshqa xesh; kanonik shakl kerak
"Eksperiment kuzatuvi registr o'rnini bosadi" Kuzatuv — hamma tajriba; registr — nomlangan, bosqichli, auditli modellar
"Arxivdagi modellarni o'chirsa bo'ladi" Rollback va regulyator savollari uchun ular kerak
"Darvoza — yangi model yaxshiroqmi degan savol" Darvoza — regressiya yo'qligi; "arziydimi" — alohida qaror
"AUC 0.002 ga oshdi — demak yaxshiroq" Juftlashgan farq va SE ni ko'rmasdan bu shovqin bo'lishi mumkin
"Rollback — favqulodda holat, sinash shart emas" Sinalmagan rollback kerakli paytda ishlamaydi
"DVC ma'lumotni git ga joylaydi" Git da faqat kichik ko'rsatkich; mazmun alohida omborda

6. Keng tarqalgan xatolar va yechimlari

1. Artefaktni ustiga yozish

python
joblib.dump(model, "modellar/model_final.pkl")                     # ⚠️
joblib.dump(model, f"modellar/{nom}/{versiya}/model.joblib")       # ✅ + registr

2. Ma'lumotni nom bilan "versiyalash"

python
manifest = {"malumot": "data/iyun.csv"}                            # ⚠️
manifest = {"malumot": "data/iyun.csv",
            "malumot_xeshi": malumot_xeshi(df)}                    # ✅

3. Kodni git holatisiz yozish

python
kod_versiyasi = "main"                                             # ⚠️ o'zgaradi
kod_versiyasi = git_commit + ("-dirty" if ozgargan_fayllar else "")  # ✅

4. To'g'ridan-to'g'ri Production

python
reg.otkaz(nom, v, "Production", kim, "shoshilinch")                # ⚠️ None -> Production
reg.otkaz(nom, v, "Staging", kim, "sinov"); reg.kotar(nom, v, kim)  # ✅ darvoza orqali

5. Darvozada juftlashmagan taqqoslash

python
otdi = auc_nomzod > auc_prod                                       # ⚠️ shovqin
d = ll_nomzod - ll_prod; otdi = d.mean() <= 2 * d.std(ddof=1) / np.sqrt(len(d))  # ✅

6. Rollback uchun qayta o'qitish

python
model = orgat(eski_malumot, eski_konfig)                           # ⚠️ soatlar, boshqa natija
reg.rollback(nom, "navbatchi", "incident #123")                    # ✅ soniyalar, aynan o'sha

7. Auditni tahrirlash

python
db.execute("DELETE FROM audit WHERE kim = 'men'")                  # ⚠️
# audit faqat INSERT; tuzatish - yangi yozuv bilan                  # ✅

7. Integratsiya — bu bilim qayerda kerak bo'ladi

  • 19-qism (o'tilgan): Pipeline va joblib — registrga yoziladigan artefakt
  • 18-qism (o'tilgan): juftlashgan taqqoslash va SE — darvozaning asosi
  • 26.10-dars (o'tilgan): model kartasi — registr yozuviga biriktiriladi
  • 27.2-dars (o'tilgan): muhit va requirements.lock — uchlikning to'rtinchi a'zosi
  • 27.3-dars (o'tilgan): ma'lumot quvuri — ma'lumot xeshi shu yerda hisoblanadi
  • 27.4-dars (o'tilgan): eksperiment kuzatuvi — run_id registrdagi lineage ga ulanadi
  • 27.6-dars: Modelni paketlash — registrdagi artefakt aslida nima ekanini aniqlaymiz
  • 27.10-dars: Bulut va deploy strategiyalari — canary va rollback amalda
  • 27.13-dars: Qayta o'qitish va A/B test — yangi versiyalar registrga shu jarayon orqali keladi

8. Eng yaxshi amaliyotlar

  1. Har model versiyasi uchun manifest: kod versiyasi, ma'lumot xeshi, konfig, muhit, artefakt xeshi, run_id.

  2. Ma'lumot xeshini kanonik shaklda hisoblang va qaysi normallashtirish qilinganini hujjatlang.

  3. Semantik versiyani iste'molchi nuqtai nazaridan qo'ying: shartnoma buzilsa — MAJOR.

  4. Bosqich qoidalari registr kodida — None -> Production va ikkita Production imkonsiz.

  5. Darvoza oldindan yozilgan va avtomatik: mutlaq chegara + juftlashgan "sezilarli yomon emas".

  6. Artefaktlarni o'chirmang, Archived — faqat bosqich.

  7. Audit append-only, har o'tishda kim va sabab majburiy.

  8. Rollback ni muntazam sinang — xuddi yong'in mashqi kabi.

  9. Katta fayllar git da emas — kontent-manzilli omborda, git da ko'rsatkich.


9. Amaliy topshiriq

Vazifa 1: Bashorat qiling

python
1.  # model versiyasining uchligi nimalardan iborat?
2.  # qatorlar aralashtirilsa, xom xesh o'zgaradimi? kanonik-chi?
3.  # yangi majburiy kirish ustuni - qaysi semver qismi?
4.  # yangi ma'lumot bilan qayta o'qitish - qaysi qismi?
5.  # model kartasidagi imlo tuzatilsa?
6.  # None -> Production ruxsatmi?
7.  # yangi versiya Production ga chiqsa, eskisi nima bo'ladi?
8.  # darvozaning ikki sharti?
9.  # nega darvozada juftlashgan farq?
10. # rollback nega tez?
11. # DVC da git nimani saqlaydi?
12. # bir xil fayl ikki marta qo'shilsa, omborda nechta obyekt?
Javoblar
  1. Kod versiyasi, ma'lumot xeshi, konfig (va muhit) — natijada artefakt
  2. Xom — ha; kanonik — yo'q (tartib normallashtiriladi)
  3. MAJOR
  4. MINOR
  5. PATCH
  6. Yo'q — faqat Staging orqali va darvoza bilan
  7. Avtomatik Archived (audit bilan)
  8. Mutlaq metrika chegarasi va joriy Production dan sezilarli yomon emaslik
  9. Bir xil namunalarda farq shovqini kichik; SE bilan sezilarlilik baholanadi
  10. Artefakt registrda turibdi — qayta o'qitish shart emas
  11. Faqat kichik ko'rsatkich fayli (xesh va hajm)
  12. Bitta

Vazifa 2: Xatolarni tuzating

python
1.  joblib.dump(model, "model_final.pkl")

2.  manifest = {"malumot": "data/iyun.csv"}

3.  reg.otkaz("churn", 5, "Production", "men", "tez kerak")

4.  otdi = auc_yangi > auc_eski

5.  db.execute("DELETE FROM audit WHERE versiya = 3")
Javoblar
python
1.  joblib.dump(model, f"modellar/{nom}/{versiya}/model.joblib")   # + registrga yozuv

2.  manifest = {"malumot": "data/iyun.csv", "malumot_xeshi": malumot_xeshi(df)}

3.  reg.otkaz("churn", 5, "Staging", "men", "sinov"); reg.kotar("churn", 5, "men")

4.  d = ll_yangi - ll_eski; otdi = d.mean() <= 2 * d.std(ddof=1) / np.sqrt(len(d))

5.  # audit o'chirilmaydi; xato yozuv bo'lsa - tuzatuvchi yangi yozuv qo'shiladi

Vazifa 3: Kanonik xesh

malumot_xeshi ga ikkita parametr qo'shing: kalit (tartiblash ustuni) va onlik (float aniqligi). Tekshiring:

  1. Ustunlar tartibi o'zgarsa xesh o'zgarmaydi
  2. onlik=2 da +0.001 o'zgarish xeshni o'zgartirmaydi, onlik=6 da o'zgartiradi
  3. Bu xossa qachon foydali, qachon xavfli?

Vazifa 4: Registrga model kartasi

2-misoldagi registrga kartalar jadvalini qo'shing (nom, versiya, maqsad, cheklovlar, egasi). Qoidalar:

  1. Kartasiz versiya Staging -> Production ga o'tolmaydi
  2. Karta tahrirlansa — auditga yozuv (sabab = "karta PATCH")
  3. Production modelning kartasini bitta so'rov bilan chiqaring

Vazifa 5: Vaqt bo'yicha so'rov

3-misol auditidan foydalanib qaysi_versiya(nom, soat) funksiyasini yozing: berilgan virtual soatda qaysi versiya Production da edi (rollback ni ham hisobga oling). Har soat uchun jadval chiqaring.

Vazifa 6: Kontent-manzilli ombor kengaytmasi

4-misoldagi Ombor ni kengaytiring:

  1. Papka qo'shish: har fayl alohida obyekt, papka uchun esa fayllar ro'yxatidan iborat "daraxt" obyekti (uning xeshi — ro'yxat xeshi)
  2. Bitta fayl o'zgarsa — nechta yangi obyekt paydo bo'ladi?
  3. push: ikkinchi ombor (masofaviy o'rnida) ga faqat unda yo'q obyektlarni ko'chiring va nechta ko'chganini chiqaring

Vazifa 7: O'ylash

Jamoangiz har hafta modelni avtomatik qayta o'qitadi va darvozadan o'tsa, avtomatik Production ga chiqaradi. Bir kun biznes bo'limi shikoyat qildi: "so'nggi ikki oyda model sekin-asta yomonlashdi, lekin har hafta darvozadan o'tgan". Qanday bo'lishi mumkin va darvozani qanday tuzatasiz?

Javob

Qisqa javob: darvoza har safar yangi modelni joriy Production bilan solishtiradi. Har hafta yangi model oldingisidan "sezilarli yomon emas" — masalan, farq 0.3*SE — lekin sakkiz hafta ketma-ket shunday kichik pasayishlar yig'ilib, sezilarli yomonlashuvga aylanadi. Bu "qaynayotgan qurbaqa" effekti: har qadam chegaradan past, yig'indi esa katta.

1. Mutlaq chegara ham bor edi — nega ushlamadi?

Mutlaq chegara (masalan, AUC >= 0.75) odatda "minimal qabul qilinadigan" darajada qo'yiladi. Model 0.82 dan boshlab 0.77 gacha tushsa ham chegaradan yuqorida qoladi.

2. Tuzatishlar

Muammo Tuzatish
Faqat joriy bilan taqqoslash Langar (anchor) model bilan ham taqqoslash — masalan, oxirgi chorakdagi eng yaxshi Production
Validatsiya to'plami har hafta yangi Qat'iy golden validatsiya to'plami ham saqlanadi — farqlar bir xil o'lchov bilan
Chegara past Mutlaq chegarani vaqti-vaqti bilan qayta ko'rib chiqish (lekin natijadan oldin, hujjatlangan holda)
Faqat umumiy metrika Segmentlar bo'yicha darvoza: yomonlashuv ko'pincha bitta segmentdan boshlanadi

3. Kuzatuv

Registr auditi va metrikalar tarixidan har bir Production versiyasining langar to'plamidagi metrikasini vaqt qatori sifatida chizing. Sekin trend ko'z bilan darhol ko'rinadi — bu 27.11 (Monitoring) va 27.12 (Drift aniqlash) darslarining mavzusi.

4. Halol chegara

Ba'zan metrika pasayishi modelning aybi emas: dunyo o'zgargan va vazifa qiyinlashgan (masalan, raqobatchi aksiya boshlagan). Buni ajratish uchun langar model ham yangi ma'lumotda baholanadi: agar langar ham xuddi shunday pasaygan bo'lsa — muammo ma'lumotda; agar faqat yangi modellar pasaygan bo'lsa — muammo o'qitish jarayonida.

Xulosa: darvoza bitta qadamni tekshiradi, trendni emas. Joriy modeldan tashqari, qat'iy langar model va qat'iy golden to'plam bilan ham solishtiring — shunda kichik qadamlar yig'indisi ham ko'rinadi.

Nimani mustahkamlaydi: 2.5, 2.6, 2.7-bo'limlar.


Xulosa

Bu darsda modelni kod, ma'lumot va konfig uchligi sifatida versiyaladik va noldan model registr qurdik.

Eng muhim uch fikr:

  1. Model versiyasi — uchlik, fayl emas. 1-misolda manifest (ma'lumot xeshi, kod versiyasi, konfig xeshi, artefakt xeshi) bo'yicha model qayta qurildi va artefakt bitma-bit mos keldi. Kanonik ma'lumot xeshi qatorlar tartibiga befarq, lekin bitta qiymatning +0.01 o'zgarishini ushlaydi. Ma'lumot, kod yoki konfig — qaysi biri o'zgarsa, model xeshi o'zgaradi; sababni esa uchlikning qolgan a'zolari ko'rsatadi. Semantik versiya iste'molchi nuqtai nazaridan: shartnoma buzilsa — MAJOR.

  2. Registr — qoidalar kodda, tarix auditda. 2-misolda None -> Production va Archived -> Production rad etildi, yangi Production chiqqanda eskisi avtomatik arxivga o'tdi va bitta nomda ikkita Production bo'lmadi. Lineage "prodda nima va qayerdan?" va "bu ma'lumotdan qaysi modellar?" savollariga bitta so'rov bilan javob beradi. 3-misolda darvozaning ikkala sharti ishladi: buzilgan join mutlaq chegarada (AUC 0.6654 < 0.7) to'xtadi, "yangi oy" logistik modeli esa mutlaq chegaradan o'tsa ham joriy Production dan log loss bo'yicha +0.0262 ga yomon edi (2*SE = 0.0080) — nisbiy shart uni to'xtatdi. Rollback bitta tranzaksiyada oldingi Production ni qaytardi — qayta o'qitishsiz.

  3. DVC g'oyasi — manzil = mazmun xeshi. 4-misolda git ga 120 baytli ko'rsatkich tushdi, 30 KB li ma'lumot esa omborda; bir xil mazmun bir marta saqlandi, eski versiya aynan tiklandi, gc ortiqcha obyektni tozaladi, bitta bitlik buzilish esa darhol aniqlandi.

Keyingi darsda Modelni paketlash: registrdagi artefakt aslida nima bo'lishi kerakligini aniqlaymiz — serializatsiya xavflari, versiya mosligi, sxema, nazorat namunalari va bitta Predictor interfeysi.

Ulashish:Telegram'da

Izohlar (0)

Izoh yozish uchun kiring.

  • Hozircha izoh yo'q. Birinchi bo'ling!
27.5-dars: Model versiyalash va registr — IlmHamroh