import React, { Component } from 'react';
import ReactDOM from 'react-dom';
import Regression from './Regression';
ReactDOM.render(<Regression name="world" />, document.getElementById('root'));
{
"name": "@plnkr/starter-react",
"version": "1.0.2",
"description": "React starter template",
"dependencies": {
"@tensorflow/tfjs": "^2.0.1",
"plotly.js": "^1.54.7",
"react": "^16.13.0",
"react-dom": "^16.13.0",
"react-plotly.js": "^2.4.0",
"react-charts": "^2.0.0-beta.7",
"recharts": "^1.8.5"
},
"plnkr": {
"runtime": "system",
"useHotReload": true
}
}
import * as tf from '@tensorflow/tfjs';
export default class AbstractRegressionModel {
/**
* Model initializing
*
* @param width - range of x coordinates and used for data normalization
* @param height - range of y coordinates and used for data normalization
* @param learningRate -
* @param expectedLoss - optional, stop tarining model when loss reach set expected loss
*/
constructor(
width,
height,
optimizerFunction = tf.train.sgd,
maxEpochPerTrainSession = 100,
learningRate = 0.1,
expectedLoss = 0.001
) {
this.width = width;
this.height = height;
this.optimizerFunction = optimizerFunction;
this.expectedLoss = expectedLoss;
this.learningRate = learningRate;
this.maxEpochPerTrainSession = maxEpochPerTrainSession;
this.initModelVariables();
this.trainSession = 0;
this.epochNumber = 0;
this.history = [];
}
initModelVariables() {
throw Error('Model variables should be defined')
}
f() {
throw Error('Model should be defined')
}
xNormalization = xs => xs.map(x => x / this.width);
yNormalization = ys => ys.map(y => y / this.height);
yDenormalization = ys => ys.map(y => y * this.height);
/**
* Calculate loss function as root-mean-square deviation
*
* @param predictedValue - tensor1d - predicted values of calculated model
* @param realValue - tensor1d - real value of experimental points
*/
loss = (predictedValue, realValue) => {
const loss = predictedValue.sub(realValue).square().mean();
this.lossVal = loss.dataSync()[0];
return loss;
};
/**
* Train model until explicitly stop process via invocation of stop method
* or loss achieve necessary accuracy, or train achieve max epoch value
*
* @param x - array of x coordinates
* @param y - array of y coordinates
* @param callback - optional, invoked after each training step
*/
async train(x, y, callback) {
const currentTrainSession = ++this.trainSession;
this.lossVal = Number.POSITIVE_INFINITY;
this.epochNumber = 0;
this.history = [];
// convert data into tensors
const input = tf.tensor1d(this.xNormalization(x));
const output = tf.tensor1d(this.yNormalization(y));
while (
currentTrainSession === this.trainSession
&& this.lossVal > this.expectedLoss
&& this.epochNumber <= this.maxEpochPerTrainSession
) {
const optimizer = this.optimizerFunction(this.learningRate);
optimizer.minimize(() => this.loss(this.f(input), output));
this.history = [...this.history, {
epoch: this.epochNumber,
loss: this.lossVal
}];
callback && callback();
this.epochNumber++;
await tf.nextFrame();
}
}
stop() {
this.trainSession++;
}
/**
* Predict value basing on trained model
* @param x - array of x coordinates
* @return Array({x: integer, y: integer}) - predicted values associated with input
*
* */
predict(x) {
const input = tf.tensor1d(this.xNormalization(x));
const output = this.yDenormalization(this.f(input).arraySync());
return output.map((y, i) => ({ x: x[i], y }));
}
}
import AbstractRegressionModel from "./AbstractRegressionModel";
import * as tf from '@tensorflow/tfjs';
export default class LinearRegressionModel extends AbstractRegressionModel {
initModelVariables() {
this.k = tf.scalar(Math.random()).variable();
this.b = tf.scalar(Math.random()).variable();
}
f = x => x.mul(this.k).add(this.b);
}
import React, { useRef, useEffect, useState } from 'react';
export default ({ width = 600, height = 600, points = [], changePoints, curvePoints = [] }) => {
const canvasRef = useRef();
const [ctx, changeCtx] = useState(null);
const [containerPosition, changePosition] = useState({ x: 0, y: 0 });
function updateCanvas() {
if (ctx) {
// container
ctx.fillStyle = '#000000';
ctx.fillRect(0, 0, width, height);
//drawing points
ctx.fillStyle = '#ffffff';
points.forEach(({ x, y }) => {
ctx.beginPath();
ctx.arc(x, y, 5, 0, 2 * Math.PI);
ctx.fill();
ctx.closePath();
});
// drawing predicted curve
ctx.strokeStyle = '#ffffff';
curvePoints.forEach(({ x, y }, i, points) => {
if (i+1 <= points.length-1){
ctx.beginPath();
ctx.moveTo(x, y);
ctx.lineTo(points[i+1].x, points[i+1].y);
ctx.stroke();
ctx.closePath();
}
});
}
}
useEffect(() => {
changeCtx(canvasRef.current.getContext('2d'));
}, []);
useEffect(() => {
updateCanvas();
const container = canvasRef.current;
changePosition({ x: container.offsetLeft, y: container.offsetTop });
}, [ctx, points, curvePoints]);
return (
<canvas
ref={canvasRef}
width={width}
height={height}
onClick={(e) => {
changePoints([...points, {
x: e.clientX - containerPosition.x,
y: e.clientY - containerPosition.y
}]);
}}
/>
)
}
import React from 'react';
import { LineChart, XAxis, YAxis, CartesianGrid, Line } from 'recharts';
export default ({ width = 400, height = 430, loss = [] }) => {
return (
<LineChart
width={width}
height={height}
margin={{ top: 0, left: 0, bottom: 0, right: 0 }}
data={loss}>
<XAxis dataKey="epoch"/>
<YAxis/>
<CartesianGrid
/>
<Line
type="monotone"
dataKey="loss"
stroke="#8884d8"
dot={false}/>
</LineChart>
)
}
import React, { useState, useEffect } from 'react';
import Canvas from './components/Canvas';
import LossPlot from './components/LossPlot';
import LinearRegressionModel from './models/LinearRegressionModel';
import './styles/style.css';
const WIDTH = 400;
const HEIGHT = 400;
const LINE_POINT_STEP = 5;
const predictedInput = Array.from({ length: WIDTH / LINE_POINT_STEP + 1 })
.map((v, i) => i * LINE_POINT_STEP);
const model = new LinearRegressionModel(WIDTH, HEIGHT);
export default () => {
const [points, changePoints] = useState([]);
const [curvePoints, changeCurvePoints] = useState([]);
const [lossHistory, changeLossHistory] = useState([]);
useEffect(() => {
if (points.length > 0) {
const input = points.map(({ x }) => x);
const output = points.map(({ y }) => y);
model.train(input, output, () => {
changeCurvePoints(() => model.predict(predictedInput));
changeLossHistory(() => model.history);
});
}
}, [points]);
return (
<div className="regression-low-level">
<div className="regression-low-level__top">
<div className="regression-low-level__workarea">
<div className="regression-low-level__canvas">
<Canvas
width={WIDTH}
height={HEIGHT}
points={points}
curvePoints={curvePoints}
changePoints={changePoints}
/>
</div>
<div className="regression-low-level__toolbar">
<button
className="btn btn-red"
onClick={() => model.stop()}>Stop
</button>
<button
className="btn btn-yellow"
onClick={() => {
model.stop();
changePoints(() => []);
changeCurvePoints(() => []);
}}>Clear
</button>
</div>
</div>
<div className="regression-low-level__loss">
<LossPlot loss={lossHistory}/>
</div>
</div>
</div>
)
}
.regression-low-level {
padding: 20px;
}
.regression-low-level__top {
display: flex
}
.regression-low-level__workarea {
display: flex;
flex-direction: column;
}
.regression-low-level__loss {
padding: 0 40px;
}
.regression-low-level__toolbar .btn {
margin-right: 5px;
}
.btn {
display: inline-block;
font-weight: 400;
color: #212529;
text-align: center;
vertical-align: middle;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
background-color: transparent;
border: 1px solid transparent;
padding: .375rem .75rem;
font-size: 1rem;
line-height: 1.5;
border-radius: .25rem;
transition: color .15s ease-in-out, background-color .15s ease-in-out, border-color .15s ease-in-out, box-shadow .15s ease-in-out;
}
.btn:hover {
opacity: 0.8;
}
.btn-red {
background: red;
}
.btn-yellow {
background: yellow;
}
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" type="text/css" href="style.css" />
<script src="index.js"></script>
</head>
<body>
<div id="root"></div>
</body>
</html>