Image for post Desmitificando la Caja Negra: Explicabilidad de Modelos de IA con LIME y SHAP en Python

Desmitificando la Caja Negra: Explicabilidad de Modelos de IA con LIME y SHAP en Python


Como desarrolladores, a menudo nos encontramos construyendo y desplegando modelos de Inteligencia Artificial que, aunque potentes, pueden parecer cajas negras. Hacen predicciones asombrosas, pero ¿por qué? ¿Qué características influyeron más en una decisión particular? ¿Es el modelo justo? Estas preguntas son cruciales, especialmente cuando la IA se aplica en campos sensibles como las finanzas, la medicina o la justicia. Aquí es donde entra en juego la Explicabilidad de la IA (XAI).

Contexto del Problema

Imagina que has desarrollado un modelo de IA para aprobar o denegar solicitudes de crédito. El modelo funciona con una precisión del 95%, lo cual es excelente. Sin embargo, un cliente es denegado y pregunta: "¿Por qué?". Como desarrollador, tu respuesta no puede ser simplemente "porque el modelo lo dijo". Necesitas entender y comunicar las razones detrás de esa decisión. Sin explicabilidad, te enfrentas a varios desafíos:

  • Falta de Confianza: Si los usuarios o las partes interesadas no entienden cómo funciona un modelo, es difícil que confíen en sus resultados.
  • Dificultad de Depuración: Cuando un modelo comete un error, sin explicabilidad, es casi imposible identificar la causa raíz y corregirla. ¿Fue un problema con los datos, con el entrenamiento, o el modelo aprendió una correlación espuria?
  • Cumplimiento Normativo: En muchas industrias, existen regulaciones (como el GDPR en Europa, que otorga el "derecho a una explicación") que exigen que las decisiones automatizadas sean comprensibles para los afectados.
  • Identificación de Sesgos: Los modelos de IA pueden perpetuar o incluso amplificar sesgos presentes en los datos de entrenamiento. La explicabilidad es una herramienta vital para detectar y mitigar estos sesgos.
  • Mejora del Modelo: Entender qué características son importantes y cómo influyen en las predicciones puede guiarte para mejorar el modelo, ya sea a través de la ingeniería de características o la selección de un modelo diferente.

Para un desarrollador junior-mid, dominar las herramientas de XAI no solo es una habilidad técnica valiosa, sino también un paso fundamental hacia la construcción de sistemas de IA más responsables y robustos.

Conceptos Clave

Antes de sumergirnos en el código, es fundamental comprender algunos conceptos básicos de la explicabilidad de modelos.

¿Qué es la Explicabilidad de la IA (XAI)?

La Explicabilidad de la IA (eXplainable AI) se refiere al conjunto de técnicas y métodos que permiten a los humanos entender el porqué de las decisiones de un modelo de IA. El objetivo es transformar los modelos de "cajas negras" en "cajas transparentes" o, al menos, "cajas de cristal" donde podamos ver y entender su funcionamiento interno o sus razones para una predicción específica.

Explicaciones Locales vs. Globales

Las explicaciones pueden ser de dos tipos principales:

  • Explicaciones Locales: Se centran en entender por qué el modelo hizo una predicción particular para una única instancia de datos. Por ejemplo, "¿Por qué a este cliente específico se le denegó el crédito?". Son cruciales para la confianza del usuario y el cumplimiento normativo.
  • Explicaciones Globales: Buscan comprender cómo funciona el modelo en general, es decir, qué características son importantes en promedio para todas las predicciones. Por ejemplo, "¿Cuáles son los factores más importantes que mi modelo considera al aprobar o denegar créditos?". Son útiles para la depuración, la mejora del modelo y la detección de sesgos generales.

Las herramientas que exploraremos, LIME y SHAP, son capaces de proporcionar ambos tipos de explicaciones, aunque con diferentes enfoques.

LIME: Local Interpretable Model-agnostic Explanations

LIME es una técnica que busca explicar las predicciones de cualquier clasificador o regresor de "caja negra" de forma local y agnóstica al modelo. Su idea central es simple pero poderosa: aproximar el comportamiento del modelo complejo alrededor de una instancia específica con un modelo más simple y transparente (como una regresión lineal o un árbol de decisión).

