IlmHamroh
Data Science va sun'iy intellekt/MLOps va deploy2/14-dars35 daqiqa
Mundarija (23)

27.2-dars: Reproduksiya va muhit

27-QISM — MLOPS VA DEPLOY · 2-dars


1. Kirish va motivatsiya

27.1-darsda ML hayot siklining har bosqichi iz qoldirishi kerakligini ko'rdik: kichik quvurimiz takroriy yurishda aynan bir xil xeshlar berdi. Lekin bu o'z-o'zidan bo'lmadi — urug'lar qo'yilgan, ma'lumot generatsiyasi deterministik edi va hammasi bitta jarayonda, bitta mashinada ishladi. Haqiqiy hayotda model boshqa kuni, boshqa kompyuterda, boshqa kutubxona versiyasi bilan, ba'zan boshqa odam tomonidan qayta o'qitiladi. Shu savolga javob beramiz: natijani istalgan vaqtda qayta olish uchun nimani qayd qilish kerak?

19.7-darsda sklearn doirasida takrorlanuvchanlikni (random_state shakllari, oqimlar, versiyalarni qulflash), 21.11-darsda esa PyTorch loyihasida seed_everything va konfigni ko'rgan edik. Bu dars ularni tizim darajasiga ko'taradi: bir necha kutubxonaning urug'lari qanday ierarxiya hosil qiladi, interpretator darajasidagi PYTHONHASHSEED qanday qilib "urug' qo'yilgan" skriptni ham buzadi, muhit, ma'lumot va konfigni qanday qilib bitta reproduksiya manifesti ga yig'ish mumkin — va nihoyat, aynan takrorlangan natija ham nega ishonchli natija emas.

Real vaziyat. Jamoa auditorga modelning AUC 0.758 ekanini ko'rsatishi kerak edi. Kodni qayta ishga tushirishdi — 0.741. Yana — 0.749. Urug'lar qo'yilgan, ma'lumot o'zgarmagan, kod o'sha commit dan. Ikki kunlik qidiruvdan keyin topildi: kategoriyalarni kodlash list(set(...)) bilan qilingan va Python har yangi jarayonda satr xeshlarini tasodifiy urug' bilan hisoblaydi — ya'ni "Toshkent" bir ishga tushishda 0, boshqasida 6 bo'ladi. Bu darsning 1-misolida xuddi shu vaziyatni takrorlaymiz va o'lchaymiz.

Bu darsda natijani qayta olish uchun nimani nazorat qilish va nimani qayd qilish kerakligini — va buni avtomatik tekshirishni o'rganamiz.

Bu darsda:

  • Reproduksiya darajalari
  • Nega natija qaytarilmaydi: olti manba
  • Urug'lar ierarxiyasi
  • PYTHONHASHSEED va hash() tuzog'i
  • Muhitni qayd qilish: lock, platforma, ma'lumot va konfig xeshi
  • Reproduksiya manifesti va uni tekshirish
  • Urug'ga sezgirlik: takrorlanadigan natija ham bitta namuna
  • Muhit boshqaruvchilari: venv, conda, uv, poetry, Docker
  • Amaliy: jarayonlararo reproduksiya testi, manifest va urug'lar bo'yicha tarqoqlik

ℹ Misollar real numpy/sklearn/torch bilan (Python 3.14). Yangi jarayonlar subprocess bilan ishga tushiriladi; fayllar faqat vaqtinchalik papkada.


2. Nazariya — chuqur tushuntirish

2.1. Reproduksiya darajalari

Adabiyotda atamalar turlicha ishlatiladi. Bu qismda quyidagi uch darajani farqlaymiz:

text
1. TAKRORLANUVCHANLIK (bit darajasida)
   bir xil kod + bir xil ma'lumot + bir xil muhit + bir xil urug'
   -> AYNAN bir xil bashoratlar (xeshlari teng)
   kim uchun: debug, audit, regressiya testlari, CI

2. REPRODUKSIYA (statistik)
   bir xil kod + bir xil ma'lumot, lekin boshqa mashina/versiya/urug'
   -> metrika ishonch oralig'i ichida, XULOSA o'zgarmaydi
   kim uchun: hisobot, maqola, qaror

3. REPLIKATSIYA (umumlashish)
   yangi ma'lumot (keyingi oy, boshqa hudud)
   -> xulosa hali ham to'g'rimi?
   kim uchun: ishlab chiqarish, monitoring 27.11-bob

MUHIM:
  1-daraja 2-darajaning o'rnini BOSMAYDI:
    aynan takrorlanadigan natija ham bitta tasodifiy namuna bo'lishi mumkin
  2-daraja 1-darajasiz QIYIN:
    natija o'zgarganda sababni topish uchun avval uni takrorlash kerak

Bit darajasidagi takrorlash — debug va audit uchun; xulosaning ishonchliligi — bir necha urug' va tarqoqlik uchun. Ikkalasi ham kerak.

2.2. Nega natija qaytarilmaydi: olti manba

text
1. URUG'LAR
   o'rnatilmagan yoki faqat bittasi o'rnatilgan (python, lekin numpy emas)
   random_state=None -> global holatga bog'liq 19.7-bob

2. TARTIB
   set / dict tartibi (satrlar uchun PYTHONHASHSEED ga bog'liq)
   fayl ro'yxati tartibi (os.listdir, glob - fayl tizimiga bog'liq)
   SQL so'rov natijasi (ORDER BY siz tartib kafolatlanmagan)
   qatorlar tartibi -> SGD, bootstrap, bo'lish natijasi o'zgaradi

3. MA'LUMOT
   "o'sha jadval" - lekin kecha yangi qatorlar qo'shilgan
   kechikib kelgan yozuvlar, tuzatilgan xatolar
   -> ma'lumot XESHI yoki snapshot/versiya 27.3-bob

4. KUTUBXONA VERSIYALARI
   sukut parametr o'zgaradi (masalan, solver, max_iter)
   algoritm ichki tartibi o'zgaradi
   pickle/joblib boshqa versiyada ochilmasligi mumkin 19.6-bob

5. FLOAT VA PARALLEL HISOB
   (a + b) + c != a + (b + c)
   oqimlar soni -> yig'indi tartibi (BLAS, OpenMP, n_jobs)
   GPU: atom amallari tartibi aniqlanmagan (cuDNN algoritmlari)

6. MUHIT
   muhit o'zgaruvchilari (PYTHONHASHSEED, OMP_NUM_THREADS,
     CUBLAS_WORKSPACE_CONFIG, TZ, LANG)
   apparat (CPU ko'rsatmalar to'plami, GPU modeli)
   operatsion tizim (fayl tizimi, satr oxiri CRLF/LF)

Bu manbalarning eng xavflisi — jim ishlaydiganlari. Urug' qo'yilmagani ko'pincha ko'rinib turadi (natija har safar boshqa). set tartibi esa bitta jarayon ichida barqaror: siz noutbukda katakni necha marta ishga tushirmang, natija bir xil — faqat yangi jarayonda o'zgaradi. Shuning uchun reproduksiya testi har doim yangi jarayonda qilinadi (1-misol).

Reproduksiyani "bir xil noutbukda ikki marta" emas, "ikki yangi jarayonda" tekshiring.

2.3. Urug'lar ierarxiyasi

text
INTERPRETATOR DARAJASI (jarayon boshlanishidan OLDIN):
  PYTHONHASHSEED           -> str/bytes hash i -> set/dict tartibi

JARAYON DARAJASI (global generatorlar):
  random.seed(s)           -> Python random moduli
  np.random.seed(s)        -> numpy GLOBAL holati (eski API)
  torch.manual_seed(s)     -> torch CPU (+ CUDA) global generatori
  torch.cuda.manual_seed_all(s)  -> barcha GPU lar

OBYEKT DARAJASI (tavsiya etiladi - mustaqil va aniq):
  rng = np.random.default_rng(s)         -> o'z Generator i
  rng.spawn(k)                           -> k ta MUSTAQIL oqim
  RandomForestClassifier(random_state=s) -> sklearn (int!)
  DataLoader(..., generator=torch.Generator().manual_seed(s))

ISHCHI JARAYONLAR (DataLoader num_workers > 0):
  har ishchi: torch urug'i = base_seed + worker_id
  torch python random va numpy global holatini ham o'zi o'rnatadi
  O'Z generatoringiz bo'lsa (Dataset ichida default_rng) -
    worker_init_fn ichida get_worker_info().seed dan hosil qiling

DETERMINISTIK ALGORITMLAR (GPU):
  torch.use_deterministic_algorithms(True)
  torch.backends.cudnn.benchmark = False
  CUBLAS_WORKSPACE_CONFIG=:4096:8

Qoidalar: (1) har kutubxonaning o'z urug'i bor — random.seed numpy ga ta'sir qilmaydi; (2) global holatga tayanish mo'rt — orada boshqa kod bitta tasodifiy son olsa, hamma narsa siljiydi; (3) eng ishonchlisi — urug'ni obyektga berish (random_state=int, default_rng, generator=).

Urug' — konfigning bir qismi. U konfig faylida turadi, manifestga yoziladi va kod ichida "sehrli son" bo'lmaydi.

2.4. PYTHONHASHSEED va hash() tuzog'i

text
Python 3.3+ da xavfsizlik uchun (hash-flooding hujumiga qarshi)
str va bytes hash i HAR JARAYONDA tasodifiy "tuz" bilan hisoblanadi.

  hash("salom")  -> jarayon A:  7213...,  jarayon B: -4410...
  hash(12345)    -> har doim 12345 (int tuzlanmaydi)

OQIBAT:
  set(satrlar) tartibi - jarayonga bog'liq
  dict tartibi - QO'SHILISH tartibida (3.7+), xeshga emas -> barqaror
  lekin dict(set(...)) yoki {k: i for i, k in enumerate(set(...))} -> YO'Q

BOSHQARISH:
  PYTHONHASHSEED=0 python orgat.py     # interpretator ISHGA TUSHISHIDAN oldin
  os.environ["PYTHONHASHSEED"] = "0"   # skript ichida - KECH, ta'sir qilmaydi
                                       # (faqat bola jarayonlarga o'tadi)

TO'G'RI YECHIM:
  hash() ga tayanmang: sorted(set(...)), barqaror xesh (hashlib)
  PYTHONHASHSEED ni qat'iylash - qo'shimcha himoya, asosiy yechim emas

Yana bir tipik holat: belgini "xeshlash" (hash(foydalanuvchi_id) % 1000 bilan savatga ajratish). Bu o'qitishda bir xil, xizmatda esa boshqa jarayonda boshqa savat beradi — bu training-serving skew 27.1-bob. Barqaror variant: int(hashlib.md5(s.encode()).hexdigest(), 16) % 1000.

hash() — jarayon ichidagi vosita, identifikator emas. Jarayonlar orasida barqaror bo'lishi kerak bo'lgan har narsa uchun hashlib va sorted ishlating.

2.5. Muhitni qayd qilish

text
NIMA QAYD QILINADI:
  python versiyasi         platform.python_version()
  platforma                platform.system(), platform.machine()
  paketlar (lock)          importlib.metadata.version(nom) -> "numpy==2.5.3"
  kod versiyasi            git rev-parse HEAD + git status --porcelain (toza?)
  ma'lumot                 sha256(fayl baytlari) yoki snapshot ID
  konfig                   konfig + sha256(kanonik JSON)
  urug'lar                 konfig ichida
  muhit o'zgaruvchilari    PYTHONHASHSEED, OMP_NUM_THREADS, ... (sirlar EMAS!)
  natija                   metrikalar + bashorat xeshi

KANONIK JSON (konfig xeshi uchun):
  json.dumps(k, sort_keys=True, separators=(",", ":"))
  -> kalit tartibi ahamiyatsiz, bo'sh joylar ahamiyatsiz

pip freeze VA importlib.metadata:
  pip freeze      - butun muhit, tashqi buyruq
  importlib.metadata - kod ichidan, faqat kerakli paketlar

Ikki narsaga e'tibor bering. Birinchisi — git holati: commit xeshi yetarli emas, agar ishchi papkada saqlanmagan o'zgarishlar bo'lsa ("dirty"). Manifestda toza: false bo'lsa, natijani aynan qayta olish mumkin emas — bu holatni ogohlantirish yoki taqiqlash kerak. Ikkinchisi — sirlar: muhit o'zgaruvchilarini qayd qilishda faqat ruxsat etilgan ro'yxatdagilarni yozing; DB_PAROL yoki API_KALIT manifestga tushmasligi kerak.

Ma'lumot va konfig — xesh bilan, kod — commit bilan, muhit — lock bilan. To'rttasi birga natijaning "manzili".

2.6. Reproduksiya manifesti va uni tekshirish

text
manifest.json:
{
  "python": "3.14.5",
  "platforma": {"tizim": "Windows", "arxitektura": "AMD64"},
  "paketlar": {"numpy": "2.5.3", "scikit-learn": "1.9.1", ...},
  "git": {"commit": "a1b2c3...", "toza": true},
  "muhit": {"PYTHONHASHSEED": "0", "OMP_NUM_THREADS": "1"},
  "malumot": {"fayl": "malumot.csv", "sha256": "952c6655a350"},
  "konfig": {"urug": 0, "lr": 0.1, "max_iter": 100},
  "konfig_xesh": "507dfdb81878",
  "natija": {"auc": 0.716766, "bashorat_xesh": "818897a04902"}
}

QAYTA ISHGA TUSHIRISHDAN OLDIN tekshir(manifest):
  python/paket versiyalari mosmi?      -> farq: OGOHLANTIRISH
  ma'lumot xeshi mosmi?                -> farq: TO'XTATISH
  konfig xeshi mosmi?                  -> farq: TO'XTATISH
KEYIN:
  natija == manifest["natija"]?        -> reproduksiya testi

Manifest ikki yo'nalishda ishlaydi: oldinga (natijani qayta olish uchun retsept) va orqaga (ishlab chiqarishdagi model qaerdan kelganini isbotlash — audit). 27.4 da har yurish manifestini eksperiment kuzatuvchisiga, 27.5 da esa registrga ulaymiz.

Manifest natija bilan birga yoziladi va qayta ishga tushirishdan oldin avtomatik tekshiriladi.

2.7. Urug'ga sezgirlik

text
BITTA URUG':
  A: 0.924, B: 0.926 -> "B yaxshiroq"
  aslida: A va B ning urug'lar bo'yicha std ~ 0.01
          farq 0.002 - shovqin ichida

TO'G'RI HISOBOT (18-qism):
  bir necha urug' (bo'lish + model), o'rtacha +- std
  JUFTLASHGAN farq: d_s = A_s - B_s (bir xil urug' - bir xil bo'lish)
  SE(d) = std(d) / sqrt(n); |mean(d)| > 2 * SE -> sezilarli
  qaror: eng yaxshisidan sezilarli yomon bo'lmagan ENG ARZON variant

