quantrs2_ml/automl/search/
hyperparameter_optimizer.rs1use crate::automl::config::{HyperparameterSearchSpace, QuantumHyperparameterSpace};
6use crate::automl::pipeline::QuantumMLPipeline;
7use crate::error::Result;
8use scirs2_core::ndarray::{Array1, Array2};
9use std::collections::HashMap;
10
11#[derive(Debug, Clone)]
13pub struct QuantumHyperparameterOptimizer {
14 strategy: HyperparameterOptimizationStrategy,
16
17 search_space: HyperparameterSearchSpace,
19
20 optimization_history: OptimizationHistory,
22
23 best_configuration: Option<HyperparameterConfiguration>,
25}
26
27#[derive(Debug, Clone)]
29pub enum HyperparameterOptimizationStrategy {
30 RandomSearch,
31 GridSearch,
32 BayesianOptimization,
33 EvolutionarySearch,
34 QuantumAnnealing,
35 QuantumVariational,
36 HybridQuantumClassical,
37}
38
39#[derive(Debug, Clone)]
41pub struct HyperparameterConfiguration {
42 pub classical_params: HashMap<String, f64>,
44
45 pub quantum_params: HashMap<String, f64>,
47
48 pub architecture_params: HashMap<String, usize>,
50
51 pub performance_score: f64,
53}
54
55#[derive(Debug, Clone)]
57pub struct OptimizationHistory {
58 pub trials: Vec<OptimizationTrial>,
60
61 pub best_trial: Option<OptimizationTrial>,
63
64 pub convergence_history: Vec<f64>,
66}
67
68#[derive(Debug, Clone)]
70pub struct OptimizationTrial {
71 pub trial_id: usize,
73
74 pub configuration: HyperparameterConfiguration,
76
77 pub performance: f64,
79
80 pub resource_usage: ResourceUsage,
82
83 pub duration: f64,
85}
86
87#[derive(Debug, Clone)]
89pub struct ResourceUsage {
90 pub memory_mb: f64,
92
93 pub quantum_resources: QuantumResourceUsage,
95
96 pub training_time: f64,
98}
99
100#[derive(Debug, Clone)]
102pub struct QuantumResourceUsage {
103 pub qubits_used: usize,
105
106 pub circuit_depth: usize,
108
109 pub num_gates: usize,
111
112 pub coherence_time_used: f64,
114}
115
116impl QuantumHyperparameterOptimizer {
117 pub fn new(search_space: &HyperparameterSearchSpace) -> Self {
119 Self {
120 strategy: HyperparameterOptimizationStrategy::BayesianOptimization,
121 search_space: search_space.clone(),
122 optimization_history: OptimizationHistory::new(),
123 best_configuration: None,
124 }
125 }
126
127 pub fn optimize(
129 &mut self,
130 pipeline: QuantumMLPipeline,
131 X: &Array2<f64>,
132 y: &Array1<f64>,
133 ) -> Result<QuantumMLPipeline> {
134 match self.strategy {
135 HyperparameterOptimizationStrategy::RandomSearch => self.random_search(pipeline, X, y),
136 HyperparameterOptimizationStrategy::BayesianOptimization => {
137 self.bayesian_optimization(pipeline, X, y)
138 }
139 _ => {
140 self.random_search(pipeline, X, y)
142 }
143 }
144 }
145
146 pub fn best_configuration(&self) -> Option<&HyperparameterConfiguration> {
148 self.best_configuration.as_ref()
149 }
150
151 pub fn history(&self) -> &OptimizationHistory {
153 &self.optimization_history
154 }
155
156 fn random_search(
159 &mut self,
160 mut pipeline: QuantumMLPipeline,
161 X: &Array2<f64>,
162 y: &Array1<f64>,
163 ) -> Result<QuantumMLPipeline> {
164 let num_trials = 20; let mut best_pipeline = pipeline.clone();
166 let mut best_score = f64::NEG_INFINITY;
167
168 for trial_id in 0..num_trials {
169 let trial_start = std::time::Instant::now();
170
171 let config = self.generate_random_configuration();
173
174 pipeline.apply_hyperparameters(&config)?;
176
177 let score = self.evaluate_configuration(&pipeline, X, y)?;
179
180 let elapsed = trial_start.elapsed().as_secs_f64();
181
182 let trial = OptimizationTrial {
184 trial_id,
185 configuration: config.clone(),
186 performance: score,
187 resource_usage: ResourceUsage::default(),
188 duration: elapsed,
189 };
190 self.optimization_history.trials.push(trial);
191
192 if score > best_score {
194 best_score = score;
195 best_pipeline = pipeline.clone();
196 self.best_configuration = Some(config);
197 self.optimization_history.best_trial = Some(
198 self.optimization_history
199 .trials
200 .last()
201 .expect("trials should not be empty")
202 .clone(),
203 );
204 }
205
206 self.optimization_history
207 .convergence_history
208 .push(best_score);
209 }
210
211 Ok(best_pipeline)
212 }
213
214 fn bayesian_optimization(
215 &mut self,
216 pipeline: QuantumMLPipeline,
217 X: &Array2<f64>,
218 y: &Array1<f64>,
219 ) -> Result<QuantumMLPipeline> {
220 self.random_search(pipeline, X, y)
223 }
224
225 fn generate_random_configuration(&self) -> HyperparameterConfiguration {
226 use fastrand;
227
228 let mut classical_params = HashMap::new();
229 let mut quantum_params = HashMap::new();
230 let mut architecture_params = HashMap::new();
231
232 let lr_min = self.search_space.learning_rates.0;
234 let lr_max = self.search_space.learning_rates.1;
235 let learning_rate = lr_min + fastrand::f64() * (lr_max - lr_min);
236 classical_params.insert("learning_rate".to_string(), learning_rate);
237
238 let reg_min = self.search_space.regularization.0;
240 let reg_max = self.search_space.regularization.1;
241 let regularization = reg_min + fastrand::f64() * (reg_max - reg_min);
242 classical_params.insert("regularization".to_string(), regularization);
243
244 if !self.search_space.batch_sizes.is_empty() {
246 let batch_size_idx = fastrand::usize(..self.search_space.batch_sizes.len());
247 let batch_size = self.search_space.batch_sizes[batch_size_idx] as f64;
248 classical_params.insert("batch_size".to_string(), batch_size);
249 }
250
251 let qubit_min = self.search_space.quantum_params.num_qubits.0;
253 let qubit_max = self.search_space.quantum_params.num_qubits.1;
254 let num_qubits = qubit_min + fastrand::usize(..(qubit_max - qubit_min + 1));
255 quantum_params.insert("num_qubits".to_string(), num_qubits as f64);
256
257 let depth_min = self.search_space.quantum_params.circuit_depth.0;
258 let depth_max = self.search_space.quantum_params.circuit_depth.1;
259 let circuit_depth = depth_min + fastrand::usize(..(depth_max - depth_min + 1));
260 quantum_params.insert("circuit_depth".to_string(), circuit_depth as f64);
261
262 HyperparameterConfiguration {
263 classical_params,
264 quantum_params,
265 architecture_params,
266 performance_score: 0.0,
267 }
268 }
269
270 fn evaluate_configuration(
271 &self,
272 pipeline: &QuantumMLPipeline,
273 X: &Array2<f64>,
274 y: &Array1<f64>,
275 ) -> Result<f64> {
276 let split_point = (X.nrows() as f64 * 0.8) as usize;
278
279 let X_train = X
280 .slice(scirs2_core::ndarray::s![0..split_point, ..])
281 .to_owned();
282 let y_train = y.slice(scirs2_core::ndarray::s![0..split_point]).to_owned();
283 let X_val = X
284 .slice(scirs2_core::ndarray::s![split_point.., ..])
285 .to_owned();
286 let y_val = y.slice(scirs2_core::ndarray::s![split_point..]).to_owned();
287
288 let mut pipeline_copy = pipeline.clone();
289 pipeline_copy.fit(&X_train, &y_train)?;
290 let predictions = pipeline_copy.predict(&X_val)?;
291
292 let score = predictions
294 .iter()
295 .zip(y_val.iter())
296 .map(|(pred, true_val)| (pred - true_val).powi(2))
297 .sum::<f64>()
298 / predictions.len() as f64;
299
300 Ok(-score) }
302}
303
304impl OptimizationHistory {
305 fn new() -> Self {
306 Self {
307 trials: Vec::new(),
308 best_trial: None,
309 convergence_history: Vec::new(),
310 }
311 }
312}
313
314impl Default for ResourceUsage {
315 fn default() -> Self {
316 Self {
317 memory_mb: 0.0,
318 quantum_resources: QuantumResourceUsage::default(),
319 training_time: 0.0,
320 }
321 }
322}
323
324impl Default for QuantumResourceUsage {
325 fn default() -> Self {
326 Self {
327 qubits_used: 0,
328 circuit_depth: 0,
329 num_gates: 0,
330 coherence_time_used: 0.0,
331 }
332 }
333}