Реализовать Evol-Instruct для instruction tuning

ТЕХНИЧЕСКОЕ ЗАДАНИЕ: Реализовать Evol-Instruct для instruction tuning

1. Цель задачи

Разработать и запустить пайплайн синтетической эволюции (Evol‑Instruct) на основе 1000 исходных инструкций для instruction tuning. Пайплайн должен последовательно применять five mutation rounds (эволюция вверх и вниз), генерируя релевантные ответы через LLM, и оценивать прирост разнообразия финального набора. Ключевой результат датасет из ~5000+ мутированных инструкций с ответами, разнообразие которого (измеренное через семантическое расстояние или n-gram novelty) увеличено не менее чем на 50% относительно исходного набора.

2. Исходные данные

Что нужноОткуда взять
1000 исходных инструкций для instruction tuningОткрытые наборы (например, databricks/databricks-dolly-15k — первые 1000 записей) или сгенерировать из Seed‑промптов через LLM
LLM для мутаций и генерации ответовДоступ через API (OpenAI, Anthropic) или локально (LLaMA, Mistral через Ollama/vLLM)
Инструмент для вычисления метрик разнообразияСкрипт на Python с sentence-transformers (all‑MiniLM‑L6‑v2) и scikit-learn (pairwise cosine distance)

Если нет реального инструмента — симулируем:

  1. Загрузите databricks/databricks-dolly-15k из Hugging Face (первые 1000 строк).
  2. Установите transformers, sentence-transformers, scikit-learn, tqdm.
  3. Для LLM-генерации используйте transformers с моделью microsoft/Phi-3-mini-4k-instruct (если нет GPU — выберите google/flan-t5-small для демонстрации).
  4. Все этапы пайплайна выполняйте в Jupyter Notebook или Python‑скрипте.

3. Технологический стек

КомпонентИнструментыНазначение
Загрузка данныхdatasets из Hugging FaceЗагрузка seed-датасета
Генерация мутацийtransformers + модель microsoft/Phi-3-mini-4k-instruct (или OpenAI API)Применение эволюционных операторов (deepening, widening, rewriting)
Генерация ответовТа же LLMСоздание релевантного ответа для каждой мутированной инструкции
Векторизация текстаsentence-transformers (all-MiniLM-L6-v2)Получение эмбеддингов для оценки разнообразия
Метрикиscikit-learn (pairwise cosine distance), numpyРасчёт среднего попарного расстояния и n‑gram diversity
ОркестрацияPython (функции + tqdm для прогресса)Управление процессом мутации в 5 раундов
Логированиеlogging / wandb (опционально)Отслеживание прогресса и метрик

4. Этапы выполнения

Этап 1: Подготовка seed-датасета и инструментов (1 час)

Действия

  1. Установить зависимости:

    pip install datasets transformers sentence-transformers scikit-learn tqdm
    
  2. Загрузить первые 1000 записей из databricks/databricks-dolly-15k:

    from datasets import load_dataset
    dataset = load_dataset("databricks/databricks-dolly-15k", split="train")
    seed = dataset.select(range(1000))
    seed_instructions = [item["instruction"] for item in seed]
    seed_responses = [item["response"] for item in seed]
    
  3. Реализовать базовые функции для генерации через LLM:

    from transformers import AutoModelForCausalLM, AutoTokenizer
    tokenizer = AutoTokenizer.from_pretrained("microsoft/Phi-3-mini-4k-instruct")
    model = AutoModelForCausalLM.from_pretrained("microsoft/Phi-3-mini-4k-instruct", device_map="auto")
    
    def generate_text(prompt, max_new_tokens=256):
        inputs = tokenizer(prompt, return_tensors="pt").to(model.device)
        outputs = model.generate(**inputs, max_new_tokens=max_new_tokens)
        return tokenizer.decode(outputs[0], skip_special_tokens=True)
    
  4. Определить список эволюционных операторов (Evol‑Instruct):

    • Deepening (добавить шаги, сделать более сложной)
    • Widening (расширить контекст, добавить альтернативные сценарии)
    • Rewriting (переформулировать без потери смысла)
    • Constraining (добавить ограничения)
    • Reasoning boost (потребовать объяснения)

    Каждый оператор — это промпт, который принимает исходную инструкцию и возвращает новую.

  5. Подготовить функцию для расчёта разнообразия:

    from sentence_transformers import SentenceTransformer
    from sklearn.metrics.pairwise import cosine_distances
    import numpy as np
    
    embedder = SentenceTransformer("all-MiniLM-L6-v2")
    def compute_diversity(texts):
        emb = embedder.encode(texts, show_progress_bar=False)
        return np.mean(cosine_distances(emb))
    

Ожидаемый результат этапа Загруженный seed-датасет, работающая генерация через LLM, заготовленные операторы мутации, функция метрики.

