Ir al contenido
wannacode

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 triple
double[] 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 honesto
int[] 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:

double[] line = trainLine(ratios, fraud, 0.01, 5000);
System.out.printf("recta, pago 0.4×: %.2f%n", line[0] * 0.4 + line[1]);
System.out.printf("recta, pago 9.0×: %.2f%n", line[0] * 9.0 + line[1]);
// un paso pequeño: 40× es un número grande
double[] ratios2 = Arrays.copyOf(ratios, ratios.length + 1);
ratios2[ratios.length] = 40.0;
int[] fraud2 = Arrays.copyOf(fraud, fraud.length + 1);
fraud2[fraud.length] = 1;
double[] moved = trainLine(ratios2, fraud2, 0.01, 5000);
report("recta", line, ratios2, fraud2);
report("recta con el pago de 40×", moved, ratios2, fraud2);
Salida
recta, pago 0.4×: 0.03
recta, pago 9.0×: 1.24
recta: 0.5 en 3.75×, fraudes no detectados: 2 de 9
recta con el pago de 40×: 0.5 en 7.31×, fraudes no detectados: 5 de 9

La 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) {
return 1 / (1 + Math.exp(-z));
}
for (int z = -4; z <= 4; z += 2) {
System.out.printf("z = %d: %.2f%n", z, sigmoid(z));
}
Salida
z = -4: 0.02
z = -2: 0.12
z = 0: 0.50
z = 2: 0.88
z = 4: 0.98

Por 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) {
return 1 / (1 + Math.exp(-z));
}
static boolean block(double ratio, double w, double b,
double threshold) {
double score = w * ratio + b;
double 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:

double[] m = trainLogistic(ratios, fraud, 0.1, 3000);
double w = m[0], b = m[1];
for (double ratio : new double[] {1.0, 3.0, 5.0}) {
double p = sigmoid(w * ratio + b);
String verdict = block(ratio, w, b, 0.5) ? "bloquear" : "permitir";
System.out.printf("%.1f×: probabilidad de fraude %.2f → %s%n", ratio, p, verdict);
}
Salida
1.0×: probabilidad de fraude 0.06 → permitir
3.0×: probabilidad de fraude 0.46 → permitir
5.0×: probabilidad de fraude 0.91 → bloquear

Un 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[] trainLine(double[] xs, int[] ys, double rate, int epochs) {
double w = 0, b = 0;
int n = xs.length;
for (int epoch = 0; epoch < epochs; epoch++) {
double gw = 0, gb = 0;
for (int i = 0; i < n; i++) {
double p = w * xs[i] + b;
gw += (p - ys[i]) * xs[i];
gb += p - ys[i];
}
w -= rate * gw / n;
b -= rate * gb / n;
}
return new double[] {w, b};
}
static double[] trainLogistic(double[] xs, int[] ys, double rate, int epochs) {
double w = 0, b = 0;
int n = xs.length;
for (int epoch = 0; epoch < epochs; epoch++) {
double gw = 0, gb = 0;
for (int i = 0; i < n; i++) {
double 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 new double[] {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 (int i = 0; i < xs.length; i++) {
double p = sigmoid(w * xs[i] + b);
sum -= ys[i] == 1 ? Math.log(p) : Math.log(1 - p);
}
return sum / xs.length;
}
for (int epochs : new int[] {10, 100, 1000, 3000}) {
double[] m = trainLogistic(ratios, fraud, 0.1, epochs);
System.out.printf("época %d: pérdida %.4f, w = %.4f, b = %.4f%n",
epochs, logLoss(ratios, fraud, m[0], m[1]), m[0], m[1]);
}
Salida
é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.9459

La 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 = Arrays.copyOf(ratios, ratios.length + 1);
ratios2[ratios.length] = 40.0;
int[] fraud2 = Arrays.copyOf(fraud, fraud.length + 1);
fraud2[fraud.length] = 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);
Salida
sigmoide: 0.5 en 3.13×, fraudes no detectados: 1 de 9
sigmoide con el pago de 40×: 0.5 en 3.13×, fraudes no detectados: 1 de 9

La 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) {
return 1 / (1 + Math.exp(-z));
}
static boolean block(double ratio, double w, double b,
double threshold) {
double score = w * ratio + b;
double p = sigmoid(score);
return p >= threshold;
}
Paso a paso, en palabras
  1. Los 20 pagos: de izquierda a derecha está el importe respecto al habitual. Los fraudes arriba (1), los honestos abajo (0).
  2. Una recta por los ceros y los unos: con 9× dice 1,24. Eso no es una probabilidad.
  3. La sigmoide curva la recta entre 0 y 1. Pasa por 0,5 donde la puntuación es 0: en 3,13×.
  4. Cada pago recibe su probabilidad y se coloca en la curva. Los rojos son fraudes, los verdes honestos.
  5. Umbral 0,5: por encima bloqueamos, por debajo dejamos pasar. Fraudes no detectados: 1, clientes honestos bloqueados: 1.
  6. El umbral 0,3 es más estricto: fraudes no detectados: 1, clientes honestos bloqueados: 2.
  7. El umbral 0,7 es más blando: fraudes no detectados: 2, clientes honestos bloqueados: 0.
  8. 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 (int i = 0; i < n; i++) {
    double p = (w * xs[i] + b);
    gw += (p - ys[i]) * xs[i];
    gb += p - ys[i];
}

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;
for (double ratio : new double[] {1.5, 3.0, 3.5}) {
System.out.println(block(ratio, w, b, 0.5) ? "bloquear" : "permitir");
}
Respuesta

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:

for (double threshold : new double[] {0.3, 0.5, 0.7, 0.9}) {
int missed = 0, blocked = 0;
for (int i = 0; i < ratios.length; i++) {
boolean stop = block(ratios[i], w, b, threshold);
if (fraud[i] == 1 && !stop) missed++;
if (fraud[i] == 0 && stop) blocked++;
}
System.out.printf("umbral %.1f: fraudes no detectados %d, honestos bloqueados %d%n",
threshold, missed, blocked);
}
Salida
umbral 0.3: fraudes no detectados 1, honestos bloqueados 2
umbral 0.5: fraudes no detectados 1, honestos bloqueados 1
umbral 0.7: fraudes no detectados 2, honestos bloqueados 0
umbral 0.9: fraudes no detectados 3, honestos bloqueados 0

Un 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 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.

String.format y printf escriben el número según la Locale por defecto. Con una española verás 0,46.

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.

Salida
palabras malas: 0 → 0.05, buena
palabras malas: 1 → 0.20, buena
palabras malas: 2 → 0.52, mala
palabras malas: 3 → 0.83, mala
palabras malas: 4 → 0.96, mala
palabras malas: 5 → 0.99, mala

Una 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

1. ¿Por qué una recta aprendida con las etiquetas 0 y 1 es un mal modelo para el fraude?
2. ¿Qué hace la sigmoide?
3. ¿Qué cambia cuando entrenas la regresión logística en lugar de la recta?
4. El banco baja el umbral de 0,5 a 0,3. ¿Qué pasa?
5. ¿Cuándo la regresión logística no es la herramienta adecuada?

Creado por Ivo Gergov como parte de la campaña de Evo Tech por una educación accesible en programación ·