Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add loglogistic, logitexp, log1mlogistic and logit1mexp #82

Merged
merged 29 commits into from
May 31, 2024

Conversation

andrewjradcliffe
Copy link
Contributor

@andrewjradcliffe andrewjradcliffe commented May 3, 2024

This takes advantage of LogExpFunctions's accurate implementations of log1pexp and log1mexp, combined with negation in the log-odds domain to provide more accurate and less expensive implementations of the function compositions.

As recommended here, I am submitting these.

Precedent for loglogistic and log1mlogistic is their existence in Stan; it is natural to include their inverses.

This takes advantage of `LogExpFunctions`'s accurate
implementations of `log1pexp` and `log1mexp`, combined with negation
in the log-odds domain to provide more accurate and less expensive
implementations of the function compositions.
src/basicfuns.jl Outdated Show resolved Hide resolved
src/basicfuns.jl Outdated Show resolved Hide resolved
src/basicfuns.jl Outdated Show resolved Hide resolved
src/basicfuns.jl Outdated Show resolved Hide resolved
src/basicfuns.jl Outdated

Its inverse is the [`loglogistic`](@ref) function.
"""
logitexp(x::Real) = x - log1mexp(x)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be done more carefully to avoid loss of precision when both terms are approximately equal?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right -- this can be done more carefully using logexpm1.
However, as illustrated in the figures, logexpm1 itself suffers a loss of precision around -log(2).

Absolute error is ok.
logitexp_abserr

But units in the last place error is a problem around -log(2)
logitexp_ulperr

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As this can only be resolved by improving the ulp error of logexpm1 around log(2), I see no reason for this to gate merging of this PR.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we could check if the argument is in a neighborhood of -log(2), and then just use a first-order approximation. My quick calculations suggest that the local derivative is 2 but someone should check this.

src/basicfuns.jl Outdated Show resolved Hide resolved
src/basicfuns.jl Outdated Show resolved Hide resolved
src/basicfuns.jl Outdated Show resolved Hide resolved
src/basicfuns.jl Outdated
Comment on lines 458 to 460
loglogistic(x::Real) = _loglogistic(x)
_loglogistic(x::Real) = -log1pexp(-x)
_loglogistic(x::Union{Integer, Rational}) = _loglogistic(float(x))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm still not sure whether unsigned integers are worth any consideration given that you will run into problems with them in many (most?) calculations and definitions in JuliaStats, JuliaMath, etc. typically do not handle them in a special way - but if we care then it would be easier to just define

Suggested change
loglogistic(x::Real) = _loglogistic(x)
_loglogistic(x::Real) = -log1pexp(-x)
_loglogistic(x::Union{Integer, Rational}) = _loglogistic(float(x))
loglogistic(x::Real) = -log1pexp(-float(x))

This would cover also cases such as e.g. unitful integer numbers that are not covered by Union{Integer, Rational} and for floating points such as x::Float64 or x::Float32 it would be equivalent to -log1pexp(-x).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I opted for the float solution, as this is simplest and, furthermore, replicates the handling of integer and rational arguments in Julia Base.

I'm still not sure whether unsigned integers are worth any consideration

SpecialFunctions.jl (another package I contributed to) converts most arguments via float, hence, it handles both the unsigned integer cases as well as the signed integer (i.e. typemin(T<:Signed)) edge case.

Use of float to convert integers and rationals to a floating point type for use in special functions is common throughout Base. Unless integers and rationals are converted, then a function interface defined as x::Real cannot be claimed. On the other hand, if the intention is to guarantee reasonable results only for floating point numbers, then this should clearly be stated in the package description. However, I am of the opinion that this package should follow the behavior of Base and SpecialFunctions, whereby integers and rationals are converted to floating point numbers internally.

Unless conversion is enforced, this leads to some obviously incorrect results, e.g., logistic(typemin(Int)) == 1.0, but, clearly, should be 0.0.

src/basicfuns.jl Outdated Show resolved Hide resolved
src/basicfuns.jl Outdated Show resolved Hide resolved
Comment on lines 190 to 194
ChainRulesCore.@scalar_rule(loglogistic(x::Real), (logistic(-x),))
ChainRulesCore.@scalar_rule(log1mlogistic(x::Real), (-logistic(x),))
ChainRulesCore.@scalar_rule(logitexp(x::Real), (inv(1 - exp(x)),))
ChainRulesCore.@scalar_rule(logit1mexp(x::Real), (-inv(1 - exp(x)),))

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMO these are not needed - AD handles the functions already fine since rules for log1pexp etc. are defined.

Suggested change
ChainRulesCore.@scalar_rule(loglogistic(x::Real), (logistic(-x),))
ChainRulesCore.@scalar_rule(log1mlogistic(x::Real), (-logistic(x),))
ChainRulesCore.@scalar_rule(logitexp(x::Real), (inv(1 - exp(x)),))
ChainRulesCore.@scalar_rule(logit1mexp(x::Real), (-inv(1 - exp(x)),))

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As directed, these definitions have been removed.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking through the issues, #36 seems to indicate that these functions should support ChainRulesCore forward and reverse rules.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, that's just not correct. These new functions are supported by ChainRulesCore-based AD systems just fine without any additional definitions. See https://juliadiff.org/ChainRulesCore.jl/stable/rule_author/which_functions_need_rules.html for some additional information on this matter.

@andrewjradcliffe
Copy link
Contributor Author

andrewjradcliffe commented May 5, 2024

As

  • all tests (included the added tests) are passing
  • documentation has been updated
  • the extension interfaces modified to support these 4 functions
  • and tests of the package extensions have been added

please let me know of any additional changes which must be made for this PR to be merged.

Copy link
Collaborator

@tpapp tpapp left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, as far as I can see all requests have been addressed. Thanks for the fine work!

@andrewjradcliffe
Copy link
Contributor Author

May we clarify: include or exclude ChainRulesCore forward/reverse rules?

Last item to adjust, then can merge.

@andrewjradcliffe
Copy link
Contributor Author

It would be best to not let this bit rot. Moreover, it is easiest to make changes when it is in recent memory.

@devmotion could you clarify with @tpapp whether to include or exclude ChainRulesCore forward/reverse rules?

Once clarified, I can either revert the last commit or leave it in place. Should I then assume that there would be no further obstacles to merging?

@tpapp
Copy link
Collaborator

tpapp commented May 9, 2024

Feel free to remove them. Thanks for the ping!

@andrewjradcliffe
Copy link
Contributor Author

Reverted ChainRulesCore support. Should be ready for merge -- @devmotion and @tpapp

lim2 = T === Float16 ? -10.0 : -37.0
xs = T[Inf, -Inf, 0.0, lim1, lim2]
for x in xs
@test loglogistic(x) == log(logistic(x))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we test multiple types T, I think it would be good to also check that - as desired - the results are of type T.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense. Comprehensive return type tests have been added for the 4 functions included in this PR.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Anything else, @devmotion?

@devmotion devmotion merged commit 6fa1f71 into JuliaStats:master May 31, 2024
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants