{"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":22881,"databundleVersionId":1552852,"sourceType":"competition"},{"sourceId":13704683,"sourceType":"datasetVersion","datasetId":8717992}],"dockerImageVersionId":31154,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"import os, random, shutil, csv, math, itertools, pathlib, time\nfrom pathlib import Path\nimport numpy as np\nimport tensorflow as tf\ntf.config.optimizer.set_jit(True)\nfrom tensorflow import keras\nfrom tensorflow.keras import layers\nfrom PIL import Image\nimport matplotlib.pyplot as plt\nimport seaborn as sns\nfrom tensorflow.keras.preprocessing.image import ImageDataGenerator, img_to_array, load_img\nfrom tensorflow.keras import models, Model\nfrom sklearn.metrics import classification_report, confusion_matrix, accuracy_score, f1_score, top_k_accuracy_score\nfrom tensorflow.keras.callbacks import ModelCheckpoint, EarlyStopping, ReduceLROnPlateau, CSVLogger, TensorBoard\nimport json\nfrom datetime import datetime\n\nsns.set()\n\nfrom tensorflow.keras import mixed_precision\nfrom tensorflow.keras.applications import EfficientNetB0, MobileNetV2, ResNet50V2\nfrom tensorflow.keras.applications.resnet_v2 import preprocess_input\nfrom tensorflow.keras.losses import CategoricalCrossentropy\n\n\n# Mount Google Drive\n# from google.colab import drive\n# drive.mount('/content/drive')\n\n# =============================================================================\n# CONFIGURATION\n# =============================================================================\nNUM_CLASSES = 4000  # Updated for your dataset\nIMG_SIZE = (64, 64 )\nBATCH_SIZE = 32  # Reduced for memory efficiency with 4000 classes\nEMBEDDING_DIM = 512  # Larger embedding for more classes\n\nbase_dir = \"/kaggle/input/11-785-fall-20-homework-2-part-2/classification_data\"\ntrain_dir = base_dir + \"/train_data\"\ntest_dir = base_dir + \"/test_data\"\nval_dir = base_dir + \"/val_data\"\n\n# Create experiment directory with timestamp\ntimestamp = datetime.now().strftime(\"%Y%m%d_%H%M%S\")\n# experiment_dir = os.path.join(base_dir, f\"experiment_{timestamp}\")\n# os.makedirs(experiment_dir, exist_ok=True)\nexperiment_dir = os.path.join(\"/kaggle/working\", f\"experiment_{timestamp}\")\nos.makedirs(experiment_dir, exist_ok=True)\n\n# Save configuration\nconfig = {\n    \"num_classes\": NUM_CLASSES,\n    \"img_size\": IMG_SIZE,\n    \"batch_size\": BATCH_SIZE,\n    \"embedding_dim\": EMBEDDING_DIM,\n    \"timestamp\": timestamp\n}\nwith open(os.path.join(experiment_dir, \"config.json\"), \"w\") as f:\n    json.dump(config, f, indent=4)\n\n# =============================================================================\n# GPU SETUP\n# =============================================================================\n\ngpus = tf.config.list_physical_devices('GPU')\nif gpus:\n    for gpu in gpus:\n        tf.config.experimental.set_memory_growth(gpu, True)\n    print(f\"GPU(s) available: {len(gpus)}\")\nelse:\n    print(\"No GPU found - training will be slow!\")\n\nmixed_precision.set_global_policy('mixed_float16')\nprint(f\"Mixed precision policy: {mixed_precision.global_policy()}\")\n# =============================================================================\n# DATA LOADING\n# =============================================================================\nprint(\"Loading datasets...\")\ntrain_ds = tf.keras.preprocessing.image_dataset_from_directory(\n    train_dir,\n    image_size=IMG_SIZE,\n    batch_size=BATCH_SIZE,\n    shuffle=True,\n    seed=42,\n    label_mode='categorical'\n)\n\nval_ds = tf.keras.preprocessing.image_dataset_from_directory(\n    val_dir,\n    image_size=IMG_SIZE,\n    batch_size=BATCH_SIZE,\n    shuffle=False,\n    seed=42,\n    label_mode='categorical'\n)\n\ntest_ds = tf.keras.preprocessing.image_dataset_from_directory(\n    test_dir,\n    image_size=IMG_SIZE,\n    batch_size=BATCH_SIZE,\n    shuffle=False,\n    seed=42,\n    label_mode='categorical'\n)\n\n# Save class names\nclass_names = train_ds.class_names\nwith open(os.path.join(experiment_dir, \"class_names.json\"), \"w\") as f:\n    json.dump(class_names, f, indent=4)\nprint(f\"Total classes: {len(class_names)}\")\n\n# Preprocessing\ntrain_ds = train_ds.map(lambda x, y: (x, y))\nval_ds = val_ds.map(lambda x, y: (x, y))\ntest_ds = test_ds.map(lambda x, y: (x, y))\n\n# Optimize pipeline\nAUTOTUNE = tf.data.AUTOTUNE\ntrain_ds = train_ds.shuffle(1000).prefetch(AUTOTUNE)\nval_ds = val_ds.prefetch(AUTOTUNE)\ntest_ds = test_ds.prefetch(AUTOTUNE)\n\n#data augmentation\ndata_augmentation = keras.Sequential([\n    layers.RandomFlip(\"horizontal\"),        # horizontal flip\n    layers.RandomRotation(0.1),             # rotate ±10%\n    layers.RandomZoom(0.1),                 # zoom in/out ±10%\n    layers.RandomContrast(0.1),             # adjust contrast ±10%\n    layers.RandomTranslation(0.1, 0.1)      # translate ±10% horizontally and vertically\n], name=\"data_augmentation\")\n# =============================================================================\n# MODEL DEFINITION\n# =============================================================================\ndef l2_norm_layer(x):\n    return tf.nn.l2_normalize(x, axis=1)\n\ndef create_model(num_classes=NUM_CLASSES, embedding_dim=EMBEDDING_DIM):\n    \"\"\"Create face recognition model with embedding layer\"\"\"\n    inputs = layers.Input(shape=(*IMG_SIZE, 3))\n\n    x = data_augmentation(inputs)\n\n    # Preprocessing for ResNet\n    x = preprocess_input(x)\n    \n    base_model = ResNet50V2(\n        input_shape=(*IMG_SIZE, 3),\n        include_top=False,\n        weights=\"imagenet\"\n    )\n    base_model.trainable = False\n    x = base_model(x, training=False)\n    \n    x = layers.GlobalAveragePooling2D()(x)\n    x = layers.Dense(1024, activation=\"relu\")(x)\n    x = layers.BatchNormalization()(x)\n    x = layers.Dropout(0.5)(x)\n    \n    # Embedding layer (unnormalized)\n    x = layers.Dense(embedding_dim, activation=None, name=\"embedding_dense\")(x)\n    x = layers.BatchNormalization()(x)\n    \n    # L2 normalized embedding\n    embedding = layers.Lambda(l2_norm_layer, name=\"l2_embedding\", output_shape=(embedding_dim,))(x)\n    \n    # Classification head\n    output = layers.Dense(num_classes, activation=\"softmax\", dtype='float32', name=\"softmax_output\")(embedding)\n    \n    model = Model(inputs=inputs, outputs=output, name=\"FaceRecognitionModel\")\n    return model, base_model\n\nmodel, base_model = create_model()\nprint(f\"Model created with {NUM_CLASSES} output classes\")\n\n# =============================================================================\n# CUSTOM CALLBACKS\n# =============================================================================\nclass DetailedMetricsCallback(keras.callbacks.Callback):\n    \"\"\"Save detailed metrics after each epoch\"\"\"\n    def __init__(self, log_dir):\n        super().__init__()\n        self.log_dir = log_dir\n        self.metrics_history = []\n        \n    def on_epoch_end(self, epoch, logs=None):\n        logs = logs or {}\n        epoch_metrics = {\n            'epoch': epoch + 1,\n            'timestamp': datetime.now().isoformat(),\n            **logs\n        }\n        self.metrics_history.append(epoch_metrics)\n        \n        # Save after each epoch\n        with open(os.path.join(self.log_dir, 'detailed_metrics.json'), 'w') as f:\n            json.dump(self.metrics_history, f, indent=4)\n        \n        print(f\"\\nEpoch {epoch+1} metrics saved.\")\n\n# =============================================================================\n# STAGE 1: TRAIN FROZEN BASE\n# =============================================================================\nprint(\"\\n\" + \"=\"*80)\nprint(\"STAGE 1: Training with frozen base model\")\nprint(\"=\"*80 + \"\\n\")\n\nstage1_dir = os.path.join(experiment_dir, \"stage1\")\nos.makedirs(stage1_dir, exist_ok=True)\n\noptimizer_stage1 = tf.keras.optimizers.AdamW(learning_rate=1e-3, weight_decay=1e-4)\n\nmodel.compile(\n    optimizer=optimizer_stage1,\n    loss=CategoricalCrossentropy(label_smoothing=0.1),\n    metrics=[\"accuracy\", \n             tf.keras.metrics.TopKCategoricalAccuracy(k=5, name='top5_accuracy'),\n             tf.keras.metrics.TopKCategoricalAccuracy(k=10, name='top10_accuracy')]\n)\n\ncallbacks_stage1 = [\n    ModelCheckpoint(\n        os.path.join(stage1_dir, \"best_model.keras\"),\n        monitor=\"val_accuracy\",\n        save_best_only=True,\n        verbose=1\n    ),\n    ModelCheckpoint(\n        os.path.join(stage1_dir, \"model_epoch_{epoch:02d}_acc_{val_accuracy:.4f}.keras\"),\n        save_freq='epoch',\n        verbose=0\n    ),\n    EarlyStopping(\n        monitor=\"val_loss\",\n        patience=8,\n        restore_best_weights=True,\n        verbose=1\n    ),\n    ReduceLROnPlateau(\n        monitor='val_loss',\n        factor=0.5,\n        patience=3,\n        min_lr=1e-7,\n        verbose=1\n    ),\n    CSVLogger(\n        os.path.join(stage1_dir, \"training_log.csv\"),\n        separator=',',\n        append=False\n    ),\n    TensorBoard(\n        log_dir=os.path.join(stage1_dir, \"tensorboard\"),\n        histogram_freq=1,\n        write_graph=True\n    ),\n    DetailedMetricsCallback(stage1_dir)\n]\n\nhistory_stage1 = model.fit(\n    train_ds,\n    validation_data=val_ds,\n    epochs=1,\n    callbacks=callbacks_stage1,\n    verbose=1\n)\n\n# Save final model and weights\nmodel.save(os.path.join(stage1_dir, \"final_model.keras\"))\nmodel.save_weights(os.path.join(stage1_dir, \"final_model.weights.h5\"))\nprint(f\"Stage 1 complete. Models saved to {stage1_dir}\")\n\n# =============================================================================\n# STAGE 2: FINE-TUNING\n# =============================================================================\nprint(\"\\n\" + \"=\"*80)\nprint(\"STAGE 2: Fine-tuning with unfrozen layers\")\nprint(\"=\"*80 + \"\\n\")\n\nstage2_dir = os.path.join(experiment_dir, \"stage2\")\nos.makedirs(stage2_dir, exist_ok=True)\n\n# Load best model from stage 1\nmodel = tf.keras.models.load_model(\n    os.path.join(stage1_dir, \"best_model.keras\"),\n    custom_objects={\"l2_norm_layer\": l2_norm_layer}\n)\n\n# Unfreeze top layers\nbase_model = model.get_layer(\"resnet50v2\")\nbase_model.trainable = True\nfor layer in base_model.layers[:-50]:\n    layer.trainable = False\nfor i, layer in enumerate(model.layers):\n    print(i, layer.name, type(layer))\n\nprint(f\"Trainable layers: {sum([1 for layer in model.layers if layer.trainable])}\")\n\noptimizer_stage2 = tf.keras.optimizers.AdamW(learning_rate=1e-5, weight_decay=1e-4)\n\nmodel.compile(\n    optimizer=optimizer_stage2,\n    loss=CategoricalCrossentropy(label_smoothing=0.05),\n    metrics=[\"accuracy\",\n             tf.keras.metrics.TopKCategoricalAccuracy(k=5, name='top5_accuracy'),\n             tf.keras.metrics.TopKCategoricalAccuracy(k=10, name='top10_accuracy')]\n)\n\ncallbacks_stage2 = [\n    ModelCheckpoint(\n        os.path.join(stage2_dir, \"best_model.keras\"),\n        monitor=\"val_accuracy\",\n        save_best_only=True,\n        verbose=1\n    ),\n    ModelCheckpoint(\n        os.path.join(stage2_dir, \"model_epoch_{epoch:02d}_acc_{val_accuracy:.4f}.keras\"),\n        save_freq='epoch',\n        verbose=0\n    ),\n    EarlyStopping(\n        monitor=\"val_loss\",\n        patience=8,\n        restore_best_weights=True,\n        verbose=1\n    ),\n    ReduceLROnPlateau(\n        monitor='val_loss',\n        factor=0.5,\n        patience=3,\n        min_lr=1e-8,\n        verbose=1\n    ),\n    CSVLogger(\n        os.path.join(stage2_dir, \"training_log.csv\"),\n        separator=',',\n        append=False\n    ),\n    TensorBoard(\n        log_dir=os.path.join(stage2_dir, \"tensorboard\"),\n        histogram_freq=1\n    ),\n    DetailedMetricsCallback(stage2_dir)\n]\n\nhistory_stage2 = model.fit(\n    train_ds,\n    validation_data=val_ds,\n    epochs=1,\n    callbacks=callbacks_stage2,\n    verbose=1\n)\n\n# Save final model\nmodel.save(os.path.join(stage2_dir, \"final_model.keras\"))\nmodel.save_weights(os.path.join(stage2_dir, \"final_model.weights.h5\"))\nprint(f\"Stage 2 complete. Models saved to {stage2_dir}\")\n\n\n# =============================================================================\n# FINAL EVALUATION\n# =============================================================================\nprint(\"\\n\" + \"=\"*80)\nprint(\"FINAL EVALUATION ON TEST SET\")\nprint(\"=\"*80 + \"\\n\")\n\n# Load best model\nbest_model_path = os.path.join(stage2_dir, \"best_model.keras\")\nmodel = tf.keras.models.load_model(best_model_path, custom_objects={\"l2_norm_layer\": l2_norm_layer})\n\n# Get predictions\ny_true_list, y_pred_list = [], []\nfor batch_x, batch_y in test_ds:\n    preds = model.predict(batch_x, verbose=0)\n    y_true_list.append(batch_y.numpy())\n    y_pred_list.append(preds)\n\ny_true = np.concatenate(y_true_list, axis=0)\ny_pred = np.concatenate(y_pred_list, axis=0)\ny_true_labels = np.argmax(y_true, axis=1)\ny_pred_labels = np.argmax(y_pred, axis=1)\n\n# Compute metrics\nacc = accuracy_score(y_true_labels, y_pred_labels)\ntop5_acc = top_k_accuracy_score(y_true_labels, y_pred_labels, k=5, labels=range(NUM_CLASSES))\ntop10_acc = top_k_accuracy_score(y_true_labels, y_pred_labels, k=10, labels=range(NUM_CLASSES))\nf1 = f1_score(y_true_labels, y_pred_labels, average='macro')\n\nconfidences = np.max(y_pred, axis=1)\navg_confidence = np.mean(confidences)\n\nprint(f\"\\nTest Accuracy: {acc:.4f}\")\nprint(f\"Top-5 Accuracy: {top5_acc:.4f}\")\nprint(f\"Top-10 Accuracy: {top10_acc:.4f}\")\nprint(f\"Macro F1-score: {f1:.4f}\")\nprint(f\"Average Confidence: {avg_confidence:.4f}\")\n\n# Save final metrics\nfinal_metrics = {\n    \"accuracy\": float(acc),\n    \"top5_accuracy\": float(top5_acc),\n    \"top10_accuracy\": float(top10_acc),\n    \"f1_macro\": float(f1),\n    \"avg_confidence\": float(avg_confidence),\n    \"timestamp\": datetime.now().isoformat()\n}\n\nwith open(os.path.join(experiment_dir, \"final_metrics.json\"), \"w\") as f:\n    json.dump(final_metrics, f, indent=4)\n\n# Save predictions\nnp.savez(\n    os.path.join(experiment_dir, \"predictions.npz\"),\n    y_true=y_true,\n    y_pred=y_pred,\n    y_true_labels=y_true_labels,\n    y_pred_labels=y_pred_labels\n)\n\n# =============================================================================\n# VISUALIZATION\n# =============================================================================\ndef plot_combined_history(h1, h2, save_dir):\n    \"\"\"Plot training history from both stages\"\"\"\n    combined = {\n        k: h1.history.get(k, []) + h2.history.get(k, [])\n        for k in set(h1.history.keys()).union(h2.history.keys())\n    }\n    epochs = range(1, len(combined[\"accuracy\"]) + 1)\n    \n    fig, axes = plt.subplots(2, 2, figsize=(15, 10))\n    \n    # Accuracy\n    axes[0, 0].plot(epochs, combined[\"accuracy\"], label=\"Train\")\n    axes[0, 0].plot(epochs, combined[\"val_accuracy\"], label=\"Validation\")\n    axes[0, 0].set_title(\"Accuracy\")\n    axes[0, 0].set_xlabel(\"Epoch\")\n    axes[0, 0].set_ylabel(\"Accuracy\")\n    axes[0, 0].legend()\n    axes[0, 0].grid(True)\n    \n    # Loss\n    axes[0, 1].plot(epochs, combined[\"loss\"], label=\"Train\")\n    axes[0, 1].plot(epochs, combined[\"val_loss\"], label=\"Validation\")\n    axes[0, 1].set_title(\"Loss\")\n    axes[0, 1].set_xlabel(\"Epoch\")\n    axes[0, 1].set_ylabel(\"Loss\")\n    axes[0, 1].legend()a\n    axes[0, 1].grid(True)\n    \n    # Top-5 Accuracy\n    axes[1, 0].plot(epochs, combined[\"top5_accuracy\"], label=\"Train\")\n    axes[1, 0].plot(epochs, combined[\"val_top5_accuracy\"], label=\"Validation\")\n    axes[1, 0].set_title(\"Top-5 Accuracy\")\n    axes[1, 0].set_xlabel(\"Epoch\")\n    axes[1, 0].set_ylabel(\"Accuracy\")\n    axes[1, 0].legend()\n    axes[1, 0].grid(True)\n    \n    # Top-10 Accuracy\n    axes[1, 1].plot(epochs, combined[\"top10_accuracy\"], label=\"Train\")\n    axes[1, 1].plot(epochs, combined[\"val_top10_accuracy\"], label=\"Validation\")\n    axes[1, 1].set_title(\"Top-10 Accuracy\")\n    axes[1, 1].set_xlabel(\"Epoch\")\n    axes[1, 1].set_ylabel(\"Accuracy\")\n    axes[1, 1].legend()\n    axes[1, 1].grid(True)\n    \n    plt.tight_layout()\n    plt.savefig(os.path.join(save_dir, \"training_history.png\"), dpi=200, bbox_inches='tight')\n    plt.show()\n\nplot_combined_history(history_stage1, history_stage2, experiment_dir)\n\n# Confidence distribution\nplt.figure(figsize=(8, 5))\nplt.hist(confidences, bins=50, color='skyblue', edgecolor='black')\nplt.title(\"Distribution of Model Confidence Scores\")\nplt.xlabel(\"Confidence\")\nplt.ylabel(\"Frequency\")\nplt.savefig(os.path.join(experiment_dir, \"confidence_distribution.png\"), dpi=200, bbox_inches='tight')\nplt.show()\n\nprint(f\"\\n{'='*80}\")\nprint(f\"Training complete! All results saved to:\")\nprint(f\"{experiment_dir}\")\nprint(f\"{'='*80}\\n\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-11T16:48:44.041143Z","iopub.execute_input":"2025-11-11T16:48:44.041708Z","iopub.status.idle":"2025-11-11T17:15:50.242383Z","shell.execute_reply.started":"2025-11-11T16:48:44.041683Z","shell.execute_reply":"2025-11-11T17:15:50.241373Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"Classification for face recognition","metadata":{}},{"cell_type":"code","source":"import os, random, shutil, csv, math, itertools, pathlib, time\nfrom pathlib import Path\nimport numpy as np\nimport tensorflow as tf\ntf.config.optimizer.set_jit(True)\nfrom tensorflow import keras\nfrom tensorflow.keras import layers\nfrom PIL import Image\nimport matplotlib.pyplot as plt\nimport seaborn as sns\nfrom tensorflow.keras.preprocessing.image import ImageDataGenerator, img_to_array, load_img\nfrom tensorflow.keras import models, Model\nfrom sklearn.metrics import classification_report, confusion_matrix, accuracy_score, f1_score, top_k_accuracy_score\nfrom tensorflow.keras.callbacks import ModelCheckpoint, EarlyStopping, ReduceLROnPlateau, CSVLogger, TensorBoard\nimport json\nfrom datetime import datetime\n\nsns.set()\n\nfrom tensorflow.keras import mixed_precision\nfrom tensorflow.keras.applications import EfficientNetB0, MobileNetV2, ResNet50V2\nfrom tensorflow.keras.applications.resnet_v2 import preprocess_input\nfrom tensorflow.keras.losses import CategoricalCrossentropy\n\n\n# Mount Google Drive\n# from google.colab import drive\n# drive.mount('/content/drive')\n\n# =============================================================================\n# CONFIGURATION\n# =============================================================================\nNUM_CLASSES = 4000  # Updated for your dataset\nIMG_SIZE = (64, 64)\nBATCH_SIZE = 32  # Reduced for memory efficiency with 4000 classes\nEMBEDDING_DIM = 512  # Larger embedding for more classes\nEPOCH_STAGE1 = 10\nEPOCH_STAGE2 = 20\n\nbase_dir = \"/kaggle/input/11-785-fall-20-homework-2-part-2/classification_data\"\n# base_dir = \"/facerec/data\"\ntrain_dir = base_dir + \"/train_data\"\ntest_dir = base_dir + \"/test_data\"\nval_dir = base_dir + \"/val_data\"\n\n# Create experiment directory with timestamp\ntimestamp = datetime.now().strftime(\"%Y%m%d_%H%M%S\")\n# experiment_dir = os.path.join(base_dir, f\"experiment_{timestamp}\")\n# os.makedirs(experiment_dir, exist_ok=True)\nexperiment_dir = os.path.join(\"/kaggle/working/\", f\"experiment_{timestamp}\")\nos.makedirs(experiment_dir, exist_ok=True)\n\n# Save configuration\nconfig = {\n    \"num_classes\": NUM_CLASSES,\n    \"img_size\": IMG_SIZE,\n    \"batch_size\": BATCH_SIZE,\n    \"embedding_dim\": EMBEDDING_DIM,\n    \"timestamp\": timestamp\n}\nwith open(os.path.join(experiment_dir, \"config.json\"), \"w\") as f:\n    json.dump(config, f, indent=4)\n\n# =============================================================================\n# GPU SETUP\n# =============================================================================\n\ngpus = tf.config.list_physical_devices('GPU')\nif gpus:\n    for gpu in gpus:\n        tf.config.experimental.set_memory_growth(gpu, True)\n    print(f\"GPU(s) available: {len(gpus)}\")\nelse:\n    print(\"No GPU found - training will be slow!\")\n\nmixed_precision.set_global_policy('mixed_float16')\nprint(f\"Mixed precision policy: {mixed_precision.global_policy()}\")\n# =============================================================================\n# DATA LOADING\n# =============================================================================\nprint(\"Loading datasets...\")\ntrain_ds = tf.keras.preprocessing.image_dataset_from_directory(\n    train_dir,\n    image_size=IMG_SIZE,\n    batch_size=BATCH_SIZE,\n    shuffle=True,\n    seed=42,\n    label_mode='categorical'\n)\n\nval_ds = tf.keras.preprocessing.image_dataset_from_directory(\n    val_dir,\n    image_size=IMG_SIZE,\n    batch_size=BATCH_SIZE,\n    shuffle=False,\n    seed=42,\n    label_mode='categorical'\n)\n\ntest_ds = tf.keras.preprocessing.image_dataset_from_directory(\n    test_dir,\n    image_size=IMG_SIZE,\n    batch_size=BATCH_SIZE,\n    shuffle=False,\n    seed=42,\n    label_mode='categorical'\n)\n\n# Save class names\nclass_names = train_ds.class_names\nwith open(os.path.join(experiment_dir, \"class_names.json\"), \"w\") as f:\n    json.dump(class_names, f, indent=4)\nprint(f\"Total classes: {len(class_names)}\")\n\n# Preprocessing\ntrain_ds = train_ds.map(lambda x, y: (x, y))\nval_ds = val_ds.map(lambda x, y: (x, y))\ntest_ds = test_ds.map(lambda x, y: (x, y))\n\n# Optimize pipeline\nAUTOTUNE = tf.data.AUTOTUNE\ntrain_ds = train_ds.shuffle(1000).prefetch(AUTOTUNE)\nval_ds = val_ds.prefetch(AUTOTUNE)\ntest_ds = test_ds.prefetch(AUTOTUNE)\n\n#data augmentation\ndata_augmentation = keras.Sequential([\n    layers.RandomFlip(\"horizontal\"),        # horizontal flip\n    layers.RandomRotation(0.1),             # rotate ±10%\n    layers.RandomZoom(0.1),                 # zoom in/out ±10%\n    layers.RandomContrast(0.1),             # adjust contrast ±10%\n    layers.RandomTranslation(0.1, 0.1)      # translate ±10% horizontally and vertically\n], name=\"data_augmentation\")\n# =============================================================================\n# MODEL DEFINITION\n# =============================================================================\n@tf.keras.utils.register_keras_serializable(package=\"Custom\")\ndef l2_norm_layer(x):\n    return tf.nn.l2_normalize(x, axis=1)\n\ndef create_model(num_classes=NUM_CLASSES, embedding_dim=EMBEDDING_DIM):\n    \"\"\"Create face recognition model with embedding layer\"\"\"\n    inputs = layers.Input(shape=(*IMG_SIZE, 3))\n\n    x = data_augmentation(inputs)\n\n    # Preprocessing for ResNet\n    x = preprocess_input(x)\n    \n    base_model = ResNet50V2(\n        input_shape=(*IMG_SIZE, 3),\n        include_top=False,\n        weights=\"imagenet\"\n    )\n    base_model.trainable = False\n    x = base_model(x, training=False)\n    \n    x = layers.GlobalAveragePooling2D()(x)\n    x = layers.Dense(1024, activation=\"relu\")(x)\n    x = layers.BatchNormalization()(x)\n    x = layers.Dropout(0.5)(x)\n    \n    # Embedding layer (unnormalized)\n    x = layers.Dense(embedding_dim, activation=None, name=\"embedding_dense\")(x)\n    x = layers.BatchNormalization()(x)\n    \n    # L2 normalized embedding\n    embedding = layers.Lambda(l2_norm_layer, name=\"l2_embedding\", output_shape=(embedding_dim,))(x)\n    \n    # Classification head\n    output = layers.Dense(num_classes, activation=\"softmax\", dtype='float32', name=\"softmax_output\")(embedding)\n    \n    model = Model(inputs=inputs, outputs=output, name=\"FaceRecognitionModel\")\n    return model, base_model\n\nmodel, base_model = create_model()\nprint(f\"Model created with {NUM_CLASSES} output classes\")\n\n# =============================================================================\n# CUSTOM CALLBACKS\n# =============================================================================\nclass DetailedMetricsCallback(keras.callbacks.Callback):\n    \"\"\"Save detailed metrics after each epoch\"\"\"\n    def __init__(self, log_dir):\n        super().__init__()\n        self.log_dir = log_dir\n        self.metrics_history = []\n        \n    def on_epoch_end(self, epoch, logs=None):\n        logs = logs or {}\n        epoch_metrics = {\n            'epoch': epoch + 1,\n            'timestamp': datetime.now().isoformat(),\n            **logs\n        }\n        self.metrics_history.append(epoch_metrics)\n        \n        # Save after each epoch\n        with open(os.path.join(self.log_dir, 'detailed_metrics.json'), 'w') as f:\n            json.dump(self.metrics_history, f, indent=4)\n        \n        print(f\"\\nEpoch {epoch+1} metrics saved.\")\n\n# =============================================================================\n# STAGE 1: TRAIN FROZEN BASE\n# =============================================================================\nprint(\"\\n\" + \"=\"*80)\nprint(\"STAGE 1: Training with frozen base model\")\nprint(\"=\"*80 + \"\\n\")\n\nstage1_dir = os.path.join(experiment_dir, \"stage1\")\nos.makedirs(stage1_dir, exist_ok=True)\n\noptimizer_stage1 = tf.keras.optimizers.AdamW(learning_rate=1e-3, weight_decay=1e-4)\n\nmodel.compile(\n    optimizer=optimizer_stage1,\n    loss=CategoricalCrossentropy(label_smoothing=0.1),\n    metrics=[\"accuracy\", \n             tf.keras.metrics.TopKCategoricalAccuracy(k=5, name='top5_accuracy'),\n             tf.keras.metrics.TopKCategoricalAccuracy(k=10, name='top10_accuracy')]\n)\n\ncallbacks_stage1 = [\n    ModelCheckpoint(\n        os.path.join(stage1_dir, \"best_model.keras\"),\n        monitor=\"val_accuracy\",\n        save_best_only=True,\n        verbose=1\n    ),\n    ModelCheckpoint(\n        os.path.join(stage1_dir, \"model_epoch_{epoch:02d}_acc_{val_accuracy:.4f}.keras\"),\n        save_freq='epoch',\n        verbose=0\n    ),\n    EarlyStopping(\n        monitor=\"val_loss\",\n        patience=8,\n        restore_best_weights=True,\n        verbose=1\n    ),\n    ReduceLROnPlateau(\n        monitor='val_loss',\n        factor=0.5,\n        patience=3,\n        min_lr=1e-7,\n        verbose=1\n    ),\n    CSVLogger(\n        os.path.join(stage1_dir, \"training_log.csv\"),\n        separator=',',\n        append=False\n    ),\n    TensorBoard(\n        log_dir=os.path.join(stage1_dir, \"tensorboard\"),\n        histogram_freq=1,\n        write_graph=True\n    ),\n    DetailedMetricsCallback(stage1_dir)\n]\n\nhistory_stage1 = model.fit(\n    train_ds,\n    validation_data=val_ds,\n    epochs=EPOCH_STAGE1,\n    callbacks=callbacks_stage1,\n    verbose=20\n)\n\n# Save final model and weights\nmodel.save(os.path.join(stage1_dir, \"final_model.keras\"))\nmodel.save_weights(os.path.join(stage1_dir, \"final_model_stage1.weights.h5\"))\nprint(f\"Stage 1 complete. Models saved to {stage1_dir}\")\n\n# =============================================================================\n# STAGE 2: FINE-TUNING\n# =============================================================================\nprint(\"\\n\" + \"=\"*80)\nprint(\"STAGE 2: Fine-tuning with unfrozen layers\")\nprint(\"=\"*80 + \"\\n\")\n\nstage2_dir = os.path.join(experiment_dir, \"stage2\")\nos.makedirs(stage2_dir, exist_ok=True)\n\n# Load best model from stage 1\nmodel = tf.keras.models.load_model(\n    os.path.join(stage1_dir, \"best_model.keras\"),\n    custom_objects={\"l2_norm_layer\": l2_norm_layer}\n)\n\n# Unfreeze top layers\n# base_model = model.get_layer(\"resnet50v2\")\n# base_model.trainable = True\n\ntry:\n    base_model = model.get_layer(\"resnet50v2\")\nexcept ValueError:\n    print(\"ResNet50V2 base not found, scanning layers...\")\n    for layer in model.layers:\n        if isinstance(layer, tf.keras.applications.ResNet50V2):\n            base_model = layer\n            break\n\nfor layer in base_model.layers[:-50]:\n    layer.trainable = False\nfor i, layer in enumerate(model.layers):\n    print(i, layer.name, type(layer))\n\nprint(f\"Trainable layers: {sum([1 for layer in model.layers if layer.trainable])}\")\n\noptimizer_stage2 = tf.keras.optimizers.AdamW(learning_rate=1e-5, weight_decay=1e-4)\n\nmodel.compile(\n    optimizer=optimizer_stage2,\n    loss=CategoricalCrossentropy(label_smoothing=0.05),\n    metrics=[\"accuracy\",\n             tf.keras.metrics.TopKCategoricalAccuracy(k=5, name='top5_accuracy'),\n             tf.keras.metrics.TopKCategoricalAccuracy(k=10, name='top10_accuracy')]\n)\n\ncallbacks_stage2 = [\n    ModelCheckpoint(\n        os.path.join(stage2_dir, \"best_model.keras\"),\n        monitor=\"val_accuracy\",\n        save_best_only=True,\n        verbose=1\n    ),\n    ModelCheckpoint(\n        os.path.join(stage2_dir, \"model_epoch_{epoch:02d}_acc_{val_accuracy:.4f}.keras\"),\n        save_freq='epoch',\n        verbose=0\n    ),\n    EarlyStopping(\n        monitor=\"val_loss\",\n        patience=8,\n        restore_best_weights=True,\n        verbose=1\n    ),\n    ReduceLROnPlateau(\n        monitor='val_loss',\n        factor=0.5,\n        patience=3,\n        min_lr=1e-8,\n        verbose=1\n    ),\n    CSVLogger(\n        os.path.join(stage2_dir, \"training_log.csv\"),\n        separator=',',\n        append=False\n    ),\n    TensorBoard(\n        log_dir=os.path.join(stage2_dir, \"tensorboard\"),\n        histogram_freq=1\n    ),\n    DetailedMetricsCallback(stage2_dir)\n]\n\nhistory_stage2 = model.fit(\n    train_ds,\n    validation_data=val_ds,\n    epochs=EPOCH_STAGE2,\n    callbacks=callbacks_stage2,\n    verbose=1\n)\n\n# Save final model\nmodel.save(os.path.join(stage2_dir, \"final_model.keras\"))\nmodel.save_weights(os.path.join(stage2_dir, \"final_model_stage2.weights.h5\"))\nprint(f\"Stage 2 complete. Models saved to {stage2_dir}\")\n\n\n# =============================================================================\n# FINAL EVALUATION\n# =============================================================================\nprint(\"\\n\" + \"=\"*80)\nprint(\"FINAL EVALUATION ON TEST SET\")\nprint(\"=\"*80 + \"\\n\")\n\n# Load best model\nbest_model_path = os.path.join(stage2_dir, \"best_model.keras\")\nmodel = tf.keras.models.load_model(best_model_path, custom_objects={\"l2_norm_layer\": l2_norm_layer})\n\n# Get predictions\ny_true_list, y_pred_list = [], []\nfor batch_x, batch_y in test_ds:\n    preds = model.predict(batch_x, verbose=0)\n    y_true_list.append(batch_y.numpy())\n    y_pred_list.append(preds)\n\ny_true = np.concatenate(y_true_list, axis=0)\ny_pred = np.concatenate(y_pred_list, axis=0)\ny_true_labels = np.argmax(y_true, axis=1)\ny_pred_labels = np.argmax(y_pred, axis=1)\n\n# Compute metrics\nacc = accuracy_score(y_true_labels, y_pred_labels)\n# top5_acc = top_k_accuracy_score(y_true_labels, y_pred, k=5, labels=range(NUM_CLASSES))\n# top10_acc = top_k_accuracy_score(y_true_labels, y_pred, k=10, labels=range(NUM_CLASSES))\ntop5_acc = top_k_accuracy_score(y_true_labels, y_pred, k=5)  # remove labels param\ntop10_acc = top_k_accuracy_score(y_true_labels, y_pred, k=10)\n\nf1 = f1_score(y_true_labels, y_pred_labels, average='macro')\n\nconfidences = np.max(y_pred, axis=1)\navg_confidence = np.mean(confidences)\n\nprint(f\"\\nTest Accuracy: {acc:.4f}\")\nprint(f\"Top-5 Accuracy: {top5_acc:.4f}\")\nprint(f\"Top-10 Accuracy: {top10_acc:.4f}\")\nprint(f\"Macro F1-score: {f1:.4f}\")\nprint(f\"Average Confidence: {avg_confidence:.4f}\")\n\n# Save final metrics\nfinal_metrics = {\n    \"accuracy\": float(acc),\n    \"top5_accuracy\": float(top5_acc),\n    \"top10_accuracy\": float(top10_acc),\n    \"f1_macro\": float(f1),\n    \"avg_confidence\": float(avg_confidence),\n    \"timestamp\": datetime.now().isoformat()\n}\n\nwith open(os.path.join(experiment_dir, \"final_metrics.json\"), \"w\") as f:\n    json.dump(final_metrics, f, indent=4)\n\n# Save predictions\nnp.savez(\n    os.path.join(experiment_dir, \"predictions.npz\"),\n    y_true=y_true,\n    y_pred=y_pred,\n    y_true_labels=y_true_labels,\n    y_pred_labels=y_pred_labels\n)\n\n\n\n\n# =============================================================================\n# VISUALIZATION\n# =============================================================================\ndef plot_combined_history(h1, h2, save_dir):\n    \"\"\"Plot training history from both stages\"\"\"\n    combined = {\n        k: h1.history.get(k, []) + h2.history.get(k, [])\n        for k in set(h1.history.keys()).union(h2.history.keys())\n    }\n    \n    # combined = {}\n    # all_keys = set(h1.history.keys()).union(h2.history.keys())\n    # for k in all_keys:\n    #     combined[k] = h1.history.get(k, []) + h2.history.get(k, [])\n    \n    epochs = range(1, len(combined[\"accuracy\"]) + 1)\n    \n    fig, axes = plt.subplots(2, 2, figsize=(15, 10))\n\n    # def safe_plot(ax, key_train, key_val, title):\n    # if key_train in combined and key_val in combined:\n    #     ax.plot(range(1, len(combined[key_train])+1), combined[key_train], label=\"Train\")\n    #     ax.plot(range(1, len(combined[key_val])+1), combined[key_val], label=\"Validation\")\n    #     ax.set_title(title)\n    #     ax.legend()\n    #     ax.grid(True)\n    \n    # Accuracy\n    axes[0, 0].plot(epochs, combined[\"accuracy\"], label=\"Train\")\n    axes[0, 0].plot(epochs, combined[\"val_accuracy\"], label=\"Validation\")\n    axes[0, 0].set_title(\"Accuracy\")\n    axes[0, 0].set_xlabel(\"Epoch\")\n    axes[0, 0].set_ylabel(\"Accuracy\")\n    axes[0, 0].legend()\n    axes[0, 0].grid(True)\n    \n    # Loss\n    axes[0, 1].plot(epochs, combined[\"loss\"], label=\"Train\")\n    axes[0, 1].plot(epochs, combined[\"val_loss\"], label=\"Validation\")\n    axes[0, 1].set_title(\"Loss\")\n    axes[0, 1].set_xlabel(\"Epoch\")\n    axes[0, 1].set_ylabel(\"Loss\")\n    axes[0, 1].legend()\n    axes[0, 1].grid(True)\n    \n    # Top-5 Accuracy\n    axes[1, 0].plot(epochs, combined[\"top5_accuracy\"], label=\"Train\")\n    axes[1, 0].plot(epochs, combined[\"val_top5_accuracy\"], label=\"Validation\")\n    axes[1, 0].set_title(\"Top-5 Accuracy\")\n    axes[1, 0].set_xlabel(\"Epoch\")\n    axes[1, 0].set_ylabel(\"Accuracy\")\n    axes[1, 0].legend()\n    axes[1, 0].grid(True)\n    \n    # Top-10 Accuracy\n    axes[1, 1].plot(epochs, combined[\"top10_accuracy\"], label=\"Train\")\n    axes[1, 1].plot(epochs, combined[\"val_top10_accuracy\"], label=\"Validation\")\n    axes[1, 1].set_title(\"Top-10 Accuracy\")\n    axes[1, 1].set_xlabel(\"Epoch\")\n    axes[1, 1].set_ylabel(\"Accuracy\")\n    axes[1, 1].legend()\n    axes[1, 1].grid(True)\n    \n    plt.tight_layout()\n    plt.savefig(os.path.join(save_dir, \"training_history.png\"), dpi=200, bbox_inches='tight')\n    plt.show()\n\nplot_combined_history(history_stage1, history_stage2, experiment_dir)\n\n# Confidence distribution\nplt.figure(figsize=(8, 5))\nplt.hist(confidences, bins=50, color='skyblue', edgecolor='black')\nplt.title(\"Distribution of Model Confidence Scores\")\nplt.xlabel(\"Confidence\")\nplt.ylabel(\"Frequency\")\nplt.savefig(os.path.join(experiment_dir, \"confidence_distribution.png\"), dpi=200, bbox_inches='tight')\nplt.show()\n\nprint(f\"\\n{'='*80}\")\nprint(f\"Training complete! All results saved to:\")\nprint(f\"{experiment_dir}\")\nprint(f\"{'='*80}\\n\")\n\ndef save_all_artifacts(experiment_dir, metrics, histories, confidences, y_true, y_pred):\n    \"\"\"Save metrics, histories, confidence distribution, and predictions\"\"\"\n    os.makedirs(experiment_dir, exist_ok=True)\n\n    # Save final metrics\n    with open(os.path.join(experiment_dir, \"final_metrics.json\"), \"w\") as f:\n        json.dump(metrics, f, indent=4)\n\n    # Save training histories (combined)\n    with open(os.path.join(experiment_dir, \"history_stage1.json\"), \"w\") as f:\n        json.dump(histories[0].history, f, indent=4)\n    with open(os.path.join(experiment_dir, \"history_stage2.json\"), \"w\") as f:\n        json.dump(histories[1].history, f, indent=4)\n\n    # Save predictions\n    np.savez(\n        os.path.join(experiment_dir, \"predictions.npz\"),\n        y_true=y_true,\n        y_pred=y_pred\n    )\n\n    # Confidence distribution\n    plt.figure(figsize=(8, 5))\n    plt.hist(confidences, bins=50, color='skyblue', edgecolor='black')\n    plt.title(\"Distribution of Model Confidence Scores\")\n    plt.xlabel(\"Confidence\")\n    plt.ylabel(\"Frequency\")\n    plt.savefig(os.path.join(experiment_dir, \"confidence_distribution.png\"), dpi=200, bbox_inches='tight')\n    plt.close()\n\n    # Save all artifacts\nsave_all_artifacts(\n    experiment_dir,\n    final_metrics,\n    (history_stage1, history_stage2),\n    confidences,\n    y_true,\n    y_pred\n)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-12T08:30:43.305744Z","iopub.execute_input":"2025-11-12T08:30:43.306267Z","iopub.status.idle":"2025-11-12T09:25:39.850149Z","shell.execute_reply.started":"2025-11-12T08:30:43.306244Z","shell.execute_reply":"2025-11-12T09:25:39.849076Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"Continue Stage 2","metadata":{}},{"cell_type":"code","source":"import os, json, numpy as np, tensorflow as tf\nfrom tensorflow import keras\nfrom tensorflow.keras import layers, Model\nfrom tensorflow.keras.applications.resnet_v2 import preprocess_input\nfrom tensorflow.keras.losses import CategoricalCrossentropy\nfrom datetime import datetime\nfrom sklearn.metrics import accuracy_score, f1_score, top_k_accuracy_score\nimport matplotlib.pyplot as plt\n\n# =============================================================================\n# 1. RELOAD CONFIGURATION\n# =============================================================================\nexperiment_dir = \"/kaggle/working/experiment_20251112_0833056\"  # ← CHANGE to your experiment folder\nstage1_dir = os.path.join(experiment_dir, \"stage1\")\nstage2_dir = os.path.join(experiment_dir, \"stage2\")\nos.makedirs(stage2_dir, exist_ok=True)\n\n# Load config\nwith open(os.path.join(experiment_dir, \"config.json\")) as f:\n    config = json.load(f)\n\nNUM_CLASSES = config[\"num_classes\"]\nIMG_SIZE = tuple(config[\"img_size\"])\nBATCH_SIZE = config[\"batch_size\"]\nEMBEDDING_DIM = config[\"embedding_dim\"]\n\n# =============================================================================\n# 2. DATASETS (train, val, test)\n# =============================================================================\nbase_dir = \"/kaggle/input/11-785-fall-20-homework-2-part-2/classification_data\"\ntrain_dir = base_dir + \"/train_data\"\nval_dir = base_dir + \"/val_data\"\ntest_dir = base_dir + \"/test_data\"\n\ntrain_ds = tf.keras.preprocessing.image_dataset_from_directory(\n    train_dir,\n    image_size=IMG_SIZE,\n    batch_size=BATCH_SIZE,\n    shuffle=True,\n    label_mode='categorical'\n)\n\nval_ds = tf.keras.preprocessing.image_dataset_from_directory(\n    val_dir,\n    image_size=IMG_SIZE,\n    batch_size=BATCH_SIZE,\n    shuffle=False,\n    label_mode='categorical'\n)\n\ntest_ds = tf.keras.preprocessing.image_dataset_from_directory(\n    test_dir,\n    image_size=IMG_SIZE,\n    batch_size=BATCH_SIZE,\n    shuffle=False,\n    label_mode='categorical'\n)\n\nAUTOTUNE = tf.data.AUTOTUNE\ntrain_ds = train_ds.shuffle(1000).prefetch(AUTOTUNE)\nval_ds = val_ds.prefetch(AUTOTUNE)\ntest_ds = test_ds.prefetch(AUTOTUNE)\n\n# =============================================================================\n# 3. LOAD STAGE 1 MODEL\n# =============================================================================\n@tf.keras.utils.register_keras_serializable(package=\"Custom\")\ndef l2_norm_layer(x):\n    return tf.nn.l2_normalize(x, axis=1)\n\nmodel = tf.keras.models.load_model(\n    os.path.join(stage1_dir, \"best_model.keras\"),\n    custom_objects={\"l2_norm_layer\": l2_norm_layer}\n)\n\n# Try to locate the ResNet50V2 backbone\ntry:\n    base_model = model.get_layer(\"resnet50v2\")\nexcept ValueError:\n    base_model = None\n    for layer in model.layers:\n        if isinstance(layer, tf.keras.Model) and \"resnet\" in layer.name.lower():\n            base_model = layer\n            break\n\n# Unfreeze the top 50 layers for fine-tuning\nif base_model is not None:\n    for layer in base_model.layers[:-50]:\n        layer.trainable = False\n    for layer in base_model.layers[-50:]:\n        layer.trainable = True\n    print(f\"Unfroze {sum([1 for l in base_model.layers if l.trainable])} layers in base model.\")\nelse:\n    print(\"⚠️ Could not find ResNet base model; skipping fine-tuning unfreeze.\")\n\n# =============================================================================\n# 4. COMPILE FOR FINE-TUNING\n# =============================================================================\noptimizer_stage2 = tf.keras.optimizers.AdamW(learning_rate=1e-5, weight_decay=1e-4)\n\nmodel.compile(\n    optimizer=optimizer_stage2,\n    loss=CategoricalCrossentropy(label_smoothing=0.05),\n    metrics=[\n        \"accuracy\",\n        tf.keras.metrics.TopKCategoricalAccuracy(k=5, name='top5_accuracy'),\n        tf.keras.metrics.TopKCategoricalAccuracy(k=10, name='top10_accuracy')\n    ]\n)\n\n# =============================================================================\n# 5. CALLBACKS\n# =============================================================================\nfrom tensorflow.keras.callbacks import (\n    ModelCheckpoint, EarlyStopping, ReduceLROnPlateau,\n    CSVLogger, TensorBoard\n)\n\nclass DetailedMetricsCallback(keras.callbacks.Callback):\n    def __init__(self, log_dir):\n        super().__init__()\n        self.log_dir = log_dir\n        self.metrics_history = []\n    def on_epoch_end(self, epoch, logs=None):\n        logs = logs or {}\n        epoch_metrics = {\n            'epoch': epoch + 1,\n            'timestamp': datetime.now().isoformat(),\n            **logs\n        }\n        self.metrics_history.append(epoch_metrics)\n        with open(os.path.join(self.log_dir, 'detailed_metrics.json'), 'w') as f:\n            json.dump(self.metrics_history, f, indent=4)\n        print(f\"\\nEpoch {epoch+1} metrics saved.\")\n\ncallbacks_stage2 = [\n    ModelCheckpoint(\n        os.path.join(stage2_dir, \"best_model.keras\"),\n        monitor=\"val_accuracy\",\n        save_best_only=True,\n        verbose=1\n    ),\n    EarlyStopping(\n        monitor=\"val_loss\",\n        patience=8,\n        restore_best_weights=True,\n        verbose=1\n    ),\n    ReduceLROnPlateau(\n        monitor='val_loss',\n        factor=0.5,\n        patience=3,\n        min_lr=1e-8,\n        verbose=1\n    ),\n    CSVLogger(os.path.join(stage2_dir, \"training_log.csv\")),\n    TensorBoard(log_dir=os.path.join(stage2_dir, \"tensorboard\")),\n    DetailedMetricsCallback(stage2_dir)\n]\n\n# =============================================================================\n# 6. RUN STAGE 2 TRAINING\n# =============================================================================\nEPOCH_STAGE2 = 20  # adjust as desired\n\nhistory_stage2 = model.fit(\n    train_ds,\n    validation_data=val_ds,\n    epochs=EPOCH_STAGE2,\n    callbacks=callbacks_stage2,\n    verbose=1\n)\n\n# Save final fine-tuned model\nmodel.save(os.path.join(stage2_dir, \"final_model.keras\"))\nmodel.save_weights(os.path.join(stage2_dir, \"final_model_stage2.weights.h5\"))\nprint(f\"✅ Stage 2 complete. Models saved to {stage2_dir}\")\n\n# =============================================================================\n# 7. FINAL EVALUATION ON TEST SET\n# =============================================================================\nprint(\"\\n\" + \"=\"*80)\nprint(\"FINAL EVALUATION ON TEST SET\")\nprint(\"=\"*80 + \"\\n\")\n\nbest_model_path = os.path.join(stage2_dir, \"best_model.keras\")\nmodel = tf.keras.models.load_model(best_model_path, custom_objects={\"l2_norm_layer\": l2_norm_layer})\n\ny_true_list, y_pred_list = [], []\nfor batch_x, batch_y in test_ds:\n    preds = model.predict(batch_x, verbose=0)\n    y_true_list.append(batch_y.numpy())\n    y_pred_list.append(preds)\n\ny_true = np.concatenate(y_true_list, axis=0)\ny_pred = np.concatenate(y_pred_list, axis=0)\ny_true_labels = np.argmax(y_true, axis=1)\ny_pred_labels = np.argmax(y_pred, axis=1)\n\n# Compute metrics\nacc = accuracy_score(y_true_labels, y_pred_labels)\ntop5_acc = top_k_accuracy_score(y_true_labels, y_pred, k=5)\ntop10_acc = top_k_accuracy_score(y_true_labels, y_pred, k=10)\nf1 = f1_score(y_true_labels, y_pred_labels, average='macro')\nconfidences = np.max(y_pred, axis=1)\navg_confidence = np.mean(confidences)\n\nprint(f\"\\nTest Accuracy: {acc:.4f}\")\nprint(f\"Top-5 Accuracy: {top5_acc:.4f}\")\nprint(f\"Top-10 Accuracy: {top10_acc:.4f}\")\nprint(f\"Macro F1-score: {f1:.4f}\")\nprint(f\"Average Confidence: {avg_confidence:.4f}\")\n\n# Save final metrics\nfinal_metrics = {\n    \"accuracy\": float(acc),\n    \"top5_accuracy\": float(top5_acc),\n    \"top10_accuracy\": float(top10_acc),\n    \"f1_macro\": float(f1),\n    \"avg_confidence\": float(avg_confidence),\n    \"timestamp\": datetime.now().isoformat()\n}\n\nwith open(os.path.join(experiment_dir, \"final_metrics.json\"), \"w\") as f:\n    json.dump(final_metrics, f, indent=4)\n\n# Save predictions\nnp.savez(\n    os.path.join(experiment_dir, \"predictions_stage2.npz\"),\n    y_true=y_true,\n    y_pred=y_pred,\n    y_true_labels=y_true_labels,\n    y_pred_labels=y_pred_labels\n)\n\n# =============================================================================\n# 8. VISUALIZATION\n# =============================================================================\nplt.figure(figsize=(8, 5))\nplt.hist(confidences, bins=50, color='skyblue', edgecolor='black')\nplt.title(\"Distribution of Model Confidence Scores\")\nplt.xlabel(\"Confidence\")\nplt.ylabel(\"Frequency\")\nplt.savefig(os.path.join(experiment_dir, \"confidence_distribution_stage2.png\"), dpi=200, bbox_inches='tight')\nplt.show()\n\nprint(f\"\\n{'='*80}\")\nprint(f\"Stage 2 and final evaluation complete! All results saved to:\")\nprint(f\"{experiment_dir}\")\nprint(f\"{'='*80}\\n\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-12T09:26:44.329363Z","iopub.execute_input":"2025-11-12T09:26:44.329901Z","iopub.status.idle":"2025-11-12T09:28:24.503552Z","shell.execute_reply.started":"2025-11-12T09:26:44.329882Z","shell.execute_reply":"2025-11-12T09:28:24.502500Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"!zip -r /kaggle/working/experiment_stage1.zip /kaggle/working/experiment_20251112_083056*\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-12T09:40:40.218920Z","iopub.execute_input":"2025-11-12T09:40:40.219644Z","iopub.status.idle":"2025-11-12T09:43:16.306935Z","shell.execute_reply.started":"2025-11-12T09:40:40.219617Z","shell.execute_reply":"2025-11-12T09:43:16.306154Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"from IPython.display import FileLink\nFileLink('/kaggle/working/experiment_stage1.zip')\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-12T09:52:56.309446Z","iopub.execute_input":"2025-11-12T09:52:56.310238Z","iopub.status.idle":"2025-11-12T09:52:56.315533Z","shell.execute_reply.started":"2025-11-12T09:52:56.310214Z","shell.execute_reply":"2025-11-12T09:52:56.314911Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"!kaggle datasets create -p /kaggle/working/experiment_20251112_083056 \\\n    -r zip --title \"Stage1 ResNet model\" \\\n    --description \"Saved Stage 1 experiment for future fine-tuning\"\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-12T10:16:04.651872Z","iopub.execute_input":"2025-11-12T10:16:04.652610Z","iopub.status.idle":"2025-11-12T10:16:05.136241Z","shell.execute_reply.started":"2025-11-12T10:16:04.652586Z","shell.execute_reply":"2025-11-12T10:16:05.135262Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"!kaggle datasets create -p /kaggle/working/experiment_20251112_083056 -r zip\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-12T10:16:52.336799Z","iopub.execute_input":"2025-11-12T10:16:52.337616Z","iopub.status.idle":"2025-11-12T10:16:52.822245Z","shell.execute_reply.started":"2025-11-12T10:16:52.337588Z","shell.execute_reply":"2025-11-12T10:16:52.821443Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"!mkdir -p ~/.kaggle\n!cp /kaggle/input/meinapi/kaggle.json ~/.kaggle/\n!chmod 600 ~/.kaggle/kaggle.json\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-12T10:11:30.523769Z","iopub.execute_input":"2025-11-12T10:11:30.524545Z","iopub.status.idle":"2025-11-12T10:11:31.091006Z","shell.execute_reply.started":"2025-11-12T10:11:30.524519Z","shell.execute_reply":"2025-11-12T10:11:31.089891Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"!kaggle datasets list -s imagenet\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-12T10:11:46.255146Z","iopub.execute_input":"2025-11-12T10:11:46.255506Z","iopub.status.idle":"2025-11-12T10:11:47.179577Z","shell.execute_reply.started":"2025-11-12T10:11:46.255479Z","shell.execute_reply":"2025-11-12T10:11:47.178791Z"}},"outputs":[],"execution_count":null}]}