diff --git a/Figaro/META-INF/MANIFEST.MF b/Figaro/META-INF/MANIFEST.MF index c5fab29a..1330151c 100644 --- a/Figaro/META-INF/MANIFEST.MF +++ b/Figaro/META-INF/MANIFEST.MF @@ -3,8 +3,6 @@ Bundle-ManifestVersion: 2 Bundle-Name: Figaro Bundle-SymbolicName: com.cra.figaro Bundle-Version: 2.2.0 -Bundle-ClassPath: ., - lib/jsci-core.jar Export-Package: com.cra.figaro.algorithm, com.cra.figaro.algorithm.decision, com.cra.figaro.algorithm.decision.index, @@ -22,6 +20,7 @@ Bundle-RequiredExecutionEnvironment: JavaSE-1.6 Require-Bundle: org.scala-lang.scala-library, org.scala-lang.scala-actors, org.scala-lang.scala-reflect +Import-Package: org.apache.commons.math3.distribution;version="3.3.0" diff --git a/Figaro/figaro_build.properties b/Figaro/figaro_build.properties index 2033fd5d..93d05ee3 100644 --- a/Figaro/figaro_build.properties +++ b/Figaro/figaro_build.properties @@ -1 +1 @@ -version=2.2.0.0 +version=2.2.1.0 diff --git a/Figaro/src/main/scala/com/cra/figaro/algorithm/Anytime.scala b/Figaro/src/main/scala/com/cra/figaro/algorithm/Anytime.scala index ce8715fd..57676c17 100644 --- a/Figaro/src/main/scala/com/cra/figaro/algorithm/Anytime.scala +++ b/Figaro/src/main/scala/com/cra/figaro/algorithm/Anytime.scala @@ -66,7 +66,7 @@ trait Anytime extends Algorithm { /** * Optional function to run when the algorithm is stopped (not killed). Used in samplers to update lazy values */ - def stopUpdate(): Unit = {} + def stopUpdate(): Unit = { } /** * A class representing the actor running the algorithm. diff --git a/Figaro/src/main/scala/com/cra/figaro/algorithm/AnytimeProbQuery.scala b/Figaro/src/main/scala/com/cra/figaro/algorithm/AnytimeProbQuery.scala index d53f046a..2783fd17 100644 --- a/Figaro/src/main/scala/com/cra/figaro/algorithm/AnytimeProbQuery.scala +++ b/Figaro/src/main/scala/com/cra/figaro/algorithm/AnytimeProbQuery.scala @@ -68,7 +68,8 @@ trait AnytimeProbQuery extends ProbQueryAlgorithm with Anytime { protected def doExpectation[T](target: Element[T], function: T => Double): Double = { runner ! Handle(ComputeExpectation(target, function)) receive { - case Expectation(result) => result + case Expectation(result) => + result } } diff --git a/Figaro/src/main/scala/com/cra/figaro/algorithm/factored/ProbFactor.scala b/Figaro/src/main/scala/com/cra/figaro/algorithm/factored/ProbFactor.scala index 0a855290..471ab929 100644 --- a/Figaro/src/main/scala/com/cra/figaro/algorithm/factored/ProbFactor.scala +++ b/Figaro/src/main/scala/com/cra/figaro/algorithm/factored/ProbFactor.scala @@ -115,9 +115,12 @@ object ProbFactor { } private def parameterizedGetProbs[T](select: ParameterizedSelect[T]): List[Double] = { - val selectVar = Variable(select) - val probs = select.parameter.expectedValue.toList - probs + val outcomes = select.outcomes + val map = select.parameter.MAPValue + for { + xvalue <- Variable(select).range + index = outcomes.indexOf(xvalue.value) + } yield map(index) } private def makeFactors[T](select: AtomicSelect[T]): List[Factor[Double]] = { @@ -567,14 +570,14 @@ object ProbFactor { private def makeFactors(flip: ParameterizedFlip): List[Factor[Double]] = { val flipVar = Variable(flip) val factor = new Factor[Double](List(flipVar)) - val prob = flip.parameter.expectedValue + val prob = flip.parameter.MAPValue val i = flipVar.range.indexOf(Regular(true)) factor.set(List(i), prob) factor.set(List(1 - i), 1.0 - prob) List(factor) } - private def concreteFactors[T](elem: Element[T]): List[Factor[Double]] = + def concreteFactors[T](elem: Element[T]): List[Factor[Double]] = elem match { case f: ParameterizedFlip => makeFactors(f) case s: ParameterizedSelect[_] => makeFactors(s) diff --git a/Figaro/src/main/scala/com/cra/figaro/algorithm/factored/SufficientStatisticsVariableElimination.scala b/Figaro/src/main/scala/com/cra/figaro/algorithm/factored/SufficientStatisticsVariableElimination.scala index 8dcc025b..12043fce 100644 --- a/Figaro/src/main/scala/com/cra/figaro/algorithm/factored/SufficientStatisticsVariableElimination.scala +++ b/Figaro/src/main/scala/com/cra/figaro/algorithm/factored/SufficientStatisticsVariableElimination.scala @@ -50,7 +50,6 @@ class SufficientStatisticsVariableElimination( */ def getFactors(neededElements: List[Element[_]], targetElements: List[Element[_]], upper: Boolean = false): List[Factor[(Double, mutable.Map[Parameter[_], Seq[Double]])]] = { val allElements = neededElements.filter(p => p.isInstanceOf[Parameter[_]] == false) - if (debug) { println("Elements appearing in factors and their ranges:") for { element <- allElements } { diff --git a/Figaro/src/main/scala/com/cra/figaro/algorithm/learning/ExpectationMaximization.scala b/Figaro/src/main/scala/com/cra/figaro/algorithm/learning/ExpectationMaximization.scala index 0a9a63a5..ed2065cd 100644 --- a/Figaro/src/main/scala/com/cra/figaro/algorithm/learning/ExpectationMaximization.scala +++ b/Figaro/src/main/scala/com/cra/figaro/algorithm/learning/ExpectationMaximization.scala @@ -1,6 +1,6 @@ /* * ExpectationMaximization.scala - * Expectation maximization algorithm. + * Expectation maximization algorithm using variable elimination as the inference algorithm. * * Created By: Michael Howard (mhoward@cra.com) * Creation Date: Jun 1, 2013 @@ -50,13 +50,13 @@ class ExpectationMaximization(universe: Universe, targetParameters: Parameter[_] /* * Obtain an estimate of sufficient statistics from expectation step */ + //println("EM iteration " + iteration) val sufficientStatistics = doExpectationStep() doMaximizationStep(sufficientStatistics) } } protected def doExpectationStep(): Map[Parameter[_], Seq[Double]] = { - val algorithm = SufficientStatisticsVariableElimination(paramMap)(universe) algorithm.start val result = algorithm.getSufficientStatisticsForAllParameters @@ -67,7 +67,7 @@ class ExpectationMaximization(universe: Universe, targetParameters: Parameter[_] protected def doMaximizationStep(parameterMapping: Map[Parameter[_], Seq[Double]]): Unit = { - for (p <- targetParameters) { + for (p <- targetParameters) yield { p.maximize(parameterMapping(p)) } } @@ -87,19 +87,39 @@ class ExpectationMaximization(universe: Universe, targetParameters: Parameter[_] protected def doKill(): Unit = {} } +/** + * @deprecated + */ object ExpectationMaximization { /** * An expectation maximization algorithm which will run for the default of 10 iterations + * + * @deprecated */ def apply(p: Parameter[_]*)(implicit universe: Universe) = new ExpectationMaximization(universe, p: _*)(10) /** * An expectation maximization algorithm which will run for the number of iterations specified + * + * @deprecated */ def apply(iterations: Int, p: Parameter[_]*)(implicit universe: Universe) = new ExpectationMaximization(universe, p: _*)(iterations) } +object EMWithVE { + /** + * An expectation maximization algorithm which will run for the default of 10 iterations + */ + def apply(p: Parameter[_]*)(implicit universe: Universe) = + new ExpectationMaximization(universe, p: _*)(10) + + /** + * An expectation maximization algorithm which will run for the number of iterations specified + */ + def apply(iterations: Int, p: Parameter[_]*)(implicit universe: Universe) = + new ExpectationMaximization(universe, p: _*)(iterations) + } \ No newline at end of file diff --git a/Figaro/src/main/scala/com/cra/figaro/algorithm/learning/GeneralizedEM.scala b/Figaro/src/main/scala/com/cra/figaro/algorithm/learning/GeneralizedEM.scala new file mode 100644 index 00000000..d5f87ffa --- /dev/null +++ b/Figaro/src/main/scala/com/cra/figaro/algorithm/learning/GeneralizedEM.scala @@ -0,0 +1,149 @@ +/* + * GeneralizedEM.scala + * Expectation maximization algorithm using any ProbQueryAlgorithm as the inference algorithm. + * + * Created By: Michael Howard (mhoward@cra.com) + * Creation Date: Jun 1, 2013 + * + * Copyright 2013 Avrom J. Pfeffer and Charles River Analytics, Inc. + * See http://www.cra.com or email figaro@cra.com for information. + * + * See http://www.github.com/p2t2/figaro for a copy of the software license. + */ + +package com.cra.figaro.algorithm.learning + +import com.cra.figaro.language._ +import com.cra.figaro.algorithm.{Algorithm, ParameterLearner, ProbQueryAlgorithm, OneTime} +import com.cra.figaro.algorithm.factored.beliefpropagation.BeliefPropagation +import com.cra.figaro.algorithm.sampling.{Importance, MetropolisHastings, ProposalScheme} + +/* + * @param inferenceAlgorithmConstructor + */ + +class GeneralizedEM(inferenceAlgorithmConstructor: Seq[Element[_]] => Universe => ProbQueryAlgorithm with OneTime, universe: Universe, targetParameters: Parameter[_]*)(val numberOfIterations: Int) extends Algorithm with ParameterLearner { + /* + * Start the algorithm. After it returns, the algorithm must be ready to provide answers. + */ + + protected def doStart(): Unit = { + em() + } + + protected def em(): Unit = { + for (iteration <- 1 to numberOfIterations) { + /* + * Obtain an estimate of sufficient statistics from expectation step + */ + + val sufficientStatistics = doExpectationStep() + doMaximizationStep(sufficientStatistics) + } + } + + protected def doExpectationStep(): Map[Parameter[_], Seq[Double]] = { + val inferenceTargets = + universe.activeElements.filter(_.isInstanceOf[Parameterized[_]]).map(_.asInstanceOf[Parameterized[_]]) + val algorithm = inferenceAlgorithmConstructor(inferenceTargets)(universe) + algorithm.start() + + var result: Map[Parameter[_], Seq[Double]] = Map() + + for { parameter <- targetParameters } { + var stats = parameter.zeroSufficientStatistics + for { + target <- inferenceTargets + if target.parameter == parameter + } { + val t: Parameterized[target.Value] = target.asInstanceOf[Parameterized[target.Value]] + val distribution: Stream[(Double, target.Value)] = algorithm.distribution(t) + val newStats = t.distributionToStatistics(distribution) + stats = (stats.zip(newStats)).map(pair => pair._1 + pair._2) + } + result += parameter -> stats + } + algorithm.kill() + result + } + + protected def doMaximizationStep(parameterMapping: Map[Parameter[_], Seq[Double]]): Unit = { + for (p <- targetParameters) yield { + p.maximize(parameterMapping(p)) + } + } + + /* + * Stop the algorithm from computing. The algorithm is still ready to provide answers after it returns. + */ + protected def doStop(): Unit = {} + + /* + * Resume the computation of the algorithm, if it has been stopped. + */ + + protected def doResume(): Unit = {} + + /* + * Kill the algorithm so that it is inactive. It will no longer be able to provide answers. + */ + + protected def doKill(): Unit = {} +} + +object EMWithBP { + private def makeBP(numIterations: Int, targets: Seq[Element[_]])(universe: Universe) = { + BeliefPropagation(numIterations, targets:_*)(universe) + } + + /** + * An expectation maximization algorithm which will run for the default of 10 iterations + */ + def apply(p: Parameter[_]*)(implicit universe: Universe) = + new GeneralizedEM((targets: Seq[Element[_]]) => (universe: Universe) => makeBP(10, targets)(universe), universe, p: _*)(10) + + /** + * An expectation maximization algorithm which will run for the number of iterations specified + */ + def apply(emIterations: Int, bpIterations: Int, p: Parameter[_]*)(implicit universe: Universe) = + new GeneralizedEM((targets: Seq[Element[_]]) => (universe: Universe) => makeBP(bpIterations, targets)(universe), universe, p: _*)(emIterations) +} + +object EMWithImportance { + private def makeImportance(numParticles: Int, targets: Seq[Element[_]])(universe: Universe) = { + Importance(numParticles, targets:_*)(universe) + } + + /** + * An expectation maximization algorithm using importance sampling for inference + * + * @param emIterations number of iterations of the EM algorithm + * @param importanceParticles number of particles of the importance sampling algorithm + */ + def apply(emIterations: Int, importanceParticles: Int, p: Parameter[_]*)(implicit universe: Universe) = + new GeneralizedEM((targets: Seq[Element[_]]) => (universe: Universe) => makeImportance(importanceParticles, targets)(universe), universe, p: _*)(emIterations) +} + +object EMWithMH { + private def makeMH(numParticles: Int, proposalScheme: ProposalScheme, targets: Seq[Element[_]])(universe: Universe) = { + MetropolisHastings(numParticles, proposalScheme, targets:_*)(universe) + } + + /** + * An expectation maximization algorithm using Metropolis Hastings for inference. + * + * @param emIterations number of iterations of the EM algorithm + * @param mhParticles number of particles of the MH algorithm + */ + def apply(emIterations: Int, mhParticles: Int, p: Parameter[_]*)(implicit universe: Universe) = + new GeneralizedEM((targets: Seq[Element[_]]) => (universe: Universe) => makeMH(mhParticles, ProposalScheme.default(universe), targets)(universe), universe, p: _*)(emIterations) + + /** + * An expectation maximization algorithm using Metropolis Hastings for inference. + * + * @param emIterations number of iterations of the EM algorithm + * @param mhParticles number of particles of the MH algorithm + */ + def apply(emIterations: Int, mhParticles: Int, proposalScheme: ProposalScheme, p: Parameter[_]*)(implicit universe: Universe) = + new GeneralizedEM((targets: Seq[Element[_]]) => (universe: Universe) => makeMH(mhParticles, proposalScheme, targets)(universe), universe, p: _*)(emIterations) +} diff --git a/Figaro/src/main/scala/com/cra/figaro/algorithm/learning/SufficientStatisticsFactor.scala b/Figaro/src/main/scala/com/cra/figaro/algorithm/learning/SufficientStatisticsFactor.scala index 357e6820..0251f898 100644 --- a/Figaro/src/main/scala/com/cra/figaro/algorithm/learning/SufficientStatisticsFactor.scala +++ b/Figaro/src/main/scala/com/cra/figaro/algorithm/learning/SufficientStatisticsFactor.scala @@ -19,10 +19,13 @@ import com.cra.figaro.algorithm.factored._ import com.cra.figaro.algorithm.lazyfactored._ import com.cra.figaro.library.decision._ import com.cra.figaro.language._ +import com.cra.figaro.library.atomic.discrete.ParameterizedBinomialFixedNumTrials import com.cra.figaro.util._ import annotation.tailrec import scala.collection._ import scala.collection.mutable.{ Set, Map } +import scala.math.{ floor, pow } +import JSci.maths.ExtraMath.binomial /** * Methods for creating probabilistic factors associated with elements and their sufficient statistics. @@ -69,7 +72,7 @@ class SufficientStatisticsFactor(parameterMap: immutable.Map[Parameter[_], Seq[D private def makeFactors(flip: ParameterizedFlip): List[Factor[(Double, Map[Parameter[_], Seq[Double]])]] = { val flipVar = Variable(flip) val factor = new Factor[(Double, Map[Parameter[_], Seq[Double]])](List(flipVar)) - val prob = flip.parameter.expectedValue + val prob = flip.parameter.MAPValue val i = flipVar.range.indexOf(Regular(true)) val falseMapping = mutable.Map(parameterMap.toSeq: _*) @@ -85,6 +88,25 @@ class SufficientStatisticsFactor(parameterMap: immutable.Map[Parameter[_], Seq[D List(factor) } + private def makeFactors(bin: ParameterizedBinomialFixedNumTrials): List[Factor[(Double, Map[Parameter[_], Seq[Double]])]] = { + val binVar = Variable(bin) + val factor = new Factor[(Double, Map[Parameter[_], Seq[Double]])](List(binVar)) + val prob = bin.parameter.MAPValue.asInstanceOf[Double] + val mappings = binVar.range.map(i => (i, mutable.Map(parameterMap.toSeq: _*))) + for { + (ext, map) <- mappings + if (ext.isRegular) + } { + val i = ext.value + map.remove(bin.parameter) + map.put(bin.parameter, Seq(i, bin.numTrials - i)) + val density = binomial(bin.numTrials, i) * pow(prob, i) * pow(1 - prob, bin.numTrials - i) + val index = binVar.range.indexOf(ext) + factor.set(List(index), (density, map)) + } + List(factor) + } + private def makeSimpleDistribution[T](target: Variable[T], probs: List[Double]): Factor[(Double, Map[Parameter[_], Seq[Double]])] = { val factor = new Factor[(Double, Map[Parameter[_], Seq[Double]])](List(target)) @@ -142,8 +164,8 @@ class SufficientStatisticsFactor(parameterMap: immutable.Map[Parameter[_], Seq[D private def selectVarAndProbs[U, T](select: ParameterizedSelect[T]): (Variable[T], List[Double]) = { val selectVar = Variable(select) val unzippedClauses = select.clauses.unzip - val expectedValue = select.parameter.expectedValue - val probs = for { xvalue <- selectVar.range } yield expectedValue(unzippedClauses._2.indexOf(xvalue.value)) + val MAPValue = select.parameter.MAPValue + val probs = for { xvalue <- selectVar.range } yield MAPValue(unzippedClauses._2.indexOf(xvalue.value)) val result = (selectVar, probs) result } @@ -431,12 +453,14 @@ class SufficientStatisticsFactor(parameterMap: immutable.Map[Parameter[_], Seq[D List(factor) } - /* - * We shouldn't make sufficient statistics factors for parameters; this is taken care of in SufficientStatisticsVariableElimination - private def makeFactors(param: Parameter[_]): List[Factor[(Double, Map[Parameter[_], Seq[Double]])]] = { - makeFactors(Constant(param.expectedValue)) + private def convertProbFactor(probFactor: Factor[Double]): Factor[(Double, Map[Parameter[_], Seq[Double]])] = { + val result = new Factor[(Double, Map[Parameter[_], Seq[Double]])](probFactor.variables) + for { indices <- result.allIndices } { + result.set(indices, (probFactor.get(indices), mutable.Map(parameterMap.toSeq: _*))) + } + result } - */ + private def concreteFactors[T](elem: Element[T]): List[Factor[(Double, Map[Parameter[_], Seq[Double]])]] = elem match { case c: Constant[_] => makeFactors(c) @@ -444,6 +468,7 @@ class SufficientStatisticsFactor(parameterMap: immutable.Map[Parameter[_], Seq[D case f: CompoundFlip => makeFactors(f) case f: ParameterizedFlip => makeFactors(f) case s: ParameterizedSelect[_] => makeFactors(s) + case b: ParameterizedBinomialFixedNumTrials => makeFactors(b) case s: AtomicSelect[_] => makeFactors(s) case s: CompoundSelect[_] => makeFactors(s) case d: AtomicDist[_] => makeFactors(d) @@ -455,7 +480,8 @@ class SufficientStatisticsFactor(parameterMap: immutable.Map[Parameter[_], Seq[D case a: Apply4[_, _, _, _, _] => makeFactors(a) case a: Apply5[_, _, _, _, _, _] => makeFactors(a) case i: Inject[_] => makeFactors(i) - case f: ProbFactorMaker => throw new UnsupportedAlgorithmException(elem) + case f: ProbFactorMaker => + ProbFactor.concreteFactors(f).map(convertProbFactor(_)) /*case p: Parameter[_] => makeFactors(p)*/ case _ => throw new UnsupportedAlgorithmException(elem) } diff --git a/Figaro/src/main/scala/com/cra/figaro/algorithm/sampling/Importance.scala b/Figaro/src/main/scala/com/cra/figaro/algorithm/sampling/Importance.scala index 918a5239..1d7696c2 100644 --- a/Figaro/src/main/scala/com/cra/figaro/algorithm/sampling/Importance.scala +++ b/Figaro/src/main/scala/com/cra/figaro/algorithm/sampling/Importance.scala @@ -26,21 +26,51 @@ abstract class Importance(universe: Universe, targets: Element[_]*) extends WeightedSampler(universe, targets: _*) { import Importance.State +/* + * Likelihood weighting works by propagating observations through Dists and Chains + * to the variables they depend on. If we don't make sure we sample those Dists and + * Chains first, we may end up sampling those other elements without the correct + * observations. To avoid this, we keep track of all these dependencies. + * The dependencies map contains all the elements that could propagate an + * observation to any given element. + * Note: the dependencies map is only concerned with active elements that are present + * at the beginning of sampling (even though we get a new active elements list each sample). + * Temporary elements will always be created after the element that could propagate + * an observation to them, because that propagation has to go through a permanent + * element. + * Therefore, we can generate the dependencies map once before all the samples are generated. + */ + private val dependencies = scala.collection.mutable.Map[Element[_], Set[Element[_]]]() + private def makeDependencies() = { + for { + element <- universe.activeElements + } { + element match { + case d: Dist[_,_] => + for { o <- d.outcomes } { dependencies += o -> (dependencies.getOrElse(o, Set()) + d) } + case c: CachingChain[_,_] => + val outcomes = Values(universe)(c.parent).map(c.get(_)) + for { o <- outcomes } { dependencies += o -> (dependencies.getOrElse(o, Set()) + c) } + case _ => () + } + } + } + makeDependencies() + /* * Produce one weighted sample of the given element. weightedSample takes into account conditions and constraints * on all elements in the Universe, including those that depend on this element. */ @tailrec final def sample(): Sample = { + /* + * We need to recreate the activeElements each sample, because non-temporary elements may have been made active + * in a previous iteration. See the relevant test in ImportanceTest. + */ + val activeElements = universe.activeElements val resultOpt: Option[Sample] = try { val state = State() - // We must make a fresh copy of the active elements since sampling can add active elements to the Universe - //val activeElements = universe.permanentElements - // use active elements instead of permanent elements since permenentElements may not resample elements inside a chain - val activeElements = universe.activeElements - // Hack alert: Using reverse gets us to sample Dists before their choices, which is important for likelihood weighting - // The algorithm is basically correct with and without reverse. This is an optimization. - activeElements.reverse.foreach(e => if (e.active) sampleOne(state, e, None)) + activeElements.foreach(e => if (e.active) sampleOne(state, e, None)) val bindings = targets map (elem => elem -> elem.value) Some((state.weight, Map(bindings: _*))) } catch { @@ -49,7 +79,8 @@ abstract class Importance(universe: Universe, targets: Element[_]*) } resultOpt match { - case Some(x) => x + case Some(x) => + x case None => sample() } @@ -63,7 +94,14 @@ abstract class Importance(universe: Universe, targets: Element[_]*) * This is made private[figaro] to allow easy testing */ private[figaro] def sampleOne[T](state: State, element: Element[T], observation: Option[T]): T = { - if (element.universe != universe || (state.assigned contains element)) element.value + /* + * We have to make sure to sample any elements this element depends on first so we can get the right + * observation for this element. + */ + dependencies.getOrElse(element, Set()).filter(!state.assigned.contains(_)).foreach(sampleOne(state, _, None)) + if (element.universe != universe || (state.assigned contains element)) { + element.value + } else { state.assigned += element sampleFresh(state, element, observation) @@ -85,7 +123,7 @@ abstract class Importance(universe: Universe, targets: Element[_]*) case _ => throw Importance.Reject // incompatible observations } val value: T = - if (fullObservation.isEmpty || !element.isInstanceOf[Atomic[_]]) { + if (fullObservation.isEmpty || !element.isInstanceOf[HasDensity[_]]) { val result = sampleValue(state, element, fullObservation) if (!element.condition(result)) throw Importance.Reject result @@ -94,10 +132,18 @@ abstract class Importance(universe: Universe, targets: Element[_]*) // This partially implements likelihood weighting by clamping the element to its // desired value and multiplying the weight by the density of the value. // This can dramatically reduce the number of rejections. + element.args.foreach(sampleOne(state, _, None)) val obs = fullObservation.get - state.weight += math.log(element.asInstanceOf[Atomic[T]].density(obs)) + + // Subtle issue taken care of by the following line + // A parameterized element may or may not be a chain to an atomic element + // If it's not, we have to make sure to set its value to the observation here + // If it is, we have to make sure to propagate the observation through the chain + sampleValue(state, element, Some(obs)) + state.weight += math.log(element.asInstanceOf[HasDensity[T]].density(obs)) obs } + element.value = value state.weight += element.constraint(value) value } @@ -130,7 +176,8 @@ abstract class Importance(universe: Universe, targets: Element[_]*) d.value case c: Chain[_, _] => val parentValue = sampleOne(state, c.parent, None) - c.value = sampleOne(state, c.get(parentValue), observation) + val next = c.get(parentValue) + c.value = sampleOne(state, next, observation) c.value case f: CompoundFlip => val probValue = sampleOne(state, f.prob, None) @@ -146,20 +193,6 @@ abstract class Importance(universe: Universe, targets: Element[_]*) f.value = result result } - case f: ParameterizedFlip => - val probValue = sampleOne(state, f.parameter, None) - observation match { - case Some(true) => - state.weight += math.log(probValue) - true - case Some(false) => - state.weight += math.log(1 - probValue) - false - case _ => - val result = random.nextDouble() < probValue - f.value = result - result - } case _ => (element.args ::: element.elementsIAmContingentOn.toList) foreach (sampleOne(state, _, None)) element.randomness = element.generateRandomness() diff --git a/Figaro/src/main/scala/com/cra/figaro/algorithm/sampling/MetropolisHastings.scala b/Figaro/src/main/scala/com/cra/figaro/algorithm/sampling/MetropolisHastings.scala index 41ff29e2..3c091ebf 100644 --- a/Figaro/src/main/scala/com/cra/figaro/algorithm/sampling/MetropolisHastings.scala +++ b/Figaro/src/main/scala/com/cra/figaro/algorithm/sampling/MetropolisHastings.scala @@ -68,10 +68,10 @@ abstract class MetropolisHastings(universe: Universe, proposalScheme: ProposalSc */ private def attemptChange[T](state: State, elem: Element[T]): State = { val newValue = elem.generateValue(elem.randomness) + // if an old value is already stored, don't overwrite it + val newOldValues = + if (state.oldValues contains elem) state.oldValues; else state.oldValues + (elem -> elem.value) if (elem.value != newValue) { - // if an old value is already stored, don't overwrite it - val newOldValues = - if (state.oldValues contains elem) state.oldValues; else state.oldValues + (elem -> elem.value) //val newProb = // state.modelProb * elem.score(elem.value, newValue) val newProb = state.modelProb @@ -84,8 +84,12 @@ abstract class MetropolisHastings(universe: Universe, proposalScheme: ProposalSc // even if the value has not changed, because we compare the dissatisfied set with the old dissatisfied set // when deciding whether to accept the proposal. val newDissatisfied = - if (elem.condition(newValue)) state.dissatisfied - elem; else state.dissatisfied + elem - State(state.oldValues, state.oldRandomness, state.proposalProb, state.modelProb, newDissatisfied) + if (elem.condition(newValue)) { + state.dissatisfied - elem + } else { + state.dissatisfied + elem + } + State(newOldValues, state.oldRandomness, state.proposalProb, state.modelProb, newDissatisfied) } } @@ -96,11 +100,12 @@ abstract class MetropolisHastings(universe: Universe, proposalScheme: ProposalSc val oldRandomness = elem.randomness val (randomness, proposalProb, modelProb) = elem.nextRandomness(elem.randomness) val state1 = - if (randomness == oldRandomness) state - else { - val newOldRandomness = - if (state.oldRandomness contains elem) state.oldRandomness - else state.oldRandomness + (elem -> elem.randomness) + if (randomness == oldRandomness) { + state + } else { + val newOldRandomness = { + state.oldRandomness + (elem -> elem.randomness) + } val newProb = state.proposalProb + log(proposalProb) elem.randomness = randomness State(state.oldValues, newOldRandomness, newProb, state.modelProb + log(modelProb), state.dissatisfied) @@ -255,15 +260,15 @@ abstract class MetropolisHastings(universe: Universe, proposalScheme: ProposalSc if (debug) println("Rejecting!\n") state.oldValues foreach (setValue(_)) state.oldRandomness foreach (setRandomness(_)) - + /* Have to call generateValue on chains after a rejection to restore the old resulting * element. We can't do this above because we have to ensure the value of parent is restored before we * do this. - */ - for((elem, value) <- state.oldValues) { + */ + for ((elem, value) <- state.oldValues) { elem match { - case c: Chain[_,_] => c.generateValue - case _ => + case c: Chain[_, _] => c.generateValue + case _ => } } } @@ -351,7 +356,7 @@ abstract class MetropolisHastings(universe: Universe, proposalScheme: ProposalSc for { i <- 1 to numSamples } { val newStateUnconstrained = proposeAndUpdate() val state1 = State(newStateUnconstrained.oldValues, newStateUnconstrained.oldRandomness, - newStateUnconstrained.proposalProb, newStateUnconstrained.modelProb + computeScores, newStateUnconstrained.dissatisfied) + newStateUnconstrained.proposalProb, newStateUnconstrained.modelProb + computeScores, newStateUnconstrained.dissatisfied) if (decideToAccept(state1)) { accepts += 1 // collect results for the new state and restore the original state diff --git a/Figaro/src/main/scala/com/cra/figaro/algorithm/sampling/WeightedSampler.scala b/Figaro/src/main/scala/com/cra/figaro/algorithm/sampling/WeightedSampler.scala index 2c2113ea..93498516 100644 --- a/Figaro/src/main/scala/com/cra/figaro/algorithm/sampling/WeightedSampler.scala +++ b/Figaro/src/main/scala/com/cra/figaro/algorithm/sampling/WeightedSampler.scala @@ -68,11 +68,13 @@ abstract class WeightedSampler(override val universe: Universe, targets: Element protected def doSample(): Unit = { val s = sample() + universe.clearTemporaries() totalWeight = logSum(s._1, totalWeight) allWeightsSeen foreach (updateWeightSeenForTarget(s, _)) } - protected def update(): Unit = {} + protected def update(): Unit = { + } private def projection[T](target: Element[T]): List[(T, Double)] = { val weightSeen = allWeightsSeen.find(_._1 == target).get._2.asInstanceOf[Map[T, Double]] diff --git a/Figaro/src/main/scala/com/cra/figaro/language/Atomic.scala b/Figaro/src/main/scala/com/cra/figaro/language/Atomic.scala index ca408e6d..b7037e63 100644 --- a/Figaro/src/main/scala/com/cra/figaro/language/Atomic.scala +++ b/Figaro/src/main/scala/com/cra/figaro/language/Atomic.scala @@ -17,12 +17,9 @@ package com.cra.figaro.language * The Atomic trait characterizes elements that do not depend on any related elements. */ -trait Atomic[T] extends Element[T] { +trait Atomic[T] extends Element[T] with HasDensity[T] { /** * Returns an empty list. */ def args: List[Element[_]] = List() - - /** The probability density of a value. */ - def density(t: T): Double } diff --git a/Figaro/src/main/scala/com/cra/figaro/language/Flip.scala b/Figaro/src/main/scala/com/cra/figaro/language/Flip.scala index 99269b26..1ff37974 100644 --- a/Figaro/src/main/scala/com/cra/figaro/language/Flip.scala +++ b/Figaro/src/main/scala/com/cra/figaro/language/Flip.scala @@ -62,6 +62,26 @@ class ParameterizedFlip(name: Name[Boolean], override val parameter: AtomicBeta, protected def probValue = parameter.value + def distributionToStatistics(distribution: Stream[(Double, Boolean)]): Seq[Double] = { + val distList = distribution.toList + val trueProb = + distList.find(_._2) match { + case Some((prob,_)) => prob + case None => 0.0 + } + val falseProb = + distList.find(!_._2) match { + case Some((prob,_)) => prob + case None => 0.0 + } + List(trueProb, falseProb) + } + + def density(value: Boolean): Double = { + val prob = parameter.value + if (value) prob; else 1.0 - prob + } + override def toString = "Parameterized Flip(" + parameter + ")" } @@ -74,15 +94,14 @@ object Flip extends Creatable { /** * A coin toss where the weight is itself an element. + * + * If the element is an atomic beta element, the flip uses that element + * as a learnable parameter. */ - def apply(prob: Element[Double])(implicit name: Name[Boolean], collection: ElementCollection) = - new CompoundFlip(name, prob, collection) - - /** - * A coin toss where the weight is specified by a learnable parameter. - */ - def apply(prob: AtomicBeta)(implicit name: Name[Boolean], collection: ElementCollection) = - new ParameterizedFlip(name, prob, collection) + def apply(prob: Element[Double])(implicit name: Name[Boolean], collection: ElementCollection) = { + if (prob.isInstanceOf[AtomicBeta]) new ParameterizedFlip(name, prob.asInstanceOf[AtomicBeta], collection) + else new CompoundFlip(name, prob, collection) + } /** Used for reflection. */ type ResultType = Boolean diff --git a/Figaro/src/main/scala/com/cra/figaro/language/HasDensity.scala b/Figaro/src/main/scala/com/cra/figaro/language/HasDensity.scala new file mode 100644 index 00000000..ba79dbcc --- /dev/null +++ b/Figaro/src/main/scala/com/cra/figaro/language/HasDensity.scala @@ -0,0 +1,6 @@ +package com.cra.figaro.language + +trait HasDensity[T] extends Element[T] { + /** The probability density of a value. */ + def density(t: T): Double +} diff --git a/Figaro/src/main/scala/com/cra/figaro/language/Parameterized.scala b/Figaro/src/main/scala/com/cra/figaro/language/Parameterized.scala index 2a4e12d4..42206378 100644 --- a/Figaro/src/main/scala/com/cra/figaro/language/Parameterized.scala +++ b/Figaro/src/main/scala/com/cra/figaro/language/Parameterized.scala @@ -18,9 +18,11 @@ package com.cra.figaro.language * Parameterized elements are compound elements whose outcome is determined by a learnable parameter. */ -trait Parameterized[T] extends Element[T] { +trait Parameterized[T] extends Element[T] with HasDensity[T] { /** * The parameter for this element. */ val parameter: Parameter[_] + + def distributionToStatistics(distribution: Stream[(Double, T)]): Seq[Double] } diff --git a/Figaro/src/main/scala/com/cra/figaro/language/Select.scala b/Figaro/src/main/scala/com/cra/figaro/language/Select.scala index 4e4c1873..a2e0f451 100644 --- a/Figaro/src/main/scala/com/cra/figaro/language/Select.scala +++ b/Figaro/src/main/scala/com/cra/figaro/language/Select.scala @@ -79,6 +79,24 @@ class ParameterizedSelect[T](name: Name[T], override val parameter: AtomicDirich def args: List[Element[_]] = List(parameter) private lazy val normalizedClauses = normalizedProbs zip outcomes + def distributionToStatistics(distribution: Stream[(Double, T)]): Seq[Double] = { + val distList = distribution.toList + for { outcome <- outcomes } + yield { + distList.find(_._2 == outcome) match { + case Some((prob, _)) => prob + case None => 0.0 + } + } + } + + def density(value: T): Double = { + outcomes.indexOf(value) match { + case -1 => 0.0 + case i => parameter.value(i) + } + } + def generateValue(rand: Randomness) = selectMultinomial(rand, normalizedClauses) } diff --git a/Figaro/src/main/scala/com/cra/figaro/library/atomic/continuous/Beta.scala b/Figaro/src/main/scala/com/cra/figaro/library/atomic/continuous/Beta.scala index 64e8c089..3498d039 100644 --- a/Figaro/src/main/scala/com/cra/figaro/library/atomic/continuous/Beta.scala +++ b/Figaro/src/main/scala/com/cra/figaro/library/atomic/continuous/Beta.scala @@ -62,6 +62,8 @@ class AtomicBeta(name: Name[Double], a: Double, b: Double, collection: ElementCo /** * Returns an element that models the learned distribution. + * + * @deprecated */ def getLearnedElement: AtomicFlip = { new AtomicFlip("", MAPValue, collection) @@ -89,7 +91,8 @@ class AtomicBeta(name: Name[Double], a: Double, b: Double, collection: ElementCo } def MAPValue: Double = { - (learnedAlpha - 1) / (learnedAlpha + learnedBeta - 2) + if (learnedAlpha + learnedBeta == 2) 0.5 + else (learnedAlpha - 1) / (learnedAlpha + learnedBeta - 2) } def makeValues(depth: Int) = ValueSet.withoutStar(Set(MAPValue)) diff --git a/Figaro/src/main/scala/com/cra/figaro/library/atomic/continuous/BetaParameter.scala b/Figaro/src/main/scala/com/cra/figaro/library/atomic/continuous/BetaParameter.scala index 05968993..bc317653 100644 --- a/Figaro/src/main/scala/com/cra/figaro/library/atomic/continuous/BetaParameter.scala +++ b/Figaro/src/main/scala/com/cra/figaro/library/atomic/continuous/BetaParameter.scala @@ -22,7 +22,7 @@ object BetaParameter extends Creatable { /** * Create a beta parameter with prior hyperparameters a and b * - * @depracated + * @deprecated */ def apply(a: Double, b: Double)(implicit name: Name[Double], collection: ElementCollection) = new AtomicBeta(name, a, b, collection) diff --git a/Figaro/src/main/scala/com/cra/figaro/library/atomic/continuous/Dirichlet.scala b/Figaro/src/main/scala/com/cra/figaro/library/atomic/continuous/Dirichlet.scala index 8f8dcd29..e1bfaa27 100644 --- a/Figaro/src/main/scala/com/cra/figaro/library/atomic/continuous/Dirichlet.scala +++ b/Figaro/src/main/scala/com/cra/figaro/library/atomic/continuous/Dirichlet.scala @@ -28,6 +28,12 @@ import scala.collection.mutable */ class AtomicDirichlet(name: Name[Array[Double]], val alphas: Array[Double], collection: ElementCollection) extends Element[Array[Double]](name, collection) with Atomic[Array[Double]] with Parameter[Array[Double]] with ValuesMaker[Array[Double]] { + + /** + * The number of concentration parameters in the Dirichlet distribution. + */ + val size = alphas.size + type Randomness = Array[Double] def generateRandomness(): Array[Double] = { @@ -61,6 +67,8 @@ class AtomicDirichlet(name: Name[Array[Double]], val alphas: Array[Double], coll /** * Returns an element that models the learned distribution. + * + * @deprecated */ def getLearnedElement[T](outcomes: List[T]): AtomicSelect[T] = { new AtomicSelect("", MAPValue.toList zip outcomes, collection) @@ -68,20 +76,13 @@ class AtomicDirichlet(name: Name[Array[Double]], val alphas: Array[Double], coll def maximize(sufficientStatistics: Seq[Double]) = { require(sufficientStatistics.size == concentrationParameters.size) - for (i <- sufficientStatistics.indices) { concentrationParameters(i) = sufficientStatistics(i) + alphas(i) } - } private val vector = alphas.map(a => 0.0) - /** - * The number of concentration parameters in the Dirichlet distribution. - */ - val size = alphas.size - private[figaro] override def sufficientStatistics[A](i: Int): Seq[Double] = { val result = vector require(i < result.size) @@ -102,8 +103,8 @@ class AtomicDirichlet(name: Name[Array[Double]], val alphas: Array[Double], coll override def expectedValue: Array[Double] = { val sumObservedAlphas = concentrationParameters reduceLeft (_ + _) - val result = new Array[Double](concentrationParameters.size) - + val result = new Array[Double](size) + concentrationParameters.zipWithIndex.foreach { case (v, i) => { result(i) = (v) / (sumObservedAlphas) @@ -116,14 +117,15 @@ class AtomicDirichlet(name: Name[Array[Double]], val alphas: Array[Double], coll override def MAPValue: Array[Double] = { val sumObservedAlphas = concentrationParameters reduceLeft (_ + _) - val result = new Array[Double](concentrationParameters.size) + val result = new Array[Double](size) concentrationParameters.zipWithIndex.foreach { case (v, i) => { - result(i) = (v - 1) / (sumObservedAlphas - concentrationParameters.size) - } + result(i) = + if (sumObservedAlphas == size) 1.0 / size + else (v - 1) / (sumObservedAlphas - size) + } } - result } diff --git a/Figaro/src/main/scala/com/cra/figaro/library/atomic/continuous/MultivariateNormal.scala b/Figaro/src/main/scala/com/cra/figaro/library/atomic/continuous/MultivariateNormal.scala new file mode 100644 index 00000000..3c898b1c --- /dev/null +++ b/Figaro/src/main/scala/com/cra/figaro/library/atomic/continuous/MultivariateNormal.scala @@ -0,0 +1,101 @@ +/* + * MultivariateNormal.scala + * + * Elements representing a multivariate normal distributions + * + * Created By: Glenn Takata (gtakata@cra.com) + * Creation Date: Jun 2, 2014 + * + * Copyright 2014 Avrom J. Pfeffer and Charles River Analytics, Inc. + * See http://www.cra.com or email figaro@cra.com for information. + * + * See http://www.github.com/p2t2/figaro for a copy of the software license. + */ + +package com.cra.figaro.library.atomic.continuous + +import com.cra.figaro.language._ +import com.cra.figaro.util.random +import scala.math._ +import scala.collection.JavaConversions +import org.apache.commons.math3.distribution.MultivariateNormalDistribution + +/** + * A multivariate normal distribution in which the means and variance-covariances are constants. + */ +class AtomicMultivariateNormal(name: Name[List[Double]], val means: List[Double], val covariances: List[List[Double]], collection: ElementCollection) + extends Element[List[Double]](name, collection) with Atomic[List[Double]] { + + val distribution = new MultivariateNormalDistribution(means.toArray, covariances.map((l: List[Double]) => l.toArray).toArray) + + lazy val standardDeviations = distribution.getStandardDeviations() + + type Randomness = List[Double] + + def generateRandomness(): List[Double] = { + distribution.sample.toList + } + + def generateValue(rand: Randomness) = rand + + /** + * Density of a value. + */ + def density(d: List[Double]) = { + distribution.density(d.toArray) + } + + override def toString = "MultivariateNormal(" + means + ",\n" + covariances + ")" +} + +/** + * A normal distribution in which the mean is an element and the variance is constant. + */ +class MultivariateNormalCompoundMean(name: Name[List[Double]], val means: Element[List[Double]], val covariances: List[List[Double]], collection: ElementCollection) + extends NonCachingChain( + name, + means, + (m: List[Double]) => new AtomicMultivariateNormal("", m, covariances, collection), + collection) { + override def toString = "Normal(" + means + ",\n " + covariances + ")" +} + +/** + * A normal distribution in which the mean and variance are both elements. + */ +class MultivariateCompoundNormal(name: Name[List[Double]], val mean: Element[List[Double]], val variance: Element[List[List[Double]]], collection: ElementCollection) + extends NonCachingChain[List[Double], List[Double]]( + name, + mean, + (m: List[Double]) => new NonCachingChain( + "", + variance, + (v: List[List[Double]]) => new AtomicMultivariateNormal("", m, v, collection), + collection), + collection) { + override def toString = "Normal(" + mean + ", " + variance + ")" +} + +object MultivariateNormal extends Creatable { + /** + * Create a normal distribution in which the mean and variance are constants. + */ + def apply(means: List[Double], covariances: List[List[Double]])(implicit name: Name[List[Double]], collection: ElementCollection) = + new AtomicMultivariateNormal(name, means, covariances, collection) + + /** + * Create a normal distribution in which the mean is an element and the variance is constant. + */ + def apply(means: Element[List[Double]], covariances: List[List[Double]])(implicit name: Name[List[Double]], collection: ElementCollection) = + new MultivariateNormalCompoundMean(name, means, covariances, collection) + + /** + * Create a normal distribution in both the mean and the variance are elements. + */ + def apply(mean: Element[List[Double]], variance: Element[List[List[Double]]])(implicit name: Name[List[Double]], collection: ElementCollection) = + new MultivariateCompoundNormal(name, mean, variance, collection) + + type ResultType = List[Double] + + def create(args: List[Element[_]]) = apply(args(0).asInstanceOf[Element[List[Double]]], args(1).asInstanceOf[Element[List[List[Double]]]]) +} diff --git a/Figaro/src/main/scala/com/cra/figaro/library/atomic/discrete/Binomial.scala b/Figaro/src/main/scala/com/cra/figaro/library/atomic/discrete/Binomial.scala index 1c3f3388..7c1b8b9b 100644 --- a/Figaro/src/main/scala/com/cra/figaro/library/atomic/discrete/Binomial.scala +++ b/Figaro/src/main/scala/com/cra/figaro/library/atomic/discrete/Binomial.scala @@ -19,31 +19,29 @@ import com.cra.figaro.algorithm.factored._ import com.cra.figaro.language._ import com.cra.figaro.library.atomic.continuous._ import annotation.tailrec -import scala.math.{ floor, pow } -import JSci.maths.ExtraMath.binomial /** * A binomial distribution in which the parameters are constants. */ -class AtomicBinomial(name: Name[Int], n: Int, p: Double, collection: ElementCollection) +class AtomicBinomial(name: Name[Int], val numTrials: Int, val probSuccess: Double, collection: ElementCollection) extends Element[Int](name, collection) with Atomic[Int] with ValuesMaker[Int] with ProbFactorMaker with Cacheable[Int] with OneShifter { protected lazy val lowerBound = 0 - protected lazy val upperBound = n + protected lazy val upperBound = numTrials - private lazy val q = 1 - p + private lazy val q = 1 - probSuccess // Devroye, p. 525 @tailrec private def generateHelper(x: Int, sum: Int): Int = { - val g = Util.generateGeometric(1 - p) + val g = Util.generateGeometric(1 - probSuccess) val newSum = sum + g val newX = x + 1 - if (newSum <= n) generateHelper(newX, newSum) + if (newSum <= numTrials) generateHelper(newX, newSum) else newX } - def generateRandomness() = if (p <= 0) 0; else if (p < 1) generateHelper(-1, 0); else n + def generateRandomness() = if (probSuccess <= 0) 0; else if (probSuccess < 1) generateHelper(-1, 0); else numTrials /** * The Metropolis-Hastings proposal is to increase or decrease the value of by 1. @@ -56,22 +54,13 @@ class AtomicBinomial(name: Name[Int], n: Int, p: Double, collection: ElementColl * Probability of a value. */ def density(k: Int) = { - if (n > 10) { - val logNFact = JSci.maths.ExtraMath.logFactorial(n) - val logKFact = JSci.maths.ExtraMath.logFactorial(k) - val logNMinusKFact = JSci.maths.ExtraMath.logFactorial(n-k) - val logBinomialCoefficient = logNFact - (logKFact + logNMinusKFact) - val result = logBinomialCoefficient + (k*Math.log(p) + ((n-k)*Math.log(q))) - Math.exp(result) - } else { - binomial(n, k) * pow(p, k) * pow(q, n - k) - } + Util.binomialDensity(numTrials, probSuccess, k) } /** * Return the range of values of the element. */ - def makeValues(depth: Int) = ValueSet.withoutStar((for { i <- 0 to n } yield i).toSet) + def makeValues(depth: Int) = ValueSet.withoutStar((for { i <- 0 to numTrials } yield i).toSet) /** * Convert an element into a list of factors. @@ -85,29 +74,63 @@ class AtomicBinomial(name: Name[Int], n: Int, p: Double, collection: ElementColl List(factor) } - override def toString = "Binomial(" + n + ", " + p + ")" + override def toString = "Binomial(" + numTrials + ", " + probSuccess + ")" } /** * A binomial distribution in which the number of trials is fixed and the success probability is an element. */ -class BinomialFixedNumTrials(name: Name[Int], n: Int, p: Element[Double], collection: ElementCollection) - extends NonCachingChain[Double, Int](name, p, (p: Double) => new AtomicBinomial("", n, p, collection), collection) +class BinomialFixedNumTrials(name: Name[Int], val numTrials: Int, val probSuccess: Element[Double], collection: ElementCollection) + extends NonCachingChain[Double, Int](name, probSuccess, (p: Double) => new AtomicBinomial("", numTrials, p, collection), collection) { + override def toString = "Binomial(" + numTrials + ", " + probSuccess + ")" +} + + /** + * A binomial with a fixed number of trials parameterized by a beta distribution. + */ +class ParameterizedBinomialFixedNumTrials(name: Name[Int], val numTrials: Int, val probSuccess: AtomicBeta, collection: ElementCollection) + extends CachingChain[Double, Int](name, probSuccess, (p: Double) => new AtomicBinomial("", numTrials, p, collection), collection) + with Parameterized[Int] { + val parameter = probSuccess + + def distributionToStatistics(distribution: Stream[(Double, Int)]): Seq[Double] = { + val distList = distribution.toList + var totalPos = 0.0 + var totalNeg = 0.0 + for { i <- 0 to numTrials } { + distList.find(_._2 == i) match { + case Some((prob, _)) => + totalPos += prob * i + totalNeg += prob * (numTrials - i) + case None => () + } + } + List(totalPos, totalNeg) + } + + def density(value: Int): Double = { + val probSuccess = parameter.value + if (value < 0 || value > numTrials) 0.0 + else Util.binomialDensity(numTrials, probSuccess, value) + } + + override def toString = "ParameterizedBinomial(" + numTrials + ", " + probSuccess + ")" +} /** * A binomial distribution in which the parameters are elements. */ -class CompoundBinomial(name: Name[Int], n: Element[Int], p: Element[Double], collection: ElementCollection) +class CompoundBinomial(name: Name[Int], val numTrials: Element[Int], val probSuccess: Element[Double], collection: ElementCollection) extends CachingChain[Int, Int]( name, - n, + numTrials, (n: Int) => new NonCachingChain( "", - p, + probSuccess, (p: Double) => new AtomicBinomial("", n, p, collection), collection), collection) { - override def toString = "Binomial(" + n + ", " + p + ")" + override def toString = "Binomial(" + numTrials + ", " + probSuccess + ")" } object Binomial extends Creatable { @@ -119,9 +142,16 @@ object Binomial extends Creatable { /** * Create a binomial distribution in which the number of trials is fixed and the success probability is an element. + * + * If the element is an atomic beta element, the flip uses that element + * as a learnable parameter. */ - def apply(n: Int, p: Element[Double])(implicit name: Name[Int], collection: ElementCollection) = - new BinomialFixedNumTrials(name, n, p, collection) + def apply(n: Int, p: Element[Double])(implicit name: Name[Int], collection: ElementCollection) = { + if (p.isInstanceOf[AtomicBeta]) + new ParameterizedBinomialFixedNumTrials(name, n, p.asInstanceOf[AtomicBeta], collection) + else new BinomialFixedNumTrials(name, n, p, collection) + } + /** * Create a binomial distribution in which the parameters are elements. */ diff --git a/Figaro/src/main/scala/com/cra/figaro/library/atomic/discrete/Util.scala b/Figaro/src/main/scala/com/cra/figaro/library/atomic/discrete/Util.scala index cd54325c..30a76c45 100644 --- a/Figaro/src/main/scala/com/cra/figaro/library/atomic/discrete/Util.scala +++ b/Figaro/src/main/scala/com/cra/figaro/library/atomic/discrete/Util.scala @@ -22,5 +22,24 @@ object Util { */ def generateGeometric(probFail: Double) = ceil(log(random.nextDouble()) / log(probFail)).toInt + + /** + * Density of the given number of positive outcomes under a binomial random variable with the given number of trials. + * Computing a binomial coefficient exactly can be very expensive for a large number of trials, so this method uses + * an approximation algorithm when the number of trials is sufficiently large. + */ + def binomialDensity(numTrials: Int, probSuccess: Double, numPositive: Int): Double = { + val q = 1 - probSuccess + if (numTrials > 10) { + val logNFact = JSci.maths.ExtraMath.logFactorial(numTrials) + val logKFact = JSci.maths.ExtraMath.logFactorial(numPositive) + val logNMinusKFact = JSci.maths.ExtraMath.logFactorial(numTrials-numPositive) + val logBinomialCoefficient = logNFact - (logKFact + logNMinusKFact) + val result = logBinomialCoefficient + (numPositive*Math.log(probSuccess) + ((numTrials-numPositive)*Math.log(q))) + Math.exp(result) + } else { + JSci.maths.ExtraMath.binomial(numTrials, numPositive) * math.pow(probSuccess, numPositive) * math.pow(q, numTrials - numPositive) + } + } } diff --git a/Figaro/src/test/scala/com/cra/figaro/test/algorithm/learning/EMWithBPTest.scala b/Figaro/src/test/scala/com/cra/figaro/test/algorithm/learning/EMWithBPTest.scala new file mode 100644 index 00000000..0d46c456 --- /dev/null +++ b/Figaro/src/test/scala/com/cra/figaro/test/algorithm/learning/EMWithBPTest.scala @@ -0,0 +1,677 @@ +/* + * EMWithBPTest.scala + * Tests for the EM algorithm + * + * Created By: Michael Howard (mhoward@cra.com) + * Creation Date: Jun 6, 2013 + * + * Copyright 2013 Avrom J. Pfeffer and Charles River Analytics, Inc. + * See http://www.cra.com or email figaro@cra.com for information. + * + * See http://www.github.com/p2t2/figaro for a copy of the software license. + */ + +package com.cra.figaro.test.algorithm.learning + +import org.scalatest.Matchers +import org.scalatest.{ PrivateMethodTester, WordSpec } +import com.cra.figaro.algorithm._ +import com.cra.figaro.algorithm.factored._ +import com.cra.figaro.algorithm.sampling._ +import com.cra.figaro.algorithm.learning._ +import com.cra.figaro.library.atomic.continuous._ +import com.cra.figaro.library.atomic.discrete.Binomial +import com.cra.figaro.library.compound._ +import com.cra.figaro.language._ +import com.cra.figaro.language.Universe._ +import com.cra.figaro.util._ +import com.cra.figaro.util.random +import scala.math.abs +import java.io._ + +class EMWithBPTest extends WordSpec with PrivateMethodTester with Matchers { + + "Expectation Maximization with belief propagation" when + { + + "used to estimate a Beta parameter" should + { + + "detect bias after a large enough number of trials" in + { + val universe = Universe.createNew + val b = BetaParameter(2, 2) + + for (i <- 1 to 7) { + + val f = Flip(b) + f.observe(true) + } + + for (i <- 1 to 3) { + + val f = Flip(b) + f.observe(false) + } + + val algorithm = EMWithBP(15, 10, b)(universe) + algorithm.start + + val result = b.getLearnedElement + algorithm.kill + result.prob should be(0.6666 +- 0.01) + + } + + "take the prior concentration parameters into account" in + { + val universe = Universe.createNew + val b = BetaParameter(3.0, 7.0) + + for (i <- 1 to 7) { + + val f = Flip(b) + f.observe(true) + } + + for (i <- 1 to 3) { + val f = Flip(b) + f.observe(false) + } + + val algorithm = EMWithBP(15, 10, b)(universe) + algorithm.start + + val result = b.getLearnedElement + algorithm.kill + result.prob should be(0.50 +- 0.01) + } + + "learn the bias from observations of binomial elements" in { + val universe = Universe.createNew + val b = BetaParameter(2, 2) + + val b1 = Binomial(7, b) + b1.observe(6) + val b2 = Binomial(3, b) + b2.observe(1) + + val algorithm = EMWithBP(15, 10, b)(universe) + algorithm.start + + val result = b.getLearnedElement + algorithm.kill + result.prob should be(0.6666 +- 0.01) + } + } + + "correctly use a uniform prior" in { + val universe = Universe.createNew + val b = BetaParameter(1, 1) + + val b1 = Binomial(7, b) + b1.observe(6) + val b2 = Binomial(3, b) + b2.observe(1) + + val algorithm = EMWithBP(15, 10, b)(universe) + algorithm.start + + val result = b.getLearnedElement + algorithm.kill + result.prob should be(0.7 +- 0.01) + + + } + + "used to estimate a Dirichlet parameter with two concentration parameters" should + { + + "detect bias after a large enough number of trials" in + { + val universe = Universe.createNew + val b = DirichletParameter(2, 2) + + for (i <- 1 to 7) { + + val f = Select(b, true, false) + f.observe(true) + } + + for (i <- 1 to 3) { + + val f = Select(b, true, false) + f.observe(false) + } + + val algorithm = EMWithBP(15, 10, b)(universe) + algorithm.start + + val result = b.getLearnedElement(List(true, false)) + algorithm.kill + result.probs(0) should be(0.6666 +- 0.01) + } + + "take the prior concentration parameters into account" in + { + val universe = Universe.createNew + + val b = DirichletParameter(3, 7) + + for (i <- 1 to 7) { + + val f = Select(b, true, false) + f.observe(true) + } + + for (i <- 1 to 3) { + val f = Select(b, true, false) + f.observe(false) + } + + val algorithm = EMWithBP(15, 10, b)(universe) + algorithm.start + + val result = b.getLearnedElement(List(true, false)) + algorithm.kill + result.probs(0) should be(0.50 +- 0.01) + + } + + + } + + "used to estimate a Dirichlet parameter with three concentration parameters" should + { + + "calculate sufficient statistics in the correct order for long lists of concentration parameters" in + { + val universe = Universe.createNew + val alphas = Seq[Double](0.0476, 0.0476, 0.0476, 0.0476, 0.0476, 0.0476, 0.0476, 0.0476, 0.0476, 0.0476, 0.0476, 0.0476, 0.0476, 0.0476, 0.0476, 0.0476, 0.0476, 0.0476, 0.0476, 0.0476, 0.0476, 0.0476) + val d = DirichletParameter(alphas: _*) + val outcomes = List(2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23) + val outcome = Select(d, outcomes: _*) + val algorithm = EMWithBP(5, 10, d) + algorithm.start + + val result = d.getLearnedElement(outcomes) + algorithm.kill + result.probs(0) should be(0.04 +- 0.01) + result.probs(1) should be(0.04 +- 0.01) + result.probs(2) should be(0.04 +- 0.01) + result.probs(3) should be(0.04 +- 0.01) + result.probs(4) should be(0.04 +- 0.01) + result.probs(5) should be(0.04 +- 0.01) + result.probs(6) should be(0.04 +- 0.01) + result.probs(7) should be(0.04 +- 0.01) + result.probs(8) should be(0.04 +- 0.01) + result.probs(9) should be(0.04 +- 0.01) + result.probs(10) should be(0.04 +- 0.01) + result.probs(11) should be(0.04 +- 0.01) + result.probs(12) should be(0.04 +- 0.01) + result.probs(13) should be(0.04 +- 0.01) + result.probs(14) should be(0.04 +- 0.01) + result.probs(15) should be(0.04 +- 0.01) + result.probs(16) should be(0.04 +- 0.01) + result.probs(17) should be(0.04 +- 0.01) + result.probs(18) should be(0.04 +- 0.01) + result.probs(19) should be(0.04 +- 0.01) + result.probs(20) should be(0.04 +- 0.01) + result.probs(21) should be(0.04 +- 0.01) + + } + + "calculate sufficient statistics in the correct order for long lists of concentration parameters, taking into account a condition" in + { + val universe = Universe.createNew + val alphas = Seq[Double](1.0476, 1.0476, 1.0476, 1.0476, 1.0476) + val d = DirichletParameter(alphas: _*) + val outcomes = List(2, 3, 4, 5, 6) + + for (i <- 1 to 10) { + val outcome = Select(d, outcomes: _*) + outcome.addCondition(x => x >= 3 && x <= 6) + } + + val algorithm = EMWithBP(2, 10, d) + algorithm.start + + val result = d.getLearnedElement(outcomes) + algorithm.kill + result.probs(0) should be(0.0 +- 0.01) + result.probs(1) should be(0.25 +- 0.01) + result.probs(2) should be(0.25 +- 0.01) + result.probs(3) should be(0.25 +- 0.01) + result.probs(4) should be(0.25 +- 0.01) + } + + "detect bias after a large enough number of trials" in + { + val universe = Universe.createNew + val b = DirichletParameter(2, 2, 2) + val outcomes = List(1, 2, 3) + val errorTolerance = 0.01 + for (i <- 1 to 8) { + val f = Select(b, outcomes: _*) + f.observe(1) + } + + for (i <- 1 to 6) { + val f = Select(b, outcomes: _*) + f.observe(2) + } + + for (i <- 1 to 2) { + val f = Select(b, outcomes: _*) + f.observe(3) + } + + val algorithm = EMWithBP(15, 10, b)(universe) + algorithm.start + + val result = b.getLearnedElement(outcomes) + algorithm.kill + //9/19 + result.probs(0) should be(0.47 +- errorTolerance) + //7/19 + result.probs(1) should be(0.36 +- errorTolerance) + //3/19 + result.probs(2) should be(0.15 +- errorTolerance) + } + + "take the prior concentration parameters into account" in + { + val universe = Universe.createNew + val b = DirichletParameter(2.0, 3.0, 2.0) + val outcomes = List(1, 2, 3) + + for (i <- 1 to 3) { + val f2 = Select(b, outcomes: _*) + + f2.observe(1) + } + + for (i <- 1 to 2) { + val f3 = Select(b, outcomes: _*) + f3.observe(2) + } + + for (i <- 1 to 3) { + + val f1 = Select(b, outcomes: _*) + f1.observe(3) + } + + val algorithm = EMWithBP(3, 10, b)(universe) + algorithm.start + + val result = b.getLearnedElement(outcomes) + algorithm.kill + result.probs(0) should be(0.33 +- 0.01) + result.probs(1) should be(0.33 +- 0.01) + result.probs(2) should be(0.33 +- 0.01) + + } + + "correctly use a uniform prior" in + { + val universe = Universe.createNew + val b = DirichletParameter(1.0, 1.0, 1.0) + val outcomes = List(1, 2, 3) + + for (i <- 1 to 3) { + + val f2 = Select(b, outcomes: _*) + f2.observe(1) + } + + for (i <- 1 to 3) { + val f3 = Select(b, outcomes: _*) + f3.observe(2) + } + + for (i <- 1 to 3) { + + val f1 = Select(b, outcomes: _*) + f1.observe(3) + } + + val algorithm = EMWithBP(3, 10, b)(universe) + algorithm.start + + val result = b.getLearnedElement(outcomes) + algorithm.kill + result.probs(0) should be(0.33 +- 0.01) + result.probs(1) should be(0.33 +- 0.01) + result.probs(2) should be(0.33 +- 0.01) + } + + } + + "used to estimate multiple parameters" should + { + + "leave parameters having no observations unchanged" in + { + val universe = Universe.createNew + val d = DirichletParameter(2.0, 4.0, 2.0) + val b = BetaParameter(2.0, 2.0) + val outcomes = List(1, 2, 3) + + for (i <- 1 to 4) { + + val f2 = Select(d, outcomes: _*) + f2.observe(1) + } + + for (i <- 1 to 2) { + + val f3 = Select(d, outcomes: _*) + f3.observe(2) + } + + for (i <- 1 to 4) { + + val f1 = Select(d, outcomes: _*) + f1.observe(3) + } + + val algorithm = EMWithBP(100, 10, d, b)(universe) + algorithm.start + + val result = d.getLearnedElement(outcomes) + algorithm.kill + result.probs(0) should be(0.33 +- 0.01) + result.probs(1) should be(0.33 +- 0.01) + result.probs(2) should be(0.33 +- 0.01) + + val betaResult = b.getLearnedElement + betaResult.prob should be(0.5) + + } + + "correctly estimate all parameters with observations" in + { + val universe = Universe.createNew + val d = DirichletParameter(2.0, 3.0, 2.0) + val b = BetaParameter(3.0, 7.0) + val outcomes = List(1, 2, 3) + + for (i <- 1 to 3) { + + val f2 = Select(d, outcomes: _*) + f2.observe(1) + } + + for (i <- 1 to 2) { + val f3 = Select(d, outcomes: _*) + f3.observe(2) + } + + for (i <- 1 to 3) { + + val f1 = Select(d, outcomes: _*) + f1.observe(3) + } + + for (i <- 1 to 7) { + + val f = Flip(b) + f.observe(true) + } + + for (i <- 1 to 3) { + + val f = Flip(b) + f.observe(false) + } + + val algorithm = EMWithBP(5, 10, b,d)(universe) + algorithm.start + + val result = d.getLearnedElement(outcomes) + + result.probs(0) should be(0.33 +- 0.01) + result.probs(1) should be(0.33 +- 0.01) + result.probs(2) should be(0.33 +- 0.01) + + val betaResult = b.getLearnedElement + betaResult.prob should be(0.5 +- 0.01) + + } + } + + val observationProbability = 0.7 + val trainingSetSize = 100 + val testSetSize = 100 + val minScale = 10 + + val maxScale = 10 + val scaleStep = 2 + + abstract class Parameters(val universe: Universe) { + val b1: Element[Double] + val b2: Element[Double] + val b3: Element[Double] + val b4: Element[Double] + val b5: Element[Double] + val b6: Element[Double] + val b7: Element[Double] + val b8: Element[Double] + val b9: Element[Double] + } + + val trueB1 = 0.1 + val trueB2 = 0.2 + val trueB3 = 0.3 + val trueB4 = 0.4 + val trueB5 = 0.5 + val trueB6 = 0.6 + val trueB7 = 0.7 + val trueB8 = 0.8 + val trueB9 = 0.9 + + val trueUniverse = new Universe + + object TrueParameters extends Parameters(trueUniverse) { + val b1 = Constant(trueB1)("b1", universe) + val b2 = Constant(trueB2)("b2", universe) + val b3 = Constant(trueB3)("b3", universe) + val b4 = Constant(trueB4)("b4", universe) + val b5 = Constant(trueB5)("b5", universe) + val b6 = Constant(trueB6)("b6", universe) + val b7 = Constant(trueB7)("b7", universe) + val b8 = Constant(trueB8)("b8", universe) + val b9 = Constant(trueB9)("b9", universe) + } + + class LearnableParameters(universe: Universe) extends Parameters(universe) { + val b1 = BetaParameter(1, 1)("b1", universe) + val b2 = BetaParameter(1, 1)("b2", universe) + val b3 = BetaParameter(1, 1)("b3", universe) + val b4 = BetaParameter(1, 1)("b4", universe) + val b5 = BetaParameter(1, 1)("b5", universe) + val b6 = BetaParameter(1, 1)("b6", universe) + val b7 = BetaParameter(1, 1)("b7", universe) + val b8 = BetaParameter(1, 1)("b8", universe) + val b9 = BetaParameter(1, 1)("b9", universe) + } + + var id = 0 + + class Model(val parameters: Parameters, flipConstructor: (Element[Double], String, Universe) => Flip) { + id += 1 + val universe = parameters.universe + val x = flipConstructor(parameters.b1, "x_" + id, universe) + val f2 = flipConstructor(parameters.b2, "f2_" + id, universe) + val f3 = flipConstructor(parameters.b3, "f3_" + id, universe) + val f4 = flipConstructor(parameters.b4, "f4_" + id, universe) + val f5 = flipConstructor(parameters.b5, "f5_" + id, universe) + val f6 = flipConstructor(parameters.b6, "f6_" + id, universe) + val f7 = flipConstructor(parameters.b7, "f7_" + id, universe) + val f8 = flipConstructor(parameters.b8, "f8_" + id, universe) + val f9 = flipConstructor(parameters.b9, "f9_" + id, universe) + val y = If(x, f2, f3)("y_" + id, universe) + val z = If(x, f4, f5)("z_" + id, universe) + val w = CPD(y, z, (true, true) -> f6, (true, false) -> f7, + (false, true) -> f8, (false, false) -> f9)("w_" + id, universe) + } + + def normalFlipConstructor(parameter: Element[Double], name: String, universe: Universe) = new CompoundFlip(name, parameter, universe) + + def learningFlipConstructor(parameter: Element[Double], name: String, universe: Universe) = { + parameter match { + case p: AtomicBeta => new ParameterizedFlip(name, p, universe) + case _ => throw new IllegalArgumentException("Not a beta parameter") + } + } + + object TrueModel extends Model(TrueParameters, normalFlipConstructor) + + case class Datum(x: Boolean, y: Boolean, z: Boolean, w: Boolean) + + def generateDatum(): Datum = { + val model = TrueModel + model.universe.generateAll() + Datum(model.x.value, model.y.value, model.z.value, model.w.value) + } + + def observe(model: Model, datum: Datum) { + if (random.nextDouble() < observationProbability) model.x.observe(datum.x) + if (random.nextDouble() < observationProbability) model.y.observe(datum.y) + if (random.nextDouble() < observationProbability) model.z.observe(datum.z) + if (random.nextDouble() < observationProbability) model.w.observe(datum.w) + } + + var nextSkip = 0 + + def predictionAccuracy(model: Model, datum: Datum): Double = { + model.x.unobserve() + model.y.unobserve() + model.z.unobserve() + model.w.unobserve() + val result = nextSkip match { + case 0 => + model.y.observe(datum.y) + model.z.observe(datum.z) + model.w.observe(datum.w) + val alg = VariableElimination(model.x)(model.universe) + alg.start() + alg.probability(model.x, datum.x) + case 1 => + model.x.observe(datum.x) + model.z.observe(datum.z) + model.w.observe(datum.w) + val alg = VariableElimination(model.y)(model.universe) + alg.start() + alg.probability(model.y, datum.y) + case 2 => + model.x.observe(datum.x) + model.y.observe(datum.y) + model.w.observe(datum.w) + val alg = VariableElimination(model.z)(model.universe) + alg.start() + alg.probability(model.z, datum.z) + case 3 => + model.x.observe(datum.x) + model.y.observe(datum.y) + model.z.observe(datum.z) + val alg = VariableElimination(model.w)(model.universe) + alg.start() + alg.probability(model.w, datum.w) + } + nextSkip = (nextSkip + 1) % 4 + result + } + + def parameterError(model: Model): Double = { + val parameters = model.parameters + (abs(parameters.b1.value - trueB1) + abs(parameters.b2.value - trueB2) + abs(parameters.b3.value - trueB3) + + abs(parameters.b4.value - trueB4) + abs(parameters.b5.value - trueB5) + abs(parameters.b6.value - trueB6) + + abs(parameters.b7.value - trueB7) + abs(parameters.b8.value - trueB8) + abs(parameters.b9.value - trueB9)) / 9.0 + } + + def assessModel(model: Model, testSet: Seq[Datum]): (Double, Double) = { + val paramErr = parameterError(model) + nextSkip = 0 + var totalPredictionAccuracy = 0.0 + for (datum <- testSet) (totalPredictionAccuracy += predictionAccuracy(model, datum)) + val predAcc = totalPredictionAccuracy / testSet.length + (paramErr, predAcc) + } + + def train(trainingSet: List[Datum], parameters: Parameters, algorithmCreator: Parameters => Algorithm, valueGetter: (Algorithm, Element[Double]) => Double, + flipConstructor: (Element[Double], String, Universe) => Flip): (Model, Double) = { + for (datum <- trainingSet) observe(new Model(parameters, flipConstructor), datum) + + val time0 = System.currentTimeMillis() + val algorithm = algorithmCreator(parameters) + algorithm.start() + + val resultUniverse = new Universe + def extractParameter(parameter: Element[Double], name: String) = + { + parameter match + { + case b: AtomicBeta => + { + + Constant(valueGetter(algorithm, parameter))(name, resultUniverse) + } + case _ => Constant(valueGetter(algorithm, parameter))(name, resultUniverse) + } + + } + val learnedParameters = new Parameters(resultUniverse) { + val b1 = extractParameter(parameters.b1, "b1"); b1.generate() + val b2 = extractParameter(parameters.b2, "b2"); b2.generate() + val b3 = extractParameter(parameters.b3, "b3"); b3.generate() + val b4 = extractParameter(parameters.b4, "b4"); b4.generate() + val b5 = extractParameter(parameters.b5, "b5"); b5.generate() + val b6 = extractParameter(parameters.b6, "b6"); b6.generate() + val b7 = extractParameter(parameters.b7, "b7"); b7.generate() + val b8 = extractParameter(parameters.b8, "b8"); b8.generate() + val b9 = extractParameter(parameters.b9, "b9"); b9.generate() + } + + algorithm.kill() + val time1 = System.currentTimeMillis() + val totalTime = (time1 - time0) / 1000.0 + println("Training time: " + totalTime + " seconds") + (new Model(learnedParameters, normalFlipConstructor), totalTime) + } + + "derive parameters within a reasonable accuracy for random data" in + { + + val numEMIterations = 5 + val testSet = List.fill(testSetSize)(generateDatum()) + val trainingSet = List.fill(trainingSetSize)(generateDatum()) + + def learner(parameters: Parameters): Algorithm = { + parameters match { + case ps: LearnableParameters => EMWithBP(numEMIterations, 10, ps.b1, ps.b2, ps.b3, ps.b4, ps.b5, ps.b6, ps.b7, ps.b8, ps.b9)(parameters.universe) + case _ => throw new IllegalArgumentException("Not learnable parameters") + } + } + + def parameterGetter(algorithm: Algorithm, parameter: Element[Double]): Double = { + parameter match { + case p: Parameter[Double] => { + p.MAPValue + } + case _ => throw new IllegalArgumentException("Not a learnable parameter") + } + } + + val (trueParamErr, truePredAcc) = assessModel(TrueModel, testSet) + val (learnedModel, learningTime) = train(trainingSet, new LearnableParameters(new Universe), learner, parameterGetter, learningFlipConstructor) + val (learnedParamErr, learnedPredAcc) = assessModel(learnedModel, testSet) + + println(learnedParamErr) + println(learnedPredAcc) + learnedParamErr should be(0.00 +- 0.12) + learnedPredAcc should be(truePredAcc +- 0.12) + } + } +} \ No newline at end of file diff --git a/Figaro/src/test/scala/com/cra/figaro/test/algorithm/learning/EMWithImportanceTest.scala b/Figaro/src/test/scala/com/cra/figaro/test/algorithm/learning/EMWithImportanceTest.scala new file mode 100644 index 00000000..f428eb94 --- /dev/null +++ b/Figaro/src/test/scala/com/cra/figaro/test/algorithm/learning/EMWithImportanceTest.scala @@ -0,0 +1,679 @@ +/* + * EMWithImportanceTest.scala + * Tests for the EM algorithm + * + * Created By: Michael Howard (mhoward@cra.com) + * Creation Date: Jun 6, 2013 + * + * Copyright 2013 Avrom J. Pfeffer and Charles River Analytics, Inc. + * See http://www.cra.com or email figaro@cra.com for information. + * + * See http://www.github.com/p2t2/figaro for a copy of the software license. + */ + +package com.cra.figaro.test.algorithm.learning + +import org.scalatest.Matchers +import org.scalatest.{ PrivateMethodTester, WordSpec } +import com.cra.figaro.algorithm._ +import com.cra.figaro.algorithm.factored._ +import com.cra.figaro.algorithm.sampling._ +import com.cra.figaro.algorithm.learning._ +import com.cra.figaro.library.atomic.continuous._ +import com.cra.figaro.library.atomic.discrete.Binomial +import com.cra.figaro.library.compound._ +import com.cra.figaro.language._ +import com.cra.figaro.language.Universe._ +import com.cra.figaro.util._ +import com.cra.figaro.util.random +import scala.math.abs +import java.io._ + +class EMWithImportanceTest extends WordSpec with PrivateMethodTester with Matchers { + "Expectation Maximization with importance sampling" when + { + "used to estimate a Beta parameter" should + { + "detect bias after a large enough number of trials" in + { + val universe = Universe.createNew + val b = BetaParameter(2, 2) + + for (i <- 1 to 7) { + + val f = Flip(b) + f.observe(true) + } + + for (i <- 1 to 3) { + + val f = Flip(b) + f.observe(false) + } + + val algorithm = EMWithImportance(2, 100, b)(universe) + algorithm.start + + val result = b.getLearnedElement + algorithm.kill + result.prob should be(0.6666 +- 0.01) + + } + + "take the prior concentration parameters into account" in + { + val universe = Universe.createNew + val b = BetaParameter(3.0, 7.0) + + for (i <- 1 to 7) { + + val f = Flip(b) + f.observe(true) + } + + for (i <- 1 to 3) { + + val f = Flip(b) + f.observe(false) + } + + val algorithm = EMWithImportance(2, 100, b)(universe) + algorithm.start + + val result = b.getLearnedElement + algorithm.kill + result.prob should be(0.50 +- 0.01) + + } + + + "learn the bias from observations of binomial elements" in { + val universe = Universe.createNew + val b = BetaParameter(2, 2) + + val b1 = Binomial(7, b) + b1.observe(6) + val b2 = Binomial(3, b) + b2.observe(1) + + val algorithm = EMWithImportance(2, 100, b)(universe) + algorithm.start + + val result = b.getLearnedElement + algorithm.kill + result.prob should be(0.6666 +- 0.01) + + + } + } + + "correctly use a uniform prior" in { + val universe = Universe.createNew + val b = BetaParameter(1, 1) + + val b1 = Binomial(7, b) + b1.observe(6) + val b2 = Binomial(3, b) + b2.observe(1) + + val algorithm = EMWithImportance(2, 100, b)(universe) + algorithm.start + + val result = b.getLearnedElement + algorithm.kill + result.prob should be(0.7 +- 0.01) + + + } + + "used to estimate a Dirichlet parameter with two concentration parameters" should + { + + "detect bias after a large enough number of trials" in + { + val universe = Universe.createNew + val b = DirichletParameter(2, 2) + + for (i <- 1 to 7) { + + val f = Select(b, true, false) + f.observe(true) + } + + for (i <- 1 to 3) { + + val f = Select(b, true, false) + f.observe(false) + } + + val algorithm = EMWithImportance(2, 1000, b)(universe) + algorithm.start + + val result = b.getLearnedElement(List(true, false)) + algorithm.kill + result.probs(0) should be(0.6666 +- 0.01) + + } + + "take the prior concentration parameters into account" in + { + val universe = Universe.createNew + + val b = DirichletParameter(3, 7) + + for (i <- 1 to 7) { + + val f = Select(b, true, false) + f.observe(true) + } + + for (i <- 1 to 3) { + + val f = Select(b, true, false) + f.observe(false) + } + + val algorithm = EMWithImportance(2, 1000, b)(universe) + algorithm.start + + val result = b.getLearnedElement(List(true, false)) + algorithm.kill + result.probs(0) should be(0.50 +- 0.01) + + } + + } + + "used to estimate a Dirichlet parameter with three concentration parameters" should + { + + "calculate sufficient statistics in the correct order for long lists of concentration parameters" in + { + val universe = Universe.createNew + val alphas = Seq[Double](0.0476, 0.0476, 0.0476, 0.0476, 0.0476, 0.0476, 0.0476, 0.0476, 0.0476, 0.0476, 0.0476, 0.0476, 0.0476, 0.0476, 0.0476, 0.0476, 0.0476, 0.0476, 0.0476, 0.0476, 0.0476, 0.0476) + val d = DirichletParameter(alphas: _*) + val outcomes = List(2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23) + val outcome = Select(d, outcomes: _*) + val algorithm = EMWithImportance(2, 1000, d) + algorithm.start + + val result = d.getLearnedElement(outcomes) + algorithm.kill + result.probs(0) should be(0.04 +- 0.01) + result.probs(1) should be(0.04 +- 0.01) + result.probs(2) should be(0.04 +- 0.01) + result.probs(3) should be(0.04 +- 0.01) + result.probs(4) should be(0.04 +- 0.01) + result.probs(5) should be(0.04 +- 0.01) + result.probs(6) should be(0.04 +- 0.01) + result.probs(7) should be(0.04 +- 0.01) + result.probs(8) should be(0.04 +- 0.01) + result.probs(9) should be(0.04 +- 0.01) + result.probs(10) should be(0.04 +- 0.01) + result.probs(11) should be(0.04 +- 0.01) + result.probs(12) should be(0.04 +- 0.01) + result.probs(13) should be(0.04 +- 0.01) + result.probs(14) should be(0.04 +- 0.01) + result.probs(15) should be(0.04 +- 0.01) + result.probs(16) should be(0.04 +- 0.01) + result.probs(17) should be(0.04 +- 0.01) + result.probs(18) should be(0.04 +- 0.01) + result.probs(19) should be(0.04 +- 0.01) + result.probs(20) should be(0.04 +- 0.01) + result.probs(21) should be(0.04 +- 0.01) + + } + + "calculate sufficient statistics in the correct order for long lists of concentration parameters, taking into account a condition" in + { + val universe = Universe.createNew + val alphas = Seq[Double](1.0476, 1.0476, 1.0476, 1.0476, 1.0476) + val d = DirichletParameter(alphas: _*) + val outcomes = List(2, 3, 4, 5, 6) + + for (i <- 1 to 10) { + val outcome = Select(d, outcomes: _*) + outcome.addCondition(x => x >= 3 && x <= 6) + } + + val algorithm = EMWithImportance(2, 1000, d) + algorithm.start + val result = d.getLearnedElement(outcomes) + algorithm.kill + result.probs(0) should be(0.0 +- 0.01) + result.probs(1) should be(0.25 +- 0.01) + result.probs(2) should be(0.25 +- 0.01) + result.probs(3) should be(0.25 +- 0.01) + result.probs(4) should be(0.25 +- 0.01) + } + + "detect bias after a large enough number of trials" in + { + val universe = Universe.createNew + val b = DirichletParameter(2, 2, 2) + val outcomes = List(1, 2, 3) + val errorTolerance = 0.01 + for (i <- 1 to 8) { + + val f = Select(b, outcomes: _*) + f.observe(1) + + } + + for (i <- 1 to 6) { + val f = Select(b, outcomes: _*) + f.observe(2) + } + + for (i <- 1 to 2) { + val f = Select(b, outcomes: _*) + f.observe(3) + } + + val algorithm = EMWithImportance(2, 1000, b)(universe) + algorithm.start + + val result = b.getLearnedElement(outcomes) + algorithm.kill + + //9/19 + result.probs(0) should be(0.47 +- errorTolerance) + + //7/19 + result.probs(1) should be(0.36 +- errorTolerance) + //3/19 + result.probs(2) should be(0.15 +- errorTolerance) + + } + + "take the prior concentration parameters into account" in + { + val universe = Universe.createNew + val b = DirichletParameter(2.0, 3.0, 2.0) + val outcomes = List(1, 2, 3) + + for (i <- 1 to 3) { + + val f2 = Select(b, outcomes: _*) + f2.observe(1) + } + + for (i <- 1 to 2) { + val f3 = Select(b, outcomes: _*) + f3.observe(2) + } + + for (i <- 1 to 3) { + + val f1 = Select(b, outcomes: _*) + f1.observe(3) + } + + val algorithm = EMWithImportance(2, 1000, b)(universe) + algorithm.start + + val result = b.getLearnedElement(outcomes) + algorithm.kill + result.probs(0) should be(0.33 +- 0.01) + result.probs(1) should be(0.33 +- 0.01) + result.probs(2) should be(0.33 +- 0.01) + + } + + "correctly use a uniform prior" in + { + val universe = Universe.createNew + val b = DirichletParameter(1.0, 1.0, 1.0) + val outcomes = List(1, 2, 3) + + for (i <- 1 to 3) { + + val f2 = Select(b, outcomes: _*) + f2.observe(1) + } + + for (i <- 1 to 3) { + val f3 = Select(b, outcomes: _*) + f3.observe(2) + } + + for (i <- 1 to 3) { + val f1 = Select(b, outcomes: _*) + f1.observe(3) + } + + val algorithm = EMWithImportance(2, 1000, b)(universe) + algorithm.start + + val result = b.getLearnedElement(outcomes) + algorithm.kill + result.probs(0) should be(0.33 +- 0.01) + result.probs(1) should be(0.33 +- 0.01) + result.probs(2) should be(0.33 +- 0.01) + } + } + + "used to estimate multiple parameters" should + { + + "leave parameters having no observations unchanged" in + { + val universe = Universe.createNew + val d = DirichletParameter(2.0, 4.0, 2.0) + val b = BetaParameter(2.0, 2.0) + val outcomes = List(1, 2, 3) + + for (i <- 1 to 4) { + + val f2 = Select(d, outcomes: _*) + f2.observe(1) + } + + for (i <- 1 to 2) { + val f3 = Select(d, outcomes: _*) + f3.observe(2) + } + + for (i <- 1 to 4) { + val f1 = Select(d, outcomes: _*) + f1.observe(3) + } + + val algorithm = EMWithImportance(2, 1000, d, b)(universe) + algorithm.start + + val result = d.getLearnedElement(outcomes) + algorithm.kill + result.probs(0) should be(0.33 +- 0.01) + result.probs(1) should be(0.33 +- 0.01) + result.probs(2) should be(0.33 +- 0.01) + + val betaResult = b.getLearnedElement + betaResult.prob should be(0.5) + + } + + "correctly estimate all parameters with observations" in + { + val universe = Universe.createNew + val d = DirichletParameter(2.0, 3.0, 2.0) + val b = BetaParameter(3.0, 7.0) + val outcomes = List(1, 2, 3) + + for (i <- 1 to 3) { + + val f2 = Select(d, outcomes: _*) + f2.observe(1) + } + + for (i <- 1 to 2) { + val f3 = Select(d, outcomes: _*) + f3.observe(2) + } + + for (i <- 1 to 3) { + val f1 = Select(d, outcomes: _*) + f1.observe(3) + } + + for (i <- 1 to 7) { + val f = Flip(b) + f.observe(true) + } + + for (i <- 1 to 3) { + val f = Flip(b) + + f.observe(false) + } + + val algorithm = EMWithImportance(2, 1000, b,d)(universe) + algorithm.start + + val result = d.getLearnedElement(outcomes) + + result.probs(0) should be(0.33 +- 0.01) + result.probs(1) should be(0.33 +- 0.01) + result.probs(2) should be(0.33 +- 0.01) + + val betaResult = b.getLearnedElement + betaResult.prob should be(0.5 +- 0.01) + + } + } + + val observationProbability = 0.7 + val trainingSetSize = 100 + val testSetSize = 100 + val minScale = 10 + val maxScale = 10 + val scaleStep = 2 + + abstract class Parameters(val universe: Universe) { + val b1: Element[Double] + val b2: Element[Double] + val b3: Element[Double] + val b4: Element[Double] + val b5: Element[Double] + val b6: Element[Double] + val b7: Element[Double] + val b8: Element[Double] + val b9: Element[Double] + } + + val trueB1 = 0.1 + val trueB2 = 0.2 + val trueB3 = 0.3 + val trueB4 = 0.4 + val trueB5 = 0.5 + val trueB6 = 0.6 + val trueB7 = 0.7 + val trueB8 = 0.8 + val trueB9 = 0.9 + + val trueUniverse = new Universe + + object TrueParameters extends Parameters(trueUniverse) { + val b1 = Constant(trueB1)("b1", universe) + val b2 = Constant(trueB2)("b2", universe) + val b3 = Constant(trueB3)("b3", universe) + val b4 = Constant(trueB4)("b4", universe) + val b5 = Constant(trueB5)("b5", universe) + val b6 = Constant(trueB6)("b6", universe) + val b7 = Constant(trueB7)("b7", universe) + val b8 = Constant(trueB8)("b8", universe) + val b9 = Constant(trueB9)("b9", universe) + } + + class LearnableParameters(universe: Universe) extends Parameters(universe) { + val b1 = BetaParameter(1, 1)("b1", universe) + val b2 = BetaParameter(1, 1)("b2", universe) + val b3 = BetaParameter(1, 1)("b3", universe) + val b4 = BetaParameter(1, 1)("b4", universe) + val b5 = BetaParameter(1, 1)("b5", universe) + val b6 = BetaParameter(1, 1)("b6", universe) + val b7 = BetaParameter(1, 1)("b7", universe) + val b8 = BetaParameter(1, 1)("b8", universe) + val b9 = BetaParameter(1, 1)("b9", universe) + } + + var id = 0 + + class Model(val parameters: Parameters, flipConstructor: (Element[Double], String, Universe) => Flip) { + id += 1 + val universe = parameters.universe + val x = flipConstructor(parameters.b1, "x_" + id, universe) + val f2 = flipConstructor(parameters.b2, "f2_" + id, universe) + val f3 = flipConstructor(parameters.b3, "f3_" + id, universe) + val f4 = flipConstructor(parameters.b4, "f4_" + id, universe) + val f5 = flipConstructor(parameters.b5, "f5_" + id, universe) + val f6 = flipConstructor(parameters.b6, "f6_" + id, universe) + val f7 = flipConstructor(parameters.b7, "f7_" + id, universe) + val f8 = flipConstructor(parameters.b8, "f8_" + id, universe) + val f9 = flipConstructor(parameters.b9, "f9_" + id, universe) + val y = If(x, f2, f3)("y_" + id, universe) + val z = If(x, f4, f5)("z_" + id, universe) + val w = CPD(y, z, (true, true) -> f6, (true, false) -> f7, + (false, true) -> f8, (false, false) -> f9)("w_" + id, universe) + } + + def normalFlipConstructor(parameter: Element[Double], name: String, universe: Universe) = new CompoundFlip(name, parameter, universe) + + def learningFlipConstructor(parameter: Element[Double], name: String, universe: Universe) = { + parameter match { + case p: AtomicBeta => new ParameterizedFlip(name, p, universe) + case _ => throw new IllegalArgumentException("Not a beta parameter") + } + } + object TrueModel extends Model(TrueParameters, normalFlipConstructor) + + case class Datum(x: Boolean, y: Boolean, z: Boolean, w: Boolean) + + def generateDatum(): Datum = { + val model = TrueModel + model.universe.generateAll() + Datum(model.x.value, model.y.value, model.z.value, model.w.value) + } + + def observe(model: Model, datum: Datum) { + if (random.nextDouble() < observationProbability) model.x.observe(datum.x) + if (random.nextDouble() < observationProbability) model.y.observe(datum.y) + if (random.nextDouble() < observationProbability) model.z.observe(datum.z) + if (random.nextDouble() < observationProbability) model.w.observe(datum.w) + } + + var nextSkip = 0 + + def predictionAccuracy(model: Model, datum: Datum): Double = { + model.x.unobserve() + model.y.unobserve() + model.z.unobserve() + model.w.unobserve() + val result = nextSkip match { + case 0 => + model.y.observe(datum.y) + model.z.observe(datum.z) + model.w.observe(datum.w) + val alg = VariableElimination(model.x)(model.universe) + alg.start() + alg.probability(model.x, datum.x) + case 1 => + model.x.observe(datum.x) + model.z.observe(datum.z) + model.w.observe(datum.w) + val alg = VariableElimination(model.y)(model.universe) + alg.start() + alg.probability(model.y, datum.y) + case 2 => + model.x.observe(datum.x) + model.y.observe(datum.y) + model.w.observe(datum.w) + val alg = VariableElimination(model.z)(model.universe) + alg.start() + alg.probability(model.z, datum.z) + case 3 => + model.x.observe(datum.x) + model.y.observe(datum.y) + model.z.observe(datum.z) + val alg = VariableElimination(model.w)(model.universe) + alg.start() + alg.probability(model.w, datum.w) + } + nextSkip = (nextSkip + 1) % 4 + result + } + + def parameterError(model: Model): Double = { + val parameters = model.parameters + (abs(parameters.b1.value - trueB1) + abs(parameters.b2.value - trueB2) + abs(parameters.b3.value - trueB3) + + abs(parameters.b4.value - trueB4) + abs(parameters.b5.value - trueB5) + abs(parameters.b6.value - trueB6) + + abs(parameters.b7.value - trueB7) + abs(parameters.b8.value - trueB8) + abs(parameters.b9.value - trueB9)) / 9.0 + } + + def assessModel(model: Model, testSet: Seq[Datum]): (Double, Double) = { + val paramErr = parameterError(model) + nextSkip = 0 + var totalPredictionAccuracy = 0.0 + for (datum <- testSet) (totalPredictionAccuracy += predictionAccuracy(model, datum)) + val predAcc = totalPredictionAccuracy / testSet.length + (paramErr, predAcc) + } + + def train(trainingSet: List[Datum], parameters: Parameters, algorithmCreator: Parameters => Algorithm, valueGetter: (Algorithm, Element[Double]) => Double, + flipConstructor: (Element[Double], String, Universe) => Flip): (Model, Double) = { + for (datum <- trainingSet) observe(new Model(parameters, flipConstructor), datum) + + val time0 = System.currentTimeMillis() + val algorithm = algorithmCreator(parameters) + algorithm.start() + + val resultUniverse = new Universe + def extractParameter(parameter: Element[Double], name: String) = + { + parameter match + { + case b: AtomicBeta => + { + + Constant(valueGetter(algorithm, parameter))(name, resultUniverse) + } + case _ => Constant(valueGetter(algorithm, parameter))(name, resultUniverse) + } + + } + + val learnedParameters = new Parameters(resultUniverse) { + val b1 = extractParameter(parameters.b1, "b1"); b1.generate() + val b2 = extractParameter(parameters.b2, "b2"); b2.generate() + val b3 = extractParameter(parameters.b3, "b3"); b3.generate() + val b4 = extractParameter(parameters.b4, "b4"); b4.generate() + val b5 = extractParameter(parameters.b5, "b5"); b5.generate() + val b6 = extractParameter(parameters.b6, "b6"); b6.generate() + val b7 = extractParameter(parameters.b7, "b7"); b7.generate() + val b8 = extractParameter(parameters.b8, "b8"); b8.generate() + val b9 = extractParameter(parameters.b9, "b9"); b9.generate() + } + + algorithm.kill() + val time1 = System.currentTimeMillis() + val totalTime = (time1 - time0) / 1000.0 + println("Training time: " + totalTime + " seconds") + (new Model(learnedParameters, normalFlipConstructor), totalTime) + } + + "derive parameters within a reasonable accuracy for random data" in + { + + val numEMIterations = 5 + val testSet = List.fill(testSetSize)(generateDatum()) + val trainingSet = List.fill(trainingSetSize)(generateDatum()) + + def learner(parameters: Parameters): Algorithm = { + parameters match { + case ps: LearnableParameters => EMWithImportance(numEMIterations, 1000, ps.b1, ps.b2, ps.b3, ps.b4, ps.b5, ps.b6, ps.b7, ps.b8, ps.b9)(parameters.universe) + case _ => throw new IllegalArgumentException("Not learnable parameters") + } + } + + def parameterGetter(algorithm: Algorithm, parameter: Element[Double]): Double = { + parameter match { + case p: Parameter[Double] => { + p.MAPValue + } + case _ => throw new IllegalArgumentException("Not a learnable parameter") + } + } + val (trueParamErr, truePredAcc) = assessModel(TrueModel, testSet) + val (learnedModel, learningTime) = train(trainingSet, new LearnableParameters(new Universe), learner, parameterGetter, learningFlipConstructor) + val (learnedParamErr, learnedPredAcc) = assessModel(learnedModel, testSet) + + println(learnedParamErr) + println(learnedPredAcc) + learnedParamErr should be(0.00 +- 0.12) + learnedPredAcc should be(truePredAcc +- 0.12) + + } + + } + +} \ No newline at end of file diff --git a/Figaro/src/test/scala/com/cra/figaro/test/algorithm/learning/EMWithMHTest.scala b/Figaro/src/test/scala/com/cra/figaro/test/algorithm/learning/EMWithMHTest.scala new file mode 100644 index 00000000..653009dd --- /dev/null +++ b/Figaro/src/test/scala/com/cra/figaro/test/algorithm/learning/EMWithMHTest.scala @@ -0,0 +1,686 @@ +/* + * EMWithMH.scala + * Tests for the EM algorithm + * + * Created By: Michael Howard (mhoward@cra.com) + * Creation Date: Jun 6, 2013 + * + * Copyright 2013 Avrom J. Pfeffer and Charles River Analytics, Inc. + * See http://www.cra.com or email figaro@cra.com for information. + * + * See http://www.github.com/p2t2/figaro for a copy of the software license. + */ + +package com.cra.figaro.test.algorithm.learning + +import org.scalatest.Matchers +import org.scalatest.{ PrivateMethodTester, WordSpec } +import com.cra.figaro.algorithm._ +import com.cra.figaro.algorithm.factored._ +import com.cra.figaro.algorithm.sampling._ +import com.cra.figaro.algorithm.learning._ +import com.cra.figaro.library.atomic.continuous._ +import com.cra.figaro.library.atomic.discrete.Binomial +import com.cra.figaro.library.compound._ +import com.cra.figaro.language._ +import com.cra.figaro.language.Universe._ +import com.cra.figaro.util._ +import com.cra.figaro.util.random +import scala.math.abs +import java.io._ + +class EMWithMHTest extends WordSpec with PrivateMethodTester with Matchers { + + "Expectation Maximization with MetropolisHastings" when + { + + "used to estimate a Beta parameter" should + { + + "detect bias after a large enough number of trials" in + { + val universe = Universe.createNew + val b = BetaParameter(2, 2) + + for (i <- 1 to 7) { + + val f = Flip(b) + f.observe(true) + } + + for (i <- 1 to 3) { + + val f = Flip(b) + f.observe(false) + } + + val algorithm = EMWithMH(2, 10000, b)(universe) + algorithm.start + + val result = b.getLearnedElement + algorithm.kill + result.prob should be(0.6666 +- 0.01) + + } + + "take the prior concentration parameters into account" in + { + val universe = Universe.createNew + val b = BetaParameter(3.0, 7.0) + + for (i <- 1 to 7) { + + val f = Flip(b) + f.observe(true) + } + + for (i <- 1 to 3) { + + val f = Flip(b) + f.observe(false) + } + + val algorithm = EMWithMH(2, 10000, b)(universe) + algorithm.start + + val result = b.getLearnedElement + algorithm.kill + result.prob should be(0.50 +- 0.01) + + } + + "learn the bias from observations of binomial elements" in { + val universe = Universe.createNew + val b = BetaParameter(2, 2) + + val b1 = Binomial(7, b) + b1.observe(6) + val b2 = Binomial(3, b) + b2.observe(1) + + val algorithm = EMWithMH(2, 10000, b)(universe) + algorithm.start + + val result = b.getLearnedElement + algorithm.kill + result.prob should be(0.6666 +- 0.01) + + + } + } + + "correctly use a uniform prior" in { + val universe = Universe.createNew + val b = BetaParameter(1, 1) + + val b1 = Binomial(7, b) + b1.observe(6) + val b2 = Binomial(3, b) + b2.observe(1) + + val algorithm = EMWithMH(2, 10000, b)(universe) + algorithm.start + + val result = b.getLearnedElement + algorithm.kill + result.prob should be(0.7 +- 0.01) + + + } + + "used to estimate a Dirichlet parameter with two concentration parameters" should + { + + "detect bias after a large enough number of trials" in + { + val universe = Universe.createNew + val b = DirichletParameter(2, 2) + + for (i <- 1 to 7) { + + val f = Select(b, true, false) + f.observe(true) + } + + for (i <- 1 to 3) { + + val f = Select(b, true, false) + f.observe(false) + } + + val algorithm = EMWithMH(2, 100000, b)(universe) + algorithm.start + + val result = b.getLearnedElement(List(true, false)) + algorithm.kill + result.probs(0) should be(0.6666 +- 0.01) + + } + + "take the prior concentration parameters into account" in + { + val universe = Universe.createNew + + val b = DirichletParameter(3, 7) + + for (i <- 1 to 7) { + + val f = Select(b, true, false) + f.observe(true) + } + + for (i <- 1 to 3) { + + val f = Select(b, true, false) + f.observe(false) + } + + val algorithm = EMWithMH(2, 100000, b)(universe) + algorithm.start + + val result = b.getLearnedElement(List(true, false)) + algorithm.kill + result.probs(0) should be(0.50 +- 0.01) + + } + + } + + "used to estimate a Dirichlet parameter with three concentration parameters" should + { + + "calculate sufficient statistics in the correct order for long lists of concentration parameters" in + { + val universe = Universe.createNew + val alphas = Seq[Double](0.0476, 0.0476, 0.0476, 0.0476, 0.0476, 0.0476, 0.0476, 0.0476, 0.0476, 0.0476, 0.0476, 0.0476, 0.0476, 0.0476, 0.0476, 0.0476, 0.0476, 0.0476, 0.0476, 0.0476, 0.0476, 0.0476) + val d = DirichletParameter(alphas: _*) + val outcomes = List(2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23) + val outcome = Select(d, outcomes: _*) + val algorithm = EMWithMH(2, 100000, d) + algorithm.start + + val result = d.getLearnedElement(outcomes) + algorithm.kill + result.probs(0) should be(0.04 +- 0.01) + result.probs(1) should be(0.04 +- 0.01) + result.probs(2) should be(0.04 +- 0.01) + result.probs(3) should be(0.04 +- 0.01) + result.probs(4) should be(0.04 +- 0.01) + result.probs(5) should be(0.04 +- 0.01) + result.probs(6) should be(0.04 +- 0.01) + result.probs(7) should be(0.04 +- 0.01) + result.probs(8) should be(0.04 +- 0.01) + result.probs(9) should be(0.04 +- 0.01) + result.probs(10) should be(0.04 +- 0.01) + result.probs(11) should be(0.04 +- 0.01) + result.probs(12) should be(0.04 +- 0.01) + result.probs(13) should be(0.04 +- 0.01) + result.probs(14) should be(0.04 +- 0.01) + result.probs(15) should be(0.04 +- 0.01) + result.probs(16) should be(0.04 +- 0.01) + result.probs(17) should be(0.04 +- 0.01) + result.probs(18) should be(0.04 +- 0.01) + result.probs(19) should be(0.04 +- 0.01) + result.probs(20) should be(0.04 +- 0.01) + result.probs(21) should be(0.04 +- 0.01) + + } + + "calculate sufficient statistics in the correct order for long lists of concentration parameters, taking into account a condition" in + { + val universe = Universe.createNew + val alphas = Seq[Double](1.0476, 1.0476, 1.0476, 1.0476, 1.0476) + val d = DirichletParameter(alphas: _*) + val outcomes = List(2, 3, 4, 5, 6) + + for (i <- 1 to 10) { + val outcome = Select(d, outcomes: _*) + outcome.addCondition(x => x >= 3 && x <= 6) + } + + val algorithm = EMWithMH(2, 100000, d) + algorithm.start + val result = d.getLearnedElement(outcomes) + algorithm.kill + result.probs(0) should be(0.0 +- 0.01) + result.probs(1) should be(0.25 +- 0.01) + result.probs(2) should be(0.25 +- 0.01) + result.probs(3) should be(0.25 +- 0.01) + result.probs(4) should be(0.25 +- 0.01) + } + + "detect bias after a large enough number of trials" in + { + val universe = Universe.createNew + val b = DirichletParameter(2, 2, 2) + val outcomes = List(1, 2, 3) + val errorTolerance = 0.01 + for (i <- 1 to 8) { + + val f = Select(b, outcomes: _*) + f.observe(1) + } + + for (i <- 1 to 6) { + + val f = Select(b, outcomes: _*) + f.observe(2) + } + + for (i <- 1 to 2) { + + val f = Select(b, outcomes: _*) + f.observe(3) + } + + val algorithm = EMWithMH(2, 100000, b)(universe) + algorithm.start + + val result = b.getLearnedElement(outcomes) + algorithm.kill + + //9/19 + result.probs(0) should be(0.47 +- errorTolerance) + //7/19 + result.probs(1) should be(0.36 +- errorTolerance) + //3/19 + result.probs(2) should be(0.15 +- errorTolerance) + + } + + "take the prior concentration parameters into account" in + { + val universe = Universe.createNew + val b = DirichletParameter(2.0, 3.0, 2.0) + val outcomes = List(1, 2, 3) + + for (i <- 1 to 3) { + + val f2 = Select(b, outcomes: _*) + f2.observe(1) + } + + for (i <- 1 to 2) { + val f3 = Select(b, outcomes: _*) + f3.observe(2) + } + + for (i <- 1 to 3) { + + val f1 = Select(b, outcomes: _*) + f1.observe(3) + } + + val algorithm = EMWithMH(2, 100000, b)(universe) + algorithm.start + + val result = b.getLearnedElement(outcomes) + algorithm.kill + result.probs(0) should be(0.33 +- 0.01) + result.probs(1) should be(0.33 +- 0.01) + result.probs(2) should be(0.33 +- 0.01) + + } + + "correctly use a uniform prior" in + { + val universe = Universe.createNew + val b = DirichletParameter(1.0, 1.0, 1.0) + val outcomes = List(1, 2, 3) + + for (i <- 1 to 3) { + + val f2 = Select(b, outcomes: _*) + f2.observe(1) + } + + for (i <- 1 to 3) { + val f3 = Select(b, outcomes: _*) + f3.observe(2) + } + + for (i <- 1 to 3) { + + val f1 = Select(b, outcomes: _*) + f1.observe(3) + } + + val algorithm = EMWithMH(2, 100000, b)(universe) + algorithm.start + + val result = b.getLearnedElement(outcomes) + algorithm.kill + result.probs(0) should be(0.33 +- 0.01) + result.probs(1) should be(0.33 +- 0.01) + result.probs(2) should be(0.33 +- 0.01) + } + + } + + "used to estimate multiple parameters" should + { + + "leave parameters having no observations unchanged" in + { + val universe = Universe.createNew + val d = DirichletParameter(2.0, 4.0, 2.0) + val b = BetaParameter(2.0, 2.0) + val outcomes = List(1, 2, 3) + + for (i <- 1 to 4) { + + val f2 = Select(d, outcomes: _*) + f2.observe(1) + } + + for (i <- 1 to 2) { + val f3 = Select(d, outcomes: _*) + f3.observe(2) + } + + for (i <- 1 to 4) { + + val f1 = Select(d, outcomes: _*) + f1.observe(3) + } + + val algorithm = EMWithMH(2, 100000, d, b)(universe) + algorithm.start + + val result = d.getLearnedElement(outcomes) + algorithm.kill + result.probs(0) should be(0.33 +- 0.01) + result.probs(1) should be(0.33 +- 0.01) + result.probs(2) should be(0.33 +- 0.01) + + val betaResult = b.getLearnedElement + betaResult.prob should be(0.5) + + } + + "correctly estimate all parameters with observations" in + { + val universe = Universe.createNew + val d = DirichletParameter(2.0, 3.0, 2.0) + val b = BetaParameter(3.0, 7.0) + val outcomes = List(1, 2, 3) + + for (i <- 1 to 3) { + + val f2 = Select(d, outcomes: _*) + f2.observe(1) + } + + for (i <- 1 to 2) { + val f3 = Select(d, outcomes: _*) + f3.observe(2) + } + + for (i <- 1 to 3) { + + val f1 = Select(d, outcomes: _*) + f1.observe(3) + } + + for (i <- 1 to 7) { + + val f = Flip(b) + f.observe(true) + } + + for (i <- 1 to 3) { + + val f = Flip(b) + f.observe(false) + } + + val algorithm = EMWithMH(2, 100000, b,d)(universe) + algorithm.start + + val result = d.getLearnedElement(outcomes) + + result.probs(0) should be(0.33 +- 0.01) + result.probs(1) should be(0.33 +- 0.01) + result.probs(2) should be(0.33 +- 0.01) + + val betaResult = b.getLearnedElement + betaResult.prob should be(0.5 +- 0.01) + + } + } + + val observationProbability = 0.7 + val trainingSetSize = 20 + val testSetSize = 100 + val minScale = 10 + val maxScale = 10 + val scaleStep = 2 + + abstract class Parameters(val universe: Universe) { + val b1: Element[Double] + val b2: Element[Double] + val b3: Element[Double] + val b4: Element[Double] + val b5: Element[Double] + val b6: Element[Double] + val b7: Element[Double] + val b8: Element[Double] + val b9: Element[Double] + } + + val trueB1 = 0.1 + val trueB2 = 0.2 + val trueB3 = 0.3 + val trueB4 = 0.4 + val trueB5 = 0.5 + val trueB6 = 0.6 + val trueB7 = 0.7 + val trueB8 = 0.8 + val trueB9 = 0.9 + val trueUniverse = new Universe + + object TrueParameters extends Parameters(trueUniverse) { + val b1 = Constant(trueB1)("b1", universe) + val b2 = Constant(trueB2)("b2", universe) + val b3 = Constant(trueB3)("b3", universe) + val b4 = Constant(trueB4)("b4", universe) + val b5 = Constant(trueB5)("b5", universe) + val b6 = Constant(trueB6)("b6", universe) + val b7 = Constant(trueB7)("b7", universe) + val b8 = Constant(trueB8)("b8", universe) + val b9 = Constant(trueB9)("b9", universe) + } + + class LearnableParameters(universe: Universe) extends Parameters(universe) { + val b1 = BetaParameter(1, 1)("b1", universe) + val b2 = BetaParameter(1, 1)("b2", universe) + val b3 = BetaParameter(1, 1)("b3", universe) + val b4 = BetaParameter(1, 1)("b4", universe) + val b5 = BetaParameter(1, 1)("b5", universe) + val b6 = BetaParameter(1, 1)("b6", universe) + val b7 = BetaParameter(1, 1)("b7", universe) + val b8 = BetaParameter(1, 1)("b8", universe) + val b9 = BetaParameter(1, 1)("b9", universe) + } + + var id = 0 + + class Model(val parameters: Parameters, flipConstructor: (Element[Double], String, Universe) => Flip) { + id += 1 + val universe = parameters.universe + val x = flipConstructor(parameters.b1, "x_" + id, universe) + val f2 = flipConstructor(parameters.b2, "f2_" + id, universe) + val f3 = flipConstructor(parameters.b3, "f3_" + id, universe) + val f4 = flipConstructor(parameters.b4, "f4_" + id, universe) + val f5 = flipConstructor(parameters.b5, "f5_" + id, universe) + val f6 = flipConstructor(parameters.b6, "f6_" + id, universe) + val f7 = flipConstructor(parameters.b7, "f7_" + id, universe) + val f8 = flipConstructor(parameters.b8, "f8_" + id, universe) + val f9 = flipConstructor(parameters.b9, "f9_" + id, universe) + val y = If(x, f2, f3)("y_" + id, universe) + val z = If(x, f4, f5)("z_" + id, universe) + val w = CPD(y, z, (true, true) -> f6, (true, false) -> f7, + (false, true) -> f8, (false, false) -> f9)("w_" + id, universe) + } + + def normalFlipConstructor(parameter: Element[Double], name: String, universe: Universe) = new CompoundFlip(name, parameter, universe) + + def learningFlipConstructor(parameter: Element[Double], name: String, universe: Universe) = { + parameter match { + case p: AtomicBeta => new ParameterizedFlip(name, p, universe) + case _ => throw new IllegalArgumentException("Not a beta parameter") + } + } + + object TrueModel extends Model(TrueParameters, normalFlipConstructor) + + case class Datum(x: Boolean, y: Boolean, z: Boolean, w: Boolean) + + def generateDatum(): Datum = { + val model = TrueModel + model.universe.generateAll() + Datum(model.x.value, model.y.value, model.z.value, model.w.value) + } + + def observe(model: Model, datum: Datum) { + if (random.nextDouble() < observationProbability) model.x.observe(datum.x) + if (random.nextDouble() < observationProbability) model.y.observe(datum.y) + if (random.nextDouble() < observationProbability) model.z.observe(datum.z) + if (random.nextDouble() < observationProbability) model.w.observe(datum.w) + } + + var nextSkip = 0 + + def predictionAccuracy(model: Model, datum: Datum): Double = { + model.x.unobserve() + model.y.unobserve() + model.z.unobserve() + model.w.unobserve() + val result = nextSkip match { + case 0 => + model.y.observe(datum.y) + model.z.observe(datum.z) + model.w.observe(datum.w) + val alg = VariableElimination(model.x)(model.universe) + alg.start() + alg.probability(model.x, datum.x) + case 1 => + model.x.observe(datum.x) + model.z.observe(datum.z) + model.w.observe(datum.w) + val alg = VariableElimination(model.y)(model.universe) + alg.start() + alg.probability(model.y, datum.y) + case 2 => + model.x.observe(datum.x) + model.y.observe(datum.y) + model.w.observe(datum.w) + val alg = VariableElimination(model.z)(model.universe) + alg.start() + alg.probability(model.z, datum.z) + case 3 => + model.x.observe(datum.x) + model.y.observe(datum.y) + model.z.observe(datum.z) + val alg = VariableElimination(model.w)(model.universe) + alg.start() + alg.probability(model.w, datum.w) + } + nextSkip = (nextSkip + 1) % 4 + result + } + + def parameterError(model: Model): Double = { + val parameters = model.parameters + (abs(parameters.b1.value - trueB1) + abs(parameters.b2.value - trueB2) + abs(parameters.b3.value - trueB3) + + abs(parameters.b4.value - trueB4) + abs(parameters.b5.value - trueB5) + abs(parameters.b6.value - trueB6) + + abs(parameters.b7.value - trueB7) + abs(parameters.b8.value - trueB8) + abs(parameters.b9.value - trueB9)) / 9.0 + } + + def assessModel(model: Model, testSet: Seq[Datum]): (Double, Double) = { + val paramErr = parameterError(model) + nextSkip = 0 + var totalPredictionAccuracy = 0.0 + for (datum <- testSet) (totalPredictionAccuracy += predictionAccuracy(model, datum)) + val predAcc = totalPredictionAccuracy / testSet.length + (paramErr, predAcc) + } + + def train(trainingSet: List[Datum], parameters: Parameters, algorithmCreator: Parameters => Algorithm, valueGetter: (Algorithm, Element[Double]) => Double, + flipConstructor: (Element[Double], String, Universe) => Flip): (Model, Double) = { + for (datum <- trainingSet) observe(new Model(parameters, flipConstructor), datum) + + val time0 = System.currentTimeMillis() + val algorithm = algorithmCreator(parameters) + algorithm.start() + + val resultUniverse = new Universe + def extractParameter(parameter: Element[Double], name: String) = + { + parameter match + { + case b: AtomicBeta => + { + + Constant(valueGetter(algorithm, parameter))(name, resultUniverse) + } + case _ => Constant(valueGetter(algorithm, parameter))(name, resultUniverse) + } + + } + val learnedParameters = new Parameters(resultUniverse) { + val b1 = extractParameter(parameters.b1, "b1"); b1.generate() + val b2 = extractParameter(parameters.b2, "b2"); b2.generate() + val b3 = extractParameter(parameters.b3, "b3"); b3.generate() + val b4 = extractParameter(parameters.b4, "b4"); b4.generate() + val b5 = extractParameter(parameters.b5, "b5"); b5.generate() + val b6 = extractParameter(parameters.b6, "b6"); b6.generate() + val b7 = extractParameter(parameters.b7, "b7"); b7.generate() + val b8 = extractParameter(parameters.b8, "b8"); b8.generate() + val b9 = extractParameter(parameters.b9, "b9"); b9.generate() + } + + algorithm.kill() + val time1 = System.currentTimeMillis() + val totalTime = (time1 - time0) / 1000.0 + println("Training time: " + totalTime + " seconds") + (new Model(learnedParameters, normalFlipConstructor), totalTime) + } + + "derive parameters within a reasonable accuracy for random data" in + { + + val numEMIterations = 5 + val testSet = List.fill(testSetSize)(generateDatum()) + val trainingSet = List.fill(trainingSetSize)(generateDatum()) + + def learner(parameters: Parameters): Algorithm = { + parameters match { + case ps: LearnableParameters => + EMWithMH(numEMIterations, 50000, ps.b1, ps.b2, ps.b3, ps.b4, ps.b5, ps.b6, ps.b7, ps.b8, ps.b9)(parameters.universe) + case _ => throw new IllegalArgumentException("Not learnable parameters") + } + } + + def parameterGetter(algorithm: Algorithm, parameter: Element[Double]): Double = { + parameter match { + case p: Parameter[Double] => { + p.MAPValue + } + case _ => throw new IllegalArgumentException("Not a learnable parameter") + } + } + val (trueParamErr, truePredAcc) = assessModel(TrueModel, testSet) + val (learnedModel, learningTime) = train(trainingSet, new LearnableParameters(new Universe), learner, parameterGetter, learningFlipConstructor) + val (learnedParamErr, learnedPredAcc) = assessModel(learnedModel, testSet) + + println(learnedParamErr) + println(learnedPredAcc) + learnedParamErr should be(0.00 +- 0.18) + learnedPredAcc should be(truePredAcc +- 0.18) + + } + + } + +} \ No newline at end of file diff --git a/Figaro/src/test/scala/com/cra/figaro/test/algorithm/learning/ExpectationMaximizationTest.scala b/Figaro/src/test/scala/com/cra/figaro/test/algorithm/learning/ExpectationMaximizationTest.scala index 3185ecc1..ce8d94fb 100644 --- a/Figaro/src/test/scala/com/cra/figaro/test/algorithm/learning/ExpectationMaximizationTest.scala +++ b/Figaro/src/test/scala/com/cra/figaro/test/algorithm/learning/ExpectationMaximizationTest.scala @@ -20,6 +20,7 @@ import com.cra.figaro.algorithm.factored._ import com.cra.figaro.algorithm.sampling._ import com.cra.figaro.algorithm.learning._ import com.cra.figaro.library.atomic.continuous._ +import com.cra.figaro.library.atomic.discrete.Binomial import com.cra.figaro.library.compound._ import com.cra.figaro.language._ import com.cra.figaro.language.Universe._ @@ -88,6 +89,44 @@ class ExpectationMaximizationTest extends WordSpec with PrivateMethodTester with } + "learn the bias from observations of binomial elements" in { + val universe = Universe.createNew + val b = BetaParameter(2, 2) + + val b1 = Binomial(7, b) + b1.observe(6) + val b2 = Binomial(3, b) + b2.observe(1) + + val algorithm = ExpectationMaximization(15, b)(universe) + algorithm.start + + val result = b.getLearnedElement + algorithm.kill + result.prob should be(0.6666 +- 0.01) + + + } + + "correctly use a uniform prior" in { + val universe = Universe.createNew + val b = BetaParameter(1, 1) + + val b1 = Binomial(7, b) + b1.observe(6) + val b2 = Binomial(3, b) + b2.observe(1) + + val algorithm = ExpectationMaximization(15, b)(universe) + algorithm.start + + val result = b.getLearnedElement + algorithm.kill + result.prob should be(0.7 +- 0.01) + + + } + } "used to estimate a Dirichlet parameter with two concentration parameters" should @@ -110,7 +149,7 @@ class ExpectationMaximizationTest extends WordSpec with PrivateMethodTester with f.observe(false) } - val algorithm = ExpectationMaximization(10000, b)(universe) + val algorithm = ExpectationMaximization(10, b)(universe) algorithm.start val result = b.getLearnedElement(List(true, false)) @@ -200,7 +239,7 @@ class ExpectationMaximizationTest extends WordSpec with PrivateMethodTester with outcome.addCondition(x => x >= 3 && x <= 6) } - val algorithm = ExpectationMaximization(1000, d) + val algorithm = ExpectationMaximization(10, d) algorithm.start val result = d.getLearnedElement(outcomes) algorithm.kill @@ -253,7 +292,7 @@ class ExpectationMaximizationTest extends WordSpec with PrivateMethodTester with f.observe(3) } - val algorithm = ExpectationMaximization(1000, b)(universe) + val algorithm = ExpectationMaximization(10, b)(universe) algorithm.start val result = b.getLearnedElement(outcomes) @@ -270,10 +309,10 @@ class ExpectationMaximizationTest extends WordSpec with PrivateMethodTester with "take the prior concentration parameters into account" in { val universe = Universe.createNew - val b = DirichletParameter(1.0, 3.0, 1.0) + val b = DirichletParameter(2.0, 3.0, 2.0) val outcomes = List(1, 2, 3) - for (i <- 1 to 4) { + for (i <- 1 to 3) { val f2 = Select(b, outcomes: _*) f2.observe(1) @@ -284,7 +323,7 @@ class ExpectationMaximizationTest extends WordSpec with PrivateMethodTester with f3.observe(2) } - for (i <- 1 to 4) { + for (i <- 1 to 3) { val f1 = Select(b, outcomes: _*) f1.observe(3) @@ -301,6 +340,40 @@ class ExpectationMaximizationTest extends WordSpec with PrivateMethodTester with } + "correctly use a uniform prior" in + { + val universe = Universe.createNew + val b = DirichletParameter(1.0, 1.0, 1.0) + val outcomes = List(1, 2, 3) + + for (i <- 1 to 3) { + + val f2 = Select(b, outcomes: _*) + f2.observe(1) + } + + for (i <- 1 to 3) { + val f3 = Select(b, outcomes: _*) + f3.observe(2) + } + + for (i <- 1 to 3) { + + val f1 = Select(b, outcomes: _*) + f1.observe(3) + } + + val algorithm = ExpectationMaximization(3, b)(universe) + algorithm.start + + val result = b.getLearnedElement(outcomes) + algorithm.kill + result.probs(0) should be(0.33 +- 0.01) + result.probs(1) should be(0.33 +- 0.01) + result.probs(2) should be(0.33 +- 0.01) + } + } + "used to estimate multiple parameters" should { @@ -328,7 +401,7 @@ class ExpectationMaximizationTest extends WordSpec with PrivateMethodTester with f1.observe(3) } - val algorithm = ExpectationMaximization(100, d, b)(universe) + val algorithm = ExpectationMaximization(10, d, b)(universe) algorithm.start val result = d.getLearnedElement(outcomes) @@ -345,11 +418,11 @@ class ExpectationMaximizationTest extends WordSpec with PrivateMethodTester with "correctly estimate all parameters with observations" in { val universe = Universe.createNew - val d = DirichletParameter(1.0, 3.0, 1.0) + val d = DirichletParameter(2.0, 3.0, 2.0) val b = BetaParameter(3.0, 7.0) val outcomes = List(1, 2, 3) - for (i <- 1 to 4) { + for (i <- 1 to 3) { val f2 = Select(d, outcomes: _*) f2.observe(1) @@ -360,7 +433,7 @@ class ExpectationMaximizationTest extends WordSpec with PrivateMethodTester with f3.observe(2) } - for (i <- 1 to 4) { + for (i <- 1 to 3) { val f1 = Select(d, outcomes: _*) f1.observe(3) @@ -392,7 +465,6 @@ class ExpectationMaximizationTest extends WordSpec with PrivateMethodTester with } } - } val observationProbability = 0.7 val trainingSetSize = 100 @@ -610,7 +682,7 @@ class ExpectationMaximizationTest extends WordSpec with PrivateMethodTester with def parameterGetter(algorithm: Algorithm, parameter: Element[Double]): Double = { parameter match { case p: Parameter[Double] => { - p.expectedValue + p.MAPValue } case _ => throw new IllegalArgumentException("Not a learnable parameter") } @@ -622,8 +694,8 @@ class ExpectationMaximizationTest extends WordSpec with PrivateMethodTester with println(learnedParamErr) println(learnedPredAcc) - learnedParamErr should be(0.00 +- 0.12) - learnedPredAcc should be(truePredAcc +- 0.12) + learnedParamErr should be(0.00 +- 0.15) + learnedPredAcc should be(truePredAcc +- 0.15) } diff --git a/Figaro/src/test/scala/com/cra/figaro/test/algorithm/sampling/ImportanceTest.scala b/Figaro/src/test/scala/com/cra/figaro/test/algorithm/sampling/ImportanceTest.scala index e8f9e9be..76d81259 100644 --- a/Figaro/src/test/scala/com/cra/figaro/test/algorithm/sampling/ImportanceTest.scala +++ b/Figaro/src/test/scala/com/cra/figaro/test/algorithm/sampling/ImportanceTest.scala @@ -231,7 +231,7 @@ class ImportanceTest extends WordSpec with Matchers with PrivateMethodTester { "with an observation on a compound flip, terminate quickly and produce the correct result" in { // Tests the likelihood weighting implementation for compound flip Universe.createNew() - val b = Beta(2.0, 5.0) + val b = Uniform(0.0, 1.0) val f1 = Flip(b) val f2 = Flip(b) val f3 = Flip(b) @@ -274,12 +274,13 @@ class ImportanceTest extends WordSpec with Matchers with PrivateMethodTester { f20.observe(false) val alg = Importance(b) alg.start() - Thread.sleep(200) + Thread.sleep(100) val time0 = System.currentTimeMillis() alg.stop() - // Result is beta(2 + 16,5 + 4) - // Expectation is (alpha) / (alpha + beta) = 18/27 - alg.expectation(b, (d: Double) => d) should be ((18.0/27.0) +- 0.02) + // Uniform(0,1) is beta(1,1) + // Result is beta(1 + 16,1 + 4) + // Expectation is (alpha) / (alpha + beta) = 17/22 + alg.expectation(b, (d: Double) => d) should be ((17.0/22.0) +- 0.02) val time1 = System.currentTimeMillis() // If likelihood weighting is working, stopping and querying the algorithm should be almost instantaneous // If likelihood weighting is not working, stopping and querying the algorithm requires waiting for a non-rejected sample @@ -332,7 +333,7 @@ class ImportanceTest extends WordSpec with Matchers with PrivateMethodTester { f20.observe(false) val alg = Importance(b) alg.start() - Thread.sleep(200) + Thread.sleep(100) val time0 = System.currentTimeMillis() alg.stop() // Result is beta(2 + 16,5 + 4) @@ -344,7 +345,7 @@ class ImportanceTest extends WordSpec with Matchers with PrivateMethodTester { (time1 - time0) should be <= (100L) } - "with an observation on a chain, terminate quickly and produce the correct result" in { + "with an observation on a parameterized binomial, terminate quickly and produce the correct result" in { // Tests the likelihood weighting implementation for chain Universe.createNew() val beta = Beta(2.0, 5.0) @@ -352,7 +353,7 @@ class ImportanceTest extends WordSpec with Matchers with PrivateMethodTester { bin.observe(1600) val alg = Importance(beta) alg.start() - Thread.sleep(2000) + Thread.sleep(1000) val time0 = System.currentTimeMillis() alg.stop() // Result is beta(2 + 1600,5 + 400) @@ -364,6 +365,27 @@ class ImportanceTest extends WordSpec with Matchers with PrivateMethodTester { (time1 - time0) should be <= (100L) } + "with an observation on a chain, terminate quickly and produce the correct result" in { + // Tests the likelihood weighting implementation for chain + Universe.createNew() + val beta = Uniform(0.0, 1.0) + val bin = Binomial(2000, beta) + bin.observe(1600) + val alg = Importance(beta) + alg.start() + Thread.sleep(1000) + val time0 = System.currentTimeMillis() + alg.stop() + // uniform(0,1) is beta(1,1) + // Result is beta(1 + 1600,1 + 400) + // Expectation is (alpha) / (alpha + beta) = 1601/2003 + alg.expectation(beta, (d: Double) => d) should be ((1601.0/2003.0) +- 0.02) + val time1 = System.currentTimeMillis() + // If likelihood weighting is working, stopping and querying the algorithm should be almost instantaneous + // If likelihood weighting is not working, stopping and querying the algorithm requires waiting for a non-rejected sample + (time1 - time0) should be <= (100L) + } + "with an observation on a dist, terminate quickly and produce the correct result" in { // Tests the likelihood weighting implementation for dist Universe.createNew() @@ -372,7 +394,7 @@ class ImportanceTest extends WordSpec with Matchers with PrivateMethodTester { dist.observe(1600) // forces it to choose bin, and observation should propagate to it val alg = Importance(beta) alg.start() - Thread.sleep(2000) + Thread.sleep(1000) val time0 = System.currentTimeMillis() alg.stop() // Result is beta(2 + 1600,5 + 400) @@ -421,7 +443,7 @@ class ImportanceTest extends WordSpec with Matchers with PrivateMethodTester { class temp { val t1 = Flip(0.9) } - val a = Chain(Constant(0), (i: Int) => Constant(new temp)) + val a = CachingChain(Constant(0), (i: Int) => Constant(new temp)) val b = Apply(a, (t: temp) => t.t1.value) val alg = Importance(10000, b) alg.start @@ -434,7 +456,7 @@ class ImportanceTest extends WordSpec with Matchers with PrivateMethodTester { class temp { val t1 = Flip(0.9) } - val a = Chain(Constant(0), (i: Int) => Constant(new temp)) + val a = CachingChain(Constant(0), (i: Int) => Constant(new temp)) val b = Apply(a, (t: temp) => t.t1.value) val prob = List.fill(1000){Forward(Universe.universe); b.value} prob.count(_ == true).toDouble/1000.0 should be (0.9 +- .01) diff --git a/Figaro/src/test/scala/com/cra/figaro/test/example/BayesianNetworkTest.scala b/Figaro/src/test/scala/com/cra/figaro/test/example/BayesianNetworkTest.scala index 5c3cb38b..4b33220d 100644 --- a/Figaro/src/test/scala/com/cra/figaro/test/example/BayesianNetworkTest.scala +++ b/Figaro/src/test/scala/com/cra/figaro/test/example/BayesianNetworkTest.scala @@ -31,10 +31,15 @@ class BayesianNetworkTest extends WordSpec with Matchers { "produce the correct probability under importance sampling" taggedAs (ExampleTest) in { test((e1: Element[Boolean], e2: Element[Boolean]) => Importance(20000, e1, e2)) } - - "produce the correct probability under Metropolis-Hastings" taggedAs (ExampleTest) in { + + "produce the correct probability under Metropolis-Hastings without burn-in or interval" taggedAs (ExampleTest) in { + test((e1: Element[Boolean], e2: Element[Boolean]) => + MetropolisHastings(80000, ProposalScheme.default, e1, e2)) + } + + "produce the correct probability under Metropolis-Hastings with burn-in and interval" taggedAs (ExampleTest) in { test((e1: Element[Boolean], e2: Element[Boolean]) => - MetropolisHastings(8000000, ProposalScheme.default, e1, e2)) + MetropolisHastings(80000, ProposalScheme.default, 800, 10, e1, e2)) } } diff --git a/Figaro/src/test/scala/com/cra/figaro/test/learning/ParameterTest.scala b/Figaro/src/test/scala/com/cra/figaro/test/learning/ParameterTest.scala index f5240fe3..49660f68 100644 --- a/Figaro/src/test/scala/com/cra/figaro/test/learning/ParameterTest.scala +++ b/Figaro/src/test/scala/com/cra/figaro/test/learning/ParameterTest.scala @@ -61,6 +61,13 @@ class ParameterTest extends WordSpec with Matchers { b2.expectedValue should be(0.6 +- 0.001) } + "properly calculate MAP value" in { + val b = BetaParameter(1, 1) + b.MAPValue should equal(0.5) + val b2 = BetaParameter(3, 2) + b2.MAPValue should be((2.0 / 3.0) +- 0.001) + } + "properly maximize its alpha and beta hyperparameters" in { val b = BetaParameter(3, 2) b.maximize(Seq(1.0, 1.0)) @@ -95,8 +102,16 @@ class ParameterTest extends WordSpec with Matchers { "properly calculate expected value" in { val d = DirichletParameter(1, 1) d.expectedValue(0) should equal(0.5) + val d2 = DirichletParameter(3, 2) + d2.expectedValue(0) should be(0.6 +- 0.001) + } + + "properly calculate MAP value" in { + val d = DirichletParameter(1, 1) + d.MAPValue(0) should equal(0.5) + val d2 = BetaParameter(3, 2) - d2.expectedValue should be(0.6 +- 0.001) + d2.MAPValue should be((2.0 / 3.0) +- 0.001) } "properly maximize its hyperparameters" in { diff --git a/Figaro/src/test/scala/com/cra/figaro/test/library/atomic/continuous/ContinuousTest.scala b/Figaro/src/test/scala/com/cra/figaro/test/library/atomic/continuous/ContinuousTest.scala index 3aed00fa..254c7ed8 100644 --- a/Figaro/src/test/scala/com/cra/figaro/test/library/atomic/continuous/ContinuousTest.scala +++ b/Figaro/src/test/scala/com/cra/figaro/test/library/atomic/continuous/ContinuousTest.scala @@ -21,6 +21,7 @@ import com.cra.figaro.library.atomic.continuous._ import com.cra.figaro.language._ import JSci.maths.statistics._ import JSci.maths.SpecialMath.gamma +import org.apache.commons.math3.distribution.MultivariateNormalDistribution class ContinuousTest extends WordSpec with Matchers { "A AtomicUniform" should { @@ -77,7 +78,7 @@ class ContinuousTest extends WordSpec with Matchers { } } - "A AtomicNormal" should { + "An AtomicNormal" should { "have value within a range with probability equal to the cumulative probability of the upper minus the lower" in { Universe.createNew() val elem = Normal(1.0, 2.0) @@ -156,6 +157,84 @@ class ContinuousTest extends WordSpec with Matchers { } } + "An AtomicMultivariateNormal" should { + val means = List(1.0, 2.0) + val covariances = List(List(.25, .15), List(.15, .25)) + +// "have value within a range with probability equal to the cumulative probability of the upper minus the lower" in { +// Universe.createNew() +// val elem = Normal(1.0, 2.0) +// val alg = Importance(20000, elem) +// alg.start() +// val dist = new NormalDistribution(1.0, 2.0) +// val targetProb = dist.cumulative(1.2) - dist.cumulative(0.7) +// alg.probability(elem, (d: Double) => 0.7 <= d && d < 1.2) should be(targetProb +- 0.01) +// } + +// "compute the correct probability under Metropolis-Hastings" in { +// Universe.createNew() +// val elem = Normal(1.0, 2.0) +// val alg = MetropolisHastings(20000, ProposalScheme.default, elem) +// alg.start() +// val dist = new NormalDistribution(1.0, 2.0) +// val targetProb = dist.cumulative(1.2) - dist.cumulative(0.7) +// alg.probability(elem, (d: Double) => 0.7 <= d && d < 1.2) should be(targetProb +- 0.01) +// } + + "have the correct density" in { + Universe.createNew() + val elem = MultivariateNormal(means, covariances) + val dist = new MultivariateNormalDistribution(means.toArray, covariances.map((l: List[Double]) => l.toArray).toArray) + + elem.density(List(1.5, 2.5)) should be(dist.density(Array(1.5, 2.5)) +- 0.00000001) + } + + "produce samples with the correct means and covariances" in { + Universe.createNew() + val elem = MultivariateNormal(means, covariances) + + var n = 0 + var sumx1 = 0.0 + var sumx2 = 0.0 + var ss1 = 0.0 + var ss2 = 0.0 + var sc12 = 0.0 + + for (i <- 0 to 1000) { + val rand = elem.generateRandomness() + val values = elem.generateValue(rand) + + val x1 = values(0) + val x2 = values(1) + + n += 1 + sumx1 += x1 + sumx2 += x2 + ss1 += x1 * x1 + ss2 += x2 * x2 + sc12 += x1 * x2 + } + + val mean1 = sumx1 / n + val mean2 = sumx2 / n + val var1 = (ss1 - (sumx1 * sumx1 / n)) / (n - 1) + val var2 = (ss2 - (sumx2 * sumx2 / n)) / (n - 1) + val cov = (sc12 - (sumx1 * sumx2 / n)) / (n - 1) + + mean1 should be (means(0) +- 0.01) + mean2 should be (means(1) +- 0.01) + + var1 should be (covariances(0)(0) +- 0.01) + var2 should be (covariances(1)(1) +- 0.01) + cov should be (covariances(0)(1) +- 0.01) + } + + "convert to the correct string" in { + Universe.createNew() + MultivariateNormal(means, covariances).toString should equal( "MultivariateNormal(" + means + ",\n" + covariances + ")") + } + } + "An AtomicExponential" should { "have value within a range with probability equal to the cumulative probability of the upper minus the lower" in { Universe.createNew() diff --git a/FigaroExamples/META-INF/MANIFEST.MF b/FigaroExamples/META-INF/MANIFEST.MF index d8e7ea83..0ec5ca74 100644 --- a/FigaroExamples/META-INF/MANIFEST.MF +++ b/FigaroExamples/META-INF/MANIFEST.MF @@ -2,7 +2,7 @@ Manifest-Version: 1.0 Bundle-ManifestVersion: 2 Bundle-Name: FigaroExamples Bundle-SymbolicName: com.cra.figaro.examples -Bundle-Version: 2.2.0 +Bundle-Version: 2.2.1 Require-Bundle: com.cra.figaro;bundle-version="2.2.0", org.scala-lang.scala-actors, org.scala-lang.scala-library, diff --git a/META-INF/MANIFEST.MF b/META-INF/MANIFEST.MF index c5fab29a..28cb923d 100644 --- a/META-INF/MANIFEST.MF +++ b/META-INF/MANIFEST.MF @@ -2,7 +2,7 @@ Manifest-Version: 1.0 Bundle-ManifestVersion: 2 Bundle-Name: Figaro Bundle-SymbolicName: com.cra.figaro -Bundle-Version: 2.2.0 +Bundle-Version: 2.2.1 Bundle-ClassPath: ., lib/jsci-core.jar Export-Package: com.cra.figaro.algorithm, diff --git a/doc/Figaro 2.0 Release Notes.pdf b/doc/Figaro 2.0 Release Notes.pdf index ac335406..46f4b4ba 100644 Binary files a/doc/Figaro 2.0 Release Notes.pdf and b/doc/Figaro 2.0 Release Notes.pdf differ diff --git a/project/Build.scala b/project/Build.scala index 90a3a50e..5a8635bd 100644 --- a/project/Build.scala +++ b/project/Build.scala @@ -9,7 +9,7 @@ object FigaroBuild extends Build { override val settings = super.settings ++ Seq( organization := "com.cra.figaro", description := "Figaro: a language for probablistic programming", - version := "2.2.0.0", + version := "2.2.1.0", scalaVersion := "2.10.4", //scalaVersion := "2.11.1", crossScalaVersions := Seq("2.10.4", "2.11.1"), @@ -57,6 +57,7 @@ object FigaroBuild extends Build { "org.scala-lang" % "scala-actors" % scalaVersion.value, "org.scala-lang" % "scala-reflect" % scalaVersion.value, "asm" % "asm" % "3.3.1", + "org.apache.commons" % "commons-math3" % "3.3", "net.sf.jsci" % "jsci" % "1.2", "org.scalatest" %% "scalatest" % "2.1.7" % "test" ))