Время прочтения: 8 мин.

Представляем разбор применения алгоритмов машинного обучения с использованием технологий LSTM для создания текстов.

В итоге должен получиться генератор более-менее осмысленного текста. Способы создания текстов на специальную, определенную пользователем, тему затронуты не будут – но в целом, текст будет создан в том стиле, в котором написана «обучающая выборка».

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

Набор необходимых данных лежит на https://www.cs.cmu.edu/~spok/grimmtmp/. Из текстовых файлов на этом сайте будут загружены и поделены на биграммы тексты первых 100 сказок. Получится что-то вроде: [‘th’, ‘e’, ‘ki’, ‘ng’, ‘w’, ‘as’…]. Для этого есть скрипт:

url = 'https://www.cs.cmu.edu/~spok/grimmtmp/'
def dwnload(filename):
    print('Downloading file: ', dir_name+ os.sep+filename)
    filename, _ = urlretrieve(url + filename, dir_name+os.sep+filename)

filenames = [format(i, '03d')+'.txt' for i in range(1,101)]

for fn in filenames:
    dwnload(fn)

def read_data(filename):
    with open(filename) as f:
        data = tensorflow.compat.as_str(f.read())
        data = list(data.lower())
    return data

documents = []

for i in range(num_files):    
    chars = read_data('folder/'+filenames[i])
    # Break the data into bigrams
    two_grams = [''.join(chars[ch_i:ch_i+2]) for ch_i in range(0,len(chars)-2,2)]
    # Create a list of lists with bigrams
    documents.append(two_grams)

Использование символьных биграмм значительно уменьшает размер словаря по сравнению с использованием отдельных слов. И еще, для более быстрой обработки, все биграммы, встречающиеся в словаре менее 10 раз, будут заменены токеном (‘UNK’).

Хотя в TensorFlow и есть подбиблиотеки, где реализованы функции для работы с LSTM, они будут написаны самостоятельно, ведь важно не просто запустить код, но постараться разобраться в том, как это работает.

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

Гиперпараметры:

• num_nodes: обозначает количество нейронов в памяти ячейки. Когда данных много, увеличение сложности памяти ячейки может дать лучшую производительность; однако в то же время это замедляет вычисления;

• batch_size: объем данных, обрабатываемых за один шаг. Увеличение этого параметра повышает производительность, но предъявляет более высокие требования к памяти;

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

• dropout: иначе — отсев (метод регуляризации), применяется чтобы уменьшить переобучение модели и получить лучшие результаты; этот параметр показывает, какое количество информации будет случайным образом удалено из входов/выходов/переменных перед передачей их для обработки. Это может приводить к повышению производительности.

num_nodes = 128
batch_size = 64
num_unrollings = 50
dropout = 0.0 

Параметры:

• ix: это веса для входов функции;

• im: это веса, соединяющие скрытые слои с входами;

• ib: это смещение (bias).

# Connects the current input to the input gate
ix = tf.Variable(tf.truncated_normal([vocabulary_size, num_nodes], stddev=0.02))
# Connects the previous hidden state to the input gate
im = tf.Variable(tf.truncated_normal([num_nodes, num_nodes], stddev=0.02))
# Bias of the input gate
ib = tf.Variable(tf.random_uniform([1, num_nodes],-0.02, 0.02))

Примерно так же будут обозначены веса для скрытых слоев нейронной сети и на выходе.

Для получения фактических прогнозов, определим слой softmax:

# Softmax Classifier weights and biases.
w = tf.Variable(tf.truncated_normal([num_nodes, vocabulary_size], stddev=0.02))
b = tf.Variable(tf.random_uniform([vocabulary_size],-0.02,0.02))

Операции в ячейке LSTM:

• Вычисление выходных данных;

• Расчет состояния ячейки;

• Расчет внешнего скрытого состояния;

