-
Notifications
You must be signed in to change notification settings - Fork 193
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
Wrong weighted sum and mean results for complex numbers #518
Comments
Indeed. That's because we use Would you make a pull request to apply that change (in weights.jl) and add a test? |
mean
results
Just ran into this in another context: #534 isn't quite the same, because it's about values that are vectors of real numbers, but the cause is the (recursive) behavior of I think using |
The reason we use |
Sounds good. Regarding on where do draw the line - BLAS would only accelerate So we'd do Base.sum(v::AbstractArray{<:Real}, w::AbstractWeights) = dot(v, values(w))
Base.sum(v::AbstractArray, w::AbstractWeights) = ... right? @nalimilan, what do you think would be best for the generic implementation: # Option 1:
sum(A .* values(w))
# Option 2:
sum(Base.Broadcast.Broadcasted(*, (A, values(w))))
# Option 3:
mapreduce(*, +, A, values(w)) |
I've been thinking - maybe we should drop use of
Though I just saw that we're using a simple sum in |
Options 2 and 3 are the best ones. Not sure how to choose between them, I guess benchmarking is needed.
IIRC we use Welford's algorithm when possible in |
Yes, it does (AFAIK the default
Oh, sure - I just stumbled over it while looking into sum and wasn't sure if it was intentional. |
Actually |
Oops, indeed - I naively thought it would be allocation-free. In that case, |
|
Ok, so maybe we do
for now, to get correct behavior in the general case without performance loss in the specific case (real numbers), and worry about precision improvement in a separate issue? Would that be fine with you, @nalimilan? |
As I noted in my first comment, I think this will work: Base.sum(v::AbstractArray{<:Complex}, w::AbstractWeights) = dot(w, v) For the general case, it looks like broadcasting is slower than the julia> x = rand(10_000);
julia> z = Vector{Union{Float64, Missing}}(x);
julia> @btime dot(x, z);
13.443 μs (1 allocation: 16 bytes)
julia> f(A, w) = sum(Base.Broadcast.Broadcasted(*, (A, w)))
f (generic function with 1 method)
julia> @btime f(x, z);
22.258 μs (11 allocations: 240 bytes) Since we don't actually need the broadcasting features, I guess we should simply use a loop similar to function dot(x::AbstractArray, y::AbstractArray)
lx = length(x)
if lx != length(y)
throw(DimensionMismatch("first array has length $(lx) which does not match the length of the second, $(length(y))."))
end
if lx == 0
return dot(zero(eltype(x)), zero(eltype(y)))
end
s = zero(dot(first(x), first(y)))
for (Ix, Iy) in zip(eachindex(x), eachindex(y))
@inbounds s += dot(x[Ix], y[Iy])
end
s
end |
You mean something like this? function dotnr_loop(x::AbstractArray, y::AbstractArray)
lx = length(x)
if lx != length(y)
throw(DimensionMismatch("first array has length $(lx) which does not match the length of the second, $(length(y))."))
end
if lx == 0
return zero(eltype(x)) * zero(eltype(y))
end
s = zero(first(x) * first(y))
for (Ix, Iy) in zip(eachindex(x), eachindex(y))
@inbounds s += x[Ix] * y[Iy]
end
s
end
Sure, where With my use case from #534 (computing the weighted center of a cluster of 3D-points),
On the other hand, reduce + broadcast is simpler, more generic and will also run on a GPU (currently needs explicit init element): dotnr_bcast(x::AbstractArray, y::AbstractArray) = reduce(
+,
Base.Broadcast.Broadcasted(*, (x, values(y))),
init = zero(eltype(x)) * zero(eltype(y))
) So when the data becomes larger
then So I wonder if we shouldn't go with the broadcast-based implementation, even if it's a bit slower on a CPU (at least for this use case it is). For |
Long term, we should find an alternative to |
Yes.
Tough call. I guess we should try to figure out with broadcast is slower than a loop on CPU. A similar alternative is using a generator, but in my tests it's also slower than a loop. Can you investigate this? This is of interest for Julia in general, not only for weighted sum, especially if a custom syntax is added for lazy broadcasting (JuliaLang/julia#31088).
Actually |
Well, I can try - but this may go fairly deep, we might need some code-dev experts there. :-) I assume some of the core (and not so core) developers are anyhow frequently thinking about how to improve broadcasting & friends. :-)
I thought it did ... but apparently not (yet):
Oops. But as you say, maybe that's coming soon? |
At least it's implemented in JuliaLang/julia#31020, so it's quite close. |
OK, I've tested that branch, and summing julia> using BenchmarkTools
julia> x = rand(100_000);
julia> y = rand(100_000);
# This isn't correct in general, but OK for this simple case
julia> Base.LinearIndices(bc::Base.Broadcast.Broadcasted{<:Any}) = axes(bc)[1]
julia> @btime sum(x);
16.212 μs (1 allocation: 16 bytes)
julia> @btime Base._mapreduce(identity, Base.add_sum, IndexLinear(), Base.Broadcast.Broadcasted(identity, (x,)));
16.942 μs (3 allocations: 48 bytes)
julia> @btime Base._mapreduce(identity, Base.add_sum, IndexLinear(), Base.Broadcast.Broadcasted(*, (x, y)));
32.016 μs (3 allocations: 64 bytes)
julia> using LinearAlgebra
julia> @btime dot(x, y);
17.722 μs (1 allocation: 16 bytes) Unfortunately this hack cannot be applied as-is, but it seems worth investigating whether we can ensure that |
The advantage of using broadcast + reduce, compared to a custom implementation, would be that we'll take advantage of future improvements automatically. :-) |
Nice!! |
I looked for that definition, but I can't find it - is it something implicit? I tried to run your example, but I get
|
Ah, let me guess, I need JuliaLang/julia#31020, of course? |
Hm, we can't just override But maybe we could use Base.Broadcast.Broadcasted(*, (A, w)) instead of Base.Broadcast.Broadcasted(*, (A, values(w))) and then add a method to Or maybe it would be possible to do this in a generic way, so that |
Actually after reading more carefully https://github.com/JuliaLang/julia/pull/31020/files#r256266019 I realized it already works, one just needs to call julia> @btime sum(Base.Broadcast.instantiate(Base.Broadcast.Broadcasted(*, (x, y))));
31.200 μs (4 allocations: 96 bytes) So we just need to wait for the PR to be merged in Julia (hopefully in time for 1.4), and we will be able to stop using |
Sounds great! Regarding the PR, sure - I guess I'll wait until #31020 is merged though, so that CI tests can run. |
I've been reminding myself about that issue myself recently, in the context of JuliaArrays/ArraysOfArrays.jl#20. Maybe an unoptimized fallback plus a super-lightweight package AbstractArraysOfArrays.jl or so that provides only interfaces like flatview, with ArraysOfArrays providing actually implementation of such arrays (and likely Base too, in the future, with JuliaLang/Pkg.jl#1285)? If a dependency on a lightweight nested-arrays-interface package would be acceptable, I'd volunteer to take this on, including the standard non-optimized case and this issue here. |
For Distributions, in principle a definition for Of course, one should add optimizations for |
I think we really would need an interface that gives access to the backing flat matrix (if there is one), something more specific than |
How about I'll make a prototype (https://github.com/JuliaArrays/AbstractArraysOfArrays.jl) and we see what it can yield, potentially? AbstractArraysOfArrays would of course add support for |
I'm not sure if an interface is needed - it's definitely not needed for supporting vectors of vectors (whereas the generic definition is). I view it merely as an optimization for custom vector types, and hence similar to other optimizations it might be fine to implement them together with the custom types in separate packages. There are also not too many of such custom types and eg ColVecs/RowVecs is used mostly in the Julia GP organization, and at least ColVecs/RowVecs I expect to be replaced with EachCol/EachRow.
There's already a PR to Compat that makes it available on older Julia versions. |
So put shortly: I think one should rather prioritize and add a generic implementation instead of working on an interface and (possibly) adding dependencies. Adding dependencies also seems problematic (impossible?) if methods with weights are moved to Statistics (IIRC this was the plan - even though in general I'm not convinced that moving things from StatsBase to a stdlib is a good idea, in particular for anything that is not absolutely stable since development will become much slower and more challenging). |
Indeed. But I think and interface is required to enable optimized (and GPU-friendly) methods. I don't believe support for efficient nested arrays should be limited to the upcoming
Oh, indeed - nice! |
Sure, that can be done first, without depending on anything. I'd just like to know if there will be room to support other efficient nested array implementations besides
Ah, that could indeed be a problem. |
If JuliaLang/julia#32310 would add an |
I agree, I assume there will always be different implementations that probably focus on different use cases and allow slightly different optimizations. I wonder, however, if StatsBase or Statistics should be concerned with it at all - or if it should be the responsibility of the packages with custom vectors to provide optimized implementations. These could be optimized for their specific vector design or eg make use of some external interface package (that StatsBase doesn't have to know about) or eg make use of an implementation for EachCol/EachRow in StatsBase. |
I think that would make those packages immense and result in a lot of code duplication. It's not just StatsBase, it would also be functions for individual distributions (e.g. an efficient and GPU-friendly What I'm going for here is a few simple functions like The upcoming Compat for If methods with weights are moved to Statistics one day (possibly), that could be accompanied by adding such an API to Base - but for now, that seems to be in the (possibly far) future. |
Code could be shared even if it is not part of StatsBase, eg by an external interface package with default definitions or by using code for EachRow etc in StatsBase.
Usually, you also have to know the orientation of the vectors (eg rows or columns), often just retrieving the underlying array is not enough. Due to these differences I still wonder if/what an interface could provide that works for and helps with generic implementations and is not covered by the AbstractVector interface.
We use |
This might be the cleanest solution - if there exists a common API for such slices that is helpful for working with slices and is not just the one for AbstractVector. |
JuliaLang/julia#32310 may not provide everything we need there, judging from the current state and what Compat will provide will neseccarily be limited as well. I'll try to draft something in AbstractArraysOfArrays that can fully integrate with JuliaLang/julia#32310 and allow for other implementations and backward-compat as well.
Absolutely, I agree that will be crucial. |
I admit I don't really use reductions over vectors, but don't they kind of work already? BTW, the present issue (about complex numbers) has been fixed recently by #737, right? Regarding optimized implementations for specific types, I'd rather avoid adding yet another dependency to StatsBase, so any solution to leverage JuliaLang/julia#32310 sounds appealing! |
For sure - the problem ist that they way it's currently written it doesn't really provide a generic API for efficient nested arrays, flattening them, reconstructing nested arrays from similar flattened arrays, and so on ... |
Unfortunately, they don't work: julia> using StatsBase
julia> mean([[0.0, 1.0, 0.0], [0.3, 0.2, 0.5], [0.4, 0.2, 0.4]], weights([0.2, 0.3, 0.5]))
ERROR: DimensionMismatch("x and y are of different lengths!")
Stacktrace:
[1] dot
@ ~/.asdf/installs/julia/1.7.0/share/julia/stdlib/v1.7/LinearAlgebra/src/generic.jl:900 [inlined]
[2] dot(x::Vector{Vector{Float64}}, y::Weights{Float64, Float64, Vector{Float64}})
@ LinearAlgebra ~/.asdf/installs/julia/1.7.0/share/julia/stdlib/v1.7/LinearAlgebra/src/generic.jl:915
[3] wsum
@ ~/.julia/packages/StatsBase/CuCbN/src/weights.jl:383 [inlined]
[4] wsum
@ ~/.julia/packages/StatsBase/CuCbN/src/weights.jl:385 [inlined]
[5] #sum#13
@ ~/.julia/packages/StatsBase/CuCbN/src/weights.jl:607 [inlined]
[6] sum
@ ~/.julia/packages/StatsBase/CuCbN/src/weights.jl:607 [inlined]
[7] _mean
@ ~/.julia/packages/StatsBase/CuCbN/src/weights.jl:656 [inlined]
[8] #mean#16
@ ~/.julia/packages/StatsBase/CuCbN/src/weights.jl:654 [inlined]
[9] mean(A::Vector{Vector{Float64}}, w::Weights{Float64, Float64, Vector{Float64}})
@ StatsBase ~/.julia/packages/StatsBase/CuCbN/src/weights.jl:654
[10] top-level scope
@ REPL[4]:1
julia> StatsBase.var([[0.0, 1.0, 0.0], [0.3, 0.2, 0.5], [0.4, 0.2, 0.4]], weights([0.2, 0.3, 0.5]))
ERROR: MethodError: no method matching var(::Vector{Vector{Float64}}, ::Weights{Float64, Float64, Vector{Float64}})
Closest candidates are:
var(::AbstractArray{T} where T<:Real, ::AbstractWeights; mean, corrected) at ~/.julia/packages/StatsBase/CuCbN/src/moments.jl:41
var(::AbstractArray{T} where T<:Real, ::AbstractWeights, ::Int64; mean, corrected) at ~/.julia/packages/StatsBase/CuCbN/src/moments.jl:92
var(::AbstractArray; corrected, mean, dims) at ~/.asdf/installs/julia/1.7.0/share/julia/stdlib/v1.7/Statistics/src/Statistics.jl:368
...
Stacktrace:
[1] top-level scope
@ REPL[6]:1
julia> StatsBase.cov([[0.0, 1.0, 0.0], [0.3, 0.2, 0.5], [0.4, 0.2, 0.4]], weights([0.2, 0.3, 0.5]))
3-element Vector{Float64}:
0.028333333333333335
-0.05333333333333333
0.024999999999999994 |
I just tested it - it's not fully fixed yet, I think: julia> A = [rand(3,4) for i in 1:6]
julia> w = FrequencyWeights([rand() for i in 1:6])
julia> mean(A, w)
ERROR: DimensionMismatch("x and y are of different lengths!") |
@devmotion was faster, hadn't updated the page yet :-) |
Please try julia> mean([[0.0, 1.0, 0.0], [0.3, 0.2, 0.5], [0.4, 0.2, 0.4]], weights([0.2, 0.3, 0.5]))
3-element Vector{Float64}:
0.29000000000000004
0.36
0.35
OK. I must say I don't really see what the implementation would look like. Ideally a high-level API would work efficiently for all array types -- otherwise there's no end to adding special methods for each array type. |
Oh nice, I didn't realize that there were unreleased changes in the master branch. Can we make a new release? |
I'm currently drafting something.
Yes indeed! I think nested arrays that are backed by flat arrays are an important class of array types, but one should only need to write a single specialized methods to support any of them in a generic fashion (possibly two, depending on inner- or outer-dims-first storage). We already do have these specialized methods in StatsBase, Distributions and all over the ML-ecosystem, the ones that take a flat array instead of nested arrays. We just need to redirect to them. |
@devmotion I think I found a clean an lightweight approach: AbstractArraysOfArrays (we can name it differently) can provide two main functions:
To handle the inner- or outer-dims-first or mixed-dims backing-storage issue, the flatten functions would
This way, we can easily support Packages like StatsBase, Distributions & friends would only need to depend on the super-lightweight AbstractArraysOfArrays (or other name) to be able to check if an array of arrays can be flattened efficiently and then use linear algebra operations to process a collection of variates in one go (just an example). Would be a good basis for custom broadcasts that prevent a lot of memory allocation and and enable GPU-compatibility. I think it would also help with moving (I believe this is the intention?) from a matrix to a vector-of-vectors sample model for multivariate distributions. It would also, I think, provide a good basis to make batched parameter transformations efficient (e.g. in Bijectors). And it this would work with the existing nested array packages as well as JuliaLang/julia#32310 (when it lands). |
A couple questions coming from https://github.com/TuringLang/Bijectors.jl/discussions/178:
|
@ToucheSir It would absolutely be higher-dimensional for vectors-of-similar-matrices or matrices-of-similar-tensors, and so on. The flattened version of an N-dimensional array of M-dimensional arrays would be an N+M-dimensional array.
ArraysOfArrays currently provides a
I think it needs to be done Holy-traits fashion: I don't think we'll get a common super-type for all array-of-arrays implementations. I've asked about adding such a supertype in JuliaLang/julia#32310 a while ago already, but there has been no favourable response. And it would be good to support With a function like |
Is this also fixed by PR #775 ? |
Looks like it! |
#534 seems fixed as well. |
Weighted
mean
gives wrong (conjugated) result forcomplex
arrays:mean([1 + 0.5im], weights([1])) == 1.0 - 0.5im
.The text was updated successfully, but these errors were encountered: