Финальный босс лингвистики: как я собрал полный NLP-стек для кабардинского из старых словарей

от автора

Привет, Хабр! Меня зовут Эдуард. В прошлой статье я рассказывал про локальный переводчик и синтез голоса на кабардинском. С тех пор проект вырос: я делаю приложение на iOS для чтения с синтезом и распознаванием речи на кабардинском и ещё паре десятков «обделённых» языков. Чтобы приложение было умным (ранжировало важные предложения, искало по смыслу, вело чат по книге, читало с выразительной интонацией), ему нужен полный набор языковых сигналов: лемма, часть речи, именованные сущности, тональность и эмбеддинги.

Для английского, немецкого и испанского всё это Apple даёт из коробки. Для кабардинского — ноль по всем пяти пунктам.

Прежде чем лезть в дебри, я решил спросить у Gemini Pro, насколько это вообще реально. Ответ меня обескуражил.

Приговор

Дословно (сокращаю, но дух передаю точно):

Кабардинский язык — это «финальный босс» мировой лингвистики. Написать для него «наивный» стеммер или POS-таггер на правилах абсолютно невозможно.

И три аргумента, каждый из которых чистая правда.

1. Полисинтетизм на стероидах. Кабардинский глагол похож на поезд, где вагоны цепляются и спереди, и сзади, и в середине. Одно слово умыгъэпӏащӏэ означает «не заставляй его торопиться»: внутри и «ты», и отрицание «не», и каузатив «заставлять», и корень «торопиться».

2. Однобуквенные корни (консонантный кошмар). В кабардинском около 48 согласных и почти нет гласных в корнях. Многие корни — это одна согласная: п варить, т давать, щ делать или продавать. Gemini рисует апокалипсис:

Если вы напишете наивный стеммер, который «разденет» слово до корня, он оставит вам токен п. Ваш TF-IDF решит, что половина базы посвящена кулинарии.

3. Эргативный строй. Окончание существительного меняется в зависимости от переходности глагола.

Вывод Gemini: никакой наивности, только сложнейшие конечные автоматы (FST) в профессиональных лабораториях. «Кабардинский оставляем нейросетям».

Красиво. Убедительно. И, как выяснилось, обжалуемо по каждому пункту. Вызов принят.

Козырь в рукаве: словари

К этому моменту у меня уже был отработанный на латышском, украинском и белорусском подход: словарь как основа плюс наивный стеммер плюс мосты через глоссы. Идея простая: если у языка есть словарь, половину NLP можно собрать без единого GPU, на хеш-таблицах и правилах.

А для кабардинского у меня, носителя, лежали сокровища, которых нет в открытых датасетах:

  • kabrudict — кабардино-русский словарь, 32 700 статей;

  • kaborfodict — орфографический словарь, 138 000 словоформ;

  • kabmorfodict — морфемный словарь (приставки, суффиксы, корни).

В машинном Wiktionary (Kaikki) кабардинского жалкие 1.8 МБ. А тут целая языковая база. Осталось её оживить.

Пункт обвинения №1: «наивный стеммер сойдёт с ума»

Gemini прав: если раздевать слово до корня, получишь п и кулинарный апокалипсис. Поэтому мы не раздеваем до корня.

Ключевая идея: лемма это не голый корень, а словарный инфинитив на , и превербы (те-, дэ-, къэ-, щӏэ- и прочие) в нём уже сидят. тесынкъищтэн — это цельные словарные слова, а не буква. Дальше по шагам:

  1. собираем инвентарь из 81 312 лемм (kaborfodict плюс kabrudict), из них 38 442 глагольных инфинитива;

  2. у словоформы снимаем только словоизменение (падеж, число, финитные суффиксы -щ/-ащ/-т/-къым) и личные/динамические префиксы (сы-, у-, е-, йо-, о-, що-);

  3. а превербы не трогаем и достраиваем инфинитив обратно, проверяя по словарю.

# упрощённо: восстановление глаголаdef as_verb(stem, verbs):    if stem in verbs:        return stem    for inf in ("ын", "эн", "ун", "н"):        if stem + inf in verbs:            # тхы + н -> тхын            return stem + inf    for p in PERSONAL_PREFIXES:            # снять е-/йо-/о-/що-        if stem.startswith(p):            b = stem[len(p):]            for inf in ("ын", "эн", "ун", "н"):                if b + inf in verbs:                    return b + inf    return None

Токен-лемма получается настоящим словом, а не согласной. тӏысыжкъищтащщоджэгу схлопываются в тӏысыжынкъищтэнджэгуэн. Кулинарный апокалипсис отменяется.

Часть речи вышла почти даром: в кабардино-русском словаре у слов есть русские пометы (гл.прил.нареч.), инфинитив на  это глагол, всё остальное по умолчанию существительное.

Я ставил себе честную планку в 75%. Прогнал на двух реальных народных сказках:

  • «Бдзэжьеящэмрэ бдзэжьеймрэ» (Рыбак и рыбка), 669 слов — 77% слов получили лемму и часть речи;

  • «Шырыкъу зыщыгъ джэду» (Кот в сапогах), 1132 слова — 73%.

В среднем около 75%, ровно вокруг планки. И это с оговоркой в мою же сторону: сказки я записывал на слух, и сам текст не идеально грамотный. По вычитанному тексту было бы выше.

Что осталось в непокрытых? Две вещи. Первая — сложные глаголыкъыфӏэрыхьэнущкъыретыжсыӏутӏыпщыжыну. Тот самый полисинтетический хвост с гроздью префиксов и суффиксов вокруг корня. Именные слова разбираются почти полностью, а глагол остаётся последним рубежом. Вторая — имена собственные: во втором тексте верхушку «промахов» заняли маркиз и Къэрэбас, персонажи сказки. Их в словаре нет и быть не должно, это отдельная задача (о ней ниже). Но 77% и 73% против «абсолютно невозможно» это, согласитесь, другой разговор.

Детективная деталь: палочка, которая ломала всё

Отдельная засада, на которую я потратил вечер. В кабардинском есть буква «палочка» (Ӏ). В моём словаре она записана латинской I, а в реальных текстах люди печатают кириллической ӏ (U+04CF), потому что стандартной кабардинской раскладки в России до сих пор нет, и каждый выкручивается как может (I, l, ӏ, |).

Результат: фӏы из текста и фIы из словаря это разные строки, совпадений ноль. Пока не нормализуешь всё к одной палочке, весь стек молча выдаёт пустоту. Одна строчка нормализации, и покрытие взлетает.

И тут же вылез приятный побочный эффект. Когда я прогнал сказку, словарь начал ловить мои ошибки, которых я, записывая на слух, даже не замечал. В тексте я писал бдзэжэй, а словарная форма бдзэжьей (рыбка) — с мягким знаком. Получился дефолтный спелл-чекер для языка, у которого спелл-чекера нет: слово не нашлось точно, но нашлось при снятии выпавшего ь и вариации э/е, значит вот правильное написание.

Правда, тут же выяснилось, где эта наивность опасна. Слепо править нельзя: хы (море) и хьы это разные слова, тхэ и Тхьэ (Бог) тоже. Автокоррекция запросто превратит «Бога» в опечатку. Поэтому словарь у меня работает как ассистент вычитки: предлагает исправление, а решает носитель. Для языка без единого автокорректора даже это уже полезный инструмент.

Пункт обвинения №2: тональность языка без модели тональности

Sentiment-модели для кабардинского не существует и в ближайшее время наверно не будет. Но она и не нужна, если построить мост.

Логика такая. Есть английский словарь валентности Warriner et al., 14 000 слов с оценкой «плохо — хорошо» по шкале 1-9. Дальше двухходовка:

  1. Английский в русский. Беру русские слова из Wiktionary, у каждого есть английское определение (глосс). Оцениваю слова глосса по Warriner и получаю валентность русской леммы. Так родилась таблица ru-sent на 40 000 русских слов.

  2. Русский в кабардинский. Беру кабардинское слово, нахожу его русский перевод в kabrudict, оцениваю по ru-sent. Получаю валентность кабардинского слова.

Итоговый перенос весов: английский, затем русский, затем кабардинский. Чистая арифметика по хеш-таблицам. Ни одной нейросети, ни одного вызова переводчика в рантайме.

Проверяем на живых словах:

зауэ (война)          -2.91уз (боль, болезнь)    -1.96шынэ (страх)          -2.64лъагъуныгъэ (любовь)  +2.35насып (счастье)       +1.81узыншагъэ (здоровье)  +1.25

Знаки на месте. 81% статей словаря получили тональность.

Этот тональный словарь я выложил в открытый доступ: 26 663 леммы, лицензия CC BY-SA 4.0, с примером использования — github.com/kubataba/kbd-sentiment. Насколько знаю, это первый открытый sentiment-ресурс для кабардинского. Забирайте, сравнивайте тон предложений, дополняйте. Вот как это выглядит на предложении:

$ python3 example.py "лъагъуныгъэрэ мамырыгъэрэ"  лъагъуныгъэрэ        2.35  мамырыгъэрэ          1.22средняя валентность = +1.79  ->  позитив (2 слова оценено)

Пункт обвинения №3: смысловой поиск без эмбеддингов

Последний рубеж — эмбеддинги (векторы смысла) для семантического поиска и сегментации. У Apple есть встроенная модель NLContextualEmbedding, но кабардинского в ней нет.

Хитрость в том, что модель скрипт-зависимая, а не язык-зависимая. Кириллица это русская модель. Что если скормить ей кабардинский текст напрямую? Проверил — не работает (то же самое было с белорусским). По дороге, кстати, вскрылись два неочевидных факта:

  • Сырой косинус бесполезен. У контекстных эмбеддингов анизотропия: всё похоже на всё с косинусом около 0.9. Судить можно только по центрированному косинусу (вычесть общий вектор). Это переворачивает результаты с ног на голову.

  • NLContextualEmbedding игнорирует подсказку языка внутри одного скрипта. Три разных варианта дали бит-в-бит одинаковые числа.

Раз напрямую нельзя, снова мост через русский. Кабардинское предложение, затем русские глоссы каждого слова, затем эмбеддим русский текст русской моделью Apple. Тест на 6 тройках предложений (похожее против несвязанного):

Kabardian (Russian gloss pivot):  CENTERED  5/6 pass  margin +0.270  ->  PASS

+0.270 это на уровне украинского (+0.321) и латышского (+0.221), которые работают нативно. И это с грубым стеммером и 75% покрытия, то есть пол, а не потолок.

Что получилось

Язык, который ИИ назвал открытым космосом, за несколько вечеров получил полноценную обработку, причём на данных, которые у меня уже были:

Сигнал

Кабардинский

Как

Лемма и часть речи

75% (две сказки ~2К слов)

словарь 81k лемм плюс стеммер на инфинитив

Тональность

81% статей

мост англ, рус, кбд по Warriner

Эмбеддинги

+0.270 (PASS)

пивот через русские глоссы плюс модель кириллицы Apple

Ни обучения, ни GPU, ни FST-лабораторий. Только эмбеддинги кириллицы от Apple, английский словарь валентности, переброшенный в русский и кабардинский, и старые добрые словари морфем.

Мораль, которая работает не только для кабардинского: малый язык можно дотянуть через высокоресурсного родственника, на которого есть двуязычный словарь. Русский дал и таблицу тональности, и модель эмбеддингов. Тот же приём применим к десяткам языков со словарями, но без своих моделей.

Чего пока нет: NER

Честный клиффхэнгер. Один сигнал из пяти ещё не сдался — распознавание имён (NER). Я его уже увидел вживую: в «Коте в сапогах» верхние строчки «непонятых» слов заняли маркиз и Къэрэбас. Это не ошибка морфологии, а именно имена, которых нет ни в одном словаре и быть не должно, но на них завязаны и поиск, и «важность» предложений. План простой: наивный газеттир, где слово с большой буквы, которого нет среди обычных слов, это кандидат в имя, а имена собственные из kabrudict дадут позитивный список. Это тема следующей серии.

Вместо заключения

Я не лингвист и не обучал нейросетей для этой задачи. Я просто не поверил, что мой родной язык «нерешаемый», и проверил каждый аргумент руками. Оказалось, «невозможно» часто значит «никто пока не приложил словарь».

Если вы носитель северокавказского языка и у вас есть словари (морфемные, двуязычные, орфографические), напишите, буду рад помочь оживить и ваш язык тем же методом. И, как в прошлый раз, зову энтузиастов кабардинского в проект записи Common Voice: без данных даже самый элегантный стек мёртв.

P.S. Отдельное спасибо Gemini Pro за приговор. Без него не было бы такого азарта его обжаловать.

ссылка на оригинал статьи https://habr.com/ru/articles/1060378/