Kolesnikov Dmitry commited on
Commit
83b4881
·
1 Parent(s): 9e5f314

feat: Вторая лабораторка

Browse files
COMPLETED.md CHANGED
@@ -90,3 +90,4 @@ NLP_Homework_1/
90
  **🎓 Лабораторная работа №1 выполнена успешно!**
91
 
92
  *Сравнительный анализ методов токенизации и нормализации текста на материале русскоязычных новостных корпусов*
 
 
90
  **🎓 Лабораторная работа №1 выполнена успешно!**
91
 
92
  *Сравнительный анализ методов токенизации и нормализации текста на материале русскоязычных новостных корпусов*
93
+
REPORT.md DELETED
@@ -1,162 +0,0 @@
1
- # 📋 Отчет о выполнении лабораторной работы №1
2
-
3
- **Тема:** Сравнительный анализ методов токенизации и нормализации текста на материале русскоязычных новостных корпусов
4
-
5
- **Дата выполнения:** 2025-01-27
6
-
7
- ## ✅ Выполненные задачи
8
-
9
- ### 1. Формирование экспериментального корпуса текстов ✅
10
- - **Реализован модуль `scrapers.py`** для автоматического сбора данных
11
- - **Поддерживаемые источники:** lenta.ru, ria.ru, tass.ru, kommersant.ru, meduza.io
12
- - **Собран корпус:** 50,000+ слов из русскоязычных новостных источников
13
- - **Формат данных:** JSONL с полями url, title, text, date, category
14
- - **Функции:** RSS-парсинг, sitemap-сканирование, вежливые задержки, robots.txt
15
-
16
- ### 2. Предварительная обработка и очистка текста ✅
17
- - **Создан модуль `text_cleaner.py`** для первичной очистки
18
- - **Функции:**
19
- - Удаление HTML-разметки
20
- - Стандартизация пробельных символов
21
- - Удаление служебных символов
22
- - Фильтрация стоп-слов (русский язык + новостные)
23
- - Удаление коротких и числовых токенов
24
- - **Конфигурируемость:** параметры очистки настраиваются
25
-
26
- ### 3. Универсальный модуль предобработки ✅
27
- - **Реализован `universal_preprocessor.py`** для стандартизации текста
28
- - **Возможности:**
29
- - Замена URL, email, телефонов на унифицированные токены
30
- - Раскрытие сокращений (т.е., г., ул., ООО, МВД и др.)
31
- - Нормализация пунктуации и кавычек
32
- - Стандартизация пробелов
33
- - **Конфигурируемость:** класс PreprocessingConfig для настройки
34
-
35
- ### 4. Сравнительный анализ методов токенизации ✅
36
- - **Создан модуль `tokenizers_cmp.py`** для комплексного сравнения
37
- - **Поддерживаемые методы:**
38
- - Наивная токенизация (по пробелам)
39
- - Регулярные выражения
40
- - Razdel (специально для русского языка)
41
- - NLTK (word_tokenize)
42
- - SpaCy (ru_core_news_sm)
43
- - PyMorphy2 (лемматизация)
44
- - Стемминг (Porter, Snowball)
45
- - **Метрики оценки:**
46
- - Объем словаря
47
- - Скорость обработки
48
- - Коэффициент сжатия
49
- - Средняя длина токена
50
- - Разнообразие словаря
51
-
52
- ### 5. Обучение подсловных моделей токенизации ✅
53
- - **Реализован модуль `train_subword.py`** для обучения моделей
54
- - **Поддерживаемые алгоритмы:**
55
- - Byte Pair Encoding (BPE)
56
- - WordPiece
57
- - Unigram Language Model
58
- - SentencePiece
59
- - **Параметры обучения:**
60
- - Размер словаря: 8,000 – 32,000 токенов
61
- - Минимальная частота: 2-5
62
- - **Метрики оценки:**
63
- - Процент фрагментации слов
64
- - Коэффициент сжатия
65
- - Точность реконструкции
66
- - Время обучения
67
-
68
- ### 6. Веб-интерфейс для интерактивного анализа ✅
69
- - **Создано приложение `streamlit_app.py`** с полным функционалом
70
- - **Возможности:**
71
- - Загрузка данных (файлы, примеры, корпус)
72
- - Настройка предобработки и очистки
73
- - Выбор методов токенизации для сравнения
74
- - Интерактивная визуализация результатов
75
- - Экспорт данных (CSV, JSON)
76
- - **Визуализация:**
77
- - Сравнительные графики методов
78
- - Распределение длин токенов
79
- - Частотность токенов
80
- - Статистика по методам
81
-
82
- ### 7. Вспомогательные модули ✅
83
- - **Создан модуль `utils.py`** с утилитами:
84
- - Работа с файлами (JSON, JSONL)
85
- - Вычисление статистики текстов
86
- - Создание графиков и визуализаций
87
- - Валидация формата корпуса
88
- - Форматирование времени и прогресс-бары
89
-
90
- ## 📊 Результаты и выводы
91
-
92
- ### Технические достижения:
93
- 1. **Полнофункциональная система** анализа токенизации с веб-интерфейсом
94
- 2. **Автоматизированный сбор данных** с соблюдением этических норм
95
- 3. **Комплексное сравнение методов** с объективными метриками
96
- 4. **Обучение подсловных моделей** с различными параметрами
97
- 5. **Интерактивная визуализация** результатов анализа
98
-
99
- ### Практическая ценность:
100
- - **Готовое решение** для анализа токенизации на русском языке
101
- - **Модульная архитектура** позволяет легко расширять функционал
102
- - **Веб-интерфейс** делает систему доступной для пользователей без технических навыков
103
- - **Документированный код** с примерами использования
104
-
105
- ## 🚀 Инструкции по запуску
106
-
107
- ### Установка зависимостей:
108
- ```bash
109
- pip install -r requirements.txt
110
- ```
111
-
112
- ### Запуск веб-интерфейса:
113
- ```bash
114
- streamlit run src/streamlit_app.py
115
- ```
116
-
117
- ### Демонстрация функционала:
118
- ```bash
119
- python demo.py
120
- ```
121
-
122
- ### Сбор дополнительных данных:
123
- ```bash
124
- python src/scrapers.py --auto --out data/raw_corpus.jsonl --min_words 50000
125
- ```
126
-
127
- ## 📁 Структура проекта
128
-
129
- ```
130
- NLP_Homework_1/
131
- ├── data/ # Данные корпуса
132
- ├── src/ # Исходный код модулей
133
- ├── models/ # Обученные модели
134
- ├── results/ # Результаты анализа
135
- ├── notebooks/ # Jupyter notebooks
136
- ├── requirements.txt # Зависимости
137
- ├── demo.py # Демонстрационный скрипт
138
- └── README.md # Документация
139
- ```
140
-
141
- ## 🎯 Соответствие требованиям задания
142
-
143
- ✅ **Этап 1:** Формирование корпуса (50k+ слов)
144
- ✅ **Этап 2:** Предобработка и очистка текста
145
- ✅ **Этап 3:** Универсальный модуль предобработки
146
- ✅ **Этап 4:** Сравнительный анализ методов токенизации
147
- ✅ **Этап 5:** Обучение подсловных моделей
148
- ✅ **Этап 6:** Веб-интерфейс для интерактивного анализа
149
- ⏳ **Этап 7:** Публикация моделей в Hugging Face Hub (опционально)
150
-
151
- ## 💡 Рекомендации по использованию
152
-
153
- 1. **Для быстрого старта** используйте веб-интерфейс Streamlit
154
- 2. **Для глубокого анализа** запускайте модули программно
155
- 3. **Для расширения функционала** добавляйте новые методы в соответствующие модули
156
- 4. **Для production** рассмотрите оптимизацию производительности
157
-
158
- ## 📝 Заключение
159
-
160
- Лабораторная работа выполнена в полном объеме. Создана комплексная система для анализа методов токенизации и нормализации текста на русском языке, включающая все требуемые компоненты и дополнительные возможности для удобства использования.
161
-
162
- Система готова к использованию и может служить основой для дальнейших исследований в области обработки естественного языка.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
STATISTICS_FIX_EXPLANATION.md CHANGED
@@ -98,3 +98,4 @@ st.metric("Всего токенов", total_tokens) # Теперь прави
98
  - ✅ **Разнообразие словаря** - стало реалистичным
99
 
100
  **Теперь веб-интерфейс работает правильно!** 🎊
 
 
98
  - ✅ **Разнообразие словаря** - стало реалистичным
99
 
100
  **Теперь веб-интерфейс работает правильно!** 🎊
101
+
TOKENIZATION_EXPLANATION.md CHANGED
@@ -63,3 +63,4 @@ def tokenize_words_only(text):
63
  - **Для быстрого анализа**: используйте `naive`
64
 
65
  **Токенизация работает корректно!** 🎉
 
 
63
  - **Для быстрого анализа**: используйте `naive`
64
 
65
  **Токенизация работает корректно!** 🎉
66
+
requirements.txt CHANGED
@@ -11,10 +11,16 @@ streamlit
11
  matplotlib
12
  plotly
13
  scikit-learn
 
14
  feedparser
15
  seaborn
16
  wordcloud
17
  tqdm
 
 
 
 
 
18
  # pymorphy2 # Несовместим с Python 3.13+
19
  # transformers # Удалено по запросу пользователя
20
  # torch # Удалено по запросу пользователя
 
11
  matplotlib
12
  plotly
13
  scikit-learn
14
+ scipy
15
  feedparser
16
  seaborn
17
  wordcloud
18
  tqdm
19
+ # ЛР2 — векторизация и эмбеддинги
20
+ gensim
21
+ umap-learn
22
+ # fasttext # опционально, требует системную установку
23
+ # glove-python-binary # опционально
24
  # pymorphy2 # Несовместим с Python 3.13+
25
  # transformers # Удалено по запросу пользователя
26
  # torch # Удалено по запросу пользователя
results/vectorization_metrics.csv ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ Метод,N-граммы,Документов,Признаков,Ненулевых,Плотность,Время fit (с),Время transform (с),Память (MB) ~
2
+ bow,1-2,100,1739,32653,0.187769,0.0203,0.0167,0.75
3
+ tfidf,1-2,100,1739,32653,0.187769,0.0167,0.0138,0.75
run.sh CHANGED
@@ -1,9 +1,7 @@
1
  #!/usr/bin/env bash
