Книга: Анализ данных с LLM. Текст, таблицы, изображения и аудио
Назад: 8. Альтернативы GPT
Дальше: 10. Фреймворки

9. Оптимизация стоимости и качества

В этой главе

• Выбор и настройка модели.

• Промпт-инжиниринг.

• Дообучение (fine-tuning) моделей.

Анализ данных с помощью больших языковых моделей — отличный способ быстро потратить деньги. Если вы уже пользовались моделью GPT-4 (или аналогичной ей), то, вероятно, замечали, с какой скоростью растут траты, заставляя вас регулярно пополнять счет. Но всегда ли требуется самая большая (и дорогая) модель? Неужели нельзя заставить более мелкие модели работать почти так же эффективно? Как получить максимальную отдачу от вложенных средств?

В этой главе речь пойдет о том, как сэкономить деньги при использовании языковых моделей на больших наборах данных. К счастью, есть несколько вариантов.

• Во-первых, больших языковых моделей множество. Выбор модели, которая является настолько маленькой (или, скорее, настолько дешевой), насколько это возможно, и при этом все еще хорошо справляется с аналитической задачей, может иметь большое значение для балансировки бюджета.

• Во-вторых, у моделей обычно есть различные параметры настройки, позволяющие регулировать все: от общей стратегии генерации текста до способа (де)приоритизации конкретных токенов. Эти настройки нужно оптимизировать, чтобы превратить небольшие модели в альтернативы GPT-4 для определенных задач.

• В-третьих, можно использовать промпт-инжиниринг и изменить форму­лировку вопросов. Иногда это кардинально меняет результаты вывода модели!

• И наконец, если ни один из этих методов не подойдет, можно создать собственные узкоспециализированные модели, обученные исключительно для решения конкретной задачи. Разумеется, учитывая, что тратить миллионы на предварительное обучение не хочется, мы не станем начинать обучение новых моделей с нуля. Вместо этого мы, скорее всего, дообучим существующие модели на нескольких сотнях примеров. Этого часто бывает достаточно для значительного повышения производительности по сравнению с базовой моделью.

Безусловно, то, какой метод окажется наиболее эффективным, зависит от задачи, которую необходимо решить, а также от свойств данных. К счастью, при работе с крупными массивами данных логично потратить небольшую сумму на тестирование разных настроек модели на контрольной выборке. Велика вероятность, что эти первоначальные вложения окупятся по завершении анализа всего набора данных!

В этой главе мы применим вышеперечисленные варианты настройки в рамках примера.

9.1. Пример использования

Вы снова в компании Banana и вновь пытаетесь классифицировать отзывы пользователей. На сайте компании пользователи могут оставлять текстовые отзывы в свободном формате, описывающие их опыт использования товаров Banana. Вам нужно выяснить, являются ли эти отзывы положительными (то есть пользователь доволен продуктом) или отрицательными (то есть их чтение отпугнет потенциальных клиентов!). Конечно, для этой задачи можно использовать языковые модели (как вы видели в главе 4). Например, модель GPT-4 (на момент написания книги она была самой большой OpenAI-моделью для обработки текста). Предоставьте GPT-4 отзыв вместе с инструкциями по его классификации (включая описание возможных меток классов, таких как «положительный» и «отрицательный») — и для большинства отзывов вывод должен быть корректным.

Однако анализ данных с помощью модели GPT-4 стоит примерно 6 центов за 1000 токенов. Сумма (6 центов) может показаться небольшой, но компания Banana получает тысячи отзывов о продуктах ежедневно! Допустим, средний отзыв содержит около 100 токенов (примерно 400 символов). Далее предположим, что компания Banana получает примерно 10 000 отзывов в день. Это означает, что вы собираете 100 × 10 000 токенов в день = около 1 миллиона токенов в день и 365 миллионов токенов в год. В какую сумму обойдется анализ годового объема комментариев? Примерно 365 000 000 × 0,06 / 1000 = 21 900 долларов.

Это может ударить по бюджету! Нельзя ли уменьшить сумму? Например, на момент написания книги стоимость работы с моделью GPT-3.5 Turbo составляла всего 0,0005 доллара за тысячу токенов (токены оцениваются по-разному в зависимости от того, считывает их модель или генерирует, но сейчас мы этот нюанс опустим, чтобы упростить расчеты). Следовательно, анализ годового объема комментариев обойдется всего в 365 000 000 × 0,0005 / 1000 = 182,5 доллара. Намного лучше! Однако, возможно, придется приложить дополнительные усилия, чтобы обеспечить удовлетворительное качество вывода и гарантировать оптимальное использование модели.

СОВЕТ Вместо GPT-3.5 Turbo можно использовать и другие модели, такие как GPT-4o mini (идентификатор модели — gpt-4o-mini), из приведенных ниже примеров.

Именно этим мы и займемся в новом примере. Начав с самой наивной реализации классификатора, мы постепенно будем его совершенствовать и опробуем все варианты, обсуждавшиеся во введении к этой главе!

9.2. Классификатор без настройки

Начнем с базовой версии классификатора. Цель — загрузить в него отзыв и определить, как его классифицировать: как положительный или отрицательный. Для этого используем следующий шаблон промпта:

[Отзыв]

Отзыв положительный или отрицательный?

Ответ ("pos"/"neg"):

В этом шаблоне промпта [Отзыв] — это заполнитель, который заменяется фактическим текстом отзыва. Например, после подстановки промпт может выглядеть так (первые две строки соответствуют сокращенной версии отзыва для классификации, видимо, нового фильма, который показывают на Banana TV и который не понравился пользователю):

В научно-фантастическом фильме я готов закрыть глаза на многие огрехи, но это было уже почти невыносимо.

Отзыв положительный или отрицательный?

Ответ ("pos"/"neg"):

В идеале, отправив этот промпт модели GPT, мы ожидаем получить в качестве ответа вариант pos или neg (в данном случае — neg). Полный код Python показан в листинге 9.1.

Листинг 9.1. Классификация отзывов на положительные или отрицательные, базовая версия

import argparse

import openai

import pandas as pd

import time

 

client = openai.OpenAI()

 

def create_prompt(text):                       

    """ Создать промпт для классификации отзывов по тональности.

 

    Аргументы:

        text: текст для классификации.

 

    Выводит:

        Промпт для классификации текста.

    """

    task = Отзыв положительный или отрицательный?'

    answer_format = 'Ответ ("pos"/"neg")'

    return f'{text}\n{task}\n{answer_format}:'

 

def call_llm(prompt):                          

    """ Отправить запрос языковой модели и вывести ответ.

    Аргументы:

        prompt: введите запрос к языковой модели.

 

    Выводит:

        Ответ языковой модели и общее количество токенов.

    """

    for nr_retries in range(1, 4):

        try:

            

            response = client.chat.completions.create(

                model='gpt-3.5-turbo',

                messages=[

                    {'role':'user', 'content':prompt}

                    ],

                temperature=0

            )

            

            answer = response.choices[0].message.content

            nr_tokens = response.usage.total_tokens

            return answer, nr_tokens

 

        except Exception as e:

            print(f'Исключение: {e}')

            time.sleep(nr_retries * 2)

 

    raise Exception('Не удается выполнить запрос к модели OpenAI!')

 

