joel-woodfield commited on
Commit
5e54979
·
1 Parent(s): 89ce55d

Fix formatting

Browse files
Files changed (44) hide show
  1. src/App.jsx +16 -20
  2. src/components/Button/Button.css +10 -10
  3. src/components/Button/Button.jsx +10 -17
  4. src/components/Switch/Switch.css +1 -2
  5. src/components/Switch/Switch.jsx +1 -1
  6. src/components/Tabs/Tabs.css +3 -4
  7. src/components/Tabs/Tabs.jsx +3 -5
  8. src/components/convolution/ConvolutionViewer.jsx +22 -27
  9. src/components/convolution/ConvolutionVisualizer.jsx +19 -21
  10. src/components/convolution/InputSelector.jsx +8 -10
  11. src/components/convolution/KernelEditor.css +17 -14
  12. src/components/convolution/KernelEditor.jsx +32 -30
  13. src/components/convolution/PresetSelector.css +0 -1
  14. src/components/convolution/PresetSelector.jsx +8 -12
  15. src/components/convolution/Sidebar.css +4 -5
  16. src/components/convolution/Sidebar.jsx +12 -16
  17. src/components/network/ArchitectureEditor.css +39 -39
  18. src/components/network/ArchitectureEditor.jsx +5 -10
  19. src/components/network/Conv2dLayerViewer.jsx +67 -54
  20. src/components/network/InfoViewer.jsx +52 -54
  21. src/components/network/InputViewer.jsx +26 -29
  22. src/components/network/LayerViewer.css +5 -5
  23. src/components/network/MaxPoolLayerViewer.jsx +62 -58
  24. src/components/network/NetworkViewer.jsx +17 -21
  25. src/components/network/NetworkVisualizer.jsx +144 -152
  26. src/components/network/OptimizerSettings.jsx +120 -72
  27. src/components/network/OutputLayerViewer.jsx +5 -4
  28. src/components/network/Sidebar.jsx +27 -30
  29. src/components/network/old/AccPlot.jsx +18 -18
  30. src/components/network/old/ActivationViewer.jsx +10 -18
  31. src/components/network/old/ArchitectureEditor.css +39 -39
  32. src/components/network/old/ArchitectureEditor.jsx +29 -40
  33. src/components/network/old/KernelViewer.jsx +0 -1
  34. src/components/network/old/LossPlot.jsx +38 -38
  35. src/components/network/old/MnistTrainer.css +8 -9
  36. src/components/network/old/MnistTrainer.jsx +139 -159
  37. src/components/network/old/mnist.js +52 -27
  38. src/components/network/train.js +181 -182
  39. src/data/convolution_presets.js +9 -10
  40. src/data/mnist.js +71 -41
  41. src/main.jsx +5 -5
  42. src/styles.css +59 -59
  43. src/utils/convolution.js +8 -12
  44. src/utils/image.js +0 -1
src/App.jsx CHANGED
@@ -1,30 +1,26 @@
1
- import './App.css'
2
- import ConvolutionVisualizer from "./components/convolution/ConvolutionVisualizer.jsx"
3
- import NetworkVisualizer from "./components/network/NetworkVisualizer.jsx"
4
- import Tabs from "./components/Tabs/Tabs.jsx"
5
-
6
 
7
  function App() {
8
- const tabData = [
9
- {
10
- label: "Convolution",
11
- content: <ConvolutionVisualizer />,
12
- },
13
- {
14
- label: "Network",
15
- content: <NetworkVisualizer />,
16
- },
17
- ];
18
 
19
  return (
20
  <>
21
  <h1>CNN Visualizer</h1>
22
- <Tabs
23
- defaultTab="Convolution"
24
- tabs={tabData}
25
- />
26
  </>
27
- )
28
  }
29
 
30
  export default App;
 
1
+ import './App.css';
2
+ import ConvolutionVisualizer from './components/convolution/ConvolutionVisualizer.jsx';
3
+ import NetworkVisualizer from './components/network/NetworkVisualizer.jsx';
4
+ import Tabs from './components/Tabs/Tabs.jsx';
 
5
 
6
  function App() {
7
+ const tabData = [
8
+ {
9
+ label: 'Convolution',
10
+ content: <ConvolutionVisualizer />,
11
+ },
12
+ {
13
+ label: 'Network',
14
+ content: <NetworkVisualizer />,
15
+ },
16
+ ];
17
 
18
  return (
19
  <>
20
  <h1>CNN Visualizer</h1>
21
+ <Tabs defaultTab="Convolution" tabs={tabData} />
 
 
 
22
  </>
23
+ );
24
  }
25
 
26
  export default App;
src/components/Button/Button.css CHANGED
@@ -1,21 +1,21 @@
1
  .app-button {
2
- padding: 8px 16px;
3
- background-color: #f0f0f0;
4
- border: none;
5
- border-radius: 8px;
6
- cursor: pointer;
7
- font-size: 1rem;
8
  }
9
 
10
  .app-button:focus {
11
- outline: none;
12
  }
13
 
14
  .app-button:hover {
15
- outline: 1px solid orange;
16
  }
17
 
18
  .app-button.selected {
19
- background-color: orange;
20
- color: white;
21
  }
 
1
  .app-button {
2
+ padding: 8px 16px;
3
+ background-color: #f0f0f0;
4
+ border: none;
5
+ border-radius: 8px;
6
+ cursor: pointer;
7
+ font-size: 1rem;
8
  }
9
 
10
  .app-button:focus {
11
+ outline: none;
12
  }
13
 
14
  .app-button:hover {
15
+ outline: 1px solid orange;
16
  }
17
 
18
  .app-button.selected {
19
+ background-color: orange;
20
+ color: white;
21
  }
src/components/Button/Button.jsx CHANGED
@@ -1,22 +1,15 @@
1
- import "./Button.css";
2
 
3
  export default function Button(props) {
4
- const {
5
- selected = false,
6
- children,
7
- className = "",
8
- ...rest
9
- } = props;
10
 
11
- const classes = [
12
- "app-button",
13
- selected ? "selected" : "",
14
- className
15
- ].join(" ");
16
 
17
- return (
18
- <button className={classes} {...rest}>
19
- {children}
20
- </button>
21
- );
22
  }
 
1
+ import './Button.css';
2
 
3
  export default function Button(props) {
4
+ const { selected = false, children, className = '', ...rest } = props;
 
 
 
 
 
5
 
6
+ const classes = ['app-button', selected ? 'selected' : '', className].join(
7
+ ' ',
8
+ );
 
 
9
 
10
+ return (
11
+ <button className={classes} {...rest}>
12
+ {children}
13
+ </button>
14
+ );
15
  }
src/components/Switch/Switch.css CHANGED
@@ -33,7 +33,7 @@
33
 
