IlmHamroh
Data Science va sun'iy intellekt/PyTorch11/12-dars20 daqiqa
Mundarija (21)

21.11-dars: Loyiha tuzilishi va takrorlanuvchanlik

21-QISM — PYTORCH · 11-dars


1. Kirish va motivatsiya

Bu qismda ko'p bo'lak yozdik: Module, Dataset, Trainer, callback lar, checkpoint, embedding. Endi savol: ularni qanday tartiblash kerak, toki bir oydan keyin ham loyihani ochib, istalgan natijani qayta olish mumkin bo'lsin?

Chuqur o'rganish loyihalari tez tartibsizlikka tushadi. train_v2_final_REAL.py, noutbukda o'zgartirilgan va saqlanmagan lr, "o'tgan haftadagi eng yaxshi natija" — lekin qaysi kod bilan va qaysi sozlama bilan olingani noma'lum. Bu nafaqat noqulay, balki xavfli: siz hisobotda yozgan raqamni hech kim, hatto o'zingiz ham takrorlay olmaysiz.

Bu darsda uch narsani quramiz. Birinchisi — konfiguratsiya: tajribaning barcha sozlamalari bitta obyektda, faylga saqlanadigan va fayldan o'qiladigan. Ikkinchisi — takrorlanuvchanlik: seed_everything va uning chegaralari. Uchinchisi — tajriba jurnali: har yurish natijasi konfig bilan birga yoziladi va keyin jadval sifatida taqqoslanadi.

Oxirida loyihani modullarga ajratamiz va uni haqiqatan paket sifatida import qilib ishga tushiramiz — tuzilma qog'ozda emas, ishlayotgan kodda ko'rinsin.

Real vaziyat. Jamoa maqola uchun eng yaxshi natijani (0.912) qayta olishga urindi va ololmadi — 0.897 dan oshmadi. Uch kun izlashdan keyin topildi: noutbukda bir katakda lr qo'lda 3e-3 ga o'zgartirilgan, faylda esa 1e-3 edi. Konfig fayli natija bilan birga saqlanganida bu savol umuman tug'ilmasdi.

Bu darsda natijani istalgan vaqtda qayta olish mumkin bo'lgan loyiha tuzamiz.

Bu darsda:

  • Konfiguratsiya obyekti
  • Takrorlanuvchanlik va uning chegaralari
  • Tajriba jurnali
  • Loyiha tuzilishi
  • Konfigdan ishga tushirish
  • Tuzoqlar
  • Amaliy: tartibli loyiha

ℹ Misollar real torch bilan (Python 3.14, torch 2.14 CPU).


2. Nazariya — chuqur tushuntirish

2.1. Konfiguratsiya

text
BARCHA SOZLAMALAR BIR JOYDA:

@dataclass
class Konfig:
    seed: int = 42
    lr: float = 3e-3
    batch: int = 128
    davrlar: int = 30
    yashirin: int = 64
    dropout: float = 0.1
    weight_decay: float = 1e-4

NIMA BERADI:
  kod ichida "sehrli son" yo'q
  konfig JSON ga saqlanadi -> natija bilan birga
  tajribalar orasidagi farq = konfiglar orasidagi farq
  buyruq qatoridan o'zgartirish oson

QOIDA: noutbukda "bir martalik" o'zgartirish ham KONFIG orqali

Kod ichida sozlama bo'lmasin — hamma narsa konfigda, konfig esa natija bilan birga saqlanadi.

2.2. Takrorlanuvchanlik

text
def seed_everything(seed):
    random.seed(seed)
    np.random.seed(seed)
    torch.manual_seed(seed)
    # GPU bo'lsa: torch.cuda.manual_seed_all(seed)

QO'SHIMCHA (to'liq determinizm, sekinroq):
  torch.use_deterministic_algorithms(True)
  torch.backends.cudnn.deterministic = True
  torch.backends.cudnn.benchmark = False
  CUBLAS_WORKSPACE_CONFIG=:4096:8  (CUDA muhit o'zgaruvchisi)

DataLoader:
  generator=torch.Generator().manual_seed(seed)

CHEGARALAR:
  bir xil mashina, bir xil versiya -> AYNAN bir xil
  boshqa GPU / boshqa torch versiyasi -> biroz farq qilishi MUMKIN
  ko'p oqimli CPU hisobi ham ba'zan farq beradi

SHUNING UCHUN: bitta seed natijasi emas, bir necha seed O'RTACHASI

Seed takrorlashni beradi, ishonchni emas — xulosa uchun bir necha seed kerak.

2.3. Tajriba jurnali

text
HAR YURISH -> BITTA YOZUV:
  {"vaqt_belgisi": ..., "konfig": {...}, "natija": {...},
   "git": "a1b2c3d", "versiyalar": {...}}

FORMAT: JSON Lines (har qator bitta JSON)
  qo'shish oson (append)
  pandas bilan o'qish: pd.read_json(yol, lines=True)

NIMA BERADI:
  "qaysi sozlama eng yaxshi edi" - bitta so'rov
  natijani qaysi kod versiyasi bergani - git xesh
  tajribalar tarixini yo'qotmaslik

TAYYOR VOSITALAR: MLflow, Weights & Biases, TensorBoard
  (29-qism) - xuddi shu g'oya, qo'shimcha interfeys bilan

Har yurish jurnalga — konfig va natija bilan birga; xotiraga ishonmang.

2.4. Loyiha tuzilishi

text
loyiha/
  konfig.py        Konfig dataclass, yuklash/saqlash
  malumot.py       Dataset, lug'at, masshtab, DataLoader yasash
  model.py         nn.Module lar
  orgatish.py      Trainer, callback lar
  utils.py         seed_everything, jurnal, checkpoint
  orgat.py         KIRISH NUQTASI: konfig -> ma'lumot -> model -> fit
  konfiglar/
    asos.json
    katta.json
  natijalar/
    jurnal.jsonl
    <yurish_id>/ konfig.json, eng_yaxshi.pt, tarix.csv

QOIDALAR:
  modul - bitta mas'uliyat
  kirish nuqtasi - faqat yig'adi, mantiq yo'q
  har yurish o'z papkasiga

Kirish nuqtasi faqat yig'adi — mantiq modullarda, shunda ular sinaladi va qayta ishlatiladi.

2.5. Konfigdan ishga tushirish

text
python orgat.py --konfig konfiglar/asos.json --lr 1e-3

1. konfig fayldan o'qiladi
2. buyruq qatori argumentlari ustiga yoziladi
3. yakuniy konfig natijalar papkasiga SAQLANADI
4. seed_everything(konfig.seed)
5. o'rgatish
6. jurnalga yozuv

NATIJA: har natija papkasida uni bergan TO'LIQ konfig bor
        -> qayta ishga tushirish: python orgat.py --konfig natijalar/X/konfig.json

Yakuniy konfig natija papkasiga yoziladi — shu fayl bilan natijani istalgan vaqtda qayta olish mumkin.

2.6. Noutbuk va skript

text
NOUTBUK:
  + tahlil, vizualizatsiya, tez sinov
  - holat yashirin (kataklar tartibi), versiyalash qiyin

SKRIPT + MODULLAR:
  + takrorlanadi, sinaladi, versiyalanadi
  - interaktivlik kam

AMALIY TARTIB:
  noutbukda g'oyani sinash
  -> ishlagan kodni modulga ko'chirish
  -> noutbuk modulni IMPORT qiladi
  -> hisobotdagi raqamlar faqat SKRIPT orqali olinadi

Hisobotdagi har raqam skriptdan — noutbuk kashfiyot uchun, natija uchun emas.

2.7. Tuzoqlar

Asosiy tuzoqlar: sozlamalarni kod ichida yozish; noutbukda qo'lda o'zgartirilgan qiymat; konfigni natija bilan saqlamaslik; bitta seed natijasini hisobotga yozish; DataLoader generatorini seed siz qoldirish; jurnalsiz ishlash; kirish nuqtasida mantiq yozish; natijalar papkasini yurishlar orasida qayta yozish.


3. Tez ma'lumotnoma

python
import json
import random
from dataclasses import asdict, dataclass, replace

import numpy as np
import torch


@dataclass
class Konfig:
    seed: int = 42
    lr: float = 3e-3
    davrlar: int = 30


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


def saqla(k, yol):
    yol.write_text(json.dumps(asdict(k), indent=2), encoding="utf-8")


def yukla(yol):
    return Konfig(**json.loads(yol.read_text(encoding="utf-8")))


def jurnalga(yol, konfig, natija):
    with open(yol, "a", encoding="utf-8") as f:
        f.write(json.dumps({"konfig": asdict(konfig),
                            "natija": natija}) + "\n")


k = replace(Konfig(), lr=1e-3)          # o'zgartirilgan nusxa

Tuzilma xulosasi

Konfig dataclass -> JSON, natija bilan birga
seed_everything + DataLoader generator
jurnal: JSON Lines, har yurish bir qator
modullar: konfig / malumot / model / orgatish / utils
kirish nuqtasi faqat yig'adi

4. Batafsil misollar

Misollar real torch bilan (Python 3.14, torch 2.14 CPU).

Misol 1 — Konfiguratsiya obyekti

python
"""dataclass konfig: saqlash, yuklash, o'zgartirish, tekshirish."""

import json
import shutil
import tempfile
from dataclasses import asdict, dataclass, fields, replace
from pathlib import Path


@dataclass(frozen=True)
class Konfig:
    seed: int = 42
    lr: float = 3e-3
    batch: int = 128
    davrlar: int = 30
    yashirin: int = 64
    dropout: float = 0.1
    weight_decay: float = 1e-4

    def __post_init__(self):
        if not 0 <= self.dropout < 1:
            raise ValueError(f"dropout [0, 1) oralig'ida: {self.dropout}")
        if self.lr <= 0:
            raise ValueError(f"lr musbat bo'lishi kerak: {self.lr}")


def main() -> None:
    papka = Path(tempfile.mkdtemp(prefix="konfig_"))
    try:
        print("=== 1. Sukut konfig ===")
        k = Konfig()
        for f in fields(k):
            print(f"  {f.name:<14} {getattr(k, f.name)!r}")

        print("\n=== 2. JSON ga saqlash va qaytarish ===")
        yol = papka / "konfig.json"
        yol.write_text(json.dumps(asdict(k), indent=2), encoding="utf-8")
        print(f"  fayl hajmi: {yol.stat().st_size} bayt")
        k2 = Konfig(**json.loads(yol.read_text(encoding="utf-8")))
        print(f"  qaytarilgan konfig teng: {k == k2}")

        print("\n=== 3. O'zgartirilgan nusxa ===")
        katta = replace(k, yashirin=256, dropout=0.3)
        farq = {f.name: (getattr(k, f.name), getattr(katta, f.name))
                for f in fields(k)
                if getattr(k, f.name) != getattr(katta, f.name)}
        print(f"  farqlar: {farq}")
        print(f"  asl o'zgarmadi: yashirin = {k.yashirin}")

        print("\n=== 4. frozen - tasodifiy o'zgartirishdan himoya ===")
        try:
            k.lr = 1e-2
            print("  o'zgardi (kutilmagan)")
        except Exception as xato:
            print(f"  {type(xato).__name__}: konfig o'zgarmas")
        print("  noutbukda 'bir martalik' o'zgartirish imkonsiz")

        print("\n=== 5. Noto'g'ri qiymat darhol ushlanadi ===")
        for kw in [{"dropout": 1.5}, {"lr": -0.01}]:
            try:
                Konfig(**kw)
                print(f"  {kw}: qabul qilindi (kutilmagan)")
            except ValueError as xato:
                print(f"  {kw}: ValueError - {xato}")

        print("\n=== 6. Noma'lum kalit ===")
        try:
            Konfig(**{"lr": 1e-3, "l_r": 5e-3})
            print("  qabul qilindi (kutilmagan)")
        except TypeError as xato:
            print(f"  TypeError: {str(xato)[:52]}")
        print("  imlo xatosi JIM o'tib ketmaydi")
        print("  ⭐ Konfig - tajribaning to'liq va tekshiriladigan ta'rifi")
    finally:
        shutil.rmtree(papka, ignore_errors=True)


if __name__ == "__main__":
    main()

Natijaning muhim qismi:

text
=== 1. Sukut konfig ===
  seed           42
  lr             0.003
  batch          128
  davrlar        30
  yashirin       64
  dropout        0.1
  weight_decay   0.0001

=== 2. JSON ga saqlash va qaytarish ===
  fayl hajmi: 134 bayt
  qaytarilgan konfig teng: True

=== 3. O'zgartirilgan nusxa ===
  farqlar: {'yashirin': (64, 256), 'dropout': (0.1, 0.3)}
  asl o'zgarmadi: yashirin = 64

=== 4. frozen - tasodifiy o'zgartirishdan himoya ===
  FrozenInstanceError: konfig o'zgarmas
  noutbukda 'bir martalik' o'zgartirish imkonsiz

=== 5. Noto'g'ri qiymat darhol ushlanadi ===
  {'dropout': 1.5}: ValueError - dropout [0, 1) oralig'ida: 1.5
  {'lr': -0.01}: ValueError - lr musbat bo'lishi kerak: -0.01

=== 6. Noma'lum kalit ===
  TypeError: Konfig.__init__() got an unexpected keyword argument
  imlo xatosi JIM o'tib ketmaydi
  ⭐ Konfig - tajribaning to'liq va tekshiriladigan ta'rifi

Nima ko'rsatdi: 2.1-bo'lim.

Misol 2 — Takrorlanuvchanlik va uning chegaralari

python
"""seed_everything, generator va 'bitta seed' muammosi (real torch)."""

import random

import numpy as np
import torch
import torch.nn as nn
from torch.utils.data import DataLoader, TensorDataset


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


def yurish(seed, generator_bilan=True, davrlar=15):
    seed_everything(seed)
    X = torch.randn(800, 12)
    y = (X[:, 0] + X[:, 1] * X[:, 2] + 0.5 * torch.randn(800) > 0).long()
    model = nn.Sequential(nn.Linear(12, 64), nn.ReLU(), nn.Dropout(0.2),
                          nn.Linear(64, 2))
    opt = torch.optim.AdamW(model.parameters(), lr=3e-3)
    g = torch.Generator().manual_seed(seed) if generator_bilan else None
    dl = DataLoader(TensorDataset(X[:600], y[:600]), batch_size=64,
                    shuffle=True, generator=g)
    for _ in range(davrlar):
        model.train()
        for xb, yb in dl:
            opt.zero_grad()
            nn.functional.cross_entropy(model(xb), yb).backward()
            opt.step()
    model.eval()
    with torch.no_grad():
        return (model(X[600:]).argmax(1) == y[600:]).float().mean().item()


def main() -> None:
    print("=== 1. Bir xil seed - aynan bir xil natija ===")
    a, b = yurish(0), yurish(0)
    print(f"  1-yurish: {a:.6f}")
    print(f"  2-yurish: {b:.6f}")
    print(f"  aynan tengmi: {a == b}")

    print("\n=== 2. Aralashtirish tartibi va oraliqdagi tasodif ===")
    ds = TensorDataset(torch.arange(10))

    def tartib(generator_bilan, oraliq_tasodif):
        seed_everything(0)
        g = (torch.Generator().manual_seed(0) if generator_bilan
             else None)
        dl = DataLoader(ds, batch_size=10, shuffle=True, generator=g)
        if oraliq_tasodif:
            torch.randn(3)                # masalan, boshqa modul
        return next(iter(dl))[0].tolist()

    print(f"  {'variant':<28} {'tartib'}")
    for gen in [False, True]:
        for oraliq in [False, True]:
            nom = (f"{'generator' if gen else 'generatorsiz'}, "
                   f"{'oraliq tasodif' if oraliq else 'toza'}")
            print(f"  {nom:<28} {tartib(gen, oraliq)}")
    print("  generatorsiz: oraliqdagi BIR randn tartibni o'zgartirdi")
    print("  generator bilan: tartib boshqa koddan MUSTAQIL")

    print("\n=== 3. Turli seed - turli natija ===")
    ballar = np.array([yurish(s) for s in range(8)])
    print(f"  8 seed: {ballar.round(4).tolist()}")
    print(f"  o'rtacha {ballar.mean():.4f}, std {ballar.std(ddof=1):.4f}")
    print(f"  min {ballar.min():.4f}, max {ballar.max():.4f}, "
          f"oraliq {ballar.max() - ballar.min():.4f}")

    print("\n=== 4. 'Eng yaxshi seed' ni tanlash - aldov ===")
    eng = int(ballar.argmax())
    print(f"  eng yaxshi seed: {eng}, ball {ballar[eng]:.4f}")
    print(f"  o'rtachadan farq: {ballar[eng] - ballar.mean():+.4f}")
    print("  bu 'yaxshilanish' - faqat tasodif")

    print("\n=== 5. Deterministik algoritmlar ===")
    torch.use_deterministic_algorithms(True)
    e1, e2 = yurish(0), yurish(0)
    torch.use_deterministic_algorithms(False)
    print(f"  use_deterministic_algorithms(True) bilan: {e1 == e2}")
    print("  CPU da odatda o'zi deterministik; GPU da bu bayroq muhim")
    print("  ⭐ Hisobotga: o'rtacha +- std, bir necha seed bo'yicha")


if __name__ == "__main__":
    main()

Natijaning muhim qismi:

text
=== 1. Bir xil seed - aynan bir xil natija ===
  1-yurish: 0.825000
  2-yurish: 0.825000
  aynan tengmi: True

=== 2. Aralashtirish tartibi va oraliqdagi tasodif ===
  variant                      tartib
  generatorsiz, toza           [6, 7, 1, 4, 2, 0, 9, 8, 3, 5]
  generatorsiz, oraliq tasodif [7, 4, 5, 1, 9, 3, 8, 2, 0, 6]
  generator, toza              [3, 7, 5, 2, 0, 8, 1, 6, 9, 4]
  generator, oraliq tasodif    [3, 7, 5, 2, 0, 8, 1, 6, 9, 4]
  generatorsiz: oraliqdagi BIR randn tartibni o'zgartirdi
  generator bilan: tartib boshqa koddan MUSTAQIL

=== 3. Turli seed - turli natija ===
  8 seed: [0.825, 0.83, 0.805, 0.86, 0.865, 0.76, 0.84, 0.8]
  o'rtacha 0.8231, std 0.0344
  min 0.7600, max 0.8650, oraliq 0.1050

=== 4. 'Eng yaxshi seed' ni tanlash - aldov ===
  eng yaxshi seed: 4, ball 0.8650
  o'rtachadan farq: +0.0419
  bu 'yaxshilanish' - faqat tasodif

=== 5. Deterministik algoritmlar ===
  use_deterministic_algorithms(True) bilan: True
  CPU da odatda o'zi deterministik; GPU da bu bayroq muhim
  ⭐ Hisobotga: o'rtacha +- std, bir necha seed bo'yicha

Nima ko'rsatdi: 2.2-bo'lim.

Misol 3 — Tajriba jurnali

python
"""JSON Lines jurnali va tajribalarni taqqoslash (real torch)."""

import json
import random
import shutil
import tempfile
from dataclasses import asdict, dataclass, replace
from pathlib import Path

import numpy as np
import pandas as pd
import torch
import torch.nn as nn


@dataclass(frozen=True)
class Konfig:
    seed: int = 0
    lr: float = 3e-3
    yashirin: int = 64
    dropout: float = 0.1
    davrlar: int = 60


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


def malumot():
    g = torch.Generator().manual_seed(1000)
    X = torch.randn(1000, 10, generator=g)
    y = ((X[:, 0] * X[:, 1] + X[:, 2] + 0.5 * torch.randn(1000, generator=g))
         > 0).long()
    return X[:700], y[:700], X[700:], y[700:]


def orgat(k, Xtr, ytr, Xva, yva):
    seed_everything(k.seed)
    model = nn.Sequential(nn.Linear(10, k.yashirin), nn.ReLU(),
                          nn.Dropout(k.dropout), nn.Linear(k.yashirin, 2))
    opt = torch.optim.AdamW(model.parameters(), lr=k.lr)
    for _ in range(k.davrlar):
        model.train()
        opt.zero_grad()
        nn.functional.cross_entropy(model(Xtr), ytr).backward()
        opt.step()
    model.eval()
    with torch.no_grad():
        return {"val_aniqlik": round(
            (model(Xva).argmax(1) == yva).float().mean().item(), 4)}


def jurnalga(yol, k, natija):
    with open(yol, "a", encoding="utf-8") as f:
        f.write(json.dumps({"konfig": asdict(k), "natija": natija,
                            "torch": str(torch.__version__)}) + "\n")


def main() -> None:
    papka = Path(tempfile.mkdtemp(prefix="jurnal_"))
    try:
        jurnal = papka / "jurnal.jsonl"
        Xtr, ytr, Xva, yva = malumot()
        asos = Konfig()

        print("=== 1. Tajribalar ===")
        variantlar = []
        for yashirin in [16, 64, 256]:
            for lr in [1e-3, 1e-2]:
                for seed in range(3):
                    variantlar.append(replace(asos, yashirin=yashirin,
                                              lr=lr, seed=seed))
        for k in variantlar:
            jurnalga(jurnal, k, orgat(k, Xtr, ytr, Xva, yva))
        print(f"  {len(variantlar)} yurish jurnalga yozildi")
        print(f"  jurnal qatorlari: "
              f"{len(jurnal.read_text(encoding='utf-8').splitlines())}")

        print("\n=== 2. Jurnalning bitta qatori ===")
        birinchi = json.loads(jurnal.read_text(encoding="utf-8")
                              .splitlines()[0])
        print(f"  {json.dumps(birinchi, ensure_ascii=False)[:78]}...")

        print("\n=== 3. Jadval sifatida ===")
        df = pd.read_json(jurnal, lines=True)
        jadval = pd.concat([pd.json_normalize(df["konfig"]),
                            pd.json_normalize(df["natija"])], axis=1)
        print(f"  shakl: {jadval.shape}")
        print(f"  ustunlar: {list(jadval.columns)}")

        print("\n=== 4. Seedlar bo'yicha jamlash ===")
        jam = (jadval.groupby(["yashirin", "lr"])["val_aniqlik"]
               .agg(["mean", "std", "count"]).round(4)
               .sort_values("mean", ascending=False))
        print(jam.to_string())

        print("\n=== 5. Eng yaxshisi va uning konfigi ===")
        eng = jam.index[0]
        print(f"  yashirin={eng[0]}, lr={eng[1]}")
        tanlangan = jadval[(jadval["yashirin"] == eng[0])
                           & (jadval["lr"] == eng[1])]
        print(f"  bu variantning yurishlari: {len(tanlangan)}")
        qator = tanlangan.iloc[0]
        # pandas butun sonlarni float qiladi - turini konfigdan tiklaymiz
        qayta = Konfig(**{k: type(v)(qator[k])
                          for k, v in asdict(asos).items()})
        print(f"  qayta ishga tushirish uchun konfig: {qayta}")
        natija2 = orgat(qayta, Xtr, ytr, Xva, yva)
        print(f"  jurnaldagi natija: {qator['val_aniqlik']}, "
              f"qayta: {natija2['val_aniqlik']}")
        print("  ⭐ Jurnal - 'qaysi sozlama yaxshi edi' ga javob")
    finally:
        shutil.rmtree(papka, ignore_errors=True)


if __name__ == "__main__":
    main()

Natijaning muhim qismi:

text
=== 1. Tajribalar ===
  18 yurish jurnalga yozildi
  jurnal qatorlari: 18

=== 2. Jurnalning bitta qatori ===
  {"konfig": {"seed": 0, "lr": 0.001, "yashirin": 16, "dropout": 0.1, "davrlar":...

=== 3. Jadval sifatida ===
  shakl: (18, 6)
  ustunlar: ['seed', 'lr', 'yashirin', 'dropout', 'davrlar', 'val_aniqlik']

=== 4. Seedlar bo'yicha jamlash ===
                  mean     std  count
yashirin lr
16       0.010  0.8422  0.0039      3
64       0.010  0.8322  0.0084      3
256      0.010  0.8144  0.0020      3
         0.001  0.7867  0.0100      3
64       0.001  0.7522  0.0139      3
16       0.001  0.6878  0.0568      3

=== 5. Eng yaxshisi va uning konfigi ===
  yashirin=16, lr=0.01
  bu variantning yurishlari: 3
  qayta ishga tushirish uchun konfig: Konfig(seed=0, lr=0.01, yashirin=16, dropout=0.1, davrlar=60)
  jurnaldagi natija: 0.84, qayta: 0.84
  ⭐ Jurnal - 'qaysi sozlama yaxshi edi' ga javob

Nima ko'rsatdi: 2.3-bo'lim.

Misol 4 — Modullarga ajratilgan loyiha

python
"""Loyihani papka va modullarga yozib, uni paket sifatida ishlatish."""

import importlib
import json
import shutil
import subprocess
import sys
import tempfile
from pathlib import Path

MODULLAR = {
    "loyiha/__init__.py": "",
    "loyiha/konfig.py": '''
import json
from dataclasses import asdict, dataclass


@dataclass(frozen=True)
class Konfig:
    seed: int = 0
    lr: float = 0.01
    yashirin: int = 32
    davrlar: int = 80

    def saqla(self, yol):
        yol.write_text(json.dumps(asdict(self), indent=2), encoding="utf-8")

    @classmethod
    def yukla(cls, yol):
        return cls(**json.loads(yol.read_text(encoding="utf-8")))
''',
    "loyiha/malumot.py": '''
import torch


def yasa(seed=1000):
    g = torch.Generator().manual_seed(seed)
    X = torch.randn(600, 8, generator=g)
    y = (X[:, 0] - X[:, 1] + 0.3 * torch.randn(600, generator=g) > 0).long()
    return X[:450], y[:450], X[450:], y[450:]
''',
    "loyiha/model.py": '''
import torch.nn as nn


def yasa(kirish, yashirin, chiqish=2):
    return nn.Sequential(nn.Linear(kirish, yashirin), nn.ReLU(),
                         nn.Linear(yashirin, chiqish))
''',
    "loyiha/utils.py": '''
import random

import numpy as np
import torch


def seed_everything(seed):
    random.seed(seed)
    np.random.seed(seed)
    torch.manual_seed(seed)
''',
    "orgat.py": '''
import argparse
import json
import sys
from dataclasses import replace
from pathlib import Path

import torch
import torch.nn as nn

from loyiha import malumot, model
from loyiha.konfig import Konfig
from loyiha.utils import seed_everything


def main():
    p = argparse.ArgumentParser()
    p.add_argument("--konfig", type=Path)
    p.add_argument("--lr", type=float)
    p.add_argument("--chiqish", type=Path, required=True)
    a = p.parse_args()
    k = Konfig.yukla(a.konfig) if a.konfig else Konfig()
    if a.lr is not None:
        k = replace(k, lr=a.lr)
    a.chiqish.mkdir(parents=True, exist_ok=True)
    k.saqla(a.chiqish / "konfig.json")
    seed_everything(k.seed)
    Xtr, ytr, Xva, yva = malumot.yasa()
    m = model.yasa(Xtr.shape[1], k.yashirin)
    opt = torch.optim.AdamW(m.parameters(), lr=k.lr)
    for _ in range(k.davrlar):
        opt.zero_grad()
        nn.functional.cross_entropy(m(Xtr), ytr).backward()
        opt.step()
    with torch.no_grad():
        aniq = (m(Xva).argmax(1) == yva).float().mean().item()
    natija = {"val_aniqlik": round(aniq, 4)}
    (a.chiqish / "natija.json").write_text(json.dumps(natija))
    print(json.dumps(natija))


if __name__ == "__main__":
    main()
''',
}


def ishga_tushir(ildiz, *args):
    r = subprocess.run([sys.executable, "orgat.py", *args], cwd=ildiz,
                       capture_output=True, text=True, timeout=120)
    if r.returncode != 0:
        raise RuntimeError(r.stderr.strip().splitlines()[-1])
    return json.loads(r.stdout.strip().splitlines()[-1])


def main() -> None:
    ildiz = Path(tempfile.mkdtemp(prefix="torch_loyiha_"))
    try:
        print("=== 1. Loyiha tuzilishi ===")
        for nisbiy, matn in MODULLAR.items():
            yol = ildiz / nisbiy
            yol.parent.mkdir(parents=True, exist_ok=True)
            yol.write_text(matn.lstrip(), encoding="utf-8")
        for yol in sorted(ildiz.rglob("*.py")):
            qatorlar = len(yol.read_text(encoding="utf-8").splitlines())
            nisbiy = yol.relative_to(ildiz).as_posix()
            print(f"  {nisbiy:<22} {qatorlar:>3} qator")

        print("\n=== 2. Modullar paket sifatida import qilinadi ===")
        sys.path.insert(0, str(ildiz))
        konfig_mod = importlib.import_module("loyiha.konfig")
        print(f"  Konfig sukut: {konfig_mod.Konfig()}")
        sys.path.remove(str(ildiz))

        print("\n=== 3. Kirish nuqtasi - buyruq qatoridan ===")
        n1 = ishga_tushir(ildiz, "--chiqish", "natijalar/asos")
        n2 = ishga_tushir(ildiz, "--lr", "0.001", "--chiqish",
                          "natijalar/kichik_lr")
        print(f"  asos:       {n1}")
        print(f"  lr=0.001:   {n2}")

        print("\n=== 4. Har natija papkasida TO'LIQ konfig ===")
        for nom in ["asos", "kichik_lr"]:
            k = json.loads((ildiz / "natijalar" / nom / "konfig.json")
                           .read_text(encoding="utf-8"))
            print(f"  {nom:<10} {k}")

        print("\n=== 5. Saqlangan konfigdan qayta ishga tushirish ===")
        n3 = ishga_tushir(ildiz, "--konfig",
                          "natijalar/kichik_lr/konfig.json",
                          "--chiqish", "natijalar/qayta")
        print(f"  asl:   {n2}")
        print(f"  qayta: {n3}")
        print(f"  aynan tengmi: {n2 == n3}")
        print("  ⭐ Natija papkasidagi konfig - natijani qayta olish kaliti")
    finally:
        shutil.rmtree(ildiz, ignore_errors=True)


if __name__ == "__main__":
    main()

Natijaning muhim qismi:

text
=== 1. Loyiha tuzilishi ===
  loyiha/__init__.py       0 qator
  loyiha/konfig.py        17 qator
  loyiha/malumot.py        8 qator
  loyiha/model.py          6 qator
  loyiha/utils.py         10 qator
  orgat.py                42 qator

=== 2. Modullar paket sifatida import qilinadi ===
  Konfig sukut: Konfig(seed=0, lr=0.01, yashirin=32, davrlar=80)

=== 3. Kirish nuqtasi - buyruq qatoridan ===
  asos:       {'val_aniqlik': 0.92}
  lr=0.001:   {'val_aniqlik': 0.86}

=== 4. Har natija papkasida TO'LIQ konfig ===
  asos       {'seed': 0, 'lr': 0.01, 'yashirin': 32, 'davrlar': 80}
  kichik_lr  {'seed': 0, 'lr': 0.001, 'yashirin': 32, 'davrlar': 80}

=== 5. Saqlangan konfigdan qayta ishga tushirish ===
  asl:   {'val_aniqlik': 0.86}
  qayta: {'val_aniqlik': 0.86}
  aynan tengmi: True
  ⭐ Natija papkasidagi konfig - natijani qayta olish kaliti

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


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

Noto'g'ri fikr To'g'risi
"Sozlamani kodda yozish tezroq" Keyin qaysi qiymat ishlatilgani noma'lum
"Seed qo'ysam natija ishonchli" Takrorlanadi, lekin bitta namuna
"Eng yaxshi seed ni tanlash mumkin" Bu tasodifni tanlash
"Jurnal ortiqcha ish" Tajribalar tarixi yo'qolmaydi
"Noutbukdagi natija yetarli" Holat yashirin, takrorlanmaydi
"Kirish nuqtasida mantiq qulay" Sinab va qayta ishlatib bo'lmaydi
"Konfigni eslab qolaman" Natija bilan birga faylga yozing
"Boshqa mashinada ham aynan teng" Ba'zan biroz farq qiladi

6. Keng tarqalgan xatolar va yechimlari

1. Kod ichidagi sozlama

python
opt = AdamW(params, lr=0.003)                        # ⚠️
opt = AdamW(params, lr=k.lr)                         # ✅

2. Konfigni saqlamaslik

python
torch.save(model.state_dict(), "eng_yaxshi.pt")      # ⚠️ qaysi sozlama?
k.saqla(papka / "konfig.json")                       # ✅

3. Bitta seed

python
print("aniqlik:", yurish(seed=0))                    # ⚠️
print(np.mean([yurish(s) for s in range(5)]))        # ✅ + std

4. Eng yaxshi seed ni tanlash

python
max(yurish(s) for s in range(20))                    # ⚠️
# o'rtacha va std ni hisobot qiling                  # ✅

5. Generatorsiz DataLoader

python
DataLoader(ds, shuffle=True)                         # ⚠️
DataLoader(ds, shuffle=True, generator=g)            # ✅

6. O'zgaruvchan konfig

python
k.lr = 1e-2   # noutbukda                            # ⚠️
k = replace(k, lr=1e-2)  # frozen dataclass           # ✅

7. Natija papkasini qayta yozish

python
chiqish = "natijalar/"                               # ⚠️ har safar ustiga
chiqish = f"natijalar/{vaqt_belgisi}_{nom}/"         # ✅

7. Integratsiya — bu bilim qayerda kerak bo'ladi

  • 19.7-dars (o'tilgan): sklearn da takrorlanuvchanlik
  • 19.10-dars (o'tilgan): sklearn loyiha tuzilishi
  • 21.5, 21.7-darslar (o'tilgan): Trainer va checkpoint
  • 21.12-dars: To'liq amaliyot
  • 29-qism: MLflow va tajribalarni kuzatish

8. Eng yaxshi amaliyotlar

  1. Hamma sozlama konfigda.

  2. Konfig frozen va tekshiriladigan.

  3. Konfig natija papkasiga yoziladi.

  4. seed_everything + DataLoader generatori.

  5. Bir necha seed, o'rtacha va std.

  6. Har yurish jurnalga.

  7. Mantiq modullarda, kirish nuqtasi yig'adi.

  8. Hisobotdagi raqamlar skriptdan.


9. Amaliy topshiriq

Vazifa 1: Bashorat qiling

python
1.  # konfig nima uchun kerak?
2.  # frozen dataclass nima beradi?
3.  # seed_everything nimalarni o'rnatadi?
4.  # DataLoader uchun qo'shimcha nima?
5.  # bir xil seed natijasi har joyda bir xilmi?
6.  # hisobotga qancha seed?
7.  # "eng yaxshi seed" nega aldov?
8.  # jurnal formati?
9.  # jurnalda nima bo'ladi?
10. # kirish nuqtasining vazifasi?
11. # natija qanday qayta olinadi?
12. # noutbuk nima uchun?
Javoblar
  1. Hamma sozlama bir joyda va saqlanadi
  2. Tasodifiy o'zgartirishdan himoya
  3. random, numpy, torch
  4. generator
  5. Bir xil mashina/versiyada — ha
  6. Kamida 3-5
  7. Tasodifni tanlaydi
  8. JSON Lines
  9. Konfig, natija, versiya
  10. Faqat yig'ish
  11. Saqlangan konfig bilan
  12. Kashfiyot va tahlil

Vazifa 2: Xatolarni tuzating

python
1.  opt = AdamW(params, lr=0.003)

2.  torch.save(model.state_dict(), "eng_yaxshi.pt")   # konfigsiz

3.  print("aniqlik:", yurish(seed=0))

4.  k.lr = 1e-2

5.  DataLoader(ds, shuffle=True)
Javoblar
python
1.  opt = AdamW(params, lr=k.lr)

2.  k.saqla(papka / "konfig.json"); torch.save(..., papka / "eng_yaxshi.pt")

3.  ballar = [yurish(s) for s in range(5)]; print(np.mean(ballar), np.std(ballar))

4.  k = replace(k, lr=1e-2)

5.  DataLoader(ds, shuffle=True, generator=g)

Vazifa 3: Konfig

Modellang:

  1. Sukut
  2. JSON
  3. replace
  4. Tekshiruv

Vazifa 4: Takrorlanuvchanlik

Modellang:

  1. Bir xil seed
  2. Generator
  3. Turli seed
  4. Eng yaxshi seed

Vazifa 5: Jurnal

Modellang:

  1. Yozish
  2. Qator
  3. Jadval
  4. Jamlash

Vazifa 6: Loyiha

Modellang:

  1. Tuzilma
  2. Import
  3. Buyruq qatori
  4. Qayta ishga tushirish

Vazifa 7: O'ylash

Maqolangizda "model 0.912 aniqlikka erishdi" deb yozilgan. Taqrizchi so'radi: "Bu natija qanchalik barqaror?" Sizda faqat bitta yurish bor. Nima qilasiz?

Javob

Qisqa javob: bitta yurish — bitta namuna. Taqrizchi haqli: 0.912 barqaror natijami yoki omadli seedmi — bilmaymiz.

1. Bir necha seed bilan qayta ishga tushiring

python
ballar = []
for seed in range(5):
    k = replace(asl_konfig, seed=seed)
    ballar.append(orgat(k)["test_aniqlik"])
print(f"{np.mean(ballar):.3f} ± {np.std(ballar, ddof=1):.3f}")

Bu yerda saqlangan konfig hal qiluvchi: agar u yo'q bo'lsa, siz aynan o'sha sozlamani qayta tiklay olmaysiz — va natija 0.912 ga yaqin chiqmasa, sabab seed dami yoki sozlamadami, bilolmaysiz.

2. Natijani qanday yozish kerak

Yomon Yaxshi
"0.912 aniqlik" "0.905 ± 0.006 (5 seed, o'rtacha ± std)"
"Eng yaxshi model 0.912" "Eng yaxshi seed 0.912, o'rtacha 0.905"
Bazaviysiz "Bazaviy HistGB: 0.896 ± 0.004"

3. Agar o'rtacha 0.912 dan ancha past chiqsa

Bu og'riqli, lekin halol yo'l — maqoladagi raqamni tuzatish:

  • o'rtacha va std ni yozing
  • 0.912 ni "eng yaxshi yurish" sifatida qoldirish mumkin, lekin asosiy natija o'rtacha bo'lishi kerak
  • bazaviy bilan farq 2 × SE dan oshadimi — shuni tekshiring

4. Bazaviy ham bir necha seed bilan

Taqqoslash adolatli bo'lishi uchun bazaviy model ham xuddi shu seedlarda ishga tushiriladi. Juftlashgan farq va uning SE si hisobot qilinadi (18-qism).

5. Takrorlash ma'lumotlarini ilova qiling

  • konfig fayli
  • kod versiyasi (git xesh)
  • torch, numpy versiyalari
  • qurilma (CPU/GPU modeli)
  • seedlar ro'yxati

Xulosa: bitta raqam — da'vo, bir necha seed bo'yicha o'rtacha va tarqoqlik — dalil. Konfig va jurnal bo'lsa, bu dalilni bir soatda tayyorlash mumkin; bo'lmasa — bir haftada ham qiyin.

Nimani mustahkamlaydi: 2.2, 2.3-bo'limlar.


Xulosa

Bu darsda loyiha tuzilishi va takrorlanuvchanlikni ko'rdik.

Eng muhim uch fikr:

  1. Hamma sozlama — konfigda, konfig — natija bilan birga. frozen dataclass tasodifiy o'zgartirishni imkonsiz qiladi, __post_init__ noto'g'ri qiymatni darhol ushlaydi, noma'lum kalit esa TypeError beradi. 4-misolda har natija papkasida uni bergan to'liq konfig saqlandi va shu fayldan qayta ishga tushirilgan yurish aynan bir xil natija berdi.

  2. Seed takrorlashni beradi, ishonchni emas. seed_everything va DataLoader generatori bilan bir xil seed bir xil mashinada aynan bir xil natija beradi. Lekin 2-misolda sakkiz seed orasidagi tarqoqlik sezilarli bo'ldi — "eng yaxshi seed" ni tanlash tasodifni tanlash degani. Hisobotga bir necha seed bo'yicha o'rtacha va std yoziladi.

  3. Jurnal va modullar — tartibning ikki ustuni. Har yurish JSON Lines jurnalga konfig va natija bilan yoziladi; keyin pandas bilan seedlar bo'yicha jamlab, qaysi sozlama yaxshiroq ekanini bitta so'rov bilan topish mumkin. Mantiq modullarda, kirish nuqtasi esa faqat konfigni o'qib, bo'laklarni yig'adi.

Keyingi darsda 21-qism amaliyoti: hamma bo'laklarni — Dataset, embedding li model, Trainer, callback lar, checkpoint, konfig va jurnal — bitta to'liq loyihada birlashtiramiz.

Ulashish:Telegram'da

Izohlar (0)

Izoh yozish uchun kiring.

  • Hozircha izoh yo'q. Birinchi bo'ling!
21.11-dars: Loyiha tuzilishi va takrorlanuvchanlik — IlmHamroh