Table of Contents Tabuľka obsahov Úvod: Štítky sú drahé, obrázky sú zadarmo Časť 1 – Vyhľadávanie dát: Pochopenie údajov, s ktorými pracujete Časť 2 – Predbežné spracovanie: hovoriť jazykom modelu Časť 3 – Extrahovanie funkcií: Obrátenie obrázkov na zmysluplné čísla Časť 4 – Nenadriadené zoskupovanie: Objavovanie štruktúry v tme Časť 5 – Semi-nadriadený tréning: Kľúčový experiment Časť 6 – Rozšírenie na milióny obrázkov: realistická cestovná mapa záver Štítky sú drahé, obrázky sú zadarmo V dokonalom svete by každý obrázok vo vašej databáze mal štítok. „Defektívny.“ „Normálny.“ „Crack typu A.“ „Scratch typu B.“ Ale v reálnom svete je označovanie brutálne drahé. Jeden lekársky rádiológ môže označiť možno 50 vyšetrení mozgu za hodinu – za 200 €/hodinu. Inšpektor priemyselnej kvality môže anotovať možno 100 častí za hodinu. Tu je paradox: spoločnosti často majú milióny obrázkov (z kamier, snímačov, užívateľských nahrávok), ale môžu si dovoliť označiť len malú časť. Namiesto toho, aby sme vyhodili neoznačené obrázky, používame ich na zlepšenie modelu – kombináciou malej označenej sady s veľkou neoznačenou sadou. semi-supervised learning Dovoľte mi použiť analógiu. Predstavte si, že ste učiteľ s 30 študentmi. Dáte im všetkým skúšku, ale máte len čas na hodnotenie 5 článkov. Ohodnotíte tých 5 starostlivo a všimnete si vzory: študenti, ktorí napísali veľa, majú tendenciu získať vysoké skóre a študenti, ktorí zanechali prázdne odpovede, majú tendenciu získať nízke skóre. Pomocou týchto vzorov môžete odhadnúť hodnotenie ostatných 25 článkov bez toho, aby ste čítali každý riadok. Tento článok stavia kompletný čiastočne dohliadaný kanál na klasifikáciu obrázkov od začiatku. Budeme pracovať cez každý krok s podrobnými vysvetlením, ako pôjdeme. Prípadová štúdia: detekcia výrobných chýb na kovových povrchoch. továreň vyrába oceľové dosky a kamery fotografujú každú dosku, keď sa valí z výrobnej linky. Väčšina dosiek je normálna, niektoré majú chyby (škrabance, praskliny, pitting, inklúzie). Máme 10 000 obrázkov, ale iba 200 označených. Prípadová štúdia : Továreň vyrába oceľové dosky a kamery fotografujú každú dosku, keď sa roluje z výrobnej linky.Väčšina dosiek je normálna, niektoré majú chyby (škrabance, praskliny, pitting, inklúzie). Máme 10 000 obrázkov, ale iba 200 označených. detecting manufacturing defects on metal surfaces. Vytvorenie polovodičového vzdelávacieho potrubia Pred ponorením do akéhokoľvek kódu, poďme mapovať celý potrubie. Pochopenie toku najprv urobí každý krok cítiť účelnejšie než náhodné. Vezmite si chvíľu študovať tento diagram - každé pole je krok, ktorý budeme implementovať: 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] Kľúčový pohľad: premeníme neoznačené obrázky na označené obrázky (prostredníctvom zoskupenia), potom použite túto približnú znalosť, aby ste konečnému modelu dali hlavný začiatok.Premýšľajte o tom, ako dať študentovi hrubý sprievodca štúdiom pred skutočnou skúškou - nebude to dokonalé, ale je to lepšie ako nič. približne Buďme tiež explicitní o slovnej zásobe, ktorú budeme používať v tomto článku, pretože miešanie pojmov je veľmi častým zdrojom zmätku: Silne označené (alebo jednoducho „označené“): obrázky s etiketami overenými ľudským odborníkom. Slabo označené (alebo „pseudo-označené“): obrázky, ktorých štítky boli odhadnuté zoskupením. lacnejšie, ale hlučnejšie. vytvoríme z nich 9 800. Neoznačené: obrázky bez etikety vôbec.Toto je ich stav predtým, než ich zoskupíme. Embedding: kompaktné číselné zhrnutie obrazu, vytvorené predtrénovanou neurálnou sieťou.Náš hlavný nástroj na porovnanie obrázkov. Ďalšie čítanie : Ďalšie čítanie : Semi-nadriadený prehľad učenia - scikit-learn Výskum spoločnosti Google o semi-nadriadenom vzdelávaní Semi-nadriadený prehľad učenia - scikit-learn Výskum spoločnosti Google o semi-nadriadenom vzdelávaní Data Exploration: porozumenie dátam, s ktorými pracujete Prečo by ste sa mali pozrieť na svoje údaje predtým, než urobíte čokoľvek iné Obrázkové dátové súbory majú jedinečné režimy zlyhania, ktoré nenájdete v tabuľkových údajoch: poškodené súbory, ktoré zlyhajú vo vašom tréningovom kruhu o 3 hodine ráno, nekonzistentné rozlíšenia, ktoré ticho deformujú vaše obrázky, nesprávne farebné kanály (šedý rozsah maskovaný ako RGB) a extrémna nerovnováha triedy, kde 95% obrázkov je "normálne." A zlaté pravidlo: Pozrime sa, s čím sa zaoberáme. never trust data you haven't inspected. 1.1 - načítanie a počítanie obrázkov Náš súbor údajov je usporiadaný do dvoch hlavných priečinkov: jeden s označenými obrázkami (rozdelený na „normálne“ a „defektné“ podpriečinky) a jeden s neoznačenými obrázkami (žiadne podpriečinky – len plochá zbierka súborov) .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" Používame namiesto spojenia reťazcov, pretože správne spracováva separátory ciest na akomkoľvek operačnom systéme. 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%}") Tento pomer značiek je zvyčajne okolo 2%. iba 2% našich údajov má odborne overené štítky. ostatných 98% je zlatá baňa, ktorú si nemôžeme dovoliť ignorovať - a to je presne to, čo semi-nadriadené učenie využíva. 1.2 — Skenovanie problémov: rozlíšenie, farba, korupcia Ďalej musíme skontrolovať každý obrázok individuálne. Jeden poškodený súbor môže zlyhať celý tréningový kurz. Nekonzistentné rozlíšenia skreslia obrázky, ak nie ste opatrní. A nesprávne farebné režimy (šedý rozmer, keď váš model očakáva RGB) produkujú nezmyselné funkcie. Napíšeme malú funkciu, ktorá sa pokúša otvoriť každý obrázok a zaznamenať jeho vlastnosti: 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 Pole je obzvlášť dôležité. znamená 3 farebné kanály (červená, zelená, modrá). znamená šedú škálu (1 kanál). znamená RGB s alfa (transparentným) kanálom.Náš predtrénovaný model očakáva RGB, takže budeme musieť všetko konvertovať neskôr. img.mode 'RGB' 'L' 'RGBA' Teraz poďme skenovať všetkých 10 000 obrázkov: all_files = labeled_files + unlabeled_files image_info = [get_image_info(f) for f in all_files] info_df = pd.DataFrame(image_info) Pozrite sa na výsledky: 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}") Čo hľadať v produkte: Poškodené obrázky > 0: Odstráňte ich okamžite. Dokonca aj jeden zlý súbor môže zlyhať váš tréning. Rôzne rozlíšenia: Ak min ≠ max pre šírku alebo výšku, obrázky majú rôzne veľkosti. Viacfarebné režimy: Ak vidíte "RGB" a "L", máte zmes farieb a šedej škály. Extrémne veľkosti súborov: 1KB súbor je pravdepodobne prázdny alebo poškodený. 50MB súbor môže byť nekomprimovaný – stojí za to skúmať. Odstráňte všetky poškodené súbory: 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 – Rozdelenie tried: Je náš označený súbor vyvážený? V priemyselných a lekárskych prostrediach sú chyby zriedkavé. Vaša označená sada môže byť 90% "normálna" a 10% "defektná". To je nesmierne dôležité: lenivý model, ktorý vždy predpovedá "normálnu" by získal 90% presnosť, zatiaľ čo je úplne zbytočný. Musíme vedieť rovnováhu vopred, aby sme ju mohli kompenzovať neskôr. 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}%)") Ak je nerovnováha závažná, budeme ju neskôr riešiť pomocou techniky nazývanej v funkcii straty - v podstate hovorí modelu "chýba chyba je 4x horšie ako falošný poplach." pos_weight Pozrime sa aj na vizualizáciu: 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 — Vizualizácia vzorových obrázkov: vždy sa pozrite pred modelom Môžete zistiť, že obrázky, ktoré sú zjavne nesprávne označené, skenovanie artefaktov (čierne hranice, rotácie), problémy s kvalitou (zmätok, nadmerná expozícia), alebo že chyby sú vizuálne jemné a vaša úloha je ťažšia, než ste si mysleli. 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() Vezmite si chvíľku na štúdium týchto obrázkov. vidieť chyby vlastnými očami? Ak nemôžete, model bude bojovať taky. Ak sú chyby zrejmé (hlboké poškriabanie, veľký trhliny), to je povzbudzujúce - model by mal byť schopný naučiť sa rozdiel. Ak sú jemné (mierne skreslenie, prasknutie vlasovej línie), budete potrebovať obzvlášť dobré predbežné spracovanie a extrahovanie funkcií. Ty si Ďalšie čítanie : Ďalšie čítanie : Pillow dokumentácia – knižnica obrázkov Python, ktorú používame na načítanie obrázkov NEW Surface Defect Database – reálny súbor údajov o poruchách povrchu ocele, s ktorými môžete pracovať Pillow dokumentácia Nová databáza Surface Defect Časť 2 – Predbežné spracovanie: hovoriť jazykom modelu Prečo nemôžeme len kŕmiť surové obrazy do neurónovej siete Predtrénovaná CNN ako ResNet50 bola vyškolená na veľmi špecifickom type vstupu: 224×224 pixelových obrázkov, vo farbe RGB, normalizovaných so špecifickými priemernými a štandardnými hodnotami odchýlok vypočítanými z dátového súboru ImageNet. Myslite na to ako na jazyk. ResNet50 "hovorí ImageNet." Ak chceme, aby pochopil naše obrazy kovového povrchu, musíme ich najprv "prekladať" do formátu ImageNet. Konvertovať na RGB (3 kanály) Zlepšenie kontrastu pomocou histogramovej rovnice Rozmery 224×224 Normalizujte hodnoty pixelov tak, aby zodpovedali štatistike ImageNet 2.1 – Čo je histogramová ekvalizácia a prečo na tom záleží? Rozdiel medzi normálnym povrchom a poškriabaným môže byť len niekoľko úrovní intenzity pixelov - neviditeľné voľným okom a veľmi ťažké pre model detekovať. Redistribuuje intenzity pixelov tak, aby sa celý rozsah (0 až 255) používal rovnomerne. Histogram equalization Používame pokročilejšiu verziu s názvom (Contrast Limited Adaptive Histogram Equalization). Na rozdiel od globálnej rovnosti (ktorá aplikuje rovnakú transformáciu na celý obraz), CLAHE rozdeľuje obraz na malé dlaždice (8×8 podľa predvoleného nastavenia) a rovná každú dlaždicu nezávisle. CLAHE Tu je analógia: predstavte si, že upravujete jas na fotografii. Globálna vyrovnanie je ako použitie jedného posúvača jasu pre celý obraz - môžete osvetliť tmavé rohy, ale vyčistiť už jasné centrum. CLAHE je ako nastavenie jasu v každej oblasti nezávisle, takže každá časť obrazu je jasná. 2.2 — Vytvorenie vlastnej databázy PyTorch PyTorch organizuje dátové načítanie okolo dvoch tried: (vedia, ako načítať jednu položku) a (vedia, ako batériu a shuffle položky). PyTorch poskytuje vstavaný databázy, ale predpokladá, že každý obrázok má označenie. Naša databáza má označené aj neoznačené obrázky, takže potrebujeme vlastnú triedu. Dataset DataLoader ImageFolder Poďme to vybudovať krok za krokom.Po prvé, kostra: 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) PyTorch nám povie, koľko obrázkov máme. ukladá obrazové cesty a (voliteľne) ich štítky. __len__ __init__ Teraz kľúčová metóda, , ktorý načíta a predbežne spracováva jediný obrázok. Rozdelíme ho na tri etapy: __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") Prečo ? Pretože ResNet50 očakáva 3 kanály. Ak je náš obrázok v šedom meradle (1 kanál), duplikuje šedé hodnoty do R, G a B. Ak je už RGB, nerobí nič. Ak je RGBA (4 kanály), spúšťa alfa kanál. .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) Prečo LAB a nie len aplikovať CLAHE priamo na RGB? Pretože ak vyrovnáte kanály R, G a B nezávisle, skreslíte farby – napríklad zmeníte modrú na zelenú. Pracovaním v priestore LAB sa dotkneme iba kanála ľahkosti (L) a necháme farby (A, B) nedotknuté. 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 — Transformácia potrubia: čo robí každý krok Teraz definujeme sekvenciu transformácií.Každá z nich má špecifický účel: 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], ), ]) Vysvetlime si každý krok: Architektúra ResNet50 vyžaduje presne túto veľkosť. Ak nahráte obrázok s rozmermi 300 × 400, tvary tensorov sa nezhodujú a PyTorch sa zrúti. Zmeny môžu mierne skresliť pomer aspektov, ale pre úlohy založené na textúre (ako je detekcia chýb) je to zriedka problém. (1) Resize to 224×224. Robí dve veci: prevedie formát pixelov z HWC (výška × šírka × kanály) na CHW (kanály × výška × šírka), čo je to, čo PyTorch očakáva; a škáluje hodnoty pixelov z [0, 255] celých na [0.0, 1.0] floatov. (2) ToTensor. Tieto magické čísla - a — sú priemerné a štandardné odchýlky pixelových hodnôt v celej databáze ImageNet, vypočítané kanál za kanálom (R, G, B). ResNet50 bol vyškolený s týmito presnými hodnotami normalizácie, takže jeho vnútorné váhy "očakávajú" vstupy sústredené okolo 0 s jednotkovou odchýlkou. (3) Normalize with ImageNet mean and std. [0.485, 0.456, 0.406] [0.229, 0.224, 0.225] 2.4 - Vytváranie dátových súborov a dátových nakladačov Všetko je v poriadku.Najdôležitejší princíp: Zmiešať ich by kontaminovalo naše hodnotenie. labeled and unlabeled data must be kept strictly separate at all times. Najprv zhromažďujte označené obrazové cesty a ich štítky tried: 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 Potom zhromažďujte neoznačené obrázkové cesty (nie sú potrebné žiadne štítky): unlabeled_paths = [str(fp) for fp in unlabeled_files] Vytvorenie PyTorch Dataset objektov: 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") A konečne, zabaliť ich do DataLoaders. DataLoader balí obrázky dohromady (batch_size=32 znamená 32 obrázkov naraz) a voliteľne ich shuffle: labeled_loader = DataLoader(labeled_dataset, batch_size=32, shuffle=False) unlabeled_loader = DataLoader(unlabeled_dataset, batch_size=32, shuffle=False) Prečo Pretože sa chystáme extrahovať funkcie a potrebujeme vloženiny, aby zostali v rovnakom poradí ako naše zoznamy súborov. shuffle=False Ďalšie čítanie : Ďalšie čítanie : Plyšové transformátory - kompletný zoznam CLAHE vysvetlil (OpenCV tutoriál) PyTorch Dataset & DataLoader výukový program Plyšové transformátory - kompletný zoznam CLAHE vysvetlil (OpenCV tutoriál) PyTorch Dataset & DataLoader výukový program Časť 3 — Extrakcia funkcií: premena obrázkov na významné čísla 3.1 — Prečo sú surové pixely hrozné zastúpenie 224×224 RGB obrázok má 150,528 čísel (224 × 224 × 3 kanály). Väčšina z nich je hluk – menšie variácie osvetlenia, senzorové artefakty, kompresné artefakty. Horšie: dve fotografie presne toho istého škrabanca, odobraté z mierne odlišných uhlov alebo osvetlenia, majú úplne odlišné hodnoty pixelov. Ak sa pokúsime zoskupovať alebo klasifikovať surové pixely, obrázky, ktoré vyzerajú rovnako pre nás, sa pre algoritmus zdajú úplne odlišné. Potrebujeme reprezentáciu, ktorá zachytí obrázku – „to vyzerá ako škrabanec“, „to je hladký povrch“ – v kompaktnej, stabilnej číselnej forme. a do. meaning embeddings 3.2 - Čo je predtrénovaný model a prečo sa necvičíme od začiatku Tréning hlbokej neurónovej siete od začiatku vyžaduje veľa dát - zvyčajne stovky tisíc obrázkov. Máme 200 označených obrázkov. Ak by sme sa pokúsili vycvičiť 25 miliónov parametrov ResNet50 na 200 obrázkoch, model by si zapamätal každý tréningový obrázok dokonale, ale úplne zlyhal na nových obrázkoch. . overfitting Namiesto toho používame model, ktorý už bol vyškolený na ImageNet – dátový súbor 14 miliónov obrázkov v 1 000 kategóriách (psy, mačky, autá, budovy atď.) Tento model sa už naučil rozpoznať základné vizuálne vlastnosti: okraje, textúry, tvary, farebné gradienty, geometrické vzory. - uplatňujú sa na oceľové povrchy rovnako ako na mačky. universal Myslite na to, ako najať skúseného fotografa, aby skontroloval vašu továreň.Nikdy predtým nevideli oceľové dosky, ale už vedia, ako : môžu vidieť nezvyčajné textúry, náhle zmeny v kvalite povrchu, vzory, ktoré porušujú normu. vidieť 3.3 – Loading ResNet50 Poďme nahrať predtrénovaný model: import torchvision.models as models resnet = models.resnet50(weights=models.ResNet50_Weights.IMAGENET1K_V1) Tento jediný riadok sťahuje (prvýkrát) model ResNet50, ktorého váhy boli vyškolené na ImageNet. 3.4 – Zmrazenie parametrov Nechceme modifikovať žiadne z naučených funkcií. Používame ResNet ako nástroj iba na čítanie: for param in resnet.parameters(): param.requires_grad = False To má dve výhody: zabraňuje náhodnej modifikácii predtrénovaných váh a robí záver rýchlejšie (žiadne sledovanie gradientov = menej výpočtu a menej pamäte). requires_grad = False 3.5 – Odstránenie hlavičky klasifikácie Architektúra ResNet50 vyzerá takto: 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) Nechceme poslednú vrstvu plne pripojenú, pretože je špecifická pre 1000 tried ImageNet (psy, mačky, lietadlá...) a je pre našu úlohu zbytočná. feature_extractor = torch.nn.Sequential(*list(resnet.children())[:-1]) Čo táto linka robí: Vráti všetky vrstvy ResNet ako zoznam. všetky vrstvy okrem poslednej (vrstva FC). Vráťte ich späť do vzoru. resnet.children() [:-1] Sequential(*) feature_extractor.eval() režim vypne vypnutie a používa bežiace štatistiky na normalizáciu dávok. výstupy – rovnaký obrázok vždy vytvára rovnaké vloženie. 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}") Použitie GPU (ak je k dispozícii) robí extrakciu funkcií približne 10x rýchlejšie. 3.6 - Funkcia extrakcie Teraz napíšme funkciu, ktorá kŕmi dávky obrázkov prostredníctvom extraktora funkcií a zhromažďuje vložky. Vonkajšia štruktúra : 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 = [] Výsledky hromadíme v zoznamoch, pretože spracovávame obrázky v dávkach (32 naraz), nie všetky naraz (nehodia sa do pamäte GPU). Hlavný krúžok : with torch.no_grad(): for batch_images, batch_labels in dataloader: batch_images = batch_images.to(device) features = model(batch_images) disables gradient tracking — essential because we're only doing inference, not training. This alone cuts memory usage in half and speeds things up. presunie obrázky na GPU, ak ho máme. torch.no_grad() batch_images.to(device) Výstup z Má tvar — posledné dve dimenzie sú priestorové zvyšky z priemerného zoskupenia. Musíme ich stlačiť: model(batch_images) (batch_size, 2048, 1, 1) features = features.squeeze(-1).squeeze(-1) # Now shape is (batch_size, 2048) — that's our embedding Nakoniec presunieme výsledky späť do CPU (numpy nemôže pracovať s GPU tensormi) a uložíme ich: all_embeddings.append(features.cpu().numpy()) all_labels.append(batch_labels.numpy()) return np.concatenate(all_embeddings), np.concatenate(all_labels) Prilepte všetky dávky do jedného array. np.concatenate 3.7 – Spustenie extrakcie 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 snímok, z ktorých každý je reprezentovaný vektorom s rozmermi 2048. To je kompresia 73x v porovnaní so surovými pixelmi (150,528 → 2 048) – a komprimované zobrazenie je viac zmysluplné ďaleko print("Extracting embeddings for unlabeled images...") unlabeled_embeddings, _ = extract_embeddings( unlabeled_loader, feature_extractor, device ) print(f" Shape: {unlabeled_embeddings.shape}") # Expected: (9800, 2048) Odstránime štítky (všetky sú aj tak -1) s a variabilné. _ 3.8 - Úspora vkladu (neopätovne extrahovať zakaždým!) Extrakcia funkcií je najdrahším krokom – potenciálne 30+ minút na GPU pre 10 000 snímok. 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") Neskôr môžete okamžite reštartovať s . np.load("data/labeled_embeddings.npy") 3.9 – Sanitárna kontrola: sú vložky primerané? Predtým, než pôjdeme ďalej, rýchla kontrola. odpadky do, odpadky von - uistite sa, že naše vložky sú zdravé: 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()}") Čo očakávať: znamená okolo 0.3-0.5, std okolo 0.5-1.0, žiadny NaN, žiadny Inf. Ak vidíte hodnoty NaN, poškodený obraz pravdepodobne prešiel krokom čistenia. Ideme ešte ďalej: Ideme ešte ďalej: Prevodové učenie vysvetlené (PyTorch tutorial) ResNet paper (He et al., 2015) – pôvodná architektúra, ktorá zaviedla prepojenia Funkcia extrakcie vs jemné nastavenie - Stanford CS231n kurzy poznámky Prevodové učenie vysvetlené (PyTorch tutorial) Čítať viac (He et al., 2015) Funkcia extrakcie vs fine-tuning Časť 4 – Nenadriadené zoskupovanie: objavovanie štruktúry v tme 4.1 – Čo robí zoskupenie a prečo ho potrebujeme Teraz máme 10 000 vkladaní – kompaktné číselné zhrnutia každého obrazu. 200 z nich majú štítky. ostatných 9 800 nie. v údajoch, ktoré dúfam, že zodpovedajú "normálnemu" a "defektnému". groupings Základný predpoklad: ak sú naše vložky dobré (a ResNet50 vložky zvyčajne sú), obrázky rovnakého typu budú Normálne povrchy sa zoskupia; chybné povrchy sa zoskupia. Dokonca aj bez štítkov je štruktúra tam – zoskupenie to odhaľuje. blízko Ale najprv praktický problém: 2048 rozmerov je nemožné vizualizovať a urobiť niektoré algoritmy pomalé. 4.2 - Štandardizácia embeddings Ak sa jedna dimenzia pohybuje od 0 do 1000 a druhá od 0 do 0,01, prvá bude úplne dominovať vzdialenosti – akoby druhá dimenzia neexistovala. 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) Prečo štandardizovať označené a neoznačené spoločne? Pretože pochádzajú z rovnakej distribúcie (rovnaká továreň, rovnaká 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 — Zníženie rozmerov s PCA PCA (Principal Component Analysis) nájde smery maximálnej variancie v údajoch a projektuje na najvyššie smery. 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% variancia zachovaná znamená, že sme vyhodili iba 5% informácií, ale zmenšili dimenzionalitu o 40x. Pozrime sa tiež na to, koľko každá zložka prispieva: 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() Tento "obličkový plot" ukazuje, kde pridávanie ďalších komponentov prestane poskytovať významné zisky. Ak sa krivka vyrovná pred 50 komponentmi, môžete použiť ešte menej. 4.4 — Vizualizácia s t-SNE t-SNE je nonlineárna technika redukcie dimenzionality, ktorá je špeciálne navrhnutá pre vizualizáciu. : obrázky, ktoré sú blízko vo vysoko dimenzionálnom priestore budú blízko v 2D pozemku.To je ideálne pre kontrolu, či normálne a chybné obrázky prirodzene oddeľujú. local structure Jedno dôležité varovanie: t-SNE deformuje globálne vzdialenosti – priestor medzi zoskupeniami nemá význam. Použite ho len na vizualizáciu a zoskupenie na pôvodné (alebo PCA-znížené) vložky. 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 parameter približne ovláda "veľkosť susedstva" - koľko blízkych bodov t-SNE považuje. 30 je rozumnou predvolenou hodnotou pre dátové súbory našej veľkosti. perplexity Teraz rozdeľme t-SNE súradnice: labeled_tsne = all_tsne[:len(labeled_embeddings)] unlabeled_tsne = all_tsne[len(labeled_embeddings):] A potom vizualizovať: 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() Ak vidíte dva odlišné mraky v tomto pozemku – zelené na jednej strane, červené na druhej – to je vynikajúce znamenie. # 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() V pravom pozemku by sa šedý oblak (neoznačené obrázky) mal prekrývať s farebnými bodmi.Toto potvrdzuje, že označené a neoznačené obrázky pochádzajú z rovnakého rozdelenia - nevyhnutnou podmienkou pre semi-nadriadené učenie, aby fungovalo. 4.5 – Klasifikácia K-means K-Means je najjednoduchší a najpoužívanejší algoritmus zoskupovania. Rozdeľuje dáta do presne k skupín iteratívne priradením každého bodu do najbližšieho centra zoskupenia a potom aktualizáciou centier. Keďže vieme, že máme 2 triedy (normálne a chybné), začneme s k = 2. ale testujeme aj k = 3, 4, 5 aby sme skontrolovali, či údaje môžu mať viac štruktúry (napr. z nedostatkov vytvárajúcich samostatné zoskupenia). typy Na vyhodnotenie toho, ako dobre klastre zodpovedajú skutočným štítkom, používame ARI = 1.0 znamená dokonalú zhodu s pravými štítkami. ARI = 0.0 znamená náhodné zoskupenie. ARI < 0 znamená horšie ako náhodné. 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 parameter znamená, že K-Means bude bežať 10 krát s rôznymi náhodnými inicializáciami a zachová najlepší výsledok. n_init=10 Ak k=2 dáva najvyššiu hodnotu ARI, potvrdzuje to, že naše údaje majú dve prirodzené skupiny, ktoré sa zhodujú s normálnym vs. defektom. 4.6 – Clustering DBSCAN (alternatívny prístup) DBSCAN funguje veľmi odlišne od K-Means. Namiesto špecifikovania počtu zoskupení špecifikujete dva parametre: eps (epsilon): maximálna vzdialenosť medzi dvoma bodmi, aby boli považované za susedov. premýšľajte o tom ako "ako blízko je dosť blízko?" min_samples: minimálny počet bodov potrebných na vytvorenie hustého regiónu (zoskupenie). Premýšľajte o tom ako o "ako preplnené musí byť susedstvo, aby sa mohlo počítať ako zoskupenie?" DBSCAN automaticky určuje počet zoskupení a identifikuje outliers (body, ktoré nepatria do žiadnych zoskupení – označené ako -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}") Musíme otestovať viaceré kombinácie parametrov, pretože "správne" hodnoty závisia od údajov: 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}") Upozorňujeme, že používame PCA-znížené údaje ( DBSCAN bojuje vo veľmi vysokých dimenziách, pretože všetky vzdialenosti sa stávajú podobnými ( "kletba dimenzionality"). all_pca Porovnajte najlepšie ARI z DBSCAN s najlepšími z K-Means a vyberte víťaza. 4.7 — Vizualizácia zoskupení na pozemku t-SNE Pozrime sa, ako vyzerá najlepšie zoskupenie na našej vizualizácii t-SNE: 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() Ak sa dve farby v tomto pozemku približne zhodujú s dvoma skupinami, ktoré ste videli v označenom pozemku t-SNE, zoskupenie funguje. 4.8 - Priradenie pseudo-etiket na neoznačené obrázky Teraz kľúčový krok: vezmeme klastrové priradenia a zaobchádzame s nimi ako s „slabými štítkami“ pre neoznačené obrázky. Ale je tu jedna jemnosť – K-Means priradí klastrové ID ľubovoľne. Cluster 0 môže zodpovedať „chybným“ alebo „normálnym“. Musíme skontrolovať. Odstráňte pseudo-etikety z nich: unlabeled_pseudo_labels = all_cluster_ids[len(labeled_embeddings):] Skontrolujte zosúladenie s reálnymi štítkami: 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'") Ak je klastra 0 väčšinou vady (normal_rate < 50%), otočíme mapovanie: 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)") Pozrime sa na distribúciu: 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") Teraz máme dve samostatné súbory údajov s veľmi odlišnými charakteristikami: Silne označené – 200 obrázkov s reálnymi odbornými štítkami. Vysoká kvalita, malé množstvo. Slabo označené – 9 800 obrázkov s klastrovými pseudo-etiketami. Nižšia kvalita (niektoré štítky sú nesprávne), ale obrovské množstvo. A zlaté pravidlo: Slúžia rôznym účelom v ďalšom kroku. never mix these two. Ďalšie čítanie : Ďalšie čítanie : K-means vysvetlené (scikit-learn) DBSCAN vysvetlil (scikit-learn) Ako správne čítať t-SNE (Distill) – základné čítanie K-means vysvetlené (scikit-learn) DBSCAN vysvetlil (scikit-learn) Ako správne čítať t-SNE (Distill) Časť 5 – Semi-nadriadený tréning: skutočný experiment 5.1 – Logika nášho dvojfázového prístupu Predstavte si, že trénujete nového inšpektora kvality v továrni: : Ukážete im 9 800 fotografií a poviete: „Ja sú normálne a tieto sú chybné, ale nie som si 100% istý.“ Inšpektor začína vytvárať hrubý mentálny model. Niektoré etikety sú nesprávne, ale celkový vzor – normálne povrchy sú hladké a jednotné, chybné povrchy majú nezrovnalosti – je väčšinou správny. . Phase 1 (pre-training on pseudo-labels) myslieť intuition Potom im ukážete 200 fotografií, ktoré boli starostlivo overené odborníkom: „Toto sú DEFINITÍVNE normálne a tieto sú DEFINITÍVNE chybné.“ Inšpektor zdokonaľuje ich mentálny model – opravuje chyby z fázy 1 a zaostruje ich úsudok o okrajových prípadoch. Phase 2 (fine-tuning on real labels) The result: an inspector who has 10 000 obrázkov (budovanie širokej intuície) a bolo Očakávame, že tento inšpektor prekoná toho, kto videl iba 200 overených príkladov. vidieť kalibrovaný Aby sme to dokázali, spustili sme dva paralelné experimenty: Experiment A – len pod dohľadom: výcvik iba na 200 označených obrázkoch Experiment B – Semi-supervised: pre-train na 9 800 pseudo-označených obrázkov, potom fin-tune na 200 označených obrázkov Rovnaká modelová architektúra, rovnaká testovacia sada. Jediný rozdiel je v tom, či model vidí neoznačené údaje alebo nie. 5.2 — Budovanie klasifikátora: architektúra My používame ResNet50 ako chrbticu opäť, ale tentoraz sme Posledná vrstva s binárnym klasifikátorom a my (na rozdiel od časti 3, kde sme len extrahovali funkcie). 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 Tu nahradíme pôvodnú hlavičku klasifikácie ImageNet (2048 → 1000 tried) našou vlastnou: 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) Prečo ? S iba 200 označenými obrázkami a 25 miliónmi parametrov je hlavnou hrozbou overfitting. Dropout náhodne deaktivuje 50% neurónov počas každého tréningového kroku, čo núti sieť učiť sa redundantné reprezentácie. V čase záveru sú všetky neuróny aktívne. Dropout(0.5) Prečo Pre binárnu klasifikáciu je jeden výstupný neurón s sigmoidnou aktiváciou matematicky ekvivalentný dvom neurónom s softmax, ale jednoduchší a mierne numericky stabilnejší. Linear(2048, 1) 5.3 - Funkcia straty: riešenie nerovnováhy tried Predtým, ako napíšeme tréningový kruh, poďme diskutovať o funkcii straty. (Binárna krížová entropia s logitami), ktorá kombinuje sigmoidnú aktiváciu s binárnou krížovou entropie v jednej numericky stabilnej operácii. BCEWithLogitsLoss Kľúčovým doplnkom je : pos_weight pos_weight = torch.tensor([4.0]).to(device) criterion = nn.BCEWithLogitsLoss(pos_weight=pos_weight) Čo robí Hovorí funkcia straty: „zmeškaná chyba (falošný negatívny) by mala byť potrestaná To kompenzuje nerovnováhu medzi triedami. „Bez nej by model mohol dosiahnuť 80% presnosť tým, že vždy predpovedá „normálne“ – čo je zbytočné. pos_weight=4.0 4 times more Hodnota 4.0 je hrubý odhad založený na pomere triedy. Ak máte 80% normálne / 20% vadu, potom Môžete nastaviť túto hodnotu, ale 4.0 je dobrý východiskový bod. pos_weight = 80/20 = 4.0 5.4 — Optimalizátor: AdamW s rozpadom hmotnosti import torch.optim as optim optimizer = optim.AdamW(model.parameters(), lr=lr, weight_decay=1e-4) Prečo AdamW? je to Adam s správnym rozpadom hmotnosti (L2 regulácia). jemne trestá veľké váhy, čo je ďalšia vrstva ochrany proti nadmernej výbave. Premýšľajte o tom, ako povedať modelu "radšej jednoduchšie vysvetlenia." weight_decay=1e-4 5.5 – Plánovač úrovne vzdelávania scheduler = optim.lr_scheduler.ReduceLROnPlateau(optimizer, patience=3, factor=0.5) To automaticky znižuje rýchlosť učenia, keď sa strata overenia prestane zlepšovať. Znamená to „počkať tri epochy bez zlepšenia pred zmenšením“. znamená "množte rýchlosť učenia o 0,5." To je kľúčové pre konvergenciu - keďže model sa blíži k minimálnemu, menšie kroky zabraňujú nadmernému počtu. patience=3 factor=0.5 5.6 – Tréningový cyklus: jedna epocha naraz Teraz budujme plnú tréningovú funkciu. Prejdeme každou časťou kruhu samostatne. (jedna epocha - jeden prechod cez všetky údaje o tréningu): 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 Každá dávka prechádza klasickým cyklom: forward pass → výpočtová strata → backpropagate gradienty → aktualizácia váhy → reset gradienty pre ďalšiu dávku. (po každom tréningovom období): 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()) Všimnite si Toto je dôležité: vypne drop-out (všetky neuróny aktívne) a používa bežiace štatistiky pre normalizáciu dávok namiesto štatistiky dávok. 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 Sledujeme ich Modely často vrcholia pred koncom tréningu (po ktorom môžu mierne prekonať). best 5.7 - Príprava dátových rozdelení Označené údaje rozdelíme na vlak (70%) a test (30%). : nikdy sa nepoužíva na školenie v oboch experimentoch. stratifikácia zaisťuje, že pomer triedy je zachovaný v oboch deleniach. 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, ) Vytvorte tri databázy, ktoré potrebujeme: # 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, ) a dátové zásielky: 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") Všimnite si rôzne veľkosti dávok: 16 pre malú značenú sadu (menej obrázkov za epochu), 32 pre veľkú slabú značenú sadu (rýchlejšie spracovanie). na školenie, aby sa zabránilo modelu naučiť sa poradie vzoriek. shuffle=True 5.8 – Experiment A: len pod dohľadom (základná línia) Jedná sa o jednoduchší experiment.Trenujeme nový model pomocou IBA 140 označených tréningových obrázkov (ostatných 60 je vyhradené na testovanie).Toto je výkon, ktorý by sme získali bez semi-nadriadeného učenia. 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 – Experiment B: polo dohľad (dvojfázový prístup) Fáza 1 dáva modelu širokú intuíciu z 9 800 pseudo-označených obrázkov. Fáza 2 ju zaostruje 140 skutočnými štítkami. 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" ) Trénujeme tu len 10 epoch, pretože pseudo-etikety sú hlučné. 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" ) Všimnite si (5e-5 v porovnaní s 1e-4 vo fáze 1). Toto je úmyselné a kritické. Ak počas jemného nastavenia použijeme vysokú rýchlosť učenia, model by rýchlo "zapomínal" na všetko, čo sa naučil počas predtréningu - gradienty by boli príliš veľké a prepisovali by predtrénované váhy. Jemná rýchlosť učenia umožňuje modelu vykonať malé korekcie k jeho existujúcim vedomostiam, pričom zachováva široké vzory z fázy 1, pričom opravuje chyby s reálnymi štítkami. lower learning rate Je to podobné ako s továrenským inšpektorom: nezačnete ich tréning od začiatku vo fáze 2 a jemne opravíte ich mylné predstavy pri zachovaní ich celkovej intuície. 5.10 — Záverečné hodnotenie: okamih pravdy Teraz hodnotíme oba modely na rovnakom testovacom súbore s viacerými metrikami. Po prvé, funkcia hodnotenia: 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()) Obidve zbierame (pre AUC-ROC, ktorý meria kvalitu hodnotenia) a (pre F1, ktorá meria kvalitu klasifikácie na prahu 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 Prečo Pretože s nevyváženými triedami je presnosť zavádzajúca. Model, ktorý vždy predpovedá "normálne" dostane 80% presnosť, ale 0% pripomenutie na chyby. F1 je harmonickým priemerom presnosti a pripomenutia - trestá modely, ktoré ignorujú menšinovú triedu. F1 Prečo Zisťuje, ako dobre model obrázky (chybné obrázky by mali mať vyššiu pravdepodobnosť ako normálne), bez ohľadu na prah klasifikácie. AUC 1,0 znamená dokonalé poradie; 0,5 znamená náhodné. AUC-ROC ranky A teraz porovnanie: 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" ) A konečný verdikt: 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}") Ak stĺpec Delta zobrazuje pozitívne čísla, dokázali sme, že neoznačené údaje boli užitočné. pseudo-etikety, napriek tomu, že sú nedokonalé, dali modelu hlavný štart, že čistý dohľad na 200 obrázkoch sa nemohol zhodovať. 5.11 - Interpretácia výsledkov Tu je, ako čítať porovnanie: F1 vylepšená o +0.05 alebo viac: jasné víťazstvo pre semi-nadriadených. F1 vylepšená o +0.01 na +0.04: skromné zlepšenie. Semi-supervised pomáha, ale marža je malá. zvážte zlepšenie kvality zoskupenia alebo použitie sofistikovanejšie pseudo-označenie. F1 nezmenené alebo horšie: pseudo-etikety boli príliš hlučné na to, aby pomohli, alebo zoskupenie nezaznamenalo skutočnú štruktúru. Ďalšie čítanie : Ďalšie čítanie : Pseudo-označovanie papier (Lee, 2013) – pôvodný prístup Semi-supervised learning survey (van Engelen & Hoos) – komplexný prehľad metód PyTorch tréningový kruh najlepšie postupy Pseudo-označovanie papier (Lee, 2013) Semi-nadriadený prieskum vzdelávania (van Engelen & Hoos) PyTorch tréningový kruh najlepšie postupy Časť 6 – Rozšírenie na milióny obrázkov: realistická cestná mapa Otázka z obchodu „Váš dôkaz o koncepte funguje na 10 000 obrázkoch. Máme na spracovanie 4 milióny obrázkov. Môžeme tento potrubný kanál rozširovať s rozpočtom 5 000 eur?“ To je otázka, ktorú budete čeliť v akomkoľvek skutočnom projekte. Počítačové náklady na našich 10 000 snímkach s jedným GPU to trvalo približne 30 minút. Feature extraction 4,000,000 images ÷ 10,000 images × 30 min = 12,000 min = 200 GPU-hours Pri ~€2/hodina pre inštanciu cloud GPU (Azure NC-séria s T4 alebo A10 GPU), to je asi . €400 tiež potrebuje adaptáciu. Štandardný K-Means nahráva všetky dáta do pamäte na výpočet vzdialeností.S 4M vkladaním 2048 rozmerov (každý 4-byte float): Clustering 4,000,000 × 2,048 × 4 bytes = ~32 GB just for the embeddings To sa nezmestí do pamäte RAM na väčšine strojov.Riešenie: používať z scikit-learn, ktorý spracováva dáta v kúskoch (napríklad) 10 000 vzoriek naraz. MiniBatchKMeans Pre-tréning na 4M pseudo-označené obrázky trvá asi 50 GPU-hodín . CNN training €100 Náklady na skladovanie Surové obrázky: 4M × ~50 KB priemer = Vloženie: 4M × 2048 × 4 bajty = Na Azure Blob Storage pri ~€0.02/GB/mesiac, to je asi . 200 GB 32 GB €5/month Stratégia označovania Ak by 200 štítkov nestačilo v mierke, mohli by sme označiť viac. ~ € 1 za obrázok (vrátane kontroly kvality), ďalších 2000 štítkov by stálo Ale existuje múdrejší prístup: . €2,000 active learning Aktívne učenie umožňuje modelu vybrať si Namiesto náhodného výberu 2 000 obrázkov model identifikuje tie, o ktorých je najviac neistý – obrázky, ktoré by ho učili najviac. Ktoré S aktívnym učením by sme mohli potrebovať len 500 ďalších štítkov namiesto 2 000 . €500 Celkový odhad rozpočtu Feature extraction (GPU): €400 CNN training (GPU): €100 Storage (year 1): €60 Additional labeling: €500 – €2,000 ────────────────────────────────────── TOTAL: €1,060 – €2,560 Dobre v rámci rozpočtu vo výške 5 000 €, s priestorom na experimentovanie a reštartovanie. Päť podmienok pre úspech Používajte cloudové GPU, nie lokálny hardvér. Prenájom za hodinu, platiť len za to, čo používate. Použite MiniBatchKMeans namiesto bežných KMeans. Rovnaká kvalita, 100x menej pamäte. Vytvorte správny dátový potrubie s batériovým spracovaním. Nikdy neuložte 4M obrázky do pamäte RAM naraz. Použite PyTorch DataLoader s num_workers > 0 pre paralelné načítanie. Zvážte aktívne učenie, aby ste maximalizovali hodnotu každého obrazu s ľudskou značkou.Každá značka by mala byť vybraná strategicky, nie náhodne. Obchodné a verziové vložky, nie len surové obrázky. Opätovné extrahovanie vložiek 4M stojí 400 €; načítanie uložených vložiek nestojí nič. Ďalšie čítanie : Ďalšie čítanie : MiniBatchKMeans (scikit-learn) — how to label smarter, not more Active learning overview MiniBatchKMeans (prečítajte si viac) Prehľad aktívneho učenia záver Semi-nadriadené učenie nie je mágia – je to inžinierstvo. Vezmite štruktúru skrytú v neoznačených údajoch (prostredníctvom vkladaní a zoskupovania), premeníte ich na približné štítky a použite ich na to, aby ste svojmu dohliadanému modelu dali špičkový štart. Pozrime sa na celý projekt, ktorý sme vybudovali: Prieskum – Skenovali sme 10 000 obrázkov na korupciu, nekonzistentné formáty a nerovnováhu tried. Predbežné spracovanie – Štandardizovali sme každý obrázok do formátu ResNet50 očakáva: 224×224, RGB, CLAHE-zlepšené, ImageNet-normalizované. Extrakcia funkcií – Použili sme predtrénovaný ResNet50 na premenu každého obrazu na 2048-dimenzionálne vkladanie, ktoré zachytáva jeho vizuálnu podstatu. Klastrovanie - Použili sme K-Means a DBSCAN na zoskupenie neoznačených obrázkov do zoskupení a potom na základe členstva v zoskupení priradili pseudo-označenia. Semi-nadriadený tréning - Pre-trénovali sme CNN na 9 800 pseudo-označených obrázkov, potom jemne naladený na 200 skutočných štítkov, a porovnané proti iba dohliadané základnej línii. Analýza škálovania – Odhadli sme náklady na výpočet, ukladanie a označovanie pre 4 milióny obrázkov, čo potvrdzuje uskutočniteľnosť v rámci rozpočtu vo výške 5 000 EUR. Kľúčové takeaways: Predtrénovaná CNN môže extrahovať zmysluplné funkcie z akejkoľvek obrazovej domény, dokonca aj z tej, na ktorú nebola nikdy vyškolená. Zoskupovanie na vkladoch odhaľuje prirodzené zoskupenia, ktoré často zodpovedajú skutočným triedam. Pseudo-labely sú nedokonalé, ale model, ktorý je vopred vyškolený na nedokonalých etiketoch a potom jemne nastavený na skutočných etiketoch, prevyšuje model vyškolený len na skutočných etiketoch. Vzor funguje v rôznych oblastiach: lekárske zobrazovanie, priemyselná kontrola kvality, satelitné zobrazovanie, klasifikácia dokumentov a monitorovanie biodiverzity.Všade, kde sú štítky drahé a neoznačené údaje sú bohaté – čo je v roku 2025 takmer všade.