A Matemática por trás da IA: Vamos Começar Simples

Como você normalmente escreveria um programa para somar dois números abaixo de 100? Fácil, certo? Você normalmente escreveria algo simples como:

#include <iostream>
using namespace std;

int main() {
    int a, b, sum;

    cout << "Digite o primeiro número: ";
    cin >> a;

    cout << "Digite o segundo número: ";
    cin >> b;

    sum = a + b;

    cout << "A soma é: " << sum << endl;

    return 0;
}

Mas vamos seguir um caminho mais empolgante—vamos ensinar uma máquina a aprender a adição sozinha, usando uma rede neural. Redes neurais estão no coração da IA moderna, impulsionando tudo, desde reconhecimento de imagem e tradução de idiomas até sistemas avançados como o ChatGPT.

Neste artigo, vamos mostrar como uma rede neural pode "aprender" a somar dois números através de treinamento, não seguindo uma regra codificada. Você verá como uma rede neural aprende a "somar" identificando padrões. Vamos até revelar os números reais (a mágica por trás da cortina) que fazem isso acontecer.

Aqui está uma espiada na estrutura interna de uma rede neural treinada:

// Pesos e viéses da camada oculta
double weight1_node1 = 0.658136;
double weight2_node1 = 0.840666;
double bias_node1 = -0.893218;

double weight1_node2 = -0.720667;
double weight2_node2 = -0.369172;
double bias_node2 = 0.036762;

double weight1_node3 = 0.512252;
double weight2_node3 = 0.292342;
double bias_node3 = -0.0745917;

// Pesos e viés da camada de saída
double weight_out_node1 = 1.93108;
double weight_out_node2 = -0.718584;
double weight_out_node3 = 0.589741;
double bias_out = -0.467899;

À primeira vista, esses valores podem parecer aleatórios, mas eles representam um pequeno cérebro artificial capaz de aprender como somar dois números.

Use esta fórmula para calcular a saída. Não se preocupe se parecer complicado agora—apenas siga o fluxo:

Aqui, x1 e x2 são os dois números que você deseja somar (normalizados dividindo por 100):

double x1 = 0.3; // 30 / 100
double x2 = 0.5; // 50 / 100

// Camada oculta
double z1 = x1 * weight1_node1 + x2 * weight2_node1 + bias_node1;
double h1 = sigmoid(z1);

double z2 = x1 * weight1_node2 + x2 * weight2_node2 + bias_node2;
double h2 = sigmoid(z2);
double z3 = x1 * weight1_node3 + x2 * weight2_node3 + bias_node3;
double h3 = sigmoid(z3);

// Camada de saída
double sum = (h1 * weight_out_node1 + h2 * weight_out_node2 + h3 * weight_out_node3 + bias_out) * 200.0f;

Vamos analisar por que multiplicamos por 200: inicialmente escalamos nossas entradas e saídas para se encaixar entre 0 e 1 para um treinamento mais fácil. Como a soma máxima de dois números abaixo de 100 é 200, escalamos a saída de volta multiplicando por 200.

Função sigmoide:

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

Vamos testar com alguns números reais:

Vamos usar x1 = 30 e x2 = 50. Primeiro, normalizamos:

x1 = 30 / 100 = 0.3
x2 = 50 / 100 = 0.5

Agora vamos passar pela matemática:

z1 = (0.3 × 0.658136) + (0.5 × 0.840666) - 0.893218
   ≈ 0.1974408 + 0.420333 - 0.893218
   ≈ -0.2754442
h1 = sigmoid(z1) ≈ 0.431844

z2 = (0.3 × -0.720667) + (0.5 × -0.369172) + 0.036762
   ≈ -0.2162001 - 0.184586 + 0.036762
   ≈ -0.3640241
h2 = sigmoid(z2) ≈ 0.409872

z3 = (0.3 × 0.512252) + (0.5 × 0.292342) - 0.0745917
   ≈ 0.1536756 + 0.146171 - 0.0745917
   ≈ 0.2252549
h3 = sigmoid(z3) ≈ 0.556078

// Camada de saída
output = (h1 × 1.93108) + (h2 × -0.718584) + (h3 × 0.589741) - 0.467899
       ≈ (0.834389) + (-0.294320) + (0.328013) - 0.467899
       ≈ 0.400183

Soma prevista = 0.400183 × 200 ≈ 80.0366
// Por que multiplicar por 200? Porque durante o treinamento, normalizamos todas as entradas e saídas para ficarem entre 0 e 1 para melhor estabilidade de aprendizado. Como a soma máxima de duas entradas é 100 + 100 = 200, escalamos a saída da rede de volta multiplicando por 200.

Boom! O resultado é aproximadamente 80, que é a soma correta de 30 e 50.

Se você não acredita que essa pequena rede realmente pode somar, vamos testá-la novamente com números diferentes: 20 e 30.

Primeiro, normalizamos as entradas:

x1 = 20 / 100 = 0.2
x2 = 30 / 100 = 0.3

Então, as cálculos:

z1 = (0.2 × 0.658136) + (0.3 × 0.840666) - 0.893218
   ≈ 0.1316272 + 0.2521998 - 0.893218
   ≈ -0.509391
h1 = sigmoid(z1) ≈ 0.375241

z2 = (0.2 × -0.720667) + (0.3 × -0.369172) + 0.036762
   ≈ -0.1441334 - 0.1107516 + 0.036762
   ≈ -0.218123
h2 = sigmoid(z2) ≈ 0.445714

z3 = (0.2 × 0.512252) + (0.3 × 0.292342) - 0.0745917
   ≈ 0.1024504 + 0.0877026 - 0.0745917
   ≈ 0.1155613
h3 = sigmoid(z3) ≈ 0.528860

// Camada de saída
output = (h1 × 1.93108) + (h2 × -0.718584) + (h3 × 0.589741) - 0.467899
       ≈ (0.724688) + (-0.320095) + (0.311644) - 0.467899
       ≈ 0.248338

Soma prevista = 0.248338 × 200 ≈ 49.6676

Ele previu cerca de 49.67. Isso está super próximo da soma real: 50!

Bem legal, né?

Então... O que é uma Rede Neural, afinal?

Introdução rápida: Uma rede neural é como uma versão matemática do seu cérebro. Ela consiste em unidades chamadas neurônios que processam entradas multiplicando-as por pesos, adicionando um viés e, em seguida, aplicando uma função de ativação. Uma função de ativação comum é a sigmoide:

sigmoid(x) = 1 / (1 + e^(-x))

Isso comprime qualquer entrada em um valor entre 0 e 1, permitindo que a rede capture padrões complexos.

Este é o gráfico da função sigmoide, mostrando claramente como as entradas são suavemente transformadas em valores entre 0 e 1:

Existem outras funções de ativação também, como ReLU (Rectified Linear Unit), tanh e softmax, cada uma com seus próprios casos de uso e comportamentos. Mas a sigmoide é um ótimo lugar para começar porque é simples e suave—perfeita para redes neurais pequenas como a nossa.

A Configuração: Ensinando Nossa Rede a Somar

Para este experimento, criamos uma pequena rede com:

  • 2 nós de entrada (para os dois números a serem somados)
  • 3 nós ocultos (fazendo o trabalho pesado com a mágica da sigmoide)
  • 1 nó de saída (nos dando a soma)

Aqui está um diagrama mostrando a estrutura da rede neural que estamos usando para somar dois números. Você pode ver como cada nó de entrada se conecta a todos os nós da camada oculta, como os pesos e viéses são aplicados, e como tudo flui em direção à saída final:

E sim, escalamos tudo. Como nossos números estão abaixo de 100, dividimos as entradas por 100 e escalamos a saída de volta por 200 no final.

Este é o código que escrevemos para somar dois números usando a rede neural:

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

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

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

int main() {
    float weight1_node1 = 0.5f, weight2_node1 = 0.5f, bias_node1 = 0.0f;
    float weight1_node2 = -0.5f, weight2_node2 = -0.5f, bias_node2 = 0.0f;
    float weight1_node3 = 0.25f, weight2_node3 = -0.25f, bias_node3 = 0.0f;

    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;

    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; // Normalizar alvo para a faixa de 0-1

                // Passagem para frente para a camada oculta
                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);

                // Camada de saída com ativação linear
                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;

                // Passagem para trás (camada de saída - linear)
                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;

                // Passagem para trás (camada oculta)
                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;

                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;

                // Atualizar 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;

                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;
            }
        }

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

    // Testar o modelo
    float a, b;
    cout << "\nTeste de previsão de soma (a + b)\nDigite a: ";
    cin >> a;
    cout << "Digite b: ";
    cin >> b;

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

    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;

    cout << "w1_node1: " << weight1_node1 << ", w2_node1: " << weight2_node1
                             << ", w1_node2: " << weight1_node2 << ", w2_node2: " << weight2_node2
                             << ", w1_node3: " << weight1_node3 << ", w2_node3: " << weight2_node3
                             << ", out_w1: " << weight_out_node1 << ", out_w2: " << weight_out_node2
                             << ", out_w3: " << weight_out_node3 << ", bias_out: " << bias_out
                             << ", bias_node1: " << bias_node1 << ", bias_node2: " << bias_node2 << ", bias_node3: " << bias_node3 << endl;

    cout << "Soma prevista: " << predicted_sum << "\nSoma real: " << (a + b) << endl;

    return 0;
}

Então... Como Ele Aprendeu?

Aqui está a parte legal: não codificamos as regras para a adição. Apenas fornecemos à rede um monte de pares de números e suas somas, e ela ajustou lentamente aqueles pesos e viéses até que as previsões melhorassem cada vez mais.

É meio que como tentativa e erro, mas mais inteligente—com uma pitada de cálculo.

Ele Realmente Aprendeu a Somar?

Eh, não exatamente como nós. Ele não entende o que “mais” significa. Mas ele aprende uma aproximação muito boa. Ele vê padrões nos números e prevê somas que muitas vezes estão corretas.

É mais como aprender sentindo padrões em vez de conhecer regras. Se você estiver curioso, pode mudar os números de entrada e testá-lo. Quanto mais você brincar, mais entenderá como esses pequenos cérebros funcionam.

Concluindo

Ensinar uma rede neural a somar dois números pode parecer bobo à primeira vista, mas é uma ótima maneira de espiar a mente da IA. Por trás de todo o hype, são apenas pesos, viéses e um pouco de matemática inteligente.

Na próxima parte, vamos nos aprofundar em como as redes neurais funcionam: vamos explorar camadas, funções de ativação, derivadas e o que realmente acontece durante o treinamento.