Así es como funciona LIME:

  1. Selecciona una Instancia: Elige la instancia de datos cuya predicción quieres explicar.
  2. Genera Datos Perturbados: Crea nuevas instancias de datos "vecinas" a la original, perturbando ligeramente sus características.
  3. Pondera las Instancias: Asigna un peso a cada instancia perturbada, siendo mayor el peso cuanto más se parezca a la instancia original.
  4. Predice con el Modelo Original: Usa el modelo de "caja negra" para predecir la salida de todas las instancias perturbadas.
  5. Entrena un Modelo Interpretable Local: Entrena un modelo simple (ej. regresión lineal) con las instancias perturbadas y sus predicciones, usando los pesos asignados. Este modelo simple solo es válido en la vecindad de la instancia original.
  6. Genera la Explicación: La explicación del modelo simple se utiliza para entender la predicción de la instancia original.

Ventajas de LIME:

  • Agnóstico al Modelo: Funciona con cualquier modelo de IA.
  • Interpretable: Las explicaciones son fáciles de entender para los humanos.
  • Local: Se enfoca en la predicción específica, lo cual es útil para la confianza y el cumplimiento.

Desventajas de LIME:

  • Estabilidad: Pequeños cambios en la instancia o en la forma de perturbar los datos pueden llevar a explicaciones ligeramente diferentes.
  • Definición de "Vecindad": La forma en que se define la "cercanía" de las instancias perturbadas puede influir en la explicación.

SHAP: SHapley Additive exPlanations

SHAP es una técnica más robusta y con una base teórica sólida, inspirada en la teoría de juegos cooperativos de Shapley. Su objetivo es calcular la "contribución" de cada característica a la predicción de una instancia, comparándola con una predicción base (o promedio).

La idea central de SHAP es que el valor de Shapley de una característica es la contribución marginal promedio de esa característica a la predicción, considerando todas las posibles combinaciones (coaliciones) de características. En términos más simples, SHAP nos dice cuánto contribuye cada característica a empujar la predicción desde el valor base (predicción promedio) hasta el valor de predicción real para una instancia dada.

Así es como SHAP se relaciona con los valores de Shapley:

Los valores de Shapley garantizan tres propiedades deseables:

  1. Eficiencia: La suma de las contribuciones de las características es igual a la diferencia entre la predicción del modelo y la predicción base.
  2. Simetría: Si dos características contribuyen de manera idéntica en todas las coaliciones, tendrán el mismo valor Shapley.
  3. Ausencia: Una característica que no tiene impacto en la predicción tiene un valor Shapley de cero.

Ventajas de SHAP:

  • Base Teórica Sólida: Derivado de la teoría de juegos, lo que le da una fuerte justificación matemática.
  • Consistencia: Las explicaciones son consistentes y justas en la atribución de la importancia.
  • Explicaciones Locales y Globales: Permite entender tanto predicciones individuales como el comportamiento general del modelo.
  • Visualizaciones Ricas: La librería SHAP ofrece visualizaciones muy intuitivas.

Desventajas de SHAP:

  • Costo Computacional: Calcular los valores de Shapley exactos es NP-hard (computacionalmente inviable) para un gran número de características. Por ello, SHAP utiliza aproximaciones (como KernelExplainer o TreeExplainer).
  • Complejidad Conceptual: La teoría detrás puede ser más difícil de entender inicialmente que LIME.

En resumen, LIME es rápido y agnóstico, ideal para una primera aproximación local. SHAP es más robusto y completo, ofreciendo una visión más profunda tanto local como global, aunque con un costo computacional potencialmente mayor.

Implementación Paso a Paso

Vamos a ver cómo implementar LIME y SHAP con un modelo de clasificación sencillo usando Python. Utilizaremos el popular dataset "Breast Cancer Wisconsin" de Scikit-learn.

1. Preparación del Entorno

Primero, asegúrate de tener las librerías necesarias instaladas. Si no las tienes, puedes instalarlas con pip:


pip install scikit-learn lime shap pandas numpy

2. Carga y Preprocesamiento de Datos

Cargaremos el dataset y lo dividiremos en conjuntos de entrenamiento y prueba. Es importante mantener los nombres de las características para una mejor interpretabilidad.


