{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.12.13","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"none","dataSources":[],"dockerImageVersionId":28755,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":false}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"# !wget -O template.py \"https://raw.githubusercontent.com/azmansikder/Computer_Vision_Template/refs/heads/main/Ultimate_Template.py\"","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2026-06-02T09:02:50.282277Z","iopub.execute_input":"2026-06-02T09:02:50.282499Z","iopub.status.idle":"2026-06-02T09:02:50.286745Z","shell.execute_reply.started":"2026-06-02T09:02:50.282476Z","shell.execute_reply":"2026-06-02T09:02:50.285975Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# **0. Import and GPU Setup**","metadata":{}},{"cell_type":"code","source":"import os, gc, warnings, math\nimport numpy as np\nimport pandas as pd\nimport matplotlib.pyplot as plt\nimport seaborn as sns\nfrom pathlib import Path\n\nimport tensorflow as tf\nfrom tensorflow.keras import layers, models, applications, mixed_precision\nfrom tensorflow.keras.preprocessing.image import ImageDataGenerator\nfrom tensorflow.keras.callbacks import (ModelCheckpoint, EarlyStopping,\n                                        CSVLogger, LambdaCallback)\nfrom sklearn.model_selection import train_test_split, GroupShuffleSplit\nfrom sklearn.utils import class_weight\nfrom sklearn.metrics import (classification_report, confusion_matrix, log_loss,\n                             roc_auc_score, f1_score, cohen_kappa_score,\n                             average_precision_score)\n\nwarnings.filterwarnings('ignore')\nprint(\"✅ TensorFlow:\", tf.__version__)\nprint(\"✅ GPUs:\", tf.config.list_physical_devices('GPU'))\n\nmixed_precision.set_global_policy('mixed_float16')\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-02T09:02:50.288142Z","iopub.execute_input":"2026-06-02T09:02:50.288325Z","iopub.status.idle":"2026-06-02T09:03:08.018649Z","shell.execute_reply.started":"2026-06-02T09:02:50.288306Z","shell.execute_reply":"2026-06-02T09:03:08.017913Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# **1. CFG**","metadata":{}},{"cell_type":"code","source":"class CFG:\n    seed = 42\n\n    # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    # BLOCK A: Dataset Info  🔴 MUST UPDATE\n    # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    num_classes = 4          # ✅ AUTO-DETECT হবে (Section 2-এ override)\n                             #    যেকোনো সংখ্যা রাখো, ঠিক হয়ে যাবে\n    img_size    = (224, 224) # 224=fast | 300=better | 380=best(slow)\n    batch_size  = 16         # OOM হলে: 32→16→8\n\n    # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    # BLOCK B: Competition Type  🔴 MUST UPDATE\n    # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    # 'csv'       → 1 folder + CSV file  (Dog Breed, Cassava, Plant)\n    # 'directory' → class-wise folders   (Cats vs Dogs: train/cat/, train/dog/)\n    data_format = 'csv'\n\n    # 'multiclass' → 1 ছবিতে exactly 1 label  (99% competition)\n    # 'multilabel' → 1 ছবিতে multiple labels   (rare)\n    task_type = 'multiclass'\n\n    # Competition rules-এ যা লেখা থাকে তা দাও:\n    # 'val_accuracy' → balanced (Dog Breed, Cassava)\n    # 'val_auc'      → imbalanced (Skin Cancer, Fraud)\n    # 'val_f1'       → hackathon F1 macro (TF 2.16+ only)\n    # 'val_map'      → multilabel mAP\n    monitor_metric = 'val_f1'\n    monitor_mode   = 'max'   # loss হলে 'min', বাকি সব 'max'\n\n    # 'class'       → id, label           (2 column submission)\n    # 'probability' → id, cat, dog, ...   (many column submission)\n    sub_type = 'class'\n\n    # True  → Dog Breed, Plant, Cassava, Medical  (flip করলে ঠিক থাকে)\n    # False → State Farm, Traffic Sign, Digits    (flip করলে label উল্টে যায়!)\n    h_flip = True\n\n    # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    # BLOCK C: Paths & Columns  🔴 MUST UPDATE\n    # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    train_dir = '/kaggle/input/competitions/plant-pathology-2021-fgvc8/train_images'\n    test_dir  = '/kaggle/input/competitions/plant-pathology-2021-fgvc8/test_images'\n    data_csv  = '/kaggle/input/competitions/plant-pathology-2021-fgvc8/train.csv'\n    test_csv  = '/kaggle/input/competitions/plant-pathology-2021-fgvc8/sample_submission.csv'\n    img_col   = 'image'        # CSV-এ image নামের column\n\n    # label_col দুইভাবে দিতে পারো:\n    #   string → normal single label column:   label_col = 'breed'\n    #   list   → one-hot encoded columns:      label_col = ['healthy','rust','scab']\n    #            (auto-convert হবে → num_classes auto-detect হবে)\n    label_col = 'labels'\n\n    val_split = 0.2\n\n    # None      → Normal — standard stratified split\n    # 'col_name'→ Same person-এর multiple ছবি → GroupShuffleSplit (no leakage)\n    group_col = None\n\n    # False → ছবি সব এক folder-এ  (Dog Breed: train/abc.jpg)\n    # True  → ছবি class folder-এ  (State Farm: train/c0/img.jpg)\n    add_class_dir = False\n\n    # Multilabel only — label column names list\n    # Normal multiclass-এ None রাখো\n    multilabel_cols = None\n\n    # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    # BLOCK D: Training Settings (সাধারণত change লাগে না)\n    # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    epochs_warmup    = 1\n    epochs_finetune  = 2\n    label_smoothing  = 0.1\n    warmup_epochs_lr = 3\n    use_mixup        = True\n    mixup_alpha      = 0.2\n    use_tta          = True\n    n_tta            = 6\n    use_concat_pool  = True   # GAP+GMP concat pooling\n    dense_units      = 512\n    dropout_rate_1   = 0.4\n    dropout_rate_2   = 0.3\n\ntf.keras.utils.set_random_seed(CFG.seed)\nnp.random.seed(CFG.seed)\n\n# Auto-configure activation, loss, class_mode\nif CFG.task_type == 'multiclass':\n    ACTIVATION = 'softmax'\n    LOSS_BASE  = tf.keras.losses.CategoricalCrossentropy(\n                     label_smoothing=CFG.label_smoothing)\n    CLASS_MODE = 'categorical'\nelse:\n    ACTIVATION = 'sigmoid'\n    LOSS_BASE  = tf.keras.losses.BinaryCrossentropy(\n                     label_smoothing=CFG.label_smoothing)\n    CLASS_MODE = 'raw'  # multilabel-এ 'raw' — NOT 'categorical'\n\nprint(f\"✅ Mode:{CFG.task_type} | Act:{ACTIVATION} | ClassMode:{CLASS_MODE}\")\nprint(f\"✅ Monitoring: {CFG.monitor_metric} ({CFG.monitor_mode})\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-02T09:03:08.01972Z","iopub.execute_input":"2026-06-02T09:03:08.020257Z","iopub.status.idle":"2026-06-02T09:03:08.031069Z","shell.execute_reply.started":"2026-06-02T09:03:08.020233Z","shell.execute_reply":"2026-06-02T09:03:08.030284Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# **2. Data Loading**","metadata":{}},{"cell_type":"code","source":"print(f\"\\n{'='*50}\\n📂 DATA LOADING ({CFG.data_format.upper()})\\n{'='*50}\")\n\nclass_indices        = None\nindex_to_class       = None\nclass_weights_dict   = None\nFILE_COL = Y_COL     = None\ntrain_df = val_df = df_full = None\ncw_arr_list          = None\n_orig_label_col_list = None   # ✅ FIX: label_col list হলে preserve করা হবে\n\nif CFG.data_format == 'csv':\n    df_full = pd.read_csv(CFG.data_csv)\n\n    # ────────────────────────────────────────────────────────\n    # ✅ NEW: Universal One-Hot → Single Label Auto-Converter\n    # label_col = ['healthy','rust','scab'] দিলে auto-convert হবে\n    # ────────────────────────────────────────────────────────\n    if CFG.task_type == 'multiclass' and isinstance(CFG.label_col, list):\n        print(f\"  [Auto-Convert] One-Hot {CFG.label_col} → Single Label\")\n        df_full['__auto_label__'] = df_full[CFG.label_col].idxmax(axis=1)\n        _orig_label_col_list = CFG.label_col  # ✅ FIX: preserve করা হলো\n        CFG.label_col        = '__auto_label__'\n\n    # Step 1: filename column\n    if CFG.add_class_dir:\n        df_full['filename'] = (df_full[CFG.label_col].astype(str)\n                               + '/' + df_full[CFG.img_col].astype(str))\n        FILE_COL = 'filename'\n    elif not df_full[CFG.img_col].astype(str).str.contains(\n            r'\\.\\w+$', regex=True).any():\n        df_full['filename'] = df_full[CFG.img_col].astype(str) + '.jpg'\n        FILE_COL = 'filename'\n    else:\n        FILE_COL = CFG.img_col\n\n    # Step 2: label type\n    if CFG.task_type == 'multiclass':\n        df_full[CFG.label_col] = df_full[CFG.label_col].astype(str)\n        Y_COL = CFG.label_col\n    else:\n        if CFG.multilabel_cols is None:\n            raise ValueError(\n                \"❌ task_type='multilabel' হলে CFG.multilabel_cols list দিতে হবে!\\n\"\n                \"   Example: multilabel_cols = ['label1', 'label2', 'label3']\")\n        Y_COL = CFG.multilabel_cols\n        print(f\"  Multilabel columns: {Y_COL}\")\n\n    # Step 3: train/val split\n    if CFG.group_col and CFG.group_col in df_full.columns:\n        print(f\"⚠️  GroupShuffleSplit → '{CFG.group_col}' (data leakage রোখা হচ্ছে)\")\n        gss = GroupShuffleSplit(n_splits=1, test_size=CFG.val_split,\n                                random_state=CFG.seed)\n        tr_idx, va_idx = next(gss.split(df_full, groups=df_full[CFG.group_col]))\n        train_df = df_full.iloc[tr_idx].reset_index(drop=True)\n        val_df   = df_full.iloc[va_idx].reset_index(drop=True)\n    else:\n        print(\"✅ Standard Stratified Split\")\n        stratify = df_full[CFG.label_col] if CFG.task_type == 'multiclass' else None\n        train_df, val_df = train_test_split(\n            df_full, test_size=CFG.val_split,\n            random_state=CFG.seed, stratify=stratify)\n\n    # ✅ NEW: num_classes auto-detect\n    CFG.num_classes = train_df[CFG.label_col].nunique()\n    print(f\"✅ Auto-detected Classes: {CFG.num_classes}\")\n    print(f\"   Train:{len(train_df)} | Val:{len(val_df)}\")\n\n    # Step 4: class weights (csv multiclass only)\n    if CFG.task_type == 'multiclass':\n        cw_arr      = class_weight.compute_class_weight(\n            'balanced',\n            classes=np.unique(train_df[CFG.label_col]),\n            y=train_df[CFG.label_col])\n        cw_arr_list = list(cw_arr)\n\nelif CFG.data_format == 'directory':\n    FILE_COL = None\n    Y_COL    = None\n    print(f\"  Train dir: {CFG.train_dir}\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-02T09:03:08.032642Z","iopub.execute_input":"2026-06-02T09:03:08.033024Z","iopub.status.idle":"2026-06-02T09:03:08.125993Z","shell.execute_reply.started":"2026-06-02T09:03:08.032992Z","shell.execute_reply":"2026-06-02T09:03:08.125447Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# **3. Data Generators**","metadata":{}},{"cell_type":"code","source":"train_datagen = ImageDataGenerator(\n    rotation_range=15,\n    width_shift_range=0.10,\n    height_shift_range=0.10,\n    shear_range=0.10,\n    zoom_range=0.15,\n    horizontal_flip=CFG.h_flip,\n    brightness_range=[0.85, 1.15],\n    fill_mode='nearest'\n    # Medical/Skin হলে: fill_mode='reflect', rotation_range=90, vertical_flip=True\n)\nval_datagen = ImageDataGenerator()\n\n\ndef make_gen(datagen, df, shuffle=True, directory=None):\n    \"\"\"Universal generator: CSV ও directory দুটোই support করে।\"\"\"\n    if CFG.data_format == 'csv':\n        return datagen.flow_from_dataframe(\n            dataframe=df,\n            directory=directory or CFG.train_dir,\n            x_col=FILE_COL,\n            y_col=Y_COL,\n            target_size=CFG.img_size,\n            batch_size=CFG.batch_size,\n            class_mode=CLASS_MODE,\n            shuffle=shuffle,\n            seed=CFG.seed\n        )\n    else:\n        aug = ImageDataGenerator(\n            rotation_range=15, width_shift_range=0.10,\n            height_shift_range=0.10, shear_range=0.10,\n            zoom_range=0.15, horizontal_flip=CFG.h_flip,\n            brightness_range=[0.85, 1.15], fill_mode='nearest',\n            validation_split=CFG.val_split)\n        val_aug = ImageDataGenerator(validation_split=CFG.val_split)\n        if shuffle:\n            return aug.flow_from_directory(\n                CFG.train_dir, target_size=CFG.img_size,\n                batch_size=CFG.batch_size, class_mode=CLASS_MODE,\n                shuffle=True, seed=CFG.seed, subset='training')\n        else:\n            return val_aug.flow_from_directory(\n                CFG.train_dir, target_size=CFG.img_size,\n                batch_size=CFG.batch_size, class_mode=CLASS_MODE,\n                shuffle=False, subset='validation')\n\n\ntrain_gen = make_gen(train_datagen, train_df, shuffle=True)\nval_gen   = make_gen(val_datagen,   val_df,   shuffle=False)\n\n# Class mapping\nif CFG.task_type == 'multiclass':\n    class_indices  = train_gen.class_indices\n    index_to_class = {v: k for k, v in class_indices.items()}\n\n    if CFG.data_format == 'csv':\n        class_weights_dict = {\n            class_indices[lbl]: cw_arr_list[i]\n            for i, lbl in enumerate(np.unique(train_df[CFG.label_col]))\n        }\n    else:\n        print(\"  Computing class weights from directory...\")\n        dir_labels = train_gen.classes\n        cw_dir     = class_weight.compute_class_weight(\n            'balanced', classes=np.unique(dir_labels), y=dir_labels)\n        class_weights_dict = {i: float(w) for i, w in enumerate(cw_dir)}\n        print(f\"  ✅ Class weights: {class_weights_dict}\")\n\n# EDA: class distribution\nif df_full is not None and CFG.task_type == 'multiclass':\n    counts = df_full[CFG.label_col].value_counts()\n    plt.figure(figsize=(20, 4))\n    sns.barplot(x=counts.index[:40].astype(str),\n                y=counts.values[:40], palette='viridis')\n    plt.xticks(rotation=90, fontsize=6)\n    plt.title(f'Class Distribution (top 40 of {len(counts)})')\n    plt.tight_layout()\n    plt.savefig('class_distribution.png', dpi=150)\n    plt.show()\n    ratio = counts.values[0] / max(counts.values[-1], 1)\n    print(f\"  Most : {counts.index[0]} ({counts.values[0]})\")\n    print(f\"  Least: {counts.index[-1]} ({counts.values[-1]})\")\n    print(f\"  Imbalance: {ratio:.1f}x {'⚠️ HIGH' if ratio > 5 else '✅ OK'}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-02T09:03:08.126784Z","iopub.execute_input":"2026-06-02T09:03:08.127097Z","iopub.status.idle":"2026-06-02T09:04:34.51063Z","shell.execute_reply.started":"2026-06-02T09:03:08.127076Z","shell.execute_reply":"2026-06-02T09:04:34.509903Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# **4. Mixup**","metadata":{}},{"cell_type":"code","source":"def mixup_generator(generator, alpha=CFG.mixup_alpha):\n    \"\"\"\n    In-batch mixup: same batch-এর ভেতরে shuffle করে mix করা।\n    Data waste নেই (পুরো dataset দেখা যায়), next() মাত্র একবার।\n    \"\"\"\n    while True:\n        x, y = next(generator)\n        bs = len(x)\n        if bs <= 1:\n            yield x, y\n            continue\n        y   = y.astype(np.float32)\n        idx = np.random.permutation(bs)\n        x2, y2 = x[idx], y[idx]\n        lam   = np.random.beta(alpha, alpha)\n        lam_x = np.reshape(lam, [-1] + [1] * (len(x.shape) - 1))\n        lam_y = np.reshape(lam, [-1] + [1] * (len(y.shape) - 1))\n        yield lam_x * x + (1 - lam_x) * x2, lam_y * y + (1 - lam_y) * y2\n\n\n# ==========================================\n# 4b. SAMPLE WEIGHT WRAPPER — Keras 3 Fix\n# ==========================================\ndef apply_sample_weights(generator, weights_dict):\n    \"\"\"\n    ✅ FIX: Keras 3 / TF 2.16+ এ class_weight= generator-এ crash করে।\n    Solution: weights-কে (x, y, sample_weight) হিসেবে yield করা।\n    \"\"\"\n    for x, y in generator:\n        class_ids = np.argmax(y, axis=1)\n        sw = np.array(\n            [weights_dict.get(int(c), 1.0) for c in class_ids],\n            dtype='float32')\n        yield x, y, sw\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-02T09:04:34.511503Z","iopub.execute_input":"2026-06-02T09:04:34.511888Z","iopub.status.idle":"2026-06-02T09:04:34.519453Z","shell.execute_reply.started":"2026-06-02T09:04:34.511863Z","shell.execute_reply":"2026-06-02T09:04:34.518593Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# **5. Cosine LR**","metadata":{}},{"cell_type":"code","source":"@tf.keras.utils.register_keras_serializable()\nclass CosineDecayWithWarmup(tf.keras.optimizers.schedules.LearningRateSchedule):\n    \"\"\"Linear Warmup → Cosine Decay। Division-by-zero safe।\"\"\"\n\n    def __init__(self, base_lr, total_steps, warmup_steps):\n        super().__init__()\n        self.base_lr      = float(base_lr)\n        self.total_steps  = float(total_steps)\n        self.warmup_steps = float(warmup_steps)\n\n    def __call__(self, step):\n        step        = tf.cast(step, tf.float32)\n        warmup_lr   = self.base_lr * (step / tf.maximum(1.0, self.warmup_steps))\n        decay_steps = tf.maximum(1.0, self.total_steps - self.warmup_steps)\n        progress    = (step - self.warmup_steps) / decay_steps\n        cosine_lr   = 0.5 * self.base_lr * (\n            1.0 + tf.cos(math.pi * tf.clip_by_value(progress, 0.0, 1.0)))\n        return tf.where(step < self.warmup_steps, warmup_lr, cosine_lr)\n\n    def get_config(self):\n        return {\n            'base_lr':      self.base_lr,\n            'total_steps':  self.total_steps,\n            'warmup_steps': self.warmup_steps,\n        }","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-02T09:04:34.520626Z","iopub.execute_input":"2026-06-02T09:04:34.520992Z","iopub.status.idle":"2026-06-02T09:04:34.539482Z","shell.execute_reply.started":"2026-06-02T09:04:34.520963Z","shell.execute_reply":"2026-06-02T09:04:34.538583Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# **6. Model Builder**","metadata":{}},{"cell_type":"code","source":"def build_model(base_fn):\n    \"\"\"\n    Dynamic preprocessing (model family অনুযায়ী):\n      EfficientNet  → [0, 255] as-is\n      ResNet/Mobile → [-1, 1]\n      DenseNet      → [0, 1] + ImageNet normalize\n      Xception/Incep→ [-1, 1]\n      Others        → [0, 1]\n    \"\"\"\n    base = base_fn(weights='imagenet', include_top=False,\n                   input_shape=(*CFG.img_size, 3))\n    base.trainable = False\n\n    inputs = tf.keras.Input(shape=(*CFG.img_size, 3))\n    name   = base_fn.__name__\n\n    if 'EfficientNet' in name:\n        x = inputs\n    elif 'ResNet' in name or 'MobileNet' in name:\n        x = layers.Rescaling(1. / 127.5, offset=-1.0)(inputs)\n    elif 'DenseNet' in name:\n        x    = layers.Rescaling(1. / 255.)(inputs)\n        mean = tf.constant([0.485, 0.456, 0.406], shape=[1, 1, 3])\n        std  = tf.constant([0.229, 0.224, 0.225], shape=[1, 1, 3])\n        x    = (x - mean) / std\n    elif 'Xception' in name or 'Inception' in name:\n        x = layers.Rescaling(1. / 127.5, offset=-1.0)(inputs)\n    else:\n        x = layers.Rescaling(1. / 255.)(inputs)\n\n    x = base(x, training=False)\n\n    if CFG.use_concat_pool:\n        gap = layers.GlobalAveragePooling2D()(x)\n        gmp = layers.GlobalMaxPooling2D()(x)\n        x   = layers.Concatenate()([gap, gmp])\n    else:\n        x = layers.GlobalAveragePooling2D()(x)\n\n    x       = layers.BatchNormalization()(x)\n    x       = layers.Dropout(CFG.dropout_rate_1)(x)\n    x       = layers.Dense(CFG.dense_units, activation='swish')(x)\n    x       = layers.BatchNormalization()(x)\n    x       = layers.Dropout(CFG.dropout_rate_2)(x)\n    outputs = layers.Dense(CFG.num_classes, activation=ACTIVATION,\n                           dtype='float32')(x)\n\n    return tf.keras.Model(inputs, outputs), base","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-02T09:04:34.540616Z","iopub.execute_input":"2026-06-02T09:04:34.541591Z","iopub.status.idle":"2026-06-02T09:04:34.558141Z","shell.execute_reply.started":"2026-06-02T09:04:34.541562Z","shell.execute_reply":"2026-06-02T09:04:34.557522Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# **7. Metrics**","metadata":{}},{"cell_type":"code","source":"def get_metrics():\n    \"\"\"\n    সব metric এক জায়গায়। TF version check সহ।\n    CFG.monitor_metric অনুযায়ী BLOCK 8-এ monitor auto-set হবে।\n\n    Metric → CFG.monitor_metric mapping:\n      'val_accuracy' → Balanced data (Dog Breed, Cassava)\n      'val_auc'      → Imbalanced (Skin Cancer, ISIC)\n      'val_f1'       → F1 Macro hackathon (TF 2.16+ only)\n      'val_map'      → Multilabel mAP\n      'val_top5_acc' → 100+ classes\n    \"\"\"\n    m = [\n        'accuracy',\n        # AUC: multiclass OvR approximation (training signal হিসেবে ভালো)\n        tf.keras.metrics.AUC(multi_label=True, name='auc'),\n        # PR-AUC as mAP proxy\n        tf.keras.metrics.AUC(curve='PR', multi_label=True, name='map'),\n        # Precision & Recall (micro-averaged — training signal হিসেবে)\n        tf.keras.metrics.Precision(name='pre'),\n        tf.keras.metrics.Recall(name='rec'),\n    ]\n\n    # F1Score: TF 2.16+ (Keras 3) এ আছে, নিচে নেই\n    try:\n        m.append(tf.keras.metrics.F1Score(average='macro', name='f1'))\n    except AttributeError:\n        print(\"  ℹ️  F1Score unavailable (TF < 2.16) — skipped\")\n\n    # Top-5: num_classes > 5 হলে যোগ করো\n    if CFG.task_type == 'multiclass' and CFG.num_classes > 5:\n        m.append(tf.keras.metrics.TopKCategoricalAccuracy(k=5, name='top5_acc'))\n\n    return m\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-02T09:04:34.559852Z","iopub.execute_input":"2026-06-02T09:04:34.560033Z","iopub.status.idle":"2026-06-02T09:04:34.576547Z","shell.execute_reply.started":"2026-06-02T09:04:34.560017Z","shell.execute_reply":"2026-06-02T09:04:34.575788Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# **8. Training Pipeline**","metadata":{}},{"cell_type":"code","source":"def run_pipeline(model_func, model_name):\n    print(f\"\\n{'='*50}\\n🚀 TRAINING: {model_name}\\n{'='*50}\")\n    tf.keras.backend.clear_session()\n    gc.collect()\n\n    model, base = build_model(model_func)\n    spe = len(train_gen)   # steps_per_epoch\n    # 🏎️ Speed hack: spe = min(500, len(train_gen))  ← uncomment করো\n\n    # ✅ CFG থেকে monitor নেওয়া হচ্ছে (hardcode নয়)\n    monitor_metric = getattr(CFG, 'monitor_metric', 'val_accuracy')\n    monitor_mode   = getattr(CFG, 'monitor_mode',   'max')\n\n    # ─── Phase 1: Warm-up (head only) ───────────────────────\n    print(f\"\\n[Phase 1] Warm-up — {CFG.epochs_warmup} epochs\")\n    model.compile(optimizer=tf.keras.optimizers.Adam(1e-3),\n                  loss=LOSS_BASE,\n                  metrics=get_metrics())\n\n    gen_p1_raw = (mixup_generator(make_gen(train_datagen, train_df, shuffle=True))\n                  if CFG.use_mixup else train_gen)\n    gen_p1     = (apply_sample_weights(gen_p1_raw, class_weights_dict)\n                  if (CFG.task_type == 'multiclass' and class_weights_dict)\n                  else gen_p1_raw)\n\n    model.fit(gen_p1,\n              steps_per_epoch=spe,\n              validation_data=val_gen,\n              epochs=CFG.epochs_warmup,\n              verbose=1)    # ✅ FIX: RAM crash থেকে সুরক্ষা\n\n    # ─── Phase 2: Fine-tune (whole model) ───────────────────\n    print(f\"\\n[Phase 2] Fine-tune — {CFG.epochs_finetune} epochs\")\n    base.trainable = True\n\n    lr_sched = CosineDecayWithWarmup(\n        base_lr=1e-5,\n        total_steps=spe * CFG.epochs_finetune,\n        warmup_steps=spe * CFG.warmup_epochs_lr)\n\n    model.compile(optimizer=tf.keras.optimizers.Adam(lr_sched),\n                  loss=LOSS_BASE,\n                  metrics=get_metrics())\n\n    save_path = f'best_{model_name}.keras'\n\n    def _safe_lr(epoch, logs):\n        try:\n            opt    = model.optimizer\n            lr_val = (float(opt.learning_rate(opt.iterations))\n                      if hasattr(opt.learning_rate, '__call__')\n                      else float(opt.learning_rate))\n            print(f\"  LR: {lr_val:.2e}\")\n        except Exception:\n            pass\n\n    callbacks = [\n        ModelCheckpoint(save_path,\n                        monitor=monitor_metric,\n                        save_best_only=True,\n                        mode=monitor_mode,\n                        verbose=1),\n        EarlyStopping(monitor=monitor_metric,\n                      patience=6,\n                      restore_best_weights=True,\n                      verbose=1),\n        CSVLogger(f'{model_name}_log.csv'),\n        LambdaCallback(on_epoch_end=_safe_lr),\n    ]\n\n    gen_p2_raw = (mixup_generator(make_gen(train_datagen, train_df, shuffle=True))\n                  if CFG.use_mixup else train_gen)\n    gen_p2     = (apply_sample_weights(gen_p2_raw, class_weights_dict)\n                  if (CFG.task_type == 'multiclass' and class_weights_dict)\n                  else gen_p2_raw)\n\n    history = model.fit(gen_p2,\n                        steps_per_epoch=spe,\n                        validation_data=val_gen,\n                        epochs=CFG.epochs_finetune,\n                        callbacks=callbacks,\n                        verbose=1)  # ✅ FIX: RAM crash থেকে সুরক্ষা\n\n    _plot_history(history, model_name)\n    _evaluate_model(model, val_df, model_name)\n\n    print(f\"\\n✅ Saved: {save_path}\")\n    del model, base\n    tf.keras.backend.clear_session()\n    gc.collect()\n    return save_path\n\n\ndef _plot_history(history, name):\n    keys = [k for k in history.history if not k.startswith('val_')]\n    n    = max(len(keys), 1)\n    fig, axes = plt.subplots(1, n, figsize=(5 * n, 4))\n    if n == 1:\n        axes = [axes]\n    for ax, k in zip(axes, keys):\n        ax.plot(history.history[k],                      label='Train')\n        ax.plot(history.history.get(f'val_{k}', []),    label='Val')\n        ax.set_title(f'{name} — {k}')\n        ax.legend()\n    plt.tight_layout()\n    plt.savefig(f'{name}_curves.png', dpi=150)\n    plt.show()\n\n\ndef _evaluate_model(model, val_df_eval, name):\n    \"\"\"\n    ✅ Sklearn দিয়ে সব metric একসাথে। try-except দিয়ে safe।\n    কোনোটা fail করলে crash নয় — skip করে এগিয়ে যাবে।\n    \"\"\"\n    val_gen_eval = make_gen(val_datagen, val_df_eval, shuffle=False)\n    y_pred_prob  = model.predict(val_gen_eval, verbose=0)\n\n    if CFG.task_type == 'multiclass':\n        y_pred    = np.argmax(y_pred_prob, axis=1)\n        y_true    = val_gen_eval.classes\n        y_true_oh = tf.keras.utils.to_categorical(y_true, CFG.num_classes)\n\n        print(f\"\\n📊 ── Evaluation: {name} ────────────────────────\")\n\n        # Log Loss (probability submission-এ গুরুত্বপূর্ণ)\n        if CFG.sub_type == 'probability':\n            try:\n                ll = log_loss(y_true_oh, y_pred_prob)\n                print(f\"  Log Loss     : {ll:.5f}  (lower = better)\")\n            except Exception:\n                pass\n\n        # Classification Report (per-class F1, Precision, Recall)\n        class_names = [index_to_class[i] for i in range(CFG.num_classes)]\n        print(classification_report(y_true, y_pred,\n                                    target_names=class_names,\n                                    zero_division=0))\n\n        # Macro AUC (OvR — multiclass-এর সঠিক AUC)\n        try:\n            auc_s = roc_auc_score(y_true_oh, y_pred_prob,\n                                  multi_class='ovr', average='macro')\n            print(f\"  Macro AUC    : {auc_s:.5f}\")\n        except Exception:\n            pass\n\n        # Macro F1\n        try:\n            f1_s = f1_score(y_true, y_pred, average='macro')\n            print(f\"  Macro F1     : {f1_s:.5f}\")\n        except Exception:\n            pass\n\n        # Quadratic Weighted Kappa (ordinal: APTOS, Prostate)\n        try:\n            kappa = cohen_kappa_score(y_true, y_pred, weights='quadratic')\n            print(f\"  QW Kappa     : {kappa:.5f}\")\n        except Exception:\n            pass\n\n        # Mean Average Precision\n        try:\n            map_s = average_precision_score(y_true_oh, y_pred_prob,\n                                            average='macro')\n            print(f\"  mAP          : {map_s:.5f}\")\n        except Exception:\n            pass\n\n        print(\"  \" + \"─\" * 44)\n\n        # Confusion Matrix\n        n_show = min(20, CFG.num_classes)\n        cm     = confusion_matrix(y_true, y_pred)[:n_show, :n_show]\n        plt.figure(figsize=(12, 10))\n        sns.heatmap(cm, annot=True, fmt='d', cmap='Blues',\n                    xticklabels=class_names[:n_show],\n                    yticklabels=class_names[:n_show])\n        plt.title(f'Confusion Matrix — {name}')\n        plt.tight_layout()\n        plt.savefig(f'{name}_cm.png', dpi=150)\n        plt.show()\n\n    else:\n        # Multilabel\n        if val_df_eval is None or CFG.multilabel_cols is None:\n            print(\"⚠️  Multilabel evaluate skipped (directory mode or no multilabel_cols)\")\n            return\n        y_true = val_df_eval[CFG.multilabel_cols].values.astype(int)\n        y_pred = (y_pred_prob > 0.5).astype(int)\n        print(\"\\n📊 Multilabel Report:\")\n        print(classification_report(y_true, y_pred,\n                                    target_names=CFG.multilabel_cols,\n                                    zero_division=0))\n\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-02T09:04:34.577593Z","iopub.execute_input":"2026-06-02T09:04:34.577864Z","iopub.status.idle":"2026-06-02T09:04:34.599227Z","shell.execute_reply.started":"2026-06-02T09:04:34.577836Z","shell.execute_reply":"2026-06-02T09:04:34.598467Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# **9. Model Selection**","metadata":{}},{"cell_type":"code","source":"models_to_train = {\n    # 'EfficientNetV2S': applications.EfficientNetV2S,    # 🥇 Best (default)\n    # 'ResNet50V2':    applications.ResNet50V2,          # 🥈 Reliable\n    'DenseNet121':   applications.DenseNet121,         # 🥉 Imbalanced dataset\n    # 'EfficientNetV2M': applications.EfficientNetV2M,  # 🔋 More time/GPU\n    # 'Xception':      applications.Xception,           # 🍽️ Texture / Food\n}\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-02T10:19:04.737Z","iopub.execute_input":"2026-06-02T10:19:04.737717Z","iopub.status.idle":"2026-06-02T10:19:04.741774Z","shell.execute_reply.started":"2026-06-02T10:19:04.737684Z","shell.execute_reply":"2026-06-02T10:19:04.74087Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# **10. Run Training**","metadata":{}},{"cell_type":"code","source":"saved_paths = []\nfor name, fn in models_to_train.items():\n    p = run_pipeline(fn, name)\n    saved_paths.append(p)\nprint(f\"\\n✅ All trained: {saved_paths}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-02T10:19:11.194323Z","iopub.execute_input":"2026-06-02T10:19:11.194951Z","iopub.status.idle":"2026-06-02T11:27:01.779807Z","shell.execute_reply.started":"2026-06-02T10:19:11.194922Z","shell.execute_reply":"2026-06-02T11:27:01.779095Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# **11. TTA Prediction**","metadata":{}},{"cell_type":"code","source":"def predict_with_tta(model_path, test_files, n_tta=CFG.n_tta):\n    \"\"\"\n    TTA: original + n_tta augmented versions average।\n    compile=False → custom LR schedule load issue নেই।\n    \"\"\"\n    print(f\"\\n📌 Predicting: {Path(model_path).name}\")\n    model  = models.load_model(model_path, compile=False)\n    df_tmp = pd.DataFrame({'filename': test_files})\n\n    def _gen(dg):\n        return dg.flow_from_dataframe(\n            df_tmp,\n            directory=CFG.test_dir,\n            x_col='filename',\n            y_col=None,\n            target_size=CFG.img_size,\n            batch_size=CFG.batch_size,\n            class_mode=None,\n            shuffle=False)\n\n    preds = model.predict(_gen(val_datagen), verbose=1)\n\n    if CFG.use_tta and n_tta > 1:\n        tta_dg = ImageDataGenerator(\n            horizontal_flip=CFG.h_flip,\n            zoom_range=0.10,\n            rotation_range=10,\n            width_shift_range=0.05,\n            height_shift_range=0.05)\n        for i in range(n_tta - 1):\n            tta_dg.seed = i\n            preds = preds + model.predict(_gen(tta_dg), verbose=0)\n        preds = preds / n_tta\n\n    del model\n    tf.keras.backend.clear_session()\n    gc.collect()\n    return preds","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-02T11:27:02.348611Z","iopub.execute_input":"2026-06-02T11:27:02.349281Z","iopub.status.idle":"2026-06-02T11:27:02.355991Z","shell.execute_reply.started":"2026-06-02T11:27:02.349252Z","shell.execute_reply":"2026-06-02T11:27:02.355088Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# **12. Ensemble**","metadata":{}},{"cell_type":"code","source":"print(f\"\\n{'='*50}\\n🎯 UNIVERSAL ENSEMBLE + SUBMISSION\\n{'='*50}\")\n\n# ── Ensemble step এ manually saved_paths দিতে পারো ──\nsaved_paths = ['best_EfficientNetV2S.keras', 'best_DenseNet121.keras']\n\n# স্যাম্পল সাবমিশন লোড ও এনালাইসিস করা\nsample_sub = pd.read_csv(CFG.test_csv)\ntest_ids   = sample_sub[CFG.img_col].astype(str).values\n\n# টেস্ট ফাইলের নাম প্রিপেয়ার করা\ntest_files = ([t + '.jpg' for t in test_ids]\n              if not any('.' in t for t in test_ids[:5])\n              else list(test_ids))\n\n# সব মডেলের প্রেডিকশন সংগ্রহ করা\nall_preds = []\nfor path in saved_paths:\n    p = predict_with_tta(path, test_files)\n    all_preds.append(p.astype('float16'))  # মেমোরি সাশ্রয়ী float16\n    print(f\"  {Path(path).name}: shape={p.shape} | \"\n          f\"conf_mean={p.max(axis=1).mean():.4f}\")\n\n# এভারেজ এনসেম্বল প্রেডিকশন\nensemble_preds = np.mean(all_preds, axis=0)\n\n# স্যাম্পল সাবমিশনের আউটপুট কলামের নাম অটো-ডিটেক্ট করা\ntarget_cols = [c for c in sample_sub.columns if c != CFG.img_col]\nout_col     = target_cols[0] if len(target_cols) > 0 else 'label'\n\n# ── Build submission ──────────────────────────────────\nif CFG.sub_type == 'class':\n    idx_list = np.argmax(ensemble_preds, axis=1)\n    \n    # সাবমিশন ফাইলটি সংখ্যার (Numeric ID) লেবেল আশা করছে কি না তা চেক করা\n    sample_target_dtype = sample_sub[out_col].dtype\n    \n    # ক্লাস ইনডেক্স থেকে ক্লাস নাম নেওয়া\n    first_val = index_to_class[0]\n    is_first_val_numeric = False\n    try:\n        float(first_val)\n        is_first_val_numeric = True\n    except (ValueError, TypeError):\n        pass\n        \n    # যদি ক্যাগলে সংখ্যা চাওয়া হয় কিন্তু জেনারেটর টেক্সট (যেমন: 'bawan') রিটার্ন করে:\n    if np.issubdtype(sample_target_dtype, np.number) and not is_first_val_numeric:\n        print(f\"ℹ️ Detected: Sample submission expects numeric IDs for '{out_col}' but classes are strings.\")\n        print(\"   Automatically mapping to raw indices (0 to 100) for TW Food 101 consistency.\")\n        labels = idx_list\n    else:\n        # সাধারণ টেক্সট সাবমিশন (যেমন: 'dog', 'cat')\n        labels = [index_to_class[i] for i in idx_list]\n        try:\n            # যদি সংখ্যাগুলো স্ট্রিং ফরম্যাটে থাকে (যেমন: '0', '1' -> 0, 1)\n            labels = [int(float(l)) for l in labels]\n        except (ValueError, TypeError):\n            pass\n\n    submission = pd.DataFrame({CFG.img_col: test_ids, out_col: labels})\n\nelif CFG.sub_type == 'probability':\n    # মাল্টি-কলাম প্রোবাবিলিটি সাবমিশন এলাইনমেন্ট\n    pred_df = pd.DataFrame(\n        ensemble_preds,\n        columns=[index_to_class[i] for i in range(CFG.num_classes)])\n    \n    # স্যাম্পল সাবমিশনের সাথে কলাম সিকোয়েন্স হুবহু মিলানো\n    missing_cols = [c for c in target_cols if c not in pred_df.columns]\n    if len(missing_cols) == 0:\n        pred_df = pred_df[target_cols]\n    else:\n        print(f\"⚠️ Warning: Target columns in sample submission do not match class names.\")\n        \n    submission = pd.concat(\n        [pd.DataFrame({CFG.img_col: test_ids}), pred_df], axis=1)\n\n# সাবমিশন সেভ করা\nsubmission.to_csv('submission.csv', index=False)\n\n# ── Sanity check ──────────────────────────────────────\nprint(f\"\\n✅ submission.csv saved!\")\nprint(f\"   Shape     : {submission.shape}  (expected: {sample_sub.shape})\")\nprint(f\"   Col match : {list(submission.columns) == list(sample_sub.columns)}\")\nprint(f\"   NaN count : {submission.isnull().sum().sum()}\")\nprint(submission.head(3).iloc[:, :min(6, len(submission.columns))])\n\n# প্রেডিকশন কনফিডেন্স হিস্টোগ্রাম তৈরি\nmax_conf = ensemble_preds.max(axis=1)\nprint(f\"\\n📊 Confidence: mean={max_conf.mean():.3f} | \"\n      f\">90%: {(max_conf > 0.9).sum()} | <30%: {(max_conf < 0.3).sum()}\")\n\nplt.figure(figsize=(8, 4))\nplt.hist(max_conf, bins=50, color='steelblue', edgecolor='white')\nplt.axvline(0.5, color='red',   ls='--', label='50%')\nplt.axvline(0.9, color='green', ls='--', label='90%')\nplt.title('Prediction Confidence Distribution')\nplt.xlabel('Max Class Probability')\nplt.ylabel('Count')\nplt.legend()\nplt.tight_layout()\nplt.savefig('confidence.png', dpi=150)\nplt.show()\n\nprint(\"\\n\" + \"=\" * 50)\nprint(\"🏆 DONE! Pipeline successfully completed. Good luck! 🚀\")\nprint(\"=\" * 50)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-02T11:27:10.488127Z","iopub.execute_input":"2026-06-02T11:27:10.488853Z","iopub.status.idle":"2026-06-02T11:27:54.570135Z","shell.execute_reply.started":"2026-06-02T11:27:10.488824Z","shell.execute_reply":"2026-06-02T11:27:54.569429Z"}},"outputs":[],"execution_count":null}]}