Синтетические данные для LLM: как генерировать датасеты без людей
opensourceaillmsynthetic-datatrainingit
Введение: данные — новое топливо
Качество 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 | Возможны | Нет
Заключение
Синтетические данные — мощный инструмент для создания датасетов.
Ключевые выводы:
- Self-Instruct и Evol-Instruct — лучшие методы для инструкций
- Back-translation даёт естественные парафразы
- Quality control обязателен — без фильтра мусора
- Diversity check предотвращает однотипные данные
- Комбинация rule-based + LLM augmentation оптимальна
- Синтетические данные дополняют, а не заменяют реальные
Ресурсы: