diff --git a/plinko/score.js b/plinko/score.js index 0a7109c..107910e 100644 --- a/plinko/score.js +++ b/plinko/score.js @@ -1,8 +1,111 @@ +const outputs = []; +// determine the corellation between bucket and predictionPoint + function onScoreUpdate(dropPosition, bounciness, size, bucketLabel) { - // Ran every time a balls drops into a bucket + outputs.push([dropPosition, bounciness, size, bucketLabel]); } function runAnalysis() { - // Write code here to analyze stuff + const testSetSize = 100; + const k = 10; + const colNames = ['Drop Position', 'Bounciness', 'Ball Size']; + + // vary k using range + _.range(0, 3).forEach((feature) => { + const data = _.map(outputs, (row) => [row[feature], _.last(row)]); + + const [testSet, trainingSet] = splitDataSet(minMax(data, 1), testSetSize); + const accuracy = _.chain(testSet) + .filter((testPoint) => knn(trainingSet, _.initial(testPoint), k) === _.last(testPoint)) + .size() + .divide(testSetSize) + .value(); + + console.log(`k(${k}) Accuracy for ${colNames[feature]}: ${accuracy * 100}%`); + }); +} + +function knn(data, point, k) { + // K-Nearest Neighbor Algorithm + return ( + _.chain(data) + // [[distance(dropPosition, predictionPoint), bucketLabel],[72,4],[227,5]] + .map((row) => { + return [distance(_.initial(row), point), _.last(row)]; + }) + // sort by drop position + .sortBy((row) => row[0]) + // Gets the top 'k' results from sorted list + .slice(0, k) + // counts frequency of buckets + // e.g., {"3":1,"4":2} + .countBy((row) => row[1]) + // e.g., [["3",1],["4",2]] + // converts the countBy obj to an multidimensional array + .toPairs() + // sorts so that the most frequent is the last array element + .sortBy((row) => row[1]) + // get the last array element of ["bucket", frequency] + .last() + // e.g., "4" + // get the bucket number (first element) + .first() + // e.g., 4 + // convert the string "4" to int 4 + .parseInt() + // end the chain and return the value + .value() + ); +} + +function distance(pointA, pointB) { + // pointA/B are arrays + // employing the pythagorean therom to solve a multidimensional point distance + _.chain(pointA) + // takes each value at the same index of each array and creates a new zipped + // array: + // [[pointA[0], pointB[0]], [pointA[1], pointB[1]] ... ] + .zip(pointB) + // subtracts b from a + .map(([a, b]) => (a - b) ** 2) + // sums the squares + .sum() + // returns the squareroot of the sum + .value() ** 0.5; } +function splitDataSet(data, testCount) { + const shuffled = _.shuffle(data); + + const testSet = _.slice(shuffled, 0, testCount); + const trainingSet = _.slice(shuffled, testCount); + + return [testSet, trainingSet]; +} + +function minMax(data, featureCount) { + const clonedData = _.cloneDeep(data); + + // iterate over each feature (independent variables) + for (let i = 0; i < featureCount; i++) { + const column = clonedData.map((row) => row[i]); + const min = _.min(column); + const max = _.max(column); + + // iterate over each row [j] in clonedData + // and normalize each feature [i] + // a row would look something like : + // + // [ position, bounciness, ballSize, bucketName ] + // + // where bucketName is a label and not normalized + for (let j = 0; j < clonedData.length; j++) { + clonedData[j][i] = (clonedData[j][i] - min) / (max - min); + if (max - min === 0) { + clonedData[j][i] = 0; + } + } + } + + return clonedData; +} diff --git a/regressions/index.js b/regressions/index.js new file mode 100644 index 0000000..54b7529 --- /dev/null +++ b/regressions/index.js @@ -0,0 +1,62 @@ +require('@tensorflow/tfjs-node'); +const tf = require('@tensorflow/tfjs'); +const loadCSV = require('./load-csv'); +const LinearRegression = require('./linear-regression'); +const plot = require('node-remote-plot'); +const { initial } = require('lodash'); + +let { features, labels, testFeatures, testLabels } = loadCSV('./cars.csv', { + shuffle: true, + splitTest: 50, + dataColumns: ['displacement', 'horsepower', 'weight', 'acceleration'], + labelColumns: ['mpg'], +}); + +const initLR = 0.1; +const regression = new LinearRegression(features, labels, { + learningRate: initLR, + iterations: 30, + batchSize: 1, +}); + +regression.train(); +/** + * weights tensor has a [2,1] shape and looks like this: + * [ + * [0], + * [0] + * ] + */ +const r2 = regression.test(testFeatures, testLabels); +plot({ + x: regression.mseHistory.reverse(), + xLabel: 'Iterations', + yLabel: 'MSE', +}); +console.log('R2 : ', r2, ' initLR: ', initLR, ' iterations: ', regression.options.iterations); + +litersToCID = (liters) => { + // There are 61 cubic inches in a liter + return liters * 61; +}; + +/** + * mpg, cyl, displacement, hp, wt, acc + * [13,8,400,175,2.57,12], + * [11,8,400,150,2.5,14], + * [12,8,383,180,2.48,11], + * [12,8,429,198,2.48,11], + * [12,8,455,225,2.48,11], + * [12,8,400,167,2.45,12], + * [13,8,400,170,2.37,12], + */ +vehicles = [ + [400, 175, 2.57, 12], + [400, 150, 2.5, 14], + [383, 180, 2.48, 11], + [429, 198, 2.48, 11], + [455, 225, 2.48, 11], + [400, 167, 2.45, 12], + [400, 170, 2.37, 12], +]; +regression.predict(vehicles).print(); diff --git a/regressions/linear-regression.js b/regressions/linear-regression.js new file mode 100644 index 0000000..648d28b --- /dev/null +++ b/regressions/linear-regression.js @@ -0,0 +1,285 @@ +const tf = require('@tensorflow/tfjs'); +const _ = require('lodash'); + +class LinearRegression { + constructor(features, labels, options) { + this.features = this.processFeatures(features); + + // Labels Tensor + this.labels = tf.tensor(labels); + + this.mseHistory = []; + + this.options = Object.assign( + { + learningRate: 0.1, + iterations: 1000, + }, + options + ); + + /** + * weights tensor + * by convention initial guesses are given the value of either 0 or 1 + * + * resulting tensor has a [n,1] shape and looks like this, where n is the + * number of colums present in the features tensor after adding the + * column of 1s : + * [ + * [0], + * [0], + * ... + * ] + * + */ + this.weights = tf.zeros([this.features.shape[1], 1]); + } + + gradientDescent(features, labels) { + /** + * Matrix Multiplication + * + * Slope of MSE features * ((features * weights) - labels) + * with respect = ------------------------------------------ + * to m & b n + * + * n = total number of features. + * (features * weights) + * -> is essentially the (mx + b) portion of the MSE formula + * -> here is represented as `currentGuesses` below + * + * (currentGuesses - labels) aka ((features * weights) - labels) + * -> is represented below as `differences` + * + * (features * differences) / n + * -> is represented below as `slopes` + * -> `features` is first transposed prior to multiplying by `differences` + */ + + /** + * features shape: [n, c] where n is number of records (rows) and + * `c` is the number of columns or unique + * features (e.g., horsepoer, weight, etc, displacement) + * + * weights shape: [2,1] + * + * initially we're only using one feature so the features size is [n, 1], but + * we prepended a column of 1s to the features tensor, so it now has the + * shape of [n,2] so that we could multiply them, e.g., [n,2][2,1] works. + * + * currentGuesses will then have the shape of [n,1] + */ + const currentGuesses = features.matMul(this.weights); + + /** + * Elementwise subtraction would render `differences` with the same shape + * that currentGuesses had of [n,1] + */ + const differences = currentGuesses.sub(labels); + + /** + * transpose features from an [n, c] to a [c, n] shape. + * -> initially `c` = 2 (feature, plus column of 1s) + * -> n is the number of records/rows. + * + * `differences` has an [n,1] shape so multiplying [c, n][n, 1] shapes works. + * + * Remember that tensors are immutable, so when we then divide by + * the number of records, we can use features.shape[0] because features + * is still the shape [n, 2] + */ + const slopes = features.transpose().matMul(differences).div(features.shape[0]); + + // update the weights by multiplying the just calculated slopes by the learning rate + this.weights = this.weights.sub(slopes.mul(this.options.learningRate)); + } + + train() { + // determine the number of batches needed to process the dataset + const batchQuantity = Math.floor(this.features.shape[0] / this.options.batchSize); + + for (let i = 0; i < this.options.iterations; i++) { + for (let j = 0; j < batchQuantity; j++) { + /** + * Example for start index + * ----------------------- + * If 88 records and batchSize of 10, batchQuantity is 9. + * So: j * batchSize iterations look like this: + * 0 * 10 = 0 + * 1 * 10 = 10 + * 2 * 10 = 20 + * ... + * 8 * 10 = 80 (only having 8 records) + * + * In this way we accurately have the record number to start with for + * the next batch slice + */ + const startIndex = j * this.options.batchSize; + + /** + * Slicing the features + * -------------------- + * Slicing a 2D tensor requires a starting coord and a shape of the + * slice you want to take. + * + * tensor.slice([0,0], [10,-1]) would slice tensor from the first + * row,col and return 10 rows with as many columns as present. + * + * tensor.slice([10,0], [10,-1]) would slice tensor from the 10th row + * and first column returning 10 rows with as many columns as present. + * + * By multiplying j * batchSize we always have the correct row index and + * we always want the first column so [j * batchSize, 0] is perfect. By + * using [batchSize,-1] we always have the correct shape to extract. + */ + const featuresSlice = this.features.slice([startIndex, 0], [this.options.batchSize, -1]); + /** + * We need the correct number of labels to process so that our Matricies + * are the right sizes for multiplication in gradientDescent. + */ + const labelsSlice = this.labels.slice([startIndex, 0], [this.options.batchSize, -1]); + this.gradientDescent(featuresSlice, labelsSlice); + } + // make updates after processing each batch + this.recordMSE(); + this.updateLearningRate(); + } + } + + predict(observations) { + return this.processFeatures(observations).matMul(this.weights); + } + + test(testFeatures, testLabels) { + // convert multidimensinal arrays to tensors + testFeatures = this.processFeatures(testFeatures); + testLabels = tf.tensor(testLabels); + const predictions = testFeatures.matMul(this.weights); + + /** + * Sum of Squares Residual + * ------------------------ + * ∑ (actual - predicted) ^2 + * + * 1. testLabels contains all of our actual mpg values + * 2. we subtract our `predictions` based on feature horsepower + * 3. we square that difference + * 4. add all those squares together into a single [1,1] tensor. Note that + * we don't use an axis argument in the sum() method because we want + * all the values in the matrix added together into a single value as + * opposed to adding all the columns together or all the rows. + * 5. get the value of that summation + * + */ + + // prettier-ignore + const ss_res = testLabels // 1. + .sub(predictions) // 2. + .pow(2) // 3. + .sum() // 4. + .get(); // 5. + + /** + * Sum of Squares Total + * ------------------------ + * ∑ (actual - average) ^2 + * + * We'll follow the process outlined above for ss_res, substituting the + * `average` value for mpg rather than the predicted value. + */ + + const ss_tot = testLabels.sub(testLabels.mean()).pow(2).sum().get(); + + // return the coefficient of determination R^2 + // R^2 = 1 - (ss_res / ss_tot) + return 1 - ss_res / ss_tot; + } + + /** + * processFeatures + * + * 1. cast features Array into a tensor + * 2. prepend a column of 1s + * 3. return new tensor. + */ + processFeatures(features) { + /** + * Cast Array into Features Tensor + * + * Initially, features is passed in as a JS array and just has a single + * feature (horsepower) and looks something like this with an [n, 1] shape. + * + * [ + * [88], + * [152], + * [245], + * ... + * ] + * + */ + features = tf.tensor(features); + + features = this.standardize(features); + /** + * prepend a column of `1s` to the features tensor so that it now looks + * something like this with a [n, 2] shape: + * [ + * [1, n], + * [1, n], + * [1, n], + * ... + * ] + */ + features = tf.ones([features.shape[0], 1]).concat(features, 1); + + return features; + } + + /** + * Standarize + */ + standardize(features) { + const { mean, variance } = tf.moments(features, 0); + + // if instance variables are not defined, define them, otherwise, use + // the previously defined value. + this.mean = this.mean || mean; + this.variance = this.variance || variance; + + return features.sub(this.mean).div(this.variance.pow(0.5)); + } + + recordMSE() { + /** + * calculating MSE + * 1/n ∑ ((features * weights) - labels)^2) + */ + // prettier-ignore + const mse = this.features + .matMul(this.weights) + .sub(this.labels) + .pow(2) + .sum() + .div(this.features.shape[0]) + .get(); + + // place current mse at the top of the array + this.mseHistory.unshift(mse); + } + + updateLearningRate() { + if (this.mseHistory.length < 2) { + return; + } + + // if our guesses are getting worse, then decrease learning rate + if (this.mseHistory[0] > this.mseHistory[1]) { + this.options.learningRate /= 2; + } else { + // increase learning rate since our guess was an improvement + this.options.learningRate *= 1.05; + } + } +} + +module.exports = LinearRegression; diff --git a/regressions/package-lock.json b/regressions/package-lock.json index 76cb410..3bc7d1b 100644 --- a/regressions/package-lock.json +++ b/regressions/package-lock.json @@ -1,8 +1,476 @@ { "name": "logistic_regression", "version": "1.0.0", - "lockfileVersion": 1, + "lockfileVersion": 2, "requires": true, + "packages": { + "": { + "name": "logistic_regression", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "@tensorflow/tfjs-node": "^0.1.17", + "lodash": "^4.17.11", + "memoize": "^0.1.1", + "mnist-data": "^1.2.6", + "node-remote-plot": "^1.2.0", + "shuffle-seed": "^1.1.6" + } + }, + "node_modules/@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha1-m4sMxmPWaafY9vXQiToU00jzD78=" + }, + "node_modules/@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==" + }, + "node_modules/@protobufjs/codegen": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", + "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==" + }, + "node_modules/@protobufjs/eventemitter": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", + "integrity": "sha1-NVy8mLr61ZePntCV85diHx0Ga3A=" + }, + "node_modules/@protobufjs/fetch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", + "integrity": "sha1-upn7WYYUr2VwDBYZ/wbUVLDYTEU=", + "dependencies": { + "@protobufjs/aspromise": "1.1.2", + "@protobufjs/inquire": "1.1.0" + } + }, + "node_modules/@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha1-Xp4avctz/Ap8uLKR33jIy9l7h9E=" + }, + "node_modules/@protobufjs/inquire": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", + "integrity": "sha1-/yAOPnzyQp4tyvwRQIKOjMY48Ik=" + }, + "node_modules/@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha1-bMKyDFya1q0NzP0hynZz2Nf79o0=" + }, + "node_modules/@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha1-Cf0V8tbTq/qbZbw2ZQbWrXhG/1Q=" + }, + "node_modules/@protobufjs/utf8": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", + "integrity": "sha1-p3c2C1s5oaLlEG+OhY8v0tBgxXA=" + }, + "node_modules/@tensorflow/tfjs": { + "version": "0.12.7", + "resolved": "https://registry.npmjs.org/@tensorflow/tfjs/-/tfjs-0.12.7.tgz", + "integrity": "sha512-sGqnS7+Zj4SK6ap+fdFDGgddQf7l9RJBkWJc36frwP2F4LmFQQ5ED4+Wq7cBM1LzuyNq0p3pREWBbCfab0pnyw==", + "dependencies": { + "@tensorflow/tfjs-converter": "0.5.9", + "@tensorflow/tfjs-core": "0.12.17", + "@tensorflow/tfjs-layers": "0.7.5" + } + }, + "node_modules/@tensorflow/tfjs-converter": { + "version": "0.5.9", + "resolved": "https://registry.npmjs.org/@tensorflow/tfjs-converter/-/tfjs-converter-0.5.9.tgz", + "integrity": "sha512-48sw17WffIoPYTN2gNZ5HWvjKLtQYXrSy+mqaZtiWaRYVjDzJdla6g7dPAL77MR2rxQAfVYMXg8GRDBmkzyBDw==", + "dependencies": { + "@types/long": "3.0.32", + "protobufjs": "6.8.8" + } + }, + "node_modules/@tensorflow/tfjs-core": { + "version": "0.12.17", + "resolved": "https://registry.npmjs.org/@tensorflow/tfjs-core/-/tfjs-core-0.12.17.tgz", + "integrity": "sha512-CgFazQpGB21n1LRSxgyMwy0cN6WtuUPBP2W75zk6Rw+gFUXb8ZNh7fhn4nObjgKeIka36TI9MvT1FYrY+z150w==", + "dependencies": { + "@types/seedrandom": "2.4.27", + "@types/webgl-ext": "0.0.29", + "@types/webgl2": "0.0.4", + "seedrandom": "2.4.4" + }, + "engines": { + "yarn": ">= 1.3.2" + } + }, + "node_modules/@tensorflow/tfjs-layers": { + "version": "0.7.5", + "resolved": "https://registry.npmjs.org/@tensorflow/tfjs-layers/-/tfjs-layers-0.7.5.tgz", + "integrity": "sha512-JIo4l0yEIfYi+quJG71wAeCP9tgXICg/MIOstowfCVGTHKh8oBVEm39bAI/zyTYYtFVLHeQSvY2KuRCN2h0nBg==" + }, + "node_modules/@tensorflow/tfjs-node": { + "version": "0.1.17", + "resolved": "https://registry.npmjs.org/@tensorflow/tfjs-node/-/tfjs-node-0.1.17.tgz", + "integrity": "sha512-NCTmf87u0XhZBE4lAHGxmjQneZrkkQRZCyUmSlFo4LRfkRIrFT+QJYIonJ/tJof8NtCNLRq9yVS/d5x+FvqntA==", + "hasInstallScript": true, + "dependencies": { + "@tensorflow/tfjs": "0.12.7", + "adm-zip": "0.4.11", + "bindings": "1.3.0", + "progress": "2.0.0", + "rimraf": "2.6.2", + "tar": "4.4.6" + } + }, + "node_modules/@types/long": { + "version": "3.0.32", + "resolved": "https://registry.npmjs.org/@types/long/-/long-3.0.32.tgz", + "integrity": "sha512-ZXyOOm83p7X8p3s0IYM3VeueNmHpkk/yMlP8CLeOnEcu6hIwPH7YjZBvhQkR0ZFS2DqZAxKtJ/M5fcuv3OU5BA==" + }, + "node_modules/@types/node": { + "version": "10.11.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-10.11.2.tgz", + "integrity": "sha512-XubfQDIg88PGJ7netQPf3QOKHF7Xht4WXGtg5W7cGBeQs9ETbYKwfchR9o+tRRA9iLTQ7nAre85M205JbYsjJA==" + }, + "node_modules/@types/seedrandom": { + "version": "2.4.27", + "resolved": "https://registry.npmjs.org/@types/seedrandom/-/seedrandom-2.4.27.tgz", + "integrity": "sha1-nbVjk33YaRX2kJK8QyWdL0hXjkE=" + }, + "node_modules/@types/webgl-ext": { + "version": "0.0.29", + "resolved": "https://registry.npmjs.org/@types/webgl-ext/-/webgl-ext-0.0.29.tgz", + "integrity": "sha512-ZlVjDQU5Vlc9hF4LGdDldujZUf0amwlwGv1RI2bfvdrEHIl6X/7MZVpemJUjS7NxD9XaKfE8SlFrxsfXpUkt/A==" + }, + "node_modules/@types/webgl2": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/@types/webgl2/-/webgl2-0.0.4.tgz", + "integrity": "sha512-PACt1xdErJbMUOUweSrbVM7gSIYm1vTncW2hF6Os/EeWi6TXYAYMPp+8v6rzHmypE5gHrxaxZNXgMkJVIdZpHw==" + }, + "node_modules/adm-zip": { + "version": "0.4.11", + "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.4.11.tgz", + "integrity": "sha512-L8vcjDTCOIJk7wFvmlEUN7AsSb8T+2JrdP7KINBjzr24TJ5Mwj590sLu3BC7zNZowvJWa/JtPmD8eJCzdtDWjA==", + "engines": { + "node": ">=0.3.0" + } + }, + "node_modules/axios": { + "version": "0.18.0", + "resolved": "http://registry.npmjs.org/axios/-/axios-0.18.0.tgz", + "integrity": "sha1-MtU+SFHv3AoRmTts0AB4nXDAUQI=", + "dependencies": { + "follow-redirects": "1.5.8", + "is-buffer": "1.1.6" + } + }, + "node_modules/balanced-match": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" + }, + "node_modules/bindings": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.3.0.tgz", + "integrity": "sha512-DpLh5EzMR2kzvX1KIlVC0VkC3iZtHKTgdtZ0a3pglBZdaQFjt5S9g9xd1lE+YvXyfd6mtCeRnrUfOLYiTMlNSw==" + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dependencies": { + "balanced-match": "1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/chownr": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.1.tgz", + "integrity": "sha512-j38EvO5+LHX84jlo6h4UzmOwi0UgW61WRyPtJz4qaadK5eY3BTS5TY/S1Stc3Uk2lIM6TPevAlULiEJwie860g==" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" + }, + "node_modules/debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/follow-redirects": { + "version": "1.5.8", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.5.8.tgz", + "integrity": "sha512-sy1mXPmv7kLAMKW/8XofG7o9T+6gAjzdZK4AJF6ryqQYUa/hnzgiypoeUecZ53x7XiqKNEpNqLtS97MshW2nxg==", + "dependencies": { + "debug": "3.1.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/fs-minipass": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-1.2.5.tgz", + "integrity": "sha512-JhBl0skXjUPCFH7x6x61gQxrKyXsxB5gcgePLZCwfyCGGsTISMoIeObbrvVeP6Xmyaudw4TT43qV2Gz+iyd2oQ==", + "dependencies": { + "minipass": "2.3.4" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" + }, + "node_modules/glob": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.3.tgz", + "integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==", + "dependencies": { + "fs.realpath": "1.0.0", + "inflight": "1.0.6", + "inherits": "2.0.3", + "minimatch": "3.0.4", + "once": "1.4.0", + "path-is-absolute": "1.0.1" + }, + "engines": { + "node": "*" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "dependencies": { + "once": "1.4.0", + "wrappy": "1.0.2" + } + }, + "node_modules/inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" + }, + "node_modules/is-buffer": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==" + }, + "node_modules/lodash": { + "version": "4.17.11", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz", + "integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==" + }, + "node_modules/long": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz", + "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==" + }, + "node_modules/memoize": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/memoize/-/memoize-0.1.1.tgz", + "integrity": "sha1-0mWjRYvlzjvyVJmLMKmVq5FmiiQ=", + "dependencies": { + "tosource": "1.0.0" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "dependencies": { + "brace-expansion": "1.1.11" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "0.0.8", + "resolved": "http://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=" + }, + "node_modules/minipass": { + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-2.3.4.tgz", + "integrity": "sha512-mlouk1OHlaUE8Odt1drMtG1bAJA4ZA6B/ehysgV0LUIrDHdKgo1KorZq3pK0b/7Z7LJIQ12MNM6aC+Tn6lUZ5w==", + "dependencies": { + "safe-buffer": "5.1.2", + "yallist": "3.0.2" + } + }, + "node_modules/minizlib": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-1.1.0.tgz", + "integrity": "sha512-4T6Ur/GctZ27nHfpt9THOdRZNgyJ9FZchYO1ceg5S8Q3DNLCKYy44nCZzgCJgcvx2UM8czmqak5BCxJMrq37lA==", + "dependencies": { + "minipass": "2.3.4" + } + }, + "node_modules/mkdirp": { + "version": "0.5.1", + "resolved": "http://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "dependencies": { + "minimist": "0.0.8" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/mnist-data": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/mnist-data/-/mnist-data-1.2.6.tgz", + "integrity": "sha1-poZd4XCdCsCJp93uc5jCvX6EJAQ=", + "dependencies": { + "underscore": "1.9.1" + } + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + }, + "node_modules/node-remote-plot": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/node-remote-plot/-/node-remote-plot-1.2.0.tgz", + "integrity": "sha512-92hjrWiusikN/Eem+LSJ/gqmraJ6QIEUlk+ZRz12o8IkrqsFros5TPqxVHM530ahDZm4t6vm1KnIZFWgNZYWsQ==", + "dependencies": { + "axios": "0.18.0" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "dependencies": { + "wrappy": "1.0.2" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/progress": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.0.tgz", + "integrity": "sha1-ihvjZr+Pwj2yvSPxDG/pILQ4nR8=", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/protobufjs": { + "version": "6.8.8", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-6.8.8.tgz", + "integrity": "sha512-AAmHtD5pXgZfi7GMpllpO3q1Xw1OYldr+dMUlAnffGTAhqkg72WdmSY71uKBF/JuyiKs8psYbtKrhi0ASCD8qw==", + "hasInstallScript": true, + "dependencies": { + "@protobufjs/aspromise": "1.1.2", + "@protobufjs/base64": "1.1.2", + "@protobufjs/codegen": "2.0.4", + "@protobufjs/eventemitter": "1.1.0", + "@protobufjs/fetch": "1.1.0", + "@protobufjs/float": "1.0.2", + "@protobufjs/inquire": "1.1.0", + "@protobufjs/path": "1.1.2", + "@protobufjs/pool": "1.1.0", + "@protobufjs/utf8": "1.1.0", + "@types/long": "4.0.0", + "@types/node": "10.11.2", + "long": "4.0.0" + }, + "bin": { + "pbjs": "bin/pbjs", + "pbts": "bin/pbts" + } + }, + "node_modules/protobufjs/node_modules/@types/long": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.0.tgz", + "integrity": "sha512-1w52Nyx4Gq47uuu0EVcsHBxZFJgurQ+rTKS3qMHxR1GY2T8c2AJYd6vZoZ9q1rupaDjU0yT+Jc2XTyXkjeMA+Q==" + }, + "node_modules/rimraf": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.2.tgz", + "integrity": "sha512-lreewLK/BlghmxtfH36YYVg1i8IAce4TI7oao75I1g245+6BctqTVQiBP3YUJ9C6DQOXJmkYR9X9fCLtCOJc5w==", + "dependencies": { + "glob": "7.1.3" + }, + "bin": { + "rimraf": "bin.js" + } + }, + "node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "node_modules/seedrandom": { + "version": "2.4.4", + "resolved": "https://registry.npmjs.org/seedrandom/-/seedrandom-2.4.4.tgz", + "integrity": "sha512-9A+PDmgm+2du77B5i0Ip2cxOqqHjgNxnBgglxLcX78A2D6c2rTo61z4jnVABpF4cKeDMDG+cmXXvdnqse2VqMA==" + }, + "node_modules/shuffle-seed": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/shuffle-seed/-/shuffle-seed-1.1.6.tgz", + "integrity": "sha1-UzwSaDurO0+j6HUfxOViFGdEJgs=", + "dependencies": { + "seedrandom": "2.4.4" + } + }, + "node_modules/tar": { + "version": "4.4.6", + "resolved": "https://registry.npmjs.org/tar/-/tar-4.4.6.tgz", + "integrity": "sha512-tMkTnh9EdzxyfW+6GK6fCahagXsnYk6kE6S9Gr9pjVdys769+laCTbodXDhPAjzVtEBazRgP0gYqOjnk9dQzLg==", + "dependencies": { + "chownr": "1.1.1", + "fs-minipass": "1.2.5", + "minipass": "2.3.4", + "minizlib": "1.1.0", + "mkdirp": "0.5.1", + "safe-buffer": "5.1.2", + "yallist": "3.0.2" + }, + "engines": { + "node": ">=4.5" + } + }, + "node_modules/tosource": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/tosource/-/tosource-1.0.0.tgz", + "integrity": "sha1-QtiN0RZhi88A1hBt1URvNCeQL/E=", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/underscore": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.9.1.tgz", + "integrity": "sha512-5/4etnCkd9c8gwgowi5/om/mYO5ajCaOgdzj/oW+0eQV9WxKBDZw5+ycmKmeaTXjInS/W0BzpGLo2xR2aBwZdg==" + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" + }, + "node_modules/yallist": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.0.2.tgz", + "integrity": "sha1-hFK0u36Dx8GI2AQcGoN8dz1ti7k=" + } + }, "dependencies": { "@protobufjs/aspromise": { "version": "1.1.2", diff --git a/regressions/plot-old.png b/regressions/plot-old.png new file mode 100644 index 0000000..d84b3b7 Binary files /dev/null and b/regressions/plot-old.png differ diff --git a/regressions/plot.png b/regressions/plot.png new file mode 100644 index 0000000..2c0592d Binary files /dev/null and b/regressions/plot.png differ