Hace tiempo, en el concepto Vectors, comentamos que «los arrays pueden tener un tamaño arbitrario (limitado únicamente por las restricciones de memoria de tu hardware) y pueden tener tantas dimensiones como se quiera».
Desde entonces, hemos ignorado en gran medida los arrays de más de una dimensión, simplemente para no complicarnos. 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 tenemos que entenderlos.
Nomenclatura: Siguiendo siglos de precedente matemático, nos referimos a los arrays de 1-D como Vectors y a los de 2-D como Matrices.
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 resulta confusa (en una pantalla 2-D).
Hemos creado muchos vectores colocando una lista separada por comas entre corchetes. También se pueden 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
Julia ahora define esto como una matriz 1×3 (en otros contextos, la llamaríamos 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 claro 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 se hace cómodamente en orden por filas, porque encaja con la intuición humana (es más fácil de mirar, para las culturas con texto 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, y esto puede marcar una enorme diferencia de rendimiento si recorres los elementos con 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 enteros del 1 al 6 y rellena una matriz 2×3 con ellos 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, normalmente 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 único elemento.
La explicación se remonta al comentario sobre el orden por columnas: Julia recorre la columna 1 hacia abajo, luego la columna 2, hasta que encuentra el tercer elemento en memoria.
Ten cuidado: esto tiene algunos usos al escribir bibliotecas de propósito general, ¡pero lo más probable es que resulte confuso!
Surge un problema similar al consultar el tamaño de un array.
length() da el número total de elementos y size() devuelve 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 fácilmente una submatriz.
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 «copiar 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 actúan 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 esto existe un argumento de palabra clave 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
El valor de dims es la dimensión que pasa a ser 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 en 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(), algo que 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 anidados de vectores.
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 se trata de 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 para el rendimiento!
Una vista como esta se puede usar para iterar con bucles, para el broadcasting o para todas las demás operaciones que hemos visto hasta ahora en el temario.
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.
Vimos 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 escalar 0.5 como equivalente a [0.5, 0.5, 0.5] al hacerle broadcasting para que coincidan las dimensiones y, después, hace la resta elemento a 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 2x1 [1.0 1.5 2.0] con la matriz 2x3 [1 2 3; 4 5 6], equivale a hacer broadcasting de [1.0 1.5 2.0] a [1.0 1.5 2.0; 1.0 1.5 2.0] y después multiplicar 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: Para el broadcasting, lo que debe coincidir en tamaño son las dimensiones que no son singleton (por ejemplo, 2x3 Matrix .* 2x1 Matrix).
Además, se puede hacer broadcasting de una función a cada elemento de una Matrix exactamente igual que 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 te parezca que las posibilidades de que el programador se confunda crecen de forma exponencial con el número de dimensiones, pero la práctica ayuda mucho. Además, estar familiarizado 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 logotipo de ExercismLa Matrix tiene el siguiente aspecto, 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 lo «renderizas», tiene este aspecto:
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 logotipo 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 entrada 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 entrada cualquier matriz de puntos con puntos 1.
Devuelve una matriz de puntos del mismo tamaño, en la que los puntos de cada columna son el número de puntos que hay en esa columna.
Si tomamos como entrada la Matrix del logotipo de Exercism, el resultado 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 lugar de limitarnos a hablar de renderizar nuestras creaciones, nos gustaría poder hacerlo de verdad para verlas fácilmente.
Define la función render() que recibe como entrada una matriz de puntos.
Devuelve un string con los puntos renderizados como 'X', los 0 como ' ' y saltos de línea que unen las filas.
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 imprimirlo, debería renderizar la Matrix de Exercism tal 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» 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.