Время прочтения: 9 мин.

Интерес к теме больших языковых моделей всё не утихает, так как их возможности интенсивно растут. А круг задач, к которым можно применять искусственный интеллект, интенсивно расширяется.

Как только я узнала об LLM, мне стало интересно их “пощупать”: и текстовых ассистентов, как ChatGPT, и те, что создают изображения по текстовому описанию, как Kandinsky или Midjourney. Однако общения в чатике и рядовых запросов для отрисовки было недостаточно. Поэтому я придумала задачу, в которой могу применить оба вида моделей – создание персонажей.

Пример созданных персонажей во время работы над кодом, слева направо: Грант и Лола от DALL-E, Ава и Джон от Kandinsky.

Это задача помогает понять сразу множество аспектов: как задавать промты, чтобы модель лучше работала, и как то или иное действие влияет на получившееся изображение. Также я приобрела новый для себя опыт использования моделей в коде, используя API, а не запуская её целиком на своём ПК.

К тому же это оказалось достаточно просто: нужно оперировать только промтами и ответами от моделей, ну и чуть-чуть вспомогательного кода.

Если вам не хочется читать много текста, а перейти сразу к коду, то перемещайтесь в конец поста, там расположена ссылка на Google Colab Jupyter Notebook.

Концепция задачи

Идея была в том, чтобы одна модель генерировала описания, а вторая принимала их на вход и визуализировала. Для того, чтобы контролировать генерацию описания, промпт необходимо составить определённым образом – параметризовать. Под параметризацией я подразумеваю зависимость промпта от параметров – ряда внешних характеристик персонажа, которые я хочу увидеть, и отсутствие тех, что мне не нужны. Также в качестве параметра передаётся количество описаний для генерации и стиль рисунка. Это позволяет создать множество персонажей в рамках одной “вселенной”. Однако, оказалось, существует множество нюансов работы с моделями, и у каждой модели они свои.

Используемые LLM

С выбором модели для генерации описаний всё было просто – LLM от OpenAI являются лидерами по качеству ответов на запросы пользователей. Их модель text-davinci-003 (davinci), основанная на GPT-3, хорошо зарекомендовала себя в работе с текстом.

А вот хороших LLM для визуализаций текстовых запросов довольно много. Я решила использовать сразу две: DALL-E 2 от OpenAI и Kandinsky 2.2 от Sber.

У OpenAI моделей есть нюанс в виде бесплатного кредита в 18$, который тратится на запросы через API. Davinci рассчитывается по токенам, а DALL-E по разрешению выходного изображения: я использую разрешение 512×512, это 0.018$ за картинку.

Реализация в JN. Задание системных переменных

Доступ к рассмотренным моделям можно получить через свой персональный API-ключ. Для его получения необходимо создать аккаунты на openai.com и replicate.com. Передаю их в системные переменные:

# Для генерации описания
%env OPENAI_API_KEY=your_openai_key
# Для генерации изображения
%env REPLICATE_API_TOKEN=your_replicate_key

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

!pip install openai
!pip install replicate

Импортирую все необходимые библиотеки:

# Для подключения к моделям от OpenAI
import openai
openai.api_key = os.environ["OPENAI_API_KEY"]
# Для подключения к Kandinsky
import replicate
# Для работы с локальными директориями
import os
# Для сохранения png по url
import requests
# Для вывода png на экран
from IPython.display import Image
from IPython.display import display

Параметризация промта

Недавно у Microsoft вышла библиотека guidance как раз для этих целей, однако более половины её функций у меня не сработала. Гораздо удобнее оказалось параметризовать промт без дополнительных библиотек – простым текстом.

Для задания правильного промта важно понимать, как языковая модель работает. Основная задача больших языковых моделей – заполнять документы. Поэтому хорошим на практике способом получения желаемого ответа от модели является задание ей промта как части недописанного текста. В данном промте мы имитируем разговор, где Q – question (вопрос), A – Answer (ответ). Пишу задачу и сразу ответ в две итерации, а после ещё раз задачу, а ответ на него генерирует модель.

prompt = f"""
Q: Generate description of not existing game character using template:
template = {template}
A: {example1}
Q: Generate description of not existing game character using template:
template = {template}
A: {example2}
Q: Generate {n} diverse description of not existing game character using template:
template = {template}
A:
"""

На показанных примерах модель лучше понимает, что от неё хотят. Такой способ “обучения” LLM называется few-shot prompting.

В задании я прошу сгенерировать n описаний несуществующих игровых персонажей, используя шаблон на английском языке, так как русский она обрабатывает немного хуже. Если убрать “несуществующих”, то возникают такие персонажи, как Тор и Один, поразительно похожие на свои оригиналы.

Теперь перехожу к шаблону:

template = '{{Name}}. Beautiful {{man or woman}} in heroic pose with {{armament}}, '\
'{{he or she}} is dressed in {{clothes}} and has {{hairstyle}}, '\
'{{body art or accessory}}, predominance {{two colors}} in {{his or her}} look, '\
'{{background}} in the background'

Самое важное для персонажа – имя, но его нельзя изобразить, буду использовать его для названия картинок. В основной текст описания вставляю параметры: пол, оружие, одежда и т.д., которые будет генерировать davinci. Каждый параметр можно ограничить, предложив модели выбор из определенного набора, как в случае с “man or woman” – ребенка или пожилого она не сгенерирует.