2
  # -*- coding: utf-8 -*-
3
- """
4
- Скрипт для быстрого запуска системы анализа токенизации.
5
- Автоматически устанавливает зависимости и запускает веб-интерфейс.
6
- """
7
 
8
  echo "🚀 Запуск системы анализа токенизации"
9
  echo "====================================="
@@ -98,4 +96,5 @@ esac
98
  echo ""
99
  echo "✅ Работа завершена!"
100
  echo "📖 Документация: README.md"
101
- echo "📋 Отчет: REPORT.md"
 
 
1
  #!/usr/bin/env bash
2
  # -*- coding: utf-8 -*-
3
+ # Скрипт для быстрого запуска системы анализа токенизации.
4
+ # Автоматически устанавливает зависимости и запускает веб-интерфейс.
 
 
5
 
6
  echo "🚀 Запуск системы анализа токенизации"
7
  echo "====================================="
 
96
  echo ""
97
  echo "✅ Работа завершена!"
98
  echo "📖 Документация: README.md"
99
+ echo "📋 Отчет: FINAL_REPORT.md"
100
+
src/__pycache__/classical_vectorizers.cpython-313.pyc ADDED
Binary file (11.3 kB). View file
 
src/__pycache__/dimensionality.cpython-313.pyc ADDED
Binary file (4.92 kB). View file
 
src/__pycache__/embeddings_train.cpython-313.pyc ADDED
Binary file (10.1 kB). View file
 
src/__pycache__/semantic_experiments.cpython-313.pyc ADDED
Binary file (4.65 kB). View file
 
src/__pycache__/streamlit_app.cpython-313.pyc ADDED
Binary file (22.8 kB). View file
 
src/__pycache__/text_cleaner.cpython-313.pyc ADDED
Binary file (7.87 kB). View file
 
src/__pycache__/tokenizers_cmp.cpython-313.pyc ADDED
Binary file (19.5 kB). View file
 
src/__pycache__/train_subword.cpython-313.pyc ADDED
Binary file (19.5 kB). View file
 
src/__pycache__/universal_preprocessor.cpython-313.pyc ADDED
Binary file (14.5 kB). View file
 
src/__pycache__/utils.cpython-313.pyc ADDED
Binary file (18.9 kB). View file
 
src/classical_vectorizers.py ADDED
@@ -0,0 +1,214 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Классические методы векторизации текста: One-Hot, Bag-of-Words, TF-IDF с поддержкой n-грамм.
3
+ Предоставляет единый интерфейс fit/transform, вычисление метрик разреженности и размерности,
4
+ а также удобные функции для сравнения конфигураций и экспорта результатов.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import time
10
+ from dataclasses import dataclass
11
+ from typing import Dict, List, Optional, Tuple, Any
12
+
13
+ import numpy as np
14
+ import pandas as pd
15
+ from scipy import sparse
16
+ from sklearn.feature_extraction import DictVectorizer
17
+ from sklearn.feature_extraction.text import CountVectorizer, TfidfVectorizer
18
+
19
+
20
+ @dataclass
21
+ class VectorizationConfig:
22
+ method: str # onehot | bow | tfidf
23
+ ngram_range: Tuple[int, int] = (1, 1)
24
+ lowercase: bool = True
25
+ min_df: int | float = 1
26
+ max_df: int | float = 1.0
27
+ max_features: Optional[int] = None
28
+ analyzer: str = "word" # word | char | char_wb
29
+ smooth_idf: bool = True # для TF-IDF
30
+ sublinear_tf: bool = False # для TF-IDF
31
+
32
+
33
+ @dataclass
34
+ class VectorizationReport:
35
+ method_name: str
36
+ ngram_range: str
37
+ num_docs: int
38
+ num_features: int
39
+ nnz: int
40
+ density: float
41
+ build_time_sec: float
42
+ transform_time_sec: float
43
+ memory_estimate_mb: float
44
+
45
+
46
+ class ClassicalVectorizers:
47
+ """Универсальный интерфейс для классических векторизаторов текста."""
48
+
49
+ def __init__(self, config: VectorizationConfig):
50
+ self.config = config
51
+ self.vectorizer = self._create_vectorizer(config)
52
+
53
+ def _create_vectorizer(self, config: VectorizationConfig):
54
+ method = config.method.lower()
55
+
56
+ if method == "bow":
57
+ return CountVectorizer(
58
+ ngram_range=config.ngram_range,
59
+ lowercase=config.lowercase,
60
+ min_df=config.min_df,
61
+ max_df=config.max_df,
62
+ max_features=config.max_features,
63
+ analyzer=config.analyzer,
64
+ )
65
+
66
+ if method == "tfidf":
67
+ return TfidfVectorizer(
68
+ ngram_range=config.ngram_range,
69
+ lowercase=config.lowercase,
70
+ min_df=config.min_df,
71
+ max_df=config.max_df,
72
+ max_features=config.max_features,
73
+ analyzer=config.analyzer,
74
+ smooth_idf=config.smooth_idf,
75
+ sublinear_tf=config.sublinear_tf,
76
+ )
77
+
78
+ if method == "onehot":
79
+ # Реализуем через словари токенов -> 1 и DictVectorizer
80
+ return DictVectorizer(sparse=True)
81
+
82
+ raise ValueError(f"Неизвестный метод векторизации: {config.method}")
83
+
84
+ @staticmethod
85
+ def _texts_to_onehot_dicts(texts: List[str], ngram_range: Tuple[int, int]) -> List[Dict[str, int]]:
86
+ """Преобразует тексты в словари признаков для one-hot (включая n-граммы)."""
87
+ def extract_ngrams(tokens: List[str], n: int) -> List[str]:
88
+ return ["_".join(tokens[i : i + n]) for i in range(len(tokens) - n + 1)]
89
+
90
+ dicts: List[Dict[str, int]] = []
91
+ n_min, n_max = ngram_range
92
+ for text in texts:
93
+ tokens = text.split()
94
+ features: Dict[str, int] = {}
95
+ for n in range(n_min, n_max + 1):
96
+ if n == 1:
97
+ grams = tokens
98
+ else:
99
+ grams = extract_ngrams(tokens, n)
100
+ for g in grams:
101
+ features[g] = 1
102
+ dicts.append(features)
103
+ return dicts
104
+
105
+ @staticmethod
106
+ def _sparsity_metrics(X: sparse.spmatrix) -> Tuple[int, int, float, float]:
107
+ nnz = int(X.nnz)
108
+ num_docs, num_features = X.shape
109
+ total = num_docs * num_features
110
+ density = (nnz / total) if total > 0 else 0.0
111
+ mem_bytes = (nnz * (8 + 8 + 8)) # грубая оценка COO/CSR: data+indices+indptr
112
+ mem_mb = mem_bytes / (1024**2)
113
+ return num_features, nnz, density, mem_mb
114
+
115
+ def fit_transform(self, texts: List[str]) -> Tuple[sparse.spmatrix, VectorizationReport]:
116
+ start = time.time()
117
+
118
+ if isinstance(self.vectorizer, DictVectorizer):
119
+ dicts = self._texts_to_onehot_dicts(texts, self.config.ngram_range)
120
+ X = self.vectorizer.fit_transform(dicts)
121
+ else:
122
+ X = self.vectorizer.fit_transform(texts)
123
+
124
+ build_time = time.time() - start
125
+ # Дополнительное преобразование для оценки времени transform
126
+ t0 = time.time()
127
+ if isinstance(self.vectorizer, DictVectorizer):
128
+ _ = self.vectorizer.transform(dicts)
129
+ else:
130
+ _ = self.vectorizer.transform(texts)
131
+ transform_time = time.time() - t0
132
+
133
+ num_features, nnz, density, mem_mb = self._sparsity_metrics(X)
134
+
135
+ report = VectorizationReport(
136
+ method_name=self.config.method,
137
+ ngram_range=f"{self.config.ngram_range[0]}-{self.config.ngram_range[1]}",
138
+ num_docs=len(texts),
139
+ num_features=num_features,
140
+ nnz=nnz,
141
+ density=round(density, 6),
142
+ build_time_sec=round(build_time, 4),
143
+ transform_time_sec=round(transform_time, 4),
144
+ memory_estimate_mb=round(mem_mb, 2),
145
+ )
146
+ return X, report
147
+
148
+ def transform(self, texts: List[str]) -> sparse.spmatrix:
149
+ if isinstance(self.vectorizer, DictVectorizer):
150
+ dicts = self._texts_to_onehot_dicts(texts, self.config.ngram_range)
151
+ return self.vectorizer.transform(dicts)
152
+ return self.vectorizer.transform(texts)
153
+
154
+ def get_feature_names(self) -> List[str]:
155
+ if hasattr(self.vectorizer, "get_feature_names_out"):
156
+ return list(self.vectorizer.get_feature_names_out())
157
+ if hasattr(self.vectorizer, "feature_names_"):
158
+ return list(self.vectorizer.feature_names_)
159
+ return []
160
+
161
+
162
+ def compare_vectorizers(
163
+ texts: List[str],
164
+ configs: List[VectorizationConfig],
165
+ ) -> Tuple[pd.DataFrame, Dict[str, Any]]:
166
+ """
167
+ Сравнивает несколько конфигураций векторизации и возвращает таблицу метрик.
168
+ Дополнительно возвращает словарь с матрицами признаков по ключу <method|ngram>.
169
+ """
170
+ results: List[VectorizationReport] = []
171
+ matrices: Dict[str, Any] = {}
172
+
173
+ for cfg in configs:
174
+ vec = ClassicalVectorizers(cfg)
175
+ X, rep = vec.fit_transform(texts)
176
+ key = f"{cfg.method}:{cfg.ngram_range}"
177
+ matrices[key] = {"X": X, "vectorizer": vec}
178
+ results.append(rep)
179
+
180
+ df = pd.DataFrame([
181
+ {
182
+ "Метод": r.method_name,
183
+ "N-граммы": r.ngram_range,
184
+ "Документов": r.num_docs,
185
+ "Признаков": r.num_features,
186
+ "Ненулевых": r.nnz,
187
+ "Плотность": r.density,
188
+ "Время fit (с)": r.build_time_sec,
189
+ "Время transform (с)": r.transform_time_sec,
190
+ "Память (MB) ~": r.memory_estimate_mb,
191
+ }
192
+ for r in results
193
+ ])
194
+ return df.sort_values(["Метод", "N-граммы"]).reset_index(drop=True), matrices
195
+
196
+
197
+ def save_metrics(df: pd.DataFrame, output_csv: str) -> None:
198
+ df.to_csv(output_csv, index=False, encoding="utf-8")
199
+
200
+
201
+ if __name__ == "__main__":
202
+ sample = [
203
+ "Россия и Франция подписали новое соглашение по энергетике.",
204
+ "Путин встретился с президентом Турции и обсудил поставки газа.",
205
+ "В Москве пройдут переговоры министров иностранных дел.",
206
+ ]
207
+ configs = [
208
+ VectorizationConfig(method="onehot", ngram_range=(1, 1)),
209
+ VectorizationConfig(method="bow", ngram_range=(1, 2)),
210
+ VectorizationConfig(method="tfidf", ngram_range=(1, 3), sublinear_tf=True),
211
+ ]
212
+ df, _ = compare_vectorizers(sample, configs)
213
+ print(df)
214
+
src/dimensionality.py ADDED
@@ -0,0 +1,88 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Снижение размерности и тематическое моделирование для классических векторных представлений.
3
+ Поддерживаются: TruncatedSVD (LSA), визуализация UMAP/t-SNE, анализ объясненной дисперсии
4
+ и интерпретация компонент через топ-термины.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from dataclasses import dataclass
10
+ from typing import List, Tuple, Dict, Any, Optional
11
+
12
+ import numpy as np
13
+ import pandas as pd
14
+ from sklearn.decomposition import TruncatedSVD
15
+ from sklearn.manifold import TSNE
16
+
17
+ try:
18
+ import umap # type: ignore
19
+ UMAP_AVAILABLE = True
20
+ except Exception:
21
+ UMAP_AVAILABLE = False
22
+
23
+
24
+ @dataclass
25
+ class SVDConfig:
26
+ n_components: int = 100
27
+ random_state: int = 42
28
+
29
+
30
+ def run_lsa(X, feature_names: List[str], config: SVDConfig) -> Dict[str, Any]:
31
+ """
32
+ Выполняет LSA (TruncatedSVD) и возвращает компоненты, объясненную дисперсию
33
+ и топ-термины для каждой компоненты.
34
+ """
35
+ svd = TruncatedSVD(n_components=config.n_components, random_state=config.random_state)
36
+ X_reduced = svd.fit_transform(X)
37
+
38
+ explained = svd.explained_variance_ratio_
39
+ cumulative = np.cumsum(explained)
40
+
41
+ # Топ-термины на компоненту
42
+ components = svd.components_
43
+ top_terms_per_component: List[List[Tuple[str, float]]] = []
44
+ for comp in components:
45
+ idx = np.argsort(-np.abs(comp))[:20]
46
+ top_terms_per_component.append([(feature_names[i], float(comp[i])) for i in idx])
47
+
48
+ return {
49
+ "svd": svd,
50
+ "X_reduced": X_reduced,
51
+ "explained_variance_ratio": explained,
52
+ "explained_variance_ratio_cum": cumulative,
53
+ "top_terms_per_component": top_terms_per_component,
54
+ }
55
+
56
+
57
+ def embed_2d(X, method: str = "umap", random_state: int = 42, n_neighbors: int = 15, min_dist: float = 0.1):
58
+ """Проецирует матрицу признаков/векторов в 2D для визуализации (UMAP или t-SNE)."""
59
+ if method == "umap":
60
+ if not UMAP_AVAILABLE:
61
+ raise ImportError("umap-learn не установлен")
62
+ reducer = umap.UMAP(n_components=2, random_state=random_state, n_neighbors=n_neighbors, min_dist=min_dist)
63
+ return reducer.fit_transform(X)
64
+
65
+ if method == "tsne":
66
+ tsne = TSNE(n_components=2, random_state=random_state, init="pca", learning_rate="auto")
67
+ return tsne.fit_transform(X)
68
+
69
+ raise ValueError("method должен быть 'umap' или 'tsne'")
70
+
71
+
72
+ def explained_variance_table(explained_ratio: np.ndarray) -> pd.DataFrame:
73
+ cum = np.cumsum(explained_ratio)
74
+ return pd.DataFrame({
75
+ "Компонента": np.arange(1, len(explained_ratio) + 1),
76
+ "Доля дисперсии": np.round(explained_ratio, 6),
77
+ "Накопленная доля": np.round(cum, 6),
78
+ })
79
+
80
+
81
+ def top_terms_dataframe(top_terms: List[List[Tuple[str, float]]], top_k: int = 10) -> pd.DataFrame:
82
+ rows = []
83
+ for comp_idx, terms in enumerate(top_terms):
84
+ for term, weight in terms[:top_k]:
85
+ rows.append({"Компонента": comp_idx + 1, "Термин": term, "Вес": float(weight)})
86
+ return pd.DataFrame(rows)
87
+
88
+
src/embeddings_train.py ADDED
@@ -0,0 +1,195 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Обучение распределённых представлений: Word2Vec (CBOW/Skip-gram), FastText (cbow/skipgram), Doc2Vec (PV-DM/PV-DBOW).
3
+ Предоставляет единый интерфейс обучения, сохранения, загрузки и базовых оценок.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import os
9
+ import time
10
+ from dataclasses import dataclass
11
+ from pathlib import Path
12
+ from typing import Iterable, List, Optional, Tuple, Dict, Any
13
+
14
+ import numpy as np
15
+ import pandas as pd
16
+ from gensim.models import Word2Vec, FastText, Doc2Vec
17
+ from gensim.models.doc2vec import TaggedDocument
18
+ from gensim.utils import simple_preprocess
19
+
20
+
21
+ @dataclass
22
+ class TrainConfig:
23
+ model_type: str # w2v | fasttext | doc2vec
24
+ vector_size: int = 300
25
+ window: int = 8
26
+ min_count: int = 2
27
+ sg: int = 1 # 0=CBOW, 1=Skip-gram для w2v/fasttext; для doc2vec игнорируется
28
+ dm: int = 1 # 1=PV-DM, 0=PV-DBOW для doc2vec
29
+ epochs: int = 10
30
+ workers: int = 4
31
+ negative: int = 5
32
+ hs: int = 0
33
+ seed: int = 42
34
+
35
+
36
+ def _tokenize_corpus(texts: Iterable[str]) -> List[List[str]]:
37
+ return [simple_preprocess(t, deacc=False, min_len=1) for t in texts]
38
+
39
+
40
+ def train_word2vec(texts: Iterable[str], cfg: TrainConfig) -> Word2Vec:
41
+ sentences = _tokenize_corpus(texts)
42
+ model = Word2Vec(
43
+ vector_size=cfg.vector_size,
44
+ window=cfg.window,
45
+ min_count=cfg.min_count,
46
+ sg=cfg.sg,
47
+ workers=cfg.workers,
48
+ negative=cfg.negative,
49
+ hs=cfg.hs,
50
+ seed=cfg.seed,
51
+ )
52
+ model.build_vocab(sentences)
53
+ # Если словарь пуст из-за min_count — понижаем порог и повторяем
54
+ if len(model.wv) == 0 and cfg.min_count > 1:
55
+ model.min_count = 1
56
+ model.build_vocab(sentences, update=False)
57
+ if len(model.wv) == 0:
58
+ return model # вернем пустую модель; UI отобразит, что соседей нет
59
+ model.train(sentences, total_examples=len(sentences), epochs=cfg.epochs)
60
+ return model
61
+
62
+
63
+ def train_fasttext(texts: Iterable[str], cfg: TrainConfig) -> FastText:
64
+ sentences = _tokenize_corpus(texts)
65
+ model = FastText(
66
+ vector_size=cfg.vector_size,
67
+ window=cfg.window,
68
+ min_count=cfg.min_count,
69
+ sg=cfg.sg,
70
+ workers=cfg.workers,
71
+ negative=cfg.negative,
72
+ hs=cfg.hs,
73
+ seed=cfg.seed,
74
+ )
75
+ model.build_vocab(sentences)
76
+ if len(model.wv) == 0 and cfg.min_count > 1:
77
+ model.min_count = 1
78
+ model.build_vocab(sentences, update=False)
79
+ if len(model.wv) == 0:
80
+ return model
81
+ model.train(sentences, total_examples=len(sentences), epochs=cfg.epochs)
82
+ return model
83
+
84
+
85
+ def train_doc2vec(texts: Iterable[str], cfg: TrainConfig) -> Doc2Vec:
86
+ tagged = [TaggedDocument(simple_preprocess(t), [i]) for i, t in enumerate(texts)]
87
+ model = Doc2Vec(
88
+ vector_size=cfg.vector_size,
89
+ window=cfg.window,
90
+ min_count=cfg.min_count,
91
+ dm=cfg.dm,
92
+ workers=cfg.workers,
93
+ negative=cfg.negative,
94
+ hs=cfg.hs,
95
+ seed=cfg.seed,
96
+ )
97
+ model.build_vocab(tagged)
98
+ if len(model.wv) == 0 and cfg.min_count > 1:
99
+ model.min_count = 1
100
+ model.build_vocab(tagged, update=False)
101
+ if len(model.wv) == 0:
102
+ return model
103
+ model.train(tagged, total_examples=len(tagged), epochs=cfg.epochs)
104
+ return model
105
+
106
+
107
+ def train_model(texts: Iterable[str], cfg: TrainConfig):
108
+ t0 = time.time()
109
+ if cfg.model_type == "w2v":
110
+ model = train_word2vec(texts, cfg)
111
+ elif cfg.model_type == "fasttext":
112
+ model = train_fasttext(texts, cfg)
113
+ elif cfg.model_type == "doc2vec":
114
+ model = train_doc2vec(texts, cfg)
115
+ else:
116
+ raise ValueError("model_type должен быть 'w2v', 'fasttext' или 'doc2vec'")
117
+ train_time = time.time() - t0
118
+ return model, train_time
119
+
120
+
121
+ def save_model(model, out_path: str) -> None:
122
+ Path(os.path.dirname(out_path)).mkdir(parents=True, exist_ok=True)
123
+ model.save(out_path)
124
+
125
+
126
+ def load_model(path: str):
127
+ # gensim сам определит тип по расширению/классу
128
+ from gensim.models import Word2Vec as _W2V, FastText as _FT, Doc2Vec as _D2V
129
+ try:
130
+ return _W2V.load(path)
131
+ except Exception:
132
+ pass
133
+ try:
134
+ return _FT.load(path)
135
+ except Exception:
136
+ pass
137
+ return _D2V.load(path)
138
+
139
+
140
+ def evaluate_neighbors(model, test_words: List[str], topn: int = 10) -> Dict[str, List[Tuple[str, float]]]:
141
+ results: Dict[str, List[Tuple[str, float]]] = {}
142
+ kv = model.wv if hasattr(model, "wv") else model
143
+ for w in test_words:
144
+ if w in kv:
145
+ results[w] = kv.most_similar(w, topn=topn)
146
+ else:
147
+ results[w] = []
148
+ return results
149
+
150
+
151
+ def cosine_similarity(model, word_pairs: List[Tuple[str, str]]) -> List[Tuple[str, str, float]]:
152
+ out: List[Tuple[str, str, float]] = []
153
+ kv = model.wv if hasattr(model, "wv") else model
154
+ for a, b in word_pairs:
155
+ if a in kv and b in kv:
156
+ out.append((a, b, float(kv.similarity(a, b))))
157
+ else:
158
+ out.append((a, b, np.nan))
159
+ return out
160
+
161
+
162
+ def word_analogy(model, a: str, b: str, c: str, topn: int = 10) -> List[Tuple[str, float]]:
163
+ kv = model.wv if hasattr(model, "wv") else model
164
+ if all(token in kv for token in [a, b, c]):
165
+ return kv.most_similar(positive=[b, c], negative=[a], topn=topn)
166
+ return []
167
+
168
+
169
+ def export_training_report(cfg: TrainConfig, train_time: float, model_path: str, extra: Optional[Dict[str, Any]] = None) -> pd.DataFrame:
170
+ data = {
171
+ "Модель": cfg.model_type,
172
+ "Размерность": cfg.vector_size,
173
+ "Окно": cfg.window,
174
+ "Min count": cfg.min_count,
175
+ "Архитектура": ("skipgram" if cfg.sg == 1 else "cbow") if cfg.model_type in {"w2v", "fasttext"} else ("pv-dm" if cfg.dm == 1 else "pv-dbow"),
176
+ "Эпохи": cfg.epochs,
177
+ "Время обучения (с)": round(train_time, 2),
178
+ "Путь": model_path,
179
+ }
180
+ if extra:
181
+ data.update(extra)
182
+ return pd.DataFrame([data])
183
+
184
+
185
+ if __name__ == "__main__":
186
+ texts = [
187
+ "Москва является столицей России.",
188
+ "Париж — столица Франции.",
189
+ "Берлин — столица Германии.",
190
+ ]
191
+ cfg = TrainConfig(model_type="w2v", vector_size=100, window=5, epochs=5, sg=1)
192
+ model, tt = train_model(texts, cfg)
193
+ save_model(model, "models/sample_w2v.model")
194
+ print(evaluate_neighbors(model, ["россии", "франции"]))
195
+
src/semantic_experiments.py ADDED
@@ -0,0 +1,89 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Семантические эксперименты с эмбеддингами: косинусное сходство, аналогии, семантические оси,
3
+ качественный анализ ближайших соседей и построение матриц близости.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ from typing import Dict, List, Tuple
9
+ import numpy as np
10
+ import pandas as pd
11
+
12
+
13
+ def cosine(u: np.ndarray, v: np.ndarray) -> float:
14
+ nu = np.linalg.norm(u)
15
+ nv = np.linalg.norm(v)
16
+ if nu == 0 or nv == 0:
17
+ return float("nan")
18
+ return float(np.dot(u, v) / (nu * nv))
19
+
20
+
21
+ def pairwise_cosine_matrix(vectors: Dict[str, np.ndarray]) -> pd.DataFrame:
22
+ keys = list(vectors.keys())
23
+ mat = np.zeros((len(keys), len(keys)), dtype=float)
24
+ for i, ki in enumerate(keys):
25
+ for j, kj in enumerate(keys):
26
+ mat[i, j] = cosine(vectors[ki], vectors[kj])
27
+ return pd.DataFrame(mat, index=keys, columns=keys)
28
+
29
+
30
+ def vector_arithmetic(model, expression: str, topn: int = 10) -> List[Tuple[str, float]]:
31
+ """
32
+ Вычисляет выражения вида "король - мужчина + женщина" и возвращает ближайшие слова.
33
+ """
34
+ kv = model.wv if hasattr(model, "wv") else model
35
+
36
+ tokens = expression.replace("+", " + ").replace("-", " - ").split()
37
+ positives: List[str] = []
38
+ negatives: List[str] = []
39
+ sign = 1
40
+ for tok in tokens:
41
+ if tok == "+":
42
+ sign = 1
43
+ elif tok == "-":
44
+ sign = -1
45
+ else:
46
+ if sign == 1:
47
+ positives.append(tok)
48
+ else:
49
+ negatives.append(tok)
50
+ if not positives:
51
+ return []
52
+ try:
53
+ return kv.most_similar(positive=positives, negative=negatives, topn=topn)
54
+ except KeyError:
55
+ return []
56
+
57
+
58
+ def semantic_axis(model, a: str, b: str, words: List[str]) -> pd.DataFrame:
59
+ """
60
+ Строит семантическую ось (a->b) и проецирует заданные слова на эту ось.
61
+ Возвращает DataFrame с координатами проекции.
62
+ """
63
+ kv = model.wv if hasattr(model, "wv") else model
64
+ if a not in kv or b not in kv:
65
+ return pd.DataFrame(columns=["слово", "проекция"])
66
+ axis = kv[b] - kv[a]
67
+ axis_norm = axis / (np.linalg.norm(axis) + 1e-9)
68
+
69
+ rows = []
70
+ for w in words:
71
+ if w in kv:
72
+ proj = float(np.dot(kv[w], axis_norm))
73
+ else:
74
+ proj = np.nan
75
+ rows.append({"слово": w, "проекция": proj})
76
+ return pd.DataFrame(rows)
77
+
78
+
79
+ def nearest_neighbors(model, words: List[str], topn: int = 10) -> Dict[str, List[Tuple[str, float]]]:
80
+ kv = model.wv if hasattr(model, "wv") else model
81
+ out: Dict[str, List[Tuple[str, float]]] = {}
82
+ for w in words:
83
+ if w in kv:
84
+ out[w] = kv.most_similar(w, topn=topn)
85
+ else:
86
+ out[w] = []
87
+ return out
88
+
89
+
src/streamlit_app.py CHANGED
@@ -32,6 +32,15 @@ from src.text_cleaner import clean_text, clean_corpus_jsonl
32
  from src.universal_preprocessor import UniversalPreprocessor, PreprocessingConfig
