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); } }
/** * 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); }
/** * 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); }
/** * 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); }
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"); }
/** * 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); }