📄 two_moons.rs
/home/palash/git/build-your-own-nn/src/examples/two_moons.rs
Language: rs • Lines: 296
use crate::{
    Layer, Rng,
    activation::{Activation, ActivationType},
    image_utils::{PlotColor, Trace, render_dual_plots},
    linear::Linear,
    loss::bce_sigmoid_delta,
    tensor::{Tensor, TensorError},
};
use std::{thread, time::Duration};

/// Generates the Two Moons dataset
pub fn generate_two_moons(samples: usize) -> (Tensor, Tensor) {
    let mut inputs = Vec::with_capacity(samples * 3); // x, y, bias
    let mut targets = Vec::with_capacity(samples);

    for i in 0..samples {
        let n = i as f32 / (samples as f32 / 2.0);
        if i < samples / 2 {
            // Top Moon
            let ra = n * std::f32::consts::PI;
            let x = ra.cos();
            let y = ra.sin();
            inputs.extend_from_slice(&[x, y, 1.0]);
            targets.push(0.0);
        } else {
            // Bottom Moon
            let ra = (n - 1.0) * std::f32::consts::PI;
            let x = 1.0 - ra.cos();
            let y = 0.5 - ra.sin();
            inputs.extend_from_slice(&[x, y, 1.0]);
            targets.push(1.0);
        }
    }

    // Convert to Tensors (using your existing Tensor implementation)
    let x_tensor = Tensor::new(inputs, vec![samples, 3]).unwrap();
    let y_tensor = Tensor::new(targets, vec![samples, 1]).unwrap();
    (x_tensor, y_tensor)
}

pub fn two_moons_neural_network(rng: &mut dyn Rng) -> Result<(), TensorError> {
    // 1. Setup Architecture: 3 Inputs (x, y, bias) -> 9 Hidden -> 1 Output
    let mut l1 = Linear::new(3, 9, rng);
    let mut a1 = Activation::new(ActivationType::Sigmoid);
    let mut l2 = Linear::new(9, 1, rng);
    let mut a2 = Activation::new(ActivationType::Sigmoid);

    // 2. Generate Data
    let (input, actual) = generate_two_moons(100);
    let learning_rate = 0.08;

    // Bounds adjusted for Two Moons coordinates
    let bounds = Some((-1.5, 2.5, -1.0, 1.5));

    for epoch in 0..100_000 {
        // Forward & Backward pass
        let z1 = l1.forward(&input)?;
        let h1 = a1.forward(&z1)?;
        let z2 = l2.forward(&h1)?;
        let pred = a2.forward(&z2)?;

        let d_z2 = bce_sigmoid_delta(&pred, &actual)?;
        let d_h1 = l2.backward(&d_z2, learning_rate)?;
        let d_z1 = a1.backward(&d_h1, learning_rate)?;
        let _ = l1.backward(&d_z1, learning_rate)?;

        if epoch % 500 == 0 {
            let mut traces = Vec::new();
            let (mut cx, mut cy, mut mx, mut my) = (vec![], vec![], vec![], vec![]);

            // 3. Generate Decision Boundary "Heatmap"
            for gx in 0..=30 {
                for gy in 0..=20 {
                    let x = -1.5 + (gx as f32 / 30.0) * 4.0;
                    let y = -1.0 + (gy as f32 / 20.0) * 2.5;

                    let test_in = Tensor::new(vec![x, y, 1.0], vec![1, 3])?;
                    let p_out = a2.forward(&l2.forward(&a1.forward(&l1.forward(&test_in)?)?)?)?;

                    if p_out.data()[0] > 0.5 {
                        cx.push(x);
                        cy.push(y);
                    } else {
                        mx.push(x);
                        my.push(y);
                    }
                }
            }

            // Add Heatmap Traces
            traces.push(Trace {
                name: "Class 1 Area".into(),
                x: cx,
                y: cy,
                color: PlotColor::Cyan,
                is_line: false,
                hide_axes: false,
            });
            traces.push(Trace {
                name: "Class 0 Area".into(),
                x: mx,
                y: my,
                color: PlotColor::Magenta,
                is_line: false,
                hide_axes: false,
            });

            // 4. Add Actual Data Points
            for i in 0..actual.data().len() {
                let color = if actual.data()[i] > 0.5 {
                    PlotColor::Green
                } else {
                    PlotColor::Red
                };
                traces.push(Trace {
                    name: "".into(),
                    x: vec![input.data()[i * 3]],
                    y: vec![input.data()[i * 3 + 1]],
                    color,
                    is_line: false,
                    hide_axes: false,
                });
            }

            render_dual_plots(
                &visualize_topology(l1.weight(), l2.weight(), -1.0, 1.5), // Note: Update visualize_topology for new layer sizes!
                &traces,
                100,
                30,
                bounds,
                format!("Two Moons Training - Epoch {}", epoch),
            );

            let weight_display = format_weights_side_by_side(l1.weight(), l2.weight());
            println!("{}", weight_display);

            thread::sleep(Duration::from_millis(50));
        }
    }
    Ok(())
}

