{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.12.12","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"nvidiaTeslaT4","dataSources":[{"sourceType":"competition","sourceId":19991,"databundleVersionId":1117522},{"sourceType":"datasetVersion","sourceId":16110399,"datasetId":10193999,"databundleVersionId":17082239},{"sourceType":"datasetVersion","sourceId":16296222,"datasetId":10447468,"databundleVersionId":17282811}],"dockerImageVersionId":31329,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"!pip install -q efficientnet_pytorch albumentations xgboost scikit-learn tqdm gradio kaggle\nprint(\"All libraries installed!\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-16T11:36:09.559223Z","iopub.execute_input":"2026-05-16T11:36:09.559475Z","iopub.status.idle":"2026-05-16T11:36:17.649056Z","shell.execute_reply.started":"2026-05-16T11:36:09.559453Z","shell.execute_reply":"2026-05-16T11:36:17.648248Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nimport random\nimport time\nimport numpy as np\nimport pandas as pd\nfrom glob import glob\nfrom datetime import datetime\nimport subprocess\n\nimport cv2\nimport torch\nimport torch.nn as nn\nfrom torch.utils.data import Dataset, DataLoader\nfrom torchvision import transforms\n\nimport albumentations as A\nfrom albumentations.pytorch.transforms import ToTensorV2\n\nfrom efficientnet_pytorch import EfficientNet\nimport xgboost as xgb\nfrom sklearn.model_selection import GroupKFold\nfrom sklearn.metrics import (accuracy_score, classification_report,\n                             roc_auc_score, confusion_matrix)\nfrom tqdm import tqdm\nimport matplotlib.pyplot as plt\nimport seaborn as sns\nimport warnings\nwarnings.filterwarnings(\"ignore\")\n\n# Fix randomness\nSEED = 42\nrandom.seed(SEED)\nnp.random.seed(SEED)\ntorch.manual_seed(SEED)\ntorch.cuda.manual_seed(SEED)\ntorch.backends.cudnn.deterministic = True\ntorch.backends.cudnn.benchmark = True\n\ndevice = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\nprint(f\"Device: {device}\")\nprint(\"All imports successful!\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-16T11:36:19.396065Z","iopub.execute_input":"2026-05-16T11:36:19.396525Z","iopub.status.idle":"2026-05-16T11:36:28.511799Z","shell.execute_reply.started":"2026-05-16T11:36:19.396492Z","shell.execute_reply":"2026-05-16T11:36:28.511094Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"DATA_ROOT = '/kaggle/input/competitions/alaska2-image-steganalysis'\n\n# 10,000 images per class exactly like the example notebook\ndataset = []\n\nfor label, kind in enumerate(['Cover', 'JMiPOD', 'JUNIWARD', 'UERD']):\n    paths = glob(f'{DATA_ROOT}/{kind}/*.jpg')[:10000]\n    for path in paths:\n        dataset.append({\n            'kind': kind,\n            'image_name': path.split('/')[-1],\n            'label': label\n        })\n\nrandom.shuffle(dataset)\ndataset = pd.DataFrame(dataset)\n\n# GroupKFold\ngkf = GroupKFold(n_splits=5)\ndataset['fold'] = 0\nfor fold_number, (train_index, val_index) in enumerate(\n    gkf.split(X=dataset.index, y=dataset['label'], groups=dataset['image_name'])\n):\n    dataset.loc[dataset.iloc[val_index].index, 'fold'] = fold_number\n\nfold_number = 0\ntrain_df = dataset[dataset['fold'] != fold_number].reset_index(drop=True)\nval_df   = dataset[dataset['fold'] == fold_number].reset_index(drop=True)\n\nprint(f\"Total samples: {len(dataset)}\")\nprint(f\"Train samples: {len(train_df)}\")\nprint(f\"Val samples:   {len(val_df)}\")\nprint(\"Data preparation done!\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-16T11:36:30.931229Z","iopub.execute_input":"2026-05-16T11:36:30.932304Z","iopub.status.idle":"2026-05-16T11:36:37.032934Z","shell.execute_reply.started":"2026-05-16T11:36:30.932273Z","shell.execute_reply":"2026-05-16T11:36:37.032263Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"SRM_FILTERS = np.array([\n    [[0,0,0,0,0],[0,0,0,0,0],[0,0,-1,1,0],[0,0,0,0,0],[0,0,0,0,0]],\n    [[0,0,0,0,0],[0,0,1,0,0],[0,0,-1,0,0],[0,0,0,0,0],[0,0,0,0,0]],\n    [[0,0,0,0,0],[0,-1,0,0,0],[0,0,1,0,0],[0,0,0,0,0],[0,0,0,0,0]],\n], dtype=np.float32)\n\ndef apply_srm_filters(image):\n    gray = cv2.cvtColor((image * 255).astype(np.uint8),\n                        cv2.COLOR_RGB2GRAY).astype(np.float32)\n    residuals = []\n    for f in SRM_FILTERS:\n        filtered = cv2.filter2D(gray, -1, f)\n        residuals.append(filtered)\n    result = np.stack(residuals, axis=-1)\n    result = np.clip(result + 128, 0, 255) / 255.0\n    return result.astype(np.float32)\n\n# Quick test\ntest_img = np.random.rand(512, 512, 3).astype(np.float32)\ntest_out = apply_srm_filters(test_img)\nprint(f\"Input shape:  {test_img.shape}\")\nprint(f\"Output shape: {test_out.shape}\")\nprint(f\"Output range: {test_out.min():.3f} to {test_out.max():.3f}\")\nprint(\"SRM filters ready!\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-16T11:36:40.600410Z","iopub.execute_input":"2026-05-16T11:36:40.600706Z","iopub.status.idle":"2026-05-16T11:36:40.679361Z","shell.execute_reply.started":"2026-05-16T11:36:40.600682Z","shell.execute_reply":"2026-05-16T11:36:40.678724Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"class ALASKA2Dataset(Dataset):\n    def __init__(self, df, transform=None):\n        self.df = df.reset_index(drop=True)\n        self.transform = transform\n\n    def __len__(self):\n        return len(self.df)\n\n    def __getitem__(self, idx):\n        row = self.df.iloc[idx]\n        image_path = f\"{DATA_ROOT}/{row['kind']}/{row['image_name']}\"\n    \n        try:\n            image = cv2.imread(image_path, cv2.IMREAD_COLOR)\n            image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB).astype(np.float32)\n            image /= 255.0\n            image = apply_srm_filters(image)\n        except:\n            # If image is corrupted, return a blank image\n            image = np.zeros((512, 512, 3), dtype=np.float32)\n    \n        if self.transform:\n            sample = self.transform(image=image)\n            image = sample['image']\n    \n        label = row['label']\n        return image, label\n        \n    def get_labels(self):\n        return list(self.df['label'].values)\n\ndef get_train_transforms():\n    return A.Compose([\n        A.HorizontalFlip(p=0.5),\n        A.VerticalFlip(p=0.5),\n        A.RandomRotate90(p=0.5),\n        A.Resize(height=512, width=512, p=1.0),\n        ToTensorV2(p=1.0),\n    ])\n\ndef get_val_transforms():\n    return A.Compose([\n        A.Resize(height=512, width=512, p=1.0),\n        ToTensorV2(p=1.0),\n    ])\n\n# Quick test\ntest_dataset = ALASKA2Dataset(train_df.head(4), transform=get_val_transforms())\nimg, label = test_dataset[0]\nprint(f\"Image tensor shape: {img.shape}\")\nprint(f\"Label: {label} (0=Cover, 1=JMiPOD, 2=JUNIWARD, 3=UERD)\")\nprint(f\"Pixel range: {img.min():.3f} to {img.max():.3f}\")\nprint(\"Dataset class ready!\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-16T11:36:44.395391Z","iopub.execute_input":"2026-05-16T11:36:44.396186Z","iopub.status.idle":"2026-05-16T11:36:44.572233Z","shell.execute_reply.started":"2026-05-16T11:36:44.396144Z","shell.execute_reply":"2026-05-16T11:36:44.571320Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"class LabelSmoothing(nn.Module):\n    def __init__(self, smoothing=0.05):\n        super().__init__()\n        self.confidence = 1.0 - smoothing\n        self.smoothing = smoothing\n\n    def forward(self, x, target):\n        if self.training:\n            x = x.float()\n            n_classes = x.size(1)\n            one_hot = torch.zeros_like(x).scatter_(1, target.unsqueeze(1), 1)\n            smooth_one_hot = one_hot * self.confidence + \\\n                            (1 - one_hot) * self.smoothing / (n_classes - 1)\n            log_prob = torch.nn.functional.log_softmax(x, dim=-1)\n            loss = -(smooth_one_hot * log_prob).sum(dim=-1)\n            return loss.mean()\n        else:\n            return torch.nn.functional.cross_entropy(x, target)\n\nclass StegoDetector(nn.Module):\n    def __init__(self):\n        super().__init__()\n        self.base = EfficientNet.from_pretrained('efficientnet-b2')\n        self.base._fc = nn.Sequential(\n            nn.Dropout(p=0.3),\n            nn.Linear(1408, 4)\n        )\n\n    def forward(self, x):\n        return self.base(x)\n\n    def get_features(self, x):\n        \"\"\"Returns 1408-dim feature vector for XGBoost\"\"\"\n        x = self.base.extract_features(x)\n        x = self.base._avg_pooling(x)\n        x = x.flatten(start_dim=1)\n        x = self.base._dropout(x)\n        return x\n\n# Test\nmodel = StegoDetector()\ndummy = torch.randn(2, 3, 512, 512)\nout = model(dummy)\nfeatures = model.get_features(dummy)\nprint(f\"Output shape:   {out.shape}  ← 4 class scores\")\nprint(f\"Features shape: {features.shape}  ← 1408 values for XGBoost\")\ncriterion = LabelSmoothing(smoothing=0.05)\ndummy_labels = torch.tensor([0, 1])\nloss = criterion(out, dummy_labels)\nprint(f\"Test loss: {loss.item():.4f}\")\nprint(\"Model and Label Smoothing ready!\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-16T11:36:46.826246Z","iopub.execute_input":"2026-05-16T11:36:46.827007Z","iopub.status.idle":"2026-05-16T11:36:50.019444Z","shell.execute_reply.started":"2026-05-16T11:36:46.826975Z","shell.execute_reply":"2026-05-16T11:36:50.018844Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"from sklearn import metrics\n\nclass AverageMeter:\n    def __init__(self):\n        self.reset()\n\n    def reset(self):\n        self.val = 0\n        self.avg = 0\n        self.sum = 0\n        self.count = 0\n\n    def update(self, val, n=1):\n        self.val = val\n        self.sum += val * n\n        self.count += n\n        self.avg = self.sum / self.count\n\n\nclass RocAucMeter:\n    def __init__(self):\n        self.reset()\n\n    def reset(self):\n        self.y_true = np.array([0, 1])\n        self.y_pred = np.array([0.5, 0.5])\n        self.score = 0\n\n    def update(self, y_true, y_pred):\n        y_true = y_true.cpu().numpy().clip(min=0, max=1).astype(int)\n        y_pred = 1 - torch.nn.functional.softmax(y_pred, dim=1)\\\n                     .data.cpu().numpy()[:, 0]\n        self.y_true = np.hstack((self.y_true, y_true))\n        self.y_pred = np.hstack((self.y_pred, y_pred))\n        try:\n            # self.score = roc_auc_score(self.y_true, self.y_pred)\n            self.score = alaska_weighted_auc(self.y_true, self.y_pred)\n        except ValueError:\n            pass\n\n    @property\n    def avg(self):\n        return self.score\n\n\ndef alaska_weighted_auc(y_true, y_valid):\n    tpr_thresholds = [0.0, 0.4, 1.0]\n    weights = [2, 1]\n\n    fpr, tpr, thresholds = metrics.roc_curve(y_true, y_valid, pos_label=1)\n    areas = np.array(tpr_thresholds[1:]) - np.array(tpr_thresholds[:-1])\n    normalization = np.dot(areas, weights)\n\n    competition_metric = 0\n    for idx, weight in enumerate(weights):\n        y_min = tpr_thresholds[idx]\n        y_max = tpr_thresholds[idx + 1]\n        mask = (y_min < tpr) & (tpr < y_max)\n        if mask.sum() == 0:\n            continue\n        x_padding = np.linspace(fpr[mask][-1], 1, 100)\n        x = np.concatenate([fpr[mask], x_padding])\n        y = np.concatenate([tpr[mask], [y_max] * len(x_padding)])\n        y = y - y_min\n        score = metrics.auc(x, y)\n        competition_metric += score * weight\n\n    return competition_metric / normalization\n\nprint(\"AUC metrics ready!\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-16T11:36:55.407145Z","iopub.execute_input":"2026-05-16T11:36:55.408269Z","iopub.status.idle":"2026-05-16T11:36:55.425329Z","shell.execute_reply.started":"2026-05-16T11:36:55.408223Z","shell.execute_reply":"2026-05-16T11:36:55.424358Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"from kaggle_secrets import UserSecretsClient\n\n# Get credentials from Kaggle Secrets\nsecrets = UserSecretsClient()\nkaggle_username = secrets.get_secret(\"KAGGLE_USERNAME\")\nkaggle_key = secrets.get_secret(\"KAGGLE_KEY\")\n\n# Configure Kaggle API\nos.makedirs('/root/.kaggle', exist_ok=True)\nwith open('/root/.kaggle/kaggle.json', 'w') as f:\n    f.write(f'{{\"username\":\"{kaggle_username}\",\"key\":\"{kaggle_key}\"}}')\nos.chmod('/root/.kaggle/kaggle.json', 0o600)\n\ndef push_checkpoint_to_dataset():\n    import threading, shutil, json\n\n    def _push():\n        try:\n            staging = '/kaggle/temp/stego-checkpoints'\n            os.makedirs(staging, exist_ok=True)\n\n            # Push checkpoints AND log\n            for fname in ['real_checkpoint.pth', 'best_model.pth', 'training_log.txt']:\n                src = f'/kaggle/working/{fname}'\n                if os.path.exists(src):\n                    shutil.copy(src, f'{staging}/{fname}')\n\n            metadata = {\n                \"title\"   : \"stego-checkpoints\",\n                \"id\"      : f\"{kaggle_username}/stego-checkpoints\",\n                \"licenses\": [{\"name\": \"CC0-1.0\"}]\n            }\n            with open(f'{staging}/dataset-metadata.json', 'w') as f:\n                json.dump(metadata, f)\n\n            result = subprocess.run([\n                'kaggle', 'datasets', 'version',\n                '-p', staging, '-m', 'checkpoint update',\n                '--dir-mode', 'zip'\n            ], capture_output=True, text=True)\n\n            if result.returncode == 0:\n                print(\"  ✓ Checkpoint + log pushed to Kaggle dataset!\")\n            else:\n                print(f\"  ⚠ Push failed: {result.stderr}\")\n        except Exception as e:\n            print(f\"  ⚠ Push error: {e}\")\n\n    threading.Thread(target=_push, daemon=True).start()\n    \n# Test connection\nresult = subprocess.run(['kaggle', 'datasets', 'list', '--mine'],\n                       capture_output=True, text=True)\nprint(result.stdout)\nprint(\"Kaggle API ready!\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-16T11:36:57.813287Z","iopub.execute_input":"2026-05-16T11:36:57.814222Z","iopub.status.idle":"2026-05-16T11:36:59.356543Z","shell.execute_reply.started":"2026-05-16T11:36:57.814184Z","shell.execute_reply":"2026-05-16T11:36:59.355698Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"CHECKPOINT_PATH = '/kaggle/working/real_checkpoint.pth'\n\ndef save_checkpoint(epoch, model, optimizer, scheduler, best_score, path):\n    torch.save({\n        'epoch': epoch,\n        'model_state_dict': model.state_dict(),\n        'optimizer_state_dict': optimizer.state_dict(),\n        'scheduler_state_dict': scheduler.state_dict(),\n        'best_score': best_score,\n    }, path)\n    print(f\"  ✓ Checkpoint saved (epoch {epoch+1}, best AUC: {best_score:.4f})\")\n    push_checkpoint_to_dataset()\n\ndef load_checkpoint(path, model, optimizer, scheduler, ignore_scheduler=False):\n    if not os.path.exists(path):\n        print(\"  No local checkpoint. Checking Kaggle dataset...\")\n        result = subprocess.run([\n            'kaggle', 'datasets', 'download',\n            '-d', f'{kaggle_username}/stego-checkpoints',\n            '-p', '/kaggle/working/', '--unzip'\n        ], capture_output=True, text=True)\n\n        # Report what was restored\n        restored = [f for f in ['real_checkpoint.pth', 'best_model.pth', 'training_log.txt']\n                    if os.path.exists(f'/kaggle/working/{f}')]\n        if restored:\n            print(f\"  ✓ Restored from dataset: {restored}\")\n        \n        if not os.path.exists(path):\n            print(\"  No checkpoint found anywhere. Starting from epoch 0.\")\n            return 0, 0.0\n\n    checkpoint = torch.load(path, map_location=device, weights_only=False)\n    model.load_state_dict(checkpoint['model_state_dict'])\n    optimizer.load_state_dict(checkpoint['optimizer_state_dict'])\n    if not ignore_scheduler:\n        scheduler.load_state_dict(checkpoint['scheduler_state_dict'])\n    start_epoch = checkpoint['epoch'] + 1\n    best_score  = checkpoint['best_score']\n    print(f\"  ✓ Checkpoint loaded! Resuming from epoch {start_epoch + 1}\")\n    print(f\"  ✓ Best AUC so far: {best_score:.4f}\")\n    return start_epoch, best_score\n\ndef run_training(total_epochs=70):\n    train_dataset = ALASKA2Dataset(train_df, transform=get_train_transforms())\n    val_dataset   = ALASKA2Dataset(val_df,   transform=get_val_transforms())\n\n    train_loader = DataLoader(train_dataset, batch_size=16, shuffle=True,\n                              num_workers=4, pin_memory=True)\n    val_loader   = DataLoader(val_dataset,   batch_size=16, shuffle=False,\n                              num_workers=4, pin_memory=True)\n    print(f\"Train: {len(train_dataset)} | Val: {len(val_dataset)}\")\n\n    model     = StegoDetector().to(device)\n    optimizer = torch.optim.AdamW(model.parameters(), lr=0.001, weight_decay=0.001)\n    scheduler = torch.optim.lr_scheduler.ReduceLROnPlateau(\n        optimizer, mode='min', factor=0.5, patience=1,\n        threshold=0.0001, min_lr=1e-8\n    )\n    class_weights = torch.tensor([3.0, 1.0, 1.0, 1.0]).to(device)\n    criterion = nn.CrossEntropyLoss(weight=class_weights)\n\n    start_epoch, best_score = load_checkpoint(\n        CHECKPOINT_PATH, model, optimizer, scheduler, ignore_scheduler=True\n    )\n\n    # ── Replay full training history on resume ────────────────\n    LOG_FILE = '/kaggle/working/training_log.txt'\n    if os.path.exists(LOG_FILE):\n        with open(LOG_FILE, 'r') as f:\n            lines = f.readlines()\n        if lines:\n            print(\"\\n\" + \"=\"*60)\n            print(\"  PREVIOUS TRAINING HISTORY\")\n            print(\"=\"*60)\n            for line in lines:\n                print(f\"  {line.rstrip()}\")\n            print(\"=\"*60 + \"\\n\")\n    else:\n        print(\"  No previous training log — fresh start.\\n\")\n\n    if start_epoch >= total_epochs:\n        print(f\"Already completed {total_epochs} epochs!\")\n        return model\n\n    for epoch in range(start_epoch, total_epochs):\n        epoch_start = time.time()\n        print(f\"\\n{'='*60}\")\n        print(f\"Epoch {epoch+1}/{total_epochs} | LR: {optimizer.param_groups[0]['lr']:.8f}\")\n        print(f\"{'='*60}\")\n\n        model.train()\n        loss_meter  = AverageMeter()\n        score_meter = RocAucMeter()\n        train_bar   = tqdm(train_loader, desc=\"  Train\",\n                           unit=\"batch\", dynamic_ncols=True)\n\n        for images, labels in train_bar:\n            images = images.to(device).float()\n            labels = labels.to(device).long()\n            optimizer.zero_grad()\n            outputs = model(images)\n            loss    = criterion(outputs, labels)\n            loss.backward()\n            optimizer.step()\n            loss_meter.update(loss.detach().item(), images.size(0))\n            score_meter.update(labels, outputs)\n            train_bar.set_postfix({\n                'loss': f'{loss_meter.avg:.4f}',\n                'auc' : f'{score_meter.avg:.4f}'\n            })\n\n        model.eval()\n        val_loss_meter  = AverageMeter()\n        val_score_meter = RocAucMeter()\n        val_bar = tqdm(val_loader, desc=\"  Val  \",\n                       unit=\"batch\", dynamic_ncols=True)\n\n        with torch.no_grad():\n            for images, labels in val_bar:\n                images = images.to(device).float()\n                labels = labels.to(device).long()\n                outputs = model(images)\n                loss    = criterion(outputs, labels)\n                val_loss_meter.update(loss.detach().item(), images.size(0))\n                val_score_meter.update(labels, outputs)\n                val_bar.set_postfix({\n                    'loss': f'{val_loss_meter.avg:.4f}',\n                    'auc' : f'{val_score_meter.avg:.4f}'\n                })\n\n        val_auc = val_score_meter.avg\n        scheduler.step(val_loss_meter.avg)\n        elapsed = time.time() - epoch_start\n\n        is_best = val_auc > best_score\n\n        # ── Build log line ────────────────────────────────────\n        msg = (\n            f\"Epoch {epoch+1:02d} | \"\n            f\"Train Loss: {loss_meter.avg:.4f} | Train AUC: {score_meter.avg:.4f} | \"\n            f\"Val Loss: {val_loss_meter.avg:.4f} | Val AUC: {val_auc:.4f} | \"\n            f\"LR: {optimizer.param_groups[0]['lr']:.8f} | \"\n            f\"Time: {elapsed/60:.1f}m\"\n        )\n        print(f\"\\n  ── Epoch {epoch+1} Summary ──\")\n        print(f\"  {msg}\")\n\n        # ── Write to log file (single source of truth) ────────\n        with open(LOG_FILE, 'a') as f:\n            f.write(msg + '\\n')\n            if is_best:\n                best_line = f\"  ★ NEW BEST: AUC={val_auc:.4f}\"\n                f.write(best_line + '\\n')\n                print(best_line)\n\n        if is_best:\n            best_score = val_auc\n            torch.save(model.state_dict(), '/kaggle/working/best_model.pth')\n\n        save_checkpoint(epoch, model, optimizer, scheduler,\n                        best_score, CHECKPOINT_PATH)\n\n    print(f\"\\n✅ Training complete! Best Val AUC: {best_score:.4f}\")\n    return model\n    \nprint(\"Training function ready!\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-16T11:37:03.038672Z","iopub.execute_input":"2026-05-16T11:37:03.039197Z","iopub.status.idle":"2026-05-16T11:37:03.060055Z","shell.execute_reply.started":"2026-05-16T11:37:03.039168Z","shell.execute_reply":"2026-05-16T11:37:03.059278Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# CELL 10 — RUN THIS TO START / RESUME TRAINING\n# Every session: run cells 1-9 first, then this cell\n# Automatically resumes from checkpoint\n# ============================================================\n\ntrained_model = run_training(total_epochs=35)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-16T11:37:08.683237Z","iopub.execute_input":"2026-05-16T11:37:08.683646Z","iopub.status.idle":"2026-05-16T11:37:29.145224Z","shell.execute_reply.started":"2026-05-16T11:37:08.683616Z","shell.execute_reply":"2026-05-16T11:37:29.144131Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"from glob import glob\nimport torch\nimport torch.nn.functional as F\nimport cv2\nimport numpy as np\nfrom tqdm import tqdm\n\n# =========================================================\n# LOAD BEST MODEL\n# =========================================================\n\nmodel = StegoDetector().to(device)\n\ncheckpoint_path = \"/kaggle/working/best_model.pth\"\n\nif not os.path.exists(checkpoint_path):\n    checkpoint_path = \"/kaggle/input/datasets/ojasmalhotra/stego-checkpoints/best_model.pth\"\n\nmodel.load_state_dict(torch.load(checkpoint_path, map_location=device))\nmodel.eval()\n\nprint(\"✓ Model loaded\")\n\n# =========================================================\n# LOAD STEGOGAN IMAGES\n# =========================================================\n\nROOT = \"/kaggle/input/datasets/ojasmalhotra/stegogan\"\n\nimage_paths = glob(f\"{ROOT}/*.jpg\")\n\nprint(f\"Total images found: {len(image_paths)}\")\n\n# =========================================================\n# TRANSFORM\n# =========================================================\n\ntransform = get_val_transforms()\n\n# =========================================================\n# INFERENCE\n# =========================================================\n\nall_probs = []\n\nwith torch.no_grad():\n\n    for path in tqdm(image_paths):\n\n        try:\n            image = cv2.imread(path, cv2.IMREAD_COLOR)\n            image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB).astype(np.float32)\n            image /= 255.0\n\n            image = apply_srm_filters(image)\n\n            tensor = transform(image=image)['image']\n            tensor = tensor.unsqueeze(0).to(device).float()\n\n            outputs = model(tensor)\n\n            probs = F.softmax(outputs, dim=1)\n\n            cover_prob = probs[0,0].item()\n            stego_prob = 1.0 - cover_prob\n\n            all_probs.append(stego_prob)\n\n        except Exception as e:\n            print(f\"Error on {path}: {e}\")\n\n# =========================================================\n# RESULTS\n# =========================================================\n\nall_probs = np.array(all_probs)\n\nprint(\"\\n==============================\")\nprint(\"STEGOGAN RESULTS\")\nprint(\"==============================\")\n\nprint(f\"Images tested: {len(all_probs)}\")\nprint(f\"Average stego probability: {all_probs.mean():.4f}\")\nprint(f\"Median stego probability : {np.median(all_probs):.4f}\")\n\nprint(\"\\nPrediction distribution:\")\n\nprint(f\"Prob > 0.50 : {(all_probs > 0.50).mean()*100:.2f}%\")\nprint(f\"Prob > 0.70 : {(all_probs > 0.70).mean()*100:.2f}%\")\nprint(f\"Prob > 0.90 : {(all_probs > 0.90).mean()*100:.2f}%\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-16T11:37:34.059764Z","iopub.execute_input":"2026-05-16T11:37:34.060199Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# ALASKA2 GRAPHS CELL\n# Run after training is complete (trained_model must exist)\n# Mirrors SteganoGAN graphs for direct comparison\n# ============================================================\n\nimport matplotlib\nmatplotlib.use(\"Agg\")\nimport matplotlib.pyplot as plt\nimport matplotlib.ticker as mticker\nfrom sklearn.metrics import (roc_curve, confusion_matrix,\n                             ConfusionMatrixDisplay, classification_report)\nimport numpy as np\nimport os, re\nfrom IPython.display import display as ipy_display\n\nGRAPHS_DIR = \"/kaggle/working/alaska2_graphs\"\nos.makedirs(GRAPHS_DIR, exist_ok=True)\n\nplt.style.use(\"seaborn-v0_8-darkgrid\")\nCOLORS = {\n    \"train\" : \"#4C9BE8\",\n    \"val\"   : \"#E8834C\",\n    \"auc\"   : \"#5DBE6E\",\n    \"lr\"    : \"#B06EE8\",\n    \"cover\" : \"#4C9BE8\",\n    \"jmipod\": \"#E8834C\",\n    \"juniward\": \"#5DBE6E\",\n    \"uerd\"  : \"#B06EE8\",\n}\nCLASS_NAMES = [\"Cover\", \"JMiPOD\", \"JUNIWARD\", \"UERD\"]\n\ndef save_fig(fig, name):\n    path = os.path.join(GRAPHS_DIR, name)\n    fig.savefig(path, dpi=150, bbox_inches=\"tight\", facecolor=\"white\")\n    ipy_display(fig)\n    plt.close(fig)\n    print(f\"  [Graph] Saved: {name}\")\n    return path\n\n\n# ── Parse training_log.txt ──────────────────────────────────────────────\nLOG_FILE = \"/kaggle/working/training_log.txt\"\n\ndef parse_log(log_path):\n    \"\"\"\n    Returns a list of dicts, one per epoch, with keys:\n    epoch, train_loss, train_auc, val_loss, val_auc, lr, time_min\n    Skips ★ lines automatically.\n    \"\"\"\n    pattern = re.compile(\n        r\"Epoch\\s+(\\d+)\\s*\\|\"\n        r\"\\s*Train Loss:\\s*([\\d.]+)\\s*\\|\"\n        r\"\\s*Train AUC:\\s*([\\d.]+)\\s*\\|\"\n        r\"\\s*Val Loss:\\s*([\\d.]+)\\s*\\|\"\n        r\"\\s*Val AUC:\\s*([\\d.]+)\\s*\\|\"\n        r\"\\s*LR:\\s*([\\d.eE+-]+)\\s*\\|\"\n        r\"\\s*Time:\\s*([\\d.]+)m\"\n    )\n    history = []\n    with open(log_path, \"r\") as f:\n        for line in f:\n            m = pattern.search(line)\n            if m:\n                history.append({\n                    \"epoch\"      : int(m.group(1)),\n                    \"train_loss\" : float(m.group(2)),\n                    \"train_auc\"  : float(m.group(3)),\n                    \"val_loss\"   : float(m.group(4)),\n                    \"val_auc\"    : float(m.group(5)),\n                    \"lr\"         : float(m.group(6)),\n                    \"time_min\"   : float(m.group(7)),\n                })\n    history.sort(key=lambda x: x[\"epoch\"])\n    return history\n\nhistory = parse_log(LOG_FILE)\nepochs      = [h[\"epoch\"]      for h in history]\ntrain_losses = [h[\"train_loss\"] for h in history]\nval_losses   = [h[\"val_loss\"]   for h in history]\ntrain_aucs   = [h[\"train_auc\"]  for h in history]\nval_aucs     = [h[\"val_auc\"]    for h in history]\nlrs          = [h[\"lr\"]         for h in history]\ntimes        = [h[\"time_min\"]   for h in history]\n\nprint(f\"Parsed {len(history)} epochs from log.\")\nbest_epoch = epochs[int(np.argmax(val_aucs))]\nbest_auc   = max(val_aucs)\nprint(f\"Best Val AUC: {best_auc:.4f} at epoch {best_epoch}\")\n\n\n# ══════════════════════════════════════════════════════════════════════════\n# GRAPH 1 — Loss curve (mirrors p2_loss_curve)\n# ══════════════════════════════════════════════════════════════════════════\nfig, ax = plt.subplots(figsize=(10, 5))\nax.plot(epochs, train_losses, color=COLORS[\"train\"], lw=2, marker=\"o\", ms=4, label=\"Train Loss\")\nax.plot(epochs, val_losses,   color=COLORS[\"val\"],   lw=2, marker=\"s\", ms=4, label=\"Val Loss\")\nax.set_xlabel(\"Epoch\")\nax.set_ylabel(\"Weighted Cross-Entropy Loss\")\nax.set_title(\"Alaska2 — EfficientNet-B2 Training & Validation Loss\", fontsize=13, fontweight=\"bold\")\nax.legend()\nax.xaxis.set_major_locator(mticker.MaxNLocator(integer=True))\nsave_fig(fig, \"alaska2_loss_curve.png\")\n\n\n# ══════════════════════════════════════════════════════════════════════════\n# GRAPH 2 — Alaska2 Weighted AUC per epoch (mirrors p2_auc_curve)\n# ══════════════════════════════════════════════════════════════════════════\nfig, ax = plt.subplots(figsize=(10, 5))\nax.plot(epochs, val_aucs,   color=COLORS[\"auc\"],   lw=2, marker=\"D\", ms=4, label=\"Val AUC (weighted)\")\nax.plot(epochs, train_aucs, color=COLORS[\"train\"], lw=2, marker=\"o\", ms=4, label=\"Train AUC (weighted)\", alpha=0.7)\nax.axhline(best_auc, color=\"gray\", lw=1, ls=\"--\", alpha=0.6)\nax.annotate(\n    f\"Best: {best_auc:.4f}\\n@ epoch {best_epoch}\",\n    xy=(best_epoch, best_auc),\n    xytext=(best_epoch + max(1, len(epochs) * 0.05), best_auc - 0.008),\n    fontsize=9, color=\"gray\",\n    arrowprops=dict(arrowstyle=\"->\", color=\"gray\")\n)\nax.set_xlabel(\"Epoch\")\nax.set_ylabel(\"Alaska2 Weighted AUC\")\nax.set_title(\"Alaska2 — Weighted AUC per Epoch\", fontsize=13, fontweight=\"bold\")\nax.set_ylim(max(0.4, min(val_aucs) - 0.02), min(1.01, best_auc + 0.04))\nax.legend()\nax.xaxis.set_major_locator(mticker.MaxNLocator(integer=True))\nsave_fig(fig, \"alaska2_auc_curve.png\")\n\n\n# ══════════════════════════════════════════════════════════════════════════\n# GRAPH 3 — Learning rate schedule (mirrors p2_lr_schedule)\n# ══════════════════════════════════════════════════════════════════════════\nfig, ax = plt.subplots(figsize=(10, 4))\nax.plot(epochs, lrs, color=COLORS[\"lr\"], lw=2, marker=\"^\", ms=4)\nax.set_xlabel(\"Epoch\")\nax.set_ylabel(\"Learning Rate\")\nax.set_title(\"Alaska2 — ReduceLROnPlateau Schedule\", fontsize=13, fontweight=\"bold\")\nax.ticklabel_format(axis=\"y\", style=\"sci\", scilimits=(0, 0))\nax.xaxis.set_major_locator(mticker.MaxNLocator(integer=True))\nsave_fig(fig, \"alaska2_lr_schedule.png\")\n\n\n# ══════════════════════════════════════════════════════════════════════════\n# GRAPH 4 — Training time per epoch\n# ══════════════════════════════════════════════════════════════════════════\nfig, ax = plt.subplots(figsize=(10, 4))\nax.bar(epochs, times, color=COLORS[\"train\"], alpha=0.8, edgecolor=\"white\", linewidth=0.4)\nax.axhline(np.mean(times), color=COLORS[\"val\"], lw=1.5, ls=\"--\",\n           label=f\"Mean: {np.mean(times):.1f} min\")\nax.set_xlabel(\"Epoch\")\nax.set_ylabel(\"Time (minutes)\")\nax.set_title(\"Alaska2 — Training Time per Epoch\", fontsize=13, fontweight=\"bold\")\nax.legend()\nax.xaxis.set_major_locator(mticker.MaxNLocator(integer=True))\nsave_fig(fig, \"alaska2_time_per_epoch.png\")\n\n\n# ══════════════════════════════════════════════════════════════════════════\n# GRAPH 5 — Per-class validation metrics (4-class confusion matrix + report)\n# Run inference on val set using best model\n# ══════════════════════════════════════════════════════════════════════════\nprint(\"\\nRunning inference on val set for confusion matrix...\")\n\nbest_model = StegoDetector().to(device)\nbest_model.load_state_dict(\n    torch.load(\"/kaggle/working/best_model.pth\", map_location=device, weights_only=False)\n)\nbest_model.eval()\n\nval_dataset_infer = ALASKA2Dataset(val_df, transform=get_val_transforms())\nval_loader_infer  = DataLoader(val_dataset_infer, batch_size=32,\n                               shuffle=False, num_workers=4, pin_memory=True)\n\nall_preds, all_labels, all_probs = [], [], []\n\nwith torch.no_grad():\n    for images, labels in tqdm(val_loader_infer, desc=\"Inference\"):\n        images  = images.to(device).float()\n        outputs = best_model(images)\n        probs   = torch.nn.functional.softmax(outputs, dim=1).cpu().numpy()\n        preds   = outputs.argmax(dim=1).cpu().numpy()\n        all_preds.extend(preds.tolist())\n        all_labels.extend(labels.numpy().tolist())\n        all_probs.extend(probs.tolist())\n\nall_preds  = np.array(all_preds)\nall_labels = np.array(all_labels)\nall_probs  = np.array(all_probs)   # shape (N, 4)\n\n# ── 4×4 Confusion matrix ──────────────────────────────────────────────────\ncm   = confusion_matrix(all_labels, all_preds)\ndisp = ConfusionMatrixDisplay(confusion_matrix=cm, display_labels=CLASS_NAMES)\n\nfig, ax = plt.subplots(figsize=(7, 6))\ndisp.plot(ax=ax, colorbar=True, cmap=\"Blues\")\nax.set_title(\"Alaska2 — 4-Class Confusion Matrix (Val Set)\",\n             fontsize=13, fontweight=\"bold\")\nsave_fig(fig, \"alaska2_confusion_matrix.png\")\n\n# ── Classification report heatmap ─────────────────────────────────────────\nreport = classification_report(all_labels, all_preds,\n                                target_names=CLASS_NAMES, output_dict=True)\nmetrics_df = pd.DataFrame(report).T.loc[CLASS_NAMES, [\"precision\", \"recall\", \"f1-score\"]]\n\nfig, ax = plt.subplots(figsize=(7, 4))\nimport seaborn as sns\nsns.heatmap(metrics_df.astype(float), annot=True, fmt=\".3f\", cmap=\"YlGnBu\",\n            ax=ax, vmin=0, vmax=1, linewidths=0.5)\nax.set_title(\"Alaska2 — Per-Class Precision / Recall / F1 (Val Set)\",\n             fontsize=13, fontweight=\"bold\")\nax.set_xlabel(\"\")\nax.set_ylabel(\"\")\nsave_fig(fig, \"alaska2_classification_report.png\")\n\n\n# ══════════════════════════════════════════════════════════════════════════\n# GRAPH 6 — One-vs-Rest ROC curves for all 4 classes\n# (mirrors p3_roc_curve but for 4-class)\n# ══════════════════════════════════════════════════════════════════════════\nfig, ax = plt.subplots(figsize=(8, 6))\nclass_colors = [COLORS[\"cover\"], COLORS[\"jmipod\"], COLORS[\"juniward\"], COLORS[\"uerd\"]]\n\nfor i, (cls_name, col) in enumerate(zip(CLASS_NAMES, class_colors)):\n    y_true_bin = (all_labels == i).astype(int)\n    y_score    = all_probs[:, i]\n    fpr, tpr, _ = roc_curve(y_true_bin, y_score)\n    auc_score   = float(np.trapz(tpr, fpr))\n    ax.plot(fpr, tpr, color=col, lw=2, label=f\"{cls_name} (AUC={auc_score:.4f})\")\n\nax.plot([0, 1], [0, 1], \"k--\", lw=1, alpha=0.5, label=\"Random\")\nax.set_xlabel(\"False Positive Rate\")\nax.set_ylabel(\"True Positive Rate\")\nax.set_title(\"Alaska2 — One-vs-Rest ROC Curves (Val Set)\", fontsize=13, fontweight=\"bold\")\nax.legend(loc=\"lower right\")\nax.fill_between([0, 1], [0, 1], alpha=0.04, color=\"gray\")\nsave_fig(fig, \"alaska2_roc_curves.png\")\n\n\n# ══════════════════════════════════════════════════════════════════════════\n# GRAPH 7 — Stego probability score distribution\n# (binary: Cover=0 vs any stego=1, mirrors p3_test_score_distribution)\n# ══════════════════════════════════════════════════════════════════════════\n# P(stego) = 1 - P(cover)\nstego_probs   = 1.0 - all_probs[:, 0]\nbinary_labels = (all_labels > 0).astype(int)\n\nfig, ax = plt.subplots(figsize=(10, 5))\ncover_mask = binary_labels == 0\nstego_mask = binary_labels == 1\nax.hist(stego_probs[cover_mask], bins=60, color=COLORS[\"cover\"], alpha=0.7,\n        edgecolor=\"white\", linewidth=0.3, label=\"Cover images\", density=True)\nax.hist(stego_probs[stego_mask], bins=60, color=COLORS[\"val\"], alpha=0.7,\n        edgecolor=\"white\", linewidth=0.3, label=\"Stego images (all 3)\", density=True)\nax.axvline(0.5, color=\"red\", lw=1.5, ls=\"--\", label=\"Decision threshold (0.5)\")\nax.set_xlabel(\"P(stego) = 1 − P(cover)\")\nax.set_ylabel(\"Density\")\nax.set_title(\"Alaska2 — Stego Probability Score Distribution (Val Set)\",\n             fontsize=13, fontweight=\"bold\")\nax.legend()\nsave_fig(fig, \"alaska2_score_distribution.png\")\n\n\n# ══════════════════════════════════════════════════════════════════════════\n# GRAPH 8 — Per-algorithm stego score distribution\n# Unique to Alaska2: shows JMiPOD vs JUNIWARD vs UERD separability\n# ══════════════════════════════════════════════════════════════════════════\nfig, ax = plt.subplots(figsize=(10, 5))\nalgo_colors = [COLORS[\"jmipod\"], COLORS[\"juniward\"], COLORS[\"uerd\"]]\nalgo_labels = [\"JMiPOD\", \"JUNIWARD\", \"UERD\"]\n\nfor label_idx, (algo, col) in zip([1, 2, 3], zip(algo_labels, algo_colors)):\n    mask  = all_labels == label_idx\n    probs = stego_probs[mask]\n    ax.hist(probs, bins=50, color=col, alpha=0.65,\n            edgecolor=\"white\", linewidth=0.3, label=algo, density=True)\n\nax.axvline(0.5, color=\"red\", lw=1.5, ls=\"--\", label=\"Threshold (0.5)\")\nax.set_xlabel(\"P(stego)\")\nax.set_ylabel(\"Density\")\nax.set_title(\"Alaska2 — Score Distribution per Stego Algorithm\",\n             fontsize=13, fontweight=\"bold\")\nax.legend()\nsave_fig(fig, \"alaska2_per_algo_distribution.png\")\n\n\n# ══════════════════════════════════════════════════════════════════════════\n# GRAPH 9 — Weighted AUC improvement over training (cumulative best)\n# ══════════════════════════════════════════════════════════════════════════\ncumulative_best = np.maximum.accumulate(val_aucs)\n\nfig, ax = plt.subplots(figsize=(10, 5))\nax.plot(epochs, val_aucs,        color=COLORS[\"auc\"],   lw=1.5, alpha=0.5,\n        marker=\"D\", ms=3, label=\"Val AUC (each epoch)\")\nax.plot(epochs, cumulative_best, color=COLORS[\"val\"],   lw=2.5,\n        label=\"Best AUC so far (cumulative)\")\nax.set_xlabel(\"Epoch\")\nax.set_ylabel(\"Alaska2 Weighted AUC\")\nax.set_title(\"Alaska2 — Best AUC Progress Over Training\", fontsize=13, fontweight=\"bold\")\nax.legend()\nax.xaxis.set_major_locator(mticker.MaxNLocator(integer=True))\nsave_fig(fig, \"alaska2_best_auc_progress.png\")\n\n\n# ══════════════════════════════════════════════════════════════════════════\n# GRAPH 10 — SRM filter residual visualisation\n# Shows what the model actually receives as input (mirrors p1 heatmap)\n# ══════════════════════════════════════════════════════════════════════════\nsample_paths = {\n    \"Cover\"   : val_df[val_df[\"label\"] == 0].iloc[0],\n    \"JMiPOD\"  : val_df[val_df[\"label\"] == 1].iloc[0],\n    \"JUNIWARD\": val_df[val_df[\"label\"] == 2].iloc[0],\n    \"UERD\"    : val_df[val_df[\"label\"] == 3].iloc[0],\n}\n\nfig, axes = plt.subplots(2, 4, figsize=(16, 8))\nfig.suptitle(\"Alaska2 — SRM Residual Preprocessing: RGB vs SRM Channels\",\n             fontsize=13, fontweight=\"bold\")\n\nfor col_idx, (kind, row) in enumerate(sample_paths.items()):\n    img_path = f\"{DATA_ROOT}/{row['kind']}/{row['image_name']}\"\n    img_bgr  = cv2.imread(img_path, cv2.IMREAD_COLOR)\n    img_rgb  = cv2.cvtColor(img_bgr, cv2.COLOR_BGR2RGB)\n    img_f32  = img_rgb.astype(np.float32) / 255.0\n    srm_out  = apply_srm_filters(img_f32)   # (H, W, 3) in [0,1]\n\n    axes[0, col_idx].imshow(img_rgb)\n    axes[0, col_idx].set_title(kind, fontsize=10, fontweight=\"bold\")\n    axes[0, col_idx].axis(\"off\")\n\n    axes[1, col_idx].imshow(srm_out, cmap=\"inferno\")\n    axes[1, col_idx].set_title(\"SRM residuals\", fontsize=9)\n    axes[1, col_idx].axis(\"off\")\n\naxes[0, 0].set_ylabel(\"Original\", fontsize=10, labelpad=6)\naxes[1, 0].set_ylabel(\"After SRM\", fontsize=10, labelpad=6)\nplt.tight_layout()\nsave_fig(fig, \"alaska2_srm_preprocessing.png\")\n\n\n# ══════════════════════════════════════════════════════════════════════════\n# GRAPH 11 — Summary bar: best AUC per stego algorithm (binary OvR)\n# ══════════════════════════════════════════════════════════════════════════\nper_algo_auc = {}\nfor label_idx, algo in zip([1, 2, 3], [\"JMiPOD\", \"JUNIWARD\", \"UERD\"]):\n    # Cover (0) vs this algo\n    mask       = (all_labels == 0) | (all_labels == label_idx)\n    y_true_bin = (all_labels[mask] > 0).astype(int)\n    y_score    = stego_probs[mask]\n    fpr, tpr, _ = roc_curve(y_true_bin, y_score)\n    per_algo_auc[algo] = float(np.trapz(tpr, fpr))\n\nfig, ax = plt.subplots(figsize=(7, 5))\nbar_colors = [COLORS[\"jmipod\"], COLORS[\"juniward\"], COLORS[\"uerd\"]]\nbars = ax.bar(list(per_algo_auc.keys()), list(per_algo_auc.values()),\n              color=bar_colors, width=0.4, edgecolor=\"white\")\nfor bar, val in zip(bars, per_algo_auc.values()):\n    ax.text(bar.get_x() + bar.get_width() / 2, bar.get_height() + 0.003,\n            f\"{val:.4f}\", ha=\"center\", va=\"bottom\", fontsize=12, fontweight=\"bold\")\nax.set_ylim(0.4, 1.05)\nax.set_ylabel(\"Binary ROC-AUC (Cover vs Algorithm)\")\nax.set_title(\"Alaska2 — Detection AUC per Stego Algorithm\",\n             fontsize=13, fontweight=\"bold\")\nsave_fig(fig, \"alaska2_per_algo_auc.png\")\n\n\n# ══════════════════════════════════════════════════════════════════════════\n# SUMMARY PRINTOUT\n# ══════════════════════════════════════════════════════════════════════════\nprint(\"\\n\" + \"=\"*60)\nprint(\"  ALASKA2 GRAPHS SUMMARY\")\nprint(\"=\"*60)\nprint(f\"  Best Val Weighted AUC : {best_auc:.4f}  (epoch {best_epoch})\")\nprint(f\"  Final Val AUC         : {val_aucs[-1]:.4f}\")\nprint(f\"  Total epochs trained  : {len(history)}\")\nprint(f\"  Avg time / epoch      : {np.mean(times):.1f} min\")\nprint(f\"\\n  Per-algorithm AUC (Cover vs single algo):\")\nfor algo, auc in per_algo_auc.items():\n    print(f\"    {algo:<12}: {auc:.4f}\")\nprint(f\"\\n  Classification report:\")\nprint(classification_report(all_labels, all_preds, target_names=CLASS_NAMES))\nprint(f\"\\n  All graphs saved to: {GRAPHS_DIR}\")\nprint(\"=\"*60)","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}