MirandasTech Assinar

Machine Learning 13/10/2026 23 min de leitura

Regressão logística: probabilidade, log-loss e limiar de decisão

Transformar um preditor linear em probabilidade, treiná-lo pela log-loss e separar a probabilidade estimada da decisão tomada com ela.

Curva sigmoide de vidro ciano sobre um pedestal escuro, cortada na parte baixa por uma lâmina violeta que representa o limiar de decisão, ao lado de duas pilhas de fichas
🎓
Especialista em IA · Módulo 03 · Machine Learning clássico (M4) · Aula 06 de 24 · com laboratório no Colab
Transformar um preditor linear em probabilidade, treiná-lo pela log-loss e separar a probabilidade estimada da decisão tomada com ela. Pré-requisitos: Aula 05 — Ridge, Lasso e Elastic Net, Aula 03 — Pré-processamento, pipelines e data leakage e a linguagem de eventos de probabilidade para IA.

Um sistema antifraude estima risco de 18% para uma transação e de 73% para outra. Nenhum dos dois números é uma decisão. Bloquear, mandar para revisão ou aprovar depende de quanto custa cada tipo de erro e de quantos casos a equipe consegue revisar por dia. Uma fraude que passa pode custar cinco vezes mais que uma revisão desnecessária, e aí 18% já pode ser risco demais. Para tomar essa decisão com critério, é preciso saber de onde vem o 18%, o que ele promete e o que ele não promete.

Nos artigos anteriores da série, o alvo era contínuo: a regressão linear previa um número, e a regularização controlava a complexidade desse modelo. Agora o alvo é binário: fraude ou não fraude, falha ou operação normal, inadimplência ou pagamento. A regressão logística mantém o preditor linear e o liga a uma probabilidade pela função sigmoide. Ao final, você vai conseguir passar de logit para odds e para probabilidade, ler um coeficiente como razão de odds, derivar a log-loss e o gradiente $X^T(p-y)/n$, calcular a perda sem estourar o ponto flutuante e escolher um limiar pelo custo dos erros sem tocar no teste. Esta aula fecha o bloco de modelos lineares da série.

💡
Ideia-chave: a regressão logística termina numa probabilidade $p$. A decisão começa depois, quando $p$ é comparada a um limiar escolhido pelo custo dos erros e pela capacidade de operação. Misturar as duas coisas, como tratar 0,5 como regra universal, é o erro mais comum com classificadores probabilísticos.

1. Por que não usar regressão linear num alvo 0 ou 1

Nada impede ajustar mínimos quadrados a um alvo que vale 0 ou 1. O problema aparece nas pontas: a reta não respeita os limites de uma probabilidade e pode prever $-0{,}14$ ou $1{,}12$, como na figura abaixo. Há também um problema de estrutura. Num alvo Bernoulli, a variância depende da média:

$$ \operatorname{Var}(Y\mid X=x)=p(x)\,[1-p(x)]. $$

Aqui, $p(x)=P(Y=1\mid X=x)$ é a probabilidade do evento para quem tem features $x$. A variância é máxima em $p=0{,}5$ e cai a zero nas pontas, enquanto a regressão linear clássica supõe variância constante.

Gráfico com 160 pontos de alvo 0 ou 1 ao longo de uma feature de menos 4 a 4. Uma reta violeta de mínimos quadrados começa em menos 0,14 e termina em 1,12, saindo das faixas vermelhas abaixo de 0 e acima de 1. Uma curva ciano em forma de S, a regressão logística, fica sempre entre 0 e 1
Os mesmos dados sintéticos, dois modelos. A reta passa de 1 e fica abaixo de 0 nas pontas; a sigmoide, não.

A regressão logística resolve isso em duas etapas. Primeiro, modela uma quantidade sem limites, as log-odds, como função linear das features. Depois, transforma essa quantidade numa probabilidade válida. A cadeia completa vai das features até a ação:

Fluxo de seis caixas ligadas por setas: features x; logit z igual a w transposto x mais b, qualquer real; sigmoide; probabilidade p igual a P de Y igual a 1 dado x, entre 0 e 1; política, que compara p com o limiar tau ou pega os top-k; ação, aprovar, revisar ou bloquear
As quatro primeiras caixas são o modelo. As duas últimas são a política de decisão, que pode mudar sem retreinar nada.

Essa separação permite usar o mesmo modelo com limiares diferentes: um para revisão manual, outro para bloqueio automático, outro para ordenar uma fila. Se o modelo devolvesse direto a classe, essa flexibilidade se perderia.

2. Do preditor linear à sigmoide

Para uma observação $x\in\mathbb{R}^d$, calculamos o logit:

$$ z=w^Tx+b, $$

onde $w$ contém os $d$ coeficientes, $b$ é o intercepto e $w^Tx$ é o produto escalar entre pesos e features. Como $z$ pode ser qualquer número real, aplicamos a função sigmoide:

$$ p=P(Y=1\mid X=x)=\sigma(z)=\frac{1}{1+e^{-z}}. $$

Propriedades que vamos usar o tempo todo:

  • $\sigma(0)=0{,}5$: logit zero significa odds iguais;
  • quando $z\to+\infty$, $\sigma(z)\to1$; quando $z\to-\infty$, $\sigma(z)\to0$;
  • simetria: $\sigma(-z)=1-\sigma(z)$;
  • derivada: $\sigma'(z)=\sigma(z)\,[1-\sigma(z)]$, que vale no máximo $0{,}25$, em $z=0$.

Alguns valores: $\sigma(-4)\approx0{,}018$ (evento pouco provável), $\sigma(-1)\approx0{,}269$, $\sigma(0)=0{,}500$, $\sigma(1)\approx0{,}731$ e $\sigma(4)\approx0{,}982$ (evento muito provável).

