{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","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":31193,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"Imports","metadata":{}},{"cell_type":"code","source":"import os, gc, math, random, warnings\nwarnings.filterwarnings(\"ignore\")\n\nimport numpy as np, pandas as pd, cv2, matplotlib.pyplot as plt\nimport tensorflow as tf\nfrom tensorflow import keras\nfrom tensorflow.keras import layers, optimizers\nfrom tensorflow.keras.callbacks import ModelCheckpoint, EarlyStopping\nfrom tensorflow.keras.optimizers.schedules import CosineDecayRestarts\nfrom tensorflow.keras.applications.efficientnet import preprocess_input\nfrom sklearn.model_selection import train_test_split\nfrom sklearn.metrics import classification_report, confusion_matrix\nimport albumentations as A, seaborn as sns\n","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2025-11-10T18:12:55.488613Z","iopub.execute_input":"2025-11-10T18:12:55.489380Z","iopub.status.idle":"2025-11-10T18:12:55.494336Z","shell.execute_reply.started":"2025-11-10T18:12:55.489356Z","shell.execute_reply":"2025-11-10T18:12:55.493489Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"Seed, Mixed Precision, GPU","metadata":{}},{"cell_type":"code","source":"def set_seed(seed=42):\n    os.environ[\"PYTHONHASHSEED\"] = str(seed)\n    random.seed(seed)\n    np.random.seed(seed)\n    tf.random.set_seed(seed)\n\n\nset_seed(42)\n\n# Mixed precision an toàn cho mọi bản TF trên Kaggle\ntry:\n    from tensorflow.keras import mixed_precision\n    mixed_precision.set_global_policy(\"mixed_float16\")\n    print(\"[INFO] Mixed precision enabled\")\nexcept Exception as e:\n    print(\"[WARN] Mixed precision not available:\", e)\n\n# GPU memory growth & XLA (tuỳ bản TF)\ntry:\n    gpus = tf.config.list_physical_devices('GPU')\n    if gpus:\n        for g in gpus:\n            tf.config.experimental.set_memory_growth(g, True)\n        print(f\"[INFO] GPUs: {len(gpus)}\")\n    else:\n        print(\"[INFO] No GPU detected\")\nexcept Exception as e:\n    print(\"[WARN] GPU config:\", e)\n\ntry:\n    tf.config.optimizer.set_jit(True)\n    print(\"[INFO] XLA JIT enabled\")\nexcept Exception:\n    pass\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-10T17:04:38.928139Z","iopub.execute_input":"2025-11-10T17:04:38.929208Z","iopub.status.idle":"2025-11-10T17:04:39.014285Z","shell.execute_reply.started":"2025-11-10T17:04:38.929179Z","shell.execute_reply":"2025-11-10T17:04:39.013381Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"CFG","metadata":{}},{"cell_type":"code","source":"class CFG:\n    data_dir = '/kaggle/input/cassava-leaf-disease-classification'\n    train_dir = '/kaggle/input/cassava-leaf-disease-classification/train_images'\n\n    img_size = 320  # 380 nếu GPU mạnh\n    num_classes = 5\n    batch_size = 16\n\n    stage1_epochs = 3\n    stage2_epochs = 10\n    stage3_epochs = 5\n\n    stage1_lr = 7e-4\n    stage2_lr = 5e-4\n    stage3_lr = 3e-4\n\n    val_split = 0.2\n\nclass_names = ['CBB', 'CBSD', 'CGM', 'CMD', 'Healthy']\nprint(\"CFG ready\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-10T17:04:42.078508Z","iopub.execute_input":"2025-11-10T17:04:42.078791Z","iopub.status.idle":"2025-11-10T17:04:42.084033Z","shell.execute_reply.started":"2025-11-10T17:04:42.078770Z","shell.execute_reply":"2025-11-10T17:04:42.083379Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"Dataframe & Đường dẫn","metadata":{}},{"cell_type":"code","source":"train_csv = os.path.join(CFG.data_dir, 'train.csv')\nsample_sub_csv = os.path.join(CFG.data_dir, 'sample_submission.csv')\n\n\ntrain_df = pd.read_csv(train_csv)\nassert {'image_id','label'}.issubset(train_df.columns)\n\n\n# image_id của Cassava ĐÃ có đuôi .jpg\ntrain_df['image_path'] = train_df['image_id'].apply(lambda x: f\"{CFG.train_dir}/{x}\")\n\n\ntrain_df, valid_df = train_test_split(\ntrain_df, test_size=CFG.val_split, stratify=train_df['label'], random_state=42\n)\ntrain_df = train_df.reset_index(drop=True)\nvalid_df = valid_df.reset_index(drop=True)\nprint(\"Train/Valid:\", len(train_df), len(valid_df))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-10T17:04:44.510257Z","iopub.execute_input":"2025-11-10T17:04:44.510997Z","iopub.status.idle":"2025-11-10T17:04:44.566663Z","shell.execute_reply.started":"2025-11-10T17:04:44.510958Z","shell.execute_reply":"2025-11-10T17:04:44.566046Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"Tiền xử lý & Augment (Albumentations) + preprocess_input","metadata":{}},{"cell_type":"code","source":"train_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([\n    A.Resize(height=CFG.img_size, width=CFG.img_size)\n])\n\ndef read_rgb(path):\n    img = cv2.imread(path, cv2.IMREAD_COLOR)\n    if img is None:\n        raise FileNotFoundError(path)\n    img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)\n    return img\n\ndef to_model_input(img_rgb, transform):\n    img = transform(image=img_rgb)['image'].astype(np.float32)\n    img = preprocess_input(img)  # GIỮ nguyên chuẩn hoá\n    return img\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-10T17:05:41.453280Z","iopub.execute_input":"2025-11-10T17:05:41.453940Z","iopub.status.idle":"2025-11-10T17:05:41.467086Z","shell.execute_reply.started":"2025-11-10T17:05:41.453906Z","shell.execute_reply":"2025-11-10T17:05:41.466352Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"Generator + MixUp (fix shuffle bằng indexes)","metadata":{}},{"cell_type":"code","source":"def mixup_batch(x, y, alpha=0.2):\n    if len(x) < 2:\n        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\n\nclass SimpleGen(keras.utils.Sequence):\n    def __init__(self, df, transform, batch_size, shuffle=True, mixup=False, mixup_alpha=0.2, num_classes=5):\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.mixup_alpha = mixup_alpha\n        self.num_classes = num_classes\n        self.indexes = np.arange(len(self.df))\n        self.on_epoch_end()\n\n    def __len__(self):\n        return int(np.ceil(len(self.df) / self.bs))\n\n    def __getitem__(self, i):\n        idxs = self.indexes[i * self.bs : (i + 1) * self.bs]\n        batch = self.df.iloc[idxs]\n        X, y = [], []\n        for _, row in batch.iterrows():\n            img = read_rgb(row['image_path'])\n            X.append(to_model_input(img, self.t))\n            y.append(row['label'])\n        X = np.asarray(X, dtype=np.float32)\n        y = keras.utils.to_categorical(y, self.num_classes)\n        if self.mixup and np.random.rand() < 0.5:\n            X, y = mixup_batch(X, y, self.mixup_alpha)\n        return X, y\n\n    def on_epoch_end(self):\n        if self.shuffle:\n            np.random.shuffle(self.indexes)\n\n\ntrain_gen = SimpleGen(train_df, train_transform, CFG.batch_size, shuffle=True, mixup=True, mixup_alpha=0.2, num_classes=CFG.num_classes)\nvalid_gen = SimpleGen(valid_df, valid_transform, CFG.batch_size, shuffle=False, mixup=False, num_classes=CFG.num_classes)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-10T17:06:52.445287Z","iopub.execute_input":"2025-11-10T17:06:52.445929Z","iopub.status.idle":"2025-11-10T17:06:52.457040Z","shell.execute_reply.started":"2025-11-10T17:06:52.445907Z","shell.execute_reply":"2025-11-10T17:06:52.456463Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"CropNet","metadata":{}},{"cell_type":"code","source":"def se_block(x, r=8, name_prefix=\"se\"):\n    f = int(x.shape[-1])\n    s = layers.GlobalAveragePooling2D(name=f\"{name_prefix}_gap\")(x)\n    s = layers.Dense(max(f // r, 4), activation='relu', name=f\"{name_prefix}_fc1\")(s)\n    s = layers.Dense(f, activation='sigmoid', name=f\"{name_prefix}_fc2\")(s)\n    s = layers.Reshape((1, 1, f), name=f\"{name_prefix}_reshape\")(s)\n    return layers.Multiply(name=f\"{name_prefix}_scale\")([x, s])\n\n\ndef dw_se_block(x, out_ch, s=1, drop=0.0, name_prefix='b'):\n    x_in = x\n    x = layers.DepthwiseConv2D(3, strides=s, padding='same', use_bias=False, name=f\"{name_prefix}_dw\")(x)\n    x = layers.BatchNormalization(name=f\"{name_prefix}_dw_bn\")(x)\n    x = layers.Activation('swish', name=f\"{name_prefix}_dw_swish\")(x)\n\n    x = layers.Conv2D(out_ch, 1, padding='same', use_bias=False, name=f\"{name_prefix}_pw\")(x)\n    x = layers.BatchNormalization(name=f\"{name_prefix}_pw_bn\")(x)\n    x = layers.Activation('swish', name=f\"{name_prefix}_pw_swish\")(x)\n\n    x = se_block(x, r=8, name_prefix=f\"{name_prefix}_se\")\n    if drop and drop > 0:\n        x = layers.Dropout(drop, name=f\"{name_prefix}_drop\")(x)\n    if s == 1 and x_in.shape[-1] == out_ch:\n        x = layers.Add(name=f\"{name_prefix}_add\")([x_in, x])\n    return x\n\n\ndef build_cropnet(input_shape=(320, 320, 3), num_classes=5, drop_rate=0.3):\n    inputs = layers.Input(shape=input_shape)\n    x = layers.Conv2D(40, 3, strides=2, padding='same', use_bias=False, name='stem_conv')(inputs)\n    x = layers.BatchNormalization(name='stem_bn')(x)\n    x = layers.Activation('swish', name='stem_swish')(x)\n\n    x = dw_se_block(x, 48, s=1, drop=0.0,  name_prefix='stage1_b1')\n    x = dw_se_block(x, 48, s=1, drop=0.0,  name_prefix='stage1_b2')\n\n    x = dw_se_block(x, 80, s=2, drop=0.05, name_prefix='stage2_b1')\n    x = dw_se_block(x, 80, s=1, drop=0.05, name_prefix='stage2_b2')\n\n    x = dw_se_block(x, 128, s=2, drop=0.10, name_prefix='stage3_b1')\n    x = dw_se_block(x, 128, s=1, drop=0.10, name_prefix='stage3_b2')\n    x = dw_se_block(x, 128, s=1, drop=0.10, name_prefix='stage3_b3')\n\n    x = dw_se_block(x, 192, s=2, drop=0.15, name_prefix='stage4_b1')\n    x = dw_se_block(x, 192, s=1, drop=0.15, name_prefix='stage4_b2')\n\n    x = layers.GlobalAveragePooling2D(name='head_gap')(x)\n    if drop_rate and drop_rate > 0:\n        x = layers.Dropout(drop_rate, name='head_drop')(x)\n    outputs = layers.Dense(num_classes, activation='softmax', dtype='float32', name='pred')(x)\n    return keras.Model(inputs, outputs, name='CropNet')\n\n\nmodel = build_cropnet(input_shape=(CFG.img_size, CFG.img_size, 3),\n                      num_classes=CFG.num_classes, drop_rate=0.3)\nloss_fn = keras.losses.CategoricalCrossentropy(label_smoothing=0.05)\n\n\ndef make_adamw(lr):\n    try:\n        from tensorflow.keras.optimizers import AdamW\n        return AdamW(learning_rate=lr, weight_decay=1e-4)\n    except Exception:\n        try:\n            import tensorflow_addons as tfa\n            return tfa.optimizers.AdamW(learning_rate=lr, weight_decay=1e-4)\n        except Exception:\n            return optimizers.Adam(learning_rate=lr)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-10T17:08:44.881887Z","iopub.execute_input":"2025-11-10T17:08:44.882671Z","iopub.status.idle":"2025-11-10T17:08:46.629078Z","shell.execute_reply.started":"2025-11-10T17:08:44.882646Z","shell.execute_reply":"2025-11-10T17:08:46.628226Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"Training 3 Stage","metadata":{}},{"cell_type":"code","source":"# Stage 1 — train toàn bộ (vì CropNet không có pretrain)\nopt1 = make_adamw(CFG.stage1_lr)\nmodel.compile(optimizer=opt1, loss=loss_fn, metrics=['accuracy'])\ncb = [EarlyStopping(monitor='val_accuracy', patience=3, restore_best_weights=True, verbose=1)]\nhistory1 = model.fit(\ntrain_gen,\nvalidation_data=valid_gen,\nepochs=CFG.stage1_epochs,\nverbose=1\n)\n\n\n# Stage 2 — tiếp tục huấn luyện với LR nhỏ hơn\nopt2 = make_adamw(CFG.stage2_lr)\nmodel.compile(optimizer=opt2, loss=loss_fn, metrics=['accuracy'])\nhistory2 = model.fit(\ntrain_gen,\nvalidation_data=valid_gen,\nepochs=CFG.stage2_epochs,\nverbose=1,\ncallbacks=cb\n)\n\n\n# Stage 3 — fine-tune thêm, LR thấp nhất\nopt3 = make_adamw(CFG.stage3_lr)\nmodel.compile(optimizer=opt3, loss=loss_fn, metrics=['accuracy'])\nhistory3 = model.fit(\ntrain_gen,\nvalidation_data=valid_gen,\nepochs=CFG.stage3_epochs,\nverbose=1,\ncallbacks=cb\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-10T17:09:44.966652Z","iopub.execute_input":"2025-11-10T17:09:44.967170Z","iopub.status.idle":"2025-11-10T18:00:20.372480Z","shell.execute_reply.started":"2025-11-10T17:09:44.967145Z","shell.execute_reply":"2025-11-10T18:00:20.371790Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"Evaluation","metadata":{}},{"cell_type":"code","source":"val_probs = model.predict(valid_gen, verbose=0)\nval_pred = val_probs.argmax(axis=1)\nval_true = valid_df['label'].values\n\nprint(\"\\n[Classification Report]\\n\")\nprint(classification_report(val_true, val_pred, target_names=class_names, digits=4))\n\ncm = confusion_matrix(val_true, val_pred)\nplt.figure(figsize=(7, 6))\nsns.heatmap(cm, annot=True, fmt='d', cmap='Blues',\n            xticklabels=class_names, yticklabels=class_names)\nplt.xlabel('Pred')\nplt.ylabel('True')\nplt.title('Confusion Matrix')\nplt.tight_layout()\nplt.show()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-10T18:13:30.764893Z","iopub.execute_input":"2025-11-10T18:13:30.765226Z","iopub.status.idle":"2025-11-10T18:14:00.811358Z","shell.execute_reply.started":"2025-11-10T18:13:30.765202Z","shell.execute_reply":"2025-11-10T18:14:00.810511Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"Test Inference (TTA 6-view) & Submission","metadata":{}},{"cell_type":"code","source":"sub_df = pd.read_csv(sample_sub_csv)\nassert 'image_id' in sub_df.columns\nsub_df['image_path'] = sub_df['image_id'].apply(lambda x: f\"{CFG.data_dir}/test_images/{x}\")\n\nTTA_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\ndef predict_tta6(df, batch_size=CFG.batch_size):\n    probs_all = []\n    for t in TTA_LIST:\n        local_probs = []\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 = read_rgb(row['image_path'])\n                X.append(to_model_input(img, t))\n            X = np.asarray(X, dtype=np.float32)\n            local_probs.append(model.predict(X, verbose=0))\n        probs_all.append(np.concatenate(local_probs, axis=0))\n    return np.mean(probs_all, axis=0)\n\nprobs = predict_tta6(sub_df, CFG.batch_size)\npreds = probs.argmax(axis=1)\n\nsubmission = pd.read_csv(sample_sub_csv)\nsubmission['label'] = preds\nsubmission.to_csv('submission.csv', index=False)\nprint(\"Saved submission.csv | head:\\n\", submission.head())\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-10T18:15:45.393282Z","iopub.execute_input":"2025-11-10T18:15:45.393591Z","iopub.status.idle":"2025-11-10T18:15:48.663829Z","shell.execute_reply.started":"2025-11-10T18:15:45.393570Z","shell.execute_reply":"2025-11-10T18:15:48.663052Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Cell 11 — Dọn bộ nhớ (tuỳ chọn)\n# =============================\ndel train_gen, valid_gen, train_df, valid_df; gc.collect();","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-10T18:16:36.921922Z","iopub.execute_input":"2025-11-10T18:16:36.922722Z","iopub.status.idle":"2025-11-10T18:16:37.584412Z","shell.execute_reply.started":"2025-11-10T18:16:36.922676Z","shell.execute_reply":"2025-11-10T18:16:37.583712Z"}},"outputs":[],"execution_count":null}]}