Buckets:
| """ | |
| 🧠 MACHINE LEARNING UTILITIES (Scikit-learn Integration) | |
| Classical ML algorithms for population-level analysis of the Butterfly System. | |
| Provides clustering, anomaly detection, and dimensionality reduction for | |
| organism populations. | |
| Features: | |
| - HDBSCAN/KMeans clustering for behavioral phenotype identification | |
| - Isolation Forest anomaly detection for unusual organisms | |
| - PCA/t-SNE dimensionality reduction for visualization | |
| - Config-driven enabling/disabling of each subsystem | |
| - ConceptTracker for semantic naming of stable behavioral clusters (Quick Win #2) | |
| - Optional dependency - system works without scikit-learn installed | |
| - Ray distributed computing for parallel feature extraction (4-5x speedup) | |
| """ | |
| import numpy as np | |
| from typing import Dict, List, Optional, Tuple, Any | |
| from dataclasses import dataclass, field | |
| import time | |
| import logging | |
| # Concept tracking for semantic naming (Quick Win #2) | |
| from .concept_tracker import ConceptTracker | |
| logger = logging.getLogger(__name__) | |
| # Ray distributed computing - optional, graceful fallback | |
| RAY_DISTRIBUTED_AVAILABLE = False | |
| try: | |
| from .distributed import get_ray_manager, RAY_AVAILABLE as _RAY_AVAIL | |
| RAY_DISTRIBUTED_AVAILABLE = _RAY_AVAIL | |
| logger.info(f"[ML Utils] Ray distributed computing available: {RAY_DISTRIBUTED_AVAILABLE}") | |
| except ImportError: | |
| logger.info("[ML Utils] Ray distributed module not available") | |
| # Optional scikit-learn import - graceful degradation if not installed | |
| SKLEARN_AVAILABLE = False | |
| try: | |
| from sklearn.cluster import KMeans, DBSCAN | |
| from sklearn.ensemble import IsolationForest | |
| from sklearn.neighbors import LocalOutlierFactor, NearestNeighbors, KNeighborsClassifier | |
| from sklearn.decomposition import PCA | |
| from sklearn.manifold import TSNE | |
| from sklearn.preprocessing import StandardScaler | |
| from sklearn.feature_extraction.text import TfidfVectorizer, CountVectorizer | |
| from sklearn.feature_selection import SelectKBest, mutual_info_classif, f_classif | |
| from sklearn.metrics import ( | |
| silhouette_score, adjusted_rand_score, mutual_info_score, | |
| accuracy_score, precision_score, recall_score | |
| ) | |
| SKLEARN_AVAILABLE = True | |
| # HDBSCAN is a separate package but commonly used with sklearn | |
| try: | |
| from hdbscan import HDBSCAN | |
| HDBSCAN_AVAILABLE = True | |
| except ImportError: | |
| HDBSCAN_AVAILABLE = False | |
| except ImportError: | |
| HDBSCAN_AVAILABLE = False | |
| class ClusteringResult: | |
| """Results from population clustering""" | |
| labels: np.ndarray # Cluster assignment for each organism (-1 = noise/outlier) | |
| n_clusters: int # Number of clusters found | |
| cluster_sizes: Dict[int, int] # Count of organisms per cluster | |
| cluster_centroids: Optional[np.ndarray] = None # Centroid positions if available | |
| algorithm: str = "none" | |
| timestamp: float = field(default_factory=time.time) | |
| # NEW: Concept tracking fields (Quick Win #2) | |
| concept_tags: Dict[int, str] = field(default_factory=dict) # cluster_id → concept_id | |
| # NEW: Confederation/Alliance analysis fields | |
| alliance_composition: Dict[int, Dict[str, int]] = field(default_factory=dict) # cluster_id → {alliance_id: count} | |
| confederation_tiers: Dict[int, Dict[str, int]] = field(default_factory=dict) # cluster_id → {tier: count} | |
| avg_alliance_participation: Dict[int, float] = field(default_factory=dict) # cluster_id → avg score | |
| avg_combat_performance: Dict[int, float] = field(default_factory=dict) # cluster_id → avg win ratio | |
| avg_reputation: Dict[int, float] = field(default_factory=dict) # cluster_id → avg reputation | |
| def get_cluster_organisms(self, cluster_id: int, organism_ids: List[str]) -> List[str]: | |
| """Get organism IDs belonging to a specific cluster""" | |
| return [org_id for org_id, label in zip(organism_ids, self.labels) if label == cluster_id] | |
| def to_dict(self) -> Dict[str, Any]: | |
| """Convert to JSON-serializable dict""" | |
| return { | |
| 'n_clusters': self.n_clusters, | |
| 'cluster_sizes': self.cluster_sizes, | |
| 'algorithm': self.algorithm, | |
| 'timestamp': self.timestamp, | |
| 'noise_count': int(np.sum(self.labels == -1)) if self.labels is not None else 0, | |
| 'concept_tags': self.concept_tags # NEW: semantic concept names | |
| } | |
| class AnomalyResult: | |
| """Results from anomaly detection""" | |
| scores: np.ndarray # Anomaly score for each organism (lower = more anomalous for IF) | |
| labels: np.ndarray # -1 = anomaly, 1 = normal | |
| anomaly_indices: List[int] # Indices of detected anomalies | |
| anomaly_ratio: float # Proportion of anomalies detected | |
| algorithm: str = "none" | |
| timestamp: float = field(default_factory=time.time) | |
| def get_anomaly_organisms(self, organism_ids: List[str]) -> List[str]: | |
| """Get organism IDs flagged as anomalies""" | |
| return [organism_ids[i] for i in self.anomaly_indices] | |
| def to_dict(self) -> Dict[str, Any]: | |
| """Convert to JSON-serializable dict""" | |
| return { | |
| 'anomaly_count': len(self.anomaly_indices), | |
| 'anomaly_ratio': self.anomaly_ratio, | |
| 'algorithm': self.algorithm, | |
| 'timestamp': self.timestamp | |
| } | |
| class ReductionResult: | |
| """Results from dimensionality reduction""" | |
| coordinates: np.ndarray # Reduced coordinates (n_organisms x n_components) | |
| n_components: int | |
| explained_variance: Optional[List[float]] = None # For PCA | |
| algorithm: str = "none" | |
| timestamp: float = field(default_factory=time.time) | |
| def get_organism_coordinates(self, organism_ids: List[str]) -> Dict[str, List[float]]: | |
| """Get coordinates mapped to organism IDs""" | |
| return {org_id: coords.tolist() for org_id, coords in zip(organism_ids, self.coordinates)} | |
| def to_dict(self) -> Dict[str, Any]: | |
| """Convert to JSON-serializable dict""" | |
| return { | |
| 'n_components': self.n_components, | |
| 'explained_variance': self.explained_variance, | |
| 'algorithm': self.algorithm, | |
| 'timestamp': self.timestamp, | |
| 'sample_count': len(self.coordinates) if self.coordinates is not None else 0 | |
| } | |
| class PopulationClusterer: | |
| """ | |
| Clusters organism population by behavioral/trait vectors. | |
| Identifies emergent phenotype groups without predefined K (using HDBSCAN) | |
| or with configurable K (using KMeans). | |
| """ | |
| def __init__(self, config: Dict[str, Any] = None): | |
| self.config = config or {} | |
| self.algorithm = self.config.get('algorithm', 'hdbscan') | |
| self.min_cluster_size = self.config.get('min_cluster_size', 5) | |
| self.min_samples = self.config.get('min_samples', 3) | |
| self.n_clusters = self.config.get('n_clusters', 5) # For KMeans | |
| self.scaler = StandardScaler() if SKLEARN_AVAILABLE else None | |
| self._last_result: Optional[ClusteringResult] = None | |
| # Integration 1: Neural-ML Symbiosis - use neural embeddings for clustering | |
| self.use_neural_embeddings = self.config.get('use_neural_embeddings', False) | |
| def extract_features(self, organisms: Dict[str, Any], | |
| context_memory: Optional[Any] = None) -> Tuple[np.ndarray, List[str]]: | |
| """ | |
| Extract feature vectors from organisms. | |
| Integration 1: Neural-ML Symbiosis - can use neural embeddings instead of behavioral features. | |
| Features include (if use_neural_embeddings=False): | |
| - Phenotype traits (trait_0 through trait_9) | |
| - Fitness value | |
| - Resources (if available) | |
| - Connection count (if available) | |
| - Language features (if context_memory available): | |
| - Vocabulary size (normalized) | |
| - Communication activity (normalized) | |
| - Linguistic connections (normalized) | |
| If use_neural_embeddings=True and organism is NeuralOrganism: | |
| - 64-dim semantic embedding from fc2 hidden state | |
| """ | |
| organism_ids = list(organisms.keys()) | |
| features = [] | |
| for org_id in organism_ids: | |
| org = organisms[org_id] | |
| # Integration 1: Neural embedding feature extraction disabled for consistency | |
| # All organisms must have the same feature vector length for clustering. | |
| # Neural embeddings (64-dim) would conflict with behavioral features (20-dim). | |
| # TODO: Enable neural embeddings when all organisms consistently provide them. | |
| # Fallback to behavioral features (original implementation) | |
| feature_vec = [] | |
| # Extract phenotype traits | |
| if hasattr(org, 'phenotype') and hasattr(org.phenotype, 'traits'): | |
| for i in range(10): # trait_0 through trait_9 | |
| trait_name = f"trait_{i}" | |
| feature_vec.append(org.phenotype.traits.get(trait_name, 0.0)) | |
| else: | |
| feature_vec.extend([0.0] * 10) | |
| # Fitness | |
| fitness = getattr(org, 'fitness', 0.0) | |
| feature_vec.append(fitness) | |
| # Resources (if available) | |
| resources = getattr(org, 'resources', 0.5) | |
| feature_vec.append(resources) | |
| # Genotype age | |
| if hasattr(org, 'genotype'): | |
| feature_vec.append(getattr(org.genotype, 'age', 0) / 100.0) # Normalize | |
| else: | |
| feature_vec.append(0.0) | |
| # NEW: Language features (if context_memory available) | |
| if context_memory and hasattr(context_memory, 'node_word_associations'): | |
| # Convert org_id to int for lookup | |
| org_id_int = hash(org_id) if isinstance(org_id, str) else org_id | |
| # Vocabulary size (normalized using log scale for 65K vocab) | |
| vocab_size = len(context_memory.node_word_associations.get(org_id_int, set())) | |
| feature_vec.append(min(1.0, np.log1p(vocab_size) / np.log1p(65536))) | |
| # Communication activity (normalized, assuming max 50 communications) | |
| comm_activity = getattr(org, 'communication_count', 0) | |
| feature_vec.append(min(1.0, comm_activity / 50.0)) | |
| # Linguistic connections (normalized, assuming max 10 linguistic edges) | |
| linguistic_conns = getattr(org, 'linguistic_connection_count', 0) | |
| feature_vec.append(min(1.0, linguistic_conns / 10.0)) | |
| else: | |
| # No language data available - use zeros | |
| feature_vec.extend([0.0] * 3) | |
| # === INTEGRATION: Extended Alliance/Combat/Learning Features === | |
| # Alliance participation (membership + reputation) | |
| alliance_participation = 0.0 | |
| if hasattr(org, 'alliance_id') and org.alliance_id is not None: | |
| alliance_participation = 0.5 # Base for being in alliance | |
| alliance_participation += getattr(org, 'alliance_reputation', 0.0) * 0.5 | |
| feature_vec.append(min(1.0, alliance_participation)) | |
| # Combat performance (battle win ratio) | |
| combat_performance = 0.5 # Neutral default | |
| battle_wins = getattr(org, 'battle_wins', 0) | |
| battle_losses = getattr(org, 'battle_losses', 0) | |
| total_battles = battle_wins + battle_losses | |
| if total_battles > 0: | |
| combat_performance = battle_wins / total_battles | |
| feature_vec.append(combat_performance) | |
| # Reputation score (social standing in network) | |
| reputation_score = getattr(org, 'alliance_reputation', 0.5) | |
| feature_vec.append(min(1.0, max(0.0, reputation_score))) | |
| # Concept maturity (language/learning sophistication) | |
| concept_maturity = 0.0 | |
| if hasattr(org, 'atomic_language') and org.atomic_language: | |
| # Use vocabulary diversity as maturity proxy | |
| vocab = getattr(org.atomic_language, 'vocabulary', set()) | |
| concept_maturity = min(1.0, len(vocab) / 50.0) | |
| elif hasattr(org, 'token_sequence'): | |
| # Fallback to token diversity | |
| unique_tokens = len(set(org.token_sequence)) | |
| concept_maturity = min(1.0, unique_tokens / 30.0) | |
| feature_vec.append(concept_maturity) | |
| # === CONFEDERATION (Super-Alliance) Features === | |
| # Confederation membership level (0=none, 0.33=confederation, 0.66=empire, 1.0=hegemony) | |
| confederation_level = 0.0 | |
| if hasattr(org, 'confederation_tier'): | |
| confederation_level = getattr(org, 'confederation_tier', 0) / 3.0 | |
| elif hasattr(org, 'alliance_id') and org.alliance_id: | |
| confederation_level = 0.1 # In alliance but not confederation | |
| feature_vec.append(min(1.0, confederation_level)) | |
| # Super-alliance war participation | |
| confed_wars = getattr(org, 'confederation_wars_participated', 0) | |
| feature_vec.append(min(1.0, confed_wars / 5.0)) | |
| # Cross-alliance influence (how many alliances organism has connections with) | |
| cross_alliance_influence = getattr(org, 'cross_alliance_connections', 0) | |
| feature_vec.append(min(1.0, cross_alliance_influence / 10.0)) | |
| # === LANGUAGE-GAME BRIDGE FEATURES === | |
| # Integration: Connect vocabulary-game correlations to ML analysis | |
| # Note: Bridge is at arena-level, not per-organism. We use organism's | |
| # arena participation metrics or default to neutral values. | |
| # Try to get bridge metrics from organism (if attached) or from context | |
| bridge_metrics = None | |
| if hasattr(org, 'language_game_bridge') and org.language_game_bridge: | |
| bridge_metrics = org.language_game_bridge.get_correlation_metrics() | |
| elif context_memory and hasattr(context_memory, 'language_game_metrics'): | |
| # Use network-level aggregated metrics as fallback | |
| bridge_metrics = context_memory.language_game_metrics | |
| # Language-game alignment (how well vocabulary correlates with game success) | |
| lang_game_alignment = 0.5 # Neutral default | |
| if bridge_metrics: | |
| alignment = bridge_metrics.get('vocabulary_game_alignment', 0.0) | |
| lang_game_alignment = (alignment + 1.0) / 2.0 # Normalize -1,1 to 0,1 | |
| feature_vec.append(min(1.0, max(0.0, lang_game_alignment))) | |
| # Language decision influence (how much vocabulary affects actions) | |
| lang_decision_influence = 0.0 | |
| if bridge_metrics: | |
| lang_decision_influence = bridge_metrics.get('language_decision_influence', 0.0) | |
| feature_vec.append(min(1.0, lang_decision_influence)) | |
| # Concept diversity in games (vocabulary breadth during gameplay) | |
| game_concept_diversity = 0.0 | |
| if bridge_metrics: | |
| game_concept_diversity = bridge_metrics.get('concept_diversity', 0.0) | |
| feature_vec.append(min(1.0, game_concept_diversity)) | |
| features.append(feature_vec) | |
| return np.array(features), organism_ids | |
| def extract_features_parallel(self, organisms: Dict[str, Any], | |
| context_memory: Optional[Any] = None, | |
| config: Dict[str, Any] = None) -> Tuple[np.ndarray, List[str]]: | |
| """ | |
| Extract features using Ray parallel processing for large populations. | |
| Automatically falls back to sequential extraction if: | |
| - Ray is not available | |
| - Population is below parallelization threshold (default 50) | |
| - Any error occurs during parallel processing | |
| This provides 4-5x speedup for populations > 100 organisms. | |
| Args: | |
| organisms: Dict of org_id -> organism | |
| context_memory: Optional context for language features | |
| config: Optional config dict with Ray settings | |
| Returns: | |
| Tuple of (feature_array, organism_ids) | |
| """ | |
| # Check if Ray parallelization is worthwhile | |
| threshold = 50 | |
| if config: | |
| ray_config = config.get('ray', {}) | |
| threshold = ray_config.get('parallelization_threshold', 50) | |
| if not RAY_DISTRIBUTED_AVAILABLE or len(organisms) < threshold: | |
| # Fall back to sequential extraction | |
| return self.extract_features(organisms, context_memory) | |
| try: | |
| # Use Ray distributed feature extraction | |
| ray_manager = get_ray_manager(config) | |
| if not ray_manager.is_initialized(): | |
| return self.extract_features(organisms, context_memory) | |
| organism_ids = list(organisms.keys()) | |
| # Prepare organism states for serialization | |
| org_states = [] | |
| for org_id in organism_ids: | |
| org = organisms[org_id] | |
| state = self._extract_organism_state(org, org_id, context_memory) | |
| org_states.append(state) | |
| # Parallel feature extraction | |
| from .distributed.ray_tasks import extract_features_batch | |
| features = extract_features_batch(org_states, context=None, use_ray=True) | |
| logger.debug(f"[ML Utils] Ray extracted features for {len(organism_ids)} organisms") | |
| return np.array(features), organism_ids | |
| except Exception as e: | |
| logger.warning(f"[ML Utils] Ray feature extraction failed, falling back: {e}") | |
| return self.extract_features(organisms, context_memory) | |
| def _extract_organism_state(self, org: Any, org_id: str, | |
| context_memory: Optional[Any] = None) -> dict: | |
| """ | |
| Extract serializable state dict from organism for Ray processing. | |
| This converts organism objects to plain dicts that can be sent to Ray workers. | |
| """ | |
| state = {'id': org_id} | |
| # Basic attributes | |
| state['fitness'] = getattr(org, 'fitness', 0.5) | |
| state['resources'] = getattr(org, 'resources', 0.5) | |
| state['energy'] = getattr(org, 'energy', 0.5) | |
| # Phenotype traits | |
| if hasattr(org, 'phenotype') and hasattr(org.phenotype, 'traits'): | |
| state['traits'] = dict(org.phenotype.traits) | |
| else: | |
| state['traits'] = {} | |
| # Genotype | |
| if hasattr(org, 'genotype'): | |
| state['genotype_age'] = getattr(org.genotype, 'age', 0) | |
| # Alliance/combat features | |
| state['alliance_id'] = getattr(org, 'alliance_id', None) | |
| state['alliance_reputation'] = getattr(org, 'alliance_reputation', 0.5) | |
| state['battle_wins'] = getattr(org, 'battle_wins', 0) | |
| state['battle_losses'] = getattr(org, 'battle_losses', 0) | |
| state['confederation_tier'] = getattr(org, 'confederation_tier', 0) | |
| state['confederation_wars_participated'] = getattr(org, 'confederation_wars_participated', 0) | |
| state['cross_alliance_connections'] = getattr(org, 'cross_alliance_connections', 0) | |
| # Language features | |
| if hasattr(org, 'atomic_language') and org.atomic_language: | |
| vocab = getattr(org.atomic_language, 'vocabulary', set()) | |
| state['vocabulary_size'] = len(vocab) | |
| elif hasattr(org, 'token_sequence'): | |
| state['vocabulary_size'] = len(set(org.token_sequence)) | |
| else: | |
| state['vocabulary_size'] = 0 | |
| state['communication_count'] = getattr(org, 'communication_count', 0) | |
| state['linguistic_connection_count'] = getattr(org, 'linguistic_connection_count', 0) | |
| return state | |
| def fit_predict(self, organisms: Dict[str, Any], | |
| context_memory: Optional[Any] = None, | |
| config: Dict[str, Any] = None, | |
| use_ray: bool = True) -> ClusteringResult: | |
| """ | |
| Cluster organisms and return results. | |
| Uses Ray parallel feature extraction for large populations (4-5x speedup). | |
| Args: | |
| organisms: Dict of org_id -> organism | |
| context_memory: Optional context for language features | |
| config: Optional config dict with Ray settings | |
| use_ray: Whether to use Ray parallel processing (default True) | |
| Returns empty result if sklearn not available or insufficient data. | |
| """ | |
| if not SKLEARN_AVAILABLE: | |
| return ClusteringResult( | |
| labels=np.array([]), | |
| n_clusters=0, | |
| cluster_sizes={}, | |
| algorithm="unavailable" | |
| ) | |
| if len(organisms) < self.min_cluster_size: | |
| return ClusteringResult( | |
| labels=np.zeros(len(organisms), dtype=int), | |
| n_clusters=1 if organisms else 0, | |
| cluster_sizes={0: len(organisms)} if organisms else {}, | |
| algorithm="insufficient_data" | |
| ) | |
| # Use Ray parallel extraction for large populations | |
| if use_ray and RAY_DISTRIBUTED_AVAILABLE: | |
| features, organism_ids = self.extract_features_parallel( | |
| organisms, context_memory, config | |
| ) | |
| else: | |
| features, organism_ids = self.extract_features(organisms, context_memory=context_memory) | |
| # Standardize features | |
| features_scaled = self.scaler.fit_transform(features) | |
| # Track which algorithm is actually used (separate from config setting) | |
| used_algorithm = self.algorithm | |
| # Select and run clustering algorithm | |
| if self.algorithm == 'hdbscan' and HDBSCAN_AVAILABLE: | |
| clusterer = HDBSCAN( | |
| min_cluster_size=self.min_cluster_size, | |
| min_samples=self.min_samples | |
| ) | |
| try: | |
| labels = clusterer.fit_predict(features_scaled) | |
| centroids = None | |
| used_algorithm = 'hdbscan' | |
| except TypeError as e: | |
| # Common failure mode when `hdbscan` and `scikit-learn` versions are incompatible | |
| # (e.g., `sklearn.utils.validation.check_array()` signature changed). | |
| msg = str(e) | |
| if 'force_all_finite' in msg and 'check_array' in msg: | |
| logger.warning( | |
| "[PopulationClusterer] HDBSCAN failed due to sklearn incompatibility (%s). " | |
| "Falling back to KMeans. Consider pinning scikit-learn to a compatible version.", | |
| msg, | |
| ) | |
| else: | |
| logger.warning( | |
| "[PopulationClusterer] HDBSCAN failed (%s). Falling back to KMeans.", | |
| msg, | |
| ) | |
| n_clusters = min(self.n_clusters, len(organisms)) | |
| clusterer = KMeans(n_clusters=n_clusters, random_state=42, n_init=10) | |
| labels = clusterer.fit_predict(features_scaled) | |
| centroids = clusterer.cluster_centers_ | |
| used_algorithm = 'kmeans_hdbscan_fallback' | |
| elif self.algorithm == 'kmeans': | |
| n_clusters = min(self.n_clusters, len(organisms)) | |
| clusterer = KMeans(n_clusters=n_clusters, random_state=42, n_init=10) | |
| labels = clusterer.fit_predict(features_scaled) | |
| centroids = clusterer.cluster_centers_ | |
| used_algorithm = 'kmeans' | |
| elif self.algorithm == 'dbscan': | |
| clusterer = DBSCAN(eps=0.5, min_samples=self.min_samples) | |
| labels = clusterer.fit_predict(features_scaled) | |
| centroids = None | |
| used_algorithm = 'dbscan' | |
| else: | |
| # Fallback to KMeans if HDBSCAN not available or unknown algorithm | |
| n_clusters = min(self.n_clusters, len(organisms)) | |
| clusterer = KMeans(n_clusters=n_clusters, random_state=42, n_init=10) | |
| labels = clusterer.fit_predict(features_scaled) | |
| centroids = clusterer.cluster_centers_ | |
| used_algorithm = 'kmeans_fallback' | |
| # Calculate cluster sizes | |
| unique_labels = set(labels) | |
| cluster_sizes = {int(label): int(np.sum(labels == label)) for label in unique_labels} | |
| n_clusters = len([l for l in unique_labels if l >= 0]) # Exclude noise (-1) | |
| self._last_result = ClusteringResult( | |
| labels=labels, | |
| n_clusters=n_clusters, | |
| cluster_sizes=cluster_sizes, | |
| cluster_centroids=centroids, | |
| algorithm=used_algorithm # Report actual algorithm used, not config setting | |
| ) | |
| return self._last_result | |
| def last_result(self) -> Optional[ClusteringResult]: | |
| return self._last_result | |
| class AnomalyDetector: | |
| """ | |
| Detects unusual organisms in the population using Isolation Forest | |
| or Local Outlier Factor. | |
| """ | |
| def __init__(self, config: Dict[str, Any] = None): | |
| self.config = config or {} | |
| self.algorithm = self.config.get('algorithm', 'isolation_forest') | |
| self.contamination = self.config.get('contamination', 0.1) | |
| self.n_estimators = self.config.get('n_estimators', 100) | |
| self.scaler = StandardScaler() if SKLEARN_AVAILABLE else None | |
| self._last_result: Optional[AnomalyResult] = None | |
| def extract_features(self, organisms: Dict[str, Any], | |
| context_memory: Optional[Any] = None) -> Tuple[np.ndarray, List[str]]: | |
| """Extract feature vectors from organisms (same as clustering, with language features)""" | |
| organism_ids = list(organisms.keys()) | |
| features = [] | |
| for org_id in organism_ids: | |
| org = organisms[org_id] | |
| feature_vec = [] | |
| # Phenotype traits | |
| if hasattr(org, 'phenotype') and hasattr(org.phenotype, 'traits'): | |
| for i in range(10): | |
| trait_name = f"trait_{i}" | |
| feature_vec.append(org.phenotype.traits.get(trait_name, 0.0)) | |
| else: | |
| feature_vec.extend([0.0] * 10) | |
| # Fitness | |
| feature_vec.append(getattr(org, 'fitness', 0.0)) | |
| # Resources | |
| feature_vec.append(getattr(org, 'resources', 0.5)) | |
| # Genotype age | |
| if hasattr(org, 'genotype'): | |
| feature_vec.append(getattr(org.genotype, 'age', 0) / 100.0) | |
| else: | |
| feature_vec.append(0.0) | |
| # Language features (if context_memory available) | |
| if context_memory and hasattr(context_memory, 'node_word_associations'): | |
| org_id_int = hash(org_id) if isinstance(org_id, str) else org_id | |
| vocab_size = len(context_memory.node_word_associations.get(org_id_int, set())) | |
| feature_vec.append(min(1.0, np.log1p(vocab_size) / np.log1p(65536))) # Log scale for 65K vocab | |
| comm_activity = getattr(org, 'communication_count', 0) | |
| feature_vec.append(min(1.0, comm_activity / 50.0)) | |
| linguistic_conns = getattr(org, 'linguistic_connection_count', 0) | |
| feature_vec.append(min(1.0, linguistic_conns / 10.0)) | |
| else: | |
| feature_vec.extend([0.0] * 3) | |
| # === ALLIANCE/CONFEDERATION FEATURES (match PopulationClusterer) === | |
| # Alliance participation | |
| alliance_participation = 0.0 | |
| if hasattr(org, 'alliance_id') and org.alliance_id is not None: | |
| alliance_participation = 0.5 | |
| alliance_participation += getattr(org, 'alliance_reputation', 0.0) * 0.5 | |
| feature_vec.append(min(1.0, alliance_participation)) | |
| # Combat performance | |
| combat_performance = 0.5 | |
| battle_wins = getattr(org, 'battle_wins', 0) | |
| battle_losses = getattr(org, 'battle_losses', 0) | |
| total_battles = battle_wins + battle_losses | |
| if total_battles > 0: | |
| combat_performance = battle_wins / total_battles | |
| feature_vec.append(combat_performance) | |
| # Reputation score | |
| reputation_score = getattr(org, 'alliance_reputation', 0.5) | |
| feature_vec.append(min(1.0, max(0.0, reputation_score))) | |
| # Concept maturity | |
| concept_maturity = 0.0 | |
| if hasattr(org, 'atomic_language') and org.atomic_language: | |
| vocab = getattr(org.atomic_language, 'vocabulary', set()) | |
| concept_maturity = min(1.0, len(vocab) / 50.0) | |
| elif hasattr(org, 'token_sequence'): | |
| unique_tokens = len(set(org.token_sequence)) | |
| concept_maturity = min(1.0, unique_tokens / 30.0) | |
| feature_vec.append(concept_maturity) | |
| # Confederation features | |
| confederation_level = 0.0 | |
| if hasattr(org, 'confederation_tier'): | |
| confederation_level = getattr(org, 'confederation_tier', 0) / 3.0 | |
| elif hasattr(org, 'alliance_id') and org.alliance_id: | |
| confederation_level = 0.1 | |
| feature_vec.append(min(1.0, confederation_level)) | |
| confed_wars = getattr(org, 'confederation_wars_participated', 0) | |
| feature_vec.append(min(1.0, confed_wars / 5.0)) | |
| cross_alliance_influence = getattr(org, 'cross_alliance_connections', 0) | |
| feature_vec.append(min(1.0, cross_alliance_influence / 10.0)) | |
| features.append(feature_vec) | |
| return np.array(features), organism_ids | |
| def fit_predict(self, organisms: Dict[str, Any], context_memory: Optional[Any] = None) -> AnomalyResult: | |
| """ | |
| Detect anomalies in organism population. | |
| Args: | |
| organisms: Dict mapping organism IDs to Organism objects | |
| context_memory: Optional ContextMemory instance for language features | |
| Returns: | |
| AnomalyResult with detected anomalies | |
| """ | |
| if not SKLEARN_AVAILABLE: | |
| return AnomalyResult( | |
| scores=np.array([]), | |
| labels=np.array([]), | |
| anomaly_indices=[], | |
| anomaly_ratio=0.0, | |
| algorithm="unavailable" | |
| ) | |
| if len(organisms) < 5: # Need minimum samples for anomaly detection | |
| return AnomalyResult( | |
| scores=np.zeros(len(organisms)), | |
| labels=np.ones(len(organisms), dtype=int), | |
| anomaly_indices=[], | |
| anomaly_ratio=0.0, | |
| algorithm="insufficient_data" | |
| ) | |
| features, organism_ids = self.extract_features(organisms, context_memory=context_memory) | |
| features_scaled = self.scaler.fit_transform(features) | |
| if self.algorithm == 'isolation_forest': | |
| detector = IsolationForest( | |
| contamination=self.contamination, | |
| n_estimators=self.n_estimators, | |
| random_state=42 | |
| ) | |
| labels = detector.fit_predict(features_scaled) | |
| scores = detector.decision_function(features_scaled) | |
| elif self.algorithm == 'lof': | |
| detector = LocalOutlierFactor( | |
| contamination=self.contamination, | |
| novelty=False | |
| ) | |
| labels = detector.fit_predict(features_scaled) | |
| scores = detector.negative_outlier_factor_ | |
| else: | |
| # Default to Isolation Forest | |
| detector = IsolationForest( | |
| contamination=self.contamination, | |
| n_estimators=self.n_estimators, | |
| random_state=42 | |
| ) | |
| labels = detector.fit_predict(features_scaled) | |
| scores = detector.decision_function(features_scaled) | |
| # Find anomaly indices (labels == -1) | |
| anomaly_indices = [i for i, label in enumerate(labels) if label == -1] | |
| anomaly_ratio = len(anomaly_indices) / len(organisms) if organisms else 0.0 | |
| self._last_result = AnomalyResult( | |
| scores=scores, | |
| labels=labels, | |
| anomaly_indices=anomaly_indices, | |
| anomaly_ratio=anomaly_ratio, | |
| algorithm=self.algorithm | |
| ) | |
| return self._last_result | |
| def last_result(self) -> Optional[AnomalyResult]: | |
| return self._last_result | |
| class TraitReducer: | |
| """ | |
| Reduces high-dimensional trait/behavior space to 2D/3D for visualization. | |
| Uses PCA or t-SNE depending on configuration. | |
| """ | |
| def __init__(self, config: Dict[str, Any] = None): | |
| self.config = config or {} | |
| self.algorithm = self.config.get('algorithm', 'pca') | |
| self.n_components = self.config.get('n_components', 3) | |
| self.tsne_perplexity = self.config.get('tsne_perplexity', 30) | |
| self.scaler = StandardScaler() if SKLEARN_AVAILABLE else None | |
| self._last_result: Optional[ReductionResult] = None | |
| def extract_features(self, organisms: Dict[str, Any], | |
| context_memory: Optional[Any] = None) -> Tuple[np.ndarray, List[str]]: | |
| """Extract feature vectors from organisms (with language features)""" | |
| organism_ids = list(organisms.keys()) | |
| features = [] | |
| for org_id in organism_ids: | |
| org = organisms[org_id] | |
| feature_vec = [] | |
| # Phenotype traits | |
| if hasattr(org, 'phenotype') and hasattr(org.phenotype, 'traits'): | |
| for i in range(10): | |
| trait_name = f"trait_{i}" | |
| feature_vec.append(org.phenotype.traits.get(trait_name, 0.0)) | |
| else: | |
| feature_vec.extend([0.0] * 10) | |
| # Fitness | |
| feature_vec.append(getattr(org, 'fitness', 0.0)) | |
| # Resources | |
| feature_vec.append(getattr(org, 'resources', 0.5)) | |
| # Genotype age | |
| if hasattr(org, 'genotype'): | |
| feature_vec.append(getattr(org.genotype, 'age', 0) / 100.0) | |
| else: | |
| feature_vec.append(0.0) | |
| # NEW: Language features (if context_memory available) | |
| if context_memory and hasattr(context_memory, 'node_word_associations'): | |
| org_id_int = hash(org_id) if isinstance(org_id, str) else org_id | |
| vocab_size = len(context_memory.node_word_associations.get(org_id_int, set())) | |
| feature_vec.append(min(1.0, np.log1p(vocab_size) / np.log1p(65536))) # Log scale for 65K vocab | |
| comm_activity = getattr(org, 'communication_count', 0) | |
| feature_vec.append(min(1.0, comm_activity / 50.0)) | |
| linguistic_conns = getattr(org, 'linguistic_connection_count', 0) | |
| feature_vec.append(min(1.0, linguistic_conns / 10.0)) | |
| else: | |
| feature_vec.extend([0.0] * 3) | |
| features.append(feature_vec) | |
| return np.array(features), organism_ids | |
| def fit_transform(self, organisms: Dict[str, Any], context_memory: Optional[Any] = None) -> ReductionResult: | |
| """ | |
| Reduce dimensionality of organism features. | |
| Args: | |
| organisms: Dict mapping organism IDs to Organism objects | |
| context_memory: Optional ContextMemory instance for language features | |
| Returns: | |
| ReductionResult with reduced coordinates | |
| """ | |
| if not SKLEARN_AVAILABLE: | |
| return ReductionResult( | |
| coordinates=np.array([]), | |
| n_components=0, | |
| algorithm="unavailable" | |
| ) | |
| if len(organisms) < 3: | |
| return ReductionResult( | |
| coordinates=np.zeros((len(organisms), self.n_components)), | |
| n_components=self.n_components, | |
| algorithm="insufficient_data" | |
| ) | |
| features, organism_ids = self.extract_features(organisms, context_memory=context_memory) | |
| features_scaled = self.scaler.fit_transform(features) | |
| # Limit n_components to number of features/samples | |
| n_components = min(self.n_components, features.shape[1], len(organisms)) | |
| if self.algorithm == 'pca': | |
| reducer = PCA(n_components=n_components) | |
| coordinates = reducer.fit_transform(features_scaled) | |
| explained_variance = reducer.explained_variance_ratio_.tolist() | |
| elif self.algorithm == 'tsne': | |
| # t-SNE requires perplexity < n_samples | |
| perplexity = min(self.tsne_perplexity, len(organisms) - 1, 30) | |
| reducer = TSNE( | |
| n_components=min(n_components, 3), # t-SNE max 3 components | |
| perplexity=max(5, perplexity), | |
| random_state=42 | |
| ) | |
| coordinates = reducer.fit_transform(features_scaled) | |
| explained_variance = None # t-SNE doesn't have explained variance | |
| else: | |
| # Default to PCA | |
| reducer = PCA(n_components=n_components) | |
| coordinates = reducer.fit_transform(features_scaled) | |
| explained_variance = reducer.explained_variance_ratio_.tolist() | |
| self._last_result = ReductionResult( | |
| coordinates=coordinates, | |
| n_components=n_components, | |
| explained_variance=explained_variance, | |
| algorithm=self.algorithm | |
| ) | |
| return self._last_result | |
| def last_result(self) -> Optional[ReductionResult]: | |
| return self._last_result | |
| class MLAnalyzer: | |
| """ | |
| Unified ML analysis interface for the Butterfly System. | |
| Coordinates clustering, anomaly detection, and dimensionality reduction, | |
| respecting configuration toggles. | |
| Includes ConceptTracker for semantic naming of stable behavioral clusters (Quick Win #2). | |
| """ | |
| def __init__(self, config: Dict[str, Any] = None): | |
| """ | |
| Initialize ML analyzer with configuration. | |
| Config structure expected: | |
| { | |
| "enabled": true/false, | |
| "clustering": {"enabled": true, "algorithm": "hdbscan", ...}, | |
| "anomaly_detection": {"enabled": true, "algorithm": "isolation_forest", ...}, | |
| "dimensionality_reduction": {"enabled": true, "algorithm": "pca", ...}, | |
| "concept_tracking": {"enabled": true, "persistence_threshold": 3, ...} | |
| } | |
| """ | |
| self.config = config or {} | |
| self.enabled = self.config.get('enabled', False) | |
| # Initialize subsystems | |
| clustering_config = self.config.get('clustering', {}) | |
| anomaly_config = self.config.get('anomaly_detection', {}) | |
| reduction_config = self.config.get('dimensionality_reduction', {}) | |
| concept_config = self.config.get('concept_tracking', {}) | |
| self.clusterer = PopulationClusterer(clustering_config) | |
| self.anomaly_detector = AnomalyDetector(anomaly_config) | |
| self.reducer = TraitReducer(reduction_config) | |
| # NEW: Concept tracking for semantic naming (Quick Win #2) | |
| self.concept_tracker = ConceptTracker( | |
| persistence_threshold=concept_config.get('persistence_threshold', 3), | |
| stale_threshold=concept_config.get('stale_threshold', 10.0), | |
| enabled=concept_config.get('enabled', True) | |
| ) | |
| # Feature toggles | |
| self.clustering_enabled = clustering_config.get('enabled', True) | |
| self.anomaly_enabled = anomaly_config.get('enabled', True) | |
| self.reduction_enabled = reduction_config.get('enabled', True) | |
| self.concept_tracking_enabled = concept_config.get('enabled', True) | |
| # NEW: Language analysis configuration | |
| language_config = self.config.get('language_analysis', {}) | |
| self.language_analysis_enabled = language_config.get('enabled', True) | |
| self.tfidf_enabled = language_config.get('tfidf', {}).get('enabled', True) | |
| self.nearest_neighbors_enabled = language_config.get('nearest_neighbors', {}).get('enabled', True) | |
| self.feature_selection_enabled = language_config.get('feature_selection', {}).get('enabled', False) | |
| self.metrics_enabled = language_config.get('metrics', {}).get('enabled', True) | |
| # Initialize language analysis components | |
| if SKLEARN_AVAILABLE and self.language_analysis_enabled: | |
| # TF-IDF vectorizer for vocabulary analysis | |
| if self.tfidf_enabled: | |
| self.tfidf_vectorizer = TfidfVectorizer( | |
| max_features=language_config.get('tfidf', {}).get('max_features', 1000), | |
| ngram_range=tuple(language_config.get('tfidf', {}).get('ngram_range', [1, 2])), | |
| min_df=language_config.get('tfidf', {}).get('min_df', 1), | |
| max_df=language_config.get('tfidf', {}).get('max_df', 0.95) | |
| ) | |
| self.count_vectorizer = CountVectorizer( | |
| max_features=language_config.get('tfidf', {}).get('max_features', 1000), | |
| ngram_range=tuple(language_config.get('tfidf', {}).get('ngram_range', [1, 2])) | |
| ) | |
| else: | |
| self.tfidf_vectorizer = None | |
| self.count_vectorizer = None | |
| # Nearest Neighbors for semantic similarity | |
| if self.nearest_neighbors_enabled: | |
| nn_config = language_config.get('nearest_neighbors', {}) | |
| self.nearest_neighbors = NearestNeighbors( | |
| n_neighbors=nn_config.get('n_neighbors', 5), | |
| metric=nn_config.get('metric', 'cosine'), | |
| algorithm=nn_config.get('algorithm', 'auto') | |
| ) | |
| else: | |
| self.nearest_neighbors = None | |
| # Feature selection | |
| if self.feature_selection_enabled: | |
| fs_config = language_config.get('feature_selection', {}) | |
| self.feature_selector = SelectKBest( | |
| score_func=mutual_info_classif if fs_config.get('method', 'mutual_info') == 'mutual_info' else f_classif, | |
| k=fs_config.get('k', 10) | |
| ) | |
| else: | |
| self.feature_selector = None | |
| else: | |
| self.tfidf_vectorizer = None | |
| self.count_vectorizer = None | |
| self.nearest_neighbors = None | |
| self.feature_selector = None | |
| # Last analysis results | |
| self._last_analysis: Dict[str, Any] = {} | |
| self._last_analysis_time: float = 0 | |
| self._analysis_interval: float = 5.0 # Minimum seconds between analyses | |
| # Optional event emitter for causation graph visualization | |
| self.event_emitter = None # Set by main.py or unified_entry.py | |
| # ═══════════════════════════════════════════════════════════════════════════ | |
| # MISSION 3: AutoTune Integration Buffer | |
| # Track ML analysis metrics for AtomicConfigSystem feedback loop | |
| # ═══════════════════════════════════════════════════════════════════════════ | |
| self.autotune_metrics_buffer = { | |
| 'cluster_count_history': [], | |
| 'anomaly_ratio_history': [], | |
| 'silhouette_score_history': [], | |
| 'avg_cluster_count': 0.0, | |
| 'avg_anomaly_ratio': 0.0, | |
| 'avg_silhouette_score': 0.0, | |
| 'cluster_stability': 0.0, # How often cluster count changes | |
| 'analysis_count': 0 | |
| } | |
| self.autotune_window = 20 # Window size for moving averages | |
| self.atomic_config_system = None # Set by main.py when available | |
| def update_config(self, config: Dict[str, Any]): | |
| """Update configuration dynamically (for hot reload)""" | |
| self.config = config | |
| self.enabled = config.get('enabled', False) | |
| # Update subsystem configs | |
| clustering_config = config.get('clustering', {}) | |
| anomaly_config = config.get('anomaly_detection', {}) | |
| reduction_config = config.get('dimensionality_reduction', {}) | |
| concept_config = config.get('concept_tracking', {}) | |
| self.clusterer = PopulationClusterer(clustering_config) | |
| self.anomaly_detector = AnomalyDetector(anomaly_config) | |
| self.reducer = TraitReducer(reduction_config) | |
| # Update concept tracker settings (preserve history, just update config) | |
| self.concept_tracker.enabled = concept_config.get('enabled', True) | |
| self.concept_tracker.persistence_threshold = concept_config.get('persistence_threshold', 3) | |
| self.concept_tracker.stale_threshold = concept_config.get('stale_threshold', 10.0) | |
| self.clustering_enabled = clustering_config.get('enabled', True) | |
| self.anomaly_enabled = anomaly_config.get('enabled', True) | |
| self.reduction_enabled = reduction_config.get('enabled', True) | |
| self.concept_tracking_enabled = concept_config.get('enabled', True) | |
| def analyze(self, organisms: Dict[str, Any], force: bool = False, context_memory: Optional[Any] = None) -> Dict[str, Any]: | |
| """ | |
| Run all enabled ML analyses on organism population. | |
| Args: | |
| organisms: Dict mapping organism IDs to Organism objects | |
| force: If True, run analysis regardless of interval | |
| context_memory: Optional ContextMemory instance for language features | |
| Returns: | |
| Dict with clustering, anomaly, and reduction results | |
| """ | |
| current_time = time.time() | |
| # Rate limiting (unless forced) | |
| if not force and (current_time - self._last_analysis_time) < self._analysis_interval: | |
| return self._last_analysis | |
| if not self.enabled or not SKLEARN_AVAILABLE: | |
| return { | |
| 'enabled': False, | |
| 'sklearn_available': SKLEARN_AVAILABLE, | |
| 'clustering': None, | |
| 'anomalies': None, | |
| 'reduction': None, | |
| 'concept_tracking': None | |
| } | |
| results = { | |
| 'enabled': True, | |
| 'sklearn_available': True, | |
| 'timestamp': current_time, | |
| 'organism_count': len(organisms), | |
| 'organism_ids': list(organisms.keys()) # CRITICAL: Map cluster indices to organism IDs | |
| } | |
| # Clustering | |
| cluster_result = None | |
| if self.clustering_enabled: | |
| cluster_result = self.clusterer.fit_predict(organisms, context_memory=context_memory) | |
| # NEW: Concept tracking - semantic naming of stable clusters (Quick Win #2) | |
| concept_tags = {} | |
| if self.concept_tracking_enabled and cluster_result.labels.size > 0: | |
| # Wire event emitter to concept tracker for causation graph events | |
| if self.event_emitter: | |
| self.concept_tracker.event_emitter = self.event_emitter | |
| # SEMANTIC CONVERGENCE: Wire context_memory for phenotype→vocabulary flow | |
| if context_memory: | |
| self.concept_tracker.context_memory = context_memory | |
| # Update concept tracking with clustering results | |
| concept_tags = self.concept_tracker.update( | |
| cluster_labels=cluster_result.labels, | |
| cluster_sizes=cluster_result.cluster_sizes, | |
| organisms=organisms, | |
| timestamp=current_time | |
| ) | |
| # Attach concept tags to cluster result | |
| cluster_result.concept_tags = concept_tags | |
| results['clustering'] = cluster_result.to_dict() | |
| results['cluster_labels'] = cluster_result.labels.tolist() if cluster_result.labels.size > 0 else [] | |
| results['concept_tags'] = concept_tags | |
| results['concept_summary'] = self.concept_tracker.get_concept_summary() if self.concept_tracking_enabled else None | |
| else: | |
| results['clustering'] = None | |
| results['concept_tags'] = {} | |
| results['concept_summary'] = None | |
| # Anomaly Detection | |
| if self.anomaly_enabled: | |
| anomaly_result = self.anomaly_detector.fit_predict(organisms, context_memory=context_memory) | |
| results['anomalies'] = anomaly_result.to_dict() | |
| results['anomaly_organisms'] = anomaly_result.get_anomaly_organisms(list(organisms.keys())) | |
| else: | |
| results['anomalies'] = None | |
| # Dimensionality Reduction | |
| if self.reduction_enabled: | |
| reduction_result = self.reducer.fit_transform(organisms, context_memory=context_memory) | |
| results['reduction'] = reduction_result.to_dict() | |
| # Include coordinates for visualization | |
| if reduction_result.coordinates.size > 0: | |
| results['coordinates'] = reduction_result.get_organism_coordinates(list(organisms.keys())) | |
| else: | |
| results['reduction'] = None | |
| # NEW: Semantic Analysis - Analyze language patterns, word co-occurrence, semantic clusters | |
| if context_memory and hasattr(context_memory, 'node_word_associations'): | |
| semantic_analysis = self._analyze_semantic_patterns(organisms, context_memory) | |
| results['semantic_analysis'] = semantic_analysis | |
| else: | |
| results['semantic_analysis'] = None | |
| self._last_analysis = results | |
| self._last_analysis_time = current_time | |
| # ═══════════════════════════════════════════════════════════════════════════ | |
| # MISSION 3: Update AutoTune metrics and emit to AtomicConfigSystem | |
| # ═══════════════════════════════════════════════════════════════════════════ | |
| self._update_autotune_metrics(results) | |
| return results | |
| # ═══════════════════════════════════════════════════════════════════════════ | |
| # MISSION 3: AutoTune Integration Methods | |
| # ═══════════════════════════════════════════════════════════════════════════ | |
| def _update_autotune_metrics(self, results: Dict[str, Any]): | |
| """ | |
| Update AutoTune metrics buffer and emit to AtomicConfigSystem. | |
| This enables closed-loop optimization where scikit-learn analysis outcomes | |
| inform config parameter adjustments. | |
| Args: | |
| results: Analysis results from analyze() method | |
| """ | |
| # Extract metrics from results | |
| clustering = results.get('clustering', {}) | |
| anomalies = results.get('anomalies', {}) | |
| # Get cluster count | |
| cluster_count = 0 | |
| if clustering: | |
| cluster_count = clustering.get('n_clusters', 0) | |
| self.autotune_metrics_buffer['cluster_count_history'].append(cluster_count) | |
| if len(self.autotune_metrics_buffer['cluster_count_history']) > self.autotune_window: | |
| self.autotune_metrics_buffer['cluster_count_history'] = \ | |
| self.autotune_metrics_buffer['cluster_count_history'][-self.autotune_window:] | |
| # Get anomaly ratio | |
| anomaly_ratio = 0.0 | |
| if anomalies: | |
| n_anomalies = anomalies.get('n_anomalies', 0) | |
| n_samples = anomalies.get('n_samples', 1) | |
| anomaly_ratio = n_anomalies / max(1, n_samples) | |
| self.autotune_metrics_buffer['anomaly_ratio_history'].append(anomaly_ratio) | |
| if len(self.autotune_metrics_buffer['anomaly_ratio_history']) > self.autotune_window: | |
| self.autotune_metrics_buffer['anomaly_ratio_history'] = \ | |
| self.autotune_metrics_buffer['anomaly_ratio_history'][-self.autotune_window:] | |
| # Get silhouette score if available | |
| silhouette_score = 0.0 | |
| if clustering and 'silhouette_score' in clustering: | |
| silhouette_score = clustering.get('silhouette_score', 0.0) | |
| self.autotune_metrics_buffer['silhouette_score_history'].append(silhouette_score) | |
| if len(self.autotune_metrics_buffer['silhouette_score_history']) > self.autotune_window: | |
| self.autotune_metrics_buffer['silhouette_score_history'] = \ | |
| self.autotune_metrics_buffer['silhouette_score_history'][-self.autotune_window:] | |
| # Calculate averages | |
| if self.autotune_metrics_buffer['cluster_count_history']: | |
| self.autotune_metrics_buffer['avg_cluster_count'] = np.mean(self.autotune_metrics_buffer['cluster_count_history']) | |
| if self.autotune_metrics_buffer['anomaly_ratio_history']: | |
| self.autotune_metrics_buffer['avg_anomaly_ratio'] = np.mean(self.autotune_metrics_buffer['anomaly_ratio_history']) | |
| if self.autotune_metrics_buffer['silhouette_score_history']: | |
| self.autotune_metrics_buffer['avg_silhouette_score'] = np.mean(self.autotune_metrics_buffer['silhouette_score_history']) | |
| # Calculate cluster stability (how often count changes) | |
| cluster_history = self.autotune_metrics_buffer['cluster_count_history'] | |
| if len(cluster_history) >= 2: | |
| changes = sum(1 for i in range(1, len(cluster_history)) if cluster_history[i] != cluster_history[i-1]) | |
| self.autotune_metrics_buffer['cluster_stability'] = 1.0 - (changes / (len(cluster_history) - 1)) | |
| self.autotune_metrics_buffer['analysis_count'] += 1 | |
| # Emit to AtomicConfigSystem if available | |
| if self.atomic_config_system is not None: | |
| try: | |
| ml_metrics = { | |
| 'cluster_count': cluster_count, | |
| 'anomaly_ratio': anomaly_ratio, | |
| 'silhouette_score': silhouette_score, | |
| 'cluster_stability': self.autotune_metrics_buffer['cluster_stability'], | |
| 'avg_cluster_count': self.autotune_metrics_buffer['avg_cluster_count'] | |
| } | |
| # Call tune() method to inform atomic configs | |
| actions = self.atomic_config_system.tune(ml_metrics, self.autotune_metrics_buffer['analysis_count']) | |
| if actions: | |
| logger.debug(f"[ML→AUTOTUNE] Applied {len(actions)} config adjustments based on ML analysis") | |
| except Exception as e: | |
| logger.debug(f"ML AutoTune integration error: {e}") | |
| # Emit event for CRA visualization | |
| if self.event_emitter: | |
| try: | |
| from causation_explorer import Event | |
| event = Event( | |
| timestamp=time.time(), | |
| component='ml_analysis', | |
| event_type='ml_autotune_metrics', | |
| data={ | |
| 'cluster_count': cluster_count, | |
| 'anomaly_ratio': anomaly_ratio, | |
| 'silhouette_score': silhouette_score, | |
| 'cluster_stability': self.autotune_metrics_buffer['cluster_stability'], | |
| 'analysis_count': self.autotune_metrics_buffer['analysis_count'] | |
| } | |
| ) | |
| self.event_emitter(event) | |
| except Exception as e: | |
| logger.debug(f"ML AutoTune event emission failed: {e}") | |
| def get_autotune_metrics(self) -> Dict[str, Any]: | |
| """ | |
| Get current AutoTune metrics buffer for CRA diagnostics. | |
| Returns: | |
| Dictionary of ML analysis metrics for AutoTune integration | |
| """ | |
| return { | |
| **self.autotune_metrics_buffer, | |
| 'buffer_size': len(self.autotune_metrics_buffer['cluster_count_history']), | |
| 'window_size': self.autotune_window, | |
| 'atomic_config_connected': self.atomic_config_system is not None | |
| } | |
| def _analyze_semantic_patterns(self, organisms: Dict[str, Any], context_memory: Any) -> Dict[str, Any]: | |
| """ | |
| Analyze semantic patterns: word co-occurrence, semantic clusters, concept formation. | |
| Enhanced with TF-IDF, Nearest Neighbors, and quality metrics. | |
| """ | |
| if not context_memory or not hasattr(context_memory, 'node_word_associations'): | |
| return None | |
| # Get Linguistic Knowledge Web if available | |
| knowledge_web = None | |
| if hasattr(context_memory, 'knowledge_web'): | |
| knowledge_web = context_memory.knowledge_web | |
| elif hasattr(context_memory, 'language_teacher') and hasattr(context_memory.language_teacher, 'knowledge_web'): | |
| knowledge_web = context_memory.language_teacher.knowledge_web | |
| if not knowledge_web: | |
| return None | |
| # Build word-organism matrix and vocabulary strings for TF-IDF | |
| organism_ids = list(organisms.keys()) | |
| word_organism_matrix = {} # word -> set of organism_ids | |
| organism_vocabularies = [] # List of space-separated word strings for each organism | |
| for org_id in organism_ids: | |
| org_id_int = hash(org_id) if isinstance(org_id, str) else org_id | |
| words = list(context_memory.node_word_associations.get(org_id_int, set())) | |
| organism_vocabularies.append(' '.join(words)) | |
| for word in words: | |
| if word not in word_organism_matrix: | |
| word_organism_matrix[word] = set() | |
| word_organism_matrix[word].add(org_id) | |
| # NEW: TF-IDF Analysis (if enabled and sklearn available) | |
| tfidf_results = None | |
| important_words = [] | |
| if SKLEARN_AVAILABLE and self.tfidf_enabled and self.tfidf_vectorizer and len(organism_vocabularies) > 0: | |
| try: | |
| # Fit TF-IDF on organism vocabularies | |
| tfidf_matrix = self.tfidf_vectorizer.fit_transform(organism_vocabularies) | |
| feature_names = self.tfidf_vectorizer.get_feature_names_out() | |
| # Get mean TF-IDF scores across all organisms (word importance) | |
| mean_tfidf = np.array(tfidf_matrix.mean(axis=0)).flatten() | |
| word_importance = list(zip(feature_names, mean_tfidf)) | |
| word_importance.sort(key=lambda x: x[1], reverse=True) | |
| important_words = [{'word': word, 'tfidf_score': float(score)} for word, score in word_importance[:20]] | |
| # Count vectorizer for raw frequencies | |
| count_matrix = self.count_vectorizer.fit_transform(organism_vocabularies) | |
| total_counts = np.array(count_matrix.sum(axis=0)).flatten() | |
| count_feature_names = self.count_vectorizer.get_feature_names_out() | |
| word_counts = dict(zip(count_feature_names, total_counts)) | |
| tfidf_results = { | |
| 'vocabulary_size': len(feature_names), | |
| 'top_important_words': important_words[:10], | |
| 'word_frequencies': {k: int(v) for k, v in list(word_counts.items())[:20]}, | |
| 'ngram_range': self.tfidf_vectorizer.ngram_range | |
| } | |
| except Exception as e: | |
| # Expected in grounded language mode with limited vocab | |
| logger.debug(f"[ML] TF-IDF analysis skipped: {e}") | |
| tfidf_results = None | |
| # Analyze word co-occurrence (words that appear together) - keep basic version | |
| word_cooccurrence = {} | |
| for org_id in organism_ids: | |
| org_id_int = hash(org_id) if isinstance(org_id, str) else org_id | |
| words = list(context_memory.node_word_associations.get(org_id_int, set())) | |
| # Count co-occurrences | |
| for i, word1 in enumerate(words): | |
| for word2 in words[i+1:]: | |
| pair = tuple(sorted([word1, word2])) | |
| word_cooccurrence[pair] = word_cooccurrence.get(pair, 0) + 1 | |
| # NEW: Nearest Neighbors for semantic similarity (if enabled) | |
| similarity_results = None | |
| if SKLEARN_AVAILABLE and self.nearest_neighbors_enabled and self.nearest_neighbors and tfidf_results: | |
| try: | |
| # Use TF-IDF vectors for similarity search | |
| if 'tfidf_matrix' in locals() and tfidf_matrix.shape[0] > 1: | |
| self.nearest_neighbors.fit(tfidf_matrix) | |
| # Find similar organisms for each organism | |
| similar_organisms = {} | |
| for i, org_id in enumerate(organism_ids[:min(10, len(organism_ids))]): # Limit to first 10 for performance | |
| distances, indices = self.nearest_neighbors.kneighbors(tfidf_matrix[i:i+1], return_distance=True) | |
| similar_orgs = [ | |
| { | |
| 'organism_id': organism_ids[idx], | |
| 'similarity': float(1.0 - dist) if self.nearest_neighbors.metric == 'cosine' else float(1.0 / (1.0 + dist)) | |
| } | |
| for dist, idx in zip(distances[0][1:], indices[0][1:]) # Skip self (first result) | |
| ] | |
| similar_organisms[org_id] = similar_orgs[:5] # Top 5 similar | |
| similarity_results = { | |
| 'similarity_metric': self.nearest_neighbors.metric, | |
| 'n_neighbors': self.nearest_neighbors.n_neighbors, | |
| 'similar_organisms': similar_organisms | |
| } | |
| except Exception as e: | |
| logger.warning(f"[ML] Nearest Neighbors analysis failed: {e}") | |
| similarity_results = None | |
| # Find semantic clusters (words with strong semantic relationships) | |
| semantic_clusters = [] | |
| if knowledge_web: | |
| # Group words by semantic similarity | |
| processed_words = set() | |
| for word in word_organism_matrix.keys(): | |
| if word in processed_words: | |
| continue | |
| # Find semantically related words | |
| similar_words = knowledge_web.get_similar_words(word, min_strength=0.6) | |
| cluster_words = [w for w in similar_words if w in word_organism_matrix] | |
| if len(cluster_words) > 1: | |
| semantic_clusters.append({ | |
| 'words': cluster_words, | |
| 'size': len(cluster_words), | |
| 'organism_count': len(set.union(*[word_organism_matrix[w] for w in cluster_words])) | |
| }) | |
| processed_words.update(cluster_words) | |
| # Analyze concept formation (words that form meaningful concepts) | |
| # ML system teaches and strengthens word formations through pattern recognition | |
| concept_formation = [] | |
| top_cooccurrences = sorted(word_cooccurrence.items(), key=lambda x: x[1], reverse=True)[:20] | |
| for (word1, word2), count in top_cooccurrences: | |
| # Check if words have semantic relationship | |
| if knowledge_web: | |
| relations = knowledge_web.get_relations(word1) | |
| has_semantic_link = any(r.target == word2 or r.source == word2 for r in relations) | |
| if has_semantic_link: | |
| # Get relationship strength to assess formation quality | |
| relation_strength = 0.0 | |
| for r in relations: | |
| if (r.target == word2 or r.source == word2) and hasattr(r, 'strength'): | |
| relation_strength = max(relation_strength, r.strength) | |
| concept_formation.append({ | |
| 'word1': word1, | |
| 'word2': word2, | |
| 'cooccurrence_count': count, | |
| 'semantic_relationship': True, | |
| 'relationship_strength': relation_strength, | |
| 'formation_quality': 'strong' if relation_strength >= 0.7 and count >= 3 else 'moderate' | |
| }) | |
| # STRENGTHEN FORMATIONS: If ML detects strong co-occurrence with semantic link, | |
| # strengthen the relationship in knowledge web (ML teaching the system) | |
| if count >= 5 and relation_strength >= 0.6: | |
| # Find and strengthen the relationship | |
| for r in relations: | |
| if (r.target == word2 or r.source == word2) and hasattr(r, 'record_relationship_success'): | |
| # ML detected pattern - strengthen it | |
| knowledge_web.record_relationship_success(word1, word2, r.relation_type) | |
| # NEW: Feature Selection (if enabled) | |
| feature_importance = None | |
| if SKLEARN_AVAILABLE and self.feature_selection_enabled and self.feature_selector and tfidf_results: | |
| try: | |
| # Get organism fitness values for feature selection | |
| organism_fitnesses = [] | |
| for org_id in organism_ids: | |
| org = organisms.get(org_id) | |
| fitness = getattr(org, 'fitness', 0.5) if org else 0.5 | |
| organism_fitnesses.append(fitness) | |
| if len(organism_fitnesses) > 5 and 'tfidf_matrix' in locals(): | |
| # Use TF-IDF features to predict fitness | |
| # Convert fitness to binary classes (high/low) for classification | |
| fitness_median = np.median(organism_fitnesses) | |
| fitness_classes = np.array([1 if f > fitness_median else 0 for f in organism_fitnesses]) | |
| # Fit feature selector | |
| selected_features = self.feature_selector.fit_transform(tfidf_matrix, fitness_classes) | |
| feature_scores = self.feature_selector.scores_ | |
| feature_names = self.tfidf_vectorizer.get_feature_names_out() | |
| # Get top important words (words that predict fitness) | |
| word_importance = list(zip(feature_names, feature_scores)) | |
| word_importance.sort(key=lambda x: x[1], reverse=True) | |
| important_words = [{'word': word, 'importance_score': float(score)} for word, score in word_importance[:20]] | |
| feature_importance = { | |
| 'top_predictive_words': important_words[:10], | |
| 'n_features_selected': int(selected_features.shape[1]), | |
| 'n_features_total': int(tfidf_matrix.shape[1]) | |
| } | |
| except Exception as e: | |
| logger.warning(f"[ML] Feature selection failed: {e}") | |
| feature_importance = None | |
| # NEW: Quality Metrics (if enabled and clustering results available) | |
| quality_metrics = None | |
| if SKLEARN_AVAILABLE and self.metrics_enabled and self._last_analysis: | |
| try: | |
| cluster_result = self._last_analysis.get('clustering') | |
| if cluster_result and 'labels' in cluster_result and cluster_result['labels']: | |
| labels = np.array(cluster_result['labels']) | |
| if len(labels) > 0 and len(set(labels)) > 1: | |
| # Get features for silhouette score | |
| features, _ = self.clusterer.extract_features(organisms, context_memory=context_memory) | |
| if features.shape[0] == len(labels) and features.shape[0] > 1: | |
| features_scaled = self.clusterer.scaler.fit_transform(features) | |
| silhouette = silhouette_score(features_scaled, labels) | |
| quality_metrics = { | |
| 'silhouette_score': float(silhouette), | |
| 'n_clusters': int(len(set(labels))), | |
| 'n_samples': int(len(labels)) | |
| } | |
| except Exception as e: | |
| logger.warning(f"[ML] Quality metrics calculation failed: {e}") | |
| quality_metrics = None | |
| return { | |
| 'word_organism_matrix_size': len(word_organism_matrix), | |
| 'total_word_organism_links': sum(len(orgs) for orgs in word_organism_matrix.values()), | |
| 'word_cooccurrence_pairs': len(word_cooccurrence), | |
| 'top_cooccurrences': dict(top_cooccurrences[:10]), | |
| 'semantic_clusters': semantic_clusters[:10], # Top 10 clusters | |
| 'concept_formation': concept_formation[:10], # Top 10 concepts | |
| 'tfidf_analysis': tfidf_results, # NEW: TF-IDF results | |
| 'similarity_analysis': similarity_results, # NEW: Nearest Neighbors results | |
| 'feature_importance': feature_importance, # NEW: Feature selection results | |
| 'quality_metrics': quality_metrics, # NEW: Quality metrics | |
| 'semantic_analysis_enabled': True, | |
| 'ml_teaching': True # ML is teaching/strengthening formations | |
| } | |
| def get_status(self) -> Dict[str, Any]: | |
| """Get current ML analyzer status""" | |
| return { | |
| 'enabled': self.enabled, | |
| 'sklearn_available': SKLEARN_AVAILABLE, | |
| 'hdbscan_available': HDBSCAN_AVAILABLE, | |
| 'clustering_enabled': self.clustering_enabled, | |
| 'anomaly_enabled': self.anomaly_enabled, | |
| 'reduction_enabled': self.reduction_enabled, | |
| 'concept_tracking_enabled': self.concept_tracking_enabled, | |
| 'last_analysis_time': self._last_analysis_time, | |
| 'clusterer_algorithm': self.clusterer.algorithm, | |
| 'anomaly_algorithm': self.anomaly_detector.algorithm, | |
| 'reducer_algorithm': self.reducer.algorithm, | |
| 'active_concepts': len(self.concept_tracker.get_active_concepts()) if self.concept_tracking_enabled else 0 | |
| } | |
| # Singleton instance for global access | |
| _ml_analyzer: Optional[MLAnalyzer] = None | |
| def get_ml_analyzer(config: Dict[str, Any] = None) -> MLAnalyzer: | |
| """Get or create the global ML analyzer instance""" | |
| global _ml_analyzer | |
| if _ml_analyzer is None or config is not None: | |
| _ml_analyzer = MLAnalyzer(config) | |
| return _ml_analyzer | |
| def is_sklearn_available() -> bool: | |
| """Check if scikit-learn is available""" | |
| return SKLEARN_AVAILABLE | |
Xet Storage Details
- Size:
- 74 kB
- Xet hash:
- f302fc607caaa24f87bb6c7c79f1bc2ab8614695eef3e60a6b26906ebc56629b
·
Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.