Использование рекуррентной нейронной сети для создания языковой модели и генерации стихов

Генерация стихов

Здравствуйте и вновь добро пожаловать на занятия по теме «Рекуррентные нейронные сети на языке Python: глубокое обучение, часть 5».

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

Как уже говорилось, это относится к обучению без учителя, а результат работы функции софтмакс – это вероятность следующего слова с учётом всех предыдущих слов в строке. Для первого приближения мы создадим начальное распределение слов, обозначив его через π. π – это просто распределение по всем словам, с которых начинается строка. Мы будем выбирать из этого распределения, так чтобы каждая строка начиналась с другого слова, поскольку, как вы понимаете, если мы будем давать в качестве входных данных один и тот же токен, то рекуррентная сеть будет выдавать один и тот же результат. Если мы будем давать в качестве входных данных только два этих токена (начальный и первый вычисленный) в рекуррентную сеть, то она будем выдавать другой, но опять-таки один и тот же результат и так далее. Не забывайте, что прогнозом нейронной сети является лишь значение функции argmax от значения функции софтмакс. Выборка из начального распределения слов позволит каждой строке начинаться с разных слов, что, в свою очередь, позволит нам генерировать различные последовательности.

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

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

Далее, функция fit в качестве аргумента будет использовать только X, поскольку внешних целевых переменных нет. В рамках самой функции fit будут определены собственные целевые переменные: для слов от первого до T-1 это будет слово в момент T. Кроме того, нужно предусмотреть конец строки, иначе у нас будут получаться бесконечно длинные строки. Поэтому целевой переменной для всей последовательности будет токен END (конец). Аналогично, для начала входной последовательности добавляется токен START (начало); его целевой переменной будет первое слово. В общем, входная последовательность будет предваряться токеном START, а исходящая последовательность будет заканчиваться токеном END.

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

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

rnn = SimpleRNN.load(имя_файла)

rnn.save(имя_файла)

Обратите внимание, что один из методов является статическим, а второй – методом экземпляра класса. Поскольку функции Theano требуют компиляции, мы не можем просто сохранить весовые коэффициенты как массив Numpy, необходимо заново инициировать объект со всеми требуемыми функциями Theano, чтобы иметь возможность сделать прогноз.

Теперь обсудим рассматриваемые данные. Мы будем пытаться сгенерировать стихи, обучаясь на поэзии Роберта Фроста. У нас есть около 1500 строк его стихов, и мы будем рассматривать каждую строку как последовательность. Базовый алгоритм следующий. В каждой строке нужно заменить заглавные буквы строчными и удалить всю пунктуацию, разделив слова пробелами. Так мы получим список токенов. Затем для каждого токена, если его нет в нашем отображении word2idx, присваивается индекс. После этого мы сохраняем каждое предложение как последовательность индексов слов и возвращаем его и отображение индексов слов.

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

Наконец, код для этого упражнения находится в файле srn_language.py. Найдите его на Github, если допустите ошибку, когда сами будете писать код или если во время его написания ошибку допущу я.

Генерация стихов в коде. Часть 1

Вначале обратимся к файлу util.py, поскольку у нас появился дополнительный код. Так, мы импортируем библиотеку string, а также имеем некоторые дополнительные функции. Функция remove_punctuation удаляет пунктуацию:

def remove_punctuation(s):

    return s.translate(None, string.punctuation)

И функция get_robert_frost. Начинается она с добавления токенов START и END, которым назначаются «волшебные» числа 0 и 1, но если хотите – можете поменять их на другие. Таким образом, переменная текущего индекса current_idx начинается с 2 и увеличивается на единицу каждый раз, когда встречается новое слово. Возвращает функция список предложений отображение индексов слов.

def get_robert_frost():

    word2idx = {‘START’: 0, ‘END’: 1}

    current_idx = 2

    sentences = []

    for line in open(‘../hmm_class/robert_frost.txt’):

        line = line.strip()

        if line:

            tokens = remove_punctuation(line.lower()).split()

            sentence = []

            for t in tokens:

                if t not in word2idx:

                    word2idx[t] = current_idx

                    current_idx += 1

                idx = word2idx[t]

                sentence.append(idx)

            sentences.append(sentence)

    return sentences, word2idx

Переходим к файлу srn_language.py. Импортируем библиотеки и функции:

import theano

import theano.tensor as T

import numpy as np

