Почему все ломается на малых датасетах
Вы скачали Qwen2.5-Coder-4B-Instruct (самая свежая версия на февраль 2026, кстати), собрали 800 пар "запрос-ответ" с TypeScript кодом, запустили обучение. Через 3 эпохи модель начинает генерировать идеальный код. Через 5 - уже шедевры. А через 10 эпох она забывает, как писать функции, и вместо TypeScript выдает абракадабру, перемешанную с фрагментами из вашего датасета.
Знакомо? Это классическое переобучение на малых данных. Модель с 4 миллиардами параметров пытается запомнить 800 примеров. Получается как слон в посудной лавке - все запоминает, но контекст теряет.
Важно: Qwen2.5-Coder-4B-Instruct на февраль 2026 поддерживает контекст до 128K токенов, но это не спасает от переобучения на малых датасетах. Больше параметров - больше риска запомнить шум.
Unsloth 2026: что изменилось за год
Если вы все еще используете обычный transformers для LoRA - остановитесь. Unsloth к 2026 году научился не только ускорять обучение в 2-3 раза, но и грамотно работать с memory-efficient fine-tuning. Последняя версия (на момент написания - 0.4.8) поддерживает все новые фичи Qwen, включая улучшенную работу с RoPE scaling.
pip install unsloth[colab-new] @ git+https://github.com/unslothai/unsloth.git
И да, они до сих пор не починили этот кривой синтаксис установки. Но работает.
Конфигурация LoRA: где тонко, там и рвется
Вот типичная ошибка, которую делают 90% разработчиков:
# КАК НЕ НАДО ДЕЛАТЬ
model, tokenizer = FastLanguageModel.from_pretrained(
model_name = "Qwen/Qwen2.5-Coder-4B-Instruct",
max_seq_length = 4096,
dtype = torch.float16,
load_in_4bit = True,
)
model = FastLanguageModel.get_peft_model(
model,
r = 16, # Слишком много для малого датасета!
target_modules = ["q_proj", "k_proj", "v_proj", "o_proj", "gate_proj", "up_proj", "down_proj"],
lora_alpha = 32,
lora_dropout = 0.1,
bias = "none",
use_gradient_checkpointing = False, # Ошибка!
random_state = 42,
)
Проблема в трех местах: r=16 слишком агрессивно для 800 примеров, target_modules включает все подряд, а gradient_checkpointing выключен. Результат - модель быстро переобучается.
1 Подготовка датасета: очистка против шума
Ваш датасет на 800 примеров скорее всего содержит:
- Дубликаты (5-10%)
- Неполные примеры
- Слишком длинные/слишком короткие пары
- Разный стиль кодирования
Перед обучением прогоните через этот скрипт:
import hashlib
from collections import defaultdict
# Удаление дубликатов по содержанию
def deduplicate_dataset(examples):
seen = set()
unique_examples = []
for example in examples:
# Хэш запроса + ответа
content = example["instruction"] + example["output"]
content_hash = hashlib.md5(content.encode()).hexdigest()
if content_hash not in seen:
seen.add(content_hash)
unique_examples.append(example)
print(f"Удалено {len(examples) - len(unique_examples)} дубликатов")
return unique_examples
# Фильтрация по длине
def filter_by_length(examples, min_tokens=50, max_tokens=2000):
filtered = []
for example in examples:
tokens = tokenizer.encode(example["instruction"] + example["output"])
if min_tokens <= len(tokens) <= max_tokens:
filtered.append(example)
print(f"Отфильтровано {len(examples) - len(filtered)} примеров по длине")
return filtered
2 Конфигурация LoRA для малых данных
Вот рабочая конфигурация для 800 примеров TypeScript:
from unsloth import FastLanguageModel
import torch
model, tokenizer = FastLanguageModel.from_pretrained(
model_name = "Qwen/Qwen2.5-Coder-4B-Instruct",
max_seq_length = 2048, # Уменьшили! Для кода 2K хватит
dtype = torch.float16,
load_in_4bit = True,
# Новый параметр в Unsloth 2026:
attn_implementation = "flash_attention_2", # Ускоряет в 1.5 раза
)
model = FastLanguageModel.get_peft_model(
model,
r = 8, # Меньше rank для меньшего переобучения
target_modules = [
"q_proj", "v_proj", # Только самые важные
"gate_proj", "up_proj", # FFN слои
# Исключаем down_proj - меньше параметров
],
lora_alpha = 16, # alpha = 2*r оптимально
lora_dropout = 0.05, # Минимальный dropout
bias = "none",
use_gradient_checkpointing = True, # ВКЛЮЧАЕМ!
random_state = 42,
# Новый параметр для борьбы с переобучением:
loftq_config = None, # Но можно попробовать loftQ для квантования
)
Почему r=8, а не 16? При малом датасете высокий rank приводит к тому, что LoRA адаптеры начинают "запоминать" конкретные примеры вместо обучения общим паттернам. 8 - золотая середина для 4B модели.
Нюанс: В последних версиях Qwen (2026) появилась улучшенная архитектура внимания. Если используете Qwen2.5-Coder, проверьте документацию - иногда нужно добавлять "qkv_proj" вместо отдельных "q_proj", "k_proj", "v_proj".
3 Параметры обучения: ранняя остановка или смерть
Самая частая ошибка - обучать до сходимости loss. С малыми данными loss сходится быстро, а переобучение начинается позже.
| Параметр | Обычное значение | Для малого датасета | Почему |
|---|---|---|---|
| Learning rate | 2e-4 | 5e-5 | Меньше LR = плавнее обучение |
| Epochs | 10-20 | 3-5 | Больше эпох = гарантированное переобучение |
| Warmup steps | 100 | 20 | Быстрый разогрев для малых данных |
| Batch size | 8-16 | 2-4 | Меньше batch = больше обновлений градиента |
from transformers import TrainingArguments
from trl import SFTTrainer
from unsloth import is_bfloat16_supported
# Проверяем поддержку bfloat16 (новая фича 2026)
use_bf16 = is_bfloat16_supported()
training_args = TrainingArguments(
output_dir = "./qwen-ts-coder",
num_train_epochs = 4, # Всего 4 эпохи!
per_device_train_batch_size = 2, # Маленький batch
gradient_accumulation_steps = 4, # Но накапливаем градиенты
warmup_steps = 20,
logging_steps = 10,
save_steps = 50,
eval_steps = 50,
evaluation_strategy = "steps",
save_strategy = "steps",
load_best_model_at_end = True, # Критически важно!
metric_for_best_model = "eval_loss",
greater_is_better = False,
learning_rate = 5e-5, # Маленький LR
fp16 = not use_bf16,
bf16 = use_bf16,
optim = "paged_adamw_8bit",
weight_decay = 0.01, # Регуляризация!
lr_scheduler_type = "cosine", # Плавное уменьшение LR
seed = 42,
report_to = "none",
# Новый параметр для ранней остановки:
early_stopping_patience = 2, # Останавливаем после 2 ухудшений
early_stopping_threshold = 0.001,
)
Мониторинг переобучения в реальном времени
Loss - это обман. Он может уменьшаться, пока модель переобучается. Нужно смотреть на:
- Perplexity на валидации - если растет, а train perplexity падает, это переобучение
- Генерация примеров - каждые 50 шагов генерируйте код по тестовым промптам
- Cosine similarity скрытых состояний - если скрытые состояния примеров становятся слишком похожими, модель запоминает, а не обобщает
Вот скрипт для мониторинга:
import numpy as np
from sklearn.metrics.pairwise import cosine_similarity
def check_overfitting(model, tokenizer, examples, sample_size=10):
"""Проверяем, не начинают ли примеры становиться слишком похожими"""
model.eval()
similarities = []
# Берем случайные примеры
indices = np.random.choice(len(examples), sample_size, replace=False)
for i in indices:
example = examples[i]
inputs = tokenizer(
example["instruction"],
return_tensors="pt",
truncation=True,
max_length=512
).to(model.device)
with torch.no_grad():
outputs = model(**inputs, output_hidden_states=True)
# Берем последнее скрытое состояние
hidden = outputs.hidden_states[-1][:, -1, :].cpu().numpy()
# Сохраняем для сравнения
if len(similarities) > 0:
prev_hidden = similarities[-1]
sim = cosine_similarity(hidden, prev_hidden)[0][0]
if sim > 0.95: # Слишком высокая похожесть
print(f"ВНИМАНИЕ: Высокая similarity {sim:.3f}")
return True
similarities.append(hidden)
model.train()
return False
Аугментация данных: когда примеров катастрофически мало
Если у вас не 800, а 200 примеров, нужна аугментация. Для кода работают:
- Переименование переменных - меняем "userData" на "userInfo", "fetchData" на "getData"
- Изменение порядка функций - если в примере несколько функций, меняем их порядок
- Добавление/удаление комментариев
- Замена стрелочных функций на обычные (и наоборот)
Но осторожно! Слишком агрессивная аугментация учит модель генерировать бесполезный код.
Специфика TypeScript: что нужно знать
Qwen2.5-Coder-4B уже знает TypeScript, но ваш датасет может содержать нишевые паттерны:
| Паттерн | Как учить | Пример в датасете |
|---|---|---|
| Generic types | Давать разнообразные примеры | <T extends Record<string, any>> |
| Decorators | Явно показывать импорты | @Injectable(), @Controller() |
| Utility types | Объяснять в комментариях | Pick<User, 'id' | 'name'> |
Формат датасета должен быть последовательным:
{
"instruction": "Напиши функцию для валидации email на TypeScript",
"output": "function isValidEmail(email: string): boolean {\n const regex = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/;\n return regex.test(email);\n}"
}
Интеграция с существующим пайплайном
После обучения нужно правильно сохранить и загрузить модель:
# Сохраняем адаптеры LoRA
model.save_pretrained("./qwen-ts-lora")
tokenizer.save_pretrained("./qwen-ts-lora")
# Для загрузки в production:
from peft import PeftModel
base_model, tokenizer = FastLanguageModel.from_pretrained(
model_name="Qwen/Qwen2.5-Coder-4B-Instruct",
load_in_4bit=True,
)
model = PeftModel.from_pretrained(base_model, "./qwen-ts-lora")
model = model.merge_and_unload() # Объединяем адаптеры с базовой моделью
# Теперь модель автономна
model.save_pretrained("./qwen-ts-finetuned")
tokenizer.save_pretrained("./qwen-ts-finetuned")
Важно: После merge_and_unload() вы не можете дообучать модель с LoRA на тех же адаптерах. Сохраняйте отдельно объединенную версию для инференса и версию с адаптерами для возможного дообучения.
Чеклист перед запуском
- Датасет очищен от дубликатов (проверьте хэшами)
- Длина примеров от 50 до 2000 токенов
- LoRA rank = 8 (не больше!)
- target_modules включает только q_proj, v_proj, gate_proj, up_proj
- Learning rate = 5e-5
- Epochs = 4 максимум
- Batch size = 2 с gradient accumulation = 4
- Включен gradient checkpointing
- Настроена ранняя остановка (patience=2)
- Есть валидационный набор (20% от данных)
Когда все равно переобучается: экстренные меры
Если модель продолжает переобучаться даже с этими параметрами:
- Уменьшите rank до 4 - меньше параметров, меньше риск
- Увеличьте dropout до 0.1 - больше регуляризации
- Добавьте weight decay 0.1 - сильнее штрафуем большие веса
- Используйте QAT+LoRA гибрид - квантование помогает против переобучения
- Примените Sequential Fine-Tuning - учите постепенно
Самый радикальный, но работающий метод: учите 1 эпоху, оценивайте, если качество падает - останавливайтесь. Иногда одной эпохи достаточно для адаптации к нишевой задаче.
Итог: меньше - лучше
С малыми датасетами работает принцип "минимальной достаточной настройки". Не пытайтесь выжать из модели максимум - выжмете переобучение. Меньший rank, меньше эпох, меньший learning rate.
Qwen2.5-Coder-4B - мощная модель, которая уже знает TypeScript. Ваша задача не переучить ее, а слегка подкорректировать под ваш стиль или специфичные библиотеки. LoRA с rank=8 на 3-4 эпохах справится с этим. Если нет - проблема в данных, а не в параметрах.
И последнее: всегда тестируйте на реальных промптах, а не только на метриках. Модель может показывать идеальный eval_loss, но генерировать бесполезный код. Запускайте сгенерированный TypeScript через tsc, проверяйте типы. Только так поймете, работает ли fine-tuning.