Hace tiempo, en el concepto Vectors, señalamos que «los arrays pueden ser de tamaño arbitrario (sujetos solo a las limitaciones de memoria de tu hardware) y pueden tener cualquier cantidad de dimensiones».
Desde entonces, en gran medida hemos ignorado los arrays de más de una dimensión, solo para no complicar las cosas. Esta decisión tendrá más sentido si intentas leer la documentación de referencia de Julia, en toda su complejidad.
Sin embargo, los arrays de dimensiones superiores son muy, muy importantes en la computación científica, así que necesitamos entenderlos.
Nomenclatura: Siguiendo siglos de precedentes matemáticos, llamamos Vectors a los arrays de 1-D y Matrices a los de 2-D.
Los ejemplos de este documento serán en su mayoría matrices. Trabajar con 3 o más dimensiones es sintácticamente casi idéntico, pero la salida es difícil de leer y confusa (en una pantalla 2-D).
Hemos creado muchos vectores colocando una lista separada por comas entre corchetes. También puedes usar puntos y coma 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
Si usamos un espacio (o un tabulador) como separador, el resultado es diferente.
julia> u = [1 2 3]
1×3 Matrix{Int64}:
1 2 3
Ahora Julia define esto como una Matrix de 1×3 (en otros contextos, la llamaríamos un row vector).
En general, los espacios unen cosas horizontalmente y los puntos y coma (o los saltos de línea) las unen verticalmente.
La referencia a «cosas» es deliberadamente vaga, ya que Julia intentará trabajar con lo que le des.
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
Existen las funciones hcat() y vcat() que hacen lo mismo, y dejan más explícito que se trata de concatenaciones horizontales y verticales.
La generalización a dimensiones superiores es la función 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
Escribir matrices explícitas es cómodo si lo haces en orden por filas (row-major), porque encaja con la intuición humana (es más fácil de mirar para las culturas que escriben en horizontal):
julia> m = [1 2 3; 4 5 6]
2×3 Matrix{Int64}:
1 2 3
4 5 6
Sin embargo, ten en cuenta que Julia (como Fortran, R y Matlab, pero a diferencia de C/C++ o NumPy) almacena los arrays de N dimensiones en orden por columnas (column-major), y esto puede marcar una enorme diferencia de rendimiento si recorres los elementos en un bucle. ¡Ayuda a la caché de tu CPU para que te ayude!
# 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
El ejemplo anterior toma los números enteros del 1 al 6 y rellena con ellos una Matrix de 2×3 por columnas.
Hay varias funciones de utilidad para construir tipos comunes de array (uniformes o aleatorios).
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
Para un array de 2-D, por lo general usamos dos índices en el orden [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
El último ejemplo quizá te sorprenda: un índice único no es un error y devuelve un solo elemento.
La explicación se remonta al comentario sobre el orden por columnas: Julia recorre la col. 1 hacia abajo, luego la col. 2, hasta encontrar el tercer elemento en memoria.
Ten cuidado: esto tiene algunos usos cuando escribes bibliotecas de propósito general, pero lo más probable es que te confunda.
Algo parecido ocurre cuando consultas el tamaño de un array.
length() da el número total de elementos y size() da una tupla de ndims() elementos con la longitud de cada dimensión.
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
Podemos copiar una submatriz con facilidad.
En el ejemplo de abajo, la función reshape() fuerza un array a las dimensiones especificadas rellenándolo por columnas, y después lo cortamos.
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 : por sí solo significa «copia todo en esta dimensión».
Usa vectores para filas o columnas no consecutivas:
julia> m12[[1, 3], :] # rows 1 and 3
2×3 Matrix{Int64}:
1 5 9
3 7 11
Ya hemos visto funciones de agregación como sum() y maximum() aplicadas a colecciones de 1-D, donde operan sobre todos los elementos y devuelven un resultado escalar.
Esto también funciona en dimensiones superiores.
Sin embargo, puede que queramos aplicar la función a una sola dimensión, por ejemplo sumando hacia abajo o hacia los lados para devolver un array con una singleton dimension de tamaño 1.
Para eso existe un argumento de palabra clave dims opcional.
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
El valor de dims es la dimensión que se convierte en singleton, así que dims=1 => 1×3 y dims=2 => 2×1 en los ejemplos anteriores.
Por extensión, los arrays de dimensiones superiores pueden reducir varias dimensiones si dims se establece como un array o un rango (de nuevo, entender la salida puede requerir algo de reflexión).
El argumento de palabra clave dims es habitual en funciones integradas como sum(), pero ¿cómo escribimos algo equivalente en nuestro propio código?
Una buena respuesta es usar funciones de orden superior como reduce(), y esto lo veremos con cierto detalle en un concepto posterior.
Como alternativa, Julia ofrece varias funciones que nos permiten tratar los arrays de N dimensiones como si fueran vectores de vectores anidados.
Para las matrices, eachrow() y eachcol() son cómodas, pero la función más general es eachslice(), que puede manejar dimensiones arbitrarias.
# 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]
El tipo asusta un poco, pero solo porque esto es una vista del array original, que nos permite trabajar con él sin copiarlo. Los arrays de Julia pueden llegar a ocupar terabytes, así que copiarlos puede ser una pesadilla de rendimiento.
Una vista como esta se puede usar para bucles, broadcasting o todas las demás operaciones que hemos visto en el temario hasta ahora.
Además, las comprensiones pueden ser potentes y versátiles con un array como entrada.
Mencionamos casos sencillos en el concepto Loops, pero habrá un análisis más amplio en un concepto posterior.
Analizamos el caso de 1-D en el concepto 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
El operador con punto .- trata el 0.5 singleton como equivalente a [0.5, 0.5, 0.5] al hacer broadcasting para que coincidan las dimensiones, y después hace la resta elemento por elemento.
Extender esto a dimensiones superiores es, en realidad, más de lo mismo.
Por ejemplo, al hacer broadcasting de la multiplicación de un vector fila de 2x1 [1.0 1.5 2.0] con la matriz de 2x3 [1 2 3; 4 5 6], es equivalente a hacer broadcasting de [1.0 1.5 2.0] a [1.0 1.5 2.0; 1.0 1.5 2.0] y luego hacer la multiplicación elemento por 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: Para hacer broadcasting, las dimensiones que no son singleton deben coincidir en tamaño (por ejemplo, 2x3 Matrix .* 2x1 Matrix).
Además, puedes hacer broadcasting de una función a cada elemento de una Matrix exactamente igual que como se hace 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
Puede que al principio sientas que el margen de confusión para quien programa crece exponencialmente con el número de dimensiones, pero la práctica ayuda mucho. Además, la familiaridad con los arrays de NumPy se traslada bastante bien a Julia, a pesar de la sintaxis diferente.
Una matriz de puntos es una imagen bidimensional formada por puntos y espacios en blanco. Los puntos forman la imagen deseada sobre el fondo de los espacios en blanco.
Una imagen de matriz de puntos se puede almacenar y manipular en memoria como una Matrix bidimensional.
En lo que sigue, los «espacios en blanco» de las matrices se representarán con 0 y los «puntos» serán distintos de cero.
Matrix del logo de ExercismLa Matrix se ve así, con 0 como espacio en blanco y 1 como punto:
[
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 la «renderizas», se ve así:
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
Define las funciones frown!() y frown() que reciben la Matrix del logo de Exercism.
Devuelve una Matrix con la boca sonriente cambiada por una boca fruncida.
La Matrix resultante se renderizaría así:
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
Define la función stickerwall(), que recibe como argumento la matriz de Exercism.
Devuelve una Matrix de la matriz de puntos que se renderiza así:
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
No estamos limitados a usar solo 1 como punto, así que podríamos codificar otra información útil si quisiéramos.
Define la función colpixelcount(), que recibe como argumento cualquier matriz de puntos con puntos 1.
Devuelve una matriz de puntos del mismo tamaño, con los puntos de cada columna iguales a la cantidad de puntos que hay en esa columna.
Si la entrada es la Matrix del logo de Exercism, la salida es una Matrix como la siguiente:
[
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;
]
En vez de solo hablar de renderizar nuestras creaciones, nos gustaría poder hacerlo de verdad para verlas con facilidad.
Define la función render(), que recibe como argumento una matriz de puntos.
Devuelve un string con los puntos renderizados como 'X', los 0 como ' ', y saltos de línea que conectan cada fila.
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 "
Al imprimirla, esto debería renderizar la Matrix de Exercism como se espera.
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
Esto también debería funcionar con «puntos» que sean distintos 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
Regístrate en Exercism para aprender y dominar Julia con 35 conceptos128 ejercicios y mentoría humana real, todo gratis.