Curva sigmoide ciano de z igual a menos 8 a 8, com pontos marcados em 0,018, 0,269, 0,500, 0,731 e 0,982 para z igual a menos 4, menos 1, 0, 1 e 4. Uma curva violeta tracejada mostra a derivada, com pico de 0,25 em z igual a 0. À direita, duas barras: subir o logit de 0 para 1 aumenta p em 23,1 pontos percentuais; de 8 para 9, em 0,02 ponto
A mesma unidade a mais no logit vale 23,1 pontos percentuais perto de $z=0$ e quase nada perto de $z=8$.

A sigmoide satura nas pontas. Por isso, o efeito de uma feature sobre $p$ depende do ponto em que a observação já está: perto de $z=0$, uma unidade a mais no logit muda muito a probabilidade; perto de $z=8$, quase não muda. É uma diferença importante em relação à regressão linear, em que o efeito de uma unidade a mais é sempre o mesmo.

3. Odds e log-odds

As odds de um evento são a razão entre a probabilidade de ocorrer e a de não ocorrer:

$$ \operatorname{odds}(p)=\frac{p}{1-p}. $$

Se $p=0{,}75$, as odds são $0{,}75/0{,}25=3$: em repetições do mesmo cenário, esperamos três ocorrências para cada não ocorrência. Probabilidade e odds não são a mesma coisa. Partindo da sigmoide e isolando $z$:

$$ \begin{gathered} p=\frac{1}{1+e^{-z}} \;\Longrightarrow\; \frac{p}{1-p}=e^{z}\\ \Longrightarrow\; \log\frac{p}{1-p}=z. \end{gathered} $$

O logit é, literalmente, o logaritmo das odds. A hipótese estrutural da regressão logística, portanto, é

$$ \log\frac{p(x)}{1-p(x)}=w^Tx+b. $$

O modelo não afirma que a probabilidade é linear nas features. Afirma que as log-odds são.

Dois gráficos lado a lado em função da probabilidade p. À esquerda, as odds p sobre 1 menos p crescem de 0 e disparam perto de p igual a 1, com o ponto p igual a 0,75 e odds igual a 3 marcado. À direita, as log-odds vão de valores negativos grandes a positivos grandes, cruzam zero em p igual a 0,5 e marcam log 3 igual a 1,099 em p igual a 0,75
As odds vão de 0 a infinito e são assimétricas; as log-odds ocupam a reta inteira e são simétricas em torno de $p=0{,}5$.

4. Como ler um coeficiente

Mantidas as outras features fixas, aumentar $x_j$ em uma unidade soma $w_j$ às log-odds. Exponenciando os dois lados:

$$ \frac{\operatorname{odds}(x_j+1)}{\operatorname{odds}(x_j)}=e^{w_j}. $$

Assim, $e^{w_j}$ é uma razão de odds condicional, sob o modelo. Se $w_j=0{,}40$, então $e^{0{,}40}\approx1{,}492$: cada unidade a mais em $x_j$ multiplica as odds por cerca de 1,492. São 49,2% a mais nas odds, não na probabilidade. Partindo de $p=20\%$, as odds são $0{,}25$; multiplicadas por 1,492, viram $0{,}373$, e a nova probabilidade é

$$ p'=\frac{0{,}373}{1+0{,}373}\approx0{,}272. $$

O aumento foi de 7,2 pontos percentuais, não de 49,2. Partindo de outro ponto, o mesmo coeficiente produz outro aumento:

Curva em forma de arco do aumento da probabilidade, em pontos percentuais, contra a probabilidade inicial, para um coeficiente 0,40. Começa perto de zero, passa por mais 1,0 ponto em 2%, mais 7,2 pontos em 20%, chega ao máximo de mais 10,0 pontos perto de 45% e cai a mais 0,7 ponto em 98%
Com $w=0{,}40$, as odds sempre ficam 1,492 vezes maiores. Em pontos percentuais, o ganho vai de quase nada nas pontas a 10 pontos perto de 45%.

No laboratório da aula, as features entram padronizadas, então uma unidade passa a ser um desvio-padrão do treino. A feature frequencia recebeu coeficiente 1,462, e um desvio-padrão a mais multiplica as odds por 4,31. A feature valor_centavos recebeu −1,008, e um desvio-padrão a mais multiplica as odds por 0,365. Padronizar facilita comparar magnitudes, mas muda a unidade da leitura. Codificação, interações e correlação também mexem nos coeficientes: no mesmo laboratório, sinal_redundante tem correlação 0,958 com frequencia e ficou com o menor coeficiente (0,182). Com duas features quase iguais, separar o efeito de cada uma fica difícil, e o coeficiente de uma só deve ser lido junto com o da outra. E, como nos modelos anteriores da série, uma associação condicional não identifica causa.

5. Da Bernoulli à log-loss

Cox (1958) tratou de testes e estimativas para sequências de 0 e 1 em que a chance de cada 1 depende de variáveis explicativas, revisando trabalhos anteriores sobre o tema. Na forma que usamos hoje, o modelo cabe em uma linha. Para cada observação,

$$ \begin{gathered} Y_i\mid X_i=x_i\sim\operatorname{Bernoulli}(p_i),\\ p_i=\sigma(w^Tx_i+b). \end{gathered} $$

A probabilidade de observar $y_i\in\{0,1\}$ se escreve numa fórmula só:

$$ P(Y_i=y_i\mid x_i)=p_i^{\,y_i}(1-p_i)^{1-y_i}. $$

Se $y_i=1$, sobra $p_i$; se $y_i=0$, sobra $1-p_i$. Supondo observações condicionalmente independentes, a verossimilhança é o produto

$$ L(w,b)=\prod_{i=1}^{n}p_i^{\,y_i}(1-p_i)^{1-y_i}. $$

Um produto de muitas probabilidades pequenas some no ponto flutuante (underflow). Tomando o logaritmo, o produto vira soma:

$$ \begin{aligned} \log L(w,b)=\sum_{i=1}^{n}\big[\,&y_i\log p_i\\ &+(1-y_i)\log(1-p_i)\big]. \end{aligned} $$

