Від базової моделі до чат-моделі: SFT-донавчання індонезійської LLM на 102M
Як індонезійська базова модель на 102M параметрів стала чат-моделлю за два етапи SFT: формат ChatML, фільтрація за довжиною, накопичення градієнтів і патч ідентичності.
Після попереднього навчання 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 — другою.