Синтетические данные для LLM: как генерировать датасеты без людей

opensourceaillmsynthetic-datatrainingit
← Back to Blog

Введение: данные — новое топливо

Качество LLM определяется данными. Но реальные данные:

  • Дорогие в сборе и разметке
  • Содержат шум и ошибки
  • Имеют юридические ограничения
  • Не покрывают edge cases

Решение: генерировать данные с помощью LLM.

Традиционный подход:
  Люди → собирают данные → размечают → обучение
  Стоимость: $10-50 на пример
  Время: недели-месяцы

Синтетический подход:
  LLM → генерирует данные → фильтрация → обучение
  Стоимость: $0.01-0.10 на пример
  Время: часы-дни

Что такое синтетические данные?

Определение

Синтетические данные = данные, сгенерированные алгоритмически,
а не собранные из реального мира.

Для LLM это обычно:
- Текст, сгенерированный другой LLM
- Код, сгененрированный кодовой моделью
- Разметка, созданная автоматически
- Вариации реальных данных

Зачем они нужны?

Проблемы реальных данных:

1. Мало данных для задачи
   → Генерируем дополнительные примеры

2. Данные несбалансированы
   → Генерируем примеры для редких классов

3. Данные содержат PII (персональные данные)
   → Генерируем синтетические аналоги

4. Нужны edge cases
   → Генерируем граничные ситуации

5. Нужны данные на другом языке
   → Генерируем через перевод/адаптацию

Уровень 1: Простая генерация

Direct Generation

from openai import OpenAI

client = OpenAI()

def generate_classification_data(topic, n_examples=100):
    """Генерируем данные для классификации"""
    
    prompt = f"""
Сгенерируй {n_examples} примеров отзывов о {topic}.
Каждый отзыв должен иметь метку: positive, negative, или neutral.

Формат JSON:
[
  {{"text": "Отличный продукт!", "label": "positive"}},
  {{"text": "Ужасное качество", "label": "negative"}},
  ...
]

Важно:
- Отзывы должны быть разнообразными
- Разная длина (1-3 предложения)
- Разные формулировки
- Реалистичные примеры
"""
    
    response = client.chat.completions.create(
        model="gpt-4",
        messages=[{"role": "user", "content": prompt}],
        temperature=0.8
    )
    
    import json
    return json.loads(response.choices[0].message.content)

# Генерируем 1000 примеров за минуту
data = generate_classification_data("смартфоны", n_examples=1000)

Few-shot Guided Generation

def guided_generation(examples, n_more=500):
    """
    Генерируем данные, похожие на предоставленные примеры.
    """
    example_text = "\n\n".join(
        f'Текст: "{ex["text"]}"\nМетка: {ex["label"]}'
        for ex in examples[:10]
    )
    
    prompt = f"""
Вот примеры данных:
{example_text}

Сгенерируй ещё {n_more} примеров в том же стиле и формате.
Соблюдай ту же длину, тон и разнообразие.
"""
    
    return generate_data(prompt)

Уровень 2: Self-Instruct

Идея

Self-Instruct (Wang et al., 2022):

1. Берём seed-задачи (50 ручных примеров)
2. LLM генерирует вариации инструкций
3. LLM генерирует ответы на новые инструкции
4. Повторяем рекурсивно

Результат: тысячи задач из десятка seed-примеров.
def self_instruct(seed_tasks, expansion_factor=10):
    """
    Расширяем seed задачи через самоинструктирование.
    """
    all_tasks = list(seed_tasks)
    
    for generation in range(3):  # 3 итерации расширения
        new_tasks = []
        
        for task in all_tasks:
            # Генерируем вариации инструкции
            variations = generate_instruction_variations(
                task['instruction'],
                n_variations=expansion_factor
            )
            
            # Для каждой вариации генерируем ответ
            for variation in variations:
                output = generate_output(variation)
                new_tasks.append({
                    'instruction': variation,
                    'input': task.get('input', ''),
                    'output': output
                })
        
        all_tasks.extend(new_tasks)
        print(f"Generation {generation + 1}: {len(all_tasks)} задач")
    
    return all_tasks

def generate_instruction_variations(instruction, n_variations=10):
    """Генерируем вариации инструкции"""
    prompt = f"""
Инструкция: "{instruction}"

Сгенерируй {n_variations} вариаций этой инструкции.
Каждая вариация должна:
- Описывать ту же задачу
- Использовать другие слова
- Быть понятной и конкретной

Возвращай список строк.
"""
    return llm.generate(prompt)

Evol-Instruct

Evol-Instruct (Zhou et al., 2023):

Усовершенствованная версия Self-Instruct.
Генерирует БОЛЕЕ СЛОЖНЫЕ вариации задач.