TARQOQLIK MANBALARI:
  bo'lish urug'i  - qaysi qatorlar testga tushdi (ko'pincha KATTA)
  model urug'i    - bootstrap, belgi tanlash, init
  ikkalasini alohida o'zgartirib ko'rish -> qaysi biri ustun

Bu 18.3 (CV dispersiyasi) va 18.11 (validatsiyaga overfitting) darslarining MLOps dagi davomi: "eng yaxshi urug'" ni tanlash — ko'p variantdan eng yaxshisini tanlashning xuddi o'zi va natija optimistik bo'ladi (4-misol).

Aynan takrorlanadigan natija ham bitta tasodifiy namuna. Qarorlar bir necha urug' bo'yicha juftlashgan farq bilan qabul qilinadi.

2.8. Muhit boshqaruvchilari

bash
# venv + pip (standart kutubxona)
python -m venv .venv
.venv/Scripts/activate            # Windows (Linux/macOS: source .venv/bin/activate)
pip install -r requirements.txt   # requirements.txt: numpy==2.5.3 ...
pip freeze > requirements.lock    # butun muhitning aniq versiyalari

# pip-tools: to'g'ridan-to'g'ri bog'liqliklar -> to'liq lock
pip-compile requirements.in --generate-hashes

# uv (tez, lock fayl bilan)
uv venv
uv pip install -r requirements.txt
uv lock                           # uv.lock - bog'liqliklar daraxti + xeshlar

# poetry
poetry add scikit-learn==1.9.1    # pyproject.toml + poetry.lock

# conda (python dan tashqari kutubxonalar: CUDA, MKL, GDAL)
conda env create -f environment.yml
conda env export --from-history > environment.yml
Vosita Kuchli tomoni E'tibor
venv + pip Standart, hamma joyda Lock uchun pip freeze yoki pip-tools
uv Juda tez, lock fayl, python versiyasini ham boshqaradi Nisbatan yangi vosita
poetry Paket yaratish + bog'liqliklar bir joyda Ba'zi ML paketlari bilan sekin hal qilish
conda Python bo'lmagan kutubxonalar (CUDA, MKL) Kanallar aralashsa, muhit mo'rt
Docker OS darajasigacha muzlatish 27.9-darsda

Lock fayl — "qaysi versiyalar" savoliga javob; Docker obrazi esa bundan ham chuqurroq: operatsion tizim kutubxonalari, python ning o'zi, muhit o'zgaruvchilari. 27.9-darsda muhitni konteynerga muzlatamiz; bu darsdagi manifest esa konteynerda ham kerak — chunki ma'lumot, konfig va natija obrazga kirmaydi.

To'g'ridan-to'g'ri bog'liqliklar — bitta faylda, aniq versiyalar — lock faylda, ikkalasi ham git da.

2.9. Tuzoqlar

Asosiy tuzoqlar: faqat bitta kutubxona urug'ini o'rnatish; random_state=None ni qoldirish; list(set(...)) bilan kategoriya kodlash; hash() ni belgi yoki savat uchun ishlatish; PYTHONHASHSEED ni skript ichida o'rnatish; reproduksiyani bitta noutbuk jarayonida tekshirish; fayl ro'yxatini sorted siz o'qish; requirements.txt da versiyasiz paketlar; commit xeshini "dirty" ishchi papkada yozish; manifestga sirlarni yozish; bitta urug' natijasini hisobotga yozish; "eng yaxshi urug'" ni tanlash.


3. Tez ma'lumotnoma

python
import hashlib
import importlib.metadata as md
import json
import platform
import random

import numpy as np
import torch


def seed_everything(urug):
    random.seed(urug)
    np.random.seed(urug)
    torch.manual_seed(urug)                      # CUDA ham (bor bo'lsa)


def worker_init_fn(worker_id):
    urug = torch.utils.data.get_worker_info().seed % 2**32
    np.random.seed(urug)                         # yoki o'z Generator ingiz
    random.seed(urug)


def lock(paketlar):
    return {p: md.version(p) for p in sorted(paketlar)}


def konfig_xesh(konfig):
    kanonik = json.dumps(konfig, sort_keys=True, separators=(",", ":"))
    return hashlib.sha256(kanonik.encode()).hexdigest()[:12]


def fayl_xesh(yol, blok=1 << 20):
    h = hashlib.sha256()
    with open(yol, "rb") as f:
        for bolak in iter(lambda: f.read(blok), b""):
            h.update(bolak)                      # katta fayl - bo'lakma-bo'lak
    return h.hexdigest()[:12]


def barqaror_savat(satr, n=1000):
    return int(hashlib.md5(satr.encode()).hexdigest(), 16) % n   # hash() EMAS


muhit = {"python": platform.python_version(), "tizim": platform.system(),
         "paketlar": lock(["numpy", "scikit-learn"])}
kategoriyalar = sorted(set(["Toshkent", "Andijon"]))  # list(set()) EMAS

Tuzilma xulosasi

text
darajalar: takrorlanuvchanlik (bit) / reproduksiya (statistik) / replikatsiya
manbalar: urug', tartib, ma'lumot, versiya, float/parallel, muhit
urug'lar: PYTHONHASHSEED (jarayondan oldin) > global > obyekt (tavsiya)
hash(): jarayonga bog'liq -> sorted() va hashlib
manifest: python, paketlar, git (+toza), ma'lumot xeshi, konfig xeshi, natija
tekshiruv: yangi jarayonda, manifestga qarshi, natija xeshi teng
sezgirlik: bir necha urug', juftlashgan farq, 2*SE

4. Batafsil misollar

Misollar real numpy/sklearn/torch bilan (Python 3.14). 1- va 2-misollar yangi Python jarayonlarini ishga tushiradi.

Misol 1 — Reproduksiya testi: bir xil skript, ikki yangi jarayon

Ma'lumot to'rtta CSV bo'lakda. O'qitish skripti — noutbukdan ko'chirilgan tipik kod: fayl nomlarini set bilan dublikatsiz qiladi, hududlarni list(set(...)) bilan kodlaydi va mini-batch SGD bilan logistik regressiya o'qitadi. Skriptni yangi jarayonlarda ishga tushirib, bashorat xeshlarini solishtiramiz. Ikki bayroq: --urug (numpy urug'i) va --sorted (set o'rniga sorted).

python
"""Reproduksiya testi: bir xil skript, ikki YANGI jarayon, natija tengmi?"""

import os
import subprocess
import sys
import tempfile
from pathlib import Path

import numpy as np
import pandas as pd

# o'qitish skripti - tipik "noutbukdan ko'chirilgan" kod
SKRIPT = r'''
import csv, hashlib, sys
from pathlib import Path
import numpy as np

urug = "--urug" in sys.argv
tartibla = "--sorted" in sys.argv
papka = Path(sys.argv[1])

nomlar = {p.name for p in papka.glob("qism_*.csv")}          # dublikatsiz
nomlar = sorted(nomlar) if tartibla else list(nomlar)
qatorlar = []
for n in nomlar:
    with open(papka / n, encoding="utf-8") as f:
        qatorlar += list(csv.DictReader(f))

hududlar = {q["hudud"] for q in qatorlar}
hududlar = sorted(hududlar) if tartibla else list(hududlar)
kod = {h: i for i, h in enumerate(hududlar)}                # label encoding
X = np.array([[float(q["x1"]), float(q["x2"]), kod[q["hudud"]]]
              for q in qatorlar])
X = (X - X.mean(0)) / X.std(0)
y = np.array([int(q["y"]) for q in qatorlar])
Xtr, ytr, Xte, yte = X[:3000], y[:3000], X[3000:], y[3000:]

if urug:
    np.random.seed(0)
w = np.random.normal(0, 0.1, 3)
b = 0.0
for davr in range(5):                                       # mini-batch SGD
    tartib = np.random.permutation(len(ytr))
    for i in range(0, len(ytr), 50):
        j = tartib[i:i + 50]
        g = 1 / (1 + np.exp(-(Xtr[j] @ w + b))) - ytr[j]
        w -= 0.1 * Xtr[j].T @ g / len(j)
        b -= 0.1 * g.mean()
p = 1 / (1 + np.exp(-(Xte @ w + b)))
r = p.argsort().argsort() + 1                               # AUC ranglardan
auc = (r[yte == 1].sum() - (yte == 1).sum() * ((yte == 1).sum() + 1) / 2) / (
    (yte == 1).sum() * (yte == 0).sum())
print(hashlib.sha256(np.round(p, 10).tobytes()).hexdigest()[:12], round(auc, 4))
'''


def ishga(skript, papka, bayroqlar, hashseed):
    """Skriptni YANGI jarayonda, berilgan PYTHONHASHSEED bilan ishga tushiradi."""
    env = dict(os.environ, PYTHONHASHSEED=str(hashseed))
    r = subprocess.run([sys.executable, str(skript), str(papka), *bayroqlar],
                       capture_output=True, text=True, env=env, timeout=120)
    if r.returncode != 0:
        raise RuntimeError(r.stderr.strip().splitlines()[-1])
    xesh, auc = r.stdout.split()
    return xesh, float(auc)


def main() -> None:
    with tempfile.TemporaryDirectory() as t:
        papka = Path(t)
        rng = np.random.default_rng(0)
        hudud = ["Andijon", "Buxoro", "Jizzax", "Namangan", "Navoiy",
                 "Samarqand", "Toshkent", "Xorazm"]
        eff = dict(zip(hudud, [0.4, -0.3, 0.8, 0.1, -0.6, 0.0, -0.9, 0.5]))
        for i in range(4):                         # ma'lumot 4 bo'lak faylda
            n = 1000
            h = rng.choice(hudud, n)
            x1, x2 = rng.normal(size=n), rng.normal(size=n)
            logit = x1 - 0.5 * x2 + pd.Series(h).map(eff).to_numpy()
            y = (rng.random(n) < 1 / (1 + np.exp(-logit))).astype(int)
            pd.DataFrame({"x1": x1.round(5), "x2": x2.round(5), "hudud": h,
                          "y": y}).to_csv(papka / f"qism_{i}.csv", index=False)
        skript = papka / "orgat.py"
        skript.write_text(SKRIPT, encoding="utf-8")

        print("=== 1. Bir xil skript, ikki yangi jarayon (PYTHONHASHSEED 1 va 2) ===")
        print(f"  {'variant':<26} {'xesh teng':>9}  izoh")
        variantlar = [("urug' yo'q, set tartibi", []),
                      ("urug' bor, set tartibi", ["--urug"]),
                      ("urug' yo'q, sorted", ["--sorted"]),
                      ("urug' bor, sorted", ["--urug", "--sorted"])]
        for nom, bayroq in variantlar:
            (x1, a1), (x2, a2) = [ishga(skript, papka, bayroq, hs) for hs in (1, 2)]
            izoh = f"AUC {a1:.4f} va {a2:.4f}" if "--urug" in bayroq else ""
            print(f"  {nom:<26} {str(x1 == x2):>9}  {izoh}")

        print("\n=== 2. PYTHONHASHSEED qat'iy bo'lsa ===")
        (xa, _), (xb, _) = [ishga(skript, papka, ["--urug"], hs) for hs in (5, 5)]
        print(f"  urug' bor, set tartibi, ikkalasida PYTHONHASHSEED=5: teng {xa == xb}")
        print("  -> set tartibi faqat PYTHONHASHSEED ga bog'liq; sukut bo'yicha u")
        print("     tasodifiy, ya'ni 'urug' qo'yilgan' skript ham har safar boshqacha")

        print("\n=== 3. Olti yangi jarayon (PYTHONHASHSEED 10..15) ===")
        for nom, bayroq in [("urug' bor, set tartibi", ["--urug"]),
                            ("urug' bor, sorted", ["--urug", "--sorted"])]:
            natijalar = {ishga(skript, papka, bayroq, hs) for hs in range(10, 16)}
            auclar = sorted(a for _, a in natijalar)
            print(f"  {nom:<24} noyob natija: {len(natijalar)}; AUC "
                  f"{auclar[0]:.4f} - {auclar[-1]:.4f}")


if __name__ == "__main__":
    main()

Natijaning muhim qismi:

text
=== 1. Bir xil skript, ikki yangi jarayon (PYTHONHASHSEED 1 va 2) ===
  variant                    xesh teng  izoh
  urug' yo'q, set tartibi        False
  urug' bor, set tartibi         False  AUC 0.7376 va 0.7456
  urug' yo'q, sorted             False
  urug' bor, sorted               True  AUC 0.7584 va 0.7584

=== 2. PYTHONHASHSEED qat'iy bo'lsa ===
  urug' bor, set tartibi, ikkalasida PYTHONHASHSEED=5: teng True
  -> set tartibi faqat PYTHONHASHSEED ga bog'liq; sukut bo'yicha u
     tasodifiy, ya'ni 'urug' qo'yilgan' skript ham har safar boshqacha

=== 3. Olti yangi jarayon (PYTHONHASHSEED 10..15) ===
  urug' bor, set tartibi   noyob natija: 6; AUC 0.7384 - 0.7590
  urug' bor, sorted        noyob natija: 1; AUC 0.7584 - 0.7584

Nima ko'rsatdi: 2.2, 2.4-bo'limlar. Yagona to'liq takrorlanadigan variant — urug' va sorted. Urug' qo'yilgan, lekin set tartibi qolgan skript ikki jarayonda boshqa natija berdi: set fayllar tartibini (qatorlar tartibi → o'quv/test bo'linishi) ham, hudud kodlarini ham o'zgartiradi. Muhimi, bu xato bitta noutbuk jarayonida umuman ko'rinmaydi — 2-qism ko'rsatganidek, PYTHONHASHSEED bir xil bo'lsa natija teng. Olti yangi jarayonda urug'li, lekin set li skript olti xil natija berdi va AUC ancha keng oraliqda "sakradi" — auditordagi vaziyatning o'zi. sorted bilan esa oltitasi ham bitta natija.

Bu misol reproduksiya testining shablonidir: skript yangi jarayonlarda, turli PYTHONHASHSEED bilan ishga tushiriladi va bashorat xeshlari solishtiriladi. CI da 27.8-bob aynan shunday test yoziladi.

Misol 2 — Urug'lar ierarxiyasi va PYTHONHASHSEED

Uch global generator, sklearn ning random_state shakllari, numpy ning mustaqil oqimlari, DataLoader generatori — va interpretator darajasidagi PYTHONHASHSEED, jumladan uni skript ichida o'rnatish tuzog'i.

python
"""Urug'lar ierarxiyasi va PYTHONHASHSEED tuzog'i."""

import os
import random
import subprocess
import sys

import numpy as np
import torch
from sklearn.datasets import make_classification
from sklearn.ensemble import RandomForestClassifier
from torch.utils.data import DataLoader, TensorDataset


def seed_everything(seed):
    random.seed(seed)
    np.random.seed(seed)
    torch.manual_seed(seed)


def jarayon(kod, hashseed):
    """Kodni yangi Python jarayonida ishga tushiradi."""
    env = dict(os.environ)
    if hashseed is None:
        env.pop("PYTHONHASHSEED", None)
    else:
        env["PYTHONHASHSEED"] = str(hashseed)
    r = subprocess.run([sys.executable, "-c", kod], capture_output=True,
                       text=True, env=env, timeout=60)
    return r.stdout.strip()


def main() -> None:
    print("=== 1. Uch generator - uch alohida urug' ===")
    seed_everything(0)
    a = (random.random(), np.random.rand(), torch.rand(1).item())
    random.seed(0)                           # FAQAT python random
    b = (random.random(), np.random.rand(), torch.rand(1).item())
    print(f"  seed_everything(0) dan keyin:   {[round(v, 4) for v in a]}")
    print(f"  faqat random.seed(0) dan keyin: {[round(v, 4) for v in b]}")
    print(f"  mos keladi: python {a[0] == b[0]}, numpy {a[1] == b[1]}, "
          f"torch {a[2] == b[2]}")

    print("\n=== 2. sklearn random_state: None va int ===")
    X, y = make_classification(n_samples=600, n_features=10, random_state=0)

    def rf(rs, oraliq=False):
        np.random.seed(0)
        if oraliq:
            np.random.rand(3)                # boshqa kod global holatni ishlatdi
        return RandomForestClassifier(n_estimators=20, random_state=rs).fit(
            X, y).predict_proba(X[:50])

    print(f"  None, global seed(0):              teng {np.array_equal(rf(None), rf(None))}")
    print(f"  None, oraliqda boshqa np.random:   teng "
          f"{np.array_equal(rf(None), rf(None, True))}")
    print(f"  int 42, oraliqda boshqa np.random: teng "
          f"{np.array_equal(rf(42), rf(42, True))}")

    print("\n=== 3. numpy Generator: mustaqil oqimlar ===")
    g = np.random.default_rng(123)
    print(f"  default_rng(123) birinchi 3: {np.round(g.random(3), 4).tolist()}")
    oqim = np.random.default_rng(123).spawn(2)
    print(f"  spawn qilingan 2 oqim bir xilmi: "
          f"{np.array_equal(oqim[0].random(3), oqim[1].random(3))}")
    oqim2 = np.random.default_rng(123).spawn(2)
    print(f"  qayta spawn - 1-oqim takrorlandimi: "
          f"{np.array_equal(np.random.default_rng(123).spawn(2)[0].random(3), oqim2[0].random(3))}")

    print("\n=== 4. DataLoader: generator global holatdan mustaqil ===")
    ds = TensorDataset(torch.arange(8))

    def tartib(oraliq):
        torch.manual_seed(0)
        if oraliq:
            torch.rand(5)                     # masalan, model init o'zgardi
        g = torch.Generator().manual_seed(7)
        return next(iter(DataLoader(ds, batch_size=8, shuffle=True,
                                    generator=g)))[0].tolist()

    print(f"  generator(7):                 {tartib(False)}")
    print(f"  generator(7), oraliqda rand:  {tartib(True)}")

    print("\n=== 5. PYTHONHASHSEED: str hash i jarayonga bog'liq ===")
    kod = "print(hash('salom'))"
    print(f"  =0 va =0 (ikki jarayon) teng: {jarayon(kod, 0) == jarayon(kod, 0)}")
    print(f"  =0 va =1 teng:                {jarayon(kod, 0) == jarayon(kod, 1)}")
    tasodifiy = {jarayon(kod, None) for _ in range(5)}
    print(f"  o'rnatilmagan: 5 jarayonda {len(tasodifiy)} xil qiymat")
    ichida = ("import os; os.environ['PYTHONHASHSEED'] = '0'; "
              "print(hash('salom'))")
    print(f"  skript ICHIDA '0' qilingan (tashqarida 1 va 2) teng: "
          f"{jarayon(ichida, 1) == jarayon(ichida, 2)}")
    kod_int = "print(hash(12345), hash((1, 2)))"
    print(f"  int va tuple hash i (=0 va =1) teng: "
          f"{jarayon(kod_int, 0) == jarayon(kod_int, 1)}")

    print("\n=== 6. Oqibat: list(set(...)) bilan kategoriya kodlash ===")
    kod = ("s = {'Andijon','Buxoro','Namangan','Samarqand','Toshkent',"
           "'Xorazm','Qarshi','Nukus'}; print(list(s).index('Toshkent'))")
    print(f"  PYTHONHASHSEED 0..5 da 'Toshkent' kodi: "
          f"{[int(jarayon(kod, hs)) for hs in range(6)]}")
    kod_s = kod.replace("list(s)", "sorted(s)")
    print(f"  sorted() bilan:                        "
          f"{[int(jarayon(kod_s, hs)) for hs in range(6)]}")


if __name__ == "__main__":
    main()

Natijaning muhim qismi:

text
=== 1. Uch generator - uch alohida urug' ===
  seed_everything(0) dan keyin:   [0.8444, 0.5488, 0.4963]
  faqat random.seed(0) dan keyin: [0.8444, 0.7152, 0.7682]
  mos keladi: python True, numpy False, torch False

=== 2. sklearn random_state: None va int ===
  None, global seed(0):              teng True
  None, oraliqda boshqa np.random:   teng False
  int 42, oraliqda boshqa np.random: teng True

=== 3. numpy Generator: mustaqil oqimlar ===
  default_rng(123) birinchi 3: [0.6824, 0.0538, 0.2204]
  spawn qilingan 2 oqim bir xilmi: False
  qayta spawn - 1-oqim takrorlandimi: True

=== 4. DataLoader: generator global holatdan mustaqil ===
  generator(7):                 [1, 2, 3, 5, 7, 4, 6, 0]
  generator(7), oraliqda rand:  [1, 2, 3, 5, 7, 4, 6, 0]

=== 5. PYTHONHASHSEED: str hash i jarayonga bog'liq ===
  =0 va =0 (ikki jarayon) teng: True
  =0 va =1 teng:                False
  o'rnatilmagan: 5 jarayonda 5 xil qiymat
  skript ICHIDA '0' qilingan (tashqarida 1 va 2) teng: False
  int va tuple hash i (=0 va =1) teng: True

=== 6. Oqibat: list(set(...)) bilan kategoriya kodlash ===
  PYTHONHASHSEED 0..5 da 'Toshkent' kodi: [0, 0, 2, 6, 7, 0]
  sorted() bilan:                        [6, 6, 6, 6, 6, 6]

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

  • Uch generator mustaqil. Faqat random.seed(0) qilinganda Python ning soni mos keldi, numpy va torch niki esa yo'q — ular o'z holatidan davom etdi.
  • random_state=None mo'rt. Global np.random.seed(0) qo'yilgan bo'lsa ham, orada boshqa kod bitta np.random.rand chaqirsa, o'rmon boshqacha bo'ldi. random_state=42 esa global holatdan mustaqil.
  • Mustaqil oqimlar. spawn bitta urug'dan bir-biriga o'xshamaydigan, lekin qayta hosil qilinadigan oqimlar beradi — parallel ishchilar yoki bir necha tajriba uchun.
  • DataLoader generatori global torch holatidan mustaqil: oraliqda torch.rand chaqirilsa ham tartib bir xil.
  • PYTHONHASHSEED. Bir xil qiymat — bir xil hash; turli yoki o'rnatilmagan — turli hash. Uni skript ichida o'rnatish hech narsa bermadi: interpretator allaqachon ishga tushgan. int va tuple (butun sonlardan) hash i tuzlanmaydi. Oxirgi qism oqibatni ko'rsatdi: bir xil kod "Toshkent" ga turli jarayonlarda turli kod beradi — sorted bilan esa har doim bir xil.

Misol 3 — Reproduksiya manifesti: yaratish, qayta ishga tushirish, buzilishni ushlash

Kichik o'qitish skripti natijasi bilan birga manifest yozadi: python va platforma, importlib.metadata dan paket versiyalari (lock), git holati, ma'lumot xeshi, konfig va uning kanonik xeshi. Keyin manifestdan qayta ishga tushiramiz va buzilishlarni simulyatsiya qilamiz.

python
"""Reproduksiya manifesti: muhit, ma'lumot va konfig xeshi, tekshirish."""

import copy
import hashlib
import importlib.metadata as md
import json
import os
import platform
import subprocess
import tempfile
from pathlib import Path

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

PAKETLAR = ["numpy", "pandas", "scikit-learn", "scipy", "joblib"]
MUHIT_ROYXAT = ["PYTHONHASHSEED", "OMP_NUM_THREADS", "MKL_NUM_THREADS"]  # sirlar EMAS


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


def konfig_xesh(konfig):
    return xesh(json.dumps(konfig, sort_keys=True, separators=(",", ":")).encode())


def lock(paketlar):
    return {p: md.version(p) for p in sorted(paketlar)}


def git_holati(papka):
    try:
        r = subprocess.run(["git", "rev-parse", "HEAD"], cwd=papka,
                           capture_output=True, text=True, timeout=10)
    except FileNotFoundError:
        return {"commit": None, "sabab": "git topilmadi"}
    if r.returncode != 0:
        return {"commit": None, "sabab": "git repozitoriy emas"}
    s = subprocess.run(["git", "status", "--porcelain"], cwd=papka,
                       capture_output=True, text=True, timeout=10)
    return {"commit": r.stdout.strip(), "toza": s.stdout.strip() == ""}


def malumot_yarat(yol, seed=0, n=3000):
    rng = np.random.default_rng(seed)
    df = pd.DataFrame({"x1": rng.normal(size=n).round(4),
                       "x2": rng.normal(size=n).round(4),
                       "x3": rng.integers(0, 5, n)})
    logit = 1.2 * df["x1"] - 0.8 * df["x2"] * (df["x3"] > 2) + 0.3
    df["y"] = (rng.random(n) < 1 / (1 + np.exp(-logit))).astype(int)
    df.to_csv(yol, index=False, lineterminator="\n")   # LF - OS dan mustaqil


def orgat(df, konfig):
    tr, te = df.iloc[:2000], df.iloc[2000:]
    X = ["x1", "x2", "x3"]
    m = HistGradientBoostingClassifier(
        max_iter=konfig["max_iter"], learning_rate=konfig["lr"],
        random_state=konfig["urug"]).fit(tr[X], tr["y"])
    p = m.predict_proba(te[X])[:, 1]
    return {"auc": round(float(roc_auc_score(te["y"], p)), 6),
            "bashorat_xesh": xesh(np.round(p, 8).tobytes())}


def manifest_yarat(papka, malumot_yol, konfig, natija):
    return {
        "python": platform.python_version(),
        "platforma": {"tizim": platform.system(), "arxitektura": platform.machine()},
        "paketlar": lock(PAKETLAR),
        "git": git_holati(papka),
        "muhit": {k: os.environ.get(k) for k in MUHIT_ROYXAT},
        "malumot": {"fayl": malumot_yol.name,
                    "sha256": xesh(malumot_yol.read_bytes())},
        "konfig": konfig, "konfig_xesh": konfig_xesh(konfig),
        "natija": natija,
    }


def tekshir(manifest, papka):
    """Hozirgi muhit va fayllarni manifest bilan solishtiradi."""
    farqlar = []
    if manifest["python"] != platform.python_version():
        farqlar.append(f"python: {manifest['python']} != {platform.python_version()}")
    joriy = lock(manifest["paketlar"])
    for p, v in manifest["paketlar"].items():
        if joriy[p] != v:
            farqlar.append(f"OGOHLANTIRISH {p}: manifest {v}, hozir {joriy[p]}")
    yol = Path(papka) / manifest["malumot"]["fayl"]
    if xesh(yol.read_bytes()) != manifest["malumot"]["sha256"]:
        farqlar.append("TO'XTATISH: ma'lumot xeshi mos emas")
    if konfig_xesh(manifest["konfig"]) != manifest["konfig_xesh"]:
        farqlar.append("TO'XTATISH: konfig o'zgartirilgan")
    return farqlar


def main() -> None:
    with tempfile.TemporaryDirectory() as t:
        papka = Path(t)
        yol = papka / "malumot.csv"
        malumot_yarat(yol)
        konfig = {"urug": 0, "lr": 0.1, "max_iter": 100}

        print("=== 1. Konfig xeshi: kalit tartibi ahamiyatsiz, qiymat - muhim ===")
        k2 = {"max_iter": 100, "lr": 0.1, "urug": 0}
        k3 = dict(konfig, lr=0.05)
        print(f"  konfig           {konfig_xesh(konfig)}")
        print(f"  tartibi boshqa   {konfig_xesh(k2)}  teng: "
              f"{konfig_xesh(k2) == konfig_xesh(konfig)}")
        print(f"  lr=0.05          {konfig_xesh(k3)}  teng: "
              f"{konfig_xesh(k3) == konfig_xesh(konfig)}")

        print("\n=== 2. O'qitish va manifest ===")
        natija = orgat(pd.read_csv(yol), konfig)
        m = manifest_yarat(papka, yol, konfig, natija)
        (papka / "manifest.json").write_text(json.dumps(m, indent=2),
                                             encoding="utf-8")
        print(f"  kalitlar: {list(m)}")
        print(f"  paketlar (lock): {len(m['paketlar'])} ta, masalan "
              f"'scikit-learn=={m['paketlar']['scikit-learn']}'")
        print(f"  muhit o'zgaruvchilari (faqat ro'yxatdagilar): {sorted(m['muhit'])}")
        print(f"  git: {m['git']}")
        print(f"  ma'lumot sha256: {m['malumot']['sha256']}")
        print(f"  natija: {natija}")

        print("\n=== 3. requirements.txt (pin) ===")
        req = "\n".join(f"{p}=={v}" for p, v in m["paketlar"].items())
        (papka / "requirements.txt").write_text(req + "\n", encoding="utf-8")
        for qator in req.splitlines():
            print(f"  {qator}")

        print("\n=== 4. Manifestdan qayta ishga tushirish ===")
        m2 = json.loads((papka / "manifest.json").read_text(encoding="utf-8"))
        print(f"  tekshiruv farqlari: {tekshir(m2, papka) or 'yoq'}")
        qayta = orgat(pd.read_csv(yol), m2["konfig"])
        print(f"  qayta natija: {qayta}")
        print(f"  aynan teng: {qayta == m2['natija']}")

        print("\n=== 5. Buzilishlarni ushlash ===")
        df = pd.read_csv(yol)
        df.loc[17, "x1"] = df.loc[17, "x1"] + 0.0001        # bitta qiymat
        df.to_csv(yol, index=False, lineterminator="\n")
        print(f"  1 qiymat o'zgardi: {tekshir(m2, papka)}")
        print(f"  shu o'zgarishdan natija o'zgardimi: "
              f"{orgat(df, m2['konfig']) != m2['natija']}")
        malumot_yarat(yol)                                   # tiklash
        crlf = yol.read_bytes().replace(b"\n", b"\r\n")      # Windows satr oxiri
        (papka / "crlf.csv").write_bytes(crlf)
        print(f"  CRLF nusxa: xesh teng {xesh(crlf) == m2['malumot']['sha256']}, "
              f"pandas jadvali teng {pd.read_csv(papka / 'crlf.csv').equals(pd.read_csv(yol))}")
        m3 = copy.deepcopy(m2)
        m3["paketlar"]["scikit-learn"] = "1.4.2"            # boshqa muhitda yozilgan
        m3["konfig"]["lr"] = 0.2                             # qo'lda o'zgartirilgan
        for f in tekshir(m3, papka):
            print(f"  {f}")


if __name__ == "__main__":
    main()

Natijaning muhim qismi:

text
=== 1. Konfig xeshi: kalit tartibi ahamiyatsiz, qiymat - muhim ===
  konfig           507dfdb81878
  tartibi boshqa   507dfdb81878  teng: True
  lr=0.05          c3db07c8be99  teng: False

=== 2. O'qitish va manifest ===
  kalitlar: ['python', 'platforma', 'paketlar', 'git', 'muhit', 'malumot', 'konfig', 'konfig_xesh', 'natija']
  paketlar (lock): 5 ta, masalan 'scikit-learn==1.9.1'
  muhit o'zgaruvchilari (faqat ro'yxatdagilar): ['MKL_NUM_THREADS', 'OMP_NUM_THREADS', 'PYTHONHASHSEED']
  git: {'commit': None, 'sabab': 'git repozitoriy emas'}
  ma'lumot sha256: 952c6655a350
  natija: {'auc': 0.716766, 'bashorat_xesh': '818897a04902'}

