Bien plus haut, dans le concept Vectors, on a noté que « les tableaux peuvent être de taille arbitraire (seule la mémoire de ton matériel les limite), et peuvent avoir un nombre arbitraire de dimensions. »
Depuis, on a largement ignoré les tableaux à plus d'une dimension, histoire de rester simples. Cette décision paraîtra plus logique si tu essaies de lire les documents de référence de Julia dans toute leur complexité.
Cependant, les tableaux à dimensions supérieures sont très, très importants en calcul scientifique, il faut donc qu'on les comprenne.
Nomenclature : en suivant des siècles de précédent mathématique, on appelle les tableaux à une dimension des Vectors et les tableaux à deux dimensions des Matrices.
Les exemples de ce document seront principalement des matrices. Travailler avec 3 dimensions ou plus est syntaxiquement presque identique, mais le résultat est difficile et pénible à lire (sur un écran en 2D).
On a créé beaucoup de vecteurs en mettant une liste séparée par des virgules entre crochets. On peut aussi utiliser des points-virgules comme séparateur.
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
Si on utilise un espace (ou une tabulation) comme séparateur, le résultat est différent.
julia> u = [1 2 3]
1×3 Matrix{Int64}:
1 2 3
Julia définit alors ceci comme une matrice 1×3 (dans d'autres contextes, on l'appellerait un row vector).
En général, les espaces joignent horizontalement, les points-virgules (ou les retours à la ligne) joignent verticalement.
La référence à des « choses » est volontairement vague, car Julia essaiera de se débrouiller avec tout ce que tu lui donnes.
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
Il existe des fonctions hcat() et vcat() qui font la même chose, en montrant plus explicitement qu'il s'agit de concaténations horizontales et verticales. La généralisation aux dimensions supérieures est la fonction 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
Taper des matrices explicites se fait commodément dans l'ordre row-major, parce que cela correspond à l'intuition humaine (plus facile à regarder, pour les cultures à écriture horizontale) :
julia> m = [1 2 3; 4 5 6]
2×3 Matrix{Int64}:
1 2 3
4 5 6
Mais attention : Julia (comme Fortran, R et Matlab, mais contrairement à C/C++ ou NumPy) stocke les tableaux à N dimensions en ordre column-major, et cela peut faire une énorme différence de performance si tu parcours les éléments. Aide le cache de ton processeur à t'aider !
# 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'exemple ci-dessus prend les entiers de 1 à 6 et remplit avec eux une matrice 2×3, colonne par colonne.
Il existe diverses fonctions utilitaires pour construire des types courants de tableau (uniformes ou aléatoires).
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
Pour un tableau à 2 dimensions, on utilise généralement deux indices dans l'ordre [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
Le dernier exemple est peut-être surprenant : un seul indice n'est pas une erreur, et renvoie un seul élément.
L'explication remonte au commentaire sur l'ordre column-major : Julia descend la colonne 1, puis la colonne 2, jusqu'à trouver le 3e élément en mémoire.
Attention : cela a quelques usages quand on écrit des bibliothèques génériques, mais c'est plus souvent source de confusion !
Un problème similaire se pose quand on interroge la taille d'un tableau. length() donne le nombre total d'éléments, size() donne un tuple de ndims() éléments contenant la longueur de chaque dimension.
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
On peut facilement copier une sous-matrice.
Dans l'exemple ci-dessous, la fonction reshape() force un tableau aux dimensions indiquées en remplissant colonne par colonne, puis on en prélève une tranche.
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 : tout seul signifie « copie tout dans cette dimension ».
Utilise des vecteurs pour des lignes ou des colonnes non consécutives :
julia> m12[[1, 3], :] # rows 1 and 3
2×3 Matrix{Int64}:
1 5 9
3 7 11
On a déjà vu des fonctions d'agrégation comme sum() et maximum() appliquées à des collections à une dimension, où elles opèrent sur tous les éléments et renvoient un résultat scalaire.
Cela fonctionne aussi en dimensions supérieures. Cependant, on peut vouloir n'appliquer la fonction qu'à une seule dimension, par exemple en sommant vers le bas ou vers la droite pour renvoyer un tableau avec une singleton dimension de taille 1.
Pour cela, il existe un argument nommé optionnel 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
La valeur de dims est la dimension qui devient singleton, donc dims=1 => 1×3 et dims=2 => 2×1 dans les exemples ci-dessus.
Par extension, les tableaux à dimensions supérieures peuvent réduire plusieurs dimensions, si dims est défini sur un tableau ou une plage (là encore, comprendre le résultat peut demander un peu de réflexion !).
L'argument nommé dims est courant dans les fonctions natives comme sum(), mais comment écrire l'équivalent dans notre propre code ?
Une bonne réponse consiste à utiliser des fonctions d'ordre supérieur comme reduce(), ce qui sera détaillé dans un concept ultérieur.
Sinon, Julia fournit plusieurs fonctions qui permettent de traiter les tableaux à N dimensions comme s'il s'agissait de vecteurs de vecteurs imbriqués.
Pour les matrices, eachrow() et eachcol() sont pratiques, mais la fonction plus générale est eachslice(), qui gère des dimensions arbitraires.
# 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]
Le type est un peu effrayant, mais uniquement parce qu'il s'agit d'une vue sur le tableau d'origine, ce qui permet de travailler dessus sans le copier. Les tableaux Julia peuvent peser des téraoctets, donc les copier peut être un cauchemar de performance !
Une vue comme celle-ci peut servir à parcourir en boucle, à faire de la diffusion, ou à toutes les autres opérations qu'on a vues dans le programme jusqu'ici.
De plus, les compréhensions peuvent être puissantes et polyvalentes avec un tableau en entrée. Des cas simples ont été mentionnés dans le concept Loops, mais une discussion plus complète aura lieu dans un concept ultérieur.
On a abordé le cas à une dimension dans le concept Vector Operations.
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'opérateur pointé .- traite le singleton 0.5 comme équivalent à [0.5, 0.5, 0.5] en lui appliquant un broadcasting qui le fait correspondre aux dimensions, puis effectue la soustraction élément par élément.
Étendre cela à des dimensions supérieures n'est vraiment qu'une répétition du même principe.
Par exemple, lorsqu'on diffuse la multiplication d'un vecteur ligne 2x1 [1.0 1.5 2.0] avec la matrice 2x3 [1 2 3; 4 5 6], cela équivaut à diffuser [1.0 1.5 2.0] en [1.0 1.5 2.0; 1.0 1.5 2.0], puis à effectuer la multiplication élément par élément.
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
Remarque : ce sont les dimensions non-singleton qui doivent avoir la même taille (par exemple 2x3 Matrix .* 2x1 Matrix) pour la diffusion.
Par ailleurs, une fonction peut être diffusée sur chaque élément d'une Matrix exactement comme on le fait avec 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
Tu peux avoir l'impression, au début, que le potentiel de confusion pour le programmeur croît exponentiellement avec le nombre de dimensions, mais la pratique aide beaucoup. De plus, la familiarité avec les tableaux NumPy se transpose assez bien à Julia, malgré la syntaxe différente.
Une matrice de points est une image en deux dimensions composée de points et d'espaces. Les points forment l'image voulue sur le fond constitué des espaces.
Une image en matrice de points peut être stockée et manipulée en mémoire sous la forme d'une Matrix en deux dimensions.
Dans ce qui suit, les « espaces » des matrices seront représentés par 0 et les « points » seront non nuls.
Matrix du logo ExercismLa Matrix se présente comme suit, avec 0 comme espace et 1 comme point :
[
0 0 1 1 0 0 0 0 0 0 0 0 0 0 1 1 0 0;
0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0;
0 1 0 0 0 1 0 0 0 0 0 0 1 0 0 0 1 0;
0 1 0 0 1 0 1 0 0 0 0 1 0 1 0 0 1 0;
0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0;
1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1;
0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0;
0 1 0 0 0 0 0 1 0 0 1 0 0 0 0 0 1 0;
0 1 0 0 0 0 0 0 1 1 0 0 0 0 0 0 1 0;
0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0;
0 0 1 1 0 0 0 0 0 0 0 0 0 0 1 1 0 0;
]
Si on l'affiche, voici ce que ça donne :
XX XX
X X
X X X X
X X X X X X
X X
X X
X X X X
X X X X
X XX X
X X
XX XX
Définis les fonctions frown!() et frown() qui prennent la Matrix du logo Exercism.
Renvoie une Matrix dans laquelle la bouche souriante est remplacée par une bouche renfrognée.
La Matrix obtenue s'afficherait comme ceci :
XX XX
X X
X X X X
X X X X X X
X X
X X
X XX X
X X X X
X X X X
X X
XX XX
Définis la fonction stickerwall(), qui prend en entrée la matrice du logo Exercism.
Renvoie une Matrix de la matrice de points qui s'affiche comme ceci :
XX XX XX XX
X X X X
X X X X X X X X
X X X X X X X X X X X X
X X X X
X XX X
X X X X X XX X
X X X X X X X X
X XX X X X X X
X X X X
XX XX XX XX
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
XX XX XX XX
X X X X
X X X X X X X X
X X X X X X X X X X X X
X X X X
X XX X
X XX X X X X X
X X X X X X X X
X X X X X XX X
X X X X
XX XX XX XX
On n'est pas obligé d'utiliser 1 comme point, et on pourrait donc, si on le souhaite, encoder d'autres informations utiles.
Définis la fonction colpixelcount(), qui prend en entrée n'importe quelle matrice de points dont les points valent 1.
Renvoie une matrice de points de même taille, dans laquelle les points de chaque colonne correspondent au nombre de points de cette colonne.
Si on lui donne la Matrix du logo Exercism en entrée, le résultat est une Matrix comme suit :
[
0 0 2 2 0 0 0 0 0 0 0 0 0 0 2 2 0 0;
0 8 0 0 0 0 0 0 0 0 0 0 0 0 0 0 8 0;
0 8 0 0 0 1 0 0 0 0 0 0 1 0 0 0 8 0;
0 8 0 0 1 0 2 0 0 0 0 2 0 1 0 0 8 0;
0 8 0 0 0 0 0 0 0 0 0 0 0 0 0 0 8 0;
1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1;
0 8 0 0 0 0 2 0 0 0 0 2 0 0 0 0 8 0;
0 8 0 0 0 0 0 1 0 0 1 0 0 0 0 0 8 0;
0 8 0 0 0 0 0 0 1 1 0 0 0 0 0 0 8 0;
0 8 0 0 0 0 0 0 0 0 0 0 0 0 0 0 8 0;
0 0 2 2 0 0 0 0 0 0 0 0 0 0 2 2 0 0;
]
Plutôt que de simplement parler d'afficher nos créations, on aimerait pouvoir le faire pour les examiner facilement.
Définis la fonction render(), qui prend en entrée une matrice de points.
Renvoie une string dans laquelle les points sont affichés sous forme de 'X', les 0 sous forme de ' ', et où des retours à la ligne relient chaque ligne.
julia> render(E)
" XX XX \n X X \n X X X X \n X X X X X X \n X X \nX X\n X X X X \n X X X X \n X XX X \n X X \n XX XX "
Une fois imprimé, le résultat doit afficher la matrice Exercism comme prévu.
julia> print(render(E))
XX XX
X X
X X X X
X X X X X X
X X
X X
X X X X
X X X X
X XX X
X X
XX XX
Cela doit aussi fonctionner avec des « points » différents de 1 :
julia> print(render(colpixelcount(E)))
XX XX
X X
X X X X
X X X X X X
X X
X X
X X X X
X X X X
X XX X
X X
XX XX
Inscris-toi sur Exercism pour apprendre et maîtriser Julia avec 35 concepts128 exercices, et un vrai mentorat humain, le tout gratuitement.