Trilhas
/
Julia
Julia
/
Programa
/
Arrays multidimensionais
Ar

Arrays multidimensionais em Julia

2 exercícios

Sobre Arrays multidimensionais

Lá no começo, no conceito de Vetores, notamos que "arrays podem ter tamanho arbitrário (sujeitos apenas às restrições de memória do seu hardware) e podem ter quantas dimensões quiser."

Desde então, temos ignorado em grande parte os arrays com mais de uma dimensão, só para manter as coisas simples. Essa decisão faz mais sentido se você tentar ler os documentos de referência do Julia, na complexidade total deles.

No entanto, arrays com mais dimensões são muito, muito importantes em computação científica, então precisamos entendê-los.

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

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

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

Por conveniência, e para manter a consistência com a nomenclatura matemática, o Julia define alguns aliases de tipo: 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

Construindo arrays

Criamos muitos vetores colocando uma lista separada por vírgulas entre colchetes. Também podemos usar ponto e vírgula 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 espaço (ou tabulação) como separador, o resultado é diferente.

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

Agora o Julia define isso como uma Matrix 1×3 (em outros contextos, chamaríamos isso de row vector).

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

A referência a "coisas" é deliberadamente vaga, já que o Julia vai tentar trabalhar com o que você der a ele.

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 a mesma coisa, deixando mais explícito que se trata de concatenações horizontais e verticais. A generalização para dimensões maiores é 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

Digitar matrizes explícitas é convenientemente feito na ordem row-major, porque isso combina com a intuição humana (mais fácil de olhar, para culturas com texto horizontal):

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

No entanto, fique atento: o Julia (como Fortran, R e Matlab, mas ao contrário de C/C++ ou NumPy) armazena arrays de N dimensões em ordem column-major, e isso pode fazer uma enorme diferença de desempenho se você percorrer os elementos em um laço. Ajude o cache da sua CPU a ajudar você!

# 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 os inteiros de 1 a 6 e preenche uma Matrix 2×3 com eles, coluna por coluna.

Há 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

Gerando valores igualmente espaçados

Quem é fã de NumPy sabe que np.linspace é uma função amplamente usada para gerar um array de comprimento especificado, com extremos especificados e valores igualmente espaçados. Normalmente ela é usada como o eixo x de um gráfico, ou como a 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 estiver trabalhando localmente, com acesso ao pacote Plots, pode rodar 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 ser usado como um vetor, inclusive na conversão para uma coluna de matriz.

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

Indexação

Para um array 2-D, em geral usamos dois índices na 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 surpreenda: um índice único não é um erro, e retorna um único elemento.

A explicação remete ao comentário sobre a ordem column-major: o Julia desce pela coluna 1, depois pela coluna 2, até encontrar o 3º elemento na memória.

Cuidado: isso tem alguns usos ao escrever bibliotecas de propósito geral, mas é mais provável que confunda!

Uma questão parecida surge quando consultamos o tamanho de um array. length() dá o número total de elementos, size() dá uma tupla 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

Indexação com intervalos e arrays

Podemos copiar facilmente uma submatriz.

No exemplo abaixo, a função reshape() ajusta um array às dimensões especificadas, preenchendo coluna por coluna, e então o fatiarmos.

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 "copie tudo nesta dimensão".

Use 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 em um Vector, coluna por 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

Aplicando funções a um array

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

Isso também funciona em dimensões maiores. No entanto, podemos querer aplicar a função a apenas uma dimensão, por exemplo somando para baixo ou para os lados, para retornar 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 se torna singleton, então dims=1 => 1×3 e dims=2 => 2×1 nos exemplos acima.

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

Escrevendo funções que entendem dimensões

O argumento nomeado dims é comum em funções embutidas 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 isso será abordado com algum detalhe em um conceito mais adiante.

Alternativamente, o Julia fornece várias funções que nos deixam tratar arrays de N dimensões 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 isso é uma view do array original, o que nos permite trabalhar nele sem copiar. Arrays do Julia podem chegar a Terabytes de tamanho, então copiar pode ser um pesadelo de desempenho!

Uma view como essa pode ser usada para laços, broadcasting, ou todas as outras operações que vimos no programa de estudos até agora.

Além disso, comprehensions podem ser poderosas e versáteis com arrays como entrada. Casos simples foram mencionados no conceito de Laços, mas haverá uma discussão ampliada em um conceito mais adiante.

Broadcasting em múltiplas dimensões

Discutimos o caso 1-D no conceito de 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 pontuado .- trata o singleton 0.5 como equivalente a [0.5, 0.5, 0.5], fazendo broadcasting dele para casar as dimensões, e então faz a subtração elemento a elemento.

Estender isso para dimensões maiores é, na verdade, só mais do mesmo.

Por exemplo, ao fazer broadcasting da multiplicação de um vetor linha 2x1 [1.0 1.5 2.0] com a matriz 2x3 [1 2 3; 4 5 6], isso equivale 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 então 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

Observe: são as dimensões não singleton que devem casar em tamanho (por exemplo, 2x3 Matrix .* 2x1 Matrix) para o broadcasting.

Além disso, uma função pode ter broadcasting para 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

Você pode achar de início que o potencial de 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 arrays do NumPy se transfere muito bem para o Julia, apesar da sintaxe diferente.

Leitura adicional

Muitas operações comuns com vetores e matrizes caem sob o título geral de álgebra linear.

Há (atualmente) dois conceitos adicionais no site do Exercism, que existem apenas como documento até conseguirmos criar exercícios para acompanhá-los.

  1. Noções básicas de álgebra linear discute principalmente vários tipos de multiplicação.
  2. Resolução de equações lineares discute sistemas de equações lineares e como resolvê-los com eficiência.
Advanced

Talvez no futuro haja um terceiro conceito sobre fatorações de matrizes.

No entanto, fique atento: Noções básicas de álgebra linear é bastante matemático e pode não agradar a todos os estudantes.

Os conceitos posteriores (resolução de equações e fatorações) são mais avançados, e são voltados de fato para quem está à vontade com matemática de nível universitário.

Desculpe, mas essa é a natureza do assunto, e um caso de uso importante para o Julia.

Editar via GitHub O link abre em uma nova janela ou aba

Aprenda Arrays multidimensionais

A prática está bloqueada

Desbloqueie mais 1 exercício para praticar Arrays multidimensionais