=== 3. requirements.txt (pin) ===
  joblib==1.6.0
  numpy==2.5.3
  pandas==3.0.6
  scikit-learn==1.9.1
  scipy==1.18.1

=== 4. Manifestdan qayta ishga tushirish ===
  tekshiruv farqlari: yoq
  qayta natija: {'auc': 0.716766, 'bashorat_xesh': '818897a04902'}
  aynan teng: True

=== 5. Buzilishlarni ushlash ===
  1 qiymat o'zgardi: ["TO'XTATISH: ma'lumot xeshi mos emas"]
  shu o'zgarishdan natija o'zgardimi: False
  CRLF nusxa: xesh teng False, pandas jadvali teng True
  OGOHLANTIRISH scikit-learn: manifest 1.4.2, hozir 1.9.1
  TO'XTATISH: konfig o'zgartirilgan

Nima ko'rsatdi: 2.5, 2.6-bo'limlar. Kanonik JSON tufayli konfig xeshi kalit tartibiga bog'liq emas, lekin bitta qiymat (lr) o'zgarsa, xesh butunlay boshqa. Manifestga python, platforma, beshta paketning aniq versiyasi, faqat ruxsat etilgan muhit o'zgaruvchilari, git holati (vaqtinchalik papka repozitoriy emas — bu ham qayd qilinadi, "noma'lum kod" sifatida), ma'lumot va konfig xeshlari hamda natija yozildi. Manifestdan qayta ishga tushirish aynan bir xil AUC va bashorat xeshini berdi.

Buzilishlar qismida uch muhim kuzatuv bor. Birinchisi: bitta qiymatning 0.0001 ga o'zgarishini xesh ushladi, garchi bu o'zgarish natijani o'zgartirmagan bo'lsa ham (gradient boosting belgini qutilarga ajratadi va kichik o'zgarish qutini almashtirmadi). Xesh natijadan qat'iy — va bu to'g'ri: "natija o'zgarmadi" degan xulosani faqat tekshirib chiqarish mumkin. Ikkinchisi: xuddi shu jadval CRLF satr oxirlari bilan saqlansa, pandas uchun jadval bir xil, lekin fayl xeshi boshqa — shuning uchun biz faylni lineterminator="\n" bilan yozdik. Ma'lumot xeshini fayl baytlaridan olsangiz, yozish formatini ham qat'iylashtiring. Uchinchisi: boshqa sklearn versiyasi ogohlantirish, o'zgartirilgan konfig esa to'xtatish sababi.

Misol 4 — Urug'ga sezgirlik: takrorlanadigan natija ham bitta namuna

Ikki model: A — tasodifiy o'rmon, har bo'linishda sqrt(20) ≈ 4 belgi; B — xuddi shu, lekin 30% (6) belgi, ya'ni sekinroq. Har urug' bo'lishni ham, modelni ham belgilaydi — har bir yurish aynan takrorlanadi. Savol: qaysi model yaxshiroq?

python
"""Urug'ga sezgirlik: bitta urug'dagi natija - bitta namuna."""

import numpy as np
from sklearn.datasets import make_classification
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import roc_auc_score
from sklearn.model_selection import train_test_split

X, Y = make_classification(n_samples=2000, n_features=20, n_informative=5,
                           n_redundant=5, flip_y=0.08, class_sep=0.8,
                           random_state=3)
MODELLAR = {"A": {"max_features": "sqrt"},      # 4 belgi har bo'linishda
            "B": {"max_features": 0.3}}         # 6 belgi - sekinroq


def yurish(nom, bolish_urug, model_urug):
    Xtr, Xte, ytr, yte = train_test_split(X, Y, test_size=0.3,
                                          random_state=bolish_urug, stratify=Y)
    m = RandomForestClassifier(n_estimators=40, random_state=model_urug,
                               n_jobs=1, **MODELLAR[nom]).fit(Xtr, ytr)
    return roc_auc_score(yte, m.predict_proba(Xte)[:, 1])


def main() -> None:
    urug = range(15)
    a = np.array([yurish("A", s, s) for s in urug])
    b = np.array([yurish("B", s, s) for s in urug])
    d = a - b

    print("=== 1. Bitta urug' - 'aniq' javob ===")
    for s in range(4):
        print(f"  urug' {s}: A {a[s]:.4f}, B {b[s]:.4f} -> g'olib "
              f"{'A' if d[s] > 0 else 'B'} ({d[s]:+.4f})")
    print(f"  takror: urug' 0 dagi A qayta hisoblandi, teng: "
          f"{yurish('A', 0, 0) == a[0]}")

    print("\n=== 2. 15 urug' (bo'lish va model urug'i birga) ===")
    for nom, v in [("A", a), ("B", b)]:
        print(f"  {nom}: {v.mean():.4f} +- {v.std(ddof=1):.4f}  "
              f"[{v.min():.4f}, {v.max():.4f}]")
    print(f"  A g'olib bo'lgan urug'lar: {int((d > 0).sum())}/15")
    se = d.std(ddof=1) / np.sqrt(len(d))
    se_juftsiz = np.sqrt(a.var(ddof=1) / len(a) + b.var(ddof=1) / len(b))
    print(f"  farq A-B: {d.mean():+.4f}; juftlashgan SE {se:.4f}, "
          f"juftlashmagan SE {se_juftsiz:.4f}")
    if abs(d.mean()) > 2 * se:
        print(f"  farq SEZILARLI -> {'A' if d.mean() > 0 else 'B'}")
    else:
        print("  farq sezilarli emas -> qoida: arzonroq A")

    print("\n=== 3. Tarqoqlik manbalari: faqat bittasini o'zgartirib ===")
    faqat_bolish = np.array([yurish("A", s, 0) for s in urug])
    faqat_model = np.array([yurish("A", 0, s) for s in urug])
    print(f"  faqat bo'lish urug'i: std {faqat_bolish.std(ddof=1):.4f}")
    print(f"  faqat model urug'i:   std {faqat_model.std(ddof=1):.4f}")
    katta = "bo'lish" if faqat_bolish.std() > faqat_model.std() else "model"
    print(f"  asosiy tarqoqlik manbai: {katta} urug'i")

    print("\n=== 4. 'Eng yaxshi urug'' ni hisobot qilish ===")
    eng = int(b.argmax())
    print(f"  B ning eng yaxshi urug'i {eng}: {b[eng]:.4f}; o'rtacha {b.mean():.4f}; "
          f"optimizm {b[eng] - b.mean():+.4f} "
          f"({(b[eng] - b.mean()) / b.std(ddof=1):.1f} std)")
    print(f"  shu urug'da B ning A dan ustunligi: {-d[eng]:+.4f}; "
          f"o'rtacha ustunlik {-d.mean():+.4f}")
    print("  hisobotga: o'rtacha +- std (15 urug'), juftlashgan farq va SE")


if __name__ == "__main__":
    main()

Natijaning muhim qismi:

text
=== 1. Bitta urug' - 'aniq' javob ===
  urug' 0: A 0.9238, B 0.9264 -> g'olib B (-0.0025)
  urug' 1: A 0.9111, B 0.9042 -> g'olib A (+0.0069)
  urug' 2: A 0.9355, B 0.9293 -> g'olib A (+0.0062)
  urug' 3: A 0.9124, B 0.9175 -> g'olib B (-0.0051)
  takror: urug' 0 dagi A qayta hisoblandi, teng: True

=== 2. 15 urug' (bo'lish va model urug'i birga) ===
  A: 0.9226 +- 0.0106  [0.9082, 0.9421]
  B: 0.9246 +- 0.0100  [0.9042, 0.9411]
  A g'olib bo'lgan urug'lar: 6/15
  farq A-B: -0.0020; juftlashgan SE 0.0013, juftlashmagan SE 0.0038
  farq sezilarli emas -> qoida: arzonroq A

=== 3. Tarqoqlik manbalari: faqat bittasini o'zgartirib ===
  faqat bo'lish urug'i: std 0.0097
  faqat model urug'i:   std 0.0033
  asosiy tarqoqlik manbai: bo'lish urug'i

=== 4. 'Eng yaxshi urug'' ni hisobot qilish ===
  B ning eng yaxshi urug'i 10: 0.9411; o'rtacha 0.9246; optimizm +0.0165 (1.6 std)
  shu urug'da B ning A dan ustunligi: -0.0011; o'rtacha ustunlik +0.0020
  hisobotga: o'rtacha +- std (15 urug'), juftlashgan farq va SE

