Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a general-purpose autoencoder class AE to serve as a building block for other autoencoder models #932

Merged
merged 14 commits into from
Jun 13, 2024
52 changes: 51 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ GPU accelerated Neural networks in JavaScript for Browsers and Node.js
![CI](https://github.com/BrainJS/brain.js/workflows/CI/badge.svg)
[![codecov](https://codecov.io/gh/BrainJS/brain.js/branch/master/graph/badge.svg?token=3SJIBJ1679)](https://codecov.io/gh/BrainJS/brain.js)
<a href="https://twitter.com/brainjsfnd"><img src="https://img.shields.io/twitter/follow/brainjsfnd?label=Twitter&style=social" alt="Twitter"></a>

[![NPM](https://nodei.co/npm/brain.js.png?compact=true)](https://nodei.co/npm/brain.js/)

</p>
Expand All @@ -43,6 +43,7 @@ GPU accelerated Neural networks in JavaScript for Browsers and Node.js
- [For training with NeuralNetwork](#for-training-with-neuralnetwork)
- [For training with `RNNTimeStep`, `LSTMTimeStep` and `GRUTimeStep`](#for-training-with-rnntimestep-lstmtimestep-and-grutimestep)
- [For training with `RNN`, `LSTM` and `GRU`](#for-training-with-rnn-lstm-and-gru)
- [For training with `AE`](#for-training-with-ae)
- [Training Options](#training-options)
- [Async Training](#async-training)
- [Cross Validation](#cross-validation)
Expand Down Expand Up @@ -317,6 +318,54 @@ net.train([
const output = net.run('I feel great about the world!'); // 'happy'
```

#### For training with `AE`

Each training pattern can either:

- Be an array of numbers
- Be an array of arrays of numbers

Training an autoencoder to compress the values of a XOR calculation:

```javascript
const net = new brain.AE(
{
hiddenLayers: [ 5, 2, 5 ]
}
);

net.train([
[ 0, 0, 0 ],
[ 0, 1, 1 ],
[ 1, 0, 1 ],
[ 1, 1, 0 ]
]);
```

Encoding/decoding:

```javascript
const input = [ 0, 1, 1 ];

const encoded = net.encode(input);
const decoded = net.decode(encoded);
```

Denoise noisy data:

```javascript
const noisyData = [ 0, 1, 0 ];

const data = net.denoise(noisyData);
```

Test for anomalies in data samples:

```javascript
const shouldBeFalse = net.includesAnomalies([0, 1, 1]);
const shouldBeTrue = net.includesAnomalies([0, 1, 0]);
```

### Training Options

`train()` takes a hash of options as its second argument:
Expand Down Expand Up @@ -595,6 +644,7 @@ The user interface used:

- [`brain.NeuralNetwork`](src/neural-network.ts) - [Feedforward Neural Network](https://en.wikipedia.org/wiki/Feedforward_neural_network) with backpropagation
- [`brain.NeuralNetworkGPU`](src/neural-network-gpu.ts) - [Feedforward Neural Network](https://en.wikipedia.org/wiki/Feedforward_neural_network) with backpropagation, GPU version
- [`brain.AE`](src/autoencoder.ts) - [Autoencoder or "AE"](https://en.wikipedia.org/wiki/Autoencoder) with backpropogation and GPU support
- [`brain.recurrent.RNNTimeStep`](src/recurrent/rnn-time-step.ts) - [Time Step Recurrent Neural Network or "RNN"](https://en.wikipedia.org/wiki/Recurrent_neural_network)
- [`brain.recurrent.LSTMTimeStep`](src/recurrent/lstm-time-step.ts) - [Time Step Long Short Term Memory Neural Network or "LSTM"](https://en.wikipedia.org/wiki/Long_short-term_memory)
- [`brain.recurrent.GRUTimeStep`](src/recurrent/gru-time-step.ts) - [Time Step Gated Recurrent Unit or "GRU"](https://en.wikipedia.org/wiki/Gated_recurrent_unit)
Expand Down
79 changes: 79 additions & 0 deletions src/autoencoder.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import AE from "./autoencoder";

const trainingData = [
[0, 0, 0],
[0, 1, 1],
[1, 0, 1],
[1, 1, 0]
];

const xornet = new AE<number[], number[]>(
{
decodedSize: 3,
hiddenLayers: [ 5, 2, 5 ]
}
);

const errorThresh = 0.011;

const result = xornet.train(
trainingData, {
iterations: 100000,
errorThresh
}
);

test(
"denoise a data sample",
async () => {
expect(result.error).toBeLessThanOrEqual(errorThresh);

function xor(...args: number[]) {
return Math.round(xornet.denoise(args)[2]);
}

const run1 = xor(0, 0, 0);
const run2 = xor(0, 1, 1);
const run3 = xor(1, 0, 1);
const run4 = xor(1, 1, 0);

expect(run1).toBe(0);
expect(run2).toBe(1);
expect(run3).toBe(1);
expect(run4).toBe(0);
}
);

test(
"encode and decode a data sample",
async () => {
expect(result.error).toBeLessThanOrEqual(errorThresh);

const run1$input = [0, 0, 0];
const run1$encoded = xornet.encode(run1$input);
const run1$decoded = xornet.decode(run1$encoded);

const run2$input = [0, 1, 1];
const run2$encoded = xornet.encode(run2$input);
const run2$decoded = xornet.decode(run2$encoded);

for (let i = 0; i < 3; i++) expect(Math.round(run1$decoded[i])).toBe(run1$input[i]);
for (let i = 0; i < 3; i++) expect(Math.round(run2$decoded[i])).toBe(run2$input[i]);
}
);

test(
"test a data sample for anomalies",
async () => {
expect(result.error).toBeLessThanOrEqual(errorThresh);

function includesAnomalies(...args: number[]) {
expect(xornet.likelyIncludesAnomalies(args)).toBe(false);
}

includesAnomalies(0, 0, 0);
includesAnomalies(0, 1, 1);
includesAnomalies(1, 0, 1);
includesAnomalies(1, 1, 0);
}
);
189 changes: 189 additions & 0 deletions src/autoencoder.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
import { KernelOutput, Texture, TextureArrayOutput } from "gpu.js";
import { IJSONLayer, INeuralNetworkData, INeuralNetworkDatum, INeuralNetworkTrainOptions } from "./neural-network";
import { INeuralNetworkGPUOptions, NeuralNetworkGPU } from "./neural-network-gpu";
import { INeuralNetworkState } from "./neural-network-types";
import { UntrainedNeuralNetworkError } from "./errors/untrained-neural-network-error";

export interface IAEOptions {
binaryThresh: number;
decodedSize: number;
hiddenLayers: number[];
}

/**
* An autoencoder learns to compress input data down to relevant features and reconstruct input data from its compressed representation.
*/
export class AE<DecodedData extends INeuralNetworkData, EncodedData extends INeuralNetworkData> {
private decoder?: NeuralNetworkGPU<EncodedData, DecodedData>;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we make AE support non-gpu Neural network? Or is there a specific reason why we are tying this to GPU NNs only?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure if it ever made it into mainstream or not, but I already wrote both CPU and GPU implementations of the autoencoder class (Autoencoder and AutoencoderGPU IIRC). If I forgot to create a PR, I'll make one soon 😊

Sorry for the lack of updates for awhile. Health got in the way of work, but I've mostly recovered and am back in good health so I've recently started to work full time again, so I'll try to make more progress with the autoencoder and loss function features 💖

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I made a PR that adds serialization (toJSON & fromJSON) to AE - #950

private denoiser: NeuralNetworkGPU<DecodedData, DecodedData>;

constructor (
options?: Partial<IAEOptions>
) {
// Create default options for the autoencoder.
options ??= {};

// Create default options for the autoencoder's denoiser subnet.
const denoiserOptions: Partial<INeuralNetworkGPUOptions> = {};

// Inherit the binary threshold of the parent autoencoder.
denoiserOptions.binaryThresh = options.binaryThresh;
// Inherit the hidden layers of the parent autoencoder.
denoiserOptions.hiddenLayers = options.hiddenLayers;

// Define the denoiser subnet's input and output sizes.
if (options.decodedSize) denoiserOptions.inputSize = denoiserOptions.outputSize = options.decodedSize;

// Create the denoiser subnet of the autoencoder.
this.denoiser = new NeuralNetworkGPU<DecodedData, DecodedData>(options);
}

/**
* Denoise input data, removing any anomalies from the data.
* @param {DecodedData} input
* @returns {DecodedData}
*/
denoise(input: DecodedData): DecodedData {
// Run the input through the generic denoiser.
// This isn't the best denoiser implementation, but it's efficient.
// Efficiency is important here because training should focus on
// optimizing for feature extraction as quickly as possible rather than
// denoising and anomaly detection; there are other specialized topologies
// better suited for these tasks anyways, many of which can be implemented
// by using an autoencoder.
return this.denoiser.run(input);
}

/**
* Decode `EncodedData` into an approximation of its original form.
*
* @param {EncodedData} input
* @returns {DecodedData}
*/
decode(input: EncodedData): DecodedData {
// If the decoder has not been trained yet, throw an error.
if (!this.decoder) throw new UntrainedNeuralNetworkError(this);

// Decode the encoded input.
return this.decoder.run(input);
}

/**
* Encode data to extract features, reduce dimensionality, etc.
*
* @param {DecodedData} input
* @returns {EncodedData}
*/
encode(input: DecodedData): EncodedData {
// If the decoder has not been trained yet, throw an error.
if (!this.denoiser) throw new UntrainedNeuralNetworkError(this);

// Process the input.
this.denoiser.run(input);

// Get the auto-encoded input.
let encodedInput: TextureArrayOutput = this.encodedLayer as TextureArrayOutput;

// If the encoded input is a `Texture`, convert it into an `Array`.
if (encodedInput instanceof Texture) encodedInput = encodedInput.toArray();
else encodedInput = encodedInput.slice(0);

// Return the encoded input.
return encodedInput as EncodedData;
}

/**
* Test whether or not a data sample likely contains anomalies.
* If anomalies are likely present in the sample, returns `true`.
* Otherwise, returns `false`.
*
* @param {DecodedData} input
* @returns {boolean}
*/
likelyIncludesAnomalies(input: DecodedData, anomalyThreshold: number = 0.2): boolean {
// Create the anomaly vector.
const anomalies: number[] = [];

// Attempt to denoise the input.
const denoised = this.denoise(input);

// Calculate the anomaly vector.
for (let i = 0; i < (input.length ?? 0); i++) {
anomalies[i] = Math.abs((input as number[])[i] - (denoised as number[])[i]);
}

// Calculate the sum of all anomalies within the vector.
const sum = anomalies.reduce(
(previousValue, value) => previousValue + value
);

// Calculate the mean anomaly.
const mean = sum / (input as number[]).length;

// Return whether or not the mean anomaly rate is greater than the anomaly threshold.
return mean > anomalyThreshold;
}

/**
* Train the auto encoder.
*
* @param {DecodedData[]} data
* @param {Partial<INeuralNetworkTrainOptions>} options
* @returns {INeuralNetworkState}
*/
train(data: DecodedData[], options?: Partial<INeuralNetworkTrainOptions>): INeuralNetworkState {
const preprocessedData: INeuralNetworkDatum<Partial<DecodedData>, Partial<DecodedData>>[] = [];

for (let datum of data) {
preprocessedData.push( { input: datum, output: datum } );
}

const results = this.denoiser.train(preprocessedData, options);

this.decoder = this.createDecoder();

return results;
}

/**
* Create a new decoder from the trained denoiser.
*
* @returns {NeuralNetworkGPU<EncodedData, DecodedData>}
*/
private createDecoder() {
const json = this.denoiser.toJSON();

const layers: IJSONLayer[] = [];
const sizes: number[] = [];

for (let i = this.encodedLayerIndex; i < this.denoiser.sizes.length; i++) {
layers.push(json.layers[i]);
sizes.push(json.sizes[i]);
}

json.layers = layers;
json.sizes = sizes;

json.options.inputSize = json.sizes[0];

const decoder = new NeuralNetworkGPU().fromJSON(json);

return decoder as unknown as NeuralNetworkGPU<EncodedData, DecodedData>;
}

/**
* Get the layer containing the encoded representation.
*/
private get encodedLayer(): KernelOutput {
return this.denoiser.outputs[this.encodedLayerIndex];
}

/**
* Get the offset of the encoded layer.
*/
private get encodedLayerIndex(): number {
return Math.round(this.denoiser.outputs.length * 0.5) - 1;
}
}

export default AE;
7 changes: 7 additions & 0 deletions src/errors/untrained-neural-network-error.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export class UntrainedNeuralNetworkError extends Error {
constructor (

Check warning on line 2 in src/errors/untrained-neural-network-error.ts

View workflow job for this annotation

GitHub Actions / Build, lint, and test on Node 16.x and ubuntu-latest

Argument 'neuralNetwork' should be typed with a non-any type

Check warning on line 2 in src/errors/untrained-neural-network-error.ts

View workflow job for this annotation

GitHub Actions / Build, lint, and test on Node 16.x and ubuntu-latest

Unexpected any. Specify a different type

Check warning on line 2 in src/errors/untrained-neural-network-error.ts

View workflow job for this annotation

GitHub Actions / Build, lint, and test on Node 16.x and windows-latest

Argument 'neuralNetwork' should be typed with a non-any type

Check warning on line 2 in src/errors/untrained-neural-network-error.ts

View workflow job for this annotation

GitHub Actions / Build, lint, and test on Node 16.x and windows-latest

Unexpected any. Specify a different type

Check warning on line 2 in src/errors/untrained-neural-network-error.ts

View workflow job for this annotation

GitHub Actions / Build, lint, and test on Node 18.x and ubuntu-latest

Argument 'neuralNetwork' should be typed with a non-any type

Check warning on line 2 in src/errors/untrained-neural-network-error.ts

View workflow job for this annotation

GitHub Actions / Build, lint, and test on Node 18.x and ubuntu-latest

Unexpected any. Specify a different type

Check warning on line 2 in src/errors/untrained-neural-network-error.ts

View workflow job for this annotation

GitHub Actions / Build, lint, and test on Node 18.x and windows-latest

Argument 'neuralNetwork' should be typed with a non-any type

Check warning on line 2 in src/errors/untrained-neural-network-error.ts

View workflow job for this annotation

GitHub Actions / Build, lint, and test on Node 18.x and windows-latest

Unexpected any. Specify a different type

Check warning on line 2 in src/errors/untrained-neural-network-error.ts

View workflow job for this annotation

GitHub Actions / Build, lint, and test on Node 20.x and ubuntu-latest

Argument 'neuralNetwork' should be typed with a non-any type

Check warning on line 2 in src/errors/untrained-neural-network-error.ts

View workflow job for this annotation

GitHub Actions / Build, lint, and test on Node 20.x and ubuntu-latest

Unexpected any. Specify a different type

Check warning on line 2 in src/errors/untrained-neural-network-error.ts

View workflow job for this annotation

GitHub Actions / Build, lint, and test on Node 20.x and windows-latest

Argument 'neuralNetwork' should be typed with a non-any type

Check warning on line 2 in src/errors/untrained-neural-network-error.ts

View workflow job for this annotation

GitHub Actions / Build, lint, and test on Node 20.x and windows-latest

Unexpected any. Specify a different type
neuralNetwork: any
) {

Check failure on line 4 in src/errors/untrained-neural-network-error.ts

View workflow job for this annotation

GitHub Actions / Build, lint, and test on Node 16.x and ubuntu-latest

Invalid type "any" of template literal expression

Check failure on line 4 in src/errors/untrained-neural-network-error.ts

View workflow job for this annotation

GitHub Actions / Build, lint, and test on Node 16.x and windows-latest

Invalid type "any" of template literal expression

Check failure on line 4 in src/errors/untrained-neural-network-error.ts

View workflow job for this annotation

GitHub Actions / Build, lint, and test on Node 18.x and ubuntu-latest

Invalid type "any" of template literal expression

Check failure on line 4 in src/errors/untrained-neural-network-error.ts

View workflow job for this annotation

GitHub Actions / Build, lint, and test on Node 18.x and windows-latest

Invalid type "any" of template literal expression

Check failure on line 4 in src/errors/untrained-neural-network-error.ts

View workflow job for this annotation

GitHub Actions / Build, lint, and test on Node 20.x and ubuntu-latest

Invalid type "any" of template literal expression

Check failure on line 4 in src/errors/untrained-neural-network-error.ts

View workflow job for this annotation

GitHub Actions / Build, lint, and test on Node 20.x and windows-latest

Invalid type "any" of template literal expression
super(`Cannot run a ${neuralNetwork.constructor.name} before it is trained.`);
}
}
2 changes: 2 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import * as activation from './activation';
import { AE } from './autoencoder';
import CrossValidate from './cross-validate';
import { FeedForward } from './feed-forward';
import * as layer from './layer';
Expand Down Expand Up @@ -53,6 +54,7 @@ const utilities = {

export {
activation,
AE,
CrossValidate,
likely,
layer,
Expand Down
Loading