{"metadata":{"kernelspec":{"display_name":"Python 3","language":"python","name":"python3"},"language_info":{"name":"python","version":"3.11.13","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"gpu","dataSources":[{"sourceId":13836,"databundleVersionId":1718836,"sourceType":"competition"}],"dockerImageVersionId":31153,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"id":"14689a7b-a2cd-46d1-a945-55131f868c59","cell_type":"markdown","source":"## 1) Imports","metadata":{}},{"id":"d5c7c075-5233-4f3c-894c-13b3d07bdf17","cell_type":"code","source":"import numpy as np, pandas as pd, cv2, os, warnings, gc\nwarnings.filterwarnings('ignore')\nimport matplotlib.pyplot as plt\nfrom tensorflow.keras.optimizers.schedules import CosineDecayRestarts\nimport tensorflow as tf\nfrom tensorflow import keras\nfrom tensorflow.keras import layers, optimizers\nfrom tensorflow.keras.applications import EfficientNetB4\nfrom tensorflow.keras.applications.efficientnet import preprocess_input\nfrom tensorflow.keras.callbacks import ModelCheckpoint, EarlyStopping\nfrom tensorflow.keras.mixed_precision import set_global_policy\n\nfrom sklearn.model_selection import train_test_split\nfrom sklearn.metrics import classification_report, confusion_matrix\n\nimport albumentations as A","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-06T22:04:10.877819Z","iopub.execute_input":"2025-11-06T22:04:10.878327Z","iopub.status.idle":"2025-11-06T22:04:28.834635Z","shell.execute_reply.started":"2025-11-06T22:04:10.878304Z","shell.execute_reply":"2025-11-06T22:04:28.834036Z"}},"outputs":[],"execution_count":null},{"id":"108398dc-fac2-4b50-b19e-80645389ec88","cell_type":"markdown","source":"## 2) Config & Seed","metadata":{}},{"id":"60f7fac4-1341-42a5-a78b-12781a38dc77","cell_type":"code","source":"def set_seed(seed=42):\n    np.random.seed(seed); tf.random.set_seed(seed); os.environ['PYTHONHASHSEED']=str(seed)\nset_seed(42); set_global_policy('mixed_float16')\n\nphysical_devices = tf.config.list_physical_devices('GPU')\nif physical_devices:\n    for gpu in physical_devices:\n        try: tf.config.experimental.set_memory_growth(gpu, True)\n        except Exception: pass\n    print(f\"GPU Enabled: {len(physical_devices)} device(s)\")\nelse:\n    print(\"No GPU found\")\ntry: tf.config.optimizer.set_jit(True)\nexcept Exception: pass\n\nclass CFG:\n    data_dir = '/kaggle/input/cassava-leaf-disease-classification'\n    train_dir = '/kaggle/input/cassava-leaf-disease-classification/train_images'\n    img_size = 320          # 380 nếu GPU đủ mạnh\n    num_classes = 5\n    batch_size = 12         # giảm nếu OOM, tăng nếu dư VRAM\n    # epochs\n    stage1_epochs = 3\n    stage2_epochs = 10\n    stage3_epochs = 5\n    # base learning rates\n    stage1_lr = 7e-4\n    stage2_lr = 5e-4\n    stage3_lr = 3e-4\n    # split\n    val_split = 0.2\n    # steps/epoch (có thể chỉnh để trade-off thời gian)\n    steps_per_epoch = 600\n    val_steps = 160\n    # MixUp\n    mixup_prob = 0.5\n    mixup_alpha = 0.2\n\nclass_names = ['CBB','CBSD','CGM','CMD','Healthy']\nprint(\"Config ready\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-06T22:04:28.835857Z","iopub.execute_input":"2025-11-06T22:04:28.836421Z","iopub.status.idle":"2025-11-06T22:04:28.913227Z","shell.execute_reply.started":"2025-11-06T22:04:28.836403Z","shell.execute_reply":"2025-11-06T22:04:28.912661Z"}},"outputs":[],"execution_count":null},{"id":"cb07b2db-f810-48fc-bfcb-23397cd26d26","cell_type":"markdown","source":"## 3) Data & Transforms (No RandomResizedCrop)","metadata":{}},{"id":"65a3701c-bc77-49e4-af73-b210961c5100","cell_type":"code","source":"train_df = pd.read_csv(f'{CFG.data_dir}/train.csv')\ntrain_df['image_path'] = train_df['image_id'].apply(lambda x: f\"{CFG.train_dir}/{x}\")\ntrain_df, valid_df = train_test_split(train_df, test_size=CFG.val_split,\n                                      stratify=train_df['label'], random_state=42)\nprint(\"Train/Valid:\", len(train_df), len(valid_df))\n\ntrain_transform = A.Compose([\n    A.Resize(height=CFG.img_size + 32, width=CFG.img_size + 32),\n    A.RandomCrop(height=CFG.img_size, width=CFG.img_size),\n    A.HorizontalFlip(p=0.5),\n    A.RandomBrightnessContrast(0.15, 0.15, p=0.3),\n    A.CoarseDropout(max_holes=4, max_height=CFG.img_size//12, max_width=CFG.img_size//12, p=0.25),\n])\nvalid_transform = A.Compose([A.Resize(height=CFG.img_size, width=CFG.img_size)])\n\ndef apply_albu_then_preprocess(t, img_rgb):\n    img = t(image=img_rgb)['image'].astype(np.float32)\n    return preprocess_input(img)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-06T22:04:28.913710Z","iopub.execute_input":"2025-11-06T22:04:28.913968Z","iopub.status.idle":"2025-11-06T22:04:28.986833Z","shell.execute_reply.started":"2025-11-06T22:04:28.913950Z","shell.execute_reply":"2025-11-06T22:04:28.986022Z"}},"outputs":[],"execution_count":null},{"id":"3099dd23-2784-41f6-bba7-94b6080152dd","cell_type":"markdown","source":"## 4) Generator with MixUp","metadata":{}},{"id":"b4235018-5433-453a-8324-b730137dc5cf","cell_type":"code","source":"def mixup_batch(x, y, alpha=0.2):\n    if len(x) < 2: return x, y\n    lam = np.random.beta(alpha, alpha)\n    idx = np.random.permutation(len(x))\n    return lam*x + (1-lam)*x[idx], lam*y + (1-lam)*y[idx]\n\nclass SimpleGen(keras.utils.Sequence):\n    def __init__(self, df, transform, batch_size, shuffle=True, mixup=False):\n        self.df = df.reset_index(drop=True)\n        self.t = transform\n        self.bs = batch_size\n        self.shuffle = shuffle\n        self.mixup = mixup\n        self.indexes = np.arange(len(self.df))\n        self.on_epoch_end()\n    def __len__(self): return int(np.ceil(len(self.df)/self.bs))\n    def __getitem__(self, idx):\n        s,e = idx*self.bs, min((idx+1)*self.bs, len(self.df))\n        batch = self.df.iloc[s:e]\n        X, y = [], []\n        for _, row in batch.iterrows():\n            img = cv2.imread(row['image_path'])\n            if img is None: img = np.zeros((CFG.img_size, CFG.img_size, 3), dtype=np.uint8)\n            else: img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)\n            X.append(apply_albu_then_preprocess(self.t, img))\n            y.append(row['label'])\n        X = np.array(X, dtype=np.float32); y = keras.utils.to_categorical(y, CFG.num_classes)\n        if self.mixup and np.random.rand() < CFG.mixup_prob:\n            X, y = mixup_batch(X, y, alpha=CFG.mixup_alpha)\n        return X, y\n    def on_epoch_end(self):\n        if self.shuffle: np.random.shuffle(self.indexes)\n\ntrain_gen = SimpleGen(train_df, train_transform, CFG.batch_size, shuffle=True, mixup=True)\nvalid_gen = SimpleGen(valid_df, valid_transform, CFG.batch_size, shuffle=False, mixup=False)\nprint(\"Generators ready\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-06T22:04:28.988491Z","iopub.execute_input":"2025-11-06T22:04:28.988758Z","iopub.status.idle":"2025-11-06T22:04:29.000045Z","shell.execute_reply.started":"2025-11-06T22:04:28.988740Z","shell.execute_reply":"2025-11-06T22:04:28.999495Z"}},"outputs":[],"execution_count":null},{"id":"7ac532dd-6ebc-4a5e-af34-69d2096e5c7d","cell_type":"markdown","source":"## 5) Optimizer & Cosine Warmup","metadata":{}},{"id":"cf607a71-0617-4a9a-8d24-f2f86547ec2a","cell_type":"code","source":"class CosineWithWarmup(tf.keras.optimizers.schedules.LearningRateSchedule):\n    def __init__(self, base_lr, total_steps, warmup_steps):\n        super().__init__()\n        self.base_lr = float(base_lr)\n        self.total_steps = int(total_steps)\n        self.warmup_steps = int(warmup_steps)\n\n    def __call__(self, step):\n        step = tf.cast(step, tf.float32)\n        total = tf.cast(self.total_steps, tf.float32)\n        warmup = tf.cast(self.warmup_steps, tf.float32)\n\n        # warmup: tăng tuyến tính 0 -> base_lr\n        warm = tf.minimum(step / tf.maximum(warmup, 1.0), 1.0)\n\n        # cosine sau warmup\n        denom = tf.maximum(total - warmup, 1.0)\n        progress = tf.clip_by_value((step - warmup) / denom, 0.0, 1.0)\n        cosine = 0.5 * (1.0 + tf.cos(np.pi * progress))\n\n        lr = self.base_lr * (warm * cosine + (1.0 - warm) * 0.0)\n        return tf.cast(lr, tf.float32)\n\n\ntotal_steps = CFG.steps_per_epoch * (CFG.stage1_epochs + CFG.stage2_epochs + CFG.stage3_epochs)\nwarmup_steps = max(total_steps // 20, 1)\nlr_schedule = CosineWithWarmup(CFG.stage1_lr, total_steps, warmup_steps)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-06T22:04:29.000812Z","iopub.execute_input":"2025-11-06T22:04:29.001075Z","iopub.status.idle":"2025-11-06T22:04:29.013854Z","shell.execute_reply.started":"2025-11-06T22:04:29.001046Z","shell.execute_reply":"2025-11-06T22:04:29.013274Z"}},"outputs":[],"execution_count":null},{"id":"32dd0874-f109-49b5-ab69-a88e328182ea","cell_type":"markdown","source":"## 6) Model (EfficientNet-B4) & Training","metadata":{}},{"id":"9cddf1c7-1aa0-4a89-978b-6961d877a8a9","cell_type":"code","source":"from tensorflow.keras.optimizers.schedules import CosineDecayRestarts\ntry:\n    from tensorflow.keras.optimizers import AdamW\n    USE_ADAMW = True\nexcept Exception:\n    from tensorflow.keras import optimizers\n    USE_ADAMW = False\n\nloss_fn = tf.keras.losses.CategoricalCrossentropy(label_smoothing=0.05)\n\ndef build_model(trainable_layers=0):\n    base = EfficientNetB4(include_top=False, weights='imagenet',\n                          input_shape=(CFG.img_size, CFG.img_size, 3))\n    if trainable_layers == 0:\n        base.trainable = False\n    else:\n        base.trainable = True\n        for layer in base.layers[:-trainable_layers]:\n            layer.trainable = False\n\n    inputs = keras.Input(shape=(CFG.img_size, CFG.img_size, 3))\n    x = base(inputs, training=(trainable_layers > 0))\n    x = layers.GlobalAveragePooling2D()(x)\n    x = layers.Dropout(0.5)(x)\n    x = layers.Dense(256, activation='relu')(x)\n    x = layers.BatchNormalization()(x)\n    x = layers.Dropout(0.3)(x)\n    outputs = layers.Dense(CFG.num_classes, activation='softmax', dtype='float32')(x)\n    return keras.Model(inputs, outputs)\n\nmodel = build_model(0)\nmodel.summary()\n\n# ===== Stage 1 =====\nlr_schedule_1 = CosineDecayRestarts(\n    initial_learning_rate=CFG.stage1_lr,\n    first_decay_steps=CFG.steps_per_epoch * max(CFG.stage1_epochs, 1),\n    t_mul=1.0, m_mul=1.0, alpha=0.0\n)\noptimizer_1 = AdamW(learning_rate=lr_schedule_1, weight_decay=1e-4) if USE_ADAMW else optimizers.Adam(learning_rate=lr_schedule_1)\nmodel.compile(optimizer=optimizer_1, loss=loss_fn, metrics=['accuracy'])\n\ncb1 = [EarlyStopping(monitor='val_accuracy', patience=3, restore_best_weights=True, verbose=1)]\nhistory1 = model.fit(\n    train_gen, validation_data=valid_gen,\n    epochs=CFG.stage1_epochs,\n    steps_per_epoch=min(CFG.steps_per_epoch, len(train_gen)),\n    validation_steps=min(CFG.val_steps, len(valid_gen)),\n    callbacks=cb1, verbose=1\n)\n\n# ===== Stage 2 (unfreeze top 160 layers) =====\nbase = model.layers[1]\nfor layer in base.layers[-160:]:\n    layer.trainable = True\n\nlr_schedule_2 = CosineDecayRestarts(\n    initial_learning_rate=CFG.stage2_lr,\n    first_decay_steps=CFG.steps_per_epoch * max(CFG.stage2_epochs, 1),\n    t_mul=1.0, m_mul=1.0, alpha=0.0\n)\noptimizer_2 = AdamW(learning_rate=lr_schedule_2, weight_decay=1e-4) if USE_ADAMW else optimizers.Adam(learning_rate=lr_schedule_2)\nmodel.compile(optimizer=optimizer_2, loss=loss_fn, metrics=['accuracy'])\n\ncb2 = [\n    ModelCheckpoint('best_b4.h5', monitor='val_accuracy', save_best_only=True, mode='max', verbose=1),\n    EarlyStopping(monitor='val_accuracy', patience=3, restore_best_weights=True, verbose=1),\n]\nhistory2 = model.fit(\n    train_gen, validation_data=valid_gen,\n    epochs=CFG.stage2_epochs,\n    steps_per_epoch=min(CFG.steps_per_epoch, len(train_gen)),\n    validation_steps=min(CFG.val_steps, len(valid_gen)),\n    callbacks=cb2, verbose=1\n)\n\n# ===== Stage 3 =====\nlr_schedule_3 = CosineDecayRestarts(\n    initial_learning_rate=CFG.stage3_lr,\n    first_decay_steps=CFG.steps_per_epoch * max(CFG.stage3_epochs, 1),\n    t_mul=1.0, m_mul=1.0, alpha=0.0\n)\noptimizer_3 = AdamW(learning_rate=lr_schedule_3, weight_decay=1e-4) if USE_ADAMW else optimizers.Adam(learning_rate=lr_schedule_3)\nmodel.compile(optimizer=optimizer_3, loss=loss_fn, metrics=['accuracy'])\n\ncb3 = [EarlyStopping(monitor='val_accuracy', patience=3, restore_best_weights=True, verbose=1)]\nhistory3 = model.fit(\n    train_gen, validation_data=valid_gen,\n    epochs=CFG.stage3_epochs,\n    steps_per_epoch=min(CFG.steps_per_epoch, len(train_gen)),\n    validation_steps=min(CFG.val_steps, len(valid_gen)),\n    callbacks=cb3, verbose=1\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-06T22:04:29.014683Z","iopub.execute_input":"2025-11-06T22:04:29.014935Z"}},"outputs":[],"execution_count":null},{"id":"d205e1a8-fe8c-4a69-8279-b6bf102318d9","cell_type":"markdown","source":"## 7) Evaluation","metadata":{}},{"id":"bbee50b6-6ff2-4369-bf87-f1c82cdcd2c1","cell_type":"code","source":"val_loss, val_acc = model.evaluate(valid_gen, verbose=0)\nprint(f\"Validation Acc: {val_acc*100:.2f}% | Loss: {val_loss:.4f}\")\n\ny_pred_probs = model.predict(valid_gen, verbose=1)\ny_pred = np.argmax(y_pred_probs, axis=1)\ny_true = valid_df['label'].values\n\nprint(\"\\nClassification report:\")\nprint(classification_report(y_true, y_pred, target_names=class_names, digits=4))\n\ncm = confusion_matrix(y_true, y_pred)\nplt.figure(figsize=(8,6))\nimport seaborn as sns\nsns.heatmap(cm, annot=True, fmt='d', cmap='Blues',\n            xticklabels=class_names, yticklabels=class_names)\nplt.xlabel(\"Pred\"); plt.ylabel(\"True\"); plt.title(\"Confusion Matrix\")\nplt.tight_layout(); plt.show()","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"id":"9cedd980-3f88-481c-a656-fa9deba1107d","cell_type":"markdown","source":"## 8) Test Inference — TTA 6-view","metadata":{}},{"id":"f0940653-b03f-4402-9b45-b8bc9d260067","cell_type":"code","source":"test_df = pd.read_csv(f'{CFG.data_dir}/sample_submission.csv')\ntest_df['image_path'] = test_df['image_id'].apply(lambda x: f'{CFG.data_dir}/test_images/{x}')\n\ndef predict_tta6(df, batch_size):\n    t_list = [\n        A.Compose([A.Resize(CFG.img_size, CFG.img_size)]),\n        A.Compose([A.Resize(CFG.img_size+24, CFG.img_size+24), A.CenterCrop(CFG.img_size, CFG.img_size)]),\n        A.Compose([A.Resize(CFG.img_size, CFG.img_size), A.HorizontalFlip(p=1.0)]),\n        A.Compose([A.Resize(CFG.img_size, CFG.img_size), A.VerticalFlip(p=1.0)]),\n        A.Compose([A.Resize(CFG.img_size, CFG.img_size), A.Rotate(limit=10, p=1.0)]),\n        A.Compose([A.Resize(CFG.img_size, CFG.img_size), A.RandomBrightnessContrast(0.1,0.1,p=1.0)]),\n    ]\n    probs = []\n    for t in t_list:\n        preds_all = []\n        for i in range(int(np.ceil(len(df)/batch_size))):\n            s,e = i*batch_size, min((i+1)*batch_size, len(df))\n            batch = df.iloc[s:e]\n            X=[]\n            for _, row in batch.iterrows():\n                img=cv2.imread(row['image_path'])\n                if img is None: img = np.zeros((CFG.img_size, CFG.img_size, 3), dtype=np.uint8)\n                else: img=cv2.cvtColor(img, cv2.COLOR_BGR2RGB)\n                arr = t(image=img)['image'].astype(np.float32)\n                X.append(preprocess_input(arr))\n            X = np.array(X, dtype=np.float32)\n            preds_all.append(model.predict(X, verbose=0))\n        probs.append(np.concatenate(preds_all, axis=0))\n    return np.mean(probs, axis=0)\n\nprobs = predict_tta6(test_df, CFG.batch_size)\npreds = np.argmax(probs, axis=1)\n\nsubmission = pd.read_csv(f'{CFG.data_dir}/sample_submission.csv')\nsubmission['label'] = preds\nsubmission.to_csv('submission.csv', index=False)\nprint(\"Saved submission.csv | head:\")\nprint(submission.head())","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"id":"8db46351-e44d-4f71-aefb-900ae0d31cd6","cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}