Phase 01 - Lesson 01
Intuição de Álgebra Linear
This lesson includes a graded coding exercise that runs in your browser, unlocked with lifetime access.
Todo modelo de IA é apenas matemática de matrizes vestida com um chapéu chique.
Tipo: Aprender Linguagens: Python, Julia Pré-requisitos: Fase 0 Tempo: ~60 minutos
Objetivos de Aprendizagem
- Implementar operações de vetores e matrizes (adição, produto escalar, multiplicação de matrizes) do zero em Python
- Explicar geometricamente o que o produto escalar, a projeção e o processo de Gram-Schmidt fazem
- Determinar a independência linear, o posto (rank) e a base de um conjunto de vetores usando redução por linhas
- Conectar conceitos de álgebra linear às suas aplicações em IA: embeddings, scores de atenção e LoRA
O Problema
Abra qualquer artigo de ML. Logo na primeira página, você verá vetores, matrizes, produtos escalares e transformações. Sem intuição de álgebra linear, isso é apenas um amontoado de símbolos. Com ela, você consegue ver o que uma rede neural está realmente fazendo -- movendo pontos pelo espaço.
Você não precisa ser matemático. Você precisa enxergar o que essas operações significam geometricamente e, então, programá-las você mesmo.
O Conceito
Vetores São Pontos (e Direções)
Um vetor é apenas uma lista de números. Mas esses números significam algo -- são coordenadas no espaço.
Vetor 2D [3, 2]:
| x | y | Ponto |
|---|---|---|
| 3 | 2 | O vetor aponta da origem (0,0) para (3, 2) no plano |
O vetor tem magnitude sqrt(3^2 + 2^2) = sqrt(13) e aponta para cima e para a direita.
Em IA, vetores representam tudo:
- Uma palavra → um vetor de 768 números (seu "significado" no espaço de embedding)
- Uma imagem → um vetor de milhões de valores de pixel
- Um usuário → um vetor de preferências
Matrizes São Transformações
Uma matriz transforma um vetor em outro. Ela pode rotacionar, escalar, esticar ou projetar.
graph LR
subgraph Before
A["Point A"]
B["Point B"]
end
subgraph Matrix["Matrix Multiplication"]
M["M (transformation)"]
end
subgraph After
A2["Point A'"]
B2["Point B'"]
end
A --> M
B --> M
M --> A2
M --> B2
Em IA, matrizes SÃO o modelo:
- Pesos de redes neurais → matrizes que transformam a entrada em saída
- Scores de atenção → matrizes que decidem no que focar
- Embeddings → matrizes que mapeiam palavras para vetores
O Produto Escalar Mede Similaridade
O produto escalar de dois vetores te diz o quão similares eles são.
a · b = a₁×b₁ + a₂×b₂ + ... + aₙ×bₙ
Same direction: a · b > 0 (similar)
Perpendicular: a · b = 0 (unrelated)
Opposite direction: a · b < 0 (dissimilar)
É literalmente assim que mecanismos de busca, sistemas de recomendação e RAG funcionam -- encontrar vetores com produtos escalares altos.
Independência Linear
Os vetores são linearmente independentes se nenhum vetor do conjunto pode ser escrito como uma combinação dos outros. Se v1, v2, v3 são independentes, eles geram um espaço 3D. Se um deles for uma combinação dos outros, eles geram apenas um plano.
Por que isso importa para IA: sua matriz de features deve ter colunas linearmente independentes. Se duas features são perfeitamente correlacionadas (linearmente dependentes), o modelo não consegue distinguir seus efeitos. Isso causa multicolinearidade na regressão -- a matriz de pesos fica instável, e pequenas mudanças na entrada produzem oscilações selvagens na saída.
Exemplo concreto:
v1 = [1, 0, 0]
v2 = [0, 1, 0]
v3 = [2, 1, 0] # v3 = 2*v1 + v2
v1 e v2 são independentes -- nenhum é um múltiplo escalar ou combinação do outro. Mas v3 = 2*v1 + v2, então {v1, v2, v3} é um conjunto dependente. Esses três vetores estão todos no plano xy. Não importa como você os combine, você não consegue alcançar [0, 0, 1]. Você tem três vetores, mas apenas duas dimensões de liberdade.
Em um conjunto de dados: se feature_3 = 2*feature_1 + feature_2, adicionar feature_3 dá ao modelo zero informação nova. Pior ainda, isso torna as equações normais singulares -- não existe solução única para os pesos.
Base e Posto (Rank)
Uma base é um conjunto mínimo de vetores linearmente independentes que geram o espaço inteiro. O número de vetores da base é a dimensão do espaço.
A base padrão para o espaço 3D é {[1,0,0], [0,1,0], [0,0,1]}. Mas quaisquer três vetores independentes em 3D formam uma base válida. A escolha da base é uma escolha de sistema de coordenadas.
O posto (rank) de uma matriz = número de colunas linearmente independentes = número de linhas linearmente independentes. Se posto < min(linhas, colunas), a matriz tem posto deficiente. Isso significa:
- O sistema tem infinitas soluções (ou nenhuma)
- Informação é perdida na transformação
- A matriz não pode ser invertida
| Situação | Posto | O que significa para ML |
|---|---|---|
| Posto completo (posto = min(m, n)) | Máximo possível | Existe solução única de mínimos quadrados. O modelo está bem condicionado. |
| Posto deficiente (posto < min(m, n)) | Abaixo do máximo | As features são redundantes. Infinitas soluções de pesos. Regularização necessária. |
| Posto 1 | 1 | Toda coluna é uma cópia escalada de um vetor. Todos os dados estão em uma linha. |
| Quase posto deficiente (valores singulares pequenos) | Numericamente baixo | A matriz é mal condicionada. Um pequeno ruído na entrada causa grandes mudanças na saída. Use truncamento de SVD ou regressão ridge. |
Projeção
Projetar o vetor a sobre o vetor b dá a componente de a na direção de b:
proj_b(a) = (a dot b / b dot b) * b
O resíduo (a - proj_b(a)) é perpendicular a b. Essa decomposição ortogonal é a base do ajuste por mínimos quadrados.
A projeção está em todo lugar em ML:
- A regressão linear minimiza a distância das observações ao espaço coluna -- a solução É uma projeção
- O PCA projeta os dados sobre as direções de máxima variância
- A atenção em transformers calcula projeções de queries sobre keys
graph LR
subgraph Projection["Projection of a onto b"]
direction TB
O["Origin"] --> |"b (direction)"| B["b"]
O --> |"a (original)"| A["a"]
O --> |"proj_b(a)"| P["projection"]
A -.-> |"residual (perpendicular)"| P
end
Exemplo: a = [3, 4], b = [1, 0]
proj_b(a) = (31 + 40) / (11 + 00) * [1, 0] = 3 * [1, 0] = [3, 0]
A projeção descarta a componente y. Essa é a redução de dimensionalidade na sua forma mais simples -- jogar fora as direções com as quais você não se importa.
Processo de Gram-Schmidt
Converter qualquer conjunto de vetores independentes em uma base ortonormal. Ortonormal significa que todo vetor tem comprimento 1 e todo par é perpendicular.
O algoritmo:
- Pegue o primeiro vetor, normalize-o
- Pegue o segundo vetor, subtraia sua projeção sobre o primeiro, normalize
- Pegue o terceiro vetor, subtraia suas projeções sobre todos os vetores anteriores, normalize
- Repita para os vetores restantes
Input: v1, v2, v3, ... (linearly independent)
u1 = v1 / |v1|
w2 = v2 - (v2 dot u1) * u1
u2 = w2 / |w2|
w3 = v3 - (v3 dot u1) * u1 - (v3 dot u2) * u2
u3 = w3 / |w3|
Output: u1, u2, u3, ... (orthonormal basis)
É assim que a decomposição QR funciona internamente. Q é a base ortonormal, R captura os coeficientes de projeção. A decomposição QR é usada em:
- Resolução de sistemas lineares (mais estável que a eliminação gaussiana)
- Cômputo de autovalores (algoritmo QR)
- Regressão por mínimos quadrados (o método numérico padrão)
Construa
Passo 1: Vetores do zero (Python)
class Vector:
def __init__(self, components):
self.components = list(components)
self.dim = len(self.components)
def __add__(self, other):
return Vector([a + b for a, b in zip(self.components, other.components)])
def __sub__(self, other):
return Vector([a - b for a, b in zip(self.components, other.components)])
def dot(self, other):
return sum(a * b for a, b in zip(self.components, other.components))
def magnitude(self):
return sum(x**2 for x in self.components) ** 0.5
def normalize(self):
mag = self.magnitude()
return Vector([x / mag for x in self.components])
def cosine_similarity(self, other):
return self.dot(other) / (self.magnitude() * other.magnitude())
def __repr__(self):
return f"Vector({self.components})"
a = Vector([1, 2, 3])
b = Vector([4, 5, 6])
print(f"a + b = {a + b}")
print(f"a · b = {a.dot(b)}")
print(f"|a| = {a.magnitude():.4f}")
print(f"cosine similarity = {a.cosine_similarity(b):.4f}")
Passo 2: Matrizes do zero (Python)
class Matrix:
def __init__(self, rows):
self.rows = [list(row) for row in rows]
self.shape = (len(self.rows), len(self.rows[0]))
def __matmul__(self, other):
if isinstance(other, Vector):
return Vector([
sum(self.rows[i][j] * other.components[j] for j in range(self.shape[1]))
for i in range(self.shape[0])
])
rows = []
for i in range(self.shape[0]):
row = []
for j in range(other.shape[1]):
row.append(sum(
self.rows[i][k] * other.rows[k][j]
for k in range(self.shape[1])
))
rows.append(row)
return Matrix(rows)
def transpose(self):
return Matrix([
[self.rows[j][i] for j in range(self.shape[0])]
for i in range(self.shape[1])
])
def __repr__(self):
return f"Matrix({self.rows})"
rotation_90 = Matrix([[0, -1], [1, 0]])
point = Vector([3, 1])
rotated = rotation_90 @ point
print(f"Original: {point}")
print(f"Rotated 90°: {rotated}")
Passo 3: Por que isso importa para IA
import random
random.seed(42)
weights = Matrix([[random.gauss(0, 0.1) for _ in range(3)] for _ in range(2)])
input_vector = Vector([1.0, 0.5, -0.3])
output = weights @ input_vector
print(f"Input (3D): {input_vector}")
print(f"Output (2D): {output}")
print("This is what a neural network layer does -- matrix multiplication.")
Passo 4: Versão em Julia
a = [1.0, 2.0, 3.0]
b = [4.0, 5.0, 6.0]
println("a + b = ", a + b)
println("a · b = ", a ⋅ b) # Julia supports unicode operators
println("|a| = ", √(a ⋅ a))
println("cosine = ", (a ⋅ b) / (√(a ⋅ a) * √(b ⋅ b)))
# Matrix-vector multiplication
W = [0.1 -0.2 0.3; 0.4 0.5 -0.1]
x = [1.0, 0.5, -0.3]
println("Wx = ", W * x)
println("This is a neural network layer.")
Passo 5: Independência linear e projeção do zero (Python)
def is_linearly_independent(vectors):
n = len(vectors)
dim = len(vectors[0].components)
mat = Matrix([v.components[:] for v in vectors])
rows = [row[:] for row in mat.rows]
rank = 0
for col in range(dim):
pivot = None
for row in range(rank, len(rows)):
if abs(rows[row][col]) > 1e-10:
pivot = row
break
if pivot is None:
continue
rows[rank], rows[pivot] = rows[pivot], rows[rank]
scale = rows[rank][col]
rows[rank] = [x / scale for x in rows[rank]]
for row in range(len(rows)):
if row != rank and abs(rows[row][col]) > 1e-10:
factor = rows[row][col]
rows[row] = [rows[row][j] - factor * rows[rank][j] for j in range(dim)]
rank += 1
return rank == n
def project(a, b):
scalar = a.dot(b) / b.dot(b)
return Vector([scalar * x for x in b.components])
def gram_schmidt(vectors):
orthonormal = []
for v in vectors:
w = v
for u in orthonormal:
proj = project(w, u)
w = w - proj
if w.magnitude() < 1e-10:
continue
orthonormal.append(w.normalize())
return orthonormal
v1 = Vector([1, 0, 0])
v2 = Vector([1, 1, 0])
v3 = Vector([1, 1, 1])
basis = gram_schmidt([v1, v2, v3])
for i, u in enumerate(basis):
print(f"u{i+1} = {u}")
print(f" |u{i+1}| = {u.magnitude():.6f}")
print(f"u1 · u2 = {basis[0].dot(basis[1]):.6f}")
print(f"u1 · u3 = {basis[0].dot(basis[2]):.6f}")
print(f"u2 · u3 = {basis[1].dot(basis[2]):.6f}")
Use
Agora a mesma coisa com NumPy -- o que você vai realmente usar na prática:
import numpy as np
a = np.array([1, 2, 3], dtype=float)
b = np.array([4, 5, 6], dtype=float)
print(f"a + b = {a + b}")
print(f"a · b = {np.dot(a, b)}")
print(f"|a| = {np.linalg.norm(a):.4f}")
print(f"cosine = {np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b)):.4f}")
W = np.random.randn(2, 3) * 0.1
x = np.array([1.0, 0.5, -0.3])
print(f"Wx = {W @ x}")
Posto, Projeção e QR com NumPy
import numpy as np
A = np.array([[1, 2], [2, 4]])
print(f"Rank: {np.linalg.matrix_rank(A)}")
a = np.array([3, 4])
b = np.array([1, 0])
proj = (np.dot(a, b) / np.dot(b, b)) * b
print(f"Projection of {a} onto {b}: {proj}")
Q, R = np.linalg.qr(np.random.randn(3, 3))
print(f"Q is orthogonal: {np.allclose(Q @ Q.T, np.eye(3))}")
print(f"R is upper triangular: {np.allclose(R, np.triu(R))}")
PyTorch -- Tensores São Vetores com Autodiff
import torch
x = torch.randn(3, requires_grad=True)
y = torch.tensor([1.0, 0.0, 0.0])
similarity = torch.dot(x, y)
similarity.backward()
print(f"x = {x.data}")
print(f"y = {y.data}")
print(f"dot product = {similarity.item():.4f}")
print(f"d(dot)/dx = {x.grad}")
O gradiente do produto escalar em relação a x é simplesmente y. O PyTorch calculou isso automaticamente. Toda operação em uma rede neural é construída a partir de operações como esta -- multiplicações de matrizes, produtos escalares, projeções -- e o autodiff rastreia os gradientes através de todas elas.
Você acabou de construir do zero o que o NumPy faz em uma linha. Agora você sabe o que está acontecendo por baixo dos panos.
Entregue
Esta lição produz:
outputs/prompt-linear-algebra-tutor.md-- um prompt para assistentes de IA ensinarem álgebra linear por meio de intuição geométrica
Conexões
Tudo nesta lição se conecta a partes específicas da IA moderna:
| Conceito | Onde aparece |
|---|---|
| Produto escalar | Scores de atenção em transformers, similaridade de cosseno em RAG |
| Multiplicação de matrizes | Toda camada de rede neural, toda transformação linear |
| Independência linear | Seleção de features, evitar multicolinearidade |
| Posto | Determinar se um sistema tem solução, LoRA (low-rank adaptation) |
| Projeção | Regressão linear (projetar no espaço coluna), PCA |
| Gram-Schmidt / QR | Resolvedores numéricos, cômputo de autovalores |
| Base ortonormal | Computação numérica estável, transformações de whitening |
O LoRA merece menção especial. Ele faz fine-tuning de grandes modelos de linguagem decompondo as atualizações de pesos em matrizes de posto baixo. Em vez de atualizar uma matriz de pesos 4096x4096 (16M de parâmetros), o LoRA atualiza duas matrizes de tamanho 4096x16 e 16x4096 (131K de parâmetros). A restrição de posto 16 significa que o LoRA assume que a atualização de pesos vive em um subespaço de 16 dimensões do espaço completo de 4096 dimensões. Isso é álgebra linear fazendo trabalho de verdade.
Exercícios
- Implemente
Vector.angle_between(other)que retorna o ângulo em graus entre dois vetores - Crie uma matriz de escala 2D que dobra a coordenada x e triplica a coordenada y, depois aplique-a ao vetor [1, 1]
- Dados 5 vetores aleatórios semelhantes a palavras (dimensão 50), encontre os dois mais similares usando a similaridade de cosseno
- Verifique que a saída do Gram-Schmidt é realmente ortonormal: confira que todo par tem produto escalar 0 e todo vetor tem magnitude 1
- Crie uma matriz 3x3 com posto 2. Verifique usando o método
rank(). Depois explique qual objeto geométrico as colunas geram. - Projete o vetor [1, 2, 3] sobre [1, 1, 1]. O que o resultado representa geometricamente?
Termos-chave
| Termo | O que as pessoas dizem | O que realmente significa |
|---|---|---|
| Vetor | "Uma flecha" | Uma lista de números representando um ponto ou direção em um espaço n-dimensional |
| Matriz | "Uma tabela de números" | Uma transformação que mapeia vetores de um espaço para outro |
| Produto escalar | "Multiplicar e somar" | Uma medida de quão alinhados dois vetores estão -- o núcleo da busca por similaridade |
| Embedding | "Alguma mágica de IA" | Um vetor que representa o significado de algo (palavra, imagem, usuário) |
| Independência linear | "Eles não se sobrepõem" | Nenhum vetor do conjunto pode ser escrito como combinação dos outros |
| Posto (Rank) | "Quantas dimensões" | O número de colunas (ou linhas) linearmente independentes em uma matriz |
| Projeção | "A sombra" | A componente de um vetor na direção de outro |
| Base | "Os eixos coordenados" | Um conjunto mínimo de vetores independentes que geram o espaço |
| Ortonormal | "Vetores unitários perpendiculares" | Vetores mutuamente perpendiculares e cada um com comprimento 1 |