Методы эволюции:
1. Role: добавить роль ("как юрист", "как ребёнок")
2. Constrain: добавить ограничения ("в 100 словах", "без запятых")
3. Exemplify: добавить примеры
4. Concretize: сделать конкретнее
5. Deduce: добавить рассуждения
6. Multi-constraint: комбинировать ограничения
def evol_instruct(task, evolution_type="constrain"):
    """Эволюционируем задачу, делая её сложнее"""
    
    evolution_prompts = {
        'role': f'Добавь роль к задаче: "{task}"',
        'constrain': f'Добавь ограничение к задаче: "{task}"',
        'exemplify': f'Добавь пример к задаче: "{task}"',
        'concretize': f'Сделай задачу более конкретной: "{task}"',
        'deduce': f'Добавь требование рассуждений: "{task}"',
        'multi-constraint': f'Добавь 2+ ограничений к задаче: "{task}"'
    }
    
    evolved = llm.generate(evolution_prompts[evolution_type])
    
    # Оцениваем сложность
    difficulty = estimate_difficulty(evolved)
    
    return evolved, difficulty

# Генерируем датасет с возрастающей сложностью
dataset = []
for seed in seed_tasks:
    for difficulty_target in [1, 2, 3, 4, 5]:
        task = seed
        while estimate_difficulty(task) < difficulty_target:
            task = evol_instruct(task, random_choice(evolution_types))
        dataset.append(task)

Уровень 3: Data Augmentation

Paraphrase Augmentation

def paraphrase_augment(dataset, n_paraphrases=3):
    """
    Создаём парафразы для каждого примера.
    Увеличиваем датасет в N раз.
    """
    augmented = []
    
    for example in dataset:
        # Оригинальный пример
        augmented.append(example)
        
        # Парафразы
        for _ in range(n_paraphrases):
            paraphrased = llm.generate(f"""
Перепиши текст другими словами, сохраняя смысл:
"{example['text']}"
""")
            augmented.append({
                'text': paraphrased,
                'label': example['label']  # Метка та же
            })
    
    return augmented

# Датасет 1000 → 4000 примеров

Back-Translation

def back_translate(text, via_language="German"):
    """
    Back-translation: EN → DE → EN
    Результат: естественный парафраз
    """
    # Переводим на промежуточный язык
    translated = llm.generate(f"Переведи на {via_language}: {text}")
    # Переводим обратно
    back_translated = llm.generate(f"Переведи на английский: {translated}")
    return back_translated

# Пример:
original = "The model performs well on this task"
back_translated = "The model does well with this task"
# Разные слова, тот же смысл → augmentation

Rule-Based Augmentation

import random

def rule_based_augment(example, rules=None):
    """
    Детерминированная augmentation без LLM.
    Быстро, бесплатно, предсказуемо.
    """
    if rules is None:
        rules = [
            # Удаление стоп-слов
            lambda text: ' '.join(
                w for w in text.split() 
                if w not in STOP_WORDS
            ),
            # Synonym replacement (через словарь)
            lambda text: replace_synonyms(text, synonym_dict),
            # Random insertion
            lambda text: insert_random_synonym(text),
            # Random swap
            lambda text: swap_random_words(text),
            # Random deletion
            lambda text: ' '.join(
                w for w in text.split() 
                if random.random() > 0.1
            ),
        ]
    
    augmented = [example]
    for rule in rules:
        new_text = rule(example['text'])
        if new_text != example['text']:  # Только если изменилось
            augmented.append({
                'text': new_text,
                'label': example['label']
            })
    
    return augmented

Уровень 4: Quality Control

Проблема

Синтетические данные могут быть:
1. Низкого качества (бессмысленные)
2. Однотипными (мало разнообразия)
3. С систематическими ошибками
4. Смещёнными (bias)

Без контроля → модель обучится на мусоре

LLM-as-Filter

def quality_filter(synthetic_data, min_score=0.7):
    """
    Используем LLM для оценки качества каждого примера.
    """
    filtered = []
    
    for example in synthetic_data:
        score = llm.generate(f"""
Оцени качество этого примера от 0 до 1:
"{example['text']}"

Критерии:
- Грамотность
- Смысл
- Реалистичность
- Соответствие метке: {example['label']}

Возвращай только число 0.0-1.0
""")
        
        if float(score) >= min_score:
            filtered.append(example)
    
    return filtered

# 1000 примеров → 700 качественных

Diversity Check

from sklearn.metrics.pairwise import cosine_similarity
import numpy as np

def check_diversity(data, embeddings, min_similarity=0.85):
    """
    Убираем слишком похожие примеры.
    """
    # Эмбеддим все тексты
    vectors = [embeddings.encode(text) for text in data]
    vectors = np.array(vectors)
    
    keep = [True] * len(data)
    
    for i in range(len(data)):
        for j in range(i + 1, len(data)):
            if keep[i] and keep[j]:
                sim = cosine_similarity(
                    vectors[i:i+1], vectors[j:j+1]
                )[0][0]
                
                if sim > min_similarity:
                    # Оставляем более качественный
                    if data[i]['quality'] < data[j]['quality']:
                        keep[i] = False
                    else:
                        keep[j] = False
    
    return [d for d, k in zip(data, keep) if k]

