{"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":132732,"databundleVersionId":16583342}],"dockerImageVersionId":31329,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"# This Python 3 environment comes with many helpful analytics libraries installed\n# It is defined by the kaggle/python Docker image: https://github.com/kaggle/docker-python\n# For example, here's several helpful packages to load\n\nimport numpy as np # linear algebra\nimport pandas as pd # data processing, CSV file I/O (e.g. pd.read_csv)\n\n# Input data files are available in the read-only \"../input/\" directory\n# For example, running this (by clicking run or pressing Shift+Enter) will list all files under the input directory\n\nimport os\nfor dirname, _, filenames in os.walk('/kaggle/input/competitions/dlmmdd-workshop-synthetic-source-attribution-challenge/Data/Data'):\n    for filename in filenames:\n        print(os.path.join(dirname, filename))\n\n# You can write up to 20GB to the current directory (/kaggle/working/) that gets preserved as output when you create a version using \"Save & Run All\" \n# You can also write temporary files to /kaggle/temp/, but they won't be saved outside of the current session","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2026-04-18T05:28:18.035429Z","iopub.execute_input":"2026-04-18T05:28:18.036985Z","iopub.status.idle":"2026-04-18T05:28:22.253715Z","shell.execute_reply.started":"2026-04-18T05:28:18.036897Z","shell.execute_reply":"2026-04-18T05:28:22.252867Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nimport pandas as pd\nfrom PIL import Image\nimport torch\nimport torch.nn as nn\nimport torch.optim as optim\nimport torch.nn.functional as F\nfrom torch.utils.data import Dataset, DataLoader\nfrom torchvision import models\nfrom torchvision.transforms import v2\nimport copy\nimport math\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-18T05:44:45.469139Z","iopub.execute_input":"2026-04-18T05:44:45.470135Z","iopub.status.idle":"2026-04-18T05:44:45.476028Z","shell.execute_reply.started":"2026-04-18T05:44:45.470090Z","shell.execute_reply":"2026-04-18T05:44:45.475104Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# **Configuration**","metadata":{}},{"cell_type":"code","source":"CONFIG = {\n    \"base_dir\": \"/kaggle/input/competitions/dlmmdd-workshop-synthetic-source-attribution-challenge\",\n    \"train_csv\": \"/kaggle/input/competitions/dlmmdd-workshop-synthetic-source-attribution-challenge/Data/Data/training.csv\",    \n    \"test_csv\": \"/kaggle/input/competitions/dlmmdd-workshop-synthetic-source-attribution-challenge/Data/Data/test.csv\",      \n    \"submission_csv\": \"submission.csv\",\n    \n    # Model & Training hyperparameters\n    \"num_classes\": 10,           \n    \"img_size\": 256,             \n    \"crop_size\": 224,            \n    \"batch_size\": 32,\n    \"epochs\": 25,\n    \"lr_backbone\": 5e-5,         # Slower LR for pre-trained weights\n    \"lr_spectral\": 5e-4,         # Faster LR for our new Spectral Head\n    \"weight_decay\": 1e-4,\n    \"ema_decay\": 0.999,\n    \"spectral_dropout\": 0.1,     # HELIX-style stochastic depth\n    \n    # Device setup\n    \"device\": \"cuda\" if torch.cuda.is_available() else \"cpu\",\n    \"save_path\": \"spectral_helix_best.pth\"\n}","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-18T05:44:46.013650Z","iopub.execute_input":"2026-04-18T05:44:46.014423Z","iopub.status.idle":"2026-04-18T05:44:46.019682Z","shell.execute_reply.started":"2026-04-18T05:44:46.014389Z","shell.execute_reply":"2026-04-18T05:44:46.019007Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# EMA Emplementation","metadata":{}},{"cell_type":"code","source":"class EMA:\n    def __init__(self, model, decay):\n        self.decay = decay\n        self.shadow = copy.deepcopy(model).eval()\n        for p in self.shadow.parameters():\n            p.requires_grad_(False)\n            \n    @torch.no_grad()\n    def update(self, model):\n        for s, p in zip(self.shadow.parameters(), model.parameters()):\n            if s.dtype.is_floating_point:\n                s.mul_(self.decay).add_(p.detach(), alpha=1 - self.decay)\n        for s, p in zip(self.shadow.buffers(), model.buffers()):\n            s.copy_(p)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-18T05:44:46.105910Z","iopub.execute_input":"2026-04-18T05:44:46.106681Z","iopub.status.idle":"2026-04-18T05:44:46.112078Z","shell.execute_reply.started":"2026-04-18T05:44:46.106655Z","shell.execute_reply":"2026-04-18T05:44:46.111361Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Data Augmentation","metadata":{}},{"cell_type":"code","source":"train_transforms = v2.Compose([\n    v2.Resize((CONFIG[\"img_size\"], CONFIG[\"img_size\"]), antialias=True),\n    v2.RandomResizedCrop(size=(CONFIG[\"crop_size\"], CONFIG[\"crop_size\"]), scale=(0.8, 1.0)),\n    v2.RandomRotation(degrees=15),\n    v2.RandomApply([\n        v2.RandomChoice([\n            v2.ColorJitter(brightness=0.2, contrast=0.2),\n            v2.GaussianBlur(kernel_size=5, sigma=(0.1, 2.0)),\n            v2.RandomGrayscale(p=1.0),\n            v2.RandomAdjustSharpness(sharpness_factor=2, p=1.0) \n        ])\n    ], p=0.8),\n    v2.RandomHorizontalFlip(p=0.5),\n    v2.ToImage(),\n    v2.ToDtype(torch.float32, scale=True),\n    v2.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]),\n])\n\ntest_transforms = v2.Compose([\n    v2.Resize((CONFIG[\"crop_size\"], CONFIG[\"crop_size\"]), antialias=True),\n    v2.ToImage(),\n    v2.ToDtype(torch.float32, scale=True),\n    v2.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]),\n])\n\nclass SIADataset(Dataset):\n    def __init__(self, csv_file, transform=None, is_test=False):\n        self.df = pd.read_csv(csv_file)\n        self.transform = transform\n        self.is_test = is_test\n        \n        # Autodetect column names to prevent KeyErrors from Kaggle variations\n        self.id_col = next((col for col in ['ID', 'id', 'image_id'] if col in self.df.columns), self.df.columns[0])\n        \n        path_aliases = ['path', 'image_path', 'file_path', 'filename']\n        self.path_col = next((col for col in path_aliases if col in self.df.columns), \n                             self.df.columns[1] if len(self.df.columns) > 1 else self.df.columns[-1])\n        \n        if not self.is_test:\n            target_aliases = ['TARGET', 'target', 'label', 'class', 'source', 'generator']\n            # Find any leftover columns that are not ID or Path\n            leftover_cols = [c for c in self.df.columns if c not in [self.id_col, self.path_col]]\n            fallback_target = leftover_cols[0] if len(leftover_cols) > 0 else 'TARGET'\n            \n            self.target_col = next((col for col in target_aliases if col in self.df.columns), fallback_target)\n            print(f\"Dataset Loaded -> Using ID column: '{self.id_col}', Path column: '{self.path_col}', Target column: '{self.target_col}'\")\n\n    def __len__(self): return len(self.df)\n\n    def __getitem__(self, idx):\n        row = self.df.iloc[idx]\n        img_id = row[self.id_col]\n        csv_path = row[self.path_col]\n        \n        # Extract just the filename to make resolution bulletproof\n        filename = os.path.basename(csv_path)\n        split_dir = \"Test\" if self.is_test else \"Training\"\n        \n        # We explicitly check for multiple valid structures by relying on the filename.\n        paths_to_try = [\n            os.path.join(CONFIG[\"base_dir\"], \"Data\", \"Data\", split_dir, filename), # Most likely for Kaggle\n            os.path.join(CONFIG[\"base_dir\"], \"Data\", split_dir, filename),\n            os.path.join(CONFIG[\"base_dir\"], split_dir, filename),\n            os.path.join(CONFIG[\"base_dir\"], csv_path), \n            os.path.join(CONFIG[\"base_dir\"], \"Data\", csv_path),\n            os.path.join(CONFIG[\"base_dir\"], \"Data\", \"Data\", csv_path),\n        ]\n        \n        img_path = None\n        for p in paths_to_try:\n            if os.path.exists(p):\n                img_path = p\n                break\n                \n        if img_path is None:\n            img_path = paths_to_try[0] # Fallback for error message output\n            \n        try:\n            image = Image.open(img_path).convert('RGB')\n        except Exception as e:\n            print(f\"Warning: Could not load image. Checked paths including {img_path}. Error: {e}\")\n            # Return a blank image as a fallback to prevent loader crash\n            image = Image.new('RGB', (CONFIG[\"img_size\"], CONFIG[\"img_size\"]))\n            \n        if self.transform: image = self.transform(image)\n            \n        if self.is_test:\n            return image, str(img_id) \n        else:\n            return image, int(row[self.target_col])\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-18T05:44:46.194091Z","iopub.execute_input":"2026-04-18T05:44:46.194758Z","iopub.status.idle":"2026-04-18T05:44:46.210907Z","shell.execute_reply.started":"2026-04-18T05:44:46.194734Z","shell.execute_reply":"2026-04-18T05:44:46.210004Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Spectral-Helix My Architechture","metadata":{}},{"cell_type":"code","source":"class GlobalFilterBlock(nn.Module):\n    \n    def __init__(self, dim, h, w, drop_path=0.0):\n        super().__init__()\n        # We need weights for the real Fourier transform output (w // 2 + 1)\n        self.complex_weight = nn.Parameter(torch.randn(dim, h, w // 2 + 1, 2, dtype=torch.float32) * 0.02)\n        # HELIX stochastic depth (Alpha Dropout)\n        self.drop_path = nn.Dropout2d(p=drop_path) if drop_path > 0. else nn.Identity()\n\n    def forward(self, x):\n        B, C, H, W = x.shape\n        # Transform to frequency domain\n        x_fft = torch.fft.rfft2(x, norm='ortho')\n        \n        # Multiply by learnable complex filter\n        weight = torch.view_as_complex(self.complex_weight)\n        x_filtered = x_fft * weight\n        \n        # Transform back to spatial domain\n        x_ifft = torch.fft.irfft2(x_filtered, s=(H, W), norm='ortho')\n        \n        # Residual connection with stochastic depth\n        return x + self.drop_path(x_ifft)\n\n\nclass SpectralHELIX(nn.Module):\n    def __init__(self, num_classes):\n        super().__init__()\n        # 1. Classical Spatial Backbone (ResNet50)\n        base_model = models.resnet50(weights=models.ResNet50_Weights.IMAGENET1K_V2)\n        self.backbone = nn.Sequential(*list(base_model.children())[:-2]) # Output: (B, 2048, 7, 7)\n        \n        # 2. Spectral Branch (Replaces HELIX Quantum Branch)\n        self.spectral_proj = nn.Conv2d(2048, 256, 1) # Reduce channels for efficiency\n        # 3 layers of Frequency Artifact Hunting\n        self.spectral_blocks = nn.Sequential(\n            GlobalFilterBlock(256, 7, 7, drop_path=CONFIG[\"spectral_dropout\"]),\n            nn.GELU(),\n            GlobalFilterBlock(256, 7, 7, drop_path=CONFIG[\"spectral_dropout\"]),\n            nn.GELU(),\n            GlobalFilterBlock(256, 7, 7, drop_path=CONFIG[\"spectral_dropout\"])\n        )\n        self.spectral_head = nn.Sequential(\n            nn.AdaptiveAvgPool2d(1), nn.Flatten(),\n            nn.Linear(256, 128), nn.GELU(), nn.Dropout(0.3),\n            nn.Linear(128, num_classes)\n        )\n        \n        # 3. Classical Spatial Head\n        self.spatial_head = nn.Sequential(\n            nn.AdaptiveAvgPool2d(1), nn.Flatten(),\n            nn.Dropout(0.3),\n            nn.Linear(2048, num_classes)\n        )\n        \n        # 4. HELIX-style Gated Fusion\n        self.gate_logit = nn.Parameter(torch.tensor(0.0))\n\n    def forward(self, x):\n        features = self.backbone(x)                     # (B, 2048, 7, 7)\n        \n        # Classical Path\n        spatial_logits = self.spatial_head(features)\n        \n        # Spectral Path\n        spec_feat = self.spectral_proj(features)\n        spec_feat = self.spectral_blocks(spec_feat)\n        spectral_logits = self.spectral_head(spec_feat)\n        \n        # Gated Fusion\n        g = torch.sigmoid(self.gate_logit)\n        fused_logits = g * spectral_logits + (1.0 - g) * spatial_logits\n        \n        return fused_logits","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-18T05:44:46.258502Z","iopub.execute_input":"2026-04-18T05:44:46.258981Z","iopub.status.idle":"2026-04-18T05:44:46.268716Z","shell.execute_reply.started":"2026-04-18T05:44:46.258914Z","shell.execute_reply":"2026-04-18T05:44:46.268149Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Training Loop","metadata":{}},{"cell_type":"code","source":"def train_model():\n    device = CONFIG[\"device\"]\n    print(f\"Initializing Spectral-HELIX training on {device}...\")\n    \n    train_dataset = SIADataset(CONFIG[\"train_csv\"], transform=train_transforms, is_test=False)\n    train_loader = DataLoader(train_dataset, batch_size=CONFIG[\"batch_size\"], shuffle=True, num_workers=4, pin_memory=True)\n    \n    model = SpectralHELIX(CONFIG[\"num_classes\"]).to(device)\n    ema = EMA(model, CONFIG[\"ema_decay\"])\n    criterion = nn.CrossEntropyLoss(label_smoothing=0.05)\n    \n    # Differential Learning Rates (Backbone needs smaller LR than the new head)\n    optimizer = optim.AdamW([\n        {'params': model.backbone.parameters(), 'lr': CONFIG[\"lr_backbone\"]},\n        {'params': model.spectral_proj.parameters(), 'lr': CONFIG[\"lr_spectral\"]},\n        {'params': model.spectral_blocks.parameters(), 'lr': CONFIG[\"lr_spectral\"]},\n        {'params': model.spectral_head.parameters(), 'lr': CONFIG[\"lr_spectral\"]},\n        {'params': model.spatial_head.parameters(), 'lr': CONFIG[\"lr_spectral\"]},\n        {'params': [model.gate_logit], 'lr': CONFIG[\"lr_spectral\"]}\n    ], weight_decay=CONFIG[\"weight_decay\"])\n    \n    scheduler = optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=CONFIG[\"epochs\"])\n    \n    best_acc = 0.0\n    \n    for epoch in range(CONFIG[\"epochs\"]):\n        model.train()\n        running_loss = 0.0\n        \n        for images, targets in train_loader:\n            images, targets = images.to(device), targets.to(device)\n            \n            optimizer.zero_grad()\n            outputs = model(images)\n            loss = criterion(outputs, targets)\n            \n            loss.backward()\n            optimizer.step()\n            ema.update(model)\n            \n            running_loss += loss.item() * images.size(0)\n            \n        scheduler.step()\n        \n        # Evaluate on training set using EMA shadow model (for sanity check)\n        ema.shadow.eval()\n        correct, total = 0, 0\n        with torch.no_grad():\n            for images, targets in train_loader:\n                images, targets = images.to(device), targets.to(device)\n                outputs = ema.shadow(images)\n                _, predicted = outputs.max(1)\n                total += targets.size(0)\n                correct += predicted.eq(targets).sum().item()\n                \n        epoch_loss = running_loss / len(train_dataset)\n        epoch_acc = correct / total\n        \n        gate_val = torch.sigmoid(model.gate_logit).item()\n        \n        print(f\"Epoch [{epoch+1}/{CONFIG['epochs']}] - Loss: {epoch_loss:.4f} | EMA Train Acc: {epoch_acc:.4f} | Spectral_Gate: {gate_val:.3f}\")\n        \n        if epoch_acc > best_acc:\n            best_acc = epoch_acc\n            torch.save(ema.shadow.state_dict(), CONFIG[\"save_path\"])","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-18T05:44:46.366739Z","iopub.execute_input":"2026-04-18T05:44:46.367496Z","iopub.status.idle":"2026-04-18T05:44:46.377640Z","shell.execute_reply.started":"2026-04-18T05:44:46.367469Z","shell.execute_reply":"2026-04-18T05:44:46.376800Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Inference and Submission Generation","metadata":{}},{"cell_type":"code","source":"def generate_submission():\n    device = CONFIG[\"device\"]\n    print(\"\\nStarting Test Set Inference using Spectral-HELIX...\")\n    \n    test_dataset = SIADataset(CONFIG[\"test_csv\"], transform=test_transforms, is_test=True)\n    test_loader = DataLoader(test_dataset, batch_size=CONFIG[\"batch_size\"], shuffle=False, num_workers=4)\n    \n    model = SpectralHELIX(CONFIG[\"num_classes\"]).to(device)\n    model.load_state_dict(torch.load(CONFIG[\"save_path\"], map_location=device))\n    model.eval()\n    \n    predictions = []\n    \n    with torch.no_grad():\n        for images, img_ids in test_loader:\n            images = images.to(device)\n            outputs = model(images)\n            _, preds = torch.max(outputs, 1)\n            \n            for img_id, pred in zip(img_ids, preds):\n                predictions.append({\n                    \"ID\": img_id,\n                    \"TARGET\": pred.item()\n                })\n                \n    submission_df = pd.DataFrame(predictions)\n    submission_df = submission_df[[\"ID\", \"TARGET\"]] \n    submission_df.to_csv(CONFIG[\"submission_csv\"], index=False)\n    \n    print(f\"Submission saved successfully to {CONFIG['submission_csv']}\")\n    print(submission_df.head())\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-18T05:44:46.378801Z","iopub.execute_input":"2026-04-18T05:44:46.379100Z","iopub.status.idle":"2026-04-18T05:44:46.394485Z","shell.execute_reply.started":"2026-04-18T05:44:46.379078Z","shell.execute_reply":"2026-04-18T05:44:46.393690Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"\nif __name__ == \"__main__\":\n    if os.path.exists(CONFIG[\"train_csv\"]):\n        train_model()\n    else:\n        print(f\"Warning: {CONFIG['train_csv']} not found. Skipping training.\")\n        \n    if os.path.exists(CONFIG[\"test_csv\"]) and os.path.exists(CONFIG[\"save_path\"]):\n        generate_submission()\n    else:\n        print(f\"Warning: Ensure {CONFIG['test_csv']} and trained model exist to generate submission.\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-18T05:44:46.405544Z","iopub.execute_input":"2026-04-18T05:44:46.406169Z","iopub.status.idle":"2026-04-18T07:30:42.452855Z","shell.execute_reply.started":"2026-04-18T05:44:46.406145Z","shell.execute_reply":"2026-04-18T07:30:42.451971Z"}},"outputs":[],"execution_count":null}]}