Source: ANN.js

/**
 * @license
 * Copyright 2020 Roberto Luiz Souza Monteiro,
 *                Renata Souza Barreto,
 *                Hernane Borges de Barros Pereira.
 *
 * Licensed under the Apache License, Version 2.0 (the 'License');
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at;
 *
 *   http://www.apache.org/licenses/LICENSE-2.0;
 *
 * Unless required by applicable law or agreed to in writing, software;
 * distributed under the License is distributed on an 'AS IS' BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, eithermath.express or implied.
 * See the License for the specific language governing permissions and;
 * limitations under the License.
 */

/**
 * MaiaScript Artificial Neural Network (ANN) library.
 * @class
 */
function ANN() {
    init();

    /**
     * Creates the attributes of the class.
     */
    function init() {
        // Class attributes goes here.
    }

    /**
     * Creates an untrained artificial neural network.
     * @param {string}   topology - Graph topology. It can be:
     *                              complete, random, small world,
     *                              scale-free, hybrid or mlp.
     * @param {number}   numVertices - Number of vertices.
     * @param {number}   numEdges - Number of edges.
     * @param {number}   edgeProbability - Edge probability.
     * @param {number}   averageDegree - Average degree.
     * @param {number}   ni - Number of input neurons.
     * @param {number}   no - Number of output neurons.
     * @param {number}   nl - Number of layers.
     * @param {number}   nhu - Number of hidden units.
     * @return {object}  A neural network.
     */
    this.createANN = function(topology, numVertices, numEdges, edgeProbability, averageDegree, ni, no, nl, nhu) {
        if (typeof topology == 'undefined') {
            var topology = 'complete';
        }
        if (typeof numVertices != 'undefined') {
            var n = numVertices;
        } else {
            var n = 0;
        }
        if (typeof numEdges != 'undefined') {
            var m = numEdges;
        } else {
            var m = 0;
        }
        if (typeof edgeProbability != 'undefined') {
            var p = edgeProbability;
        } else {
            var p = 0;
        }
        if (typeof averageDegree != 'undefined') {
            var d = averageDegree;
        } else {
            var d = 0;
        }
        if (typeof ni == 'undefined') {
            var ni = 0;
        }
        if (typeof no == 'undefined') {
            var no = 0;
        }
        if (typeof nl == 'undefined') {
            var nl = 0;
        }
        if (typeof nhu == 'undefined') {
            var nhu = 0;
        }
        // Create a Multi-layer Perceptron (MLP)
        if (topology == 'mlp') {
            var n = ni + nl * nhu + no;
        }
        // Create a complete graph.
        if (topology == 'complete') {
            var NN = core.matrix(1, n + 1, n + 1);
        } else {
            var NN = core.matrix(0, n + 1, n + 1);
        }
        dimNN = core.dim(NN);
        // Create a random graph.
        if (topology == 'random') {
            // Calculate the edge probability.
            if (d > 0) {
                p = d / (n - 1);
            }
            // Calculate the number of edge.
            if ((m == 0) && (p > 0)) {
                e = n / 2 * (n - 1) * p;
            } else {
                e = m;
            }
            while (e > 0) {
                i = math.round(math.random() * n);
                j = math.round(math.random() * n);
                if (!((i == j) || (i == 0) || (j == 0))) {
                    if ((NN[i][j] == 0) && (NN[j][i] == 0)) {
                        NN[i][j] = 1;
                        NN[j][i] = 1;
                        e--;
                    }
                }
            }
        // Create a small world network.
        } else if (topology == 'smallworld') {
            // Create the initial random network.
            for (var i = 1; i < dimNN[0]; i = i + 1) {
                while (true) {
                    ki = matrix.count(NN, i, 1, i, dimNN[1] - 1);
                    if (ki < d) {
                        j = math.round(math.random() * n);
                        if ((i != j) && (j != 0)) {
                            NN[i][j] = 1;
                            NN[j][i] = 1;
                        }
                    } else {
                        break;
                    }
                }
            }
            // Rewire network with edge probability p.
            for (var i = 1; i < dimNN[0]; i = i + 1) {
                for (var j = 1; j < dimNN[1]; j = j + 1) {
                    if (NN[i][j] == 1) {
                        pij = math.random();
                        if (pij < p) {
                            while (true) {
                                k = math.round(math.random() * n);
                                if ((k != 0) && (i != k) && (NN[i][k] == 0)) {
                                    NN[i][j] = 0;
                                    NN[j][i] = 0;
                                    NN[i][k] = 1;
                                    NN[k][i] = 1;
                                    break;
                                }
                            }
                        }
                    }
                }
            }
        // Create a scale-free network.
        } else if (topology == 'scalefree') {
            // Create the initial random network.
            for (var i = 1; i < dimNN[0]; i = i + 1) {
                while (true) {
                    ki = matrix.count(NN, i, 1, i, dimNN[1] - 1);
                    if (ki == 0) {
                        j = math.round(math.random() * n);
                        if ((j != 0) && (i != j)) {
                            NN[i][j] = 1;
                            NN[j][i] = 1;
                            break;
                        }
                    } else {
                        break;
                    }
                }
            }
            // Add new edges with probability p.
            for (var i = 1; i < dimNN[0]; i = i + 1) {
                for (var j = 1; j < dimNN[1]; j = j + 1) {
                    if ((i != j) && (NN[i][j] == 0)) {
                        ki = matrix.count(NN, i, 1, i, dimNN[1] - 1);
                        if (ki < d) {
                            sk = matrix.sum(NN, 1, 1, dimNN[0] - 1, dimNN[1] - 1);
                            p = math.random();
                            pi = ki / sk;
                            if (pi < p) {
                                NN[i][j] = 1;
                                NN[j][i] = 1;
                            }
                        } else {
                            break;
                        }
                    }
                }
            }
        // Create an hybrid (scale-free small world) network.
        } else if (topology == 'hybrid') {
            // Create the small world network.
            // Create the initial random network.
            for (var i = 1; i < dimNN[0]; i = i + 1) {
                while (true) {
                    ki = matrix.count(NN, i, 1, i, dimNN[1] - 1);
                    if (ki < d) {
                        j = math.round(math.random() * n);
                        if ((j != 0) && (i != j)) {
                            NN[i][j] = 1;
                            NN[j][i] = 1;
                        }
                    } else {
                        break;
                    }
                }
            }
            // Rewire network with edge probability p.
            for (var i = 1; i < dimNN[0]; i = i + 1) {
                for (var j = 1; j < dimNN[1]; j = j + 1) {
                    if (ANN[i][j] == 1) {
                        pij = math.random();
                        if (pij < p) {
                            while (true) {
                                k = math.round(math.random() * n);
                                if ((k != 0) && (i != k) && (NN[i][k] == 0)) {
                                    NN[i][j] = 0;
                                    NN[j][i] = 0;
                                    NN[i][k] = 1;
                                    NN[k][i] = 1;
                                    break;
                                }
                            }
                        }
                    }
                }
            }
            // Change it to scale-free.
            // Add new edges with probability p.
            for (var i = 1; i < dimNN[0]; i = i + 1) {
                for (var j = 1; j < dimNN[1]; j = j + 1) {
                    if ((i != j) && (NN[i][j] == 0)) {
                        ki = matrix.count(NN, i, 1, i, dimNN[1] - 1);
                        if (ki < d) {
                            sk = matrix.sum(NN, 1, 1, dimNN[0] - 1, dimNN[1] - 1);
                            p = math.random();
                            pi = ki / sk;
                            if (pi < p) {
                                NN[i][j] = 1;
                                NN[j][i] = 1;
                            }
                        } else {
                            break;
                        }
                    }
                }
            }
        } else if (topology == 'mlp') {
            var lindex = 0;
            var nindex = 1;
            // Create synapses.
            // Connect inputs to the first layer.
            nindex = ni;
            for (var i = 1; i <= ni; i++) {
                for (var j = 1; j <= nhu; j++) {
                    NN[i][j + nindex] = 1;
                }
            }
            // Connect hidden layers.
            for (var l = 1; l < nl; l++) {
                n1index = ni + (l - 1) * nhu;
                n2index = ni + l * nhu;
                for (var i = 1; i <= nhu; i++) {
                    for (var j = 1; j <= nhu; j++) {
                        NN[i + n1index][j + n2index] = 1;
                    }
                    //NN[i + n1index][i + n1index] = 1;
                }
            }
            // Connect last layer to outputs.
            n1index = ni + (nl - 1) * nhu;
            n2index = ni + nl * nhu;
            for (var i = 1; i <= nhu; i++) {
                for (var j = 1; j <= no; j++) {
                    NN[i + n1index][j + n2index] = 1;
                    //NN[j + n2index][j + n2index] = 1;
                }
                //NN[i + n1index, i + n1index] = 1;
            }
            // Add the neurons labels.
            lindex = 0;
            nindex = 1;
            for (var i = 1; i < dimNN[0]; i++) {
                if (lindex == 0) {
                    label = "i" + nindex;
                    nindex++;
                    if (nindex > ni) {
                        lindex++;
                        nindex = 1;
                    }
                } else if ((lindex > 0) & (lindex <= nl)) {
                    label = "h" + lindex + "," + nindex;
                    nindex++;
                    if (nindex > nhu) {
                        lindex++;
                        nindex = 1;
                    }
                } else {
                    label = "o" + nindex;
                    nindex++;
                }
                NN[0][i] = label;
                NN[i][0] = label;
            }
        }
        // Add loops (for neural networks).
        if (ni > 0) {
            for (var i = ni + 1; i < dimNN[0]; i = i + 1) {
                NN[i][i] = 1;
            }
        } else {
            // Remove loops.
            for (var i = 0; i < dimNN[0]; i = i + 1) {
                NN[i][i] = 0;
            }
        }
        if (topology == 'mlp') {
            // Add the neurons labels.
            lindex = 0;
            nindex = 1;
            for (var i = 1; i < dimNN[0]; i++) {
                if (lindex == 0) {
                    label = "i" + nindex;
                    nindex++;
                    if (nindex > ni) {
                        lindex++;
                        nindex = 1;
                    }
                } else if ((lindex > 0) & (lindex <= nl)) {
                    label = "h" + lindex + "," + nindex;
                    nindex++;
                    if (nindex > nhu) {
                        lindex++;
                        nindex = 1;
                    }
                } else {
                    label = "o" + nindex;
                    nindex++;
                }
                NN[0][i] = label;
                NN[i][0] = label;
            }
        } else {
            // Add the vertices labels.
            for (var i = 1; i < dimNN[0]; i = i + 1) {
                NN[0][i] = 'v' + i;
                NN[i][0] = 'v' + i;
            }
        }
        return NN;
    }

    /**
     * Returns the labels of an adjacency matrix.
     * @param {object}   NN - Adjacency matrix.
     * @return {object}  The labels of an adjacency matrix.
     */
    this.getLabels = function(NN) {
        var dimNN = core.dim(NN);
        var dimI = dimNN[0];
        var labels = [''];
        for (var i = 1; i < dimI; i++) {
            labels.push(NN[i][0]);
        }
        return(labels);
    }

    /**
     * Trains an artificial neural network, represented as an adjacency matrix.
     * @param {object}   NN - Adjacency matrix.
     * @param {object}   inMatrix - Input data for training.
     * @param {object}   outMatrix - Output data for training.
     * @param {number}   ni - Number of input neurons.
     * @param {number}   no - Number of output neurons.
     * @param {number}   lRate - Learning rate.
     * @param {string}   AF - Activation function. It can be:
     *                        linear, logistic or tanh.
     * @param {string}   OAF - Activation function of the last layer. It can be:
     *                         linear, logistic or tanh.
     * @return {object}  Trained neural network.
     */
    this.learn = function(NN, inMatrix, outMatrix, ni, no, lRate, AF, OAF) {
        if (typeof ni == 'undefined') {
            var ni = 0;
        }
        if (typeof no == 'undefined') {
            var no = 0;
        }
        if (typeof lRate == 'undefined') {
            var lRate = 1;
        }
        if (typeof AF == 'undefined') {
            var AF = 'logistic';
        }
        if (typeof OAF == 'undefined') {
            var OAF = 'linear';
        }
        var dimNN = core.dim(NN);
        var dimI = dimNN[0];
        var dimJ = dimNN[1];
        var firstOut = dimJ - 1 - no;
        // Clear inputs and outputs.
        for (var i = 0; i < dimI - 1; i++) {
            NN[0][i] = 0.0;
            NN[i][0] = 0.0;
            NN[i][dimJ - 1] = 0.0;
            NN[dimI - 1][i] = 0.0;
        }
        // Assign inputs.
        for (var j = 0; j < ni; j++) {
            NN[j + 1][0] = inMatrix[j];
        }
        // Calculate the neurons output.
        for (var j = ni + 1; j < (dimJ - 1); j++) {
            NN[0][j] = 0.0;
            // Weighted sums.
            // x = x1 * w1 + x2 * w2 + ...
            for (var i = 1; i < (dimI - 1); i++) {
                if (i < j) {
                    if (NN[i][j] != 0) {
                        NN[0][j] = NN[0][j] + NN[i][j] * NN[i][0];
                    }
                } else if (i == j) {
                    if (NN[i][j] != 0) {
                        NN[0][j] = NN[0][j] + NN[i][j];
                    }
                } else {
                    break;
                };
            }
            // Activation function.
            if (j < firstOut) {
                // Linear: f(x) = x
                //         df(x)/dx = 1
                if (AF == 'linear') {
                    // Calculate y = f(x)
                    NN[j][0] = NN[0, j];
                    // Calculate df(x)/dx for backpropagation.
                    NN[j][dimJ - 1] = 1.0;
                // Logistic: f(x) = 1.0 / (1.0 + e^(-x))
                //          df(x)/dx = f(x) * (1 - f(x))
                } else if (AF == 'logistic') {
                    // Calculate y = f(x)
                    NN[j][0] = 1.0 / (1.0 + math.exp(-1.0 * NN[0][j]));
                    // Calculate df(x)/dx for backpropagation.
                    NN[j][dimJ - 1] = NN[j][0] * (1.0 - NN[j][0]);
                // Hyperbolic tangent: f(x) = 2 / (1 + e^(-2x)) - 1
                //                     df(x)/dx = 1 - f(x)^2
                } else if (AF == 'tanh') {
                    // Calculate y = f(x)
                    NN[j][0] = 2.0 / (1.0 + math.exp(-2.0 * NN[0][j])) - 1.0;
                    // Calculate df(x)/dx for backpropagation.
                    NN[j][dimJ- 1] = 1.0 - NN[j][0] * NN[j][0];
                // Logistic: f(x) = 1.0 / (1.0 + e^(-x))
                //          df(x)/dx = f(x) * (1 - f(x))
                } else {
                    // Calculate y = f(x)
                    NN[j][0] = 1.0 / (1.0 + math.exp(-1.0 * NN[0][j]));
                    // Calculate df(x)/dx for backpropagation.
                    NN[j][dimJ - 1] = NN[j][0] * (1.0 - NN[j][0]);
                }
            } else {
                // Linear: f(x) = x
                //         df(x)/dx = 1
                if (OAF == 'linear') {
                    // Calculate y = f(x)
                    NN[j][0] = NN[0][j];
                    // Calculate df(x)/dx for backpropagation.
                    NN[j][dimJ - 1] = 1.0;
                // Logistic: f(x) = 1.0 / (1.0 + e^(-x))
                //          df(x)/dx = f(x) * (1 - f(x))
                } else if (OAF == 'logistic') {
                    // Calculate y = f(x)
                    NN[j][0] = 1.0 / (1.0 + math.exp(-1.0 * NN[0][j]));
                    // Calculate df(x)/dx for backpropagation.
                    NN[j][dimJ - 1] = NN[j][0] * (1.0 - NN[j][0]);
                // Hyperbolic tangent: f(x) = 2 / (1 + e^(-2x)) - 1
                //                     df(x)/dx = 1 - f(x)^2
                } else if (OAF == 'tanh') {
                    // Calculate y = f(x)
                    NN[j][0] = 2.0 / (1.0 + math.exp(-2.0 * NN[0][j])) - 1.0;
                    // Calculate df(x)/dx for backpropagation.
                    NN[j][dimJ - 1] = 1.0 - NN[j][0] * NN[j][0];
                // Logistic: f(x) = 1.0 / (1.0 + e^(-x))
                //          df(x)/dx = f(x) * (1 - f(x))
                } else {
                    // Calculate y = f(x)
                    NN[j][0] = 1.0 / (1.0 + math.exp(-1.0 * NN[0][j]));
                    // Calculate df(x)/dx for backpropagation.
                    NN[j][dimJ - 1] = NN[j][0] * (1.0 - NN[j][0]);
                }
            }
        }
        // Calculate delta for the output neurons.
        // d = z - y;
        for (var i = 0; i < no; i++) {
            NN[dimI - 1][firstOut + i] = outMatrix[i] - NN[firstOut + i][0];
        }
        // Calculate delta for hidden neurons.
        // d1 = w1 * d2 + w2 * d2 + ...
        for (var j = dimJ - 2; j > ni; j--) {
            for (i = ni + 1; i < (dimI - 1 - no); i++) {
                if (i == j) {
                    break;
                }
                if (NN[i][j] != 0) {
                    NN[dimI - 1][i] = NN[dimI - 1][i] + NN[i][j] * NN[dimI - 1][j];
                }
            }
        }
        // Adjust weights.
        // x = x1 * w1 + x2 * w2 + ...
        // w1 = w1 + n * d * df(x)/dx * x1
        // w2 = w2 + n * d * df(x)/dx * x2
        for (var j = no + 1; j < (dimJ - 1); j++) {
            for (var i = 1; i < (dimI - 1 - no); i++) {
                if (i < j) {
                    if (NN[i][j] != 0) {
                        NN[i][j] = NN[i][j] + lRate * NN[dimI - 1][j] * NN[j][dimJ - 1] * NN[i][0];
                    }
                } else if (i == j) {
                    if (NN[i][j] != 0) {
                        NN[i][j] = NN[i][j] + lRate * NN[dimI - 1][j] * NN[j][dimJ - 1];
                    }
                } else {
                    break;
                }
            }
        }
        return NN;
    }

    /**
     * It prepares a neural network, represented as an adjacency matrix,
     * replacing cells with value one (1), with random real numbers.
     * @param {object}   ANNMatrix - Adjacency matrix.
     * @param {boolean}  randomize - Fill cells with random real numbers.
     * @param {boolean}  allowLoops - Allow loops.
     * @param {boolean}  negativeWeights - Allow negative weights.
     * @return {object}  Matrix filled with random numbers.
     */
    this.prepare = function(ANNMatrix, randomize, allowLoops, negativeWeights) {
        if (typeof randomize == 'undefined') {
            var randomize = false;
        }
        if (typeof allowLoops == 'undefined') {
            var allowLoops = false;
        }
        if (typeof negativeWeights == 'undefined') {
            var negativeWeights = false;
        }
        var dimANN = core.dim(ANNMatrix);
        var dimI = dimANN[0];
        var dimJ = dimANN[1];
        let NN = core.matrix(0.0, dimI + 1, dimJ + 1);
        for (var i = 1; i < dimI; i++) {
            for (var j = 1; j < dimJ; j++) {
                NN[i][j] = ANNMatrix[i][j];
            }
        }
        var dimNN = core.dim(NN);
        var dimI = dimNN[0];
        var dimJ = dimNN[1];
        // Clear inputs and outputs.
        for (var i = 0; i < dimI; i++) {
            NN[0][i] = 0.0;
            NN[i][0] = 0.0;
            if (!allowLoops) {
                NN[i][i] = 0.0;
            }
            NN[i][dimJ - 1] = 0.0;
            NN[dimI - 1][i] = 0.0;
        }
        // Clear the lower triangular matrix.
        for (var i = 1; i < dimI; i++) {
            for (var j = 1; j < dimJ; j++) {
                if (i > j) {
                    NN[i][j] = 0.0;
                }
            }
        }
        // Set random weights.
        if (randomize) {
            for (var i = 1; i < (dimI - 1); i++) {
                for (var j = 1; j < (dimJ - 1); j++) {
                    if (NN[i][j] == 1) {
                        if (negativeWeights) {
                            NN[i][j] = 2.0 * math.random() - 1.0;
                        } else {
                            NN[i][j] = math.random();
                        }
                    }
                }
            }
        }
        return NN;
    }

    /**
     * Remove the last row and last column from the matrix.
     * @param {object}   ANNMatrix - Adjacency matrix.
     * @return {object}  The matrix without the last row and last column.
     */
     this.reduce = function(ANNMatrix) {
        var dimANN = core.dim(ANNMatrix);
        var dimI = dimANN[0];
        var dimJ = dimANN[1];
        var NN = core.matrix(0.0, dimI - 1, dimJ - 1);
        for (var i = 1; i < (dimI - 1); i++) {
            for (var j = 1; j < (dimJ - 1); j++) {
                NN[i][j] = ANNMatrix[i][j];
            }
        }
        return(NN);
    }

    /**
     * Sets the labels of an adjacency matrix.
     * @param {object}   NN - Adjacency matrix.
     * @param {object}   labels - Matrix labels.
     * @return {object}  The adjacency matrix.
     */
    this.setLabels = function(NN, labels) {
        var dimNN = core.dim(NN);
        var dimI = dimNN[0];
        for (var i = 1; i < dimI; i++) {
            NN[i][0] = labels[i];
            NN[0][i] = labels[i];
        }
        return(labels);
    }

    /**
     * It processes incoming data using a trained neural network.
     * @param {object}   NN - adjacency matrix.
     * @param {object}   inMatrix - Input data for training.
     * @param {number}   ni - Number of input neurons.
     * @param {number}   no - Number of output neurons.
     * @param {string}   AF - Activation function. It can be:
     *                        linear, logistic or tanh.
     * @param {string}   OAF - Activation function of the last layer. It can be:
     *                         linear, logistic or tanh.
     * @param {string}   OF - Output function. It can be:
     *                        linear, step, or none.
     * @param {object}   OFC - Output function coefficients.
     * @return {object}  Trained neural network.
     */
    this.think = function(NN, inMatrix, ni, no, AF, OAF, OF, OFC) {
        if (typeof ni == 'undefined') {
            var ni = 0;
        }
        if (typeof no == 'undefined') {
            var no = 0;
        }
        if (typeof AF == 'undefined') {
            var AF = 'logistic';
        }
        if (typeof OAF == 'undefined') {
            var OAF = 'linear';
        }
        if (typeof OF == 'undefined') {
            var OF = 'none';
        }
        if (typeof OFC == 'undefined') {
            var OFC = [1, 0];
        }
        var output = core.matrix(0.0, 1, no);
        var dimNN = core.dim(NN);
        var dimI = dimNN[0];
        var dimJ = dimNN[1];
        var firstOut = dimJ - 1 - no;
        // Clear inputs and outputs.
        for (var i = 0; i < dimI - 1; i++) {
            NN[0][i] = 0.0;
            NN[i][0] = 0.0;
            NN[i][dimJ - 1] = 0.0;
            NN[dimI - 1][i] = 0.0;
        }
        // Assign inputs.
        for (var j = 0; j < ni; j++) {
            NN[j + 1][0] = inMatrix[j];
        }
        // Calculate the neurons output.
        for (var j = ni + 1; j < (dimJ - 1); j++) {
            NN[0][j] = 0.0;
            // Weighted sums.
            // x = x1 * w1 + x2 * w2 + ...
            for (var i = 1; i < (dimI - 1); i++) {
                if (i < j) {
                    if (NN[i][j] != 0) {
                        NN[0][j] = NN[0][j] + NN[i][j] * NN[i][0];
                    }
                } else if (i == j) {
                    if (NN[i][j] != 0) {
                        NN[0][j] = NN[0][j] + NN[i][j];
                    }
                } else {
                    break;
                }
            }
            // Activation function.
            if (j < firstOut) {
                // Linear: f(x) = x
                if (AF == 'linear') {
                    // Calculate y = f(x)
                    NN[j][0] = NN[0][j];
                // Logistic: f(x) = 1.0 / (1.0 + e^(-x))
                } else if (AF == 'logistic') {
                    // Calculate y = f(x)
                    NN[j][0] = 1.0 / (1.0 + math.exp(-1.0 * NN[0][j]));
                // Hyperbolic tangent: f(x) = 2 / (1 + e^(-2x)) - 1
                } else if (AF == 'tanh') {
                    // Calculate y = f(x)
                    NN[j][0] = 2.0 / (1.0 + math.exp(-2.0 * NN[0][j])) - 1.0;
                // Logistic: f(x) = 1.0 / (1.0 + e^(-x))
                } else {
                    // Calculate y = f(x)
                    NN[j][0] = 1.0 / (1.0 + math.exp(-1.0 * NN[0][j]));
                }
            } else {
                // Linear: f(x) = x
                if (OAF == 'linear') {
                    // Calculate y = f(x)
                    NN[j][0] = NN[0][j];
                // Logistic: f(x) = 1.0 / (1.0 + e^(-x))
                } else if (OAF == 'logistic') {
                    // Calculate y = f(x)
                    NN[j][0] = 1.0 / (1.0 + math.exp(-1.0 * NN[0][j]));
                // Hyperbolic tangent: f(x) = 2 / (1 + e^(-2x)) - 1
                } else if (OAF == 'tanh') {
                    // Calculate y = f(x)
                    NN[j][0] = 2.0 / (1.0 + math.exp(-2.0 * NN[0][j])) - 1.0;
                // Logistic: f(x) = 1.0 / (1.0 + e^(-x))
                } else {
                    // Calculate y = f(x)
                    NN[j][0] = 1.0 / (1.0 + math.exp(-1.0 * NN[0][j]));
                }
            }
        }
        // Set the output matrix.
        for (var i = 0; i < no; i++) {
            if (OF == 'linear') {
                output[i] = OFC[0] * NN[firstOut + i][0] + OFC[1];
            } else if (OF == 'step') {
                if (OAF == 'linear') {
                    if (NN[firstOut + i][0] >= 0.0) {
                        output[i] = 1;
                    } else {
                        output[i] = 0;
                    }
                } else if (OAF == 'logistic') {
                    if (NN[firstOut + i][0] >= 0.5) {
                        output[i] = 1;
                    } else {
                        output[i] = 0;
                    }
                } else if (OAF == 'tanh') {
                    if (NN[firstOut + i][0] >= 0.0) {
                        output[i] = 1;
                    } else {
                        output[i] = 0;
                    }
                } else {
                    if (NN[firstOut + i][0] >= 0.0) {
                        output[i] = 1;
                    } else {
                        output[i] = 0;
                    }
                }
            } else if (OF == 'none') {
                output[i] = NN[firstOut + i][0];
            } else {
                output[i] = NN[firstOut + i][0];
            }
        }
        return output;
    }

    /**
     * Train an artificial neural network, represented as an adjacency matrix.
     * @param {object}    NN - Adjacency matrix.
     * @param {object}    inMatrix - Input data for training.
     * @param {object}    outMatrix - Output data for training.
     * @param {number}    lRate - Learning rate.
     * @param {string}    AF - Activation function. It can be:
     *                         linear, logistic or tanh.
     * @param {string}    OAF - Activation function of the last layer. It can be:
     *                          linear, logistic or tanh.
     * @param {string}    OF - Output function. It can be:
     *                         linear, step or none.
     * @param {string}    OFC - Output function coefficients.
     * @param {number}    maxEpochs - Maximum number of epochs.
     * @param {number}    minimumCorrectness - Minimum correctness.
     * @param {function}  callback - Callback function.
     * @param {number}    interval - Interval between calls from the callback function.
     * @return {object}   Trained neural network.
     */
    this.training = function(NN, inMatrix, outMatrix, lRate, AF, OAF, OF, OFC, maxEpochs, minimumCorrectness, callback, interval) {
        if (typeof lRate == 'undefined') {
            var lRate = 1;
        }
        if (typeof AF == 'undefined') {
            var AF = 'logistic';
        }
        if (typeof OAF == 'undefined') {
            var OAF = 'linear';
        }
        if (typeof OF == 'undefined') {
            var OF = 'none';
        }
        if (typeof OFC == 'undefined') {
            var OFC = [1, 0];
        }
        if (typeof maxEpochs == 'undefined') {
            var maxEpochs = 1;
        }
        if (typeof minimumCorrectness == 'undefined') {
            var minimumCorrectness = 1;
        }
        if (typeof correctnessMatrix == 'undefined') {
            var correctnessMatrix = [];
        }
        if (typeof interval == 'undefined') {
            var interval = 0;
        }
        var dimIn = core.dim(inMatrix);
        var dimOut = core.dim(outMatrix);
        var input = core.matrix(0.0, 1, dimIn[1]);
        var output = core.matrix(0.0, 1, dimOut[1]);
        var NNOut = core.matrix(0.0, 1, dimOut[1]);
        var epochs = 0;
        var epochsCounter = 0;
        var date = core.date();
        var ETL1 = date.getTime();
        var ETL2 = date.getTime();
        var squaredError = core.matrix(0.0, 1, dimIn[0]);
        var ERR = [];
        var SE = 0;
        var RSS = 0;
        var correctness = 0;
        var correctnessMatrix = core.matrix(0.0, maxEpochs + 1, 2);
        while (epochs < maxEpochs) {
            var hits = 0;
            epochs++;
            // Verify learning.
            for (var i = 0; i < dimIn[0]; i++) {
                // Assign inputs and outputs.
                for (var j = 0; j < dimIn[1]; j++) {
                    input[j] = inMatrix[i][j];
                }
                for (var j = 0; j < dimOut[1]; j++) {
                    output[j] = outMatrix[i][j];
                }
                // Verify learning.
                if (OFC != []) {
                    NNOut = this.think(NN, input, dimIn[1], dimOut[1], AF, OAF, OF, OFC);
                } else {
                    NNOut = this.think(NN, input, dimIn[1], dimOut[1], AF, OAF, OF);
                }
                if (output == NNOut) {
                    hits++;
                }
                ERR = core.sub(output, NNOut);
                if (typeof ERR == 'number') {
                    ERR = [ERR];
                }
                SE = matrix.sum2(ERR) / 2.0;
                squaredError[i] = SE;
                RSS = matrix.sum(squaredError);
                correctness = hits / dimIn[0];
                correctnessMatrix[epochs][0] = RSS;
                correctnessMatrix[epochs][1] = correctness;
                if (hits == dimIn[0]) {
                    result = [epochs, RSS, correctnessMatrix];
                    return result;
                }
                if (correctness >= minimumCorrectness) {
                    result = [epochs, RSS, correctnessMatrix];
                    return result;
                }
            }
            // Learn this set.
            for (var i = 0; i < dimIn[0]; i++) {
                // Assign inputs and outputs.
                input = inMatrix[i];
                output = outMatrix[i];
                // Learn this set.
                this.learn(NN, input, output, dimIn[1], dimOut[1], lRate, AF, OAF);
            }
            epochsCounter++;
            if (interval != 0) {
                if (typeof callback != 'undefined') {
                    if (epochsCounter >= interval) {
                        ETL2 = date.getTime();
                        var ETL = ETL2 - ETL1;
                        if (typeof callback != 'undefined') {
                            callback(epochs, RSS, correctness, ETL);
                        }
                        epochsCounter = 0;
                        ETL1 = date.getTime();
                    }
                }
            }
        }
        result = [epochs, RSS, correctnessMatrix];
        return result;
    }
}

ann = new ANN();