Главная / Статьи / От базовой модели к чат-модели: SFT-дообучение индонезийской LLM на 102M

От базовой модели к чат-модели: SFT-дообучение индонезийской LLM на 102M

Как индонезийская базовая модель на 102M параметров стала чат-моделью за два этапа SFT: формат ChatML, фильтрация по длине, накопление градиентов и патч идентичности.

1980 слов

После предобучения NovAI Base 102M следующим шагом стало не масштабирование, а обучение следовать инструкциям. Базовая модель — decoder-only трансформер на 102 миллиона параметров, обученный с нуля примерно на 2 миллиардах индонезийских токенов, и она работает: пишет по-индонезийски, продолжает промпт и усвоила достаточно статистической структуры, чтобы выдавать связный текст. Но настоящим чат-ботом она не является. Задайте ей вопрос, и ответ не гарантирован, потому что её учили предсказывать следующий токен, а не распознавать такой разговорный шаблон:

User → question
Assistant → answer

Более мощных GPU для модели побольше не было, поэтому в эксперименте оставили ту же модель, а поменяли цель обучения и данные. Методом стало Supervised Fine-Tuning (SFT).

Базовая модель — ещё не ассистент

Этот разрыв легко не заметить, когда пользуешься ChatGPT, Claude, Gemini и похожими инструментами, потому что эти модели прошли несколько этапов обучения. Предобученная языковая модель начинает с куда более простого: предсказать следующий токен. Например:

Indonesia adalah negara ...

Она учится тому, что вероятно последует дальше. Это принципиально отличается от такого обмена:

User:
Apa itu machine learning?
Assistant:
Machine learning adalah ...

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

Indonesian Corpus
       ↓
Pretraining
       ↓
NovAI Base 102M
       ↓
Supervised Fine-Tuning
       ↓
NovAI Chat 102M

Примечательно, что архитектура осталась прежней. Число параметров так и осталось около 102 миллионов. Изменилось то, чему модель учили подражать.

Отправная точка

SFT начинался с предобученного чекпойнта NovAI Base 102M V3. Предобучение охватило 2 000 027 648 токенов за 15 259 шагов, и логи показывают BF16 на CUDA во время запуска на A100. К концу модель стала пригодной языковой моделью, но теперь ей были нужны совсем другие данные. Больше веб-текста не помогло бы; помогли бы примеры желаемых ответов.

Датасет для SFT

Датасет прошёл несколько итераций. Вторая версия началась с существующего набора из 30 084 SFT-примеров, к которому добавили 324 примера с персоной и фактическими вопросами-ответами, всего 30 408. Это добавление было важным. Новые примеры не были обычными вопросами-ответами. Часть написали специально, чтобы закрепить факты о самой модели: её идентичность, роль и связанные ответы. Данные хранились как разговоры, концептуально вот так:

User:
Apa itu machine learning?
Assistant:
Machine learning adalah ...

Но модель не видит аккуратно оформленных разговоров. Она видит токены, поэтому нужно было решить, как разговоры сериализуются.

Обучение формату разговора

Использовалась структура в стиле ChatML с явными границами разговора:

<|im_start|>
<|im_end|>

Разговор получается примерно таким:

<|im_start|>user
Apa itu machine learning?
<|im_end|>
<|im_start|>assistant
Machine learning adalah ...
<|im_end|>

Так модель понимает, где начинается сообщение, где заканчивается и какая роль его написала. В токенизаторе также настроили шаблон чата, чтобы ту же структуру можно было генерировать в коде и при обучении, и при инференсе. Может показаться мелкой деталью реализации, но это не так. Если формат обучения и формат инференса различаются, модель может выучить один разговорный протокол, а работать её попросят с другим. Для маленькой модели приоритетом было держать этот интерфейс простым и детерминированным.

Отсев длинных разговоров

Следующим вопросом была длина последовательности. Для этого запуска максимум токенизатора выставили на 512 токенов. Ноутбук токенизирует каждый отрендеренный разговор без обрезки и отбрасывает примеры, превышающие лимит. Логика фильтра по сути такая:

def filter_long_sequences(example):
    text = tokenizer.apply_chat_template(
        example["messages"],
        tokenize=False
    )
    tokens = tokenizer(
        text,
        truncation=False
    )    return len(tokens["input_ids"]) <= 512

Почему бы просто не обрезать? Потому что обрезка может отрезать именно то, что важно. Если разрезать обучающий пример посередине ответа ассистента, модель будет учиться на неполном ответе. Решили выбрасывать слишком длинные последовательности, а не тихо менять их смысл. Небольшое инженерное решение, но в маленьких моделях такие решения имеют вес.

Запуск обучения

Обучение шло на стеке Hugging Face: PyTorch, Transformers, TRL и его SFTTrainer. Тренер настроили прямо на предобученной модели и объединённом SFT-датасете. Эффективный размер батча получается таким:

16 × 2 = 32

Накопление градиентов использовали потому, что цели уместить весь эффективный батч в память GPU разом не было. Работает это так:

Batch 1
   ↓
forward / backward
Batch 2
   ↓
forward / backward
   ↓
optimizer update

В итоге эффективный батч равен 32, и GPU не нужно держать все 32 примера одновременно. В ноутбуке max_steps выводится из размера датасета и эффективного батча, так что основной запуск — это примерно один проход по данным, а не произвольно большое число эпох.

Первый результат не стал финалом

После первого этапа SFT появилась новая модель, NovAI Chat 102M, которую можно было использовать в формате чата. Тесты показали поведение, которого до конца не ожидали. Модель отвечала, но не всегда чисто. В одном интерактивном тесте она пропустила короткий ответ и ушла в посторонний текст, в итоге воспроизводя фрагменты вроде:

<Kata-kata yang menggambarkan kehidupan seorang wanita...

а потом начала выдавать незавершённый спецтокен:

<|im_

Так задумано не было, и это показывает, чем полезны эксперименты с маленькими моделями. Странный вывод передовой модели легко списать на кого-то другого. С моделью, которую собрал сам, приходится разбираться, какая часть пайплайна в этом виновата. Датасет? Шаблон чата? Цель обучения? Настройки сэмплирования? Ограниченная ёмкость модели? Слишком мало примеров ожидаемого поведения? Единственного ответа могло и не быть, но одну конкретную проблему решить было можно.

Проблема идентичности

У модели должна быть стабильная идентичность. Если кто-то спрашивает:

Siapa kamu?

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

Переобучать с нуля было бы перебором, поэтому решение вышло гораздо скромнее: отдельный датасет идентичности и ещё один этап SFT в роли патча идентичности. В ноутбуке этап помечен сообщением PATCH TRAINING IDENTITAS (LLM-NOVA) DIMULAI («начато обучение патча идентичности»). Он не должен был давать новых общих знаний. Цель была узкой: закрепить идентичность и поведение, которые модель должна связывать с собой.

Второй этап SFT

Второй этап начинался с уже дообученной чат-модели, а не с исходной базовой. Пайплайн стал таким:

NovAI Base 102M
       │
       │ General SFT
       ▼
NovAI Chat 102M
       │
       │ Identity SFT
       ▼
NovAI Chat 102M + Identity

На этапе идентичности конфигурация была осторожнее, и это намеренно. Общий SFT пытается изменить разговорное поведение модели целиком. Этап идентичности гораздо уже, и целью была небольшая поведенческая поправка, а не очередная агрессивная переделка.

Почему не включить это в первый запуск?

Естественный вопрос: почему примеры идентичности просто не добавили в основной датасет и не обучили всё вместе? Доводы за это есть. Однако отдельные этапы позволяют изолировать эффект. Вместо такого:

30K+ examples
      ↓
one opaque training run

можно рассуждать о двух этапах:

General instruction data
        ↓
Chat behavior
thenIdentity examples
        ↓
