Broadcast for Array of Array

I’ve the below function

sigmoid(z::Real) = one(z) / (one(z) + exp(-z))

And want to broadcast it for all the element of x, where:

X = [
           [1, 2, 3],
           [3, 4, 5],
           [5, 6, 7]
       ]

I tried the normal way but failed

sigmoid.(X)

Broadcast works on the immediate elements of its arguments — in this case, the elements of X are themselves arrays, thus sigmoid.(X) is passing those inner arrays to sigmoid. You need to go down a second layer; often this is clearest with a comprehension:

[sigmoid.(inner_array) for inner_array in X]
3 Likes

Another way is to just do

(x -> sigmoid.(x)).(X)

This creates an anonymous function (x -> sigmoid.(x)) whose whole point is to broadcast sigmoid and then you can broadcast that function.

Not to beat a dead horse, but I do wish we had a cleaner way to do recursive broadcast. Maybe something like @. sigmoid(X) recursive where the flag recursive tells it to keep apply broadcasting until it reaches a scalar.

3 Likes

If wishes were horses, there’d be twenty different resurrected horse zombies running around, each doing broadcast just slightly differently. If you’ll pardon the mixed metaphor.

5 Likes

I think it’s kinda ironic that 3 hours ago the OP posted this thread: Sigmoid function in Julia - #4 by hasanOryx where he was told “don’t define elementwise functions on arrays, just use broadcast.” But now when he runs into a nested array it becomes clear that the broadcast machinery isn’t general enough to do what he wants.

I guess I just don’t understand why recursive broadcast is always dismissed out of hand.

1 Like

Because this is a #user:first-steps question. Let’s describe the status quo here, and not bog new users down with potential new and/or competing designs.

6 Likes

Maybe it should also be metioned that, instead of an array of arrays, you are often better off with one higher-dimensional array, in this case a matrix. Some languages don’t distinguish these concepts, but Julia does.

julia> M = [1 2 3; 3 4 5; 5 6 7]
3×3 Array{Int64,2}:
 1  2  3
 3  4  5
 5  6  7

julia> transpose(M) == reduce(hcat, X)
true

julia> sigmoid.(M)
3×3 Array{Float64,2}:
 0.731059  0.880797  0.952574
 0.952574  0.982014  0.993307
 0.993307  0.997527  0.999089
5 Likes

Thanks, actually the input come like an output of a previous operation if you can help me to fix it to match your approach, it will be very helpful:

a = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
x, y = split_seq(a, 3)

function split_seq(sequence::Array, n_steps::Int)
    x, y = [], []
    for (index, value) in enumerate(sequence)
        if index <= length(sequence) - n_steps
            seq_x, seq_y = sequence[index:index+n_steps-1], sequence[index+n_steps]
            push!(x, seq_x)
            push!(y, seq_y)
        end
    end
    return x, y
end

Then I get x as:

7-element Array{Any,1}:
 [1, 2, 3]
 [2, 3, 4]
 [3, 4, 5]
 [4, 5, 6]
 [5, 6, 7]
 [6, 7, 8]
 [7, 8, 9]

Here are some ways…

julia> reduce(hcat, @view a[i:i+2] for i=1:length(a)-2)
3×8 Array{Int64,2}:
 1  2  3  4  5  6  7   8
 2  3  4  5  6  7  8   9
 3  4  5  6  7  8  9  10

julia> reduce(vcat, a[i:i+2]' for i=1:length(a)-2)
8×3 Array{Int64,2}:
 1  2   3
 2  3   4
...

You could also just make a new array & fill it in with loops, which may well be fastest (if you care) but won’t fit on one line.

1 Like

I think I’d use a two-dimensional comprehension here:

split_seq(sequence, n) = [sequence[offset+column] for offset in 0:n-1, column in 1:length(sequence)-n+1]

Or if you really want to be clever —perhaps too clever — you can use broadcast to select your indices (but this won’t be quite as efficient as the above):

split_seq(sequence, n) = sequence[(0:n-1) .+ transpose(1:end-n+1)]
1 Like

Perfectly done, but did not get how this code generated a 2D array?

nice, so my function became:

function split_seq(sequence::Array, n_steps::Int)
    x = reduce(vcat, sequence[i:i+n_steps-1]' for i=1:length(sequence)-n_steps)
    y = reduce(vcat, sequence[i+n_steps]' for i=1:length(sequence)-n_steps)
    return x, y
end

https://docs.julialang.org/en/v1/manual/arrays/#Comprehensions-1

We could probably use an example for the multi-dimensional capability, but the trick is that there are two comma-separated for loops. It creates the Cartesian product of the two, using the first to generate the rows and the second to generate the columns.

julia> [row+10*col for row in 1:4, col in 1:5]
4×5 Array{Int64,2}:
 11  21  31  41  51
 12  22  32  42  52
 13  23  33  43  53
 14  24  34  44  54
2 Likes

Thanks, and hope you be patent with my questions:

I tried the below and got 2 different answers:

split_seq(sequence, n) = [sequence[offset+column] for offset in 0:n-1, column in 1:length(sequence)-n+1]

a = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
x = split_seq(a, 3)
@show x

Answer:

x = ([1 2 3; 2 3 4; 3 4 5; 4 5 6; 5 6 7; 6 7 8; 7 8 9], [4, 5, 6, 7, 8, 9, 10])

Other way, without creating a function:

sequence = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
z = [sequence[offset+column] for offset in 0:3-1, column in 1:length(sequence)-3+1]
@show z

Answer:

z = [1 2 3 4 5 6 7 8; 2 3 4 5 6 7 8 9; 3 4 5 6 7 8 9 10]

I believe that’s because you already have multiple split_seq methods defined in your current Julia session — and your original definition is taking precedence because it’s more specific. If you look at methods(split_seq), I believe you’ll see two (or more) things listed:

julia> methods(split_seq)
# 2 methods for generic function "split_seq":
[1] split_seq(sequence::Array, n_steps::Int64) in Main at REPL[100]:1
[2] split_seq(sequence, n) in Main at REPL[96]:1

For simple definitions like these, I wouldn’t put types on the arguments — you can rely on duck-typing to accept lots of arguments, and there’s no difference in speed/efficiency.

1 Like

Here is an issue with an idea for nested broadcasts, for those that want to discuss potential enhancements around this in Julia itself. The debate there is quite outdated, though.

Thanks a lot