Проект Natasha

Navec — компактные эмбеддинги для русского языка

Библиотека Navec — часть проекта Natasha, коллекция предобученных эмбеддингов для русского языка. Качество решения сравнимо или выше, чем у статических моделей от RusVectores, размер в 5–6 раз меньше:

Качество Размер модели, МБ Размер словаря, ×103
Navec 0.719 50.6 500
RusVectores 0.638–0.726 220.6–290.7 189–249

В этой статье поговорим про старые добрые пословные эмбеддинги, совершившие революцию в NLP в 2013 году.

python − счастье + страдание = perl

Технология актуальна до сих пор. В проекте Natasha модели для разбора морфологии, синтаксиса и извлечения именованных сущностей работают на пословных Navec-эмбеддингах, показывают качество выше других открытых решений.

RusVectores

Для русского языка принято использовать предобученные эмбеддинги от RusVectores, у них есть неприятная особенность: в таблице записаны не слова, а пары «слово_POS-тег». Идея хорошая, для пары «печь_VERB» ожидаем вектор, похожий на «готовить_VERB», «варить_VERB», а для «печь_NOUN» — «изба_NOUN», «топка_NOUN».

На практике использовать такие эмбеддинги неудобно. Недостаточно разделить текст на токены, для каждого нужно как-то определить POS-тег. Таблица эмбеддингов разбухает. Вместо одного слова «стать», мы храним 6: 2 разумных «стать_VERB», «стать_NOUN» и 4 странных «стать_ADV», «стать_PROPN», «стать_NUM», «стать_ADJ». В таблице на 250 000 записей 195 000 уникальных слов.

Качество

Оценим качество эмбеддингов на задаче семантической близости. Возьмём пару слов, для каждого найдём вектор-эмбеддинг, посчитаем косинусное сходство. Navec для похожих слов «чашка» и «кувшин» возвращет 0.49, для «фрукт» и «печь» — −0.0047. Соберём много пар с эталонными метками похожести, посчитаем корреляцию Спирмена с нашими ответами.

Кроме семантической близости для оценки качества эмбеддингов используют задачу решения аналогий. RusVectores публикует переведённый датасет Google Analogies. Прогнать на нём Navec пока не дошли руки.

Авторы RusVectores используют небольшой аккуратно проверенный и исправленный тестовый список пар SimLex965. Добавим свежий яндексовый LRWC и датасеты из проекта RUSSE: HJ, RT, AE, AE2:

Среднее качество на 6 датасетах Время загрузки, секунды Размер модели, МБ Размер словаря, ×103
Navec hudlit_12B_500K_300d_100q 0.719 1.0 50.6 500
news_1B_250K_300d_100q 0.653 0.5 25.4 250
RusVectores ruscorpora_upos_cbow_300_20_2019 0.692 3.3 220.6 189
ruwikiruscorpora_upos_skipgram_300_2_2019 0.691 5.0 290.0 248
tayga_upos_skipgram_300_2_2019 0.726 5.2 290.7 249
tayga_none_fasttextcbow_300_10_2019 0.638 8.0 2741.9 192
araneum_none_fasttextcbow_300_5_2018 0.664 16.4 2752.1 195

Качество hudlit_12B_500K_300d_100q сравнимо или лучше, чем у решений RusVectores, словарь больше в 2–3 раза, размер модели меньше в 5–6 раз. Как удалось получить такое качество и размер?

Принцип работы

hudlit_12B_500K_300d_100qGloVe-эмбеддинги обученные на 145ГБ художественной литературы. Архив с текстами возьмём из проекта RUSSE. Используем оригинальную реализацию GloVe на C, обернём её в удобный Python-интерфейс.

Почему не word2vec? Эксперименты на большом датасете быстрее с GloVe. Один раз считаем матрицу коллокаций, по ней готовим эмбеддинги разных размерностей, выбираем оптимальный вариант.

Оригинальная статья про GloVe: Global Vectors for Word Representation

Почему не fastText? В проекте Natasha мы работаем с текстами новостей. В них мало опечаток, проблему OOV-токенов решает большой словарь. 250 000 строк в таблице news_1B_250K_300d_100q покрывают 98% слов в новостных статьях.

Статья Давида Дале про компактный предобученный fastText для русского языка: Как сжать модель fastText в 100 раз

Размер словаря hudlit_12B_500K_300d_100q — 500 000 записей, он покрывает 98% слов в художественных текстах. Оптимальная размерность векторов — 300. Таблица 500 000 × 300 из float-чисел занимает 578МБ, размер архива с весами hudlit_12B_500K_300d_100q в 12 раз меньше (48МБ). Дело в квантизации.

Квантизация

Заменим 32-битные float-числа на 8-битные коды: [−∞, −0.86) — код 0, [−0.86, -0.79) — код 1, [-0.79, -0.74) — 2, …, [0.86, ∞) — 255. Размер таблицы уменьшится в 4 раз (143МБ):

Было:
-0.220 -0.071  0.320 -0.279  0.376  0.409  0.340 -0.329  0.400
 0.046  0.870 -0.163  0.075  0.198 -0.357 -0.279  0.267  0.239
 0.111  0.057  0.746 -0.240 -0.254  0.504  0.202  0.212  0.570
 0.529  0.088  0.444 -0.005 -0.003 -0.350 -0.001  0.472  0.635
-0.170  0.677  0.212  0.202 -0.030  0.279  0.229 -0.475 -0.031

Стало:
    63    105    215     49    225    230    219     39    228
   143    255     78    152    187     34     49    204    198
   163    146    253     58     55    240    188    191    246
   243    155    234    127    127     35    128    237    249
    76    251    191    188    118    207    195     18    118

Данные огрубляются, разные значения -0.005 и -0.003 заменяет один код 127, -0.030 и -0.031 — 118

Заменим кодом не одно, а 3 числа. Кластеризуем все тройки чисел из таблицы эмбеддингов алгоритмом k-means на 256 кластеров, вместо каждой тройки будем хранить код от 0 до 255. Таблица уменьшится ещё в 3 раза (48МБ). Navec использует библиотеку PQk-means, она разбивает матрицу на 100 колонок, каждую кластеризует отдельно, качество на синтетических тестах падёт на 1 процентный пункт.

Понятно про квантизацию в статье Chris McCormick Product Quantizers for k-NN Tutorial

Квантованные эмбеддинги проигрывают обычным по скорости. Сжатый вектор перед использованием нужно распаковать. Аккуратно реализуем процедуру, применим Numpy-магию, в PyTorch используем torch.gather. В Slovnet NER доступ к таблице эмбеддингов занимает 0.1% от общего времени вычислений.

Использование

Список предобученных моделей, инструкция по установке — в репозитории Navec.

Модуль NavecEmbedding из библиотеки Slovnet интегрирует Navec в PyTorch-модели:

>>> import torch

>>> from navec import Navec
>>> from slovnet.model.emb import NavecEmbedding

>>> path = 'hudlit_12B_500K_300d_100q.tar'  # 51MB
>>> navec = Navec.load(path)  # ~1 sec, ~100MB RAM

>>> words = ['навек', '<unk>', '<pad>']
>>> ids = [navec.vocab[_] for _ in words]

>>> emb = NavecEmbedding(navec)
>>> input = torch.tensor(ids)

>>> emb(input)  # 3 x 300
tensor([[ 4.2000e-01,  3.6666e-01,  1.7728e-01,
        [ 1.6954e-01, -4.6063e-01,  5.4519e-01,
        [ 0.0000e+00,  0.0000e+00,  0.0000e+00,
	  ...