Этап 2: Однократная мутация (2 часа)

Действия

  1. Для каждой из 1000 инструкций случайно выбрать оператор (1 из 5) и применить его:
    import random
    def mutate(instruction, operator):
        prompt = f"Apply {operator} to the following instruction, output only the mutated instruction.\nInstruction: {instruction}\nMutated:"
        return generate_text(prompt, max_new_tokens=128).split("Mutated:")[-1].strip()
    
    operators = ["deepening", "widening", "rewriting", "constraining", "reasoning_boost"]
    mutated_round1 = []
    for instr in tqdm(seed_instructions):
        op = random.choice(operators)
        mutated = mutate(instr, op)
        mutated_round1.append(mutated)
    
  2. Сгенерировать ответ для каждой мутированной инструкции (используя ту же LLM):
    def generate_response(instruction):
        prompt = f"Instruction: {instruction}\nResponse:"
        return generate_text(prompt, max_new_tokens=200).split("Response:")[-1].strip()
    
    responses_round1 = [generate_response(instr) for instr in tqdm(mutated_round1)]
    
  3. Сохранить первую порцию мутированных пар (инструкция + ответ) в JSON:
    import json
    round1_data = [{"original": seed_instructions[i], "mutated": mutated_round1[i], "response": responses_round1[i]} for i in range(len(seed_instructions))]
    with open("round1.json", "w") as f:
        json.dump(round1_data, f)
    
  4. Вычислить разнообразие исходного набора и набора после первого раунда:
    div_original = compute_diversity(seed_instructions)
    div_round1 = compute_diversity(mutated_round1)
    print(f"Diversity original: {div_original:.4f}, round1: {div_round1:.4f}")
    

Ожидаемый результат этапа Файл round1.json с 1000 мутированных пар, метрики разнообразия для исходного набора и после раунда.

Этап 3: Многократная эволюция (3 часа)

Действия

  1. Реализовать цикл из 5 раундов, где на каждом раунде мутируется весь текущий набор инструкций (начиная с исходных, затем с результата предыдущего раунда):
    all_instructions = seed_instructions.copy()
    all_responses = seed_responses.copy()
    history = {"round0": {"instructions": all_instructions, "diversity": div_original}}
    
    for round_num in range(1, 6):
        new_instructions = []
        for instr in tqdm(all_instructions, desc=f"Round {round_num}"):
            op = random.choice(operators)
            mutated = mutate(instr, op)
            new_instructions.append(mutated)
        new_responses = [generate_response(instr) for instr in tqdm(new_instructions, desc=f"Response round {round_num}")]
        # Сохраняем раунд
        round_data = [{"original": all_instructions[i], "mutated": new_instructions[i], "response": new_responses[i]} for i in range(len(all_instructions))]
        with open(f"round{round_num}.json", "w") as f:
            json.dump(round_data, f)
        # Обновляем набор для следующего раунда (берём мутированные как новые seed)
        all_instructions = new_instructions
        all_responses = new_responses
        div = compute_diversity(all_instructions)
        history[f"round{round_num}"] = {"instructions": all_instructions, "diversity": div}
        print(f"Round {round_num} diversity: {div:.4f}")
    
  2. В конце собрать весь датасет (исходные + 5 раундов) в единый набор:
    final_instructions = seed_instructions[:]  # исходные
    final_responses = seed_responses[:]
    for round_num in range(1, 6):
        with open(f"round{round_num}.json", "r") as f:
            data = json.load(f)
        final_instructions.extend([item["mutated"] for item in data])
        final_responses.extend([item["response"] for item in data])
    print(f"Total samples: {len(final_instructions)}")
    
  3. Вычислить итоговое разнообразие:
    div_final = compute_diversity(final_instructions)
    improvement = (div_final - div_original) / div_original * 100
    print(f"Improvement: {improvement:.2f}%")
    

Ожидаемый результат этапа 6 JSON-файлов (round1..5), собранный final_dataset.json с ~6000 инструкций (1000 исходных + 5×1000 мутированных), метрика improvement.

Этап 4: Оценка качества и чистка (1.5 часа)

Действия

  1. Вычислить дополнительную метрику — n‑gram diversity (доля уникальных 4-грамм):
    def ngram_diversity(texts, n=4):
        all_ngrams = set()
        for t in texts:
            tokens = t.split()
            for i in range(len(tokens)-n+1):
                all_ngrams.add(" ".join(tokens[i:i+n]))
        return len(all_ngrams) / sum(len(t.split())-n+1 for t in texts)
    orig_ngram = ngram_diversity(seed_instructions)
    final_ngram = ngram_diversity(final_instructions)
    print(f"N‑gram diversity: original={orig_ngram:.4f}, final={final_ngram:.4f}")
    
  2. Проверить логическую релевантность ответов (sample 50 пар, прочитать вручную или с помощью LLM-as‑judge).
  3. Удалить явно нерелевантные мутации (если ответ пустой или не соответствует инструкции) — например, фильтр по длине ответа (>5 токенов) и по наличию ключевых слов инструкции.
  4. Составить итоговый отчёт в формате markdown с таблицей метрик по раундам.

