Bem-vindo de volta! Na Parte 1, vimos como uma rede neural poderia somar dois números usando uma estrutura feedforward básica. No final do artigo, até mostramos uma implementação completa em C++ que incluía a passagem para frente, o loop de treinamento, o cálculo da perda e as atualizações de peso—tudo em código.

Isso naturalmente leva à próxima pergunta: como exatamente uma rede neural aprende a ajustar esses pesos sozinha durante o treinamento? É exatamente isso que vamos explorar aqui na Parte 2.

Antes de irmos mais longe, aqui está o código completo em C++ da Parte 1 que implementa uma rede neural simples para aprender a somar dois números. Ele inclui a passagem para frente, cálculo da perda, retropropagação e atualizações de peso:

#include <iostream>
#include <cmath>
using namespace std;

// Função de ativação sigmoid
float sigmoid(float x) {
    return 1.0f / (1.0f + exp(-x));
}

// Derivada da sigmoid (usada na retropropagação)
float sigmoid_derivative(float y) {
    return y * (1 - y); // onde y = sigmoid(x)
}

int main() {
    // Inicializa pesos e vieses para 3 nós ocultos
    // Esses valores são escolhidos manualmente para mostrar o processo de aprendizado de forma mais clara.
    // Usar pesos positivos e negativos ajuda a prevenir simetria e fornece um ponto de partida diversificado.
    float weight1_node1 = 0.5f, weight2_node1 = 0.5f, bias_node1 = 0.0f;      // O Nó 1 começa com influência positiva de ambas as entradas
    float weight1_node2 = -0.5f, weight2_node2 = -0.5f, bias_node2 = 0.0f;    // O Nó 2 começa com influência negativa, incentivando o equilíbrio
    float weight1_node3 = 0.25f, weight2_node3 = -0.25f, bias_node3 = 0.0f;   // O Nó 3 é misto, útil para capturar interações

    // Inicializa pesos e viés da camada de saída
    float weight_out_node1 = 0.5f, weight_out_node2 = 0.5f, weight_out_node3 = 0.5f, bias_out = 0.0f;

    float learning_rate = 0.01f;
    int epochs = 1000; // Número de vezes que passamos pelos dados de treinamento

    // Loop de treinamento
    for (int epoch = 0; epoch < epochs; ++epoch) {
        float total_loss = 0.0f;
        for (int a = 0; a < 100; ++a) {
            for (int b = 0; b < 100; ++b) {
                float x1 = a / 100.0f;
                float x2 = b / 100.0f;
                float target = (a + b) / 200.0f; // Normaliza a soma alvo para [0, 1]

                // ===== Passagem para Frente =====
                float z1 = x1 * weight1_node1 + x2 * weight2_node1 + bias_node1;
                float h1 = sigmoid(z1);

                float z2 = x1 * weight1_node2 + x2 * weight2_node2 + bias_node2;
                float h2 = sigmoid(z2);

                float z3 = x1 * weight1_node3 + x2 * weight2_node3 + bias_node3;
                float h3 = sigmoid(z3);

                float y_pred = h1 * weight_out_node1 + h2 * weight_out_node2 + h3 * weight_out_node3 + bias_out;

                float error = target - y_pred;
                total_loss += error * error; // Erro quadrático (perda)

                // ===== Retropropagação - Camada de Saída =====
                float delta_out = error; // Derivada da perda em relação a y_pred
                float grad_out_w1 = delta_out * h1; // Gradiente para o peso de saída 1
                float grad_out_w2 = delta_out * h2; // Gradiente para o peso de saída 2
                float grad_out_w3 = delta_out * h3; // Gradiente para o peso de saída 3
                float grad_out_b = delta_out;       // Gradiente para o viés de saída

                // ===== Retropropagação - Camada Oculta =====
                // Calcula como cada nó oculto contribuiu para o erro
                float delta_h1 = delta_out * weight_out_node1 * sigmoid_derivative(h1);
                float delta_h2 = delta_out * weight_out_node2 * sigmoid_derivative(h2);
                float delta_h3 = delta_out * weight_out_node3 * sigmoid_derivative(h3);

                float grad_w1_node1 = delta_h1 * x1; // Gradiente para w1 do nó1
                float grad_w2_node1 = delta_h1 * x2; // Gradiente para w2 do nó1
                float grad_b_node1 = delta_h1;       // Gradiente para o viés do nó1

                float grad_w1_node2 = delta_h2 * x1;
                float grad_w2_node2 = delta_h2 * x2;
                float grad_b_node2 = delta_h2;

                float grad_w1_node3 = delta_h3 * x1;
                float grad_w2_node3 = delta_h3 * x2;
                float grad_b_node3 = delta_h3;

                // ===== Atualiza Pesos da Camada de Saída =====
                weight_out_node1 += learning_rate * grad_out_w1;
                weight_out_node2 += learning_rate * grad_out_w2;
                weight_out_node3 += learning_rate * grad_out_w3;
                bias_out += learning_rate * grad_out_b;

                // ===== Atualiza Pesos da Camada Oculta =====
                weight1_node1 += learning_rate * grad_w1_node1;
                weight2_node1 += learning_rate * grad_w2_node1;
                bias_node1 += learning_rate * grad_b_node1;

                weight1_node2 += learning_rate * grad_w1_node2;
                weight2_node2 += learning_rate * grad_w2_node2;
                bias_node2 += learning_rate * grad_b_node2;

                weight1_node3 += learning_rate * grad_w1_node3;
                weight2_node3 += learning_rate * grad_w2_node3;
                bias_node3 += learning_rate * grad_b_node3;
            }
        }
        // Registra a perda a cada 100 épocas
        if ((epoch + 1) % 100 == 0 || epoch == 0)
            cout << "[Resumo] Época " << epoch + 1 << ": Perda = " << total_loss << endl;
    }

    // ===== Testa o modelo com entrada do usuário =====
    float a, b;
    cout << "
Teste de previsão de soma (a + b)
Digite a: ";
    cin >> a;
    cout << "Digite b: ";
    cin >> b;

    float x1 = a / 100.0f;
    float x2 = b / 100.0f;

    // Passagem para frente novamente com pesos treinados
    float h1 = sigmoid(x1 * weight1_node1 + x2 * weight2_node1 + bias_node1);
    float h2 = sigmoid(x1 * weight1_node2 + x2 * weight2_node2 + bias_node2);
    float h3 = sigmoid(x1 * weight1_node3 + x2 * weight2_node3 + bias_node3);
    float y_pred = h1 * weight_out_node1 + h2 * weight_out_node2 + h3 * weight_out_node3 + bias_out;
    float predicted_sum = y_pred * 200.0f;

    // Saída do resultado
    cout << "Soma prevista: " << predicted_sum << "
Soma real: " << (a + b) << endl;
    return 0;
}