import pandas as pd
import numpy as np
from sklearn.datasets import load_breast_cancer
from sklearn.model_selection import train_test_split
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import accuracy_score

# Cargar el dataset
data = load_breast_cancer()
X = pd.DataFrame(data.data, columns=data.feature_names)
y = data.target

# Dividir en conjuntos de entrenamiento y prueba
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)

print(f"Dimensiones de X_train: {X_train.shape}")
print(f"Dimensiones de X_test: {X_test.shape}")
print(f"Nombres de las características: {X.columns.tolist()}")

3. Entrenamiento de un Modelo

Entrenaremos un RandomForestClassifier, un modelo de "caja negra" que funciona bien para este tipo de datos.


# Entrenar un RandomForestClassifier
model = RandomForestClassifier(n_estimators=100, random_state=42)
model.fit(X_train, y_train)

# Evaluar el modelo
y_pred = model.predict(X_test)
print(f"Precisión del modelo: {accuracy_score(y_test, y_pred):.4f}")

4. Explicaciones con LIME

Ahora, usaremos LIME para explicar una predicción específica de nuestro modelo. Elegiremos una instancia del conjunto de prueba.


import lime
import lime.lime_tabular

# Seleccionar una instancia para explicar (ej. la primera del conjunto de prueba)
instance_to_explain_idx = 0
instance_to_explain = X_test.iloc[instance_to_explain_idx]

# Crear el explicador de LIME
# feature_names: Nombres de las características para una mejor lectura
# class_names: Nombres de las clases (0: maligno, 1: benigno)
# mode: 'classification' o 'regression'
explainer = lime.lime_tabular.LimeTabularExplainer(
    training_data=X_train.values, # LIME espera un array numpy
    feature_names=X.columns.tolist(),
    class_names=data.target_names.tolist(),
    mode='classification'
)

# Generar la explicación para la instancia seleccionada
# num_features: Cuántas características mostrar en la explicación
explanation = explainer.explain_instance(
    data_row=instance_to_explain.values, # LIME espera un array numpy
    predict_fn=model.predict_proba, # Función que devuelve probabilidades
    num_features=5
)

# Mostrar la predicción del modelo para esta instancia
predicted_class = model.predict(instance_to_explain.to_frame().T)[0]
print(f"\nInstancia a explicar:\n{instance_to_explain.head()}")
print(f"Clase real: {data.target_names[y_test.iloc[instance_to_explain_idx]]}")
print(f"Clase predicha por el modelo: {data.target_names[predicted_class]}")
print(f"Probabilidades predichas: {model.predict_proba(instance_to_explain.to_frame().T)[0]}")

# Imprimir la explicación textual
print("\nExplicación LIME (textual):")
for feature, weight in explanation.as_list():
    print(f"  - {feature}: {weight:.4f}")

# Opcional: Visualizar la explicación en HTML (requiere un entorno con navegador)
# explanation.show_in_notebook(show_table=True, show_all=False)

# Para una visualización más sencilla en consola o si no tienes un entorno de notebook:
print("\nExplicación LIME (formato de lista):", explanation.as_list())

La salida de LIME te mostrará las características que más contribuyeron a la predicción de esa instancia específica, junto con la dirección de su impacto (positivo o negativo). Por ejemplo, un valor positivo para 'mean radius' podría significar que un radio medio grande empujó la predicción hacia la clase 'maligno'.

5. Explicaciones con SHAP

Ahora, aplicaremos SHAP para obtener explicaciones locales y globales.


import shap

# SHAP tiene diferentes "explainers" dependiendo del tipo de modelo.
# Para modelos basados en árboles (RandomForest, XGBoost, LightGBM), TreeExplainer es muy eficiente.
explainer_shap = shap.TreeExplainer(model)

# Calcular los valores SHAP para la instancia seleccionada
# shap_values_instance será una lista de arrays (uno por clase) para clasificadores.
# Por ejemplo, shap_values_instance[0] para la clase 0, shap_values_instance[1] para la clase 1.
shap_values_instance = explainer_shap.shap_values(instance_to_explain)

# Para la clase predicha, podemos usar shap_values_instance[predicted_class]

