No notebook anterior, nós aprendemos sobre o Perceptron. Vimos como ele aprende e como pode ser utilizado tanto para classificação binária quanto para regressão linear. Nesse notebook, nós veremos um algoritmo muito parecido com o Perceptron, mais conhecido como Adaline, que foi uma proposta de melhoria ao algoritmo original do Perceptron. Veremos as semelhanças e diferenças entre os dois algoritmos e iremos implementá-lo utilizando python e numpy. Por fim, vamos aplicar nos mesmos problemas de classificação do notebook do Perceptron para entender de fato suas diferenças. O código para utilizar o Adaline em problemas de regressão é exatamente o mesmo do perceptron.
Objetivos:
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
Poucos meses após a publicação do teorema da convergência do Perceptron por Rosenblatt, os engenheiros da Universidade de Stanford, Bernard Widrow e Marcian Hoff, publicaram um trabalho descrevendo uma rede neural muito parecida com o Perceptron, a Adaline (do inglês ADAptive LINear Element). Porém, ao invés de utilizar a função step como função de ativação, a Adaline utiliza função de ativação linear e tem uma nova regra de aprendizado supervisionado, conhecida como regra de Widrow-Hoff (ou regra delta, ou ainda regra LMS).
De fato, tanto o Perceptron quanto o Adaline possuem muitas características semelhantes e é comum ver o pessoal confundindo o Perceptron com o Adaline. Entre as principais semelhanças, podemos destacar:
Porém, a principal diferença entre o Perceptron e a Adaline é que o Perceptron utiliza os labels das classes para fazer a atualização dos pesos, enquanto a Adaline utiliza o resultado da função de ativação (linear) como valor contínuo de predição. Isto é, ao invés da saída ser discreta como no Perceptron (0 ou 1), na Adaline a saída pode ser qualquer valor contínuo. Essa diferença fica mais clara quando vemos a figura a seguir:
Repare, como dito, que ambos têm a função step. No Perceptron, ela é utilizada como função de ativação. No Adaline, por sua vez, a função de ativação é linear e a funcão step é utilizada para gerar a predição.
Por calcular a saída como um valor contínuo, muitos consideram o Adaline mais poderoso, uma vez que a diferença entre a saída desejada e o valor predito ($y_i - \widehat{y}_i$) nos diz agora "o quanto estamos certos ou errados". Na prática, isso faz com o que o Adaline tente encontrar a "melhor solução" para o problema, ao invés de somente uma "solução adequada". Tomando como exemplo a figura abaixo, o Perceptron pode encontrar diversas retas que separam as classes, enquanto o Adaline tenta encontrar a melhor reta que separa as classes.
Ok, mas como isso muda o aprendizado? É o que veremos a seguir.
A atualização dos pesos do Adaline é dada pela mesma fórmula do Perceptron:
$$w_i = w_i + \lambda(y_i - \widehat{y}_i)x_i$$Onde $\lambda$ é a taxa de aprendizagem.
Mas você já imaginou da onde vem essa fórmula? Em primeiro lugar, o método de atualização dos pesos é baseado na Regra Delta (Delta Rule). Sendo $\overrightarrow{w} = \{w_1, w_2, ..., w_D\}$, a atualização dos pesos é dada por:
$$\overrightarrow{w} = \overrightarrow{w} - \Delta{\overrightarrow{w}}$$em que:
$$\Delta{\overrightarrow{w}} = \lambda\nabla E(\overrightarrow{w})$$Sendo $\nabla E(\overrightarrow{w})$ o gradiente de uma função que depende de $\overrightarrow{w}$ e que queremos minimizar.
No caso do Adaline, a função de custo é dada pela soma dos erros quadrados:
$$J(w) = \frac{1}{2}\sum_{i}^N (y_i - \widehat{y}_i)^2$$Onde $N$ é a quantidade de amostras nos dados, e as demais variáveis representam as mesmas vistas anteriormente. Repare que a função de custo é quase uma Mean Squared Error (MSE), só que ao invés de dividir por $N$, estamos dividindo por 2 o resultado do somatório. O por quê disso será entendido mais a frente na demonstração.
Queremos encontrar, então, o vetor $\overrightarrow{w}$ que minimiza a função $J$. Assim, temos:
$$\frac{\partial J}{\partial w_i} = \frac{\partial}{\partial w_i}\frac{1}{2}\sum_i^N (y_i - \widehat{y}_i)^2$$Como a derivada do somatório é igual ao somatório das derivadas:
$$= \frac{1}{2}\sum_i^N \frac{\partial}{\partial w_i}(y_i - \widehat{y}_i)^2$$Aplicando a regra da cadeia:
$$= \sum_i^N (y_i - \widehat{y}_i)\frac{\partial}{\partial w_i}(y_i - \widehat{y}_i)$$Repare que, quando derivamos $(y_i - \widehat{y}_i)^2$, o expoente 2, ao sair do somatório, foi multiplicado por $\frac{1}{2}$, tornando-o 1. Isso é o que os matemáticos denominam de "conveniência matemática".
Como $\widehat{y}_i = x_iw_i + b$ é uma função que depende de $w$, e sua derivada em relação a $w_i$ é apenas $x_i$, temos que:
$$\frac{\partial J}{\partial w_i} = \sum_i^N (y_i - \widehat{y}_i)(-x_i)$$$$\frac{\partial J}{\partial w_i} = -\sum_i^N (y_i - \widehat{y}_i)x_i$$$$\frac{\partial J}{\partial \overrightarrow{w}} = -(\overrightarrow{y} - \overrightarrow{\widehat{y}_i})\overrightarrow{x}$$De maneira análoga, podemos calcular que a derivada de $J$ em relação a $b_i$ é:
$$\frac{\partial J}{\partial b_i} = -\sum_i^N (y_i - \widehat{y}_i)*1.0$$Já que a derivada de $\widehat{y}_i$ em relação a $b_i$ ($\frac{\partial J}{\partial b_i}$) é igual a 1.0. Logo, a atualização dos bias será dada por:
$$b_i = b_i + \lambda(y_i - \widehat{y}_i)$$
In [ ]:
df = pd.read_csv('data/notas.csv')
print(df.shape)
df.head(10)
In [ ]:
x = df[['prova1', 'prova2', 'prova3']].values
y = df['final'].values.reshape(-1, 1)
print(x.shape, y.shape)
In [ ]:
minmax = MinMaxScaler(feature_range=(-1,1))
x = minmax.fit_transform(x.astype(np.float64))
In [ ]:
D = x.shape[1]
w = [2*random() - 1 for i in range(D)]
b = 2*random() - 1
learning_rate = 1e-2
for step in range(2001):
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)
In [ ]:
x = np.array([[0, 0], [0, 1], [1, 0], [1, 1]])
# y = np.array([[0, 1, 1, 1]]).T # porta OR
y = np.array([0, 0, 0, 1]).T # porta AND
print(x.shape, y.shape)
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(101):
cost = 0
for x_n, y_n in zip(x, y):
# qual linha devemos remover para transformar o Perceptron num Adaline?
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 + 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%10 == 0:
print('step {0}: {1}'.format(step, cost))
print('w: ', w)
print('b: ', b)
In [ ]:
D = x.shape[1]
w = 2*np.random.random(size=D)-1
b = 2*np.random.random()-1
learning_rate = 1.0 # <- use a mesma learning rate do python
for step in range(101):
cost = 0
for x_n, y_n in zip(x, y):
# qual linha devemos remover para transformar o Perceptron num Adaline?
y_pred = np.dot(x_n, w) + b
y_pred = np.where(y_pred > 0, 1, 0)
error = y_n - y_pred
w = w + learning_rate*np.dot(error, x_n)
b = b + learning_rate*error
cost += error**2
if step%10 == 0:
print('step {0}: {1}'.format(step, cost))
print('w: ', w)
print('b: ', b)
print('y_pred: {0}'.format(np.dot(x, w)+b))
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.5, 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)
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 a #epochs
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%100 == 0:
print('step {0}: {1}'.format(step, cost))
print('w: ', w)
print('b: ', b)
plot_linear_classifier(x, y, w, b)
In [ ]:
D = x.shape[1]
w = 2*np.random.random(size=D)-1
b = 2*np.random.random()-1
learning_rate = 1.0 # <- use a mesma learning rate do python
for step in range(1): # <- use a mesma #epochs do python
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)
plot_linear_classifier(x, y, w, b)