{"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,"isSourceIdPinned":false}],"dockerImageVersionId":31328,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"import numpy as np # linear algebra\nimport pandas as pd # data processing, CSV file I/O (e.g. pd.read_csv)\nimport os\nimport matplotlib.pyplot as plt\nfrom PIL import Image, ImageFilter, ImageEnhance, ImageOps\nimport time\nimport math","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2026-05-24T20:31:11.083096Z","iopub.execute_input":"2026-05-24T20:31:11.083908Z","iopub.status.idle":"2026-05-24T20:31:11.088109Z","shell.execute_reply.started":"2026-05-24T20:31:11.083876Z","shell.execute_reply":"2026-05-24T20:31:11.087317Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"from sklearn.model_selection import StratifiedKFold, train_test_split\nfrom sklearn.preprocessing import LabelEncoder\nimport torch\nimport torch.nn as nn\nimport torch.optim as optim\nfrom torch.utils.data import Dataset, DataLoader\nfrom torchvision import transforms, models\nfrom torch.optim.lr_scheduler import CosineAnnealingLR\nfrom torch.cuda.amp import autocast, GradScaler\nfrom torchvision.transforms import functional as F\nimport io\nfrom tqdm import tqdm\nimport warnings\nwarnings.filterwarnings(\"ignore\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-24T20:31:11.089609Z","iopub.execute_input":"2026-05-24T20:31:11.089945Z","iopub.status.idle":"2026-05-24T20:31:11.106768Z","shell.execute_reply.started":"2026-05-24T20:31:11.089924Z","shell.execute_reply":"2026-05-24T20:31:11.106083Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"class Config:\n    Base_path = \"/kaggle/input/competitions/dlmmdd-workshop-synthetic-source-attribution-challenge/Data/Data\"\n    Working_dir = \"/kaggle/working\"\n    train_dir = os.path.join(Base_path,\"Training\")\n    test_dir = os.path.join(Base_path, \"Test\")\n    train_csv = os.path.join(Base_path, \"training.csv\")\n    test_csv = os.path.join(Base_path, \"test.csv\")\n\n    IMG_SIZE    = 320\n    BATCH_SIZE  = 32\n    EPOCHS      = 15\n    LR          = 3e-4\n    NUM_FOLDS   = 5\n    SEED        = 42\n    DEVICE      = \"cuda\" if torch.cuda.is_available() else \"cpu\"\n\n    #Simulated Annealing\n    SA_INIT_TEMP    = 1.0       # initial temp\n    SA_MIN_TEMP     = 1e-4      # min temp\n    SA_COOLING      = 0.995     # cooling rate\n    SA_MAX_ITER     = 1000     # max iter\n    SA_STEP_SIZE    = 0.05      # neighbor size\n\ncfg = Config()\ntorch.manual_seed(cfg.SEED)\nnp.random.seed(cfg.SEED)\nscaler = GradScaler()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-24T20:31:11.135932Z","iopub.execute_input":"2026-05-24T20:31:11.136397Z","iopub.status.idle":"2026-05-24T20:31:11.143912Z","shell.execute_reply.started":"2026-05-24T20:31:11.136376Z","shell.execute_reply":"2026-05-24T20:31:11.142891Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def load_data():\n    train_df = pd.read_csv(cfg.train_csv)\n    test_df  = pd.read_csv(cfg.test_csv)\n\n    print(f\"\\nTrain : {train_df.shape}  |  Test : {test_df.shape}\")\n    print(f\"Class distribution:\\n{train_df['y'].value_counts()}\\n\")\n\n    le = LabelEncoder()\n    train_df[\"label\"] = le.fit_transform(train_df[\"y\"])\n    num_classes = len(le.classes_)\n    print(f\"Class number : {num_classes}\")\n    print(f\"Label Map : { {i: c for i, c in enumerate(le.classes_)} }\")\n\n    return train_df, test_df, le, num_classes","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-24T20:31:11.145342Z","iopub.execute_input":"2026-05-24T20:31:11.145656Z","iopub.status.idle":"2026-05-24T20:31:11.156350Z","shell.execute_reply.started":"2026-05-24T20:31:11.145633Z","shell.execute_reply":"2026-05-24T20:31:11.155586Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"class SyntheticDataset(Dataset):\n    def __init__(self, df, img_dir, transform=None, is_test=False):\n        self.data = df.reset_index(drop=True)\n        self.img_dir = img_dir\n        self.transform = transform\n        self.is_test = is_test\n\n    def __len__(self):\n        return len(self.data)\n\n    def __getitem__(self, idx):\n        row = self.data.iloc[idx]\n        img_name = os.path.basename(row['path']) \n        img_path = os.path.join(self.img_dir, img_name)\n        \n        try:\n            image = Image.open(img_path).convert(\"RGB\")\n        except FileNotFoundError:\n            # error handling\n            raise FileNotFoundError(f\"Resim bulunamadı! Denenen Yol: {img_path}\")\n    \n        if self.transform:\n            image = self.transform(image)\n            \n        if self.is_test:\n            return image, row['ID']\n        else:\n            return image, row['y']\n        ","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-24T20:31:11.196775Z","iopub.execute_input":"2026-05-24T20:31:11.196976Z","iopub.status.idle":"2026-05-24T20:31:11.203132Z","shell.execute_reply.started":"2026-05-24T20:31:11.196957Z","shell.execute_reply":"2026-05-24T20:31:11.202309Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"Because there is post-processing made in the test data set, we implemented same processings in the traning.","metadata":{}},{"cell_type":"code","source":"class RandomPostProcess:\n    \"\"\"It simulates the post-processing in the competition.\"\"\"\n    def __init__(self, target_size=320):\n        self.target_size = target_size\n    \n    def __call__(self, img):\n        ops = [\n            self.jpeg_compress,\n            self.webp_compress,\n            self.gaussian_blur,\n            self.grayscale,\n            self.brightness_contrast,\n            self.random_central_crop,\n            self.resizing,\n            self.small_rotation_aspect_preserve,\n            self.ai_super_resolution,\n            self.jpeg_ai_compress\n        ]\n        # apply 1 to 3 different processings\n        k = np.random.randint(1, 4)\n        chosen = np.random.choice(ops, size=k, replace=False)\n        for op in chosen:\n            img = op(img)\n        img = img.resize((self.target_size, self.target_size), resample=Image.Resampling.BICUBIC)\n        return img\n\n    def jpeg_compress(self, img):\n        quality = np.random.randint(30, 95)\n        buf = io.BytesIO()\n        img.save(buf, format=\"JPEG\", quality=quality)\n        buf.seek(0)\n        return Image.open(buf).convert(\"RGB\")\n\n    def webp_compress(self, img):\n        quality = np.random.randint(30, 95)\n        buf = io.BytesIO()\n        img.save(buf, format=\"WEBP\", quality=quality)\n        buf.seek(0)\n        return Image.open(buf).convert(\"RGB\")\n\n    def gaussian_blur(self, img):\n        radius = np.random.uniform(0.5, 2.0)\n        return img.filter(ImageFilter.GaussianBlur(radius=radius))\n\n    def grayscale(self, img):\n        if np.random.random() < 0.3:\n            img = img.convert(\"L\").convert(\"RGB\")\n        return img\n\n    def brightness_contrast(self, img):\n        from PIL import ImageEnhance\n        brightness = np.random.uniform(0.7, 1.3)\n        contrast   = np.random.uniform(0.7, 1.3)\n        img = ImageEnhance.Brightness(img).enhance(brightness)\n        img = ImageEnhance.Contrast(img).enhance(contrast)\n        return img\n\n    def random_central_crop(self, img):\n        w, h = img.size\n        crop_ratio = np.random.uniform(0.8, 0.95)\n        new_w, new_h = int(w * crop_ratio), int(h * crop_ratio)\n        left = (w - new_w) // 2\n        top = (h - new_h) // 2\n        return img.crop((left, top, left + new_w, top + new_h))\n\n    def resizing(self, img):\n        scale = np.random.uniform(0.5, 1.5)\n        w, h = img.size\n        new_w, new_h = int(w * scale), int(h * scale)\n        interp = np.random.choice([Image.Resampling.BILINEAR, Image.Resampling.BICUBIC, Image.Resampling.LANCZOS])\n        return img.resize((new_w, new_h), resample=interp)\n\n    def small_rotation_aspect_preserve(self, img):\n        angle = np.random.uniform(-10, 10)\n        rotated = img.rotate(angle, resample=Image.Resampling.BICUBIC, expand=False)\n        w, h = img.size\n        rad = np.deg2rad(abs(angle))\n        crop_ratio = 1.0 / (np.cos(rad) + np.sin(rad))\n        new_w, new_h = int(w * crop_ratio), int(h * crop_ratio)\n        left = (w - new_w) // 2\n        top = (h - new_h) // 2\n        return rotated.crop((left, top, left + new_w, top + new_h))\n\n    def ai_super_resolution(self, img):\n        w, h = img.size\n        downscaled = img.resize((w // 2, h // 2), resample=Image.Resampling.BILINEAR)\n        return downscaled.resize((w, h), resample=Image.Resampling.LANCZOS)\n\n    def jpeg_ai_compress(self, img):\n        q_factor = np.random.uniform(0.1, 0.5)\n        w, h = img.size\n        small = img.resize((int(w * q_factor), int(h * q_factor)), resample=Image.Resampling.BICUBIC)\n        buf = io.BytesIO()\n        small.save(buf, format=\"JPEG\", quality=85)\n        buf.seek(0)\n        compressed = Image.open(buf)\n        return compressed.resize((w, h), resample=Image.Resampling.LANCZOS)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-24T20:31:11.232676Z","iopub.execute_input":"2026-05-24T20:31:11.232882Z","iopub.status.idle":"2026-05-24T20:31:11.248082Z","shell.execute_reply.started":"2026-05-24T20:31:11.232865Z","shell.execute_reply":"2026-05-24T20:31:11.247027Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"#Model def:\ndef build_efficientnet_b3 (num_classes: int):\n    \"\"\"EfficientNet-B3 — strong baseline\"\"\"\n    m = models.efficientnet_b3(weights = \"IMAGENET1K_V1\")\n    in_f = m.classifier[1].in_features\n    m.classifier = nn.Sequential(\n        nn.Dropout (p = 0.3),\n        nn.Linear (in_f, num_classes)\n    )\n    return m.to(cfg.DEVICE)\n\ndef build_convnext_tiny (num_classes: int):\n    \"\"\"ConvNeXt-Tiny — modern ConvNet; strong representation capacity\"\"\"\n    m = models.convnext_tiny(weights=\"IMAGENET1K_V1\")\n    in_f = m.classifier[2].in_features\n    m.classifier[2] = nn.Sequential(\n        nn.Dropout(p=0.3),\n        nn.Linear(in_f, num_classes)\n    )\n    return m.to(cfg.DEVICE)\n\nMODEL_BUILDERS = {\n    \"efficientnet_b3\": build_efficientnet_b3,\n    \"convnext_tiny\":   build_convnext_tiny,\n}","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-24T20:31:11.249279Z","iopub.execute_input":"2026-05-24T20:31:11.249951Z","iopub.status.idle":"2026-05-24T20:31:11.266127Z","shell.execute_reply.started":"2026-05-24T20:31:11.249927Z","shell.execute_reply":"2026-05-24T20:31:11.265369Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def get_transforms(is_train = True):\n    mean = [0.485, 0.456, 0.406]\n    std = [0.229, 0.224, 0.225]\n\n    if is_train:\n        return transforms.Compose([\n            transforms.Resize((cfg.IMG_SIZE, cfg.IMG_SIZE)),\n            RandomPostProcess(target_size=cfg.IMG_SIZE),\n            transforms.RandomHorizontalFlip(p=0.5),\n            transforms.ColorJitter(brightness=0.2, contrast=0.2),\n            transforms.ToTensor(),\n            transforms.Normalize(mean, std),\n        ])\n    return transforms.Compose([\n        transforms.Resize((cfg.IMG_SIZE, cfg.IMG_SIZE), interpolation=transforms.InterpolationMode.BICUBIC),\n        transforms.ToTensor(),\n        transforms.Normalize(mean, std),\n    ])","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-24T20:31:11.267335Z","iopub.execute_input":"2026-05-24T20:31:11.267628Z","iopub.status.idle":"2026-05-24T20:31:11.280870Z","shell.execute_reply.started":"2026-05-24T20:31:11.267609Z","shell.execute_reply":"2026-05-24T20:31:11.280079Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"#train and val\ndef train_one_epoch(model, loader, optimizer, criterion):\n    model.train()\n    total_loss = correct = total = 0\n    for imgs, labels in tqdm(loader, desc=\"   train\", leave=False):\n        imgs, labels = imgs.to(cfg.DEVICE), labels.to(cfg.DEVICE)\n        optimizer.zero_grad()\n\n        with autocast():\n            out  = model(imgs)\n            loss = criterion(out, labels)\n\n        scaler.scale(loss).backward()\n        scaler.step(optimizer)\n        scaler.update()\n        \n        total_loss += loss.item() * imgs.size(0)\n        correct    += (out.argmax(1) == labels).sum().item()\n        total      += imgs.size(0)\n    return total_loss / total, correct / total\n\n\n@torch.no_grad()\ndef evaluate(model, loader, criterion):\n    model.eval()\n    total_loss = correct = total = 0\n    for imgs, labels in tqdm(loader, desc=\"   val  \", leave=False):\n        imgs, labels = imgs.to(cfg.DEVICE), labels.to(cfg.DEVICE)\n        out  = model(imgs)\n        loss = criterion(out, labels)\n        total_loss += loss.item() * imgs.size(0)\n        correct    += (out.argmax(1) == labels).sum().item()\n        total      += imgs.size(0)\n    return total_loss / total, correct / total\n\n\n@torch.no_grad()\ndef get_probs(model, loader, is_test=False):\n    \"\"\"Softmax prob matrix.\n       is_test=True  → (probs, ids)\n       is_test=False → probs\n    \"\"\"\n    model.eval()\n    all_probs, all_ids = [], []\n    for batch in tqdm(loader, desc=\"   infer\", leave=False):\n        if is_test:\n            imgs, ids = batch\n        else:\n            imgs, _ = batch\n            ids = None\n        imgs  = imgs.to(cfg.DEVICE)\n        probs = torch.softmax(model(imgs), dim=1).cpu().numpy()\n        all_probs.append(probs)\n        if is_test:\n            all_ids.extend(ids if isinstance(ids[0], str) else ids.tolist())\n    probs_arr = np.vstack(all_probs)\n    return (probs_arr, all_ids) if is_test else probs_arr","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-24T20:31:11.286529Z","iopub.execute_input":"2026-05-24T20:31:11.286954Z","iopub.status.idle":"2026-05-24T20:31:11.297923Z","shell.execute_reply.started":"2026-05-24T20:31:11.286931Z","shell.execute_reply":"2026-05-24T20:31:11.297315Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"#k-fold train\ndef train_model_kfold(model_name, train_df, test_df, num_classes, patience=5):\n    builder    = MODEL_BUILDERS[model_name]\n    skf        = StratifiedKFold(cfg.NUM_FOLDS, shuffle=True, random_state=cfg.SEED)\n    oof_probs  = np.zeros((len(train_df), num_classes))\n    test_probs = np.zeros((len(test_df),  num_classes))\n\n    inf_tf      = get_transforms(False)\n    test_dataset = SyntheticDataset(test_df, cfg.test_dir, inf_tf, is_test=True)\n    test_loader  = DataLoader(test_dataset, cfg.BATCH_SIZE, shuffle=False, num_workers=4, pin_memory=True)\n\n    print(f\"\\n\")\n    print(f\"  MODEL: {model_name.upper()}\")\n\n    for fold, (tr_idx, va_idx) in enumerate(skf.split(train_df, train_df[\"label\"])):\n        print(f\"\\nFold {fold+1}/{cfg.NUM_FOLDS} ──\")\n        fold_tr = train_df.iloc[tr_idx]\n        fold_va = train_df.iloc[va_idx]\n\n        tr_loader = DataLoader(\n            SyntheticDataset(fold_tr, cfg.train_dir, get_transforms(True)),\n            cfg.BATCH_SIZE, shuffle=True, num_workers=4, pin_memory=True)\n        va_loader = DataLoader(\n            SyntheticDataset(fold_va, cfg.train_dir, inf_tf),\n            cfg.BATCH_SIZE, shuffle=False, num_workers=4, pin_memory=True)\n\n        model     = builder(num_classes)\n        criterion = nn.CrossEntropyLoss(label_smoothing=0.1)\n        optimizer = optim.AdamW(model.parameters(), lr=cfg.LR, weight_decay=1e-4)\n        scheduler = CosineAnnealingLR(optimizer, T_max=cfg.EPOCHS, eta_min=1e-6)\n\n        best_acc = 0.0\n        no_improve = 0\n        save_path  = f\"ckpt_{model_name}_fold{fold+1}.pth\"\n\n        for epoch in range(cfg.EPOCHS):\n            tl, ta = train_one_epoch(model, tr_loader, optimizer, criterion)\n            vl, va_ = evaluate(model, va_loader, criterion)\n            scheduler.step()\n            flag = \"\"\n            if va_ > best_acc:\n                best_acc = va_\n                no_improve = 0\n                torch.save(model.state_dict(), save_path)\n                flag = \" +\"\n            else:\n                no_improve +=1\n                flag = f\" (patience {no_improve}/{patience})\"\n            print(f\"    ep{epoch+1:>2} | loss {tl:.4f}/{vl:.4f} | acc {ta:.4f}/{va_:.4f}{flag}\")\n\n            if no_improve >= patience:\n                print(f\"  Early stopping — fold {fold+1}, ep{epoch+1}'de durdu\")\n                break\n\n        # En iyi model → OOF + test probs\n        model.load_state_dict(torch.load(save_path, map_location=cfg.DEVICE))\n        oof_probs[va_idx]  = get_probs(model, va_loader)\n        fold_test, _       = get_probs(model, test_loader, is_test=True)\n        test_probs        += fold_test / cfg.NUM_FOLDS\n\n        del model; torch.cuda.empty_cache()\n\n    oof_acc = (oof_probs.argmax(1) == train_df[\"label\"].values).mean()\n    print(f\"\\n  {model_name} OOF Accuracy : {oof_acc:.4f}\")\n    return oof_probs, test_probs","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-24T20:31:11.299287Z","iopub.execute_input":"2026-05-24T20:31:11.300317Z","iopub.status.idle":"2026-05-24T20:31:11.317150Z","shell.execute_reply.started":"2026-05-24T20:31:11.300243Z","shell.execute_reply":"2026-05-24T20:31:11.316453Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"SA wieght optimization","metadata":{"execution":{"iopub.status.busy":"2026-05-05T11:03:15.931752Z","iopub.execute_input":"2026-05-05T11:03:15.932119Z","iopub.status.idle":"2026-05-05T11:03:15.945768Z","shell.execute_reply.started":"2026-05-05T11:03:15.932067Z","shell.execute_reply":"2026-05-05T11:03:15.944905Z"}}},{"cell_type":"code","source":"def ensemble_accuracy(weights: np.ndarray, probs_list: list, labels: np.ndarray) -> float:\n    \"\"\"Objective Func\"\"\"\n    w    = np.array(weights) / np.sum(weights)          # normalize (simplex)\n    blend = sum(w[i] * probs_list[i] for i in range(len(probs_list)))\n    return (blend.argmax(1) == labels).mean()\n\n\ndef simulated_annealing(probs_list: list, labels: np.ndarray, n_models: int) -> np.ndarray:\n    \"\"\"\n    State Space : n_models simplex  (w_i >= 0, sum(w_i) = 1)\n    Neighbor    : Small transfer between two random weights\"\"\"\n    \n    print(f\"\\n\")\n    print(\"  SİMULATED ANNEALING — Weight Optimization\")\n\n    rng = np.random.default_rng(cfg.SEED)\n\n    # Starting with same wieghts\n    current_w   = np.ones(n_models) / n_models\n    current_acc = ensemble_accuracy(current_w, probs_list, labels)\n\n    best_w   = current_w.copy()\n    best_acc = current_acc\n\n    temp     = cfg.SA_INIT_TEMP\n    history  = []          # (iter, temp, best_acc) log\n\n    t0 = time.time()\n    for it in range(cfg.SA_MAX_ITER):\n\n        # Neighbor sol \n        # Choose two random indices and perform a small transfer between them.\n        candidate = current_w.copy()\n        i, j = rng.choice(n_models, size=2, replace=False)\n        delta = rng.uniform(0, cfg.SA_STEP_SIZE)\n        delta = min(delta, candidate[i])          # not negative\n        candidate[i] -= delta\n        candidate[j] += delta\n        # Numeric safety\n        candidate = np.clip(candidate, 0, None)\n        candidate /= candidate.sum()\n\n        # Acceptance\n        candidate_acc = ensemble_accuracy(candidate, probs_list, labels)\n        delta_acc     = candidate_acc - current_acc\n\n        if delta_acc > 0:\n            # Better (accept always)\n            current_w   = candidate\n            current_acc = candidate_acc\n        else:\n            # Worse (Accept under condition)\n            prob = math.exp(delta_acc / temp)\n            if rng.random() < prob:\n                current_w   = candidate\n                current_acc = candidate_acc\n\n        # update best \n        if current_acc > best_acc:\n            best_acc = current_acc\n            best_w   = current_w.copy()\n\n        # cool\n        temp = max(cfg.SA_MIN_TEMP, temp * cfg.SA_COOLING)\n\n        # Log \n        if (it + 1) % 500 == 0:\n            elapsed = time.time() - t0\n            history.append((it + 1, temp, best_acc))\n            print(f\"  iter {it+1:>5} | T={temp:.5f} | best_acc={best_acc:.4f} | \"\n                  f\"cur_acc={current_acc:.4f} | {elapsed:.1f}\")\n\n    print(f\"  Best OOF Accuracy : {best_acc:.4f}\")\n    print(f\"  Optimized weights :\")\n    model_names = [\"efficientnet_b3\", \"convnext_tiny\"]\n    for name, w in zip(model_names, best_w):\n        bar = \"*\" * int(w * 40)\n        print(f\"    {name:<20} w={w:.4f}  {bar}\")\n\n    return best_w, history","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-24T20:31:11.347516Z","iopub.execute_input":"2026-05-24T20:31:11.347715Z","iopub.status.idle":"2026-05-24T20:31:11.357682Z","shell.execute_reply.started":"2026-05-24T20:31:11.347696Z","shell.execute_reply":"2026-05-24T20:31:11.357065Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def run():\n    train_df, test_df, le, num_classes = load_data()\n    labels = train_df[\"label\"].values\n\n    model_names = [\"efficientnet_b3\", \"convnext_tiny\"]\n    all_oof   = []   # OOF prob matrix for all models\n    all_test  = []   # test prob matrix for all models\n\n    # train each model\n    for name in model_names:\n        oof_path  = f\"{cfg.Working_dir}/oof_{name}.npy\"\n        test_path = f\"{cfg.Working_dir}/test_{name}.npy\"\n\n        # skip if path exists\n        if os.path.exists(oof_path) and os.path.exists(test_path):\n            print(f\"{name} — train skipped\")\n            all_oof.append(np.load(oof_path))\n            all_test.append(np.load(test_path))\n            continue\n\n        # else train and save \n        print(f\"{name} — train starts\")\n        oof, test = train_model_kfold(name, train_df, test_df, num_classes, patience=5)\n        all_oof.append(oof)\n        all_test.append(test)\n        np.save(oof_path,  oof)\n        np.save(test_path, test)\n        print(f\"{name} — saved\")\n\n    # SA weight opt \n    best_weights, sa_history = simulated_annealing(all_oof, labels, n_models=2)\n\n    # Reference comparison\n    print(f\"\\n\")\n    print(\"  Comparison (OOF)\")\n    print(f\"\\n\")\n\n    # Same weight baseline\n    equal_w     = np.ones(2) / 2\n    equal_blend = sum(equal_w[i] * all_oof[i] for i in range(2))\n    equal_acc   = (equal_blend.argmax(1) == labels).mean()\n    print(f\"  Egual weight (1/2, 1/2) : {equal_acc:.4f}\")\n\n    # Each Model\n    for i, name in enumerate(model_names):\n        ind_acc = (all_oof[i].argmax(1) == labels).mean()\n        print(f\"  Model — {name:<20}: {ind_acc:.4f}\")\n\n    # SA optimize\n    sa_blend = sum(best_weights[i] * all_oof[i] for i in range(2))\n    sa_acc   = (sa_blend.argmax(1) == labels).mean()\n    print(f\"  SA optimized weights: {sa_acc:.4f}\")\n\n    # Test pred & submission \n    test_blend  = sum(best_weights[i] * all_test[i] for i in range(2))\n    final_preds = le.inverse_transform(test_blend.argmax(1))\n\n    # test IDs\n    test_ids = test_df[\"ID\"].tolist()\n\n    submission = pd.DataFrame({\"ID\": test_ids, \"TARGET\": final_preds})\n    submission.to_csv(f\"{cfg.Working_dir}/submission.csv\", index=False)\n    print(f\"\\n\")\n    print(f\"  submission.csv saved ({len(submission)} line)\")\n    print(submission.head(10))\n\n    # SA history save\n    hist_df = pd.DataFrame(sa_history, columns=[\"iter\", \"temp\", \"best_acc\"])\n    hist_df.to_csv(f\"{cfg.Working_dir}/sa_history.csv\", index=False)\n    print(\"\\n  sa_history.csv saved.\")\n\n    np.save(f\"{cfg.Working_dir}/best_weights.npy\", best_weights)\n\n    return submission","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-24T20:31:11.358892Z","iopub.execute_input":"2026-05-24T20:31:11.359209Z","iopub.status.idle":"2026-05-24T20:31:11.372981Z","shell.execute_reply.started":"2026-05-24T20:31:11.359189Z","shell.execute_reply":"2026-05-24T20:31:11.372366Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"if __name__ == \"__main__\":\n    run()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-24T20:31:11.373829Z","iopub.execute_input":"2026-05-24T20:31:11.374742Z","iopub.status.idle":"2026-05-24T20:31:20.757677Z","shell.execute_reply.started":"2026-05-24T20:31:11.374708Z","shell.execute_reply":"2026-05-24T20:31:20.756332Z"}},"outputs":[],"execution_count":null}]}