{"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":31153,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"import pandas as pd\nimport numpy as np\nimport matplotlib.pyplot as plt\nimport seaborn as sns\nfrom sklearn.model_selection import train_test_split  \nimport cv2  \nimport os\nfrom PIL import Image  \nimport warnings\nimport torch\nfrom torch.utils.data import WeightedRandomSampler\nwarnings.filterwarnings('ignore')\n\nplt.style.use('default')\nsns.set_palette(\"husl\")\n","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2025-11-05T03:20:21.122587Z","iopub.execute_input":"2025-11-05T03:20:21.122997Z","iopub.status.idle":"2025-11-05T03:20:26.234574Z","shell.execute_reply.started":"2025-11-05T03:20:21.122977Z","shell.execute_reply":"2025-11-05T03:20:26.233259Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import tensorflow as tf\nfrom tensorflow.keras.mixed_precision import set_global_policy\n\nphysical_devices = tf.config.list_physical_devices()\nprint(\"Available physical devices:\", physical_devices)\n\ngpus = tf.config.list_physical_devices('GPU')\nif gpus:\n    try:\n\n        tf.config.set_visible_devices(gpus[0], 'GPU')\n        \n        tf.config.experimental.set_memory_growth(gpus[0], True)\n        \n        set_global_policy('mixed_float16')\n        \n        print(\"GPU is available and set for use:\", gpus)\n        print(\"Mixed precision enabled for faster training.\")\n    except RuntimeError as e:\n        print(\"Error setting GPU:\", e)\nelse:\n    print(\"No GPU detected. Falling back to CPU. Please check Kaggle accelerator settings.\")\n\n# Optional: In version TF để confirm\nprint(\"TensorFlow version:\", tf.__version__)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-05T03:20:26.236281Z","iopub.execute_input":"2025-11-05T03:20:26.236777Z","iopub.status.idle":"2025-11-05T03:20:38.542534Z","shell.execute_reply.started":"2025-11-05T03:20:26.236757Z","shell.execute_reply":"2025-11-05T03:20:38.541674Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Overview","metadata":{}},{"cell_type":"code","source":"# Data \ndata_path = '/kaggle/input/cassava-leaf-disease-classification/'","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-05T03:20:38.543460Z","iopub.execute_input":"2025-11-05T03:20:38.544072Z","iopub.status.idle":"2025-11-05T03:20:38.547544Z","shell.execute_reply.started":"2025-11-05T03:20:38.544043Z","shell.execute_reply":"2025-11-05T03:20:38.546751Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Đọc và đếm dữ liệu từ tập train:\ntrain_csv_path = os.path.join(data_path, 'train.csv')\ntrain_df = pd.read_csv(train_csv_path)\ntrain_df.head(10)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-05T03:20:38.549300Z","iopub.execute_input":"2025-11-05T03:20:38.549530Z","iopub.status.idle":"2025-11-05T03:20:38.625192Z","shell.execute_reply.started":"2025-11-05T03:20:38.549514Z","shell.execute_reply":"2025-11-05T03:20:38.624529Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Xem overview\nprint(\"Shape:\", train_df.shape)\nprint(\"\\nThông tin cột:\")\nprint(train_df.info())\nprint(\"\\nSố lượng unique labels:\", train_df['label'].nunique())","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-05T03:20:38.625975Z","iopub.execute_input":"2025-11-05T03:20:38.626187Z","iopub.status.idle":"2025-11-05T03:20:38.649041Z","shell.execute_reply.started":"2025-11-05T03:20:38.626169Z","shell.execute_reply":"2025-11-05T03:20:38.648410Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Kiểm tra ảnh trùng lặp","metadata":{}},{"cell_type":"code","source":"# Kiểm tra duplicate\nduplicates = train_df['image_id'].duplicated().sum()\nprint(f\"Số duplicate image_id: {duplicates}\")\n\nif duplicates > 0:\n    train_df = train_df.drop_duplicates(subset=['image_id'], keep='first')\n    print(\"Đã drop duplicates!\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-05T03:20:38.649807Z","iopub.execute_input":"2025-11-05T03:20:38.650064Z","iopub.status.idle":"2025-11-05T03:20:38.664852Z","shell.execute_reply.started":"2025-11-05T03:20:38.650037Z","shell.execute_reply":"2025-11-05T03:20:38.664118Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Kiểm tra phân phối của các nhãn","metadata":{}},{"cell_type":"code","source":"train_df['label'].value_counts()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-05T03:20:38.665604Z","iopub.execute_input":"2025-11-05T03:20:38.665942Z","iopub.status.idle":"2025-11-05T03:20:38.682299Z","shell.execute_reply.started":"2025-11-05T03:20:38.665919Z","shell.execute_reply":"2025-11-05T03:20:38.681724Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import json\nwith open('../input/cassava-leaf-disease-classification/label_num_to_disease_map.json') as file:\n    print(json.dumps(json.loads(file.read()), indent=4))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-05T03:20:38.683074Z","iopub.execute_input":"2025-11-05T03:20:38.683797Z","iopub.status.idle":"2025-11-05T03:20:38.703619Z","shell.execute_reply.started":"2025-11-05T03:20:38.683778Z","shell.execute_reply":"2025-11-05T03:20:38.702886Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import matplotlib.pyplot as plt\nimport seaborn as sns\n\nplt.style.use('default')  \nsns.set_palette(\"viridis\")  \n\nplt.figure(figsize=(12, 6))  \nax = sns.countplot(data=train_df, x='label', palette='viridis')\n\nplt.title('Phân phối các lớp bệnh lá sắn (Cassava Leaf Disease)', fontsize=18, fontweight='bold', pad=20)\nplt.xlabel('Lớp bệnh (0: CBB, 1: CBSD, 2: CGM, 3: CMD, 4: Healthy)', fontsize=14)\nplt.ylabel('Số lượng ảnh', fontsize=14)\n\nlabel_names = ['CBB', 'CBSD', 'CGM', 'CMD', 'Healthy']\nplt.xticks(ticks=range(5), labels=label_names, rotation=45, ha='right')\n\nax.grid(axis='y', alpha=0.3, linestyle='--')\n\nplt.tight_layout()\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-05T03:20:38.704377Z","iopub.execute_input":"2025-11-05T03:20:38.704664Z","iopub.status.idle":"2025-11-05T03:20:38.994989Z","shell.execute_reply.started":"2025-11-05T03:20:38.704640Z","shell.execute_reply":"2025-11-05T03:20:38.994136Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import matplotlib.pyplot as plt\n\n# Tính số lượng từng label \nlabel_counts = train_df['label'].value_counts().sort_index()\n\n# Tên lớp đơn giản\nlabel_names = ['CBB', 'CBSD', 'CGM', 'CMD', 'Healthy']\ncolors = ['red', 'blue', 'green', 'orange', 'purple']\n\n# Vẽ pie chart \nplt.figure(figsize=(8, 6))\nplt.pie(label_counts.values, \n        labels=[label_names[i] for i in label_counts.index], \n        autopct='%1.1f%%', \n        startangle=90,\n        colors=colors) \n\nplt.title('Phân phối lớp bệnh lá sắn')\nplt.axis('equal')\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-05T03:20:38.997286Z","iopub.execute_input":"2025-11-05T03:20:38.997727Z","iopub.status.idle":"2025-11-05T03:20:39.121979Z","shell.execute_reply.started":"2025-11-05T03:20:38.997708Z","shell.execute_reply":"2025-11-05T03:20:39.121178Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"#### Có thể nhận thấy phân phối đang bị lệch vì tập dữ liệu đang có quá nhiều ảnh của lớp bệnh CMD","metadata":{}},{"cell_type":"markdown","source":"## EDA (Exploratory Data Analysis)","metadata":{}},{"cell_type":"markdown","source":"### Lớp bệnh đầu tiên","metadata":{}},{"cell_type":"code","source":"# Lấy và hiển thị 6 ảnh ngẫu nhiên từ lớp bệnh Bacterial Blight\n\nsample = train_df[train_df.label == 0].sample(6, random_state=42) \nplt.figure(figsize=(12, 8))\n\nfor ind, (image_id, label) in enumerate(zip(sample.image_id, sample.label)):\n    plt.subplot(2, 3, ind + 1)\n    \n    img_path = os.path.join(\"../input/cassava-leaf-disease-classification/train_images\", image_id)\n    image = cv2.imread(img_path)\n    image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)\n    \n    plt.imshow(image)\n    plt.axis(\"off\")\n    \nplt.suptitle('Cassava Bacterial Blight', fontsize=14, fontweight='bold')\nplt.tight_layout()  \nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-05T03:20:39.122611Z","iopub.execute_input":"2025-11-05T03:20:39.122805Z","iopub.status.idle":"2025-11-05T03:20:40.070479Z","shell.execute_reply.started":"2025-11-05T03:20:39.122790Z","shell.execute_reply":"2025-11-05T03:20:40.069601Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"#### Tất cả 6 ảnh đều thể hiện dấu hiệu điển hình của bệnh do vi khuẩn Xanthomonas gây ra: đốm nâu/xanh lá úa vàng trên lá, vết hoại tử với viền ướt, và lá bị cong queo hoặc rũ xuống.","metadata":{}},{"cell_type":"markdown","source":"### Lớp bệnh thứ 2","metadata":{}},{"cell_type":"code","source":"sample = train_df[train_df.label == 1].sample(6, random_state=42)  \n\nplt.figure(figsize=(12, 8))\nfor ind, (image_id, label) in enumerate(zip(sample.image_id, sample.label)):\n    plt.subplot(2, 3, ind + 1)\n    \n    img_path = os.path.join(\"../input/cassava-leaf-disease-classification/train_images\", image_id)\n    image = cv2.imread(img_path)\n    image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)\n    \n    plt.imshow(image)\n    plt.axis(\"off\")\n\nplt.suptitle('Cassava Brown Streak Disease', fontsize=14, fontweight='bold')\nplt.tight_layout() \nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-05T03:20:40.071418Z","iopub.execute_input":"2025-11-05T03:20:40.071678Z","iopub.status.idle":"2025-11-05T03:20:40.989856Z","shell.execute_reply.started":"2025-11-05T03:20:40.071660Z","shell.execute_reply":"2025-11-05T03:20:40.988747Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"#### Các ảnh thể hiện rõ dấu hiệu virus: đốm nâu/chlorosis (vàng lá) ở mặt dưới lá, vết sọc nâu không đều, và lá bị xoăn mép hoặc khô giòn","metadata":{}},{"cell_type":"markdown","source":"### Lớp bệnh thứ 3","metadata":{}},{"cell_type":"code","source":"sample = train_df[train_df.label == 2].sample(6, random_state=42)  \n\nplt.figure(figsize=(12, 8))\nfor ind, (image_id, label) in enumerate(zip(sample.image_id, sample.label)):\n    plt.subplot(2, 3, ind + 1)\n    \n    img_path = os.path.join(\"../input/cassava-leaf-disease-classification/train_images\", image_id)\n    image = cv2.imread(img_path)\n    image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)\n    \n    plt.imshow(image)\n    plt.axis(\"off\")\n\nplt.suptitle('Cassava Green Mottle', fontsize=14, fontweight='bold')\nplt.tight_layout() \nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-05T03:20:40.991203Z","iopub.execute_input":"2025-11-05T03:20:40.991426Z","iopub.status.idle":"2025-11-05T03:20:41.975219Z","shell.execute_reply.started":"2025-11-05T03:20:40.991410Z","shell.execute_reply":"2025-11-05T03:20:41.974380Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"#### Ảnh thể hiện rõ tác hại của ve xanh (Mononychellus tanajoa): đốm vàng nhỏ lốm đốm trên mặt dưới lá, lá xoăn và khô ở rìa, kèm theo việc lá bị vàng nhẹ","metadata":{}},{"cell_type":"markdown","source":"### Lớp bệnh thứ 4","metadata":{}},{"cell_type":"code","source":"sample = train_df[train_df.label == 3].sample(6, random_state=42)  \n\nplt.figure(figsize=(12, 8))\nfor ind, (image_id, label) in enumerate(zip(sample.image_id, sample.label)):\n    plt.subplot(2, 3, ind + 1)\n    \n    img_path = os.path.join(\"../input/cassava-leaf-disease-classification/train_images\", image_id)\n    image = cv2.imread(img_path)\n    image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)\n    \n    plt.imshow(image)\n    plt.axis(\"off\")\n\nplt.suptitle('Cassava Mosaic Disease', fontsize=14, fontweight='bold')\nplt.tight_layout() \nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-05T03:20:41.976150Z","iopub.execute_input":"2025-11-05T03:20:41.976374Z","iopub.status.idle":"2025-11-05T03:20:43.147796Z","shell.execute_reply.started":"2025-11-05T03:20:41.976356Z","shell.execute_reply":"2025-11-05T03:20:43.146927Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"#### Lá vàng-xanh lốm đốm theo hình vuông hoặc xoắn ốc, biến dạng nhỏ, xoăn và bị vàng nghiêm trọng","metadata":{}},{"cell_type":"markdown","source":"### Lớp cây khỏe mạnh","metadata":{}},{"cell_type":"code","source":"sample = train_df[train_df.label == 4].sample(6, random_state=42)  \n\nplt.figure(figsize=(12, 8))\nfor ind, (image_id, label) in enumerate(zip(sample.image_id, sample.label)):\n    plt.subplot(2, 3, ind + 1)\n    \n    img_path = os.path.join(\"../input/cassava-leaf-disease-classification/train_images\", image_id)\n    image = cv2.imread(img_path)\n    image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)\n    \n    plt.imshow(image)\n    plt.axis(\"off\")\n\nplt.suptitle('Healthy', fontsize=14, fontweight='bold')\nplt.tight_layout() \nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-05T03:20:43.148890Z","iopub.execute_input":"2025-11-05T03:20:43.149257Z","iopub.status.idle":"2025-11-05T03:20:44.060751Z","shell.execute_reply.started":"2025-11-05T03:20:43.149227Z","shell.execute_reply":"2025-11-05T03:20:44.059525Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"#### Lá sắn khỏe: màu xanh đều đặn, bóng mịn, lá to, thẳng, không xoăn hay đốm, và cấu trúc gân lá rõ ràng. Ảnh thứ 6 là lá khô tự nhiên, đúng theo chu trình của tự nhiên.","metadata":{}},{"cell_type":"markdown","source":"## **Preprocesing data**","metadata":{}},{"cell_type":"code","source":"train_img_dir = os.path.join(data_path, 'train_images')\n\n# Thêm đường dẫn đầy đủ vào DataFrame để tiện dùng\ntrain_df['file_path'] = train_df['image_id'].apply(lambda x: os.path.join(train_img_dir, x))\ntrain_df.head()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-05T03:20:44.061701Z","iopub.execute_input":"2025-11-05T03:20:44.061972Z","iopub.status.idle":"2025-11-05T03:20:44.090161Z","shell.execute_reply.started":"2025-11-05T03:20:44.061953Z","shell.execute_reply":"2025-11-05T03:20:44.089330Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"train_df, val_df = train_test_split(\n    train_df,\n    test_size=0.15,\n    stratify=train_df['label'],\n    random_state=42\n)\n\nprint(f\"Số lượng ảnh train: {len(train_df)}, val: {len(val_df)}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-05T03:20:44.091011Z","iopub.execute_input":"2025-11-05T03:20:44.091214Z","iopub.status.idle":"2025-11-05T03:20:44.116520Z","shell.execute_reply.started":"2025-11-05T03:20:44.091198Z","shell.execute_reply":"2025-11-05T03:20:44.115875Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"from sklearn.utils.class_weight import compute_class_weight\n\ntrain_labels_for_weights = train_df['label'].values\nclasses = np.unique(train_labels_for_weights)\n\nclass_weights = compute_class_weight(\n    class_weight='balanced',\n    classes=classes,\n    y=train_labels_for_weights\n)\n\nclass_weight_dict = dict(zip(classes, class_weights))\n\nprint(\"Danh sách các lớp:\", classes)\nprint(\"Trọng số tương ứng cho từng lớp:\", class_weights)\nprint(\"\\nTừ điển Class Weight cho Keras model.fit():\")\nprint(class_weight_dict)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-05T03:20:44.117144Z","iopub.execute_input":"2025-11-05T03:20:44.117320Z","iopub.status.idle":"2025-11-05T03:20:44.127187Z","shell.execute_reply.started":"2025-11-05T03:20:44.117305Z","shell.execute_reply":"2025-11-05T03:20:44.126473Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import tensorflow as tf\n\nIMAGE_SIZE = (224, 224) \nBATCH_SIZE = 32\n\ntrain_filepaths = train_df['file_path'].values\ntrain_labels = train_df['label'].astype(np.int32).values\n\nval_filepaths = val_df['file_path'].values\nval_labels = val_df['label'].astype(np.int32).values\n\ndata_augmentation = tf.keras.Sequential([\n    tf.keras.layers.RandomFlip(\"horizontal\"),\n    tf.keras.layers.RandomRotation(0.2),\n    tf.keras.layers.RandomZoom(height_factor=0.2, width_factor=0.2),\n    tf.keras.layers.RandomContrast(0.2),\n], name=\"data_augmentation\")\n\n\ndef build_dataset(filepaths, labels, is_training=True):\n    dataset = tf.data.Dataset.from_tensor_slices((filepaths, labels))\n    \n    SHUFFLE_BUFFER_SIZE = 1024 \n\n    def decode_image(filepath, label):\n        image = tf.io.read_file(filepath)\n        image = tf.image.decode_jpeg(image, channels=3)\n        return image, label\n\n    dataset = dataset.map(decode_image, num_parallel_calls=tf.data.AUTOTUNE)\n\n    if is_training:\n        dataset = dataset.shuffle(buffer_size=SHUFFLE_BUFFER_SIZE) \n        \n        dataset = dataset.map(lambda image, label: (data_augmentation(image, training=True), label),\n                              num_parallel_calls=tf.data.AUTOTUNE)\n\n    dataset = dataset.map(lambda image, label: (tf.image.resize(image, IMAGE_SIZE), label),\n                          num_parallel_calls=tf.data.AUTOTUNE)\n                          \n    dataset = dataset.batch(BATCH_SIZE)\n\n    dataset = dataset.map(lambda images, labels: (tf.keras.applications.resnet50.preprocess_input(images), labels),\n                          num_parallel_calls=tf.data.AUTOTUNE)\n\n    dataset = dataset.prefetch(buffer_size=tf.data.AUTOTUNE)\n\n    return dataset\n\ntrain_ds = build_dataset(train_filepaths, train_labels, is_training=True)\nval_ds = build_dataset(val_filepaths, val_labels, is_training=False)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-05T03:20:44.127919Z","iopub.execute_input":"2025-11-05T03:20:44.128112Z","iopub.status.idle":"2025-11-05T03:20:44.825375Z","shell.execute_reply.started":"2025-11-05T03:20:44.128087Z","shell.execute_reply":"2025-11-05T03:20:44.824606Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"class_counts = train_df['label'].value_counts()\nclass_weights_map = (1.0 / class_counts).to_dict()\n\ntrain_df['sample_weight'] = train_df['label'].map(class_weights_map)\nsimulated_balanced_df = train_df.sample(\n    n=len(train_df),\n    weights='sample_weight',\n    replace=True,\n    random_state=42\n)\nsampled_labels = simulated_balanced_df['label']\n\nfig, axes = plt.subplots(1, 1, figsize=(12, 7)) \n\nsns.countplot(ax=axes, x=sampled_labels, palette='plasma')\naxes.set_title(\"Phân phối nhãn Sau khi Mô phỏng Weighted Sampling\", fontsize=16)\naxes.set_xlabel(\"Nhãn\", fontsize=12)\naxes.set_ylabel(\"Số lượng mẫu\", fontsize=12)\n\nplt.tight_layout()\nplt.show()\n\ntrain_df = train_df.drop(columns=['sample_weight'])\n\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-05T03:20:44.826242Z","iopub.execute_input":"2025-11-05T03:20:44.826553Z","iopub.status.idle":"2025-11-05T03:20:45.025359Z","shell.execute_reply.started":"2025-11-05T03:20:44.826534Z","shell.execute_reply":"2025-11-05T03:20:45.024717Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### Build Resnet50 model","metadata":{}},{"cell_type":"code","source":"from tensorflow.keras.applications import ResNet50\nfrom tensorflow.keras.models import Model\nfrom tensorflow.keras.layers import GlobalAveragePooling2D, Dense, Dropout\nfrom tensorflow.keras.optimizers import Adam\n\nNUM_CLASSES = len(train_df['label'].unique())\n\nbase_model = ResNet50(weights='imagenet', include_top=False, input_shape=(*IMAGE_SIZE, 3))\n\nbase_model.trainable = False\n\nx = base_model.output\nx = GlobalAveragePooling2D()(x)\nx = Dropout(0.5)(x)\npredictions = Dense(NUM_CLASSES, activation='softmax', dtype='float32')(x)\n\nmodel = Model(inputs=base_model.input, outputs=predictions)\n\nprint(f\"Đã xây dựng mô hình thành công với {NUM_CLASSES} lớp đầu ra.\")\nmodel.summary()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-05T03:20:45.026084Z","iopub.execute_input":"2025-11-05T03:20:45.026282Z","iopub.status.idle":"2025-11-05T03:20:48.145144Z","shell.execute_reply.started":"2025-11-05T03:20:45.026267Z","shell.execute_reply":"2025-11-05T03:20:48.144383Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"from tensorflow.keras.callbacks import ModelCheckpoint, EarlyStopping\n\nmodel_checkpoint = ModelCheckpoint(\n    filepath='best_resnet50_model.keras',\n    save_best_only=True,\n    monitor='val_sparse_categorical_accuracy',\n    mode='max',\n    verbose=1\n)\n\nearly_stopping = EarlyStopping(\n    monitor='val_loss',\n    patience=5,\n    restore_best_weights=True,\n    verbose=1\n)\n\nmodel.compile(\n    optimizer=Adam(learning_rate=1e-3),\n    loss='sparse_categorical_crossentropy',\n    metrics=['sparse_categorical_accuracy']\n)\n\nEPOCHS = 20\n\nhistory = model.fit(\n    train_ds,\n    epochs=EPOCHS,\n    validation_data=val_ds,\n    callbacks=[model_checkpoint, early_stopping],\n    class_weight=class_weight_dict\n)\n\nprint(\"\\nHoàn tất quá trình huấn luyện!\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-05T03:20:48.146033Z","iopub.execute_input":"2025-11-05T03:20:48.146226Z","iopub.status.idle":"2025-11-05T05:01:02.234570Z","shell.execute_reply.started":"2025-11-05T03:20:48.146211Z","shell.execute_reply":"2025-11-05T05:01:02.232510Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import tensorflow as tf\nfrom tensorflow.keras.optimizers import Adam\nfrom tensorflow.keras.models import load_model\n\nbest_model = load_model('best_resnet50_model.keras')\nprint(\"Tải mô hình thành công!\")\n\nbest_model.trainable = True\nfreeze_until = 140 \n\nfor layer in best_model.layers[:freeze_until]:\n    layer.trainable = False\n\nbest_model.compile(\n    optimizer=Adam(learning_rate=1e-5),\n    loss='sparse_categorical_crossentropy',\n    metrics=['sparse_categorical_accuracy']\n)\n\ninitial_epochs = 5 \nfine_tune_epochs = 10\ntotal_epochs = initial_epochs + fine_tune_epochs\n\nhistory_fine_tune = best_model.fit(\n    train_ds,\n    epochs=total_epochs,\n    initial_epoch=initial_epochs,\n    validation_data=val_ds,\n    callbacks=[model_checkpoint, early_stopping]\n)","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### Visualization","metadata":{}},{"cell_type":"code","source":"try:\n    history_df1 = pd.DataFrame(history.history)\nexcept NameError:\n    history_df1 = pd.DataFrame() \n\nhistory_df2 = pd.DataFrame(history_fine_tune.history)\n\nif not history_df1.empty:\n    full_history_df = pd.concat([history_df1, history_df2], axis=0)\nelse:\n    full_history_df = history_df2\n\nfull_history_df = full_history_df.reset_index(drop=True)\n\nbest_epoch = full_history_df['val_sparse_categorical_accuracy'].idxmax()\nbest_val_acc = full_history_df['val_sparse_categorical_accuracy'].max()\n\nfig, axes = plt.subplots(1, 2, figsize=(20, 7))\nfig.suptitle('Biểu đồ Toàn bộ Lịch sử Huấn luyện (Feature Extraction + Fine-Tuning)', fontsize=16)\n\naxes[0].plot(full_history_df.index, full_history_df['sparse_categorical_accuracy'], label='Train Accuracy', color='blue', marker='o', markersize=3)\naxes[0].plot(full_history_df.index, full_history_df['val_sparse_categorical_accuracy'], label='Validation Accuracy', color='orange', marker='o', markersize=3)\naxes[0].set_title('Training & Validation Accuracy', fontsize=14)\naxes[0].set_xlabel('Epochs')\naxes[0].set_ylabel('Accuracy')\naxes[0].scatter(best_epoch, best_val_acc, s=150, c='red', zorder=5, label=f'Best Val Acc: {best_val_acc:.4f} at Epoch {best_epoch+1}')\n\nif not history_df1.empty:\n    axes[0].axvline(x=len(history_df1)-1, color='grey', linestyle='--', label='Start Fine-Tuning')\naxes[0].legend()\naxes[0].grid(True)\n\naxes[1].plot(full_history_df.index, full_history_df['loss'], label='Train Loss', color='blue', marker='o', markersize=3)\naxes[1].plot(full_history_df.index, full_history_df['val_loss'], label='Validation Loss', color='orange', marker='o', markersize=3)\naxes[1].set_title('Training & Validation Loss', fontsize=14)\naxes[1].set_xlabel('Epochs')\naxes[1].set_ylabel('Loss')\n\nif not history_df1.empty:\n    axes[1].axvline(x=len(history_df1)-1, color='grey', linestyle='--', label='Start Fine-Tuning')\naxes[1].legend()\naxes[1].grid(True)\n\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-05T06:51:00.445523Z","iopub.execute_input":"2025-11-05T06:51:00.445821Z","iopub.status.idle":"2025-11-05T06:51:00.471162Z","shell.execute_reply.started":"2025-11-05T06:51:00.445800Z","shell.execute_reply":"2025-11-05T06:51:00.470107Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"from sklearn.metrics import classification_report, confusion_matrix\nimport seaborn as sns\n\nprint(\"Tải mô hình tốt nhất từ file 'best_resnet50_model.keras' để đánh giá...\")\nbest_model = tf.keras.models.load_model('best_resnet50_model.keras')\n\nprint(\"\\n--- Đánh giá hiệu năng tổng quan trên tập Validation ---\")\nloss, accuracy = best_model.evaluate(val_ds, verbose=0)\nprint(f\"  - Validation Loss: {loss:.4f}\")\nprint(f\"  - Validation Accuracy: {accuracy:.4f} ({accuracy:.2%})\")\n\ny_true = np.concatenate([labels for images, labels in val_ds], axis=0)\ny_pred_probs = best_model.predict(val_ds)\ny_pred = np.argmax(y_pred_probs, axis=1)\n\nlabel_map = {0: 'CBB', 1: 'CBSD', 2: 'CGM', 3: 'CMD', 4: 'Healthy'}\nlabel_names = [label_map[i] for i in range(NUM_CLASSES)]\n\nprint(\"\\n--- Báo cáo Phân loại Chi tiết (Precision, Recall, F1-Score) ---\")\nprint(classification_report(y_true, y_pred, target_names=label_names))\n\nfig, axes = plt.subplots(1, 2, figsize=(22, 8))\nfig.suptitle('Trực quan hóa Đánh giá Hiệu năng Mô hình', fontsize=20)\n\ncm = confusion_matrix(y_true, y_pred)\nsns.heatmap(cm, annot=True, fmt='d', cmap='Blues',\n            xticklabels=label_names, yticklabels=label_names, ax=axes[0])\naxes[0].set_title('Ma trận nhầm lẫn', fontsize=16)\naxes[0].set_ylabel('Nhãn thực tế (Actual Label)', fontsize=12)\naxes[0].set_xlabel('Nhãn dự đoán (Predicted Label)', fontsize=12)\n\nreport_dict = classification_report(y_true, y_pred, target_names=label_names, output_dict=True)\nreport_df = pd.DataFrame(report_dict).transpose()\nreport_df = report_df.drop(['accuracy', 'macro avg', 'weighted avg']) \nreport_df[['precision', 'recall', 'f1-score']].plot(kind='bar', ax=axes[1], colormap='viridis')\naxes[1].set_title('Các chỉ số theo từng lớp', fontsize=16)\naxes[1].set_xlabel('Lớp bệnh', fontsize=12)\naxes[1].set_ylabel('Điểm số', fontsize=12)\naxes[1].tick_params(axis='x', rotation=45) \naxes[1].grid(axis='y', linestyle='--')\n\nplt.tight_layout(rect=[0, 0.03, 1, 0.95])\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-05T06:48:52.564777Z","iopub.execute_input":"2025-11-05T06:48:52.565345Z","iopub.status.idle":"2025-11-05T06:49:36.075636Z","shell.execute_reply.started":"2025-11-05T06:48:52.565320Z","shell.execute_reply":"2025-11-05T06:49:36.074754Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"from sklearn.metrics import confusion_matrix, ConfusionMatrixDisplay\nimport numpy as np\nimport matplotlib.pyplot as plt\n\nprint(\"Đang tạo Confusion Matrix...\")\n\ny_pred_probs = model.predict(val_dataset)\ny_pred = np.argmax(y_pred_probs, axis=1)\n\ny_true = np.concatenate([labels for images, labels in val_dataset], axis=0)\n\ncm = confusion_matrix(y_true, y_pred)\ndisp = ConfusionMatrixDisplay(confusion_matrix=cm)\n\nfig, ax = plt.subplots(figsize=(8, 8))\ndisp.plot(cmap=plt.cm.Blues, ax=ax)\nplt.title('Confusion Matrix on Validation Set')\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-05T05:01:02.240926Z","iopub.status.idle":"2025-11-05T05:01:02.241250Z","shell.execute_reply.started":"2025-11-05T05:01:02.241089Z","shell.execute_reply":"2025-11-05T05:01:02.241104Z"}},"outputs":[],"execution_count":null}]}