Я начал говорить об этом проекте с нетехнической беседы об анализе, который я сделал для корпуса из 5000 текстов (более 5 миллионов символов) Mexican banda music (на испанском языке).



Анализ 5000 песен региональной мексиканской музыки (banda)
Como parte de un proyecto personal en el que estoy trabajando, он recopilado un corpus… medium .com



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

Полный код и корпус можно найти в этом репозитории на github.

ОБНОВЛЕНИЕ: Вторая часть доступна здесь. Где я объясняю обучение, используя вложения слов. Вложения слов обеспечивают плотное представление слов и их относительных значений.

Быстрый фон

Блоки (или блоки) долгосрочной краткосрочной памяти (LSTM) являются строительной единицей для слоев рекуррентной нейронной сети (RNN). RNN, состоящую из модулей LSTM, часто называют сетью LSTM. Обычный блок LSTM состоит из ячейки, входного элемента, выходного элемента и элемента забывания. Источник википедия.

RNN могут использоваться для прогнозирования или обучения на основе последовательных данных и генерации аналогичных данных.

Как и во многих других случаях создания текста в Интернете, я черпал вдохновение из примера keras-team и из этого проекта по созданию твитов в стиле Трампа.

Основное отличие состоит в том, что я создал текстовый генератор, работающий на уровне слов, а не на уровне символов, как в предыдущих примерах. Кроме того, поскольку обучающий набор не помещается в память (особенно при отправке на GPU), мне нужно было создать генератор данных для функций fit и оценивать Keras.

Должен признать, что я работал над версией этого проекта, прежде чем выполнить быстрый поиск в Google. Я наконец сделал это и нашел этот проект, который также основан на том же скрипте и, следовательно, чем-то похож на мою версию:

Пояснение к алгоритму

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

>>> sentences[0]
['put', 'a', 'gun', 'against', 'his']
>>> next_words[0]
'head'
>>> sentences[1]
['a', 'gun', 'against', 'his', 'head']
>>> next_words[1]
'pulled'

На самом деле мы отправляем не строки, а векторизованное представление слова в словаре возможных слов (подробнее об этом позже). Идея состоит в том, что по прошествии многих эпох RNN изучит «стиль» написания корпуса, пытаясь настроить веса сети, чтобы предсказать следующее слово с учетом последовательности N предыдущих слов.

Текстовый корпус

Как я объяснил в другой истории, корпус содержит более 5 миллионов символов и более 1 миллиона слов. Получение текста - это только начало проблемы, потому что, как и в любом другом проекте машинного обучения, необходимо было проанализировать, очистить и выполнить некоторую предварительную обработку этих данных.

Я не буду сейчас вдаваться в подробности (возможно, это отдельная история), но, мягко говоря, данные были грязными. Тысячи опечаток, орфографических ошибок, сленга, неправильной пунктуации, спанглиша и т. д.

Теперь об алгоритме.

Читая корпус, разбиваю на слова

Первый шаг - прочитать корпус и разбить его на слова.

corpus = sys.argv[1] # first command line arg
with io.open(corpus, encoding='utf-8') as f:
    text = f.read().lower().replace('\n', ' \n ')
print('Corpus length in characters:', len(text))

text_in_words = [w for w in text.split(' ') if w.strip() != '' or w == '\n']
print('Corpus length in words:', len(text_in_words))

