Table of Contents tabel konten Introduksi: Label mahal, gambar gratis Bagian 1 - Data Exploration: Memahami Data yang Anda Kerjakan Bagian 2 – Preprocessing: Berbicara Bahasa Model Bagian 3 — Ekstraksi Fitur: Mengubah Gambar Menjadi Angka yang Bermakna Bagian 4 - Kelompok tanpa pengawasan: Menemukan Struktur dalam Gelap Bagian 5 - Pelatihan Semisupervised: Eksperimen inti Bagian 6 — Meningkatkan ke Jutaan Gambar: Peta Jalan yang Realistis Kesimpulan Label mahal, gambar gratis Dalam dunia yang sempurna, setiap gambar dalam dataset Anda akan memiliki label. "Defective." "Normal." "Crack type A." "Scratch type B." Tapi di dunia nyata, labeling sangat mahal. seorang radiolog medis tunggal dapat label mungkin 50 pemindaian otak per jam - pada € 200 / jam. Seorang inspektur kualitas industri dapat menandai mungkin 100 bagian per jam. Pada skala, label 100.000 gambar dapat biaya lebih dari pelatihan model itu sendiri. Berikut adalah paradoks: perusahaan sering memiliki jutaan gambar (dari kamera, sensor, upload pengguna) tetapi hanya mampu menandai sebagian kecil. Alih-alih membuang gambar yang tidak diberi label, kami menggunakannya untuk meningkatkan model – menggabungkan set label kecil dengan set label besar. semi-supervised learning Biarkan saya menggunakan analogi. Bayangkan Anda seorang guru dengan 30 siswa. Anda memberi mereka semua ujian, tetapi Anda hanya memiliki waktu untuk mengevaluasi 5 kertas. Anda mengevaluasi 5 kertas dengan hati-hati, dan Anda memperhatikan pola: siswa yang menulis banyak cenderung mendapatkan skor tinggi, dan siswa yang meninggalkan jawaban kosong cenderung mendapatkan skor rendah. Menggunakan pola itu, Anda dapat memperkirakan peringkat 25 kertas lainnya tanpa membaca setiap baris. Itu adalah pembelajaran semi-pengawas: Anda menggunakan beberapa kertas yang dinilai (data yang dilabel) untuk memahami pola, kemudian menerapkan pola itu pada yang tidak dinilai (data yang tidak dilabel). Artikel ini membangun pipa klasifikasi gambar semi-terawasi lengkap dari awal. kita akan bekerja melalui setiap langkah dengan penjelasan rinci saat kita pergi. Studi kasus: mendeteksi cacat manufaktur pada permukaan logam. Sebuah pabrik memproduksi plat baja, dan kamera memotret masing-masing plat saat meluncur dari jalur produksi. Kebanyakan plat normal, beberapa memiliki cacat (keriput, retak, petting, inklusi). Kami memiliki 10.000 gambar, tetapi hanya 200 yang diberi label. Studi kasus : Sebuah pabrik memproduksi piring baja, dan kamera memotret setiap piring saat meluncur dari jalur produksi. sebagian besar piring normal, beberapa memiliki cacat (kerut, retak, pitting, inklusi). Kami memiliki 10.000 gambar, tetapi hanya 200 yang diberi label. detecting manufacturing defects on metal surfaces. Membangun Pipeline Pembelajaran Semisupervised Sebelum menyelam ke dalam kode apa pun, mari kita memetakan seluruh pipa. Memahami aliran pertama akan membuat setiap langkah terasa bertujuan daripada acak. Luangkan waktu untuk mempelajari diagram ini - setiap kotak adalah langkah yang akan kita implementasikan: 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] Key insight: kita akan mengubah gambar yang tidak ditandai menjadi Pikirkan tentang itu seperti memberi siswa panduan studi kasar sebelum ujian nyata - itu tidak akan sempurna, tetapi itu lebih baik daripada tidak ada. Sekitar Mari kita juga eksplisit tentang kata-kata yang akan kita gunakan sepanjang artikel ini, karena mencampur istilah adalah sumber kebingungan yang sangat umum: Merek (atau hanya "merek"): gambar dengan label yang dikonfirmasi oleh seorang ahli manusia. standar emas. Label yang lemah (atau "pseudo-labeled"): gambar yang labelnya ditebak oleh cluster. lebih murah tetapi lebih berisik. Unlabelled: gambar tanpa label sama sekali. ini adalah keadaan mereka sebelum kita mengkluster mereka. Embedding: ringkasan numerik yang kompak dari gambar, yang dihasilkan oleh jaringan saraf pra-latih. alat utama kami untuk membuat gambar yang sebanding. Baca lebih lanjut: Baca lebih lanjut: Pendekatan Pembelajaran Semi-Supervised — scikit-learn Penelitian Google tentang pembelajaran semi-supervised Pendekatan Pembelajaran Semi-Supervised — scikit-learn Penelitian Google tentang pembelajaran semi-supervised Data Exploration: memahami data yang sedang Anda kerjakan Mengapa Anda harus melihat data Anda sebelum melakukan apa pun Set data gambar memiliki mode kegagalan unik yang tidak akan Anda temukan dalam data tabel: file yang rusak yang menabrak lingkaran pelatihan Anda pada pukul 3 pagi, resolusi yang tidak konsisten yang secara diam-diam merusak gambar Anda, saluran warna yang salah (gray scale disguised sebagai RGB), dan ketidakseimbangan kelas ekstrim di mana 95% gambar "normal." Aturan yang Emas: Mari kita lihat apa yang kita hadapi. never trust data you haven't inspected. 1.1 - Mengunggah dan menghitung gambar Dataset kami diatur menjadi dua folder utama: satu dengan gambar yang ditandai (dibagi menjadi subfolder "normal" dan "defekt"), dan satu dengan gambar yang tidak ditandai (tidak ada subfolder - hanya koleksi datar dengan file) .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" Kami menggunakan bukannya konkatenasi string karena menangani pemisah jalur dengan benar pada sistem operasi apa pun. 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%}") Rasio label tersebut biasanya sekitar 2%. Hanya 2% dari data kami yang memiliki label yang terverifikasi oleh para ahli. 98% lainnya adalah tambang emas yang tidak dapat kita abaikan - dan itulah yang dimanfaatkan oleh pembelajaran semi-supervised. 1.2 — Pemindaian untuk masalah: resolusi, warna, korupsi Selanjutnya, kita perlu memeriksa setiap gambar secara individual. satu file yang rusak dapat menghancurkan seluruh pelatihan. resolusi yang tidak konsisten akan membingungkan gambar jika Anda tidak berhati-hati. dan mode warna yang salah (skala abu-abu ketika model Anda mengharapkan RGB) akan menghasilkan fitur nonsens. Kami menulis fungsi kecil yang mencoba untuk membuka setiap gambar dan mencatat sifatnya: 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, } yang Bidang ini sangat penting. berarti 3 saluran warna (merah, hijau, biru). artinya kelabu (1 saluran). berarti RGB dengan saluran alpha (transparensi). model pra-latih kami mengharapkan RGB, jadi kita harus mengkonversi semuanya nanti. img.mode 'RGB' 'L' 'RGBA' Sekarang mari kita memindai semua 10.000 gambar: all_files = labeled_files + unlabeled_files image_info = [get_image_info(f) for f in all_files] info_df = pd.DataFrame(image_info) Dan periksa hasilnya: 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}") Apa yang harus dicari dalam output: Gambar yang rusak > 0: Hapus mereka segera. bahkan satu file yang buruk dapat merusak pelatihan Anda. Resolusi yang berbeda: Jika min ≠ max untuk lebar atau tinggi, gambar memiliki ukuran yang berbeda. Berbagai mode warna: Jika Anda melihat 'RGB' dan 'L', Anda memiliki campuran warna dan skala abu-abu. Ukuran file yang ekstrim: Sebuah file 1KB mungkin kosong atau rusak. sebuah file 50MB mungkin tidak dikompres — layak untuk diperiksa. Untuk menghapus file yang rusak: 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 — Distribusi kelas: Apakah set label kami seimbang? Dalam pengaturan industri dan medis, cacat jarang terjadi. set label Anda mungkin 90% "normal" dan 10% "defekt." Ini sangat penting: model malas yang selalu memprediksi "normal" akan mendapatkan akurasi 90% sementara benar-benar tidak berguna. 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}%)") Jika ketidakseimbangan itu parah, kita akan mengatasinya kemudian menggunakan teknik yang disebut dalam fungsi hilangnya – pada dasarnya mengatakan pada model “melewatkan cacat 4x lebih buruk daripada alarm palsu.” pos_weight Mari kita juga memvisualisasikan: 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 — Visualisasi sampel gambar: selalu melihat sebelum Anda model Anda mungkin menemukan gambar yang jelas disalahartikan, memindai artefak (batasan hitam, rotasi), masalah kualitas (sembunyi, overexposure), atau bahwa cacat secara visual halus dan tugas Anda lebih sulit daripada yang Anda pikirkan. 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() Gunakan waktu untuk mempelajari gambar-gambar ini. melihat cacat dengan mata Anda sendiri? Jika Anda tidak bisa, model akan berjuang juga. Jika cacat jelas (keriput dalam, retak besar), itu mendorong - model harus dapat mempelajari perbedaan. Jika mereka halus (perubahan sedikit warna, retak garis rambut), Anda akan membutuhkan preprocessing dan ekstraksi fitur yang sangat baik. Anda Baca lebih lanjut: Baca lebih lanjut: Dokumen Pillow — perpustakaan gambar Python yang kita gunakan untuk mengunggah gambar NEU Surface Defect Database — dataset cacat permukaan baja dunia nyata yang dapat Anda praktekkan dengan Dokumentasi Pillow Informasi tentang Surface Defect Database Bagian 2 — Preprocessing: berbicara bahasa model Mengapa kita tidak bisa hanya memberi makan gambar mentah ke jaringan saraf Sebuah CNN pra-latih seperti ResNet50 dilatih pada jenis input yang sangat spesifik: 224×224 gambar piksel, dalam warna RGB, dinormalisasi dengan nilai rata-rata dan deviasi standar tertentu yang dihitung dari dataset ImageNet. ResNet50 "bercakap dengan ImageNet."Jika kita ingin memahaminya untuk memahami gambar permukaan logam kita, kita perlu "menerjemahkan" mereka ke dalam format ImageNet terlebih dahulu. Konversi ke RGB (3 saluran) Meningkatkan kontras melalui histogram equalization Ukuran : 224×224 Normalisasi nilai pixel untuk cocok dengan statistik ImageNet 2.1 - Apa itu histogram equalization, dan mengapa itu penting di sini? Perbedaan antara permukaan normal dan yang digoreng mungkin hanya beberapa tingkat intensitas pixel - tidak terlihat dengan mata telanjang, dan sangat sulit bagi model untuk mendeteksi. redistribusikan intensitas pixel sehingga rentang penuh (0 hingga 255) digunakan secara merata. Hasilnya: fitur halus "pop out" secara visual dan numerik. Histogram equalization Kami menggunakan versi yang lebih maju yang disebut (Contract Limited Adaptive Histogram Equalization). Tidak seperti globalisasi (yang menerapkan transformasi yang sama untuk seluruh gambar), CLAHE membagi gambar menjadi ubin kecil (8×8 secara default) dan menyeimbangkan setiap ubin secara independen. CLAHE Ini adalah analogi: bayangkan Anda menyesuaikan kecerahan pada foto. equalization global adalah seperti menggunakan slider kecerahan tunggal untuk seluruh gambar - Anda dapat menerangi sudut-sudut gelap tetapi mencuci pusat yang sudah cerah. CLAHE seperti menyesuaikan kecerahan di setiap wilayah secara independen, sehingga setiap bagian dari gambar menjadi jelas. 2.2 — Membangun Dataset PyTorch khusus PyTorch mengatur pengisian data di sekitar dua kelas: (Tahu cara mengisi satu item) dan (mengetahui cara mengumpulkan dan memindahkan barang-barang). PyTorch menyediakan Dataset, tetapi mengasumsikan setiap gambar memiliki label. Dataset kami memiliki gambar yang ditandai dan tidak ditandai, jadi kami membutuhkan kelas kustom. Dataset DataLoader ImageFolder Mari kita membangunnya langkah demi langkah. pertama, kerangka: 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 mengatakan berapa banyak gambar yang kita miliki. menyimpan jalur gambar dan (opsi) label mereka. __len__ __init__ Sekarang metode dasarnya, , yang mengunggah dan memproses gambar tunggal. kita akan memecahkannya menjadi tiga tahap: __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") Mengapa Karena ResNet50 mengharapkan 3 saluran. jika gambar kita adalah skala abu-abu (1 saluran), ini menggandakan nilai abu-abu menjadi R, G, dan B. Jika itu sudah RGB, itu tidak melakukan apa-apa. .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) Mengapa LAB dan tidak hanya menerapkan CLAHE secara langsung pada RGB? Karena jika Anda menyeimbangkan saluran R, G, dan B secara independen, Anda akan membingungkan warna – mengubah biru menjadi hijau, misalnya. 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 — Pipa transformasi: apa yang dilakukan setiap langkah Sekarang kita mendefinisikan urutan transformasi. masing-masing memiliki tujuan tertentu: 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], ), ]) Mari kita jelaskan setiap langkah: Arsitektur ResNet50 membutuhkan ukuran ini persis.Jika Anda memasukkan gambar 300×400, bentuk tensor tidak akan cocok dan PyTorch akan crash.Resizing dapat merusak rasio aspek sedikit, tetapi untuk tugas berbasis tekstur (seperti deteksi cacat) ini jarang menjadi masalah. (1) Resize to 224×224. Melakukan dua hal: mengkonversi format pixel dari HWC (Height × Width × Channels) ke CHW (Channels × Height × Width), yang merupakan apa yang diharapkan PyTorch; dan mengukur nilai pixel dari [0, 255] integer ke [0.0, 1.0] floats. (2) ToTensor. Angka ajaib ini - dan — adalah kesenjangan rata-rata dan standar nilai pixel di seluruh ImageNet dataset, dihitung saluran per saluran (R, G, B). ResNet50 dilatih dengan nilai normalisasi yang tepat ini, sehingga berat internal "harapkan" input berpusat di sekitar 0 dengan varians unit. (3) Normalize with ImageNet mean and std. [0.485, 0.456, 0.406] [0.229, 0.224, 0.225] 2.4 - Membuat Datasets dan DataLoaders Sekarang kita mengumpulkan semuanya. prinsip kritis: Menggabungkan mereka akan merusak evaluasi kami. labeled and unlabeled data must be kept strictly separate at all times. Pertama, kumpulkan jalur gambar yang ditandai dan label kelas mereka: 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 Kemudian, kumpulkan jalur gambar yang tidak ditandai (tidak diperlukan label): unlabeled_paths = [str(fp) for fp in unlabeled_files] Menggunakan PyTorch Dataset: 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 DataLoader batch gambar bersama-sama (batch_size=32 berarti 32 gambar pada satu waktu) dan secara opsional shuffles mereka: labeled_loader = DataLoader(labeled_dataset, batch_size=32, shuffle=False) unlabeled_loader = DataLoader(unlabeled_dataset, batch_size=32, shuffle=False) Mengapa Karena kami akan mengekstrak fitur, dan kami membutuhkan embeddings untuk tetap dalam urutan yang sama dengan daftar file kami. shuffle=False Baca lebih lanjut: Baca lebih lanjut: Transformer - Daftar Lengkap CLAHE menjelaskan (OpenCV tutorial) Aplikasi PyTorch Dataset & DataLoader Transformer - Daftar Lengkap CLAHE menjelaskan (OpenCV tutorial) Aplikasi PyTorch Dataset & DataLoader Bagian 3 — Ekstraksi fitur: mengubah gambar menjadi angka yang berarti 3.1 — Mengapa pixel mentah adalah representasi yang mengerikan Sebuah gambar RGB 224×224 memiliki 150,528 angka (224 × 224 × 3 saluran). Sebagian besar dari mereka adalah kebisingan — variasi kecil dalam pencahayaan, artefak sensor, artefak kompresi. Lebih buruk: dua foto yang sama persis, diambil dari sudut yang sedikit berbeda atau pencahayaan, memiliki nilai piksel yang sama sekali berbeda. Apa yang kita butuhkan adalah representasi yang menangkap dari gambar - "ini terlihat seperti goresan," "ini adalah permukaan halus" - dalam bentuk numerik yang kompak dan stabil. melakukan meaning embeddings Apa itu model pra-latih dan mengapa kita tidak melatih dari awal Latihan jaringan saraf mendalam dari awal membutuhkan banyak data – biasanya ratusan ribu gambar. kita memiliki 200 gambar yang diberi label. jika kita mencoba untuk melatih 25 juta parameter ResNet50 pada 200 gambar, model akan mengingat setiap gambar pelatihan dengan sempurna tetapi gagal sepenuhnya pada gambar baru. . overfitting Sebagai gantinya, kami menggunakan model yang sudah dilatih di ImageNet — sebuah dataset dari 14 juta gambar di 1.000 kategori (anjing, kucing, mobil, bangunan, dll.). model ini telah belajar untuk mengenali fitur visual dasar: tepi, tekstur, bentuk, gradient warna, pola geometris. - mereka berlaku untuk permukaan baja sama seperti mereka berlaku untuk kucing. universal Pikirkan seperti mempekerjakan fotografer yang berpengalaman untuk memeriksa pabrik Anda. mereka belum pernah melihat plat baja sebelumnya, tetapi mereka sudah tahu bagaimana : mereka dapat melihat tekstur yang tidak biasa, perubahan mendadak dalam kualitas permukaan, pola yang melanggar norma. Lihat 3.3 — Loading ResNet50 Mari kita simak model yang sudah disiapkan: import torchvision.models as models resnet = models.resnet50(weights=models.ResNet50_Weights.IMAGENET1K_V1) Satu baris ini mengunduh (untuk pertama kalinya) model ResNet50 yang beratnya telah dilatih di ImageNet. model ini tahu bagaimana mengklasifikasikan 1.000 kategori objek sehari-hari. 3.4 – Membekukan parameter Kami tidak ingin mengubah salah satu fitur yang dipelajari. kami menggunakan ResNet sebagai alat yang hanya dapat dibaca: for param in resnet.parameters(): param.requires_grad = False PyTorch mengatakan "jangan menghitung gradient untuk parameter ini." Ini memiliki dua keuntungan: itu mencegah modifikasi acak dari beban pra-latih, dan itu membuat inferensi lebih cepat (tidak ada pelacakan gradient = kurang komputasi dan kurang memori). requires_grad = False 3.5 - Menghapus kepala klasifikasi Arsitektur ResNet50 terlihat seperti ini: 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) Kami tidak ingin lapisan Terhubung sepenuhnya terakhir, karena itu spesifik untuk 1000 kelas ImageNet (anjing, kucing, pesawat...) dan tidak berguna untuk tugas kami. feature_extractor = torch.nn.Sequential(*list(resnet.children())[:-1]) Apa yang dilakukan garis ini: Mengembalikan semua lapisan ResNet sebagai daftar. Semua yang ada di dalamnya, kecuali yang terakhir. mengembalikannya kembali ke sebuah model. resnet.children() [:-1] Sequential(*) feature_extractor.eval() mode menonaktifkan drop-out dan menggunakan statistik yang berjalan untuk normalisasi batch. output – gambar yang sama selalu menghasilkan embedding yang sama. 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}") Menggunakan GPU (jika tersedia) membuat ekstraksi fitur sekitar 10x lebih cepat. 3.6 - Fungsi ekstraksi Sekarang mari kita menulis fungsi yang memberi makan batch gambar melalui ekstraktor fitur dan mengumpulkan embeddings. Struktur eksternal : 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 = [] Kami mengumpulkan hasil dalam daftar karena kami memproses gambar dalam batch (32 pada satu waktu), tidak semua sekaligus (tidak akan cocok dalam memori GPU). Langkah utama loop: with torch.no_grad(): for batch_images, batch_labels in dataloader: batch_images = batch_images.to(device) features = model(batch_images) menonaktifkan pelacakan gradient – penting karena kita hanya melakukan inferensi, bukan pelatihan. Memindahkan gambar ke GPU jika ada. torch.no_grad() batch_images.to(device) Produksi dari Memiliki bentuk - dua dimensi terakhir adalah sisa-sisa spasial dari gabungan rata-rata. kita perlu memerasnya: model(batch_images) (batch_size, 2048, 1, 1) features = features.squeeze(-1).squeeze(-1) # Now shape is (batch_size, 2048) — that's our embedding Akhirnya, kami memindahkan hasil kembali ke CPU (numpy tidak dapat bekerja dengan tensor GPU) dan menyimpannya: all_embeddings.append(features.cpu().numpy()) all_labels.append(batch_labels.numpy()) return np.concatenate(all_embeddings), np.concatenate(all_labels) Menggabungkan semua batch ke dalam satu array. np.concatenate 3.7 - Mengoperasikan ekstraksi 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 gambar, masing-masing diwakili oleh vektor 2048-dimensi. yang merupakan kompresi 73x dibandingkan dengan piksel mentah (150,528 → 2,048) — dan representasi yang dikompresi adalah lebih bermakna jauh print("Extracting embeddings for unlabeled images...") unlabeled_embeddings, _ = extract_embeddings( unlabeled_loader, feature_extractor, device ) print(f" Shape: {unlabeled_embeddings.shape}") # Expected: (9800, 2048) Kami membuang label (mereka semua -1 anyway) dengan yang variabel. _ 3.8 - Menghemat embeddings (jangan mengekstrak ulang setiap kali!) Ekstraksi fitur adalah langkah yang paling mahal – berpotensi lebih dari 30 menit pada GPU untuk 10.000 gambar. simpan hasilnya sehingga Anda tidak perlu melakukannya lagi: 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") Setelah itu, Anda dapat langsung mengunduh . np.load("data/labeled_embeddings.npy") 3.9 — Pemeriksaan sanitas: apakah embeddings masuk akal? Sampah masuk, sampah keluar – mari kita pastikan embeddings kita sehat: 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()}") Apa yang diharapkan: berarti sekitar 0.3-0.5, std sekitar 0.5-1.0, tidak ada NaN, tidak ada Inf. Jika Anda melihat nilai NaN, gambar yang rusak mungkin meluncur melalui langkah pembersihan. Pergi lebih jauh: Pergi lebih jauh: Pembelajaran Transfer yang dijelaskan (PyTorch tutorial) ResNet paper (He et al., 2015) — arsitektur asli yang memperkenalkan koneksi skip Fitur ekstraksi vs fine-tuning — Stanford CS231n catatan kursus Pembelajaran Transfer yang dijelaskan (PyTorch tutorial) Penelitian yang dilakukan (He et al., 2015) Fitur ekstraksi vs fine-tuning Bagian 4 - Kelompok tanpa pengawasan: menemukan struktur dalam kegelapan 4.1 — Apa clustering dan mengapa kita membutuhkannya Kami sekarang memiliki 10.000 embeddings - ringkasan numerik kompak dari setiap gambar. 200 dari mereka memiliki label. yang lain 9.800 tidak. dalam data yang diharapkan sesuai dengan “normal” dan “defekt”. groupings Asumsi dasar: jika embeddings kami baik (dan ResNet50 embeddings biasanya), gambar dari jenis yang sama akan permukaan normal akan berkumpul bersama; permukaan cacat akan berkumpul bersama. bahkan tanpa label, struktur ada di sana - berkumpul mengungkapkannya. dekat Tetapi pertama, masalah praktis: 2048 dimensi tidak mungkin untuk memvisualisasikan dan membuat beberapa algoritma lambat. 4.2 - Standarisasi embeddings Algoritma kluster, terutama K-Means, menghitung jarak antara titik.Jika satu dimensi berkisar dari 0 hingga 1000 dan yang lain dari 0 hingga 0,01, yang pertama akan sepenuhnya mendominasi jarak – seolah-olah dimensi kedua tidak ada. 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) Mengapa mengstandardisasi label dan non-label bersama-sama? karena mereka berasal dari distribusi yang sama (pabrik yang sama, kamera yang sama). # 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 — Mengurangi dimensi dengan PCA PCA (Principal Component Analysis) menemukan arah varians maksimum dalam data dan proyek ke arah atas. kita pergi dari 2048 ke 50 dimensi: 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% varians dipertahankan berarti kita hanya membuang 5% dari informasi tetapi mengurangi dimensionalitas 40x. Mari kita juga membayangkan berapa banyak masing-masing komponen berkontribusi: 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() "Plot elang" ini menunjukkan di mana menambahkan lebih banyak komponen berhenti memberikan keuntungan yang signifikan. 4.4 — Visualisasi dengan t-SNE t-SNE adalah teknik pengurangan dimensi nonlinear yang dirancang khusus untuk visualisasi. gambar yang dekat di ruang dimensi tinggi akan dekat di plot 2D. Ini membuatnya sempurna untuk memeriksa apakah gambar normal dan cacat secara alami terpisah. local structure Satu peringatan penting: t-SNE membingungkan jarak global — ruang antara cluster tidak berarti. gunakan hanya untuk visualisasi, dan cluster pada embeddings asli (atau PCA-reduced). 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) yang parameter kira-kira mengendalikan "ukuran lingkungan" — berapa banyak titik dekat t-SNE mempertimbangkan. 30 adalah default yang wajar untuk kumpulan data ukuran kita. perplexity Sekarang mari kita membagi koordinat t-SNE: labeled_tsne = all_tsne[:len(labeled_embeddings)] unlabeled_tsne = all_tsne[len(labeled_embeddings):] Untuk visualisasi : 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() Jika Anda melihat dua awan yang berbeda di plot ini - hijau di satu sisi, merah di sisi lain - itu adalah tanda yang sangat baik. # 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() Dalam plot yang benar, awan abu-abu (gambar yang tidak dilabel) harus meliputi titik-titik berwarna. ini mengkonfirmasi bahwa gambar yang dilabel dan yang tidak dilabel berasal dari distribusi yang sama - kondisi yang diperlukan untuk pembelajaran semi-pengawas untuk bekerja. 4.5 – K-Means mengklusur K-Means adalah algoritma clustering yang paling sederhana dan paling banyak digunakan, yang membagi data menjadi kelompok-kelompok k secara iteratif dengan mengalokasikan setiap titik ke pusat cluster terdekat, kemudian memperbarui pusat-pusat. Karena kita tahu kita memiliki 2 kelas (normal dan cacat), kita mulai dengan k = 2. tetapi kita juga menguji k = 3, 4, 5 untuk memeriksa apakah data mungkin memiliki lebih banyak struktur (misalnya, berbeda). dari kelemahan-kelemahan yang membentuk kelompok terpisah). Jenis Untuk mengevaluasi seberapa baik cluster cocok dengan label nyata, kita menggunakan ARI = 1.0 berarti kesepakatan sempurna dengan label yang benar. ARI = 0.0 berarti clustering acak. ARI < 0 berarti lebih buruk daripada acak. 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}") yang parameter berarti K-Means akan berjalan 10 kali dengan inisialisasi acak yang berbeda dan mempertahankan hasil terbaik. n_init=10 Jika k=2 memberikan ARI tertinggi, yang mengkonfirmasi data kami memiliki dua kelompok alami yang sejalan dengan normal vs cacat. 4.6 — Clustering DBSCAN (metode alternatif) DBSCAN bekerja sangat berbeda dari K-Means. alih-alih menentukan jumlah cluster, Anda menentukan dua parameter: eps (epsilon): jarak maksimum antara dua titik untuk mereka dianggap tetangga. berpikir tentang itu sebagai "seberapa dekat cukup dekat?" min_samples: jumlah minimum poin yang diperlukan untuk membentuk wilayah padat (kluster). pikirkan tentang hal itu sebagai "seberapa padatnya lingkungan harus dihitung sebagai cluster?" DBSCAN secara otomatis menentukan jumlah cluster dan mengidentifikasi outliers (titik yang tidak termasuk ke dalam cluster apa pun - ditandai sebagai -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}") Kita perlu menguji beberapa kombinasi parameter karena nilai "benar" tergantung pada data: 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}") Perhatikan bahwa kami menggunakan data PCA-reduced ( DBSCAN berjuang dalam dimensi yang sangat tinggi karena semua jarak menjadi serupa (setan dimensi). all_pca Bandingkan ARI terbaik dari DBSCAN dengan yang terbaik dari K-Means, dan pilih pemenang. 4.7 — Visualisasi cluster pada plot t-SNE Mari kita lihat bagaimana clustering terbaik terlihat pada visualisasi t-SNE kami: 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() Jika dua warna dalam plot ini kira-kira cocok dengan dua kelompok yang Anda lihat di plot t-SNE yang ditandai, clustering bekerja. 4.8 - Menugaskan pseudo-label pada gambar yang tidak dilabel Sekarang langkah penting: kita mengambil atribut cluster dan memperlakukan mereka sebagai "label lemah" untuk gambar yang tidak ditandai. tetapi ada satu kesederhanaan - K-Means mengalokasikan ID cluster secara arbitrase. Cluster 0 mungkin sesuai dengan "kelemahan" atau "normal." Menggunakan pseudo-label tersebut : unlabeled_pseudo_labels = all_cluster_ids[len(labeled_embeddings):] Periksa penyesuaian dengan label nyata: 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'") Jika cluster 0 sebagian besar cacat (normal_rate < 50%), kita membalikkan pemetaan: 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)") Mari kita lihat distribusi: 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") Sekarang kita memiliki dua dataset terpisah dengan karakteristik yang sangat berbeda: Dengan label yang kuat — 200 gambar dengan label ahli nyata. kualitas tinggi, kuantitas kecil. Kurang ditandai — 9.800 gambar dengan pseudo-label berbasis cluster. Kualitas yang lebih rendah (beberapa label salah), tetapi jumlah yang besar. Aturan yang Emas: Mereka melayani tujuan yang berbeda di langkah berikutnya. never mix these two. Baca lebih lanjut: Baca lebih lanjut: K-Means yang dijelaskan (scikit-learn) DBSCAN menjelaskan (scikit-learn) Cara membaca t-SNE dengan benar (Distill) — bacaan penting K-Means yang dijelaskan (scikit-learn) DBSCAN menjelaskan (scikit-learn) Cara Membaca T-SNE dengan Benar (Distill) Bagian 5 - Pelatihan Semisupervised: Eksperimen yang sebenarnya 5.1 — Logika di balik pendekatan dua tahap kami Ini adalah intuisi. bayangkan Anda melatih inspektur kualitas baru di pabrik: : Anda menunjukkan kepada mereka 9.800 foto dan mengatakan "Saya ini normal dan ini cacat, tetapi saya tidak yakin 100%." Inspektor mulai membentuk model mental kasar. Beberapa label salah, tetapi pola keseluruhan - permukaan normal halus dan seragam, permukaan cacat memiliki ketidakaturan - sebagian besar benar. . Phase 1 (pre-training on pseudo-labels) berpikir Intuisi Anda kemudian menunjukkan kepada mereka 200 foto yang telah diperiksa dengan hati-hati oleh seorang ahli: "Ini benar-benar normal, dan ini pasti cacat." Phase 2 (fine-tuning on real labels) Hasilnya, seorang petugas yang memiliki 10.000 gambar (mengembangkan intuisi yang luas) dan telah dengan 200 contoh yang terverifikasi oleh ahli (menjamin keakuratan). kami mengharapkan inspektor ini untuk melampaui yang hanya pernah melihat 200 contoh yang terverifikasi. melihat Kalibrasi Untuk membuktikan hal ini, kami menjalankan dua eksperimen paralel: Eksperimen A — Hanya dipantau: pelatihan hanya pada 200 gambar yang diberi label Eksperimen B – Semi-pengawasan: pra-latihan pada 9.800 gambar pseudo-label, kemudian tuning halus pada 200 gambar label Arsitektur model yang sama, set tes yang sama. satu-satunya perbedaan adalah apakah model melihat data yang tidak ditandai atau tidak. 5.2 — Membangun klasifikator: arsitektur Kami menggunakan ResNet50 sebagai tulang belakang lagi, tetapi kali ini kami tingkat akhir dengan klasifikator biner dan kita Hal ini berbeda dengan bagian ketiga (di mana kami hanya mengekstrak fitur). 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 Di sini kita mengganti kepala klasifikasi ImageNet asli (2048 → 1000 kelas) dengan kita sendiri: 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) Mengapa Dengan hanya 200 gambar yang ditandai dan 25 juta parameter, overfitting adalah ancaman utama. Dropout secara acak menonaktifkan 50% neuron selama setiap tahap pelatihan, memaksa jaringan untuk mempelajari representasi redundant. Pada waktu inferensi, semua neuron aktif. Dropout(0.5) Mengapa Untuk klasifikasi biner, satu neuron output dengan aktivasi sigmoid secara matematis setara dengan dua neuron dengan softmax, tetapi lebih sederhana dan sedikit lebih stabil secara numerik. Linear(2048, 1) 5.3 - Fungsi kerugian: menangani ketidakseimbangan kelas Sebelum menulis lingkaran pelatihan, mari kita membahas fungsi kerugian. (Binary Cross-Entropy with Logits), yang menggabungkan aktivasi sigmoid dengan cross-entropy biner dalam satu operasi yang stabil secara numerik. BCEWithLogitsLoss Suplemen utama adalah : pos_weight pos_weight = torch.tensor([4.0]).to(device) criterion = nn.BCEWithLogitsLoss(pos_weight=pos_weight) Apa yang dilakukan Ini memberi tahu fungsi kerugian: "defekt yang hilang (negatif palsu) harus dihukum Ini mengkompensasi ketidakseimbangan kelas. tanpa itu, model dapat mencapai akurasi 80% dengan selalu memprediksi "normal" - yang tidak berguna. pos_weight=4.0 4 times more Nilai 4.0 adalah perkiraan kasar berdasarkan rasio kelas. Jika Anda memiliki 80% normal / 20% cacat, maka Anda dapat menyesuaikan nilai ini, tetapi 4.0 adalah titik awal yang baik. pos_weight = 80/20 = 4.0 5.4 — Optimizer: AdamW dengan penurunan berat badan import torch.optim as optim optimizer = optim.AdamW(model.parameters(), lr=lr, weight_decay=1e-4) Mengapa AdamW? itu Adam dengan penurunan berat badan yang tepat (L2 regulasi). lembut menghukum beban besar, yang merupakan lapisan lain dari perlindungan terhadap overfitting. pikirkan tentang hal itu seperti mengatakan model "mencintai penjelasan yang lebih sederhana." weight_decay=1e-4 5.5 – Jadwal Tingkat Belajar scheduler = optim.lr_scheduler.ReduceLROnPlateau(optimizer, patience=3, factor=0.5) Ini secara otomatis mengurangi tingkat belajar ketika kehilangan validasi berhenti membaik. Ini berarti "tunggu 3 era tanpa perbaikan sebelum mengurangi." Ini sangat penting untuk konvergensi – karena model ini mendekati minimum, langkah-langkah yang lebih kecil mencegah overshooting. patience=3 factor=0.5 5.6 - Lingkaran pelatihan: satu era pada satu waktu Sekarang mari kita membangun fungsi pelatihan penuh. kita akan melalui setiap bagian dari lingkaran secara terpisah. (satu era - satu lulus melalui semua data pelatihan): 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 Setiap batch melewati siklus klasik: forward pass → compute loss → backpropagate gradients → update weights → reset gradients untuk batch berikutnya. (Setelah setiap periode pelatihan): 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()) Perhatikan yang Ini penting: itu menonaktifkan drop-out (semua neuron aktif) dan menggunakan statistik berjalan untuk normalisasi batch alih-alih statistik batch. 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 Kami melacak Model sering memuncak sebelum akhir pelatihan (setelah itu mereka mungkin sedikit melebihi). best 5.7 – Mempersiapkan pemisahan data Kami membagi data yang ditandai menjadi tren (70%) dan tes (30%). : tidak pernah digunakan untuk pelatihan di kedua eksperimen. stratifikasi memastikan rasio kelas dipertahankan di kedua split. 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, ) Buatlah tiga data yang kita butuhkan: # 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, ) dan data yang dikumpulkan: 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") Perhatikan ukuran batch yang berbeda: 16 untuk set label kecil (kurang gambar per era), 32 untuk set label lemah besar (pengolahan yang lebih cepat). untuk pelatihan untuk mencegah model belajar urutan sampel. shuffle=True 5.8 — Eksperimen A: hanya diawasi (baseline) Ini adalah percobaan yang lebih sederhana. kami melatih model baru menggunakan HANYA 140 gambar pelatihan yang diberi label (60 lainnya disediakan untuk pengujian). 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 — Eksperimen B: semi-pengawasan (pendekatan dua tahap) Fase 1 memberi model intuisi luas dari 9.800 gambar pseudo-label. Fase 2 mempercepatnya dengan 140 label nyata. 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" ) Kami hanya melatih selama 10 era di sini karena pseudo-label berisik. pelatihan terlalu lama pada label berisik akan memperkuat kesalahan. 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" ) Perhatikan yang (5e-5 vs 1e-4 di fase 1). Ini adalah sengaja dan penting. Jika kita menggunakan tingkat belajar yang tinggi selama tuning halus, model akan dengan cepat "lupa" semua yang telah dipelajari selama pra-latihan - gradient akan terlalu besar dan akan menuliskannya berat yang telah dilatih sebelumnya. tingkat belajar yang lembut memungkinkan model untuk melakukan koreksi kecil pada pengetahuan yang ada, mempertahankan pola yang luas dari fase 1 sambil memperbaiki kesalahan dengan label nyata. lower learning rate Ini mirip dengan inspektur pabrik: Anda tidak memulai pelatihan mereka dari awal di fase 2. 5.10 — Penilaian akhir: saat kebenaran Sekarang kami mengevaluasi kedua model pada set tes yang sama dengan beberapa metrik. Pertama, fungsi evaluasi : 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()) Kami mengumpulkan keduanya (untuk AUC-ROC, yang mengukur kualitas peringkat) dan (untuk F1, yang mengukur kualitas klasifikasi pada ambang 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 Mengapa Karena dengan kelas yang tidak seimbang, akurasi menyesatkan. model yang selalu memprediksi "normal" mendapatkan akurasi 80% tetapi 0% pengingat pada cacat. F1 adalah rata-rata harmonis dari akurasi dan pengingat - itu menghukum model yang mengabaikan kelas minoritas. F1 Mengapa Ini mengukur seberapa baik model gambar (gambar yang cacat harus mendapatkan probabilitas yang lebih tinggi daripada yang normal), terlepas dari ambang klasifikasi.AUC 1.0 berarti peringkat sempurna; 0.5 berarti acak. AUC-ROC rangkaian Sekarang perbandingan : 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" ) Dan putusan terakhir: 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}") Jika kolom Delta menunjukkan angka positif, kami telah membuktikan bahwa data yang tidak ditandai berguna. pseudo-label, meskipun tidak sempurna, memberi model head start bahwa pengawasan murni pada 200 gambar tidak bisa cocok. 5.11 Interpretasi hasil Berikut cara membaca perbandingan: F1 ditingkatkan dengan +0.05 atau lebih: kemenangan jelas untuk semi-pengawasan. data yang tidak ditandai memberikan sinyal yang berarti. F1 ditingkatkan dengan +0.01 ke +0.04: perbaikan sederhana. semi-pengawasan membantu tetapi margin kecil. pertimbangkan untuk meningkatkan kualitas cluster atau menggunakan pseudo-labeling yang lebih canggih. F1 tidak berubah atau lebih buruk: pseudo-label terlalu berisik untuk membantu, atau cluster tidak menangkap struktur yang sebenarnya. cobalah ekstraktor fitur yang berbeda, algoritma cluster yang berbeda, atau ambang keyakinan yang lebih tinggi untuk pseudo-label. Baca lebih lanjut: Baca lebih lanjut: Dokumen Pseudo-Labeling (Lee, 2013) — pendekatan asli Survei pembelajaran semi-pengawasan (van Engelen & Hoos) — gambaran keseluruhan metode Pelatihan PyTorch Best Practices Buku tentang Pseudo-Labeling (Lee, 2013) Survei Pembelajaran Semisupervised (van Engelen & Hoos) Pelatihan PyTorch Best Practices Bagian 6 — Meningkatkan hingga jutaan gambar: peta jalan yang realistis Pertanyaan dari bisnis "Bukti konsep Anda bekerja pada 10.000 gambar. Kami memiliki 4 juta gambar untuk diproses. Dapatkah kami mengukur pipa ini dengan anggaran € 5.000?" Ini adalah pertanyaan yang akan Anda hadapi dalam setiap proyek nyata. mari kita memecahkannya dengan jujur. Biaya Komputer Pada 10.000 gambar kami dengan GPU tunggal, itu memakan waktu sekitar 30 menit. Feature extraction 4,000,000 images ÷ 10,000 images × 30 min = 12,000 min = 200 GPU-hours Pada ~€2/jam untuk instansi GPU cloud (Azure NC-series dengan GPU T4 atau A10), itu tentang . €400 K-Means standar mengunggah semua data ke memori untuk menghitung jarak. Dengan embeddings 4M dari 2048 dimensi (setiap float 4 byte): Clustering 4,000,000 × 2,048 × 4 bytes = ~32 GB just for the embeddings Itu tidak akan cocok dalam RAM pada sebagian besar mesin. Solusi: gunakan dari scikit-learn, yang memproses data dalam potongan (seperti) 10.000 sampel pada satu waktu. MiniBatchKMeans Pre-training pada 4M pseudo-labeled gambar membutuhkan sekitar 50 jam GPU . CNN training €100 Biaya Penyimpanan gambar mentah: 4M × ~50 KB rata-rata = Embeddings: 4M × 2048 × 4 byte = Pada Azure Blob Storage pada ~€0.02/GB/bulan, itu tentang . 200 GB 32 GB €5/month Strategi Labeling Jika 200 label tidak cukup dalam skala, kita bisa menandai lebih banyak. pada ~ € 1 per gambar (termasuk kontrol kualitas), 2.000 lagi label akan biaya Tetapi ada pendekatan yang lebih cerdas: . €2,000 active learning Aktif belajar memungkinkan model untuk memilih Daripada secara acak memilih 2.000 gambar, model ini mengidentifikasi yang paling tidak pasti – gambar yang akan mengajarkannya paling banyak. yang Dengan pembelajaran aktif, kita mungkin hanya membutuhkan 500 label tambahan daripada 2.000 . €500 Perkiraan total anggaran Feature extraction (GPU): €400 CNN training (GPU): €100 Storage (year 1): €60 Additional labeling: €500 – €2,000 ────────────────────────────────────── TOTAL: €1,060 – €2,560 Baik dalam anggaran € 5.000, dengan ruang untuk bereksperimen dan berlanjut. 5 Syarat Untuk Sukses Gunakan GPU cloud, bukan perangkat keras lokal. sewa per jam, bayar hanya untuk apa yang Anda gunakan. Gunakan MiniBatchKMeans alih-alih KMeans biasa. kualitas yang sama, memori 100x kurang. Buat pipa data yang tepat dengan pengolahan batch.Jangan pernah memuat gambar 4M ke RAM sekaligus.Gunakan PyTorch DataLoader dengan num_workers > 0 untuk memuat secara paralel. Pertimbangkan belajar aktif untuk memaksimalkan nilai setiap gambar yang diberi label manusia. Store dan versi embeddings, tidak hanya gambar mentah. re-ekstraksi embeddings 4M biaya € 400; mengunggah embeddings disimpan tidak biaya apa-apa. Baca lebih lanjut: Baca lebih lanjut: MiniBatchKMeans (scikit-learn) — how to label smarter, not more Active learning overview MiniBatchKMeans (sikit belajar) Aktivitas Pembelajaran Overview Kesimpulan Anda mengambil struktur yang tersembunyi dalam data yang tidak ditandai (melalui embeddings dan clustering), mengubahnya menjadi label kira-kira, dan menggunakannya untuk memberi model yang diawasi Anda awal. data yang tidak ditandai tidak menggantikan label nyata - itu melengkapi mereka. Mari kita ulas seluruh pipa yang kami bangun: Eksplorasi – Kami memindai 10.000 gambar untuk korupsi, format yang tidak konsisten, dan ketidakseimbangan kelas. Preprocessing – Kami telah menyederhanakan setiap gambar ke format ResNet50 mengharapkan: 224×224, RGB, CLAHE-meningkatkan, ImageNet-normalized. Ekstraksi fitur – Kami menggunakan ResNet50 yang diprediksi untuk mengubah setiap gambar menjadi embedding 2048-dimensi yang menangkap esensi visualnya. Clustering — Kami menerapkan K-Means dan DBSCAN untuk mengklasifikasikan gambar yang tidak dilabel ke dalam cluster, lalu mengalokasikan pseudo-label berdasarkan keanggotaan cluster. Pelatihan semi-pengawas - Kami mempelajari CNN pada 9.800 gambar yang ditandai dengan nama palsu, kemudian disesuaikan dengan 200 label nyata, dan membandingkan dengan garis awal yang hanya diawasi. Analisis skala - Kami memperkirakan biaya komputasi, penyimpanan, dan label untuk 4M gambar, mengkonfirmasi kelayakan dalam anggaran € 5.000. Keunggulan dari Takeaways: Kelompok pada embeddings mengungkapkan kelompok alami yang sering sesuai dengan kelas nyata. Pseudo-label tidak sempurna, tetapi model yang dilatih sebelumnya pada label yang tidak sempurna dan kemudian disesuaikan dengan baik pada label nyata melampaui model yang dilatih hanya pada label nyata. Dan pendekatan semi-supervised paling berharga tepat ketika label jarang dan mahal – yang merupakan situasi yang akan Anda hadapi dalam sebagian besar proyek dunia nyata. Pattern ini bekerja di berbagai bidang: pencitraan medis, kontrol kualitas industri, gambar satelit, klasifikasi dokumen, dan pemantauan keanekaragaman hayati. di mana-mana label mahal dan data yang tidak ditandai berlimpah - yang, pada tahun 2025, hampir di mana-mana.