33
  from src.tokenizers_cmp import TokenizationComparator, load_corpus_from_jsonl
34
  from src.train_subword import SubwordModelTrainer, SubwordModelConfig
 
 
 
 
 
 
 
 
 
35
 
36
 
37
  # Настройка страницы
@@ -284,187 +293,292 @@ def main():
284
  st.info("💡 Используйте боковую панель для загрузки файла или выберите примеры.")
285
  return
286
 
287
- # Применяем предобработку и очистку
 
 
 
 
 
 
 
 
288
  if use_preprocessing:
289
  config = PreprocessingConfig(**preprocessing_options)
290
  preprocessor = UniversalPreprocessor(config)
291
-
292
- processed_texts = []
293
- for text in texts:
294
  processed_text = preprocessor.preprocess(text)
295
  processed_text = clean_text(processed_text, **cleaning_options)
296
- processed_texts.append(processed_text)
297
- texts = processed_texts
298
-
299
- # Выбор методов токенизации
300
- st.subheader("🎯 Методы токенизации")
301
-
302
- comparator = TokenizationComparator()
303
- available_methods = list(comparator.methods.keys())
304
-
305
- selected_methods = st.multiselect(
306
- "Выберите методы для сравнения:",
307
- available_methods,
308
- default=available_methods[:3] if len(available_methods) >= 3 else available_methods
309
- )
310
-
311
- if not selected_methods:
312
- st.warning("⚠️ Пожалуйста, выберите хотя бы один метод токенизации.")
313
- return
314
-
315
- # Кнопка запуска анализа
316
- if st.button("🚀 Запустить анализ", type="primary"):
317
-
318
- with st.spinner("Выполняется анализ..."):
319
- # Сравниваем методы
320
- results_df = comparator.compare_methods(texts, selected_methods)
321
-
322
- # Сохраняем результаты в сессии
323
- st.session_state['results_df'] = results_df
324
- st.session_state['texts'] = texts
325
- st.session_state['selected_methods'] = selected_methods
326
 
327
- # Отображение результатов
328
- if 'results_df' in st.session_state:
329
- results_df = st.session_state['results_df']
330
- texts = st.session_state['texts']
331
- selected_methods = st.session_state['selected_methods']
332
-
333
- # Общая статистика
334
- st.subheader("📊 Общая статистика")
335
-
336
- col1, col2, col3, col4 = st.columns(4)
337
-
338
- with col1:
339
- st.metric("Количество текстов", len(texts))
340
-
341
- with col2:
342
- total_words = sum(len(text.split()) for text in texts)
343
- st.metric("Общее количество слов", total_words)
344
-
345
- with col3:
346
- avg_words_per_text = total_words / len(texts) if texts else 0
347
- st.metric("Среднее слов на текст", round(avg_words_per_text, 1))
348
-
349
- with col4:
350
- st.metric("Проанализировано методов", len(selected_methods))
351
-
352
- # Таблица результатов
353
- st.subheader("📋 Результаты сравнения")
354
- st.dataframe(results_df, use_container_width=True)
355
-
356
- # Графики сравнения
357
- st.subheader("📈 Визуализация результатов")
358
-
359
- comparison_chart = create_comparison_chart(results_df)
360
- st.plotly_chart(comparison_chart, use_container_width=True)
361
-
362
- # Детальный анализ для каждого метода
363
- st.subheader("🔍 Детальный анализ методов")
364
-
365
- method_tabs = st.tabs(selected_methods)
366
-
367
- for i, method in enumerate(selected_methods):
368
- with method_tabs[i]:
369
- # Анализируем все тексты для получения полной статистики
370
- if texts:
371
- # Анализируем все тексты
372
- all_tokens = []
373
- total_processing_time = 0
374
-
375
- for text in texts:
376
- tokens, processing_time = comparator.tokenize_text(text, method)
377
- all_tokens.extend(tokens)
378
- total_processing_time += processing_time
379
-
380
- # Используем первый текст для демонстрации
381
- sample_text = texts[0]
382
- sample_tokens, _ = comparator.tokenize_text(sample_text, method)
383
-
384
- col1, col2 = st.columns(2)
385
-
386
- with col1:
387
- st.write("**Исходный текст:**")
388
- st.text(sample_text[:200] + "..." if len(sample_text) > 200 else sample_text)
389
-
390
- with col2:
391
- st.write("**Токены (пример из первого текста):**")
392
- st.write(sample_tokens[:20]) # Показываем первые 20 токенов
393
- if len(sample_tokens) > 20:
394
- st.write(f"... и еще {len(sample_tokens) - 20} токенов")
395
-
396
- # Графики распределения
397
- col1, col2 = st.columns(2)
398
-
399
- with col1:
400
- dist_plot = create_token_distribution_plot(all_tokens, method)
401
- st.plotly_chart(dist_plot, use_container_width=True)
402
-
403
- with col2:
404
- freq_plot = create_frequency_plot(all_tokens, method)
405
- st.plotly_chart(freq_plot, use_container_width=True)
406
-
407
- # Статистика по методу (для всех текстов)
408
- from collections import Counter
409
- token_counts = Counter(all_tokens)
410
- unique_tokens = len(token_counts)
411
- total_tokens = len(all_tokens)
412
- vocabulary_diversity = unique_tokens / total_tokens if total_tokens > 0 else 0
413
-
414
- st.write("**Статистика:**")
415
- col1, col2, col3, col4 = st.columns(4)
416
-
417
- with col1:
418
- st.metric("Всего токенов", total_tokens)
419
-
420
- with col2:
421
- st.metric("Уникальных токенов", unique_tokens)
422
-
423
- with col3:
424
- st.metric("Разнообразие словаря", f"{vocabulary_diversity:.2%}")
425
-
426
- with col4:
427
- st.metric("Время обработки", f"{total_processing_time:.4f}с")
428
-
429
- # Экспорт результатов
430
- st.subheader("💾 Экспорт результатов")
431
-
432
- col1, col2 = st.columns(2)
433
-
434
- with col1:
435
- # CSV экспорт
436
- csv_data = results_df.to_csv(index=False, encoding='utf-8')
437
- st.download_button(
438
- label="📥 Скачать CSV",
439
- data=csv_data,
440
- file_name="tokenization_results.csv",
441
- mime="text/csv"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
442
  )
443
-
444
- with col2:
445
- # JSON экспорт
446
- json_data = results_df.to_json(orient='records', force_ascii=False, indent=2)
447
- st.download_button(
448
- label="📥 Скачать JSON",
449
- data=json_data,
450
- file_name="tokenization_results.json",
451
- mime="application/json"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
452
  )
453
-
454
- # Информация о проекте
455
- st.sidebar.markdown("---")
456
- st.sidebar.subheader("ℹ️ О проекте")
457
- st.sidebar.info("""
458
- **Лабораторная работа №1**
459
-
460
- Сравнительный анализ методов токенизации и нормализации текста на материале русскоязычных новостных корпусов.
461
-
462
- **Возможности:**
463
- - Сравнение различных методов токенизации
464
- - Предобработка и очистка текста
465
- - Визуализация результатов
466
- - Экспорт данных
467
- """)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
468
 
469
 
470
  if __name__ == "__main__":
 
32
  from src.universal_preprocessor import UniversalPreprocessor, PreprocessingConfig
33
  from src.tokenizers_cmp import TokenizationComparator, load_corpus_from_jsonl
34
  from src.train_subword import SubwordModelTrainer, SubwordModelConfig
35
+ from src.classical_vectorizers import (
36
+ VectorizationConfig,
37
+ ClassicalVectorizers,
38
+ compare_vectorizers,
39
+ save_metrics as save_vectorization_metrics,
40
+ )
41
+ from src.dimensionality import SVDConfig, run_lsa, embed_2d, explained_variance_table, top_terms_dataframe
42
+ 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
43
+ from src.semantic_experiments import vector_arithmetic, semantic_axis, nearest_neighbors
44
 
45
 
46
  # Настройка страницы
 
293
  st.info("💡 Используйте боковую панель для загрузки файла или выберите примеры.")
294
  return
295
 
296
+ # Сохраняем исходные тексты и метаданные источника
297
+ raw_texts = list(texts)
298
+ st.session_state["data_meta"] = {
299
+ "source": data_source,
300
+ "num_texts": len(raw_texts),
301
+ }
302
+
303
+ # Применяем предобработку и очистку, параллельно сохраняя обе версии
304
+ processed_texts = list(raw_texts)
305
  if use_preprocessing:
306
  config = PreprocessingConfig(**preprocessing_options)
307
  preprocessor = UniversalPreprocessor(config)
308
+ tmp = []
309
+ for text in raw_texts:
 
310
  processed_text = preprocessor.preprocess(text)
311
  processed_text = clean_text(processed_text, **cleaning_options)
312
+ tmp.append(processed_text)
313
+ processed_texts = tmp
314
+
315
+ # Положим обе версии в состояние для явного выбора на вкладках
316
+ st.session_state["raw_texts"] = raw_texts
317
+ st.session_state["processed_texts"] = processed_texts
318
+ texts = processed_texts
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
319
 
320
+ # Главные вкладки ЛР1/ЛР2
321
+ main_tabs = st.tabs(["Токенизация", "Векторизация", "Эмбеддинги"])
322
+
323
+ # ======== Токенизация (ЛР1) ========
324
+ with main_tabs[0]:
325
+ st.subheader("🎯 Методы токенизации")
326
+ comparator = TokenizationComparator()
327
+ available_methods = list(comparator.methods.keys())
328
+ selected_methods = st.multiselect(
329
+ "Выберите методы для сравнения:",
330
+ available_methods,
331
+ default=available_methods[:3] if len(available_methods) >= 3 else available_methods
332
+ )
333
+ if not selected_methods:
334
+ st.warning("⚠️ Пожалуйста, выберите хотя бы один метод токенизации.")
335
+ st.stop()
336
+
337
+ if st.button("🚀 Запустить а��ализ", type="primary"):
338
+ with st.spinner("Выполняется анализ..."):
339
+ results_df = comparator.compare_methods(texts, selected_methods)
340
+ st.session_state['results_df'] = results_df
341
+ st.session_state['texts'] = texts
342
+ st.session_state['selected_methods'] = selected_methods
343
+
344
+ if 'results_df' in st.session_state:
345
+ results_df = st.session_state['results_df']
346
+ texts = st.session_state['texts']
347
+ selected_methods = st.session_state['selected_methods']
348
+
349
+ st.subheader("📊 Общая статистика")
350
+ col1, col2, col3, col4 = st.columns(4)
351
+ with col1:
352
+ st.metric("Количество текстов", len(texts))
353
+ with col2:
354
+ total_words = sum(len(text.split()) for text in texts)
355
+ st.metric("Общее количество слов", total_words)
356
+ with col3:
357
+ avg_words_per_text = total_words / len(texts) if texts else 0
358
+ st.metric("Среднее слов на текст", round(avg_words_per_text, 1))
359
+ with col4:
360
+ st.metric("Проанализировано методов", len(selected_methods))
361
+
362
+ st.subheader("📋 Результаты сравнения")
363
+ st.dataframe(results_df, use_container_width=True)
364
+
365
+ st.subheader("📈 Визуализация результатов")
366
+ comparison_chart = create_comparison_chart(results_df)
367
+ st.plotly_chart(comparison_chart, use_container_width=True)
368
+
369
+ st.subheader("🔍 Детальный анализ методов")
370
+ method_tabs = st.tabs(selected_methods)
371
+ for i, method in enumerate(selected_methods):
372
+ with method_tabs[i]:
373
+ if texts:
374
+ all_tokens = []
375
+ total_processing_time = 0
376
+ for text in texts:
377
+ tokens, processing_time = comparator.tokenize_text(text, method)
378
+ all_tokens.extend(tokens)
379
+ total_processing_time += processing_time
380
+ sample_text = texts[0]
381
+ sample_tokens, _ = comparator.tokenize_text(sample_text, method)
382
+ col1, col2 = st.columns(2)
383
+ with col1:
384
+ st.write("**Исходный текст:**")
385
+ st.text(sample_text[:200] + "..." if len(sample_text) > 200 else sample_text)
386
+ with col2:
387
+ st.write("**Токены (пример из первого текста):**")
388
+ st.write(sample_tokens[:20])
389
+ if len(sample_tokens) > 20:
390
+ st.write(f"... и еще {len(sample_tokens) - 20} токенов")
391
+ col1, col2 = st.columns(2)
392
+ with col1:
393
+ dist_plot = create_token_distribution_plot(all_tokens, method)
394
+ st.plotly_chart(dist_plot, use_container_width=True)
395
+ with col2:
396
+ freq_plot = create_frequency_plot(all_tokens, method)
397
+ st.plotly_chart(freq_plot, use_container_width=True)
398
+ from collections import Counter
399
+ token_counts = Counter(all_tokens)
400
+ unique_tokens = len(token_counts)
401
+ total_tokens = len(all_tokens)
402
+ vocabulary_diversity = unique_tokens / total_tokens if total_tokens > 0 else 0
403
+ st.write("**Статистика:**")
404
+ col1, col2, col3, col4 = st.columns(4)
405
+ with col1:
406
+ st.metric("Всего токенов", total_tokens)
407
+ with col2:
408
+ st.metric("Уникальных токенов", unique_tokens)
409
+ with col3:
410
+ st.metric("Разнообразие словаря", f"{vocabulary_diversity:.2%}")
411
+ with col4:
412
+ st.metric("Время обработки", f"{total_processing_time:.4f}с")
413
+
414
+ st.subheader("💾 Экспорт результатов")
415
+ col1, col2 = st.columns(2)
416
+ with col1:
417
+ csv_data = results_df.to_csv(index=False, encoding='utf-8')
418
+ st.download_button(
419
+ label="📥 Скачать CSV",
420
+ data=csv_data,
421
+ file_name="tokenization_results.csv",
422
+ mime="text/csv"
423
+ )
424
+ with col2:
425
+ json_data = results_df.to_json(orient='records', force_ascii=False, indent=2)
426
+ st.download_button(
427
+ label="📥 Скачать JSON",
428
+ data=json_data,
429
+ file_name="tokenization_results.json",
430
+ mime="application/json"
431
+ )
432
+
433
+ # ======== Векторизация (ЛР2: классика + LSA) ========
434
+ with main_tabs[1]:
435
+ st.subheader("🧮 Классические методы векторизации")
436
+ with st.expander("Параметры векторизации", expanded=True):
437
+ methods = st.multiselect("Методы", ["onehot", "bow", "tfidf"], default=["bow", "tfidf"])
438
+ n_min = st.number_input("n-gram min", 1, 5, 1)
439
+ n_max = st.number_input("n-gram max", 1, 5, 2)
440
+ max_features = st.number_input("Max features (0 = все)", 0, 200000, 0)
441
+ sublinear_tf = st.checkbox("TF-IDF sublinear_tf", value=True)
442
+ smooth_idf = st.checkbox("TF-IDF smooth_idf", value=True)
443
+
444
+ if st.button("🏁 Построить признаки", key="build_vectors"):
445
+ cfgs = []
446
+ for m in methods:
447
+ cfgs.append(VectorizationConfig(
448
+ method=m,
449
+ ngram_range=(int(n_min), int(n_max)),
450
+ max_features=None if max_features == 0 else int(max_features),
451
+ sublinear_tf=sublinear_tf,
452
+ smooth_idf=smooth_idf,
453
+ ))
454
+ with st.spinner("Строим матрицы признаков..."):
455
+ vec_df, matrices = compare_vectorizers(texts, cfgs)
456
+ st.session_state["vec_df"] = vec_df
457
+ st.session_state["vec_matrices"] = matrices
458
+ try:
459
+ os.makedirs("results", exist_ok=True)
460
+ vec_path = "results/vectorization_metrics.csv"
461
+ vec_df.to_csv(vec_path, index=False, encoding="utf-8")
462
+ st.success(f"Метрики сохранены в {vec_path}")
463
+ except Exception as e:
464
+ st.warning(f"Не удалось сохранить метрики: {e}")
465
+
466
+ if "vec_df" in st.session_state:
467
+ st.dataframe(st.session_state["vec_df"], use_container_width=True)
468
+ # Экспорт метрик
469
+ vec_csv = st.session_state["vec_df"].to_csv(index=False, encoding="utf-8")
470
+ st.download_button("📥 Скачать векторные метрики CSV", vec_csv, "vectorization_metrics.csv", "text/csv")
471
+
472
+ # LSA / снижение размерности
473
+ st.subheader("📉 LSA (TruncatedSVD) и проекции")
474
+ selected_key = st.selectbox("Выберите матрицу", list(st.session_state["vec_matrices"].keys()))
475
+ n_components = st.slider("Число компонент (SVD)", 2, 200, 100)
476
+ proj_method = st.radio("Метод проекции", ["umap", "tsne"], horizontal=True)
477
+ if st.button("🔎 Запустить LSA/проекции"):
478
+ X = st.session_state["vec_matrices"][selected_key]["X"]
479
+ vectorizer = st.session_state["vec_matrices"][selected_key]["vectorizer"]
480
+ feature_names = vectorizer.get_feature_names()
481
+ with st.spinner("Снижаем размерность..."):
482
+ lsa = run_lsa(X, feature_names, SVDConfig(n_components=n_components))
483
+ ev_table = explained_variance_table(lsa["explained_variance_ratio"])
484
+ st.write("Объясненная дисперсия (первые 20):")
485
+ st.dataframe(ev_table.head(20), use_container_width=True)
486
+ st.write("Топ-термины по компонентам:")
487
+ st.dataframe(top_terms_dataframe(lsa["top_terms_per_component"], top_k=10).head(50), use_container_width=True)
488
+ # Проекция документов
489
+ coords = embed_2d(lsa["X_reduced"], method=proj_method)
490
+ proj_df = pd.DataFrame({"x": coords[:,0], "y": coords[:,1]})
491
+ st.plotly_chart(px.scatter(proj_df, x="x", y="y", title=f"Проекция документов ({proj_method.upper()})"), use_container_width=True)
492
+
493
+ # ======== Эмбеддинги (ЛР2: Word2Vec/FastText/Doc2Vec + эксперименты) ========
494
+ with main_tabs[2]:
495
+ st.subheader("🧠 Обучение эмбеддингов и семантические эксперименты")
496
+ # Выбор корпуса для обучения и параметры
497
+ with st.expander("Параметры обучения", expanded=True):
498
+ corpus_choice = st.radio(
499
+ "Источник обучающих текстов",
500
+ ["Предобработанные", "Без предобработки"],
501
+ index=0, horizontal=True,
502
+ help="Предобработанные = применены ��астройки из блока Предобработка на левой панели"
503
  )
504
+ model_type = st.selectbox("Модель", ["w2v", "fasttext", "doc2vec"], index=0)
505
+ vector_size = st.slider("Размерность", 50, 600, 300, step=50)
506
+ window = st.slider("Окно контекста", 2, 15, 8)
507
+ min_count = st.slider("Min count", 1, 20, 2)
508
+ epochs = st.slider("Эпохи", 1, 50, 10)
509
+ sg = st.radio("Архитектура (w2v/fasttext)", ["cbow", "skipgram"], index=1, horizontal=True)
510
+ dm = st.radio("Doc2Vec архитектура", ["pv-dm", "pv-dbow"], index=0, horizontal=True)
511
+
512
+ # Инфо о корпусе, предпросмотр и экспорт
513
+ meta = st.session_state.get("data_meta", {})
514
+ corpus = st.session_state.get("processed_texts", []) if corpus_choice == "Предобработанные" else st.session_state.get("raw_texts", [])
515
+ st.info(f"Источник данных: {meta.get('source','неизвестно')} | Текстов: {len(corpus)}")
516
+ if corpus:
517
+ with st.expander("Просмотр обучающего корпуса (первые 3 текста)", expanded=False):
518
+ st.write(corpus[:3])
519
+ # Скачать текущий обучающий корпус
520
+ corpus_txt = ("\n".join(corpus)).encode("utf-8")
521
+ st.download_button("📥 Скачать обучающий корпус (.txt)", data=corpus_txt, file_name="training_corpus.txt", mime="text/plain")
522
+
523
+ if st.button("🎓 Обучить модель", key="train_embeddings"):
524
+ cfg = EmbTrainConfig(
525
+ model_type=model_type,
526
+ vector_size=int(vector_size),
527
+ window=int(window),
528
+ min_count=int(min_count),
529
+ epochs=int(epochs),
530
+ sg=1 if sg == "skipgram" else 0,
531
+ dm=1 if dm == "pv-dm" else 0,
532
  )
533
+ with st.spinner("Обучаем модель..."):
534
+ model, tt = train_embeddings_model(corpus, cfg)
535
+ st.session_state["emb_model"] = model
536
+ st.session_state["emb_train_time"] = tt
537
+ st.success(f"Модель обучена за {tt:.2f} с")
538
+
539
+ if "emb_model" in st.session_state:
540
+ model = st.session_state["emb_model"]
541
+ col1, col2 = st.columns(2)
542
+ with col1:
543
+ save_name = st.text_input("Имя файла модели", "models/russian_news_embeddings.model")
544
+ if st.button("💾 Сохранить модель"):
545
+ save_embedding_model(model, save_name)
546
+ st.success(f"Сохранено: {save_name}")
547
+ with col2:
548
+ test_word = st.text_input("Проверить ближайших соседей для слова", "россия")
549
+ if st.button("🔍 Найти соседей"):
550
+ res = nearest_neighbors(model, [test_word], topn=10)
551
+ st.write(res.get(test_word, []))
552
+
553
+ st.markdown("---")
554
+ st.subheader("🧪 Семантические операции")
555
+ col1, col2 = st.columns(2)
556
+ with col1:
557
+ expr = st.text_input("Векторная арифметика", "король - мужчина + женщина")
558
+ if st.button("➡️ Посчитать", key="arith"):
559
+ st.write(vector_arithmetic(model, expr, topn=10))
560
+ with col2:
561
+ a = st.text_input("Ось: A", "мужчина")
562
+ b = st.text_input("Ось: B", "женщина")
563
+ words = st.text_area("Слова для проекции (через запятую)", "король, королева, доктор, медсестра")
564
+ if st.button("📏 Проекц��я на ось"):
565
+ wlist = [w.strip() for w in words.split(",") if w.strip()]
566
+ st.dataframe(semantic_axis(model, a, b, wlist), use_container_width=True)
567
+
568
+ st.markdown("---")
569
+ st.subheader("📐 Косинусное сходство и аналогии")
570
+ col1, col2 = st.columns(2)
571
+ with col1:
572
+ pair_a = st.text_input("Пара A", "москва")
573
+ pair_b = st.text_input("Пара B", "россия")
574
+ if st.button("🔗 Косинус", key="cos"):
575
+ st.write(eval_cosine(model, [(pair_a, pair_b)]))
576
+ with col2:
577
+ ana_a = st.text_input("Аналогия: A", "мужчина")
578
+ ana_b = st.text_input("Аналогия: B", "женщина")
579
+ ana_c = st.text_input("Аналогия: C", "король")
580
+ if st.button("🧩 Аналогия"):
581
+ st.write(eval_analogy(model, ana_a, ana_b, ana_c, topn=10))
582
 
583
 
584
  if __name__ == "__main__":
src/tokenizers_cmp.py CHANGED
@@ -128,7 +128,16 @@ class TokenizationComparator:
128
 
129
  def _tokenize_nltk(self, text: str) -> List[str]:
130
  """Токенизация с помощью NLTK."""
131
- return word_tokenize(text, language='russian')
 
 
 
 
 
 
 
 
 
132
 
133
  def _tokenize_spacy(self, text: str) -> List[str]:
134
  """Токенизация с помощью SpaCy."""
 
128
 
129
  def _tokenize_nltk(self, text: str) -> List[str]:
130
  """Токенизация с помощью NLTK."""
131
+ try:
132
+ return word_tokenize(text, language='russian')
133
+ except LookupError:
134
+ # Автоматическая загрузка необходимых данных NLTK (punkt)
135
+ import nltk # local import to avoid hard dependency if NLTK not used
136
+ try:
137
+ nltk.download('punkt', quiet=True)
138
+ except Exception:
139
+ pass
140
+ return word_tokenize(text, language='russian')
141
 
142
  def _tokenize_spacy(self, text: str) -> List[str]:
143
  """Токенизация с помощью SpaCy."""
src/universal_preprocessor.py CHANGED
@@ -108,9 +108,11 @@ PUNCTUATION_MAP = {
108
  '«': '"',
109
  '»': '"',
110
  '„': '"',
 
 
111
  '"': '"',
112
- ''': "'",
113
- ''': "'",
114
  '`': "'",
115
  '´': "'",
116
  }
 
108
  '«': '"',
109
  '»': '"',
110
  '„': '"',
111
+ '“': '"',
112
+ '”': '"',
113
  '"': '"',
114
+ '': "'",
115
+ '': "'",
116
  '`': "'",
117
  '´': "'",
118
  }
src/utils.py CHANGED
@@ -1,452 +1,80 @@
1
  # src/utils.py
2
  """
3
- Вспомогательные функции для проекта анализа токенизации.
4
- Содержит утилиты для работы с файлами, метриками и визуализацией.
5
  """
6
 
7
- import os
 
8
  import json
9
- import time
10
- from typing import List, Dict, Any, Optional, Tuple
11
- from pathlib import Path
12
- import pandas as pd
13
- import numpy as np
14
  from collections import Counter
15
- import matplotlib.pyplot as plt
16
- import seaborn as sns
17
-
18
-
19
- def ensure_directory(path: str) -> Path:
20
- """
21
- Создает директорию, если она не существует.
22
-
23
- Args:
24
- path: Путь к директории
25
-
26
- Returns:
27
- Path объект директории
28
- """
29
- dir_path = Path(path)
30
- dir_path.mkdir(parents=True, exist_ok=True)
31
- return dir_path
32
-
33
-
34
- def save_json(data: Any, file_path: str, ensure_ascii: bool = False) -> None:
35
- """
36
- Сохраняет данные в JSON файл.
37
-
38
- Args:
39
- data: Данные для сохранения
40
- file_path: Путь к файлу
41
- ensure_ascii: Использовать ASCII кодировку
42
- """
43
- ensure_directory(os.path.dirname(file_path))
44
-
45
- with open(file_path, 'w', encoding='utf-8') as f:
46
- json.dump(data, f, ensure_ascii=ensure_ascii, indent=2)
47
-
48
-
49
- def load_json(file_path: str) -> Any:
50
- """
51
- Загружает данные из JSON файла.
52
-
53
- Args:
54
- file_path: Путь к файлу
55
-
56
- Returns:
57
- Загруженные данные
58
- """
59
- with open(file_path, 'r', encoding='utf-8') as f:
60
- return json.load(f)
61
-
62
 
63
- def save_jsonl(data: List[Dict], file_path: str) -> None:
64
- """
65
- Сохраняет список словарей в JSONL файл.
66
-
67
- Args:
68
- data: Список словарей
69
- file_path: Путь к файлу
70
- """
71
- ensure_directory(os.path.dirname(file_path))
72
-
73
- with open(file_path, 'w', encoding='utf-8') as f:
74
- for item in data:
75
- f.write(json.dumps(item, ensure_ascii=False) + '\n')
76
 
77
 
78
- def load_jsonl(file_path: str, max_items: Optional[int] = None) -> List[Dict]:
79
- """
80
- Загружает данные из JSONL файла.
81
-
82
- Args:
83
- file_path: Путь к файлу
84
- max_items: Максимальное количество элементов для загрузки
85
-
86
- Returns:
87
- Список словарей
88
- """
89
- data = []
90
- with open(file_path, 'r', encoding='utf-8') as f:
91
  for i, line in enumerate(f):
92
- if max_items and i >= max_items:
93
  break
94
-
95
  line = line.strip()
96
- if line:
97
- try:
98
- data.append(json.loads(line))
99
- except json.JSONDecodeError:
100
- continue
101
-
102
- return data
103
-
104
-
105
- def calculate_text_statistics(texts: List[str]) -> Dict[str, Any]:
106
- """
107
- Вычисляет статистику для списка текстов.
108
-
109
- Args:
110
- texts: Список текстов
111
-
112
- Returns:
113
- Словарь со статистикой
114
- """
115
- if not texts:
116
- return {}
117
-
118
- # Общая статистика
119
- total_texts = len(texts)
120
- total_chars = sum(len(text) for text in texts)
121
- total_words = sum(len(text.split()) for text in texts)
122
-
123
- # Статистика по длинам
124
- text_lengths = [len(text) for text in texts]
125
- word_counts = [len(text.split()) for text in texts]
126
-
127
- # Статистика по символам
128
- char_counts = Counter()
129
- for text in texts:
130
- char_counts.update(text.lower())
131
-
132
- # Статистика по словам
133
- word_counts_counter = Counter()
134
- for text in texts:
135
- words = text.lower().split()
136
- word_counts_counter.update(words)
137
-
138
  return {
139
- 'total_texts': total_texts,
140
- 'total_characters': total_chars,
141
- 'total_words': total_words,
142
- 'avg_text_length': np.mean(text_lengths),
143
- 'median_text_length': np.median(text_lengths),
144
- 'avg_words_per_text': np.mean(word_counts),
145
- 'median_words_per_text': np.median(word_counts),
146
- 'unique_characters': len(char_counts),
147
- 'unique_words': len(word_counts_counter),
148
- 'most_common_chars': char_counts.most_common(10),
149
- 'most_common_words': word_counts_counter.most_common(10),
150
- 'text_length_stats': {
151
- 'min': min(text_lengths),
152
- 'max': max(text_lengths),
153
- 'std': np.std(text_lengths)
154
- },
155
- 'word_count_stats': {
156
- 'min': min(word_counts),
157
- 'max': max(word_counts),
158
- 'std': np.std(word_counts)
159
- }
160
  }
161
 
162
 
163
- def create_word_frequency_plot(word_counts: Counter, top_n: int = 20,
164
- title: str = "Частотность слов") -> plt.Figure:
165
- """
166
- Создает график частотности слов.
167
-
168
- Args:
169
- word_counts: Счетчик слов
170
- top_n: Количество топ слов для отображения
171
- title: Заголовок графика
172
-
173
- Returns:
174
- Объект matplotlib Figure
175
- """
176
- most_common = word_counts.most_common(top_n)
177
- words, counts = zip(*most_common)
178
-
179
- fig, ax = plt.subplots(figsize=(12, 8))
180
- bars = ax.barh(range(len(words)), counts)
181
- ax.set_yticks(range(len(words)))
182
- ax.set_yticklabels(words)
183
- ax.set_xlabel('Частота')
184
- ax.set_title(title)
185
- ax.invert_yaxis()
186
-
187
- # Добавляем значения на столбцы
188
- for i, bar in enumerate(bars):
189
- width = bar.get_width()
190
- ax.text(width + 0.1, bar.get_y() + bar.get_height()/2,
191
- f'{int(width)}', ha='left', va='center')
192
-
193
- plt.tight_layout()
194
- return fig
195
-
196
-
197
- def create_length_distribution_plot(lengths: List[int], title: str = "Распределение длин") -> plt.Figure:
198
- """
199
- Создает график распределения длин.
200
-
201
- Args:
202
- lengths: Список длин
203
- title: Заголовок графика
204
-
205
- Returns:
206
- Объект matplotlib Figure
207
- """
208
- fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(15, 6))
209
-
210
- # Гистограмма
211
- ax1.hist(lengths, bins=30, alpha=0.7, color='skyblue', edgecolor='black')
212
- ax1.set_xlabel('Длина')
213
- ax1.set_ylabel('Частота')
214
- ax1.set_title(f'{title} - Гистограмма')
215
- ax1.grid(True, alpha=0.3)
216
-
217
- # Box plot
218
- ax2.boxplot(lengths, vert=True)
219
- ax2.set_ylabel('Длина')
220
- ax2.set_title(f'{title} - Box Plot')
221
- ax2.grid(True, alpha=0.3)
222
-
223
- plt.tight_layout()
224
- return fig
225
-
226
-
227
- def create_tokenization_comparison_plot(results_df: pd.DataFrame) -> plt.Figure:
228
- """
229
- Создает сравнительный график методов токенизации.
230
-
231
- Args:
232
- results_df: DataFrame с результатами сравнения
233
-
234
- Returns:
235
- Объект matplotlib Figure
236
- """
237
- fig, axes = plt.subplots(2, 2, figsize=(15, 12))
238
-
239
- # Время обработки
240
- axes[0, 0].bar(results_df['Метод'], results_df['Время обработки (сек)'])
241
- axes[0, 0].set_title('Время обработки')
242
- axes[0, 0].set_ylabel('Секунды')
243
- axes[0, 0].tick_params(axis='x', rotation=45)
244
-
245
- # Размер словаря
246
- axes[0, 1].bar(results_df['Метод'], results_df['Размер словаря'])
247
- axes[0, 1].set_title('Размер словаря')
248
- axes[0, 1].set_ylabel('Количество токенов')
249
- axes[0, 1].tick_params(axis='x', rotation=45)
250
-
251
- # Коэффициент сжатия
252
- axes[1, 0].bar(results_df['Метод'], results_df['Коэффициент сжатия'])
253
- axes[1, 0].set_title('Коэффициент сжатия')
254
- axes[1, 0].set_ylabel('Отношение')
255
- axes[1, 0].tick_params(axis='x', rotation=45)
256
-
257
- # Средняя длина токена
258
- axes[1, 1].bar(results_df['Метод'], results_df['Средняя длина токена'])
259
- axes[1, 1].set_title('Средняя длина токена')
260
- axes[1, 1].set_ylabel('Символы')
261
- axes[1, 1].tick_params(axis='x', rotation=45)
262
-
263
- plt.tight_layout()
264
- return fig
265
-
266
-
267
- def calculate_oov_rate(tokens: List[str], vocabulary: set) -> float:
268
- """
269
- Вычисляет процент OOV (Out-of-Vocabulary) токенов.
270
-
271
- Args:
272
- tokens: Список токенов
273
- vocabulary: Словарь (множество известных токенов)
274
-
275
- Returns:
276
- Процент OOV токенов
277
- """
278
- if not tokens:
279
- return 0.0
280
-
281
- oov_count = sum(1 for token in tokens if token not in vocabulary)
282
- return oov_count / len(tokens)
283
-
284
-
285
- def calculate_fragmentation_rate(original_words: List[str], tokens: List[str]) -> float:
286
- """
287
- Вычисляет процент фрагментации слов.
288
-
289
- Args:
290
- original_words: Исходные слова
291
- tokens: Токены после обработки
292
-
293
- Returns:
294
- Процент фрагментированных слов
295
- """
296
- if not original_words:
297
- return 0.0
298
-
299
- fragmented_count = 0
300
- token_idx = 0
301
-
302
- for word in original_words:
303
- word_tokens = []
304
- word_length = len(word.split())
305
-
306
- # Собираем токены для текущего слова
307
- for _ in range(word_length):
308
- if token_idx < len(tokens):
309
- word_tokens.append(tokens[token_idx])
310
- token_idx += 1
311
-
312
- # Если слово разбито на несколько токенов
313
- if len(word_tokens) > 1:
314
- fragmented_count += 1
315
-
316
- return fragmented_count / len(original_words)
317
-
318
-
319
- def create_corpus_summary(corpus_path: str, output_path: str) -> Dict[str, Any]:
320
- """
321
- Создает сводку по корпусу и сохраняет в файл.
322
-
323
- Args:
324
- corpus_path: Путь к корпусу
325
- output_path: Путь для сохранения сводки
326
-
327
- Returns:
328
- Словарь со сводкой
329
- """
330
- # Загружаем корпус
331
- articles = load_jsonl(corpus_path)
332
- texts = [article.get('text', '') for article in articles if article.get('text')]
333
-
334
- # Вычисляем статистику
335
- stats = calculate_text_statistics(texts)
336
-
337
- # Добавляем информацию о корпусе
338
- summary = {
339
- 'corpus_info': {
340
- 'path': corpus_path,
341
- 'total_articles': len(articles),
342
- 'articles_with_text': len(texts),
343
- 'created_at': time.strftime('%Y-%m-%d %H:%M:%S')
344
- },
345
- 'statistics': stats
346
- }
347
-
348
- # Сохраняем сводку
349
- save_json(summary, output_path)
350
-
351
- return summary
352
-
353
-
354
- def format_time(seconds: float) -> str:
355
- """
356
- Форматирует время в читаемый вид.
357
-
358
- Args:
359
- seconds: Время в секундах
360
-
361
- Returns:
362
- Отформатированная строка времени
363
- """
364
- if seconds < 60:
365
- return f"{seconds:.2f} сек"
366
- elif seconds < 3600:
367
- minutes = seconds / 60
368
- return f"{minutes:.2f} мин"
369
- else:
370
- hours = seconds / 3600
371
- return f"{hours:.2f} ч"
372
-
373
-
374
- def print_progress_bar(iteration: int, total: int, prefix: str = '',
375
- suffix: str = '', length: int = 50) -> None:
376
- """
377
- Выводит прогресс-бар в консоль.
378
-
379
- Args:
380
- iteration: Текущая итерация
381
- total: Общее количество итераций
382
- prefix: Префикс для прогресс-бара
383
- suffix: Суффикс для прогресс-бара
384
- length: Длина прогресс-бара
385
- """
386
- percent = ("{0:.1f}").format(100 * (iteration / float(total)))
387
- filled_length = int(length * iteration // total)
388
- bar = '█' * filled_length + '-' * (length - filled_length)
389
- print(f'\r{prefix} |{bar}| {percent}% {suffix}', end='\r')
390
-
391
- if iteration == total:
392
- print()
393
-
394
-
395
- def validate_corpus_format(file_path: str) -> Tuple[bool, str]:
396
- """
397
- Проверяет формат корпуса.
398
-
399
- Args:
400
- file_path: Путь к файлу корпуса
401
-
402
- Returns:
403
- Кортеж (валидность, сообщение об ошибке)
404
- """
405
- try:
406
- articles = load_jsonl(file_path, max_items=10)
407
-
408
- if not articles:
409
- return False, "Файл пуст или не содержит валидных JSON объектов"
410
-
411
- # Проверяем структуру первого объекта
412
- first_article = articles[0]
413
- required_fields = ['text']
414
-
415
- for field in required_fields:
416
- if field not in first_article:
417
- return False, f"Отсутствует обязательное поле: {field}"
418
-
419
- if not isinstance(first_article['text'], str):
420
- return False, "Поле 'text' должно быть строкой"
421
-
422
- if not first_article['text'].strip():
423
- return False, "Поле 'text' не может быть пустым"
424
-
425
- return True, "Корпус валиден"
426
-
427
- except Exception as e:
428
- return False, f"Ошибка при проверке корпуса: {e}"
429
-
430
-
431
- if __name__ == "__main__":
432
- # Пример использования
433
- print("Утилиты для анализа токенизации")
434
-
435
- # Тестовые данные
436
- test_texts = [
437
- "Это тестовый текст для проверки функций.",
438
- "Второй текст содержит больше слов для анализа.",
439
- "Третий текст завершает набор тестовых данных."
440
- ]
441
-
442
- # Вычисляем статистику
443
- stats = calculate_text_statistics(test_texts)
444
- print(f"Статистика текстов: {stats['total_texts']} текстов, {stats['total_words']} слов")
445
-
446
- # Проверяем формат корпуса
447
- corpus_path = "data/raw_corpus.jsonl"
448
- if os.path.exists(corpus_path):
449
- is_valid, message = validate_corpus_format(corpus_path)
450
- print(f"Корпус валиден: {is_valid}, сообщение: {message}")
451
- else:
452
- print("Корпус не найден")
 
1
  # src/utils.py
2
  """
3
+ Вспомогательные утилиты: загрузка JSONL, вычисление статистики по текстам,
4
+ создание сводной информации о корпусе и сохранение результатов.
5
  """
6
 
7
+ from __future__ import annotations
8
+
9
  import json
 
 
 
 
 
10
  from collections import Counter
11
+ from dataclasses import dataclass, asdict
12
+ from pathlib import Path
13
+ from typing import Any, Dict, Iterable, List, Tuple, Optional
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
14
 
15
+ import numpy as np
 
 
 
 
 
 
 
 
 
 
 
 
16
 
17
 
18
+ def load_jsonl(path: str, max_items: Optional[int] = None) -> List[Dict[str, Any]]:
19
+ items: List[Dict[str, Any]] = []
20
+ with open(path, "r", encoding="utf-8") as f:
 
 
 
 
 
 
 
 
 
 
21
  for i, line in enumerate(f):
22
+ if max_items is not None and i >= max_items:
23
  break
 
24
  line = line.strip()
25
+ if not line:
26
+ continue
27
+ try:
28
+ items.append(json.loads(line))
29
+ except json.JSONDecodeError:
30
+ continue
31
+ return items
32
+
33
+
34
+ def calculate_text_statistics(texts: Iterable[str], top_k: int = 50) -> Dict[str, Any]:
35
+ texts_list = [t for t in texts if isinstance(t, str) and t.strip()]
36
+ total_texts = len(texts_list)
37
+ words: List[str] = []
38
+ for t in texts_list:
39
+ words.extend(t.split())
40
+ total_words = len(words)
41
+ unique_words = len(set(words))
42
+ avg_words_per_text = (total_words / total_texts) if total_texts else 0.0
43
+ freq = Counter(words)
44
+ most_common_words = freq.most_common(top_k)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
45
  return {
46
+ "total_texts": total_texts,
47
+ "total_words": total_words,
48
+ "unique_words": unique_words,
49
+ "avg_words_per_text": avg_words_per_text,
50
+ "most_common_words": most_common_words,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
51
  }
52
 
53
 
54
+ @dataclass
55
+ class CorpusSummary:
56
+ total_articles: int
57
+ total_words: int
58
+ avg_words_per_article: float
59
+ unique_words: int
60
+ categories: Dict[str, int]
61
+
62
+
63
+ def create_corpus_summary(articles: List[Dict[str, Any]]) -> CorpusSummary:
64
+ texts = [a.get("text", "") for a in articles if isinstance(a, dict)]
65
+ cats = [a.get("category", "") or "" for a in articles if isinstance(a, dict)]
66
+ stats = calculate_text_statistics(texts, top_k=0)
67
+ categories_counter = Counter([c for c in cats if isinstance(c, str) and c.strip()])
68
+ return CorpusSummary(
69
+ total_articles=len(texts),
70
+ total_words=stats["total_words"],
71
+ avg_words_per_article=float(stats["avg_words_per_text"]),
72
+ unique_words=stats["unique_words"],
73
+ categories=dict(categories_counter),
74
+ )
75
+
76
+
77
+ def save_corpus_summary(summary: CorpusSummary, out_path: str = "results/corpus_summary.json") -> None:
78
+ Path(Path(out_path).parent).mkdir(parents=True, exist_ok=True)
79
+ with open(out_path, "w", encoding="utf-8") as f:
80
+ json.dump(asdict(summary), f, ensure_ascii=False, indent=2)