← Voltar pra playlist

Lab Opcional: a Função de Custo

No post passado você achou w=200w = 200 e b=100b = 100 brincando com o slider até o erro zerar. Beleza, funcionou. Mas para. Como você sabia que tinha achado o "melhor" ajuste? Você olhou pro número de erro total caindo até zero e confiou nele.

Esse post é sobre dar nome, sobrenome e fórmula pra esse número. Ele se chama , escrita J(w,b)J(w,b).

J(w,b)=12mi=0m1(fw,b(x(i))y(i))2J(w,b) = \frac{1}{2m}\sum_{i=0}^{m-1}\left(f_{w,b}(x^{(i)}) - y^{(i)}\right)^2

Eu sei, parece assustador. Vamos abrir essa fórmula peça por peça, tipo desmontando um Lego.

Cuidado com a palavra "custo"

Antes de mais nada: aqui custo não tem nada a ver com preço de casa. É a medida de quão errado o modelo está. Pra preço da casa a gente continua usando a palavra preço mesmo. Essa confusão de vocabulário pega muita gente no começo, porque "custo" no dia a dia é sempre dinheiro.

Desmontando a fórmula

Pensa numa prova de tiro ao alvo. Cada casa do seu conjunto de treino é uma tentativa de acertar o centro do alvo (o preço real, y(i)y^{(i)}). fw,b(x(i))f_{w,b}(x^{(i)}) é onde a sua flecha caiu (a previsão do modelo). A distância entre onde a flecha caiu e o centro é o daquele tiro específico.

Pedaço da fórmulaNomeO que faz
fw,b(x(i))y(i)f_{w,b}(x^{(i)}) - y^{(i)}erro (resíduo)a distância entre a previsão e o real
()2(\ldots)^2erro ao quadradotira o sinal (errar pra cima ou pra baixo pesa igual) e pune erro grande com força bem maior
i=0m1\sum_{i=0}^{m-1}somajunta o erro de todo mundo
12m\frac{1}{2m}média (com o 2 extra)dividir por mm vira média (senão o custo só cresceria por ter mais dados, mesmo que o ajuste seja igualmente bom), e o 2 é só um ajuste de conta pro próximo post ficar mais limpo, explico melhor lá

Duas coisas saem direto dessa fórmula, e eu nem precisei decorar nada:

  1. J(w,b)J(w,b) nunca é negativo. É soma de coisas ao quadrado, então o menor valor possível é zero.
  2. J(w,b)=0J(w,b) = 0 significa ajuste perfeito. A reta passa exatamente em cima de todos os pontos.

E por que elevar ao quadrado em vez de, sei lá, pegar o módulo do erro? Porque o quadrado castiga desproporcionalmente os erros grandes. Errar o dobro custa quatro vezes mais caro em JJ, não o dobro. Isso faz o modelo detestar previsões muito ruins de um jeito que o módulo não faria, e é justamente esse comportamento que dá pro custo o formato de U que a gente vai ver daqui a pouco. Voltando pro tiro ao alvo: é como se o juiz da competição desse nota exponencialmente pior quanto mais longe do centro a flecha cair. Errar por pouco dói pouco, errar feio dói muito mais que o dobro.

Colocando isso em código

def compute_cost(x, y, w, b):
    """
    Calcula a função de custo da regressão linear.

    Args:
      x (ndarray (m,)) : dados de entrada, m exemplos
      y (ndarray (m,)) : valores-alvo
      w, b (scalar)    : parâmetros do modelo

    Returns:
      total_cost (float): o custo de usar w e b como parâmetros para ajustar
                          os pontos (x, y)
    """
    m = x.shape[0]

    cost_sum = 0

    for i in range(m):
        f_wb = w * x[i] + b           # previsão do modelo pro exemplo i
        cost = (f_wb - y[i]) ** 2     # erro do exemplo i, ao quadrado
        cost_sum = cost_sum + cost    # acumula no total

    total_cost = (1 / (2 * m)) * cost_sum

    return total_cost

Repara que é o mesmo laço for do post passado (o compute_model_output), só que em vez de guardar as previsões, a gente acumula o erro ao quadrado de cada uma. Estrutura idêntica, propósito diferente.

Congelando um parâmetro pra enxergar o outro

Com dois parâmetros pra mexer (ww e bb), eu tive dificuldade de visualizar o custo direto. Truque clássico: trava um deles e olha só pro outro. Fixa b=100b = 100 e mexe só em ww.

Arrasta o slider abaixo e olha duas coisas ao mesmo tempo: como a reta (painel esquerdo) se aproxima ou se afasta dos pontos, e onde o ponto vermelho fica na curva J(w)J(w) (painel direito). Conforme a reta melhora, o ponto desce na curva.

Repara três coisas:

  • O custo é mínimo exatamente em w=200w = 200, o mesmo valor que você achou no olho no post passado. Com b=100b = 100, o custo nesse ponto vai a zero, porque a reta passa nos dois pontos.
  • O custo explode rápido quando ww fica muito grande ou muito pequeno. É o quadrado da fórmula fazendo esse trabalho.
  • Aí eu saquei: minimizar o custo é o mesmo que achar o melhor ajuste. Não é coincidência, é a definição. Escolher ww e bb que minimizam JJ é literalmente o que a gente chama de treinar um modelo.

Experimenta levar o slider até w=0w = 0: a reta fica horizontal (prevê o mesmo preço pra qualquer tamanho de casa) e o custo dispara.

O mundo real não tem só 2 pontos perfeitos

Até aqui, com 2 pontos e 2 parâmetros, eu conseguia zerar o custo. Isso é raro. Bora trocar o conjunto de treino por um mais realista, com 6 casas, onde os preços não caem exatamente sobre reta nenhuma (ruído de verdade, tipo casas parecidas vendendo por preços um pouco diferentes):

Tamanho (1000 pés²)Preço (1000 dólares)
1.0250
1.7300
2.0480
2.5430
3.0630
3.2730

Pergunta que fica no ar: será que ainda existe algum (w,b)(w, b) que zere o custo aqui?

Mexe nos dois sliders abaixo (agora ww e bb ao mesmo tempo) e tenta chegar no centro do mapa de calor, onde o custo é mais baixo. Alterna pra visão 3D e gira a superfície com o dedo ou o mouse pra sentir o formato do "vale".

custo J(w,b) = 11862.5

Você deve ter percebido: dessa vez o custo não zera. O melhor que dá pra conseguir fica perto de w209w \approx 209 e b2.4b \approx 2.4, com custo em torno de 1736. Por quê? Porque essas 6 casas não estão alinhadas. Nenhuma reta única passa exatamente por todas. O melhor ajuste é aquele que deixa o menor erro quadrático total possível, e é isso que a função de custo define como "o melhor".

Guarda essa ideia: custo mínimo diferente de zero é o normal, não um bug. Custo zero costuma acontecer quando você tem poucos dados demais (como no nosso caso de 2 pontos) ou quando o modelo decorou os dados em vez de aprender o padrão deles (isso tem nome, , mas isso é assunto pra mais pra frente no curso).

Por que a superfície é sempre uma tigela

Girando a superfície 3D acima você deve ter notado: ela sempre tem essa cara de tigela de sopa, um único vale, sem picos falsos no meio do caminho. Isso não é coincidência do nosso exemplo específico, é consequência direta de elevar o erro ao quadrado na fórmula. Toda vez que a gente eleva algo ao quadrado, o resultado é uma superfície .

Pra ver isso de um jeito mais limpo, sem a distorção de escala que os dados reais trazem (repara que ww vai de 0 a 400 e bb de -200 a 200 no gráfico acima, o que estica o vale), aqui vai a versão idealizada da mesma forma, só w2+b2w^2 + b^2, com os dois eixos na mesma escala:

Por que essa forma importa tanto? Porque uma superfície convexa garante que só existe um mínimo, o mínimo global. Não tem vale escondido em outro canto pra um algoritmo de busca cair e ficar preso, achando que já chegou no fundo sem ter chegado. Essa garantia é o que torna confiável o próximo passo do curso: um jeito automático de descer até o fundo dessa tigela, sem você precisar arrastar slider a vida inteira.

Fechando

ConceitoO que ficou estabelecido
Função de custoJ(w,b)J(w,b), um número que mede o quanto as previsões erram os dados de treino
Por que ao quadradoremove o sinal do erro e pune fortemente erros grandes, além de garantir uma superfície convexa
Por que 2m2mmm transforma soma em média, e o 2 simplifica a conta que vem no próximo post
Formatocurva em U (um parâmetro) e tigela de sopa (dois parâmetros)
Treinar o modeloé achar o par (w,b)(w,b) que minimiza J(w,b)J(w,b)
Custo mínimo diferente de zeronormal quando os dados têm ruído, não é sinal de erro

No próximo post: você já sentiu na mão como é procurar o fundo da tigela arrastando slider. Não escala. O próximo post apresenta o gradiente descendente, um algoritmo que usa a inclinação da superfície de custo pra descer até o fundo sozinho, sem você precisar chutar valor nenhum.

Aplicação prática

Mesmo dataset real de imóveis do post anterior (500 casas, Housing Prices Regression, Kaggle). Agora com compute_cost de verdade.

def compute_cost(x, y, w, b):
    m = x.shape[0]
    cost_sum = 0
    for i in range(m):
        f_wb = w * x[i] + b
        cost_sum += (f_wb - y[i]) ** 2
    return (1 / (2 * m)) * cost_sum

# x_sqft já em "centenas de pés²", y_price já em "milhares de dólares"
# (mesma amostra de 50 casas do post anterior)

print("custo com um chute ruim, w=50, b=100:", compute_cost(x_sqft, y_price, 50, 100))
print("custo perto do ajuste ótimo, w=116, b=399:", compute_cost(x_sqft, y_price, 116, 399))

Saída: custo com um chute ruim, w=50, b=100: 90317.6 / custo perto do ajuste ótimo, w=116, b=399: 5189.6

Quase 20 vezes menos custo só de acertar melhor os parâmetros. Mexe você mesmo nos sliders e tenta chegar perto desses ~5189 (repara: não dá pra zerar, o mínimo real é isso mesmo, dado real tem ruído):

Carregando dados reais...