if __name__ == '__main__':

 

    parser = argparse.ArgumentParser()   

    parser.add_argument('file_path', type=str, help='Путь к входному файлу')

    args = parser.parse_args()

 

    df = pd.read_csv(args.file_path)

 

    nr_correct = 0

    nr_tokens = 0

 

    for _, row in df.iterrows():         

 

        text = row['text']               

        prompt = create_prompt(text)

        label, current_tokens = call_llm(prompt)

 

        ground_truth = row['sentiment']  

 

        if label == ground_truth:

            nr_correct += 1

        nr_tokens += current_tokens

 

        print(f'Метка: {label}; Истинная метка: {ground_truth}')

 

    print(f'Количество правильных меток      :\t{nr_correct}')

    print(f'Количество использованных токенов:\t{nr_tokens}')

Этот код похож на код классификаторов, представленных в главе 4. Функция create_prompt создает экземпляр шаблона промпта для конкретного отзыва (хранящегося во входном параметре text). Результатом является промпт, который можно отправить языковой модели с помощью функции call_llm . Здесь мы вызываем языковую модель GPT-3.5 Turbo (экономя деньги). Кроме того, параметру temperature задано значение 0, а значит, случайность при генерации вывода минимизирована. Следовательно, при многократном запуске кода должен выводиться один и тот же результат. Вдобавок можно заметить, что вызов функции call_llm в листинге 9.1 немного длиннее, чем в версиях, рассмотренных в предыдущих листингах. Дело в том, что мы извлекаем не только сгенерированный языковой моделью ответ, но и количество использованных токенов . Подсчет количества токенов позволит определить затраты на вызов для выборки данных.

Предположим, что отзывы для классификации хранятся в файле формата .csv. Пользователь должен указать путь к этому файлу в качестве аргумента командной строки . После чтения .csv-файла отзывы просматриваются в том порядке, в котором они расположены в файле с входными данными. Для каждого отзыва извлекается связанный с ним текст (предполагается, что он находится в столбце text), создается промпт для классификации и отправляется вызов языковой модели. Результат — текст ответа, сгенерированный языковой моделью (будем надеяться, что это одна из двух меток классов — pos или neg), а также количество использованных токенов.

Наша цель — опробовать разные способы составления запросов к языковой модели, а затем сравнить качество результатов и затраты. Чтобы оценить качество вывода, предположим, что входной .csv-файл содержит не только текст отзыва, но и метку эталонных данных. Иными словами, каждый отзыв уже соотнесен с корректной меткой класса, хранящейся в столбце sentiment (поскольку наши два класса описывают тональность отзыва). Получив ответ языковой модели, мы сравниваем его с эталонной меткой и обновляем количество верно классифицированных отзывов (переменная nr_correct). Одновременно мы суммируем общее количество использованных токенов (так как стоимость обработки пропорциональна ему) и сохраняем в счетчике nr_tokens. Когда все отзывы будут обработаны, код, показанный в листинге 9.1, выводит итоговое количество корректных классификаций и количество использованных токенов.

9.3. Настройка модели

Код из листинга 9.1 доступен по ссылке Untuned Classifier, размещенной на сайте книги, в разделе дополнительных материалов к этой главе. Мы повторно используем отзывы о фильмах из главы 4 — найдите в дополнительных материалах к ней ссылку Reviews.csv и скачайте файл. Он содержит десять отзывов вместе с соответствующими истинными метками (ground-truth label). Предположим, что код из листинга 9.1 и файл с отзывами хранятся на диске в одной папке. Откройте терминал, перейдите в эту папку и выполните следующую команду:

python basic_classifier.py reviews.csv

Должен отобразиться вывод:

Метка: neg; Истинная метка: neg

Метка: neg; Истинная метка: neg

Метка: neg; Истинная метка: neg

Метка: neg; Истинная метка: neg

Метка: pos; Истинная метка: pos

Метка: pos; Истинная метка: neg       

Метка: pos; Истинная метка: neg

Метка: negative; Истинная метка: neg  

Метка: negative; Истинная метка: pos

Метка: neg; Истинная метка: neg

Количество правильных меток      :    6

Количество использованных токенов:    2228

Первые десять строк описывают результаты для каждого отзыва. У нас есть метка, сгенерированная языковой моделью, а также истинная метка (взятая из входных данных). В конце указано количество верно классифицированных отзывов и количество использованных токенов.

Из десяти отзывов правильно были классифицированы шесть. Это больше 50 %, но результат все равно не впечатляет. Что пошло не так? Изучив вывод, можно кое-что понять. Бывают случаи , когда языковая модель просто выбирает неверную метку класса. Подобное не является неожиданностью. Однако есть и ситуации , когда модель выбирает метку, которой даже не существует! Правда, она не так уж далека от истины (negative вместо neg), и это, похоже, легко исправить.

Мы сосредоточимся на (вероятно) самом простом и очевидном улучшении — заставим языковую модель генерировать только одну из двух возможных меток классов. Как это сделать? С помощью параметра logit_bias. Он позволяет пользователям изменять вероятность выбора определенных токенов (мы кратко обсуждали его и другие параметры моделей GPT в главе 3). В данном конкретном случае требуется существенно увеличить вероятность токенов, связанных с двумя метками классов (neg и pos). Параметр logit_bias задается в виде словаря Python, который сопоставляет идентификаторы токенов со значением смещения. Положительное смещение означает, что необходимо увеличить вероятность генерации соответствующего токена моделью. Отрицательное смещение означает, что вероятность генерации связанного токена нужно уменьшить.

В данном случае необходимо повысить шансы, что модель GPT-3.5 выберет один из двух токенов, представляющих метки классов. Поэтому мы задаем большое значение смещения для этих двух идентификаторов токенов. Значения смещения лежат в диапазоне от –100 до +100. Мы возьмем максимум и присвоим смещение +100 токенам, представляющим метки классов. Сначала нужно найти их идентификаторы. Языковые модели представляют текст в виде последовательности идентификаторов токенов. Чтобы изменить смещение токенов, требуется сослаться на идентификаторы интересующих нас токенов.

Токенизатор — это компонент, преобразующий текст в идентификаторы токенов. Токенизаторы для всех моделей GPT можно найти по адресу https://plat­form.openai.com/tokenizer. Мы используем модель GPT-3.5, поэтому выберите токенизатор с пометкой GPT-4&GPT-3.5 (legacy). На рис. 9.1 показан его веб-интерфейс.

Можно ввести текст в поле и нажать кнопку Token IDs (Идентификаторы токенов), чтобы увидеть соответствующие идентификаторы. Используя токенизатор, мы узнаем, что идентификатор токена pos981, а токена neg29875. Теперь мы готовы добавить смещение к вызову модели следующим образом:

import openai

client = openai.OpenAI()

 

response = client.chat.completions.create(

    model='gpt-3.5-turbo',

    messages=[

        {'role':'user', 'content':prompt}

        ],

    logit_bias = {981:100, 29875:100},

    temperature=0

    )

Рис. 9.1. Токенизатор GPT на сайте https://platform.openai.com/tokenizer: введите текст, чтобы узнать соответствующие идентификаторы токенов

По сравнению с предыдущим вызовом (см. листинг 9.1) мы добавляем смещение логитов , сопоставляя идентификаторы двух интересующих нас токенов (pos с ID 981 и neg с ID 29875) с максимально возможным значением смещения 100. Это должно решить проблему генерации токенов, не соответствующих меткам классов, не так ли?

