Table of Contents বিষয়বস্তু টেবিল প্রবর্তন: লেবেল ব্যয়বহুল, ছবি বিনামূল্যে অংশ 1 - ডেটা অনুসন্ধান: আপনি যে ডেটা দিয়ে কাজ করছেন তা বোঝা অংশ 2 - প্রাক-প্রসেসিং: মডেলের ভাষা কথা বলতে অংশ 3 — বৈশিষ্ট্য আউটপুট: ইমেজগুলি গুরুত্বপূর্ণ সংখ্যায় রূপান্তর অংশ 4 - অননুমোদিত ক্লাস্টারিং: অন্ধকারে গঠন আবিষ্কার অংশ 5 - অর্ধ-নিরীক্ষিত প্রশিক্ষণ: মৌলিক পরীক্ষা অংশ 6 - মিলিয়ন ইমেজে স্ক্যালিং: একটি বাস্তব মানচিত্র উপসংহার লেবেল ব্যয়বহুল, ছবি বিনামূল্যে একটি নিখুঁত পৃথিবীতে, আপনার ডাটা সেটের প্রতিটি ইমেজ একটি লেবেল থাকবে. "ভুল", "স্বাভাবিক", "এ-টাইপ ক্র্যাক", "বি-টাইপ স্ক্র্যাচ" কিন্তু বাস্তব জগতে, লেবেলিং অত্যন্ত ব্যয়বহুল। একটি একক মেডিকেল রেডিওলজিস্ট প্রতি ঘন্টায় সম্ভবত 50 টি মস্তিষ্ক স্ক্যান লেবেল করতে পারে - ২০০ ইউরো / ঘন্টা। এখানে বিভ্রান্তি: কোম্পানির প্রায়শই মিলিয়ন ইমেজ (ক্যামেরা, সেন্সর, ব্যবহারকারীর আপলোড থেকে) থাকে তবে শুধুমাত্র একটি ছোট ছোট অংশকে লেবেল করতে সক্ষম। অ-লিম্পিত ইমেজগুলি ছাড়ার পরিবর্তে, আমরা তাদের মডেল উন্নত করার জন্য ব্যবহার করি - একটি ছোট লেবেল করা সেট এবং একটি বড় অ-লিম্পিত সেটকে একত্রিত করে। semi-supervised learning আমাকে একটি অনুরূপ ব্যবহার করুন. কল্পনা করুন আপনি 30 শিক্ষার্থীদের সাথে একজন শিক্ষক। আপনি তাদের সবাইকে একটি পরীক্ষা দেবেন, কিন্তু আপনি শুধুমাত্র 5 টি কাগজের জন্য সময় পাবেন। আপনি সাবধানে সেই 5 টি কাগজগুলি পর্যালোচনা করেন এবং আপনি প্যাটার্নগুলি লক্ষ্য করেন: যারা অনেক লিখেছেন, তারা উচ্চ পয়েন্ট পেতে থাকে, এবং যারা খালি উত্তরগুলি রেখেছে, তারা কম পয়েন্ট পেতে থাকে। এই প্যাটার্নগুলি ব্যবহার করে, আপনি অন্য 25 টি কাগজের পর্যালোচনাগুলি অনুমান করতে পারেন, প্রতিটি লাইন পড়া ছাড়া। এটি অর্ধেক পর্যবেক্ষণ করা শিখা: আপনি প্যাটার্নগুলি বুঝতে কয়েকটি পর্যবেক্ষণ করা কাগজগুলি (ল্যাব এই নিবন্ধটি শূন্য থেকে একটি সম্পূর্ণ অর্ধ-নিরীক্ষিত ইমেজ শ্রেণীকরণ পাইপলাইন তৈরি করে। ক্ষেত্রে গবেষণা: ধাতু পৃষ্ঠের উত্পাদন ত্রুটি সনাক্তকরণ. একটি কারখানা ইস্পাত প্লেট উত্পাদন করে, এবং ক্যামেরা উত্পাদন লাইন থেকে প্রতিটি প্লেট চিত্রনাট্য। অধিকাংশ প্লেটগুলি স্বাভাবিক, কিছু ত্রুটি আছে (চাপা, বিচ্ছিন্নতা, পিটিং, অন্তর্ভুক্ত)। কেস গবেষণা : একটি কারখানা ইস্পাত প্লেট উত্পাদন করে, এবং ক্যামেরা প্রতিটি প্লেটটি উত্পাদন লাইন থেকে রোল করার সময় ফটোগ্রাফি করে। অধিকাংশ প্লেটগুলি স্বাভাবিক, কিছুগুলিতে ত্রুটি রয়েছে (চিঁচুড়ি, ধাক্কা, পিটিং, অন্তর্ভুক্ত)। detecting manufacturing defects on metal surfaces. অর্ধ-নিরীক্ষিত শিক্ষা পাইপলাইন নির্মাণ যেকোনো কোডে ডুবে যাওয়ার আগে, আমরা পুরো পাইপলাইনটি মানচিত্র করে দেখি। প্রবাহটি বুঝতে প্রথমে প্রতিটি পদক্ষেপকে অসুবিধার পরিবর্তে উদ্দেশ্যমূলক মনে হবে। 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] কৌশলগত ধারণা: আমরা অ-লাইব্রেড ইমেজগুলি রূপান্তর করব তালিকাভুক্ত ইমেজগুলি (ক্লাস্টারিংয়ের মাধ্যমে) ব্যবহার করুন, তারপরে শেষ মডেলটি একটি শুরু করার জন্য সেই অনুমানীয় জ্ঞানটি ব্যবহার করুন. এটি একটি ছাত্রকে প্রকৃত পরীক্ষার আগে একটি কঠোর গাইড দেওয়ার মত মনে করুন - এটি নিখুঁত হবে না, কিন্তু এটি কিছুই নয়। প্রায় আমরা এই নিবন্ধটিতে যে শব্দবাজি ব্যবহার করব তা নিয়েও স্পষ্ট হবো, কারণ শব্দগুলি মিশ্রিত করা বিভ্রান্তির একটি খুব সাধারণ উৎস: শক্তিশালীভাবে লেবেল (বা শুধুমাত্র "লাইবেল"): একটি মানব বিশেষজ্ঞ দ্বারা নিশ্চিত করা লেবেল সঙ্গে ইমেজ। দুর্বল লেবেল (বা "পেসডো লেবেল"): ইমেজ যার লেবেলগুলি গ্রাসিং দ্বারা অনুমান করা হয়েছিল। Unlabelled: কোনও লেবেল সঙ্গে ইমেজ. এটি তাদের অবস্থা আগে আমরা তাদের গ্রুপ। অন্তর্ভুক্ত: একটি ইমেজের একটি কম্প্যাক্ট সংক্ষিপ্ত সংক্ষিপ্ত, একটি পূর্বশিক্ষিত নিউরাল নেটওয়ার্ক দ্বারা উত্পাদিত। আরও পড়ুন : আরও পড়ুন : Semi-supervised learning overview - স্কাইটি-লার্ন গুগল অর্ধ-নিরীক্ষিত শিক্ষা নিয়ে গবেষণা Semi-supervised learning overview - স্কাইটি-লার্ন গুগল অর্ধ-নিরীক্ষিত শিক্ষা নিয়ে গবেষণা 1. ডাটা অনুসন্ধান: আপনি যে ডেটা দিয়ে কাজ করছেন তা বুঝতে অন্য কিছু করার আগে কেন আপনার ডেটা দেখতে হবে ইমেজ ডেটা সেটগুলিতে অনন্য ব্যর্থতা মোড রয়েছে যা আপনি টেবিল ডেটাতে খুঁজে পাবেন না: ক্ষতিগ্রস্ত ফাইলগুলি যা 3 AM এ আপনার প্রশিক্ষণ লুপটি বিধ্বস্ত করে, অবিশ্বাস্য রেজল্যুশনগুলি যা নীরবে আপনার ইমেজগুলি ভঙ্গ করে, ভুল রঙের চ্যানেলগুলি (গ্রীস্কেল RGB হিসাবে মুখোমুখি) এবং অত্যন্ত শ্রেণী ব্যায়াম যেখানে 95% ইমেজগুলি "স্বাভাবিক" হয়। গোল্ডেন নিয়মঃ চলুন দেখে নেওয়া যাক আমরা কী করছি। never trust data you haven't inspected. 1.1 — ইমেজ লোড এবং গণনা আমাদের ডেটা সেট দুটি প্রধান ফোল্ডারে সংগৃহীত হয়: একটি লেবেল করা ইমেজ ( "স্বাভাবিক" এবং "ভুল" সাবফোল্ডারগুলিতে বিভক্ত) এবং একটি অ লেবেল করা ইমেজ (কোনো সাবফোল্ডার - শুধুমাত্র একটি ফ্ল্যাট সংগ্রহ ফাইল .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" আমরা ব্যবহার করি কারণ এটি কোনও অপারেটিং সিস্টেমে পথ বিচ্ছিন্নকারীদের সঠিকভাবে পরিচালনা করে। 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%}") এই লেবেল অনুপাত সাধারণত প্রায় 2% হয়। শুধুমাত্র 2% আমাদের ডেটা পেশাদার-নিরীক্ষিত লেবেল আছে. বাকি 98% একটি স্বর্ণের খনি যা আমরা উপেক্ষা করতে পারি না - এবং এটি ঠিক যা অর্ধ-নিরীক্ষিত শিখা ব্যবহার করে। 1.2 — সমস্যাগুলির জন্য স্ক্যানিং: রেজোলিউশন, রঙ, দুর্নীতি পরে, আমাদের প্রতিটি ইমেজকে আলাদাভাবে চেক করতে হবে. একটি একক ক্ষতিগ্রস্ত ফাইল একটি সম্পূর্ণ প্রশিক্ষণ চালানকে ধ্বংস করতে পারে. অনির্দিষ্ট রেজল্যুশনগুলি যদি আপনি সাবধান না হন তবে ইমেজগুলি ভুল করে ফেলবে. এবং ভুল রঙের মোডগুলি (গ্রীস্কেল যখন আপনার মডেলটি RGB প্রত্যাশা করে) বোকা বৈশিষ্ট্যগুলি উত্পাদন করবে। আমরা একটি ছোট ফাংশন লিখি যা প্রতিটি ইমেজ খুলতে এবং তার বৈশিষ্ট্যগুলি রেকর্ড করার চেষ্টা করে: 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, } এর মাঠ বিশেষভাবে গুরুত্বপূর্ণ। 3 রঙ চ্যানেল (রেড, সবুজ, নীল) মানে। এর মানে হল (১টি চ্যানেল)। মানে RGB একটি আলফা (পরিচ্ছন্নতা) চ্যানেল সঙ্গে. আমাদের প্রাক ট্রেনিং মডেল RGB প্রত্যাশা করে, তাই আমরা পরে সবকিছু রূপান্তর করতে হবে। img.mode 'RGB' 'L' 'RGBA' এখন আমরা সব 10,000 ছবির স্ক্যান করি: all_files = labeled_files + unlabeled_files image_info = [get_image_info(f) for f in all_files] info_df = pd.DataFrame(image_info) এবং ফলাফল দেখুন: 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}") উৎপাদনে কী খুঁজে বের করতে হবে: ক্ষতিগ্রস্ত ইমেজ > 0: তাদের অবিলম্বে মুছে ফেলুন. এমনকি একটি খারাপ ফাইল আপনার প্রশিক্ষণ বিধ্বস্ত করতে পারে. বিভিন্ন রেজোলিউশন: যদি min ≠ max গভীরতা বা উচ্চতা জন্য, ইমেজ বিভিন্ন আকার আছে. আমরা প্রাক প্রক্রিয়াকরণের সময় তাদের সবকিছুতে 224×224 আকার পরিবর্তন করব। একাধিক রঙ মোড: যদি আপনি উভয় 'RGB' এবং 'L' দেখেন, তাহলে আপনার রঙ এবং গ্রীস্কেলের একটি মিশ্রণ আছে। চরম ফাইল আকার: 1KB ফাইল সম্ভবত খালি বা ক্ষতিগ্রস্ত। ক্ষতিগ্রস্ত ফাইল পরিষ্কার করুন: 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 — শ্রেণী বিতরণ: আমাদের লেবেল করা সেট ভারসাম্যপূর্ণ? শিল্প এবং মেডিকেল সেটিংসগুলিতে, ত্রুটিগুলি বিরল। আপনার লেবেল করা সেটটি 90% "স্বাভাবিক" এবং 10% "ভুল" হতে পারে। এটি অত্যন্ত গুরুত্বপূর্ণ: একটি খারাপ মডেল যা সর্বদা "স্বাভাবিক" পূর্বাভাস করে 90% সঠিকতা পাবেন যখন এটি সম্পূর্ণরূপে অকার্যকর। 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}%)") যদি ভারসাম্যহীনতা গুরুতর হয়, তাহলে আমরা পরে একটি প্রযুক্তি ব্যবহার করে এটি মোকাবেলা করব। হারানোর ফাংশন - মৌলিকভাবে মডেলকে বলুন "একটি ত্রুটি মিস করা মিথ্যা অ্যালার্মের চেয়ে 4 গুণ খারাপ। pos_weight এছাড়াও এটি ভিজ্যুয়াল করুন: 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 — নমুনা ইমেজ দৃশ্যমান করা: সবসময় আপনার মডেলের আগে দেখুন এটি পুরো পাইপলাইনের সবচেয়ে গুরুত্বপূর্ণ পদক্ষেপ হতে পারে. আপনার ডেটা দেখুন. আপনি এমন ইমেজ খুঁজে পেতে পারেন যা স্পষ্টতই ভুল লেবেল করা হয়, সেন্সিং বস্তুগুলি (কালো সীমানা, ঘূর্ণিঝড়), গুণগত সমস্যা (ব্লুর, অতিরিক্ত এক্সপোজার), অথবা যে ত্রুটিগুলি দৃশ্যমানভাবে সুনির্দিষ্ট এবং আপনার কাজটি আপনি ভেবেছিলেন তুলনায় কঠিন। 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() এই ছবিগুলো পড়ার জন্য একটু সময় নিন। আপনার নিজের চোখ দিয়ে দুর্বলতাগুলি দেখুন? যদি আপনি না পারেন, তবে মডেলটিও লড়াই করবে. যদি দুর্বলতাগুলি স্পষ্ট হয় (একটি গভীর স্ক্র্যাচ, একটি বড় বিচ্ছিন্নতা), এটি উত্সাহিত করে - মডেলটি পার্থক্য শিখতে সক্ষম হওয়া উচিত। আপনি আরও পড়ুন : আরও পড়ুন : Pillow ডকুমেন্টেশন - পাইথন ইমেজিং লাইব্রেরি আমরা ইমেজ লোড করার জন্য ব্যবহার করি NEU Surface Defect Database — একটি বাস্তব বিশ্বের ইস্পাত পৃষ্ঠ ত্রুটি ডেটাবেস আপনি অনুশীলন করতে পারেন Pillow ডকুমেন্টেশন Surface Defect ডাটাবেস অংশ 2 — প্রাক প্রক্রিয়াকরণ: মডেলের ভাষা কথা বলতে কেন আমরা শুধু একটি নিউরাল নেটওয়ার্কে কাঁচা ইমেজ সরবরাহ করতে পারি না ResNet50 মত একটি প্রাক ট্রেনিং সিএনএ একটি খুব নির্দিষ্ট ধরণের ইনপুট প্রশিক্ষণ করা হয়েছিল: 224 × 224 পিক্সেল ইমেজ, RGB রঙে, ImageNet ডেটা সেট থেকে হিসাব করা নির্দিষ্ট গড় এবং স্ট্যান্ডার্ড পার্থক্য মান সঙ্গে স্বাভাবিক। এটি ভাষা হিসাবে চিন্তা করুন. ResNet50 "ImageNet কথা বলে." যদি আমরা চাই যে এটি আমাদের মেটাল পৃষ্ঠের ইমেজগুলি বুঝতে পারে, তাহলে আমাদের প্রথমে তাদের ImageNet ফরম্যাটে "প্রচার" করতে হবে. এই অনুবাদ চারটি ধাপ অন্তর্ভুক্ত: RGB রূপান্তর (3 চ্যানেল) histogram equalization এর মাধ্যমে তুলনা বৃদ্ধি ২২৪ × ২২৪ ImageNet পরিসংখ্যান সঙ্গে সামঞ্জস্যপূর্ণ পিক্সেল মান স্বাভাবিক করুন 2.1 - histogram সমীকরণ কি, এবং কেন এটি এখানে গুরুত্বপূর্ণ? একটি স্বাভাবিক পৃষ্ঠ এবং একটি কাঁচা একটি মধ্যে পার্থক্য শুধুমাত্র কয়েক পিক্সেল তীব্রতা স্তর হতে পারে - নগ্ন চোখের জন্য অদৃশ্য, এবং একটি মডেল সনাক্ত করা খুব কঠিন। পিক্সেল তীব্রতা পুনরায় বিতরণ করে যাতে পুরো বর্গ (0 থেকে 255) সমানভাবে ব্যবহৃত হয়. ফলাফল: মসৃণ বৈশিষ্ট্যগুলি দৃশ্যমান এবং সংখ্যালঘুভাবে "পপপ" করে। Histogram equalization আমরা একটি উন্নত সংস্করণ ব্যবহার করি যা বলা হয় (কনট্রাস্ট সীমিত Adaptive Histogram Equalization) গ্লোবাল equalization (যা পুরো ইমেজের জন্য একই রূপান্তর প্রয়োগ করে) এর বিপরীতে, CLAHE ছবির ছোট টাইলগুলিতে বিভক্ত করে (8x8 ডিফল্ট) এবং প্রতিটি টাইল স্বাধীনভাবে সমান করে। CLAHE গ্লোবাল সমীকরণটি পুরো ইমেজের জন্য একটি একক উজ্জ্বলতা স্লাইডার ব্যবহার করার মতো - আপনি অন্ধকার কোণগুলি উজ্জ্বল করতে পারেন কিন্তু ইতিমধ্যে উজ্জ্বল কেন্দ্রটি ধুয়ে ফেলতে পারেন। 2.2 — একটি কাস্টম PyTorch ডেটা সেট তৈরি করুন PyTorch ডেটা লোডিং দুই শ্রেণীর চারপাশে সংগঠিত করে: (একটি আইটেম কীভাবে লোড করা যায় তা জানেন) এবং (আপনি কিভাবে প্যাকেজিং এবং শুফল আইটেম জানেন) PyTorch একটি গঠিত ডাটা সেট, কিন্তু এটি প্রতিটি ইমেজ একটি লেবেল আছে অনুমান করে. আমাদের ডাটা সেট উভয় লেবেল এবং অ লেবেল ইমেজ আছে, তাই আমরা একটি কাস্টম ক্লাস প্রয়োজন। Dataset DataLoader ImageFolder প্রথমে ধাপে ধাপে এগিয়ে চলুন, প্রথমে এগিয়ে চলুন: 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 বলুন আমরা কত ছবি আছে। ইমেজ পথগুলি এবং (অতিরিক্তভাবে) তাদের লেবেলগুলি সংরক্ষণ করে। __len__ __init__ এখন মৌলিক পদ্ধতি, , যা একটি একক ইমেজ লোড করে এবং প্রাক-প্রসেস করে. আমরা এটি তিনটি ধাপে ভাগ করব: __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") কেন ? কারণ ResNet50 3 চ্যানেলের জন্য আশা করে। যদি আমাদের ইমেজটি গ্রীস স্ক্যালার (1 চ্যানেল) হয় তবে এটি গ্রীস মানগুলি R, G এবং B. যদি এটি ইতিমধ্যে RGB হয় তবে এটি কিছুই করে না। .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) কেন LAB এবং কেবলমাত্র RGB উপর CLAHE প্রয়োগ করবেন না? কারণ আপনি যদি R, G এবং B চ্যানেলগুলি স্বাধীনভাবে সমান করে তাহলে আপনি রঙগুলি বিচ্ছিন্ন করবেন - উদাহরণস্বরূপ, নীলকে সবুজে রূপান্তর করুন। LAB স্পেসে কাজ করে, আমরা শুধুমাত্র হালকা (L) চ্যানেলটি স্পর্শ করি এবং রঙগুলি (A, B) স্পর্শ না করে রাখি। 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 — রূপান্তর পাইপলাইন: প্রতিটি ধাপ কী করে এখন আমরা রূপান্তরগুলির সিরিজটি সংজ্ঞায়িত করি. প্রত্যেকটির একটি নির্দিষ্ট উদ্দেশ্য রয়েছে: 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], ), ]) প্রতিটি ধাপ ব্যাখ্যা করুন: ResNet50 এর আর্কিটেকচারটি ঠিক এই আকারের প্রয়োজন। যদি আপনি একটি 300×400 ইমেজ সরবরাহ করেন, তাহলে টেনসার আকৃতিগুলি মিলিত হবে না এবং PyTorch দুর্ঘটনা ঘটবে। (1) Resize to 224×224. এটি দুটি কাজ করে: HWC (Hight × Width × Channels) থেকে পিক্সেল ফরম্যাটটি CHW (Channels × Height × Width) এ রূপান্তর করে, যা PyTorch এর প্রত্যাশা; এবং [0, 255] সম্পূর্ণ থেকে [0.0, 1.0] ফ্লোটে পিক্সেল মান স্ক্যাল করে। (2) ToTensor. এই ম্যাজিক সংখ্যাগুলি - এবং — পুরো ImageNet ডেটা সেটের মধ্যে পিক্সেল মানের গড় এবং স্ট্যান্ডার্ড অপসারণ, চ্যানেল দ্বারা চ্যানেল (R, G, B) হিসাবে কম্পিউটার করা হয়। ResNet50 এই সঠিক স্বাভাবিককরণ মানগুলির সাথে প্রশিক্ষিত হয়েছিল, তাই তার অভ্যন্তরীণ ওজনগুলি 0 এর চারপাশে কেন্দ্রিত ইনপুট "প্রত্যাশা" করে। (3) Normalize with ImageNet mean and std. [0.485, 0.456, 0.406] [0.229, 0.224, 0.225] 2.4 - ডাটা সেট এবং ডাটা লোডার তৈরি করা এখন আমরা সবকিছু একত্রিত করি. একটি গুরুত্বপূর্ণ নীতি: তাদের মিশ্রণ আমাদের মূল্যায়ন দূষিত করবে। labeled and unlabeled data must be kept strictly separate at all times. প্রথমে, লেবেল করা ইমেজ পথ এবং তাদের শ্রেণী লেবেল সংগ্রহ করুন: 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 তারপরে, অনির্দিষ্ট ইমেজ পথগুলি সংগ্রহ করুন (একটি লেবেল প্রয়োজন নেই): unlabeled_paths = [str(fp) for fp in unlabeled_files] PyTorch Dataset Objects তৈরি করুন: 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") অবশেষে, ডাটা লোডারগুলিতে তাদের প্যাক করুন. একটি ডাটা লোডার ইমেজগুলি একসঙ্গে প্যাক করে (batch_size=32 একটি সময়ে 32 ইমেজ মানে) এবং অপশনীয়ভাবে তাদের shuffles: labeled_loader = DataLoader(labeled_dataset, batch_size=32, shuffle=False) unlabeled_loader = DataLoader(unlabeled_dataset, batch_size=32, shuffle=False) কেন কারণ আমরা বৈশিষ্ট্যগুলি মুছে ফেলতে যাচ্ছি, এবং আমাদের অন্তর্ভুক্তগুলি আমাদের ফাইল তালিকাগুলির সাথে একই রকম থাকতে হবে। shuffle=False আরও পড়ুন : আরও পড়ুন : ফরেক্স ট্রান্সফার - সম্পূর্ণ তালিকা CLAHE ব্যাখ্যা (OpenCV টিউটোরিয়াল) PyTorch Dataset & DataLoader টিউটোরিয়াল ফরেক্স ট্রান্সফার - সম্পূর্ণ তালিকা CLAHE ব্যাখ্যা (OpenCV টিউটোরিয়াল) PyTorch Dataset & DataLoader টিউটোরিয়াল অংশ 3 — বৈশিষ্ট্য আউটপুট: ইমেজকে অর্থবহ সংখ্যাগুলিতে রূপান্তরিত করা 3.1 — কেন কাঁচা পিক্সেল একটি ভয়ঙ্কর প্রতিনিধি একটি 224×224 RGB ইমেজ 150,528 সংখ্যক (224 × 224 × 3 চ্যানেল) আছে। তাদের বেশিরভাগই শব্দ - আলো, সেন্সর আর্দ্রতা, কম্প্রেশন আর্দ্রতা ক্ষুদ্র পরিবর্তন। খারাপ: একই রঙের দুটি ছবি, সামান্য আলাদা কোণ বা আলো থেকে নেওয়া, সম্পূর্ণ ভিন্ন পিক্সেল মান আছে। যা আমাদের দরকার তা হল একটি প্রতিনিধিত্ব যা সংগ্রহ করে ইমেজ - "এটা একটি কাঁচা মত," "এটা একটি নরম পৃষ্ঠ" - একটি কম্প্যাক্ট, স্থিতিশীল সংখ্যাগত আকারে। কর। meaning embeddings 3.2 — একটি পূর্বশিক্ষিত মডেল কি এবং কেন আমরা শূন্য থেকে প্রশিক্ষণ না একটি গভীর নিউরন নেটওয়ার্ক প্রশিক্ষণ শূন্য থেকে অনেক ডেটা প্রয়োজন - সাধারণত শত শত হাজার ইমেজ. আমাদের 200 লেবেল ইমেজ আছে. যদি আমরা 200 ইমেজে ResNet50 এর 25 মিলিয়ন পরামিতি প্রশিক্ষণ করার চেষ্টা করি, মডেলটি প্রতিটি প্রশিক্ষণ ইমেজকে নিখুঁতভাবে স্মরণ করবে কিন্তু নতুন ইমেজগুলিতে সম্পূর্ণরূপে ব্যর্থ হবে। . overfitting পরিবর্তে, আমরা একটি মডেল ব্যবহার করি যা ইতিমধ্যে ইমেজনেটের মাধ্যমে প্রশিক্ষণ করা হয়েছিল-১৪ মিলিয়ন চিত্রগুলির একটি ডেটা সেট 1,000 বিভাগ (কুকুর, বিড়াল, গাড়ি, ভবন, ইত্যাদি)। — they apply to steel surfaces just as well as they apply to cats. universal এটি একটি অভিজ্ঞ ফটোগ্রাফার নিয়োগ আপনার কারখানা পরিদর্শন করার মত ভাবুন. তারা আগে কখনো ইস্পাত প্লেট দেখেনি, কিন্তু তারা ইতিমধ্যে জানেন কিভাবে : তারা অস্বাভাবিক কাঠামো, পৃষ্ঠের গুণমানের হঠাৎ পরিবর্তন, প্যাটার্ন যা নিয়ম ভঙ্গ করতে পারেন তারা শুধু জানতে হবে আপনার নির্দিষ্ট কারখানায় "ভুল" হিসাবে গণনা করা হয়। দেখুন 3.3 — লোডিং ResNet50 চলুন প্রাকৃতিক মডেলটি লোড করি: import torchvision.models as models resnet = models.resnet50(weights=models.ResNet50_Weights.IMAGENET1K_V1) এই একক লাইনটি (প্রথমবার) একটি ResNet50 মডেল ডাউনলোড করে, যার ওজন ImageNet এ প্রশিক্ষিত হয়েছে। 3.4 - প্যারামিটারগুলি ফ্রিজ করা আমরা শিখে যাওয়া কোনও বৈশিষ্ট্য সংশোধন করতে চাই না. আমরা একটি পড়া শুধুমাত্র সরঞ্জাম হিসাবে ResNet ব্যবহার করছি: for param in resnet.parameters(): param.requires_grad = False PyTorch বলে "এই প্যারামিটারগুলির জন্য gradients গণনা করবেন না" এই দুটি সুবিধা আছে: এটি পূর্বশিক্ষিত ওজনগুলির দুর্ঘটনাক্রান্ত সংশোধন প্রতিরোধ করে এবং এটি অনুমানগুলি দ্রুত করে তোলে (না gradient tracking = কম গণনা এবং কম মেমরি)। requires_grad = False 3.5 - শ্রেণীকরণ শিরোনাম মুছে ফেলা ResNet50 এর আর্কিটেকচারটি এই রকম দেখায়: 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) আমরা 2048-দৈর্ঘ্য ভেক্টর চাই - এটি আমাদের embedding. আমরা শেষ সম্পূর্ণরূপে সংযুক্ত স্তর চান না, কারণ এটি ImageNet এর 1000 শ্রেণির জন্য নির্দিষ্ট (কুকুর, বিড়াল, বিমান ...) এবং আমাদের কাজের জন্য অপরিহার্য। feature_extractor = torch.nn.Sequential(*list(resnet.children())[:-1]) এই লাইনটি কী করে: তালিকা হিসাবে ResNet এর সমস্ত স্তর ফেরত দেয়। শেষের দিকে বাদে সবাই এগিয়ে যায়। এরপর তাদেরকে একটি মডেলে ফেরত দেয়া হয়। resnet.children() [:-1] Sequential(*) feature_extractor.eval() মোড ড্রপুট অক্ষম করে এবং ব্যাটারি স্বাভাবিককরণের জন্য চলমান পরিসংখ্যান ব্যবহার করে। আউটপুট - একই চিত্র সবসময় একই অন্তর্ভুক্ত উত্পাদন করে। 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}") GPU ব্যবহার করে (যদি উপলব্ধ থাকে) বৈশিষ্ট্য এক্সট্র্যাকশন প্রায় 10x দ্রুত হয়। 3.6 - এক্সট্র্যাকশন ফাংশন এখন আমরা একটি ফাংশন লিখি যা ফাংশন এক্সট্র্যাক্টরের মাধ্যমে ইমেজের ব্যাগগুলি সরবরাহ করে এবং অন্তর্ভুক্তগুলি সংগ্রহ করে। বাইরের কাঠামো : 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 = [] আমরা তালিকায় ফলাফল সংগ্রহ করি কারণ আমরা ব্যাটগুলিতে ইমেজ প্রক্রিয়াকরণ করি (32 একবার), একই সময়ে না (GPU মেমরিতে সজ্জিত হবে না)। মৌলিক লুক: with torch.no_grad(): for batch_images, batch_labels in dataloader: batch_images = batch_images.to(device) features = model(batch_images) gradient ট্র্যাকিং অক্ষম করে - অপরিহার্য কারণ আমরা শুধুমাত্র অনুমান করি, প্রশিক্ষণ না. এই একা মেমরি ব্যবহার অর্ধেকে কেটে এবং জিনিসগুলি গতিশীল করে। আমরা যদি একটি জিপিইউতে ইমেজগুলি স্থানান্তর করি। torch.no_grad() batch_images.to(device) উৎপাদন আকৃতি আছে - শেষ দুটি মাত্রা গড় সংগ্রহের স্থানীয় বাকিগুলি। model(batch_images) (batch_size, 2048, 1, 1) features = features.squeeze(-1).squeeze(-1) # Now shape is (batch_size, 2048) — that's our embedding অবশেষে, আমরা ফলাফলগুলি CPU-এ ফেরত পাঠাই (numpy GPU টেনসারগুলির সাথে কাজ করতে পারে না) এবং তাদের সংরক্ষণ করি: all_embeddings.append(features.cpu().numpy()) all_labels.append(batch_labels.numpy()) return np.concatenate(all_embeddings), np.concatenate(all_labels) সমস্ত প্যাকেজ একসাথে একটি মেরুতে সংযুক্ত করুন। np.concatenate 3.7 - অপসারণ চালান 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 ইমেজ, প্রতিটি 2048-দৈর্ঘ্য ভেক্টর দ্বারা প্রতিনিধিত্ব করা হয়. যা কাঁচা পিক্সেল (150,528 → 2,048) তুলনায় 73x কম্প্রেশন। আরো গুরুত্বপূর্ণ দূরে print("Extracting embeddings for unlabeled images...") unlabeled_embeddings, _ = extract_embeddings( unlabeled_loader, feature_extractor, device ) print(f" Shape: {unlabeled_embeddings.shape}") # Expected: (9800, 2048) আমরা লেবেলগুলি ছাড়াই (তারা সব -1 যাই হোক) বদলি _ 3.8 - অন্তর্ভুক্তগুলি সংরক্ষণ করুন (প্রতিবার পুনরায় অন্তর্ভুক্ত করবেন না!) বৈশিষ্ট্য অপসারণ সবচেয়ে ব্যয়বহুল পদক্ষেপ - সম্ভবত 30+ মিনিট 10,000 ছবির জন্য একটি GPU. ফলাফলগুলি সংরক্ষণ করুন যাতে আপনাকে এটি পুনরায় করতে হবে না: 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") পরে, আপনি অবিলম্বে পুনরায় লোড করতে পারেন . np.load("data/labeled_embeddings.npy") 3.9 — স্বাস্থ্য পরীক্ষা: ইনব্যাকশনগুলি যুক্তিসঙ্গত? এগিয়ে যাওয়ার আগে, একটি দ্রুত নিশ্চিতকরণ. বর্জ্য মধ্যে, বর্জ্য বের করুন - আসুন নিশ্চিত করুন যে আমাদের embeddings সঠিক: 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()}") কি আশা করতে হবে: 0.3-0.5, std প্রায় 0.5-1.0, কোন NaN, কোন Inf. যদি আপনি NaN মান দেখতে, একটি ক্ষতিগ্রস্ত ইমেজ সম্ভবত পরিষ্কারের ধাপের মাধ্যমে পতিত হয়. যদি গড় ঠিক 0 হয়, তবে স্বাভাবিককরণের সাথে কিছু ভুল। এগিয়ে যাওয়া : এগিয়ে যাওয়া : Transfer learning explained (PyTorch টিউটোরিয়াল) ResNet কাগজ (He et al., 2015) - মূল আর্কিটেকচার যা skip সংযোগগুলি চালু করেছে বৈশিষ্ট্য এক্সট্র্যাকশন vs fine-tuning — স্ট্যানফোর্ড CS231n কোর্স নোট Transfer learning explained (PyTorch টিউটোরিয়াল) রিসেট কাগজ (He et al., 2015) বৈশিষ্ট্য এক্সট্র্যাকশন vs fine-tuning অংশ 4 - অ-নিরীক্ষিত গ্রুপিং: অন্ধকারে গঠন আবিষ্কার 4.1 — ক্লাস্টারিং কি এবং কেন আমরা এটি প্রয়োজন আমরা এখন 10,000 embeddings আছে - প্রতিটি ইমেজের কম্প্যাক্ট সংক্ষিপ্ত সংক্ষিপ্ত. তাদের মধ্যে 200 লেবেল আছে. অন্যান্য 9,800 না. আমাদের লক্ষ্য প্রকৃতির খুঁজে বের করা হয় যে ডেটা আশা করি "স্বাভাবিক" এবং "ভুল" এর সাথে মিলবে। groupings মূল অনুমান: যদি আমাদের embeddings ভাল হয় (এবং ResNet50 embeddings সাধারণত হয়), একই ধরনের ইমেজ হবে স্বাভাবিক পৃষ্ঠাগুলি একসঙ্গে গ্রাস করবে; ত্রুটিপূর্ণ পৃষ্ঠাগুলি একসঙ্গে গ্রাস করবে। বন্ধ কিন্তু প্রথমে, একটি বাস্তব সমস্যা: 2048 মাত্রা দৃশ্যমান করা অসম্ভব এবং কিছু অ্যালগরিদমকে ধীর করে তোলে। 4.2 - ইনব্যাডিংগুলি স্ট্যান্ডার্ড করুন গ্রাস্টারিং অ্যালগরিদম, বিশেষ করে K-Means, পয়েন্টগুলির মধ্যে দূরত্ব গণনা করে. যদি একটি দূরত্ব 0 থেকে 1000 এবং আরেকটি দূরত্ব 0 থেকে 0.01 পর্যন্ত হয়, তাহলে প্রথম দূরত্ব সম্পূর্ণরূপে দূরত্বের উপর নির্ভর করবে - যেন দ্বিতীয় দূরত্ব অস্তিত্বহীন। 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) কেন লেবেল এবং অ লেবেল একসঙ্গে স্ট্যান্ডার্ড করা? কারণ তারা একই বিতরণ থেকে আসে (একই কারখানা, একই ক্যামেরা)। # 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 — PCA সঙ্গে মাত্রা হ্রাস PCA (Principal Component Analysis) ডেটাতে সর্বোচ্চ variance দিকগুলি খুঁজে পায় এবং শীর্ষ দিকগুলিতে প্রজেক্ট করে। 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% variance retained মানে আমরা তথ্যের শুধুমাত্র 5% বাদ দিয়েছি কিন্তু মাত্রা 40x কমে যাচ্ছে. এই t-SNE এবং DBSCAN অনেক দ্রুত এবং আরও স্থিতিশীল করে তোলে. এছাড়াও দেখুন প্রতিটি উপাদান কতটুকু অবদান রাখে: 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() এই "অলব প্লট" দেখায় যেখানে আরো উপাদান যোগ করা গুরুত্বপূর্ণ লাভের জন্য থেমে থাকে। 4.4 — t-SNE সঙ্গে ভিজ্যুয়াল t-SNE একটি nonlinear dimensionality reduction প্রযুক্তি যা বিশেষভাবে ভিজ্যুয়ালাইজেশন জন্য ডিজাইন করা হয়েছে। : উচ্চ মাত্রার মহাকাশে কাছাকাছি অবস্থিত ইমেজগুলি 2D জায়গায় কাছাকাছি অবস্থিত হবে, যা এটি চেক করার জন্য নিখুঁত করে তোলে যে স্বাভাবিক এবং ত্রুটিপূর্ণ ইমেজগুলি স্বাভাবিকভাবে আলাদা হয় কিনা। local structure একটি গুরুত্বপূর্ণ সতর্কবাণী: t-SNE গ্লোবাল দূরত্বগুলি বিপরীত করে - গ্রুপগুলির মধ্যে স্থানটি অর্থহীন। 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) এর প্যারামিটার প্রায়ই "বিদেশের আকার" নিয়ন্ত্রণ করে — কতটি নিকটবর্তী পয়েন্ট t-SNE বিবেচনা করে. 30 আমাদের আকারের ডেটা সেটের জন্য একটি যুক্তিসঙ্গত ডিফল্ট। perplexity এখন আমরা t-SNE সমন্বয়গুলি বিভক্ত করি: labeled_tsne = all_tsne[:len(labeled_embeddings)] unlabeled_tsne = all_tsne[len(labeled_embeddings):] এবং দৃশ্যমান: 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() যদি আপনি এই জায়গায় দুটি আলাদা মেঘ দেখেন - একদিকে সবুজ, অন্যদিকে লাল - এটি একটি চমৎকার চিহ্ন। # 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() সঠিক জায়গায়, হালকা মেঘ (অন্যাবিলিত ইমেজগুলি) রঙিন পয়েন্টগুলির সাথে অন্তর্ভুক্ত হওয়া উচিত. এটি নিশ্চিত করে যে লেবেলিত এবং অন্যাবিলিত ইমেজ একই বিতরণ থেকে আসে - অর্ধ-নিরীক্ষিত শেখার জন্য একটি প্রয়োজনীয় শর্ত। 4.5 - K-Means গ্রুপিং K-Means সবচেয়ে সহজ এবং সবচেয়ে ব্যাপকভাবে ব্যবহৃত ক্লাস্টারিং অ্যালগরিদম. এটি প্রতিটি পয়েন্টকে নিকটতম ক্লাস্টার কেন্দ্রে iteratively প্রদান করে, তারপর কেন্দ্রগুলি আপডেট করে, ডেটা সঠিকভাবে k গ্রুপে বিভক্ত করে। যেহেতু আমরা জানি যে আমাদের দুটি শ্রেণী আছে (স্বাভাবিক এবং ত্রুটিপূর্ণ), আমরা k = 2 দিয়ে শুরু করি, কিন্তু আমরা k = 3, 4, 5 পরীক্ষা করি যাতে আমরা পরীক্ষা করি যে ডেটাতে আরো গঠন থাকতে পারে (উদাহরণস্বরূপ, বিভিন্ন আলাদা আলাদা আলাদা আলাদা আলাদা আলাদা আলাদা আলাদা আলাদা আলাদা আলাদা আলাদা আলাদা আলাদা আলাদা আলাদা আলাদা আলাদা আলাদা আলাদা আলাদা আলাদা আলাদা আলাদা আলাদা আলাদা আলাদা আলাদা আলাদা আলাদা আলাদা আলাদা আলাদা আলাদা আলাদা আলাদা আলাদা আলাদা আলাদা আলাদা আলাদা আলাদা আলাদা আলাদা আলাদা আলাদা। ধরন ক্লাস্টারগুলি প্রকৃত লেবেলগুলির সাথে কতটুকু মিলিত হয় তা মূল্যায়ন করার জন্য, আমরা ARI = 1.0 মানে সত্য লেবেলগুলির সাথে নিখুঁত সমঝোতা। ARI = 0.0 মানে র্যান্ডম গ্রাসিং। 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}") এর প্যারামিটার মানে K-Means 10 বার বিভিন্ন র্যান্ডম ইনিংসের সাথে চালানো হবে এবং সেরা ফলাফল বজায় রাখবে। n_init=10 যদি k=2 সর্বোচ্চ এআরআই দেয়, তাহলে আমাদের ডেটাতে দুটি প্রাকৃতিক গ্রুপ রয়েছে যা স্বাভাবিক vs ত্রুটি সংযুক্ত করে। 4.6 — DBSCAN গ্রাসিং (একটি বিকল্প পদ্ধতি) DBSCAN K-Means থেকে খুব আলাদাভাবে কাজ করে. ক্লাস্টারগুলির সংখ্যা নির্ধারণ করার পরিবর্তে, আপনি দুটি পরামিতি নির্ধারণ করেন: eps (epsilon): দুটি পয়েন্টের মধ্যে সর্বোচ্চ দূরত্ব যাতে তারা প্রতিবেশী হিসাবে বিবেচিত হয়। min_samples: একটি ঘন অঞ্চল (ক্লাস্টার) গঠনের জন্য প্রয়োজনীয় ন্যূনতম সংখ্যক পয়েন্ট। DBSCAN স্বয়ংক্রিয়ভাবে ক্লাস্টার সংখ্যা নির্ধারণ করে এবং outliers (পয়েন্ট যা কোনও ক্লাস্টার অন্তর্ভুক্ত নয় - -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}") আমরা একাধিক প্যারামিটার সংমিশ্রণ পরীক্ষা করতে হবে কারণ "সঠিক" মানগুলি ডেটা উপর নির্ভর করে: 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}") পিসিএ-র কম ডেটা ব্যবহার করা হয়েছে ( , 50 dims) সম্পূর্ণ 2048-dim embeddings এর পরিবর্তে. DBSCAN খুব উচ্চ মাত্রায় লড়াই করে কারণ সমস্ত দূরত্ব অনুরূপ হয়ে যায় ( "দৈর্ঘ্যের অভিশাপ"). all_pca Compare the best ARI from DBSCAN with the best from K-Means, and pick the winner. 4.7 — t-SNE প্ল্যাটফর্মে গ্রুপগুলি ভিজ্যুয়াল করা আসুন দেখে নেওয়া যাক আমাদের 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() যদি এই পর্দায় দুটি রং প্রায় আপনি ট-এসএনই পর্দায় দেখেছিলেন যে দুটি গ্রুপের সাথে মিলিত হয়, তাহলে ক্লাস্টারিং কাজ করছে। 4.8 - অনির্দিষ্ট ইমেজগুলির জন্য পাসওয়ার্ড লেবেল প্রদান এখন গুরুত্বপূর্ণ পদক্ষেপ: আমরা ক্লাস্টার নির্দেশাবলী গ্রহণ করি এবং তাদেরকে অ-লিঙ্কিত ইমেজগুলির জন্য "দুর্বল লেবেল" হিসাবে বিবেচনা করি। পেসডো লেবেল বের করুন: unlabeled_pseudo_labels = all_cluster_ids[len(labeled_embeddings):] সত্যিকারের লেবেলগুলির সাথে সমন্বয় চেক করুন: 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'") যদি গ্রুপ 0 প্রধানত ত্রুটি (normal_rate < 50%) হয়, তাহলে আমরা ম্যাপিংটি ফ্ল্যাট করি: 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)") চলুন দেখে নেওয়া যাক বিতরণ: 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") এখন আমাদের দুটি আলাদা ডেটা সেট রয়েছে যা খুব আলাদা বৈশিষ্ট্যগুলির সাথে রয়েছে: শক্তিশালীভাবে লেবেল — 200 বাস্তব বিশেষজ্ঞ লেবেল সঙ্গে ইমেজ. উচ্চ মানের, ছোট পরিমাণ. এটি আমাদের মৌলিক সত্য। দুর্বল লেবেল — 9,800 ক্লাস্টার ভিত্তিক পেসডো লেবেল সঙ্গে ইমেজ. কম মানের (কয়েকটি লেবেল ভুল), কিন্তু বিশাল পরিমাণ। গোল্ডেন নিয়মঃ তারা পরবর্তী ধাপে বিভিন্ন উদ্দেশ্যে কাজ করে। never mix these two. আরও পড়ুন : আরও পড়ুন : K-Means ব্যাখ্যা করা হয়েছে (scikit-learn) DBSCAN ব্যাখ্যা (scikit-learn) কিভাবে সঠিকভাবে t-SNE পড়া (Distill) — প্রয়োজনীয় পড়া K-Means ব্যাখ্যা করা হয়েছে (scikit-learn) DBSCAN ব্যাখ্যা (scikit-learn) কিভাবে সঠিকভাবে t-SNE পড়বেন (Distill) অংশ 5 - অর্ধ-নিরীক্ষিত প্রশিক্ষণ: বাস্তব পরীক্ষা 5.1 — আমাদের দুটি ধাপের পদ্ধতির পিছনে যুক্তি কল্পনা করুন আপনি কারখানায় একটি নতুন মান পরীক্ষক প্রশিক্ষণ করছেন: : আপনি তাদের ৯,৮০০ ফটো দেখান এবং বলুন "আমি এইগুলি স্বাভাবিক এবং এইগুলি ত্রুটিপূর্ণ, কিন্তু আমি 100% নিশ্চিত নই." ইনফেক্টর একটি কঠোর মানসিক মডেল তৈরি করতে শুরু করে। কিছু লেবেলগুলি ভুল, কিন্তু সামগ্রিক প্যাটার্ন - স্বাভাবিক পৃষ্ঠগুলি নরম এবং সমান, ত্রুটিপূর্ণ পৃষ্ঠগুলি ত্রুটিপূর্ণ - বেশিরভাগই সঠিক। . Phase 1 (pre-training on pseudo-labels) ভাবুন অনুভূতি : তারপর আপনি তাদের 200 ছবি দেখান যা একটি বিশেষজ্ঞ দ্বারা সাবধানে নিশ্চিত করা হয়েছে: "এগুলো অবশ্যই স্বাভাবিক, এবং এইগুলি অবশ্যই ত্রুটিপূর্ণ। Phase 2 (fine-tuning on real labels) ফলাফল: যিনি একজন 10,000 ইমেজ (বৃদ্ধি ব্যাপক অনুভূতি) এবং হয়েছে আমরা আশা করি এই ইনফেক্সটরটি এমন একজনকে অতিক্রম করবে যিনি কেবলমাত্র 200 টি নিশ্চিত নমুনা দেখেছেন। দেখা ক্যালিফোর্ন এই প্রমাণ করার জন্য, আমরা দুটি সামঞ্জস্যপূর্ণ পরীক্ষা চালাচ্ছি: পরীক্ষা A — শুধুমাত্র পর্যবেক্ষণ: শুধুমাত্র 200 লেবেল ইমেজ ট্রেন পরীক্ষা B — অর্ধ-নিরীক্ষিত: 9,800 পুস্তক-বিক্রিত ইমেজগুলিতে প্রাক-ট্রেন, তারপর 200 লেবেল করা ইমেজগুলিতে সুন্দর টান একই মডেল আর্কিটেকচার, একই টেস্ট সেট. একমাত্র পার্থক্য হল যদি মডেলটি অ-ল্যাব ডেটা দেখে বা না। 5.2 — শ্রেণীকরণ নির্মাণ: আর্কিটেকচার আমরা পুনরায় ResNet50 ব্যবহার করি, কিন্তু এবার আমরা বাইনারি শ্রেণীকারীর সাথে শেষ স্তর এবং আমরা তৃতীয় অংশের বিপরীতে (যেখানে আমরা শুধুমাত্র বৈশিষ্ট্যগুলি তুলনা করেছি)। 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 এখানে আমরা মূল ImageNet শ্রেণীকরণ হেড (2048 → 1000 ক্লাস) আমাদের নিজস্ব সঙ্গে প্রতিস্থাপন করি: 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) কেন ? শুধুমাত্র 200 লেবেল ইমেজ এবং 25 মিলিয়ন প্যারামিটার সঙ্গে, overfitting প্রধান হুমকি। Dropout প্রতিটি প্রশিক্ষণ ধাপের সময় নিউরনগুলির 50% অক্ষম করে, নেটওয়ার্কটি অতিরিক্ত প্রতিনিধিত্ব শিখতে বাধ্য করে। Dropout(0.5) কেন লাইনাল(2048, 2) এর পরিবর্তে? বাইনারি শ্রেণীকরণের জন্য, একটি সিগমোইড অ্যাক্টিভেশন সহ একটি একক আউটপুট নিউরন softmax সহ দুটি নিউরনের সমতুল্য, কিন্তু আরও সহজ এবং সামান্য বেশি সংখ্যাগতভাবে স্থিতিশীল। Linear(2048, 1) 5.3 — হারানোর ফাংশন: শ্রেণীর অস্থিতিশীলতা পরিচালনা প্রশিক্ষণ লুপ লিখার আগে, আমরা হারানোর ফাংশন আলোচনা করব। (Logits সঙ্গে বাইনারি ক্রস এন্ট্রোপি), যা একটি একক, সংখ্যাগতভাবে স্থিতিশীল অপারেশন মধ্যে সিগমোইড সক্রিয়করণ এবং বাইনারি ক্রস এন্ট্রোপি একত্রিত করে। BCEWithLogitsLoss মূল অতিরিক্তটি হচ্ছে : pos_weight pos_weight = torch.tensor([4.0]).to(device) criterion = nn.BCEWithLogitsLoss(pos_weight=pos_weight) কী করে এটি হারানোর ফাংশনটি বলছে: "একটি অনুপস্থিত ত্রুটি (ভুয়া নেতিবাচক) শাস্তি করা উচিত এটি শ্রেণি অস্থিতিশীলতা প্রতিরোধ করে. এটি ছাড়া, মডেলটি সর্বদা "স্বাভাবিক" পূর্বাভাস করে 80% সঠিকতা অর্জন করতে পারে - যা অকার্যকর। pos_weight=4.0 4 times more মূল্য 4.0 ক্লাস অনুপাত উপর ভিত্তি করে একটি গভীর অনুমান। যদি আপনি 80% স্বাভাবিক / 20% ত্রুটি আছে, তাহলে আপনি এই মান নিয়ন্ত্রণ করতে পারেন, কিন্তু 4.0 একটি ভাল শুরু পয়েন্ট। pos_weight = 80/20 = 4.0 5.4 — অপ্টিমাইজার: ওজন হ্রাস সঙ্গে AdamW import torch.optim as optim optimizer = optim.AdamW(model.parameters(), lr=lr, weight_decay=1e-4) কেন AdamW? এটি আদম সঠিক ওজন হ্রাস (L2 নিয়মিত) সঙ্গে। হালকাভাবে বড় ওজনের শাস্তি দেয়, যা অতিরিক্ত সংযোজন প্রতিরোধের আরেকটি স্তর। এটি মডেলকে বলার মতো এটি ভাবুন "সম্ভবত সহজ ব্যাখ্যাগুলি পছন্দ করুন। weight_decay=1e-4 ৫.৫ শিক্ষার সময়সূচী scheduler = optim.lr_scheduler.ReduceLROnPlateau(optimizer, patience=3, factor=0.5) এটি স্বয়ংক্রিয়ভাবে শিক্ষার হার কমায় যখন যাচাইয়ের হার উন্নতি বন্ধ করে দেয়। এর মানে হচ্ছে, ‘সীমিত করার আগে উন্নতি ছাড়া তিনটি যুগ অপেক্ষা করা। মানে "শিক্ষা হার 0.5 দ্বারা সংখ্যাগরিষ্ঠ করুন" এটি সংযোগের জন্য গুরুত্বপূর্ণ - মডেলটি সর্বনিম্ন, ছোট পদক্ষেপগুলি অতিরিক্ত পরিমাপ প্রতিরোধ করে। patience=3 factor=0.5 5.6 — প্রশিক্ষণ লুক: এক সময়ে এক যুগ এখন আমরা সম্পূর্ণ প্রশিক্ষণ ফাংশন নির্মাণ করি. আমরা পর্দাটির প্রতিটি অংশকে আলাদাভাবে পরিচালনা করব। (একটি যুগ - এক পাস সমস্ত প্রশিক্ষণ ডেটা): 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 প্রতিটি ব্যাটারি ক্লাসিক চক্রের মাধ্যমে চলে যায়: অগ্রগতি পাস → কম্পিউটার হ্রাস → পিছনে পুনরায় পুনরায় পুনরায় পুনরায় পুনরায় পুনরায় পুনরায় পুনরায় পুনরায় পুনরায় পুনরায় পুনরায় পুনরায় পুনরায় পুনরায় পুনরায় পুনরায় পুনরায় পুনরায় পুনরায় পুনরায় পুনরায় পুনরায় পুনরায় পুনরায়। (প্রত্যেকটি প্রশিক্ষণের পরে): 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()) লক্ষ্য করুন এটি গুরুত্বপূর্ণ: এটি dropout (সব নিউরন সক্রিয়) অক্ষম করে এবং ব্যাটারি পরিসংখ্যানের পরিবর্তে ব্যাটারি স্বাভাবিককরণের জন্য চলমান পরিসংখ্যান ব্যবহার করে। 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 আমরা অনুসরণ করি F1 সমস্ত যুগের মধ্যে, শুধু শেষ নয়. মডেল প্রায়ই প্রশিক্ষণ শেষ হওয়ার আগে শীর্ষে (যখন তারা সামান্য অতিক্রম করতে পারে)। best 5.7 - ডেটা ভাগের প্রস্তুতি আমরা লেবেল ডেটা ট্রেন (70%) এবং টেস্ট (30%) বিভক্ত। : এটি কোনও পরীক্ষায় প্রশিক্ষণের জন্য কখনো ব্যবহার করা হয় না। 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, ) আমাদের প্রয়োজনীয় তিনটি ডাটা সেট তৈরি করুন: # 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, ) এবং ডাটালাইজাররা: 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") বিভিন্ন ব্যাটারি আকার উল্লেখ করুন: ছোট লেবেল করা সেটের জন্য 16 (একটি যুগে কম ইমেজ), বড় দুর্বল লেবেল করা সেটের জন্য 32 (একটি দ্রুত প্রক্রিয়াকরণ)। মডেলটি নমুনা অর্ডার শিখতে বাধা দেওয়ার জন্য প্রশিক্ষণ। shuffle=True 5.8 — পরীক্ষা A: শুধুমাত্র পর্যবেক্ষণ করা হয় (ভিত্তিক লাইন) আমরা শুধুমাত্র 140 লেবেল প্রশিক্ষণ ইমেজ ব্যবহার করে একটি নতুন মডেল প্রশিক্ষণ করি (অন্য 60 পরীক্ষার জন্য সংরক্ষিত)। 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 — পরীক্ষা B: অর্ধ-নিরীক্ষিত (দুটি ধাপের পদ্ধতি) এখন পুরো পাইপলাইন। ফেজ 1 মডেলকে 9,800 পেসডো লেবেল ইমেজ থেকে ব্যাপক অনুভূতি প্রদান করে। 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" ) আমরা এখানে শুধুমাত্র 10 যুগের জন্য প্রশিক্ষণ করি কারণ পেসডো লেবেলগুলি নোংরা। 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" ) লক্ষ্য করুন (5e-5 vs 1e-4 ফেজ 1). এটি উদ্দেশ্যমূলক এবং গুরুত্বপূর্ণ. যদি আমরা ফাইন টুইংয়ের সময় একটি উচ্চ শিখার হার ব্যবহার করি, তবে মডেলটি দ্রুত প্রাক প্রশিক্ষণের সময় যা কিছু শিখেছিল তা "ভুলে যাবে" - gradients খুব বড় হবে এবং প্রাক প্রশিক্ষিত ওজনগুলি অতিক্রম করবে. একটি নরম শিখার হার মডেলটি তার বিদ্যমান জ্ঞানের ক্ষুদ্র সংশোধন করতে দেয়, ফেজ 1 থেকে ব্যাপক প্যাটার্নগুলি সংরক্ষণ করে এবং প্রকৃত লেবেলগুলির সাথে ত্রুটিগুলি সংশোধন করে। lower learning rate এটি কারখানার তদন্তকারীর সাথে অনুরূপ: আপনি পর্যায়ে তাদের প্রশিক্ষণ শুরু করেন না 2. আপনি সহজেই তাদের ভুল ধারণাগুলি সংশোধন করেন এবং তাদের সামগ্রিক অনুভূতি রক্ষা করেন। 5.10 — চূড়ান্ত মূল্যায়ন: সত্যের মুহূর্ত এখন আমরা একই টেস্ট সেটের উপর উভয় মডেল মূল্যায়ন করি যাতে একাধিক মিটার থাকে। প্রথমত, মূল্যায়ন ফাংশন: 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()) আমরা উভয়কে সংগ্রহ করি। (AUC-ROC জন্য, যা র্যাংকিং মান পরিমাপ করে) এবং (F1 জন্য, যা 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 কেন এবং সঠিকতা নয়? কারণ অসমন্বিত শ্রেণির সাথে, সঠিকতা বিভ্রান্তিকর। একটি মডেল যা সর্বদা "স্বাভাবিক" পূর্বাভাস দেয়, 80% সঠিকতা পায় কিন্তু 0% ত্রুটিগুলি স্মরণ করে। F1 কেন এটি মডেলটি কতটা ভাল পরিমাপ করে ইমেজগুলি (ভুল ইমেজগুলি স্বাভাবিক ইমেজগুলির চেয়ে উচ্চতর সম্ভাবনা পেতে হবে), শ্রেণীকরণ সীমানাটি যাই হোক না কেন. AUC 1.0 এর মানে নিখুঁত র ্যাঙ্কিং; 0.5 এর মানে র ্যাঙ্কিং। AUC-ROC র ্যাংক এখন তুলনা : 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" ) চূড়ান্ত রায় : 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}") যদি ডেল্টা কলামটি ইতিবাচক সংখ্যা প্রদর্শন করে, তবে আমরা প্রমাণ করেছি যে অ-লাইব্রেড ডেটা উপকারী ছিল। 5.11 - ফলাফলের ব্যাখ্যা এখানে তুলনা পড়া কিভাবে: F1 +0.05 বা তার বেশি উন্নত: অর্ধ-নিরীক্ষিতের জন্য স্পষ্ট জয়। F1 +0.01 থেকে +0.04 পর্যন্ত উন্নত: নমনীয় উন্নতি. অর্ধ-নিরীক্ষিত সাহায্য করে কিন্তু মর্যাদা ছোট। F1 অপরিবর্তিত বা আরও খারাপ: পেসডো লেবেলগুলি সাহায্য করার জন্য খুব শব্দহীন ছিল, অথবা গ্রাসিং সত্য কাঠামো ক্যাপচার করে না. বিভিন্ন বৈশিষ্ট্য এক্সট্র্যাক্টর, বিভিন্ন গ্রাসিং অ্যালগরিদম বা পেসডো লেবেলগুলির জন্য উচ্চতর আস্থা সীমা চেষ্টা করুন. আরও পড়ুন : আরও পড়ুন : পেসডো-ব্যাগিং কাগজ (লি, 2013) - মূল পদ্ধতি semi-supervised learning survey (van Engelen & Hoos) - পদ্ধতিগুলির ব্যাপক পর্যালোচনা PyTorch প্রশিক্ষণ সেরা অনুশীলন পেসডো লেবেলিং কাগজ (Lee, 2013) ভ্যান এঞ্জেলেন ও হুস (Van Engelen & Hoos) PyTorch প্রশিক্ষণ সেরা অনুশীলন অধ্যায় 6 - মিলিয়ন ইমেজে স্ক্যালিং: একটি বাস্তব রুটব্যাপ ব্যবসা থেকে প্রশ্ন "আপনার ধারণা প্রমাণ 10,000 চিত্রগুলিতে কাজ করে. আমাদের প্রক্রিয়াকরণের জন্য 4 মিলিয়ন চিত্র রয়েছে. আমরা এই পাইপলাইনটি 5,000 ইউরো বাজেটের সাথে স্কেল করতে পারি? এটি একটি প্রশ্ন যা আপনি যেকোনো বাস্তব প্রকল্পে মুখোমুখি হবেন. আসুন সৎভাবে এটি ভেঙে ফেলি। কম্পিউটার খরচ একটি একক GPU সঙ্গে আমাদের 10,000 ইমেজে, এটি প্রায় 30 মিনিট সময় নেয়। Feature extraction 4,000,000 images ÷ 10,000 images × 30 min = 12,000 min = 200 GPU-hours ~ € / ঘন্টা একটি ক্লাউড GPU ইনস্টিটিউটের জন্য (এজুরি এনসি সিরিজ একটি T4 বা A10 GPU সহ), এটি প্রায় . €400 স্ট্যান্ডার্ড K-Means সমস্ত ডেটা মেমোরিতে লোড করে দূরত্ব হিসাবে গণনা করে। 2048 মাত্রার 4M অন্তর্ভুক্তির সাথে (প্রত্যেকটি 4 বাইট ফ্লুট): Clustering 4,000,000 × 2,048 × 4 bytes = ~32 GB just for the embeddings এটি বেশিরভাগ মেশিনে র্যামে ঢুকবে না. সমাধান: ব্যবহার করুন সিকিট-লার্ন, যা একই সময়ে 10,000 নমুনা (উদাহরণস্বরূপ) টুকরা ডেটা প্রক্রিয়াকরণ করে। MiniBatchKMeans : 4M পেসডো লেবেল ইমেজের জন্য প্রাক-শিক্ষা প্রায় 50 GPU-ঘন্টা লাগে . CNN training €100 স্টোরেজ খরচ রাবার ইমেজ: 4M × ~50 KB গড় = অন্তর্ভুক্ত: 4M × 2048 × 4 বাইট = Azure Blob Storage এ ~€0.02/GB/মাসে, এটি প্রায় . 200 GB 32 GB €5/month লেবেল কৌশল যদি 200 লেবেল মাত্রায় যথেষ্ট না হয়, তাহলে আমরা আরও লেবেল করতে পারি. প্রতিটি ইমেজে ~ 1 ইউরো (স্বাভাবিকতা নিয়ন্ত্রণ সহ), 2,000 আরো লেবেল খরচ হবে কিন্তু আরো বুদ্ধিমান পদ্ধতি আছে: . €2,000 active learning সক্রিয় শেখা মডেল নির্বাচন করতে দেয় ইমেজগুলি লেবেল করতে. 2,000 ইমেজগুলি র্যান্ডমভাবে নির্বাচন করার পরিবর্তে, মডেলটি তাদের সম্পর্কে সবচেয়ে অনিশ্চয়তা সনাক্ত করে - এমন ইমেজগুলি যা এটি সবচেয়ে বেশি শিক্ষা দেবে। যার সক্রিয় শেখার সাথে সাথে, আমরা শুধুমাত্র 2,000 এর পরিবর্তে 500 অতিরিক্ত লেবেল প্রয়োজন হতে পারে . €500 মোট বাজেট অনুমান Feature extraction (GPU): €400 CNN training (GPU): €100 Storage (year 1): €60 Additional labeling: €500 – €2,000 ────────────────────────────────────── TOTAL: €1,060 – €2,560 5000 ইউরো বাজেটের মধ্যে, পরীক্ষা এবং পুনরায় চালানোর জন্য স্থান সংরক্ষণ করে। সাফল্যের পাঁচটি শর্ত ক্লাউড GPUs ব্যবহার করুন, স্থানীয় হার্ডওয়্যার নয়. ঘন্টা ভাড়া, শুধুমাত্র আপনি যা ব্যবহার করেন তার জন্য পরিশোধ করুন। সাধারণ KMeans এর পরিবর্তে MiniBatchKMeans ব্যবহার করুন. একই মানের, 100x কম মেমরি। ব্যাটারি প্রক্রিয়াকরণের সাথে একটি সঠিক ডেটা পাইপলাইন তৈরি করুন. একই সময়ে RAM-এ 4M ইমেজ লোড করবেন না. প্যারাল লোড করার জন্য num_workers > 0 এর সাথে PyTorch DataLoader ব্যবহার করুন. প্রতিটি মানুষের লেবেলযুক্ত ইমেজের মূল্য সর্বোচ্চ করার জন্য সক্রিয় শেখার বিবেচনা করুন. প্রতিটি লেবেল কৌশলগতভাবে নির্বাচন করা উচিত, র্যান্ডমভাবে নয়। স্টোরেজ এবং সংস্করণ ইনব্যাডিংস, শুধুমাত্র কাঁচা ইমেজ নয়. 4M ইনব্যাডিংস পুনরায় প্রত্যাহার করা € 400 খরচ করে; সংরক্ষিত ইনব্যাডিংস লোড করা কোন খরচ করে না। আরও পড়ুন : আরও পড়ুন : MiniBatchKMeans (scikit-learn) — how to label smarter, not more Active learning overview MiniBatchKMeans (শিখুন) সক্রিয় শেখার বিস্তারিত উপসংহার অর্ধ-নিরীক্ষিত শিখা জাদু নয় - এটি ইঞ্জিনিয়ারিং। আপনি অমানবিক ডেটাতে লুকানো কাঠামোটি (ভিত্তিক এবং গ্রাসিংয়ের মাধ্যমে) গ্রহণ করেন, এটি অন্তর্নিহিত লেবেলগুলিতে রূপান্তর করুন, এবং আপনার নিরীক্ষিত মডেলকে একটি হেড স্টার্ট দিতে ব্যবহার করুন। আসুন আমরা যে পাইপলাইন তৈরি করেছি তা সম্পূর্ণরূপে পুনরায় সংগ্রহ করি: অনুসন্ধান — আমরা দুর্নীতির, অনিশ্চয়তা ফরম্যাট এবং শ্রেণি অস্থিতিশীলতা জন্য 10,000 ইমেজ স্ক্যান করেছি। প্রাক-প্রসেসিং - আমরা প্রতিটি ইমেজকে ফরম্যাট ResNet50 প্রত্যাশা করে: 224×224, RGB, CLAHE উন্নত, ImageNet স্বাভাবিক। বৈশিষ্ট্য অন্তর্ভুক্ত — আমরা প্রতিটি ইমেজকে একটি 2048-দৈর্ঘ্য অন্তর্ভুক্ত করার জন্য একটি Pre-trained ResNet50 ব্যবহার করেছি যা তার দৃশ্যমান অন্তর্ভুক্ত করে। ক্লাস্টারিং - আমরা K-Means এবং DBSCAN প্রয়োগ করেছি যাতে অ-লিঙ্কিত ইমেজগুলি ক্লাস্টারগুলিতে গ্রুপ করা হয়, তারপর ক্লাস্টার সদস্যতা ভিত্তিতে পেসডো-লিঙ্কগুলি নির্ধারণ করা হয়। অর্ধ-নিরীক্ষিত প্রশিক্ষণ - আমরা ৯,৮০০ পেসডো লেবেলযুক্ত ইমেজগুলিতে একটি সিএনএনকে প্রাক-নিরীক্ষিত করেছি, তারপর 200 বাস্তব লেবেলগুলিতে সুন্দরভাবে তুলনা করেছি এবং তুলনা করেছি শুধুমাত্র নিয়ন্ত্রিত মৌলিক লাইনগুলির সাথে। স্ক্যালিং বিশ্লেষণ - আমরা 4 মিলিয়ন ইমেজের জন্য কম্পিউটিং, স্টোরেজ এবং লেবেলিং খরচ অনুমান করেছি, যা 5,000 ইউরো বাজেটের মধ্যে বাস্তবায়নযোগ্যতা নিশ্চিত করে। কৌশল Takeaways: একটি প্রথমে প্রশিক্ষিত সিএনএন যে কোনও ইমেজ ডোমেইন থেকে গুরুত্বপূর্ণ বৈশিষ্ট্যগুলি উত্পাদন করতে পারে, এমনকি এমন একটি যেখানে এটি কখনো প্রশিক্ষিত হয়নি। ইনব্যাডিংগুলিতে গ্রাসিং প্রাকৃতিক গ্রুপিংগুলি প্রকাশ করে যা প্রায়শই বাস্তব শ্রেণির সাথে সংশ্লিষ্ট হয়। পেসডো লেবেলগুলি অসম্পূর্ণ, কিন্তু একটি মডেল যা অসম্পূর্ণ লেবেলগুলিতে পূর্বে প্রশিক্ষিত হয় এবং তারপর বাস্তব লেবেলগুলিতে সুন্দরভাবে সংশ্লিষ্ট হয়, এটি কেবলমাত্র বাস্তব লেবেলগুলিতে প্রশিক্ষিত মডেলের চেয়েও বেশি। প্যাটার্নটি বিভিন্ন ক্ষেত্রে কাজ করে: মেডিকেল ইমেজিং, শিল্প মান নিয়ন্ত্রণ, স্যাটেলাইট ইমেজিং, ডকুমেন্ট শ্রেণীকরণ এবং জৈব বৈচিত্র্য পর্যবেক্ষণ।