-
Notifications
You must be signed in to change notification settings - Fork 1.1k
/
autoencoder.ts
209 lines (173 loc) · 6.17 KB
/
autoencoder.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
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>;
private readonly 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 = 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: Array<INeuralNetworkDatum<
Partial<DecodedData>,
Partial<DecodedData>
>> = [];
for (const 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;