Predicción Conformal para la Clasificación de Aprendizaje Automático Desde Cero

Predicción Conformal para la Clasificación de Aprendizaje Automático Desde el Principio

Implementando la predicción conforme para la clasificación sin necesidad de paquetes personalizados

Esta publicación del blog está inspirada en el libro de Chris Molner — Introducción a la Predicción Conforme con Python. Chris es brillante al hacer que las nuevas técnicas de aprendizaje automático sean accesibles para los demás. También recomendaría especialmente sus libros sobre Aprendizaje Automático Explicable.

Un repositorio de GitHub con el código completo se puede encontrar aquí: Predicción Conforme.

¿Qué es la Predicción Conforme?

La predicción conforme es tanto un método de cuantificación de incertidumbre como un método de clasificación de instancias (que se puede ajustar para clases o subgrupos). La incertidumbre se transmite mediante la clasificación en conjuntos de clases potenciales en lugar de predicciones individuales.

La predicción conforme especifica una cobertura, que especifica la probabilidad de que el resultado real esté dentro de la región de predicción. La interpretación de las regiones de predicción en la predicción conforme depende de la tarea. Para la clasificación, obtenemos conjuntos de predicción, mientras que para la regresión obtenemos intervalos de predicción.

A continuación se muestra un ejemplo de la diferencia entre la clasificación ‘tradicional’ (basada en la probabilidad más alta) y la predicción conforme (conjuntos).

La diferencia entre la clasificación 'normal' basada en la clase más probable y la predicción conforme que crea conjuntos de posibles clases.

Las ventajas de este método son:

Cobertura garantizada: Los conjuntos de predicción generados por la predicción conforme vienen con garantías de cobertura del resultado real, es decir, detectarán el porcentaje de valores reales que establezcas como una cobertura mínima objetivo. La predicción conforme no depende de un modelo bien calibrado, lo único que importa es que, al igual que en el aprendizaje automático en general, las nuevas muestras que se clasifiquen deben provenir de distribuciones de datos similares a los datos de entrenamiento y calibración. La cobertura también se puede garantizar en clases o subgrupos, aunque esto requiere un paso adicional en el método que cubriremos.

  • Fácil de usar: Los enfoques de predicción conforme se pueden implementar desde cero con solo unas pocas líneas de código, como haremos aquí.
  • Independiente del modelo: La predicción conforme funciona con cualquier modelo de aprendizaje automático. Utiliza las salidas normales del modelo que prefieras.
  • No requiere volver a entrenar: La predicción conforme se puede usar sin volver a entrenar tu modelo. Es otra forma de ver y utilizar las salidas del modelo.
  • Amplia aplicación: la predicción conforme funciona para la clasificación de datos tabulares, clasificación de imágenes o series temporales, regresión y muchas otras tareas, aunque aquí solo demostraremos la clasificación.

¿Por qué debemos preocuparnos por la cuantificación de incertidumbre?

La cuantificación de incertidumbre es esencial en muchas situaciones:

  • Cuando usamos predicciones del modelo para tomar decisiones. ¿Qué tan seguros estamos de esas predicciones? ¿Es suficiente usar solo la “clase más probable” para la tarea que tenemos?
  • Cuando queremos comunicar la incertidumbre asociada con nuestras predicciones a las partes interesadas, sin hablar de probabilidades ni probabilidades logarítmicas, ¡incluso sin hablar del registro de probabilidades!

Alpha en la predicción conforme — describe la cobertura

La cobertura es clave en la predicción conforme. En la clasificación, es la región normal de datos que habita una clase en particular. La cobertura es equivalente a la sensibilidad o al recuerdo; es la proporción de valores observados que se identifican en los conjuntos de clasificación. Podemos ajustar el área de cobertura para que sea más ajustada o más amplia ajustando 𝛼 (cobertura = 1 — 𝛼).

¡Vamos a programar!

Importar paquetes

import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
from sklearn.datasets import make_blobs
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import train_test_split

Crear datos sintéticos para clasificación

Se generarán datos de ejemplo utilizando el método `make_blobs` de SK-Learn.

