Рис 0. Демо-чат
Рис 0. Демо-чат

Давно ничего не писал где-либо: разработка занимает много времени. Надеюсь, наш путь будет кому-либо полезен, особенно тем, кто обучает модельки на малоресурсных языках.

Большая в данном контексте - это условно. По сути, взяли Qwen3-4B-Instruct-2507 и обучили на карачаево-балкарском языке (тюркский, половецко-кыпчакская группа). По дороге пришлось написать собственный морфологический процессор для аугментации диалектов, обучить токенизатор с нуля, и найти баланс в обучении модели на сырых данных, чтобы она не забывала инструкций (а хотелось, чтобы могла отвечать).

Модель лежит на HuggingFace: TSjB/QM-4B. Работу представляли на конференции TurkLang 2026.

Зачем это вообще

Карачаево-балкарский (в дальнейшем КБ) — официальный региональный язык в Карачаево-Черкесии и Кабардино-Балкарии, на нём говорят примерно 300 тысяч человек на Северном Кавказе и около 450 тысяч в мире. При этом язык вытесняется русским: молодёжь всё чаще переходит на него, и ЮНЕСКО присвоила карачаево-балкарскому статус vulnerable (уязвимый).

Только недавно Cluade и Gemini стали понимать и писать на карачаево-балкарском. Но нет поддержки диалектов и частенько они вставляют из других языков слова, к тому же для многих моделей родной язык - английский. Наша же задача/миссия - развивать на своём языке технологии, в т.ч. сделать так, что основным языком для ИИ стал КБ. У нас уже был задел. В предыдущих работах мы собрали первый электронный параллельный корпус (289к предложений КБ и русский) (который стал в том числе основой для обучения в яндекс.переводчике) и построили первую систему нейромашинного перевода карачаево-балкарский ↔ русский на базе NLLB-200.

Логичный следующий шаг — сделать так, чтобы у языка появилась полноценная LLM, для которой он был бы основным, а не воспринимался через призму доминирующих языков обучения.


Данные: 75 733 записи, 662 млн символов

Монокорпус собран из 18 источников по семи жанрам. Три крупнейших по объёму — газета «Заман» (28,2%), антология литературы карачаево-балкарских авторов (25,8%) и сборник поэзии (10,7%). Периодика в сумме («Заман», «Карачай», «Минги Тау») даёт 45,4% — оцифрованной прессы для языка относительно много.

#

Источник

Жанр

Фрагментов

Симв. (млн)

%

1

Газета «Заман»

Периодика

13 190

125,8

28,2

2

Антология литературы

Проза

13 506

114,8

25,8

3

Сборник поэзии

Поэзия

5 853

47,7

10,7

4

Газета «Карачай»

Периодика

4 686

44,0

9,9

5

Журнал «Минги Тау»

Периодика

3 828

32,6

7,3

6

Учебники

Образование

2 151

17,9

4,0

7

Литературоведение

Наука

1 317

11,7

2,7

...

(ещё 11 источников)

...

6 918

51,0

11,4

Итого

51 449

445,5

100

 Рис. 1. Состав корпуса по источникам.
Рис. 1. Состав корпуса по источникам.

Сырые тексты прогоняются через чистку управляющих символов и пунктуации, иерархическую нарезку на чанки по 50–1200 слов с перекрытием в 200 слов, источник-специфичную обработку (срезание Wikipedia-разметки, фильтрацию газетного контента) и дедупликацию по точному совпадению (удалили 713 дублей). Финальные 75 733 записи делятся 97,5% / 2,5% на train и eval с фиксированным seed.

Диалекты

У карачаево-балкарского несколько диалектных варианта, различающихся прежде всего на фонемном уровне:

  1. Чокающие говоры (пример: чач — «волос»):

    1. Джокающий карачаевский (джол — «дорога»). Территория — Карачай.

    2. Джокающий баксанский (джол, где дж обозначает более смягчённый звук, чем дж). Территория — долина реки Баксан.

    3. Жокающий чегемский (жол) (балкарский). Территория — долина реки Чегем.

    4. Смешанный хуламско-безенгиевский (возможны одновременно жол и зол) (балкарский). Территория — долина реки Черек Хуламский.

  2. Цокающе-зокающий балкарский (цац и зол) (малкарский). Территория — долина реки Черек Балкарский.

Различия не только фонемные — многие слова отличаются целиком: «тюл» / «тюйюл» («нет»), «бусагъат» / «шёндю» («сейчас»), «гугурукку» / «хораз» («петух»). В сыром корпусе диалекты представлены неравномерно, и чтобы модель не уехала в один из них, корпус нужно сбалансировать. Тут можно было бы натренировать маленькую модель-конвертер, но диалектное отображение — это регулярная фонология, и нейросетка стала бы галлюцинировать там, где правила работают детерминированно. Поэтому мы написали rule-based морфологический процессор.

Морфологический процессор: аугментация диалектов по правилам

Процессор переводит словоформы между диалектами, уважая агглютинативную морфологию. Для самого процессора сделан словарь пар (литературный - карачаевский диалект - балкарский диалект), благодаря которому он может перевести в необходимый диалект.

Работает в три стадии:

Стадия 1. Нормализация. Каждое слово приводится к диалект-нейтральной форме: диалект-специфичные буквы заменяются абстрактными символами. «дж»/«ж» → «j» (в начале слова), «б»/«п» → «b» (в конце), «у»/«ў» → «w» (в интервокальной позиции). Словарь диалектов (~10 700 записей) проходит ту же нормализацию — так становится возможным кросс-диалектное сопоставление.

Стадия 2. Сопоставление по словарю. Нормализованные слова матчатся со словарём по стратегии наидлиннейшего префикса. Например, «джашчыкъ» («мальчик», уменьшительное) после нормализации становится «jашчыкъ» и матчится со словарной записью «jаш» («парень»), оставляя «чыкъ» как суффикс. Примерно у 50% слов в типичном тексте находится префиксный матч, остальные остаются как есть.

Стадия 3. Морфонологическое преобразование. При совпадении корень заменяется на корень целевого диалекта, а извлечённый суффикс преобразуется по правилам:

  1. Сингармонизм. Гласные суффикса подстраиваются под последнюю гласную нового корня (передний/задний ряд): а→е, ы→и, у→ю, о→ё.

  2. Ассимиляция согласных. Дательный суффикс «-гъа» становится «-ха» после глухих {ш, ц, т, с, щ, б, ч, п}: «бабуш» + «гъа» → «бабушха».

  3. Озвончение. Конечные «к»/«къ» переходят в «г»/«гъ» перед суффиксом на гласный: «башлыкъ» + «ын» → «башлыгъын».

  4. Эпентеза «с». Между гласной основы и гласной суффикса вставляется «с»: «башха» + «ын» → «башхасын».

Под конец абстрактные символы переводятся в орфографию целевого диалекта («j» → «дж» для карачаевского, «ж» для балкарского, «з» для малкарского).

Полный разбор на живом примере (балкарский → карачаевский).

Вход: балтузугъуз («ваш сахар»)
1. Замена корня по словарю: балтуз → шекер
2. Аффикс → фонемная форма: угъуз → u-g-u-z
3. Сингармонизм (последняя гласная «е», e-класс): u-g-u-z → и-г-и-з
4. Фонологические правки:
Выход: шекеригиз

С помощью процессора мы аугментировали корпус: +10 000 литературных записей, +7 998 карачаевских, +5 499 балкарских и +1 500 малкарских — всего 24 997.

Сложив с Оригинальными записями, по итогу получили 75 733 записей.


Семь экспериментов на 0.6B

Прежде чем жечь A100 на 4B-модели, мы прогнали пилотную сетку на Qwen3-0.6B: 6000 примеров, одна эпоха, RTX 4060 8 ГБ, ~1,5–2,5 часа на прогон. Сравнивали три стратегии токенизации:

  1. оригинальный токенизатор;

  2. add_tokens — добавление КБ-токенов к старому через resize_token_embeddings;

  3. полностью новый (кастомный) 130K-токенизатор (о нём дальше)

× несколько методов обучения (QLoRA r=8 α=16; FT только эмбеддингов; Full FT всех слоёв)

#

Метод

Токенизатор

Eval loss

Perplexity

Качество

1

QLoRA

Оригинал

2,728

15,30

Хорошее

2

QLoRA

Add tokens

2,530

12,55

Лучшее

3

QLoRA

Новый 130K

6,802

899,73

Плохое

4

FT (emb)

Новый 130K

7,950

2836

Плохое

5

Full FT

Оригинал

2,745

15,56

Отличное

6

Full FT

Add tokens

2,752

15,67

Отличное

7

Full FT

Новый 130K

7,783

2399

Плохое

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

В итоге, если мало данных (очень малоресурсный язык), то оптимальным вариантом является обучение, используя адаптеры и оригинальный токенизатор. Главное подобрать модель, которая знает родственные крупные языки и схожа по системе письма (кириллица, латиница, ...).

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

Свой токенизатор: 130K vocab и 3× плотность

Стандартные мультиязычные токенизаторы катастрофически неэффективны для агглютинативных языков. Вот как оригинальный токенизатор Qwen3 режет слово «къарачай» («Карачай»):

къарачай → [«к», «ъ», «ар», «ача», «й»]   # 5 токенов

Вот так "посимвольно" генерировать очень долго модель будет, заставить её понимать - нужно много данных и часов обучения. И всё равно для неё количество токенов из словаря с близкой вероятностью будет большим при генерации последующего.

Поэтому мы обучили новый BPE-токенизатор с нуля на тщательно подобранном корпусе: ~76,5% карачаево-балкарский (все диалекты), ~11,5% русский, ~11,5% английский и ~0,5% черкесский. Мультиязычная добавка нужна, чтобы модель не разучилась обрабатывать русский и английский. Целевой размер словаря — 130 000 токенов.

Результат на полном датасете:

Метрика

Qwen3 (оригинал)

Кастомный

Всего токенов

372 305 700

123 084 597

Символов на токен

1,78

5,38

Сокращение

66,94%

Соотношение

3,02×

Рис. 2. Сравнение токенизаторов: всего токенов и плотность.
Рис. 2. Сравнение токенизаторов: всего токенов и плотность.

То есть на тех же данных модель видит в три раза меньше токенов, а каждый токен несёт в три раза больше смысла. А также скорость генерации выше.

И, главное:

къарачай → [«къарачай»]   # 1 токен вместо 5

Инициализация эмбеддингов: усреднение подтокенов

Заменив токенизатор, мы получаем 130 000 новых токенов, для которых нужно с чего-то начать. Случайная инициализация потребовала бы на порядки больше данных, чтобы сойтись. Мы использовали метод усреднения подтокенов из работы Impact of Tokenization on LLaMa Russian Adaptation (arXiv:2312.02598): для каждого нового токена смотрим, как его резал бы старый токенизатор Qwen3, и инициализируем новый эмбеддинг как среднее эмбеддингов этих подтокенов.

v_{new}(token) = \frac{1}{K} \sum_{j=1}^{K} v_{old}(subtoken_j)

Где v_new - вектор токенов нового токенизатора, v_old - вектор токенов старого токенизатора, token - токен в новом токенизаторе, subtoken_j - токен в старом (подтокен для нового).

Например, новый токен «къарачай» старый токенизатор резал на [«к», «ъ», «ар», «ача», «й»] (K=5). Новый эмбеддинг = среднее этих пяти старых эмбеддингов. Так новый токен наследует смысл от своих частей, и модели остаётся не учить его с нуля, а лишь уточнить. Тем же способом инициализируем оба слоя — и входной (embed_tokens), и выходную проекцию (lm_head).

Финальное обучение: два этапа с постепенной разморозкой

Напоминаю: данные — это монокорпус, сырой текст без инструкций. Наивный файнтюн instruct-модели на нём убивает чат-режим. Поэтому, чтобы найти баланс между знаниями и чат режимов, — два этапа.

Этап 1: только эмбеддинги

Замораживаем все трансформерные блоки. Учим только входной и выходной эмбеддинг-слои (~0,4% параметров) с высоким learning rate 2e-4 на 3 эпохи. Batch size 16 на устройство, gradient accumulation 2 (эффективный BS 32), cosine scheduler с 5% warmup, max seq length 2048. Цель — сдвинуть новые эмбеддинги в правильные места пространства, не трогая накопленные знания трансформера.

# После 3 эпох на A100 80GB (~4,4 ч):
{'eval_loss': 4.4946, 'epoch': 2.93}
{'train_loss': 1.2727, 'epoch': 3.0}

Большой разрыв между train и eval loss ожидаем: одни эмбеддинги не могут полностью схватить понимание языка, но создают фундамент для этапа 2.

Этап 2: Full FT всех слоёв

Размораживаем всё, но с learning rate 5e-6 — в 40 раз ниже, чем на этапе 1 — строго на одну эпоху. Batch size 4, gradient accumulation 8, cosine scheduler с 3% warmup, gradient checkpointing для экономии памяти. ~6,3 часа на A100 80 ГБ.

{'loss': 4.4295, 'epoch': 0.02}
{'eval_loss': 4.3847, 'epoch': 0.16}
{'eval_loss': 4.3666, 'epoch': 0.33}
{'eval_loss': 4.3609, 'epoch': 0.49}
{'eval_loss': 4.3591, 'epoch': 0.65}
{'eval_loss': 4.3586, 'epoch': 0.81}
{'eval_loss': 4.3585, 'epoch': 0.98}
{'train_loss': 4.1965, 'epoch': 1.0}

Eval_loss падает с 4,43 до ~4,36 и выходит на плато к середине эпохи. Это ровно то поведение, которого мы хотели: модель адаптируется к языку, не перезаписывая базовые знания. Кривая — быстрый старт, потом плато.

Рис. 3. Динамика eval_loss по двум этапам обучения.
Рис. 3. Динамика eval_loss по двум этапам обучения.

Ловушка второй эпохи

Мы попробовали добавить вторую эпоху. Loss упал ниже 4,0 — вроде бы хорошо. Но модель перестала связно следовать инструкциям, уйдя в чистое продолжение текста. Классический catastrophic forgetting при претрейне на монолингвальных данных без инструкций. Пришлось откатиться на чекпоинт первой эпохи. Иногда меньший loss — это ловушка.


Результаты: что получилось

Модель (TSjB/QM-4B) опубликована на HuggingFace с интерактивным демо-чатом. Каких-то данных-бенчмарков нет на КБ языке, поэтому оценивали визуально.

Примеры

Карачаево-балкарский

Чтобы настроить чат-режим использовался следующий системный промпт

Сен къарачай-малкъар тилде болушлукъчуса. Соруўлагъа къысха, тюз эм ачыкъ джуўабла бер. Орусча неда ингилизче сорсала — ол тилде джуўаб бер.

Перевести если на русский:

Ты помощник по карачаево-балкарскому языку. Давай краткие, точные и ясные ответы на вопросы. Если спросят по-русски или по-английски отвечай на этом языке.

и настройки:

max_new_tokens=100,
temperature=0.7,
top_p=0.9,
do_sample=True,
repetition_penalty=1.2,  
no_repeat_ngram_size=4, 

Результат:

Запрос: Кавказны табигъатын суратла

Генерация: Шимал Кавказны табигъат энчилиги - деменгили чучхурла эмда чыран суула. 
Эм уллу суулары (суу секиртмеле) Архыз бла Басхан сууду. 
Бу сууланы ичинде Минги тау да бек белгили болгъаны белгилиди. 
Ол Шимал Кавказда тауланы арасында биринчиди дерчады.
Табийгъат байлыкъладан сора Къарачай табигъат байлыгъы байды: 
гара суу дегенча затла бардыла. 
Аланы кючлери бизни республиканы айбат этиуде хайырланыргъа боллукъду...
Запрос: Google компанияны юсюнден айт

Генерация: Alpha Google компания тюлдю - Google да деб айтылады. 
Компания Интернетни эмда башха затла бла байламлашдырыу джаны бла ишлейди 
(сёз ючюн: интернетархы). Аны баш офислери НьюЙоркда орналыбды. 
Компанияны бек уллу белюмю уа Лос Анджелесдады. 
Алай а аны белюмлери бютеу дунияда бардыла...
Запрос: Мен къарачай-малкъар тилде сёлешеме

