diff --git a/CHANGELOG.md b/CHANGELOG.md index c8317c9b..781ea725 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,15 @@ +# [1.22.0](https://github.com/javascriptdata/scikit.js/compare/v1.21.0...v1.22.0) (2022-05-18) + + +### Features + +* added back in logistic regression tests ([dc2ec4a](https://github.com/javascriptdata/scikit.js/commit/dc2ec4a5caf7f2eb95e9f91a730a28af6914a144)) +* first pass at removing tensorflow from bundle ([7562da2](https://github.com/javascriptdata/scikit.js/commit/7562da244513f43e8e9d4ccbfb490c80f81f2704)) +* more tests moved over ([76509d8](https://github.com/javascriptdata/scikit.js/commit/76509d8cc34628cf8f9e1ec2eeb076ecc332e191)) +* removed hard dependency on tensorflow ([0f2736e](https://github.com/javascriptdata/scikit.js/commit/0f2736ef7abdfa47f535b396cc9cdac2ee40de47)) +* removed unneeded build steps ([d3814ca](https://github.com/javascriptdata/scikit.js/commit/d3814caff4e204a0e919977ffec3686f69e28d24)) +* updated serialize / deserialize to avoid tfjs error ([1bf508d](https://github.com/javascriptdata/scikit.js/commit/1bf508dd2e4baea5867c81183fdb609adde1938a)) + # [1.21.0](https://github.com/javascriptdata/scikit.js/compare/v1.20.0...v1.21.0) (2022-05-08) diff --git a/build/browserReplacer.js b/build/browserReplacer.js deleted file mode 100644 index e4a8865a..00000000 --- a/build/browserReplacer.js +++ /dev/null @@ -1,10 +0,0 @@ -exports.default = function exampleReplacer({ - orig - // file -}) { - if (orig.includes('/shared-node/globals')) - return orig.replace('/shared-node/globals', '/shared-esm/globals') - else if (orig.includes('/shared/globals')) - return orig.replace('/shared/globals', '/shared-esm/globals') - return orig -} diff --git a/build/nodeGpuReplacer.js b/build/nodeGpuReplacer.js deleted file mode 100644 index 86a35e92..00000000 --- a/build/nodeGpuReplacer.js +++ /dev/null @@ -1,7 +0,0 @@ -exports.default = function exampleReplacer({ - orig - // file -}) { - if (orig.includes('/shared/globals')) return orig.replace('/shared/globals', '/shared/globals-node-gpu') - return orig; -} diff --git a/build/nodeReplacer.js b/build/nodeReplacer.js deleted file mode 100644 index 1c8e15bb..00000000 --- a/build/nodeReplacer.js +++ /dev/null @@ -1,8 +0,0 @@ -exports.default = function exampleReplacer({ - orig - // file -}) { - if (orig.includes('/shared/globals')) - return orig.replace('/shared/globals', '/shared-node/globals') - return orig -} diff --git a/package-lock.json b/package-lock.json index 83c3e9fd..2d1ffc2c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,17 +1,17 @@ { "name": "scikitjs", - "version": "1.21.0", + "version": "1.22.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "scikitjs", - "version": "1.21.0", + "version": "1.22.0", "hasInstallScript": true, "license": "ISC", "dependencies": { - "@tensorflow/tfjs": "^3.16.0", - "@tensorflow/tfjs-node": "^3.16.0", + "@tensorflow/tfjs-core": "^3.16.0", + "@tensorflow/tfjs-layers": "^3.16.0", "base64-arraybuffer": "^1.0.2", "lodash": "^4.17.21", "mathjs": "^10.0.0", @@ -31,6 +31,8 @@ "@semantic-release/git": "9.0.0", "@semantic-release/npm": "^7.1.0", "@semantic-release/release-notes-generator": "9.0.3", + "@tensorflow/tfjs": "^3.16.0", + "@tensorflow/tfjs-node": "^3.16.0", "@types/chai": "^4.2.22", "@types/jest": "^27.4.0", "@types/lodash": "^4.14.177", @@ -2580,6 +2582,7 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.4.tgz", "integrity": "sha512-M669Qo4nRT7iDmQEjQYC7RU8Z6dpz9UmSbkJ1OFEja3uevCdLKh7IZZki7L1TZj02kRyl82snXFY8QqkyfowrQ==", + "dev": true, "dependencies": { "detect-libc": "^1.0.3", "https-proxy-agent": "^5.0.0", @@ -2599,6 +2602,7 @@ "version": "6.0.2", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "dev": true, "dependencies": { "debug": "4" }, @@ -2610,6 +2614,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "dev": true, "engines": { "node": ">=10" } @@ -2618,6 +2623,7 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "dev": true, "dependencies": { "minipass": "^3.0.0" }, @@ -2629,6 +2635,7 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz", "integrity": "sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA==", + "dev": true, "dependencies": { "agent-base": "6", "debug": "4" @@ -2641,6 +2648,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "dev": true, "dependencies": { "semver": "^6.0.0" }, @@ -2655,6 +2663,7 @@ "version": "6.3.0", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true, "bin": { "semver": "bin/semver.js" } @@ -2663,6 +2672,7 @@ "version": "3.1.6", "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.1.6.tgz", "integrity": "sha512-rty5kpw9/z8SX9dmxblFA6edItUmwJgMeYDZRrwlIVN27i8gysGbznJwUggw2V/FVqFSDdWy040ZPS811DYAqQ==", + "dev": true, "dependencies": { "yallist": "^4.0.0" }, @@ -2674,6 +2684,7 @@ "version": "2.1.2", "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "dev": true, "dependencies": { "minipass": "^3.0.0", "yallist": "^4.0.0" @@ -2686,6 +2697,7 @@ "version": "6.1.11", "resolved": "https://registry.npmjs.org/tar/-/tar-6.1.11.tgz", "integrity": "sha512-an/KZQzQUkZCkuoAA64hM92X0Urb6VpRhAFllDzz44U2mcD5scmT3zBc4VgVpkugF580+DQn8eAFSyoQt0tznA==", + "dev": true, "dependencies": { "chownr": "^2.0.0", "fs-minipass": "^2.0.0", @@ -3408,6 +3420,7 @@ "version": "3.16.0", "resolved": "https://registry.npmjs.org/@tensorflow/tfjs/-/tfjs-3.16.0.tgz", "integrity": "sha512-RgsNaG/+krMMiKiG/uGPAjWM6KpT+z2wWQ2aLYSTTuQqQIksFJSUzhZncWAXykHCKgg64Pr14wyAT6gLV1amng==", + "dev": true, "dependencies": { "@tensorflow/tfjs-backend-cpu": "3.16.0", "@tensorflow/tfjs-backend-webgl": "3.16.0", @@ -3429,6 +3442,7 @@ "version": "3.16.0", "resolved": "https://registry.npmjs.org/@tensorflow/tfjs-backend-cpu/-/tfjs-backend-cpu-3.16.0.tgz", "integrity": "sha512-8hpk/FSbx0TGV58E3qIOqNYrNXYGAgZwap3i/7A+rDuKZYtYb+EX9a+aEFBomeMSetp4xqaEYBuhOOImw4/CaA==", + "dev": true, "dependencies": { "@types/seedrandom": "2.4.27", "seedrandom": "2.4.3" @@ -3443,12 +3457,14 @@ "node_modules/@tensorflow/tfjs-backend-cpu/node_modules/seedrandom": { "version": "2.4.3", "resolved": "https://registry.npmjs.org/seedrandom/-/seedrandom-2.4.3.tgz", - "integrity": "sha1-JDhQTa0zkXMUv/GKxNeU8W1qrsw=" + "integrity": "sha1-JDhQTa0zkXMUv/GKxNeU8W1qrsw=", + "dev": true }, "node_modules/@tensorflow/tfjs-backend-webgl": { "version": "3.16.0", "resolved": "https://registry.npmjs.org/@tensorflow/tfjs-backend-webgl/-/tfjs-backend-webgl-3.16.0.tgz", "integrity": "sha512-PrZ4//pbsP5DCz25huC3YYG6bq4+KotepPrt81pA6zCVE401qPe5CvzG/5vq0/GjVqB8uNtR1BdRL0Yonu+Urw==", + "dev": true, "dependencies": { "@tensorflow/tfjs-backend-cpu": "3.16.0", "@types/offscreencanvas": "~2019.3.0", @@ -3467,12 +3483,14 @@ "node_modules/@tensorflow/tfjs-backend-webgl/node_modules/seedrandom": { "version": "2.4.3", "resolved": "https://registry.npmjs.org/seedrandom/-/seedrandom-2.4.3.tgz", - "integrity": "sha1-JDhQTa0zkXMUv/GKxNeU8W1qrsw=" + "integrity": "sha1-JDhQTa0zkXMUv/GKxNeU8W1qrsw=", + "dev": true }, "node_modules/@tensorflow/tfjs-converter": { "version": "3.16.0", "resolved": "https://registry.npmjs.org/@tensorflow/tfjs-converter/-/tfjs-converter-3.16.0.tgz", "integrity": "sha512-qPzI0BvPa//YTyk704RhlshMjM++FWaery1ns/lhGJBXD50HQazUpjP+bzJ4OIOmPSKB85BOQuzZo58JPpZSug==", + "dev": true, "peerDependencies": { "@tensorflow/tfjs-core": "3.16.0" } @@ -3511,6 +3529,7 @@ "version": "3.16.0", "resolved": "https://registry.npmjs.org/@tensorflow/tfjs-node/-/tfjs-node-3.16.0.tgz", "integrity": "sha512-v1hS5F5gnhit7haDer75qnE8OnrRkhpcYktRKJkhIvvRc3XviuZMsKcLaqUp+lSOhb438cgJdzbENrB445FSoA==", + "dev": true, "hasInstallScript": true, "dependencies": { "@mapbox/node-pre-gyp": "1.0.4", @@ -3528,6 +3547,7 @@ }, "node_modules/@tensorflow/tfjs-node/node_modules/rimraf": { "version": "2.7.1", + "dev": true, "license": "ISC", "dependencies": { "glob": "^7.1.3" @@ -3540,6 +3560,7 @@ "version": "3.16.0", "resolved": "https://registry.npmjs.org/@tensorflow/tfjs-data/-/tfjs-data-3.16.0.tgz", "integrity": "sha512-VuHG0b1obO5vQsnl4atDYxroMmnKqThYnXiHccr+KNkNcZbo4MgDxeRczTsRqIDTnA0cMYn3WQjgYaYX3NxftA==", + "dev": true, "dependencies": { "@types/node-fetch": "^2.1.2", "node-fetch": "~2.6.1" @@ -3553,6 +3574,7 @@ "version": "1.0.10", "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, "dependencies": { "sprintf-js": "~1.0.2" } @@ -3561,12 +3583,14 @@ "version": "2.4.4", "resolved": "https://registry.npmjs.org/seedrandom/-/seedrandom-2.4.4.tgz", "integrity": "sha512-9A+PDmgm+2du77B5i0Ip2cxOqqHjgNxnBgglxLcX78A2D6c2rTo61z4jnVABpF4cKeDMDG+cmXXvdnqse2VqMA==", + "dev": true, "peer": true }, "node_modules/@tensorflow/tfjs/node_modules/yargs": { "version": "16.2.0", "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "dev": true, "dependencies": { "cliui": "^7.0.2", "escalade": "^3.1.1", @@ -3774,12 +3798,14 @@ }, "node_modules/@types/node": { "version": "16.11.7", + "dev": true, "license": "MIT" }, "node_modules/@types/node-fetch": { "version": "2.6.1", "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.1.tgz", "integrity": "sha512-oMqjURCaxoSIsHSr1E47QHzbmzNR5rK8McHuNb11BOM9cHcIK3Avy0s/b2JlXHoQGTYS3NsvWzV1M0iK7l0wbA==", + "dev": true, "dependencies": { "@types/node": "*", "form-data": "^3.0.0" @@ -3789,6 +3815,7 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.1.tgz", "integrity": "sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg==", + "dev": true, "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", @@ -3853,7 +3880,8 @@ "node_modules/@types/webgl2": { "version": "0.0.6", "resolved": "https://registry.npmjs.org/@types/webgl2/-/webgl2-0.0.6.tgz", - "integrity": "sha512-50GQhDVTq/herLMiqSQkdtRu+d5q/cWHn4VvKJtrj4DJAjo1MNkWYa2MA41BaBO1q1HgsUjuQvEOk0QHvlnAaQ==" + "integrity": "sha512-50GQhDVTq/herLMiqSQkdtRu+d5q/cWHn4VvKJtrj4DJAjo1MNkWYa2MA41BaBO1q1HgsUjuQvEOk0QHvlnAaQ==", + "dev": true }, "node_modules/@types/yargs": { "version": "16.0.4", @@ -4215,7 +4243,8 @@ "node_modules/abbrev": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", - "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==" + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "dev": true }, "node_modules/accepts": { "version": "1.3.8", @@ -4317,12 +4346,14 @@ "version": "0.5.9", "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.9.tgz", "integrity": "sha512-s+3fXLkeeLjZ2kLjCBwQufpI5fuN+kIGBxu6530nVQZGVol0d7Y/M88/xw9HGGUcJjKf8LutN3VPRUBq6N7Ajg==", + "dev": true, "engines": { "node": ">=6.0" } }, "node_modules/agent-base": { "version": "4.3.0", + "dev": true, "license": "MIT", "dependencies": { "es6-promisify": "^5.0.0" @@ -4404,6 +4435,7 @@ }, "node_modules/ansi-regex": { "version": "5.0.1", + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -4411,6 +4443,7 @@ }, "node_modules/ansi-styles": { "version": "4.3.0", + "dev": true, "license": "MIT", "dependencies": { "color-convert": "^2.0.1" @@ -4443,12 +4476,14 @@ "node_modules/aproba": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", - "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==" + "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==", + "dev": true }, "node_modules/are-we-there-yet": { "version": "1.1.7", "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.7.tgz", "integrity": "sha512-nxwy40TuMiUGqMyRHgCSWZ9FM4VAoRP4xUYSTv5ImRog+h9yISPbVH7H8fASCIzYn9wlEv4zvFL7uKDMCFQm3g==", + "dev": true, "dependencies": { "delegates": "^1.0.0", "readable-stream": "^2.0.6" @@ -4458,6 +4493,7 @@ "version": "2.3.7", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "dev": true, "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", @@ -4471,12 +4507,14 @@ "node_modules/are-we-there-yet/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==" + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true }, "node_modules/are-we-there-yet/node_modules/string_decoder": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, "dependencies": { "safe-buffer": "~5.1.0" } @@ -4544,6 +4582,7 @@ }, "node_modules/asynckit": { "version": "0.4.0", + "dev": true, "license": "MIT" }, "node_modules/at-least-node": { @@ -4756,6 +4795,7 @@ }, "node_modules/balanced-match": { "version": "1.0.2", + "dev": true, "license": "MIT" }, "node_modules/base64-arraybuffer": { @@ -4864,6 +4904,7 @@ }, "node_modules/brace-expansion": { "version": "1.1.11", + "dev": true, "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", @@ -5051,6 +5092,7 @@ }, "node_modules/chalk": { "version": "4.1.2", + "dev": true, "license": "MIT", "dependencies": { "ansi-styles": "^4.1.0", @@ -5107,7 +5149,8 @@ "node_modules/chownr": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", - "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==" + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "dev": true }, "node_modules/chrome-trace-event": { "version": "1.0.3", @@ -5157,6 +5200,7 @@ }, "node_modules/cliui": { "version": "7.0.4", + "dev": true, "license": "ISC", "dependencies": { "string-width": "^4.2.0", @@ -5192,6 +5236,7 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", + "dev": true, "engines": { "node": ">=0.10.0" } @@ -5214,6 +5259,7 @@ }, "node_modules/color-convert": { "version": "2.0.1", + "dev": true, "license": "MIT", "dependencies": { "color-name": "~1.1.4" @@ -5224,6 +5270,7 @@ }, "node_modules/color-name": { "version": "1.1.4", + "dev": true, "license": "MIT" }, "node_modules/colors": { @@ -5238,6 +5285,7 @@ }, "node_modules/combined-stream": { "version": "1.0.8", + "dev": true, "license": "MIT", "dependencies": { "delayed-stream": "~1.0.0" @@ -5285,6 +5333,7 @@ }, "node_modules/concat-map": { "version": "0.0.1", + "dev": true, "license": "MIT" }, "node_modules/connect": { @@ -5320,7 +5369,8 @@ "node_modules/console-control-strings": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", - "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=" + "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=", + "dev": true }, "node_modules/content-type": { "version": "1.0.4", @@ -5447,6 +5497,7 @@ "version": "3.20.2", "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.20.2.tgz", "integrity": "sha512-nuqhq11DcOAbFBV4zCbKeGbKQsUDRqTX0oqx7AttUBuqe3h20ixsE039QHelbL6P4h+9kytVqyEtyZ6gsiwEYw==", + "dev": true, "hasInstallScript": true, "funding": { "type": "opencollective", @@ -5476,6 +5527,7 @@ }, "node_modules/core-util-is": { "version": "1.0.2", + "dev": true, "license": "MIT" }, "node_modules/cors": { @@ -5861,6 +5913,7 @@ "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, "dependencies": { "ms": "2.1.2" }, @@ -5994,6 +6047,7 @@ }, "node_modules/delayed-stream": { "version": "1.0.0", + "dev": true, "license": "MIT", "engines": { "node": ">=0.4.0" @@ -6002,7 +6056,8 @@ "node_modules/delegates": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", - "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=" + "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=", + "dev": true }, "node_modules/depd": { "version": "2.0.0", @@ -6041,6 +6096,7 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", "integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=", + "dev": true, "bin": { "detect-libc": "bin/detect-libc.js" }, @@ -6221,6 +6277,7 @@ }, "node_modules/emoji-regex": { "version": "8.0.0", + "dev": true, "license": "MIT" }, "node_modules/encodeurl": { @@ -6360,10 +6417,12 @@ }, "node_modules/es6-promise": { "version": "4.2.8", + "dev": true, "license": "MIT" }, "node_modules/es6-promisify": { "version": "5.0.0", + "dev": true, "license": "MIT", "dependencies": { "es6-promise": "^4.0.3" @@ -6726,6 +6785,7 @@ }, "node_modules/escalade": { "version": "3.1.1", + "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -7486,6 +7546,7 @@ "version": "1.2.7", "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-1.2.7.tgz", "integrity": "sha512-GWSSJGFy4e9GUeCcbIkED+bgAoFyj7XF1mV8rma3QW4NIqX9Kyx79N/PF61H5udOV3aY1IaMLs6pGbH71nlCTA==", + "dev": true, "dependencies": { "minipass": "^2.6.0" } @@ -7497,6 +7558,7 @@ }, "node_modules/fs.realpath": { "version": "1.0.0", + "dev": true, "license": "ISC" }, "node_modules/fsevents": { @@ -7525,6 +7587,7 @@ "version": "2.7.4", "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz", "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=", + "dev": true, "dependencies": { "aproba": "^1.0.3", "console-control-strings": "^1.0.0", @@ -7540,6 +7603,7 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "dev": true, "engines": { "node": ">=0.10.0" } @@ -7548,6 +7612,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", + "dev": true, "dependencies": { "number-is-nan": "^1.0.0" }, @@ -7559,6 +7624,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", + "dev": true, "dependencies": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -7572,6 +7638,7 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "dev": true, "dependencies": { "ansi-regex": "^2.0.0" }, @@ -7589,6 +7656,7 @@ }, "node_modules/get-caller-file": { "version": "2.0.5", + "dev": true, "license": "ISC", "engines": { "node": "6.* || 8.* || >= 10.*" @@ -7727,6 +7795,7 @@ }, "node_modules/glob": { "version": "7.2.0", + "dev": true, "license": "ISC", "dependencies": { "fs.realpath": "^1.0.0", @@ -7861,6 +7930,7 @@ }, "node_modules/google-protobuf": { "version": "3.19.1", + "dev": true, "license": "BSD-3-Clause" }, "node_modules/graceful-fs": { @@ -7940,6 +8010,7 @@ }, "node_modules/has-flag": { "version": "4.0.0", + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -7959,7 +8030,8 @@ "node_modules/has-unicode": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", - "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=" + "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=", + "dev": true }, "node_modules/homedir-polyfill": { "version": "1.0.3", @@ -8091,6 +8163,7 @@ }, "node_modules/https-proxy-agent": { "version": "2.2.4", + "dev": true, "license": "MIT", "dependencies": { "agent-base": "^4.3.0", @@ -8102,6 +8175,7 @@ }, "node_modules/https-proxy-agent/node_modules/debug": { "version": "3.2.7", + "dev": true, "license": "MIT", "dependencies": { "ms": "^2.1.1" @@ -8289,6 +8363,7 @@ }, "node_modules/inflight": { "version": "1.0.6", + "dev": true, "license": "ISC", "dependencies": { "once": "^1.3.0", @@ -8297,6 +8372,7 @@ }, "node_modules/inherits": { "version": "2.0.4", + "dev": true, "license": "ISC" }, "node_modules/ini": { @@ -8357,6 +8433,7 @@ }, "node_modules/is-fullwidth-code-point": { "version": "3.0.0", + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -8479,6 +8556,7 @@ }, "node_modules/isarray": { "version": "1.0.0", + "dev": true, "license": "MIT" }, "node_modules/isbinaryfile": { @@ -10080,6 +10158,7 @@ }, "node_modules/lru-cache": { "version": "6.0.0", + "dev": true, "license": "ISC", "dependencies": { "yallist": "^4.0.0" @@ -10295,6 +10374,7 @@ }, "node_modules/mime-db": { "version": "1.51.0", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.6" @@ -10302,6 +10382,7 @@ }, "node_modules/mime-types": { "version": "2.1.34", + "dev": true, "license": "MIT", "dependencies": { "mime-db": "1.51.0" @@ -10328,6 +10409,7 @@ }, "node_modules/minimatch": { "version": "3.0.4", + "dev": true, "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" @@ -10339,7 +10421,8 @@ "node_modules/minimist": { "version": "1.2.6", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz", - "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==" + "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==", + "dev": true }, "node_modules/minimist-options": { "version": "4.1.0", @@ -10358,6 +10441,7 @@ "version": "2.9.0", "resolved": "https://registry.npmjs.org/minipass/-/minipass-2.9.0.tgz", "integrity": "sha512-wxfUjg9WebH+CUDX/CdbRlh5SmfZiy/hpkxaRI16Y9W56Pa75sWgd/rvFilSgrauD9NyFymP/+JFV3KwzIsJeg==", + "dev": true, "dependencies": { "safe-buffer": "^5.1.2", "yallist": "^3.0.0" @@ -10366,18 +10450,21 @@ "node_modules/minipass/node_modules/yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==" + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true }, "node_modules/minizlib": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-1.3.3.tgz", "integrity": "sha512-6ZYMOEnmVsdCeTJVE0W9ZD+pVnE8h9Hma/iOwwRDsdQoePpoX56/8B6z3P9VNwppJuBKNRuFDRNRqRWexT9G9Q==", + "dev": true, "dependencies": { "minipass": "^2.9.0" } }, "node_modules/mkdirp": { "version": "1.0.4", + "dev": true, "license": "MIT", "bin": { "mkdirp": "bin/cmd.js" @@ -10397,6 +10484,7 @@ }, "node_modules/ms": { "version": "2.1.2", + "dev": true, "license": "MIT" }, "node_modules/mylas": { @@ -10481,6 +10569,7 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", + "dev": true, "dependencies": { "abbrev": "1" }, @@ -13448,6 +13537,7 @@ "version": "4.1.2", "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz", "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==", + "dev": true, "dependencies": { "are-we-there-yet": "~1.1.2", "console-control-strings": "~1.1.0", @@ -13459,6 +13549,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", + "dev": true, "engines": { "node": ">=0.10.0" } @@ -13479,6 +13570,7 @@ }, "node_modules/object-assign": { "version": "4.1.1", + "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -13532,6 +13624,7 @@ }, "node_modules/once": { "version": "1.4.0", + "dev": true, "license": "ISC", "dependencies": { "wrappy": "1" @@ -13754,6 +13847,7 @@ }, "node_modules/path-is-absolute": { "version": "1.0.1", + "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -14029,10 +14123,12 @@ }, "node_modules/process-nextick-args": { "version": "2.0.1", + "dev": true, "license": "MIT" }, "node_modules/progress": { "version": "2.0.3", + "dev": true, "license": "MIT", "engines": { "node": ">=0.4.0" @@ -14486,6 +14582,7 @@ }, "node_modules/require-directory": { "version": "2.1.1", + "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -14615,6 +14712,7 @@ }, "node_modules/rimraf": { "version": "3.0.2", + "dev": true, "license": "ISC", "dependencies": { "glob": "^7.1.3" @@ -14650,6 +14748,7 @@ }, "node_modules/safe-buffer": { "version": "5.2.1", + "dev": true, "funding": [ { "type": "github", @@ -14805,6 +14904,7 @@ }, "node_modules/semver": { "version": "7.3.5", + "dev": true, "license": "ISC", "dependencies": { "lru-cache": "^6.0.0" @@ -14861,7 +14961,8 @@ "node_modules/set-blocking": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", - "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=" + "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=", + "dev": true }, "node_modules/setprototypeof": { "version": "1.2.0", @@ -14925,6 +15026,7 @@ }, "node_modules/signal-exit": { "version": "3.0.6", + "dev": true, "license": "ISC" }, "node_modules/signale": { @@ -15180,6 +15282,7 @@ }, "node_modules/sprintf-js": { "version": "1.0.3", + "dev": true, "license": "BSD-3-Clause" }, "node_modules/ssf": { @@ -15339,6 +15442,7 @@ }, "node_modules/string-width": { "version": "4.2.3", + "dev": true, "license": "MIT", "dependencies": { "emoji-regex": "^8.0.0", @@ -15351,6 +15455,7 @@ }, "node_modules/strip-ansi": { "version": "6.0.1", + "dev": true, "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" @@ -15400,6 +15505,7 @@ }, "node_modules/supports-color": { "version": "7.2.0", + "dev": true, "license": "MIT", "dependencies": { "has-flag": "^4.0.0" @@ -15479,6 +15585,7 @@ "version": "4.4.19", "resolved": "https://registry.npmjs.org/tar/-/tar-4.4.19.tgz", "integrity": "sha512-a20gEsvHnWe0ygBY8JbxoM4w3SJdhc7ZAuxkLqh+nvNQN2IOt0B5lLgM490X5Hl8FF0dl0tOf2ewFYAlIFgzVA==", + "dev": true, "dependencies": { "chownr": "^1.1.4", "fs-minipass": "^1.2.7", @@ -15496,6 +15603,7 @@ "version": "0.5.5", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", + "dev": true, "dependencies": { "minimist": "^1.2.5" }, @@ -15506,7 +15614,8 @@ "node_modules/tar/node_modules/yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==" + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true }, "node_modules/temp-dir": { "version": "2.0.0", @@ -16143,6 +16252,7 @@ }, "node_modules/util-deprecate": { "version": "1.0.2", + "dev": true, "license": "MIT" }, "node_modules/utils-merge": { @@ -16405,6 +16515,7 @@ "version": "1.1.5", "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", + "dev": true, "dependencies": { "string-width": "^1.0.2 || 2 || 3 || 4" } @@ -16447,6 +16558,7 @@ }, "node_modules/wrap-ansi": { "version": "7.0.0", + "dev": true, "license": "MIT", "dependencies": { "ansi-styles": "^4.0.0", @@ -16462,6 +16574,7 @@ }, "node_modules/wrappy": { "version": "1.0.2", + "dev": true, "license": "ISC" }, "node_modules/write-file-atomic": { @@ -16540,6 +16653,7 @@ }, "node_modules/y18n": { "version": "5.0.8", + "dev": true, "license": "ISC", "engines": { "node": ">=10" @@ -16547,6 +16661,7 @@ }, "node_modules/yallist": { "version": "4.0.0", + "dev": true, "license": "ISC" }, "node_modules/yaml": { @@ -16576,6 +16691,7 @@ }, "node_modules/yargs-parser": { "version": "20.2.9", + "dev": true, "license": "ISC", "engines": { "node": ">=10" @@ -18286,6 +18402,7 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.4.tgz", "integrity": "sha512-M669Qo4nRT7iDmQEjQYC7RU8Z6dpz9UmSbkJ1OFEja3uevCdLKh7IZZki7L1TZj02kRyl82snXFY8QqkyfowrQ==", + "dev": true, "requires": { "detect-libc": "^1.0.3", "https-proxy-agent": "^5.0.0", @@ -18302,6 +18419,7 @@ "version": "6.0.2", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "dev": true, "requires": { "debug": "4" } @@ -18309,12 +18427,14 @@ "chownr": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", - "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==" + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "dev": true }, "fs-minipass": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "dev": true, "requires": { "minipass": "^3.0.0" } @@ -18323,6 +18443,7 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz", "integrity": "sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA==", + "dev": true, "requires": { "agent-base": "6", "debug": "4" @@ -18332,6 +18453,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "dev": true, "requires": { "semver": "^6.0.0" }, @@ -18339,7 +18461,8 @@ "semver": { "version": "6.3.0", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true } } }, @@ -18347,6 +18470,7 @@ "version": "3.1.6", "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.1.6.tgz", "integrity": "sha512-rty5kpw9/z8SX9dmxblFA6edItUmwJgMeYDZRrwlIVN27i8gysGbznJwUggw2V/FVqFSDdWy040ZPS811DYAqQ==", + "dev": true, "requires": { "yallist": "^4.0.0" } @@ -18355,6 +18479,7 @@ "version": "2.1.2", "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "dev": true, "requires": { "minipass": "^3.0.0", "yallist": "^4.0.0" @@ -18364,6 +18489,7 @@ "version": "6.1.11", "resolved": "https://registry.npmjs.org/tar/-/tar-6.1.11.tgz", "integrity": "sha512-an/KZQzQUkZCkuoAA64hM92X0Urb6VpRhAFllDzz44U2mcD5scmT3zBc4VgVpkugF580+DQn8eAFSyoQt0tznA==", + "dev": true, "requires": { "chownr": "^2.0.0", "fs-minipass": "^2.0.0", @@ -18878,6 +19004,7 @@ "version": "3.16.0", "resolved": "https://registry.npmjs.org/@tensorflow/tfjs/-/tfjs-3.16.0.tgz", "integrity": "sha512-RgsNaG/+krMMiKiG/uGPAjWM6KpT+z2wWQ2aLYSTTuQqQIksFJSUzhZncWAXykHCKgg64Pr14wyAT6gLV1amng==", + "dev": true, "requires": { "@tensorflow/tfjs-backend-cpu": "3.16.0", "@tensorflow/tfjs-backend-webgl": "3.16.0", @@ -18896,6 +19023,7 @@ "version": "3.16.0", "resolved": "https://registry.npmjs.org/@tensorflow/tfjs-data/-/tfjs-data-3.16.0.tgz", "integrity": "sha512-VuHG0b1obO5vQsnl4atDYxroMmnKqThYnXiHccr+KNkNcZbo4MgDxeRczTsRqIDTnA0cMYn3WQjgYaYX3NxftA==", + "dev": true, "requires": { "@types/node-fetch": "^2.1.2", "node-fetch": "~2.6.1" @@ -18905,6 +19033,7 @@ "version": "1.0.10", "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, "requires": { "sprintf-js": "~1.0.2" } @@ -18913,12 +19042,14 @@ "version": "2.4.4", "resolved": "https://registry.npmjs.org/seedrandom/-/seedrandom-2.4.4.tgz", "integrity": "sha512-9A+PDmgm+2du77B5i0Ip2cxOqqHjgNxnBgglxLcX78A2D6c2rTo61z4jnVABpF4cKeDMDG+cmXXvdnqse2VqMA==", + "dev": true, "peer": true }, "yargs": { "version": "16.2.0", "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "dev": true, "requires": { "cliui": "^7.0.2", "escalade": "^3.1.1", @@ -18935,6 +19066,7 @@ "version": "3.16.0", "resolved": "https://registry.npmjs.org/@tensorflow/tfjs-backend-cpu/-/tfjs-backend-cpu-3.16.0.tgz", "integrity": "sha512-8hpk/FSbx0TGV58E3qIOqNYrNXYGAgZwap3i/7A+rDuKZYtYb+EX9a+aEFBomeMSetp4xqaEYBuhOOImw4/CaA==", + "dev": true, "requires": { "@types/seedrandom": "2.4.27", "seedrandom": "2.4.3" @@ -18943,7 +19075,8 @@ "seedrandom": { "version": "2.4.3", "resolved": "https://registry.npmjs.org/seedrandom/-/seedrandom-2.4.3.tgz", - "integrity": "sha1-JDhQTa0zkXMUv/GKxNeU8W1qrsw=" + "integrity": "sha1-JDhQTa0zkXMUv/GKxNeU8W1qrsw=", + "dev": true } } }, @@ -18951,6 +19084,7 @@ "version": "3.16.0", "resolved": "https://registry.npmjs.org/@tensorflow/tfjs-backend-webgl/-/tfjs-backend-webgl-3.16.0.tgz", "integrity": "sha512-PrZ4//pbsP5DCz25huC3YYG6bq4+KotepPrt81pA6zCVE401qPe5CvzG/5vq0/GjVqB8uNtR1BdRL0Yonu+Urw==", + "dev": true, "requires": { "@tensorflow/tfjs-backend-cpu": "3.16.0", "@types/offscreencanvas": "~2019.3.0", @@ -18963,7 +19097,8 @@ "seedrandom": { "version": "2.4.3", "resolved": "https://registry.npmjs.org/seedrandom/-/seedrandom-2.4.3.tgz", - "integrity": "sha1-JDhQTa0zkXMUv/GKxNeU8W1qrsw=" + "integrity": "sha1-JDhQTa0zkXMUv/GKxNeU8W1qrsw=", + "dev": true } } }, @@ -18971,6 +19106,7 @@ "version": "3.16.0", "resolved": "https://registry.npmjs.org/@tensorflow/tfjs-converter/-/tfjs-converter-3.16.0.tgz", "integrity": "sha512-qPzI0BvPa//YTyk704RhlshMjM++FWaery1ns/lhGJBXD50HQazUpjP+bzJ4OIOmPSKB85BOQuzZo58JPpZSug==", + "dev": true, "requires": {} }, "@tensorflow/tfjs-core": { @@ -19004,6 +19140,7 @@ "version": "3.16.0", "resolved": "https://registry.npmjs.org/@tensorflow/tfjs-node/-/tfjs-node-3.16.0.tgz", "integrity": "sha512-v1hS5F5gnhit7haDer75qnE8OnrRkhpcYktRKJkhIvvRc3XviuZMsKcLaqUp+lSOhb438cgJdzbENrB445FSoA==", + "dev": true, "requires": { "@mapbox/node-pre-gyp": "1.0.4", "@tensorflow/tfjs": "3.16.0", @@ -19017,6 +19154,7 @@ "dependencies": { "rimraf": { "version": "2.7.1", + "dev": true, "requires": { "glob": "^7.1.3" } @@ -19204,12 +19342,14 @@ "dev": true }, "@types/node": { - "version": "16.11.7" + "version": "16.11.7", + "dev": true }, "@types/node-fetch": { "version": "2.6.1", "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.1.tgz", "integrity": "sha512-oMqjURCaxoSIsHSr1E47QHzbmzNR5rK8McHuNb11BOM9cHcIK3Avy0s/b2JlXHoQGTYS3NsvWzV1M0iK7l0wbA==", + "dev": true, "requires": { "@types/node": "*", "form-data": "^3.0.0" @@ -19219,6 +19359,7 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.1.tgz", "integrity": "sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg==", + "dev": true, "requires": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", @@ -19279,7 +19420,8 @@ "@types/webgl2": { "version": "0.0.6", "resolved": "https://registry.npmjs.org/@types/webgl2/-/webgl2-0.0.6.tgz", - "integrity": "sha512-50GQhDVTq/herLMiqSQkdtRu+d5q/cWHn4VvKJtrj4DJAjo1MNkWYa2MA41BaBO1q1HgsUjuQvEOk0QHvlnAaQ==" + "integrity": "sha512-50GQhDVTq/herLMiqSQkdtRu+d5q/cWHn4VvKJtrj4DJAjo1MNkWYa2MA41BaBO1q1HgsUjuQvEOk0QHvlnAaQ==", + "dev": true }, "@types/yargs": { "version": "16.0.4", @@ -19552,7 +19694,8 @@ "abbrev": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", - "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==" + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "dev": true }, "accepts": { "version": "1.3.8", @@ -19621,10 +19764,12 @@ "adm-zip": { "version": "0.5.9", "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.9.tgz", - "integrity": "sha512-s+3fXLkeeLjZ2kLjCBwQufpI5fuN+kIGBxu6530nVQZGVol0d7Y/M88/xw9HGGUcJjKf8LutN3VPRUBq6N7Ajg==" + "integrity": "sha512-s+3fXLkeeLjZ2kLjCBwQufpI5fuN+kIGBxu6530nVQZGVol0d7Y/M88/xw9HGGUcJjKf8LutN3VPRUBq6N7Ajg==", + "dev": true }, "agent-base": { "version": "4.3.0", + "dev": true, "requires": { "es6-promisify": "^5.0.0" } @@ -19676,10 +19821,12 @@ } }, "ansi-regex": { - "version": "5.0.1" + "version": "5.0.1", + "dev": true }, "ansi-styles": { "version": "4.3.0", + "dev": true, "requires": { "color-convert": "^2.0.1" } @@ -19701,12 +19848,14 @@ "aproba": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", - "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==" + "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==", + "dev": true }, "are-we-there-yet": { "version": "1.1.7", "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.7.tgz", "integrity": "sha512-nxwy40TuMiUGqMyRHgCSWZ9FM4VAoRP4xUYSTv5ImRog+h9yISPbVH7H8fASCIzYn9wlEv4zvFL7uKDMCFQm3g==", + "dev": true, "requires": { "delegates": "^1.0.0", "readable-stream": "^2.0.6" @@ -19716,6 +19865,7 @@ "version": "2.3.7", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "dev": true, "requires": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", @@ -19729,12 +19879,14 @@ "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==" + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true }, "string_decoder": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, "requires": { "safe-buffer": "~5.1.0" } @@ -19783,7 +19935,8 @@ "dev": true }, "asynckit": { - "version": "0.4.0" + "version": "0.4.0", + "dev": true }, "at-least-node": { "version": "1.0.0", @@ -19945,7 +20098,8 @@ } }, "balanced-match": { - "version": "1.0.2" + "version": "1.0.2", + "dev": true }, "base64-arraybuffer": { "version": "1.0.2", @@ -20033,6 +20187,7 @@ }, "brace-expansion": { "version": "1.1.11", + "dev": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -20161,6 +20316,7 @@ }, "chalk": { "version": "4.1.2", + "dev": true, "requires": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -20199,7 +20355,8 @@ "chownr": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", - "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==" + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "dev": true }, "chrome-trace-event": { "version": "1.0.3", @@ -20237,6 +20394,7 @@ }, "cliui": { "version": "7.0.4", + "dev": true, "requires": { "string-width": "^4.2.0", "strip-ansi": "^6.0.0", @@ -20262,7 +20420,8 @@ "code-point-at": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", - "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=" + "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", + "dev": true }, "codepage": { "version": "1.15.0", @@ -20279,12 +20438,14 @@ }, "color-convert": { "version": "2.0.1", + "dev": true, "requires": { "color-name": "~1.1.4" } }, "color-name": { - "version": "1.1.4" + "version": "1.1.4", + "dev": true }, "colors": { "version": "1.4.0", @@ -20295,6 +20456,7 @@ }, "combined-stream": { "version": "1.0.8", + "dev": true, "requires": { "delayed-stream": "~1.0.0" } @@ -20327,7 +20489,8 @@ "dev": true }, "concat-map": { - "version": "0.0.1" + "version": "0.0.1", + "dev": true }, "connect": { "version": "3.7.0", @@ -20361,7 +20524,8 @@ "console-control-strings": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", - "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=" + "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=", + "dev": true }, "content-type": { "version": "1.0.4", @@ -20456,7 +20620,8 @@ "core-js": { "version": "3.20.2", "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.20.2.tgz", - "integrity": "sha512-nuqhq11DcOAbFBV4zCbKeGbKQsUDRqTX0oqx7AttUBuqe3h20ixsE039QHelbL6P4h+9kytVqyEtyZ6gsiwEYw==" + "integrity": "sha512-nuqhq11DcOAbFBV4zCbKeGbKQsUDRqTX0oqx7AttUBuqe3h20ixsE039QHelbL6P4h+9kytVqyEtyZ6gsiwEYw==", + "dev": true }, "core-js-compat": { "version": "3.20.1", @@ -20473,7 +20638,8 @@ } }, "core-util-is": { - "version": "1.0.2" + "version": "1.0.2", + "dev": true }, "cors": { "version": "2.8.5", @@ -20776,6 +20942,7 @@ "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, "requires": { "ms": "2.1.2" } @@ -20864,12 +21031,14 @@ } }, "delayed-stream": { - "version": "1.0.0" + "version": "1.0.0", + "dev": true }, "delegates": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", - "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=" + "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=", + "dev": true }, "depd": { "version": "2.0.0", @@ -20896,7 +21065,8 @@ "detect-libc": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", - "integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=" + "integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=", + "dev": true }, "detect-newline": { "version": "3.1.0", @@ -21038,7 +21208,8 @@ "peer": true }, "emoji-regex": { - "version": "8.0.0" + "version": "8.0.0", + "dev": true }, "encodeurl": { "version": "1.0.2", @@ -21143,10 +21314,12 @@ "dev": true }, "es6-promise": { - "version": "4.2.8" + "version": "4.2.8", + "dev": true }, "es6-promisify": { "version": "5.0.0", + "dev": true, "requires": { "es6-promise": "^4.0.3" } @@ -21320,7 +21493,8 @@ "optional": true }, "escalade": { - "version": "3.1.1" + "version": "3.1.1", + "dev": true }, "escape-html": { "version": "1.0.3", @@ -21850,6 +22024,7 @@ "version": "1.2.7", "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-1.2.7.tgz", "integrity": "sha512-GWSSJGFy4e9GUeCcbIkED+bgAoFyj7XF1mV8rma3QW4NIqX9Kyx79N/PF61H5udOV3aY1IaMLs6pGbH71nlCTA==", + "dev": true, "requires": { "minipass": "^2.6.0" } @@ -21859,7 +22034,8 @@ "dev": true }, "fs.realpath": { - "version": "1.0.0" + "version": "1.0.0", + "dev": true }, "fsevents": { "version": "2.3.2", @@ -21878,6 +22054,7 @@ "version": "2.7.4", "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz", "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=", + "dev": true, "requires": { "aproba": "^1.0.3", "console-control-strings": "^1.0.0", @@ -21892,12 +22069,14 @@ "ansi-regex": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=" + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "dev": true }, "is-fullwidth-code-point": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", + "dev": true, "requires": { "number-is-nan": "^1.0.0" } @@ -21906,6 +22085,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", + "dev": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -21916,6 +22096,7 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "dev": true, "requires": { "ansi-regex": "^2.0.0" } @@ -21927,7 +22108,8 @@ "dev": true }, "get-caller-file": { - "version": "2.0.5" + "version": "2.0.5", + "dev": true }, "get-intrinsic": { "version": "1.1.1", @@ -22038,6 +22220,7 @@ }, "glob": { "version": "7.2.0", + "dev": true, "requires": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", @@ -22126,7 +22309,8 @@ } }, "google-protobuf": { - "version": "3.19.1" + "version": "3.19.1", + "dev": true }, "graceful-fs": { "version": "4.2.10", @@ -22179,7 +22363,8 @@ } }, "has-flag": { - "version": "4.0.0" + "version": "4.0.0", + "dev": true }, "has-symbols": { "version": "1.0.2", @@ -22188,7 +22373,8 @@ "has-unicode": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", - "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=" + "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=", + "dev": true }, "homedir-polyfill": { "version": "1.0.3", @@ -22289,6 +22475,7 @@ }, "https-proxy-agent": { "version": "2.2.4", + "dev": true, "requires": { "agent-base": "^4.3.0", "debug": "^3.1.0" @@ -22296,6 +22483,7 @@ "dependencies": { "debug": { "version": "3.2.7", + "dev": true, "requires": { "ms": "^2.1.1" } @@ -22422,13 +22610,15 @@ }, "inflight": { "version": "1.0.6", + "dev": true, "requires": { "once": "^1.3.0", "wrappy": "1" } }, "inherits": { - "version": "2.0.4" + "version": "2.0.4", + "dev": true }, "ini": { "version": "1.3.8", @@ -22467,7 +22657,8 @@ "dev": true }, "is-fullwidth-code-point": { - "version": "3.0.0" + "version": "3.0.0", + "dev": true }, "is-generator-fn": { "version": "2.1.0", @@ -22540,7 +22731,8 @@ "dev": true }, "isarray": { - "version": "1.0.0" + "version": "1.0.0", + "dev": true }, "isbinaryfile": { "version": "4.0.10", @@ -23773,6 +23965,7 @@ }, "lru-cache": { "version": "6.0.0", + "dev": true, "requires": { "yallist": "^4.0.0" } @@ -23916,10 +24109,12 @@ "dev": true }, "mime-db": { - "version": "1.51.0" + "version": "1.51.0", + "dev": true }, "mime-types": { "version": "2.1.34", + "dev": true, "requires": { "mime-db": "1.51.0" } @@ -23934,6 +24129,7 @@ }, "minimatch": { "version": "3.0.4", + "dev": true, "requires": { "brace-expansion": "^1.1.7" } @@ -23941,7 +24137,8 @@ "minimist": { "version": "1.2.6", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz", - "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==" + "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==", + "dev": true }, "minimist-options": { "version": "4.1.0", @@ -23956,6 +24153,7 @@ "version": "2.9.0", "resolved": "https://registry.npmjs.org/minipass/-/minipass-2.9.0.tgz", "integrity": "sha512-wxfUjg9WebH+CUDX/CdbRlh5SmfZiy/hpkxaRI16Y9W56Pa75sWgd/rvFilSgrauD9NyFymP/+JFV3KwzIsJeg==", + "dev": true, "requires": { "safe-buffer": "^5.1.2", "yallist": "^3.0.0" @@ -23964,7 +24162,8 @@ "yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==" + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true } } }, @@ -23972,12 +24171,14 @@ "version": "1.3.3", "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-1.3.3.tgz", "integrity": "sha512-6ZYMOEnmVsdCeTJVE0W9ZD+pVnE8h9Hma/iOwwRDsdQoePpoX56/8B6z3P9VNwppJuBKNRuFDRNRqRWexT9G9Q==", + "dev": true, "requires": { "minipass": "^2.9.0" } }, "mkdirp": { - "version": "1.0.4" + "version": "1.0.4", + "dev": true }, "modify-values": { "version": "1.0.1", @@ -23986,7 +24187,8 @@ "dev": true }, "ms": { - "version": "2.1.2" + "version": "2.1.2", + "dev": true }, "mylas": { "version": "2.1.6", @@ -24046,6 +24248,7 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", + "dev": true, "requires": { "abbrev": "1" } @@ -26169,6 +26372,7 @@ "version": "4.1.2", "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz", "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==", + "dev": true, "requires": { "are-we-there-yet": "~1.1.2", "console-control-strings": "~1.1.0", @@ -26179,7 +26383,8 @@ "number-is-nan": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", - "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=" + "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", + "dev": true }, "nwsapi": { "version": "2.2.0", @@ -26192,7 +26397,8 @@ "dev": true }, "object-assign": { - "version": "4.1.1" + "version": "4.1.1", + "dev": true }, "object-inspect": { "version": "1.12.0", @@ -26225,6 +26431,7 @@ }, "once": { "version": "1.4.0", + "dev": true, "requires": { "wrappy": "1" } @@ -26373,7 +26580,8 @@ "dev": true }, "path-is-absolute": { - "version": "1.0.1" + "version": "1.0.1", + "dev": true }, "path-key": { "version": "3.1.1", @@ -26550,10 +26758,12 @@ "dev": true }, "process-nextick-args": { - "version": "2.0.1" + "version": "2.0.1", + "dev": true }, "progress": { - "version": "2.0.3" + "version": "2.0.3", + "dev": true }, "prompts": { "version": "2.4.2", @@ -26866,7 +27076,8 @@ } }, "require-directory": { - "version": "2.1.1" + "version": "2.1.1", + "dev": true }, "require-from-string": { "version": "2.0.2", @@ -26956,6 +27167,7 @@ }, "rimraf": { "version": "3.0.2", + "dev": true, "requires": { "glob": "^7.1.3" } @@ -26968,7 +27180,8 @@ } }, "safe-buffer": { - "version": "5.2.1" + "version": "5.2.1", + "dev": true }, "safer-buffer": { "version": "2.1.2", @@ -27078,6 +27291,7 @@ }, "semver": { "version": "7.3.5", + "dev": true, "requires": { "lru-cache": "^6.0.0" } @@ -27117,7 +27331,8 @@ "set-blocking": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", - "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=" + "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=", + "dev": true }, "setprototypeof": { "version": "1.2.0", @@ -27164,7 +27379,8 @@ } }, "signal-exit": { - "version": "3.0.6" + "version": "3.0.6", + "dev": true }, "signale": { "version": "1.4.0", @@ -27363,7 +27579,8 @@ } }, "sprintf-js": { - "version": "1.0.3" + "version": "1.0.3", + "dev": true }, "ssf": { "version": "0.11.2", @@ -27496,6 +27713,7 @@ }, "string-width": { "version": "4.2.3", + "dev": true, "requires": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", @@ -27504,6 +27722,7 @@ }, "strip-ansi": { "version": "6.0.1", + "dev": true, "requires": { "ansi-regex": "^5.0.1" } @@ -27530,6 +27749,7 @@ }, "supports-color": { "version": "7.2.0", + "dev": true, "requires": { "has-flag": "^4.0.0" } @@ -27591,6 +27811,7 @@ "version": "4.4.19", "resolved": "https://registry.npmjs.org/tar/-/tar-4.4.19.tgz", "integrity": "sha512-a20gEsvHnWe0ygBY8JbxoM4w3SJdhc7ZAuxkLqh+nvNQN2IOt0B5lLgM490X5Hl8FF0dl0tOf2ewFYAlIFgzVA==", + "dev": true, "requires": { "chownr": "^1.1.4", "fs-minipass": "^1.2.7", @@ -27605,6 +27826,7 @@ "version": "0.5.5", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", + "dev": true, "requires": { "minimist": "^1.2.5" } @@ -27612,7 +27834,8 @@ "yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==" + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true } } }, @@ -28013,7 +28236,8 @@ "dev": true }, "util-deprecate": { - "version": "1.0.2" + "version": "1.0.2", + "dev": true }, "utils-merge": { "version": "1.0.1", @@ -28217,6 +28441,7 @@ "version": "1.1.5", "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", + "dev": true, "requires": { "string-width": "^1.0.2 || 2 || 3 || 4" } @@ -28249,6 +28474,7 @@ }, "wrap-ansi": { "version": "7.0.0", + "dev": true, "requires": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", @@ -28256,7 +28482,8 @@ } }, "wrappy": { - "version": "1.0.2" + "version": "1.0.2", + "dev": true }, "write-file-atomic": { "version": "3.0.3", @@ -28311,10 +28538,12 @@ "dev": true }, "y18n": { - "version": "5.0.8" + "version": "5.0.8", + "dev": true }, "yallist": { - "version": "4.0.0" + "version": "4.0.0", + "dev": true }, "yaml": { "version": "1.10.2", @@ -28340,7 +28569,8 @@ } }, "yargs-parser": { - "version": "20.2.9" + "version": "20.2.9", + "dev": true }, "yn": { "version": "3.1.1", diff --git a/package.json b/package.json index 339f817f..59bc3324 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "scikitjs", - "version": "1.21.0", + "version": "1.22.0", "description": "Scikit-Learn for JS", "output": { "node": "dist/node/index.js", @@ -21,16 +21,17 @@ "doc": "docs" }, "browser": { - "danfojs-node": "danfojs" + "danfojs-node": "danfojs", + "@tensorflow/tfjs-node": "@tensorflow/tfjs" }, "scripts": { "test": "node_modules/.bin/jest src/**/*.test.ts src/*.test.ts --coverage && npm run prettier:check && npm run test:browser", "test:ci": "node --max_old_space_size=8192 node_modules/.bin/jest src/**/*.test.ts src/*.test.ts --coverage --runInBand --ci && npm run prettier:check && npm run test:browser", "test:clean": "node_modules/.bin/jest src/**/*.test.ts src/*.test.ts --coverage && npm run prettier:check && npm run test:browser", "compile:web": "node_modules/esbuild/bin/esbuild src/index.ts --bundle --platform=browser --minify --legal-comments=none --format=esm --outfile=dist/web/index.min.js", - "compile:esm": "node_modules/.bin/tsc -p tsconfig.build-esm.json && node_modules/.bin/tsc-alias -p tsconfig.build-esm.json", - "compile:node-cjs": "node_modules/.bin/tsc -p tsconfig.build-node.json && node_modules/.bin/tsc-alias -p tsconfig.build-node.json", - "compile:es5": "node_modules/.bin/tsc -p tsconfig.build-es5.json && node_modules/.bin/tsc-alias -p tsconfig.build-es5.json", + "compile:esm": "node_modules/.bin/tsc -p tsconfig.build-esm.json", + "compile:node-cjs": "node_modules/.bin/tsc -p tsconfig.build-node.json", + "compile:es5": "node_modules/.bin/tsc -p tsconfig.build-es5.json", "test:browser": "node esbuild.config.js && node_modules/karma/bin/karma start karma.config.js", "prettier:check": "node_modules/prettier/bin-prettier.js --check src", "build": "npm run compile:esm && npm run compile:node-cjs && npm run compile:web && npm run compile:es5", @@ -49,8 +50,8 @@ }, "homepage": "https://github.com/javascriptdata/scikit.js#readme", "dependencies": { - "@tensorflow/tfjs": "^3.16.0", - "@tensorflow/tfjs-node": "^3.16.0", + "@tensorflow/tfjs-core": "^3.16.0", + "@tensorflow/tfjs-layers": "^3.16.0", "base64-arraybuffer": "^1.0.2", "lodash": "^4.17.21", "mathjs": "^10.0.0", @@ -70,6 +71,8 @@ "@semantic-release/git": "9.0.0", "@semantic-release/npm": "^7.1.0", "@semantic-release/release-notes-generator": "9.0.3", + "@tensorflow/tfjs": "^3.16.0", + "@tensorflow/tfjs-node": "^3.16.0", "@types/chai": "^4.2.22", "@types/jest": "^27.4.0", "@types/lodash": "^4.14.177", diff --git a/src/cluster/KMeans.test.ts b/src/cluster/KMeans.test.ts index b5f3ce2a..e6badaeb 100644 --- a/src/cluster/KMeans.test.ts +++ b/src/cluster/KMeans.test.ts @@ -1,4 +1,6 @@ -import { fromJSON, KMeans } from '../index' +import * as tf from '@tensorflow/tfjs' +import { KMeans, setBackend, fromJSON } from '../index' +setBackend(tf) // Next steps: Improve on kmeans cluster testing describe('KMeans', () => { const X = [ @@ -56,6 +58,7 @@ describe('KMeans', () => { } const kmean = new KMeans({ nClusters: 2, randomState: 0 }) kmean.fit(X) + delete kmean.tf const ksave = await kmean.toObject() expect(expectedResult).toEqual(ksave) diff --git a/src/cluster/KMeans.ts b/src/cluster/KMeans.ts index f154084e..bdafc494 100644 --- a/src/cluster/KMeans.ts +++ b/src/cluster/KMeans.ts @@ -1,7 +1,7 @@ -import { Scikit2D } from '../types' +import { Scikit2D, Tensor1D, Tensor2D } from '../types' import { convertToNumericTensor2D, sampleWithoutReplacement } from '../utils' +import { getBackend } from '../tf-singleton' import { Serialize } from '../simpleSerializer' -import { tf } from '../shared/globals' /* Next steps @@ -68,10 +68,11 @@ export class KMeans extends Serialize { // Attributes /** The actual cluster centers found by KMeans */ - clusterCenters: tf.Tensor2D + clusterCenters: Tensor2D /** Useful for pipelines and column transformers to have a default name for transforms */ name = 'KMeans' + tf: any constructor({ nClusters = 8, @@ -82,51 +83,58 @@ export class KMeans extends Serialize { randomState }: KMeansParams = {}) { super() + this.tf = getBackend() this.nClusters = nClusters this.init = init this.maxIter = maxIter this.tol = tol this.randomState = randomState this.nInit = nInit - this.clusterCenters = tf.tensor2d([[]]) + this.clusterCenters = this.tf.tensor2d([[]]) } - initCentroids(X: tf.Tensor2D) { + initCentroids(X: Tensor2D) { if (this.init === 'random') { let indices = sampleWithoutReplacement( X.shape[0], this.nClusters, this.randomState ) - this.clusterCenters = tf.gather(X, indices) + this.clusterCenters = this.tf.gather(X, indices) return } throw new Error(`init ${this.init} is not currently implemented`) } - closestCentroid(X: tf.Tensor2D): tf.Tensor1D { - return tf.tidy(() => { - const expandedX = tf.expandDims(X, 1) - const expandedClusters = tf.expandDims(this.clusterCenters, 0) - return tf.squaredDifference(expandedX, expandedClusters).sum(2).argMin(1) + closestCentroid(X: Tensor2D): Tensor1D { + return this.tf.tidy(() => { + const expandedX = this.tf.expandDims(X, 1) + const expandedClusters = this.tf.expandDims(this.clusterCenters, 0) + return this.tf + .squaredDifference(expandedX, expandedClusters) + .sum(2) + .argMin(1) }) } - updateCentroids(X: tf.Tensor2D, nearestIndices: tf.Tensor1D): tf.Tensor2D { - return tf.tidy(() => { + updateCentroids(X: Tensor2D, nearestIndices: Tensor1D): Tensor2D { + return this.tf.tidy(() => { const newCentroids = [] for (let i = 0; i < this.nClusters; i++) { - const mask = tf.equal(nearestIndices, tf.scalar(i).toInt()) - const currentCentroid = tf.div( + const mask = this.tf.equal(nearestIndices, this.tf.scalar(i).toInt()) + const currentCentroid = this.tf.div( // set all masked instances to 0 by multiplying the mask tensor, // then sum across all instances - tf.sum(tf.mul(tf.expandDims(mask.toFloat(), 1), X), 0), + this.tf.sum( + this.tf.mul(this.tf.expandDims(mask.toFloat(), 1), X), + 0 + ), // divided by number of instances - tf.sum(mask.toFloat()) + this.tf.sum(mask.toFloat()) ) newCentroids.push(currentCentroid) } - return tf.stack(newCentroids) as tf.Tensor2D + return this.tf.stack(newCentroids) as Tensor2D }) } @@ -148,20 +156,20 @@ export class KMeans extends Serialize { * Converts 2D input into a 1D Tensor which holds the KMeans cluster Class label * @param X The 2D Matrix that you wish to cluster */ - public predict(X: Scikit2D): tf.Tensor1D { + public predict(X: Scikit2D): Tensor1D { let XTensor2D = convertToNumericTensor2D(X) return this.closestCentroid(XTensor2D) } - public transform(X: Scikit2D): tf.Tensor2D { - return tf.tidy(() => { + public transform(X: Scikit2D): Tensor2D { + return this.tf.tidy(() => { const XTensor2D = convertToNumericTensor2D(X) - const expandedX = tf.expandDims(XTensor2D, 1) - const expandedClusters = tf.expandDims(this.clusterCenters, 0) - return tf + const expandedX = this.tf.expandDims(XTensor2D, 1) + const expandedClusters = this.tf.expandDims(this.clusterCenters, 0) + return this.tf .squaredDifference(expandedX, expandedClusters) .sum(2) - .sqrt() as tf.Tensor2D + .sqrt() as Tensor2D }) } @@ -173,12 +181,12 @@ export class KMeans extends Serialize { return this.fit(X).transform(X) } - public score(X: Scikit2D): tf.Tensor1D { - return tf.tidy(() => { + public score(X: Scikit2D): Tensor1D { + return this.tf.tidy(() => { const XTensor2D = convertToNumericTensor2D(X) - const expandedX = tf.expandDims(XTensor2D, 1) - const expandedClusters = tf.expandDims(this.clusterCenters, 0) - return tf + const expandedX = this.tf.expandDims(XTensor2D, 1) + const expandedClusters = this.tf.expandDims(this.clusterCenters, 0) + return this.tf .squaredDifference(expandedX, expandedClusters) .sum(2) .min(1) diff --git a/src/compose/ColumnTransformer.test.ts b/src/compose/ColumnTransformer.test.ts index b31a918e..299b3023 100644 --- a/src/compose/ColumnTransformer.test.ts +++ b/src/compose/ColumnTransformer.test.ts @@ -1,10 +1,13 @@ import { - fromJSON, - SimpleImputer, + ColumnTransformer, MinMaxScaler, - ColumnTransformer + SimpleImputer, + setBackend, + fromJSON } from '../index' import * as dfd from 'danfojs-node' +import * as tf from '@tensorflow/tfjs' +setBackend(tf) describe('ColumnTransformer', function () { it('ColumnTransformer simple test', function () { diff --git a/src/compose/ColumnTransformer.ts b/src/compose/ColumnTransformer.ts index 096337c1..b2317ee0 100644 --- a/src/compose/ColumnTransformer.ts +++ b/src/compose/ColumnTransformer.ts @@ -1,7 +1,7 @@ -import { DataFrameInterface, Scikit1D, Transformer } from '../types' +import { DataFrameInterface, Scikit1D, Transformer, Tensor2D } from '../types' import { isDataFrameInterface } from '../typesUtils' +import { getBackend } from '../tf-singleton' import { Serialize } from '../simpleSerializer' -import { tf } from '../shared/globals' /* Next steps: 1. Support 'passthrough' and 'drop' and estimator for remainder (also in transformer list) @@ -71,17 +71,19 @@ export class ColumnTransformer extends Serialize { /** Useful for pipelines and column transformers to have a default name for transforms */ name = 'ColumnTransformer' + tf: any constructor({ transformers = [], remainder = 'drop' }: ColumnTransformerParams = {}) { super() + this.tf = getBackend() this.transformers = transformers this.remainder = remainder } - public fit(X: tf.Tensor2D | DataFrameInterface, y?: Scikit1D) { + public fit(X: Tensor2D | DataFrameInterface, y?: Scikit1D) { for (let i = 0; i < this.transformers.length; i++) { let [, curTransform, selection] = this.transformers[i] @@ -91,7 +93,7 @@ export class ColumnTransformer extends Serialize { return this } - public transform(X: tf.Tensor2D | DataFrameInterface, y?: Scikit1D) { + public transform(X: Tensor2D | DataFrameInterface, y?: Scikit1D) { let output = [] for (let i = 0; i < this.transformers.length; i++) { let [, curTransform, selection] = this.transformers[i] @@ -100,10 +102,10 @@ export class ColumnTransformer extends Serialize { output.push(curTransform.transform(subsetX, y)) } - return tf.concat(output, 1) + return this.tf.concat(output, 1) } - public fitTransform(X: tf.Tensor2D | DataFrameInterface, y?: Scikit1D) { + public fitTransform(X: Tensor2D | DataFrameInterface, y?: Scikit1D) { let output = [] for (let i = 0; i < this.transformers.length; i++) { let [, curTransform, selection] = this.transformers[i] @@ -112,27 +114,27 @@ export class ColumnTransformer extends Serialize { output.push(curTransform.fitTransform(subsetX, y)) } - return tf.concat(output, 1) + return this.tf.concat(output, 1) } getColumns( - X: DataFrameInterface | tf.Tensor2D, + X: DataFrameInterface | Tensor2D, selectedColumns: Selection - ): tf.Tensor2D { + ): Tensor2D { if (isDataFrameInterface(X)) { if (isStringArray(selectedColumns)) { return X.loc({ columns: selectedColumns }) - .tensor as unknown as tf.Tensor2D + .tensor as unknown as Tensor2D } if (Array.isArray(selectedColumns)) { return X.iloc({ columns: selectedColumns }) - .tensor as unknown as tf.Tensor2D + .tensor as unknown as Tensor2D } if (typeof selectedColumns === 'string') { return X[selectedColumns].tensor } return X.iloc({ columns: [selectedColumns] }) - .tensor as unknown as tf.Tensor2D + .tensor as unknown as Tensor2D } else { if ( isStringArray(selectedColumns) || @@ -143,10 +145,10 @@ export class ColumnTransformer extends Serialize { ) } if (typeof selectedColumns === 'number') { - let columns = tf.tensor1d([selectedColumns]) + let columns = this.tf.tensor1d([selectedColumns]) return X.gather(columns, 1) } else { - let columns = tf.tensor1d(selectedColumns) + let columns = this.tf.tensor1d(selectedColumns) return X.gather(columns, 1) } } diff --git a/src/datasets/makeRegression.test.ts b/src/datasets/makeRegression.test.ts index e5e216bf..97bbc09b 100644 --- a/src/datasets/makeRegression.test.ts +++ b/src/datasets/makeRegression.test.ts @@ -1,5 +1,6 @@ -import { makeLowRankMatrix, makeRegression } from './makeRegression' -import { tf } from '../shared/globals' +import { makeLowRankMatrix, makeRegression, setBackend } from '../index' +import * as tf from '@tensorflow/tfjs' +setBackend(tf) describe('makeRegression tests', () => { it('returns the right size', () => { diff --git a/src/datasets/makeRegression.ts b/src/datasets/makeRegression.ts index ab3178ba..babf51ef 100644 --- a/src/datasets/makeRegression.ts +++ b/src/datasets/makeRegression.ts @@ -1,6 +1,6 @@ -import { tf } from '../shared/globals' -type Tensor2D = tf.Tensor2D -type Tensor1D = tf.Tensor1D +import { Tensor1D, Tensor2D } from '../types' +import { getBackend } from '../tf-singleton' + interface MakeRegressionInput { nSamples?: number nFeatures?: number @@ -29,10 +29,11 @@ export const makeRegression = ({ | [Tensor2D, Tensor1D | Tensor2D] | [Tensor2D, Tensor1D, Tensor1D] | [Tensor2D, Tensor2D, Tensor2D] => { + let tf = getBackend() return tf.tidy(() => { const numberInformative = Math.min(nFeatures, nInformative) - let X: tf.Tensor2D + let X: Tensor2D if (effectiveRank === null) { // Randomly generate a well conditioned input set X = tf.randomNormal([nSamples, nFeatures]) @@ -87,7 +88,9 @@ export const makeLowRankMatrix = ({ nFeatures = 100, effectiveRank = 10, tailStrength = 0.5 -}: MakeLowRankMatrixInput = {}): tf.Tensor2D => { +}: MakeLowRankMatrixInput = {}): Tensor2D => { + let tf = getBackend() + return tf.tidy(() => { let n = Math.min(nSamples, nFeatures) @@ -107,6 +110,6 @@ export const makeLowRankMatrix = ({ let s = lowRank.add(tail) - return u.mul(s).dot(v.transpose()) as tf.Tensor2D + return u.mul(s).dot(v.transpose()) as Tensor2D }) } diff --git a/src/dummy/DummyClassifier.test.ts b/src/dummy/DummyClassifier.test.ts index b1f0e920..d4d455a4 100644 --- a/src/dummy/DummyClassifier.test.ts +++ b/src/dummy/DummyClassifier.test.ts @@ -1,4 +1,7 @@ -import { DummyClassifier, fromJSON } from '../index' +import { DummyClassifier, setBackend, fromJSON } from '../index' +import * as tf from '@tensorflow/tfjs' +setBackend(tf) + describe('DummyClassifier', function () { it('Use DummyClassifier on simple example (mostFrequent)', function () { const clf = new DummyClassifier() @@ -70,6 +73,8 @@ describe('DummyClassifier', function () { clf.fit(X, y) const clfSave = await clf.toObject() + // We don't care what version of tf is saved on there + delete clfSave.tf expect(expectedResult).toEqual(clfSave) }) it('should load DummyClassifier', async function () { diff --git a/src/dummy/DummyClassifier.ts b/src/dummy/DummyClassifier.ts index 04e12465..f574a210 100644 --- a/src/dummy/DummyClassifier.ts +++ b/src/dummy/DummyClassifier.ts @@ -14,13 +14,13 @@ */ import { convertToNumericTensor1D, convertToNumericTensor2D } from '../utils' -import { Scikit1D, Scikit2D } from '../types' +import { Scikit1D, Scikit2D, Tensor2D, Tensor1D } from '../types' import { isScikit2D, assert, isScikit1D } from '../typesUtils' import { modeFast } from 'simple-statistics' import uniq from 'lodash/uniq' import sample from 'lodash/sample' import { ClassifierMixin } from '../mixins' -import { tf } from '../shared/globals' +import { getBackend } from '../tf-singleton' /* Next steps: @@ -88,11 +88,13 @@ export class DummyClassifier extends ClassifierMixin { /** Useful for pipelines and column transformers to have a default name for transforms */ name = 'DummyClassifier' + tf: any constructor({ strategy = 'mostFrequent', constant = 0 }: DummyClassifierParams = {}) { super() + this.tf = getBackend() this.constant = constant this.strategy = strategy this.classes = [] @@ -118,19 +120,19 @@ export class DummyClassifier extends ClassifierMixin { return this } - public predictProba(X: Scikit2D): tf.Tensor2D { + public predictProba(X: Scikit2D): Tensor2D { assert(isScikit2D(X), 'Data can not be converted to a 1D or 2D matrix.') assert( ['mostFrequent', 'uniform', 'constant'].includes(this.strategy), `Strategy ${this.strategy} not supported. We support 'mostFrequent', 'uniform', and 'constant'` ) - return tf.oneHot( + return this.tf.oneHot( this.predict(X).toInt(), this.classes.length - ) as tf.Tensor2D + ) as Tensor2D } - public predict(X: Scikit2D): tf.Tensor1D { + public predict(X: Scikit2D): Tensor1D { assert(isScikit2D(X), 'Data can not be converted to a 1D or 2D matrix.') assert( ['mostFrequent', 'uniform', 'constant'].includes(this.strategy), @@ -139,7 +141,7 @@ export class DummyClassifier extends ClassifierMixin { let newData = convertToNumericTensor2D(X) let length = newData.shape[0] if (this.strategy === 'mostFrequent' || this.strategy === 'constant') { - return tf.tensor1d(Array(length).fill(this.constant)) + return this.tf.tensor1d(Array(length).fill(this.constant)) } // "Uniform case" @@ -147,6 +149,6 @@ export class DummyClassifier extends ClassifierMixin { for (let i = 0; i < length; i++) { returnArr.push(sample(this.classes)) } - return tf.tensor1d(returnArr as number[]) + return this.tf.tensor1d(returnArr as number[]) } } diff --git a/src/dummy/DummyRegressor.test.ts b/src/dummy/DummyRegressor.test.ts index 6c79feb7..45577959 100644 --- a/src/dummy/DummyRegressor.test.ts +++ b/src/dummy/DummyRegressor.test.ts @@ -1,4 +1,6 @@ -import { DummyRegressor, fromJSON } from '../index' +import { DummyRegressor, setBackend, fromJSON } from '../index' +import * as tf from '@tensorflow/tfjs' +setBackend(tf) describe('DummyRegressor', function () { it('Use DummyRegressor on simple example (mean)', function () { @@ -73,7 +75,7 @@ describe('DummyRegressor', function () { } reg.fit(X, y) - + delete reg.tf expect(saveResult).toEqual(await reg.toObject()) }) diff --git a/src/dummy/DummyRegressor.ts b/src/dummy/DummyRegressor.ts index 124b2249..f895fdb9 100644 --- a/src/dummy/DummyRegressor.ts +++ b/src/dummy/DummyRegressor.ts @@ -14,11 +14,11 @@ */ import { convertToNumericTensor1D, convertToNumericTensor2D } from '../utils' -import { Scikit1D, Scikit2D } from '../types' +import { Scikit1D, Scikit2D, Tensor1D } from '../types' import { assert, isScikit1D, isScikit2D } from '../typesUtils' import { median, quantileSeq } from 'mathjs' import { RegressorMixin } from '../mixins' -import { tf } from '../shared/globals' +import { getBackend } from '../tf-singleton' /* Next steps: @@ -86,12 +86,14 @@ export class DummyRegressor extends RegressorMixin { /** Useful for pipelines and column transformers to have a default name for transforms */ name = 'DummyRegressor' + tf: any constructor({ strategy = 'mean', constant, quantile }: DummyRegressorParams = {}) { super() + this.tf = getBackend() this.strategy = strategy this.constant = constant this.quantile = quantile @@ -135,10 +137,10 @@ export class DummyRegressor extends RegressorMixin { return this } - public predict(X: Scikit2D): tf.Tensor1D { + public predict(X: Scikit2D): Tensor1D { assert(isScikit2D(X), 'Data can not be converted to a 2D matrix.') let newData = convertToNumericTensor2D(X) let length = newData.shape[0] - return tf.tensor1d(Array(length).fill(this.constant)) + return this.tf.tensor1d(Array(length).fill(this.constant)) } } diff --git a/src/ensemble/VotingClassifier.test.ts b/src/ensemble/VotingClassifier.test.ts index cafb9973..43a8daab 100644 --- a/src/ensemble/VotingClassifier.test.ts +++ b/src/ensemble/VotingClassifier.test.ts @@ -3,8 +3,11 @@ import { VotingClassifier, DummyClassifier, LogisticRegression, + setBackend, fromJSON } from '../index' +import * as tf from '@tensorflow/tfjs' +setBackend(tf) describe('VotingClassifier', function () { it('Use VotingClassifier on simple example (voting = hard)', async function () { diff --git a/src/ensemble/VotingClassifier.ts b/src/ensemble/VotingClassifier.ts index ce93c91a..9e7404cc 100644 --- a/src/ensemble/VotingClassifier.ts +++ b/src/ensemble/VotingClassifier.ts @@ -1,5 +1,5 @@ -import { Scikit1D, Scikit2D } from '../types' -import { tf } from '../shared/globals' +import { Scikit1D, Scikit2D, Tensor1D, Tensor2D } from '../types' +import { getBackend } from '../tf-singleton' import { ClassifierMixin } from '../mixins' import { LabelEncoder } from '../preprocessing/LabelEncoder' @@ -62,12 +62,14 @@ export class VotingClassifier extends ClassifierMixin { le: any name = 'VotingClassifier' + tf: any constructor({ estimators = [], weights = undefined, voting = 'hard' }: VotingClassifierParams = {}) { super() + this.tf = getBackend() this.estimators = estimators this.weights = weights this.voting = voting @@ -83,7 +85,7 @@ export class VotingClassifier extends ClassifierMixin { return this } - public predictProba(X: Scikit2D): tf.Tensor1D { + public predictProba(X: Scikit2D): Tensor1D { let responses = [] let numEstimators = this.estimators.length const weights = @@ -94,11 +96,11 @@ export class VotingClassifier extends ClassifierMixin { responses.push(curEstimator.predictProba(X).mul(curWeight)) } - return tf.addN(responses) + return this.tf.addN(responses) } // only hard case - public predict(X: Scikit2D): tf.Tensor1D { + public predict(X: Scikit2D): Tensor1D { let responses = [] let numEstimators = this.estimators.length const weights = @@ -109,11 +111,11 @@ export class VotingClassifier extends ClassifierMixin { let [_, curEstimator] = this.estimators[i] let curWeight = weights[i] let predictions = curEstimator.predict(X).toInt() - let oneHot = tf.oneHot(predictions, this.le.classes.length) + let oneHot = this.tf.oneHot(predictions, this.le.classes.length) responses.push(oneHot.mul(curWeight)) } - return tf.tensor1d( - this.le.inverseTransform(tf.addN(responses).argMax(1)) + return this.tf.tensor1d( + this.le.inverseTransform(this.tf.addN(responses).argMax(1)) ) } else { for (let i = 0; i < numEstimators; i++) { @@ -122,13 +124,13 @@ export class VotingClassifier extends ClassifierMixin { let predictions = curEstimator.predictProba(X) responses.push(predictions.mul(curWeight)) } - return tf.tensor1d( - this.le.inverseTransform(tf.addN(responses).argMax(1)) + return this.tf.tensor1d( + this.le.inverseTransform(this.tf.addN(responses).argMax(1)) ) } } - public transform(X: Scikit2D): Array | Array { + public transform(X: Scikit2D): Array | Array { let responses = [] let numEstimators = this.estimators.length @@ -150,7 +152,7 @@ export class VotingClassifier extends ClassifierMixin { public async fitTransform( X: Scikit2D, y: Scikit1D - ): Promise | Array> { + ): Promise | Array> { return (await this.fit(X, y)).transform(X) } } diff --git a/src/ensemble/VotingRegressor.test.ts b/src/ensemble/VotingRegressor.test.ts index 7782ab86..51ae89f0 100644 --- a/src/ensemble/VotingRegressor.test.ts +++ b/src/ensemble/VotingRegressor.test.ts @@ -1,10 +1,13 @@ import { makeVotingRegressor, VotingRegressor, - fromJSON, DummyRegressor, - LinearRegression + LinearRegression, + setBackend, + fromJSON } from '../index' +import * as tf from '@tensorflow/tfjs' +setBackend(tf) describe('VotingRegressor', function () { it('Use VotingRegressor on simple example ', async function () { diff --git a/src/ensemble/VotingRegressor.ts b/src/ensemble/VotingRegressor.ts index 41396e4d..265be528 100644 --- a/src/ensemble/VotingRegressor.ts +++ b/src/ensemble/VotingRegressor.ts @@ -1,6 +1,6 @@ -import { Scikit1D, Scikit2D } from '../types' -import { tf } from '../shared/globals' +import { Scikit1D, Scikit2D, Tensor1D } from '../types' import { RegressorMixin } from '../mixins' +import { getBackend } from '../tf-singleton' /* Next steps: 0. Write validation code to check Estimator inputs @@ -55,6 +55,7 @@ export class VotingRegressor extends RegressorMixin { weights = undefined }: VotingRegressorParams = {}) { super() + this.tf = getBackend() this.estimators = estimators this.weights = weights } @@ -67,7 +68,7 @@ export class VotingRegressor extends RegressorMixin { return this } - public predict(X: Scikit2D): tf.Tensor1D { + public predict(X: Scikit2D): Tensor1D { let responses = [] let numEstimators = this.estimators.length const weights = @@ -78,10 +79,10 @@ export class VotingRegressor extends RegressorMixin { responses.push(curEstimator.predict(X).mul(curWeight)) } - return tf.addN(responses) + return this.tf.addN(responses) } - public transform(X: Scikit2D): Array { + public transform(X: Scikit2D): Array { let responses = [] let numEstimators = this.estimators.length for (let i = 0; i < numEstimators; i++) { diff --git a/src/impute/SimpleImputer.test.ts b/src/impute/SimpleImputer.test.ts index d9957b5e..3ea0c0c7 100644 --- a/src/impute/SimpleImputer.test.ts +++ b/src/impute/SimpleImputer.test.ts @@ -1,5 +1,6 @@ -import { tf } from '../shared/globals' -import { SimpleImputer, fromJSON } from '../index' +import { SimpleImputer, setBackend, fromJSON } from '../index' +import * as tf from '@tensorflow/tfjs' +setBackend(tf) describe('SimpleImputer', function () { it('Imputes with "constant" strategy 2D one column. In this strategy, we give the fill value', function () { @@ -141,6 +142,7 @@ describe('SimpleImputer', function () { } imputer.fitTransform(data) + delete imputer.tf expect(await imputer.toObject()).toEqual(expected) }) it('Should load serialized Imputer', async function () { diff --git a/src/impute/SimpleImputer.ts b/src/impute/SimpleImputer.ts index 722591c7..cd18126e 100644 --- a/src/impute/SimpleImputer.ts +++ b/src/impute/SimpleImputer.ts @@ -14,12 +14,12 @@ */ import { convertToNumericTensor2D, convertToTensor2D } from '../utils' -import { Scikit2D } from '../types' +import { Scikit2D, Tensor2D, Tensor1D, TensorLike } from '../types' import { tensorMean } from '../math' import { median } from 'mathjs' import { modeFast } from 'simple-statistics' import { TransformerMixin } from '../mixins' -import { tf } from '../shared/globals' +import { getBackend } from '../tf-singleton' /* Next steps: @@ -63,21 +63,23 @@ export class SimpleImputer extends TransformerMixin { fillValue: string | number | undefined strategy: 'mean' | 'median' | 'mostFrequent' | 'constant' - statistics: tf.Tensor1D + statistics: Tensor1D /** Useful for pipelines and column transformers to have a default name for transforms */ name = 'SimpleImputer' + tf: any constructor({ strategy = 'mean', fillValue = undefined, missingValues = NaN }: SimpleImputerParams = {}) { super() + this.tf = getBackend() this.missingValues = missingValues this.strategy = strategy this.fillValue = fillValue - this.statistics = tf.tensor1d([]) + this.statistics = this.tf.tensor1d([]) } public fit(X: Scikit2D): SimpleImputer { @@ -88,29 +90,29 @@ export class SimpleImputer extends TransformerMixin { if (this.strategy === 'mean') { const newTensor = convertToNumericTensor2D(X) const mean = tensorMean(newTensor, 0, true) - this.statistics = mean as tf.Tensor1D + this.statistics = mean as Tensor1D return this } if (this.strategy === 'mostFrequent') { const newTensor = convertToNumericTensor2D(X) const mostFrequents = newTensor - .transpose() + .transpose() .arraySync() .map((arr: number[] | string[]) => modeFast(removeMissingValuesFromArray(arr)) ) - this.statistics = tf.tensor1d(mostFrequents) + this.statistics = this.tf.tensor1d(mostFrequents) return this } if (this.strategy === 'median') { const newTensor = convertToNumericTensor2D(X) const medians = newTensor - .transpose() + .transpose() .arraySync() .map((arr: number[] | string[]) => median(removeMissingValuesFromArray(arr)) ) - this.statistics = tf.tensor1d(medians) + this.statistics = this.tf.tensor1d(medians) return this } throw new Error( @@ -118,37 +120,37 @@ export class SimpleImputer extends TransformerMixin { ) } - public transform(X: Scikit2D): tf.Tensor2D { + public transform(X: Scikit2D): Tensor2D { if (this.strategy === 'constant') { const newTensor = convertToTensor2D(X) if (this.fillValue === undefined) { if (newTensor.dtype !== 'string') { - return tf.where( + return this.tf.where( newTensor.isNaN(), 0, - newTensor as unknown as tf.TensorLike - ) as tf.Tensor2D + newTensor as unknown as TensorLike + ) as Tensor2D } else { - return tf.where( + return this.tf.where( newTensor.isNaN(), 'missing_value', - newTensor as unknown as tf.TensorLike - ) as tf.Tensor2D + newTensor as unknown as TensorLike + ) as Tensor2D } } - return tf.where( + return this.tf.where( newTensor.isNaN(), this.fillValue, - newTensor as unknown as tf.TensorLike - ) as tf.Tensor2D + newTensor as unknown as TensorLike + ) as Tensor2D } // Not strategy constant const newTensor = convertToNumericTensor2D(X) - return tf.where( + return this.tf.where( newTensor.isNaN(), - this.statistics.reshape([1, -1]) as tf.Tensor2D, + this.statistics.reshape([1, -1]) as Tensor2D, newTensor - ) as tf.Tensor2D + ) as Tensor2D } } diff --git a/src/index.test.ts b/src/index.test.ts index eb1f7291..90146e5a 100644 --- a/src/index.test.ts +++ b/src/index.test.ts @@ -7,8 +7,7 @@ import './ensemble/VotingClassifier.test' import './ensemble/VotingRegressor.test' import './impute/SimpleImputer.test' import './linear_model/LinearRegression.test' -/* When we figure out why we can't save / load logistic regressions */ -// import './linear_model/logisticRegression.test' +import './linear_model/LogisticRegression.test' import './metrics/metrics.test' // import './model_selection/KFold.test' import './model_selection/trainTestSplit.test' diff --git a/src/index.ts b/src/index.ts index 95c1e491..4bc6d661 100644 --- a/src/index.ts +++ b/src/index.ts @@ -12,8 +12,7 @@ * limitations under the License. * ========================================================================== */ -export { KNeighborsRegressor } from './neighbors/KNeighborsRegressor' -export { KNeighborsClassifier } from './neighbors/KNeighborsClassifier' + export { LinearRegression, LinearRegressionParams @@ -70,6 +69,9 @@ export { VotingClassifier, VotingClassifierParams } from './ensemble/VotingClassifier' +export { KNeighborsRegressor } from './neighbors/KNeighborsRegressor' +export { KNeighborsClassifier } from './neighbors/KNeighborsClassifier' + export { LinearSVC, LinearSVCParams } from './svm/LinearSVC' export { LinearSVR, LinearSVRParams } from './svm/LinearSVR' @@ -84,6 +86,8 @@ export { DecisionTreeRegressor, DecisionTreeRegressorParams } from './tree/DecisionTree' +export { makeRegression, makeLowRankMatrix } from './datasets/makeRegression' +export { setBackend, getBackend } from './tf-singleton' export { KFold } from './model_selection/KFold' export { trainTestSplit } from './model_selection/trainTestSplit' export { crossValScore } from './model_selection/crossValScore' diff --git a/src/jestTensorMatchers.test.ts b/src/jestTensorMatchers.test.ts index e1466671..1739ddf0 100644 --- a/src/jestTensorMatchers.test.ts +++ b/src/jestTensorMatchers.test.ts @@ -14,6 +14,9 @@ */ import './jestTensorMatchers' +import * as tf from '@tensorflow/tfjs' +import { setBackend } from './index' +setBackend(tf) describe('Custom Jest Tensor Matchers', () => { it('passes handcrafted tests', () => { diff --git a/src/jestTensorMatchers.ts b/src/jestTensorMatchers.ts index ad16ffff..6f248d63 100644 --- a/src/jestTensorMatchers.ts +++ b/src/jestTensorMatchers.ts @@ -13,7 +13,9 @@ * ========================================================================== */ -import { tf } from './shared/globals' +import { getBackend } from './tf-singleton' +import { Tensor, TensorLike } from './types' +import { isTensor } from './typesUtils' declare global { // eslint-disable-next-line @typescript-eslint/no-namespace @@ -31,9 +33,9 @@ declare global { * iff `|x-y| <= max(|x|, |y|)*rtol + atol`. Set `{rtol: 0, atol: 0}` * for exact equality. */ - toBeAllCloseTo: T extends tf.Tensor | tf.TensorLike + toBeAllCloseTo: T extends Tensor | TensorLike ? ( - expected: tf.Tensor | tf.TensorLike, + expected: Tensor | TensorLike, params?: { rtol?: number atol?: number @@ -54,9 +56,9 @@ declare global { * iff `x-y <= max(|x|, |y|)*rtol + atol`. Set `{rtol: 0, atol: 0}` * for exact equality. */ - toBeAllLessOrClose: T extends tf.Tensor | tf.TensorLike + toBeAllLessOrClose: T extends Tensor | TensorLike ? ( - expected: tf.Tensor | tf.TensorLike, + expected: Tensor | TensorLike, params?: { rtol?: number atol?: number @@ -77,9 +79,9 @@ declare global { * iff `x-y >= -max(|x|, |y|)*rtol - atol`. Set `{rtol: 0, atol: 0}` * for exact equality. */ - toBeAllGreaterOrClose: T extends tf.Tensor | tf.TensorLike + toBeAllGreaterOrClose: T extends Tensor | TensorLike ? ( - expected: tf.Tensor | tf.TensorLike, + expected: Tensor | TensorLike, params?: { rtol?: number atol?: number @@ -100,9 +102,9 @@ declare global { * iff `x-y < -max(|x|, |y|)*rtol - atol`. Set `{rtol: 0, atol: 0}` * for exact equality. */ - toBeAllLessNotClose: T extends tf.Tensor | tf.TensorLike + toBeAllLessNotClose: T extends Tensor | TensorLike ? ( - expected: tf.Tensor | tf.TensorLike, + expected: Tensor | TensorLike, params?: { rtol?: number atol?: number @@ -123,9 +125,9 @@ declare global { * iff `x-y > max(|x|, |y|)*rtol + atol`. Set `{rtol: 0, atol: 0}` * for exact equality. */ - toBeAllGreaterNotClose: T extends tf.Tensor | tf.TensorLike + toBeAllGreaterNotClose: T extends Tensor | TensorLike ? ( - expected: tf.Tensor | tf.TensorLike, + expected: Tensor | TensorLike, params?: { rtol?: number atol?: number @@ -186,15 +188,16 @@ const isLessNotClose = export function toBeAll( this: { isNot: boolean }, - result: tf.TensorLike | tf.Tensor, - expect: tf.TensorLike | tf.Tensor, + result: TensorLike | Tensor, + expect: TensorLike | Tensor, { broadcast = true, allowEmpty = false }, description: string, match: (x: number, y: number) => boolean ) { + let tf = getBackend() const { isNot } = this - const a = result instanceof tf.Tensor ? result : tf.tensor(result) - const b = expect instanceof tf.Tensor ? expect : tf.tensor(expect) + const a = isTensor(result) ? result : tf.tensor(result) + const b = isTensor(expect) ? expect : tf.tensor(expect) const msg = (msg: string) => () => `\nA: ${a.toString(true)}` + @@ -312,8 +315,8 @@ export function toBeAll( export function toBeAllCloseTo( this: { isNot: boolean }, - result: tf.TensorLike | tf.Tensor, - expect: tf.TensorLike | tf.Tensor, + result: TensorLike | Tensor, + expect: TensorLike | Tensor, params: { rtol?: number; atol?: number; broadcast?: boolean } = {} ) { return toBeAll.call( @@ -328,8 +331,8 @@ export function toBeAllCloseTo( export function toBeAllLessOrClose( this: { isNot: boolean }, - result: tf.TensorLike | tf.Tensor, - expect: tf.TensorLike | tf.Tensor, + result: TensorLike | Tensor, + expect: TensorLike | Tensor, params: { rtol?: number; atol?: number; broadcast?: boolean } = {} ) { return toBeAll.call( @@ -344,8 +347,8 @@ export function toBeAllLessOrClose( export function toBeAllGreaterOrClose( this: { isNot: boolean }, - result: tf.TensorLike | tf.Tensor, - expect: tf.TensorLike | tf.Tensor, + result: TensorLike | Tensor, + expect: TensorLike | Tensor, params: { rtol?: number; atol?: number; broadcast?: boolean } = {} ) { const le = isLessOrClose(params) @@ -361,8 +364,8 @@ export function toBeAllGreaterOrClose( export function toBeAllLessNotClose( this: { isNot: boolean }, - result: tf.TensorLike | tf.Tensor, - expect: tf.TensorLike | tf.Tensor, + result: TensorLike | Tensor, + expect: TensorLike | Tensor, params: { rtol?: number; atol?: number; broadcast?: boolean } = {} ) { return toBeAll.call( @@ -377,8 +380,8 @@ export function toBeAllLessNotClose( export function toBeAllGreaterNotClose( this: { isNot: boolean }, - result: tf.TensorLike | tf.Tensor, - expect: tf.TensorLike | tf.Tensor, + result: TensorLike | Tensor, + expect: TensorLike | Tensor, params: { rtol?: number; atol?: number; broadcast?: boolean } = {} ) { const le = isLessNotClose(params) diff --git a/src/linear_model/ElasticNet.ts b/src/linear_model/ElasticNet.ts index a35f35b6..3cf406b4 100644 --- a/src/linear_model/ElasticNet.ts +++ b/src/linear_model/ElasticNet.ts @@ -14,7 +14,7 @@ */ import { SGDRegressor } from './SgdRegressor' -import { tf } from '../shared/globals' +import { getBackend } from '../tf-singleton' // First pass at a ElasticNet implementation using gradient descent // Trying to mimic the API of https://scikit-learn.org/stable/modules/generated/sklearn.linear_model.ElasticNet.html#sklearn.linear_model.ElasticNet @@ -33,14 +33,12 @@ export interface ElasticNetParams { * Linear regression with combined L1 and L2 priors as regularizer. */ export class ElasticNet extends SGDRegressor { - /** Useful for pipelines and column transformers to have a default name for transforms */ - name = 'ElasticNet' - constructor({ alpha = 1, l1Ratio = 0.5, fitIntercept = true }: ElasticNetParams = {}) { + let tf = getBackend() super({ modelCompileArgs: { optimizer: tf.train.adam(0.1), @@ -66,5 +64,7 @@ export class ElasticNet extends SGDRegressor { optimizerType: 'adam', lossType: 'meanSquaredError' }) + /** Useful for pipelines and column transformers to have a default name for transforms */ + this.name = 'ElasticNet' } } diff --git a/src/linear_model/LassoRegression.ts b/src/linear_model/LassoRegression.ts index 32f1438c..8ed933ba 100644 --- a/src/linear_model/LassoRegression.ts +++ b/src/linear_model/LassoRegression.ts @@ -14,7 +14,7 @@ */ import { SGDRegressor } from './SgdRegressor' -import { tf } from '../shared/globals' +import { getBackend } from '../tf-singleton' // First pass at a LassoRegression implementation using gradient descent // Trying to mimic the API of https://scikit-learn.org/stable/modules/generated/sklearn.linear_model.Lasso.html#sklearn.linear_model.Lasso @@ -28,10 +28,8 @@ export interface LassoParams { /** Linear Model trained with L1 prior as regularizer (aka the Lasso). */ export class LassoRegression extends SGDRegressor { - /** Useful for pipelines and column transformers to have a default name for transforms */ - name = 'LassoRegression' - constructor({ fitIntercept = true, alpha = 1.0 }: LassoParams = {}) { + let tf = getBackend() super({ modelCompileArgs: { optimizer: tf.train.adam(0.1), @@ -54,5 +52,7 @@ export class LassoRegression extends SGDRegressor { optimizerType: 'adam', lossType: 'meanSquaredError' }) + /** Useful for pipelines and column transformers to have a default name for transforms */ + this.name = 'LassoRegression' } } diff --git a/src/linear_model/LinearRegression.test.ts b/src/linear_model/LinearRegression.test.ts index 1a85e235..2e54a970 100644 --- a/src/linear_model/LinearRegression.test.ts +++ b/src/linear_model/LinearRegression.test.ts @@ -1,6 +1,10 @@ -import { LinearRegression, fromJSON } from '../index' +import { LinearRegression, setBackend, fromJSON, fromObject } from '../index' import { tensorEqual } from '../utils' -import { tf } from '../shared/globals' +// Needs to be tfjs and not tfjs-node because the node +// version messes with jest unit tests +import * as tf from '@tensorflow/tfjs' +setBackend(tf) + function roughlyEqual(a: number, b: number, tol = 0.1) { return Math.abs(a - b) < tol } @@ -147,8 +151,8 @@ describe('LinearRegression', function () { const lr = new LinearRegression({ fitIntercept: false }) await lr.fit(mediumX, yPlusJitter) - const serialized = await lr.toJSON() - const newModel = await fromJSON(serialized) + const serialized = await lr.toObject() + const newModel = await fromObject(serialized) expect(tensorEqual(newModel.coef, tf.tensor1d([2.5, 1]), 0.1)).toBe(true) expect(roughlyEqual(newModel.intercept as number, 0)).toBe(true) diff --git a/src/linear_model/LinearRegression.ts b/src/linear_model/LinearRegression.ts index 3c4e099c..1913ed2f 100644 --- a/src/linear_model/LinearRegression.ts +++ b/src/linear_model/LinearRegression.ts @@ -14,7 +14,7 @@ */ import { SGDRegressor } from './SgdRegressor' -import { tf } from '../shared/globals' +import { getBackend } from '../tf-singleton' /** * LinearRegression implementation using gradient descent @@ -66,10 +66,8 @@ Next steps: * ``` */ export class LinearRegression extends SGDRegressor { - /** Useful for pipelines and column transformers to have a default name for transforms */ - name = 'LinearRegression' - constructor({ fitIntercept = true }: LinearRegressionParams = {}) { + let tf = getBackend() super({ modelCompileArgs: { optimizer: tf.train.adam(0.1), @@ -91,5 +89,7 @@ export class LinearRegression extends SGDRegressor { optimizerType: 'adam', lossType: 'meanSquaredError' }) + /** Useful for pipelines and column transformers to have a default name for transforms */ + this.name = 'LinearRegression' } } diff --git a/src/linear_model/LogisticRegression.test.ts b/src/linear_model/LogisticRegression.test.ts index be3fe8e1..1d38ba59 100644 --- a/src/linear_model/LogisticRegression.test.ts +++ b/src/linear_model/LogisticRegression.test.ts @@ -1,5 +1,7 @@ -import { LogisticRegression, fromJSON } from '../index' -import { tf } from '../shared/globals' +import { LogisticRegression, setBackend, fromJSON } from '../index' +import * as tf from '@tensorflow/tfjs' +setBackend(tf) + describe('LogisticRegression', function () { it('Works on arrays (small example)', async function () { const lr = new LogisticRegression() @@ -90,7 +92,7 @@ describe('LogisticRegression', function () { expect(finalResults.arraySync()).toEqual([0, 0, 0, 1, 1, 1, 2, 2, 2]) }, 30000) - it('Should save and load trained model', async function () { + it('Should save and load trained model', async function () { let X = [ [0, -1], [1, 0], diff --git a/src/linear_model/LogisticRegression.ts b/src/linear_model/LogisticRegression.ts index 30651225..159cd366 100644 --- a/src/linear_model/LogisticRegression.ts +++ b/src/linear_model/LogisticRegression.ts @@ -14,7 +14,7 @@ // */ import { SGDClassifier } from './SgdClassifier' -import { tf } from '../shared/globals' +import { getBackend } from '../tf-singleton' // First pass at a LogisticRegression implementation using gradient descent // Trying to mimic the API of scikit-learn.org/stable/modules/generated/sklearn.linear_model.LogisticRegression.html @@ -60,9 +60,6 @@ export interface LogisticRegressionParams { * ``` */ export class LogisticRegression extends SGDClassifier { - /** Useful for pipelines and column transformers to have a default name for transforms */ - name = 'LogisticRegression' - constructor({ penalty = 'l2', C = 1, @@ -70,6 +67,7 @@ export class LogisticRegression extends SGDClassifier { }: LogisticRegressionParams = {}) { // Assume Binary classification // If we call fit, and it isn't binary then update args + let tf = getBackend() super({ modelCompileArgs: { optimizer: tf.train.adam(0.1), @@ -101,5 +99,7 @@ export class LogisticRegression extends SGDClassifier { optimizerType: 'adam', lossType: 'softmaxCrossEntropy' }) + /** Useful for pipelines and column transformers to have a default name for transforms */ + this.name = 'LogisticRegression' } } diff --git a/src/linear_model/RidgeRegression.ts b/src/linear_model/RidgeRegression.ts index 1d3cbf72..d0ab33ed 100644 --- a/src/linear_model/RidgeRegression.ts +++ b/src/linear_model/RidgeRegression.ts @@ -14,7 +14,7 @@ */ import { SGDRegressor } from './SgdRegressor' -import { tf } from '../shared/globals' +import { getBackend } from '../tf-singleton' // RidgeRegression implementation using gradient descent // This is a placeholder until we can do an analytic solution instead @@ -30,13 +30,11 @@ export interface RidgeRegressionParams { /** Linear least squares with l2 regularization. */ export class RidgeRegression extends SGDRegressor { - /** Useful for pipelines and column transformers to have a default name for transforms */ - name = 'RidgeRegression' - constructor({ fitIntercept = true, alpha = 0.01 }: RidgeRegressionParams = {}) { + let tf = getBackend() super({ modelCompileArgs: { optimizer: tf.train.adam(0.1), @@ -59,5 +57,7 @@ export class RidgeRegression extends SGDRegressor { optimizerType: 'adam', lossType: 'meanSquaredError' }) + /** Useful for pipelines and column transformers to have a default name for transforms */ + this.name = 'RidgeRegression' } } diff --git a/src/linear_model/SgdClassifier.ts b/src/linear_model/SgdClassifier.ts index 06f3f539..56a55e16 100644 --- a/src/linear_model/SgdClassifier.ts +++ b/src/linear_model/SgdClassifier.ts @@ -13,13 +13,23 @@ * ========================================================================== */ -import { tf } from '../shared/globals' -// import { DenseLayerArgs } from '@tensorflow/tfjs-layers/dist/layers/core' import { convertToNumericTensor1D, convertToNumericTensor2D } from '../utils' -import { Scikit2D, Scikit1D, OptimizerTypes, LossTypes } from '../types' +import { + Scikit2D, + Scikit1D, + OptimizerTypes, + LossTypes, + Tensor1D, + Tensor2D, + Tensor, + ModelCompileArgs, + ModelFitArgs, + RecursiveArray +} from '../types' import { OneHotEncoder } from '../preprocessing/OneHotEncoder' import { assert } from '../typesUtils' import { ClassifierMixin } from '../mixins' +import { getBackend } from '../tf-singleton' /** * SGD is a thin Wrapper around Tensorflow's model api with a single dense layer. @@ -42,7 +52,7 @@ export interface SGDClassifierParams { metrics: ['mse'], }) */ - modelCompileArgs: tf.ModelCompileArgs + modelCompileArgs: ModelCompileArgs /** * The complete list of `model.fit` args from Tensorflow.js @@ -55,7 +65,7 @@ export interface SGDClassifierParams { callbacks: [callbacks.earlyStopping({ monitor: 'mse', patience: 50 })], }) */ - modelFitArgs: tf.ModelFitArgs + modelFitArgs: ModelFitArgs /** * The arguments for a single dense layer in tensorflow. This also defaults to @@ -85,14 +95,14 @@ export interface SGDClassifierParams { } export class SGDClassifier extends ClassifierMixin { - model: tf.Sequential - modelFitArgs: tf.ModelFitArgs - modelCompileArgs: tf.ModelCompileArgs + model: any //tf.Sequential + modelFitArgs: ModelFitArgs + modelCompileArgs: ModelCompileArgs denseLayerArgs: any //DenseLayerArgs optimizerType: OptimizerTypes lossType: LossTypes - oneHot: OneHotEncoder + tf: any constructor({ modelFitArgs, @@ -102,7 +112,8 @@ export class SGDClassifier extends ClassifierMixin { lossType }: SGDClassifierParams) { super() - this.model = tf.sequential() + this.tf = getBackend() + this.model = this.tf.sequential() this.modelFitArgs = modelFitArgs this.modelCompileArgs = modelCompileArgs this.denseLayerArgs = denseLayerArgs @@ -114,22 +125,22 @@ export class SGDClassifier extends ClassifierMixin { this.oneHot = new OneHotEncoder() } - initializeModelForClassification(y: tf.Tensor1D | tf.Tensor2D): tf.Tensor2D { + initializeModelForClassification(y: Tensor1D | Tensor2D): Tensor2D { let yToInt = y.toInt() // This covers the case of a dependent variable that is already one hot encoded. // There are other cases where you do "multi-variable output which isn't one hot encoded" // Like say you were predicting which diseases a person could have (hasCancer, hasMeningitis, etc) // Then you would have to run a sigmoid on each independent variable if (yToInt.shape.length === 2) { - this.modelCompileArgs.loss = tf.losses.softmaxCrossEntropy - return yToInt as tf.Tensor2D + this.modelCompileArgs.loss = this.tf.losses.softmaxCrossEntropy + return yToInt as Tensor2D } else { - const yTwoD = y.reshape([-1, 1]) as tf.Tensor2D + const yTwoD = y.reshape([-1, 1]) as Tensor2D const yTwoDOneHotEncoded = this.oneHot.fitTransform(yTwoD) if (this.oneHot.categories[0].length > 2) { - this.modelCompileArgs.loss = tf.losses.softmaxCrossEntropy + this.modelCompileArgs.loss = this.tf.losses.softmaxCrossEntropy } else { - this.modelCompileArgs.loss = tf.losses.sigmoidCrossEntropy + this.modelCompileArgs.loss = this.tf.losses.sigmoidCrossEntropy } return yTwoDOneHotEncoded } @@ -148,14 +159,17 @@ export class SGDClassifier extends ClassifierMixin { */ initializeModel( - X: tf.Tensor2D, - y: tf.Tensor1D | tf.Tensor2D, - weightsTensors: tf.Tensor[] = [] + X: Tensor2D, + y: Tensor1D | Tensor2D, + weightsTensors: Tensor[] = [] ): void { this.denseLayerArgs.units = y.shape.length === 1 ? 1 : y.shape[1] - const model = tf.sequential() + const model = this.tf.sequential() model.add( - tf.layers.dense({ inputShape: [X.shape[1]], ...this.denseLayerArgs }) + this.tf.layers.dense({ + inputShape: [X.shape[1]], + ...this.denseLayerArgs + }) ) model.compile(this.modelCompileArgs) if (weightsTensors?.length) { @@ -223,8 +237,12 @@ export class SGDClassifier extends ClassifierMixin { importModel(params: { coef: number[]; intercept: number }): SGDClassifier { // Next steps: Need to update for possible 2D coef case, and 1D intercept case - let myCoef = tf.tensor2d(params.coef, [params.coef.length, 1], 'float32') - let myIntercept = tf.tensor1d([params.intercept], 'float32') + let myCoef = this.tf.tensor2d( + params.coef, + [params.coef.length, 1], + 'float32' + ) + let myIntercept = this.tf.tensor1d([params.intercept], 'float32') this.initializeModel(myCoef, myIntercept, [myCoef, myIntercept]) return this } @@ -295,10 +313,10 @@ export class SGDClassifier extends ClassifierMixin { return this } - public predictProba(X: Scikit2D): tf.Tensor2D { + public predictProba(X: Scikit2D): Tensor2D { assert(this.model.layers.length > 0, 'Need to call "fit" before "predict"') let XTwoD = convertToNumericTensor2D(X) - return this.model.predict(XTwoD) as tf.Tensor2D + return this.model.predict(XTwoD) as Tensor2D } /** * Similar to scikit-learn, this returns a Tensor2D (2D Matrix) of predictions. @@ -323,10 +341,10 @@ export class SGDClassifier extends ClassifierMixin { * // => tensor2d([[ 4.5, 10.3, 19.1, 0.22 ]]) */ - public predict(X: Scikit2D): tf.Tensor1D { + public predict(X: Scikit2D): Tensor1D { assert(this.model.layers.length > 0, 'Need to call "fit" before "predict"') const y2D = this.predictProba(X) - return tf.tensor1d(this.oneHot.inverseTransform(y2D)) + return this.tf.tensor1d(this.oneHot.inverseTransform(y2D)) } /** @@ -352,16 +370,16 @@ export class SGDClassifier extends ClassifierMixin { */ - get coef(): tf.Tensor1D | tf.Tensor2D { + get coef(): Tensor1D | Tensor2D { const modelWeights = this.model.getWeights() if (modelWeights.length === 0) { - return tf.tensor2d([]) + return this.tf.tensor2d([]) } let coefficients = modelWeights[0] if (coefficients.shape[1] === 1) { - return coefficients.reshape([coefficients.shape[0]]) as tf.Tensor1D + return coefficients.reshape([coefficients.shape[0]]) as Tensor1D } - return coefficients as tf.Tensor2D + return coefficients as Tensor2D } /** @@ -388,12 +406,12 @@ export class SGDClassifier extends ClassifierMixin { * lr.intercept * // => tensor1d([1.2, 2.3]) */ - get intercept(): number | tf.Tensor1D { + get intercept(): number | Tensor1D { const modelWeights = this.model.getWeights() if (modelWeights.length < 2) { return 0.0 } - let intercept = modelWeights[1] as tf.Tensor1D + let intercept = modelWeights[1] as Tensor1D if (intercept.size === 1) { return intercept.arraySync()[0] } @@ -401,7 +419,9 @@ export class SGDClassifier extends ClassifierMixin { return intercept } - private getModelWeight(): Promise> { - return Promise.all(this.model.getWeights().map((weight) => weight.array())) + private getModelWeight(): Promise> { + return Promise.all( + this.model.getWeights().map((weight: any) => weight.array()) + ) } } diff --git a/src/linear_model/SgdRegressor.ts b/src/linear_model/SgdRegressor.ts index e178889f..b089b552 100644 --- a/src/linear_model/SgdRegressor.ts +++ b/src/linear_model/SgdRegressor.ts @@ -12,14 +12,23 @@ * limitations under the License. * ========================================================================== */ -import { tf } from '../shared/globals' -// import { DenseLayerArgs } from '@tensorflow/tfjs-layers/dist/layers/core' import { convertToNumericTensor1D_2D, convertToNumericTensor2D } from '../utils' -import { Scikit2D, Scikit1D, OptimizerTypes, LossTypes } from '../types' +import { + Scikit2D, + Scikit1D, + OptimizerTypes, + LossTypes, + Tensor1D, + Tensor2D, + Tensor, + ModelCompileArgs, + ModelFitArgs +} from '../types' import { RegressorMixin } from '../mixins' +import { getBackend } from '../tf-singleton' /** * SGD is a thin Wrapper around Tensorflow's model api with a single dense layer. @@ -42,7 +51,7 @@ export interface SGDRegressorParams { metrics: ['mse'], }) */ - modelCompileArgs: tf.ModelCompileArgs + modelCompileArgs: ModelCompileArgs /** * The complete list of `model.fit` args from Tensorflow.js @@ -55,7 +64,7 @@ export interface SGDRegressorParams { callbacks: [callbacks.earlyStopping({ monitor: 'mse', patience: 50 })], }) */ - modelFitArgs: tf.ModelFitArgs + modelFitArgs: ModelFitArgs /** * The arguments for a single dense layer in tensorflow. This also defaults to @@ -85,9 +94,9 @@ export interface SGDRegressorParams { } export class SGDRegressor extends RegressorMixin { - model: tf.Sequential - modelFitArgs: tf.ModelFitArgs - modelCompileArgs: tf.ModelCompileArgs + model: any //this.tf.Sequential + modelFitArgs: ModelFitArgs + modelCompileArgs: ModelCompileArgs denseLayerArgs: any //DenseLayerArgs isMultiOutput: boolean optimizerType: OptimizerTypes @@ -101,7 +110,8 @@ export class SGDRegressor extends RegressorMixin { lossType }: SGDRegressorParams) { super() - this.model = tf.sequential() + this.tf = getBackend() + this.model = this.tf.sequential() this.modelFitArgs = modelFitArgs this.modelCompileArgs = modelCompileArgs this.denseLayerArgs = denseLayerArgs @@ -123,14 +133,17 @@ export class SGDRegressor extends RegressorMixin { */ initializeModel( - X: tf.Tensor2D, - y: tf.Tensor1D | tf.Tensor2D, - weightsTensors: tf.Tensor[] = [] + X: Tensor2D, + y: Tensor1D | Tensor2D, + weightsTensors: Tensor[] = [] ): void { this.denseLayerArgs.units = y.shape.length === 1 ? 1 : y.shape[1] - const model = tf.sequential() + const model = this.tf.sequential() model.add( - tf.layers.dense({ inputShape: [X.shape[1]], ...this.denseLayerArgs }) + this.tf.layers.dense({ + inputShape: [X.shape[1]], + ...this.denseLayerArgs + }) ) model.compile(this.modelCompileArgs) if (weightsTensors?.length) { @@ -202,8 +215,12 @@ export class SGDRegressor extends RegressorMixin { importModel(params: { coef: number[]; intercept: number }): SGDRegressor { // Next steps: Need to update for possible 2D coef case, and 1D intercept case - let myCoef = tf.tensor2d(params.coef, [params.coef.length, 1], 'float32') - let myIntercept = tf.tensor1d([params.intercept], 'float32') + let myCoef = this.tf.tensor2d( + params.coef, + [params.coef.length, 1], + 'float32' + ) + let myIntercept = this.tf.tensor1d([params.intercept], 'float32') this.initializeModel(myCoef, myIntercept, [myCoef, myIntercept]) return this } @@ -297,16 +314,16 @@ export class SGDRegressor extends RegressorMixin { * // => tensor2d([[ 4.5, 10.3, 19.1, 0.22 ]]) */ - public predict(X: Scikit2D): tf.Tensor1D | tf.Tensor2D { + public predict(X: Scikit2D): Tensor1D | Tensor2D { let XTwoD = convertToNumericTensor2D(X) if (this.model.layers.length === 0) { throw new RangeError('Need to call "fit" before "predict"') } - const predictions = this.model.predict(XTwoD) as tf.Tensor2D + const predictions = this.model.predict(XTwoD) as Tensor2D if (!this.isMultiOutput) { - return predictions.reshape([-1]) as tf.Tensor1D + return predictions.reshape([-1]) as Tensor1D } - return predictions as tf.Tensor2D + return predictions as Tensor2D } /** @@ -332,16 +349,16 @@ export class SGDRegressor extends RegressorMixin { */ - get coef(): tf.Tensor1D | tf.Tensor2D { + get coef(): Tensor1D | Tensor2D { const modelWeights = this.model.getWeights() if (modelWeights.length === 0) { - return tf.tensor2d([]) + return this.tf.tensor2d([]) } let coefficients = modelWeights[0] if (coefficients.shape[1] === 1) { - return coefficients.reshape([coefficients.shape[0]]) as tf.Tensor1D + return coefficients.reshape([coefficients.shape[0]]) as Tensor1D } - return coefficients as tf.Tensor2D + return coefficients as Tensor2D } /** @@ -368,12 +385,12 @@ export class SGDRegressor extends RegressorMixin { * lr.intercept * // => tensor1d([1.2, 2.3]) */ - get intercept(): number | tf.Tensor1D { + get intercept(): number | Tensor1D { const modelWeights = this.model.getWeights() if (modelWeights.length < 2) { return 0.0 } - let intercept = modelWeights[1] as tf.Tensor1D + let intercept = modelWeights[1] as Tensor1D if (intercept.size === 1) { return intercept.arraySync()[0] } diff --git a/src/math.ts b/src/math.ts index 1234efa2..794f63f1 100644 --- a/src/math.ts +++ b/src/math.ts @@ -1,6 +1,6 @@ -import { Iterable } from './types' +import { Iterable, Tensor } from './types' import { assert } from './typesUtils' -import { tf } from './shared/globals' +import { getBackend } from './tf-singleton' /* In creating the preprocessors, I wanted functions that computed the min, max, mean, etc... but that also ignored NaN values. The min / max functions that come from @@ -44,10 +44,11 @@ export function simpleMin>( } export function tensorMin( - tensor: tf.Tensor, + tensor: Tensor, axis: number, ignoreNaN: boolean -): tf.Tensor { +): Tensor { + let tf = getBackend() if (ignoreNaN) { return tf.tidy(() => tf.where(tensor.isNaN(), Infinity, tensor).min(axis)) } @@ -80,10 +81,11 @@ export function simpleMax>( } export function tensorMax( - tensor: tf.Tensor, + tensor: Tensor, axis: number, ignoreNaN?: boolean -): tf.Tensor { +): Tensor { + let tf = getBackend() if (ignoreNaN) { return tf.tidy(() => tf.where(tensor.isNaN(), -Infinity, tensor).max(axis)) } @@ -109,11 +111,8 @@ export function simpleSum>( return total } -export function tensorSum( - tensor: tf.Tensor, - axis: number, - ignoreNaN?: boolean -) { +export function tensorSum(tensor: Tensor, axis: number, ignoreNaN?: boolean) { + let tf = getBackend() if (ignoreNaN) { return tf.tidy(() => tf.where(tensor.isNaN(), 0, tensor).sum(axis)) } @@ -144,10 +143,11 @@ export function simpleCount>( } export function tensorCount( - tensor: tf.Tensor, + tensor: Tensor, axis: number, ignoreNaN?: boolean ) { + let tf = getBackend() if (ignoreNaN) { return tf.tidy(() => tf.logicalNot(tensor.isNaN()).sum(axis)) } @@ -172,11 +172,12 @@ export function simpleMean>( } export function tensorMean( - tensor: tf.Tensor, + tensor: Tensor, axis: number, ignoreNaN?: boolean, safe?: boolean ) { + let tf = getBackend() if (!ignoreNaN) { return tensor.mean(axis) } @@ -200,16 +201,12 @@ export function tensorMean( // Std Functions ////////////////////////////////////////////////////////////////////////// -export function tensorStd( - tensor: tf.Tensor, - dim: number, - ignoreNaN?: boolean -) { +export function tensorStd(tensor: Tensor, dim: number, ignoreNaN?: boolean) { assert( Boolean(ignoreNaN), 'We only need to call this function when ignoreNaN is true' ) - + let tf = getBackend() return tf.tidy(() => { const mean = tensorMean(tensor, dim, ignoreNaN) const countNaN = tensorCount(tensor, dim, ignoreNaN) @@ -223,7 +220,8 @@ export function tensorStd( }) } -export function turnZerosToOnes(tensor: tf.Tensor) { +export function turnZerosToOnes(tensor: Tensor) { + let tf = getBackend() return tf.tidy(() => { const zeros = tf.zerosLike(tensor) const booleanAddition = tensor.equal(zeros) diff --git a/src/metrics/metrics.test.ts b/src/metrics/metrics.test.ts index bf637de5..771fbc41 100644 --- a/src/metrics/metrics.test.ts +++ b/src/metrics/metrics.test.ts @@ -1,4 +1,7 @@ import * as metrics from './metrics' +import { setBackend } from '../index' +import * as tf from '@tensorflow/tfjs' +setBackend(tf) describe('Metrics', function () { it('accuracyScore', function () { diff --git a/src/metrics/metrics.ts b/src/metrics/metrics.ts index 3c03575f..e944f9ae 100644 --- a/src/metrics/metrics.ts +++ b/src/metrics/metrics.ts @@ -17,8 +17,7 @@ import { convertToNumericTensor1D } from '../utils' import { Scikit1D } from '../types' import { assert, isScikit1D } from '../typesUtils' import uniq from 'lodash/uniq' - -import { tf } from '../shared/globals' +import { getBackend } from '../tf-singleton' function assertInputIsWellFormed(labels: Scikit1D, predictions: Scikit1D) { assert(isScikit1D(labels), "Labels can't be converted to a 1D Tensor") @@ -64,6 +63,7 @@ export function accuracyScore( } export function precisionScore(labels: Scikit1D, predictions: Scikit1D) { + let tf = getBackend() const { labelsT, predictionsT } = assertInputIsWellFormed( labels, predictions @@ -73,6 +73,7 @@ export function precisionScore(labels: Scikit1D, predictions: Scikit1D) { } export function recallScore(labels: Scikit1D, predictions: Scikit1D) { + let tf = getBackend() const { labelsT, predictionsT } = assertInputIsWellFormed( labels, predictions @@ -82,6 +83,7 @@ export function recallScore(labels: Scikit1D, predictions: Scikit1D) { } export function r2Score(labels: Scikit1D, predictions: Scikit1D) { + let tf = getBackend() const { labelsT, predictionsT } = assertInputIsWellFormed( labels, predictions @@ -102,6 +104,7 @@ export function meanAbsoluteError( labels: Scikit1D, predictions: Scikit1D ): number { + let tf = getBackend() const { labelsT, predictionsT } = assertInputIsWellFormed( labels, predictions @@ -111,6 +114,7 @@ export function meanAbsoluteError( } export function meanSquaredError(labels: Scikit1D, predictions: Scikit1D) { + let tf = getBackend() const { labelsT, predictionsT } = assertInputIsWellFormed( labels, predictions @@ -120,6 +124,7 @@ export function meanSquaredError(labels: Scikit1D, predictions: Scikit1D) { } export function meanSquaredLogError(labels: Scikit1D, predictions: Scikit1D) { + let tf = getBackend() const { labelsT, predictionsT } = assertInputIsWellFormed( labels, predictions @@ -132,6 +137,7 @@ export function meanSquaredLogError(labels: Scikit1D, predictions: Scikit1D) { } export function hingeLoss(labels: Scikit1D, predictions: Scikit1D) { + let tf = getBackend() const { labelsT, predictionsT } = assertInputIsWellFormed( labels, predictions @@ -141,6 +147,7 @@ export function hingeLoss(labels: Scikit1D, predictions: Scikit1D) { } export function huberLoss(labels: Scikit1D, predictions: Scikit1D) { + let tf = getBackend() const { labelsT, predictionsT } = assertInputIsWellFormed( labels, predictions @@ -150,6 +157,7 @@ export function huberLoss(labels: Scikit1D, predictions: Scikit1D) { } export function logLoss(labels: Scikit1D, predictions: Scikit1D) { + let tf = getBackend() const { labelsT, predictionsT } = assertInputIsWellFormed( labels, predictions @@ -159,6 +167,7 @@ export function logLoss(labels: Scikit1D, predictions: Scikit1D) { } export function zeroOneLoss(labels: Scikit1D, predictions: Scikit1D) { + let tf = getBackend() const { labelsT, predictionsT } = assertInputIsWellFormed( labels, predictions @@ -172,6 +181,7 @@ export function zeroOneLoss(labels: Scikit1D, predictions: Scikit1D) { ////////////////////////////////////// export function confusionMatrix(labels: Scikit1D, predictions: Scikit1D) { + let tf = getBackend() const { labelsT, predictionsT } = assertInputIsWellFormed( labels, predictions diff --git a/src/mixins.ts b/src/mixins.ts index 20085282..2d71b816 100644 --- a/src/mixins.ts +++ b/src/mixins.ts @@ -1,12 +1,11 @@ -import { Scikit2D, Scikit1D } from './types' +import { Scikit2D, Scikit1D, Tensor2D, Tensor1D } from './types' import { r2Score, accuracyScore } from './metrics/metrics' import { Serialize } from './simpleSerializer' -import { tf } from './shared/globals' export class TransformerMixin extends Serialize { // We assume that fit and transform exist [x: string]: any - public fitTransform(X: Scikit2D): tf.Tensor2D { + public fitTransform(X: Scikit2D): Tensor2D { return this.fit(X).transform(X) } } @@ -15,7 +14,7 @@ export class PredictorMixin { // We assume that fit and predict exist [x: string]: any - public fitPredict(X: Scikit2D, y: Scikit1D): tf.Tensor1D { + public fitPredict(X: Scikit2D, y: Scikit1D): Tensor1D { return this.fit(X, y).predict(X) } } diff --git a/src/model_selection/CrossValidator.ts b/src/model_selection/CrossValidator.ts index ceba577c..a74a109c 100644 --- a/src/model_selection/CrossValidator.ts +++ b/src/model_selection/CrossValidator.ts @@ -13,9 +13,7 @@ * ========================================================================== */ -import { Scikit1D, Scikit2D } from '../types' -import { tf } from '../shared/globals' -type Tensor1D = tf.Tensor1D +import { Scikit1D, Scikit2D, Tensor1D } from '../types' /** * Interface for cross validation splitting strategies. diff --git a/src/model_selection/KFold.test.ts b/src/model_selection/KFold.test.ts index 3b0678fb..8093601a 100644 --- a/src/model_selection/KFold.test.ts +++ b/src/model_selection/KFold.test.ts @@ -14,11 +14,12 @@ */ import * as fc from 'fast-check' -import { KFold } from '../index' +import { KFold, setBackend } from '../index' import { alea } from '../randUtils' +import { Tensor2D } from '../types' import '../jestTensorMatchers' -import { tf } from '../shared/globals' -type Tensor2D = tf.Tensor2D +import * as tf from '@tensorflow/tfjs' +setBackend(tf) describe('KFold', () => { const numRuns = 128 diff --git a/src/model_selection/KFold.ts b/src/model_selection/KFold.ts index cf67abbb..86c338d9 100644 --- a/src/model_selection/KFold.ts +++ b/src/model_selection/KFold.ts @@ -16,10 +16,9 @@ import { assert } from '../typesUtils' import { CrossValidator } from './CrossValidator' import * as randUtils from '../randUtils' -import { Scikit1D, Scikit2D } from '../types' +import { Scikit1D, Scikit2D, Tensor1D } from '../types' import { getLength } from '../utils' -import { tf } from '../shared/globals' -type Tensor1D = tf.Tensor1D +import { getBackend } from '../tf-singleton' export interface KFoldParams { /** @@ -82,6 +81,7 @@ export class KFold implements CrossValidator { shuffle: boolean randomState?: number name: string + tf: any constructor({ nSplits = 5, shuffle = false, @@ -92,6 +92,7 @@ export class KFold implements CrossValidator { Number.isInteger(nSplits) && nSplits > 1, 'new KFold({nSplits}): nSplits must be an int greater than 1.' ) + this.tf = getBackend() this.nSplits = nSplits this.shuffle = Boolean(shuffle) this.randomState = randomState @@ -153,8 +154,8 @@ export class KFold implements CrossValidator { const test = range.slice(offset, offset + chunk) yield { - trainIndex: tf.tensor1d(train, 'int32'), - testIndex: tf.tensor1d(test, 'int32') + trainIndex: this.tf.tensor1d(train, 'int32'), + testIndex: this.tf.tensor1d(test, 'int32') } offset += chunk diff --git a/src/model_selection/crossValScore.ts b/src/model_selection/crossValScore.ts index 7d0b551c..c7cf00e1 100644 --- a/src/model_selection/crossValScore.ts +++ b/src/model_selection/crossValScore.ts @@ -16,13 +16,10 @@ import { assert } from '../typesUtils' import { CrossValidator } from './CrossValidator' import { KFold } from './KFold' -import { Scikit1D, Scikit2D } from '../types' +import { Scikit1D, Scikit2D, Scalar, Tensor1D, Tensor2D } from '../types' import { isScikit1D } from '../typesUtils' import { convertToTensor1D, convertToTensor2D } from '../utils' -import { tf } from '../shared/globals' -type Scalar = tf.Scalar -type Tensor1D = tf.Tensor1D -type Tensor2D = tf.Tensor2D +import { getBackend } from '../tf-singleton' /** * Evaluates a score by cross-validation. This particular overload @@ -176,6 +173,7 @@ export async function crossValScore( scoring?: any } ): Promise { + let tf = getBackend() let unsupervised = y == null || (params == null && !isScikit1D(y)) if (unsupervised) { params = params ?? y diff --git a/src/model_selection/scorers.ts b/src/model_selection/scorers.ts index 0342d1cc..b637b907 100644 --- a/src/model_selection/scorers.ts +++ b/src/model_selection/scorers.ts @@ -13,11 +13,9 @@ * ========================================================================== */ -import { Scikit1D, Scikit2D } from '../types' +import { getBackend } from '../tf-singleton' +import { Scikit1D, Scikit2D, Scalar, Tensor1D } from '../types' import { convertToTensor1D } from '../utils' -import { tf } from '../shared/globals' -type Scalar = tf.Scalar -type Tensor1D = tf.Tensor1D export function negMeanAbsoluteError( this: { @@ -26,6 +24,7 @@ export function negMeanAbsoluteError( X: Scikit2D, y: Scikit1D ): Scalar { + let tf = getBackend() return tf.tidy(() => { y = convertToTensor1D(y) const yPred = this.predict(X) @@ -40,6 +39,7 @@ export function negMeanSquaredError( X: Scikit2D, y: Scikit1D ): Scalar { + let tf = getBackend() return tf.tidy(() => { y = convertToTensor1D(y) const yPred = this.predict(X) @@ -54,6 +54,7 @@ export function accuracy( X: Scikit2D, y: Scikit1D ): Scalar { + let tf = getBackend() return tf.tidy(() => { y = convertToTensor1D(y) const yPred = this.predict(X) diff --git a/src/model_selection/trainTestSplit.test.ts b/src/model_selection/trainTestSplit.test.ts index 3673b9f8..392d8f62 100644 --- a/src/model_selection/trainTestSplit.test.ts +++ b/src/model_selection/trainTestSplit.test.ts @@ -1,12 +1,9 @@ -import { - trainTestSplit, - validateShuffleSplit, - getIndices -} from './trainTestSplit' - +import { validateShuffleSplit, getIndices } from './trainTestSplit' +import { trainTestSplit, setBackend } from '../index' import * as dfd from 'danfojs-node' -import { tf } from '../shared/globals' +import * as tf from '@tensorflow/tfjs' import { DataFrameInterface } from '../types' +setBackend(tf) describe('Testing trainTestSplit', function () { it('Testing train/test validation logic', () => { diff --git a/src/model_selection/trainTestSplit.ts b/src/model_selection/trainTestSplit.ts index b99bf85e..4052d4b4 100644 --- a/src/model_selection/trainTestSplit.ts +++ b/src/model_selection/trainTestSplit.ts @@ -1,7 +1,12 @@ import { Scikit1D, Scikit2D } from '../types' -import { assert, isDataFrameInterface, isSeriesInterface } from '../typesUtils' +import { + assert, + isDataFrameInterface, + isSeriesInterface, + isTensor +} from '../typesUtils' import { getLength, sampleWithoutReplacement } from '../utils' -import { tf } from '../shared/globals' +import { getBackend } from '../tf-singleton' /** * Validation helper to check if the test/test sizes are meaningful wrt to the @@ -111,7 +116,8 @@ export function validateShuffleSplit( } export function getIndices(X: Scikit2D | Scikit1D, indices: number[]) { - if (X instanceof tf.Tensor) { + let tf = getBackend() + if (isTensor(X)) { return tf.gather(X, indices) } if (isDataFrameInterface(X)) { @@ -120,7 +126,7 @@ export function getIndices(X: Scikit2D | Scikit1D, indices: number[]) { if (isSeriesInterface(X)) { return X.iloc(indices) } - return indices.map((i) => X[i]) + return indices.map((i) => (X as any)[i]) } /** * diff --git a/src/naive_bayes/BaseNaiveBayes.ts b/src/naive_bayes/BaseNaiveBayes.ts index fd48b42b..818ed2dd 100644 --- a/src/naive_bayes/BaseNaiveBayes.ts +++ b/src/naive_bayes/BaseNaiveBayes.ts @@ -13,9 +13,9 @@ * ========================================================================== */ import { polyfillUnique } from '../tfUtils' -import { tf } from '../shared/globals' -import { Scikit1D, Scikit2D } from '../types' +import { Scikit1D, Scikit2D, Tensor1D, Tensor2D, Tensor } from '../types' import { convertToNumericTensor2D, convertToTensor1D } from '../utils' +import { getBackend } from '../tf-singleton' import { Serialize } from '../simpleSerializer' export interface NaiveBayesParams { @@ -32,16 +32,18 @@ export interface NaiveBayesParams { } export abstract class BaseNaiveBayes extends Serialize { - priors?: tf.Tensor1D + priors?: Tensor1D varSmoothing: number - public classes: tf.Tensor1D - public means: tf.Tensor1D[] - public variances: tf.Tensor1D[] + public classes: Tensor1D + public means: Tensor1D[] + public variances: Tensor1D[] + tf: any constructor(params: NaiveBayesParams = {}) { super() - this.classes = tf.tensor1d([]) + this.tf = getBackend() + this.classes = this.tf.tensor1d([]) this.means = [] this.variances = [] if (params.priors) { @@ -60,25 +62,25 @@ export abstract class BaseNaiveBayes extends Serialize { const features = convertToNumericTensor2D(X) const labels = convertToTensor1D(y) - const { values, meansByLabel, variancesByLabel } = tf.tidy(() => { - polyfillUnique(tf) - const meansByLabel: tf.Tensor1D[] = [] - const variancesByLabel: tf.Tensor1D[] = [] + const { values, meansByLabel, variancesByLabel } = this.tf.tidy(() => { + polyfillUnique(this.tf) + const meansByLabel: Tensor1D[] = [] + const variancesByLabel: Tensor1D[] = [] // Get the list of unique labels - const { values } = tf.unique(labels) + const { values } = this.tf.unique(labels) - const { variance } = tf.moments(features, 0) + const { variance } = this.tf.moments(features, 0) const epsilon = variance.max().mul(this.varSmoothing) - tf.unstack(values).forEach((c: tf.Tensor) => { - const mask = tf.equal(labels, c).toFloat() - const numInstances = tf.sum(mask) - const mean = tf + this.tf.unstack(values).forEach((c: Tensor) => { + const mask = this.tf.equal(labels, c).toFloat() + const numInstances = this.tf.sum(mask) + const mean = this.tf .mul(features, mask.expandDims(1)) .sum(0) .div(numInstances) - const variance = tf + const variance = this.tf .sub(features, mean) .mul(mask.expandDims(1)) .pow(2) @@ -86,8 +88,8 @@ export abstract class BaseNaiveBayes extends Serialize { .div(numInstances) .add(epsilon) - meansByLabel.push(mean as tf.Tensor1D) - variancesByLabel.push(variance as tf.Tensor1D) + meansByLabel.push(mean as Tensor1D) + variancesByLabel.push(variance as Tensor1D) }) return { values, meansByLabel, variancesByLabel } @@ -104,23 +106,23 @@ export abstract class BaseNaiveBayes extends Serialize { /** * Predict the probability of samples assigned to each observed label. * @param X - * @returns {tf.Tensor} Probabilities + * @returns {this.tf.Tensor} Probabilities */ public predictProba(X: Scikit2D) { const features = convertToNumericTensor2D(X) - const probabilities = tf.tidy(() => { - let probs: tf.Tensor1D[] = [] + const probabilities = this.tf.tidy(() => { + let probs: Tensor1D[] = [] this.classes.unstack().forEach((_, idx) => { // Get the mean for this label const mean = this.means[idx] const variance = this.variances[idx] const prob = this.kernel(features, mean, variance) - probs.push(prob as tf.Tensor1D) + probs.push(prob as Tensor1D) }) - const withoutPriors = tf.stack(probs, 1) as tf.Tensor2D + const withoutPriors = this.tf.stack(probs, 1) as Tensor2D if (this.priors) { return withoutPriors.mul(this.priors) } else { @@ -134,7 +136,7 @@ export abstract class BaseNaiveBayes extends Serialize { /** * Predict the labels assigned to each sample * @param X - * @returns {tf.Tensor} Labels + * @returns {this.tf.Tensor} Labels */ public predict(X: Scikit2D) { const probs = this.predictProba(X) @@ -148,8 +150,8 @@ export abstract class BaseNaiveBayes extends Serialize { * @param variance */ protected abstract kernel( - features: tf.Tensor2D, - mean: tf.Tensor1D, - variance: tf.Tensor1D - ): tf.Tensor1D + features: Tensor2D, + mean: Tensor1D, + variance: Tensor1D + ): Tensor1D } diff --git a/src/naive_bayes/GaussianNB.test.ts b/src/naive_bayes/GaussianNB.test.ts index 5cdfc9bb..b3cb8331 100644 --- a/src/naive_bayes/GaussianNB.test.ts +++ b/src/naive_bayes/GaussianNB.test.ts @@ -12,7 +12,9 @@ * limitations under the License. * ========================================================================== */ -import { GaussianNB, fromJSON } from '../index' +import { GaussianNB, setBackend, fromJSON } from '../index' +import * as tf from '@tensorflow/tfjs' +setBackend(tf) describe('GaussianNB', function () { it('without priors', async () => { diff --git a/src/naive_bayes/GaussianNB.ts b/src/naive_bayes/GaussianNB.ts index 6801b841..303f44d8 100644 --- a/src/naive_bayes/GaussianNB.ts +++ b/src/naive_bayes/GaussianNB.ts @@ -12,9 +12,9 @@ * limitations under the License. * ========================================================================== */ -import { tf } from '../shared/globals' import { BaseNaiveBayes } from './BaseNaiveBayes' - +import { getBackend } from '../tf-singleton' +import { Tensor1D, Tensor2D } from '../types' /** * Gaussian Naive Bayes classifier * @@ -47,10 +47,11 @@ import { BaseNaiveBayes } from './BaseNaiveBayes' export class GaussianNB extends BaseNaiveBayes { name = 'GaussianNB' protected kernel( - features: tf.Tensor2D, - mean: tf.Tensor1D, - variance: tf.Tensor1D - ): tf.Tensor1D { + features: Tensor2D, + mean: Tensor1D, + variance: Tensor1D + ): Tensor1D { + let tf = getBackend() return tf.tidy(() => { return tf .sub(features, mean.expandDims(0)) @@ -63,7 +64,7 @@ export class GaussianNB extends BaseNaiveBayes { .expandDims(0) .sqrt() ) - .prod(1) as tf.Tensor1D + .prod(1) as Tensor1D }) } } diff --git a/src/neighbors/BruteNeighborhood.test.ts b/src/neighbors/BruteNeighborhood.test.ts index b263ff51..db39f80c 100644 --- a/src/neighbors/BruteNeighborhood.test.ts +++ b/src/neighbors/BruteNeighborhood.test.ts @@ -15,6 +15,9 @@ import { neighborhoodGenericTests } from './neighborhoodGenericTests' import { BruteNeighborhood } from './BruteNeighborhood' +import { setBackend } from '../tf-singleton' +import * as tf from '@tensorflow/tfjs' +setBackend(tf) neighborhoodGenericTests( 'BruteNeighborhood', diff --git a/src/neighbors/BruteNeighborhood.ts b/src/neighbors/BruteNeighborhood.ts index 720d7a1d..b1d326f4 100644 --- a/src/neighbors/BruteNeighborhood.ts +++ b/src/neighbors/BruteNeighborhood.ts @@ -15,24 +15,26 @@ import { Neighborhood, NeighborhoodParams } from './Neighborhood' import { Metric } from './Metric' -import { tf } from '../shared/globals' import { assert } from '../typesUtils' - +import { Tensor2D } from '../types' +import { getBackend } from '../tf-singleton' /** * A {@link Neighborhood} implementation that uses a brute force approach * to nearest neighbor search. During a {@link BruteNeighborhood#kNearest} * query, the distance between every entry and the query point is computed. */ export class BruteNeighborhood implements Neighborhood { - private _metric: Metric - private _entries: tf.Tensor2D + _metric: Metric + _entries: Tensor2D + tf: any constructor({ metric, entries }: NeighborhoodParams) { this._metric = metric this._entries = entries + this.tf = getBackend() } - kNearest(k: number, queryPoints: tf.Tensor2D) { + kNearest(k: number, queryPoints: Tensor2D) { const { _metric, _entries } = this assert( @@ -42,27 +44,29 @@ export class BruteNeighborhood implements Neighborhood { // // batched version // const [m, n] = queryPoints.shape - // return tf.tidy(() => { + // return this.tf.tidy(() => { // const negDist = _metric.tensorDistance( // queryPoints.reshape([m, 1, n]), // _entries // ).neg() as Tensor2D - // const { values, indices } = tf.topk(negDist, k) + // const { values, indices } = this.tf.topk(negDist, k) // return { distances: values.neg(), indices } // }) // unbatched version - return tf.tidy(() => { - const result = tf.unstack(queryPoints).map((queryPoint) => { - return tf.tidy(() => { + return this.tf.tidy(() => { + const result = this.tf.unstack(queryPoints).map((queryPoint: any) => { + return this.tf.tidy(() => { const dist = _metric.tensorDistance(queryPoint, _entries).neg() - const { values, indices } = tf.topk(dist, k) + const { values, indices } = this.tf.topk(dist, k) return [values, indices] }) }) return { - distances: tf.stack(result.map((x) => x[0])).neg() as tf.Tensor2D, - indices: tf.stack(result.map((x) => x[1])) as tf.Tensor2D + distances: this.tf + .stack(result.map((x: any) => x[0])) + .neg() as Tensor2D, + indices: this.tf.stack(result.map((x: any) => x[1])) as Tensor2D } }) } diff --git a/src/neighbors/CappedMaxHeap.ts b/src/neighbors/CappedMaxHeap.ts index c77f25db..3031f121 100644 --- a/src/neighbors/CappedMaxHeap.ts +++ b/src/neighbors/CappedMaxHeap.ts @@ -24,8 +24,8 @@ import { assert } from '../typesUtils' * tree traversal. */ export class CappedMaxHeap { - private _keys: Float32Array - private _vals: Int32Array + _keys: Float32Array + _vals: Int32Array /** * Index of the currently first entry. * The entries are added from right to @@ -33,7 +33,7 @@ export class CappedMaxHeap { * adding further elements results in * replacement. */ - private _pos: number + _pos: number /** * Creates a new heap using the given key diff --git a/src/neighbors/KNeighborsBase.ts b/src/neighbors/KNeighborsBase.ts index 31774e18..78ab67e4 100644 --- a/src/neighbors/KNeighborsBase.ts +++ b/src/neighbors/KNeighborsBase.ts @@ -16,19 +16,21 @@ import { Neighborhood, NeighborhoodParams } from './Neighborhood' import { BruteNeighborhood } from './BruteNeighborhood' import { minkowskiMetric } from './Metric' -import { Scikit1D, Scikit2D } from '../types' +import { Scikit1D, Scikit2D, Tensor2D, Tensor1D } from '../types' import { convertToNumericTensor1D, convertToNumericTensor2D } from '../utils' import { assert } from '../typesUtils' -import { tf } from '../shared/globals' import { KdTree } from './KdTree' +import { getBackend } from '../tf-singleton' import { Serialize } from '../simpleSerializer' const WEIGHTS_FUNCTIONS = { - uniform(distances: tf.Tensor2D) { + uniform(distances: Tensor2D) { + let tf = getBackend() const { shape } = distances - return tf.fill(shape, 1 / shape[1]) as tf.Tensor2D + return tf.fill(shape, 1 / shape[1]) as Tensor2D }, - distance(distances: tf.Tensor2D) { + distance(distances: Tensor2D) { + let tf = getBackend() return tf.tidy(() => { // scale inverse distances by min. to avoid `1/tinyVal == Infinity` const min = distances.min(1, /*keepDims=*/ true) @@ -111,8 +113,8 @@ export class KNeighborsBase extends Serialize implements KNeighborsParams { Object.keys(ALGORITHMS) ) as (keyof typeof ALGORITHMS)[] - private _neighborhood: Neighborhood | undefined - private _y: tf.Tensor1D | undefined + _neighborhood: Neighborhood | undefined + _y: Tensor1D | undefined weights: KNeighborsParams['weights'] algorithm: KNeighborsParams['algorithm'] @@ -148,7 +150,7 @@ export class KNeighborsBase extends Serialize implements KNeighborsParams { nNeighbors, weightsFn, neighborhood: _neighborhood as Neighborhood, - y: _y as tf.Tensor1D + y: _y as Tensor1D } } diff --git a/src/neighbors/KNeighborsClassifier.test.ts b/src/neighbors/KNeighborsClassifier.test.ts index b69464fc..e9895529 100644 --- a/src/neighbors/KNeighborsClassifier.test.ts +++ b/src/neighbors/KNeighborsClassifier.test.ts @@ -13,17 +13,17 @@ * ========================================================================== */ -import { KNeighborsClassifier } from '../index' +import { KNeighborsClassifier, setBackend } from '../index' import { KNeighborsParams } from './KNeighborsBase' import { dataUrls } from '../datasets/datasets' import { crossValScore } from '../model_selection/crossValScore' import { KFold } from '../model_selection/KFold' import { arrayEqual } from '../utils' +import { Tensor1D, Tensor2D } from '../types' import '../jestTensorMatchers' import * as dfd from 'danfojs-node' -import { tf } from '../shared/globals' -type Tensor1D = tf.Tensor1D -type Tensor2D = tf.Tensor2D +import * as tf from '@tensorflow/tfjs' +setBackend(tf) function testWithDataset( loadDataUrl: string, diff --git a/src/neighbors/KNeighborsClassifier.ts b/src/neighbors/KNeighborsClassifier.ts index cc38d9a3..6687e537 100644 --- a/src/neighbors/KNeighborsClassifier.ts +++ b/src/neighbors/KNeighborsClassifier.ts @@ -13,14 +13,12 @@ * ========================================================================== */ -import { Scikit1D, Scikit2D } from '../types' +import { Scikit1D, Scikit2D, Tensor1D, Tensor2D } from '../types' import { KNeighborsBase } from './KNeighborsBase' import { convertToNumericTensor2D, convertToTensor1D } from '../utils' import { polyfillUnique } from '../tfUtils' import { accuracy } from '../model_selection/scorers' -import { tf } from '../shared/globals' -type Tensor1D = tf.Tensor1D -type Tensor2D = tf.Tensor2D +import { getBackend } from '../tf-singleton' /** * K-Nearest neighbor regressor. @@ -58,7 +56,7 @@ export class KNeighborsClassifier extends KNeighborsBase { public predictProba(X: Scikit2D): Tensor2D { const { neighborhood, y, nNeighbors, weightsFn } = this._getFitParams() const [nClasses] = this.classes_?.shape as [number] - + let tf = getBackend() return tf.tidy(() => { const _X = convertToNumericTensor2D(X) const nSamples = _X.shape[0] @@ -88,7 +86,7 @@ export class KNeighborsClassifier extends KNeighborsBase { */ public predict(X: Scikit2D): Tensor1D { const classes = this.classes_ as Tensor1D - + let tf = getBackend() return tf.tidy(() => { const probs = this.predictProba(X) const labels = probs.argMax(1) @@ -97,6 +95,7 @@ export class KNeighborsClassifier extends KNeighborsBase { } public async fit(X: Scikit2D, labels: Scikit1D): Promise { + let tf = getBackend() const { values, indices } = tf.tidy(() => { const _labels = convertToTensor1D(labels) polyfillUnique(tf) diff --git a/src/neighbors/KNeighborsRegressor.test.ts b/src/neighbors/KNeighborsRegressor.test.ts index 751fb8fb..d7f5321c 100644 --- a/src/neighbors/KNeighborsRegressor.test.ts +++ b/src/neighbors/KNeighborsRegressor.test.ts @@ -13,17 +13,21 @@ * ========================================================================== */ -import { KNeighborsRegressor, crossValScore, KFold } from '../index' +import { + KNeighborsRegressor, + setBackend, + KFold, + crossValScore +} from '../index' import { KNeighborsParams } from './KNeighborsBase' import { dataUrls } from '../datasets/datasets' import { arrayEqual } from '../utils' import { negMeanSquaredError } from '../model_selection/scorers' import '../jestTensorMatchers' import * as dfd from 'danfojs-node' -import { tf } from '../shared/globals' - -type Tensor1D = tf.Tensor1D -type Tensor2D = tf.Tensor2D +import { Tensor1D, Tensor2D } from '../types' +import * as tf from '@tensorflow/tfjs' +setBackend(tf) function testWithDataset( loadDataUrl: string, @@ -44,7 +48,7 @@ function testWithDataset( const X = Xy.slice([0, 0], [nSamples, nFeatures]) const y = Xy.slice([0, nFeatures]).reshape([nSamples]) as Tensor1D - const scores = await crossValScore( + const scores = await (crossValScore as any)( new KNeighborsRegressor(params), X, y, diff --git a/src/neighbors/KNeighborsRegressor.ts b/src/neighbors/KNeighborsRegressor.ts index 8bda66f7..cc66f1e8 100644 --- a/src/neighbors/KNeighborsRegressor.ts +++ b/src/neighbors/KNeighborsRegressor.ts @@ -16,7 +16,7 @@ import { Scikit2D } from '../types' import { KNeighborsBase } from './KNeighborsBase' import { convertToNumericTensor2D } from '../utils' -import { tf } from '../shared/globals' +import { getBackend } from '../tf-singleton' /** * K-Nearest neighbor regressor. @@ -46,7 +46,9 @@ export class KNeighborsRegressor extends KNeighborsBase { * for sample `X[i,:]` */ name = 'KNeighborsRegressor' + public predict(X: Scikit2D) { + let tf = getBackend() const { neighborhood, y, nNeighbors, weightsFn } = this._getFitParams() return tf.tidy(() => { @@ -56,7 +58,6 @@ export class KNeighborsRegressor extends KNeighborsBase { const targets = y.gather(indices) const weights = weightsFn(distances) - // return tf.einsum('ij,ij->i', targets, weights) as Tensor1D return tf .matMul( targets.reshape([-1, 1, nNeighbors]), diff --git a/src/neighbors/KdTree.test.ts b/src/neighbors/KdTree.test.ts index 87736ed3..11e4d8ac 100644 --- a/src/neighbors/KdTree.test.ts +++ b/src/neighbors/KdTree.test.ts @@ -15,5 +15,8 @@ import { neighborhoodGenericTests } from './neighborhoodGenericTests' import { KdTree } from './KdTree' +import { setBackend } from '../index' +import * as tf from '@tensorflow/tfjs' +setBackend(tf) neighborhoodGenericTests('KdTree', KdTree.build) diff --git a/src/neighbors/KdTree.ts b/src/neighbors/KdTree.ts index 2adefcd3..c33a7b1b 100644 --- a/src/neighbors/KdTree.ts +++ b/src/neighbors/KdTree.ts @@ -14,11 +14,13 @@ */ import { assert } from '../typesUtils' -import { tf } from '../shared/globals' + import { Neighborhood, NeighborhoodParams } from './Neighborhood' import * as randUtils from '../randUtils' import { alea } from '../randUtils' import { CappedMaxHeap } from './CappedMaxHeap' +import { Tensor2D } from '../types' +import { getBackend } from '../tf-singleton' const child = (parent: number) => (parent << 1) + 1 const parent = (child: number) => (child - 1) >> 1 @@ -62,35 +64,36 @@ interface KdMetric { * set of points. */ export class KdTree implements Neighborhood { - private _nSamples: number - private _nFeatures: number + _nSamples: number + _nFeatures: number - private _metric: KdMetric + _metric: KdMetric /** * Coordinates of the points contained in this kdTree, not in the order * as they were passed to {@link KdTree.build}. */ - private _points: Vec[] + _points: Vec[] /** * Keeps track of the order, in which the points were originally passed * to {@link KdTree.build}. The `i+1`-th point in `_points` was originally * passed as `_indices[i]+1`-th point to {@link KdTree.build}. */ - private _indices: Int32Array + _indices: Int32Array /** * The bounding box of each tree node. */ - private _bBoxes: Float32Array[] + _bBoxes: Float32Array[] /** * The (i+1)-th leaf of this tree contains the points * `_points[_offsets[i]]` to `_points[_offsets[i+1]-1]`. */ - private _offsets: Int32Array + _offsets: Int32Array - private constructor( + tf: any + constructor( nSamples: number, nFeatures: number, metric: KdMetric, @@ -99,6 +102,7 @@ export class KdTree implements Neighborhood { offsets: Int32Array, indices: Int32Array ) { + this.tf = getBackend() this._nSamples = nSamples this._nFeatures = nFeatures @@ -280,8 +284,8 @@ export class KdTree implements Neighborhood { kNearest( k: number, - queryPoints: tf.Tensor2D - ): { distances: tf.Tensor2D; indices: tf.Tensor2D } { + queryPoints: Tensor2D + ): { distances: Tensor2D; indices: Tensor2D } { const { _nSamples, _nFeatures, @@ -364,8 +368,8 @@ export class KdTree implements Neighborhood { // KNeighborsBaseParams and add backpropagation support // to KdTree. return { - distances: tf.tensor2d(dists, [nQueries, k], 'float32'), - indices: tf.tensor2d(indxs, [nQueries, k], 'int32') + distances: this.tf.tensor2d(dists, [nQueries, k], 'float32'), + indices: this.tf.tensor2d(indxs, [nQueries, k], 'int32') } } } diff --git a/src/neighbors/Metric.test.ts b/src/neighbors/Metric.test.ts index c05c5125..67487b8e 100644 --- a/src/neighbors/Metric.test.ts +++ b/src/neighbors/Metric.test.ts @@ -14,8 +14,10 @@ */ import * as fc from 'fast-check' - +import { setBackend } from '../index' import { Metric, minkowskiMetric } from './Metric' +import * as tf from '@tensorflow/tfjs' +setBackend(tf) const NDIMS = Object.freeze([1, 2, 7]) diff --git a/src/neighbors/Metric.ts b/src/neighbors/Metric.ts index f671d546..cb4dc074 100644 --- a/src/neighbors/Metric.ts +++ b/src/neighbors/Metric.ts @@ -14,7 +14,8 @@ */ import { assert } from '../typesUtils' -import { tf } from '../shared/globals' +import { Tensor, Tensor2D } from '../types' +import { getBackend } from '../tf-singleton' /** * Abstract type of neighbohood distance metrics. @@ -32,7 +33,7 @@ export interface Metric { * @returns A broadcasted distance tensor `D`, where `D[..., i, j]` represents * the distance between point `X[..., i, j, k]` and point `Y[..., i, j, k]`. */ - tensorDistance(u: tf.Tensor, v: tf.Tensor): tf.Tensor + tensorDistance(u: Tensor, v: Tensor): Tensor /** * Returns the distance between two points `u` and `v`. @@ -66,20 +67,20 @@ export interface Metric { toString(): string } -const minkowskiTensorDistance = - (p: number) => (u: tf.Tensor, v: tf.Tensor) => { - // FIXME: tf.norm still underflows and overflows, - // see: https://github.com/tensorflow/tfjs/issues/895 - const m = u.shape[u.rank - 1] ?? NaN - const n = v.shape[v.rank - 1] ?? NaN - assert( - m === n, - `minkowskiDistance(${p}).tensorDistance(u,v): u.shape[-1] must equal v.shape[-1].` - ) - return tf.tidy(() => { - return tf.norm(tf.sub(u, v), p, -1) - }) as tf.Tensor2D - } +const minkowskiTensorDistance = (p: number) => (u: Tensor, v: Tensor) => { + let tf = getBackend() + // FIXME: tf.norm still underflows and overflows, + // see: https://github.com/tensorflow/tfjs/issues/895 + const m = u.shape[u.rank - 1] ?? NaN + const n = v.shape[v.rank - 1] ?? NaN + assert( + m === n, + `minkowskiDistance(${p}).tensorDistance(u,v): u.shape[-1] must equal v.shape[-1].` + ) + return tf.tidy(() => { + return tf.norm(tf.sub(u, v), p, -1) + }) as Tensor2D +} /** * Returns the Minkowski distance metric with the given power `p`. diff --git a/src/neighbors/Neighborhood.ts b/src/neighbors/Neighborhood.ts index 266822f2..504460f0 100644 --- a/src/neighbors/Neighborhood.ts +++ b/src/neighbors/Neighborhood.ts @@ -13,7 +13,7 @@ * ========================================================================== */ -import { tf } from '../shared/globals' +import { Tensor2D } from '../types' import { Metric } from './Metric' /** @@ -29,7 +29,7 @@ export interface NeighborhoodParams { * The row `entries[i,:]` represents the (i+1)-th point. * The nearest neighbors are searched for in these points. */ - entries: tf.Tensor2D + entries: Tensor2D /** * For tree-based neighborhood data structures, this is a * hint as to how many points are to be stored in a single @@ -58,6 +58,6 @@ export interface Neighborhood { */ kNearest( k: number, - queryPoints: tf.Tensor2D - ): { distances: tf.Tensor2D; indices: tf.Tensor2D } + queryPoints: Tensor2D + ): { distances: Tensor2D; indices: Tensor2D } } diff --git a/src/neighbors/neighborhoodGenericTests.ts b/src/neighbors/neighborhoodGenericTests.ts index 275c8583..509275d4 100644 --- a/src/neighbors/neighborhoodGenericTests.ts +++ b/src/neighbors/neighborhoodGenericTests.ts @@ -14,13 +14,15 @@ */ import * as fc from 'fast-check' -import { tf } from '../shared/globals' import { alea } from '../randUtils' +import { setBackend } from '../tf-singleton' +import * as tf from '@tensorflow/tfjs' import { Neighborhood, NeighborhoodParams } from './Neighborhood' import { lhs, shuffle } from '../randUtils' import { minkowskiMetric } from './Metric' import { polyfillUnique } from '../tfUtils' import '../jestTensorMatchers' +setBackend(tf) export function neighborhoodGenericTests( name: string, diff --git a/src/pipeline/Pipeline.test.ts b/src/pipeline/Pipeline.test.ts index 8a9ac2ec..e6ecaa4a 100644 --- a/src/pipeline/Pipeline.test.ts +++ b/src/pipeline/Pipeline.test.ts @@ -4,10 +4,12 @@ import { LinearRegression, SimpleImputer, MinMaxScaler, + setBackend, fromJSON } from '../index' -import { tf } from '../shared/globals' +import * as tf from '@tensorflow/tfjs' import { tensorEqual } from '../utils' +setBackend(tf) describe('Pipeline', function () { it('Use a Pipeline (min-max scaler, and linear regression)', async function () { diff --git a/src/pipeline/Pipeline.ts b/src/pipeline/Pipeline.ts index a2c5e923..d7c2cc0e 100644 --- a/src/pipeline/Pipeline.ts +++ b/src/pipeline/Pipeline.ts @@ -1,8 +1,7 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import { assert } from '../typesUtils' -import { Scikit1D, Scikit2D } from '../types' +import { Scikit1D, Scikit2D, Tensor2D } from '../types' import { Serialize } from '../simpleSerializer' -import { tf } from '../shared/globals' /* Next steps: @@ -170,22 +169,22 @@ export class Pipeline extends Serialize { return this } - public transform(X: Scikit2D): tf.Tensor2D { + public transform(X: Scikit2D): Tensor2D { this.validateSteps(this.steps) const lastEstimator = this.getLastEstimator() this.assertEstimatorHasFunction(lastEstimator, 'transform') let XT = this.transformExceptLast(X) - return lastEstimator.transform(XT) as tf.Tensor2D + return lastEstimator.transform(XT) as Tensor2D } - public fitTransform(X: Scikit2D, y: Scikit1D): tf.Tensor2D { + public fitTransform(X: Scikit2D, y: Scikit1D): Tensor2D { this.validateSteps(this.steps) const lastEstimator = this.getLastEstimator() this.assertEstimatorHasFunction(lastEstimator, 'fitTransform') let XT = this.fitTransformExceptLast(X) - return lastEstimator.fitTransform(XT) as tf.Tensor2D + return lastEstimator.fitTransform(XT) as Tensor2D } public predict(X: Scikit2D) { diff --git a/src/preprocessing/LabelEncoder.test.ts b/src/preprocessing/LabelEncoder.test.ts index b56ede1a..3afc03bd 100644 --- a/src/preprocessing/LabelEncoder.test.ts +++ b/src/preprocessing/LabelEncoder.test.ts @@ -1,5 +1,7 @@ -import { LabelEncoder } from '../index' +import { LabelEncoder, setBackend } from '../index' import * as dfd from 'danfojs-node' +import * as tf from '@tensorflow/tfjs' +setBackend(tf) describe('LabelEncoder', function () { it('LabelEncoder works for Series', function () { diff --git a/src/preprocessing/LabelEncoder.ts b/src/preprocessing/LabelEncoder.ts index 6173f31b..88b12597 100644 --- a/src/preprocessing/LabelEncoder.ts +++ b/src/preprocessing/LabelEncoder.ts @@ -13,9 +13,9 @@ * ========================================================================== */ -import { Scikit1D } from '../types' -import { tf } from '../shared/globals' -import { isSeriesInterface } from '../typesUtils' +import { Scikit1D, Tensor1D } from '../types' +import { isSeriesInterface, isTensor } from '../typesUtils' +import { getBackend } from '../tf-singleton' import { Serialize } from '../simpleSerializer' /* @@ -43,8 +43,11 @@ export class LabelEncoder extends Serialize { /** Useful for pipelines and column transformers to have a default name for transforms */ name = 'LabelEncoder' + tf: any + constructor() { super() + this.tf = getBackend() this.classes = [] } @@ -52,7 +55,7 @@ export class LabelEncoder extends Serialize { if (isSeriesInterface(X)) { return X.values as any[] } - if (X instanceof tf.Tensor) { + if (isTensor(X)) { return X.arraySync() } return X @@ -93,7 +96,7 @@ export class LabelEncoder extends Serialize { * // [0, 1, 2, 3] * ``` */ - public transform(X: Scikit1D): tf.Tensor1D { + public transform(X: Scikit1D): Tensor1D { const arr = this.convertTo1DArray(X) const labels = this.classesToMapping(this.classes) @@ -101,10 +104,10 @@ export class LabelEncoder extends Serialize { let val = labels.get(value) return val === undefined ? -1 : val }) - return tf.tensor1d(encodedData) + return this.tf.tensor1d(encodedData) } - public fitTransform(X: Scikit1D): tf.Tensor1D { + public fitTransform(X: Scikit1D): Tensor1D { return this.fit(X).transform(X) } diff --git a/src/preprocessing/MaxAbsScaler.test.ts b/src/preprocessing/MaxAbsScaler.test.ts index f742bdcc..8c6a222d 100644 --- a/src/preprocessing/MaxAbsScaler.test.ts +++ b/src/preprocessing/MaxAbsScaler.test.ts @@ -1,6 +1,7 @@ -import { MaxAbsScaler, fromJSON } from '../index' +import { MaxAbsScaler, setBackend, fromJSON } from '../index' import * as dfd from 'danfojs-node' -import { tf } from '../shared/globals' +import * as tf from '@tensorflow/tfjs' +setBackend(tf) import { arrayEqual } from '../utils' describe('MaxAbsScaler', function () { diff --git a/src/preprocessing/MaxAbsScaler.ts b/src/preprocessing/MaxAbsScaler.ts index a0c7a818..e9f52bac 100644 --- a/src/preprocessing/MaxAbsScaler.ts +++ b/src/preprocessing/MaxAbsScaler.ts @@ -17,8 +17,8 @@ import { convertToNumericTensor2D } from '../utils' import { assert, isDataFrameInterface, isScikit2D } from '../typesUtils' import { tensorMax, turnZerosToOnes } from '../math' import { TransformerMixin } from '../mixins' -import { Scikit2D } from '../types' -import { tf } from '../shared/globals' +import { Scikit2D, Tensor1D, Tensor2D } from '../types' +import { getBackend } from '../tf-singleton' /* Next steps: @@ -55,7 +55,7 @@ Next steps: */ export class MaxAbsScaler extends TransformerMixin { /** The per-feature scale that we see in the dataset. We divide by this number. */ - scale: tf.Tensor1D + scale: Tensor1D /** The number of features seen during fit */ nFeaturesIn: number @@ -71,7 +71,8 @@ export class MaxAbsScaler extends TransformerMixin { constructor() { super() - this.scale = tf.tensor1d([]) + this.tf = getBackend() + this.scale = this.tf.tensor1d([]) this.nFeaturesIn = 0 this.nSamplesSeen = 0 this.featureNamesIn = [] @@ -83,10 +84,10 @@ export class MaxAbsScaler extends TransformerMixin { public fit(X: Scikit2D): MaxAbsScaler { assert(isScikit2D(X), 'Data can not be converted to a 2D matrix.') const tensorArray = convertToNumericTensor2D(X) - const scale = tensorMax(tensorArray.abs(), 0, true) as tf.Tensor1D + const scale = tensorMax(tensorArray.abs(), 0, true) as Tensor1D // Deal with 0 scale values - this.scale = turnZerosToOnes(scale) as tf.Tensor1D + this.scale = turnZerosToOnes(scale) as Tensor1D this.nSamplesSeen = tensorArray.shape[0] this.nFeaturesIn = tensorArray.shape[1] @@ -99,20 +100,20 @@ export class MaxAbsScaler extends TransformerMixin { /** * Transform the data using the fitted scaler */ - public transform(X: Scikit2D): tf.Tensor2D { + public transform(X: Scikit2D): Tensor2D { assert(isScikit2D(X), 'Data can not be converted to a 2D matrix.') const tensorArray = convertToNumericTensor2D(X) - const outputData = tensorArray.div(this.scale) + const outputData = tensorArray.div(this.scale) return outputData } /** * Inverse transform the data using the fitted scaler */ - public inverseTransform(X: Scikit2D): tf.Tensor2D { + public inverseTransform(X: Scikit2D): Tensor2D { assert(isScikit2D(X), 'Data can not be converted to a 2D matrix.') const tensorArray = convertToNumericTensor2D(X) - const outputData = tensorArray.mul(this.scale) + const outputData = tensorArray.mul(this.scale) return outputData } } diff --git a/src/preprocessing/MinMaxScaler.test.ts b/src/preprocessing/MinMaxScaler.test.ts index a4efd446..eee559c0 100644 --- a/src/preprocessing/MinMaxScaler.test.ts +++ b/src/preprocessing/MinMaxScaler.test.ts @@ -1,8 +1,10 @@ -import { MinMaxScaler, fromJSON } from '../index' +import { MinMaxScaler, setBackend, fromJSON } from '../index' import * as dfd from 'danfojs-node' import { isDataFrameInterface, isSeriesInterface } from '../typesUtils' import { ScikitVecOrMatrix } from '../types' -import { tf } from '../shared/globals' +import * as tf from '@tensorflow/tfjs' +setBackend(tf) + export function convertTensorToInputType( tensor: tf.Tensor, inputData: ScikitVecOrMatrix diff --git a/src/preprocessing/MinMaxScaler.ts b/src/preprocessing/MinMaxScaler.ts index fe37e04d..64d655ed 100644 --- a/src/preprocessing/MinMaxScaler.ts +++ b/src/preprocessing/MinMaxScaler.ts @@ -14,11 +14,11 @@ */ import { convertToNumericTensor2D } from '../utils' -import { Scikit2D, Transformer } from '../types' +import { Scikit2D, Tensor1D, Tensor2D, Transformer } from '../types' import { isScikit2D, assert, isDataFrameInterface } from '../typesUtils' import { tensorMin, tensorMax, turnZerosToOnes } from '../math' import { TransformerMixin } from '../mixins' -import { tf } from '../shared/globals' +import { getBackend } from '../tf-singleton' /* Next steps: @@ -63,16 +63,16 @@ export class MinMaxScaler extends TransformerMixin implements Transformer { featureRange: [number, number] /** The per-feature scale that we see in the dataset. */ - scale: tf.Tensor1D + scale: Tensor1D - min: tf.Tensor1D + min: Tensor1D /** The per-feature minimum that we see in the dataset. */ - dataMin: tf.Tensor1D + dataMin: Tensor1D /** The per-feature maximum that we see in the dataset. */ - dataMax: tf.Tensor1D + dataMax: Tensor1D /** The per-feature range that we see in the dataset. */ - dataRange: tf.Tensor1D + dataRange: Tensor1D /** The number of features seen during fit */ nFeaturesIn: number @@ -87,13 +87,14 @@ export class MinMaxScaler extends TransformerMixin implements Transformer { constructor({ featureRange = [0, 1] }: MinMaxScalerParams = {}) { super() + this.tf = getBackend() this.featureRange = featureRange - this.scale = tf.tensor1d([]) - this.min = tf.tensor1d([]) - this.dataMin = tf.tensor1d([]) - this.dataMax = tf.tensor1d([]) - this.dataRange = tf.tensor1d([]) + this.scale = this.tf.tensor1d([]) + this.min = this.tf.tensor1d([]) + this.dataMin = this.tf.tensor1d([]) + this.dataMax = this.tf.tensor1d([]) + this.dataRange = this.tf.tensor1d([]) this.nFeaturesIn = 0 this.nSamplesSeen = 0 @@ -115,14 +116,14 @@ export class MinMaxScaler extends TransformerMixin implements Transformer { 'featureRange needs to contain exactly two numbers where the first is less than the second' ) const tensorArray = convertToNumericTensor2D(X) - const max = tensorMax(tensorArray, 0, true) as tf.Tensor1D - const min = tensorMin(tensorArray, 0, true) as tf.Tensor1D - const range = max.sub(min) - this.scale = tf.div( + const max = tensorMax(tensorArray, 0, true) as Tensor1D + const min = tensorMin(tensorArray, 0, true) as Tensor1D + const range = max.sub(min) + this.scale = this.tf.div( this.featureRange[1] - this.featureRange[0], turnZerosToOnes(range) ) - this.min = tf.sub(this.featureRange[0], min.mul(this.scale)) + this.min = this.tf.sub(this.featureRange[0], min.mul(this.scale)) this.dataMin = min this.dataMax = max this.dataRange = range @@ -137,20 +138,20 @@ export class MinMaxScaler extends TransformerMixin implements Transformer { /** * Transform the data using the fitted scaler * */ - public transform(X: Scikit2D): tf.Tensor2D { + public transform(X: Scikit2D): Tensor2D { assert(isScikit2D(X), 'Data can not be converted to a 2D matrix.') const tensorArray = convertToNumericTensor2D(X) - const outputData = tensorArray.mul(this.scale).add(this.min) + const outputData = tensorArray.mul(this.scale).add(this.min) return outputData } /** * Inverse transform the data using the fitted scaler * */ - public inverseTransform(X: Scikit2D): tf.Tensor2D { + public inverseTransform(X: Scikit2D): Tensor2D { assert(isScikit2D(X), 'Data can not be converted to a 2D matrix.') const tensorArray = convertToNumericTensor2D(X) - const outputData = tensorArray.sub(this.min).div(this.scale) + const outputData = tensorArray.sub(this.min).div(this.scale) return outputData } } diff --git a/src/preprocessing/Normalizer.test.ts b/src/preprocessing/Normalizer.test.ts index b202c377..f0dff026 100644 --- a/src/preprocessing/Normalizer.test.ts +++ b/src/preprocessing/Normalizer.test.ts @@ -1,6 +1,8 @@ -import { Normalizer } from '../index' +import { Normalizer, setBackend } from '../index' import * as dfd from 'danfojs-node' import { arrayEqual } from '../utils' +import * as tf from '@tensorflow/tfjs' +setBackend(tf) describe('Normalizer', function () { it('Standardize values in a DataFrame using a Normalizer (l1 case)', function () { diff --git a/src/preprocessing/Normalizer.ts b/src/preprocessing/Normalizer.ts index e02cc70a..73cecc52 100644 --- a/src/preprocessing/Normalizer.ts +++ b/src/preprocessing/Normalizer.ts @@ -14,10 +14,10 @@ */ import { convertToNumericTensor2D } from '../utils' -import { Scikit2D } from '../types' +import { Scikit2D, Tensor2D } from '../types' import { isScikit2D, assert, isDataFrameInterface } from '../typesUtils' import { TransformerMixin } from '../mixins' -import { tf } from '../shared/globals' +import { getBackend } from '../tf-singleton' /* Next steps: @@ -69,6 +69,7 @@ export class Normalizer extends TransformerMixin { constructor({ norm = 'l2' }: NormalizerParams = {}) { super() + this.tf = getBackend() this.norm = norm this.nFeaturesIn = 0 this.featureNamesIn = [] @@ -90,11 +91,11 @@ export class Normalizer extends TransformerMixin { /** * Transform the data using the Normalizer * */ - public transform(X: Scikit2D): tf.Tensor2D { + public transform(X: Scikit2D): Tensor2D { assert(isScikit2D(X), 'Data can not be converted to a 2D matrix.') const tensorArray = convertToNumericTensor2D(X) if (this.norm === 'l1') { - const means = tf.abs(tensorArray).sum(1).reshape([-1, 1]) + const means = this.tf.abs(tensorArray).sum(1).reshape([-1, 1]) return tensorArray.divNoNan(means) } if (this.norm === 'l2') { @@ -102,7 +103,7 @@ export class Normalizer extends TransformerMixin { return tensorArray.divNoNan(means) } // max case - const means = tf.abs(tensorArray).max(1).reshape([-1, 1]) + const means = this.tf.abs(tensorArray).max(1).reshape([-1, 1]) return tensorArray.divNoNan(means) } } diff --git a/src/preprocessing/OneHotEncoder.test.ts b/src/preprocessing/OneHotEncoder.test.ts index 2ddd7fce..4f9e622c 100644 --- a/src/preprocessing/OneHotEncoder.test.ts +++ b/src/preprocessing/OneHotEncoder.test.ts @@ -1,6 +1,7 @@ -import { tf } from '../shared/globals' -import { OneHotEncoder } from '../index' +import { OneHotEncoder, setBackend } from '../index' import { arrayTo2DColumn } from '../utils' +import * as tf from '@tensorflow/tfjs' +setBackend(tf) describe('OneHotEncoder', function () { it('OneHotEncoder works on array', function () { diff --git a/src/preprocessing/OneHotEncoder.ts b/src/preprocessing/OneHotEncoder.ts index ea5f4f93..f08685f4 100644 --- a/src/preprocessing/OneHotEncoder.ts +++ b/src/preprocessing/OneHotEncoder.ts @@ -14,9 +14,10 @@ */ import { convertScikit2DToArray } from '../utils' -import { Scikit1D, Scikit2D } from '../types' +import { Scikit1D, Scikit2D, Tensor1D, Tensor2D } from '../types' import { TransformerMixin } from '../mixins' -import { tf } from '../shared/globals' +import { getBackend } from '../tf-singleton' + import { isDataFrameInterface } from '../typesUtils' /* @@ -106,6 +107,7 @@ export class OneHotEncoder extends TransformerMixin { drop }: OneHotEncoderParams = {}) { super() + this.tf = getBackend() this.categoriesParam = categories this.categories = [] this.handleUnknown = handleUnknown @@ -192,26 +194,23 @@ export class OneHotEncoder extends TransformerMixin { /** Generalization of the tf.oneHot that can handle "one-hotting" with a single column * output. */ - convertToOneHot( - tensor: tf.Tensor1D, - numberOfOneHotColumns: number - ): tf.Tensor2D { + convertToOneHot(tensor: Tensor1D, numberOfOneHotColumns: number): Tensor2D { if (numberOfOneHotColumns >= 2) { - return tf.oneHot(tensor, numberOfOneHotColumns) as tf.Tensor2D + return this.tf.oneHot(tensor, numberOfOneHotColumns) as Tensor2D } if (numberOfOneHotColumns === 1) { // Every integer that isn't 0 becomes 0 - tensor = tf.where( + tensor = this.tf.where( tensor.equal(0), - tf.ones(tensor.shape, 'int32'), - tf.zeros(tensor.shape, 'int32') + this.tf.ones(tensor.shape, 'int32'), + this.tf.zeros(tensor.shape, 'int32') ) return tensor.reshape([-1, 1]) } // Case where numberOfOneHotColumns = 0 - return tf.tensor2d([]) + return this.tf.tensor2d([]) } /** @@ -225,12 +224,16 @@ export class OneHotEncoder extends TransformerMixin { * ``` */ // eslint-disable-next-line @typescript-eslint/no-unused-vars - public transform(X: Scikit2D, y?: Scikit1D): tf.Tensor2D { + public transform(X: Scikit2D, y?: Scikit1D): Tensor2D { const array2D = convertScikit2DToArray(X) const result2D = this.loopOver2DArrayToUseLabels(array2D) - const newTensor = tf.tensor2d(result2D as number[][], undefined, 'int32') - return tf.concat( - newTensor.unstack(1).map((el, i) => { + const newTensor = this.tf.tensor2d( + result2D as number[][], + undefined, + 'int32' + ) + return this.tf.concat( + newTensor.unstack(1).map((el: any, i: any) => { let categoryNumber = this.categories[i].length let numberOfOneHotColumns = this.drop === 'first' ? categoryNumber - 1 : categoryNumber @@ -238,13 +241,13 @@ export class OneHotEncoder extends TransformerMixin { return val }), 1 - ) as tf.Tensor2D + ) as Tensor2D } /** Only works for single column OneHotEncoding */ - public inverseTransform(X: tf.Tensor2D): any[] { + public inverseTransform(X: Tensor2D): any[] { let labels = this.classesToMapping(this.categories[0]) - const tensorLabels = X.argMax(1) as tf.Tensor1D + const tensorLabels = X.argMax(1) as Tensor1D const invMap = new Map(Array.from(labels, (a) => a.reverse()) as any) const tempData = tensorLabels.arraySync().map((value) => { diff --git a/src/preprocessing/OrdinalEncoder.test.ts b/src/preprocessing/OrdinalEncoder.test.ts index 53438b53..68f5a978 100644 --- a/src/preprocessing/OrdinalEncoder.test.ts +++ b/src/preprocessing/OrdinalEncoder.test.ts @@ -1,5 +1,7 @@ -import { OrdinalEncoder } from '../index' +import { OrdinalEncoder, setBackend } from '../index' import { arrayTo2DColumn } from '../utils' +import * as tf from '@tensorflow/tfjs' +setBackend(tf) describe('OrdinalEncoder', function () { it('OrdinalEncoder works on array', function () { diff --git a/src/preprocessing/OrdinalEncoder.ts b/src/preprocessing/OrdinalEncoder.ts index 4eb78752..d5372652 100644 --- a/src/preprocessing/OrdinalEncoder.ts +++ b/src/preprocessing/OrdinalEncoder.ts @@ -14,9 +14,9 @@ */ import { convertScikit2DToArray } from '../utils' -import { Scikit1D, Scikit2D } from '../types' +import { Scikit1D, Scikit2D, Tensor2D } from '../types' import { TransformerMixin } from '../mixins' -import { tf } from '../shared/globals' +import { getBackend } from '../tf-singleton' import { isDataFrameInterface } from '../typesUtils' /* @@ -97,6 +97,7 @@ export class OrdinalEncoder extends TransformerMixin { unknownValue = NaN }: OrdinalEncoderParams = {}) { super() + this.tf = getBackend() this.categoriesParam = categories this.categories = [] this.handleUnknown = handleUnknown @@ -172,9 +173,9 @@ export class OrdinalEncoder extends TransformerMixin { * Encodes the data using the fitted OrdinalEncoder. */ // eslint-disable-next-line @typescript-eslint/no-unused-vars - public transform(X: Scikit2D, y?: Scikit1D): tf.Tensor2D { + public transform(X: Scikit2D, y?: Scikit1D): Tensor2D { const array2D = convertScikit2DToArray(X) const result2D = this.loopOver2DArrayToUseLabels(array2D) - return tf.tensor2d(result2D as number[][], undefined, 'int32') + return this.tf.tensor2d(result2D as number[][], undefined, 'int32') } } diff --git a/src/preprocessing/RobustScaler.test.ts b/src/preprocessing/RobustScaler.test.ts index 3653c9d4..37f9156a 100644 --- a/src/preprocessing/RobustScaler.test.ts +++ b/src/preprocessing/RobustScaler.test.ts @@ -1,6 +1,8 @@ -import { RobustScaler } from '../index' +import { RobustScaler, setBackend } from '../index' import * as dfd from 'danfojs-node' import { arrayEqual } from '../utils' +import * as tf from '@tensorflow/tfjs' +setBackend(tf) describe('RobustScaler', function () { it('Standardize values in a DataFrame using a RobustScaler', function () { diff --git a/src/preprocessing/RobustScaler.ts b/src/preprocessing/RobustScaler.ts index 9a78f129..33ea6d72 100644 --- a/src/preprocessing/RobustScaler.ts +++ b/src/preprocessing/RobustScaler.ts @@ -14,12 +14,12 @@ */ import { convertToNumericTensor2D } from '../utils' -import { Scikit2D } from '../types' +import { Scikit2D, Tensor1D, Tensor2D } from '../types' import { isScikit2D, assert, isDataFrameInterface } from '../typesUtils' import { turnZerosToOnes } from '../math' import { TransformerMixin } from '../mixins' import { quantileSeq } from 'mathjs' -import { tf } from '../shared/globals' +import { getBackend } from '../tf-singleton' /* Next steps: @@ -91,10 +91,10 @@ function removeMissingValuesFromArray(arr: any[]) { */ export class RobustScaler extends TransformerMixin { /** The per-feature scale that we see in the dataset. We divide by this number. */ - scale: tf.Tensor1D + scale: Tensor1D /** The per-feature median that we see in the dataset. We subtrace this number. */ - center: tf.Tensor1D + center: Tensor1D /** The number of features seen during fit */ nFeaturesIn: number @@ -115,8 +115,9 @@ export class RobustScaler extends TransformerMixin { withScaling = true }: RobustScalerParams = {}) { super() - this.scale = tf.tensor1d([]) - this.center = tf.tensor1d([]) + this.tf = getBackend() + this.scale = this.tf.tensor1d([]) + this.center = this.tf.tensor1d([]) this.quantileRange = quantileRange this.withScaling = withScaling this.withCentering = withCentering @@ -146,13 +147,13 @@ export class RobustScaler extends TransformerMixin { ) const tensorArray = convertToNumericTensor2D(X) - const rowOrientedArray = tensorArray.transpose().arraySync() + const rowOrientedArray = tensorArray.transpose().arraySync() if (this.withCentering) { const quantiles = rowOrientedArray.map((arr: number[] | string[]) => quantileSeq(removeMissingValuesFromArray(arr), 0.5) ) - this.center = tf.tensor1d(quantiles as number[]) + this.center = this.tf.tensor1d(quantiles as number[]) } if (this.withScaling) { const quantiles = rowOrientedArray.map((arr: number[] | string[]) => @@ -161,13 +162,13 @@ export class RobustScaler extends TransformerMixin { highPercentile / 100 ]) ) - const scale = tf.tensor1d(quantiles.map((el: any) => el[1] - el[0])) + const scale = this.tf.tensor1d(quantiles.map((el: any) => el[1] - el[0])) // But what happens if max = min, ie.. we are dealing with a constant vector? // In the case above, scale = max - min = 0 and we'll divide by 0 which is no bueno. // The common practice in cases where the vector is constant is to change the 0 elements // in scale to 1, so that the division doesn't fail. We do that below - this.scale = turnZerosToOnes(scale) as tf.Tensor1D + this.scale = turnZerosToOnes(scale) as Tensor1D } this.nFeaturesIn = tensorArray.shape[1] @@ -177,7 +178,7 @@ export class RobustScaler extends TransformerMixin { return this } - public transform(X: Scikit2D): tf.Tensor2D { + public transform(X: Scikit2D): Tensor2D { assert(isScikit2D(X), 'Data can not be converted to a 2D matrix.') let tensorArray = convertToNumericTensor2D(X) @@ -185,17 +186,17 @@ export class RobustScaler extends TransformerMixin { tensorArray = tensorArray.sub(this.center) } if (this.withScaling) { - tensorArray = tensorArray.div(this.scale) + tensorArray = tensorArray.div(this.scale) } return tensorArray } - public inverseTransform(X: Scikit2D): tf.Tensor2D { + public inverseTransform(X: Scikit2D): Tensor2D { assert(isScikit2D(X), 'Data can not be converted to a 2D matrix.') let tensorArray = convertToNumericTensor2D(X) if (this.withScaling) { - tensorArray = tensorArray.mul(this.scale) + tensorArray = tensorArray.mul(this.scale) } if (this.withCentering) { tensorArray = tensorArray.add(this.center) diff --git a/src/preprocessing/StandardScaler.test.ts b/src/preprocessing/StandardScaler.test.ts index d80621d6..d8199824 100644 --- a/src/preprocessing/StandardScaler.test.ts +++ b/src/preprocessing/StandardScaler.test.ts @@ -1,5 +1,7 @@ -import { StandardScaler } from '../index' +import { StandardScaler, setBackend } from '../index' import * as dfd from 'danfojs-node' +import * as tf from '@tensorflow/tfjs' +setBackend(tf) describe('StandardScaler', function () { it('StandardScaler works for DataFrame', function () { diff --git a/src/preprocessing/StandardScaler.ts b/src/preprocessing/StandardScaler.ts index 90cd9dde..43b4d40d 100644 --- a/src/preprocessing/StandardScaler.ts +++ b/src/preprocessing/StandardScaler.ts @@ -14,11 +14,11 @@ */ import { convertToNumericTensor2D } from '../utils' -import { Scikit2D } from '../types' +import { Scikit2D, Tensor1D, Tensor2D, Tensor } from '../types' import { isScikit2D, assert, isDataFrameInterface } from '../typesUtils' import { tensorMean, tensorStd, turnZerosToOnes } from '../math' import { TransformerMixin } from '../mixins' -import { tf } from '../shared/globals' +import { getBackend } from '../tf-singleton' /* Next steps: @@ -65,10 +65,10 @@ export interface StandardScalerParams { */ export class StandardScaler extends TransformerMixin { /** The per-feature scale that we see in the dataset. We divide by this number. */ - scale: tf.Tensor + scale: Tensor /** The per-feature mean that we see in the dataset. We subtract by this number. */ - mean: tf.Tensor + mean: Tensor /** Whether or not we should subtract the mean */ withMean: boolean @@ -90,10 +90,11 @@ export class StandardScaler extends TransformerMixin { constructor({ withMean = true, withStd = true }: StandardScalerParams = {}) { super() + this.tf = getBackend() this.withMean = withMean this.withStd = withStd - this.scale = tf.tensor1d([]) - this.mean = tf.tensor1d([]) + this.scale = this.tf.tensor1d([]) + this.mean = this.tf.tensor1d([]) this.nFeaturesIn = 0 this.nSamplesSeen = 0 this.featureNamesIn = [] @@ -111,7 +112,7 @@ export class StandardScaler extends TransformerMixin { if (this.withStd) { const std = tensorStd(tensorArray, 0, true) // Deal with zero variance issues - this.scale = turnZerosToOnes(std) as tf.Tensor1D + this.scale = turnZerosToOnes(std) as Tensor1D } this.nSamplesSeen = tensorArray.shape[0] @@ -125,7 +126,7 @@ export class StandardScaler extends TransformerMixin { /** * Transform the data using the fitted scaler */ - public transform(X: Scikit2D): tf.Tensor2D { + public transform(X: Scikit2D): Tensor2D { assert(isScikit2D(X), 'Data can not be converted to a 2D matrix.') let tensorArray = convertToNumericTensor2D(X) if (this.withMean) { @@ -140,7 +141,7 @@ export class StandardScaler extends TransformerMixin { /** * Inverse transform the data using the fitted scaler */ - public inverseTransform(X: Scikit2D): tf.Tensor2D { + public inverseTransform(X: Scikit2D): Tensor2D { assert(isScikit2D(X), 'Data can not be converted to a 2D matrix.') let tensorArray = convertToNumericTensor2D(X) if (this.withStd) { diff --git a/src/shared-esm/globals.ts b/src/shared-esm/globals.ts deleted file mode 100644 index 424eb264..00000000 --- a/src/shared-esm/globals.ts +++ /dev/null @@ -1,3 +0,0 @@ -import * as tf from '@tensorflow/tfjs' - -export { tf } diff --git a/src/shared-node/globals.ts b/src/shared-node/globals.ts deleted file mode 100644 index d26b85d6..00000000 --- a/src/shared-node/globals.ts +++ /dev/null @@ -1,3 +0,0 @@ -import * as tf from '@tensorflow/tfjs-node' - -export { tf } diff --git a/src/shared/globals.ts b/src/shared/globals.ts deleted file mode 100644 index 424eb264..00000000 --- a/src/shared/globals.ts +++ /dev/null @@ -1,3 +0,0 @@ -import * as tf from '@tensorflow/tfjs' - -export { tf } diff --git a/src/simpleSerializer.ts b/src/simpleSerializer.ts index 6b54b764..066b1184 100644 --- a/src/simpleSerializer.ts +++ b/src/simpleSerializer.ts @@ -1,5 +1,5 @@ -import { tf } from './shared/globals' import { encode, decode } from 'base64-arraybuffer' +import { getBackend } from './tf-singleton' const EstimatorList = [ 'KNeighborsRegressor', 'LinearRegression', @@ -35,6 +35,16 @@ const EstimatorList = [ 'DecisionTree' ] +let letters = 'abcdefghijklmnopqrstuvwxy' +function randomString(numLetters: number) { + let curLetter = '' + for (let i = 0; i < numLetters; i++) { + let index = Math.floor(Math.random() * letters.length) + curLetter += letters[index] + } + return curLetter +} + /** * 1. Make a list called EstimatorList * 2. Do a dynamic import here @@ -49,6 +59,29 @@ class JSONHandler { async save(artifacts: any) { // Base 64 encoding artifacts.weightData = encode(artifacts.weightData) + + // Remaps the names of the layers, so that when we deserialize we + // don't run into a tfjs error where it says "you've already created these + // names in our backend" + let mapping: any = {} + + for (let i = 0; i < artifacts.modelTopology.config.layers.length; i++) { + let curWeightSpec = artifacts.modelTopology.config.layers[i] + let randomName = randomString(6) + mapping[curWeightSpec.config.name] = randomName + curWeightSpec.config.name = randomName + } + + for (let i = 0; i < artifacts.weightSpecs.length; i++) { + let cur = artifacts.weightSpecs[i] + let allMaps = Object.keys(mapping) + allMaps.forEach((el) => { + if (cur.name.includes(el)) { + cur.name = cur.name.replace(el, mapping[el]) + } + }) + } + this.savedArtifacts = artifacts return { modelArtifactsInfo: { @@ -72,6 +105,7 @@ export async function toObjectInner( val: any, ignoreKeys: string[] = [] ): Promise { + let tf = getBackend() if (['number', 'string', 'undefined', 'boolean'].includes(typeof val)) { return val } @@ -113,11 +147,10 @@ export async function toObjectInner( } } - // The tf object - if (val.ENV && val.AdadeltaOptimizer && val.version) { + if (val instanceof Float32Array) { return { - name: 'TF', - version: val.version.tfjs + name: 'Float32Array', + value: Array.from(val) } } @@ -131,6 +164,14 @@ export async function toObjectInner( } } + // The tf object + if (val.ENV && val.AdadeltaOptimizer && val.version) { + return { + name: 'TF', + version: val.version.tfjs + } + } + // Generic object case / class case let response: any = {} for (let key of Object.keys(val)) { @@ -138,10 +179,7 @@ export async function toObjectInner( if (ignoreKeys.includes(key)) { continue } - // Ignore any function when we serialize - // if (typeof val[key] === 'function') { - // continue - // } + response[key] = await toObjectInner(val[key], ignoreKeys) } return response @@ -149,6 +187,7 @@ export async function toObjectInner( } export async function fromObjectInner(val: any): Promise { + let tf = getBackend() // Ignores all types that aren't objects if (typeof val !== 'object') { return val @@ -173,6 +212,14 @@ export async function fromObjectInner(val: any): Promise { return new Int32Array(val.value) } + if (val.name === 'Float32Array') { + return new Float32Array(val.value) + } + + if (val.name === 'TF') { + return tf + } + // Array case if (Array.isArray(val)) { return await Promise.all(val.map(async (el) => await fromObjectInner(el))) @@ -221,11 +268,11 @@ let ignoredKeysForSGDRegressor = [ export class Serialize { async toObject(): Promise { - try { - return await toObjectInner(this, ignoredKeysForSGDRegressor) - } catch (e) { - console.error(e) - } + // try { + return await toObjectInner(this, ignoredKeysForSGDRegressor) + // } catch (e) { + // console.error(e) + // } } async toJSON(): Promise { diff --git a/src/svm/LinearSVC.test.ts b/src/svm/LinearSVC.test.ts index 142a7034..79fa6fea 100644 --- a/src/svm/LinearSVC.test.ts +++ b/src/svm/LinearSVC.test.ts @@ -1,4 +1,6 @@ -import { LinearSVC } from './LinearSVC' +import { LinearSVC, setBackend } from '../index' +import * as tf from '@tensorflow/tfjs' +setBackend(tf) describe('LinearSVC', function () { it('Works on arrays (small example)', async function () { diff --git a/src/svm/LinearSVC.ts b/src/svm/LinearSVC.ts index bedd3c64..ee6ff40f 100644 --- a/src/svm/LinearSVC.ts +++ b/src/svm/LinearSVC.ts @@ -14,7 +14,7 @@ // */ import { SGDClassifier } from '../linear_model/SgdClassifier' -import { tf } from '../shared/globals' +import { getBackend } from '../tf-singleton' // First pass at a LinearSVC implementation using gradient descent // Trying to mimic the API of scikit-learn.org/stable/modules/generated/sklearn.linear_model.LinearSVC.html @@ -60,9 +60,6 @@ export interface LinearSVCParams { * ``` */ export class LinearSVC extends SGDClassifier { - /** Useful for pipelines and column transformers to have a default name for transforms */ - name = 'LinearSVC' - constructor({ penalty = 'l2', C = 1, @@ -70,6 +67,7 @@ export class LinearSVC extends SGDClassifier { }: LinearSVCParams = {}) { // Assume Binary classification // If we call fit, and it isn't binary then update args + let tf = getBackend() super({ modelCompileArgs: { optimizer: tf.train.adam(0.1), @@ -101,5 +99,7 @@ export class LinearSVC extends SGDClassifier { optimizerType: 'adam', lossType: 'hingeLoss' }) + /** Useful for pipelines and column transformers to have a default name for transforms */ + this.name = 'LinearSVC' } } diff --git a/src/svm/LinearSVR.test.ts b/src/svm/LinearSVR.test.ts index 2e325983..b3ba12d2 100644 --- a/src/svm/LinearSVR.test.ts +++ b/src/svm/LinearSVR.test.ts @@ -1,7 +1,7 @@ -import { LinearSVR } from './LinearSVR' - +import { LinearSVR, setBackend } from '../index' +import * as tf from '@tensorflow/tfjs' +setBackend(tf) import { tensorEqual } from '../utils' -import { tf } from '../shared/globals' describe('LinearSVR', function () { it('Works on arrays (small example)', async function () { diff --git a/src/svm/LinearSVR.ts b/src/svm/LinearSVR.ts index f7efe1e0..8891243b 100644 --- a/src/svm/LinearSVR.ts +++ b/src/svm/LinearSVR.ts @@ -14,7 +14,7 @@ // */ import { SGDRegressor } from '../linear_model/SgdRegressor' -import { tf } from '../shared/globals' +import { getBackend } from '../tf-singleton' // First pass at a LinearSVC implementation using gradient descent // Trying to mimic the API of scikit-learn.org/stable/modules/generated/sklearn.linear_model.LinearSVC.html @@ -64,9 +64,6 @@ export interface LinearSVRParams { * ``` */ export class LinearSVR extends SGDRegressor { - /** Useful for pipelines and column transformers to have a default name for transforms */ - name = 'LinearSVR' - constructor({ epsilon = 0, C = 1, @@ -74,6 +71,7 @@ export class LinearSVR extends SGDRegressor { }: LinearSVRParams = {}) { // Assume Binary classification // If we call fit, and it isn't binary then update args + let tf = getBackend() super({ modelCompileArgs: { optimizer: tf.train.adam(0.1), @@ -98,5 +96,7 @@ export class LinearSVR extends SGDRegressor { optimizerType: 'adam', lossType: 'custom' }) + /** Useful for pipelines and column transformers to have a default name for transforms */ + this.name = 'LinearSVR' } } diff --git a/src/svm/SVC.ts b/src/svm/SVC.ts index b8d0ef8a..1c67f6a4 100644 --- a/src/svm/SVC.ts +++ b/src/svm/SVC.ts @@ -1,4 +1,4 @@ -// import { tf } from '../shared/globals' +// import { getBackend } from '../tf-singleton' // import { Scikit1D, Scikit2D } from '../index' // import { SVM, SVMParam, KERNEL_TYPE, ISVMParam, SVM_TYPE } from 'libsvm-wasm' // import { convertToNumericTensor1D, convertToNumericTensor2D } from '../utils' @@ -19,10 +19,10 @@ // } // export class SVC { -// private svm?: SVM -// private svmParam: SVMParam -// private gammaMode = 'scale' -// private classWeight: { [key: number]: number } | 'balanced' | undefined +// svm?: SVM +// svmParam: SVMParam +// gammaMode = 'scale' +// classWeight: { [key: number]: number } | 'balanced' | undefined // constructor({ // kernel = 'RBF', diff --git a/src/svm/SVR.ts b/src/svm/SVR.ts index bf817f79..47dc6b60 100644 --- a/src/svm/SVR.ts +++ b/src/svm/SVR.ts @@ -1,4 +1,4 @@ -// import { tf } from '../shared/globals' +// import { getBackend } from '../tf-singleton' // import { Scikit1D, Scikit2D } from '../index' // import { SVM, SVMParam, KERNEL_TYPE, ISVMParam, SVM_TYPE } from 'libsvm-wasm' // import { convertToNumericTensor1D, convertToNumericTensor2D } from '../utils' @@ -18,9 +18,9 @@ // } // export class SVR { -// private svm?: SVM -// private svmParam: SVMParam -// private gammaMode = 'scale' +// svm?: SVM +// svmParam: SVMParam +// gammaMode = 'scale' // constructor({ // kernel = 'RBF', diff --git a/src/tf-singleton.ts b/src/tf-singleton.ts new file mode 100644 index 00000000..acb5ffb8 --- /dev/null +++ b/src/tf-singleton.ts @@ -0,0 +1,23 @@ +let tf: any = null + +export function setBackend(tfInput: any) { + tf = tfInput +} + +export function getBackend() { + if (tf === null) { + throw Error(` +============================ +Howdy 👋👋. Looks like you are running scikit but you haven't set a Tensorflow backend. +To do so, simply import (or require) your tensorflow library, and call setBackend like so, + +import * as tf from '@tensorflow/tfjs' +import * as sk from 'scikitjs' +sk.setBackend(tf) + +That will let scikit know you wish to use a tensorflow library to perform your calculations. +============================ + `) + } + return tf +} diff --git a/src/tfUtils.ts b/src/tfUtils.ts index 8aee8e65..4616ff7b 100644 --- a/src/tfUtils.ts +++ b/src/tfUtils.ts @@ -13,7 +13,6 @@ * ========================================================================== */ -import { tf as _tf } from './shared/globals' import { assert } from './typesUtils' /** @@ -27,7 +26,7 @@ import { assert } from './typesUtils' * * @param tf The TFJS instance to be polyfilled. */ -export function polyfillUnique(tf: typeof _tf) { +export function polyfillUnique(tf: any) { // TODO: remove this method as soon as tfjs-node supports tf.unique if ( tf.engine().backendNames().includes('tensorflow') && @@ -38,8 +37,8 @@ export function polyfillUnique(tf: typeof _tf) { tf.registerKernel({ kernelName: 'Unique', backendName: 'tensorflow', - kernelFunc: (args) => { - const x = args.inputs.x as _tf.Tensor + kernelFunc: (args: any) => { + const x = args.inputs.x const backend = args.backend as any const { axis } = args.attrs as { axis: number } diff --git a/src/tree/Criterion.test.ts b/src/tree/Criterion.test.ts index 76e88960..c1d8f932 100644 --- a/src/tree/Criterion.test.ts +++ b/src/tree/Criterion.test.ts @@ -1,5 +1,8 @@ import { ClassificationCriterion, giniCoefficient, entropy } from './Criterion' import { fromJSON } from '../simpleSerializer' +import { setBackend } from '../index' +import * as tf from '@tensorflow/tfjs' +setBackend(tf) describe('Criterion', function () { let X = [ [-2, -1], diff --git a/src/tree/DecisionTree.test.ts b/src/tree/DecisionTree.test.ts index 71652fac..c94c1132 100644 --- a/src/tree/DecisionTree.test.ts +++ b/src/tree/DecisionTree.test.ts @@ -1,7 +1,13 @@ -import { DecisionTreeClassifier, DecisionTreeRegressor } from './DecisionTree' +import { + DecisionTreeClassifier, + DecisionTreeRegressor, + setBackend, + fromJSON +} from '../index' import { dataUrls } from '../datasets/datasets' import * as dfd from 'danfojs-node' -import { fromJSON } from '../simpleSerializer' +import * as tf from '@tensorflow/tfjs' +setBackend(tf) describe('DecisionTree', function () { it('Use the DecisionTree (toy)', async function () { diff --git a/src/tree/Splitter.test.ts b/src/tree/Splitter.test.ts index 620aa37e..732c0610 100644 --- a/src/tree/Splitter.test.ts +++ b/src/tree/Splitter.test.ts @@ -1,6 +1,9 @@ import { ImpurityMeasure } from './Criterion' import { Splitter } from './Splitter' import { fromJSON } from '../simpleSerializer' +import { setBackend } from '../index' +import * as tf from '@tensorflow/tfjs' +setBackend(tf) describe('Splitter', function () { let types = ['gini', 'entropy', 'squared_error'] diff --git a/src/types.ts b/src/types.ts index e7a74658..0d782521 100644 --- a/src/types.ts +++ b/src/types.ts @@ -13,7 +13,20 @@ * ========================================================================== */ -import { tf } from './shared/globals' +import { + Scalar, + Tensor1D, + Tensor2D, + TensorLike, + Tensor, + RecursiveArray, + DataType +} from '@tensorflow/tfjs-core/dist/index.d' + +import { + ModelCompileArgs, + ModelFitArgs +} from '@tensorflow/tfjs-layers/dist/index.d' /////////////////////////Danfo types export interface NDframeInterface { @@ -298,11 +311,22 @@ export interface DataFrameInterface extends NDframeInterface { //////////////////////////////////// // The Types that Scikit uses +export { + Tensor1D, + Tensor2D, + TensorLike, + Tensor, + ModelCompileArgs, + ModelFitArgs, + RecursiveArray, + Scalar, + DataType +} export type TypedArray = Float32Array | Int32Array | Uint8Array export type ScikitLike1D = TypedArray | number[] | boolean[] | string[] export type ScikitLike2D = TypedArray[] | number[][] | boolean[][] | string[][] -export type Scikit1D = ScikitLike1D | tf.Tensor1D | SeriesInterface -export type Scikit2D = ScikitLike2D | tf.Tensor2D | DataFrameInterface +export type Scikit1D = ScikitLike1D | Tensor1D | SeriesInterface +export type Scikit2D = ScikitLike2D | Tensor2D | DataFrameInterface export type ScikitVecOrMatrix = Scikit1D | Scikit2D export type OptimizerTypes = | 'sgd' @@ -342,6 +366,6 @@ export type int = number export interface Transformer { fit(X: Scikit2D, y?: Scikit1D): any - transform(X: Scikit2D, y?: Scikit1D): tf.Tensor2D - fitTransform(X: Scikit2D, y?: Scikit1D): tf.Tensor2D + transform(X: Scikit2D, y?: Scikit1D): Tensor2D + fitTransform(X: Scikit2D, y?: Scikit1D): Tensor2D } diff --git a/src/typesUtils.ts b/src/typesUtils.ts index b18e7ac6..26dcfbfd 100644 --- a/src/typesUtils.ts +++ b/src/typesUtils.ts @@ -20,10 +20,13 @@ import { ScikitLike1D, ScikitLike2D, ScikitVecOrMatrix, - SeriesInterface + SeriesInterface, + Tensor, + TensorLike, + DataType } from './types' -import { tf } from './shared/globals' +import { getBackend } from './tf-singleton' export function isString(value: unknown): value is string { return typeof value === 'string' || value instanceof String @@ -42,7 +45,7 @@ export function assert(expr: boolean, msg: string) { throw new Error(msg) } } -export function inferShape(val: tf.TensorLike, dtype?: tf.DataType): number[] { +export function inferShape(val: TensorLike, dtype?: DataType): number[] { let firstElem: typeof val = val if (isTypedArray(val)) { @@ -68,7 +71,7 @@ export function inferShape(val: tf.TensorLike, dtype?: tf.DataType): number[] { } export function deepAssertShapeConsistency( - val: tf.TensorLike, + val: TensorLike, shape: number[], indices: number[] ) { @@ -97,7 +100,7 @@ export function deepAssertShapeConsistency( } } -export function inferDtype(values: tf.TensorLike): tf.DataType | null { +export function inferDtype(values: TensorLike): DataType | null { if (Array.isArray(values)) { return inferDtype(values[0]) } @@ -136,6 +139,11 @@ export function isScikitLike2D(arr: any): arr is ScikitLike2D { return shape.length === 2 && dtype !== null } +export function isTensor(arr: any): arr is Tensor { + let tf = getBackend() + return arr instanceof tf.Tensor +} + export function isSeriesInterface(arr: any): arr is SeriesInterface { if (typeof arr !== 'object') { return false @@ -159,7 +167,7 @@ export function isScikit1D(arr: unknown): arr is Scikit1D { if (isSeriesInterface(arr)) { return true } - if (arr instanceof tf.Tensor) { + if (isTensor(arr)) { return arr.rank === 1 } return isScikitLike1D(arr) @@ -169,7 +177,7 @@ export function isScikit2D(arr: unknown): arr is Scikit2D { if (isDataFrameInterface(arr)) { return true } - if (arr instanceof tf.Tensor) { + if (isTensor(arr)) { return arr.rank === 2 } return isScikitLike2D(arr) diff --git a/src/utils.ts b/src/utils.ts index 2dd62f7f..40fc7e8f 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -24,7 +24,12 @@ import { Scikit2D, ScikitVecOrMatrix, SeriesInterface, - TypedArray + TypedArray, + DataType, + Tensor2D, + Tensor1D, + Tensor, + TensorLike } from './types' import { assert, @@ -32,10 +37,11 @@ import { isDataFrameInterface, isScikitVecOrMatrix, isSeriesInterface, - isTypedArray + isTypedArray, + isTensor } from './typesUtils' +import { getBackend } from './tf-singleton' -import { tf } from './shared/globals' /** * Generates an array of dim (row x column) with inner values set to zero * @param row @@ -77,17 +83,15 @@ export const is1DArray = (arr: ArrayType1D | ArrayType2D): boolean => { * @returns Tensor1D. If you pass in something that isn't 1D, then it will throw an error. * This is the case with 2D Tensors as well. If you really want to reshape them then use tf.reshape */ -export function convertToTensor1D( - data: Scikit1D, - dtype?: tf.DataType -): tf.Tensor1D { +export function convertToTensor1D(data: Scikit1D, dtype?: DataType): Tensor1D { + let tf = getBackend() if (isSeriesInterface(data)) { // Do type inference if no dtype is passed, otherwise try to parse as that dtype return dtype - ? (data.tensor.asType(dtype) as unknown as tf.Tensor1D) - : (data.tensor as unknown as tf.Tensor1D) + ? (data.tensor.asType(dtype) as unknown as Tensor1D) + : (data.tensor as unknown as Tensor1D) } - if (data instanceof tf.Tensor) { + if (isTensor(data)) { if (data.shape.length === 1) { if (!dtype || data.dtype == dtype) { return data @@ -102,7 +106,7 @@ export function convertToTensor1D( return dtype ? tf.tensor1d(data, dtype) : tf.tensor1d(data) } -export function convertToNumericTensor1D(data: Scikit1D, dtype?: tf.DataType) { +export function convertToNumericTensor1D(data: Scikit1D, dtype?: DataType) { const newTensor = convertToTensor1D(data, dtype) if (newTensor.dtype === 'string') { throw new Error( @@ -112,16 +116,14 @@ export function convertToNumericTensor1D(data: Scikit1D, dtype?: tf.DataType) { return newTensor } -export function convertToTensor2D( - data: Scikit2D, - dtype?: tf.DataType -): tf.Tensor2D { +export function convertToTensor2D(data: Scikit2D, dtype?: DataType): Tensor2D { + let tf = getBackend() if (isDataFrameInterface(data)) { return dtype - ? (data.tensor.asType(dtype) as unknown as tf.Tensor2D) - : (data.tensor as unknown as tf.Tensor2D) + ? (data.tensor.asType(dtype) as unknown as Tensor2D) + : (data.tensor as unknown as Tensor2D) } - if (data instanceof tf.Tensor) { + if (isTensor(data)) { if (data.shape.length === 2) { if (!dtype || data.dtype == dtype) { return data @@ -148,14 +150,14 @@ export function convertToTensor2D( export function convertToTensor1D_2D( data: ScikitVecOrMatrix, - dtype?: tf.DataType -): tf.Tensor1D | tf.Tensor2D { + dtype?: DataType +): Tensor1D | Tensor2D { try { - const new1DTensor = convertToTensor1D(data as tf.Tensor1D, dtype) + const new1DTensor = convertToTensor1D(data as Tensor1D, dtype) return new1DTensor } catch (e) { try { - const new2DTensor = convertToTensor2D(data as tf.Tensor2D, dtype) + const new2DTensor = convertToTensor2D(data as Tensor2D, dtype) return new2DTensor } catch (newE) { throw new Error('ParamError: Can"t convert data into 1D or 2D tensor') @@ -163,7 +165,7 @@ export function convertToTensor1D_2D( } } -export function convertToNumericTensor2D(data: Scikit2D, dtype?: tf.DataType) { +export function convertToNumericTensor2D(data: Scikit2D, dtype?: DataType) { const newTensor = convertToTensor2D(data, dtype) if (newTensor.dtype === 'string') { throw new Error( @@ -175,7 +177,7 @@ export function convertToNumericTensor2D(data: Scikit2D, dtype?: tf.DataType) { export function convertToNumericTensor1D_2D( data: ScikitVecOrMatrix, - dtype?: tf.DataType + dtype?: DataType ) { const newTensor = convertToTensor1D_2D(data, dtype) if (newTensor.dtype === 'string') { @@ -187,17 +189,18 @@ export function convertToNumericTensor1D_2D( } export function convertToTensor( - data: tf.TensorLike | tf.Tensor | DataFrameInterface | SeriesInterface, + data: TensorLike | Tensor | DataFrameInterface | SeriesInterface, shape?: number[], - dtype?: keyof tf.DataTypeMap -): tf.Tensor { + dtype?: DataType +): Tensor { + let tf = getBackend() if (isDataFrameInterface(data)) { - return data.tensor as unknown as tf.Tensor2D + return data.tensor as unknown as Tensor2D } if (isSeriesInterface(data)) { - return data.tensor as unknown as tf.Tensor2D + return data.tensor as unknown as Tensor2D } - if (data instanceof tf.Tensor) { + if (isTensor(data)) { let newData = data if (shape) { newData = newData.reshape(shape) @@ -216,10 +219,7 @@ export function convertToTensor( * @param tensor2 * @returns */ -export const shapeEqual = ( - tensor1: tf.Tensor, - tensor2: tf.Tensor -): boolean => { +export const shapeEqual = (tensor1: Tensor, tensor2: Tensor): boolean => { const shape1 = tensor1.shape const shape2 = tensor2.shape if (shape1.length != shape2.length) { @@ -240,13 +240,14 @@ export const shapeEqual = ( * @param */ export const tensorEqual = ( - tensor1: tf.Tensor, - tensor2: tf.Tensor, + tensor1: Tensor, + tensor2: Tensor, tol = 0 ): boolean => { if (!shapeEqual(tensor1, tensor2)) { throw new Error('tensor1 and tensor2 not of same shape') } + let tf = getBackend() return Boolean( tf.lessEqual(tf.max(tf.abs(tf.sub(tensor1, tensor2))), tol).dataSync()[0] ) @@ -278,7 +279,7 @@ export function convertScikit2DToArray( if (isDataFrameInterface(data)) { return data.values as any[][] } - if (data instanceof tf.Tensor) { + if (isTensor(data)) { return data.arraySync() } return data @@ -288,7 +289,7 @@ export function convertScikit1DToArray(data: Scikit1D): any[] | TypedArray { if (isSeriesInterface(data)) { return data.values } - if (data instanceof tf.Tensor) { + if (isTensor(data)) { return data.arraySync() } return data @@ -304,7 +305,7 @@ export function arrayTo2DColumn(array: any[] | TypedArray) { export function getLength(X: Scikit2D | Scikit1D): number { assert(isScikitVecOrMatrix(X), "X isn't a Scikit2D or Scikit1D object") - if (X instanceof tf.Tensor) { + if (isTensor(X)) { return X.shape[0] } if (isDataFrameInterface(X) || isSeriesInterface(X)) { @@ -323,6 +324,7 @@ export function sampleWithoutReplacement( n: number, seed?: number ) { + let tf = getBackend() let curMap = new Map() let finalNumbs = [] let randoms = tf.randomUniform([n], 0, size, 'float32', seed).dataSync() @@ -346,6 +348,7 @@ export function sampleWithoutReplacement( } export function optimizer(opt: OptimizerTypes) { + let tf = getBackend() switch (opt) { case 'sgd': return tf.train.sgd(0.1) @@ -365,6 +368,7 @@ export function optimizer(opt: OptimizerTypes) { } export function getLoss(loss: LossTypes) { + let tf = getBackend() switch (loss) { case 'meanSquaredError': return tf.losses.meanSquaredError @@ -390,6 +394,7 @@ export function getLoss(loss: LossTypes) { } export function initializer(init: Initializers) { + let tf = getBackend() switch (init) { case 'Zeros': return tf.initializers.zeros() diff --git a/tsconfig.build-es5.json b/tsconfig.build-es5.json index 6c6aa1bc..d4572e54 100644 --- a/tsconfig.build-es5.json +++ b/tsconfig.build-es5.json @@ -5,19 +5,6 @@ "outDir": "./dist/es5", "target": "ES5", "downlevelIteration": true, - "baseUrl": "./src", - "paths": { - "shared/*": ["shared/*"] - } - }, - "tsc-alias": { - "verbose": false, - "resolveFullPaths": true, - "replacers": { - "exampleReplacer": { - "enabled": true, - "file": "./build/browserReplacer.js" - } - } + "baseUrl": "./src" } } diff --git a/tsconfig.build-esm.json b/tsconfig.build-esm.json index 1ea8eec2..bc3c1635 100644 --- a/tsconfig.build-esm.json +++ b/tsconfig.build-esm.json @@ -4,19 +4,6 @@ "declarationDir": "./dist/esm", "outDir": "./dist/esm", "target": "esnext", - "baseUrl": "./src", - "paths": { - "shared/*": ["shared/*"] - } - }, - "tsc-alias": { - "verbose": false, - "resolveFullPaths": true, - "replacers": { - "exampleReplacer": { - "enabled": true, - "file": "./build/browserReplacer.js" - } - } + "baseUrl": "./src" } } diff --git a/tsconfig.build-node-gpu.json b/tsconfig.build-node-gpu.json deleted file mode 100644 index f449feaa..00000000 --- a/tsconfig.build-node-gpu.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "extends": "./tsconfig.build.json", - "compilerOptions": { - "declarationDir": "./dist/node", - "outDir": "./dist/node", - "module": "commonjs", - "target": "esnext", - "baseUrl": "./src", - "paths": { - "shared/*": ["shared-node/*"] - } - }, - "tsc-alias": { - "verbose": false, - "resolveFullPaths": true, - "replacers": { - "exampleReplacer": { - "enabled": true, - "file": "./build/nodeGpuReplacer.js" - } - } - } -} diff --git a/tsconfig.build-node.json b/tsconfig.build-node.json index 2d8151fb..ad3ca102 100644 --- a/tsconfig.build-node.json +++ b/tsconfig.build-node.json @@ -5,19 +5,6 @@ "outDir": "./dist/node", "module": "commonjs", "target": "esnext", - "baseUrl": "./src", - "paths": { - "shared/*": ["shared-node/*"] - } - }, - "tsc-alias": { - "verbose": false, - "resolveFullPaths": true, - "replacers": { - "exampleReplacer": { - "enabled": true, - "file": "./build/nodeReplacer.js" - } - } + "baseUrl": "./src" } }