¿Qué es el «álgebra lineal»?
Hay muchas definiciones técnicas, en [Wikipedia][linalg-wiki], en libros de texto y en sitios web excelentes como [3Blue1Brown][3blue1brown], pero para nuestros propósitos podemos ser más informales.
El álgebra lineal trata de muchas cosas interesantes (y muy útiles) que puedes hacer con vectores y matrices.
A los mantenedores de Julia (y Python) les encanta este tipo de cosas. A la dirección de Exercism, no tanto.
Este concepto se limitará a los aspectos más sencillos de un tema enorme, pero te avisamos: esto es, inevitablemente, un concepto bastante matemático.
Depende de a quién preguntes y de cómo quieras visualizar algo bastante abstracto.
Vector{T} no es más que un alias de Array{T, 1}, y el eltype T puede ser cualquier cosa.length y direction en un espacio de N dimensiones, pero sin una posición fija.point en un espacio de N dimensiones, con los N elementos que representan la distancia desde el origen a lo largo de cada uno de los N ejes.Para lo que nos ocupa, ignora los vectores de strings o caracteres.
En este concepto trabajaremos con tipos numéricos: Int, Float o Complex.
De nuevo, encontrarás una variedad de respuestas.
linear combination de vectores columna, apilados uno al lado del otro.transformation que se puede aplicar a un vector, igual que una function se aplica a otros tipos de entrada.Algunos tipos de matriz cuadrada son lo bastante comunes como para tener nombres especiales.
Matriz diagonal: Todas las entradas no nulas están en la main diagonal (de arriba a la izquierda a abajo a la derecha).
julia> [1 0 0; 0 2 0; 0 0 3]
3×3 Matrix{Int64}:
1 0 0
0 2 0
0 0 3
# a convenient shortcut
julia> diagm(1:3)
3×3 Matrix{Int64}:
1 0 0
0 2 0
0 0 3
Matriz identidad: Una matriz diagonal con solo unos en la diagonal (por razones que quedarán más claras más adelante).
A menudo se abrevia como I.
julia> Matrix{Float64}(I, 3, 3)
3×3 Matrix{Float64}:
1.0 0.0 0.0
0.0 1.0 0.0
0.0 0.0 1.0
Triangular superior: Valores no nulos en la diagonal y por encima de ella, ceros por debajo. Triangular inferior, puedes imaginártelo.
Si de verdad quieres intercambiar filas por columnas, la función permutedims() hará esto y te dará una nueva matriz.
Esto implica copiar, lo cual es lento y consume mucha memoria cuando se trabaja con matrices grandes.
Para el álgebra lineal, la función transpose() es más útil, ya que crea rápidamente un envoltorio perezoso alrededor de la matriz original.
Aún más útil es la función adjoint(), que además invierte el signo de la parte imaginaria en cualquier número complejo.
Esta operación es lo bastante común como para que baste con añadir un apóstrofo ' al nombre de la variable para crear la adjunta.
julia> m
2×3 Matrix{Int64}:
1 2 3
4 5 6
# new, full copy
julia> permutedims(m)
3×2 Matrix{Int64}:
1 4
2 5
3 6
# lazy version
julia> transpose(m)
3×2 transpose(::Matrix{Int64}) with eltype Int64:
1 4
2 5
3 6
julia> mc = [1+2im 2+3im; 3+2im 1+2im]
2×2 Matrix{Complex{Int64}}:
1+2im 2+3im
3+2im 1+2im
# lazy conjugate transpose
julia> adjoint(mc)
2×2 adjoint(::Matrix{Complex{Int64}}) with eltype Complex{Int64}:
1-2im 3-2im
2-3im 1-2im
# syntactic sugar for adjoint
julia> mc'
2×2 adjoint(::Matrix{Complex{Int64}}) with eltype Complex{Int64}:
1-2im 3-2im
2-3im 1-2im
El resto de este documento incluye funcionalidad del módulo LinearAlgebra.
Una línea using LinearAlgebra lo traerá al espacio de nombres, pero no vamos a repetirlo en los ejemplos (demasiado desorden visual).
Esto se trató en el concepto [Operaciones con vectores][vector-ops].
El operador es .*, que opera sobre los vectores de entrada por pares, dando una salida del mismo tamaño y tipo que las entradas.
julia> [1, 2] .* [3, 4]
2-element Vector{Int64}:
3
8
Esta operación tan común se escribe en los libros de texto como u ⋅ v y se llama producto escalar.
Un producto escalar equivale a la suma del producto elemento a elemento.
Dos vectores se pueden multiplicar con el operador habitual *, pero solo si el vector izquierdo es una adjoint: convenientemente escrito u' * v.
Esto quedará más claro en la sección posterior sobre multiplicación de matrices.
El punto elevado (centrado) está disponible en Julia (se introduce escribiendo \cdot y pulsando tabulador) como azúcar sintáctico para la función dot().
No es necesario especificar el adjunto, ya que este detalle se gestiona automáticamente.
julia> using LinearAlgebra
# with element-wise syntax
julia> sum([1, 2] .* [3, 4])
11
# with u' * v syntax
julia> [1, 2]' * [3, 4]
11
# with dot()
julia> dot([1, 2], [3, 4])
11
# with \cdot syntax
julia> [1, 2] ⋅ [3, 4]
11
Para vectores con valores complejos, el vector izquierdo debe ser el conjugado (con el signo de la parte imaginaria invertido).
La función dot lo hace automáticamente, la sintaxis u' lo hace explícitamente, pero sum(u .* v) fallará si u y v son complejos.
¿Cómo de «grande» es un vector?
La norm es un intento de capturar esto, reduciendo el vector a un escalar adecuado.
Se define toda una familia de normas, pero de lejos la más común es la norma 2, que es √(v ⋅ v).
Esta operación de raíz cuadrada media es la distancia pitagórica desde el origen (en un espacio de N dimensiones). Si visualizamos el vector como una flecha, con su cola en el origen, la norma 2 es la longitud de la flecha.
Cualquier norma p se puede calcular proporcionando p como segundo argumento.
La norma 1 a veces es útil: es simplemente la suma de los valores absolutos de las entradas (así que es muy rápida y fácil de calcular).
# defaults to the 2-norm
julia> norm([1, 2, 3])
3.7416573867739413
# the 1-norm
julia> norm([1, -2, 3], 1)
6.0
A veces parece que todos los matemáticos aplicados del mundo han pasado gran parte de los últimos 80 años convirtiendo cada cálculo en una serie de multiplicaciones de matrices.
Esta es una operación en la que los ordenadores son muy buenos:
Los detalles son bastante sencillos, aunque no muy intuitivos a primera vista.
Considera una matriz A que multiplica un vector v para dar una salida w (por convención del álgebra lineal, usaremos letras mayúsculas para las matrices y minúsculas para los vectores).
La primera fila de A se multiplica escalarmente con v para obtener el primer elemento de w, la segunda fila da el segundo elemento, y así sucesivamente hacia abajo.
julia> A = [1 2; 3 4]
2×2 Matrix{Int64}:
1 2
3 4
julia> v = [5, 6]
2-element Vector{Int64}:
5
6
julia> A * v
2-element Vector{Int64}:
17 # equals [1, 2] ⋅ [5, 6]
39 # equals [3, 4] ⋅ [5, 6]
La multiplicación matriz * matriz extiende esto a lo largo de las columnas de la matriz de la derecha.
Para C = A * B, podemos considerar que A multiplica cada columna de B para dar la columna correspondiente de C: una serie de multiplicaciones matriz-vector.
De forma equivalente, podríamos decir que se hace el producto escalar de la primera fila de A con cada columna de B para dar la primera fila de C, la segunda fila da la segunda fila, y así sucesivamente hacia abajo.
julia> A
2×2 Matrix{Int64}:
1 2
3 4
julia> B = [5 7; 6 8]
2×2 Matrix{Int64}:
5 7
6 8
# left column is the same as A*v previously
julia> A * B
2×2 Matrix{Int64}:
17 23
39 53
julia> B * A
2×2 Matrix{Int64}:
26 38
30 44
Como se muestra en el ejemplo anterior, la multiplicación de matrices no conmuta: no hay una relación simple entre A*B y B*A.
Probablemente sea difícil de visualizar para quien se acerca por primera vez, solo con leer las palabras. YouTube tiene muchos vídeos que lo demuestran gráficamente, así que busca «multiplicación de matrices» y elige uno con tu estilo, nivel de detalle e idioma preferidos.
El producto escalar de dos vectores depende de que tengan la misma longitud.
Por extensión, para la multiplicación de matrices, el número de columnas de la matriz izquierda debe coincidir con el número de filas de la matriz derecha.
Si expresamos los tamaños como tuplas (nrows, ncols), tal como las devuelve size(A), tenemos (a, b) * (b, c) -> (a, c).
Las dimensiones «internas», aquí b y b, son compatibles para calcular productos escalares.
Las dimensiones «externas», aquí a y c, determinan las dimensiones de la salida.
Un ejemplo con matrices rectangulares:
julia> D = reshape(1:6, 2, 3)
2×3 reshape(::UnitRange{Int64}, 2, 3) with eltype Int64:
1 3 5
2 4 6
julia> E = reshape(1:12, 3, 4)
3×4 reshape(::UnitRange{Int64}, 3, 4) with eltype Int64:
1 4 7 10
2 5 8 11
3 6 9 12
julia> D * E
2×4 Matrix{Int64}:
22 49 76 103
28 64 100 136
# (3, 4) * (2, 3) not possible
julia> E * D
ERROR: DimensionMismatch: matrix A has axes (Base.OneTo(3),Base.OneTo(4)), matrix B has axes (Base.OneTo(2),Base.OneTo(3))
Multiplicar pares de vectores al estilo de matrices tiene dos posibilidades.
Convencionalmente, usaríamos u' * v como equivalente al producto escalar.
Las dimensiones son (1, 3) * (3, 1), y Julia simplifica la salida (1, 1) a un escalar (a diferencia, por ejemplo, de R).
Alternativamente, podríamos usar u * v', con dimensiones (3, 1) * (1, 3) -> (3, 3), para multiplicar todos los pares posibles de elementos y expandir los vectores a una matriz.
julia> u = [1, 2]
2-element Vector{Int64}:
1
2
julia> v = [3, 4]
2-element Vector{Int64}:
3
4
julia> u' * v
11
julia> u * v'
2×2 Matrix{Int64}:
3 4
6 8
u' * v a veces se llama producto interior.
Un tipo de multiplicación de matrices particularmente común implica matrices de rotación.
En 2D, hay una matriz relativamente sencilla para rotar un vector en sentido antihorario θ radianes.
julia> rot2d(θ, vec) = [cos(θ) -sin(θ); sin(θ) cos(θ)] * vec
rot2d (generic function with 1 method)
# unit vector in the x direction
julia> i_hat = [1, 0]
2-element Vector{Int64}:
1
0
# rotate 45 degrees
julia> rot2d(π/4, i_hat)
2-element Vector{Float64}:
0.7071067811865476
0.7071067811865475
# rotate 90 degrees -> unit vector in the y direction, j_hat
julia> rot2d(π/2, i_hat)
2-element Vector{Float64}:
6.123233995736766e-17 # zero, within numerical error
1.0
Trabajas para una startup de robótica que está desarrollando un robot sencillo como prueba de concepto. Se te ha encomendado la tarea de proporcionar cierta funcionalidad para controlar el movimiento del robot.
Para llevar un registro de la orientación y la extensión del robot, este tiene tres marcadores que están a 1 unidad de distancia de su centro. Para inicializar su posición, tenemos que tomar los tres vectores de dirección, normalizarlos y colocarlos en una matriz.
Implementa la función orientrobot(vectors), que recibe un vector de tres vectores.
Devuelve una matriz 2x3 con los vectores normalizados como columnas.
julia> orientrobot([[-1,1],[1,0],[-1,-1]])
2×3 Matrix{Float64}:
-0.707107 1.0 -0.707107
0.707107 0.0 -0.707107
A continuación, necesitamos funcionalidad para cambiar la dirección del movimiento. Para ello, tenemos que girar el robot para que mire hacia donde quiere ir.
Implementa la función rotaterobot(orientation, θ), que recibe la matriz de orientación del robot y un ángulo θ para girar en sentido antihorario.
Devuelve la nueva matriz de orientación.
julia> orientmatrix = initialize([[-1,1],[1,0],[-1,-1]]);
julia> rotaterobot(orientmatrix, π/2)
2×3 Matrix{Float64}:
-0.707107 6.12323e-17 0.707107
-0.707107 1.0 -0.707107
Para mover el robot de un lugar a otro, primero tenemos que comprobar que tiene la orientación correcta antes de moverlo. La segunda columna de la matriz de orientación representa la dirección hacia delante.
Implementa la función robotoriented(orientation, direction), que recibe una matriz de orientación y un vector de posición relativa.
Devuelve true si el robot está orientado en la misma dirección que el vector de posición relativa (dentro del error de redondeo).
julia> orientmatrix = initialize([[-1,1],[1,0],[-1,-1]]);
julia> robotoriented(orientmatrix, [5, 0])
true
julia> robotoriented(orientmatrix, [0, 5])
false
julia> robotoriented(orientmatrix, [-5, 0])
false
Dado que la matriz de orientación también lleva un registro de la forma del robot, necesitamos saber dónde están estos puntos con respecto al origen después de mover el robot. Esto ayudará al robot a evitar colisiones con otros objetos cuando se mueve.
Implementa la función bodylocation(orientation, position), que recibe una matriz de orientación y la posición actual del centro del robot.
Devuelve la matriz de orientación trasladada.
julia> orientmatrix = initialize([[-1,1],[1,0],[-1,-1]]);
julia> bodylocation(orientmatrix, [5, 3])
2×3 Matrix{Float64}:
4.29289 6.0 4.29289
3.70711 3.0 2.29289
Regístrate en Exercism para aprender y dominar Julia con 35 conceptos128 ejercicios y mentoría humana real, todo gratis.