No artigo anterior, vimos como um perceptron combina entradas, pesos e um viés para produzir decisões simples.
Implementando um Perceptron em Python do zero (sem bibliotecas)
Agora, vamos transformar aquele conceito em código.
Neste tutorial, construiremos um perceptron em Python sem NumPy, Pandas, scikit-learn ou qualquer outra biblioteca. Usaremos apenas listas, variáveis, operações matemáticas, for, if e else.
O exemplo será executado em um notebook dividido em poucas células. Para o treinamento, criaremos dados sintéticos de vazão e ensinaremos o modelo a distinguir uma situação normal de uma situação de alerta.
Aviso importante: todos os dados deste artigo são sintéticos. Eles foram inventados exclusivamente para demonstrar o funcionamento do algoritmo. Não representam medições reais, limites operacionais ou recomendações para sistemas hidráulicos.
O problema#
Imagine um sistema que acompanha a vazão de água durante determinado intervalo.
Para cada intervalo, temos duas informações:
- vazão média: o valor médio observado durante o período;
- vazão máxima: o maior valor observado durante o mesmo período.
Os valores serão representados em litros por segundo, ou L/s.
Cada observação também receberá uma classe:
0: funcionamento normal;1: situação de alerta.
Nosso perceptron deverá observar exemplos já classificados e ajustar seus pesos para separar as duas classes.
Os dados deste exemplo foram construídos propositalmente para serem linearmente separáveis. Isso significa que é possível dividir as observações normais e de alerta usando uma única fronteira de decisão.
Quando essa condição existe, o algoritmo do perceptron consegue encontrar uma separação depois de um número finito de correções.
Quando as classes se misturam ou formam padrões mais complexos, o perceptron simples pode nunca chegar a zero erros. Por isso, também definiremos um número máximo de épocas.
Célula 1: criando os dados sintéticos#
Na primeira célula de código, crie a lista de treinamento:
dados = [
[8, 12, 0],
[10, 14, 0],
[12, 16, 0],
[14, 18, 0],
[18, 24, 1],
[20, 27, 1],
[24, 31, 1],
[28, 36, 1],
]Cada linha possui esta estrutura:
[vazão média, vazão máxima, classe]Por exemplo:
[8, 12, 0]Essa linha representa um intervalo fictício com:
- vazão média de 8 L/s;
- vazão máxima de 12 L/s;
- classificação normal.
Já esta observação:
[24, 31, 1]representa:
- vazão média de 24 L/s;
- vazão máxima de 31 L/s;
- classificação de alerta.
Os exemplos foram criados para formar dois grupos simples. Valores menores pertencem à classe normal, enquanto valores maiores pertencem à classe de alerta.
Em um projeto real, as classes precisariam ser definidas com medições confiáveis, critérios técnicos, conhecimento do sistema e validação de especialistas.
Célula 2: criando os parâmetros#
Agora, vamos criar os valores que serão ajustados durante o treinamento:
pesos = [0.0, 0.0]
vies = 0.0
taxa_aprendizado = 0.1
maximo_epocas = 20A lista pesos possui dois valores porque nosso modelo recebe duas entradas:
pesos[0] → peso da vazão média
pesos[1] → peso da vazão máximaOs pesos e o viés começam em zero.
A taxa_aprendizado controla o tamanho de cada correção realizada pelo algoritmo.
Uma época corresponde a uma passagem completa por todos os exemplos do conjunto de treinamento.
Permitiremos no máximo 20 épocas para evitar que o código continue executando indefinidamente caso os dados não possam ser separados.
Célula 3: treinando o perceptron#
Adicione a célula responsável pelo treinamento:
for epoca in range(maximo_epocas):
erros = 0
for linha in dados:
entradas = [
linha[0] / 10,
linha[1] / 10,
]
esperado = linha[2]
soma = (
entradas[0] * pesos[0]
+ entradas[1] * pesos[1]
+ vies
)
if soma >= 0:
previsto = 1
else:
previsto = 0
erro = esperado - previsto
if erro != 0:
pesos[0] = pesos[0] + (
taxa_aprendizado * erro * entradas[0]
)
pesos[1] = pesos[1] + (
taxa_aprendizado * erro * entradas[1]
)
vies = vies + taxa_aprendizado * erro
erros = erros + 1
print("Época", epoca + 1, "- erros:", erros)
if erros == 0:
breakEsse é o treinamento completo do perceptron.
Nenhuma biblioteca foi importada.
Entendendo o treinamento#
O primeiro for controla as épocas:
for epoca in range(maximo_epocas):Dentro dele, o segundo for percorre os exemplos:
for linha in dados:As duas entradas são retiradas de cada linha e divididas por 10:
entradas = [
linha[0] / 10,
linha[1] / 10,
]Essa divisão é um reescalonamento simples dos dados.
Uma vazão de 8 L/s, por exemplo, passa a ser representada internamente como 0.8. A observação continua ocupando a mesma posição em relação às demais, mas o algoritmo passa a trabalhar com valores menores.
Esse reescalonamento não é apenas visual. O valor das entradas participa diretamente das correções feitas nos pesos.
Por isso, a escala influencia:
- o tamanho de cada atualização;
- a trajetória seguida pelos pesos;
- a velocidade do treinamento;
- a quantidade de épocas necessária para a convergência.
Dividir as duas características pelo mesmo valor preserva a organização dos grupos, mas faz parte da configuração deste exemplo.
Para reproduzir os resultados do artigo, a divisão por 10 deve ser mantida.
Calculando a pontuação#
Em seguida, o perceptron calcula uma pontuação:
pontuação =
vazão média × peso 1
+ vazão máxima × peso 2
+ viésNo código:
soma = (
entradas[0] * pesos[0]
+ entradas[1] * pesos[1]
+ vies
)O if transforma essa pontuação em uma classe:
if soma >= 0:
previsto = 1
else:
previsto = 0Quando a soma é igual ou maior que zero, o modelo retorna 1, indicando alerta.
Quando a soma é menor que zero, retorna 0, indicando funcionamento normal.
Calculando o erro#
Depois, a previsão é comparada com a resposta esperada:
erro = esperado - previstoHá três possibilidades:
erro igual a 0 → o modelo acertou
erro igual a 1 → deveria prever 1, mas previu 0
erro igual a -1 → deveria prever 0, mas previu 1Quando o erro é diferente de zero, os pesos e o viés são corrigidos:
if erro != 0:
pesos[0] = pesos[0] + (
taxa_aprendizado * erro * entradas[0]
)
pesos[1] = pesos[1] + (
taxa_aprendizado * erro * entradas[1]
)
vies = vies + taxa_aprendizado * erroA primeira correção passo a passo#
Podemos observar o que acontece logo no primeiro exemplo do treinamento.
Os pesos e o viés começam em zero:
pesos = [0.0, 0.0]
viés = 0.0A primeira observação é:
[8, 12, 0]Depois da divisão por 10:
entradas = [0.8, 1.2]Como todos os parâmetros começam em zero, a primeira pontuação será:
0.8 × 0
+ 1.2 × 0
+ 0
= 0Como nosso código considera valores iguais ou maiores que zero como classe 1, a previsão será:
previsto = 1Entretanto, a classe correta dessa observação é 0.
Portanto:
erro = esperado - previsto
erro = 0 - 1
erro = -1Agora podemos aplicar a regra de atualização.
Para o primeiro peso:
novo peso =
peso atual
+ taxa de aprendizado × erro × entrada
novo peso =
0 + 0.1 × (-1) × 0.8
novo peso = -0.08Para o segundo:
novo peso =
0 + 0.1 × (-1) × 1.2
novo peso = -0.12E para o viés:
novo viés =
0 + 0.1 × (-1)
novo viés = -0.1Depois da primeira correção, os parâmetros passam a ser:
pesos = [-0.08, -0.12]
viés = -0.1O restante do treinamento repete esse mesmo processo para os demais exemplos.
Sempre que o perceptron erra, seus parâmetros são ajustados.
Interrompendo o treinamento#
O treinamento termina quando uma época inteira é concluída sem erros:
if erros == 0:
breakComo os dados foram criados para serem linearmente separáveis, o perceptron consegue encontrar pesos que classificam todos os exemplos corretamente.
Resultado do treinamento#
Ao executar a célula desde o início, o resultado será:
Época 1 - erros: 2
Época 2 - erros: 2
Época 3 - erros: 3
Época 4 - erros: 3
Época 5 - erros: 3
Época 6 - erros: 2
Época 7 - erros: 3
Época 8 - erros: 2
Época 9 - erros: 3
Época 10 - erros: 2
Época 11 - erros: 3
Época 12 - erros: 2
Época 13 - erros: 3
Época 14 - erros: 1
Época 15 - erros: 0O número de erros não precisa diminuir em todas as épocas.
Ao corrigir um exemplo, o perceptron modifica sua fronteira de decisão. Essa alteração pode fazer com que outro exemplo, anteriormente correto, passe a ser classificado de maneira errada.
A ordem dos dados também influencia esse caminho.
Em nossa lista, todos os exemplos normais aparecem primeiro e todos os alertas aparecem depois.
O modelo faz correções em uma direção ao percorrer o primeiro grupo e pode precisar corrigir novamente seus pesos quando chega ao segundo.
Isso explica por que um conjunto aparentemente simples ainda pode exigir várias épocas.
Reprodutibilidade: o código não utiliza números aleatórios. Ao executar as células na mesma ordem, com os mesmos dados, pesos iniciais, taxa de aprendizado e divisão por 10, a saída deve ser a mesma. Alterar qualquer um desses elementos pode mudar a sequência de erros, a quantidade de épocas e os pesos finais. O perceptron também pode encontrar diferentes fronteiras válidas para um mesmo conjunto linearmente separável.
Célula 4: consultando os pesos#
Agora podemos visualizar os valores aprendidos:
print("Peso da vazão média:", round(pesos[0], 2))
print("Peso da vazão máxima:", round(pesos[1], 2))
print("Viés:", round(vies, 2))O resultado será:
Peso da vazão média: 0.26
Peso da vazão máxima: 0.2
Viés: -0.8Internamente, o Python armazena valores aproximados:
Pesos: [0.26000000000000023, 0.19999999999999987]
Viés: -0.7999999999999999Isso acontece porque números decimais nem sempre podem ser representados de forma exata pelo sistema binário utilizado pelo computador.
Por isso, usamos round apenas na exibição.
Os dois pesos ficaram positivos. Dentro deste conjunto sintético, isso significa que o aumento da vazão média ou da vazão máxima empurra a decisão em direção à classe de alerta.
O viés negativo exige que a combinação das duas entradas alcance determinado valor antes que a saída se torne positiva.
Esses números não são limites universais. Eles dependem exclusivamente dos dados artificiais, da escala, da ordem dos exemplos e dos parâmetros usados neste notebook.
Célula 5: classificando novos exemplos#
Agora apresentaremos três observações que não estavam na lista de treinamento:
testes = [
[9, 13],
[16, 21],
[23, 30],
]
for teste in testes:
entradas = [
teste[0] / 10,
teste[1] / 10,
]
soma = (
entradas[0] * pesos[0]
+ entradas[1] * pesos[1]
+ vies
)
if soma >= 0:
resultado = "alerta"
else:
resultado = "normal"
print(
"Vazão média:", teste[0], "L/s |",
"Vazão máxima:", teste[1], "L/s |",
resultado
)O resultado será:
Vazão média: 9 L/s | Vazão máxima: 13 L/s | normal
Vazão média: 16 L/s | Vazão máxima: 21 L/s | alerta
Vazão média: 23 L/s | Vazão máxima: 30 L/s | alertaO modelo aplicou os pesos aprendidos aos novos exemplos e produziu uma classificação para cada um.
Isso não significa que essas classificações sejam tecnicamente corretas para uma rede hidráulica real.
O código apenas encontrou uma regra matemática compatível com os dados sintéticos que fornecemos.
Testando uma observação passo a passo#
Podemos observar o cálculo realizado para o segundo teste:
teste = [16, 21]
entrada_1 = teste[0] / 10
entrada_2 = teste[1] / 10
soma = (
entrada_1 * pesos[0]
+ entrada_2 * pesos[1]
+ vies
)
print("Pontuação:", soma)
if soma >= 0:
print("Classificação: alerta")
else:
print("Classificação: normal")A saída será aproximadamente:
Pontuação: 0.036
Classificação: alertaO valor ficou pouco acima de zero. Por isso, a observação foi colocada na classe de alerta.
Isso também mostra que o perceptron não informa uma probabilidade.
Uma pontuação de 0.036 não significa 3,6% de chance de alerta. Ela apenas indica que o resultado ficou do lado positivo da fronteira de decisão.
A magnitude da pontuação também não deve ser interpretada automaticamente como uma medida de confiança do modelo.
O que construímos#
Nosso perceptron executa cinco operações:
- recebe a vazão média e a vazão máxima;
- multiplica cada entrada por um peso;
- adiciona o viés;
- usa
ifeelsepara escolher uma classe; - ajusta os pesos quando a previsão está errada.
Todo o processo foi implementado com recursos básicos do Python.
Não utilizamos:
- NumPy;
- Pandas;
- scikit-learn;
- funções prontas de aprendizado de máquina;
- classes ou arquiteturas complexas.
Essa implementação ajuda a enxergar o que normalmente fica escondido dentro de uma biblioteca.
Limitações do exemplo#
Este notebook foi construído para ensinar o algoritmo, não para criar um sistema real de monitoramento.
O conjunto possui poucas observações, classes fáceis de separar e nenhum ruído. Também utilizamos os mesmos dados para treinar e observar o desempenho do modelo.
Em uma aplicação real, seria necessário considerar fatores como:
- qualidade e calibração dos sensores;
- unidade e frequência das medições;
- variações normais do sistema;
- erros e valores ausentes;
- definição técnica das situações de alerta;
- separação entre dados de treinamento e avaliação;
- desempenho com observações nunca vistas;
- consequências de falsos alertas;
- consequências de falhas de detecção.
Também seria possível encontrar situações nas quais uma vazão média alta não representa alerta ou uma vazão menor exige atenção por causa de outras condições.
Um perceptron com apenas duas entradas não consegue representar toda essa complexidade.
Os números deste tutorial não devem ser utilizados como limites operacionais.
Exercícios#
Alterar a taxa de aprendizado#
Substitua:
taxa_aprendizado = 0.1por:
taxa_aprendizado = 0.05Execute novamente todas as células e observe:
- quantas épocas são necessárias;
- como muda a sequência de erros;
- quais pesos são encontrados ao final.
Depois, experimente:
taxa_aprendizado = 0.2Compare os resultados.
Alterar a ordem dos dados#
Inverta a ordem das observações no conjunto de treinamento ou intercale exemplos normais e de alerta.
Execute novamente o treinamento e observe a sequência de erros.
Mesmo quando a classificação final é correta, o caminho seguido pelo algoritmo pode mudar.
Criar novos testes#
Adicione outras observações à lista:
testes = [
[9, 13],
[16, 21],
[23, 30],
[15, 20],
[19, 25],
]Observe de qual lado da fronteira de decisão cada ponto fica.
Remover o reescalonamento#
No treinamento e nos testes, substitua:
linha[0] / 10
linha[1] / 10pelos valores originais:
linha[0]
linha[1]Execute novamente o treinamento.
Compare:
- o número de épocas;
- a sequência de erros;
- os pesos finais.
Esse exercício mostra como a escala das entradas influencia as atualizações realizadas pelo algoritmo.
Criar um conjunto que não seja linearmente separável#
Depois de executar os exemplos anteriores, experimente criar dados nos quais observações das duas classes estejam misturadas.
Mantenha:
maximo_epocas = 20e observe se o treinamento consegue chegar a uma época com zero erros.
Esse experimento ajuda a visualizar uma das principais limitações do perceptron simples.
O que aprendemos#
Neste artigo, implementamos um perceptron em Python sem usar nenhuma biblioteca.
Criamos uma lista com dados sintéticos de vazão, inicializamos os pesos, percorremos os exemplos com for e usamos if e else para produzir classificações.
Quando o modelo errou, seus pesos e seu viés foram corrigidos.
Também acompanhamos numericamente a primeira atualização realizada pelo algoritmo, mostrando como a taxa de aprendizado, o erro e o valor de cada entrada participam diretamente da correção.
Como os dados foram construídos para serem linearmente separáveis, o perceptron conseguiu classificar corretamente todos os exemplos depois de 15 épocas.
Também vimos que a escala, a ordem dos dados, a taxa de aprendizado e os valores iniciais influenciam o caminho seguido durante o treinamento.
O exemplo é pequeno, mas revela o mecanismo central do perceptron:
receber entradas, calcular uma pontuação, comparar o resultado com um limite e ajustar os pesos quando a resposta está errada.
Agora que construímos uma unidade de decisão do zero, o próximo passo é entender como várias unidades podem ser combinadas para formar uma rede neural.