modules/analyze/components/nn.js

import "../../../external/tensorflow/tensorflow.js";

/**
 * Main class for the creation of machine learning models using Tensorflow.
 * @class 
 * @name nn
 */
export default class nn {
  /**
   * Neural network sequential model creator. Depends solely on the type
   * of problem that the user is trying to solve and should be used accordingly.
   * @method createModel
   * @memberof nn
   * @param {Object} params - Contains: numinputs (data inputs), numneurons (total hidden layer neurons), numoutputs (neuron outputs)
   * @returns {Object} Model created based on the specifications.
   * @example
   * hydro.analyze.nn.createModel({params: {numinputs: 30, numneurons: 11, numoutputs:50})
   */

  static createModel({ params, args, data } = {}) {
    var numinputs = params.inputs,
      numneurons = params.neurons,
      numoutputs = params.outputs;
    const model = tf.sequential();

    //Add input layer considering only 1 input layer for the training.
    model.add(
      tf.layers.dense({
        inputShape: [numinputs],
        units: numinputs,
      })
    );

    //Create a 1 layer of convolutioned neurons
    /*model.add(
      tf.layers.conv1d({
        kernelSize: 2,
        filters: 128,
        strides: 1,
        use_bias: true,
        activation: "relu",
        kernelInitializer: 'VarianceScaling'
      })
    );*/

    model.add(
      tf.layers.dense({
        units: numneurons,
        useBias: true,
        activation: "sigmoid",
      })
    );

    //Add average pooling layer
    /*model.add(
      tf.layers.averagePooling1d({
        poolSize: [numinputs],
        strides: [1]
      })
    );*/

    //flatten the lalayters and reshape the input to (number of samples, number of features)
    /*model.add(
      tf.layers.flatten({})
    );*/

    //Add output layer considering only 1 output layer for the training.
    model.add(
      tf.layers.dense({
        units: numoutputs,
        useBias: true,
        activation: "sigmoid",
      })
    );

    //print the model
    return model;
  }

  /**
   * Converts data serving as input for either training or calculations into Tensorflow tensors.
   * @method convertToTensor
   * @memberof nn
   * @param {Object} data - Contains: 2d-JS array with inputs and outputs as [[inputs], [outputs]]
   * @returns {Object} Object with minmax of data as well as the arrays converted into tensors.
   * @example
   * hydro.analyze.nn.convertToTensor({data: [[inputs],[outputs]]})
   */

  static convertToTensor({ params, args, data } = {}) {
    var arr1 = data.inputSet,
      arr2 = data.outputSet;
    return tf.tidy(() => {
      //Convert the data to tensors.
      /*const inputs = arr1.map((d) => d);
      const outputs = arr2.map((d) => d);*/

      const inputTensor = tf.tensor1d(arr1).reshape([1, arr1.length]),
        outputTensor = tf.tensor1d(arr2).reshape([1, arr2.length]),
        //normalizing the data between range 0 - 1.
        inputMax = inputTensor.max(),
        inputMin = inputTensor.min(),
        outputMax = outputTensor.max(),
        outputMin = outputTensor.min(),
        normalizedInputs = inputTensor
          .sub(inputMin)
          .div(inputMax.sub(inputMin)),
        normalizedOutputs = outputTensor
          .sub(outputMin)
          .div(outputMax.sub(outputMin));

      return {
        inputs: normalizedInputs,
        outputs: normalizedOutputs,

        //return the min and max bounds to use afterwards.
        inputMax: inputMax,
        inputMin: inputMin,
        outputMax: outputMax,
        outputMin: outputMin,
      };
    });
  }

  /**
   * Trains the model given inputs and ouputs of training data. Generates weights for each expected outcome.
   * The model can be saved in local storage using the save model function.
   * The compilation is donw using loss function binary Cross entropy, ADAM optimizer and MSE for evaluation metric.
   * @method trainModel
   * @memberof nn
   * @param {Object} params - Contains: model (from createModel function)
   * @param {Object} args - Contains: epochs (1-1000), lr (learning rate between 0-0.2), batch (size depending on inputs/output ratio)
   * @param {Object} data - Contains: 2d-JS array containing the normalized TF arrays for input and output as [[inputs], [outputs]]
   * @returns {Object} Trained model.
   * @example
   * hydro.analyze.nn.trainModel({params:{model: model}, args: {epochs: 'someNum', lr: 'someNum', batch: 'someNum'},
   * data: [[inputs, outputs]]})
   */

  static async trainModel({ params, args, data } = {}) {
    //Grabbing data from the parameters
    var model = params.model,
      inputs = data.inputs,
      outputs = data.outputs,
      //Grabbing data from the arguments
      epochs = args && args.epochs !== undefined ? args.epochs : 50,
      learningrate = args && args.lr !== undefined ? args.lr : 0.001,
      batch = args && args.batch !== undefined ? args.batch : 32;

    //temporary solution for the split method to be fixed on the tf.js backend.
    tf.env().set("WEBGL_CPU_FORWARD", false);

    model.compile({
      loss: "binaryCrossentropy",
      optimizer: "adam",
      metrics: ["mse"],
      lr: learningrate,
    });

    const batchsize = batch;

    return await model.fit(inputs, outputs, {
      batchsize,
      epochs: epochs,
      shuffle: true,
      callbacks: tfvis.show.fitCallbacks(
        {
          name: "Training Performance",
        },
        ["loss", "mse"],
        {
          height: 200,
          callbacks: ["onEpochEnd"],
        }
      ),
    });
  }

  /**
   * Given a trained model, uses it for calculating outputs based on raw data.
   * The data is fed as input on the parameters and needs to be of the same size as the
   * training data. The input data needs to be converted into a TS tensor (array) previous
   * to be fed. The function also requires the minmax of the outputs.
   * @method prediction
   * @memberof nn
   * @param {Object} params - Contains: model (pretrained)
   * @param {Object} args - Contains: outputMin (Minimum value of observation), outputMax (Maximum value of observation)
   * @param {Object} data - Contains: 1d-JS array with inputs for model outcome as [inputs]
   * @returns {Object} Object with predictions as array. It also renders to screen.
   * @example
   * hydro.analyze.nn.prediction({params: {model: model}, args: {outputMin: 'someValue', outputMax: 'someValue'},
   * data: [inputs]})
   */

  static prediction({ params, args, data } = {}) {
    //Grab the data from the arguments.
    var model = params.model,
      inputs = tf.tensor1d(data).reshape([1, data.length]),
      outputMin = args.outputMin,
      outputMax = args.outputMax;
    //Create prediction from model and inputs.
    const predictedPoints = model.predict(inputs),
      //The predictions are normalized, unnormalizing step.
      unNormPreds = predictedPoints
        .mul(outputMax.sub(outputMin))
        .add(outputMin);
    return Array.from(unNormPreds.dataSync());
  }

  /**
   * Function for downloading a model that is already trained. It is saved in
   * the user's download folder.
   * @method saveModel
   * @memberof nn
   * @param {Object} params - Contains: model (pretrained), name (name for the model)
   * @returns {Object} saved model on local storage.
   * @example
   * hydro.analyze.nn.saveModel({params: {model: model, name: 'someName'}})
   */

  static async saveModel({ params, args, data } = {}) {
    var model = params.model,
      name = params.name;
    await model.save(`downloads://${name}`);
  }
}