import matplotlib.pyplot as plt

from sklearn.utils import shuffle

from util import init_weight, get_robert_frost

Теперь определяем класс SimpleRNN. D – размерность векторного представления слов, M – размер скрытого слоя, V – размер словаря.

class SimpleRNN:

    def __init__(self, D, M, V):

        self.D = D

        self.M = M

        self.V = V

Определяем функцию fit. В аргументах у неё только X, ведь это обучение без учителя.

    def fit(self, X, learning_rate=10e-1, mu=0.99, reg=1.0, activation=T.tanh, epochs=500, show_fig=False):

        N = len(X)

        D = self.D

        M = self.M

        V = self.V

        self.f = activation

Теперь инициируем весовые коэффициенты.

        # initial weights

        We = init_weight(V, D)

        Wx = init_weight(D, M)

        Wh = init_weight(M, M)

        bh = np.zeros(M)

        h0 = np.zeros(M)

        Wo = init_weight(M, V)

        bo = np.zeros(V)

        self.We = theano.shared(We)

        self.Wx = theano.shared(Wx)

        self.Wh = theano.shared(Wh)

        self.bh = theano.shared(bh)

        self.h0 = theano.shared(h0)

        self.Wo = theano.shared(Wo)

        self.bo = theano.shared(bo)

        self.params = [self.We, self.Wx, self.Wh, self.bh, self.h0, self.Wo, self.bo]

        thX = T.ivector(‘X’)

        Ei = self.We[thX] # will be a TxD matrix

        thY = T.ivector(‘Y’)

Далее определяем функцию recurrence, возвращающую, опять же, h(t) и y(t).

        def recurrence(x_t, h_t1):

            # returns h(t), y(t)

            h_t = self.f(x_t.dot(self.Wx) + h_t1.dot(self.Wh) + self.bh)

            y_t = T.nnet.softmax(h_t.dot(self.Wo) + self.bo)

            return h_t, y_t

Теперь функция scan.

        [h, y], _ = theano.scan(

            fn=recurrence,

            outputs_info=[self.h0, None],

            sequences=Ei,

            n_steps=Ei.shape[0],

        )

        py_x = y[:, 0, :]

        prediction = T.argmax(py_x, axis=1)

        cost = -T.mean(T.log(py_x[T.arange(thY.shape[0]), thY]))

        grads = T.grad(cost, self.params)

        dparams = [theano.shared(p.get_value()*0) for p in self.params]

Определяем обновления.

        updates = [

            (p, p + mu*dp – learning_rate*g) for p, dp, g in zip(self.params, dparams, grads)

        ] + [

            (dp, mu*dp – learning_rate*g) for dp, g in zip(dparams, grads)

        ]

Определяем функции Theano.

        self.predict_op = theano.function(inputs=[thX], outputs=prediction)

        self.train_op = theano.function(

            inputs=[thX, thY],

            outputs=[cost, prediction],

            updates=updates

        )

Теперь мы готовы к главному обучающему циклу.

        costs = []

        n_total = sum((len(sentence)+1) for sentence in X)

        for i in xrange(epochs):

            X = shuffle(X)

            n_correct = 0

            cost = 0

            for j in xrange(N):

                input_sequence = [0] + X[j]

                output_sequence = X[j] + [1]

Не забывайте, что 0 и 1 у нас зарезервированы для токенов начала и конца строки!

                c, p = self.train_op(input_sequence, output_sequence)

                cost += c

                for pj, xj in zip(p, output_sequence):

                    if pj == xj:

                        n_correct += 1

            print(“i:”, i, “cost:”, cost, “correct rate:”, (float(n_correct)/n_total))

            costs.append(cost)

        if show_fig:

            plt.plot(costs)

            plt.show()

Это всё, что касается функции fit. Переходим к функции save. Она очень проста.

    def save(self, filename):

        np.savez(filename, *[p.get_value() for p in self.params])

Теперь функция загрузки load.

    @staticmethod

    def load(filename, activation):

        npz = np.load(filename)

        We = npz[‘arr_0’]

        Wx = npz[‘arr_1’]

        Wh = npz[‘arr_2’]

        bh = npz[‘arr_3’]

        h0 = npz[‘arr_4’]

        Wo = npz[‘arr_5’]

        bo = npz[‘arr_6’]

        V, D = We.shape

        _, M = Wx.shape

        rnn = SimpleRNN(D, M, V)

        rnn.set(We, Wx, Wh, bh, h0, Wo, bo, activation)

        return rnn

И напишем функцию set.

    def set(self, We, Wx, Wh, bh, h0, Wo, bo, activation):

        self.f = activation

        self.We = theano.shared(We)

        self.Wx = theano.shared(Wx)

        self.Wh = theano.shared(Wh)

        self.bh = theano.shared(bh)

        self.h0 = theano.shared(h0)

        self.Wo = theano.shared(Wo)

        self.bo = theano.shared(bo)

        self.params = [self.We, self.Wx, self.Wh, self.bh, self.h0, self.Wo, self.bo]

        thX = T.ivector(‘X’)

        Ei = self.We[thX] # will be a TxD matrix

        thY = T.ivector(‘Y’)

        def recurrence(x_t, h_t1):

            # returns h(t), y(t)

            h_t = self.f(x_t.dot(self.Wx) + h_t1.dot(self.Wh) + self.bh)

            y_t = T.nnet.softmax(h_t.dot(self.Wo) + self.bo)

            return h_t, y_t

        [h, y], _ = theano.scan(

            fn=recurrence,

            outputs_info=[self.h0, None],

            sequences=Ei,

            n_steps=Ei.shape[0],

        )

        py_x = y[:, 0, :]

        prediction = T.argmax(py_x, axis=1)

        self.predict_op = theano.function(

            inputs=[thX],

            outputs=prediction,

            allow_input_downcast=True,

        )

И последняя функция – generate.

    def generate(self, pi, word2idx):

        idx2word = {v:k for k,v in iteritems(word2idx)}

        V = len(pi)

        n_lines = 0

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

        X = [ np.random.choice(V, p=pi) ]

        print(idx2word[X[0]])

Стихотворения будем создавать 4-строчными.

        while n_lines < 4:

            P = self.predict_op(X)[-1]

            X += [P]

            if P > 1:

                word = idx2word[P]

                print word,

            elif P == 1:

                n_lines += 1

                print(”)

                if n_lines < 4:

                    X = [ np.random.choice(V, p=pi) ]

                    print(idx2word[X[0]], end=” “)

Это всё, что касается функции generate. Переходим к функции train_poetry.

Генерация стихов в коде. Часть 2

Продолжение генерации стихов в коде.

Функция train_poetry весьма проста, мы уже почти сделали всю работу.

def train_poetry():

    sentences, word2idx = get_robert_frost()

    rnn = SimpleRNN(30, 30, len(word2idx))

    rnn.fit(sentences, learning_rate=10e-5, show_fig=True, activation=T.nnet.relu, epochs=2000)

    rnn.save(‘RNN_D30_M30_epochs2000_relu.npz’)

Переходим к функции generate_poetry.

def generate_poetry():

    sentences, word2idx = get_robert_frost()

    rnn = SimpleRNN.load(‘RNN_D30_M30_epochs2000_relu.npz’, T.nnet.relu)

    V = len(word2idx)

    pi = np.zeros(V)

    for sentence in sentences:

        pi[sentence[0]] += 1

    pi /= pi.sum()

    rnn.generate(pi, word2idx)

if __name__ == ‘__main__’:

    train_poetry()

    generate_poetry()

При генерации стихов видим, что строки очень короткие и у нас повторяются одни и те же строки. Следовательно, в дальнейшем нужно ещё улучшить программу.

Классификация стихотворений

Сейчас рассмотрим задачу, с которой впервые столкнулись в курсе по скрытым марковским моделям, – различие стихотворений Роберта Фроста и Эдгара Алана По с использованием лишь последовательностей разметок по частям речи. С помощью скрытых марковских моделей на проверочном наборе нам удалось достичь от 60 до 70 процентов точности, и теперь мы постараемся превзойти этот результат с помощью рекуррентной нейронной сети.

Прежде всего, поскольку обработка данных в процессе разметки по частям речи несколько сложнее, мы обсудим это здесь. Я упомянул разметку по частям речи. Эти части в основном составляют существительные, наречия, прилагательные и так далее; они указывают на роль слова в предложении. Таким образом, имея предложение, то есть последовательность слов, мы имеем последовательность меток по частям речи такой же длины. Для работы будет использоваться библиотека NLTK, что переводится как инструментарий для работы с естественным языком. Если вы изучали мой курс по введению в обработку естественных языков, то она у вас уже есть. Но если нет, то это очень просто, наберите лишь команду

sudo pip install nltk

Для установки могут потребоваться некоторые дополнительные паки, но если это так, то вам будет выведено соответствующее сообщение. Тут тоже всё очень просто, надо лишь в консоли набрать команду

nltk.download()

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

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

Теперь рассмотрим собственно классификатор. Вновь-таки, он будет несколько отличаться от предыдущей рекуррентной нейронной сети. Хотя это тоже своего рода языковая модель, в нём не будет векторного представления слов, так как слова мы не используем. Вектор, составленный методом прямого кодирования и представляющий метку по части речи, идёт прямо в рекуррентный нейрон. Имейте в виду, это значит, что Wx теперь будет иметь размерность VxM, а не DxM. D отсутствует, поскольку отсутствует и векторное представление слов. Количество исходящих классов теперь равно 2, а не V, поскольку мы прогнозируем не слова, а поэта.

Как вы могли догадаться, из трёх способов составления прогноза с помощью рекуррентной нейронной сети это будет первый – один класс на последовательность. Это значит, что нас интересует классификация в конце последовательности или, другими словами, после того, как мы увидели всю последовательность.

И ещё одно замечание. Будет использоваться переменный коэффициент обучения, поскольку, как я заметил, функция затрат сильно колеблется на позднейших циклах обучения. В связи с этим коэффициент обучения будет уменьшаться в 0,9999 раз после каждого цикла.

Классификация поэзии в коде

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

Но начнём мы вновь-таки с рассмотрения файла util.py. В данном случае мы импортируем библиотеку NLTK с функциями pos_tag и word_tokenize. Соответствующие дополнительные функции следующие. Это get_tags, которая принимает строку и токенизирует её:

def get_tags(s):

    tuples = pos_tag(word_tokenize(s))

    return [y for x, y in tuples]

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

def get_poetry_classifier_data(samples_per_class, load_cached=True, save_cached=True):

    datafile = ‘poetry_classifier_data.npz’

    if load_cached and os.path.exists(datafile):

        npz = np.load(datafile)

        X = npz[‘arr_0’]

        Y = npz[‘arr_1’]

        V = int(npz[‘arr_2’])

        return X, Y, V

    word2idx = {}

    current_idx = 0

    X = []

    Y = []

    for fn, label in zip((‘../hmm_class/edgar_allan_poe.txt’, ‘../hmm_class/robert_frost.txt’), (0, 1)):

        count = 0

        for line in open(fn):

            line = line.rstrip()

            if line:

                print(line)

                # tokens = remove_punctuation(line.lower()).split()

                tokens = get_tags(line)

                if len(tokens) > 1:

                    # scan doesn’t work nice here, technically could fix…

                    for token in tokens:

                        if token not in word2idx:

                            word2idx[token] = current_idx

                            current_idx += 1

                    sequence = np.array([word2idx[w] for w in tokens])

                    X.append(sequence)

                    Y.append(label)

                    count += 1

                    print(count)

                    # quit early because the tokenizer is very slow

                    if count >= samples_per_class:

                        break

    if save_cached:

        np.savez(datafile, X, Y, current_idx)

    return X, Y, current_idx

Теперь начнём создание нашего классификатора поэзии. Импортируем почти то же самое, что и прежде.

import theano

import theano.tensor as T

import numpy as np

import matplotlib.pyplot as plt

from sklearn.utils import shuffle

from util import init_weight, get_poetry_classifier_data

Создаём класс SimpleRNN. M – размер скрытого слоя, V – размер словаря.

class SimpleRNN:

    def __init__(self, M, V):

        self.M = M

        self.V = V

Функция fit.

    def fit(self, X, Y, learning_rate=10e-1, mu=0.99, reg=1.0, activation=T.tanh, epochs=500, show_fig=False):

        M = self.M

        V = self.V

        K = len(set(Y))

        print(“V:”, V)

        X, Y = shuffle(X, Y)

        Nvalid = 10

        Xvalid, Yvalid = X[-Nvalid:], Y[-Nvalid:]

        X, Y = X[:-Nvalid], Y[:-Nvalid]

        N = len(X)