ВНИМАНИЕ! Приведенный ниже код некорректен: он требует много времени на выполнение и серьезных финансовых затрат. Не запускайте его без исправления, представленного в конце этого раздела!

Итак, можно добавить смещение логитов к коду из листинга 9.1. Как вариант, далее в этой главе я представлю настраиваемую версию классификатора, которая позволит пробовать разные комбинации параметров настройки (включая смещение логитов). Запустив классификатор с добавленным смещением, скорее всего, вы увидите вывод, похожий на приведенный ниже (на самом деле, поскольку выполнение кода занимает много времени и требует немалых затрат, думаю, вы просто поверите мне на слово):

Метка: negnegnegnegnegnegnegnegnegneg ...; Истинная метка: neg

Метка: negposnegnegnegnegnegnegnegneg ...; Истинная метка: neg

Метка: negposnegnegnegnegnegnegnegneg ...; Истинная метка: neg

Метка: negposnegposnegnegnegnegnegneg ...; Истинная метка: neg

Метка: posnegposnegposnegposnegposneg ...; Истинная метка: pos

Метка: posnegpospospospospospospospos ...; Истинная метка: neg

Метка: posnegpospospospospospospospos ...; Истинная метка: neg

Метка: negposnegposnegposnegposnegpos ...; Истинная метка: neg

Метка: negposnegposnegposnegposnegpos ...; Истинная метка: pos

Метка: negposnegposnegnegnegnegnegneg ...; Истинная метка: neg

Количество правильных меток      :   0

Количество использованных токенов:   2318   

О нет — ни одной правильной классификации! Что произошло? Сравнение сгенерированных «меток» с эталонными выявляет проблему : мы генерируем только два возможных токена (это отлично!), но слишком много раз (а это уже хуже!). В результате увеличивается потребление токенов (заметьте, что длина вывода при генерации примера была ограничена; в противном случае потребление токенов было бы намного выше), но главная проблема в другом: вывод не соответствует ни одной из меток класса.

Почему модель генерирует так много токенов

По сути, мы ограничили модель генерацией текста с использованием только двух токенов. Именно их мы хотим видеть в выводе. Однако мы забыли разрешить модели генерировать токен, указывающий на конец вывода! Вот почему модель не может остановить генерацию.

Существует несколько способов решения этой проблемы. Конечно, можно добавить этап последующей обработки для извлечения только первого токена из вывода языковой модели. Это (в основном) решило бы проблему с метками классов. Изучив вывод, вы увидите, что использование первого токена приводит к верному результату в семи из десяти случаев. Однако такой подход чреват другой проблемой: мы платим за генерацию токенов, которые в итоге не используем! Нам он точно не подходит. Поэтому продолжим настраивать модель, ограничив также длину вывода. Нам нужен всего один токен (этот способ работает только потому, что две возможные метки классов могут быть представлены одним токеном). С этим справляется параметр max_tokens. Используем его при вызове языковой модели:

При тестировании (которое должно пройти быстро и стоить дешево) вы должны увидеть следующий вывод:

Метка: neg; Истинная метка: neg

Метка: neg; Истинная метка: neg

Метка: neg; Истинная метка: neg

Метка: neg; Истинная метка: neg

Метка: pos; Истинная метка: pos

Метка: pos; Истинная метка: neg

Метка: pos; Истинная метка: neg

Метка: neg; Истинная метка: neg

Метка: neg; Истинная метка: pos

Метка: neg; Истинная метка: neg

Количество правильных меток      :    7      

Количество использованных токенов:    2228   

Намного лучше! Количество корректно обработанных случаев выросло с шести (для версии без настройки) до семи . Возможно, результат не слишком впечатляет. Но в масштабах всего набора данных он фактически означает, что мы повысили точность с 60 до 70 %: то есть теперь тысячи дополнительных отзывов будут классифицированы правильно! Конечно, есть нюанс. В реальности, вероятно, следует использовать гораздо более крупную выборку. Из-за случайных колебаний показатели точности на тестовой выборке могут не отражать показатели точности на всем наборе данных. Чтобы не усложнять (и не тратить лишних денег при тестировании), мы ограничиваемся здесь десятью образцами. Дополнительный плюс — потребление токенов снова сократилось (на самом деле разрыв в потреблении токенов по сравнению с версией без ограничения размера вывода, вероятно, будет намного больше). Заметьте, что два рассмотренных здесь параметра — лишь малая часть доступных вариантов настройки. Более подробную информацию о соответствующих параметрах вы найдете в главе 3. Каждый раз, настраивая модель для выполнения новой задачи, обязательно учитывайте все параметры, которые могут оказаться потенциально важными. Затем поэкспериментируйте со значениями параметров на небольшой выборке данных и оцените, какой вариант дает наилучший результат.

9.4. Выбор модели

Предположим, что возможности повышения производительности путем настройки текущей модели исчерпаны. Что еще можно сделать? Конечно, можно выбрать другую модель. В предыдущей главе описывалось несколько альтернатив моделям GPT. Наличие модели, специально обученной для выполнения интересующей вас задачи (например, классификации текста), — повод обратить на нее внимание. К прочим факторам, способным повлиять на выбор модели, относятся конфиденциальность данных, которые планируется обрабатывать, и допустимость отправки этих данных конкретным поставщикам языковых моделей.

Получить информацию об относительной производительности разных моделей можно на сайте https://crfm.stanford.edu/helm/lite/latest/. Там содержатся результаты теста всесторонней оценки языковых моделей Стэнфордского университета — HELM. Тест сравнивает языковые модели по различным сценариям и предоставляет как результаты выполнения конкретных задач, так и данные о средней производительности, агрегированные из различных сценариев.

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

Для простоты возьмем только модель GPT-4 в качестве альтернативы модели GPT-3.5 Turbo (использовавшейся до сих пор). При вызове языковой модели замените ее название:

При выполнении получившегося кода должен появиться такой вывод:

Метка: neg; Истинная метка: neg

Метка: neg; Истинная метка: neg

Метка: neg; Истинная метка: neg

Метка: neg; Истинная метка: neg

Метка: pos; Истинная метка: pos

Метка: pos; Истинная метка: neg

Метка: pos; Истинная метка: neg

Метка: neg; Истинная метка: neg

Метка: pos; Истинная метка: pos              

Метка: neg; Истинная метка: neg

Количество правильных меток      :    8      

Количество использованных токенов:    2228   

По сравнению с предыдущей версией GPT-4 правильно решает на один тестовый пример больше ! Точность повышается до 80 % , в то время как потребление токенов остается неизменным . Это, кстати, не гарантируется при смене модели. Разные модели могут использовать разные токенизаторы, поэтому представление одного и того же текста может требовать разного количества токенов в зависимости от модели. В данном конкретном случае количество токенов не меняется, поскольку GPT-4 и GPT-3.5 используют один и тот же токенизатор.

Означает ли это, что сумма оплаты останется прежней? Не совсем. Поскольку модель GPT-4 взимает гораздо более высокую плату за токен, мы заплатим примерно в 120 раз больше, чем раньше (относительная разница между ценами за токен у моделей GPT-4 и GPT-3.5 Turbo). Именно поэтому мы постараемся добиться от модели GPT-3.5 максимально возможной производительности, чтобы не пришлось использовать GPT-4.

