Clasificación: fraude o no
En resumen: La clasificación binaria responde «sí» o «no»: la regresión logística aprieta un número con la sigmoide hasta una probabilidad entre 0 y 1, y un umbral decide.
Problema
Fraude o no
El cliente pulsa «Pagar». La tienda tiene unos milisegundos para decidir: ¿deja pasar el pago o lo bloquea para revisarlo? Si deja pasar un fraude, pierde la mercancía y paga una comisión por el pago devuelto. Si bloquea a un cliente honesto, el cliente se enfada y compra en otro sitio.
Aquí la respuesta no es un número como el plazo de entrega, sino «fraude» o «no». Tenemos 20 pagos pasados. La característica es el importe respecto al habitual del cliente: 3,0 significa «el triple de lo normal». La etiqueta es 1 para fraude y 0 para un pago honesto:
// el importe respecto al habitual del cliente: 3.0 es el tripledouble[] ratios = [0.4, 0.6, 0.7, 0.9, 1.0, 1.1, 1.2, 1.3, 1.5, 2.0, 2.6, 3.2, 0.8, 3.6, 4.1, 5.0, 6.0, 7.5, 8.2, 9.0];// 1 = fraude, 0 = pago honestoint[] fraud = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1];La primera idea es la recta de la regresión lineal: las etiquetas son 0 y 1, y por encima de 0,5 bloqueamos. Aprendida con el mismo descenso:
var line = TrainLine(ratios, fraud, 0.01, 5000);Console.WriteLine($"recta, pago 0.4×: {line.W * 0.4 + line.B:F2}");Console.WriteLine($"recta, pago 9.0×: {line.W * 9.0 + line.B:F2}");// un paso pequeño: 40× es un número grandedouble[] ratios2 = [.. ratios, 40.0];int[] fraud2 = [.. fraud, 1];var moved = TrainLine(ratios2, fraud2, 0.01, 5000);Report("recta", line, ratios2, fraud2);Report("recta con el pago de 40×", moved, ratios2, fraud2);recta, pago 0.4×: 0.03recta, pago 9.0×: 1.24recta: 0.5 en 3.75×, fraudes no detectados: 2 de 9recta con el pago de 40×: 0.5 en 7.31×, fraudes no detectados: 5 de 9La recta dice 1,24 para un pago de 9×. Eso no es una probabilidad: «124 % de fraude» no significa nada. Lo peor viene ahora. Llega un fraude con 40 veces el importe habitual. Es evidente, y aun así inclina la recta tanto que su frontera pasa de 3,75× a 7,31×. Ahora la recta deja pasar 5 de 9 fraudes en lugar de 2. Un fraude claro empeoró el modelo para todos los demás.
La idea
La respuesta es una clase
Cuando el modelo elige entre unas pocas respuestas («fraude» u «honesto», una reseña «mala» o «buena»), la tarea se llama clasificación, y las respuestas son clases. En la regresión lineal la respuesta era un número sobre una recta. Aquí queremos otra cosa: un número entre 0 y 1 que diga cómo de probable es el fraude. Con él decidimos después.
Necesitamos dos cosas:
- una función que apriete cualquier número entre 0 y 1 sin cortarlo;
- una frontera por encima de la cual bloqueamos el pago.
La idea
La sigmoide
La sigmoide es sigmoid(z) = 1 / (1 + e−z). Una z grande la deja casi en 1, una z negativa grande la deja casi en 0, y z = 0 da exactamente 0,5:
static double Sigmoid(double z) => 1 / (1 + Math.Exp(-z));for (var z = -4; z <= 4; z += 2){ Console.WriteLine($"z = {z}: {Sigmoid(z):F2}");}z = -4: 0.02z = -2: 0.12z = 0: 0.50z = 2: 0.88z = 4: 0.98Por grande que sea z, el resultado nunca pasa de 1. Por eso el pago de 40× no puede «arrastrar» la curva como arrastró la recta: para él la sigmoide ya dice casi 1, y no hay nada más que corregir.
Míralo
Regresión logística
El modelo es la misma recta, pero pasada por la sigmoide. Primero calcula una puntuación (score): w · x + b, como en la regresión lineal. Luego la sigmoide la convierte en una probabilidad p. Al final el umbral decide: p ≥ 0,5 significa «bloquear».
static double Sigmoid(double z) => 1 / (1 + Math.Exp(-z));
static bool Block(double ratio, double w, double b, double threshold){ var score = w * ratio + b; var p = Sigmoid(score); return p >= threshold;}Esto es la regresión logística. El nombre engaña: no predice un número, predice una clase. Se llama «regresión» por la recta que lleva dentro.
Con los w y b aprendidos (cómo, en la tarjeta siguiente), para tres pagos nuevos:
var (w, b) = TrainLogistic(ratios, fraud, 0.1, 3000);foreach (var ratio in new[] { 1.0, 3.0, 5.0 }){ var p = Sigmoid(w * ratio + b); var verdict = Block(ratio, w, b, 0.5) ? "bloquear" : "permitir"; Console.WriteLine($"{ratio:F1}×: probabilidad de fraude {p:F2} → {verdict}");}1.0×: probabilidad de fraude 0.06 → permitir3.0×: probabilidad de fraude 0.46 → permitir5.0×: probabilidad de fraude 0.91 → bloquearUn pago de 3× da 0,46: sospechoso, pero por debajo del umbral. El número entre 0 y 1 es útil por sí mismo. La tienda puede dejar pasar 0,46 sin preguntas, pedir al banco una confirmación (3-D Secure) con 0,6 y bloquear 0,95.
Míralo
Aprendida con el mismo descenso
¿Cómo se encuentran w y b? Exactamente como en la lección del descenso del gradiente: empezamos en cero, calculamos cuánto se equivoca el modelo en cada pago y movemos un poco los pesos en contra de la pendiente. Aquí está el entrenamiento de la recta del problema y, a su lado, el de la logística:
static (double W, double B) TrainLine(double[] xs, int[] ys, double rate, int epochs){ double w = 0, b = 0; int n = xs.Length; for (var epoch = 0; epoch < epochs; epoch++) { double gw = 0, gb = 0; for (var i = 0; i < n; i++) { var p = w * xs[i] + b; gw += (p - ys[i]) * xs[i]; gb += p - ys[i]; } w -= rate * gw / n; b -= rate * gb / n; } return (w, b);}static (double W, double B) TrainLogistic(double[] xs, int[] ys, double rate, int epochs){ double w = 0, b = 0; int n = xs.Length; for (var epoch = 0; epoch < epochs; epoch++) { double gw = 0, gb = 0; for (var i = 0; i < n; i++) { var p = Sigmoid(w * xs[i] + b); // la única línea que cambia gw += (p - ys[i]) * xs[i]; gb += p - ys[i]; } w -= rate * gw / n; b -= rate * gb / n; } return (w, b);}Solo una línea es distinta: la predicción pasa por la sigmoide. El paso (p − y) · x es el mismo. Si a un fraude le tocó 0,9, el error es pequeño y los pesos casi no se mueven. Si le tocó 0,1, el error es grande y el paso también.
Para ver que un modelo aprende, medimos su error. Para «sí» y «no» la medida es la pérdida logarítmica (log loss): cuánto se sorprende el modelo con la respuesta correcta. Por qué no el error cuadrático medio, en «Más» al final.
static double LogLoss(double[] xs, int[] ys, double w, double b){ double sum = 0; for (var i = 0; i < xs.Length; i++) { var p = Sigmoid(w * xs[i] + b); sum -= ys[i] == 1 ? Math.Log(p) : Math.Log(1 - p); } return sum / xs.Length;}foreach (var epochs in new[] { 10, 100, 1000, 3000 }){ var (w, b) = TrainLogistic(ratios, fraud, 0.1, epochs); var loss = LogLoss(ratios, fraud, w, b); Console.WriteLine($"época {epochs}: pérdida {loss:F4}, w = {w:F4}, b = {b:F4}");}época 10: pérdida 0.5895, w = 0.2139, b = -0.1837época 100: pérdida 0.3899, w = 0.5246, b = -1.4580época 1000: pérdida 0.2906, w = 1.1696, b = -3.6490época 3000: pérdida 0.2897, w = 1.2624, b = -3.9459La pérdida baja y se detiene hacia 0,29. Los pesos llegan a w = 1,26 y b = −3,95. La sigmoide pasa por 0,5 donde la puntuación es 0, es decir, en x = −b / w ≈ 3,13×. ¿Y el pago de 40× del problema?
double[] ratios2 = [.. ratios, 40.0];int[] fraud2 = [.. fraud, 1];Report("sigmoide", TrainLogistic(ratios, fraud, 0.1, 3000), ratios2, fraud2);Report("sigmoide con el pago de 40×", TrainLogistic(ratios2, fraud2, 0.1, 3000), ratios2, fraud2);sigmoide: 0.5 en 3.13×, fraudes no detectados: 1 de 9sigmoide con el pago de 40×: 0.5 en 3.13×, fraudes no detectados: 1 de 9La frontera no se mueve. El único fraude no detectado sigue siendo el de 0,8×: un pago pequeño con el que el ladrón comprueba que la tarjeta funciona. Por su importe se parece a los honestos, y ningún umbral sobre este importe lo pillará. Hace falta otra característica (un país nuevo, un dispositivo nuevo, la hora de la noche).
Míralo
La sigmoide y el umbral
static double Sigmoid(double z) => 1 / (1 + Math.Exp(-z));
static bool Block(double ratio, double w, double b, double threshold){ var score = w * ratio + b; var p = Sigmoid(score); return p >= threshold;}Paso a paso, en palabras
- Los 20 pagos: de izquierda a derecha está el importe respecto al habitual. Los fraudes arriba (1), los honestos abajo (0).
- Una recta por los ceros y los unos: con 9× dice 1,24. Eso no es una probabilidad.
- La sigmoide curva la recta entre 0 y 1. Pasa por 0,5 donde la puntuación es 0: en 3,13×.
- Cada pago recibe su probabilidad y se coloca en la curva. Los rojos son fraudes, los verdes honestos.
- Umbral 0,5: por encima bloqueamos, por debajo dejamos pasar. Fraudes no detectados: 1, clientes honestos bloqueados: 1.
- El umbral 0,3 es más estricto: fraudes no detectados: 1, clientes honestos bloqueados: 2.
- El umbral 0,7 es más blando: fraudes no detectados: 2, clientes honestos bloqueados: 0.
- El modelo da una probabilidad y el umbral es una decisión: uno más bajo pilla más fraudes y bloquea más clientes honestos.
La recta sube por encima de 1 y baja por debajo de 0. La sigmoide se queda entre los dos, y la línea horizontal del umbral separa los pagos en «bloquear» y «permitir». Cuando el umbral baja, pasan menos fraudes, pero también se bloquean más clientes honestos.
Pruébalo
Completa
¿Qué única línea convierte el entrenamiento de la recta en regresión logística?
for (var i = 0; i < n; i++)
{
var p = (w * xs[i] + b);
gw += (p - ys[i]) * xs[i];
gb += p - ys[i];
}La predicción pasa por Sigmoid, así que p queda entre 0 y 1. El resto es el mismo descenso: la pendiente es (p − y) · x.
Pruébalo
Predice la salida
El modelo tiene w = 1,26 y b = −3,95, y el umbral es 0,5. ¿Qué imprime para pagos de 1,5×, 3,0× y 3,5×?
double w = 1.26, b = -3.95;foreach (var ratio in new[] { 1.5, 3.0, 3.5 }){ Console.WriteLine(Block(ratio, w, b, 0.5) ? "bloquear" : "permitir");}El umbral 0,5 está donde la puntuación w · x + b vale 0: en x = 3,95 / 1,26 ≈ 3,13. Los pagos de 1,5× y 3,0× quedan por debajo de esa frontera, y el de 3,5× por encima. No hace falta calcular la sigmoide: p ≥ 0,5 exactamente cuando la puntuación es ≥ 0.
La idea
El umbral es una decisión de negocio
El modelo da una probabilidad. Lo que hacemos con ella lo decide el umbral, y el modelo no aprende el umbral. Aquí están los mismos 20 pagos con cuatro umbrales:
foreach (var threshold in new[] { 0.3, 0.5, 0.7, 0.9 }){ int missed = 0, blocked = 0; for (var i = 0; i < ratios.Length; i++) { var stop = Block(ratios[i], w, b, threshold); if (fraud[i] == 1 && !stop) missed++; if (fraud[i] == 0 && stop) blocked++; } Console.WriteLine($"umbral {threshold:F1}: fraudes no detectados {missed}, honestos bloqueados {blocked}");}umbral 0.3: fraudes no detectados 1, honestos bloqueados 2umbral 0.5: fraudes no detectados 1, honestos bloqueados 1umbral 0.7: fraudes no detectados 2, honestos bloqueados 0umbral 0.9: fraudes no detectados 3, honestos bloqueados 0Un umbral bajo es estricto: con 0,3 dejamos pasar 1 fraude, pero bloqueamos a 2 clientes honestos. Un umbral alto es blando: con 0,9 no bloqueamos a ningún cliente honesto, pero dejamos pasar 3 fraudes. No hay umbral sin errores, porque los fraudes y los pagos honestos se solapan.
¿Qué umbral es el correcto? Depende de lo que cueste cada error. Un fraude no detectado de 300 € cuesta 300 € más la comisión. Un cliente honesto bloqueado cuesta una persona enfadada, una llamada a soporte y a veces un cliente perdido. Cómo comparar los dos con números, cuando el fraude es 1 de cada 100, está en la lección siguiente.
Pruébalo
Encuentra el error
Un compañero escribe la sigmoide directamente en block. Sus pruebas usan fraudes y pasan. El viernes la tienda bloquea todos los pagos, también los honestos. ¿En qué línea está el error?
Elige la línea con el error.
La división va antes que la suma: 1 / 1 + e^(−score) es 1 + e−score, y eso siempre es mayor que 1. Así que p ≥ umbral para todos los pagos. Los paréntesis son obligatorios: 1 / (1 + ...). Unas pruebas solo con fraudes no pueden detectar un modelo que bloquea todo. Las pruebas necesitan también pagos honestos.
La idea
Cuándo duele
La regresión logística es una buena primera solución: es rápida, fácil de explicar («lo bloqueamos porque el importe es 6 veces el habitual») y da una probabilidad. Aquí está cuándo no basta o hace daño:
- Hay una sola frontera. El modelo bloquea por encima de un umbral sobre una suma ponderada de las características. Si lo peligroso está en el medio (ni importes muy pequeños ni muy grandes), una frontera no puede separarlo. Necesitas otra característica u otro modelo, por ejemplo un árbol o una red neuronal (el módulo siguiente).
- La probabilidad es tan cierta como los datos. Aquí los fraudes son 8 de 20, porque así tenemos de dónde aprender. En el flujo real son más o menos 1 de cada 100. Un modelo entrenado con un 40 % de fraude ve fraude demasiado a menudo. Eso es la lección siguiente.
- Una regla que es ley no se aprende. «No aceptamos tarjetas de un país sancionado» es un
if, no un modelo. El modelo es para lo que no puedes escribir como regla. - Una característica que no existe en el momento de decidir. «El cliente disputó el pago» es una característica de fraude estupenda, pero llega semanas después del pago. Si está en los datos de entrenamiento, el modelo parece excelente, y en el momento de decidir está ciego.
- Decisiones sobre personas. Crédito, empleo, seguros: ahí el modelo tiene que ser justo con los grupos de personas y explicable. Es un tema aparte, y a menudo lo exige la ley.
Más precisamente, para curiosos
Por qué pérdida logarítmica y no MSE. La pérdida logarítmica de un pago es −log(p) si es fraude y −log(1 − p) si es honesto. Si el modelo dice 0,01 para un fraude real, el error es −log(0,01) ≈ 4,6. Con el MSE el mismo fallo es solo (1 − 0,01)² ≈ 0,98. La pérdida logarítmica castiga mucho más una respuesta equivocada y segura de sí misma. Y la pendiente de la pérdida logarítmica respecto a w es exactamente (p − y) · x, por eso el código es tan corto. Con sigmoide y MSE la pendiente lleva un factor extra p · (1 − p), que se vuelve casi 0 cuando el modelo está seguro y equivocado: el aprendizaje se detiene justo cuando debería ser más rápido.
Por qué los pasos son distintos. La recta se aprende con un paso de 0,01, la regresión logística con 0,1. En la recta el error crece con x, y el pago de 40× da una pendiente enorme: con un paso de 0,1 la recta se dispara. En la sigmoide p − y está entre −1 y 1, así que ni siquiera una x grande la sacude tanto.
Más características. Con varias características (el importe, si es de noche, si el dispositivo es nuevo) la puntuación es w₁ · x₁ + w₂ · x₂ + w₃ · x₃ + b, y el código es el mismo con arreglos de pesos. La frontera es una recta en un plano o un plano en el espacio: sigue siendo una sola.
Más de dos clases. Para «buena / neutra / mala» hay una puntuación por clase, y softmax las convierte en probabilidades que suman 1. Lo encontrarás en los modelos de lenguaje: eligen el siguiente token exactamente así, entre decenas de miles de clases.
En código real. En scikit-learn esto es LogisticRegression, en ML.NET es SdcaLogisticRegression, en Tribuo (Java) es LogisticRegressionTrainer. Por dentro está la misma idea con un descenso más listo y con regularización, que verás dos lecciones después.
:F2 escribe el número según la cultura del sistema. En una computadora con cultura española verás 0,46, y la comprobación de la lección corre con la cultura invariante y un punto. (double W, double B) es una tupla con nombres: la función devuelve dos números a la vez.Ejercicio
Si la reseña es mala
Resuélvelo en tu editor.
Debajo de cada producto hay reseñas. La tienda quiere que las malas lleguen a soporte enseguida. La característica es cuántas palabras de una lista («roto», «devuelto», «tarde»…) tiene la reseña. Tienes 18 reseñas: el número de palabras malas es 0, 0, 1, 0, 2, 0, 1, 0, 1, 3, 2, 3, 4, 1, 5, 3, 2, 4, y las etiquetas (1 = reseña mala) son diez ceros y luego ocho unos. Entrena la regresión logística con un paso de 0,5 durante 2000 épocas e imprime la probabilidad y la decisión (umbral 0,5) para una reseña con de 0 a 5 palabras malas.
palabras malas: 0 → 0.05, buenapalabras malas: 1 → 0.20, buenapalabras malas: 2 → 0.52, malapalabras malas: 3 → 0.83, malapalabras malas: 4 → 0.96, malapalabras malas: 5 → 0.99, malaUna reseña con 2 palabras malas está en el borde: 0,52. Ahí también están las reseñas buenas que dicen «no está roto, pero llegó tarde».
Más tareas
- Otro umbral. Soporte quiere ver todo lo sospechoso. ¿Con qué umbral le llegan también las reseñas con 1 palabra mala?
- Una característica nueva. Añade las estrellas (de 1 a 5) como segunda característica: w₁ · palabras + w₂ · estrellas + b. ¿Cuánto cambia la probabilidad con 2 palabras malas y 5 estrellas?
Test
Ponte a prueba
Lecciones relacionadas
Sección «Lecciones relacionadas»- Se basa en: Parámetros y return · Qué es el aprendizaje automático · Regresión lineal · Descenso del gradiente
- Continúa en: Evaluación: la exactitud miente · La neurona · Capas · Retropropagación
- Ejercicios sobre el tema: Cómo aprende un modelo