This commit is contained in:
MythEclipse
2026-06-12 15:24:49 +00:00
parent 731b5728f7
commit 2e925a4188
2 changed files with 16 additions and 115 deletions
+4 -109
View File
@@ -892,39 +892,7 @@
"id": "decaf5cb",
"metadata": {},
"outputs": [],
"source": [
"IMG_128 = (128, 128)\n",
"\n",
"# Rebuild datasets at 128x128\n",
"train_ds_128 = tf.keras.utils.image_dataset_from_directory(\n",
" train_dir, shuffle=True, batch_size=BATCH_SIZE, image_size=IMG_128)\n",
"val_ds_128 = tf.keras.utils.image_dataset_from_directory(\n",
" val_dir, shuffle=True, batch_size=BATCH_SIZE, image_size=IMG_128)\n",
"\n",
"train_ds_128 = (train_ds_128.map(augment_and_mix, num_parallel_calls=AUTOTUNE)\n",
" .map(preprocess_fn, num_parallel_calls=AUTOTUNE)\n",
" .map(add_sample_weight, num_parallel_calls=AUTOTUNE).prefetch(AUTOTUNE))\n",
"val_ds_128 = (val_ds_128.map(lambda img, lbl: (preprocess_input(tf.cast(img, tf.float32)), lbl), num_parallel_calls=AUTOTUNE)\n",
" .map(lambda img, lbl: (img, tf.one_hot(lbl, NUM_CLASSES)), num_parallel_calls=AUTOTUNE).prefetch(AUTOTUNE))\n",
"\n",
"EPOCHS_P1 = 25\n",
"steps_per_epoch = tf.data.experimental.cardinality(train_ds_128).numpy() or 100\n",
"total_steps = steps_per_epoch * EPOCHS_P1\n",
"warmup_steps = steps_per_epoch * 3 # 3 epoch warmup\n",
"\n",
"lr_schedule_p1 = WarmupCosineDecay(warmup_steps, total_steps, peak_lr=1e-3, min_lr=1e-5)\n",
"\n",
"model.compile(\n",
" optimizer=AdamW(\n",
" learning_rate=lr_schedule_p1, weight_decay=1e-4),\n",
" loss=tf.keras.losses.CategoricalCrossentropy(from_logits=True, label_smoothing=0.15),\n",
" metrics=['accuracy']\n",
")\n",
"\n",
"print(\"Phase 1: Head training at 128×128...\")\n",
"history_1 = model.fit(train_ds_128, validation_data=val_ds_128,\n",
" epochs=EPOCHS_P1, callbacks=make_callbacks())\n"
]
"source": "IMG_128 = (128, 128)\n\n# Rebuild datasets at 128x128\ntrain_ds_128 = tf.keras.utils.image_dataset_from_directory(\n train_dir, shuffle=True, batch_size=BATCH_SIZE, image_size=IMG_128)\nval_ds_128 = tf.keras.utils.image_dataset_from_directory(\n val_dir, shuffle=True, batch_size=BATCH_SIZE, image_size=IMG_128)\n\ntrain_ds_128 = (train_ds_128.map(augment_and_mix, num_parallel_calls=AUTOTUNE)\n .map(preprocess_fn, num_parallel_calls=AUTOTUNE)\n .map(add_sample_weight, num_parallel_calls=AUTOTUNE).prefetch(AUTOTUNE))\nval_ds_128 = (val_ds_128.map(lambda img, lbl: (preprocess_input(tf.cast(img, tf.float32)), lbl), num_parallel_calls=AUTOTUNE)\n .map(lambda img, lbl: (img, tf.one_hot(lbl, NUM_CLASSES)), num_parallel_calls=AUTOTUNE).prefetch(AUTOTUNE))\n\nEPOCHS_P1 = 25\ncard = tf.data.experimental.cardinality(train_ds_128).numpy()\nsteps_per_epoch = card if card > 0 else 100\ntotal_steps = steps_per_epoch * EPOCHS_P1\nwarmup_steps = steps_per_epoch * 3 # 3 epoch warmup\n\nlr_schedule_p1 = WarmupCosineDecay(warmup_steps, total_steps, peak_lr=1e-3, min_lr=1e-5)\n\nmodel.compile(\n optimizer=AdamW(\n learning_rate=lr_schedule_p1, weight_decay=1e-4),\n loss=tf.keras.losses.CategoricalCrossentropy(from_logits=True, label_smoothing=0.15),\n metrics=['accuracy']\n)\n\nprint(\"Phase 1: Head training at 128×128...\")\nhistory_1 = model.fit(train_ds_128, validation_data=val_ds_128,\n epochs=EPOCHS_P1, callbacks=make_callbacks())"
},
{
"cell_type": "markdown",
@@ -943,44 +911,7 @@
"id": "fd7fb926",
"metadata": {},
"outputs": [],
"source": [
"IMG_192 = (192, 192)\n",
"\n",
"train_ds_192 = tf.keras.utils.image_dataset_from_directory(\n",
" train_dir, shuffle=True, batch_size=BATCH_SIZE, image_size=IMG_192)\n",
"val_ds_192 = tf.keras.utils.image_dataset_from_directory(\n",
" val_dir, shuffle=True, batch_size=BATCH_SIZE, image_size=IMG_192)\n",
"\n",
"train_ds_192 = (train_ds_192.map(augment_and_mix, num_parallel_calls=AUTOTUNE)\n",
" .map(preprocess_fn, num_parallel_calls=AUTOTUNE)\n",
" .map(add_sample_weight, num_parallel_calls=AUTOTUNE).prefetch(AUTOTUNE))\n",
"val_ds_192 = (val_ds_192.map(lambda img, lbl: (preprocess_input(tf.cast(img, tf.float32)), lbl), num_parallel_calls=AUTOTUNE)\n",
" .map(lambda img, lbl: (img, tf.one_hot(lbl, NUM_CLASSES)), num_parallel_calls=AUTOTUNE).prefetch(AUTOTUNE))\n",
"\n",
"# Unfreeze top 100 layers\n",
"base_model.trainable = True\n",
"for layer in base_model.layers[:-100]:\n",
" layer.trainable = False\n",
"\n",
"EPOCHS_P2 = 30\n",
"steps_p2 = tf.data.experimental.cardinality(train_ds_192).numpy() or 100\n",
"total_p2 = steps_p2 * EPOCHS_P2\n",
"warmup_p2 = steps_p2 * 2\n",
"\n",
"lr_schedule_p2 = WarmupCosineDecay(warmup_p2, total_p2, peak_lr=5e-4, min_lr=1e-6)\n",
"\n",
"model.compile(\n",
" optimizer=AdamW(\n",
" learning_rate=lr_schedule_p2, weight_decay=1e-4),\n",
" loss=tf.keras.losses.CategoricalCrossentropy(from_logits=True, label_smoothing=0.15),\n",
" metrics=['accuracy']\n",
")\n",
"\n",
"print(\"Phase 2: Fine-tuning top 100 layers at 192×192...\")\n",
"history_2 = model.fit(train_ds_192, validation_data=val_ds_192,\n",
" epochs=EPOCHS_P1 + EPOCHS_P2, initial_epoch=history_1.epoch[-1] + 1,\n",
" callbacks=make_callbacks())\n"
]
"source": "IMG_192 = (192, 192)\n\ntrain_ds_192 = tf.keras.utils.image_dataset_from_directory(\n train_dir, shuffle=True, batch_size=BATCH_SIZE, image_size=IMG_192)\nval_ds_192 = tf.keras.utils.image_dataset_from_directory(\n val_dir, shuffle=True, batch_size=BATCH_SIZE, image_size=IMG_192)\n\ntrain_ds_192 = (train_ds_192.map(augment_and_mix, num_parallel_calls=AUTOTUNE)\n .map(preprocess_fn, num_parallel_calls=AUTOTUNE)\n .map(add_sample_weight, num_parallel_calls=AUTOTUNE).prefetch(AUTOTUNE))\nval_ds_192 = (val_ds_192.map(lambda img, lbl: (preprocess_input(tf.cast(img, tf.float32)), lbl), num_parallel_calls=AUTOTUNE)\n .map(lambda img, lbl: (img, tf.one_hot(lbl, NUM_CLASSES)), num_parallel_calls=AUTOTUNE).prefetch(AUTOTUNE))\n\n# Unfreeze top 100 layers\nbase_model.trainable = True\nfor layer in base_model.layers[:-100]:\n layer.trainable = False\n\nEPOCHS_P2 = 30\ncard_p2 = tf.data.experimental.cardinality(train_ds_192).numpy()\nsteps_p2 = card_p2 if card_p2 > 0 else 100\ntotal_p2 = steps_p2 * EPOCHS_P2\nwarmup_p2 = steps_p2 * 2\n\nlr_schedule_p2 = WarmupCosineDecay(warmup_p2, total_p2, peak_lr=5e-4, min_lr=1e-6)\n\nmodel.compile(\n optimizer=AdamW(\n learning_rate=lr_schedule_p2, weight_decay=1e-4),\n loss=tf.keras.losses.CategoricalCrossentropy(from_logits=True, label_smoothing=0.15),\n metrics=['accuracy']\n)\n\nprint(\"Phase 2: Fine-tuning top 100 layers at 192×192...\")\nhistory_2 = model.fit(train_ds_192, validation_data=val_ds_192,\n epochs=EPOCHS_P1 + EPOCHS_P2, initial_epoch=history_1.epoch[-1] + 1,\n callbacks=make_callbacks())"
},
{
"cell_type": "markdown",
@@ -1000,43 +931,7 @@
"id": "14115063",
"metadata": {},
"outputs": [],
"source": [
"img_size = IMG_SIZE # (224, 224)\n",
"\n",
"train_ds_full = tf.keras.utils.image_dataset_from_directory(\n",
" train_dir, shuffle=True, batch_size=BATCH_SIZE, image_size=img_size)\n",
"val_ds_full = tf.keras.utils.image_dataset_from_directory(\n",
" val_dir, shuffle=True, batch_size=BATCH_SIZE, image_size=img_size)\n",
"\n",
"train_ds_full = (train_ds_full.map(augment_and_mix, num_parallel_calls=AUTOTUNE)\n",
" .map(preprocess_fn, num_parallel_calls=AUTOTUNE)\n",
" .map(add_sample_weight, num_parallel_calls=AUTOTUNE).prefetch(AUTOTUNE))\n",
"val_ds_full = (val_ds_full.map(lambda img, lbl: (preprocess_input(tf.cast(img, tf.float32)), lbl), num_parallel_calls=AUTOTUNE)\n",
" .map(lambda img, lbl: (img, tf.one_hot(lbl, NUM_CLASSES)), num_parallel_calls=AUTOTUNE).prefetch(AUTOTUNE))\n",
"\n",
"# Full unfreeze\n",
"for layer in base_model.layers:\n",
" layer.trainable = True\n",
"\n",
"EPOCHS_P3 = 30\n",
"steps_p3 = tf.data.experimental.cardinality(train_ds_full).numpy() or 100\n",
"total_p3 = steps_p3 * EPOCHS_P3\n",
"warmup_p3 = steps_p3 * 2\n",
"\n",
"lr_schedule_p3 = WarmupCosineDecay(warmup_p3, total_p3, peak_lr=1e-4, min_lr=1e-7)\n",
"\n",
"model.compile(\n",
" optimizer=AdamW(\n",
" learning_rate=lr_schedule_p3, weight_decay=1e-4),\n",
" loss=tf.keras.losses.CategoricalCrossentropy(from_logits=True, label_smoothing=0.10),\n",
" metrics=['accuracy']\n",
")\n",
"\n",
"print(\"Phase 3: Full fine-tuning at 224×224...\")\n",
"history_3 = model.fit(train_ds_full, validation_data=val_ds_full,\n",
" epochs=EPOCHS_P1 + EPOCHS_P2 + EPOCHS_P3, initial_epoch=(history_2.epoch[-1] + 1) if history_2.epoch else EPOCHS_P1 + EPOCHS_P2,\n",
" callbacks=make_callbacks())\n"
]
"source": "img_size = IMG_SIZE # (224, 224)\n\ntrain_ds_full = tf.keras.utils.image_dataset_from_directory(\n train_dir, shuffle=True, batch_size=BATCH_SIZE, image_size=img_size)\nval_ds_full = tf.keras.utils.image_dataset_from_directory(\n val_dir, shuffle=True, batch_size=BATCH_SIZE, image_size=img_size)\n\ntrain_ds_full = (train_ds_full.map(augment_and_mix, num_parallel_calls=AUTOTUNE)\n .map(preprocess_fn, num_parallel_calls=AUTOTUNE)\n .map(add_sample_weight, num_parallel_calls=AUTOTUNE).prefetch(AUTOTUNE))\nval_ds_full = (val_ds_full.map(lambda img, lbl: (preprocess_input(tf.cast(img, tf.float32)), lbl), num_parallel_calls=AUTOTUNE)\n .map(lambda img, lbl: (img, tf.one_hot(lbl, NUM_CLASSES)), num_parallel_calls=AUTOTUNE).prefetch(AUTOTUNE))\n\n# Full unfreeze\nfor layer in base_model.layers:\n layer.trainable = True\n\nEPOCHS_P3 = 30\ncard_p3 = tf.data.experimental.cardinality(train_ds_full).numpy()\nsteps_p3 = card_p3 if card_p3 > 0 else 100\ntotal_p3 = steps_p3 * EPOCHS_P3\nwarmup_p3 = steps_p3 * 2\n\nlr_schedule_p3 = WarmupCosineDecay(warmup_p3, total_p3, peak_lr=1e-4, min_lr=1e-7)\n\nmodel.compile(\n optimizer=AdamW(\n learning_rate=lr_schedule_p3, weight_decay=1e-4),\n loss=tf.keras.losses.CategoricalCrossentropy(from_logits=True, label_smoothing=0.10),\n metrics=['accuracy']\n)\n\nprint(\"Phase 3: Full fine-tuning at 224×224...\")\nhistory_3 = model.fit(train_ds_full, validation_data=val_ds_full,\n epochs=EPOCHS_P1 + EPOCHS_P2 + EPOCHS_P3, initial_epoch=(history_2.epoch[-1] + 1) if history_2.epoch else EPOCHS_P1 + EPOCHS_P2,\n callbacks=make_callbacks())"
},
{
"cell_type": "markdown",
@@ -1577,4 +1472,4 @@
},
"nbformat": 4,
"nbformat_minor": 5
}
}
+12 -6
View File
@@ -11,7 +11,7 @@ log = logging.getLogger(__name__)
def cbam_block(x, ratio=8, name="cbam"):
"""Convolutional Block Attention Module — lightweight foreground attention."""
channels = tf.shape(x)[-1]
channels = x.shape[-1]
# Channel attention
avg_pool = layers.GlobalAveragePooling2D()(x)
@@ -34,17 +34,23 @@ def cbam_block(x, ratio=8, name="cbam"):
return x
def build_clean_model(num_classes, img_size=(224, 224)):
"""Build the production architecture: CBAM + lightweight head, outputting raw logits."""
def build_clean_model(num_classes, target_size=(224, 224)):
"""Build the production architecture: CBAM + lightweight head, outputting raw logits.
Mirrors the notebook's build_model() exactly so set_weights() maps correctly.
"""
base_model = tf.keras.applications.EfficientNetV2B0(
input_shape=img_size + (3,),
input_shape=target_size + (3,),
include_top=False,
weights=None,
)
base_model.trainable = False
inputs = tf.keras.Input(shape=img_size + (3,), name="input")
x = base_model(inputs, training=False)
inputs = tf.keras.Input(shape=(None, None, 3), name="input")
x = layers.Resizing(target_size[0], target_size[1], interpolation="bilinear",
name="resize_input")(inputs)
x = layers.GaussianNoise(0.05, name="gauss_noise")(x)
x = base_model(x, training=False)
# CBAM attention — focus on leaf regions, ignore background
x = cbam_block(x, ratio=8, name="cbam")
x = layers.GlobalAveragePooling2D(name="gap")(x)