# Definition of the cell computation.
def lstm_cell(i, o, state):
    """Create an LSTM cell"""
    input_gate = tf.sigmoid(tf.matmul(i, ix) + tf.matmul(o, im) + ib)
    forget_gate = tf.sigmoid(tf.matmul(i, fx) + tf.matmul(o, fm) + fb)
    update = tf.matmul(i, cx) + tf.matmul(o, cm) + cb
    state = forget_gate * state + input_gate * tf.tanh(update)
    output_gate = tf.sigmoid(tf.matmul(i, ox) + tf.matmul(o, om) + ob)
    return output_gate * tf.tanh(state), state

Входные (тренировочные) данные и лейблы:

Входные данные для обучения представляют собой список с последовательными пакетами данных, где каждый пакет данных имеет размер [batch_size, word_size]:

train_inputs, train_labels = [],[]

# Defining unrolled training inputs
for ui in range(num_unrollings):
    train_inputs.append(tf.placeholder(tf.float32, shape=[batch_size,vocabulary_size],name='train_inputs_%d'%ui))
    train_labels.append(tf.placeholder(tf.float32, shape=[batch_size,vocabulary_size], name = 'train_labels_%d'%ui))

Данные для тестирования и валидации:

# Validation data placeholders
valid_inputs = tf.placeholder(tf.float32, shape=[1,vocabulary_size],name='valid_inputs')
valid_labels = tf.placeholder(tf.float32, shape=[1,vocabulary_size], name = 'valid_labels')
# Text generation: batch 1, no unrolling.
test_input = tf.placeholder(tf.float32, shape=[1, vocabulary_size], name = 'test_input')

Последовательные вычисления, для обработки данных:

Здесь рекурсивно рассчитываются выходы ячеек и логиты (модели логистических функций) для скрытых выходных данных из тренировочного сета.

# Keeps the calculated state outputs in all the unrollings
# Used to calculate loss
outputs = list()

# These two python variables are iteratively updated each step of unrolling
output = saved_output
state = saved_state

# Compute the hidden state (output) and cell state (state)
# recursively for all the steps in unrolling
for i in train_inputs:
    output, state = lstm_cell(i, output, state)
    output = tf.nn.dropout(output,keep_prob=1.0-dropout)
    # Append each computed output value
    outputs.append(output)

# calculate the score values
logits = tf.matmul(tf.concat(axis=0, values=outputs), w) + b

Далее, перед расчетом потерь, нужно убедиться, что значения выводов обновлены до самого последнего вычисленного значения. Для этого, добавим условие tf.control_dependencies:

with tf.control_dependencies([saved_output.assign(output),
                            saved_state.assign(state)]):
    # Calculate the training loss by
    # concatenating the results from all the unrolled time steps
    loss = tf.reduce_mean(
      tf.nn.softmax_cross_entropy_with_logits_v2(
        logits=logits, labels=tf.concat(axis=0, values=train_labels)))

Также определяем логику прямого распространения ошибки для валидационных данных. Dropout на валидационных данных использован не будет:

# Validation phase related inference logic

# Compute the LSTM cell output for validation data
valid_output, valid_state = lstm_cell(
    valid_inputs, saved_valid_output, saved_valid_state)
# Compute the logits
valid_logits = tf.nn.xw_plus_b(valid_output, w, b)

Оптимайзер:

Определим процесс оптимизации. Будем использовать Adam-оптимайзер, который на сегодняшний день является одним из лучших на основе стохастического градиента. Переменная gstep в коде используется для уменьшения скорости обучения с течением времени. Подробности будут в следующем разделе. Кроме того, будем использовать отсечение градиента, чтобы избежать так называемых «взрывов» градиента:

# Decays learning rate everytime the gstep increases
tf_learning_rate = tf.train.exponential_decay(0.001,gstep,decay_steps=1, decay_rate=0.5)

# Adam Optimizer. And gradient clipping.
optimizer = tf.train.AdamOptimizer(tf_learning_rate)
gradients, v = zip(*optimizer.compute_gradients(loss))
# Clipping gradients
gradients, _ = tf.clip_by_global_norm(gradients, 5.0)

optimizer = optimizer.apply_gradients(
    zip(gradients, v))

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

Вот как это реализовано. Сначала определяем gstep и операцию увеличения gstep (inc_gstep), следующим образом:

# learning rate decay
gstep = tf.Variable(0,trainable=False,name='global_step')
# Running this operation will cause the value of gstep
# to increase, while in turn reducing the learning rate
inc_gstep = tf.assign(gstep, gstep+1)

Затем, всякий раз, когда потери на валидационной выборке не уменьшаются, вызываем inc_gstep следующим образом:

# decrease the learning rate
decay_threshold = 5
# Keep counting perplexity increases
decay_count = 0

min_perplexity = 1e10

# Learning rate decay logic
def decay_learning_rate(session, v_perplexity):
  global decay_threshold, decay_count, min_perplexity  
  # Decay learning rate
  if v_perplexity < min_perplexity:
    decay_count = 0
    min_perplexity= v_perplexity
  else:
    decay_count += 1
      if decay_count >= decay_threshold:
        print('\t Reducing learning rate')
        decay_count = 0
        session.run(inc_gstep)

Прогнозирование:

Теперь можно делать прогнозы, просто применяя softmax к логитам, которые рассчитали ранее. Определим операцию прогнозирования для логитов на валидационных данных:

train_prediction = tf.nn.softmax(logits)
# Make sure that the state variables are updated
# before moving on to the next iteration of generation
with tf.control_dependencies([saved_valid_output.assign(valid_output),
                            saved_valid_state.assign(valid_state)]):
   valid_prediction = tf.nn.softmax(valid_logits)

Расчет потерь:

train_perplexity_without_exp = tf.reduce_sum(
   tf.concat(train_labels,0)*-tf.log(tf.concat(
       train_prediction,0)+1e-10))/(num_unrollings*batch_size)
# Compute validation perplexity
valid_perplexity_without_exp = tf.reduce_sum(valid_labels*-tf.
log(valid_prediction+1e-10))

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

Генерация текста:

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

# Text generation: batch 1, no unrolling.
test_input = tf.placeholder(tf.float32, shape=[1, vocabulary_size],
name = 'test_input')

# Same variables for testing phase
saved_test_output = tf.Variable(tf.zeros([1
                               num_nodes]),
                               trainable=False, name='test_hidden')
saved_test_state = tf.Variable(tf.zeros([1,
                              num_nodes]),
                              trainable=False, name='test_cell')

# Compute the LSTM cell output for testing data
test_output, test_state = lstm_cell(
test_input, saved_test_output, saved_test_state)


# Make sure that the state variables are updated
# before moving on to the next iteration of generation
with tf.control_dependencies([saved_test_output.assign(test_output),
                            saved_test_state.assign(test_state)]):
   test_prediction = tf.nn.softmax(tf.nn.xw_plus_b(test_output,
                                   w, b))
# Reset test state
reset_test_state = tf.group(
   saved_test_output.assign(tf.random_normal([1,
                            num_nodes],stddev=0.05)),
   saved_test_state.assign(tf.random_normal([1,
                           num_nodes],stddev=0.05)))

Пример сгенерированного текста:

they saw that the birds were at her bread, and threw behind him a comb
which made a great ridge with a thousand times thousands of spikes. That was a collier.
the nixie was at church, and thousands of spikes, they were flowers,
however, and had hewn through the glass, the children had formed a
hill of mirrors, and was so slippery that it was impossible for th
nixie to cross it. then she thought, i will go home quickly and
fetch my axe, and cut the hill of glass in half. long before she
returned, however, and had hewn through the glass, the children saw
her from afar,
and he sat down close to it,
and was so slippery that it was impossible for the
nixie to cross it.

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

Так же советую к прочтению книгу: Natural Language Processing with TensorFlow от автора Thushan Ganegedara.