Maximizar a log-verossimilhança é o mesmo que minimizar sua média com sinal trocado, a binary cross-entropy (BCE), também chamada de log-loss:

$$ \begin{aligned} J(w,b)=-\frac{1}{n}\sum_{i=1}^{n}\big[\,&y_i\log p_i\\ &+(1-y_i)\log(1-p_i)\big]. \end{aligned} $$

Cada observação paga $-\log$ da probabilidade que o modelo deu ao que de fato aconteceu.

Duas curvas de perda em função da probabilidade prevista. A ciano, menos log p, para casos positivos, é alta perto de p igual a 0 e cai a zero em p igual a 1, com pontos em p 0,6 perda 0,511 e p 0,9 perda 0,105. A violeta, menos log de 1 menos p, para casos negativos, é o espelho, com ponto em p 0,802 perda 1,620
Acertar com confiança custa pouco; errar com confiança custa muito, e a perda cresce sem limite.

Duas observações positivas com $p=0{,}9$ e $p=0{,}6$ pagam $0{,}105$ e $0{,}511$: a segunda deu menos probabilidade ao que ocorreu. Uma observação negativa com $p\approx0{,}802$ paga $1{,}620$. A log-loss, portanto, não premia só o lado certo do limiar. Ela incentiva o modelo a representar a incerteza com honestidade.

6. Estabilidade numérica

A fórmula da seção anterior é correta e frágil. Para um logit muito negativo, $e^{-z}$ transborda; para um logit muito positivo, $p$ arredonda para exatamente 1 e $\log(1-p)$ vira $-\infty$. A saída é escrever a perda direto no logit. Como $\log\sigma(z)=-\log(1+e^{-z})$ e $\log[1-\sigma(z)]=-\log(1+e^{z})$, a perda de uma observação fica

$$ \ell(z,y)=\log\!\left(1+e^{z}\right)-y\,z. $$

O termo $\log(1+e^{z})$ é a função softplus. Em NumPy, np.logaddexp(0, z) calcula esse valor sem formar $e^{z}$ gigante:

perda = np.mean(np.logaddexp(0.0, logits) - y * logits)
Perda em função do logit z de menos 8 a 8. A curva ciano, para y igual a 1, cai quase em linha reta da esquerda até perto de zero à direita; a violeta, para y igual a 0, é o espelho. Linhas pontilhadas mostram que, longe de zero, a perda de um erro confiante cresce como o módulo de z. Uma nota em âmbar diz que, em float64, e elevado a z estoura acima de z aproximadamente 709
Escrita no logit, a perda é suave e cresce em linha reta para erros confiantes. Nada de exponencial gigante nem de $\log 0$.

A seção 11 mostra a diferença em código: com logits de ±1.000, a conta ingênua devolve perda infinita, e a estável devolve um número finito. Recortar probabilidades (por exemplo, para o intervalo $[10^{-15},\,1-10^{-15}]$) serve para calcular uma métrica, mas não substitui a formulação estável durante a otimização.

7. O gradiente $p-y$

Derivando a perda de uma observação em relação ao logit:

$$ \frac{\partial\ell}{\partial z}=\sigma(z)-y=p-y. $$

Pela regra da cadeia, a derivada da BCE em relação a $p$ vale $(p-y)/[p(1-p)]$, e $\partial p/\partial z=p(1-p)$: os fatores se cancelam. Como $z=Xw+b$, para o conjunto inteiro,

$$ \begin{gathered} \nabla_w J=\frac{1}{n}X^T(p-y),\\ \frac{\partial J}{\partial b}=\frac{1}{n}\sum_{i=1}^{n}(p_i-y_i). \end{gathered} $$

O vetor $p-y$ é o erro probabilístico com sinal: positivo quando o modelo superestimou o evento, negativo quando subestimou. O gradiente propaga esse erro para cada feature, ponderado pelo valor dela.

Curva de BCE de treino ao longo de 8.000 iterações de gradiente descendente. Começa em log 2 igual a 0,693147 e cai quase verticalmente nas primeiras dezenas de iterações até estabilizar em 0,429826, marcado por uma linha tracejada violeta, o mesmo ótimo do scikit-learn
Gradiente descendente com essa fórmula nos dados do laboratório: chega ao mesmo ótimo do scikit-learn.

Duas consequências práticas. A primeira: a BCE logística é convexa em $w$ e $b$, então não há mínimos locais ruins, e métodos numéricos chegam ao ótimo global quando ele existe. A segunda sai da derivada em $b$. No ótimo sem penalidade (e também com penalidade só em $w$, como no solver lbfgs, que não penaliza o intercepto), ela é zero, ou seja, $\frac{1}{n}\sum_i p_i=\frac{1}{n}\sum_i y_i$: a probabilidade média prevista no treino reproduz exatamente a prevalência do treino. A documentação do scikit-learn chama isso de balance property (scikit-learn developers, 2026c); a tabela de solvers indica que só o liblinear penaliza o intercepto (scikit-learn developers, 2026a). O bloco de código da seção 12 confere as duas coisas.

A mesma estrutura, erro $p-y$ propagado para os pesos, reaparece em redes neurais com sigmoide ou softmax na saída. É por isso que vale entender bem este caso pequeno.

8. Regularização e separação perfeita

Convexa não quer dizer que o ótimo sempre exista. Se uma reta (ou um hiperplano) separa perfeitamente as classes no treino, multiplicar $w$ por um número maior mantém todos os sinais corretos e empurra as probabilidades para 0 e 1. A log-loss continua caindo sem nunca chegar a um mínimo finito, e os coeficientes crescem sem limite. Albert e Anderson (1984) classificaram os padrões possíveis dos dados em separação completa, separação quase completa e sobreposição, e mostraram que a existência da estimativa de máxima verossimilhança depende desse padrão.

Dois gráficos em escala logarítmica de iterações, para 40 pontos separados por x igual a 0. À esquerda, o módulo do coeficiente: sem penalidade, a curva violeta sobe sem parar até 9,2 e continua subindo; com penalidade L2, a ciano estabiliza em 2,65. À direita, a log-loss de treino: sem penalidade, cai em linha reta até 0,0002 e segue caindo; com L2, estabiliza
Sem penalidade, $|w|$ só cresce e a perda tende a zero. Com $\lambda=0{,}01$, os dois se estabilizam.

O que a aula de regularização ensinou continua valendo. Com penalidade $L_2$:

$$ J_{L_2}(w,b)=J(w,b)+\frac{\lambda}{2}\|w\|_2^2. $$

O intercepto normalmente fica fora da penalidade. A regularização limita os coeficientes, ajuda com features colineares e garante uma solução finita no caso separável.

No scikit-learn, a LogisticRegression regulariza por padrão, e a força vem do parâmetro C, que é o inverso da força: C maior significa penalidade mais fraca, e C=np.inf desliga a penalidade (scikit-learn developers, 2026a). Sem pesos por amostra nem por classe, a função que a biblioteca minimiza no caso $L_2$ é a log-loss média mais $\frac{1}{nC}\cdot\frac{1}{2}\|w\|_2^2$. Comparando com a fórmula acima, $\lambda=1/(nC)$. Com C=1 e 1.012 linhas de treino, como no laboratório, $\lambda\approx0{,}001$. Como $\lambda$ depende de $n$, o mesmo C regulariza menos quando o modelo é reajustado com mais linhas. No reajuste final, com 1.350 linhas, $\lambda\approx0{,}0007$. A partir da versão 1.8, o tipo de penalidade se escolhe por l1_ratio, e o parâmetro penalty está marcado para sair na 1.10. Registre a versão junto com o valor de C.

Duas regras que vêm das aulas anteriores. Se as features têm escalas diferentes, o StandardScaler fica dentro do pipeline, para aprender médias e desvios só no treino de cada etapa (o porquê está na aula sobre pipelines e vazamento). E C é escolhido por validação no desenvolvimento, nunca pelo teste.

9. Probabilidade não é decisão

A regra binária usual é

$$ \widehat y= \begin{cases} 1, & p\geq\tau,\\ 0, & p\lt\tau, \end{cases} $$

onde $\tau$ é o limiar. O valor 0,5 é só uma convenção. É o corte que o predict do scikit-learn usa (com desigualdade estrita), e a própria documentação avisa que essa regra fixa não é a ideal na maioria dos casos de uso (scikit-learn developers, 2026b). Ela costuma ser inadequada quando custos, prevalência ou capacidade de revisão são assimétricos. Considere os custos do exemplo da abertura:

  • verdadeiro positivo e verdadeiro negativo: custo 0;
  • falso positivo, uma revisão desnecessária: custo $c_{FP}=1$;
  • falso negativo, uma fraude que passou: custo $c_{FN}=5$.

Num conjunto de validação, o custo médio de um limiar é

$$ \widehat C(\tau)=\frac{c_{FP}\,FP(\tau)+c_{FN}\,FN(\tau)}{n_{val}}, $$

onde $FP(\tau)$ e $FN(\tau)$ contam os falsos positivos e falsos negativos com aquele limiar. Há também uma conta de referência. Se $p$ for uma probabilidade calibrada, marcar o caso como positivo custa, em média, $(1-p)\,c_{FP}$, e deixar passar custa $p\,c_{FN}$. Vale marcar quando $p\,c_{FN}\geq(1-p)\,c_{FP}$, isto é, quando

$$ p\;\geq\;\frac{c_{FP}}{c_{FP}+c_{FN}}=\frac{1}{1+5}=\frac{1}{6}\approx0{,}167. $$

Um falso negativo cinco vezes mais caro empurra o limiar para baixo: aceitamos mais alarmes para deixar passar menos fraudes. Na prática, as probabilidades não são perfeitamente calibradas, e por isso o limiar é escolhido pelo custo medido na validação:

Curva do custo médio por caso na validação em função do limiar, de 0,05 a 0,95. O custo é mínimo à esquerda, com ponto em limiar 0,13 e custo 0,4112, e sobe com o limiar até passar de 1,2. Uma linha tracejada violeta marca a regra teórica um sexto, com custo 0,4172; uma pontilhada cinza marca 0,50, com custo 0,6538
Na validação do laboratório, o mínimo da grade (0,13) e a regra teórica (1/6) custam quase o mesmo. O limiar 0,5 custa quase 60% a mais.

A diferença entre 0,13 e 1/6 é pequena perto da distância para 0,5, e um mínimo de grade medido em 338 casos carrega ruído. O ponto não é decorar 0,13: é que o limiar sai de uma matriz de custos definida antes de olhar os resultados e é escolhido na validação. Depois, a política é congelada e só então o teste é usado. Se a equipe só consegue revisar $k$ casos por dia, a política certa pode ser outra: revisar os $k$ maiores riscos, em vez de um limiar fixo. Para automatizar a busca do limiar com validação cruzada interna, o scikit-learn oferece o TunedThresholdClassifierCV (scikit-learn developers, 2026b).

Fluxo de seis passos: separar o teste antes de qualquer escolha; ajustar scaler e modelo no treino, dentro do pipeline; escolher C por validação cruzada no treino, por log-loss; escolher o limiar tau pelo custo médio na validação; reajustar no desenvolvimento inteiro, com C e tau congelados; testar uma vez, com modelo e política já decididos
O protocolo do laboratório. O teste só aparece no último passo.

10. Calibração: o que significa prever 0,7?

Um modelo é calibrado quando, entre os casos aos quais atribui probabilidade perto de 0,7, cerca de 70% são de fato positivos (scikit-learn developers, 2026c). Essa garantia vale para condições comparáveis às do treino. Calibração é diferente de ordenação. Imagine um modelo que dá 0,6 a todos os positivos e 0,4 a todos os negativos. Ele ordena perfeitamente, mas, se todo caso com 0,6 for positivo, prever 60% subestima uma frequência observada de 100%.

A log-loss avalia a distribuição probabilística inteira e pune confiança errada. O Brier score mede o erro quadrático das probabilidades:

$$ \operatorname{Brier}=\frac{1}{n}\sum_{i=1}^{n}(p_i-y_i)^2. $$

Nenhum dos dois mede só calibração: um Brier menor pode vir de um modelo mais discriminativo e pior calibrado (scikit-learn developers, 2026c). Para olhar a calibração, a ferramenta é o diagrama de confiabilidade: agrupa as previsões em faixas e compara a probabilidade média prevista com a frequência observada em cada faixa.

À esquerda, diagrama de confiabilidade no teste com 10 faixas: a curva ciano acompanha a diagonal tracejada até 0,45, fica acima dela na faixa de 0,5 a 0,6 e abaixo nas faixas de 0,7 a 0,9; a faixa de 0,8 a 0,9 previu 0,85 e observou 0,70. À direita, histograma de quantos dos 450 casos caem em cada faixa: 90, 52, 46, 43, 28, 28, 37, 42, 33 e 51
No teste do laboratório, as faixas do meio têm só 28 a 42 casos cada, e a curva oscila em torno da diagonal.

Faixas com poucas observações dão estimativas ruidosas, e o número e o tipo de faixas mudam o desenho. No laboratório, os rótulos são sorteados de um modelo logístico exato, então a especificação está certa; mesmo assim, com 28 a 42 casos por faixa no meio, a curva oscila em torno da diagonal. O diagnóstico de um único teste não substitui monitoramento depois da implantação.

A regressão logística tende a sair razoavelmente calibrada quando bem especificada. A documentação do scikit-learn atribui isso ao uso da ligação canônica (o logit) para a log-loss (scikit-learn developers, 2026c). Mas calibração não é garantida. Relações não lineares mal modeladas, regularização forte e, principalmente, mudança de prevalência podem degradá-la. Se o modelo foi treinado com 42% de positivos e opera num lugar onde há 5%, as probabilidades a posteriori que ele devolve deixam de valer para a nova população. Saerens, Latinne e Decaestecker (2002) propuseram um procedimento para reajustar as saídas às novas probabilidades a priori sem retreinar o modelo. O diagnóstico e os métodos de calibração ficam para a aula 15 da série.

11. Exemplo resolvido, passo a passo

Considere $w=1{,}2$, $x=2$ e $b=-1$.

  • Logit: $z=1{,}2\cdot2-1=1{,}4$.
  • Probabilidade: $p=\sigma(1{,}4)=1/(1+e^{-1{,}4})\approx0{,}8022$.
  • Odds: $p/(1-p)\approx0{,}8022/0{,}1978\approx4{,}055$, que é também $e^{1{,}4}$.
  • Perda se $y=1$: $-\log(0{,}8022)\approx0{,}2204$.
  • Perda se $y=0$: $-\log(1-0{,}8022)\approx1{,}6204$.
  • Decisão: com $\tau=0{,}5$, a classe é positiva; com $\tau=0{,}85$, é negativa. A probabilidade não mudou; só a política.

Em código, junto com a razão de odds da seção 4 e o teste de estabilidade da seção 6:

import numpy as np

def sigmoide(z):
    return 1 / (1 + np.exp(-z))

# Exemplo resolvido: w = 1,2, x = 2, b = -1
z = 1.2 * 2 - 1
p = sigmoide(z)
print(f"logit z = {z:.1f} | p = {p:.4f} | odds = {p / (1 - p):.3f} | e^z = {np.exp(z):.3f}")
print(f"perda se y=1: {-np.log(p):.4f} | perda se y=0: {-np.log(1 - p):.4f}")

# Razão de odds: w_j = 0,40 partindo de p = 20%
odds = 0.20 / 0.80 * np.exp(0.40)
print(f"e^0,40 = {np.exp(0.40):.3f} | nova odds = {odds:.3f} | nova p = {odds / (1 + odds):.3f}")

# Logits extremos: a conta ingênua quebra, a estável não
logits = np.array([-1000.0, 0.0, 1000.0])
y = np.array([1.0, 1.0, 0.0])          # dois erros confiantes e um caso neutro
with np.errstate(over="ignore", divide="ignore", invalid="ignore"):
    p_ing = sigmoide(logits)
    bce_ing = -np.mean(y * np.log(p_ing) + (1 - y) * np.log(1 - p_ing))
bce_est = np.mean(np.logaddexp(0.0, logits) - y * logits)
print("p ingênua:", p_ing, "| BCE ingênua:", bce_ing)
print(f"BCE estável: {bce_est:.4f}")

Saída real, com Python 3.13 e NumPy 2.2:

logit z = 1.4 | p = 0.8022 | odds = 4.055 | e^z = 4.055
perda se y=1: 0.2204 | perda se y=0: 1.6204
e^0,40 = 1.492 | nova odds = 0.373 | nova p = 0.272
p ingênua: [0.  0.5 1. ] | BCE ingênua: inf
BCE estável: 666.8977

A conta ingênua arredonda $\sigma(-1000)$ para 0 e $\sigma(1000)$ para 1, e $\log 0$ leva a perda a infinito: nenhum gradiente útil sai daí. A versão com logaddexp devolve 666,9, que é a média de 1.000, $\log 2$ e 1.000. É um valor grande, como deve ser para dois erros confiantes, mas finito.

12. Na prática: o laboratório em dois blocos

O notebook da aula gera 1.800 transações sintéticas com probabilidade conhecida. São seis features, uma delas quase cópia de outra (correlação 0,958), com unidades que diferem em até quatro ordens de grandeza. O rótulo é sorteado de uma Bernoulli: mesmo com o modelo certo, há incerteza irredutível. O primeiro bloco separa o teste, implementa a regressão logística do zero e confere o gradiente, o ótimo e a balance property:

import numpy as np
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler

SEED = 20260908
rng = np.random.default_rng(SEED)

# Dados sintéticos do laboratório: 6 features, uma quase cópia de outra, escalas muito diferentes
n = 1_800
u = rng.normal(size=(n, 5))
X_base = np.column_stack([u[:, 0], u[:, 1], u[:, 2], u[:, 3],
                          0.82 * u[:, 0] + 0.25 * rng.normal(size=n), u[:, 4]])
X = X_base * np.array([1.0, 100.0, 0.01, 5.0, 1.0, 20.0])
beta = np.array([1.15, -0.85, 0.75, 0.55, 0.35, -0.60])
p_real = 1 / (1 + np.exp(-(-0.55 + X_base @ beta)))
y = rng.binomial(1, p_real)               # o rótulo é sorteado: há ruído mesmo com o modelo certo

# Teste separado antes de qualquer escolha; desenvolvimento = treino + validação
X_dev, X_test, y_dev, y_test = train_test_split(X, y, test_size=0.25, stratify=y, random_state=SEED)
X_tr, X_val, y_tr, y_val = train_test_split(X_dev, y_dev, test_size=0.25, stratify=y_dev, random_state=SEED + 1)
print("treino/validação/teste:", len(X_tr), len(X_val), len(X_test), f"| prevalência {y.mean():.3f}")

def sigmoide_estavel(z):
    out = np.empty_like(z, dtype=float)
    pos = z >= 0
    out[pos] = 1 / (1 + np.exp(-z[pos]))
    ez = np.exp(z[~pos])
    out[~pos] = ez / (1 + ez)
    return out

def perda_e_gradiente(w, b, Z, y):
    z = Z @ w + b
    erro = sigmoide_estavel(z) - y                     # p - y
    perda = np.mean(np.logaddexp(0.0, z) - y * z)
    return perda, Z.T @ erro / len(y), erro.mean()

Z_tr = StandardScaler().fit(X_tr).transform(X_tr)      # scaler aprende só no treino

# 1) Gradiente analítico x diferenças finitas
w0 = rng.normal(0, 0.2, 6); h = 1e-5
_, g_w, _ = perda_e_gradiente(w0, -0.1, Z_tr[:250], y_tr[:250])
g_num = [(perda_e_gradiente(w0 + h * e, -0.1, Z_tr[:250], y_tr[:250])[0]
          - perda_e_gradiente(w0 - h * e, -0.1, Z_tr[:250], y_tr[:250])[0]) / (2 * h) for e in np.eye(6)]
print(f"maior diferença analítico x numérico: {np.max(np.abs(g_w - g_num)):.1e}")

# 2) Gradiente descendente sem penalidade
w, b = np.zeros(6), 0.0
for passo in range(8_000):
    perda, g_w, g_b = perda_e_gradiente(w, b, Z_tr, y_tr)
    w -= 0.25 * g_w
    b -= 0.25 * g_b
perda, g_w, g_b = perda_e_gradiente(w, b, Z_tr, y_tr)
print(f"BCE inicial {np.log(2):.6f} -> final {perda:.6f} | norma do gradiente {np.sqrt(g_w @ g_w + g_b**2):.1e}")

# 3) Mesmo ótimo que o scikit-learn com penalidade desprezível (C enorme)
sk = LogisticRegression(C=1e8, max_iter=10_000, tol=1e-12).fit(Z_tr, y_tr)
print(f"maior diferença de coeficientes para o scikit-learn: {np.max(np.abs(w - sk.coef_[0])):.1e}")

# 4) No ótimo, dJ/db = média(p - y) = 0: a probabilidade média reproduz a prevalência do treino
p_tr = sigmoide_estavel(Z_tr @ w + b)
print(f"média de p no treino {p_tr.mean():.6f} | prevalência no treino {y_tr.mean():.6f}")

Saída real (Python 3.13, NumPy 2.2, scikit-learn 1.9):

treino/validação/teste: 1012 338 450 | prevalência 0.422
maior diferença analítico x numérico: 9.2e-12
BCE inicial 0.693147 -> final 0.429826 | norma do gradiente 6.3e-08
maior diferença de coeficientes para o scikit-learn: 8.0e-06
média de p no treino 0.421937 | prevalência no treino 0.421937

O gradiente analítico e as diferenças finitas diferem em menos de $10^{-11}$. O gradiente descendente sai de $\log 2$, a perda de quem prevê 0,5 para todos, e chega ao mesmo ótimo do scikit-learn, com coeficientes a $8\times10^{-6}$ de distância. No ótimo, a probabilidade média do treino é idêntica à prevalência do treino, como a derivada em $b$ prometia.

O segundo bloco continua do primeiro e faz o protocolo completo: escolhe C por validação cruzada no treino, escolhe o limiar pelo custo na validação, reajusta no desenvolvimento e olha o teste uma vez.

from sklearn.metrics import brier_score_loss, log_loss, roc_auc_score
from sklearn.model_selection import StratifiedKFold, cross_val_score
from sklearn.pipeline import make_pipeline

def modelo(C):
    return make_pipeline(StandardScaler(), LogisticRegression(C=C, max_iter=3_000))

# 1) C escolhido por validação cruzada só no treino (log-loss média em 5 folds)
cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=SEED)
grade = np.logspace(-2, 2, 9)
ll_cv = [-cross_val_score(modelo(C), X_tr, y_tr, scoring="neg_log_loss", cv=cv).mean() for C in grade]
C_best = grade[int(np.argmin(ll_cv))]
print("log-loss CV:", " ".join(f"{v:.4f}" for v in ll_cv), f"-> C = {C_best:g}")

# 2) Limiar escolhido por custo na validação: falso positivo custa 1, falso negativo custa 5
def custo(y, p, t):
    pred = p >= t
    return (1 * np.sum(pred & (y == 0)) + 5 * np.sum(~pred & (y == 1))) / len(y)

p_val = modelo(C_best).fit(X_tr, y_tr).predict_proba(X_val)[:, 1]
limiares = np.round(np.linspace(0.05, 0.95, 91), 2)
custos = [custo(y_val, p_val, t) for t in limiares]
t_best = limiares[int(np.argmin(custos))]
print(f"validação | custo(0.50) = {custo(y_val, p_val, 0.5):.4f} | custo(1/6) = {custo(y_val, p_val, 1/6):.4f}"
      f" | mínimo da grade: custo({t_best:.2f}) = {min(custos):.4f}")

# 3) Modelo e política congelados; reajuste no desenvolvimento e UMA avaliação no teste
final = modelo(C_best).fit(X_dev, y_dev)
assert list(final[-1].classes_) == [0, 1]        # a coluna 1 de predict_proba é o evento y=1
p_test = final.predict_proba(X_test)[:, 1]
base = np.full(len(y_test), y_dev.mean())
print(f"teste     | log-loss {log_loss(y_test, p_test):.4f} (prevalência constante: {log_loss(y_test, base):.4f})"
      f" | Brier {brier_score_loss(y_test, p_test):.4f} | AUC {roc_auc_score(y_test, p_test):.4f}")
print(f"teste     | custo(0.50) = {custo(y_test, p_test, 0.5):.4f} | custo(1/6) = {custo(y_test, p_test, 1/6):.4f}"
      f" | política congelada: custo({t_best:.2f}) = {custo(y_test, p_test, t_best):.4f}")

Saída real:

log-loss CV: 0.4818 0.4498 0.4390 0.4367 0.4366 0.4367 0.4368 0.4368 0.4368 -> C = 1
validação | custo(0.50) = 0.6538 | custo(1/6) = 0.4172 | mínimo da grade: custo(0.13) = 0.4112
teste     | log-loss 0.4466 (prevalência constante: 0.6810) | Brier 0.1467 | AUC 0.8667
teste     | custo(0.50) = 0.6156 | custo(1/6) = 0.3911 | política congelada: custo(0.13) = 0.3822

Algumas leituras:

  • A log-loss de validação cruzada é quase plana de C = 0,3 a 100, e o mínimo da grade ficou em C = 1. Com 1.012 linhas e seis features, a penalidade pouco importa para este modelo.
  • No teste, o modelo bate com folga a referência que prevê sempre a prevalência: log-loss de 0,4466 contra 0,6810. Comparar com essa referência é o mínimo para saber se as probabilidades carregam informação.
  • A política congelada (τ = 0,13) custou 0,3822 por caso no teste, contra 0,6156 com τ = 0,5. No notebook, que imprime a matriz de confusão completa, dá para ver a troca: os falsos negativos caem de 46 para 4, e os falsos positivos sobem de 47 para 152. É exatamente o que os custos pediam.
  • O assert sobre classes_ não é enfeite. O predict_proba devolve as colunas na ordem dos rótulos em classes_ (scikit-learn developers, 2026a). Se o evento de interesse estiver codificado de outro jeito, a "coluna 1" pode ser o evento errado.

13. Armadilhas comuns

Lista numerada de seis armadilhas: chamar logit de probabilidade; ler e elevado a w como aumento da probabilidade; escolher o limiar no teste; tratar 0,5 como universal; calcular a log-loss de forma ingênua; confundir ordenar bem com estar calibrado. Cada item traz uma linha curta de explicação
As seis que mais aparecem. A lista completa vem a seguir.
  1. Chamar logit de probabilidade. $w^Tx+b$ não fica preso a $[0,1]$.
  2. Usar regressão linear e recortar as saídas. Isso não produz o modelo Bernoulli nem a perda correta.
  3. Escolher o limiar no teste. A política fica ajustada ao conjunto que deveria julgá-la.
  4. Tratar 0,5 como universal. Custos e capacidade operacional decidem o limiar.
  5. Ler $e^{w_j}$ como aumento percentual da probabilidade. Ele multiplica odds.
  6. Esquecer qual é a classe positiva. Inverter 0 e 1 inverte a interpretação dos coeficientes e das colunas do predict_proba.
  7. Calcular a BCE de forma instável. Use os logits e logaddexp, ou funções confiáveis da biblioteca.
  8. Ignorar a separação perfeita. Sem penalidade, os coeficientes podem divergir.
  9. Padronizar antes do split. O scaler aprende só no treino de cada etapa.
  10. Tratar ordenação como calibração. AUC alta não garante que 0,7 signifique 70%.
  11. Confiar nas probabilidades depois de mudar a prevalência. Monitore o contexto de implantação.
  12. Tirar causalidade dos coeficientes. O modelo é associativo, condicionado às variáveis observadas.

14. Checklist prático

  • Defini claramente qual evento é $y=1$?
  • Declarei a unidade de análise e o instante da previsão?
  • Separei o teste antes de ajustar modelo, regularização ou limiar?
  • Todo o pré-processamento está dentro da fronteira de treino?
  • A sigmoide e a log-loss estão numericamente estáveis?
  • Escolhi C só no desenvolvimento?
  • Defini custos ou objetivo operacional antes de escolher o limiar?
  • Comparei as probabilidades com uma referência que prevê a prevalência?
  • Reportei ao menos a log-loss e uma medida de decisão coerente com os custos?
  • Inspecionei a confiabilidade e os casos confiantes e errados?
  • Registrei seed, versões, coeficientes e política de decisão?
  • Evitei afirmações causais sem sustentação?

15. Onde isso aparece em IA aplicada

Com mais de duas classes, a sigmoide vira a softmax. Para $K$ classes com logits $z_1,\dots,z_K$,

$$ p_k=\frac{e^{z_k}}{\sum_{j=1}^{K}e^{z_j}}, \qquad \ell=-\log p_{y}, $$

onde $p_y$ é a probabilidade dada à classe correta. Com duas classes e logits $(z,0)$, a softmax devolve $e^{z}/(e^{z}+1)=\sigma(z)$, e a perda vira a log-loss binária deste artigo. O gradiente em relação aos logits continua com a mesma forma: probabilidade prevista menos o indicador da classe correta.

A mesma conta é a perda de treino de modelos de linguagem autorregressivos. No estudo sobre leis de escala, Kaplan et al. (2020) treinam Transformers otimizando a log-verossimilhança autorregressiva, isto é, a cross-entropy média sobre um contexto de 1.024 tokens, e usam essa mesma perda como principal medida de desempenho. O GPT-4 foi pré-treinado na mesma tarefa, prever o próximo token de um documento (OpenAI, 2023), embora o relatório não detalhe a perda. A cada posição, o modelo produz um logit para cada token do vocabulário, a softmax os transforma em probabilidades, e a perda é $-\log$ da probabilidade do token que de fato veio.

Exemplo ilustrativo com logits inventados para o próximo token após A capital da França é. À esquerda, barras violeta de logits: Paris 4,0, Lyon 1,5, Marselha 1,0, Roma 0,5 e Londres 0,2. À direita, as probabilidades da softmax: Paris 0,844 em ciano, e Lyon 0,069, Marselha 0,042, Roma 0,025 e Londres 0,019 em cinza. O texto diz que a cross-entropy é menos log de 0,844, igual a 0,169
Exemplo ilustrativo, com logits inventados: a mesma perda da regressão logística, aplicada a cada token.

A segunda ponte é a calibração, e ela importa mais do que parece quando um sistema usa a confiança do modelo para decidir. Guo et al. (2017) mostraram que redes neurais modernas, ao contrário das de uma década antes, saem mal calibradas, e que, na maioria dos conjuntos de dados testados, o temperature scaling foi surpreendentemente eficaz para corrigir isso. A técnica aplica a softmax a $z/T$, com um único parâmetro $T$ aprendido minimizando a log-loss num conjunto separado; como $T$ não muda qual logit é o maior, a acurácia fica igual (scikit-learn developers, 2026c). Em modelos de linguagem, Kadavath et al. (2022) encontraram modelos grandes bem calibrados em questões de múltipla escolha e verdadeiro ou falso apresentadas no formato certo. O relatório técnico do GPT-4 descreve o modelo pré-treinado como altamente calibrado num subconjunto do MMLU e mostra que o pós-treinamento reduziu essa calibração (OpenAI, 2023). Ou seja, a probabilidade que sai de um modelo grande também precisa ser verificada, não presumida.

A terceira ponte é a separação entre estimativa e política. Um classificador de risco num fluxo de triagem assistida por IA, um filtro de segurança ou um agente que decide se pede confirmação antes de agir usam a mesma arquitetura desta aula: um modelo que devolve uma probabilidade e uma política, com custos explícitos, que decide o que fazer com ela. Trocar a política é barato e auditável. Embutir o limiar no modelo esconde a decisão.

Próximo passo

A próxima aula da série troca a fronteira global por decisões locais com o KNN, os K vizinhos mais próximos. Em vez de aprender um único hiperplano, o modelo decide olhando os exemplos parecidos, e isso torna explícitos o papel da escala, da métrica de distância e da dimensionalidade.

Referências

  1. Cox, D. R. The Regression Analysis of Binary Sequences. Journal of the Royal Statistical Society: Series B (Methodological), 20(2), p. 215–232, 1958. doi.org/10.1111/j.2517-6161.1958.tb00292.x
  2. Albert, A.; Anderson, J. A. On the existence of maximum likelihood estimates in logistic regression models. Biometrika, 71(1), p. 1–10, 1984. doi.org/10.1093/biomet/71.1.1
  3. scikit-learn developers. Logistic regression (User Guide, seção 1.1.11) e classe LogisticRegression. Documentação oficial do scikit-learn 1.9 (acesso em 9 out. 2026), 2026a. scikit-learn.org/stable/modules/linear_model.html#logistic-regression
  4. scikit-learn developers. Tuning the decision threshold for class prediction (User Guide, seção 3.3). Documentação oficial do scikit-learn 1.9 (acesso em 9 out. 2026), 2026b. scikit-learn.org/stable/modules/classification_threshold.html
  5. scikit-learn developers. Probability calibration (User Guide, seção 1.16). Documentação oficial do scikit-learn 1.9 (acesso em 9 out. 2026), 2026c. scikit-learn.org/stable/modules/calibration.html
  6. Saerens, Marco; Latinne, Patrice; Decaestecker, Christine. Adjusting the Outputs of a Classifier to New a Priori Probabilities: A Simple Procedure. Neural Computation, 14(1), p. 21–41, 2002. doi.org/10.1162/089976602753284446
  7. Guo, Chuan; Pleiss, Geoff; Sun, Yu et al. On Calibration of Modern Neural Networks. Proceedings of the 34th International Conference on Machine Learning (ICML 2017), PMLR 70, p. 1321–1330. arXiv:1706.04599, 2017. arxiv.org/abs/1706.04599
  8. Kadavath, Saurav; Conerly, Tom; Askell, Amanda et al. Language Models (Mostly) Know What They Know. Preprint arXiv:2207.05221, 2022. arxiv.org/abs/2207.05221
  9. Kaplan, Jared; McCandlish, Sam; Henighan, Tom et al. Scaling Laws for Neural Language Models. Preprint arXiv:2001.08361, 2020. arxiv.org/abs/2001.08361
  10. OpenAI. GPT-4 Technical Report. Relatório técnico, arXiv:2303.08774, 2023. arxiv.org/abs/2303.08774
Gostou do artigo? Compartilhe. Conhecimento ganha força quando circula.

Esta aula faz parte do AI Lab, o laboratório aberto de estudo da MirandasTech. Código, notebooks e exercícios: 03-machine-learning/aulas/06-regressao-logistica-classificacao-probabilistica.md.

Newsletter

Um texto por semana, com fonte.

Sem spam. Só o que foi revisado e tem referência.

Pronto. Confira a caixa de entrada para confirmar.