Приключения в неконтролируемом обучении

Обучение без присмотра в подзаголовке относится к моему собственному обучению без присмотра в попытке выяснить, как заставить библиотеку Трансформеры Hugging Face работать с TensorFlow. Я сделал этот урок и кучу других, и я просмотрел все статьи, но все еще чувствовал, что делаю это без присмотра.

Преамбула: Трансформеры

Насколько я понимаю, модель трансформатора — это модель на основе нейронной сети, которая является самореферентной — она может принимать решения и прогнозы на основе данных, предшествующих и следующих за рассматриваемым элементом. Таким образом, для генерации текста модель преобразователя будет делать предсказание, скажем, слова на основе слов, которые ему предшествовали, или даже на основе слов, которые шли после него, или удваивать даже то и другое. Таким образом учитывается контекст. Учебная страница TensorFlow по этой теме довольно хороша, и мне жаль, что я не обнаружил ее намного раньше, чем я.

Но сначала я как бы вслепую бросился использовать пакет transformers Hugging Face, чтобы точно настроить предварительно обученную модель GPT-2 на пользовательских текстовых данных для генерации текста на елизаветинском английском языке из подсказок, с реальной сложной целью иметь модель напишет мне несколько сонетов (до этого я еще не дошел).

Это моя игра за игрой, как заставить библиотеку transformers работать с пользовательскими данными для генерации текста из GPT-2 с использованием серверной части TensorFlow.

1. Установите

pip install transformers

Вам, вероятно, также понадобится tensorflow, и вам абсолютно точно понадобится datasets, так что вы тоже можете приобрести их. И если ничего не помогает, создайте новую среду Python с вашим любимым менеджером среды Python, чтобы установить вышеперечисленное. В настоящее время я использую pyenv-virtualenv. Установите минимально необходимые зависимости и вспомогательные пакеты в этой среде, чтобы избежать конфликтов версий.

2. Настройте среду

Начните с импорта:

# tensorflow back-end, I think this is needed for the transformers model
import tensorflow as tf

# data formatting for the model
from datasets import Dataset, DatasetDict

# tokenizer
from transformers import AutoTokenizer

# collator
from transformers import DataCollatorForLanguageModeling

# model and optimizer
from transformers import TFAudioModelForCausalLM, AdamWeightDecay

Я установил константу для типа модели, так как ее нужно передать в несколько мест:

# can be gpt2-medium, gpt2-large, etc.
# Plain old 'gpt2' is good for testing as an epoch will only take a few minutes 
# (depending on input size)
MODEL_TYPE = 'gpt2'

Загрузить пользовательские данные. Здесь я загрузил немного елизаветинской поэзии, которую я предварительно очистил, чтобы удалить любой посторонний текст (не оставив ничего, кроме милого, милого стиха) и преобразовать любые сумасшедшие апострофы в более репрезентативную форму (например, «devour'd» стало «пожирался», «tak'st» стало «takest» и т. д.):

paths = ["<all your texts here, mine were like 'shakespeare-sonnets.clean.txt', 'browning-sonnets.clean.txt', etc.>"]

text = list()

for path in paths:
  with open(path, 'r') as f:
    text.append(f.read())

fulltext = '\n'.join(text)

3. Предварительная обработка

3а. Оставаться в пределах токенов

Здесь вы можете нарезать его, как хотите. Я думаю, что модель gpt2 предполагает до 1024 токенов на обучающую последовательность (может быть меньше, 512 — еще один распространенный предел), поэтому эти тексты нужно разделить на более мелкие фрагменты. Для этого примера я разделю на предложения:

# regex library - put this import at top
import re

# regex for sentences. Captures minimum chars between a word character and any of # '.', '?', '!'. 're.S' flag allows searching across newlines. 
# Quick and dirty... and effective.
RE_SENTENCE = re.compile(r'\w.*?[.?!]', re.S)

# regex for whitespace
RE_WHITESPACE = re.compile(r'\s+')

# extract sentences as list
sentences = RE_SENTENCE.findall(fulltext)

# collapse all whitespace in each sentence into a single space
sentences = [RE_WHITESPACE.sub(' ', sentence) for sentence in sentences]

3б. Разделение поезда/теста

