Tracce
/
Julia
Julia
/
Programma
/
Array multidimensionali
Ar

Array multidimensionali in Julia

2 esercizi

Informazioni su Array multidimensionali

Molto tempo fa, nel concetto Vettori, abbiamo notato che «gli array possono avere dimensioni arbitrarie (soggette solo ai vincoli di memoria del tuo hardware) e possono avere un numero arbitrario di dimensioni.»

Da allora, abbiamo in gran parte ignorato gli array con più di una dimensione, solo per mantenere le cose semplici. Questa decisione avrà più senso se provi a leggere i documenti di riferimento di Julia, nella loro piena complessità.

Tuttavia, gli array con dimensioni superiori sono molto, molto importanti nel calcolo scientifico, quindi dobbiamo capirli.

Nomenclatura: Seguendo secoli di precedenti matematici, chiamiamo gli array 1-D Vectors e gli array 2-D Matrices.

Gli esempi in questo documento saranno per lo più matrici. Lavorare con 3 o più dimensioni è sintatticamente quasi identico, ma l'output è difficile e confusionario da leggere (su uno schermo 2-D).

Il type di un array N-dimensionale di eltype T è Array{T, N}.

Per comodità, e per coerenza con la nomenclatura matematica, Julia definisce alcuni alias di tipo: Vector{T} per Array{T, 1} e Matrix{T} per 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

Costruire gli array

Abbiamo creato molti vettori inserendo un elenco separato da virgole tra parentesi quadre. Si possono usare come separatore anche i punti e virgola.

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 usiamo lo spazio (o la tabulazione) come separatore, il risultato è diverso.

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

Ora Julia lo definisce come una Matrix 1×3 (in altri contesti, lo chiameremmo row vector).

In generale, gli spazi uniscono le cose orizzontalmente, i punti e virgola (o gli a capo) le uniscono verticalmente.

Il riferimento alle «cose» è volutamente vago, perché Julia cercherà di lavorare con qualunque cosa le venga data.

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

Esistono le funzioni hcat() e vcat() che fanno la stessa cosa, rendendo più esplicito che si tratta di concatenazioni orizzontali e verticali. La generalizzazione a dimensioni superiori è la funzione 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

Scrivere matrici esplicite è comodo farlo in ordine row-major, perché si adatta all'intuizione umana (più facili da guardare, per le culture con testo orizzontale):

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

Tuttavia, tieni presente che Julia (come Fortran, R e Matlab, ma a differenza di C/C++ o NumPy) memorizza gli array N-dimensionali in ordine column-major, e questo può fare un'enorme differenza in termini di prestazioni se iteri sugli elementi. Aiuta la cache della tua CPU, così ti aiuterà!

# 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

L'esempio sopra prende gli interi da 1 a 6 e li inserisce in una Matrix 2×3 per colonne.

Esistono varie funzioni di utilità per costruire tipi comuni di array (uniformi o casuali).

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

Generare valori equidistanti

Chi apprezza NumPy saprà che np.linspace è una funzione molto usata per generare un array di lunghezza specificata, con estremi specificati e valori equidistanti. Di solito viene usata come asse x di un grafico, o come variabile indipendente nei modelli lineari.

Julia non ha un equivalente esatto, ma la flessibilissima funzione range() può imitarla specificando il limite inferiore e quello superiore, più l'argomento keyword length.

Chi lavora in locale, con accesso al pacchetto Plots, può eseguire questo codice:

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])

Il StepRangeLen può essere usato come un vettore, inclusa la conversione a colonna di una matrice.

Vedi anche la funzione equivalente logrange(), per generare valori equidistanti su un asse logaritmico.

Indicizzazione

Per un array 2-D, in genere usiamo due indici nell'ordine [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

L'ultimo esempio è forse sorprendente: un singolo indice non è un errore, e restituisce un singolo elemento.

La spiegazione risale al commento sull'ordine column-major: Julia scende lungo la col 1, poi la col 2, finché non trova il 3° elemento in memoria.

Fai attenzione: questo ha alcuni usi quando si scrivono librerie generiche, ma è più probabile che sia confusionario!

Un problema simile si presenta quando si interroga la dimensione di un array. length() restituisce il numero totale di elementi, size() restituisce una tupla di ndims() elementi con la lunghezza di ciascuna dimensione.

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

Indicizzare con intervalli e array

Possiamo facilmente copiare una sottomatrice.

Nell'esempio sotto, la funzione reshape() forza un array alle dimensioni specificate riempiendolo per colonne, poi ne estraiamo una porzione.

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

Un : da solo significa «copia tutto in questa dimensione».

Usa i vettori per righe o colonne non consecutive:

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

