fbpx

Redes Neurais Artificiais: criando um Perceptron de uma camada

Uma rede neural artificial opera de forma muito parecida com nossa rede neural biológica. Mas você sabe como funciona na prática?

Já tive dificuldade em encontrar exemplos que implementassem o funcionamento de um neurônio artificial de um jeito simples de entender, por isso, acredito que este artigo irá ajudar quem já passou pela mesma situação.

Neste artigo, meu objetivo é apresentar a implementação de um perceptron, o tipo mais simples de rede neural feedforward, utilizando C#.

Como funciona nosso Sistema Nervoso?

A unidade fundamental do sistema nervoso é a célula nervosa, o neurônio, que se distingue das outras células por apresentar excitabilidade, que lhe permite responder a estímulos externos e internos. Isso possibilita a transmissão de impulsos nervosos a outros neurônios e células musculares e glandulares.

Os dendritos são prolongamentos dos neurônios e têm a função de receber estímulos nervosos provenientes de outros neurônios. Esses estímulos são transmitidos para o corpo celular.
O corpo celular coleta as informações recebidas dos dendritos, as combina e processa. De acordo com a intensidade e frequência dos estímulos recebidos, o corpo celular gera um novo impulso, que é enviado para o axônio.

O axônio é um prolongamento dos neurônios, responsável pela condução dos impulsos elétricos produzidos no corpo celular até outro local mais distante, geralmente outros neurônios.
O contato entre a terminação de um axônio e o dendrito de outro neurônio é denominado sinapse.

De acordo com Haykin (1999), as sinapses são unidade que medeiam as interações entre os neurônios, e podem ser excitatórias ou inibitórias.

Componentes

Antes de entrar em detalhes sobre o funcionamento de RNA, vamos entender primeiro o que é um perceptron. O perceptron é um tipo de rede neural artificial. Ela pode ser vista como o tipo mais simples de rede neural feedforward, conhecido também como classificador linear.

Uma rede neural recebe um valor de entrada, processa as entradas e retorna uma resposta.
O neurônio é ativado somente se o valor for maior que um limiar. Quando o peso é positivo, ocorre uma sinapse excitadora, quando negativo, a sinapse é inibidora. Sinapses são pesos que amplificam ou reduzem o sinal de entrada. É importante destacar que todo conhecimento da rede neural são os pesos.

Minsky e Papert (1969) apontaram uma limitação da rede perceptron a problemas linearmente separáveis.

Um problema é linearmente separável quando um conjunto de dados pode ser separado por um plano ou hiperplano. No exemplo abaixo, temos a representação dos dois problemas. No primeiro, é possível distinguir os dados e separá-los por um plano. Já no segundo exemplo, temos a demonstração dos dados não-linearmente separáveis.

Linearmente separável e Não linearmente separável

Arquitetura

O neurônio é a unidade de processamento de uma RNA. Na figura abaixo é apresentado um modelo simples de neurônio artificial (Haykin, 1999). As unidades de processamento desempenham um papel muito simples. Cada terminal de entrada do neurônio, simulando os dendritos, recebe um valor.

Os valores recebidos são armazenados e combinados por uma função matemática, equivalendo ao processamento realizado pelo soma. A saída da função é a resposta do neurônio para a entrada. Várias funções diferentes podem ser utilizadas. A entrada total recebida pelo neurônio (u), pode ser definida pela equação abaixo:

(1)    \begin{equation*} u=\sum_{j=1}^{d}{x_jw_j} \end{equation*}

Conforme já mencionado, os neurônios podem apresentar conexões de entrada negativas ou positivas. Um valor de peso igual a zero equivale a ausência da conexão associada.

A saída de um neurônio é definida por meio da aplicação de uma função de ativação à entrada total. Várias funções de ativação têm sido propostas na literatura. Quando a soma das entradas recebidas ultrapassa o limiar estabelecido, o neurônio torna-se ativo, ou seja, com saída +1. Quanto maior o valor do limiar, maior tem que ser o valor da entrada total para que o valor de saída do neurônio seja igual a 1.

Implementando o Perceptron de uma camada em C#

Agora vamos criar nossa rede perceptron de uma camada. O objetivo é fazer com que a rede neural aprenda a efetuar a operação lógica AND, bastante conhecida entre nós programados. Como sabemos, a tabela verdade do operador AND possui o seguinte resultado, onde o valor 0 é igual a falso e 1 é igual a verdadeiro:

Vamos fazer nosso modelo de rede neural aprender a chegar a este resultado sempre que receber dois valores de entrada.

Passo 1

Vamos começar definindo as entradas do nosso modelo, onde declaramos um vetor de vetores contendo dois elementos. Cada elemento representa um operando do operador AND.

using System;

namespace rna
{
    class Program
    {
        private static readonly double[][] inputs = new double[][]
        {
                new double[] { 0, 0 },
                new double[] { 0, 1 },
                new double[] { 1, 0 },
                new double[] { 1, 1 }
        };

        static void Main(string[] args)
        {
            
        }
    }
}

Passo 2

Agora vamos inicializar os pesos do modelo. Como foi dito anteriormente, todo conhecimento da rede neural está em seus pesos. É comum os pesos serem iniciados com valores aleatórios, porém, para simplificar, vamos iniciá-los com valor zero.

using System;

namespace rna
{
    class Program
    {
        private static readonly double[] weights = new double[] { .0, .0 };
        private static readonly double[][] inputs = new double[][]
        {
                new double[] { 0, 0 },
                new double[] { 0, 1 },
                new double[] { 1, 0 },
                new double[] { 1, 1 }
        };

        static void Main(string[] args)
        {
        }
    }
}

Passo 3

O próximo passo é criar uma função que irá calcular a saída do nosso neurônio. A função irá receber, por parâmetro, a saída esperada, calcular o produto escalar dos valores de entrada por seus pesos, e, após o cálculo do produto escalar, com a função DotProduct, irá avaliar o resultado, através da função de ativação que chamamos de StepFunction. Utilizamos a função de Heaviside na implementação, conhecida também como função degrau. Ela poderá ou não ativar o neurônio.

using System;

namespace rna
{
    class Program
    {
        private static readonly double[] weights = new double[] { .0, .0 };
        private static readonly double[][] inputs = new double[][]
        {
                new double[] { 0, 0 },
                new double[] { 0, 1 },
                new double[] { 1, 0 },
                new double[] { 1, 1 }
        };

        static void Main(string[] args)
        {
        }

        private static double CalculateOutput(double[] input)
        {

            var value = DotProduct(input, weights);

            var output = StepFunction(value);

            return output;
        }

        private static double DotProduct(double[] left, double[] right)
        {
            var total = 0d;
            for (int i = 0; i < left.Length; i++) 
            { 
               total += left[i] * right[i]; 
            } 

            return total; 
        } 

        private static int StepFunction(double value) => value >= 1 ? 1 : 0;
    }
}

Passo 4

Dado que nossa rede neural irá aprender a efetuar operações AND para as entradas que definimos,
o próximo passo é declarar o resultado esperado pela operação, que será 0, 0, 0 e 1, respectivamente. É com base no resultado esperado que calculamos a taxa de erro e balanceamos os pesos do nosso perceptron.

Então, temos:

using System;

namespace rna
{
    class Program
    {
        private static readonly double[] weights = new double[] { .0, .0 };
        private static readonly double[][] inputs = new double[][]
        {
                new double[] { 0, 0 },
                new double[] { 0, 1 },
                new double[] { 1, 0 },
                new double[] { 1, 1 }
        };

        static void Main(string[] args)
        {
            var outputs = new double[] { 0, 0, 0, 1 };
        }

        private static double CalculateOutput(double[] input)
        {

            var value = DotProduct(input, weights);

            var output = StepFunction(value);

            return output;
        }

        private static double DotProduct(double[] left, double[] right)
        {
            var total = 0d;
            for (int i = 0; i < left.Length; i++) 
            { 
               total += left[i] * right[i]; 
            } 
            return total; 
        } 

        private static int StepFunction(double value) => value >= 1 ? 1 : 0;
    }
}

Passo 5

O próximo passo é criar a função que irá treinar nossa rede neural. A função irá percorrer todos os valores de entrada e calcular o valor ideal, com base na saída esperada. Após obter a saída, fará o cálculo do erro simplificado, obtido pela diferença entre a saída esperada pela saída obtida. Vamos criar um laço para repetir as operações por um número infinito de épocas, até encontrar o menor erro, ou seja, erro igual a 0.

using System;

namespace rna
{
    class Program
    {
        private static readonly double[] weights = new double[] { .0, .0 };
        private static readonly double[][] inputs = new double[][]
        {
                new double[] { 0, 0 },
                new double[] { 0, 1 },
                new double[] { 1, 0 },
                new double[] { 1, 1 }
        };

        static void Main(string[] args)
        {
            var outputs = new double[] { 0, 0, 0, 1 };

            Train(outputs);
        }

        private static void Train(double[] outputs)
        {
            var errors = 1d;
            while (errors != 0)
            {
                errors = 0;
                for (var i = 0; i < outputs.Length; i++)
                {
                    var output = CalculateOutput(inputs[i]);
                    var error = Math.Abs(outputs[i] - output);
                    errors += error;
                }
                Console.WriteLine("Total de erros: " + errors);
            }
        }

        private static double CalculateOutput(double[] input)
        {

            var value = DotProduct(input, weights);

            var output = StepFunction(value);

            return output;
        }

        private static double DotProduct(double[] left, double[] right)
        {
            var total = 0d;
            for (int i = 0; i < left.Length; i++) 
            { 
               total += left[i] * right[i]; 
            } 
            return total; 
        } 

        private static int StepFunction(double value) => value >= 1 ? 1 : 0;
    }
}

Passo 6

Após o cálculo do erro, atualizamos os pesos de nossos valores de entrada, em seguida, exibimos o valor dos pesos atualizados:

using System;

namespace rna
{
    class Program
    {
        private static readonly double[] weights = new double[] { .0, .0 };
        private static readonly double[][] inputs = new double[][]
        {
                new double[] { 0, 0 },
                new double[] { 0, 1 },
                new double[] { 1, 0 },
                new double[] { 1, 1 }
        };

        static void Main(string[] args)
        {
            var outputs = new double[] { 0, 0, 0, 1 };

            Train(outputs);

        }

        private static void Train(double[] outputs)
        {
            var errors = 1d;
            while (errors != 0)
            {
                errors = 0;
                for (var i = 0; i < outputs.Length; i++)
                {
                    var output = CalculateOutput(inputs[i]);
                    var error = Math.Abs(outputs[i] - output);
                    errors += error;
                    for (var j = 0; j < weights.Length; j++)
                    {
                        weights[j] = weights[j] + (learningRate * inputs[i][j] * error);
                        Console.WriteLine("Peso atualizado: " + weights[j]);
                    }
                }
                Console.WriteLine("Total de erros: " + errors);
            }
        }

        private static double CalculateOutput(double[] input)
        {

            var value = DotProduct(input, weights);

            var output = StepFunction(value);

            return output;
        }

        private static double DotProduct(double[] left, double[] right)
        {
            var total = 0d;
            for (int i = 0; i < left.Length; i++) 
            { 
               total += left[i] * right[i]; 
            } 
            return total; 
        } 
 
        private static int StepFunction(double value) => value >= 1 ? 1 : 0;
    }
}

Passo 7

Após a implementação da função de treino do nosso perceptron, vamos verificar os resultados obtidos:

using System;

namespace rna
{
    class Program
    {
        private static readonly double[] weights = new double[] { .0, .0 };
        private static readonly double[][] inputs = new double[][]
        {
                new double[] { 0, 0 },
                new double[] { 0, 1 },
                new double[] { 1, 0 },
                new double[] { 1, 1 }
        };

        static void Main(string[] args)
        {
            Console.WriteLine("Treinando rede neural...\n");

            Train(outputs);

            Console.WriteLine("\nRede neural treinada.");

            Console.WriteLine("\nResultados obtidos:");

            ShowResults();
        }

        private static void Train(double[] outputs)
        {
            var errors = 1d;
            while (errors != 0)
            {
                errors = 0;
                for (var i = 0; i < outputs.Length; i++)
                {
                    var output = CalculateOutput(inputs[i]);
                    var error = Math.Abs(outputs[i] - output);
                    errors += error;
                    for (var j = 0; j < weights.Length; j++)
                    {
                        weights[j] = weights[j] + (learningRate * inputs[i][j] * error);
                        Console.WriteLine("Peso atualizado: " + weights[j]);
                    }
                }
                Console.WriteLine("Total de erros: " + errors);
            }
        }

        private static double CalculateOutput(double[] input)
        {

            var value = DotProduct(input, weights);

            var output = StepFunction(value);

            return output;
        }

        private static double DotProduct(double[] left, double[] right)
        {
            var total = 0d;
            for (int i = 0; i < left.Length; i++) 
            { 
               total += left[i] * right[i]; 
            } 
            return total; 
        } 

        private static int StepFunction(double value) => value >= 1 ? 1 : 0;

        private static void ShowResults()
        {
            for (int i = 0; i < inputs.GetLength(0); i++)
            {
                Console.WriteLine($"Input #{ i + 1}: { CalculateOutput(inputs[i]) }");
            }
        }
    }
}

Resultado da execução

$ dotnet run

Treinando rede neural...

Peso atualizado: 0
Peso atualizado: 0
Peso atualizado: 0
Peso atualizado: 0
Peso atualizado: 0
Peso atualizado: 0
Peso atualizado: 0.10000000149011612
Peso atualizado: 0.10000000149011612
Total de erros: 1
Peso atualizado: 0.10000000149011612
Peso atualizado: 0.10000000149011612
Peso atualizado: 0.10000000149011612
Peso atualizado: 0.10000000149011612
Peso atualizado: 0.10000000149011612
Peso atualizado: 0.10000000149011612
Peso atualizado: 0.20000000298023224
Peso atualizado: 0.20000000298023224
Total de erros: 1
Peso atualizado: 0.20000000298023224
Peso atualizado: 0.20000000298023224
Peso atualizado: 0.20000000298023224
Peso atualizado: 0.20000000298023224
Peso atualizado: 0.20000000298023224
Peso atualizado: 0.20000000298023224
Peso atualizado: 0.30000000447034836
Peso atualizado: 0.30000000447034836
Total de erros: 1
Peso atualizado: 0.30000000447034836
Peso atualizado: 0.30000000447034836
Peso atualizado: 0.30000000447034836
Peso atualizado: 0.30000000447034836
Peso atualizado: 0.30000000447034836
Peso atualizado: 0.30000000447034836
Peso atualizado: 0.4000000059604645
Peso atualizado: 0.4000000059604645
Total de erros: 1
Peso atualizado: 0.4000000059604645
Peso atualizado: 0.4000000059604645
Peso atualizado: 0.4000000059604645
Peso atualizado: 0.4000000059604645
Peso atualizado: 0.4000000059604645
Peso atualizado: 0.4000000059604645
Peso atualizado: 0.5000000074505806
Peso atualizado: 0.5000000074505806
Total de erros: 1
Peso atualizado: 0.5000000074505806
Peso atualizado: 0.5000000074505806
Peso atualizado: 0.5000000074505806
Peso atualizado: 0.5000000074505806
Peso atualizado: 0.5000000074505806
Peso atualizado: 0.5000000074505806
Peso atualizado: 0.5000000074505806
Peso atualizado: 0.5000000074505806
Total de erros: 0

Rede neural treinada.

Resultados obtidos:
Input #1: 0
Input #2: 0
Input #3: 0
Input #4: 1

Conclusão

Trouxe aqui uma maneira de implementar uma rede neural artificial de uma camada usando C#.
Espero que você tenha achado esse artigo interessante e inspirador, e caso tenha qualquer dúvida, ou apenas queira falar mais sobre o assunto, conecte-se comigo no Instagram e confira meus projetos no GitHub.


Referência:

Inteligência Artificial – Uma Abordagem de Aprendizado de Máquina – Katti Faceli, Ana C. Lorena, João Gama e André Carvalho, 2011.

Receba meu conteúdo no Telegram

Se você deseja receber outros conteúdos direto no seu celular, entre no meu canal no Telegram.
Lá eu compartilho dicas para você dominar definitivamente a escrita do código limpo.

Hey,

o que você achou deste conteúdo? Conte nos comentários.

O seu endereço de e-mail não será publicado. Campos obrigatórios são marcados com *