Nima ko'rsatdi: 2.1, 2.7-bo'limlar. Har yurish bit darajasida takrorlanadi (urug' 0 dagi A qayta hisoblanganda aynan teng) — 1-daraja bajarildi. Lekin birinchi to'rtta urug'ning o'zida g'olib almashib turdi: bitta urug' bilan qilingan xulosa tangaga o'xshaydi. 15 urug'da ikkala model ham taxminan 0.01 std ga ega, A 15 tadan 6 tasida g'olib, juftlashgan farq 2*SE dan kichik — sezilarli emas, shuning uchun qoida arzonroq A ni tanlaydi.

E'tibor bering: juftlashgan SE juftlashmaganidan taxminan uch marta kichik — chunki bir xil urug'da ikkala model bir xil bo'linishda baholanadi va bo'linish shovqini farqda qisqaradi. 3-qism buning sababini ko'rsatdi: tarqoqlikning asosiy manbai model urug'i emas, bo'lish urug'i (qaysi qatorlar testga tushgani). Nihoyat, "eng yaxshi urug'" ni hisobot qilish B ni o'rtachasidan +0.0165 ga (1.6 std) optimistik ko'rsatadi — o'sha urug'da esa B A dan hatto biroz yomon (-0.0011).


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

Noto'g'ri fikr To'g'risi
"Urug' qo'ydim — natija takrorlanadi" 1-misolda urug'li skript set tartibi tufayli olti jarayonda olti xil natija berdi
"Noutbukda ikki marta ishga tushirdim — bir xil" set tartibi jarayon ichida barqaror; test yangi jarayonda qilinadi
"random.seed hamma narsani o'rnatadi" numpy va torch ning o'z urug'lari bor
"os.environ['PYTHONHASHSEED'] skript boshida yetarli" Interpretator allaqachon ishga tushgan — ta'sir qilmaydi
"hash() bilan savatga ajratish barqaror" Satr hash i jarayonga bog'liq; hashlib ishlating
"Ma'lumot o'zgarmagan, chunki natija bir xil" 3-misolda bitta qiymat o'zgardi, natija bir xil qoldi — faqat xesh ushladi
"requirements.txt da paket nomi yetarli" Aniq versiya (==) va lock fayl kerak
"Aynan takrorlanadigan natija — ishonchli natija" 4-misolda bitta urug'da g'olib almashib turdi
"Eng yaxshi urug'ni hisobot qilsa bo'ladi" Optimistik: +0.0165 (1.6 std)

6. Keng tarqalgan xatolar va yechimlari

1. Kategoriyani set tartibi bilan kodlash

python
kod = {h: i for i, h in enumerate(set(df["hudud"]))}          # ⚠️ jarayonga bog'liq
kod = {h: i for i, h in enumerate(sorted(set(df["hudud"])))}  # ✅

2. Fayllarni tartibsiz o'qish

python
fayllar = os.listdir(papka)                                   # ⚠️ OS ga bog'liq
fayllar = sorted(os.listdir(papka))                           # ✅

3. Faqat bitta urug'

python
random.seed(0)                                                # ⚠️ numpy/torch erkin
seed_everything(0); model = RandomForestClassifier(random_state=0)   # ✅

4. hash() bilan savat

python
savat = hash(foydalanuvchi_id) % 100                          # ⚠️ har jarayonda boshqa
savat = int(hashlib.md5(foydalanuvchi_id.encode()).hexdigest(), 16) % 100   # ✅

5. PYTHONHASHSEED ni skript ichida o'rnatish

python
os.environ["PYTHONHASHSEED"] = "0"                            # ⚠️ kech
# PYTHONHASHSEED=0 python orgat.py  (yoki Dockerfile da ENV)  # ✅ jarayondan oldin

6. Versiyasiz bog'liqliklar

python
# requirements.txt: scikit-learn                              # ⚠️
# requirements.txt: scikit-learn==1.9.1  (+ lock fayl)        # ✅

7. Manifestga barcha muhit o'zgaruvchilarini yozish

python
manifest["muhit"] = dict(os.environ)                          # ⚠️ sirlar tushadi
manifest["muhit"] = {k: os.environ.get(k) for k in RUXSAT_ROYXATI}   # ✅

7. Integratsiya — bu bilim qayerda kerak bo'ladi

  • 19.7-dars (o'tilgan): random_state shakllari, oqimlar va suzuvchi nuqta, versiyalarni qulflash
  • 21.11-dars (o'tilgan): seed_everything, DataLoader generatori, konfig natija bilan birga
  • 18.3, 18.10, 18.11-darslar (o'tilgan): tarqoqlik, juftlashgan taqqoslash, "eng yaxshisini tanlash" optimizmi
  • 27.3-dars: ma'lumot xeshi quvur keshining kaliti bo'ladi
  • 27.4-dars: manifest har eksperiment yurishining bir qismi bo'ladi
  • 27.8-dars: 1-misoldagi "yangi jarayonda reproduksiya testi" CI testiga aylanadi
  • 27.9-dars: Docker muhitni OS darajasigacha muzlatadi; PYTHONHASHSEED ni ENV bilan beradi

8. Eng yaxshi amaliyotlar

  1. Urug'larni obyektlarga bering (random_state=int, default_rng, generator=), globalga tayanmang.

  2. Jarayonlar orasida barqaror bo'lishi kerak bo'lgan hamma narsa uchun sorted va hashlib.

  3. Reproduksiya testini yangi jarayonlarda, turli PYTHONHASHSEED bilan qiling.

  4. Har yurishda manifest yozing: python, lock, git (+toza), ma'lumot va konfig xeshi, natija.

  5. Konfig xeshini kanonik JSON dan, ma'lumot xeshini qat'iy formatdagi fayldan oling.

  6. Qayta ishga tushirishdan oldin manifestni avtomatik tekshiring: versiya — ogohlantirish, ma'lumot/konfig — to'xtatish.

  7. Manifestga faqat ruxsat etilgan muhit o'zgaruvchilarini yozing — sirlarni hech qachon.

  8. Qarorni bir necha urug' bo'yicha juftlashgan farq bilan qabul qiling; eng yaxshi urug'ni hisobot qilmang.


9. Amaliy topshiriq

Vazifa 1: Bashorat qiling

python
1.  # takrorlanuvchanlik va reproduksiya farqi?
2.  # natija qaytarilmasligining olti manbasi?
3.  # random.seed(0) numpy ga ta'sir qiladimi?
4.  # random_state=None nima uchun mo'rt?
5.  # hash("salom") ikki yangi jarayonda tengmi (PYTHONHASHSEED o'rnatilmagan)?
6.  # hash(12345) chi?
7.  # os.environ["PYTHONHASHSEED"] = "0" skript boshida ishlaydimi?
8.  # dict tartibi xeshga bog'liqmi?
9.  # manifestda nima bo'ladi?
10. # konfig xeshi uchun JSON qanday yoziladi?
11. # bir xil jadval CRLF va LF da saqlansa, fayl xeshi tengmi?
12. # tarqoqlikning asosiy manbai odatda qaysi urug'?
Javoblar
  1. Takrorlanuvchanlik — bit darajasida bir xil natija; reproduksiya — boshqa sharoitda ham bir xil xulosa
  2. Urug'lar, tartib, ma'lumot, kutubxona versiyalari, float/parallel hisob, muhit
  3. Yo'q
  4. Global holatga bog'liq — orada boshqa kod tasodifiy son olsa, natija o'zgaradi
  5. Yo'q
  6. Ha — int tuzlanmaydi
  7. Yo'q — interpretator allaqachon ishga tushgan
  8. Yo'q, qo'shilish tartibiga (3.7+); lekin set dan yasalgan dict — bog'liq
  9. Python, platforma, paketlar, git, ruxsat etilgan muhit o'zgaruvchilari, ma'lumot va konfig xeshi, natija
  10. sort_keys=True, qat'iy separators
  11. Yo'q
  12. Bo'lish urug'i (4-misolda)

Vazifa 2: Xatolarni tuzating

python
1.  kod = {h: i for i, h in enumerate(set(df["hudud"]))}

2.  for f in os.listdir(papka): qismlar.append(pd.read_csv(f))

3.  model = RandomForestClassifier()

4.  savat = hash(user_id) % 10

5.  manifest["muhit"] = dict(os.environ)
Javoblar
python
1.  kod = {h: i for i, h in enumerate(sorted(set(df["hudud"])))}

2.  for f in sorted(os.listdir(papka)): qismlar.append(pd.read_csv(papka / f))

3.  model = RandomForestClassifier(random_state=konfig["urug"])

4.  savat = int(hashlib.md5(str(user_id).encode()).hexdigest(), 16) % 10

5.  manifest["muhit"] = {k: os.environ.get(k) for k in ["PYTHONHASHSEED", "OMP_NUM_THREADS"]}

Vazifa 3: Reproduksiya testi

1-misolni kengaytiring:

  1. Yana bir manba qo'shing: skript n_jobs ga o'xshash "bo'laklarda yig'indi" hisoblaydi (masalan, X.mean(0) ni np.array_split bilan 1 va 7 bo'lakda) — bashorat xeshi o'zgaradimi?
  2. Reproduksiya testini funksiya qiling: reproduksiya_testi(skript, bayroqlar, urinish=3) -> bool
  3. Test muvaffaqiyatsiz bo'lsa, qaysi manba ekanini aniqlash uchun bayroqlarni bittalab o'zgartiradigan "bisektsiya" yozing
  4. Natijani JSON hisobot sifatida saqlang

Vazifa 4: Urug'lar

2-misol asosida:

  1. DataLoader(num_workers=2, worker_init_fn=...) bilan (kichik, tez Dataset) har ishchi o'z urug'ini olishini tekshiring
  2. Dataset.__getitem__ ichida np.random.default_rng() (urug'siz) ishlatilsa nima bo'ladi? worker_init_fn bilan tuzating
  3. np.random.default_rng(s).spawn(4) ni 4 ta tajribaga bering va ularning natijasi urug' tartibiga bog'liq emasligini ko'rsating
  4. PYTHONHASHSEED ni tekshiradigan "himoya" yozing: o'rnatilmagan bo'lsa, skript ogohlantirish chiqarsin (stdout ga)

Vazifa 5: Manifest

3-misoldagi manifestni kuchaytiring:

  1. Git holatini haqiqiy repozitoriyda sinang (vaqtinchalik papkada git init, fayl qo'shish, commit) va toza bayrog'ini tekshiring
  2. Katta fayl uchun bo'lakma-bo'lak xesh funksiyasini yozing va kichik fayldagi natija bilan solishtiring
  3. tekshir natijasini darajalarga ajrating: {"toxtatish": [...], "ogohlantirish": [...]}
  4. Manifestga "bashorat namunasi" qo'shing: 5 ta kirish va ularning ehtimollari — qayta yuklangan modelda ham aynan mos kelishini tekshiring

Vazifa 6: Sezgirlik

4-misolni o'zgartiring:

  1. Urug'lar sonini 5, 15, 30 qilib, juftlashgan SE qanday kamayishini ko'ring
  2. n_estimators ni 40 dan 200 ga oshiring — model urug'i tarqoqligi qanday o'zgaradi? Bo'lish urug'iniki-chi?
  3. Takroriy stratifikatsiyalangan K-fold 18.3-bob bilan xuddi shu taqqoslashni qiling
  4. Qaror funksiyasini yozing: tanla(natijalar, narx) -> str ("eng yaxshisidan sezilarli yomon bo'lmagan eng arzon")

Vazifa 7: O'ylash

Auditor sizdan so'radi: "Ikki yil oldin ishlab chiqarishga chiqarilgan kredit modelining AUC 0.812 ekanini isbotlang." Sizda model fayli (model.joblib) va o'sha paytdagi git commit bor. Nima qila olasiz, nima qila olmaysiz va bundan keyin nimani o'zgartirasiz?

Javob

Qisqa javob: model fayli va commit — yetarli emas. 0.812 ni aynan qayta olish uchun o'sha test ma'lumoti, o'sha kutubxona versiyalari va o'sha konfig kerak. Ular qayd qilinmagan bo'lsa, siz faqat taxminiy dalil bera olasiz.

1. Nima qila olasiz

Qadam Natija
Commit dan kodni tiklash O'qitish va baholash mantiqi ma'lum
model.joblib ni ochish Faqat mos sklearn versiyasida ishonchli 19.6-bob; boshqa versiyada ogohlantirish yoki xato
Versiyani topish Commit dagi requirements.txt da pin bo'lsa — yaxshi; bo'lmasa, joblib fayl metama'lumotidan yoki CI loglaridan
Test ma'lumotini topish Eng qiyin qism: ombordagi jadval ikki yilda o'zgargan bo'lishi mumkin
python
# eski muhitni tiklash (izolyatsiyada)
# uv venv eski && uv pip install scikit-learn==<o'sha versiya> ...
model = joblib.load("model.joblib")
p = model.predict_proba(X_test_eski)[:, 1]
print(roc_auc_score(y_test_eski, p))   # 0.812 chiqsa - takrorlandi

Agar test ma'lumotining aynan o'sha nusxasi yo'q bo'lsa, siz "bit darajasida" isbot bera olmaysiz. Qila oladiganingiz: o'sha davrga oid ma'lumotni qayta yig'ib, AUC ni ishonch oralig'i bilan hisoblash va 0.812 bu oraliqqa tushishini ko'rsatish (reproduksiya, 2.1). Farqni halol yozing.

2. Nima qila olmaysiz

  • Test ma'lumoti xeshi yo'q bo'lsa — "bu o'sha ma'lumot" ekanini isbotlay olmaysiz
  • Konfig (chegara, belgilar ro'yxati, urug') kodda "sehrli son" sifatida bo'lmagan bo'lsa — qaysi qiymat ishlatilgani noma'lum
  • list(set(...)) kabi jarayonga bog'liq kod bo'lsa — aynan takrorlash mumkin bo'lmasligi mumkin

3. Bundan keyin nimani o'zgartirasiz

python
manifest = {
    "python": ..., "paketlar": lock([...]),
    "git": {"commit": ..., "toza": True},           # dirty bo'lsa - deployga ruxsat yo'q
    "malumot": {"oquv_sha256": ..., "test_sha256": ...},
    "konfig": ..., "konfig_xesh": ...,
    "natija": {"auc": 0.812, "bashorat_xesh": ...},
    "nazorat_namunasi": {"X": X_test[:5].tolist(), "p": p_test[:5].tolist()},
}
  • Test ma'lumotining o'zi (yoki o'zgarmas snapshot identifikatori) model bilan birga arxivlanadi — 27.3 va 27.5
  • Muhit Docker obrazi sifatida saqlanadi 27.9-bob — ikki yildan keyin ham ishga tushadi
  • CI da har deploydan oldin reproduksiya testi (1-misol) va manifest tekshiruvi
  • Model registridagi yozuv manifestga ishora qiladi (27.5)

Auditorga javob: "Hozir bizda model va kod bor; o'sha paytdagi test ma'lumotining aynan nusxasini tiklay olsak, natijani qayta olamiz — aks holda o'sha davr ma'lumotida ishonch oralig'ini ko'rsatamiz. Bundan buyon har deployda manifest, ma'lumot snapshoti va muhit obrazi saqlanadi, shuning uchun keyingi auditda javob bir soatda tayyor bo'ladi."

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


Xulosa

Bu darsda natijani qayta olish uchun nimani nazorat qilish va nimani qayd qilish kerakligini ko'rdik.

Eng muhim uch fikr:

  1. Reproduksiya yangi jarayonda tekshiriladi — set va hash() jim buzadi. 1-misolda urug' qo'yilgan skript list(set(...)) tufayli olti yangi jarayonda olti xil natija berdi; sorted bilan esa hammasi bir xil bo'ldi. 2-misol mexanizmni ko'rsatdi: satr hash i PYTHONHASHSEED ga bog'liq, uni skript ichida o'rnatish kech, int hash i esa tuzlanmaydi. Uch global generator (random, numpy, torch) mustaqil; eng ishonchlisi — urug'ni obyektga berish (random_state=int, default_rng, generator=).

  2. Manifest — natijaning manzili. 3-misolda python, lock, git holati, ruxsat etilgan muhit o'zgaruvchilari, ma'lumot va kanonik konfig xeshi natija bilan birga yozildi va manifestdan qayta ishga tushirish aynan bir xil natija berdi. Tekshiruv bitta qiymat o'zgarishini ham ushladi (garchi natija o'zgarmagan bo'lsa ham), CRLF/LF farqi fayl xeshini o'zgartirishini ko'rsatdi va versiya farqini ogohlantirish, konfig farqini to'xtatish sababi sifatida ajratdi.

  3. Aynan takrorlanadigan natija ham bitta namuna. 4-misolda har yurish bit darajasida takrorlandi, lekin bitta urug'da g'olib almashib turdi; 15 urug'da farq sezilarli bo'lmadi va qoida arzonroq modelni tanladi. Juftlashgan SE juftlashmaganidan taxminan uch marta kichik chiqdi, tarqoqlikning asosiy manbai bo'lish urug'i bo'ldi, "eng yaxshi urug'" esa natijani 1.6 std ga optimistik ko'rsatdi.

Keyingi darsda Ma'lumot quvuri: ETL va ELT, bosqichlar va ularning bog'liqlik grafi, qayta ishga tushirilganda dublikat yaratmaydigan idempotent yuklash, inkremental yuklash, ma'lumot sifati tekshiruvlari va buzilgan partiyani karantinga olish — hammasini noldan quramiz.

Ulashish:Telegram'da

Izohlar (0)

Izoh yozish uchun kiring.

  • Hozircha izoh yo'q. Birinchi bo'ling!
27.2-dars: Reproduksiya va muhit — IlmHamroh