Reductions

julia> using TensorCast

julia> M = collect(reshape(1:12, 3,4));

This is the basic syntax to sum over one index:

julia> @reduce S[i] := sum(j) M[i,j] + 1000
3-element Array{Int64,1}:
 4022
 4026
 4030

julia> @pretty @reduce S[i] := sum(j) M[i,j] + 1000
begin
    S = dropdims(sum(@__dot__(M + 1000), dims=2), dims=2)
end

Note that:

  • You must always specify the index to be summed over.
  • The sum applies to the whole right side (including the 1000 here).
  • And the summed dimensions are always dropped (unless you explicitly say S[i,_] := ...).

Not just sum

You may use any reduction funciton which understands keyword dims=..., like sum does. For example:

julia> using Statistics

julia> @reduce A[j] := mean(i) M[i,j]^2
4-element Array{Float64,1}:
   4.666666666666666
  25.666666666666664
  64.66666666666666 
 121.66666666666666 

If writing into an existing array, then the function must work like sum! does:

julia> @pretty @reduce A[j] = mean(i) M[i,j]^2
begin
    local pelican = PermuteDims(M)
    mean!(A, @__dot__(pelican ^ 2))
end

Otherwise all the same notation as @cast works. Here's a max-pooling trick with combined indices, in which we group the numbers in R into tiplets and keep the maximum of each set – equivalent to the maximum of each column below.

julia> R = collect(0:5:149);

julia> @reduce Rmax[_,c] := maximum(r) R[r\c]  r:3
1×10 LinearAlgebra.Transpose{Int64,Array{Int64,1}}:
 10  25  40  55  70  85  100  115  130  145

julia> reshape(R, 3,:)
3×10 Array{Int64,2}:
  0  15  30  45  60  75   90  105  120  135
  5  20  35  50  65  80   95  110  125  140
 10  25  40  55  70  85  100  115  130  145

Here r:3 indicates the range of r, implying c ∈ 1:10. You may also write maximum(r:3) R[r\c].

In the same way, this down-samples an image by a factor of 2, taking the mean of each 2×2 block of pixels, in each colour c:

julia> @reduce smaller[I,J,c] := mean(i:2, j:2) orig[i\I, j\J, c]

Scalar output

Here are different ways to write complete reduction:

julia> @reduce Z := sum(i,j) M[i,j]      # Z = sum(M)
78

julia> Z = @reduce sum(i,j) M[i,j]
78

julia> @reduce Z0[] := sum(i,j) M[i,j]   # Z0 = dropdims(sum(M, dims=(1, 2)), dims=(1, 2))
0-dimensional Array{Int64,0}:
78

julia> @reduce Z1[_] := sum(i,j) M[i,j]
1-element Array{Int64,1}:
 78

Recursion

If you want to sum only part of an expression, or have one sum inside another, then you can place a complete @reduce expression inside @cast or @reduce. There is no need to name the intermediate array, here termite[x], but you must show its indices:

julia> @pretty @reduce sum(x,θ) L[x,θ] * p[θ] * log(L[x,θ] / @reduce [x] := sum(θ′) L[x,θ′] * p[θ′])
begin
    local pelican = orient(p, (*, :))
    termite = dropdims(sum(@__dot__(L * pelican), dims=2), dims=2)
    local caterpillar = orient(p, (*, :))
    goat = sum(@__dot__(L * caterpillar * log(L / termite)))
end

Notice that the inner sum here is a matrix-vector product, so it will be more efficient to write @matmul [x] := sum(θ') L[x,θ′] * p[θ′] to call L * p instead of broadcasting.

Matrix multiplication

It's possible to multiply matrices using @reduce, but this is exceptionally inefficient, as it first broadcasts out a 3-tensor before summing over one index:

julia> @pretty @reduce R[i,k] := sum(j) M[i,j] * N[j,k]
begin
    local pelican = orient(N, (*, :, :))
    R = dropdims(sum(@__dot__(M * pelican), dims=2), dims=2)
end

The macro @matmul has the same syntax, but instead calls A*B.

julia> A = rand(100,100); B = rand(100,100);

julia> red(A,B) = @reduce R[i,k] := sum(j) A[i,j] * B[j,k];

julia> mul(A,B) = @matmul P[i,k] := sum(j) A[i,j] * B[j,k];

julia> using BenchmarkTools

julia> @btime red($A, $B);
  1.471 ms (25 allocations: 7.71 MiB)

julia> @btime mul($A, $B);
  33.489 μs (2 allocations: 78.20 KiB)

Of course you may just write A * B yourself, but in general @matmul will handle the same reshaping, transposing, fixed indices, etc. steps as @reduce does. Once all of that is done, however, the result must be a product like A * B in which the indices being summed over appear on both tensors. <!– If there are more than two factors, then multiplication proceeds from the left, (A * B) * C. But that has bugs! –>

To use the more flexible @reduce in cases like this, when creating the large intermediate tensor will be expensive, the option lazy which creates a LazyArrays.BroadcastArray instead can help:

julia> lazyred(A,B) = @reduce R[i,k] := sum(j) A[i,j] * B[j,k]  lazy;

julia> @btime lazyred($A, $B);
  223.172 μs (26 allocations: 78.95 KiB) 

More details on the next page.

No reduction

The syntax of @reduce can also be used with @cast, to apply functions which take a dims keyword, but do not produce trivial dimensions.

julia> normalise(p; dims) = p ./ sum(p; dims=dims);

julia> @cast N[x,y] := normalise(x) M[x,y]
3×4 Array{Float64,2}:
 0.166667  0.266667  0.291667  0.30303
 0.333333  0.333333  0.333333  0.333333
 0.5       0.4       0.375     0.363636