Итак, на этом этапе работы с 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

Доктор Ту - великий мастер машинного обучения
Уитон использует эти притирки и приносит прибыль,
И пусть она основывает их, и пусть мои медведи будут честными, и скажите, что если Докажите, что уважаемые по контракту,
И что ж, я, милорд, я бы не стал говорить с вами,