Sumário

Imports e Configurações


In [ ]:
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from sklearn.linear_model import LogisticRegression
from sklearn.preprocessing import MinMaxScaler
from sklearn.metrics import accuracy_score
from mpl_toolkits.mplot3d import Axes3D

%matplotlib inline

Introdução

A Regressão Logística, apesar do nome, é uma técnica utilizada para fazer classificação binária. Nesse caso, ao invés de prever um valor contínuo, a nossa saída é composta de apenas dois valores: 0 ou 1, em geral. Para fazer a regressão logística, utilizamos como função de ativação a função conhecida como sigmoid. Tal função, é descrita pela seguinte fórmula:

$$\widehat{y} = \frac{1}{1+e^{-z}} = \frac{e^z}{1+e^z}$$

No caso de redes neurais, em geral consideramos $z(w,b) = xw^T + b$.

Função de Custo

A função de custo da regressão logística é chamada de entropia cruzada (do inglês, cross-entropy) e é definida pela seguinte fórmula:

$$J(z) = -\frac{1}{N}\sum_{i}^N y_i\log(\widehat{y}_i) + (1-y_i)\log(1-\widehat{y}_i)$$

Onde $N$ é quantidade de amostras e $y_i$ representa o valor da $i$-ésima amostra (0 ou 1). Lembrando que $\widehat{y}_i$ é agora calculada agora utilizando a função sigmoid, como mostrado na seção anterior.

Repare também que:

  • quando $y_i = 0$, o primeiro termo anula-se (pois $y_i = 0$). Logo, vamos considerar os dois casos extremos para $\widehat{y}_i = 0$ no segundo termo da equação ($(1-y_i)\log(1-\widehat{y}_i)$):
    • quando $\widehat{y}_i = 0$, temos que o $\log(1-\widehat{y}_i) = \log(1) = 0$. Logo, o nosso custo $J = 0$. Repare que isso faz todo sentido, pois $y_i = 0$ e $\widehat{y}_i = 0$.
    • quando $\widehat{y}_i = 1$, temos que o $\log(1-\widehat{y}_i) = \log(0) = \infty$. Agora, o nosso custo $J = \infty$. Ou seja, quanto mais diferente são $y_i$ e $\widehat{y}_i$, maior o nosso custo.
  • quando $y_i = 1$, o segundo termo anula-se (pois $(1-y_i) = 0$). Novamente, vamos considerar os dois casos extremos para $\widehat{y}_i = 0$, só que agora no primeiro termo da equação ($y_i\log(\widehat{y}_i)$):
    • quando $\widehat{y}_i = 0$, temos que o $\log(\widehat{y}_i) = \infty$. Logo, o nosso custo $J = \infty$. Novamente, como $y_i$ e $\widehat{y}_i$ são bem diferentes, o custo tende a aumentar.
    • quando $\widehat{y}_i = 1$, temos que $\log(\widehat{y}_i) = \log(1) = 0$. Agora, o nosso custo $J = 0$. Novamente, isso faz todo sentido, pois $y_i = 1$ e $\widehat{y}_i = 1$.

Derivada da Cross-Entropy

Para calcular a derivada da nossa função de custo $J(z)$, primeiramente vamos calcular $\log(\widehat{y}_i)$:

$$\log(\widehat{y}_i) = log\frac{1}{1+e^{-z}} = log(1) - log(1+e^{-z}) = -log(1+e^{-z})$$

E $\log(1-\widehat{y}_i)$:

$$\log(1-\widehat{y}_i) = log \left(1-\frac{1}{1+e^{-z}}\right) = log(e^{-z}) - log(1+e^{-z}) = -z -log(1+e^{-z})$$

Substituindo as duas equações anteriores na fórmula da função de custo, temos:

$$J(z) = -\frac{1}{N}\sum_{i}^N \left[-y_i\log(1+e^{-z}) + (1-y_i)(-z -\log(1+e^{-z}))\right]$$

Efetuando as distribuições, podemos simplificar a equação acima para:

$$J(z) = -\frac{1}{N}\sum_{i}^N \left[y_iz -z -\log(1+e^{-z})\right]$$

Uma vez que:

$$-z -\log(1+e^{-z}) = -\left[\log e^{z} + log(1+e^{-z})\right] = -log(1+e^z)$$

Temos:

$$J(z) = -\frac{1}{N}\sum_{i}^N \left[y_iz -\log(1+e^z)\right]$$

Como a derivada da diferença é igual a diferença das derivadas, podemos calcular cada derivada individualmente em relação a $w$:

$$\frac{\partial}{\partial w_i}y_iz = y_ix_i,\quad \frac{\partial}{\partial w_i}\log(1+e^z) = \frac{x_ie^z}{1+e^z} = x_i \widehat{y}_i$$

e em relação à $b$:

$$\frac{\partial}{\partial b}y_iz = y_i,\quad \frac{\partial}{\partial b}\log(1+e^z) = \frac{e^z}{1+e^z} = \widehat{y}_i$$

Assim, a derivada da nossa função de custo $J(z)$ é:

$$\frac{\partial}{\partial w_i}J(z) = \sum_i^N (y_i - \widehat{y}_i)x_i$$$$\frac{\partial}{\partial b}J(z) = \sum_i^N (y_i - \widehat{y}_i)$$

Por fim, repare que o gradiente de J ($\nabla J$) é exatamente o mesmo que o gradiente da função de custo do Perceptron Linear. Portanto, os pesos serão atualizados da mesma maneira. O que muda é a forma como calculamos $\widehat{y}$ (agora usando a função sigmoid) e a função de custo $J$.

Regressão Logística


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

In [ ]:
x, y = df.idade.values.reshape(-1,1), df.comprou.values.reshape(-1,1)

print(x.shape, y.shape)

In [ ]:
plt.scatter(x, y, c=y, cmap='bwr')
plt.xlabel('idade')
plt.ylabel('comprou?')

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

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

Vamos utilizar o sklearn como gabarito para nossa implementação. Entretanto, como a Regressão Logística do sklearn faz uma regularização L2 automaticamente, temos de definir $C=10^{15}$ para "anular" a regularização. O parâmetro $C$ define a inversa da força da regularização (ver documentação). Logo, quanto menor for o $C$, maior será a regularização e menores serão os valores dos pesos e bias.


In [ ]:
clf_sk = LogisticRegression(C=1e15)
clf_sk.fit(x, y.ravel())

print(clf_sk.coef_, clf_sk.intercept_)
print(clf_sk.score(x, y))

In [ ]:
x_test = np.linspace(x.min(), x.max(), 100).reshape(-1,1)
y_sk = clf_sk.predict_proba(x_test)

plt.scatter(x, y, c=y, cmap='bwr')
plt.plot(x_test, y_sk[:,1], color='black')
plt.xlabel('idade')
plt.ylabel('comprou?')

In [ ]:
# implemente a função sigmoid aqui

Numpy


In [ ]:
# implemente o neurônio sigmoid aqui

In [ ]:
x_test = np.linspace(x.min(), x.max(), 100).reshape(-1,1)
y_sk = clf_sk.predict_proba(x_test)
y_pred = sigmoid(np.dot(x_test, w.T) + b)

plt.scatter(x, y, c=y, cmap='bwr')
plt.plot(x_test, y_sk[:,1], color='black', linewidth=7.0)
plt.plot(x_test, y_pred, color='yellow')
plt.xlabel('idade')
plt.ylabel('comprou?')

In [ ]:
print('Acurácia pelo Scikit-learn: {:.2f}%'.format(clf_sk.score(x, y)*100))

y_pred = np.round(sigmoid(np.dot(x, w.T) + b))
print('Acurária pela nossa implementação: {:.2f}%'.format(accuracy_score(y, y_pred)*100))

Exercícios


In [ ]:
x, y = df[['idade', 'salario']].values, df.comprou.values.reshape(-1,1)

print(x.shape, y.shape)

In [ ]:
fig = plt.figure(figsize=(8,6))
ax = fig.add_subplot(111, projection='3d')
ax.scatter3D(x[:,0], x[:,1], y, c=y.ravel())

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

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

In [ ]:
clf_sk = LogisticRegression(C=1e15)
clf_sk.fit(x, y.ravel())

print(clf_sk.coef_, clf_sk.intercept_)
print(clf_sk.score(x, y))

Numpy


In [ ]:
D = x.shape[1]
w = 2*np.random.random((1, D))-1 # [1x2]
b = 2*np.random.random()-1       # [1x1]

learning_rate = 1.0 # <- tente estimar a learning rate

for step in range(1): # <- tente estimar a #epochs
    # calcule a saida do neuronio sigmoid
    z = 
    y_pred =
    
    error = y - y_pred     # [400x1]
    
    w = w + learning_rate*np.dot(error.T, x)
    b = b + learning_rate*error.sum()
    
    if step%100 == 0:
        # implemente a entropia cruzada (1 linhas)
        cost = 
        print('step {0}: {1}'.format(step, cost))

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

In [ ]:
x1 = np.linspace(x[:, 0].min(), x[:, 0].max())
x2 = np.linspace(x[:, 1].min(), x[:, 1].max())
x1_mesh, x2_mesh = np.meshgrid(x1, x2)
x1_mesh = x1_mesh.reshape(-1, 1)
x2_mesh = x2_mesh.reshape(-1, 1)

x_mesh = np.hstack((x1_mesh, x2_mesh))
y_pred = sigmoid(np.dot(x_mesh, w.T) + b)

fig = plt.figure(figsize=(8, 6))
ax = fig.add_subplot(111, projection='3d')
ax.scatter3D(x[:,0], x[:,1], y, c=y.ravel())
ax.plot_trisurf(x1_mesh.ravel(), x2_mesh.ravel(), y_pred.ravel(), alpha=0.3, shade=False)

In [ ]:
print('Acurácia pelo Scikit-learn: {:.2f}%'.format(clf_sk.score(x, y)*100))

y_pred = np.round(sigmoid(np.dot(x, w.T) + b))
print('Acurária pela nossa implementação: {:.2f}%'.format(accuracy_score(y, y_pred)*100))

Referências