{"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":"gpu","dataSources":[{"sourceId":113558,"databundleVersionId":14878066,"sourceType":"competition"}],"dockerImageVersionId":31236,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"import os\nimport cv2\nimport numpy as np\nimport torch\nimport torch.nn as nn\nimport torch.optim as optim\nfrom torch.utils.data import Dataset, DataLoader, random_split\nfrom torchvision.models.segmentation import deeplabv3_resnet50\nimport albumentations as A\nfrom albumentations.pytorch import ToTensorV2\nfrom tqdm import tqdm\n\n# --- 1. CONFIGURATION ---\nBASE_PATH = '/kaggle/input/recodai-luc-scientific-image-forgery-detection'\nTRAIN_IMG_PATH = os.path.join(BASE_PATH, 'train_images')\nTRAIN_MASK_PATH = os.path.join(BASE_PATH, 'train_masks')\nSUPP_IMG_PATH = os.path.join(BASE_PATH, 'supplemental_images')\nSUPP_MASK_PATH = os.path.join(BASE_PATH, 'supplemental_masks')\n\nIMG_SIZE = 512      \nBATCH_SIZE = 8      \nEPOCHS = 15\nLEARNING_RATE = 1e-4\nDEVICE = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\n\n# --- 2. AUGMENTATIONS ---\ntrain_transform = A.Compose([\n    A.Resize(IMG_SIZE, IMG_SIZE),\n    A.HorizontalFlip(p=0.5),\n    A.VerticalFlip(p=0.5),\n    A.Rotate(limit=90, p=0.5),\n    A.Normalize(mean=(0.485, 0.456, 0.406), std=(0.229, 0.224, 0.225)),\n    ToTensorV2()\n])\n\nval_transform = A.Compose([\n    A.Resize(IMG_SIZE, IMG_SIZE),\n    A.Normalize(mean=(0.485, 0.456, 0.406), std=(0.229, 0.224, 0.225)),\n    ToTensorV2()\n])\n\n# --- 3. DATASET CLASS ---\nclass ProForgeryDataset(Dataset):\n    def __init__(self, img_dirs, mask_dirs, transform=None):\n        self.transform = transform\n        self.images = []\n        for img_d, mask_d in zip(img_dirs, mask_dirs):\n            if os.path.exists(img_d):\n                for root, _, files in os.walk(img_d):\n                    for f in files:\n                        if f.lower().endswith(('.png', '.jpg', '.jpeg', '.tif', '.tiff')):\n                            self.images.append((os.path.join(root, f), f, mask_d))\n\n    def __len__(self):\n        return len(self.images)\n\n    def __getitem__(self, idx):\n        img_path, filename, mask_dir_path = self.images[idx]\n        image = cv2.imread(img_path)\n        if image is None: return self._get_empty_batch()\n        image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)\n        \n        file_id = filename.rsplit('.', 1)[0]\n        mask_path = os.path.join(mask_dir_path, file_id + \".npy\")\n        mask = np.zeros(image.shape[:2], dtype=np.float32)\n        \n        if os.path.exists(mask_path):\n            try:\n                m = np.load(mask_path)\n                if m.ndim == 3: m = np.max(m, axis=0) \n                if m.ndim == 3: m = np.max(m, axis=-1)\n                mask = (m > 0).astype(np.float32)\n            except: pass\n\n        if self.transform:\n            augmented = self.transform(image=image, mask=mask)\n            image = augmented['image']\n            mask = augmented['mask'].unsqueeze(0)\n            \n        return image, mask\n    \n    def _get_empty_batch(self):\n        return torch.zeros(3, IMG_SIZE, IMG_SIZE), torch.zeros(1, IMG_SIZE, IMG_SIZE)\n\n# --- 4. LOSS & IOU HELPER FUNCTIONS ---\ndef dice_loss(pred, target, smooth=1.0):\n    pred = torch.sigmoid(pred)\n    intersection = (pred * target).sum()\n    return 1 - (2. * intersection + smooth) / (pred.sum() + target.sum() + smooth)\n\ndef combined_loss(pred, target):\n    bce = nn.BCEWithLogitsLoss()(pred, target)\n    dice = dice_loss(pred, target)\n    return bce + dice\n\n# NEW: IOU Calculation Function\ndef calculate_iou(pred, target, smooth=1e-6):\n    pred = torch.sigmoid(pred)\n    # Convert probabilities to binary mask (0 or 1)\n    pred = (pred > 0.5).float()\n    \n    intersection = (pred * target).sum()\n    union = pred.sum() + target.sum() - intersection\n    \n    iou = (intersection + smooth) / (union + smooth)\n    return iou.item()\n\n# --- 5. SETUP ---\nfull_dataset = ProForgeryDataset(\n    img_dirs=[TRAIN_IMG_PATH, SUPP_IMG_PATH], \n    mask_dirs=[TRAIN_MASK_PATH, SUPP_MASK_PATH],\n    transform=train_transform\n)\n\ntrain_size = int(0.9 * len(full_dataset))\nval_size = len(full_dataset) - train_size\ntrain_ds, val_ds = random_split(full_dataset, [train_size, val_size])\nval_ds.dataset.transform = val_transform \n\ntrain_loader = DataLoader(train_ds, batch_size=BATCH_SIZE, shuffle=True, num_workers=2)\nval_loader = DataLoader(val_ds, batch_size=BATCH_SIZE, shuffle=False, num_workers=2)\n\nprint(\"Loading DeepLabV3 with ImageNet Weights...\")\ntry:\n    model = deeplabv3_resnet50(weights='DEFAULT')\n    print(\"✅ Loaded ImageNet Weights!\")\nexcept:\n    model = deeplabv3_resnet50(weights=None)\n\nmodel.classifier[4] = nn.Conv2d(256, 1, kernel_size=(1, 1), stride=(1, 1))\nmodel.aux_classifier[4] = nn.Conv2d(256, 1, kernel_size=(1, 1), stride=(1, 1))\n\nmodel = model.to(DEVICE)\noptimizer = optim.AdamW(model.parameters(), lr=LEARNING_RATE)\n\n# --- 6. TRAINING LOOP (With IoU Display) ---\nprint(f\"--- STARTING TRAINING (DeepLabV3 + IoU Tracking) ---\")\nbest_loss = float('inf')\n\nfor epoch in range(EPOCHS):\n    model.train()\n    epoch_loss = 0\n    epoch_iou = 0  # Accumulator for Training IoU\n    \n    loop = tqdm(train_loader, desc=f\"Epoch {epoch+1}/{EPOCHS}\")\n    \n    for images, masks in loop:\n        images, masks = images.to(DEVICE), masks.to(DEVICE)\n        \n        optimizer.zero_grad()\n        outputs = model(images)['out']\n        \n        loss = combined_loss(outputs, masks)\n        loss.backward()\n        optimizer.step()\n        \n        # Calculate Metric\n        iou = calculate_iou(outputs, masks)\n        \n        epoch_loss += loss.item()\n        epoch_iou += iou\n        \n        # Update Progress Bar with current IoU\n        loop.set_postfix(loss=loss.item(), iou=iou)\n        \n    avg_train_loss = epoch_loss / len(train_loader)\n    avg_train_iou = epoch_iou / len(train_loader)\n    \n    # Validation\n    model.eval()\n    val_loss = 0\n    val_iou = 0 # Accumulator for Validation IoU\n    \n    with torch.no_grad():\n        for images, masks in val_loader:\n            images, masks = images.to(DEVICE), masks.to(DEVICE)\n            outputs = model(images)['out']\n            \n            val_loss += combined_loss(outputs, masks).item()\n            val_iou += calculate_iou(outputs, masks)\n            \n    avg_val_loss = val_loss / len(val_loader)\n    avg_val_iou = val_iou / len(val_loader)\n    \n    # Print Full Stats\n    print(f\"Epoch {epoch+1} | Train IoU: {avg_train_iou:.4f} | Val Loss: {avg_val_loss:.4f} | Val IoU: {avg_val_iou:.4f}\")\n    \n    # Save Best Model (Based on Loss is still safer for stability)\n    if avg_val_loss < best_loss:\n        best_loss = avg_val_loss\n        torch.save(model.state_dict(), \"deeplab_best_model.pth\")\n        print(\"New Best Model Saved!\")\n\nprint(\"Training Complete.\")","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nimport cv2\nimport numpy as np\nimport torch\nimport torch.nn as nn\nimport torch.optim as optim\nfrom torch.utils.data import Dataset, DataLoader, random_split\nfrom torchvision.models.segmentation import deeplabv3_resnet50\nimport albumentations as A\nfrom albumentations.pytorch import ToTensorV2\nfrom tqdm import tqdm\n\n# --- 1. CONFIGURATION ---\nBASE_PATH = '/kaggle/input/recodai-luc-scientific-image-forgery-detection'\nTRAIN_IMG_PATH = os.path.join(BASE_PATH, 'train_images')\nTRAIN_MASK_PATH = os.path.join(BASE_PATH, 'train_masks')\nSUPP_IMG_PATH = os.path.join(BASE_PATH, 'supplemental_images')\nSUPP_MASK_PATH = os.path.join(BASE_PATH, 'supplemental_masks')\n\nIMG_SIZE = 512      \nBATCH_SIZE = 8      \nEPOCHS = 25              # INCREASED from 15 to 25\nLEARNING_RATE = 1e-4\nDEVICE = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\n\n# --- 2. AUGMENTATIONS ---\ntrain_transform = A.Compose([\n    A.Resize(IMG_SIZE, IMG_SIZE),\n    A.HorizontalFlip(p=0.5),\n    A.VerticalFlip(p=0.5),\n    A.Rotate(limit=90, p=0.5),\n    A.RandomBrightnessContrast(p=0.2), # NEW: Makes model robust to lighting\n    A.Normalize(mean=(0.485, 0.456, 0.406), std=(0.229, 0.224, 0.225)),\n    ToTensorV2()\n])\n\nval_transform = A.Compose([\n    A.Resize(IMG_SIZE, IMG_SIZE),\n    A.Normalize(mean=(0.485, 0.456, 0.406), std=(0.229, 0.224, 0.225)),\n    ToTensorV2()\n])\n\n# --- 3. DATASET CLASS ---\nclass ProForgeryDataset(Dataset):\n    def __init__(self, img_dirs, mask_dirs, transform=None):\n        self.transform = transform\n        self.images = []\n        for img_d, mask_d in zip(img_dirs, mask_dirs):\n            if os.path.exists(img_d):\n                for root, _, files in os.walk(img_d):\n                    for f in files:\n                        if f.lower().endswith(('.png', '.jpg', '.jpeg', '.tif', '.tiff')):\n                            self.images.append((os.path.join(root, f), f, mask_d))\n\n        if len(self.images) == 0:\n            raise RuntimeError(f\"❌ No images found in {img_dirs}\")\n\n    def __len__(self):\n        return len(self.images)\n\n    def __getitem__(self, idx):\n        img_path, filename, mask_dir_path = self.images[idx]\n        image = cv2.imread(img_path)\n        if image is None: return self._get_empty_batch()\n        image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)\n        \n        file_id = filename.rsplit('.', 1)[0]\n        mask_path = os.path.join(mask_dir_path, file_id + \".npy\")\n        mask = np.zeros(image.shape[:2], dtype=np.float32)\n        \n        if os.path.exists(mask_path):\n            try:\n                m = np.load(mask_path)\n                if m.ndim == 3: m = np.max(m, axis=0) \n                if m.ndim == 3: m = np.max(m, axis=-1)\n                mask = (m > 0).astype(np.float32)\n            except: pass\n\n        if self.transform:\n            augmented = self.transform(image=image, mask=mask)\n            image = augmented['image']\n            mask = augmented['mask'].unsqueeze(0)\n            \n        return image, mask\n    \n    def _get_empty_batch(self):\n        return torch.zeros(3, IMG_SIZE, IMG_SIZE), torch.zeros(1, IMG_SIZE, IMG_SIZE)\n\n# --- 4. LOSS & IOU ---\ndef dice_loss(pred, target, smooth=1.0):\n    pred = torch.sigmoid(pred)\n    intersection = (pred * target).sum()\n    return 1 - (2. * intersection + smooth) / (pred.sum() + target.sum() + smooth)\n\ndef combined_loss(pred, target):\n    bce = nn.BCEWithLogitsLoss()(pred, target)\n    dice = dice_loss(pred, target)\n    return bce + dice\n\ndef calculate_iou(pred, target, smooth=1e-6):\n    pred = torch.sigmoid(pred)\n    pred = (pred > 0.5).float()\n    intersection = (pred * target).sum()\n    union = pred.sum() + target.sum() - intersection\n    return (intersection + smooth) / (union + smooth)\n\n# --- 5. SETUP ---\nfull_dataset = ProForgeryDataset(\n    img_dirs=[TRAIN_IMG_PATH, SUPP_IMG_PATH], \n    mask_dirs=[TRAIN_MASK_PATH, SUPP_MASK_PATH],\n    transform=train_transform\n)\n\ntrain_size = int(0.9 * len(full_dataset))\nval_size = len(full_dataset) - train_size\ntrain_ds, val_ds = random_split(full_dataset, [train_size, val_size])\nval_ds.dataset.transform = val_transform \n\ntrain_loader = DataLoader(train_ds, batch_size=BATCH_SIZE, shuffle=True, num_workers=2)\nval_loader = DataLoader(val_ds, batch_size=BATCH_SIZE, shuffle=False, num_workers=2)\n\nprint(\"Loading DeepLabV3 with ImageNet Weights...\")\ntry:\n    model = deeplabv3_resnet50(weights='DEFAULT')\n    print(\"✅ Loaded ImageNet Weights!\")\nexcept:\n    model = deeplabv3_resnet50(weights=None)\n\nmodel.classifier[4] = nn.Conv2d(256, 1, kernel_size=(1, 1), stride=(1, 1))\nmodel.aux_classifier[4] = nn.Conv2d(256, 1, kernel_size=(1, 1), stride=(1, 1))\nmodel = model.to(DEVICE)\n\noptimizer = optim.AdamW(model.parameters(), lr=LEARNING_RATE)\n\n# --- NEW: SCHEDULER ---\n# This slowly reduces LR to help the model converge perfectly\nscheduler = optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=EPOCHS, eta_min=1e-6)\n\n# --- 6. TRAINING LOOP ---\nprint(f\"--- STARTING PRO TRAINING (25 Epochs + Scheduler) ---\")\nbest_iou = 0.0 # We track Best IoU now, it's a better metric than Loss\n\nfor epoch in range(EPOCHS):\n    model.train()\n    epoch_loss = 0\n    epoch_iou = 0\n    \n    loop = tqdm(train_loader, desc=f\"Epoch {epoch+1}/{EPOCHS}\")\n    \n    for images, masks in loop:\n        images, masks = images.to(DEVICE), masks.to(DEVICE)\n        \n        optimizer.zero_grad()\n        outputs = model(images)['out']\n        loss = combined_loss(outputs, masks)\n        \n        loss.backward()\n        # NEW: Clip Gradients (Prevents crashing if loss spikes)\n        torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=2.0)\n        \n        optimizer.step()\n        \n        iou = calculate_iou(outputs, masks).item()\n        epoch_loss += loss.item()\n        epoch_iou += iou\n        loop.set_postfix(loss=loss.item(), iou=iou, lr=optimizer.param_groups[0]['lr'])\n        \n    # Step the scheduler\n    scheduler.step()\n    \n    # Validation\n    model.eval()\n    val_loss = 0\n    val_iou = 0\n    with torch.no_grad():\n        for images, masks in val_loader:\n            images, masks = images.to(DEVICE), masks.to(DEVICE)\n            outputs = model(images)['out']\n            val_loss += combined_loss(outputs, masks).item()\n            val_iou += calculate_iou(outputs, masks).item()\n            \n    avg_val_loss = val_loss / len(val_loader)\n    avg_val_iou = val_iou / len(val_loader)\n    \n    print(f\"Epoch {epoch+1} | Val Loss: {avg_val_loss:.4f} | Val IoU: {avg_val_iou:.4f}\")\n    \n    # Save Best IoU Model (Better than Loss for Leaderboard)\n    if avg_val_iou > best_iou:\n        best_iou = avg_val_iou\n        torch.save(model.state_dict(), \"deeplab_pro_tuned.pth\")\n        print(f\"💾 New Best IoU Model Saved! ({best_iou:.4f})\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-21T05:03:06.266521Z","iopub.execute_input":"2025-12-21T05:03:06.266700Z","iopub.status.idle":"2025-12-21T10:58:49.461856Z","shell.execute_reply.started":"2025-12-21T05:03:06.266679Z","shell.execute_reply":"2025-12-21T10:58:49.461044Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}