Spaces:
Sleeping
Sleeping
| # src/streamlit_app.py | |
| """ | |
| Веб-интерфейс для интерактивного анализа методов токенизации и нормализации текста. | |
| Позволяет загружать датасеты, выбирать методы обработки и визуализировать результаты. | |
| """ | |
| import os | |
| import sys | |
| import json | |
| import tempfile | |
| from pathlib import Path | |
| from typing import List, Dict, Any, Optional | |
| import streamlit as st | |
| import pandas as pd | |
| import numpy as np | |
| import plotly.express as px | |
| import plotly.graph_objects as go | |
| from plotly.subplots import make_subplots | |
| import matplotlib.pyplot as plt | |
| import seaborn as sns | |
| # Добавляем путь к модулям проекта | |
| _this_file = os.path.abspath(__file__) | |
| _this_dir = os.path.dirname(_this_file) | |
| project_root = os.path.abspath(os.path.join(_this_dir, '..')) | |
| if project_root not in sys.path: | |
| sys.path.insert(0, project_root) | |
| # Импорты наших модулей | |
| from src.text_cleaner import clean_text, clean_corpus_jsonl | |
| from src.universal_preprocessor import UniversalPreprocessor, PreprocessingConfig as UniversalPreprocessingConfig | |
| from src.tokenizers_cmp import TokenizationComparator, load_corpus_from_jsonl | |
| from src.train_subword import SubwordModelTrainer, SubwordModelConfig | |
| from src.classical_vectorizers import ( | |
| VectorizationConfig, | |
| ClassicalVectorizers, | |
| compare_vectorizers, | |
| save_metrics as save_vectorization_metrics, | |
| ) | |
| from src.dimensionality import SVDConfig, run_lsa, embed_2d, explained_variance_table, top_terms_dataframe | |
| from src.embeddings_train import TrainConfig as EmbTrainConfig, train_model as train_embeddings_model, save_model as save_embedding_model, evaluate_neighbors as eval_neighbors, cosine_similarity as eval_cosine, word_analogy as eval_analogy | |
| # Проверяем доступность GloVe | |
| try: | |
| from glove import Glove, Corpus | |
| GLOVE_AVAILABLE = True | |
| except ImportError: | |
| try: | |
| from glove_python import Glove, Corpus | |
| GLOVE_AVAILABLE = True | |
| except ImportError: | |
| GLOVE_AVAILABLE = False | |
| from src.semantic_experiments import vector_arithmetic, semantic_axis, nearest_neighbors | |
| from src.text_preprocessing import TextPreprocessor, PreprocessingConfig, extract_meta_features, vectorize_with_classical, vectorize_with_embeddings | |
| from src.classical_classifiers import ClassicalClassifiers, ClassifierConfig, compare_classifiers, evaluate_classifier | |
| from src.neural_classifiers import NeuralClassifiers, NeuralConfig | |
| from src.imbalance_handling import compute_class_weights, random_oversample, smote_oversample, augment_texts | |
| from src.model_evaluation import evaluate_classifier as eval_classifier_full, cross_validate, grid_search | |
| from src.model_interpretation import get_feature_importance_linear, get_tfidf_important_words, explain_with_shap, explain_with_lime_text | |
| from src.text_to_vector import vectorize_texts | |
| from src.clustering import ClusteringAlgorithms, ClusteringConfig, evaluate_clustering, compare_clustering_methods | |
| # Настройка страницы | |
| st.set_page_config( | |
| page_title="Анализ токенизации текста", | |
| page_icon="🔤", | |
| layout="wide", | |
| initial_sidebar_state="expanded" | |
| ) | |
| # CSS стили | |
| st.markdown(""" | |
| <style> | |
| .main-header { | |
| font-size: 2.5rem; | |
| font-weight: bold; | |
| text-align: center; | |
| margin-bottom: 2rem; | |
| color: #1f77b4; | |
| } | |
| .metric-card { | |
| background-color: #f0f2f6; | |
| padding: 1rem; | |
| border-radius: 0.5rem; | |
| border-left: 4px solid #1f77b4; | |
| } | |
| .success-message { | |
| background-color: #d4edda; | |
| color: #155724; | |
| padding: 1rem; | |
| border-radius: 0.5rem; | |
| border: 1px solid #c3e6cb; | |
| } | |
| .error-message { | |
| background-color: #f8d7da; | |
| color: #721c24; | |
| padding: 1rem; | |
| border-radius: 0.5rem; | |
| border: 1px solid #f5c6cb; | |
| } | |
| </style> | |
| """, unsafe_allow_html=True) | |
| def load_sample_data() -> List[str]: | |
| """Загружает примеры данных для демонстрации.""" | |
| sample_texts = [ | |
| "Это тестовый текст для проверки различных методов токенизации.", | |
| "В России работает множество новостных агентств: РИА Новости, ТАСС, Интерфакс.", | |
| "Компания ООО 'Тест' сообщила о результатах за 2023 год. Контакты: info@test.ru", | |
| "Президент России Владимир Путин провел встречу с министрами в Кремле.", | |
| "Экономика страны показывает стабильный рост на фоне санкций Запада." | |
| ] | |
| return sample_texts | |
| def create_token_distribution_plot(tokens: List[str], method_name: str) -> go.Figure: | |
| """Создает график распределения длин токенов.""" | |
| token_lengths = [len(token) for token in tokens] | |
| fig = go.Figure() | |
| fig.add_trace(go.Histogram( | |
| x=token_lengths, | |
| nbinsx=20, | |
| name=f'Распределение длин токенов ({method_name})', | |
| marker_color='lightblue', | |
| opacity=0.7 | |
| )) | |
| fig.update_layout( | |
| title=f'Распределение длин токенов - {method_name}', | |
| xaxis_title='Длина токена (символы)', | |
| yaxis_title='Количество токенов', | |
| showlegend=False | |
| ) | |
| return fig | |
| def create_frequency_plot(tokens: List[str], method_name: str, top_n: int = 20) -> go.Figure: | |
| """Создает график частотности токенов.""" | |
| from collections import Counter | |
| token_counts = Counter(tokens) | |
| most_common = token_counts.most_common(top_n) | |
| tokens_list, counts_list = zip(*most_common) | |
| fig = go.Figure() | |
| fig.add_trace(go.Bar( | |
| x=list(counts_list), | |
| y=list(tokens_list), | |
| orientation='h', | |
| name=f'Топ-{top_n} токенов ({method_name})', | |
| marker_color='lightcoral' | |
| )) | |
| fig.update_layout( | |
| title=f'Топ-{top_n} наиболее частых токенов - {method_name}', | |
| xaxis_title='Частота', | |
| yaxis_title='Токены', | |
| height=600 | |
| ) | |
| return fig | |
| def create_comparison_chart(results_df: pd.DataFrame) -> go.Figure: | |
| """Создает сравнительную диаграмму методов токенизации.""" | |
| fig = make_subplots( | |
| rows=2, cols=2, | |
| subplot_titles=('Время обработки', 'Размер словаря', 'Коэффициент сжатия', 'Средняя длина токена'), | |
| specs=[[{"type": "bar"}, {"type": "bar"}], | |
| [{"type": "bar"}, {"type": "bar"}]] | |
| ) | |
| # Время обработки | |
| fig.add_trace( | |
| go.Bar(x=results_df['Метод'], y=results_df['Время обработки (сек)'], | |
| name='Время обработки', marker_color='lightblue'), | |
| row=1, col=1 | |
| ) | |
| # Размер словаря | |
| fig.add_trace( | |
| go.Bar(x=results_df['Метод'], y=results_df['Размер словаря'], | |
| name='Размер словаря', marker_color='lightgreen'), | |
| row=1, col=2 | |
| ) | |
| # Коэффициент сжатия | |
| fig.add_trace( | |
| go.Bar(x=results_df['Метод'], y=results_df['Коэффициент сжатия'], | |
| name='Коэффициент сжатия', marker_color='lightcoral'), | |
| row=2, col=1 | |
| ) | |
| # Средняя длина токена | |
| fig.add_trace( | |
| go.Bar(x=results_df['Метод'], y=results_df['Средняя длина токена'], | |
| name='Средняя длина токена', marker_color='lightyellow'), | |
| row=2, col=2 | |
| ) | |
| fig.update_layout( | |
| title='Сравнение методов токенизации', | |
| height=800, | |
| showlegend=False | |
| ) | |
| return fig | |
| def main(): | |
| """Основная функция приложения.""" | |
| # Заголовок | |
| st.markdown('<h1 class="main-header">🔤 Анализ токенизации и нормализации текста</h1>', | |
| unsafe_allow_html=True) | |
| # Боковая панель | |
| st.sidebar.title("⚙️ Настройки") | |
| # Выбор источника данных | |
| st.sidebar.subheader("📁 Источник данных") | |
| data_source = st.sidebar.radio( | |
| "Выберите источник данных:", | |
| ["Загрузить файл", "Использовать примеры", "Загрузить из корпуса"] | |
| ) | |
| texts = [] | |
| if data_source == "Загрузить файл": | |
| uploaded_file = st.sidebar.file_uploader( | |
| "Загрузите JSONL или TXT файл", | |
| type=['jsonl', 'txt', 'json'], | |
| help="Поддерживаются файлы в формате JSONL, TXT или JSON" | |
| ) | |
| if uploaded_file is not None: | |
| try: | |
| if uploaded_file.name.endswith('.jsonl'): | |
| content = uploaded_file.read().decode('utf-8') | |
| for line in content.split('\n'): | |
| if line.strip(): | |
| try: | |
| article = json.loads(line) | |
| if 'text' in article: | |
| texts.append(article['text']) | |
| except json.JSONDecodeError: | |
| continue | |
| elif uploaded_file.name.endswith('.txt'): | |
| content = uploaded_file.read().decode('utf-8') | |
| texts = [line.strip() for line in content.split('\n') if line.strip()] | |
| elif uploaded_file.name.endswith('.json'): | |
| content = uploaded_file.read().decode('utf-8') | |
| data = json.loads(content) | |
| if isinstance(data, list): | |
| for item in data: | |
| if isinstance(item, dict) and 'text' in item: | |
| texts.append(item['text']) | |
| elif isinstance(item, str): | |
| texts.append(item) | |
| elif isinstance(data, dict) and 'text' in data: | |
| texts.append(data['text']) | |
| st.sidebar.success(f"Загружено {len(texts)} текстов") | |
| except Exception as e: | |
| st.sidebar.error(f"Ошибка при загрузке файла: {e}") | |
| elif data_source == "Использовать примеры": | |
| texts = load_sample_data() | |
| st.sidebar.success(f"Загружено {len(texts)} примеров") | |
| elif data_source == "Загрузить из корпуса": | |
| corpus_path = "data/raw_corpus.jsonl" | |
| if os.path.exists(corpus_path): | |
| max_articles = st.sidebar.slider("Максимальное количество статей", 10, 1000, 100) | |
| texts = load_corpus_from_jsonl(corpus_path, max_articles=max_articles) | |
| st.sidebar.success(f"Загружено {len(texts)} статей из корпуса") | |
| else: | |
| st.sidebar.error("Корпус не найден. Используйте примеры или загрузите файл.") | |
| # Настройки предобработки | |
| st.sidebar.subheader("🔧 Предобработка") | |
| use_preprocessing = st.sidebar.checkbox("Применить предобработку", value=True) | |
| if use_preprocessing: | |
| preprocessing_options = { | |
| "replace_urls": st.sidebar.checkbox("Заменять URL", value=True), | |
| "replace_emails": st.sidebar.checkbox("Заменять email", value=True), | |
| "replace_numbers": st.sidebar.checkbox("Заменять числа", value=True), | |
| "expand_abbreviations": st.sidebar.checkbox("Раскрывать сокращения", value=True), | |
| "normalize_punctuation": st.sidebar.checkbox("Нормализовать пунктуацию", value=True) | |
| } | |
| # Настройки очистки текста | |
| cleaning_options = { | |
| "lower": st.sidebar.checkbox("Приводить к нижнему регистру", value=True), | |
| "remove_stopwords": st.sidebar.checkbox("Удалять стоп-слова", value=False), | |
| "min_token_length": st.sidebar.slider("Минимальная длина токена", 1, 5, 2), | |
| "remove_numbers": st.sidebar.checkbox("Удалять числовые токены", value=False) | |
| } | |
| # Основной контент | |
| if not texts: | |
| st.warning("⚠️ Пожалуйста, загрузите данные для анализа.") | |
| st.info("💡 Используйте боковую панель для загрузки файла или выберите примеры.") | |
| return | |
| # Сохраняем исходные тексты и метаданные источника | |
| raw_texts = list(texts) | |
| st.session_state["data_meta"] = { | |
| "source": data_source, | |
| "num_texts": len(raw_texts), | |
| } | |
| # Применяем предобработку и очистку, параллельно сохраняя обе версии | |
| processed_texts = list(raw_texts) | |
| if use_preprocessing: | |
| config = UniversalPreprocessingConfig(**preprocessing_options) | |
| preprocessor = UniversalPreprocessor(config) | |
| tmp = [] | |
| for text in raw_texts: | |
| processed_text = preprocessor.preprocess(text) | |
| processed_text = clean_text(processed_text, **cleaning_options) | |
| tmp.append(processed_text) | |
| processed_texts = tmp | |
| # Положим обе версии в состояние для явного выбора на вкладках | |
| st.session_state["raw_texts"] = raw_texts | |
| st.session_state["processed_texts"] = processed_texts | |
| texts = processed_texts | |
| # Главные вкладки ЛР1/ЛР2/ЛР3/ЛР4 | |
| main_tabs = st.tabs(["Токенизация", "Векторизация", "Эмбеддинги", "Классификация", "Кластеризация"]) | |
| # ======== Токенизация (ЛР1) ======== | |
| with main_tabs[0]: | |
| st.subheader("🎯 Методы токенизации") | |
| comparator = TokenizationComparator() | |
| available_methods = list(comparator.methods.keys()) | |
| selected_methods = st.multiselect( | |
| "Выберите методы для сравнения:", | |
| available_methods, | |
| default=available_methods[:3] if len(available_methods) >= 3 else available_methods | |
| ) | |
| if not selected_methods: | |
| st.warning("⚠️ Пожалуйста, выберите хотя бы один метод токенизации.") | |
| st.stop() | |
| if st.button("🚀 Запустить анализ", type="primary"): | |
| with st.spinner("Выполняется анализ..."): | |
| results_df = comparator.compare_methods(texts, selected_methods) | |
| st.session_state['results_df'] = results_df | |
| st.session_state['texts'] = texts | |
| st.session_state['selected_methods'] = selected_methods | |
| if 'results_df' in st.session_state: | |
| results_df = st.session_state['results_df'] | |
| texts = st.session_state['texts'] | |
| selected_methods = st.session_state['selected_methods'] | |
| st.subheader("📊 Общая статистика") | |
| col1, col2, col3, col4 = st.columns(4) | |
| with col1: | |
| st.metric("Количество текстов", len(texts)) | |
| with col2: | |
| total_words = sum(len(text.split()) for text in texts) | |
| st.metric("Общее количество слов", total_words) | |
| with col3: | |
| avg_words_per_text = total_words / len(texts) if texts else 0 | |
| st.metric("Среднее слов на текст", round(avg_words_per_text, 1)) | |
| with col4: | |
| st.metric("Проанализировано методов", len(selected_methods)) | |
| st.subheader("📋 Результаты сравнения") | |
| st.dataframe(results_df, use_container_width=True) | |
| st.subheader("📈 Визуализация результатов") | |
| comparison_chart = create_comparison_chart(results_df) | |
| st.plotly_chart(comparison_chart, use_container_width=True) | |
| st.subheader("🔍 Детальный анализ методов") | |
| method_tabs = st.tabs(selected_methods) | |
| for i, method in enumerate(selected_methods): | |
| with method_tabs[i]: | |
| if texts: | |
| all_tokens = [] | |
| total_processing_time = 0 | |
| for text in texts: | |
| tokens, processing_time = comparator.tokenize_text(text, method) | |
| all_tokens.extend(tokens) | |
| total_processing_time += processing_time | |
| sample_text = texts[0] | |
| sample_tokens, _ = comparator.tokenize_text(sample_text, method) | |
| col1, col2 = st.columns(2) | |
| with col1: | |
| st.write("**Исходный текст:**") | |
| st.text(sample_text[:200] + "..." if len(sample_text) > 200 else sample_text) | |
| with col2: | |
| st.write("**Токены (пример из первого текста):**") | |
| st.write(sample_tokens[:20]) | |
| if len(sample_tokens) > 20: | |
| st.write(f"... и еще {len(sample_tokens) - 20} токенов") | |
| col1, col2 = st.columns(2) | |
| with col1: | |
| dist_plot = create_token_distribution_plot(all_tokens, method) | |
| st.plotly_chart(dist_plot, use_container_width=True) | |
| with col2: | |
| freq_plot = create_frequency_plot(all_tokens, method) | |
| st.plotly_chart(freq_plot, use_container_width=True) | |
| from collections import Counter | |
| token_counts = Counter(all_tokens) | |
| unique_tokens = len(token_counts) | |
| total_tokens = len(all_tokens) | |
| vocabulary_diversity = unique_tokens / total_tokens if total_tokens > 0 else 0 | |
| st.write("**Статистика:**") | |
| col1, col2, col3, col4 = st.columns(4) | |
| with col1: | |
| st.metric("Всего токенов", total_tokens) | |
| with col2: | |
| st.metric("Уникальных токенов", unique_tokens) | |
| with col3: | |
| st.metric("Разнообразие словаря", f"{vocabulary_diversity:.2%}") | |
| with col4: | |
| st.metric("Время обработки", f"{total_processing_time:.4f}с") | |
| st.subheader("💾 Экспорт результатов") | |
| col1, col2 = st.columns(2) | |
| with col1: | |
| csv_data = results_df.to_csv(index=False, encoding='utf-8') | |
| st.download_button( | |
| label="📥 Скачать CSV", | |
| data=csv_data, | |
| file_name="tokenization_results.csv", | |
| mime="text/csv" | |
| ) | |
| with col2: | |
| json_data = results_df.to_json(orient='records', force_ascii=False, indent=2) | |
| st.download_button( | |
| label="📥 Скачать JSON", | |
| data=json_data, | |
| file_name="tokenization_results.json", | |
| mime="application/json" | |
| ) | |
| # ======== Векторизация (ЛР2: классика + LSA) ======== | |
| with main_tabs[1]: | |
| st.subheader("🧮 Классические методы векторизации") | |
| with st.expander("Параметры векторизации", expanded=True): | |
| methods = st.multiselect("Методы", ["onehot", "bow", "tfidf"], default=["bow", "tfidf"]) | |
| n_min = st.number_input("n-gram min", 1, 5, 1) | |
| n_max = st.number_input("n-gram max", 1, 5, 2) | |
| max_features = st.number_input("Max features (0 = все)", 0, 200000, 0) | |
| sublinear_tf = st.checkbox("TF-IDF sublinear_tf", value=True) | |
| smooth_idf = st.checkbox("TF-IDF smooth_idf", value=True) | |
| if st.button("🏁 Построить признаки", key="build_vectors"): | |
| cfgs = [] | |
| for m in methods: | |
| cfgs.append(VectorizationConfig( | |
| method=m, | |
| ngram_range=(int(n_min), int(n_max)), | |
| max_features=None if max_features == 0 else int(max_features), | |
| sublinear_tf=sublinear_tf, | |
| smooth_idf=smooth_idf, | |
| )) | |
| with st.spinner("Строим матрицы признаков..."): | |
| vec_df, matrices = compare_vectorizers(texts, cfgs) | |
| st.session_state["vec_df"] = vec_df | |
| st.session_state["vec_matrices"] = matrices | |
| try: | |
| os.makedirs("results", exist_ok=True) | |
| vec_path = "results/vectorization_metrics.csv" | |
| vec_df.to_csv(vec_path, index=False, encoding="utf-8") | |
| st.success(f"Метрики сохранены в {vec_path}") | |
| except Exception as e: | |
| st.warning(f"Не удалось сохранить метрики: {e}") | |
| if "vec_df" in st.session_state: | |
| st.dataframe(st.session_state["vec_df"], use_container_width=True) | |
| # Экспорт метрик | |
| vec_csv = st.session_state["vec_df"].to_csv(index=False, encoding="utf-8") | |
| st.download_button("📥 Скачать векторные метрики CSV", vec_csv, "vectorization_metrics.csv", "text/csv") | |
| # LSA / снижение размерности | |
| st.subheader("📉 LSA (TruncatedSVD) и проекции") | |
| selected_key = st.selectbox("Выберите матрицу", list(st.session_state["vec_matrices"].keys())) | |
| n_components = st.slider("Число компонент (SVD)", 2, 200, 100) | |
| proj_method = st.radio("Метод проекции", ["umap", "tsne"], horizontal=True) | |
| if st.button("🔎 Запустить LSA/проекции"): | |
| X = st.session_state["vec_matrices"][selected_key]["X"] | |
| vectorizer = st.session_state["vec_matrices"][selected_key]["vectorizer"] | |
| feature_names = vectorizer.get_feature_names() | |
| with st.spinner("Снижаем размерность..."): | |
| lsa = run_lsa(X, feature_names, SVDConfig(n_components=n_components)) | |
| ev_table = explained_variance_table(lsa["explained_variance_ratio"]) | |
| st.write("Объясненная дисперсия (первые 20):") | |
| st.dataframe(ev_table.head(20), use_container_width=True) | |
| st.write("Топ-термины по компонентам:") | |
| st.dataframe(top_terms_dataframe(lsa["top_terms_per_component"], top_k=10).head(50), use_container_width=True) | |
| # Проекция документов | |
| coords = embed_2d(lsa["X_reduced"], method=proj_method) | |
| proj_df = pd.DataFrame({"x": coords[:,0], "y": coords[:,1]}) | |
| import plotly.express as px_plot | |
| fig = px_plot.scatter(proj_df, x="x", y="y", title=f"Проекция документов ({proj_method.upper()})") | |
| st.plotly_chart(fig, use_container_width=True) | |
| # ======== Эмбеддинги (ЛР2: Word2Vec/FastText/Doc2Vec + эксперименты) ======== | |
| with main_tabs[2]: | |
| st.subheader("🧠 Обучение эмбеддингов и семантические эксперименты") | |
| # Выбор корпуса для обучения и параметры | |
| with st.expander("Параметры обучения", expanded=True): | |
| corpus_choice = st.radio( | |
| "Источник обучающих текстов", | |
| ["Предобработанные", "Без предобработки"], | |
| index=0, horizontal=True, | |
| help="Предобработанные = применены настройки из блока Предобработка на левой панели" | |
| ) | |
| # Формируем список доступных моделей | |
| available_models = ["w2v", "fasttext", "doc2vec"] | |
| if GLOVE_AVAILABLE: | |
| available_models.append("glove") | |
| model_type = st.selectbox("Модель", available_models, index=0) | |
| # Показываем предупреждение, если GloVe выбран, но недоступен | |
| if model_type == "glove" and not GLOVE_AVAILABLE: | |
| st.warning("⚠️ GloVe не установлен. Установите: `pip install glove-python-binary`. Модель не будет обучена.") | |
| vector_size = st.slider("Размерность", 50, 600, 300, step=50) | |
| window = st.slider("Окно контекста", 2, 15, 8) | |
| min_count = st.slider("Min count", 1, 20, 2) | |
| epochs = st.slider("Эпохи", 1, 50, 10) | |
| sg = st.radio("Архитектура (w2v/fasttext)", ["cbow", "skipgram"], index=1, horizontal=True) | |
| dm = st.radio("Doc2Vec архитектура", ["pv-dm", "pv-dbow"], index=0, horizontal=True) | |
| # Инфо о корпусе, предпросмотр и экспорт | |
| meta = st.session_state.get("data_meta", {}) | |
| corpus = st.session_state.get("processed_texts", []) if corpus_choice == "Предобработанные" else st.session_state.get("raw_texts", []) | |
| st.info(f"Источник данных: {meta.get('source','неизвестно')} | Текстов: {len(corpus)}") | |
| if corpus: | |
| with st.expander("Просмотр обучающего корпуса (первые 3 текста)", expanded=False): | |
| st.write(corpus[:3]) | |
| # Скачать текущий обучающий корпус | |
| corpus_txt = ("\n".join(corpus)).encode("utf-8") | |
| st.download_button("📥 Скачать обучающий корпус (.txt)", data=corpus_txt, file_name="training_corpus.txt", mime="text/plain") | |
| if st.button("🎓 Обучить модель", key="train_embeddings"): | |
| # Проверяем доступность GloVe перед обучением | |
| if model_type == "glove" and not GLOVE_AVAILABLE: | |
| st.error("❌ GloVe не установлен. Установите: `pip install glove-python-binary`") | |
| st.stop() | |
| cfg = EmbTrainConfig( | |
| model_type=model_type, | |
| vector_size=int(vector_size), | |
| window=int(window), | |
| min_count=int(min_count), | |
| epochs=int(epochs), | |
| sg=1 if sg == "skipgram" else 0, | |
| dm=1 if dm == "pv-dm" else 0, | |
| ) | |
| with st.spinner("Обучаем модель..."): | |
| try: | |
| model, tt = train_embeddings_model(corpus, cfg) | |
| st.session_state["emb_model"] = model | |
| st.session_state["emb_train_time"] = tt | |
| st.success(f"Модель обучена за {tt:.2f} с") | |
| except ImportError as e: | |
| if "GloVe" in str(e) or "glove" in str(e).lower(): | |
| st.error(f"❌ GloVe не установлен. Установите: `pip install glove-python-binary`") | |
| else: | |
| st.error(f"❌ Ошибка импорта: {e}") | |
| except Exception as e: | |
| st.error(f"❌ Ошибка при обучении модели: {e}") | |
| if "emb_model" in st.session_state: | |
| model = st.session_state["emb_model"] | |
| col1, col2 = st.columns(2) | |
| with col1: | |
| save_name = st.text_input("Имя файла модели", "models/russian_news_embeddings.model") | |
| if st.button("💾 Сохранить модель"): | |
| save_embedding_model(model, save_name) | |
| st.success(f"Сохранено: {save_name}") | |
| with col2: | |
| test_word = st.text_input("Проверить ближайших соседей для слова", "россия") | |
| if st.button("🔍 Найти соседей"): | |
| res = nearest_neighbors(model, [test_word], topn=10) | |
| st.write(res.get(test_word, [])) | |
| st.markdown("---") | |
| st.subheader("🧪 Семантические операции") | |
| col1, col2 = st.columns(2) | |
| with col1: | |
| expr = st.text_input("Векторная арифметика", "король - мужчина + женщина") | |
| if st.button("➡️ Посчитать", key="arith"): | |
| st.write(vector_arithmetic(model, expr, topn=10)) | |
| with col2: | |
| a = st.text_input("Ось: A", "мужчина") | |
| b = st.text_input("Ось: B", "женщина") | |
| words = st.text_area("Слова для проекции (через запятую)", "король, королева, доктор, медсестра") | |
| if st.button("📏 Проекция на ось"): | |
| wlist = [w.strip() for w in words.split(",") if w.strip()] | |
| st.dataframe(semantic_axis(model, a, b, wlist), use_container_width=True) | |
| st.markdown("---") | |
| st.subheader("📐 Косинусное сходство и аналогии") | |
| col1, col2 = st.columns(2) | |
| with col1: | |
| pair_a = st.text_input("Пара A", "москва") | |
| pair_b = st.text_input("Пара B", "россия") | |
| if st.button("🔗 Косинус", key="cos"): | |
| st.write(eval_cosine(model, [(pair_a, pair_b)])) | |
| with col2: | |
| ana_a = st.text_input("Аналогия: A", "мужчина") | |
| ana_b = st.text_input("Аналогия: B", "женщина") | |
| ana_c = st.text_input("Аналогия: C", "король") | |
| if st.button("🧩 Аналогия"): | |
| st.write(eval_analogy(model, ana_a, ana_b, ana_c, topn=10)) | |
| # ======== Классификация (ЛР3) ======== | |
| with main_tabs[3]: | |
| st.subheader("📊 Классификация текстов") | |
| st.markdown("**Лабораторная работа №3: Сравнительный анализ методов классификации текстов**") | |
| # Загрузка корпуса из ЛР1 | |
| st.subheader("📁 Загрузка корпуса из ЛР №1") | |
| corpus_source = st.radio( | |
| "Источник корпуса:", | |
| ["Из загруженных данных", "Из файла корпуса (JSONL)"], | |
| horizontal=True, | |
| key="classification_corpus_source" | |
| ) | |
| classification_texts = [] | |
| classification_articles = [] | |
| if corpus_source == "Из загруженных данных": | |
| if not texts: | |
| st.warning("⚠️ Сначала загрузите данные на главной странице или используйте корпус из файла.") | |
| else: | |
| classification_texts = texts | |
| st.info(f"✅ Используется {len(classification_texts)} текстов из загруженных данных") | |
| else: | |
| # Загрузка из файла корпуса | |
| corpus_file_path = st.text_input( | |
| "Путь к файлу корпуса (JSONL):", | |
| value="data/raw_corpus.jsonl", | |
| key="classification_corpus_path" | |
| ) | |
| if st.button("📂 Загрузить корпус", key="load_classification_corpus"): | |
| if os.path.exists(corpus_file_path): | |
| try: | |
| from src.utils import load_jsonl | |
| classification_articles = load_jsonl(corpus_file_path) | |
| classification_texts = [article.get('text', '') for article in classification_articles if article.get('text')] | |
| st.success(f"✅ Загружено {len(classification_texts)} статей из корпуса") | |
| st.session_state["classification_articles"] = classification_articles | |
| except Exception as e: | |
| st.error(f"❌ Ошибка при загрузке корпуса: {e}") | |
| else: | |
| st.error(f"❌ Файл не найден: {corpus_file_path}") | |
| # Используем сохраненные статьи, если они есть | |
| if "classification_articles" in st.session_state and not classification_texts: | |
| classification_articles = st.session_state["classification_articles"] | |
| classification_texts = [article.get('text', '') for article in classification_articles if article.get('text')] | |
| if not classification_texts: | |
| st.warning("⚠️ Загрузите корпус для классификации.") | |
| st.info("💡 Используйте корпус из ЛР №1 в формате JSONL с полями: title, text, date, url, category") | |
| else: | |
| # Показываем информацию о корпусе | |
| total_words = sum(len(text.split()) for text in classification_texts) | |
| st.info(f"📊 Корпус: {len(classification_texts)} документов, ~{total_words:,} слов") | |
| # Выбор типа задачи | |
| st.subheader("🎯 Тип задачи классификации") | |
| task_type = st.radio( | |
| "Выберите тип задачи:", | |
| ["Бинарная", "Многоклассовая", "Многометочная"], | |
| horizontal=True, | |
| help="Бинарная: тональность (позитивные/негативные)\nМногоклассовая: категории (политика, экономика, спорт, культура)\nМногометочная: несколько категорий одновременно" | |
| ) | |
| # Разметка данных | |
| st.subheader("🏷️ Разметка данных") | |
| # Функция для автоматической разметки по тональности (бинарная) | |
| def label_sentiment_binary(texts_list): | |
| """Простая эвристика для определения тональности.""" | |
| positive_words = ['хорошо', 'отлично', 'успех', 'победа', 'рост', 'улучшение', 'развитие', | |
| 'достижение', 'прогресс', 'радость', 'счастье', 'праздник'] | |
| negative_words = ['плохо', 'проблема', 'кризис', 'падение', 'ухудшение', 'поражение', | |
| 'ошибка', 'неудача', 'трагедия', 'катастрофа', 'война', 'конфликт'] | |
| labels = [] | |
| for text in texts_list: | |
| text_lower = text.lower() | |
| pos_count = sum(1 for word in positive_words if word in text_lower) | |
| neg_count = sum(1 for word in negative_words if word in text_lower) | |
| # 0 = негативная, 1 = позитивная | |
| label = 1 if pos_count > neg_count else 0 | |
| labels.append(label) | |
| return np.array(labels) | |
| # Функция для разметки по категориям (многоклассовая) | |
| def label_categories_multiclass(articles_list): | |
| """Разметка по категориям из метаданных или по ключевым словам.""" | |
| categories_map = { | |
| 'политика': 0, | |
| 'экономика': 1, | |
| 'спорт': 2, | |
| 'культура': 3, | |
| 'технологии': 4, | |
| 'общество': 5 | |
| } | |
| category_keywords = { | |
| 0: ['политика', 'правительство', 'президент', 'министр', 'выборы', 'парламент', 'депутат'], | |
| 1: ['экономика', 'рынок', 'компания', 'бизнес', 'финансы', 'банк', 'инвестиции', 'рубль'], | |
| 2: ['спорт', 'футбол', 'хоккей', 'олимпиада', 'чемпионат', 'матч', 'игрок', 'команда'], | |
| 3: ['культура', 'кино', 'театр', 'музыка', 'литература', 'искусство', 'выставка', 'концерт'], | |
| 4: ['технологии', 'интернет', 'компьютер', 'смартфон', 'приложение', 'цифровой', 'IT'], | |
| 5: ['общество', 'люди', 'город', 'образование', 'здравоохранение', 'социальный'] | |
| } | |
| labels = [] | |
| for article in articles_list: | |
| # Сначала пробуем взять категорию из метаданных | |
| category = article.get('category', '').lower() if isinstance(article, dict) else '' | |
| if category and category in categories_map: | |
| labels.append(categories_map[category]) | |
| else: | |
| # Определяем по ключевым словам в тексте | |
| text = (article.get('text', '') + ' ' + article.get('title', '')).lower() if isinstance(article, dict) else str(article).lower() | |
| scores = {} | |
| for cat_id, keywords in category_keywords.items(): | |
| scores[cat_id] = sum(1 for kw in keywords if kw in text) | |
| if max(scores.values()) > 0: | |
| labels.append(max(scores, key=scores.get)) | |
| else: | |
| labels.append(0) # По умолчанию политика | |
| return np.array(labels) | |
| # Функция для многометочной разметки | |
| def label_categories_multilabel(articles_list): | |
| """Многометочная разметка - документ может иметь несколько категорий.""" | |
| category_keywords = { | |
| 'политика': ['политика', 'правительство', 'президент', 'министр', 'выборы'], | |
| 'экономика': ['экономика', 'рынок', 'компания', 'бизнес', 'финансы'], | |
| 'спорт': ['спорт', 'футбол', 'хоккей', 'олимпиада', 'чемпионат'], | |
| 'культура': ['культура', 'кино', 'театр', 'музыка', 'искусство'] | |
| } | |
| labels = [] | |
| for article in articles_list: | |
| text = (article.get('text', '') + ' ' + article.get('title', '')).lower() if isinstance(article, dict) else str(article).lower() | |
| doc_labels = [] | |
| for category, keywords in category_keywords.items(): | |
| # Документ относится к категории, если содержит хотя бы одно ключевое слово | |
| if any(kw in text for kw in keywords): | |
| doc_labels.append(1) | |
| else: | |
| doc_labels.append(0) | |
| labels.append(doc_labels) | |
| return np.array(labels) | |
| # Выполняем разметку | |
| if "classification_labels" not in st.session_state or st.session_state.get("classification_task_type") != task_type: | |
| with st.spinner("Выполняется разметка данных..."): | |
| if task_type == "Бинарная": | |
| labels = label_sentiment_binary(classification_texts) | |
| st.session_state["classification_labels"] = labels | |
| st.session_state["classification_task_type"] = task_type | |
| elif task_type == "Многоклассовая": | |
| labels = label_categories_multiclass(classification_articles if classification_articles else classification_texts) | |
| st.session_state["classification_labels"] = labels | |
| st.session_state["classification_task_type"] = task_type | |
| elif task_type == "Многометочная": | |
| labels = label_categories_multilabel(classification_articles if classification_articles else classification_texts) | |
| st.session_state["classification_labels"] = labels | |
| st.session_state["classification_task_type"] = task_type | |
| st.session_state["num_labels"] = labels.shape[1] if len(labels.shape) > 1 else 4 | |
| # Показываем статистику разметки | |
| if task_type == "Бинарная": | |
| unique, counts = np.unique(labels, return_counts=True) | |
| st.success(f"✅ Разметка завершена: {dict(zip(['Негативные', 'Позитивные'], counts))}") | |
| elif task_type == "Многоклассовая": | |
| unique, counts = np.unique(labels, return_counts=True) | |
| category_names = ['Политика', 'Экономика', 'Спорт', 'Культура', 'Технологии', 'Общество'] | |
| dist = {category_names[i] if i < len(category_names) else f'Класс {i}': count | |
| for i, count in zip(unique, counts)} | |
| st.success(f"✅ Разметка завершена. Распределение: {dist}") | |
| else: | |
| # Многометочная | |
| category_names = ['Политика', 'Экономика', 'Спорт', 'Культура'] | |
| if len(labels.shape) > 1: | |
| counts = labels.sum(axis=0) | |
| dist = {name: int(count) for name, count in zip(category_names[:len(counts)], counts)} | |
| st.success(f"✅ Разметка завершена. Документов по категориям: {dist}") | |
| labels = st.session_state.get("classification_labels", np.array([])) | |
| if len(labels) == 0: | |
| st.warning("⚠️ Разметка не выполнена. Пожалуйста, дождитесь завершения разметки.") | |
| else: | |
| # Предобработка | |
| st.subheader("🔧 Предобработка текстов") | |
| max_docs = st.slider( | |
| "Максимальное количество документов для обработки:", | |
| min_value=100, | |
| max_value=min(10000, len(classification_texts)), | |
| value=min(1000, len(classification_texts)), | |
| step=100, | |
| help="Для ускорения можно ограничить количество документов" | |
| ) | |
| preprocess_config = PreprocessingConfig( | |
| lowercase=True, | |
| remove_html=True, | |
| lemmatize=st.checkbox("Использовать лемматизацию", value=False, help="Замедляет обработку, но улучшает качество"), | |
| remove_stopwords=st.checkbox("Удалять стоп-слова", value=False) | |
| ) | |
| preprocessor = TextPreprocessor(preprocess_config) | |
| if st.button("🔄 Выполнить предобработку", key="preprocess_classification"): | |
| with st.spinner(f"Обрабатываем {max_docs} документов..."): | |
| processed_texts = preprocessor.preprocess_batch(classification_texts[:max_docs]) | |
| st.session_state["processed_classification_texts"] = processed_texts | |
| st.session_state["classification_max_docs"] = max_docs | |
| st.success(f"✅ Обработано {len(processed_texts)} документов") | |
| # Используем сохраненные обработанные тексты | |
| if "processed_classification_texts" in st.session_state: | |
| processed_texts = st.session_state["processed_classification_texts"] | |
| max_docs_used = st.session_state.get("classification_max_docs", len(processed_texts)) | |
| labels_used = labels[:max_docs_used] | |
| # Векторизация | |
| st.subheader("🧮 Векторизация") | |
| vectorization_method = st.selectbox( | |
| "Метод векторизации:", | |
| ["tfidf", "bow"], | |
| help="TF-IDF: учитывает важность терминов\nBoW: простой подсчет частот" | |
| ) | |
| ngram_max = st.number_input("Максимальный n-gram", 1, 3, 2, help="1=униграммы, 2=биграммы, 3=триграммы") | |
| max_features = st.number_input("Максимальное количество признаков", 100, 10000, 1000, step=100) | |
| if st.button("🔨 Векторизовать тексты", key="vectorize_for_classification"): | |
| with st.spinner("Векторизация..."): | |
| X, vectorizer = vectorize_with_classical( | |
| processed_texts, | |
| method=vectorization_method, | |
| ngram_range=(1, ngram_max), | |
| max_features=max_features | |
| ) | |
| st.session_state["X_classification"] = X | |
| st.session_state["vectorizer_classification"] = vectorizer | |
| st.success(f"✅ Векторизовано {len(processed_texts)} текстов, размерность: {X.shape}") | |
| # Классификация | |
| if "X_classification" in st.session_state: | |
| X = st.session_state["X_classification"] | |
| y = labels_used | |
| # Разделение на train/validation/test (70/15/15) | |
| from sklearn.model_selection import train_test_split | |
| # Сначала разделяем на train (70%) и temp (30%) | |
| if task_type == "Многометочная": | |
| X_train, X_temp, y_train, y_temp = train_test_split( | |
| X, y, test_size=0.3, random_state=42 | |
| ) | |
| else: | |
| X_train, X_temp, y_train, y_temp = train_test_split( | |
| X, y, test_size=0.3, random_state=42, stratify=y | |
| ) | |
| # Затем temp разделяем на validation (15%) и test (15%) | |
| if task_type == "Многометочная": | |
| X_val, X_test, y_val, y_test = train_test_split( | |
| X_temp, y_temp, test_size=0.5, random_state=42 | |
| ) | |
| else: | |
| X_val, X_test, y_val, y_test = train_test_split( | |
| X_temp, y_temp, test_size=0.5, random_state=42, stratify=y_temp | |
| ) | |
| # Показываем статистику разделения | |
| st.subheader("📊 Разделение данных") | |
| col1, col2, col3 = st.columns(3) | |
| with col1: | |
| st.metric("Обучающая выборка", f"{len(X_train)} ({len(X_train)/len(X)*100:.1f}%)") | |
| with col2: | |
| st.metric("Валидационная выборка", f"{len(X_val)} ({len(X_val)/len(X)*100:.1f}%)") | |
| with col3: | |
| st.metric("Тестовая выборка", f"{len(X_test)} ({len(X_test)/len(X)*100:.1f}%)") | |
| st.subheader("🎯 Обучение классификаторов") | |
| selected_models = st.multiselect( | |
| "Выберите модели:", | |
| ["Logistic Regression", "SVM", "Random Forest"], | |
| default=["Logistic Regression", "Random Forest"] | |
| ) | |
| if st.button("🚀 Обучить модели", key="train_classifiers"): | |
| configs = [] | |
| if "Logistic Regression" in selected_models: | |
| configs.append(ClassifierConfig(name="Logistic Regression", model_type="lr")) | |
| if "SVM" in selected_models: | |
| configs.append(ClassifierConfig(name="SVM", model_type="svm", params={"kernel": "linear"})) | |
| if "Random Forest" in selected_models: | |
| configs.append(ClassifierConfig(name="Random Forest", model_type="rf")) | |
| with st.spinner("Обучение моделей..."): | |
| # Определяем тип задачи | |
| if task_type == "Многометочная": | |
| task_type_str = "multilabel" | |
| elif task_type == "Многоклассовая": | |
| task_type_str = "multiclass" | |
| else: | |
| task_type_str = "binary" | |
| results_df = compare_classifiers( | |
| X_train, y_train, X_test, y_test, | |
| configs, | |
| task_type=task_type_str | |
| ) | |
| st.session_state["classification_results"] = results_df | |
| if "classification_results" in st.session_state: | |
| st.subheader("📊 Результаты классификации") | |
| st.dataframe(st.session_state["classification_results"], use_container_width=True) | |
| # Важность признаков | |
| if "vectorizer_classification" in st.session_state and "X_classification" in st.session_state: | |
| st.subheader("🔍 Важные слова") | |
| vectorizer = st.session_state["vectorizer_classification"] | |
| if "Logistic Regression" in selected_models: | |
| # Создаем простую модель для демонстрации | |
| from sklearn.linear_model import LogisticRegression | |
| from sklearn.multioutput import MultiOutputClassifier | |
| # Получаем данные из session_state | |
| X_full = st.session_state["X_classification"] | |
| y_full = labels_used | |
| # Для multilabel используем MultiOutputClassifier | |
| if task_type == "Многометочная": | |
| # Проверяем, что y_full - это 2D массив | |
| if len(y_full.shape) == 1: | |
| y_full = y_full.reshape(-1, 1) | |
| # Используем только часть данных для быстрой демонстрации | |
| X_demo = X_full[:min(100, len(X_full))] | |
| y_demo = y_full[:min(100, len(y_full))] | |
| model = MultiOutputClassifier(LogisticRegression(max_iter=1000, random_state=42)) | |
| else: | |
| # Для бинарной и многоклассовой классификации используем обычную модель | |
| # Убеждаемся, что y_full - это 1D массив | |
| if len(y_full.shape) > 1: | |
| y_full = y_full.flatten() if y_full.shape[1] == 1 else y_full.argmax(axis=1) | |
| # Используем только часть данных для быстрой демонстрации | |
| X_demo = X_full[:min(100, len(X_full))] | |
| y_demo = y_full[:min(100, len(y_full))] | |
| model = LogisticRegression(max_iter=1000, random_state=42) | |
| try: | |
| model.fit(X_demo, y_demo) | |
| # Для multilabel берем первый классификатор | |
| if task_type == "Многометочная": | |
| base_model = model.estimators_[0] if hasattr(model, 'estimators_') and len(model.estimators_) > 0 else model | |
| else: | |
| base_model = model | |
| important_words = get_tfidf_important_words(vectorizer, base_model, class_idx=0, top_k=20) | |
| st.dataframe(important_words, use_container_width=True) | |
| except Exception as e: | |
| st.warning(f"Не удалось показать важные слова: {e}") | |
| # ======== Кластеризация (ЛР4) ======== | |
| with main_tabs[4]: | |
| st.subheader("🔍 Кластеризация текстов") | |
| if not texts: | |
| st.warning("⚠️ Загрузите тексты для кластеризации.") | |
| else: | |
| # Предобработка | |
| st.subheader("🔧 Предобработка") | |
| preprocess_config = PreprocessingConfig( | |
| lowercase=True, | |
| remove_html=True, | |
| lemmatize=False, | |
| remove_stopwords=False | |
| ) | |
| preprocessor = TextPreprocessor(preprocess_config) | |
| processed_texts = preprocessor.preprocess_batch(texts[:min(200, len(texts))]) # Ограничиваем для демо | |
| # Векторизация | |
| st.subheader("🧮 Векторизация") | |
| vectorization_method = st.selectbox( | |
| "Метод векторизации:", | |
| ["tfidf", "bm25"], | |
| key="clustering_vectorization" | |
| ) | |
| if st.button("🔨 Векторизовать тексты", key="vectorize_for_clustering"): | |
| with st.spinner("Векторизация..."): | |
| try: | |
| X, vectorizer_obj = vectorize_texts( | |
| processed_texts, | |
| method=vectorization_method, | |
| max_features=500 | |
| ) | |
| st.session_state["X_clustering"] = X | |
| st.session_state["vectorizer_clustering"] = vectorizer_obj | |
| st.success(f"Векторизовано {len(processed_texts)} текстов, размерность: {X.shape}") | |
| except Exception as e: | |
| st.error(f"Ошибка векторизации: {e}") | |
| # Кластеризация | |
| if "X_clustering" in st.session_state: | |
| X = st.session_state["X_clustering"] | |
| st.subheader("🎯 Кластеризация") | |
| clustering_method = st.selectbox( | |
| "Метод кластеризации:", | |
| ["kmeans", "dbscan", "agglomerative", "gmm"], | |
| key="clustering_method" | |
| ) | |
| n_clusters = None | |
| if clustering_method in ["kmeans", "agglomerative", "gmm"]: | |
| n_clusters = st.slider("Число кластеров", 2, 20, 5, key="n_clusters") | |
| if clustering_method == "dbscan": | |
| eps = st.slider("EPS", 0.1, 1.0, 0.5, 0.1, key="dbscan_eps") | |
| min_samples = st.slider("Min samples", 2, 10, 5, key="dbscan_min_samples") | |
| else: | |
| eps = 0.5 | |
| min_samples = 5 | |
| if st.button("🚀 Выполнить кластеризацию", key="run_clustering"): | |
| with st.spinner("Кластеризация..."): | |
| try: | |
| config = ClusteringConfig( | |
| method=clustering_method, | |
| n_clusters=n_clusters, | |
| eps=eps, | |
| min_samples=min_samples | |
| ) | |
| clusterer = ClusteringAlgorithms(config) | |
| clusterer.fit(X) | |
| # Оценка качества | |
| metrics = evaluate_clustering(X, clusterer.labels_) | |
| st.session_state["clustering_labels"] = clusterer.labels_ | |
| st.session_state["clustering_metrics"] = metrics | |
| st.session_state["clustering_model"] = clusterer | |
| st.success("Кластеризация завершена!") | |
| except Exception as e: | |
| st.error(f"Ошибка кластеризации: {e}") | |
| if "clustering_labels" in st.session_state: | |
| labels = st.session_state["clustering_labels"] | |
| metrics = st.session_state["clustering_metrics"] | |
| st.subheader("📊 Результаты кластеризации") | |
| # Метрики | |
| col1, col2, col3, col4 = st.columns(4) | |
| with col1: | |
| st.metric("Число кластеров", metrics.get("n_clusters", 0)) | |
| with col2: | |
| st.metric("Silhouette", round(metrics.get("silhouette", -1), 3)) | |
| with col3: | |
| st.metric("Calinski-Harabasz", round(metrics.get("calinski_harabasz", 0), 2)) | |
| with col4: | |
| st.metric("Davies-Bouldin", round(metrics.get("davies_bouldin", np.inf), 3)) | |
| # Распределение по кластерам | |
| unique_labels, counts = np.unique(labels, return_counts=True) | |
| cluster_df = pd.DataFrame({ | |
| "Кластер": unique_labels, | |
| "Количество документов": counts | |
| }) | |
| st.dataframe(cluster_df, use_container_width=True) | |
| # Примеры документов из кластеров | |
| st.subheader("📝 Примеры документов по кластерам") | |
| selected_cluster = st.selectbox( | |
| "Выберите кластер:", | |
| unique_labels[unique_labels != -1] if -1 in labels else unique_labels, | |
| key="selected_cluster" | |
| ) | |
| cluster_indices = np.where(labels == selected_cluster)[0] | |
| if len(cluster_indices) > 0: | |
| sample_indices = cluster_indices[:5] # Показываем первые 5 | |
| for idx in sample_indices: | |
| st.text_area( | |
| f"Документ {idx}", | |
| processed_texts[idx][:200] + "..." if len(processed_texts[idx]) > 200 else processed_texts[idx], | |
| height=100, | |
| key=f"doc_{idx}" | |
| ) | |
| # Визуализация (если возможно) | |
| if X.shape[1] > 2: | |
| st.subheader("📈 Визуализация кластеров") | |
| try: | |
| from sklearn.decomposition import PCA | |
| pca = PCA(n_components=2) | |
| X_2d = pca.fit_transform(X) | |
| import plotly.express as px | |
| viz_df = pd.DataFrame({ | |
| "x": X_2d[:, 0], | |
| "y": X_2d[:, 1], | |
| "Кластер": labels.astype(str) | |
| }) | |
| fig = px.scatter(viz_df, x="x", y="y", color="Кластер", | |
| title="Проекция кластеров (PCA)") | |
| fig.update_traces(marker_size=5) | |
| st.plotly_chart(fig, use_container_width=True) | |
| except Exception as e: | |
| st.warning(f"Не удалось создать визуализацию: {e}") | |
| if __name__ == "__main__": | |
| main() |