ruvector/npm/packages/ruvector-cnn/index.js
rUv e743785c7d feat(ruvector-cnn): CNN contrastive learning + SIMD optimization fixes (#252)
* feat: add CNN contrastive learning crate with SIMD optimization

- Add ruvector-cnn crate with SIMD-optimized convolutions and contrastive losses
- Implement InfoNCE (SimCLR) and TripletLoss for contrastive learning
- Add MobileNet-V3 inspired backbone architecture
- Include AVX2, NEON, WASM SIMD support with scalar fallback
- Add WASM bindings (ruvector-cnn-wasm) for browser/Node.js
- Add npm package with TypeScript definitions
- Include comprehensive research docs and ADR-088
- 36 tests passing

Co-Authored-By: claude-flow <ruv@ruv.net>

* feat: add npm package JavaScript wrapper and TypeScript definitions

Co-Authored-By: claude-flow <ruv@ruv.net>

* fix(ruvector-cnn): implement real SIMD and fix stubbed code

## SIMD Implementations (was using scalar fallbacks)
- AVX2: conv_3x3_avx2, conv_3x3_avx2_fma, depthwise_conv_3x3_avx2
- AVX2: global_avg_pool_avx2, max_pool_2x2_avx2
- WASM: conv_3x3_wasm, depthwise_conv_3x3_wasm

All now use real SIMD intrinsics processing 8 (AVX2) or 4 (WASM)
channels simultaneously with scalar fallback for remainders.

## Backbone Fixes
- Deprecated MobileNetV3Small/Large (use unified MobileNetV3 instead)
- Implemented actual block processing in forward() methods
- Fixed hardcoded channel counts in global_avg_pool calls

## Dead Code Fixes
- Added #[allow(dead_code)] for momentum field (used in training)
- Added #[allow(dead_code)] for rng field (feature-gated)
- Added #[cfg(feature = "augmentation")] for rand::Rng import
- Commented out undefined "parallel" feature reference

Co-Authored-By: claude-flow <ruv@ruv.net>

* feat(ruvector-cnn): add Winograd F(2,3) and π-calibrated INT8 quantization

- Add Winograd F(2,3) transforms for 2.25x faster 3x3 convolutions
- Implement π-calibrated INT8 quantization with anti-resonance offsets
- Apply 4x loop unrolling with 4 accumulators to AVX2 convolutions
- Update README with practical intro, capabilities table, benchmarks
- Update npm README with simpler language and examples
- Add CNN image embeddings to root README capabilities

Co-Authored-By: claude-flow <ruv@ruv.net>

* feat: publish @ruvector/cnn v0.1.0 WASM npm package

- Add unsafe blocks for WASM SIMD intrinsics (v128_load/v128_store)
- Disable wasm-opt to avoid SIMD validation issues
- Build and include WASM bindings in npm package
- Update npm package.json with all WASM files
- Published to npm as @ruvector/cnn@0.1.0

Co-Authored-By: claude-flow <ruv@ruv.net>

---------

Co-authored-by: Reuven <cohen@ruv-mac-mini.local>
2026-03-11 17:41:53 -04:00

266 lines
6.9 KiB
JavaScript

/**
* @ruvector/cnn - CNN feature extraction for image embeddings
*
* SIMD-optimized image embedding extraction using contrastive learning.
*
* @example
* ```javascript
* const { CnnEmbedder, InfoNCELoss, SimdOps } = require('@ruvector/cnn');
*
* // Create embedder
* const embedder = new CnnEmbedder({ embeddingDim: 512, normalize: true });
*
* // Extract embedding from image data
* const imageData = new Uint8Array(224 * 224 * 3); // RGB image
* const embedding = embedder.extract(imageData, 224, 224);
*
* // Compute similarity
* const sim = embedder.cosineSimilarity(embedding1, embedding2);
* ```
*/
'use strict';
let wasm = null;
let initialized = false;
/**
* Initialize the WASM module
* @returns {Promise<void>}
*/
async function init() {
if (initialized) return;
if (typeof window !== 'undefined') {
// Browser environment
const wasmModule = await import('./ruvector_cnn_wasm.js');
await wasmModule.default();
wasm = wasmModule;
} else {
// Node.js environment
const fs = require('fs');
const path = require('path');
const wasmPath = path.join(__dirname, 'ruvector_cnn_wasm_bg.wasm');
if (fs.existsSync(wasmPath)) {
const wasmModule = require('./ruvector_cnn_wasm.js');
const wasmBuffer = fs.readFileSync(wasmPath);
await wasmModule.default(wasmBuffer);
wasm = wasmModule;
} else {
throw new Error('WASM file not found. Run `npm run build` first.');
}
}
initialized = true;
}
/**
* CNN Embedder for extracting image features
*/
class CnnEmbedder {
/**
* Create a new CNN embedder
* @param {Object} [config] - Configuration options
* @param {number} [config.inputSize=224] - Input image size (square)
* @param {number} [config.embeddingDim=512] - Output embedding dimension
* @param {boolean} [config.normalize=true] - L2 normalize embeddings
*/
constructor(config = {}) {
if (!initialized) {
throw new Error('Module not initialized. Call init() first.');
}
const wasmConfig = new wasm.EmbedderConfig();
wasmConfig.input_size = config.inputSize || 224;
wasmConfig.embedding_dim = config.embeddingDim || 512;
wasmConfig.normalize = config.normalize !== false;
this._inner = new wasm.WasmCnnEmbedder(wasmConfig);
this._embeddingDim = wasmConfig.embedding_dim;
}
/**
* Extract embedding from image data
* @param {Uint8Array} imageData - RGB image data (row-major, no alpha)
* @param {number} width - Image width
* @param {number} height - Image height
* @returns {Float32Array} - Embedding vector
*/
extract(imageData, width, height) {
const result = this._inner.extract(imageData, width, height);
return new Float32Array(result);
}
/**
* Compute cosine similarity between two embeddings
* @param {Float32Array} a - First embedding
* @param {Float32Array} b - Second embedding
* @returns {number} - Similarity in [-1, 1]
*/
cosineSimilarity(a, b) {
return this._inner.cosine_similarity(a, b);
}
/**
* Get the embedding dimension
* @returns {number}
*/
get embeddingDim() {
return this._embeddingDim;
}
}
/**
* InfoNCE loss for contrastive learning (SimCLR style)
*/
class InfoNCELoss {
/**
* Create InfoNCE loss
* @param {number} [temperature=0.1] - Temperature parameter
*/
constructor(temperature = 0.1) {
if (!initialized) {
throw new Error('Module not initialized. Call init() first.');
}
this._inner = new wasm.WasmInfoNCELoss(temperature);
}
/**
* Compute loss for embedding pairs
* @param {Float32Array} embeddings - Flattened [2N, D] array
* @param {number} batchSize - N (number of pairs)
* @param {number} dim - D (embedding dimension)
* @returns {number} - Loss value
*/
forward(embeddings, batchSize, dim) {
return this._inner.forward(embeddings, batchSize, dim);
}
/**
* Get temperature parameter
* @returns {number}
*/
get temperature() {
return this._inner.temperature;
}
}
/**
* Triplet loss for metric learning
*/
class TripletLoss {
/**
* Create triplet loss
* @param {number} [margin=1.0] - Margin parameter
*/
constructor(margin = 1.0) {
if (!initialized) {
throw new Error('Module not initialized. Call init() first.');
}
this._inner = new wasm.WasmTripletLoss(margin);
}
/**
* Compute triplet loss
* @param {Float32Array} anchors - Anchor embeddings [N, D]
* @param {Float32Array} positives - Positive embeddings [N, D]
* @param {Float32Array} negatives - Negative embeddings [N, D]
* @param {number} dim - Embedding dimension D
* @returns {number} - Loss value
*/
forward(anchors, positives, negatives, dim) {
return this._inner.forward(anchors, positives, negatives, dim);
}
/**
* Get margin parameter
* @returns {number}
*/
get margin() {
return this._inner.margin;
}
}
/**
* SIMD-optimized operations
*/
const SimdOps = {
/**
* Compute dot product of two vectors
* @param {Float32Array} a
* @param {Float32Array} b
* @returns {number}
*/
dotProduct(a, b) {
if (!initialized) throw new Error('Module not initialized');
return wasm.SimdOps.dot_product(a, b);
},
/**
* Apply ReLU activation in-place
* @param {Float32Array} data
*/
relu(data) {
if (!initialized) throw new Error('Module not initialized');
wasm.SimdOps.relu(data);
},
/**
* Apply ReLU6 activation in-place
* @param {Float32Array} data
*/
relu6(data) {
if (!initialized) throw new Error('Module not initialized');
wasm.SimdOps.relu6(data);
},
/**
* L2 normalize a vector in-place
* @param {Float32Array} data
*/
l2Normalize(data) {
if (!initialized) throw new Error('Module not initialized');
wasm.SimdOps.l2_normalize(data);
}
};
/**
* Layer operations for building custom networks
*/
const LayerOps = {
/**
* Apply batch normalization
* @param {Float32Array} input - Input data (modified in-place)
* @param {Float32Array} gamma - Scale parameter
* @param {Float32Array} beta - Shift parameter
* @param {Float32Array} mean - Running mean
* @param {Float32Array} variance - Running variance
* @param {number} [epsilon=1e-5] - Numerical stability
*/
batchNorm(input, gamma, beta, mean, variance, epsilon = 1e-5) {
if (!initialized) throw new Error('Module not initialized');
wasm.LayerOps.batch_norm(input, gamma, beta, mean, variance, epsilon);
},
/**
* Apply global average pooling
* @param {Float32Array} input - Input tensor [C, H, W]
* @param {number} channels - Number of channels
* @param {number} spatialSize - H * W
* @returns {Float32Array} - Output [C]
*/
globalAvgPool(input, channels, spatialSize) {
if (!initialized) throw new Error('Module not initialized');
return new Float32Array(wasm.LayerOps.global_avg_pool(input, channels, spatialSize));
}
};
module.exports = {
init,
CnnEmbedder,
InfoNCELoss,
TripletLoss,
SimdOps,
LayerOps
};