Percursos
/
Julia
Julia
/
Programa
/
Arrays multidimensionais
Ar

Arrays multidimensionais em Julia

2 exercícios

Sobre Arrays multidimensionais

Muito atrás, no Conceito Vetores, notámos que «os arrays podem ter tamanho arbitrário (sujeitos apenas às restrições de memória do teu hardware) e podem ter um número arbitrário de dimensões.»

Desde então, temos ignorado em grande parte os arrays com mais de uma dimensão, só para manter as coisas simples. Esta decisão fará mais sentido se tentares ler a documentação de referência do Julia, na sua complexidade total.

No entanto, os arrays de dimensão superior são muito, muito importantes na computação científica, por isso precisamos de os compreender.

Nomenclatura: Seguindo séculos de precedente matemático, referimo-nos aos arrays 1-D como Vectors e aos arrays 2-D como Matrices.

Os exemplos neste documento serão, na sua maioria, matrizes. Trabalhar com 3 ou mais dimensões é sintaticamente quase idêntico, mas a saída é difícil e confusa de ler (num ecrã 2-D).

O type de um array N-dimensional de eltype T é Array{T, N}.

Por conveniência, e por consistência com a nomenclatura matemática, o Julia define alguns aliases de tipos: Vector{T} para Array{T, 1} e Matrix{T} para Array{T, 2}.

# 3-D array
julia> m3 = ones(2, 3, 4);

julia> typeof(m3)
Array{Float64, 3}

# Vector
julia> v = [1, 2]
2-element Vector{Int64}:
 1
 2

julia> typeof(v) == Array{Int64, 1}
true

# Matrix
julia> m
2×3 Matrix{Int64}:
 1  2  3
 4  5  6

julia> typeof(m)
Matrix{Int64} (alias for Array{Int64, 2})

julia> typeof(m) == Array{Int64, 2}
true

Construir arrays

Já criámos imensos vetores ao colocar uma lista separada por vírgulas entre parênteses retos. Também se podem usar pontos e vírgulas como separador.

julia> v = [1, 2, 3]
3-element Vector{Int64}:
 1
 2
 3

julia> w = [1; 2; 3]
3-element Vector{Int64}:
 1
 2
 3

julia> v == w
true

Se usarmos um espaço (ou um tabulador) como separador, o resultado é diferente.

julia> u = [1 2 3]
1×3 Matrix{Int64}:
 1  2  3

O Julia define agora isto como uma Matriz 1×3 (noutros contextos, chamar-lhe-íamos um row vector).

Em geral, os espaços juntam as coisas na horizontal e os pontos e vírgulas (ou as mudanças de linha) juntam-nas na vertical.

A referência a «coisas» é deliberadamente vaga, porque o Julia vai tentar trabalhar com tudo o que lhe deres.

julia> [v 2v]
3×2 Matrix{Int64}:
 1  2
 2  4
 3  6

julia> [v; 2v]
6-element Vector{Int64}:
 1
 2
 3
 2
 4
 6

Existem as funções hcat() e vcat() que fazem o mesmo, tornando mais explícito que se trata de concatenações horizontais e verticais. A generalização para dimensões superiores é a função cat().

jjulia> hcat(v, 2v)
3×2 Matrix{Int64}:
 1  2
 2  4
 3  6

julia> vcat(v, 2v)
6-element Vector{Int64}:
 1
 2
 3
 2
 4
 6

Escrever matrizes explícitas faz-se com conveniência por ordem de linhas, porque isso encaixa na intuição humana (é mais fácil de olhar para elas, nas culturas com texto horizontal):

julia> m = [1 2 3; 4 5 6]
2×3 Matrix{Int64}:
 1  2  3
 4  5  6

No entanto, tem em atenção que o Julia (tal como o Fortran, o R e o Matlab, mas ao contrário do C/C++ ou do NumPy) armazena os arrays de N dimensões em ordem por colunas, e isto pode fazer uma diferença enorme de desempenho se percorreres os elementos num ciclo. Ajuda a cache da tua CPU para que ela te ajude!

