Trilhas
/
Julia
Julia
/
Exercícios
/
Revolução dos robôs
Revolução dos robôs

Revolução dos robôs

Exercício de aprendizagem

Introdução

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.

Note

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.

O que é um vetor?

Depende de quem você pergunta, e de como você quer visualizar algo bastante abstrato.

  • Em Julia, Vector{T} é apenas um alias para Array{T, 1}, e o eltype T pode ser qualquer coisa.
  • Em (boa parte da) Física, um vetor é uma seta com um length e uma direction no espaço N-dimensional, mas sem posição fixa.
  • Em Álgebra Linear, a seta dos físicos é fixada no espaço, com sua cauda na origem. Isso torna a haste da seta redundante, então podemos representar o vetor como um 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.

O que é uma matriz?

Novamente, você encontrará uma variedade de respostas.

  • Um array retangular 2-D de números (sem bordas irregulares). Uma matriz quadrada é um caso especial comum.
  • Uma linear combination de vetores coluna, empilhados lado a lado.
  • Uma 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.

Transpondo uma matriz

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

Multiplicação

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).

Vetores, produto elemento a elemento

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

Vetores, produto escalar

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.

Normas

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

Multiplicação de matrizes

À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:

  • É altamente repetitiva.
  • Pode ser paralelizada com eficiência.
  • Muito hardware especializado foi desenvolvido para torná-la mais rápida, incluindo a GPU que provavelmente está integrada ao seu laptop.

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.

Dimensões

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.

Rotações

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

Instruções

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ô.

1. Oriente o 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

2. Gire o robô

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

3. Verifique se a orientação está correta

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

4. Coordenadas do corpo do robô

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
Editar via GitHub O link abre em uma nova janela ou aba
Julia Exercism

Tudo pronto para começar Revolução dos robôs?

Crie sua conta no Exercism para aprender e dominar Julia com 35 conceitos128 exercícios e mentoria humana de verdade, tudo de graça.