Example #1
0
        public void TestAgainstReference()
        {
            try
            {
                // All this boilerplate is just to load JSON

                double   maxTol         = 5e-3;
                string   path           = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), @"Ebisu/Ebisu_test.json");
                string[] testData       = File.ReadAllLines(path);
                JArray   expectedResult = (JArray)JsonConvert.DeserializeObject(testData[0]);

                foreach (var child in expectedResult)
                {
                    //subtest might be either
                    // a) ["update", [3.3, 4.4, 1.0], [0, 5, 0.1], {"post": [7.333641958415551, 8.949256654818793,
                    // 0.4148304099305316]}] or b) ["predict", [34.4, 34.4, 1.0], [5.5], {"mean": 0.026134289032202798}]
                    //
                    // In both cases, the first two elements are a string and an array of numbers. Then the remaining vary depend on
                    // what that string is. where the numbers are arbitrary. So here we go...
                    String operation = child[0].ToString();

                    JArray     second = (JArray)child[1];
                    EbisuModel ebisu  = new EbisuModel(double.Parse(second[2].ToString()), double.Parse(second[0].ToString()), double.Parse(second[1].ToString()));

                    if (operation.Equals("update"))
                    {
                        int        successes = Convert.ToInt32(child[2][0].ToString());
                        int        total     = Convert.ToInt32(child[2][1].ToString());
                        double     t         = Convert.ToDouble(child[2][2].ToString());
                        JArray     third     = (JArray)child[3].Last.Last;//subtest.get(3).get("post");
                        EbisuModel expected  = new EbisuModel(double.Parse(third[2].ToString()), double.Parse(third[0].ToString()), double.Parse(third[1].ToString()));

                        IEbisu actual = Ebisu.UpdateRecall(ebisu, successes, total, t);

                        Assert.AreEqual(expected.getAlpha(), actual.getAlpha(), maxTol);
                        Assert.AreEqual(expected.getBeta(), actual.getBeta(), maxTol);
                        Assert.AreEqual(expected.getTime(), actual.getTime(), maxTol);
                    }
                    else if (operation.Equals("predict"))
                    {
                        double t        = Convert.ToDouble(child[2][0].ToString());
                        double expected = Convert.ToDouble(child[3].First.Last.ToString());
                        double actual   = Ebisu.PredictRecall(ebisu, t, true);
                        Assert.AreEqual(expected, actual, maxTol);
                    }
                    else
                    {
                        throw new Exception("unknown operation");
                    }
                }
            }
            catch (Exception ex)
            {
                ex.StackTrace.ToString();
                Console.WriteLine("¡¡¡OOOPS SOMETHING BAD HAPPENED!!!");
                Assert.IsTrue(false);
            }
        }
Example #2
0
        /**
         * Estimate recall probability.
         *
         * Given a learned fact, encoded by an Ebisu model, estimate its probability
         * of recall given how long it's been since it was studied/learned.
         *
         * @param prior the existing Ebisu model
         * @param tnow the time elapsed since this model was last reviewed
         * @param exact if false, return log-probabilities (faster)
         * @return the probability of recall (0 (will fail) to 1 (will pass))
         */
        public static double PredictRecall(IEbisu prior, double tnow, bool exact)
        {
            double alpha = prior.getAlpha();
            double beta  = prior.getBeta();
            double dt    = tnow / prior.getTime();
            double ret   = LogBetaRatio(alpha + dt, alpha, beta);

            return(exact ? Math.Exp(ret) : ret);
        }
Example #3
0
        /**
         * Compute time at which an Ebisu memory model predicts a given percentile at
         * a given accuracy
         *
         * @param model Ebisu memory model
         * @param percentile between 0 and 1 (0.5 corresponds to half-life)
         * @param coarse if true, returns an approximate solution (within an order of magnitude)
         * @param tolerance accuracy of the search for this `percentile`. Ignored if `coarse`.
         * @return time at which `predictRecall` would return `percentile`
         */
        public static double ModelToPercentileDecay(IEbisu model, double percentile, bool coarse,
                                                    double tolerance)
        {
            if (percentile < 0 || percentile > 1)
            {
                throw new Exception("percentiles must be between (0, 1) exclusive");
            }
            double alpha = model.getAlpha();
            double beta  = model.getBeta();
            double t0    = model.getTime();

            double logBab           = LogBeta(alpha, beta);
            double logPercentile    = Math.Log(percentile);
            Func <Double, Double> f = lndelta => (LogBeta(alpha + Math.Exp(lndelta), beta) - logBab) - logPercentile;

            double bracket_width = coarse ? 1.0 : 6.0;
            double blow          = -bracket_width / 2.0;
            double bhigh         = bracket_width / 2.0;
            double flow          = f(blow);
            double fhigh         = f(bhigh);

            while (flow > 0 && fhigh > 0)
            {
                // Move the bracket up.
                blow   = bhigh;
                flow   = fhigh;
                bhigh += bracket_width;
                fhigh  = f(bhigh);
            }
            while (flow < 0 && fhigh < 0)
            {
                // Move the bracket down.
                bhigh = blow;
                fhigh = flow;
                blow -= bracket_width;
                flow  = f(blow);
            }

            if (!(flow > 0 && fhigh < 0))
            {
                throw new Exception("failed to bracket");
            }
            if (coarse)
            {
                return((Math.Exp(blow) + Math.Exp(bhigh)) / 2 * t0);
            }
            Status status = MinimizeGolden.MinimizeGolden.Min(y => Math.Abs(f(y)), blow, bhigh, tolerance, 10000);

            if (!status.converged)
            {
                throw new Exception();
            }
            double sol = status.argmin;

            return(Math.Exp(sol) * t0);
        }
Example #4
0
        /**
         * Actual worker method that calculates the posterior memory model at the same
         * time in the future as the prior, and rebalances as necessary.
         */
        private static IEbisu UpdateRecall(IEbisu prior, int successes, int total, double tnow,
                                           bool rebalance, double tback)
        {
            double alpha = prior.getAlpha();
            double beta  = prior.getBeta();
            double t     = prior.getTime();
            double dt    = tnow / t;
            double et    = tback / tnow;

            double[] binomlns = Enumerable.Range(0, total - successes + 1).Select(i => LogBinom(total - successes, i)).ToArray();
            double[] logs     =
                Enumerable.Range(0, 3)
                .Select(m =>
            {
                List <Double> a =
                    Enumerable.Range(0, total - successes + 1)
                    .Select(i => binomlns[i] + LogBeta(beta, alpha + dt * (successes + i) + m * dt * et)).ToList();
                //.boxed()
                //.collect(Collectors.toList());
                List <Double> b = Enumerable.Range(0, total - successes + 1)
                                  .Select(i => Math.Pow(-1.0, i)).ToList();
                //.boxed()
                //.collect(Collectors.toList());
                return(LogSumExp(a, b)[0]);
            }).ToArray();

            double logDenominator = logs[0];
            double logMeanNum     = logs[1];
            double logM2Num       = logs[2];

            double mean   = Math.Exp(logMeanNum - logDenominator);
            double m2     = Math.Exp(logM2Num - logDenominator);
            double meanSq = Math.Exp(2 * (logMeanNum - logDenominator));
            double sig2   = m2 - meanSq;

            if (mean <= 0)
            {
                throw new Exception("invalid mean found");
            }
            if (m2 <= 0)
            {
                throw new Exception("invalid second moment found");
            }
            if (sig2 <= 0)
            {
                throw new Exception("invalid variance found " +
                                    String.Format("a=%g, b=%g, t=%g, k=%d, n=%d, tnow=%g, mean=%g, m2=%g, sig2=%g", alpha,
                                                  beta, t, successes, total, tnow, mean, m2, sig2));
            }
            List <Double> newAlphaBeta = MeanVarToBeta(mean, sig2);
            EbisuModel    proposed     = new EbisuModel(tback, newAlphaBeta[0], newAlphaBeta[1]);

            return(rebalance ? Rebalance(prior, successes, total, tnow, proposed) : proposed);
        }
Example #5
0
        public void Update()
        {
            IEbisu m       = new EbisuModel(2, 2, 2);
            IEbisu success = Ebisu.UpdateRecall(m, 1, 1, 2.0);
            IEbisu failure = Ebisu.UpdateRecall(m, 0, 1, 2.0);

            Assert.AreEqual(3.0, success.getAlpha(), 500 * EPS, "success/alpha");
            Assert.AreEqual(2.0, success.getBeta(), 500 * EPS, "success/beta");

            Assert.AreEqual(2.0, failure.getAlpha(), 500 * EPS, "failure/alpha");
            Assert.AreEqual(3.0, failure.getBeta(), 500 * EPS, "failure/beta");
        }
Example #6
0
        /**
         * Given a prior Ebisu model, a quiz result, the time of a quiz, and a
         * proposed posterior model, rebalance the posterior so its alpha and beta
         * parameters are close. In other words, move the posterior closer to its
         * approximate halflife for numerical stability.
         */
        private static IEbisu Rebalance(IEbisu prior, int successes, int total, double tnow,
                                        IEbisu proposed)
        {
            double newAlpha = proposed.getAlpha();
            double newBeta  = proposed.getBeta();

            if (newAlpha > 2 * newBeta || newBeta > 2 * newAlpha)
            {
                double roughHalflife = ModelToPercentileDecay(proposed, 0.5, true);
                return(UpdateRecall(prior, successes, total, tnow, false, roughHalflife));
            }
            return(proposed);
        }