Генерация: Хаў! Мен къарачайча да селешеме (черкес тилден). 
Не амал этейим? Сиз къайсы миллетденсиз огъесе къайры ишлейсиз? 
Бусагъатда мен Тюркде джашайма эмда анда бир талай иш бардырама:
китап чыгъарыу бла бирге ана тиллени окъутуугъа энчи эс бурама...

Модель неплохо выучила язык и доменный знания о Кавказе, однако очень много генерирует, уходит в размышления, генерирует порой бессмыслицу, либо ошибается. Хотя и пытается следовать инструкциям.

Другие языки

При смешении языков (системный промпт на КБ, вопрос на английском или русском) модель деградирует в кашу либо в кб срывается:

Запрос: Hello, how are you?

Генерация: Ассалам алейкум! Мен ахшыма - сау бол дейме да къойІ)
I am well, thx. иангамыз. i'll be in bishoop later.
CuCUiMeet u nybout somethng mch dwnldwrk kmt otihrs...
Запрос: Привет! Как дела?

Генерация: Салам алейкумІ да этиб турама. Сени ишинг алай болсун деп ышанама. 
Хар зат ючюн ыспас этеме мен сени бла тюбеширге деб кесими бек сюеме деген оюмну айтыргъа 
сюе эдим. Мен санга быллай соруу береме: «Мени бир тюрлю къайгъы этме»...

Зато без карачаево-балкарского системного промпта с теми же языками работает нормально:

Запрос: Hello, how are you?

Генерация: Hi there! I'm just a virtual assistant, so no feelings
or body to get "sick" or not. But I love having these little chats
with humans like yourself...
Запрос: Привет! Как дела?

Генерация: Приветствую тебя, радуйтесь радости! Пускай все будет хорошо и легко. 
У меня уа не затерлося настроение - я готов помочь в решении любых задач или вопросов.
Что вам по душе: научный переводчик для книг, помощник с расчётом суммамы на пиршество 
неда что-нибудь другое интересное? Мен здесь, чтобы сделать жизнь легчею!
Не забывайте об этом. А как у...

То есть базовые мультиязычные способности живы — ломается именно code-switching, когда система на одном языке, а пользователь на другом.

Проблемы

  1. Маловато знаний. Модель обучена на относительно небольшом объёме КБ-данных. Решение — более интенсивный файнтюн (сначала base модель, а потом instruct).

  2. Путает «ё» с «е». Модель пишет «сез» вместо «сёз», «селеш» вместо «сёлеш». Причина забавная и поучительная: распарсенные данные при помощи OCR вместо «ё» везде «е» ставит, так как базовый язык использовался русский с некоторыми правками. И LLM почему-то зацепилась именно за эти данные, хотя были и корректные источники. Решение — пересобрать данные, восстановив все подменённые «ё», и докинуть карачаевского и малкарского диалектов.

  3. Быстро уходит в «размышления». Не хватает инструкционных данных (чат-режима), поэтому модель склонна к многословию и повторам. Решение — instruct data, в том числе на диалектах.

Что дальше

Следующий этап напрямую вытекает из ограничений:

  • систематическая коррекция е → ё по всем источникам;

  • подготовка инструкционного датасета и Stage 3 — instruction fine-tuning на всех диалектах;

  • первый бенчмарк для карачаево-балкарского — сейчас его попросту не существует;

  • LLM-переводчик на замену baseline на NLLB-200;

  • в перспективе — распознавание и синтез речи, image-to-text для оцифровки старых рукописей (но сначала нужно собрать данные).

Главный вывод, если коротко: для агглютинативного низкоресурсного языка дело не в количестве примеров, а в правильной последовательности шагов — свой токенизатор, осмысленная инициализация эмбеддингов и аккуратная многоэтапная разморозка, которая не даёт модели забыть саму себя. Модель уже генерирует связный карачаево-балкарский текст, сохраняя русский и английский. Это не финал, а фундамент.

Работа представлена на XIV Международной конференции по компьютерной обработке тюркских языков TurkLang 2026. Авторы: Богдан Теунаев, Али Берберов, Лиана Берберова.