├── .eslintrc.yml ├── .github └── workflows │ ├── documentationjs.yml │ ├── nodejs.yml │ └── release.yml ├── .gitignore ├── .npmrc ├── .prettierrc.json ├── CHANGELOG.md ├── LICENSE ├── README.md ├── babel.config.js ├── package.json ├── rollup.config.js └── src ├── .npmignore ├── __tests__ └── test.js ├── logreg.js └── logreg_2classes.js /.eslintrc.yml: -------------------------------------------------------------------------------- 1 | extends: cheminfo 2 | parserOptions: 3 | sourceType: module 4 | -------------------------------------------------------------------------------- /.github/workflows/documentationjs.yml: -------------------------------------------------------------------------------- 1 | name: Deploy documentation.js on GitHub pages 2 | 3 | on: 4 | workflow_dispatch: 5 | release: 6 | types: [published] 7 | 8 | jobs: 9 | deploy: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v2 13 | - name: Build documentation 14 | uses: zakodium/documentationjs-action@v1 15 | - name: Deploy to GitHub pages 16 | uses: JamesIves/github-pages-deploy-action@releases/v4 17 | with: 18 | token: ${{ secrets.BOT_TOKEN }} 19 | branch: gh-pages 20 | folder: docs 21 | clean: true 22 | -------------------------------------------------------------------------------- /.github/workflows/nodejs.yml: -------------------------------------------------------------------------------- 1 | name: Node.js CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | 9 | jobs: 10 | nodejs: 11 | # Documentation: https://github.com/zakodium/workflows#nodejs-ci 12 | uses: zakodium/workflows/.github/workflows/nodejs.yml@nodejs-v1 13 | with: 14 | node-version-matrix: '[12, 14, 16]' 15 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | release: 10 | # Documentation: https://github.com/zakodium/workflows#release 11 | uses: zakodium/workflows/.github/workflows/release.yml@release-v1 12 | with: 13 | npm: true 14 | secrets: 15 | github-token: ${{ secrets.BOT_TOKEN }} 16 | npm-token: ${{ secrets.NPM_BOT_TOKEN }} 17 | 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (http://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # Typescript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | .idea 61 | 62 | /lib 63 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "arrowParens": "always", 3 | "tabWidth": 2, 4 | "singleQuote": true, 5 | "semi": true, 6 | "trailingComma": "all" 7 | } 8 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | # [2.0.0](https://github.com/mljs/logistic-regression/compare/v1.0.2...v2.0.0) (2020-05-03) 4 | 5 | 6 | ### Bug Fixes 7 | 8 | * README.md ([bab5f0f](https://github.com/mljs/logistic-regression/commit/bab5f0f281a9a127b67a375554e7da6ea345609c)) 9 | 10 | 11 | 12 | 13 | ## [1.0.2](https://github.com/mljs/logistic-regression/compare/v1.0.1...v1.0.2) (2017-07-21) 14 | 15 | 16 | ### Bug Fixes 17 | 18 | * Bug fixed ([da247e8](https://github.com/mljs/logistic-regression/commit/da247e8)) 19 | 20 | 21 | 22 | 23 | ## [1.0.1](https://github.com/mljs/logistic-regression/compare/v1.0.0...v1.0.1) (2017-07-21) 24 | 25 | 26 | 27 | 28 | # 1.0.0 (2017-07-21) 29 | 30 | 31 | ### Bug Fixes 32 | 33 | * fix errors in the tests ([836177f](https://github.com/mljs/logistic-regression/commit/836177f)) 34 | * modifications to use jest ([e19dc5a](https://github.com/mljs/logistic-regression/commit/e19dc5a)) 35 | * Modify name of methods. test() become predict() ([a6aca4c](https://github.com/mljs/logistic-regression/commit/a6aca4c)) 36 | * modify number of steps of the tests (and add the second test) ([c175f82](https://github.com/mljs/logistic-regression/commit/c175f82)) 37 | * Update tests to enable 'npm test' ([d3ac5f9](https://github.com/mljs/logistic-regression/commit/d3ac5f9)) 38 | 39 | 40 | ### Features 41 | 42 | * Add the logistic regression ([78c250c](https://github.com/mljs/logistic-regression/commit/78c250c)) 43 | * add toJSON() and load() ([214a00e](https://github.com/mljs/logistic-regression/commit/214a00e)) 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 ml.js 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # logistic-regression 2 | 3 | [![NPM version][npm-image]][npm-url] 4 | [![build status][ci-image]][ci-url] 5 | [![Test coverage][codecov-image]][codecov-url] 6 | [![npm download][download-image]][download-url] 7 | 8 | This is an implementation of the logistic regression. When there are more than 2 classes, the method used is the _One VS All_. 9 | 10 | ## Installation 11 | 12 | `$ npm i ml-logistic-regression` 13 | 14 | ## Usage 15 | 16 | ```js 17 | const LogisticRegression = require('ml-logistic-regression'); 18 | const { Matrix } = require('ml-matrix'); 19 | 20 | // Our training set (X,Y). 21 | const X = new Matrix([[0, -1], [1, 0], [1, 1], [1, -1], [2, 0], [2, 1], [2, -1], [3, 2], [0, 4], [1, 3], [1, 4], [1, 5], [2, 3], [2, 4], [2, 5], [3, 4], [1, 10], [1, 12], [2, 10], [2, 11], [2, 14], [3, 11]]); 22 | const Y = Matrix.columnVector([0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2]); 23 | 24 | // The test set (Xtest, Ytest). 25 | const Xtest = new Matrix([ 26 | [0, -2], 27 | [1, 0.5], 28 | [1.5, -1], 29 | [1, 2.5], 30 | [2, 3.5], 31 | [1.5, 4], 32 | [1, 10.5], 33 | [2.5, 10.5], 34 | [2, 11.5], 35 | ]); 36 | const Ytest = Matrix.columnVector([0, 0, 0, 1, 1, 1, 2, 2, 2]); 37 | 38 | // We will train our model. 39 | const logreg = new LogisticRegression({ numSteps: 1000, learningRate: 5e-3 }); 40 | logreg.train(X, Y); 41 | 42 | // We try to predict the test set. 43 | const finalResults = logreg.predict(Xtest); 44 | 45 | // Now, you can compare finalResults with the Ytest, which is what you wanted to have. 46 | ``` 47 | 48 | ## License 49 | 50 | [MIT](./LICENSE) 51 | 52 | [npm-image]: https://img.shields.io/npm/v/ml-logistic-regression.svg 53 | [npm-url]: https://npmjs.org/package/ml-logistic-regression 54 | [ci-image]: https://github.com/mljs/logistic-regression/workflows/Node.js%20CI/badge.svg?branch=master 55 | [ci-url]: https://github.com/mljs/logistic-regression/actions?query=workflow%3A%22Node.js+CI%22 56 | [codecov-image]: https://img.shields.io/codecov/c/github/mljs/logistic-regression.svg 57 | [codecov-url]: https://codecov.io/gh/mljs/logistic-regression 58 | [download-image]: https://img.shields.io/npm/dm/ml-logistic-regression.svg 59 | [download-url]: https://npmjs.org/package/ml-logistic-regression 60 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: ['@babel/plugin-transform-modules-commonjs'], 3 | }; 4 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ml-logistic-regression", 3 | "version": "2.0.0", 4 | "description": "Logistic regression", 5 | "main": "lib/logreg.js", 6 | "module": "src/logreg.js", 7 | "files": [ 8 | "lib", 9 | "src" 10 | ], 11 | "scripts": { 12 | "eslint": "eslint src", 13 | "eslint-fix": "npm run eslint -- --fix", 14 | "prepack": "rollup -c", 15 | "prettier": "prettier --check src", 16 | "prettier-write": "prettier --write src", 17 | "test": "npm run test-only && npm run eslint && npm run prettier", 18 | "test-only": "jest --coverage" 19 | }, 20 | "repository": { 21 | "type": "git", 22 | "url": "git+https://github.com/mljs/logistic-regression.git" 23 | }, 24 | "keywords": [ 25 | "machine learning", 26 | "logistic regression", 27 | "classification", 28 | "classifier" 29 | ], 30 | "author": "jajoe", 31 | "license": "MIT", 32 | "bugs": { 33 | "url": "https://github.com/mljs/logistic-regression/issues" 34 | }, 35 | "homepage": "https://github.com/mljs/logistic-regression#readme", 36 | "dependencies": { 37 | "ml-matrix": "^6.9.0" 38 | }, 39 | "devDependencies": { 40 | "@babel/plugin-transform-modules-commonjs": "^7.16.8", 41 | "@types/jest": "^27.4.1", 42 | "eslint": "^8.10.0", 43 | "eslint-config-cheminfo": "^7.2.2", 44 | "jest": "^27.5.1", 45 | "prettier": "^2.5.1", 46 | "rollup": "^2.69.0" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | input: 'src/logreg.js', 3 | output: { 4 | format: 'cjs', 5 | file: 'lib/logreg.js', 6 | }, 7 | }; 8 | -------------------------------------------------------------------------------- /src/.npmignore: -------------------------------------------------------------------------------- 1 | __tests__ 2 | .npmignore 3 | -------------------------------------------------------------------------------- /src/__tests__/test.js: -------------------------------------------------------------------------------- 1 | import { Matrix } from 'ml-matrix'; 2 | 3 | import LogisticRegression from '../logreg.js'; 4 | import LogisticRegressionTwoClasses from '../logreg_2classes.js'; 5 | 6 | describe('Logistic Regression algorithm', () => { 7 | // test for 2 classes 8 | 9 | it('Test of the function used with 2 classes', () => { 10 | let X = new Matrix([ 11 | [0, -1], 12 | [1, 0], 13 | [1, 1], 14 | [1, -1], 15 | [2, 0], 16 | [2, 1], 17 | [2, -1], 18 | [3, 2], 19 | [0, 4], 20 | [1, 3], 21 | [1, 4], 22 | [1, 5], 23 | [2, 3], 24 | [2, 4], 25 | [2, 5], 26 | [3, 4], 27 | ]); 28 | let Y = Matrix.columnVector([ 29 | 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 30 | ]); 31 | 32 | let Xtest = new Matrix([ 33 | [0, -2], 34 | [1, 0.5], 35 | [1.5, -1], 36 | [1, 4.5], 37 | [2, 3.5], 38 | [1.5, 5], 39 | ]); 40 | 41 | let logreg = new LogisticRegressionTwoClasses({ 42 | numSteps: 500, 43 | learningRate: 5e-4, 44 | }); 45 | logreg.train(X, Y); 46 | let results = logreg.predict(Xtest); // compute results of the training set 47 | expect(results).toStrictEqual([0, 0, 0, 1, 1, 1]); 48 | }); 49 | 50 | it('Test of the prediction with 3 classes', () => { 51 | let X = new Matrix([ 52 | [0, -1], 53 | [1, 0], 54 | [1, 1], 55 | [1, -1], 56 | [2, 0], 57 | [2, 1], 58 | [2, -1], 59 | [3, 2], 60 | [0, 4], 61 | [1, 3], 62 | [1, 4], 63 | [1, 5], 64 | [2, 3], 65 | [2, 4], 66 | [2, 5], 67 | [3, 4], 68 | [1, 10], 69 | [1, 12], 70 | [2, 10], 71 | [2, 11], 72 | [2, 14], 73 | [3, 11], 74 | ]); 75 | let Y = Matrix.columnVector([ 76 | 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 77 | ]); 78 | 79 | let Xtest = new Matrix([ 80 | [0, -2], 81 | [1, 0.5], 82 | [1.5, -1], 83 | [1, 2.5], 84 | [2, 3.5], 85 | [1.5, 4], 86 | [1, 10.5], 87 | [2.5, 10.5], 88 | [2, 11.5], 89 | ]); 90 | 91 | let logreg = new LogisticRegression({ numSteps: 1000, learningRate: 5e-3 }); 92 | logreg.train(X, Y); 93 | let finalResults = logreg.predict(Xtest); 94 | expect(finalResults).toStrictEqual([0, 0, 0, 1, 1, 1, 2, 2, 2]); 95 | }); 96 | 97 | it('toJSON and load', () => { 98 | let X = new Matrix([ 99 | [0, -1], 100 | [1, 0], 101 | [1, 1], 102 | [1, -1], 103 | [2, 0], 104 | [2, 1], 105 | [2, -1], 106 | [3, 2], 107 | [0, 4], 108 | [1, 3], 109 | [1, 4], 110 | [1, 5], 111 | [2, 3], 112 | [2, 4], 113 | [2, 5], 114 | [3, 4], 115 | [1, 10], 116 | [1, 12], 117 | [2, 10], 118 | [2, 11], 119 | [2, 14], 120 | [3, 11], 121 | ]); 122 | let Y = Matrix.columnVector([ 123 | 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 124 | ]); 125 | 126 | let Xtest = new Matrix([ 127 | [0, -2], 128 | [1, 0.5], 129 | [1.5, -1], 130 | [1, 2.5], 131 | [2, 3.5], 132 | [1.5, 4], 133 | [1, 10.5], 134 | [2.5, 10.5], 135 | [2, 11.5], 136 | ]); 137 | 138 | let logreg = new LogisticRegression({ numSteps: 1000, learningRate: 5e-3 }); 139 | logreg.train(X, Y); 140 | 141 | let model = JSON.parse(JSON.stringify(logreg)); 142 | let logreg2 = LogisticRegression.load(model); 143 | let finalResults = logreg2.predict(Xtest); 144 | expect(finalResults).toStrictEqual([0, 0, 0, 1, 1, 1, 2, 2, 2]); 145 | }); 146 | }); 147 | -------------------------------------------------------------------------------- /src/logreg.js: -------------------------------------------------------------------------------- 1 | import { Matrix } from 'ml-matrix'; 2 | 3 | import LogisticRegressionTwoClasses from './logreg_2classes.js'; 4 | 5 | function transformClassesForOneVsAll(Y, oneClass) { 6 | let y = Y.to1DArray(); 7 | for (let i = 0; i < y.length; i++) { 8 | if (y[i] === oneClass) { 9 | y[i] = 0; 10 | } else { 11 | y[i] = 1; 12 | } 13 | } 14 | return Matrix.columnVector(y); 15 | } 16 | 17 | export default class LogisticRegression { 18 | constructor(options = {}) { 19 | const { 20 | numSteps = 50000, 21 | learningRate = 5e-4, 22 | classifiers = [], 23 | numberClasses = 0, 24 | } = options; 25 | this.numSteps = numSteps; 26 | this.learningRate = learningRate; 27 | this.classifiers = classifiers; 28 | this.numberClasses = numberClasses; 29 | } 30 | 31 | train(X, Y) { 32 | this.numberClasses = new Set(Y.to1DArray()).size; 33 | this.classifiers = new Array(this.numberClasses); 34 | 35 | // train the classifiers 36 | for (let i = 0; i < this.numberClasses; i++) { 37 | this.classifiers[i] = new LogisticRegressionTwoClasses({ 38 | numSteps: this.numSteps, 39 | learningRate: this.learningRate, 40 | }); 41 | let y = Y.clone(); 42 | y = transformClassesForOneVsAll(y, i); 43 | this.classifiers[i].train(X, y); 44 | } 45 | } 46 | 47 | predict(Xtest) { 48 | let resultsOneClass = new Array(this.numberClasses).fill(0); 49 | let i; 50 | for (i = 0; i < this.numberClasses; i++) { 51 | resultsOneClass[i] = this.classifiers[i].testScores(Xtest); 52 | } 53 | let finalResults = new Array(Xtest.rows).fill(0); 54 | for (i = 0; i < Xtest.rows; i++) { 55 | let minimum = 100000; 56 | for (let j = 0; j < this.numberClasses; j++) { 57 | if (resultsOneClass[j][i] < minimum) { 58 | minimum = resultsOneClass[j][i]; 59 | finalResults[i] = j; 60 | } 61 | } 62 | } 63 | return finalResults; 64 | } 65 | 66 | static load(model) { 67 | if (model.name !== 'LogisticRegression') { 68 | throw new Error(`invalid model: ${model.name}`); 69 | } 70 | const newClassifier = new LogisticRegression(model); 71 | for (let i = 0; i < newClassifier.numberClasses; i++) { 72 | newClassifier.classifiers[i] = LogisticRegressionTwoClasses.load( 73 | model.classifiers[i], 74 | ); 75 | } 76 | return newClassifier; 77 | } 78 | 79 | toJSON() { 80 | return { 81 | name: 'LogisticRegression', 82 | numSteps: this.numSteps, 83 | learningRate: this.learningRate, 84 | numberClasses: this.numberClasses, 85 | classifiers: this.classifiers, 86 | }; 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/logreg_2classes.js: -------------------------------------------------------------------------------- 1 | import { Matrix } from 'ml-matrix'; 2 | 3 | export default class LogisticRegressionTwoClasses { 4 | constructor(options = {}) { 5 | const { numSteps = 50000, learningRate = 5e-4, weights = null } = options; 6 | this.numSteps = numSteps; 7 | this.learningRate = learningRate; 8 | this.weights = weights ? Matrix.checkMatrix(weights) : null; 9 | } 10 | 11 | train(features, target) { 12 | let weights = Matrix.zeros(1, features.columns); 13 | 14 | for (let step = 0; step < this.numSteps; step++) { 15 | const scores = features.mmul(weights.transpose()); 16 | const predictions = sigmoid(scores); 17 | 18 | // Update weights with gradient 19 | const outputErrorSignal = Matrix.columnVector(predictions) 20 | .neg() 21 | .add(target); 22 | const gradient = features.transpose().mmul(outputErrorSignal); 23 | weights = weights.add(gradient.mul(this.learningRate).transpose()); 24 | } 25 | 26 | this.weights = weights; 27 | } 28 | 29 | testScores(features) { 30 | const finalData = features.mmul(this.weights.transpose()); 31 | return sigmoid(finalData); 32 | } 33 | 34 | predict(features) { 35 | const finalData = features.mmul(this.weights.transpose()); 36 | return sigmoid(finalData).map(Math.round); 37 | } 38 | 39 | static load(model) { 40 | return new LogisticRegressionTwoClasses(model); 41 | } 42 | 43 | toJSON() { 44 | return { 45 | numSteps: this.numSteps, 46 | learningRate: this.learningRate, 47 | weights: this.weights, 48 | }; 49 | } 50 | } 51 | 52 | function sigmoid(scores) { 53 | scores = scores.to1DArray(); 54 | let result = []; 55 | for (let i = 0; i < scores.length; i++) { 56 | result.push(1 / (1 + Math.exp(-scores[i]))); 57 | } 58 | return result; 59 | } 60 | --------------------------------------------------------------------------------