Nesta parte, vamos detalhar como uma rede neural aprende: desde a configuração de camadas e funções de ativação até o ajuste de pesos através da retropropagação. Você também aprenderá como treinamos o modelo usando taxas de aprendizado e épocas—e exploraremos se uma rede pode ir além de tarefas simples como adição.

  • O que os nós e camadas realmente fazem em uma rede neural.
  • Como definimos pesos iniciais e os ajustamos durante o treinamento.
  • O papel da taxa de aprendizado e das épocas na formação do processo de aprendizado.
  • O que são funções de ativação, como funcionam e quando usar sigmoid, tanh ou ReLU.
  • Como a retropropagação e as derivadas permitem que a rede aprenda com os erros.
  • Se uma rede neural pode ir além de tarefas simples como adição e aprender múltiplas coisas.

Pronto? Vamos lá!

Nós e Camadas: O que realmente está acontecendo?

Um neurônio (ou nó) é o bloco básico de construção de uma rede neural. Cada neurônio recebe valores de entrada, multiplica-os por pesos, adiciona um viés e, em seguida, passa o resultado por uma função de ativação para produzir uma saída. É como um pequeno tomador de decisões que transforma dados passo a passo.

Uma camada é uma coleção de neurônios que operam no mesmo nível da rede. A informação flui de uma camada para a próxima. Os três principais tipos de camadas são:

  • Camada de entrada: recebe os dados brutos (por exemplo, os números que você deseja somar).
  • Camadas ocultas: realizam transformações na entrada, aprendendo padrões através de conexões ponderadas.
  • Camada de saída: produz a previsão ou resultado final.

Cada neurônio em uma camada está conectado aos neurônios na próxima camada, e cada uma dessas conexões tem um peso que a rede aprende e ajusta durante o treinamento.

Pense em cada nó (neurônio) em uma rede neural como um pequeno tomador de decisões. Ele pega algumas entradas, multiplica-as por seus "pesos", adiciona um "viés", e então aplica uma função de ativação (como sigmoid, tanh ou ReLU).

Mais nós = mais poder cerebral:

  • Um pequeno número de nós pode resolver rapidamente problemas simples.
  • Mais nós significam que sua rede pode entender padrões mais complexos, mas muitos podem desacelerá-la ou fazê-la pensar demais (overfit)!

E se empilharmos camadas? Você pode empilhar várias camadas ocultas—isso é uma "rede neural profunda". Cada camada aprende algo um pouco mais abstrato:

  • A primeira camada aprende padrões simples.
  • A segunda camada combina esses padrões em ideias mais complexas.
  • Camadas terceira e posteriores constroem uma compreensão cada vez mais abstrata.

Pesos e Vieses em Redes Neurais

O que é um peso em uma rede neural?

Em uma rede neural, um peso é um valor que representa a força da conexão entre dois neurônios. Quando os dados de entrada fluem pela rede, eles são multiplicados por esses pesos. Um peso mais alto significa que a entrada tem uma influência mais forte na saída do próximo neurônio. Durante o treinamento, esses pesos são ajustados para ajudar a rede a fazer previsões melhores.

O que é um viés em uma rede neural?

Um viés é um parâmetro adicional em uma rede neural que permite que a ativação de um neurônio seja deslocada. Ele atua como um offset ou limite que ajuda o modelo a se ajustar melhor aos dados, permitindo que ele aprenda padrões que não passam pela origem. Assim como os pesos, os vieses são ajustados durante o treinamento para melhorar as previsões.

Como definimos os pesos iniciais?

Quando começamos o treinamento, os pesos devem ser inicializados com alguns valores. No nosso exemplo de código, simplesmente os definimos manualmente no início:

float weight1_node1 = 0.5f, weight2_node1 = 0.5f, bias_node1 = 0.0f;

Em sistemas mais complexos, os pesos são tipicamente inicializados aleatoriamente usando valores pequenos (como entre -0.5 e 0.5). Essa aleatoriedade ajuda a prevenir simetria—onde todos os neurônios aprendem a mesma coisa—e dá à rede uma chance melhor de descobrir padrões úteis. Estratégias comuns de inicialização incluem Xavier (Glorot) e He initialization, que são projetadas para manter um sinal estável enquanto flui pela rede. Para experimentos simples como o nosso, valores manuais ou pequenos valores aleatórios funcionam bem o suficiente.

Como ajustamos os pesos?

Durante o treinamento, aqui está como o ajuste funciona:

  1. A rede faz uma previsão usando os pesos atuais.
  2. Ela compara a previsão com a resposta correta (saída alvo).
  3. Ela calcula quão longe estava — esse é o erro ou "perda".
  4. Ela usa a retropropagação para calcular como cada peso afetou o erro.
  5. Cada peso é ajustado ligeiramente para reduzir o erro da próxima vez — esse passo é feito usando gradientes (derivadas) e uma taxa de aprendizado.

No nosso exemplo de código em C++, esse processo acontece dentro dos loops de treinamento aninhados. Aqui está uma referência rápida:

float error = target - y_pred;
float delta_out = error;
float grad_out_w1 = delta_out * h1;
float grad_out_w2 = delta_out * h2;
float grad_out_w3 = delta_out * h3;
float grad_out_b = delta_out;

