{"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":[{"sourceType":"competition","sourceId":129617,"databundleVersionId":15872846,"isSourceIdPinned":false}],"dockerImageVersionId":31286,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"# Welcome to the ACM AI Mini ML Challenge!\nIn this challenge, your task is to build a machine learning model capable of classifying Star Wars characters as either \"Good\" or \"Bad\" (Light side vs. Dark side).\n\nBecause we are working with complex image data, training a model from scratch would take hours (or days!) and require millions of images. Instead, we will use a technique called **Transfer Learning**.\n\nThis starter notebook will help you:\n1. Load and visualize the dataset.\n2. Understand the core concepts of Transfer Learning and Hyperparameters.\n3. Build the scaffolding for your PyTorch model.\n4. Format your final predictions for submission.","metadata":{}},{"cell_type":"code","source":"# ==========================================\n# 1. SETUP & IMPORTS\n# ==========================================\n# Install required libraries if you are running locally:\n# %pip install numpy pandas pillow torch torchvision matplotlib\n\nimport os\nimport numpy as np\nimport pandas as pd\nimport matplotlib.pyplot as plt\nfrom PIL import Image\n\nimport torch\nimport torch.nn as nn\nimport torch.optim as optim\nfrom torch.utils.data import Dataset, DataLoader, random_split\nfrom torchvision import transforms\nimport torchvision.models as models\nimport torchvision.transforms.functional as F2\n\n# Challenge Dataset Paths\ntrain_path = \"/kaggle/input/competitions/acm-ai-mini-ml-challenge-2026/train/train\"\ntest_path = \"/kaggle/input/competitions/acm-ai-mini-ml-challenge-2026/test/test\"\ncsv_drive_path = \"/kaggle/input/competitions/acm-ai-mini-ml-challenge-2026\"\n\n# Set device to GPU if available, otherwise CPU\ndevice = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\nprint(f\"Using device: {device}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-28T21:15:13.142812Z","iopub.execute_input":"2026-02-28T21:15:13.143128Z","iopub.status.idle":"2026-02-28T21:15:21.449422Z","shell.execute_reply.started":"2026-02-28T21:15:13.143093Z","shell.execute_reply":"2026-02-28T21:15:21.448774Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Part 1: Loading the Dataset\nWe use a custom PyTorch `Dataset` class to load our images and map them to their labels. The data is provided in CSV format, linking image file paths to character names and their target alignment (Good=1, Bad=0). \n\n**Important:** Our paths in `train.csv` already include the `train/` folder prefix, so we set our `root_dir` simply to `\".\"` (the current directory).","metadata":{}},{"cell_type":"code","source":"# ==========================================\n# 2. DATASET DEFINITION\n# ==========================================\n\n# training transforms: adds variety so the model doesn't just memorize pixels\ntrain_transform = transforms.Compose([\n    transforms.Resize((256, 256)),          # resize slightly larger\n    transforms.RandomResizedCrop(224),     # take a random crop\n    transforms.RandomHorizontalFlip(p=0.5), # flip 50% of images\n    transforms.RandomRotation(15),          # rotate slightly\n    transforms.ToTensor(),\n    transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]) \n])\n\n# validation/test transforms: no randomness here, just standardizing\nval_transform = transforms.Compose([\n    transforms.Resize((224, 224)),\n    transforms.ToTensor(),\n    transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]) \n])\n\nclass ImageCSVDataset(Dataset):\n    def __init__(self, csv_file, root_dir, transform=None, label_map=None, is_test=False):\n        self.df = pd.read_csv(csv_file)\n        self.root_dir = root_dir\n        self.label_map = label_map\n        self.transform = transform\n        self.is_test = is_test\n\n    def __len__(self):\n        return len(self.df)\n\n    def __getitem__(self, idx):\n        row = self.df.iloc[idx]\n        clean_filename = os.path.basename(row['path']) \n        img_path = os.path.join(self.root_dir, clean_filename)\n\n        image = Image.open(img_path).convert('RGB')\n\n        # apply transform if it exists\n        if self.transform:\n            image = self.transform(image)\n\n        if self.is_test:\n            image_id = int(row['ID'])\n            return image, image_id\n        else:\n            label = self.label_map[row['target']]\n            character = row['character']\n            return image, label, character\n\n\nlabel_map = {\"bad\": 0, \"good\": 1}\n\ntrain_dataset = ImageCSVDataset(\n    csv_file=os.path.join(csv_drive_path, \"train.csv\"), \n    root_dir=train_path,    \n    transform=None, \n    label_map=label_map\n)\n\nprint(f\"train_dataset successfully initialized with {len(train_dataset)} images.\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-28T23:28:28.997775Z","iopub.execute_input":"2026-02-28T23:28:28.998343Z","iopub.status.idle":"2026-02-28T23:28:29.011566Z","shell.execute_reply.started":"2026-02-28T23:28:28.998316Z","shell.execute_reply":"2026-02-28T23:28:29.010981Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Part 2: Data Playground\nBefore training any model, it's critical to perform Exploratory Data Analysis (EDA). This helps us understand what the model will \"see\". Run the cell below to visualize the class balance and inspect random samples from the training set. Feel free to re-run it multiple times!","metadata":{}},{"cell_type":"code","source":"# ==========================================\n# 3. VISUALIZATION PLAYGROUND\n# ==========================================\n\n# 1. analyze class distribution\nprint(\"--- Class Distribution ---\")\n# this works now because train_dataset was created in Step 2\nclass_counts = train_dataset.df['target'].value_counts()\nprint(class_counts)\n\nplt.figure(figsize=(6, 4))\nclass_counts.plot(kind='bar', color=['skyblue', 'salmon'])\nplt.title('Good vs. Bad Characters')\nplt.ylabel('Count')\nplt.xticks(rotation=0) \nplt.show()\n\n# ------------------------------------------\n\n# 2. display a random grid of images\nprint(\"\\n--- Sample Images ---\")\nplt.figure(figsize=(10, 10))\n\n# grab 9 random rows\nsample_df = train_dataset.df.sample(9)\n\nfor i, (_, row) in enumerate(sample_df.iterrows()):\n    # use basename to prevent \"train/train/img.jpg\" errors\n    clean_filename = os.path.basename(row['path']) \n    img_path = os.path.join(train_path, clean_filename)\n    \n    # open the raw image\n    img = Image.open(img_path)\n    \n    plt.subplot(3, 3, i + 1)\n    plt.imshow(img)\n    \n    # add a nice title\n    label_name = row['target'].capitalize()\n    plt.title(f\"{row['character']}\\n({label_name})\", fontsize=10)\n    plt.axis('off')\n\nplt.tight_layout()\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-28T23:28:30.511929Z","iopub.execute_input":"2026-02-28T23:28:30.512469Z","iopub.status.idle":"2026-02-28T23:28:32.368479Z","shell.execute_reply.started":"2026-02-28T23:28:30.512441Z","shell.execute_reply":"2026-02-28T23:28:32.367661Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Part 3: Core Concepts\n\n### What is Transfer Learning?\n\n\nTransfer Learning is the practice of taking a model that has already been trained on a massive dataset (like ImageNet, which contains millions of images) and repurposing it for a specific task—in our case, classifying Star Wars characters. \n\nThe pre-trained model has already learned how to detect basic visual features like edges, shapes, and textures. Instead of training a model from scratch, we keep the model's foundational knowledge and simply \"swap out\" its final output layer (the \"head\") to output our specific classes (Good vs. Bad).\n\n### Tuning the Engine: Hyperparameters\nHyperparameters are the settings you configure *before* the model starts training. Think of them like the dials on a dashboard. Modifying these will dictate how well your model learns:\n\n* **Epochs:** How many times the model will look at the entire dataset. Too few, and it won't learn enough (underfitting). Too many, and it will memorize the training data but fail on new data (overfitting).\n* **Batch Size:** How many images the model processes at once before updating its knowledge. Common values are 16, 32, or 64.\n* **Learning Rate (LR):** The \"step size\" the model takes when adjusting its weights. \n    * *High LR:* The model learns fast but might overshoot the optimal solution.\n    * *Low LR:* The model learns carefully but very slowly.\n* **Optimizer:** The algorithm used to update the model (e.g., `Adam` or `SGD`).","metadata":{}},{"cell_type":"code","source":"# ==========================================\n# 4. MODEL DEFINITION & HYPERPARAMETERS\n# ==========================================\n\n# --- HYPERPARAMETERS ---\nLEARNING_RATE = 0.001\nBATCH_SIZE = 32\nEPOCHS = 20\n# -----------------------\n\nclass StarWarsClassifier(nn.Module):\n    def __init__(self, num_classes=2):\n        super(StarWarsClassifier, self).__init__()\n        \n        # resnet50 backbone\n        self.model = models.resnet50(weights='DEFAULT')\n\n        # freeze layers\n        for param in self.model.parameters():\n            param.requires_grad = False\n\n        # replace the head (fc layer)\n        # we use the input features of the original layer to match dimensions\n        self.model.fc = nn.Linear(self.model.fc.in_features, num_classes)\n        \n    def forward(self, x):\n        return self.model(x)\n\n# initialize everything\nmodel = StarWarsClassifier(num_classes=2).to(device)\ncriterion = nn.CrossEntropyLoss()\n\n# filter ensures the optimizer doesn't track frozen parameters\noptimizer = optim.Adam(filter(lambda p: p.requires_grad, model.parameters()), lr=LEARNING_RATE)\n\n# scheduler: lowers learning rate if val_acc plateaus\nscheduler = optim.lr_scheduler.ReduceLROnPlateau(optimizer, mode='max', factor=0.5, patience=2)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-28T23:28:52.416354Z","iopub.execute_input":"2026-02-28T23:28:52.417044Z","iopub.status.idle":"2026-02-28T23:28:52.790570Z","shell.execute_reply.started":"2026-02-28T23:28:52.417014Z","shell.execute_reply":"2026-02-28T23:28:52.790007Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Part 4: Training the Model\nWe split our training data into a `train` set and a `validation` set. The validation set allows us to test the model on images it hasn't seen during training, giving us an idea of how well it generalizes.","metadata":{}},{"cell_type":"code","source":"# ==========================================\n# 5. THE TRAINING LOOP\n# ==========================================\n\n# 1. split indices for 70/15/15\ntrain_size = int(0.7 * len(train_dataset))\nval_size = int(0.15 * len(train_dataset))\ntest_size = len(train_dataset) - train_size - val_size\n\ntrain_subset, val_subset, test_subset = random_split(train_dataset, [train_size, val_size, test_size])\n\n# 2. apply different transforms to each subset\n# we use a small helper class or just override the dataset's transform attribute\n# important: this ensures training is \"noisy\" but validation is \"clean\"\nimport copy\n\ntrain_subset.dataset = copy.copy(train_dataset)\ntrain_subset.dataset.transform = train_transform\n\nval_subset.dataset = copy.copy(train_dataset)\nval_subset.dataset.transform = val_transform\n\ntest_subset.dataset = copy.copy(train_dataset)\ntest_subset.dataset.transform = val_transform\n\n# 3. initialize loaders\ntrain_loader = DataLoader(train_subset, batch_size=BATCH_SIZE, shuffle=True)\nval_loader = DataLoader(val_subset, batch_size=BATCH_SIZE, shuffle=False)\nlocal_test_loader = DataLoader(test_subset, batch_size=BATCH_SIZE, shuffle=False)\n\ndef train_one_epoch(model, dataloader):\n    model.train()\n    total_loss = 0\n\n    for batch in dataloader:\n        imgs, labels, _ = batch\n        imgs, labels = imgs.to(device), labels.to(device)\n\n        # the 5 crucial steps\n        optimizer.zero_grad()           # 1. clear gradients\n        outputs = model(imgs)           # 2. forward pass\n        loss = criterion(outputs, labels) # 3. calculate loss\n        loss.backward()                 # 4. backward pass\n        optimizer.step()                # 5. update weights\n        \n        total_loss += loss.item()\n\n    return total_loss / len(dataloader)\n\ndef evaluate(model, dataloader):\n    model.eval()\n    correct, total = 0, 0\n\n    with torch.no_grad():\n        for batch in dataloader:\n            imgs, labels, _ = batch\n            imgs, labels = imgs.to(device), labels.to(device)\n\n            outputs = model(imgs)\n            _, predicted = torch.max(outputs.data, 1)\n            total += labels.size(0)\n            correct += (predicted\n                        == labels).sum().item()\n            \n    return correct / total\n\n# start training!\nprint(\"Starting Training...\")\nfor epoch in range(EPOCHS):\n    train_loss = train_one_epoch(model, train_loader)\n    val_acc = evaluate(model, val_loader)\n    test_acc = evaluate(model, local_test_loader)\n    \n    # update scheduler based on validation accuracy\n    scheduler.step(val_acc)\n    \n    print(f\"Epoch {epoch+1}/{EPOCHS}: Loss = {train_loss:.4f} | Val Acc = {val_acc:.2%} | Test Acc = {test_acc:.2%}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-28T23:29:14.907041Z","iopub.execute_input":"2026-02-28T23:29:14.907329Z","iopub.status.idle":"2026-02-28T23:37:28.130438Z","shell.execute_reply.started":"2026-02-28T23:29:14.907304Z","shell.execute_reply":"2026-02-28T23:37:28.129762Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Part 5: Generating your Submission\nOnce you are happy with your validation accuracy, run your model on the test set and save the results to a CSV file to submit to the leaderboard!","metadata":{}},{"cell_type":"code","source":"# ==========================================\n# 6. MAKE PREDICTIONS ON TEST DATA\n# ==========================================\n\n# ensure we use val_transform (no randomness) for the final test\ntest_dataset = ImageCSVDataset(\n    csv_file=os.path.join(csv_drive_path, \"test.csv\"), \n    root_dir=test_path,    \n    transform=val_transform,\n    is_test=True\n)\ntest_loader = DataLoader(test_dataset, batch_size=BATCH_SIZE, shuffle=False)\n\nidx2label = {v: k for k, v in label_map.items()}\n\nmodel.eval()\npreds = []\nids = []\n\nprint(\"starting final inference...\")\nwith torch.no_grad():\n    for image, image_id in test_loader:\n        image = image.to(device)\n        outputs = model(image)\n        _, predicted = torch.max(outputs, 1)\n        \n        preds.extend(predicted.cpu().numpy())\n        ids.extend([id.item() if isinstance(id, torch.Tensor) else id for id in image_id])\n\n# create the dataframe\npred_labels = [idx2label[p] for p in preds]\nsubmission = pd.DataFrame({\n    \"ID\": ids,\n    \"target\": pred_labels\n})\n\n# sort by ID to ensure consistency\nsubmission = submission.sort_values(\"ID\")\n\n# use an absolute path to avoid any ambiguity\noutput_file = \"/kaggle/working/submission.csv\"\nsubmission.to_csv(output_file, index=False)\n\n# verification check\nif os.path.exists(output_file):\n    print(f\"success: {output_file} created with {len(submission)} rows.\")\n    # display the first few rows to make sure it looks right\n    print(submission.head())\nelse:\n    print(\"error: submission file was not saved!\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-28T23:37:30.395642Z","iopub.execute_input":"2026-02-28T23:37:30.396354Z","iopub.status.idle":"2026-02-28T23:37:36.553181Z","shell.execute_reply.started":"2026-02-28T23:37:30.396324Z","shell.execute_reply":"2026-02-28T23:37:36.552511Z"}},"outputs":[],"execution_count":null}]}