IlmHamroh
Data Science va sun'iy intellekt/Kompyuter korish10/14-dars33 daqiqa
Mundarija (21)

22.10-dars: Rasm klassifikatsiya loyihasi

22-QISM — KOMPYUTER KO'RISH · 10-dars


1. Kirish va motivatsiya

Shu paytgacha rasmlarimiz tayyor tensor bo'lib kelardi: load_digits() chaqirildi — va qo'limizda (N, 8, 8) massiv. Haqiqiy loyihada ma'lumot hech qachon bunday kelmaydi. Sizga papka beriladi: ichida sinf_nomi/ kichik papkalari, ularda turli o'lchamdagi .png va .jpg fayllar, ba'zilari rangli, ba'zilari kulrang, orasida esa kimdir tasodifan qoldirgan izoh.txt yoki Thumbs.db.

Bu darsda rasm klassifikatsiyasini fayllardan boshlab to'liq loyiha sifatida quramiz. torchvision.datasets.ImageFolder aynan shu tuzilmani o'qiydi, lekin u bu muhitda yo'q — shuning uchun uning o'rnini bosuvchi PapkaDataset ni o'zimiz yozamiz va ichida nima bo'layotganini to'liq ko'ramiz: sinflar ro'yxati qanday tuziladi, har xil o'lchamdagi rasm qanday bitta o'lchamga keltiriladi, kulrang rasm uch kanalga qanday aylanadi, normalizatsiya uchun mean/std qayerdan olinadi.

Keyin 21-qismdagi tartib: stratifikatsiyalangan bo'lish, sinflar nomutanosibligi bilan ishlash, Trainer va eng yaxshi holat, test to'plamini bir marta ochish. Oxirida bitta aniqlik raqami bilan cheklanmaymiz — chalkashlik matritsasi va sinf bo'yicha aniqlik modelning qaysi sinflarda qoqilishini ko'rsatadi.

Real vaziyat. Logistika kompaniyasi posilkalardagi yorliq turlarini (4 xil) aniqlaydigan model yaratdi. Umumiy aniqlik 91% edi va hamma xursand bo'ldi. Keyin ma'lum bo'ldiki, eng kam uchraydigan "xavfli yuk" yorlig'ining faqat 55% i to'g'ri topilgan — umumiy raqam uni ko'p sinflar ortiga yashirgan edi. Sinf bo'yicha hisobot va nomutanosiblikni hisobga olgan o'rgatish bu muammoni bir haftada ochib berdi.

Bu darsda papkadagi rasmlardan boshlab to'liq klassifikatsiya loyihasini quramiz.

Bu darsda:

  • Papka tuzilmasi va PapkaDataset
  • O'lcham, kanal va normalizatsiya
  • Stratifikatsiyalangan bo'lish
  • Sinflar nomutanosibligi
  • Trainer, eng yaxshi holat va test
  • Chalkashlik matritsasi va sinf bo'yicha aniqlik
  • Tuzoqlar

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


2. Nazariya — chuqur tushuntirish

2.1. Papka tuzilmasi

text
ildiz/
  doira/          rasm_000.png  rasm_001.png ...
  kvadrat/        rasm_000.png ...
  uchburchak/     ...
  xoch/           ...

QOIDALAR (ImageFolder bilan bir xil):
  sinf nomi = kichik papka nomi
  sinflar ALIFBO tartibida saralanadi -> indeks 0, 1, 2, ...
  faqat ruxsat etilgan kengaytmalar (.png, .jpg, .jpeg, ...)
  boshqa fayllar (izoh.txt, Thumbs.db) tashlab yuboriladi

NEGA SARALASH MUHIM:
  os.listdir / iterdir tartibi kafolatlanmagan
  saralanmasa, boshqa kompyuterda "doira" = 2 bo'lib qolishi mumkin
  -> model boshqa sinf nomini chiqaradi, xato ko'rinmaydi

Sinf indeksi = saralangan papka nomlaridagi o'rin; bu xarita modelning bir qismi va paketga saqlanadi.

2.2. O'lcham, kanal, normalizatsiya

text
HAR BIR RASM UCHUN:
  Image.open(yol)
  .convert("RGB")              kulrang (L), RGBA, P -> 3 kanal
  .resize((S, S), BILINEAR)    har xil o'lcham -> bitta o'lcham
  np.asarray / 255             [0, 255] uint8 -> [0, 1] float
  (H, W, C) -> (C, H, W)       PyTorch tartibi
  (x - mean) / std             kanal bo'yicha

MEAN/STD QAYERDAN:
  FAQAT o'quv to'plamidan, kanal bo'yicha (3 son + 3 son)
  val / test / ishlab chiqarish - AYNAN shu sonlar bilan
  (pretrained model bo'lsa - uning o'z mean/std si, 22.9)

O'LCHAMNI MOSLASH VARIANTLARI:
  resize        - tomonlar nisbati buziladi, lekin hamma narsa qoladi
  resize + crop - nisbat saqlanadi, chetlari kesiladi
  pad + resize  - nisbat saqlanadi, bo'sh joy qo'shiladi

Har bir rasm bir xil yo'ldan o'tadi: RGB → o'lcham → [0, 1] → (C, H, W) → normalizatsiya; mean/std faqat o'quvdan.

2.3. Stratifikatsiyalangan bo'lish

text
idx = np.arange(len(ds)); y = ds.yorliqlar
ish, test = train_test_split(idx, test_size=0.2, stratify=y, random_state=0)
oquv, val = train_test_split(ish, test_size=0.25, stratify=y[ish], ...)
Subset(ds, oquv), Subset(ds, val), Subset(ds, test)

NEGA STRATIFY:
  kam sinfda (masalan, 30 rasm) tasodifiy bo'lish testga 2 ta
  yoki 10 ta tushirishi mumkin -> sinf bo'yicha baho ishonchsiz

HAQIQIY LOYIHADA QO'SHIMCHA:
  bir obyektning bir necha surati -> guruh bo'yicha bo'lish (18-qism)
  vaqt bo'yicha yig'ilgan -> vaqt bo'yicha bo'lish
  dublikat rasmlar -> bo'lishdan OLDIN tozalanadi

Stratifikatsiya har sinf ulushini uch to'plamda bir xil saqlaydi — kam sinflarda bu ayniqsa muhim.

2.4. Sinflar nomutanosibligi

text
MUAMMO: doira 120, xoch 30 -> model "ko'p sinf" tomonga og'adi
        oddiy aniqlik (accuracy) kam sinf xatolarini yashiradi

YECHIMLAR:
1. SINF VAZNLARI:
   w_c = N / (K * n_c)
   nn.CrossEntropyLoss(weight=torch.tensor(w))
2. WeightedRandomSampler:
   har namunaga 1 / n_c vazn -> batchlarda sinflar ~teng
3. METRIKA:
   balanced accuracy = sinf bo'yicha recall larning o'rtachasi
   eng kichik sinf recall i
   (18-qism: accuracy yetarli emas)

TANLOV: bittasi yetarli (vazn YOKI sampler), ikkalasini birga
        qo'llash ko'pincha kam sinfni haddan tashqari kuchaytiradi

Nomutanosiblikda metrika — balanced accuracy va sinf bo'yicha recall; vaznlar esa o'rgatishni shu metrikaga yo'naltiradi.

2.5. Trainer va eng yaxshi holat

text
Trainer 21.5-bob:
  _davr(dl, orgatish) -> loss, acc, bacc (butun davr bashoratlarida)
  EngYaxshisi(monitor="val_bacc", sabr=8):
    yaxshilansa - deepcopy(state_dict)
    oxirida - load_state_dict(eng yaxshi)

MONITOR TANLASH:
  nomutanosib sinflarda val_bacc (val_acc emas)
  val_loss - silliq, lekin metrikaga to'g'ridan-to'g'ri mos emas

TEST:
  model, giperparametrlar, epoxalar soni - FAQAT val bo'yicha
  test oxirida BIR MARTA

Monitor metrika — loyihaning asosiy metrikasi bilan bir xil bo'lishi kerak; nomutanosiblikda bu balanced accuracy.

2.6. Chalkashlik matritsasi va sinf bo'yicha hisobot

text
confusion_matrix(y_haqiqiy, y_bashorat)
  satr    = haqiqiy sinf
  ustun   = bashorat qilingan sinf
  diagonal = to'g'ri

SINF BO'YICHA:
  recall_c    = diagonal_c / satr_yig'indisi_c    "sinfning qanchasi topildi"
  precision_c = diagonal_c / ustun_yig'indisi_c   "bashoratning qanchasi to'g'ri"

NIMA IZLANADI:
  eng katta diagonaldan tashqari katak -> qaysi juftlik chalkashadi
  kam sinf recall i past -> nomutanosiblik ta'siri
  bitta ustun katta -> model "yig'uvchi" sinfga og'gan

XATOLARNI KO'ZDAN KECHIRISH:
  eng ishonchli xato bashoratlar -> ko'pincha belgilash xatosi

Chalkashlik matritsasi "qancha xato" emas, "qanday xato" ni ko'rsatadi.

2.7. Tuzoqlar

Asosiy tuzoqlar: sinflarni saralamasdan indekslash; kulrang yoki RGBA rasmni convert("RGB") siz yuklash (kanal soni har xil bo'lib, batch yig'ilmaydi); mean/std ni butun to'plamda yoki testda hisoblash; stratifikatsiyasiz bo'lish; nomutanosib sinflarda faqat accuracy ga qarash; vazn va samplerni birga qo'llab kam sinfni haddan oshirish; testni epoxa tanlashda ishlatish; paketga sinf nomlari va mean/std ni qo'shmaslik; papkadagi begona fayllarni filtrlamaslik.


3. Tez ma'lumotnoma

python
from pathlib import Path

import numpy as np
import torch
from PIL import Image
from torch.utils.data import Dataset

KENGAYTMALAR = {".png", ".jpg", ".jpeg", ".bmp"}


class PapkaDataset(Dataset):
    def __init__(self, ildiz, olcham=24):
        ildiz = Path(ildiz)
        self.sinflar = sorted(p.name for p in ildiz.iterdir() if p.is_dir())
        self.namunalar = [(f, i) for i, s in enumerate(self.sinflar)
                          for f in sorted((ildiz / s).iterdir())
                          if f.suffix.lower() in KENGAYTMALAR]
        self.olcham, self.mean, self.std = olcham, None, None

    def __len__(self):
        return len(self.namunalar)

    def __getitem__(self, i):
        yol, y = self.namunalar[i]
        with Image.open(yol) as im:
            im = im.convert("RGB").resize((self.olcham, self.olcham),
                                          Image.BILINEAR)
        x = torch.from_numpy(np.asarray(im, np.float32) / 255).permute(2, 0, 1)
        if self.mean is not None:
            x = (x - self.mean[:, None, None]) / self.std[:, None, None]
        return x, y


# sinf vaznlari: w_c = N / (K * n_c)
soni = np.bincount(y_oquv, minlength=K)
kriteriy = torch.nn.CrossEntropyLoss(
    weight=torch.tensor(len(y_oquv) / (K * soni), dtype=torch.float32))

Loyiha xulosasi

papka -> sinflar (saralangan) -> PapkaDataset (RGB, resize, /255, CHW)
stratify bo'lish -> mean/std faqat o'quvda
nomutanosiblik: sinf vaznlari yoki sampler; metrika - balanced accuracy
Trainer + EngYaxshisi(val_bacc) -> test BIR MARTA
chalkashlik matritsasi + sinf bo'yicha recall/precision
paket: holat + sinflar + mean/std + olcham

4. Batafsil misollar

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

Barcha misollar vaqtincha papkada PIL bilan sintetik rasmlar yaratadi: to'rt sinf — doira, kvadrat, uchburchak, xoch. Rasmlar turli o'lchamda (tomonlari 20–48 piksel, nisbati 0,7–1,4 gacha), fon qorong'iroq, shakl yorqinroq, ranglar tasodifiy, ustiga kuchli shovqin qo'shilgan, har yettinchi rasm atrofida kulrang (L rejim). Sinflar nomutanosib: 120, 90, 60 va 30 rasm. doira papkasida esa begona izoh.txt fayli ham bor.

Misol 1 — Papkadan o'qiydigan PapkaDataset

python
"""Papka tuzilmasi va ImageFolder ga o'xshash o'z Dataset imiz."""

import shutil
import tempfile
from collections import Counter
from pathlib import Path

import numpy as np
import torch
from PIL import Image, ImageDraw
from torch.utils.data import DataLoader, Dataset

SINFLAR = {"doira": 120, "kvadrat": 90, "uchburchak": 60, "xoch": 30}
KENGAYTMALAR = {".png", ".jpg", ".jpeg", ".bmp"}


def rasm_chiz(sinf, rng):
    w = int(rng.integers(20, 49))
    h = int(np.clip(w * rng.uniform(0.7, 1.4), 20, 48))
    fon = rng.integers(0, 130, 3)
    rang = rng.integers(110, 256, 3)
    im = Image.new("RGB", (w, h), tuple(int(v) for v in fon))
    d = ImageDraw.Draw(im)
    r = rng.uniform(0.22, 0.42) * min(w, h)
    cx, cy = rng.uniform(r, w - r), rng.uniform(r, h - r)
    f = tuple(int(v) for v in rang)
    if sinf == "doira":
        d.ellipse([cx - r, cy - r, cx + r, cy + r], fill=f)
    elif sinf == "kvadrat":
        k = 0.8 * r
        d.rectangle([cx - k, cy - k, cx + k, cy + k], fill=f)
    elif sinf == "uchburchak":
        d.polygon([(cx, cy - r), (cx - r, cy + 0.8 * r),
                   (cx + r, cy + 0.8 * r)], fill=f)
    else:
        t = 0.3 * r
        d.rectangle([cx - r, cy - t, cx + r, cy + t], fill=f)
        d.rectangle([cx - t, cy - r, cx + t, cy + r], fill=f)
    arr = np.asarray(im, np.float32) + rng.normal(0, 30, (h, w, 3))
    im = Image.fromarray(np.clip(arr, 0, 255).astype(np.uint8))
    return im.convert("L") if rng.random() < 0.15 else im


def papka_yarat(ildiz, seed=0):
    rng = np.random.default_rng(seed)
    for sinf, soni in SINFLAR.items():
        (ildiz / sinf).mkdir(parents=True)
        for i in range(soni):
            rasm_chiz(sinf, rng).save(ildiz / sinf / f"rasm_{i:03d}.png")
    (ildiz / "doira" / "izoh.txt").write_text("begona fayl", encoding="utf-8")


class PapkaDataset(Dataset):
    def __init__(self, ildiz, olcham=24):
        ildiz = Path(ildiz)
        self.sinflar = sorted(p.name for p in ildiz.iterdir() if p.is_dir())
        self.sinf_idx = {s: i for i, s in enumerate(self.sinflar)}
        self.namunalar, self.tashlangan = [], []
        for s in self.sinflar:
            for f in sorted((ildiz / s).iterdir()):
                if f.suffix.lower() in KENGAYTMALAR:
                    self.namunalar.append((f, self.sinf_idx[s]))
                else:
                    self.tashlangan.append(f)
        self.yorliqlar = np.array([y for _, y in self.namunalar])
        self.olcham, self.mean, self.std = olcham, None, None

    def __len__(self):
        return len(self.namunalar)

    def __getitem__(self, i):
        yol, y = self.namunalar[i]
        with Image.open(yol) as im:
            im = im.convert("RGB").resize((self.olcham, self.olcham),
                                          Image.BILINEAR)
        x = torch.from_numpy(np.asarray(im, np.float32) / 255)
        x = x.permute(2, 0, 1).contiguous()
        if self.mean is not None:
            x = (x - self.mean[:, None, None]) / self.std[:, None, None]
        return x, y


