1use crate::NeuralResult;
8use scirs2_core::ndarray::{Array2, Array3};
9use sklears_core::error::SklearsError;
10use sklears_core::types::FloatBounds;
11use std::collections::HashMap;
12use std::fs::File;
13use std::io::Write;
14
15#[derive(Debug, Clone)]
17pub struct VisualizationConfig {
18 pub output_dir: String,
20 pub format: ImageFormat,
22 pub color_scheme: ColorScheme,
24 pub dpi: u32,
26 pub show_layer_names: bool,
28 pub show_tensor_shapes: bool,
30}
31
32impl Default for VisualizationConfig {
33 fn default() -> Self {
34 Self {
35 output_dir: "./visualizations".to_string(),
36 format: ImageFormat::SVG,
37 color_scheme: ColorScheme::Default,
38 dpi: 300,
39 show_layer_names: true,
40 show_tensor_shapes: true,
41 }
42 }
43}
44
45#[derive(Debug, Clone, Copy, PartialEq)]
47pub enum ImageFormat {
48 SVG,
50 PNG,
52 HTML,
54}
55
56#[derive(Debug, Clone, Copy, PartialEq)]
58pub enum ColorScheme {
59 Default,
61 Viridis,
63 Plasma,
65 Grayscale,
67}
68
69pub struct ModelVisualizer {
71 config: VisualizationConfig,
72}
73
74impl ModelVisualizer {
75 pub fn new(config: VisualizationConfig) -> Self {
77 Self { config }
78 }
79
80 pub fn visualize_architecture(&self, layers: &[LayerInfo], filename: &str) -> NeuralResult<()> {
82 let output_path = format!(
83 "{}/{}.{}",
84 self.config.output_dir,
85 filename,
86 self.format_extension()
87 );
88
89 match self.config.format {
90 ImageFormat::SVG => self.generate_svg_architecture(layers, &output_path),
91 ImageFormat::HTML => self.generate_html_architecture(layers, &output_path),
92 ImageFormat::PNG => {
93 self.generate_svg_architecture(layers, &output_path.replace(".png", ".svg"))?;
95 println!("SVG generated. Use external tool to convert to PNG if needed.");
96 Ok(())
97 }
98 }
99 }
100
101 fn generate_svg_architecture(
103 &self,
104 layers: &[LayerInfo],
105 output_path: &str,
106 ) -> NeuralResult<()> {
107 let mut svg = String::new();
108
109 svg.push_str(&format!(
111 "<svg width=\"800\" height=\"{}\" xmlns=\"http://www.w3.org/2000/svg\">\n <defs>\n <style>\n .layer-box {{ fill: #e1f5fe; stroke: #0277bd; stroke-width: 2; }}\n .layer-text {{ font-family: Arial, sans-serif; font-size: 12px; text-anchor: middle; }}\n .layer-name {{ font-weight: bold; }}\n .layer-shape {{ font-size: 10px; fill: #666; }}\n .connection {{ stroke: #424242; stroke-width: 2; marker-end: url(#arrowhead); }}\n </style>\n <marker id=\"arrowhead\" markerWidth=\"10\" markerHeight=\"7\" \n refX=\"10\" refY=\"3.5\" orient=\"auto\">\n <polygon points=\"0,0 10,3.5 0,7\" fill=\"#424242\" />\n </marker>\n </defs>\n ",
112 layers.len() * 100 + 100
113 ));
114
115 for (i, layer) in layers.iter().enumerate() {
117 let y = i * 100 + 50;
118 let x = 400;
119
120 svg.push_str(&format!(
122 r#"<rect x="{}" y="{}" width="200" height="60" class="layer-box" />
123 "#,
124 x - 100,
125 y - 30
126 ));
127
128 if self.config.show_layer_names {
130 svg.push_str(&format!(
131 r#"<text x="{}" y="{}" class="layer-text layer-name">{}</text>
132 "#,
133 x,
134 y - 10,
135 layer.name
136 ));
137 }
138
139 svg.push_str(&format!(
141 r#"<text x="{}" y="{}" class="layer-text">{}</text>
142 "#,
143 x,
144 y + 5,
145 layer.layer_type
146 ));
147
148 if self.config.show_tensor_shapes {
150 svg.push_str(&format!(
151 r#"<text x="{}" y="{}" class="layer-text layer-shape">{:?}</text>
152 "#,
153 x,
154 y + 20,
155 layer.output_shape
156 ));
157 }
158
159 if i < layers.len() - 1 {
161 svg.push_str(&format!(
162 r#"<line x1="{}" y1="{}" x2="{}" y2="{}" class="connection" />
163 "#,
164 x,
165 y + 30,
166 x,
167 y + 70
168 ));
169 }
170 }
171
172 svg.push_str("</svg>");
173
174 let mut file = File::create(output_path)
176 .map_err(|e| SklearsError::InvalidInput(format!("Failed to create file: {}", e)))?;
177 file.write_all(svg.as_bytes())
178 .map_err(|e| SklearsError::InvalidInput(format!("Failed to write file: {}", e)))?;
179
180 Ok(())
181 }
182
183 fn generate_html_architecture(
185 &self,
186 layers: &[LayerInfo],
187 output_path: &str,
188 ) -> NeuralResult<()> {
189 let mut html = String::new();
190
191 html.push_str(
192 r#"
193 <!DOCTYPE html>
194 <html>
195 <head>
196 <title>Neural Network Architecture</title>
197 <style>
198 body { font-family: Arial, sans-serif; margin: 20px; }
199 .architecture { display: flex; flex-direction: column; align-items: center; }
200 .layer {
201 background: #e1f5fe;
202 border: 2px solid #0277bd;
203 border-radius: 8px;
204 padding: 15px;
205 margin: 10px;
206 min-width: 200px;
207 text-align: center;
208 transition: all 0.3s ease;
209 }
210 .layer:hover {
211 background: #b3e5fc;
212 transform: scale(1.05);
213 box-shadow: 0 4px 8px rgba(0,0,0,0.2);
214 }
215 .layer-name { font-weight: bold; font-size: 16px; color: #0277bd; }
216 .layer-type { font-size: 14px; color: #424242; margin: 5px 0; }
217 .layer-shape { font-size: 12px; color: #666; }
218 .arrow {
219 font-size: 24px;
220 color: #424242;
221 margin: 5px 0;
222 }
223 .layer-details {
224 display: none;
225 margin-top: 10px;
226 padding: 10px;
227 background: #f5f5f5;
228 border-radius: 4px;
229 font-size: 12px;
230 }
231 </style>
232 <script>
233 function toggleDetails(layerId) {
234 const details = document.getElementById(layerId);
235 details.style.display = details.style.display === 'none' ? 'block' : 'none';
236 }
237 </script>
238 </head>
239 <body>
240 <h1>Neural Network Architecture</h1>
241 <div class="architecture">
242 "#,
243 );
244
245 for (i, layer) in layers.iter().enumerate() {
246 html.push_str(&format!(
247 r#"
248 <div class="layer" onclick="toggleDetails('details_{}')">
249 <div class="layer-name">{}</div>
250 <div class="layer-type">{}</div>
251 <div class="layer-shape">Shape: {:?}</div>
252 <div id="details_{}" class="layer-details">
253 <strong>Parameters:</strong> {}<br>
254 <strong>Activation:</strong> {}<br>
255 <strong>Trainable:</strong> {}
256 </div>
257 </div>
258 "#,
259 i,
260 layer.name,
261 layer.layer_type,
262 layer.output_shape,
263 i,
264 layer.num_parameters,
265 layer.activation.as_deref().unwrap_or("None"),
266 layer.trainable
267 ));
268
269 if i < layers.len() - 1 {
270 html.push_str(r#"<div class="arrow">↓</div>"#);
271 }
272 }
273
274 html.push_str(
275 r#"
276 </div>
277 </body>
278 </html>
279 "#,
280 );
281
282 let mut file = File::create(output_path)
284 .map_err(|e| SklearsError::InvalidInput(format!("Failed to create file: {}", e)))?;
285 file.write_all(html.as_bytes())
286 .map_err(|e| SklearsError::InvalidInput(format!("Failed to write file: {}", e)))?;
287
288 Ok(())
289 }
290
291 fn format_extension(&self) -> &'static str {
293 match self.config.format {
294 ImageFormat::SVG => "svg",
295 ImageFormat::PNG => "png",
296 ImageFormat::HTML => "html",
297 }
298 }
299}
300
301#[derive(Debug, Clone)]
303pub struct LayerInfo {
304 pub name: String,
306 pub layer_type: String,
308 pub output_shape: Vec<usize>,
310 pub num_parameters: usize,
312 pub activation: Option<String>,
314 pub trainable: bool,
316}
317
318pub struct TrainingVisualizer {
320 config: VisualizationConfig,
321}
322
323impl TrainingVisualizer {
324 pub fn new(config: VisualizationConfig) -> Self {
326 Self { config }
327 }
328
329 pub fn plot_training_history(
331 &self,
332 metrics: &TrainingMetrics,
333 filename: &str,
334 ) -> NeuralResult<()> {
335 let output_path = format!("{}/{}.html", self.config.output_dir, filename);
336
337 let mut html = String::new();
338 html.push_str(
339 r#"
340 <!DOCTYPE html>
341 <html>
342 <head>
343 <title>Training History</title>
344 <script src="https://cdn.plot.ly/plotly-latest.min.js"></script>
345 <style>
346 body { font-family: Arial, sans-serif; margin: 20px; }
347 .plot-container { width: 100%; height: 400px; margin: 20px 0; }
348 </style>
349 </head>
350 <body>
351 <h1>Training History</h1>
352 "#,
353 );
354
355 html.push_str(r#"<div id="loss-plot" class="plot-container"></div>"#);
357 html.push_str(&format!(
358 r#"
359 <script>
360 var lossData = [{{
361 x: [{}],
362 y: [{}],
363 type: 'scatter',
364 mode: 'lines',
365 name: 'Training Loss',
366 line: {{color: '#1f77b4'}}
367 }}
368 "#,
369 (0..metrics.train_loss.len())
370 .map(|i| i.to_string())
371 .collect::<Vec<_>>()
372 .join(","),
373 metrics
374 .train_loss
375 .iter()
376 .map(|x| x.to_string())
377 .collect::<Vec<_>>()
378 .join(",")
379 ));
380
381 if !metrics.val_loss.is_empty() {
382 html.push_str(&format!(
383 r#",{{
384 x: [{}],
385 y: [{}],
386 type: 'scatter',
387 mode: 'lines',
388 name: 'Validation Loss',
389 line: {{color: '#ff7f0e'}}
390 }}"#,
391 (0..metrics.val_loss.len())
392 .map(|i| i.to_string())
393 .collect::<Vec<_>>()
394 .join(","),
395 metrics
396 .val_loss
397 .iter()
398 .map(|x| x.to_string())
399 .collect::<Vec<_>>()
400 .join(",")
401 ));
402 }
403
404 html.push_str(
405 r#"];
406 var lossLayout = {
407 title: 'Training Loss',
408 xaxis: { title: 'Epoch' },
409 yaxis: { title: 'Loss' }
410 };
411 Plotly.newPlot('loss-plot', lossData, lossLayout);
412 </script>
413 "#,
414 );
415
416 if !metrics.train_accuracy.is_empty() {
418 html.push_str(r#"<div id="accuracy-plot" class="plot-container"></div>"#);
419 html.push_str(&format!(
420 r#"
421 <script>
422 var accuracyData = [{{
423 x: [{}],
424 y: [{}],
425 type: 'scatter',
426 mode: 'lines',
427 name: 'Training Accuracy',
428 line: {{color: '#2ca02c'}}
429 }}
430 "#,
431 (0..metrics.train_accuracy.len())
432 .map(|i| i.to_string())
433 .collect::<Vec<_>>()
434 .join(","),
435 metrics
436 .train_accuracy
437 .iter()
438 .map(|x| x.to_string())
439 .collect::<Vec<_>>()
440 .join(",")
441 ));
442
443 if !metrics.val_accuracy.is_empty() {
444 html.push_str(&format!(
445 r#",{{
446 x: [{}],
447 y: [{}],
448 type: 'scatter',
449 mode: 'lines',
450 name: 'Validation Accuracy',
451 line: {{color: '#d62728'}}
452 }}"#,
453 (0..metrics.val_accuracy.len())
454 .map(|i| i.to_string())
455 .collect::<Vec<_>>()
456 .join(","),
457 metrics
458 .val_accuracy
459 .iter()
460 .map(|x| x.to_string())
461 .collect::<Vec<_>>()
462 .join(",")
463 ));
464 }
465
466 html.push_str(
467 r#"];
468 var accuracyLayout = {
469 title: 'Training Accuracy',
470 xaxis: { title: 'Epoch' },
471 yaxis: { title: 'Accuracy' }
472 };
473 Plotly.newPlot('accuracy-plot', accuracyData, accuracyLayout);
474 </script>
475 "#,
476 );
477 }
478
479 html.push_str(
480 r#"
481 </body>
482 </html>
483 "#,
484 );
485
486 let mut file = File::create(&output_path)
488 .map_err(|e| SklearsError::InvalidInput(format!("Failed to create file: {}", e)))?;
489 file.write_all(html.as_bytes())
490 .map_err(|e| SklearsError::InvalidInput(format!("Failed to write file: {}", e)))?;
491
492 println!("Training history saved to: {}", output_path);
493 Ok(())
494 }
495}
496
497#[derive(Debug, Clone, Default)]
499pub struct TrainingMetrics {
500 pub train_loss: Vec<f64>,
502 pub val_loss: Vec<f64>,
504 pub train_accuracy: Vec<f64>,
506 pub val_accuracy: Vec<f64>,
508 pub learning_rates: Vec<f64>,
510}
511
512pub struct AttentionVisualizer {
514 config: VisualizationConfig,
515}
516
517impl AttentionVisualizer {
518 pub fn new(config: VisualizationConfig) -> Self {
520 Self { config }
521 }
522
523 pub fn visualize_attention_weights<T: FloatBounds>(
525 &self,
526 attention_weights: &Array3<T>,
527 tokens: &[String],
528 filename: &str,
529 ) -> NeuralResult<()> {
530 let output_path = format!("{}/{}.html", self.config.output_dir, filename);
531
532 let mut html = String::new();
533 html.push_str(
534 r#"
535 <!DOCTYPE html>
536 <html>
537 <head>
538 <title>Attention Heatmap</title>
539 <script src="https://cdn.plot.ly/plotly-latest.min.js"></script>
540 <style>
541 body { font-family: Arial, sans-serif; margin: 20px; }
542 .heatmap-container { width: 100%; height: 600px; margin: 20px 0; }
543 </style>
544 </head>
545 <body>
546 <h1>Attention Weights Heatmap</h1>
547 <div id="heatmap" class="heatmap-container"></div>
548 <script>
549 "#,
550 );
551
552 let (batch_size, seq_len, _) = attention_weights.dim();
554 if batch_size > 0 && seq_len > 0 {
555 let mut weights_js = String::new();
557 weights_js.push('[');
558 for i in 0..seq_len {
559 weights_js.push('[');
560 for j in 0..seq_len {
561 if j > 0 {
562 weights_js.push(',');
563 }
564 weights_js.push_str(
565 &attention_weights[[0, i, j]]
566 .to_f64()
567 .unwrap_or(0.0)
568 .to_string(),
569 );
570 }
571 weights_js.push(']');
572 if i < seq_len - 1 {
573 weights_js.push(',');
574 }
575 }
576 weights_js.push(']');
577
578 let tokens_js = format!("[\"{}\"]", tokens.join("\",\""));
580
581 html.push_str(&format!(
582 r#"
583 var data = [{{
584 z: {},
585 x: {},
586 y: {},
587 type: 'heatmap',
588 colorscale: 'Viridis'
589 }}];
590
591 var layout = {{
592 title: 'Attention Weights',
593 xaxis: {{ title: 'Key Tokens' }},
594 yaxis: {{ title: 'Query Tokens' }}
595 }};
596
597 Plotly.newPlot('heatmap', data, layout);
598 "#,
599 weights_js, tokens_js, tokens_js
600 ));
601 }
602
603 html.push_str(
604 r#"
605 </script>
606 </body>
607 </html>
608 "#,
609 );
610
611 let mut file = File::create(&output_path)
613 .map_err(|e| SklearsError::InvalidInput(format!("Failed to create file: {}", e)))?;
614 file.write_all(html.as_bytes())
615 .map_err(|e| SklearsError::InvalidInput(format!("Failed to write file: {}", e)))?;
616
617 println!("Attention heatmap saved to: {}", output_path);
618 Ok(())
619 }
620}
621
622pub struct WeightVisualizer {
624 config: VisualizationConfig,
625}
626
627impl WeightVisualizer {
628 pub fn new(config: VisualizationConfig) -> Self {
630 Self { config }
631 }
632
633 pub fn visualize_weight_distributions<T: FloatBounds>(
635 &self,
636 weights: &HashMap<String, Array2<T>>,
637 filename: &str,
638 ) -> NeuralResult<()> {
639 let output_path = format!("{}/{}.html", self.config.output_dir, filename);
640
641 let mut html = String::new();
642 html.push_str(
643 r#"
644 <!DOCTYPE html>
645 <html>
646 <head>
647 <title>Weight Distributions</title>
648 <script src="https://cdn.plot.ly/plotly-latest.min.js"></script>
649 <style>
650 body { font-family: Arial, sans-serif; margin: 20px; }
651 .histogram-container { width: 100%; height: 400px; margin: 20px 0; }
652 </style>
653 </head>
654 <body>
655 <h1>Weight Distributions</h1>
656 "#,
657 );
658
659 for (layer_name, weight_matrix) in weights.iter() {
660 let div_id = format!("histogram-{}", layer_name.replace(".", "-"));
661 html.push_str(&format!(r#"<h2>{}</h2>"#, layer_name));
662 html.push_str(&format!(
663 r#"<div id="{}" class="histogram-container"></div>"#,
664 div_id
665 ));
666
667 let flattened: Vec<f64> = weight_matrix
669 .iter()
670 .map(|&w| w.to_f64().unwrap_or(0.0))
671 .collect();
672
673 let weights_js = format!(
674 "[{}]",
675 flattened
676 .iter()
677 .map(|x| x.to_string())
678 .collect::<Vec<_>>()
679 .join(",")
680 );
681
682 html.push_str(&format!(
683 r#"
684 <script>
685 var data_{} = [{{
686 x: {},
687 type: 'histogram',
688 nbinsx: 50,
689 name: '{}'
690 }}];
691
692 var layout_{} = {{
693 title: '{} Weight Distribution',
694 xaxis: {{ title: 'Weight Value' }},
695 yaxis: {{ title: 'Frequency' }}
696 }};
697
698 Plotly.newPlot('{}', data_{}, layout_{});
699 </script>
700 "#,
701 div_id, weights_js, layer_name, div_id, layer_name, div_id, div_id, div_id
702 ));
703 }
704
705 html.push_str(
706 r#"
707 </body>
708 </html>
709 "#,
710 );
711
712 let mut file = File::create(&output_path)
714 .map_err(|e| SklearsError::InvalidInput(format!("Failed to create file: {}", e)))?;
715 file.write_all(html.as_bytes())
716 .map_err(|e| SklearsError::InvalidInput(format!("Failed to write file: {}", e)))?;
717
718 println!("Weight distributions saved to: {}", output_path);
719 Ok(())
720 }
721}
722
723pub fn ensure_output_directory(path: &str) -> NeuralResult<()> {
725 std::fs::create_dir_all(path)
726 .map_err(|e| SklearsError::InvalidInput(format!("Failed to create directory: {}", e)))
727}
728
729#[allow(non_snake_case)]
730#[cfg(test)]
731mod tests {
732 use super::*;
733
734 #[test]
735 fn test_visualization_config_default() {
736 let config = VisualizationConfig::default();
737 assert_eq!(config.format, ImageFormat::SVG);
738 assert_eq!(config.color_scheme, ColorScheme::Default);
739 assert_eq!(config.dpi, 300);
740 }
741
742 #[test]
743 fn test_layer_info_creation() {
744 let layer_info = LayerInfo {
745 name: "dense_1".to_string(),
746 layer_type: "Dense".to_string(),
747 output_shape: vec![128, 64],
748 num_parameters: 8256,
749 activation: Some("ReLU".to_string()),
750 trainable: true,
751 };
752
753 assert_eq!(layer_info.name, "dense_1");
754 assert_eq!(layer_info.num_parameters, 8256);
755 }
756
757 #[test]
758 fn test_training_metrics_default() {
759 let metrics = TrainingMetrics::default();
760 assert!(metrics.train_loss.is_empty());
761 assert!(metrics.val_loss.is_empty());
762 assert!(metrics.train_accuracy.is_empty());
763 }
764}