Identity behavior

Разделение этапов делает эксперимент понятнее. Оно же отражает урок, который с маленькими моделями становится всё весомее: не каждая проблема требует ещё одного большого запуска обучения. Иногда правильный ответ — точечное вмешательство.

Число параметров так и не изменилось

Посчитайте параметры на каждом шаге. До SFT — 102M. После SFT — 102M. После прохода идентичности — всё те же 102M. Архитектура не стала мощнее, и никаких миллиардов параметров не появилось. Изменились веса и то, на чём их обучали. Предобучение оптимизировало моделирование языка. SFT оптимизировал подражание примерам следования инструкциям. А этап идентичности подтолкнул модель к одному конкретному шаблону поведения.

Что показывает эксперимент

Не стоит переоценивать результат. Модель на 102M не может конкурировать с передовыми моделями класса GPT, и ничто здесь не утверждает обратного. И это не доказывает, что SFT волшебным образом улучшает рассуждения маленькой модели — не улучшает. Результат практичнее: предобученная индонезийская языковая модель может пройти ещё один этап обучения, который явно учит её форме разговора, тому, как отвечать на инструкцию, как выглядит хороший ответ ассистента, и, через точечные примеры, тому, как описывать себя. Это совсем другая способность, чем голое предсказание следующего токена.

У SFT есть пределы

SFT даёт многое, но это не последний этап. Сам по себе он не уберёт галлюцинации, не гарантирует фактическую точность, не создаст безграничную способность рассуждать и не даст модели доступ к информации за пределами её параметров. И он не учит, какой ответ люди предпочтут, когда возможны несколько. Для этих пробелов нужны другие техники:

Pretraining
     ↓
SFT
     ↓
Preference Optimization
     ↓
RAG / External Knowledge
     ↓
Tools / Agents

Каждый слой закрывает своё ограничение, и поэтому создание LLM перестаёт выглядеть как один запуск обучения и начинает выглядеть как пайплайн.

Чем интересны маленькие модели

Интересно здесь не то, что обучили модель на 102M. Интересно то, что весь пайплайн можно изучить — от корпуса до токенизатора и дальше:

Corpus
  ↓
Tokenizer
  ↓
Architecture
  ↓
Pretraining
  ↓
Checkpoint
  ↓
SFT Dataset
  ↓
Chat Template
  ↓
SFT
  ↓
Identity Patch
  ↓
Inference

Когда что-то идёт не так, можно вернуться и разобрать слой, который за это отвечает, а это совсем не то же самое, что вызывать API. Именно здесь окупается сборка с нуля.

Сначала построить, потом улучшать

Серия началась с вопроса, может ли Индонезия построить собственную фундаментальную модель. Модель на 102M далеко не дотягивает до передового масштаба, но отвечает на уменьшенную версию этого вопроса: маленькую языковую модель действительно можно построить и обучить локально. Следующий вопрос был в том, можно ли превратить её во что-то, что ведёт себя как ассистент. SFT дал путь, а эксперимент с идентичностью добавил урок: модель, которая умеет говорить, ещё нужно научить, как себя вести.

Что остаётся открытым

Проверить предстоит ещё несколько вещей:

  • Заметно ли поднимет результаты более сильный индонезийский набор инструкций?
  • Значит ли качество данных больше, чем их объём, и насколько?
  • Сделает ли оптимизация по предпочтениям ответы точнее?
  • Может ли узкий специалист в одном домене оказаться неожиданно полезным в таком размере, в отличие от универсала, который пытается знать всё?
  • Где потолок у маленькой индонезийской модели, если её данные, цель и оценка целиком заточены под индонезийские сценарии?

Ответов пока ни у кого нет, поэтому работа продолжается. Ничто здесь не утверждает, что модель такого размера вытеснит передовой ИИ. Цель — понять, что на самом деле происходит, когда весь пайплайн собирается вручную, этап за этапом. Предобучение было первой вехой, SFT — второй.