n_classes = 3
X, y = make_blobs(n_samples=10000, n_features=2, centers=n_classes, cluster_std=3.75, random_state=42)
# Reducir el tamaño de la primera clase para crear un conjunto de datos desequilibrado
# Establecer una semilla aleatoria de numpy
np.random.seed(42)
# Obtener el índice cuando y es clase 0
class_0_idx = np.where(y == 0)[0]
# Obtener el 30% de los índices de la clase 0
class_0_idx = np.random.choice(class_0_idx, int(len(class_0_idx) * 0.3), replace=False)
# Obtener el índice para todas las demás clases
rest_idx = np.where(y != 0)[0]
# Combinar los índices
idx = np.concatenate([class_0_idx, rest_idx])
# Mezclar los índices
np.random.shuffle(idx)
# Dividir los datos
X = X[idx]
y = y[idx]
# Dividir el conjunto de entrenamiento del modelo
X_train, X_rest, y_train, y_rest = train_test_split(X, y, test_size=0.5, random_state=42)
# Dividir el resto en calibración y prueba
X_Cal, X_test, y_cal, y_test = train_test_split(X_rest, y_rest, test_size=0.5, random_state=42)
# Establecer etiquetas de clase
class_labels = ['azul', 'naranja', 'verde']

# Graficar los datos
fig = plt.subplots(figsize=(5, 5))
ax = plt.subplot(111)
for i in range(n_classes):
    ax.scatter(X_test[y_test == i, 0], X_test[y_test == i, 1], label=class_labels[i], alpha=0.5, s=10)
legend = ax.legend()
legend.set_title("Clase")
ax.set_xlabel("Característica 1")
ax.set_ylabel("Característica 2")
plt.show()
Datos generados (los datos se crean de forma desequilibrada — la clase azul tiene solo aproximadamente el 30% de los puntos de datos de las clases verde o naranja).

Construir un clasificador

Utilizaremos un modelo de regresión logística simple aquí, pero el método puede funcionar con cualquier modelo, desde un modelo de regresión logística simple basado en datos tabulares hasta ConvNets 3D para clasificación de imágenes.

# Construir y entrenar el clasificador
classifier = LogisticRegression(random_state=42)
classifier.fit(X_train, y_train)
# Probar el clasificador
y_pred = classifier.predict(X_test)
accuracy = np.mean(y_pred == y_test)
print(f"Precisión: {accuracy:0.3f}")
# Probar la recuperación para cada clase
for i in range(n_classes):
    recall = np.mean(y_pred[y_test == i] == y_test[y_test == i])
    print(f"Recuperación para la clase {class_labels[i]}: {recall:0.3f}")

Precisión: 0.930
Recuperación para la clase azul: 0.772
Recuperación para la clase naranja: 0.938
Recuperación para la clase verde: 0.969

Observa cómo la recuperación para la clase minoritaria es menor que las demás clases. La recuperación, también conocida como sensibilidad, es el número en una clase que se identifica correctamente por el clasificador.

S_i, o el puntaje de no conformidad score

En la predicción conforme, el puntaje de no conformidad, a menudo denotado como s_i, es una medida de cuánto se desvía una nueva instancia de las instancias existentes en el conjunto de entrenamiento. Se utiliza para determinar si una nueva instancia pertenece a una clase en particular o no.

En el contexto de la clasificación, la medida de no conformidad más común es 1 menos la probabilidad de clase predicha para la etiqueta dada. Por lo tanto, si la probabilidad predicha de que una nueva instancia pertenezca a una cierta clase es alta, el puntaje de no conformidad será bajo, y viceversa.

Para la predicción conforme, obtenemos puntuaciones s_i para todas las clases (nota: solo miramos la salida del modelo para la clase verdadera de una instancia, incluso si tiene una probabilidad de predicción más alta de pertenecer a otra clase). Luego encontramos un umbral de puntuaciones que contiene (o cubre) el 95% de los datos. La clasificación identificará entonces el 95% de las nuevas instancias (siempre y cuando nuestros nuevos datos sean similares a nuestros datos de entrenamiento).

Calcular umbral de predicción conforme

Ahora prediremos las probabilidades de clasificación del conjunto de calibración. Esto se utilizará para establecer un umbral de clasificación para datos nuevos.

# Obtener predicciones para el conjunto de calibración
y_pred = classifier.predict(X_Cal)
y_pred_proba = classifier.predict_proba(X_Cal)
# Mostrar las primeras 5 instancias
y_pred_proba[0:5]

array([[4.65677826e-04, 1.29602253e-03, 9.98238300e-01],
       [1.73428257e-03, 1.20718182e-02, 9.86193899e-01],
       [2.51649788e-01, 7.48331668e-01, 1.85434981e-05],
       [5.97545130e-04, 3.51642214e-04, 9.99050813e-01],
       [4.54193815e-06, 9.99983628e-01, 1.18300819e-05]])

Calcular puntuaciones no conformes:

Calcularemos las puntuaciones s_i solo basándonos en las probabilidades asociadas con la clase observada. Para cada instancia, obtendremos la probabilidad predicha para la clase de esa instancia. La puntuación s_i (no conformidad) es 1 menos la probabilidad. Cuanto mayor sea la puntuación s_i, menos se ajustará ese ejemplo a esa clase en comparación con otras clases.

si_scores = []
# Recorrer todas las instancias de calibración
for i, true_class in enumerate(y_cal):
    # Obtener probabilidad predicha para la clase observada/verdadera
    predicted_prob = y_pred_proba[i][true_class]
    si_scores.append(1 - predicted_prob)
# Convertir a arreglo NumPy
si_scores = np.array(si_scores)
# Mostrar las primeras 5 instancias
si_scores[0:5]

array([1.76170035e-03, 1.38061008e-02, 2.51668332e-01, 9.49187344e-04,
       1.63720201e-05])

Obtener umbral del percentil 95:

El umbral determina qué cobertura tendrá nuestra clasificación. La cobertura se refiere a la proporción de predicciones que contiene realmente el resultado verdadero.

El umbral es el percentil correspondiente a 1 – 𝛼. Para obtener una cobertura del 95%, establecemos 𝛼 en 0.05.

Cuando se usa en la vida real, el nivel de cuantil (basado en 𝛼) requiere una corrección de muestra finita para calcular el cuantil correspondiente 𝑞. Multiplicamos 0.95 por $(𝑛+1)/𝑛$, lo que significa que 𝑞𝑙𝑒𝑣𝑒𝑙 sería 0.951 para n = 1000.

numero_de_muestras = len(X_Cal)
alpha = 0.05
nivel_q = (1 - alpha) * ((numero_de_muestras + 1) / numero_de_muestras)
umbral = np.percentile(si_scores, nivel_q*100)
print(f'Umbral: {umbral:0.3f}')

Umbral: 0.598

Mostrar gráfico de los valores s_i, con umbral de corte.

x = np.arange(len(si_scores)) + 1
si_scores_ordenados = np.sort(si_scores)
indice_percentil_95 = int(len(si_scores) * 0.95)
# Colorear según el umbral
conformidad = 'g' * indice_percentil_95
noconformidad = 'r' * (len(si_scores) - indice_percentil_95)
color = list(conformidad + noconformidad)
fig = plt.figure(figsize=((6,4)))
ax = fig.add_subplot()
# Agregar barras
ax.bar(x, si_scores_ordenados, width=1.0, color = color)
# Agregar líneas para el percentil 95
ax.plot([0, indice_percentil_95],[umbral, umbral], c='k', linestyle='--')
ax.plot([indice_percentil_95, indice_percentil_95], [umbral, 0], c='k', linestyle='--')
# Agregar texto
txt = 'Umbral de conformidad del percentil 95'
ax.text(5, umbral + 0.04, txt)
# Agregar etiquetas de los ejes
ax.set_xlabel('Instancia de muestra (ordenada por $s_i$)')
ax.set_ylabel('$S_i$ (no conformidad)')
plt.show()
puntuaciones s_i para todos los datos. El umbral es el nivel de s_i que contiene el 95% de todos los datos (si 𝛼 se establece en 0.05).

Obtener muestras/clases del conjunto de prueba clasificadas como positivas

Ahora podemos encontrar todos aquellos resultados del modelo que sean menores que el umbral.

Es posible que un ejemplo individual no tenga ningún valor o más de un valor por debajo del umbral.

prediction_sets = (1 - classifier.predict_proba(X_test) <= umbral)# Mostrar las primeras diez instanciasprediction_sets[0:10]

array([[ True, False, False],       [False, False,  True],       [ True, False, False],       [False, False,  True],       [False,  True, False],       [False,  True, False],       [False,  True, False],       [ True,  True, False],       [False,  True, False],       [False,  True, False]])

Obtener etiquetas del conjunto de predicción y comparar con la clasificación estándar.

# Obtener predicciones estándary_pred = classifier.predict(X_test)# Función para obtener etiquetas del conjunto de prediccióndef obtener_etiquetas_conjunto_prediccion(conjunto_prediccion, etiquetas_clase):    # Obtener conjunto de etiquetas de clase para cada instancia en los conjuntos de predicción    etiquetas_conjunto_prediccion = [        set([etiquetas_clase[i] for i, x in enumerate(conjunto_prediccion) if x]) for conjunto_prediccion in         prediction_sets]    return etiquetas_conjunto_prediccion# Recopilar resultadosresults_sets = pd.DataFrame()results_sets['observado'] = [etiquetas_clase[i] for i in y_test]results_sets['etiquetas'] = obtener_etiquetas_conjunto_prediccion(prediction_sets, etiquetas_clase)results_sets['clasificaciones'] = [etiquetas_clase[i] for i in y_pred]results_sets.head(10)

   observado  etiquetas           clasificaciones0  azul      {azul}           azul1  verde     {verde}          verde2  azul      {azul}           azul3  verde     {verde}          verde4  naranja    {naranja}         naranja5  naranja    {naranja}         naranja6  naranja    {naranja}         naranja7  naranja    {azul, naranja}   azul8  naranja    {naranja}         naranja9  naranja    {naranja}         naranja

Observa que la instancia 7 es en realidad de la clase naranja, pero el clasificador simple la clasifica como azul. La predicción conforme la clasifica como un conjunto de naranja y azul.

Representa los datos que muestran la instancia 7, que se predice que posiblemente pertenezca a 2 clases:

# Representa los datosfig = plt.subplots(figsize=(5, 5))ax = plt.subplot(111)for i in range(n_clases):    ax.scatter(X_test[y_test == i, 0], X_test[y_test == i, 1],               label=etiquetas_clase[i], alpha=0.5, s=10)# Agrega la instancia 7etiqueta_conjunto = results_sets['etiquetas'].iloc[7]ax.scatter(X_test[7, 0], X_test[7, 1], color='k', s=100, marker='*', label=f'Instancia 7')legend = ax.legend()legend.set_title("Clase")ax.set_xlabel("Característica 1")ax.set_ylabel("Característica 2")txt = f"Conjunto de predicción para la instancia 7: {etiqueta_conjunto}"ax.text(-20, 18, txt)plt.show()
Diagrama de dispersión que muestra cómo se clasificó la instancia de prueba 7 como perteneciente a dos posibles conjuntos: {'azul', 'naranja'}

Mostrar cobertura y tamaño promedio del conjunto

La cobertura es la proporción de conjuntos de predicción que realmente contienen el resultado real.

El tamaño promedio del conjunto es el número promedio de clases predichas por instancia.

Definiremos algunas funciones para calcular los resultados.

# Obtener recuentos de clases
def obtener_recuentos_clase(y_test):
    recuentos_clase = []
    for i in range(n_clases):
        recuentos_clase.append(np.sum(y_test == i))
    return recuentos_clase

# Obtener cobertura para cada clase
def obtener_cobertura_por_clase(conjunto_predicciones, y_test):
    cobertura = []
    for i in range(n_clases):
        cobertura.append(np.mean(conjunto_predicciones[y_test == i, i]))
    return cobertura

# Obtener tamaño promedio de conjunto para cada clase
def obtener_tamaño_promedio(conjunto_predicciones, y_test):
    tamaño_promedio = []
    for i in range(n_clases):
        tamaño_promedio.append(
            np.mean(np.sum(conjunto_predicciones[y_test == i], axis=1)))
    return tamaño_promedio

# Obtener cobertura ponderada (ponderada por tamaño de clase)
def obtener_cobertura_ponderada(cobertura, recuentos_clase):
    total_recuentos = np.sum(recuentos_clase)
    cobertura_ponderada = np.sum((cobertura * recuentos_clase) / total_recuentos)
    cobertura_ponderada = round(cobertura_ponderada, 3)
    return cobertura_ponderada

# Obtener tamaño promedio ponderado (ponderado por tamaño de clase)
def obtener_tamaño_promedio_ponderado(tamaño_promedio, recuentos_clase):
    total_recuentos = np.sum(recuentos_clase)
    tamaño_promedio_ponderado = np.sum((tamaño_promedio * recuentos_clase) / total_recuentos)
    tamaño_promedio_ponderado = round(tamaño_promedio_ponderado, 3)
    return tamaño_promedio_ponderado

Mostrar resultados para cada clase.

resultados = pd.DataFrame(index=etiquetas_clase)
resultados['Recuentos de clase'] = obtener_recuentos_clase(y_test)
resultados['Cobertura'] = obtener_cobertura_por_clase(conjunto_predicciones, y_test)
resultados['Tamaño promedio de conjunto'] = obtener_tamaño_promedio(conjunto_predicciones, y_test)
resultados

        Recuentos de clase  Cobertura       Tamaño promedio de conjunto
blue    241                  0.817427       1.087137
orange  848                  0.954009       1.037736
green   828                  0.977053       1.016908

Mostrar resultados generales.

cobertura_ponderada = obtener_cobertura_ponderada(
    resultados['Cobertura'], resultados['Recuentos de clase'])
tamaño_promedio_ponderado = obtener_tamaño_promedio_ponderado(
    resultados['Tamaño promedio de conjunto'], resultados['Recuentos de clase'])
print(f'Cobertura general: {cobertura_ponderada}')
print(f'Tamaño promedio: {tamaño_promedio_ponderado}')

Cobertura general: 0.947
Tamaño promedio: 1.035

NOTA: Aunque nuestra cobertura general es la deseada, muy cercana al 95%, la cobertura de las diferentes clases varía y es la más baja (83%) para nuestra clase más pequeña. Si la cobertura de las clases individuales es importante, podemos establecer umbrales para las clases de manera independiente, que es lo que haremos ahora.

Clasificación conforme con cobertura igual para todas las clases

Cuando queremos asegurarnos de tener cobertura en todas las clases, podemos establecer umbrales para cada clase de manera independiente.

Nota: también podríamos hacer esto para subgrupos de datos, como asegurar una cobertura igual para un diagnóstico en grupos raciales, si encontramos que la cobertura utilizando un umbral compartido causó problemas.

Obtener umbrales para cada clase de manera independiente

# Establecer alpha (1 - cobertura)
alpha = 0.05
umbrales = []
# Obtener probabilidades predichas para conjunto de calibración
y_cal_prob = clasificador.predict_proba(X_Cal)
# Obtener percentil 95 para cada clase de las puntuaciones s
for etiqueta_clase in range(n_clases):
    mascara = y_cal == etiqueta_clase
    y_cal_prob_clase = y_cal_prob[mascara][:, etiqueta_clase]
    puntuaciones_s = 1 - y_cal_prob_clase
    q = (1 - alpha) * 100
    tamaño_clase = mascara.sum()
    corrección = (tamaño_clase + 1) / tamaño_clase
    q *= corrección
    umbral = np.percentile(puntuaciones_s, q)
    umbrales.append(umbral)
print(umbrales)

[0.9030202125697161, 0.6317149025299887, 0.26033562285411]

Aplicar umbral específico de clase a cada clasificación de clase

# Obtener puntuaciones Si para conjunto de prueba
probabilidades_predichas = clasificador.predict_proba(X_test)
puntuaciones_si = 1 - probabilidades_predichas
# Para cada clase, verificar si cada instancia está por debajo del umbral
conjunto_predicciones = []
for i in range(n_clases):
    conjunto_predicciones.append(puntuaciones_si[:, i] <= umbrales[i])
conjunto_predicciones = np.array(conjunto_predicciones).T
# Obtener etiquetas de conjunto de predicción y mostrar las primeras 10
etiquetas_conjunto_prediccion = obtener_etiquetas_conjunto_prediccion(conjunto_predicciones, etiquetas_clase)
# Obtener predicciones estándar
y_pred = clasificador.predict(X_test)
# Agrupar predicciones
resultados_conjuntos = pd.DataFrame()
resultados_conjuntos['observado'] = [etiquetas_clase[i] for i in y_test]
resultados_conjuntos['etiquetas'] = obtener_etiquetas_conjunto_prediccion(conjunto_predicciones, etiquetas_clase)
resultados_conjuntos['clasificaciones'] = [etiquetas_clase[i] for i in y_pred]
# Mostrar los primeros 10 resultados
resultados_conjuntos.head(10)

  observado  etiquetas           clasificaciones
0 blue     {blue}            blue
1 green    {green}           green
2 blue     {blue}            blue
3 green    {green}           green
4 orange   {orange}          orange
5 orange   {orange}          orange
6 orange   {orange}          orange
7 orange   {blue, orange}    blue
8 orange   {orange}          orange
9 orange   {orange}          orange

Verificar cobertura y establecer tamaño en todas las clases

Ahora tenemos cerca de un 95% de cobertura en todas las clases. El método de predicción conforme nos proporciona una mejor cobertura de la clase minoritaria que el método estándar de clasificación.

results = pd.DataFrame(index=class_labels)results['Recuento de clases'] = get_class_counts(y_test)results['Cobertura'] = get_coverage_by_class(prediction_sets, y_test)results['Tamaño promedio del conjunto'] = get_average_set_size(prediction_sets, y_test)results

        Recuento de clases  Cobertura   Tamaño promedio del conjuntoblue    241           0.954357   1.228216orange  848           0.956368   1.139151green   828           0.942029   1.006039

cobertura_ponderada = get_weighted_coverage(    results['Cobertura'], results['Recuento de clases'])tamaño_promedio_ponderado = get_weighted_set_size(    results['Tamaño promedio del conjunto'], results['Recuento de clases'])print (f'Cobertura general: {cobertura_ponderada}')print (f'Tamaño promedio del conjunto: {tamaño_promedio_ponderado}')

Cobertura general: 0.95Tamaño promedio del conjunto: 1.093

Resumen

Se utilizó la predicción conforme para clasificar instancias en conjuntos en lugar de predicciones individuales. Las instancias en los límites entre dos clases se etiquetaron con ambas clases en lugar de elegir la clase con mayor probabilidad.

Cuando es importante que se detecten todas las clases con la misma cobertura, el umbral para clasificar las instancias se puede establecer por separado (este método también se podría utilizar para subgrupos de datos, por ejemplo, para garantizar la misma cobertura en diferentes grupos étnicos).

La predicción conforme no cambia las predicciones del modelo. Simplemente las utiliza de una manera diferente a la clasificación tradicional. Se puede usar junto con métodos más tradicionales.

(Todas las imágenes son del autor)

We will continue to update Zepes; if you have any questions or suggestions, please contact us!

Share:

Was this article helpful?

93 out of 132 found this helpful

Discover more

Inteligencia Artificial

Gorra de Ondas Cerebrales Salva Vidas al Identificar Derrames Cerebrales

El gorro StrokePointer de ondas cerebrales, diseñado por investigadores en los Países Bajos, puede diagnosticar un de...

Inteligencia Artificial

Fiber Óptica Pantalones Inteligentes Ofrecen una Forma de Bajo Costo para Monitorear Movimientos

Los investigadores han desarrollado pantalones inteligentes de fibra óptica de polímeros que pueden rastrear los movi...

Inteligencia Artificial

Arquitecturas de Transformadores y el Surgimiento de BERT, GPT y T5 Una Guía para Principiantes

En el vasto y siempre cambiante reino de la inteligencia artificial (IA), existen innovaciones que no solo dejan huel...

Inteligencia Artificial

Investigadores de Microsoft Research y Georgia Tech revelan los límites estadísticos de las alucinaciones en los modelos de lenguaje

Un problema clave que ha surgido recientemente en los Modelos de Lenguaje es la alta tasa a la que proporcionan infor...

Inteligencia Artificial

Técnica de Machine Learning Mejor para Predecir Tasas de Cura del Cáncer

Un modelo de aprendizaje automático desarrollado por investigadores de la Universidad de Texas en Arlington (UTA) pue...