DEV Community

Cover image for Gradient Descent — opimizando seu neurônio em rust
Alexandre
Alexandre

Posted on

Gradient Descent — opimizando seu neurônio em rust

IA do zero: Gradient Descent — otimizando seu neurônio em rust

banner do projeto

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

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;
}
Enter fullscreen mode Exit fullscreen mode

O problema aparece na prática:

erro = 500  → ajusta 0.01
erro = 0.5  → ajusta 0.01
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

A solução é o MSE — Mean Squared Error (Erro Quadrático Médio):

loss = (1/n) × Σ (previsto - real)²
Enter fullscreen mode Exit fullscreen mode

Elevar ao quadrado tem dois efeitos:

  1. Remove o sinal: erros positivos e negativos não se cancelam mais
  2. 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
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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.

parábola do loss variando w com b fixo em 18, mostrando o mínimo em w=0.92

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.

superfície 3D do loss em função de w e b, com destaque no ponto mínimo


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
Enter fullscreen mode Exit fullscreen mode

É 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)
Enter fullscreen mode Exit fullscreen mode
  1. w multiplica por x porque w está ligado a x na equação do neurônio (y = wx + b).
  2. b acumula apenas o erro, pois é um deslocamento constante, não depende de x.

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Para aplicar vamos definir alguns valores iniciais

w  = 10.0
b  = 10.0
lr = 0.0001
dataset_size = 4
Enter fullscreen mode Exit fullscreen mode

Também vamos definir um dataset

(1,  60)
(2,  80)
(10, 60)
(4,  70)
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Calculando as derivadas ∂L/∂w e ∂L/∂b

∂L/∂w = (2 / 4) * 280 = 140
∂L/∂b = (2 / 4) * -60 = -30
Enter fullscreen mode Exit fullscreen mode

Calculando Gradient Descent

weight = 10.0 - 0.0001 * 140     9.986
bias   = 10.0 - 0.0001 * (-30)   10.003
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

4. Aplica o Gradient Descent

Andamos na direção oposta ao gradiente (por isso o -), com passo controlado pelo lr.

  • grad_w positivo → diminuímos w
  • grad_w negativo → aumentamos w
  • grad_w próximo de zero → estamos perto do mínimo
self.weight -= lr * grad_w;
self.bias   -= lr * grad_b;
Enter fullscreen mode Exit fullscreen mode

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;
    }
}
Enter fullscreen mode Exit fullscreen mode

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

comparação dos ajustes de ±0.01 e gradient descent sobre o dataset

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

curva de loss ao longo das epochs comparando ±0.01 e gradient descent em escala log

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

caminho de cada algoritmo na parábola epoch a epoch, mostrando passos fixos vs proporcionais

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 w e b até 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


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)