Tensors
If you have reached here, you probably know what a tensor is, and probably have heard many jokes about what a tensor is[1]. Nevertheless, we are gonna give a brief remainder.
A tensor
In layman's terms, you can view a tensor as a linear function that maps a set of vectors to a scalar.
Just like with matrices and vectors,
The dimensions of the tensors are usually identified with labels and known as tensor indices or just indices. By appropeately fixing the indices in a expression, a lot of different linear algebra operations can be described.
For example, the trace operation...
... a tranposition of dimensions...
... or a matrix multiplication.
The Tensor
type
In Tenet
, a tensor is represented by the Tensor
type, which wraps an array and a list of index names. As it subtypes AbstractArray
, many array operations are automatically dispatched.
You can create a Tensor
by passing an AbstractArray
and a Vector
or Tuple
of Symbol
s.
julia> Tᵢⱼₖ = Tensor(rand(3,5,2), (:i,:j,:k))
3×5×2 Tensor{Float64, 3, Array{Float64, 3}}:
[:, :, 1] =
0.163435 0.641855 0.375458 0.967186 0.713751
0.00110075 0.026363 0.142351 0.822358 0.247281
0.101489 0.185081 0.0986353 0.730459 0.760425
[:, :, 2] =
0.383064 0.763941 0.461439 0.420327 0.316705
0.704078 0.41044 0.815594 0.624859 0.417672
0.103802 0.856713 0.711181 0.193854 0.993505
Use parent
and inds
to access the underlying array and indices respectively.
julia> parent(Tᵢⱼₖ)
3×5×2 Array{Float64, 3}:
[:, :, 1] =
0.163435 0.641855 0.375458 0.967186 0.713751
0.00110075 0.026363 0.142351 0.822358 0.247281
0.101489 0.185081 0.0986353 0.730459 0.760425
[:, :, 2] =
0.383064 0.763941 0.461439 0.420327 0.316705
0.704078 0.41044 0.815594 0.624859 0.417672
0.103802 0.856713 0.711181 0.193854 0.993505
julia> inds(Tᵢⱼₖ)
(:i, :j, :k)
The dimensionality or size of each index can be consulted using the size
function.
julia> size(Tᵢⱼₖ)
(3, 5, 2)
julia> size(Tᵢⱼₖ, :j)
5
julia> length(Tᵢⱼₖ)
30
Note that these indices are the ones that really define the dimensions of the tensor and not the order of the array dimensions.
julia> a = Tensor([1 0; 1 0], (:i, :j))
2×2 Tensor{Int64, 2, Matrix{Int64}}:
1 0
1 0
julia> b = Tensor([1 1; 0 0], (:j, :i))
2×2 Tensor{Int64, 2, Matrix{Int64}}:
1 1
0 0
julia> a ≈ b
true
This is key for interacting with other tensors.
julia> c = a + b
2×2 Tensor{Int64, 2, Matrix{Int64}}:
2 0
2 0
julia> parent(a) + parent(b)
2×2 Matrix{Int64}:
2 1
1 0
As such adjoint
doesn't permute the dimensions; it just conjugates the array.
julia> d = Tensor([1im 2im; 3im 4im], (:i, :j))
2×2 Tensor{Complex{Int64}, 2, Matrix{Complex{Int64}}}:
0+1im 0+2im
0+3im 0+4im
julia> d'
2×2 Tensor{Complex{Int64}, 2, Matrix{Complex{Int64}}}:
0-1im 0-2im
0-3im 0-4im
julia> conj(d)
2×2 Tensor{Complex{Int64}, 2, Matrix{Complex{Int64}}}:
0-1im 0-2im
0-3im 0-4im
Contraction
Einsum operations are performed automatically with contract
. Unlike other tensor libraries, the einsum pattern is not explicitly stated by the user but implicitly inferred from the Tensor
objects; i.e. repeated indices will be contracted while unique indices will remain. However, the user might require some flexibility on the output and contracted indices. That's why contract
has two extra keyword arguments: dims
, which lists the indices to be contracted, and out
, which lists the resulting indices after the contraction. Keep in mind that you're not forced to define them: dims
defaults to the repeated indices and out
defaults to the unique indices, but it's not recommended to define both.
For example, let's imagine that we want to perform the following operation: A sum over one dimension of a tensor.
contract
can act on just one tensor (unary contraction) and the user can write the following operation in two different ways:
julia> contract(a; dims=[:i])
2-element Tensor{Int64, 1, Vector{Int64}}:
2
0
julia> contract(a; out=[:j])
2-element Tensor{Int64, 1, Vector{Int64}}:
2
0
For the case of binary contraction, imagine the following matrix multiplication:
Then the default would be enough, although you can still define dims
or out
.
julia> contract(a, b)
0-dimensional Tensor{Int64, 0, Array{Int64, 0}}:
2
julia> contract(a, b; dims=[:i])
2-element Tensor{Int64, 1, Vector{Int64}}:
2
0
julia> contract(a, b; out=[:j])
2-element Tensor{Int64, 1, Vector{Int64}}:
2
0
But what if instead of contracting index :i
, we want to perform a Hadamard product (element-wise multiplication)? Then that's a case where implicit inference of the einsum rule is not enough and you need to specify dims
or out
.
julia> contract(a, b; dims=Symbol[])
2×2 Tensor{Int64, 2, Matrix{Int64}}:
1 0
1 0
julia> contract(a, b; out=[:i,:j])
2×2 Tensor{Int64, 2, Matrix{Int64}}:
1 0
1 0
Indexing
Tensor
, as a subtype of AbstractArray
, allows direct indexing of the underneath array with getindex
/setindex!
or the [...]
notation.
julia> a[1,1] = 3
3
julia> a[1,:]
2-element Vector{Int64}:
3
0
But like explained above, on Tensor
you should refer the dimensions by their index label, which Tenet allows in many methods.
julia> a[i=1,j=1]
3
Check out that not specifying all the indices is equivalent to using :
on the non-specified indices.
julia> a[i=1]
2-element Vector{Int64}:
3
0
julia> a[i=1,j=:]
2-element Vector{Int64}:
3
0
Other supported methods are permutedims
, selectdim
and view
.
julia> permutedims(a, [:j, :i])
2×2 Tensor{Int64, 2, Matrix{Int64}}:
3 1
0 0
julia> selectdim(a, :i, 1)
2-element Tensor{Int64, 1, SubArray{Int64, 1, Matrix{Int64}, Tuple{Int64, Base.Slice{Base.OneTo{Int64}}}, true}}:
3
0
julia> view(a, :i=>1)
2-element Tensor{Int64, 1, SubArray{Int64, 1, Matrix{Int64}, Tuple{Int64, Base.Slice{Base.OneTo{Int64}}}, true}}:
3
0
DocumenterMermaid.MermaidScriptBlock([...])
For example, recursive definitions like a tensor is whatever that transforms as a tensor. ↩︎
The order of a tensor may also be known as rank or dimensionality in other fields. However, these can be missleading, since it has nothing to do with the rank of linear algebra nor with the dimensionality of a vector space. Thus we prefer to use the word order. ↩︎
Meaning that the relationships between the output and the inputs, and the inputs between them, are linear. ↩︎