{"metadata":{"kernelspec":{"display_name":"Python 3","language":"python","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":70203,"databundleVersionId":8068726},{"sourceType":"datasetVersion","sourceId":8209908,"datasetId":4865209,"databundleVersionId":8334538},{"sourceType":"modelInstanceVersion","sourceId":516989,"databundleVersionId":13353982,"modelInstanceId":404337,"modelId":319},{"sourceType":"kernelVersion","sourceId":314625876},{"sourceType":"kernelVersion","sourceId":314648639},{"sourceType":"kernelVersion","sourceId":314956547}],"dockerImageVersionId":31329,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"# BirdCLEF 2024: Day 5 - Premium MLP Head Training\nThis notebook implements a **Residual MLP Head** with **Logit-Adjusted Focal Loss**, **Focal-Soundscape Mixup**, and **Per-Class Temperature Calibration**. It is designed to run on Kaggle using cached embeddings.","metadata":{}},{"cell_type":"markdown","source":"## 1. Imports & Core Components\nWe include the `src` logic here so the notebook is self-contained for Kaggle.","metadata":{}},{"cell_type":"code","source":"import torch\nimport torch.nn as nn\nimport torch.nn.functional as F\nfrom torch.utils.data import Dataset, DataLoader, WeightedRandomSampler\nimport pandas as pd\nimport numpy as np\nfrom pathlib import Path\nfrom tqdm.auto import tqdm\nimport matplotlib.pyplot as plt\nimport seaborn as sns\nfrom sklearn.metrics import roc_auc_score\n\n# --- 1.1 Model (Residual MLP + Temperature Scaling) ---\nclass ResidualBlock(nn.Module):\n    def __init__(self, dim, dropout=0.3):\n        super().__init__()\n        self.net = nn.Sequential(\n            nn.Linear(dim, dim),\n            nn.LayerNorm(dim),\n            nn.GELU(),\n            nn.Dropout(dropout)\n        )\n    def forward(self, x): return x + self.net(x)\n\nclass MLPHead(nn.Module):\n    def __init__(self, in_dim=1536, hidden_dim=512, n_classes=182, dropout=0.3):\n        super().__init__()\n        self.input_layer = nn.Sequential(nn.Linear(in_dim, hidden_dim), nn.LayerNorm(hidden_dim), nn.GELU())\n        self.res_block = ResidualBlock(hidden_dim, dropout)\n        self.output_layer = nn.Linear(hidden_dim, n_classes)\n        self.temperature = nn.Parameter(torch.ones(n_classes))\n    def forward(self, x, apply_calibration=True):\n        x = self.input_layer(x)\n        x = self.res_block(x)\n        logits = self.output_layer(x)\n        if apply_calibration:\n            t = torch.nn.functional.softplus(self.temperature) + 1e-4\n            logits = logits / t\n        return logits\n\n# --- 1.2 Loss (Logit-Adjusted Focal BCE) ---\nclass LogitAdjustedFocalBCE(nn.Module):\n    def __init__(self, log_prior, tau=1.0, gamma=2.0, alpha=0.25):\n        super().__init__()\n        self.register_buffer(\"log_prior\", log_prior)\n        self.tau = tau; self.gamma = gamma; self.alpha = alpha\n    def forward(self, logits, targets):\n        logits_adj = logits + self.tau * self.log_prior\n        bce_loss = F.binary_cross_entropy_with_logits(logits_adj, targets, reduction='none')\n        p = torch.sigmoid(logits_adj)\n        p_t = p * targets + (1 - p) * (1 - targets)\n        focal_weight = (1 - p_t) ** self.gamma\n        alpha_weight = self.alpha * targets + (1 - self.alpha) * (1 - targets)\n        return (alpha_weight * focal_weight * bce_loss).mean()\n\n# --- 1.3 Dataset (Focal-Soundscape Mixup) ---\nclass MixupEmbeddingDataset(Dataset):\n    def __init__(self, focal_emb, focal_y, sound_emb=None, sound_y=None, alpha=0.4, p_mix=0.5):\n        self.focal_emb = torch.tensor(focal_emb, dtype=torch.float32)\n        self.focal_y = torch.tensor(focal_y, dtype=torch.float32)\n        self.sound_emb = torch.tensor(sound_emb, dtype=torch.float32) if sound_emb is not None else None\n        self.sound_y = torch.tensor(sound_y, dtype=torch.float32) if sound_y is not None else None\n        self.alpha = alpha; self.p_mix = p_mix\n    def __len__(self): return len(self.focal_emb)\n    def __getitem__(self, idx):\n        x, y = self.focal_emb[idx], self.focal_y[idx]\n        if self.sound_emb is not None and np.random.rand() < self.p_mix:\n            j = np.random.randint(0, len(self.sound_emb))\n            lam = np.random.beta(self.alpha, self.alpha)\n            x = lam * x + (1 - lam) * self.sound_emb[j]\n            y = lam * y + (1 - lam) * self.sound_y[j]\n        return x, y","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-28T16:45:13.751631Z","iopub.execute_input":"2026-04-28T16:45:13.751912Z","iopub.status.idle":"2026-04-28T16:45:19.414497Z","shell.execute_reply.started":"2026-04-28T16:45:13.751887Z","shell.execute_reply":"2026-04-28T16:45:19.413851Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 2. Configuration & Paths","metadata":{}},{"cell_type":"code","source":"IS_KAGGLE = Path('/kaggle/input').exists()\nINPUT_DIR = Path('/kaggle/input') if IS_KAGGLE else Path('../data')\nOUTPUT_DIR = Path('./') if IS_KAGGLE else Path('../cache')\n\nEPOCHS = 50\nBATCH_SIZE = 256\nLR = 1e-3\nTAU = 1.0       # Logit adjustment strength\nP_MIX = 0.5     # Focal-Soundscape Mixup prob\nDEVICE = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\n\nprint(f\"Running on {'Kaggle' if IS_KAGGLE else 'Local'} | Device: {DEVICE}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-28T16:45:19.415674Z","iopub.execute_input":"2026-04-28T16:45:19.41633Z","iopub.status.idle":"2026-04-28T16:45:19.671862Z","shell.execute_reply.started":"2026-04-28T16:45:19.416302Z","shell.execute_reply":"2026-04-28T16:45:19.671039Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 3. Data Loading\nHere we load the focus species, training embeddings, and the pseudo-labels generated in Day 4.","metadata":{}},{"cell_type":"code","source":"# 3.1 Load All Species (Master List)\n# We keep the name 'focus_species' so the rest of your code doesn't break\ntry:\n    # Pull the official competition species list (182 total)\n    meta_df = pd.read_csv(INPUT_DIR / 'competitions/birdclef-2024/train_metadata.csv')\n    focus_species = sorted(meta_df['primary_label'].unique())\n    n_classes = len(focus_species)\n    print(f\"Loaded all {n_classes} competition species into focus_species.\")\nexcept Exception as e:\n    print(f\"Error loading metadata: {e}. Falling back to 182 placeholders.\")\n    n_classes = 182\n    focus_species = [f'sp_{i}' for i in range(182)]\n\n# 3.2 Load Focal Embeddings\ntrain_files = list(INPUT_DIR.rglob('perch_train*.parquet'))\nif train_files:\n    train_df = pd.concat([pd.read_parquet(f) for f in train_files]).reset_index(drop=True)\n    focal_emb = np.stack(train_df['emb'].values)\n    focal_y = np.zeros((len(focal_emb), n_classes))\n    \n    # Map each species_code to its index in our master list\n    # Using a dictionary for 10x faster mapping\n    sp_to_idx = {sp: i for i, sp in enumerate(focus_species)}\n    for i, sp in enumerate(train_df['species_code']):\n        if sp in sp_to_idx:\n            focal_y[i, sp_to_idx[sp]] = 1\n            \n    print(f\"Loaded {len(focal_emb)} focal embeddings mapped to {n_classes} classes.\")\nelse:\n    print(\"No training embeddings found!\")\n    focal_emb, focal_y = np.zeros((100, 1536)), np.zeros((100, n_classes))\n\nimport ast  # For safely parsing the list string in the CSV\n\n# 3.3 Load Pseudo-Labels (Kaggle Engine Output)\ntry:\n    # 1. Locate the files\n    pseudo_csv_path = next(INPUT_DIR.rglob('pseudo_labels_v1.csv'))\n    pseudo_npy_path = next(INPUT_DIR.rglob('unlabeled_embeddings_v1.npy'))\n    \n    # 2. Load the CSV metadata\n    pseudo_df = pd.read_csv(pseudo_csv_path)\n    \n    # 3. Load the raw embeddings\n    sound_emb = np.load(pseudo_npy_path)\n    \n    # 4. Parse the 'pred' column (converts string \"[0.1, -0.2...]\" to a numpy array)\n    # If the CSV 'pred' column is a string representation of a list:\n    def parse_pred(pred_str):\n        # Cleans up potential formatting issues (multiple spaces, newlines)\n        cleaned = pred_str.replace('[', '').replace(']', '').replace('\\n', ' ').split()\n        return np.array([float(x) for x in cleaned])\n\n    print(\"Parsing pseudo-label logits...\")\n    logits_list = [parse_pred(p) for p in tqdm(pseudo_df['pred'].values)]\n    sound_logits = np.stack(logits_list)\n    \n    # 5. Convert logits to soft pseudo-labels using sigmoid\n    sound_y = 1 / (1 + np.exp(-sound_logits))\n    \n    print(f\"Loaded {len(sound_emb)} pseudo-labeled chunks from {pseudo_csv_path.name}\")\n    print(f\"Embeddings shape: {sound_emb.shape}, Labels shape: {sound_y.shape}\")\n\nexcept Exception as e:\n    print(f\"Error loading pseudo-label engine output: {e}\")\n    print(\"Check if both pseudo_labels_v1.csv and unlabeled_embeddings_v1.npy are attached.\")\n    sound_emb, sound_y = None, None\n\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-28T16:45:19.67292Z","iopub.execute_input":"2026-04-28T16:45:19.673386Z","iopub.status.idle":"2026-04-28T16:47:24.025087Z","shell.execute_reply.started":"2026-04-28T16:45:19.673349Z","shell.execute_reply":"2026-04-28T16:47:24.024475Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 4. Training Loop","metadata":{}},{"cell_type":"code","source":"# Setup\nclass_counts = focal_y.sum(axis=0) + (sound_y.sum(axis=0) if sound_y is not None else 1)\nlog_prior = torch.log(torch.tensor(class_counts / class_counts.sum()) + 1e-12).to(DEVICE)\n\ndataset = MixupEmbeddingDataset(focal_emb, focal_y, sound_emb, sound_y, p_mix=P_MIX)\nfreq = focal_y.sum(axis=0) + 1\nsample_weights = (focal_y * (1.0 / freq)).sum(axis=1)\nsampler = WeightedRandomSampler(sample_weights, num_samples=len(sample_weights), replacement=True)\ndataloader = DataLoader(dataset, batch_size=BATCH_SIZE, sampler=sampler)\n\nmodel = MLPHead(in_dim=focal_emb.shape[1], n_classes=n_classes).to(DEVICE)\ncriterion = LogitAdjustedFocalBCE(log_prior, tau=TAU)\noptimizer = torch.optim.AdamW(model.parameters(), lr=LR, weight_decay=1e-4)\nscheduler = torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=EPOCHS)\n\n# Run\nhistory = []\nfor epoch in range(EPOCHS):\n    model.train()\n    epoch_loss = 0\n    for x, y in tqdm(dataloader, desc=f\"Epoch {epoch+1}\", leave=False):\n        x, y = x.to(DEVICE), y.to(DEVICE)\n        optimizer.zero_grad()\n        loss = criterion(model(x, apply_calibration=False), y)\n        loss.backward(); optimizer.step()\n        epoch_loss += loss.item()\n    scheduler.step()\n    history.append(epoch_loss/len(dataloader))\n    if (epoch+1) % 10 == 0: print(f\"Epoch {epoch+1} | Loss: {history[-1]:.4f}\")\n\nplt.plot(history); plt.title(\"Loss Curve\"); plt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-28T16:47:24.026536Z","iopub.execute_input":"2026-04-28T16:47:24.026756Z","iopub.status.idle":"2026-04-28T16:47:56.937255Z","shell.execute_reply.started":"2026-04-28T16:47:24.026728Z","shell.execute_reply":"2026-04-28T16:47:56.93657Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 5. Per-Class Temperature Calibration (Upgrade #8)\nWe fit the temperature parameters on a small validation subset to fix the long-tail bias.","metadata":{}},{"cell_type":"code","source":"model.eval()\nwith torch.no_grad():\n    # Using a subset of training as a proxy for val if no separate val provided\n    val_logits = model(torch.tensor(focal_emb[:2000]).to(DEVICE), apply_calibration=False)\n    val_targets = torch.tensor(focal_y[:2000]).to(DEVICE)\n\n# Optimization for T\ntemp_optimizer = torch.optim.Adam([model.temperature], lr=0.01)\nfor _ in range(200):\n    temp_optimizer.zero_grad()\n    t = torch.nn.functional.softplus(model.temperature) + 1e-4\n    loss = F.binary_cross_entropy_with_logits(val_logits / t, val_targets)\n    loss.backward(); temp_optimizer.step()\n\nprint(\"Temperature Calibration Complete.\")\nprint(f\"Mean Temperature: {torch.nn.functional.softplus(model.temperature).mean().item():.4f}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-28T16:47:56.938203Z","iopub.execute_input":"2026-04-28T16:47:56.938755Z","iopub.status.idle":"2026-04-28T16:47:57.127221Z","shell.execute_reply.started":"2026-04-28T16:47:56.938706Z","shell.execute_reply":"2026-04-28T16:47:57.126414Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 6. Evaluation & Bucket Reporting (Upgrade #3)","metadata":{}},{"cell_type":"code","source":"# --- 6. Realistic Evaluation & Proper Bucketing ---\nfrom sklearn.model_selection import train_test_split\n\n# 1. Get the REAL counts from the original metadata for bucketing\n# This is the \"ground truth\" of how rare a species is in the wild.\nreal_counts = meta_df['primary_label'].value_counts().to_dict()\n\n# 2. Evaluation\nmodel.eval()\nwith torch.no_grad():\n    # Note: For a real AUC, you should use a held-out val set. \n    # Here we still use focal_emb but we'll fix the bucket logic.\n    preds = torch.sigmoid(model(torch.tensor(focal_emb).to(DEVICE))).cpu().numpy()\n\naucs = {}\nfor i, sp in enumerate(focus_species):\n    # Only compute AUC if the species actually exists in this focal set\n    if focal_y[:, i].sum() > 0:\n        aucs[sp] = roc_auc_score(focal_y[:, i], preds[:, i])\n\n# 3. Proper Bucket Reporting based on ORIGINAL abundance\nbuckets = {'<10': [], '10-50': [], '50-200': [], '200+': []}\nfor sp, auc in aucs.items():\n    # Use the count from the full competition metadata\n    cnt = real_counts.get(sp, 0) \n    \n    if cnt < 10: buckets['<10'].append(auc)\n    elif cnt < 50: buckets['10-50'].append(auc)\n    elif cnt < 200: buckets['50-200'].append(auc)\n    else: buckets['200+'].append(auc)\n\nprint(\"\\n--- Per-Bucket Macro AUC (Based on Training Abundance) ---\")\nfor b, vals in buckets.items():\n    if len(vals) > 0:\n        print(f\"Bucket {b:6}: {np.mean(vals):.4f} ({len(vals)} species)\")\n    else:\n        print(f\"Bucket {b:6}: No species in this set\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-28T16:51:35.295191Z","iopub.execute_input":"2026-04-28T16:51:35.295982Z","iopub.status.idle":"2026-04-28T16:51:36.053101Z","shell.execute_reply.started":"2026-04-28T16:51:35.295947Z","shell.execute_reply":"2026-04-28T16:51:36.052219Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 7. Save Artifacts","metadata":{}},{"cell_type":"code","source":"torch.save(model.state_dict(), 'mlp_head_final.pth')\nprint(\"Model saved as mlp_head_final.pth\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-28T16:47:57.874445Z","iopub.execute_input":"2026-04-28T16:47:57.874766Z","iopub.status.idle":"2026-04-28T16:47:57.894988Z","shell.execute_reply.started":"2026-04-28T16:47:57.874736Z","shell.execute_reply":"2026-04-28T16:47:57.89443Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# --- 8. Final Soundscape Evaluation (NaN-Proof) ---\nprint(\"Computing NaN-proof Macro AUC...\")\n\ntry:\n    # ... [Same merge logic as before] ...\n    ss_df['row_id'] = ss_df['audio_id'].astype(str) + \"_\" + ss_df['time'].astype(str)\n    merged_df = pd.merge(eval_labels_df, ss_df[['row_id', 'pred_idx']], on='row_id', how='inner')\n\n    indices = merged_df['pred_idx'].values\n    y_true = merged_df[species_in_eval].values\n    model_indices = [focus_species.index(sp) for sp in species_in_eval]\n    y_pred = ss_probs[indices][:, model_indices]\n\n    # COMPUTE PER-CLASS AUC AND SKIP NANS\n    per_class_aucs = []\n    skipped_species = 0\n    \n    for i in range(y_true.shape[1]):\n        # Check if the class has both 0 and 1 in the ground truth\n        if len(np.unique(y_true[:, i])) > 1:\n            class_auc = roc_auc_score(y_true[:, i], y_pred[:, i])\n            per_class_aucs.append(class_auc)\n        else:\n            skipped_species += 1\n            \n    final_macro_auc = np.mean(per_class_aucs)\n    \n    print(f\"\\n✅ SUCCESS! REAL SOUNDSCAPE MACRO AUC: {final_macro_auc:.4f}\")\n    print(f\"Evaluated on {len(per_class_aucs)} species (Skipped {skipped_species} species not present in this subset).\")\n    print(f\"Total chunks matched: {len(merged_df)}\")\n\nexcept Exception as e:\n    print(f\"❌ Final Evaluation Error: {e}\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-28T16:59:41.805208Z","iopub.execute_input":"2026-04-28T16:59:41.806145Z","iopub.status.idle":"2026-04-28T16:59:42.190657Z","shell.execute_reply.started":"2026-04-28T16:59:41.80609Z","shell.execute_reply":"2026-04-28T16:59:42.190044Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# --- 9. Detailed Performance Analytics (Rare vs. Common) ---\nprint(\"\\n--- PERFORMANCE ANALYTICS BY ABUNDANCE ---\")\n\n# 1. Bucket definitions\nrare_thresholds = {\n    'Very Rare (<10)': (0, 10),\n    'Rare (10-50)': (10, 50),\n    'Mid (50-200)': (50, 200),\n    'Common (>200)': (200, 100000)\n}\n\n# 2. Map AUCs to Buckets\nbucket_results = {name: [] for name in rare_thresholds.keys()}\nper_sp_auc = {}\n\nfor i, sp in enumerate(species_in_eval):\n    if len(np.unique(y_true[:, i])) > 1:\n        auc = roc_auc_score(y_true[:, i], y_pred[:, i])\n        per_sp_auc[sp] = auc\n        \n        # Determine bucket from original metadata counts\n        cnt = real_counts.get(sp, 0)\n        for name, (low, high) in rare_thresholds.items():\n            if low <= cnt < high:\n                bucket_results[name].append(auc)\n                break\n\n# 3. Print the Scientific Report\nprint(f\"{'Bucket Name':<20} | {'Macro AUC':<10} | {'Species Count':<15}\")\nprint(\"-\" * 55)\n\nall_aucs = []\nfor name, aucs in bucket_results.items():\n    if len(aucs) > 0:\n        avg_auc = np.mean(aucs)\n        all_aucs.append(avg_auc)\n        print(f\"{name:<20} | {avg_auc:.4f}     | {len(aucs):<15}\")\n    else:\n        print(f\"{name:<20} | N/A        | 0\")\n\nprint(\"-\" * 55)\nprint(f\"{'OVERALL SOUNDSCAPE':<20} | {np.mean(all_aucs):.4f}     | {len(per_sp_auc)} Total\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-28T17:01:18.776205Z","iopub.execute_input":"2026-04-28T17:01:18.776516Z","iopub.status.idle":"2026-04-28T17:01:19.113345Z","shell.execute_reply.started":"2026-04-28T17:01:18.776491Z","shell.execute_reply":"2026-04-28T17:01:19.112712Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# --- 9. Mega-Ensemble Style Performance Analytics ---\nprint(\"\\n--- SCIENTIFIC PERFORMANCE BREAKDOWN BY ABUNDANCE ---\")\n\n# 1. Official Bucket Definitions (from Mega-Ensemble)\nabundance_buckets = {\n    'Rare (<50)': (0, 50),\n    'Medium (50-200)': (50, 200),\n    'Common (>200)': (200, 100000)\n}\n\n# 2. sub-category for Very Rare (tracking endemics)\nvery_rare_threshold = 10\n\n# 3. Calculate AUCs and map to buckets\nbucket_results = {name: [] for name in abundance_buckets.keys()}\nvery_rare_aucs = []\n\nfor i, sp in enumerate(species_in_eval):\n    if len(np.unique(y_true[:, i])) > 1:\n        auc = roc_auc_score(y_true[:, i], y_pred[:, i])\n        \n        cnt = real_counts.get(sp, 0)\n        \n        # Track \"Very Rare\" separately as a highlight\n        if cnt < very_rare_threshold:\n            very_rare_aucs.append(auc)\n            \n        # Standard Mega-Ensemble Bucketing\n        for name, (low, high) in abundance_buckets.items():\n            if low <= cnt < high:\n                bucket_results[name].append(auc)\n                break\n\n# 4. Final Scientific Report\nprint(f\"{'Abundance Bucket':<20} | {'Macro AUC':<10} | {'Species Count':<15}\")\nprint(\"-\" * 55)\n\nfor name, aucs in bucket_results.items():\n    if len(aucs) > 0:\n        print(f\"{name:<20} | {np.mean(aucs):.4f}     | {len(aucs):<15}\")\n    else:\n        print(f\"{name:<20} | N/A        | 0\")\n\nprint(\"-\" * 55)\nif very_rare_aucs:\n    print(f\"{'Very Rare (<10) Highlight':<20} | {np.mean(very_rare_aucs):.4f}     | {len(very_rare_aucs):<15}\")\nprint(f\"{'TOTAL SOUNDSCAPE':<20} | {final_ss_auc:.4f}     | {len(per_class_aucs)} species\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-28T17:06:55.77641Z","iopub.execute_input":"2026-04-28T17:06:55.777077Z","iopub.status.idle":"2026-04-28T17:06:56.134691Z","shell.execute_reply.started":"2026-04-28T17:06:55.77701Z","shell.execute_reply":"2026-04-28T17:06:56.133973Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}