# put these integers in 2 rows and 3 columns

julia> reshape(collect(1:6), 2, 3)
2×3 Matrix{Int64}:
 1  3  5
 2  4  6

O exemplo acima pega nos números inteiros de 1 a 6 e preenche com eles uma Matriz 2×3, coluna a coluna.

Existem várias funções utilitárias para construir tipos comuns de array (uniformes ou aleatórios).

julia> zeros(2, 3)  # see also ones()
2×3 Matrix{Float64}:
 0.0  0.0  0.0
 0.0  0.0  0.0

julia> falses(2, 2)  # booleans, see also trues()
2×2 BitMatrix:
 0  0
 0  0

julia> rand(Float32, 2, 3)  # random numbers in the interval [0, 1)
2×3 Matrix{Float32}:
 0.768823  0.169633  0.632565
 0.388451  0.109176  0.850381

Gerar valores igualmente espaçados

Quem gosta de NumPy saberá que np.linspace é uma função muito usada para gerar um array de comprimento especificado, com extremos especificados e valores igualmente espaçados. Costuma usar-se como eixo dos x de um gráfico, ou como variável independente em modelos lineares.

O Julia não tem um equivalente exato, mas a função range(), muito flexível, consegue imitá-la especificando os limites inferior e superior, mais o argumento nomeado length.

Quem esteja a trabalhar localmente, com acesso ao pacote Plots, pode executar este código:

julia> using Plots

julia> x = range(0, 2π; length=100)
0.0:0.06346651825433926:6.283185307179586

julia> typeof(x)
StepRangeLen{Float64, Base.TwicePrecision{Float64}, Base.TwicePrecision{Float64}, Int64}

julia> vals = [x sin.(x) cos.(x)]
100×3 Matrix{Float64}:
 0.0         0.0           1.0
 0.0634665   0.0634239     0.997987
 0.126933    0.126592      0.991955
 0.1904      0.189251      0.981929
(...truncated)

julia> plot(vals[:, 1], vals[:, 2:3])

O StepRangeLen pode usar-se como um vetor, incluindo a conversão para uma coluna de uma matriz.

Vê também a função equivalente logrange(), para gerar valores igualmente espaçados num eixo logarítmico.

Indexação

Para um array 2-D, usamos geralmente dois índices pela ordem [row, col].

julia> m
2×3 Matrix{Int64}:
 1  2  3
 4  5  6

julia> m[1, 2] # row 1, col 2
2

# Stay within bounds! There is no row 3.
julia> m[3, 1]
ERROR: BoundsError: attempt to access 2×3 Matrix{Int64} at index [3, 1]

julia> m[3]
2

O último exemplo é, talvez, surpreendente: um índice único não é um erro e devolve um único elemento.

A explicação remete para o comentário sobre a ordem por colunas: o Julia desce pela coluna 1, depois pela coluna 2, até encontrar o 3.º elemento em memória.

Tem cuidado: isto tem algumas utilizações quando escreves bibliotecas de uso geral, mas é mais provável que seja confuso!

Surge um problema semelhante quando consultas o tamanho de um array. length() dá o número total de elementos, size() dá um tuplo de ndims() elementos com o comprimento de cada dimensão.

julia> m
2×3 Matrix{Int64}:
 1  2  3
 4  5  6

julia> length(m)
6

julia> size(m)  # 2 rows, 3 cols
(2, 3)

julia> ndims(m)  # how many dimensions? Like `m |> size |> length`
2

Indexar com intervalos e arrays

Podemos copiar facilmente uma submatriz.

No exemplo abaixo, a função reshape() força um array a ter as dimensões especificadas, preenchendo coluna a coluna, e depois extraímos uma fatia.

julia> m12 = reshape(collect(1:12), 4, 3)
4×3 Matrix{Int64}:
 1  5   9
 2  6  10
 3  7  11
 4  8  12

julia> m12[2:4, 1:2]
3×2 Matrix{Int64}:
 2  6
 3  7
 4  8

# some rows, all columns
julia> m12[2:4, :]
3×3 Matrix{Int64}:
 2  6  10
 3  7  11
 4  8  12

Um : sozinho significa «copiar tudo nesta dimensão».

Usa vetores para linhas/colunas não consecutivas:

julia> m12[[1, 3], :]  # rows 1 and 3
2×3 Matrix{Int64}:
 1  5   9
 3  7  11

Um : sozinho achata qualquer array num vetor, coluna a coluna.

julia> m
2×3 Matrix{Int64}:
 1  2  3
 4  5  6

julia> m[:]
6-element Vector{Int64}:
 1
 4
 2
 5
 3
 6

Aplicar funções a um array

Já vimos anteriormente funções de agregação como sum() e maximum() aplicadas a coleções 1-D, em que operam sobre todos os elementos e devolvem um resultado escalar.

Isto também funciona em dimensões superiores. No entanto, podemos querer aplicar a função a apenas uma dimensão, por exemplo somando para baixo ou para o lado, de modo a devolver um array com uma singleton dimension de tamanho 1.

Para isso, existe um argumento nomeado opcional dims.

julia> m
2×3 Matrix{Int64}:
 1  2  3
 4  5  6

julia> sum(m)  # sum everything
21

julia> sum(m; dims=1)  # sum down
1×3 Matrix{Int64}:
 5  7  9

julia> sum(m; dims=2)  # sum across
2×1 Matrix{Int64}:
  6
 15

# other aggregation functions are similar
julia> maximum(m; dims=2)
2×1 Matrix{Int64}:
 3
 6

O valor de dims é a dimensão que passa a ter tamanho 1, por isso dims=1 => 1×3 e dims=2 => 2×1 nos exemplos acima.

Por extensão, os arrays de dimensão superior podem reduzir várias dimensões, se dims for definido como um array ou um intervalo (mais uma vez, compreender a saída pode exigir alguma reflexão!).

Escrever funções conscientes das dimensões

O argumento nomeado dims é comum em funções incorporadas como sum(), mas como escrevemos algo equivalente no nosso próprio código?

Uma boa resposta é usar funções de ordem superior como reduce(), e isto será abordado com algum detalhe num Conceito posterior.

Em alternativa, o Julia fornece várias funções que nos permitem tratar arrays N-dimensionais como se fossem vetores aninhados de vetores.

Para matrizes, eachrow() e eachcol() são convenientes, mas a função mais geral é eachslice(), que consegue lidar com dimensões arbitrárias.

# m is as in the previous examples

julia> eachrow(m)
2-element RowSlices{Matrix{Int64}, Tuple{Base.OneTo{Int64}}, SubArray{Int64, 1, Matrix{Int64}, Tuple{Int64, Base.Slice{Base.OneTo{Int64}}}, true}}:
 [1, 2, 3]
 [4, 5, 6]

julia> eachcol(m)
3-element ColumnSlices{Matrix{Int64}, Tuple{Base.OneTo{Int64}}, SubArray{Int64, 1, Matrix{Int64}, Tuple{Base.Slice{Base.OneTo{Int64}}, Int64}, true}}:
 [1, 4]
 [2, 5]
 [3, 6]

O tipo é um pouco assustador, mas só porque isto é uma vista sobre o array original, que nos permite trabalhar nele sem copiar. Os arrays do Julia podem ter potencialmente terabytes de tamanho, por isso copiar pode ser um pesadelo de desempenho!

Uma vista como esta pode ser usada para iterar, fazer broadcasting ou para todas as outras operações que vimos no currículo até agora.

Além disso, as compreensões de arrays podem ser poderosas e versáteis quando a entrada é um array. Os casos simples foram mencionados no conceito Ciclos, mas haverá uma discussão mais desenvolvida num conceito posterior.