Поскольку я не мог найти способ обойти это, мне пришлось использовать библиотеку datasets для соответствующего форматирования данных, а это означало, что мне нужно было разделить свои данные на обучающие и тестовые наборы перед преобразованием в формат Dataset. Спойлер: я никогда не добираюсь до тестирования или оценки, поэтому я не думаю, что это разделение необходимо (я все равно делаю это, потому что я ожидаю, что оно сломается, если я этого не сделаю)!

Я использую scikit-learn train-test-split для разделения:

# tts library - put this import at top
from sklearn.model_selection import train_test_split

# use a split size that works for your data
s_train, s_test = train_test_split(sentences, test_size=0.05)

3в. Преобразование в формат наборов данных

Я буду сожалеть об этом: я не мог пройти мимо пакета datasets, и как бы я ни пытался, transformers продолжал выдавать ошибку, если я не отформатировал данные точно правильно, что означало использование классов Dataset и DatasetDict. Увы! Вот так:

# convert train and test data to Dataset format - basically format the 
# text as a single column table with column name "text", plus some extra stuff
train_dataset = Dataset.from_dict({'text': s_train})
test_dataset = Dataset.from_dict({'text': s_test})

# combine into DatasetDict format
datasets = DatasetDict({'train': train_dataset, 'test': test_dataset})

4. Процесс моделирования

Моделирование здесь включает в себя множество движущихся частей. Одна ошибка в любом из них, и все развалится. Не путайте.

4а. Токенизация

Сначала создайте токенизатор (он понадобится вам сейчас и позже):

# create tokenizer from model type as specified in constant above
tokenizer = AutoTokenizer.from_pretrained(MODEL_TYPE)

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

# a function that DatasetDict will use to process the text data
def token_preproc(data):
  return tokenizer(data['text'])

Теперь мы можем токенизировать текст:

# map the preceding function to the datasets
# the transformers tutorial online has some other parameters you can 
# use but I think they are optional
tokened_data = datasets.map(token_preproc, remove_columns=['text'])

Теперь мы можем проверить токенизированные данные:

# random - import at top
import random

