Spaces:
Running
Running
Commit ·
5e54979
1
Parent(s): 89ce55d
Fix formatting
Browse files- src/App.jsx +16 -20
- src/components/Button/Button.css +10 -10
- src/components/Button/Button.jsx +10 -17
- src/components/Switch/Switch.css +1 -2
- src/components/Switch/Switch.jsx +1 -1
- src/components/Tabs/Tabs.css +3 -4
- src/components/Tabs/Tabs.jsx +3 -5
- src/components/convolution/ConvolutionViewer.jsx +22 -27
- src/components/convolution/ConvolutionVisualizer.jsx +19 -21
- src/components/convolution/InputSelector.jsx +8 -10
- src/components/convolution/KernelEditor.css +17 -14
- src/components/convolution/KernelEditor.jsx +32 -30
- src/components/convolution/PresetSelector.css +0 -1
- src/components/convolution/PresetSelector.jsx +8 -12
- src/components/convolution/Sidebar.css +4 -5
- src/components/convolution/Sidebar.jsx +12 -16
- src/components/network/ArchitectureEditor.css +39 -39
- src/components/network/ArchitectureEditor.jsx +5 -10
- src/components/network/Conv2dLayerViewer.jsx +67 -54
- src/components/network/InfoViewer.jsx +52 -54
- src/components/network/InputViewer.jsx +26 -29
- src/components/network/LayerViewer.css +5 -5
- src/components/network/MaxPoolLayerViewer.jsx +62 -58
- src/components/network/NetworkViewer.jsx +17 -21
- src/components/network/NetworkVisualizer.jsx +144 -152
- src/components/network/OptimizerSettings.jsx +120 -72
- src/components/network/OutputLayerViewer.jsx +5 -4
- src/components/network/Sidebar.jsx +27 -30
- src/components/network/old/AccPlot.jsx +18 -18
- src/components/network/old/ActivationViewer.jsx +10 -18
- src/components/network/old/ArchitectureEditor.css +39 -39
- src/components/network/old/ArchitectureEditor.jsx +29 -40
- src/components/network/old/KernelViewer.jsx +0 -1
- src/components/network/old/LossPlot.jsx +38 -38
- src/components/network/old/MnistTrainer.css +8 -9
- src/components/network/old/MnistTrainer.jsx +139 -159
- src/components/network/old/mnist.js +52 -27
- src/components/network/train.js +181 -182
- src/data/convolution_presets.js +9 -10
- src/data/mnist.js +71 -41
- src/main.jsx +5 -5
- src/styles.css +59 -59
- src/utils/convolution.js +8 -12
- src/utils/image.js +0 -1
src/App.jsx
CHANGED
|
@@ -1,30 +1,26 @@
|
|
| 1 |
-
import './App.css'
|
| 2 |
-
import ConvolutionVisualizer from
|
| 3 |
-
import NetworkVisualizer from
|
| 4 |
-
import Tabs from
|
| 5 |
-
|
| 6 |
|
| 7 |
function App() {
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
|
| 19 |
return (
|
| 20 |
<>
|
| 21 |
<h1>CNN Visualizer</h1>
|
| 22 |
-
|
| 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 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
}
|
| 9 |
|
| 10 |
.app-button:focus {
|
| 11 |
-
|
| 12 |
}
|
| 13 |
|
| 14 |
.app-button:hover {
|
| 15 |
-
|
| 16 |
}
|
| 17 |
|
| 18 |
.app-button.selected {
|
| 19 |
-
|
| 20 |
-
|
| 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
|
| 2 |
|
| 3 |
export default function Button(props) {
|
| 4 |
-
|
| 5 |
-
selected = false,
|
| 6 |
-
children,
|
| 7 |
-
className = "",
|
| 8 |
-
...rest
|
| 9 |
-
} = props;
|
| 10 |
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
className
|
| 15 |
-
].join(" ");
|
| 16 |
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 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
|
| 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 |
-
|
| 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
|
| 2 |
-
import
|
| 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 ?
|
| 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
|
| 2 |
-
import { getImageData, convertToGrayscale } from
|
| 3 |
-
import { convolveGray, convolveColor } from
|
| 4 |
-
import
|
| 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(
|
| 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(
|
| 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
|
| 54 |
-
(
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
v === undefined ||
|
| 58 |
-
|
| 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(
|
| 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 |
-
{
|
| 91 |
-
<img src={inputUrl} alt="Input" />
|
| 92 |
-
) : ( <p>Loading...</p>
|
| 93 |
-
)}
|
| 94 |
</div>
|
| 95 |
<div className="convolution-viewer-item">
|
| 96 |
<h3>Output</h3>
|
| 97 |
-
{
|
| 98 |
<img src={outputUrl} alt="Output" />
|
| 99 |
-
) : (
|
|
|
|
| 100 |
)}
|
| 101 |
</div>
|
| 102 |
-
<canvas ref={canvasRef} style={{ display:
|
| 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
|
| 4 |
-
import Sidebar from
|
| 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,
|
| 15 |
[-1, -1, -1],
|
| 16 |
],
|
| 17 |
[
|
| 18 |
[-1, -1, -1],
|
| 19 |
-
[-1,
|
| 20 |
[-1, -1, -1],
|
| 21 |
],
|
| 22 |
[
|
| 23 |
[-1, -1, -1],
|
| 24 |
-
[-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:
|
| 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
|
| 2 |
-
import Switch from
|
| 3 |
-
import Button from
|
| 4 |
-
import
|
| 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:
|
| 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 |
-
|
| 8 |
}
|
| 9 |
|
| 10 |
.kernel-matrix {
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
}
|
| 15 |
|
| 16 |
.kernel-row {
|
|
@@ -19,11 +19,11 @@
|
|
| 19 |
}
|
| 20 |
|
| 21 |
.kernel-size-options {
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
}
|
| 28 |
|
| 29 |
.kernel-size-options form {
|
|
@@ -32,7 +32,7 @@
|
|
| 32 |
align-items: center;
|
| 33 |
}
|
| 34 |
|
| 35 |
-
.kernel-size-options input[type=
|
| 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:
|
|
|
|
|
|
|
|
|
|
| 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=
|
| 84 |
-
input[type=
|
| 85 |
-webkit-appearance: none;
|
| 86 |
margin: 0;
|
| 87 |
}
|
| 88 |
|
| 89 |
-
input[type=
|
| 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
|
| 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);
|
| 11 |
|
| 12 |
function handleChange(channel, row, col, value) {
|
| 13 |
const newKernel = props.kernel.map((r) => [...r]);
|
| 14 |
-
newKernel[channel][row][col] = 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 |
-
|
| 79 |
-
|
| 80 |
>
|
| 81 |
R
|
| 82 |
</Button>
|
| 83 |
<Button
|
| 84 |
-
|
| 85 |
-
|
| 86 |
>
|
| 87 |
G
|
| 88 |
</Button>
|
| 89 |
<Button
|
| 90 |
-
|
| 91 |
-
|
| 92 |
>
|
| 93 |
B
|
| 94 |
</Button>
|
| 95 |
-
</div>
|
| 96 |
</div>
|
| 97 |
)}
|
| 98 |
<div className="kernel-matrix">
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
|
|
|
|
|
|
| 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
|
| 2 |
-
import Button from
|
| 3 |
-
import { PRESETS_COLOR, PRESETS_GRAY } from
|
| 4 |
-
import
|
| 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(
|
| 24 |
-
console.log(
|
| 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: #
|
| 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
|
| 2 |
-
import Tabs from
|
| 3 |
-
import InputSelector from
|
| 4 |
-
import
|
| 5 |
|
| 6 |
-
|
| 7 |
-
const DEFAULT_TAB = "Input";
|
| 8 |
|
| 9 |
function Sidebar(props) {
|
| 10 |
const tabData = [
|
| 11 |
{
|
| 12 |
-
label:
|
| 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:
|
| 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
|
| 2 |
-
import Button from
|
| 3 |
-
import
|
| 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:
|
| 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
|
| 2 |
-
import
|
| 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 =
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 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;
|
| 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 =
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 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;
|
| 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 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
return extractKernels(props.kernels, selectedChannel, kh, kw, inC, outC);
|
| 107 |
}, [props.tick, selectedChannel]);
|
| 108 |
|
| 109 |
const kernelSrcs = useMemo(() => {
|
| 110 |
-
|
| 111 |
-
|
| 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>
|
| 130 |
-
|
| 131 |
-
<
|
| 132 |
-
<div>
|
| 133 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 134 |
</div>
|
| 135 |
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
|
| 150 |
|
| 151 |
<h4>Activation Maps (min-max normalized)</h4>
|
| 152 |
<div className="layer-grid">
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
|
| 157 |
-
|
| 158 |
-
onClick={() =>
|
| 159 |
-
|
| 160 |
-
|
| 161 |
-
style={{
|
| 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
|
| 2 |
-
import MaxPoolLayerViewer from
|
| 3 |
-
import InputViewer from
|
| 4 |
-
import OutputLayerViewer from
|
| 5 |
|
| 6 |
function InfoViewer(props) {
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
onSampleIndexChange={props.onSampleIndexChange}
|
| 16 |
tick={props.tick}
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
key={idx}
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
tick={props.tick}
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
key={idx}
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
tick={props.tick}
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 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 |
-
|
| 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
|
| 2 |
-
import
|
| 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 =
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 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 |
-
|
| 41 |
|
| 42 |
-
|
| 43 |
-
|
| 44 |
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
|
|
|
|
|
|
| 52 |
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
<img key={0} src={imgSrc} alt={`Input Image`} />
|
| 57 |
-
)}
|
| 58 |
<Button onClick={props.onSampleIndexChange}>New Sample</Button>
|
| 59 |
-
|
| 60 |
-
|
| 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 |
-
|
| 17 |
-
|
| 18 |
margin-bottom: 12px;
|
| 19 |
color: #444;
|
| 20 |
font-size: 14px;
|
|
@@ -27,7 +27,7 @@
|
|
| 27 |
}
|
| 28 |
|
| 29 |
.layer-grid img {
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 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
|
| 2 |
-
import
|
| 3 |
-
|
| 4 |
|
| 5 |
function extractActivationMaps(activations, H, W, C) {
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
|
|
|
| 31 |
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
|
| 43 |
-
|
| 44 |
}
|
| 45 |
|
| 46 |
function imgDataToSrc(imgData) {
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
}
|
| 54 |
|
| 55 |
function MaxPoolLayerViewer(props) {
|
| 56 |
-
|
| 57 |
|
| 58 |
-
|
| 59 |
return extractActivationMaps(props.output, H, W, C);
|
| 60 |
}, [props.output]);
|
| 61 |
|
| 62 |
-
|
| 63 |
return activations.map(imgDataToSrc);
|
| 64 |
}, [activations]);
|
| 65 |
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
|
| 70 |
<div className="layer-details">
|
| 71 |
-
<div>
|
| 72 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 73 |
</div>
|
| 74 |
|
| 75 |
<h4>Outputs (min-max normalized)</h4>
|
| 76 |
<div className="layer-grid">
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 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 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 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
|
| 4 |
-
import Sidebar from
|
| 5 |
-
import { load, train, Cnn } from
|
| 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(
|
| 55 |
|
| 56 |
if (!rawKey || rawValue === undefined) continue;
|
| 57 |
|
| 58 |
let key = rawKey;
|
| 59 |
let value = parseValue(rawValue);
|
| 60 |
|
| 61 |
-
if (key ===
|
| 62 |
-
key =
|
| 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(
|
| 80 |
const [imageSize, setImageSize] = useState(28);
|
| 81 |
const [numClasses, setNumClasses] = useState(10);
|
| 82 |
-
|
| 83 |
|
| 84 |
// optimization states
|
| 85 |
-
|
| 86 |
-
const [optimizerParams, setOptimizerParams] = useState(
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
);
|
| 96 |
-
|
| 97 |
// architecture states
|
| 98 |
const [architecture, setArchitecture] = useState(DEFAULT_ARCHITECTURE);
|
| 99 |
|
| 100 |
// training states
|
| 101 |
-
|
| 102 |
const lossesRef = useRef([]);
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
const modelRef = useRef(null);
|
| 109 |
const optimizerRef = useRef(null);
|
| 110 |
|
| 111 |
-
|
| 112 |
const infoRef = useRef([]);
|
| 113 |
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
resetOptimizer();
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
|
| 157 |
-
|
| 158 |
-
|
| 159 |
-
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
|
| 166 |
-
|
| 167 |
-
|
| 168 |
-
|
| 169 |
-
|
| 170 |
lossesRef.current = [];
|
| 171 |
infoRef.current = [];
|
| 172 |
-
|
| 173 |
resetOptimizer();
|
| 174 |
-
|
| 175 |
|
| 176 |
-
|
| 177 |
-
|
| 178 |
-
|
| 179 |
-
|
| 180 |
|
| 181 |
if (modelRef.current) {
|
| 182 |
modelRef.current.dispose();
|
| 183 |
}
|
| 184 |
|
| 185 |
-
|
| 186 |
-
|
| 187 |
modelRef.current = cnn;
|
| 188 |
updateTick();
|
| 189 |
-
|
| 190 |
|
| 191 |
function resetOptimizer() {
|
| 192 |
-
if (optimizerType ===
|
| 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(
|
| 200 |
return;
|
| 201 |
}
|
| 202 |
if (Number.isNaN(beta1) || beta1 < 0) {
|
| 203 |
-
alert(
|
| 204 |
return;
|
| 205 |
}
|
| 206 |
if (Number.isNaN(beta2) || beta2 < 0) {
|
| 207 |
-
alert(
|
| 208 |
return;
|
| 209 |
}
|
| 210 |
if (Number.isNaN(epsilon) || epsilon <= 0) {
|
| 211 |
-
alert(
|
| 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 ===
|
| 221 |
const learningRate = parseFloat(optimizerParams.learningRate);
|
| 222 |
if (Number.isNaN(learningRate) || learningRate <= 0) {
|
| 223 |
-
alert(
|
| 224 |
return;
|
| 225 |
}
|
| 226 |
|
|
@@ -234,42 +229,39 @@ function NetworkVisualizer() {
|
|
| 234 |
}
|
| 235 |
}
|
| 236 |
|
| 237 |
-
|
| 238 |
-
|
| 239 |
-
|
| 240 |
-
|
| 241 |
|
| 242 |
-
|
| 243 |
-
|
| 244 |
-
|
| 245 |
|
| 246 |
const batchSize = parseFloat(optimizerParams.batchSize);
|
| 247 |
if (Number.isNaN(batchSize) || batchSize <= 0) {
|
| 248 |
-
alert(
|
| 249 |
setIsTraining(false);
|
| 250 |
return;
|
| 251 |
}
|
| 252 |
const epochs = parseFloat(optimizerParams.epochs);
|
| 253 |
if (Number.isNaN(epochs) || epochs <= 0) {
|
| 254 |
-
alert(
|
| 255 |
setIsTraining(false);
|
| 256 |
return;
|
| 257 |
}
|
| 258 |
|
| 259 |
let lastTickUpdate = 0;
|
| 260 |
-
|
| 261 |
-
|
| 262 |
-
|
| 263 |
-
|
| 264 |
-
|
| 265 |
batchSize,
|
| 266 |
epochs,
|
| 267 |
-
|
| 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 |
-
|
| 285 |
-
|
| 286 |
-
|
| 287 |
-
|
| 288 |
-
|
| 289 |
-
|
| 290 |
-
|
| 291 |
|
| 292 |
function handleArchitectureChange(newArchitecture) {
|
| 293 |
if (isTraining) {
|
| 294 |
-
alert(
|
| 295 |
} else {
|
| 296 |
setArchitecture(newArchitecture);
|
| 297 |
}
|
|
@@ -299,7 +291,7 @@ function NetworkVisualizer() {
|
|
| 299 |
|
| 300 |
function handleOptimizerChange(newOptimizerType, newOptimizerParams) {
|
| 301 |
if (isTraining) {
|
| 302 |
-
alert(
|
| 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 |
-
|
| 322 |
-
|
| 323 |
-
|
| 324 |
onSampleIndexChange={handleSampleIndexChange}
|
| 325 |
tick={tick}
|
| 326 |
-
|
| 327 |
-
<Sidebar
|
| 328 |
-
|
| 329 |
-
|
| 330 |
-
|
| 331 |
-
|
| 332 |
-
|
| 333 |
-
|
| 334 |
-
|
| 335 |
-
|
| 336 |
-
|
| 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
|
| 2 |
|
| 3 |
const DEFAULT_PARAMS = {
|
| 4 |
sgd: {
|
| 5 |
-
learningRate:
|
| 6 |
-
momentum:
|
| 7 |
-
batchSize:
|
| 8 |
-
epochs:
|
| 9 |
},
|
| 10 |
adam: {
|
| 11 |
-
learningRate:
|
| 12 |
-
beta1:
|
| 13 |
-
beta2:
|
| 14 |
-
epsilon:
|
| 15 |
-
batchSize:
|
| 16 |
-
epochs:
|
| 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 ===
|
| 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) =>
|
| 58 |
-
|
| 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) =>
|
| 69 |
-
|
| 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) =>
|
| 80 |
-
|
| 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) =>
|
| 91 |
-
|
| 92 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 93 |
/>
|
| 94 |
-
</div>
|
| 95 |
</>
|
| 96 |
)}
|
| 97 |
-
{props.optimizer ===
|
| 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) =>
|
| 106 |
-
|
| 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) =>
|
| 117 |
-
|
| 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) =>
|
| 128 |
-
|
| 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) =>
|
| 139 |
-
|
| 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) =>
|
| 150 |
-
|
| 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) =>
|
| 161 |
-
|
| 162 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 163 |
/>
|
| 164 |
</div>
|
| 165 |
</>
|
| 166 |
)}
|
| 167 |
-
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
|
| 171 |
-
|
| 172 |
-
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
|
| 176 |
-
|
| 177 |
-
|
| 178 |
-
|
| 179 |
-
|
| 180 |
-
|
| 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
|
| 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>
|
|
|
|
|
|
|
| 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> {
|
| 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
|
| 2 |
-
import Button from
|
| 3 |
-
import ArchitectureEditor from
|
| 4 |
-
import OptimizerSettings from
|
| 5 |
|
| 6 |
function Sidebar(props) {
|
| 7 |
const tabData = [
|
| 8 |
{
|
| 9 |
-
label:
|
| 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:
|
| 22 |
content: (
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
},
|
| 29 |
{
|
| 30 |
-
label:
|
| 31 |
content: (
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 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
|
| 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:
|
| 11 |
-
mode:
|
| 12 |
-
name:
|
| 13 |
-
line: { color:
|
| 14 |
-
|
| 15 |
-
|
| 16 |
},
|
| 17 |
{
|
| 18 |
x: props.epochs,
|
| 19 |
y: props.valAcc,
|
| 20 |
-
type:
|
| 21 |
-
mode:
|
| 22 |
-
name:
|
| 23 |
-
line: { color:
|
| 24 |
-
|
| 25 |
-
|
| 26 |
},
|
| 27 |
]}
|
| 28 |
layout={{
|
| 29 |
-
title:
|
| 30 |
autosize: true,
|
| 31 |
-
xaxis: { title:
|
| 32 |
-
yaxis: { title:
|
| 33 |
-
legend: { orientation:
|
| 34 |
margin: { t: 0 },
|
| 35 |
}}
|
| 36 |
-
style={{ width:
|
| 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
|
| 2 |
|
| 3 |
function ActivationViewer(props) {
|
| 4 |
return (
|
| 5 |
<div className="activations">
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 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
|
| 2 |
-
import
|
| 3 |
|
| 4 |
function ArchitectureEditor() {
|
| 5 |
const [layers, setLayers] = useState([
|
| 6 |
{
|
| 7 |
-
type:
|
| 8 |
kernelSize: 5,
|
| 9 |
filters: 8,
|
| 10 |
stride: 1,
|
| 11 |
padding: 1,
|
| 12 |
-
activation:
|
| 13 |
},
|
| 14 |
{
|
| 15 |
-
type:
|
| 16 |
kernelSize: 2,
|
| 17 |
stride: 2,
|
| 18 |
padding: 0,
|
| 19 |
},
|
| 20 |
{
|
| 21 |
-
type:
|
| 22 |
kernelSize: 5,
|
| 23 |
filters: 16,
|
| 24 |
stride: 1,
|
| 25 |
padding: 1,
|
| 26 |
-
activation:
|
| 27 |
},
|
| 28 |
{
|
| 29 |
-
type:
|
| 30 |
kernelSize: 2,
|
| 31 |
stride: 2,
|
| 32 |
padding: 0,
|
| 33 |
},
|
| 34 |
{
|
| 35 |
-
type:
|
| 36 |
units: 10,
|
| 37 |
-
activation:
|
| 38 |
},
|
| 39 |
]);
|
| 40 |
|
|
@@ -53,12 +53,12 @@ function ArchitectureEditor() {
|
|
| 53 |
setLayers((prev) => [
|
| 54 |
...prev,
|
| 55 |
{
|
| 56 |
-
type:
|
| 57 |
kernelSize: 3,
|
| 58 |
filters: 8,
|
| 59 |
stride: 1,
|
| 60 |
padding: 1,
|
| 61 |
-
activation:
|
| 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 ===
|
| 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,
|
| 118 |
}
|
| 119 |
/>
|
| 120 |
</div>
|
|
@@ -126,7 +118,7 @@ function ArchitectureEditor() {
|
|
| 126 |
type="number"
|
| 127 |
value={layer.kernelSize}
|
| 128 |
onChange={(e) =>
|
| 129 |
-
updateLayer(index,
|
| 130 |
}
|
| 131 |
/>
|
| 132 |
</div>
|
|
@@ -138,7 +130,7 @@ function ArchitectureEditor() {
|
|
| 138 |
type="number"
|
| 139 |
value={layer.stride}
|
| 140 |
onChange={(e) =>
|
| 141 |
-
updateLayer(index,
|
| 142 |
}
|
| 143 |
/>
|
| 144 |
</div>
|
|
@@ -150,7 +142,7 @@ function ArchitectureEditor() {
|
|
| 150 |
type="number"
|
| 151 |
value={layer.padding}
|
| 152 |
onChange={(e) =>
|
| 153 |
-
updateLayer(index,
|
| 154 |
}
|
| 155 |
/>
|
| 156 |
</div>
|
|
@@ -161,7 +153,7 @@ function ArchitectureEditor() {
|
|
| 161 |
<select
|
| 162 |
value={layer.activation}
|
| 163 |
onChange={(e) =>
|
| 164 |
-
updateLayer(index,
|
| 165 |
}
|
| 166 |
>
|
| 167 |
<option value="relu">ReLU</option>
|
|
@@ -174,7 +166,7 @@ function ArchitectureEditor() {
|
|
| 174 |
)}
|
| 175 |
|
| 176 |
{/* Pool fields */}
|
| 177 |
-
{(layer.type ===
|
| 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,
|
| 187 |
}
|
| 188 |
/>
|
| 189 |
</div>
|
|
@@ -195,7 +187,7 @@ function ArchitectureEditor() {
|
|
| 195 |
type="number"
|
| 196 |
value={layer.stride}
|
| 197 |
onChange={(e) =>
|
| 198 |
-
updateLayer(index,
|
| 199 |
}
|
| 200 |
/>
|
| 201 |
</div>
|
|
@@ -207,7 +199,7 @@ function ArchitectureEditor() {
|
|
| 207 |
type="number"
|
| 208 |
value={layer.padding}
|
| 209 |
onChange={(e) =>
|
| 210 |
-
updateLayer(index,
|
| 211 |
}
|
| 212 |
/>
|
| 213 |
</div>
|
|
@@ -215,7 +207,7 @@ function ArchitectureEditor() {
|
|
| 215 |
)}
|
| 216 |
|
| 217 |
{/* Dense fields */}
|
| 218 |
-
{layer.type ===
|
| 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,
|
| 228 |
}
|
| 229 |
/>
|
| 230 |
</div>
|
|
@@ -235,7 +227,7 @@ function ArchitectureEditor() {
|
|
| 235 |
<select
|
| 236 |
value={layer.activation}
|
| 237 |
onChange={(e) =>
|
| 238 |
-
updateLayer(index,
|
| 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
|
| 2 |
-
|
| 3 |
-
function LossPlot(props) {
|
| 4 |
-
return (
|
| 5 |
-
<Plot
|
| 6 |
-
data={[
|
| 7 |
-
{
|
| 8 |
-
x: props.epochs,
|
| 9 |
-
y: props.loss,
|
| 10 |
-
type:
|
| 11 |
-
mode:
|
| 12 |
-
name:
|
| 13 |
-
line: { color:
|
| 14 |
-
},
|
| 15 |
-
{
|
| 16 |
-
x: props.epochs,
|
| 17 |
-
y: props.valLoss,
|
| 18 |
-
type:
|
| 19 |
-
mode:
|
| 20 |
-
name:
|
| 21 |
-
line: { color:
|
| 22 |
-
},
|
| 23 |
-
]}
|
| 24 |
-
layout={{
|
| 25 |
-
title:
|
| 26 |
-
autosize: true,
|
| 27 |
-
xaxis: { title:
|
| 28 |
-
yaxis: { title:
|
| 29 |
-
legend: { orientation:
|
| 30 |
-
margin: { t: 0 },
|
| 31 |
-
}}
|
| 32 |
-
style={{ width:
|
| 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 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
}
|
| 7 |
|
| 8 |
.plots {
|
|
@@ -10,12 +10,11 @@
|
|
| 10 |
}
|
| 11 |
|
| 12 |
.activations {
|
| 13 |
-
|
| 14 |
}
|
| 15 |
|
| 16 |
.sidebar {
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 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
|
| 2 |
-
import * as tf from
|
| 3 |
-
import { MnistData } from
|
| 4 |
-
import LossPlot from
|
| 5 |
-
import AccPlot from
|
| 6 |
-
import Tabs from
|
| 7 |
-
import ActivationViewer from
|
| 8 |
-
import ArchitectureEditor from
|
| 9 |
-
import KernelViewer from
|
| 10 |
-
import
|
| 11 |
|
| 12 |
export default function MnistTrainer() {
|
| 13 |
const [data, setData] = useState(null);
|
| 14 |
const [model, setModel] = useState(null);
|
| 15 |
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 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(
|
| 28 |
|
| 29 |
const progressTabData = [
|
| 30 |
{
|
| 31 |
-
label:
|
| 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:
|
| 41 |
content: (
|
| 42 |
-
<ActivationViewer
|
| 43 |
-
activations={activations}
|
| 44 |
sampleImage={sampleImage}
|
| 45 |
onSampleChange={handleSampleChange}
|
| 46 |
/>
|
| 47 |
),
|
| 48 |
},
|
| 49 |
{
|
| 50 |
-
label:
|
| 51 |
-
content:
|
| 52 |
-
<KernelViewer
|
| 53 |
-
/>
|
| 54 |
-
),
|
| 55 |
},
|
| 56 |
-
|
| 57 |
];
|
| 58 |
|
| 59 |
const optionsTabData = [
|
| 60 |
{
|
| 61 |
-
label:
|
| 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:
|
| 82 |
content: (
|
| 83 |
<div>
|
| 84 |
<ArchitectureEditor />
|
|
@@ -86,13 +76,13 @@ export default function MnistTrainer() {
|
|
| 86 |
),
|
| 87 |
},
|
| 88 |
{
|
| 89 |
-
label:
|
| 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 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
|
| 122 |
-
|
| 123 |
-
|
| 124 |
|
| 125 |
-
|
| 126 |
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
|
| 131 |
-
|
| 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:
|
| 147 |
-
kernelInitializer:
|
| 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:
|
| 158 |
-
kernelInitializer:
|
| 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:
|
| 167 |
|
| 168 |
model.compile({
|
| 169 |
optimizer: tf.train.adam(),
|
| 170 |
-
loss:
|
| 171 |
-
metrics: [
|
| 172 |
});
|
| 173 |
|
| 174 |
return model;
|
| 175 |
}
|
| 176 |
|
| 177 |
-
|
| 178 |
-
|
| 179 |
-
|
| 180 |
-
|
| 181 |
-
|
| 182 |
-
|
| 183 |
-
|
| 184 |
-
|
| 185 |
-
|
| 186 |
-
|
| 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 |
-
|
| 201 |
-
|
| 202 |
|
| 203 |
-
setStatus(
|
| 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(
|
| 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 |
-
|
| 250 |
-
|
| 251 |
-
|
| 252 |
-
|
| 253 |
-
|
| 254 |
-
|
| 255 |
-
|
| 256 |
-
|
| 257 |
-
|
| 258 |
-
|
| 259 |
-
|
| 260 |
-
|
| 261 |
-
|
| 262 |
-
|
| 263 |
-
|
| 264 |
-
|
| 265 |
-
|
| 266 |
-
|
| 267 |
-
|
| 268 |
-
|
| 269 |
-
|
| 270 |
-
|
| 271 |
-
|
| 272 |
-
|
| 273 |
-
|
| 274 |
-
|
| 275 |
-
|
| 276 |
-
|
| 277 |
-
|
| 278 |
-
|
| 279 |
-
|
| 280 |
-
|
| 281 |
-
|
| 282 |
-
|
| 283 |
-
|
| 284 |
-
|
| 285 |
await tf.nextFrame();
|
| 286 |
},
|
| 287 |
|
|
@@ -295,7 +276,7 @@ export default function MnistTrainer() {
|
|
| 295 |
},
|
| 296 |
|
| 297 |
onTrainEnd: () => {
|
| 298 |
-
|
| 299 |
},
|
| 300 |
},
|
| 301 |
});
|
|
@@ -306,44 +287,43 @@ export default function MnistTrainer() {
|
|
| 306 |
testYs.dispose();
|
| 307 |
}
|
| 308 |
|
| 309 |
-
|
| 310 |
-
|
| 311 |
|
| 312 |
-
|
| 313 |
|
| 314 |
-
|
| 315 |
-
|
| 316 |
-
|
| 317 |
-
|
| 318 |
-
|
| 319 |
-
|
| 320 |
|
| 321 |
-
|
| 322 |
-
|
| 323 |
|
| 324 |
-
|
| 325 |
-
|
| 326 |
|
| 327 |
-
|
| 328 |
-
|
| 329 |
-
|
| 330 |
|
| 331 |
-
|
| 332 |
-
|
| 333 |
-
|
| 334 |
-
|
| 335 |
-
|
| 336 |
|
| 337 |
return (
|
| 338 |
<div className="mnist-trainer">
|
| 339 |
-
|
| 340 |
<Tabs tabs={progressTabData} defaultTab="Curves" />
|
| 341 |
-
|
| 342 |
|
| 343 |
-
|
| 344 |
<Tabs tabs={optionsTabData} defaultTab="Train" />
|
| 345 |
-
|
| 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 |
-
|
| 31 |
const MNIST_LABELS_PATH =
|
| 32 |
-
|
| 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 |
-
|
|
|
|
| 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 |
-
|
| 67 |
-
|
|
|
|
|
|
|
| 68 |
ctx.drawImage(
|
| 69 |
-
|
| 70 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
| 101 |
this.testImages = this.datasetImages.slice(IMAGE_SIZE * NUM_TRAIN_ELEMENTS);
|
| 102 |
-
this.trainLabels =
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
|
|
|
|
|
|
|
|
|
| 106 |
}
|
| 107 |
|
| 108 |
nextTrainBatch(batchSize) {
|
| 109 |
return this.nextBatch(
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
|
|
|
|
|
|
|
|
|
| 115 |
}
|
| 116 |
|
| 117 |
nextTestBatch(batchSize) {
|
| 118 |
return this.nextBatch(batchSize, [this.testImages, this.testLabels], () => {
|
| 119 |
this.shuffledTestIndex =
|
| 120 |
-
|
| 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 |
-
|
|
|
|
|
|
|
| 134 |
batchImagesArray.set(image, i * IMAGE_SIZE);
|
| 135 |
|
| 136 |
-
const label =
|
| 137 |
-
|
|
|
|
|
|
|
| 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 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
await data.load();
|
| 12 |
-
|
| 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
|
| 67 |
out = out.conv2d(layer.weights, layer.stride, layer.padding);
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
break;
|
| 72 |
-
case
|
| 73 |
-
out = out.maxPool(
|
|
|
|
|
|
|
|
|
|
|
|
|
| 74 |
break;
|
| 75 |
-
case
|
| 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 ===
|
| 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
|
| 94 |
out = out.matMul(layer.weights).add(layer.biases);
|
| 95 |
break;
|
| 96 |
}
|
| 97 |
}
|
| 98 |
return out;
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 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 |
-
|
| 193 |
-
|
| 194 |
-
|
| 195 |
-
|
| 196 |
-
|
| 197 |
-
|
| 198 |
-
|
| 199 |
) {
|
| 200 |
-
|
| 201 |
-
|
| 202 |
-
|
| 203 |
-
|
| 204 |
-
|
| 205 |
-
|
| 206 |
-
|
| 207 |
-
|
| 208 |
-
|
| 209 |
-
|
| 210 |
-
|
| 211 |
-
|
| 212 |
-
|
| 213 |
-
|
| 214 |
-
|
| 215 |
-
|
| 216 |
-
|
| 217 |
-
|
| 218 |
-
|
| 219 |
-
|
| 220 |
-
|
| 221 |
-
|
| 222 |
-
|
| 223 |
-
|
| 224 |
-
|
| 225 |
-
|
| 226 |
-
|
| 227 |
-
|
| 228 |
-
|
| 229 |
-
|
| 230 |
-
|
| 231 |
-
|
| 232 |
-
|
| 233 |
-
|
| 234 |
-
|
| 235 |
-
|
| 236 |
-
|
| 237 |
-
|
| 238 |
-
|
| 239 |
-
data.imageSize,
|
| 240 |
-
data.numInputChannels
|
| 241 |
-
]
|
| 242 |
-
)
|
| 243 |
-
);
|
| 244 |
|
| 245 |
// Add output layer info
|
| 246 |
const probs = tf.tidy(() => {
|
| 247 |
-
|
| 248 |
-
|
| 249 |
-
|
| 250 |
info.push({
|
| 251 |
-
type:
|
| 252 |
output: probs.dataSync(),
|
| 253 |
-
|
| 254 |
});
|
| 255 |
|
| 256 |
-
|
| 257 |
-
|
| 258 |
-
|
| 259 |
-
|
| 260 |
|
| 261 |
-
|
| 262 |
-
|
| 263 |
-
|
| 264 |
|
| 265 |
-
|
| 266 |
-
|
| 267 |
-
|
| 268 |
-
|
| 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 |
-
|
| 3 |
[
|
| 4 |
[-1, -1, -1],
|
| 5 |
[-1, 8, -1],
|
|
@@ -16,7 +16,7 @@ const PRESETS_COLOR = {
|
|
| 16 |
[-1, -1, -1],
|
| 17 |
],
|
| 18 |
],
|
| 19 |
-
|
| 20 |
[
|
| 21 |
[0, 0, 0],
|
| 22 |
[0, 1, 0],
|
|
@@ -33,7 +33,7 @@ const PRESETS_COLOR = {
|
|
| 33 |
[0, 0, 0],
|
| 34 |
],
|
| 35 |
],
|
| 36 |
-
|
| 37 |
[
|
| 38 |
[0, 0, 0],
|
| 39 |
[0, 0, 0],
|
|
@@ -50,7 +50,7 @@ const PRESETS_COLOR = {
|
|
| 50 |
[0, 0, 0],
|
| 51 |
],
|
| 52 |
],
|
| 53 |
-
|
| 54 |
[
|
| 55 |
[0, 0, 0],
|
| 56 |
[0, 0, 0],
|
|
@@ -70,7 +70,7 @@ const PRESETS_COLOR = {
|
|
| 70 |
};
|
| 71 |
|
| 72 |
const PRESETS_GRAY = {
|
| 73 |
-
|
| 74 |
[
|
| 75 |
[-1, -1, -1],
|
| 76 |
[-1, 8, -1],
|
|
@@ -87,7 +87,7 @@ const PRESETS_GRAY = {
|
|
| 87 |
[-1, -1, -1],
|
| 88 |
],
|
| 89 |
],
|
| 90 |
-
|
| 91 |
[
|
| 92 |
[-1, 0, 1],
|
| 93 |
[-2, 0, 2],
|
|
@@ -104,7 +104,7 @@ const PRESETS_GRAY = {
|
|
| 104 |
[-1, 0, 1],
|
| 105 |
],
|
| 106 |
],
|
| 107 |
-
|
| 108 |
[
|
| 109 |
[-1, -2, -1],
|
| 110 |
[0, 0, 0],
|
|
@@ -121,7 +121,7 @@ const PRESETS_GRAY = {
|
|
| 121 |
[1, 2, 1],
|
| 122 |
],
|
| 123 |
],
|
| 124 |
-
|
| 125 |
[
|
| 126 |
[0, -1, 0],
|
| 127 |
[-1, 5, -1],
|
|
@@ -138,7 +138,7 @@ const PRESETS_GRAY = {
|
|
| 138 |
[0, -1, 0],
|
| 139 |
],
|
| 140 |
],
|
| 141 |
-
|
| 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 |
-
|
| 31 |
const MNIST_LABELS_PATH =
|
| 32 |
-
|
| 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 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 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 |
-
|
|
|
|
| 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 |
-
|
| 73 |
-
|
|
|
|
|
|
|
| 74 |
ctx.drawImage(
|
| 75 |
-
|
| 76 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
| 107 |
this.testImages = this.datasetImages.slice(IMAGE_SIZE * NUM_TRAIN_ELEMENTS);
|
| 108 |
-
this.trainLabels =
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
|
|
|
|
|
|
|
|
|
| 112 |
}
|
| 113 |
|
| 114 |
nextTrainBatch(batchSize) {
|
| 115 |
return this.nextBatch(
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
|
|
|
|
|
|
|
|
|
| 121 |
}
|
| 122 |
|
| 123 |
nextTestBatch(batchSize) {
|
| 124 |
return this.nextBatch(batchSize, [this.testImages, this.testLabels], () => {
|
| 125 |
this.shuffledTestIndex =
|
| 126 |
-
|
| 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 |
-
|
|
|
|
|
|
|
| 140 |
batchImagesArray.set(image, i * IMAGE_SIZE);
|
| 141 |
|
| 142 |
-
const label =
|
| 143 |
-
|
|
|
|
|
|
|
| 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 |
-
|
| 154 |
-
|
| 155 |
|
| 156 |
-
|
| 157 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 158 |
|
| 159 |
-
|
| 160 |
-
|
| 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 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
}
|
| 12 |
|
| 13 |
.field {
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
}
|
| 18 |
|
| 19 |
.field label,
|
| 20 |
.field legend {
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
}
|
| 25 |
|
| 26 |
-
.field input[type=
|
| 27 |
-
.field input[type=
|
| 28 |
.field textarea {
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
}
|
| 36 |
|
| 37 |
.field textarea {
|
| 38 |
-
|
| 39 |
}
|
| 40 |
|
| 41 |
.field input[readonly],
|
| 42 |
.field textarea[readonly] {
|
| 43 |
-
|
| 44 |
-
|
| 45 |
}
|
| 46 |
|
| 47 |
.double-field {
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
}
|
| 53 |
|
| 54 |
.double-field .field {
|
| 55 |
-
|
| 56 |
}
|
| 57 |
|
| 58 |
.input-button {
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
}
|
| 63 |
|
| 64 |
.fieldset {
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
}
|
| 72 |
|
| 73 |
.fieldset label {
|
| 74 |
-
|
| 75 |
-
|
| 76 |
}
|
| 77 |
|
| 78 |
.slider-field {
|
| 79 |
-
|
| 80 |
}
|
| 81 |
|
| 82 |
.slider-control {
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
}
|
| 87 |
|
| 88 |
-
.slider-control input[type=
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
}
|
| 96 |
|
| 97 |
-
.slider-control input[type=
|
| 98 |
height: 6px;
|
| 99 |
background: lightgray;
|
| 100 |
border-radius: 6px;
|
| 101 |
}
|
| 102 |
|
| 103 |
-
.slider-control input[type=
|
| 104 |
-webkit-appearance: none;
|
| 105 |
width: 14px;
|
| 106 |
height: 14px;
|
|
@@ -112,7 +112,7 @@
|
|
| 112 |
}
|
| 113 |
|
| 114 |
.slider-control span {
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 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;
|
| 30 |
outputData[outputIndex + 1] = clampedValue; // G
|
| 31 |
outputData[outputIndex + 2] = clampedValue; // B
|
| 32 |
-
outputData[outputIndex + 3] = 255;
|
| 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;
|
| 73 |
outputData[outputIndex + 1] = clampedValue; // G
|
| 74 |
outputData[outputIndex + 2] = clampedValue; // B
|
| 75 |
-
outputData[outputIndex + 3] = 255;
|
| 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 |
}
|
|
|