Итак, на этом этапе работы с FIRE вы должны знать, что нельзя просто передавать предложения в нейронную сеть.
Вам не хватает ключевого слова:
ТЕНЗОРЫ !!!
Звучит пугающе, но на самом деле это просто причудливое слово для обозначения массивов; Поверьте мне.
Я продемонстрирую, как научить RNN писать, как Шекспир.
Сначала мы разберемся с данными.
Затем мы определим архитектуру нашей модели и, наконец, проведем обучение.
Для простоты в сети будет предсказание на уровне персонажа. Итак, учитывая последовательность символов, спрогнозируйте следующий символ
. (Видите, что я там делал?)
Имейте в виду, хотя код в этой статье кажется безупречным, мне пришлось немало потрудиться. Да, даже коллеги-наставники не являются гуру нейронных сетей. Мы еще не достигли цели, но скорее приближаемся к цели ...
Рассмотрение
Во-первых, давайте рассмотрим, что такое архитектура RNN:
Таким образом, у вас есть последовательность из n входов, обозначенных x. И затем вы хотите сгенерировать другую последовательность выходных данных, обозначенную o.
Когда мы развернем это, мы сможем более четко увидеть роль, которую играет скрытое состояние:
Для скрытого слоя, и вы можете думать об этом как о «памяти» сети или, если вы выполняете обработку естественного языка, о «контексте».
Это «упрощенная» версия того, что мы на самом деле будем делать . (В зависимости от вашей точки зрения.)
RNN чрезвычайно полезны для данных временных рядов.
Данные
Где эти данные?
Я получаю данные отсюда:
Http://cs.stanford.edu/people/karpathy/char-rnn/shakespear.txt
Ссылка на резервную копию:
https://drive.google.com/file/d/1zkVhSAO5xddlzg59nGOQHrTZzNXTQjbd/view?usp=sharing
В его блоге также рассказывается об обучении RNN, он ведется на PyTorch и Lua. (Конечно, я буду использовать Keras.)
Поскольку этих данных мало, мы можем просто загрузить все в:
with open("shakespear.txt") as f: text = f.read() uniques = list(set(text)) # consistency for id generation uniques.sort() char2id = {k:i for i, k in enumerate(uniques)} id2char = {i:k for i, k in enumerate(uniques)}
Вы заметите, что это очень наивно, как и остальная часть статьи.
Почему?
Модель должна выучить строчные и прописные буквы. Это нормально, но есть символы новой строки и разные знаки препинания.
Я решил просто пойти на это, и тот факт, что я обнаружил, что данные не очень шумные.
(Это Шекспир! Вы уснете ... без шума. Шучу, мне действительно нравится, чтобы был или не быть.)
Предварительная обработка
Теперь о преобразовании символов в тензоры. Самым простым способом, как вы могли догадаться, является их горячее кодирование.
def to_tensor(char2id, text): tensor = np.zeros((len(text), len(char2id))) for i, e in enumerate(text): tensor[i, char2id[e]] = 1 return tensor
Как мы тестируем или выполняем проверку работоспособности?
def to_text(id2char, tensor): char_list = [] assert len(id2char) == tensor.shape[1] first, second = np.where(tensor == 1) assert first.shape[0] == tensor.shape[0] for i in range(second.shape[0]): char_list.append(id2char[second[i]]) return ''.join(char_list)
Сделайте конечно же декодер! Чтобы проверить, запустите это и убедитесь, что они одинаковы:
# test with first 8 characters print(text[:8]) test = to_tensor(char2id, text[:8]) print(to_text(id2char, test))
Теперь мы можем написать генератор!
Обратите внимание, что я выбрал «контекстные» символы равными 32 для более быстрого обучения. Это то количество символов, которое дается RNN для предсказания следующего символа.
ARBITRARY_LENGTH = 32 def the_generator(char2id, text, batch_size): # minus one for target character l = len(text) - ARBITRARY_LENGTH - 1 picker = np.arange(0, l) np.random.shuffle(picker) up_to = l - batch_size i = 0 while True: if i >= up_to: i = 0 np.random.shuffle(picker) input_list = [] target_list = [] for j in range(batch_size): index = picker[i + j] input_list.append(to_tensor(char2id, text[index : index + ARBITRARY_LENGTH])) target_list.append(to_tensor(char2id, text[index + ARBITRARY_LENGTH])) yield (np.array(input_list), np.array(target_list)) i += batch_size
Отлично, правда? Мы реализовали все по модульному принципу, и нам просто нужно вызвать to_tensor в функции генератора. Теперь все, что нам нужно сделать, это определить и обучить модель.
НЕПРАВИЛЬНЫЙ!
Эта реализация (по крайней мере, для 100 контекстных символов) заключалась в обучении ДЕСЯТЬ ЧАСОВ на эпоху на Google Colab.
То есть без многопроцессорности и GPU Tesla K80.
Поэтому мне пришлось вернуться и изменить генератор данных.
Что я сделал? Что ж, многопроцессорность требует генератора классов. (В противном случае возникает ошибка дублирования данных). Думайте об этом как о реализации списка.
К счастью, следующая партия здесь не зависит от предыдущей.
Обратите внимание, что я забыл отключить случайный выбор для генератора проверки.
В этом случае я правильно настроил партии, так что это несколько или что-то в этом роде.
Вам следует быть более осторожным, чем я, и добавить в конструктор случайный логический параметр.
class DataGenerator(tensorflow.keras.utils.Sequence): ARBITRARY_LENGTH = 32 'Generates data for Keras' def __init__(self, char2id, text, batch_size): 'Initialization' self.char2id = char2id self.text = text self.batch_size = batch_size self.l = len(self.text) - DataGenerator.ARBITRARY_LENGTH - 1 self.picker = np.arange(0, self.l) self.on_epoch_end() self.lenchar2id = len(char2id) def __len__(self): 'Denotes the number of batches per epoch' return int(np.floor(self.l / self.batch_size)) def __getitem__(self, batch_index): 'Generate one batch of data' start_index = batch_index * self.batch_size input_arr = np.zeros((self.batch_size, DataGenerator.ARBITRARY_LENGTH, self.lenchar2id)) target_arr = np.zeros((self.batch_size, self.lenchar2id)) for j in range(self.batch_size): index = self.picker[start_index + j] for k, e in enumerate(self.text[index : index + DataGenerator.ARBITRARY_LENGTH]): input_arr[j, k, self.char2id[e]] = 1 # end for target_arr[j, self.char2id[self.text[index + DataGenerator.ARBITRARY_LENGTH]]] = 1 # end for return input_arr, target_arr def on_epoch_end(self): 'Updates indexes after each epoch' np.random.shuffle(self.picker) ################ # create objects BATCH_SIZE = 32 # Just so happens (1025 - 1 - ARBITRARY LENGTH) % BATCH_SIZE = 0 VAL_SIZE = 1025 train_gen = DataGenerator(char2id, text[:-VAL_SIZE], batch_size = BATCH_SIZE) val_gen = DataGenerator(char2id, text[-VAL_SIZE:], batch_size = BATCH_SIZE)
Поскольку это список вроде:
# to test you can print(train_gen[0])
Оказывается, Google Colab продолжал умирать от меня, потому что, если я бездельничал, он отключается или что-то в этом роде.
Итак, я использовал свои кредиты AWS, которые сидели и ждали, чтобы их использовали более полутора лет!
Решение?
Арендуйте на AWS инстанс «Эластичное компьютерное облако», оптимизированный для вычислений на 16 ЦП!
Он называется c5n.4xlarge.
(К сожалению, я не знал, пока после того, как я смогу потратить свои кредиты на GPU. Если бы у меня был, конечно, я бы использовал GPU.)
Предыдущая конфигурация поезда:
use_multiprocessing = Ложь
worker = 1
max_queue_size = 10
Текущая конфигурация:
use_multiprocessing = True
worker = 32
max_queue_size = 4096
Эффективно?
Да ладно, как я могу забыть, я даже макетную архитектуру не показывал!
Модель
Итак, на самом деле я это реализую так:
Здесь у нас есть классический «БЫТЬ ИЛИ НЕ БЫТЬ… QU», и мы хотим предсказать следующий символ, которым будет E.
Имейте в виду, что веса используются повторно по всем входам. Вспомните первое изображение с одной полосой.
(В противном случае у нас была бы прямая сеть с гораздо большим весом!)
Также обратите внимание, что я также абстрагировал вывод «промежуточного» слоя.
Вот что я реализовал сначала:
input_layer = KL.Input((ARBITRARY_LENGTH, len(char2id)), name="the_input") x = KL.LSTM(62, return_sequences=True, name="intermediate")(input_layer) x = KL.LSTM(62, activation="softmax", name="the_output")(x) model = Model(input_layer, x) model = Model(input_layer, x) model.summary() model.compile(loss='categorical_crossentropy', optimizer='adam', metrics=['accuracy'])
Всего 62000 параметров.
На практике Keras не возвращает каждый вывод по умолчанию. Он возвращает только последний результат.
Я установил return_sequences = True для промежуточного уровня, чтобы реализовать такую архитектуру.
Теперь этот код кажется правильным?
Вы угадали!
НЕПРАВИЛЬНЫЙ!
После обучения в течение 12 эпох или около того, я получил потерю как NAN!
Моя интуиция и исследования подсказали мне, что это могло иметь какое-то отношение к «категориальной кроссентропии» с использованием логарифмов.
На отладку у меня ушло бы 100 лет, если бы я не имел опыта работы с тензорным потоком.
Я серьезно!
Вместо этого мы будем использовать активацию relu:
input_layer = KL.Input((ARBITRARY_LENGTH, len(char2id)), name="the_input") x = KL.LSTM(62, return_sequences=True, name="intermediate")(input_layer) # used to do softmax but experimenting here x = KL.LSTM(62, activation="relu", name="the_output")(x) model = Model(input_layer, x) model.summary()
Но Дерек, это неправильно! С relu нельзя тренироваться категорично. И вы были бы правы ... если только тензорный поток не придет на помощь!
x = KL.LSTM(62, return_sequences=True, name="intermediate")(input_layer) # used to do softmax but experimenting here x = KL.LSTM(62, activation="relu", name="the_output")(x) model = Model(input_layer, x) model.summary() def customLoss(yTrue,yPred): return tf.nn.softmax_cross_entropy_with_logits_v2(yTrue, yPred) model.compile(loss=customLoss, optimizer='adam', metrics=['accuracy'])
Вау, какая очень сложная функция пользовательских потерь в одну строку!
Мы перемещаем softmax и категориальную кросс-энтропию в функцию потерь. Tensorflow автоматически применяет эпсилон, и это избавляет от проблемы NAN. (Я прочитал документацию, так что поверьте мне.)
Я считаю, что, поскольку softmax просто масштабирует все в сумме до одного, мы все равно можем сделать то же самое, используя argmax.
Во время тренировок очень полезно периодически сохранять веса на случай, если что-то пойдет не так:
# make sure logs directory ALREADY EXISTS checkpt = KC.ModelCheckpoint('./logs/weights.{epoch:02d}-{val_loss:.2f}.hdf5', monitor='val_loss', verbose=0, save_best_only=False, save_weights_only=True, mode='auto', period=1)
Теперь можно тренироваться :)
VERBOSE = 2 # minus 1 EPOCH_START = 0 EPOCH_END = 256 model.fit_generator(train_gen, epochs=EPOCH_END, verbose=VERBOSE, validation_data=val_gen, use_multiprocessing=True, initial_epoch=EPOCH_START, callbacks=[checkpt], workers = 32, max_queue_size = 4096)
Полный код можно найти здесь:
Https://github.com/chromestone/Random_ML/blob/master/rnn_character/ultimate.py
Полученные результаты
Это не самый лучший вариант, но я просто для развлечения воспользуюсь последней эпохой и посмотрю, что из этого получится.
Для предсказания вы можете использовать это:
ARBITRARY_LENGTH = 32 def predict(how_many, put_into_this, char2id, id2char, model): assert len(put_into_this) >= ARBITRARY_LENGTH for i in range(how_many): input_this = to_tensor(char2id, put_into_this[-ARBITRARY_LENGTH:]) max_this = model.predict(input_this[np.newaxis]) put_into_this.append(id2char[np.argmax(max_this)])
Это одна из самых запоминающихся строк, которые я прочитал из Гамлета:
Иди в женский монастырь. Прощай…
Подавая его в модель, модель выводит это:
Иди в женский монастырь. Прощайте, и несите великому из избранных имение, и не ломайте господина
Похоже, он придумывает слова. Прохладный!
У меня есть блокнот для вывода (со ссылкой на веса) в репозитории Github.
Https://github.com/chromestone/Random_ML/tree/master/rnn_character
Недостатки
Довольно очевидный недостаток заключается в том, что модель предсказывает только каждые 32 символа. По сути, эта архитектура заставляет модель забыть все символы до этого.
Еще один недостаток - разные персонажи, которых модель должна предсказать. Мы просто накинули уникальный документ и потренировались. У нас может не хватить данных (98 КБ), чтобы сосредоточиться на разных заглавных буквах, пунктуации и т. Д.
Один из важнейших недостатков - РЕЗКОСТЬ. Только 1 из 62 входов равен 1, а остальные - нули! Это не лучший способ тренироваться. Хотя оптимизаторы, такие как Адам, могут справиться с этим, это все еще не идеально.
В будущих статьях мы увидим вложения для уменьшения разреженности, чтобы иметь компактное и содержательное представление.
Больше прогнозов
Гамлет
Быть или не быть - вот титул небес, незнакомки и уступающей, и сделать ее моей
Для подсчетов я не могу говорить о кубке.
Сепорд подсчитывает:
Я не досадил на рябую ее медведицу
Нам лучше быть порносайтами, и мы прямые и странные из поросят,
Как бы ни было продано, мы тоже отдадим мне провон
И Каррону, чтобы он должен был показать свой прорыв порта, великому принцу, не весь князь, я бы не хотел в сердце, он действительно будет моим, чтобы нести ее иметь в сердце, Херрелл,
Чей с древнейшего до истины несет символ
Ромео и Джульетта
Разлука - такая сладкая печаль.
Твой слышит сын, самый близкий и сердечный, каким был этот незнакомец, и человек, которого мы взяли
Власть одержимых и чужая контрам больше, чем жениться на моих постах, а не в В мозгу было то, что я хранил мир,
Который будет не самым лучшим и самым любезным случаем для меня в сердце, носил все больше и больше, если бы ты не был больше моим
с тобою склоняющиеся делают уста,
чтобы сделать верхушку, и быть человеком и надеждой, и это означает, что притязатель от твоей служанки
Тим
Тимоти Лин - величайший ровесник странного, бедного и великого.
И возьми богача, если поспоришь с миром,
Который делает ее мужчиной, а тебя всем придворным.
СИЛАН: Я бы больше всего был собой и сломал бы мои изменения.
ТРИНА:
У меня в голове, черт возьми,
Джессика
Джессика Цинь - величайшая ровесница странного, бедного и великого
… [то же самое!]
Джошуа
Джошуа Ло - величайший ровесник меня, хотя, умоляю вас, я был бы в цепи
Чтобы медведи не слышали, как я дышу в великом из них, и тем больше, чем больше мой медведь будет говорить, будет будь мужем милого и будь средним великим смерти,
что с братом, и
Dr. Tu
Доктор Ту - великий мастер машинного обучения
Уитон использует эти притирки и приносит прибыль,
И пусть она основывает их, и пусть мои медведи будут честными, и скажите, что если Докажите, что уважаемые по контракту,
И что ж, я, милорд, я бы не стал говорить с вами,