Spaces:
Sleeping
Sleeping
| """ | |
| Модуль для интерпретации моделей классификации: 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)} создано") | |