В некоторых случаях при выборе и настройке модели стоит самостоятельно просмотреть тестовые данные. Это позволит получить более полное представление о сильных сторонах и ограничениях различных моделей и оценить, являются ли репрезентативными тестовые случаи, на которых модель работает плохо. Напри­мер, модель GPT-4 обрабатывает этот отзыв корректно, а GPT-3.5 — нет:

Если вы хотели увидеть фильм со Стэном Лорелом из комедийного дуэта Лорела и Харди, то вы не угадали. Стэну потребуется еще два года, чтобы хотя бы приблизиться к характерной игре и ритму тех фильмов. Но если вы хотите посмотреть качественную пародию на "Кровь и песок" Рудольфа Валентино, снятый годом ранее, этот фильм для вас. Здесь использованы все возможные приемы, как в самой комедии, так и в титрах. И пусть фильм держится не на персонажах, зато в нем используется сюжет фильма Валентино — ну, почти.

В отзыве содержатся как положительные (к концу), так и отрицательные (в начале) аспекты. Хотя окончательный вердикт положительный, можно сделать вывод, что тратить дополнительные средства на тщательный анализ пограничных случаев, подобных этому отзыву, не стоит.

9.5. Промпт-инжиниринг

Помимо смены, что еще можно сделать для улучшения производительности модели? Мы пока не рассмотрели одну область — составление промпта, используемого для классификации. Изменение шаблона промпта может сильно повлиять на качество результата. Тот факт, что настройка промпта часто имеет решающее значение, даже привел к появлению специального термина промпт-инжиниринг, который описывает процесс поиска оптимальных шаблонов промптов. Более того, сложные задачи создания промптов стали причиной разработки множества платформ, предлагающих шаблоны промптов для огромного количества различных задач. Если у вас закончились идеи вариантов промптов, то посетите https://promptbase.com/, https://prompthero.com/ и другие подобные платформы. Бизнес-модель таких платформ заключается в предоставлении пользователям возможности покупать и продавать шаблоны промптов, оптимизирующие производительность определенных моделей для выполнения конкретных задач.

Чтобы понять, какой промпт наиболее эффективен, обычно приходится экспериментировать. Далее мы сосредоточимся на основах работы с промптами и рассмотрим классический метод повышения качества вывода за счет изменения промпта. Речь идет об обучении на небольшом количестве примеров: мы помогаем модели, предоставляя ей несколько образцов решения задачи. Концепция знакома нам из повседневной жизни: часто бывает трудно понять новую задачу или подход, имея лишь их описание. Разобраться в них гораздо проще, если есть несколько примеров. Так, в предыдущих разделах можно было бы просто обсудить семантику нескольких релевантных параметров настройки модели. Но разве не лучше было изучить их настройку на конкретном практическом примере?

Конечно, лучше. Языковые модели «чувствуют» то же самое, и добавление нескольких полезных примеров часто может повысить их производительность. Как же показать им примеры? Легко: указать их как часть промпта. Например, в сценарии классификации требуется, чтобы языковые модели классифицировали отзывы. Примером будет отзыв вместе с эталонной меткой класса.

Используем этот шаблон, чтобы интегрировать в промпт один пример:

[Пример отзыва]

Отзыв положительный или отрицательный?

Ответ ("pos"/"neg"):[Пример решения]

[Классификация отзывов]

Отзыв положительный или отрицательный?

Ответ ("pos"/"neg"):

Подставив вместо меток-заполнителей пример отзыва, эталонный ответ к нему и целевой отзыв для классификации, мы получим примерно такой промпт:

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

Безусловно, существует много вариантов добавления примеров в промпт. Мы выбрали, пожалуй, самое простое решение — использовать одну и ту же структуру промпта дважды для двух отзывов. Поскольку используемая структура не меняется, наш промпт избыточен: мы повторяем инструкцию к задаче ( и ), указывая две возможные метки классов. Хотя мы не будем этого делать здесь, было бы интересно поэкспериментировать и посмотреть, можно ли интегрировать примеры в промпт иначе, убрав избыточность и сократив его длину (тем самым уменьшив количество обрабатываемых токенов и в конечном счете стоимость обработки).

До сих пор мы рассматривали добавление только одного примера. Но порой его недостаточно. Поэтому целесообразно предоставить языковой модели несколько примеров. Предположим, они у нас есть: отзывы с относящимися к ним метками классов, которые хранятся в структуре данных под названием samples. Для генерации промптов, интегрирующих эти примеры, можно использовать такой код:

def create_single_text_prompt(text, label):   

    """ Создать промпт для классификации одного текста.

 

    Аргументы:

        text: текст для классификации.

        label: корректная метка класса (оставить пустым, если недоступна).

 

    Выводит:

        Промпт для классификации текста.

    """

    task = 'Отзыв положительный или отрицательный?'

    answer_format = 'Ответ ("pos"/"neg")'

    return f'{text}\n{task}\n{answer_format}:{label}'

 

def create_prompt(text, samples):             

    """ Генерирует промпт для классификации отзывов по тональности.

 

    Аргументы:

        text: классифицировать этот текст.

        samples: добавить эти образцы в промпт.

 

    Выводит:

        Ввод данных для LLM.

    """

    parts = []

    for _, row in samples.iterrows():             

        sample_text = row['text']

        sample_label = row['sentiment']

        prompt = create_single_text_prompt(sample_text, sample_label)

        parts += [prompt]

 

    prompt = create_single_text_prompt(text, ")   

    parts += [prompt]

    return '\n'.join(parts)

Функция create_single_text_prompt создает экземпляр следующего шаблона для одного отзыва:

[Отзыв]

Отзыв положительный или отрицательный?

Ответ ("pos"/"neg"):[Метка]

Мы используем одну и ту же функцию, чтобы указать как примеры отзывов, так и сам отзыв вместе с задачей классификации, которую модель должна решить за нас. Если пример отзыва указан, то метка-заполнитель [Метка] заменяется эталонной меткой класса для соответствующего отзыва. Обозначая задачу, которую должна решить языковая модель, мы еще не знаем верную метку класса. В этом случае мы заменяем заполнитель [Метка] пустой строкой. Задача языковой модели — дополнить промпт фактической меткой класса.

Функция create_prompt генерирует полный промпт, содержащий все примеры отзывов, а также отзыв, который требуется классифицировать. Сначала она перебирает примеры отзывов. Мы предполагаем, что структура samples хранит текст отзыва в столбце text, а связанные метки классов — в столбце sentiment. Мы добавляем часть промпта для примера отзыва, используя функцию create_single_text_prompt (обсуждавшуюся ранее). Наконец, мы добавляем инструкции для классификации интересующего нас отзыва.

Теперь вернемся к работе с моделью GPT-3.5 Turbo. Однако на этот раз применим новую функцию генерации промптов. Пока ограничимся в промпте одним примером отзыва. По ссылке Reviews Training (см. сайт книги, раздел дополнительных материалов к этой главе) доступны обучающие отзывы с корректными метками классов (файл train_reviews.csv). Отзывы в этом файле не совпадают с отзывами в файле reviews.csv (с помощью которого мы тестируем свой подход). После того как в промпты будет добавлен в качестве примера только первый отзыв из файла train_reviews.csv, должен отобразиться следующий вывод:

Метка: neg; Истинная метка: neg

Метка: neg; Истинная метка: neg

Метка: neg; Истинная метка: neg

Метка: neg; Истинная метка: neg

Метка: pos; Истинная метка: pos

Метка: pos; Истинная метка: neg

Метка: pos; Истинная метка: neg

Метка: neg; Истинная метка: neg

Метка: pos; Истинная метка: pos

Метка: neg; Истинная метка: neg

Количество правильных меток      :  8    

Количество использованных токенов:  4078

Ура! Мы повысили точность до 80 % . Именно такой показатель был получен при использовании модели GPT-4 с исходными промптами (без примеров отзывов). В то же время использование токенов увеличилось . А точнее, поскольку мы добавляем к каждому промпту второй отзыв (то есть у нас есть один пример отзыва и отзыв для классификации), то потребление токенов выросло почти вдвое по сравнению с последней версией. Но если сравнивать с использованием модели GPT-4 на более коротких промптах, то наш текущий подход все еще примерно в 60 раз дешевле (поскольку использование модели GPT-4 приблизительно в 120 раз дороже, чем GPT-3.5 Turbo).

9.6. Настраиваемый классификатор

Теперь, после изучения нескольких вариантов настройки, у вас может возникнуть соблазн попробовать новые комбинации. Например, нужно ли по-прежнему добавлять смещение (по сути, ограничивая вывод двумя возможными метками классов), если добавлены примеры? Можно ли добиться большей точности, используя в промпте для более крупной модели несколько примеров? Менять код для проверки новых сочетаний быстро надоедает. Но не волнуйтесь, есть вариант и на такой случай. Он показан в листинг 9.2 (код доступен по ссылке Tunable Classifier, размещенной на сайте книги, в разделе дополнительных материалов к этой главе).

Листинг 9.2. Настраиваемая версия классификатора тональности  отзывов

import argparse

import openai

import pandas as pd

import time

 

client = openai.OpenAI()

 

def create_single_text_prompt(text, label):

    """ Создать промпт для классификации одного текста

 

    Аргументы:

        text: текст для классификации.

        label: корректная метка класса (оставить пустым, если недоступна).

 

    Выводит:

        Промпт для классификации текста.

    """

    task = 'Отзыв положительный или отрицательный?'

    answer_format = 'Ответ ("pos"/"neg")'

    return f'{text}\n{task}\n{answer_format}:{label}'

 

def create_prompt(text, samples):        

    """ Генерирует промпт для классификации отзывов по тональности.

 

    Аргументы:

        text: классифицировать этот текст.

        samples: добавить эти образцы в промпт.

 

    Выводит:

        Ввод данных для LLM.

    """

    parts = []

    for _, row in samples.iterrows():

        sample_text = row['text']

        sample_label = row['sentiment']

        prompt = create_single_text_prompt(sample_text, sample_label)

        parts += [prompt]

 

    prompt = create_single_text_prompt(text, ")

    parts += [prompt]

    return '\n'.join(parts)

 

def call_llm(prompt, model, max_tokens, out_tokens):

    """ Отправить запрос большой языковой модели и вывести ответ.

 

    Аргументы:

        prompt: введите запрос к языковой модели.

        model: назовите, какую модель OpenAI выбрать.

        max_tokens: максимальная длина вывода в токенах.

        out_tokens: укажите приоритет ID этих токенов в выводе.

 

    Выводит:

        В ответе укажи языковую модель и общее количество токенов.

    """

    optional_parameters = {}

    if max_tokens:

        optional_parameters['max_tokens'] = max_tokens

    if out_tokens:

        logit_bias = {int(tid):100 for tid in out_tokens.split(',')}

        optional_parameters['logit_bias'] = logit_bias

 

    for nr_retries in range(1, 4):

        try:

            response = client.chat.completions.create(

                model=model,

                messages=[

                    {'role':'user', 'content':prompt}

                    ],

                **optional_parameters, temperature=0

                )

 

            answer = response.choices[0].message.content

            nr_tokens = response.usage.total_tokens

            return answer, nr_tokens

 

        except Exception as e:

            print(f'Исключение: {e}')

            time.sleep(nr_retries * 2)

    raise Exception('Не удается выполнить запрос к модели OpenAI!')

 

if __name__ == '__main__':

 

    parser = argparse.ArgumentParser()   

    parser.add_argument('file_path', type=str, help='Путь к входному файлу')

    parser.add_argument('model', type=str, help='Название модели OpenAI')

    parser.add_argument('max_tokens', type=int, help='Максимальный размер вывода')

    parser.add_argument('out_tokens', type=str, help='Токены, которым отдать приоритет')

    parser.add_argument('nr_samples', type=int, help='Количество примеров')

    parser.add_argument('sample_path', type=str, help='Путь к примерам')

    args = parser.parse_args()

 

    df = pd.read_csv(args.file_path)

 

    samples = pd.DataFrame()             

    if args.nr_samples:

        samples = pd.read_csv(args.sample_path)

        samples = samples[:args.nr_samples]

 

    nr_correct = 0

    nr_tokens = 0

 

    for _, row in df.iterrows():

 

        text = row['text']               

        prompt = create_prompt(text, samples)

        label, current_tokens = call_llm(

            prompt, args.model,

            args.max_tokens,

            args.out_tokens)

 

        ground_truth = row['sentiment']  

        if label == ground_truth:

            nr_correct += 1

        nr_tokens += current_tokens

 

        print(f'Метка: {label}; Истинная метка: {ground_truth}')

 

    

    print(f'Количество правильных меток:\t{nr_correct}')

    print(f'Количество использованных токенов   :\t{nr_tokens}')

Эта реализация позволяет опробовать все варианты настройки, задав корректные параметры командной строки.

Генерация промптов работает так, как описано в предыдущем разделе. Функция create_prompt в качестве входных данных принимает текст отзыва для классификации и примеры отзывов. Примеры добавляются в промпт, потенциально помогая языковым моделям классифицировать интересующий нас отзыв. Обратите внимание, что можно по-прежнему посмотреть, как работает языковая модель без примеров (не указав их). Классификация без примеров — это частный случай.

Функция call_llm объединяет все упомянутые ранее параметры настройки.

• Во-первых, это имя вызываемой модели (параметр model).

• Во-вторых, можно указать максимальное количество токенов вывода (max_tokens).

• Наконец, можно указать смещение: токены, которым следует отдать приоритет при генерации вывода. Параметр out_tokens позволяет пользователям задавать список идентификаторов токенов, разделенных запятыми, которым мы присваиваем высокий приоритет (по сути, ограничивая вывод одним из этих токенов). Имя модели указывать обязательно, но, задав значение 0 для параметра max_tokens и пустую строку для параметра out_tokens, можно будет не менять заданные по умолчанию настройки OpenAI.

Настраиваемый классификатор использует довольно много параметров командной строки . Рассмотрим параметры в том порядке, в котором их нужно указывать:

file_path — путь к файлу .csv, содержащему отзывы для оценки языковой моделью;

• model — имя языковой модели, которую следует использовать (например, gpt-3.5-turbo);

• max_tokens — максимальное количество выходных токенов для генерации в ответе на один отзыв;

• out_tokens — список токенов, разделенных запятыми, для приоритизации при генерации вывода;

• nr_samples — количество примеров отзывов с ответами, которое будет добавлено в каждый промпт;

sample_path — путь к файлу в формате .csv, содержащему отзывы с корректными метками классов для использования в качестве примеров (может быть пустым, если для параметра nr_samples задано значение 0).

ПРЕДУПРЕЖДЕНИЕ! Почти всегда рекомендуется ограничивать количество токенов вывода. В частности, это необходимо делать всякий раз, если вы задаете смещение в сторону определенных токенов и не добавляете при этом стоп-токены (указывающие на конец вывода).

После обработки входных параметров классификатор считывает примеры с диска и классифицирует отзывы , одновременно обновляя счетчики , данные которых в итоге выводит .

Посмотрим, как можно воспроизвести все версии классификатора, которые мы обсуждали до сих пор. Введя указанную ниже команду, вы должны получить версию классификатора без настроек, при условии, что файл reviews.csv находится в том же каталоге, что и сам код:

python tunable_classifier.py reviews.csv

    gpt-3.5-turbo 0 "" 0 ""

Обратите внимание, что мы не указываем токены, которым требуется отдать прио­ритет (оставляем пустую строку), не ограничиваем длину вывода (заданное значение 0 говорит об отсутствии ограничений) и задаем количество примеров в промпте равным 0 (следовательно, путь к файлу с примерами можно не прописывать).

Приведенная ниже команда, напротив, создаст версию, которая ограничит длину вывода, одновременно приоритизируя токены, соответствующие нашим меткам классов:

python tunable_classifier.py reviews.csv

    gpt-3.5-turbo 1 "981,29875" 0 ""

Наконец, последний вариант, который мы обсуждали, — с одним примером в каждом промпте и теми же настройками модели — можно запустить с помощью следующей команды (при условии, что файл train_reviews.csv находится в том же репозитории, что и код):

python tunable_classifier.py reviews.csv

    gpt-3.5-turbo 1 "981,29875" 1 "train_reviews.csv"

Смело тестируйте новые комбинации, которые мы не рассматривали!

9.7. Дообучение, или тонкая настройка

До сих пор мы делали все возможное, чтобы добиться максимальной производительности существующих моделей. Эти модели обучались для решения задач, которые, возможно, похожи на нашу, но не точно такие же. Правда, было бы чудесно получить модель, настроенную специально для решения нашей задачи? Это возможно, если применить дообучение. Посмотрим, как реализовать до­обучение на практике с моделями OpenAI.

Дообучение означает, что мы берем существующую модель, например GPT-3.5 Turbo от OpenAI, и специализируем ее для решения интересующей нас задачи. Конечно, можно было бы обучить модель с нуля. Но обычно это непомерно дорого, к тому же зачастую сложно найти объем целевых данных, необходимый для обучения большой модели. Поэтому гораздо эффективнее использовать дообучение.

Обычно дообучение — последнее, что стоит пробовать при максимизации производительности для конкретной задачи. Причина в том, что дообучение требует определенных первоначальных вложений времени и денег. Во время дообучения мы платим компании OpenAI за создание версии одной из ее базовых моделей, настроенной специально для решения нашей задачи. Цена зависит от размера обучающих данных и количества раз, которое эти данные считываются (то есть количества эпох). Например, на момент написания книги дообучение модели GPT-3.5 Turbo стоило около 0,8 цента за 1000 токенов обучающих данных за эпоху. Кроме того, после дообучения мы платим за использование дообученной модели. Цена за ее токен выше, чем в базовой версии. Это логично: ведь, по крайней мере теоретически, дообученная модель должна эффективнее решать нашу конкретную задачу.

Одно из возможных преимуществ дообучения — повышение точности вывода модели. Другой вероятный плюс заключается в возможности сократить длину промпта. При использовании универсальной модели в промпт необходимо добавить описание задачи, которую нужно выполнить (вместе со всеми релевантными данными). Дообученная модель, напротив, должна быть специализирована для выполнения одной задачи и хорошо с ней справляться. Если модель предназначена для решения лишь одной задачи, то ее описание можно вообще исключить из промпта, поскольку оно подразумевается. Помимо описания задачи, можно опустить и другую информацию, полезную для универсальной модели, но не являющуюся необходимой для специализированной. Например, в промпт для универсальной модели может потребоваться добавить примеры, которые позволят получить вывод надлежащего качества, а при работе с до­обученной версией это делать не обязательно.

В нашем конкретном сценарии задача состоит в том, чтобы сопоставлять отзывы с меткой класса, отражающей их тональность (отношение автора отзыва). Ранее мы прописывали задачу классификации в промпте (и даже предоставляли несколько полезных примеров). Сейчас при дообучении модели, полагаю, эти инструкции можно опустить. Точнее, теперь мы можем обойтись без промптов наподобие этого (содержит примеры отзывов с инструкциями и примерами ответов , а также отзыв для классификации и соответствующие инструкции ):

Вместо этого предположим, что модель уже знает, что она должна классифи­цировать отзывы и какие метки классов доступны. Тогда промпт можно упростить:

В научно-фантастическом фильме я готов закрыть глаза на многие огрехи, но это было уже почти невыносимо....

В этом промпте содержится только отзыв, который необходимо классифицировать. Мы предполагаем, что вся другая информация, характерная для задачи (например, инструкции и примеры), уже известна модели по умолчанию. Как вы, конечно, заметили, этот промпт намного короче предыдущего. Следовательно, мы можем сэкономить деньги, используя вместо базовой модели дообученную. Однако помните, что при использовании дообученной модели стоимость токена выше, чем в базовой версии. Соответствующие расчеты мы произведем позже, а пока посмотрим, удастся ли вообще, используя дообучение, заставить такие лаконичные промпты работать на практике.

9.8. Генерация обучающих данных

Сначала необходимо сгенерировать обучающие данные для дообучения. Для этого мы используем отзывы со связанными метками классов, содержащиеся в файле train_reviews.csv, доступном по ссылке Review Training, размещенной на сайте книги, в разделе дополнительных материалов к этой главе. Компания OpenAI требует, чтобы обучающие данные для дообучения предоставлялись в специальном формате. Прежде чем приступать к дообучению, придется преобразовать наши данные из формата .csv в требуемый формат.

Обучающие данные для дообучения чат-моделей OpenAI обычно представлены в виде успешных взаимодействий с моделью (то есть примеров, когда она выдает вывод, который мы в идеале хотим получить). В случае чат-моделей OpenAI такие взаимодействия описываются с помощью истории сообщений. Каждому сообщению соответствует объект словаря Python. Например, в коде ниже показан успешный ответ модели на основе упомянутого ранее примера отзыва:

{'messages':[

    {'role':'user', 'content':'я готов закрыть глаза на многие огрехи...'},

    {'role':'assistant', 'content':'neg'}

]}

Отзыв отрицательный (то есть его автор не рекомендует фильм), и поэтому в идеале хотелось бы, чтобы модель сгенерировала сообщение, содержащее единственный токен neg. Такое взаимодействие и показано в коде.

Чтобы дообучение было результативным, обычно требуется использовать от 50 до нескольких тысяч примеров. Добавление большего количества примеров для дообучения может повысить производительность, но и стоит дороже. С другой стороны, это однократная трата, ведь одну и ту же дообученную модель можно повторно использовать для потенциально большого набора данных (и плата за использование дообученной модели не зависит от объема обучающих данных, представленных для дообучения). Файл примера (reviews_train.csv) содержит 100 примеров и, следовательно, соответствует диапазону объемов данных, в котором дообучение может стать результативным.

Компания OpenAI требует предоставлять данные для дообучения в формате JSON-lines (в именах таких файлов обычно есть окончание .jsonl). Файлы, соответствующие этому формату, по сути, содержат в каждой строке один словарь Python. В данном случае каждая строка описывает одно успешное взаимодействие с моделью (используя тот же формат, что и в предыдущем примере). Чтобы упростить работу с файлами JSON-lines из Python, мы используем библиотеку jsonlines. Сначала перейдите в терминал и установите библиотеку, введя команду:

pip install jsonlines==4.0

Теперь с помощью этой библиотеки можно преобразовать данные из формата .csv в формат, требуемый OpenAI (листинг 9.3).

Листинг 9.3. Генерация обучающих данных для дообучения

import argparse

import jsonlines

import pandas

 

def get_samples(df):                     

    """ Сгенерировать образцы из фрейма данных.

 

    Аргументы:

        df: фрейм данных, содержащий образцы.

 

    Выводит:

        Список образцов в формате OpenAI для дообучения.

    """

    samples = []

    for _, row in df.iterrows():

 

        

        text = row['text']

        user_message = {'role':'user', 'content':text}

 

        

        label = row['sentiment']

        assistant_message = {'role':'assistant', 'content':label}

 

        sample = {'messages':[user_message, assistant_message]}

        samples += [sample]

 

    return samples

 

if __name__ == '__main__':

 

    

    parser = argparse.ArgumentParser()

    parser.add_argument('in_path', type=str, help='Путь к входным данным')

    parser.add_argument('out_path', type=str, help='Путь к выходным данным')

    args = parser.parse_args()

 

    df = pandas.read_csv(args.in_path)

    samples = get_samples(df)

 

    

    with jsonlines.open(args.out_path, 'w') as file:

        for sample in samples:

            file.write(sample)

В этом коде подготовка примеров в требуемом формате выполняется с помощью функции get_samples . Входным параметром является объект DataFrame из библиотеки pandas (параметр df), который хранит обучающую выборку в стандартном виде (предполагается, что столбец text содержит отзывы, а столбец sentiment — связанные с ними метки классов). Мы превращаем каждый пример в успешный обмен сообщениями с моделью. Сначала создаем сообщение, отправляемое пользователем и содержащее только текст отзыва. Затем добавляем желаемое ответное сообщение, которое должна сгенерировать модель (связанное с ролью «ассистента») . Полный набор обучающих примеров представляет собой список обменов сообщениями, каждое из которых подготовлено в формате JSON-lines.

В качестве входных данных должен быть представлен путь к файлу формата .csv с обучающими примерами, а также путь к файлу вывода . Файл вывода соответствует формату JSON-lines, поэтому мы, как правило, задаем выходной путь с окончанием .jsonl. После преобразования входного файла из формата .csv в формат для дообучения с помощью библиотеки jsonlines мы записываем преобразованные примеры в файл формата JSON-lines .

Как обычно, вводить код листинга вручную не требуется. Его можно найти по ссылке Prepare Fine-Tuning на сайте книги, в разделе дополнительных материалов к главе. Запустите код из терминала с помощью следующей команды (предполагается, что файл train_reviews.csv находится в том же репозитории, что и код):

python prep_fine_tuning.py    train_reviews.csv train_reviews.jsonl

Возможно, вы решите вручную проверить файл train_reviews.jsonl, который (надеюсь) был сгенерирован после запуска этой команды. В каждой строке должен отобразиться один обучающий пример, представленный в виде словаря Python.

9.9. Запуск задачи дообучения

Теперь, когда у нас есть обучающие данные в корректном формате, можно создать задачу дообучения на платформе OpenAI. Конечно, поскольку модель хранится только на этой платформе, нам не удастся выполнить дообучение самостоятельно. Вместо этого мы отправим свои обучающие данные в компанию OpenAI и попросим использовать их для создания настроенной модели. Чтобы получить в итоге настроенную модель, сначала необходимо выбрать базовую. В данном случае мы начнем с модели GPT-3.5 Turbo (что упростит сравнение с результатами, которые мы получали ранее).

Можно создать задачу дообучения, используя следующий фрагмент кода (предположим, что in_path — это путь к файлу, содержащему обучающие данные):

import openai

client = openai.OpenAI()

 

reply = client.files.create(

    file=open(in_path, 'rb'), purpose='fine-tune')

Объект reply будет содержать объект Python с метаданными о задаче дообучения (при условии успешного создания задачи). Самое важное: в поле reply.id мы получаем идентификатор только что созданной задачи. Для выполнения задач дообучения обычно требуется некоторое время (например, выполнение описываемой здесь задачи занимает около 15 минут). Следовательно, придется подождать, пока будет создана дообученная модель. Идентификатор задачи позволяет проверять ее статус и получить идентификатор вновь созданной модели, как только она станет доступна. Статус можно узнать, используя следующий фрагмент кода на Python:

reply = client.fine_tuning.jobs.retrieve(job_id)

Поле reply.status сообщает статус задачи дообучения, который в конечном счете достигнет значения succeeded. По завершении этого этапа идентификатор дообученной модели будет доступен в поле reply.fine_tuned_model.

Код, показанный в листинге 9.4, запускает процесс дообучения, ждет, пока соответствующая задача завершится, и, наконец, выводит идентификатор сгенерированной модели. При наличии пути к файлу с обучающими данными код сначала загружает этот файл . Он получает идентификатор файла, присвоенный компанией OpenAI, и использует его для создания задачи дообучения . Затем выполняются итерации, пока задача не завершится успешно . На каждой итерации выводится таймер (измеряющий секунды с начала задачи дообучения) и проверяются обновления статуса задачи . Наконец, мы получаем идентификатор модели и выводим его .

Листинг 9.4. Дообучение модели GPT с использованием обучающих данных

import argparse

import openai

import time

 

client = openai.OpenAI()

 

if __name__ == '__main__':

 

    parser = argparse.ArgumentParser()

    parser.add_argument('in_path', type=str, help='Путь к входному файлу')

    args = parser.parse_args()

 

    reply = client.files.create(            

        file=open(args.in_path, 'rb'), purpose='fine-tune')

    file_id = reply.id

 

    reply = client.fine_tuning.jobs.create(

        training_file=file_id, model='gpt-3.5-turbo')

    job_id = reply.id

    print(f'Job ID: job_id')

 

    status = None

    start_s = time.time()

 

    while not (status == 'succeeded'):      

 

        time.sleep(5)

        total_s = time.time() - start_s

        print(f'Fine-tuning since total_s seconds.')

 

        

        reply = client.fine_tuning.jobs.retrieve(job_id)

        status = reply.status

        print(f'Status: {status}')

 

    

    print(f'Fine-tuning is finished!')

    model_id = reply.fine_tuned_model

    print(f'Model ID: {model_id}')

Этот код доступен по ссылке Start Fine-Tuning, размещенной на сайте книги, в разделе дополнительных материалов к этой главе. Скачайте файл и запустите код, используя команду ниже (где train_reviews.jsonl — ранее сгенерированный файл):

python fine_tune.py train_reviews.jsonl

Если позволить скрипту работать до завершения, то отобразится вывод, подобный указанному ниже (это, конечно, лишь часть вывода; многоточиями обозначены пропущенные строки):

Job ID: ...

Fine-tuning since 5.00495171546936 seconds.

Status: validating_files

...

Fine-tuning since 46.79299879074097 seconds.

Status: running

...

Fine-tuning since 834.6565797328949 seconds.

Status: succeeded

Fine-tuning is finished!

Model ID: ft:gpt-3.5-turbo-0613...

После вывода идентификатора задачи мы будем получать регулярные обновления о статусе задачи, который обычно меняется с validating_files на running и (надеемся) на succeeded. Проблема в том, что завершение задачи может занять некоторое время (в предыдущем примере около 14 минут). Если вы не хотите, чтобы скрипт работал непрерывно (например, вам нужно выключить компьютер), то его можно остановить после того, как задача дообучения начнет выполняться (вы узнаете о начале этого процесса, так как в этот момент скрипт выведет идентификатор задачи). Задача дообучения продолжится по плану на серверах компании OpenAI. В зависимости от настроек, возможно, вам даже придет электронное письмо с уведомлением о ее завершении. В ином случае можно периодически запускать скрипт, проверяющий статус задачи (листинг 9.5).

Листинг 9.5. Проверка статуса задачи дообучения

import argparse

import openai

 

client = openai.OpenAI()

 

if __name__ == '__main__':

 

    parser = argparse.ArgumentParser()

    parser.add_argument('job_id', type=str, help='ID задачи дообучения')

    args = parser.parse_args()

 

    

    job_info = client.fine_tuning.jobs.retrieve(args.job_id)

    print(job_info)

При наличии идентификатора задачи (полученного из вывода листинга 9.4), скрипт получает и выводит метаданные задачи , в том числе ее статус и идентификатор получившейся в итоге модели (после успешного завершения задачи).

9.10. Использование дообученной модели

Поздравляю! Вы создали специализированную модель, дообученную для выполнения интересующей вас задачи (классификации отзывов). Как ее использовать? К счастью, это несложно сделать с помощью библиотеки компании OpenAI. Вместо имени одной из стандартных моделей (например, gpt-3.5-turbo) теперь указывается идентификатор дообученной модели — метка-заполнитель [Fine-tuned model ID] заменяется фактическим идентификатором модели:

import openai

client = openai.OpenAI()

 

response = client.chat.completions.create(

    model='[Fine-tuned model ID]',

    messages=[

        {'role':'user', 'content':prompt}

        ]

    )

Как и раньше, мы предполагаем, что в переменной prompt содержится текст промпта. Однако промпты для нашей дообученной модели различаются. Ранее мы предоставляли в промпте задачу классификации и текст отзыва. Теперь мы обучили модель сопоставлять только текст отзыва с соответствующим классом. Это означает, что функция генерации промптов упрощается (фактически можно утверждать, что создавать специальную функцию больше не нужно):

def create_prompt(text):

    """ Сгенерировать промпт для классификации отзывов по тональности.

 

    Аргументы:

        text: текст для классификации.

 

    Выводит:

        Промпт для классификации текста.

    """

    return text

Вместо генерации многокомпонентных промптов мы возвращаем текст отзыва для классификации. Полагаю, вам любопытно, что произойдет, если отправить упрощенный промпт в исходную модель (gpt-3.5-turbo). Вы получите примерно такой вывод:

Метка: Мне понятна ваша озабоченность по поводу сцен курения в кинофильмах, особенно в предназначенных для детской и подростковой аудитории. Курение в фильмах может повлиять на молодых зрителей, и такое поведение потенциально может начать казаться им нормальным. Однако важно отметить, что не все случаи демонстрации сцен курения в фильмах являются результатом целенаправленной рекламы и не всегда их спонсорами выступают табачные компании…

Очевидно, модель не понимает наши намерения — то есть того, каких действий с поступающими в качестве входных данных отзывами мы ожидаем от нее. Вместо генерации корректных меток классов она пишет развернутые анализы, комментируя основные моменты, затронутые в отзывах. Это вполне ожидаемо. Представьте, что вы получили текст отзыва без каких-либо дальнейших инструкций. Как бы вы узнали, что его требуется классифицировать, не говоря уже о корректных метках возможных классов? Сделать это было бы почти невозможно, и то же самое относится к языковым моделям.

Но если мы возьмем дообученную модель и предоставим ей в качестве входных данных те же самые промпты, то получим следующий вывод:

Метка: neg; Истинная метка: neg

Метка: neg; Истинная метка: neg

Метка: neg; Истинная метка: neg

Метка: neg; Истинная метка: neg

Метка: pos; Истинная метка: pos

Метка: pos; Истинная метка: neg

Метка: pos; Истинная метка: neg

Метка: neg; Истинная метка: neg

Метка: neg; Истинная метка: pos

Метка: neg; Истинная метка: neg

Количество правильных меток      :    7     

Количество использованных токенов:    2085  

Обратите внимание, что, даже не задавая параметры настройки (и не предоставляя примеры в промпте), мы теперь получаем точность 70 % , а не 60, как в исходной версии! Кроме того, количество использованных токенов по сравнению с исходной версией сократилось примерно на 200 . Все потому, что мы опускаем во всех промптах инструкции (и метки классов).

Итак, мы выяснили, что можно дообучить модель для точной классификации отзывов, одновременно сократив длину промпта. Но остается вопрос: стоило ли оно того? Проведем расчеты, чтобы это выяснить. Не будем учитывать стоимость создания дообученной модели, ведь ее требуется дообучить лишь однажды (а в примере предполагается, что нужно проанализировать отзывы за год). Без дообучения можно добиться той же точности (70 %), используя универсальную модель и параметры настройки (задав смещение и ограничение на количество токенов вывода). В этом случае для десяти тестовых отзывов потребуется 2228 токенов. После дообучения для тестовых отзывов нужно всего 2085 токенов. Однако, работая с универсальной моделью, мы платим 0,05 цента за 1000 входных токенов. А при использовании дообученной модели мы платим 0,3 цента за 1000 токенов. Следовательно, после дообучения цена за токен выросла в шесть раз! В этом конкретном сценарии небольшое сокращение количества обрабатываемых токенов не окупает более высокую плату за токен.

В целом дообучение может помочь существенно повысить эффективность модели и, возможно, снизить стоимость. Но имейте в виду, что оно влечет за собой определенные затраты. Прежде чем использовать дообученную модель в производственной среде, оцените ее в тестовой, проведите расчеты и убедитесь, что ее применение целесообразно!

Резюме

• Настройка параметров модели может влиять на ее производительность и стоимость.

• Можно ограничивать длину вывода и вводить смещение логитов токенов.

• Не всегда стоит использовать самую большую доступную модель, так как это увеличивает затраты.

• Определите оптимальную модель для своей задачи, оценив ее на примерах.

• Структура промпта может значительно повлиять на производительность модели.

• Для обучения на нескольких примерах добавьте в промпт примеры правильно решенных задач.

• Дообучение позволяет настроить базовые модели для решения интересующих вас задач. Благодаря такой специализации можно сократить длину промпта.

• Дообучение требует затрат, которые пропорциональны объему данных для обучения. Кроме того, оно увеличивает стоимость токена при использовании полученной модели.


Или для русскоязычных моделей на сайте https://mera.a-ai.ru/ru/text. — Примеч. науч. ред.

Назад: 8. Альтернативы GPT
Дальше: 10. Фреймворки