Introduction
The idea for rray sprung from frustration with the following example:
x <- matrix(1:6, nrow = 3)
x
#> [,1] [,2]
#> [1,] 1 4
#> [2,] 2 5
#> [3,] 3 6
x + 1
#> [,1] [,2]
#> [1,] 2 5
#> [2,] 3 6
#> [3,] 4 7
y <- matrix(1)
x + y
#> Error in `x + y`:
#> ! non-conformable arraysTo someone who has worked with matrices in R before, this error
message is probably nothing new. It’s stating that because
x has dimensions (3, 2) and is being added to
an object with dimensions (1, 1), which is a matrix that
does not have exactly the same dimensions, the operation cannot
be completed. My frustration lies with the fact that I had this
“feeling” that I knew the answer to this operation. It shouldn’t be an
error, it should have the same answer as x + 1. Why does
this work, when the other doesn’t?
In one sentence, the answer is that R recycles, but I want it to broadcast.
This vignette’s goal is to introduce the concept of broadcasting. Broadcasting is the idea of repeating dimensions of one object to match the dimensions of another, so that an operation can be applied between the two inputs. Later you will learn two rules which formalize this idea.
What did I expect?
For this first example, I expected this result:
x
#> [,1] [,2]
#> [1,] 1 4
#> [2,] 2 5
#> [3,] 3 6
x + 1
#> [,1] [,2]
#> [1,] 2 5
#> [2,] 3 6
#> [3,] 4 7
rray_add(x, y)
#> [,1] [,2]
#> [1,] 2 5
#> [2,] 3 6
#> [3,] 4 7rray implements broadcasting throughout the package, which is what allows this to work. It solves this motivating example, but is generally useful in variety of ways.
Rules
Broadcasting has two steps:
- The dimensionality of the inputs are matched by appending 1’s to the input with a lower dimensionality.
- The dimensions of the inputs are matched by recycling the dimension of each axis as needed.
By recycling, we mean that an axis of dimension 1 can be repeated to any other dimension. If you’re familiar with the tidyverse recycling rules, then this should feel familiar. It’s the same idea, just applied to all axes of the array.
Example - x + y
Let’s revisit the original example, but use the broadcasting rules to understand how we got the result.
x
#> [,1] [,2]
#> [1,] 1 4
#> [2,] 2 5
#> [3,] 3 6
y
#> [,1]
#> [1,] 1When adding these two matrices together, it’s useful to write out the dimensions explicitly, using the general notation of:
(rows, cols) | object
(3, 2) | x
(1, 1) | y
-------|-------
(3, 2) | result
To understand how we get from x + y -> result,
compare axes vertically, and apply the broadcasting rules.
-
1st dimension:
-
xhas3rows -
yhas1row - Recycle the 1 to 3
-
-
2nd dimension:
-
xhas2column -
yhas1column - Recycle the 1 to 2
-
( 3, 2) | x
(1 -> 3, 1 -> 2) | y
You can explicitly see these rules in action with
rray_dimensions_common()
dimensions <- rray_dimensions_common(x, y)
dimensions
#> [1] 3 2And you can explicitly broadcast with rray_broadcast(),
which generates “conformable” arrays that base R can add together:
x_broadcast <- rray_broadcast(x, dimensions)
x_broadcast
#> [,1] [,2]
#> [1,] 1 4
#> [2,] 2 5
#> [3,] 3 6
y_broadcast <- rray_broadcast(y, dimensions)
y_broadcast
#> [,1] [,2]
#> [1,] 1 1
#> [2,] 1 1
#> [3,] 1 1
x_broadcast + y_broadcast
#> [,1] [,2]
#> [1,] 2 5
#> [2,] 3 6
#> [3,] 4 7The downside of rray_broadcast() is that this
materializes the fully broadcast version of y in memory.
rray_add() instead iterates over both x and
y without materializing the intermediate result, which is
much more efficient!
Example - Implicit dimensions
If y was a 1-D array, not a 2-D matrix, what would have
changed?
x
#> [,1] [,2]
#> [1,] 1 4
#> [2,] 2 5
#> [3,] 3 6
y <- array(c(1L, 2L, 3L))
y
#> [1] 1 2 3
rray_add(x, y)
#> [,1] [,2]
#> [1,] 2 5
#> [2,] 4 7
#> [3,] 6 9This operation is still valid, and represents the first broadcasting
rule in action - the dimensionality of the inputs are matched
by appending 1’s to the input with a lower dimensionality.
Here, that’s y:
(3, 2) | x
(3) | y
-------|-------
(3, 2) | result
x has a dimensionality of 2, y has a
dimensionality of 1, so we append 1s to the right hand side of
y until dimensionalities match:
(3, 2) | x
(3, 1) | y
-------|-------
(3, 2) | result
Now we apply recycling to y 2nd axis:
(3, 2) | x
(3, 2) | y
-------|-------
(3, 2) | result
Explicitly:
y
#> [1] 1 2 3
dimensions <- rray_dimensions_common(x, y)
dimensions
#> [1] 3 2
rray_broadcast(y, dimensions)
#> [,1] [,2]
#> [1,] 1 1
#> [2,] 2 2
#> [3,] 3 3Example - Higher dimensions
Broadcasting becomes more interesting when higher dimensional objects are used. Additionally, at this point only 1 object at a time has needed altering. This example will require both inputs to be changed.
a <- array(1:3, c(1, 3))
a
#> [,1] [,2] [,3]
#> [1,] 1 2 3
b <- array(1:8, c(4, 1, 2))
b
#> , , 1
#>
#> [,1]
#> [1,] 1
#> [2,] 2
#> [3,] 3
#> [4,] 4
#>
#> , , 2
#>
#> [,1]
#> [1,] 5
#> [2,] 6
#> [3,] 7
#> [4,] 8Perhaps surprisingly, these inputs can be added together using broadcasting.
rray_add(a, b)
#> , , 1
#>
#> [,1] [,2] [,3]
#> [1,] 2 3 4
#> [2,] 3 4 5
#> [3,] 4 5 6
#> [4,] 5 6 7
#>
#> , , 2
#>
#> [,1] [,2] [,3]
#> [1,] 6 7 8
#> [2,] 7 8 9
#> [3,] 8 9 10
#> [4,] 9 10 11To understand this, write out dimensions and follow the rules:
(1, 3) | a
(4, 1, 2) | b
Match dimensionalities by appending 1s to
a:
(1, 3, 1) | a
(4, 1, 2) | b
and then recycle the axes with dimension 1 within each vertical pair:
(1 -> 4, 3, 1 -> 2) | a
( 4, 1 -> 3, 2) | b
Written compactly:
(1, 3) | a
(4, 1, 2) | b
--------- | ------
(4, 3, 2) | result
Addendum
Base R Recycling
The only exception to the strict behavior of base R is when an array is combined with a 1D vector. “Scalar” operations work as one might expect:
x
#> [,1] [,2]
#> [1,] 1 4
#> [2,] 2 5
#> [3,] 3 6
x * 2
#> [,1] [,2]
#> [1,] 2 8
#> [2,] 4 10
#> [3,] 6 12But when a vector and an array are combined, the vector is recycled in its 1D flat form to fit the total size of the array. This is very different from broadcasting.
vec <- c(1, 2, 3)
x
#> [,1] [,2]
#> [1,] 1 4
#> [2,] 2 5
#> [3,] 3 6
x + vec
#> [,1] [,2]
#> [1,] 2 5
#> [2,] 4 7
#> [3,] 6 9
# Equivalent to:
rep(vec, 2)
#> [1] 1 2 3 1 2 3
x + rep(vec, 2)
#> [,1] [,2]
#> [1,] 2 5
#> [2,] 4 7
#> [3,] 6 9This example seems intuitive and harmless, but even partial recycling is allowed, which can be quite confusing and dangerous.
vec <- c(1, 2)
x + vec
#> [,1] [,2]
#> [1,] 2 6
#> [2,] 4 6
#> [3,] 4 8This is identical to the following, where vec is
repeatedly recycled to construct something that can be added to
x.
recycled <- matrix(NA, 3, 2)
recycled[1, 1] <- vec[1]
recycled[2, 1] <- vec[2]
recycled[3, 1] <- vec[1]
recycled[1, 2] <- vec[2]
recycled[2, 2] <- vec[1]
recycled[3, 2] <- vec[2]
recycled
#> [,1] [,2]
#> [1,] 1 2
#> [2,] 2 1
#> [3,] 1 2
x + recycled
#> [,1] [,2]
#> [1,] 2 6
#> [2,] 4 6
#> [3,] 4 8rray never performs partial recycling when it is going through the steps of broadcasting, and will error if this example is attempted.
rray_add(x, vec)
#> Error in `rray_add()`:
#> ! Can't find common dimensions at axis 1. `x` has dimension 3 and `y` has dimension 2.Differences from Python
In the Python world, NumPy is used for broadcasting operations. One
interesting thing to note is that when applying broadcasting rules for
NumPy, implicit dimensions are prepended to the left hand side
as 1s. This is used consistently in the NumPy world, even
down to how they are printed. However, R already uses the concept of
appended implicit dimensions on the right hand side,
and prints with this concept in mind.
For a simple example of R doing this, try coercing a vector to a matrix:
# (3) -> (3, 1)
as.matrix(1:3)
#> [,1]
#> [1,] 1
#> [2,] 2
#> [3,] 3The dimensions went from (3) to (3, 1).
NumPy actually does the opposite, and would convert (3) to
(1, 3). This is fine, because they use it consistently
there, but can be confusing when comparing broadcasting of rray to
NumPy, and is just something to keep in mind.
