File size: 19,417 Bytes
54ccdcb
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
753b589
54ccdcb
 
753b589
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
54ccdcb
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
753b589
83b4881
 
753b589
 
83b4881
753b589
 
 
83b4881
753b589
 
 
 
 
 
 
 
54ccdcb
 
 
 
 
 
 
 
753b589
 
 
 
 
 
 
 
 
 
 
 
 
54ccdcb
 
 
 
 
753b589
 
 
 
 
 
 
 
 
 
 
 
 
54ccdcb
 
 
 
 
 
 
 
 
 
753b589
 
 
 
 
 
 
 
 
 
 
 
 
54ccdcb
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
# src/tokenizers_cmp.py
"""
Модуль для сравнения различных методов токенизации и нормализации текста.
Реализует классические и современные методы токенизации, стемминга и лемматизации.
"""

import re
import time
from typing import List, Dict, Tuple, Optional, Any
from dataclasses import dataclass
from collections import Counter
import pandas as pd
import numpy as np

# Импорты для различных методов токенизации
try:
    from razdel import tokenize as rz_tokenize
    RAZDEL_AVAILABLE = True
except ImportError:
    RAZDEL_AVAILABLE = False

try:
    import nltk
    from nltk.tokenize import word_tokenize
    from nltk.stem import PorterStemmer, SnowballStemmer
    NLTK_AVAILABLE = True
except ImportError:
    NLTK_AVAILABLE = False

try:
    import spacy
    SPACY_AVAILABLE = True
except ImportError:
    SPACY_AVAILABLE = False

try:
    import pymorphy2
    # Проверяем совместимость с текущей версией Python
    import inspect
    if hasattr(inspect, 'getargspec'):
        PYMORPHY_AVAILABLE = True
    else:
        PYMORPHY_AVAILABLE = False
        print("⚠️ pymorphy2 несовместим с Python 3.13+. Используйте Python 3.11 или ниже для полной функциональности.")
except ImportError:
    PYMORPHY_AVAILABLE = False

try:
    from transformers import AutoTokenizer
    TRANSFORMERS_AVAILABLE = True
except ImportError:
    TRANSFORMERS_AVAILABLE = False


@dataclass
class TokenizationMetrics:
    """Метрики для оценки качества токенизации."""
    method_name: str
    total_tokens: int
    unique_tokens: int
    vocabulary_size: int
    avg_token_length: float
    processing_time: float
    oov_rate: float = 0.0
    fragmentation_rate: float = 0.0
    compression_ratio: float = 1.0


class TokenizationComparator:
    """Класс для сравнения различных методов токенизации."""
    
    def __init__(self):
        """Инициализация компаратора."""
        self.methods = {}
        self.results = {}
        self._ensure_nltk_resources()
        self._initialize_methods()
    
    def _ensure_nltk_resources(self):
        """Обеспечивает наличие необходимых ресурсов NLTK."""
        if not NLTK_AVAILABLE:
            return
        
        import nltk
        try:
            # Пробуем использовать punkt_tab для русского языка
            try:
                nltk.data.find('tokenizers/punkt_tab/russian')
            except LookupError:
                try:
                    nltk.download('punkt_tab', quiet=True)
                except Exception:
                    pass
        except Exception:
            pass
        
        # Также загружаем обычный punkt как fallback
        try:
            nltk.data.find('tokenizers/punkt')
        except LookupError:
            try:
                nltk.download('punkt', quiet=True)
            except Exception:
                pass
    
    def _initialize_methods(self):
        """Инициализирует доступные методы токенизации."""
        # Наивная токенизация
        self.methods['naive'] = self._tokenize_naive
        
        # Регулярные выражения
        self.methods['regex'] = self._tokenize_regex
        
        # Razdel (специально для русского языка)
        if RAZDEL_AVAILABLE:
            self.methods['razdel'] = self._tokenize_razdel
        
        # NLTK
        if NLTK_AVAILABLE:
            self.methods['nltk'] = self._tokenize_nltk
            self.methods['porter_stemmer'] = self._tokenize_with_stemming
            self.methods['snowball_stemmer'] = self._tokenize_with_snowball
        
        # SpaCy
        if SPACY_AVAILABLE:
            try:
                self.nlp = spacy.load('ru_core_news_sm')
                self.methods['spacy'] = self._tokenize_spacy
                self.methods['spacy_lemmatize'] = self._tokenize_with_lemmatization
            except OSError:
                print("SpaCy русская модель не найдена. Установите: python -m spacy download ru_core_news_sm")
        
        # PyMorphy2
        if PYMORPHY_AVAILABLE:
            self.morph = pymorphy2.MorphAnalyzer()
            self.methods['pymorphy'] = self._tokenize_with_pymorphy
    
    def _tokenize_naive(self, text: str) -> List[str]:
        """Наивная токенизация по пробелам."""
        return text.split()
    
    def _tokenize_regex(self, text: str) -> List[str]:
        """Токенизация с помощью регулярных выражений."""
        # Улучшенная токенизация: слова + основные знаки препинания
        tokens = re.findall(r"\b\w+\b|[.,!?;:]", text, flags=re.U)
        # Фильтруем слишком короткие токены (кроме знаков препинания)
        filtered_tokens = []
        for token in tokens:
            if len(token) > 1 or token in '.,!?;:':
                filtered_tokens.append(token)
        return filtered_tokens
    
    def _tokenize_razdel(self, text: str) -> List[str]:
        """Токенизация с помощью razdel."""
        return [t.text for t in rz_tokenize(text)]
    
    def _tokenize_nltk(self, text: str) -> List[str]:
        """Токенизация с помощью NLTK."""
        import nltk
        try:
            return word_tokenize(text, language='russian')
        except LookupError as e:
            # Автоматическая загрузка необходимых данных NLTK
            try:
                # Пробуем загрузить punkt_tab для русского языка
                nltk.download('punkt_tab', quiet=True)
                return word_tokenize(text, language='russian')
            except Exception:
                try:
                    # Если не получилось, пробуем загрузить обычный punkt
                    nltk.download('punkt', quiet=True)
                    # Используем английский язык как fallback
                    return word_tokenize(text, language='english')
                except Exception:
                    # Если и это не сработало, используем простую токенизацию
                    return text.split()
    
    def _tokenize_spacy(self, text: str) -> List[str]:
        """Токенизация с помощью SpaCy."""
        doc = self.nlp(text)
        return [token.text for token in doc if not token.is_space]
    
    def _tokenize_with_stemming(self, text: str) -> List[str]:
        """Токенизация с применением стемминга Porter."""
        import nltk
        try:
            tokens = word_tokenize(text, language='russian')
        except LookupError:
            try:
                nltk.download('punkt_tab', quiet=True)
                tokens = word_tokenize(text, language='russian')
            except Exception:
                try:
                    nltk.download('punkt', quiet=True)
                    tokens = word_tokenize(text, language='english')
                except Exception:
                    tokens = text.split()
        stemmer = PorterStemmer()
        return [stemmer.stem(token) for token in tokens if token.isalpha()]
    
    def _tokenize_with_snowball(self, text: str) -> List[str]:
        """Токенизация с применением стемминга Snowball."""
        import nltk
        try:
            tokens = word_tokenize(text, language='russian')
        except LookupError:
            try:
                nltk.download('punkt_tab', quiet=True)
                tokens = word_tokenize(text, language='russian')
            except Exception:
                try:
                    nltk.download('punkt', quiet=True)
                    tokens = word_tokenize(text, language='english')
                except Exception:
                    tokens = text.split()
        stemmer = SnowballStemmer('russian')
        return [stemmer.stem(token) for token in tokens if token.isalpha()]
    
    def _tokenize_with_lemmatization(self, text: str) -> List[str]:
        """Токенизация с применением лемматизации SpaCy."""
        doc = self.nlp(text)
        return [token.lemma_ for token in doc if not token.is_space and token.is_alpha]
    
    def _tokenize_with_pymorphy(self, text: str) -> List[str]:
        """Токенизация с применением лемматизации PyMorphy2."""
        import nltk
        try:
            tokens = word_tokenize(text, language='russian')
        except LookupError:
            try:
                nltk.download('punkt_tab', quiet=True)
                tokens = word_tokenize(text, language='russian')
            except Exception:
                try:
                    nltk.download('punkt', quiet=True)
                    tokens = word_tokenize(text, language='english')
                except Exception:
                    tokens = text.split()
        lemmas = []
        for token in tokens:
            if token.isalpha():
                parsed = self.morph.parse(token)[0]
                lemmas.append(parsed.normal_form)
        return lemmas
    
    def tokenize_text(self, text: str, method: str) -> Tuple[List[str], float]:
        """
        Токенизирует текст указанным методом.
        
        Args:
            text: Исходный текст
            method: Название метода токенизации
        
        Returns:
            Кортеж (список токенов, время обработки)
        """
        if method not in self.methods:
            raise ValueError(f"Метод '{method}' не поддерживается")
        
        start_time = time.time()
        tokens = self.methods[method](text)
        processing_time = time.time() - start_time
        
        return tokens, processing_time
    
    def calculate_metrics(self, tokens: List[str], original_text: str, method: str, processing_time: float) -> TokenizationMetrics:
        """
        Вычисляет метрики для токенизации.
        
        Args:
            tokens: Список токенов
            original_text: Исходный текст
            method: Название метода
            processing_time: Время обработки
        
        Returns:
            Объект с метриками
        """
        total_tokens = len(tokens)
        unique_tokens = len(set(tokens))
        vocabulary_size = unique_tokens
        
        # Средняя длина токена
        if total_tokens > 0:
            avg_token_length = sum(len(token) for token in tokens) / total_tokens
        else:
            avg_token_length = 0
        
        # Коэффициент сжатия (отношение исходных слов к токенам)
        original_words = len(original_text.split())
        compression_ratio = original_words / total_tokens if total_tokens > 0 else 1.0
        
        # Процент фрагментации (слова, разбитые на несколько токенов)
        fragmentation_rate = 0.0  # Будет вычислено отдельно для подсловых методов
        
        return TokenizationMetrics(
            method_name=method,
            total_tokens=total_tokens,
            unique_tokens=unique_tokens,
            vocabulary_size=vocabulary_size,
            avg_token_length=avg_token_length,
            processing_time=processing_time,
            compression_ratio=compression_ratio,
            fragmentation_rate=fragmentation_rate
        )
    
    def compare_methods(self, texts: List[str], methods: Optional[List[str]] = None) -> pd.DataFrame:
        """
        Сравнивает различные методы токенизации на наборе текстов.
        
        Args:
            texts: Список текстов для анализа
            methods: Список методов для сравнения (если None, используются все доступные)
        
        Returns:
            DataFrame с результатами сравнения
        """
        if methods is None:
            methods = list(self.methods.keys())
        
        results = []
        
        for method in methods:
            print(f"Тестируем метод: {method}")
            
            total_tokens = 0
            total_unique_tokens = set()
            total_processing_time = 0
            total_original_words = 0
            
            for text in texts:
                try:
                    tokens, processing_time = self.tokenize_text(text, method)
                    total_tokens += len(tokens)
                    total_unique_tokens.update(tokens)
                    total_processing_time += processing_time
                    total_original_words += len(text.split())
                except Exception as e:
                    print(f"Ошибка при обработке текста методом {method}: {e}")
                    continue
            
            # Вычисляем агрегированные метрики
            vocabulary_size = len(total_unique_tokens)
            avg_token_length = sum(len(token) for token in total_unique_tokens) / vocabulary_size if vocabulary_size > 0 else 0
            compression_ratio = total_original_words / total_tokens if total_tokens > 0 else 1.0
            
            metrics = TokenizationMetrics(
                method_name=method,
                total_tokens=total_tokens,
                unique_tokens=vocabulary_size,
                vocabulary_size=vocabulary_size,
                avg_token_length=avg_token_length,
                processing_time=total_processing_time,
                compression_ratio=compression_ratio
            )
            
            results.append(metrics)
        
        # Преобразуем в DataFrame
        df = pd.DataFrame([{
            'Метод': r.method_name,
            'Всего токенов': r.total_tokens,
            'Уникальных токенов': r.unique_tokens,
            'Размер словаря': r.vocabulary_size,
            'Средняя длина токена': round(r.avg_token_length, 2),
            'Время обработки (сек)': round(r.processing_time, 3),
            'Коэффициент сжатия': round(r.compression_ratio, 3)
        } for r in results])
        
        return df.sort_values('Время обработки (сек)')
    
    def analyze_token_distribution(self, text: str, method: str) -> Dict[str, Any]:
        """
        Анализирует распределение токенов для указанного метода.
        
        Args:
            text: Исходный текст
            method: Метод токенизации
        
        Returns:
            Словарь с анализом распределения
        """
        tokens, _ = self.tokenize_text(text, method)
        
        # Подсчет частот
        token_counts = Counter(tokens)
        
        # Статистика по длинам токенов
        token_lengths = [len(token) for token in tokens]
        
        return {
            'method': method,
            'total_tokens': len(tokens),
            'unique_tokens': len(token_counts),
            'most_common_tokens': token_counts.most_common(10),
            'token_length_stats': {
                'min': min(token_lengths) if token_lengths else 0,
                'max': max(token_lengths) if token_lengths else 0,
                'mean': np.mean(token_lengths) if token_lengths else 0,
                'median': np.median(token_lengths) if token_lengths else 0
            },
            'vocabulary_diversity': len(token_counts) / len(tokens) if tokens else 0
        }
    
    def save_results(self, results_df: pd.DataFrame, output_path: str):
        """Сохраняет результаты в CSV файл."""
        results_df.to_csv(output_path, index=False, encoding='utf-8')
        print(f"Результаты сохранены в {output_path}")


def load_corpus_from_jsonl(file_path: str, text_field: str = 'text', max_articles: Optional[int] = None) -> List[str]:
    """
    Загружает корпус из JSONL файла.
    
    Args:
        file_path: Путь к JSONL файлу
        text_field: Поле с текстом статьи
        max_articles: Максимальное количество статей для загрузки
    
    Returns:
        Список текстов
    """
    import json
    
    texts = []
    with open(file_path, 'r', encoding='utf-8') as f:
        for i, line in enumerate(f):
            if max_articles and i >= max_articles:
                break
            
            try:
                article = json.loads(line.strip())
                if text_field in article and article[text_field].strip():
                    texts.append(article[text_field])
            except json.JSONDecodeError:
                continue
    
    return texts


if __name__ == "__main__":
    # Пример использования
    comparator = TokenizationComparator()
    
    # Тестовые тексты
    test_texts = [
        "Это тестовый текст для проверки различных методов токенизации.",
        "В России работает множество новостных агентств: РИА Новости, ТАСС, Интерфакс.",
        "Компания ООО 'Тест' сообщила о результатах за 2023 год. Контакты: info@test.ru"
    ]
    
    print("Доступные методы токенизации:")
    for method in comparator.methods.keys():
        print(f"- {method}")
    
    # Сравниваем методы
    results = comparator.compare_methods(test_texts)
    print("\nРезультаты сравнения:")
    print(results)
    
    # Анализируем распределение токенов для одного метода
    if 'razdel' in comparator.methods:
        analysis = comparator.analyze_token_distribution(test_texts[0], 'razdel')
        print(f"\nАнализ распределения токенов (razdel):")
        print(f"Всего токенов: {analysis['total_tokens']}")
        print(f"Уникальных токенов: {analysis['unique_tokens']}")
        print(f"Наиболее частые токены: {analysis['most_common_tokens'][:5]}")