pub fn visualize_topology(
    l1_weights: &Tensor, 
    l2_weights: &Tensor, 
    y_min: f32, 
    y_max: f32
) -> Vec<Trace> {
    let mut traces = Vec::new();

    let input_count = l1_weights.shape()[0];
    let hidden_count = l1_weights.shape()[1];
    let output_count = l2_weights.shape()[1];
    let layers = [input_count, hidden_count, output_count];

    let mut node_coords = Vec::new();
    
    let height = y_max - y_min;
    let padding = height * 0.1;
    let effective_min = y_min + padding;
    let effective_max = y_max - padding;
    let effective_height = effective_max - effective_min;

    for (l_idx, &count) in layers.iter().enumerate() {
        let mut layer_nodes = Vec::new();
        let x = -1.0 + (l_idx as f32 / (layers.len() - 1) as f32) * 3.0; 
        
        for i in 0..count {
            let y = if count > 1 {
                if l_idx == 0 {
                    // SHRINK LOGIC FOR INPUT LAYER
                    // We take the center point and spread nodes by only 40% of the total height
                    let center = y_min + (height / 2.0);
                    let shrink_factor = 0.4; 
                    let start = center - (effective_height * shrink_factor / 2.0);
                    start + (i as f32 / (count - 1) as f32) * (effective_height * shrink_factor)
                } else {
                    // FULL SPREAD FOR HIDDEN LAYER
                    effective_min + (i as f32 / (count - 1) as f32) * effective_height
                }
            } else {
                y_min + (height / 2.0) // Output layer
            };
            layer_nodes.push((x, y));
        }
        node_coords.push(layer_nodes);
    }

    // Drawing logic for lines (Input -> Hidden)
    let w1 = l1_weights.data();
    for i in 0..input_count {
        for j in 0..hidden_count {
            let weight = w1[i * hidden_count + j];
            traces.push(Trace {
                name: "".into(),
                x: vec![node_coords[0][i].0, node_coords[1][j].0],
                y: vec![node_coords[0][i].1, node_coords[1][j].1],
                color: if weight > 0.0 { PlotColor::Green } else { PlotColor::Red },
                is_line: true,
                hide_axes: true,
            });
        }
    }

    // Drawing logic for lines (Hidden -> Output)
    let w2 = l2_weights.data();
    for i in 0..hidden_count {
        for j in 0..output_count {
            let weight = w2[i * output_count + j];
            traces.push(Trace {
                name: "".into(),
                x: vec![node_coords[1][i].0, node_coords[2][j].0],
                y: vec![node_coords[1][i].1, node_coords[2][j].1],
                color: if weight > 0.0 { PlotColor::Cyan } else { PlotColor::Magenta },
                is_line: true,
                hide_axes: true,
            });
        }
    }

    // Drawing Nodes
    for (l_idx, layer) in node_coords.iter().enumerate() {
        for (i, &(x, y)) in layer.iter().enumerate() {
            let color = match l_idx {
                0 => PlotColor::Magenta, 
                1 => PlotColor::Blue,    
                _ => PlotColor::White,   
            };
            traces.push(Trace {
                name: format!("L{}N{}", l_idx, i),
                x: vec![x],
                y: vec![y],
                color,
                is_line: false,
                hide_axes: true,
            });
        }
    }
    traces
}


pub fn format_weights_side_by_side(l1_weights: &Tensor, l2_weights: &Tensor) -> String {
    let mut output = String::new();

    let w1 = l1_weights.data();
    let s1 = l1_weights.shape();
    let w2 = l2_weights.data();
    let s2 = l2_weights.shape();

    let max_rows = s1[0].max(s2[0]);

    // Import or use the ANSI codes from your PlotColor enum
    let green = "\x1b[32m";
    let red = "\x1b[31m";
    let reset = "\x1b[0m";

    output.push_str("\n    L1 Weights (Input -> Hidden)         L2 Weights (Hidden -> Output)\n");
    output.push_str("    ----------------------------         -----------------------------\n");

    for r in 0..max_rows {
        let mut row_str = String::from("    ");

        // Format L1 Row
        if r < s1[0] {
            row_str.push_str("[ ");
            for c in 0..s1[1] {
                let val = w1[r * s1[1] + c];
                let color = if val >= 0.0 { green } else { red };
                row_str.push_str(&format!("{}{:>7.3}{} ", color, val, reset));
            }
            row_str.push_str("]");
        } else {
            // Adjust spacing for ANSI codes (which don't take up visual space)
            row_str.push_str(&" ".repeat(s1[1] * 8 + 3));
        }

        row_str.push_str("         ");

        // Format L2 Row
        if r < s2[0] {
            row_str.push_str("[ ");
            for c in 0..s2[1] {
                let val = w2[r * s2[1] + c];
                let color = if val >= 0.0 { green } else { red };
                row_str.push_str(&format!("{}{:>7.3}{} ", color, val, reset));
            }
            row_str.push_str("]");
        }

        output.push_str(&row_str);
        output.push('\n');
    }

    output
}