print(f"\nValores SHAP para la instancia (clase {data.target_names[predicted_class]}):")
# shap.Explanation es una clase que contiene los valores SHAP, valores base, etc.
# Para visualizar, necesitamos un objeto Explanation.
shap_explanation_obj = shap.Explanation(
    values=shap_values_instance[predicted_class],
    base_values=explainer_shap.expected_value[predicted_class],
    data=instance_to_explain.values,
    feature_names=X.columns.tolist()
)

# Visualización local (Force Plot - requiere JS, ideal para notebooks)
# shap.initjs() # Ejecutar una vez al inicio del notebook
# shap.force_plot(explainer_shap.expected_value[predicted_class], shap_values_instance[predicted_class], instance_to_explain)

# Visualización local (Waterfall Plot - más fácil de mostrar en texto/imagen)
# shap.waterfall_plot(shap_explanation_obj)

# Para mostrar en consola, podemos imprimir los valores SHAP y sus características
print("\nExplicación SHAP (valores por característica):")
feature_shap_values = list(zip(X.columns.tolist(), shap_values_instance[predicted_class]))
feature_shap_values.sort(key=lambda x: abs(x[1]), reverse=True)
for feature, value in feature_shap_values:
    print(f"  - {feature}: {value:.4f}")

print(f"Valor base (expected value) para la clase '{data.target_names[predicted_class]}': {explainer_shap.expected_value[predicted_class]:.4f}")
# La suma de los valores SHAP + valor base debería aproximarse a la salida del modelo para la clase predicha.
# Para RandomForest, la salida es la probabilidad (no logit).
# La suma de shap_values[predicted_class] + expected_value[predicted_class] es la salida del modelo para la clase predicha.
print(f"Suma de valores SHAP + valor base: {sum(shap_values_instance[predicted_class]) + explainer_shap.expected_value[predicted_class]:.4f}")
print(f"Probabilidad predicha del modelo para la clase '{data.target_names[predicted_class]}': {model.predict_proba(instance_to_explain.to_frame().T)[0][predicted_class]:.4f}")

# Calcular valores SHAP para todo el conjunto de prueba (o una muestra para grandes datasets)
shap_values_test = explainer_shap.shap_values(X_test)

# Visualización global (Summary Plot - requiere JS, ideal para notebooks)
# shap.summary_plot(shap_values_test[1], X_test, plot_type="bar", class_names=data.target_names.tolist())
# shap.summary_plot(shap_values_test[1], X_test)

# Para mostrar en consola, podemos imprimir la importancia global de las características
print("\nImportancia global de características (SHAP):")
# Calculamos la importancia media absoluta de SHAP para cada característica
# Usamos la clase 1 (benigno) para la importancia global en este ejemplo.
shap_importance = pd.DataFrame({
    'feature': X.columns.tolist(),
    'shap_values': np.abs(shap_values_test[1]).mean(axis=0) 
})
shap_importance = shap_importance.sort_values(by='shap_values', ascending=False)
print(shap_importance)

SHAP proporciona una visión más detallada. El force_plot (si lo ejecutas en un notebook) muestra cómo cada característica empuja la predicción desde el valor base hacia el valor final. El summary_plot te dará una visión global de la importancia de las características y cómo afectan a las predicciones en todo el dataset.

Mini Proyecto / Aplicación Sencilla: Explicando un Clasificador de Cáncer de Mama

Vamos a consolidar lo aprendido construyendo un script que entrene un clasificador y luego genere explicaciones LIME y SHAP para una instancia específica, así como una explicación global SHAP.


import pandas as pd
import numpy as np
from sklearn.datasets import load_breast_cancer
from sklearn.model_selection import train_test_split
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import accuracy_score
import lime
import lime.lime_tabular
import shap
import os

# --- 1. Cargar y Preparar Datos ---
print("\n--- 1. Cargar y Preparar Datos ---")
data = load_breast_cancer()
X = pd.DataFrame(data.data, columns=data.feature_names)
y = data.target

X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)

# --- 2. Entrenar Modelo ---
print("\n--- 2. Entrenar Modelo ---")
model = RandomForestClassifier(n_estimators=100, random_state=42)
model.fit(X_train, y_train)
y_pred = model.predict(X_test)
print(f"Precisión del modelo: {accuracy_score(y_test, y_pred):.4f}")

# --- 3. Seleccionar Instancia para Explicar ---
instance_to_explain_idx = 5 # Elegimos una instancia diferente para variar
instance_to_explain = X_test.iloc[instance_to_explain_idx]
actual_class = data.target_names[y_test.iloc[instance_to_explain_idx]]
predicted_class_idx = model.predict(instance_to_explain.to_frame().T)[0]
predicted_class_name = data.target_names[predicted_class_idx]

print(f"\n--- 3. Instancia Seleccionada para Explicar (Índice: {instance_to_explain_idx}) ---")
print(f"Clase Real: {actual_class}")
print(f"Clase Predicha: {predicted_class_name}")
print(f"Características de la instancia:\n{instance_to_explain.head(7)}")

# --- 4. Explicación Local con LIME ---
print("\n--- 4. Explicación Local con LIME ---")
explainer_lime = lime.lime_tabular.LimeTabularExplainer(
    training_data=X_train.values,
    feature_names=X.columns.tolist(),
    class_names=data.target_names.tolist(),
    mode='classification'
)

explanation_lime = explainer_lime.explain_instance(
    data_row=instance_to_explain.values,
    predict_fn=model.predict_proba,
    num_features=7 # Mostrar más características
)

print(f"Explicación LIME para la predicción '{predicted_class_name}':")
for feature, weight in explanation_lime.as_list():
    print(f"  - {feature}: {weight:.4f}")

# --- 5. Explicación Local con SHAP ---
print("\n--- 5. Explicación Local con SHAP ---")
explainer_shap = shap.TreeExplainer(model)
shap_values_instance = explainer_shap.shap_values(instance_to_explain)

# Para la clase predicha
shap_values_for_predicted_class = shap_values_instance[predicted_class_idx]
expected_value_for_predicted_class = explainer_shap.expected_value[predicted_class_idx]

print(f"Explicación SHAP para la predicción '{predicted_class_name}':")
feature_shap_values = list(zip(X.columns.tolist(), shap_values_for_predicted_class))
feature_shap_values.sort(key=lambda x: abs(x[1]), reverse=True)
for feature, value in feature_shap_values[:7]: # Mostrar las 7 más importantes
    print(f"  - {feature}: {value:.4f}")

print(f"Valor base (expected value) para '{predicted_class_name}': {expected_value_for_predicted_class:.4f}")

# --- 6. Explicación Global con SHAP (Importancia de Características) ---
print("\n--- 6. Explicación Global con SHAP (Importancia de Características) ---")
# Calcular SHAP values para todo el conjunto de prueba
# Esto puede tardar un poco en datasets grandes
shap_values_full_test = explainer_shap.shap_values(X_test)

# Para la importancia global, a menudo nos interesan los valores SHAP para la clase positiva (1 en este caso)
# o la clase que queremos explicar predominantemente.
# Calculamos la importancia media absoluta de SHAP para cada característica para la clase 1 (benigno)
shap_importance_global = pd.DataFrame({
    'feature': X.columns.tolist(),
    'mean_abs_shap_value': np.abs(shap_values_full_test[1]).mean(axis=0) 
})
shap_importance_global = shap_importance_global.sort_values(by='mean_abs_shap_value', ascending=False)

print("Importancia Global de Características (SHAP, media absoluta para clase 'benigno'):")
print(shap_importance_global.head(10).to_string(index=False))

# Nota sobre visualizaciones: Para ver los gráficos interactivos de LIME y SHAP (force_plot, summary_plot, waterfall_plot),
# necesitarías ejecutar este código en un entorno como Jupyter Notebook o Google Colab.
# Por ejemplo, para el summary_plot global:
# shap.summary_plot(shap_values_full_test[1], X_test, plot_type="bar", class_names=data.target_names.tolist())
# shap.initjs() # Solo una vez al inicio del notebook

Este script te permite ejecutar el proceso completo y ver las explicaciones directamente en tu consola. Observa cómo LIME y SHAP identifican características similares como importantes, pero con matices en la forma en que presentan la contribución.

Errores Comunes y Depuración