Примеры придумываю сама, они не будут изображены, но будут прочтены моделью. Важно, чтобы примеры соответствовали заданному шаблону.

example1 = 'Jack. Beautiful man in heroic pose with gun, '\
'he is dressed in cassok and has iroquois, '\
'cap, predominance green and brown in his look, '\
'desert in the background '

Всё описание построено на внешности, также задаётся фон параметром “background” и стиль (добавляется в промпт для визуализации):

style = "digital art"

Это сделано исключительно для более красочной картинки, чтобы персонаж был уникален и выделялся. Можно попросить сгенерировать такие параметры, как слоган, возраст или числовое значение силы, но это будет полезно только для задачи c практической целью. Например, если вы создаёте свою игру.

Функции для работы с описанием персонажей

  • Функция для создания описаний с помощью davinci
def make_character(prompt, temperature = 1.0, max_tokens = 300):
response = openai.Completion.create(
engine="text-davinci-003",
prompt=prompt,
temperature=temperature,
max_tokens=max_tokens
)
return response.choices[0]['text']

Принимает промт, возвращает ответ – всё просто. Опционально задаётся температура, отвечающая за креативность (от 0 до 1) и максимальное количество токенов.

  • Функция обработки ответа модели – чистка, разделение и сохранение
def desc_into_parts(desc):
# Удаляем подстроки '1.', ..., 'n.'
for num in range(1, n + 1):
desc = desc.replace(f'{num}. ', '')
# Делим на части
parts = desc.split('\n')
# Удаляем лишние пробелы и фильтруем пустые строки
parts = [p.strip() for p in parts if p.strip()]
# Записываем имена
names = [p.split(".")[0].strip() for p in parts]
for num, _ in enumerate(parts):
# Удаляем имена из описаний
parts[num] = parts[num].replace(f'{names[num]}. ','')
# Сохраняем описание в файл
with open(f"{path}/{names[num]}.txt", mode="w") as txt:
txt.write(f"Name: {names[num]}\n")
txt.write(f"Description: {parts[num]}\n")
txt.write(f"Style: {style}")
return parts, names

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

Функции для работы с изображением персонажей

DALL-E и Kandinsky на запрос возвращают url сгенерированной картинки, сохраняю её, чтобы не потерять.

  • Функция сохранения картинки по url
def save_pic_url(img_name, url):
img_data = requests.get(url).content
with open(f'{path}/{img_name}.png', 'wb') as png:
png.write(img_data)
  • Функция генераций изображений DALL-E
def generate_dall_e(desc, name):
response = openai.Image.create(
prompt=desc,
n=1,
size="512x512"
)
res_url = response["data"][0]["url"]
image_name = name + '_dall_e'
save_pic_url(image_name, res_url)
  • Функция генераций изображений Kandinsky
def generate_kandinsky(desc, name):
response = replicate.run(
"adalab-ai/kandinsky_v2_2:eb68c00d7fc7d7297a04dcecbdd29032361ab8f1d3f6843c32fbd6ec70532319",
input={"prompt": desc}
)
res_url = response[0]
image_name = name + '_kandinsky'
save_pic_url(image_name, res_url)

Функции generate_dall_e и generate_kandinsky принимают описание и имя персонажа и создают картинку, которую сохраняют в локальной директории.

  • Функция вывода описаний и изображений на экран JN
def show_pics(model):
if model not in models:
raise Exception(f'Модель генерации изображения отсутствует. Доступные модели:{models}.')
for num in range(n):
# Файл изображения
image_path = f'{path}/{names[num]}_{model}.png'
print(names[num])
print(parts[num] + ', ' + style)
display(Image(filename=image_path))

Результат

После вызова всех функций в указанном порядке наслаждаемся результатом. Встречайте! Вот они, слева направо: DALL-E, Kandinsky.

Daniel

Beautiful man in heroic pose with sword, he is dressed in armor and has dreadlocks, necklace, predominance red and black in his look, mountain range in the background, digital art

Anna

Beautiful woman in heroic pose with bow and arrow, she is dressed in jumpsuit and has cornrows, tattoo, predominance pink and purple in her look, forest in the background, digital art

Joseph

Beautiful man in heroic pose with hammer, he is dressed in tunic and has spiky hair, earrings, predominance yellow and gray in his look, river valley in the background, digital art

Выводы

Davinci очень хорошо запоминает структуру шаблона, заполняет всё верно и без лишней информации. Однако генерация некоторых параметров была довольно однотипной, например, по причёскам и орудию. Значения параметров, конечно, можно расширить, вручную вписав желаемые значения через ‘or’. Зато имена придумывает разнообразные!

Очень интересным оказалось то, как по-разному модели видят одного и того же персонажа.

DALL-E способна генерировать невероятно реалистичные фото, но увидеть желаемый стиль у картинки оказалось сложной задачей. Также DALL-E плохо cправляется с динамикой, лицами, оружием и деталями. Молот оказался молотком, зато такого персонажа точно ещё не было!

По моему скромному мнению Kandinsky лучше справился с заданием, но тоже неидеален: пропускает детали и может обрезать картинку. А с изображением рук и лука общая проблема обеих моделей.

Тем не менее, результат меня порадовал. LLM можно успешно применять и объединять для решения каких-либо творческих (и не только) задач.

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

Ссылка на Google Colab Jupyter Notebook.