Interactive Evolution Tutorial
Interactive Genetic Algorithms (IGA) incorporate human feedback into the evolutionary process. Instead of an automated fitness function, users evaluate candidates through ratings, comparisons, or selections.
When to Use Interactive Evolution
Ideal for:
- Aesthetic optimization (art, design, music)
- Subjective preferences (user interfaces)
- Hard-to-formalize objectives
- Creative exploration
Challenges:
- User fatigue limits evaluations
- Noisy, inconsistent feedback
- Slower convergence than automated GA
Evaluation Modes
Fugue-evo supports multiple ways to gather user feedback:
| Mode | User Action | Best For |
|---|---|---|
| Rating | Score each candidate 1-10 | Absolute quality assessment |
| Pairwise | Choose better of two | Relative comparisons |
| Batch Selection | Pick best N from batch | Quick approximate ranking |
Complete Example
//! Interactive Genetic Algorithm Example
//!
//! This example demonstrates how to use the Interactive GA for human-in-the-loop
//! evolutionary optimization. Instead of an automated fitness function, users
//! provide feedback by rating, comparing, or selecting candidates.
//!
//! In this example, we simulate user feedback with a simple automated scorer,
//! but in a real application, you would present candidates to users via a UI.
use fugue_evo::genome::bounds::Bounds;
use fugue_evo::interactive::prelude::*;
use fugue_evo::prelude::*;
use rand::rngs::StdRng;
use rand::SeedableRng;
/// Simulates user preference for solutions close to a target
struct SimulatedUserPreference {
target: Vec<f64>,
}
impl SimulatedUserPreference {
fn new(dim: usize) -> Self {
// User prefers solutions where values are around 0.5
Self {
target: vec![0.5; dim],
}
}
/// Simulate a user rating (1-10 scale)
fn rate(&self, genome: &RealVector) -> f64 {
let distance: f64 = genome
.genes()
.iter()
.zip(self.target.iter())
.map(|(g, t)| (g - t).powi(2))
.sum::<f64>()
.sqrt();
// Convert distance to rating (closer = higher rating)
let rating = 10.0 - (distance * 5.0).min(9.0);
rating.max(1.0)
}
/// Simulate pairwise comparison
fn compare(&self, a: &RealVector, b: &RealVector) -> std::cmp::Ordering {
let rating_a = self.rate(a);
let rating_b = self.rate(b);
rating_a.partial_cmp(&rating_b).unwrap()
}
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
println!("=== Interactive Genetic Algorithm Demo ===\n");
let mut rng = StdRng::seed_from_u64(42);
const DIM: usize = 5;
let bounds = MultiBounds::uniform(Bounds::new(0.0, 1.0), DIM);
// Create the simulated user preference
let user = SimulatedUserPreference::new(DIM);
// Build the Interactive GA
let mut iga = InteractiveGABuilder::<RealVector, (), (), ()>::new()
.population_size(12)
.elitism_count(2)
.evaluation_mode(EvaluationMode::Rating)
.batch_size(4)
.min_coverage(0.8)
.max_generations(5)
.aggregation_model(AggregationModel::DirectRating {
default_rating: 5.0,
})
.bounds(bounds)
.selection(TournamentSelection::new(2))
.crossover(SbxCrossover::new(15.0))
.mutation(PolynomialMutation::new(20.0))
.build()?;
println!("Starting interactive evolution...\n");
println!(
"Configuration: {} individuals, {} mode, {} generations max",
iga.config().population_size,
match iga.config().evaluation_mode {
EvaluationMode::Rating => "rating",
EvaluationMode::Pairwise => "pairwise",
EvaluationMode::BatchSelection => "batch selection",
EvaluationMode::Adaptive => "adaptive",
},
iga.config().max_generations
);
println!();
// Main evolution loop
loop {
match iga.step(&mut rng) {
StepResult::NeedsEvaluation(request) => {
// In a real app, you'd present this to a user via UI
// Here we simulate user feedback
let response = simulate_user_response(&user, &request);
iga.provide_response(response);
}
StepResult::GenerationComplete {
generation,
best_fitness,
coverage,
} => {
println!(
"Generation {} complete: best = {:.2}, coverage = {:.0}%",
generation,
best_fitness.unwrap_or(0.0),
coverage * 100.0
);
}
StepResult::Complete(result) => {
println!("\n=== Evolution Complete ===");
println!("Reason: {}", result.termination_reason);
println!("Generations: {}", result.generations);
println!("Total evaluations: {}", result.total_evaluations);
println!("\nTop 3 candidates:");
for (i, candidate) in result.best_candidates.iter().take(3).enumerate() {
println!(
" #{}: fitness = {:.2}, genes = {:?}",
i + 1,
candidate.fitness_estimate.unwrap_or(0.0),
candidate
.genome
.genes()
.iter()
.map(|g| format!("{:.3}", g))
.collect::<Vec<_>>()
.join(", ")
);
}
break;
}
}
}
// Demonstrate different evaluation modes
println!("\n=== Batch Selection Mode Demo ===\n");
let mut iga_batch = InteractiveGABuilder::<RealVector, (), (), ()>::new()
.population_size(12)
.evaluation_mode(EvaluationMode::BatchSelection)
.batch_size(6)
.select_count(2)
.min_coverage(0.5)
.max_generations(3)
.aggregation_model(AggregationModel::ImplicitRanking {
selected_bonus: 1.0,
not_selected_penalty: 0.3,
base_fitness: 5.0,
})
.bounds(MultiBounds::uniform(Bounds::new(0.0, 1.0), DIM))
.selection(TournamentSelection::new(2))
.crossover(SbxCrossover::new(15.0))
.mutation(PolynomialMutation::new(20.0))
.build()?;
loop {
match iga_batch.step(&mut rng) {
StepResult::NeedsEvaluation(request) => {
let response = simulate_user_response(&user, &request);
iga_batch.provide_response(response);
}
StepResult::GenerationComplete {
generation,
best_fitness,
..
} => {
println!(
"Generation {}: best = {:.2}",
generation,
best_fitness.unwrap_or(0.0)
);
}
StepResult::Complete(result) => {
println!("\nBatch selection mode complete!");
println!(
"Best fitness: {:.2}",
result.best_candidates[0].fitness_estimate.unwrap_or(0.0)
);
break;
}
}
}
Ok(())
}
/// Simulate user response to an evaluation request
fn simulate_user_response(
user: &SimulatedUserPreference,
request: &EvaluationRequest<RealVector>,
) -> EvaluationResponse {
match request {
EvaluationRequest::RateCandidates { candidates, .. } => {
let ratings: Vec<_> = candidates
.iter()
.map(|c| (c.id, user.rate(&c.genome)))
.collect();
EvaluationResponse::ratings(ratings)
}
EvaluationRequest::PairwiseComparison {
candidate_a,
candidate_b,
..
} => {
use std::cmp::Ordering;
match user.compare(&candidate_a.genome, &candidate_b.genome) {
Ordering::Greater => EvaluationResponse::winner(candidate_a.id),
Ordering::Less => EvaluationResponse::winner(candidate_b.id),
Ordering::Equal => EvaluationResponse::tie(),
}
}
EvaluationRequest::BatchSelection {
candidates,
select_count,
..
} => {
// Sort by rating and select top N
let mut rated: Vec<_> = candidates
.iter()
.map(|c| (c.id, user.rate(&c.genome)))
.collect();
rated.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap());
let selected: Vec<_> = rated
.iter()
.take(*select_count)
.map(|(id, _)| *id)
.collect();
EvaluationResponse::selected(selected)
}
}
}
Running the Example
cargo run --example interactive_evolution
This example simulates user feedback. In a real application, you would replace the simulation with actual UI interaction.
Key Components
Building an Interactive GA
let mut iga = InteractiveGABuilder::<RealVector, (), (), ()>::new()
.population_size(12)
.elitism_count(2)
.evaluation_mode(EvaluationMode::Rating)
.batch_size(4)
.min_coverage(0.8)
.max_generations(5)
.aggregation_model(AggregationModel::DirectRating {
default_rating: 5.0,
})
.bounds(bounds)
.selection(TournamentSelection::new(2))
.crossover(SbxCrossover::new(15.0))
.mutation(PolynomialMutation::new(20.0))
.build()?;
Key parameters:
evaluation_mode: How users provide feedbackbatch_size: Candidates shown per evaluation roundmin_coverage: Fraction of population needing evaluationaggregation_model: How to combine multiple evaluations
The Step Loop
loop {
match iga.step(&mut rng) {
StepResult::NeedsEvaluation(request) => {
// Present to user, get feedback
let response = get_user_response(&request);
iga.provide_response(response);
}
StepResult::GenerationComplete { generation, best_fitness, coverage } => {
println!("Generation {} complete", generation);
}
StepResult::Complete(result) => {
println!("Evolution complete!");
break;
}
}
}
Handling Evaluation Requests
Rating Mode:
EvaluationRequest::RateCandidates { candidates, .. } => {
// Show candidates to user
for candidate in candidates {
display_candidate(&candidate.genome);
}
// Collect ratings
let ratings: Vec<(CandidateId, f64)> = /* user input */;
EvaluationResponse::ratings(ratings)
}
Pairwise Mode:
EvaluationRequest::PairwiseComparison { candidate_a, candidate_b, .. } => {
// Show both candidates
display_comparison(&candidate_a.genome, &candidate_b.genome);
// Get user's choice
let winner = /* user choice */;
EvaluationResponse::winner(winner)
}
Batch Selection:
EvaluationRequest::BatchSelection { candidates, select_count, .. } => {
// Show all candidates
for c in candidates { display_candidate(&c.genome); }
// User selects best N
let selected: Vec<CandidateId> = /* user picks */;
EvaluationResponse::selected(selected)
}
Aggregation Models
How to combine feedback into fitness estimates:
Direct Rating
AggregationModel::DirectRating { default_rating: 5.0 }
Uses ratings directly as fitness. Unevaluated candidates get the default.
Implicit Ranking
AggregationModel::ImplicitRanking {
selected_bonus: 1.0,
not_selected_penalty: 0.3,
base_fitness: 5.0,
}
For batch selection mode:
- Selected candidates get bonus
- Non-selected get penalty
- Accumulates over evaluations
Bradley-Terry Model
For pairwise comparisons, estimates latent "skill" from win/loss records using the Bradley-Terry statistical model.
Reducing User Fatigue
Smaller Population
.population_size(12)
Fewer candidates = fewer evaluations needed.
Coverage Threshold
.min_coverage(0.5) // Only evaluate 50% of population
Not every candidate needs evaluation each generation.
Batch Size Tuning
.batch_size(4) // Show 4 at a time
- Too small: Many rounds, tedious
- Too large: Overwhelming, poor decisions
Evaluation Budget
.max_evaluations(100) // Stop after 100 user interactions
Simulating User Feedback
For testing, simulate user preferences:
struct SimulatedUser {
target: Vec<f64>,
}
impl SimulatedUser {
fn rate(&self, genome: &RealVector) -> f64 {
let distance: f64 = genome.genes().iter()
.zip(self.target.iter())
.map(|(g, t)| (g - t).powi(2))
.sum::<f64>()
.sqrt();
// Closer to target = higher rating
10.0 - distance.min(9.0)
}
}
This allows testing IGA logic without human interaction.
Real-World Integration
Web Application
// Pseudocode for web integration
async fn evolution_endpoint(state: &mut IgaState) -> Response {
match state.iga.step(&mut state.rng) {
StepResult::NeedsEvaluation(request) => {
// Return candidates to frontend
Json(CandidatesForEvaluation::from(request))
}
// ...
}
}
async fn feedback_endpoint(feedback: UserFeedback, state: &mut IgaState) {
let response = EvaluationResponse::from(feedback);
state.iga.provide_response(response);
}
GUI Application
fn update(&mut self, message: Message) {
match message {
Message::NextStep => {
if let StepResult::NeedsEvaluation(req) = self.iga.step(&mut self.rng) {
self.current_request = Some(req);
}
}
Message::UserRated(ratings) => {
let response = EvaluationResponse::ratings(ratings);
self.iga.provide_response(response);
}
}
}
Exercises
- Different modes: Compare Rating vs Batch Selection modes
- Noisy preferences: Add randomness to simulated user, observe robustness
- Visualization: Display RealVector as colors/shapes for visual evaluation
Next Steps
- Hyperparameter Learning - Adaptive parameter tuning
- Custom Fitness Functions - Combine interactive with automated fitness