Distribution Matching

def match_distribution(synthetic, real, feature_fn):
    """
    Подстраиваем распределение синтетических данных
    под распределение реальных.
    """
    # Считаем распределение фич в реальных данных
    real_dist = compute_distribution(real, feature_fn)
    synth_dist = compute_distribution(synthetic, feature_fn)
    
    # Находим расхождения
    for category in real_dist:
        real_ratio = real_dist[category]
        synth_ratio = synth_dist.get(category, 0)
        
        if synth_ratio < real_ratio * 0.8:
            # Недостаточно примеров этой категории
            missing = int(len(synthetic) * (real_ratio - synth_ratio))
            extra = generate_for_category(category, missing)
            synthetic.extend(extra)
        elif synth_ratio > real_ratio * 1.2:
            # Слишком много — убираем
            synthetic = sample_proportionally(
                synthetic, category, real_ratio
            )
    
    return synthetic

Уровень 5: Продвинутые методы

Instruction Back-Translation

def instruction_backtranslate(output, n_variations=5):
    """
    Имея ответ, генерируем инструкции,
    которые привели бы к этому ответу.
    
    Reverse engineering задач из ответов.
    """
    prompt = f"""
Ответ: "{output}"

Сгенерируй {n_variations} разных инструкций/вопросов,
на которые этот ответ был бы правильным.

Инструкции должны быть:
- Разнообразными
- Конкретными
- Реалистичными
"""
    return llm.generate(prompt)

# Пример:
output = "42"
instructions = [
    "Сколько будет 6 × 7?",
    "Ответ на великую тайну вселенной?",
    "Сколько лет Артуру в начале книги?"
]

Self-Refine Data Generation

def self_refine_generation(initial_data, n_iterations=3):
    """
    Итеративно улучшаем сгенерированные данные.
    """
    current = initial_data
    
    for iteration in range(n_iterations):
        refined = []
        
        for example in current:
            # Kритикуем пример
            critique = llm.generate(f"""
Найди проблемы в этом примере:
Текст: "{example['text']}"
Метка: {example['label']}

Проблемы:
- Несоответствие метке
- Неестественный язык
- Слишком простой/сложный
""")
            
            # Улучшаем
            if critique.has_issues:
                improved = llm.generate(f"""
Исходный: "{example['text']}"
Проблемы: {critique}

Улучши пример, исправив все проблемы.
Сохрани метку: {example['label']}
""")
                refined.append(improved)
            else:
                refined.append(example)
        
        current = refined
    
    return current

Практика: полный пайплайн

import json

class SyntheticDataPipeline:
    def __init__(self, generator_llm, judge_llm):
        self.generator = generator_llm
        self.judge = judge_llm
    
    def run(self, seed_data, target_size=10000):
        # 1. Расширяем через self-instruct
        expanded = self.self_instruct(seed_data)
        print(f"1. Self-instruct: {len(expanded)} → {len(expanded * 10)}")
        
        # 2. Генерируем ответы
        with_responses = self.generate_responses(expanded)
        print(f"2. С ответами: {len(with_responses)}")
        
        # 3. Парафразная augmentation
        augmented = self.paraphrase_augment(with_responses, n=2)
        print(f"3. Augmented: {len(augmented)}")
        
        # 4. Фильтр качества
        quality = self.quality_filter(augmented, min_score=0.7)
        print(f"4. After quality filter: {len(quality)}")
        
        # 5. Убираем дубли
        diverse = self.remove_duplicates(quality)
        print(f"5. After dedup: {len(diverse)}")
        
        # 6. Балансировка
        balanced = self.balance_classes(diverse)
        print(f"6. Balanced: {len(balanced)}")
        
        # 7. Обрезаем до целевого размера
        final = balanced[:target_size]
        
        return final
    
    def save(self, data, path="synthetic_dataset.json"):
        with open(path, 'w') as f:
            json.dump(data, f, indent=2, ensure_ascii=False)

Сравнение: реальные vs синтетические

Критерий          | Реальные | Синтетические
------------------|----------|-------------
Стоимость         | $$$      | $
Скорость          | Недели   | Часы
Качество          | Высокое  | Зависит от пайплайна
Разнообразие      | Естественное | Контролируемое
Масштабируемость  | Низкая   | Очень высокая
Edge cases        | Редкие   | Можно генерировать
Bias              | Реальный | Можно контролировать
Legal issues      | Возможны | Нет

Заключение

Синтетические данные — мощный инструмент для создания датасетов.

Ключевые выводы:

  1. Self-Instruct и Evol-Instruct — лучшие методы для инструкций
  2. Back-translation даёт естественные парафразы
  3. Quality control обязателен — без фильтра мусора
  4. Diversity check предотвращает однотипные данные
  5. Комбинация rule-based + LLM augmentation оптимальна
  6. Синтетические данные дополняют, а не заменяют реальные

Ресурсы: