Привет, ты уже знаешь, как генерировать новости с помощью Марка. Теперь расскажем, как же так получилось, что мы обучили языковую модель генерации новостей.
Время пришло!
Немного истории
В статье «Тестим Марка: как происходит генерация новостей» ты узнал, что люди придумывают языковые модели, чтобы начать общаться с компьютерами и поддерживать двустороннюю коммуникацию.
Огромную популярность сейчас приобрела модель ChatGPT, потому что она умеет вести диалог с человеком и поддерживать контекст. Но мало кто знает, что эта модель — немного доработанная версия модели GPT-3, которая лежит на hugging face ещё c 2020-ого.
Инфа для тех, кто знает: большинство телеграм‑ботов с названием ChatGPT — это не ChatGPT, а GPT-3 c правильной входной формулировкой, потому что ко второй модели доступ сильно проще из‑за меньшего потока людей, а генерируют они обе практически одно и то же)
Представляешь, как давно на самом деле существуют такие технологии?
Интересные факты:
GPT-3 пишет эссе о себе самой (проверь дату выпуска статьи и удивись)
Тем, кто думает, что ключевое отличие ChatGPT от GPT-3 в том, что первая умеет вести диалог с пользователем — вот вам видео‑интервью, разрушающее эти стереотипы:
GPT-3 не прошла тест Тьюринга в отличие от ChatGPT ?, которая стала второй языковой моделью, подающей признаки интеллекта (странный факт, учитывая, что они генерируют почти одно и то же).
Что интересно: первая модель, которая прошла этот знаменитый тест — это ИИ, который берёт на себя личность 13-летнего украинского мальчика, возраст, который, по мнению разработчиков, повышает вероятность обмана людей. Вот пример диалога (странного, но явно неосмысленного).
Обсудили исторический контекст, теперь перейдём к самой модели.
Языковая модель GPT-3
GPT-3 — это большая модель, которая умеет генерировать текст по запросу. Иными словами, это ИИ, который берёт строку текста и стремится предсказать, какое слово «должно» (или, скорее всего, будет) идти дальше.
Чтобы данная языковая модель хорошо улавливала семантику слов, в неё заключили 175 миллиардов обучаемых параметров — это те самые параметры для слов, чтобы компьютер мог «почувствовать» их значение. Но, чтобы модель была действительно умной, разработчики из OpenAI заставили GPT-3 «просматривать» миллиарды слов в Интернете, в новостных статьях, сообщениях на форумах, веб‑сайтах и т. д.
❓ Зачем? Представь себе человека, который за всю свою жизнь прочитал только Колобка, это была единственная информация, которую он “потребил”. Как думаешь, можно ли с ним поговорить о космосе? Он ведь даже слова такого не знает. Так и с языковой моделью: ей нужно понять мировой контекст, какие есть слова, знания, формулировки, правила языка и прочее.
В итоге после обучения получилась модель, которая много дней потребляла информацию из интернета (600 гигабайт текста) и весит почти 3 гигабайта. Эта языковая модель умеет генерировать текст и понимать запрос человека.
Обучение vs дообучение
Факт: чтобы обучать большие и мощные модели (такие как GPT-3), нужно много денег и времени. Если не верится, вот тут попытка обучить GPT-3 для русского языка, только в 13 раз меньше.
Для нашего генератора новостей мы использовали ruGPT-3 среднего размера, и, если бы мы обучали её сами, мы бы потратили 5.6 млн рублей и больше 30 тысячи часов. Неплохо?
НО! Из‑за того, что тратить такое огромное количество ресурсов могут позволить себе только корпорации, а исследовать хочется всем, было найдено решение: дообучение (fine‑tuning).
? Основная идея дообучения: нам не нужно заново учить языковую модель понимать мировой контекст, нужно сфокусировать её внимание на той информации, которая нас интересует.
Так и человек: он один раз научился читать и понимать значения слов в языке, а если он хочет стать программистом, то ему нужно сфокусироваться на потреблении релевантной информации, попутно запоминая новые словечки.
Взрыв мозга: такие большие нейросети содержат внутри специальный математический слой, названный Attention, через который пропускается весь текст для обучения и этот слой буквально заставляет модель уделить внимание нужной информации (ключевым словам). Гениально, правда?
Разрабатываем новостник
Целиком код с обучением и генерацией тут — COLAB
Пайплайн для разработки генератора такой:
Найти данные: собрать новости по интересующей нас тематике (например, IT), отделяя заголовок от описания (как это сделать не ручками будет в следующей статье)
Например, как тут:
Обработать данные: скачиваем эту таблицу, сохраняя её где-нибудь, где будут храниться все наши файлы, и записываем в переменную
data
в формате датафрейма (та же табличка, только по-умному):!wget <https://www.dropbox.com/s/89rmm4wucjve2ll/news.csv?dl=0> -O /content/news.csv my_path = '/content/' # путь, где будут сохраняться все файлы data = pd.read_csv(f'{my_path}news.csv')
Теперь нужно почистить наши данные:
Удаляем дубликаты (вдруг в наших данных есть две одинаковые новости?) и пустые строчки:
data.drop_duplicates(subset = ['title'], inplace=True) data.dropna(inplace=True)
Пишем общую функцию, куда можно добавить разные условия очистки. Например, избавляться от слишком коротких и поэтому несодержательных описаний (если их оставить, модель будет путаться и генерировать затравки вместо фактов). Каждое короткое описание мы заменяем специальным значением NaN, которое обозначает пустышку:
def clear_data(text: str): # если длина описания меньше 20 слов-удаляем новость if len( text.split(' ')) < 20: return np.nan return text
Применяем эту функцию для очистки нашего столбца с описаниями и удаляем строки, где теперь лежат пустышки:
data['text'] = data['text'].apply(clear_data) data.dropna(subset=['text'], inplace=True)
Подключаемся к вычислительным мощностям:
DEVICE = torch.device("cuda") if torch.cuda.is_available() else None
В колабе можно сменить среду выполнения и подключиться к аппаратному ускорителю GPU, тогда твой код будет выполняться в разы быстрее. Так что если обучать самому: попробуй подключить видеокарту локально (гугл в помощь) или запросить мощности у колаба.
Импортируем нашу большую модель по API, как мы это делали в генерации. У нас появляются две главные сущности: модель и токенайзер.
from transformers import GPT2LMHeadModel, GPT2Tokenizer model_name_or_path = "sberbank-ai/rugpt3medium_based_on_gpt2" tokenizer = GPT2Tokenizer.from_pretrained(model_name_or_path) model = GPT2LMHeadModel.from_pretrained(model_name_or_path).to(DEVICE)
Добавляем специальные токены: этот шаг тоже был в генерации, только теперь нам нужно расширить размер входящих токенов в модель, чтобы она их тоже учла при обучении (и потом генерации):
SPECIAL_TOKENS = {'bos_token':'<bos>','eos_token' :'<eos>', 'pad_token':'<pad>', 'sep_token': '<sep>'} tokenizer.add_special_tokens(SPECIAL_TOKENS) model.resize_token_embeddings(len(tokenizer))
Создаем специальный датасет — это класс, к которому модель будет обращаться по ключевым функциям (важно: это просто структура, которая нужна нашей языковой модели в дальнейшем — лучше её не менять):
class myDataset(Dataset): def __init__(self, data, tokenizer, gpt2_type="gpt2", max_length=150): self.tokenizer = tokenizer # the gpt2 tokenizer we instantiated self.input_ids = [] self.attn_masks = [] for i in data.index.to_list(): title = data['title'][i] description = data['text'][i] if type(data['text'][i]) == str else '' form = '<bos>'+ title + '<sep>' + description + '<eos>' encodings_dict = tokenizer(form, truncation=True, max_length=max_length, padding="max_length") self.input_ids.append(torch.tensor(encodings_dict['input_ids'])) self.attn_masks.append(torch.tensor(encodings_dict['attention_mask'])) def __len__(self): return len(self.input_ids) def __getitem__(self, idx): return { 'input_ids': self.input_ids[idx], 'attn_masks': self.attn_masks[idx] }
Когда мы инициализируем наш датасет (
def __init__
), мы проходимся построчно по нашей табличке, берём оттуда тайтл и описание и оформляем их в одну текстовую последовательность вот так:? форма =
<bos>
заголовок<sep>
описание<eos>
<bos>
специальный токен начала предложения;<sep>
токен, который показывает нам и языковой модели, что здесь заканчивается заголовок и начинается описание;<eos>
специальный токен конца предложения.
(именно такие текстовые последовательности генерирует потом наша модель)
Мы делаем знакомые нам преобразования токенайзером (и внутри него энкодером). → Мы пилим слова на кусочки и присваиваем им уникальные значения, по которым можно найти вектора из чисел, задающие семантику.
encodings_dict = tokenizer(form, truncation=True, max_length=max_length, padding="max_length")
С некоторыми нововведениями:
Каждая текстовая последовательность может быть разной длины (какие-то новости покороче, какие-то — длиннее), но так как все, что происходит внутри языковых моделей во время обучения, математические операции (а именно - перемножение матриц), то удобнее, чтобы все предложения были одной длины. Поэтому появляется параметр
padding
:⇒ фиксируем максимальную адекватную длину нашей текстовой последовательности: у нас большинство текстовых последовательностей содержат около 130 слов, но иногда встречаются очень длинные новости.
Оптимальный вариант = установить максимальную длину предложения в 150 слов.
⇒ Те последовательности, которые короче максимума, будут дополнены специальным токеном
<pad>
⇒ Те последовательности, которые длиннее, будут обрезаны
В результате все текстовые последовательности будут максимально короткими и информативными, и, что очень важно, одной длины!
На выход у нас формируется словарь
encodings_dict
, где по ключу'input_ids'
лежат те самые, уникальные значения для кусочков, из которых мы можем составить текстовую последовательность в машинном переводе:self.input_ids.append(torch.tensor(encodings_dict['input_ids'])) self.attn_masks.append(torch.tensor(encodings_dict['attention_mask']))
Но еще там есть ключ
'attention_mask'
: его задача — показать, где заканчивается смысловое предложение и начинаются токены подгонки под размер (<pad>
).Это вектор, в котором хранятся значения 1 и 0, обозначающие:
1 — модель, обрати внимание на это слово, оно важное и
0 — не смотри, это просто пустота.
Запускаем код — получаем сформированный датасет:
train_dataset = myDataset(data, tokenizer)
Интересный факт
Вот этот кусок в цикле
for
, проходящему по твоему датафрейму построчно — единственная часть, где тебе нужно что-то изменить под другую задачу.title = data['title'][i] description = data['text'][i] if type(data['text'][i]) == str else '' form = '<bos>'+ title + '<sep>' + description + '<eos>'
Например, у тебя просто куски текста-четверостишья и твоя задача научить модель генерировать их. Тогда в твоём датафрейме построчно будут лежать такие куски стихотворений в колонке
quatrain
. А твой код будет выглядеть так:text = data['quatrain'][i] form = '<bos>' + text + '<eos>'
А всё остальное останется неизменным!
Пилим наш огромный датасет на кусочки, которые наша языковая модель будет поэтапно обрабатывать:
data_collator = DataCollatorForLanguageModeling(tokenizer=tokenizer, mlm=False)
Задаём параметры дообучения (можно просто скопировать и поиграться)
training_args = TrainingArguments( output_dir=f'{my_path}Checkouts', #The output directory overwrite_output_dir = True, #overwrite the content of the output directory num_train_epochs = 10, # number of training epochs per_device_train_batch_size = 3, # batch size for training per_device_eval_batch_size = 3, # batch size for evaluation warmup_steps = 100,# number of warmup steps for learning rate scheduler gradient_accumulation_steps = 1, # to make "virtual" batch size larger save_steps = 3000 )
Тут просто детали нашего обучения: количество эпох, сколько статей одновременно мы рассматриваем при обучении и оценке качества обучения + тебе нужно периодически сохранять свою модель, чтобы, если вдруг всё упадет, тебе не приходилось начинать заново, а была возможность начать там, где закончил.
Если твоя видеокарта вдруг не справляется (выдает ошибку), то стоит уменьшить батч)
А этот код просто надо копирнуть, сюда ты подтягиваешь ключевые штуки для языковой модели:
trainer = Trainer( model=model, args=training_args, data_collator=data_collator, train_dataset=train_dataset, # Optimizer and lr scheduler optimizers = (torch.optim.AdamW(model.parameters(),lr=1e-5),None) )
Самое сложное: обучаем — это может занять мнооооооого времени ?
trainer.train()
И после обучения обязательно сохраняем модель:
tokenizer.save_vocabulary(f'{my_path}tokenizer') trainer.save_model(f'{my_path}model_with_summary')
А дальше ты можешь её импортнуть вот так:
tokenizer = GPT2Tokenizer.from_pretrained(f'{my_path}tokenizer') model = GPT2LMHeadModel.from_pretrained(f'{my_path}model_with_summary').to(DEVICE)
И добавить всё то, что было в статье про генерацию, чтобы посмотреть, что же ты там наобучал. Не всё получится с первого раза, но мы в тебя верим!
Милая напоминашка, если ты потеряшка: целиком код с обучением и генерацией на COLAB
Автор статьи: @anyaschenikova