{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.11.13","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"nvidiaTeslaT4","dataSources":[{"sourceId":14420,"databundleVersionId":868327,"sourceType":"competition"}],"dockerImageVersionId":31193,"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'):\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":"2025-11-30T05:06:16.676987Z","iopub.execute_input":"2025-11-30T05:06:16.677634Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"\nimport numpy as np\nimport pandas as pd\nimport os\nimport torch\nimport torch.nn as nn\nfrom torch.utils.data import Dataset, DataLoader\nfrom torchvision import transforms\nimport timm\nfrom sklearn.model_selection import train_test_split\nfrom tqdm import tqdm\nimport cv2\nfrom PIL import Image\n\n# Set device\ndevice = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\nprint(f\"Using device: {device}\")\n\n# Configuration\nclass Config:\n    # Paths (adjust these to your Kaggle paths)\n    DATA_DIR = '/kaggle/input/recursion-cellular-image-classification'\n    TRAIN_CSV = f'{DATA_DIR}/train.csv'\n    TEST_CSV = f'{DATA_DIR}/test.csv'\n    \n    # Model settings\n    MODEL_NAME = 'efficientnet_b3'  # Fast and accurate\n    IMG_SIZE = 320  # Reduced from 512 for speed\n    BATCH_SIZE = 32  # Adjust based on GPU memory\n    EPOCHS = 40  # Reduced for time\n    LR = 3e-4\n    \n    # Training settings\n    NUM_WORKERS = 2\n    SEED = 42\n    NUM_CLASSES = 1108\n    \n    # Use only HUVEC cell type for speed (you can add more if time permits)\n    CELL_TYPES = ['HUVEC']  # Add 'RPE', 'HEPG2', 'U2OS' if you have time\n    \n    # sirna needs to be converted to numeric labels\n    CONVERT_SIRNA = True","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-30T05:52:09.316335Z","iopub.execute_input":"2025-11-30T05:52:09.317226Z","iopub.status.idle":"2025-11-30T05:52:09.324292Z","shell.execute_reply.started":"2025-11-30T05:52:09.317192Z","shell.execute_reply":"2025-11-30T05:52:09.323399Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def set_seed(seed):\n    np.random.seed(seed)\n    torch.manual_seed(seed)\n    torch.cuda.manual_seed_all(seed)\n    torch.backends.cudnn.deterministic = True\n\nset_seed(Config.SEED)\n\n# Dataset class\nclass CellularDataset(Dataset):\n    def __init__(self, df, data_dir, mode='train', transform=None):\n        self.df = df\n        self.data_dir = data_dir\n        self.mode = mode\n        self.transform = transform\n        \n    def __len__(self):\n        return len(self.df)\n    \n    def load_image(self, row):\n        \"\"\"Load 6-channel image\"\"\"\n        if self.mode == 'train':\n            exp = row['experiment']\n            plate = row['plate']\n            well = row['well']\n            site = row['site']\n            path_template = f'{self.data_dir}/train/{exp}/Plate{plate}/{well}_s{site}_w'\n        else:\n            img_id = row['id_code']\n            exp = row['experiment']\n            plate = row['plate']\n            well = row['well']\n            site = 1  # Test images are site 1\n            path_template = f'{self.data_dir}/test/{exp}/Plate{plate}/{well}_s{site}_w'\n        \n        # Load all 6 channels\n        channels = []\n        for i in range(1, 7):\n            img_path = f'{path_template}{i}.png'\n            if os.path.exists(img_path):\n                img = cv2.imread(img_path, cv2.IMREAD_GRAYSCALE)\n                channels.append(img)\n            else:\n                # Fallback if file doesn't exist\n                channels.append(np.zeros((512, 512), dtype=np.uint8))\n        \n        # Stack channels and resize\n        img = np.stack(channels, axis=-1)\n        img = cv2.resize(img, (Config.IMG_SIZE, Config.IMG_SIZE))\n        \n        return img\n    \n    def __getitem__(self, idx):\n        row = self.df.iloc[idx]\n        img = self.load_image(row)\n        \n        # Normalize to [0, 1]\n        img = img.astype(np.float32) / 255.0\n        \n        if self.transform:\n            # Convert to PIL for transforms (handle 6 channels)\n            img = torch.from_numpy(img).permute(2, 0, 1)  # C, H, W\n        else:\n            img = torch.from_numpy(img).permute(2, 0, 1)\n        \n        if self.mode == 'train':\n            label = row['label']\n            return img, label\n        else:\n            return img, row['id_code']\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-30T05:52:14.595065Z","iopub.execute_input":"2025-11-30T05:52:14.59533Z","iopub.status.idle":"2025-11-30T05:52:14.606274Z","shell.execute_reply.started":"2025-11-30T05:52:14.595309Z","shell.execute_reply":"2025-11-30T05:52:14.605606Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"class CellularModel(nn.Module):\n    def __init__(self, model_name, num_classes, in_channels=6):\n        super().__init__()\n        # Load pretrained model\n        self.backbone = timm.create_model(model_name, pretrained=True, in_chans=3)\n        \n        # Modify first conv layer to accept 6 channels\n        if hasattr(self.backbone, 'conv_stem'):\n            old_conv = self.backbone.conv_stem\n            self.backbone.conv_stem = nn.Conv2d(\n                in_channels, old_conv.out_channels,\n                kernel_size=old_conv.kernel_size,\n                stride=old_conv.stride,\n                padding=old_conv.padding,\n                bias=False\n            )\n            # Initialize with average of pretrained weights\n            with torch.no_grad():\n                self.backbone.conv_stem.weight[:, :3] = old_conv.weight\n                self.backbone.conv_stem.weight[:, 3:] = old_conv.weight\n        \n        # Get number of features\n        n_features = self.backbone.get_classifier().in_features\n        self.backbone.reset_classifier(0)  # Remove classifier\n        \n        # Custom classifier\n        self.classifier = nn.Sequential(\n            nn.Dropout(0.3),\n            nn.Linear(n_features, num_classes)\n        )\n    \n    def forward(self, x):\n        features = self.backbone(x)\n        return self.classifier(features)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-30T05:52:21.026805Z","iopub.execute_input":"2025-11-30T05:52:21.027521Z","iopub.status.idle":"2025-11-30T05:52:21.033548Z","shell.execute_reply.started":"2025-11-30T05:52:21.027493Z","shell.execute_reply":"2025-11-30T05:52:21.032875Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def train_epoch(model, loader, criterion, optimizer, device):\n    model.train()\n    running_loss = 0.0\n    correct = 0\n    total = 0\n    \n    pbar = tqdm(loader, desc='Training')\n    for imgs, labels in pbar:\n        imgs, labels = imgs.to(device), labels.to(device)\n        \n        optimizer.zero_grad()\n        outputs = model(imgs)\n        loss = criterion(outputs, labels)\n        loss.backward()\n        optimizer.step()\n        \n        running_loss += loss.item()\n        _, predicted = outputs.max(1)\n        total += labels.size(0)\n        correct += predicted.eq(labels).sum().item()\n        \n        pbar.set_postfix({'loss': running_loss/len(loader), 'acc': 100.*correct/total})\n    \n    return running_loss/len(loader), 100.*correct/total\n\n# Validation function\ndef validate(model, loader, criterion, device):\n    model.eval()\n    running_loss = 0.0\n    correct = 0\n    total = 0\n    \n    with torch.no_grad():\n        for imgs, labels in tqdm(loader, desc='Validation'):\n            imgs, labels = imgs.to(device), labels.to(device)\n            outputs = model(imgs)\n            loss = criterion(outputs, labels)\n            \n            running_loss += loss.item()\n            _, predicted = outputs.max(1)\n            total += labels.size(0)\n            correct += predicted.eq(labels).sum().item()\n    \n    return running_loss/len(loader), 100.*correct/total\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-30T05:52:27.529263Z","iopub.execute_input":"2025-11-30T05:52:27.529761Z","iopub.status.idle":"2025-11-30T05:52:27.536861Z","shell.execute_reply.started":"2025-11-30T05:52:27.529734Z","shell.execute_reply":"2025-11-30T05:52:27.536096Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def main():\n    # Load data\n    print(\"Loading data...\")\n    train_df = pd.read_csv(Config.TRAIN_CSV)\n    test_df = pd.read_csv(Config.TEST_CSV)\n    \n    # Extract cell type from experiment column\n    train_df['cell_type'] = train_df['experiment'].str.split('-').str[0]\n    test_df['cell_type'] = test_df['experiment'].str.split('-').str[0]\n    \n    # Filter by cell type for speed\n    train_df = train_df[train_df['cell_type'].isin(Config.CELL_TYPES)].reset_index(drop=True)\n    test_df = test_df[test_df['cell_type'].isin(Config.CELL_TYPES)].reset_index(drop=True)\n    \n    print(f\"Training samples: {len(train_df)}\")\n    print(f\"Test samples: {len(test_df)}\")\n    print(f\"Cell types in train: {train_df['cell_type'].unique()}\")\n    \n    # Convert sirna labels to numeric (sirna_1 -> 1, sirna_10 -> 10, etc.)\n    train_df['sirna_id'] = train_df['sirna'].str.replace('sirna_', '').astype(int)\n    \n    # Create label mapping (need to map to 0-indexed consecutive integers)\n    unique_sirnas = sorted(train_df['sirna_id'].unique())\n    sirna_to_label = {sirna: idx for idx, sirna in enumerate(unique_sirnas)}\n    train_df['label'] = train_df['sirna_id'].map(sirna_to_label)\n    \n    print(f\"Number of unique sirnas: {len(unique_sirnas)}\")\n    print(f\"Label range: 0 to {train_df['label'].max()}\")\n    \n    # Update NUM_CLASSES based on actual data\n    Config.NUM_CLASSES = len(unique_sirnas)\n    \n    # Split train/val\n    train_data, val_data = train_test_split(train_df, test_size=0.15, random_state=Config.SEED, stratify=train_df['label'])\n    \n    # Create datasets\n    train_dataset = CellularDataset(train_data, Config.DATA_DIR, mode='train')\n    val_dataset = CellularDataset(val_data, Config.DATA_DIR, mode='train')\n    \n    # Create dataloaders\n    train_loader = DataLoader(train_dataset, batch_size=Config.BATCH_SIZE, \n                             shuffle=True, num_workers=Config.NUM_WORKERS, pin_memory=True)\n    val_loader = DataLoader(val_dataset, batch_size=Config.BATCH_SIZE, \n                           shuffle=False, num_workers=Config.NUM_WORKERS, pin_memory=True)\n    \n    # Create model\n    print(\"Creating model...\")\n    model = CellularModel(Config.MODEL_NAME, Config.NUM_CLASSES).to(device)\n    \n    # Loss and optimizer\n    criterion = nn.CrossEntropyLoss()\n    optimizer = torch.optim.AdamW(model.parameters(), lr=Config.LR, weight_decay=1e-4)\n    scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=Config.EPOCHS)\n    \n    # Training loop\n    best_acc = 0\n    for epoch in range(Config.EPOCHS):\n        print(f\"\\nEpoch {epoch+1}/{Config.EPOCHS}\")\n        train_loss, train_acc = train_epoch(model, train_loader, criterion, optimizer, device)\n        val_loss, val_acc = validate(model, val_loader, criterion, device)\n        scheduler.step()\n        \n        print(f\"Train Loss: {train_loss:.4f}, Train Acc: {train_acc:.2f}%\")\n        print(f\"Val Loss: {val_loss:.4f}, Val Acc: {val_acc:.2f}%\")\n        \n        # Save best model\n        if val_acc > best_acc:\n            best_acc = val_acc\n            torch.save(model.state_dict(), 'best_model.pth')\n            print(f\"Saved best model with accuracy: {best_acc:.2f}%\")\n    \n    # Load best model for inference\n    print(\"\\nLoading best model for inference...\")\n    model.load_state_dict(torch.load('best_model.pth'))\n    \n    # Inference on test set\n    print(\"Generating predictions...\")\n    test_dataset = CellularDataset(test_df, Config.DATA_DIR, mode='test')\n    test_loader = DataLoader(test_dataset, batch_size=Config.BATCH_SIZE, \n                            shuffle=False, num_workers=Config.NUM_WORKERS)\n    \n    model.eval()\n    predictions = []\n    ids = []\n    \n    with torch.no_grad():\n        for imgs, img_ids in tqdm(test_loader, desc='Inference'):\n            imgs = imgs.to(device)\n            outputs = model(imgs)\n            _, preds = outputs.max(1)\n            \n            predictions.extend(preds.cpu().numpy())\n            ids.extend(img_ids)\n    \n    # Convert predictions back to sirna format\n    label_to_sirna = {idx: sirna for sirna, idx in sirna_to_label.items()}\n    predictions_sirna = [label_to_sirna[pred] for pred in predictions]\n    \n    # Create submission\n    submission = pd.DataFrame({\n        'id_code': ids,\n        'sirna': predictions_sirna\n    })\n    submission.to_csv('submission.csv', index=False)\n    print(\"\\nSubmission saved to submission.csv\")\n    print(f\"Best validation accuracy: {best_acc:.2f}%\")\n    print(f\"Sample predictions:\")\n    print(submission.head(10))\n\nif __name__ == '__main__':\n    main()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-30T05:52:34.518985Z","iopub.execute_input":"2025-11-30T05:52:34.519253Z","iopub.status.idle":"2025-11-30T05:52:35.118047Z","shell.execute_reply.started":"2025-11-30T05:52:34.519234Z","shell.execute_reply":"2025-11-30T05:52:35.117048Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import pandas as pd\n\n# Load the CSVs\nDATA_DIR = '/kaggle/input/recursion-cellular-image-classification'\ntrain_df = pd.read_csv(f'{DATA_DIR}/train.csv')\ntest_df = pd.read_csv(f'{DATA_DIR}/test.csv')\n\n# Print info about the dataframes\nprint(\"TRAIN CSV INFO:\")\nprint(train_df.head())\nprint(\"\\nTrain columns:\", train_df.columns.tolist())\nprint(\"Train shape:\", train_df.shape)\n\nprint(\"\\n\" + \"=\"*50)\nprint(\"\\nTEST CSV INFO:\")\nprint(test_df.head())\nprint(\"\\nTest columns:\", test_df.columns.tolist())\nprint(\"Test shape:\", test_df.shape)\n\n# Check unique values for some columns\nif 'experiment' in train_df.columns:\n    print(\"\\nUnique experiments:\", train_df['experiment'].unique())\nif 'plate' in train_df.columns:\n    print(\"Unique plates:\", train_df['plate'].nunique())","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-30T05:52:44.950606Z","iopub.execute_input":"2025-11-30T05:52:44.95087Z","iopub.status.idle":"2025-11-30T05:52:45.0135Z","shell.execute_reply.started":"2025-11-30T05:52:44.950852Z","shell.execute_reply":"2025-11-30T05:52:45.01272Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nimport glob\n\n# Check one example to see the file structure\nsample = train_df.iloc[0]\nexp = sample['experiment']\nplate = sample['plate']\nwell = sample['well']\n\npath = f'/kaggle/input/recursion-cellular-image-classification/train/{exp}/Plate{plate}/'\nfiles = sorted(glob.glob(f'{path}{well}*.png'))\nprint(f\"Files for {well}:\")\nfor f in files[:10]:  # Show first 10\n    print(f)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-30T05:52:51.286688Z","iopub.execute_input":"2025-11-30T05:52:51.287215Z","iopub.status.idle":"2025-11-30T05:52:51.299419Z","shell.execute_reply.started":"2025-11-30T05:52:51.287192Z","shell.execute_reply":"2025-11-30T05:52:51.298739Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"\"\"\"\nRecursion Cellular Image Classification - Quick Solution\nOptimized for time constraints with 15 epochs\n\"\"\"\n\nimport numpy as np\nimport pandas as pd\nimport os\nimport torch\nimport torch.nn as nn\nfrom torch.utils.data import Dataset, DataLoader\nimport timm\nfrom sklearn.model_selection import train_test_split\nfrom tqdm import tqdm\nimport cv2\n\n# Set device\ndevice = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\nprint(f\"Using device: {device}\")\nprint(f\"CUDA available: {torch.cuda.is_available()}\\n\")\n\n# Configuration\nclass Config:\n    # Paths\n    DATA_DIR = '/kaggle/input/recursion-cellular-image-classification'\n    TRAIN_CSV = f'{DATA_DIR}/train.csv'\n    TEST_CSV = f'{DATA_DIR}/test.csv'\n    \n    # Model settings\n    MODEL_NAME = 'efficientnet_b3'\n    IMG_SIZE = 320\n    BATCH_SIZE = 32\n    EPOCHS = 11  # REDUCED FROM 40 TO 11 FOR MAXIMUM SPEED\n    LR = 3e-4\n    \n    # Training settings\n    NUM_WORKERS = 2\n    SEED = 42\n    NUM_CLASSES = 1108\n    \n    # Cell types (add more if you have extra time)\n    CELL_TYPES = ['HUVEC']  # Can add: 'RPE', 'HEPG2', 'U2OS'\n    \n    CONVERT_SIRNA = True\n\n# Set seed\ndef set_seed(seed):\n    np.random.seed(seed)\n    torch.manual_seed(seed)\n    torch.cuda.manual_seed_all(seed)\n    torch.backends.cudnn.deterministic = True\n\nset_seed(Config.SEED)\n\n# Dataset class\nclass CellularDataset(Dataset):\n    def __init__(self, df, data_dir, mode='train', transform=None):\n        self.df = df\n        self.data_dir = data_dir\n        self.mode = mode\n        self.transform = transform\n        \n    def __len__(self):\n        return len(self.df)\n    \n    def load_image(self, row):\n        \"\"\"Load 6-channel image\"\"\"\n        exp = row['experiment']\n        plate = row['plate']\n        well = row['well']\n        \n        if self.mode == 'train':\n            path_template = f'{self.data_dir}/train/{exp}/Plate{plate}/{well}_s1_w'\n        else:\n            path_template = f'{self.data_dir}/test/{exp}/Plate{plate}/{well}_s1_w'\n        \n        # Load all 6 channels\n        channels = []\n        for i in range(1, 7):\n            img_path = f'{path_template}{i}.png'\n            if os.path.exists(img_path):\n                img = cv2.imread(img_path, cv2.IMREAD_GRAYSCALE)\n                channels.append(img)\n            else:\n                channels.append(np.zeros((512, 512), dtype=np.uint8))\n        \n        # Stack channels and resize\n        img = np.stack(channels, axis=-1)\n        img = cv2.resize(img, (Config.IMG_SIZE, Config.IMG_SIZE))\n        \n        return img\n    \n    def __getitem__(self, idx):\n        row = self.df.iloc[idx]\n        img = self.load_image(row)\n        \n        # Normalize to [0, 1]\n        img = img.astype(np.float32) / 255.0\n        img = torch.from_numpy(img).permute(2, 0, 1)  # C, H, W\n        \n        if self.mode == 'train':\n            label = row['label']\n            return img, label\n        else:\n            return img, row['id_code']\n\n# Model\nclass CellularModel(nn.Module):\n    def __init__(self, model_name, num_classes, in_channels=6):\n        super().__init__()\n        # Load pretrained model\n        self.backbone = timm.create_model(model_name, pretrained=True, in_chans=3)\n        \n        # Modify first conv layer to accept 6 channels\n        if hasattr(self.backbone, 'conv_stem'):\n            old_conv = self.backbone.conv_stem\n            self.backbone.conv_stem = nn.Conv2d(\n                in_channels, old_conv.out_channels,\n                kernel_size=old_conv.kernel_size,\n                stride=old_conv.stride,\n                padding=old_conv.padding,\n                bias=False\n            )\n            # Initialize with average of pretrained weights\n            with torch.no_grad():\n                self.backbone.conv_stem.weight[:, :3] = old_conv.weight\n                self.backbone.conv_stem.weight[:, 3:] = old_conv.weight\n        \n        # Get number of features\n        n_features = self.backbone.get_classifier().in_features\n        self.backbone.reset_classifier(0)  # Remove classifier\n        \n        # Custom classifier\n        self.classifier = nn.Sequential(\n            nn.Dropout(0.3),\n            nn.Linear(n_features, num_classes)\n        )\n    \n    def forward(self, x):\n        features = self.backbone(x)\n        return self.classifier(features)\n\n# Training function\ndef train_epoch(model, loader, criterion, optimizer, device):\n    model.train()\n    running_loss = 0.0\n    correct = 0\n    total = 0\n    \n    pbar = tqdm(loader, desc='Training')\n    for imgs, labels in pbar:\n        imgs, labels = imgs.to(device), labels.to(device)\n        \n        optimizer.zero_grad()\n        outputs = model(imgs)\n        loss = criterion(outputs, labels)\n        loss.backward()\n        optimizer.step()\n        \n        running_loss += loss.item()\n        _, predicted = outputs.max(1)\n        total += labels.size(0)\n        correct += predicted.eq(labels).sum().item()\n        \n        pbar.set_postfix({'loss': running_loss/len(loader), 'acc': 100.*correct/total})\n    \n    return running_loss/len(loader), 100.*correct/total\n\n# Validation function\ndef validate(model, loader, criterion, device):\n    model.eval()\n    running_loss = 0.0\n    correct = 0\n    total = 0\n    \n    with torch.no_grad():\n        for imgs, labels in tqdm(loader, desc='Validation'):\n            imgs, labels = imgs.to(device), labels.to(device)\n            outputs = model(imgs)\n            loss = criterion(outputs, labels)\n            \n            running_loss += loss.item()\n            _, predicted = outputs.max(1)\n            total += labels.size(0)\n            correct += predicted.eq(labels).sum().item()\n    \n    return running_loss/len(loader), 100.*correct/total\n\n# Main training pipeline\ndef main():\n    print(\"=\"*70)\n    print(\"CELL IMAGE CLASSIFICATION - 15 EPOCHS\")\n    print(\"=\"*70 + \"\\n\")\n    \n    # Load data\n    print(\"📥 Loading data...\")\n    train_df = pd.read_csv(Config.TRAIN_CSV)\n    test_df = pd.read_csv(Config.TEST_CSV)\n    \n    # Extract cell type from experiment column\n    train_df['cell_type'] = train_df['experiment'].str.split('-').str[0]\n    test_df['cell_type'] = test_df['experiment'].str.split('-').str[0]\n    \n    # Filter by cell type for speed\n    train_df = train_df[train_df['cell_type'].isin(Config.CELL_TYPES)].reset_index(drop=True)\n    test_df = test_df[test_df['cell_type'].isin(Config.CELL_TYPES)].reset_index(drop=True)\n    \n    print(f\"Training samples: {len(train_df):,}\")\n    print(f\"Test samples: {len(test_df):,}\")\n    print(f\"Cell types: {train_df['cell_type'].unique()}\\n\")\n    \n    # Convert sirna labels to numeric\n    train_df['sirna_id'] = train_df['sirna'].str.replace('sirna_', '').astype(int)\n    \n    # Create label mapping\n    unique_sirnas = sorted(train_df['sirna_id'].unique())\n    sirna_to_label = {sirna: idx for idx, sirna in enumerate(unique_sirnas)}\n    train_df['label'] = train_df['sirna_id'].map(sirna_to_label)\n    \n    print(f\"Number of unique sirnas: {len(unique_sirnas)}\")\n    print(f\"Label range: 0 to {train_df['label'].max()}\\n\")\n    \n    # Update NUM_CLASSES\n    Config.NUM_CLASSES = len(unique_sirnas)\n    \n    # Split train/val\n    train_data, val_data = train_test_split(train_df, test_size=0.15, random_state=Config.SEED, stratify=train_df['label'])\n    \n    print(f\"Train samples: {len(train_data):,}\")\n    print(f\"Val samples: {len(val_data):,}\\n\")\n    \n    # Create datasets\n    train_dataset = CellularDataset(train_data, Config.DATA_DIR, mode='train')\n    val_dataset = CellularDataset(val_data, Config.DATA_DIR, mode='train')\n    \n    # Create dataloaders\n    train_loader = DataLoader(train_dataset, batch_size=Config.BATCH_SIZE, \n                             shuffle=True, num_workers=Config.NUM_WORKERS, pin_memory=True)\n    val_loader = DataLoader(val_dataset, batch_size=Config.BATCH_SIZE, \n                           shuffle=False, num_workers=Config.NUM_WORKERS, pin_memory=True)\n    \n    print(f\"Train batches: {len(train_loader)}\")\n    print(f\"Val batches: {len(val_loader)}\\n\")\n    \n    # Create model\n    print(\"🔨 Creating model...\")\n    model = CellularModel(Config.MODEL_NAME, Config.NUM_CLASSES).to(device)\n    \n    # Loss and optimizer\n    criterion = nn.CrossEntropyLoss()\n    optimizer = torch.optim.AdamW(model.parameters(), lr=Config.LR, weight_decay=1e-4)\n    scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=Config.EPOCHS)\n    \n    # Training loop\n    print(\"\\n\" + \"=\"*70)\n    print(\"TRAINING\")\n    print(\"=\"*70 + \"\\n\")\n    \n    best_acc = 0\n    for epoch in range(Config.EPOCHS):\n        print(f\"\\n📍 Epoch {epoch+1}/{Config.EPOCHS}\")\n        print(\"-\" * 70)\n        \n        train_loss, train_acc = train_epoch(model, train_loader, criterion, optimizer, device)\n        val_loss, val_acc = validate(model, val_loader, criterion, device)\n        scheduler.step()\n        \n        print(f\"\\nTrain Loss: {train_loss:.4f} | Train Acc: {train_acc:.2f}%\")\n        print(f\"Val Loss: {val_loss:.4f} | Val Acc: {val_acc:.2f}%\")\n        \n        # Save best model\n        if val_acc > best_acc:\n            best_acc = val_acc\n            torch.save(model.state_dict(), '/kaggle/working/best_model.pth')\n            print(f\"✅ Saved best model with accuracy: {best_acc:.2f}%\")\n    \n    print(\"\\n\" + \"=\"*70)\n    print(f\"✅ TRAINING COMPLETE - Best Accuracy: {best_acc:.2f}%\")\n    print(\"=\"*70 + \"\\n\")\n    \n    # Load best model for inference\n    print(\"📥 Loading best model for inference...\")\n    model.load_state_dict(torch.load('/kaggle/working/best_model.pth'))\n    \n    # Inference on test set\n    print(\"🔮 Generating predictions...\\n\")\n    test_dataset = CellularDataset(test_df, Config.DATA_DIR, mode='test')\n    test_loader = DataLoader(test_dataset, batch_size=Config.BATCH_SIZE, \n                            shuffle=False, num_workers=Config.NUM_WORKERS)\n    \n    model.eval()\n    predictions = []\n    ids = []\n    \n    with torch.no_grad():\n        for imgs, img_ids in tqdm(test_loader, desc='Inference'):\n            imgs = imgs.to(device)\n            outputs = model(imgs)\n            _, preds = outputs.max(1)\n            \n            predictions.extend(preds.cpu().numpy())\n            ids.extend(img_ids)\n    \n    # Convert predictions back to sirna format\n    label_to_sirna = {idx: sirna for sirna, idx in sirna_to_label.items()}\n    predictions_sirna = [label_to_sirna[pred] for pred in predictions]\n    \n    # Create submission\n    submission = pd.DataFrame({\n        'id_code': ids,\n        'sirna': predictions_sirna\n    })\n    submission.to_csv('/kaggle/working/submission.csv', index=False)\n    \n    print(\"\\n\" + \"=\"*70)\n    print(\"✅ SUBMISSION SAVED\")\n    print(\"=\"*70)\n    print(f\"\\nBest validation accuracy: {best_acc:.2f}%\")\n    print(f\"Total predictions: {len(submission)}\")\n    print(f\"\\nSample predictions:\")\n    print(submission.head(10))\n\nif __name__ == '__main__':\n    main()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-30T05:58:00.293223Z","iopub.execute_input":"2025-11-30T05:58:00.293524Z"}},"outputs":[],"execution_count":null}]}