No notebook Regressão Linear Simples com Descida de Gradiente foi mostrado como funciona a regressão linear para prever uma resposta (preço de uma casa) a partir de uma variável de entrada (tamanho de uma casa).
Entretanto, existem diversos casos nos quais temos duas ou mais variáveis de entrada para prever uma resposta. Para isso precisamos de uma abordagem que estenda a Regressão Linear Simples para suportar múltiplas variáveis de entrada.
Neste notebook, além dos dados dos tamanhos das casas para prever os preços, usaremos também o número de quartos. A seguir temos um gráfico tridimensional mostrando cada casa dos dados de treinamento como um ponto de acordo com as três características (tamanho, número de quartos e preço).
import plotly
import plotly.graph_objs as go
plotly.offline.init_notebook_mode()
sizes = [0.998, 1.500, 1.175, 1.232, 1.121, 0.988, 1.240, 1.501, 1.225, 1.552, 0.975, 1.121, 1.020,
1.501, 1.664, 1.488, 1.376, 1.500, 1.256, 1.690, 1.820, 1.652, 1.777, 1.504, 1.831, 1.200]
bedrooms = [4, 4, 3, 3, 3, 3, 3, 3, 3, 3, 2, 3, 2, 3, 4, 3, 3, 3, 3, 3, 4, 3, 4, 3, 4, 3]
prices = [25.9, 29.5, 27.9, 25.9, 29.9, 29.9, 30.9, 28.9, 35.9, 31.5, 31.0, 30.9, 30.0, 28.9, 36.9,
41.9, 40.5, 43.9, 37.5, 37.9, 44.5, 37.9, 38.9, 36.9, 45.8, 41.0]
plot = go.Scatter3d(x=sizes, y=bedrooms, z=prices, mode='markers')
layout = go.Layout(width=900, height=400,
margin=go.layout.Margin(l=10, r=10, b=10, t=10, pad=10),
scene=dict(xaxis=dict(title="tamanho"), yaxis=dict(title="quartos"), zaxis=dict(title="preço")))
data = [plot]
plotly.offline.iplot({'data': data, 'layout': layout})
A Regressão Linear Múltipla é uma generalização da Regressão Linear Simples, que podemos usar para qualquer quantidade de variáveis.
Diferente da Regressão Linear Simples, na qual foi gerada uma reta para prever o valor de uma casa de acordo com um tamanho, na Regressão Linear Múltipla, como temos três ou mais dimensões, geraremos um plano (ou hiperplano).
Para entender os próximos passos definiremos algumas notações.
Cada característica de um exemplo dos dados de treinamento será representada por $x_i$. Nesse caso, $x_1$ é o tamanho da casa e $x_2$ é a quantidade de quartos. Na regressão múltipla podemos ter $N$ características.
Com isso, a função que representa o plano para esse exemplo é:
\begin{equation*} f(x) = w_0 + w_1x_1 + w_2x_2 \end{equation*}Generalizando a função para exemplos com $N$ características, temos:
\begin{equation*} f(x) = w_0 + w_1x_1 + ... + w_nx_n \end{equation*}O vetor $x$ vai representar todas as características de uma casa, mas com uma ressalva, ele terá $N+1$ elementos, sendo o primeiro sempre $x_0 = 1$. Isto nos ajudará em cálculos posteriores. Assim:
$x = \begin{bmatrix} x_0 \\ x_1 \\ ... \\ x_n \end{bmatrix}$
Além disso, temos o vetor $w$ que contém todos os coeficientes da função do plano.
$w = \begin{bmatrix} w_0 \\ w_1 \\ ... \\ w_n \end{bmatrix}$
Representando os dados das casas e os coeficientes como vetores, podemos simplificar a função escrevendo da seguinte forma:
$f(x) = w^Tx$
Onde $w^T$ é a matriz transposta de $w$.
Efetuando a multiplicação $w^Tx$ temos: $w_0x_0 + w_1x_1 + ... + w_nx_n$
Abaixo temos a implementação para calcular o resultado da função de acordo com os vetores $x$ e $w$.
def f(x, w):
result = 0
for i in range(len(x)):
result += x[i]*w[i]
return result
print(f([1,3,-1], [1,1,2]))
2
Além disso, precisamos especificar a qual exemplo pertence uma característica. Para isso usaremos a notação $x_j^{(i)}$ para representar a característica $x_j$ da i-ésima casa. Por exemplo, o tamanho da quinta casa nos dados de treinamento é representado por $x_1^{(5)}$, já a quantidade de quartos da terceira casa é representada por $x_2^{(3)}$. Dessa forma, o vetor de características da i-ésima casa será representado por $x^{(i)}$.
Como na Regressão Linear Simples, a função resultante ideal na Regressão Linear Múltipla é a que minimiza a função de erro médio quadrático $\frac{1}{2m} \sum_{i=1}^{m} (f(x^{(i)}) - y^{(i)})^2$, onde $m$ é a quantidade de exemplos nos dados de treinamento.
Escrevendo ela em função de $w$, temos:
\begin{equation*} J(w) = \frac{1}{2m} \sum_{i=1}^{m} (w^Tx^{(i)} - y^{(i)})^2 \end{equation*}O código para esse cálculo pode ser visto abaixo. Na função implementada são passados todos os dados dos tamanhos, números de quartos e preços das casas, além do vetor $w$ com os coeficientes da função.
def squared_error(sizes, bedrooms, prices, w):
total = 0
for i in range(len(sizes)):
x1, x2, y = sizes[i], bedrooms[i], prices[i]
yi = f([x1, x2], w)
error = (yi - y)**2
total += error
return total/(2*len(sizes))
Novamente, para encontrar o valor mínimo de $J(w)$ utilizaremos o algoritmo da descida de gradiente.
Na Regressão Linear Simples tínhamos duas regras para atualizar $w_0$ e $w_1$ utilizando derivadas parciais. Entretanto, podemos generalizar o cálculo das derivadas parciais para qualquer quantidade de coeficientes.
\begin{equation*} \frac{\partial}{\partial w_j} J(w) = \frac{1}{m} \sum_{i=1}^{m} (f(x^{(i)}) - y^{(i)})x_j^{(i)} \end{equation*}Como convencionamos que $x_0^{(i)}$ é sempre igual a 1, então $\frac{\partial}{\partial w_0} J(w)$ vai ser igual ao que calculamos na Regressão Linear Simples, assim como $\frac{\partial}{\partial w_1} J(w)$.
As derivadas podem ser calculadas usando o código a seguir. Na função são passados todos os dados dos tamanhos, números de quartos e preços das casas, o vetor $w$ e o índice $j$ para especificar qual derivada parcial está sendo calculada.
def d_error(sizes, bedrooms, prices, w, j_index):
total = 0
for i in range(len(sizes)):
x1, x2, y = sizes[i], bedrooms[i], prices[i]
xij = [1, sizes[i], bedrooms[i]][j_index]
yi = f([x1, x2], w)
error = (yi - y)
total += error*xij
return total/(len(sizes))
Com isso, podemos resumir a atualização dos pesos nas iterações do algoritmo de descida de gradiente para Regressão Múltipla assim:
\begin{equation*} w_j := w_j - \alpha \frac{\partial}{\partial w_j} J(w) \end{equation*}ou seja
\begin{equation*} w_j := w_j - \alpha \frac{1}{m} \sum_{i=1}^{m} (f(x^{(i)}) - y^{(i)})x_j^{(i)} \end{equation*}onde $j$ varia de 0 até $N$.
Vamos agora fazer atualizações dos coeficientes até o erro médio quadrático não diminuir pelo menos 0.000001.
import random
w0 = random.uniform(-2, 2)
w1 = random.uniform(-2, 2)
w2 = random.uniform(-2, 2)
alpha = 0.01
coefficients = [(w0, w1, w2)]
errors = [squared_error(sizes, bedrooms, prices, coefficients[-1])]
while True:
w = coefficients[-1]
w0i = w0 - alpha*d_error(sizes, bedrooms, prices, w, 0)
w1i = w1 - alpha*d_error(sizes, bedrooms, prices, w, 1)
w2i = w2 - alpha*d_error(sizes, bedrooms, prices, w, 2)
w0 = w0i
w1 = w1i
w2 = w2i
coefficients.append((w0, w1, w2))
errors.append(squared_error(sizes, bedrooms, prices, coefficients[-1]))
if (abs(errors[-2] - errors[-1]) < 0.000001):
break
print("Mean squared error: {}".format(squared_error(sizes, bedrooms, prices, coefficients[-1])))
print("f(x) = {}x_2 + {}x_1 + {}".format(w2, w1, w0))
Mean squared error: 14.08053985116923 f(x) = -131.7521267287713x_2 + 2.0491315215998336x_1 + 20.167629801043585
Usando a função encontrada, vamos visualizar o plano que ela representa:
import numpy as np
plot = go.Scatter3d(x=sizes, y=bedrooms, z=prices, mode='markers',
marker=dict(size=12, line=dict(color='rgba(217, 217, 217, 0.14)', width=0.5),
opacity=0.8))
x = np.linspace(1, 2, 50)
y = np.linspace(2, 4, 50)
z = []
for i in range(len(x)):
z.append([])
for j in range(len(y)):
z[-1].append(f([x[i], y[j]], coefficients[-1]))
plane = go.Surface(x=x, y=y, z=z, showscale=False)
camera = dict(
up=dict(x=0, y=0, z=0),
center=dict(x=0, y=0, z=0),
eye=dict(x=2, y=-2, z=0.5)
)
layout = go.Layout(width=900, height=400,
margin=go.layout.Margin(l=10, r=10, b=10, t=10, pad=10),
scene=dict(xaxis=dict(title="tamanho"), yaxis=dict(title="quartos"), zaxis=dict(title="preço")))
data = [plot, plane]
fig = go.Figure(data=data, layout=layout)
fig['layout'].update(scene=dict(camera=camera))
plotly.offline.iplot(fig)