📄 linear-regression.html
/home/palash/git/build-your-own-nn/visualizers/linear-regression.html
Language: html • Lines: 464
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <title>Linear Regression Visualizer | Interactive Machine Learning Lab - Palash Kanti Kundu</title>
    <meta name="title" content="Linear Regression Visualizer | Interactive Machine Learning Lab">
    <meta name="description"
        content="A real-time, from-scratch Linear Regression engine. Build architectures, tune hyperparameters, and visualize weight traces and decision boundaries in your browser.">
    <meta name="keywords"
        content="Linear Regression, Machine Learning, JavaScript, AI Visualization, Deep Learning Tutorial, XOR Problem, Backpropagation Visualizer">

    <meta property="og:type" content="website">
    <meta property="og:url" content="https://palashkantikundu.in">
    <meta property="og:title" content="Linear Regression Demystified: Interactive Trace Engine">
    <meta property="og:description"
        content="Explore the math behind AI. Watch synapses fire and weights adjust in real-time with this browser-based Linear Regression architect.">

    <meta property="twitter:card" content="summary_large_image">
    <meta property="twitter:title" content="Linear Regression Demystified: Interactive Trace Engine">
    <meta property="twitter:description"
        content="Real-time Linear Regression training and weight tracing in pure JavaScript. Build, train, and demystify AI architecture.">
    <script src="https://cdn.jsdelivr.net/npm/plotly.js-dist@3.3.1/plotly.min.js"></script>
    <style>
        body {
            font-family: 'Courier New', Courier, monospace;
            display: flex;
            flex-direction: column;
            align-items: center;
            background: #1a1a1a;
            color: #00ff00;
            margin: 0;
            padding: 20px;
        }

        .main-container {
            display: flex;
            flex-direction: row;
            /* Horizontal layout for the two columns */
            gap: 20px;
            width: 100%;
            max-width: 1200px;
            align-items: flex-start;
        }

        .charts-column {
            display: flex;
            flex-direction: column;
            gap: 20px;
            flex: 1.5;
            /* Takes up more width than the internals panel */
            min-width: 0;
        }

        .charts-stack {
            display: flex;
            flex-direction: column;
            gap: 20px;
            flex: 2;
            /* Takes up more space compared to the viz panel */
        }

        .panel {
            background: #2d2d2d;
            border-radius: 8px;
            border: 1px solid #444;
            width: 100%;
            /* Ensure panels take full width of container */
            box-sizing: border-box;
            overflow: hidden;
        }

        .panel {
            background: #2d2d2d;
            border-radius: 8px;
            border: 1px solid #444;
            width: 100%;
            box-sizing: border-box;
        }


        #chart {
            height: 450px;
            /* Specific height for main chart */
        }

        #lossChart {
            height: 450px;
            /* Specific height for loss chart */
        }

        #chart,
        #lossChart {
            height: 500px;
            width: 100%;
            /* Force 80% width */
            margin: 0 auto;
            /* Center within the .charts-column */
            box-sizing: border-box;
        }

        .layer-viz {
            flex: 1;
            /* Narrower than the charts column */
            display: flex;
            flex-direction: column;
            gap: 10px;
            font-size: 0.85em;
        }

        .matrix-box {
            border: 1px solid #00ff00;
            padding: 10px;
            margin: 5px 0;
            background: #000;
            position: relative;
        }

        .matrix-label {
            position: absolute;
            top: -10px;
            left: 10px;
            background: #2d2d2d;
            padding: 0 5px;
            font-size: 0.7em;
            color: #aaa;
        }

        .controls {
            width: 100%;
            max-width: 1160px;
            display: grid;
            grid-template-columns: repeat(4, 1fr);
            gap: 10px;
            margin-top: 20px;
        }

        input,
        button {
            background: #333;
            color: #00ff00;
            border: 1px solid #00ff00;
            padding: 10px;
        }

        button {
            cursor: pointer;
            font-weight: bold;
        }

        #trainBtn {
            background: #004400;
        }

        #stopBtn {
            background: #440000;
            color: #ff5555;
            border-color: #ff5555;
            display: none;
        }

        #resetBtn {
            background: #444;
            color: #fff;
            border-color: #888;
        }

        button:hover {
            background: #00ff00;
            color: #000;
        }

        .highlight {
            color: #fff;
            font-weight: bold;
            text-shadow: 0 0 5px #00ff00;
        }

        label {
            font-size: 0.8em;
            color: #ccc;
        }

        .status-tag {
            font-size: 0.7em;
            padding: 2px 5px;
            border-radius: 3px;
            float: right;
            background: #00ff00;
            color: #000;
        }
    </style>
</head>

<body>

    <h1>Linear Layer: Internal Trace &nbsp; &nbsp; &nbsp;<span id="status" class="status-tag"
            style="display:none">TRAINING...</span></h1>
    <p>This tool accompanies the learning guide: <a href="/#linear-regression">Linear Regression</a></p>

    <div class="main-container">
        <div class="charts-column">
            <div id="chart" class="panel"></div>
            <div id="lossChart" class="panel"></div>
        </div>

        <div class="layer-viz panel">
            <h3>Linear Layer Internals</h3>
            <div class="matrix-box">
                <span class="matrix-label">Input X (with Bias Trick)</span>
                <div id="matrixX"></div>
            </div>
            <div style="text-align: center; font-size: 1.5em;">×</div>
            <div class="matrix-box">
                <span class="matrix-label">Model Weights W (m and c)</span>
                <div id="matrixW" class="highlight"></div>
            </div>
            <div style="text-align: center; font-size: 1.5em;">=</div>
            <div class="matrix-box">
                <span class="matrix-label">Output (Forward Pass)</span>
                <div id="matrixOut"></div>
            </div>
            <hr style="width: 100%; border-color: #444;">
            <div class="matrix-box">
                <span class="matrix-label">True Weights W (m and c)</span>
                <div id="trueWeights" class="highlight"></div>
            </div>
            <hr style="width: 100%; border-color: #444;">
            <div id="traceLog">
                <strong>Computation Trace:</strong><br>
                1. $X \cdot W$ (Matmul) ✅<br>
                2. Current Epoch: <span id="currentEpoch" class="highlight">0.00</span><br>
                2. MSE Loss: <span id="valLoss" class="highlight">0.00</span><br>
                3. Gradient: <span id="valGrad" class="highlight">m:0, c:0</span><br>
                4. $W = W - \eta \cdot \nabla L$ ✅
            </div>
        </div>
    </div>
    <div class="controls panel">
        <div style="display:flex; flex-direction:column">
            <label>Data X</label>
            <input type="text" id="inputX" value="1, 2, 3, 4, 5">
        </div>
        <div style="display:flex; flex-direction:column">
            <label>Data Y</label>
            <input type="text" id="inputY" value="5.6, 6.6, 9.5, 10.2, 14">
        </div>
        <div style="display:flex; flex-direction:column">
            <label>Epochs, learning Rate & Delay(ms)</label>
            <div style="display:flex; gap:5px">
                <input type="number" id="epochsInput" value="100" style="width:50%">
                <input type="number" id="learningRate" value="0.001" style="width:25%" step="0.001">
                <input type="number" id="speedInput" value="50" style="width:25%">
            </div>
        </div>
        <div style="display:flex; flex-direction:column; gap: 5px;">
            <button id="randomDataBtn" style="background: #444; color: #00ff00;">RANDOMIZE DATA</button>
            <button id="trainBtn">START TRAINING</button>
            <button id="stopBtn">STOP</button>
            <button id="resetBtn">RESET WEIGHTS</button>
        </div>
    </div>

    <script>
        let m, c;
        let isTraining = false;
        let stopFlag = false;

        function getInputs() {
            const x = document.getElementById('inputX').value.split(',').map(v => v.trim()).filter(v => v !== "").map(Number);
            const y = document.getElementById('inputY').value.split(',').map(v => v.trim()).filter(v => v !== "").map(Number);
            return { x, y };
        }

        // Updated internal viz to handle cases where math might break
        function updateInternalViz(x, predictions, loss, m_grad, c_grad, currentEpoch) {
            document.getElementById('matrixX').innerHTML = x.slice(0, 5).map(val => `| ${val.toFixed(1)}, 1.0 |`).join('<br>') + (x.length > 5 ? '<br>...' : '');
            document.getElementById('matrixW').innerHTML = `| ${Number(m).toFixed(4)} | (m)<br>| ${Number(c).toFixed(4)} | (c)`;
            document.getElementById('matrixOut').innerHTML = predictions.slice(0, 5).map(val => `| ${val.toFixed(2)} |`).join('<br>') + (predictions.length > 5 ? '<br>...' : '');

            document.getElementById('currentEpoch').innerText = currentEpoch;
            document.getElementById('valLoss').innerText = isNaN(loss) ? "EXPLODED" : loss.toFixed(6);
            document.getElementById('valGrad').innerText = `m:${m_grad.toFixed(4)}, c:${c_grad.toFixed(4)}`;
        }

        function updateChart() {
            const { x: xInput, y: yInput } = getInputs();
            if (xInput.length === 0 || yInput.length === 0) return;

            const minX = Math.min(...xInput);
            const maxX = Math.max(...xInput);
            const xRange = maxX - minX || 2;
            const xPad = xRange * 0.2;

            const xLine = [minX - xPad, maxX + xPad];
            const yLine = xLine.map(val => (m * val) + c);

            const allVisibleY = [...yInput, ...yLine].filter(v => !isNaN(v));
            const minY = Math.min(...allVisibleY);
            const maxY = Math.max(...allVisibleY);
            const yRange = (maxY - minY) || 10;
            const yPad = yRange * 0.2;

            const data = [
                { x: xInput, y: yInput, mode: 'markers', name: 'Data', marker: { color: '#00ff00', size: 10 } },
                { x: xLine, y: yLine, mode: 'lines', name: 'Model', line: { color: '#ff00ff', width: 3 } }
            ];

            const layout = {
                autosize: true,
                title: { text: 'Data vs Model', font: { color: '#aaa', size: 12 } },
                paper_bgcolor: '#1a1a1a', plot_bgcolor: '#1a1a1a', font: { color: '#00ff00' },
                xaxis: { gridcolor: '#333', range: [xLine[0], xLine[1]], zerolinecolor: '#444', title: "Y" },
                yaxis: { gridcolor: '#333', range: [minY - yPad, maxY + yPad], zerolinecolor: '#444', title: "X" }
            };

            Plotly.react('chart', data, layout, { responsive: true });
        }

        function resetWeights() {
            stopFlag = true;
            m = (Math.random() - 0.5) * 2; // Randomize between -1 and 1
            c = (Math.random() - 0.5) * 2;
            const { x } = getInputs();
            if (x.length > 0) {
                const preds = x.map(val => (val * m) + c);
                updateInternalViz(x, preds, 0, 0, 0, 0);
                updateChart();
                updateLossChart();
            }
        }

        let lossHistory = []; // To store [epoch, loss] pairs

        async function train() {
            if (isTraining) return;

            const { x: xRaw, y: yRaw } = getInputs();
            // ... (Keep your existing safety checks here) ...

            isTraining = true;
            stopFlag = false;
            lossHistory = []; // Reset history for new run

            document.getElementById('trainBtn').style.display = 'none';
            document.getElementById('stopBtn').style.display = 'block';
            document.getElementById('status').style.display = 'inline';

            const epochs = parseInt(document.getElementById('epochsInput').value);
            const delay = parseInt(document.getElementById('speedInput').value);
            const lr = parseFloat(document.getElementById('learningRate').value);
            const n = xRaw.length;

            for (let e = 0; e < epochs; e++) {
                if (stopFlag) break;

                let preds = xRaw.map(x => (x * m) + c);
                let loss = preds.reduce((acc, p, i) => acc + Math.pow(p - yRaw[i], 2), 0) / n;

                // Save loss for the curve
                lossHistory.push(loss);

                let m_grad = 0, c_grad = 0;
                for (let i = 0; i < n; i++) {
                    let err = preds[i] - yRaw[i];
                    m_grad += (2 / n) * err * xRaw[i];
                    c_grad += (2 / n) * err;
                }

                if (isNaN(m_grad) || isNaN(c_grad)) {
                    alert("GRADIENT EXPLODED! LR is too high for this data scale.");
                    stopFlag = true;
                    break;
                }

                m -= lr * m_grad;
                c -= lr * c_grad;

                updateInternalViz(xRaw, preds, loss, m_grad, c_grad, e);
                updateChart();
                updateLossChart(); // Update the new curve

                if (delay > 0) await new Promise(r => setTimeout(r, delay));
            }

            isTraining = false;
            document.getElementById('trainBtn').style.display = 'block';
            document.getElementById('stopBtn').style.display = 'none';
            document.getElementById('status').style.display = 'none';
        }

        function updateLossChart() {
            const trace = {
                x: lossHistory.map((_, i) => i),
                y: lossHistory,
                type: 'scatter',
                mode: 'lines',
                name: 'MSE Loss',
                line: { color: '#FF0000' }
            };

            const layout = {
                autosize: true,
                title: { text: 'Epoch vs Loss (Error)', font: { color: '#aaa', size: 12 } },
                paper_bgcolor: '#1a1a1a', plot_bgcolor: '#1a1a1a',
                font: { color: '#00ff00' },
                xaxis: { gridcolor: '#333', title: 'Epoch' },
                yaxis: { gridcolor: '#333', title: 'Loss', type: 'log' } // Log scale helps see small improvements
            };

            Plotly.react('lossChart', [trace], layout, { responsive: true });
        }

        function generateUniformDistribution(min, max, count, isInteger = false) {
            const list = [];
            for (let i = 0; i < count; i++) {
                if (isInteger) {
                    // For inclusive integers between min and max
                    const minCeiled = Math.ceil(min);
                    const maxFloored = Math.floor(max);
                    list.push(Math.floor(Math.random() * (maxFloored - minCeiled + 1)) + minCeiled);
                } else {
                    // For inclusive min, exclusive max floats
                    list.push(Math.random() * (max - min) + min);
                }
            }
            return list;
        }

        function randomizeData() {
            stopFlag = true; // Stop training if it's running

            const numPoints = 15;
            const trueM = (Math.random() - 0.5) * 5; // Target slope
            const trueC = (Math.random() - 0.5) * 10; // Target intercept

            let xVals = generateUniformDistribution(1, 20, numPoints, true);
            let yVals = xVals.map(x => {
                const noise = (Math.random() - 0.5) * 2;
                return (trueM * x) + trueC + noise;
            });


            // Update the input fields in the UI
            document.getElementById('inputX').value = xVals.join(', ');
            document.getElementById('inputY').value = yVals.join(', ');

            document.getElementById('trueWeights').innerHTML = `| ${Number(trueM).toFixed(4)} | (m)<br>| ${Number(trueC).toFixed(4)} | (c)`;

            // Refresh the visualizer with the new data
            resetWeights();
        }

        // Add the click listener
        document.getElementById('randomDataBtn').addEventListener('click', randomizeData);
        document.getElementById('trainBtn').addEventListener('click', train);
        document.getElementById('stopBtn').addEventListener('click', () => stopFlag = true);
        document.getElementById('resetBtn').addEventListener('click', resetWeights);

        randomizeData();
    </script>
</body>

</html>