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

22.14-dars: Amaliyot — sanoat nuqsonlarini aniqlash loyihasi

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


1. Kirish va motivatsiya

22-qismda kompyuter ko'rishning asosiy qurilish bloklarini ko'rdik: rasm tensor sifatida, konvolyutsiya va pooling, birinchi CNN, ResNet bloki va GAP, BatchNorm2d, augmentatsiya, transfer learning, papkadan o'qiladigan Dataset, Grad-CAM, obyekt aniqlash va segmentatsiya. Endi ularni bitta ishlaydigan loyihaga yig'amiz.

Vazifa — ishlab chiqarish liniyasidagi metall sirt rasmlarida nuqson turini aniqlash. To'rt sinf bor: nuqsonsiz, tirnalish (to'g'ri, yorug' chiziq), dog' (dumaloq qorong'i joy) va yoriq (siniq, qorong'i chiziq). Ma'lumot nomutanosib: rasmlarning 55 foizi nuqsonsiz, yoriqlar esa atigi 10 foiz atrofida. Eng xavfli nuqson ham aynan yoriq — u keyinchalik detal sinishiga olib keladi.

Loyiha 21.12-darsdagi skeletni saqlaydi: konfig, testni qulflash, tez tekshiruvlar, Trainer va eng yaxshi holat, bir necha seed, bazaviy bilan juftlashgan taqqoslash, testni bir marta ochish va weights_only=True bilan yuklanadigan paket. Farq — endi har bosqich rasmga moslashgan: mean/std piksellar bo'yicha, augmentatsiya geometrik, model konvolyutsion, tekshiruv esa Grad-CAM bilan.

Ikki savolga alohida e'tibor beramiz. Birinchisi: qaysi xato qimmatroq? Nuqsonli detalni "nuqsonsiz" deb o'tkazib yuborish yolg'on signaldan ko'p marta qimmat — shuning uchun umumiy aniqlik emas, sinf bo'yicha recall asosiy o'lchov. Ikkinchisi: model to'g'ri sababga ko'ra to'g'rimi? Yuqori ball berib, nuqson o'rniga fon teksturasiga qarayotgan model ishlab chiqarishda birinchi yorug'lik o'zgarishida sinadi.

Real vaziyat. Keramik plitka zavodida nuqson klassifikatori testda 96% aniqlik ko'rsatdi va ishga tushirildi. Bir oydan keyin reklamatsiyalar ko'paydi: yoriqli plitkalarning uchdan biri "nuqsonsiz" deb o'tib ketayotgan edi. Aniqlik yuqori edi, chunki yoriqlar ma'lumotning 4 foizi edi. Sinf bo'yicha recall hisoblanganda yoriq uchun u 0.64 chiqdi — bu raqam dastlabki hisobotda umuman yo'q edi. Ushbu darsdagi loyiha aynan shu raqamni birinchi o'ringa qo'yadi.

Bu darsda to'liq kompyuter ko'rish loyihasini quramiz.

Bu darsda:

  • Papkadan Dataset gacha: bo'lish, mean/std, augmentatsiya
  • Kichik ResNet, sinf vazni, Trainer va eng yaxshi holat
  • Bir necha seed
  • MLP va ko'pchilik bazaviysi bilan juftlashgan taqqoslash
  • Test bir marta: sinf bo'yicha recall va nuqsonni o'tkazib yuborish narxi
  • Grad-CAM bilan tekshiruv
  • Topshirish paketi va hisobot
  • Tuzoqlar

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


2. Nazariya — chuqur tushuntirish

2.1. Loyiha xaritasi

text
Konfig (frozen dataclass)
  |
  v
Ma'lumot: papka/<sinf>/<raqam>.png  (22.10 uslubi)
  |
  v
Bo'lish 60/20/20, stratifikatsiya -> TEST QULFLANADI
  |
  v
mean/std FAQAT o'quvdan -> PapkaDataset (+ augmentatsiya faqat o'quvda)
  |
  v
Tez tekshiruvlar + bitta batch testi              21.9-bob
  |
  v
Kichik ResNet + sinf vazni + Trainer + EngYaxshisi (21.5, 22.6-22.7)
  |
  v
3 seed -> o'rtacha +- std
  |
  v
Juftlashgan CV: Ko'pchilik vs MLP vs CNN -> qaror  21.12-bob
  |
  v
Test BIR MARTA -> sinf bo'yicha recall -> Grad-CAM tekshiruvi 22.11-bob
  |
  v
Paket (weights_only) -> Bashoratchi -> hisobot

21.12-darsdagi xarita bilan solishtirsak, tuzilma deyarli bir xil. Bu tasodif emas: yaxshi loyiha skeleti ma'lumot turiga bog'liq emas. Jadval o'rnida rasm, Embedding o'rnida konvolyutsiya, AUC o'rnida macro-recall — lekin "testni qulflash → tayyorlashni o'quvda qurish → tekshirish → o'rgatish → juftlashgan qaror → test bir marta → paket" zanjiri o'zgarmaydi.

Rasm loyihasiga xos ikkita qo'shimcha qadam bor: augmentatsiya (faqat o'quvda va faqat yorliqni saqlaydigan o'zgarishlar) va vizual tekshiruv (Grad-CAM). Ikkinchisi jadval loyihasida yo'q edi — u yerda belgilar ahamiyatini tekshirish mumkin edi, lekin "model qayerga qarayapti" savoli rasmda ancha o'tkirroq turadi.

Skelet 21.12 niki — rasmga xos qo'shimchalar augmentatsiya va Grad-CAM.

2.2. Ma'lumot quvuri

text
1. PAPKA:
     papka/nuqsonsiz/00003.png, papka/yoriq/00017.png, ...
     sinf = papka nomi; fayllar tartiblangan (sorted) -> deterministik
     kichik ma'lumot: bir marta o'qib, xotirada saqlash (N, 1, H, W)

2. BO'LISH (stratifikatsiya, har sinfdan bir xil ulush):
     ish / test = 80 / 20;  ish -> o'quv / validatsiya = 75 / 25
     yoriq (10%) har bo'lakda ~10% bo'lishi SHART
     test indekslari shu yerda qulflanadi

3. NORMALLASH:
     mean, std = x[o'quv].mean(), x[o'quv].std()   <- FAQAT o'quvdan
     x_norm = (x - mean) / std                      <- hamma bo'lakka
     paketga ham aynan shu ikki son yoziladi

4. AUGMENTATSIYA (faqat o'quvda, 22.8):
     gorizontal / vertikal akslantirish, 90 gradusga burish
     - sirt nuqsonining YO'NALISHI yo'q -> yorliq o'zgarmaydi
     - piksellar to'plami o'zgarmaydi (faqat joyi) -> mean/std buzilmaydi
     yorqinlikni kuchli o'zgartirish XAVFLI:
       tirnalish (yorug') va yoriq (qorong'i) farqi aynan yorqinlikda

5. TEKSHIRUV:
     normallangan o'quv: o'rtacha ~0, std ~1
     boshlang'ich loss ~ ln(4) = 1.386
     32 namunani yodlay oladimi (bitta batch testi)

Stratifikatsiya nomutanosib ma'lumotda majburiy. Oddiy tasodifiy bo'lishda 280 ta test rasmida yoriqlar soni o'rtacha 27 atrofida, lekin taxminan 17 dan 37 gacha tebranishi mumkin (binomial standart og'ish ~5) — va yoriq recall i shu kichik songa bog'liq. 1-misolda uchala bo'lakda ham yoriq ulushi 0.096-0.100 oralig'ida chiqdi.

Augmentatsiyani tanlashda yagona savol: "bu o'zgarish yorliqni saqlaydimi?". Sirt nuqsoni uchun yo'nalish ahamiyatsiz — akslantirish va 90 gradusga burish xavfsiz. 1-misol buni tekshirdi: augmentatsiya faqat piksellar joyini o'zgartiradi, ularning to'plamini emas. Yorqinlikni o'zgartirish esa bu ma'lumotda xavfli: tirnalishni yoriqdan ajratadigan belgi aynan yorug' yoki qorong'iligi.

mean/std va augmentatsiya faqat o'quvda; augmentatsiya faqat yorliqni saqlaydigan o'zgarishlardan tuziladi.

2.3. Model va o'rgatish

text
KICHIK ResNet (22.6-22.7):
  conv3x3(1 -> 8) + BN + ReLU          24x24
  ResBlok(8)                           24x24   (x + F(x))
  pastga: conv3x3 stride 2 (8 -> 16)   12x12
  ResBlok(16)
  pastga: conv3x3 stride 2 (16 -> 32)  6x6
  ResBlok(32)
  GAP -> Linear(32 -> 4)
  ~30 ming parametr

NOMUTANOSIBLIK - SINF VAZNI:
  w_k = N / (K * n_k)                   (sklearn "balanced" bilan bir xil)
  F.cross_entropy(logit, y, weight=w)
  kam sinf xatosi qimmatroq -> recall muvozanatlashadi

METRIKA - MACRO-RECALL:
  har sinf recall ining o'rtachasi
  "hammasi nuqsonsiz" modeli: 0.25 (aniqlik esa 0.55!)

TRAINER 21.5-bob:
  AdamW + CosineAnnealingLR
  EngYaxshisi(monitor="val_mrecall", sabr=4)
    -> copy.deepcopy(state_dict()) va oxirida tiklash

SEEDLAR:
  3 seed -> o'rtacha +- std
  har seedda: model init, DataLoader generator, augmentatsiya generator

Model ataylab kichik: ~30 ming parametr va uchta ResNet bloki. 24x24 rasm va ming atrofidagi misol uchun bu yetarli, katta model esa faqat yodlash xavfini oshiradi. Stride 2 li konvolyutsiya pooling o'rnida o'lchamni kichraytiradi, oxirida GAP — klassifikatsiya uchun joylashuv kerak emas (22.12-darsdagi lokalizatsiyadan farqli).

Metrika tanlovi — loyihaning eng muhim qarori. Aniqlik bu ma'lumotda aldaydi: "hammasi nuqsonsiz" modeli 55% aniqlik oladi. Macro-recall har sinf recall ini teng vaznda o'rtachalaydi, shuning uchun u "hamma yoriqlarni o'tkazib yuborish" ni darhol jazolaydi — o'sha bazaviy model uchun u atigi 0.25.

Eng yaxshi holat nega kerakligini 2-misol ko'rsatdi: eng yaxshi val macro-recall 0.8711 8-davrda bo'ldi, oxirgi davrda esa 0.8600 — deepcopy siz hisobot va paket oxirgi davr modeliniki bo'lardi. 3-davrda esa val macro-recall vaqtincha 0.4424 gacha tushgan edi — BatchNorm li kichik tarmoqlarda boshlang'ich davrlardagi bunday sakrashlar odatiy.

Nomutanosib sinflarda metrika macro-recall, loss esa sinf vazni bilan; eng yaxshi holat deepcopy bilan saqlanadi.

2.4. Taqqoslash va qaror

text
BIR XIL 3 TA FOLD (ish = o'quv + validatsiya, test TEGILMAYDI):
  Ko'pchilik  - doim "nuqsonsiz"          (eng sodda)
  MLP         - piksellar tekislanadi      (fazoviy tuzilma yo'q)
  CNN         - kichik ResNet

HAR FOLDDA:
  mean/std - fold o'quvidan
  belgilangan davrlar soni, erta to'xtatishsiz
    (fold validatsiyasi ham tanlashga, ham baholashga ishlatilmasin)

JUFTLASHGAN FARQ:
  d_f = mrecall_CNN,f - mrecall_MLP,f
  SE = std(d) / sqrt(3)
  sezilarli: |mean(d)| > 2 * SE

QOIDA: eng yaxshisidan SEZILARLI yomon bo'lmagan ENG SODDA model
  soddalik tartibi: Ko'pchilik < MLP < CNN

MLP — halol bazaviy: u ham tarmoq, ham o'sha optimizator va sinf vazni bilan o'rgatiladi. Farqi bitta — u rasmni 576 ta mustaqil son deb ko'radi va qo'shni piksellar orasidagi bog'liqlikdan foydalana olmaydi. Nuqson rasmning istalgan joyida va istalgan yo'nalishda bo'lishi mumkin; konvolyutsiyaning og'irlik bo'lishishi 22.2-bob aynan shu holat uchun yaratilgan.

3-misolda CNN uchala foldda ham MLP dan yaxshi chiqdi: o'rtacha farq +0.2132, 2*SE = 0.0646. Qoida bo'yicha tanlov — CNN. Diqqat qiling: bu natija oldindan ma'lum emas edi. Agar nuqsonlar doim rasmning bir joyida bo'lganda yoki farq faqat umumiy yorqinlikda bo'lganda, MLP ham yetarli bo'lishi mumkin edi — va qoida uni tanlardi.

Uch fold — kam. SE ning o'zi ham noaniq bahoga ega. Bu yerda farq 2*SE dan uch barobardan ko'p bo'lgani uchun xulosa ishonchli; farq chegaraga yaqin bo'lganda 5 fold yoki takroriy CV kerak bo'ladi (18-qism).

Qarorni juftlashgan farq va SE belgilaydi; soddaroq model sezilarli yomon bo'lmasa — u tanlanadi.

2.5. Test, sinf bo'yicha recall va Grad-CAM tekshiruvi

text
TEST BIR MARTA:
  macro-recall + aniqlik
  SINF BO'YICHA RECALL - asosiy jadval
  chalkashlik matritsasi qatorlari: yoriq -> [nuqsonsiz, tirnalish, dog', yoriq]

XATO NARXI (biznes tili):
  nuqsonni o'tkazib yuborish = P(bashorat "nuqsonsiz" | nuqsonli)
    -> nuqsonli detal mijozga ketadi: reklamatsiya, xavfsizlik
  yolg'on signal = P(bashorat nuqsonli | nuqsonsiz)
    -> sog'lom detal qo'lda tekshiruvga: ish vaqti
  odatda o'tkazib yuborish 10-100 marta qimmatroq

GRAD-CAM TEKSHIRUVI 22.11-bob:
  to'g'ri topilgan nuqsonli test rasmlari uchun
  CAM energiyasining qancha qismi nuqson niqobida (+1 piksel)?
  solishtirish: niqob yuzasi (tasodifiy qarash shuncha beradi)
  nisbat > 2  -> model nuqsonga qarayapti
  nisbat ~ 1  -> model fonga yoki teksturaga qarayapti (xavfli!)

Test oxirida bir marta ochiladi va qarorni o'zgartirmaydi. Uning vazifasi — haqiqiy xatolar tarkibini ko'rsatish. 4-misolda test macro-recall 0.9003 chiqdi, lekin sinf bo'yicha jadval muhimroq narsani ko'rsatdi: yoriq recall i 0.778 — to'rt sinf ichida eng past. 27 ta yoriqdan 6 tasi "nuqsonsiz" deb o'tib ketgan. Umuman olganda nuqsonli detallarning 0.079 qismi o'tkazib yuborilgan.

Bu raqam ishlab chiqarish uchun hisobotdagi eng muhim qator. Uni kamaytirishning bir necha yo'li bor: qaror chegarasini o'zgartirish (masalan, "nuqsonsiz" ehtimolligi 0.9 dan past bo'lsa — qo'lda tekshiruv), yoriq uchun sinf vaznini oshirish, yoriq misollarini ko'proq yig'ish. Qaysi biri to'g'ri — o'tkazib yuborish va yolg'on signal narxlariga bog'liq; bu biznes bilan birga hal qilinadigan savol.

Grad-CAM tekshiruvi modelning nima uchun to'g'ri ekanini tekshiradi. Sintetik ma'lumotda har nuqsonning niqobi ma'lum, shuning uchun "model nuqsonga qarayaptimi?" savoliga raqam bilan javob berish mumkin. 4-misolda CAM energiyasi niqob sohasida uning yuzasiga nisbatan 2.4 barobar zich chiqdi — model asosan nuqson piksellariga qarayapti. Real loyihada niqob bo'lmasa, bir necha o'nlab CAM rasmini ko'z bilan ko'rish ham xuddi shu maqsadga xizmat qiladi.

Hisobotda umumiy ball emas, sinf bo'yicha recall va o'tkazib yuborish ulushi; Grad-CAM esa to'g'ri javob to'g'ri sababga ko'ra ekanini tasdiqlaydi.

2.6. Topshirish paketi va hisobot

text
paket = {
  "format": 1,
  "konfig": asdict(k),                      # kanal, davrlar, ...
  "holat": model.state_dict(),
  "kirish": {"mean": 0.4985, "std": 0.0926, "R": 24, "kanal": 1},
  "sinflar": ["nuqsonsiz", "tirnalish", "dog'", "yoriq"],   # TARTIB muhim!
  "metrika": {"val_mrecall": ..., "test_mrecall": ...,
              "test_recall": {"yoriq": ..., ...}},
  "versiyalar": {"torch": str(torch.__version__), "python": ...},
  "nazorat": {"x": 8 ta xom rasm, "logit": ularning logitlari},
}
torch.save(paket, yol)
torch.load(yol, weights_only=True)          # hammasi xavfsiz turlar

BASHORATCHI:
  paketdan model quradi -> eval() -> nazorat logitlarini qayta hisoblab tekshiradi
  bashorat(fayllar): PNG -> [0, 1] -> (x - mean) / std -> softmax -> (sinf, ehtimol)

HISOBOT:
  VAZIFA, METRIKA (macro-recall - nega), DIZAYN (bo'lish, test bir marta)
  QAROR: juftlashgan CV (CNN vs MLP, farq +- SE)
  TEST: macro-recall, SINF BO'YICHA recall, o'tkazib yuborish ulushi
  TEKSHIRUV: Grad-CAM nisbati
  PAKET: format, weights_only, nazorat
  CHEKLOVLAR: sintetik ma'lumot, bitta tekstura, kamera sharoitlari

Paketdagi eng nozik element — sinflar ro'yxati va uning tartibi. Model chiqishining 3-indeksi "yoriq" ekanini faqat shu ro'yxat aytadi. Papka nomlaridan tartibni qayta tiklashga urinish (masalan, sorted(os.listdir(...))) boshqa kompyuterda yoki yangi sinf qo'shilganda boshqacha natija berishi mumkin. 1-misolda papkalar alifbo tartibida ['dog', 'nuqsonsiz', 'tirnalish', 'yoriq'] chiqdi — model tartibi esa boshqa.

Nazorat namunasi 21.12-darsdagi kabi paketning o'zini o'zi tekshirishini ta'minlaydi. Bu yerda muhim tafsilot: nazorat xom rasmlarda (normallashdan oldin) saqlanadi, shuning uchun u mean/std ning ham to'g'ri yuklanganini tekshiradi.

Hisobot faqat yaxshi raqamlardan iborat bo'lmasligi kerak. 4-misoldagi Bashoratchi namunasida yoriqli rasm "nuqsonsiz" deb bashorat qilindi (0.649 ishonch bilan) — va bu test jadvalidagi eng past recall bilan mos keladi. Hisobotdagi "ZAIF JOY" qatori aynan shu uchun bor.

Paketda sinflar ro'yxati, mean/std va nazorat namunasi — ularsiz model boshqa joyda boshqacha ishlaydi.

2.7. Tuzoqlar

Asosiy tuzoqlar: aniqlikni nomutanosib sinflarda asosiy metrika qilish; stratifikatsiyasiz bo'lish (kam sinf testda tasodifiy sonda); mean/std ni butun ma'lumotda hisoblash; validatsiya va testga augmentatsiya qo'llash; yorliqni o'zgartiradigan augmentatsiya (bu yerda — kuchli yorqinlik o'zgarishi); sinf vaznisiz o'rgatib, kam sinfni "yo'qotish"; eng yaxshi holatni havola bilan saqlash; bitta seed natijasini hisobot qilish; bazaviyni boshqa foldlarda yoki boshqa sozlamalarda baholash; fold validatsiyasini ham erta to'xtatishga, ham baholashga ishlatish; testni bir necha marta ochish; sinflar tartibini papka nomlaridan qayta tiklash; paketga TorchVersion yoki Path obyektini qo'yish; Grad-CAM siz "model ishlayapti" deb xulosa qilish; hisobotda faqat umumiy ballni ko'rsatish.


3. Tez ma'lumotnoma

python
k = Konfig()
seed_everything(k.seed)
fayllar, y, x = papkadan_oqi(papka)                  # (N, 1, R, R)
ish, te = bolish(y, 0.2, k.seed)                     # test QULF
tr_i, va_i = bolish(y[ish], 0.25, k.seed); tr, va = ish[tr_i], ish[va_i]
mean, std = x[tr].mean().item(), x[tr].std().item()  # faqat o'quvdan
ds_tr = PapkaDataset(x[tr], y[tr], mean, std, augment=True, seed=k.seed)
ds_va = PapkaDataset(x[va], y[va], mean, std)
model = NuqsonCNN(k.kanal)
tarix, eyx = orgat(model, k, ds_tr, ds_va, y[tr])    # sinf vazni + EngYaxshisi
# juftlashgan CV: d = mrecall_CNN - mrecall_MLP; |mean(d)| > 2 * SE
p = model((x[te] - mean) / std).argmax(1)            # test BIR MARTA
recall = {s: (p[y[te] == i] == i).float().mean() for i, s in enumerate(SINFLAR)}
cam, _ = grad_cam(model, (x[te] - mean) / std)       # tekshiruv
torch.save(paket, yol); Bashoratchi(yol)             # weights_only=True

Amaliyot xulosasi

konfig -> papka -> stratifikatsiyali bo'lish (test qulf) -> mean/std o'quvdan
augmentatsiya faqat o'quvda va faqat yorliqni saqlaydigan
kichik ResNet + sinf vazni + Trainer + EngYaxshisi (deepcopy) -> 3 seed
juftlashgan CV: Ko'pchilik < MLP < CNN -> eng sodda munosib model
test bir marta: sinf bo'yicha recall, o'tkazib yuborish ulushi
Grad-CAM tekshiruvi -> paket (sinflar, mean/std, nazorat) -> hisobot

4. Batafsil misollar

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

Misol 1 — Konfig, papkadan ma'lumot va tekshiruvlar

python
"""1-qadam: ma'lumot quvuri (papka -> Dataset) va o'rgatishdan oldingi tekshiruvlar."""

import math
import random
import shutil
import tempfile
from dataclasses import dataclass
from pathlib import Path

import numpy as np
import torch
import torch.nn as nn
import torch.nn.functional as F
from PIL import Image
from torch.utils.data import DataLoader, Dataset

SINFLAR = ["nuqsonsiz", "tirnalish", "dog'", "yoriq"]
ULUSHLAR = [0.55, 0.20, 0.15, 0.10]          # nomutanosib
R = 24


@dataclass(frozen=True)
class Konfig:
    seed: int = 42
    n: int = 1400
    lr: float = 3e-3
    batch: int = 64
    davrlar: int = 10
    kanal: int = 8
    weight_decay: float = 1e-4
    sabr: int = 4


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


def yarat(n, seed):
    """Sintetik sanoat nuqsonlari: tekstura + tirnalish / dog' / yoriq.
    Qaytaradi: rasmlar (n, R, R) [0, 1], yorliqlar (n,), niqoblar (n, R, R)."""
    rng = np.random.default_rng(seed)
    yy, xx = np.mgrid[0:R, 0:R].astype(float)
    y = rng.choice(4, size=n, p=ULUSHLAR)
    rasmlar = np.zeros((n, R, R))
    niqoblar = np.zeros((n, R, R), dtype=bool)
    for i in range(n):
        th, f = rng.uniform(0, np.pi), rng.uniform(0.15, 0.3)
        fz = rng.uniform(0, 6.3)
        img = 0.5 + 0.08 * np.sin(2 * np.pi * f * (xx * np.cos(th)
                                                    + yy * np.sin(th)) + fz)
        img += rng.normal(0, 0.07, (R, R))
        kontrast = rng.uniform(0.1, 0.25)
        m = np.zeros((R, R), dtype=bool)
        if y[i] == 1:                           # tirnalish: to'g'ri, yorug'
            y0, x0 = rng.uniform(6, R - 6, 2)
            a = rng.uniform(0, np.pi)
            for t in np.linspace(-8, 8, 60):
                py = int(round(y0 + t * np.sin(a)))
                px = int(round(x0 + t * np.cos(a)))
                if 0 <= py < R and 0 <= px < R:
                    m[py, px] = True
            img[m] += kontrast
        elif y[i] == 2:                                  # dog': dumaloq, qorong'i
            cy, cx = rng.uniform(5, R - 5, 2)
            r = rng.uniform(1.8, 3.5)
            g = np.exp(-((yy - cy) ** 2 + (xx - cx) ** 2) / (2 * r ** 2))
            img -= kontrast * g
            m = g > 0.4
        elif y[i] == 3:                                  # yoriq: siniq, qorong'i
            py, px = rng.uniform(6, R - 6, 2)
            a = rng.uniform(0, 2 * np.pi)
            for _ in range(16):
                a += rng.normal(0, 0.7)
                py, px = py + np.sin(a), px + np.cos(a)
                if 0 <= int(py) < R and 0 <= int(px) < R:
                    m[int(py), int(px)] = True
            img[m] -= kontrast
        rasmlar[i] = np.clip(img, 0, 1)
        niqoblar[i] = m
    return rasmlar, y, niqoblar


def papkaga_yoz(papka, rasmlar, y):
    """ImageFolder uslubi: papka/<sinf>/<raqam>.png"""
    for nom in SINFLAR:
        (papka / nom.replace("'", "")).mkdir(parents=True, exist_ok=True)
    for i, (r, k) in enumerate(zip(rasmlar, y)):
        Image.fromarray((r * 255).round().astype(np.uint8)).save(
            papka / SINFLAR[k].replace("'", "") / f"{i:05d}.png")


def papkadan_oqi(papka):
    """Fayl ro'yxati, yorliqlar va rasmlar (bir marta o'qib, xotirada)."""
    fayllar, yorliqlar = [], []
    for k, nom in enumerate(SINFLAR):
        for f in sorted((papka / nom.replace("'", "")).glob("*.png")):
            fayllar.append(f)
            yorliqlar.append(k)
    x = np.stack([np.asarray(Image.open(f), dtype=np.float32) / 255.0
                  for f in fayllar])
    return fayllar, np.array(yorliqlar), torch.tensor(x)[:, None]


def bolish(y, ulush, seed):
    """Stratifikatsiyalangan bo'lish: har sinfdan bir xil ulush."""
    rng = np.random.default_rng(seed)
    a, b = [], []
    for k in np.unique(y):
        idx = rng.permutation(np.flatnonzero(y == k))
        m = int(round(ulush * len(idx)))
        b.extend(idx[:m])
        a.extend(idx[m:])
    return np.sort(a), np.sort(b)


class PapkaDataset(Dataset):
    """Xotiradagi rasmlar; mean/std tashqaridan (o'quvdan) beriladi."""

    def __init__(self, x, y, mean, std, augment=False, seed=0):
        self.x, self.y = x, torch.as_tensor(y)
        self.mean, self.std, self.augment = mean, std, augment
        self.g = torch.Generator().manual_seed(seed)

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

    def __getitem__(self, i):
        x = self.x[i]
        if self.augment:                       # nuqson yo'nalishsiz
            if torch.rand(1, generator=self.g) < 0.5:
                x = x.flip(-1)
            if torch.rand(1, generator=self.g) < 0.5:
                x = x.flip(-2)
            k = int(torch.randint(0, 4, (1,), generator=self.g))
            x = torch.rot90(x, k, (-2, -1))
        return (x - self.mean) / self.std, self.y[i]


class ResBlok(nn.Module):
    def __init__(self, k):
        super().__init__()
        self.c1 = nn.Conv2d(k, k, 3, padding=1, bias=False)
        self.b1 = nn.BatchNorm2d(k)
        self.c2 = nn.Conv2d(k, k, 3, padding=1, bias=False)
        self.b2 = nn.BatchNorm2d(k)

    def forward(self, x):
        h = F.relu(self.b1(self.c1(x)))
        return F.relu(x + self.b2(self.c2(h)))


def pastga(k_in, k_out):
    return nn.Sequential(nn.Conv2d(k_in, k_out, 3, stride=2, padding=1,
                                   bias=False),
                         nn.BatchNorm2d(k_out), nn.ReLU())


class NuqsonCNN(nn.Module):
    """Kichik ResNet: 24 -> 12 -> 6, GAP, 4 sinf."""

    def __init__(self, k=16):
        super().__init__()
        self.tana = nn.Sequential(
            nn.Conv2d(1, k, 3, padding=1, bias=False), nn.BatchNorm2d(k),
            nn.ReLU(), ResBlok(k), pastga(k, 2 * k), ResBlok(2 * k),
            pastga(2 * k, 4 * k), ResBlok(4 * k))
        self.bosh = nn.Linear(4 * k, len(SINFLAR))

    def forward(self, x):
        return self.bosh(self.tana(x).mean((2, 3)))


class MLP(nn.Module):
    """Bazaviy: piksellar tekislanadi, fazoviy tuzilma e'tiborsiz."""

    def __init__(self, yashirin=128):
        super().__init__()
        self.tarmoq = nn.Sequential(
            nn.Flatten(), nn.Linear(R * R, yashirin), nn.ReLU(),
            nn.Linear(yashirin, len(SINFLAR)))

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


def main() -> None:
    k = Konfig()
    seed_everything(k.seed)
    papka = Path(tempfile.mkdtemp(prefix="nuqson_"))
    try:
        print("=== 1. Ma'lumot: papkaga yozish va qayta o'qish ===")
        rasmlar, y, _ = yarat(k.n, seed=7)
        papkaga_yoz(papka, rasmlar, y)
        fayllar, yorliq, x = papkadan_oqi(papka)
        print(f"  papkalar: {sorted(p.name for p in papka.iterdir())}")
        print(f"  fayllar: {len(fayllar)}, namuna: "
              f"{fayllar[0].relative_to(papka).as_posix()}")
        print(f"  xotiradagi tensor: {tuple(x.shape)}, "
              f"[{x.min().item():.2f}, {x.max().item():.2f}]")
        soni = np.bincount(yorliq, minlength=4)
        for nom, s in zip(SINFLAR, soni):
            print(f"    {nom:<10} {s:>5}  ({s / len(yorliq):.1%})")

        print("\n=== 2. Bo'lish: 60/20/20, stratifikatsiya, test QULF ===")
        ish, te = bolish(yorliq, 0.2, k.seed)
        tr_i, va_i = bolish(yorliq[ish], 0.25, k.seed)
        tr, va = ish[tr_i], ish[va_i]
        print(f"  kesishma yo'q: {len(set(tr) | set(va) | set(te)) == len(yorliq)}")
        for nom, ii in [("o'quv", tr), ("validatsiya", va), ("test", te)]:
            print(f"  {nom:<12} {len(ii):>5}  yoriq ulushi "
                  f"{(yorliq[ii] == 3).mean():.3f}")

        print("\n=== 3. mean/std FAQAT o'quvdan ===")
        mean, std = x[tr].mean().item(), x[tr].std().item()
        print(f"  o'quv: mean {mean:.4f}, std {std:.4f}")
        ds_tr = PapkaDataset(x[tr], yorliq[tr], mean, std, augment=True,
                             seed=k.seed)
        ds_va = PapkaDataset(x[va], yorliq[va], mean, std)
        x0, y0 = ds_va[0]
        xs = torch.stack([ds_tr[i][0] for i in range(len(ds_tr))])
        print(f"  bitta namuna: {tuple(x0.shape)}, {x0.dtype}, "
              f"yorliq {int(y0)}")
        print(f"  normallangan o'quv: o'rtacha {xs.mean().item():+.3f}, "
              f"std {xs.std().item():.3f}")

        print("\n=== 4. Augmentatsiya: yorliqni saqlaydimi? ===")
        asl = (x[tr][0, 0] - mean) / std
        variantlar = [ds_tr[0][0][0] for _ in range(8)]
        farqli = sum(not torch.allclose(v, asl) for v in variantlar)
        print(f"  8 marta o'qildi: {farqli} tasi asl rasmdan farq qiladi")
        piksellar = asl.flatten().sort().values
        bir_xil = all(torch.equal(v.flatten().sort().values, piksellar)
                      for v in variantlar)
        print(f"  piksellar to'plami o'zgarmadi (faqat joyi): {bir_xil}")
        print("  flip va 90 gradusli burish nuqson turini o'zgartirmaydi")

        print("\n=== 5. Tez tekshiruvlar ===")
        g = torch.Generator().manual_seed(k.seed)
        dl = DataLoader(ds_tr, batch_size=k.batch, shuffle=True, generator=g)
        xb, yb = next(iter(dl))
        model = NuqsonCNN(k.kanal)
        model.eval()
        with torch.no_grad():
            ch = model(xb)
            boshl = F.cross_entropy(ch, yb).item()
        param = sum(p.numel() for p in model.parameters())
        tekshiruvlar = [
            ("kirish shakli", tuple(xb.shape),
             tuple(xb.shape)[1:] == (1, R, R)),
            ("chiqish shakli", tuple(ch.shape),
             tuple(ch.shape) == (k.batch, 4)),
            ("yorliqlar", sorted(set(yb.tolist())),
             set(yb.tolist()) <= {0, 1, 2, 3}),
            ("boshlang'ich loss", round(boshl, 3),
             abs(boshl - math.log(4)) < 0.3),
            ("parametrlar", param, param > 0),
        ]
        print(f"  {'tekshiruv':<18} {'qiymat':>18} {'holat':>6}")
        for nom, q, ok in tekshiruvlar:
            print(f"  {nom:<18} {str(q):>18} {'OK' if ok else 'XATO':>6}")
        print(f"  kutilgan boshlang'ich loss ln(4) = {math.log(4):.3f}")

        print("\n=== 6. Bitta batch testi ===")
        model.train()
        opt = torch.optim.Adam(model.parameters(), lr=1e-2)
        xk, yk = xb[:32], yb[:32]
        for _ in range(150):
            opt.zero_grad()
            loss = F.cross_entropy(model(xk), yk)
            loss.backward()
            opt.step()
        print(f"  32 namuna, 150 qadam: loss {loss.item():.4f}")
        print(f"  natija: {'OTDI' if loss.item() < 0.05 else 'OTMADI'}")
        print("  ⭐ Quvur tayyor - o'rgatishga o'tish mumkin")
    finally:
        shutil.rmtree(papka, ignore_errors=True)


if __name__ == "__main__":
    main()

Natijaning muhim qismi:

text
=== 1. Ma'lumot: papkaga yozish va qayta o'qish ===
  papkalar: ['dog', 'nuqsonsiz', 'tirnalish', 'yoriq']
  fayllar: 1400, namuna: nuqsonsiz/00003.png
  xotiradagi tensor: (1400, 1, 24, 24), [0.00, 1.00]
    nuqsonsiz    770  (55.0%)
    tirnalish    280  (20.0%)
    dog'         213  (15.2%)
    yoriq        137  (9.8%)

=== 2. Bo'lish: 60/20/20, stratifikatsiya, test QULF ===
  kesishma yo'q: True
  o'quv          840  yoriq ulushi 0.098
  validatsiya    280  yoriq ulushi 0.100
  test           280  yoriq ulushi 0.096

=== 3. mean/std FAQAT o'quvdan ===
  o'quv: mean 0.4985, std 0.0926
  bitta namuna: (1, 24, 24), torch.float32, yorliq 0
  normallangan o'quv: o'rtacha +0.000, std 1.000

=== 4. Augmentatsiya: yorliqni saqlaydimi? ===
  8 marta o'qildi: 8 tasi asl rasmdan farq qiladi
  piksellar to'plami o'zgarmadi (faqat joyi): True
  flip va 90 gradusli burish nuqson turini o'zgartirmaydi

=== 5. Tez tekshiruvlar ===
  tekshiruv                      qiymat  holat
  kirish shakli         (64, 1, 24, 24)     OK
  chiqish shakli                (64, 4)     OK
  yorliqlar                [0, 1, 2, 3]     OK
  boshlang'ich loss               1.429     OK
  parametrlar                     30492     OK
  kutilgan boshlang'ich loss ln(4) = 1.386

=== 6. Bitta batch testi ===
  32 namuna, 150 qadam: loss 0.0002
  natija: OTDI
  ⭐ Quvur tayyor - o'rgatishga o'tish mumkin

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

Misol 2 — Trainer, eng yaxshi holat va seedlar

python
"""2-qadam: Trainer, eng yaxshi holat (deepcopy) va bir necha seed."""

import copy
import random
from dataclasses import dataclass, replace

import numpy as np
import torch
import torch.nn as nn
import torch.nn.functional as F
from torch.utils.data import DataLoader, Dataset

SINFLAR = ["nuqsonsiz", "tirnalish", "dog'", "yoriq"]
ULUSHLAR = [0.55, 0.20, 0.15, 0.10]          # nomutanosib
R = 24


@dataclass(frozen=True)
class Konfig:
    seed: int = 42
    n: int = 1400
    lr: float = 3e-3
    batch: int = 64
    davrlar: int = 10
    kanal: int = 8
    weight_decay: float = 1e-4
    sabr: int = 4


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


def yarat(n, seed):
    """Sintetik sanoat nuqsonlari: tekstura + tirnalish / dog' / yoriq.
    Qaytaradi: rasmlar (n, R, R) [0, 1], yorliqlar (n,), niqoblar (n, R, R)."""
    rng = np.random.default_rng(seed)
    yy, xx = np.mgrid[0:R, 0:R].astype(float)
    y = rng.choice(4, size=n, p=ULUSHLAR)
    rasmlar = np.zeros((n, R, R))
    niqoblar = np.zeros((n, R, R), dtype=bool)
    for i in range(n):
        th, f = rng.uniform(0, np.pi), rng.uniform(0.15, 0.3)
        fz = rng.uniform(0, 6.3)
        img = 0.5 + 0.08 * np.sin(2 * np.pi * f * (xx * np.cos(th)
                                                    + yy * np.sin(th)) + fz)
        img += rng.normal(0, 0.07, (R, R))
        kontrast = rng.uniform(0.1, 0.25)
        m = np.zeros((R, R), dtype=bool)
        if y[i] == 1:                           # tirnalish: to'g'ri, yorug'
            y0, x0 = rng.uniform(6, R - 6, 2)
            a = rng.uniform(0, np.pi)
            for t in np.linspace(-8, 8, 60):
                py = int(round(y0 + t * np.sin(a)))
                px = int(round(x0 + t * np.cos(a)))
                if 0 <= py < R and 0 <= px < R:
                    m[py, px] = True
            img[m] += kontrast
        elif y[i] == 2:                                  # dog': dumaloq, qorong'i
            cy, cx = rng.uniform(5, R - 5, 2)
            r = rng.uniform(1.8, 3.5)
            g = np.exp(-((yy - cy) ** 2 + (xx - cx) ** 2) / (2 * r ** 2))
            img -= kontrast * g
            m = g > 0.4
        elif y[i] == 3:                                  # yoriq: siniq, qorong'i
            py, px = rng.uniform(6, R - 6, 2)
            a = rng.uniform(0, 2 * np.pi)
            for _ in range(16):
                a += rng.normal(0, 0.7)
                py, px = py + np.sin(a), px + np.cos(a)
                if 0 <= int(py) < R and 0 <= int(px) < R:
                    m[int(py), int(px)] = True
            img[m] -= kontrast
        rasmlar[i] = np.clip(img, 0, 1)
        niqoblar[i] = m
    return rasmlar, y, niqoblar


def bolish(y, ulush, seed):
    """Stratifikatsiyalangan bo'lish: har sinfdan bir xil ulush."""
    rng = np.random.default_rng(seed)
    a, b = [], []
    for k in np.unique(y):
        idx = rng.permutation(np.flatnonzero(y == k))
        m = int(round(ulush * len(idx)))
        b.extend(idx[:m])
        a.extend(idx[m:])
    return np.sort(a), np.sort(b)


class PapkaDataset(Dataset):
    """Xotiradagi rasmlar; mean/std tashqaridan (o'quvdan) beriladi."""

    def __init__(self, x, y, mean, std, augment=False, seed=0):
        self.x, self.y = x, torch.as_tensor(y)
        self.mean, self.std, self.augment = mean, std, augment
        self.g = torch.Generator().manual_seed(seed)

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

    def __getitem__(self, i):
        x = self.x[i]
        if self.augment:                       # nuqson yo'nalishsiz
            if torch.rand(1, generator=self.g) < 0.5:
                x = x.flip(-1)
            if torch.rand(1, generator=self.g) < 0.5:
                x = x.flip(-2)
            k = int(torch.randint(0, 4, (1,), generator=self.g))
            x = torch.rot90(x, k, (-2, -1))
        return (x - self.mean) / self.std, self.y[i]


class ResBlok(nn.Module):
    def __init__(self, k):
        super().__init__()
        self.c1 = nn.Conv2d(k, k, 3, padding=1, bias=False)
        self.b1 = nn.BatchNorm2d(k)
        self.c2 = nn.Conv2d(k, k, 3, padding=1, bias=False)
        self.b2 = nn.BatchNorm2d(k)

    def forward(self, x):
        h = F.relu(self.b1(self.c1(x)))
        return F.relu(x + self.b2(self.c2(h)))


def pastga(k_in, k_out):
    return nn.Sequential(nn.Conv2d(k_in, k_out, 3, stride=2, padding=1,
                                   bias=False),
                         nn.BatchNorm2d(k_out), nn.ReLU())


class NuqsonCNN(nn.Module):
    """Kichik ResNet: 24 -> 12 -> 6, GAP, 4 sinf."""

    def __init__(self, k=16):
        super().__init__()
        self.tana = nn.Sequential(
            nn.Conv2d(1, k, 3, padding=1, bias=False), nn.BatchNorm2d(k),
            nn.ReLU(), ResBlok(k), pastga(k, 2 * k), ResBlok(2 * k),
            pastga(2 * k, 4 * k), ResBlok(4 * k))
        self.bosh = nn.Linear(4 * k, len(SINFLAR))

    def forward(self, x):
        return self.bosh(self.tana(x).mean((2, 3)))


class MLP(nn.Module):
    """Bazaviy: piksellar tekislanadi, fazoviy tuzilma e'tiborsiz."""

    def __init__(self, yashirin=128):
        super().__init__()
        self.tarmoq = nn.Sequential(
            nn.Flatten(), nn.Linear(R * R, yashirin), nn.ReLU(),
            nn.Linear(yashirin, len(SINFLAR)))

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


def macro_recall(y, p):
    return float(np.mean([(p[y == k] == k).mean()
                          for k in range(len(SINFLAR))]))


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, sched, vazn, callbacklar):
        self.model, self.opt, self.sched, self.vazn = model, opt, sched, vazn
        self.callbacklar, self.tarix, self.toxtash = callbacklar, [], False

    def _davr(self, dl, orgatish):
        self.model.train(orgatish)
        jami, n, plar, ylar = 0.0, 0, [], []
        with torch.set_grad_enabled(orgatish):
            for x, y in dl:
                ch = self.model(x)
                loss = F.cross_entropy(ch, y, weight=self.vazn)
                if orgatish:
                    self.opt.zero_grad()
                    loss.backward()
                    self.opt.step()
                jami += loss.item() * len(y)
                n += len(y)
                plar.append(ch.argmax(1))
                ylar.append(y)
        y, p = torch.cat(ylar).numpy(), torch.cat(plar).numpy()
        return {"loss": jami / n, "mrecall": macro_recall(y, 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):
            t, v = self._davr(dl_tr, True), self._davr(dl_va, False)
            self.sched.step()
            log = {"train_loss": t["loss"], "val_loss": v["loss"],
                   "val_mrecall": v["mrecall"]}
            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 self.tarix


def sinf_vazni(y):
    soni = np.bincount(y, minlength=len(SINFLAR))
    w = len(y) / (len(SINFLAR) * soni)
    return torch.tensor(w, dtype=torch.float32)


def orgat(model, k, ds_tr, ds_va, y_tr):
    seed_everything(k.seed)
    opt = torch.optim.AdamW(model.parameters(), lr=k.lr,
                            weight_decay=k.weight_decay)
    sched = torch.optim.lr_scheduler.CosineAnnealingLR(opt, k.davrlar)
    g = torch.Generator().manual_seed(k.seed)
    dl_tr = DataLoader(ds_tr, batch_size=k.batch, shuffle=True, generator=g)
    dl_va = DataLoader(ds_va, batch_size=256)
    eyx = EngYaxshisi("val_mrecall", k.sabr)
    tarix = Trainer(model, opt, sched, sinf_vazni(y_tr), [eyx]).fit(
        dl_tr, dl_va, k.davrlar)
    return tarix, eyx


def tayyorla(k):
    """1-misoldagi quvur (papka bosqichisiz - vaqtni tejash uchun)."""
    rasmlar, yorliq, _ = yarat(k.n, seed=7)
    x = torch.tensor(rasmlar, dtype=torch.float32)[:, None]
    ish, te = bolish(yorliq, 0.2, k.seed)
    tr_i, va_i = bolish(yorliq[ish], 0.25, k.seed)
    tr, va = ish[tr_i], ish[va_i]
    mean, std = x[tr].mean().item(), x[tr].std().item()
    return x, yorliq, tr, va, mean, std


def main() -> None:
    k = Konfig()
    x, yorliq, tr, va, mean, std = tayyorla(k)
    ds_va = PapkaDataset(x[va], yorliq[va], mean, std)

    def yurish(seed):
        kk = replace(k, seed=seed)
        seed_everything(seed)
        model = NuqsonCNN(kk.kanal)
        ds_tr = PapkaDataset(x[tr], yorliq[tr], mean, std, augment=True,
                             seed=seed)
        tarix, eyx = orgat(model, kk, ds_tr, ds_va, yorliq[tr])
        return model, tarix, eyx

    print("=== 1. Sinf vaznlari (o'quvdan) ===")
    w = sinf_vazni(yorliq[tr])
    print(f"  {dict(zip(SINFLAR, [round(v, 2) for v in w.tolist()]))}")
    print("  kam uchraydigan sinf (yoriq) xatosi qimmatroq")

    print("\n=== 2. Bitta yurish (seed 0) ===")
    model, tarix, eyx = yurish(0)
    print(f"  {len(tarix)} davr, eng yaxshi val macro-recall {eyx.eng:.4f} "
          f"({eyx.davr}-davr)")
    print(f"  {'davr':>5} {'train_loss':>11} {'val_loss':>9} "
          f"{'val_mrecall':>12}")
    for q in tarix:
        if q["davr"] in (1, 3, eyx.davr, len(tarix)):
            print(f"  {q['davr']:>5} {q['train_loss']:>11.4f} "
                  f"{q['val_loss']:>9.4f} {q['val_mrecall']:>12.4f}")

    print("\n=== 3. Tiklangan holat tekshiruvi ===")
    model.eval()
    with torch.no_grad():
        p = model((x[va] - mean) / std).argmax(1).numpy()
    tiklangan = macro_recall(yorliq[va], p)
    print(f"  tiklangan model val macro-recall: {tiklangan:.4f}")
    print(f"  callback eslagan:                 {eyx.eng:.4f}")
    print(f"  mos: {abs(tiklangan - eyx.eng) < 1e-9}")
    oxirgi = tarix[-1]["val_mrecall"]
    if eyx.davr < len(tarix):
        print(f"  oxirgi davr {oxirgi:.4f} edi - deepcopy siz shu "
              "qolardi")

    print("\n=== 4. Uch seed ===")
    ballar = [eyx.eng]
    for s in (1, 2):
        ballar.append(yurish(s)[2].eng)
    ballar = np.array(ballar)
    print(f"  seedlar 0, 1, 2: {ballar.round(4).tolist()}")
    print(f"  o'rtacha {ballar.mean():.4f} +- {ballar.std(ddof=1):.4f}")
    print("  ⭐ Hisobotga o'rtacha va std yoziladi, eng yaxshi seed emas")


if __name__ == "__main__":
    main()

Natijaning muhim qismi:

text
=== 1. Sinf vaznlari (o'quvdan) ===
  {'nuqsonsiz': 0.45, 'tirnalish': 1.25, "dog'": 1.64, 'yoriq': 2.56}
  kam uchraydigan sinf (yoriq) xatosi qimmatroq

=== 2. Bitta yurish (seed 0) ===
  10 davr, eng yaxshi val macro-recall 0.8711 (8-davr)
   davr  train_loss  val_loss  val_mrecall
      1      0.9413    0.8911       0.6262
      3      0.5543    1.5838       0.4424
      8      0.3336    0.3944       0.8711
     10      0.2999    0.3431       0.8600

=== 3. Tiklangan holat tekshiruvi ===
  tiklangan model val macro-recall: 0.8711
  callback eslagan:                 0.8711
  mos: True
  oxirgi davr 0.8600 edi - deepcopy siz shu qolardi

=== 4. Uch seed ===
  seedlar 0, 1, 2: [0.8711, 0.8727, 0.8938]
  o'rtacha 0.8792 +- 0.0127
  ⭐ Hisobotga o'rtacha va std yoziladi, eng yaxshi seed emas

Nima ko'rsatdi: 2.3-bo'lim.

Misol 3 — MLP va ko'pchilik bazaviysi bilan juftlashgan taqqoslash

python
"""3-qadam: bir xil foldlarda CNN va MLP - juftlashgan taqqoslash va qaror."""

import copy
import random
from dataclasses import dataclass

import numpy as np
import torch
import torch.nn as nn
import torch.nn.functional as F
from torch.utils.data import DataLoader, Dataset

SINFLAR = ["nuqsonsiz", "tirnalish", "dog'", "yoriq"]
ULUSHLAR = [0.55, 0.20, 0.15, 0.10]          # nomutanosib
R = 24


@dataclass(frozen=True)
class Konfig:
    seed: int = 42
    n: int = 1400
    lr: float = 3e-3
    batch: int = 64
    davrlar: int = 10
    kanal: int = 8
    weight_decay: float = 1e-4
    sabr: int = 4


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


def yarat(n, seed):
    """Sintetik sanoat nuqsonlari: tekstura + tirnalish / dog' / yoriq.
    Qaytaradi: rasmlar (n, R, R) [0, 1], yorliqlar (n,), niqoblar (n, R, R)."""
    rng = np.random.default_rng(seed)
    yy, xx = np.mgrid[0:R, 0:R].astype(float)
    y = rng.choice(4, size=n, p=ULUSHLAR)
    rasmlar = np.zeros((n, R, R))
    niqoblar = np.zeros((n, R, R), dtype=bool)
    for i in range(n):
        th, f = rng.uniform(0, np.pi), rng.uniform(0.15, 0.3)
        fz = rng.uniform(0, 6.3)
        img = 0.5 + 0.08 * np.sin(2 * np.pi * f * (xx * np.cos(th)
                                                    + yy * np.sin(th)) + fz)
        img += rng.normal(0, 0.07, (R, R))
        kontrast = rng.uniform(0.1, 0.25)
        m = np.zeros((R, R), dtype=bool)
        if y[i] == 1:                           # tirnalish: to'g'ri, yorug'
            y0, x0 = rng.uniform(6, R - 6, 2)
            a = rng.uniform(0, np.pi)
            for t in np.linspace(-8, 8, 60):
                py = int(round(y0 + t * np.sin(a)))
                px = int(round(x0 + t * np.cos(a)))
                if 0 <= py < R and 0 <= px < R:
                    m[py, px] = True
            img[m] += kontrast
        elif y[i] == 2:                                  # dog': dumaloq, qorong'i
            cy, cx = rng.uniform(5, R - 5, 2)
            r = rng.uniform(1.8, 3.5)
            g = np.exp(-((yy - cy) ** 2 + (xx - cx) ** 2) / (2 * r ** 2))
            img -= kontrast * g
            m = g > 0.4
        elif y[i] == 3:                                  # yoriq: siniq, qorong'i
            py, px = rng.uniform(6, R - 6, 2)
            a = rng.uniform(0, 2 * np.pi)
            for _ in range(16):
                a += rng.normal(0, 0.7)
                py, px = py + np.sin(a), px + np.cos(a)
                if 0 <= int(py) < R and 0 <= int(px) < R:
                    m[int(py), int(px)] = True
            img[m] -= kontrast
        rasmlar[i] = np.clip(img, 0, 1)
        niqoblar[i] = m
    return rasmlar, y, niqoblar


def bolish(y, ulush, seed):
    """Stratifikatsiyalangan bo'lish: har sinfdan bir xil ulush."""
    rng = np.random.default_rng(seed)
    a, b = [], []
    for k in np.unique(y):
        idx = rng.permutation(np.flatnonzero(y == k))
        m = int(round(ulush * len(idx)))
        b.extend(idx[:m])
        a.extend(idx[m:])
    return np.sort(a), np.sort(b)


class PapkaDataset(Dataset):
    """Xotiradagi rasmlar; mean/std tashqaridan (o'quvdan) beriladi."""

    def __init__(self, x, y, mean, std, augment=False, seed=0):
        self.x, self.y = x, torch.as_tensor(y)
        self.mean, self.std, self.augment = mean, std, augment
        self.g = torch.Generator().manual_seed(seed)

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

    def __getitem__(self, i):
        x = self.x[i]
        if self.augment:                       # nuqson yo'nalishsiz
            if torch.rand(1, generator=self.g) < 0.5:
                x = x.flip(-1)
            if torch.rand(1, generator=self.g) < 0.5:
                x = x.flip(-2)
            k = int(torch.randint(0, 4, (1,), generator=self.g))
            x = torch.rot90(x, k, (-2, -1))
        return (x - self.mean) / self.std, self.y[i]


class ResBlok(nn.Module):
    def __init__(self, k):
        super().__init__()
        self.c1 = nn.Conv2d(k, k, 3, padding=1, bias=False)
        self.b1 = nn.BatchNorm2d(k)
        self.c2 = nn.Conv2d(k, k, 3, padding=1, bias=False)
        self.b2 = nn.BatchNorm2d(k)

    def forward(self, x):
        h = F.relu(self.b1(self.c1(x)))
        return F.relu(x + self.b2(self.c2(h)))


def pastga(k_in, k_out):
    return nn.Sequential(nn.Conv2d(k_in, k_out, 3, stride=2, padding=1,
                                   bias=False),
                         nn.BatchNorm2d(k_out), nn.ReLU())


class NuqsonCNN(nn.Module):
    """Kichik ResNet: 24 -> 12 -> 6, GAP, 4 sinf."""

    def __init__(self, k=16):
        super().__init__()
        self.tana = nn.Sequential(
            nn.Conv2d(1, k, 3, padding=1, bias=False), nn.BatchNorm2d(k),
            nn.ReLU(), ResBlok(k), pastga(k, 2 * k), ResBlok(2 * k),
            pastga(2 * k, 4 * k), ResBlok(4 * k))
        self.bosh = nn.Linear(4 * k, len(SINFLAR))

    def forward(self, x):
        return self.bosh(self.tana(x).mean((2, 3)))


class MLP(nn.Module):
    """Bazaviy: piksellar tekislanadi, fazoviy tuzilma e'tiborsiz."""

    def __init__(self, yashirin=128):
        super().__init__()
        self.tarmoq = nn.Sequential(
            nn.Flatten(), nn.Linear(R * R, yashirin), nn.ReLU(),
            nn.Linear(yashirin, len(SINFLAR)))

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


def macro_recall(y, p):
    return float(np.mean([(p[y == k] == k).mean()
                          for k in range(len(SINFLAR))]))


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, sched, vazn, callbacklar):
        self.model, self.opt, self.sched, self.vazn = model, opt, sched, vazn
        self.callbacklar, self.tarix, self.toxtash = callbacklar, [], False

    def _davr(self, dl, orgatish):
        self.model.train(orgatish)
        jami, n, plar, ylar = 0.0, 0, [], []
        with torch.set_grad_enabled(orgatish):
            for x, y in dl:
                ch = self.model(x)
                loss = F.cross_entropy(ch, y, weight=self.vazn)
                if orgatish:
                    self.opt.zero_grad()
                    loss.backward()
                    self.opt.step()
                jami += loss.item() * len(y)
                n += len(y)
                plar.append(ch.argmax(1))
                ylar.append(y)
        y, p = torch.cat(ylar).numpy(), torch.cat(plar).numpy()
        return {"loss": jami / n, "mrecall": macro_recall(y, 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):
            t, v = self._davr(dl_tr, True), self._davr(dl_va, False)
            self.sched.step()
            log = {"train_loss": t["loss"], "val_loss": v["loss"],
                   "val_mrecall": v["mrecall"]}
            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 self.tarix


def sinf_vazni(y):
    soni = np.bincount(y, minlength=len(SINFLAR))
    w = len(y) / (len(SINFLAR) * soni)
    return torch.tensor(w, dtype=torch.float32)


def orgat(model, k, ds_tr, ds_va, y_tr):
    seed_everything(k.seed)
    opt = torch.optim.AdamW(model.parameters(), lr=k.lr,
                            weight_decay=k.weight_decay)
    sched = torch.optim.lr_scheduler.CosineAnnealingLR(opt, k.davrlar)
    g = torch.Generator().manual_seed(k.seed)
    dl_tr = DataLoader(ds_tr, batch_size=k.batch, shuffle=True, generator=g)
    dl_va = DataLoader(ds_va, batch_size=256)
    eyx = EngYaxshisi("val_mrecall", k.sabr)
    tarix = Trainer(model, opt, sched, sinf_vazni(y_tr), [eyx]).fit(
        dl_tr, dl_va, k.davrlar)
    return tarix, eyx


def stratified_foldlar(y, n_fold, seed):
    rng = np.random.default_rng(seed)
    fold = np.zeros(len(y), dtype=int)
    for k in np.unique(y):
        idx = rng.permutation(np.flatnonzero(y == k))
        fold[idx] = np.arange(len(idx)) % n_fold
    return fold


def fold_bahosi(yasovchi, k, x, y, tr, va):
    """Fold o'quvida mean/std, belgilangan davrlar (erta to'xtatishsiz)."""
    mean, std = x[tr].mean().item(), x[tr].std().item()
    seed_everything(k.seed)
    model = yasovchi()
    ds_tr = PapkaDataset(x[tr], y[tr], mean, std, augment=True, seed=k.seed)
    ds_va = PapkaDataset(x[va], y[va], mean, std)
    opt = torch.optim.AdamW(model.parameters(), lr=k.lr,
                            weight_decay=k.weight_decay)
    sched = torch.optim.lr_scheduler.CosineAnnealingLR(opt, k.davrlar)
    g = torch.Generator().manual_seed(k.seed)
    dl_tr = DataLoader(ds_tr, batch_size=k.batch, shuffle=True, generator=g)
    dl_va = DataLoader(ds_va, batch_size=256)
    tarix = Trainer(model, opt, sched, sinf_vazni(y[tr]), []).fit(
        dl_tr, dl_va, k.davrlar)
    return tarix[-1]["val_mrecall"]


def main() -> None:
    k = Konfig()
    rasmlar, yorliq, _ = yarat(k.n, seed=7)
    x = torch.tensor(rasmlar, dtype=torch.float32)[:, None]
    ish, _ = bolish(yorliq, 0.2, k.seed)          # test QULF - tegilmaydi
    x_ish, y_ish = x[ish], yorliq[ish]
    fold = stratified_foldlar(y_ish, 3, seed=0)

    print("=== 1. Uch model, bir xil 3 ta fold ===")
    modellar = {
        "Ko'pchilik": None,                        # doim "nuqsonsiz"
        "MLP": lambda: MLP(128),
        "CNN": lambda: NuqsonCNN(k.kanal),
    }
    natija = {m: [] for m in modellar}
    for f in range(3):
        tr, va = np.flatnonzero(fold != f), np.flatnonzero(fold == f)
        natija["Ko'pchilik"].append(
            macro_recall(y_ish[va], np.zeros(len(va), dtype=int)))
        for m in ("MLP", "CNN"):
            natija[m].append(fold_bahosi(modellar[m], k, x_ish, y_ish,
                                         tr, va))
    print(f"  {'fold':>5}" + "".join(f"{m:>12}" for m in natija))
    for f in range(3):
        print(f"  {f + 1:>5}" + "".join(f"{natija[m][f]:>12.4f}"
                                        for m in natija))

    print("\n=== 2. O'rtachalar (macro-recall) ===")
    for m, b in natija.items():
        b = np.array(b)
        print(f"  {m:<11} {b.mean():.4f} +- {b.std(ddof=1):.4f}")

    print("\n=== 3. Juftlashgan farq ===")
    d = np.array(natija["CNN"]) - np.array(natija["MLP"])
    se = d.std(ddof=1) / np.sqrt(len(d))
    print(f"  CNN - MLP: foldlar {d.round(4).tolist()}")
    print(f"  o'rtacha {d.mean():+.4f}, SE {se:.4f}, 2*SE {2 * se:.4f}")
    print(f"  sezilarli: {abs(d.mean()) > 2 * se}")

    print("\n=== 4. Qaror: eng yaxshisidan sezilarli yomon bo'lmagan")
    print("    ENG SODDA model ===")
    oddiydan = ["Ko'pchilik", "MLP", "CNN"]
    eng = max(natija, key=lambda m: np.mean(natija[m]))
    print(f"  o'rtacha bo'yicha eng yaxshi: {eng}")
    for m in oddiydan:
        if m == eng:
            tanlov, sabab = m, "eng yaxshisining o'zi"
            break
        dd = np.array(natija[eng]) - np.array(natija[m])
        se_m = dd.std(ddof=1) / np.sqrt(len(dd))
        print(f"  {m:<11}: {eng} dan {dd.mean():+.4f} past, "
              f"2*SE = {2 * se_m:.4f}")
        if dd.mean() <= 2 * se_m:
            tanlov, sabab = m, f"{eng} dan sezilarli yomon emas"
            break
    print(f"  TANLOV: {tanlov} ({sabab})")
    print("  ⭐ Qaror foldlardan, test hali ham yopiq")


if __name__ == "__main__":
    main()

Natijaning muhim qismi:

text
=== 1. Uch model, bir xil 3 ta fold ===
   fold  Ko'pchilik         MLP         CNN
      1      0.2500      0.6830      0.9238
      2      0.2500      0.6575      0.9075
      3      0.2500      0.7066      0.8555

=== 2. O'rtachalar (macro-recall) ===
  Ko'pchilik  0.2500 +- 0.0000
  MLP         0.6824 +- 0.0245
  CNN         0.8956 +- 0.0357

=== 3. Juftlashgan farq ===
  CNN - MLP: foldlar [0.2408, 0.25, 0.1489]
  o'rtacha +0.2132, SE 0.0323, 2*SE 0.0646
  sezilarli: True

=== 4. Qaror: eng yaxshisidan sezilarli yomon bo'lmagan
    ENG SODDA model ===
  o'rtacha bo'yicha eng yaxshi: CNN
  Ko'pchilik : CNN dan +0.6456 past, 2*SE = 0.0412
  MLP        : CNN dan +0.2132 past, 2*SE = 0.0646
  TANLOV: CNN (eng yaxshisining o'zi)
  ⭐ Qaror foldlardan, test hali ham yopiq

Nima ko'rsatdi: 2.4-bo'lim.

Misol 4 — Test, Grad-CAM, topshirish paketi va hisobot

python
"""4-qadam: test bir marta, sinf bo'yicha recall, Grad-CAM, paket va hisobot."""

import copy
import random
import shutil
import sys
import tempfile
from dataclasses import asdict, dataclass
from pathlib import Path

import numpy as np
import torch
import torch.nn as nn
import torch.nn.functional as F
from PIL import Image
from torch.utils.data import DataLoader, Dataset

FORMAT = 1

SINFLAR = ["nuqsonsiz", "tirnalish", "dog'", "yoriq"]
ULUSHLAR = [0.55, 0.20, 0.15, 0.10]          # nomutanosib
R = 24


@dataclass(frozen=True)
class Konfig:
    seed: int = 42
    n: int = 1400
    lr: float = 3e-3
    batch: int = 64
    davrlar: int = 10
    kanal: int = 8
    weight_decay: float = 1e-4
    sabr: int = 4


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


def yarat(n, seed):
    """Sintetik sanoat nuqsonlari: tekstura + tirnalish / dog' / yoriq.
    Qaytaradi: rasmlar (n, R, R) [0, 1], yorliqlar (n,), niqoblar (n, R, R)."""
    rng = np.random.default_rng(seed)
    yy, xx = np.mgrid[0:R, 0:R].astype(float)
    y = rng.choice(4, size=n, p=ULUSHLAR)
    rasmlar = np.zeros((n, R, R))
    niqoblar = np.zeros((n, R, R), dtype=bool)
    for i in range(n):
        th, f = rng.uniform(0, np.pi), rng.uniform(0.15, 0.3)
        fz = rng.uniform(0, 6.3)
        img = 0.5 + 0.08 * np.sin(2 * np.pi * f * (xx * np.cos(th)
                                                    + yy * np.sin(th)) + fz)
        img += rng.normal(0, 0.07, (R, R))
        kontrast = rng.uniform(0.1, 0.25)
        m = np.zeros((R, R), dtype=bool)
        if y[i] == 1:                           # tirnalish: to'g'ri, yorug'
            y0, x0 = rng.uniform(6, R - 6, 2)
            a = rng.uniform(0, np.pi)
            for t in np.linspace(-8, 8, 60):
                py = int(round(y0 + t * np.sin(a)))
                px = int(round(x0 + t * np.cos(a)))
                if 0 <= py < R and 0 <= px < R:
                    m[py, px] = True
            img[m] += kontrast
        elif y[i] == 2:                                  # dog': dumaloq, qorong'i
            cy, cx = rng.uniform(5, R - 5, 2)
            r = rng.uniform(1.8, 3.5)
            g = np.exp(-((yy - cy) ** 2 + (xx - cx) ** 2) / (2 * r ** 2))
            img -= kontrast * g
            m = g > 0.4
        elif y[i] == 3:                                  # yoriq: siniq, qorong'i
            py, px = rng.uniform(6, R - 6, 2)
            a = rng.uniform(0, 2 * np.pi)
            for _ in range(16):
                a += rng.normal(0, 0.7)
                py, px = py + np.sin(a), px + np.cos(a)
                if 0 <= int(py) < R and 0 <= int(px) < R:
                    m[int(py), int(px)] = True
            img[m] -= kontrast
        rasmlar[i] = np.clip(img, 0, 1)
        niqoblar[i] = m
    return rasmlar, y, niqoblar


def bolish(y, ulush, seed):
    """Stratifikatsiyalangan bo'lish: har sinfdan bir xil ulush."""
    rng = np.random.default_rng(seed)
    a, b = [], []
    for k in np.unique(y):
        idx = rng.permutation(np.flatnonzero(y == k))
        m = int(round(ulush * len(idx)))
        b.extend(idx[:m])
        a.extend(idx[m:])
    return np.sort(a), np.sort(b)


class PapkaDataset(Dataset):
    """Xotiradagi rasmlar; mean/std tashqaridan (o'quvdan) beriladi."""

    def __init__(self, x, y, mean, std, augment=False, seed=0):
        self.x, self.y = x, torch.as_tensor(y)
        self.mean, self.std, self.augment = mean, std, augment
        self.g = torch.Generator().manual_seed(seed)

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

    def __getitem__(self, i):
        x = self.x[i]
        if self.augment:                       # nuqson yo'nalishsiz
            if torch.rand(1, generator=self.g) < 0.5:
                x = x.flip(-1)
            if torch.rand(1, generator=self.g) < 0.5:
                x = x.flip(-2)
            k = int(torch.randint(0, 4, (1,), generator=self.g))
            x = torch.rot90(x, k, (-2, -1))
        return (x - self.mean) / self.std, self.y[i]


class ResBlok(nn.Module):
    def __init__(self, k):
        super().__init__()
        self.c1 = nn.Conv2d(k, k, 3, padding=1, bias=False)
        self.b1 = nn.BatchNorm2d(k)
        self.c2 = nn.Conv2d(k, k, 3, padding=1, bias=False)
        self.b2 = nn.BatchNorm2d(k)

    def forward(self, x):
        h = F.relu(self.b1(self.c1(x)))
        return F.relu(x + self.b2(self.c2(h)))


def pastga(k_in, k_out):
    return nn.Sequential(nn.Conv2d(k_in, k_out, 3, stride=2, padding=1,
                                   bias=False),
                         nn.BatchNorm2d(k_out), nn.ReLU())


class NuqsonCNN(nn.Module):
    """Kichik ResNet: 24 -> 12 -> 6, GAP, 4 sinf."""

    def __init__(self, k=16):
        super().__init__()
        self.tana = nn.Sequential(
            nn.Conv2d(1, k, 3, padding=1, bias=False), nn.BatchNorm2d(k),
            nn.ReLU(), ResBlok(k), pastga(k, 2 * k), ResBlok(2 * k),
            pastga(2 * k, 4 * k), ResBlok(4 * k))
        self.bosh = nn.Linear(4 * k, len(SINFLAR))

    def forward(self, x):
        return self.bosh(self.tana(x).mean((2, 3)))


class MLP(nn.Module):
    """Bazaviy: piksellar tekislanadi, fazoviy tuzilma e'tiborsiz."""

    def __init__(self, yashirin=128):
        super().__init__()
        self.tarmoq = nn.Sequential(
            nn.Flatten(), nn.Linear(R * R, yashirin), nn.ReLU(),
            nn.Linear(yashirin, len(SINFLAR)))

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


def macro_recall(y, p):
    return float(np.mean([(p[y == k] == k).mean()
                          for k in range(len(SINFLAR))]))


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, sched, vazn, callbacklar):
        self.model, self.opt, self.sched, self.vazn = model, opt, sched, vazn
        self.callbacklar, self.tarix, self.toxtash = callbacklar, [], False

    def _davr(self, dl, orgatish):
        self.model.train(orgatish)
        jami, n, plar, ylar = 0.0, 0, [], []
        with torch.set_grad_enabled(orgatish):
            for x, y in dl:
                ch = self.model(x)
                loss = F.cross_entropy(ch, y, weight=self.vazn)
                if orgatish:
                    self.opt.zero_grad()
                    loss.backward()
                    self.opt.step()
                jami += loss.item() * len(y)
                n += len(y)
                plar.append(ch.argmax(1))
                ylar.append(y)
        y, p = torch.cat(ylar).numpy(), torch.cat(plar).numpy()
        return {"loss": jami / n, "mrecall": macro_recall(y, 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):
            t, v = self._davr(dl_tr, True), self._davr(dl_va, False)
            self.sched.step()
            log = {"train_loss": t["loss"], "val_loss": v["loss"],
                   "val_mrecall": v["mrecall"]}
            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 self.tarix


def sinf_vazni(y):
    soni = np.bincount(y, minlength=len(SINFLAR))
    w = len(y) / (len(SINFLAR) * soni)
    return torch.tensor(w, dtype=torch.float32)


def orgat(model, k, ds_tr, ds_va, y_tr):
    seed_everything(k.seed)
    opt = torch.optim.AdamW(model.parameters(), lr=k.lr,
                            weight_decay=k.weight_decay)
    sched = torch.optim.lr_scheduler.CosineAnnealingLR(opt, k.davrlar)
    g = torch.Generator().manual_seed(k.seed)
    dl_tr = DataLoader(ds_tr, batch_size=k.batch, shuffle=True, generator=g)
    dl_va = DataLoader(ds_va, batch_size=256)
    eyx = EngYaxshisi("val_mrecall", k.sabr)
    tarix = Trainer(model, opt, sched, sinf_vazni(y_tr), [eyx]).fit(
        dl_tr, dl_va, k.davrlar)
    return tarix, eyx


def grad_cam(model, xn):
    """Oxirgi blok xaritasi bo'yicha Grad-CAM, (N, R, R) ga kattalashtirilgan."""
    model.eval()
    xarita = model.tana(xn)
    xarita.retain_grad()
    logit = model.bosh(xarita.mean((2, 3)))
    sinf = logit.argmax(1)
    logit[torch.arange(len(xn)), sinf].sum().backward()
    w = xarita.grad.mean((2, 3), keepdim=True)
    cam = F.relu((w * xarita).sum(1, keepdim=True)).detach()
    cam = F.interpolate(cam, size=(R, R), mode="bilinear",
                        align_corners=False)[:, 0]
    return cam, sinf


class Bashoratchi:
    def __init__(self, yol):
        p = torch.load(yol, weights_only=True)
        if p["format"] != FORMAT:
            raise ValueError("format mos emas")
        self.sinflar = p["sinflar"]
        self.mean, self.std = p["kirish"]["mean"], p["kirish"]["std"]
        self.model = NuqsonCNN(p["konfig"]["kanal"])
        self.model.load_state_dict(p["holat"])
        self.model.eval()
        with torch.no_grad():
            q = self.model((p["nazorat"]["x"] - self.mean) / self.std)
        self.nazorat_ok = bool(torch.allclose(q, p["nazorat"]["logit"],
                                              atol=1e-5))

    def bashorat(self, fayllar):
        x = torch.stack([torch.tensor(np.asarray(Image.open(f),
                                                 dtype=np.float32) / 255.0)
                         for f in fayllar])[:, None]
        with torch.no_grad():
            p = torch.softmax(self.model((x - self.mean) / self.std), 1)
        return [(self.sinflar[int(i)], float(v))
                for v, i in zip(*p.max(1))]


def main() -> None:
    k = Konfig()
    rasmlar, yorliq, niqoblar = yarat(k.n, seed=7)
    x = torch.tensor(rasmlar, dtype=torch.float32)[:, None]
    ish, te = bolish(yorliq, 0.2, k.seed)
    tr_i, va_i = bolish(yorliq[ish], 0.25, k.seed)
    tr, va = ish[tr_i], ish[va_i]
    mean, std = x[tr].mean().item(), x[tr].std().item()

    print("=== 1. Yakuniy model (3-qadam qarori: CNN) ===")
    seed_everything(k.seed)
    model = NuqsonCNN(k.kanal)
    ds_tr = PapkaDataset(x[tr], yorliq[tr], mean, std, augment=True,
                         seed=k.seed)
    ds_va = PapkaDataset(x[va], yorliq[va], mean, std)
    tarix, eyx = orgat(model, k, ds_tr, ds_va, yorliq[tr])
    print(f"  val macro-recall {eyx.eng:.4f} ({eyx.davr}-davr, "
          f"{len(tarix)} davr)")

    print("\n=== 2. Testni BIR MARTA ochamiz ===")
    model.eval()
    xn_te = (x[te] - mean) / std
    with torch.no_grad():
        p = model(xn_te).argmax(1).numpy()
    y_te = yorliq[te]
    mr = macro_recall(y_te, p)
    print(f"  test macro-recall {mr:.4f}, aniqlik {(p == y_te).mean():.4f}")
    print(f"  {'sinf':<10} {'n':>4} {'recall':>7}   chalkashlik qatori")
    recall = {}
    for kk, nom in enumerate(SINFLAR):
        m = y_te == kk
        recall[nom] = float((p[m] == kk).mean())
        qator = np.bincount(p[m], minlength=4).tolist()
        print(f"  {nom:<10} {int(m.sum()):>4} {recall[nom]:>7.3f}   {qator}")
    nuqsonli = y_te > 0
    otkazildi = float((p[nuqsonli] == 0).mean())
    yolgon = float((p[~nuqsonli] > 0).mean())
    print(f"  nuqsonni o'tkazib yuborish (nuqsonli -> nuqsonsiz): "
          f"{otkazildi:.3f}")
    print(f"  yolg'on signal (nuqsonsiz -> nuqsonli):          {yolgon:.3f}")
    eng_yomon = min(recall, key=recall.get)
    print(f"  eng past recall: {eng_yomon} ({recall[eng_yomon]:.3f})")

    print("\n=== 3. Grad-CAM: model nuqsonga qarayaptimi? ===")
    togri = np.flatnonzero((p == y_te) & (y_te > 0))
    cam, _ = grad_cam(model, xn_te[togri].clone().requires_grad_(False))
    niqob = torch.tensor(niqoblar[te][togri], dtype=torch.float32)
    kengroq = F.max_pool2d(niqob[:, None], 3, 1, 1)[:, 0]   # +1 piksel
    energiya = (cam * kengroq).sum((1, 2)) / cam.sum((1, 2)).clamp(min=1e-8)
    yuza = kengroq.mean((1, 2))
    print(f"  {'sinf':<10} {'n':>4} {'CAM niqobda':>12} {'niqob yuzasi':>13}")
    for kk in (1, 2, 3):
        m = torch.tensor(y_te[togri] == kk)
        print(f"  {SINFLAR[kk]:<10} {int(m.sum()):>4} "
              f"{energiya[m].mean().item():>12.3f} "
              f"{yuza[m].mean().item():>13.3f}")
    nisbat = energiya.mean().item() / yuza.mean().item()
    print(f"  CAM energiyasi niqob yuzasidan {nisbat:.1f} barobar zich")
    if nisbat > 2:
        print("  model asosan nuqson piksellariga qarayapti - OK")
    else:
        print("  DIQQAT: model nuqsondan boshqa joyga qarayapti")

    papka = Path(tempfile.mkdtemp(prefix="nuqson_paket_"))
    try:
        print("\n=== 4. Topshirish paketi ===")
        nazorat_x = x[te[:8]]
        with torch.no_grad():
            nazorat_logit = model((nazorat_x - mean) / std)
        paket = {
            "format": FORMAT,
            "konfig": asdict(k),
            "holat": model.state_dict(),
            "kirish": {"mean": mean, "std": std, "R": R, "kanal": 1},
            "sinflar": list(SINFLAR),
            "metrika": {"val_mrecall": round(float(eyx.eng), 4),
                        "test_mrecall": round(mr, 4),
                        "test_recall": {s: round(v, 4)
                                        for s, v in recall.items()}},
            "versiyalar": {"torch": str(torch.__version__),
                           "python": sys.version.split()[0]},
            "nazorat": {"x": nazorat_x, "logit": nazorat_logit},
        }
        yol = papka / "nuqson_model.pt"
        torch.save(paket, yol)
        print(f"  kalitlar: {sorted(paket)}")
        print(f"  hajm: {yol.stat().st_size / 1024:.1f} KB")

        print("\n=== 5. Bashoratchi (weights_only=True) ===")
        b = Bashoratchi(yol)
        print(f"  nazorat tekshiruvi: {'OK' if b.nazorat_ok else 'XATO'}")
        yangi = papka / "yangi"
        yangi.mkdir()
        fayllar = []
        for j in range(4):
            i = te[np.flatnonzero(y_te == j)[0]]
            f = yangi / f"rasm_{j}.png"
            Image.fromarray((rasmlar[i] * 255).round().astype(np.uint8)).save(f)
            fayllar.append(f)
        for f, (sinf, ehtimol), j in zip(fayllar, b.bashorat(fayllar),
                                         range(4)):
            print(f"  {f.relative_to(papka).as_posix():<16} haqiqiy "
                  f"{SINFLAR[j]:<10} -> {sinf:<10} ({ehtimol:.3f})")

        print("\n=== 6. Yakuniy hisobot ===")
        print("  VAZIFA: 24x24 sirt rasmida nuqson turini aniqlash (4 sinf)")
        print("  METRIKA: macro-recall (sinflar teng vaznda)")
        print("  DIZAYN: stratifikatsiya 60/20/20; mean/std o'quvdan; test "
              "bir marta")
        print(f"  TEST: macro-recall {mr:.4f}; nuqsonni o'tkazib yuborish "
              f"{otkazildi:.3f}")
        print(f"  ZAIF JOY: {eng_yomon} recall {recall[eng_yomon]:.3f}")
        print(f"  TEKSHIRUV: Grad-CAM nuqsonda {nisbat:.1f}x zich")
        print(f"  PAKET: format {FORMAT}, weights_only=True, nazorat "
              f"{'OK' if b.nazorat_ok else 'XATO'}")
        print("  CHEKLOVLAR: sintetik ma'lumot; bitta tekstura turi;")
        print("    real kamerada yorug'lik va fokus o'zgarishi tekshirilmagan")
        print("  ⭐ 22-qism yakunlandi: rasmdan topshiriladigan modelgacha")
    finally:
        shutil.rmtree(papka, ignore_errors=True)


if __name__ == "__main__":
    main()

Natijaning muhim qismi:

text
=== 1. Yakuniy model (3-qadam qarori: CNN) ===
  val macro-recall 0.8718 (9-davr, 10 davr)

=== 2. Testni BIR MARTA ochamiz ===
  test macro-recall 0.9003, aniqlik 0.9250
  sinf          n  recall   chalkashlik qatori
  nuqsonsiz   154   0.942   [145, 3, 0, 6]
  tirnalish    56   0.929   [4, 52, 0, 0]
  dog'         43   0.953   [0, 0, 41, 2]
  yoriq        27   0.778   [6, 0, 0, 21]
  nuqsonni o'tkazib yuborish (nuqsonli -> nuqsonsiz): 0.079
  yolg'on signal (nuqsonsiz -> nuqsonli):          0.058
  eng past recall: yoriq 0.778-bob

=== 3. Grad-CAM: model nuqsonga qarayaptimi? ===
  sinf          n  CAM niqobda  niqob yuzasi
  tirnalish    52        0.245         0.117
  dog'         41        0.363         0.134
  yoriq        21        0.222         0.096
  CAM energiyasi niqob yuzasidan 2.4 barobar zich
  model asosan nuqson piksellariga qarayapti - OK

=== 4. Topshirish paketi ===
  kalitlar: ['format', 'holat', 'kirish', 'konfig', 'metrika', 'nazorat', 'sinflar', 'versiyalar']
  hajm: 157.0 KB

=== 5. Bashoratchi (weights_only=True) ===
  nazorat tekshiruvi: OK
  yangi/rasm_0.png haqiqiy nuqsonsiz  -> nuqsonsiz  0.867-bob
  yangi/rasm_1.png haqiqiy tirnalish  -> tirnalish  0.995-bob
  yangi/rasm_2.png haqiqiy dog'       -> dog'       0.659-bob
  yangi/rasm_3.png haqiqiy yoriq      -> nuqsonsiz  0.649-bob

=== 6. Yakuniy hisobot ===
  VAZIFA: 24x24 sirt rasmida nuqson turini aniqlash (4 sinf)
  METRIKA: macro-recall (sinflar teng vaznda)
  DIZAYN: stratifikatsiya 60/20/20; mean/std o'quvdan; test bir marta
  TEST: macro-recall 0.9003; nuqsonni o'tkazib yuborish 0.079
  ZAIF JOY: yoriq recall 0.778
  TEKSHIRUV: Grad-CAM nuqsonda 2.4x zich
  PAKET: format 1, weights_only=True, nazorat OK
  CHEKLOVLAR: sintetik ma'lumot; bitta tekstura turi;
    real kamerada yorug'lik va fokus o'zgarishi tekshirilmagan
  ⭐ 22-qism yakunlandi: rasmdan topshiriladigan modelgacha

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
"Aniqlik 92% — model tayyor" Sinf bo'yicha recall ga qarang
"Augmentatsiya qancha ko'p — shuncha yaxshi" Faqat yorliqni saqlaydiganlari
"mean/std ni butun ma'lumotda hisoblash qulay" Faqat o'quvdan, paketga ham shu
"CNN rasmda har doim MLP dan yaxshi" Juftlashgan farq hal qiladi
"Test yaxshi chiqsa — model to'g'ri sababga ko'ra ishlaydi" Grad-CAM bilan tekshiring
"Sinflar tartibini papkadan tiklash mumkin" Paketga ro'yxat yoziladi
"Nuqsonni o'tkazish va yolg'on signal teng" O'tkazib yuborish ko'p marta qimmat
"Bitta seed yetarli" O'rtacha va std

6. Keng tarqalgan xatolar va yechimlari

1. Aniqlik bilan hisobot

python
print((p == y).mean())                               # ⚠️ 55% "hammasi nuqsonsiz"
print(macro_recall(y, p), recall_sinf_boyicha)       # ✅

2. Stratifikatsiyasiz bo'lish

python
te = rng.permutation(n)[:280]                        # ⚠️ yoriqlar soni tasodifiy
ish, te = bolish(y, 0.2, seed)                       # ✅ har sinfdan 20%

3. Validatsiyada augmentatsiya

python
PapkaDataset(x[va], y[va], mean, std, augment=True)  # ⚠️
PapkaDataset(x[va], y[va], mean, std)                # ✅

4. Yorliqni buzadigan augmentatsiya

python
x = x * rng.uniform(0.5, 1.5)                        # ⚠️ tirnalish/yoriq farqi yorqinlikda
x = torch.rot90(x.flip(-1), k, (-2, -1))             # ✅ yo'nalish ahamiyatsiz

5. Sinf vaznisiz nomutanosib o'rgatish

python
F.cross_entropy(logit, y)                            # ⚠️ yoriq "yo'qoladi"
F.cross_entropy(logit, y, weight=sinf_vazni(y_tr))   # ✅

6. Sinflar tartibi paketsiz

python
sinflar = sorted(os.listdir(papka))                  # ⚠️ ['dog', 'nuqsonsiz', ...]
sinflar = paket["sinflar"]                           # ✅ model tartibi

7. Paketda obyekt

python
{"torch": torch.__version__, "papka": Path(...)}     # ⚠️ weights_only rad etadi
{"torch": str(torch.__version__)}                    # ✅

7. Integratsiya — bu bilim qayerda kerak bo'ladi

  • 21.5, 21.7, 21.11, 21.12-darslar (o'tilgan): Trainer, checkpoint, loyiha tuzilishi va birinchi to'liq loyiha skeleti
  • 18-qism (o'tilgan): Juftlashgan taqqoslash, recall va xato narxi
  • 22.1-22.13-darslar (o'tilgan): Butun qism — rasm, CNN, augmentatsiya, Grad-CAM
  • NLP qismida: Xuddi shu skelet matn bilan: token, embedding, ketma-ketlik modellari
  • MLOps qismida: Paketni xizmatga aylantirish, kamera sharoitlari o'zgarishini (drift) kuzatish

8. Eng yaxshi amaliyotlar

  1. Metrikani xato narxidan tanlang — nuqson loyihasida sinf bo'yicha recall.

  2. Stratifikatsiyalangan bo'lish va testni birinchi qadamda qulflang.

  3. mean/std faqat o'quvdan, paketga ham aynan shu qiymatlar.

  4. Augmentatsiyani "yorliqni saqlaydimi?" savoli bilan tanlang.

  5. Sinf vazni yoki boshqa nomutanosiblik choralari.

  6. Eng yaxshi holatni deepcopy qiling va 3 seed bering.

  7. Bazaviyni bir xil foldlarda, juftlashgan farq bilan taqqoslang.

  8. Grad-CAM bilan "to'g'ri sababga ko'ra to'g'rimi" ni tekshiring va zaif joyni hisobotga yozing.


9. Amaliy topshiriq

Vazifa 1: Bashorat qiling

python
1.  # 4 sinfda boshlang'ich loss taxminan?
2.  # test qachon qulflanadi?
3.  # mean/std qaysi bo'lakdan?
4.  # bu loyihada qaysi augmentatsiyalar xavfsiz?
5.  # nega yorqinlik augmentatsiyasi xavfli?
6.  # "hammasi nuqsonsiz" modelining macro-recall i?
7.  # sinf vazni formulasi?
8.  # juftlashgan taqqoslashda sezilarlilik mezoni?
9.  # teng natijada MLP va CNN dan qaysi biri?
10. # hisobotdagi eng muhim qator (ishlab chiqarish uchun)?
11. # Grad-CAM nisbati ~1 bo'lsa nima degani?
12. # sinflar tartibi qayerdan olinadi?
Javoblar
  1. ln(4) = 1.386
  2. Birinchi qadamda, bo'lish paytida
  3. Faqat o'quv bo'lagidan
  4. Akslantirish va 90 gradusga burish
  5. Tirnalish (yorug') va yoriq (qorong'i) farqi yorqinlikda
  6. 0.25
  7. w_k = N / (K * n_k)
  8. |mean(d)| > 2 * SE
  9. MLP (soddaroq)
  10. Nuqsonni o'tkazib yuborish ulushi va yoriq recall i
  11. Model nuqsonga emas, fonga yoki teksturaga qarayapti
  12. Paketdagi "sinflar" ro'yxatidan

Vazifa 2: Xatolarni tuzating

python
1.  mean, std = x.mean(), x.std()

2.  ds_va = PapkaDataset(x[va], y[va], mean, std, augment=True)

3.  loss = F.cross_entropy(logit, y)            # yoriq 10%

4.  sinflar = sorted(os.listdir(papka))         # Bashoratchi ichida

5.  print(f"test aniqligi {aniqlik:.3f}")        # hisobotdagi yagona qator
Javoblar
python
1.  mean, std = x[tr].mean(), x[tr].std()

2.  ds_va = PapkaDataset(x[va], y[va], mean, std)

3.  loss = F.cross_entropy(logit, y, weight=sinf_vazni(y_tr))

4.  sinflar = paket["sinflar"]

5.  print(f"macro-recall {mr:.3f}, yoriq recall {recall['yoriq']:.3f}, "
          f"o'tkazib yuborish {otkazildi:.3f}")

Vazifa 3: Ma'lumot quvuri

Modellang:

  1. Papkaga yozish va qayta o'qish
  2. Stratifikatsiyalangan bo'lish
  3. mean/std va normallash tekshiruvi
  4. Augmentatsiya yorliqni saqlashini tekshirish

Vazifa 4: O'rgatish

Modellang:

  1. Sinf vazni bilan va vaznsiz — yoriq recall i qanday o'zgaradi?
  2. EngYaxshisi va tiklash tekshiruvi
  3. 3 seed
  4. Jurnal yozuvi (konfig + metrika)

Vazifa 5: Taqqoslash

Modellang:

  1. 5 fold bilan qayta taqqoslash
  2. MLP ga yashirin qatlam qo'shing — farq qisqaradimi?
  3. Juftlashgan farq va SE
  4. Qaror qoidasi

Vazifa 6: Topshirish

Modellang:

  1. Test bir marta va sinf bo'yicha recall
  2. "Nuqsonsiz" ehtimolligi 0.9 dan past bo'lsa — qo'lda tekshiruv: o'tkazib yuborish va tekshiruv yuki qanday o'zgaradi?
  3. Grad-CAM nisbati sinf bo'yicha
  4. Paket, Bashoratchi va hisobot

Vazifa 7: O'ylash

Loyihangiz ishga tushdi. Ikki haftadan keyin zavod liniyaga yangi, yorqinroq yoritgich o'rnatdi. Model hisobotlarida hech qanday xato yo'q, lekin nazoratchi "yoriqlar ko'proq o'tib ketyapti" deydi. Nima bo'lgan bo'lishi mumkin, qanday tekshirasiz va nima qilasiz?

Javob

Qisqa javob: bu ma'lumot drifti — kirish rasmlarining taqsimoti o'zgargan, model esa eski taqsimotda o'rgatilgan. Xato bermaydi, faqat sifat jimgina tushadi.

Nima uchun aynan yoriqlar:

1. Normallash qotirilgan. Paketdagi mean = 0.4985 va std = 0.0926 eski yoritgich bo'yicha hisoblangan. Yorqinroq rasmlarda (x - mean) / std natijasi butunlay siljiydi — model hech qachon ko'rmagan qiymatlar oralig'iga tushadi.

2. Yoriq — qorong'i nuqson. Tirnalish va yoriqni ajratadigan asosiy belgi yorqinlik (2.2-bo'lim). Kuchli yorug'likda qorong'i chiziqlar kontrasti pasayadi, yoriqlar fonga "singib" ketadi. Test jadvalida ham yoriq eng zaif sinf edi (0.778) — chegaradagi misollar birinchi bo'lib yo'qoladi.

3. Augmentatsiya bu holatni qamramagan. Biz yorqinlik augmentatsiyasini ataylab ishlatmadik (yorliqni buzishi mumkin edi). Demak model yorqinlik o'zgarishiga chidamli bo'lishni o'rganmagan.

Qanday tekshirasiz:

  • Yangi rasmlarning o'rtacha yorqinligi va std sini paketdagi mean/std bilan solishtiring — bu eng arzon drift signali.
  • Bashoratlar taqsimotini kuzating: "nuqsonsiz" ulushi oshganmi? (Yoriqlar nuqsonsizga o'tsa, aynan shu ko'rinadi.)
  • Yangi yoritgich ostida bir necha o'nta nuqsonli detalni qo'lda yorliqlab, sinf bo'yicha recall ni qayta hisoblang.
  • Grad-CAM: yangi rasmlarda model hali ham nuqsonga qarayaptimi?

Nima qilasiz:

  • Qisqa muddat: "nuqsonsiz" ehtimolligi chegarasini ko'tarib, shubhali detallarni qo'lda tekshiruvga yuborish — o'tkazib yuborish qimmat.
  • O'rta muddat: yangi yoritgich ostida ma'lumot yig'ib, yorliqlash va qayta o'rgatish; normallashni har rasm bo'yicha (per-image) qilishni ko'rib chiqish; yorliqni buzmaydigan yumshoq yorqinlik augmentatsiyasini sinab ko'rish.
  • Uzoq muddat: kirish statistikasi va bashorat taqsimotini doimiy kuzatish (MLOps qismida), liniyadagi har qanday apparat o'zgarishi haqida jamoaga xabar berish jarayoni.

Muhim nuans: "model hisobotlarida xato yo'q" — drift ning eng xavfli xususiyati. Paketdagi nazorat namunasi faqat model o'zgarmaganini tasdiqlaydi, dunyo o'zgarmaganini emas. Shuning uchun hisobotdagi "CHEKLOVLAR: real kamerada yorug'lik va fokus o'zgarishi tekshirilmagan" qatori rasmiyatchilik emas, aynan shu vaziyat haqidagi ogohlantirish edi.

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


Xulosa

Bu darsda to'liq kompyuter ko'rish loyihasini qurdik: papkadan paketgacha.

Eng muhim uch fikr:

  1. Loyiha skeleti 21.12 niki, rasmga xos qo'shimchalar esa augmentatsiya va vizual tekshiruv. 1-misolda 1400 ta PNG papkalardan o'qildi, stratifikatsiya har bo'lakda yoriq ulushini 0.096-0.100 da ushladi, mean/std faqat o'quvdan olindi va augmentatsiya piksellar to'plamini o'zgartirmasligi tekshirildi. Tez tekshiruvlar va bitta batch testi o'rgatishdan oldin quvurni tasdiqladi.

  2. Qaror juftlashgan farq bilan, test esa xatolar tarkibini ko'rsatish uchun. Uch seedda CNN val macro-recall i 0.8792 +- 0.0127 bo'ldi. Bir xil foldlarda CNN MLP dan +0.2132 ga yaxshi chiqdi (2*SE = 0.0646), shuning uchun qoida CNN ni tanladi. Test bir marta ochildi: macro-recall 0.9003, lekin yoriq recall i atigi 0.778 va nuqsonli detallarning 0.079 qismi o'tkazib yuborildi — hisobotdagi eng muhim raqam aynan shu.

  3. Model to'g'ri sababga ko'ra to'g'ri, paket esa o'zini tekshiradi. Grad-CAM energiyasi nuqson niqobida uning yuzasidan 2.4 barobar zich chiqdi. Paket sinflar ro'yxati, mean/std, satr versiya va xom nazorat namunasi bilan weights_only=True orqali to'liq yuklandi; Bashoratchi esa PNG fayllardan to'g'ridan-to'g'ri bashorat qildi — va zaif joyni ham halol ko'rsatdi: yoriqli namuna "nuqsonsiz" deb tasniflandi.

Bu bilan 22-qism — Kompyuter ko'rish yakunlandi. Endi bizda rasm bilan ishlashning to'liq zanjiri bor: piksellardan konvolyutsiyagacha, klassifikatsiyadan obyekt aniqlash va segmentatsiyagacha, o'rgatishdan tekshiriladigan va topshiriladigan paketgacha. Keyingi qism — NLP: matn bilan ishlash, tokenlash, so'z embeddinglari va ketma-ketlik modellari. Ko'rasizki, bu yerda qurgan skelet u yerda ham deyarli o'zgarmay ishlaydi.

Ulashish:Telegram'da

Izohlar (0)

Izoh yozish uchun kiring.

  • Hozircha izoh yo'q. Birinchi bo'ling!
22.14-dars: Amaliyot — sanoat nuqsonlarini aniqlash loyihasi — IlmHamroh