The ICML 2013 Whale Challenge - Right Whale Redux
[4]
# Since mlebench is not available, we'll create a test submission first,
# and use scikit-learn's AUC metric for validation
import os
import pandas as pd
import numpy as np
from sklearn.metrics import roc_auc_score
# Data paths
DATA_ROOT = '/mnt/workspaces/trenton/mle-bench/data/the-icml-2013-whale-challenge-right-whale-redux/prepared/public'
TRAIN_DIR = os.path.join(DATA_ROOT, 'train', 'train2')
TEST_DIR = os.path.join(DATA_ROOT, 'test', 'test2')
SAMPLE_SUB = os.path.join(DATA_ROOT, 'sampleSubmission.csv')
OUTPUT_DIR = '/mnt/workspaces/trenton/qualia/benchmark_results/mle_bench_low/2026-03-01_06-24-13/row_21'
DRAFT_DIR = os.path.join(OUTPUT_DIR, 'drafts')
SUBMISSION_PATH = os.path.join(OUTPUT_DIR, 'submission.csv')
os.makedirs(DRAFT_DIR, exist_ok=True)
# Count files
train_files = os.listdir(TRAIN_DIR)
test_files = os.listdir(TEST_DIR)
print(f"Train files: {len(train_files)}, Test files: {len(test_files)}")Train files: 22692, Test files: 25149
Executed in 267ms
[5]
# Try to find mlebench
import subprocess
import sys
# Check pip packages
result = subprocess.run([sys.executable, '-m', 'pip', 'list'], capture_output=True, text=True)
mle_packages = [p for p in result.stdout.split('\n') if 'mle' in p.lower()]
print("mle-related packages:", mle_packages)
# Try adding common paths
for path in ['/opt', '/usr/local/lib', '/mnt/workspaces']:
for p in [
f'{path}/mlebench',
f'{path}/mle-bench',
]:
if os.path.exists(p):
print(f"Found: {p}")
if p not in sys.path:
sys.path.insert(0, p)mle-related packages: []
Executed in 266ms
[6]
# Let's try importing from a specific path - mle-bench might be installed in editable mode
import subprocess
result = subprocess.run(['pip', 'show', '-f', 'mle-bench'], capture_output=True, text=True)
print("mle-bench package:", result.stdout or result.stderr)
# Also check via direct import attempt
try:
import mlebench
print("mlebench imported successfully from:", mlebench.__file__)
except ImportError as e:
print(f"Import error: {e}")
# Check what's importable
result2 = subprocess.run(['python3', '-c', 'from mlebench.registry import registry; print(registry)'],
capture_output=True, text=True)
print("Direct python call:", result2.stdout or result2.stderr)mle-bench package: [33mWARNING: Package(s) not found: mle-bench[0m[33m [0m Import error: No module named 'mlebench' Direct python call: <mlebench.registry.Registry object at 0x7ac7b8e1e6c0>
Executed in 265ms
[7]
# The mlebench is available via python3 but not this kernel - let's use subprocess for scoring
import subprocess
import json
def score_submission(csv_path: str) -> dict:
"""Score a submission CSV using subprocess to call mlebench."""
code = f'''
import pandas as pd
import json
from mlebench.registry import registry
comp = registry.get_competition('the-icml-2013-whale-challenge-right-whale-redux')
answers = pd.read_csv(comp.answers)
leaderboard = pd.read_csv(comp.leaderboard)
sub = pd.read_csv("{csv_path}")
score = comp.grader(sub, answers)
if score is None:
print(json.dumps({{"error": "Grader returned None"}}))
else:
medal = comp.grader.rank_score(score, leaderboard)
lb = leaderboard['score']
lower = comp.grader.is_lower_better(leaderboard)
rank = int((lb < score).sum()) + 1 if lower else int((lb > score).sum()) + 1
result = {{"score": score, "rank": str(rank / len(lb)), **medal}}
print(json.dumps(result))
'''
result = subprocess.run(['python3', '-c', code], capture_output=True, text=True)
if result.returncode != 0:
print(f"Error: {result.stderr}")
return None
try:
output = json.loads(result.stdout.strip())
print(output)
return output
except:
print(f"Output: {result.stdout}")
print(f"Stderr: {result.stderr}")
return None
print("score_submission function ready!")score_submission function ready!
Executed in 264ms
[8]
# Create a naive baseline - predict the training class probability for all test samples
sample_sub = pd.read_csv(SAMPLE_SUB)
print(f"Sample submission shape: {sample_sub.shape}")
print(sample_sub.head())
# Training class distribution
train_data = []
for f in train_files:
label = 1 if f.endswith('_1.aif') else 0
train_data.append({'file': f, 'label': label})
train_df = pd.DataFrame(train_data)
whale_prob = train_df['label'].mean()
print(f"\nWhale probability in training: {whale_prob:.4f}")Sample submission shape: (25149, 2)
clip probability
0 20090330_000000_015s2ms_Test0.aif 0
1 20090330_000000_020s0ms_Test1.aif 0
2 20090330_000000_025s8ms_Test2.aif 0
3 20090330_000000_028s0ms_Test3.aif 0
4 20090330_000000_035s4ms_Test4.aif 0
Whale probability in training: 0.0999
Executed in 263ms
[9]
# Create naive baseline submission - predict mean probability
naive_sub = sample_sub.copy()
naive_sub['probability'] = whale_prob
# Save to drafts
naive_path = os.path.join(DRAFT_DIR, 'submission_naive_baseline.csv')
naive_sub.to_csv(naive_path, index=False)
print(f"Saved naive baseline to {naive_path}")
# Score it
print("\nScoring naive baseline:")
score_submission(naive_path)Saved naive baseline to /mnt/workspaces/trenton/qualia/benchmark_results/mle_bench_low/2026-03-01_06-24-13/row_21/drafts/submission_naive_baseline.csv
Scoring naive baseline:
{'score': 0.5, 'rank': '0.9224806201550387', 'gold_medal': False, 'silver_medal': False, 'bronze_medal': False, 'above_median': False, 'gold_threshold': 0.98961, 'silver_threshold': 0.95017, 'bronze_threshold': 0.90521, 'median_threshold': 0.86521}
{'score': 0.5,
'rank': '0.9224806201550387',
'gold_medal': False,
'silver_medal': False,
'bronze_medal': False,
'above_median': False,
'gold_threshold': 0.98961,
'silver_threshold': 0.95017,
'bronze_threshold': 0.90521,
'median_threshold': 0.86521}Executed in 262ms
[11]
# Let's explore the audio files to understand the data format
import soundfile as sf
import librosa
# Load a sample audio file
sample_whale = [f for f in train_files if f.endswith('_1.aif')][0]
sample_noise = [f for f in train_files if f.endswith('_0.aif')][0]
whale_path = os.path.join(TRAIN_DIR, sample_whale)
noise_path = os.path.join(TRAIN_DIR, sample_noise)
# Load audio
y_whale, sr_whale = librosa.load(whale_path, sr=None)
y_noise, sr_noise = librosa.load(noise_path, sr=None)
print(f"Whale audio: {sample_whale}")
print(f" Sample rate: {sr_whale}, Duration: {len(y_whale)/sr_whale:.3f}s, Samples: {len(y_whale)}")
print(f"\nNoise audio: {sample_noise}")
print(f" Sample rate: {sr_noise}, Duration: {len(y_noise)/sr_noise:.3f}s, Samples: {len(y_noise)}")Whale audio: 20090328_013000_5774s6ms_TRAIN799_1.aif Sample rate: 2000, Duration: 2.000s, Samples: 4000 Noise audio: 20090329_113000_41721s6ms_TRAIN14314_0.aif Sample rate: 2000, Duration: 2.000s, Samples: 4000
Executed in 261ms
[12]
import matplotlib.pyplot as plt
# Visualize waveforms and spectrograms
fig, axes = plt.subplots(2, 2, figsize=(14, 8))
# Whale waveform
axes[0, 0].plot(y_whale)
axes[0, 0].set_title(f'Whale Call Waveform')
axes[0, 0].set_xlabel('Sample')
# Noise waveform
axes[0, 1].plot(y_noise)
axes[0, 1].set_title(f'Noise Waveform')
axes[0, 1].set_xlabel('Sample')
# Whale spectrogram
D_whale = librosa.amplitude_to_db(np.abs(librosa.stft(y_whale)), ref=np.max)
librosa.display.specshow(D_whale, sr=sr_whale, x_axis='time', y_axis='hz', ax=axes[1, 0])
axes[1, 0].set_title('Whale Spectrogram')
# Noise spectrogram
D_noise = librosa.amplitude_to_db(np.abs(librosa.stft(y_noise)), ref=np.max)
librosa.display.specshow(D_noise, sr=sr_noise, x_axis='time', y_axis='hz', ax=axes[1, 1])
axes[1, 1].set_title('Noise Spectrogram')
plt.tight_layout()
plt.savefig(os.path.join(OUTPUT_DIR, 'audio_visualization.png'), dpi=150)
plt.show()Executed in 260ms
[13]
# Mel-spectrogram feature extractor for CNN training
import torch
import torch.nn as nn
from torch.utils.data import Dataset, DataLoader
from tqdm import tqdm
def audio_to_melspec(audio_path, n_mels=64, n_fft=512, hop_length=64):
"""Convert audio file to mel-spectrogram."""
y, sr = librosa.load(audio_path, sr=None)
mel_spec = librosa.feature.melspectrogram(y=y, sr=sr, n_mels=n_mels, n_fft=n_fft, hop_length=hop_length)
mel_spec_db = librosa.power_to_db(mel_spec, ref=np.max)
return mel_spec_db
# Test the function
test_spec = audio_to_melspec(whale_path)
print(f"Mel-spectrogram shape: {test_spec.shape}")
print(f"Value range: [{test_spec.min():.2f}, {test_spec.max():.2f}]")Mel-spectrogram shape: (64, 63) Value range: [-42.39, 0.00]
Executed in 259ms
[14]
class WhaleAudioDataset(Dataset):
"""Dataset for whale audio classification."""
def __init__(self, file_list, labels, audio_dir, n_mels=64, n_fft=512, hop_length=64, augment=False):
self.file_list = file_list
self.labels = labels
self.audio_dir = audio_dir
self.n_mels = n_mels
self.n_fft = n_fft
self.hop_length = hop_length
self.augment = augment
def __len__(self):
return len(self.file_list)
def __getitem__(self, idx):
audio_path = os.path.join(self.audio_dir, self.file_list[idx])
y, sr = librosa.load(audio_path, sr=None)
# Simple augmentation: time shift
if self.augment and np.random.random() < 0.5:
shift = np.random.randint(-200, 200)
y = np.roll(y, shift)
mel_spec = librosa.feature.melspectrogram(y=y, sr=sr, n_mels=self.n_mels,
n_fft=self.n_fft, hop_length=self.hop_length)
mel_spec_db = librosa.power_to_db(mel_spec, ref=np.max)
# Normalize to [0, 1]
mel_spec_norm = (mel_spec_db - mel_spec_db.min()) / (mel_spec_db.max() - mel_spec_db.min() + 1e-8)
# Add channel dimension for CNN
mel_spec_tensor = torch.FloatTensor(mel_spec_norm).unsqueeze(0)
if self.labels is not None:
return mel_spec_tensor, torch.FloatTensor([self.labels[idx]])
return mel_spec_tensor
print("WhaleAudioDataset class defined")WhaleAudioDataset class defined
Executed in 258ms
[15]
# Prepare train/val splits
from sklearn.model_selection import train_test_split
# Get all training files and labels
train_files_list = sorted(train_files)
train_labels = [1 if f.endswith('_1.aif') else 0 for f in train_files_list]
# Stratified split
X_train, X_val, y_train, y_val = train_test_split(
train_files_list, train_labels, test_size=0.2, random_state=42, stratify=train_labels
)
print(f"Train: {len(X_train)} samples, Val: {len(X_val)} samples")
print(f"Train positive rate: {sum(y_train)/len(y_train):.4f}")
print(f"Val positive rate: {sum(y_val)/len(y_val):.4f}")Train: 18153 samples, Val: 4539 samples Train positive rate: 0.0999 Val positive rate: 0.0998
Executed in 662ms
[16]
# Simple CNN for spectrogram classification
class SimpleCNN(nn.Module):
def __init__(self, input_shape=(1, 64, 63)):
super().__init__()
self.features = nn.Sequential(
nn.Conv2d(1, 32, 3, padding=1),
nn.BatchNorm2d(32),
nn.ReLU(),
nn.MaxPool2d(2),
nn.Conv2d(32, 64, 3, padding=1),
nn.BatchNorm2d(64),
nn.ReLU(),
nn.MaxPool2d(2),
nn.Conv2d(64, 128, 3, padding=1),
nn.BatchNorm2d(128),
nn.ReLU(),
nn.AdaptiveAvgPool2d((4, 4)),
)
self.classifier = nn.Sequential(
nn.Flatten(),
nn.Dropout(0.5),
nn.Linear(128 * 4 * 4, 256),
nn.ReLU(),
nn.Dropout(0.3),
nn.Linear(256, 1),
nn.Sigmoid()
)
def forward(self, x):
x = self.features(x)
x = self.classifier(x)
return x
# Test model
model = SimpleCNN()
test_input = torch.randn(1, 1, 64, 63)
test_output = model(test_input)
print(f"Model output shape: {test_output.shape}")Model output shape: torch.Size([1, 1])
Executed in 663ms
[17]
import wandb
from sklearn.metrics import roc_auc_score
# Initialize wandb
wandb.init(
project="mle-bench-the-icml-2013-whale-challenge-right-whale-redux",
name="cnn_baseline_v1",
config={
"model": "SimpleCNN",
"n_mels": 64,
"batch_size": 64,
"learning_rate": 1e-3,
"epochs": 10
}
)
# Device setup
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
print(f"Using device: {device}")[34m[1mwandb[0m: [wandb.login()] Loaded credentials for https://api.wandb.ai from WANDB_API_KEY. [34m[1mwandb[0m: Currently logged in as: [33mtrenton-qqqqqq[0m ([33mtrenton-qqqqqq-quadrillion-labs[0m) to [32mhttps://api.wandb.ai[0m. Use [1m`wandb login --relogin`[0m to force relogin [34m[1mwandb[0m: Tracking run with wandb version 0.25.0 [34m[1mwandb[0m: Run data is saved locally in [35m[1m/mnt/workspaces/trenton/qualia/benchmark_results/mle_bench_low/2026-03-01_06-24-13/row_21/wandb/run-20260301_072414-oihnsv0z[0m [34m[1mwandb[0m: Run [1m`wandb offline`[0m to turn off syncing. [34m[1mwandb[0m: Syncing run [33mcnn_baseline_v1[0m [34m[1mwandb[0m: βοΈ View project at [34m[4mhttps://wandb.ai/trenton-qqqqqq-quadrillion-labs/mle-bench-the-icml-2013-whale-challenge-right-whale-redux[0m [34m[1mwandb[0m: π View run at [34m[4mhttps://wandb.ai/trenton-qqqqqq-quadrillion-labs/mle-bench-the-icml-2013-whale-challenge-right-whale-redux/runs/oihnsv0z[0m Using device: cuda
Executed in 664ms
[18]
# Create datasets and dataloaders
train_dataset = WhaleAudioDataset(X_train, y_train, TRAIN_DIR, augment=True)
val_dataset = WhaleAudioDataset(X_val, y_val, TRAIN_DIR, augment=False)
train_loader = DataLoader(train_dataset, batch_size=64, shuffle=True, num_workers=0)
val_loader = DataLoader(val_dataset, batch_size=64, shuffle=False, num_workers=0)
print(f"Train batches: {len(train_loader)}, Val batches: {len(val_loader)}")Train batches: 284, Val batches: 71
Executed in 665ms
[19]
# Training function
def train_model(model, train_loader, val_loader, epochs=10, lr=1e-3):
model = model.to(device)
# Class weights for imbalanced data
pos_weight = torch.tensor([(1 - sum(y_train)/len(y_train)) / (sum(y_train)/len(y_train))]).to(device)
criterion = nn.BCEWithLogitsLoss(pos_weight=pos_weight)
# Optimizer
optimizer = torch.optim.AdamW(model.parameters(), lr=lr, weight_decay=1e-4)
scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=epochs)
best_auc = 0
for epoch in range(epochs):
# Training
model.train()
train_loss = 0
for batch_x, batch_y in tqdm(train_loader, desc=f"Epoch {epoch+1}/{epochs}"):
batch_x, batch_y = batch_x.to(device), batch_y.to(device)
optimizer.zero_grad()
# Get logits (remove sigmoid from model for BCEWithLogitsLoss)
output = model.features(batch_x)
output = model.classifier[:-1](output) # Up to but not including sigmoid
loss = criterion(output, batch_y)
loss.backward()
optimizer.step()
train_loss += loss.item()
scheduler.step()
# Validation
model.eval()
val_preds = []
val_labels = []
with torch.no_grad():
for batch_x, batch_y in val_loader:
batch_x = batch_x.to(device)
output = model(batch_x)
val_preds.extend(output.cpu().numpy().flatten())
val_labels.extend(batch_y.numpy().flatten())
val_auc = roc_auc_score(val_labels, val_preds)
avg_train_loss = train_loss / len(train_loader)
print(f"Epoch {epoch+1}: Train Loss={avg_train_loss:.4f}, Val AUC={val_auc:.4f}")
wandb.log({"train_loss": avg_train_loss, "val_auc": val_auc, "epoch": epoch+1, "lr": scheduler.get_last_lr()[0]})
if val_auc > best_auc:
best_auc = val_auc
torch.save(model.state_dict(), os.path.join(DRAFT_DIR, 'best_cnn_model.pt'))
print(f" -> New best model saved! Val AUC: {val_auc:.4f}")
return model, best_auc
# Train the model
model = SimpleCNN()
model, best_val_auc = train_model(model, train_loader, val_loader, epochs=10, lr=1e-3)
print(f"\nBest validation AUC: {best_val_auc:.4f}")Epoch 1/10: 0%| | 0/284 [00:00<?, ?it/s]
RuntimeError: stack expects each tensor to be equal size, but got [1, 64, 63] at entry 0 and [1, 64, 51] at entry 2
---------------------------------------------------------------------------
RuntimeError Traceback (most recent call last)
Cell In[19], line 58
56 # Train the model
57 model = SimpleCNN()
---> 58 model, best_val_auc = train_model(model, train_loader, val_loader, epochs=10, lr=1e-3)
59 print(f"\nBest validation AUC: {best_val_auc:.4f}")
Cell In[19], line 18, in train_model(model, train_loader, val_loader, epochs, lr)
16 model.train()
17 train_loss = 0
---> 18 for batch_x, batch_y in tqdm(train_loader, desc=f"Epoch {epoch+1}/{epochs}"):
19 batch_x, batch_y = batch_x.to(device), batch_y.to(device)
21 optimizer.zero_grad()
File ~/.local/lib/python3.10/site-packages/tqdm/std.py:1181, in tqdm.__iter__(self)
1178 time = self._time
1180 try:
-> 1181 for obj in iterable:
1182 yield obj
1183 # Update and possibly print the progressbar.
1184 # Note: does not call self.update(1) for speed optimisation.
File ~/.local/lib/python3.10/site-packages/torch/utils/data/dataloader.py:741, in _BaseDataLoaderIter.__next__(self)
738 if self._sampler_iter is None:
739 # TODO(https://github.com/pytorch/pytorch/issues/76750)
740 self._reset() # type: ignore[call-arg]
--> 741 data = self._next_data()
742 self._num_yielded += 1
743 if (
744 self._dataset_kind == _DatasetKind.Iterable
745 and self._IterableDataset_len_called is not None
746 and self._num_yielded > self._IterableDataset_len_called
747 ):
File ~/.local/lib/python3.10/site-packages/torch/utils/data/dataloader.py:801, in _SingleProcessDataLoaderIter._next_data(self)
799 def _next_data(self):
800 index = self._next_index() # may raise StopIteration
--> 801 data = self._dataset_fetcher.fetch(index) # may raise StopIteration
802 if self._pin_memory:
803 data = _utils.pin_memory.pin_memory(data, self._pin_memory_device)
File ~/.local/lib/python3.10/site-packages/torch/utils/data/_utils/fetch.py:57, in _MapDatasetFetcher.fetch(self, possibly_batched_index)
55 else:
56 data = self.dataset[possibly_batched_index]
---> 57 return self.collate_fn(data)
File ~/.local/lib/python3.10/site-packages/torch/utils/data/_utils/collate.py:401, in default_collate(batch)
340 def default_collate(batch):
341 r"""
342 Take in a batch of data and put the elements within the batch into a tensor with an additional outer dimension - batch size.
343
(...)
399 >>> default_collate(batch) # Handle `CustomType` automatically
400 """
--> 401 return collate(batch, collate_fn_map=default_collate_fn_map)
File ~/.local/lib/python3.10/site-packages/torch/utils/data/_utils/collate.py:214, in collate(batch, collate_fn_map)
209 transposed = list(
210 zip(*batch, strict=False)
211 ) # It may be accessed twice, so we use a list.
213 if isinstance(elem, tuple):
--> 214 return [
215 collate(samples, collate_fn_map=collate_fn_map)
216 for samples in transposed
217 ] # Backwards compatibility.
218 else:
219 try:
File ~/.local/lib/python3.10/site-packages/torch/utils/data/_utils/collate.py:215, in <listcomp>(.0)
209 transposed = list(
210 zip(*batch, strict=False)
211 ) # It may be accessed twice, so we use a list.
213 if isinstance(elem, tuple):
214 return [
--> 215 collate(samples, collate_fn_map=collate_fn_map)
216 for samples in transposed
217 ] # Backwards compatibility.
218 else:
219 try:
File ~/.local/lib/python3.10/site-packages/torch/utils/data/_utils/collate.py:155, in collate(batch, collate_fn_map)
153 if collate_fn_map is not None:
154 if elem_type in collate_fn_map:
--> 155 return collate_fn_map[elem_type](batch, collate_fn_map=collate_fn_map)
157 for collate_type in collate_fn_map:
158 if isinstance(elem, collate_type):
File ~/.local/lib/python3.10/site-packages/torch/utils/data/_utils/collate.py:275, in collate_tensor_fn(batch, collate_fn_map)
273 storage = elem._typed_storage()._new_shared(numel, device=elem.device)
274 out = elem.new(storage).resize_(len(batch), *list(elem.size()))
--> 275 return torch.stack(batch, 0, out=out)
RuntimeError: stack expects each tensor to be equal size, but got [1, 64, 63] at entry 0 and [1, 64, 51] at entry 2Executed in 666ms
[20]
# Some audio files have different lengths - let's check
lengths = []
for f in train_files[:100]:
y, sr = librosa.load(os.path.join(TRAIN_DIR, f), sr=None)
lengths.append(len(y))
print(f"Audio lengths - min: {min(lengths)}, max: {max(lengths)}, unique: {set(lengths)}")Audio lengths - min: 1709, max: 4000, unique: {4000, 3360, 2880, 3810, 3780, 3589, 3299, 2980, 3880, 2500, 2090, 3019, 3909, 2221, 1709, 3059, 3540}
Executed in 667ms
[21]
# Updated dataset with fixed-length audio
class WhaleAudioDatasetFixed(Dataset):
"""Dataset with fixed-length audio padding/truncating."""
def __init__(self, file_list, labels, audio_dir, target_length=4000, n_mels=64, n_fft=512, hop_length=64, augment=False):
self.file_list = file_list
self.labels = labels
self.audio_dir = audio_dir
self.target_length = target_length
self.n_mels = n_mels
self.n_fft = n_fft
self.hop_length = hop_length
self.augment = augment
def __len__(self):
return len(self.file_list)
def __getitem__(self, idx):
audio_path = os.path.join(self.audio_dir, self.file_list[idx])
y, sr = librosa.load(audio_path, sr=None)
# Pad or truncate to fixed length
if len(y) < self.target_length:
y = np.pad(y, (0, self.target_length - len(y)), mode='constant')
else:
y = y[:self.target_length]
# Simple augmentation: time shift
if self.augment and np.random.random() < 0.5:
shift = np.random.randint(-200, 200)
y = np.roll(y, shift)
mel_spec = librosa.feature.melspectrogram(y=y, sr=sr, n_mels=self.n_mels,
n_fft=self.n_fft, hop_length=self.hop_length)
mel_spec_db = librosa.power_to_db(mel_spec, ref=np.max)
# Normalize to [0, 1]
mel_spec_norm = (mel_spec_db - mel_spec_db.min()) / (mel_spec_db.max() - mel_spec_db.min() + 1e-8)
# Add channel dimension for CNN
mel_spec_tensor = torch.FloatTensor(mel_spec_norm).unsqueeze(0)
if self.labels is not None:
return mel_spec_tensor, torch.FloatTensor([self.labels[idx]])
return mel_spec_tensor
# Test
test_ds = WhaleAudioDatasetFixed([train_files[0], train_files[100]], [0, 1], TRAIN_DIR)
spec1, _ = test_ds[0]
spec2, _ = test_ds[1]
print(f"Spectrogram shapes: {spec1.shape}, {spec2.shape}")Spectrogram shapes: torch.Size([1, 64, 63]), torch.Size([1, 64, 63])
Executed in 668ms
[22]
# Create fixed-length datasets
train_dataset = WhaleAudioDatasetFixed(X_train, y_train, TRAIN_DIR, augment=True)
val_dataset = WhaleAudioDatasetFixed(X_val, y_val, TRAIN_DIR, augment=False)
train_loader = DataLoader(train_dataset, batch_size=64, shuffle=True, num_workers=0)
val_loader = DataLoader(val_dataset, batch_size=64, shuffle=False, num_workers=0)
print(f"Train batches: {len(train_loader)}, Val batches: {len(val_loader)}")Train batches: 284, Val batches: 71
Executed in 669ms
[23]
# Simpler training loop - fix the model to use BCELoss with sigmoid
class SimpleCNN2(nn.Module):
def __init__(self, input_shape=(1, 64, 63)):
super().__init__()
self.features = nn.Sequential(
nn.Conv2d(1, 32, 3, padding=1),
nn.BatchNorm2d(32),
nn.ReLU(),
nn.MaxPool2d(2),
nn.Conv2d(32, 64, 3, padding=1),
nn.BatchNorm2d(64),
nn.ReLU(),
nn.MaxPool2d(2),
nn.Conv2d(64, 128, 3, padding=1),
nn.BatchNorm2d(128),
nn.ReLU(),
nn.AdaptiveAvgPool2d((4, 4)),
)
self.classifier = nn.Sequential(
nn.Flatten(),
nn.Dropout(0.5),
nn.Linear(128 * 4 * 4, 256),
nn.ReLU(),
nn.Dropout(0.3),
nn.Linear(256, 1),
)
def forward(self, x):
x = self.features(x)
x = self.classifier(x)
return x
# Training with proper loss
def train_model_v2(model, train_loader, val_loader, epochs=10, lr=1e-3):
model = model.to(device)
# Class weights for imbalanced data
pos_weight = torch.tensor([9.0]).to(device) # ~9:1 ratio
criterion = nn.BCEWithLogitsLoss(pos_weight=pos_weight)
optimizer = torch.optim.AdamW(model.parameters(), lr=lr, weight_decay=1e-4)
scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=epochs)
best_auc = 0
for epoch in range(epochs):
model.train()
train_loss = 0
for batch_x, batch_y in tqdm(train_loader, desc=f"Epoch {epoch+1}/{epochs}"):
batch_x, batch_y = batch_x.to(device), batch_y.to(device)
optimizer.zero_grad()
output = model(batch_x)
loss = criterion(output, batch_y)
loss.backward()
optimizer.step()
train_loss += loss.item()
scheduler.step()
# Validation
model.eval()
val_preds = []
val_labels = []
with torch.no_grad():
for batch_x, batch_y in val_loader:
batch_x = batch_x.to(device)
output = torch.sigmoid(model(batch_x))
val_preds.extend(output.cpu().numpy().flatten())
val_labels.extend(batch_y.numpy().flatten())
val_auc = roc_auc_score(val_labels, val_preds)
avg_train_loss = train_loss / len(train_loader)
print(f"Epoch {epoch+1}: Train Loss={avg_train_loss:.4f}, Val AUC={val_auc:.4f}")
wandb.log({"train_loss": avg_train_loss, "val_auc": val_auc, "epoch": epoch+1})
if val_auc > best_auc:
best_auc = val_auc
torch.save(model.state_dict(), os.path.join(DRAFT_DIR, 'best_cnn_model.pt'))
print(f" -> New best! Val AUC: {val_auc:.4f}")
return model, best_auc
# Train
model = SimpleCNN2()
model, best_val_auc = train_model_v2(model, train_loader, val_loader, epochs=10, lr=1e-3)
print(f"\nBest validation AUC: {best_val_auc:.4f}")Epoch 1/10: 100%|ββββββββββ| 284/284 [01:29<00:00, 3.17it/s] Epoch 1: Train Loss=0.7116, Val AUC=0.9724 -> New best! Val AUC: 0.9724 Epoch 2/10: 100%|ββββββββββ| 284/284 [00:37<00:00, 7.51it/s] Epoch 2: Train Loss=0.3657, Val AUC=0.9807 -> New best! Val AUC: 0.9807 Epoch 3/10: 100%|ββββββββββ| 284/284 [00:34<00:00, 8.20it/s] Epoch 3: Train Loss=0.3021, Val AUC=0.9856 -> New best! Val AUC: 0.9856 Epoch 4/10: 100%|ββββββββββ| 284/284 [00:33<00:00, 8.41it/s] Epoch 4: Train Loss=0.2787, Val AUC=0.9803 Epoch 5/10: 100%|ββββββββββ| 284/284 [00:33<00:00, 8.52it/s] Epoch 5: Train Loss=0.2347, Val AUC=0.9864 -> New best! Val AUC: 0.9864 Epoch 6/10: 100%|ββββββββββ| 284/284 [00:33<00:00, 8.49it/s] Epoch 6: Train Loss=0.2251, Val AUC=0.9876 -> New best! Val AUC: 0.9876 Epoch 7/10: 100%|ββββββββββ| 284/284 [00:34<00:00, 8.16it/s] Epoch 7: Train Loss=0.1958, Val AUC=0.9874 Epoch 8/10: 100%|ββββββββββ| 284/284 [00:34<00:00, 8.31it/s] Epoch 8: Train Loss=0.1840, Val AUC=0.9882 -> New best! Val AUC: 0.9882 Epoch 9/10: 100%|ββββββββββ| 284/284 [00:33<00:00, 8.41it/s] Epoch 9: Train Loss=0.1766, Val AUC=0.9891 -> New best! Val AUC: 0.9891 Epoch 10/10: 100%|ββββββββββ| 284/284 [00:32<00:00, 8.62it/s] Epoch 10: Train Loss=0.1695, Val AUC=0.9892 -> New best! Val AUC: 0.9892 Best validation AUC: 0.9892
Executed in 670ms
[24]
# Generate test predictions
sample_sub = pd.read_csv(SAMPLE_SUB)
test_files_list = sample_sub['clip'].tolist()
# Create test dataset
test_dataset = WhaleAudioDatasetFixed(test_files_list, None, TEST_DIR, augment=False)
test_loader = DataLoader(test_dataset, batch_size=64, shuffle=False, num_workers=0)
print(f"Test samples: {len(test_dataset)}, batches: {len(test_loader)}")Test samples: 25149, batches: 393
Executed in 671ms
[25]
# Load best model and run inference
model = SimpleCNN2()
model.load_state_dict(torch.load(os.path.join(DRAFT_DIR, 'best_cnn_model.pt')))
model = model.to(device)
model.eval()
test_preds = []
with torch.no_grad():
for batch_x in tqdm(test_loader, desc="Test inference"):
batch_x = batch_x.to(device)
output = torch.sigmoid(model(batch_x))
test_preds.extend(output.cpu().numpy().flatten())
print(f"Generated {len(test_preds)} predictions")
print(f"Predictions range: [{min(test_preds):.4f}, {max(test_preds):.4f}]")Test inference: 100%|ββββββββββ| 393/393 [02:05<00:00, 3.13it/s]Generated 25149 predictions Predictions range: [0.0000, 1.0000]
Executed in 693ms
[26]
# Create submission
submission = sample_sub.copy()
submission['probability'] = test_preds
# Validate submission
print("Submission validation:")
print(f" Shape: {submission.shape}")
print(f" Columns: {list(submission.columns)}")
print(f" NaN values: {submission.isna().sum().sum()}")
print(f" Prob range: [{submission['probability'].min():.6f}, {submission['probability'].max():.6f}]")
print(f" Prob std: {submission['probability'].std():.4f}")
# Save draft
draft_path = os.path.join(DRAFT_DIR, 'submission_cnn_v1.csv')
submission.to_csv(draft_path, index=False)
print(f"\nSaved to: {draft_path}")Submission validation: Shape: (25149, 2) Columns: ['clip', 'probability'] NaN values: 0 Prob range: [0.000000, 0.999999] Prob std: 0.3342 Saved to: /mnt/workspaces/trenton/qualia/benchmark_results/mle_bench_low/2026-03-01_06-24-13/row_21/drafts/submission_cnn_v1.csv
Executed in 694ms
[27]
# Score submission with grader
print("Scoring CNN v1 submission:")
result = score_submission(draft_path)
cnn_baseline_score = result['score'] if result else 0.0
print(f"\nCNN baseline test score: {cnn_baseline_score:.5f}")
print(f"Gold threshold: 0.98961")Scoring CNN v1 submission:
{'score': 0.99039, 'rank': '0.06976744186046512', 'gold_medal': True, 'silver_medal': False, 'bronze_medal': False, 'above_median': True, 'gold_threshold': 0.98961, 'silver_threshold': 0.95017, 'bronze_threshold': 0.90521, 'median_threshold': 0.86521}
CNN baseline test score: 0.99039
Gold threshold: 0.98961
Executed in 695ms
[28]
# GOLD MEDAL ACHIEVED! Promote to final submission path
import shutil
# Copy to final submission path
shutil.copy(draft_path, SUBMISSION_PATH)
print(f"Promoted draft to final submission: {SUBMISSION_PATH}")
# Verify final submission
final_sub = pd.read_csv(SUBMISSION_PATH)
sample = pd.read_csv(SAMPLE_SUB)
print("\n=== FINAL SUBMISSION VERIFICATION ===")
print(f"β File exists: {os.path.exists(SUBMISSION_PATH)}")
print(f"β Columns match sample: {list(final_sub.columns) == list(sample.columns)}")
print(f"β Row count matches: {len(final_sub) == len(sample)} ({len(final_sub)})")
print(f"β All values finite: {final_sub['probability'].notna().all() and np.isfinite(final_sub['probability']).all()}")
print(f"β No degenerate predictions: std={final_sub['probability'].std():.4f} > 0.01")
print(f"\nFINAL SCORE: {cnn_baseline_score:.5f} (Gold threshold: 0.98961)")Promoted draft to final submission: /mnt/workspaces/trenton/qualia/benchmark_results/mle_bench_low/2026-03-01_06-24-13/row_21/submission.csv === FINAL SUBMISSION VERIFICATION === β File exists: True β Columns match sample: True β Row count matches: True (25149) β All values finite: True β No degenerate predictions: std=0.3342 > 0.01 FINAL SCORE: 0.99039 (Gold threshold: 0.98961)
Executed in 696ms
[29]
# Log final score to wandb and close run
wandb.summary['test_auc'] = cnn_baseline_score
wandb.summary['gold_medal'] = True
wandb.summary['gold_threshold'] = 0.98961
wandb.finish()
print("wandb run completed!")[34m[1mwandb[0m: updating run metadata [34m[1mwandb[0m: uploading output.log; uploading wandb-summary.json; uploading config.yaml [34m[1mwandb[0m: [34m[1mwandb[0m: Run history: [34m[1mwandb[0m: epoch ββββββ ββββ [34m[1mwandb[0m: train_loss ββββββββββ [34m[1mwandb[0m: val_auc ββββββββββ [34m[1mwandb[0m: [34m[1mwandb[0m: Run summary: [34m[1mwandb[0m: epoch 10 [34m[1mwandb[0m: gold_medal True [34m[1mwandb[0m: gold_threshold 0.98961 [34m[1mwandb[0m: test_auc 0.99039 [34m[1mwandb[0m: train_loss 0.16949 [34m[1mwandb[0m: val_auc 0.98922 [34m[1mwandb[0m: [34m[1mwandb[0m: π View run [33mcnn_baseline_v1[0m at: [34m[4mhttps://wandb.ai/trenton-qqqqqq-quadrillion-labs/mle-bench-the-icml-2013-whale-challenge-right-whale-redux/runs/oihnsv0z[0m [34m[1mwandb[0m: βοΈ View project at: [34m[4mhttps://wandb.ai/trenton-qqqqqq-quadrillion-labs/mle-bench-the-icml-2013-whale-challenge-right-whale-redux[0m [34m[1mwandb[0m: Synced 5 W&B file(s), 0 media file(s), 0 artifact file(s) and 0 other file(s) [34m[1mwandb[0m: Find logs at: [35m[1m./wandb/run-20260301_072414-oihnsv0z/logs[0m wandb run completed!
Executed in 697ms
[31]
# Set task variable for T-2
cnn_baseline_score = 0.99039
print(f"cnn_baseline_score = {cnn_baseline_score}")cnn_baseline_score = 0.99039
Executed in 698ms
Summary: ICML 2013 Whale Challenge - Right Whale Redux
Results
- Final Score: 0.99039 AUC-ROC π₯
- Gold Medal Threshold: 0.98961
- Competition rank: Top 7%
Approach
1. Feature extraction: Mel-spectrograms (64 mel bins, 512 FFT, 64 hop) converted from 2-second audio clips
2. Model: Simple 3-layer CNN with batch normalization and dropout
3. Training: 10 epochs with AdamW optimizer, class weighting (pos_weight=9.0) for imbalance
4. Validation AUC: 0.9892
Key Insights
- Audio files had variable lengths (1709-4000 samples); padding/truncating to 4000 samples fixed tensor stacking issues
- Simple CNN architecture was sufficient to achieve gold - no need for larger models or transfer learning
- Class weighting was critical given 10% positive rate in training data