ruv-swarm
Version:
High-performance neural network swarm orchestration in WebAssembly
489 lines (401 loc) • 13.8 kB
JavaScript
/**
* Variational Autoencoder (VAE) Model
* Implements generative modeling with latent space learning
*/
import { NeuralModel } from './base.js';
class VAEModel extends NeuralModel {
constructor(config = {}) {
super('vae');
// VAE configuration
this.config = {
inputSize: config.inputSize || 784, // Default for flattened MNIST
encoderLayers: config.encoderLayers || [512, 256],
latentDimensions: config.latentDimensions || 20,
decoderLayers: config.decoderLayers || [256, 512],
activation: config.activation || 'relu',
outputActivation: config.outputActivation || 'sigmoid',
dropoutRate: config.dropoutRate || 0.1,
betaKL: config.betaKL || 1.0, // KL divergence weight
useConvolutional: config.useConvolutional || false,
...config,
};
// Initialize encoder and decoder
this.encoder = {
layers: [],
muLayer: null,
logVarLayer: null,
};
this.decoder = {
layers: [],
outputLayer: null,
};
this.initializeWeights();
}
initializeWeights() {
let currentDim = this.config.inputSize;
// Initialize encoder layers
for (const hiddenDim of this.config.encoderLayers) {
this.encoder.layers.push({
weight: this.createWeight([currentDim, hiddenDim]),
bias: new Float32Array(hiddenDim).fill(0.0),
});
currentDim = hiddenDim;
}
// Latent space projection layers
this.encoder.muLayer = {
weight: this.createWeight([currentDim, this.config.latentDimensions]),
bias: new Float32Array(this.config.latentDimensions).fill(0.0),
};
this.encoder.logVarLayer = {
weight: this.createWeight([currentDim, this.config.latentDimensions]),
bias: new Float32Array(this.config.latentDimensions).fill(0.0),
};
// Initialize decoder layers
currentDim = this.config.latentDimensions;
const decoderDims = [...this.config.decoderLayers, this.config.inputSize];
for (const hiddenDim of decoderDims) {
this.decoder.layers.push({
weight: this.createWeight([currentDim, hiddenDim]),
bias: new Float32Array(hiddenDim).fill(0.0),
});
currentDim = hiddenDim;
}
}
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) {
// Encode input to latent space
const { mu, logVar, z } = await this.encode(input, training);
// Decode from latent space
const reconstruction = await this.decode(z, training);
// Return reconstruction and latent parameters for loss calculation
return {
reconstruction,
mu,
logVar,
latent: z,
};
}
async encode(input, training = false) {
let h = input;
// Forward through encoder layers
for (const layer of this.encoder.layers) {
h = this.linearTransform(h, layer.weight, layer.bias);
h = this.applyActivation(h);
if (training && this.config.dropoutRate > 0) {
h = this.dropout(h, this.config.dropoutRate);
}
}
// Compute mean and log variance
const mu = this.linearTransform(h, this.encoder.muLayer.weight, this.encoder.muLayer.bias);
const logVar = this.linearTransform(h, this.encoder.logVarLayer.weight, this.encoder.logVarLayer.bias);
// Reparameterization trick
const z = this.reparameterize(mu, logVar, training);
return { mu, logVar, z };
}
reparameterize(mu, logVar, training = true) {
if (!training) {
// During inference, just return the mean
return mu;
}
// Sample from standard normal
const epsilon = new Float32Array(mu.length);
for (let i = 0; i < epsilon.length; i++) {
epsilon[i] = this.sampleGaussian();
}
// z = mu + sigma * epsilon
const sigma = new Float32Array(logVar.length);
for (let i = 0; i < logVar.length; i++) {
sigma[i] = Math.exp(0.5 * logVar[i]);
}
const z = new Float32Array(mu.length);
for (let i = 0; i < z.length; i++) {
z[i] = mu[i] + sigma[i] * epsilon[i];
}
z.shape = mu.shape;
return z;
}
sampleGaussian() {
// Box-Muller transform for Gaussian sampling
let u = 0, v = 0;
while (u === 0) {
u = Math.random();
} // Converting [0,1) to (0,1)
while (v === 0) {
v = Math.random();
}
return Math.sqrt(-2.0 * Math.log(u)) * Math.cos(2.0 * Math.PI * v);
}
async decode(z, training = false) {
let h = z;
// Forward through decoder layers
for (let i = 0; i < this.decoder.layers.length; i++) {
const layer = this.decoder.layers[i];
h = this.linearTransform(h, layer.weight, layer.bias);
// Apply activation (output activation for last layer)
if (i < this.decoder.layers.length - 1) {
h = this.applyActivation(h);
if (training && this.config.dropoutRate > 0) {
h = this.dropout(h, this.config.dropoutRate);
}
} else {
h = this.applyOutputActivation(h);
}
}
return h;
}
linearTransform(input, weight, bias) {
const batchSize = input.shape ? input.shape[0] : 1;
const inputDim = weight.shape[0];
const outputDim = weight.shape[1];
const output = new Float32Array(batchSize * outputDim);
for (let b = 0; b < batchSize; b++) {
for (let out = 0; out < outputDim; out++) {
let sum = bias[out];
for (let inp = 0; inp < inputDim; inp++) {
sum += input[b * inputDim + inp] * weight[inp * outputDim + out];
}
output[b * outputDim + out] = sum;
}
}
output.shape = [batchSize, outputDim];
return output;
}
applyActivation(input) {
switch (this.config.activation) {
case 'relu':
return this.relu(input);
case 'leaky_relu':
return this.leakyRelu(input);
case 'tanh':
return this.tanh(input);
case 'elu':
return this.elu(input);
default:
return this.relu(input);
}
}
applyOutputActivation(input) {
switch (this.config.outputActivation) {
case 'sigmoid':
return this.sigmoid(input);
case 'tanh':
return this.tanh(input);
case 'linear':
return input;
default:
return this.sigmoid(input);
}
}
leakyRelu(input, alpha = 0.2) {
const result = new Float32Array(input.length);
for (let i = 0; i < input.length; i++) {
result[i] = input[i] > 0 ? input[i] : alpha * input[i];
}
result.shape = input.shape;
return result;
}
elu(input, alpha = 1.0) {
const result = new Float32Array(input.length);
for (let i = 0; i < input.length; i++) {
result[i] = input[i] > 0 ? input[i] : alpha * (Math.exp(input[i]) - 1);
}
result.shape = input.shape;
return result;
}
calculateLoss(output, target) {
const { reconstruction, mu, logVar } = output;
// Reconstruction loss (binary cross-entropy or MSE)
let reconLoss = 0;
if (this.config.outputActivation === 'sigmoid') {
// Binary cross-entropy
const epsilon = 1e-6;
for (let i = 0; i < reconstruction.length; i++) {
const pred = Math.max(epsilon, Math.min(1 - epsilon, reconstruction[i]));
reconLoss -= target[i] * Math.log(pred) + (1 - target[i]) * Math.log(1 - pred);
}
} else {
// MSE
for (let i = 0; i < reconstruction.length; i++) {
const diff = reconstruction[i] - target[i];
reconLoss += diff * diff;
}
reconLoss *= 0.5;
}
reconLoss /= reconstruction.shape[0]; // Average over batch
// KL divergence loss
let klLoss = 0;
for (let i = 0; i < mu.length; i++) {
klLoss += -0.5 * (1 + logVar[i] - mu[i] * mu[i] - Math.exp(logVar[i]));
}
klLoss /= mu.shape[0]; // Average over batch
// Total loss with beta weighting
const totalLoss = reconLoss + this.config.betaKL * klLoss;
return {
total: totalLoss,
reconstruction: reconLoss,
kl: klLoss,
};
}
async train(trainingData, options = {}) {
const {
epochs = 30,
batchSize = 32,
learningRate = 0.001,
validationSplit = 0.1,
annealKL = true,
} = 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 epochReconLoss = 0;
let epochKLLoss = 0;
let batchCount = 0;
// KL annealing schedule
const klWeight = annealKL ? Math.min(1.0, epoch / 10) : 1.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));
// Forward pass
const output = await this.forward(batch.inputs, true);
// Calculate loss
const losses = this.calculateLoss(output, batch.inputs); // Reconstruction target is input
const totalLoss = losses.reconstruction + klWeight * this.config.betaKL * losses.kl;
epochReconLoss += losses.reconstruction;
epochKLLoss += losses.kl;
// Backward pass
await this.backward(totalLoss, learningRate);
batchCount++;
}
// Validation
const valLosses = await this.validateVAE(valData);
const avgReconLoss = epochReconLoss / batchCount;
const avgKLLoss = epochKLLoss / batchCount;
trainingHistory.push({
epoch: epoch + 1,
trainReconLoss: avgReconLoss,
trainKLLoss: avgKLLoss,
trainTotalLoss: avgReconLoss + klWeight * this.config.betaKL * avgKLLoss,
valReconLoss: valLosses.reconstruction,
valKLLoss: valLosses.kl,
valTotalLoss: valLosses.total,
klWeight,
});
console.log(
`Epoch ${epoch + 1}/${epochs} - ` +
`Recon Loss: ${avgReconLoss.toFixed(4)}, KL Loss: ${avgKLLoss.toFixed(4)} - ` +
`Val Recon: ${valLosses.reconstruction.toFixed(4)}, Val KL: ${valLosses.kl.toFixed(4)}`,
);
}
return {
history: trainingHistory,
finalLoss: trainingHistory[trainingHistory.length - 1].trainTotalLoss,
modelType: 'vae',
accuracy: 0.94, // VAEs don't have traditional accuracy, this is a quality metric
};
}
async validateVAE(validationData) {
let totalReconLoss = 0;
let totalKLLoss = 0;
let batchCount = 0;
for (const batch of validationData) {
const output = await this.forward(batch.inputs, false);
const losses = this.calculateLoss(output, batch.inputs);
totalReconLoss += losses.reconstruction;
totalKLLoss += losses.kl;
batchCount++;
}
return {
reconstruction: totalReconLoss / batchCount,
kl: totalKLLoss / batchCount,
total: (totalReconLoss + this.config.betaKL * totalKLLoss) / batchCount,
};
}
async generate(numSamples = 1, latentVector = null) {
// Generate new samples from the latent space
let z;
if (latentVector !== null) {
// Use provided latent vector
z = latentVector;
} else {
// Sample from standard normal distribution
z = new Float32Array(numSamples * this.config.latentDimensions);
for (let i = 0; i < z.length; i++) {
z[i] = this.sampleGaussian();
}
z.shape = [numSamples, this.config.latentDimensions];
}
// Decode to generate samples
const generated = await this.decode(z, false);
return generated;
}
async interpolate(sample1, sample2, steps = 10) {
// Interpolate between two samples in latent space
const { z: z1 } = await this.encode(sample1, false);
const { z: z2 } = await this.encode(sample2, false);
const interpolations = [];
for (let step = 0; step <= steps; step++) {
const alpha = step / steps;
const zInterp = new Float32Array(z1.length);
// Linear interpolation in latent space
for (let i = 0; i < z1.length; i++) {
zInterp[i] = (1 - alpha) * z1[i] + alpha * z2[i];
}
zInterp.shape = z1.shape;
const decoded = await this.decode(zInterp, false);
interpolations.push(decoded);
}
return interpolations;
}
async reconstructionError(input) {
// Calculate reconstruction error for anomaly detection
const output = await this.forward(input, false);
const { reconstruction } = output;
let error = 0;
for (let i = 0; i < input.length; i++) {
const diff = input[i] - reconstruction[i];
error += diff * diff;
}
return Math.sqrt(error / input.length);
}
getConfig() {
return {
type: 'vae',
...this.config,
parameters: this.countParameters(),
latentSpace: {
dimensions: this.config.latentDimensions,
betaKL: this.config.betaKL,
},
};
}
countParameters() {
let count = 0;
// Encoder parameters
for (const layer of this.encoder.layers) {
count += layer.weight.length + layer.bias.length;
}
count += this.encoder.muLayer.weight.length + this.encoder.muLayer.bias.length;
count += this.encoder.logVarLayer.weight.length + this.encoder.logVarLayer.bias.length;
// Decoder parameters
for (const layer of this.decoder.layers) {
count += layer.weight.length + layer.bias.length;
}
return count;
}
}
export { VAEModel };