Regressão Logística

Introdução

Na aprendizagem de máquina supervisionada, quando a resposta que precisa ser prevista é qualitativa (discreta), é usado o processo chamado de Classificação. São problemas de classificação, por exemplo, decidir se um e-mail recebido é spam ou não, ou decidir se uma foto contém a imagem de um gato, cachorro ou um pássaro.

Para funcionar, um classificador precisa ser treinado usando dados de treinamento. Cada exemplo nos dados de treinamento possui um vetor de características $(x_1, x_2, ..., x_n)$ e uma resposta associada $y$ que é a classificação do exemplo.

Apesar do nome, Regressão Logística é um algoritmo de classificação que pode ser usado para resolver problemas com duas ou mais possíveis classes. Entretanto, para facilitar o entendimento, consideraremos que $y$ pode ter apenas dois valores, 0 ou 1, sendo 0 uma classe e 1 a outra.

Função logística

Com as respostas variando apenas entre dois valores, 0 e 1, usar regressão linear não é adequado, pois ela pode prever qualquer valor contínuo que pode ir muito além dos limites de 0 e 1. Assim, mudaremos a função $f(x)$, que representa a hipótese para explicar os dados, para uma função que varie apenas entre 0 e 1.

Na regressão logística usaremos a função logística da seguinte forma:

$$f(x) = g(w^Tx)$$$$g(z) = \frac{1}{1+e^{-z}}$$

Perceba que continua-se usando $w^Tx$ como na regressão linear, mas agora aplica-se o resultado na função logística $g(z)$.

A seguir temos a implementação das duas funções acima, $f(x)$ e $g(z)$. $f(x)$ recebe o vetor de coeficientes $w$ e as características $x$, já $g(z)$ recebe apenas um valor que é aplicado a função logística.

In [1]:
import math

def f(x, w):
    z = 0

    for i in range(len(x)):
        z += x[i]*w[i]
    
    return g(z)

def g(z):
    return 1 / (1 + math.exp(-z))

A função logística tem uma forma de "S" como pode ser visto no gráfico seguir:

In [2]:
import numpy as np
from bokeh.io import show, output_notebook
from bokeh.plotting import figure
output_notebook()

x_points = np.linspace(-10, 10, 100)
y_points = [g(i) for i in x_points]

p = figure(plot_width=700, plot_height=250)
p.xaxis.axis_label = "z"
p.yaxis.axis_label = "g(z)"
p.line(x_points, y_points, line_width=2)
show(p, notebook_handle=True)
Loading BokehJS ...
Out[2]:

<Bokeh Notebook handle for In[2]>

Note que quanto menor o valor de $z$, ou seja, o valor de $w^Tx$, mais próximo de zero é o resultado da função. Por outro lado, quanto maior o valor, mais próximo de 1 é o resultado. Dessa forma, o resultado nunca ultrapassará o intervalo entre 0 e 1.

Na regressão logística, interpretamos o resultado de $f(x)$ como a probabilidade da resposta ser 1 para o vetor de características $x$. Assim, se $f(x) = 0,8$, então $x$ tem probabilidade $0,8$ de ser da classe 1, e $0,2$ de ser da classe 0.

Fronteira de decisão

Para obter uma classificação discreta, é necessário definir um limiar em relação à probabilidade. Por exemplo, se $f(x) \geq 0,5 \Rightarrow y = 1$. Ou seja, se a probabilidade de ser 1 for maior ou igual 50%, então classificamos como 1. Adicionalmente, se $f(x) < 0,5 \Rightarrow y = 0$.

Na função logística $g(z)$, se $z \geq 0$, então $g(z) \geq 0,5$. Como a hipótese na regressão logística é $f(x) = g(w^Tx)$, então, se $w^Tx \geq 0$, isto resultará em $g(z) \geq 0,5$.

Com isso, $w^Tx$ especifica uma fronteira de decisão que separa os pontos que são classificados como 0 ou 1. Definimos matematicamente a fronteira de decisão da seguinte forma:

$$w^Tx = 0 $$

Assim,

$$w^Tx \geq 0 \Rightarrow y = 1$$$$w^Tx < 0 \Rightarrow y = 0$$

Dependendo do tipo de função definida por $w^Tx$ a fronteira de decisão pode ter diferentes formas.

Por exemplo, usando a função $f(x) = w_0 + w_1x_1 + w_2x_2$, definimos a fronteira como:

$$w_0 + w_1x_1 + w_2x_2 = 0$$

Essa equação define uma reta.

Abaixo temos o gráfico mostrando a fronteira usando a equação da reta anterior para $w^T = \begin{bmatrix} -3 & 0,8 & 1 \end{bmatrix}$. Ou seja, a equação é: $0,8x_1 + x_2 - 3 = 0$.

Note que no gráfico, os pontos são separados em dois grupos pela reta, sendo os de cor azul correspondentes a $y=0$ e os de cor vermelha a $y=1$.

In [3]:
def boundary(x):
    return 3-0.8*x

x = [0.5, 1, 1.2, 1.1, 1.5, 2.6, 2.3, 3, 2.8, 2.4]
y = [0.7, 1.2, 0.5, 1.8, 1, 2.5, 2.8, 2.5, 3, 2.2]
colors = ["blue", "blue", "blue", "blue", "blue", "red", "red", "red", "red", "red"]

p = figure(plot_width=700, plot_height=250)
p.circle(x, y, radius=0.025, color=colors)
p.line([0, 3.5], [boundary(0), boundary(3.5)], line_width=2)
show(p, notebook_handle=True)
Out[3]:

<Bokeh Notebook handle for In[3]>

Usando funções diferentes, podemos ter curvas mais complexas. No exemplo a seguir usamos a função $f(x) = -1,2 + 3x_1^2 + 3x_2^2$, criando a fronteira $3x_1^2 + 3x_2^2 - 1,2 = 0$. Esta equação define uma elipse.

In [4]:
import math
import itertools

def boundary(x1):
    return math.sqrt((1.2-3*x1**2)/0.3)

x = [0.5, -0.1, 0.2, -0.3, -0.3, 0, 0.7, 1, 0.8, 0, -0.5, -0.8, -0.5]
y = [0.2, -0.8, 0.5, -0.1, 0.7, 3.5, 2.0, 0.5, -1.5, -3.5, -2, 0, 2]
colors = ["blue", "blue", "blue", "blue", "blue", "red", "red", "red", "red", "red", "red", "red", "red"]

curve_x_points = np.linspace(-0.8, 0.8, 100000)
curve_x_points = [i for i in curve_x_points if ((1.2-3*i**2)/0.3) >= 0]

curve_y_points = [boundary(i) for i in curve_x_points]
curve_neg_y_points = [-boundary(i) for i in curve_x_points]

p = figure(plot_width=700, plot_height=250)
p.circle(x, y, radius=0.015, color=colors)

p.line(curve_x_points, curve_y_points, line_width=2)
p.line(curve_x_points, curve_neg_y_points, line_width=2)
show(p, notebook_handle=True)