420 lines
16 KiB
Python
420 lines
16 KiB
Python
|
|
"""
|
||
|
|
Strategy Selector - Intelligent optimization strategy recommendation.
|
||
|
|
|
||
|
|
This module implements decision logic to recommend the best optimization strategy
|
||
|
|
based on landscape characteristics. Uses expert knowledge and empirical heuristics
|
||
|
|
to match problem types to appropriate algorithms.
|
||
|
|
|
||
|
|
Part of Protocol 10: Intelligent Multi-Strategy Optimization (IMSO)
|
||
|
|
"""
|
||
|
|
|
||
|
|
import optuna
|
||
|
|
from typing import Dict, Optional, Tuple
|
||
|
|
import json
|
||
|
|
from pathlib import Path
|
||
|
|
|
||
|
|
|
||
|
|
class IntelligentStrategySelector:
|
||
|
|
"""
|
||
|
|
Selects optimal optimization strategy based on problem characteristics.
|
||
|
|
|
||
|
|
Decision tree combines:
|
||
|
|
1. Landscape analysis (smoothness, multimodality, noise)
|
||
|
|
2. Problem dimensionality
|
||
|
|
3. Trial budget and evaluation cost
|
||
|
|
4. Historical performance data (if available)
|
||
|
|
"""
|
||
|
|
|
||
|
|
def __init__(self, verbose: bool = True):
|
||
|
|
"""
|
||
|
|
Args:
|
||
|
|
verbose: Print recommendation explanations
|
||
|
|
"""
|
||
|
|
self.verbose = verbose
|
||
|
|
self.recommendation_history = []
|
||
|
|
|
||
|
|
def recommend_strategy(
|
||
|
|
self,
|
||
|
|
landscape: Dict,
|
||
|
|
trials_completed: int = 0,
|
||
|
|
trials_budget: Optional[int] = None,
|
||
|
|
current_best_value: Optional[float] = None
|
||
|
|
) -> Tuple[str, Dict]:
|
||
|
|
"""
|
||
|
|
Recommend optimization strategy based on problem characteristics.
|
||
|
|
|
||
|
|
Args:
|
||
|
|
landscape: Output from LandscapeAnalyzer.analyze()
|
||
|
|
trials_completed: Number of trials completed so far
|
||
|
|
trials_budget: Total trial budget (if known)
|
||
|
|
current_best_value: Current best objective value
|
||
|
|
|
||
|
|
Returns:
|
||
|
|
(strategy_name, recommendation_details)
|
||
|
|
|
||
|
|
strategy_name: One of ['tpe', 'cmaes', 'gp_bo', 'random', 'hybrid_gp_cmaes']
|
||
|
|
recommendation_details: Dict with confidence, reasoning, and sampler config
|
||
|
|
"""
|
||
|
|
# Handle None landscape (multi-objective optimization)
|
||
|
|
if landscape is None:
|
||
|
|
# Multi-objective: Use NSGA-II/NSGA-III based on trial count
|
||
|
|
return self._recommend_multiobjective_strategy(trials_completed)
|
||
|
|
|
||
|
|
if not landscape.get('ready', False):
|
||
|
|
# Not enough data, use random exploration
|
||
|
|
return self._recommend_random_exploration(trials_completed)
|
||
|
|
|
||
|
|
# Extract key characteristics
|
||
|
|
landscape_type = landscape.get('landscape_type', 'unknown')
|
||
|
|
smoothness = landscape.get('smoothness', 0.5)
|
||
|
|
multimodal = landscape.get('multimodal', False)
|
||
|
|
noise_level = landscape.get('noise_level', 0.0)
|
||
|
|
dimensionality = landscape.get('dimensionality', 2)
|
||
|
|
correlation_strength = landscape['parameter_correlation'].get('overall_strength', 0.3)
|
||
|
|
|
||
|
|
# Use characterization trial count for strategy decisions (not total trials)
|
||
|
|
# This prevents premature algorithm selection when many trials were pruned
|
||
|
|
char_trials = landscape.get('total_trials', trials_completed)
|
||
|
|
|
||
|
|
# Decision tree for strategy selection
|
||
|
|
strategy, details = self._apply_decision_tree(
|
||
|
|
landscape_type=landscape_type,
|
||
|
|
smoothness=smoothness,
|
||
|
|
multimodal=multimodal,
|
||
|
|
noise_level=noise_level,
|
||
|
|
dimensionality=dimensionality,
|
||
|
|
correlation_strength=correlation_strength,
|
||
|
|
trials_completed=char_trials # Use characterization trials, not total
|
||
|
|
)
|
||
|
|
|
||
|
|
# Add landscape info to recommendation
|
||
|
|
details['landscape_analysis'] = {
|
||
|
|
'type': landscape_type,
|
||
|
|
'smoothness': smoothness,
|
||
|
|
'multimodal': multimodal,
|
||
|
|
'dimensionality': dimensionality
|
||
|
|
}
|
||
|
|
|
||
|
|
# Log recommendation
|
||
|
|
self._log_recommendation(strategy, details, trials_completed)
|
||
|
|
|
||
|
|
if self.verbose:
|
||
|
|
self._print_recommendation(strategy, details)
|
||
|
|
|
||
|
|
return strategy, details
|
||
|
|
|
||
|
|
def _apply_decision_tree(
|
||
|
|
self,
|
||
|
|
landscape_type: str,
|
||
|
|
smoothness: float,
|
||
|
|
multimodal: bool,
|
||
|
|
noise_level: float,
|
||
|
|
dimensionality: int,
|
||
|
|
correlation_strength: float,
|
||
|
|
trials_completed: int
|
||
|
|
) -> Tuple[str, Dict]:
|
||
|
|
"""
|
||
|
|
Apply expert decision tree for strategy selection.
|
||
|
|
|
||
|
|
Decision logic based on optimization algorithm strengths:
|
||
|
|
|
||
|
|
CMA-ES:
|
||
|
|
- Best for: Smooth unimodal landscapes, correlated parameters
|
||
|
|
- Strengths: Fast local convergence, handles parameter correlations
|
||
|
|
- Weaknesses: Poor for multimodal, needs reasonable initialization
|
||
|
|
|
||
|
|
GP-BO (Gaussian Process Bayesian Optimization):
|
||
|
|
- Best for: Smooth landscapes, expensive evaluations, low-dimensional
|
||
|
|
- Strengths: Sample efficient, good uncertainty quantification
|
||
|
|
- Weaknesses: Scales poorly >10D, expensive surrogate training
|
||
|
|
|
||
|
|
TPE (Tree-structured Parzen Estimator):
|
||
|
|
- Best for: General purpose, multimodal, moderate dimensional
|
||
|
|
- Strengths: Handles multimodality, scales to ~50D, robust
|
||
|
|
- Weaknesses: Slower convergence than CMA-ES on smooth problems
|
||
|
|
|
||
|
|
Hybrid GP→CMA-ES:
|
||
|
|
- Best for: Smooth landscapes needing global+local search
|
||
|
|
- Strengths: GP finds basin, CMA-ES refines locally
|
||
|
|
- Weaknesses: More complex, needs transition logic
|
||
|
|
"""
|
||
|
|
|
||
|
|
# CASE 1: High noise - use robust methods
|
||
|
|
if noise_level > 0.5:
|
||
|
|
return 'tpe', {
|
||
|
|
'confidence': 0.85,
|
||
|
|
'reasoning': 'High noise detected - TPE is more robust to noisy evaluations',
|
||
|
|
'sampler_config': {
|
||
|
|
'type': 'TPESampler',
|
||
|
|
'params': {
|
||
|
|
'multivariate': True,
|
||
|
|
'n_startup_trials': 15, # More exploration for noisy problems
|
||
|
|
'n_ei_candidates': 24
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
# CASE 2: Smooth unimodal with strong correlation - CMA-ES excels
|
||
|
|
if landscape_type == 'smooth_unimodal' and correlation_strength > 0.5:
|
||
|
|
return 'cmaes', {
|
||
|
|
'confidence': 0.92,
|
||
|
|
'reasoning': f'Smooth unimodal landscape with strong parameter correlation ({correlation_strength:.2f}) - CMA-ES will converge quickly',
|
||
|
|
'sampler_config': {
|
||
|
|
'type': 'CmaEsSampler',
|
||
|
|
'params': {
|
||
|
|
'restart_strategy': 'ipop', # Increasing population restart
|
||
|
|
'with_margin': True # Use margin for constraint handling
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
# CASE 3: Smooth but multimodal - Hybrid GP→CMA-ES or GP-BO
|
||
|
|
if landscape_type == 'smooth_multimodal':
|
||
|
|
if dimensionality <= 5 and trials_completed < 30:
|
||
|
|
# Early stage: GP-BO for exploration
|
||
|
|
return 'gp_bo', {
|
||
|
|
'confidence': 0.78,
|
||
|
|
'reasoning': f'Smooth multimodal landscape, {dimensionality}D - GP-BO for intelligent exploration, plan CMA-ES refinement later',
|
||
|
|
'sampler_config': {
|
||
|
|
'type': 'GPSampler', # Custom implementation needed
|
||
|
|
'params': {
|
||
|
|
'acquisition': 'EI', # Expected Improvement
|
||
|
|
'n_initial_points': 10
|
||
|
|
}
|
||
|
|
},
|
||
|
|
'transition_plan': {
|
||
|
|
'switch_to': 'cmaes',
|
||
|
|
'when': 'error < 1.0 OR trials > 40'
|
||
|
|
}
|
||
|
|
}
|
||
|
|
else:
|
||
|
|
# Later stage or higher dimensional: TPE
|
||
|
|
return 'tpe', {
|
||
|
|
'confidence': 0.75,
|
||
|
|
'reasoning': f'Smooth multimodal landscape - TPE handles multiple modes well',
|
||
|
|
'sampler_config': {
|
||
|
|
'type': 'TPESampler',
|
||
|
|
'params': {
|
||
|
|
'multivariate': True,
|
||
|
|
'n_startup_trials': 10,
|
||
|
|
'n_ei_candidates': 32 # More exploitation
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
# CASE 4: Smooth unimodal, low-dimensional, expensive - GP-BO then CMA-ES
|
||
|
|
if landscape_type == 'smooth_unimodal' and dimensionality <= 5:
|
||
|
|
if trials_completed < 25:
|
||
|
|
return 'gp_bo', {
|
||
|
|
'confidence': 0.82,
|
||
|
|
'reasoning': f'Smooth {dimensionality}D landscape - GP-BO for sample-efficient exploration',
|
||
|
|
'sampler_config': {
|
||
|
|
'type': 'GPSampler',
|
||
|
|
'params': {
|
||
|
|
'acquisition': 'EI',
|
||
|
|
'n_initial_points': 8
|
||
|
|
}
|
||
|
|
},
|
||
|
|
'transition_plan': {
|
||
|
|
'switch_to': 'cmaes',
|
||
|
|
'when': 'error < 2.0 OR trials > 25'
|
||
|
|
}
|
||
|
|
}
|
||
|
|
else:
|
||
|
|
# Switch to CMA-ES for final refinement
|
||
|
|
return 'cmaes', {
|
||
|
|
'confidence': 0.88,
|
||
|
|
'reasoning': 'Switching to CMA-ES for final local refinement',
|
||
|
|
'sampler_config': {
|
||
|
|
'type': 'CmaEsSampler',
|
||
|
|
'params': {
|
||
|
|
'restart_strategy': 'ipop',
|
||
|
|
'with_margin': True
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
# CASE 5: Rugged multimodal - TPE is most robust
|
||
|
|
if landscape_type == 'rugged_multimodal' or multimodal:
|
||
|
|
return 'tpe', {
|
||
|
|
'confidence': 0.80,
|
||
|
|
'reasoning': 'Rugged/multimodal landscape - TPE is robust to multiple local optima',
|
||
|
|
'sampler_config': {
|
||
|
|
'type': 'TPESampler',
|
||
|
|
'params': {
|
||
|
|
'multivariate': True,
|
||
|
|
'n_startup_trials': 12,
|
||
|
|
'n_ei_candidates': 24
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
# CASE 6: Rugged unimodal - TPE with more exploration
|
||
|
|
if landscape_type == 'rugged_unimodal':
|
||
|
|
return 'tpe', {
|
||
|
|
'confidence': 0.72,
|
||
|
|
'reasoning': 'Rugged landscape - TPE with extended exploration',
|
||
|
|
'sampler_config': {
|
||
|
|
'type': 'TPESampler',
|
||
|
|
'params': {
|
||
|
|
'multivariate': True,
|
||
|
|
'n_startup_trials': 15,
|
||
|
|
'n_ei_candidates': 20
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
# CASE 7: High dimensional (>5D) - TPE scales best
|
||
|
|
if dimensionality > 5:
|
||
|
|
return 'tpe', {
|
||
|
|
'confidence': 0.77,
|
||
|
|
'reasoning': f'High dimensionality ({dimensionality}D) - TPE scales well to moderate dimensions',
|
||
|
|
'sampler_config': {
|
||
|
|
'type': 'TPESampler',
|
||
|
|
'params': {
|
||
|
|
'multivariate': True,
|
||
|
|
'n_startup_trials': min(20, dimensionality * 3),
|
||
|
|
'n_ei_candidates': 24
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
# DEFAULT: TPE as safe general-purpose choice
|
||
|
|
return 'tpe', {
|
||
|
|
'confidence': 0.65,
|
||
|
|
'reasoning': 'Default robust strategy - TPE works well for most problems',
|
||
|
|
'sampler_config': {
|
||
|
|
'type': 'TPESampler',
|
||
|
|
'params': {
|
||
|
|
'multivariate': True,
|
||
|
|
'n_startup_trials': 10,
|
||
|
|
'n_ei_candidates': 24
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
def _recommend_random_exploration(self, trials_completed: int) -> Tuple[str, Dict]:
|
||
|
|
"""Recommend random exploration when insufficient data for analysis."""
|
||
|
|
return 'random', {
|
||
|
|
'confidence': 1.0,
|
||
|
|
'reasoning': f'Insufficient data ({trials_completed} trials) - using random exploration for landscape characterization',
|
||
|
|
'sampler_config': {
|
||
|
|
'type': 'RandomSampler',
|
||
|
|
'params': {}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
def _recommend_multiobjective_strategy(self, trials_completed: int) -> Tuple[str, Dict]:
|
||
|
|
"""
|
||
|
|
Recommend strategy for multi-objective optimization.
|
||
|
|
|
||
|
|
For multi-objective problems, landscape analysis is not applicable.
|
||
|
|
Use NSGA-II (default) or TPE with multivariate support.
|
||
|
|
"""
|
||
|
|
# Start with random for initial exploration
|
||
|
|
if trials_completed < 8:
|
||
|
|
return 'random', {
|
||
|
|
'confidence': 1.0,
|
||
|
|
'reasoning': f'Multi-objective: Random exploration for initial {trials_completed}/8 trials',
|
||
|
|
'sampler_config': {
|
||
|
|
'type': 'RandomSampler',
|
||
|
|
'params': {}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
# After initial exploration, use TPE with multivariate support
|
||
|
|
# (NSGA-II sampler is already used at study creation level)
|
||
|
|
return 'tpe', {
|
||
|
|
'confidence': 0.95,
|
||
|
|
'reasoning': f'Multi-objective: TPE with multivariate support for Pareto front exploration ({trials_completed} trials)',
|
||
|
|
'sampler_config': {
|
||
|
|
'type': 'TPESampler',
|
||
|
|
'params': {
|
||
|
|
'multivariate': True,
|
||
|
|
'n_startup_trials': 8,
|
||
|
|
'n_ei_candidates': 24,
|
||
|
|
'constant_liar': True # Better for parallel multi-objective
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
def _log_recommendation(self, strategy: str, details: Dict, trial_number: int):
|
||
|
|
"""Log recommendation for learning and transfer."""
|
||
|
|
self.recommendation_history.append({
|
||
|
|
'trial_number': trial_number,
|
||
|
|
'strategy': strategy,
|
||
|
|
'confidence': details.get('confidence', 0.0),
|
||
|
|
'reasoning': details.get('reasoning', ''),
|
||
|
|
'landscape': details.get('landscape_analysis', {})
|
||
|
|
})
|
||
|
|
|
||
|
|
def _print_recommendation(self, strategy: str, details: Dict):
|
||
|
|
"""Print formatted recommendation."""
|
||
|
|
print(f"\n{'='*70}")
|
||
|
|
print(f" STRATEGY RECOMMENDATION")
|
||
|
|
print(f"{'='*70}")
|
||
|
|
print(f" Recommended: {strategy.upper()}")
|
||
|
|
print(f" Confidence: {details['confidence']:.1%}")
|
||
|
|
print(f" Reasoning: {details['reasoning']}")
|
||
|
|
|
||
|
|
if 'transition_plan' in details:
|
||
|
|
plan = details['transition_plan']
|
||
|
|
print(f"\n TRANSITION PLAN:")
|
||
|
|
print(f" Switch to: {plan['switch_to'].upper()}")
|
||
|
|
print(f" When: {plan['when']}")
|
||
|
|
|
||
|
|
print(f"{'='*70}\n")
|
||
|
|
|
||
|
|
def save_recommendation_history(self, filepath: Path):
|
||
|
|
"""Save recommendation history to JSON for learning."""
|
||
|
|
try:
|
||
|
|
with open(filepath, 'w') as f:
|
||
|
|
json.dump(self.recommendation_history, f, indent=2)
|
||
|
|
except Exception as e:
|
||
|
|
if self.verbose:
|
||
|
|
print(f" Warning: Failed to save recommendation history: {e}")
|
||
|
|
|
||
|
|
def load_recommendation_history(self, filepath: Path):
|
||
|
|
"""Load previous recommendation history."""
|
||
|
|
try:
|
||
|
|
if filepath.exists():
|
||
|
|
with open(filepath, 'r') as f:
|
||
|
|
self.recommendation_history = json.load(f)
|
||
|
|
except Exception as e:
|
||
|
|
if self.verbose:
|
||
|
|
print(f" Warning: Failed to load recommendation history: {e}")
|
||
|
|
|
||
|
|
|
||
|
|
def create_sampler_from_config(config: Dict) -> optuna.samplers.BaseSampler:
|
||
|
|
"""
|
||
|
|
Create Optuna sampler from configuration dictionary.
|
||
|
|
|
||
|
|
Args:
|
||
|
|
config: Sampler configuration from strategy recommendation
|
||
|
|
|
||
|
|
Returns:
|
||
|
|
Configured Optuna sampler
|
||
|
|
"""
|
||
|
|
sampler_type = config.get('type', 'TPESampler')
|
||
|
|
params = config.get('params', {})
|
||
|
|
|
||
|
|
if sampler_type == 'TPESampler':
|
||
|
|
return optuna.samplers.TPESampler(**params)
|
||
|
|
|
||
|
|
elif sampler_type == 'CmaEsSampler':
|
||
|
|
return optuna.samplers.CmaEsSampler(**params)
|
||
|
|
|
||
|
|
elif sampler_type == 'RandomSampler':
|
||
|
|
return optuna.samplers.RandomSampler(**params)
|
||
|
|
|
||
|
|
elif sampler_type == 'GPSampler':
|
||
|
|
# GP-BO not directly available in Optuna
|
||
|
|
# Would need custom implementation or use skopt integration
|
||
|
|
print(" Warning: GP-BO sampler not yet implemented, falling back to TPE")
|
||
|
|
return optuna.samplers.TPESampler(multivariate=True, n_startup_trials=10)
|
||
|
|
|
||
|
|
else:
|
||
|
|
# Default fallback
|
||
|
|
print(f" Warning: Unknown sampler type {sampler_type}, using TPE")
|
||
|
|
return optuna.samplers.TPESampler(multivariate=True, n_startup_trials=10)
|