{"metadata":{"kernelspec":{"display_name":"Python 3","language":"python","name":"python3"},"language_info":{"name":"python","version":"3.10.0"},"kaggle":{"accelerator":"nvidiaTeslaT4","dataSources":[{"sourceType":"competition","sourceId":14774,"databundleVersionId":875431}],"dockerImageVersionId":31329,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"# Reduced AMCA — VGG16 — APTOS 2019 Binary Classification\n**Architecture**: VGG16 (ImageNet pretrained)  \n**Task**: Diabetic Retinopathy Detection (Binary)  \n**Features**: Auto cutting point · Adaptive freezing · Multi-phase fine-tuning · FLOPs · Size per layer · Full graphs","metadata":{}},{"cell_type":"code","source":"# ============================================================\n# Imports\n# ============================================================\n\nimport os\nimport math\nimport random\nimport numpy as np\nimport pandas as pd\nimport tensorflow as tf\nimport matplotlib.pyplot as plt\nimport matplotlib.gridspec as gridspec\nimport seaborn as sns\n\nfrom dataclasses import dataclass\nfrom typing import Dict, Tuple, Literal\nfrom tabulate import tabulate\n\nfrom sklearn.model_selection import train_test_split\nfrom sklearn.utils.class_weight import compute_class_weight\nfrom sklearn.metrics import (\n    classification_report, confusion_matrix,\n    roc_auc_score, roc_curve, precision_recall_curve\n)\n\n# ============================================================\n# Reproducibility\n# ============================================================\n\nSEED = 42\nrandom.seed(SEED)\nnp.random.seed(SEED)\ntf.random.set_seed(SEED)\n\nAUTOTUNE = tf.data.AUTOTUNE","metadata":{},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# Paths  (edit for local / Kaggle)\n# ============================================================\n\nDATA_DIR  = \"/kaggle/input/competitions/aptos2019-blindness-detection\"\nCSV_PATH  = os.path.join(DATA_DIR, \"train.csv\")\nIMAGE_DIR = os.path.join(DATA_DIR, \"train_images\")\n\n# ============================================================\n# Global Config  ← VGG16 here\n# ============================================================\n\nIMG_SIZE        = 224\nBATCH_SIZE      = 16\nEPOCHS_PHASE1   = 8\nEPOCHS_PHASE2   = 8\nEPOCHS_PHASE3   = 6\n\nARCHITECTURE       = \"vgg16\"      # ← changed from resnet50\nCOMPUTE_BUDGET     = \"medium\"\nTARGET_PERFORMANCE = \"high\"\nTASK_TYPE          = \"classification\"\n\nUSE_CLASS_WEIGHTS   = True\nUSE_MIXED_PRECISION = False\n\nif USE_MIXED_PRECISION:\n    tf.keras.mixed_precision.set_global_policy(\"mixed_float16\")","metadata":{},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# Reduced AMCA  (New AMCA)\n# ============================================================\n\nProblemLevel    = Literal[\"simple\", \"moderate\", \"complex\"]\nComputeLevel    = Literal[\"very_low\", \"low\", \"medium\", \"high\"]\nPerformanceLevel = Literal[\"low\", \"medium\", \"high\", \"very_high\"]\n\n\n@dataclass\nclass ProblemAnalysis:\n    num_classes: int\n    intra_class_variability: str\n    task_type: str\n\n\nclass ReducedAutomatedModelCuttingAlgorithm:\n\n    def __init__(self, problem_analysis: Dict, constraints: Dict, requirements: Dict):\n        self.problem      = problem_analysis\n        self.constraints  = constraints\n        self.requirements = requirements\n\n    # ---------- public ----------\n\n    def analyze_problem_characteristics(self):\n        return {\n            \"complexity_level\" : self._assess_problem_complexity(),\n            \"compute_level\"    : self.constraints.get(\"compute_budget\",     \"medium\"),\n            \"performance_level\": self.requirements.get(\"target_performance\", \"medium\"),\n            \"task_type\"        : self.problem.get(\"task_type\", \"classification\")\n        }\n\n    def determine_cutting_strategy(self, architecture: str):\n        characteristics = self.analyze_problem_characteristics()\n        strategy = {\n            \"cutting_point\"        : self._determine_cutting_point(characteristics, architecture),\n            \"freezing_strategy\"    : self._determine_freezing_strategy(characteristics),\n            \"head_architecture\"    : self._design_head_architecture(characteristics),\n            \"fine_tuning_plan\"     : self._create_fine_tuning_plan(characteristics),\n            \"learning_rate_strategy\": self._determine_learning_rate_strategy(characteristics),\n            \"characteristics\"      : characteristics\n        }\n        return strategy\n\n    # ---------- private ----------\n\n    def _assess_problem_complexity(self):\n        num_classes  = self.problem.get(\"num_classes\", 2)\n        variability  = self.problem.get(\"intra_class_variability\", \"medium\")\n        if num_classes <= 2 and variability == \"low\":                      return \"simple\"\n        elif num_classes <= 5 and variability in [\"low\", \"medium\"]:        return \"moderate\"\n        else:                                                               return \"complex\"\n\n    def _determine_cutting_point(self, characteristics, architecture):\n        complexity   = characteristics[\"complexity_level\"]\n        compute_level = characteristics[\"compute_level\"]\n        performance  = characteristics[\"performance_level\"]\n        arch = architecture.lower()\n\n        if arch == \"vgg16\":\n            if compute_level == \"very_low\":                        return \"block2_pool\"\n            if complexity == \"simple\":                             return \"block3_pool\"\n            if performance in [\"high\", \"very_high\"]:              return \"block5_pool\"   # ← AMCA chose this\n            return \"block4_pool\"\n\n        elif arch == \"resnet50\":\n            if compute_level == \"very_low\":                        return \"conv2_block3_out\"\n            if complexity == \"simple\":                             return \"conv3_block4_out\"\n            if performance in [\"high\", \"very_high\"]:              return \"conv5_block3_out\"\n            return \"conv4_block6_out\"\n\n        elif arch == \"mobilenetv2\":\n            if compute_level in [\"very_low\", \"low\"]:              return \"block_6_expand_relu\"\n            if complexity == \"simple\":                             return \"block_13_expand_relu\"\n            return \"out_relu\"\n\n        elif arch == \"inceptionv3\":\n            if compute_level == \"very_low\":                        return \"mixed3\"\n            if complexity == \"simple\":                             return \"mixed7\"\n            return \"mixed10\"\n\n        return \"late\"\n\n    def _determine_freezing_strategy(self, characteristics):\n        complexity    = characteristics[\"complexity_level\"]\n        compute_level = characteristics[\"compute_level\"]\n        performance   = characteristics[\"performance_level\"]\n\n        if compute_level == \"very_low\":                                return {\"freeze_ratio\": 1.00, \"unfreeze_from\": None,                  \"description\": \"Train head only\"}\n        if compute_level == \"low\":                                     return {\"freeze_ratio\": 0.85, \"unfreeze_from\": \"top_15_percent\",        \"description\": \"Unfreeze top layers only\"}\n        if complexity == \"simple\" and performance in [\"low\",\"medium\"]: return {\"freeze_ratio\": 0.80, \"unfreeze_from\": \"top_20_percent\",        \"description\": \"Mostly frozen\"}\n        if complexity == \"moderate\":                                   return {\"freeze_ratio\": 0.50, \"unfreeze_from\": \"top_50_percent\",        \"description\": \"Half frozen\"}\n        return                                                                {\"freeze_ratio\": 0.20, \"unfreeze_from\": \"top_80_percent\",        \"description\": \"Light freezing\"}\n\n    def _design_head_architecture(self, characteristics):\n        complexity = characteristics[\"complexity_level\"]\n        if complexity == \"simple\":    return {\"dense_units\": [256],        \"dropout\": [0.30]}\n        elif complexity == \"moderate\": return {\"dense_units\": [512, 256],   \"dropout\": [0.30, 0.30]}\n        else:                          return {\"dense_units\": [1024, 512],  \"dropout\": [0.40, 0.30]}\n\n    def _create_fine_tuning_plan(self, characteristics):\n        complexity    = characteristics[\"complexity_level\"]\n        compute_level = characteristics[\"compute_level\"]\n        performance   = characteristics[\"performance_level\"]\n        if compute_level == \"very_low\":                                           return {\"phases\": 1}\n        if complexity == \"simple\" and performance in [\"low\", \"medium\"]:           return {\"phases\": 1}\n        if complexity == \"moderate\":                                               return {\"phases\": 2}\n        return                                                                            {\"phases\": 3}\n\n    def _determine_learning_rate_strategy(self, characteristics):\n        complexity    = characteristics[\"complexity_level\"]\n        compute_level = characteristics[\"compute_level\"]\n        if compute_level == \"very_low\": return {\"phase1_lr\": 1e-3, \"phase2_lr\": 1e-4, \"phase3_lr\": 1e-5}\n        if complexity == \"simple\":      return {\"phase1_lr\": 1e-3, \"phase2_lr\": 5e-4, \"phase3_lr\": 1e-5}\n        if complexity == \"moderate\":    return {\"phase1_lr\": 1e-3, \"phase2_lr\": 1e-4, \"phase3_lr\": 1e-5}\n        return                                 {\"phase1_lr\": 5e-4, \"phase2_lr\": 1e-4, \"phase3_lr\": 5e-6}","metadata":{},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# Data utilities\n# ============================================================\n\ndef make_binary_label(x: int) -> int:\n    return 0 if int(x) == 0 else 1\n\n\ndef load_dataframe(csv_path: str, image_dir: str) -> pd.DataFrame:\n    df = pd.read_csv(csv_path)\n    df[\"label\"]      = df[\"diagnosis\"].apply(make_binary_label)\n    df[\"image_path\"] = df[\"id_code\"].apply(lambda x: os.path.join(image_dir, f\"{x}.png\"))\n    df = df[df[\"image_path\"].apply(os.path.exists)].reset_index(drop=True)\n    return df\n\n\ndef split_dataframe(df: pd.DataFrame):\n    train_df, val_df = train_test_split(\n        df, test_size=0.2, random_state=SEED, stratify=df[\"label\"]\n    )\n    return train_df.reset_index(drop=True), val_df.reset_index(drop=True)\n\n\n# ============================================================\n# Image preprocessing\n# ============================================================\n\ndef crop_black_borders(image: tf.Tensor, tolerance: int = 7) -> tf.Tensor:\n    gray  = tf.image.rgb_to_grayscale(image)\n    mask  = gray > tolerance\n    coords = tf.where(mask[:, :, 0])\n    if tf.shape(coords)[0] == 0:\n        return image\n    y_min = tf.reduce_min(coords[:, 0]);  y_max = tf.reduce_max(coords[:, 0])\n    x_min = tf.reduce_min(coords[:, 1]);  x_max = tf.reduce_max(coords[:, 1])\n    return image[y_min:y_max + 1, x_min:x_max + 1, :]\n\n\ndef decode_and_resize(path, label, img_size=224, training=False):\n    image   = tf.io.read_file(path)\n    image   = tf.image.decode_png(image, channels=3)\n    image   = tf.cast(image, tf.float32)\n    image   = crop_black_borders(image)\n    image   = tf.image.resize(image, (img_size, img_size))\n    if training:\n        image = tf.image.random_flip_left_right(image)\n        image = tf.image.random_flip_up_down(image)\n        image = tf.image.random_brightness(image, 0.10)\n        image = tf.image.random_contrast(image,   0.90, 1.10)\n        image = tf.image.random_saturation(image, 0.90, 1.10)\n    return image, tf.cast(label, tf.float32)\n\n\ndef build_dataset(df: pd.DataFrame, img_size=224, batch_size=16, training=False):\n    ds = tf.data.Dataset.from_tensor_slices((df[\"image_path\"].values, df[\"label\"].values))\n    if training:\n        ds = ds.shuffle(buffer_size=len(df), seed=SEED, reshuffle_each_iteration=True)\n    ds = ds.map(\n        lambda p, y: decode_and_resize(p, y, img_size=img_size, training=training),\n        num_parallel_calls=AUTOTUNE\n    )\n    return ds.batch(batch_size).prefetch(AUTOTUNE)","metadata":{},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# Backbone utilities\n# ============================================================\n\ndef get_backbone_and_preprocess(architecture: str, input_shape=(224, 224, 3)):\n    arch = architecture.lower()\n    if arch == \"vgg16\":\n        base = tf.keras.applications.VGG16(include_top=False, weights=\"imagenet\", input_shape=input_shape)\n        preprocess_fn = tf.keras.applications.vgg16.preprocess_input\n    elif arch == \"resnet50\":\n        base = tf.keras.applications.ResNet50(include_top=False, weights=\"imagenet\", input_shape=input_shape)\n        preprocess_fn = tf.keras.applications.resnet50.preprocess_input\n    elif arch == \"mobilenetv2\":\n        base = tf.keras.applications.MobileNetV2(include_top=False, weights=\"imagenet\", input_shape=input_shape)\n        preprocess_fn = tf.keras.applications.mobilenet_v2.preprocess_input\n    elif arch == \"inceptionv3\":\n        input_shape   = (299, 299, 3)\n        base = tf.keras.applications.InceptionV3(include_top=False, weights=\"imagenet\", input_shape=input_shape)\n        preprocess_fn = tf.keras.applications.inception_v3.preprocess_input\n    else:\n        raise ValueError(f\"Unsupported architecture: {architecture}\")\n    return base, preprocess_fn\n\n\ndef truncate_backbone(base_model, cut_layer_name):\n    try:\n        output    = base_model.get_layer(cut_layer_name).output\n        truncated = tf.keras.Model(\n            inputs=base_model.input, outputs=output,\n            name=f\"{base_model.name}_truncated\"\n        )\n        return truncated\n    except Exception:\n        return base_model\n\n\ndef build_classifier_head(x, head_cfg):\n    x = tf.keras.layers.GlobalAveragePooling2D()(x)\n    for units, drop in zip(head_cfg[\"dense_units\"], head_cfg[\"dropout\"]):\n        x = tf.keras.layers.Dense(units, activation=\"relu\")(x)\n        x = tf.keras.layers.BatchNormalization()(x)\n        x = tf.keras.layers.Dropout(drop)(x)\n    return tf.keras.layers.Dense(1, activation=\"sigmoid\", dtype=\"float32\")(x)\n\n\ndef apply_freezing_policy(model, freeze_ratio: float):\n    total_layers  = len(model.layers)\n    freeze_until  = int(total_layers * freeze_ratio)\n    for i, layer in enumerate(model.layers):\n        layer.trainable = i >= freeze_until\n    return freeze_until\n\n\ndef build_amca_model(architecture: str, strategy: Dict, input_size: int):\n    input_shape    = (input_size, input_size, 3)\n    base_model, preprocess_fn = get_backbone_and_preprocess(architecture, input_shape=input_shape)\n    if architecture.lower() == \"inceptionv3\":\n        input_shape = (299, 299, 3)\n\n    cut_layer          = strategy[\"cutting_point\"]\n    truncated_backbone = truncate_backbone(base_model, cut_layer)\n    freeze_ratio       = strategy[\"freezing_strategy\"][\"freeze_ratio\"]\n    freeze_until       = apply_freezing_policy(truncated_backbone, freeze_ratio)\n\n    inputs  = tf.keras.Input(shape=input_shape)\n    x       = preprocess_fn(inputs)\n    x       = truncated_backbone(x, training=False)\n    outputs = build_classifier_head(x, strategy[\"head_architecture\"])\n\n    model = tf.keras.Model(inputs, outputs, name=f\"ReducedAMCA_{architecture}\")\n\n    info = {\n        \"cut_layer\"              : cut_layer,\n        \"freeze_ratio\"           : freeze_ratio,\n        \"freeze_until_index\"     : freeze_until,\n        \"total_backbone_layers\"  : len(truncated_backbone.layers),\n        \"trainable_backbone_layers\": sum(int(l.trainable) for l in truncated_backbone.layers)\n    }\n    return model, truncated_backbone, info","metadata":{},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# FLOPs  &  Size per layer\n# ============================================================\n\ndef get_layer_flops_conv(layer, input_shape):\n    \"\"\"Estimate MACs for a Conv2D layer (1 MAC = 2 FLOPs).\"\"\"\n    cfg    = layer.get_config()\n    kH, kW = cfg[\"kernel_size\"]\n    Ci     = input_shape[-1]\n    Co     = cfg[\"filters\"]\n    # output spatial dims\n    oH = input_shape[0] // cfg[\"strides\"][0]\n    oW = input_shape[1] // cfg[\"strides\"][1]\n    macs = kH * kW * Ci * Co * oH * oW\n    return 2 * macs          # 1 MAC = 2 FLOPs\n\n\ndef get_layer_flops_dense(layer, input_units):\n    cfg   = layer.get_config()\n    units = cfg[\"units\"]\n    return 2 * input_units * units\n\n\ndef compute_model_stats(model) -> pd.DataFrame:\n    \"\"\"\n    Returns a DataFrame with one row per layer:\n      layer_name | type | params | param_size_KB | flops | trainable\n    \"\"\"\n    rows = []\n    for layer in model.layers:\n        ltype       = type(layer).__name__\n        params      = layer.count_params()\n        size_kb     = params * 4 / 1024          # float32 = 4 bytes\n        trainable   = layer.trainable\n\n        # FLOPs estimation\n        flops = 0\n        try:\n            in_shape = layer.input_shape\n            if isinstance(in_shape, list):\n                in_shape = in_shape[0]\n            in_shape = in_shape[1:]   # strip batch\n\n            if isinstance(layer, tf.keras.layers.Conv2D):\n                flops = get_layer_flops_conv(layer, in_shape)\n            elif isinstance(layer, tf.keras.layers.Dense):\n                flops = get_layer_flops_dense(layer, in_shape[-1])\n            elif isinstance(layer, tf.keras.layers.DepthwiseConv2D):\n                cfg  = layer.get_config()\n                kH, kW = cfg[\"kernel_size\"]\n                C    = in_shape[-1]\n                oH   = in_shape[0] // cfg[\"strides\"][0]\n                oW   = in_shape[1] // cfg[\"strides\"][1]\n                flops = 2 * kH * kW * C * oH * oW\n        except Exception:\n            pass\n\n        rows.append({\n            \"layer_name\"    : layer.name,\n            \"type\"          : ltype,\n            \"params\"        : params,\n            \"size_KB\"       : round(size_kb, 2),\n            \"flops\"         : flops,\n            \"trainable\"     : trainable\n        })\n\n    return pd.DataFrame(rows)\n\n\ndef print_model_stats(df: pd.DataFrame, model_name: str = \"Model\"):\n    total_params  = df[\"params\"].sum()\n    total_size_mb = total_params * 4 / (1024 ** 2)\n    total_flops   = df[\"flops\"].sum()\n    trainable_p   = df[df[\"trainable\"]][\"params\"].sum()\n    frozen_p      = total_params - trainable_p\n\n    print(f\"\\n{'='*64}\")\n    print(f\"  {model_name} — Parameter & FLOPs Summary\")\n    print(f\"{'='*64}\")\n    print(f\"  Total parameters   : {total_params:>15,}\")\n    print(f\"  Trainable params   : {trainable_p:>15,}\")\n    print(f\"  Frozen params      : {frozen_p:>15,}\")\n    print(f\"  Model size (FP32)  : {total_size_mb:>14.2f} MB\")\n    print(f\"  Total FLOPs (est.) : {total_flops:>15,}\")\n    print(f\"  Total GFLOPs       : {total_flops/1e9:>14.3f} GFLOPs\")\n    print(f\"{'='*64}\")\n\n    # Per-layer table (top 20 by FLOPs)\n    df_show = df[df[\"params\"] > 0].copy()\n    df_show = df_show.sort_values(\"flops\", ascending=False).head(20)\n    df_show[\"flops_M\"]    = (df_show[\"flops\"] / 1e6).round(3)\n    df_show[\"size_KB\"]    = df_show[\"size_KB\"].apply(lambda x: f\"{x:.1f} KB\")\n    df_show[\"trainable\"]  = df_show[\"trainable\"].map({True: \"YES\", False: \"frozen\"})\n\n    print(tabulate(\n        df_show[[\"layer_name\", \"type\", \"params\", \"size_KB\", \"flops_M\", \"trainable\"]],\n        headers=[\"Layer\", \"Type\", \"Params\", \"Size\", \"MFLOPs\", \"Trainable\"],\n        tablefmt=\"fancy_grid\",\n        showindex=False\n    ))\n    return total_params, total_size_mb, total_flops","metadata":{},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# Compile / callbacks / fine-tuning helpers\n# ============================================================\n\ndef get_metrics():\n    return [\n        tf.keras.metrics.BinaryAccuracy(name=\"accuracy\"),\n        tf.keras.metrics.AUC(name=\"auc\"),\n        tf.keras.metrics.Precision(name=\"precision\"),\n        tf.keras.metrics.Recall(name=\"recall\")\n    ]\n\n\ndef compile_model(model, lr):\n    model.compile(\n        optimizer=tf.keras.optimizers.Adam(learning_rate=lr),\n        loss=tf.keras.losses.BinaryCrossentropy(),\n        metrics=get_metrics()\n    )\n    return model\n\n\ndef get_callbacks(model_name=\"reduced_amca_vgg16\"):\n    return [\n        tf.keras.callbacks.ModelCheckpoint(\n            f\"{model_name}.keras\", monitor=\"val_auc\", mode=\"max\",\n            save_best_only=True, verbose=1\n        ),\n        tf.keras.callbacks.EarlyStopping(\n            monitor=\"val_auc\", mode=\"max\", patience=5,\n            restore_best_weights=True, verbose=1\n        ),\n        tf.keras.callbacks.ReduceLROnPlateau(\n            monitor=\"val_loss\", factor=0.2, patience=2,\n            min_lr=1e-7, verbose=1\n        )\n    ]\n\n\ndef unfreeze_top_percent(backbone_model, percent: float):\n    total = len(backbone_model.layers)\n    start = int(total * (1.0 - percent))\n    for i, layer in enumerate(backbone_model.layers):\n        layer.trainable = i >= start\n    return start\n\n\ndef unfreeze_all(backbone_model):\n    for layer in backbone_model.layers:\n        layer.trainable = True","metadata":{},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# Evaluation\n# ============================================================\n\ndef evaluate_model(model, val_ds, val_df):\n    preds  = model.predict(val_ds, verbose=1).ravel()\n    y_true = val_df[\"label\"].values\n    y_pred = (preds >= 0.5).astype(int)\n\n    print(\"\\nClassification Report:\")\n    print(classification_report(y_true, y_pred, digits=4))\n    print(\"Confusion Matrix:\")\n    print(confusion_matrix(y_true, y_pred))\n\n    try:\n        auc = roc_auc_score(y_true, preds)\n        print(f\"ROC AUC: {auc:.4f}\")\n    except Exception:\n        pass\n\n    return preds, y_pred, y_true","metadata":{},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# Graphs\n# ============================================================\n\nCOLORS = {\n    \"train\" : \"#2563EB\",\n    \"val\"   : \"#DC2626\",\n    \"phase\" : \"#6B7280\",\n    \"flops\" : \"#7C3AED\",\n    \"size\"  : \"#0891B2\",\n    \"frozen\": \"#9CA3AF\",\n    \"train_bar\": \"#2563EB\",\n    \"pos\"   : \"#16A34A\",\n    \"neg\"   : \"#DC2626\",\n}\n\n\ndef plot_learning_curves(history_objects):\n    \"\"\"Accuracy, Loss, AUC, Precision/Recall — one figure, 4 subplots.\"\"\"\n    if not isinstance(history_objects, list):\n        history_objects = [history_objects]\n\n    acc, val_acc = [], []\n    loss, val_loss = [], []\n    auc, val_auc = [], []\n    prec, val_prec = [], []\n    rec, val_rec = [], []\n\n    phase_boundaries = []\n    cumulative = 0\n    for hist in history_objects:\n        h = hist.history\n        acc.extend(h.get(\"accuracy\", h.get(\"acc\", [])))\n        val_acc.extend(h.get(\"val_accuracy\", h.get(\"val_acc\", [])))\n        loss.extend(h[\"loss\"])\n        val_loss.extend(h[\"val_loss\"])\n        auc.extend(h.get(\"auc\", []))\n        val_auc.extend(h.get(\"val_auc\", []))\n        prec.extend(h.get(\"precision\", []))\n        val_prec.extend(h.get(\"val_precision\", []))\n        rec.extend(h.get(\"recall\", []))\n        val_rec.extend(h.get(\"val_recall\", []))\n        cumulative += len(h[\"loss\"])\n        phase_boundaries.append(cumulative)\n    phase_boundaries = phase_boundaries[:-1]\n    epochs = range(1, len(loss) + 1)\n\n    fig, axes = plt.subplots(2, 2, figsize=(14, 10))\n    fig.suptitle(\"Reduced AMCA — VGG16 — Learning Curves\", fontsize=15, fontweight=\"bold\")\n\n    def _plot(ax, train_vals, val_vals, ylabel, title):\n        ax.plot(epochs, train_vals, color=COLORS[\"train\"], lw=2, label=\"Train\")\n        ax.plot(epochs, val_vals,   color=COLORS[\"val\"],   lw=2, label=\"Val\",   linestyle=\"--\")\n        for i, b in enumerate(phase_boundaries):\n            ax.axvline(x=b + 0.5, color=COLORS[\"phase\"], linestyle=\":\", lw=1.2,\n                       label=f\"Phase {i+2}\" if i == 0 else \"_\")\n        ax.set_title(title, fontweight=\"bold\")\n        ax.set_xlabel(\"Epoch\")\n        ax.set_ylabel(ylabel)\n        ax.legend(fontsize=9)\n        ax.grid(True, alpha=0.3)\n        ax.set_xlim(1, len(loss))\n\n    _plot(axes[0,0], acc,  val_acc,  \"Accuracy\",  \"Accuracy\")\n    _plot(axes[0,1], loss, val_loss, \"Loss\",       \"Loss (BinaryCrossentropy)\")\n    if auc:\n        _plot(axes[1,0], auc, val_auc, \"AUC\", \"AUC (ROC)\")\n    else:\n        axes[1,0].set_visible(False)\n\n    if prec and rec:\n        axes[1,1].plot(epochs, prec,     color=\"#7C3AED\", lw=2, label=\"Train Prec\")\n        axes[1,1].plot(epochs, val_prec, color=\"#7C3AED\", lw=2, label=\"Val Prec\",   linestyle=\"--\", alpha=0.7)\n        axes[1,1].plot(epochs, rec,      color=\"#0891B2\", lw=2, label=\"Train Rec\")\n        axes[1,1].plot(epochs, val_rec,  color=\"#0891B2\", lw=2, label=\"Val Rec\",    linestyle=\"--\", alpha=0.7)\n        for b in phase_boundaries:\n            axes[1,1].axvline(x=b + 0.5, color=COLORS[\"phase\"], linestyle=\":\", lw=1.2)\n        axes[1,1].set_title(\"Precision & Recall\", fontweight=\"bold\")\n        axes[1,1].set_xlabel(\"Epoch\")\n        axes[1,1].legend(fontsize=9)\n        axes[1,1].grid(True, alpha=0.3)\n        axes[1,1].set_xlim(1, len(loss))\n\n    plt.tight_layout()\n    plt.savefig(\"learning_curves_vgg16.png\", dpi=150, bbox_inches=\"tight\")\n    plt.show()\n    print(\"[Saved] learning_curves_vgg16.png\")\n\n\ndef plot_layer_stats(df_stats: pd.DataFrame):\n    \"\"\"Two horizontal bar charts: FLOPs per layer + Size per layer.\"\"\"\n    df_plot = df_stats[df_stats[\"params\"] > 0].copy()\n    df_plot[\"flops_M\"]  = df_plot[\"flops\"]   / 1e6\n    df_plot[\"size_MB\"]  = df_plot[\"size_KB\"]  / 1024\n    df_plot[\"color\"]    = df_plot[\"trainable\"].map({True: COLORS[\"train_bar\"], False: COLORS[\"frozen\"]})\n\n    # Keep top 20 by FLOPs\n    df_top = df_plot.sort_values(\"flops_M\", ascending=True).tail(20)\n\n    fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(16, 8))\n    fig.suptitle(\"VGG16 Backbone — Per-Layer Stats\", fontsize=14, fontweight=\"bold\")\n\n    # FLOPs\n    bars = ax1.barh(df_top[\"layer_name\"], df_top[\"flops_M\"],\n                    color=df_top[\"color\"], edgecolor=\"white\", height=0.7)\n    ax1.set_xlabel(\"MFLOPs\", fontsize=10)\n    ax1.set_title(\"FLOPs per layer (top 20)\", fontweight=\"bold\")\n    ax1.grid(axis=\"x\", alpha=0.3)\n    ax1.tick_params(axis=\"y\", labelsize=8)\n    for bar, val in zip(bars, df_top[\"flops_M\"]):\n        if val > 0:\n            ax1.text(bar.get_width() * 1.01, bar.get_y() + bar.get_height()/2,\n                     f\"{val:.0f}\", va=\"center\", fontsize=7)\n\n    # Size\n    df_top_s = df_plot.sort_values(\"size_MB\", ascending=True).tail(20)\n    bars2 = ax2.barh(df_top_s[\"layer_name\"], df_top_s[\"size_MB\"],\n                     color=df_top_s[\"color\"], edgecolor=\"white\", height=0.7)\n    ax2.set_xlabel(\"Size (MB)\", fontsize=10)\n    ax2.set_title(\"Parameter size per layer (top 20)\", fontweight=\"bold\")\n    ax2.grid(axis=\"x\", alpha=0.3)\n    ax2.tick_params(axis=\"y\", labelsize=8)\n    for bar, val in zip(bars2, df_top_s[\"size_MB\"]):\n        if val > 0:\n            ax2.text(bar.get_width() * 1.01, bar.get_y() + bar.get_height()/2,\n                     f\"{val:.2f}\", va=\"center\", fontsize=7)\n\n    # Legend\n    from matplotlib.patches import Patch\n    legend_elements = [\n        Patch(facecolor=COLORS[\"train_bar\"], label=\"Trainable\"),\n        Patch(facecolor=COLORS[\"frozen\"],    label=\"Frozen\")\n    ]\n    fig.legend(handles=legend_elements, loc=\"lower center\", ncol=2, fontsize=10, frameon=True)\n\n    plt.tight_layout(rect=[0, 0.04, 1, 1])\n    plt.savefig(\"layer_stats_vgg16.png\", dpi=150, bbox_inches=\"tight\")\n    plt.show()\n    print(\"[Saved] layer_stats_vgg16.png\")\n\n\ndef plot_flops_pie(df_stats: pd.DataFrame):\n    \"\"\"Pie chart: FLOPs distribution by layer type.\"\"\"\n    df_plot = df_stats[df_stats[\"flops\"] > 0].copy()\n    by_type = df_plot.groupby(\"type\")[\"flops\"].sum().sort_values(ascending=False)\n    if by_type.empty:\n        return\n\n    top_n = 6\n    if len(by_type) > top_n:\n        top    = by_type.iloc[:top_n]\n        others = pd.Series({\"Others\": by_type.iloc[top_n:].sum()})\n        by_type = pd.concat([top, others])\n\n    fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(14, 6))\n    fig.suptitle(\"VGG16 — FLOPs & Size Distribution\", fontsize=13, fontweight=\"bold\")\n\n    palette = [\"#2563EB\",\"#7C3AED\",\"#0891B2\",\"#16A34A\",\"#DC2626\",\"#F59E0B\",\"#9CA3AF\"]\n\n    ax1.pie(\n        by_type.values,\n        labels=by_type.index,\n        colors=palette[:len(by_type)],\n        autopct=\"%1.1f%%\",\n        startangle=140,\n        pctdistance=0.80,\n        textprops={\"fontsize\": 9}\n    )\n    ax1.set_title(\"FLOPs by layer type\", fontweight=\"bold\")\n\n    # Trainable vs frozen params bar\n    trainable_p = df_stats[df_stats[\"trainable\"]][\"params\"].sum()\n    frozen_p    = df_stats[~df_stats[\"trainable\"]][\"params\"].sum()\n    total_p     = trainable_p + frozen_p\n\n    cats   = [\"Trainable\", \"Frozen\"]\n    vals   = [trainable_p / total_p * 100, frozen_p / total_p * 100]\n    colors = [COLORS[\"train_bar\"], COLORS[\"frozen\"]]\n    bars   = ax2.bar(cats, vals, color=colors, width=0.4, edgecolor=\"white\")\n    ax2.set_ylim(0, 110)\n    ax2.set_ylabel(\"% of total parameters\")\n    ax2.set_title(\"Trainable vs Frozen parameters\", fontweight=\"bold\")\n    ax2.grid(axis=\"y\", alpha=0.3)\n    for bar, val in zip(bars, vals):\n        ax2.text(bar.get_x() + bar.get_width()/2, val + 1.5,\n                 f\"{val:.1f}%\", ha=\"center\", fontweight=\"bold\", fontsize=11)\n\n    plt.tight_layout()\n    plt.savefig(\"flops_distribution_vgg16.png\", dpi=150, bbox_inches=\"tight\")\n    plt.show()\n    print(\"[Saved] flops_distribution_vgg16.png\")\n\n\ndef plot_confusion_and_roc(preds, y_true):\n    \"\"\"Confusion matrix + ROC + Precision-Recall curves.\"\"\"\n    y_pred = (preds >= 0.5).astype(int)\n    cm     = confusion_matrix(y_true, y_pred)\n\n    fig, (ax1, ax2, ax3) = plt.subplots(1, 3, figsize=(18, 5))\n    fig.suptitle(\"Reduced AMCA — VGG16 — Evaluation\", fontsize=14, fontweight=\"bold\")\n\n    # Confusion matrix\n    sns.heatmap(cm, annot=True, fmt=\"d\", cmap=\"Blues\", ax=ax1,\n                xticklabels=[\"No DR\", \"DR\"], yticklabels=[\"No DR\", \"DR\"],\n                linewidths=0.5, linecolor=\"white\", cbar=False,\n                annot_kws={\"size\": 14, \"weight\": \"bold\"})\n    ax1.set_xlabel(\"Predicted\", fontsize=11)\n    ax1.set_ylabel(\"Actual\",    fontsize=11)\n    ax1.set_title(\"Confusion Matrix\", fontweight=\"bold\")\n\n    # ROC\n    fpr, tpr, _ = roc_curve(y_true, preds)\n    auc_val     = roc_auc_score(y_true, preds)\n    ax2.plot(fpr, tpr, color=COLORS[\"train\"], lw=2, label=f\"AUC = {auc_val:.4f}\")\n    ax2.plot([0,1],[0,1], color=COLORS[\"phase\"], lw=1, linestyle=\"--\")\n    ax2.fill_between(fpr, tpr, alpha=0.07, color=COLORS[\"train\"])\n    ax2.set_xlabel(\"False Positive Rate\");  ax2.set_ylabel(\"True Positive Rate\")\n    ax2.set_title(\"ROC Curve\", fontweight=\"bold\")\n    ax2.legend(fontsize=10);  ax2.grid(True, alpha=0.3)\n\n    # Precision-Recall\n    prec_c, rec_c, _ = precision_recall_curve(y_true, preds)\n    ax3.plot(rec_c, prec_c, color=COLORS[\"flops\"], lw=2)\n    ax3.fill_between(rec_c, prec_c, alpha=0.07, color=COLORS[\"flops\"])\n    ax3.set_xlabel(\"Recall\");  ax3.set_ylabel(\"Precision\")\n    ax3.set_title(\"Precision-Recall Curve\", fontweight=\"bold\")\n    ax3.grid(True, alpha=0.3)\n\n    plt.tight_layout()\n    plt.savefig(\"evaluation_vgg16.png\", dpi=150, bbox_inches=\"tight\")\n    plt.show()\n    print(\"[Saved] evaluation_vgg16.png\")","metadata":{},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# Main\n# ============================================================\n\ndef main():\n\n    # ── 1. Data ──────────────────────────────────────────────\n    print(\"Loading dataframe...\")\n    df = load_dataframe(CSV_PATH, IMAGE_DIR)\n    print(f\"Total samples: {len(df)}\")\n    print(df[\"label\"].value_counts())\n\n    train_df, val_df = split_dataframe(df)\n    print(f\"Train: {len(train_df)}  |  Val: {len(val_df)}\")\n\n    input_size = 299 if ARCHITECTURE.lower() == \"inceptionv3\" else IMG_SIZE\n\n    train_ds = build_dataset(train_df, img_size=input_size, batch_size=BATCH_SIZE, training=True)\n    val_ds   = build_dataset(val_df,   img_size=input_size, batch_size=BATCH_SIZE, training=False)\n\n    # ── 2. AMCA strategy ─────────────────────────────────────\n    problem_analysis = {\n        \"num_classes\"             : 2,\n        \"intra_class_variability\" : \"medium\",\n        \"task_type\"               : TASK_TYPE\n    }\n    constraints  = {\"compute_budget\": COMPUTE_BUDGET,    \"memory_budget\": \"medium\"}\n    requirements = {\"target_performance\": TARGET_PERFORMANCE}\n\n    amca     = ReducedAutomatedModelCuttingAlgorithm(problem_analysis, constraints, requirements)\n    strategy = amca.determine_cutting_strategy(ARCHITECTURE)\n\n    print(\"\\n=== Reduced AMCA Strategy ===\")\n    for k, v in strategy.items():\n        print(f\"  {k:28s}: {v}\")\n\n    # ── 3. Build model ───────────────────────────────────────\n    model, backbone, model_info = build_amca_model(\n        architecture=ARCHITECTURE, strategy=strategy, input_size=input_size\n    )\n    print(\"\\n=== Model Info ===\")\n    for k, v in model_info.items():\n        print(f\"  {k:35s}: {v}\")\n\n    model.summary()\n\n    # ── 4. FLOPs + size stats ────────────────────────────────\n    df_stats = compute_model_stats(model)\n    total_params, total_size_mb, total_flops = print_model_stats(df_stats, model_name=f\"Reduced AMCA ({ARCHITECTURE})\")\n\n    # ── 5. Layer stats graphs ─────────────────────────────────\n    plot_layer_stats(df_stats)\n    plot_flops_pie(df_stats)\n\n    # ── 6. Class weights ─────────────────────────────────────\n    class_weight = None\n    if USE_CLASS_WEIGHTS:\n        classes      = np.array(sorted(train_df[\"label\"].unique()))\n        weights      = compute_class_weight(\"balanced\", classes=classes, y=train_df[\"label\"].values)\n        class_weight = {int(c): float(w) for c, w in zip(classes, weights)}\n        print(\"\\nClass weights:\", class_weight)\n\n    lr_cfg    = strategy[\"learning_rate_strategy\"]\n    phases    = strategy[\"fine_tuning_plan\"][\"phases\"]\n    callbacks = get_callbacks(\"reduced_amca_vgg16\")\n    histories = []\n\n    # ── 7. Phase 1  (frozen backbone, train head) ─────────────\n    print(\"\\n===== Phase 1 — Train head only =====\")\n    model = compile_model(model, lr=lr_cfg[\"phase1_lr\"])\n    history1 = model.fit(\n        train_ds, validation_data=val_ds,\n        epochs=EPOCHS_PHASE1,\n        class_weight=class_weight,\n        callbacks=callbacks,\n        verbose=1\n    )\n    histories.append(history1)\n\n    # ── 8. Phase 2  (unfreeze top 50%) ───────────────────────\n    if phases >= 2:\n        print(\"\\n===== Phase 2 — Unfreeze top 50% =====\")\n        unfreeze_top_percent(backbone, 0.5)\n        model = compile_model(model, lr=lr_cfg[\"phase2_lr\"])\n        history2 = model.fit(\n            train_ds, validation_data=val_ds,\n            epochs=EPOCHS_PHASE2,\n            class_weight=class_weight,\n            callbacks=callbacks,\n            verbose=1\n        )\n        histories.append(history2)\n\n    # ── 9. Phase 3  (full fine-tuning) ────────────────────────\n    if phases >= 3:\n        print(\"\\n===== Phase 3 — Full fine-tuning =====\")\n        unfreeze_all(backbone)\n        model = compile_model(model, lr=lr_cfg[\"phase3_lr\"])\n        history3 = model.fit(\n            train_ds, validation_data=val_ds,\n            epochs=EPOCHS_PHASE3,\n            class_weight=class_weight,\n            callbacks=callbacks,\n            verbose=1\n        )\n        histories.append(history3)\n\n    # ── 10. Learning curves ───────────────────────────────────\n    print(\"\\n===== Learning Curves =====\")\n    plot_learning_curves(histories)\n\n    # ── 11. Evaluation ───────────────────────────────────────\n    print(\"\\n===== Final Evaluation =====\")\n    preds, y_pred, y_true = evaluate_model(model, val_ds, val_df)\n    plot_confusion_and_roc(preds, y_true)\n\n    # ── 12. Final summary ────────────────────────────────────\n    print(\"\\n\" + \"=\"*64)\n    print(\"  FINAL SUMMARY\")\n    print(\"=\"*64)\n    print(f\"  Architecture    : {ARCHITECTURE.upper()}\")\n    print(f\"  Cutting point   : {strategy['cutting_point']}\")\n    print(f\"  Freezing        : {strategy['freezing_strategy']['description']}\")\n    print(f\"  Phases          : {phases}\")\n    print(f\"  Total params    : {total_params:,}\")\n    print(f\"  Model size      : {total_size_mb:.2f} MB\")\n    print(f\"  Total GFLOPs    : {total_flops/1e9:.3f}\")\n    print(f\"  ROC AUC         : {roc_auc_score(y_true, preds):.4f}\")\n    acc_final = (y_pred == y_true).mean()\n    print(f\"  Final Accuracy  : {acc_final:.4f}\")\n    print(\"=\"*64)\n    print(\"Training complete.\")\n\n\nif __name__ == \"__main__\":\n    main()","metadata":{},"outputs":[],"execution_count":null}]}