No notebook anterior, nós aprendemos intuitivamente como o perceptron aprende. De maneira geral, nós vamos atualizando os pesos e o bias sempre buscando diminuir uma função de custo. Nesse notebook, nós vamos ver como esse aprendizado realmente acontence, tanto na teoria quanto na prática. Também utilizaremos o Perceptron para resolver problemas de classificação e regressão.

Objetivos:

  • Implementar o perceptron e seu modelo de aprendizado em Python puro e Numpy
  • Utilizar o perceptron para regressão e classificação

Sumário

Imports e Configurações


In [ ]:
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from random import random
from sklearn.linear_model import LinearRegression
from sklearn.preprocessing import MinMaxScaler
from sklearn.datasets.samples_generator import make_blobs

%matplotlib inline

Introdução

O tipo mais básico de Rede Neural Artificial é formada por apenas um neurônio, o Perceptron. Inicialmente, o Perceptron foi projetado para ser um classificador binário linear responsável por mapear uma ou mais entradas em uma saída desejada. Porém, também podemos utilizá-lo para resolver problemas de regressão linear. Ele foi projetado em 1957 por Frank Rosenblatt.

O perceptron é formado por:

  • entradas $x_1,...,x_D$: representam os atributos dos seus dados com dimensionalidade $D$. O Perceptron aceita qualquer tamanho de entrada, porém a saída é sempre apenas um valor.
  • junção aditiva $\sum$: também chamada de função agregadora, nada mais é que a soma ponderada das entradas com os pesos ($w_1,...,w_D)$. Em geral, o resultado é somado com um bias $b$, responsável por deslocar o resultado do somatório. A junção aditiva é descrita pela seguinte fórmula:
$$\sum_i^D{x_iw_i} + b$$
  • função de ativação $f$: utilizada para mapear o resultado da junção aditiva em uma saída esperada. Mais detalhes abaixo.

Logo, o Perceptron é representado pela seguinte fórmula matemática:

$$\widehat{y}_i = f(\sum_i^D{x_iw_i} + b)$$

Onde:

  • $D$: representa a dimensionalidade das amostras, ou seja, a quantidade de atributos de cada amostra.
  • $x_i$: representam os atributos de uma amostra que servem de entrada para o Perceptron.
  • $w_i$: representam os pesos sinápticos que ponderam as entradas.
  • $b$: representa o bias, responsável por deslocar a fronteira de decisão além da origem e não depende de nenhum valor de entrada. Repare que o bias encontra-se fora do somatório.
  • $f$: função de ativação. Quando a função de ativação é linear, ou seja, nenhuma transformação é aplicada no resultado da junção aditiva, o Perceptron atua como um Regressor Linear. Se precisamos efetuar uma Classificação binária, devemos utilizar a função step (também conhecida como função degrau) para mapear a saída em um valor discreto (0 ou 1):
$$f = \begin{cases}1 & se \ wx+b > 0\\0 & caso \ contr\acute ario\end{cases}$$
  • $\widehat{y}$: representa a saída do Perceptron (o valor predito).

Observações importantes:

  • O Perceptron não faz Classificação Multiclasse.
  • A atualização dos pesos é online, ou seja, efetuada amostra a amostra utilizando uma fórmula pré-definida que veremos na seção a seguir.

Regra de Aprendizado do Perceptron

O Perceptron tem sua própria forma de aprendizado conforme definido no seu artigo original. Na verdade, a fórmula para atualização dos pesos e bias é bem simples:

$$w_i = w_i + \lambda(y_i - \widehat{y}_i)x_i$$


$$b_i = b_i + \lambda(y_i - \widehat{y}_i)$$

Onde $\lambda$ é a taxa de aprendizagem (learning rate).

Repare que $y_i - \widehat{y}_i$ significa calcular a diferença entre o valor esperado ($y_i$) e o valor predito ($\widehat{y}_i$). Supondo que estamos fazendo classificação binária de uma amostra $(x_i, y_i)$. Nesse caso, teremos duas possibilidades:

  • O valor esperado é $y_i = \widehat{y}_i$, ou seja, a saída do Perceptron (após a função de ativação step) é igual a saída esperada. Nesse caso, a diferença $y_i - \widehat{y}_i = 0$ e não haverá atualização de pesos.
  • O valor esperado é $y_i \neq \widehat{y}_i$, ou seja, a saída do Perceptron (após a função de ativação step) é diferente da saída esperada. Nesse caso, a atualização dos pesos será dada pela diferença $y_i - \widehat{y}_i$. Repare que:
    • quando essa diferença é negativa (ou seja, $y_i = 0$ e $\widehat{y}_i = 1$), os pesos tendem a diminuir.
    • quando essa diferença é positiva (ou seja, $y_i = 1$ e $\widehat{y}_i = 0$), os pesos tendem a aumentar.

Pseudo-algoritmo do Perceptron

  1. Inicialize os pesos $w$ e o bias $b$
  2. Para cada amostra $(x_n, y_n)$ do nosso banco:
    1. Calcule $\widehat{y} = f(\sum_i^D{x_iw_i} + b)$, onde $f$ é a função step para classificação e linear no caso da regressão
    2. Calcule o $erro = y_n - \widehat{y}$
    3. Atualize os pesos $w_i = w_i + \lambda*erro*x_i$
    4. Atualize o bias $b_i = b_i + \lambda*erro$
  3. Repita o passo 2 por N vezes ou até que alguma medida de custo para o $erro$ seja menor que um valor pré-determinado.

Repare, como dito lá em cima, que a atualização dos pesos e bias é feito a cada amostra, e não somente após ver todas as amostras do banco.

Classificação

Porta AND/OR


In [ ]:
x = np.array([[0, 0], [0, 1], [1, 0], [1, 1]])
#y = np.array([0, 1, 1, 1]) # porta OR
y = np.array([0, 0, 0, 1]).T # porta AND

print(x.shape, y.shape)

Python


In [ ]:

Numpy


In [ ]:

Exercício de Classificação


In [ ]:
x, y = make_blobs(n_samples=100, n_features=2, centers=2, random_state=1234)

print(x.shape, y.shape)
plt.scatter(x[:,0], x[:,1], c=y.ravel(), cmap='bwr')

In [ ]:
def plot_linear_classifier(x, y, w, b):
    x1_min, x1_max = x[:,0].min(), x[:,0].max()
    x2_min, x2_max = x[:,1].min(), x[:,1].max()

    x1, x2 = np.meshgrid(np.linspace(x1_min-1, x1_max+1,100), np.linspace(x2_min-1, x2_max+1, 100))
    x_mesh = np.array([x1.ravel(), x2.ravel()]).T

    plt.scatter(x[:,0], x[:,1], c=y.ravel(), cmap='bwr')

    y_mesh = np.dot(x_mesh, np.array(w).reshape(1, -1).T) + b
    y_mesh = np.where(y_mesh <= 0, 0, 1)

    plt.contourf(x1, x2, y_mesh.reshape(x1.shape), cmap='bwr', alpha=0.5)
    plt.xlim(x1_min-1, x1_max+1)
    plt.ylim(x2_min-1, x2_max+1)

Python


In [ ]:

Numpy


In [ ]:

Regressão

Para transformar o Perceptron em um regressor linear, só o que temos de fazer é remover a função de ativação step, transformando-a em uma função de ativação linear.

Apesar dessa modificação, a fórmula de atualização dos pesos não sofre nenhuma alteração.

Vamos, então, implementar nosso perceptron para classificação em Python, Numpy, Keras e TensorFlow:


In [ ]:
df = pd.read_csv('data/medidas.csv')
print(df.shape)
df.head(10)

In [ ]:
x = df.Altura.values
y = df.Peso.values

plt.figure()
plt.scatter(x, y)
plt.xlabel('Altura')
plt.ylabel('Peso')

In [ ]:
print(x.shape, y.shape)

In [ ]:
x = x.reshape(-1, 1)

print(x.shape, y.shape)

Python

Exercício: tentar estimar as learning_rates de w e b. Elas são diferentes por que nossos dados não estão na mesma escala!


In [ ]:
D = x.shape[1]
w = [2*random() - 1 for i in range(D)]
b = 2*random() - 1

for step in range(10001):
    cost = 0
    for x_n, y_n in zip(x, y):
        # qual linha devemos remover para transformar o Perceptron num regressor?
        y_pred = sum([x_i*w_i for x_i, w_i in zip(x_n, w)]) + b
        y_pred = 1 if y_pred > 0 else 0
        error = y_n - y_pred
        w = [w_i + 1.0*error*x_i for x_i, w_i in zip(x_n, w)]
        b = b + 1.0*error
        cost += error**2

    if step%1000 == 0:
        print('step {0}: {1}'.format(step, cost))

print('w: ', w)
print('b: ', b)

Numpy


In [ ]:
D = x.shape[1]
w = 2*np.random.random(size=D)-1
b = 2*np.random.random()-1 

for step in range(10001):
    cost = 0
    for x_n, y_n in zip(x, y):
        # qual linha devemos remover para transformar o Perceptron num regressor?
        y_pred = np.dot(x_n, w) + b 
        y_pred = np.where(y_pred > 0, 1, 0)
        error = y_n - y_pred
        w = w + 1.0*np.dot(error, x_n)
        b = b + 1.0*error
        cost += error**2
    
    if step%1000 == 0:
        print('step {0}: {1}'.format(step, cost))
    
print('w: ', w)
print('b: ', b)

Numpy com Pré-processamento


In [ ]:
minmax = MinMaxScaler(feature_range=(-1,1))
x = minmax.fit_transform(x.astype(np.float64))

print(x.min(), x.max())

In [ ]:
reg = LinearRegression()
reg.fit(x,y)

print('w: ', reg.coef_)
print('b: ', reg.intercept_)

In [ ]:
D = x.shape[1]
w = 2*np.random.random(size=D)-1
b = 2*np.random.random()-1 

learning_rate = 1.0 # <- tente estimar a learning_rate

for step in range(1001):
    cost = 0
    for x_n, y_n in zip(x, y):
        y_pred = np.dot(x_n, w) + b 
        error = y_n - y_pred
        w = w + learning_rate*np.dot(error, x_n)
        b = b + learning_rate*error
        cost += error**2
    
    if step%100 == 0:
        print('step {0}: {1}'.format(step, cost))
    
print('w: ', w)
print('b: ', b)

Exercício de Regressão


In [ ]:
df = pd.read_csv('data/notas.csv')

print(df.shape)
df.head(10)

In [ ]:
plt.figure(figsize=(20, 4))

plt.subplot(1, 3, 1)
plt.scatter(df.prova1.values, df.final.values)
plt.xlabel('Prova 1')
plt.ylabel('Final')

plt.subplot(1, 3, 2)
plt.scatter(df.prova2.values, df.final.values)
plt.xlabel('Prova 2')
plt.ylabel('Final')

plt.subplot(1, 3, 3)
plt.scatter(df.prova3.values, df.final.values)
plt.xlabel('Prova 3')
plt.ylabel('Final')

In [ ]:
x = df[['prova1', 'prova2', 'prova3']].values
y = df['final'].values

print(x.shape, y.shape)

In [ ]:
minmax = MinMaxScaler(feature_range=(-1,1))
x = minmax.fit_transform(x.astype(np.float64))

In [ ]:
reg = LinearRegression()
reg.fit(x, y)

print('w: ', reg.coef_)
print('b: ', reg.intercept_)

Python


In [ ]:
D = x.shape[1]
w = [2*random() - 1 for i in range(D)]
b = 2*random() - 1

learning_rate = 1.0 # <- tente estimar a learning_rate

for step in range(1): # <- tente estimar o número de passos
    cost = 0
    for x_n, y_n in zip(x, y):
        y_pred = sum([x_i*w_i for x_i, w_i in zip(x_n, w)]) + b
        error = y_n - y_pred
        w = [w_i + learning_rate*error*x_i for x_i, w_i in zip(x_n, w)]
        b = b + learning_rate*error
        cost += error**2
        
    if step%200 == 0:
        print('step {0}: {1}'.format(step, cost))

print('w: ', w)
print('b: ', b)

Numpy


In [ ]:
D = x.shape[1]
w = 2*np.random.random(size=D)-1
b = 2*np.random.random()-1       

learning_rate = 1.0 # <- tente estimar a learning_rate

for step in range(1): # <- tente estimar o número de passos
    cost = 0
    for x_n, y_n in zip(x, y):
        y_pred = np.dot(x_n, w) + b 
        error = y_n - y_pred
        w = w + learning_rate*np.dot(error, x_n)
        b = b + learning_rate*error
        cost += error**2
    
    if step%200 == 0:
        print('step {0}: {1}'.format(step, cost))
    
print('w: ', w)
print('b: ', b)

Referências