Skip to content
KarkAngelo114Public

Repository files navigation

Alt text

NPM NPM

How to get started

  1. Ensure you have NodeJS installed on your machine. If you haven't installed it yet, download it here: https://nodejs.org/en/download
  2. Create a new folder and navigate to your created folder in your terminal.
  3. Once you're inside your project directory, install Neurex using this command:
npm install neurex

Documentation

Checkout the documentation for full API reference, live demos, and some starter examples here.

Neurex

Neurex is a Javascript-based, deep learning for Node.js. It supports training on CPU and can also utilized GPU with the help of OpenCL if available. This library supports:

  1. 🧠 Easy model building through sequential stacking ✅
  2. 🛠️ Both CommonJS and ES module importing ✅
  3. 🔃 Retraining and transfer learning ✅
  4. ⚡ GPU acceleration for faster training ✅

Why use Neurex

  1. Modular - Built with a modular structure so you can easily extend.
  2. Simple To Use - Has intuitive, organized high-level APIs for beginners to use without needing to learn low-level machinery of the library.
  3. Educational - Good for experimenting or learning how to build Neural networks.
  4. Production-Ready - Stable for production use and easy model loading and inferencing.

Build your model sequentially

Use built-in layers from the Layers class to build your model.

const { Neurex, Layers } = require('neurex');

(async () => {
    const nrx = new Neurex();
    const layer = new Layers();

    // stack layers in sequential order
    nrx.sequentialBuild([
        layer.inputShape({ features: 3 }),
        layer.connectedLayer(5), // layer size: 5, activation: relu (by default)
        layer.connectedLayer(5), // layer size: 5, activation: relu (by default)
        layer.connectedLayer(5), // layer size: 5, activation: relu (by default)
        layer.connectedLayer(10, 'softmax')
    ]);
})();

Built-in layers:

The Layers class acts as a factory for generating neural network layer configurations. Here are some layers that are avaulable to use:

connectedLayer(layer_size: number, activation: string, useBias: boolean)

  • Allows you to build a layer with number of neurons and the activation function to use in a layer. Stacking more layers will build connected layers or multilayer perceptron
layer.connectedLayer(5, 'tanh');

convolutionalLayer(filters: number, strides: number, kernel_size: number[], activation_function: string, padding: string, useBias: boolean)

  • Allows you to add convolutional layers in your model architecture in sequential building.
layer.convolutionalLayer(12, 1, [3, 3], 'relu', 'same'); // or use 'valid'

embeddingLayer(vocabSize: number, embeddingDim: number, maxSequenceLength: number)

  • Creates an embedding layer for token encoding.
layer.embeddingLayer(5000, 50, 10)

maxPooling(poolSize: number[], strides: number, padding: string)

  • is use for downsampling operation that reduces the spatial dimensions of an input tensor by taking the maximum value over a defined sliding window
layer.maxPooling([2, 2], 2, 'same'); // or use 'valid'

recurrentCell(units: Number, activation_function: string, return_sequence: boolean, useBias: boolean)

  • is the fundamental building block of a Recurrent Neural Network (RNN) designed to process sequential data. It maintains an internal memory by taking its output from the previous time step and feeding it back into itself alongside the new input.
layer.recurrentCell(18, 'tanh', true); // or false if the next layer in the stack is not recurrent

reshape(targetSize: number[]);

  • changes the dimensions (shape) of the data passing through it without changing the data values. This acts as the connector to bridge data from different layers (e.g: from connected layer to convolutional layer).
layer.reshape([28, 28, 1]);

transConvLayer(filters: number, strides: number, kernel_size: number[], activation_function: string, padding: string, useBias: boolean)

  • transConv (or transpose convolution) is a specialized convolutional layer that upsamples incoming tensor map, which does the opposite of the normal convolution
layer.transConvLayer(3, 2, [3, 3], 'linear', 'same', false),

simpleAttention(useBias: boolean)

  • simpleAttention is the implementation of an attention layer in its simpliest and basic form.
layer.simpleAttention()

