Как построить генеративную модель вывода стиля

Все мы читали книги авторов, писавших в эксцентричном стиле, искажавших английские слова, чтобы создать свой собственный уникальный рисунок для своих книг (Шекспир, Стивен Эриксон, Патрик Ротфусс и т.д.) или даже разработавших свои собственные языки (Дж. Р.Р. Толкиен, Джордж Р.Р. Мартин, Джордж Оруэлл и т.д.). Как любители книг, мы все хотели бы, если это возможно, иметь возможность подражать стилю письма нашего любимого автора, но для этого, скорее всего, потребуются месяцы или годы интенсивного обучения.

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

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

Книги, которые будут использоваться для обучения модели, написаны величайшим писателем-фантастом всех времен Стивеном Эриксоном («Малазанская книга павших»). Роман представляет собой серию из 10 книг общим объемом более 10 000 страниц. Вы можете скачать книги здесь, конвертировать файл[ы] в txt, чтобы их можно было легко открыть в python.

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

  1. Построение модели детектора общих фраз Малазанской империи
  2. Построение вектора слов Малазанской империи
import os
import sys
import glob
import itertools
import numpy as np
import pandas as pd

from nltk.tokenize import WordPunctTokenizer
from nltk.stem import WordNetLemmatizer

from gensim.models.phrases import Phrases
from gensim.models.word2vec import Word2Vec

import tensorflow as tf
from tensorflow import keras

# you should be familiar with this
%run utils.py
Вход в полноэкранный режим Выход из полноэкранного режима

Настройка предварительных моделей

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

def load_phrase_detector_model(fname, reduce_size=False):
    phrases = Phrases.load(fname)

    print(f"Loading complete")
    return phrases.freeze() if reduce_size else phrases

# load a phrase detector model
phrase_model_path = "malaz_phrase_detector"
phrases = load_phrase_detector_model(phrase_model_path, reduce_size=True)

sentences_iterator = CustomPathLineSentences('Books', include_phrase=True,
                                             phrase_model=phrases)

# load word2vec model
word2vec_path = "malaz_word2vec.bin"
word2vec = Word2Vec.load(word2vec_path)
word2vec = word2vec.wv

print("Setup complete")
Вход в полноэкранный режим Выход из полноэкранного режима

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

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

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

def preprocess_texts(sentences_iterator):
    text = []
    for sentence in sentences_iterator:
        # remember each sentence is a list of tokens
        # punctuation included
        text.extend(sentence)

    print(f"Total number of words (Phrases included) {len(text)}")

    return text

text = preprocess_texts(sentences_iterator)
Вход в полноэкранный режим Выход из полноэкранного режима

Функция preprocess_text итеративно просматривает текстовые файлы, выполняет токенизацию, определяет и объединяет фразы, а также сохраняет каждый токен (слово) в список.

vocab = sorted(list(set(text)))
word_indices = {word: idx for idx, word in enumerate(vocab)}
indices_word = {idx: word for idx, word in enumerate(vocab)}

print(f"Total number of unique words: {len(vocab)}")
Вход в полноэкранный режим Выход из полноэкранного режима

vocab содержит уникальные слова, присутствующие в тексте, word_indices — словарь слов для индексации при построении одноточечной кодированной цели и, наконец, indices_word — обратный словарь, который служит для поиска при интерпретации одноточечной кодированной цели.

total_words = len(text)
total_sentences = len(sentences_iterator)
avg_word_sentences = total_words / total_sentences

print(f"Avg word per sentence: {avg_word_sentences}")
Вход в полноэкранный режим Выход из полноэкранного режима

Мы подсчитали среднее количество слов в каждом предложении, и в зависимости от используемых вами текстовых файлов оно может отличаться [у меня получилось значение 11,73]. Цель этого значения — определить минимальное базовое значение, которое мы должны использовать при выборе количества слов, которые будет просматривать модель, прежде чем делать предсказания о том, каким будет следующее слово.

Создание набора данных

maxlen = int(avg_word_sentences + 40)
step = 3
sentences = []
next_word_target []

for idx in range(0, len(text) - maxlen, step):
    sentences.append(text[idx: idx + maxlen])
    next_word_target.append(text[idx + maxlen])

# no longer need
del text
print(len(sentences), len(next_word_target))
Войдите в полноэкранный режим Выход из полноэкранного режима

В приведенном выше блоке кода сделано очень многое: переменная maxlen содержит максимальное количество слов, которые модель должна будет просмотреть, прежде чем сделать прогноз о том, каким будет следующее слово, step указывает количество слов, которые нужно пропустить от начала (например, взять 50 слов от начала текста, перейти к третьему слову от начала, взять еще 50, перейти к шестому… и так далее).

Выполнение кода создаст список, содержащий список слов длиной maxlen и соответствующую цель, оба из которых будут иметь одинаковую длину. Итак, наша модель будет делать следующее: она будет просматривать слова в переменной sentence и пытаться предсказать соответствующее слово, найденное в next_word_target. При достаточно мощной модели она сможет изучить стиль письма автора, пунктуацию, тон, грамматику и даже расположение слов (т.е. она сможет узнать, что если автор, например, пишет T’lan, то за ним всегда следует Imass), идеальный имитатор. Страшно и интересно, давайте покопаемся!

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

Превращение воды в вино

def cached_dataset(sentences, next_word_target, word2vec, word_indices):
    """A factory function, it returns a generator function
    that has various variables cached in it scope that will
    be unaffected by the outer scope.

    Doing this will enable the iterator function to be called
    without the need for arguments and without the function
    to request the variables from the outer scopes, because
    they are stored in it internals."""

    def generator():
        """An iterator function, it simply iterates through our earlier
        created dataset, changing the words in each sentence to its
        word vector representatives"""


        # words not available in the word vector
        # will be given a default word vectors where
        # every dimension equals to zero
        unknown_word = np.zeros(shape=(word_vector.vector_size,),
                                dtype=np.float32)

        for sentence, target_word in itertools.zip_longest(sentences, next_word_target):
            # create an dummy array of shape (len(sentence), embed_dim)
            # this is the word vector representation
            data = np.zeros(shape=(maxlen, word2vec.vector_size),
                            dtype=np.float32)

            # fills in the dummy array with the real word vector
            # values.
            for idx, word in enumerate(itertools.islice(sentence, None)):
                if word in word2vec:
                    data[idx] = word_vector[word]
                else:
                    data[idx] = unknown_word

            # create the target array
            target = np.array([word_indices[target_word]], dtype=np.int32)

            yield (tf.convert_to_tensor(data, dtype=tf.float32),
                   tf.convert_to_tensor(target, dtype=tf.int32))
    return generator

gen = cached_dataset(sentences, next_word_target, word2vec, word_indices)

# create a tensor dataset generator using 
# the Dataset API 
dataset_generator = tf.data.Dataset.from_generator(
    gen, 
    output_signature=(tf.TensorSpec(shape=(maxlen, word2vec.vector_size),
                                    dtype=tf.float32),
                      tf.TensorSpec(shape=(1,), dtype=tf.int32)))
Вход в полноэкранный режим Выход из полноэкранного режима

Магия этой функции заключается в том, что она преобразует строки в их словесно-векторное представление на лету, без промежуточных шагов по сохранению в файл. Она использует Tensorflow Dataset API для подачи числовых данных непосредственно в модель во время обучения [как вы скоро увидите].

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

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

Момент, которого мы ждем, а также боимся — построение и обучение набора данных. При построении модели мы будем использовать архитектуру LSTM в слоях (изучите их, чтобы узнать, как они работают).

num_neuron = 500
model = keras.models.Sequential()
model.add(keras.layers.Input(shape=(maxlen, word2vec.vector_size)))
model.add(keras.layers.LSTM(num_neuron, return_sequences=True))
model.add(keras.layers.LSTM(num_neuron, return_sequences=True))
model.add(keras.layers.LSTM(num_neuron))

model.add(keras.layers.Dense(len(vocab), activation='softmax'))

optimizer = keras.optimizer.Adam(learning_rate=0.001)
model.compile(loss='sparse_categorical_crossentropy',
              optimizer=optimizer)
model.summary()
Вход в полноэкранный режим Выход из полноэкранного режима
# define a checkpoint callback [you will really need this]
model_cb = keras.callbacks.ModelCheckpoint("model.h5", monitor='loss')

# GET READY TO RECIEVE THE GREATEST SHOCK
# OF YOUR LIFE WHEN YOU RUN THIS CODE

# Tell me what you see when you execute the code
# what did it display (You will know what I am
# talking about when you see it)
epochs = 100
batch_sixe = 32
num_workers = 4
model.fit(dataset_generator.repeat(-1).batch(batch_size).prefetch(5),
          steps_per_epoch=len(sentences) // batch_size,
          batch_size=batch_size, epochs=epochs,
          workers=num_workers, callbacks=[model_cb])
Войти в полноэкранный режим Выход из полноэкранного режима

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

Вам будет гораздо лучше обучать эту модель на Kaggle или Google Colab, чем на своем локальном компьютере, так как обучение этой модели очень интенсивное и может занять необычайно много времени (считанные дни).

Включение в работу

def sample(preds, temperature=1.0):
    preds = np.asarray(preds).astype('float64')
    preds = np.log(preds) / temperature
    exp_preds = np.exp(preds)

    preds = exp_preds / np.sum(exp_preds)
    probas = np.random.multinomial(1, preds, 1)

    return np.argmax(probas)
Войдите в полноэкранный режим Выход из полноэкранного режима

Функция sample просто сообщает нам, какое слово имеет наибольшую вероятность оказаться следующим1

text = """
it was not of Jaghut construction, that it had arisen beside the three Jaghut towers of its own accord, in answer to a law unfathomable to god and mortal alike. Arisen to await the coming of those whom it would imprison for eternity. Creatures of deadly power.
"""
text = sentences_iterator.clean_token_words(text)
text = text[:maxlen]
generated = []
generated.extend(text)
sys.stdout.write(generate)

unknown_word = np.zeros(shape=(word_vector.vector_size,),
                        dtype=np.float32)

for temp in np.arange(0.1, 0.5, 0.1):
    for i in range(400):
        data = np.zeros(shape=(1, maxlen, word2vec.vector_size),
                        dtype=np.float32)

        for t, word in enumerate(text):
            if word in word2vec:
                data[0, t] = word2vec[word]
            else:
                data[0, t] = unknown_word

        preds = model.predict(data)
        next_index = sample(preds.ravel(), t)  # optimal value 0.2 to 0.4, test
        next_word = indices_word[next_index]
        generated += next_word

        text[1:].append(next_word)
        sys.stdout.write(next_word)
        sys.stdout.flush()
Войти в полноэкранный режим Выход из полноэкранного режима

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

Бог любит тебя!


  1. Естественный язык в действии

Оцените статью
Procodings.ru
Добавить комментарий