Tiny Universe, часть 5: TinyEval — своя система оценки LLM
Полный перевод раздела TinyEval из tiny-universe: двухэтапная оценка LLM (инференс → подсчёт метрик), промпты для датасетов LongBench, усечение длинного входа посередине, F1 по словам для генеративных ответов, точность для выбора и ROUGE для длинных ответов, а также свой датасет.
TinyEval
Заголовок раздела «TinyEval»Эфир «Система оценки LLM своими руками»: запись трансляции.
Сейчас я шаг за шагом проведу вас через реализацию простого фреймворка оценки LLM. Это двухэтапная система
оценки — мы называем её TinyEval. В ней есть ядро универсальной оценки LLM: поддерживаются генеративные,
дискриминативные задачи и задачи с выбором ответа. Фреймворк состоит из двух частей — inference и eval; его цель —
помочь лучше понять, как устроена и как реализуется оценка LLM.
1. Зачем этот проект?
Заголовок раздела «1. Зачем этот проект?»Только входите в мир LLM — и вас мучают похожие вопросы?
- У каждой модели свои метрики оценки, их целый зоопарк. Новичку непонятно, как в них разобраться?
- Не знаете, какую метрику (
metric) выбрать, и кромеrougeиbleuничего в голову не приходит? - Хотите, чтобы
LLMрешала тест с выбором ответа, а модель выдаёт кучу текста — как оценить умение выбирать? - Моделей множество, узкоспециальных задач тоже. Как, кроме
human_eval, дать убедительную количественную оценку качества на своей, нестандартной задаче?
Так вот, этот проект по очереди разрешит все эти сомнения!
2. Из каких этапов состоит оценка?
Заголовок раздела «2. Из каких этапов состоит оценка?»Сначала нужно понять базовый конвейер (pipeline) оценки. На рисунке ниже — упрощённая схема.
Рисунок в оригинале (Eval/docs/compass.png): схема процесса оценки в стиле OpenCompass — перерисована ниже.
flowchart LR D["Датасет и тип задачи"] --> M["Выбор метрики"] D --> P["Промпт-шаблон"] P --> I["Инференс модели<br/>(inference.py)"] I --> X["Извлечение ответа<br/>(очистка, правила)"] X --> S["Подсчёт метрики: pred vs answer<br/>(eval.py)"] M --> S
- Сначала по типу задачи целевого датасета выбираем подходящую метрику оценки.
- По форме данных составляем промпт (
prompt), который направляет модель. - По предварительным ответам модели выбираем разумный способ извлечь из них ответ.
- Считаем оценку для соответствующих пар
pred(предсказание) иanswer(эталон).
Вот это и есть все модули репозитория TinyEval.
3. Поддерживаемые датасеты и метрики
Заголовок раздела «3. Поддерживаемые датасеты и метрики»Используемые датасеты лежат здесь. Сейчас есть такие датасеты и типы задач (список будет пополняться!):
| Название | Тип | Метрика |
|---|---|---|
| multi_news | вопросы-ответы по длинному тексту | Rouge |
| multifieldqa_zh | вопросы-ответы с коротким ответом | F1 |
| trec | генеративный выбор (классификация) | accuracy |
Исследуйте задачи по своим нуждам, а дальше я шаг за шагом разберу с вами процесс оценки!
Как идёт оценка
Заголовок раздела «Как идёт оценка»Глядя на метрики выше, вы, возможно, удивились:
- Что?
F1же метрика классификации — как она попала вllm? - Для
accuracyразве не нужны метки классов (label)? Как она оказалась в генеративной задаче?
Хорошо, этот раздел как раз отвечает на эти вопросы. Кто уже знаком с основами, может сначала сам изучить соответствующий код.
1. F1 для генеративных ответов
Заголовок раздела «1. F1 для генеративных ответов»1.1 Инференс модели
Заголовок раздела «1.1 Инференс модели»- Сначала для датасета нужно составить направляющий промпт, то есть подтолкнуть LLM к ответу в нужном виде.
У большинства готовых датасетов промпты уже есть; для своего датасета можно придумать собственный. Например, для
multifieldqa_zhпромпт такой:
Прочитай текст ниже и кратко ответь по-китайски:\n\n{context}\n\nТеперь, опираясь на этот текст, ответь на вопрос ниже — скажи только ответ, не выводи никаких других слов.\n\nВопрос: {input}\nОтвет:(Перевод промпта: в оригинале он на китайском, как и сам датасет.)
-
Затем задаём длину входа модели — то есть сколько токенов подаётся в модель за раз. Ради качества её обычно ставят равной максимальной длине модели: её можно найти в файле
config.jsonскачанной модели в поле"max_position_embeddings", а можно не задавать — тогда возьмётся максимальная длина по умолчанию. В этом проекте выставлено 2048 — в основном для демонстрации. -
Затем создаём модель целиком. Я сделал для неё класс; по его образцу можно собрать любую другую модель:
class BaseLLM: def __init__(self, path: str, model_name: str) -> None: self.path = path self.model_name = model_name
def build_chat(self, tokenizer: str, prompt: str, model_name: str): pass
def load_model_and_tokenizer(self, path: str, model_name: str, device): pass
def post_process(self, response: str, model_name: str): pass
def get_pred(self, data: list, max_length: int, max_gen: int, prompt_format: str, device, out_path: str): pass- Что значат методы.
build_chatоборачивает запрос в «родной» формат диалога модели. Дляinternlm2это так:
def build_chat(self, prompt): prompt = f'<|im_start|>user\n{prompt}<|im_end|>\n<|im_start|>assistant\n' return prompt- Про модель и токенизатор говорить нечего; постобработку применяем или нет в зависимости от модели. Подробнее
остановимся на функции
get_pred:
def get_pred(self, data, max_length, max_gen, prompt_format, device, out_path): model, tokenizer = self.load_model_and_tokenizer(self.path, device) for json_obj in tqdm(data): prompt = prompt_format.format(**json_obj) # Усекаем посередине, потому что на обоих концах — ключевая информация. tokenized_prompt = tokenizer(prompt, truncation=False, return_tensors="pt").input_ids[0] if len(tokenized_prompt) > max_length: half = int(max_length/2) prompt = tokenizer.decode(tokenized_prompt[:half], skip_special_tokens=True)+tokenizer.decode(tokenized_prompt[-half:], skip_special_tokens=True)
prompt = self.build_chat(prompt)
input = tokenizer(prompt, truncation=False, return_tensors="pt").to(device) # длина поданных в модель токенов context_length = input.input_ids.shape[-1] eos_token_id = [tokenizer.eos_token_id, tokenizer.convert_tokens_to_ids(["<|im_end|>"])[0]]
output = model.generate( **input, max_new_tokens=max_gen, do_sample=False, temperature=1.0, eos_token_id=eos_token_id, )[0]
pred = tokenizer.decode(output[context_length:], skip_special_tokens=True) pred = self.post_process(pred)
with open(out_path, "a", encoding="utf-8") as f: json.dump({"pred": pred, "answers": json_obj["answers"], "all_classes": json_obj["all_classes"], "length": json_obj["length"]}, f, ensure_ascii=False) f.write('\n')- Кто-то спросит: зачем городить такую длинную функцию, разве нельзя просто вызвать
model.chat()? - Хорошо! Эта функция и есть ответ. Всё дело в стратегии усечения. Если модели задана длина входа, обычная команда усечения обрезает вход в конце. Но из-за направляющего промпта ключевая информация есть на обоих концах входа (инструкция в начале, вопрос в конце), поэтому нужно сохранить оба конца, а усекать середину — так качество ответа пострадает меньше всего!
Совет: часть с
get_predможно сравнить с функциейchatв скриптах модели (уinternlm2— вmodeling_internlm2.py) — так лучше видно, как сырой текст превращается во вход модели и как структурирован её выход.
1.2 Подсчёт результата
Заголовок раздела «1.2 Подсчёт результата»Сразу пример:
"pred": "57081.86 юаня", "answers": "57081.86 юаня жэньминьби."(В оригинале пример на китайском; мы перевели его на русский, числа те же.)
- Сначала чистим данные и делим фразы на слова. В китайском тексте между словами нет пробелов, поэтому оригинал
делит его библиотекой
jieba; русский текст достаточно разрезать по пробелам. Для нашего примера после деления на слова и удаления знаков препинания получаем:
"pred": ['5708186', 'юаня'], "answers": ['5708186', 'юаня', 'жэньминьби']Эти два «чистых» результата подаём в функцию F1:
def f1_score(prediction, ground_truth, **kwargs): # Counter хранит, сколько раз встречается каждое слово; оператор & возвращает общие элементы двух Counter common = Counter(prediction) & Counter(ground_truth) # число общих элементов предсказания и эталона num_same = sum(common.values()) if num_same == 0: return 0 # доля верных слов среди всех предсказанных precision = 1.0 * num_same / len(prediction) # доля верно предсказанных слов среди всех слов эталона recall = 1.0 * num_same / len(ground_truth) f1 = (2 * precision * recall) / (precision + recall) return f1- Сначала находим общие элементы двух списков, считаем их число и по определениям precision и recall вычисляем соответствующие оценки.
- Так получаем оценку для одного ответа; затем усредняем по всем ответам — это и есть
F1_scoreзадачи (task).
2. Над чем подумать
Заголовок раздела «2. Над чем подумать»Конечно, это лишь базовые метрики. Внимательный читатель, возможно, уже заметил дыры: например, здесь сравниваются результаты, уже извлечённые по правилам. Если эталон — «Сямэньский университет», а предсказание — «не Сямэньский университет» или сокращённое «Сяда», текущая метрика оценит их несправедливо.
Более точные метрики — цель, к которой академическое сообщество стремится постоянно. Проект будет следить за более совершенными стратегиями оценки; будем рады пул-реквестам от знатоков!
😆 Запуск
Заголовок раздела «😆 Запуск»1. Получить ответы модели
Заголовок раздела «1. Получить ответы модели»python inference.py2. Получить оценки
Заголовок раздела «2. Получить оценки»python eval.pyПоддерживаемые метрики
Заголовок раздела «Поддерживаемые метрики»- F1 score
- семейства rouge / bleu
- accuracy
Своя оценка
Заголовок раздела «Своя оценка»Репозиторий поддерживает и оценку на своих данных. Если у вас есть свои данные для SFT, назовите датасет
custom_zh (или custom_en, если он на английском). Формат данных — как у SFT:
{ "instruction": "Представь, что ты женщина при императоре — Чжэнь Хуань", "input": "Кто ты?", "output": "Ваша наложница — Чжэнь Хуань, мой отец — заместитель главы Далисы." # Далисы — Палата судебного надзора}Так поддерживается оценка на своём датасете.
Литература и благодарности
Заголовок раздела «Литература и благодарности»LongBench: A Bilingual, Multitask Benchmark for Long Context Understanding
От переводчика: что сделать руками
Заголовок раздела «От переводчика: что сделать руками»- Посчитайте вручную F1 для пары «Сямэньский университет» (эталон) / «не Сямэньский университет» (предсказание).
После деления по пробелам получится
['сямэньский', 'университет']и['не', 'сямэньский', 'университет']: precision , recall , — хотя ответ противоположен эталону. Это наглядная причина не доверять одной метрике. - Оцените одну и ту же маленькую модель (например, Qwen2.5-1.5B-Instruct) с
max_length= 2048 и 8192 наmultifieldqa_zh. Как длина контекста влияет на F1? - Сделайте свой
custom_zhили русскоязычный датасет из 20 вопросов по урокам курса и посчитайте F1 и accuracy.
Словарик
Заголовок раздела «Словарик»| Русский | Пояснение |
|---|---|
| оценка (evaluation) | измерение качества модели |
| метрика (metric) | число, которое считаем |
| инференс (inference) | получение ответов модели |
| генеративная / дискриминативная / с выбором | типы задач оценки |
| усечение (truncation) | обрезка слишком длинного входа |
| разбиение на слова (jieba) | в китайском нет пробелов между словами |
| точность / полнота (precision / recall) | доли верных среди предсказанных / эталонных |
| доля верных ответов (accuracy) | для задач с выбором |
| узкая (профильная) область (vertical domain) | медицина, право, своя задача |