Skip to content

Commit

Permalink
Add a tile procedure
Browse files Browse the repository at this point in the history
This procedure lets you construct a new tensor by repeating the input tensor a number of times on one or more axes. This is similar to numpy's `tile` and Matlab's `repmat` functions.

I measured the performance using the `timeit` library. The results show that the performance is comparable to (but not as good as) numpy's `tile`. In particular, a small example which takes numpy's `tile` ~3-4 usec per iteration, takes ~8-9 usec in --d:release mode, and ~5-6 usec in --d:danger mode.

I believe that the performance could be improved further by preallocating the result Tensor before the tiling operation. The current implementation is not as efficient as it could be because it is based on calling `concat` multiple times, which requires at least as many tensor allocations (of increasing size).
  • Loading branch information
AngelEzquerra committed Apr 21, 2024
1 parent e34dc35 commit 535db47
Show file tree
Hide file tree
Showing 2 changed files with 117 additions and 0 deletions.
64 changes: 64 additions & 0 deletions src/arraymancer/tensor/shapeshifting.nim
Original file line number Diff line number Diff line change
Expand Up @@ -687,3 +687,67 @@ proc repeat_values*[T](t: Tensor[T], reps: Tensor[int]): Tensor[T] {.noinit, inl
## version.
## ```
t.repeat_values(reps.toSeq1D)

proc tile*[T](t: Tensor[T], reps: varargs[int]): Tensor[T] =
## Construct a new tensor by repeating the input tensor a number of times on one or more axes
##
## Inputs:
## - t: The tensor to repeat
## - reps: One or more integers indicating the number of times to repeat
## the tensor on each axis (starting with axis 0)
##
## Result:
## - A new tensor whose shape is `t.shape *. reps`
##
## Notes:
## - If a rep value is 1, the tensor is not repeated on that particular axis
## - If there are more rep values than the input tensor has axes, additional
## dimensions are prepended to the input tensor as needed. Note that this
## is similar to numpy's `tile` function behavior, but different to
## Matlab's `repmat` behavior, which appends missing dimensions instead
## of prepending them.
## - This function behavior is similar to nims `sequtils.repeat`, in that
## it repeats the full tensor multiple times. If what you want is to
## repeat the _elements_ of the tensor multiple times, rather than the
## full tensor, use the `repeat_values` procedure instead.
##
## Examples:
## ```nim
## let x = arange(4).reshape(2, 2)
##
## # When the number of reps and tensor dimensions match, the ouptut tensor
## # shape is the `reps *. t.shape`
## echo tile(x, 2, 3)
## > Tensor[system.int] of shape "[4, 6]" on backend "Cpu"
## > |0 1 0 1 0 1|
## > |2 3 2 3 2 3|
## > |0 1 0 1 0 1|
## > |2 3 2 3 2 3|
##
## # If there are fewer reps than tensor dimensions, start
## # repeating on the first axis (leaving alone axis with missing reps)
## echo tile(x, 2)
## > Tensor[system.int] of shape "[4, 2]" on backend "Cpu"
## > |0 1|
## > |2 3|
## > |0 1|
## > |2 3|
##
## # If there are more reps than tensor dimensions, prepend the missing
## # dimensions before repeating
## echo tile(x, 1, 2, 3)
## > Tensor[system.int] of shape "[1, 4, 6]" on backend "Cpu"
## > 0
## > |0 1 0 1 0 1|
## > |2 3 2 3 2 3|
## > |0 1 0 1 0 1|
## > |2 3 2 3 2 3|
## ```
result = t
for ax in countdown(reps.high, 0):
var concat_seq = repeat(result, reps[ax])
if ax >= result.shape.len:
# mutate the repeated tensors to have one more axis
concat_seq.applyIt(unsqueeze(it, 0))
result = concat(concat_seq, axis=ax)

53 changes: 53 additions & 0 deletions tests/tensor/test_shapeshifting.nim
Original file line number Diff line number Diff line change
Expand Up @@ -344,5 +344,58 @@ proc main() =
check: a.repeat_values([1, 0, 3, 2]) == expected
check: a.repeat_values([1, 0, 3, 2].toTensor) == expected

test "Tile":
let t = arange(6).reshape(2, 3)

block: # Tile over the first axis
let expected = [
[0, 1, 2],
[3, 4, 5],
[0, 1, 2],
[3, 4, 5],
].toTensor
check: t.tile(2) == expected

block: # Tile over the all the axis of the input tensor
let expected = [
[0, 1, 2, 0, 1, 2, 0, 1, 2],
[3, 4, 5, 3, 4, 5, 3, 4, 5],
[0, 1, 2, 0, 1, 2, 0, 1, 2],
[3, 4, 5, 3, 4, 5, 3, 4, 5]
].toTensor
check: t.tile(2, 3) == expected

block: # Tile over the more axis than the input tensor has
let expected = [
[
[0, 1, 2, 0, 1, 2, 0, 1, 2],
[3, 4, 5, 3, 4, 5, 3, 4, 5],
[0, 1, 2, 0, 1, 2, 0, 1, 2],
[3, 4, 5, 3, 4, 5, 3, 4, 5]
],
[
[0, 1, 2, 0, 1, 2, 0, 1, 2],
[3, 4, 5, 3, 4, 5, 3, 4, 5],
[0, 1, 2, 0, 1, 2, 0, 1, 2],
[3, 4, 5, 3, 4, 5, 3, 4, 5]
],
[
[0, 1, 2, 0, 1, 2, 0, 1, 2],
[3, 4, 5, 3, 4, 5, 3, 4, 5],
[0, 1, 2, 0, 1, 2, 0, 1, 2],
[3, 4, 5, 3, 4, 5, 3, 4, 5]
],
[
[0, 1, 2, 0, 1, 2, 0, 1, 2],
[3, 4, 5, 3, 4, 5, 3, 4, 5],
[0, 1, 2, 0, 1, 2, 0, 1, 2],
[3, 4, 5, 3, 4, 5, 3, 4, 5]
]
].toTensor
check: t.tile(4, 2, 3) == expected

block: # tiling and repeating values are sometimes equivalent
check: t.tile(2, 1, 1) == t.unsqueeze(axis=0).repeat_values(2, axis = 0)

main()
GC_fullCollect()

0 comments on commit 535db47

Please sign in to comment.