Реально Бесконечное (лето) RuGPT3.5: Генерация новеллы на ходу нейросетью
Что это за мировая линия?
Я уж было подумал, что эпоха локальных трансформерных нейросетей ушла, оставив после себя невеликое наследие (можно пересчитать на пальцах), однако пару недель назад RuGPT3.5 от сбера вышла в открытый доступ и, за неимением сильных генеративных моделей для русского языка, показалась мне удачным шансом для реализации давней идеи.
Оглавление:
Статья, будет практически на треть короче, если не читать примеры в выпадающих спойлерах. В противном случае получится лонгрид, так что я буду разбавлять текст левыми пикчами, просто держу в курсе.
Те, кого я заинтересовал, приглашаются под кат.
Живы ли сейчас открытые GPT?
Для ознакомления с принципом работы архитектуры GPT советую прочитать этот подробный разбор: GPT-2 в картинках, и этот: GPT-3
Что мы имеем из ванильных моделей:
GPT-1 (2018) (Context: 512) — Работала не очень хорошо (длинные тексты генерировались плохо), но при файнтюнинге на отдельных задачах эта модель могла выполнять несложные задания.
GPT-2 (2019) (Context: 1024) — стала лучше, научилась писать длинные связные тексты и даже решать задачи при помощи prompt engineering без обучения.
GPT-3 (2020) (Context: 2048) — Стала в 10 (!) раз больше, и настолько крутой, что даже научилась писать рабочий программный код.
RuGPT3, RuDialoGPT3, (Context: 512! ) — и множество других дообученных версий GPT3 под русский язык было обучено и выложено Сбером в открытый доступ (В свое время я даже эссе по истории, общаге и паре спецов написал исключительно ими).
Но был у них один небольшой нюанс. Небольшой он настолько же, как их контекст, длиной 512 токенов. (это как у самой первой GPT, да).
Просто выяснилось, что если тренировать модель, рассчитанную на 2048 контекст кусками текста по 512, то она немного отупеет.
Свято место пусто не бывает, кто-то должен был начать это монетизировать. Этим занялись сами создатели архитектуры — OpenAI, которые решили пойти против своего названия и запустить сайт, с чат интерфейсом своей новой версии GPT3, дообученной на контексте разметки чата — ChatGPT. В первый день её выхода в открытый тест я зарегал temp phone number и был разочарован. Он работал ничуть не лучше ванильной GPT3 на английском, а русский язык был вообще машинным переводом на входе и выходе.
После выхода ChatGPT3.5 с закрытыми исходниками долгое время была видимость того, что развитие отечественной индустрии энтузиастами умрёт вместе с рождением ChatGPT4. OpenAI стали продавать API частным компаниям и получать деньги на дальнейшую разработку.
Менее чем через год стали заметны продвижения в пользовательских моделях.
YaML 100b (2022)(Context: ?) — нечто жидкое, сопоставимое с плевком в лицо. Модель о которой неизвестно ничего, хоть она и в опен сорс… Так как весит сие чудо 250 гб! А их ещё нужно на видеокарту выгрузить, в общем, своеобразный прикол от всеми любимой компании. Всё что о ней известно, ограничивается одной статьёй, самого Ya:
Яндекс выложил YaLM 100B — сейчас это крупнейшая GPT-подобная нейросеть в свободном доступе.
LLama 2 (2023) (Context: 4096) — Самый сок, совсем недавно вышедшая модель, которая запустила множество процессов. Стали появляться десятки кастомных маленьких моделей.
HF сейчас наполнен чат-ботами на базе LLama, дообученных пользователями за сущие часы для выполнения конкретных задач. Вот только в русский язык эта модель не могла вообще (в данных для обучения его было менее 1%). Дообучение тут не поможет. В то время как ChatGPT стабильно работает на разных языках.
Сбер же недавно анонсировали GigaChat, которую все посчитали рофлом и распилом денег, доступы в ЗБТ давали очень неторопливо.
Я попал в первые 10к человек после блогеров и компаний, и как и ожидалось, GigaChat был тупой. Далее я за ним не следил.
Сейчас вроде уже идёт ОБТ и каждый желающий может потыкать «чисто русский ChatGPT», но совсем недавно случился неожиданный поворот событий. Они выложили в опен сорс чекпоинт, на котором основан GigaChat: Сбер открывает доступ к нейросетевой модели ruGPT-3.5. Скорее всего, стала очевидна неликвидность GigaChat как нового, отечественного API для создания ботов, и они решили дать разработчикам потрогать удовольствие создания чат-бота на базе голых весов, а не API, и если подобная стратегия сработает, то они смогут распространять подобные модели под коммерческой лицензией для компаний, которым нужен «свой, независимый чат-бот». Своего рода превращение услуги в товар.
Не привязанная к разметке диалога и вопросно/ответной системе эта модель является всё той-же продолжалкой теста как и RuGPT3, только без кастрированного контекста (он составляет 2048 токенов, так сказать, догнали оригинал)
Знакомство с RuGPT3.5
Для начала нужно запомнить, что в оригинале наша новая подруга весит 50 гигаметров, однако такое количество видеопамяти мне не по карману. Благо добрые люди уже конвертировали сеть, уменьшив битность каждого из её нейронов, сжав её тем самым в 4 раза! Размер 4х битной модели составляет около 7 гигов.
Теперь мы можем запустить её в бесплатном Google Colab на NVIDIA Tesla T4 16Gb и получить, пусть и менее точный, но практически полный опыт готовки её в домашних условиях.
pip install transformers auto-gptq gradio >> nul
Устанавливаем необходимые библиотеки и переходим к скачиванию и выгрузке на видеокарту самой нейросети.
from transformers import AutoTokenizer
from auto_gptq import AutoGPTQForCausalLM
import gradio as gr
model_name = 'fffrrt/ruGPT-3.5-13B-GPTQ'
model_basename = 'gptq_model-4bit-128g'
tokenizer = AutoTokenizer.from_pretrained(model_name, use_fast=True)
model = AutoGPTQForCausalLM.from_quantized(model_name,
model_basename = model_basename,
use_safetensors=True,
trust_remote_code=True,
device="cuda:0",
use_triton=False,
quantize_config=None)
Теперь у нас есть model и tokenizer, как в старые добрые времена GPT2/3.
Простые тесты
Если вы не знаете кто такие эти ваши top_k, top_p, temperature и no_repeat_ngram, но знаете единственный богоугодный язык (он же английский), то можете наверстать или вспомнить всё по ссылке:
Using different decoding methods for language generation with Transformers
Параметры генерации
K: 40,
P: 0.98,
temperature: 0.6,
no_repeat_ngram_size: 4
И да, beam search работает уж совсем медленно и плохо. Модель зацикливается буквально на ровном месте, так что в дальнейшем я буду использовать исключительно top_p семплинг.
Предлагаю испытать модель на том, что ей должно быть как кровь родное — литературные тексты. Возьму я, однако, не самый простой кусок: Это будет перегруженный контекст «Зова Ктулуху» Лавкрафта
Пример с Зовом Ктулуху
Великие Старейшины, продолжал Кастро, не целиком состоят из плоти и крови. У них есть форма — …, Великие старейшины обращались к самым чутким из них при помощи внедрения в них сновидений, ибо только таким образом мог Их язык достичь сознания людей.
И вот, прошептал Кастро, <Дальше генерация> старейшины, обитатели Р«льеха, должны были узнать, что на Земле есть люди, которые могут общаться с Ними, и потому Они послали своего слугу, Ктулхха, чтобы он отыскал таких людей. Он нашел их — по крайней мере, некоторых из них. Он нашел людей, которых мог подчинить себе. И он подчинил их себе. Он нашел таких людей, которые могли общаться с Н…
Получилось ниже среднего. Думаю, даже старая rugpt3 справилась бы на том-же уровне. В конце видно явное зацикливание и несвязность. Дадим модели второй шанс. Попробуем максимально открытый контекст, но приближенный к нашей основной теме — диалоги персонажей, преимущественно разных полов.
Диалог Ани и Васи в песочнице, когда Аню забирает мама.
Анна: Ты правда не вернёшься?
Я должен уйти, всем будет так лучше, ответил Василий.
Но мне будет плохо без тебя, я не смогу жить без тебя, — заплакала Анна.
Я вернусь, — сказал Василий.
Анна: Я буду ждать тебя, я буду ждать тебя всегда, я буду любить тебя, я всегда буду любить тебя.
Василий: Я вернусь, я обязательно вернусь.
В этот момент в комнату вошла мать Анны.
Мать Анны: Анна, что ты наделала?
Анна (плача): Я люблю его.
Я люблю его, — сказала Анна.
Она подошла к Василию, обняла его и поцеловала.
После этого Василий и Анна вышли из дома и пошли по дороге
Получилось плоско, мда. Ну, как отечественный фильм для зомбоящика покатит.
Ладно, переходим к нашему герою
Игре, которая состоит на 99% из дженерик текста, писанного анончиками, игра, которая получила любовь простых игроков, будучи нишевым произведением про маскотов имиджбордов — Бесконечное Лето.
Но, как оно обычно и бывает — есть один нюанс.
Почему Лето не Бесконечное?
Так как в нём всего 7 дней, а название игры это обман и провокация.
Качаем игру из стима
Делаем вид, что мы разработчик модов, а значит можем лезть в файлы игры.
Распаковываем файлы RenPy при помощи Unrpyc (github)
Вручную превращаем сценарии RenPy в текст (парсим регулярками (regex))
Получаем сценарий игры в виде текстового документа.
За неделю пассивного перечитывания и редактуры я выполнил пункт 3 (ну, почти)
Результат проделанной работы тут: https://github.com/CodeDruidX/Everlasting-Summer-txt
Делал я это, увы, практически год назад и вообще изначально рассчитывал поиграться этим с RuGPT3, но она была для этого слишком глупа.
Последние события же сподвигли меня найти этот репозиторий и продолжить свои опыты, результатами которых я с вами и хочу подлиться.
В этой статье мы будем тестировать RuGPT3.5, как генератор истории для визуальной новеллы (буду честен, просто новеллы, ни разу не визуальной). Хотелось бы дообучить RuGPT3.5 на этом наборе текста и добиться адекватного результата, в лучшем случае — интегрировать генератор сценария прямо в RenPy, и получить готовый продукт с минимальными затратами.
Как выглядит сценарий?
.rpy — это файлик на питоне, где от питона остался только синтаксис циклов\ветвлений.
Реплики персонажей выглядят как:
» (id) Привет, как дела!» — где id является идентификатором героя — он определяет имя и его стиль в графическом окне.
В моем случае все эти конструкции были превращены в обычный текст:
«Я: Привет, как дела!»
Для визуальных элементов, в общих чертах, используется что-то вроде »(id)-в-костюме пионера-смотрит-вперед». Это мы всё пока-что убираем, в надежде на светлое будущее.
Промпт, который мы будем использовать в спойлере ниже.
Начало 2-ого дня — Осторожно, длинный текст
Мне снился сон…
Казалось, я нахожусь в каком-то вакууме, а вокруг — пустота.
Но не только _вокруг_ — Я единственное существо во Вселенной.
Как будто она вернулась в некое сингулярное состояние перед самым Большим Взрывом.
И вот-вот что-то должно было произойти.
Вдруг я начал слышать голос.
Слов не разобрать, но он показался мне знакомым.
Голос что-то нежно нашёптывал, как будто убаюкивая.
И тут я понял… Это был голос той Незнакомки из автобуса. Той девушки из сна.
Мысли: Но что она хочет мне сказать? Кто она?…
Я проснулся.
Яркие лучи солнца били в глаза.
Время приближалось к полудню.
Лениво потянувшись и зевнув, я начал вспоминать вчерашний день.
За несколько секунд все его события пронеслись перед глазами: автобус, лагерь, местные обитатели.
Мысли: Но ведь всё не так, неправильно!
Не эта ситуация, не моё положение — они неправильны априори, –, а моё отношение к ним.
Мысли: Ведь я вчера запросто заснул здесь, а до этого мило беседовал с местными пионерами, даже умудрялся шутить!
Мысли: Но как можно себя так вести в подобной ситуации?!
Мысли: Я же должен бояться, шарахаться от каждого шороха, избегать любого контакта с потенциально враждебными существами.
Все события прошедшего дня словно заволокло похмельной дымкой.
Мысли: Это очень похоже на утро после хорошей пьянки: вчерашнее естественное, непредосудительное, в высшей степени нормальное поведение утром становится кошмаром, гротескной гравюрой из иллюстраций к «Божественной комедии».
Мысли: Да, всё именно так, однако прошлого уже не вернуть.
Хотя, возможно, оценив обстановку, я действовал по ситуации.
Я огляделся по сторонам, пытаясь понять, не забросило ли меня куда-нибудь ещё, но домик Ольги Дмитриевны выглядел так же, как и вчера.
Всё было как будто на своих местах, разве что на спинке кровати висела пионерская форма.
Я с недоверием покрутил её в руках, примерил и оделся.
Мысли: Всё равно это лучше, чем ходить в зимней одежде.
Мысли: Посмотреть бы теперь на себя — наверняка выгляжу как клоун!
А для этого нужно зеркало. Хотя бы самое маленькое.
Нашлось оно на дверце шкафа.
Семён: Твою!…
Я взглянул на новоиспечённого пионера и аж отпрыгнул в сторону от неожиданности!
На другой стороне зеркала стоял какой-то подросток!
Похожий на меня, но не я!
Куда пропали недельная небритость, мешки под глазами, сутулость и смертельно уставшее выражение лица?!
Похоже, меня не закинули назад во времени или в параллельную реальность, а просто поменяли с кем-то телами.
Мысли: Действительно просто! Такое же на каждом шагу встречается!
Я пригляделся к незнакомцу повнимательнее и только тогда понял, что это я сам!
Только образца конца школы — начала института.
Мысли: Ладно, хотя бы так.
Мысли: Да уж, _человек в стрессовой ситуации_ слона и не приметил!
Мысли: А вот вожатая обратила внимание и вчера ночью меня отчитала за неподобающее обращение к ней…
Мысли: К чёрту!
Мысли: Вряд ли мой внешний вид влияет на что-то ещё.
Если верить часам, завтрак уже давно позади.
Мысли: Ну ладно, попробую всё же в столовой что-нибудь найти.
Мысли: Вчера же со Славей получилось.
От этих воспоминаний я невольно улыбнулся.
На улице ярко светило солнце, дул лёгкий ветерок.
Мысли: Прекрасный летний день.
Я уже несколько лет не чувствовал себя по утрам так хорошо.
Все проблемы на секунду улетели куда-то далеко, растворились в редких, цвета первого снега облаках.
Вдруг передо мной словно из ниоткуда появилась Ольга Дмитриевна.
Ольга Дмитриевна: Доброе утро, Семён!
Семён: Доброе!
Я улыбнулся, пытаясь всем своим видом показать, что несмотря ни на что утро моё было действительно добрым.
Ольга Дмитриевна: Ты только вчера приехал, так что будить я тебя не стала, но завтрак-то…
Ольга Дмитриевна: Хотя ладно! Вот, держи!
Она протянула мне бумажный свёрток.
Судя по масляным пятнам, внутри, скорее всего, бутерброды.
Семён: Ой, спасибо!
Ольга Дмитриевна: А теперь марш умываться!
Я уже собирался уходить.
Ольга Дмитриевна: Сейчас, подожди.
Ольга Дмитриевна забежала в домик и, вернувшись, сунула мне небольшой пакетик.
Внутри оказались зубная щётка, мыло, небольшое полотенце и что-то ещё — я особо не всматривался.
Ольга Дмитриевна: Пионер должен быть всегда чист и опрятен!
Ольга Дмитриевна: Дай я тебе галстук правильно завяжу на первый раз, а то он болтается. Потом научишься, сам будешь!
Семён: А может, не надо? Я сейчас умываться иду.
Мысли: Ну да, вдруг зацеплюсь за кран и удавлюсь…
Ольга Дмитриевна: Ладно, тогда потом. И не забудь про линейку.
Мысли: Карандаши, ручки, линейки… Такие вещи не забываются!
Семён: Какую линейку?
Ольга Дмитриевна: В смысле — какую линейку?!
Она нахмурилась.
Ольга Дмитриевна: Сегодня же понедельник!
Мысли: Странно, а по моим подсчётам — воскресенье…
Мысли: Впрочем, смена дня недели — это ещё не самое страшное.
Ольга Дмитриевна: Обычно у нас линейки рано утром, до завтрака, но сегодня понедельник, поэтому она будет в 12 часов.
Ольга Дмитриевна: Не опаздывай!
Семён: Хорошо. А где?
Ольга Дмитриевна: На площади, где же ещё!
Спорить было бессмысленно.
Я направился в сторону «помывочной».
На отдельный душ и туалет рассчитывать не приходилось, но при виде этого выкидыша загнивающего социализма — причудливой черепашки с панцирем из жести, множеством ног-кранов и кафельным брюшком — мне стало несколько не по себе.
Я не был брезгливым человеком, но тем не менее, стоя тут, понял, что всё же есть какой-то минимальный уровень привычного комфорта, без которого жить мне довольно проблематично.
Вот ведь как бывает — когда теряешь вещи, которые всегда казались совершенно обыденными и естественными, понимаешь, что на самом деле они были незаменимы.
Мысли: А, да и чёрт с ним! Выбирать всё равно не из чего.
Вода оказалась просто ледяной.
Если помыть руки не составило особого труда, то вот умыться или прополоскать рот ей — уже большая проблема.
В пакетике, который мне дала Ольга Дмитриевна, не нашлось зубной пасты.
Можно, конечно, было почистить зубы и так, но в полотенце была завернута какая-то кругленькая коробочка.
«Зубной порошок».
Мысли: Прелестно! +1 за то, что я где-то в прошлом.
Умылся я довольно быстро, в том числе и из-за ледяной воды.
Кто-то быстро шёл, даже бежал в мою сторону.
Я обернулся.
Передо мной стояла Славя в спортивном костюме.
Похоже, эта девочка будет хорошо выглядеть абсолютно во всём — и в пионерской форме, и в купальнике, и, наверное, даже в космическом скафандре.
Славя: Физкульт-привет!
Семён: Охай… То есть, бобр… Доброе утро! Вот…
Приветствие мне удалось выбрать не сразу.
Славя: Почему на завтрак не пришёл?
Семён: Проспал.
Я сказал это так, словно гордился своим достижением.
Семён: Но мне Ольга Дмитриевна бутерброды принесла.
Славя: А, ну отлично тогда! Не забудь про линейку!
Семён: Да, конечно.
Мысли: Забудешь тут.
Славя: Ладно, я побежала, не скучай!
Она помахала мне на прощание и скрылась за поворотом тропинки.
Мысли: Судя по всему, линейка начнётся через пару минут.
Стоило быстренько забежать «домой», закинуть пакетик с умывальными принадлежностями, съесть бутерброды и только уже потом идти на площадь.
Я распахнул дверь домика вожатой и вбежал внутрь так, как будто запрыгивал в последний вагон уходящего поезда.
Но, кажется, это было не лучшим решением — посреди комнаты стояла Ольга Дмитриевна…
И
А вот что было дальше, нам расскажет нейросеть, окей? Кстати, отвечаю на ваш немой вопрос — да, у этой модели отличный текстовый NSFW, в этом её никто не ограничивал.
Примеров с ним я приводить не буду, просто держу в курсе, да и к этому мы ещё вернёмся.
Подготавливаем функционал
cfg={
"K":50,
"P":0.92, # Конфигурация модели
"temperature":0.2,
"uningram":4
}
def editcfg(name,val): # Функция, которую будет вызывать Gradio
print(name,val) # для изменения настроек в cfg
global cfg
cfg[name]=val
def gen(text,tokens=10): # Обёртка для простой генерации заданного количества токенов
encoded_input = tokenizer(text, return_tensors='pt').to('cuda:0')
#print(encoded_input.input_ids[:,-1900:].shape)
output = model.generate(
input_ids=encoded_input.input_ids[:,-1900:],
max_new_tokens=tokens,
do_sample=True,
top_k=cfg['K'],
top_p=cfg['P'],
temperature=cfg['temperature'],
#num_beams=4,
no_repeat_ngram_size=cfg['uningram'],
pad_token_id=tokenizer.eos_token_id,
# num_return_sequences=5,
# do_sample=True
)
return tokenizer.decode(output[0], skip_special_tokens=True)
Теперь у нас есть интуитивно понятная функция gen (), которая просто продолжает переданную в неё строку.
Вы можете заметить что я беру 1900 последних токенов, обрезая все предыдущие, это нужно для того, что-бы с большой вероятностью уложиться в 2048 и не словить случайно Out Of Memory, который случается если контекст перегружен и не влазит в видеопамять (а у нас её в обрез)
Тесты в лабораторных условиях
Тестируем продолжение имеющегося у нас текста, никакого черри пикинга или редактуры — все свежевыжатое из модели (кроме NSFW, тут уж извините):
Куча тестов с комментариями
Но, кажется, это было не лучшим решением — посреди комнаты стояла Ольга Дмитриевна…
И она была не одна.
Рядом с ней стоял какой- то парень.
Парень: Доброе утро!
Он был одет в белую
Но, кажется, это было не лучшим решением — посреди комнаты стояла Ольга Дмитриевна…
И она была не одна.
Рядом с ней стоял какой- то парень.
Парень: Доброе утро!
Парень был одет в джинсы
Хммм, кажется, как под копирку (интересно откуда такое стремление добавить персонажа…)
Но, кажется, это было не лучшим решением — посреди комнаты стояла Ольга Дмитриевна…
И она была не одна.
Рядом с ней стоял какой- то парень.
Парень: Доброе утро!
Парень был одет в спортивный
Это заговор? Ладно дам ей подсказку, пусть и неверную:
И ела мои бутерброды!
Но, кажется, это было не лучшим решением — посреди комнаты стояла Ольга Дмитриевна…
И ела мои бутерброды!
Вожатая: Доброе утро!
От неожиданности я чуть не подавился.
Слава: Доброе утро.
Она стояла
Тут без комментариев, как я дальше выяснил она вообще не принимает «Славя» как имя! Это большой косяк, ведь дальше модель будет постоянно менять имя «Славя» на «Слава», как следствие, со временем меняя пол персонажу, мда…
UPD: В этом примере все сломалось еще до появления Слави, но проблему это не отменяет.
Ещё один пример:
Но, кажется, это было не лучшим решением — посреди комнаты стояла Ольга Дмитриевна…
И ела мои бутерброды!
Вожатая: А, Семён, ты уже проснулся!
Слава: Доброе утро!
Оля: Доброе утро.
Славя как персонаж-баг, это имя ломает модель:
Она меняет его на «Слава»
А дальше начинает изменять имена всем персонажам как только вздумается
Изменим начальные условия ещё немного:
И танцевала балет!
Но, кажется, это было не лучшим решением — посреди комнаты стояла Ольга Дмитриевна…
И танцевала балет!
В руках у неё была какая-то палка, которой она выделывала замысловатые па.
При этом она напевала что
Неплохо, теперь попробуем отыграть за главного героя вручную.
Но, кажется, это было не лучшим решением — посреди комнаты стояла Ольга Дмитриевна…
И танцевала балет!
Я невольно выпалил.
Семён: Ольга Дмитриевна, что вы делаете?!
========Далее — нейросеть========
Ольга: А что, не видно?
Мысль: И что, она действительно танцует?
Я посмотрел на Ольгу Дмитриевну, которая, похоже, была в прекрасном настроении.
Она была одета в спортивный костюм, а на голове
Очень даже хорошо.
В NSFW тестах результаты всё ещё лучше, примерно 3/5 успешных сценариев со вступлением, выдержанным повествованием и логичным концом.
Это можно объяснить тем, что в них обычно два действующих лица и в повествовании главную роль играет косвенная речь, а не прямая.
Это доказывает самую главную проблему, которую я озвучу далее.
Генерация каждого из них занимала от 5 до 10 секунд — весьма шустро.
Набрасываем Gradio Webui
def complete_with_gpt(text):
text=gen(text,30)
return text
with gr.Blocks() as demo:
gr.Markdown("# Ever novel")
textbox = gr.Textbox(placeholder="Верни контекст на место!", value=prompt)
btn = gr.Button("Ожидать")
gr.Markdown("---")
with gr.Row():
K=gr.Number(label="top_K",value=cfg["K"],minimum=1)
P=gr.Number(label="top_P",value=cfg["P"],minimum=0,maximum=1)
T=gr.Number(label="Temperature",value=cfg["temperature"],minimum=0)
U=gr.Number(label="Unique ngram len",value=cfg["uningram"],minimum=0)
APPLY=gr.Button(value="Применить")
APPLY.click(fn=lambda x: editcfg("K",x),inputs=K)
APPLY.click(fn=lambda x: editcfg("P",x),inputs=P)
APPLY.click(fn=lambda x: editcfg("temperature",x),inputs=T)
APPLY.click(fn=lambda x: editcfg("uningram",x),inputs=U)
btn.click(fn=complete_with_gpt, inputs=textbox, outputs=textbox, queue=False)
demo.launch(debug=True)
как элегантно!
Интерфейс из себя представляет текстовое поле огромной длины, на нижнем конце которого генератор и его параметры — вроде стильно.
Ещё парочка заключительных тестов ваниллы.
Тесты
И танцевала балет!
Я невольно выпалил.
Семён: Ольга Дмитриевна, что вы делаете?!
Ольга: А что, не видно?
Мысль: И что, она действительно танцует?
Я был в шоке.
Танец вожатой был похож на какой-то странный гибрид вальса, танго и ещё чего-то, что я не мог определить.
Она двигалась так, словно не было вокруг никого, кроме неё и музыки.
И это было действительно красиво.
Не знаю, сколько времени прошло, но когда танец закончился, я понял, что Ольга Дмитриевна всё ещё стоит посреди комнаты.
Только вот её лицо было красным, а глаза — мокрыми.
— Семён, — сказала она, — ты не мог бы…
— Да, конечно, — ответил я, — я сейчас.
Мне было стыдно за то, как я себя вёл.
Стыдно за то, каким я был.
А ещё я был рад, что она не заметила моего смущения.
Потому что я был смущён.
Очень смущён. И мне было стыдно за это.
За то, что мне было стыдно.
Опа, всё бы ничего -, но мы зациклились, господа. Без действия игрока сюжет не хочет двигаться, а модель заполняется контекст мусором, которой вводит её саму в заблуждение.
Попробуем уменьшить температуру, для понижения уровня хаоса генерации.
temperature=0.07 — это реально низко. Рассчитываю на адекватный и предсказуемый ответ.
Ольга: А что, не видно?
Мысль: И что, она действительно танцует?
Я посмотрел на Ольгу Дмитриевну, которая, похоже, была в отличном настроении.
Она, улыбаясь, продолжала танцевать.
Танец был довольно странным, но в то же время завораживающим.
Не знаю, сколько времени прошло, но в какой-то момент я понял, что Ольга Дмитриевна танцует не одна.
Рядом с ней был кто-то ещё, и этот кто-то тоже танцевал.
Мысли
Мыслей было много, но все они были какими-то обрывочными, не связанными друг с другом
Это плохо. Определённо. Получается максимально дженерик текст, но мы опять перешли к литературному стилю повествования — вот что меня реально волнует.
Даём ей еще один шанс:
Рядом с ней был кто-то ещё, и этот кто-то тоже танцевал.
Это был я!
Я был в своём теле, но в теле Ольги Дмитриевной!
Вот это плот-твист. Кажется, температура у нас сегодня работает в обратную сторону — либо мне сильно не повезло.
Возвращаем температуру к значению 0.2 и опускаем top_K до 35 вместо 50
И танцевала балет!
Вожатая была одета в балетную пачку и танцевала, как заправская балерина.
Она кружилась, делала пируэты, прыгала, приседала и даже делала какие-то замысловатые па ногами.
При этом она напевала какую-то мелодию, а в конце танца даже сделала несколько балетных па в стиле «Лебединое озеро».
Я стоял, открыв рот, и не знал, что делать.
А Ольга Дмитриевна тем временем закончила свой танец и, подойдя ко мне, сказала:
Ольга: Ну что же ты стоишь?
Я: А что мне делать?
Она: Как что? Танцевать!
И тут я понял, что она не шутит.
И я начал танцевать.
Сначала я просто стоял, не зная, что делать, но потом, когда Ольга Дмитриевна начала напевать какую-то знакомую мелодию, я вспомнил, что когда-то давно, ещё в школе, мы с ребятами танцевали под эту песню.
Тогда я ещё не умел танцевать, но сейчас, когда Ольга начала петь, я вспомнил всё, чему меня
Нудно и душно, так ещё и переход к местоимениям вместо полных имён (привет разметка).
Я осознал свою ошибку: «Ольга Дмитриева:» токенизируется 5 токенами, и не проходит по минимальной длине уникального блока текста (равной 4), однако этот костыль необходим — что будет если его ослабить:
Результат удручает
И танцевала балет!
Мысли (в панике): Что за…?!
Ольга Дмитриевна (танцуя): А вот и ты!
Мысли (в панике): Что за?!
Ольга Дмитриева: А вот и я!
Мысли в панике: Что за?!
Мысли в ужасе: Что за?!
Мысли: Что за?!
Ольга Дмитриевна: Ну что ты стоишь?
Мысли
Что за?!
Результаты опыта
— И самый важный: Ванильная модель противится сценарному стилю повествования.
Она пытается генерировать литературный текст, где имена не повторяются, а прямая речь либо сведена к необходимому минимуму, либо просто полностью переходит в косвенную.На текущий момент модель точно нельзя заставить писать .rpy файл, для отправки его непосредственно в движок игры.
Тут нельзя притвориться что всё хорошо, ну никак. Она периодически теряет нить повествования. Но эта проблема прямо и непосредственно связана с разметкой и стилем.
Поможет ли дообучение (fine-tuning)?
Да — поможет. Заставить модель генерировать другую разметку — не язык ей менять.
Для начала: полный файл текста игры, до момента с выбором рута в 4 дне находится ТУТ
Наш файл представляет из себя:
225к букв
36к слов
4.5к строк
Пишем вызов стандартного transformers.Trainer, и натравливаем этого зверька на нашу модель. Примера ради, запущу 3 эпохи обучения.
import torch
from transformers import TextDataset, DataCollatorForLanguageModeling
train_path = 'train_dataset.txt'
train_dataset = TextDataset(tokenizer=tokenizer,file_path=train_path,block_size=64)
data_collator = DataCollatorForLanguageModeling(tokenizer=tokenizer, mlm=False)
from transformers import Trainer, TrainingArguments
training_args = TrainingArguments(
output_dir="./finetuned",
overwrite_output_dir=True,
num_train_epochs=3,
per_device_train_batch_size=10,
auto_find_batch_size=True,
per_device_eval_batch_size=1,
warmup_steps=10,
gradient_accumulation_steps=10
)
trainer = Trainer(
model=model,
args=training_args,
data_collator=data_collator,
train_dataset=train_dataset,
optimizers = (torch.optim.AdamW(model.parameters(),lr=1e-5), None)
)
trainer.train()
Вот уже и одна эпоха обучилась, пойду приготовлю кофе. Если серьёзно — то времени потраченного жаль. Квантованная модель (с пониженной битностью) ломается во время обучения стандартным тренером и превращается в битую гадость, которая потом отказывается работать, обнаруживая в своих тензорах крайние значения.
RuntimeError: probability tensor contains either `inf`, `nan` or element < 0
Нужно сказать, что ситуация эта меня отправила в тупик. Над решением проблемы я бился всю ночь, и в итоге лёг спать в 9:00 с пустыми руками (или выводом модели, тут как вам угодно).
Отчаявшись я сам задал пару вопросов на форумах и получив ответ об отсутствии такой возможности успокоился.
Остаётся вариант покупки платной A100 в колабе за 900 деревянных в месяц, но даже её 40 гигов не хватит для полной загрузки нейросети.
Уходит красиво
Статья бы и осталась в таком виде, если бы спустя день мне не написал чел, сказав что никто не запрещал натренить LoRA, в случае с Quantized сетями — QLoRA, соответственно.
Я, как человек знакомый с Лорами только в контексте модов на чекпоинты Stable-Diffusion удивился, но был весьма заинтересован.
Сейчас начнётся подготовка, и поверьте, она заняла больше всего времени.
Нам понадобится репозиторий GptQlora, и немного настойчивости.
!pip install auto-gptq gradio
!git clone -b peft_integration https://github.com/PanQiWei/AutoGPTQ.git && cd AutoGPTQ
!pip install .[triton]
%cd ..
#!git clone https://github.com/timdettmers/bitsandbytes.git
!pip install bitsandbytes
!git clone https://github.com/qwopqwop200/gptqlora
%cd gptqlora
!pip install git+https://github.com/huggingface/transformers.git
!pip install git+https://github.com/huggingface/peft.git
!pip install git+https://github.com/huggingface/accelerate.git
!pip install -r requirements.txt
!pip install protobuf==3.20.*
%cd ..
!python -m bitsandbytes
Разворачиваем исключительно проверенные и стабильные зависимости.
Скачиваем нейросеть в текущую папку
!pip install huggingface_hub
from huggingface_hub import snapshot_download
snapshot_download(repo_id="gurgutan/ruGPT-13B-4bit",local_dir=r"./ruGPT-3.5")
P.S. Я скачал тут репозиторий gurgutan/ruGPT-13B-4bit, вместо fffrrt/ruGPT-3.5–13B-GPTQ. В нём кроме .safetensors есть простой .bin (gptqlora ищет конкретно .bin), а менять код библиотеки, я на данный момент не рассчитывал.
Ритуал с бубном
Пытаемся начать обучение с единичной выборкой и 100 эпох
!python /gptqlora/gptqlora.py –learning_rate 0.0001 --model_path /ruGPT-3.5 --max_train_samples 1 --max_steps 100
Кто-то нажал Ctrl+C и завершил процесс и это был не я — честно.
Идём смотреть на место завершения в файле библиотеки gptqlora.py
Выполнение отменяется по ходу работы 282 строки. Во время обычной загрузки основной модели — это мы легко отделались… В начале статьи мы уже делали это вручную.
Как оказалось, она переполняет оперативную память. Добавляем в набор аргументов с 282 по 292 строку этот флаг:
model = AutoGPTQForCausalLM.from_quantized(
...
low_cpu_mem_usage=True, #Пониженное использование RAM
...
Всё запустилось! — жду летающие помидоры в комментариях)
Держу в курсе, дальше в gptqlora.py будет вноситься ещё больше изменений, так что я просто приложу готовый файл. Итоговый вид gptqlora.py
Пробуем запустить с датасетом по умолчанию — Alpaca.
Успех. Обучение завершилось за 2.5 минуты.
Видим, что создался некий .bin, размером 800 мб.
Адаптируем старый код под LoR адаптер и проверим жизнеспособность такой задумки…
Новый код с поддержкой LoRA
import torch
from transformers import AutoTokenizer
import auto_gptq
from auto_gptq import AutoGPTQForCausalLM, get_gptq_peft_model
from auto_gptq.utils.peft_utils import GPTQLoraConfig
#import gradio as gr
model_name = 'fffrrt/ruGPT-3.5-13B-GPTQ'
model_basename = 'gptq_model-4bit-128g'
tokenizer = AutoTokenizer.from_pretrained(model_name, use_fast=True)
model = AutoGPTQForCausalLM.from_quantized("/ruGPT-3.5",
low_cpu_mem_usage=True,
device_map='auto',
trust_remote_code=True,
inject_fused_attention = True,
inject_fused_mlp = False,
use_triton=True,
warmup_triton=False,
trainable=True)
peft_config = GPTQLoraConfig(
inference_mode=True,
)
mo