| name | run2_parallel_grid_search |
| description | Efficient parallel hyperparameter grid search using joblib with 847 combinations |
Parallel Grid Search Optimization
Grid Search Space
For Mars cloud clustering, evaluate all combinations of:
- min_samples: 3, 4, 5, 6, 7, 8, 9 (7 values)
- epsilon: 4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24 (11 values)
- shape_weight: 0.9, 1.0, 1.1, 1.2, 1.3, 1.4, 1.5, 1.6, 1.7, 1.8, 1.9 (11 values)
Total combinations: 7 × 11 × 11 = 847
Parallelization with joblib
Setup
from joblib import Parallel, delayed
import pandas as pd
import numpy as np
min_samples_vals = list(range(3, 10))
epsilon_vals = list(range(4, 26, 2))
shape_weight_vals = list(np.round(np.arange(0.9, 2.0, 0.1), 1))
hp_list = []
for ms in min_samples_vals:
for eps in epsilon_vals:
for sw in shape_weight_vals:
hp_list.append((ms, eps, sw))
print(f"Total combinations: {len(hp_list)}")
Evaluation Function
Each worker evaluates one hyperparameter combination:
def evaluate_hyperparams(min_samples, epsilon, shape_weight, citsci_df, expert_df, expert_images):
"""
Evaluate one hyperparameter combination across all expert images.
Returns: (F1_avg, delta_avg, min_samples, epsilon, shape_weight)
"""
f1_scores = []
delta_scores = []
for image_id in expert_images:
f1, delta = evaluate_image(image_id, citsci_df, expert_df,
min_samples, epsilon, shape_weight)
f1_scores.append(f1)
if not np.isnan(delta):
delta_scores.append(delta)
avg_f1 = np.mean(f1_scores) if f1_scores else 0.0
avg_delta = np.mean(delta_scores) if delta_scores else np.nan
return avg_f1, avg_delta, min_samples, epsilon, shape_weight
Parallel Execution
citsci_df = pd.read_csv('/root/data/citsci_train.csv')
expert_df = pd.read_csv('/root/data/expert_train.csv')
expert_images = sorted(expert_df['file_rad'].unique())
results = Parallel(n_jobs=-1, verbose=1)(
delayed(evaluate_hyperparams)(ms, eps, sw, citsci_df, expert_df, expert_images)
for ms, eps, sw in hp_list
)
results_df = pd.DataFrame(results,
columns=['F1', 'delta', 'min_samples', 'epsilon', 'shape_weight'])
Joblib Parameters
n_jobs=-1
Uses all available CPU cores. For typical 22-core machines:
- ~22 parallel workers
- 847 combinations ÷ 22 ≈ 39 tasks per worker
- Total time: ~1.7 minutes (vs. ~60+ minutes sequentially)
verbose=1
Prints progress:
[Parallel(n_jobs=-1)]: Done 6 tasks | elapsed: 3.0s
[Parallel(n_jobs=-1)]: Done 156 tasks | elapsed: 20.7s
[Parallel(n_jobs=-1)]: Done 847 out of 847 | elapsed: 1.7min finished
Memory Efficiency
Data Sharing
Using Unix fork (default for Loky backend):
- Data loaded once in parent process
- Child processes inherit via copy-on-write
- No duplication overhead
Per-Evaluation Memory
Each worker needs:
- DBSCAN per image: O(n²) for distance matrix (n = points per image)
- Typical image: 50-100 points → 3KB-10KB distance matrix
- All workers combined: reasonable memory usage
citsci_df = pd.read_csv('/root/data/citsci_train.csv')
expert_df = pd.read_csv('/root/data/expert_train.csv')
expert_images = sorted(expert_df['file_rad'].unique())
results = Parallel(n_jobs=-1)(
delayed(evaluate_hyperparams)(ms, eps, sw,
citsci_df, expert_df, expert_images)
for ms, eps, sw in hp_list
)
Progress Tracking Alternatives
Option 1: joblib verbose
results = Parallel(n_jobs=-1, verbose=10)(
delayed(fn)(args) for args in arguments
)
Option 2: tqdm progress bar
from tqdm import tqdm
results = Parallel(n_jobs=-1)(
delayed(fn)(args)
for args in tqdm(arguments, total=len(arguments))
)
Option 3: Custom progress tracking
from joblib import Parallel, delayed, parallel_backend
def progress_callback(n_tasks):
"""Called every n_tasks completed."""
print(f"Progress: {n_tasks} tasks completed")
results = Parallel(n_jobs=-1)(
delayed(fn)(args) for args in arguments
)
Results Aggregation
results_df = pd.DataFrame(results,
columns=['F1', 'delta', 'min_samples', 'epsilon', 'shape_weight'])
assert len(results_df) == 847, "Missing results"
print(f"F1 range: [{results_df['F1'].min():.5f}, {results_df['F1'].max():.5f}]")
print(f"Delta range: [{results_df['delta'].min():.5f}, {results_df['delta'].max():.5f}]")
print(f"Solutions with F1 > 0.5: {(results_df['F1'] > 0.5).sum()}")
Error Handling
Timeout Protection
results = Parallel(n_jobs=-1, timeout=300)(
delayed(evaluate_hyperparams)(ms, eps, sw, citsci_df, expert_df, expert_images)
for ms, eps, sw in hp_list
)
Worker Crash Recovery
Performance Optimization
Batch Size
results = Parallel(n_jobs=-1, batch_size='auto')(
delayed(fn)(args) for args in arguments
)
Backend Selection
from joblib import parallel_backend
with parallel_backend('loky', n_jobs=-1):
results = Parallel()(delayed(fn)(args) for args in arguments)
with parallel_backend('threading', n_jobs=-1):
results = Parallel()(delayed(fn)(args) for args in arguments)
Reproducibility
import numpy as np
np.random.seed(42)
Typical Performance
For 847 DBSCAN evaluations across 369 images:
| Configuration | Time |
|---|
| Sequential (1 core) | ~60-90 minutes |
| Parallel (22 cores) | ~1.5-2 minutes |
| Speedup | ~40-45× |
Debugging Parallel Code
result = evaluate_hyperparams(3, 4, 0.9, citsci_df, expert_df, expert_images)
print(result)
results = Parallel(n_jobs=-1, verbose=10)(
delayed(evaluate_hyperparams)(ms, eps, sw, citsci_df, expert_df, expert_images)
for ms, eps, sw in hp_list[:10]
)
results = Parallel(n_jobs=-1, verbose=1)(
delayed(evaluate_hyperparams)(ms, eps, sw, citsci_df, expert_df, expert_images)
for ms, eps, sw in hp_list
)
Key Takeaways
- Load data before parallelization: Avoids redundant I/O in each worker
- Return simple types: Tuples faster than dictionaries or objects
- Use n_jobs=-1: Automatic CPU count detection and full utilization
- Monitor progress: verbose=1 or tqdm for user feedback
- Test sequentially first: Easier debugging before parallel execution
- Check results completeness: Verify all 847 combinations evaluated