ruv-swarm
Version:
High-performance neural network swarm orchestration in WebAssembly
543 lines (443 loc) • 16.1 kB
JavaScript
/**
* Autoencoder Neural Network Model
* For dimensionality reduction, feature learning, and data compression
*/
import { NeuralModel } from './base.js';
class AutoencoderModel extends NeuralModel {
constructor(config = {}) {
super('autoencoder');
// Autoencoder configuration
this.config = {
inputSize: config.inputSize || 784, // e.g., 28x28 flattened image
encoderLayers: config.encoderLayers || [512, 256, 128, 64], // Progressive compression
bottleneckSize: config.bottleneckSize || 32, // Latent space dimension
decoderLayers: config.decoderLayers || null, // Mirror of encoder if not specified
activation: config.activation || 'relu',
outputActivation: config.outputActivation || 'sigmoid',
dropoutRate: config.dropoutRate || 0.1,
sparseRegularization: config.sparseRegularization || 0.01,
denoisingNoise: config.denoisingNoise || 0, // For denoising autoencoder
variational: config.variational || false, // For VAE
...config,
};
// Set decoder layers as mirror of encoder if not specified
if (!this.config.decoderLayers) {
this.config.decoderLayers = [...this.config.encoderLayers].reverse();
}
// Initialize network components
this.encoderWeights = [];
this.encoderBiases = [];
this.decoderWeights = [];
this.decoderBiases = [];
// For variational autoencoder
if (this.config.variational) {
this.muLayer = null;
this.logVarLayer = null;
}
this.initializeWeights();
}
initializeWeights() {
let lastSize = this.config.inputSize;
// Initialize encoder layers
for (const units of this.config.encoderLayers) {
this.encoderWeights.push(this.createWeight([lastSize, units]));
this.encoderBiases.push(new Float32Array(units).fill(0));
lastSize = units;
}
// Bottleneck layer
if (this.config.variational) {
// For VAE: separate layers for mean and log variance
this.muLayer = {
weight: this.createWeight([lastSize, this.config.bottleneckSize]),
bias: new Float32Array(this.config.bottleneckSize).fill(0),
};
this.logVarLayer = {
weight: this.createWeight([lastSize, this.config.bottleneckSize]),
bias: new Float32Array(this.config.bottleneckSize).fill(0),
};
lastSize = this.config.bottleneckSize;
} else {
// Standard autoencoder bottleneck
this.encoderWeights.push(this.createWeight([lastSize, this.config.bottleneckSize]));
this.encoderBiases.push(new Float32Array(this.config.bottleneckSize).fill(0));
lastSize = this.config.bottleneckSize;
}
// Initialize decoder layers
for (const units of this.config.decoderLayers) {
this.decoderWeights.push(this.createWeight([lastSize, units]));
this.decoderBiases.push(new Float32Array(units).fill(0));
lastSize = units;
}
// Output layer (reconstruction)
this.decoderWeights.push(this.createWeight([lastSize, this.config.inputSize]));
this.decoderBiases.push(new Float32Array(this.config.inputSize).fill(0));
}
createWeight(shape) {
const size = shape.reduce((a, b) => a * b, 1);
const weight = new Float32Array(size);
// Xavier/Glorot initialization
const scale = Math.sqrt(2.0 / (shape[0] + shape[1]));
for (let i = 0; i < size; i++) {
weight[i] = (Math.random() * 2 - 1) * scale;
}
weight.shape = shape;
return weight;
}
async forward(input, training = false) {
// Add noise for denoising autoencoder
let x = input;
if (training && this.config.denoisingNoise > 0) {
x = this.addNoise(input, this.config.denoisingNoise);
}
// Encode
const encodingResult = await this.encode(x, training);
// Decode
const reconstruction = await this.decode(encodingResult.latent, training);
return {
reconstruction,
latent: encodingResult.latent,
mu: encodingResult.mu,
logVar: encodingResult.logVar,
};
}
async encode(input, training = false) {
let x = input;
// Pass through encoder layers
for (let i = 0; i < this.encoderWeights.length; i++) {
x = this.dense(x, this.encoderWeights[i], this.encoderBiases[i]);
// Apply activation
if (this.config.activation === 'relu') {
x = this.relu(x);
} else if (this.config.activation === 'tanh') {
x = this.tanh(x);
} else if (this.config.activation === 'sigmoid') {
x = this.sigmoid(x);
}
// Apply dropout if training (except last layer)
if (training && this.config.dropoutRate > 0 && i < this.encoderWeights.length - 1) {
x = this.dropout(x, this.config.dropoutRate);
}
}
// Handle variational autoencoder
if (this.config.variational) {
const mu = this.dense(x, this.muLayer.weight, this.muLayer.bias);
const logVar = this.dense(x, this.logVarLayer.weight, this.logVarLayer.bias);
// Reparameterization trick
const latent = training ? this.reparameterize(mu, logVar) : mu;
return { latent, mu, logVar };
}
return { latent: x, mu: null, logVar: null };
}
async decode(latent, training = false) {
let x = latent;
// Pass through decoder layers
for (let i = 0; i < this.decoderWeights.length; i++) {
x = this.dense(x, this.decoderWeights[i], this.decoderBiases[i]);
// Apply activation (use output activation for last layer)
if (i === this.decoderWeights.length - 1) {
if (this.config.outputActivation === 'sigmoid') {
x = this.sigmoid(x);
} else if (this.config.outputActivation === 'tanh') {
x = this.tanh(x);
}
// 'linear' means no activation
} else {
// Hidden layers
if (this.config.activation === 'relu') {
x = this.relu(x);
} else if (this.config.activation === 'tanh') {
x = this.tanh(x);
} else if (this.config.activation === 'sigmoid') {
x = this.sigmoid(x);
}
// Apply dropout if training
if (training && this.config.dropoutRate > 0) {
x = this.dropout(x, this.config.dropoutRate);
}
}
}
return x;
}
dense(input, weights, biases) {
const [batchSize, inputSize] = input.shape;
const outputSize = biases.length;
const output = new Float32Array(batchSize * outputSize);
for (let b = 0; b < batchSize; b++) {
for (let o = 0; o < outputSize; o++) {
let sum = biases[o];
for (let i = 0; i < inputSize; i++) {
sum += input[b * inputSize + i] * weights[i * outputSize + o];
}
output[b * outputSize + o] = sum;
}
}
output.shape = [batchSize, outputSize];
return output;
}
addNoise(input, noiseLevel) {
const noisy = new Float32Array(input.length);
for (let i = 0; i < input.length; i++) {
// Add Gaussian noise
const noise = (Math.random() - 0.5) * 2 * noiseLevel;
noisy[i] = Math.max(0, Math.min(1, input[i] + noise));
}
noisy.shape = input.shape;
return noisy;
}
reparameterize(mu, logVar) {
// VAE reparameterization trick: z = mu + sigma * epsilon
const [batchSize, latentSize] = mu.shape;
const z = new Float32Array(batchSize * latentSize);
for (let b = 0; b < batchSize; b++) {
for (let l = 0; l < latentSize; l++) {
const idx = b * latentSize + l;
const epsilon = this.sampleGaussian(); // N(0, 1)
const sigma = Math.exp(0.5 * logVar[idx]);
z[idx] = mu[idx] + sigma * epsilon;
}
}
z.shape = mu.shape;
return z;
}
sampleGaussian() {
// Box-Muller transform for sampling from standard normal distribution
let u = 0, v = 0;
while (u === 0) {
u = Math.random();
}
while (v === 0) {
v = Math.random();
}
return Math.sqrt(-2.0 * Math.log(u)) * Math.cos(2.0 * Math.PI * v);
}
calculateLoss(input, output, mu = null, logVar = null) {
const [batchSize] = input.shape;
// Reconstruction loss (MSE or binary cross-entropy)
let reconstructionLoss = 0;
if (this.config.outputActivation === 'sigmoid') {
// Binary cross-entropy for outputs in [0, 1]
for (let i = 0; i < input.length; i++) {
const epsilon = 1e-7;
const pred = Math.max(epsilon, Math.min(1 - epsilon, output.reconstruction[i]));
reconstructionLoss -= input[i] * Math.log(pred) + (1 - input[i]) * Math.log(1 - pred);
}
} else {
// MSE for continuous outputs
for (let i = 0; i < input.length; i++) {
const diff = input[i] - output.reconstruction[i];
reconstructionLoss += diff * diff;
}
}
reconstructionLoss /= batchSize;
// KL divergence for VAE
let klLoss = 0;
if (this.config.variational && mu && logVar) {
for (let i = 0; i < mu.length; i++) {
klLoss += -0.5 * (1 + logVar[i] - mu[i] * mu[i] - Math.exp(logVar[i]));
}
klLoss /= batchSize;
}
// Sparsity regularization (encourage sparse activations)
let sparsityLoss = 0;
if (this.config.sparseRegularization > 0) {
const targetSparsity = 0.05; // Target average activation
const latentMean = output.latent.reduce((a, b) => a + b, 0) / output.latent.length;
sparsityLoss = this.config.sparseRegularization * Math.abs(latentMean - targetSparsity);
}
return {
total: reconstructionLoss + klLoss + sparsityLoss,
reconstruction: reconstructionLoss,
kl: klLoss,
sparsity: sparsityLoss,
};
}
async train(trainingData, options = {}) {
const {
epochs = 10,
batchSize = 32,
learningRate = 0.001,
validationSplit = 0.1,
beta = 1.0, // Beta-VAE parameter
} = options;
const trainingHistory = [];
// Split data
const splitIndex = Math.floor(trainingData.length * (1 - validationSplit));
const trainData = trainingData.slice(0, splitIndex);
const valData = trainingData.slice(splitIndex);
for (let epoch = 0; epoch < epochs; epoch++) {
let epochLoss = 0;
let epochReconLoss = 0;
let epochKLLoss = 0;
let batchCount = 0;
// Shuffle training data
const shuffled = this.shuffle(trainData);
// Process batches
for (let i = 0; i < shuffled.length; i += batchSize) {
const batch = shuffled.slice(i, Math.min(i + batchSize, shuffled.length));
// Prepare batch input
const batchInput = {
data: batch.inputs,
shape: [batch.inputs.length, this.config.inputSize],
};
batchInput.data.shape = batchInput.shape;
// Forward pass
const output = await this.forward(batchInput.data, true);
// Calculate losses
const losses = this.calculateLoss(
batchInput.data,
output,
output.mu,
output.logVar,
);
// Apply beta weighting for VAE
const totalLoss = losses.reconstruction + beta * losses.kl + losses.sparsity;
epochLoss += totalLoss;
epochReconLoss += losses.reconstruction;
epochKLLoss += losses.kl;
// Backward pass
await this.backward(totalLoss, learningRate);
batchCount++;
}
// Validation
const valLosses = await this.evaluate(valData);
const avgTrainLoss = epochLoss / batchCount;
const avgReconLoss = epochReconLoss / batchCount;
const avgKLLoss = epochKLLoss / batchCount;
const historyEntry = {
epoch: epoch + 1,
trainLoss: avgTrainLoss,
reconstructionLoss: avgReconLoss,
klLoss: avgKLLoss,
valLoss: valLosses.total,
valReconstructionLoss: valLosses.reconstruction,
};
trainingHistory.push(historyEntry);
console.log(
`Epoch ${epoch + 1}/${epochs} - ` +
`Loss: ${avgTrainLoss.toFixed(4)} ` +
`(Recon: ${avgReconLoss.toFixed(4)}, ` +
`KL: ${avgKLLoss.toFixed(4)}) - ` +
`Val Loss: ${valLosses.total.toFixed(4)}`,
);
this.updateMetrics(avgTrainLoss);
}
return {
history: trainingHistory,
finalLoss: trainingHistory[trainingHistory.length - 1].trainLoss,
modelType: 'autoencoder',
};
}
async evaluate(data) {
let totalLoss = 0;
let reconLoss = 0;
let klLoss = 0;
let batchCount = 0;
for (const batch of data) {
const batchInput = {
data: batch.inputs,
shape: [batch.inputs.length, this.config.inputSize],
};
batchInput.data.shape = batchInput.shape;
const output = await this.forward(batchInput.data, false);
const losses = this.calculateLoss(batchInput.data, output, output.mu, output.logVar);
totalLoss += losses.total;
reconLoss += losses.reconstruction;
klLoss += losses.kl;
batchCount++;
}
return {
total: totalLoss / batchCount,
reconstruction: reconLoss / batchCount,
kl: klLoss / batchCount,
};
}
// Get only the encoder part for feature extraction
async getEncoder() {
return {
encode: async(input) => {
const result = await this.encode(input, false);
return result.latent;
},
config: {
inputSize: this.config.inputSize,
bottleneckSize: this.config.bottleneckSize,
layers: this.config.encoderLayers,
},
};
}
// Get only the decoder part for generation
async getDecoder() {
return {
decode: async(latent) => {
return await this.decode(latent, false);
},
config: {
bottleneckSize: this.config.bottleneckSize,
outputSize: this.config.inputSize,
layers: this.config.decoderLayers,
},
};
}
// Generate new samples (for VAE)
async generate(numSamples = 1) {
if (!this.config.variational) {
throw new Error('Generation is only available for variational autoencoders');
}
// Sample from standard normal distribution
const latent = new Float32Array(numSamples * this.config.bottleneckSize);
for (let i = 0; i < latent.length; i++) {
latent[i] = this.sampleGaussian();
}
latent.shape = [numSamples, this.config.bottleneckSize];
// Decode to generate samples
return await this.decode(latent, false);
}
// Interpolate between two inputs
async interpolate(input1, input2, steps = 10) {
// Encode both inputs
const encoded1 = await this.encode(input1, false);
const encoded2 = await this.encode(input2, false);
const interpolations = [];
for (let step = 0; step <= steps; step++) {
const alpha = step / steps;
const interpolatedLatent = new Float32Array(encoded1.latent.length);
// Linear interpolation in latent space
for (let i = 0; i < interpolatedLatent.length; i++) {
interpolatedLatent[i] = (1 - alpha) * encoded1.latent[i] + alpha * encoded2.latent[i];
}
interpolatedLatent.shape = encoded1.latent.shape;
// Decode interpolated latent vector
const decoded = await this.decode(interpolatedLatent, false);
interpolations.push(decoded);
}
return interpolations;
}
getConfig() {
return {
type: 'autoencoder',
variant: this.config.variational ? 'variational' : 'standard',
...this.config,
parameters: this.countParameters(),
};
}
countParameters() {
let count = 0;
// Encoder parameters
for (let i = 0; i < this.encoderWeights.length; i++) {
count += this.encoderWeights[i].length;
count += this.encoderBiases[i].length;
}
// VAE-specific parameters
if (this.config.variational) {
count += this.muLayer.weight.length + this.muLayer.bias.length;
count += this.logVarLayer.weight.length + this.logVarLayer.bias.length;
}
// Decoder parameters
for (let i = 0; i < this.decoderWeights.length; i++) {
count += this.decoderWeights[i].length;
count += this.decoderBiases[i].length;
}
return count;
}
}
export { AutoencoderModel };