//:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: /** @author John Miller * @version 1.6 * @date Sat Dec 21 12:53:44 EST 2019 * @see LICENSE (MIT style license file). * * @title Model: Neural Network Classifier with 4+ Layers (input, {hidden} and output layers) * @see hebb.mit.edu/courses/9.641/2002/lectures/lecture03.pdf */ package scalation.analytics package classifier import scalation.linalgebra.{FunctionV_2V, MatriD, MatrixD, MatriI, VectoD, VectoI, VectorI} import scalation.random.PermutedVecI import scalation.stat.Statistic import scalation.util.banner import ActivationFun._ import ConfusionFit._ import PredictorMat2.rescaleX //:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: /** The `NeuralNet_Classif_XL` class supports multi-output, multi-layer (input, {hidden} and output) * Neural-Network classifiers. Given several input vectors and output vectors (training data), * fit the parameters connecting the layers, so that for a new input vector 'v', the net * can classify the output value. * Note: 'f.last' is set to 'f_sigmoid' * @param x the m-by-nx input matrix (training data consisting of m input vectors) * @param y the m output vector (training data consisting of m output integer values) * @param nz the number of nodes in each hidden layer, e.g., Array (9, 8) => 2 hidden of sizes 9 and 8 * @param fname_ the feature/variable names (if null, use x_j's) * @param hparam the hyper-parameters for the model/network * @param f the array of activation function families between every pair of layers */ class NeuralNet_Classif_XL (x: MatriD, y: VectoI, private var nz: Array [Int] = null, fname_ : Strings = null, hparam: HyperParameter = NeuralNet_Classif_XL.hp, f: Array [AFF] = Array (f_tanh, f_tanh, f_sigmoid)) extends NeuralNet_XL (x, MatrixD (Seq (y.toDouble)), nz, fname_, hparam, f) // with Classifier // FIX { private val DEBUG = true // debug flag private val cThresh = hparam ("cThresh") // classification/decision threshold private var stream = 0 // random number stream to use private val conf = new ConfusionFit (y) // Quality of Fit (QoF) / Confusion Matrix //:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: /** Given a new input vector 'v', classify the output/response value 'f(v)'. * @param v the new input vector */ def classify (v: VectoD): Int = (predict (v) + cThresh).toInt //:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: /** Given an input matrix 'v', predict the output/response vector 'f(v)'. * @param v the input matrix */ def classify (v: MatriD = x): VectoI = (predictV (v).col(0) + cThresh).toInt //:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: /** Compare the actual class 'y' vector versus the predicted class 'yp' vector, * returning the confusion matrix 'cmat', which for 'k = 2' is *

* yp 0 1 * ---------- * y 0 | tn fp | * 1 | fn tp | * ---------- *

* Note: ScalaTion's confusion matrix is Actual × Predicted, but to swap the position of * actual 'y' (rows) with predicted 'yp' (columns) simply use 'cmat.t', the transpose of 'cmat'. * @see www.dataschool.io/simple-guide-to-confusion-matrix-terminology * @param yp the precicted class values/labels * @param yy the actual class values/labels for full (y) or test (y_e) dataset */ def confusion (yp: VectoI, yy: VectoI = y): MatriI = conf.confusion (yp, yy) //:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: /** Contract the actual class 'y' vector versus the predicted class 'yp' vector. * @param yp the precicted class values/labels * @param yy the actual class values/labels for full (y) or test (y_e) dataset */ def contrast (yp: VectoI, yy: VectoI = y) { conf.contrast (yp, yy) } //:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: /** Produce a summary report with diagnostics and the overall quality of fit. * @param b the parameters of the model * @param show flag indicating whether to print the summary */ def summary (b: VectoD = null, show: Boolean = false): String = conf.summary (b, show) //:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: /** Test the accuracy of the classified results by cross-validation, returning * the Quality of Fit (QoF) measures such as accuracy. * This method randomizes the instances/rows selected for the test dataset. * @param nx number of folds/crosses and cross-validations (defaults to 10x). * @param show the show flag (show result from each iteration) */ def crossValidateRand (nx: Int = 10, show: Boolean = false): Array [Statistic] = { if (nx < MIN_FOLDS) flaw ("crossValidateRand", s"nx = $nx must be at least $MIN_FOLDS") val stats = qofStatTable // create table for QoF measures val permGen = PermutedVecI (VectorI.range (0, x.dim1), stream) // random permutation generator val indices = permGen.igen.split (nx) conf.clearConfusion () for (idx <- indices) { val (x_e, x_r) = x.splitRows (idx) // test, training data/input matrices val (y_e, y_r) = y.split (idx) // test, training response/output matrices train (x_r, MatrixD (Seq (y_r.toDouble))) // train the model eval (x_e, MatrixD (Seq (y_e.toDouble))) // evaluate model on test dataset for (j <- 0 until ny) { val fit_j = fitA(j) val qof = fit_j.fit // get quality of fit measures val qsz = qof.size if (qof(index_sst) > 0.0) { // requires variation in test set for (q <- qof.range) stats(j*qsz + q).tally (qof(q)) // tally these measures } // if } // for } // for if (show) { banner ("crossValidateRand: Statistical Table for QoF") println (Statistic.labels) for (i <- stats.indices) println (stats(i)) val tcmat = conf.total_cmat () println (s"total cmat = $tcmat") } // if stats } // crossValidateRand } // NeuralNet_Classif_XL //:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: /** The `NeuralNet_Classif_XL` companion object provides factory functions for buidling three-layer * (one hidden layer) neural network classifiers. Note, 'rescale' is defined in `ModelFactory` * in Model.scala. */ object NeuralNet_Classif_XL extends ModelFactory { private val DEBUG = true // debug flag val hp = Classifier.hp ++ Optimizer.hp // combine hyper-parameters //:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: /** Create a `NeuralNet_XL` for a data matrix and response vector. * @param x the input/data matrix * @param y the output/response vector * @param nz the number of nodes in each hidden layer, e.g., Array (9, 8) => 2 hidden of sizes 9 and 8 * @param fname_ the feature/variable names (if null, use x_j's) * @param hparam the hyper-parameters for the model/network * @param f the array of activation function families between every pair of layers */ def apply (x: MatriD, y: VectoI, nz: Array [Int] = null, fname: Strings = null, hparam: HyperParameter = hp, f: Array [AFF] = Array (f_tanh, f_tanh, f_sigmoid)): NeuralNet_Classif_XL = { val x_s = if (rescale) rescaleX (x, f(0)) else x if (DEBUG) println (s" scaled: x = $x_s \n y = $y") new NeuralNet_Classif_XL (x_s, y, nz, fname, hparam, f) } // apply } // NeuralNet_Classif_XL object //::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: /** The `NeuralNet_Classif_XLTest` object is used to test the `NeuralNet_Classif_XL` class. * It tests the Neural Network three layer classifier on Diabetes dataset. * > runMain scalation.analytics.classifier.NeuralNet_Classif_XLTest */ object NeuralNet_Classif_XLTest extends App { val fname = BASE_DIR + "diabetes.csv" val xy = MatrixD (fname) val (x, y) = ClassifierReal.pullResponse (xy) val fn = Array ("pregnancies", "glucose", "blood pressure", "skin thickness", "insulin", "BMI", "diabetes pedigree function", "age") // feature names val cn = Array ("tested_positive", "tested_negative") // class names banner ("NeuralNet_Classif_XLTest: diabetes dataset") val hp2 = NeuralNet_Classif_XL.hp.updateReturn (("cThresh", 0.48), ("eta", 0.2)) val nn = NeuralNet_Classif_XL (x, y, null, fn, hp2) nn.train () val yp = nn.classify () nn.confusion (yp) banner ("NeuralNet_Classif_XLTest Results") // nn.contrast (yp) // println (nn.report) println (nn.summary ()) nn.crossValidateRand (5, true) banner ("NullModel: diabetes dataset") val nm = new NullModel (y) nm.train () val yp0 = nm.classify () nm.confusion (yp0) banner ("NullModel Results") // nm.contrast (yp0) // println (nm.report) println (nm.summary ()) } // NeuralNet_Classif_XLTest object