Ожидаемый результат этапа Очищенный датасет final_clean.json, отчёт diversity_report.md.

Этап 5: Сборка финального артефакта (0.5 часа)

Действия

  1. Упаковать все результаты: папка evolved_dataset/ с JSON-файлами по раундам, финальным датасетом, отчётом и скриптами.
  2. Написать README.md с инструкцией по воспроизведению, версиями зависимостей и ключевыми метриками.
  3. Зафиксировать репозиторий (git commit) с тегом v1.0-evol-instruct.

Ожидаемый результат этапа Полностью документированный артефакт задания.

5. Критерии приемки (Definition of Done)

  • Исходный seed-датасет (1000 инструкций с ответами) загружен и подготовлен.
  • Реализованы 5 эволюционных операторов в виде промптов.
  • Выполнено 5 полных раундов мутаций (каждый раунд мутирует все инструкции предыдущего раунда).
  • Для каждой мутированной инструкции сгенерирован ответ.
  • Разнообразие финального датасета (всех инструкций вместе) улучшено на ≥50% относительно исходного набора (по косинусному расстоянию эмбеддингов).
  • Метрики n‑gram diversity также улучшены (документировано).
  • Датасет сохранён в формате JSON с полями instruction, response, source_round.
  • Проведена минимальная фильтрация некачественных пар (пустые ответы удалены).
  • Создан отчёт с графиком изменения разнообразия по раундам.

6. Ожидаемый результат

  • Основной артефакт файл final_clean.json, содержащий ~6000 пар инструкция-ответ (возможно меньше после фильтрации). Каждая запись: {"instruction": "...", "response": "...", "source_round": 0..5}.
  • Сопутствующие артефакты diversity_report.md с таблицей (round, diversity, ngram_diversity, % improvement), скрипты evol_instruct.py и evaluate_diversity.py, README.md.
  • Опционально график (PNG) изменения метрик, логи wandb (если настроен).

7. Возможные сложности и их решение

СложностьРешение
LLM генерирует невалидные мутации (пустые или копии)Добавить проверку: если мутация совпадает с оригиналом или пустая, повторить с другим оператором (max 3 попытки)
Высокое потребление ресурсов (GPU OOM)Использовать batch‑генерацию (если поддерживается) или уменьшить модель до google/flan-t5-small, либо использовать API
Падение разнообразия после нескольких раундов (сужение стиля)Добавить оператор "random injection" (вставка случайного токена из словаря) и увеличить долю widening-операторов
Некорректный расчёт метрики на большом наборе (6000 эмбеддингов)Вычислить среднюю попарную дистанцию на репрезентативной выборке (1000 случайных пар)
Ответы не соответствуют мутированным инструкциямПровести ручную валидацию на 50 примерах; при необходимости дообучить генерацию ответов с помощью few‑shot

8. Бюджет времени (оценка)

ЭтапВремя
Этап 1: Подготовка1 час
Этап 2: Однократная мутация2 часа
Этап 3: Многократная эволюция3 часа
Этап 4: Оценка и чистка1.5 часа
Этап 5: Сборка финального артефакта0.5 часа
Итого8 часов

Примечание при первом выполнении время может увеличиться до 10-12 часов из-за отладки промптов и настройки модели. Рекомендуется начать с flan-t5-small для быстрой итерации.

9. Связанные вопросы из базы знаний

ВопросТема
12What is the difference between supervised fine-tuning and instruction tuning?
45How to measure diversity in NLP datasets?
78What are the key components of Evol‑Instruct?
134How to generate synthetic data using LLMs?
205Techniques for data augmentation in text classification
310Evaluation metrics for instruction‑tuned models
422Handling low‑quality synthetic samples
567Sentence‑BERT embeddings for semantic similarity
689Prompt engineering for mutation operators
890Scaling synthetic data pipelines

10. Чек-лист самопроверки

  • Я загрузил и подготовил ровно 1000 seed-инструкций с ответами.
  • Каждая из 5 эволюционных операций реализована в отдельной функции с соответствующим промптом.
  • Я выполнил 5 итераций мутации, на каждой сохраняя промежуточный результат.
  • После всех итераций я объединил исходные и мутированные инструкции в один датасет.
  • Я измерил разнообразие исходного и финального датасета и убедился, что улучшение ≥50%.
  • Я отфильтровал пустые или явно некорректные пары (ответ короче 5 токенов).
  • Я написал краткий отчёт и зафиксировал код в git.