Обратите внимание на вызов .replace(‘\n’, ‘ \n ‘, это потому, что нам нужна новая строка как слово. Идея заключается в том, что мы также оставляем за сетью решение о том, когда начинать новую строку (после нескольких слов). После этого text_in_words представляет собой большой массив, содержащий весь корпус, слово за словом.

>>> text_in_words[3000:3005]
[‘ella’, ‘era’, ‘como’, ‘estar’, ‘\n’]

Получение частот слов

В генераторах текста на уровне символов вы можете закончить с 30–50 различными размерами, по одному для каждого из разных символов. В генераторе уровня слов, подобном текущему, у вас будет измерение для каждого из разных слов, которые могут быть десятками тысяч (особенно в таком грязном корпусе, как этот).

Чтобы избежать этих больших чисел, мы вычисляем частоту каждого из слов, поэтому мы можем использовать эту информацию для фильтрации необычных слов, уменьшая размерность и, следовательно, память и время для обучения сети.

# Calculate word frequency
word_freq = {}
for word in text_in_words:
    word_freq[word] = word_freq.get(word, 0) + 1

ignored_words = set()
for k, v in word_freq.items():
    if word_freq[k] < MIN_WORD_FREQUENCY:
        ignored_words.add(k)

words = set(text_in_words)
print('Unique words before ignoring:', len(words))
print('Ignoring words with frequency <', MIN_WORD_FREQUENCY)
words = sorted(set(words) - ignored_words)
print('Unique words after ignoring:', len(words))

word_indices = dict((c, i) for i, c in enumerate(words))
indices_word = dict((i, c) for i, c in enumerate(words))

Переменная MIN_WORD_FREQUENCY - это параметр, который указывает, какое минимальное количество появлений должно иметь слово, чтобы «сделать сокращение» и попасть в окончательный словарь слов.

Другими словами, если мы установим MIN_WORD_FREQUENCY на 10, мы будем рассматривать только слова, которые встречаются в корпусе 10 или более раз. В целях отладки мы печатаем размер исходного словаря и его размер после вырезания необычных слов.

Затем мы создаем словари для перевода от слова к указателю и от указателя к слову. Это как пример keras-team.

Создание и фильтрация последовательностей

Если мы помним, на данный момент у нас есть text_in_words, который представляет собой массив, содержащий пословно весь корпус. Нам нужно создать последовательности размером SEQUENCE_LEN (еще один параметр, который можно выбрать вручную) и сохранить их в предложениях, а в том же индексе сохранить следующее слово в next_words.

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

# cut the text in semi-redundant sequences of SEQUENCE_LEN words
STEP = 1
sentences = []
next_words = []
ignored = 0
for i in range(0, len(text_in_words) - SEQUENCE_LEN, STEP):
    # Only add sequences where no word is in ignored_words
    if len(set(text_in_words[i: i+SEQUENCE_LEN+1]).intersection(ignored_words)) == 0:
        sentences.append(text_in_words[i: i + SEQUENCE_LEN])
        next_words.append(text_in_words[i + SEQUENCE_LEN])
    else:
        ignored = ignored+1
print('Ignored sequences:', ignored)
print('Remaining sequences:', len(sentences))

Набор для обучения в случайном порядке и разделение

Следующий шаг стандартный, мы перетасовываем обучающий набор и разделяем его на обучающий и тестовый набор (98% -2% по умолчанию).

sentences, next_words, sentences_test, next_words_test = shuffle_and_split_training_set(sentences, next_words)

Построение модели

Теперь строим модель RNN. В этом примере я использовал двухуровневый набор двунаправленных модулей LSTM.

model = Sequential()
model.add(Bidirectional(LSTM(128), input_shape=(SEQUENCE_LEN, len(words))))
if dropout > 0:
    model.add(Dropout(dropout))
model.add(Dense(len(words)))
model.add(Activation('softmax'))

В этой архитектуре есть несколько произвольных решений, у меня фактически не было времени для перекрестной проверки различных размеров, типов единиц и т. Д.

Это некоторые дискуссии о возможностях архитектуры:

  • Двунаправленный или обычный LSTM здесь.
  • Отсев - это метод регуляризации, чтобы предотвратить чрезмерную подгонку здесь.
  • Количество модулей LSTM, я подозреваю, 256 может быть слишком много. Обсуждение здесь.

В зависимости от логического параметра SIMPLE_MODEL мы создаем одно- или двухслойную модель. Одного слоя обычного LSTM должно хватить, чтобы дать достаточно хорошие результаты, как в примере keras-team.

model.add(LSTM(128, input_shape=(maxlen, len(chars))))

Генератор данных

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

После быстрого анализа я нашел причину. Чтобы выполнить векторизацию последовательностей в обучающий набор (x, y), нам понадобятся следующие массивы:

x = np.zeros((len(sentences), SEQUENCE_LEN, len(words)), dtype=np.bool)
y = np.zeros((len(sentences), len(words)), dtype=np.bool)

Без фильтрации слов у меня было примерно 1 миллион предложений (len (предложения) = 1000000), SEQUENCE_LEN = 10 и 40 000 различных слов (len (слова) = 40000). С этими числами x имел размер 400 000 000 000 (!). Учитывая, что in numpy - 1 байт, это дало мне примерно 400 ГБ памяти (!).

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

def generator(sentence_list, next_word_list, batch_size):
    index = 0
    while True:
        x = np.zeros((batch_size, SEQUENCE_LEN, len(words)), dtype=np.bool)
        y = np.zeros((batch_size, len(words)), dtype=np.bool)
        for i in range(batch_size):
            for t, w in enumerate(sentence_list[index]):
                x[i, t, word_indices[w]] = 1
            y[i, word_indices[next_word_list[index]]] = 1

            index = index + 1
            if index == len(sentence_list):
                index = 0
        yield x, y

Функция генератора получает список предложений и next_words, а также размер пакета. Затем он дает два множества массива batch_size. Мы используем переменную index, чтобы отслеживать примеры, которые мы уже вернули. Конечно, его нужно повторно инициализировать до 0, когда мы дойдем до конца списков. Этот генератор можно использовать как для обучения, так и для оценки (просто передавая разные предложения_список и список_следующих_слов).

Завершение модели

Функции sample и on_epoch_end практически не отличаются от примера keras-team. Однако при компиляции модели я добавил несколько обратных вызовов Keras.

file_path = "./checkpoints/LSTM_LYRICS-epoch{epoch:03d}-words%d-sequence%d-minfreq%d-loss{loss:.4f}-acc{acc:.4f}-val_loss{val_loss:.4f}-val_acc{val_acc:.4f}" % (
    len(words),
    SEQUENCE_LEN,
    MIN_WORD_FREQUENCY
)
checkpoint = ModelCheckpoint(file_path, monitor='val_acc', save_best_only=True)
print_callback = LambdaCallback(on_epoch_end=on_epoch_end)
early_stopping = EarlyStopping(monitor='val_acc', patience=5)
callbacks_list = [checkpoint, print_callback, early_stopping]

Первый - это ModelCheckpoint для сохранения весов каждую эпоху, а второй - EarlyStopping, чтобы остановить обучение, если нет увеличения потерь в 5 эпохах.

Обучение модели

Наконец, мы вызываем model.fit_generator (вместо model.fit) с генератором данных, обратными вызовами и количеством эпох. Мы также отправляем еще один генератор с тестовыми данными, чтобы он оценивался каждую эпоху.

model.fit_generator(generator(sentences, next_words, BATCH_SIZE),
    steps_per_epoch=int(len(sentences)/BATCH_SIZE) + 1,
    epochs=100,
    callbacks=callbacks_list,
    validation_data=generator(sentences_test, next_words_test, BATCH_SIZE),              validation_steps=int(len(sentences_test)/BATCH_SIZE) + 1)

RNN может быть сложно обучить. Даже с довольно мощным графическим процессором (GeForce GTX 1070 ti) каждая эпоха занимает более одного часа со стековой архитектурой LSTM.

Проведение обучения

Чтобы начать обучение, нужно запустить его (естественно, вы можете запустить его с собственным корпусом).

git clone https://github.com/enriqueav/lstm_lyrics.git
cd lstm_lyrics
python3 lstm_train.py corpora/corpus_banda.txt examples.txt

ОБНОВЛЕНИЕ: вы также можете ссылаться на версию встраивания.

После этого скрипт распечатает информацию о текущем обучающем наборе, предварительной обработке и т. Д. Корпус примера (мексиканская музыка «банда») содержит более 5 миллионов символов в более чем 1 миллионе слов.

Corpus length in characters: 5502159
Corpus length in words: 1066242

Изначально в корпусе более 35 000 различных слов. После фильтрации слов с частотой менее 10 (MIN_WORD_FREQUENCY = 10) их всего 6605.

Unique words before ignoring: 36990
Ignoring words with frequency < 10
Unique words after ignoring: 6605

Поскольку ШАГ равен 1, изначально существует примерно 1 миллион различных последовательностей. Однако, поскольку мы уже проигнорировали 30 000 менее часто встречающихся слов, нам также необходимо игнорировать последовательности, содержащие хотя бы одно из этих игнорируемых слов. После этого сокращения мы получаем примерно 537 000 действительных последовательностей.

Ignored sequences: 529230
Remaining sequences: 537002
Shuffling sentences
Shuffling finished

Наконец, мы разделили эти перемешанные 537000 на тестовый набор 98%, обучающий 2%.

Size of training set = 526261
Size of test set = 10741

Затем мы строим модель и начинаем обучение.

Build model...
Epoch 1/100...

Следите за результатами

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

К эпохе номер 20 точность набора поездов будет около 90%, однако на тестовом наборе мы не увидим таких высоких чисел, это нормально. Помните, что мы не стремимся к точности на человеческом уровне, а стремимся только к изучению «стиля» и написанию несколько связных текстов.

Примеры

К сожалению, это будет иметь больше смысла, если вы немного знаете испанский, и особенно если вы знакомы с мексиканским стилем банда.

Создание с помощью семени: «de mi porque estoy comprometido la que se queda la quiero y la que se»

de mi porque estoy comprometido la que se queda la quiero y la que se va la olvido con el mundo de mi vida siempre tu te marchas el amor con mis caricias fuerza que he sido tan caminar el destino en la mafia la noche y fue el demás мне puse a ver a todo el mundo y ahi que te ame decir que

Создание с помощью семян: «tus penas o si alguna vez alguien te ha lastimado si tu corazón por el»

tus penas o si alguna vez alguien te ha lastimado si tu corazón por el momento es libre o ya está ocupado porque el mío creo que a asi de hoy alguien todo me ha robado a nadie mujer no hay alguien me la estoy pero no me estoy perdiendo se tiene de no te lo que a hacer no se te vaya a olvidar

Создание с помощью seed: «mis brazos me muero de ganas por volverte a besar en mis noches despierto gritando»

mis brazos me muero de ganas por volverte a besar en mis noches despierto gritando tu nombre y me muero de miedo al pensar que a otro hombre le estaras en la vida tan bella y lo que alguna quise tener que me hubieras pasado nada era un sueño no se diga estas flores del norte estas palabras y muero por eso es tan cierto

Следующие шаги

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

ОБНОВЛЕНИЕ 15 июня 2018 г .: теперь добавлено использование новой строки как отдельного слова и отправка проверочных данных на fit_generator.

ОБНОВЛЕНИЕ 21 января 2019 г .: Добавьте ссылку на вторую часть истории.

Также прочтите

Получайте лучшие предложения по программному обеспечению прямо в свой почтовый ящик