Un : da solo appiattirà qualsiasi array in un Vector, per colonne.

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

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

Applicare funzioni a un array

Abbiamo già visto funzioni di aggregazione come sum() e maximum() applicate a collezioni 1-D, dove operano su tutti gli elementi e restituiscono un risultato scalare.

Funzionano anche in dimensioni superiori. Tuttavia, potremmo voler applicare la funzione a una sola dimensione, ad esempio sommando verso il basso o di lato per restituire un array con una singleton dimension di dimensione 1.

Per questo c'è un argomento keyword opzionale 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

Il valore di dims è la dimensione che diventa singleton, quindi negli esempi sopra dims=1 => 1×3 e dims=2 => 2×1.

Per estensione, gli array con dimensioni superiori possono ridurre più dimensioni, se dims è impostato su un array o un intervallo (anche qui, capire l'output potrebbe richiedere un po' di riflessione!).

Scrivere funzioni consapevoli delle dimensioni

L'argomento keyword dims è comune nelle funzioni integrate come sum(), ma come scriviamo qualcosa di equivalente nel nostro codice?

Una buona risposta è usare funzioni di ordine superiore come reduce(), e questo sarà trattato in dettaglio in un concetto successivo.

In alternativa, Julia fornisce diverse funzioni che ci permettono di trattare gli array N-dimensionali come se fossero vettori annidati di vettori.

Per le matrici, eachrow() e eachcol() sono comode, ma la funzione più generale è eachslice(), che può gestire dimensioni arbitrarie.

# 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]

Il tipo è un po' allarmante, ma solo perché questa è una vista sull'array originale, che ci permette di lavorarci senza copiarlo. Gli array di Julia possono potenzialmente essere di Terabyte, quindi copiare è potenzialmente un incubo per le prestazioni!

Una vista come questa può essere usata per i cicli, per il broadcasting, o per tutte le altre operazioni che abbiamo visto nel syllabus finora.

Inoltre, le comprehension possono essere potenti e versatili con un array in input. I casi semplici sono stati menzionati nel concetto Cicli, ma ci sarà una discussione più ampia in un concetto successivo.

Il broadcasting in più dimensioni

Abbiamo discusso il caso 1-D nel concetto Operazioni sui vettori.

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

L'operatore puntato .- tratta il singleton 0.5 come equivalente a [0.5, 0.5, 0.5] applicandogli il broadcasting per farlo corrispondere alle dimensioni, poi esegue la sottrazione elemento per elemento.

Estendere questo a dimensioni superiori è in realtà solo la stessa cosa.

Ad esempio, quando si applica il broadcasting alla moltiplicazione di un vettore riga 2x1 [1.0 1.5 2.0] con la matrice 2x3 [1 2 3; 4 5 6], equivale a fare il broadcasting di [1.0 1.5 2.0] a [1.0 1.5 2.0; 1.0 1.5 2.0] per poi eseguire la moltiplicazione elemento per 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: per il broadcasting sono le dimensioni non singleton che devono combaciare in dimensione (es. 2x3 Matrix .* 2x1 Matrix).

Inoltre, una funzione può essere applicata in broadcasting a ciascun elemento di una Matrix esattamente come si fa con un 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

All'inizio potresti pensare che il margine di confusione per chi programma cresca esponenzialmente con il numero di dimensioni, ma la pratica aiuta molto. Inoltre, la familiarità con gli array NumPy si trasferisce abbastanza bene a Julia, nonostante la sintassi diversa.

Per approfondire

Molte operazioni comuni su vettori e matrici rientrano nella categoria generale dell'algebra lineare.

Ci sono (attualmente) due concetti aggiuntivi sul sito di Exercism, che sono solo documenti finché non riusciamo a progettare esercizi da abbinarvi.

  1. Basi di algebra lineare discute principalmente vari tipi di moltiplicazione.
  2. Risoluzione di equazioni lineari discute i sistemi di equazioni lineari e come risolverli in modo efficiente.
Advanced

In futuro potrebbe esserci un terzo concetto sulle fattorizzazioni di matrici.

Tuttavia, tieni presente che Basi di algebra lineare è piuttosto matematica e potrebbe non essere adatta a tutti gli studenti.

I concetti successivi (risoluzione di equazioni e fattorizzazioni) sono più avanzati, e sono pensati davvero solo per chi ha dimestichezza con la matematica a livello universitario.

Ci scusiamo, ma questa è la natura della materia, e un caso d'uso importante per Julia.

Modifica tramite GitHub Il collegamento si apre in una nuova finestra o scheda

Impara Array multidimensionali

La pratica è bloccata

Sblocca un altro esercizio per esercitarti su Array multidimensionali