Почему мансийский? И почему это сложнее, чем кажется
Мансийский язык - это не просто "еще один язык". Это финно-угорский язык с агглютинативной морфологией, где одно слово может превратиться в целое предложение. Например, "я пойду домой" - это одно слово с кучей суффиксов. Теперь представьте, что носителей осталось меньше тысячи, а параллельных текстов - пара тысяч предложений. Вот и весь датасет.
Главная ошибка новичков: пытаться скормить мансийский стандартной NMT-архитектуре. Так модель просто запомнит пару примеров и начнет галлюцинировать.
Сбор данных: охота за редкими текстами
Когда у тебя нет доступа к миллионам параллельных предложений, каждый текст на вес золота. ЮНИИИТ начинал с того, что собирал:
- Учебники по мансийскому языку 1960-х годов (бумажные, отсканированные)
- Переводы классической литературы (Пушкин, Толстой - по 5-10 страниц)
- Фольклорные тексты (сказки, легенды)
- Переводы законов и официальных документов
1Препроцессинг: не просто токенизация
Стандартная токенизация по пробелам для мансийского - это путь в никуда. Слово "хӯталтыгла" ("мы с ним поговорили") нужно разбирать на морфемы. Используем морфологический анализатор, если он есть. Если нет - пишем свой на основе правил.
# Пример морфологического разбора мансийского слова
def analyze_mansi_word(word):
# Базовые суффиксы (упрощенно)
suffixes = {
'тыг': 'прошедшее время',
'л': 'множественное число',
'гла': '1 лицо множественное число',
'с': 'принадлежность'
}
# Ищем суффиксы с конца слова
stems = []
current_word = word
for suffix in sorted(suffixes.keys(), key=len, reverse=True):
if current_word.endswith(suffix):
stems.append((suffix, suffixes[suffix]))
current_word = current_word[:-len(suffix)]
stems.append(('основа', current_word))
return stems[::-1]
# Разбираем слово "хӯталтыгла"
print(analyze_mansi_word("хӯталтыгла"))
# [('основа', 'хӯта'), ('л', 'множественное число'),
# ('тыг', 'прошедшее время'), ('гла', '1 лицо множественное число')]Выбор архитектуры: трансформеры против RNN
Здесь начинается самое интересное. Трансформеры жрут данные как не в себя. Им нужно минимум 100К примеров, чтобы начать что-то понимать. У нас - 5К. RNN (точнее, LSTM) более экономны, но медленнее и капризнее.
| Архитектура | BLEU на 5К данных | Время обучения | Память |
|---|---|---|---|
| Transformer (базовый) | 8.2 | 3 часа | 4 ГБ |
| LSTM с вниманием | 12.7 | 6 часов | 2 ГБ |
| Tiny Transformer | 15.3 | 2 часа | 1.5 ГБ |
Мы выбрали Tiny Transformer - уменьшенную версию с 4 слоями вместо 6, размером эмбеддингов 256 вместо 512. Работает на ноутбуке с GTX 1660 Ti. Если интересно про другие компактные архитектуры, посмотрите обзор офлайн-ИИ моделей.
2Аугментация данных: как из 5К сделать 50К
Когда данных мало, их нужно размножать. Но не просто дублировать - это бесполезно. Мы использовали:
- Back-translation: переводим русский текст через Google Translate на английский, потом обратно на русский. Получаем парафразы.
- Морфологическая замена: меняем суффиксы в мансийских словах ("хӯтал" → "хӯтас", "хӯтав")
- Синтаксический переворот: меняем порядок слов в предложении (для мансийского это работает иначе, чем для русского)
# Пример back-translation аугментации
import googletrans
from googletrans import Translator
translator = Translator()
def augment_with_backtranslation(text, src='ru', pivot='en'):
# Русский → Английский
en_text = translator.translate(text, src=src, dest=pivot).text
# Английский → Русский
ru_back = translator.translate(en_text, src=pivot, dest=src).text
# Если текст изменился более чем на 30% - используем
if calculate_similarity(text, ru_back) < 0.7:
return ru_back
return None
# Для мансийского используем русский как pivot
# Мансийский → Русский → Английский → Русский → Мансийский (в идеале)Важно: Google Translate не знает мансийский. Поэтому цепочка длиннее: мансийский → (ручной перевод) → русский → английский → русский → (ручной перевод обратно) → мансийский. Полуавтоматически, зато работает.
Обучение с transfer learning: воруем знание у больших моделей
Вот тут фокус. Берем предобученную модель для перевода с русского на английский (их полно). Заменяем embedding слои: русский оставляем, английский меняем на мансийский. Первые несколько эпох замораживаем все слои, кроме последних двух и embedding'ов.
Почему это работает? Модель уже умеет понимать структуру языка, внимание, контекст. Нам нужно только научить ее "соответствию" между русскими и мансийскими паттернами. Это как переучить полиглота на новый язык, а не учить с нуля.
3Файнтюнинг: как не переобучить
С малыми данными переобучение наступает через 2-3 эпохи. Используем:
- Early stopping с patience=2
- Dropout 0.5 (да, половинку нейронов выключаем)
- Label smoothing (делаем таргеты "мягкими")
- Gradient clipping (обрезаем большие градиенты)
# Конфигурация обучения для малоресурсного языка
from transformers import Seq2SeqTrainingArguments
training_args = Seq2SeqTrainingArguments(
output_dir="./models/mansi_translator",
num_train_epochs=20, # Но остановимся раньше
per_device_train_batch_size=4, # Маленький батч
per_device_eval_batch_size=4,
warmup_steps=100,
weight_decay=0.01,
logging_dir="./logs",
logging_steps=10,
save_steps=100,
eval_steps=100,
save_total_limit=2, # Храним только 2 лучшие модели
load_best_model_at_end=True,
metric_for_best_model="bleu",
greater_is_better=True,
prediction_loss_only=False,
# Критически важные параметры:
learning_rate=5e-5, # Очень маленький LR
gradient_accumulation_steps=4, # Эмулируем батч 16
fp16=True, # Используем половинную точность
label_smoothing_factor=0.1, # Сглаживание таргетов
max_grad_norm=1.0, # Clipping градиентов
dataloader_drop_last=True,
)Оценка качества: BLEU - это только начало
BLEU score 15 для перевода с русского на мансийский - это как 50 для популярных языков. Почему? Потому что один и тот же смысл можно выразить десятком разных способов из-за богатой морфологии.
Мы добавили:
- Морфологическую точность: правильно ли проставлены суффиксы
- Словарный охват: использует ли модель редкие слова или только частотные
- Ручную оценку носителями (самое важное!)
| Метрика | Наша модель | Rule-based | Google Translate* |
|---|---|---|---|
| BLEU | 15.3 | 8.7 | N/A |
| Морф. точность | 72% | 85% | N/A |
| Охват слов | 64% | 41% | N/A |
| Скорость (с/предл) | 0.8 | 0.1 | N/A |
* Google Translate не поддерживает мансийский. Впрочем, как и большинство коммерческих систем. Если интересно, как работают современные коммерческие переводчики, почитайте про обновления Google Translate.
Проблемы, которые заставят вас вырвать волосы
Проблема 1: OOV (Out-of-Vocabulary) слова
Словарь мансийского - 20К слов. Наша модель знает 5К. Что делать с остальными? Byte Pair Encoding (BPE) спасает, но частично. Для редких языков лучше использовать character-level модели или гибридные подходы.
Проблема 2: Диалектные различия
Северный мансийский vs южный мансийский - как британский английский vs американский, только хуже. Модель начинает мешать диалекты. Решение: указывать диалект как специальный токен в начале предложения.
Проблема 3: Оценка без референсов
Для многих предложений у нас есть только один эталонный перевод. Модель может выдать грамматически правильный, но лексически другой вариант - и получит низкий BLEU. Используем BERTScore или COMET.
Самый болезненный момент: носители языка критикуют перевод, потому что "так не говорят". Но они сами говорят по-разному! Приходится выбирать "усредненный" вариант, который всех разочаровывает.
Развертывание: чтобы пользоваться могли даже бабушки
Носители мансийского - часто пожилые люди в отдаленных поселках. Им нужен:
- Простой веб-интерфейс (большие кнопки, крупный шрифт)
- Офлайн-режим (интернет в тайге - это роскошь)
- Голосовой ввод/вывод (многие не любят печатать)
Мы сделали Telegram-бота и простое веб-приложение на Flask. Модель весит 300 МБ - помещается на телефон. Для голосового интерфейса использовали легкие TTS-модели.
# Минимальный Flask API для перевода
from flask import Flask, request, jsonify
import torch
from transformers import AutoModelForSeq2SeqLM, AutoTokenizer
app = Flask(__name__)
# Загружаем модель один раз при старте
model = AutoModelForSeq2SeqLM.from_pretrained("./models/mansi_final")
tokenizer = AutoTokenizer.from_pretrained("./models/mansi_final")
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
model.to(device)
model.eval()
@app.route('/translate', methods=['POST'])
def translate():
data = request.get_json()
text = data.get('text', '')
dialect = data.get('dialect', 'northern') # северный или южный
# Добавляем токен диалекта
if dialect == 'southern':
text = " " + text
else:
text = " " + text
inputs = tokenizer(text, return_tensors="pt", max_length=128, truncation=True)
inputs = {k: v.to(device) for k, v in inputs.items()}
with torch.no_grad():
outputs = model.generate(**inputs, max_length=256, num_beams=4)
translated = tokenizer.decode(outputs[0], skip_special_tokens=True)
return jsonify({
'original': data['text'],
'translated': translated,
'dialect': dialect
})
if __name__ == '__main__':
app.run(host='0.0.0.0', port=5000, debug=False) Что в итоге получилось?
Модель, которая переводит простые бытовые фразы с точностью около 70%. Сложные предложения, идиомы, поэзию - не тянет. Но для:
- Перевода меню в школе
- Объявлений в больнице
- Простых разговорников
- Оцифровки архивных записей
- работает. Главное - она дает базовый инструмент, который могут дорабатывать лингвисты.
Советы, если беретесь за свой исчезающий язык
- Начинайте со сбора данных, а не с выбора модели. Без данных - ничего.
- Привлекайте носителей с самого начала. Не делайте "для них", делайте "с ними".
- Используйте transfer learning. Не пытайтесь обучить с нуля на 1000 примеров.
- Ожидайте низких метрик. BLEU 10-15 - это успех для малоресурсного языка.
- Сделайте простой интерфейс. Если людям сложно пользоваться - проект умрет.
- Планируйте постоянное обновление. Каждый новый текст улучшает модель.
И последнее: не надейтесь сделать идеальный переводчик. Цель - не заменить человека, а создать инструмент, который поможет сохранить язык. Даже если он ошибается в 30% случаев - это лучше, чем ничего.
Кстати, похожие подходы работают и для других задач - например, для создания RAG-систем на специфичных доменах или для перевода кода между языками. Мало данных - не приговор, это вызов.
P.S. Через год после запуска проекта в мансийский корпус добавили 2000 новых предложений - школьные сочинения, переписки в соцсетях. Модель дообучили - и BLEU вырос с 15.3 до 18.7. Мораль: такие проекты должны быть живыми. Как и языки, которые они пытаются сохранить.