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

scalismo.statisticalmodel.DiscreteGaussianProcess.scala Maven / Gradle / Ivy

There is a newer version: 1.0-RC1
Show newest version
/*
 * Copyright 2015 University of Basel, Graphics and Vision Research Group
 *
 * 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, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package scalismo.statisticalmodel

import breeze.linalg.{DenseMatrix, DenseVector}
import scalismo.common._
import scalismo.common.interpolation.{FieldInterpolator, NearestNeighborInterpolator}
import scalismo.geometry._
import scalismo.kernels.{DiscreteMatrixValuedPDKernel, MatrixValuedPDKernel}
import scalismo.numerics.PivotedCholesky
import scalismo.numerics.PivotedCholesky.RelativeTolerance
import scalismo.utils.Random

import scala.language.higherKinds

/**
 * A representation of a gaussian process, which is only defined on a discrete domain.
 * While this is technically similar to a MultivariateNormalDistribution, we highlight with this
 * class that we represent (discrete) functions, defined on the given domain.
 */
class DiscreteGaussianProcess[D: NDSpace, DDomain[D] <: DiscreteDomain[D], Value] private[scalismo] (
  val mean: DiscreteField[D, DDomain, Value],
  val cov: DiscreteMatrixValuedPDKernel[D]
)(implicit val vectorizer: Vectorizer[Value]) {
  self =>

  require(mean.domain == cov.domain)

  val domain = mean.domain
  private val pointSet = mean.domain.pointSet
  val outputDim = vectorizer.dim

  def sample()(implicit rand: Random): DiscreteField[D, DDomain, Value] = {
    // define the mean and kernel matrix for the given points and construct the
    // corresponding MV Normal distribution, from which we then sample

    val mu = DiscreteField.vectorize[D, Value](mean.data)
    val K = cov.asBreezeMatrix

    val mvNormal = MultivariateNormalDistribution(mu, K)

    val sampleVec = mvNormal.sample()

    // The sample is a vector. We convert it back to a discreteVectorField.
    DiscreteField.createFromDenseVector[D, DDomain, Value](domain, sampleVec)
  }

  /**
   * The marginal distribution at a given (single) point, specified by the pointId.
   */
  def marginal(pointId: PointId) = {
    MultivariateNormalDistribution(vectorizer.vectorize(mean(pointId)), cov(pointId, pointId))
  }

  /**
   * The marginal distribution for the points specified by the given point ids.
   * Note that this is again a DiscreteGaussianProcess.
   */
  def marginal(pointIds: Seq[PointId])(
    implicit
    domainCreator: UnstructuredPoints.Create[D]
  ): DiscreteGaussianProcess[D, UnstructuredPointsDomain, Value] = {
    val domainPts = pointSet.points.toIndexedSeq

    val newPts = pointIds.map(pointId => domainPts(pointId.id)).toIndexedSeq
    val newDomain = UnstructuredPointsDomain(domainCreator.create(newPts))

    val newMean =
      DiscreteField[D, UnstructuredPointsDomain, Value](newDomain, pointIds.toIndexedSeq.map(id => mean(id)))
    val newCov = (i: PointId, j: PointId) => {
      cov(pointIds(i.id), pointIds(j.id))
    }
    val newDiscreteCov = DiscreteMatrixValuedPDKernel(newDomain, newCov, outputDim)

    new DiscreteGaussianProcess(newMean, newDiscreteCov)
  }

  /**
   * calculates the log marginal likelihood given trainingData.
   *
   * @param trainingData Point/value pairs where that the sample should approximate, together with an error model (the uncertainty) at each point.
   */
  def marginalLikelihood(trainingData: IndexedSeq[(PointId, Value, MultivariateNormalDistribution)]): Double = {
    require(trainingData.nonEmpty, "provide observations to calculate the marginal likelihood")
    GaussianProcess.marginalLikelihoodCalculation[PointId, Value](cov.k, mean, trainingData, outputDim)
  }

  /**
   * Interpolates discrete Gaussian process to have a new, continuous representation as a [[DiscreteLowRankGaussianProcess]],
   * using nearest neighbor interpolation (for both mean and covariance function)
   */
  @deprecated("please use the [[interpolate]] method with a [[NearestNeighborInterpolator]] instead", "0.16")
  def interpolateNearestNeighbor: GaussianProcess[D, Value] = {

    val meanDiscreteGp = this.mean

    val newDomain = RealSpace[D]
    def meanFun(pt: Point[D]): Value = {
      val closestPtId = pointSet.findClosestPoint(pt).id
      meanDiscreteGp(closestPtId)
    }

    val newCov = new MatrixValuedPDKernel[D] {
      override val domain = newDomain

      override def k(pt1: Point[D], pt2: Point[D]): DenseMatrix[Double] = {
        val closestPtId1 = self.pointSet.findClosestPoint(pt1).id
        val closestPtId2 = self.pointSet.findClosestPoint(pt2).id
        cov(closestPtId1, closestPtId2)
      }

      override def outputDim = self.outputDim
    }

    GaussianProcess(Field[D, Value](newDomain, meanFun), newCov)

  }

  def interpolate(interpolator: FieldInterpolator[D, DDomain, Value]): GaussianProcess[D, Value] = {

    // We know how to interpolate DiscreteLowRankGaussianProcesses, but
    // not this more generic type of DiscreteGP. Since we are sure that our
    // cov matrix is always relatively small (it has to fit in memory)
    // we can do the trick and do a pivoted cholesky, create a
    // DiscreteLowRankGP and interpolate that.

    val (basis, scale) = PivotedCholesky.computeApproximateEig(cov.asBreezeMatrix, RelativeTolerance(0.0))

    val nBasisFunctions = basis.cols

    val klBasis: DiscreteLowRankGaussianProcess.KLBasis[D, DDomain, Value] = for (i <- 0 until nBasisFunctions) yield {
      val discreteEV = DiscreteField.createFromDenseVector[D, DDomain, Value](domain, basis(::, i))
      DiscreteLowRankGaussianProcess.Eigenpair(scale(i), discreteEV)
    }

    val dgp = DiscreteLowRankGaussianProcess[D, DDomain, Value](mean, klBasis)
    dgp.interpolate(interpolator)
  }

  /**
   * Discrete version of [[LowRankGaussianProcess.project(IndexedSeq[(Point[D], Vector[DO])], Double)]]
   */
  def project(s: DiscreteField[D, DDomain, Value]): DiscreteField[D, DDomain, Value] = {

    val sigma2 = 1e-5 // regularization weight to avoid numerical problems
    val noiseDist =
      MultivariateNormalDistribution(DenseVector.zeros[Double](outputDim), DenseMatrix.eye[Double](outputDim) * sigma2)
    val td = s.values.zipWithIndex.map { case (v, id) => (id, v, noiseDist) }.toIndexedSeq
    DiscreteGaussianProcess.regression(this, td).mean

  }

  /**
   * Returns the probability density of the given instance
   */
  def pdf(instance: DiscreteField[D, DDomain, Value]): Double = {
    val mvnormal = MultivariateNormalDistribution(DiscreteField.vectorize[D, Value](mean.data), cov.asBreezeMatrix)
    val instvec = DiscreteField.vectorize[D, Value](instance.data)
    mvnormal.pdf(instvec)
  }

  /**
   * Returns the log of the probability density of the given instance
   *
   * If you are interested in ordinal comparisons of PDFs, use this as it is numerically more stable
   */
  def logpdf(instance: DiscreteField[D, DDomain, Value]): Double = {
    val mvnormal = MultivariateNormalDistribution(DiscreteField.vectorize[D, Value](mean.data), cov.asBreezeMatrix)
    val instvec = DiscreteField.vectorize[D, Value](instance.data)
    mvnormal.logpdf(instvec)
  }

}