Trabajar con XAI puede presentar algunos desafíos. Aquí te presento algunos errores comunes y cómo abordarlos:

  • Dimensiones Incorrectas de los Datos: Tanto LIME como SHAP esperan que los datos de entrada (data_row, training_data, X_test) sean arrays de NumPy o DataFrames de Pandas con las dimensiones correctas. Asegúrate de que predict_fn (para LIME) o el modelo (para SHAP) reciban los datos en el formato esperado (ej. .values para NumPy, .to_frame().T para una sola fila de Pandas).
  • Manejo de Características Categóricas: Si tus datos tienen características categóricas, LIME y SHAP necesitan saberlo. Para LIME, puedes usar el parámetro categorical_features en LimeTabularExplainer. Para SHAP, si usas KernelExplainer, el preprocesamiento (como One-Hot Encoding) debe aplicarse antes de calcular los valores SHAP. TreeExplainer puede manejar algunas características categóricas si el modelo subyacente las maneja.
  • Rendimiento de SHAP: Calcular los valores SHAP para un dataset grande con KernelExplainer puede ser muy lento. Si estás usando un modelo basado en árboles (RandomForest, XGBoost, LightGBM), utiliza shap.TreeExplainer, que es mucho más rápido. Para otros modelos, considera muestrear tu conjunto de datos de prueba para calcular los valores SHAP o usar aproximaciones.
  • Interpretación Errónea de las Explicaciones: Recuerda que las explicaciones de LIME y SHAP muestran la contribución de una característica a la predicción, no necesariamente una causalidad. Una característica puede ser importante porque está correlacionada con una característica causal real.
  • Estabilidad de LIME: Debido a su naturaleza de perturbación local, las explicaciones de LIME pueden variar ligeramente entre ejecuciones o si se cambia la semilla aleatoria. Esto es una limitación inherente. SHAP tiende a ser más estable debido a su base teórica.
  • Problemas de Visualización: Las visualizaciones interactivas de LIME y SHAP (como force_plot, summary_plot) están diseñadas para entornos de notebook (Jupyter, Colab) que pueden renderizar JavaScript. Si estás ejecutando scripts Python puros, no verás estos gráficos directamente. Puedes guardar las visualizaciones como archivos HTML o PNG si es necesario.

Aprendizaje Futuro / Próximos Pasos

La explicabilidad de la IA es un campo en constante evolución. Aquí hay algunas áreas que puedes explorar para profundizar tus conocimientos:

  • Otras Librerías y Técnicas XAI: Investiga otras herramientas como ELI5, InterpretML, o Captum (para PyTorch). Cada una tiene sus fortalezas y debilidades, y algunas están más orientadas a tipos específicos de modelos (ej. redes neuronales).
  • Explicabilidad para Modelos de Texto e Imagen: LIME y SHAP tienen variantes para datos no tabulares. Explora cómo se aplican para entender por qué un modelo de procesamiento de lenguaje natural (NLP) clasifica un texto de cierta manera, o por qué un modelo de visión por computadora identifica un objeto en una imagen.
  • XAI en Producción (MLOps): Considera cómo integrar la explicabilidad en tus pipelines de MLOps. Esto podría incluir el monitoreo de las explicaciones a lo largo del tiempo para detectar cambios en el comportamiento del modelo o la deriva de datos.
  • Fairness y Sesgos en IA: La explicabilidad es una herramienta clave para identificar y mitigar sesgos algorítmicos. Profundiza en cómo se pueden usar estas técnicas para evaluar la equidad de tus modelos.
  • Regulaciones y Ética de la IA: Mantente al tanto de las regulaciones emergentes sobre la IA y cómo la explicabilidad juega un papel crucial en el desarrollo de sistemas de IA éticos y responsables.
  • Explicaciones Contrafactuales: ¿Qué tendría que haber cambiado en la entrada para que la predicción fuera diferente? Las explicaciones contrafactuales son otra rama de la XAI que busca responder a esta pregunta.

Dominar la explicabilidad no solo te hará un mejor desarrollador de IA, sino también uno más consciente y responsable. ¡Sigue explorando y construyendo sistemas de IA que no solo sean potentes, sino también comprensibles!