⚡
VITORNMS
>Início>Projetos>Bancada>Notas>Blog>Contato
GitHubLinkedIn
status: construindo
>Início>Projetos>Bancada>Notas>Blog>Contato
status: construindo
VITORNMS logoVITORNMS

Transformando ideias em soluções inovadoras de software & hardware. Construindo o futuro, um commit de cada vez.

Navegação

>Início>Projetos>Bancada>Blog>Contato

Contato

vitornms@gmail.com+55 31 98415-2360Belo Horizonte, MG, Brasil
enviar um sinal→
Language
Forjado com& código

© 2026 VITORNMS — Todos os experimentos reservados

back to blog
aifeatured

Construindo uma Rede Neural do Zero em Python

Porque deitei fora o PyTorch e o TensorFlow. A jornada masoquista de calcular derivadas à mão — e o pesadelo inesperado do CORS no browser.

VN

Vitor Neuenschwander

CS Student & Developer

Nov 20, 202418 min
#python#neural-network#machine-learning#numpy

Por que Jogar Fora o PyTorch?

Qualquer um pode fazer model.fit() no TensorFlow. Mas entender o que acontece dentro? Isso exige sujar as mãos. Este projeto foi minha resposta à pergunta: "Eu realmente entendo redes neurais, ou só sei usar a API?"

Inspirado pelo livro "Make Your Own Neural Network" do Tariq Rashid (2016, ISBN: 978-1530826605), decidi implementar um MLP completo usando apenas NumPy. Sem abstrações. Sem autograd. Cada multiplicação de matrizes, cada derivada parcial, cada atualização de peso — tudo manual, tudo exposto.

O resultado: 2.310 KB de repositório, 8 commits, e um reconhecedor de dígitos que funciona no browser sem servidor.


A Arquitetura: 784 → 200 → 10

┌──────────────┐     ┌──────────────┐     ┌──────────────┐
│  ENTRADA     │     │   OCULTA     │     │    SAÍDA     │
│  784 nós     │────▶│  200 nós     │────▶│   10 nós     │
│  (pixels)    │     │  (sigmoid)   │     │  (classes)   │
│  28×28 = 784 │     │  N(0, 1/√n)  │     │  0-9 digits  │
└──────────────┘     └──────────────┘     └──────────────┘
      Input             Hidden              Output

Hiperparâmetros:
• Learning rate: 0.1
• Epochs (Python): 7
• Epochs (Browser): 5
• Inicialização: Distribuição Normal — N(0, 1/√n)

Cada pixel de um dígito 28×28 do MNIST vira um input normalizado entre 0.01 e 0.99. A rede olha para 784 valores e decide: "isso é um 7".

import numpy as np

class NeuralNetwork:
    def __init__(self, input_nodes, hidden_nodes, output_nodes, lr):
        self.lr = lr
        # Inicialização de pesos: N(0, 1/√n)
        self.wih = np.random.normal(
            0.0, pow(input_nodes, -0.5),
            (hidden_nodes, input_nodes)
        )
        self.who = np.random.normal(
            0.0, pow(hidden_nodes, -0.5),
            (output_nodes, hidden_nodes)
        )

    def sigmoid(self, x):
        return 1 / (1 + np.exp(-np.clip(x, -500, 500)))

O Dataset: MNIST em Miniatura

Não usei o MNIST completo (60.000 amostras). Trabalhei com um subconjunto minimalista:

  • Treino: 1.000 amostras (train-1000.csv)
  • Teste: 10 amostras (test-10.csv)
  • Auxiliar: 100 amostras (train-100.csv)

Com data augmentation (rotação ±10° via scipy), o dataset de treino cresce de 1.000 para 3.000 amostras. É pouco comparado ao MNIST real, mas mais que suficiente para provar que a matemática funciona.


Backpropagation: A Matemática à Mão

O coração do aprendizado. Cada iteração calcula o erro na saída, propaga de volta pelas camadas, e ajusta os pesos proporcionalmente à sua contribuição para o erro:

Fórmulas de atualização:
  • ΔWho = lr × (hidden_outputsᵀ · (output_errors × σ(O) × (1 − σ(O))))
  • ΔWih = lr × (inputsᵀ · (hidden_errors × σ(H) × (1 − σ(H))))
def train(self, inputs_list, targets_list):
    inputs = np.array(inputs_list, ndmin=2).T
    targets = np.array(targets_list, ndmin=2).T

    # Forward pass
    hidden_inputs = np.dot(self.wih, inputs)
    hidden_outputs = self.sigmoid(hidden_inputs)
    final_inputs = np.dot(self.who, hidden_outputs)
    final_outputs = self.sigmoid(final_inputs)

    # Cálculo de erros
    output_errors = targets - final_outputs
    hidden_errors = np.dot(self.who.T, output_errors)

    # Backpropagation — atualização de pesos
    self.who += self.lr * np.dot(
        (output_errors * final_outputs * (1 - final_outputs)),
        hidden_outputs.T
    )
    self.wih += self.lr * np.dot(
        (hidden_errors * hidden_outputs * (1 - hidden_outputs)),
        inputs.T
    )

Nota: A orientação das matrizes difere da formulação clássica de Tariq Rashid, mas a lógica é matematicamente equivalente.


Data Augmentation: Falsa vs. Verdadeira

No início, cometi um bug clássico: repetia os mesmos dados 3 vezes dentro do loop de epochs achando que era augmentation. A rede simplesmente memorizava os mesmos padrões com mais ênfase — sem ganho de generalização nenhum.

A correção usou scipy.ndimage para aplicar rotações reais de ±10° por amostra, criando variações genuínas dos dígitos:

from scipy.ndimage import rotate

def augment(image_array, angle=10):
    img = image_array.reshape(28, 28)
    rotated_plus = rotate(img, angle, reshape=False, order=1).flatten()
    rotated_minus = rotate(img, -angle, reshape=False, order=1).flatten()
    return [image_array, rotated_plus, rotated_minus]

# 1.000 originais × 3 = 3.000 amostras de treino reais
augmented_data = []
for sample in training_data:
    augmented_data.extend(augment(sample))

O Pesadelo do CORS e o Ficheiro de 3.4 MB

O obstáculo mais irritante não foi a álgebra linear — foi o browser. Queria que a interface web fosse 100% client-side: abrir o ficheiro HTML com duplo clique (file:///) e funcionar. Sem servidor, sem npm, sem nada.

O problema? Quando abres um HTML via file:///, o browser bloqueia qualquer carregamento de JSON por políticas de CORS. O weights.json de 3.4 MB simplesmente não carregava.

A solução foi o script export_weights.py: converte toda a matriz pré-treinada para um ficheiro weights.js que se autocarrega como variável global:

# export_weights.py — Treina e exporta
import json, numpy as np

# Treina a rede...
nn = NeuralNetwork(784, 200, 10, 0.1)
for epoch in range(7):
    for record in augmented_data:
        nn.train(record[1:], targets)

# Exporta como JS (não JSON!)
weights = {
    'wih': nn.wih.tolist(),
    'who': nn.who.tolist()
}

with open('weights.js', 'w') as f:
    f.write('const TRAINED_WEIGHTS = ')
    f.write(json.dumps(weights))
    f.write(';')
# Resultado: weights.js (3.4 MB)
# Carrega via <script src="weights.js"> no HTML

Duplo clique no index.html e funciona. Plug and play.


A Arte de Centrar um Número: Normalização MNIST-Style

A rede treinava bem nos dados do MNIST. Mas quando desenhava no canvas do browser, tudo virava "2" ou "8". Accuracy catastrófica.

Porquê? O MNIST original tem os dígitos centrados pelo centro de massa. O desenho do rato no canvas começa onde o utilizador clica — completamente desalinhado.

A correção envolveu implementar, em JavaScript puro, o mesmo pré-processamento do MNIST:

  1. Bounding Box: encontrar a área mínima que contém o desenho
  2. Resize: interpolação bilinear para 20×20 pixels
  3. Centralizar: colar no centro exato de um frame 28×28
  4. Normalizar: valores entre [0.01, 0.99]
function preProcessImage(imageData) {
  // 1. Encontrar bounding box do desenho
  const bbox = findBoundingBox(imageData);

  // 2. Extrair e redimensionar para 20×20
  const cropped = cropCanvas(imageData, bbox);
  const resized = bilinearInterpolation(cropped, 20, 20);

  // 3. Centralizar em frame 28×28 (margem de 4px)
  const centered = new Float32Array(784).fill(0);
  const offsetX = 4;  // (28 - 20) / 2
  const offsetY = 4;
  for (let y = 0; y < 20; y++) {
    for (let x = 0; x < 20; x++) {
      centered[(y + offsetY) * 28 + (x + offsetX)] =
        resized[y * 20 + x];
    }
  }

  // 4. Normalizar para [0.01, 0.99]
  return centered.map(v => v * 0.98 + 0.01);
}

Depois desta normalização, a accuracy no browser subiu dramaticamente. O canvas agora mostra um Live Preview 28×28 em tempo real enquanto desenhas.


A Interface Web: Canvas + Classificação em Tempo Real

O index.html (73% do repositório em bytes!) oferece uma interface completa sem dependências:

  • Canvas para desenho livre com ajuste de tamanho do pincel via slider
  • Botão Undo: desfaz o último traço
  • Botão Classify: classifica o dígito desenhado
  • Botão Re-train: re-treina a rede no browser (5 epochs)
  • Live Preview: mostra a versão 28×28 normalizada em tempo real

Tudo em Vanilla JS. Sem React, sem npm, sem build step.


Bugs Corrigidos (O Diário de Debugging)

BugEfeitoCorreção
readlines() dentro do loop de epochsLista ficava vazia após 1ª epoch — treinava só 1×Mover chamada para antes do loop
Learning rate = 0.01Convergência lentíssimaAumentar para 0.1
Augmentação "falsa" no browserMesmos dados repetidos 3× sem variaçãoRotações reais ±10° via canvas transform
Pesos aleatórios na interfaceRede sem treino → sempre previa o mesmo dígitoCarregar weights.js automaticamente

Estrutura do Projeto

Neural-Network/
├── main.py              # Treinamento interativo no terminal
├── export_weights.py    # Treina + exporta para weights.js
├── verify.py            # Verificação dos pesos (Python)
├── verify.js            # Verificação dos pesos (Node.js)
├── index.html           # Interface web standalone
├── weights.json         # Pesos pré-treinados (JSON, 3.4 MB)
├── weights.js           # Pesos pré-treinados (JS, 3.4 MB)
├── draw_table.pde       # Sketch Processing para coleta de dados
├── train-1000.csv       # Dataset treino (1.000 amostras)
├── train-100.csv        # Dataset auxiliar (100 amostras)
├── test-10.csv          # Dataset teste (10 amostras)
└── README.md            # Documentação completa

O que Levei Deste Projeto

Implementar uma rede neural do zero é como aprender a dirigir com câmbio manual antes de comprar um automático. Doloroso, frustrante, mas depois de fazer, nenhuma abstração do PyTorch te confunde mais.

As verdadeiras lições não foram sobre IA:

  1. CORS é o boss final da web — Mais horas perdidas com políticas de segurança do browser do que com álgebra linear
  2. Pré-processamento > Arquitetura — A normalização do canvas importou mais que qualquer hiperparâmetro
  3. Data augmentation errada é pior que nenhuma — Repetir dados ≠ augmentação
Contents
share
share:
[RELATED_POSTS]

Continue Reading

algorithms

Algoritmos de Grafos para Programação Competitiva

A pressão do relógio na OBI. 16 implementações, 10 problemas, 3 edições, e a lição dolorosa de que O(N³) te custa uma medalha.

Oct 5, 2024•20 min