{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.12.12","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"none","dataSources":[{"sourceType":"competition","sourceId":25563,"databundleVersionId":2094376}],"dockerImageVersionId":31328,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":false}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"# This Python 3 environment comes with many helpful analytics libraries installed\n# It is defined by the kaggle/python Docker image: https://github.com/kaggle/docker-python\n# For example, here's several helpful packages to load\n\nimport numpy as np # linear algebra\nimport pandas as pd # data processing, CSV file I/O (e.g. pd.read_csv)\n\n# Input data files are available in the read-only \"../input/\" directory\n# For example, running this (by clicking run or pressing Shift+Enter) will list all files under the input directory\n\nimport os\nfor dirname, _, filenames in os.walk('/kaggle/input'):\n    for filename in filenames:\n        print(os.path.join(dirname, filename))\n\n# You can write up to 20GB to the current directory (/kaggle/working/) that gets preserved as output when you create a version using \"Save & Run All\" \n# You can also write temporary files to /kaggle/temp/, but they won't be saved outside of the current session","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2026-04-09T04:02:11.106258Z","iopub.execute_input":"2026-04-09T04:02:11.106963Z","iopub.status.idle":"2026-04-09T04:02:19.736929Z","shell.execute_reply.started":"2026-04-09T04:02:11.106914Z","shell.execute_reply":"2026-04-09T04:02:19.735702Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nos.environ[\"TF_CPP_MIN_LOG_LEVEL\"] = \"2\"\n\nimport numpy as np\nimport pandas as pd\nimport matplotlib.pyplot as plt\nimport tensorflow as tf\nfrom sklearn.preprocessing import MultiLabelBinarizer\nfrom sklearn.model_selection import train_test_split\n\ntf.get_logger().setLevel('ERROR')\n\nprint(\"TensorFlow:\", tf.__version__)\nprint(\"GPU available:\", len(tf.config.list_physical_devices('GPU')) > 0)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-09T04:03:26.109231Z","iopub.execute_input":"2026-04-09T04:03:26.109923Z","iopub.status.idle":"2026-04-09T04:03:26.115487Z","shell.execute_reply.started":"2026-04-09T04:03:26.109891Z","shell.execute_reply":"2026-04-09T04:03:26.114620Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ── Kaggle Notebook (recommended) ──────────────────────────────────────────\nBASE_DIR    = \"/kaggle/code/alin678/assignment-three/edit\"\n\n# ── Local machine ── uncomment and set your own path ───────────────────────\n# BASE_DIR  = \"./plant-pathology-2021-fgvc8\"  \n\nBASE_DIR   = \"/kaggle/input/competitions/plant-pathology-2021-fgvc8\"\nTRAIN_DIR  = os.path.join(BASE_DIR, \"train_images\")\nTEST_DIR   = os.path.join(BASE_DIR, \"test_images\")\n\n# Confirm images exist\nprint(os.listdir(TRAIN_DIR)[:5])\n\nprint(\"Train images:\", TRAIN_DIR)\nprint(\"Test  images:\", TEST_DIR)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-09T04:03:34.458882Z","iopub.execute_input":"2026-04-09T04:03:34.459578Z","iopub.status.idle":"2026-04-09T04:03:34.471751Z","shell.execute_reply.started":"2026-04-09T04:03:34.459548Z","shell.execute_reply":"2026-04-09T04:03:34.470937Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"train_df = pd.read_csv(\"/kaggle/input/competitions/plant-pathology-2021-fgvc8/train.csv\")\nsample_df = pd.read_csv(\"/kaggle/input/competitions/plant-pathology-2021-fgvc8/sample_submission.csv\")\n\nprint(\"Train shape:\", train_df.shape)\nprint(\"\\nFirst 5 rows:\")\nprint(train_df.head())\n\n# 6 unique disease classes\nALL_LABELS = ['complex', 'frog_eye_leaf_spot', 'healthy',\n              'powdery_mildew', 'rust', 'scab']\nNUM_CLASSES = len(ALL_LABELS)\nprint(\"\\nClasses:\", ALL_LABELS)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-09T04:03:45.476354Z","iopub.execute_input":"2026-04-09T04:03:45.477127Z","iopub.status.idle":"2026-04-09T04:03:45.504072Z","shell.execute_reply.started":"2026-04-09T04:03:45.477096Z","shell.execute_reply":"2026-04-09T04:03:45.503303Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Show label distribution\nlabel_counts = train_df['labels'].value_counts().head(10)\n\nplt.figure(figsize=(10, 4))\nlabel_counts.plot(kind='bar')\nplt.title(\"Top 10 Label Combinations\")\nplt.xticks(rotation=30, ha='right')\nplt.tight_layout()\nplt.show()\n\nprint(\"\\nUnique label combinations:\", train_df['labels'].nunique())","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-09T04:03:58.524272Z","iopub.execute_input":"2026-04-09T04:03:58.524550Z","iopub.status.idle":"2026-04-09T04:03:58.704375Z","shell.execute_reply.started":"2026-04-09T04:03:58.524524Z","shell.execute_reply":"2026-04-09T04:03:58.703403Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"plt.figure(figsize=(16, 4))\nfor i in range(8):\n    row   = train_df.iloc[i]\n    path  = os.path.join(TRAIN_DIR, row['image'])\n    image = tf.keras.utils.load_img(path, target_size=(128, 128))\n    image = tf.keras.utils.img_to_array(image).astype(\"uint8\")\n    plt.subplot(1, 8, i + 1)\n    plt.imshow(image)\n    plt.title(row['labels'], fontsize=6)\n    plt.axis(\"off\")\n\nplt.suptitle(\"Sample Training Images\", fontsize=13)\nplt.tight_layout()\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-09T04:04:22.762477Z","iopub.execute_input":"2026-04-09T04:04:22.763231Z","iopub.status.idle":"2026-04-09T04:04:23.758279Z","shell.execute_reply.started":"2026-04-09T04:04:22.763200Z","shell.execute_reply":"2026-04-09T04:04:23.757391Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Split label strings into lists\ntrain_df['label_list'] = train_df['labels'].apply(lambda x: x.split())\n\n# Multi-hot encode\nmlb = MultiLabelBinarizer(classes=ALL_LABELS)\ny_all = mlb.fit_transform(train_df['label_list']).astype('float32')\n\nprint(\"Label matrix shape:\", y_all.shape)   # (18632, 6)\nprint(\"Classes:\", mlb.classes_)\nprint(\"\\nExample — first row label:\", train_df['labels'].iloc[0])\nprint(\"Encoded:\", y_all[0])\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-09T04:04:46.950371Z","iopub.execute_input":"2026-04-09T04:04:46.951200Z","iopub.status.idle":"2026-04-09T04:04:46.978926Z","shell.execute_reply.started":"2026-04-09T04:04:46.951169Z","shell.execute_reply":"2026-04-09T04:04:46.978020Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"IMG_SIZE   = 224\nBATCH_SIZE = 32\nAUTOTUNE   = tf.data.AUTOTUNE\n\n# Train / validation split\nX_train, X_val, y_train, y_val = train_test_split(\n    train_df['image'].values, y_all,\n    test_size=0.2, random_state=42\n)\nprint(f\"Train: {len(X_train)} | Val: {len(X_val)}\")\n\n\ndef load_image(path, label):\n    \"\"\"Read JPEG, resize, normalize to [0,1].\"\"\"\n    full_path = tf.strings.join([TRAIN_DIR + \"/\", path])\n    image = tf.io.read_file(full_path)\n    image = tf.image.decode_jpeg(image, channels=3)\n    image = tf.image.resize(image, (IMG_SIZE, IMG_SIZE))\n    image = tf.cast(image, tf.float32) / 255.0\n    return image, label\n\ndef load_test_image(path):\n    \"\"\"Read test image — no label.\"\"\"\n    full_path = tf.strings.join([TEST_DIR + \"/\", path])\n    image = tf.io.read_file(full_path)\n    image = tf.image.decode_jpeg(image, channels=3)\n    image = tf.image.resize(image, (IMG_SIZE, IMG_SIZE))\n    image = tf.cast(image, tf.float32) / 255.0\n    return image\n\n\ntrain_ds = (\n    tf.data.Dataset.from_tensor_slices((X_train, y_train))\n    .shuffle(2000)\n    .map(load_image, num_parallel_calls=AUTOTUNE)\n    .batch(BATCH_SIZE)\n    .prefetch(AUTOTUNE)\n)\n\nval_ds = (\n    tf.data.Dataset.from_tensor_slices((X_val, y_val))\n    .map(load_image, num_parallel_calls=AUTOTUNE)\n    .batch(BATCH_SIZE)\n    .prefetch(AUTOTUNE)\n)\n\ntest_paths = sample_df['image'].values\ntest_ds = (\n    tf.data.Dataset.from_tensor_slices(test_paths)\n    .map(load_test_image, num_parallel_calls=AUTOTUNE)\n    .batch(BATCH_SIZE)\n    .prefetch(AUTOTUNE)\n)\n\nprint(\"Pipelines ready!\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-09T04:04:57.227753Z","iopub.execute_input":"2026-04-09T04:04:57.228589Z","iopub.status.idle":"2026-04-09T04:04:57.330540Z","shell.execute_reply.started":"2026-04-09T04:04:57.228557Z","shell.execute_reply":"2026-04-09T04:04:57.329797Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ── Geometric augmentation ──────────────────────────────────────────────────\ngeometric_augmentation = tf.keras.Sequential([\n    tf.keras.layers.RandomFlip(\"horizontal\"),\n    tf.keras.layers.RandomRotation(factor=0.05),\n    tf.keras.layers.RandomZoom(height_factor=(-0.1, 0.1), width_factor=(-0.1, 0.1)),\n    tf.keras.layers.RandomTranslation(height_factor=0.1, width_factor=0.1),\n], name=\"geometric_augmentation\")\n\n\n# ── Color augmentation custom layers ───────────────────────────────────────\nclass RandomBrightnessLayer(tf.keras.layers.Layer):\n    def __init__(self, max_delta=0.2, **kwargs):\n        super().__init__(**kwargs)\n        self.max_delta = max_delta\n    def call(self, x, training=False):\n        if training:\n            x = tf.image.random_brightness(x, self.max_delta)\n            x = tf.clip_by_value(x, 0.0, 1.0)\n        return x\n\nclass RandomContrastLayer(tf.keras.layers.Layer):\n    def __init__(self, lower=0.7, upper=1.3, **kwargs):\n        super().__init__(**kwargs)\n        self.lower, self.upper = lower, upper\n    def call(self, x, training=False):\n        if training:\n            x = tf.image.random_contrast(x, self.lower, self.upper)\n            x = tf.clip_by_value(x, 0.0, 1.0)\n        return x\n\nclass RandomSaturationLayer(tf.keras.layers.Layer):\n    def __init__(self, lower=0.6, upper=1.4, **kwargs):\n        super().__init__(**kwargs)\n        self.lower, self.upper = lower, upper\n    def call(self, x, training=False):\n        if training:\n            x = tf.image.random_saturation(x, self.lower, self.upper)\n            x = tf.clip_by_value(x, 0.0, 1.0)\n        return x\n\nclass RandomHueLayer(tf.keras.layers.Layer):\n    def __init__(self, max_delta=0.1, **kwargs):\n        super().__init__(**kwargs)\n        self.max_delta = max_delta\n    def call(self, x, training=False):\n        if training:\n            x = tf.image.random_hue(x, self.max_delta)\n            x = tf.clip_by_value(x, 0.0, 1.0)\n        return x\n\ncolor_augmentation = tf.keras.Sequential([\n    RandomBrightnessLayer(max_delta=0.2),\n    RandomContrastLayer(lower=0.7, upper=1.3),\n    RandomSaturationLayer(lower=0.6, upper=1.4),\n    RandomHueLayer(max_delta=0.1),\n], name=\"color_augmentation\")\n\n# Output activation: sigmoid (not softmax) - each class is independent  # 修正这行\nprint(\"Augmentation blocks ready!\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-09T04:05:13.759847Z","iopub.execute_input":"2026-04-09T04:05:13.760454Z","iopub.status.idle":"2026-04-09T04:05:13.786076Z","shell.execute_reply.started":"2026-04-09T04:05:13.760422Z","shell.execute_reply":"2026-04-09T04:05:13.785417Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"inputs = tf.keras.Input(shape=(IMG_SIZE, IMG_SIZE, 3), name=\"input_image\")\n\n# 1) Geometric augmentation\nx = geometric_augmentation(inputs)\n\n# 2) Color augmentation (expects [0,1])\nx = color_augmentation(x)\n\n# 3) Scale back to [0,255] for EfficientNet\nx = x * 255.0\nx = tf.keras.applications.efficientnet_v2.preprocess_input(x)\n\n# 4) Backbone — EfficientNetV2B0 pretrained on ImageNet\nbase_model = tf.keras.applications.EfficientNetV2B0(\n    include_top=False,\n    weights=\"imagenet\"\n)\nbase_model.trainable = False   # freeze backbone first\nx = base_model(x, training=False)\n\n# 5) Classification head — sigmoid for multi-label\nx = tf.keras.layers.GlobalAveragePooling2D()(x)\nx = tf.keras.layers.Dropout(0.3)(x)\noutputs = tf.keras.layers.Dense(\n    NUM_CLASSES, activation=\"sigmoid\", name=\"predictions\"\n)(x)\n\nmodel = tf.keras.Model(inputs, outputs, name=\"plant_pathology_model\")\nmodel.summary()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-09T04:05:18.330687Z","iopub.execute_input":"2026-04-09T04:05:18.331622Z","iopub.status.idle":"2026-04-09T04:05:19.550228Z","shell.execute_reply.started":"2026-04-09T04:05:18.331576Z","shell.execute_reply":"2026-04-09T04:05:19.549574Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"model.compile(\n    optimizer=tf.keras.optimizers.Adam(learning_rate=1e-3),\n    loss=\"binary_crossentropy\",       # multi-label loss\n    metrics=[\"accuracy\"]\n)\n\nprint(\"Training frozen backbone (5 epochs)...\")\nhistory = model.fit(\n    train_ds,\n    validation_data=val_ds,\n    epochs=5\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-09T04:05:26.086065Z","iopub.execute_input":"2026-04-09T04:05:26.086766Z","iopub.status.idle":"2026-04-09T04:30:32.588228Z","shell.execute_reply.started":"2026-04-09T04:05:26.086737Z","shell.execute_reply":"2026-04-09T04:30:32.587346Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Unfreeze backbone\nbase_model.trainable = True\n\nmodel.compile(\n    optimizer=tf.keras.optimizers.Adam(learning_rate=1e-4),  # lower LR for fine-tuning\n    loss=\"binary_crossentropy\",\n    metrics=[\"accuracy\"]\n)\n\nprint(\"Fine-tuning full model (5 epochs)...\")\nhistory_ft = model.fit(\n    train_ds,\n    validation_data=val_ds,\n    epochs=5\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-09T04:31:34.420431Z","iopub.execute_input":"2026-04-09T04:31:34.421200Z","iopub.status.idle":"2026-04-09T04:59:32.612150Z","shell.execute_reply.started":"2026-04-09T04:31:34.421168Z","shell.execute_reply":"2026-04-09T04:59:32.611442Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Get probabilities for all test images\npreds = model.predict(test_ds, verbose=1)   # shape: (N, 6)\n\nTHRESHOLD = 0.5\n\ndef probs_to_label(prob_row, threshold=THRESHOLD):\n    \"\"\"Convert probability array to space-separated label string.\"\"\"\n    selected = [ALL_LABELS[i] for i, p in enumerate(prob_row) if p >= threshold]\n    # If nothing passes threshold, pick the class with the highest probability\n    if len(selected) == 0:\n        selected = [ALL_LABELS[np.argmax(prob_row)]]\n    return \" \".join(selected)\n\npredicted_labels = [probs_to_label(row) for row in preds]\n\nprint(\"Sample predictions:\")\nfor i in range(min(5, len(test_paths))):\n    print(f\"  {test_paths[i]}  →  {predicted_labels[i]}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-09T05:24:46.377036Z","iopub.execute_input":"2026-04-09T05:24:46.377474Z","iopub.status.idle":"2026-04-09T05:24:46.525139Z","shell.execute_reply.started":"2026-04-09T05:24:46.377442Z","shell.execute_reply":"2026-04-09T05:24:46.524290Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"submission = pd.DataFrame({\n    'image':  test_paths,\n    'labels': predicted_labels\n})\n\nsubmission.to_csv('submission.csv', index=False)\n\nprint(\"submission.csv saved!\")\nprint(\"\\nFirst 5 rows:\")\nprint(submission.head())\nprint(\"\\nShape:\", submission.shape)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-09T05:25:23.556221Z","iopub.execute_input":"2026-04-09T05:25:23.556545Z","iopub.status.idle":"2026-04-09T05:25:23.565021Z","shell.execute_reply.started":"2026-04-09T05:25:23.556521Z","shell.execute_reply":"2026-04-09T05:25:23.563966Z"}},"outputs":[],"execution_count":null}]}