quantrs2_core/qml/
quantum_boltzmann.rs1use 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#[derive(Debug, Clone)]
34pub struct QRBMConfig {
35 pub num_visible: usize,
37 pub num_hidden: usize,
39 pub learning_rate: f64,
41 pub k_steps: usize,
43 pub temperature: f64,
45 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#[derive(Debug, Clone)]
64pub struct QuantumRBM {
65 config: QRBMConfig,
67 weights: Array2<f64>,
69 visible_bias: Array1<f64>,
71 hidden_bias: Array1<f64>,
73 history: Vec<f64>,
75}
76
77impl QuantumRBM {
78 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 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 let visible = self.quantum_to_classical(state)?;
106
107 let hidden_probs = self.hidden_given_visible(&visible)?;
109 let hidden_sample = self.sample_binary(&hidden_probs)?;
110
111 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 let pos_grad = self.outer_product(&visible, &hidden_probs);
122 let neg_grad = self.outer_product(&v_neg, &h_neg);
123
124 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 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 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 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 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 hidden_probs[j] = 1.0 / (1.0 + (-hidden_probs[j] / self.config.temperature).exp());
191 }
192
193 Ok(hidden_probs)
194 }
195
196 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 visible_probs[i] = 1.0 / (1.0 + (-visible_probs[i] / self.config.temperature).exp());
206 }
207
208 Ok(visible_probs)
209 }
210
211 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 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 pub fn generate_sample(&self) -> QuantRS2Result<Array1<Complex64>> {
242 let mut rng = thread_rng();
243
244 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 for _ in 0..100 {
255 let visible = self.visible_given_hidden(&hidden)?;
256 hidden = self.hidden_given_visible(&visible)?;
257 }
258
259 let visible_probs = self.visible_given_hidden(&hidden)?;
261
262 self.classical_to_quantum(&visible_probs)
264 }
265
266 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 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 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 pub fn free_energy(&self, visible: &Array1<f64>) -> QuantRS2Result<f64> {
302 let mut energy = 0.0;
303
304 for i in 0..self.config.num_visible {
306 energy -= self.visible_bias[i] * visible[i];
307 }
308
309 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 pub fn history(&self) -> &[f64] {
325 &self.history
326 }
327
328 pub const fn weights(&self) -> &Array2<f64> {
330 &self.weights
331 }
332}
333
334#[derive(Debug)]
336pub struct DeepQuantumBoltzmannMachine {
337 layers: Vec<QuantumRBM>,
339 layer_configs: Vec<QRBMConfig>,
341}
342
343impl DeepQuantumBoltzmannMachine {
344 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 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(¤t_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 if layer_idx < num_layers - 1 {
384 current_data =
385 self.transform_to_next_layer(¤t_data, &self.layers[layer_idx])?;
386 }
387 }
388
389 Ok(all_histories)
390 }
391
392 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 transformed.push(layer.classical_to_quantum(&hidden_probs)?);
406 }
407
408 Ok(transformed)
409 }
410
411 pub fn generate(&self) -> QuantRS2Result<Array1<Complex64>> {
413 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 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 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}