{"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":"markdown","source":"# DLMMDD - ConvNeXt Tiny\n\n* **Goal:** Identify which of the 10 open-source text-to-image models generated a given image.\n* **Problem Type:** 10-class image classification (Closed-set).\n* **Evaluation Metric:** Accuracy\n* **Dataset:**\n    *  Total Images: 10,000 (1,000 per source model)\n    *  Training Set: 7,000 images (Labeled)\n    *  Test Set: 3,000 images (Unlabeled)\n    *  *Data Constraint: No redistribution or outside use allowed.*\n\nAll test images have been degraded with 1 to 3 random post-processing operations (e.g., JPEG/WebP/JPEG AI compression, cropping, resizing, rotation, contrast/brightness adjustments, Gaussian blur, grayscale, AI super-resolution). **The final model must be robust to these augmentations.**\n\n**Notebooks Ref.:**\n* https://www.kaggle.com/code/jek1wantaufik/dlmmdd-workshop-synthetic-image-attribution\n* https://www.kaggle.com/code/ambrosm/dlmmdd-baseline-with-convnext\n\n**Used AI assistants:**\n* ChatGPT / Gemini\n\n**Reference:**\n\nAndrea Montibeller, Barbara Corradini, Pietro Bongini, Sara Mandelli, and Simone Bonechi. DLMMDD Workshop: Synthetic Image Attribution. https://kaggle.com/competitions/dlmmdd-workshop-synthetic-source-attribution-challenge, 2026. Kaggle.","metadata":{}},{"cell_type":"markdown","source":"## Exploratory Data Analysis","metadata":{}},{"cell_type":"code","source":"import pandas as pd\n\ntrain_df = pd.read_csv(\"/kaggle/input/competitions/dlmmdd-workshop-synthetic-source-attribution-challenge/Data/Data/training.csv\")\ntest_df = pd.read_csv(\"/kaggle/input/competitions/dlmmdd-workshop-synthetic-source-attribution-challenge/Data/Data/test.csv\")\nsubmit_df = pd.read_csv(\"/kaggle/input/competitions/dlmmdd-workshop-synthetic-source-attribution-challenge/sample_submission.csv\")\n\ntrain_df.head()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-18T19:29:34.190737Z","iopub.execute_input":"2026-05-18T19:29:34.191174Z","iopub.status.idle":"2026-05-18T19:29:34.225979Z","shell.execute_reply.started":"2026-05-18T19:29:34.191125Z","shell.execute_reply":"2026-05-18T19:29:34.225454Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"labels_dict = dict()\nwith open(\"/kaggle/input/competitions/dlmmdd-workshop-synthetic-source-attribution-challenge/Data/Data/sources.txt\", \"r\") as f:\n    labels = f.read()\n    for line in labels.split(\"\\n\"):\n        idx, gen_model_name = line.split(\":\")\n        labels_dict[int(idx)] = gen_model_name.strip()\nprint(list(labels_dict.values()))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-18T19:29:34.496079Z","iopub.execute_input":"2026-05-18T19:29:34.496509Z","iopub.status.idle":"2026-05-18T19:29:34.503066Z","shell.execute_reply.started":"2026-05-18T19:29:34.496485Z","shell.execute_reply":"2026-05-18T19:29:34.502252Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"train_df[\"y\"].value_counts().plot(kind='bar')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-18T19:29:59.532070Z","iopub.execute_input":"2026-05-18T19:29:59.532404Z","iopub.status.idle":"2026-05-18T19:29:59.668528Z","shell.execute_reply.started":"2026-05-18T19:29:59.532376Z","shell.execute_reply":"2026-05-18T19:29:59.667884Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import matplotlib.pyplot as plt\nfrom PIL import Image\nimport random\nimport os\n\nRANDOM_SEED = 67 # for repoducibility\nbase_dir = \"/kaggle/input/competitions/dlmmdd-workshop-synthetic-source-attribution-challenge/Data\"\n\nrandom.seed(RANDOM_SEED)\n\n# take 1 sample per class\nsamples = []\n\nfor label_id, generator_name in labels_dict.items():\n    class_samples = train_df[train_df[\"y\"] == label_id]\n    sampled_row = class_samples.sample(n=1, random_state=RANDOM_SEED).iloc[0]\n    samples.append((label_id, generator_name, sampled_row))\n\n# 2 x 5 grid\nfig, axes = plt.subplots(2, 5, figsize=(10, 8))\naxes = axes.flatten()\n\nfor ax, (label_id, generator_name, row) in zip(axes, samples):\n    img_path = os.path.join(base_dir, row[\"path\"])\n    img = Image.open(img_path)\n    ax.imshow(img)\n    ax.set_title(f\"{label_id}: {generator_name}\", fontsize=10)\n    ax.axis(\"off\")\n\nfor i in range(len(samples), len(axes)):\n    axes[i].axis(\"off\")\n\nplt.tight_layout()\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-18T18:44:10.688835Z","iopub.execute_input":"2026-05-18T18:44:10.689286Z","iopub.status.idle":"2026-05-18T18:44:12.020097Z","shell.execute_reply.started":"2026-05-18T18:44:10.689254Z","shell.execute_reply":"2026-05-18T18:44:12.019342Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Config","metadata":{}},{"cell_type":"code","source":"import os\nimport gc\nimport random\nimport numpy as np\nimport pandas as pd\nfrom PIL import Image\nfrom tqdm.auto import tqdm\n\nimport torch\nimport torch.nn as nn\nfrom torch.utils.data import Dataset, DataLoader\n\nfrom torchvision import transforms\nfrom torchvision.models import convnext_tiny\nfrom torchvision.models import ConvNeXt_Tiny_Weights\n\nfrom sklearn.model_selection import StratifiedKFold\nfrom sklearn.metrics import accuracy_score\n\nclass CFG:\n    root = \"/kaggle/input/competitions/dlmmdd-workshop-synthetic-source-attribution-challenge/Data/\"\n    train_csv = os.path.join(root, \"Data\", \"training.csv\")\n    test_csv = os.path.join(root, \"Data\", \"test.csv\")\n    train_dir = os.path.join(root, \"Training\")\n    test_dir = os.path.join(root, \"Test\")\n    output_dir = \"./\"\n    seed = 67\n    device = \"cuda\" if torch.cuda.is_available() else \"cpu\"\n    num_classes = 10\n    image_size = 224\n    batch_size = 64\n    num_workers = 0\n    epochs = 12\n    lr = 1e-4\n    weight_decay = 1e-4\n    folds = 5\n    use_amp = True","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-18T19:02:28.857588Z","iopub.execute_input":"2026-05-18T19:02:28.858589Z","iopub.status.idle":"2026-05-18T19:02:28.866372Z","shell.execute_reply.started":"2026-05-18T19:02:28.858546Z","shell.execute_reply":"2026-05-18T19:02:28.865540Z"},"_kg_hide-input":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def seed_everything(seed):\n    random.seed(seed)\n    np.random.seed(seed)\n\n    torch.manual_seed(seed)\n    torch.cuda.manual_seed(seed)\n    torch.cuda.manual_seed_all(seed)\n\n    os.environ[\"PYTHONHASHSEED\"] = str(seed)\n\n    torch.backends.cudnn.deterministic = True\n    torch.backends.cudnn.benchmark = True\n\nseed_everything(CFG.seed)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-18T19:02:29.828312Z","iopub.execute_input":"2026-05-18T19:02:29.828594Z","iopub.status.idle":"2026-05-18T19:02:29.835347Z","shell.execute_reply.started":"2026-05-18T19:02:29.828572Z","shell.execute_reply":"2026-05-18T19:02:29.834523Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## DATASET","metadata":{}},{"cell_type":"code","source":"train_df = pd.read_csv(CFG.train_csv)\ntest_df = pd.read_csv(CFG.test_csv)\nprint(train_df.shape)\nprint(test_df.shape)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-18T19:02:30.010281Z","iopub.execute_input":"2026-05-18T19:02:30.010717Z","iopub.status.idle":"2026-05-18T19:02:30.037358Z","shell.execute_reply.started":"2026-05-18T19:02:30.010663Z","shell.execute_reply":"2026-05-18T19:02:30.036634Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"class SyntheticDataset(Dataset):\n    def __init__(self, df, transforms=None, train=True):\n        self.df = df.reset_index(drop=True)\n        self.transforms = transforms\n        self.train = train\n\n    def __len__(self):\n        return len(self.df)\n\n    def __getitem__(self, idx):\n        row = self.df.iloc[idx]\n        img_path = os.path.join(CFG.root, row[\"path\"])\n        image = Image.open(img_path).convert(\"RGB\")\n        if self.transforms:\n            image = self.transforms(image)\n        if self.train:\n            label = int(row[\"y\"])\n            return image, label\n        return image, int(row[\"ID\"])","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-18T19:02:30.148803Z","iopub.execute_input":"2026-05-18T19:02:30.149463Z","iopub.status.idle":"2026-05-18T19:02:30.154421Z","shell.execute_reply.started":"2026-05-18T19:02:30.149434Z","shell.execute_reply":"2026-05-18T19:02:30.153714Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## MixUp / CutMix utilities","metadata":{}},{"cell_type":"code","source":"# -------------------------\n# MixUp\n# -------------------------\ndef mixup_data(x, y, alpha=0.4):\n    if alpha > 0:\n        lam = np.random.beta(alpha, alpha)\n    else:\n        lam = 1\n    batch_size = x.size(0)\n    index = torch.randperm(batch_size).to(x.device)\n    mixed_x = lam * x + (1 - lam) * x[index]\n    y_a, y_b = y, y[index]\n    return mixed_x, y_a, y_b, lam\n\n\n# -------------------------\n# CutMix\n# -------------------------\ndef cutmix_data(x, y, alpha=1.0):\n    if alpha > 0:\n        lam = np.random.beta(alpha, alpha)\n    else:\n        lam = 1\n    batch_size, C, H, W = x.size()\n    index = torch.randperm(batch_size).to(x.device)\n    # bounding box\n    cx = np.random.randint(W)\n    cy = np.random.randint(H)\n    bw = int(W * np.sqrt(1 - lam))\n    bh = int(H * np.sqrt(1 - lam))\n    x1 = np.clip(cx - bw // 2, 0, W)\n    x2 = np.clip(cx + bw // 2, 0, W)\n    y1 = np.clip(cy - bh // 2, 0, H)\n    y2 = np.clip(cy + bh // 2, 0, H)\n    x[:, :, y1:y2, x1:x2] = x[index, :, y1:y2, x1:x2]\n    lam = 1 - ((x2 - x1) * (y2 - y1) / (W * H))\n    y_a, y_b = y, y[index]\n    return x, y_a, y_b, lam\n\ndef apply_mixup_cutmix(x, y, mixup_alpha=0.4, cutmix_alpha=1.0, prob=0.5):\n    if np.random.rand() < prob:\n        return mixup_data(x, y, mixup_alpha)\n    else:\n        return cutmix_data(x, y, cutmix_alpha)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-18T19:02:30.299077Z","iopub.execute_input":"2026-05-18T19:02:30.299493Z","iopub.status.idle":"2026-05-18T19:02:30.307988Z","shell.execute_reply.started":"2026-05-18T19:02:30.299469Z","shell.execute_reply":"2026-05-18T19:02:30.307232Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## TRANSFORMS","metadata":{}},{"cell_type":"code","source":"mean = (0.485, 0.456, 0.406)\nstd  = (0.229, 0.224, 0.225)\n\ntrain_transforms = transforms.Compose([\n    transforms.RandomResizedCrop(224, scale=(0.8, 1.0)),\n    transforms.RandomHorizontalFlip(),\n    transforms.RandomApply([\n        transforms.ColorJitter(0.2, 0.2, 0.2, 0.1)\n    ], p=0.7),\n    transforms.ToTensor(),\n    transforms.Normalize(mean, std)\n])\n\nvalid_transforms = transforms.Compose([\n    transforms.Resize((CFG.image_size, CFG.image_size)),\n    transforms.ToTensor(),\n    transforms.Normalize(mean, std)\n])","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-18T19:02:30.441652Z","iopub.execute_input":"2026-05-18T19:02:30.442285Z","iopub.status.idle":"2026-05-18T19:02:30.447584Z","shell.execute_reply.started":"2026-05-18T19:02:30.442262Z","shell.execute_reply":"2026-05-18T19:02:30.447008Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## MODEL","metadata":{}},{"cell_type":"code","source":"class ConvNeXtTinyModel(nn.Module):\n    def __init__(self, num_classes=10):\n        super().__init__()\n        self.backbone = convnext_tiny(\n            weights=ConvNeXt_Tiny_Weights.DEFAULT\n        )\n        in_features = self.backbone.classifier[2].in_features\n        self.backbone.classifier[2] = nn.Sequential(\n            nn.Dropout(0.2),\n            nn.Linear(in_features, num_classes)\n        )\n    def forward(self, x):\n        return self.backbone(x)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-18T19:02:30.561943Z","iopub.execute_input":"2026-05-18T19:02:30.562494Z","iopub.status.idle":"2026-05-18T19:02:30.566760Z","shell.execute_reply.started":"2026-05-18T19:02:30.562470Z","shell.execute_reply":"2026-05-18T19:02:30.566224Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## TRAIN FUNCTION","metadata":{}},{"cell_type":"code","source":"def train_one_epoch(model, loader, optimizer, criterion, scaler):\n\n    model.train()\n    running_loss = 0\n\n    for images, labels in tqdm(loader):\n\n        images = images.to(CFG.device)\n        labels = labels.to(CFG.device)\n\n        optimizer.zero_grad()\n\n        # =========================\n        # APPLY MIXUP / CUTMIX\n        # =========================\n        images, y_a, y_b, lam = apply_mixup_cutmix(\n            images, labels,\n            mixup_alpha=0.4,\n            cutmix_alpha=1.0,\n            prob=0.5\n        )\n\n        with torch.amp.autocast(device_type=\"cuda\", enabled=CFG.use_amp):\n            outputs = model(images)\n\n            loss = lam * criterion(outputs, y_a) + (1 - lam) * criterion(outputs, y_b)\n\n        scaler.scale(loss).backward()\n        scaler.step(optimizer)\n        scaler.update()\n\n        running_loss += loss.item()\n\n    return running_loss / len(loader)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-18T19:02:31.868711Z","iopub.execute_input":"2026-05-18T19:02:31.869518Z","iopub.status.idle":"2026-05-18T19:02:31.875272Z","shell.execute_reply.started":"2026-05-18T19:02:31.869486Z","shell.execute_reply":"2026-05-18T19:02:31.874523Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## VALID FUNCTION","metadata":{}},{"cell_type":"code","source":"def valid_one_epoch(\n    model,\n    loader,\n    criterion\n):\n    model.eval()\n    running_loss = 0\n    preds = []\n    targets = []\n    with torch.no_grad():\n        for images, labels in tqdm(loader):\n            images = images.to(CFG.device)\n            labels = labels.to(CFG.device)\n            outputs = model(images)\n            loss = criterion(outputs, labels)\n            running_loss += loss.item()\n            predictions = outputs.argmax(1)\n            preds.extend(\n                predictions.cpu().numpy()\n            )\n            targets.extend(\n                labels.cpu().numpy()\n            )\n    acc = accuracy_score(targets, preds)\n    return running_loss / len(loader), acc","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-18T19:02:32.129927Z","iopub.execute_input":"2026-05-18T19:02:32.130643Z","iopub.status.idle":"2026-05-18T19:02:32.135863Z","shell.execute_reply.started":"2026-05-18T19:02:32.130614Z","shell.execute_reply":"2026-05-18T19:02:32.135178Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## INFERENCE","metadata":{}},{"cell_type":"code","source":"def predict(model, loader):\n    model.eval()\n    preds = []\n    ids = []\n    with torch.no_grad():\n        for images, batch_ids in tqdm(loader):\n            images = images.to(CFG.device)\n            outputs = model(images)\n            predictions = outputs.argmax(1)\n            preds.extend(\n                predictions.cpu().numpy()\n            )\n            ids.extend(batch_ids.numpy())\n    return ids, preds","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-18T19:02:33.938379Z","iopub.execute_input":"2026-05-18T19:02:33.939067Z","iopub.status.idle":"2026-05-18T19:02:33.943880Z","shell.execute_reply.started":"2026-05-18T19:02:33.939036Z","shell.execute_reply":"2026-05-18T19:02:33.943012Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## CROSS VALIDATION TRAINING","metadata":{}},{"cell_type":"code","source":"skf = StratifiedKFold(\n    n_splits=CFG.folds,\n    shuffle=True,\n    random_state=CFG.seed\n)\n\noof_predictions = np.zeros(len(train_df))\n\nbest_models = []\n\nfor fold, (train_idx, valid_idx) in enumerate(\n    skf.split(train_df, train_df[\"y\"])\n):\n\n    print(\"=\" * 50)\n    print(f\"FOLD {fold}\")\n    print(\"=\" * 50)\n\n    train_fold = train_df.iloc[train_idx]\n    valid_fold = train_df.iloc[valid_idx]\n\n    train_dataset = SyntheticDataset(\n        train_fold,\n        transforms=train_transforms,\n        train=True\n    )\n\n    valid_dataset = SyntheticDataset(\n        valid_fold,\n        transforms=valid_transforms,\n        train=True\n    )\n\n    train_loader = DataLoader(\n        train_dataset,\n        batch_size=CFG.batch_size,\n        shuffle=True,\n        num_workers=CFG.num_workers,\n        pin_memory=True\n    )\n\n    valid_loader = DataLoader(\n        valid_dataset,\n        batch_size=CFG.batch_size,\n        shuffle=False,\n        num_workers=CFG.num_workers,\n        pin_memory=True\n    )\n\n    model = ConvNeXtTinyModel(\n        num_classes=CFG.num_classes\n    ).to(CFG.device)\n\n    criterion = nn.CrossEntropyLoss()\n\n    optimizer = torch.optim.AdamW(\n        model.parameters(),\n        lr=CFG.lr,\n        weight_decay=CFG.weight_decay\n    )\n\n    scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(\n        optimizer,\n        T_max=CFG.epochs\n    )\n\n    scaler = torch.amp.GradScaler()\n    \n    best_acc = 0\n\n    for epoch in range(CFG.epochs):\n\n        print(f\"\\nEpoch {epoch+1}/{CFG.epochs}\")\n\n        train_loss = train_one_epoch(\n            model,\n            train_loader,\n            optimizer,\n            criterion,\n            scaler\n        )\n\n\n        valid_loss, valid_acc = valid_one_epoch(\n            model,\n            valid_loader,\n            criterion\n        )\n\n        scheduler.step()\n\n        print(f\"Train Loss: {train_loss:.4f} | Valid Loss: {valid_loss:.4f} | Valid Acc: {valid_acc:.4f}\")\n\n        if valid_acc > best_acc:\n\n            best_acc = valid_acc\n\n            save_path = f\"convnext_fold_{fold}.pth\"\n\n            torch.save(\n                model.state_dict(),\n                save_path\n            )\n\n            print(f\"Saved best model to {save_path}\")\n\n    best_models.append(\n        f\"convnext_fold_{fold}.pth\"\n    )\n\n    del model\n    gc.collect()\n    torch.cuda.empty_cache()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-18T19:02:34.228826Z","iopub.execute_input":"2026-05-18T19:02:34.229057Z","iopub.status.idle":"2026-05-18T19:29:11.295869Z","shell.execute_reply.started":"2026-05-18T19:02:34.229035Z","shell.execute_reply":"2026-05-18T19:29:11.294349Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## TEST DATASET","metadata":{}},{"cell_type":"code","source":"test_dataset = SyntheticDataset(\n    test_df,\n    transforms=valid_transforms,\n    train=False\n)\n\ntest_loader = DataLoader(\n    test_dataset,\n    batch_size=CFG.batch_size,\n    shuffle=False,\n    num_workers=CFG.num_workers,\n    pin_memory=True\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-18T19:29:11.296805Z","iopub.status.idle":"2026-05-18T19:29:11.297349Z","shell.execute_reply.started":"2026-05-18T19:29:11.297143Z","shell.execute_reply":"2026-05-18T19:29:11.297197Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## ENSEMBLE INFERENCE","metadata":{}},{"cell_type":"code","source":"all_fold_predictions = []\n\nfor fold in range(CFG.folds):\n\n    print(f\"\\nInference Fold {fold}\")\n\n    model = ConvNeXtTinyModel(\n        num_classes=CFG.num_classes\n    ).to(CFG.device)\n\n    model.load_state_dict(\n        torch.load(\n            f\"convnext_fold_{fold}.pth\",\n            map_location=CFG.device\n        )\n    )\n\n    model.eval()\n\n    fold_probs = []\n\n    with torch.no_grad():\n\n        for images, _ in tqdm(test_loader):\n\n            images = images.to(CFG.device)\n\n            outputs = model(images)\n\n            probs = torch.softmax(\n                outputs,\n                dim=1\n            )\n\n            fold_probs.append(\n                probs.cpu().numpy()\n            )\n\n    fold_probs = np.concatenate(\n        fold_probs,\n        axis=0\n    )\n\n    all_fold_predictions.append(fold_probs)\n\n    del model\n    gc.collect()\n    torch.cuda.empty_cache()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-18T19:29:11.299056Z","iopub.status.idle":"2026-05-18T19:29:11.299552Z","shell.execute_reply.started":"2026-05-18T19:29:11.299352Z","shell.execute_reply":"2026-05-18T19:29:11.299380Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## SUBMISSION","metadata":{}},{"cell_type":"code","source":"final_probs = np.mean(\n    all_fold_predictions,\n    axis=0\n)\n\nfinal_predictions = final_probs.argmax(1)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-18T19:29:11.300574Z","iopub.status.idle":"2026-05-18T19:29:11.301211Z","shell.execute_reply.started":"2026-05-18T19:29:11.301019Z","shell.execute_reply":"2026-05-18T19:29:11.301042Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Distribution of predicted classes\npred_dist = pd.Series(final_predictions).value_counts().sort_index()\n\n# Plot\nplt.figure(figsize=(10, 5))\nbars = plt.bar(pred_dist.index.astype(str), pred_dist.values)\n\nplt.title(\"Final Predictions Distribution\")\nplt.xlabel(\"Predicted Class\")\nplt.ylabel(\"Count\")\nplt.grid(axis=\"y\", linestyle=\"--\", alpha=0.4)\n\n# Add value labels on top\nfor bar in bars:\n    height = bar.get_height()\n    plt.text(\n        bar.get_x() + bar.get_width() / 2,\n        height,\n        f\"{int(height)}\",\n        ha=\"center\",\n        va=\"bottom\",\n        fontsize=9\n    )\n\nplt.tight_layout()\nplt.show()","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"submission = pd.DataFrame({\n    \"ID\": test_df[\"ID\"],\n    \"TARGET\": final_predictions\n})\n\nsubmission.to_csv(\n    \"submission.csv\",\n    index=False\n)\n\nprint(\"\\nSubmission saved!\")\n\nprint(submission.head())","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-18T19:29:11.302262Z","iopub.status.idle":"2026-05-18T19:29:11.302550Z","shell.execute_reply.started":"2026-05-18T19:29:11.302416Z","shell.execute_reply":"2026-05-18T19:29:11.302443Z"}},"outputs":[],"execution_count":null}]}