NLP_Homework_1 / src /model_interpretation.py
Kolesnikov Dmitry
feat: Попытка навайбкодить 3 и 4 лабораторные
68545bc
"""
Модуль для интерпретации моделей классификации: SHAP, LIME, важность признаков,
визуализация внимания для нейросетей и трансформеров.
"""
from __future__ import annotations
from typing import List, Dict, Any, Optional, Tuple
import numpy as np
import pandas as pd
try:
import shap
SHAP_AVAILABLE = True
except ImportError:
SHAP_AVAILABLE = False
print("⚠️ SHAP не установлен. Установите: pip install shap")
try:
from lime import lime_text
from lime.lime_text import LimeTextExplainer
LIME_AVAILABLE = True
except ImportError:
LIME_AVAILABLE = False
print("⚠️ LIME не установлен. Установите: pip install lime")
try:
import matplotlib.pyplot as plt
import seaborn as sns
MATPLOTLIB_AVAILABLE = True
except ImportError:
MATPLOTLIB_AVAILABLE = False
print("⚠️ Matplotlib не установлен. Визуализация недоступна.")
def get_feature_importance_linear(model, feature_names: Optional[List[str]] = None) -> pd.DataFrame:
"""
Извлекает важность признаков для линейных моделей (LR, SVM).
Args:
model: Обученная модель
feature_names: Названия признаков
Returns:
DataFrame с важностью признаков
"""
if hasattr(model, 'coef_'):
coef = model.coef_
if len(coef.shape) > 1:
# Многоклассовая классификация - берем среднее по классам
importance = np.abs(coef).mean(axis=0)
else:
importance = np.abs(coef)
if feature_names is None:
feature_names = [f"Признак {i}" for i in range(len(importance))]
df = pd.DataFrame({
"Признак": feature_names,
"Важность": importance
}).sort_values("Важность", ascending=False)
return df
return pd.DataFrame()
def get_feature_importance_tree(model, feature_names: Optional[List[str]] = None) -> pd.DataFrame:
"""
Извлекает важность признаков для tree-based моделей (RF, XGBoost, etc.).
Args:
model: Обученная модель
feature_names: Названия признаков
Returns:
DataFrame с важностью признаков
"""
if hasattr(model, 'feature_importances_'):
importance = model.feature_importances_
if feature_names is None:
feature_names = [f"Признак {i}" for i in range(len(importance))]
df = pd.DataFrame({
"Признак": feature_names,
"Важность": importance
}).sort_values("Важность", ascending=False)
return df
return pd.DataFrame()
def get_tfidf_important_words(vectorizer, model, class_idx: int = 0, top_k: int = 20) -> pd.DataFrame:
"""
Извлекает наиболее важные слова для TF-IDF векторизации.
Args:
vectorizer: Обученный векторизатор
model: Обученная модель
class_idx: Индекс класса
top_k: Количество топ-слов
Returns:
DataFrame с важными словами
"""
if not hasattr(model, 'coef_'):
return pd.DataFrame()
coef = model.coef_[class_idx] if len(model.coef_.shape) > 1 else model.coef_
if hasattr(vectorizer, 'get_feature_names_out'):
feature_names = vectorizer.get_feature_names_out()
elif hasattr(vectorizer, 'get_feature_names'):
feature_names = vectorizer.get_feature_names()
else:
return pd.DataFrame()
# Сортируем по важности
indices = np.argsort(np.abs(coef))[-top_k:][::-1]
df = pd.DataFrame({
"Слово": [feature_names[i] for i in indices],
"Коэффициент": [coef[i] for i in indices],
"Абсолютное значение": [np.abs(coef[i]) for i in indices]
})
return df
def explain_with_shap(model, X: np.ndarray,
feature_names: Optional[List[str]] = None,
max_samples: int = 100) -> Optional[shap.Explanation]:
"""
Объяснение предсказаний модели с помощью SHAP.
Args:
model: Обученная модель с методом predict_proba
X: Признаки для объяснения
feature_names: Названия признаков
max_samples: Максимальное количество образцов для объяснения
Returns:
SHAP Explanation объект или None
"""
if not SHAP_AVAILABLE:
print("SHAP не установлен. Установите: pip install shap")
return None
# Ограничиваем количество образцов для производительности
if len(X) > max_samples:
indices = np.random.choice(len(X), max_samples, replace=False)
X_sample = X[indices]
else:
X_sample = X
try:
# Создаем explainer в зависимости от типа модели
if hasattr(model, 'predict_proba'):
explainer = shap.Explainer(model, X_sample)
else:
# Для моделей без predict_proba используем KernelExplainer
explainer = shap.KernelExplainer(model.predict, X_sample)
shap_values = explainer(X_sample)
if feature_names is not None:
shap_values.feature_names = feature_names
return shap_values
except Exception as e:
print(f"Ошибка при создании SHAP объяснения: {e}")
return None
def explain_with_lime_text(model, texts: List[str],
vectorizer: Any,
class_names: Optional[List[str]] = None,
num_features: int = 10) -> List[Dict[str, Any]]:
"""
Объяснение предсказаний модели с помощью LIME для текста.
Args:
model: Обученная модель
texts: Тексты для объяснения
vectorizer: Векторизатор текстов
class_names: Названия классов
num_features: Количество важных признаков для показа
Returns:
Список объяснений для каждого текста
"""
if not LIME_AVAILABLE:
print("LIME не установлен. Установите: pip install lime")
return []
explainer = LimeTextExplainer(class_names=class_names)
def predict_proba_wrapper(texts_list):
"""Обертка для predict_proba с векторизацией."""
X = vectorizer.transform(texts_list)
if hasattr(model, 'predict_proba'):
return model.predict_proba(X)
else:
# Для моделей без predict_proba
predictions = model.predict(X)
# Создаем псевдо-вероятности
proba = np.zeros((len(predictions), len(np.unique(predictions))))
for i, pred in enumerate(predictions):
proba[i, pred] = 1.0
return proba
explanations = []
for text in texts:
try:
explanation = explainer.explain_instance(
text,
predict_proba_wrapper,
num_features=num_features
)
# Извлекаем важные слова
exp_list = explanation.as_list()
explanations.append({
"text": text,
"important_words": exp_list,
"prediction": explanation.predict_proba.argmax() if hasattr(explanation, 'predict_proba') else None
})
except Exception as e:
print(f"Ошибка при объяснении текста: {e}")
explanations.append({
"text": text,
"important_words": [],
"prediction": None
})
return explanations
def visualize_attention_weights(attention_weights: np.ndarray,
tokens: List[str],
save_path: Optional[str] = None) -> None:
"""
Визуализация весов внимания для трансформерных моделей.
Args:
attention_weights: Матрица весов внимания (n_heads, seq_len, seq_len) или (seq_len, seq_len)
tokens: Список токенов
save_path: Путь для сохранения изображения
"""
if not MATPLOTLIB_AVAILABLE:
print("Matplotlib не установлен. Визуализация недоступна.")
return
# Если несколько голов внимания, усредняем
if len(attention_weights.shape) == 3:
attention_weights = attention_weights.mean(axis=0)
# Ограничиваем длину для визуализации
max_len = min(50, len(tokens))
attention_weights = attention_weights[:max_len, :max_len]
tokens = tokens[:max_len]
plt.figure(figsize=(12, 10))
sns.heatmap(
attention_weights,
xticklabels=tokens,
yticklabels=tokens,
cmap='Blues',
cbar=True
)
plt.title("Визуализация весов внимания")
plt.xlabel("Токены")
plt.ylabel("Токены")
plt.xticks(rotation=45, ha='right')
plt.yticks(rotation=0)
plt.tight_layout()
if save_path:
plt.savefig(save_path, dpi=300, bbox_inches='tight')
plt.show()
def analyze_error_cases(y_true: np.ndarray, y_pred: np.ndarray,
texts: Optional[List[str]] = None,
top_k: int = 10) -> pd.DataFrame:
"""
Анализ случаев, где модель ошибается.
Args:
y_true: Истинные метки
y_pred: Предсказанные метки
texts: Тексты (опционально)
top_k: Количество примеров для показа
Returns:
DataFrame с примерами ошибок
"""
errors = y_true != y_pred
error_indices = np.where(errors)[0]
if len(error_indices) == 0:
return pd.DataFrame({"Сообщение": ["Ошибок не найдено"]})
# Ограничиваем количество
if len(error_indices) > top_k:
error_indices = np.random.choice(error_indices, top_k, replace=False)
results = []
for idx in error_indices:
result = {
"Индекс": int(idx),
"Истинный класс": int(y_true[idx]),
"Предсказанный класс": int(y_pred[idx])
}
if texts is not None:
result["Текст"] = texts[idx][:200] + "..." if len(texts[idx]) > 200 else texts[idx]
results.append(result)
return pd.DataFrame(results)
if __name__ == "__main__":
# Тестирование
from sklearn.datasets import make_classification
from sklearn.linear_model import LogisticRegression
from sklearn.feature_extraction.text import TfidfVectorizer
# Создаем тестовые данные
texts = [
"Это положительный отзыв о продукте",
"Отрицательный отзыв не понравилось",
"Нейтральный отзыв нормально",
] * 10
vectorizer = TfidfVectorizer()
X = vectorizer.fit_transform(texts).toarray()
y = np.array([0, 1, 2] * 10)
# Обучение модели
model = LogisticRegression(max_iter=1000, random_state=42)
model.fit(X, y)
# Важность признаков
feature_importance = get_feature_importance_linear(model)
print("Важность признаков (топ-10):")
print(feature_importance.head(10))
# Важные слова для TF-IDF
important_words = get_tfidf_important_words(vectorizer, model, class_idx=0, top_k=10)
print("\nВажные слова для класса 0:")
print(important_words)
# SHAP (если доступен)
if SHAP_AVAILABLE:
shap_values = explain_with_shap(model, X[:5], max_samples=5)
if shap_values is not None:
print("\nSHAP объяснение создано успешно")
# LIME (если доступен)
if LIME_AVAILABLE:
lime_explanations = explain_with_lime_text(model, texts[:3], vectorizer)
print(f"\nLIME объяснения: {len(lime_explanations)} создано")