multiHeadAttention(numHeads: number, useCasualMasking: boolean, useBias: boolean)

  • multiHeadAttention is the advance and improved variant of the existing simpleAttention. It splits Query, Key, and Value projections into multiple independent attention heads.
layer.multiHeadAttention(8, true, true)

layerNorm(epsilon: number)

  • normalizes the activations of the previous layer for each individual sample independently.
layer.layerNorm(1e-5) // default value is `1e-5`

sinusoidalEncoding()

  • Your classic sinusoidal positional encoding
layer.sinusoidalEncoding()

residualStart()

  • The residualStart allows you to start the residual connection. It will cache the input to be use by the residualEnd
layer.residualStart()

residualEnd()

  • The residualEnd marks the end of the residual connection. It will add the cached input set by the residualStart with the output projected by earlier layers.
layer.residualEnd()

For more info about layers, check the official documentation.

Sample usage - training a XOR

Here's an example on how you can use Neurex to train on XOR problem.

const {Neurex, Layers, optimizers, schedulers, gradientNormalizers, modelVisualizer, lossVisualizer, lossLandscapeVisualizer} = require('neurex');

const nrx = new Neurex();
const layer = new Layers();


(async () => {
    const trainX = [
        [0, 0],
        [0, 1],
        [1, 0],
        [1, 1]
    ]

    const trainY = [
        [0],
        [1],
        [1],
        [0]
    ];

    // configurations.  (Note: most of these options are just optional. This is to show the full option object only)
    nrx.configure({
        optimizer: optimizers.Adam(), // use built-in optimizers or plug your own optimizer function here!
        learning_rate: 0.001, // learning rate value
        mode: "cpu", // "gpu" or "auto"
        onFLoat32Module: true, // if set to true, the underlying core engine will use pure JS ops and no need to use "mode"

        visualizerPlugins: [ // visualizer plugins
            modelVisualizer(),
            lossVisualizer(),
            lossLandscapeVisualizer()
        ],

        onChange_optimizer: { // on change optimizer mechanism
            optimizer: optimizers.SGD(), // optimizer to use. Use built-in optimizers or plug your own optimizer function here!
            targetEpoch: 50 // target epoch
        },

        lr_scheduler: schedulers.stepDecay(), // built-in learning rate schedulers or plug your own schedulers

        // gradient normalizers
        gradient_normalizers: [
            gradientNormalizers.clipGradient()
        ]
    });


    // stack layers in sequential order
    nrx.sequentialBuild([
        layer.inputShape({ features:2 }),
        layer.connectedLayer(4), // layer size: 4, activation: relu (by default)
        layer.connectedLayer(1, 'sigmoid')
    ]);


    // you can show the summary of your model by calling modelSummary()
    nrx.modelSummary();

    // train the model
    await nrx.train(trainX, trainY, 'binary_cross_entropy', 1000, 2);

    // save model
    nrx.saveModel('model'); // this will be saved as model.nrx

    // predict
    const predictions = await nrx.predict(trainX);
    console.log(pedictions); // predicted outputs are in float32array. You may convert it to normal JS array if you need
    /*
    * Example:
    * [
    *   Float32Array(1) [ 0.2107422947883606 ]
    * ]
    */
    
    for (let i = 0; i < trainY.length; i++) {
        console.log('Predicted:',[predictions[i][0] > 0.5 ? 1 : 0], 'Actuals:',trainY[i]);
    }
})();

Write your own training loop (For Advance Users)

While train() existed as a high-level API for convenience, Neurex exposes low-level primitive APIs that you can be use for writing custom training loops, offering full control of how you will train your model. By using feedforward(), getOutputlayerDelta(), backpropagation(), and updateParams(), you can create your own training loop and own training rules.

const {Neurex, Layers} = require('neurex');

const nrx = new Neurex();
const layer = new Layers();


(async () => {

    /** ... data preparation, dataset splitting, model construction and configuration ... */


    nrx.setParams(); // <- this is a MUST!

    let batchSize = 12;
    let totalEpoch = 10000;

    // epoch loop:
    for (let epoch = 0; epoch < totalEpoch; epoch++) {
        let batchLoss = 0;

        // batch loop:
        for (let batchStart = 0; batchStart < trainX.length; batchStart += batchSize) {

            let weightGrads, biasGrads; // instantiate accumulator variables to be reference later

            // loop through datasets:
            for (let j = batchStart; j < batchStart + batchSize && j < trainX.length; j++) {

                let input = trainX[j];
                let label = trainY[j];

                // feedforward:
                const {predictions, activations, zs} = nrx.feedforward(new Float32Array(input));

                // get output layer delta:
                const { outputLayerDelta, loss } = nrx.getOutputLayerDelta(predictions, label, zs, 'binary_cross_entropy');
                batchLoss += loss;

                // backprop:
                // the backprop must be placed inside the mini-batch loop to properly accumulate gradients internally before returnig the accumulated gradients.
                const {accumulatedWeightGrads, accumulatedBiasGrads} = nrx.backpropagation(activations, zs, outputLayerDelta);

                weightGrads = accumulatedWeightGrads;
                biasGrads = accumulatedBiasGrads;

            }

            // model param update:
            // model parameter update must be placed outside mini-batch loop to get the accumulated gradients across batches
            // internally, this method do scaling gradients and normalizing gradients before updating them using an optimizer.
            nrx.updateParams(weightGrads, biasGrads);
        }

        console.log(`Epoch ${epoch+1} finished... Loss: ${((batchLoss /= batchSize).toFixed(7))}`);
    }

    // await nrx.train(trainX, trainY, 'binary_cross_entropy', 10000, 12); // commented out to demonstrate custom training loop

    // predict
    const predictions = await nrx.predict(trainX);

    console.log(predictions);
})();

Having those exposed APis and writing custom training loops could theoretically allows you to train multiple model architecture at once or train a GAN-like network model.

One thing to note though - when writing custom training loop, expect that the features that the train() method uses, like using of visualizer tools and learning rate schedulers, etc. will not take effect (yet). The core training APIs only does the process of forwarding the input where even lower-level internals does the processing of data when calling them, traversing the error, and updating the model parameters.

Saving, loading, popping and adding new layers for transfer learning

Saving models is very straightforward.

const {Neurex, Layers} = require('neurex');

(async () => {
        
    const nrx = new Neurex();

    // ... data preprocessing, layer stacking, configurations, train()

    await nrx.saveModel()
})()

Your model will be saved in a binary file which can be use later on.

Note: nrx (or neurex) models are exclusive model file format in Neurex only.

To load .nrx models, you can use loadSavedModel(). This will load and recontruct your trained model

const { Neurex, Layers } = require('neurex');

(async () => {
    const nrx = new Neurex();
    const layer = new Layers();

    await nrx.loadSavedModel("model.nrx");

    nrx.modelSummary(); // prints the model summary
})();

To remove and add a new layer, use pop() and add_layer(). These methods are essentials if you do transfer learning

const { Neurex, Layers } = require('neurex');

(async () => {
    const nrx = new Neurex();
    const layer = new Layers();

    await nrx.loadSavedModel("model.nrx");
    nrx.pop(); 

    // calling more pop() will remove every last layer
    // nrx.pop(); 
    // nrx.pop(); 
    // nrx.pop(); 
    nrx.add_layer(layer.connectedLayer(3,'softmax')) // append a new layer with untrained parameters

    nrx.modelSummary(); // prints the model summary
})();

You can now also export trained nrx models to ONNX to use it on any environment that supports ONNX runtime. This can be done easily using the export_to_ONNX() method in Neurex.

nrx.export_to_ONNX("model"); // this will be saved as model.onnx

Note: this is yet an experimental proof-of-concept showing that it is possible to export to ONNX. This SHOULD NOT be a replacement for the native neurex (.nrx) model as there are no method or functions yet to map back trained models from ONNX to Neurex model internal semantics.

Use built-in templates

Want to train a model immediately? The templates module offers curated templates you can use which you can drop in to the sequentialBuild() method.

const { Neurex, Layers, templates } = require('neurex');

(() => {
    const nrx = new Neurex();
    const layer = new Layers();

    nrx.sequentialBuild([
        layer.inputShape({features: 2}),
        // drop in a connected network having 3 hidden layers, 5 neurons each
        templates.simpleNeuralNetwork(),
        layer.connectedLayer(1, 'sigmoid')
    ])
})();
const { Neurex, Layers, templates } = require('neurex');

(() => {
    const nrx = new Neurex();
    const layer = new Layers();

    nrx.sequentialBuild([
        layer.inputShape({features: 2}),
        // drop in a convolutional network. If "isHeadless" parameter is set to true, the funnel-shape connected layer will be removed. Default is `false`
        templates.simpleCNN(isHeadless = true),
        layer.recurrentCell(18, 'tanh', true),
        layer.recurrentCell(18, 'tanh', true),
        layer.recurrentCell(18, 'tanh'),
        layer.connectedLayer(1, 'sigmoid')
    ])
})();
const { Neurex, Layers, templates } = require('neurex');

(() => {
    const nrx = new Neurex();
    const layer = new Layers();

    nrx.sequentialBuild([
        layer.embeddingLayer(5000, 50, 10),
        templates.vanillaRNN(18, 'relu'),
        layer.connectedLayer(1, 'sigmoid')
    ])
})();

Learn more about neural network templates here.

Experiment and plug your own optimizer and learning rate scheduler (in Development)

Thanks to the updated core engine and flexible API, you can now write and plug your own optimizer and learning rate scheduler!

const { Neurex } = require('neurex');

function MyOptimizer() {
    return function AwesomeOptimizer(data) {
        // destructure to extract the data use for computation
        // tip: log the whole data object to know what the engine gives you for optimizing params
        const { params, grads, state: state = {}, lr } = data; 
        

        // your actual implementation...


        // ALWAYS RETURN UPDATED PARAM AND STATE
        return {
            params: params,
            state: state,
        };
    };
}

function CustomScheduler() {
    return function scheduler(data) {
        // destructure to extract the data use for computation
        // tip: log the whole data object to know what the engine gives you for calculating new learning rate
        const {current_epoch, learning_rate, previousEpochLoss } = data;

        // your actual implementation...

        // ALWAYS RETURN NEWLY CALCULATED LEARNING RATE
        return updated_learning_rate;
    }
}


(() => {
    const nrx = new Neurex();

    nrx.configure({
        optimizer: MyOptimizer(),
        lr_scheduler: CustomScheduler(),
        /* other configs */
    });
})();

Use pluggable monitoring tools (in Development)

Use monitoring tool plugins to monitor training in real-time!

const { Neurex, Layers, lossVisualizer, modelVisualizer, lossLandscapeVisualizer } = require('neurex');

(async () => {
    const nrx = new Neurex();
    const layer = new Layers();
    
    nrx.sequentialBuild([
        layer.inputShape({height: 28, width: 28, depth: 1}),
        ...templates.simpleCNN(), // conv [3, 3] stride = 1 "same" -> maxPool [2, 2] stride = 2 "valid" -> conv [3, 3] stride = 1 "same" -> maxPool [2, 2] stride = 2 "valid" -> dense: 128 -> 64 -> 32
        layer.connectedLayer(10, 'softmax')
    ]);

    nrx.configure({
        /* ... other configs */
        visualizerPlugins: [
            modelVisualizer(),
            lossVisualizer(),
            lossLandscapeVisualizer()
        ]
    })

    await nrx.train(X, Y, 'categorical_cross_entropy', 1000, 12);

})()

Dashboard Dashboard Dashboard

Test the Experimental Upcoming Updates 🔥

If you'd like to try the upcoming major updates before it is officially released on NPM, you can install the latest development version directly from GitHub.

Install from GitHub

npm install git+https://github.com/KarkAngelo114/Neurex.git

Notes

  • APIs may change without notice
  • Some features may be incomplete
  • Documentation may lag behind implementation
  • Expect frequent updates and fixes

This is mainly intended for:

  • early adopters
  • contributors
  • testers
  • developers who want access to the newest features

Feedback and bug reports are highly appreciated 🙌