for _ in range(10):
  n = random.randint(0, len(tokened_data['train']))
  print(tokenizer.convert_ids_to_tokens(tokened_data['train'][n]['input_ids'])

Надеюсь, это даст вам кучу списков искаженных слов.

4б. Коллатор

Следующий шаг — дополнить последовательности токенов, чтобы они были одинаковой длины. Класс DataCollatorForLanguageModeling сделает для вас один, который вы будете использовать позже:

# create the collator, which will be used a bit later
# do not expect this to work like tokenizer above
collator = DataCollatorForLanguageModeling(tokenizer=tokenizer, mlm=False, return_tensors='tf')

Важное примечание: используйте «tf» для return_tensor здесь (и ниже в тестовом коде)! Я думаю, что в официальном руководстве мне было сказано использовать вместо этого «np», из-за чего я потратил впустую по крайней мере 24 часа своей жизни, пытаясь отладить проблемы с более поздним выводом тестирования!

4в. Настройка модели

В отличие от токенизации, мы не сопоставляем данные перед настройкой модели. На самом деле мы настроим модель, а затем воспользуемся подборщиком, когда будем снова конвертировать наборы данных. Сначала настройте модель:

# instantiate the model as determined by MODEL_TYPE constant
model = TFAutoModelForCausalLM.from_pretrained(MODEL_TYPE, pad_token_id = tokenizer.eos_token_id)

4д. Преобразование данных снова

Теперь мы снова конвертируем данные, используя коллатор:

# I thought we set this up in the model, but needs to be set here too to 
# avoid a warning
tokenizer.pad_token = tokenizer.eos_token

# new train set
tf_train_set = model.prepare_tf_dataset(tokened_data['train'], shuffle=True, batch_size=16, collate_fn=collator)

# new test set
tf_test_set = model.prepare_tf_dataset(tokened_data['test'], shuffle=False, batch_size=16, collate_fn=collator)

4е. Настройте оптимизатор и скомпилируйте модель

Мы почти там:

# set up optimizer using recommended parameter values
optimizer = AdamWeightDecay(learning_rate=2e-5, weight_decay_rate=0.01)

# compile the model with the optimizer
model.compile(optimizer=optimizer)

4ф. Подходит для модели

Это заняло у меня несколько минут в эпоху:

# fit model
model.fit(tf_train_set, validation_data=tf_test_set, epochs=8)

Ура! Достаточно долго. Мне потребовалось несколько дней, чтобы добраться до этого места без ошибок.

5. Протестируйте модель

Я написал функцию для тестирования:

# function to get predicted text
def test(text, max_new=50, temp=1, top_k=50, rep_penalty=1.5, len_penalty=0.75, n_seq=1):
  # USE 'tf' HERE FOR return_tensors!!! Do not do like I did and try 'np'!
  tokened = tokenizer(text, return_tensors='tf')
  output = model.generate(**tokened,
                          do_sample=True, # this is what took a day off my life
                          max_new_tokens=max_new,
                          temperature=temp,
                          top_k=top_k,
                          repetition_penalty=rep_penalty,
                          length_penalty=len_penalty,
                          num_return_sequences=n_seq)
  return tokenizer.decode(output[0], skip_special_tokens=True)

И теперь вы должны быть в состоянии генерировать текст, вводя начальный текст в эту функцию test(). Например, для подсказки Tomorrow I will я получил:

Tomorrow I will not speak, nor weep; For my soul is lost through the gloom of night...

Этот текст продолжается до max_new_tokens после исходной подсказки, поэтому в более поздней версии я просто обрезаю его после первого предложения.

Обратите внимание, что с параметрами model.generate происходит много действий. Я провел несколько часов, пытаясь понять, что они сделали, часто в результате чего возникала какая-то ошибка. Но на самом деле ключ в том, чтобы не использовать 'np' в качестве аргумента return_tensors — вместо этого используйте 'tf'! Когда do_sample становится True, все работает намного лучше.

Послесловие: Сохранение модели

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

# import os package for helpful path utilities - put at top of code
import os

# somewhere at the top of the code, set up a model base directory and give this
# run a unique name
DIR_MODEL = './models/' # or wherever
MODEL_NAME = 'sentences' # or whatever you want

# combine into a full path where we'll save the model, adjust to fit your taste
MODEL_FULL_PATH = os.path.join(DIR_MODEL, f'{MODEL_TYPE}.{MODEL_NAME}')

# to load the model: 
# before instantiating the model, check that path exists 
# if it doesn't exist, instantiate fresh, else load pre-saved model
if not os.path.exists(MODEL_FULL_PATH):
  model = TFAutoModelForCausalLM.from_pretrained(MODEL_TYPE)
else:
  model = TFAutoModelForCausalLM.from_pretrained(MODEL_FULL_PATH)

# to save the model:
# after fitting the model, save it (here I'm checking whether it has been 
# saved already, but could just force the save as well)
if not os.path.exists(MODEL_FULL_PATH):
  os.makedirs(MODEL_FULL_PATH)
  model.save_pretrained(MODEL_FULL_PATH)

Теперь вы можете быстро загрузить предварительно настроенную модель для проверки любых подсказок. Я скачал некоторые винные обзоры с Kaggle, разделил описания на предложения и использовал часть случайных предложений в качестве подсказок к моей сохраненной модели:

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

  • Вывод: «Темно-малиновые ноты своей твердой линией возвращаются к его сердцу, Когда оно гудит с одинокого ложа, Одинокое небо, еще одинокая роща, И одинокий ручей, одинокий на ветру».

Источник: «В соответствии со стилем винодельни, это вино не вызывает резкости, с ароматами черной лакрицы, темной вишни и древесных специй».

  • Вывод: «Соответствуя стилю винодельни, в этом вине нет и следа красоты — оттенок исчезает, а текстура остается».

Довольно круто, говорю я (и это на относительно небольшом базовом наборе данных и очень небольшом пользовательском наборе данных для тонкой настройки). На мой взгляд, результаты модели в целом более интересны — меньше предсказуемого поэтизма в стиле критика и больше непредсказуемого поэтизма в стиле поэта… Я думаю. Я думаю, что все будущие описания вин должны проходить через точно настроенную модель генерации языка, чтобы действительно дать им то, что je ne sais quoi, если вы понимаете, что я имею в виду.