Fine-tuning Qwen 4B для генерации кода: LoRA конфигурация и борьба с переобучением | AiManual
AiManual Logo Ai / Manual.
22 Фев 2026 Гайд

Qwen 4B учится писать TypeScript: как настроить LoRA на 800 примерах и не сойти с ума

Пошаговое руководство по настройке Qwen 4B для генерации TypeScript кода на малом датасете. Конфигурация LoRA в Unsloth, target_modules, параметры против переоб

Почему все ломается на малых датасетах

Вы скачали 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 выключен. Результат - модель быстро переобучается.

💡
Для генерации кода наиболее важны q_proj и v_proj - они отвечают за внимание к контексту и генерацию значений. gate_proj и up_proj в FFN слоях тоже важны, но down_proj можно иногда исключить для уменьшения переобучения.

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 - это обман. Он может уменьшаться, пока модель переобучается. Нужно смотреть на:

  1. Perplexity на валидации - если растет, а train perplexity падает, это переобучение
  2. Генерация примеров - каждые 50 шагов генерируйте код по тестовым промптам
  3. 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 на тех же адаптерах. Сохраняйте отдельно объединенную версию для инференса и версию с адаптерами для возможного дообучения.

Чеклист перед запуском

  1. Датасет очищен от дубликатов (проверьте хэшами)
  2. Длина примеров от 50 до 2000 токенов
  3. LoRA rank = 8 (не больше!)
  4. target_modules включает только q_proj, v_proj, gate_proj, up_proj
  5. Learning rate = 5e-5
  6. Epochs = 4 максимум
  7. Batch size = 2 с gradient accumulation = 4
  8. Включен gradient checkpointing
  9. Настроена ранняя остановка (patience=2)
  10. Есть валидационный набор (20% от данных)

Когда все равно переобучается: экстренные меры

Если модель продолжает переобучаться даже с этими параметрами:

  1. Уменьшите rank до 4 - меньше параметров, меньше риск
  2. Увеличьте dropout до 0.1 - больше регуляризации
  3. Добавьте weight decay 0.1 - сильнее штрафуем большие веса
  4. Используйте QAT+LoRA гибрид - квантование помогает против переобучения
  5. Примените Sequential Fine-Tuning - учите постепенно

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

Итог: меньше - лучше

С малыми датасетами работает принцип "минимальной достаточной настройки". Не пытайтесь выжать из модели максимум - выжмете переобучение. Меньший rank, меньше эпох, меньший learning rate.

Qwen2.5-Coder-4B - мощная модель, которая уже знает TypeScript. Ваша задача не переучить ее, а слегка подкорректировать под ваш стиль или специфичные библиотеки. LoRA с rank=8 на 3-4 эпохах справится с этим. Если нет - проблема в данных, а не в параметрах.

И последнее: всегда тестируйте на реальных промптах, а не только на метриках. Модель может показывать идеальный eval_loss, но генерировать бесполезный код. Запускайте сгенерированный TypeScript через tsc, проверяйте типы. Только так поймете, работает ли fine-tuning.