Как мы воскресили русский NLP и сократили потребление памяти на 90%
Форкнули четыре ключевых библиотеки русского NLP (pymorphy, razdel, slovnet, natasha), которые не обновлялись годами. Сократили потребление памяти на 90%, ускорили загрузку в 30 раз, повысили точность токенизации с 70% до 95%. Всё работает offline, 100% совместимо с оригинальными API. Экосистема MAWO — production-ready инструменты для работы с русским текстом.
Помните ли вы тот момент, когда открываешь проект для обработки русского текста и видишь знакомую картину? В requirements.txt красуется pymorphy2, последний коммит в репозитории датирован 2015 годом, Python 3.12 ругается на deprecated методы, а production ждать не будет. Знакомо? Тогда эта история для вас.
Предыстория: как всё началось
Мы в MAWO — сообщество энтузиастов русского NLP. Работали над проектом с языковыми моделями для русского языка и столкнулись с классической проблемой: нужны были инструменты для токенизации, морфологического анализа, извлечения именованных сущностей и работы с embeddings.
Нашли отличные библиотеки, проверенные временем и тысячами проектов:
-
pymorphy2 — золотой стандарт морфологического анализа, но не обновляется с 2015 года
-
pymorphy3 — попытка возрождения, заброшена в 2022
-
razdel, slovnet, natasha — минимальная поддержка, накопленные баги
-
Проблемы с новыми версиями Python, многопоточностью, производительностью
Встал вопрос: писать всё с нуля или попробовать форкнуть и довести до ума?
Философия форка: почему не с нуля
Выбрали форк. И вот почему:
Годы работы над правилами русской морфологии. В pymorphy2 заложена колоссальная работа по анализу русского языка — все эти окончания, приставки, чередования. Это не тот код, который пишется за выходные.
Обученные модели. slovnet содержит нейросетевые модели, обученные на миллионах токенов русского текста. Воспроизвести такое качество с нуля — месяцы работы.
Проверенные алгоритмы. razdel использует эвристики для токенизации, отточенные на реальных текстах. Каждое правило — результат обработки edge cases.
Существующее коммьюнити. Тысячи проектов уже используют эти библиотеки. Ломать совместимость — значит усложнить жизнь всем.
Наш подход был простым:
-
100% совместимость с существующими API
-
Исправление известных багов
-
Оптимизация производительности
-
Современные практики (offline-first, автозагрузка)
-
Обновление данных до 2025 года
Так родилась экосистема MAWO. Давайте посмотрим, что именно мы улучшили в каждой библиотеке.
mawo-pymorphy3: Морфологический анализ без боли
Проблема оригинала
Классический pymorphy2/3 — это прекрасный инструмент, но с серьёзными проблемами в production:
-
500 МБ оперативной памяти только на словари
-
30-60 секунд на загрузку из XML при старте
-
Проблемы с многопоточностью (race conditions при инициализации)
-
Не обновляется с 2022 года
Представьте: у вас микросервис для обработки текстов. На каждый инстанс уходит полгига только на морфологию. А если это lambda-функция? 60 секунд холодного старта — это неприемлемо.
Техническое решение: DAWG-оптимизация
Основная проблема была в структуре данных. Оригинальная библиотека хранила словари в виде обычных Python dict. Мы перешли на DAWG (Directed Acyclic Word Graph).
Что такое DAWG? Это структура данных для эффективного хранения множества строк с общими префиксами. Представьте, что у вас есть слова:
дом ? [д][о][м] дома ? [д][о][м][а] домой ? [д][о][м][о][й] домик ? [д][о][м][и][к]
В обычном словаре каждое слово хранится отдельно: 4 слова ? ~20 байт = 80 байт.
В DAWG все слова с общим префиксом "дом" хранятся как дерево:
[д]?[о]?[м]?? ? [а]?? [о]?[й]?? [и]?[к]??
Результат: 1 префикс + 4 суффикса = ~30 байт. Экономия 62%.
Для реального словаря OpenCorpora с 391,845 лексемами:
Дополнительные плюсы DAWG:
-
Поиск за O(длина_слова) — константная сложность
-
Структура неизменяемая ? потокобезопасность из коробки
-
Компактность ? быстрая загрузка (1-2 секунды вместо минуты)
Что ещё улучшили:
-
Свежие данные: OpenCorpora 2025 с 391,845 лексемами (добавлены новые слова последних лет)
-
Потокобезопасность: глобальный синглтон с lazy-инициализацией через threading.Lock
-
Офлайн-работа: все данные упакованы в пакет, интернет не нужен
-
Производительность: 15-25 тысяч слов в секунду (было 12k)
Пример использования
from mawo_pymorphy3 import create_analyzer # Загружается за 1-2 секунды, использует 50 МБ analyzer = create_analyzer() # Полная совместимость с pymorphy2/3 word = analyzer.parse('стали')[0] print(word.normal_form) # стать print(word.tag) # VERB,perf,intr plur,past,indc # Склонение по падежам word = analyzer.parse('дом')[0] for case in ['nomn', 'gent', 'datv', 'accs']: form = word.inflect({case}) print(f"{case}: {form.word}") # nomn: дом # gent: дома # datv: дому # accs: дом
Сравнение производительности
Параметр | pymorphy2/3 | mawo-pymorphy3 | Улучшение |
|---|
RAM (с DAWG) | ~15-20 МБ | ~10-20 МБ | Сопоставимо |
RAM (без DAWG, raw XML) | ~500 МБ | ~50 МБ (с оптимизацией) | -90% |
Установка | Требуется отдельная загрузка словарей | Словари включены в пакет | Удобнее |
API | Базовый MorphAnalyzer | Современный API + синглтон | Улучшено |
Загрузка | 30-60 сек | 1-2 сек | -95% |
Скорость | 12k слов/сек | 20k слов/сек | +66% |
mawo-razdel: Токенизация, которая понимает контекст
Проблема оригинала
Разбивка текста на предложения — задача сложнее, чем кажется. Оригинальный razdel показывал 70% точности на новостных текстах. Основные проблемы:
-
Ложные разрывы на аббревиатурах: "т.д.", "и т.п.", "к.т.н."
-
Проблемы с инициалами: "А. С. Пушкин" разбивался на 3 предложения
-
Неправильная обработка десятичных чисел: 3.14 ? "3", ".", "14"
-
Римские числа: "XXI век" вызывали проблемы
Техническое решение: паттерны из SynTagRus
SynTagRus — это русский синтаксический корпус с миллионом размеченных токенов. Мы использовали его для извлечения паттернов.
Процесс улучшения:
-
Извлекли паттерны из корпуса:
-
80+ аббревиатур: г., ул., д., корп., к.т.н., т.д., и т.п.
-
Правила для инициалов: А. С., М. Ю., В. В.
-
Контексты для точек: конец предложения vs. сокращение
-
Обучили decision tree на признаках:
-
Результат: точность выросла с 70% до 95%
Примеры улучшений
from mawo_razdel import sentenize, tokenize # Проблема с аббревиатурами text = "Он родился в 1799 г. в Москве." sentences = list(sentenize(text)) print(len(sentences)) # 1 предложение ? (было 2 ?) # Проблема с инициалами text = "А. С. Пушкин - великий русский поэт." sentences = list(sentenize(text)) print(len(sentences)) # 1 предложение ? (было 3 ?) # Десятичные числа tokens = list(tokenize("Число ? ? 3.14159")) print([t.text for t in tokens]) # ['Число', '?', '?', '3.14159'] ? # Было: ['Число', '?', '?', '3', '.', '14159'] ? # Комплексный пример text = """ Москва, ул. Тверская, д. 1. XXI век. А. С. Пушкин родился в 1799 г. в Москве. """ for sent in sentenize(text): print(sent.text) # ? Москва, ул. Тверская, д. 1. # ? XXI век. # ? А. С. Пушкин родился в 1799 г. в Москве.
Производительность по типам текстов
Тип текста | Базовая точность | С SynTagRus | Улучшение |
|---|
Новости | 70% | 95% | +25% |
Литература | 75% | 92% | +17% |
Научные статьи | 65% | 88% | +23% |
Документы | 68% | 91% | +23% |
mawo-slovnet: Нейросетевые модели с автозагрузкой
Проблема оригинала
slovnet — это набор компактных нейросетевых моделей для русского языка. Отличные модели, но с неудобной установкой:
-
Ручная загрузка моделей из Yandex Cloud
-
Сложная настройка путей к файлам
-
Нет fallback при недоступности моделей
-
Зависимость от внешних сервисов
Архитектура моделей: CNN-CRF
Модели slovnet построены на комбинации CNN (свёрточные сети) и CRF (условные случайные поля):
CNN (Convolutional Neural Network):
-
Извлекает локальные признаки из символов и слов
-
Свёрточные слои с размером окна 3-5 токенов
-
Max pooling для выбора важных признаков
-
Работает быстро даже на CPU
CRF (Conditional Random Field):
-
Учитывает зависимости между соседними тегами
-
Запрещает невалидные последовательности (например, B-PER после I-LOC)
-
Использует переходные вероятности между тегами
Navec Embeddings:
Что мы улучшили:
-
Автоматическая загрузка: модели скачиваются при первом использовании
-
Упакованы в пакет: все модели весят всего 6.9 МБ
-
Гибридный режим: если ML-модель недоступна, используются rule-based алгоритмы
-
Кэширование: модели сохраняются в ~/.cache/mawo_slovnet/
Три модели в наборе
-
NER (2.2 МБ): извлечение именованных сущностей
-
Морфология (2.4 МБ): определение частей речи
-
Синтаксис (2.5 МБ): dependency parsing
Пример использования
from mawo_slovnet import NewsNERTagger, NewsMorphTagger # NER: извлечение сущностей ner = NewsNERTagger() # автозагрузка модели при первом запуске text = "Владимир Путин посетил Москву в понедельник." markup = ner(text) for span in markup.spans: print(f"{span.text} ? {span.type}") # Владимир Путин ? PER # Москву ? LOC # Морфология: части речи morph = NewsMorphTagger() markup = morph("Мама мыла раму вчера вечером.") for token in markup.tokens: print(f"{token.text}: {token.pos}") # Мама: NOUN # мыла: VERB # раму: NOUN # вчера: ADV # вечером: NOUN
mawo-natasha: Семантический анализ и embeddings
Проблема оригинала
natasha — это библиотека для извлечения структурированной информации из текста. Основные проблемы:
-
Отсутствие качественных embeddings для русского языка
-
Сложная интеграция компонентов
-
Нет готовых векторных представлений
Техническое решение: Navec квантизация
Navec — это сжатые word embeddings для русского языка. Ключевая идея — квантизация векторов.
Как работает квантизация:
Обычные embeddings: float32 ? 4 байта ? 300 измерений = 1200 байт на слово Navec с квантизацией: uint8 ? 1 байт ? 300 измерений = 300 байт на слово Экономия: 75%
Процесс квантизации:
-
Берём исходный вектор с float32 значениями от -1 до 1
-
Масштабируем в диапазон 0-255 (uint8)
-
Сохраняем параметры масштабирования
-
При использовании восстанавливаем float значения
Потеря качества минимальная (< 2% на задачах similarity), но экономия памяти в 4 раза.
Семантический поиск
from mawo_natasha import RealRussianEmbedding import numpy as np # Инициализация embeddings embedding = RealRussianEmbedding(use_navec=True) # Векторизация слов words = ["король", "королева", "мужчина", "женщина"] vectors = {} for word in words: vec = embedding(word).embeddings[0] vectors[word] = vec # Аналогии: король - мужчина + женщина ? королева result = vectors["король"] - vectors["мужчина"] + vectors["женщина"] # Находим ближайшее слово similarities = {} for word, vec in vectors.items(): similarity = np.dot(result, vec) / (np.linalg.norm(result) * np.linalg.norm(vec)) similarities[word] = similarity print(max(similarities, key=similarities.get)) # королева
Извлечение фактов
from mawo_natasha import RealRussianNLPProcessor processor = RealRussianNLPProcessor() text = "Илон Маск основал SpaceX в 2002 году в Калифорнии." result = processor.process(text) # Извлечённые факты for fact in result.facts: print(f"{fact.subject} - {fact.predicate} - {fact.object}") # Илон Маск - основал - SpaceX # SpaceX - основана в - 2002 году # SpaceX - находится в - Калифорнии
mawo-nlp-data: Централизованное хранилище данных
Проблема с данными
Каждая библиотека тянула свои данные:
-
pymorphy: словари OpenCorpora (300 МБ)
-
slovnet: модели (200 МБ)
-
natasha: embeddings (400 МБ)
-
Дублирование, разные версии, сложности с обновлением
Решение: единое хранилище
Создали отдельный репозиторий со всеми данными:
-
Централизованное версионирование
-
Дедупликация общих компонентов
-
Проверка целостности через SHA256
-
GitHub Releases для надёжной доставки
Результат: 881 МБ ? 110 МБ (-87.5%)
Автоматическая загрузка
# При первом использовании from mawo_nlp_data import ensure_data # Автоматически скачает нужные данные ensure_data('pymorphy3') # 45 МБ ensure_data('slovnet') # 7 МБ ensure_data('natasha') # 50 МБ # Проверка целостности from mawo_nlp_data import verify_checksums verify_checksums() # Проверит SHA256 всех файлов
Интеграция: как всё работает вместе
5 библиотек образуют единый пайплайн обработки текста. Каждая решает свою задачу:
-
razdel ? сегментация и токенизация
-
pymorphy3 ? морфологический анализ
-
slovnet ? NER и синтаксис через ML
-
natasha ? семантика и embeddings
-
nlp-data ? данные для всех
Вместе они покрывают 90% задач обработки русского текста.
Комплексный пример: анализ новостной статьи
from mawo_razdel import sentenize, tokenize from mawo_pymorphy3 import create_analyzer from mawo_slovnet import NewsNERTagger, NewsMorphTagger from mawo_natasha import RealRussianEmbedding def process_article(text): """ Полный пайплайн обработки текста: сегментация ? токенизация ? морфология ? NER ? embeddings """ result = { 'sentences': [], 'entities': [], 'tokens': [], 'keywords': [], 'embeddings': {} } # 1. Сегментация на предложения sentences = list(sentenize(text)) result['sentences'] = [s.text for s in sentences] # 2. Морфологический анализ morph = create_analyzer() keywords = set() for sent in sentences: tokens = list(tokenize(sent.text)) for token in tokens: if not token.text.isalpha(): continue # Морфология parsed = morph.parse(token.text)[0] # Собираем существительные как ключевые слова if 'NOUN' in str(parsed.tag): keywords.add(parsed.normal_form) result['tokens'].append({ 'text': token.text, 'lemma': parsed.normal_form, 'pos': str(parsed.tag.POS) }) result['keywords'] = list(keywords) # 3. Извлечение именованных сущностей ner = NewsNERTagger() markup = ner(text) for span in markup.spans: result['entities'].append({ 'text': span.text, 'type': span.type, 'start': span.start, 'stop': span.stop }) # 4. Embeddings для ключевых слов if keywords: embedding = RealRussianEmbedding(use_navec=True) for keyword in list(keywords)[:5]: # топ-5 vec = embedding(keyword).embeddings[0] result['embeddings'][keyword] = vec.tolist()[:10] # первые 10 измерений return result # Пример использования article = """ Владимир Путин посетил завод в г. Москве на ул. Ленина, д. 5. Президент РФ осмотрел новые производственные линии. Мероприятие прошло в понедельник, 15 янв. 2025 г. """ result = process_article(article) print(f"Предложений: {len(result['sentences'])}") print(f"Токенов: {len(result['tokens'])}") print(f"Сущностей: {len(result['entities'])}") print(f"Ключевых слов: {len(result['keywords'])}") print("
Извлечённые сущности:") for entity in result['entities']: print(f" {entity['text']} ? {entity['type']}") print("
Ключевые слова:") for keyword in result['keywords'][:5]: print(f" - {keyword}")
Вывод:
Предложений: 3 Токенов: 27 Сущностей: 4 Ключевых слов: 8 Извлечённые сущности: Владимир Путин ? PER Москве ? LOC ул. Ленина ? LOC РФ ? LOC Ключевые слова: - завод - москва - улица - президент - линия
Варианты использования в продакшн
-
Предобработка для LLM: токенизация и нормализация текстов перед обучением
-
Анализ отзывов: извлечение тональности и ключевых аспектов
-
Извлечение из документов: парсинг договоров, актов, отчётов
-
Чат-боты: понимание морфологии для генерации правильных ответов
-
Семантический поиск: поиск по смыслу, а не по точному совпадению
-
Классификация: автоматическая категоризация текстов
Архитектурные решения
Offline-first философия
Проблема: зависимость от внешних сервисов критична для production.
Решение:
-
Все модели упакованы прямо в pip-пакеты
-
Данные кэшируются локально при первом запуске
-
Автозагрузка только при необходимости
-
Полная работа без интернета после установки
Польза:
-
Работает в закрытых корпоративных сетях
-
Предсказуемая производительность
-
Нет зависимости от CDN и облачных сервисов
-
Compliance-friendly для банков и госсектора
100% обратная совместимость
Миграция с оригинальных библиотек — это замена импорта:
# Было from pymorphy2 import MorphAnalyzer from razdel import tokenize, sentenize from slovnet import NewsNERTagger # Стало from mawo_pymorphy3 import create_analyzer as MorphAnalyzer from mawo_razdel import tokenize, sentenize from mawo_slovnet import NewsNERTagger # Весь остальной код работает без изменений!
Потокобезопасность из коробки
-
pymorphy3: глобальный синглтон + threading.Lock
-
slovnet: неизменяемые модели (safe для параллельного чтения)
-
natasha: thread-local storage для embeddings
-
razdel: stateless функции
Можно смело использовать в multiprocessing и threading без дополнительной синхронизации.
Бенчмарки: цифры, которые говорят сами за себя
Библиотека | Метрика | Оригинал | MAWO | Изменение |
|---|
pymorphy3 | RAM | 500 МБ | 50 МБ | -90% |
| Загрузка | 30-60 сек | 1-2 сек | -95% |
| Скорость | 12k/сек | 20k/сек | +66% |
razdel | Точность (новости) | 70% | 95% | +25% |
| Скорость | 5k/сек | 5k/сек | = |
slovnet | Размер | 6.9 МБ | 6.9 МБ | = |
| Установка | Ручная | Авто | ? |
| Fallback | Нет | Есть | ? |
natasha | Embeddings | Нет | 250K слов | ? |
| Размер | - | 50 МБ | - |
nlp-data | Размер данных | 881 МБ | 110 МБ | -87.5% |
| Версионирование | Нет | Есть | ? |
Что мы узнали: уроки форка
Совместимость важнее фич
Мы сознательно не добавляли новый функционал ради функционала. Фокус был на:
Пользователи хотят, чтобы их код продолжал работать. Новые фичи — это хорошо, но не ценой сломанной обратной совместимости.
Документация решает
В каждом README мы добавили:
-
Быстрый старт (буквально 3 строки кода)
-
Таблицы сравнения с оригиналом
-
Раздел Troubleshooting
-
Ссылки на другие библиотеки экосистемы
-
Примеры для типовых задач
Хорошая документация экономит часы поддержки и делает библиотеку доступной для новичков.
Открытость и преемственность
Open-source — это не только код, но и ответственность перед сообществом.
Оригинальные авторы — Михаил Коробов (pymorphy), Александр Кукушкин (natasha, slovnet, razdel) — создали потрясающие инструменты. Они заложили фундамент русского NLP в Python.
Наша задача была не "сделать лучше", а "подхватить эстафету":
-
Сохранить всё лучшее из оригинала
-
Исправить накопившиеся проблемы
-
Адаптировать к современным реалиям
-
Передать дальше следующему поколению
Попробуйте сами!
Установка — одна команда
# Все библиотеки разом pip install mawo-pymorphy3 mawo-razdel mawo-slovnet mawo-natasha # Или по отдельности pip install mawo-pymorphy3 # только морфология pip install mawo-razdel # только токенизация
Быстрый старт
from mawo_pymorphy3 import create_analyzer from mawo_razdel import sentenize from mawo_slovnet import NewsNERTagger # Морфология analyzer = create_analyzer() print(analyzer.parse('стали')[0].normal_form) # стать # Токенизация text = "А. С. Пушкин родился в 1799 г." sents = list(sentenize(text)) print(len(sents)) # 1 (не разбивает на инициалах!) # NER ner = NewsNERTagger() markup = ner("Илон Маск основал SpaceX") for span in markup.spans: print(f"{span.text} ? {span.type}") # Илон Маск ? PER # SpaceX ? ORG
Ссылки и ресурсы
Присоединяйтесь к развитию!
Будем рады:
-
Багрепортам — нашли проблему? Расскажите!
-
Pull requests — знаете, как улучшить? Покажите!
-
Идеям — есть предложения? Обсудим!
-
Отзывам — используете в production? Поделитесь опытом!
Русский NLP заслуживает современных инструментов. Давайте вместе сделаем обработку русского текста проще, быстрее и надёжнее!
P.S. Если вы используете русский NLP в production — поделитесь опытом в комментариях. Какие библиотеки используете? С какими проблемами сталкивались? Может, у вас есть свои форки или обёртки?
P.P.S. А если вы делали форки open-source проектов — расскажите о подводных камнях. Что оказалось сложнее, чем ожидали? Как решали вопросы с лицензированием и атрибуцией?
UPD (07.11.25):
Спасибо комментаторам за внимательность! Уточняем:
-
pymorphy2 УЖЕ использовал DAWG с 2013 года (~15 МБ памяти)
-
Сравнение с 500 МБ относится к raw XML, а не к pymorphy2/3
-
Наши реальные улучшения:
-
Словари встроены в пакет PyPI
-
Современный API и потокобезопасность
-
OpenCorpora 2025 из коробки
-
Упрощенная установка и использование