Как сделать своего “Марка”? Обучение

от автора

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

Время пришло!

Немного истории

В статье «Тестим Марка: как происходит генерация новостей» ты узнал, что люди придумывают языковые модели, чтобы начать общаться с компьютерами и поддерживать двустороннюю коммуникацию.

Огромную популярность сейчас приобрела модель ChatGPT, потому что она умеет вести диалог с человеком и поддерживать контекст. Но мало кто знает, что эта модель — немного доработанная версия модели GPT-3, которая лежит на hugging face ещё c 2020-ого.

Инфа для тех, кто знает: большинство телеграм‑ботов с названием ChatGPT — это не ChatGPT, а GPT-3 c правильной входной формулировкой, потому что ко второй модели доступ сильно проще из‑за меньшего потока людей, а генерируют они обе практически одно и то же)

Представляешь, как давно на самом деле существуют такие технологии?

Интересные факты:

  1. GPT-3 пишет эссе о себе самой (проверь дату выпуска статьи и удивись)

  2. Тем, кто думает, что ключевое отличие ChatGPT от GPT-3 в том, что первая умеет вести диалог с пользователем — вот вам видео‑интервью, разрушающее эти стереотипы:

  3. GPT-3 не прошла тест Тьюринга в отличие от ChatGPT ?, которая стала второй языковой моделью, подающей признаки интеллекта (странный факт, учитывая, что они генерируют почти одно и то же).

  4. Что интересно: первая модель, которая прошла этот знаменитый тест — это ИИ, который берёт на себя личность 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

Пайплайн для разработки генератора такой:

  1. Найти данные: собрать новости по интересующей нас тематике (например, IT), отделяя заголовок от описания (как это сделать не ручками будет в следующей статье)

    Например, как тут:

  2. Обработать данные: скачиваем эту таблицу, сохраняя её где-нибудь, где будут храниться все наши файлы, и записываем в переменную 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)
  3. Подключаемся к вычислительным мощностям:

    DEVICE = torch.device("cuda") if torch.cuda.is_available() else None

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

  1. Импортируем нашу большую модель по 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)
  2. Добавляем специальные токены: этот шаг тоже был в генерации, только теперь нам нужно расширить размер входящих токенов в модель, чтобы она их тоже учла при обучении (и потом генерации):

    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))
  1. Создаем специальный датасет — это класс, к которому модель будет обращаться по ключевым функциям (важно: это просто структура, которая нужна нашей языковой модели в дальнейшем — лучше её не менять):

    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>'

    А всё остальное останется неизменным!

  1. Пилим наш огромный датасет на кусочки, которые наша языковая модель будет поэтапно обрабатывать:

    data_collator = DataCollatorForLanguageModeling(tokenizer=tokenizer, mlm=False)
  2. Задаём параметры дообучения (можно просто скопировать и поиграться)

    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) )
  3. Самое сложное: обучаем — это может занять мнооооооого времени ?

    trainer.train()
  4. И после обучения обязательно сохраняем модель:

    tokenizer.save_vocabulary(f'{my_path}tokenizer') trainer.save_model(f'{my_path}model_with_summary')
  5. А дальше ты можешь её импортнуть вот так:

    tokenizer = GPT2Tokenizer.from_pretrained(f'{my_path}tokenizer') model = GPT2LMHeadModel.from_pretrained(f'{my_path}model_with_summary').to(DEVICE)

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

Милая напоминашка, если ты потеряшка: целиком код с обучением и генерацией на COLAB

Автор статьи: @anyaschenikova


ссылка на оригинал статьи https://habr.com/ru/post/719874/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *