Lab Opcional: Representação do Modelo
Baseado no Lab Opcional 02 (Semana 1, Curso 1) da Machine Learning Specialization, do Andrew Ng (DeepLearning.AI / Stanford). Mas esquece o notebook por um segundo, bora entender o que tá rolando de verdade.

Cola aqui: todo mundo que começa em ML ouve falar de rede neural, transformer, LLM, essas coisas todas cheias de hype. E aí abre o primeiro lab do curso mais recomendado da área e encontra... uma reta. y = ax + b do ensino médio, só que com nome trocado.
Isso não é o curso "enrolando" antes de chegar no assunto de verdade. É o assunto de verdade. Regressão linear é o Lego básico: todo modelo mais chique que você for estudar depois (rede neural, regressão logística, sei lá o que vier) é, no fundo, um monte desses Legos empilhados. Entender esse aqui até o osso me ensinou muito mais do que decorar fórmula de rede neural sem saber de onde ela vem.
O que você vai sair sabendo daqui
- Por que a gente representa dado de treino como array NumPy, e não lista Python.
- Contar quantos exemplos de treino você tem () e pescar um exemplo específico.
- Olhar pros dados antes de sair modelando (isso não é enrolação, é regra de ouro).
- Montar o modelo em código.
- Brincar com os parâmetros até achar o ajuste perfeito, na mão, sem fórmula mágica, e sentir na pele por que isso não escala.
Todo modelo supervisionado tem três peças: os dados, a função do modelo e os parâmetros. Esse post monta as três peças na versão mais simples que existe. Guarda essa estrutura: ela reaparece, só que maior, em praticamente tudo que vem depois no curso.
O dicionário da área
ML tem um vício chato: adora inventar símbolo pra coisa simples. Aqui vai o "dicionário" que o curso usa, porque ele reaparece do início ao fim:
| Notação | O que é | Em Python |
|---|---|---|
| um número solto (escalar) | - | |
| uma lista de números (vetor, letra em negrito) | - | |
| os valores de entrada de todos os exemplos de treino | x_train | |
| os valores que a gente quer prever, de todos os exemplos | y_train | |
| , | o par de valores do exemplo número | x_i, y_i |
| quantos exemplos de treino você tem | m | |
| peso (a inclinação da reta) | w | |
| viés (o ponto de partida da reta) | b | |
| o que o modelo prevê pro exemplo | f_wb |
Duas pegadinhas que confundem todo iniciante:
- não é potência. O
(i)entre parênteses é só "qual exemplo da tabela", tipo o número da linha. é o terceiro exemplo (lembra, começa do zero), enquanto seria "x ao quadrado", coisa bem diferente. - é número de linhas (exemplos). Mais pra frente aparece , número de colunas (features/atributos). Pensa numa planilha de Excel: é quantas casas você tem cadastradas, é quantas colunas de informação você anotou sobre cada uma (tamanho, número de quartos, bairro...). Aqui a gente só tem uma coluna, então .
O problema: quanto vale essa casa aí?
Cenário: você é corretor e só fechou duas vendas até agora. Uma casa de 1000 pés² foi vendida por 300 mil dólares, e uma de 2000 pés², por 500 mil. Aparece um cliente com uma casa de 1200 pés² perguntando quanto ela vale. Você não tem bola de cristal, mas tem dois pontos de referência, e foi surpreendente quanto eu consegui tirar disso.
Pra não ficar escrevendo zero pra caramba, tamanho vira "milhares de pés²" e preço vira "milhares de dólares":
| Tamanho (1000 pés²) → | Preço (1000 dólares) → | |
|---|---|---|
| 0 | 1.0 | 300 |
| 1 | 2.0 | 500 |
Ou seja: exemplos, e .
Meu plano era achar uma reta que passasse (ou chegasse perto) desses pontos. Achando essa reta (ou seja, achando e ) eu consigo estimar o preço de qualquer casa nova, incluindo uma que a gente nunca viu, tipo a de 1200 pés².
Escala menor é hábito, não frescura: mais pra frente no curso, quando os números ficarem grandes de verdade (preço de casa em SP, tipo), normalizar as escalas (feature scaling) vai ser essencial pro treino nem travar nem enlouquecer. Já vai acostumando o olho.
Bota isso num array
# =====================================================================
# DADOS DE TREINO
# =====================================================================
# x_train -> variável de ENTRADA (feature): tamanho da casa, em milhares de pés²
# y_train -> variável ALVO (target): preço da casa, em milhares de dólares
# A ordem importa: x_train[0] e y_train[0] descrevem a MESMA casa.
x_train = np.array([1.0, 2.0]) # Cria um array NumPy 1-D com os dois tamanhos.
y_train = np.array([300.0, 500.0]) # Cria um array NumPy 1-D com os dois preços correspondentes.
print(f"x_train = {x_train}")
print(f"y_train = {y_train}")
Saída:
x_train = [1. 2.] y_train = [300. 500.]
Repara que usei np.array, não uma lista Python normal ([1.0, 2.0] puro). Não é frescura de biblioteca chique: array NumPy guarda os números "grudados" na memória e faz conta em cima de todo mundo de uma vez (isso chama vetorização), em vez de percorrer item por item como uma lista faria. Com 2 números não faz diferença nenhuma, mas com 2 milhões é a diferença entre rodar em segundos ou travar sua máquina. Todo o curso, e praticamente todo código sério de ML, parte do NumPy por causa disso.
Sobre f-strings: o
fantes das aspas é Python avaliando o que tá dentro de{ }e enfiando no texto.{valor:.2f}formata com 2 casas decimais. Vai aparecer o tempo todo daqui pra frente.
Quantos exemplos eu tenho? ()
# Via .shape -- devolve uma TUPLA com o tamanho de cada dimensão.
print(f"x_train.shape: {x_train.shape}") # (2,) para um vetor 1-D com 2 elementos.
m = x_train.shape[0]
print(f"Número de exemplos de treino é: {m}")
# Via len() -- funciona em arrays NumPy como funciona em listas.
m = len(x_train)
print(f"Número de exemplos de treino é: {m}")
As duas dão o mesmo resultado aqui, mas eu peguei o hábito de usar .shape cedo, e vale a pena você pegar também: quando seus dados tiverem várias colunas (aquele que falei ali em cima), .shape te conta linhas e colunas de cara, enquanto len() só te dá o número de linhas e te deixa no escuro sobre o resto.
Pescando um exemplo específico
Python conta a partir do zero, sempre:
| Índice | Em Python | |
|---|---|---|
| 0 | x_train[0], y_train[0] | |
| 1 | x_train[1], y_train[1] |
i = 0 # Índice do exemplo que queremos inspecionar.
x_i = x_train[i]
y_i = y_train[i]
print(f"(x^({i}), y^({i})) = ({x_i}, {y_i})")
Saída:
(x^(0), y^(0)) = (1.0, 300.0)
Detalhe chato que pega gente boa: nas aulas às vezes o Andrew Ng conta a partir de 1 ( sendo o primeiro exemplo). No código, o primeiro é sempre x_train[0]. É a mesma casa, só muda a régua de contagem. Não é bug seu se der um "off-by-one" na cabeça, é padrão da área mesmo.
Olha pros dados antes de sair modelando
Isso não é conselho de "boa prática" genérico, é regra de sobrevivência. Se você sair direto ajustando reta sem olhar o formato dos dados, você não vai perceber quando a relação não é nem de longe linear (e aí regressão linear seria a ferramenta errada pro trabalho). Um gráfico de dispersão resolve isso em 2 segundos de olhada. Passa o mouse nos pontos:
Com só 2 pontos, "linear ou não" é meio óbvio, mas o hábito de olhar antes é o que importa aqui, porque com 200 ou 2000 pontos ele vai te salvar de horas perdidas tentando ajustar o modelo errado.
A função do modelo: o que w e b realmente significam
Beleza, é a equação de uma reta. Mas "inclinação" e "intercepto" são termos meio secos, deixa eu te dar uma imagem melhor: pensa num aplicativo de corrida tipo Uber. Toda corrida tem uma taxa fixa de embarque (você paga isso só por entrar no carro, não importa a distância) e um valor por quilômetro rodado. O preço final é taxa_fixa + valor_por_km * distância.
Bate exatamente com :
| Parâmetro | No Uber | Aqui | Significado geométrico |
|---|---|---|---|
| valor por km rodado | quanto o preço sobe por 1000 pés² a mais | inclinação da reta | |
| taxa fixa de embarque | preço-base, quando o tamanho é zero | intercepto (onde a reta cruza o eixo vertical) |
Se é grande, cada km (ou cada 1000 pés²) pesa mais no preço final: a reta sobe rápido. Se é grande, você já começa "pagando caro" antes de andar um metro, e a reta nasce mais alto no eixo vertical. Combinações diferentes de e desenham retas completamente diferentes.
Foi aí que eu entendi: treinar o modelo é justamente achar o par que melhor descreve os dados que você tem. Só que a gente ainda não viu como automatizar essa busca (isso é assunto dos próximos dois posts: função de custo e gradiente descendente). Por enquanto, vamos fazer isso na mão mesmo, chutando valores, pra você sentir na pele o problema que esses próximos posts resolvem.
Transformando a fórmula em função
Com 2 pontos, eu conseguia calcular w * x[0] + b e w * x[1] + b na mão. Com mil pontos isso vira trabalho braçal inútil, e é exatamente esse tipo de repetição chata que a gente empacota numa função:
def compute_model_output(x, w, b):
"""
Calcula a previsão de um modelo linear f_wb(x) = w*x + b.
Args:
x (ndarray (m,)) : dados de entrada, m exemplos (a feature)
w, b (scalar) : parâmetros do modelo (peso e viés)
Returns:
f_wb (ndarray (m,)) : previsão do modelo para cada exemplo de x
"""
m = x.shape[0] # 1) Quantos exemplos existem no array de entrada.
f_wb = np.zeros(m) # 2) Recipiente de saída: array de m zeros.
for i in range(m): # 3) Percorre i = 0, 1, ..., m-1.
f_wb[i] = w * x[i] + b # Aplica a equação da reta ao exemplo i.
return f_wb # Devolve o array completo de previsões.
Guardei isso numa função com contrato claro (entra x, w, b, sai um array de previsões), e isso significa que ela funciona pra 2 exemplos, 2 mil ou 2 milhões, sem você reescrever nada. É um detalhe chato de engenharia, mas é esse tipo de detalhe que separa "script que roda uma vez" de "código que aguenta um sistema de verdade".
Agora é a sua vez: acha o ajuste perfeito
Aqui embaixo tem um gráfico de verdade interativo: arraste os sliders de e e observa três coisas ao mesmo tempo: a linha azul (a previsão do seu modelo), as linhas pontilhadas cinzas (a distância entre cada previsão e o valor real, isso é o erro de cada exemplo) e o número de "Erro total" embaixo do gráfico.
Começa em , (o primeiro chute do notebook original), repara como a linha fica longe dos losangos vermelhos, e o erro total é grande. Seu desafio: mexe nos sliders até o erro total zerar.
Duas dicas, se quiser pensar antes de sair arrastando no chute:
- A inclinação : o preço sobe de 300 pra 500 (subiu 200) enquanto o tamanho sobe de 1.0 pra 2.0 (andou 1.0). Inclinação é "quanto subiu dividido por quanto andou".
- O intercepto : já sabendo , pega um dos pontos, joga na equação e isola o .
Erro total: 300.0
Travou? Aqui vai a conta (clique pra revelar)
Inclinação:
Intercepto, usando o primeiro ponto:
Então: , . Bota isso nos sliders lá em cima e confere o erro zerando.
Beleza, você achou e na mão, mas repara: você só conseguiu porque tinha exatamente 2 pontos e 2 parâmetros pra ajustar, então dava pra resolver com álgebra de ensino médio. Bota mais dados reais (com ruído, tipo casas parecidas vendendo por preços um pouco diferentes) e nenhuma reta única vai passar por todo mundo, não vai ter "erro zero" possível.
Aí surge uma pergunta nova: qual reta é a "menos errada"? Eu precisei de um jeito de medir "o quão errada" uma reta é, de forma numérica, comparável entre diferentes . Isso é a função de custo, (o "Erro total" que você viu no playground é uma versão simplificada dela). E eu precisei de um jeito de buscar automaticamente o que minimiza esse erro, sem você ficar arrastando slider a vida inteira. Isso é o gradiente descendente: imagina que você tá vendado no topo de uma ladeira e seu único jeito de descer é sentir com o pé qual direção desce mais rápido e dar um passo. Repete isso um monte de vezes e, se tudo der certo, você chega lá embaixo, no ponto de menor erro. É basicamente isso que o próximo post vai destrinchar.
Fazendo a previsão que a gente queria desde o início
, ajustados. Agora sim: quanto vale a casa de 1200 pés²?
Lembrando a escala: 1200 pés² é .
340, na escala do problema, é 340 mil dólares.
w = 200 # Peso ajustado (achado no desafio).
b = 100 # Viés ajustado (achado no desafio).
x_i = 1.2 # Entrada nova: 1200 pés² = 1.2, porque x está em milhares de pés².
cost_1200sqft = w * x_i + b # Aplica o modelo. Não confundir com "função de custo", nome parecido, coisa diferente.
print(f"${cost_1200sqft:.0f} mil dólares")
Saída:
$340 mil dólares
Presta atenção no que rolou aqui: nunca apareceu nos dados de treino. A gente só tinha casas de 1.0 e 2.0. O modelo generalizou pra um caso que ele nunca viu, e é exatamente por isso que a gente treina modelo, não é pra decorar os 2 pontos que já sabíamos (isso qualquer print de tabela faz). É pra prever o que a gente não sabe.
Fechando
| Etapa | O que rolou | Código-chave |
|---|---|---|
| 1. Dados | representar os exemplos como arrays NumPy | np.array([...]) |
| 2. Inspeção | contar exemplos e pescar um exemplo específico | .shape[0], x_train[i] |
| 3. Visualização | olhar os dados antes de modelar | gráfico interativo |
| 4. Modelo | definir e implementar | compute_model_output() |
| 5. Parâmetros | achar e na mão, mexendo até o erro zerar | playground interativo |
| 6. Previsão | aplicar o modelo pra um dado novo | w * x_i + b |
Três ideias pra levar:
- Regressão linear modela uma relação entre uma entrada (feature) e uma saída (target). Aqui: tamanho → preço. Amanhã pode ser qualquer outra coisa: km rodado → preço da corrida, tamanho do arquivo → tempo de processamento, sei lá.
- O modelo só tem dois botões: e . É só isso que muda quando o modelo "aprende". Não tem mágica escondida.
- O ponto todo é generalizar: prever coisa que o modelo nunca viu. Se ele só reproduzisse os dados de treino, seria uma tabela disfarçada, não um modelo.
No próximo post: você achou arrastando slider porque tinha só 2 pontos. Isso não escala pra dados de verdade. A gente formaliza a função de custo , a "nota" que mede o quão ruim uma reta é, e começa a preparar o terreno pro gradiente descendente, que é quem vai fazer essa busca por você, automaticamente, em dados que não cabem mais numa conta de cabeça.
Aplicação prática
Chega de dado de brinquedo. Vamos aplicar exatamente o que você aprendeu aqui num dataset real de imóveis: 500 casas, com tamanho, número de quartos, distância do centro e preço de verdade (Housing Prices Regression, Kaggle). Sem conceito novo, só o que esse post já ensinou, agora em cima de dado com ruído de verdade.
Explorando as features
import pandas as pd
df = pd.read_csv("real_estate_dataset.csv")
print(df[["Square_Feet", "Num_Bedrooms", "Location_Score", "Distance_to_Center", "Price"]].describe())
print(df.corr(numeric_only=True)["Price"].sort_values(ascending=False))
Saída (correlação com o preço):
Square_Feet 0.65,Num_Bedrooms 0.57,Distance_to_Center 0.26,Location_Score 0.05.
Square_Feet é de longe a feature que mais anda junto com o preço aqui. Bate com a história que a gente contou o post inteiro: tamanho da casa é o ponto de partida certo.
A "matriz de features" em 3D
Antes de reduzir pra uma variável só, girei as três dimensões que mais importam ao mesmo tempo: tamanho, quartos e preço.
Carregando dados reais...
Repara como a altura (preço) sobe visivelmente junto com o tamanho, mais do que junto com o número de quartos. É a mesma correlação de cima, só que agora você tá vendo com os próprios olhos em vez de ler um número.
Achando e na mão, com dado de verdade
Mesmo playground do post, mesma mecânica, só que agora com as 50 casas reais no lugar dos 2 pontos perfeitos:
Carregando dados reais...
Repara que dessa vez o erro não zera, por mais que você mexa nos sliders. Isso não é bug, é o que o próximo post inteiro vai explicar: com dado real e ruidoso, não existe reta perfeita, só a "menos errada".
Prevendo o preço de uma casa nova
# Usando o ajuste aproximado que achamos brincando com o playground:
w = 116 # dólares (em milhares) a mais por cada 100 pés² de tamanho
b = 399 # preço-base, em milhares de dólares
casa_nova = 220 / 100 # 220 pés², na mesma escala do gráfico
preco_previsto = w * casa_nova + b # em milhares de dólares
print(f"${preco_previsto * 1000:,.0f} dólares")
Saída:
$654,200 dólares
Mesma conta do post, mesma fórmula, só que agora validada em cima de um dataset que você (ou qualquer leitor) pode baixar e conferir.