quantrs2_core/qml/
quantum_boltzmann.rs

1//! Quantum Boltzmann Machines
2//!
3//! This module implements quantum versions of Boltzmann machines for
4//! probabilistic modeling and generative learning with quantum advantages.
5//!
6//! # Theoretical Background
7//!
8//! Quantum Boltzmann Machines (QBMs) extend classical Boltzmann machines
9//! to leverage quantum superposition and entanglement for enhanced
10//! representational power and potentially faster sampling via quantum annealing.
11//!
12//! # Key Features
13//!
14//! - **Quantum Restricted Boltzmann Machines (QRBM)**: Bipartite quantum architecture
15//! - **Quantum Sampling**: Quantum annealing for Boltzmann sampling
16//! - **Contrastive Divergence**: Quantum CD-k training algorithm
17//! - **Energy-Based Learning**: Quantum energy function optimization
18//! - **Generative Modeling**: Quantum state generation and sampling
19//!
20//! # References
21//!
22//! - "Quantum Boltzmann Machine" (Amin et al., 2018)
23//! - "Training Quantum Boltzmann Machines" (Kieferová & Wiebe, 2017)
24//! - "Quantum-Enhanced Machine Learning" (2024)
25
26use crate::error::{QuantRS2Error, QuantRS2Result};
27use scirs2_core::ndarray::{Array1, Array2};
28use scirs2_core::random::prelude::*;
29use scirs2_core::Complex64;
30use std::f64::consts::PI;
31
32/// Configuration for Quantum Restricted Boltzmann Machine
33#[derive(Debug, Clone)]
34pub struct QRBMConfig {
35    /// Number of visible qubits
36    pub num_visible: usize,
37    /// Number of hidden qubits
38    pub num_hidden: usize,
39    /// Learning rate
40    pub learning_rate: f64,
41    /// Number of Gibbs sampling steps (CD-k)
42    pub k_steps: usize,
43    /// Temperature for Boltzmann sampling
44    pub temperature: f64,
45    /// Regularization strength
46    pub l2_reg: f64,
47}
48
49impl Default for QRBMConfig {
50    fn default() -> Self {
51        Self {
52            num_visible: 4,
53            num_hidden: 2,
54            learning_rate: 0.01,
55            k_steps: 1,
56            temperature: 1.0,
57            l2_reg: 0.001,
58        }
59    }
60}
61
62/// Quantum Restricted Boltzmann Machine
63#[derive(Debug, Clone)]
64pub struct QuantumRBM {
65    /// Configuration
66    config: QRBMConfig,
67    /// Weight matrix (visible Ă— hidden)
68    weights: Array2<f64>,
69    /// Visible bias
70    visible_bias: Array1<f64>,
71    /// Hidden bias
72    hidden_bias: Array1<f64>,
73    /// Training history
74    history: Vec<f64>,
75}
76
77impl QuantumRBM {
78    /// Create new Quantum RBM
79    pub fn new(config: QRBMConfig) -> Self {
80        let mut rng = thread_rng();
81        let scale = 0.01;
82
83        let weights = Array2::from_shape_fn((config.num_visible, config.num_hidden), |_| {
84            rng.gen_range(-scale..scale)
85        });
86
87        let visible_bias = Array1::zeros(config.num_visible);
88        let hidden_bias = Array1::zeros(config.num_hidden);
89
90        Self {
91            config,
92            weights,
93            visible_bias,
94            hidden_bias,
95            history: Vec::new(),
96        }
97    }
98
99    /// Train on batch of quantum states
100    pub fn train_batch(&mut self, data: &[Array1<Complex64>]) -> QuantRS2Result<f64> {
101        let mut total_error = 0.0;
102
103        for state in data {
104            // Convert quantum state to classical visible units
105            let visible = self.quantum_to_classical(state)?;
106
107            // Positive phase: compute hidden probabilities given data
108            let hidden_probs = self.hidden_given_visible(&visible)?;
109            let hidden_sample = self.sample_binary(&hidden_probs)?;
110
111            // Negative phase: run k steps of Gibbs sampling
112            let mut v_neg = visible.clone();
113            let mut h_neg = hidden_sample.clone();
114
115            for _ in 0..self.config.k_steps {
116                v_neg = self.visible_given_hidden(&h_neg)?;
117                h_neg = self.hidden_given_visible(&v_neg)?;
118            }
119
120            // Compute contrastive divergence gradient
121            let pos_grad = self.outer_product(&visible, &hidden_probs);
122            let neg_grad = self.outer_product(&v_neg, &h_neg);
123
124            // Update weights and biases
125            let grad = (pos_grad - neg_grad) / data.len() as f64;
126            self.weights = &self.weights + &(grad * self.config.learning_rate)
127                - &(&self.weights * self.config.l2_reg * self.config.learning_rate);
128
129            let visible_grad = &visible - &v_neg;
130            let hidden_grad = &hidden_probs - &h_neg;
131
132            self.visible_bias = &self.visible_bias + &(visible_grad * self.config.learning_rate);
133            self.hidden_bias = &self.hidden_bias + &(hidden_grad * self.config.learning_rate);
134
135            // Reconstruction error
136            let error = (&visible - &v_neg)
137                .iter()
138                .map(|x| x * x)
139                .sum::<f64>()
140                .sqrt();
141            total_error += error;
142        }
143
144        let avg_error = total_error / data.len() as f64;
145        self.history.push(avg_error);
146
147        Ok(avg_error)
148    }
149
150    /// Convert quantum state to classical probabilities
151    fn quantum_to_classical(&self, state: &Array1<Complex64>) -> QuantRS2Result<Array1<f64>> {
152        let dim = 1 << self.config.num_visible;
153
154        if state.len() != dim {
155            return Err(QuantRS2Error::InvalidInput(format!(
156                "State dimension {} doesn't match visible units 2^{}",
157                state.len(),
158                self.config.num_visible
159            )));
160        }
161
162        // Extract marginal probabilities for each qubit
163        let mut probs = Array1::zeros(self.config.num_visible);
164
165        for q in 0..self.config.num_visible {
166            let mut prob_one = 0.0;
167
168            for i in 0..dim {
169                let bit = (i >> q) & 1;
170                if bit == 1 {
171                    prob_one += state[i].norm_sqr();
172                }
173            }
174
175            probs[q] = prob_one;
176        }
177
178        Ok(probs)
179    }
180
181    /// Compute hidden probabilities given visible
182    fn hidden_given_visible(&self, visible: &Array1<f64>) -> QuantRS2Result<Array1<f64>> {
183        let mut hidden_probs = self.hidden_bias.clone();
184
185        for j in 0..self.config.num_hidden {
186            for i in 0..self.config.num_visible {
187                hidden_probs[j] += self.weights[[i, j]] * visible[i];
188            }
189            // Sigmoid activation
190            hidden_probs[j] = 1.0 / (1.0 + (-hidden_probs[j] / self.config.temperature).exp());
191        }
192
193        Ok(hidden_probs)
194    }
195
196    /// Compute visible probabilities given hidden
197    fn visible_given_hidden(&self, hidden: &Array1<f64>) -> QuantRS2Result<Array1<f64>> {
198        let mut visible_probs = self.visible_bias.clone();
199
200        for i in 0..self.config.num_visible {
201            for j in 0..self.config.num_hidden {
202                visible_probs[i] += self.weights[[i, j]] * hidden[j];
203            }
204            // Sigmoid activation
205            visible_probs[i] = 1.0 / (1.0 + (-visible_probs[i] / self.config.temperature).exp());
206        }
207
208        Ok(visible_probs)
209    }
210
211    /// Sample binary units from probabilities
212    fn sample_binary(&self, probs: &Array1<f64>) -> QuantRS2Result<Array1<f64>> {
213        let mut rng = thread_rng();
214        let mut samples = Array1::zeros(probs.len());
215
216        for i in 0..probs.len() {
217            samples[i] = if rng.gen::<f64>() < probs[i] {
218                1.0
219            } else {
220                0.0
221            };
222        }
223
224        Ok(samples)
225    }
226
227    /// Outer product of two vectors
228    fn outer_product(&self, a: &Array1<f64>, b: &Array1<f64>) -> Array2<f64> {
229        let mut result = Array2::zeros((a.len(), b.len()));
230
231        for i in 0..a.len() {
232            for j in 0..b.len() {
233                result[[i, j]] = a[i] * b[j];
234            }
235        }
236
237        result
238    }
239
240    /// Generate quantum sample from learned distribution
241    pub fn generate_sample(&self) -> QuantRS2Result<Array1<Complex64>> {
242        let mut rng = thread_rng();
243
244        // Start with random hidden state
245        let mut hidden = Array1::from_shape_fn(self.config.num_hidden, |_| {
246            if rng.gen::<f64>() < 0.5 {
247                0.0
248            } else {
249                1.0
250            }
251        });
252
253        // Run Gibbs sampling to equilibrate
254        for _ in 0..100 {
255            let visible = self.visible_given_hidden(&hidden)?;
256            hidden = self.hidden_given_visible(&visible)?;
257        }
258
259        // Final visible probabilities
260        let visible_probs = self.visible_given_hidden(&hidden)?;
261
262        // Convert to quantum state
263        self.classical_to_quantum(&visible_probs)
264    }
265
266    /// Convert classical probabilities to quantum state
267    fn classical_to_quantum(&self, probs: &Array1<f64>) -> QuantRS2Result<Array1<Complex64>> {
268        let dim = 1 << self.config.num_visible;
269        let mut state = Array1::zeros(dim);
270
271        // Create product state from marginals
272        for i in 0..dim {
273            let mut amplitude = 1.0;
274
275            for q in 0..self.config.num_visible {
276                let bit = (i >> q) & 1;
277                amplitude *= if bit == 1 {
278                    probs[q].sqrt()
279                } else {
280                    (1.0 - probs[q]).sqrt()
281                };
282            }
283
284            state[i] = Complex64::new(amplitude, 0.0);
285        }
286
287        // Normalize
288        let norm: f64 = state
289            .iter()
290            .map(|x: &Complex64| x.norm_sqr())
291            .sum::<f64>()
292            .sqrt();
293        for i in 0..dim {
294            state[i] = state[i] / norm;
295        }
296
297        Ok(state)
298    }
299
300    /// Compute free energy of visible configuration
301    pub fn free_energy(&self, visible: &Array1<f64>) -> QuantRS2Result<f64> {
302        let mut energy = 0.0;
303
304        // Visible bias term
305        for i in 0..self.config.num_visible {
306            energy -= self.visible_bias[i] * visible[i];
307        }
308
309        // Hidden layer contribution
310        for j in 0..self.config.num_hidden {
311            let mut h_input = self.hidden_bias[j];
312
313            for i in 0..self.config.num_visible {
314                h_input += self.weights[[i, j]] * visible[i];
315            }
316
317            energy -= h_input.exp().ln_1p();
318        }
319
320        Ok(energy)
321    }
322
323    /// Get training history
324    pub fn history(&self) -> &[f64] {
325        &self.history
326    }
327
328    /// Get weights
329    pub const fn weights(&self) -> &Array2<f64> {
330        &self.weights
331    }
332}
333
334/// Deep Quantum Boltzmann Machine (stacked RBMs)
335#[derive(Debug)]
336pub struct DeepQuantumBoltzmannMachine {
337    /// Layers of RBMs
338    layers: Vec<QuantumRBM>,
339    /// Layer configurations
340    layer_configs: Vec<QRBMConfig>,
341}
342
343impl DeepQuantumBoltzmannMachine {
344    /// Create new deep QBM
345    pub fn new(layer_configs: Vec<QRBMConfig>) -> Self {
346        let layers = layer_configs
347            .iter()
348            .map(|config| QuantumRBM::new(config.clone()))
349            .collect();
350
351        Self {
352            layers,
353            layer_configs,
354        }
355    }
356
357    /// Pretrain layers greedily
358    pub fn pretrain(
359        &mut self,
360        data: &[Array1<Complex64>],
361        epochs_per_layer: usize,
362    ) -> QuantRS2Result<Vec<Vec<f64>>> {
363        let mut all_histories = Vec::new();
364        let mut current_data = data.to_vec();
365        let num_layers = self.layers.len();
366
367        for layer_idx in 0..num_layers {
368            println!("Pretraining layer {layer_idx}...");
369            let mut layer_history = Vec::new();
370
371            for epoch in 0..epochs_per_layer {
372                let error = self.layers[layer_idx].train_batch(&current_data)?;
373                layer_history.push(error);
374
375                if epoch % 10 == 0 {
376                    println!("  Epoch {epoch}: Error = {error:.6}");
377                }
378            }
379
380            all_histories.push(layer_history);
381
382            // Transform data for next layer
383            if layer_idx < num_layers - 1 {
384                current_data =
385                    self.transform_to_next_layer(&current_data, &self.layers[layer_idx])?;
386            }
387        }
388
389        Ok(all_histories)
390    }
391
392    /// Transform data through a layer
393    fn transform_to_next_layer(
394        &self,
395        data: &[Array1<Complex64>],
396        layer: &QuantumRBM,
397    ) -> QuantRS2Result<Vec<Array1<Complex64>>> {
398        let mut transformed = Vec::new();
399
400        for state in data {
401            let visible = layer.quantum_to_classical(state)?;
402            let hidden_probs = layer.hidden_given_visible(&visible)?;
403
404            // Convert hidden probabilities to quantum state
405            transformed.push(layer.classical_to_quantum(&hidden_probs)?);
406        }
407
408        Ok(transformed)
409    }
410
411    /// Generate sample from deep model
412    pub fn generate(&self) -> QuantRS2Result<Array1<Complex64>> {
413        // Start from top layer
414        let mut sample = self
415            .layers
416            .last()
417            .ok_or_else(|| {
418                QuantRS2Error::RuntimeError(
419                    "No layers in deep quantum Boltzmann machine".to_string(),
420                )
421            })?
422            .generate_sample()?;
423
424        // Propagate down through layers
425        for layer in self.layers.iter().rev().skip(1) {
426            let hidden = layer.quantum_to_classical(&sample)?;
427            let visible_probs = layer.visible_given_hidden(&hidden)?;
428            sample = layer.classical_to_quantum(&visible_probs)?;
429        }
430
431        Ok(sample)
432    }
433}
434
435#[cfg(test)]
436mod tests {
437    use super::*;
438
439    #[test]
440    fn test_qrbm() {
441        let config = QRBMConfig {
442            num_visible: 2,
443            num_hidden: 2,
444            learning_rate: 0.01,
445            k_steps: 1,
446            temperature: 1.0,
447            l2_reg: 0.001,
448        };
449
450        let mut rbm = QuantumRBM::new(config);
451
452        // Create simple training data
453        let state = Array1::from_vec(vec![
454            Complex64::new(0.7, 0.0),
455            Complex64::new(0.3, 0.0),
456            Complex64::new(0.2, 0.0),
457            Complex64::new(0.6, 0.0),
458        ]);
459
460        let error = rbm
461            .train_batch(&[state])
462            .expect("Failed to train quantum RBM on batch");
463        assert!(error >= 0.0);
464    }
465
466    #[test]
467    fn test_deep_qbm() {
468        let layer1 = QRBMConfig {
469            num_visible: 2,
470            num_hidden: 2,
471            ..Default::default()
472        };
473
474        let layer2 = QRBMConfig {
475            num_visible: 2,
476            num_hidden: 1,
477            ..Default::default()
478        };
479
480        let dbm = DeepQuantumBoltzmannMachine::new(vec![layer1, layer2]);
481        assert_eq!(dbm.layers.len(), 2);
482    }
483}