This JavaScript program demonstrates how to train an ordinary least squares line via backpropagation. The program uses an explicit formula to create generate a least squares line in red. Alternatively, a least squares line is generated iteratively via backprogation. The original randomly generated line is shown in green. The randomly generated green line is then iteratively corrected to generate the least squares line via backpropagation. The successive approximations are shown in blue with increasing alpha values.
<!DOCTYPE html> <html> <head> <title>XoaX.net's Javascript</title> <script type="text/javascript" src="OrdinaryLeastSquaresViaBackpropagation.js"></script> </head> <body onload="Initialize()"> <div style="width:848px;height:630px;"> <div style="width:48px;height:630px;float:left;"> <input id="idHighY" type="text" size="3" style="width:40px;height:16px;"/> <div style="width:48px;height:556px;"></div> <input id="idLowY" type="text" size="3" style="width:40px;height:16px;"/> </div> <canvas id="idCanvas" width="800" height ="600" style="background-color: #F0F0F0;float:left;"></canvas> <div style="width:800px;height:22px;float:left;"> <input id="idLowX" type="text" size="3" style="width:40px;height:16px;float:left;"/> <div style="width:704px;height:22px;float:left;"> <label style="color:black;float:left;height:22px;margin-left:5px;">Source Line: <span id="idSourceLine">y = mx + b</span></label> <button style="color:black;height:22px;float:left;margin-left:100px;" onclick="Initialize()">Reset</button> <label style="color:red;float:right;height:22px;margin-right:5px;">Least Squares Line: <span id="idLeastSquaresLine">y = mx + b</span></label> </div> <input id="idHighX" type="text" size="3" style="width:40px;height:16px;float:left;"/> </div> <div id="idBackPropLines" style="position:relative;left:0px;width:800px;height:66px;float:left;"> </div> </div> </body> </html>
class CLine {
#mdM;
#mdB;
constructor() {
this.#mdM = Math.random();
this.#mdB = Math.random();
}
BackpropagateOneEpoch(daY) {
//const kiEpochs = 100000;
const kdLearningRate = .0001
for (let iX = 0; iX < daY.length; ++iX) {
let dTargetY = daY[iX];
let dY = iX*this.#mdM + this.#mdB;
let dError = dY - dTargetY;
this.#mdB -= dError*kdLearningRate;
this.#mdM -= iX*dError*kdLearningRate;
}
}
SetToLeastSquares(daY) {
let dSumOfX = 0.0;
let dSumOfX2 = 0.0;
let dSumOfY = 0.0;
let dSumOfXY = 0.0;
for (let iX = 0; iX < daY.length; ++iX) {
dSumOfX += iX;
dSumOfX2 += (iX*iX);
dSumOfY += daY[iX];
dSumOfXY += iX*daY[iX];
}
// Denominator = (1.1)(x.x) - (1.x)(1.x)
let dDenominator = (daY.length*dSumOfX2 - dSumOfX*dSumOfX);
// Intercept = (x.x)(1.y) - (1.x)(x.y)/Denominator
this.#mdB = (dSumOfX2*dSumOfY - dSumOfX*dSumOfXY)/dDenominator;
// Slope = (1.1)(x.y) - (1.x)(1.y)/Denominator
this.#mdM =(daY.length*dSumOfXY - dSumOfX*dSumOfY)/dDenominator;
}
CalculateY(dX) {
return dX*this.#mdM + this.#mdB;
}
ToString() {
return "y = "+this.#mdM.toFixed(2)+"x + "+this.#mdB.toFixed(2);
}
}
function GenerateRandomPointFromNormalDistribution(dMean = 0.0, dStdDev = 1.0) {
return dStdDev*InversePhi(Math.random()) + dMean;
}
function InversePhi(p) {
// Constants for the approximation
const p_low = 0.02425;
const p_high = 1 - p_low;
// Coefficients for the rational approximation
const a1 = -3.969683028665376e+01;
const a2 = 2.209460984245205e+02;
const a3 = -2.759285104469687e+02;
const a4 = 1.383577518672690e+02;
const a5 = -3.066479806614716e+01;
const a6 = 2.506628277459239e+00;
const b1 = -5.447609879822406e+01;
const b2 = 1.615858368580409e+02;
const b3 = -1.556989798598866e+02;
const b4 = 6.680131188771972e+01;
const b5 = -1.328068155288572e+01;
const c1 = -7.784894002430293e-03;
const c2 = -3.223964580411365e-01;
const c3 = -2.400758277161838e+00;
const c4 = -2.549732539343734e+00;
const c5 = 4.374664141464968e+00;
const c6 = 2.938163982698783e+00;
const d1 = 7.784695709041462e-03;
const d2 = 3.224671290700398e-01;
const d3 = 2.445134137142996e+00;
const d4 = 3.754408661907416e+00;
let x = 0.0;
if (0.0 < p && p < 1) {
if (p < p_low) {
let q = Math.sqrt(-2*Math.log(p));
x = (((((c1*q+c2)*q+c3)*q+c4)*q+c5)*q+c6) / ((((d1*q+d2)*q+d3)*q+d4)*q+1);
} else if (p_high < p) {
let q = Math.sqrt(-2*Math.log(1-p));
x = -(((((c1*q+c2)*q+c3)*q+c4)*q+c5)*q+c6) / ((((d1*q+d2)*q+d3)*q+d4)*q+1);
} else {
let q = p - 0.5;
let r = q*q;
x = (((((a1*r+a2)*r+a3)*r+a4)*r+a5)*r+a6)*q / (((((b1*r+b2)*r+b3)*r+b4)*r+b5)*r+1);
}
}
return x;
}
function Initialize() {
let qCanvas = document.getElementById("idCanvas");
let qContext2D = qCanvas.getContext("2d");
qContext2D.clearRect(0, 0, 800, 600);
// Transform the coordinates
const kiCanvasW = qContext2D.canvas.width;
const kiCanvasH = qContext2D.canvas.height;
// Reset the transformation so that the flip transformations do not build up an cancel each other.
qContext2D.resetTransform();
// Flip the y-axis and translate by the height
qContext2D.transform(1, 0, 0, -1, 0, kiCanvasH);
const kiGraphW = 16.0;
const kiGraphH = 12.0;
// I will make this transformation myself because this scaling changes the thickness of the lines.
// The range will be 0 to 8 and 0 to 6: [0, 8)x[0 ,6)
//qContext2D.transform(kiCanvasW/kiGraphW, 0, 0, kiCanvasH/kiGraphH, 0, 0);
// This a simple scaling that puts the points in canvas coordinates
const kdScaleW = kiCanvasW/kiGraphW;
const kdScaleH = kiCanvasH/kiGraphH;
// Set the bounds in the graph
let qLowYElement = document.getElementById("idLowY");
qLowYElement.value = 0;
let qHighYElement = document.getElementById("idHighY");
qHighYElement.value = kiGraphH;
let qLowXElement = document.getElementById("idLowX");
qLowXElement.value = 0;
let qHighXElement = document.getElementById("idHighX");
qHighXElement.value = kiGraphW;
// Generate a random line as the source model
// Make the intercept b in [2,3] and the slope m in [0, .5]
const kdIntercept = Math.random() + 2.0;
const kdSlope = .5*Math.random();
let qSourceLineElement = document.getElementById("idSourceLine");
qSourceLineElement.innerHTML = "y = "+kdSlope.toFixed(2)+"x + "+kdIntercept.toFixed(2);
// Graph the source model line
qContext2D.strokeStyle = "black";
qContext2D.lineWidth = 1;
qContext2D.beginPath();
qContext2D.moveTo(0*kdScaleW, kdIntercept*kdScaleH);
qContext2D.lineTo(kiGraphW*kdScaleW, (kdIntercept + kiGraphW*kdSlope)*kdScaleH);
qContext2D.stroke();
// Generate a set of points with a normal distribution to represent the error
let daY = new Array(kiGraphW + 1);
for (let i = 0; i <= kiGraphW; ++i) {
// Graph the position lines
qContext2D.strokeStyle = "gray";
qContext2D.lineWidth = .25;
qContext2D.beginPath();
qContext2D.moveTo(i*kdScaleW, 0*kdScaleH);
qContext2D.lineTo(i*kdScaleW, kiGraphH*kdScaleH);
qContext2D.stroke();
// Calculate the random points
daY[i] = kdIntercept + i*kdSlope + GenerateRandomPointFromNormalDistribution();
}
for (let i = 0; i <= kiGraphH; ++i) {
qContext2D.beginPath();
qContext2D.moveTo(0*kdScaleW, i*kdScaleH);
qContext2D.lineTo(kiGraphW*kdScaleW, i*kdScaleH);
qContext2D.stroke();
}
// Calculate the least squares
let qOLS = new CLine();
qOLS.SetToLeastSquares(daY);
let qLeastSquaresLineElement = document.getElementById("idLeastSquaresLine");
qLeastSquaresLineElement.innerHTML = qOLS.ToString();
// Graph the position lines
qContext2D.strokeStyle = "red";
qContext2D.lineWidth = 3;
const kdStartY = qOLS.CalculateY(0);
const kdEndY = qOLS.CalculateY(kiGraphW);
qContext2D.beginPath();
qContext2D.moveTo(0*kdScaleW, kdStartY*kdScaleH);
qContext2D.lineTo(kiGraphW*kdScaleW, kdEndY*kdScaleH);
qContext2D.stroke();
for (let i = 0; i <= kiGraphW; ++i) {
// Draw the error bars
const kdLineY = qOLS.CalculateY(i);
qContext2D.strokeStyle = "red";
qContext2D.lineWidth = 1;
qContext2D.beginPath();
qContext2D.moveTo(i*kdScaleW, kdLineY*kdScaleH);
qContext2D.lineTo(i*kdScaleW, daY[i]*kdScaleH);
qContext2D.stroke();
// Graph the points
qContext2D.fillStyle = "red";
qContext2D.beginPath();
const kdRadius = 5;
qContext2D.ellipse(i*kdScaleW, daY[i]*kdScaleH, kdRadius, kdRadius, 0, 0, 2*Math.PI);
qContext2D.fill();
}
// 1, 10, 100, 1000, 10000
let qLine2 = new CLine();
qContext2D.strokeStyle = "green";
qContext2D.lineWidth = 2;
let kdStartYB = qLine2.CalculateY(0);
let kdEndYB = qLine2.CalculateY(kiGraphW);
qContext2D.beginPath();
qContext2D.moveTo(0*kdScaleW, kdStartYB*kdScaleH);
qContext2D.lineTo(kiGraphW*kdScaleW, kdEndYB*kdScaleH);
qContext2D.stroke();
let qBackPropDiv = document.getElementById("idBackPropLines");
qBackPropDiv.innerHTML = "";
// <label style="width:400px;color:blue;float:left;">Backpropagation Line: y = mx + b</label>
let qLineLabel = document.createElement("label");
qLineLabel.style.width = "400px";
qLineLabel.style.color = "green";
qLineLabel.style.float = "left";
qLineLabel.innerHTML = "Backpropagation Line (Initial Line): "+qLine2.ToString();
qBackPropDiv.appendChild(qLineLabel);
qContext2D.strokeStyle = "blue";
for (i = 1; i <= 10000; ++i) {
let dLog = Math.log10(10*i);
qLine2.BackpropagateOneEpoch(daY);
if (Math.abs(dLog - Math.round(dLog)) < 1.0e-15) {
const kdAlpha = .25 + .75*(dLog*dLog*dLog/125);
qContext2D.globalAlpha = kdAlpha;
kdStartYB = qLine2.CalculateY(0);
kdEndYB = qLine2.CalculateY(kiGraphW);
qContext2D.beginPath();
qContext2D.moveTo(0*kdScaleW, kdStartYB*kdScaleH);
qContext2D.lineTo(kiGraphW*kdScaleW, kdEndYB*kdScaleH);
qContext2D.stroke();
qLineLabel = document.createElement("label");
qLineLabel.style.width = "400px";
qLineLabel.style.color = " rgba(0, 0, 255,"+kdAlpha+")";
qLineLabel.style.float = "left";
qLineLabel.innerHTML = "Backpropagation Line (Epoch = "+i+"): "+qLine2.ToString();
qBackPropDiv.appendChild(qLineLabel);
}
}
}
© 20072026 XoaX.net LLC. All rights reserved.