← Voltar pra playlist

Regularização: Deixa Todas as Features, Só Não Deixa Nenhuma Mandar Demais

O lab passado fechou com três saídas pro overfitting: mais dados, menos features, ou regularização. As duas primeiras têm um problema: mais dados nem sempre existe, e cortar features acaba jogando fora informação que podia ser útil. Regularização resolve diferente: mantém todas as features, só desencoraja qualquer peso de crescer demais. O b fica de fora dessa penalidade, só os w_j sentem o freio.

Custo com regularização

Pra regressão linear, o termo novo é só uma soma dos pesos ao quadrado, multiplicada por lambda e dividida por 2m:

J(w,b)=12m∑i=0m−1(fw,b(x(i))−y(i))2+λ2m∑j=0n−1wj2J(\mathbf{w},b) = \frac{1}{2m} \sum\limits_{i = 0}^{m-1} (f_{\mathbf{w},b}(\mathbf{x}^{(i)}) - y^{(i)})^2 + \frac{\lambda}{2m} \sum_{j=0}^{n-1} w_j^2

def compute_cost_linear_reg(X, y, w, b, lambda_ = 1):
    m  = X.shape[0]
    n  = len(w)
    cost = 0.
    for i in range(m):
        f_wb_i = np.dot(X[i], w) + b
        cost = cost + (f_wb_i - y[i])**2
    cost = cost / (2 * m)

    reg_cost = 0
    for j in range(n):
        reg_cost += (w[j]**2)
    reg_cost = (lambda_/(2*m)) * reg_cost

    return cost + reg_cost

A primeira metade da função é o custo de sempre, sem mudar nada. A segunda metade (reg_cost) só soma o quadrado de cada peso, multiplica por lambda_/(2m), e soma no final. Testando com dados aleatórios fixos (np.random.seed(1), seis features, lambda=0.7):

Saída: Regularized cost: 0.07917239320214275

Pra regressão logística, a mudança é idêntica, o termo de regularização é copiado e colado:

J(w,b)=1m∑i=0m−1[−y(i)log⁡(fw,b(x(i)))−(1−y(i))log⁡(1−fw,b(x(i)))]+λ2m∑j=0n−1wj2J(\mathbf{w},b) = \frac{1}{m} \sum_{i=0}^{m-1} \left[ -y^{(i)} \log\left(f_{\mathbf{w},b}(\mathbf{x}^{(i)})\right) - \left( 1 - y^{(i)}\right) \log \left( 1 - f_{\mathbf{w},b}(\mathbf{x}^{(i)}) \right) \right] + \frac{\lambda}{2m} \sum_{j=0}^{n-1} w_j^2

def compute_cost_logistic_reg(X, y, w, b, lambda_ = 1):
    m, n = X.shape
    cost = 0.
    for i in range(m):
        z_i = np.dot(X[i], w) + b
        f_wb_i = sigmoid(z_i)
        cost += -y[i]*np.log(f_wb_i) - (1-y[i])*np.log(1-f_wb_i)
    cost = cost/m

    reg_cost = 0
    for j in range(n):
        reg_cost += (w[j]**2)
    reg_cost = (lambda_/(2*m)) * reg_cost

    return cost + reg_cost

O custo de sempre na primeira metade, o mesmo reg_cost de cima colado na segunda. Testando com os mesmos dados aleatórios:

Saída: Regularized cost: 0.6850849138741673

Gradiente com regularização

O gradiente também ganha um termo extra, só em cima de w_j (o b continua sem penalidade nenhuma):

∂J(w,b)∂wj=1m∑i=0m−1(fw,b(x(i))−y(i))xj(i)+λmwj\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)} + \frac{\lambda}{m} w_j

O termo (lambda/m) * w_j é a única diferença entre esse gradiente e o de sempre, pra regressão linear ou logística, a única coisa que muda entre as duas é como f é calculado:

def compute_gradient_logistic_reg(X, y, w, b, lambda_):
    m, n = X.shape
    dj_dw = np.zeros((n,))
    dj_db = 0.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

    for j in range(n):
        dj_dw[j] = dj_dw[j] + (lambda_/m) * w[j]

    return dj_db, dj_dw

O for i é o gradiente logístico de sempre, sem mudar nada. O for j extra no final é a única linha nova: soma (lambda_/m) * w[j] em cada posição de dj_dw, depois que a média já foi tirada. Testando (linear e logística, mesmos dados aleatórios, lambda=0.7):

Saída (linear): dj_db: 0.6648774569425726 · dj_dw: [0.2965, 0.4912, 0.2165]

Saída (logística): dj_db: 0.341798994972791 · dj_dw: [0.1738, 0.3201, 0.1078]

Rodando de novo o exemplo do anel

O mesmo componente do lab passado, agora com o slider de lambda no centro das atenções. Deixa o grau fixo em 10 (já bem overfitado, como vimos) e sobe o lambda aos poucos:

grau 10 · lambda 0 · acurácia de treino ≈ 1.0000

A acurácia continua em 100% em quase todo o intervalo de lambda, porque esse dataset de brinquedo é fácil demais pra confundir o modelo. Mas repara na intensidade da cor: com lambda=0, a região central fica bem saturada, o modelo tem quase certeza absoluta (acima de 99.9%) até bem perto da borda do anel. Com lambda=5, aquela mesma borda fica bem mais clara, a confiança do modelo caiu pra perto de 81.6% na mesma posição. Regularização não mudou a decisão final aqui, mas deixou o modelo menos "espalhafatoso", menos convicto além do que os dados sustentam. Ainda dá pra ir longe demais: sobe o lambda até 10 e repara a região central começar a encolher, sinal de que agora o modelo já está regularizado demais, puxando os pesos pra perto de zero a ponto de prejudicar até a decisão simples que ele já tinha acertado.

O gancho com a tarefa avaliada da semana

A tarefa avaliada dessa semana (a parte que eu não converto aqui porque os dados brutos dela não vieram junto com o notebook) pega um dataset de duas notas de prova pra decidir admissão, e depois um de duas medidas de teste de qualidade pra decidir se um microchip passa ou não. A segunda parte pede exatamente o que esse post já construiu: mapear duas features num polinômio de grau alto (a função map_feature do curso é o mesmo mapeamento que o componente acima já faz por baixo dos panos) e depois regularizar a regressão logística pra essa fronteira, livre pra curvar, não ficar maluca. Você já viu o resultado final funcionando aqui em cima, só que num dataset diferente.

Fechando

O que eu já sabiaO que essa aula assentou
Overfitting tem três saídas: mais dados, menos features, ou regularizaçãoRegularização é a mais usada porque mantém toda a informação, só freia o peso de cada uma
O gradiente logístico de sempreO termo (lambda/m) * w_j, somado só depois da média, sem tocar no b
Grau alto deixa a fronteira malucalambda alto acalma a confiança do modelo sem necessariamente mudar a decisão, até passar do ponto e regularizar demais

Aplicação Prática

No dataset real de câncer de mama (grau fixo em 8, já visto no lab passado como levemente overfitado), varrendo lambda:

lambdacusto treinocusto testeacurácia treinoacurácia teste
00.13350.081493.5%94.7%
10.13630.085194.5%95.6%
20.13850.089194.7%95.6%
50.14400.099194.3%93.9%
100.15160.111493.6%93.9%
500.19050.160694.5%94.7%

Um lambda pequeno (1 ou 2) realmente recupera um pontinho de acurácia de teste em cima do lambda=0 (94.7% → 95.6%), mas o efeito aqui é modesto, bem mais sutil que no anel de brinquedo. Faz sentido: com só duas features, a fronteira desse dataset real já é quase uma reta por natureza, overfitting nunca foi um problema grave aqui pra começar. E o oposto também aparece nítido: lambda=10 já piora as duas acurácias em relação a lambda=1, regularização em excesso prejudicando um problema que não precisava de tanto freio.

grau 8 · lambda 0 · acurácia de treino ≈ 0.9833

Sobe o lambda devagar e repara as dobras nas bordas do lab passado suavizando. Regularização não é uma vitória automática, é uma ferramenta: ajuda muito quando o overfitting é grave, ajuda pouco (e pode até atrapalhar) quando o problema já não pedia tanta flexibilidade assim.