{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.13.15","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"none","dataSources":[],"dockerImageVersionId":28755,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":false}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"# Cell 1: Library versions, random seeds, and data path check\nimport os, random, sys\nimport numpy as np\nimport pandas as pd\n\n# Fix random seeds so results are reproducible\nSEED = 42\nrandom.seed(SEED)\nnp.random.seed(SEED)\nos.environ[\"PYTHONHASHSEED\"] = str(SEED)\n\nimport torch\nimport librosa\nimport sklearn\nimport matplotlib\ntorch.manual_seed(SEED)\ntorch.cuda.manual_seed_all(SEED)\n\n# Print library versions (required by the submission guidelines)\nprint(\"Python     :\", sys.version.split()[0])\nprint(\"numpy      :\", np.__version__)\nprint(\"pandas     :\", pd.__version__)\nprint(\"torch      :\", torch.__version__)\nprint(\"librosa    :\", librosa.__version__)\nprint(\"sklearn    :\", sklearn.__version__)\nprint(\"matplotlib :\", matplotlib.__version__)\nprint(\"GPU available:\", torch.cuda.is_available())\nif torch.cuda.is_available():\n    print(\"GPU name:\", torch.cuda.get_device_name(0))\n\n# Kaggle input path (do not download data manually)\nDATA_DIR = \"/kaggle/input/competitions/freesound-audio-tagging\"\nprint(\"\\nTop-level items in data folder:\")\nprint(os.listdir(DATA_DIR))","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2026-10-07T10:17:59.083389Z","iopub.execute_input":"2026-10-07T10:17:59.083711Z","iopub.status.idle":"2026-10-07T10:18:01.909551Z","shell.execute_reply.started":"2026-10-07T10:17:59.083689Z","shell.execute_reply":"2026-10-07T10:18:01.908896Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Find the real data path inside /kaggle/input\nimport os\n\nfor root, dirs, files in os.walk(\"/kaggle/input\"):\n    depth = root.count(\"/\") - \"/kaggle/input\".count(\"/\")\n    if depth <= 2:\n        print(root)\n        for f in files[:5]:\n            print(\"    file:\", f)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-10-07T10:18:01.910484Z","iopub.execute_input":"2026-10-07T10:18:01.910962Z","iopub.status.idle":"2026-10-07T10:18:18.636888Z","shell.execute_reply.started":"2026-10-07T10:18:01.910929Z","shell.execute_reply":"2026-10-07T10:18:18.636262Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Cell 2: EDA part 1 - load csv files and check basic info\nimport os\nimport pandas as pd\nimport matplotlib.pyplot as plt\n\nDATA_DIR = \"/kaggle/input/competitions/freesound-audio-tagging\"\nTRAIN_DIR = os.path.join(DATA_DIR, \"audio_train\")\nTEST_DIR = os.path.join(DATA_DIR, \"audio_test\")\n\ntrain_df = pd.read_csv(os.path.join(DATA_DIR, \"train.csv\"))\nsample_sub = pd.read_csv(os.path.join(DATA_DIR, \"sample_submission.csv\"))\n\nprint(\"Train shape:\", train_df.shape)\nprint(\"Sample submission shape:\", sample_sub.shape)\nprint(\"\\nFirst 5 rows of train.csv:\")\nprint(train_df.head())\n\nprint(\"\\nNumber of classes:\", train_df[\"label\"].nunique())\nprint(\"Missing values:\\n\", train_df.isnull().sum())\nprint(\"Duplicate file names:\", train_df[\"fname\"].duplicated().sum())\n\nprint(\"\\nManually verified counts (1 = verified, 0 = not verified):\")\nprint(train_df[\"manually_verified\"].value_counts())\n\nprint(\"\\nNumber of wav files in audio_train:\", len(os.listdir(TRAIN_DIR)))\nprint(\"Number of wav files in audio_test :\", len(os.listdir(TEST_DIR)))\n\n# Class distribution plot\ncounts = train_df[\"label\"].value_counts()\nplt.figure(figsize=(12, 5))\ncounts.plot(kind=\"bar\")\nplt.title(\"Class distribution in training data\")\nplt.xlabel(\"Class\")\nplt.ylabel(\"Number of files\")\nplt.tight_layout()\nplt.show()\n\nprint(\"\\nMin files per class:\", counts.min(), \"| Max files per class:\", counts.max())","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-10-07T10:18:18.637827Z","iopub.execute_input":"2026-10-07T10:18:18.638147Z","iopub.status.idle":"2026-10-07T10:18:19.145624Z","shell.execute_reply.started":"2026-10-07T10:18:18.638113Z","shell.execute_reply":"2026-10-07T10:18:19.144883Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Cell 3: EDA part 2 - audio durations and sample waveforms/spectrograms\nimport numpy as np\nimport soundfile as sf\nimport librosa\nimport librosa.display\n\n# 1) Duration of every training file (reads only the file header, so it is fast)\ndurations = []\nfor fname in train_df[\"fname\"]:\n    info = sf.info(os.path.join(TRAIN_DIR, fname))\n    durations.append(info.frames / info.samplerate)\ntrain_df[\"duration\"] = durations\n\nprint(train_df[\"duration\"].describe())\nprint(\"Sample rate of first file:\", sf.info(os.path.join(TRAIN_DIR, train_df['fname'][0])).samplerate)\n\nplt.figure(figsize=(10, 4))\nplt.hist(train_df[\"duration\"], bins=50)\nplt.title(\"Distribution of audio duration (seconds)\")\nplt.xlabel(\"Duration (s)\")\nplt.ylabel(\"Number of files\")\nplt.tight_layout()\nplt.show()\n\n# 2) Waveform + mel spectrogram of 4 random classes\nrng = np.random.RandomState(42)\nsample_labels = rng.choice(train_df[\"label\"].unique(), 4, replace=False)\n\nfig, axes = plt.subplots(4, 2, figsize=(14, 12))\nfor i, lab in enumerate(sample_labels):\n    row = train_df[train_df[\"label\"] == lab].iloc[0]\n    y, sr = librosa.load(os.path.join(TRAIN_DIR, row[\"fname\"]), sr=None)\n    axes[i, 0].plot(y)\n    axes[i, 0].set_title(f\"{lab} - waveform\")\n    mel = librosa.power_to_db(librosa.feature.melspectrogram(y=y, sr=sr, n_mels=64), ref=np.max)\n    librosa.display.specshow(mel, sr=sr, x_axis=\"time\", y_axis=\"mel\", ax=axes[i, 1])\n    axes[i, 1].set_title(f\"{lab} - mel spectrogram\")\nplt.tight_layout()\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-10-07T10:18:19.146529Z","iopub.execute_input":"2026-10-07T10:18:19.146831Z","iopub.status.idle":"2026-10-07T10:19:03.46693Z","shell.execute_reply.started":"2026-10-07T10:18:19.1468Z","shell.execute_reply":"2026-10-07T10:19:03.466195Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Cell 4: Preprocessing - convert train audio to fixed-size log-mel spectrograms\nimport numpy as np\nfrom joblib import Parallel, delayed\nfrom sklearn.model_selection import train_test_split\nfrom sklearn.preprocessing import LabelEncoder\n\nSR = 22050            # target sample rate\nDURATION = 4          # seconds per clip\nN_SAMPLES = SR * DURATION\nN_MELS = 64\nHOP = 512\n\ndef audio_to_logmel(path):\n    y, _ = librosa.load(path, sr=SR)\n    # make every clip exactly 4 seconds\n    if len(y) < N_SAMPLES:\n        y = np.pad(y, (0, N_SAMPLES - len(y)), mode=\"wrap\")   # repeat short clips\n    else:\n        start = (len(y) - N_SAMPLES) // 2                     # centre crop long clips\n        y = y[start:start + N_SAMPLES]\n    mel = librosa.feature.melspectrogram(y=y, sr=SR, n_mels=N_MELS, hop_length=HOP)\n    return librosa.power_to_db(mel, ref=np.max).astype(np.float32)\n\npaths = [os.path.join(TRAIN_DIR, f) for f in train_df[\"fname\"]]\nfeats = Parallel(n_jobs=-1)(delayed(audio_to_logmel)(p) for p in paths)\nX = np.stack(feats)[..., np.newaxis]        # shape: (N, 64, 173, 1)\n\nle = LabelEncoder()\ny = le.fit_transform(train_df[\"label\"])\nCLASSES = list(le.classes_)\n\nprint(\"X shape:\", X.shape, \"| y shape:\", y.shape, \"| classes:\", len(CLASSES))\n\n# Stratified 80/20 train/validation split\nidx = np.arange(len(X))\ntr_idx, va_idx = train_test_split(idx, test_size=0.2, stratify=y, random_state=SEED)\nX_train, y_train = X[tr_idx], y[tr_idx]\nX_val, y_val = X[va_idx], y[va_idx]\nprint(\"Train:\", X_train.shape, \"| Validation:\", X_val.shape)\n\n# Normalise using training-set statistics only\nMEAN, STD = X_train.mean(), X_train.std()\nX_train = (X_train - MEAN) / STD\nX_val = (X_val - MEAN) / STD\nprint(\"Mean/Std used:\", MEAN, STD)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-10-07T10:19:03.469432Z","iopub.execute_input":"2026-10-07T10:19:03.469833Z","iopub.status.idle":"2026-10-07T10:19:59.253132Z","shell.execute_reply.started":"2026-10-07T10:19:03.469811Z","shell.execute_reply":"2026-10-07T10:19:59.252409Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Cell 5: Baseline model M0 (simple CNN trained from scratch) + data loaders\nimport torch\nimport torch.nn as nn\nfrom torch.utils.data import TensorDataset, DataLoader\n\ndevice = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\nprint(\"Using device:\", device)\n\n# PyTorch expects (N, channels, height, width) -> move the channel axis forward\nXtr_t = torch.tensor(X_train).permute(0, 3, 1, 2).float()\nXva_t = torch.tensor(X_val).permute(0, 3, 1, 2).float()\nytr_t = torch.tensor(y_train).long()\nyva_t = torch.tensor(y_val).long()\n\nBATCH_SIZE = 64\ntrain_loader = DataLoader(TensorDataset(Xtr_t, ytr_t), batch_size=BATCH_SIZE, shuffle=True)\nval_loader = DataLoader(TensorDataset(Xva_t, yva_t), batch_size=BATCH_SIZE, shuffle=False)\n\nclass BaselineCNN(nn.Module):\n    def __init__(self, n_classes=41):\n        super().__init__()\n        self.features = nn.Sequential(\n            nn.Conv2d(1, 16, 3, padding=1), nn.ReLU(), nn.MaxPool2d(2),\n            nn.Conv2d(16, 32, 3, padding=1), nn.ReLU(), nn.MaxPool2d(2),\n            nn.Conv2d(32, 64, 3, padding=1), nn.ReLU(), nn.MaxPool2d(2),\n        )\n        self.pool = nn.AdaptiveAvgPool2d(1)      # global average pooling\n        self.classifier = nn.Linear(64, n_classes)   # raw scores (softmax is inside the loss)\n\n    def forward(self, x):\n        x = self.features(x)\n        x = self.pool(x).flatten(1)\n        return self.classifier(x)\n\nmodel_m0 = BaselineCNN(n_classes=len(CLASSES)).to(device)\nprint(model_m0)\nn_params = sum(p.numel() for p in model_m0.parameters() if p.requires_grad)\nprint(\"\\nTotal trainable parameters:\", n_params)\n\n# Quick test: pass one batch through the model\nxb, yb = next(iter(train_loader))\nout = model_m0(xb.to(device))\nprint(\"Input batch shape :\", xb.shape)\nprint(\"Output batch shape:\", out.shape, \"(should be [64, 41])\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-10-07T10:19:59.254294Z","iopub.execute_input":"2026-10-07T10:19:59.254901Z","iopub.status.idle":"2026-10-07T10:20:00.393164Z","shell.execute_reply.started":"2026-10-07T10:19:59.254879Z","shell.execute_reply":"2026-10-07T10:20:00.392327Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Cell 6: Training function + train baseline M0\nimport time\nimport torch.nn.functional as F\n\ndef map3(logits, labels):\n    \"\"\"Mean Average Precision @ 3 for single-label data.\"\"\"\n    top3 = torch.topk(logits, 3, dim=1).indices\n    hits = (top3 == labels.unsqueeze(1)).float()\n    weights = torch.tensor([1.0, 1/2, 1/3], device=logits.device)\n    return (hits * weights).sum(1).mean().item()\n\ndef run_epoch(model, loader, optimizer=None):\n    train_mode = optimizer is not None\n    model.train(train_mode)\n    total_loss, total_acc, total_map, n = 0.0, 0.0, 0.0, 0\n    with torch.set_grad_enabled(train_mode):\n        for xb, yb in loader:\n            xb, yb = xb.to(device), yb.to(device)\n            out = model(xb)\n            loss = F.cross_entropy(out, yb)\n            if train_mode:\n                optimizer.zero_grad()\n                loss.backward()\n                optimizer.step()\n            bs = len(yb)\n            total_loss += loss.item() * bs\n            total_acc += (out.argmax(1) == yb).float().sum().item()\n            total_map += map3(out, yb) * bs\n            n += bs\n    return total_loss / n, total_acc / n, total_map / n\n\ndef train_model(model, name, epochs=30, lr=1e-3, weight_decay=0.0,\n                use_scheduler=False, patience=None):\n    optimizer = torch.optim.Adam(model.parameters(), lr=lr, weight_decay=weight_decay)\n    scheduler = torch.optim.lr_scheduler.ReduceLROnPlateau(optimizer, factor=0.5, patience=3) if use_scheduler else None\n    hist = {\"tr_loss\": [], \"va_loss\": [], \"tr_acc\": [], \"va_acc\": [], \"tr_map\": [], \"va_map\": []}\n    best_map, best_epoch, wait = 0.0, 0, 0\n    start = time.time()\n    for ep in range(1, epochs + 1):\n        tr_loss, tr_acc, tr_map = run_epoch(model, train_loader, optimizer)\n        va_loss, va_acc, va_map = run_epoch(model, val_loader)\n        for k, v in zip(hist.keys(), [tr_loss, va_loss, tr_acc, va_acc, tr_map, va_map]):\n            hist[k].append(v)\n        if scheduler: scheduler.step(va_loss)\n        print(f\"[{name}] Epoch {ep:02d}/{epochs} | train loss {tr_loss:.3f} acc {tr_acc:.3f} | \"\n              f\"val loss {va_loss:.3f} acc {va_acc:.3f} MAP@3 {va_map:.3f}\")\n        if va_map > best_map:\n            best_map, best_epoch, wait = va_map, ep, 0\n            torch.save(model.state_dict(), f\"/kaggle/working/{name}_best.pt\")\n        else:\n            wait += 1\n            if patience and wait >= patience:\n                print(f\"Early stopping at epoch {ep}\")\n                break\n    minutes = (time.time() - start) / 60\n    print(f\"\\n{name} done. Best val MAP@3 = {best_map:.4f} at epoch {best_epoch}. Training time: {minutes:.1f} min\")\n    return hist, best_map, minutes\n\n# Train the baseline (no regularisation, no scheduler, no early stopping)\ntorch.manual_seed(SEED)\nmodel_m0 = BaselineCNN(n_classes=len(CLASSES)).to(device)\nhist_m0, best_map_m0, time_m0 = train_model(model_m0, \"M0\", epochs=30, lr=1e-3)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-10-07T10:20:00.394241Z","iopub.execute_input":"2026-10-07T10:20:00.395024Z","iopub.status.idle":"2026-10-07T10:20:47.775508Z","shell.execute_reply.started":"2026-10-07T10:20:00.394999Z","shell.execute_reply":"2026-10-07T10:20:47.77472Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Cell 7: Improved model M1 (deeper CNN + BatchNorm + Dropout) and training\ndef conv_block(in_ch, out_ch):\n    return nn.Sequential(\n        nn.Conv2d(in_ch, out_ch, 3, padding=1),\n        nn.BatchNorm2d(out_ch),\n        nn.ReLU(),\n        nn.MaxPool2d(2),\n    )\n\nclass ImprovedCNN(nn.Module):\n    def __init__(self, n_classes=41, dropout=0.3):\n        super().__init__()\n        self.features = nn.Sequential(\n            conv_block(1, 32),\n            conv_block(32, 64),\n            conv_block(64, 128),\n            conv_block(128, 256),\n        )\n        self.pool = nn.AdaptiveAvgPool2d(1)\n        self.dropout = nn.Dropout(dropout)\n        self.classifier = nn.Linear(256, n_classes)\n\n    def forward(self, x):\n        x = self.features(x)\n        x = self.pool(x).flatten(1)\n        x = self.dropout(x)\n        return self.classifier(x)\n\ntorch.manual_seed(SEED)\nmodel_m1 = ImprovedCNN(n_classes=len(CLASSES), dropout=0.3).to(device)\nprint(model_m1)\nprint(\"\\nTotal trainable parameters:\",\n      sum(p.numel() for p in model_m1.parameters() if p.requires_grad))\n\nhist_m1, best_map_m1, time_m1 = train_model(\n    model_m1, \"M1\", epochs=40, lr=1e-3, weight_decay=1e-4,\n    use_scheduler=True, patience=8)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-10-07T10:20:47.776629Z","iopub.execute_input":"2026-10-07T10:20:47.777116Z","iopub.status.idle":"2026-10-07T10:23:37.855657Z","shell.execute_reply.started":"2026-10-07T10:20:47.777092Z","shell.execute_reply":"2026-10-07T10:23:37.854949Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Cell 8: M2 = same CNN as M1 + data augmentation on training batches only\ndef augment_batch(x):\n    \"\"\"Random frequency mask, time mask and time shift for each spectrogram.\"\"\"\n    x = x.clone()\n    B, _, n_freq, n_time = x.shape\n    for i in range(B):\n        f = random.randint(0, 8)                       # frequency mask width\n        f0 = random.randint(0, n_freq - f)\n        x[i, :, f0:f0 + f, :] = 0\n        t = random.randint(0, 20)                      # time mask width\n        t0 = random.randint(0, n_time - t)\n        x[i, :, :, t0:t0 + t] = 0\n        shift = random.randint(-20, 20)                # circular time shift\n        x[i] = torch.roll(x[i], shifts=shift, dims=2)\n    return x\n\n# Redefine run_epoch so that augmentation is applied only when training\ndef run_epoch(model, loader, optimizer=None):\n    train_mode = optimizer is not None\n    model.train(train_mode)\n    total_loss, total_acc, total_map, n = 0.0, 0.0, 0.0, 0\n    with torch.set_grad_enabled(train_mode):\n        for xb, yb in loader:\n            xb, yb = xb.to(device), yb.to(device)\n            if train_mode and USE_AUG:\n                xb = augment_batch(xb)\n            out = model(xb)\n            loss = F.cross_entropy(out, yb)\n            if train_mode:\n                optimizer.zero_grad()\n                loss.backward()\n                optimizer.step()\n            bs = len(yb)\n            total_loss += loss.item() * bs\n            total_acc += (out.argmax(1) == yb).float().sum().item()\n            total_map += map3(out, yb) * bs\n            n += bs\n    return total_loss / n, total_acc / n, total_map / n\n\nUSE_AUG = True\ntorch.manual_seed(SEED)\nmodel_m2 = ImprovedCNN(n_classes=len(CLASSES), dropout=0.3).to(device)\nhist_m2, best_map_m2, time_m2 = train_model(\n    model_m2, \"M2\", epochs=60, lr=1e-3, weight_decay=1e-4,\n    use_scheduler=True, patience=10)\nUSE_AUG = False","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-10-07T10:23:37.856745Z","iopub.execute_input":"2026-10-07T10:23:37.857053Z","iopub.status.idle":"2026-10-07T10:28:18.107542Z","shell.execute_reply.started":"2026-10-07T10:23:37.85703Z","shell.execute_reply":"2026-10-07T10:28:18.106796Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Cell 9: Evaluation - best checkpoints, learning curves, confusion matrix, results table\nimport matplotlib.pyplot as plt\nfrom sklearn.metrics import confusion_matrix, classification_report\n\ndef get_val_logits(model, name):\n    model.load_state_dict(torch.load(f\"/kaggle/working/{name}_best.pt\", map_location=device))\n    model.eval()\n    outs = []\n    with torch.no_grad():\n        for xb, _ in val_loader:\n            outs.append(model(xb.to(device)).cpu())\n    return torch.cat(outs)\n\nmodels = {\"M0\": model_m0, \"M1\": model_m1, \"M2\": model_m2}\nhists = {\"M0\": hist_m0, \"M1\": hist_m1, \"M2\": hist_m2}\nval_logits = {}\nfor name, m in models.items():\n    val_logits[name] = get_val_logits(m, name)\n    acc = (val_logits[name].argmax(1) == yva_t).float().mean().item()\n    print(f\"{name}: val MAP@3 = {map3(val_logits[name], yva_t):.4f} | val accuracy = {acc:.4f}\")\n\n# Learning curves: loss (top row) and MAP@3 (bottom row) for each model\nfig, axes = plt.subplots(2, 3, figsize=(16, 8))\nfor j, name in enumerate(models):\n    h = hists[name]\n    axes[0, j].plot(h[\"tr_loss\"], label=\"train\")\n    axes[0, j].plot(h[\"va_loss\"], label=\"validation\")\n    axes[0, j].set_title(f\"{name} - loss\")\n    axes[0, j].set_xlabel(\"Epoch\"); axes[0, j].legend()\n    axes[1, j].plot(h[\"tr_map\"], label=\"train\")\n    axes[1, j].plot(h[\"va_map\"], label=\"validation\")\n    axes[1, j].set_title(f\"{name} - MAP@3\")\n    axes[1, j].set_xlabel(\"Epoch\"); axes[1, j].legend()\nplt.tight_layout()\nplt.show()\n\n# Pick the final model by best validation MAP@3\nscores = {\"M0\": best_map_m0, \"M1\": best_map_m1, \"M2\": best_map_m2}\nFINAL = max(scores, key=scores.get)\nprint(\"\\nFinal selected model:\", FINAL)\n\n# Confusion matrix of the final model\npred = val_logits[FINAL].argmax(1).numpy()\ncm = confusion_matrix(y_val, pred)\nplt.figure(figsize=(14, 12))\nplt.imshow(cm, cmap=\"Blues\")\nplt.xticks(range(len(CLASSES)), CLASSES, rotation=90, fontsize=7)\nplt.yticks(range(len(CLASSES)), CLASSES, fontsize=7)\nplt.xlabel(\"Predicted\"); plt.ylabel(\"True\")\nplt.title(f\"Confusion matrix - {FINAL} (validation set)\")\nplt.colorbar()\nplt.tight_layout()\nplt.show()\n\n# Most confused class pairs\ncm_off = cm.copy()\nnp.fill_diagonal(cm_off, 0)\nprint(\"\\nTop 10 most confused pairs (true -> predicted : count):\")\nfor idx in np.argsort(cm_off, axis=None)[::-1][:10]:\n    i, j = divmod(idx, len(CLASSES))\n    print(f\"  {CLASSES[i]} -> {CLASSES[j]} : {cm_off[i, j]}\")\n\n# Per-class precision / recall / F1\nprint(\"\\n\", classification_report(y_val, pred, target_names=CLASSES, digits=3))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-10-07T10:32:42.315201Z","iopub.execute_input":"2026-10-07T10:32:42.31568Z","iopub.status.idle":"2026-10-07T10:32:44.630333Z","shell.execute_reply.started":"2026-10-07T10:32:42.315653Z","shell.execute_reply":"2026-10-07T10:32:44.629766Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Cell 10: Create test features and build the submission file with the final model\n\n# Safe version of the preprocessing: empty or unreadable files become silence\ndef audio_to_logmel_safe(path):\n    try:\n        y, _ = librosa.load(path, sr=SR)\n    except Exception:\n        y = np.zeros(0, dtype=np.float32)\n    if len(y) == 0:\n        y = np.zeros(N_SAMPLES, dtype=np.float32)\n    if len(y) < N_SAMPLES:\n        y = np.pad(y, (0, N_SAMPLES - len(y)), mode=\"wrap\")\n    else:\n        start = (len(y) - N_SAMPLES) // 2\n        y = y[start:start + N_SAMPLES]\n    mel = librosa.feature.melspectrogram(y=y, sr=SR, n_mels=N_MELS, hop_length=HOP)\n    return librosa.power_to_db(mel, ref=np.max).astype(np.float32)\n\n# 1) Convert test audio to log-mel spectrograms (same preprocessing as training)\ntest_paths = [os.path.join(TEST_DIR, f) for f in sample_sub[\"fname\"]]\n\n# Report any empty test files\nempty_files = [os.path.basename(p) for p in test_paths if sf.info(p).frames == 0]\nprint(\"Empty test files found:\", empty_files)\n\ntest_feats = Parallel(n_jobs=-1)(delayed(audio_to_logmel_safe)(p) for p in test_paths)\nX_test = np.stack(test_feats)[..., np.newaxis]\nX_test = (X_test - MEAN) / STD                     # normalise with TRAIN statistics\nXte_t = torch.tensor(X_test).permute(0, 3, 1, 2).float()\ntest_loader = DataLoader(TensorDataset(Xte_t), batch_size=128, shuffle=False)\nprint(\"X_test shape:\", X_test.shape)\n\n# 2) Predict with the final model's best checkpoint\nfinal_model = ImprovedCNN(n_classes=len(CLASSES), dropout=0.3).to(device)\nfinal_model.load_state_dict(torch.load(f\"/kaggle/working/{FINAL}_best.pt\", map_location=device))\nfinal_model.eval()\nlogits = []\nwith torch.no_grad():\n    for (xb,) in test_loader:\n        logits.append(final_model(xb.to(device)).cpu())\nlogits = torch.cat(logits)\ntop3 = torch.topk(logits, 3, dim=1).indices.numpy()\n\n# 3) Build the submission file in the exact sample_submission format\nid_col, label_col = sample_sub.columns[0], sample_sub.columns[1]\nsubmission = pd.DataFrame({\n    id_col: sample_sub[id_col],\n    label_col: [\" \".join(CLASSES[k] for k in row) for row in top3],\n})\nsubmission.to_csv(\"/kaggle/working/submission.csv\", index=False)\n\nprint(\"\\nSample submission format:\")\nprint(sample_sub.head(3))\nprint(\"\\nMy submission:\")\nprint(submission.head(3))\nprint(\"Same columns:\", list(submission.columns) == list(sample_sub.columns),\n      \"| Same rows:\", len(submission) == len(sample_sub))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-10-07T10:35:06.819706Z","iopub.execute_input":"2026-10-07T10:35:06.8206Z","iopub.status.idle":"2026-10-07T10:36:39.633655Z","shell.execute_reply.started":"2026-10-07T10:35:06.820564Z","shell.execute_reply":"2026-10-07T10:36:39.632994Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Check how the class \"Hi-hat\" is spelled in the real test labels\npost = pd.read_csv(os.path.join(DATA_DIR, \"test_post_competition.csv\"))\nlabels = post[\"label\"].dropna().unique()          # padding rows have no label\nprint(\"Number of labelled test files:\", post[\"label\"].notna().sum())\nprint(\"Test labels with 'hat':\", [c for c in labels if \"hat\" in c.lower()])\nprint(\"Our classes with 'hat':\", [c for c in CLASSES if \"hat\" in c.lower()])","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-10-07T10:40:17.077503Z","iopub.execute_input":"2026-10-07T10:40:17.077834Z","iopub.status.idle":"2026-10-07T10:40:17.098607Z","shell.execute_reply.started":"2026-10-07T10:40:17.07781Z","shell.execute_reply":"2026-10-07T10:40:17.097854Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Cell 11: Make one submission file per model + results table\ndef make_submission_file(name, model_class, **kwargs):\n    m = model_class(n_classes=len(CLASSES), **kwargs).to(device)\n    m.load_state_dict(torch.load(f\"/kaggle/working/{name}_best.pt\", map_location=device))\n    m.eval()\n    outs = []\n    with torch.no_grad():\n        for (xb,) in test_loader:\n            outs.append(m(xb.to(device)).cpu())\n    top3_ = torch.topk(torch.cat(outs), 3, dim=1).indices.numpy()\n    sub = pd.DataFrame({\n        id_col: sample_sub[id_col],\n        label_col: [\" \".join(CLASSES[k] for k in row) for row in top3_],\n    })\n    sub.to_csv(f\"/kaggle/working/submission_{name}.csv\", index=False)\n    return sub\n\nmake_submission_file(\"M0\", BaselineCNN)\nmake_submission_file(\"M1\", ImprovedCNN, dropout=0.3)\nmake_submission_file(\"M2\", ImprovedCNN, dropout=0.3)\nprint(\"Files in /kaggle/working:\", sorted(f for f in os.listdir(\"/kaggle/working\") if f.endswith(\".csv\")))\n\n# Results table (Kaggle scores will be filled in after the submissions)\nresults = pd.DataFrame([\n    {\"Model ID\": \"M0 (Baseline)\", \"Architecture\": \"3 Conv + GAP + Linear, no BN/dropout\",\n     \"Key hyperparameters\": \"Adam lr=1e-3, batch 64, 30 epochs\",\n     \"Val MAP@3\": round(best_map_m0, 4), \"Kaggle Public\": \"-\", \"Kaggle Private\": \"-\"},\n    {\"Model ID\": \"M1\", \"Architecture\": \"4 Conv + BatchNorm + Dropout 0.3 + GAP + Linear\",\n     \"Key hyperparameters\": \"Adam lr=1e-3, wd=1e-4, ReduceLROnPlateau, early stop (p=8), 40 epochs\",\n     \"Val MAP@3\": round(best_map_m1, 4), \"Kaggle Public\": \"-\", \"Kaggle Private\": \"-\"},\n    {\"Model ID\": \"M2\", \"Architecture\": \"Same as M1 + SpecAugment & time-shift augmentation\",\n     \"Key hyperparameters\": \"Adam lr=1e-3, wd=1e-4, ReduceLROnPlateau, early stop (p=10), 60 epochs\",\n     \"Val MAP@3\": round(best_map_m2, 4), \"Kaggle Public\": \"-\", \"Kaggle Private\": \"-\"},\n])\npd.set_option(\"display.max_colwidth\", 80)\nprint(results.to_string(index=False))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-10-07T10:41:15.224086Z","iopub.execute_input":"2026-10-07T10:41:15.224413Z","iopub.status.idle":"2026-10-07T10:41:19.257725Z","shell.execute_reply.started":"2026-10-07T10:41:15.224389Z","shell.execute_reply":"2026-10-07T10:41:19.25697Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Cell 12: Fill Kaggle scores into the results table and prepare the final submission file\nimport shutil\n\nresults[\"Kaggle Public\"] = [0.73366, 0.88316, 0.89700]\nresults[\"Kaggle Private\"] = [0.70964, 0.87054, 0.87682]\nresults[\"Public-Private gap\"] = (results[\"Kaggle Public\"] - results[\"Kaggle Private\"]).round(5)\nprint(results[[\"Model ID\", \"Val MAP@3\", \"Kaggle Public\", \"Kaggle Private\", \"Public-Private gap\"]].to_string(index=False))\nprint(\"\\nFinal selected model: M2 (best validation MAP@3 and best Kaggle Private score)\")\n\n# The final submission file (M2) with the required file name\nshutil.copy(\"/kaggle/working/submission_M2.csv\", \"/kaggle/working/2329220_submission.csv\")\nprint(\"Final file saved as 2329220_submission.csv\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-10-07T10:49:28.700364Z","iopub.execute_input":"2026-10-07T10:49:28.701221Z","iopub.status.idle":"2026-10-07T10:49:28.71538Z","shell.execute_reply.started":"2026-10-07T10:49:28.701191Z","shell.execute_reply":"2026-10-07T10:49:28.714587Z"}},"outputs":[],"execution_count":null}]}