O que é "Álgebra Linear"?
Há muitas definições técnicas, na [Wikipedia][linalg-wiki], em livros didáticos e em sites excelentes como [3Blue1Brown][3blue1brown], mas para nossos propósitos podemos ser mais informais.
A Álgebra Linear trata de muitas coisas interessantes (e muito úteis) que você pode fazer com vetores e matrizes.
Os mantenedores de Julia (e Python) adoram esse tipo de coisa. A gerência do Exercism, nem tanto.
Este Conceito será restrito aos aspectos mais simples de um assunto enorme, mas fique avisado: isto é, inevitavelmente, um conceito bastante matemático.
Depende de quem você pergunta, e de como você quer visualizar algo bastante abstrato.
Vector{T} é apenas um alias para Array{T, 1}, e o eltype T pode ser qualquer coisa.length e uma direction no espaço N-dimensional, mas sem posição fixa.point no espaço N-dimensional, com os N elementos representando a distância da origem ao longo de cada um dos N eixos.Para os propósitos atuais, ignore vetores de strings ou caracteres.
Vamos trabalhar com tipos numéricos neste Conceito: Int, Float ou Complex.
Novamente, você encontrará uma variedade de respostas.
linear combination de vetores coluna, empilhados lado a lado.transformation que pode ser aplicada a um vetor, assim como uma function é aplicada a outros tipos de entrada.Alguns tipos de matriz quadrada são comuns o suficiente para terem nomes especiais.
Matriz diagonal: Todas as entradas não nulas estão na main diagonal (do canto superior esquerdo ao inferior direito).
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 identidade: Uma matriz diagonal com apenas uns na diagonal (por razões que ficarão mais claras depois).
Frequentemente abreviada 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 não nulos na diagonal e acima dela, zeros abaixo. Triangular inferior você pode imaginar.
Se você realmente quer trocar linhas por colunas, a função permutedims() fará isso e dará uma nova matriz.
Isso envolve cópia, o que é lento e consome muita memória ao trabalhar com matrizes grandes.
Para propósitos de Álgebra Linear, a função transpose() é mais útil, pois cria rapidamente um wrapper lazy em torno da matriz original.
Ainda mais útil é a função adjoint(), que também inverte o sinal da parte imaginária em quaisquer números complexos.
Essa operação é comum o suficiente para que possamos apenas adicionar um apóstrofo ' ao nome da variável para criar a 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
O restante deste documento inclui funcionalidades do módulo LinearAlgebra.
Uma linha using LinearAlgebra a trará para o namespace, mas não vamos repetir isso nos exemplos (poluição visual demais).
Isso foi discutido no Conceito [Operações com Vetores][vector-ops].
O operador é .*, que opera sobre os vetores de entrada aos pares, produzindo uma saída do mesmo tamanho e tipo das entradas.
julia> [1, 2] .* [3, 4]
2-element Vector{Int64}:
3
8
Essa operação extremamente comum é escrita em livros didáticos como u ⋅ v, e chamada de produto "dot".
Um produto escalar é equivalente à soma do produto elemento a elemento.
Dois vetores podem ser multiplicados com o operador * usual, mas apenas se o vetor à esquerda for um adjoint: convenientemente escrito u' * v.
Isso deve ficar mais claro na seção posterior sobre multiplicação de matrizes.
O ponto elevado (central) está disponível em Julia (digitado \cdot e tab) como açúcar sintático para a função dot().
Não há necessidade de especificar a adjunta, pois esse detalhe é tratado automaticamente.
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 vetores com valores complexos, o vetor à esquerda precisa ser o conjugado (sinal invertido na parte imaginária).
A função dot faz isso automaticamente, a sintaxe u' faz isso explicitamente, mas sum(u .* v) falhará se u e v forem complexos.
Quão "grande" é um vetor?
A norm é uma tentativa de capturar isso, reduzindo o vetor a um escalar apropriado.
Toda uma família de normas é definida, mas de longe a mais comum é a 2-norma, que é √(v ⋅ v).
Essa operação de raiz quadrada média é a distância pitagórica da origem (no espaço N-dimensional). Se visualizarmos o vetor como uma seta, com sua cauda na origem, a 2-norma é o comprimento da seta.
Qualquer p-norma pode ser calculada fornecendo p como segundo argumento.
A 1-norma às vezes é útil: é simplesmente a soma dos valores absolutos das entradas (portanto, muito rápida e 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
Às vezes parece que todo matemático aplicado do mundo passou boa parte dos últimos 80 anos transformando cada cálculo em uma série de multiplicações de matrizes.
Essa é uma operação na qual os computadores são muito bons:
Os detalhes são bem simples, embora não muito intuitivos à primeira vista.
Considere uma matriz A multiplicando um vetor v para dar uma saída w (por convenção da Álgebra Linear, usaremos letras maiúsculas para matrizes e minúsculas para vetores).
A linha superior de A faz produto escalar com v para obter o primeiro elemento de w, a segunda linha dá o segundo elemento, e assim por diante.
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]
A multiplicação de matriz por matriz estende isso pelas colunas da matriz à direita.
Para C = A * B, podemos considerar que A multiplica cada coluna de B para dar a coluna correspondente de C: uma série de multiplicações matriz-vetor.
Equivalentemente, poderíamos dizer que a primeira linha de A faz produto escalar com cada coluna de B para dar a linha superior de C, a segunda linha dá a segunda linha, e assim por diante.
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 mostrado no exemplo acima, a multiplicação de matrizes não é comutativa: não há uma relação simples entre A*B e B*A.
A multiplicação de matrizes provavelmente é difícil de visualizar para quem é novo nisso, apenas lendo as palavras. O YouTube tem muitos vídeos demonstrando isso graficamente, então pesquise por "multiplicação de matrizes" e escolha um com seu estilo, nível de detalhe e idioma preferidos.
O produto escalar de dois vetores depende de eles terem comprimento igual.
Por extensão, para a multiplicação de matrizes, o número de colunas da matriz à esquerda deve corresponder ao número de linhas da matriz à direita.
Expressando os tamanhos como tuplas (nrows, ncols), como saída de size(A), temos (a, b) * (b, c) -> (a, c).
As dimensões "internas", aqui b e b, são compatíveis com o cálculo de produtos escalares.
As dimensões "externas", aqui a e c, determinam as dimensões da saída.
Um exemplo com matrizes retangulares:
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 vetores, no estilo matricial, tem duas possibilidades.
Convencionalmente, usaríamos u' * v como equivalente ao produto escalar.
As dimensões são (1, 3) * (3, 1), e Julia simplifica a saída (1, 1) para um escalar (ao contrário, por exemplo, de R).
Alternativamente, poderíamos usar u * v', com dimensões (3, 1) * (1, 3) -> (3, 3), para multiplicar todos os pares possíveis de elementos e expandir os vetores para uma 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 às vezes é chamado de produto interno.
Um tipo particularmente comum de multiplicação de matrizes envolve matrizes de rotação.
Em 2D, há uma matriz relativamente simples para girar um vetor no sentido anti-horário em θ radianos.
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
Você trabalha em uma startup de robótica que está desenvolvendo um robô simples como prova de conceito. Sua tarefa é fornecer uma funcionalidade para controlar o movimento do robô.
Para acompanhar a orientação e a extensão do robô, ele tem três marcadores que ficam a 1 unidade de distância do seu centro. Para inicializar a posição dele, precisamos pegar os três vetores de direção, normalizá-los e colocá-los em uma matriz.
Implemente a função orientrobot(vectors), que recebe um vetor de três vetores.
Retorne uma matriz 2x3 com os vetores normalizados como colunas.
julia> orientrobot([[-1,1],[1,0],[-1,-1]])
2×3 Matrix{Float64}:
-0.707107 1.0 -0.707107
0.707107 0.0 -0.707107
Agora, precisamos de uma funcionalidade para mudar a direção do movimento. Para isso, precisamos girar o robô para que ele fique voltado para onde quer ir.
Implemente a função rotaterobot(orientation, θ), que recebe a matriz de orientação do robô e um ângulo θ para girar no sentido anti-horário.
Retorne a nova matriz de orientação.
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 o robô de um lugar para outro, primeiro precisamos verificar se ele tem a orientação correta antes de movê-lo. A segunda coluna da matriz de orientação representa a direção para a qual ele está voltado.
Implemente a função robotoriented(orientation, direction), que recebe uma matriz de orientação e um vetor de posição relativa.
Retorne true se o robô estiver orientado na mesma direção que o vetor de posição relativa (dentro do erro de arredondamento).
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
Como a matriz de orientação também acompanha o formato do robô, precisamos saber onde esses pontos estão em relação à origem depois de mover o robô. Isso vai ajudar o robô a evitar colisões com outros objetos enquanto se move.
Implemente a função bodylocation(orientation, position), que recebe uma matriz de orientação e a posição atual do centro do robô.
Retorne a matriz de orientação transladada.
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
Crie sua conta no Exercism para aprender e dominar Julia com 35 conceitos128 exercícios e mentoria humana de verdade, tudo de graça.