def main() -> None:
    ildiz = Path(tempfile.mkdtemp(prefix="rasmlar_"))
    try:
        papka_yarat(ildiz)

        print("=== 1. Papka tuzilmasi ===")
        for p in sorted(ildiz.iterdir()):
            fayllar = sorted(p.iterdir())
            turlar = Counter(f.suffix for f in fayllar)
            print(f"  {p.relative_to(ildiz).as_posix() + '/':<12} "
                  f"{len(fayllar):>4} fayl  {dict(sorted(turlar.items()))}")

        print("\n=== 2. Rasmlar bir xil emas ===")
        olchamlar, rejimlar = [], Counter()
        for p in sorted(ildiz.glob("*/*.png")):
            with Image.open(p) as im:
                olchamlar.append(im.size)
                rejimlar[im.mode] += 1
        w = np.array([s[0] for s in olchamlar])
        h = np.array([s[1] for s in olchamlar])
        print(f"  kenglik {w.min()}..{w.max()}, balandlik {h.min()}..{h.max()}")
        print(f"  har xil o'lchamlar soni: {len(set(olchamlar))}")
        print(f"  rejimlar: {dict(sorted(rejimlar.items()))}")

        print("\n=== 3. PapkaDataset ===")
        ds = PapkaDataset(ildiz, olcham=24)
        print(f"  sinflar (saralangan): {ds.sinf_idx}")
        print(f"  namunalar: {len(ds)}")
        print(f"  tashlangan: "
              f"{[f.relative_to(ildiz).as_posix() for f in ds.tashlangan]}")
        print(f"  sinf bo'yicha: {np.bincount(ds.yorliqlar).tolist()}")
        x, y = ds[0]
        print(f"  ds[0]: x {tuple(x.shape)} {x.dtype}, "
              f"oraliq [{x.min():.3f}, {x.max():.3f}], y = {y} "
              f"({ds.sinflar[y]})")

        print("\n=== 4. Kulrang rasm -> 3 kanal ===")
        kul = next(i for i, (f, _) in enumerate(ds.namunalar)
                   if Image.open(f).mode == "L")
        xk, _ = ds[kul]
        print(f"  {ds.namunalar[kul][0].relative_to(ildiz).as_posix()}: "
              f"shakl {tuple(xk.shape)}")
        print(f"  uch kanal teng: {torch.equal(xk[0], xk[1]) and torch.equal(xk[1], xk[2])}")
        with Image.open(ds.namunalar[kul][0]) as im:
            xom = np.asarray(im)
        print(f"  convert siz shakl bo'lardi: {xom.shape} - batch yig'ilmaydi")

        print("\n=== 5. DataLoader ===")
        dl = DataLoader(ds, batch_size=32, shuffle=True,
                        generator=torch.Generator().manual_seed(0))
        xb, yb = next(iter(dl))
        print(f"  batch: x {tuple(xb.shape)}, y {tuple(yb.shape)}")
        print(f"  batchdagi sinflar: {np.bincount(yb.numpy(), minlength=4).tolist()}")
        print("  ⭐ Har xil fayllar -> bir xil (3, 24, 24) tensor")
    finally:
        shutil.rmtree(ildiz, ignore_errors=True)


if __name__ == "__main__":
    main()

Natijaning muhim qismi:

text
=== 1. Papka tuzilmasi ===
  doira/        121 fayl  {'.png': 120, '.txt': 1}
  kvadrat/       90 fayl  {'.png': 90}
  uchburchak/    60 fayl  {'.png': 60}
  xoch/          30 fayl  {'.png': 30}

=== 2. Rasmlar bir xil emas ===
  kenglik 20..48, balandlik 20..48
  har xil o'lchamlar soni: 201
  rejimlar: {'L': 42, 'RGB': 258}

=== 3. PapkaDataset ===
  sinflar (saralangan): {'doira': 0, 'kvadrat': 1, 'uchburchak': 2, 'xoch': 3}
  namunalar: 300
  tashlangan: ['doira/izoh.txt']
  sinf bo'yicha: [120, 90, 60, 30]
  ds[0]: x (3, 24, 24) torch.float32, oraliq [0.000, 0.675], y = 0 (doira)

=== 4. Kulrang rasm -> 3 kanal ===
  doira/rasm_001.png: shakl (3, 24, 24)
  uch kanal teng: True
  convert siz shakl bo'lardi: (34, 43) - batch yig'ilmaydi

=== 5. DataLoader ===
  batch: x (32, 3, 24, 24), y (32,)
  batchdagi sinflar: [6, 13, 8, 5]
  ⭐ Har xil fayllar -> bir xil (3, 24, 24) tensor

Nima ko'rsatdi: 2.1, 2.2-bo'limlar.

Misol 2 — Stratifikatsiya, normalizatsiya va nomutanosiblik

python
"""Bo'lish, o'quvdan mean/std va sinflar nomutanosibligi."""

import shutil
import tempfile
from pathlib import Path

import numpy as np
import torch
from PIL import Image, ImageDraw
from sklearn.model_selection import train_test_split
from torch.utils.data import (DataLoader, Dataset, Subset,
                              WeightedRandomSampler)

SINFLAR = {"doira": 120, "kvadrat": 90, "uchburchak": 60, "xoch": 30}
KENGAYTMALAR = {".png", ".jpg", ".jpeg", ".bmp"}


def rasm_chiz(sinf, rng):
    w = int(rng.integers(20, 49))
    h = int(np.clip(w * rng.uniform(0.7, 1.4), 20, 48))
    fon = rng.integers(0, 130, 3)
    rang = rng.integers(110, 256, 3)
    im = Image.new("RGB", (w, h), tuple(int(v) for v in fon))
    d = ImageDraw.Draw(im)
    r = rng.uniform(0.22, 0.42) * min(w, h)
    cx, cy = rng.uniform(r, w - r), rng.uniform(r, h - r)
    f = tuple(int(v) for v in rang)
    if sinf == "doira":
        d.ellipse([cx - r, cy - r, cx + r, cy + r], fill=f)
    elif sinf == "kvadrat":
        k = 0.8 * r
        d.rectangle([cx - k, cy - k, cx + k, cy + k], fill=f)
    elif sinf == "uchburchak":
        d.polygon([(cx, cy - r), (cx - r, cy + 0.8 * r),
                   (cx + r, cy + 0.8 * r)], fill=f)
    else:
        t = 0.3 * r
        d.rectangle([cx - r, cy - t, cx + r, cy + t], fill=f)
        d.rectangle([cx - t, cy - r, cx + t, cy + r], fill=f)
    arr = np.asarray(im, np.float32) + rng.normal(0, 30, (h, w, 3))
    im = Image.fromarray(np.clip(arr, 0, 255).astype(np.uint8))
    return im.convert("L") if rng.random() < 0.15 else im


def papka_yarat(ildiz, seed=0):
    rng = np.random.default_rng(seed)
    for sinf, soni in SINFLAR.items():
        (ildiz / sinf).mkdir(parents=True)
        for i in range(soni):
            rasm_chiz(sinf, rng).save(ildiz / sinf / f"rasm_{i:03d}.png")
    (ildiz / "doira" / "izoh.txt").write_text("begona fayl", encoding="utf-8")


class PapkaDataset(Dataset):
    def __init__(self, ildiz, olcham=24):
        ildiz = Path(ildiz)
        self.sinflar = sorted(p.name for p in ildiz.iterdir() if p.is_dir())
        self.namunalar = [(f, i) for i, s in enumerate(self.sinflar)
                          for f in sorted((ildiz / s).iterdir())
                          if f.suffix.lower() in KENGAYTMALAR]
        self.yorliqlar = np.array([y for _, y in self.namunalar])
        self.olcham, self.mean, self.std = olcham, None, None

    def __len__(self):
        return len(self.namunalar)

    def __getitem__(self, i):
        yol, y = self.namunalar[i]
        with Image.open(yol) as im:
            im = im.convert("RGB").resize((self.olcham, self.olcham),
                                          Image.BILINEAR)
        x = torch.from_numpy(np.asarray(im, np.float32) / 255)
        x = x.permute(2, 0, 1).contiguous()
        if self.mean is not None:
            x = (x - self.mean[:, None, None]) / self.std[:, None, None]
        return x, y


def kanal_statistika(ds, indekslar):
    x = torch.stack([ds[i][0] for i in indekslar])
    return x.mean((0, 2, 3)), x.std((0, 2, 3))


def main() -> None:
    ildiz = Path(tempfile.mkdtemp(prefix="rasmlar_"))
    try:
        papka_yarat(ildiz)
        ds = PapkaDataset(ildiz)
        y = ds.yorliqlar
        K = len(ds.sinflar)

        print("=== 1. Stratifikatsiyalangan bo'lish 60/20/20 ===")
        idx = np.arange(len(ds))
        ish, te = train_test_split(idx, test_size=0.2, stratify=y,
                                   random_state=0)
        tr, va = train_test_split(ish, test_size=0.25, stratify=y[ish],
                                  random_state=0)
        print(f"  {'sinf':<11} {'jami':>5} {'o_quv':>6} {'val':>5} "
              f"{'test':>5} {'test ulushi':>12}")
        for c, s in enumerate(ds.sinflar):
            print(f"  {s:<11} {(y == c).sum():>5} {(y[tr] == c).sum():>6} "
                  f"{(y[va] == c).sum():>5} {(y[te] == c).sum():>5} "
                  f"{(y[te] == c).mean():>12.3f}")
        rng = np.random.default_rng(3)
        tasodif_xoch = [int((y[rng.permutation(len(y))[:84]] == 3).sum())
                        for _ in range(200)]
        print(f"  stratifysiz: testdagi xoch soni 200 urinishda "
              f"{min(tasodif_xoch)}..{max(tasodif_xoch)} (stratify: "
              f"{(y[te] == 3).sum()})")

        print("\n=== 2. Mean/std faqat o'quvdan ===")
        m_tr, s_tr = kanal_statistika(ds, tr)
        m_all, s_all = kanal_statistika(ds, idx)
        print(f"  o'quv:  mean {m_tr.numpy().round(4)}, "
              f"std {s_tr.numpy().round(4)}")
        print(f"  hammasi: mean {m_all.numpy().round(4)} (ishlatilmaydi)")
        ds.mean, ds.std = m_tr, s_tr
        m2, s2 = kanal_statistika(ds, tr)
        m3, s3 = kanal_statistika(ds, va)
        print(f"  normallashgan o'quv: mean {m2.numpy().round(3)}, "
              f"std {s2.numpy().round(3)}")
        print(f"  normallashgan val:   mean {m3.numpy().round(3)}, "
              f"std {s3.numpy().round(3)}")
        print("  val ~0/1 ga yaqin, lekin aynan emas - bu normal")

        print("\n=== 3. Nomutanosiblik ===")
        soni = np.bincount(y[tr], minlength=K)
        print(f"  o'quvdagi soni: {dict(zip(ds.sinflar, soni.tolist()))}")
        eng_kop = soni.argmax()
        print(f"  hammasini '{ds.sinflar[eng_kop]}' deyish: accuracy "
              f"{(y[te] == eng_kop).mean():.3f}, balanced accuracy "
              f"{1 / K:.3f}")
        w = len(y[tr]) / (K * soni)
        print(f"  sinf vaznlari N/(K*n_c): "
              f"{dict(zip(ds.sinflar, w.round(3).tolist()))}")
        print(f"  vaznli yig'indi har sinfga: {(w * soni).round(1).tolist()}")

        print("\n=== 4. WeightedRandomSampler ===")
        namuna_vazn = torch.tensor(1.0 / soni[y[tr]], dtype=torch.double)
        g = torch.Generator().manual_seed(0)
        sampler = WeightedRandomSampler(namuna_vazn, len(tr),
                                        replacement=True, generator=g)
        dl_s = DataLoader(Subset(ds, tr), batch_size=32, sampler=sampler)
        dl_o = DataLoader(Subset(ds, tr), batch_size=32, shuffle=True,
                          generator=torch.Generator().manual_seed(0))
        for nom, dl in [("oddiy shuffle", dl_o), ("sampler", dl_s)]:
            yy = torch.cat([b for _, b in dl]).numpy()
            ulush = np.bincount(yy, minlength=K) / len(yy)
            print(f"  {nom:<14} bir davrdagi ulushlar: {ulush.round(3).tolist()}")
        takror = len(tr) - len(set(sampler))
        print("  sampler: kam sinf takrorlanadi, ko'p sinfning bir qismi "
              "ko'rilmaydi")
        print(f"  (bir davrda noyob bo'lmagan tanlovlar: {takror > 0})")
        print("  ⭐ Vazn YOKI sampler - ikkalasi birga emas")
    finally:
        shutil.rmtree(ildiz, ignore_errors=True)


if __name__ == "__main__":
    main()

Natijaning muhim qismi:

text
=== 1. Stratifikatsiyalangan bo'lish 60/20/20 ===
  sinf         jami  o_quv   val  test  test ulushi
  doira         120     72    24    24        0.400
  kvadrat        90     54    18    18        0.300
  uchburchak     60     36    12    12        0.200
  xoch           30     18     6     6        0.100
  stratifysiz: testdagi xoch soni 200 urinishda 3..14 (stratify: 6)

=== 2. Mean/std faqat o'quvdan ===
  o'quv:  mean [0.3746 0.3752 0.3871], std [0.2299 0.2421 0.2286]
  hammasi: mean [0.3816 0.3792 0.3777] (ishlatilmaydi)
  normallashgan o'quv: mean [-0.  0.  0.], std [1. 1. 1.]
  normallashgan val:   mean [ 0.11  -0.004 -0.079], std [1.067 0.943 1.05 ]
  val ~0/1 ga yaqin, lekin aynan emas - bu normal

=== 3. Nomutanosiblik ===
  o'quvdagi soni: {'doira': 72, 'kvadrat': 54, 'uchburchak': 36, 'xoch': 18}
  hammasini 'doira' deyish: accuracy 0.400, balanced accuracy 0.250
  sinf vaznlari N/(K*n_c): {'doira': 0.625, 'kvadrat': 0.833, 'uchburchak': 1.25, 'xoch': 2.5}
  vaznli yig'indi har sinfga: [45.0, 45.0, 45.0, 45.0]

=== 4. WeightedRandomSampler ===
  oddiy shuffle  bir davrdagi ulushlar: [0.4, 0.3, 0.2, 0.1]
  sampler        bir davrdagi ulushlar: [0.222, 0.211, 0.278, 0.289]
  sampler: kam sinf takrorlanadi, ko'p sinfning bir qismi ko'rilmaydi
  (bir davrda noyob bo'lmagan tanlovlar: True)
  ⭐ Vazn YOKI sampler - ikkalasi birga emas

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

Misol 3 — Trainer, eng yaxshi holat va sinf vaznlari

python
"""Trainer + EngYaxshisi(val_bacc); vaznli va vaznsiz loss taqqoslash."""

import copy
import shutil
import tempfile
from pathlib import Path

import numpy as np
import pandas as pd
import torch
import torch.nn as nn
from PIL import Image, ImageDraw
from sklearn.metrics import balanced_accuracy_score, recall_score
from sklearn.model_selection import train_test_split
from torch.utils.data import DataLoader, Dataset, TensorDataset

SINFLAR = {"doira": 120, "kvadrat": 90, "uchburchak": 60, "xoch": 30}
KENGAYTMALAR = {".png", ".jpg", ".jpeg", ".bmp"}


def rasm_chiz(sinf, rng):
    w = int(rng.integers(20, 49))
    h = int(np.clip(w * rng.uniform(0.7, 1.4), 20, 48))
    fon = rng.integers(0, 130, 3)
    rang = rng.integers(110, 256, 3)
    im = Image.new("RGB", (w, h), tuple(int(v) for v in fon))
    d = ImageDraw.Draw(im)
    r = rng.uniform(0.22, 0.42) * min(w, h)
    cx, cy = rng.uniform(r, w - r), rng.uniform(r, h - r)
    f = tuple(int(v) for v in rang)
    if sinf == "doira":
        d.ellipse([cx - r, cy - r, cx + r, cy + r], fill=f)
    elif sinf == "kvadrat":
        k = 0.8 * r
        d.rectangle([cx - k, cy - k, cx + k, cy + k], fill=f)
    elif sinf == "uchburchak":
        d.polygon([(cx, cy - r), (cx - r, cy + 0.8 * r),
                   (cx + r, cy + 0.8 * r)], fill=f)
    else:
        t = 0.3 * r
        d.rectangle([cx - r, cy - t, cx + r, cy + t], fill=f)
        d.rectangle([cx - t, cy - r, cx + t, cy + r], fill=f)
    arr = np.asarray(im, np.float32) + rng.normal(0, 30, (h, w, 3))
    im = Image.fromarray(np.clip(arr, 0, 255).astype(np.uint8))
    return im.convert("L") if rng.random() < 0.15 else im


def papka_yarat(ildiz, seed=0):
    rng = np.random.default_rng(seed)
    for sinf, soni in SINFLAR.items():
        (ildiz / sinf).mkdir(parents=True)
        for i in range(soni):
            rasm_chiz(sinf, rng).save(ildiz / sinf / f"rasm_{i:03d}.png")


class PapkaDataset(Dataset):
    def __init__(self, ildiz, olcham=24):
        ildiz = Path(ildiz)
        self.sinflar = sorted(p.name for p in ildiz.iterdir() if p.is_dir())
        self.namunalar = [(f, i) for i, s in enumerate(self.sinflar)
                          for f in sorted((ildiz / s).iterdir())
                          if f.suffix.lower() in KENGAYTMALAR]
        self.yorliqlar = np.array([y for _, y in self.namunalar])
        self.olcham = olcham

    def __len__(self):
        return len(self.namunalar)

    def __getitem__(self, i):
        yol, y = self.namunalar[i]
        with Image.open(yol) as im:
            im = im.convert("RGB").resize((self.olcham, self.olcham),
                                          Image.BILINEAR)
        x = torch.from_numpy(np.asarray(im, np.float32) / 255)
        return x.permute(2, 0, 1).contiguous(), y


def tayyor_tensorlar(ds, tr, va):
    """Kichik to'plam: hamma rasm bir marta o'qiladi (kesh), mean/std o'quvdan."""
    x = torch.stack([ds[i][0] for i in range(len(ds))])
    y = torch.tensor(ds.yorliqlar)
    m = x[tr].mean((0, 2, 3))[:, None, None]
    s = x[tr].std((0, 2, 3))[:, None, None]
    return (x - m) / s, y


class CNN(nn.Module):
    def __init__(self, k):
        super().__init__()
        def blok(a, b):
            return [nn.Conv2d(a, b, 3, padding=1), nn.BatchNorm2d(b),
                    nn.ReLU()]
        self.q = nn.Sequential(*blok(3, 16), nn.MaxPool2d(2),
                               *blok(16, 32), nn.MaxPool2d(2),
                               *blok(32, 64), nn.AdaptiveAvgPool2d(1),
                               nn.Flatten(), nn.Linear(64, k))

    def forward(self, x):
        return self.q(x)


class EngYaxshisi:
    def __init__(self, monitor, sabr):
        self.monitor, self.sabr = monitor, sabr

    def fit_boshi(self, tr):
        self.eng, self.holat, self.davr, self.hisob = -np.inf, None, 0, 0

    def davr_oxiri(self, tr, davr, log):
        if log[self.monitor] > self.eng:
            self.eng, self.davr, self.hisob = log[self.monitor], davr, 0
            self.holat = copy.deepcopy(tr.model.state_dict())
        else:
            self.hisob += 1
            tr.toxtash = self.hisob >= self.sabr

    def fit_oxiri(self, tr):
        tr.model.load_state_dict(self.holat)


class Trainer:
    def __init__(self, model, opt, kriteriy, callbacklar, jadval=None):
        self.model, self.opt, self.kriteriy = model, opt, kriteriy
        self.jadval = jadval
        self.callbacklar, self.tarix, self.toxtash = callbacklar, [], False

    def _davr(self, dl, orgatish):
        self.model.train(orgatish)
        jami, n, p, t = 0.0, 0, [], []
        with torch.set_grad_enabled(orgatish):
            for xb, yb in dl:
                ch = self.model(xb)
                loss = self.kriteriy(ch, yb)
                if orgatish:
                    self.opt.zero_grad()
                    loss.backward()
                    self.opt.step()
                jami += loss.item() * len(yb)
                n += len(yb)
                p.append(ch.argmax(1))
                t.append(yb)
        p, t = torch.cat(p).numpy(), torch.cat(t).numpy()
        return {"loss": jami / n, "acc": (p == t).mean(),
                "bacc": balanced_accuracy_score(t, p)}

    def fit(self, dl_tr, dl_va, davrlar):
        for cb in self.callbacklar:
            cb.fit_boshi(self)
        for davr in range(1, davrlar + 1):
            a, b = self._davr(dl_tr, True), self._davr(dl_va, False)
            if self.jadval is not None:
                self.jadval.step()
            log = {"train_loss": a["loss"], "val_loss": b["loss"],
                   "val_acc": b["acc"], "val_bacc": b["bacc"]}
            self.tarix.append({"davr": davr, **log})
            for cb in self.callbacklar:
                cb.davr_oxiri(self, davr, log)
            if self.toxtash:
                break
        for cb in self.callbacklar:
            cb.fit_oxiri(self)
        return pd.DataFrame(self.tarix)


def yurish(x, y, tr, va, vaznli, seed, davrlar=30):
    torch.manual_seed(seed)
    K = int(y.max()) + 1
    soni = np.bincount(y[tr].numpy(), minlength=K)
    w = torch.tensor(len(tr) / (K * soni), dtype=torch.float32)
    kriteriy = nn.CrossEntropyLoss(weight=w if vaznli else None)
    model = CNN(K)
    opt = torch.optim.AdamW(model.parameters(), lr=3e-3, weight_decay=1e-4)
    jadval = torch.optim.lr_scheduler.CosineAnnealingLR(opt, davrlar)
    dl_tr = DataLoader(TensorDataset(x[tr], y[tr]), batch_size=32,
                       shuffle=True,
                       generator=torch.Generator().manual_seed(seed))
    dl_va = DataLoader(TensorDataset(x[va], y[va]), batch_size=256)
    eyx = EngYaxshisi("val_bacc", sabr=8)
    tarix = Trainer(model, opt, kriteriy, [eyx], jadval).fit(dl_tr, dl_va,
                                                             davrlar)
    model.eval()
    with torch.no_grad():
        p = model(x[va]).argmax(1).numpy()
    rec = recall_score(y[va].numpy(), p, average=None)
    return model, tarix, eyx, rec


def main() -> None:
    ildiz = Path(tempfile.mkdtemp(prefix="rasmlar_"))
    try:
        papka_yarat(ildiz)
        ds = PapkaDataset(ildiz)
        y_np = ds.yorliqlar
        idx = np.arange(len(ds))
        ish, _ = train_test_split(idx, test_size=0.2, stratify=y_np,
                                  random_state=0)
        tr, va = train_test_split(ish, test_size=0.25, stratify=y_np[ish],
                                  random_state=0)
        x, y = tayyor_tensorlar(ds, tr, va)
    finally:
        shutil.rmtree(ildiz, ignore_errors=True)
    print(f"  o'quv {len(tr)}, val {len(va)}; test bu misolda OCHILMAYDI")

    print("\n=== 1. Bitta yurish: vaznli loss, monitor val_bacc ===")
    model, tarix, eyx, rec = yurish(x, y, tr, va, vaznli=True, seed=0)
    print(f"  {len(tarix)} davr, eng yaxshi val_bacc {eyx.eng:.4f} "
          f"({eyx.davr}-davr)")
    tanlov = sorted({1, 5, 10, eyx.davr, len(tarix)})
    print(tarix[tarix["davr"].isin(tanlov)].round(4).to_string(index=False))

    print("\n=== 2. Tiklangan holat ===")
    with torch.no_grad():
        p = model(x[va]).argmax(1).numpy()
    qayta = balanced_accuracy_score(y[va].numpy(), p)
    print(f"  qayta hisoblangan val_bacc: {qayta:.4f}, callback: "
          f"{eyx.eng:.4f}, mos: {abs(qayta - eyx.eng) < 1e-9}")

    print("\n=== 3. Vaznli va vaznsiz loss, 3 seed ===")
    print(f"  {'seed':>4} {'vaznsiz bacc':>13} {'vaznli bacc':>12} "
          f"{'vaznsiz xoch':>13} {'vaznli xoch':>12}")
    b0, b1, r0, r1 = [], [], [], []
    for s in range(3):
        _, _, e0, q0 = yurish(x, y, tr, va, vaznli=False, seed=s)
        _, _, e1, q1 = yurish(x, y, tr, va, vaznli=True, seed=s)
        b0.append(e0.eng)
        b1.append(e1.eng)
        r0.append(q0[3])
        r1.append(q1[3])
        print(f"  {s:>4} {e0.eng:>13.4f} {e1.eng:>12.4f} {q0[3]:>13.3f} "
              f"{q1[3]:>12.3f}")
    for nom, a, b in [("bacc", b0, b1), ("xoch recall", r0, r1)]:
        d = np.array(b) - np.array(a)
        se = d.std(ddof=1) / np.sqrt(len(d))
        belgi = "sezilarli" if abs(d.mean()) > 2 * se else "2*SE ichida"
        print(f"  vaznli - vaznsiz, {nom:<12}: {d.mean():+.4f} "
              f"(SE {se:.4f}) {belgi}")
        if se < 1e-9:
            print(f"    SE = 0: uch farq aynan bir xil - val da atigi "
                  f"{int((y[va] == 3).sum())} ta xoch, metrika pog'onali")
    if np.mean(b1) > np.mean(b0) and np.mean(r1) > np.mean(r0):
        print("  yo'nalish vaznli loss foydasiga, lekin kichik val da buni")
        print("  kattaroq to'plam yoki ko'proq seed bilan tasdiqlash kerak")
    print("  ⭐ Qaror juftlashgan farq bilan; test hali yopiq")


if __name__ == "__main__":
    main()

Natijaning muhim qismi:

text
  o'quv 180, val 60; test bu misolda OCHILMAYDI

=== 1. Bitta yurish: vaznli loss, monitor val_bacc ===
  29 davr, eng yaxshi val_bacc 0.9167 (21-davr)
 davr  train_loss  val_loss  val_acc  val_bacc
    1      1.4023    1.3624   0.2667    0.2986
    5      1.0460    1.3839   0.5000    0.3715
   10      0.4066    0.7241   0.8833    0.8021
   21      0.0638    0.3073   0.9667    0.9167
   29      0.0663    0.3283   0.9333    0.8333

=== 2. Tiklangan holat ===
  qayta hisoblangan val_bacc: 0.9167, callback: 0.9167, mos: True

=== 3. Vaznli va vaznsiz loss, 3 seed ===
  seed  vaznsiz bacc  vaznli bacc  vaznsiz xoch  vaznli xoch
     0        0.8750       0.9167         0.500        0.667
     1        0.9167       0.9583         0.667        0.833
     2        0.9236       0.9653         0.833        1.000
  vaznli - vaznsiz, bacc        : +0.0417 (SE 0.0000) sezilarli
    SE = 0: uch farq aynan bir xil - val da atigi 6 ta xoch, metrika pog'onali
  vaznli - vaznsiz, xoch recall : +0.1667 (SE 0.0000) sezilarli
    SE = 0: uch farq aynan bir xil - val da atigi 6 ta xoch, metrika pog'onali
  yo'nalish vaznli loss foydasiga, lekin kichik val da buni
  kattaroq to'plam yoki ko'proq seed bilan tasdiqlash kerak
  ⭐ Qaror juftlashgan farq bilan; test hali yopiq

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

Misol 4 — Test bir marta, chalkashlik matritsasi va paket

python
"""Yakuniy model: test bir marta, sinf bo'yicha hisobot, paket."""

import copy
import shutil
import tempfile
from pathlib import Path

import numpy as np
import torch
import torch.nn as nn
from PIL import Image, ImageDraw
from sklearn.metrics import (balanced_accuracy_score, confusion_matrix,
                             precision_score, recall_score)
from sklearn.model_selection import train_test_split
from torch.utils.data import DataLoader, Dataset, TensorDataset

SINFLAR = {"doira": 120, "kvadrat": 90, "uchburchak": 60, "xoch": 30}
KENGAYTMALAR = {".png", ".jpg", ".jpeg", ".bmp"}


def rasm_chiz(sinf, rng):
    w = int(rng.integers(20, 49))
    h = int(np.clip(w * rng.uniform(0.7, 1.4), 20, 48))
    fon = rng.integers(0, 130, 3)
    rang = rng.integers(110, 256, 3)
    im = Image.new("RGB", (w, h), tuple(int(v) for v in fon))
    d = ImageDraw.Draw(im)
    r = rng.uniform(0.22, 0.42) * min(w, h)
    cx, cy = rng.uniform(r, w - r), rng.uniform(r, h - r)
    f = tuple(int(v) for v in rang)
    if sinf == "doira":
        d.ellipse([cx - r, cy - r, cx + r, cy + r], fill=f)
    elif sinf == "kvadrat":
        k = 0.8 * r
        d.rectangle([cx - k, cy - k, cx + k, cy + k], fill=f)
    elif sinf == "uchburchak":
        d.polygon([(cx, cy - r), (cx - r, cy + 0.8 * r),
                   (cx + r, cy + 0.8 * r)], fill=f)
    else:
        t = 0.3 * r
        d.rectangle([cx - r, cy - t, cx + r, cy + t], fill=f)
        d.rectangle([cx - t, cy - r, cx + t, cy + r], fill=f)
    arr = np.asarray(im, np.float32) + rng.normal(0, 30, (h, w, 3))
    im = Image.fromarray(np.clip(arr, 0, 255).astype(np.uint8))
    return im.convert("L") if rng.random() < 0.15 else im


def papka_yarat(ildiz, seed=0):
    rng = np.random.default_rng(seed)
    for sinf, soni in SINFLAR.items():
        (ildiz / sinf).mkdir(parents=True)
        for i in range(soni):
            rasm_chiz(sinf, rng).save(ildiz / sinf / f"rasm_{i:03d}.png")


def rasm_tensor(yol, olcham):
    with Image.open(yol) as im:
        im = im.convert("RGB").resize((olcham, olcham), Image.BILINEAR)
    return torch.from_numpy(np.asarray(im, np.float32) / 255).permute(2, 0, 1)


class PapkaDataset(Dataset):
    def __init__(self, ildiz, olcham=24):
        ildiz = Path(ildiz)
        self.sinflar = sorted(p.name for p in ildiz.iterdir() if p.is_dir())
        self.namunalar = [(f, i) for i, s in enumerate(self.sinflar)
                          for f in sorted((ildiz / s).iterdir())
                          if f.suffix.lower() in KENGAYTMALAR]
        self.yorliqlar = np.array([y for _, y in self.namunalar])
        self.olcham = olcham

    def __len__(self):
        return len(self.namunalar)

    def __getitem__(self, i):
        yol, y = self.namunalar[i]
        return rasm_tensor(yol, self.olcham), y


class CNN(nn.Module):
    def __init__(self, k):
        super().__init__()
        def blok(a, b):
            return [nn.Conv2d(a, b, 3, padding=1), nn.BatchNorm2d(b),
                    nn.ReLU()]
        self.q = nn.Sequential(*blok(3, 16), nn.MaxPool2d(2),
                               *blok(16, 32), nn.MaxPool2d(2),
                               *blok(32, 64), nn.AdaptiveAvgPool2d(1),
                               nn.Flatten(), nn.Linear(64, k))

    def forward(self, x):
        return self.q(x)


def orgat(x, y, tr, va, seed=0, davrlar=30, sabr=8):
    """Vaznli loss, eng yaxshi holat val_bacc bo'yicha (3-misol)."""
    torch.manual_seed(seed)
    K = int(y.max()) + 1
    soni = np.bincount(y[tr].numpy(), minlength=K)
    kriteriy = nn.CrossEntropyLoss(
        weight=torch.tensor(len(tr) / (K * soni), dtype=torch.float32))
    model = CNN(K)
    opt = torch.optim.AdamW(model.parameters(), lr=3e-3, weight_decay=1e-4)
    jadval = torch.optim.lr_scheduler.CosineAnnealingLR(opt, davrlar)
    dl = DataLoader(TensorDataset(x[tr], y[tr]), batch_size=32, shuffle=True,
                    generator=torch.Generator().manual_seed(seed))
    eng, holat, hisob = -1.0, None, 0
    for _ in range(davrlar):
        model.train()
        for xb, yb in dl:
            opt.zero_grad()
            kriteriy(model(xb), yb).backward()
            opt.step()
        jadval.step()
        model.eval()
        with torch.no_grad():
            b = balanced_accuracy_score(y[va].numpy(),
                                        model(x[va]).argmax(1).numpy())
        if b > eng:
            eng, hisob, holat = b, 0, copy.deepcopy(model.state_dict())
        else:
            hisob += 1
            if hisob >= sabr:
                break
    model.load_state_dict(holat)
    return model.eval(), eng


class Bashoratchi:
    def __init__(self, yol):
        p = torch.load(yol, weights_only=True)
        self.sinflar, self.olcham = p["sinflar"], p["olcham"]
        self.mean = torch.tensor(p["mean"])[:, None, None]
        self.std = torch.tensor(p["std"])[:, None, None]
        self.model = CNN(len(self.sinflar))
        self.model.load_state_dict(p["holat"])
        self.model.eval()

    def __call__(self, yollar):
        x = torch.stack([rasm_tensor(f, self.olcham) for f in yollar])
        with torch.no_grad():
            pr = torch.softmax(self.model((x - self.mean) / self.std), 1)
        return [(self.sinflar[i], q.item()) for q, i in zip(*pr.max(1))]


def main() -> None:
    ildiz = Path(tempfile.mkdtemp(prefix="rasmlar_"))
    try:
        papka_yarat(ildiz)
        ds = PapkaDataset(ildiz)
        y_np = ds.yorliqlar
        idx = np.arange(len(ds))
        ish, te = train_test_split(idx, test_size=0.2, stratify=y_np,
                                   random_state=0)
        tr, va = train_test_split(ish, test_size=0.25, stratify=y_np[ish],
                                  random_state=0)
        xs = torch.stack([ds[i][0] for i in range(len(ds))])
        mean = xs[tr].mean((0, 2, 3))
        std = xs[tr].std((0, 2, 3))
        x = (xs - mean[:, None, None]) / std[:, None, None]
        y = torch.tensor(y_np)

        print("=== 1. Yakuniy model (val bo'yicha tanlangan) ===")
        model, val_bacc = orgat(x, y, tr, va)
        print(f"  val balanced accuracy: {val_bacc:.4f}")

        print("\n=== 2. Test BIR MARTA ===")
        with torch.no_grad():
            p = model(x[te]).argmax(1).numpy()
        t = y_np[te]
        print(f"  test: {len(te)} rasm, accuracy {(p == t).mean():.4f}, "
              f"balanced {balanced_accuracy_score(t, p):.4f}")

        print("\n=== 3. Chalkashlik matritsasi (satr = haqiqiy) ===")
        cm = confusion_matrix(t, p)
        qisqa = [s[:6] for s in ds.sinflar]
        print("  " + " " * 11 + "".join(f"{s:>8}" for s in qisqa))
        for s, satr in zip(ds.sinflar, cm):
            print(f"  {s:<11}" + "".join(f"{v:>8}" for v in satr))
        tash = cm.copy()
        np.fill_diagonal(tash, 0)
        i, j = np.unravel_index(tash.argmax(), tash.shape)
        print(f"  eng ko'p chalkashlik: {ds.sinflar[i]} -> "
              f"{ds.sinflar[j]} ({tash[i, j]} ta)")

        print("\n=== 4. Sinf bo'yicha hisobot ===")
        rec = recall_score(t, p, average=None)
        pre = precision_score(t, p, average=None, zero_division=0)
        print(f"  {'sinf':<11} {'soni':>5} {'recall':>7} {'precision':>10}")
        for c, s in enumerate(ds.sinflar):
            print(f"  {s:<11} {(t == c).sum():>5} {rec[c]:>7.3f} "
                  f"{pre[c]:>10.3f}")
        eng_past = int(rec.argmin())
        print(f"  eng past recall: {ds.sinflar[eng_past]} "
              f"({rec[eng_past]:.3f})")
        if rec.min() < (p == t).mean() - 0.05:
            print("  umumiy accuracy bu sinfning muammosini yashiradi")

        print("\n=== 5. Paket va yangi fayllar ===")
        yol = ildiz / "model.pt"
        torch.save({"holat": model.state_dict(), "sinflar": ds.sinflar,
                    "olcham": ds.olcham, "mean": mean.tolist(),
                    "std": std.tolist(),
                    "metrika": {"val_bacc": round(float(val_bacc), 4)},
                    "torch": str(torch.__version__)}, yol)
        b = Bashoratchi(yol)
        print(f"  yuklandi (weights_only=True), sinflar: {b.sinflar}")
        yangi = ildiz / "yangi"
        yangi.mkdir()
        rng = np.random.default_rng(99)
        haqiqiy, yollar = [], []
        for k, s in enumerate(["xoch", "doira", "uchburchak", "kvadrat"] * 10):
            f = yangi / f"surat_{k:02d}.png"
            rasm_chiz(s, rng).save(f)
            haqiqiy.append(s)
            yollar.append(f)
        natija = b(yollar)
        for f, h, (s, q) in list(zip(yollar, haqiqiy, natija))[:4]:
            print(f"  {f.relative_to(ildiz).as_posix()}: {s:<10} "
                  f"({q:.2f})  haqiqiy {h}")
        tog = np.mean([h == s for h, (s, _) in zip(haqiqiy, natija)])
        print(f"  40 ta yangi faylda aniqlik: {tog:.3f}")
        print("  ⭐ Paketda sinflar, olcham va mean/std - aks holda bashorat "
              "ma'nosiz")
    finally:
        shutil.rmtree(ildiz, ignore_errors=True)


if __name__ == "__main__":
    main()

Natijaning muhim qismi:

text
=== 1. Yakuniy model (val bo'yicha tanlangan) ===
  val balanced accuracy: 0.9167

=== 2. Test BIR MARTA ===
  test: 60 rasm, accuracy 0.9667, balanced 0.9479

=== 3. Chalkashlik matritsasi (satr = haqiqiy) ===
                doira  kvadra  uchbur    xoch
  doira            23       1       0       0
  kvadrat           0      18       0       0
  uchburchak        0       0      12       0
  xoch              1       0       0       5
  eng ko'p chalkashlik: doira -> kvadrat (1 ta)

=== 4. Sinf bo'yicha hisobot ===
  sinf         soni  recall  precision
  doira          24   0.958      0.958
  kvadrat        18   1.000      0.947
  uchburchak     12   1.000      1.000
  xoch            6   0.833      1.000
  eng past recall: xoch 0.833-bob
  umumiy accuracy bu sinfning muammosini yashiradi

=== 5. Paket va yangi fayllar ===
  yuklandi (weights_only=True), sinflar: ['doira', 'kvadrat', 'uchburchak', 'xoch']
  yangi/surat_00.png: xoch       1.00-bob  haqiqiy xoch
  yangi/surat_01.png: doira      0.88-bob  haqiqiy doira
  yangi/surat_02.png: uchburchak 0.69-bob  haqiqiy uchburchak
  yangi/surat_03.png: kvadrat    0.76-bob  haqiqiy kvadrat
  40 ta yangi faylda aniqlik: 1.000
  ⭐ Paketda sinflar, olcham va mean/std - aks holda bashorat ma'nosiz

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


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

Noto'g'ri fikr To'g'risi
"Papka tartibi doim bir xil" iterdir tartibi kafolatlanmagan — saralash shart
"Hamma rasm RGB" Kulrang, RGBA, palitrali rasmlar uchraydi — convert("RGB")
"Rasmlar bir xil o'lchamda" Batch uchun resize majburiy
"Mean/std ni butun to'plamda hisoblash mumkin" Faqat o'quvda — aks holda sizish (leakage)
"Accuracy 90% — yaxshi model" Kam sinf recall i past bo'lishi mumkin
"Vazn va sampler birga — ikki barobar yaxshi" Kam sinfni haddan oshiradi — bittasini tanlang
"Test natijasi bo'yicha epoxani tanlash mumkin" Faqat val; test bir marta
"Paketga faqat state_dict kifoya" Sinflar, o'lcham, mean/std ham kerak

6. Keng tarqalgan xatolar va yechimlari

1. Saralanmagan sinflar

python
sinflar = [p.name for p in ildiz.iterdir()]                 # ⚠️
sinflar = sorted(p.name for p in ildiz.iterdir() if p.is_dir())  # ✅

2. Kanal soni har xil

python
x = np.asarray(Image.open(yol))                             # ⚠️ (H, W) yoki (H, W, 4)
x = np.asarray(Image.open(yol).convert("RGB"))              # ✅ doim (H, W, 3)

3. Begona fayllar

python
fayllar = list((ildiz / s).iterdir())                       # ⚠️ izoh.txt ham
fayllar = [f for f in ... if f.suffix.lower() in KENGAYTMALAR]   # ✅

4. Mean/std hamma to'plamda

python
mean = x.mean((0, 2, 3))                                    # ⚠️ test ham kirdi
mean = x[oquv].mean((0, 2, 3))                              # ✅

5. Stratifikatsiyasiz bo'lish

python
train_test_split(idx, test_size=0.2)                        # ⚠️
train_test_split(idx, test_size=0.2, stratify=y)            # ✅

6. Faqat accuracy

python
print((p == t).mean())                                      # ⚠️
print(balanced_accuracy_score(t, p), recall_score(t, p, average=None))  # ✅

7. To'liq bo'lmagan paket

python
torch.save(model.state_dict(), yol)                         # ⚠️
torch.save({"holat": ..., "sinflar": ..., "mean": ..., "std": ...,
            "olcham": ...}, yol)                            # ✅

7. Integratsiya — bu bilim qayerda kerak bo'ladi

  • 21.3-dars (o'tilgan): Dataset va DataLoader
  • 21.5-dars (o'tilgan): Trainer va callback lar
  • 21.7-dars (o'tilgan): Checkpoint va weights_only=True
  • 18-qism (o'tilgan): Balanced accuracy, chalkashlik matritsasi, stratifikatsiya
  • 22.8-, 22.9-darslar (o'tilgan): Augmentatsiya va transfer learning — shu loyihaga qo'shiladi
  • Keyingi darslar: model nimani ko'rishini tahlil qilish, obyekt aniqlash — bir xil papka va Dataset g'oyasi, faqat yorliq murakkabroq

8. Eng yaxshi amaliyotlar

  1. Sinflarni saralab indekslang va xaritani saqlang.

  2. Har rasmni convert("RGB") va resize dan o'tkazing.

  3. Begona fayllarni kengaytma bo'yicha filtrlang.

  4. mean/std ni faqat o'quvda hisoblang.

  5. Stratifikatsiyalangan bo'lishdan foydalaning.

  6. Nomutanosiblikda balanced accuracy va sinf bo'yicha recall ni kuzating.

  7. Test to'plamini oxirida bir marta oching.

  8. Paketga sinflar, o'lcham va mean/std ni qo'shing.


9. Amaliy topshiriq

Vazifa 1: Bashorat qiling

python
1.  # ImageFolder da sinf indeksi qanday aniqlanadi?
2.  # nega sinflarni saralash kerak?
3.  # kulrang rasmni qanday 3 kanalga aylantirish?
4.  # PIL rasm -> tensor: o'q tartibi?
5.  # mean/std qaysi to'plamdan?
6.  # stratify nima beradi?
7.  # sinf vazni formulasi?
8.  # WeightedRandomSampler da namuna vazni?
9.  # nomutanosiblikda qaysi metrika?
10. # chalkashlik matritsasida satr nima?
11. # recall va precision farqi?
12. # paketga nima kiradi?
Javoblar
  1. Saralangan papka nomlaridagi o'rin
  2. iterdir tartibi kafolatlanmagan — indekslar boshqa mashinada almashishi mumkin
  3. im.convert("RGB")
  4. (H, W, C) → permute(2, 0, 1) → (C, H, W)
  5. Faqat o'quv
  6. Har sinf ulushi o'quv, val va testda bir xil
  7. w_c = N / (K * n_c)
  8. 1 / n_c (namuna sinfining soni)
  9. Balanced accuracy va sinf bo'yicha recall
  10. Haqiqiy sinf
  11. Recall — sinfning qanchasi topildi; precision — bashoratning qanchasi to'g'ri
  12. state_dict, sinflar, o'lcham, mean/std, metrika, versiya

Vazifa 2: Xatolarni tuzating

python
1.  sinflar = [p.name for p in ildiz.iterdir()]

2.  x = torch.from_numpy(np.asarray(Image.open(yol)) / 255)

3.  mean = x_hammasi.mean((0, 2, 3))

4.  ish, te = train_test_split(idx, test_size=0.2)

5.  eyx = EngYaxshisi("val_acc", sabr=8)   # sinflar 120/90/60/30
Javoblar
python
1.  sinflar = sorted(p.name for p in ildiz.iterdir() if p.is_dir())

2.  im = Image.open(yol).convert("RGB").resize((S, S))
    x = torch.from_numpy(np.asarray(im, np.float32) / 255).permute(2, 0, 1)

3.  mean = x_hammasi[oquv].mean((0, 2, 3))

4.  ish, te = train_test_split(idx, test_size=0.2, stratify=y)

5.  eyx = EngYaxshisi("val_bacc", sabr=8)

Vazifa 3: Dataset

Modellang:

  1. Papka tuzilmasi
  2. Har xil o'lcham va rejim
  3. Begona fayl
  4. Kulrang rasm

Vazifa 4: Tayyorlash

Modellang:

  1. Stratify
  2. Mean/std
  3. Sinf vaznlari
  4. Sampler

Vazifa 5: O'rgatish

Modellang:

  1. Trainer
  2. Eng yaxshi holat
  3. Vaznli loss
  4. Juftlashgan farq

Vazifa 6: Hisobot

Modellang:

  1. Test bir marta
  2. Chalkashlik matritsasi
  3. Sinf bo'yicha recall
  4. Paket va Bashoratchi

Vazifa 7: O'ylash

Model ishlab chiqarishga chiqdi. Bir oydan keyin operator shikoyat qildi: "Telefon kamerasidan kelgan suratlarda model deyarli hamma narsani doira deyapti". O'quv rasmlari skanerdan olingan edi. Nima bo'lgan bo'lishi mumkin va qanday tekshirasiz?

Javob

Qisqa javob: bu ma'lumot siljishi (distribution shift). Yangi rasmlar o'quvdagilardan statistik jihatdan farq qiladi, model esa o'quvda ko'rmagan narsaga ishonchli, lekin noto'g'ri javob beradi.

Ehtimoliy sabablar:

  1. Kanal va rang: telefon rasmi RGBA yoki EXIF bo'yicha burilgan bo'lishi mumkin. convert("RGB") bo'lmasa, kod umuman ishlamasdi — demak kanal emas, lekin burilish (ImageOps.exif_transpose) hisobga olinmagan bo'lishi mumkin.
  2. Yorug'lik va kontrast: kanal o'rtachalari o'quvdagi mean/std dan juda uzoq — normalizatsiyadan keyin qiymatlar model ko'rmagan oraliqqa tushadi.
  3. O'lcham va nisbat: telefon surati 4000×3000, obyekt kichik va chetda; resize dan keyin u bir necha pikselga aylanadi.
  4. Yig'uvchi sinf: model noaniq kirishda ko'p sinf (doira — 120 ta) tomonga og'adi.

Qanday tekshiriladi:

python
# 1. Kirish statistikasi: o'quv va yangi rasmlar
m_yangi = x_yangi.mean((0, 2, 3)); s_yangi = x_yangi.std((0, 2, 3))
print(m_yangi - mean_oquv, s_yangi / std_oquv)

# 2. Bashorat taqsimoti: o'quvdagi sinf ulushlari bilan
print(np.bincount(bashoratlar, minlength=K) / len(bashoratlar))

# 3. Ishonch: o'rtacha max softmax pasaydimi?
# 4. 50 ta yangi suratni qo'lda belgilab, chalkashlik matritsasi

Yechimlar:

  • exif_transpose, markaziy kesish yoki obyektni topib kesish — quvurni yangi manbaga moslash
  • Augmentatsiya 22.8-bob: yorug'lik, kontrast, masshtab, burilish — model bunga chidamli bo'lsin
  • Telefon suratlaridan kichik belgilangan to'plam yig'ib, fine-tune (22.9)
  • Monitoring: bashorat taqsimoti va o'rtacha ishonchni har kuni kuzatish — siljish shikoyatdan oldin ko'rinadi

Xulosa: test to'plami faqat o'quv bilan bir xil manbadagi ma'lumotda sifatni kafolatlaydi. Yangi manba — yangi test.

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


Xulosa

Bu darsda papkadagi rasmlardan boshlab to'liq klassifikatsiya loyihasini qurdik.

Eng muhim uch fikr:

  1. Fayldan tensorgacha bo'lgan yo'l — loyihaning birinchi qismi. 1-misolda 300 ta rasm 201 xil o'lchamda edi, 42 tasi kulrang (L) rejimda, papkada esa begona izoh.txt bor edi. PapkaDataset sinflarni saralab indeksladi, begona faylni tashlab yubordi va har bir rasmni convert("RGB") → resize → /255 → (C, H, W) yo'lidan o'tkazib, bir xil (3, 24, 24) tensorga aylantirdi. convert siz kulrang rasm (H, W) shaklida qolib, batchni buzardi.

  2. Bo'lish va normalizatsiya o'quvdan boshlanadi. Stratifikatsiya bilan testga har doim aynan 6 ta xoch tushdi; stratifikatsiyasiz tasodifiy bo'lish 200 urinishda 3 tadan 14 tagacha berdi — bunday testda kam sinf bo'yicha xulosa qilib bo'lmaydi. mean/std faqat o'quvda hisoblandi: o'quv to'plami aniq 0/1 ga keldi, val esa unga yaqin, lekin aynan emas — shunday bo'lishi kerak.

  3. Nomutanosiblikda umumiy aniqlik yetarli emas. 3-misolda vaznli loss uch seedning har birida val balanced accuracy ni +0.042 ga, xoch recall ini +0.167 ga oshirdi; lekin val da atigi 6 ta xoch bo'lgani uchun metrika pog'onali va SE = 0 ishonch emas, kichik to'plam belgisi. Test bir marta ochilganda accuracy 0.967, balanced accuracy 0.948 chiqdi, eng kam sinf xoch ning recall i esa 0.833 — chalkashlik matritsasi va sinf bo'yicha hisobot shuni ko'rsatdi. Paketga sinflar, o'lcham va mean/std qo'shilgani uchun Bashoratchi yangi fayllarni to'g'ridan-to'g'ri yo'l bo'yicha tasnifladi.

Keyingi darsda model nimani ko'radi: birinchi qatlam filtrlari, aktivatsiya xaritalari, gradient bo'yicha saliency, Grad-CAM va okklyuziya testi — va ular haqiqatan signal joyini topadimi, raqam bilan o'lchaymiz.

Ulashish:Telegram'da

Izohlar (0)

Izoh yozish uchun kiring.

  • Hozircha izoh yo'q. Birinchi bo'ling!
22.10-dars: Rasm klassifikatsiya loyihasi — IlmHamroh