Инициируем весовые коэффициенты. Тут ничего нового.

        # initial weights

        Wx = init_weight(V, M)

        Wh = init_weight(M, M)

        bh = np.zeros(M)

        h0 = np.zeros(M)

        Wo = init_weight(M, K)

        bo = np.zeros(K)

        thX, thY, py_x, prediction = self.set(Wx, Wh, bh, h0, Wo, bo, activation)

        cost = -T.mean(T.log(py_x[thY]))

        grads = T.grad(cost, self.params)

        dparams = [theano.shared(p.get_value()*0) for p in self.params]

        lr = T.scalar(‘learning_rate’)

Обновления.

        updates = [

            (p, p + mu*dp – lr*g) for p, dp, g in zip(self.params, dparams, grads)

        ] + [

            (dp, mu*dp – lr*g) for dp, g in zip(dparams, grads)

        ]

Определяем train_op.

        self.train_op = theano.function(

            inputs=[thX, thY, lr],

            outputs=[cost, prediction],

            updates=updates,

            allow_input_downcast=True,

        )

Переходим к основному циклу.

        costs = []

        for i in xrange(epochs):

            X, Y = shuffle(X, Y)

            n_correct = 0

            cost = 0

            for j in xrange(N):

                c, p = self.train_op(X[j], Y[j], learning_rate)

                cost += c

                if p == Y[j]:

                    n_correct += 1

После каждого цикла уменьшаем коэффициент обучения:

            learning_rate *= 0.9999

            n_correct_valid = 0

            for j in xrange(Nvalid):

                p = self.predict_op(Xvalid[j])

                if p == Yvalid[j]:

                    n_correct_valid += 1

            print(“i:”, i, “cost:”, cost, “correct rate:”, (float(n_correct)/N),

            print(“validation correct rate:”, (float(n_correct_valid)/Nvalid))

            costs.append(cost)

        if show_fig:

            plt.plot(costs)

            plt.show()

Напишем функции для сохранения и загрузки, она прежние.

    def save(self, filename):

        np.savez(filename, *[p.get_value() for p in self.params])

    @staticmethod

    def load(filename, activation):

        npz = np.load(filename)

        Wx = npz[‘arr_0’]

        Wh = npz[‘arr_1’]

        bh = npz[‘arr_2’]

        h0 = npz[‘arr_3’]

        Wo = npz[‘arr_4’]

        bo = npz[‘arr_5’]

        V, M = Wx.shape

        rnn = SimpleRNN(M, V)

        rnn.set(Wx, Wh, bh, h0, Wo, bo, activation)

        return rnn

Теперь напишем функцию set, уже использовавшуюся выше.

    def set(self, Wx, Wh, bh, h0, Wo, bo, activation):

        self.f = activation

        self.Wx = theano.shared(Wx)

        self.Wh = theano.shared(Wh)

        self.bh = theano.shared(bh)

        self.h0 = theano.shared(h0)

        self.Wo = theano.shared(Wo)

        self.bo = theano.shared(bo)

        self.params = [self.Wx, self.Wh, self.bh, self.h0, self.Wo, self.bo]

        thX = T.ivector(‘X’)

        thY = T.iscalar(‘Y’)

        def recurrence(x_t, h_t1):

            h_t = self.f(self.Wx[x_t] + h_t1.dot(self.Wh) + self.bh)

            y_t = T.nnet.softmax(h_t.dot(self.Wo) + self.bo)

            return h_t, y_t

        [h, y], _ = theano.scan(

            fn=recurrence,

            outputs_info=[self.h0, None],

            sequences=thX,

            n_steps=thX.shape[0],

        )

        py_x = y[-1, 0, :]

        prediction = T.argmax(py_x)

        self.predict_op = theano.function(

            inputs=[thX],

            outputs=prediction,

            allow_input_downcast=True,

        )

        return thX, thY, py_x, prediction

Теперь можем написать функцию train_poetry.

def train_poetry():

    X, Y, V = get_poetry_classifier_data(samples_per_class=500)

    rnn = SimpleRNN(30, V)

    rnn.fit(X, Y, learning_rate=10e-7, show_fig=True, activation=T.nnet.relu, epochs=1000)

if __name__ == ‘__main__’:

train_poetry()

Запустим наш файл.

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

Понравилась статья? Поделить с друзьями:
Добавить комментарий

;-) :| :x :twisted: :smile: :shock: :sad: :roll: :razz: :oops: :o :mrgreen: :lol: :idea: :grin: :evil: :cry: :cool: :arrow: :???: :?: :!: