Phase 02 - Lesson 02
Regressão Linear
This lesson includes a graded coding exercise that runs in your browser, unlocked with lifetime access.
A regressão linear desenha a melhor linha reta através dos seus dados. É o “olá mundo” do aprendizado de máquina.
Tipo: Construir Idiomas: Python Pré-requisitos: Fase 1 (Álgebra Linear, Cálculo, Otimização), Fase 2 Lição 1 Tempo: ~90 minutos
Objetivos de aprendizagem
- Derive as regras de atualização de gradiente descendente para erro quadrático médio e implemente a regressão linear do zero
- Compare a descida do gradiente e a equação normal em termos de complexidade computacional e quando usar cada uma
- Construir um modelo de regressão linear múltipla com padronização de recursos e interpretar os pesos aprendidos
- Explique como a regressão Ridge (regularização L2) evita o overfitting ao penalizar pesos grandes
O problema
Você tem dados: tamanhos de casas e seus preços de venda. Você deseja prever o preço de uma casa nova de acordo com seu tamanho. Você poderia observar isso em um gráfico de dispersão, mas precisa de uma fórmula. Você precisa de uma linha que melhor se ajuste aos dados para poder inserir qualquer tamanho e obter uma previsão de preço.
A regressão linear fornece essa linha. Mais importante ainda, apresenta todo o ciclo de treinamento de ML: definir um modelo, definir uma função de custo, otimizar os parâmetros. Todo algoritmo de ML segue esse mesmo padrão. Domine-o aqui com o caso mais simples e você o reconhecerá em todos os lugares.
Isto não se aplica apenas a problemas simples. A regressão linear é usada em sistemas de produção para previsão de demanda, análise de testes A/B, modelagem financeira e como linha de base para todas as tarefas de regressão.
O Conceito
O Modelo
A regressão linear assume uma relação linear entre entrada (x) e saída (y):
y = wx + b
w(peso/inclinação): quanto y muda quando x aumenta em 1b(viés/interceptação): o valor de y quando x = 0
Para múltiplas entradas (recursos), isso se estende a:
y = w1*x1 + w2*x2 + ... + wn*xn + b
Ou em forma vetorial: y = w^T * x + b
O objetivo: encontrar os valores de w e b que tornem o y previsto o mais próximo possível do y real em todos os exemplos de treinamento.
A função de custo (erro quadrático médio)
Como você mede "o mais próximo possível"? Você precisa de um único número que capture o quão erradas estão suas previsões. A escolha mais comum é o erro quadrático médio (MSE):
MSE = (1/n) * sum((y_predicted - y_actual)^2)
Por que ao quadrado? Duas razões. Primeiro, penaliza mais erros grandes do que erros pequenos (um erro de 10 é 100x pior do que um erro de 1, não 10x). Em segundo lugar, a função quadrada é suave e diferenciável em todos os lugares, o que torna a otimização simples.
A função de custo cria uma superfície. Para um único peso w e tendência b, a superfície MSE se parece com uma tigela (um parabolóide convexo). A parte inferior da tigela é onde o MSE é minimizado. Treinar significa encontrar esse fundo.
Gradiente Descendente
A descida do gradiente encontra o fundo da tigela descendo degraus.
flowchart TD
A[Initialize w and b randomly] --> B[Compute predictions: y_hat = wx + b]
B --> C[Compute cost: MSE]
C --> D[Compute gradients: dMSE/dw, dMSE/db]
D --> E[Update parameters]
E --> F{Cost low enough?}
F -->|No| B
F -->|Yes| G[Done: optimal w and b found]
Os gradientes informam duas coisas: em qual direção mover cada parâmetro e quanto mover.
Para MSE com y_hat = wx + b:
dMSE/dw = (2/n) * sum((y_hat - y) * x)
dMSE/db = (2/n) * sum(y_hat - y)
A regra de atualização:
w = w - learning_rate * dMSE/dw
b = b - learning_rate * dMSE/db
A taxa de aprendizagem controla o tamanho do passo. Muito grande: você ultrapassa o mínimo e diverge. Muito pequeno: o treinamento leva uma eternidade. Valores iniciais típicos: 0,01, 0,001 ou 0,0001.
A equação normal (solução de formato fechado)
Especificamente para regressão linear, existe uma fórmula direta que fornece os pesos ideais sem qualquer iteração:
w = (X^T * X)^(-1) * X^T * y
Isso inverte uma matriz para resolver w em uma única etapa. Funciona perfeitamente para pequenos conjuntos de dados. Para grandes conjuntos de dados (milhões de linhas ou milhares de recursos), a descida do gradiente é preferida porque a inversão da matriz é O(n^3) no número de recursos.
Regressão Linear Múltipla
Com múltiplos recursos, o modelo se torna:
y = w1*x1 + w2*x2 + ... + wn*xn + b
Tudo funciona da mesma forma: MSE é a função de custo, a descida do gradiente atualiza todos os pesos simultaneamente. A única diferença é que você está ajustando um hiperplano em vez de uma linha.
O dimensionamento de recursos é importante aqui. Se um recurso variar de 0 a 1 e outro variar de 0 a 1.000.000, a descida do gradiente terá dificuldades porque a superfície de custo se torna alongada. Padronize os recursos (subtraia a média, divida pelo desvio padrão) antes do treino.
Regressão Polinomial
E se a relação não for linear? Você ainda pode usar a regressão linear criando recursos polinomiais:
y = w1*x + w2*x^2 + w3*x^3 + b
Esta ainda é uma regressão “linear” porque o modelo é linear nos pesos (w1, w2, w3). Você está apenas usando recursos não lineares de x.
Polinômios de grau superior podem ajustar curvas mais complexas, mas correm o risco de overfitting. Um polinômio de grau 10 passará por todos os pontos em um conjunto de dados de 10 pontos, mas fará uma previsão ruim em novos dados.
Pontuação R-Quadrado
MSE informa o quão errado você está, mas o número depende da escala de y. R-quadrado (R ^ 2) fornece uma medida independente de escala:
R^2 = 1 - (sum of squared residuals) / (sum of squared deviations from mean)
= 1 - SS_res / SS_tot
- R ^ 2 = 1,0: previsões perfeitas
- R ^ 2 = 0,0: o modelo não é melhor do que prever a média todas as vezes
- R ^ 2 <0,0: o modelo é pior do que prever a média
Visualização da regularização (Ridge Regressão)
Quando você tem muitos recursos, o modelo pode se ajustar demais atribuindo pesos grandes. Ridge regressão (regularização L2) adiciona uma penalidade:
Cost = MSE + lambda * sum(w_i^2)
O prazo de penalidade desencoraja pesos grandes. O hiperparâmetro lambda controla a compensação: lambda mais alto significa pesos menores e mais regularização. Isso será abordado em profundidade em uma lição posterior. Por enquanto, saiba que ele existe e por que ajuda.
Construa
Etapa 1: gerar dados de amostra
import random
import math
random.seed(42)
TRUE_W = 3.0
TRUE_B = 7.0
N_SAMPLES = 100
X = [random.uniform(0, 10) for _ in range(N_SAMPLES)]
y = [TRUE_W * x + TRUE_B + random.gauss(0, 2.0) for x in X]
print(f"Generated {N_SAMPLES} samples")
print(f"True relationship: y = {TRUE_W}x + {TRUE_B} (+ noise)")
print(f"First 5 points: {[(round(X[i], 2), round(y[i], 2)) for i in range(5)]}")
Etapa 2: Regressão linear do zero com descida do gradiente
class LinearRegression:
def __init__(self, learning_rate=0.01):
self.w = 0.0
self.b = 0.0
self.lr = learning_rate
self.cost_history = []
def predict(self, X):
return [self.w * x + self.b for x in X]
def compute_cost(self, X, y):
predictions = self.predict(X)
n = len(y)
cost = sum((pred - actual) ** 2 for pred, actual in zip(predictions, y)) / n
return cost
def compute_gradients(self, X, y):
predictions = self.predict(X)
n = len(y)
dw = (2 / n) * sum((pred - actual) * x for pred, actual, x in zip(predictions, y, X))
db = (2 / n) * sum(pred - actual for pred, actual in zip(predictions, y))
return dw, db
def fit(self, X, y, epochs=1000, print_every=200):
for epoch in range(epochs):
dw, db = self.compute_gradients(X, y)
self.w -= self.lr * dw
self.b -= self.lr * db
cost = self.compute_cost(X, y)
self.cost_history.append(cost)
if epoch % print_every == 0:
print(f" Epoch {epoch:4d} | Cost: {cost:.4f} | w: {self.w:.4f} | b: {self.b:.4f}")
return self
def r_squared(self, X, y):
predictions = self.predict(X)
y_mean = sum(y) / len(y)
ss_res = sum((actual - pred) ** 2 for actual, pred in zip(y, predictions))
ss_tot = sum((actual - y_mean) ** 2 for actual in y)
return 1 - (ss_res / ss_tot)
print("=== Training Linear Regression (Gradient Descent) ===")
model = LinearRegression(learning_rate=0.005)
model.fit(X, y, epochs=1000, print_every=200)
print(f"\nLearned: y = {model.w:.4f}x + {model.b:.4f}")
print(f"True: y = {TRUE_W}x + {TRUE_B}")
print(f"R-squared: {model.r_squared(X, y):.4f}")
Etapa 3: Equação normal (solução de forma fechada)
class LinearRegressionNormal:
def __init__(self):
self.w = 0.0
self.b = 0.0
def fit(self, X, y):
n = len(X)
x_mean = sum(X) / n
y_mean = sum(y) / n
numerator = sum((X[i] - x_mean) * (y[i] - y_mean) for i in range(n))
denominator = sum((X[i] - x_mean) ** 2 for i in range(n))
self.w = numerator / denominator
self.b = y_mean - self.w * x_mean
return self
def predict(self, X):
return [self.w * x + self.b for x in X]
def r_squared(self, X, y):
predictions = self.predict(X)
y_mean = sum(y) / len(y)
ss_res = sum((actual - pred) ** 2 for actual, pred in zip(y, predictions))
ss_tot = sum((actual - y_mean) ** 2 for actual in y)
return 1 - (ss_res / ss_tot)
print("\n=== Normal Equation (Closed-Form) ===")
model_normal = LinearRegressionNormal()
model_normal.fit(X, y)
print(f"Learned: y = {model_normal.w:.4f}x + {model_normal.b:.4f}")
print(f"R-squared: {model_normal.r_squared(X, y):.4f}")
Etapa 4: Regressão linear múltipla
class MultipleLinearRegression:
def __init__(self, n_features, learning_rate=0.01):
self.weights = [0.0] * n_features
self.bias = 0.0
self.lr = learning_rate
self.cost_history = []
def predict_single(self, x):
return sum(w * xi for w, xi in zip(self.weights, x)) + self.bias
def predict(self, X):
return [self.predict_single(x) for x in X]
def compute_cost(self, X, y):
predictions = self.predict(X)
n = len(y)
return sum((pred - actual) ** 2 for pred, actual in zip(predictions, y)) / n
def fit(self, X, y, epochs=1000, print_every=200):
n = len(y)
n_features = len(X[0])
for epoch in range(epochs):
predictions = self.predict(X)
errors = [pred - actual for pred, actual in zip(predictions, y)]
for j in range(n_features):
grad = (2 / n) * sum(errors[i] * X[i][j] for i in range(n))
self.weights[j] -= self.lr * grad
grad_b = (2 / n) * sum(errors)
self.bias -= self.lr * grad_b
cost = self.compute_cost(X, y)
self.cost_history.append(cost)
if epoch % print_every == 0:
print(f" Epoch {epoch:4d} | Cost: {cost:.4f}")
return self
def r_squared(self, X, y):
predictions = self.predict(X)
y_mean = sum(y) / len(y)
ss_res = sum((actual - pred) ** 2 for actual, pred in zip(y, predictions))
ss_tot = sum((actual - y_mean) ** 2 for actual in y)
return 1 - (ss_res / ss_tot)
random.seed(42)
N = 100
X_multi = []
y_multi = []
for _ in range(N):
size = random.uniform(500, 3000)
bedrooms = random.randint(1, 5)
age = random.uniform(0, 50)
price = 50 * size + 10000 * bedrooms - 1000 * age + 50000 + random.gauss(0, 20000)
X_multi.append([size, bedrooms, age])
y_multi.append(price)
def standardize(X):
n_features = len(X[0])
means = [sum(X[i][j] for i in range(len(X))) / len(X) for j in range(n_features)]
stds = []
for j in range(n_features):
variance = sum((X[i][j] - means[j]) ** 2 for i in range(len(X))) / len(X)
stds.append(variance ** 0.5)
X_scaled = []
for i in range(len(X)):
row = [(X[i][j] - means[j]) / stds[j] if stds[j] > 0 else 0 for j in range(n_features)]
X_scaled.append(row)
return X_scaled, means, stds
y_mean_val = sum(y_multi) / len(y_multi)
y_std_val = (sum((yi - y_mean_val) ** 2 for yi in y_multi) / len(y_multi)) ** 0.5
y_scaled = [(yi - y_mean_val) / y_std_val for yi in y_multi]
X_scaled, x_means, x_stds = standardize(X_multi)
print("\n=== Multiple Linear Regression (3 features) ===")
print("Features: house size, bedrooms, age")
multi_model = MultipleLinearRegression(n_features=3, learning_rate=0.01)
multi_model.fit(X_scaled, y_scaled, epochs=1000, print_every=200)
print(f"\nWeights (standardized): {[round(w, 4) for w in multi_model.weights]}")
print(f"Bias (standardized): {multi_model.bias:.4f}")
print(f"R-squared: {multi_model.r_squared(X_scaled, y_scaled):.4f}")
Etapa 5: regressão polinomial
class PolynomialRegression:
def __init__(self, degree, learning_rate=0.01):
self.degree = degree
self.weights = [0.0] * degree
self.bias = 0.0
self.lr = learning_rate
def make_features(self, X):
return [[x ** (d + 1) for d in range(self.degree)] for x in X]
def predict(self, X):
features = self.make_features(X)
return [sum(w * f for w, f in zip(self.weights, row)) + self.bias for row in features]
def fit(self, X, y, epochs=1000, print_every=200):
features = self.make_features(X)
n = len(y)
for epoch in range(epochs):
predictions = [sum(w * f for w, f in zip(self.weights, row)) + self.bias for row in features]
errors = [pred - actual for pred, actual in zip(predictions, y)]
for j in range(self.degree):
grad = (2 / n) * sum(errors[i] * features[i][j] for i in range(n))
self.weights[j] -= self.lr * grad
grad_b = (2 / n) * sum(errors)
self.bias -= self.lr * grad_b
if epoch % print_every == 0:
cost = sum(e ** 2 for e in errors) / n
print(f" Epoch {epoch:4d} | Cost: {cost:.6f}")
return self
def r_squared(self, X, y):
predictions = self.predict(X)
y_mean = sum(y) / len(y)
ss_res = sum((actual - pred) ** 2 for actual, pred in zip(y, predictions))
ss_tot = sum((actual - y_mean) ** 2 for actual in y)
return 1 - (ss_res / ss_tot)
random.seed(42)
X_poly = [x / 10.0 for x in range(0, 50)]
y_poly = [0.5 * x ** 2 - 2 * x + 3 + random.gauss(0, 1.0) for x in X_poly]
x_max = max(abs(x) for x in X_poly)
X_poly_norm = [x / x_max for x in X_poly]
y_poly_mean = sum(y_poly) / len(y_poly)
y_poly_std = (sum((yi - y_poly_mean) ** 2 for yi in y_poly) / len(y_poly)) ** 0.5
y_poly_norm = [(yi - y_poly_mean) / y_poly_std for yi in y_poly]
print("\n=== Polynomial Regression (degree 2 vs degree 5) ===")
print("True relationship: y = 0.5x^2 - 2x + 3")
print("\nDegree 2:")
poly2 = PolynomialRegression(degree=2, learning_rate=0.1)
poly2.fit(X_poly_norm, y_poly_norm, epochs=2000, print_every=500)
print(f" R-squared: {poly2.r_squared(X_poly_norm, y_poly_norm):.4f}")
print("\nDegree 5:")
poly5 = PolynomialRegression(degree=5, learning_rate=0.1)
poly5.fit(X_poly_norm, y_poly_norm, epochs=2000, print_every=500)
print(f" R-squared: {poly5.r_squared(X_poly_norm, y_poly_norm):.4f}")
print("\nDegree 2 fits the true curve well. Degree 5 fits training data slightly better")
print("but risks overfitting on new data.")
Etapa 6: regressão Ridge (regularização L2)
class RidgeRegression:
def __init__(self, n_features, learning_rate=0.01, alpha=1.0):
self.weights = [0.0] * n_features
self.bias = 0.0
self.lr = learning_rate
self.alpha = alpha
def predict_single(self, x):
return sum(w * xi for w, xi in zip(self.weights, x)) + self.bias
def predict(self, X):
return [self.predict_single(x) for x in X]
def fit(self, X, y, epochs=1000, print_every=200):
n = len(y)
n_features = len(X[0])
for epoch in range(epochs):
predictions = self.predict(X)
errors = [pred - actual for pred, actual in zip(predictions, y)]
mse = sum(e ** 2 for e in errors) / n
reg_term = self.alpha * sum(w ** 2 for w in self.weights)
cost = mse + reg_term
for j in range(n_features):
grad = (2 / n) * sum(errors[i] * X[i][j] for i in range(n))
grad += 2 * self.alpha * self.weights[j]
self.weights[j] -= self.lr * grad
grad_b = (2 / n) * sum(errors)
self.bias -= self.lr * grad_b
if epoch % print_every == 0:
print(f" Epoch {epoch:4d} | Cost: {cost:.4f} | L2 penalty: {reg_term:.4f}")
return self
print("\n=== Ridge Regression (L2 Regularization) ===")
print("Same data as multiple regression, with alpha=0.1")
ridge = RidgeRegression(n_features=3, learning_rate=0.01, alpha=0.1)
ridge.fit(X_scaled, y_scaled, epochs=1000, print_every=200)
print(f"\nRidge weights: {[round(w, 4) for w in ridge.weights]}")
print(f"Plain weights: {[round(w, 4) for w in multi_model.weights]}")
print("Ridge weights are smaller (shrunk toward zero) due to the L2 penalty.")
Use-o
Agora a mesma coisa com scikit-learn, que é o que você realmente usará na produção.
from sklearn.linear_model import LinearRegression as SklearnLR
from sklearn.linear_model import Ridge
from sklearn.preprocessing import PolynomialFeatures, StandardScaler
from sklearn.model_selection import train_test_split
from sklearn.metrics import mean_squared_error, r2_score
import numpy as np
np.random.seed(42)
X_sk = np.random.uniform(0, 10, (100, 1))
y_sk = 3.0 * X_sk.squeeze() + 7.0 + np.random.normal(0, 2.0, 100)
X_train, X_test, y_train, y_test = train_test_split(X_sk, y_sk, test_size=0.2, random_state=42)
lr = SklearnLR()
lr.fit(X_train, y_train)
y_pred = lr.predict(X_test)
print("=== Scikit-learn Linear Regression ===")
print(f"Coefficient (w): {lr.coef_[0]:.4f}")
print(f"Intercept (b): {lr.intercept_:.4f}")
print(f"R-squared (test): {r2_score(y_test, y_pred):.4f}")
print(f"MSE (test): {mean_squared_error(y_test, y_pred):.4f}")
poly = PolynomialFeatures(degree=2, include_bias=False)
X_poly_sk = poly.fit_transform(X_train)
X_poly_test = poly.transform(X_test)
lr_poly = SklearnLR()
lr_poly.fit(X_poly_sk, y_train)
print(f"\nPolynomial degree 2 R-squared: {r2_score(y_test, lr_poly.predict(X_poly_test)):.4f}")
scaler = StandardScaler()
X_train_scaled = scaler.fit_transform(X_train)
X_test_scaled = scaler.transform(X_test)
ridge = Ridge(alpha=1.0)
ridge.fit(X_train_scaled, y_train)
print(f"Ridge R-squared: {r2_score(y_test, ridge.predict(X_test_scaled)):.4f}")
print(f"Ridge coefficient: {ridge.coef_[0]:.4f}")
Sua implementação do zero e scikit-learn produzem os mesmos resultados. A diferença: scikit-learn lida com casos extremos, estabilidade numérica e otimizações de desempenho. Use a biblioteca para produção. Use a versão do zero para entender o que está acontecendo.
Envie
Esta lição produz:
outputs/skill-regression.md- uma habilidade para escolher a abordagem de regressão correta com base no problema
Exercícios
- Implementar descida do gradiente em lote, descida do gradiente estocástica (SGD) e descida do gradiente em minilote. Compare a velocidade de convergência no mesmo conjunto de dados. Qual converge mais rápido? Qual tem a curva de custo mais suave?
- Gere dados a partir de uma função cúbica (y = ax^3 + bx^2 + cx + d + ruído). Ajuste polinômios de grau 1, 3 e 10. Compare o treinamento R ^ 2 e o teste R ^ 2. Em que grau o overfitting se torna óbvio?
- Implementar regressão Lasso (regularização L1: penalidade = alfa * soma(|w_i|)). Treine com base em dados habitacionais multifuncionais. Compare quais pesos vão para zero com Ridge. Por que L1 produz soluções esparsas e L2 não?
Termos-chave
| Prazo | O que as pessoas dizem | O que isso realmente significa |
|---|---|---|
| Regressão linear | "Desenhe uma linha através dos dados" | Encontre o peso w e o viés b que minimizem a soma das diferenças quadradas entre wx+b e os valores reais de y |
| Função custo | “Quão ruim é o modelo” | Uma função que mapeia parâmetros do modelo para um único erro de previsão de medição de número, cuja otimização minimiza |
| Erro quadrático médio | “Média de erros quadráticos” | (1/n) * soma de (previsto - real)^2, penalizando desproporcionalmente grandes erros |
| Descida do gradiente | "Caminhar ladeira abaixo" | Ajustar iterativamente os parâmetros na direção que reduz a função de custo, usando derivadas parciais |
| Taxa de aprendizagem | "Tamanho do passo" | Um escalar que controla quanto os parâmetros mudam por etapa de descida do gradiente |
| Equação normal | "Resolva diretamente" | A solução de forma fechada w = (X^T X)^-1 X^T y que fornece pesos ideais sem iteração |
| R-quadrado | "Quão bom é o ajuste" | A fração de variância em y explicada pelo modelo, variando de infinito negativo a 1,0 |
| Dimensionamento de recursos | "Tornar recursos comparáveis" | Transformando recursos em intervalos semelhantes (por exemplo, média zero, variância unitária) para que a descida do gradiente convirja mais rapidamente |
| Regularização | “Penalizar a complexidade” | Adicionando um termo à função de custo que reduz os pesos, evitando overfitting |
| Ridge regressão | “Regularização L2” | Regressão linear com penalidade de lambda * sum(w_i^2) adicionada ao MSE |
| Regressão polinomial | "Ajustando curvas com matemática linear" | Regressão linear em características polinomiais (x, x^2, x^3, ...), ainda linear nos pesos |
| Sobreajuste | “Memorizando dados de treinamento” | Usando um modelo tão complexo que ajusta ruído em dados de treinamento e falha em novos dados |
Leitura Adicional
- Uma introdução ao aprendizado estatístico (ISLR) - PDF gratuito, capítulos 3 e 6 cobrem regressão linear e regularização com exemplos práticos de R
- Os Elementos de Aprendizagem Estatística (ESL) - PDF gratuito, o companheiro mais matemático do ISLR com tratamento mais profundo de crista e laço
- [Notas da aula Stanford CS229 sobre regressão linear] (https://cs229.stanford.edu/main_notes.pdf) - Notas de Andrew Ng derivando a equação normal e a descida do gradiente dos primeiros princípios
- documentação scikit-learn LinearRegression - referência prática para LinearRegression, Ridge, Lasso e ElasticNet com exemplos de código