Source: bot.js

const Neuron = require("./neuron");
// const Network = require("./network");

/**
 * @typedef Datum
 *
 * @prop {number[]} inputs
 * @prop {number[]} outputs
 *
 * @example
 * {
 *   "inputs": [0,1],
 *   "outputs": [1]
 * }
 */

/**
 * @typedef {Datum[]} Dataset
 *
 * @example
 * [{
 *   "inputs": [0,0],
 *   "outputs": [0]
 * }, {
 *   "inputs": [0,1],
 *   "outputs": [1]
 * }, {
 *   "inputs": [1,0],
 *   "outputs": [1]
 * }, {
 *   "inputs": [1,1],
 *   "outputs": [0]
 * }]
 */

/**
 * @typedef {Object[]} JSON
 *
 * @example
 * [{
 *   "a": 0,
 *   "b": 0,
 *   "c": 0
 * }, {
 *   "a": 0,
 *   "b": 1,
 *   "c": 1
 * }, {
 *   "a": 1,
 *   "b": 0,
 *   "c": 1
 * }, {
 *   "a": 1,
 *   "b": 1,
 *   "c": 0
 * }]
 */

/**
 * @typedef {string} CSV
 *
 * @example
 * a,b,c
 * 0,0,0
 * 0,1,1
 * 1,0,1
 * 1,1,0
 *
 * @example
 * 0,0,0
 * 0,1,1
 * 1,0,1
 * 1,1,0
 *
 * @example
 * 0;0;0
 * 0;1;1
 * 1;0;1
 * 1;1;0
 */

/**
 * @typedef {string} XML
 *
 * @example
 * <?xml version="1.0" encoding="UTF-8"?>
 * <dataset>
 *   <datum>
 *     <a>0</a>
 *     <b>0</b>
 *     <c>0</c>
 *   </datum>
 *   <datum>
 *     <a>0</a>
 *     <b>1</b>
 *     <c>1</c>
 *   </datum>
 *   <datum>
 *     <a>1</a>
 *     <b>0</b>
 *     <c>1</c>
 *   </datum>
 *   <datum>
 *     <a>1</a>
 *     <b>1</b>
 *     <c>0</c>
 *   </datum>
 * </dataset>
 */

/**
 * @constructs Bot
 *
 * @param {Network} [network]
 * @param {Object} [options]
 * @param {Dataset} [options._dataset] Testing dataset
 * @param {Dataset} [options.dataset] Training dataset
 */
function Bot(network, options={}) {
  this.network = network || new Network();
  this.dataset = options.dataset || []; // Training dataset
  this._dataset = options._dataset || options.dataset || []; // Testing dataset
  this.environment;

  this.activate = function(input) {
    return this.network.activate(input);
  }
  this.propagate = function(target) {
    return this.network.propagate(target);
  }
  this.train = function(iterations=1) {
    const self = this;

    let error = 0;

    while(iterations > 0) {
      error = this.dataset.map(function(datum) {
        self.network.activate(datum.inputs);
        return self.network.propagate(datum.outputs);
      }).reduce(function(total, error) {
        return total += error;
      }, 0) / self.dataset.length;

      iterations--;
    }

    return error;
  }

  /**
   * Test the bots performance on the test dataset
   *
   * @param {Object} options
   * @param {boolean} [options.accuracy=false] Iff `true`, returns model accuracy instead of error
   * @param {boolean} [options.round=false] Iff `true`, rounds the output when testing
   *
   * @returns {number} Returns the average error on the test dataset
   */
  this.test = function(options={}) {
    const self = this;

    return this._dataset.map(function(datum) {
      self.network.activate(datum.inputs);
      return self.network.propagate(datum.outputs);
    }).reduce(function(total, error) {
      return total += error;
    }, 0) / self._dataset.length;
  }
}

/**
 * @param {Dataset} dataset
 * @param {Object} [options]
 * @param {boolean|number} [options.train=1] Will train `bot` for `options.train` iterations before creating it
 * @param {number} [options.test=0] Will use `options.test` ratio (e.g. `0.2 === 20%`) of the `dataset` for testing the bot's accuracy
 * @param {boolean} [options.shuffle=false] Iff `true`, the dataset will be shuffled before splitting the dataset or training the bot.
 *
 * @returns {Bot}
 */
Bot.fromDataset = function(dataset, options={}) {
  const shuffle = (array) => array.sort(() => Math.random() - 0.5);
  const inputs = Math.max(...dataset.map(function(datum) {
    return datum.inputs.length
  }));
  const outputs = Math.max(...dataset.map(function(datum) {
    return datum.outputs.length
  }));

  // Shuffle Dataset
  if(options.shuffle) dataset = shuffle(dataset);

  // Create Testing Dataset
  let _dataset = dataset || [];
  if(options.test) {
    options.test = Math.round(dataset.length * options.test) || 1;

    _dataset = dataset.slice(0, options.test);
    dataset = dataset.slice(options.test);
  }

  // Create Bot
  const bot = new Bot(new Network([inputs, outputs]), {
    _dataset,
    dataset
  })

  // Train Bot
  if(options.train) Number.isFinite(options.train) ? bot.train(options.train) : bot.train();

  return bot;
}

/**
 * @param {string} url
 * @param {Object} [options]
 *
 * @example
 * const bot = Bot.fromURL(https://liquidcarrot.io/dataset/monkeys.csv)
 */
Bot.fromURL = function(url, options) {

}

/**
 * @example JSON
 * const bot = Bot.fromPath("./data.train.json");
 *
 * bot.test(dataset); // { error: 0.01457, accuracy: 96.453%, fitness: 34.3412 }
 *
 * @example CSV
 * const bot = Bot.fromPath("./data.train.csv", { outputs: ["age", "height"] });
 *
 * bot.test(dataset); // { error: 0.01457, accuracy: 96.453%, fitness: 34.3412 }
 *
 * @example XML
 * const bot = Bot.fromPath("./data.train.xml");
 *
 * bot.test(dataset); // { error: 0.01457, accuracy: 96.453%, fitness: 34.3412 }
 *
 *
 */
Bot.fromPath = function(path, options) {

}

/**
 * @example Advanced CSV - White Wine Quality
 * const dataset = require("data.cjyvyspsy0000l2m932iv07k1");
 * const bot = Bot.fromString(dataset, {
 *   type: "csv",
 *   headers: true,
 *   outputs: ["quality"],
 *   delimeter: ";",
 *   test: 0.2 // 20% of data will used for testing, not training
 * });
 *
 * bot.test(); // { error: 0.01457, accuracy: 96.453%, fitness: 34.3412 }
 */
Bot.fromString = function(string, options) {

}

Bot.fromStream = function(stream, options) {

}

/**
 * @param {Object[]} json
 * @param {Object} options
 * @param {number} options.test Ratio of dataset to test (e.g. `0.2` is 20%)
 * @param {string[]} options.outputs JSON Keys which hold "outputs" desired outputs - _bots will try to mimic or recreate these keys given all the other keys in the objects given_
 *
 *
 * @example
 * const dataset = require("@liquid-carrot/data.cjyvyspsy0000l2m932iv07k1");
 * const bot = Bot.fromJSON(dataset, {
 *   outputs: ["quality"],
 *   test: 0.2 // 20% of data will used for testing, not training
 * })
 */
Bot.fromJSON = function(json, options) {
  const keys = new Set();

  json.forEach(function(datum) {
    Object.keys(datum).forEach(function(key) {
      keys.add(key);
    })
  })

  const outputs = options.outputs;
  const inputs = Array.from(keys).filter(function(key) {
    return outputs.every(function(output) {
      return output !== key;
    })
  })

  console.log(Array.from(keys));
  console.log(`Inputs: ${inputs.length}`);
  console.log(`Outputs ${outputs.length}`);

  const ineurons = Array.from({ length: inputs.length }, () => new Neuron()); // Input Neurons
  const oneurons = Array.from({ length: outputs.length }, () => new Neuron()); // Output Neurons

  ineurons.forEach(function(ineuron) {
    oneurons.forEach(function(oneuron) {
      ineuron.connect(oneuron);
    })
  })

  const shuffle = (array) => array.sort(() => Math.random() - 0.5);

  json = shuffle(json);

  const testing = Math.round(json.length * options.test);

  const testset = json.slice(0, testing).map(function(entry) {
    const inps = inputs.map(function(input) {
      return entry[input];
    });
    const outs = outputs.map(function(output) {
      return entry[output];
    })

    return {
      inputs: inps,
      outputs: outs
    }
  });
  const trainset = json.slice(testing).map(function(entry) {
    const inps = inputs.map(function(input) {
      return entry[input];
    });
    const outs = outputs.map(function(output) {
      return entry[output];
    })

    return {
      inputs: inps,
      outputs: outs
    }
  });

  console.log(testing);
  console.log(testset.length);
  console.log(trainset.length);

  const activate = function(inputs) {
    ineurons.forEach(function(neuron, index) {
      neuron.activate(inputs[index]);
    });
    return oneurons.map(function(neuron) {
      return neuron.activate();
    })
  }
  const propagate = function(targets) {
    // MSE
    const error = targets.reduce(function(total, target, index) {
      return total += 0.5 * Math.pow(target - oneurons[index].output, 2);
    }, 0)

    oneurons.forEach(function(neuron, index) {
      return neuron.propagate(targets[index],0.5);
    })
    ineurons.forEach(function(neuron) {
      return neuron.propagate(undefined,0.5);
    })

    return error;
  }
  const train = function(iterations=1) {
    function _train() {
      const error = trainset.reduce(function(total, datum) {
        activate(datum.inputs);
        return total += propagate(datum.outputs);
      }, 0)
      return error / trainset.length;
    }
    const error = Array.from({ length: iterations }, () => {
      const error = _train();

      console.log(error);

      return error;
    }).reduce(function(total, error) {
      return total += error;
    }, 0);

    return error / iterations;
  }
  const test = function() {
    // console.log(testset.length);

    const error = testset.reduce(function(total, datum) {
      activate(datum.inputs);
      return total += propagate(datum.outputs);
    }, 0)

    // console.log(error);

    return error / testset.length;
  }

  console.log(train(60));
  console.log(test());
}

module.exports = Bot;