import { useEffect, useState, useRef } from "react"; import * as tf from "@tensorflow/tfjs"; import Plot from "react-plotly.js"; import { MnistData } from "./mnist.js"; import { Cnn, train } from "./train.ts"; import type { TrainController, RunInfo, OptimizerParams } from "./train.ts"; import { Button, InputField, Tabs, Dropdown, Card } from "@elvis/ui"; import InfoViewer from "./InfoViewer.tsx"; const DEFAULT_ARCHITECTURE = `[conv2d filters=8 kernel=11 stride=1 padding=1 activation=relu] [maxpool size=2 stride=2] [flatten] [dense units=10 activation=softmax]`; const isFirefox = navigator.userAgent.toLowerCase().includes("firefox"); await tf.setBackend(isFirefox ? "cpu" : "webgl"); await tf.ready(); export default function NetworkVisualizer() { const [dataset, setDataset] = useState(null); useEffect(() => { const loadData = async () => { const data = new MnistData(); await data.load(); setDataset(data); console.log("dataset loaded"); } loadData(); }, []); // architecture states const [architecture, setArchitecture] = useState(DEFAULT_ARCHITECTURE); const [optimizerType, setOptimizerType] = useState('adam'); const [optimizerParams, setOptimizerParams] = useState({ learningRate: '0.001', beta1: '0.9', beta2: '0.999', epsilon: '1e-8', batchSize: '32', epochs: '5', }); const modelRef = useRef(null); const optimizerRef = useRef(null); function handleArchitectureChange(newArchitecture: string) { if (isTraining) { alert('Cannot change architecture while training is in progress.'); } else { setArchitecture(newArchitecture); } } function handleOptimizerChange(newOptimizerType: string, newOptimizerParams: OptimizerParams) { if (isTraining) { alert('Cannot change optimizer settings while training is in progress.'); } else { setOptimizerType(newOptimizerType); setOptimizerParams(newOptimizerParams); } } function handleSampleIndexChange() { trainController.current.sampleIndex += 1; refreshSampleInfo(); } function resetModel() { if (!dataset) return; if (modelRef.current) { modelRef.current.dispose(); } const cnn = new Cnn(architecture, dataset.numInputChannels); modelRef.current = cnn; } function resetOptimizer() { if (optimizerType === 'adam') { const learningRate = parseFloat(optimizerParams.learningRate); const beta1 = parseFloat(optimizerParams.beta1 || "0.9"); const beta2 = parseFloat(optimizerParams.beta2 || "0.999"); const epsilon = parseFloat(optimizerParams.epsilon || "1e-8"); if (Number.isNaN(learningRate) || learningRate <= 0) { alert('Invalid learning rate for Adam optimizer.'); return; } if (Number.isNaN(beta1) || beta1 < 0) { alert('Invalid beta1 for Adam optimizer.'); return; } if (Number.isNaN(beta2) || beta2 < 0) { alert('Invalid beta2 for Adam optimizer.'); return; } if (Number.isNaN(epsilon) || epsilon <= 0) { alert('Invalid epsilon for Adam optimizer.'); return; } const opt = tf.train.adam(learningRate, beta1, beta2, epsilon); if (optimizerRef.current) { optimizerRef.current.dispose(); } optimizerRef.current = opt; } else if (optimizerType === 'sgd') { const learningRate = parseFloat(optimizerParams.learningRate); if (Number.isNaN(learningRate) || learningRate <= 0) { alert('Invalid learning rate for SGD optimizer.'); return; } const opt = tf.train.sgd(learningRate); if (optimizerRef.current) { optimizerRef.current.dispose(); } optimizerRef.current = opt; } else { alert(`Unsupported optimizer type: ${optimizerType}`); } } // Reset the model only when its architecture or input shape changes. useEffect(() => { resetModel(); }, [architecture, dataset]); // Optimizer settings can change without discarding the trained model. useEffect(() => { resetOptimizer(); }, [optimizerType, optimizerParams]); // training states const [isTraining, setIsTraining] = useState(false); const isTrainingRef = useRef(false); const lossesRef = useRef>([]); const trainController = useRef({ isPaused: false, stopRequested: false, sampleIndex: 0, }); const infoRef = useRef>([]); function refreshSampleInfo() { if (!dataset || !modelRef.current) return; let nextInfo: RunInfo[] = []; const cleanupToken = tf.tidy(() => { const sample = dataset.getTestSample(trainController.current.sampleIndex); const { output, info } = modelRef.current!.forwardWithInfo( sample.xs.reshape([ 1, dataset.imageSize, dataset.imageSize, dataset.numInputChannels, ]) as tf.Tensor4D, ); const probs = tf.softmax(output); info.push({ type: 'output', output: probs.dataSync(), shape: probs.shape, }); nextInfo = info; return tf.scalar(0); }); cleanupToken.dispose(); infoRef.current = nextInfo; updateTick(); } useEffect(() => { refreshSampleInfo(); }, [architecture, dataset]); // render timing const [, setTick] = useState(0); function updateTick() { setTick((tick) => tick + 1); } async function startTraining() { if (!modelRef.current || !dataset || !optimizerRef.current || isTrainingRef.current) { return; } isTrainingRef.current = true; setIsTraining(true); trainController.current.isPaused = false; trainController.current.stopRequested = false; const batchSize = parseFloat(optimizerParams.batchSize); if (Number.isNaN(batchSize) || batchSize <= 0) { alert('Invalid batch size.'); isTrainingRef.current = false; setIsTraining(false); return; } const epochs = parseFloat(optimizerParams.epochs); if (Number.isNaN(epochs) || epochs <= 0) { alert('Invalid number of epochs.'); isTrainingRef.current = false; setIsTraining(false); return; } let lastTickUpdate = 0; if (!modelRef.current) return; if (!optimizerRef.current) return; try { await train( dataset, modelRef.current, optimizerRef.current, batchSize, epochs, trainController.current, (_epoch, _batch, loss, info) => { // lossesRef.current.push({ epoch, batch, loss }); lossesRef.current.push(loss); console.log(loss); infoRef.current = info; // update tick every 50ms const now = performance.now(); if (now - lastTickUpdate > 50) { lastTickUpdate = now; updateTick(); } }, ); } finally { const wasStopped = trainController.current.stopRequested; isTrainingRef.current = false; setIsTraining(false); trainController.current.isPaused = false; trainController.current.stopRequested = false; if (!wasStopped) { alert('Training finished.'); } } } function handleTrain() { if (isTrainingRef.current) { trainController.current.isPaused = false; return; } console.log('Starting training...'); startTraining(); } function handlePauseTraining() { console.log('Pausing training...'); trainController.current.isPaused = true; } function handleStopTraining() { console.log('Stopping training...'); trainController.current.stopRequested = true; trainController.current.isPaused = false; } async function waitUntilNotTraining() { return new Promise((resolve) => { function check() { if (!isTrainingRef.current) { resolve(); } else { requestAnimationFrame(check); } } check(); }); } async function handleResetTraining() { console.log('Resetting training...'); handleStopTraining(); await waitUntilNotTraining(); console.log('Training stopped. Resetting model.'); lossesRef.current = []; infoRef.current = []; resetModel(); resetOptimizer(); refreshSampleInfo(); } return (

Configure and train a convolutional neural network (CNN) to classify handwritten digits from the MNIST dataset.

); } interface TrainingViewerProps { infoRef: React.RefObject>; handleSampleIndexChange: () => void; } function TrainingViewer({ infoRef, handleSampleIndexChange, }: TrainingViewerProps) { return (
) } interface SidebarProps { architecture: string; onArchitectureChange: (newArchitecture: string) => void; optimizerType: string; optimizerParams: OptimizerParams; onOptimizerChange: (newOptimizerType: string, newOptimizerParams: OptimizerParams) => void; isTraining: boolean; lossesRef: React.RefObject>; onTrain: () => void; onPauseTraining: () => void; onResetTraining: () => void; } type TrainingInputRowProps = { label: string; value: string | undefined; onChange: (value: string) => void; }; function TrainingInputRow({ label, value, onChange }: TrainingInputRowProps) { return (
{label}
); } function Sidebar({ architecture, onArchitectureChange, optimizerType, optimizerParams, onOptimizerChange, isTraining, lossesRef, onTrain, onPauseTraining, onResetTraining, }: SidebarProps) { const tabs = ["Architecture", "Train"]; const [activeTab, setActiveTab] = useState(tabs[0]); const [architectureDraft, setArchitectureDraft] = useState(architecture); return (
{ isFirefox && (

Warning: This demo may be quite slow on Firefox.

)} { activeTab === "Architecture" && ( <>
)}
) }