Files

420 lines
16 KiB
Python
Raw Permalink Normal View History

"""
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 GPCMA-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)