Conversation
Member
Author
|
I'll be away for the next two weeks, but made this PR to avoid duplicated work |
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Replace the incomplete beta gradient roots with a continued-fraction implementation from TensorFlow Probability (this PR was Claude assisted)
Summary
grad_reg_inc_beta,inc_beta_ddaandinc_beta_ddbcompute the derivatives of the regularized incomplete beta functionI_z(a, b)with respect to its two shape parameters. They are two separate implementations of the same quantity. Path A (inc_beta_dda,inc_beta_ddb) servesbeta_cdf,neg_binomial_cdfandneg_binomial_2_cdf; path B (grad_reg_inc_beta, throughgrad_2F1) serves everylcdfandlccdfofbeta,beta_proportion,neg_binomial,student_t, plusstudent_t_cdfand the autodiff ofinc_betaitself. Both have defects that make the gradient NaN, or wrong by O(1), in regions that ordinary models reach. This PR replaces both with one implementation: the Boik and Robinson-Cox (1998) algorithm in the form implemented by TensorFlow Probability, a power series near the endpoints and a modified-Lentz continued fraction elsewhere, both differentiated term by term, with the prefactor in log space.inc_beta_ddaandinc_beta_ddbbecome wrappers;beta_cdfmakes one root call instead of two. Signatures do not change.The values of the distributions do not change. Only the gradients change.
Comparison, both trees compiled and run over the same 18 000 points in nine regions (details below):
log Iwrong by > 1e-2, path A,b ≫ alog Iwrong by > 1e-2, path B, negbinn > 200log I, branchdouble,var,fvar,fvar<fvar>grad_reg_inc_betaindoublebeta_lcdfper observation,α = β = 5beta_cdfper observation,α = β = 50The defects in the current code on
developEverything in this section describes
developat5252d51d47. All numbers were measured against an mpmath reference at 80 digits built through two independent routes (density-form quadrature and numerical differentiation at 110 digits) that agree at 60 digits at 54 check points, and confirmed by compiling and running the realdevelopheaders.1.
grad_reg_inc_betareturns NaN for every parameter whenbeta(a, b)underflows. Its callers passbeta(a, b)asbetaABand it divides by it.beta(a, b)underflows fora + babove about 1100 withanearb.beta_lcdf(y | 600, 600)has a NaN gradient with respect to both shapes today, and so doesinc_beta(a, b, z)with autodiff shapes. In the sweep regiona, bin [100, 12500] near the mode, the region whereIis of order 1, 1325 of 2000 points are NaN.2.
grad_reg_inc_betadrops the hypergeometric term whenz^a (1-z)^bunderflows. WhenC = z^a (1-z)^b / ais 0 it skipsgrad_2F1and returnsI (log z - 1/a - ψ(a) + ψ(a+b)). The dropped term is the whole answer whenIis near 1, because2F1(a+b, 1; a+1; z)grows like(1-z)^(-b).beta_lcdf(0.758 | 352, 590)returnsd/dα = 0.705; the true value is-3.4e-138.neg_binomial_lcdf(132 | 313, 0.068)returnsd/dα = 0for a true-2.3.3.
inc_beta_ddaandinc_beta_ddbstop on an absolute threshold that can be met before the first term. Both stop the series whenfabs(summand) < 1e-10, and the first summand carries((a+1)/(a+b))^3. Forb ≫ athat is below 1e-10 before the loop starts, and the returned ratio is its first term.beta_cdf(1.8e-4 | 1.145, 6786)returnsd/dα = -0.203for a true-0.386, atI = 0.65. In the sweep regionb ≫ a, smallz, 1301 of 2000 points have an error above 1e-6 in the gradient oflog Iand 408 above 1e-2.4.
inc_beta_ddaandinc_beta_ddbreflect on fixedzthresholds, not on which side is small. Forz > 0.75(and other fixed bands) they evaluate1 - I_{1-z}(b, a)regardless of where the mass is. In the deep lower tail the result is thenI' × (a bracket that must cancel to 1e-47)withI' ≈ 1, and the bracket comes out as 1e-15 noise.beta_cdf(0.76 | 446, 5)returnsd/dα = 8.9e-16for a true-1.06e-47;log(beta_cdf(...))then has a gradient of 2.5e+31. Some large-shape points also reach thek > 1e5iteration guard and throw, inside the range the docstring calls tested.5. The existing unit tests assert the wrong values.
inc_beta_ddb_test.cpppassesdigamma(a)where the fourth parameter isdigamma(b), and its expected constants were generated from that call. Bothinc_beta_dda_test.cppandinc_beta_ddb_test.cppalso assertdevelop's own output at points where it is wrong by orders of magnitude:1.8226241for a true-3.94e-5,9.3959293for a true1.48e-8.grad_reg_inc_beta_test.cppexpectsd/db = -infatz = 1, whereI ≡ 1and the derivative is 0.What this PR does
stan/math/prim/fun/grad_reg_inc_beta.hppgets a new body, a C++ port of_betainc_partialsfrom TensorFlow Probability (tensorflow_probability/python/math/special.py, Apache-2.0; see the licence note below). The algorithm is Boik and Robinson-Cox (1998), Derivatives of the Incomplete Beta Function, JSS 3(1): the DLMF 8.17.22 continued fraction evaluated by the modified Lentz method with the derivatives of the partial numerators carried through the recurrence, plus a power-series region near the endpoints from Cephes, also differentiated term by term. The prefactorz^a (1-z)^b / (a B(a, b))is evaluated in log space, which removes defects 1 and 2. The symmetry relationI_z(a, b) = 1 - I_{1-z}(b, a)is applied on the mode for the continued fraction and on the mean for the series, which removes defect 4. There is no absolute threshold; the series stops ateps / aand the fraction at3 eps, which removes defect 3.Two changes from the TFP source, both marked in the code. TFP's stop tests watch the value only. When
bis a positive integer, the series factor(n - b)and the continued-fraction numeratord_{2m}are exactly 0 atn = m = b; the value is then converged but the derivatives still need the tail of the series. TFP's ownd/dbis off by 4.2e-4 at(2, 3, 0.25)and by 6.7 % at(10, 1, 0.5), and itsd/daby 5.6 % at(1, 100, 0.01), where the symmetry relation makesb' = 1. The port stops only when the derivative increments have also converged. Integer shapes are common (initial values, fixed shapes). The defect will be reported upstream.inc_beta_ddaandinc_beta_ddbbecome wrappers overgrad_reg_inc_betathat return one of the two derivatives.beta_cdfcallsgrad_reg_inc_betaonce instead ofinc_beta_ddaandinc_beta_ddbseparately.prim/fun.hppincludesinc_beta_dda.hpp,inc_beta_ddb.hppandinc_beta_ddz.hpp, which it previously reached only throughprob/beta_cdf.hpp. ThebetaABargument ofgrad_reg_inc_betais kept for interface compatibility and is no longer used.Net change: about 180 lines of algorithm replacing 123.
Accuracy after the change
Same 18 000 points, same reference, both trees compiled with the same probe. The metric is the absolute error of the gradient of
log I, which is whatbeta_lcdfreturns and whatlog(beta_cdf(...))differentiates to; a relative error on a derivative that is 1e-300 ofIis not a useful number.a, b < 11 ≤ a, b ≤ 20,z < 0.75z > 0.75,a < 500b > a,b > 500,z ≤ 0.75b ≫ a,zsmalla, b,zuniforma, b,znear the moden ≤ 200200 < n ≤ 5000(1) Excluding points where
Iitself is subnormal in double.The relative error where the derivative is not negligible (
|dI| / I ≥ 1e-3) is at or below 6e-13 in every region exceptb ≫ a, smallz, where one continued-fraction point at the symmetry switch reaches 3.7e-9. The four scalar configurationsdouble,var,fvar<double>,fvar<fvar<double>>give bit-identical values on both trees, and the branch's autodiff columns have the same error profile asdouble.Speed
Xeon E5-2680 v3 at 2.50 GHz, exclusive node,
performancegovernor, two rounds agreeing within 2 %. Grida, bin [0.5, 60],zin [0.05, 0.95]. The loop adds an accumulator-dependent term to every input so it is not loop-invariant; a baseline is subtracted; the cycle counts of the baseline (6) and of3 digamma + beta(310) show nothing was removed by the compiler.Function level, net ns per call:
grad_reg_inc_betadouble, incl. 3 digamma + betagrad_reg_inc_betavar, value + gradgrad_reg_inc_betafvargrad_reg_inc_betafvar<fvar>inc_beta_ddadouble, incl. 2 digammaDistribution level,
N = 1000observations, sharedvarshapes, value plus reverse sweep, ns per observation:beta_cdfbeta_lcdfbeta_lccdfbeta_cdfbeta_lcdfbeta_lccdfneg_binomial_cdfneg_binomial_lcdfstudent_t_lcdfdevelop's path B cost is the 2F1 power series with threeexpand threelogper term and no reflection; atz = 0.998it takes 607 607 terms. The branch's continued fraction costs about 600 cycles above the digamma calls.Testing
Six test files, all with fixed references from the 80-digit mpmath computation, not from finite differences, which cannot see these defects:
prim/fun/grad_reg_inc_beta_test.cpp: the two existing cases (with thez = 1expectation corrected to 0 and the tolerance tightened to 1e-12), plus cases for each defect class:beta(a, b)underflow, prefactor underflow, integerb, and the tails.prim/fun/inc_beta_dda_test.cpp,prim/fun/inc_beta_ddb_test.cpp: rewritten from the reference at the same twelve points, with thedigamma_bargument corrected, plus regression points.rev/fun/inc_beta_test.cpp(new): shape gradients ofinc_betawithvararguments at the defect points.rev/prob/beta_cdf_test.cpp(new):beta_cdfshape partials at theb ≫ apoint, with both shapes autodiff and with each shape alone.rev/prob/beta_cdf_log_test.cpp(new):beta_lcdfshape gradients at the NaN point, the dropped-term point and an integer shape.Regression swap: with the
developheaders installed under these six files, 6 of 6 suites fail, 13 of 15 cases; with this branch's headers, 15 of 15 pass, before and after the swap. The two cases that pass ondevelopare the points where it was already correct.Licence note
The new body of
grad_reg_inc_beta.hppis derived from TensorFlow Probability, which is Apache-2.0. The file carries the TFP copyright notice and points atlicenses/tensorflow-probability-license.txt, following the form used for the Boost-derived code inprim/fun/log_modified_bessel_first_kind.hpp. Apache-2.0 code is already in the tree as vendored libraries (lib/tbb_2020.3,lib/benchmark_1.5.1); this would be the first inline derived file understan/.Known limits, not addressed here
beta_lccdfforms1 - Pnin linear space afterinc_beta. At(352, 590, 0.758)the doubleIis exactly 1, the value is-infand the gradients are infinite, where the true value islog(1 - I) ≈ -0.34. That is in the value code, on both trees, and is a separate change (the symmetry relation for the value).Ibelow about 1e-315 (subnormal), the gradient's relative error rises to 1e-2. The reference itself has few digits there.Not included
grad_2F1, which other functions use.neg_binomial*_cdfcall pattern; they need one derivative and already make one root call.Release notes
Replaces the incomplete beta gradient roots with a more stable continued-fraction implementation
Checklist
Copyright holder: Aalto University
The copyright holder is typically you or your assignee, such as a university or company. By submitting this pull request, the copyright holder is agreeing to the license the submitted work under the following licenses:
- Code: BSD 3-clause (https://opensource.org/licenses/BSD-3-Clause)
- Documentation: CC-BY 4.0 (https://creativecommons.org/licenses/by/4.0/)
the basic tests are passing
./runTests.py test/unit)make test-headers)make test-math-dependencies)make doxygen)make cpplint)the code is written in idiomatic C++ and changes are documented in the doxygen
the new changes are tested