object DiscreteGaussianProcess {

  def apply[D: NDSpace, DDomain[D] <: DiscreteDomain[D], Value](
    mean: DiscreteField[D, DDomain, Value],
    cov: DiscreteMatrixValuedPDKernel[D]
  )(implicit vectorizer: Vectorizer[Value]): DiscreteGaussianProcess[D, DDomain, Value] = {
    new DiscreteGaussianProcess[D, DDomain, Value](mean, cov)
  }

  def apply[D: NDSpace, DDomain[D] <: DiscreteDomain[D], Value](domain: DDomain[D], gp: GaussianProcess[D, Value])(
    implicit
    vectorizer: Vectorizer[Value]
  ): DiscreteGaussianProcess[D, DDomain, Value] = {
    val domainPoints = domain.pointSet.points.toIndexedSeq

    val discreteMean = DiscreteField[D, DDomain, Value](domain, domainPoints.map(pt => gp.mean(pt)))

    val k = (i: PointId, j: PointId) => gp.cov(domainPoints(i.id), domainPoints(j.id))
    val discreteCov = DiscreteMatrixValuedPDKernel(domain, k, gp.outputDim)

    new DiscreteGaussianProcess[D, DDomain, Value](discreteMean, discreteCov)
  }

  def regression[D: NDSpace, DDomain[D] <: DiscreteDomain[D], Value](
    discreteGp: DiscreteGaussianProcess[D, DDomain, Value],
    trainingData: IndexedSeq[(Int, Value, MultivariateNormalDistribution)]
  )(implicit vectorizer: Vectorizer[Value]): DiscreteGaussianProcess[D, DDomain, Value] = {

    // TODO, this is somehow a hack to reuse the code written for the general GP regression. We should think if that has disadvantages
    // TODO We should think whether we can do it in  a conceptually more clean way.

    val domainPoints = discreteGp.domain.pointSet.points.toIndexedSeq
    val gp = discreteGp.interpolate(NearestNeighborInterpolator())
    val tdForGp = trainingData.map({ case (id, vec, error) => (domainPoints(id), vec, error) })
    val posterior = GaussianProcess.regression(gp, tdForGp)

    DiscreteGaussianProcess(discreteGp.domain, posterior)
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy