Добрый день! Меня зовут Александр. Меня очень интересует тематика нейронных сетей связанных с работой с текстом. И по этому я решил пройти курс от OTUS «Natural Language Processing». В рамках выпускной работы на этом курсе, я хотел попробовать расширить знания в сфере генерации текстов и использовании различных стратегий генерации. А так как мне еще нравится стилистика жанра фэнтези, то выбор пал на генерацию текстов этого жанра. При этом хотелось попробовать генерировать не только тексты выбранной стилистики но и по заданным параметрам, например имя главного героя, место действия и др.
Цели проекта
Погружаясь в реализацию этого проекта, основными целями для себя я ставил следующие:
Разобраться с дообучением моделей для генерации произвольных текстов
Изучить и использовать на практике различные стратегии обучения
Попробовать обучить несколько моделей имеющих различное число параметров
В конечном итоге обучить модели способные генерировать тексты в необходимой стилистике и по заданным параметрам
Поиск данных
Описание обучающих данных
Обучающие тексты должны были состоять из 2 частей. Первая часть это затравка. Вторая часть собственно сгенерированный текст. В затравке я планировал передавать интересующие меня параметры для состава текста. Пример затравки и выходного текста будет дан ниже.
Так как нужных мне обучающих выборок и текстов мне найти в интернете не удалось, я решил воспользоваться очень популярным сейчас сервисом ChatGPT.
Автоматизация генерация обучающей выборки
ChatGPT уже достаточно хорошо выполнял задачу, я поставил перед собой на этот проект, по этому я решил сгенерированные им тексты в качестве обучающей выборки. В качестве входных параметров я давал ему затравку.
Представь что ты рассказчик. Cоздай небольшой рассказ. В качестве его элементов используй следующее:
Имя героя: Стиг
Раса героя: Чудища
Способности героя: Священник
Место действия: Скалистые ущелья
Великий артефакт: Медаль великой отваги
Противник: Повелитель демонов
А в качестве ответа он возвращал текст примерно такого содержания:
В мрачных и запутанных Скалистых ущельях, где каменные скалы словно стояли стражами над тайнами прошлого, жил один необычный герой по имени Стиг. Он был чудищем, с видом, который мог бы пугать даже самых отважных, но в его сердце горел огонь веры и благородства.
Стиг был священником, непохожим ни на кого другого. Его способности лежали не в силе меча или огня, а в исцеляющей силе слова и веры. Он обладал способностью призывать свет в самых темных уголках, просветляя тьму надеждой.
В этих ущельях, где каждый камень казался пропитанным злом, где демоны скрывались в тени, Стиг был последней надеждой. Его медаль великой отваги была символом его решимости и верности своему призванию.
Против него встал сам Повелитель демонов, тёмный властелин этих мест. Он хотел захватить ущелья и погрузить их во вечную тьму. Но Стиг не позволил этому случиться.
Силой своей веры и мудрости, он смог противостоять Повелителю демонов. Он призывал свет, который разгонял тьму, и слова, которые проникали в самые глубины души.
Наконец, великая битва подошла к своему концу. Повелитель демонов был побежден, а ущелья остались в безопасности под защитой Стига, священника-чудища, чей свет проникал в самые темные уголки мирарод , наполняя их надеждой и миром.
Теперь, когда формат необходимых текстов был определен, осталась только автоматизировать этап генерации обучающей выборки.
Затравку я генерирую сам, Имя героя, специализация и род деятельности я взял из игры Герои меча и магии 3. Расы использованы самые обычные для любого фэнтези, место действия, названия артефакта и злодея я сочинил произвольно. Осталось только с использованием этих данных автоматизировать генерацию затравки. Каждая часть хранится в своем списке, и при каждом вызове функции случайным образом берется элемент из каждого списка
import random # Пример генерации затравки с использованием заранее придуманых def get_shot(names, professions, race, places, artefacts, apponents): res = 'Представь что ты рассказчик.' res+=' Cоздай небольшой рассказ. В качестве его элементов используй следующее:' name = names[random.randint(0, len(names)-1)] profession = professions[random.randint(0, len(profs)-1)] race = races[random.randint(0, len(rass)-1)] place = places[random.randint(0, len(places)-1)] artefact = artefacts[random.randint(0, len(artefacts)-1)] appon = apponents[random.randint(0, len(apponents)-1)] apponent+=f'\nИмя героя: {name}\nРаса героя: {race}\nСпособности героя: {profession}\nМесто действия: {place}\nВеликий артефакт: {artefact}\nПротивник: {apponent}' return res
Генерировать тексты можно непосредственно через сам чат, но это долго и неудобно если нет api токена, а в ручную отправлять использовать web интерфейс чата очень долго, большую выборку сгенерировать не получится. По этому для автоматизации запросов к ChatGPT я воспользовалсяpython библиотекой g4f, которая собрала в себя много бесплатных сервисов для запросов к ChatGPT.
i = 0 k = 0 while len(dct)<3000: promt = get_shot(names, list(set(prof)), ras, place, artefacts, apponent) data = await asc_gpt(promt) dct[promt].append(data) if len(dct)%10==0: i+=1 print(f'{i} step 10') if len(dct)%100==0: print(f'В словаре уже {len(dct)} затравок') with open('backupdct', 'wb') as file: pickle.dump(dct, file) k+=1 time.sleep(45)
Итогом этого этапа стал обучающий датасет из 2146. К сожалению сервисы периодически блокируют подключение.
Выбор моделей и обучение
Для обучения я выбрал три модели с архитектурой gpt:
ai-forever/rugpt3medium_based_on_gpt2
ai-forever/rugpt3large_based_on_gpt2
ai-forever/ruGPT-3.5-13B
Первые две модели я выбрал так как они не очень "тяжелые" и их можно обучить прямо в google colab. Третья же достаточно большая и сильная модель, для нее пришлось искать отдельный сервер, но хотелось посмотреть как она сможет показать себя на данной задаче.
Весь дальнейший код по обучению я продемонстрирую на примере модели ai-forever/rugpt3medium_based_on_gpt2, потому как кроме названия модели сам код ничем не отличается.
Подготовка данных и загрузка датасета
Весь датасет у меня после этапа генерации хранился в словаре. Считываю его и записываю в формат pandas dataframe.
with open('dataset', 'rb') as file: dct = pickle.load(file) df_data = pd.DataFrame([(key, val[0]) for key, val in dct.items() if len(val)==1], columns=['promt', 'text']) df_data['text'] = df_data['text'].apply(lambda x: x.replace('Конечно, я могу создать небольшой рассказ на русском языке.', '')) df_data['text'] = df_data['text'].apply(lambda x: x.replace('Конечно, я могу создать рассказ на русском языке.', '')) df_data['train_text'] = df_data['promt']+'\n\nРассказ: '+df_data['text']+'<|endoftext|>'
Далее разбиваю данные на обучающую и тестовую выборку (желательно с указанием параметра random_state для воспроизводимости результатов)
df_train, df_val = train_test_split(df_data[['train_text']], test_size=0.1, random_state=42)
Создаю функции подготовки данных для использования моделью
from datasets import Dataset, DatasetDict def preprocess_examples(examples, tokenizer): # преобразует данные в тензор имеющий входные метки после токенезатора и такие же выходные model_inputs = tokenizer( text = examples["train_text"], return_tensors='pt', truncation=True, padding='max_length', max_length=1024, ) labels = tokenizer(examples["train_text"], truncation=True, padding='max_length', max_length=1024, return_tensors='pt') model_inputs["labels"] = labels["input_ids"] return model_inputs def read_splits(train_df, dev_df): # преобразует данные в формат необходимый для обучения train_df_, val_df = train_test_split(train_df, test_size=0.2, random_state=42) train, dev, test = map(Dataset.from_pandas, (train_df_, val_df, dev_df)) return DatasetDict(train=train, dev=dev, test=test)
Загружаю модель, используя библиотеку transformers от huggingface
from transformers import AutoTokenizer, AutoModelForCausalLM name = 'ai-forever/rugpt3medium_based_on_gpt2' tokenizer = AutoTokenizer.from_pretrained(name, cache_dir="./models") model = AutoModelForCausalLM.from_pretrained('./fif_step', # low_cpu_mem_usage=True, device_map='auto', load_in_4bit=True, # позволяет сократить размер модели, переводя 8-битовые целые числа к 4-битному формату cache_dir="./models",)
Для того, что бы модель обучалась быстрее использую технологию peft так же от huggingface, позволяющую настроить конкретные слои, которые будут обучатся
# Данный код позволи указать только те слои которые будут обучатся peft_config = LoraConfig( task_type=TaskType.CAUSAL_LM, inference_mode=False, r=4, lora_alpha=16, lora_dropout=0.1, target_modules = ['c_attn', 'c_proj', 'c_fc', 'c_proj'] ) model_peft = get_peft_model(model, peft_config) model_peft.print_trainable_parameters() #Выведет количество параметрок, которые будут обучатся
Последние преобразования над входными данными
from functools import partial tokenized_splits = splits.map( partial(preprocess_examples, tokenizer=tokenizer), batched=True, remove_columns=["train_text"], keep_in_memory=True, )
И непосредственно само обучение модели, с параметрами
seed = 1 N_EPOCHS = 1 learning_rate = 1e-5 weight_decay = 1e-4 batch_size = 16 data_collator = DataCollatorWithPadding(tokenizer, pad_to_multiple_of=8) training_args = TrainingArguments( output_dir=f"checkpoints_1/", overwrite_output_dir=True, evaluation_strategy="epoch", per_device_train_batch_size=batch_size, per_device_eval_batch_size=batch_size, learning_rate=learning_rate, weight_decay=weight_decay, num_train_epochs=N_EPOCHS, warmup_ratio=0.1, optim="adamw_torch", save_strategy="epoch", save_total_limit=1, seed=seed, dataloader_num_workers=4, group_by_length=True, report_to="none", load_best_model_at_end=True, ) trainer = Trainer( model=model_peft, args=training_args, train_dataset=tokenized_splits['train'], eval_dataset=tokenized_splits['dev'], tokenizer=tokenizer, data_collator=data_collator, ) train_result = trainer.train()
В итоге у меня получились 3 обученные модели:
ai-forever/rugpt3medium_based_on_gpt2
обучалась 10 эпох
Метрика потерь для тестовой выборки лучшей эпохи обучения: 3.12
Лучшая стратегия для генерации: Семплирование с лучами
ai-forever/rugpt3large_based_on_gpt2
обучалась 10 эпох
Метрика потерь для тестовой выборки лучшей эпохи обучения: 1.92
Лучшая стратегия для генерации: Семплирование с лучами
ai-forever/ruGPT-3.5-13B
обучалась 2 эпох
Метрика потерь для тестовой выборки для 2 эпохи обучения: 0.31
Лучшая стратегия для генерации: Семплирование с лучами
Стратегии генерации и примеры
Так как я хотел получать разные тексты на один и тот же входной набор данных, при этом так же хотелось что бы язык был более живой, и текст более разнообразный, лучше всего было использовать стратегию генерации на основе семплирования. Подробнее про различные стратегии генерации можно прочитать тут.
У обученных моделей с использованием технологии peft есть особенность что их нужно загружать используя специальный класс
from transformers import AutoTokenizer from peft import AutoPeftModelForCausalLM tokenizer = AutoTokenizer.from_pretrained(name, cache_dir="./models") model_small = AutoPeftModelForCausalLM.from_pretrained(name, load_in_4bit=True, cache_dir="./models", device_map='auto',)
Если использовать класс, с помощью которого модель загружалась на обучение, то адаптеры peft не подхватятся, и просто загрузится модель из репозитория
Пример использованных настроек генерации
output = model_small.generate(**inputs, top_k=6, top_p=0.9, num_beams=10, # num_return_sequences=3, do_sample=True, no_repeat_ngram_size=2, temperature=1.5,max_length=max_l,early_stopping =True )
Примеры хороших генераций полученных от моделей
ai-forever/rugpt3medium_based_on_gpt2
Имя героя: Чарна
Раса героя: Эльфы
Способности героя: Стихии
Место действия: Заброшенное кладбище
Великий артефакт: Меч небесного всадника
Противник: Король огров
Чарна - один из самых могущественных волшебников Средиземья. Он был одним из тех, кто сражался на стороне гномов против орков и эльфов. Но в один прекрасный день он узнал о том, что на его стороне сражаются не только гномы, но и эльфы. Чарну это не обрадовало, и он решил отомстить за своих друзей, которые погибли, сражаясь с орками и эльфами. И вот, когда он был уже готов отправиться в путь, на него напали орки и эльфийские воины, которых он не мог победить, так как у него не было волшебных мечей. В конце концов, он все-таки одолел врагов и победил их, после чего вернулся в свое королевство, где и встретил свою возлюбленную, которая полюбила его и стала его женой, а затем и его сына, который стал великим воином и великим волшебником. После того, как они поженились, у них родился сын, которого назвали Чарном, в честь своего отца.
С тех пор прошло много времени, прежде чем они снова встретились, чтобы сразиться друг с другом. На этот раз Чарн был намного сильнее, чем в первый раз и смог победить всех своих врагов, кроме одного.
ai-forever/rugpt3large_based_on_gpt2
Имя героя: Чарна
Раса героя: Эльфы
Способности героя: Магия
Место действия: Заброшенное кладбище
Великий артефакт: Меч небесного всадника
Противник: Король огров
Давным-давно, в далёкой-далёкой галактике, где-то на границе между мирами, жила-была маленькая девочка. Её родители погибли, когда она была ещё совсем маленькой, и она осталась одна-одинешенька в этом огромном мире, полном опасностей и приключений.
Однажды, во время одного из таких приключений, девочка случайно наткнулась на древний артефакт, который назывался Меч Небесного Всадника. С его помощью можно было не только сражаться с врагами, но и путешествовать по мирам, не боясь быть пойманной или убитой. Этот меч был настолько древним, что никто не мог с уверенностью сказать, сколько ему лет. Но, несмотря на это, он был очень могущественным оружием, способным уничтожить любого, кто осмелился бы приблизиться к нему. И, конечно же, девочке очень хотелось завладеть этим мечом, ведь она очень любила приключения и мечтала о них всю свою жизнь. Поэтому она решила отправиться на поиски этого легендарного меча, чтобы узнать, как он выглядит и как с ним нужно обращаться. В конце концов, она всё-таки нашла его, спрятанного глубоко под землёй, недалеко от заброшенного кладбища.
ai-forever/ruGPT-3.5-13B
На заброшенном кладбище, где уже давно никто не хоронил умерших, возвышался величественный памятник. На нем была высечена надпись: "Здесь покоится великий герой, который спас наш мир от нашествия демонов. Он пожертвовал своей жизнью, чтобы мы могли жить в мире и спокойствии. Его имя - Чарн".
Однажды ночью, когда на небе сияла полная луна, на кладбище появился всадник на белом коне. Это был молодой эльф, одетый во все белое, с длинными светлыми волосами, заплетенными в косу, и голубыми глазами.
Всадник спешился, подошел к памятнику и прочитал надпись на нем. Затем он достал из ножен меч и вонзил его себе в сердце. Из раны хлынула кровь, но эльф не издал ни звука. Через несколько минут он упал на землю, не подавая признаков жизни. Вокруг него начала образовываться лужа крови, которая становилась все больше и больше, пока не достигла надгробия. Внезапно, из лужи крови поднялась фигура, закутанная в черный плащ с капюшоном. Она подошла к эльфу и склонилась над ним.
Выводы
Что удалось:
Разобраться с генерацией текстов и обучением моделей
Протестировать различные стратегии генерации
Обучить модели, способные генерировать фэнтезийные тексты
Над чем еще надо работать:
Генерируются тексты а не рассказы. Отсутствует законченность истории.
Сгенерированные тексты учитывают мало параметров изначальной затравки.
Нужно дольше обучать модель.
