Capítulo 23, Intermediário
Álgebra linear
Resolver sistemas de equações, medir distâncias, ajustar uma reta aos dados: a álgebra linear está por baixo de quase tudo em ciência de dados, e o NumPy a entrega pronta.
Código deste capítulo: intermediario/cap23_algebra_linear.py
Multiplicação de matrizes
Há duas multiplicações, e confundi-las é um erro clássico. O * multiplica elemento a elemento. O @ (ou np.matmul) é a multiplicação de matrizes, linhas por colunas:
import numpy as np
A = np.array([[2.0, 1.0], [1.0, 3.0]])
b = np.array([3.0, 5.0])
print(A @ A)
print(A.T @ b)
print(A * A)
print(np.array_equal(A * A, A @ A))
[[ 5. 5.]
[ 5. 10.]]
[11. 18.]
[[4. 1.]
[1. 9.]]
False
Para vetores, o @ é o produto escalar. Com ele e com a norma (o comprimento do vetor) se calcula o cosseno do ângulo entre dois vetores, uma medida de similaridade usada em busca e recomendação:
u = np.array([3.0, 4.0])
v = np.array([1.0, 0.0])
print(u @ v, np.linalg.norm(u), (u @ v) / (np.linalg.norm(u) * np.linalg.norm(v)))
3.0 5.0 0.6
Resolver um sistema de equações
O sistema 2x + y = 3 e x + 3y = 5 se escreve como A @ x = b. O np.linalg.solve o resolve, e conferir é simples: multiplicar de volta e comparar:
x = np.linalg.solve(A, b)
print(x, np.allclose(A @ x, b))
[0.8 1.4] True
Não calcule a inversa para resolver sistemas
Muita gente escreve
inv(A) @ b. Evite: calcular a inversa é mais lento e menos preciso do que osolve. Use osolvesempre que o objetivo for resolver um sistema, e deixe a inversa para quando você precisa dela de verdade.
Determinante, inversa e autovalores
print(round(np.linalg.det(A), 6))
print(np.round(np.linalg.inv(A), 3))
valores, vetores = np.linalg.eigh(A)
print(np.round(valores, 3))
5.0
[[ 0.6 -0.2]
[-0.2 0.4]]
[1.382 3.618]
O eigh é a versão para matrizes simétricas, mais rápida e mais estável do que a geral (eig). Os autovalores descrevem, de forma resumida, "quanto a matriz estica" cada direção, e estão por trás de técnicas como a análise de componentes principais.
Ajustar uma reta: mínimos quadrados
Dado um conjunto de pontos com ruído, o np.linalg.lstsq encontra a reta que minimiza o erro quadrático. Basta montar a matriz com a coluna de x e uma coluna de uns (para o intercepto):
rng = np.random.default_rng(1)
xs = np.linspace(0, 10, 50)
ys = 3 * xs + 2 + rng.normal(0, 0.5, 50)
X = np.column_stack([xs, np.ones_like(xs)])
(coef, intercepto), *_ = np.linalg.lstsq(X, ys, rcond=None)
print(abs(coef - 3) < 0.1, abs(intercepto - 2) < 0.5)
True True
Eu gerei os dados com inclinação 3 e intercepto 2, mais ruído, e o ajuste os recupera com boa precisão. É o mesmo cálculo que o capítulo 40 compara com o scikit-learn.
Lotes de matrizes
O @ funciona em lotes: ele multiplica as duas últimas dimensões e trata as anteriores como lote (com broadcasting). É assim que uma rede neural processa vários exemplos de uma vez:
lote_a = np.ones((10, 3, 4))
lote_b = np.ones((10, 4, 2))
print((lote_a @ lote_b).shape)
(10, 3, 2)
Exercício 1
Projetar um vetor sobre outro
Escreva projetar(u, v), a projeção de u na direção de v: (u · v) / (v · v) * v.