float delta_h1 = delta_out * weight_out_node1 * sigmoid_derivative(h1);
float delta_h2 = delta_out * weight_out_node2 * sigmoid_derivative(h2);
float delta_h3 = delta_out * weight_out_node3 * sigmoid_derivative(h3);

float grad_w1_node1 = delta_h1 * x1;
float grad_w2_node1 = delta_h1 * x2;
float grad_b_node1 = delta_h1;

// Atualiza pesos
weight_out_node1 += learning_rate * grad_out_w1;
weight_out_node2 += learning_rate * grad_out_w2;
weight_out_node3 += learning_rate * grad_out_w3;
bias_out += learning_rate * grad_out_b;

weight1_node1 += learning_rate * grad_w1_node1;
weight2_node1 += learning_rate * grad_w2_node1;
bias_node1 += learning_rate * grad_b_node1;

Esse trecho demonstra como calculamos gradientes e os aplicamos para atualizar os pesos tanto na camada de saída quanto na camada oculta. Cada iteração usa os gradientes calculados a partir da derivada da função de ativação e do erro para empurrar os pesos na direção que reduz a perda. Ao longo de milhares de iterações, os pesos gradualmente se tornam mais precisos.

Funções de Ativação: Dando personalidade aos seus nós

As funções de ativação decidem se um neurônio deve "disparar" ou não. Elas são aplicadas ao resultado da soma ponderada e do viés em cada nó, introduzindo não linearidade na rede—sem a qual, a rede só poderia aprender relações lineares (não muito úteis para problemas complexos).

Aqui estão algumas funções de ativação comuns:

1. Sigmoid

  • Fórmula:   1 / (1 + e^(-x))
  • Faixa de saída: 0 a 1
  • Caso de uso: Ótimo para classificação binária ou camadas de saída onde você precisa de valores entre 0 e 1.
  • Prós: Saída suave e limitada.
  • Contras: Satura em extremos (perto de 0 ou 1), desacelera o aprendizado devido a gradientes que desaparecem.

2. Tanh

  • Fórmula(e^x - e^(-x)) / (e^x + e^(-x))
  • Faixa de saída: -1 a 1
  • Caso de uso: Quando você deseja uma saída centrada em zero.
  • Prós: Gradientes mais fortes que a sigmoid; aprendizado mais rápido em camadas ocultas.
  • Contras: Ainda sofre com gradientes que desaparecem nos extremos.

3. ReLU (Unidade Linear Retificada)

  • Fórmula:   max(0, x)
  • Faixa de saída: 0 a infinito.
  • Caso de uso: Padrão para a maioria das camadas ocultas em aprendizado profundo.
  • Prós: Cálculo rápido, ajuda com ativação esparsa.
  • Contras: Pode "morrer" se as entradas forem sempre negativas ("problema do ReLU moribundo").

Quando usar qual função de ativação?

AtivaçãoFaixa de SaídaBom ParaTipo de Camada ComumNotas
Sigmoid0 a 1Saída BináriaCamada de saídaPode causar gradiente que desaparece
Tanh-1 a 1Camadas ocultas centradas em zeroCamada ocultaGradientes mais fortes que sigmoid
ReLU0 a ∞A maioria das redes profundas e CNNsCamada ocultaRápido, eficiente, mas pode "morrer" em negativos

No nosso código de exemplo, usamos sigmoid para camadas ocultas. Funciona para pequenas demonstrações, mas em redes maiores, ReLU é frequentemente preferido para melhor desempenho e treinamento mais rápido.

Derivadas e Retropropagação: Como as redes aprendem com os erros

Imagine que você está vendado, em pé em uma colina, e precisa encontrar seu caminho para baixo. Você sentiria ao redor com seu pé para perceber qual direção desce, e então se moveria de acordo.

É isso que as redes neurais fazem usando derivadas—elas verificam quais pequenos ajustes tornam as previsões mais precisas.

Após fazer uma previsão, a rede calcula quão errada estava (chamamos isso de "perda"). Em seguida, usa algo chamado retropropagação para rastrear esse erro de volta pela rede, descobrindo exatamente como cada peso contribuiu para o erro.

Aqui está o loop de retropropagação simplificado:

  1. Passagem para Frente: Adivinhe a resposta.
  2. Calcule a Perda: Quão errada foi essa adivinhação?
  3. Passagem para Trás: Descubra como cada peso afetou esse erro.
  4. Atualize Pesos: Ajuste ligeiramente os pesos para reduzir erros futuros.

Esse processo se repete milhares de vezes, tornando a rede cada vez mais inteligente.

Vamos olhar uma implementação simplificada em C++ dessa ideia:

float sigmoid(float x) {
    return 1.0f / (1.0f + exp(-x));
}

float sigmoid_derivative(float y) {
    return y * (1 - y); // y = sigmoid(x)
}

Essa é nossa função de ativação e sua derivada. Elas são usadas tanto nas passagens para frente quanto para trás.

O loop de treinamento usa duas entradas (a e b), normaliza-as entre 0 e 1, e treina a rede para prever sua soma. Usamos 3 nós ocultos e 1 nó de saída. Aqui está uma parte chave do loop:

float error = target - y_pred;
total_loss += error * error;

float delta_out = error;
...
float delta_h1 = delta_out * weight_out_node1 * sigmoid_derivative(h1);

Aqui é onde calculamos o erro e usamos derivadas para empurrar ajustes de volta pela rede—retropropagação em ação.

No final do treinamento, permitimos que os usuários insiram dois números, e a rede prevê a soma. Não está memorizando—está generalizando a partir do que aprendeu.

Taxa de Aprendizado e Épocas

Como você pode ter notado no código, usamos dois hiperparâmetros importantes para controlar o processo de treinamento: taxa_de_aprendizado e épocas.

O que é uma taxa de aprendizado?

A taxa de aprendizado determina quão grande é cada passo ao atualizar os pesos. Uma taxa de aprendizado pequena (como 0.01) significa que a rede faz ajustes pequenos a cada vez, o que é mais seguro, mas mais lento. Uma taxa de aprendizado grande pode acelerar as coisas, mas se for muito grande, a rede pode ultrapassar e falhar em aprender corretamente.

No nosso código, usamos:

float learning_rate = 0.01f;

Isso nos dá um equilíbrio entre velocidade e estabilidade.

O que é uma época?

Uma época é uma passagem completa pelo conjunto de dados de treinamento inteiro. No nosso exemplo, para cada época, treinamos a rede usando cada combinação de a e b de 0 a 99. Então fazemos isso novamente para a próxima época.

Treinamos por várias épocas para permitir que a rede refine seus pesos repetidamente.

  • Se você aumentar o número de épocas, a rede terá mais chances de melhorar, potencialmente reduzindo ainda mais a perda. No entanto, muitas épocas podem levar a overfitting, onde o modelo se torna muito adaptado aos dados de treinamento e tem um desempenho ruim em novas entradas.
  • Se você diminuir o número de épocas, o treinamento será mais rápido, mas a rede pode não aprender o suficiente e pode ter um desempenho abaixo do esperado.

No código, usamos:

int epochs = 1000;

Isso é tipicamente um bom começo para problemas simples como aprender adição, mas você pode ajustar com base em quão rápido ou devagar a perda diminui.

Uma rede neural pode aprender múltiplas coisas?

Absolutamente. Redes neurais não estão limitadas a aprender apenas um tipo de tarefa. Na verdade, a mesma arquitetura de rede pode ser treinada para fazer uma variedade de coisas—como reconhecer dígitos manuscritos, traduzir idiomas ou até mesmo gerar música—dependendo dos dados que ela recebe e de como é treinada.

No nosso exemplo, a rede aprendeu a somar dois números, mas com dados de treinamento diferentes e uma camada de saída modificada, ela também poderia aprender a:

  • Subtrair números
  • Classificar imagens
  • Prever valores futuros em uma série temporal

Quanto mais tarefas você quiser que a rede gerencie—ou quanto mais complexa for a tarefa—mais neurônios e camadas você pode precisar. Um problema simples como adição pode precisar apenas de alguns nós, enquanto tarefas como reconhecimento de imagem ou tradução de linguagem podem exigir redes muito mais profundas e largas. Apenas tenha em mente: mais complexidade nem sempre é melhor—adicionar muitos nós pode levar ao overfitting, onde a rede memoriza em vez de aprender a generalizar.

Esta é a versão aprimorada da nossa rede neural onde treinamos para tanto somar quanto subtrair números de entrada usando uma rede compartilhada única:


#include <iostream>
#include <cmath>
using namespace std;

// Função de ativação sigmoid (compressa a entrada na faixa [0, 1])
float sigmoid(float x) {
    return 1.0f / (1.0f + exp(-x));
}

// Derivada da sigmoid (usada para calcular gradientes durante a retropropagação)
float sigmoid_derivative(float y) {
    return y * (1 - y); // y = sigmoid(x)
}

int main() {
    // === Pesos e Vieses da Camada Oculta (3 neurônios ocultos, 2 entradas cada) ===
    float w1_n1 = 0.5f, w2_n1 = 0.5f, b1 = 0.0f;
    float w1_n2 = -0.5f, w2_n2 = -0.5f, b2 = 0.0f;
    float w1_n3 = 0.25f, w2_n3 = -0.25f, b3 = 0.0f;

    // === Pesos e Vieses da Camada de Saída (2 neurônios de saída: soma e subtração) ===
    float out_w1_sum = 0.5f, out_w2_sum = 0.5f, out_w3_sum = 0.5f, out_b_sum = 0.0f;
    float out_w1_sub = -0.5f, out_w2_sub = -0.5f, out_w3_sub = 0.5f, out_b_sub = 0.0f;

    float learning_rate = 0.01f;
    int epochs = 1000;

    // === Loop de Treinamento ===
    for (int epoch = 0; epoch < epochs; ++epoch) {
        float total_loss = 0.0f;

        for (int a = 0; a < 100; ++a) {
            for (int b = 0; b < 100; ++b) {
                // === Normaliza os valores de entrada ===
                float x1 = a / 100.0f;
                float x2 = b / 100.0f;

                // === Normaliza os alvos ===
                float target_sum = (a + b) / 200.0f;          // A soma varia de 0 a 198 → [0, ~1]
                float target_sub = (a - b + 100.0f) / 200.0f; // A subtração varia de -99 a +99 → [0, ~1]

                // === Passagem para Frente (Entrada → Camada Oculta) ===
                float z1 = x1 * w1_n1 + x2 * w2_n1 + b1;
                float h1 = sigmoid(z1);

                float z2 = x1 * w1_n2 + x2 * w2_n2 + b2;
                float h2 = sigmoid(z2);

                float z3 = x1 * w1_n3 + x2 * w2_n3 + b3;
                float h3 = sigmoid(z3);

                // === Passagem para Frente (Camada Oculta → Camada de Saída) ===
                float y_pred_sum = h1 * out_w1_sum + h2 * out_w2_sum + h3 * out_w3_sum + out_b_sum;
                float y_pred_sub = h1 * out_w1_sub + h2 * out_w2_sub + h3 * out_w3_sub + out_b_sub;

                // === Calcula a Perda (Erro Quadrático) ===
                float error_sum = target_sum - y_pred_sum;
                float error_sub = target_sub - y_pred_sub;
                total_loss += error_sum * error_sum + error_sub * error_sub;

                // === Retropropagação - Camada de Saída (Gradientes para cada nó de saída) ===
                float delta_out_sum = error_sum;
                float grad_out_w1_sum = delta_out_sum * h1;
                float grad_out_w2_sum = delta_out_sum * h2;
                float grad_out_w3_sum = delta_out_sum * h3;
                float grad_out_b_sum = delta_out_sum;

                float delta_out_sub = error_sub;
                float grad_out_w1_sub = delta_out_sub * h1;
                float grad_out_w2_sub = delta_out_sub * h2;
                float grad_out_w3_sub = delta_out_sub * h3;
                float grad_out_b_sub = delta_out_sub;

                // === Retropropagação - Camada Oculta ===
                float delta_h1 = (delta_out_sum * out_w1_sum + delta_out_sub * out_w1_sub) * sigmoid_derivative(h1);
                float delta_h2 = (delta_out_sum * out_w2_sum + delta_out_sub * out_w2_sub) * sigmoid_derivative(h2);
                float delta_h3 = (delta_out_sum * out_w3_sum + delta_out_sub * out_w3_sub) * sigmoid_derivative(h3);

                float grad_w1_n1 = delta_h1 * x1;
                float grad_w2_n1 = delta_h1 * x2;
                float grad_b1 = delta_h1;

                float grad_w1_n2 = delta_h2 * x1;
                float grad_w2_n2 = delta_h2 * x2;
                float grad_b2 = delta_h2;

                float grad_w1_n3 = delta_h3 * x1;
                float grad_w2_n3 = delta_h3 * x2;
                float grad_b3 = delta_h3;

                // === Atualiza Pesos de Saída ===
                out_w1_sum += learning_rate * grad_out_w1_sum;
                out_w2_sum += learning_rate * grad_out_w2_sum;
                out_w3_sum += learning_rate * grad_out_w3_sum;
                out_b_sum += learning_rate * grad_out_b_sum;

                out_w1_sub += learning_rate * grad_out_w1_sub;
                out_w2_sub += learning_rate * grad_out_w2_sub;
                out_w3_sub += learning_rate * grad_out_w3_sub;
                out_b_sub += learning_rate * grad_out_b_sub;

                // === Atualiza Pesos Ocultos ===
                w1_n1 += learning_rate * grad_w1_n1;
                w2_n1 += learning_rate * grad_w2_n1;
                b1 += learning_rate * grad_b1;

                w1_n2 += learning_rate * grad_w1_n2;
                w2_n2 += learning_rate * grad_w2_n2;
                b2 += learning_rate * grad_b2;

                w1_n3 += learning_rate * grad_w1_n3;
                w2_n3 += learning_rate * grad_w2_n3;
                b3 += learning_rate * grad_b3;
            }
        }

        // === Imprime a Perda a Cada 100 Épocas ===
        if ((epoch + 1) % 100 == 0 || epoch == 0)
            cout << "[Época " << epoch + 1 << "] Perda: " << total_loss << endl;
    }

    // === Fase de Teste ===
    float a, b;
    cout << "\nTeste de previsão\nDigite a: ";
    cin >> a;
    cout << "Digite b: ";
    cin >> b;

    float x1 = a / 100.0f;
    float x2 = b / 100.0f;

    // Passagem para frente novamente para entrada do usuário
    float h1 = sigmoid(x1 * w1_n1 + x2 * w2_n1 + b1);
    float h2 = sigmoid(x1 * w1_n2 + x2 * w2_n2 + b2);
    float h3 = sigmoid(x1 * w1_n3 + x2 * w2_n3 + b3);

    float y_sum = h1 * out_w1_sum + h2 * out_w2_sum + h3 * out_w3_sum + out_b_sum;
    float y_sub = h1 * out_w1_sub + h2 * out_w2_sub + h3 * out_w3_sub + out_b_sub;

    // Desnormaliza as saídas
    float predicted_sum = y_sum * 200.0f;
    float predicted_sub = y_sub * 200.0f - 100.0f;

    cout << "Soma prevista:        " << predicted_sum << endl;
    cout << "Soma real:           " << (a + b) << endl;
    cout << "Diferença prevista: " << predicted_sub << endl;
    cout << "Diferença real:    " << (a - b) << endl;

    return 0;
}

Concluindo

Isso foi muito para absorver—mas meio incrível, certo? Não falamos apenas de teoria; vimos uma rede neural aprender a fazer algo real: somar e subtrair números. Sem regras codificadas—apenas pesos se ajustando através do treinamento. Ao longo do caminho, conhecemos taxas de aprendizado, funções de ativação e a mágica da retropropagação que une tudo isso.

Na próxima parte, daremos um passo atrás do código e mergulharemos na história por trás de tudo isso. De onde vieram as redes neurais? Por que a IA explodiu apenas nos últimos anos? O que mudou? Vamos explorar como tudo começou—e por que isso está apenas começando.