Автор статьи: Рустем Галиев

IBM Senior DevOps Engineer & Integration Architect. Официальный DevOps ментор и коуч в IBM

Одним из самых крутых и, возможно, самых неприятных приложений NLP является генерация текста. Способность генерировать убедительный текст с помощью ИИ имеет широкое применение, от чат-ботов до создания художественной литературы или фейковых новостей. Сегодня мы рассмотрим создание фейковой фантастики на основе романа Льюиса Кэрролла «Алиса в стране чудес».

Из реквизитов на нужен Python и Tensorflow.

Мы загружаем некоторые импорты для обработки текста и загрузки:

import tensorflow as tf

import numpy as np
import os
import time
import urllib.request
import re
tf.__version__

Загружаем книгу из Project Gutenberg с помощью:

url = "https://www.gutenberg.org/files/11/11-0.txt"
file = urllib.request.urlopen(url)
text = [line.decode('utf-8') for line in file]
text = ''.join(text)
text = re.sub(' +',' ',text)
text = re.sub(r'[^A-Za-z.,!\r ]+', '', text)
text = text[1150:]
text[:200]


После того, как книга скачана, мы загружаем текст и очищаем его регулярным выражением, используя re.sub. Затем мы извлекаем только начало книги и выгружаем ее на консоль.

Токенизировать и кодировать

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

Мы можем извлечь словарь токенов символов из текста с помощью:

vocab = sorted(set(text))
",".join(vocab)

Затем создадим кодировку для символов и функций отображения с помощью:

char2idx = {u:i for i, u in enumerate(vocab)}
idx2char = np.array(vocab)

text_as_int = np.array([char2idx[c] for c in text])
[f"{char} = {i}" for char,i in zip(char2idx, range(20))]

Функции отображения char2idx и idx2char отображают символы в индексы и обратно. Эти функции помогают нам кодировать, а затем декодировать символы.

Затем мы можем построить обучающие наборы с помощью:

seq_length = 100
examples_per_epoch = len(text)//(seq_length+1)

char_dataset = tf.data.Dataset.from_tensor_slices(text_as_int)

[idx2char[i.numpy()] for i in char_dataset.take(5)]

Переменная example_per_epoch — это количество выборок или фрагментов текста, которые мы будем передавать модели. char_dataset — это преобразование кодировок text_as_int в тензоры.

После разметки текста и закодированных символов переходим к построению обучающих последовательностей.

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

Создать их достаточно легко с помощью:

sequences = char_dataset.batch(seq_length+1, drop_remainder=True)
[repr(''.join(idx2char[item.numpy()])) for item in sequences.take(5)]

Затем мы берем и выгружаем 5 обучающих последовательностей в качестве примера.

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

Создать входную и таргетовую последовательности можно с помощью простой функции карты:

@tf.autograph.experimental.do_not_convert
def split_input_target(chunk):
    input_text = chunk[:-1]
    target_text = chunk[1:]
    return input_text, target_text

dataset = sequences.map(split_input_target)

После чего можем взглянуть на входную и таргетовую последовательность с помощью:

for input_example, target_example in  dataset.take(1):
  print ('Input data: ', repr(''.join(idx2char[input_example.numpy()])))
  print ('Target data:', repr(''.join(idx2char[target_example.numpy()])))

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

for i, (input_idx, target_idx) in enumerate(zip(input_example[:5], target_example[:5])):
    print("Step {:4d}".format(i))
    print("  input: {} ({:s})".format(input_idx, repr(idx2char[input_idx])))
    print("  expected output: {} ({:s})".format(target_idx, repr(idx2char[target_i

На каждом шаге примера показаны входные данные, а затем ожидаемые выходные данные, к которым мы будем обучать сеть.

Построение и обучение модели

Все данные готовы, поэтому мы можем перейти к построению модели и ее обучению.

Однако перед этим установим некоторые гиперпараметры с помощью:

BATCH_SIZE = 64
BUFFER_SIZE = 10000
dataset = dataset.shuffle(BUFFER_SIZE).batch(BATCH_SIZE, drop_remainder=True)
dataset

vocab_size = len(vocab)
embedding_dim = 256
rnn_units = 1024
rnn_units_2 = 512

Также в этом коде мы извлекаем данные в обучающие пакеты и выводим форму набора данных.

Далее можем создать модель с помощью:

model = tf.keras.Sequential([
  tf.keras.layers.Embedding(vocab_size, embedding_dim,
                            batch_input_shape=[BATCH_SIZE, None]),
  tf.keras.layers.GRU(rnn_units,
                      return_sequences=True,
                      stateful=True,
                      recurrent_initializer='glorot_uniform'), 
  tf.keras.layers.GRU(rnn_units_2,
                      return_sequences=True,
                      stateful=True,
                      recurrent_initializer='glorot_uniform'),  
  tf.keras.layers.Dense(vocab_size)
])
model.summary()

В этой модели используются 2 уровня RNN типа GRU или вентилируемого рекуррентного блока. Уровни GRU проще, чем LSTM, и не требуют ввода состояния.

Для этого примера определим пользовательскую функцию потерь, а затем скомпилируем модель следующим образом:

def loss(labels, logits):
  return tf.keras.losses.sparse_categorical_crossentropy(labels, logits, from_logits=True)

model.compile(optimizer='adam', loss=loss)

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

checkpoint_dir = './training_checkpoints'
checkpoint_prefix = os.path.join(checkpoint_dir, "ckpt_{epoch}")
checkpoint_callback=tf.keras.callbacks.ModelCheckpoint(
    filepath=checkpoint_prefix,
    save_weights_only=True)

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

epochs = 1
epochs = 5
epochs = 10

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

history = model.fit(dataset, epochs=epochs, callbacks=[checkpoint_callback])

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

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

Для начала нам нужна пользовательская функция для запроса модели и генерации текста:

def generate_text(model, start_string, temp, gen_chars):     
  input_eval = [char2idx[s] for s in start_string]
  input_eval = tf.expand_dims(input_eval, 0)  
  text_generated = []
  model.reset_states()
  for i in range(gen_chars):
    predictions = model(input_eval)      
    predictions = tf.squeeze(predictions, 0)
    predictions = predictions / temp
    predicted_id = tf.random.categorical(predictions, num_samples=1)[-1,0].numpy()
    input_eval = tf.expand_dims([predicted_id], 0)
    text_generated.append(idx2char[predicted_id])  
  return (start_string + ''.join(text_generated))

Эта функция принимает в качестве входных данных модель, начальную строку, температуру и количество символов для генерации. Температура используется для определения предсказуемости текста. Более низкая температура (0,25) создает интеллектуальный текст. В то время как более высокая температура (2.0) генерирует более уникальный текст. Более высокие температуры могут привести к бессмысленному тексту.

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

model = tf.keras.Sequential([
  tf.keras.layers.Embedding(vocab_size, embedding_dim,
                            batch_input_shape=[1, None]),
  tf.keras.layers.GRU(rnn_units,
                      return_sequences=True,
                      stateful=True,
                      recurrent_initializer='glorot_uniform'),
  tf.keras.layers.GRU(rnn_units_2,
                      return_sequences=True,
                      stateful=True,
                      recurrent_initializer='glorot_uniform'), 
  tf.keras.layers.Dense(vocab_size)
])
model.summary()
model.load_weights(tf.train.latest_checkpoint(checkpoint_dir))
model.build(tf.TensorShape([1, None]))

С перестроенной моделью теперь мы можем сгенерировать некоторый текст:

generate_text(model, u"Alice said ", 1.0, 200)
generate_text(model, u"Alice said ", .25, 200)
generate_text(model, u"Alice said ", 2.0, 200)

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

Генерация текста может быть одной из самых интересных, но и сложных задач в NLP. Большая часть генерации текста в наши дни выполняется текстовыми преобразователями, такими как BERT или GPT. Использование преобразователей — это продвинутая процедура моделирования NLP, которая лучше подходит для всех текстовых задач.

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

Комментарии (2)


  1. MAXH0
    00.00.0000 00:00
    +1

    Знаете, в свое время меня поразил бредогенератор "Генератор Псалмов на основе цепей Маркова". Сдаётся мне, что иногда прогресс со времен Элизы только количественный, но не качественный.


  1. user18383
    00.00.0000 00:00
    +1

    Есть же более современные способы обработки слов - векторизация. Использование токенов было бы уместно для маленького гайдов к примеру классификации. Но используя термины GRU и LSTM которые вы не объясняете это показывает что цель статьи не рассказать как сделать нейросеть а пропиарить свой курс.