Pull to refresh

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

Level of difficultyMedium
Reading time8 min
Views9.7K

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

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

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

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

Огромную популярность сейчас приобрела модель 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

Tags:
Hubs:
Total votes 8: ↑6 and ↓2+4
Comments4

Articles