Реализовать 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) |
Если нет реального инструмента — симулируем:
- Загрузите
databricks/databricks-dolly-15kиз Hugging Face (первые 1000 строк). - Установите transformers, sentence-transformers, scikit-learn, tqdm.
- Для LLM-генерации используйте transformers с моделью microsoft/Phi-3-mini-4k-instruct (если нет GPU — выберите google/flan-t5-small для демонстрации).
- Все этапы пайплайна выполняйте в 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 час)
Действия
-
Установить зависимости:
pip install datasets transformers sentence-transformers scikit-learn tqdm -
Загрузить первые 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] -
Реализовать базовые функции для генерации через 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) -
Определить список эволюционных операторов (Evol‑Instruct):
- Deepening (добавить шаги, сделать более сложной)
- Widening (расширить контекст, добавить альтернативные сценарии)
- Rewriting (переформулировать без потери смысла)
- Constraining (добавить ограничения)
- Reasoning boost (потребовать объяснения)
Каждый оператор — это промпт, который принимает исходную инструкцию и возвращает новую.
-
Подготовить функцию для расчёта разнообразия:
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 часа)
Действия
- Для каждой из 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) - Сгенерировать ответ для каждой мутированной инструкции (используя ту же 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)] - Сохранить первую порцию мутированных пар (инструкция + ответ) в 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) - Вычислить разнообразие исходного набора и набора после первого раунда:
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 часа)
Действия
- Реализовать цикл из 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}") - В конце собрать весь датасет (исходные + 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)}") - Вычислить итоговое разнообразие:
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 часа)
Действия
- Вычислить дополнительную метрику — 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}") - Проверить логическую релевантность ответов (sample 50 пар, прочитать вручную или с помощью LLM-as‑judge).
- Удалить явно нерелевантные мутации (если ответ пустой или не соответствует инструкции) — например, фильтр по длине ответа (>5 токенов) и по наличию ключевых слов инструкции.
- Составить итоговый отчёт в формате markdown с таблицей метрик по раундам.
Ожидаемый результат этапа Очищенный датасет final_clean.json, отчёт diversity_report.md.
Этап 5: Сборка финального артефакта (0.5 часа)
Действия
- Упаковать все результаты: папка
evolved_dataset/с JSON-файлами по раундам, финальным датасетом, отчётом и скриптами. - Написать README.md с инструкцией по воспроизведению, версиями зависимостей и ключевыми метриками.
- Зафиксировать репозиторий (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. Связанные вопросы из базы знаний
| Вопрос | Тема |
|---|---|
| 12 | What is the difference between supervised fine-tuning and instruction tuning? |
| 45 | How to measure diversity in NLP datasets? |
| 78 | What are the key components of Evol‑Instruct? |
| 134 | How to generate synthetic data using LLMs? |
| 205 | Techniques for data augmentation in text classification |
| 310 | Evaluation metrics for instruction‑tuned models |
| 422 | Handling low‑quality synthetic samples |
| 567 | Sentence‑BERT embeddings for semantic similarity |
| 689 | Prompt engineering for mutation operators |
| 890 | Scaling synthetic data pipelines |
10. Чек-лист самопроверки
- Я загрузил и подготовил ровно 1000 seed-инструкций с ответами.
- Каждая из 5 эволюционных операций реализована в отдельной функции с соответствующим промптом.
- Я выполнил 5 итераций мутации, на каждой сохраняя промежуточный результат.
- После всех итераций я объединил исходные и мутированные инструкции в один датасет.
- Я измерил разнообразие исходного и финального датасета и убедился, что улучшение ≥50%.
- Я отфильтровал пустые или явно некорректные пары (ответ короче 5 токенов).
- Я написал краткий отчёт и зафиксировал код в git.