All Downloads are FREE. Search and download functionalities are using the official Maven repository.

nak.util.Evaluation.scala Maven / Gradle / Ivy

The newest version!
package nak.util

import nak.data.Example
import nak.core.FeaturizedClassifier
import nak.core.IndexedClassifier

case class Scores(accuracy: Double, precisionAverage: Double, recallAverage: Double, fscoreAverage: Double, all: Seq[Seq[Double]] )

/**
 * A confusion matrix for comparing gold clusters to some predicted clusters.
 *
 * @param goldLabels	the set of labels
 * @param counts the matrix, where each cell is the number of data points that had a given gold label and predicted label
 */
class ConfusionMatrix[L, I](
  labels: Seq[L],
  counts: Seq[Seq[Int]],
  examples: Seq[Seq[Seq[I]]]) {

  lazy val numLabels = labels.length

  lazy val lineSeparator = "-" * 80 + "\n"

  def safeDivide(num: Double, denom: Double) =
    if (denom == 0.0) 0.0 else num / denom

  def formatPercent(percent: Double) = "%1.2f".format(100 * percent)

  lazy val scores = {
    // Calculate accuracy, precision, recall, and F-score
    val goodCounts = counts.indices.map(index => counts(index)(index))
    val totalCounts = counts.map(_.sum).sum.toDouble
    val accuracy = goodCounts.sum / totalCounts

    val recallDenominators = counts.map(_.sum.toDouble)
    val recallValues = counts.indices.map { index =>
      safeDivide(goodCounts(index), recallDenominators(index))
    }

    val precisionDenominators = counts.transpose.map(_.sum.toDouble)
    val precisionValues = counts.indices.map { index =>
      safeDivide(goodCounts(index), precisionDenominators(index))
    }

    val fscores = precisionValues.zip(recallValues).map {
      case (p, r) => safeDivide(2 * p * r, p + r)
    }

    val prfs = Seq(precisionValues, recallValues, fscores).transpose

    val precisionAverage = precisionValues.sum / numLabels
    val recallAverage = recallValues.sum / numLabels
    val fscoreAverage = fscores.sum / numLabels
    Scores(accuracy, precisionAverage, recallAverage, fscoreAverage, prfs)
  }

  lazy val measurements = {
    (lineSeparator
      + "\t" * 2 + formatPercent(scores.accuracy) + "\tOverall accuracy\n"
      + lineSeparator + "P\tR\tF\n"
      + scores.all.zip(labels).map {
        case (prf, label) => prf.map(formatPercent).mkString("\t") + "\t" + label
      }.mkString("\n")
      + "\n" + "." * 35 + "\n"
      + formatPercent(scores.precisionAverage) + "\t"
      + formatPercent(scores.recallAverage) + "\t"
      + formatPercent(scores.fscoreAverage) + "\tAverage")
  }

  lazy val detailedOutput = {
    val sep = "-" * 80 + "\n"
    val correct =
      for (i <- 0 until numLabels) yield "Correctly labeled: " + labels(i) + "\n" + sep + examples(i)(i).mkString("\n\n")

    val incorrect =
      for (
        i <- 0 until numLabels;
        j <- 0 until numLabels;
        if i != j
      ) yield {
        ("Incorrectly labeled: " + labels(i) + " mistaken as " + labels(j) + "\n" + sep
          + examples(i)(j).mkString("\n\n"))
      }
    (sep + "CORRECTLY LABELED EXAMPLES\n" + sep + correct.mkString("\n\n\n")
      + "\n\n\n" + sep + "INCORRECTLY LABELED EXAMPLES\n" + sep + incorrect.mkString("\n\n\n"))
  }

  // Create a string representation. Be lazy so that we only do it once.
  lazy val stringRep = {
    val lengthOfRow = if (counts.length > 0) counts(0).mkString.length + counts(0).length * 7
    else 0

    val tableString =
      counts.zip(labels)
        .map {
          case (goldLine, goldLabel) =>
            (goldLine.mkString("\t") + "\t|\t" + goldLine.sum + "\t" + goldLabel)
        }
        .mkString("\n")

    ("-" * 80 + "\n" + "Confusion matrix.\n" +
      "Columns give predicted counts. Rows give gold counts.\n" +
      "-" * 80 + "\n" +
      tableString + "\n" +
      "-" * lengthOfRow + "\n" +
      counts.transpose.map(_.sum).mkString("\t") + "\n" +
      labels.mkString("\t") + "\n"
      + "\n" + measurements)
  }

  // Get the string representation.
  override def toString = stringRep
}

