diff --git a/src/playground.ts b/src/playground.ts index aeac0f9c..c9969713 100644 --- a/src/playground.ts +++ b/src/playground.ts @@ -14,7 +14,7 @@ limitations under the License. ==============================================================================*/ import * as nn from "./nn"; -import {HeatMap, reduceMatrix} from "./heatmap"; +import { HeatMap, reduceMatrix } from "./heatmap"; import { State, datasets, @@ -25,14 +25,14 @@ import { getKeyFromValue, Problem } from "./state"; -import {Example2D, shuffle} from "./dataset"; -import {AppendingLineChart} from "./linechart"; +import { Example2D, shuffle } from "./dataset"; +import { AppendingLineChart } from "./linechart"; import * as d3 from 'd3'; let mainWidth; // More scrolling -d3.select(".more button").on("click", function() { +d3.select(".more button").on("click", function () { let position = 800; d3.transition() .duration(1000) @@ -40,10 +40,10 @@ d3.select(".more button").on("click", function() { }); function scrollTween(offset) { - return function() { + return function () { let i = d3.interpolateNumber(window.pageYOffset || - document.documentElement.scrollTop, offset); - return function(t) { scrollTo(0, i(t)); }; + document.documentElement.scrollTop, offset); + return function (t) { scrollTo(0, i(t)); }; }; } @@ -62,14 +62,14 @@ interface InputFeature { label?: string; } -let INPUTS: {[name: string]: InputFeature} = { - "x": {f: (x, y) => x, label: "X_1"}, - "y": {f: (x, y) => y, label: "X_2"}, - "xSquared": {f: (x, y) => x * x, label: "X_1^2"}, - "ySquared": {f: (x, y) => y * y, label: "X_2^2"}, - "xTimesY": {f: (x, y) => x * y, label: "X_1X_2"}, - "sinX": {f: (x, y) => Math.sin(x), label: "sin(X_1)"}, - "sinY": {f: (x, y) => Math.sin(y), label: "sin(X_2)"}, +let INPUTS: { [name: string]: InputFeature } = { + "x": { f: (x, y) => x, label: "X_1" }, + "y": { f: (x, y) => y, label: "X_2" }, + "xSquared": { f: (x, y) => x * x, label: "X_1^2" }, + "ySquared": { f: (x, y) => y * y, label: "X_2^2" }, + "xTimesY": { f: (x, y) => x * y, label: "X_1X_2" }, + "sinX": { f: (x, y) => Math.sin(x), label: "sin(X_1)" }, + "sinY": { f: (x, y) => Math.sin(y), label: "sin(X_2)" }, }; let HIDABLE_CONTROLS = [ @@ -150,21 +150,21 @@ state.getHiddenProps().forEach(prop => { } }); -let boundary: {[id: string]: number[][]} = {}; +let boundary: { [id: string]: number[][] } = {}; let selectedNodeId: string = null; // Plot the heatmap. let xDomain: [number, number] = [-6, 6]; let heatMap = - new HeatMap(300, DENSITY, xDomain, xDomain, d3.select("#heatmap"), - {showAxes: true}); + new HeatMap(300, DENSITY, xDomain, xDomain, d3.select("#heatmap"), + { showAxes: true }); let linkWidthScale = d3.scale.linear() .domain([0, 5]) .range([1, 10]) .clamp(true); let colorScale = d3.scale.linear() - .domain([-1, 0, 1]) - .range(["#f59322", "#e8eaeb", "#0877bd"]) - .clamp(true); + .domain([-1, 0, 1]) + .range(["#f59322", "#e8eaeb", "#0877bd"]) + .clamp(true); let iter = 0; let trainData: Example2D[] = []; let testData: Example2D[] = []; @@ -173,7 +173,7 @@ let lossTrain = 0; let lossTest = 0; let player = new Player(); let lineChart = new AppendingLineChart(d3.select("#linechart"), - ["#777", "black"]); + ["#777", "black"]); function makeGUI() { d3.select("#reset-button").on("click", () => { @@ -207,12 +207,12 @@ function makeGUI() { }); let dataThumbnails = d3.selectAll("canvas[data-dataset]"); - dataThumbnails.on("click", function() { + dataThumbnails.on("click", function () { let newDataset = datasets[this.dataset.dataset]; if (newDataset === state.dataset) { return; // No-op. } - state.dataset = newDataset; + state.dataset = newDataset; dataThumbnails.classed("selected", false); d3.select(this).classed("selected", true); generateData(); @@ -226,12 +226,12 @@ function makeGUI() { .classed("selected", true); let regDataThumbnails = d3.selectAll("canvas[data-regDataset]"); - regDataThumbnails.on("click", function() { + regDataThumbnails.on("click", function () { let newDataset = regDatasets[this.dataset.regdataset]; if (newDataset === state.regDataset) { return; // No-op. } - state.regDataset = newDataset; + state.regDataset = newDataset; regDataThumbnails.classed("selected", false); d3.select(this).classed("selected", true); generateData(); @@ -264,7 +264,7 @@ function makeGUI() { reset(); }); - let showTestData = d3.select("#show-test-data").on("change", function() { + let showTestData = d3.select("#show-test-data").on("change", function () { state.showTestData = this.checked; state.serialize(); userHasInteracted(); @@ -273,7 +273,7 @@ function makeGUI() { // Check/uncheck the checkbox according to the current state. showTestData.property("checked", state.showTestData); - let discretize = d3.select("#discretize").on("change", function() { + let discretize = d3.select("#discretize").on("change", function () { state.discretize = this.checked; state.serialize(); userHasInteracted(); @@ -282,7 +282,7 @@ function makeGUI() { // Check/uncheck the checbox according to the current state. discretize.property("checked", state.discretize); - let percTrain = d3.select("#percTrainData").on("input", function() { + let percTrain = d3.select("#percTrainData").on("input", function () { state.percTrainData = this.value; d3.select("label[for='percTrainData'] .value").text(this.value); generateData(); @@ -292,7 +292,7 @@ function makeGUI() { percTrain.property("value", state.percTrainData); d3.select("label[for='percTrainData'] .value").text(state.percTrainData); - let noise = d3.select("#noise").on("input", function() { + let noise = d3.select("#noise").on("input", function () { state.noise = this.value; d3.select("label[for='noise'] .value").text(this.value); generateData(); @@ -312,7 +312,7 @@ function makeGUI() { noise.property("value", state.noise); d3.select("label[for='noise'] .value").text(state.noise); - let batchSize = d3.select("#batchSize").on("input", function() { + let batchSize = d3.select("#batchSize").on("input", function () { state.batchSize = this.value; d3.select("label[for='batchSize'] .value").text(this.value); parametersChanged = true; @@ -321,15 +321,15 @@ function makeGUI() { batchSize.property("value", state.batchSize); d3.select("label[for='batchSize'] .value").text(state.batchSize); - let activationDropdown = d3.select("#activations").on("change", function() { + let activationDropdown = d3.select("#activations").on("change", function () { state.activation = activations[this.value]; parametersChanged = true; reset(); }); activationDropdown.property("value", - getKeyFromValue(activations, state.activation)); + getKeyFromValue(activations, state.activation)); - let learningRate = d3.select("#learningRate").on("change", function() { + let learningRate = d3.select("#learningRate").on("change", function () { state.learningRate = +this.value; state.serialize(); userHasInteracted(); @@ -338,22 +338,22 @@ function makeGUI() { learningRate.property("value", state.learningRate); let regularDropdown = d3.select("#regularizations").on("change", - function() { - state.regularization = regularizations[this.value]; - parametersChanged = true; - reset(); - }); + function () { + state.regularization = regularizations[this.value]; + parametersChanged = true; + reset(); + }); regularDropdown.property("value", - getKeyFromValue(regularizations, state.regularization)); + getKeyFromValue(regularizations, state.regularization)); - let regularRate = d3.select("#regularRate").on("change", function() { + let regularRate = d3.select("#regularRate").on("change", function () { state.regularizationRate = +this.value; parametersChanged = true; reset(); }); regularRate.property("value", state.regularizationRate); - let problem = d3.select("#problem").on("change", function() { + let problem = d3.select("#problem").on("change", function () { state.problem = problems[this.value]; generateData(); drawDatasetThumbnails(); @@ -378,7 +378,7 @@ function makeGUI() { window.addEventListener("resize", () => { let newWidth = document.querySelector("#main-part") - .getBoundingClientRect().width; + .getBoundingClientRect().width; if (newWidth !== mainWidth) { mainWidth = newWidth; drawNetwork(network); @@ -409,19 +409,19 @@ function updateWeightsUI(network: nn.Node[][], container) { for (let j = 0; j < node.inputLinks.length; j++) { let link = node.inputLinks[j]; container.select(`#link${link.source.id}-${link.dest.id}`) - .style({ - "stroke-dashoffset": -iter / 3, - "stroke-width": linkWidthScale(Math.abs(link.weight)), - "stroke": colorScale(link.weight) - }) - .datum(link); + .style({ + "stroke-dashoffset": -iter / 3, + "stroke-width": linkWidthScale(Math.abs(link.weight)), + "stroke": colorScale(link.weight) + }) + .datum(link); } } } } function drawNode(cx: number, cy: number, nodeId: string, isInput: boolean, - container, node?: nn.Node) { + container, node?: nn.Node) { let x = cx - RECT_SIZE / 2; let y = cy - RECT_SIZE / 2; @@ -443,7 +443,7 @@ function drawNode(cx: number, cy: number, nodeId: string, isInput: boolean, let activeOrNotClass = state[nodeId] ? "active" : "inactive"; if (isInput) { let label = INPUTS[nodeId].label != null ? - INPUTS[nodeId].label : nodeId; + INPUTS[nodeId].label : nodeId; // Draw the input label. let text = nodeGroup.append("text").attr({ class: "main-label", @@ -463,9 +463,9 @@ function drawNode(cx: number, cy: number, nodeId: string, isInput: boolean, text.append("tspan").text(prefix); } text.append("tspan") - .attr("baseline-shift", sep === "_" ? "sub" : "super") - .style("font-size", "9px") - .text(suffix); + .attr("baseline-shift", sep === "_" ? "sub" : "super") + .style("font-size", "9px") + .text(suffix); } if (label.substring(lastIndex)) { text.append("tspan").text(label.substring(lastIndex)); @@ -484,9 +484,9 @@ function drawNode(cx: number, cy: number, nodeId: string, isInput: boolean, y: RECT_SIZE - BIAS_SIZE + 3, width: BIAS_SIZE, height: BIAS_SIZE, - }).on("mouseenter", function() { + }).on("mouseenter", function () { updateHoverCard(HoverType.BIAS, node, d3.mouse(container.node())); - }).on("mouseleave", function() { + }).on("mouseleave", function () { updateHoverCard(null); }); } @@ -502,23 +502,23 @@ function drawNode(cx: number, cy: number, nodeId: string, isInput: boolean, left: `${x + 3}px`, top: `${y + 3}px` }) - .on("mouseenter", function() { + .on("mouseenter", function () { selectedNodeId = nodeId; div.classed("hovered", true); nodeGroup.classed("hovered", true); updateDecisionBoundary(network, false); heatMap.updateBackground(boundary[nodeId], state.discretize); }) - .on("mouseleave", function() { + .on("mouseleave", function () { selectedNodeId = null; div.classed("hovered", false); nodeGroup.classed("hovered", false); updateDecisionBoundary(network, false); heatMap.updateBackground(boundary[nn.getOutputNode(network).id], - state.discretize); + state.discretize); }); if (isInput) { - div.on("click", function() { + div.on("click", function () { state[nodeId] = !state[nodeId]; parametersChanged = true; reset(); @@ -529,8 +529,8 @@ function drawNode(cx: number, cy: number, nodeId: string, isInput: boolean, div.classed(activeOrNotClass, true); } let nodeHeatMap = new HeatMap(RECT_SIZE, DENSITY / 10, xDomain, - xDomain, div, {noSvg: true}); - div.datum({heatmap: nodeHeatMap, id: nodeId}); + xDomain, div, { noSvg: true }); + div.datum({ heatmap: nodeHeatMap, id: nodeId }); } @@ -551,7 +551,7 @@ function drawNetwork(network: nn.Node[][]): void { svg.attr("width", width); // Map of all node coordinates. - let node2coord: {[id: string]: {cx: number, cy: number}} = {}; + let node2coord: { [id: string]: { cx: number, cy: number } } = {}; let container = svg.append("g") .classed("core", true) .attr("transform", `translate(${padding},${padding})`); @@ -559,8 +559,8 @@ function drawNetwork(network: nn.Node[][]): void { let numLayers = network.length; let featureWidth = 118; let layerScale = d3.scale.ordinal() - .domain(d3.range(1, numLayers - 1)) - .rangePoints([featureWidth, width - RECT_SIZE], 0.7); + .domain(d3.range(1, numLayers - 1)) + .rangePoints([featureWidth, width - RECT_SIZE], 0.7); let nodeIndexScale = (nodeIndex: number) => nodeIndex * (RECT_SIZE + 25); @@ -575,7 +575,7 @@ function drawNetwork(network: nn.Node[][]): void { let maxY = nodeIndexScale(nodeIds.length); nodeIds.forEach((nodeId, i) => { let cy = nodeIndexScale(i) + RECT_SIZE / 2; - node2coord[nodeId] = {cx, cy}; + node2coord[nodeId] = { cx, cy }; drawNode(cx, cy, nodeId, true, container); }); @@ -588,15 +588,15 @@ function drawNetwork(network: nn.Node[][]): void { for (let i = 0; i < numNodes; i++) { let node = network[layerIdx][i]; let cy = nodeIndexScale(i) + RECT_SIZE / 2; - node2coord[node.id] = {cx, cy}; + node2coord[node.id] = { cx, cy }; drawNode(cx, cy, node.id, false, container, node); // Show callout to thumbnails. let numNodes = network[layerIdx].length; let nextNumNodes = network[layerIdx + 1].length; if (idWithCallout == null && - i === numNodes - 1 && - nextNumNodes <= numNodes) { + i === numNodes - 1 && + nextNumNodes <= numNodes) { calloutThumb.style({ display: null, top: `${20 + 3 + cy}px`, @@ -609,16 +609,16 @@ function drawNetwork(network: nn.Node[][]): void { for (let j = 0; j < node.inputLinks.length; j++) { let link = node.inputLinks[j]; let path: SVGPathElement = drawLink(link, node2coord, network, - container, j === 0, j, node.inputLinks.length).node() as any; + container, j === 0, j, node.inputLinks.length).node() as any; // Show callout to weights. let prevLayer = network[layerIdx - 1]; let lastNodePrevLayer = prevLayer[prevLayer.length - 1]; if (targetIdWithCallout == null && - i === numNodes - 1 && - link.source.id === lastNodePrevLayer.id && - (link.source.id !== idWithCallout || numLayers <= 5) && - link.dest.id !== idWithCallout && - prevLayer.length >= numNodes) { + i === numNodes - 1 && + link.source.id === lastNodePrevLayer.id && + (link.source.id !== idWithCallout || numLayers <= 5) && + link.dest.id !== idWithCallout && + prevLayer.length >= numNodes) { let midPoint = path.getPointAtLength(path.getTotalLength() * 0.7); calloutWeights.style({ display: null, @@ -635,23 +635,27 @@ function drawNetwork(network: nn.Node[][]): void { cx = width + RECT_SIZE / 2; let node = network[numLayers - 1][0]; let cy = nodeIndexScale(0) + RECT_SIZE / 2; - node2coord[node.id] = {cx, cy}; + node2coord[node.id] = { cx, cy }; // Draw links. for (let i = 0; i < node.inputLinks.length; i++) { let link = node.inputLinks[i]; drawLink(link, node2coord, network, container, i === 0, i, - node.inputLinks.length); + node.inputLinks.length); } // Adjust the height of the svg. svg.attr("height", maxY); // Adjust the height of the features column. + const EXTRA_BOTTOM_PADDING = 24; + let height = Math.max( getRelativeHeight(calloutThumb), getRelativeHeight(calloutWeights), getRelativeHeight(d3.select("#network")) - ); - d3.select(".column.features").style("height", height + "px"); + ) + EXTRA_BOTTOM_PADDING; + + d3.select(".column.features") + .style("height", `${height}px`); } function getRelativeHeight(selection) { @@ -667,34 +671,34 @@ function addPlusMinusControl(x: number, layerIdx: number) { let i = layerIdx - 1; let firstRow = div.append("div").attr("class", `ui-numNodes${layerIdx}`); firstRow.append("button") - .attr("class", "mdl-button mdl-js-button mdl-button--icon") - .on("click", () => { - let numNeurons = state.networkShape[i]; - if (numNeurons >= 8) { - return; - } - state.networkShape[i]++; - parametersChanged = true; - reset(); - }) + .attr("class", "mdl-button mdl-js-button mdl-button--icon") + .on("click", () => { + let numNeurons = state.networkShape[i]; + if (numNeurons >= 8) { + return; + } + state.networkShape[i]++; + parametersChanged = true; + reset(); + }) .append("i") - .attr("class", "material-icons") - .text("add"); + .attr("class", "material-icons") + .text("add"); firstRow.append("button") - .attr("class", "mdl-button mdl-js-button mdl-button--icon") - .on("click", () => { - let numNeurons = state.networkShape[i]; - if (numNeurons <= 1) { - return; - } - state.networkShape[i]--; - parametersChanged = true; - reset(); - }) + .attr("class", "mdl-button mdl-js-button mdl-button--icon") + .on("click", () => { + let numNeurons = state.networkShape[i]; + if (numNeurons <= 1) { + return; + } + state.networkShape[i]--; + parametersChanged = true; + reset(); + }) .append("i") - .attr("class", "material-icons") - .text("remove"); + .attr("class", "material-icons") + .text("remove"); let suffix = state.networkShape[i] > 1 ? "s" : ""; div.append("div").text( @@ -703,7 +707,7 @@ function addPlusMinusControl(x: number, layerIdx: number) { } function updateHoverCard(type: HoverType, nodeOrLink?: nn.Node | nn.Link, - coordinates?: [number, number]) { + coordinates?: [number, number]) { let hovercard = d3.select("#hovercard"); if (type == null) { hovercard.style("display", "none"); @@ -714,7 +718,7 @@ function updateHoverCard(type: HoverType, nodeOrLink?: nn.Node | nn.Link, hovercard.select(".value").style("display", "none"); let input = hovercard.select("input"); input.style("display", null); - input.on("input", function() { + input.on("input", function () { if (this.value != null && this.value !== "") { if (type === HoverType.WEIGHT) { (nodeOrLink as nn.Link).weight = +this.value; @@ -750,9 +754,9 @@ function updateHoverCard(type: HoverType, nodeOrLink?: nn.Node | nn.Link, } function drawLink( - input: nn.Link, node2coord: {[id: string]: {cx: number, cy: number}}, - network: nn.Node[][], container, - isFirst: boolean, index: number, length: number) { + input: nn.Link, node2coord: { [id: string]: { cx: number, cy: number } }, + network: nn.Node[][], container, + isFirst: boolean, index: number, length: number) { let line = container.insert("path", ":first-child"); let source = node2coord[input.source.id]; let dest = node2coord[input.dest.id]; @@ -779,9 +783,9 @@ function drawLink( container.append("path") .attr("d", diagonal(datum, 0)) .attr("class", "link-hover") - .on("mouseenter", function() { + .on("mouseenter", function () { updateHoverCard(HoverType.WEIGHT, input, d3.mouse(this)); - }).on("mouseleave", function() { + }).on("mouseleave", function () { updateHoverCard(null); }); return line; @@ -856,15 +860,15 @@ function updateUI(firstStep = false) { // Get the decision boundary of the network. updateDecisionBoundary(network, firstStep); let selectedId = selectedNodeId != null ? - selectedNodeId : nn.getOutputNode(network).id; + selectedNodeId : nn.getOutputNode(network).id; heatMap.updateBackground(boundary[selectedId], state.discretize); // Update all decision boundaries. d3.select("#network").selectAll("div.canvas") - .each(function(data: {heatmap: HeatMap, id: string}) { - data.heatmap.updateBackground(reduceMatrix(boundary[data.id], 10), + .each(function (data: { heatmap: HeatMap, id: string }) { + data.heatmap.updateBackground(reduceMatrix(boundary[data.id], 10), state.discretize); - }); + }); function zeroPad(n: number): string { let pad = "000000"; @@ -937,7 +941,7 @@ export function getOutputWeights(network: nn.Node[][]): number[] { return weights; } -function reset(onStartup=false) { +function reset(onStartup = false) { lineChart.reset(); state.serialize(); if (!onStartup) { @@ -951,12 +955,12 @@ function reset(onStartup=false) { // Make a simple network. iter = 0; - let numInputs = constructInput(0 , 0).length; + let numInputs = constructInput(0, 0).length; let shape = [numInputs].concat(state.networkShape).concat([1]); let outputActivation = (state.problem === Problem.REGRESSION) ? - nn.Activations.LINEAR : nn.Activations.TANH; + nn.Activations.LINEAR : nn.Activations.TANH; network = nn.buildNetwork(shape, state.activation, outputActivation, - state.regularization, constructInputIds(), state.initZero); + state.regularization, constructInputIds(), state.initZero); lossTrain = getLoss(network, trainData); lossTest = getLoss(network, testData); drawNetwork(network); @@ -984,7 +988,7 @@ function initTutorial() { "margin-top": "20px", "margin-bottom": "20px", }) - .text(title.text()); + .text(title.text()); document.title = title.text(); } }); @@ -998,7 +1002,7 @@ function drawDatasetThumbnails() { canvas.setAttribute("height", h); let context = canvas.getContext("2d"); let data = dataGenerator(200, 0); - data.forEach(function(d) { + data.forEach(function (d) { context.fillStyle = colorScale(d.label); context.fillRect(w * (d.x + 6) / 12, h * (d.y + 6) / 12, 4, 4); }); @@ -1009,7 +1013,7 @@ function drawDatasetThumbnails() { if (state.problem === Problem.CLASSIFICATION) { for (let dataset in datasets) { let canvas: any = - document.querySelector(`canvas[data-dataset=${dataset}]`); + document.querySelector(`canvas[data-dataset=${dataset}]`); let dataGenerator = datasets[dataset]; renderThumbnail(canvas, dataGenerator); } @@ -1017,7 +1021,7 @@ function drawDatasetThumbnails() { if (state.problem === Problem.REGRESSION) { for (let regDataset in regDatasets) { let canvas: any = - document.querySelector(`canvas[data-regDataset=${regDataset}]`); + document.querySelector(`canvas[data-regDataset=${regDataset}]`); let dataGenerator = regDatasets[regDataset]; renderThumbnail(canvas, dataGenerator); } @@ -1049,7 +1053,7 @@ function hideControls() { if (hiddenProps.indexOf(id) === -1) { input.attr("checked", "true"); } - input.on("change", function() { + input.on("change", function () { state.setHideProperty(id, !this.checked); state.serialize(); userHasInteracted(); @@ -1073,9 +1077,9 @@ function generateData(firstTime = false) { } Math.seedrandom(state.seed); let numSamples = (state.problem === Problem.REGRESSION) ? - NUM_SAMPLES_REGRESS : NUM_SAMPLES_CLASSIFY; + NUM_SAMPLES_REGRESS : NUM_SAMPLES_CLASSIFY; let generator = state.problem === Problem.CLASSIFICATION ? - state.dataset : state.regDataset; + state.dataset : state.regDataset; let data = generator(numSamples, state.noise / 100); // Shuffle the data in-place. shuffle(data); @@ -1100,7 +1104,7 @@ function userHasInteracted() { page = `/v/tutorials/${state.tutorial}`; } ga('set', 'page', page); - ga('send', 'pageview', {'sessionControl': 'start'}); + ga('send', 'pageview', { 'sessionControl': 'start' }); } function simulationStarted() {