├── .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 |
--------------------------------------------------------------------------------