ruv-swarm
Version:
High-performance neural network swarm orchestration in WebAssembly
497 lines (393 loc) • 14.4 kB
JavaScript
/**
* Convolutional Neural Network (CNN) Model
* For pattern recognition and image processing tasks
*/
import { NeuralModel } from './base.js';
class CNNModel extends NeuralModel {
constructor(config = {}) {
super('cnn');
// CNN configuration
this.config = {
inputShape: config.inputShape || [28, 28, 1], // [height, width, channels]
convLayers: config.convLayers || [
{ filters: 32, kernelSize: 3, stride: 1, padding: 'same', activation: 'relu' },
{ filters: 64, kernelSize: 3, stride: 1, padding: 'same', activation: 'relu' },
{ filters: 128, kernelSize: 3, stride: 1, padding: 'same', activation: 'relu' },
],
poolingSize: config.poolingSize || 2,
denseLayers: config.denseLayers || [128, 64],
outputSize: config.outputSize || 10,
dropoutRate: config.dropoutRate || 0.5,
...config,
};
// Initialize layers
this.convWeights = [];
this.convBiases = [];
this.denseWeights = [];
this.denseBiases = [];
this.initializeWeights();
}
initializeWeights() {
let currentShape = [...this.config.inputShape];
// Initialize convolutional layers
for (const convLayer of this.config.convLayers) {
const { filters, kernelSize } = convLayer;
const inputChannels = currentShape[2];
// Initialize kernel weights [kernelSize, kernelSize, inputChannels, filters]
const kernelWeights = this.createWeight([
kernelSize,
kernelSize,
inputChannels,
filters,
]);
this.convWeights.push({
kernel: kernelWeights,
shape: [kernelSize, kernelSize, inputChannels, filters],
});
// Initialize biases for each filter
this.convBiases.push(new Float32Array(filters).fill(0));
// Update shape for next layer
currentShape = this.getConvOutputShape(currentShape, convLayer);
// Apply pooling
if (this.config.poolingSize > 1) {
currentShape = [
Math.floor(currentShape[0] / this.config.poolingSize),
Math.floor(currentShape[1] / this.config.poolingSize),
currentShape[2],
];
}
}
// Calculate flattened size
const flattenedSize = currentShape.reduce((a, b) => a * b, 1);
// Initialize dense layers
let lastSize = flattenedSize;
for (const units of this.config.denseLayers) {
this.denseWeights.push(this.createWeight([lastSize, units]));
this.denseBiases.push(new Float32Array(units).fill(0));
lastSize = units;
}
// Output layer
this.denseWeights.push(this.createWeight([lastSize, this.config.outputSize]));
this.denseBiases.push(new Float32Array(this.config.outputSize).fill(0));
}
createWeight(shape) {
const size = shape.reduce((a, b) => a * b, 1);
const weight = new Float32Array(size);
// He initialization for ReLU activation
const fanIn = shape.slice(0, -1).reduce((a, b) => a * b, 1);
const scale = Math.sqrt(2.0 / fanIn);
for (let i = 0; i < size; i++) {
weight[i] = (Math.random() * 2 - 1) * scale;
}
return weight;
}
getConvOutputShape(inputShape, convLayer) {
const [height, width, channels] = inputShape;
const { filters, kernelSize, stride = 1, padding } = convLayer;
let outputHeight, outputWidth;
if (padding === 'same') {
outputHeight = Math.ceil(height / stride);
outputWidth = Math.ceil(width / stride);
} else {
outputHeight = Math.floor((height - kernelSize) / stride) + 1;
outputWidth = Math.floor((width - kernelSize) / stride) + 1;
}
return [outputHeight, outputWidth, filters];
}
async forward(input, training = false) {
let x = input;
// Convolutional layers
for (let i = 0; i < this.config.convLayers.length; i++) {
x = this.conv2d(x, i);
// Apply activation
const { activation } = this.config.convLayers[i];
if (activation === 'relu') {
x = this.relu(x);
}
// Apply pooling
if (this.config.poolingSize > 1) {
x = this.maxPool2d(x, this.config.poolingSize);
}
}
// Flatten
x = this.flatten(x);
// Dense layers
for (let i = 0; i < this.config.denseLayers.length; i++) {
x = this.dense(x, this.denseWeights[i], this.denseBiases[i]);
x = this.relu(x);
// Apply dropout if training
if (training && this.config.dropoutRate > 0) {
x = this.dropout(x, this.config.dropoutRate);
}
}
// Output layer
const outputIndex = this.denseWeights.length - 1;
x = this.dense(x, this.denseWeights[outputIndex], this.denseBiases[outputIndex]);
// Apply softmax for classification
x = this.softmax(x);
return x;
}
conv2d(input, layerIndex) {
const convLayer = this.config.convLayers[layerIndex];
const weights = this.convWeights[layerIndex];
const biases = this.convBiases[layerIndex];
const [batchSize, height, width, inputChannels] = input.shape;
const { filters, kernelSize, stride = 1, padding } = convLayer;
// Calculate output dimensions
const outputShape = this.getConvOutputShape([height, width, inputChannels], convLayer);
const [outputHeight, outputWidth, outputChannels] = outputShape;
const output = new Float32Array(batchSize * outputHeight * outputWidth * outputChannels);
// Apply convolution
for (let b = 0; b < batchSize; b++) {
for (let oh = 0; oh < outputHeight; oh++) {
for (let ow = 0; ow < outputWidth; ow++) {
for (let oc = 0; oc < outputChannels; oc++) {
let sum = biases[oc];
// Apply kernel
for (let kh = 0; kh < kernelSize; kh++) {
for (let kw = 0; kw < kernelSize; kw++) {
for (let ic = 0; ic < inputChannels; ic++) {
let ih = oh * stride + kh;
let iw = ow * stride + kw;
// Handle padding
if (padding === 'same') {
ih -= Math.floor(kernelSize / 2);
iw -= Math.floor(kernelSize / 2);
}
// Check bounds
if (ih >= 0 && ih < height && iw >= 0 && iw < width) {
const inputIdx = b * height * width * inputChannels +
ih * width * inputChannels +
iw * inputChannels + ic;
const weightIdx = kh * kernelSize * inputChannels * filters +
kw * inputChannels * filters +
ic * filters + oc;
sum += input[inputIdx] * weights.kernel[weightIdx];
}
}
}
}
const outputIdx = b * outputHeight * outputWidth * outputChannels +
oh * outputWidth * outputChannels +
ow * outputChannels + oc;
output[outputIdx] = sum;
}
}
}
}
output.shape = [batchSize, outputHeight, outputWidth, outputChannels];
return output;
}
maxPool2d(input, poolSize) {
const [batchSize, height, width, channels] = input.shape;
const outputHeight = Math.floor(height / poolSize);
const outputWidth = Math.floor(width / poolSize);
const output = new Float32Array(batchSize * outputHeight * outputWidth * channels);
for (let b = 0; b < batchSize; b++) {
for (let oh = 0; oh < outputHeight; oh++) {
for (let ow = 0; ow < outputWidth; ow++) {
for (let c = 0; c < channels; c++) {
let maxVal = -Infinity;
// Find max in pool window
for (let ph = 0; ph < poolSize; ph++) {
for (let pw = 0; pw < poolSize; pw++) {
const ih = oh * poolSize + ph;
const iw = ow * poolSize + pw;
if (ih < height && iw < width) {
const inputIdx = b * height * width * channels +
ih * width * channels +
iw * channels + c;
maxVal = Math.max(maxVal, input[inputIdx]);
}
}
}
const outputIdx = b * outputHeight * outputWidth * channels +
oh * outputWidth * channels +
ow * channels + c;
output[outputIdx] = maxVal;
}
}
}
}
output.shape = [batchSize, outputHeight, outputWidth, channels];
return output;
}
flatten(input) {
const [batchSize, ...dims] = input.shape;
const flatSize = dims.reduce((a, b) => a * b, 1);
const output = new Float32Array(batchSize * flatSize);
// Copy data in flattened order
for (let i = 0; i < output.length; i++) {
output[i] = input[i];
}
output.shape = [batchSize, flatSize];
return output;
}
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;
}
softmax(input) {
const [batchSize, size] = input.shape;
const output = new Float32Array(input.length);
for (let b = 0; b < batchSize; b++) {
const offset = b * size;
let maxVal = -Infinity;
// Find max for numerical stability
for (let i = 0; i < size; i++) {
maxVal = Math.max(maxVal, input[offset + i]);
}
// Compute exp and sum
let sumExp = 0;
for (let i = 0; i < size; i++) {
output[offset + i] = Math.exp(input[offset + i] - maxVal);
sumExp += output[offset + i];
}
// Normalize
for (let i = 0; i < size; i++) {
output[offset + i] /= sumExp;
}
}
output.shape = input.shape;
return output;
}
async train(trainingData, options = {}) {
const {
epochs = 10,
batchSize = 32,
learningRate = 0.001,
validationSplit = 0.1,
} = 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 epochAccuracy = 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));
// Forward pass
const predictions = await this.forward(batch.inputs, true);
// Calculate loss and accuracy
const loss = this.crossEntropyLoss(predictions, batch.targets);
const accuracy = this.calculateAccuracy(predictions, batch.targets);
epochLoss += loss;
epochAccuracy += accuracy;
// Backward pass (simplified)
await this.backward(loss, learningRate);
batchCount++;
}
// Validation
const valMetrics = await this.evaluate(valData);
const avgTrainLoss = epochLoss / batchCount;
const avgTrainAccuracy = epochAccuracy / batchCount;
trainingHistory.push({
epoch: epoch + 1,
trainLoss: avgTrainLoss,
trainAccuracy: avgTrainAccuracy,
valLoss: valMetrics.loss,
valAccuracy: valMetrics.accuracy,
});
console.log(
`Epoch ${epoch + 1}/${epochs} - ` +
`Train Loss: ${avgTrainLoss.toFixed(4)}, ` +
`Train Acc: ${(avgTrainAccuracy * 100).toFixed(2)}%, ` +
`Val Loss: ${valMetrics.loss.toFixed(4)}, ` +
`Val Acc: ${(valMetrics.accuracy * 100).toFixed(2)}%`,
);
this.updateMetrics(avgTrainLoss, avgTrainAccuracy);
}
return {
history: trainingHistory,
finalLoss: trainingHistory[trainingHistory.length - 1].trainLoss,
finalAccuracy: trainingHistory[trainingHistory.length - 1].trainAccuracy,
modelType: 'cnn',
};
}
async evaluate(data) {
let totalLoss = 0;
let totalAccuracy = 0;
let batchCount = 0;
for (const batch of data) {
const predictions = await this.forward(batch.inputs, false);
const loss = this.crossEntropyLoss(predictions, batch.targets);
const accuracy = this.calculateAccuracy(predictions, batch.targets);
totalLoss += loss;
totalAccuracy += accuracy;
batchCount++;
}
return {
loss: totalLoss / batchCount,
accuracy: totalAccuracy / batchCount,
};
}
calculateAccuracy(predictions, targets) {
let correct = 0;
const batchSize = predictions.shape[0];
const numClasses = predictions.shape[1];
for (let b = 0; b < batchSize; b++) {
let predClass = 0;
let maxProb = -Infinity;
// Find predicted class
for (let c = 0; c < numClasses; c++) {
const prob = predictions[b * numClasses + c];
if (prob > maxProb) {
maxProb = prob;
predClass = c;
}
}
// Find true class
let trueClass = 0;
for (let c = 0; c < numClasses; c++) {
if (targets[b * numClasses + c] === 1) {
trueClass = c;
break;
}
}
if (predClass === trueClass) {
correct++;
}
}
return correct / batchSize;
}
getConfig() {
return {
type: 'cnn',
...this.config,
parameters: this.countParameters(),
};
}
countParameters() {
let count = 0;
// Convolutional layers
for (let i = 0; i < this.convWeights.length; i++) {
count += this.convWeights[i].kernel.length;
count += this.convBiases[i].length;
}
// Dense layers
for (let i = 0; i < this.denseWeights.length; i++) {
count += this.denseWeights[i].length;
count += this.denseBiases[i].length;
}
return count;
}
}
export { CNNModel };