Broadcasting em múltiplas dimensões

Discutimos o caso 1-D no conceito Operações com Vetores.

julia> v = [1.2, 1.5, 1.7]
3-element Vector{Float64}:
 1.2
 1.5
 1.7

julia> v .- 0.5
3-element Vector{Float64}:
 0.7
 1.0
 1.2

O operador com ponto .- trata o escalar 0.5 como equivalente a [0.5, 0.5, 0.5], ao fazer broadcasting dele para corresponder às dimensões, e depois faz a subtração elemento a elemento.

Estender isto a dimensões superiores é, na verdade, apenas mais do mesmo.

Por exemplo, ao fazer broadcasting da multiplicação de um vetor linha 2x1 [1.0 1.5 2.0] pela matriz 2x3 [1 2 3; 4 5 6], é equivalente a fazer broadcasting de [1.0 1.5 2.0] para [1.0 1.5 2.0; 1.0 1.5 2.0], com a multiplicação feita depois elemento a elemento.

julia> m
2×3 Matrix{Int64}:
 1  2  3
 4  5  6

julia> m .* [1.0 1.5 2.0]  # broadcast a 1x3 row vector down
2×3 Matrix{Float64}:
 1.0  3.0   6.0
 4.0  7.5  12.0

julia> m .* [1.0 1.5 2.0; 1.0 1.5 2.0]  # 2x3 elementwise multiplication
2×3 Matrix{Float64}:
 1.0  3.0   6.0
 4.0  7.5  12.0

julia> m ./ [1, 2]  # broadcast a 2x1 column vector across
2×3 Matrix{Float64}:
 1.0  2.0  3.0
 2.0  2.5  3.0

julia> m ./ [1 1 1; 2 2 2]  # 2x3 elementwise division
2×3 Matrix{Float64}:
 1.0  2.0  3.0
 2.0  2.5  3.0

Nota: São as dimensões que não têm tamanho 1 que devem coincidir em tamanho (por exemplo, 2x3 Matrix .* 2x1 Matrix) para o broadcasting.

Além disso, uma função pode ser aplicada por broadcasting a cada elemento de uma Matrix exatamente como se faz com um Vector.

julia> m
2×3 Matrix{Int64}:
 1  2  3
 4  5  6

julia> (x -> x^2).(m)  # broadcast a function to all elements
2×3 Matrix{Int64}:
  1   4   9
 16  25  36

Podes ter a sensação, ao início, de que o âmbito para a confusão do programador cresce exponencialmente com o número de dimensões, mas a prática ajuda muito. Além disso, a familiaridade com os arrays do NumPy transfere-se bastante bem para o Julia, apesar da sintaxe diferente.

Leitura adicional

Muitas operações comuns sobre vetores e matrizes inserem-se no âmbito geral da Álgebra Linear.

Existem (atualmente) dois Conceitos adicionais no site do Exercism, que são apenas documentação até conseguirmos conceber exercícios para os acompanhar.

  1. Noções Básicas de Álgebra Linear aborda principalmente vários tipos de multiplicação.
  2. Resolução de Equações Lineares aborda sistemas de equações lineares e como os resolver de forma eficiente.
Advanced

É possível que, no futuro, exista um terceiro Conceito sobre fatorizações de matrizes.

No entanto, tem em atenção que Noções Básicas de Álgebra Linear é bastante matemático e pode não servir para todos os estudantes.

Os conceitos posteriores (resolução de equações e fatorizações) são mais avançados e destinam-se apenas a quem esteja à vontade com matemática de nível universitário.

As nossas desculpas, mas é a natureza do tema e um caso de uso importante do Julia.

Editar via GitHub A ligação abre numa nova janela ou separador

Aprende Arrays multidimensionais

A prática está bloqueada

Desbloqueia mais 1 exercício para praticares Arrays multidimensionais