Допустим, нам нужно решить задачу NLP, в которой мы принимаем и возвращаем текст (seq2seq). Существует великое множество таких задач: генерация текста/кода, перевод/стилизация, суммаризация, коррекция текста, распознавание именованных сущностей, даже классификацию текста можно свести к seq2seq. Если нам нужно очень быстро написать высококачественное решение и у нас есть возможность платить за API, тогда нам подойдет использование GPT-4 API. При решении seq2seq-задачи с использованием GPT-4 API нам нужно будет составить промт, который состоит из системного сообщения и набора примеров для модели (few-shot learning). Туториал по GPT-4 API можно найти в моей предыдущей статье. Далее возникает задача измерения качества модели и оптимизации промта: Непонятно, как изменение системного сообщения влияет на результат Неясно, как подбирать примеры Чем длиннее промт, тем дороже использование модели. Хочется найти минимальную длину промта, при которой качество нас устраивает  Вдобавок, можно использовать либо GPT-3.5, либо GPT-4 (если у вас есть к ней доступ). GPT-4 гораздо дороже. Хочется понять, даст ли GPT-4 достаточный буст к качеству, который оправдает ее цену. Я написал небольшой тул, который измеряет качество модели на различных промтах и позволяет выбрать оптимальный: https://github.com/einhornus/prompt_gpt. В этой статье я сначала расскажу, как он работает, а потом покажу процесс оптимизации промта на конкретном примере - на задаче коррекции грамматики из своей предыдущей статьи. В API есть метод ChatCompletion, который принимает список сообщений, который мы будем называть промтом, и возвращает следующее сообщение. Каждое сообщение этого списка представляет собой словарь с двумя полями: role и content. По значению role сообщения классифицируются на 3 типа: системные сообщения (role="system"), сообщения пользователя (role="user") и сообщения ассистента (role="assistant"). Системное сообщение содержит в своем поле content высокоуровневые инструкции для ассистента - то, как он должен себя вести. В системном сообщении мы пытаемся объяснить модели, что мы от нее хотим. Если в промте несколько системных сообщений, иметь значение будет только последнее из них (новые системные сообщения переписывают старые). Сообщения от пользователя и ассистента образуют предшествующий диалог между ними. Однако, эти сообщения могут быть использованы также для демонстрации примеров решения задач (метод обучения на основе небольшого количества примеров - few shot learning). В каждом сообщении пользователя содержится пример ввода данных, а в ответном сообщении ассистента - ожидаемый результат. После этих примеров идет сообщение от пользователя с данными, для которых требуется найти решение. Например, промт для перевода с английского на русский может выглядеть следующим образом: На практике выяснилось, что системное сообщение лучше добавлять в конец, перед последним сообщением от юзера. В этом случае модель уделяет системному сообщения наибольшее внимание. По этой же причине, более хорошие примеры тоже желательно ставить в конец.  Если послать API-запрос с таким промтом, в ответ мы получим сообщение ассистента, которое будет содержать перевод нашего текста. Таким образом, следующий код переводит текст с английского на русский: Полную справку по параметрам в API можно посмотреть здесь. Поиграться с API можно в плейграунде. У нас есть две выборки данных: обучающая и тестовая. Каждый элемент обеих выборок содержит два поля: input и output. Из элементов обучающей выборки мы формируем примеры для промта. Чтобы измерить качество модели на конкретном промте, мы запускаем модель на каждом элементе тестовой выборки, и находим метрику сходства между выводом модели и полем output. Анализируя распределение этой метрики (ее среднее, медиану и другие статистические параметры), мы можем выбрать оптимальный промт. Для экономии денег и времени мы кешируем результаты вызовов модели. Нам нужно варьировать системное сообщение: у нас в отдельной папке лежит набор возможных системных сообщений (каждое в своем файле), и мы можем запустить модель с любым из них. Формирование списка примеров происходит следующим образом: мы задаемся значением параметра k (максимальное суммарное количество символов в наборе примеров), и идем от начала обучающей выборки до ее конца: если текущий элемент возможно поместить в набор примеров, то добавляем его; иначе пропускаем. По завершении процедуры список примеров переворачивается, чтобы более хорошие примеры были расположены ближе к концу. Эффективность набора примеров часто зависит от того, в каком порядке элементы обучающей выборки были расставлены изначально (причем эта эффективность очень непредсказуема). Поэтому я решил переставлять элементы обучающей выборки n раз: в первый раз они расставлены в дефолтном порядке, а следующие n-1 раз они переставляются в случайном порядке (причем seed равен номеру итерации). Таким образом, возможно позапускать модель много раз при самых разных наборах примеров и таким образом найти оптимальный набор. У нас есть функция create_report, которая составляет .json-отчет о качестве модели. Она принимает следующие аргументы: project_name - название проекта model - gpt-3.5-turbo или gpt-4 system_message_file - имя файла, в котором находится системное сообщение parameters - о его смысле я расскажу чуть позже k - максимальное суммарное количество символов в наборе примеров n - количество наборов примеров (примеры рандомизируются каждый раз по-разному) metric - используемая метрика сходства между ожидаемым и реальным результатом. На данный момент поддерживается только метрика BLEU. Эта метрика - золотой стандарт при оценке качества машинного перевода. Есть планы в будущем добавить расстояние Левенштейна и косинусное сходство между различными эмбеддингами текста. Функция генерирует отчет в формате .json, в названии которого закодированы эти параметры, например: "system=1 language=English  model=gpt-3.5-turbo k=0 metric=bleu.json". В отчете содержатся результаты для самого лучшего из n запусков - того, где среднее значение метрики было самым высоким. Содержимое отчета: Параметры вызова функции create_report Среднее, медиана, перцентили и стандартное отклонение значений метрики Элементы тестовой выборки, сортированные по значению метрики (от лучших результатов к худшим); значение метрики также прилагается Промт в виде списка сообщений history - среднее значение метрики на каждом из n запусков Пример отчета можно посмотреть здесь. Для каждого элемента обучающей/тестовой выборки также хранится набор их "параметров". Комбинации значений параметров делят выборки на независимые подвыборки; все отчеты формируются на подвыборках с одинаковым значением всех параметров. Например, если нам нужно реализовать машинный перевод, тогда параметрами будут служить исходный и конечный языки, потому что для каждой языковой пары нам нужно брать отдельные наборы примеров. Каждый отчет будет относиться к конкретной языковой паре, задаваемой значениями двух параметров. При записи системного сообщения параметры можно обобщенно указывать значения параметров, например: "You translate messages from {source_language} to {destination_language}". Функция visualize визуализирует несколько отчетов в виде набора ящиков с усами. Ящики строятся с помощью boxplot из matplotlib, поэтому границы ящика представляют собой 25-й и 75-й перцентили, жирная линия в центре - медиана. Треугольный вырез в центре ящика называется "notch" и он показывает доверительный интервал для медианы. Функция visualize принимает следующие аргументы: project - название проекта system_message_files - массив возможных значений system_message_file models - массив возможных значений model parameters_configurations - массив возможных значений parameters ks - массив возможных значений k metric - используемая метрика Функция фильтрует отчеты: она визуализирует только те отчеты, все параметры которых попадают в соответствующие массивы. При визуализации в заголовке графика указаны значения параметров, которые одинаковы для всех визуализированных отчетов. Под каждым из ящиков записаны значения параметров, которые отличаются между отчетами. В корне data нужно создать папку с именем своего проекта, в ней будут храниться данные, относящиеся к этому проекту. Кроме того, в data есть БД llm_cache, с помощью которой кешируются запросы к LLM, а также пустой шаблон dataset - предполагается его наполнить своими данными и положить в корень своего проекта. В system лежат файлы, в которых прописываются различные вариации системного сообщения. Название сообщения - имя файла без .txt. В reports находятся сгенерированные отчеты. dataset.sqlite3 - база данных SQLite, у которой есть 2 таблицы: train и test. В train находятся элементы обучающей выборки, а в test - тестовой. У обеих таблиц одинаковая схема: id - первичный ключ. На первой итерации при генерации примеров элементы обучающей выборки располагаются по возрастанию id. Поэтому имеет смысл помещать самые лучшие примеры в начало выборки (однако при n>1 на следующих итерациях элементы рандомно перемешиваются и их исходный порядок не имеет значения) name - название элемента выборки. Логически оно ни на что не влияет parameters - значения параметров, разделенные переносом строки input - исходное сообщение, входные данные модели output - ожидаемый результат работы модели В src находится код: create_reports.py - скрипт для создания отчетов, содержит функцию create_report visualize.py - скрипт для визуализации, содержит функцию visualize llm_utils.py - тут находится код для работы с GPT API и кеширования metrics.py - код для расчета метрик сходства Зависимости проекта:  openai - для запросов к LLM nltk - для токенизации и расчета BLEU matplotlib - для визуализации numpy - для статистики Сначала нужно сгенерировать API-ключ на этой странице и поместить его в переменную окружения OPENAI_API_KEY. Раньше существовал сайт Lang8, на котором носители языка исправляли ошибки в текстах изучающих. Сейчас он уже не работает, но данные, собранные за многие годы, можно скачать. С обработкой этих данных пришлось повозиться, но в итоге у меня получился очень качественный датасет для решения моей задачи: для каждого из 6 языков (английский, русский, испанский, немецкий, французский, голландский) у меня есть от 30 до 90 примеров в тестовой выборке и вдвое меньше - в обучающей. В данном проекте у нас будет один параметр - language. Большинство измерений мы будем проводить на английском подмножестве нашего датасета {language=English} (если тесты проводить на всех языках, то они займут уж слишком много времени). Для каждого языка в данных Lang8 доступны тексты от носителей самых разных языков (но подавляющее большинство - от носителей японского, китайского и корейского). Поэтому при составлении датасета я решил сбалансировать родные языки авторов. Например, в английской части датасета есть по 15 текстов от носителей английского, русского, испанского, французского, немецкого, японского, китайского и корейского, плюс 15 текстов от носителей других языков. Поле name (которое логически ни на что не влияет) содержит язык текста и родной язык автора текста. Сначала подберем оптимальное системное сообщение. Составим 6 системных сообщений и положим их в папку correct_grammar/system: 1.txt, 2.txt, ..., 6.txt. Первое и второе сообщения - это то, что я использовал в своей предыдущей статье для двух разных типов коррекции: 1.txt: You're ImproveGPT. You improve the provided {language} text language-wise: you fix grammar and spelling mistakes and make it sound more natural. Your reply should contain ONLY the corrected text, nothing else. Please use exactly the same formatting as the original text. 2.txt: You're CorrectGPT. You fix grammar and spelling mistakes in {language} texts. Please only fix grammar and spelling mistakes in the given user message. Your reply should contain ONLY the corrected text, nothing else. Please use exactly the same formatting as the original text. Далее, в текстах 3 и 4 я решил добавить фразу "You're the best proofreading tool in the world." 3.txt: You're ImproveGPT. You're the best proofreading tool in the world. You improve the provided {language} text language-wise: you fix grammar and spelling mistakes and make it sound more natural. Your reply should contain ONLY the corrected text, nothing else. Please use exactly the same formatting as the original text. 4.txt: You're CorrectGPT. You're the best proofreading tool in the world. You fix grammar and spelling mistakes in {language} texts. Please only fix grammar and spelling mistakes in the given user message. Your reply should contain ONLY the corrected text, nothing else. Please use exactly the same formatting as the original text. В пятом сообщении я использовал второе сообщение в качестве основы и просто расписал его более подробно, добавив элементы из первого сообщения: 5.txt: You're CorrectGPT. You fix grammar and spelling mistakes in {language} texts. You're the best proofreader in the world.  You can fix any grammar and spelling mistakes in any text. The user supplies a text in {language} and you have to write a reply with the same text but with all grammar and spelling mistakes fixed. You're allowed to make slight changes to the text so it sounds more natural, but it should still have the same meaning as the original. Your reply should contain ONLY the corrected text, nothing else. Please use exactly the same formatting as the original text. В шестом варианте сообщения я, наоборот, сформулировал инструкцию максимально лаконично. 6.txt: You fix grammar and spelling mistakes in {language} texts. Составим отчет для каждого промта (при n=10 и двух значениях k - 0 (нет примеров) и 2000). Для этого запустим следующий код из файла create_reports.py Внимание: составление ответов - дело небыстрое. Нам нужно для каждого из 6-ти системных сообщений 11 раз пройтись GPT-3.5 по 90 элементам тестовой выборки. Это заняло всю ночь и скушало несколько долларов за использование API. Визуализируем результаты отдельно при k=0 и при k=2000 с помощью кода в visualize.py: При k=2000 все промты (за исключением последнего) показывают примерно одинаковые результаты, однако при k=0 разница видна очень явно. Из этого можно сделать вывод что в нашей задаче при отсутствии примеров критически важно правильно составить системное сообщение, а при их наличии это не так принципиально - модель по примерам сообразит, чего от нее хотят. В итоге я выбрал сообщение номер 4 - у него самые лучшие результаты при k=0, а при k=2000 у него наименьшее стандартное отклонение метрики. Теперь будем менять k (без изменения прочих параметров) Качество модели практически не меняется при увеличении количества примеров (но немного все-таки улучшается). Cгенерируем отчет по GPT-4: Визуализируем 3 ситуации: GPT-3.5 при k=0 и k=2000, и GPT-4 при k=0 Видно, что gpt-4 намного круче: gpt-4 без примеров бьет gpt-3.5 с примерами на 2000 символов. Теперь посмотрим результаты на других языках. На русском GPT работает хуже всего - вероятно, из-за кривой токенизации. Оптимально использовать следующее системное сообщение: You're CorrectGPT. You're the best proofreading tool in the world. You fix grammar and spelling mistakes in {language} texts. Please only fix grammar and spelling mistakes in the given user message. Your reply should contain ONLY the corrected text, nothing else. Please use exactly the same formatting as the original text.  Если у нас есть доступ к GPT-4, то наилучшее качество будет достигаться с ее помощью (можно даже не использовать примеры). При использовании GPT-3.5 у нас будет небольшой выигрыш в качестве, если мы будем использовать примеры. Оптимальный промт в таком случае мы возьмем из соответствующего файла вида "system=4 language=English  model=gpt-3.5-turbo k=<500, 1000, или 2000> metric=bleu.json". Например, при k=2000 оптимальный промт выглядит следующим образом:  Модель будет работать со сравнимым уровнем качества на испанских, французских, немецких и голландских текстах. При работе на русском языке качество результатов будет значительно хуже.  Data scientist