34
  .slider::before {
35
  position: absolute;
36
- content: "";
37
  height: 28px;
38
  width: 28px;
39
  left: 4px;
@@ -46,4 +46,3 @@
46
  .switch input:checked + .slider {
47
  background: orange;
48
  }
49
-
 
33
 
34
  .slider::before {
35
  position: absolute;
36
+ content: '';
37
  height: 28px;
38
  width: 28px;
39
  left: 4px;
 
46
  .switch input:checked + .slider {
47
  background: orange;
48
  }
 
src/components/Switch/Switch.jsx CHANGED
@@ -1,4 +1,4 @@
1
- import "./Switch.css";
2
 
3
  function Switch(props) {
4
  return (
 
1
+ import './Switch.css';
2
 
3
  function Switch(props) {
4
  return (
src/components/Tabs/Tabs.css CHANGED
@@ -26,13 +26,12 @@
26
  .tab-content {
27
  flex: 1;
28
  padding: 20px;
29
- border: none;
30
  }
31
 
32
  .tab-buttons button:focus {
33
- outline: none;
34
  }
35
  .tab-buttons button:focus-visible {
36
- outline: 2px solid orange;
37
  }
38
-
 
26
  .tab-content {
27
  flex: 1;
28
  padding: 20px;
29
+ border: none;
30
  }
31
 
32
  .tab-buttons button:focus {
33
+ outline: none;
34
  }
35
  .tab-buttons button:focus-visible {
36
+ outline: 2px solid orange;
37
  }
 
src/components/Tabs/Tabs.jsx CHANGED
@@ -1,5 +1,5 @@
1
- import { useState } from "react";
2
- import "./Tabs.css";
3
 
4
  function Tabs(props) {
5
  const [activeTab, setActiveTab] = useState(props.defaultTab);
@@ -10,14 +10,12 @@ function Tabs(props) {
10
  {props.tabs.map((tab) => (
11
  <button
12
  key={tab.label}
13
- className={activeTab === tab.label ? "active" : ""}
14
  onClick={() => setActiveTab(tab.label)}
15
  >
16
  {tab.label}
17
  </button>
18
  ))}
19
-
20
-
21
  </div>
22
 
23
  <div className="tab-content">
 
1
+ import { useState } from 'react';
2
+ import './Tabs.css';
3
 
4
  function Tabs(props) {
5
  const [activeTab, setActiveTab] = useState(props.defaultTab);
 
10
  {props.tabs.map((tab) => (
11
  <button
12
  key={tab.label}
13
+ className={activeTab === tab.label ? 'active' : ''}
14
  onClick={() => setActiveTab(tab.label)}
15
  >
16
  {tab.label}
17
  </button>
18
  ))}
 
 
19
  </div>
20
 
21
  <div className="tab-content">
src/components/convolution/ConvolutionViewer.jsx CHANGED
@@ -1,7 +1,7 @@
1
- import { useState, useEffect, useRef } from "react";
2
- import { getImageData, convertToGrayscale } from "../../utils/image";
3
- import { convolveGray, convolveColor } from "../../utils/convolution";
4
- import "./ConvolutionViewer.css";
5
 
6
  function ConvolutionViewer(props) {
7
  const canvasRef = useRef(null);
@@ -12,7 +12,7 @@ function ConvolutionViewer(props) {
12
  useEffect(() => {
13
  async function process() {
14
  const canvas = canvasRef.current;
15
- const ctx = canvas.getContext("2d", { willReadFrequently: true });
16
  const imageData = await getImageData(props.inputImageUrl, canvas, ctx);
17
 
18
  const grayImageData = convertToGrayscale(imageData);
@@ -21,7 +21,6 @@ function ConvolutionViewer(props) {
21
  }
22
 
23
  process();
24
-
25
  }, [props.inputImageUrl]);
26
 
27
  // set input image URL for display
@@ -34,7 +33,7 @@ function ConvolutionViewer(props) {
34
  }
35
 
36
  const canvas = canvasRef.current;
37
- const ctx = canvas.getContext("2d", { willReadFrequently: true });
38
 
39
  if (props.useColor) {
40
  ctx.putImageData(props.colorImage, 0, 0);
@@ -46,17 +45,16 @@ function ConvolutionViewer(props) {
46
  }, [props.useColor, props.colorImage, props.grayImage]);
47
 
48
  // perform convolution and set output image URL for display
49
- useEffect( () => {
50
  if (!props.kernel) {
51
  return;
52
  }
53
- const hasInvalid = props.kernel.flat().flat().some(
54
- (v) =>
55
- v === "" ||
56
- v === null ||
57
- v === undefined ||
58
- !Number.isFinite(v)
59
- );
60
  if (hasInvalid) {
61
  return;
62
  }
@@ -68,38 +66,35 @@ function ConvolutionViewer(props) {
68
  return;
69
  }
70
 
71
- const outputImageData = props.useColor
72
- ? convolveColor(props.colorImage, props.kernel)
73
  : convolveGray(props.grayImage, props.kernel[0]);
74
-
75
  // draw output on canvas to get data URL
76
  const canvas = canvasRef.current;
77
- const ctx = canvas.getContext("2d", { willReadFrequently: true });
78
  canvas.width = outputImageData.width;
79
  canvas.height = outputImageData.height;
80
  ctx.putImageData(outputImageData, 0, 0);
81
  const url = canvas.toDataURL();
82
  setOutputUrl(url);
83
-
84
  }, [props.useColor, props.grayImage, props.colorImage, props.kernel]);
85
 
86
  return (
87
  <div className="convolution-viewer">
88
  <div className="convolution-viewer-item">
89
  <h3>Input</h3>
90
- { inputUrl ? (
91
- <img src={inputUrl} alt="Input" />
92
- ) : ( <p>Loading...</p>
93
- )}
94
  </div>
95
  <div className="convolution-viewer-item">
96
  <h3>Output</h3>
97
- { outputUrl ? (
98
  <img src={outputUrl} alt="Output" />
99
- ) : ( <p>Processing...</p>
 
100
  )}
101
  </div>
102
- <canvas ref={canvasRef} style={{ display: "none" }}></canvas>
103
  </div>
104
  );
105
  }
 
1
+ import { useState, useEffect, useRef } from 'react';
2
+ import { getImageData, convertToGrayscale } from '../../utils/image';
3
+ import { convolveGray, convolveColor } from '../../utils/convolution';
4
+ import './ConvolutionViewer.css';
5
 
6
  function ConvolutionViewer(props) {
7
  const canvasRef = useRef(null);
 
12
  useEffect(() => {
13
  async function process() {
14
  const canvas = canvasRef.current;
15
+ const ctx = canvas.getContext('2d', { willReadFrequently: true });
16
  const imageData = await getImageData(props.inputImageUrl, canvas, ctx);
17
 
18
  const grayImageData = convertToGrayscale(imageData);
 
21
  }
22
 
23
  process();
 
24
  }, [props.inputImageUrl]);
25
 
26
  // set input image URL for display
 
33
  }
34
 
35
  const canvas = canvasRef.current;
36
+ const ctx = canvas.getContext('2d', { willReadFrequently: true });
37
 
38
  if (props.useColor) {
39
  ctx.putImageData(props.colorImage, 0, 0);
 
45
  }, [props.useColor, props.colorImage, props.grayImage]);
46
 
47
  // perform convolution and set output image URL for display
48
+ useEffect(() => {
49
  if (!props.kernel) {
50
  return;
51
  }
52
+ const hasInvalid = props.kernel
53
+ .flat()
54
+ .flat()
55
+ .some(
56
+ (v) => v === '' || v === null || v === undefined || !Number.isFinite(v),
57
+ );
 
58
  if (hasInvalid) {
59
  return;
60
  }
 
66
  return;
67
  }
68
 
69
+ const outputImageData = props.useColor
70
+ ? convolveColor(props.colorImage, props.kernel)
71
  : convolveGray(props.grayImage, props.kernel[0]);
72
+
73
  // draw output on canvas to get data URL
74
  const canvas = canvasRef.current;
75
+ const ctx = canvas.getContext('2d', { willReadFrequently: true });
76
  canvas.width = outputImageData.width;
77
  canvas.height = outputImageData.height;
78
  ctx.putImageData(outputImageData, 0, 0);
79
  const url = canvas.toDataURL();
80
  setOutputUrl(url);
 
81
  }, [props.useColor, props.grayImage, props.colorImage, props.kernel]);
82
 
83
  return (
84
  <div className="convolution-viewer">
85
  <div className="convolution-viewer-item">
86
  <h3>Input</h3>
87
+ {inputUrl ? <img src={inputUrl} alt="Input" /> : <p>Loading...</p>}
 
 
 
88
  </div>
89
  <div className="convolution-viewer-item">
90
  <h3>Output</h3>
91
+ {outputUrl ? (
92
  <img src={outputUrl} alt="Output" />
93
+ ) : (
94
+ <p>Processing...</p>
95
  )}
96
  </div>
97
+ <canvas ref={canvasRef} style={{ display: 'none' }}></canvas>
98
  </div>
99
  );
100
  }
src/components/convolution/ConvolutionVisualizer.jsx CHANGED
@@ -1,32 +1,30 @@
1
- import { useRef, useState } from 'react'
2
- import './ConvolutionViewer.css'
3
- import ConvolutionViewer from "./ConvolutionViewer.jsx"
4
- import Sidebar from "./Sidebar.jsx"
5
-
6
- import marioImage from "../../assets/mario.png";
7
 
 
8
 
9
  const DEFAULT_IMAGE = marioImage;
10
 
11
  const DEFAULT_KERNEL = [
12
  [
13
  [-1, -1, -1],
14
- [-1, 8, -1],
15
  [-1, -1, -1],
16
  ],
17
  [
18
  [-1, -1, -1],
19
- [-1, 8, -1],
20
  [-1, -1, -1],
21
  ],
22
  [
23
  [-1, -1, -1],
24
- [-1, 8, -1],
25
  [-1, -1, -1],
26
- ]
27
  ];
28
 
29
-
30
  function ConvolutionVisualizer() {
31
  const canvasRef = useRef(null);
32
 
@@ -46,26 +44,26 @@ function ConvolutionVisualizer() {
46
  return (
47
  <>
48
  <div className="main-container">
49
- <ConvolutionViewer
50
- inputImageUrl={image}
51
- kernel={kernel}
52
- grayImage={grayImage}
53
  setGrayImage={setGrayImage}
54
  colorImage={colorImage}
55
  setColorImage={setColorImage}
56
  useColor={useColor}
57
  />
58
- <Sidebar
59
- kernel={kernel}
60
- onKernelChange={setKernel}
61
- onUpload={handleUpload}
62
  useColor={useColor}
63
  onUseColor={setUseColor}
64
  />
65
  </div>
66
- <canvas ref={canvasRef} style={{ display: "none" }}></canvas>
67
  </>
68
- )
69
  }
70
 
71
  export default ConvolutionVisualizer;
 
1
+ import { useRef, useState } from 'react';
2
+ import './ConvolutionViewer.css';
3
+ import ConvolutionViewer from './ConvolutionViewer.jsx';
4
+ import Sidebar from './Sidebar.jsx';
 
 
5
 
6
+ import marioImage from '../../assets/mario.png';
7
 
8
  const DEFAULT_IMAGE = marioImage;
9
 
10
  const DEFAULT_KERNEL = [
11
  [
12
  [-1, -1, -1],
13
+ [-1, 8, -1],
14
  [-1, -1, -1],
15
  ],
16
  [
17
  [-1, -1, -1],
18
+ [-1, 8, -1],
19
  [-1, -1, -1],
20
  ],
21
  [
22
  [-1, -1, -1],
23
+ [-1, 8, -1],
24
  [-1, -1, -1],
25
+ ],
26
  ];
27
 
 
28
  function ConvolutionVisualizer() {
29
  const canvasRef = useRef(null);
30
 
 
44
  return (
45
  <>
46
  <div className="main-container">
47
+ <ConvolutionViewer
48
+ inputImageUrl={image}
49
+ kernel={kernel}
50
+ grayImage={grayImage}
51
  setGrayImage={setGrayImage}
52
  colorImage={colorImage}
53
  setColorImage={setColorImage}
54
  useColor={useColor}
55
  />
56
+ <Sidebar
57
+ kernel={kernel}
58
+ onKernelChange={setKernel}
59
+ onUpload={handleUpload}
60
  useColor={useColor}
61
  onUseColor={setUseColor}
62
  />
63
  </div>
64
+ <canvas ref={canvasRef} style={{ display: 'none' }}></canvas>
65
  </>
66
+ );
67
  }
68
 
69
  export default ConvolutionVisualizer;
src/components/convolution/InputSelector.jsx CHANGED
@@ -1,23 +1,21 @@
1
- import { useRef } from "react";
2
- import Switch from "../Switch/Switch.jsx";
3
- import Button from "../Button/Button.jsx";
4
- import "./InputSelector.css";
5
 
6
  function InputSelector(props) {
7
  const fileInputRef = useRef(null);
8
 
9
  return (
10
  <div className="input-selector">
11
- <input
12
  ref={fileInputRef}
13
  type="file"
14
  accept="image/*"
15
  onChange={props.handleFileChange}
16
- style={{ display: "none" }}
17
- />
18
- <Button onClick={() => fileInputRef.current.click()}>
19
- Upload Image
20
- </Button>
21
  <Switch
22
  checked={props.useColor}
23
  onChange={(e) => props.onUseColor(e)}
 
1
+ import { useRef } from 'react';
2
+ import Switch from '../Switch/Switch.jsx';
3
+ import Button from '../Button/Button.jsx';
4
+ import './InputSelector.css';
5
 
6
  function InputSelector(props) {
7
  const fileInputRef = useRef(null);
8
 
9
  return (
10
  <div className="input-selector">
11
+ <input
12
  ref={fileInputRef}
13
  type="file"
14
  accept="image/*"
15
  onChange={props.handleFileChange}
16
+ style={{ display: 'none' }}
17
+ />
18
+ <Button onClick={() => fileInputRef.current.click()}>Upload Image</Button>
 
 
19
  <Switch
20
  checked={props.useColor}
21
  onChange={(e) => props.onUseColor(e)}
src/components/convolution/KernelEditor.css CHANGED
@@ -4,13 +4,13 @@
4
  gap: 30px;
5
  padding: 12px;
6
  max-width: fit-content;
7
- align-items: center;
8
  }
9
 
10
  .kernel-matrix {
11
- display: flex;
12
- flex-direction: column;
13
- gap: 8px;
14
  }
15
 
16
  .kernel-row {
@@ -19,11 +19,11 @@
19
  }
20
 
21
  .kernel-size-options {
22
- border: 1px solid #d0d0d0;
23
- border-radius: 6px;
24
- padding: 8px 12px;
25
- background: #f9f9f9;
26
- box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04);
27
  }
28
 
29
  .kernel-size-options form {
@@ -32,7 +32,7 @@
32
  align-items: center;
33
  }
34
 
35
- .kernel-size-options input[type="number"] {
36
  width: auto;
37
  max-width: 4ch;
38
  margin: 0 8px;
@@ -46,7 +46,10 @@
46
  border: 1px solid #d0d0d0;
47
  border-radius: 6px;
48
  background: white;
49
- transition: border-color 0.2s, box-shadow 0.2s, transform 0.1s;
 
 
 
50
  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04);
51
  }
52
 
@@ -80,12 +83,12 @@
80
  }
81
 
82
  /* Remove default spinner arrows */
83
- input[type="number"]::-webkit-inner-spin-button,
84
- input[type="number"]::-webkit-outer-spin-button {
85
  -webkit-appearance: none;
86
  margin: 0;
87
  }
88
 
89
- input[type="number"] {
90
  -moz-appearance: textfield;
91
  }
 
4
  gap: 30px;
5
  padding: 12px;
6
  max-width: fit-content;
7
+ align-items: center;
8
  }
9
 
10
  .kernel-matrix {
11
+ display: flex;
12
+ flex-direction: column;
13
+ gap: 8px;
14
  }
15
 
16
  .kernel-row {
 
19
  }
20
 
21
  .kernel-size-options {
22
+ border: 1px solid #d0d0d0;
23
+ border-radius: 6px;
24
+ padding: 8px 12px;
25
+ background: #f9f9f9;
26
+ box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04);
27
  }
28
 
29
  .kernel-size-options form {
 
32
  align-items: center;
33
  }
34
 
35
+ .kernel-size-options input[type='number'] {
36
  width: auto;
37
  max-width: 4ch;
38
  margin: 0 8px;
 
46
  border: 1px solid #d0d0d0;
47
  border-radius: 6px;
48
  background: white;
49
+ transition:
50
+ border-color 0.2s,
51
+ box-shadow 0.2s,
52
+ transform 0.1s;
53
  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04);
54
  }
55
 
 
83
  }
84
 
85
  /* Remove default spinner arrows */
86
+ input[type='number']::-webkit-inner-spin-button,
87
+ input[type='number']::-webkit-outer-spin-button {
88
  -webkit-appearance: none;
89
  margin: 0;
90
  }
91
 
92
+ input[type='number'] {
93
  -moz-appearance: textfield;
94
  }
src/components/convolution/KernelEditor.jsx CHANGED
@@ -1,17 +1,17 @@
1
  import { useState, useEffect } from 'react';
2
  import PresetSelector from './PresetSelector.jsx';
3
- import Button from "../Button/Button.jsx";
4
  import './KernelEditor.css';
5
 
6
  function KernelEditor(props) {
7
  const [width, setWidth] = useState(props.kernel[0].length);
8
  const [height, setHeight] = useState(props.kernel.length);
9
 
10
- const [selectedChannel, setSelectedChannel] = useState(0); // 0=R, 1=G, 2=B
11
 
12
  function handleChange(channel, row, col, value) {
13
  const newKernel = props.kernel.map((r) => [...r]);
14
- newKernel[channel][row][col] = value === "" ? "" : parseFloat(value);
15
  props.onKernelChange?.(newKernel);
16
  }
17
 
@@ -54,14 +54,14 @@ function KernelEditor(props) {
54
  <div className="kernel-editor">
55
  <div className="kernel-size-options">
56
  <form onSubmit={handleSizeSubmit}>
57
- Width: {" "}
58
  <input
59
  type="number"
60
  value={width}
61
  min={1}
62
  onChange={(e) => setWidth(parseInt(e.target.value))}
63
  />
64
- Height: {" "}
65
  <input
66
  type="number"
67
  value={height}
@@ -74,45 +74,47 @@ function KernelEditor(props) {
74
  {props.useColor && (
75
  <div className="channel-selector">
76
  <div className="channel-buttons">
77
- <Button
78
- selected={selectedChannel === 0}
79
- onClick={() => setSelectedChannel(0)}
80
  >
81
  R
82
  </Button>
83
  <Button
84
- selected={selectedChannel === 1}
85
- onClick={() => setSelectedChannel(1)}
86
  >
87
  G
88
  </Button>
89
  <Button
90
- selected={selectedChannel === 2}
91
- onClick={() => setSelectedChannel(2)}
92
  >
93
  B
94
  </Button>
95
- </div>
96
  </div>
97
  )}
98
  <div className="kernel-matrix">
99
- {props.kernel[selectedChannel].map((row, i) => (
100
- <div key={i} className="kernel-row">
101
- {row.map((value, j) => (
102
- <input
103
- key={j}
104
- type="number"
105
- value={value}
106
- onChange={(e) => handleChange(selectedChannel, i, j, e.target.value)}
107
- className="kernel-cell"
108
- />
109
- ))}
110
- </div>
111
- ))}
112
- </div>
113
- <PresetSelector
114
- onKernelChange={props.onKernelChange}
115
- useColor={props.useColor}
 
 
116
  />
117
  </div>
118
  );
 
1
  import { useState, useEffect } from 'react';
2
  import PresetSelector from './PresetSelector.jsx';
3
+ import Button from '../Button/Button.jsx';
4
  import './KernelEditor.css';
5
 
6
  function KernelEditor(props) {
7
  const [width, setWidth] = useState(props.kernel[0].length);
8
  const [height, setHeight] = useState(props.kernel.length);
9
 
10
+ const [selectedChannel, setSelectedChannel] = useState(0); // 0=R, 1=G, 2=B
11
 
12
  function handleChange(channel, row, col, value) {
13
  const newKernel = props.kernel.map((r) => [...r]);
14
+ newKernel[channel][row][col] = value === '' ? '' : parseFloat(value);
15
  props.onKernelChange?.(newKernel);
16
  }
17
 
 
54
  <div className="kernel-editor">
55
  <div className="kernel-size-options">
56
  <form onSubmit={handleSizeSubmit}>
57
+ Width:{' '}
58
  <input
59
  type="number"
60
  value={width}
61
  min={1}
62
  onChange={(e) => setWidth(parseInt(e.target.value))}
63
  />
64
+ Height:{' '}
65
  <input
66
  type="number"
67
  value={height}
 
74
  {props.useColor && (
75
  <div className="channel-selector">
76
  <div className="channel-buttons">
77
+ <Button
78
+ selected={selectedChannel === 0}
79
+ onClick={() => setSelectedChannel(0)}
80
  >
81
  R
82
  </Button>
83
  <Button
84
+ selected={selectedChannel === 1}
85
+ onClick={() => setSelectedChannel(1)}
86
  >
87
  G
88
  </Button>
89
  <Button
90
+ selected={selectedChannel === 2}
91
+ onClick={() => setSelectedChannel(2)}
92
  >
93
  B
94
  </Button>
95
+ </div>
96
  </div>
97
  )}
98
  <div className="kernel-matrix">
99
+ {props.kernel[selectedChannel].map((row, i) => (
100
+ <div key={i} className="kernel-row">
101
+ {row.map((value, j) => (
102
+ <input
103
+ key={j}
104
+ type="number"
105
+ value={value}
106
+ onChange={(e) =>
107
+ handleChange(selectedChannel, i, j, e.target.value)
108
+ }
109
+ className="kernel-cell"
110
+ />
111
+ ))}
112
+ </div>
113
+ ))}
114
+ </div>
115
+ <PresetSelector
116
+ onKernelChange={props.onKernelChange}
117
+ useColor={props.useColor}
118
  />
119
  </div>
120
  );
src/components/convolution/PresetSelector.css CHANGED
@@ -49,4 +49,3 @@
49
  .preset-apply:hover {
50
  background: #1e7be6;
51
  }
52
-
 
49
  .preset-apply:hover {
50
  background: #1e7be6;
51
  }
 
src/components/convolution/PresetSelector.jsx CHANGED
@@ -1,13 +1,11 @@
1
- import { useState, useEffect } from "react";
2
- import Button from "../Button/Button.jsx";
3
- import { PRESETS_COLOR, PRESETS_GRAY } from "../../data/convolution_presets.js";
4
- import "./PresetSelector.css";
5
 
6
  function PresetSelector(props) {
7
  const [presetNames, setPresetNames] = useState(
8
- props.useColor
9
- ? Object.keys(PRESETS_COLOR)
10
- : Object.keys(PRESETS_GRAY)
11
  );
12
  const [selectedPreset, setSelectedPreset] = useState(presetNames[0]);
13
 
@@ -20,8 +18,8 @@ function PresetSelector(props) {
20
  }
21
 
22
  useEffect(() => {
23
- console.log("PresetSelector: useEffect triggered");
24
- console.log("useColor:", props.useColor);
25
  const names = props.useColor
26
  ? Object.keys(PRESETS_COLOR)
27
  : Object.keys(PRESETS_GRAY);
@@ -45,9 +43,7 @@ function PresetSelector(props) {
45
  ))}
46
  </select>
47
 
48
- <Button onClick={applyPreset}>
49
- Apply
50
- </Button>
51
  </div>
52
  );
53
  }
 
1
+ import { useState, useEffect } from 'react';
2
+ import Button from '../Button/Button.jsx';
3
+ import { PRESETS_COLOR, PRESETS_GRAY } from '../../data/convolution_presets.js';
4
+ import './PresetSelector.css';
5
 
6
  function PresetSelector(props) {
7
  const [presetNames, setPresetNames] = useState(
8
+ props.useColor ? Object.keys(PRESETS_COLOR) : Object.keys(PRESETS_GRAY),
 
 
9
  );
10
  const [selectedPreset, setSelectedPreset] = useState(presetNames[0]);
11
 
 
18
  }
19
 
20
  useEffect(() => {
21
+ console.log('PresetSelector: useEffect triggered');
22
+ console.log('useColor:', props.useColor);
23
  const names = props.useColor
24
  ? Object.keys(PRESETS_COLOR)
25
  : Object.keys(PRESETS_GRAY);
 
43
  ))}
44
  </select>
45
 
46
+ <Button onClick={applyPreset}>Apply</Button>
 
 
47
  </div>
48
  );
49
  }
src/components/convolution/Sidebar.css CHANGED
@@ -19,27 +19,26 @@
19
  right: 0;
20
  bottom: 0;
21
  background-color: #ccc;
22
- transition: .4s;
23
  border-radius: 24px;
24
  }
25
 
26
  .slider:before {
27
  position: absolute;
28
- content: "";
29
  height: 18px;
30
  width: 18px;
31
  left: 3px;
32
  bottom: 3px;
33
  background-color: white;
34
- transition: .4s;
35
  border-radius: 50%;
36
  }
37
 
38
  input:checked + .slider {
39
- background-color: #4CAF50;
40
  }
41
 
42
  input:checked + .slider:before {
43
  transform: translateX(26px);
44
  }
45
-
 
19
  right: 0;
20
  bottom: 0;
21
  background-color: #ccc;
22
+ transition: 0.4s;
23
  border-radius: 24px;
24
  }
25
 
26
  .slider:before {
27
  position: absolute;
28
+ content: '';
29
  height: 18px;
30
  width: 18px;
31
  left: 3px;
32
  bottom: 3px;
33
  background-color: white;
34
+ transition: 0.4s;
35
  border-radius: 50%;
36
  }
37
 
38
  input:checked + .slider {
39
+ background-color: #4caf50;
40
  }
41
 
42
  input:checked + .slider:before {
43
  transform: translateX(26px);
44
  }
 
src/components/convolution/Sidebar.jsx CHANGED
@@ -1,17 +1,16 @@
1
- import KernelEditor from "./KernelEditor.jsx";
2
- import Tabs from "../Tabs/Tabs.jsx";
3
- import InputSelector from "./InputSelector.jsx";
4
- import "./Sidebar.css";
5
 
6
-
7
- const DEFAULT_TAB = "Input";
8
 
9
  function Sidebar(props) {
10
  const tabData = [
11
  {
12
- label: "Input",
13
  content: (
14
- <InputSelector
15
  handleFileChange={handleFileChange}
16
  useColor={props.useColor}
17
  onUseColor={props.onUseColor}
@@ -19,11 +18,11 @@ function Sidebar(props) {
19
  ),
20
  },
21
  {
22
- label: "Kernel",
23
  content: (
24
- <KernelEditor
25
- kernel={props.kernel}
26
- onKernelChange={props.onKernelChange}
27
  onBoundingBoxButton={props.onBoundingBoxButton}
28
  useColor={props.useColor}
29
  />
@@ -40,10 +39,7 @@ function Sidebar(props) {
40
 
41
  return (
42
  <div className="sidebar">
43
- <Tabs
44
- tabs={tabData}
45
- defaultTab={DEFAULT_TAB}
46
- />
47
  </div>
48
  );
49
  }
 
1
+ import KernelEditor from './KernelEditor.jsx';
2
+ import Tabs from '../Tabs/Tabs.jsx';
3
+ import InputSelector from './InputSelector.jsx';
4
+ import './Sidebar.css';
5
 
6
+ const DEFAULT_TAB = 'Input';
 
7
 
8
  function Sidebar(props) {
9
  const tabData = [
10
  {
11
+ label: 'Input',
12
  content: (
13
+ <InputSelector
14
  handleFileChange={handleFileChange}
15
  useColor={props.useColor}
16
  onUseColor={props.onUseColor}
 
18
  ),
19
  },
20
  {
21
+ label: 'Kernel',
22
  content: (
23
+ <KernelEditor
24
+ kernel={props.kernel}
25
+ onKernelChange={props.onKernelChange}
26
  onBoundingBoxButton={props.onBoundingBoxButton}
27
  useColor={props.useColor}
28
  />
 
39
 
40
  return (
41
  <div className="sidebar">
42
+ <Tabs tabs={tabData} defaultTab={DEFAULT_TAB} />
 
 
 
43
  </div>
44
  );
45
  }
src/components/network/ArchitectureEditor.css CHANGED
@@ -1,69 +1,69 @@
1
  .cnn-editor {
2
- max-width: 700px;
3
- margin: 20px auto;
4
- font-family: sans-serif;
5
  }
6
 
7
  .layers {
8
- display: flex;
9
- flex-direction: column;
10
- gap: 20px;
11
  }
12
 
13
  .layer-card {
14
- border: 1px solid #ccc;
15
- padding: 15px;
16
- border-radius: 10px;
17
- background: #fafafa;
18
  }
19
 
20
  .layer-header {
21
- display: flex;
22
- justify-content: space-between;
23
- margin-bottom: 12px;
24
  }
25
 
26
  .layer-grid {
27
- display: grid;
28
- grid-template-columns: 1fr 1fr;
29
- gap: 10px 20px;
30
  }
31
 
32
  .field {
33
- display: flex;
34
- flex-direction: column;
35
  }
36
 
37
  .field label {
38
- font-size: 12px;
39
- margin-bottom: 4px;
40
- color: #555;
41
  }
42
 
43
  .field input,
44
  .field select {
45
- padding: 5px;
46
- border-radius: 5px;
47
- border: 1px solid #ccc;
48
  }
49
 
50
  .add-btn {
51
- margin-top: 20px;
52
- padding: 12px;
53
- width: 100%;
54
- background: #4caf50;
55
- border: none;
56
- color: white;
57
- font-size: 16px;
58
- border-radius: 8px;
59
- cursor: pointer;
60
  }
61
 
62
  .remove-btn {
63
- background: #ff4444;
64
- border: none;
65
- padding: 6px 10px;
66
- color: white;
67
- border-radius: 5px;
68
- cursor: pointer;
69
  }
 
1
  .cnn-editor {
2
+ max-width: 700px;
3
+ margin: 20px auto;
4
+ font-family: sans-serif;
5
  }
6
 
7
  .layers {
8
+ display: flex;
9
+ flex-direction: column;
10
+ gap: 20px;
11
  }
12
 
13
  .layer-card {
14
+ border: 1px solid #ccc;
15
+ padding: 15px;
16
+ border-radius: 10px;
17
+ background: #fafafa;
18
  }
19
 
20
  .layer-header {
21
+ display: flex;
22
+ justify-content: space-between;
23
+ margin-bottom: 12px;
24
  }
25
 
26
  .layer-grid {
27
+ display: grid;
28
+ grid-template-columns: 1fr 1fr;
29
+ gap: 10px 20px;
30
  }
31
 
32
  .field {
33
+ display: flex;
34
+ flex-direction: column;
35
  }
36
 
37
  .field label {
38
+ font-size: 12px;
39
+ margin-bottom: 4px;
40
+ color: #555;
41
  }
42
 
43
  .field input,
44
  .field select {
45
+ padding: 5px;
46
+ border-radius: 5px;
47
+ border: 1px solid #ccc;
48
  }
49
 
50
  .add-btn {
51
+ margin-top: 20px;
52
+ padding: 12px;
53
+ width: 100%;
54
+ background: #4caf50;
55
+ border: none;
56
+ color: white;
57
+ font-size: 16px;
58
+ border-radius: 8px;
59
+ cursor: pointer;
60
  }
61
 
62
  .remove-btn {
63
+ background: #ff4444;
64
+ border: none;
65
+ padding: 6px 10px;
66
+ color: white;
67
+ border-radius: 5px;
68
+ cursor: pointer;
69
  }
src/components/network/ArchitectureEditor.jsx CHANGED
@@ -1,7 +1,6 @@
1
- import { useState } from "react";
2
- import Button from "../Button/Button.jsx";
3
- import "../../styles.css";
4
-
5
 
6
  function ArchitectureEditor(props) {
7
  const [draft, setDraft] = useState(props.architecture);
@@ -14,14 +13,10 @@ function ArchitectureEditor(props) {
14
  value={draft}
15
  onChange={(e) => setDraft(e.target.value)}
16
  rows={20}
17
- style={{ height: "auto" }}
18
  />
19
  </div>
20
- <Button
21
- onClick={() => props.onChange(draft)}
22
- >
23
- Apply Changes
24
- </Button>
25
  </div>
26
  );
27
  }
 
1
+ import { useState } from 'react';
2
+ import Button from '../Button/Button.jsx';
3
+ import '../../styles.css';
 
4
 
5
  function ArchitectureEditor(props) {
6
  const [draft, setDraft] = useState(props.architecture);
 
13
  value={draft}
14
  onChange={(e) => setDraft(e.target.value)}
15
  rows={20}
16
+ style={{ height: 'auto' }}
17
  />
18
  </div>
19
+ <Button onClick={() => props.onChange(draft)}>Apply Changes</Button>
 
 
 
 
20
  </div>
21
  );
22
  }
src/components/network/Conv2dLayerViewer.jsx CHANGED
@@ -1,6 +1,5 @@
1
- import { useState, useEffect, useMemo } from "react";
2
- import "./LayerViewer.css";
3
-
4
 
5
  function extractKernels(kernels, selectedOutputChannel, kh, kw, inC, outC) {
6
  let minVal = Infinity;
@@ -21,18 +20,19 @@ function extractKernels(kernels, selectedOutputChannel, kh, kw, inC, outC) {
21
 
22
  for (let i = 0; i < kh; ++i) {
23
  for (let j = 0; j < kw; ++j) {
24
- const val = kernels[
25
- i * kw * inC * outC +
26
- j * inC * outC +
27
- ic * outC +
28
- selectedOutputChannel
29
- ];
 
30
  const normVal = (val - minVal) / (maxVal - minVal + 1e-8);
31
 
32
  buffer[(i * kw + j) * 4 + 0] = normVal * 255; // R
33
  buffer[(i * kw + j) * 4 + 1] = normVal * 255; // G
34
  buffer[(i * kw + j) * 4 + 2] = normVal * 255; // B
35
- buffer[(i * kw + j) * 4 + 3] = 255; // A
36
  }
37
  }
38
 
@@ -62,18 +62,19 @@ function extractActivationMaps(output, H, W, C) {
62
 
63
  for (let i = 0; i < H; ++i) {
64
  for (let j = 0; j < W; ++j) {
65
- const val = output[
66
- 0 * H * W * C + // batch index 0
67
- i * W * C +
68
- j * C +
69
- c
70
- ];
 
71
  const normVal = (val - minVal) / (maxVal - minVal + 1e-8);
72
 
73
  buffer[(i * W + j) * 4 + 0] = normVal * 255; // R
74
  buffer[(i * W + j) * 4 + 1] = normVal * 255; // G
75
  buffer[(i * W + j) * 4 + 2] = normVal * 255; // B
76
- buffer[(i * W + j) * 4 + 3] = 255; // A
77
  }
78
  }
79
 
@@ -82,7 +83,7 @@ function extractActivationMaps(output, H, W, C) {
82
  }
83
 
84
  return activationImgDatas;
85
- }
86
 
87
  function imgDataToSrc(imgData) {
88
  const canvas = document.createElement('canvas');
@@ -100,16 +101,16 @@ function Conv2dLayerViewer(props) {
100
  const [batch, H, W, C] = props.outputShape;
101
 
102
  const kernels = useMemo(() => {
103
- if (selectedChannel == null) {
104
- return null;
105
- }
106
  return extractKernels(props.kernels, selectedChannel, kh, kw, inC, outC);
107
  }, [props.tick, selectedChannel]);
108
 
109
  const kernelSrcs = useMemo(() => {
110
- if (kernels == null) {
111
- return null;
112
- }
113
  return kernels.map(imgDataToSrc);
114
  }, [kernels]);
115
 
@@ -126,41 +127,53 @@ function Conv2dLayerViewer(props) {
126
  <h3 className="layer-title">Convolution Layer</h3>
127
 
128
  <div className="layer-details">
129
- <div><strong>Kernel Size:</strong> {kh} × {kw}</div>
130
- <div><strong>Stride:</strong> {props.stride}</div>
131
- <div><strong>Padding:</strong> {props.padding}</div>
132
- <div><strong>Activation:</strong> {props.activationType}</div>
133
- <div><strong>Output channels:</strong> {C}</div>
 
 
 
 
 
 
 
 
 
 
134
  </div>
135
 
136
- {selectedChannel != null && (
137
- <>
138
- <h4>Kernel for output {selectedChannel} (min-max normalized)</h4>
139
- <div className="layer-grid">
140
- {kernelSrcs.map((src, idx) => (
141
- <img
142
- key={`${props.layerIdx}-${idx}-kernel`}
143
- src={src}
144
- alt={`Kernel ${idx}`}
145
- />
146
- ))}
147
- </div>
148
- </>
149
- )}
150
 
151
  <h4>Activation Maps (min-max normalized)</h4>
152
  <div className="layer-grid">
153
- {activationSrcs.map((src, idx) => (
154
- <img
155
- key={`${props.layerIdx}-${idx}-activation`}
156
- src={src}
157
- alt={`Activation Map ${idx}`}
158
- onClick={() =>
159
- setSelectedChannel(selectedChannel === idx ? null : idx)
160
- }
161
- style={{border: selectedChannel === idx ? '4px solid lime' : 'none'}}
162
- />
163
- ))}
 
 
164
  </div>
165
  </div>
166
  );
 
1
+ import { useState, useEffect, useMemo } from 'react';
2
+ import './LayerViewer.css';
 
3
 
4
  function extractKernels(kernels, selectedOutputChannel, kh, kw, inC, outC) {
5
  let minVal = Infinity;
 
20
 
21
  for (let i = 0; i < kh; ++i) {
22
  for (let j = 0; j < kw; ++j) {
23
+ const val =
24
+ kernels[
25
+ i * kw * inC * outC +
26
+ j * inC * outC +
27
+ ic * outC +
28
+ selectedOutputChannel
29
+ ];
30
  const normVal = (val - minVal) / (maxVal - minVal + 1e-8);
31
 
32
  buffer[(i * kw + j) * 4 + 0] = normVal * 255; // R
33
  buffer[(i * kw + j) * 4 + 1] = normVal * 255; // G
34
  buffer[(i * kw + j) * 4 + 2] = normVal * 255; // B
35
+ buffer[(i * kw + j) * 4 + 3] = 255; // A
36
  }
37
  }
38
 
 
62
 
63
  for (let i = 0; i < H; ++i) {
64
  for (let j = 0; j < W; ++j) {
65
+ const val =
66
+ output[
67
+ 0 * H * W * C + // batch index 0
68
+ i * W * C +
69
+ j * C +
70
+ c
71
+ ];
72
  const normVal = (val - minVal) / (maxVal - minVal + 1e-8);
73
 
74
  buffer[(i * W + j) * 4 + 0] = normVal * 255; // R
75
  buffer[(i * W + j) * 4 + 1] = normVal * 255; // G
76
  buffer[(i * W + j) * 4 + 2] = normVal * 255; // B
77
+ buffer[(i * W + j) * 4 + 3] = 255; // A
78
  }
79
  }
80
 
 
83
  }
84
 
85
  return activationImgDatas;
86
+ }
87
 
88
  function imgDataToSrc(imgData) {
89
  const canvas = document.createElement('canvas');
 
101
  const [batch, H, W, C] = props.outputShape;
102
 
103
  const kernels = useMemo(() => {
104
+ if (selectedChannel == null) {
105
+ return null;
106
+ }
107
  return extractKernels(props.kernels, selectedChannel, kh, kw, inC, outC);
108
  }, [props.tick, selectedChannel]);
109
 
110
  const kernelSrcs = useMemo(() => {
111
+ if (kernels == null) {
112
+ return null;
113
+ }
114
  return kernels.map(imgDataToSrc);
115
  }, [kernels]);
116
 
 
127
  <h3 className="layer-title">Convolution Layer</h3>
128
 
129
  <div className="layer-details">
130
+ <div>
131
+ <strong>Kernel Size:</strong> {kh} × {kw}
132
+ </div>
133
+ <div>
134
+ <strong>Stride:</strong> {props.stride}
135
+ </div>
136
+ <div>
137
+ <strong>Padding:</strong> {props.padding}
138
+ </div>
139
+ <div>
140
+ <strong>Activation:</strong> {props.activationType}
141
+ </div>
142
+ <div>
143
+ <strong>Output channels:</strong> {C}
144
+ </div>
145
  </div>
146
 
147
+ {selectedChannel != null && (
148
+ <>
149
+ <h4>Kernel for output {selectedChannel} (min-max normalized)</h4>
150
+ <div className="layer-grid">
151
+ {kernelSrcs.map((src, idx) => (
152
+ <img
153
+ key={`${props.layerIdx}-${idx}-kernel`}
154
+ src={src}
155
+ alt={`Kernel ${idx}`}
156
+ />
157
+ ))}
158
+ </div>
159
+ </>
160
+ )}
161
 
162
  <h4>Activation Maps (min-max normalized)</h4>
163
  <div className="layer-grid">
164
+ {activationSrcs.map((src, idx) => (
165
+ <img
166
+ key={`${props.layerIdx}-${idx}-activation`}
167
+ src={src}
168
+ alt={`Activation Map ${idx}`}
169
+ onClick={() =>
170
+ setSelectedChannel(selectedChannel === idx ? null : idx)
171
+ }
172
+ style={{
173
+ border: selectedChannel === idx ? '4px solid lime' : 'none',
174
+ }}
175
+ />
176
+ ))}
177
  </div>
178
  </div>
179
  );
src/components/network/InfoViewer.jsx CHANGED
@@ -1,71 +1,69 @@
1
- import Conv2dLayerViewer from "./Conv2dLayerViewer.jsx";
2
- import MaxPoolLayerViewer from "./MaxPoolLayerViewer.jsx";
3
- import InputViewer from "./InputViewer.jsx";
4
- import OutputLayerViewer from "./OutputLayerViewer.jsx";
5
 
6
  function InfoViewer(props) {
7
- function renderLayer(layer, idx) {
8
- switch (layer.type) {
9
- case 'input':
10
- return (
11
- <InputViewer
12
- key={idx}
13
- output={layer.output}
14
- shape={layer.shape}
15
  onSampleIndexChange={props.onSampleIndexChange}
16
  tick={props.tick}
17
- />
18
- );
19
- case 'conv2d':
20
- return (
21
- <Conv2dLayerViewer
22
  key={idx}
23
- layerIdx={idx}
24
- stride={layer.stride}
25
- padding={layer.padding}
26
- activationType={layer.activationType}
27
- kernels={layer.kernels}
28
- output={layer.output}
29
- kernelShape={layer.kernelShape}
30
- outputShape={layer.outputShape}
31
- type={layer.type}
32
  tick={props.tick}
33
- />
34
- );
35
- case 'maxpool':
36
- return (
37
- <MaxPoolLayerViewer
38
  key={idx}
39
- stride={layer.stride}
40
- size={layer.size}
41
- output={layer.output}
42
- shape={layer.shape}
43
  tick={props.tick}
44
- />
45
- );
46
- case 'flatten':
47
- return;
48
- case 'dense':
49
- return <p key={idx}>Dense Layer: {layer.details}</p>;
50
- default:
51
- return <p key={idx}>Unknown Layer Type</p>;
52
- }
53
- }
54
 
55
- return (
56
- <div>
57
- {props.info && props.info.slice(0, -1).map((layer, idx) => (
58
- renderLayer(layer, idx)
59
- ))}
60
  {props.info && props.info.length > 0 && (
61
  <OutputLayerViewer
62
  probs={props.info[props.info.length - 1]?.output}
63
  tick={props.tick}
64
  />
65
- )
66
- }
67
- </div>
68
- );
69
  }
70
 
71
  export default InfoViewer;
 
1
+ import Conv2dLayerViewer from './Conv2dLayerViewer.jsx';
2
+ import MaxPoolLayerViewer from './MaxPoolLayerViewer.jsx';
3
+ import InputViewer from './InputViewer.jsx';
4
+ import OutputLayerViewer from './OutputLayerViewer.jsx';
5
 
6
  function InfoViewer(props) {
7
+ function renderLayer(layer, idx) {
8
+ switch (layer.type) {
9
+ case 'input':
10
+ return (
11
+ <InputViewer
12
+ key={idx}
13
+ output={layer.output}
14
+ shape={layer.shape}
15
  onSampleIndexChange={props.onSampleIndexChange}
16
  tick={props.tick}
17
+ />
18
+ );
19
+ case 'conv2d':
20
+ return (
21
+ <Conv2dLayerViewer
22
  key={idx}
23
+ layerIdx={idx}
24
+ stride={layer.stride}
25
+ padding={layer.padding}
26
+ activationType={layer.activationType}
27
+ kernels={layer.kernels}
28
+ output={layer.output}
29
+ kernelShape={layer.kernelShape}
30
+ outputShape={layer.outputShape}
31
+ type={layer.type}
32
  tick={props.tick}
33
+ />
34
+ );
35
+ case 'maxpool':
36
+ return (
37
+ <MaxPoolLayerViewer
38
  key={idx}
39
+ stride={layer.stride}
40
+ size={layer.size}
41
+ output={layer.output}
42
+ shape={layer.shape}
43
  tick={props.tick}
44
+ />
45
+ );
46
+ case 'flatten':
47
+ return;
48
+ case 'dense':
49
+ return <p key={idx}>Dense Layer: {layer.details}</p>;
50
+ default:
51
+ return <p key={idx}>Unknown Layer Type</p>;
52
+ }
53
+ }
54
 
55
+ return (
56
+ <div>
57
+ {props.info &&
58
+ props.info.slice(0, -1).map((layer, idx) => renderLayer(layer, idx))}
 
59
  {props.info && props.info.length > 0 && (
60
  <OutputLayerViewer
61
  probs={props.info[props.info.length - 1]?.output}
62
  tick={props.tick}
63
  />
64
+ )}
65
+ </div>
66
+ );
 
67
  }
68
 
69
  export default InfoViewer;
src/components/network/InputViewer.jsx CHANGED
@@ -1,18 +1,18 @@
1
- import Button from "../Button/Button.jsx";
2
- import "./LayerViewer.css";
3
-
4
 
5
  function extractImage(output, H, W, C) {
6
  const buffer = new Uint8ClampedArray(H * W * 4);
7
  for (let i = 0; i < H; ++i) {
8
  for (let j = 0; j < W; ++j) {
9
  for (let c = 0; c < C; ++c) {
10
- const val = output[
11
- 0 * H * W * C + // batch index 0
12
- i * W * C +
13
- j * C +
14
- c
15
- ];
 
16
  buffer[(i * W + j) * 4 + c] = val * 255;
17
  }
18
  for (let c = C; c < 3; ++c) {
@@ -25,7 +25,6 @@ function extractImage(output, H, W, C) {
25
  return new ImageData(buffer, W, H);
26
  }
27
 
28
-
29
  function imgDataToSrc(imgData) {
30
  const canvas = document.createElement('canvas');
31
  canvas.width = imgData.width;
@@ -35,31 +34,29 @@ function imgDataToSrc(imgData) {
35
  return canvas.toDataURL();
36
  }
37
 
38
-
39
  function InputViewer(props) {
40
- const [batchSize, H, W, C] = props.shape;
41
 
42
- const imgData = extractImage(props.output, H, W, C);
43
- const imgSrc = imgDataToSrc(imgData);
44
 
45
- return (
46
- <div className="layer-viewer">
47
- <h3 className="layer-title">Input Layer</h3>
48
 
49
- <div className="layer-details">
50
- <div><strong>Input size:</strong> {imgData.width} x {imgData.height}</div>
51
- </div>
 
 
52
 
53
- <h4>Sample input</h4>
54
- <div className="layer-grid">
55
- {imgSrc && (
56
- <img key={0} src={imgSrc} alt={`Input Image`} />
57
- )}
58
  <Button onClick={props.onSampleIndexChange}>New Sample</Button>
59
- </div>
60
- </div>
61
- );
62
  }
63
 
64
  export default InputViewer;
65
-
 
1
+ import Button from '../Button/Button.jsx';
2
+ import './LayerViewer.css';
 
3
 
4
  function extractImage(output, H, W, C) {
5
  const buffer = new Uint8ClampedArray(H * W * 4);
6
  for (let i = 0; i < H; ++i) {
7
  for (let j = 0; j < W; ++j) {
8
  for (let c = 0; c < C; ++c) {
9
+ const val =
10
+ output[
11
+ 0 * H * W * C + // batch index 0
12
+ i * W * C +
13
+ j * C +
14
+ c
15
+ ];
16
  buffer[(i * W + j) * 4 + c] = val * 255;
17
  }
18
  for (let c = C; c < 3; ++c) {
 
25
  return new ImageData(buffer, W, H);
26
  }
27
 
 
28
  function imgDataToSrc(imgData) {
29
  const canvas = document.createElement('canvas');
30
  canvas.width = imgData.width;
 
34
  return canvas.toDataURL();
35
  }
36
 
 
37
  function InputViewer(props) {
38
+ const [batchSize, H, W, C] = props.shape;
39
 
40
+ const imgData = extractImage(props.output, H, W, C);
41
+ const imgSrc = imgDataToSrc(imgData);
42
 
43
+ return (
44
+ <div className="layer-viewer">
45
+ <h3 className="layer-title">Input Layer</h3>
46
 
47
+ <div className="layer-details">
48
+ <div>
49
+ <strong>Input size:</strong> {imgData.width} x {imgData.height}
50
+ </div>
51
+ </div>
52
 
53
+ <h4>Sample input</h4>
54
+ <div className="layer-grid">
55
+ {imgSrc && <img key={0} src={imgSrc} alt={`Input Image`} />}
 
 
56
  <Button onClick={props.onSampleIndexChange}>New Sample</Button>
57
+ </div>
58
+ </div>
59
+ );
60
  }
61
 
62
  export default InputViewer;
 
src/components/network/LayerViewer.css CHANGED
@@ -13,8 +13,8 @@
13
  }
14
 
15
  .layer-details {
16
- display: flex;
17
- justify-content: space-evenly;
18
  margin-bottom: 12px;
19
  color: #444;
20
  font-size: 14px;
@@ -27,7 +27,7 @@
27
  }
28
 
29
  .layer-grid img {
30
- width: 100px;
31
- height: 100px;
32
- image-rendering: pixelated;
33
  }
 
13
  }
14
 
15
  .layer-details {
16
+ display: flex;
17
+ justify-content: space-evenly;
18
  margin-bottom: 12px;
19
  color: #444;
20
  font-size: 14px;
 
27
  }
28
 
29
  .layer-grid img {
30
+ width: 100px;
31
+ height: 100px;
32
+ image-rendering: pixelated;
33
  }
src/components/network/MaxPoolLayerViewer.jsx CHANGED
@@ -1,89 +1,93 @@
1
- import { useMemo } from "react";
2
- import "./LayerViewer.css";
3
-
4
 
5
  function extractActivationMaps(activations, H, W, C) {
6
- let minVal = Infinity;
7
- let maxVal = -Infinity;
8
- for (let i = 0; i < activations.length; ++i) {
9
- const val = activations[i];
10
- if (val < minVal) {
11
- minVal = val;
12
- }
13
- if (val > maxVal) {
14
- maxVal = val;
15
- }
16
- }
17
 
18
- let activationImgDatas = [];
19
- for (let c = 0; c < C; ++c) {
20
- const buffer = new Uint8ClampedArray(H * W * 4);
21
 
22
- for (let i = 0; i < H; ++i) {
23
- for (let j = 0; j < W; ++j) {
24
- const val = activations[
25
- 0 * H * W * C + // batch index 0
26
- i * W * C +
27
- j * C +
28
- c
29
- ];
30
- const normVal = (val - minVal) / (maxVal - minVal + 1e-8);
 
31
 
32
- buffer[(i * W + j) * 4 + 0] = normVal * 255; // R
33
- buffer[(i * W + j) * 4 + 1] = normVal * 255; // G
34
- buffer[(i * W + j) * 4 + 2] = normVal * 255; // B
35
- buffer[(i * W + j) * 4 + 3] = 255; // A
36
- }
37
- }
38
 
39
- const imgData = new ImageData(buffer, W, H);
40
- activationImgDatas.push(imgData);
41
- }
42
 
43
- return activationImgDatas;
44
  }
45
 
46
  function imgDataToSrc(imgData) {
47
- const canvas = document.createElement('canvas');
48
- canvas.width = imgData.width;
49
- canvas.height = imgData.height;
50
- const ctx = canvas.getContext('2d');
51
- ctx.putImageData(imgData, 0, 0);
52
- return canvas.toDataURL();
53
  }
54
 
55
  function MaxPoolLayerViewer(props) {
56
- const [batchSize, H, W, C] = props.shape;
57
 
58
- const activations = useMemo(() => {
59
  return extractActivationMaps(props.output, H, W, C);
60
  }, [props.output]);
61
 
62
- const activationSrcs = useMemo(() => {
63
  return activations.map(imgDataToSrc);
64
  }, [activations]);
65
 
66
- return (
67
- <div className="layer-viewer">
68
- <h3 className="layer-title">MaxPool Layer</h3>
69
 
70
  <div className="layer-details">
71
- <div><strong>Pool Size:</strong> {props.size}x{props.size}</div>
72
- <div><strong>Stride:</strong> {props.stride}</div>
 
 
 
 
73
  </div>
74
 
75
  <h4>Outputs (min-max normalized)</h4>
76
  <div className="layer-grid">
77
- {activationSrcs.map((src, idx) => (
78
- <img
79
- key={`${props.key}-${idx}-output`}
80
- src={src}
81
- alt={`Output ${idx}`}
82
- />
83
- ))}
84
  </div>
85
  </div>
86
- );
87
  }
88
 
89
  export default MaxPoolLayerViewer;
 
1
+ import { useMemo } from 'react';
2
+ import './LayerViewer.css';
 
3
 
4
  function extractActivationMaps(activations, H, W, C) {
5
+ let minVal = Infinity;
6
+ let maxVal = -Infinity;
7
+ for (let i = 0; i < activations.length; ++i) {
8
+ const val = activations[i];
9
+ if (val < minVal) {
10
+ minVal = val;
11
+ }
12
+ if (val > maxVal) {
13
+ maxVal = val;
14
+ }
15
+ }
16
 
17
+ let activationImgDatas = [];
18
+ for (let c = 0; c < C; ++c) {
19
+ const buffer = new Uint8ClampedArray(H * W * 4);
20
 
21
+ for (let i = 0; i < H; ++i) {
22
+ for (let j = 0; j < W; ++j) {
23
+ const val =
24
+ activations[
25
+ 0 * H * W * C + // batch index 0
26
+ i * W * C +
27
+ j * C +
28
+ c
29
+ ];
30
+ const normVal = (val - minVal) / (maxVal - minVal + 1e-8);
31
 
32
+ buffer[(i * W + j) * 4 + 0] = normVal * 255; // R
33
+ buffer[(i * W + j) * 4 + 1] = normVal * 255; // G
34
+ buffer[(i * W + j) * 4 + 2] = normVal * 255; // B
35
+ buffer[(i * W + j) * 4 + 3] = 255; // A
36
+ }
37
+ }
38
 
39
+ const imgData = new ImageData(buffer, W, H);
40
+ activationImgDatas.push(imgData);
41
+ }
42
 
43
+ return activationImgDatas;
44
  }
45
 
46
  function imgDataToSrc(imgData) {
47
+ const canvas = document.createElement('canvas');
48
+ canvas.width = imgData.width;
49
+ canvas.height = imgData.height;
50
+ const ctx = canvas.getContext('2d');
51
+ ctx.putImageData(imgData, 0, 0);
52
+ return canvas.toDataURL();
53
  }
54
 
55
  function MaxPoolLayerViewer(props) {
56
+ const [batchSize, H, W, C] = props.shape;
57
 
58
+ const activations = useMemo(() => {
59
  return extractActivationMaps(props.output, H, W, C);
60
  }, [props.output]);
61
 
62
+ const activationSrcs = useMemo(() => {
63
  return activations.map(imgDataToSrc);
64
  }, [activations]);
65
 
66
+ return (
67
+ <div className="layer-viewer">
68
+ <h3 className="layer-title">MaxPool Layer</h3>
69
 
70
  <div className="layer-details">
71
+ <div>
72
+ <strong>Pool Size:</strong> {props.size}x{props.size}
73
+ </div>
74
+ <div>
75
+ <strong>Stride:</strong> {props.stride}
76
+ </div>
77
  </div>
78
 
79
  <h4>Outputs (min-max normalized)</h4>
80
  <div className="layer-grid">
81
+ {activationSrcs.map((src, idx) => (
82
+ <img
83
+ key={`${props.key}-${idx}-output`}
84
+ src={src}
85
+ alt={`Output ${idx}`}
86
+ />
87
+ ))}
88
  </div>
89
  </div>
90
+ );
91
  }
92
 
93
  export default MaxPoolLayerViewer;
src/components/network/NetworkViewer.jsx CHANGED
@@ -4,27 +4,23 @@ import Plot from 'react-plotly.js';
4
  function NetworkViewer(props) {
5
  return (
6
  <div>
7
- <p>Training status: {
8
- props.isTraining
9
- ? "Running"
10
- : "Not running"
11
- }</p>
12
- <Plot
13
- data={[
14
- {
15
- x: props.losses.map((_, i) => i),
16
- y: props.losses.map(l => l.loss),
17
- mode: "lines",
18
- type: "scatter"
19
- }
20
- ]}
21
- layout={{
22
- xaxis: { title: { text: "Training steps" } },
23
- yaxis: { title: { text: "Train loss" } },
24
- }}
25
- />
26
- <InfoViewer
27
- info={props.info}
28
  onSampleIndexChange={props.onSampleIndexChange}
29
  tick={props.tick}
30
  />
 
4
  function NetworkViewer(props) {
5
  return (
6
  <div>
7
+ <p>Training status: {props.isTraining ? 'Running' : 'Not running'}</p>
8
+ <Plot
9
+ data={[
10
+ {
11
+ x: props.losses.map((_, i) => i),
12
+ y: props.losses.map((l) => l.loss),
13
+ mode: 'lines',
14
+ type: 'scatter',
15
+ },
16
+ ]}
17
+ layout={{
18
+ xaxis: { title: { text: 'Training steps' } },
19
+ yaxis: { title: { text: 'Train loss' } },
20
+ }}
21
+ />
22
+ <InfoViewer
23
+ info={props.info}
 
 
 
 
24
  onSampleIndexChange={props.onSampleIndexChange}
25
  tick={props.tick}
26
  />
src/components/network/NetworkVisualizer.jsx CHANGED
@@ -1,25 +1,22 @@
1
  import * as tf from '@tensorflow/tfjs';
2
  import { useState, useEffect, useRef } from 'react';
3
- import NetworkViewer from "./NetworkViewer.jsx";
4
- import Sidebar from "./Sidebar.jsx";
5
- import { load, train, Cnn } from "./train.js";
6
  import './NetworkVisualizer.css';
7
 
8
- const DEFAULT_ARCHITECTURE = (
9
- `[conv2d filters=8 kernel=11
10
  stride=1 padding=1 activation=relu]
11
 
12
  [maxpool size=2 stride=2]
13
 
14
  [flatten]
15
 
16
- [dense units=10 activation=softmax]`
17
- );
18
-
19
 
20
  function parseValue(raw) {
21
  // Try to parse as number; if NaN, keep as string
22
- if (raw.trim() === "") {
23
  return raw;
24
  }
25
 
@@ -51,15 +48,15 @@ function parseArchitecture(text) {
51
 
52
  for (let i = 1; i < tokens.length; ++i) {
53
  const token = tokens[i];
54
- const [rawKey, rawValue] = token.split("=", 2);
55
 
56
  if (!rawKey || rawValue === undefined) continue;
57
 
58
  let key = rawKey;
59
  let value = parseValue(rawValue);
60
 
61
- if (key === "activation") {
62
- key = "activationType";
63
  }
64
 
65
  layer[key] = value;
@@ -76,151 +73,149 @@ function NetworkVisualizer() {
76
  const [tick, setTick] = useState(0);
77
 
78
  // dataset states
79
- const [dataset, setDataset] = useState("mnist");
80
  const [imageSize, setImageSize] = useState(28);
81
  const [numClasses, setNumClasses] = useState(10);
82
- const [data, setData] = useState(null);
83
 
84
  // optimization states
85
- const [optimizerType, setOptimizerType] = useState("adam");
86
- const [optimizerParams, setOptimizerParams] = useState(
87
- {
88
- learningRate: "0.001",
89
- beta1: "0.9",
90
- beta2: "0.999",
91
- epsilon: "1e-8",
92
- batchSize: "32",
93
- epochs: "5",
94
- }
95
- );
96
-
97
  // architecture states
98
  const [architecture, setArchitecture] = useState(DEFAULT_ARCHITECTURE);
99
 
100
  // training states
101
- const [isTraining, setIsTraining] = useState(false);
102
  const lossesRef = useRef([]);
103
- const trainController = useRef({
104
- isPaused: false,
105
- stopRequested: false,
106
- sampleIndex: 0,
107
- });
108
  const modelRef = useRef(null);
109
  const optimizerRef = useRef(null);
110
 
111
- // training visualization
112
  const infoRef = useRef([]);
113
 
114
- // init dataset
115
- useEffect(() => {
116
- async function loadData() {
117
- const d = await load(dataset);
118
- setData(d);
119
- }
120
- loadData();
121
- }, [dataset]);
122
-
123
- // init model
124
- useEffect(() => {
125
- resetModel();
126
  resetOptimizer();
127
- }, [architecture, data, optimizerType, optimizerParams]);
128
-
129
- function handleStartTraining() {
130
- console.log("Starting training...");
131
- // trainController updated in startTraining
132
- startTraining();
133
- }
134
-
135
- function handlePauseTraining() {
136
- console.log("Pausing training...");
137
- trainController.current.isPaused = true;
138
- }
139
-
140
- function handleContinueTraining() {
141
- console.log("Continuing training...");
142
- trainController.current.isPaused = false;
143
- }
144
-
145
- function handleStopTraining() {
146
- console.log("Stopping training...");
147
- trainController.current.stopRequested = true;
148
- trainController.current.isPaused = false;
149
- }
150
-
151
- async function waitUntilNotTraining() {
152
- return new Promise(resolve => {
153
- function check() {
154
- if (!isTraining) {
155
- resolve();
156
- } else {
157
- requestAnimationFrame(check);
158
- }
159
- }
160
- check();
161
- });
162
- }
163
-
164
- async function handleResetTraining() {
165
- console.log("Resetting training...");
166
- handleStopTraining();
167
-
168
- await waitUntilNotTraining();
169
- console.log("Training stopped. Resetting model.");
170
  lossesRef.current = [];
171
  infoRef.current = [];
172
- resetModel();
173
  resetOptimizer();
174
- }
175
 
176
- function resetModel() {
177
- if (!data) {
178
- return;
179
- }
180
 
181
  if (modelRef.current) {
182
  modelRef.current.dispose();
183
  }
184
 
185
- const inChannels = data.numInputChannels;
186
- const cnn = new Cnn(parseArchitecture(architecture), inChannels);
187
  modelRef.current = cnn;
188
  updateTick();
189
- }
190
 
191
  function resetOptimizer() {
192
- if (optimizerType === "adam") {
193
  const learningRate = parseFloat(optimizerParams.learningRate);
194
  const beta1 = parseFloat(optimizerParams.beta1);
195
  const beta2 = parseFloat(optimizerParams.beta2);
196
  const epsilon = parseFloat(optimizerParams.epsilon);
197
 
198
  if (Number.isNaN(learningRate) || learningRate <= 0) {
199
- alert("Invalid learning rate for Adam optimizer.");
200
  return;
201
  }
202
  if (Number.isNaN(beta1) || beta1 < 0) {
203
- alert("Invalid beta1 for Adam optimizer.");
204
  return;
205
  }
206
  if (Number.isNaN(beta2) || beta2 < 0) {
207
- alert("Invalid beta2 for Adam optimizer.");
208
  return;
209
  }
210
  if (Number.isNaN(epsilon) || epsilon <= 0) {
211
- alert("Invalid epsilon for Adam optimizer.");
212
  return;
213
- }
214
 
215
  const opt = tf.train.adam(learningRate, beta1, beta2, epsilon);
216
  if (optimizerRef.current) {
217
  optimizerRef.current.dispose();
218
  }
219
  optimizerRef.current = opt;
220
- } else if (optimizerType === "sgd") {
221
  const learningRate = parseFloat(optimizerParams.learningRate);
222
  if (Number.isNaN(learningRate) || learningRate <= 0) {
223
- alert("Invalid learning rate for SGD optimizer.");
224
  return;
225
  }
226
 
@@ -234,42 +229,39 @@ function NetworkVisualizer() {
234
  }
235
  }
236
 
237
- async function startTraining() {
238
- if (!modelRef || !data || !optimizerRef || isTraining) {
239
- return;
240
- }
241
 
242
- setIsTraining(true);
243
- trainController.current.isPaused = false;
244
- trainController.current.stopRequested = false;
245
 
246
  const batchSize = parseFloat(optimizerParams.batchSize);
247
  if (Number.isNaN(batchSize) || batchSize <= 0) {
248
- alert("Invalid batch size.");
249
  setIsTraining(false);
250
  return;
251
  }
252
  const epochs = parseFloat(optimizerParams.epochs);
253
  if (Number.isNaN(epochs) || epochs <= 0) {
254
- alert("Invalid number of epochs.");
255
  setIsTraining(false);
256
  return;
257
  }
258
 
259
  let lastTickUpdate = 0;
260
- try {
261
- await train(
262
- data,
263
- modelRef.current,
264
- optimizerRef.current,
265
  batchSize,
266
  epochs,
267
- trainController.current,
268
  (epoch, batch, loss, info) => {
269
-
270
- lossesRef.current.push(
271
- { epoch, batch, loss }
272
- );
273
 
274
  infoRef.current = info;
275
 
@@ -279,19 +271,19 @@ function NetworkVisualizer() {
279
  lastTickUpdate = now;
280
  updateTick();
281
  }
282
- }
283
- );
284
- } finally {
285
- setIsTraining(false);
286
- trainController.current.isPaused = false;
287
- trainController.current.stopRequested = false;
288
- alert("Training finished.");
289
- }
290
- }
291
 
292
  function handleArchitectureChange(newArchitecture) {
293
  if (isTraining) {
294
- alert("Cannot change architecture while training is in progress.");
295
  } else {
296
  setArchitecture(newArchitecture);
297
  }
@@ -299,7 +291,7 @@ function NetworkVisualizer() {
299
 
300
  function handleOptimizerChange(newOptimizerType, newOptimizerParams) {
301
  if (isTraining) {
302
- alert("Cannot change optimizer settings while training is in progress.");
303
  } else {
304
  setOptimizerType(newOptimizerType);
305
  setOptimizerParams(newOptimizerParams);
@@ -312,33 +304,33 @@ function NetworkVisualizer() {
312
  }
313
 
314
  function updateTick() {
315
- setTick(tick => tick + 1);
316
  }
317
 
318
  return (
319
  <div className="network-visualizer">
320
- <NetworkViewer
321
- losses={lossesRef.current}
322
- info={infoRef.current}
323
- isTraining={isTraining}
324
  onSampleIndexChange={handleSampleIndexChange}
325
  tick={tick}
326
- />
327
- <Sidebar
328
- onStartTraining={handleStartTraining}
329
- onPauseTraining={handlePauseTraining}
330
- onContinueTraining={handleContinueTraining}
331
- onStopTraining={handleStopTraining}
332
- onResetTraining={handleResetTraining}
333
- isTraining={isTraining}
334
- isPaused={trainController.current.isPaused}
335
- architecture={architecture}
336
- onArchitectureChange={handleArchitectureChange}
337
  optimizerType={optimizerType}
338
  optimizerParams={optimizerParams}
339
  onOptimizerChange={handleOptimizerChange}
340
  tick={tick}
341
- />
342
  </div>
343
  );
344
  }
 
1
  import * as tf from '@tensorflow/tfjs';
2
  import { useState, useEffect, useRef } from 'react';
3
+ import NetworkViewer from './NetworkViewer.jsx';
4
+ import Sidebar from './Sidebar.jsx';
5
+ import { load, train, Cnn } from './train.js';
6
  import './NetworkVisualizer.css';
7
 
8
+ const DEFAULT_ARCHITECTURE = `[conv2d filters=8 kernel=11
 
9
  stride=1 padding=1 activation=relu]
10
 
11
  [maxpool size=2 stride=2]
12
 
13
  [flatten]
14
 
15
+ [dense units=10 activation=softmax]`;
 
 
16
 
17
  function parseValue(raw) {
18
  // Try to parse as number; if NaN, keep as string
19
+ if (raw.trim() === '') {
20
  return raw;
21
  }
22
 
 
48
 
49
  for (let i = 1; i < tokens.length; ++i) {
50
  const token = tokens[i];
51
+ const [rawKey, rawValue] = token.split('=', 2);
52
 
53
  if (!rawKey || rawValue === undefined) continue;
54
 
55
  let key = rawKey;
56
  let value = parseValue(rawValue);
57
 
58
+ if (key === 'activation') {
59
+ key = 'activationType';
60
  }
61
 
62
  layer[key] = value;
 
73
  const [tick, setTick] = useState(0);
74
 
75
  // dataset states
76
+ const [dataset, setDataset] = useState('mnist');
77
  const [imageSize, setImageSize] = useState(28);
78
  const [numClasses, setNumClasses] = useState(10);
79
+ const [data, setData] = useState(null);
80
 
81
  // optimization states
82
+ const [optimizerType, setOptimizerType] = useState('adam');
83
+ const [optimizerParams, setOptimizerParams] = useState({
84
+ learningRate: '0.001',
85
+ beta1: '0.9',
86
+ beta2: '0.999',
87
+ epsilon: '1e-8',
88
+ batchSize: '32',
89
+ epochs: '5',
90
+ });
91
+
 
 
92
  // architecture states
93
  const [architecture, setArchitecture] = useState(DEFAULT_ARCHITECTURE);
94
 
95
  // training states
96
+ const [isTraining, setIsTraining] = useState(false);
97
  const lossesRef = useRef([]);
98
+ const trainController = useRef({
99
+ isPaused: false,
100
+ stopRequested: false,
101
+ sampleIndex: 0,
102
+ });
103
  const modelRef = useRef(null);
104
  const optimizerRef = useRef(null);
105
 
106
+ // training visualization
107
  const infoRef = useRef([]);
108
 
109
+ // init dataset
110
+ useEffect(() => {
111
+ async function loadData() {
112
+ const d = await load(dataset);
113
+ setData(d);
114
+ }
115
+ loadData();
116
+ }, [dataset]);
117
+
118
+ // init model
119
+ useEffect(() => {
120
+ resetModel();
121
  resetOptimizer();
122
+ }, [architecture, data, optimizerType, optimizerParams]);
123
+
124
+ function handleStartTraining() {
125
+ console.log('Starting training...');
126
+ // trainController updated in startTraining
127
+ startTraining();
128
+ }
129
+
130
+ function handlePauseTraining() {
131
+ console.log('Pausing training...');
132
+ trainController.current.isPaused = true;
133
+ }
134
+
135
+ function handleContinueTraining() {
136
+ console.log('Continuing training...');
137
+ trainController.current.isPaused = false;
138
+ }
139
+
140
+ function handleStopTraining() {
141
+ console.log('Stopping training...');
142
+ trainController.current.stopRequested = true;
143
+ trainController.current.isPaused = false;
144
+ }
145
+
146
+ async function waitUntilNotTraining() {
147
+ return new Promise((resolve) => {
148
+ function check() {
149
+ if (!isTraining) {
150
+ resolve();
151
+ } else {
152
+ requestAnimationFrame(check);
153
+ }
154
+ }
155
+ check();
156
+ });
157
+ }
158
+
159
+ async function handleResetTraining() {
160
+ console.log('Resetting training...');
161
+ handleStopTraining();
162
+
163
+ await waitUntilNotTraining();
164
+ console.log('Training stopped. Resetting model.');
165
  lossesRef.current = [];
166
  infoRef.current = [];
167
+ resetModel();
168
  resetOptimizer();
169
+ }
170
 
171
+ function resetModel() {
172
+ if (!data) {
173
+ return;
174
+ }
175
 
176
  if (modelRef.current) {
177
  modelRef.current.dispose();
178
  }
179
 
180
+ const inChannels = data.numInputChannels;
181
+ const cnn = new Cnn(parseArchitecture(architecture), inChannels);
182
  modelRef.current = cnn;
183
  updateTick();
184
+ }
185
 
186
  function resetOptimizer() {
187
+ if (optimizerType === 'adam') {
188
  const learningRate = parseFloat(optimizerParams.learningRate);
189
  const beta1 = parseFloat(optimizerParams.beta1);
190
  const beta2 = parseFloat(optimizerParams.beta2);
191
  const epsilon = parseFloat(optimizerParams.epsilon);
192
 
193
  if (Number.isNaN(learningRate) || learningRate <= 0) {
194
+ alert('Invalid learning rate for Adam optimizer.');
195
  return;
196
  }
197
  if (Number.isNaN(beta1) || beta1 < 0) {
198
+ alert('Invalid beta1 for Adam optimizer.');
199
  return;
200
  }
201
  if (Number.isNaN(beta2) || beta2 < 0) {
202
+ alert('Invalid beta2 for Adam optimizer.');
203
  return;
204
  }
205
  if (Number.isNaN(epsilon) || epsilon <= 0) {
206
+ alert('Invalid epsilon for Adam optimizer.');
207
  return;
208
+ }
209
 
210
  const opt = tf.train.adam(learningRate, beta1, beta2, epsilon);
211
  if (optimizerRef.current) {
212
  optimizerRef.current.dispose();
213
  }
214
  optimizerRef.current = opt;
215
+ } else if (optimizerType === 'sgd') {
216
  const learningRate = parseFloat(optimizerParams.learningRate);
217
  if (Number.isNaN(learningRate) || learningRate <= 0) {
218
+ alert('Invalid learning rate for SGD optimizer.');
219
  return;
220
  }
221
 
 
229
  }
230
  }
231
 
232
+ async function startTraining() {
233
+ if (!modelRef || !data || !optimizerRef || isTraining) {
234
+ return;
235
+ }
236
 
237
+ setIsTraining(true);
238
+ trainController.current.isPaused = false;
239
+ trainController.current.stopRequested = false;
240
 
241
  const batchSize = parseFloat(optimizerParams.batchSize);
242
  if (Number.isNaN(batchSize) || batchSize <= 0) {
243
+ alert('Invalid batch size.');
244
  setIsTraining(false);
245
  return;
246
  }
247
  const epochs = parseFloat(optimizerParams.epochs);
248
  if (Number.isNaN(epochs) || epochs <= 0) {
249
+ alert('Invalid number of epochs.');
250
  setIsTraining(false);
251
  return;
252
  }
253
 
254
  let lastTickUpdate = 0;
255
+ try {
256
+ await train(
257
+ data,
258
+ modelRef.current,
259
+ optimizerRef.current,
260
  batchSize,
261
  epochs,
262
+ trainController.current,
263
  (epoch, batch, loss, info) => {
264
+ lossesRef.current.push({ epoch, batch, loss });
 
 
 
265
 
266
  infoRef.current = info;
267
 
 
271
  lastTickUpdate = now;
272
  updateTick();
273
  }
274
+ },
275
+ );
276
+ } finally {
277
+ setIsTraining(false);
278
+ trainController.current.isPaused = false;
279
+ trainController.current.stopRequested = false;
280
+ alert('Training finished.');
281
+ }
282
+ }
283
 
284
  function handleArchitectureChange(newArchitecture) {
285
  if (isTraining) {
286
+ alert('Cannot change architecture while training is in progress.');
287
  } else {
288
  setArchitecture(newArchitecture);
289
  }
 
291
 
292
  function handleOptimizerChange(newOptimizerType, newOptimizerParams) {
293
  if (isTraining) {
294
+ alert('Cannot change optimizer settings while training is in progress.');
295
  } else {
296
  setOptimizerType(newOptimizerType);
297
  setOptimizerParams(newOptimizerParams);
 
304
  }
305
 
306
  function updateTick() {
307
+ setTick((tick) => tick + 1);
308
  }
309
 
310
  return (
311
  <div className="network-visualizer">
312
+ <NetworkViewer
313
+ losses={lossesRef.current}
314
+ info={infoRef.current}
315
+ isTraining={isTraining}
316
  onSampleIndexChange={handleSampleIndexChange}
317
  tick={tick}
318
+ />
319
+ <Sidebar
320
+ onStartTraining={handleStartTraining}
321
+ onPauseTraining={handlePauseTraining}
322
+ onContinueTraining={handleContinueTraining}
323
+ onStopTraining={handleStopTraining}
324
+ onResetTraining={handleResetTraining}
325
+ isTraining={isTraining}
326
+ isPaused={trainController.current.isPaused}
327
+ architecture={architecture}
328
+ onArchitectureChange={handleArchitectureChange}
329
  optimizerType={optimizerType}
330
  optimizerParams={optimizerParams}
331
  onOptimizerChange={handleOptimizerChange}
332
  tick={tick}
333
+ />
334
  </div>
335
  );
336
  }
src/components/network/OptimizerSettings.jsx CHANGED
@@ -1,22 +1,21 @@
1
- import "../../styles.css";
2
 
3
  const DEFAULT_PARAMS = {
4
  sgd: {
5
- learningRate: "0.01",
6
- momentum: "0.0",
7
- batchSize: "32",
8
- epochs: "5",
9
  },
10
  adam: {
11
- learningRate: "0.001",
12
- beta1: "0.9",
13
- beta2: "0.999",
14
- epsilon: "1e-8",
15
- batchSize: "32",
16
- epochs: "5",
17
- }
18
- }
19
-
20
 
21
  function handleOptimizerChange(event, onChange) {
22
  const selectedOptimizer = event.target.value;
@@ -31,7 +30,6 @@ function handleParamChange(param, value, oldParams, optimizerType, onChange) {
31
  onChange(optimizerType, newParams);
32
  }
33
 
34
-
35
  function OptimizerSettings(props) {
36
  return (
37
  <div className="card">
@@ -46,7 +44,7 @@ function OptimizerSettings(props) {
46
  <option value="adam">Adam</option>
47
  </select>
48
  </div>
49
- {props.optimizer === "sgd" && (
50
  <>
51
  <div className="field">
52
  <label htmlFor="sgd-learning-rate-input">Learning Rate</label>
@@ -54,9 +52,15 @@ function OptimizerSettings(props) {
54
  id="sgd-learning-rate-input"
55
  type="text"
56
  value={props.params.learningRate}
57
- onChange={(e) => handleParamChange(
58
- 'learningRate', e.target.value, props.params, props.optimizer, props.onOptimizerChange
59
- )}
 
 
 
 
 
 
60
  />
61
  </div>
62
  <div className="field">
@@ -65,9 +69,15 @@ function OptimizerSettings(props) {
65
  id="sgd-momentum-input"
66
  type="text"
67
  value={props.params.momentum}
68
- onChange={(e) => handleParamChange(
69
- 'momentum', e.target.value, props.params, props.optimizer, props.onOptimizerChange
70
- )}
 
 
 
 
 
 
71
  />
72
  </div>
73
  <div className="field">
@@ -76,9 +86,15 @@ function OptimizerSettings(props) {
76
  id="sgd-batch-size-input"
77
  type="text"
78
  value={props.params.batchSize}
79
- onChange={(e) => handleParamChange(
80
- 'batchSize', e.target.value, props.params, props.optimizer, props.onOptimizerChange
81
- )}
 
 
 
 
 
 
82
  />
83
  </div>
84
  <div className="field">
@@ -87,14 +103,20 @@ function OptimizerSettings(props) {
87
  id="sgd-epochs-input"
88
  type="text"
89
  value={props.params.epochs}
90
- onChange={(e) => handleParamChange(
91
- 'epochs', e.target.value, props.params, props.optimizer, props.onOptimizerChange
92
- )}
 
 
 
 
 
 
93
  />
94
- </div>
95
  </>
96
  )}
97
- {props.optimizer === "adam" && (
98
  <>
99
  <div className="field">
100
  <label htmlFor="adam-learning-rate-input">Learning Rate</label>
@@ -102,9 +124,15 @@ function OptimizerSettings(props) {
102
  id="adam-learning-rate-input"
103
  type="text"
104
  value={props.params.learningRate}
105
- onChange={(e) => handleParamChange(
106
- 'learningRate', e.target.value, props.params, props.optimizer, props.onOptimizerChange
107
- )}
 
 
 
 
 
 
108
  />
109
  </div>
110
  <div className="field">
@@ -113,9 +141,15 @@ function OptimizerSettings(props) {
113
  id="adam-beta1-input"
114
  type="text"
115
  value={props.params.beta1}
116
- onChange={(e) => handleParamChange(
117
- 'beta1', e.target.value, props.params, props.optimizer, props.onOptimizerChange
118
- )}
 
 
 
 
 
 
119
  />
120
  </div>
121
  <div className="field">
@@ -124,9 +158,15 @@ function OptimizerSettings(props) {
124
  id="adam-beta2-input"
125
  type="text"
126
  value={props.params.beta2}
127
- onChange={(e) => handleParamChange(
128
- 'beta2', e.target.value, props.params, props.optimizer, props.onOptimizerChange
129
- )}
 
 
 
 
 
 
130
  />
131
  </div>
132
  <div className="field">
@@ -135,9 +175,15 @@ function OptimizerSettings(props) {
135
  id="adam-epsilon-input"
136
  type="text"
137
  value={props.params.epsilon}
138
- onChange={(e) => handleParamChange(
139
- 'epsilon', e.target.value, props.params, props.optimizer, props.onOptimizerChange
140
- )}
 
 
 
 
 
 
141
  />
142
  </div>
143
  <div className="field">
@@ -146,9 +192,15 @@ function OptimizerSettings(props) {
146
  id="adam-batch-size-input"
147
  type="text"
148
  value={props.params.batchSize}
149
- onChange={(e) => handleParamChange(
150
- 'batchSize', e.target.value, props.params, props.optimizer, props.onOptimizerChange
151
- )}
 
 
 
 
 
 
152
  />
153
  </div>
154
  <div className="field">
@@ -157,37 +209,33 @@ function OptimizerSettings(props) {
157
  id="adam-epochs-input"
158
  type="text"
159
  value={props.params.epochs}
160
- onChange={(e) => handleParamChange(
161
- 'epochs', e.target.value, props.params, props.optimizer, props.onOptimizerChange
162
- )}
 
 
 
 
 
 
163
  />
164
  </div>
165
  </>
166
  )}
167
- { !props.isTraining && (
168
- <>
169
- <button onClick={props.onStartTraining}>
170
- Start Training
171
- </button>
172
- <button onClick={props.onResetTraining}>
173
- Reset Training
174
- </button>
175
- </>
176
- )}
177
- { props.isTraining && (
178
- <>
179
- <button onClick={props.onPauseTraining}>
180
- Pause Training
181
- </button>
182
- <button onClick={props.onContinueTraining}>
183
- Continue Training
184
- </button>
185
- <button onClick={props.onStopTraining}>
186
- Stop Training
187
- </button>
188
- </>
189
- )}
190
- </div>
191
  );
192
  }
193
 
 
1
+ import '../../styles.css';
2
 
3
  const DEFAULT_PARAMS = {
4
  sgd: {
5
+ learningRate: '0.01',
6
+ momentum: '0.0',
7
+ batchSize: '32',
8
+ epochs: '5',
9
  },
10
  adam: {
11
+ learningRate: '0.001',
12
+ beta1: '0.9',
13
+ beta2: '0.999',
14
+ epsilon: '1e-8',
15
+ batchSize: '32',
16
+ epochs: '5',
17
+ },
18
+ };
 
19
 
20
  function handleOptimizerChange(event, onChange) {
21
  const selectedOptimizer = event.target.value;
 
30
  onChange(optimizerType, newParams);
31
  }
32
 
 
33
  function OptimizerSettings(props) {
34
  return (
35
  <div className="card">
 
44
  <option value="adam">Adam</option>
45
  </select>
46
  </div>
47
+ {props.optimizer === 'sgd' && (
48
  <>
49
  <div className="field">
50
  <label htmlFor="sgd-learning-rate-input">Learning Rate</label>
 
52
  id="sgd-learning-rate-input"
53
  type="text"
54
  value={props.params.learningRate}
55
+ onChange={(e) =>
56
+ handleParamChange(
57
+ 'learningRate',
58
+ e.target.value,
59
+ props.params,
60
+ props.optimizer,
61
+ props.onOptimizerChange,
62
+ )
63
+ }
64
  />
65
  </div>
66
  <div className="field">
 
69
  id="sgd-momentum-input"
70
  type="text"
71
  value={props.params.momentum}
72
+ onChange={(e) =>
73
+ handleParamChange(
74
+ 'momentum',
75
+ e.target.value,
76
+ props.params,
77
+ props.optimizer,
78
+ props.onOptimizerChange,
79
+ )
80
+ }
81
  />
82
  </div>
83
  <div className="field">
 
86
  id="sgd-batch-size-input"
87
  type="text"
88
  value={props.params.batchSize}
89
+ onChange={(e) =>
90
+ handleParamChange(
91
+ 'batchSize',
92
+ e.target.value,
93
+ props.params,
94
+ props.optimizer,
95
+ props.onOptimizerChange,
96
+ )
97
+ }
98
  />
99
  </div>
100
  <div className="field">
 
103
  id="sgd-epochs-input"
104
  type="text"
105
  value={props.params.epochs}
106
+ onChange={(e) =>
107
+ handleParamChange(
108
+ 'epochs',
109
+ e.target.value,
110
+ props.params,
111
+ props.optimizer,
112
+ props.onOptimizerChange,
113
+ )
114
+ }
115
  />
116
+ </div>
117
  </>
118
  )}
119
+ {props.optimizer === 'adam' && (
120
  <>
121
  <div className="field">
122
  <label htmlFor="adam-learning-rate-input">Learning Rate</label>
 
124
  id="adam-learning-rate-input"
125
  type="text"
126
  value={props.params.learningRate}
127
+ onChange={(e) =>
128
+ handleParamChange(
129
+ 'learningRate',
130
+ e.target.value,
131
+ props.params,
132
+ props.optimizer,
133
+ props.onOptimizerChange,
134
+ )
135
+ }
136
  />
137
  </div>
138
  <div className="field">
 
141
  id="adam-beta1-input"
142
  type="text"
143
  value={props.params.beta1}
144
+ onChange={(e) =>
145
+ handleParamChange(
146
+ 'beta1',
147
+ e.target.value,
148
+ props.params,
149
+ props.optimizer,
150
+ props.onOptimizerChange,
151
+ )
152
+ }
153
  />
154
  </div>
155
  <div className="field">
 
158
  id="adam-beta2-input"
159
  type="text"
160
  value={props.params.beta2}
161
+ onChange={(e) =>
162
+ handleParamChange(
163
+ 'beta2',
164
+ e.target.value,
165
+ props.params,
166
+ props.optimizer,
167
+ props.onOptimizerChange,
168
+ )
169
+ }
170
  />
171
  </div>
172
  <div className="field">
 
175
  id="adam-epsilon-input"
176
  type="text"
177
  value={props.params.epsilon}
178
+ onChange={(e) =>
179
+ handleParamChange(
180
+ 'epsilon',
181
+ e.target.value,
182
+ props.params,
183
+ props.optimizer,
184
+ props.onOptimizerChange,
185
+ )
186
+ }
187
  />
188
  </div>
189
  <div className="field">
 
192
  id="adam-batch-size-input"
193
  type="text"
194
  value={props.params.batchSize}
195
+ onChange={(e) =>
196
+ handleParamChange(
197
+ 'batchSize',
198
+ e.target.value,
199
+ props.params,
200
+ props.optimizer,
201
+ props.onOptimizerChange,
202
+ )
203
+ }
204
  />
205
  </div>
206
  <div className="field">
 
209
  id="adam-epochs-input"
210
  type="text"
211
  value={props.params.epochs}
212
+ onChange={(e) =>
213
+ handleParamChange(
214
+ 'epochs',
215
+ e.target.value,
216
+ props.params,
217
+ props.optimizer,
218
+ props.onOptimizerChange,
219
+ )
220
+ }
221
  />
222
  </div>
223
  </>
224
  )}
225
+ {!props.isTraining && (
226
+ <>
227
+ <button onClick={props.onStartTraining}>Start Training</button>
228
+ <button onClick={props.onResetTraining}>Reset Training</button>
229
+ </>
230
+ )}
231
+ {props.isTraining && (
232
+ <>
233
+ <button onClick={props.onPauseTraining}>Pause Training</button>
234
+ <button onClick={props.onContinueTraining}>Continue Training</button>
235
+ <button onClick={props.onStopTraining}>Stop Training</button>
236
+ </>
237
+ )}
238
+ </div>
 
 
 
 
 
 
 
 
 
 
239
  );
240
  }
241
 
src/components/network/OutputLayerViewer.jsx CHANGED
@@ -1,4 +1,4 @@
1
- import "./LayerViewer.css";
2
 
3
  function OutputLayerViewer(props) {
4
  const probs = props.probs;
@@ -9,14 +9,16 @@ function OutputLayerViewer(props) {
9
  <h3 className="layer-title">Output Layer (softmax)</h3>
10
 
11
  <div className="layer-details">
12
- <div><strong>Number of Classes:</strong> {numClasses}</div>
 
 
13
  </div>
14
 
15
  <h4>Class Probabilities</h4>
16
  <div className="layer-grid output-layer-grid">
17
  {Array.from({ length: numClasses }).map((_, i) => (
18
  <div key={i} className="output-class-probability">
19
- <strong>Class {i}:</strong> {(probs[i]).toFixed(2)}
20
  </div>
21
  ))}
22
  </div>
@@ -24,5 +26,4 @@ function OutputLayerViewer(props) {
24
  );
25
  }
26
 
27
-
28
  export default OutputLayerViewer;
 
1
+ import './LayerViewer.css';
2
 
3
  function OutputLayerViewer(props) {
4
  const probs = props.probs;
 
9
  <h3 className="layer-title">Output Layer (softmax)</h3>
10
 
11
  <div className="layer-details">
12
+ <div>
13
+ <strong>Number of Classes:</strong> {numClasses}
14
+ </div>
15
  </div>
16
 
17
  <h4>Class Probabilities</h4>
18
  <div className="layer-grid output-layer-grid">
19
  {Array.from({ length: numClasses }).map((_, i) => (
20
  <div key={i} className="output-class-probability">
21
+ <strong>Class {i}:</strong> {probs[i].toFixed(2)}
22
  </div>
23
  ))}
24
  </div>
 
26
  );
27
  }
28
 
 
29
  export default OutputLayerViewer;
src/components/network/Sidebar.jsx CHANGED
@@ -1,15 +1,15 @@
1
- import Tabs from "../Tabs/Tabs.jsx";
2
- import Button from "../Button/Button.jsx";
3
- import ArchitectureEditor from "./ArchitectureEditor.jsx";
4
- import OptimizerSettings from "./OptimizerSettings.jsx";
5
 
6
  function Sidebar(props) {
7
  const tabData = [
8
  {
9
- label: "Dataset",
10
  content: (
11
  <div>
12
- Dataset:
13
  <select value="mnist">
14
  <option value="mnist">MNIST</option>
15
  <option value="cifar-10">CIFAR-10</option>
@@ -18,40 +18,37 @@ function Sidebar(props) {
18
  ),
19
  },
20
  {
21
- label: "Architecture",
22
  content: (
23
- <ArchitectureEditor
24
- architecture={props.architecture}
25
- onChange={props.onArchitectureChange}
26
- />
27
- ),
28
  },
29
  {
30
- label: "Optimize",
31
  content: (
32
- <>
33
- <OptimizerSettings
34
- optimizer={props.optimizerType}
35
- params={props.optimizerParams}
36
- onOptimizerChange={props.onOptimizerChange}
37
- onStartTraining={props.onStartTraining}
38
- onResetTraining={props.onResetTraining}
39
- onPauseTraining={props.onPauseTraining}
40
- onContinueTraining={props.onContinueTraining}
41
- onStopTraining={props.onStopTraining}
42
- isTraining={props.isTraining}
43
- />
44
- </>
45
  ),
46
  },
47
  ];
48
 
49
  return (
50
  <div className="sidebar">
51
- <Tabs
52
- defaultTab="Dataset"
53
- tabs={tabData}
54
- />
55
  </div>
56
  );
57
  }
 
1
+ import Tabs from '../Tabs/Tabs.jsx';
2
+ import Button from '../Button/Button.jsx';
3
+ import ArchitectureEditor from './ArchitectureEditor.jsx';
4
+ import OptimizerSettings from './OptimizerSettings.jsx';
5
 
6
  function Sidebar(props) {
7
  const tabData = [
8
  {
9
+ label: 'Dataset',
10
  content: (
11
  <div>
12
+ Dataset:
13
  <select value="mnist">
14
  <option value="mnist">MNIST</option>
15
  <option value="cifar-10">CIFAR-10</option>
 
18
  ),
19
  },
20
  {
21
+ label: 'Architecture',
22
  content: (
23
+ <ArchitectureEditor
24
+ architecture={props.architecture}
25
+ onChange={props.onArchitectureChange}
26
+ />
27
+ ),
28
  },
29
  {
30
+ label: 'Optimize',
31
  content: (
32
+ <>
33
+ <OptimizerSettings
34
+ optimizer={props.optimizerType}
35
+ params={props.optimizerParams}
36
+ onOptimizerChange={props.onOptimizerChange}
37
+ onStartTraining={props.onStartTraining}
38
+ onResetTraining={props.onResetTraining}
39
+ onPauseTraining={props.onPauseTraining}
40
+ onContinueTraining={props.onContinueTraining}
41
+ onStopTraining={props.onStopTraining}
42
+ isTraining={props.isTraining}
43
+ />
44
+ </>
45
  ),
46
  },
47
  ];
48
 
49
  return (
50
  <div className="sidebar">
51
+ <Tabs defaultTab="Dataset" tabs={tabData} />
 
 
 
52
  </div>
53
  );
54
  }
src/components/network/old/AccPlot.jsx CHANGED
@@ -1,4 +1,4 @@
1
- import Plot from "react-plotly.js";
2
 
3
  function AccPlot(props) {
4
  return (
@@ -7,33 +7,33 @@ function AccPlot(props) {
7
  {
8
  x: props.epochs,
9
  y: props.acc,
10
- type: "scatter",
11
- mode: "lines",
12
- name: "Training Accuracy",
13
- line: { color: "red" },
14
- xlabel: "Epochs",
15
- ylabel: "Accuracy"
16
  },
17
  {
18
  x: props.epochs,
19
  y: props.valAcc,
20
- type: "scatter",
21
- mode: "lines",
22
- name: "Validation Accuracy",
23
- line: { color: "orange" },
24
- xlabel: "Epochs",
25
- ylabel: "Accuracy"
26
  },
27
  ]}
28
  layout={{
29
- title: "Training Curves",
30
  autosize: true,
31
- xaxis: { title: "Epoch" },
32
- yaxis: { title: "Value" },
33
- legend: { orientation: "h" },
34
  margin: { t: 0 },
35
  }}
36
- style={{ width: "100%", height: "100%" }}
37
  useResizeHandler={true}
38
  />
39
  );
 
1
+ import Plot from 'react-plotly.js';
2
 
3
  function AccPlot(props) {
4
  return (
 
7
  {
8
  x: props.epochs,
9
  y: props.acc,
10
+ type: 'scatter',
11
+ mode: 'lines',
12
+ name: 'Training Accuracy',
13
+ line: { color: 'red' },
14
+ xlabel: 'Epochs',
15
+ ylabel: 'Accuracy',
16
  },
17
  {
18
  x: props.epochs,
19
  y: props.valAcc,
20
+ type: 'scatter',
21
+ mode: 'lines',
22
+ name: 'Validation Accuracy',
23
+ line: { color: 'orange' },
24
+ xlabel: 'Epochs',
25
+ ylabel: 'Accuracy',
26
  },
27
  ]}
28
  layout={{
29
+ title: 'Training Curves',
30
  autosize: true,
31
+ xaxis: { title: 'Epoch' },
32
+ yaxis: { title: 'Value' },
33
+ legend: { orientation: 'h' },
34
  margin: { t: 0 },
35
  }}
36
+ style={{ width: '100%', height: '100%' }}
37
  useResizeHandler={true}
38
  />
39
  );
src/components/network/old/ActivationViewer.jsx CHANGED
@@ -1,21 +1,17 @@
1
- import "./ActivationViewer.css";
2
 
3
  function ActivationViewer(props) {
4
  return (
5
  <div className="activations">
6
- {props.sampleImage && (
7
- <div>
8
- <div className="sample-header">
9
- <h3>Sample Input</h3>
10
- <button
11
- onClick={props.onSampleChange}
12
- >
13
- Change
14
- </button>
15
- </div>
16
- <img src={props.sampleImage} alt="Sample Input" />
17
  </div>
18
- )}
 
 
19
 
20
  {props.activations &&
21
  props.activations.length > 0 &&
@@ -24,11 +20,7 @@ function ActivationViewer(props) {
24
  <h3>Layer {i + 1} Activations</h3>
25
  <div className="activation-maps">
26
  {layer.map((map, j) => (
27
- <img
28
- key={j}
29
- src={map}
30
- alt={`Layer ${i + 1} Map ${j + 1}`}
31
- />
32
  ))}
33
  </div>
34
  </div>
 
1
+ import './ActivationViewer.css';
2
 
3
  function ActivationViewer(props) {
4
  return (
5
  <div className="activations">
6
+ {props.sampleImage && (
7
+ <div>
8
+ <div className="sample-header">
9
+ <h3>Sample Input</h3>
10
+ <button onClick={props.onSampleChange}>Change</button>
 
 
 
 
 
 
11
  </div>
12
+ <img src={props.sampleImage} alt="Sample Input" />
13
+ </div>
14
+ )}
15
 
16
  {props.activations &&
17
  props.activations.length > 0 &&
 
20
  <h3>Layer {i + 1} Activations</h3>
21
  <div className="activation-maps">
22
  {layer.map((map, j) => (
23
+ <img key={j} src={map} alt={`Layer ${i + 1} Map ${j + 1}`} />
 
 
 
 
24
  ))}
25
  </div>
26
  </div>
src/components/network/old/ArchitectureEditor.css CHANGED
@@ -1,69 +1,69 @@
1
  .cnn-editor {
2
- max-width: 700px;
3
- margin: 20px auto;
4
- font-family: sans-serif;
5
  }
6
 
7
  .layers {
8
- display: flex;
9
- flex-direction: column;
10
- gap: 20px;
11
  }
12
 
13
  .layer-card {
14
- border: 1px solid #ccc;
15
- padding: 15px;
16
- border-radius: 10px;
17
- background: #fafafa;
18
  }
19
 
20
  .layer-header {
21
- display: flex;
22
- justify-content: space-between;
23
- margin-bottom: 12px;
24
  }
25
 
26
  .layer-grid {
27
- display: grid;
28
- grid-template-columns: 1fr 1fr;
29
- gap: 10px 20px;
30
  }
31
 
32
  .field {
33
- display: flex;
34
- flex-direction: column;
35
  }
36
 
37
  .field label {
38
- font-size: 12px;
39
- margin-bottom: 4px;
40
- color: #555;
41
  }
42
 
43
  .field input,
44
  .field select {
45
- padding: 5px;
46
- border-radius: 5px;
47
- border: 1px solid #ccc;
48
  }
49
 
50
  .add-btn {
51
- margin-top: 20px;
52
- padding: 12px;
53
- width: 100%;
54
- background: #4caf50;
55
- border: none;
56
- color: white;
57
- font-size: 16px;
58
- border-radius: 8px;
59
- cursor: pointer;
60
  }
61
 
62
  .remove-btn {
63
- background: #ff4444;
64
- border: none;
65
- padding: 6px 10px;
66
- color: white;
67
- border-radius: 5px;
68
- cursor: pointer;
69
  }
 
1
  .cnn-editor {
2
+ max-width: 700px;
3
+ margin: 20px auto;
4
+ font-family: sans-serif;
5
  }
6
 
7
  .layers {
8
+ display: flex;
9
+ flex-direction: column;
10
+ gap: 20px;
11
  }
12
 
13
  .layer-card {
14
+ border: 1px solid #ccc;
15
+ padding: 15px;
16
+ border-radius: 10px;
17
+ background: #fafafa;
18
  }
19
 
20
  .layer-header {
21
+ display: flex;
22
+ justify-content: space-between;
23
+ margin-bottom: 12px;
24
  }
25
 
26
  .layer-grid {
27
+ display: grid;
28
+ grid-template-columns: 1fr 1fr;
29
+ gap: 10px 20px;
30
  }
31
 
32
  .field {
33
+ display: flex;
34
+ flex-direction: column;
35
  }
36
 
37
  .field label {
38
+ font-size: 12px;
39
+ margin-bottom: 4px;
40
+ color: #555;
41
  }
42
 
43
  .field input,
44
  .field select {
45
+ padding: 5px;
46
+ border-radius: 5px;
47
+ border: 1px solid #ccc;
48
  }
49
 
50
  .add-btn {
51
+ margin-top: 20px;
52
+ padding: 12px;
53
+ width: 100%;
54
+ background: #4caf50;
55
+ border: none;
56
+ color: white;
57
+ font-size: 16px;
58
+ border-radius: 8px;
59
+ cursor: pointer;
60
  }
61
 
62
  .remove-btn {
63
+ background: #ff4444;
64
+ border: none;
65
+ padding: 6px 10px;
66
+ color: white;
67
+ border-radius: 5px;
68
+ cursor: pointer;
69
  }
src/components/network/old/ArchitectureEditor.jsx CHANGED
@@ -1,40 +1,40 @@
1
- import { useState } from "react";
2
- import "./ArchitectureEditor.css";
3
 
4
  function ArchitectureEditor() {
5
  const [layers, setLayers] = useState([
6
  {
7
- type: "conv",
8
  kernelSize: 5,
9
  filters: 8,
10
  stride: 1,
11
  padding: 1,
12
- activation: "relu",
13
  },
14
  {
15
- type: "maxpool",
16
  kernelSize: 2,
17
  stride: 2,
18
  padding: 0,
19
  },
20
  {
21
- type: "conv",
22
  kernelSize: 5,
23
  filters: 16,
24
  stride: 1,
25
  padding: 1,
26
- activation: "relu",
27
  },
28
  {
29
- type: "maxpool",
30
  kernelSize: 2,
31
  stride: 2,
32
  padding: 0,
33
  },
34
  {
35
- type: "dense",
36
  units: 10,
37
- activation: "softmax",
38
  },
39
  ]);
40
 
@@ -53,12 +53,12 @@ function ArchitectureEditor() {
53
  setLayers((prev) => [
54
  ...prev,
55
  {
56
- type: "conv",
57
  kernelSize: 3,
58
  filters: 8,
59
  stride: 1,
60
  padding: 1,
61
- activation: "relu",
62
  },
63
  ]);
64
  }
@@ -71,17 +71,11 @@ function ArchitectureEditor() {
71
  <div className="cnn-editor">
72
  <div className="layers">
73
  {layers.map((layer, index) => (
74
- <div
75
- key={index}
76
- className="layer-card"
77
- >
78
  <div className="layer-header">
79
  <strong>Layer {index + 1}</strong>
80
 
81
- <button
82
- className="remove-btn"
83
- onClick={() => removeLayer(index)}
84
- >
85
  Remove
86
  </button>
87
  </div>
@@ -93,9 +87,7 @@ function ArchitectureEditor() {
93
 
94
  <select
95
  value={layer.type}
96
- onChange={(e) =>
97
- updateLayer(index, "type", e.target.value)
98
- }
99
  >
100
  <option value="conv">Conv</option>
101
  <option value="maxpool">Max Pool</option>
@@ -105,7 +97,7 @@ function ArchitectureEditor() {
105
  </div>
106
 
107
  {/* Conv fields */}
108
- {layer.type === "conv" && (
109
  <>
110
  <div className="field">
111
  <label>Filters</label>
@@ -114,7 +106,7 @@ function ArchitectureEditor() {
114
  type="number"
115
  value={layer.filters}
116
  onChange={(e) =>
117
- updateLayer(index, "filters", Number(e.target.value))
118
  }
119
  />
120
  </div>
@@ -126,7 +118,7 @@ function ArchitectureEditor() {
126
  type="number"
127
  value={layer.kernelSize}
128
  onChange={(e) =>
129
- updateLayer(index, "kernelSize", Number(e.target.value))
130
  }
131
  />
132
  </div>
@@ -138,7 +130,7 @@ function ArchitectureEditor() {
138
  type="number"
139
  value={layer.stride}
140
  onChange={(e) =>
141
- updateLayer(index, "stride", Number(e.target.value))
142
  }
143
  />
144
  </div>
@@ -150,7 +142,7 @@ function ArchitectureEditor() {
150
  type="number"
151
  value={layer.padding}
152
  onChange={(e) =>
153
- updateLayer(index, "padding", Number(e.target.value))
154
  }
155
  />
156
  </div>
@@ -161,7 +153,7 @@ function ArchitectureEditor() {
161
  <select
162
  value={layer.activation}
163
  onChange={(e) =>
164
- updateLayer(index, "activation", e.target.value)
165
  }
166
  >
167
  <option value="relu">ReLU</option>
@@ -174,7 +166,7 @@ function ArchitectureEditor() {
174
  )}
175
 
176
  {/* Pool fields */}
177
- {(layer.type === "maxpool" || layer.type === "avgpool") && (
178
  <>
179
  <div className="field">
180
  <label>Kernel Size</label>
@@ -183,7 +175,7 @@ function ArchitectureEditor() {
183
  type="number"
184
  value={layer.kernelSize}
185
  onChange={(e) =>
186
- updateLayer(index, "kernelSize", Number(e.target.value))
187
  }
188
  />
189
  </div>
@@ -195,7 +187,7 @@ function ArchitectureEditor() {
195
  type="number"
196
  value={layer.stride}
197
  onChange={(e) =>
198
- updateLayer(index, "stride", Number(e.target.value))
199
  }
200
  />
201
  </div>
@@ -207,7 +199,7 @@ function ArchitectureEditor() {
207
  type="number"
208
  value={layer.padding}
209
  onChange={(e) =>
210
- updateLayer(index, "padding", Number(e.target.value))
211
  }
212
  />
213
  </div>
@@ -215,7 +207,7 @@ function ArchitectureEditor() {
215
  )}
216
 
217
  {/* Dense fields */}
218
- {layer.type === "dense" && (
219
  <>
220
  <div className="field">
221
  <label>Units</label>
@@ -224,7 +216,7 @@ function ArchitectureEditor() {
224
  type="number"
225
  value={layer.units}
226
  onChange={(e) =>
227
- updateLayer(index, "units", Number(e.target.value))
228
  }
229
  />
230
  </div>
@@ -235,7 +227,7 @@ function ArchitectureEditor() {
235
  <select
236
  value={layer.activation}
237
  onChange={(e) =>
238
- updateLayer(index, "activation", e.target.value)
239
  }
240
  >
241
  <option value="relu">ReLU</option>
@@ -252,10 +244,7 @@ function ArchitectureEditor() {
252
  ))}
253
  </div>
254
 
255
- <button
256
- className="add-btn"
257
- onClick={addLayer}
258
- >
259
  + Add Layer
260
  </button>
261
  </div>
 
1
+ import { useState } from 'react';
2
+ import './ArchitectureEditor.css';
3
 
4
  function ArchitectureEditor() {
5
  const [layers, setLayers] = useState([
6
  {
7
+ type: 'conv',
8
  kernelSize: 5,
9
  filters: 8,
10
  stride: 1,
11
  padding: 1,
12
+ activation: 'relu',
13
  },
14
  {
15
+ type: 'maxpool',
16
  kernelSize: 2,
17
  stride: 2,
18
  padding: 0,
19
  },
20
  {
21
+ type: 'conv',
22
  kernelSize: 5,
23
  filters: 16,
24
  stride: 1,
25
  padding: 1,
26
+ activation: 'relu',
27
  },
28
  {
29
+ type: 'maxpool',
30
  kernelSize: 2,
31
  stride: 2,
32
  padding: 0,
33
  },
34
  {
35
+ type: 'dense',
36
  units: 10,
37
+ activation: 'softmax',
38
  },
39
  ]);
40
 
 
53
  setLayers((prev) => [
54
  ...prev,
55
  {
56
+ type: 'conv',
57
  kernelSize: 3,
58
  filters: 8,
59
  stride: 1,
60
  padding: 1,
61
+ activation: 'relu',
62
  },
63
  ]);
64
  }
 
71
  <div className="cnn-editor">
72
  <div className="layers">
73
  {layers.map((layer, index) => (
74
+ <div key={index} className="layer-card">
 
 
 
75
  <div className="layer-header">
76
  <strong>Layer {index + 1}</strong>
77
 
78
+ <button className="remove-btn" onClick={() => removeLayer(index)}>
 
 
 
79
  Remove
80
  </button>
81
  </div>
 
87
 
88
  <select
89
  value={layer.type}
90
+ onChange={(e) => updateLayer(index, 'type', e.target.value)}
 
 
91
  >
92
  <option value="conv">Conv</option>
93
  <option value="maxpool">Max Pool</option>
 
97
  </div>
98
 
99
  {/* Conv fields */}
100
+ {layer.type === 'conv' && (
101
  <>
102
  <div className="field">
103
  <label>Filters</label>
 
106
  type="number"
107
  value={layer.filters}
108
  onChange={(e) =>
109
+ updateLayer(index, 'filters', Number(e.target.value))
110
  }
111
  />
112
  </div>
 
118
  type="number"
119
  value={layer.kernelSize}
120
  onChange={(e) =>
121
+ updateLayer(index, 'kernelSize', Number(e.target.value))
122
  }
123
  />
124
  </div>
 
130
  type="number"
131
  value={layer.stride}
132
  onChange={(e) =>
133
+ updateLayer(index, 'stride', Number(e.target.value))
134
  }
135
  />
136
  </div>
 
142
  type="number"
143
  value={layer.padding}
144
  onChange={(e) =>
145
+ updateLayer(index, 'padding', Number(e.target.value))
146
  }
147
  />
148
  </div>
 
153
  <select
154
  value={layer.activation}
155
  onChange={(e) =>
156
+ updateLayer(index, 'activation', e.target.value)
157
  }
158
  >
159
  <option value="relu">ReLU</option>
 
166
  )}
167
 
168
  {/* Pool fields */}
169
+ {(layer.type === 'maxpool' || layer.type === 'avgpool') && (
170
  <>
171
  <div className="field">
172
  <label>Kernel Size</label>
 
175
  type="number"
176
  value={layer.kernelSize}
177
  onChange={(e) =>
178
+ updateLayer(index, 'kernelSize', Number(e.target.value))
179
  }
180
  />
181
  </div>
 
187
  type="number"
188
  value={layer.stride}
189
  onChange={(e) =>
190
+ updateLayer(index, 'stride', Number(e.target.value))
191
  }
192
  />
193
  </div>
 
199
  type="number"
200
  value={layer.padding}
201
  onChange={(e) =>
202
+ updateLayer(index, 'padding', Number(e.target.value))
203
  }
204
  />
205
  </div>
 
207
  )}
208
 
209
  {/* Dense fields */}
210
+ {layer.type === 'dense' && (
211
  <>
212
  <div className="field">
213
  <label>Units</label>
 
216
  type="number"
217
  value={layer.units}
218
  onChange={(e) =>
219
+ updateLayer(index, 'units', Number(e.target.value))
220
  }
221
  />
222
  </div>
 
227
  <select
228
  value={layer.activation}
229
  onChange={(e) =>
230
+ updateLayer(index, 'activation', e.target.value)
231
  }
232
  >
233
  <option value="relu">ReLU</option>
 
244
  ))}
245
  </div>
246
 
247
+ <button className="add-btn" onClick={addLayer}>
 
 
 
248
  + Add Layer
249
  </button>
250
  </div>
src/components/network/old/KernelViewer.jsx CHANGED
@@ -7,4 +7,3 @@ function KernelViewer() {
7
  }
8
 
9
  export default KernelViewer;
10
-
 
7
  }
8
 
9
  export default KernelViewer;
 
src/components/network/old/LossPlot.jsx CHANGED
@@ -1,38 +1,38 @@
1
- import Plot from "react-plotly.js";
2
-
3
- function LossPlot(props) {
4
- return (
5
- <Plot
6
- data={[
7
- {
8
- x: props.epochs,
9
- y: props.loss,
10
- type: "scatter",
11
- mode: "lines",
12
- name: "Training Loss",
13
- line: { color: "red" },
14
- },
15
- {
16
- x: props.epochs,
17
- y: props.valLoss,
18
- type: "scatter",
19
- mode: "lines",
20
- name: "Validation Loss",
21
- line: { color: "orange" },
22
- },
23
- ]}
24
- layout={{
25
- title: "Training Curves",
26
- autosize: true,
27
- xaxis: { title: "Epoch" },
28
- yaxis: { title: "Cross Entropy Loss" },
29
- legend: { orientation: "h" },
30
- margin: { t: 0 },
31
- }}
32
- style={{ width: "100%", height: "100%" }}
33
- useResizeHandler={true}
34
- />
35
- );
36
- }
37
-
38
- export default LossPlot;
 
1
+ import Plot from 'react-plotly.js';
2
+
3
+ function LossPlot(props) {
4
+ return (
5
+ <Plot
6
+ data={[
7
+ {
8
+ x: props.epochs,
9
+ y: props.loss,
10
+ type: 'scatter',
11
+ mode: 'lines',
12
+ name: 'Training Loss',
13
+ line: { color: 'red' },
14
+ },
15
+ {
16
+ x: props.epochs,
17
+ y: props.valLoss,
18
+ type: 'scatter',
19
+ mode: 'lines',
20
+ name: 'Validation Loss',
21
+ line: { color: 'orange' },
22
+ },
23
+ ]}
24
+ layout={{
25
+ title: 'Training Curves',
26
+ autosize: true,
27
+ xaxis: { title: 'Epoch' },
28
+ yaxis: { title: 'Cross Entropy Loss' },
29
+ legend: { orientation: 'h' },
30
+ margin: { t: 0 },
31
+ }}
32
+ style={{ width: '100%', height: '100%' }}
33
+ useResizeHandler={true}
34
+ />
35
+ );
36
+ }
37
+
38
+ export default LossPlot;
src/components/network/old/MnistTrainer.css CHANGED
@@ -1,8 +1,8 @@
1
  .mnist-trainer {
2
- display: grid;
3
- grid-template-columns: 2fr 1fr;
4
- gap: 20px;
5
- padding: 20px;
6
  }
7
 
8
  .plots {
@@ -10,12 +10,11 @@
10
  }
11
 
12
  .activations {
13
- min-width: 0;
14
  }
15
 
16
  .sidebar {
17
- display: flex;
18
- flex-direction: column;
19
- gap: 20px;
20
  }
21
-
 
1
  .mnist-trainer {
2
+ display: grid;
3
+ grid-template-columns: 2fr 1fr;
4
+ gap: 20px;
5
+ padding: 20px;
6
  }
7
 
8
  .plots {
 
10
  }
11
 
12
  .activations {
13
+ min-width: 0;
14
  }
15
 
16
  .sidebar {
17
+ display: flex;
18
+ flex-direction: column;
19
+ gap: 20px;
20
  }
 
src/components/network/old/MnistTrainer.jsx CHANGED
@@ -1,22 +1,22 @@
1
- import { useEffect, useState, useRef } from "react";
2
- import * as tf from "@tensorflow/tfjs";
3
- import { MnistData } from "./mnist.js";
4
- import LossPlot from "./LossPlot.jsx";
5
- import AccPlot from "./AccPlot.jsx";
6
- import Tabs from "../utils/Tabs.jsx";
7
- import ActivationViewer from "./ActivationViewer.jsx";
8
- import ArchitectureEditor from "./ArchitectureEditor.jsx";
9
- import KernelViewer from "./KernelViewer.jsx";
10
- import "./MnistTrainer.css";
11
 
12
  export default function MnistTrainer() {
13
  const [data, setData] = useState(null);
14
  const [model, setModel] = useState(null);
15
 
16
- const [activationModel, setActivationModel] = useState(null);
17
- const [activations, setActivations] = useState(null);
18
- const [sampleTensor, setSampleTensor] = useState(null);
19
- const [sampleImage, setSampleImage] = useState(null);
20
 
21
  const [epochs, setEpochs] = useState([]);
22
  const [loss, setLoss] = useState([]);
@@ -24,11 +24,11 @@ export default function MnistTrainer() {
24
  const [acc, setAcc] = useState([]);
25
  const [valAcc, setValAcc] = useState([]);
26
 
27
- const [status, setStatus] = useState("Loading MNIST…");
28
 
29
  const progressTabData = [
30
  {
31
- label: "Curves",
32
  content: (
33
  <div className="plots">
34
  <LossPlot epochs={epochs} loss={loss} valLoss={valLoss} />
@@ -37,48 +37,38 @@ export default function MnistTrainer() {
37
  ),
38
  },
39
  {
40
- label: "Activations",
41
  content: (
42
- <ActivationViewer
43
- activations={activations}
44
  sampleImage={sampleImage}
45
  onSampleChange={handleSampleChange}
46
  />
47
  ),
48
  },
49
  {
50
- label: "Kernels",
51
- content: (
52
- <KernelViewer
53
- />
54
- ),
55
  },
56
-
57
  ];
58
 
59
  const optionsTabData = [
60
  {
61
- label: "Train",
62
  content: (
63
  <div className="train-options">
64
  <p>Status: {status}</p>
65
- <button
66
- onClick={handleTrain}
67
- disabled={status === "Loading MNIST…"}
68
- >
69
  Train Model
70
  </button>
71
- <button
72
- onClick={resetModel}
73
- disabled={status === "Loading MNIST…"}
74
- >
75
  Reset Model
76
  </button>
77
  </div>
78
  ),
79
  },
80
  {
81
- label: "Architecture",
82
  content: (
83
  <div>
84
  <ArchitectureEditor />
@@ -86,13 +76,13 @@ export default function MnistTrainer() {
86
  ),
87
  },
88
  {
89
- label: "Optimization",
90
  content: (
91
  <div>
92
  <p>Optimization options will go here.</p>
93
  </div>
94
  ),
95
- }
96
  ];
97
 
98
  function handleSampleChange() {
@@ -109,27 +99,27 @@ export default function MnistTrainer() {
109
  });
110
  }
111
 
112
- async function load() {
113
- const d = new MnistData();
114
- await d.load();
115
- setData(d);
116
 
117
- const m = createModel();
118
- setModel(m);
119
- const aModel = createActivationModel(m);
120
- setActivationModel(aModel);
121
 
122
- const sample = d.nextTestBatch(1);
123
- const sampleImg = sample.xs.reshape([1, 28, 28, 1]);
124
 
125
- setSampleTensor(sampleImg);
126
 
127
- tensorToImageUrl(sampleImg).then((url) => {
128
- setSampleImage(url);
129
- });
130
 
131
- setStatus("Ready");
132
- }
133
 
134
  useEffect(() => {
135
  load();
@@ -143,9 +133,9 @@ export default function MnistTrainer() {
143
  inputShape: [28, 28, 1],
144
  kernelSize: 5,
145
  filters: 8,
146
- activation: "relu",
147
- kernelInitializer: "varianceScaling",
148
- })
149
  );
150
 
151
  model.add(tf.layers.maxPooling2d({ poolSize: [2, 2], strides: [2, 2] }));
@@ -154,74 +144,67 @@ export default function MnistTrainer() {
154
  tf.layers.conv2d({
155
  kernelSize: 5,
156
  filters: 16,
157
- activation: "relu",
158
- kernelInitializer: "varianceScaling",
159
- })
160
  );
161
 
162
  model.add(tf.layers.maxPooling2d({ poolSize: [2, 2], strides: [2, 2] }));
163
 
164
  model.add(tf.layers.flatten());
165
 
166
- model.add(tf.layers.dense({ units: 10, activation: "softmax" }));
167
 
168
  model.compile({
169
  optimizer: tf.train.adam(),
170
- loss: "categoricalCrossentropy",
171
- metrics: ["accuracy"],
172
  });
173
 
174
  return model;
175
  }
176
 
177
- function createActivationModel(model) {
178
- const layerOutputs = model.layers
179
- .filter((layer) => layer.name.includes("conv"))
180
- .map((layer) => layer.output);
181
- return tf.model({ inputs: model.input, outputs: layerOutputs });
182
- }
183
-
184
- async function tensorToImageUrl(tensor) {
185
- // tensor must be shape [1, 28, 28, 1] or [28, 28]
186
- const squeezed = tensor.squeeze();
187
-
188
- const canvas = document.createElement("canvas");
189
- canvas.width = squeezed.shape[0];
190
- canvas.height = squeezed.shape[1];
191
-
192
- await tf.browser.toPixels(squeezed, canvas);
193
-
194
- return canvas.toDataURL();
195
- }
196
 
 
 
 
 
 
 
 
 
197
 
198
  async function handleTrain() {
199
  if (!model || !data) {
200
- return;
201
- }
202
 
203
- setStatus("Preparing training data…");
204
 
205
  const TRAIN_SIZE = 5000;
206
  const TEST_SIZE = 1000;
207
 
208
  const [trainXs, trainYs] = tf.tidy(() => {
209
  const d = data.nextTrainBatch(TRAIN_SIZE);
210
- return [
211
- d.xs.reshape([TRAIN_SIZE, 28, 28, 1]),
212
- d.labels,
213
- ];
214
  });
215
 
216
  const [testXs, testYs] = tf.tidy(() => {
217
  const d = data.nextTestBatch(TEST_SIZE);
218
- return [
219
- d.xs.reshape([TEST_SIZE, 28, 28, 1]),
220
- d.labels,
221
- ];
222
  });
223
 
224
- setStatus("Training…");
225
 
226
  setEpochs([]);
227
  setLoss([]);
@@ -237,51 +220,49 @@ export default function MnistTrainer() {
237
 
238
  callbacks: {
239
  onBatchEnd: async (batch, logs) => {
240
- setStatus(
241
- `Training: epoch ${currentEpoch + 1} batch ${batch + 1}`
242
- );
243
 
244
  if (conv) {
245
  console.log(kernel);
246
  kernel.dispose();
247
  }
248
 
249
- if (activationModel && sampleTensor) {
250
- const acts = activationModel.predict(sampleTensor);
251
-
252
- const allMaps = await Promise.all(
253
- acts.map(async (act) => {
254
- const [b, h, w, c] = act.shape;
255
- const maps = [];
256
- const slice = act.squeeze();
257
-
258
- for (let i = 0; i < c; ++i) {
259
- const channel = slice
260
- .slice([0, 0, i], [h, w, 1])
261
- .reshape([h, w]);
262
-
263
- const min = channel.min();
264
- const max = channel.max();
265
- const range = max.sub(min);
266
-
267
- let norm = channel.sub(min);
268
- if (range.dataSync()[0] !== 0) {
269
- norm = norm.div(range);
270
- }
271
-
272
- const canvas = document.createElement("canvas");
273
- canvas.width = w;
274
- canvas.height = h;
275
- await tf.browser.toPixels(norm, canvas);
276
- maps.push(canvas.toDataURL());
277
- }
278
- return maps;
279
- })
280
- );
281
-
282
- setActivations(allMaps);
283
- }
284
-
285
  await tf.nextFrame();
286
  },
287
 
@@ -295,7 +276,7 @@ export default function MnistTrainer() {
295
  },
296
 
297
  onTrainEnd: () => {
298
- setStatus("Training complete");
299
  },
300
  },
301
  });
@@ -306,44 +287,43 @@ export default function MnistTrainer() {
306
  testYs.dispose();
307
  }
308
 
309
- async function resetModel() {
310
- setStatus("Resetting...");
311
 
312
- await stopModel();
313
 
314
- setEpochs([]);
315
- setLoss([]);
316
- setValLoss([]);
317
- setAcc([]);
318
- setValAcc([]);
319
- setActivations(null);
320
 
321
- await load();
322
- }
323
 
324
- async function stopModel() {
325
- setStatus("Stopping...");
326
 
327
- if (model) {
328
- model.stopTraining = true;
329
- }
330
 
331
- await new Promise((resolve) => setTimeout(resolve, 50));
332
-
333
- setModel(null);
334
- setActivationModel(null);
335
- }
336
 
337
  return (
338
  <div className="mnist-trainer">
339
- <div className="progress-section">
340
  <Tabs tabs={progressTabData} defaultTab="Curves" />
341
- </div>
342
 
343
- <div className="sidebar">
344
  <Tabs tabs={optionsTabData} defaultTab="Train" />
345
- </div>
346
  </div>
347
  );
348
  }
349
-
 
1
+ import { useEffect, useState, useRef } from 'react';
2
+ import * as tf from '@tensorflow/tfjs';
3
+ import { MnistData } from './mnist.js';
4
+ import LossPlot from './LossPlot.jsx';
5
+ import AccPlot from './AccPlot.jsx';
6
+ import Tabs from '../utils/Tabs.jsx';
7
+ import ActivationViewer from './ActivationViewer.jsx';
8
+ import ArchitectureEditor from './ArchitectureEditor.jsx';
9
+ import KernelViewer from './KernelViewer.jsx';
10
+ import './MnistTrainer.css';
11
 
12
  export default function MnistTrainer() {
13
  const [data, setData] = useState(null);
14
  const [model, setModel] = useState(null);
15
 
16
+ const [activationModel, setActivationModel] = useState(null);
17
+ const [activations, setActivations] = useState(null);
18
+ const [sampleTensor, setSampleTensor] = useState(null);
19
+ const [sampleImage, setSampleImage] = useState(null);
20
 
21
  const [epochs, setEpochs] = useState([]);
22
  const [loss, setLoss] = useState([]);
 
24
  const [acc, setAcc] = useState([]);
25
  const [valAcc, setValAcc] = useState([]);
26
 
27
+ const [status, setStatus] = useState('Loading MNIST…');
28
 
29
  const progressTabData = [
30
  {
31
+ label: 'Curves',
32
  content: (
33
  <div className="plots">
34
  <LossPlot epochs={epochs} loss={loss} valLoss={valLoss} />
 
37
  ),
38
  },
39
  {
40
+ label: 'Activations',
41
  content: (
42
+ <ActivationViewer
43
+ activations={activations}
44
  sampleImage={sampleImage}
45
  onSampleChange={handleSampleChange}
46
  />
47
  ),
48
  },
49
  {
50
+ label: 'Kernels',
51
+ content: <KernelViewer />,
 
 
 
52
  },
 
53
  ];
54
 
55
  const optionsTabData = [
56
  {
57
+ label: 'Train',
58
  content: (
59
  <div className="train-options">
60
  <p>Status: {status}</p>
61
+ <button onClick={handleTrain} disabled={status === 'Loading MNIST…'}>
 
 
 
62
  Train Model
63
  </button>
64
+ <button onClick={resetModel} disabled={status === 'Loading MNIST…'}>
 
 
 
65
  Reset Model
66
  </button>
67
  </div>
68
  ),
69
  },
70
  {
71
+ label: 'Architecture',
72
  content: (
73
  <div>
74
  <ArchitectureEditor />
 
76
  ),
77
  },
78
  {
79
+ label: 'Optimization',
80
  content: (
81
  <div>
82
  <p>Optimization options will go here.</p>
83
  </div>
84
  ),
85
+ },
86
  ];
87
 
88
  function handleSampleChange() {
 
99
  });
100
  }
101
 
102
+ async function load() {
103
+ const d = new MnistData();
104
+ await d.load();
105
+ setData(d);
106
 
107
+ const m = createModel();
108
+ setModel(m);
109
+ const aModel = createActivationModel(m);
110
+ setActivationModel(aModel);
111
 
112
+ const sample = d.nextTestBatch(1);
113
+ const sampleImg = sample.xs.reshape([1, 28, 28, 1]);
114
 
115
+ setSampleTensor(sampleImg);
116
 
117
+ tensorToImageUrl(sampleImg).then((url) => {
118
+ setSampleImage(url);
119
+ });
120
 
121
+ setStatus('Ready');
122
+ }
123
 
124
  useEffect(() => {
125
  load();
 
133
  inputShape: [28, 28, 1],
134
  kernelSize: 5,
135
  filters: 8,
136
+ activation: 'relu',
137
+ kernelInitializer: 'varianceScaling',
138
+ }),
139
  );
140
 
141
  model.add(tf.layers.maxPooling2d({ poolSize: [2, 2], strides: [2, 2] }));
 
144
  tf.layers.conv2d({
145
  kernelSize: 5,
146
  filters: 16,
147
+ activation: 'relu',
148
+ kernelInitializer: 'varianceScaling',
149
+ }),
150
  );
151
 
152
  model.add(tf.layers.maxPooling2d({ poolSize: [2, 2], strides: [2, 2] }));
153
 
154
  model.add(tf.layers.flatten());
155
 
156
+ model.add(tf.layers.dense({ units: 10, activation: 'softmax' }));
157
 
158
  model.compile({
159
  optimizer: tf.train.adam(),
160
+ loss: 'categoricalCrossentropy',
161
+ metrics: ['accuracy'],
162
  });
163
 
164
  return model;
165
  }
166
 
167
+ function createActivationModel(model) {
168
+ const layerOutputs = model.layers
169
+ .filter((layer) => layer.name.includes('conv'))
170
+ .map((layer) => layer.output);
171
+ return tf.model({ inputs: model.input, outputs: layerOutputs });
172
+ }
173
+
174
+ async function tensorToImageUrl(tensor) {
175
+ // tensor must be shape [1, 28, 28, 1] or [28, 28]
176
+ const squeezed = tensor.squeeze();
 
 
 
 
 
 
 
 
 
177
 
178
+ const canvas = document.createElement('canvas');
179
+ canvas.width = squeezed.shape[0];
180
+ canvas.height = squeezed.shape[1];
181
+
182
+ await tf.browser.toPixels(squeezed, canvas);
183
+
184
+ return canvas.toDataURL();
185
+ }
186
 
187
  async function handleTrain() {
188
  if (!model || !data) {
189
+ return;
190
+ }
191
 
192
+ setStatus('Preparing training data…');
193
 
194
  const TRAIN_SIZE = 5000;
195
  const TEST_SIZE = 1000;
196
 
197
  const [trainXs, trainYs] = tf.tidy(() => {
198
  const d = data.nextTrainBatch(TRAIN_SIZE);
199
+ return [d.xs.reshape([TRAIN_SIZE, 28, 28, 1]), d.labels];
 
 
 
200
  });
201
 
202
  const [testXs, testYs] = tf.tidy(() => {
203
  const d = data.nextTestBatch(TEST_SIZE);
204
+ return [d.xs.reshape([TEST_SIZE, 28, 28, 1]), d.labels];
 
 
 
205
  });
206
 
207
+ setStatus('Training…');
208
 
209
  setEpochs([]);
210
  setLoss([]);
 
220
 
221
  callbacks: {
222
  onBatchEnd: async (batch, logs) => {
223
+ setStatus(`Training: epoch ${currentEpoch + 1} batch ${batch + 1}`);
 
 
224
 
225
  if (conv) {
226
  console.log(kernel);
227
  kernel.dispose();
228
  }
229
 
230
+ if (activationModel && sampleTensor) {
231
+ const acts = activationModel.predict(sampleTensor);
232
+
233
+ const allMaps = await Promise.all(
234
+ acts.map(async (act) => {
235
+ const [b, h, w, c] = act.shape;
236
+ const maps = [];
237
+ const slice = act.squeeze();
238
+
239
+ for (let i = 0; i < c; ++i) {
240
+ const channel = slice
241
+ .slice([0, 0, i], [h, w, 1])
242
+ .reshape([h, w]);
243
+
244
+ const min = channel.min();
245
+ const max = channel.max();
246
+ const range = max.sub(min);
247
+
248
+ let norm = channel.sub(min);
249
+ if (range.dataSync()[0] !== 0) {
250
+ norm = norm.div(range);
251
+ }
252
+
253
+ const canvas = document.createElement('canvas');
254
+ canvas.width = w;
255
+ canvas.height = h;
256
+ await tf.browser.toPixels(norm, canvas);
257
+ maps.push(canvas.toDataURL());
258
+ }
259
+ return maps;
260
+ }),
261
+ );
262
+
263
+ setActivations(allMaps);
264
+ }
265
+
266
  await tf.nextFrame();
267
  },
268
 
 
276
  },
277
 
278
  onTrainEnd: () => {
279
+ setStatus('Training complete');
280
  },
281
  },
282
  });
 
287
  testYs.dispose();
288
  }
289
 
290
+ async function resetModel() {
291
+ setStatus('Resetting...');
292
 
293
+ await stopModel();
294
 
295
+ setEpochs([]);
296
+ setLoss([]);
297
+ setValLoss([]);
298
+ setAcc([]);
299
+ setValAcc([]);
300
+ setActivations(null);
301
 
302
+ await load();
303
+ }
304
 
305
+ async function stopModel() {
306
+ setStatus('Stopping...');
307
 
308
+ if (model) {
309
+ model.stopTraining = true;
310
+ }
311
 
312
+ await new Promise((resolve) => setTimeout(resolve, 50));
313
+
314
+ setModel(null);
315
+ setActivationModel(null);
316
+ }
317
 
318
  return (
319
  <div className="mnist-trainer">
320
+ <div className="progress-section">
321
  <Tabs tabs={progressTabData} defaultTab="Curves" />
322
+ </div>
323
 
324
+ <div className="sidebar">
325
  <Tabs tabs={optionsTabData} defaultTab="Train" />
326
+ </div>
327
  </div>
328
  );
329
  }
 
src/components/network/old/mnist.js CHANGED
@@ -27,9 +27,9 @@ const NUM_TRAIN_ELEMENTS = Math.floor(TRAIN_TEST_RATIO * NUM_DATASET_ELEMENTS);
27
  const NUM_TEST_ELEMENTS = NUM_DATASET_ELEMENTS - NUM_TRAIN_ELEMENTS;
28
 
29
  const MNIST_IMAGES_SPRITE_PATH =
30
- 'https://storage.googleapis.com/learnjs-data/model-builder/mnist_images.png';
31
  const MNIST_LABELS_PATH =
32
- 'https://storage.googleapis.com/learnjs-data/model-builder/mnist_labels_uint8';
33
 
34
  /**
35
  * A class that fetches the sprited MNIST dataset and returns shuffled batches.
@@ -54,8 +54,9 @@ export class MnistData {
54
  img.width = img.naturalWidth;
55
  img.height = img.naturalHeight;
56
 
57
- const datasetBytesBuffer =
58
- new ArrayBuffer(NUM_DATASET_ELEMENTS * IMAGE_SIZE * 4);
 
59
 
60
  const chunkSize = 5000;
61
  canvas.width = img.width;
@@ -63,11 +64,21 @@ export class MnistData {
63
 
64
  for (let i = 0; i < NUM_DATASET_ELEMENTS / chunkSize; i++) {
65
  const datasetBytesView = new Float32Array(
66
- datasetBytesBuffer, i * IMAGE_SIZE * chunkSize * 4,
67
- IMAGE_SIZE * chunkSize);
 
 
68
  ctx.drawImage(
69
- img, 0, i * chunkSize, img.width, chunkSize, 0, 0, img.width,
70
- chunkSize);
 
 
 
 
 
 
 
 
71
 
72
  const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
73
 
@@ -85,8 +96,10 @@ export class MnistData {
85
  });
86
 
87
  const labelsRequest = fetch(MNIST_LABELS_PATH);
88
- const [imgResponse, labelsResponse] =
89
- await Promise.all([imgRequest, labelsRequest]);
 
 
90
 
91
  this.datasetLabels = new Uint8Array(await labelsResponse.arrayBuffer());
92
 
@@ -96,28 +109,36 @@ export class MnistData {
96
  this.testIndices = tf.util.createShuffledIndices(NUM_TEST_ELEMENTS);
97
 
98
  // Slice the the images and labels into train and test sets.
99
- this.trainImages =
100
- this.datasetImages.slice(0, IMAGE_SIZE * NUM_TRAIN_ELEMENTS);
 
 
101
  this.testImages = this.datasetImages.slice(IMAGE_SIZE * NUM_TRAIN_ELEMENTS);
102
- this.trainLabels =
103
- this.datasetLabels.slice(0, NUM_CLASSES * NUM_TRAIN_ELEMENTS);
104
- this.testLabels =
105
- this.datasetLabels.slice(NUM_CLASSES * NUM_TRAIN_ELEMENTS);
 
 
 
106
  }
107
 
108
  nextTrainBatch(batchSize) {
109
  return this.nextBatch(
110
- batchSize, [this.trainImages, this.trainLabels], () => {
111
- this.shuffledTrainIndex =
112
- (this.shuffledTrainIndex + 1) % this.trainIndices.length;
113
- return this.trainIndices[this.shuffledTrainIndex];
114
- });
 
 
 
115
  }
116
 
117
  nextTestBatch(batchSize) {
118
  return this.nextBatch(batchSize, [this.testImages, this.testLabels], () => {
119
  this.shuffledTestIndex =
120
- (this.shuffledTestIndex + 1) % this.testIndices.length;
121
  return this.testIndices[this.shuffledTestIndex];
122
  });
123
  }
@@ -129,18 +150,22 @@ export class MnistData {
129
  for (let i = 0; i < batchSize; i++) {
130
  const idx = index();
131
 
132
- const image =
133
- data[0].slice(idx * IMAGE_SIZE, idx * IMAGE_SIZE + IMAGE_SIZE);
 
 
134
  batchImagesArray.set(image, i * IMAGE_SIZE);
135
 
136
- const label =
137
- data[1].slice(idx * NUM_CLASSES, idx * NUM_CLASSES + NUM_CLASSES);
 
 
138
  batchLabelsArray.set(label, i * NUM_CLASSES);
139
  }
140
 
141
  const xs = tf.tensor2d(batchImagesArray, [batchSize, IMAGE_SIZE]);
142
  const labels = tf.tensor2d(batchLabelsArray, [batchSize, NUM_CLASSES]);
143
 
144
- return {xs, labels};
145
  }
146
  }
 
27
  const NUM_TEST_ELEMENTS = NUM_DATASET_ELEMENTS - NUM_TRAIN_ELEMENTS;
28
 
29
  const MNIST_IMAGES_SPRITE_PATH =
30
+ 'https://storage.googleapis.com/learnjs-data/model-builder/mnist_images.png';
31
  const MNIST_LABELS_PATH =
32
+ 'https://storage.googleapis.com/learnjs-data/model-builder/mnist_labels_uint8';
33
 
34
  /**
35
  * A class that fetches the sprited MNIST dataset and returns shuffled batches.
 
54
  img.width = img.naturalWidth;
55
  img.height = img.naturalHeight;
56
 
57
+ const datasetBytesBuffer = new ArrayBuffer(
58
+ NUM_DATASET_ELEMENTS * IMAGE_SIZE * 4,
59
+ );
60
 
61
  const chunkSize = 5000;
62
  canvas.width = img.width;
 
64
 
65
  for (let i = 0; i < NUM_DATASET_ELEMENTS / chunkSize; i++) {
66
  const datasetBytesView = new Float32Array(
67
+ datasetBytesBuffer,
68
+ i * IMAGE_SIZE * chunkSize * 4,
69
+ IMAGE_SIZE * chunkSize,
70
+ );
71
  ctx.drawImage(
72
+ img,
73
+ 0,
74
+ i * chunkSize,
75
+ img.width,
76
+ chunkSize,
77
+ 0,
78
+ 0,
79
+ img.width,
80
+ chunkSize,
81
+ );
82
 
83
  const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
84
 
 
96
  });
97
 
98
  const labelsRequest = fetch(MNIST_LABELS_PATH);
99
+ const [imgResponse, labelsResponse] = await Promise.all([
100
+ imgRequest,
101
+ labelsRequest,
102
+ ]);
103
 
104
  this.datasetLabels = new Uint8Array(await labelsResponse.arrayBuffer());
105
 
 
109
  this.testIndices = tf.util.createShuffledIndices(NUM_TEST_ELEMENTS);
110
 
111
  // Slice the the images and labels into train and test sets.
112
+ this.trainImages = this.datasetImages.slice(
113
+ 0,
114
+ IMAGE_SIZE * NUM_TRAIN_ELEMENTS,
115
+ );
116
  this.testImages = this.datasetImages.slice(IMAGE_SIZE * NUM_TRAIN_ELEMENTS);
117
+ this.trainLabels = this.datasetLabels.slice(
118
+ 0,
119
+ NUM_CLASSES * NUM_TRAIN_ELEMENTS,
120
+ );
121
+ this.testLabels = this.datasetLabels.slice(
122
+ NUM_CLASSES * NUM_TRAIN_ELEMENTS,
123
+ );
124
  }
125
 
126
  nextTrainBatch(batchSize) {
127
  return this.nextBatch(
128
+ batchSize,
129
+ [this.trainImages, this.trainLabels],
130
+ () => {
131
+ this.shuffledTrainIndex =
132
+ (this.shuffledTrainIndex + 1) % this.trainIndices.length;
133
+ return this.trainIndices[this.shuffledTrainIndex];
134
+ },
135
+ );
136
  }
137
 
138
  nextTestBatch(batchSize) {
139
  return this.nextBatch(batchSize, [this.testImages, this.testLabels], () => {
140
  this.shuffledTestIndex =
141
+ (this.shuffledTestIndex + 1) % this.testIndices.length;
142
  return this.testIndices[this.shuffledTestIndex];
143
  });
144
  }
 
150
  for (let i = 0; i < batchSize; i++) {
151
  const idx = index();
152
 
153
+ const image = data[0].slice(
154
+ idx * IMAGE_SIZE,
155
+ idx * IMAGE_SIZE + IMAGE_SIZE,
156
+ );
157
  batchImagesArray.set(image, i * IMAGE_SIZE);
158
 
159
+ const label = data[1].slice(
160
+ idx * NUM_CLASSES,
161
+ idx * NUM_CLASSES + NUM_CLASSES,
162
+ );
163
  batchLabelsArray.set(label, i * NUM_CLASSES);
164
  }
165
 
166
  const xs = tf.tensor2d(batchImagesArray, [batchSize, IMAGE_SIZE]);
167
  const labels = tf.tensor2d(batchLabelsArray, [batchSize, NUM_CLASSES]);
168
 
169
+ return { xs, labels };
170
  }
171
  }
src/components/network/train.js CHANGED
@@ -2,14 +2,14 @@ import * as tf from '@tensorflow/tfjs';
2
  import { MnistData } from '../../data/mnist.js';
3
 
4
  async function load(dataset) {
5
- let data;
6
- if (dataset === "mnist") {
7
- data = new MnistData();
8
- } else {
9
- throw new Error(`Dataset ${dataset} not supported`);
10
- }
11
  await data.load();
12
- return data;
13
  }
14
 
15
  class Cnn {
@@ -28,16 +28,15 @@ class Cnn {
28
  const shape = [layer.kernel, layer.kernel, inChannels, layer.filters];
29
  const layerWeights = tf.variable(
30
  tf.randomUniform(
31
- shape,
32
- -Math.sqrt(1 / (layer.kernel * layer.kernel * inChannels)),
33
- Math.sqrt(1 / (layer.kernel * layer.kernel * inChannels))
34
- )
35
  );
36
 
37
  weights.push(layerWeights);
38
  layer.weights = layerWeights;
39
  inChannels = layer.filters;
40
-
41
  } else if (layer.type === 'dense') {
42
  // determined when flatten layer is reached
43
  layer.weights = null;
@@ -63,210 +62,210 @@ class Cnn {
63
 
64
  for (const layer of this.architecture) {
65
  switch (layer.type) {
66
- case "conv2d":
67
  out = out.conv2d(layer.weights, layer.stride, layer.padding);
68
- if (layer.activationType === "relu") {
69
- out = out.relu();
70
- }
71
  break;
72
- case "maxpool":
73
- out = out.maxPool([layer.size, layer.size], [layer.stride, layer.stride], 0);
 
 
 
 
74
  break;
75
- case "flatten":
76
  const flatDim = out.shape[1] * out.shape[2] * out.shape[3];
77
  out = out.reshape([-1, flatDim]);
78
 
79
  // initialize dense layer weights and biases
80
  const next = this.architecture[this.architecture.indexOf(layer) + 1];
81
- if (next?.type === "dense" && next.weights === null) {
82
  next.weights = tf.variable(
83
  tf.randomUniform(
84
- [flatDim, next.units],
85
- -Math.sqrt(1 / flatDim),
86
- Math.sqrt(1 / flatDim)
87
- )
88
  );
89
  next.biases = tf.variable(tf.zeros([next.units]));
90
  }
91
  break;
92
 
93
- case "dense":
94
  out = out.matMul(layer.weights).add(layer.biases);
95
  break;
96
  }
97
  }
98
  return out;
99
- }
100
-
101
- forwardWithInfo(x) {
102
- let out = x;
103
- const info = [];
104
-
105
- const inputShape = x.shape;
106
- info.push({
107
- type: "input",
108
- output: out.dataSync(),
109
- shape: inputShape
110
- });
111
-
112
- for (const layer of this.architecture) {
113
- switch (layer.type) {
114
-
115
- case "conv2d":
116
- out = out.conv2d(layer.weights, layer.stride, layer.padding);
117
- if (layer.activationType === "relu") {
118
- out = out.relu();
119
- }
120
-
121
- info.push({
122
- type: "conv2d",
123
- output: out.dataSync(),
124
- kernels: layer.weights.dataSync(),
125
- outputShape: out.shape,
126
- kernelShape: layer.weights.shape,
127
- stride: layer.stride,
128
- padding: layer.padding,
129
- activationType: layer.activationType
130
- });
131
- break;
132
-
133
- case "maxpool":
134
- out = out.maxPool([layer.size, layer.size], [layer.stride, layer.stride], 0);
135
-
136
- info.push({
137
- type: "maxpool",
138
- output: out.dataSync(),
139
- shape: out.shape,
140
- size: layer.size,
141
- stride: layer.stride
142
- });
143
- break;
144
-
145
- case "flatten":
146
- const flatDim = out.shape[1] * out.shape[2] * out.shape[3];
147
- out = out.reshape([-1, flatDim]);
148
-
149
- info.push({
150
- type: "flatten",
151
- output: out.dataSync(),
152
- shape: out.shape
153
- });
154
-
155
- // dense init
156
- const next = this.architecture[this.architecture.indexOf(layer) + 1];
157
- if (next?.type === "dense" && next.weights === null) {
158
- next.weights = tf.variable(
159
- tf.randomUniform(
160
- [flatDim, next.units],
161
- -Math.sqrt(1 / flatDim),
162
- Math.sqrt(1 / flatDim)
163
- )
164
- );
165
- next.biases = tf.variable(tf.zeros([next.units]));
166
- }
167
- break;
168
-
169
- case "dense":
170
- out = out.matMul(layer.weights).add(layer.biases);
171
-
172
- info.push({
173
- type: "dense",
174
- output: out.dataSync(),
175
- weights: layer.weights.dataSync(),
176
- biases: layer.biases.dataSync(),
177
- outputShape: out.shape,
178
- weightShape: layer.weights.shape,
179
- biasShape: layer.biases.shape,
180
- units: layer.units
181
- });
182
- break;
183
- }
184
- }
185
-
186
- return { output: out, info: info };
187
- }
188
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
189
  }
190
 
191
  async function train(
192
- data,
193
- model,
194
- optimizer,
195
- batchSize,
196
- epochs,
197
- controller,
198
- onBatchEnd = null
199
  ) {
200
- const numBatches = Math.floor(data.trainSize / batchSize);
201
- for (let epoch = 0; epoch < epochs; ++epoch) {
202
- for (let b = 0; b < numBatches; ++b) {
203
-
204
- if (controller.stopRequested) {
205
- console.log("Training stopped");
206
- return;
207
- }
208
-
209
- while (controller.isPaused) {
210
- await tf.nextFrame();
211
- }
212
-
213
- const cost = optimizer.minimize(() => {
214
- const batch = data.nextTrainBatch(batchSize);
215
- const xs = batch.xs.reshape(
216
- [
217
- batchSize,
218
- data.imageSize,
219
- data.imageSize,
220
- data.numInputChannels
221
- ]
222
- );
223
- const preds = model.forward(xs);
224
- const lossValue = tf.losses.softmaxCrossEntropy(
225
- batch.labels, preds
226
- ).mean();
227
- return lossValue;
228
- }, true);
229
-
230
- const lossVal = (await cost.data())[0];
231
-
232
- const sample = data.getTestSample(controller.sampleIndex);
233
-
234
- const { output, info } = model.forwardWithInfo(
235
- sample.xs.reshape(
236
- [
237
- 1,
238
- data.imageSize,
239
- data.imageSize,
240
- data.numInputChannels
241
- ]
242
- )
243
- );
244
 
245
  // Add output layer info
246
  const probs = tf.tidy(() => {
247
- return tf.softmax(output);
248
- });
249
- const shape = probs.shape;
250
  info.push({
251
- type: "output",
252
  output: probs.dataSync(),
253
- shape: shape
254
  });
255
 
256
- if (controller.stopRequested) {
257
- console.log("Training stopped");
258
- return;
259
- }
260
 
261
- if (onBatchEnd) {
262
- onBatchEnd(epoch, b, lossVal, info);
263
- }
264
 
265
- await tf.nextFrame();
266
- }
267
- }
268
- console.log("Training complete");
269
  }
270
 
271
-
272
  export { load, train, Cnn };
 
2
  import { MnistData } from '../../data/mnist.js';
3
 
4
  async function load(dataset) {
5
+ let data;
6
+ if (dataset === 'mnist') {
7
+ data = new MnistData();
8
+ } else {
9
+ throw new Error(`Dataset ${dataset} not supported`);
10
+ }
11
  await data.load();
12
+ return data;
13
  }
14
 
15
  class Cnn {
 
28
  const shape = [layer.kernel, layer.kernel, inChannels, layer.filters];
29
  const layerWeights = tf.variable(
30
  tf.randomUniform(
31
+ shape,
32
+ -Math.sqrt(1 / (layer.kernel * layer.kernel * inChannels)),
33
+ Math.sqrt(1 / (layer.kernel * layer.kernel * inChannels)),
34
+ ),
35
  );
36
 
37
  weights.push(layerWeights);
38
  layer.weights = layerWeights;
39
  inChannels = layer.filters;
 
40
  } else if (layer.type === 'dense') {
41
  // determined when flatten layer is reached
42
  layer.weights = null;
 
62
 
63
  for (const layer of this.architecture) {
64
  switch (layer.type) {
65
+ case 'conv2d':
66
  out = out.conv2d(layer.weights, layer.stride, layer.padding);
67
+ if (layer.activationType === 'relu') {
68
+ out = out.relu();
69
+ }
70
  break;
71
+ case 'maxpool':
72
+ out = out.maxPool(
73
+ [layer.size, layer.size],
74
+ [layer.stride, layer.stride],
75
+ 0,
76
+ );
77
  break;
78
+ case 'flatten':
79
  const flatDim = out.shape[1] * out.shape[2] * out.shape[3];
80
  out = out.reshape([-1, flatDim]);
81
 
82
  // initialize dense layer weights and biases
83
  const next = this.architecture[this.architecture.indexOf(layer) + 1];
84
+ if (next?.type === 'dense' && next.weights === null) {
85
  next.weights = tf.variable(
86
  tf.randomUniform(
87
+ [flatDim, next.units],
88
+ -Math.sqrt(1 / flatDim),
89
+ Math.sqrt(1 / flatDim),
90
+ ),
91
  );
92
  next.biases = tf.variable(tf.zeros([next.units]));
93
  }
94
  break;
95
 
96
+ case 'dense':
97
  out = out.matMul(layer.weights).add(layer.biases);
98
  break;
99
  }
100
  }
101
  return out;
102
+ }
103
+
104
+ forwardWithInfo(x) {
105
+ let out = x;
106
+ const info = [];
107
+
108
+ const inputShape = x.shape;
109
+ info.push({
110
+ type: 'input',
111
+ output: out.dataSync(),
112
+ shape: inputShape,
113
+ });
114
+
115
+ for (const layer of this.architecture) {
116
+ switch (layer.type) {
117
+ case 'conv2d':
118
+ out = out.conv2d(layer.weights, layer.stride, layer.padding);
119
+ if (layer.activationType === 'relu') {
120
+ out = out.relu();
121
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
122
 
123
+ info.push({
124
+ type: 'conv2d',
125
+ output: out.dataSync(),
126
+ kernels: layer.weights.dataSync(),
127
+ outputShape: out.shape,
128
+ kernelShape: layer.weights.shape,
129
+ stride: layer.stride,
130
+ padding: layer.padding,
131
+ activationType: layer.activationType,
132
+ });
133
+ break;
134
+
135
+ case 'maxpool':
136
+ out = out.maxPool(
137
+ [layer.size, layer.size],
138
+ [layer.stride, layer.stride],
139
+ 0,
140
+ );
141
+
142
+ info.push({
143
+ type: 'maxpool',
144
+ output: out.dataSync(),
145
+ shape: out.shape,
146
+ size: layer.size,
147
+ stride: layer.stride,
148
+ });
149
+ break;
150
+
151
+ case 'flatten':
152
+ const flatDim = out.shape[1] * out.shape[2] * out.shape[3];
153
+ out = out.reshape([-1, flatDim]);
154
+
155
+ info.push({
156
+ type: 'flatten',
157
+ output: out.dataSync(),
158
+ shape: out.shape,
159
+ });
160
+
161
+ // dense init
162
+ const next = this.architecture[this.architecture.indexOf(layer) + 1];
163
+ if (next?.type === 'dense' && next.weights === null) {
164
+ next.weights = tf.variable(
165
+ tf.randomUniform(
166
+ [flatDim, next.units],
167
+ -Math.sqrt(1 / flatDim),
168
+ Math.sqrt(1 / flatDim),
169
+ ),
170
+ );
171
+ next.biases = tf.variable(tf.zeros([next.units]));
172
+ }
173
+ break;
174
+
175
+ case 'dense':
176
+ out = out.matMul(layer.weights).add(layer.biases);
177
+
178
+ info.push({
179
+ type: 'dense',
180
+ output: out.dataSync(),
181
+ weights: layer.weights.dataSync(),
182
+ biases: layer.biases.dataSync(),
183
+ outputShape: out.shape,
184
+ weightShape: layer.weights.shape,
185
+ biasShape: layer.biases.shape,
186
+ units: layer.units,
187
+ });
188
+ break;
189
+ }
190
+ }
191
+
192
+ return { output: out, info: info };
193
+ }
194
  }
195
 
196
  async function train(
197
+ data,
198
+ model,
199
+ optimizer,
200
+ batchSize,
201
+ epochs,
202
+ controller,
203
+ onBatchEnd = null,
204
  ) {
205
+ const numBatches = Math.floor(data.trainSize / batchSize);
206
+ for (let epoch = 0; epoch < epochs; ++epoch) {
207
+ for (let b = 0; b < numBatches; ++b) {
208
+ if (controller.stopRequested) {
209
+ console.log('Training stopped');
210
+ return;
211
+ }
212
+
213
+ while (controller.isPaused) {
214
+ await tf.nextFrame();
215
+ }
216
+
217
+ const cost = optimizer.minimize(() => {
218
+ const batch = data.nextTrainBatch(batchSize);
219
+ const xs = batch.xs.reshape([
220
+ batchSize,
221
+ data.imageSize,
222
+ data.imageSize,
223
+ data.numInputChannels,
224
+ ]);
225
+ const preds = model.forward(xs);
226
+ const lossValue = tf.losses
227
+ .softmaxCrossEntropy(batch.labels, preds)
228
+ .mean();
229
+ return lossValue;
230
+ }, true);
231
+
232
+ const lossVal = (await cost.data())[0];
233
+
234
+ const sample = data.getTestSample(controller.sampleIndex);
235
+
236
+ const { output, info } = model.forwardWithInfo(
237
+ sample.xs.reshape([
238
+ 1,
239
+ data.imageSize,
240
+ data.imageSize,
241
+ data.numInputChannels,
242
+ ]),
243
+ );
 
 
 
 
 
244
 
245
  // Add output layer info
246
  const probs = tf.tidy(() => {
247
+ return tf.softmax(output);
248
+ });
249
+ const shape = probs.shape;
250
  info.push({
251
+ type: 'output',
252
  output: probs.dataSync(),
253
+ shape: shape,
254
  });
255
 
256
+ if (controller.stopRequested) {
257
+ console.log('Training stopped');
258
+ return;
259
+ }
260
 
261
+ if (onBatchEnd) {
262
+ onBatchEnd(epoch, b, lossVal, info);
263
+ }
264
 
265
+ await tf.nextFrame();
266
+ }
267
+ }
268
+ console.log('Training complete');
269
  }
270
 
 
271
  export { load, train, Cnn };
src/data/convolution_presets.js CHANGED
@@ -1,5 +1,5 @@
1
  const PRESETS_COLOR = {
2
- "Laplacian": [
3
  [
4
  [-1, -1, -1],
5
  [-1, 8, -1],
@@ -16,7 +16,7 @@ const PRESETS_COLOR = {
16
  [-1, -1, -1],
17
  ],
18
  ],
19
- "Red": [
20
  [
21
  [0, 0, 0],
22
  [0, 1, 0],
@@ -33,7 +33,7 @@ const PRESETS_COLOR = {
33
  [0, 0, 0],
34
  ],
35
  ],
36
- "Green": [
37
  [
38
  [0, 0, 0],
39
  [0, 0, 0],
@@ -50,7 +50,7 @@ const PRESETS_COLOR = {
50
  [0, 0, 0],
51
  ],
52
  ],
53
- "Blue": [
54
  [
55
  [0, 0, 0],
56
  [0, 0, 0],
@@ -70,7 +70,7 @@ const PRESETS_COLOR = {
70
  };
71
 
72
  const PRESETS_GRAY = {
73
- "Laplacian": [
74
  [
75
  [-1, -1, -1],
76
  [-1, 8, -1],
@@ -87,7 +87,7 @@ const PRESETS_GRAY = {
87
  [-1, -1, -1],
88
  ],
89
  ],
90
- "Sobel X": [
91
  [
92
  [-1, 0, 1],
93
  [-2, 0, 2],
@@ -104,7 +104,7 @@ const PRESETS_GRAY = {
104
  [-1, 0, 1],
105
  ],
106
  ],
107
- "Sobel Y": [
108
  [
109
  [-1, -2, -1],
110
  [0, 0, 0],
@@ -121,7 +121,7 @@ const PRESETS_GRAY = {
121
  [1, 2, 1],
122
  ],
123
  ],
124
- "Sharpen": [
125
  [
126
  [0, -1, 0],
127
  [-1, 5, -1],
@@ -138,7 +138,7 @@ const PRESETS_GRAY = {
138
  [0, -1, 0],
139
  ],
140
  ],
141
- "Box Blur": [
142
  [
143
  [0.11, 0.11, 0.11],
144
  [0.11, 0.11, 0.11],
@@ -157,5 +157,4 @@ const PRESETS_GRAY = {
157
  ],
158
  };
159
 
160
-
161
  export { PRESETS_COLOR, PRESETS_GRAY };
 
1
  const PRESETS_COLOR = {
2
+ Laplacian: [
3
  [
4
  [-1, -1, -1],
5
  [-1, 8, -1],
 
16
  [-1, -1, -1],
17
  ],
18
  ],
19
+ Red: [
20
  [
21
  [0, 0, 0],
22
  [0, 1, 0],
 
33
  [0, 0, 0],
34
  ],
35
  ],
36
+ Green: [
37
  [
38
  [0, 0, 0],
39
  [0, 0, 0],
 
50
  [0, 0, 0],
51
  ],
52
  ],
53
+ Blue: [
54
  [
55
  [0, 0, 0],
56
  [0, 0, 0],
 
70
  };
71
 
72
  const PRESETS_GRAY = {
73
+ Laplacian: [
74
  [
75
  [-1, -1, -1],
76
  [-1, 8, -1],
 
87
  [-1, -1, -1],
88
  ],
89
  ],
90
+ 'Sobel X': [
91
  [
92
  [-1, 0, 1],
93
  [-2, 0, 2],
 
104
  [-1, 0, 1],
105
  ],
106
  ],
107
+ 'Sobel Y': [
108
  [
109
  [-1, -2, -1],
110
  [0, 0, 0],
 
121
  [1, 2, 1],
122
  ],
123
  ],
124
+ Sharpen: [
125
  [
126
  [0, -1, 0],
127
  [-1, 5, -1],
 
138
  [0, -1, 0],
139
  ],
140
  ],
141
+ 'Box Blur': [
142
  [
143
  [0.11, 0.11, 0.11],
144
  [0.11, 0.11, 0.11],
 
157
  ],
158
  };
159
 
 
160
  export { PRESETS_COLOR, PRESETS_GRAY };
src/data/mnist.js CHANGED
@@ -27,9 +27,9 @@ const NUM_TRAIN_ELEMENTS = Math.floor(TRAIN_TEST_RATIO * NUM_DATASET_ELEMENTS);
27
  const NUM_TEST_ELEMENTS = NUM_DATASET_ELEMENTS - NUM_TRAIN_ELEMENTS;
28
 
29
  const MNIST_IMAGES_SPRITE_PATH =
30
- 'https://storage.googleapis.com/learnjs-data/model-builder/mnist_images.png';
31
  const MNIST_LABELS_PATH =
32
- 'https://storage.googleapis.com/learnjs-data/model-builder/mnist_labels_uint8';
33
 
34
  /**
35
  * A class that fetches the sprited MNIST dataset and returns shuffled batches.
@@ -42,11 +42,11 @@ export class MnistData {
42
  this.shuffledTrainIndex = 0;
43
  this.shuffledTestIndex = 0;
44
 
45
- this.numClasses = NUM_CLASSES;
46
- this.numInputChannels = 1;
47
- this.trainSize = NUM_TRAIN_ELEMENTS;
48
- this.testSize = NUM_TEST_ELEMENTS;
49
- this.imageSize = 28;
50
  }
51
 
52
  async load() {
@@ -60,8 +60,9 @@ export class MnistData {
60
  img.width = img.naturalWidth;
61
  img.height = img.naturalHeight;
62
 
63
- const datasetBytesBuffer =
64
- new ArrayBuffer(NUM_DATASET_ELEMENTS * IMAGE_SIZE * 4);
 
65
 
66
  const chunkSize = 5000;
67
  canvas.width = img.width;
@@ -69,11 +70,21 @@ export class MnistData {
69
 
70
  for (let i = 0; i < NUM_DATASET_ELEMENTS / chunkSize; i++) {
71
  const datasetBytesView = new Float32Array(
72
- datasetBytesBuffer, i * IMAGE_SIZE * chunkSize * 4,
73
- IMAGE_SIZE * chunkSize);
 
 
74
  ctx.drawImage(
75
- img, 0, i * chunkSize, img.width, chunkSize, 0, 0, img.width,
76
- chunkSize);
 
 
 
 
 
 
 
 
77
 
78
  const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
79
 
@@ -91,8 +102,10 @@ export class MnistData {
91
  });
92
 
93
  const labelsRequest = fetch(MNIST_LABELS_PATH);
94
- const [imgResponse, labelsResponse] =
95
- await Promise.all([imgRequest, labelsRequest]);
 
 
96
 
97
  this.datasetLabels = new Uint8Array(await labelsResponse.arrayBuffer());
98
 
@@ -102,28 +115,36 @@ export class MnistData {
102
  this.testIndices = tf.util.createShuffledIndices(NUM_TEST_ELEMENTS);
103
 
104
  // Slice the the images and labels into train and test sets.
105
- this.trainImages =
106
- this.datasetImages.slice(0, IMAGE_SIZE * NUM_TRAIN_ELEMENTS);
 
 
107
  this.testImages = this.datasetImages.slice(IMAGE_SIZE * NUM_TRAIN_ELEMENTS);
108
- this.trainLabels =
109
- this.datasetLabels.slice(0, NUM_CLASSES * NUM_TRAIN_ELEMENTS);
110
- this.testLabels =
111
- this.datasetLabels.slice(NUM_CLASSES * NUM_TRAIN_ELEMENTS);
 
 
 
112
  }
113
 
114
  nextTrainBatch(batchSize) {
115
  return this.nextBatch(
116
- batchSize, [this.trainImages, this.trainLabels], () => {
117
- this.shuffledTrainIndex =
118
- (this.shuffledTrainIndex + 1) % this.trainIndices.length;
119
- return this.trainIndices[this.shuffledTrainIndex];
120
- });
 
 
 
121
  }
122
 
123
  nextTestBatch(batchSize) {
124
  return this.nextBatch(batchSize, [this.testImages, this.testLabels], () => {
125
  this.shuffledTestIndex =
126
- (this.shuffledTestIndex + 1) % this.testIndices.length;
127
  return this.testIndices[this.shuffledTestIndex];
128
  });
129
  }
@@ -135,31 +156,40 @@ export class MnistData {
135
  for (let i = 0; i < batchSize; i++) {
136
  const idx = index();
137
 
138
- const image =
139
- data[0].slice(idx * IMAGE_SIZE, idx * IMAGE_SIZE + IMAGE_SIZE);
 
 
140
  batchImagesArray.set(image, i * IMAGE_SIZE);
141
 
142
- const label =
143
- data[1].slice(idx * NUM_CLASSES, idx * NUM_CLASSES + NUM_CLASSES);
 
 
144
  batchLabelsArray.set(label, i * NUM_CLASSES);
145
  }
146
 
147
  const xs = tf.tensor2d(batchImagesArray, [batchSize, IMAGE_SIZE]);
148
  const labels = tf.tensor2d(batchLabelsArray, [batchSize, NUM_CLASSES]);
149
 
150
- return {xs, labels};
151
  }
152
 
153
- getTestSample(index) {
154
- const idx = index % NUM_TEST_ELEMENTS;
155
 
156
- const image = this.testImages.slice(idx * IMAGE_SIZE, (idx + 1) * IMAGE_SIZE);
157
- const label = this.testLabels.slice(idx * NUM_CLASSES, (idx + 1) * NUM_CLASSES);
 
 
 
 
 
 
158
 
159
- const xs = tf.tensor2d(image, [1, IMAGE_SIZE]);
160
- const labels = tf.tensor2d(label, [1, NUM_CLASSES]);
161
-
162
- return { xs, labels };
163
- }
164
 
 
 
165
  }
 
27
  const NUM_TEST_ELEMENTS = NUM_DATASET_ELEMENTS - NUM_TRAIN_ELEMENTS;
28
 
29
  const MNIST_IMAGES_SPRITE_PATH =
30
+ 'https://storage.googleapis.com/learnjs-data/model-builder/mnist_images.png';
31
  const MNIST_LABELS_PATH =
32
+ 'https://storage.googleapis.com/learnjs-data/model-builder/mnist_labels_uint8';
33
 
34
  /**
35
  * A class that fetches the sprited MNIST dataset and returns shuffled batches.
 
42
  this.shuffledTrainIndex = 0;
43
  this.shuffledTestIndex = 0;
44
 
45
+ this.numClasses = NUM_CLASSES;
46
+ this.numInputChannels = 1;
47
+ this.trainSize = NUM_TRAIN_ELEMENTS;
48
+ this.testSize = NUM_TEST_ELEMENTS;
49
+ this.imageSize = 28;
50
  }
51
 
52
  async load() {
 
60
  img.width = img.naturalWidth;
61
  img.height = img.naturalHeight;
62
 
63
+ const datasetBytesBuffer = new ArrayBuffer(
64
+ NUM_DATASET_ELEMENTS * IMAGE_SIZE * 4,
65
+ );
66
 
67
  const chunkSize = 5000;
68
  canvas.width = img.width;
 
70
 
71
  for (let i = 0; i < NUM_DATASET_ELEMENTS / chunkSize; i++) {
72
  const datasetBytesView = new Float32Array(
73
+ datasetBytesBuffer,
74
+ i * IMAGE_SIZE * chunkSize * 4,
75
+ IMAGE_SIZE * chunkSize,
76
+ );
77
  ctx.drawImage(
78
+ img,
79
+ 0,
80
+ i * chunkSize,
81
+ img.width,
82
+ chunkSize,
83
+ 0,
84
+ 0,
85
+ img.width,
86
+ chunkSize,
87
+ );
88
 
89
  const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
90
 
 
102
  });
103
 
104
  const labelsRequest = fetch(MNIST_LABELS_PATH);
105
+ const [imgResponse, labelsResponse] = await Promise.all([
106
+ imgRequest,
107
+ labelsRequest,
108
+ ]);
109
 
110
  this.datasetLabels = new Uint8Array(await labelsResponse.arrayBuffer());
111
 
 
115
  this.testIndices = tf.util.createShuffledIndices(NUM_TEST_ELEMENTS);
116
 
117
  // Slice the the images and labels into train and test sets.
118
+ this.trainImages = this.datasetImages.slice(
119
+ 0,
120
+ IMAGE_SIZE * NUM_TRAIN_ELEMENTS,
121
+ );
122
  this.testImages = this.datasetImages.slice(IMAGE_SIZE * NUM_TRAIN_ELEMENTS);
123
+ this.trainLabels = this.datasetLabels.slice(
124
+ 0,
125
+ NUM_CLASSES * NUM_TRAIN_ELEMENTS,
126
+ );
127
+ this.testLabels = this.datasetLabels.slice(
128
+ NUM_CLASSES * NUM_TRAIN_ELEMENTS,
129
+ );
130
  }
131
 
132
  nextTrainBatch(batchSize) {
133
  return this.nextBatch(
134
+ batchSize,
135
+ [this.trainImages, this.trainLabels],
136
+ () => {
137
+ this.shuffledTrainIndex =
138
+ (this.shuffledTrainIndex + 1) % this.trainIndices.length;
139
+ return this.trainIndices[this.shuffledTrainIndex];
140
+ },
141
+ );
142
  }
143
 
144
  nextTestBatch(batchSize) {
145
  return this.nextBatch(batchSize, [this.testImages, this.testLabels], () => {
146
  this.shuffledTestIndex =
147
+ (this.shuffledTestIndex + 1) % this.testIndices.length;
148
  return this.testIndices[this.shuffledTestIndex];
149
  });
150
  }
 
156
  for (let i = 0; i < batchSize; i++) {
157
  const idx = index();
158
 
159
+ const image = data[0].slice(
160
+ idx * IMAGE_SIZE,
161
+ idx * IMAGE_SIZE + IMAGE_SIZE,
162
+ );
163
  batchImagesArray.set(image, i * IMAGE_SIZE);
164
 
165
+ const label = data[1].slice(
166
+ idx * NUM_CLASSES,
167
+ idx * NUM_CLASSES + NUM_CLASSES,
168
+ );
169
  batchLabelsArray.set(label, i * NUM_CLASSES);
170
  }
171
 
172
  const xs = tf.tensor2d(batchImagesArray, [batchSize, IMAGE_SIZE]);
173
  const labels = tf.tensor2d(batchLabelsArray, [batchSize, NUM_CLASSES]);
174
 
175
+ return { xs, labels };
176
  }
177
 
178
+ getTestSample(index) {
179
+ const idx = index % NUM_TEST_ELEMENTS;
180
 
181
+ const image = this.testImages.slice(
182
+ idx * IMAGE_SIZE,
183
+ (idx + 1) * IMAGE_SIZE,
184
+ );
185
+ const label = this.testLabels.slice(
186
+ idx * NUM_CLASSES,
187
+ (idx + 1) * NUM_CLASSES,
188
+ );
189
 
190
+ const xs = tf.tensor2d(image, [1, IMAGE_SIZE]);
191
+ const labels = tf.tensor2d(label, [1, NUM_CLASSES]);
 
 
 
192
 
193
+ return { xs, labels };
194
+ }
195
  }
src/main.jsx CHANGED
@@ -1,10 +1,10 @@
1
- import { StrictMode } from 'react'
2
- import { createRoot } from 'react-dom/client'
3
- import './index.css'
4
- import App from './App.jsx'
5
 
6
  createRoot(document.getElementById('root')).render(
7
  <StrictMode>
8
  <App />
9
  </StrictMode>,
10
- )
 
1
+ import { StrictMode } from 'react';
2
+ import { createRoot } from 'react-dom/client';
3
+ import './index.css';
4
+ import App from './App.jsx';
5
 
6
  createRoot(document.getElementById('root')).render(
7
  <StrictMode>
8
  <App />
9
  </StrictMode>,
10
+ );
src/styles.css CHANGED
@@ -1,106 +1,106 @@
1
  .card {
2
- background-color: #fff;
3
- border: 1px solid #dcdfe6;
4
- border-radius: 12px;
5
- padding: 18px;
6
- display: flex;
7
- flex-direction: column;
8
- gap: 24px;
9
- width: 300px;
10
- /* box-shadow: 0 10px 40px rgba(47, 61, 103, 0.08); */
11
  }
12
 
13
  .field {
14
- display: flex;
15
- flex-direction: column;
16
- gap: 6px;
17
  }
18
 
19
  .field label,
20
  .field legend {
21
- font-weight: 600;
22
- color: #2c2f37;
23
- align-self: flex-start;
24
  }
25
 
26
- .field input[type="text"],
27
- .field input[type="number"],
28
  .field textarea {
29
- border: 1px solid #ccd0dd;
30
- border-radius: 6px;
31
- padding: 8px 10px;
32
- font-size: 0.95rem;
33
- font-family: inherit;
34
- background-color: #fafafe;
35
  }
36
 
37
  .field textarea {
38
- resize: vertical;
39
  }
40
 
41
  .field input[readonly],
42
  .field textarea[readonly] {
43
- background-color: #f4f6fc;
44
- color: #4a4f5c;
45
  }
46
 
47
  .double-field {
48
- display: flex;
49
- gap: 6px;
50
- width: 100%;
51
- justify-content: space-between;
52
  }
53
 
54
  .double-field .field {
55
- width: 140px;
56
  }
57
 
58
  .input-button {
59
- display: grid;
60
- grid-template-columns: 1fr 1fr;
61
- gap: 8px;
62
  }
63
 
64
  .fieldset {
65
- border: 1px solid #ccd0dd;
66
- border-radius: 6px;
67
- padding: 10px 12px 12px 12px;
68
- display: flex;
69
- flex-direction: column;
70
- gap: 6px;
71
  }
72
 
73
  .fieldset label {
74
- font-weight: 500;
75
- color: #343844;
76
  }
77
 
78
  .slider-field {
79
- gap: 10px;
80
  }
81
 
82
  .slider-control {
83
- display: flex;
84
- align-items: center;
85
- gap: 12px;
86
  }
87
 
88
- .slider-control input[type="range"] {
89
- width: 80px;
90
- -webkit-appearance: none;
91
- background-color: lightgray;
92
- border-radius: 6px;
93
- accent-color: var(--accent-color);
94
- flex: 1;
95
  }
96
 
97
- .slider-control input[type="range"]::-webkit-slider-runnable-track {
98
  height: 6px;
99
  background: lightgray;
100
  border-radius: 6px;
101
  }
102
 
103
- .slider-control input[type="range"]::-webkit-slider-thumb {
104
  -webkit-appearance: none;
105
  width: 14px;
106
  height: 14px;
@@ -112,7 +112,7 @@
112
  }
113
 
114
  .slider-control span {
115
- min-width: 28px;
116
- text-align: right;
117
- font-weight: 600;
118
- }
 
1
  .card {
2
+ background-color: #fff;
3
+ border: 1px solid #dcdfe6;
4
+ border-radius: 12px;
5
+ padding: 18px;
6
+ display: flex;
7
+ flex-direction: column;
8
+ gap: 24px;
9
+ width: 300px;
10
+ /* box-shadow: 0 10px 40px rgba(47, 61, 103, 0.08); */
11
  }
12
 
13
  .field {
14
+ display: flex;
15
+ flex-direction: column;
16
+ gap: 6px;
17
  }
18
 
19
  .field label,
20
  .field legend {
21
+ font-weight: 600;
22
+ color: #2c2f37;
23
+ align-self: flex-start;
24
  }
25
 
26
+ .field input[type='text'],
27
+ .field input[type='number'],
28
  .field textarea {
29
+ border: 1px solid #ccd0dd;
30
+ border-radius: 6px;
31
+ padding: 8px 10px;
32
+ font-size: 0.95rem;
33
+ font-family: inherit;
34
+ background-color: #fafafe;
35
  }
36
 
37
  .field textarea {
38
+ resize: vertical;
39
  }
40
 
41
  .field input[readonly],
42
  .field textarea[readonly] {
43
+ background-color: #f4f6fc;
44
+ color: #4a4f5c;
45
  }
46
 
47
  .double-field {
48
+ display: flex;
49
+ gap: 6px;
50
+ width: 100%;
51
+ justify-content: space-between;
52
  }
53
 
54
  .double-field .field {
55
+ width: 140px;
56
  }
57
 
58
  .input-button {
59
+ display: grid;
60
+ grid-template-columns: 1fr 1fr;
61
+ gap: 8px;
62
  }
63
 
64
  .fieldset {
65
+ border: 1px solid #ccd0dd;
66
+ border-radius: 6px;
67
+ padding: 10px 12px 12px 12px;
68
+ display: flex;
69
+ flex-direction: column;
70
+ gap: 6px;
71
  }
72
 
73
  .fieldset label {
74
+ font-weight: 500;
75
+ color: #343844;
76
  }
77
 
78
  .slider-field {
79
+ gap: 10px;
80
  }
81
 
82
  .slider-control {
83
+ display: flex;
84
+ align-items: center;
85
+ gap: 12px;
86
  }
87
 
88
+ .slider-control input[type='range'] {
89
+ width: 80px;
90
+ -webkit-appearance: none;
91
+ background-color: lightgray;
92
+ border-radius: 6px;
93
+ accent-color: var(--accent-color);
94
+ flex: 1;
95
  }
96
 
97
+ .slider-control input[type='range']::-webkit-slider-runnable-track {
98
  height: 6px;
99
  background: lightgray;
100
  border-radius: 6px;
101
  }
102
 
103
+ .slider-control input[type='range']::-webkit-slider-thumb {
104
  -webkit-appearance: none;
105
  width: 14px;
106
  height: 14px;
 
112
  }
113
 
114
  .slider-control span {
115
+ min-width: 28px;
116
+ text-align: right;
117
+ font-weight: 600;
118
+ }
src/utils/convolution.js CHANGED
@@ -6,13 +6,12 @@ export function convolveGray(image, kernel) {
6
  const height = image.height;
7
  const inputData = image.data;
8
 
9
- const outputWidth= width - kernelWidth + 1;
10
  const outputHeight = height - kernelHeight + 1;
11
  const outputData = new Uint8ClampedArray(outputWidth * outputHeight * 4);
12
 
13
  for (let y = 0; y < outputHeight; ++y) {
14
  for (let x = 0; x < outputWidth; ++x) {
15
-
16
  // dot product
17
  let sum = 0;
18
  for (let ky = 0; ky < kernelHeight; ++ky) {
@@ -26,16 +25,16 @@ export function convolveGray(image, kernel) {
26
 
27
  const outputIndex = (y * outputWidth + x) * 4;
28
  const clampedValue = Math.min(Math.max(sum, 0), 255);
29
- outputData[outputIndex] = clampedValue; // R
30
  outputData[outputIndex + 1] = clampedValue; // G
31
  outputData[outputIndex + 2] = clampedValue; // B
32
- outputData[outputIndex + 3] = 255; // A
33
  }
34
  }
35
 
36
  return new ImageData(outputData, outputWidth, outputHeight);
37
  }
38
-
39
  export function convolveColor(image, kernel) {
40
  const kernelWidth = kernel[0][0].length;
41
  const kernelHeight = kernel[0].length;
@@ -44,13 +43,12 @@ export function convolveColor(image, kernel) {
44
  const height = image.height;
45
  const inputData = image.data;
46
 
47
- const outputWidth= width - kernelWidth + 1;
48
  const outputHeight = height - kernelHeight + 1;
49
  const outputData = new Uint8ClampedArray(outputWidth * outputHeight * 4);
50
 
51
  for (let y = 0; y < outputHeight; ++y) {
52
  for (let x = 0; x < outputWidth; ++x) {
53
-
54
  // dot product over 3 channels
55
  let sum = 0;
56
  for (let ky = 0; ky < kernelHeight; ++ky) {
@@ -59,7 +57,7 @@ export function convolveColor(image, kernel) {
59
  const r = inputData[pixelIndex];
60
  const g = inputData[pixelIndex + 1];
61
  const b = inputData[pixelIndex + 2];
62
-
63
  const kernelR = kernel[0][ky][kx];
64
  const kernelG = kernel[1][ky][kx];
65
  const kernelB = kernel[2][ky][kx];
@@ -69,14 +67,12 @@ export function convolveColor(image, kernel) {
69
 
70
  const outputIndex = (y * outputWidth + x) * 4;
71
  const clampedValue = Math.min(Math.max(sum, 0), 255);
72
- outputData[outputIndex] = clampedValue; // R
73
  outputData[outputIndex + 1] = clampedValue; // G
74
  outputData[outputIndex + 2] = clampedValue; // B
75
- outputData[outputIndex + 3] = 255; // A
76
  }
77
  }
78
 
79
  return new ImageData(outputData, outputWidth, outputHeight);
80
  }
81
-
82
-
 
6
  const height = image.height;
7
  const inputData = image.data;
8
 
9
+ const outputWidth = width - kernelWidth + 1;
10
  const outputHeight = height - kernelHeight + 1;
11
  const outputData = new Uint8ClampedArray(outputWidth * outputHeight * 4);
12
 
13
  for (let y = 0; y < outputHeight; ++y) {
14
  for (let x = 0; x < outputWidth; ++x) {
 
15
  // dot product
16
  let sum = 0;
17
  for (let ky = 0; ky < kernelHeight; ++ky) {
 
25
 
26
  const outputIndex = (y * outputWidth + x) * 4;
27
  const clampedValue = Math.min(Math.max(sum, 0), 255);
28
+ outputData[outputIndex] = clampedValue; // R
29
  outputData[outputIndex + 1] = clampedValue; // G
30
  outputData[outputIndex + 2] = clampedValue; // B
31
+ outputData[outputIndex + 3] = 255; // A
32
  }
33
  }
34
 
35
  return new ImageData(outputData, outputWidth, outputHeight);
36
  }
37
+
38
  export function convolveColor(image, kernel) {
39
  const kernelWidth = kernel[0][0].length;
40
  const kernelHeight = kernel[0].length;
 
43
  const height = image.height;
44
  const inputData = image.data;
45
 
46
+ const outputWidth = width - kernelWidth + 1;
47
  const outputHeight = height - kernelHeight + 1;
48
  const outputData = new Uint8ClampedArray(outputWidth * outputHeight * 4);
49
 
50
  for (let y = 0; y < outputHeight; ++y) {
51
  for (let x = 0; x < outputWidth; ++x) {
 
52
  // dot product over 3 channels
53
  let sum = 0;
54
  for (let ky = 0; ky < kernelHeight; ++ky) {
 
57
  const r = inputData[pixelIndex];
58
  const g = inputData[pixelIndex + 1];
59
  const b = inputData[pixelIndex + 2];
60
+
61
  const kernelR = kernel[0][ky][kx];
62
  const kernelG = kernel[1][ky][kx];
63
  const kernelB = kernel[2][ky][kx];
 
67
 
68
  const outputIndex = (y * outputWidth + x) * 4;
69
  const clampedValue = Math.min(Math.max(sum, 0), 255);
70
+ outputData[outputIndex] = clampedValue; // R
71
  outputData[outputIndex + 1] = clampedValue; // G
72
  outputData[outputIndex + 2] = clampedValue; // B
73
+ outputData[outputIndex + 3] = 255; // A
74
  }
75
  }
76
 
77
  return new ImageData(outputData, outputWidth, outputHeight);
78
  }
 
 
src/utils/image.js CHANGED
@@ -35,4 +35,3 @@ export function convertToGrayscale(imageData) {
35
 
36
  return grayData;
37
  }
38
-
 
35
 
36
  return grayData;
37
  }