← Voltar pra playlist

O Gradiente Descendente Não Precisou Aprender Nada de Novo

Lá na Semana 1, gradiente descendente pra regressão linear era: calcula o gradiente, dá um passo, repete. O código dessa vez já tem sigmoide, fronteira de decisão, perda e custo logísticos prontos. Falta uma coisa: rodar gradiente descendente em cima desse custo novo pra achar o w e o b que minimizam ele. E aqui vem a parte satisfeita do capítulo: o loop que dá os passos é literalmente o mesmo. Só a conta de dentro do loop (o gradiente) muda, porque f trocou de reta pra sigmoide.

O loop não muda, só o que ele deriva

wj=wj−α∂J(w,b)∂wjb=b−α∂J(w,b)∂bw_j = w_j - \alpha \frac{\partial J(\mathbf{w},b)}{\partial w_j} \qquad b = b - \alpha \frac{\partial J(\mathbf{w},b)}{\partial b}

Essa é a receita de sempre: pega o gradiente, dá um passo do tamanho alpha na direção contrária (atualizando w e b ao mesmo tempo), repete até parar de melhorar. E as derivadas parciais também têm a mesma cara de sempre:

∂J(w,b)∂wj=1m∑i=0m−1(fw,b(x(i))−y(i))xj(i),∂J(w,b)∂b=1m∑i=0m−1(fw,b(x(i))−y(i))\frac{\partial J(\mathbf{w},b)}{\partial w_j} = \frac{1}{m} \sum\limits_{i = 0}^{m-1} (f_{\mathbf{w},b}(\mathbf{x}^{(i)}) - y^{(i)})x_{j}^{(i)}, \qquad \frac{\partial J(\mathbf{w},b)}{\partial b} = \frac{1}{m} \sum\limits_{i = 0}^{m-1} (f_{\mathbf{w},b}(\mathbf{x}^{(i)}) - y^{(i)})

Se você olhar rápido, essa fórmula é indistinguível da de regressão linear. A pegadinha (boa) é que ela é igual, byte a byte. O que muda é o que f significa: na regressão linear, f = w·x + b; aqui, f = sigmoid(w·x + b). É essa troca, escondida dentro de f, que faz o resto da conta se comportar diferente sem precisar reescrever uma linha da fórmula do gradiente.

O código, linha a linha

def compute_gradient_logistic(X, y, w, b):
    m, n = X.shape
    dj_dw = np.zeros((n,))
    dj_db = 0.

    for i in range(m):
        f_wb_i = sigmoid(np.dot(X[i], w) + b)
        err_i = f_wb_i - y[i]
        for j in range(n):
            dj_dw[j] = dj_dw[j] + err_i * X[i, j]
        dj_db = dj_db + err_i
    dj_dw = dj_dw / m
    dj_db = dj_db / m

    return dj_db, dj_dw
  • f_wb_i = sigmoid(...): a mesma combinação linear de sempre, espremida pela sigmoide pra virar probabilidade.
  • err_i = f_wb_i - y[i]: o erro desse paciente, previsão menos rótulo real.
  • o for j interno multiplica esse erro por cada variável X[i,j] e acumula em dj_dw[j], a mesma mecânica de "erro vezes entrada" que a regressão linear já fazia.
  • dj_db acumula só o erro puro, sem multiplicar por nada, também igual à regressão linear.
  • no fim, os dois viram média (/ m).

Conferindo com w=[2,3], b=1 no dataset de sempre:

dj_db_tmp, dj_dw_tmp = compute_gradient_logistic(X_tmp, y_tmp, w_tmp, b_tmp)
print(f"dj_db: {dj_db_tmp}")
print(f"dj_dw: {dj_dw_tmp.tolist()}")

Saída: dj_db: 0.49861806546328574 · dj_dw: [0.498333393278696, 0.49883942983996693]

O loop de fora, esse sim, é cópia e cola

def gradient_descent(X, y, w_in, b_in, alpha, num_iters):
    w = copy.deepcopy(w_in)
    b = b_in

    for i in range(num_iters):
        dj_db, dj_dw = compute_gradient_logistic(X, y, w, b)
        w = w - alpha * dj_dw
        b = b - alpha * dj_db

        if i % math.ceil(num_iters / 10) == 0:
            print(f"Iteration {i:4d}: Cost {compute_cost_logistic(X, y, w, b)}")

    return w, b

Compara com o gradient_descent da Semana 1: a única diferença de verdade é qual função de custo entra no print de acompanhamento. O for, a atualização w = w - alpha * dj_dw, tudo igual.

Rodando no dataset de seis pacientes de sempre, alpha=0.1, 10 mil iterações:

Saída:

Iteration    0: Cost 0.6846
Iteration 1000: Cost 0.1591
Iteration 2000: Cost 0.0846
Iteration 3000: Cost 0.0571
Iteration 4000: Cost 0.0429
Iteration 5000: Cost 0.0343
Iteration 6000: Cost 0.0286
Iteration 7000: Cost 0.0245
Iteration 8000: Cost 0.0214
Iteration 9000: Cost 0.0190

w: [5.2812, 5.0782], b: -14.2224

O custo despenca no começo (de 0.68 pra 0.16 só nas primeiras mil voltas) e depois vai afinando devagar, a mesma curva de "aprende rápido no início, refina depois" que já apareceu em toda superfície de custo convexa que treinamos até aqui.

A fronteira separa os seis pacientes sem errar nenhum, exatamente como a versão treinada à mão que eu já tinha mostrado antes, só que dessa vez eu vi o gradiente descendente chegar lá passo a passo, em vez de receber os pesos prontos.

Andando morro abaixo, ao vivo

O dataset acima tem duas variáveis, então a superfície de custo mora num espaço de 4 dimensões (w0, w1, b, custo) que não dá pra desenhar. Pra ver gradiente descendente andando de verdade, volto pra um dataset de uma variável só, os mesmos seis pacientes do primeiro lab da semana:

x_train = np.array([0., 1, 2, 3, 4, 5])
y_train = np.array([0,  0, 0, 1, 1, 1])

Com só w e b, o custo logístico vira uma superfície 3D de verdade, a mesma tigela convexa que o lab passado já tinha comparado contra a tentativa "custo errado" (aquela, sim, cheia de vales falsos). Mexe no alpha, clica em "Rodar" e repara a bolinha vermelha descendo até o fundo, sempre pelo mesmo caminho que o formato convexo garante:

iteração 0 · w = 0.00 · b = 0.00 · custo J = 0.69

Repara que os controles são os mesmos que você já usou nos posts de regressão linear: alpha, passo a passo, rodar em lote, trocar entre 2D/3D/parábola. Gradiente descendente não fica mais esperto nem mais burro por trabalhar com um custo diferente, ele só desce a superfície que você entrega pra ele.

Fechando

O que eu já sabiaO que essa aula assentou
Gradiente descendente atualiza w e b na direção contrária ao gradienteA fórmula do gradiente é idêntica à da regressão linear, só que f agora é a sigmoide, não a reta
O custo logístico é convexoIsso garante que gradiente descendente sempre acha o mesmo fundo de vale, não importa de onde começa
Eu via a fronteira treinada prontaAgora eu vi ela nascer passo a passo, o mesmo loop que já rodou pra regressão linear

Aplicação Prática

Reaproveito a mesma variável real que a playlist já vem usando (worst concave points, normalizada por z-score), agora com a amostra de 60 pacientes do lab da fronteira de decisão, e rodo o gradiente descendente eu mesmo em vez de receber os pesos prontos:

iteração 0 · w = 0.00 · b = 0.00 · custo J = 0.69

Rodando com alpha=0.5, o custo cai de 0.6931 (a pior chute possível, -log(0.5)) pra 0.2372 e converge por volta de w=4.175, b=-1.094, valores bem parecidos com o w=3.9565, b=-1.1326 que os labs anteriores citam pro conjunto de treino completo (essa amostra aqui é só 60 pacientes, o modelo cheio viu bem mais gente). Acurácia nessa amostra: 0.90, no mesmo patamar dos 0.91-0.93 que os labs anteriores já reportavam. O mesmo loop que rodou em cima de seis pontos de brinquedo, agora rodando em cima de pacientes reais, sem precisar mudar uma linha de código, só o dado que entra.