IA do zero: Gradient Descent — otimizando seu neurônio em rust
Você já usou o GPT, mas sabe o que existe dentro dele? Neste post vamos ensinar o neurônio a melhorar suas previsões de forma inteligente usando gradient descent, o algoritmo que está por trás de praticamente todo o aprendizado de máquina moderno.
Conteúdo
- 1 Prólogo
- 2 O problema com o ±0.01
- 3 Medindo o erro geral — Loss (MSE)
- 4 A parábola — visualizando o loss
- 5 O gradiente — a inclinação como bússola
- 6 Gradient Descent — descendo a parábola
- 7 Implementando o Gradient Descent
- 8 Resultado — ±0.01 vs Gradient Descent
- 9 Conclusão
1. Prólogo
Retomando o post anterior, implementamos um neurônio capaz de aprender a prever a distância de um projétil com base na energia do disparo. O treinamento e os resultados foram razoáveis, mas o algoritmo de ajuste utilizado era propositalmente simples para fins educacionais.
Agora vamos entender e implementar o tão temido Gradient Descent.
Confesso que, durante meu aprendizado desse tópico, me assustei um pouco com as notações matemáticas, uso de derivadas e gráficos em três dimensões para dar um pequeno passo além do que parecia tão simples no último post. Mas, depois de estudar um pouco mais, acredito que consegui entender um pouco melhor o assunto então vou tentar compartilhar esse entendimento aqui.
2. O problema com o ±0.01
O treino anterior ajustava w e b com um passo fixo:
if error > 0.0 {
self.weight -= 0.01;
self.bias -= 0.01;
} else if error < 0.0 {
self.weight += 0.01;
self.bias += 0.01;
}
O problema aparece na prática:
erro = 500 → ajusta 0.01
erro = 0.5 → ajusta 0.01
Quando o neurônio está muito errado, deveria dar passos grandes e quando está quase certo, passos pequenos para não passar do ponto.
O que precisamos é de um ajuste proporcional ao erro. É aí que entra o loss e depois o gradient descent.
3. Medindo o erro geral — Loss (MSE)
Já calculamos o erro de um item no dataset de forma simples no post anterior
erro = previsto - real
Mas precisamos entender o quão errado o neurônio está no conjunto inteiro de dados, e não apenas em um exemplo específico. Se ficarmos ajustando as variáveis somente com base no erro de um par do dataset, acabamos prejudicando o resultado nos outros. Precisamos, portanto, de uma forma de calcular o erro médio sobre todo o dataset — isso é o que chamaremos de loss.
Para isso precisamos da média dos erros
loss = (erro1 + erro2 + erro3) / n
O problema é que erros se cancelam:
ponto A: erro = +30 (previu demais)
ponto B: erro = -30 (previu de menos)
média = 0 ← parece perfeito, mas está errado nos dois casos
A solução é o MSE — Mean Squared Error (Erro Quadrático Médio):
loss = (1/n) × Σ (previsto - real)²
Elevar ao quadrado tem dois efeitos:
- Remove o sinal: erros positivos e negativos não se cancelam mais
- Penaliza erros grandes: um erro de 10 vira 100, um erro de 2 vira 4
erro = 2 → contribuição = 4
erro = 10 → contribuição = 100
Um erro cinco vezes maior gera uma penalização vinte e cinco vezes maior, o loss deve gritar quando o neurônio está muito errado.
pub fn loss(dataset: &[(f64, f64)], neuron: &Neuron) -> f64 {
let n = dataset.len() as f64;
let sum: f64 = dataset
.iter()
.map(|(x, actual)| {
let error = neuron.predict(*x) - actual;
error * error
})
.sum();
sum / n
}
MSE é a fórmula. Loss é o conceito. A partir daqui vamos usar o termo loss para falar do erro geral do neurônio.
4. A parábola — visualizando o loss
Agora que temos o loss, podemos calcular o loss para cada valor possível de w e plotar o resultado. Para isso, fixamos b=18 como estimativa inicial, isolando o efeito de w no gráfico.
Podemos observar que o gráfico tem formato de parábola caindo até um mínimo e sobe novamente, o ponto onde w=0.92 é o valor ideal para b=18, o mínimo do loss nesse dataset.
O objetivo do treino é encontrar os valores de w e b que reduzem ao máximo o loss.
Na prática, quando temos dois parâmetros (w e b), o loss vira uma superfície 3D — algo parecido com uma tigela. Nosso objetivo continua o mesmo: atingir o ponto mais fundo. O gradient descent faz isso descendo essa superfície simultaneamente nos dois eixos.
5. O gradiente — a inclinação como bússola
Você está em algum ponto da parábola e quer chegar no fundo, o gradiente é a inclinação da curva naquele ponto e ele te diz duas coisas:
- Direção: se a curva está subindo, você vai na direção oposta
- Magnitude: curva íngreme (longe do fundo) → passo grande. Quase plana (perto do fundo) → passo pequeno
w=0 → inclinação íngreme → passo grande
w=0.8 → inclinação suave → passo pequeno
w=0.9 → fundo da parábola → gradiente = 0, para
É exatamente o que faltava no ±0.01: o passo proporcional à inclinação, não fixo.
Os gradientes do loss em relação a w e b são:
∂loss/∂w = (2 / dataset_size) * Σ(erro * x)
∂loss/∂b = (2 / dataset_size) * Σ(erro)
-
wmultiplica porxporquewestá ligado axna equação do neurônio (y = wx + b). -
bacumula apenas o erro, pois é um deslocamento constante, não depende dex.
Não precisa se assustar com essas fórmulas, pois não vamos derivá-las agora.
Por enquanto, basta entender que elas medem a inclinação do erro em relação a w e b, indicando para qual direção devemos mover os parâmetros e o quão forte deve ser essa atualização.
Na implementação, veremos que calcular os gradientes é bem mais simples do que a notação matemática sugere.
6. Gradient Descent — descendo a parábola
Agora que entendemos que precisamos alterar w e b em direção ao mínimo podemos aplicar essa fórmula
erro = previsto - real
dataset_size = dataset.len
∂L/∂w = (2 / dataset_size) * Σ(erro * x)
∂L/∂b = (2 / dataset_size) * Σ(erro)
Gradient Descent
w = w - lr * ∂loss/∂w
b = b - lr * ∂loss/∂b
Assim nasce o Gradient Descent, para aplicar vamos primeiro definir um valor de lr=0.0001, que é o tamanho do passo que vamos dar.
lr muito grande → passa do fundo, fica oscilando
lr muito pequeno → chega lá, mas demora muito
lr ideal → desce suave até o mínimo
Para aplicar vamos definir alguns valores iniciais
w = 10.0
b = 10.0
lr = 0.0001
dataset_size = 4
Também vamos definir um dataset
(1, 60)
(2, 80)
(10, 60)
(4, 70)
Com w=10, b=10 o neurônio calcula previsto = 10x + 10. Agora acumulamos Σ(erro) e Σ(erro * x) ponto a ponto:
| posição | x | real | previsto | erro = previsto - real | erro × x |
|---|---|---|---|---|---|
| 1 | 1 | 60 | 20 | -40 | -40 |
| 2 | 2 | 80 | 30 | -50 | -100 |
| 3 | 10 | 60 | 110 | 50 | 500 |
| 4 | 4 | 70 | 50 | -20 | -80 |
Calculando os somatórios Σ(erro) e Σ(erro * x)
Σ(erro * x) = -40 + -100 + 500 + -80 = 280
Σ(erro) = -40 + -50 + 50 + -20 = -60
Calculando as derivadas ∂L/∂w e ∂L/∂b
∂L/∂w = (2 / 4) * 280 = 140
∂L/∂b = (2 / 4) * -60 = -30
Calculando Gradient Descent
weight = 10.0 - 0.0001 * 140 → 9.986
bias = 10.0 - 0.0001 * (-30) → 10.003
Chegamos em um ajuste de w=10 para w=9.986 e b=10 para b=10.003. Com ∂L/∂w = +140 o gradiente é positivo, então diminuímos w, o neurônio estava prevendo demais em x=10. Com ∂L/∂b = -30 o gradiente é negativo, então aumentamos b, a maioria dos pontos estava sendo subestimada.
7. Implementando o Gradient Descent
Agora que calculamos manualmente os valores chegou a parte mais fácil que é implementar nosso algoritmo.
1. Inicializa Σ(erro * x) e Σ(erro)
let mut error_x_sum = 0.0; // Σ(erro * x)
let mut error_sum = 0.0; // Σ(erro)
2. Acumula os erros do dataset
Para cada ponto calculamos o erro e acumulamos os dois somatórios.
for (x, actual) in dataset {
let error = self.predict(*x) - actual;
error_x_sum += error * x;
error_sum += error;
}
3. Calcula as derivadas ∂L/∂w e ∂L/∂b
Transformamos os somatórios nos gradientes finais dividindo pelo tamanho do dataset.
let grad_w = (2.0 / dataset_size) * error_x_sum; // ∂L/∂w
let grad_b = (2.0 / dataset_size) * error_sum; // ∂L/∂b
4. Aplica o Gradient Descent
Andamos na direção oposta ao gradiente (por isso o -), com passo controlado pelo lr.
-
grad_wpositivo → diminuímosw -
grad_wnegativo → aumentamosw -
grad_wpróximo de zero → estamos perto do mínimo
self.weight -= lr * grad_w;
self.bias -= lr * grad_b;
5. Implementação completa
pub fn train(&mut self, dataset: &[(f64, f64)], lr: f64, epochs: usize) {
let dataset_size = dataset.len() as f64;
for _epoch in 0..epochs {
let mut error_x_sum = 0.0;
let mut error_sum = 0.0;
for (x, actual) in dataset {
let error = self.predict(*x) - actual;
error_x_sum += error * x;
error_sum += error;
}
let grad_w = (2.0 / dataset_size) * error_x_sum;
let grad_b = (2.0 / dataset_size) * error_sum;
self.weight -= lr * grad_w;
self.bias -= lr * grad_b;
}
}
8. Resultado — ±0.01 vs Gradient Descent
Rodamos os dois algoritmos com as mesmas condições iniciais:
| ±0.01 | Gradient Descent | |
|---|---|---|
| Epochs | 1000 | 1000 |
| Passo | fixo 0.01
|
proporcional ao gradiente |
| Learning rate | — | 0.0003 |
| W inicial | 5.0 | 5.0 |
| B inicial | 5.0 | 5.0 |
Com isso obtivemos os seguintes resultados
O que o gradient descent faz é encontrar a melhor reta possível dentro dessa limitação e o loss mostra isso claramente caindo de 77 no ±0.01 para 37. No gráfico abaixo podemos observar isso
Outro ponto que vale destacar é a forma como o gradient descent realiza os ajustes de w e b, cada ponto é uma epoch. Com ±0.01 dá passos iguais o tempo todo, mesmo perto do fundo continua com a mesma força, já o gradient descent desacelera conforme se aproxima e para quando o gradiente chega a zero. No gráfico abaixo podemos observar isso
Ainda assim a linha não tocou perfeitamente todos os pontos, para isso seriam necessários mais parâmetros e mais neurônios, ou seja, uma rede neural. Isso vem nas próximas fases.
9. Conclusão
Neste post saímos de um ajuste cego e implementamos gradient descent, o algoritmo base do aprendizado de máquina moderno.
O que foi aprendido:
- O loss (MSE) transforma os erros individuais num único número que representa o desempenho geral do neurônio
- O gradiente é a inclinação do loss em relação a cada parâmetro e ele diz a direção e o tamanho do passo
- O gradient descent desce a superfície de loss simultaneamente em
webaté encontrar o mínimo
O que ainda não resolvemos: com um único neurônio e uma função linear, o melhor que conseguimos é uma reta e dados que não seguem uma reta linear não podem ser modelados assim, independente de quantas epochs ou de qual algoritmo de treino.
No próximo post: redes neurais, múltiplos neurônios em camadas que juntos conseguem aprender padrões não-lineares.
Referências
- Código-fonte do projeto
- Neural Network from Scratch — vídeo que inspirou essa série
- Gradient Descent — Wikipedia
Se este post fizer sentido pra você, o próximo passo natural é adicionar mais neurônios e introduzir funções de ativação é aí que o aprendizado começa a capturar padrões que uma reta simples não consegue descrever.






Top comments (0)