| """Model Soup: average weights of multiple seed models → single model. |
| Zero inference cost vs single model, near-ensemble performance. |
| """ |
| import sys, os |
| import copy |
| import numpy as np |
|
|
| PROJECT_DIR = '/home/user/goat' |
| os.chdir(PROJECT_DIR) |
| sys.path.insert(0, PROJECT_DIR) |
|
|
| import torch |
| from ultralytics import YOLO |
| from tqdm import tqdm |
|
|
|
|
| def soup_models(model_paths, output_path): |
| """Average weights of multiple YOLO models.""" |
| models = [YOLO(p) for p in model_paths] |
| base = models[0] |
|
|
| |
| state_dicts = [] |
| for m in models: |
| sd = m.model.state_dict() |
| state_dicts.append(sd) |
|
|
| |
| avg_sd = {} |
| for key in state_dicts[0].keys(): |
| if any(kw in key.lower() for kw in ['num_batches_tracked', 'running_mean', 'running_var']): |
| |
| avg_sd[key] = state_dicts[0][key].clone() |
| elif 'bn' in key.lower() or 'batch_norm' in key.lower(): |
| |
| avg_sd[key] = sum(sd[key] for sd in state_dicts) / len(state_dicts) |
| else: |
| avg_sd[key] = sum(sd[key] for sd in state_dicts) / len(state_dicts) |
|
|
| base.model.load_state_dict(avg_sd) |
| base.model.eval() |
| base.save(output_path) |
| print(f'Model Soup saved to {output_path}') |
| return output_path |
|
|
|
|
| def evaluate_model(model_path, name): |
| """Quick eval on val set.""" |
| model = YOLO(model_path) |
| results = model.val(data='Data/Detection_dataset/dataset.yaml', imgsz=1536, batch=2, verbose=False) |
| return float(results.box.map50), float(results.box.map) |
|
|
|
|
| def main(): |
| model_paths = [ |
| 'runs/detect/Detection_experiments/v6_1_s_refined/weights/best.pt', |
| 'runs/detect/Detection_experiments/v12_seed_42/weights/best.pt', |
| 'runs/detect/Detection_experiments/v12_seed_123/weights/best.pt', |
| ] |
|
|
| |
| valid = [p for p in model_paths if os.path.exists(p)] |
| print(f'Models available: {len(valid)}/{len(model_paths)}') |
| for p in valid: |
| print(f' {p}') |
|
|
| |
| print(f'\n{"="*50}') |
| print('Individual models:') |
| singles = {} |
| for p in valid: |
| name = p.split('/')[-3] |
| m50, m5095 = evaluate_model(p, name) |
| singles[name] = (m50, m5095) |
| print(f' {name}: mAP50={m50:.4f}, mAP50-95={m5095:.4f}') |
|
|
| |
| print(f'\n{"="*50}') |
| print('Model Soup experiments:') |
|
|
| |
| import itertools |
| for r in range(2, len(valid) + 1): |
| for combo in itertools.combinations(range(len(valid)), r): |
| paths = [valid[i] for i in combo] |
| names = [p.split('/')[-3] for p in paths] |
| label = ' + '.join(n.split('_')[0] + '_' + n.split('_')[-1][:4] for n in names) |
| output = f'runs/soup/soup_{len(paths)}models.pt' |
| os.makedirs('runs/soup', exist_ok=True) |
| soup_models(paths, output) |
| m50, m5095 = evaluate_model(output, label) |
| delta = m5095 - singles[names[0]][1] |
| print(f' {label}: mAP50={m50:.4f}, mAP50-95={m5095:.4f} (d={delta:+.4f} vs {names[0]})') |
|
|
| |
| print(f'\n{"="*50}') |
| print('WBF Ensemble (for comparison):') |
| print(' (Run eval_ensemble.py for full WBF results)') |
|
|
|
|
| if __name__ == '__main__': |
| main() |
|
|