Redes Neurais 3: Implementação de um Perceptron em Python

Com os conceitos dos posts anteriores e todo esse embasamento, vamos seguir aos notebooks Jupyter!

Eu gosto de começar um projeto pensando em como podemos representá-lo em classes. Parece razoável pensar em uma classe Perceptron com alguns métodos que falamos anteriormente.

Minha estratégia de implementação será:

  1. Discutir minha definição de classe do Perceptron;
  2. Implementar os métodos concretos;
  3. Determinar um data set de exemplo para um teste;
  4. Ver se o perceptron aprende mesmo!

1. Definindo a classe Perceptron

Vamos pensar em quais atributos e métodos vamos precisar para desenhar nossa classe Perceptron.

Atributos

  • Taxa de aprendizagem (η);
  • Número de Epochs (para mais informações, veja [1]);

Métodos

  • Método para a função ativação: _activation(X);
    • Descrição: Como função de ativação, escolhi a função degrau, descrita no post anterior. Outras funções são comuns para esta finalidade (para mais informações, veja [2]).
  • Método para a função soma: _sum(X);
    • Descrição: O processo de sum vai multiplicar X pelo pelo peso correspondente.
  • Método para iniciar um treinamento: fit(X, y);
    • Descrição: É o método que vai de fato colocar as epochs para rodar, calcular os erros e manter um historico de erros na lista _errors.
  • Método para gerar uma predição: predict(X);
    • Descrição: Passa o input X para a função de soma e de ativação e retorna a classe esperada.

2. Implementação

class Perceptron(object):
    """
    Um perceptron simples com componentes simples.
    """
    
    def __init__(self, learning_rate=0.01, epochs=50, verbose=False):
        """
        Inicializa o perceptron com os valores de
        learning rate (taxa de aprendizado), epochs e um
        parametro de verbose, sinalizando que queremos ver os detalhes
        do funcionamento.
        """
        self.verbose = verbose
        self.learning_rate = learning_rate
        self.epochs = epochs
        
        if(self.verbose):
            print("[>] Perceptron parameters:\n- learning rate: {}\n- # epochs: {}".format(
                self.learning_rate,
                self.epochs
            ))
    
    def _sum(self, X):
        """
        O processo de sum vai multiplicar o input pelo peso.
        """
        dp = np.dot(X, self._weights) + self._bias
        
        if(self.verbose):
            print("[!] Aggregation ({} and {}) + weight {} = {}".format(
                X,
                self._weights,
                self._bias,
                dp
            ))
        return dp
    
    def _activation(self, value):
        """
        Como função de ativação, escolhi a função degrau.
        Outras funções são bem comuns, mas o ganho entre elas
        faz com que a degrau seja uma das com melhor custo-benefício.
        """
        return np.where(value >= 0.0, 1, -1)

    def fit(self, X, y):
        """
        Vamos começar com os pesos como um array
        numpy zerado, já que não temos pistas sobre
        quais são os pesos corretos.
        """
        self._bias = np.random.uniform(-1, 1)
        self._weights = np.random.uniform(-1, 1, (X.shape[1]))
        self._errors = []
        
        if(self.verbose):
            print("[>>] Starting training routine:\n- initial weights: {}\n- initial errors: {}".format(
                self._weights,
                self._errors
            ))
            print("- train set (X): {}\n- train set (y): {}\n---".format(
                X,
                y
            ))

        # Iterando o numero de epochs...
        for epoch_number in range(self.epochs):
            errors = 0
            
            if(self.verbose):
                print("[>>] Entering epoch {}\n- weights: {}\n- errors: {}".format(
                    epoch_number,
                    self._weights,
                    self._errors
                ))
            
            for xi, target in zip(X, y):
                if(self.verbose):
                    print("[!] Learning xi = {} (target {})".format(xi, target))
                
                output = self.predict(xi)
                update = self.learning_rate * (target - output)
                
                if(self.verbose):
                    print("[!] {} = {}({} - {})".format(
                        update,
                        self.learning_rate,
                        target,
                        output
                    ))
                    print("[<] weights was {}".format(self._weights))
                
                self._bias += update
                
                self._weights += update * xi
                
                if(self.verbose):
                    print("[>] weights are now {}".format(self._weights))
                
                errors += int(update != 0.0)
                
                if(self.verbose):
                    print("-")
                
            self._errors.append(errors)
            
            if(self.verbose):
                print("--")
        
        
        return self
    
    def predict(self, X):
        result = self._activation(self._sum(X))
        
        if(self.verbose):
            print("[?] Should fire? (is >= 0): {}".format(
                result
            ))
        
        return result

3. Teste

Vamos testar nosso perceptron! Para fazer isso, vamos utilizar um método muito pratico do sklearn, o make_blobs. Este método cria dados concentrados em volta de um ponto. É bastante util para gerar dados para testes de regressão e classificação. Nosso perceptron é capaz de aprender a classificar dados lineares.

    import matplotlib.pyplot as plt
    from sklearn.datasets import make_blobs

    # Vamos criar 2 blobs com 200 samples e 2 features (2 dimensões) e 2 centros.
    blobs = make_blobs(n_samples=2, n_features=2, centers=2)

    # Em seguida, vamos plotar os dados gerados para visualização.
    plt.scatter(blobs[0][:,0], blobs[0][:,1], c=blobs[1])
    plt.show()

Pronto. Vamos colocar nosso perceptron para rodar e classificar os blobs.

Para isso, basta inicializarmos a nossa classe anterior, separar os X (features) e o y (classe).

Vamos em seguidar rodar o comando fit do perceptron para treinar os pesos.

    ppn = Perceptron(epochs=50, learning_rate=0.01, verbose=False)
    X = blobs[0]
    y = blobs[1]

    # Apenas para substituir as classes 0 e 1 para 1 e -1
    # A classe 1 é a que desejamos encontrar.
    y[y == 0] = -1
    ppn.fit(X, y)

Após alguns segundos de treinamento, nosso perceptron já vai ter aprendido os pesos para classificar os blobs.

Com a ajuda do método plot_decision_regions da lib mlxtend, podemos plotar a região de classificação:

plot_decision_regions(X, y, clf=ppn)
plt.title('Perceptron')
plt.show()
Regiões plotadas do nosso perceptron.

Vamos também dar uma olhada no histórico de erros:

Histórico de missclassification do perceptron.

Podemos notar que nas primeiras epochs, os pesos estavam fazendo o perceptron classificar algumas observações de maneira errada.

Conforme os pesos foram sendo corrigidos, o perceptron passa a classificar 100% dos pontos de maneira correta.

Com pequenas alterações no código, podemos desenhar uma sequência de gráficos mostrando como perceptron está aprendendo:

A cada epoch, o perceptron chega mais próximo da classificação correta.

O gráfico de erros da animação anterior é:

Erros do perceptron até 0 erros.

Espero que tenha sido interessante para você leitor como foi pra mim construir essa série de posts sobre Machine Learning!

Até a próxima!

Bibliografia

[1] https://towardsdatascience.com/epoch-vs-iterations-vs-batch-size-4dfb9c7ce9c9

[2] https://medium.com/the-theory-of-everything/understanding-activation-functions-in-neural-networks-9491262884e0

Redes Neurais 2: Perceptrons de Rosenblatt

“For me there is no absolute knowledge: everything goes only by probability. Both Descartes and Schelling explicitly reported an experience of sudden illumination when they began to see everything in a different light.”
Wang, H.  (1996)

Eu acho fundamental (e interessante à beça) alinhar a história com o pensamento, vamos começar então pela:

História e filosofia por trás do perceptron de Rosenblatt

Com o advento do computador, as idéias de McCulloch e Pitts puderam ganhar um pouco mais de tangibilidade. Já éramos capazes de construir máquinas para realizar cálculos por nós.

Frank Rosenblatt, psicólogo estadunidense, sugeriu um método de aprendizado computacional. Seu objetivo era transcrever o “mundo dos fenômenos”, mundo que nós, humanos, estamos familiarizados, para uma “linguagem”, de modo que um computador pudesse receber estes “fenômenos” como entradas. Como ele mesmo explica:

Since the advent of electronic computers and modern servo systems, an increasing amount of attention has been focused on the feasibility of constructing a device processing such human-like functions as perceptions, recognition, concept formation, and the ability to generalize from experience. In particular, interest has centered on the idea of a machine which would be capable of conceptualizing inputs impinging directly from the physical environment of light, sound, temperature, etc. — the “phenomenal world” with which we are all familiar — rather than requiring the intervention of a human agent to digest the code has necessary information.

Frank Rosenblatt (1957) [1]

O objetivo do perceptron, segundo Rosenblatt, é ilustrar de maneira geral algumas das propriedades fundamentais de um sistema inteligente sem se apegar as condições, muitas vezes desconhecidas, de um sistema biológico [2, p2-3].

Os componentes de um perceptron são:

  • Uma arquitetura de rede;
  • Neurônios MCP;
  • Uma Regra de Aprendizado.

Na época causou uma grande euforia, porém sem as necessárias ferramentas teóricas e sem a devida capacidade computacional (como o algoritmo de back propagation de 1986), o machine learning ficou durante um bom tempo sem ter alguma aplicação prática. Hoje em dia, têm aplicações em quase os todos campos da ciência e impacto direto na nossa vida.

Finalmente, a matemática

Atualmente, o perceptron é um conceito que já foi generalizado em termos de machine learning. O perceptron pertence a categoria de aprendizado supervisionado (vou escrever sobre isso mais pra frente). De maneira simplificada isto quer dizer que o modelo, em sua fase inicial, não sabe nada (que seja útil pelo menos, pois começa com um estado aleatório) e se corrige (diminui seu erro) para aprender o que estamos ensinando a ele.

O perceptron resolve uma categoria específica de problemas, os chamados problemas de classificação binária. Como vamos ver, o perceptron consegue aprender a classificar pontos em um plano e atualizar sua estimativa conforme novas observações são adicionadas.

Função de ativação

A função de ativação (leia-se o que faz o neurônio artificial se ativar ou desativar), ou seja, o output y, é definido como sendo:

\[
y=g\left(\sum ^{M}_{j=0} I_{j} \ .\ w_{j}\right) =g\left( w^{T} I\right)
\]

Onde I são os inputs do perceptron, w os pesos relacionados a cada input e g uma “função degrau”. Essa função degrau determina basicamente qual classe o perceptron “ativou”. Vamos convencionar para simplificar a utilização das classes “-1” e “1”. Portanto nossa função degrau g pode ser definida como sendo:

\[
g(z)=\begin{cases}
1 & \text{if } z\geq \theta \\
-1 & \text{caso contrário} .
\end{cases}
\]

Onde θ é o nosso limite, valor limite, ou threshold, como é comum na literatura em inglês.

Função degrau utilizada como função de ativação [3].

Essa abordagem está de acordo com a regra “all-or-none” que escrevi no post anterior.

De maneira simplificada: o output do perceptron é calculado por uma função degrau que leva como parametro a soma dos inputs vezes os pesos de cada input. Se a soma for > que um número qualquer (inicialmente aleatório), o output é “1”. Caso contrário “-1”.

É fácil notar que pesos (w) e o valor de threshold (θ) são as variáveis que podemos corrigir aqui, certo? É é justamente onde acontece a…

Aprendizagem

A ideia de Rosenblatt começa a ficar interessante nesse momento. A diferença é que o algoritmo proposto por ele é capaz de aprender (se ajustar com o intuito de diminuir o erro) os pesos para os sinais de input. Assim, após um determinado número de interações, o perceptron espontaneamente aprende o que estamos ensinando a ele, nesse caso separar os inputs em duas categorias -1 e 1.

Aprendizado de um perceptron para separar dois grupos de observações em um plano cartesiano [4].

Para aprender, o perceptron segue o seguinte algoritmo:

  1. Defina os pesos w dos inputs I como números aleatórios;
  2. Para cada conjunto de percepções (ou observações):
    1. Calcule o output do perceptron utilizando a função de ativação g;
    2. Atualize os pesos do perceptron para que se aproxime da resposta correta.

O calculo de atualização dos pesos é a etapa mais bela desse algoritmo. Para cada iteração, vamos atualizar os pesos levando em consideração o peso anterior e o quão errado ele estava, usando, para isto, o valor “alvo” (esperado) e o valor “saida” (encontrado):

\[
w_{j} =w_{j} +\Delta w_{j}\\
\]

Onde:

\[
\Delta w_{j} =\eta \ (\text{alvo}^{(i)} -\text{saida}^{(i)} )\ x^{(i)}_{j}
\]

e η é a taxa de aprendizado (learning rate), geralmente um número bastante pequeno, utilizado para que o perceptron não fique procurando a solução ideal muito longe, “alvo” é a label real do objeto e “saída” a label que o perceptron julgou ser verdadeira.

É interessante notar que caso o perceptron julgue uma label como correta, o delta será zero:

\[
\begin{array}{l}
\Delta w_{j} =\eta (-1^{(i)} – -1^{(i)} )\ x^{(i)}_{j} =0\\
\Delta w_{j} =\eta (1^{(i)} -1^{(i)} )\ x^{(i)}_{j} =0
\end{array}
\]

Desta maneira, a cada nova interação, os pesos são corrigidos e atualizados de modo que esteja cada vez mais próximo do resultado verdadeiro das observações que entregamos a ele.

No próximo vou tentar fazer uma implementação em Python rudimentar do que vi até aqui.

Um abraço!

Bibliografia

Referências

[1] Frank Rosenblatt, The Perceptron – A perceiving and recognizing automaton. January 1957 – https://blogs.umass.edu/brain-wars/files/2016/03/rosenblatt-1957.pdf

[2] http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.335.3398&rep=rep1&type=pdf

[3] https://sandipanweb.wordpress.com/2016/07/09/convergence-of-the-gradient-descent-algorithms-stochastic-and-batch-for-the-linear-logistic-regression-and-perceptron/

[4] http://sebastianraschka.com/Articles/2015_singlelayer_neurons.html

Leituras

https://blogs.umass.edu/brain-wars/files/2016/01/rosenblatt-1967.pdf

https://babel.hathitrust.org/cgi/pt?id=mdp.39015039846566;view=1up;seq=1;size=125

https://hdl.handle.net/2027/mdp.39015039846566

Redes Neurais 1: O primeiro modelo de neurônio artificial de McCulloch e Pitts

Obter um embasamento teórico mais robusto em machine learning + não ter encontrado blogs com o formato que eu gosto de ler = me veio a motivação para fazer uma série de posts sobre redes neurais com um pouco mais de profundidade!

Biologia & Matemática

Começando em 1943. Uma fusão de biologia com matemática criou o paper A Logical Calculus of the Ideas Immanent In Nervous Activity [1]. A junção poderosa produziu um modelo abstrato para os neurônios artificiais criado pelos cientistas Warren S. McCulloch e Walter H. Pitts.

Eles observaram alguns comportamentos dos neurônios biológicos. Entre eles: a observação de fenômenos de refração de sinais caso os neurônios já houvessem sido excitados, um número fixo obrigatório de sinapses para que ele alterasse o seu estado (all-or-none), entre algumas outras características muito interessantes.

Ao final, foram descritas 5 suposições para que os cálculos dos cientistas pudessem ser coerentes. Então eles descreveram (abaixo uma tradução livre direto do paper) os tópicos:

A atividade do neurônio é um processo de “all-or-none“.
Um certo número fixo de sinapses precisam ser “disparadas” dentro de um período de adição latente para excitar o neurônio a qualquer momento. Este número independe de qualquer atividade prévia ou localização do neurônio.
O único delay significativo dentro de um sistema sináptico é o delay sináptico.
A atividade de um inibidor em um determinado momento pára completamente a atividade de um determinado neurônio naquele momento.
A estrutura da rede não se altera com o tempo.

(mcculloh; pitts, 1943).

Abaixo segue uma esquemática do modelo de neurônio artificial:

Um neurônio completo com entradas, pesos, uma somatória para calcular o número resultante do Input In vezes o peso Wn (In x Wn) e a função de ativação com o número T de limite para que o neurônio possa ativar em um output y.

A função após a soma, neste caso, aplica-se uma função simples:

  • Pegue a soma dos inputs vezes os pesos (∑Ii  . Wi):
    • Se a soma for > que  o limite T, y = 1;
    • y = 0 caso contrário.

Ao colocarmos os parâmetros corretos neste modelo de neurônios artificiais podemos construir coisas interessantes, como portas lógicas:

NOT, AND e OR gate usando neurônios artificiais. [2].

Em seguida, Rosenblat desenvolveu, incluindo algumas melhorias no modelo proposto por McCulloch e Pitts, o Perceptron.

Toda essa teoria, que começou muitos anos atrás, possibilita, hoje, juntamente com os avanços tecnológicos no quesito de velocidade computacional e memória extremamente veloz, a construção de soluções para problemas muito complexos, que de certa forma ensinam o computador como agir dado uma entrada. Deixou de ser uma programação guiada a algoritmos rígidos e passou a ser uma programação por exemplos e aprendizagem.

É uma mistura de prazer e orgulho viver em uma época como a nossa.

Bom, é isso pessoal. Até o próximo post!

Links e leituras complementares

[1] http://www.cse.chalmers.se/~coquand/AUTOMATA/mcp.pdf
[2] http://www.cs.bham.ac.uk/~jxb/NN/l3.pdf

Import Public SSH Keys from Github

When setting up a new development server, we usually add our public ssh key to ~/.ssh/authorized_keys file.
Github has a pretty nice feature, change your nick from the below url and send it contents to authorized_keys with curl command:

curl https://github.com/waldirbertazzijr.keys > ~/.ssh/authorized_keys

Simple as that!

Permissions

Both servers’ .ssh folder should be 700 chmod for this to work. Don’t ever forget this one.