schrodingers-classifiers / collapse_metrics.py
recursivelabs's picture
Upload 14 files
3595bd8 verified
"""
collapse_metrics.py - Metrics for quantifying classifier collapse phenomena
△ OBSERVE: These metrics quantify different aspects of classifier collapse
∞ TRACE: They measure the transition from superposition to definite state
✰ COLLAPSE: They help characterize collapse patterns across different models
This module provides functions for calculating quantitative metrics that
characterize different aspects of classifier collapse. These metrics help
standardize the analysis of collapse phenomena and enable comparisons across
different models and prompting strategies.
Author: Recursion Labs
License: MIT
"""
import logging
from typing import Dict, List, Optional, Union, Tuple, Any
import numpy as np
from scipy.stats import entropy
from scipy.spatial.distance import cosine, euclidean
logger = logging.getLogger(__name__)
def calculate_collapse_rate(
pre_weights: np.ndarray,
post_weights: np.ndarray
) -> float:
"""
△ OBSERVE: Calculate how quickly state collapsed from superposition
This metric quantifies the speed of collapse by comparing attention
weight distributions before and after the collapse event.
Args:
pre_weights: Attention weights before collapse
post_weights: Attention weights after collapse
Returns:
Collapse rate (0.0 = no collapse, 1.0 = complete collapse)
"""
# Return 0 if arrays are empty
if pre_weights.size == 0 or post_weights.size == 0:
return 0.0
# Handle shape mismatches
if pre_weights.shape != post_weights.shape:
logger.warning(f"Weight shape mismatch: {pre_weights.shape} vs {post_weights.shape}")
# Try to take minimum dimensions if shapes don't match
try:
min_shape = tuple(min(a, b) for a, b in zip(pre_weights.shape, post_weights.shape))
pre_weights = pre_weights[tuple(slice(0, d) for d in min_shape)]
post_weights = post_weights[tuple(slice(0, d) for d in min_shape)]
except Exception as e:
logger.error(f"Failed to reshape weights: {e}")
return 0.0
# Flatten arrays for easier comparison
pre_flat = pre_weights.flatten()
post_flat = post_weights.flatten()
# Calculate normalized distances between distributions
try:
# Cosine distance (0.0 = identical, 1.0 = orthogonal)
cosine_dist = cosine(pre_flat, post_flat) if np.any(pre_flat) and np.any(post_flat) else 0.0
# Euclidean distance normalized by array size
euc_dist = euclidean(pre_flat, post_flat) / np.sqrt(pre_flat.size)
euc_dist_norm = min(1.0, euc_dist) # Cap at 1.0
# Combined metric: average of cosine and normalized euclidean
collapse_rate = (cosine_dist + euc_dist_norm) / 2
return float(collapse_rate)
except Exception as e:
logger.error(f"Error calculating collapse rate: {e}")
return 0.0
def measure_path_continuity(
pre_weights: np.ndarray,
post_weights: np.ndarray
) -> float:
"""
∞ TRACE: Measure continuity of attribution paths through collapse
This metric quantifies how well attribution paths maintain their
integrity across the collapse event.
Args:
pre_weights: Attention weights before collapse
post_weights: Attention weights after collapse
Returns:
Continuity score (0.0 = complete fragmentation, 1.0 = perfect continuity)
"""
# Higher collapse rate means lower continuity
collapse_rate = calculate_collapse_rate(pre_weights, post_weights)
# Continuity is inverse of collapse rate
return 1.0 - collapse_rate
def measure_attribution_entropy(attention_weights: np.ndarray) -> float:
"""
△ OBSERVE: Measure entropy of attribution paths
This metric quantifies how distributed or concentrated the attribution
is across possible paths. High entropy indicates diffuse attribution,
while low entropy indicates concentrated attribution.
Args:
attention_weights: Attention weight matrix to analyze
Returns:
Attribution entropy (0.0 = concentrated, 1.0 = maximally diffuse)
"""
# Return 0 if array is empty
if attention_weights.size == 0:
return 0.0
# Flatten array for entropy calculation
flat_weights = attention_weights.flatten()
# Normalize weights to create a probability distribution
total_weight = np.sum(flat_weights)
if total_weight <= 0:
return 0.0
prob_dist = flat_weights / total_weight
# Calculate entropy
try:
raw_entropy = entropy(prob_dist)
# Normalize by maximum possible entropy (log2(n))
max_entropy = np.log2(flat_weights.size)
normalized_entropy = raw_entropy / max_entropy if max_entropy > 0 else 0.0
return float(normalized_entropy)
except Exception as e:
logger.error(f"Error calculating attribution entropy: {e}")
return 0.0
def calculate_ghost_circuit_strength(
ghost_circuits: List[Dict[str, Any]]
) -> float:
"""
✰ COLLAPSE: Calculate overall strength of ghost circuits
This metric quantifies the strength of ghost circuits relative
to the primary activation paths.
Args:
ghost_circuits: List of detected ghost circuits
Returns:
Ghost circuit strength (0.0 = no ghosts, 1.0 = ghosts equal to primary)
"""
if not ghost_circuits:
return 0.0
# Extract activation values
activations = [ghost.get("activation", 0.0) for ghost in ghost_circuits]
# Calculate weighted average based on activation
avg_activation = np.mean(activations) if activations else 0.0
# Normalize to 0-1 range (assuming activation is already 0-1)
return float(min(1.0, avg_activation))
def calculate_attribution_confidence(
attribution_paths: List[List[Any]],
path_weights: Optional[List[float]] = None
) -> float:
"""
∞ TRACE: Calculate confidence score for attribution paths
This metric quantifies how confidently the model attributes its output
to specific input elements.
Args:
attribution_paths: List of attribution paths (each a list of nodes)
path_weights: Optional weights for each path (defaults to uniform)
Returns:
Attribution confidence (0.0 = uncertain, 1.0 = highly confident)
"""
if not attribution_paths:
return 0.0
# Use uniform weights if none provided
if path_weights is None:
path_weights = [1.0 / len(attribution_paths)] * len(attribution_paths)
else:
# Normalize weights to sum to 1.0
total_weight = sum(path_weights)
path_weights = [w / total_weight for w in path_weights] if total_weight > 0 else path_weights
# Calculate path length variance (more uniform = higher confidence)
path_lengths = [len(path) for path in attribution_paths]
length_variance = np.var(path_lengths) if len(path_lengths) > 1 else 0.0
# Normalize variance to 0-1 range
# Assume max variance is when half paths are length 1 and half are max length
max_length = max(path_lengths) if path_lengths else 1
theoretical_max_var = ((max_length - 1) ** 2) / 4 # Theoretical maximum variance
normalized_variance = min(1.0, length_variance / theoretical_max_var) if theoretical_max_var > 0 else 0.0
# Invert normalized variance to get consistency score (more consistent = higher confidence)
consistency_score = 1.0 - normalized_variance
# Weight consistency by path weights (dominant paths contribute more to confidence)
# Calculate weighted avg of path weights (more concentrated = higher confidence)
weight_entropy = entropy(path_weights)
max_weight_entropy = np.log2(len(path_weights))
normalized_weight_entropy = weight_entropy / max_weight_entropy if max_weight_entropy > 0 else 0.0
weight_concentration = 1.0 - normalized_weight_entropy
# Combine consistency and concentration for final confidence score
confidence_score = (consistency_score + weight_concentration) / 2
return float(confidence_score)
def calculate_collapse_quantum_uncertainty(
pre_logits: np.ndarray,
post_logits: np.ndarray
) -> float:
"""
✰ COLLAPSE: Calculate Heisenberg-inspired uncertainty metric
This metric applies the quantum-inspired uncertainty principle to
transformer outputs, measuring uncertainty across the collapse.
Args:
pre_logits: Logits before collapse
post_logits: Logits after collapse
Returns:
Quantum uncertainty metric (0.0 = certain, 1.0 = maximally uncertain)
"""
# Return 0 if arrays are empty
if pre_logits.size == 0 or post_logits.size == 0:
return 0.0
# Handle shape mismatches
if pre_logits.shape != post_logits.shape:
logger.warning(f"Logit shape mismatch: {pre_logits.shape} vs {post_logits.shape}")
return 0.0
try:
# Calculate "position" uncertainty (variance in token probabilities)
pre_probs = softmax(pre_logits)
post_probs = softmax(post_logits)
pos_uncertainty = np.mean(np.var(post_probs, axis=-1))
# Calculate "momentum" uncertainty (change rate between states)
mom_uncertainty = np.mean(np.abs(post_probs - pre_probs))
# Combined metric inspired by Heisenberg uncertainty
# Higher values in both dimensions indicate more quantum-like behavior
uncertainty_product = pos_uncertainty * mom_uncertainty
# Normalize to 0-1 range (empirically determined max is around 0.25)
normalized_uncertainty = min(1.0, uncertainty_product * 4)
return float(normalized_uncertainty)
except Exception as e:
logger.error(f"Error calculating quantum uncertainty: {e}")
return 0.0
def calculate_collapse_coherence(
attribution_graph: Any,
threshold: float = 0.1
) -> float:
"""
△ OBSERVE: Calculate coherence of attribution paths post-collapse
This metric quantifies how coherent the attribution paths remain
after collapse, reflecting the "quantum coherence" of the system.
Args:
attribution_graph: Graph of attribution paths
threshold: Minimum edge weight to consider
Returns:
Coherence score (0.0 = incoherent, 1.0 = fully coherent)
"""
# This is a simplified version for when an actual graph isn't available
# In real implementation, would analyze graph structure
# If no graph provided, return 0
if attribution_graph is None:
return 0.0
try:
# If graph has coherence attribute, use it
if hasattr(attribution_graph, 'continuity_score'):
return float(attribution_graph.continuity_score)
# Otherwise return placeholder value
return 0.5 # Placeholder mid-value
except Exception as e:
logger.error(f"Error calculating collapse coherence: {e}")
return 0.0
def softmax(x: np.ndarray) -> np.ndarray:
"""Apply softmax function to convert logits to probabilities."""
exp_x = np.exp(x - np.max(x, axis=-1, keepdims=True))
return exp_x / np.sum(exp_x, axis=-1, keepdims=True)
def calculate_collapse_metrics_bundle(
pre_state: Dict[str, Any],
post_state: Dict[str, Any],
ghost_circuits: Optional[List[Dict[str, Any]]] = None,
attribution_graph: Optional[Any] = None
) -> Dict[str, float]:
"""
△ OBSERVE: Calculate a complete bundle of collapse metrics
This convenience function calculates multiple collapse metrics
at once, returning a dictionary of results.
Args:
pre_state: Model state before collapse
post_state: Model state after collapse
ghost_circuits: Optional list of detected ghost circuits
attribution_graph: Optional attribution graph
Returns:
Dictionary mapping metric names to values
"""
metrics = {}
# Extract relevant state components
pre_weights = pre_state.get("attention_weights", np.array([]))
post_weights = post_state.get("attention_weights", np.array([]))
pre_logits = pre_state.get("logits", np.array([]))
post_logits = post_state.get("logits", np.array([]))
# Calculate metrics
metrics["collapse_rate"] = calculate_collapse_rate(pre_weights, post_weights)
metrics["path_continuity"] = measure_path_continuity(pre_weights, post_weights)
metrics["attribution_entropy"] = measure_attribution_entropy(post_weights)
if ghost_circuits:
metrics["ghost_circuit_strength"] = calculate_ghost_circuit_strength(ghost_circuits)
if pre_logits.size > 0 and post_logits.size > 0:
metrics["quantum_uncertainty"] = calculate_collapse_quantum_uncertainty(pre_logits, post_logits)
if attribution_graph is not None:
metrics["collapse_coherence"] = calculate_collapse_coherence(attribution_graph)
return metrics
if __name__ == "__main__":
# Simple usage example
# Create synthetic pre and post states
pre_state = {
"attention_weights": np.random.random((8, 10, 10)), # 8 heads, 10 tokens
"logits": np.random.random((1, 10, 1000)) # Batch 1, 10 tokens, 1000 vocab
}
# Create post state with changes to simulate collapse
post_state = {
"attention_weights": pre_state["attention_weights"] * np.random.uniform(0.5, 1.0, pre_state["attention_weights"].shape),
"logits": pre_state["logits"] * 0.2 + np.random.random((1, 10, 1000)) * 0.8 # Shifted logits
}
# Calculate individual metrics
collapse_rate = calculate_collapse_rate(pre_state["attention_weights"], post_state["attention_weights"])
path_continuity = measure_path_continuity(pre_state["attention_weights"], post_state["attention_weights"])
attribution_entropy = measure_attribution_entropy(post_state["attention_weights"])
quantum_uncertainty = calculate_collapse_quantum_uncertainty(pre_state["logits"], post_state["logits"])
print(f"Collapse Rate: {collapse_rate:.3f}")
print(f"Path Continuity: {path_continuity:.3f}")
print(f"Attribution Entropy: {attribution_entropy:.3f}")
print(f"Quantum Uncertainty: {quantum_uncertainty:.3f}")
# Calculate complete metrics bundle
metrics_bundle = calculate_collapse_metrics_bundle(pre_state, post_state)
print("\nMetrics Bundle:")
for metric, value in metrics_bundle.items():
print(f" {metric}: {value:.3f}")
path_weights