/**
 * A companion object for constructing ConfusionMatrices.
 */
object ConfusionMatrix {

  import scala.collection.mutable.ListBuffer

  def apply[L,I](goldLabels: Seq[L], predictedLabels: Seq[L], items: Seq[I])(implicit ord: Ordering[L]) = {

    val labels = (goldLabels.toSet ++ predictedLabels.toSet).toIndexedSeq.sorted
    val labelIndices = labels.zipWithIndex.toMap
    val numLabels = labels.length
    val counts = Array.fill(numLabels, numLabels)(0)
    val examples = Array.fill(numLabels, numLabels)(new ListBuffer[I])

    goldLabels.zip(predictedLabels).zip(items).foreach {
      case ((goldLabel, predLabel), item) =>
        counts(labelIndices(goldLabel))(labelIndices(predLabel)) += 1
        examples(labelIndices(goldLabel))(labelIndices(predLabel)) += item
    }

    new ConfusionMatrix(
      labels,
      counts.map(_.toIndexedSeq).toIndexedSeq,
      examples.map(_.toIndexedSeq).toIndexedSeq)

  }

}

object CrossValidation {

  /**
   * Runs a N-Fold cross-validation and returns a ConfusionMatrix with all the results (i.e. you get an average result over all folds). XS, the set of examples is split in `nbrFold` subsets. We then run `nbrFold` evaluation, where we test on the i-th subset and train on all the other one. For instance if we have `xs = Seq(1,2,3,4)` with `nbrFold=4`, we will run 4 evaluations:
   * - test= Seq(1), train = Seq(2,3,4)
   * - test= Seq(2), train = Seq(1,3,4)
   * - test= Seq(3), train = Seq(1,2,4)
   * - test= Seq(4), train = Seq(1,2,3)
   */
  def crossValidation[L,I](xs: Traversable[Example[L, I]], nbrFold: Int)(f: Traversable[Example[L, I]] => IndexedClassifier[L] with FeaturizedClassifier[L, I])(implicit ord: Ordering[L]): ConfusionMatrix[L,I] = {
    val size = (xs.size / nbrFold).ceil.toInt
    val tests = for {
      fold <- 0 until nbrFold
    } yield {
      val test = xs.slice(fold * size, (fold + 1) * size)
      val train = xs.slice(0, fold * size) ++ xs.slice((fold + 1) * size, xs.size)

      val classifier = f(train)

      (for {
        t <- test
      } yield (t.label, classifier.predict(t.features), t.features))
    }
    val testZ = tests.flatten.unzip3
    ConfusionMatrix(testZ._1, testZ._2, testZ._3)
  }

  /**
   * Runs a leave one out evaluation. This is equivalent to a n-fold cross-validation where the number of fold is equal to the number of examples. That is, we take one example out, we train on all other examples and test on the example that we reserved, this is repeated for each example.
   * While this is useful for cases where there aren't many examples, it might be quite slow for large datasets and a n-fold with a smaller number of splits might yield a good evaluation anyway.
   */
  def leaveOneOut[L,I](xs: Traversable[Example[L, I]])(f: Traversable[Example[L, I]] => IndexedClassifier[L] with FeaturizedClassifier[L, I])(implicit ord: Ordering[L]): ConfusionMatrix[L,I] = {
    crossValidation(xs, xs.size)(f)
  }

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy