Kolesnikov Dmitry commited on
Commit
41c2e74
·
1 Parent(s): 6dcad4a

fix: Данные для классификации

Browse files
Files changed (2) hide show
  1. src/embeddings_train.py +4 -1
  2. src/streamlit_app.py +377 -138
src/embeddings_train.py CHANGED
@@ -121,7 +121,10 @@ def train_doc2vec(texts: Iterable[str], cfg: TrainConfig) -> Doc2Vec:
121
  def train_glove(texts: Iterable[str], cfg: TrainConfig):
122
  """Обучает GloVe модель."""
123
  if not GLOVE_AVAILABLE:
124
- raise ImportError("GloVe не установлен. Установите: pip install glove-python-binary")
 
 
 
125
 
126
  sentences = _tokenize_corpus(texts)
127
 
 
121
  def train_glove(texts: Iterable[str], cfg: TrainConfig):
122
  """Обучает GloVe модель."""
123
  if not GLOVE_AVAILABLE:
124
+ raise ImportError(
125
+ "GloVe не установлен. Установите: pip install glove-python-binary\n"
126
+ "Или используйте альтернативу: pip install glove-python"
127
+ )
128
 
129
  sentences = _tokenize_corpus(texts)
130
 
src/streamlit_app.py CHANGED
@@ -41,6 +41,17 @@ from src.classical_vectorizers import (
41
  )
42
  from src.dimensionality import SVDConfig, run_lsa, embed_2d, explained_variance_table, top_terms_dataframe
43
  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
 
 
 
 
 
 
 
 
 
 
 
44
  from src.semantic_experiments import vector_arithmetic, semantic_axis, nearest_neighbors
45
  from src.text_preprocessing import TextPreprocessor, PreprocessingConfig, extract_meta_features, vectorize_with_classical, vectorize_with_embeddings
46
  from src.classical_classifiers import ClassicalClassifiers, ClassifierConfig, compare_classifiers, evaluate_classifier
@@ -512,7 +523,15 @@ def main():
512
  index=0, horizontal=True,
513
  help="Предобработанные = применены настройки из блока Предобработка на левой панели"
514
  )
515
- model_type = st.selectbox("Модель", ["w2v", "fasttext", "doc2vec", "glove"], index=0)
 
 
 
 
 
 
 
 
516
  vector_size = st.slider("Размерность", 50, 600, 300, step=50)
517
  window = st.slider("Окно контекста", 2, 15, 8)
518
  min_count = st.slider("Min count", 1, 20, 2)
@@ -532,6 +551,11 @@ def main():
532
  st.download_button("📥 Скачать обучающий корпус (.txt)", data=corpus_txt, file_name="training_corpus.txt", mime="text/plain")
533
 
534
  if st.button("🎓 Обучить модель", key="train_embeddings"):
 
 
 
 
 
535
  cfg = EmbTrainConfig(
536
  model_type=model_type,
537
  vector_size=int(vector_size),
@@ -542,10 +566,18 @@ def main():
542
  dm=1 if dm == "pv-dm" else 0,
543
  )
544
  with st.spinner("Обучаем модель..."):
545
- model, tt = train_embeddings_model(corpus, cfg)
546
- st.session_state["emb_model"] = model
547
- st.session_state["emb_train_time"] = tt
548
- st.success(f"Модель обучена за {tt:.2f} с")
 
 
 
 
 
 
 
 
549
 
550
  if "emb_model" in st.session_state:
551
  model = st.session_state["emb_model"]
@@ -594,163 +626,370 @@ def main():
594
  # ======== Классификация (ЛР3) ========
595
  with main_tabs[3]:
596
  st.subheader("📊 Классификация текстов")
 
597
 
598
- if not texts:
599
- st.warning("⚠️ Загрузите тексты для классификации.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
600
  else:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
601
  # Выбор типа задачи
 
602
  task_type = st.radio(
603
- "Тип задачи классификации:",
604
  ["Бинарная", "Многоклассовая", "Многометочная"],
605
- horizontal=True
 
606
  )
607
 
608
- # Создание разметки (упрощенная версия - пользователь должен разметить данные заранее)
609
- with st.expander("ℹ️ Информация о данных", expanded=False):
610
- st.info("💡 Для полноценной работы требуется размеченный датасет. Здесь показана демонстрация на синтетических данных, сгенерированных случайным образом.")
611
-
612
- # Генерация синтетических меток для демонстрации
613
- if "labels" not in st.session_state or st.session_state.get("task_type") != task_type:
614
- if task_type == "Бинарная":
615
- st.session_state["labels"] = np.random.choice([0, 1], size=len(texts))
616
- elif task_type == "Многоклассовая":
617
- st.session_state["labels"] = np.random.choice([0, 1, 2, 3], size=len(texts))
618
- elif task_type == "Многометочная":
619
- # Многометочная - создаем бинарные метки для каждой категории
620
- # Каждый документ ��ожет иметь несколько меток
621
- num_labels = 4
622
- st.session_state["labels"] = np.random.randint(0, 2, size=(len(texts), num_labels))
623
- st.session_state["num_labels"] = num_labels
624
- st.session_state["task_type"] = task_type
625
-
626
- labels = st.session_state["labels"]
627
 
628
- # Предобработка
629
- st.subheader("🔧 Предобработка")
630
- preprocess_config = PreprocessingConfig(
631
- lowercase=True,
632
- remove_html=True,
633
- lemmatize=False, # Упрощенно для скорости
634
- remove_stopwords=False
635
- )
636
- preprocessor = TextPreprocessor(preprocess_config)
637
- processed_texts = preprocessor.preprocess_batch(texts[:min(100, len(texts))]) # Ограничиваем для демо
 
 
 
 
 
 
 
638
 
639
- # Векторизация
640
- st.subheader("🧮 Векторизация")
641
- vectorization_method = st.selectbox(
642
- "Метод векторизации:",
643
- ["tfidf", "bow"]
644
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
645
 
646
- if st.button("🔨 Векторизовать тексты", key="vectorize_for_classification"):
647
- with st.spinner("Векторизация..."):
648
- X, vectorizer = vectorize_with_classical(
649
- processed_texts,
650
- method=vectorization_method,
651
- ngram_range=(1, 2),
652
- max_features=1000
653
- )
654
- st.session_state["X_classification"] = X
655
- st.session_state["vectorizer_classification"] = vectorizer
656
- st.success(f"Векторизовано {len(processed_texts)} текстов, размерность: {X.shape}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
657
 
658
- # Классификация
659
- if "X_classification" in st.session_state:
660
- X = st.session_state["X_classification"]
661
- y = labels[:len(processed_texts)]
 
 
 
 
 
 
 
 
 
 
 
 
662
 
663
- # Разделение на train/test
664
- from sklearn.model_selection import train_test_split
665
- # Для multilabel stratify не поддерживается напрямую
666
- if task_type == "Многометочная":
667
- X_train, X_test, y_train, y_test = train_test_split(
668
- X, y, test_size=0.2, random_state=42
669
- )
 
 
 
670
  else:
671
- X_train, X_test, y_train, y_test = train_test_split(
672
- X, y, test_size=0.2, random_state=42, stratify=y
673
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
674
 
675
- st.subheader("🎯 Обучение классификаторов")
676
- selected_models = st.multiselect(
677
- "Выберите модели:",
678
- ["Logistic Regression", "SVM", "Random Forest"],
679
- default=["Logistic Regression", "Random Forest"]
680
  )
 
681
 
682
- if st.button("🚀 Обучить модели", key="train_classifiers"):
683
- configs = []
684
- if "Logistic Regression" in selected_models:
685
- configs.append(ClassifierConfig(name="Logistic Regression", model_type="lr"))
686
- if "SVM" in selected_models:
687
- configs.append(ClassifierConfig(name="SVM", model_type="svm", params={"kernel": "linear"}))
688
- if "Random Forest" in selected_models:
689
- configs.append(ClassifierConfig(name="Random Forest", model_type="rf"))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
690
 
691
- with st.spinner("Обучение моделей..."):
692
- # Определяем тип задачи
 
 
 
 
 
 
 
 
 
 
 
 
693
  if task_type == "Многометочная":
694
- task_type_str = "multilabel"
695
- elif task_type == "Многоклассовая":
696
- task_type_str = "multiclass"
697
  else:
698
- task_type_str = "binary"
 
 
 
 
 
 
 
 
 
 
 
 
699
 
700
- results_df = compare_classifiers(
701
- X_train, y_train, X_test, y_test,
702
- configs,
703
- task_type=task_type_str
 
704
  )
705
- st.session_state["classification_results"] = results_df
706
-
707
- if "classification_results" in st.session_state:
708
- st.subheader("📊 Результаты классификации")
709
- st.dataframe(st.session_state["classification_results"], use_container_width=True)
710
-
711
- # Важность признаков
712
- if "vectorizer_classification" in st.session_state and "X_classification" in st.session_state:
713
- st.subheader("🔍 Важные слова")
714
- vectorizer = st.session_state["vectorizer_classification"]
715
- if "Logistic Regression" in selected_models:
716
- # Создаем простую модель для демонстрации
717
- from sklearn.linear_model import LogisticRegression
718
- from sklearn.multioutput import MultiOutputClassifier
719
-
720
- # Получаем данные из session_state
721
- X_full = st.session_state["X_classification"]
722
- y_full = labels[:len(processed_texts)]
723
-
724
- # Для multilabel используем MultiOutputClassifier
725
- if task_type == "Многометочная":
726
- # Проверяем, что y_full - это 2D массив
727
- if len(y_full.shape) == 1:
728
- y_full = y_full.reshape(-1, 1)
729
- # Используем только часть данных для быстрой демонстрации
730
- X_demo = X_full[:min(100, len(X_full))]
731
- y_demo = y_full[:min(100, len(y_full))]
732
- model = MultiOutputClassifier(LogisticRegression(max_iter=1000, random_state=42))
733
- else:
734
- # Для бинарной и многоклассовой классификации используем обычную модель
735
- # Убеждаемся, что y_full - это 1D массив
736
- if len(y_full.shape) > 1:
737
- y_full = y_full.flatten() if y_full.shape[1] == 1 else y_full.argmax(axis=1)
738
- # Используем только часть данных для быстрой демонстрации
739
- X_demo = X_full[:min(100, len(X_full))]
740
- y_demo = y_full[:min(100, len(y_full))]
741
- model = LogisticRegression(max_iter=1000, random_state=42)
742
 
743
- try:
744
- model.fit(X_demo, y_demo)
745
- # Для multilabel берем первый классификатор
746
  if task_type == "Многометочная":
747
- base_model = model.estimators_[0] if hasattr(model, 'estimators_') and len(model.estimators_) > 0 else model
 
 
748
  else:
749
- base_model = model
750
- important_words = get_tfidf_important_words(vectorizer, base_model, class_idx=0, top_k=20)
751
- st.dataframe(important_words, use_container_width=True)
752
- except Exception as e:
753
- st.warning(f"Не удалось показать важные слова: {e}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
754
 
755
  # ======== Кластеризация (ЛР4) ========
756
  with main_tabs[4]:
 
41
  )
42
  from src.dimensionality import SVDConfig, run_lsa, embed_2d, explained_variance_table, top_terms_dataframe
43
  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
44
+
45
+ # Проверяем доступность GloVe
46
+ try:
47
+ from glove import Glove, Corpus
48
+ GLOVE_AVAILABLE = True
49
+ except ImportError:
50
+ try:
51
+ from glove_python import Glove, Corpus
52
+ GLOVE_AVAILABLE = True
53
+ except ImportError:
54
+ GLOVE_AVAILABLE = False
55
  from src.semantic_experiments import vector_arithmetic, semantic_axis, nearest_neighbors
56
  from src.text_preprocessing import TextPreprocessor, PreprocessingConfig, extract_meta_features, vectorize_with_classical, vectorize_with_embeddings
57
  from src.classical_classifiers import ClassicalClassifiers, ClassifierConfig, compare_classifiers, evaluate_classifier
 
523
  index=0, horizontal=True,
524
  help="Предобработанные = применены настройки из блока Предобработка на левой панели"
525
  )
526
+ # Формируем список доступных моделей
527
+ available_models = ["w2v", "fasttext", "doc2vec"]
528
+ if GLOVE_AVAILABLE:
529
+ available_models.append("glove")
530
+ model_type = st.selectbox("Модель", available_models, index=0)
531
+
532
+ # Показываем предупреждение, если GloVe выбран, но недоступен
533
+ if model_type == "glove" and not GLOVE_AVAILABLE:
534
+ st.warning("⚠️ GloVe не установлен. Установите: `pip install glove-python-binary`. Модель не будет обучена.")
535
  vector_size = st.slider("Размерность", 50, 600, 300, step=50)
536
  window = st.slider("Окно контекста", 2, 15, 8)
537
  min_count = st.slider("Min count", 1, 20, 2)
 
551
  st.download_button("📥 Скачать обучающий корпус (.txt)", data=corpus_txt, file_name="training_corpus.txt", mime="text/plain")
552
 
553
  if st.button("🎓 Обучить модель", key="train_embeddings"):
554
+ # Проверяем доступность GloVe перед обучением
555
+ if model_type == "glove" and not GLOVE_AVAILABLE:
556
+ st.error("❌ GloVe не установлен. Установите: `pip install glove-python-binary`")
557
+ st.stop()
558
+
559
  cfg = EmbTrainConfig(
560
  model_type=model_type,
561
  vector_size=int(vector_size),
 
566
  dm=1 if dm == "pv-dm" else 0,
567
  )
568
  with st.spinner("Обучаем модель..."):
569
+ try:
570
+ model, tt = train_embeddings_model(corpus, cfg)
571
+ st.session_state["emb_model"] = model
572
+ st.session_state["emb_train_time"] = tt
573
+ st.success(f"Модель обучена за {tt:.2f} с")
574
+ except ImportError as e:
575
+ if "GloVe" in str(e) or "glove" in str(e).lower():
576
+ st.error(f"❌ GloVe не установлен. Установите: `pip install glove-python-binary`")
577
+ else:
578
+ st.error(f"❌ Ошибка импорта: {e}")
579
+ except Exception as e:
580
+ st.error(f"❌ Ошибка при обучении модели: {e}")
581
 
582
  if "emb_model" in st.session_state:
583
  model = st.session_state["emb_model"]
 
626
  # ======== Классификация (ЛР3) ========
627
  with main_tabs[3]:
628
  st.subheader("📊 Классификация текстов")
629
+ st.markdown("**Лабораторная работа №3: Сравнительный анализ методов классификации текстов**")
630
 
631
+ # Загрузка корпуса из ЛР1
632
+ st.subheader("📁 Загрузка корпуса из ЛР №1")
633
+ corpus_source = st.radio(
634
+ "Источник корпуса:",
635
+ ["Из загруженных данных", "Из файла к��рпуса (JSONL)"],
636
+ horizontal=True,
637
+ key="classification_corpus_source"
638
+ )
639
+
640
+ classification_texts = []
641
+ classification_articles = []
642
+
643
+ if corpus_source == "Из загруженных данных":
644
+ if not texts:
645
+ st.warning("⚠️ Сначала загрузите данные на главной странице или используйте корпус из файла.")
646
+ else:
647
+ classification_texts = texts
648
+ st.info(f"✅ Используется {len(classification_texts)} текстов из загруженных данных")
649
  else:
650
+ # Загрузка из файла корпуса
651
+ corpus_file_path = st.text_input(
652
+ "Путь к файлу корпуса (JSONL):",
653
+ value="data/raw_corpus.jsonl",
654
+ key="classification_corpus_path"
655
+ )
656
+
657
+ if st.button("📂 Загрузить корпус", key="load_classification_corpus"):
658
+ if os.path.exists(corpus_file_path):
659
+ try:
660
+ from src.utils import load_jsonl
661
+ classification_articles = load_jsonl(corpus_file_path)
662
+ classification_texts = [article.get('text', '') for article in classification_articles if article.get('text')]
663
+ st.success(f"✅ Загружено {len(classification_texts)} статей из корпуса")
664
+ st.session_state["classification_articles"] = classification_articles
665
+ except Exception as e:
666
+ st.error(f"❌ Ошибка при загрузке корпуса: {e}")
667
+ else:
668
+ st.error(f"❌ Файл не найден: {corpus_file_path}")
669
+
670
+ # Используем сохраненные статьи, если они есть
671
+ if "classification_articles" in st.session_state and not classification_texts:
672
+ classification_articles = st.session_state["classification_articles"]
673
+ classification_texts = [article.get('text', '') for article in classification_articles if article.get('text')]
674
+
675
+ if not classification_texts:
676
+ st.warning("⚠️ Загрузите корпус для классификации.")
677
+ st.info("💡 Используйте корпус из ЛР №1 в формате JSONL с полями: title, text, date, url, category")
678
+ else:
679
+ # Показываем информацию о корпусе
680
+ total_words = sum(len(text.split()) for text in classification_texts)
681
+ st.info(f"📊 Корпус: {len(classification_texts)} документов, ~{total_words:,} слов")
682
+
683
  # Выбор типа задачи
684
+ st.subheader("🎯 Тип задачи классификации")
685
  task_type = st.radio(
686
+ "Выберите тип задачи:",
687
  ["Бинарная", "Многоклассовая", "Многометочная"],
688
+ horizontal=True,
689
+ help="Бинарная: тональность (позитивные/негативные)\nМногоклассовая: категории (политика, экономика, спорт, культура)\nМногометочная: несколько категорий одновременно"
690
  )
691
 
692
+ # Разметка данных
693
+ st.subheader("🏷️ Разметка данных")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
694
 
695
+ # Функция для автоматической разметки по тональности (бинарная)
696
+ def label_sentiment_binary(texts_list):
697
+ """Простая эвристика для определения тональности."""
698
+ positive_words = ['хорошо', 'отлично', 'успех', 'победа', 'рост', 'улучшение', 'развитие',
699
+ 'достижение', 'прогресс', 'радость', 'счастье', 'праздник']
700
+ negative_words = ['плохо', 'проблема', 'кризис', 'падение', 'ухудшение', 'поражение',
701
+ 'ошибка', 'неудача', 'трагедия', 'катастрофа', 'война', 'конфликт']
702
+
703
+ labels = []
704
+ for text in texts_list:
705
+ text_lower = text.lower()
706
+ pos_count = sum(1 for word in positive_words if word in text_lower)
707
+ neg_count = sum(1 for word in negative_words if word in text_lower)
708
+ # 0 = негативная, 1 = позитивная
709
+ label = 1 if pos_count > neg_count else 0
710
+ labels.append(label)
711
+ return np.array(labels)
712
 
713
+ # Функция для разметки по категориям (многоклассовая)
714
+ def label_categories_multiclass(articles_list):
715
+ """Разметка по категориям из метаданных или по ключевым словам."""
716
+ categories_map = {
717
+ 'политика': 0,
718
+ 'экономика': 1,
719
+ 'спорт': 2,
720
+ 'культура': 3,
721
+ 'технологии': 4,
722
+ 'общество': 5
723
+ }
724
+
725
+ category_keywords = {
726
+ 0: ['политика', 'правительство', 'президент', 'министр', 'выборы', 'парламент', 'депутат'],
727
+ 1: ['экономика', 'рынок', 'компания', 'бизнес', 'финансы', 'банк', 'инвестиции', 'рубль'],
728
+ 2: ['спорт', 'футбол', 'хоккей', 'олимпиада', 'чемпионат', 'матч', 'игрок', 'команда'],
729
+ 3: ['культура', 'кино', 'театр', 'музыка', 'литература', 'искусство', 'выставка', 'концерт'],
730
+ 4: ['технологии', 'интернет', 'компьютер', 'смартфон', 'приложение', 'цифровой', 'IT'],
731
+ 5: ['общество', 'люди', 'город', 'образование', 'здравоохранение', 'социальный']
732
+ }
733
+
734
+ labels = []
735
+ for article in articles_list:
736
+ # Сначала пробуем взять категорию из метаданных
737
+ category = article.get('category', '').lower() if isinstance(article, dict) else ''
738
+
739
+ if category and category in categories_map:
740
+ labels.append(categories_map[category])
741
+ else:
742
+ # Определяем по ключевым словам в тексте
743
+ text = (article.get('text', '') + ' ' + article.get('title', '')).lower() if isinstance(article, dict) else str(article).lower()
744
+ scores = {}
745
+ for cat_id, keywords in category_keywords.items():
746
+ scores[cat_id] = sum(1 for kw in keywords if kw in text)
747
+
748
+ if max(scores.values()) > 0:
749
+ labels.append(max(scores, key=scores.get))
750
+ else:
751
+ labels.append(0) # По умолчанию политика
752
+
753
+ return np.array(labels)
754
 
755
+ # Функция для многометочной разметки
756
+ def label_categories_multilabel(articles_list):
757
+ """Многометочная разметка - документ может иметь несколько категорий."""
758
+ category_keywords = {
759
+ 'политика': ['политика', 'правительство', 'президент', 'министр', 'выборы'],
760
+ 'экономика': ['экономика', 'рынок', 'компания', 'бизнес', 'финансы'],
761
+ 'спорт': ['спорт', 'футбол', 'хоккей', 'олимпиада', 'чемпионат'],
762
+ 'культура': ['культура', 'кино', 'театр', 'музыка', 'искусство']
763
+ }
764
+
765
+ labels = []
766
+ for article in articles_list:
767
+ text = (article.get('text', '') + ' ' + article.get('title', '')).lower() if isinstance(article, dict) else str(article).lower()
768
+ doc_labels = []
769
+
770
+ for category, keywords in category_keywords.items():
771
+ # Документ относится к категории, если содержит хотя бы одно ключевое слово
772
+ if any(kw in text for kw in keywords):
773
+ doc_labels.append(1)
774
+ else:
775
+ doc_labels.append(0)
776
+
777
+ labels.append(doc_labels)
778
+
779
+ return np.array(labels)
780
 
781
+ # Выполняем разметку
782
+ if "classification_labels" not in st.session_state or st.session_state.get("classification_task_type") != task_type:
783
+ with st.spinner("Выполняется разметка данных..."):
784
+ if task_type == "Бинарная":
785
+ labels = label_sentiment_binary(classification_texts)
786
+ st.session_state["classification_labels"] = labels
787
+ st.session_state["classification_task_type"] = task_type
788
+ elif task_type == "Многоклассовая":
789
+ labels = label_categories_multiclass(classification_articles if classification_articles else classification_texts)
790
+ st.session_state["classification_labels"] = labels
791
+ st.session_state["classification_task_type"] = task_type
792
+ elif task_type == "Многометочная":
793
+ labels = label_categories_multilabel(classification_articles if classification_articles else classification_texts)
794
+ st.session_state["classification_labels"] = labels
795
+ st.session_state["classification_task_type"] = task_type
796
+ st.session_state["num_labels"] = labels.shape[1] if len(labels.shape) > 1 else 4
797
 
798
+ # Показываем статистику разметки
799
+ if task_type == "Бинарная":
800
+ unique, counts = np.unique(labels, return_counts=True)
801
+ st.success(f"✅ Разметка завершена: {dict(zip(['Негативные', 'Позитивные'], counts))}")
802
+ elif task_type == "Многоклассовая":
803
+ unique, counts = np.unique(labels, return_counts=True)
804
+ category_names = ['Политика', 'Экономика', 'Спорт', 'Культура', 'Технологии', 'Общество']
805
+ dist = {category_names[i] if i < len(category_names) else f'Класс {i}': count
806
+ for i, count in zip(unique, counts)}
807
+ st.success(f"✅ Разметка завершена. Распределение: {dist}")
808
  else:
809
+ # Многометочная
810
+ category_names = ['Политика', 'Экономика', 'Спорт', 'Культура']
811
+ if len(labels.shape) > 1:
812
+ counts = labels.sum(axis=0)
813
+ dist = {name: int(count) for name, count in zip(category_names[:len(counts)], counts)}
814
+ st.success(f"✅ Разметка завершена. Документов по категориям: {dist}")
815
+
816
+ labels = st.session_state.get("classification_labels", np.array([]))
817
+
818
+ if len(labels) == 0:
819
+ st.warning("⚠️ Разметка не выполнена. Пожалуйста, дождитесь завершения разметки.")
820
+ else:
821
+ # Предобработка
822
+ st.subheader("🔧 Предобработка текстов")
823
+ max_docs = st.slider(
824
+ "Максимальное количество документов для обработки:",
825
+ min_value=100,
826
+ max_value=min(10000, len(classification_texts)),
827
+ value=min(1000, len(classification_texts)),
828
+ step=100,
829
+ help="Для ускорения можно ограничить количество документов"
830
+ )
831
 
832
+ preprocess_config = PreprocessingConfig(
833
+ lowercase=True,
834
+ remove_html=True,
835
+ lemmatize=st.checkbox("Использовать лемматизацию", value=False, help="Замедляет обработку, но улучшает качество"),
836
+ remove_stopwords=st.checkbox("Удалять стоп-слова", value=False)
837
  )
838
+ preprocessor = TextPreprocessor(preprocess_config)
839
 
840
+ if st.button("🔄 Выполнить предобработку", key="preprocess_classification"):
841
+ with st.spinner(f"Обрабатываем {max_docs} документов..."):
842
+ processed_texts = preprocessor.preprocess_batch(classification_texts[:max_docs])
843
+ st.session_state["processed_classification_texts"] = processed_texts
844
+ st.session_state["classification_max_docs"] = max_docs
845
+ st.success(f" Обработано {len(processed_texts)} документов")
846
+
847
+ # Используем сохраненные обработанные тексты
848
+ if "processed_classification_texts" in st.session_state:
849
+ processed_texts = st.session_state["processed_classification_texts"]
850
+ max_docs_used = st.session_state.get("classification_max_docs", len(processed_texts))
851
+ labels_used = labels[:max_docs_used]
852
+
853
+ # Векторизация
854
+ st.subheader("🧮 Векторизация")
855
+ vectorization_method = st.selectbox(
856
+ "Метод векторизации:",
857
+ ["tfidf", "bow"],
858
+ help="TF-IDF: учитывает важность терминов\nBoW: простой подсчет частот"
859
+ )
860
+
861
+ ngram_max = st.number_input("Максимальный n-gram", 1, 3, 2, help="1=униграммы, 2=биграммы, 3=триграммы")
862
+ max_features = st.number_input("Максимальное количество признаков", 100, 10000, 1000, step=100)
863
+
864
+ if st.button("🔨 Векторизовать тексты", key="vectorize_for_classification"):
865
+ with st.spinner("Векторизация..."):
866
+ X, vectorizer = vectorize_with_classical(
867
+ processed_texts,
868
+ method=vectorization_method,
869
+ ngram_range=(1, ngram_max),
870
+ max_features=max_features
871
+ )
872
+ st.session_state["X_classification"] = X
873
+ st.session_state["vectorizer_classification"] = vectorizer
874
+ st.success(f"✅ Векторизовано {len(processed_texts)} текстов, размерность: {X.shape}")
875
+
876
+ # Классификация
877
+ if "X_classification" in st.session_state:
878
+ X = st.session_state["X_classification"]
879
+ y = labels_used
880
 
881
+ # Разделение на train/validation/test (70/15/15)
882
+ from sklearn.model_selection import train_test_split
883
+
884
+ # Сначала разделяем на train (70%) и temp (30%)
885
+ if task_type == "Многометочная":
886
+ X_train, X_temp, y_train, y_temp = train_test_split(
887
+ X, y, test_size=0.3, random_state=42
888
+ )
889
+ else:
890
+ X_train, X_temp, y_train, y_temp = train_test_split(
891
+ X, y, test_size=0.3, random_state=42, stratify=y
892
+ )
893
+
894
+ # Затем temp разделяем на validation (15%) и test (15%)
895
  if task_type == "Многометочная":
896
+ X_val, X_test, y_val, y_test = train_test_split(
897
+ X_temp, y_temp, test_size=0.5, random_state=42
898
+ )
899
  else:
900
+ X_val, X_test, y_val, y_test = train_test_split(
901
+ X_temp, y_temp, test_size=0.5, random_state=42, stratify=y_temp
902
+ )
903
+
904
+ # Показываем статистику разделения
905
+ st.subheader("📊 Разделение данных")
906
+ col1, col2, col3 = st.columns(3)
907
+ with col1:
908
+ st.metric("Обучающая выборка", f"{len(X_train)} ({len(X_train)/len(X)*100:.1f}%)")
909
+ with col2:
910
+ st.metric("Валидационная выборка", f"{len(X_val)} ({len(X_val)/len(X)*100:.1f}%)")
911
+ with col3:
912
+ st.metric("Тестовая выборка", f"{len(X_test)} ({len(X_test)/len(X)*100:.1f}%)")
913
 
914
+ st.subheader("🎯 Обучение классификаторов")
915
+ selected_models = st.multiselect(
916
+ "Выберите модели:",
917
+ ["Logistic Regression", "SVM", "Random Forest"],
918
+ default=["Logistic Regression", "Random Forest"]
919
  )
920
+
921
+ if st.button("🚀 Обучить модели", key="train_classifiers"):
922
+ configs = []
923
+ if "Logistic Regression" in selected_models:
924
+ configs.append(ClassifierConfig(name="Logistic Regression", model_type="lr"))
925
+ if "SVM" in selected_models:
926
+ configs.append(ClassifierConfig(name="SVM", model_type="svm", params={"kernel": "linear"}))
927
+ if "Random Forest" in selected_models:
928
+ configs.append(ClassifierConfig(name="Random Forest", model_type="rf"))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
929
 
930
+ with st.spinner("Обучение моделей..."):
931
+ # Определяем тип задачи
 
932
  if task_type == "Многометочная":
933
+ task_type_str = "multilabel"
934
+ elif task_type == "Многоклассовая":
935
+ task_type_str = "multiclass"
936
  else:
937
+ task_type_str = "binary"
938
+
939
+ results_df = compare_classifiers(
940
+ X_train, y_train, X_test, y_test,
941
+ configs,
942
+ task_type=task_type_str
943
+ )
944
+ st.session_state["classification_results"] = results_df
945
+
946
+ if "classification_results" in st.session_state:
947
+ st.subheader("📊 Результаты классификации")
948
+ st.dataframe(st.session_state["classification_results"], use_container_width=True)
949
+
950
+ # Важность признаков
951
+ if "vectorizer_classification" in st.session_state and "X_classification" in st.session_state:
952
+ st.subheader("🔍 Важные слова")
953
+ vectorizer = st.session_state["vectorizer_classification"]
954
+ if "Logistic Regression" in selected_models:
955
+ # Создаем простую модель для демонстрации
956
+ from sklearn.linear_model import LogisticRegression
957
+ from sklearn.multioutput import MultiOutputClassifier
958
+
959
+ # Получаем данные из session_state
960
+ X_full = st.session_state["X_classification"]
961
+ y_full = labels_used
962
+
963
+ # Для multilabel используем MultiOutputClassifier
964
+ if task_type == "Многометочная":
965
+ # Проверяем, что y_full - это 2D массив
966
+ if len(y_full.shape) == 1:
967
+ y_full = y_full.reshape(-1, 1)
968
+ # Используем только часть данных для быстрой демонстрации
969
+ X_demo = X_full[:min(100, len(X_full))]
970
+ y_demo = y_full[:min(100, len(y_full))]
971
+ model = MultiOutputClassifier(LogisticRegression(max_iter=1000, random_state=42))
972
+ else:
973
+ # Для бинарной и многоклассовой классификации используем обычную модель
974
+ # Убеждаемся, что y_full - это 1D массив
975
+ if len(y_full.shape) > 1:
976
+ y_full = y_full.flatten() if y_full.shape[1] == 1 else y_full.argmax(axis=1)
977
+ # Используем только часть данных для быстрой демонстрации
978
+ X_demo = X_full[:min(100, len(X_full))]
979
+ y_demo = y_full[:min(100, len(y_full))]
980
+ model = LogisticRegression(max_iter=1000, random_state=42)
981
+
982
+ try:
983
+ model.fit(X_demo, y_demo)
984
+ # Для multilabel берем первый классификатор
985
+ if task_type == "Многометочная":
986
+ base_model = model.estimators_[0] if hasattr(model, 'estimators_') and len(model.estimators_) > 0 else model
987
+ else:
988
+ base_model = model
989
+ important_words = get_tfidf_important_words(vectorizer, base_model, class_idx=0, top_k=20)
990
+ st.dataframe(important_words, use_container_width=True)
991
+ except Exception as e:
992
+ st.warning(f"Не удалось показать важные слова: {e}")
993
 
994
  # ======== Кластеризация (ЛР4) ========
995
  with main_tabs[4]: