Table of Contents Tablica sadržaja Uvod: Oznake su skupe, slike su besplatne Deo 1 – istraživanje podataka: Razumijevanje podataka s kojima radite Deo 2 – Predobrada: Govorimo jezikom modela Deo 3 — Ekstrakcija značajki: Pretvaranje slika u značajne brojeve Četvrti deo – Nezaštićeno skupljanje: Otkrivanje strukture u mraku Peti deo – Semi-nadzorni trening: Osnovni eksperiment Deo 6 – Širenje na milijune slika: realističan putnički plan Zaključak Oznake su skupe, slike su besplatne U savršenom svetu, svaka slika u vašem skupu podataka imala bi oznaku. „Defektivna.“ „Normalna.“ „Crack tip A.“ „Scratch tip B.“ Ali u stvarnom svetu, označivanje je brutalno skupo. Jedan medicinski radiolog može označiti možda 50 skeniranja mozga na sat – za 200 €/h. Industrijski inspektor kvaliteta može anotirati možda 100 delova na sat. Na skali, označivanje 100.000 slika može koštati više nego trening samog modela. Ovde je paradoks: kompanije često imaju milione slika (s fotoaparata, senzora, korisničkih učitavanja), ali mogu priuštiti samo da označe sitnu frakciju. Umesto odbacivanja neoznačenih slika, koristimo ih za poboljšanje modela – kombinirajući mali sastav s oznakama s velikim sastavom bez oznakama. semi-supervised learning Dozvolite mi da koristim analogiju. Zamislite da ste učitelj sa 30 studenata. Dajte im sve ispit, ali samo imate vremena da ocjenjujete 5 radova. Pažljivo ocjenjujete tih 5 radova i primetite obrasce: učenici koji su pisali mnogo imaju tendenciju da dobiju visoke ocjene, a učenici koji su ostavili prazne odgovore imaju tendenciju da dobiju niske ocene. Koristeći te obrasce, možete procijeniti ocjene ostalih 25 radova bez čitanja svake linije. To je polu-nadzirano učenje: koristite nekoliko radova s ocjenama (data s oznakama) da biste razumeli obrasce, a zatim primenite te obrasce na one bez ocjene (data bez oznakama). Ovaj članak gradi kompletnu polu-nadzorovanu klasifikaciju slika od nule.Mi ćemo raditi kroz svaki korak sa detaljnim objašnjenjima kako idemo. Studija slučaja: otkrivanje defekata u proizvodnji na metalnim površinama. Tvornica proizvodi čelične ploče, a kamere fotografiraju svaku ploču dok se izvlači iz proizvodne linije. Većina ploča je normalna, neke imaju nedostatke (grebanje, pukotine, pitting, inkluzije). Imamo 10.000 slika, ali samo 200 označenih. Studija slučaja: otkrivanje defekata u proizvodnji na metalnim površinama. Tvornica proizvodi čelične ploče, a kamere fotografiraju svaku ploču dok se izvlači iz proizvodne linije. Većina ploča je normalna, neke imaju nedostatke (grebanje, pukotine, pitting, inkluzije). Imamo 10.000 slika, ali samo 200 označenih. Izgradnja polu-nadzorovanog učenja Pipeline Prije nego što uronite u bilo koji kod, hajde da mapiramo čitavu cevovodu. Razumijevanje protoka prvo će učiniti da se svaki korak osjeća namerno, a ne slučajno. Uzmite trenutak da proučite ovaj diagram - svako polje je korak koji ćemo provesti: THE COMPLETE SEMI-SUPERVISED PIPELINE: [10,000 raw images] ──▶ [Exploration & Cleaning] │ ▼ [Preprocessing: resize, normalize, histogram equalization] │ ▼ [Feature Extraction: pretrained ResNet50 → 2048-dim embedding per image] │ ┌─────────┴──────────┐ │ │ ▼ ▼ [200 LABELED images] [9,800 UNLABELED images] │ │ │ ▼ │ [Clustering: K-Means, DBSCAN │ on embeddings → pseudo-labels] │ │ │ ▼ │ [WEAKLY labeled dataset] │ (cluster assignments) │ │ ▼ ▼ ┌────────────────────────────────────┐ │ SEMI-SUPERVISED TRAINING: │ │ 1. Pre-train CNN on weakly labeled │ │ 2. Fine-tune CNN on strongly labeled│ │ 3. Compare vs supervised-only │ └────────────────────────────────────┘ │ ▼ [Evaluation: F1, AUC-ROC, confusion matrix, comparison] Ključni uvid: pretvoriti ćemo neoznačene slike u označene slike (preko grupiranja), a zatim koristite to približno znanje da biste dali konačnom modelu početak. mislite o tome kao da dajete učeniku grubo vodič za učenje prije stvarnog ispita - to neće biti savršeno, ali je bolje nego ništa. Otprilike Budimo eksplicitni i o rječniku koji ćemo koristiti kroz ovaj članak, jer je miješanje pojmova vrlo čest izvor zbunjenosti: Snažno označene (ili jednostavno „označene“): slike s oznakama koje je provjerio ljudski stručnjak. Slabo označene (ili "pseudo-označene"): slike čije su oznake pretpostavljene grupiranjem. Jeftinije, ali bučnije. Bez oznake: slike bez oznake uopće. Ovo je njihovo stanje prije nego što ih grupiramo. Ugradnja: kompaktni numerički sažetak slike, proizveden od strane pretrenirane neuronske mreže. Još jedno čitanje: Još jedno čitanje: Pregled polu-nadzorovanog učenja - scikit-learn Google istraživanje o polu-nadzorovanom učenju Pregled polu-nadzorovanog učenja - scikit-learn Google istraživanje o polu-nadzorovanom učenju Pretraga podataka: razumevanje podataka s kojima radite Zašto morate pogledati svoje podatke prije nego što učinite bilo šta drugo Seti podataka o slikama imaju jedinstvene načine neuspjeha koje nećete naći u tabularnim podacima: oštećene datoteke koje se sruše u vašem treningskom krugu u 3 sata ujutro, nedosljedne rezolucije koje tiho iskrivljuju vaše slike, pogrešni kanali boja (siva skala prikrivena kao RGB) i ekstremna neravnoteža klase u kojoj je 95% slika "normalno". Zlatno pravilo : Hajde da vidimo s čim se bavimo. never trust data you haven't inspected. 1.1 – Ukladištenje i brojanje slika Naš skup podataka je organizovan u dve glavne fascikle: jedna sa označenim slikama (podijeljen u „normalne“ i „defektne“ podfascikle) i jedna sa neoznačenim slikama (bez podfascikli – samo ravna zbirka za fajlove) .png import os import numpy as np import pandas as pd import matplotlib.pyplot as plt from PIL import Image from pathlib import Path data_dir = Path("data/metal_surfaces") labeled_dir = data_dir / "labeled" unlabeled_dir = data_dir / "unlabeled" Mi koristimo umesto konkateniranja niza jer pravilno rukuje separatorima staza na bilo kojem operativnom sistemu. pathlib.Path labeled_files = list(labeled_dir.glob("**/*.png")) unlabeled_files = list(unlabeled_dir.glob("**/*.png")) print(f"Labeled images: {len(labeled_files)}") print(f"Unlabeled images: {len(unlabeled_files)}") print(f"Total: {len(labeled_files) + len(unlabeled_files)}") label_ratio = len(labeled_files) / (len(labeled_files) + len(unlabeled_files)) print(f"Label ratio: {label_ratio:.1%}") Taj omjer oznaka je obično oko 2%. Samo 2% naših podataka ima stručno provjerene oznake. Ostalih 98% je rudnik zlata koji ne možemo priuštiti da ignorišemo - i to je upravo ono što polu-nadzorno učenje iskorištava. 1.2 — Skeniranje za probleme: rezolucija, boja, korupcija Zatim, moramo provjeriti svaku sliku pojedinačno. Jedan oštećena datoteka može srušiti čitavu obuku. Nedosljedne rezolucije će iskriviti slike ako niste pažljivi. A pogrešni načini boja (sive veličine kada vaš model očekuje RGB) će proizvesti gluposti. Pišemo malu funkciju koja pokušava otvoriti svaku sliku i zabilježiti njena svojstva: def get_image_info(filepath): """ Try to open an image and record its properties. If the file is corrupted, Pillow will throw an exception. """ try: img = Image.open(filepath) return { "path": str(filepath), "width": img.size[0], "height": img.size[1], "mode": img.mode, # 'RGB', 'L' (grayscale), 'RGBA' "filesize_kb": os.path.getsize(filepath) / 1024, "corrupted": False, } except Exception: return { "path": str(filepath), "width": None, "height": None, "mode": None, "filesize_kb": None, "corrupted": True, } Naši Polje je posebno važno. znači 3 kanala u boji (crvena, zelena, plava). znači grayscale (1 kanal) znači RGB sa alfa (transparentni) kanal. Naš predtrenirani model očekuje RGB, tako da ćemo morati pretvoriti sve kasnije. img.mode 'RGB' 'L' 'RGBA' Sada ćemo skenirati svih 10.000 slika: all_files = labeled_files + unlabeled_files image_info = [get_image_info(f) for f in all_files] info_df = pd.DataFrame(image_info) I pogledajte rezultate: print(f"Total images scanned: {len(info_df)}") print(f"Corrupted images: {info_df['corrupted'].sum()}") print(f"\nResolution distribution:") print(info_df[~info_df["corrupted"]][["width", "height"]].describe().round(0)) print(f"\nColor modes: {info_df['mode'].value_counts().to_dict()}") print(f"File size (KB): min={info_df['filesize_kb'].min():.0f}, " f"max={info_df['filesize_kb'].max():.0f}, " f"mean={info_df['filesize_kb'].mean():.0f}") Šta tražiti u izlazu: Korumpirane slike > 0: Uklonite ih odmah. Čak i jedna loša datoteka može srušiti vaš trening. Različite rezolucije: Ako je min ≠ max za širinu ili visinu, slike imaju različite veličine. Mnogobrojni načini boja: Ako vidite i 'RGB' i 'L', imate mešavinu boja i sive veličine. Ekstremne veličine datoteke: 1KB datoteka je verovatno prazna ili oštećena. 50MB datoteka može biti nekomprimirana – vrijedi istražiti. Hajde da očistimo oštećene datoteke: corrupted_paths = set(info_df[info_df["corrupted"]]["path"].tolist()) if corrupted_paths: print(f"Removing {len(corrupted_paths)} corrupted images") labeled_files = [f for f in labeled_files if str(f) not in corrupted_paths] unlabeled_files = [f for f in unlabeled_files if str(f) not in corrupted_paths] 1.3 — Razredna distribucija: Je li naš označeni set uravnotežen? U industrijskim i medicinskim uslovima, nedostatci su rijetki. Vaš označeni set može biti 90% "normalan" i 10% "defektan". Ovo je od ogromne važnosti: lenji model koji uvek predviđa "normalan" bi dobio 90% tačnost dok je potpuno beskoristan. Moramo unaprijed znati ravnotežu kako bismo je kasnije mogli nadoknaditi. class_counts = {} for class_dir in labeled_dir.iterdir(): if class_dir.is_dir(): count = len(list(class_dir.glob("*.png"))) class_counts[class_dir.name] = count print("Class distribution (labeled set):") for cls, count in class_counts.items(): pct = count / sum(class_counts.values()) * 100 print(f" {cls}: {count} images ({pct:.1f}%)") Ako je neravnoteža ozbiljna, kasnije ćemo se nositi s njom koristeći tehniku nazvanu u funkciji gubitka - u suštini govoreći modelu "izostanak greške je 4x gore od lažne alarme". pos_weight Vizualizirajmo i to: fig, ax = plt.subplots(figsize=(6, 4)) ax.bar(class_counts.keys(), class_counts.values(), color=["#2ecc71", "#e74c3c"]) ax.set_title("Class Distribution (Labeled Images)") ax.set_ylabel("Number of images") plt.tight_layout() plt.savefig("outputs/class_distribution.png", dpi=150) plt.show() 1.4 — Vizualizacija uzoraka slika: uvek pogledajte ispred modela Možda ćete otkriti slike koje su očigledno pogrešno označene, skeniranje artefakata (crne granice, rotacije), probleme kvalitete (zamrljavanje, prekomjerno izlaganje), ili da su defekti vizualno suptilni i vaš zadatak je teži nego što ste mislili. fig, axes = plt.subplots(2, 5, figsize=(15, 6)) fig.suptitle("Sample Images — Top: Normal | Bottom: Defect", fontsize=14) for i, class_name in enumerate(["normal", "defect"]): class_files = list((labeled_dir / class_name).glob("*.png"))[:5] for j, filepath in enumerate(class_files): img = Image.open(filepath) axes[i, j].imshow(img, cmap="gray" if img.mode == "L" else None) axes[i, j].set_title(class_name, fontsize=10) axes[i, j].axis("off") plt.tight_layout() plt.savefig("outputs/sample_images.png", dpi=150) plt.show() Odvojite trenutak da proučite ove slike. Vidite nedostatke vlastitim očima? Ako ne možete, model će se boriti. Ako su nedostatci očigledni (duboka ogrebotina, velika pukotina), to je ohrabrujuće - model bi trebao biti u mogućnosti da nauči razliku. Ako su suptilni (mala promjena boje, pukotina linije kose), potrebna vam je posebno dobra predobrada i ekstrakcija funkcija. ti Još jedno čitanje: Još jedno čitanje: Pillow dokumentacija – Python knjižnica slika koje koristimo za učitavanje slika NEU Surface Defect Database – set podataka o greškama površine čelika u stvarnom svijetu s kojim možete vežbati Pillow dokumentacija Surface Defect baza podataka Deo 2 – Predobrada: govoreći jezik modela Zašto ne možemo samo hraniti sirove slike u neuronskoj mreži Pretrenirani CNN kao što je ResNet50 bio je obučavan na vrlo specifičnom tipu ulaza: 224×224 piksela slike, u RGB boji, normalizovane sa specifičnim prosječnim i standardnim vrednostima odstupanja izračunate iz ImageNet podatkovnog skupa. Razmislite o tome kao jezik. ResNet50 "govori ImageNet." Ako želimo da razume naše slike metalne površine, moramo ih prvo "prevesti" u format ImageNet. Konverzija na RGB (3 kanala) Povećanje kontrasta pomoću histogramske izjednačavanja Veličina: 224×224 Normalizirajte vrednosti piksela kako biste odgovarali ImageNet statistici 2.1 - Šta je histogram izjednačavanja, i zašto je važno ovdje? Razlika između normalne površine i ogrebotine može biti samo nekoliko nivoa intenziteta piksela – nevidljive golim okom, a vrlo teško za model da detektira. redistribuira intenzitet piksela tako da se pun raspon (0 do 255) koristi ravnomjerno. Rezultat: suptilne karakteristike "izlaze" vizualno i numerički. Histogram equalization Koristimo napredniju verziju nazvanu (Contrast Limited Adaptive Histogram Equalization). Za razliku od globalne izjednačavanja (koji primjenjuje istu transformaciju na celu sliku), CLAHE dijeli sliku na male pločice (8×8 podrazumevano) i izjednačuje svaku pločicu nezavisno. CLAHE Ovo je analogija: zamislite da prilagođavate svetlost na fotografiji. Globalna izjednačavanje je kao da koristite jedan slajd za svetlost za celu sliku - možete osvetliti tamne kutove, ali isprati već svetli centar. CLAHE je kao da prilagođavate svetlost u svakom regionu samostalno, tako da svaki deo slike postane jasan. 2.2 — Izgradnja prilagođenog PyTorch dataset PyTorch organizuje učitavanje podataka oko dva razreda: (zna kako da učita jednu temu) i (zna kako da partije i shuffle predmete). PyTorch pruža ugrađeni Sastav podataka, ali pretpostavlja da svaka slika ima oznaku. Naš skup podataka ima i označene I neoznačene slike, pa nam je potrebna prilagođena klasa. Dataset DataLoader ImageFolder Hajde da ga izgradimo korak po korak.Prvo, kostur: import torch import torchvision.transforms as T from torch.utils.data import Dataset, DataLoader import cv2 class MetalSurfaceDataset(Dataset): """ Custom Dataset that handles both labeled and unlabeled images. Returns -1 as the label for unlabeled images. """ def __init__(self, image_paths, labels=None, transform=None): self.image_paths = image_paths self.labels = labels # None for unlabeled images self.transform = transform def __len__(self): return len(self.image_paths) kaže PyTorch koliko slika imamo. pohranjuje putove slike i (opcionalno) njihove oznake. __len__ __init__ Sada je osnovna metoda, , koji učitava i prethodno obrađuje jednu sliku. Mi ćemo je podijeliti u tri faze: __getitem__ Stage 1 — Load the image and force RGB: def __getitem__(self, idx): # Load image and convert to RGB # .convert("RGB") handles grayscale → RGB conversion automatically # (it duplicates the single channel into R, G, and B) img = Image.open(self.image_paths[idx]).convert("RGB") Zašto ? Jer ResNet50 očekuje 3 kanala. Ako je naša slika sive veličine (1 kanal), to duplira sive vrijednosti u R, G i B. Ako je već RGB, to ne radi ništa. Ako je RGBA (4 kanala), to pada na alfa kanal. .convert("RGB") Stage 2 — Apply CLAHE histogram equalization: # Convert PIL Image → numpy array for OpenCV processing img_np = np.array(img) # Convert RGB → LAB color space # L = Lightness (brightness), A and B = color channels # We only equalize L (brightness) to avoid distorting colors img_lab = cv2.cvtColor(img_np, cv2.COLOR_RGB2LAB) # Create CLAHE object and apply to L channel # clipLimit=2.0 prevents over-amplification of noise # tileGridSize=(8,8) means 8x8 tiles for local equalization clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8, 8)) img_lab[:, :, 0] = clahe.apply(img_lab[:, :, 0]) # Convert back LAB → RGB → PIL Image img_np = cv2.cvtColor(img_lab, cv2.COLOR_LAB2RGB) img = Image.fromarray(img_np) Zašto LAB i ne samo primeniti CLAHE direktno na RGB? Jer ako izjednačite R, G i B kanale samostalno, izobličite boje – pretvarajući plavu u zelenu, na primjer. Radom u LAB prostoru, mi samo dodirujemo kanal svetlosti (L) i ostavljamo boje (A, B) netaknute. Stage 3 — Apply transforms and return: # Apply resize + normalize transforms if self.transform: img = self.transform(img) # Return the label (or -1 if this image has no label) label = self.labels[idx] if self.labels is not None else -1 return img, label 2.3 — Transformacijski cevovod: šta svaki korak čini Sada definiramo sekvencu transformacija. Svaka od njih ima specifičnu svrhu: preprocessing = T.Compose([ T.Resize((224, 224)), # (1) Resize to model's expected input size T.ToTensor(), # (2) PIL Image → PyTorch Tensor, scale [0,1] T.Normalize( mean=[0.485, 0.456, 0.406], # (3) Normalize with ImageNet statistics std=[0.229, 0.224, 0.225], ), ]) Objasnimo svaki korak: Arhitektura ResNet50 zahtijeva upravo ovu veličinu. Ako hranite sliku od 300×400, oblici tenzora se neće podudarati i PyTorch će se srušiti. (1) Resize to 224×224. Radi dvije stvari: pretvara format piksela iz HWC (visina × širina × kanali) u CHW (kanali × visina × širina), što je ono što PyTorch očekuje; i skala vrednosti piksela od [0, 255] čestih do [0.0, 1.0] pluta. (2) ToTensor. Ovi magični brojevi - i — su prosječno i standardno odstupanje vrednosti piksela u čitavom ImageNet skupu podataka, izračunano kanal po kanalu (R, G, B). ResNet50 je bio obučavan sa ovim točnim normalizacijskim vrijednostima, tako da njegove unutarnje težine "očekuju" ulaze usredotočene oko 0 sa jedinicom varijacije. Upotrebom različitih brojeva bilo bi kao merenje u inčima, ali izračunavanje u centimetrima - matematički prelomi. (3) Normalize with ImageNet mean and std. [0.485, 0.456, 0.406] [0.229, 0.224, 0.225] 2.4 – Kreiranje skupova podataka i DataLoaders Sada smo skupili sve. kritični princip: Mešanje njih bi zagađivalo našu procjenu. labeled and unlabeled data must be kept strictly separate at all times. Prvo, prikupite označene putove slike i njihove oznake klase: labeled_paths = [] labeled_labels = [] for class_idx, class_name in enumerate(["normal", "defect"]): class_dir = labeled_dir / class_name for fp in class_dir.glob("*.png"): labeled_paths.append(str(fp)) labeled_labels.append(class_idx) # 0 = normal, 1 = defect Zatim prikupite neoznačene putove slike (nije potrebna oznaka): unlabeled_paths = [str(fp) for fp in unlabeled_files] Kreirajte PyTorch Dataset objekte: labeled_dataset = MetalSurfaceDataset(labeled_paths, labeled_labels, preprocessing) unlabeled_dataset = MetalSurfaceDataset(unlabeled_paths, labels=None, transform=preprocessing) print(f"Labeled dataset: {len(labeled_dataset)} images") print(f"Unlabeled dataset: {len(unlabeled_dataset)} images") Konačno, zavijte ih u DataLoaders. DataLoader skuplja slike zajedno (batch_size=32 znači 32 slike odjednom) i opcionalno ih shuffles: labeled_loader = DataLoader(labeled_dataset, batch_size=32, shuffle=False) unlabeled_loader = DataLoader(unlabeled_dataset, batch_size=32, shuffle=False) Zašto Zato što smo na putu da izvučemo značajke, a mi trebamo ugrađivanja da ostanu u istom redoslijedu kao i naše liste datoteka. shuffle=False Još jedno čitanje: Još jedno čitanje: torchvision transforms — potpuna lista CLAHE objašnjeno (OpenCV tutorial) PyTorch Dataset & DataLoader aplikacije torchvision transforms — potpuna lista CLAHE objašnjeno (OpenCV tutorial) PyTorch Dataset & DataLoader aplikacije Deo 3 — Ekstrakcija značajki: pretvaranje slika u značajne brojeve 3.1 — Zašto su sirovi pikseli strašna reprezentacija 224×224 RGB slika ima 150,528 brojeva (224 × 224 × 3 kanala). Većina njih su buke – manje varijacije u osvetljenju, senzorskim artefaktima, kompresijskim artefaktima. Još gore: dve fotografije istog crta, uzete iz pomalo različitih uglova ili osvetljenja, imaju potpuno različite vrednosti piksela. Ako pokušamo grupisati ili klasificirati sirove piksele, slike koje izgledaju isto za nas će izgledati potpuno drugačije prema algoritmu. Ono što nam je potrebno je predstavljanje koje obuhvaća od slike – „ovo izgleda kao ogrebotina“, „ovo je glatka površina“ – u kompaktnom, stabilnom numeričkom obliku. Učinite meaning embeddings 3.2 — Šta je predtrenirani model i zašto ne treniramo od nule Obuka duboke neuronske mreže od nule zahtijeva puno podataka – obično stotine hiljada slika. Imamo 200 označenih slika. Ako bismo pokušali obučiti 25 miliona parametara ResNet50 na 200 slika, model bi savršeno zapamtio svaku sliku obuke, ali potpuno ne bi uspio na novim slikama. . overfitting Umjesto toga, koristimo model koji je već osposobljen na ImageNet-u – skup podataka od 14 miliona slika u 1.000 kategorija (psi, mačke, automobili, zgrade, itd.). Ovaj model je već naučio prepoznati osnovne vizualne značajke: rubove, teksture, oblike, gradiente boja, geometrijske obrasce. – primjenjuju se na čelične površine baš kao što se primjenjuju na mačke. universal Razmislite o tome kao zapošljavanje iskusnog fotografa da pregleda vašu tvornicu.Nikada ranije nisu videli čelične ploče, ali već znaju kako : mogu primijetiti neobične teksture, nagle promene u kvaliteti površine, obrasce koji krše normu. vidiš 3.3 Loading ResNet50 Preuzimanje Hajde da preuzmemo predtrenirani model: import torchvision.models as models resnet = models.resnet50(weights=models.ResNet50_Weights.IMAGENET1K_V1) Ova jedna linija preuzima (prvi put) model ResNet50 čije su težine obučene na ImageNet. 3.4 – Zamrzavanje parametara Ne želimo da modificiramo bilo koju od naučene funkcije. Koristimo ResNet kao alat samo za čitanje: for param in resnet.parameters(): param.requires_grad = False To ima dvije prednosti: sprečava slučajnu modifikaciju pretreniranih težina, i čini zaključak bržim (bez gradijentnog praćenja = manje izračuna i manje memorije). requires_grad = False 3.5 – Uklanjanje glave klasifikacije Arhitektura ResNet50 izgleda ovako: Input image (224×224×3) ↓ [Convolutional layers] — learn visual features ↓ [Average Pooling] — compress spatial dimensions → 2048-dim vector ↓ [Fully Connected layer] — classify into 1000 ImageNet categories ↓ Output (1000 probabilities) Mi ne želimo zadnji sloj potpuno povezan, jer je specifičan za ImageNet 1000 klase (pas, mačka, avion...) i beskoristan za naš zadatak. feature_extractor = torch.nn.Sequential(*list(resnet.children())[:-1]) Šta ova linija čini: Vraća sve slojeve ResNet kao listu. uzima sve slojeve osim posljednjeg (FC sloj). Vratite ih u model. resnet.children() [:-1] Sequential(*) feature_extractor.eval() način onemogućava odustajanje i koristi statistiku rada za normalizaciju serije. izlazi – ista slika uvek proizvodi isto ugrađivanje. eval() deterministic device = torch.device("cuda" if torch.cuda.is_available() else "cpu") feature_extractor = feature_extractor.to(device) print(f"Feature extractor ready on {device}") Korištenje GPU-a (ako je dostupno) čini ekstrakciju značajki otprilike 10 puta bržom. 3.6 – Funkcija ekstrakcije Sada hajde da napišemo funkciju koja hrani serije slika kroz ekstraktor funkcija i prikuplja ugrađivanja. Spoljna struktura : def extract_embeddings(dataloader, model, device): """ Feed all images through the model and collect embeddings. Returns: embeddings: numpy array, shape (n_images, 2048) labels: numpy array, shape (n_images,) — -1 if unlabeled """ all_embeddings = [] all_labels = [] Akumuliramo rezultate u listama jer obrađujemo slike u serijama (32 odjednom), a ne sve odjednom (ne bi se uklopilo u GPU memoriju). Glavni ciklus : with torch.no_grad(): for batch_images, batch_labels in dataloader: batch_images = batch_images.to(device) features = model(batch_images) onemogućava praćenje gradijenta – bitno je jer mi samo radimo zaključke, a ne obuku.To samo smanjuje upotrebu memorije za polovinu i ubrzava stvari. premešta slike na GPU ako ih imamo. torch.no_grad() batch_images.to(device) Izlaznost od ima oblik — poslednje dve dimenzije su prostorni ostaci iz prosječnog skupljanja. Moramo ih stisnuti: model(batch_images) (batch_size, 2048, 1, 1) features = features.squeeze(-1).squeeze(-1) # Now shape is (batch_size, 2048) — that's our embedding Konačno, vratimo rezultate na CPU (numpy ne može raditi s GPU tenzorima) i pohranimo ih: all_embeddings.append(features.cpu().numpy()) all_labels.append(batch_labels.numpy()) return np.concatenate(all_embeddings), np.concatenate(all_labels) Uključite sve grupe u jednu skupinu. np.concatenate 3.7 – Pokretanje ekstrakcije print("Extracting embeddings for labeled images...") labeled_embeddings, labeled_labels_arr = extract_embeddings( labeled_loader, feature_extractor, device ) print(f" Shape: {labeled_embeddings.shape}") # Expected: (200, 2048) 200 slika, od kojih je svaka predstavljena 2048-dimenzionalnim vektorom. To je 73x kompresija u odnosu na sirove piksele (150,528 → 2 048) – a komprimirana reprezentacija je više smisleno Dalje print("Extracting embeddings for unlabeled images...") unlabeled_embeddings, _ = extract_embeddings( unlabeled_loader, feature_extractor, device ) print(f" Shape: {unlabeled_embeddings.shape}") # Expected: (9800, 2048) Mi bacamo oznake (oni su svi -1 u svakom slučaju) sa Varijabilni _ 3.8 – Ušteda ugrađenosti (ne ponavljajte svaki put!) Ekstrakcija značajki je najskuplji korak – potencijalno 30+ minuta na GPU-u za 10.000 slika. np.save("data/labeled_embeddings.npy", labeled_embeddings) np.save("data/labeled_labels.npy", labeled_labels_arr) np.save("data/unlabeled_embeddings.npy", unlabeled_embeddings) print("Embeddings saved to disk") Kasnije, možete odmah ponovno učitati sa . np.load("data/labeled_embeddings.npy") 3.9 – Pregled zdravlja: jesu li ugrađivanja razumna? Prije nego što nastavite, brza provjera. smeće u, smeće van – hajde da se pobrinemo da su naši ugrađivači zdravi: print(f"Embedding statistics:") print(f" Mean: {labeled_embeddings.mean():.4f}") print(f" Std: {labeled_embeddings.std():.4f}") print(f" Min: {labeled_embeddings.min():.4f}") print(f" Max: {labeled_embeddings.max():.4f}") print(f" NaN: {np.isnan(labeled_embeddings).any()}") print(f" Inf: {np.isinf(labeled_embeddings).any()}") Što očekivati: znači oko 0.3-0.5, std oko 0.5-1.0, nema NaN, nema Inf. Ako vidite NaN vrednosti, oštećena slika verovatno je prošla kroz korak čišćenja. Idemo dalje: Idemo dalje: Transfer učenje objašnjeno (PyTorch tutorial) ResNet papir (He et al., 2015) – originalna arhitektura koja je uvela skakanje veze Ekstrakcija značajki protiv fine-tuning — Stanford CS231n kurseve bilješke Transfer učenje objašnjeno (PyTorch tutorial) ResNet papir (He et al., 2015) Funkcionalna ekstrakcija vs fine-tuning Deo 4 – Nezaštićeno skupljanje: otkrivanje strukture u mraku 4.1 – Šta čini grupiranje i zašto nam je potrebno Sada imamo 10.000 ugrađivanja – kompaktne numeričke sažetke svake slike. 200 od njih imaju oznake. Ostalih 9.800 ne. u podacima koji se nadaju da odgovaraju "normalnom" i "defektnom". groupings Osnovna pretpostavka: ako su naša ugrađivanja dobra (a ResNet50 ugrađivanja obično jesu), slike istog tipa će biti Normalne površine će se skupljati zajedno; neispravne površine će se skupljati zajedno. Čak i bez oznaka, struktura je tu – skupljanje to otkriva. Blizu Ali prvo, praktičan problem: 2048 dimenzija je nemoguće vizualizirati i usporiti neke algoritme. 4.2 – Standardizovanje ugrađivanja Ako jedna dimenzija varira od 0 do 1000, a druga od 0 do 0,01, prva će potpuno dominirati udaljenosti – kao da druga dimenzija ne postoji. from sklearn.preprocessing import StandardScaler # Combine labeled + unlabeled for joint standardization all_embeddings = np.concatenate([labeled_embeddings, unlabeled_embeddings], axis=0) scaler = StandardScaler() all_embeddings_scaled = scaler.fit_transform(all_embeddings) Zašto standardizovati označene i neoznačene zajedno? Zato što dolaze iz iste distribucije (isto tvornica, ista kamera). # Split back — we'll need them separate later labeled_scaled = all_embeddings_scaled[:len(labeled_embeddings)] unlabeled_scaled = all_embeddings_scaled[len(labeled_embeddings):] print(f"After standardization: mean={all_embeddings_scaled.mean():.4f}, " f"std={all_embeddings_scaled.std():.4f}") 4.3 — Smanjite dimenzije pomoću PCA PCA (Principal Component Analysis) pronalazi smjernice maksimalne varijacije u podacima i projektira u gornjim smjerovima. from sklearn.decomposition import PCA pca = PCA(n_components=50, random_state=42) all_pca = pca.fit_transform(all_embeddings_scaled) print(f"PCA: 2048 → 50 dimensions") print(f"Variance retained: {pca.explained_variance_ratio_.sum():.1%}") ~95% varijanse zadržan znači da smo odbacili samo 5% informacija, ali smanjena dimenzionalnost za 40x. Vizualizirajmo i koliko svaka komponenta doprinosi: plt.figure(figsize=(10, 4)) plt.plot(np.cumsum(pca.explained_variance_ratio_), marker="o", markersize=3) plt.xlabel("Number of PCA components") plt.ylabel("Cumulative explained variance") plt.title("PCA: How many components do we need?") plt.axhline(y=0.95, color="r", linestyle="--", label="95% threshold") plt.legend() plt.grid(True, alpha=0.3) plt.tight_layout() plt.savefig("outputs/pca_variance.png", dpi=150) plt.show() Ova "lobi parcela" pokazuje gde dodavanje više komponenti prestaje da pruža značajne dobitke. Ako se krivina ravna prije 50 komponenti, možete koristiti još manje. 4.4 — Vizualizacija pomoću t-SNE t-SNE je ne-linearna tehnika redukcije dimenzionalnosti koja je posebno dizajnirana za vizualizaciju. : slike koje su blizu u visoko-dimenzionalnom prostoru će biti blizu u 2D parceli.To ga čini savršenim za provjeru da li se normalne i neispravne slike prirodno razdvoje. local structure Jedno važno upozorenje: t-SNE iskrivljuje globalne udaljenosti – prostor između klastera nije bitan. Koristite ga samo za vizualizaciju i klaster na originalnim (ili PCA-smanjenim) ugrađivanjima. never cluster on t-SNE output. from sklearn.manifold import TSNE # Apply t-SNE on PCA output (faster and more stable than on raw 2048-dim) tsne = TSNE(n_components=2, random_state=42, perplexity=30, n_iter=1000) all_tsne = tsne.fit_transform(all_pca) Naši parametar otprilike kontrolira "velikost susjedstva" - koliko bliskih tačaka t-SNE smatra. 30 je razumna podrazumevana za skupove podataka naše veličine. perplexity Sada da podijelimo t-SNE koordinate: labeled_tsne = all_tsne[:len(labeled_embeddings)] unlabeled_tsne = all_tsne[len(labeled_embeddings):] Za vizualizovanje: fig, axes = plt.subplots(1, 2, figsize=(14, 6)) # Left plot: labeled images only, colored by true label for cls_idx, cls_name, color in [(0, "Normal", "#2ecc71"), (1, "Defect", "#e74c3c")]: mask = labeled_labels_arr == cls_idx axes[0].scatter(labeled_tsne[mask, 0], labeled_tsne[mask, 1], label=cls_name, alpha=0.7, s=40, c=color) axes[0].set_title("t-SNE: Labeled Images (True Labels)") axes[0].legend() Ako vidite dva različita oblaka u ovoj parceli - zelena na jednoj strani, crvena na drugoj - to je odličan znak. # Right plot: all images (unlabeled in gray, labeled overlaid) axes[1].scatter(unlabeled_tsne[:, 0], unlabeled_tsne[:, 1], c="lightgray", alpha=0.2, s=10, label="Unlabeled") for cls_idx, cls_name, color in [(0, "Normal", "#2ecc71"), (1, "Defect", "#e74c3c")]: mask = labeled_labels_arr == cls_idx axes[1].scatter(labeled_tsne[mask, 0], labeled_tsne[mask, 1], label=f"Labeled: {cls_name}", alpha=0.8, s=40, c=color) axes[1].set_title("t-SNE: All Images (Labeled in Color)") axes[1].legend() plt.tight_layout() plt.savefig("outputs/tsne_visualization.png", dpi=150) plt.show() U desnoj parceli, sivi oblak (neoznačene slike) treba da se preklapaju sa obojenim točkama.To potvrđuje da označene i neoznačene slike dolaze iz iste distribucije - neophodan uslov za semi-nadzirano učenje da radi. 4.5 – K-means grupiranje K-Means je najjednostavniji i najčešće korišteni algoritam grupiranja. On dijeli podatke u tačno k grupe tako što iterativno dodjeljuje svaku tačku najbližem centru klastera, a zatim ažurira centre. Budući da znamo da imamo 2 klase (normalno i neispravno), počinjemo s k = 2. Ali takođe testiramo k = 3, 4, 5 kako bismo provjerili da li podaci mogu imati više strukture (npr. različite). od nedostataka koji formiraju odvojene grupe). tipovi Da bismo procijenili koliko se klasteri podudaraju sa stvarnim etiketama, koristimo ARI = 1.0 znači savršeno sukladnost sa istinitim oznakama. ARI = 0.0 znači slučajno grupiranje. ARI < 0 znači lošije od slučajnog. ARI (Adjusted Rand Index) from sklearn.cluster import KMeans from sklearn.metrics import adjusted_rand_score, silhouette_score print("K-Means Clustering:") print(f" {'k':<5s} {'ARI':>8s} {'Silhouette':>12s}") print(f" {'-'*27}") for k in [2, 3, 4, 5]: kmeans = KMeans(n_clusters=k, random_state=42, n_init=10) all_clusters = kmeans.fit_predict(all_embeddings_scaled) # ARI: compare clusters vs true labels (on labeled images only) labeled_clusters = all_clusters[:len(labeled_embeddings)] ari = adjusted_rand_score(labeled_labels_arr, labeled_clusters) # Silhouette: internal quality measure (no labels needed) # How well-separated are the clusters? Range: -1 to +1 sil = silhouette_score(all_embeddings_scaled, all_clusters) print(f" {k:<5d} {ari:>8.4f} {sil:>12.4f}") Naši parametar znači K-Means će se pokrenuti 10 puta s različitim slučajnim inicijalizacijama i zadržati najbolji rezultat. n_init=10 Ako k=2 daje najviši ARI, to potvrđuje da naši podaci imaju dve prirodne grupe koje se usklađuju s normalnim vs defektom. 4.6 – DBSCAN grupiranje (alternativni pristup) DBSCAN works very differently from K-Means. Instead of specifying the number of clusters, you specify two parameters: eps (epsilon): maksimalna udaljenost između dvije točke za njih da se smatraju susjedima. Razmislite o tome kao "koliko blizu je dovoljno blizu?" min_samples: minimalni broj točaka koji su potrebni za stvaranje guste regije (grupa). Razmislite o tome kao "koliko je prepun susjedstva potrebno da se računa kao grupa?" DBSCAN automatski određuje broj klastera I identificira outliere (točke koje ne pripadaju bilo kojem klasteru – označene kao -1). from sklearn.cluster import DBSCAN print("\nDBSCAN Clustering:") print(f" {'eps':<6s} {'min_s':<7s} {'clusters':>9s} {'noise':>7s} {'ARI':>8s}") print(f" {'-'*40}") Potrebno je testirati više kombinacija parametara jer „prave“ vrednosti ovise o podacima: for eps in [3.0, 5.0, 7.0, 10.0]: for min_samples in [5, 10, 20]: dbscan = DBSCAN(eps=eps, min_samples=min_samples) db_clusters = dbscan.fit_predict(all_pca) # Use PCA-reduced data n_clusters = len(set(db_clusters)) - (1 if -1 in db_clusters else 0) n_noise = (db_clusters == -1).sum() if n_clusters >= 2: labeled_db = db_clusters[:len(labeled_embeddings)] mask = labeled_db != -1 # Exclude noise points from ARI if mask.sum() > 10: ari = adjusted_rand_score(labeled_labels_arr[mask], labeled_db[mask]) print(f" {eps:<6.1f} {min_samples:<7d} {n_clusters:>9d} " f"{n_noise:>7d} {ari:>8.4f}") Imajte na umu da koristimo PCA-smanjene podatke ( DBSCAN se bori u veoma visokim dimenzijama jer sve udaljenosti postaju slične ( "prokletstvo dimenzionalnosti"). all_pca Usporedite najbolji ARI iz DBSCAN-a s najboljim iz K-Means-a i odaberite pobjednika. 4.7 — Vizualizacija klastera na t-SNE parceli Pogledajmo kako najbolje klasteriranje izgleda na našoj t-SNE vizualizaciji: best_kmeans = KMeans(n_clusters=2, random_state=42, n_init=10) all_cluster_ids = best_kmeans.fit_predict(all_embeddings_scaled) fig, ax = plt.subplots(figsize=(8, 6)) scatter = ax.scatter(all_tsne[:, 0], all_tsne[:, 1], c=all_cluster_ids, cmap="coolwarm", alpha=0.4, s=15) ax.set_title("K-Means Clusters (k=2) on t-SNE") plt.colorbar(scatter, label="Cluster ID") plt.tight_layout() plt.savefig("outputs/kmeans_clusters_tsne.png", dpi=150) plt.show() Ako se dvije boje u ovoj parceli otprilike podudaraju s dvjema grupama koje ste vidjeli u označenoj parceli t-SNE, grupiranje radi. 4.8 – Dodjeljivanje pseudo-oznaka neoznačenim slikama Sada je ključni korak: uzimamo udele klastera i tretiramo ih kao „slabe oznake“ za neoznačene slike. Ali postoji suptilnost – K-Means dodjeljuje ID klastera proizvoljno. Izdvojite pseudo-oznake unlabeled_pseudo_labels = all_cluster_ids[len(labeled_embeddings):] Provjerite usklađenost sa stvarnim oznaka: labeled_cluster_ids = all_cluster_ids[:len(labeled_embeddings)] # What fraction of labeled images in cluster 0 are actually "normal"? cluster_0_normal_rate = (labeled_labels_arr[labeled_cluster_ids == 0] == 0).mean() cluster_1_normal_rate = (labeled_labels_arr[labeled_cluster_ids == 1] == 0).mean() print(f"Cluster 0: {cluster_0_normal_rate:.1%} of labeled images are 'normal'") print(f"Cluster 1: {cluster_1_normal_rate:.1%} of labeled images are 'normal'") Ako je klaster 0 uglavnom greške (normal_rate < 50%), mi preokrenemo mape: if cluster_0_normal_rate < 0.5: unlabeled_pseudo_labels = 1 - unlabeled_pseudo_labels print("Cluster IDs flipped to match convention (0=normal, 1=defect)") Hajde da pogledamo distribuciju: print(f"\nPseudo-label distribution:") print(f" Normal (0): {(unlabeled_pseudo_labels == 0).sum()} images") print(f" Defect (1): {(unlabeled_pseudo_labels == 1).sum()} images") Sada imamo dva odvojena skupa podataka sa vrlo različitim karakteristikama: — 200 images with real expert labels. High quality, small quantity. This is our ground truth. Strongly labeled Slabo označene – 9.800 slika sa pseudo-labelima zasnovanima na klasterima. Niža kvaliteta (neke etikete su pogrešne), ali ogromna količina. Zlatno pravilo : Oni služe različitim ciljevima u narednom koraku. never mix these two. Još jedno čitanje: Još jedno čitanje: K-Means objašnjeno (scikit-learn) DBSCAN objašnjeno (scikit-learn) Kako pravilno čitati t-SNE (Distill) – bitno čitanje K-Means objašnjeno (scikit-learn) DBSCAN objašnjeno (scikit-learn) Kako pravilno čitati t-SNE (Distil) Peti deo – Semi-nadzirano osposobljavanje: stvarni eksperiment 5.1 – Logika iza našeg dvostupanjskog pristupa Zamislite da trenirate novog inspektora kvaliteta u tvornici: : Prikazujete im 9.800 fotografija i kažete "Ja ovo je normalno i ovo je neispravno, ali nisam 100% siguran.“ Inspektor počinje formirati grubo mentalni model. Neke od etiketa su pogrešne, ali ukupni uzorak – normalne površine su glatke i ujednačene, neispravne površine imaju nepravilnosti – uglavnom je točan. Nakon ove faze, inspektor ima pristojan . Phase 1 (pre-training on pseudo-labels) Misli Intuicija : Tada im pokažete 200 fotografija koje je pažljivo proverio stručnjak: "Ovo su DEFINITELNO normalne, a one su DEFINITELNO defektne." Phase 2 (fine-tuning on real labels) Rezultati istraživanja: čovek koji ima 10.000 slika (izgradnja široke intuicije) i bio je Očekujemo da će ovaj inspektor nadmašiti onog koji je ikada vidio samo 200 verifikovanih primjera. Vidi Kalibracija Da bismo to dokazali, sprovodimo dva paralelna eksperimenta: Eksperiment A - Samo pod nadzorom: tren na samo 200 označenih slika Eksperiment B – Semi-nadzor: pre-tren na 9.800 pseudo-označenih slika, a zatim fin-tune na 200 označenih slika Isti model arhitekture, isti test set. Jedina razlika je da li model vidi neoznačene podatke ili ne. 5.2 — Izgradnja klasifikatora: arhitektura Mi koristimo ResNet50 kao leđa opet, ali ovaj put smo konačni sloj sa binarnim klasifikatorom i mi to (za razliku od dijela 3, gde smo samo izvukli karakteristike). replace train import torch.nn as nn class DefectClassifier(nn.Module): """ Binary classifier: Normal (0) vs Defect (1). Based on ResNet50 with a custom classification head. """ def __init__(self, dropout_rate=0.5): super().__init__() self.backbone = models.resnet50(weights=models.ResNet50_Weights.IMAGENET1K_V1) num_features = self.backbone.fc.in_features # 2048 Ovde zamenimo originalnu ImageNet klasifikaciju glave (2048 → 1000 klase) sa našim: self.backbone.fc = nn.Sequential( nn.Dropout(p=dropout_rate), # Anti-overfitting nn.Linear(num_features, 1), # Binary output ) def forward(self, x): return self.backbone(x) Zašto ? Sa samo 200 označenih slika i 25 miliona parametara, prekomjerna oprema je glavna prijetnja. Dropout slučajno onemogućuje 50% neurona tokom svakog koraka obuke, prisiljavajući mrežu da uči redundantne reprezentacije. U trenutku zaključivanja, svi neuroni su aktivni. Ovo je jedna najučinkovitija tehnika regularizacije za male skupove podataka. Dropout(0.5) Zašto za binarnu klasifikaciju, jedan izlazni neuron sa sigmoidnom aktivacijom je matematički ekvivalentan dvjema neuronima sa softmax, ali jednostavniji i nešto više numerički stabilan. Linear(2048, 1) 5.3 – Funkcija gubitka: rješavanje neravnoteže klase Prije nego što napišemo obuku, hajde da razgovaramo o funkciji gubitka. (Binarna križna entropija sa Logitima), koja kombinuje sigmoidnu aktivaciju sa binarnom križnom entropijom u jednoj, numerički stabilnoj operaciji. BCEWithLogitsLoss Ključni dodatak je : pos_weight pos_weight = torch.tensor([4.0]).to(device) criterion = nn.BCEWithLogitsLoss(pos_weight=pos_weight) Šta čini To govori o funkciji gubitka: "izostavljeni nedostatak (lažni negativni) treba kazniti Bez njega, model bi mogao postići 80% tačnost tako što će uvek predvidjeti "normalno" - što je beskorisno. pos_weight=4.0 4 times more Vrijednost 4.0 je grozna procjena zasnovana na omjeru klase. Ako imate 80% normalno / 20% nedostatak, onda Možete podesiti ovu vrednost, ali 4.0 je dobra polazna točka. pos_weight = 80/20 = 4.0 5.4 — Optimizer: AdamW sa raspadom težine import torch.optim as optim optimizer = optim.AdamW(model.parameters(), lr=lr, weight_decay=1e-4) Zašto AdamW? To je Adam s pravilnim raspadom težine (L2 regularizacija). nežno kažnjava velike težine, što je još jedan sloj zaštite od prekomjerne opreme. Razmislite o tome kao da kažete modelu "preferiraju jednostavnija objašnjenja." weight_decay=1e-4 5.5 – Raspored stope učenja scheduler = optim.lr_scheduler.ReduceLROnPlateau(optimizer, patience=3, factor=0.5) To automatski smanjuje brzinu učenja kada se gubitak validacije prestaje poboljšavati. znači "čakati 3 epohe bez poboljšanja prije smanjenja". znači "multiplicirajte stopu učenja za 0,5." Ovo je ključno za konvergenciju - budući da se model približava minimumu, manji koraci sprečavaju pretjerivanje. patience=3 factor=0.5 5.6 — Trening ciklus: jedna epoha u jednom trenutku Sada hajde da izgradimo punu funkciju obuke. Proći ćemo kroz svaki deo zgloba odvojeno. (jedna epoha - jedan prolaz kroz sve podatke o obuci): The training phase from sklearn.metrics import f1_score def train_model(model, train_loader, val_loader, epochs, lr, device, phase_name=""): """Train the model and track validation F1 score.""" pos_weight = torch.tensor([4.0]).to(device) criterion = nn.BCEWithLogitsLoss(pos_weight=pos_weight) optimizer = optim.AdamW(model.parameters(), lr=lr, weight_decay=1e-4) scheduler = optim.lr_scheduler.ReduceLROnPlateau(optimizer, patience=3, factor=0.5) best_f1 = 0 for epoch in range(epochs): # ---- TRAINING ---- model.train() # Enable dropout, update batch norm stats for images, labels in train_loader: images = images.to(device) labels = labels.float().unsqueeze(1).to(device) # .float() because BCEWithLogitsLoss expects float targets # .unsqueeze(1) adds a dimension: shape (batch,) → (batch, 1) optimizer.zero_grad() # Reset gradients from previous batch outputs = model(images) # Forward pass loss = criterion(outputs, labels) # Compute loss loss.backward() # Compute gradients (backpropagation) optimizer.step() # Update weights Svaka serija prolazi kroz klasični ciklus: napredni prolaz → izračun gubitka → gradijenti backpropagate → ažuriranje težine → reset gradijenti za sledeću seriju. (nakon svakog treninga): The validation phase # ---- VALIDATION ---- model.eval() # Disable dropout, use fixed batch norm stats all_preds, all_true = [], [] val_loss_total = 0 with torch.no_grad(): # No gradients needed for evaluation for images, labels in val_loader: images = images.to(device) outputs = model(images) # Compute validation loss val_loss_total += criterion( outputs, labels.float().unsqueeze(1).to(device) ).item() # Convert raw logits → binary predictions # sigmoid maps logits to [0, 1], then threshold at 0.5 preds = (torch.sigmoid(outputs) >= 0.5).int().cpu().numpy().flatten() all_preds.extend(preds) all_true.extend(labels.numpy()) Obaveštenje o Ovo je važno: on onemogućava dropout (svi neuroni aktivni) i koristi statistiku rada za normaliziranje serije umesto statistike serije. model.eval() Track metrics and update scheduler: val_f1 = f1_score(all_true, all_preds, average="binary") scheduler.step(val_loss_total) # Reduce LR if loss plateaued if val_f1 > best_f1: best_f1 = val_f1 if (epoch + 1) % 5 == 0: print(f" [{phase_name}] Epoch {epoch+1}/{epochs}: val_f1={val_f1:.4f}") print(f" [{phase_name}] Best F1: {best_f1:.4f}") return best_f1 Mi pratimo F1 kroz sve epohe, ne samo posljednji. modeli često vrhunac prije završetka obuke (nakon čega oni mogu nadmašiti malo). best 5.7 – Priprema podjela podataka Podijelimo označene podatke na voz (70%) i test (30%). : nikada se ne koristi za obuku u bilo kojem eksperimentu. Stratifikacija osigurava da se razredni omjer očuva u oba dijela. sacred from sklearn.model_selection import train_test_split labeled_train_idx, labeled_test_idx = train_test_split( range(len(labeled_paths)), test_size=0.3, random_state=42, stratify=labeled_labels, ) Kreirajte tri datoteke koje su nam potrebne: # 1. Labeled training data (for supervised training + fine-tuning) train_labeled_ds = MetalSurfaceDataset( [labeled_paths[i] for i in labeled_train_idx], [labeled_labels[i] for i in labeled_train_idx], preprocessing, ) # 2. Test data (for evaluation only — NEVER used for training) test_ds = MetalSurfaceDataset( [labeled_paths[i] for i in labeled_test_idx], [labeled_labels[i] for i in labeled_test_idx], preprocessing, ) # 3. Weakly labeled data (pseudo-labels from clustering) weakly_labeled_ds = MetalSurfaceDataset( unlabeled_paths, unlabeled_pseudo_labels.tolist(), preprocessing, ) I za podatke: train_labeled_loader = DataLoader(train_labeled_ds, batch_size=16, shuffle=True) test_loader = DataLoader(test_ds, batch_size=16, shuffle=False) weakly_labeled_loader = DataLoader(weakly_labeled_ds, batch_size=32, shuffle=True) print(f"Train (labeled): {len(train_labeled_ds)} images") print(f"Test: {len(test_ds)} images") print(f"Weakly labeled: {len(weakly_labeled_ds)} images") Imajte na umu različite veličine serije: 16 za mali set označen (manje slika po epohi), 32 za veliki set slabo označen (brža obrada). za obuku da spreči model da uči redoslijed uzoraka. shuffle=True 5.8 – Eksperiment A: samo pod nadzorom (osnovna linija) Ovo je jednostavniji eksperiment. Mi obučavamo novi model koristeći SAMO 140 označenih obrazaca za obuku (ostalih 60 je rezervirano za testiranje). print("=" * 60) print("EXPERIMENT A: SUPERVISED ONLY (140 labeled images)") print("=" * 60) model_supervised = DefectClassifier(dropout_rate=0.5).to(device) f1_supervised = train_model( model_supervised, train_labeled_loader, test_loader, epochs=30, lr=1e-4, device=device, phase_name="Supervised" ) 5.9 – Eksperiment B: polu-nadzor (dvofazni pristup) Faza 1 daje modelu široku intuiciju iz 9.800 pseudo-označenih slika. Phase 1 — Pre-training on weakly labeled data: print("\n" + "=" * 60) print("EXPERIMENT B: SEMI-SUPERVISED") print("=" * 60) model_semi = DefectClassifier(dropout_rate=0.5).to(device) print("\nPhase 1: Pre-train on pseudo-labeled data (9,800 images)...") train_model( model_semi, weakly_labeled_loader, test_loader, epochs=10, lr=1e-4, device=device, phase_name="Pre-train" ) Ovde treniramo samo 10 epoha jer su pseudo-labelovi bučni. Phase 2 — Fine-tuning on strongly labeled data: print("\nPhase 2: Fine-tune on real labeled data (140 images)...") f1_semi = train_model( model_semi, train_labeled_loader, test_loader, epochs=20, lr=5e-5, device=device, phase_name="Fine-tune" ) Obaveštenje o (5e-5 vs 1e-4 u fazi 1). Ovo je namerno i kritično. Ako koristimo visoku stopu učenja tokom fine-tuning-a, model bi brzo "zaboravio" sve što je naučio tokom pre-treninga - gradijenti bi bili preveliki i prepisali bi pre-trenirane težine. Blaga stopa učenja omogućava modelu da napravi male korekcije na svoje postojeće znanje, zadržavajući široke obrasce iz faze 1 dok popravlja greške pravim etiketama. lower learning rate Ovo je analogno tvornicom inspektora: ne počinjete njihov trening od nule u fazi 2. 5.10 — Završna procjena: trenutak istine Sada ćemo ocijeniti oba modela na istom test setu sa više metrika. Kao prvo, funkcija evaluacije: from sklearn.metrics import roc_auc_score, classification_report def full_evaluation(model, test_loader, device, name): """ Evaluate on the test set. Returns F1 score and AUC-ROC. """ model.eval() all_preds, all_probs, all_true = [], [], [] with torch.no_grad(): for images, labels in test_loader: outputs = model(images.to(device)) probs = torch.sigmoid(outputs).cpu().numpy().flatten() all_probs.extend(probs) all_preds.extend((probs >= 0.5).astype(int)) all_true.extend(labels.numpy()) Obojica skupljamo (za AUC-ROC, koji mjeri kvalitet rangiranja) i (za F1, koji mjeri kvalitet klasifikacije na pragu od 0,5): probabilities binary predictions f1 = f1_score(all_true, all_preds, average="binary") auc = roc_auc_score(all_true, all_probs) print(f"\n{name}:") print(f" F1 Score: {f1:.4f}") print(f" AUC-ROC: {auc:.4f}") print(classification_report( all_true, all_preds, target_names=["Normal", "Defect"] )) return f1, auc Zašto Jer sa neuravnoteženim klasama, tačnost je zavaravajuća. Model koji uvek predviđa „normalno“ dobija 80% tačnost, ali 0% podsjetnik na nedostatke. F1 je harmonična sredina preciznosti i podsjetnika – on kažnjava modele koji ignoriraju manjinsku klasu. F1 Zašto Ona mjeri koliko dobro model slike (neispravne slike treba da dobiju veće vjerojatnosti od normalnih), bez obzira na prag klasifikacije. AUC od 1,0 znači savršeno rangiranje; 0,5 znači slučajno. AUC-ROC Ranjeni A sada usporedba: f1_sup, auc_sup = full_evaluation( model_supervised, test_loader, device, "SUPERVISED ONLY" ) f1_semi, auc_semi = full_evaluation( model_semi, test_loader, device, "SEMI-SUPERVISED" ) I konačna presuda: print("=" * 60) print("FINAL COMPARISON") print("=" * 60) print(f" {'Metric':<12s} {'Supervised':>12s} {'Semi-supervised':>16s} {'Delta':>8s}") print(f" {'-'*50}") print(f" {'F1':<12s} {f1_sup:>12.4f} {f1_semi:>16.4f} {f1_semi - f1_sup:>+8.4f}") print(f" {'AUC-ROC':<12s} {auc_sup:>12.4f} {auc_semi:>16.4f} {auc_semi - auc_sup:>+8.4f}") Ako kolona Delta pokazuje pozitivne brojeve, dokazali smo da su neoznačeni podaci bili korisni. pseudo-označnice, uprkos tome što su nesavršene, dali su modelu početak da se čisti nadzor na 200 slika nije mogao podudarati. 5.11 – Interpretacija rezultata Evo kako da pročitate poređenje: F1 poboljšana za +0.05 ili više: jasna pobjeda za polu-nadzor. F1 poboljšan od +0.01 do +0.04: skromno poboljšanje. Semi-nadzor pomaže, ali marža je mala. Razmislite o poboljšanju kvalitete grupiranja ili korištenju sofisticiranijeg pseudo-označivanja. F1 nepromijenjena ili još gore: pseudo-etikete su bile previše bučne da bi pomogle, ili grupiranje nije uhvatilo pravu strukturu. Isprobajte različite ekstraktore značajki, različite algoritme grupiranja ili veće pragove poverenja za pseudo-etikete. Još jedno čitanje: Još jedno čitanje: Pseudo-označavanje radova (Lee, 2013) – originalni pristup Semi-nadzirano istraživanje učenja (van Engelen & Hoos) – sveobuhvatan pregled metoda PyTorch trening loop najbolje prakse Pseudo-obeležavanje papir (Lee, 2013) Istraživanje poluprovedenog učenja (van Engelen & Hoos) PyTorch trening loop najbolje prakse Deo 6 – Širenje na milijune slika: realističan putnički plan Pitanje iz biznisa "Vaša dokaza o konceptu radi na 10.000 slika. Imamo 4 miliona slika za obradu. možemo li proširiti ovaj cevovod s budžetom od 5.000 evra?" Ovo je pitanje s kojim ćete se suočiti u bilo kojem stvarnom projektu. Troškovi računanja Na našim 10.000 slika sa jednim GPU-om, to je trajalo oko 30 minuta. Feature extraction 4,000,000 images ÷ 10,000 images × 30 min = 12,000 min = 200 GPU-hours Na ~€2/h za instanci GPU u oblaku (Azure NC serija sa T4 ili A10 GPU), to je oko . €400 Standardni K-Means učita sve podatke u memoriju za izračunavanje udaljenosti. Uz 4M ugrađivanja od 2048 dimenzija (svaki 4 bajt float): Clustering 4,000,000 × 2,048 × 4 bytes = ~32 GB just for the embeddings To se neće uklopiti u RAM na većini mašina. Rješenje: koristite od scikit-learn, koji obrađuje podatke u komadićima (reci) 10.000 uzoraka u isto vrijeme. MiniBatchKMeans Pre-trening na 4M pseudo-označenim slikama traje oko 50 sati GPU-a . CNN training €100 Troškovi skladištenja Raw slike: 4M × ~50 KB prosjek = Ugradnje: 4M × 2048 × 4 bajta = Na Azure Blob skladištenju na ~€0.02/GB/mesec, to je oko . 200 GB 32 GB €5/month Strategija označivanja Ako 200 oznaka nije dovoljno u skali, mogli bismo označiti više. Za ~1 € po slici (uključujući kontrolu kvalitete), 2.000 dodatnih oznaka bi koštalo Ali postoji pametniji pristup: . €2,000 active learning Aktivno učenje omogućuje modelu da izabere Umjesto da slučajno odabere 2.000 slika, model identificira one o kojima je najosjetljiviji – slike koje bi ga najviše naučile. Koji Uz aktivno učenje, možda nam je potrebno samo 500 dodatnih oznaka umesto 2.000 . €500 Procjena ukupnog budžeta Feature extraction (GPU): €400 CNN training (GPU): €100 Storage (year 1): €60 Additional labeling: €500 – €2,000 ────────────────────────────────────── TOTAL: €1,060 – €2,560 Dobro u okviru budžeta od 5.000 evra, sa prostorom za eksperimentiranje i ponovne pokretanje. Pet uvjeta za uspeh Koristite GPU u oblaku, a ne lokalni hardver. Iznajmljivanje po satu, plaćajte samo za ono što koristite. Koristite MiniBatchKMeans umesto redovnih KMeans. Isti kvalitet, 100x manje memorije. Izgradite odgovarajuću podatkovnu cijev sa serijskom obradom. Nikada ne učitajte 4M slike u RAM-u odjednom. Koristite PyTorch DataLoader sa num_workers > 0 za paralelno učitavanje. Razmotrite aktivno učenje kako biste maksimalno povećali vrednost svake slike označene ljudima. Svaka oznaka treba odabrati strateški, a ne slučajno. Trgovina i ugradnje verzija, ne samo sirove slike. Ponovno ekstrakcija 4M ugradnje košta 400 €; učitavanje sačuvane ugradnje ne košta ništa. Još jedno čitanje: Još jedno čitanje: MiniBatchKMeans (scikit-learn) — how to label smarter, not more Active learning overview MiniBatchKMeans (scikit-uči) Aktivno učenje Pregled Zaključak Polu-nadzorno učenje nije magija – to je inženjering. Vi uzimate strukturu skrivenu u neoznačenim podacima (preko ugrađivanja i grupiranja), pretvorite je u približne oznake, i koristite one da date vašem nadzorovanom modelu početak. Hajde da pregledamo cjelokupnu cijev koju smo izgradili: Istraživanje – Skenirali smo 10.000 slika za korupciju, nedosledne formate i neravnotežu klase. Predobrada – Standardizovali smo svaku sliku u format ResNet50 očekuje: 224×224, RGB, CLAHE-pojačano, ImageNet-normalizovano. Ekstrakcija značajki - Koristili smo pretrenirani ResNet50 da pretvorimo svaku sliku u 2048-dimenzionalno ugrađivanje koje hvata njegovu vizualnu suštinu. Clustering - Primijenili smo K-Means i DBSCAN da grupiramo neoznačene slike u klastere, a zatim dodijelimo pseudo-etikete na osnovu članstva u klasteru. Polu-nadzorovana obuka - Pre-trenirali smo CNN na 9.800 pseudo-označenih slika, a zatim fino prilagođavali na 200 pravih oznaka i uspoređivali ih s baznom linijom samo pod nadzorom. Analiza razmjera – Procijenili smo troškove izračuna, skladištenja i označavanja za 4M slika, što potvrđuje izvedivost u okviru budžeta od 5.000 eura. Ključne reči takeaways: Pre-trenirani CNN može da izvuče značajne karakteristike iz bilo kojeg domena slike, čak i onog na kojem nikada nije bio treniran. Klupiranje na ugrađivanjima otkriva prirodne grupiranja koja često odgovaraju stvarnim klasama. Pseudo-labelovi su nesavršeni, ali model koji je prethodno osposobljen na nesavršenim etiketama, a zatim fino prilagođen na stvarnim etiketama, nadmašuje model koji je osposobljen samo na stvarnim etiketama. A polu-nadzoreni pristup je najvredniji upravo kada su etikete rijetke i skupe – to je situacija s kojom ćete se suočiti u većini projekata u stvarnom svijetu. Uzorak radi širom domena: medicinske slike, kontrolu industrijske kvalitete, satelitske slike, klasifikaciju dokumenata i praćenje biološke raznolikosti.Gde god su oznake skupe i neoznačeni podaci su obilni – što je, do 2025, gotovo svugdje.