Сейчас мы обучим рекуррентную нейронную сеть создавать тексты в стиле Фёдора Михайловича Достоевского. Всё что ей для этого понадобится — это способность предсказывать следующую букву для строки из уже имеющихся. Не стоит ожидать от сети осмысленных фраз и предложений, но правила композиции слов, общую структуру и настроение она улавливает довольно неплохо.
Приведенный здесь результат работы получен за примерно 1 час обучения на ПК с видеокартой. Результат можно улучшить, увеличив время обучения или размер сети (её глубину или ширину слоев).
Итак, приступим!
Писать нейросеть мы будем на python, сейчас это фактически основной язык для Data Scientist. Использовать будем популярный фреймворк Keras, который позволяет очень просто описывать структуру нейронной сети и абстрагироваться от деталей её реализации. Keras внутри себя может использовать для вычислений библиотеки Tensorflow от Google или Theano. В нашем случае это будет Tensorflow. Библиотека поддерживает расчеты на GPU, так что мощная видеокарта от NVidia может ощутимо сократить время работы.
Загружаем все необходимые библиотеки, разрешаем бекенду Tensorflow увеличивать размер используемой GPU памяти при необходимости
Ввод:
import tensorflow as tf
from keras import backend as K
config = tf.ConfigProto()
config.gpu_options.allow_growth=True
sess = tf.Session(config=config)
K.set_session(sess) # разрешаем использовать больше видеопамяти
from keras.models import Sequential
from keras.layers import Dense, Activation, Dropout, LSTM,TimeDistributed, Embedding
from keras.callbacks import ModelCheckpoint, Callback, EarlyStopping, ReduceLROnPlateau
from keras.optimizers import RMSprop, Adam, SGD
import sys
import numpy as np
Using TensorFlow backend.
Загружаем текст, на котором будем обучаться и смотрим на его длину. Для более-менее интересного результата сети нужен передать на обучение больше миллиона символов текста.
Ввод:
fname = 'dostoevsky.txt' #тексты основных романов Федора Михайловича, объединенные в один файл
text = open(fname, 'r', encoding='utf-8').read()
print('corpus length:', len(text))
corpus length: 9469628
Мы будем обучать нашу сеть генерировать последовательность шаг за шагом. В качестве элементов текста можно выбрать множество разных вариантов. Это могут быть просто буквы, их пары или тройки или даже слова целиком. Посчитаем статистику этих показателей по загруженному тексту:
- Число уникальных букв
- Число уникальных пар букв (биграмм)
- Число уникальных троек букв (триграмм)
- Общее число слов и число уникальных слов
Для простоты дальше мы будем обучать нашу модель только на отдельных буквах.
Ввод:
chars = sorted(list(set(text)))
print('unique chars:', len(chars))
char_idx = dict((c, i) for i, c in enumerate(chars))
idx_char = dict((i, c) for i, c in enumerate(chars))
bigrams = []
for i in range(0, len(text)-1, 1):
bigrams.append(text[i] + text[i+1])
bigrams = sorted(list(set(bigrams)))
print('unique bigrams:', len(bigrams))
bigram_idx = dict((c, i) for i, c in enumerate(bigrams))
idx_bigram = dict((i, c) for i, c in enumerate(bigrams))
trigrams = []
for i in range(0, len(text)-2, 1):
trigrams.append(text[i] + text[i+1] + text[i+2])
trigrams = sorted(list(set(trigrams)))
print('unique trigrams:', len(trigrams))
trigram_idx = dict((c, i) for i, c in enumerate(trigrams))
idx_trigram = dict((i, c) for i, c in enumerate(trigrams))
words = text.split()
print('word count:', len(words))
words = sorted(list(set(words)))
print('unique words:', len(words))
unique chars: 162
unique bigrams: 3566
unique trigrams: 26082
word count: 1528727
unique words: 165930
Ввод:
batch_size = 1024 # можно уменьшить, если при запуску возникает ошибка аллокации памяти
track_size = len(text) // batch_size
tracks = ['' for i in range(batch_size)]
for i in range(0, track_size):
for track in range(batch_size):
tracks[track] += text[track * track_size + i]
# посмотрим что у нас получилось внутри отдельноно трека
print(tracks[4][:1000])
Далее мы будем работать только посимвольно, варианты с биграммами и триграммами оставим на будущее. Для начала соберем словари букв, биграмм и триграмм в списки. А затем определим функцию, которая будет преобразовывать переданный набор текстов в последовательности индексов для символов указанной длины.
В нашем случае мы указываем длину символа 1, т.е. использовать будем словарь отдельных букв.
Ввод:
gram_idx = [char_idx, bigram_idx, trigram_idx]
idx_gram = [idx_char, idx_bigram, idx_trigram]
# конвертируем блоки текста в наборы индексов символов
def grams(tracks, n=1):
indexed = []
for t in tracks:
track = []
for i in range(0, len(t)-n+1, n):
gram = ''
for j in range(n):
gram += t[i+j] # склеиваем каждые n символов в последовательность, получая символ, биграмму или триграмму в зависимости от n
idx = gram_idx[n-1][gram] # ищем индекс последовательности в соответствующем словаре
track.append(idx) # добавляем этот индекс в результирующий список
indexed.append(track)
return indexed
indexed = grams(tracks, 1)
vocab_size = len(gram_idx[0])
print('Vocabulary done: ', vocab_size)
Теперь нам необходимо разбить наши наборы индексов на батчи для обучения, а также подготовить целевые лейблы, которые сеть будет предсказывать.
Наша сеть будет предсказывать следующую букву для последовательности. Пример с фразой HELLO:
- мы начинаем подавать в сеть буквы одну за другой с начала. Одновременно на каждом шаге мы проверяем, что на выходе сети появилась следующая буква из этого слова.
- вначале подаем на вход H, на выходе ждем E
- на следующем шаге подаем E, ждём на выходе L
- подаем L, ждём L
- подаем L, ждём O.
Всё это время сеть сохраняет своё состояние с самого начала, что позволяет ей запомнить, в какой ситуации после L надо ответить L, а в какой O.
Для предсказания индекса буквы нам необходимо преобразовать его в one-hot вектор — длина которого равна размеру нашего словаря, а во всех позициях кроме позиции с номером текущего индекса стоят нули. В позиции индекса же будет стоять единица. Например, для словаря из [E, H, L, O] длина словаря была бы 4, индекс буквы L — 3, а её one-hot вектор 0010.
Также мы расставляем блоки из последовательностей длины N так, чтобы в каждом новом батче на позиции 1 стояла последовательность, продолжающая последовательность 1 из предыдущего батча, на позиции 2 — продолжающая текст позиции 2 предыдущего батча, и так далее.
Ввод:
seq_len = 40 # длина последовательности в батче
def onehot(n):
v = [0 for i in range(vocab_size)]
v[n] = 1
return v
# расставляем последовательности из блоков таким образом, что первая последовательность из следующего батча продолжает первую последовательность текущего. Чтобы слой LSTM мог использовать состояние из предыдущего батча
def vectorize(tracks):
track_size = len(tracks[0])
X = []
y = []
for i in range(0, track_size - seq_len + 1, seq_len):
for t in tracks:
X.append(t[i:i + seq_len])
target = [onehot(c) for c in t[i+1:i + seq_len + 1]]
y.append(target)
return X, y
x,y = vectorize(indexed)
print('Number of training samples', len(x))
print('Number of training labels', len(y))
print('Label sequence length', len(y[0]))
print('Label character one-hot vector length', len(y[0][0]))
Number of training samples 236544
Number of training labels 236544
Label sequence length 40
Label character one-hot vector length 162
Приготовим нашу рекуррентную сеть. В начале идёт embedding-слой, преобразующий индексы входной последовательности в dense вектор фиксированного размера. Затем несколько рекуррентных LSTM слоёв и один полносвязный слой с число выходов, равным размеру нашего словаря. В качестве loss-функции нам нужна кросс энтропия, в качестве оптимизатора будем использовать популярный сейчас алгоритм Adam, также в нём важно не забыть установить clipnorm — ограничение на размер градиентов.
Ввод:
cells = 512
drop = 0.2
embed = 30 # размер вектора символа (эмбеддинга)
layers = 2
lr = 0.01
clip = 5.0 # ограничение градиентов при оптимизации
stateful=True # сохраняем состояние слоя между батчами
model = Sequential()
model.add(Embedding(vocab_size, embed, batch_input_shape=(batch_size, seq_len)))
for l in range(layers):
model.add(LSTM(cells, return_sequences=True, stateful=stateful, dropout=drop))
model.add(Dense(vocab_size))
model.add(Activation('softmax'))
optimizer = Adam(lr, clipnorm=clip)
model.compile(loss="categorical_crossentropy", optimizer=optimizer)
model.summary()
Layer (type) Output Shape Param #
=================================================================
embedding_1 (Embedding) (1024, 40, 30) 4860
_________________________________________________________________
lstm_1 (LSTM) (1024, 40, 512) 1112064
_________________________________________________________________
lstm_2 (LSTM) (1024, 40, 512) 2099200
_________________________________________________________________
dense_1 (Dense) (1024, 40, 162) 83106
_________________________________________________________________
activation_1 (Activation) (1024, 40, 162) 0
=================================================================
Total params: 3,299,230
Trainable params: 3,299,230
Non-trainable params: 0
_________________________________________________________________
Как видно, размер сети получился порядка 3.3 миллиона параметров.
Ввод:
def get_callbacks(filepath, patience=5):
learning_rate_reduction = ReduceLROnPlateau(monitor='loss',
patience=patience,
verbose=1,
factor=0.5,
min_lr=0.00001)
es = EarlyStopping('loss', verbose=1, min_delta=0.02, patience=patience, mode="min")
return [learning_rate_reduction, es]
Функция sample позволит нам выбирать из выходов сети не просто самый вероятный символ, а получать случайный символ из распределения с заданными вероятностями. Параметр температуры позволяет регулировать степень строгости выбора, низкая температура приведет к более консервативной стратегии, в то время как с низкой температурой редкие символы будут выбираться немного чаще.
Ввод:
def sample(preds, temperature=0.5):
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)
Наша изначальная модель была построена исходя из указанного размера батча, а значит плохо подходит для работы на одной последовательности. Создадим её копию с размером батча 1 и перенесем туда веса обученной модели
Ввод:
def predictive_model(main_model):
# меняем размер батча на 1, чтобы подавать всего один блок текста
model = Sequential()
model.add(Embedding(vocab_size, embed, batch_input_shape=(1, seq_len)))
for l in range(layers):
model.add(LSTM(cells, return_sequences=True, stateful=stateful, dropout=drop))
model.add(Dense(vocab_size))
model.add(Activation('softmax'))
optimizer = Adam(lr, clipnorm=clip)
model.compile(loss="categorical_crossentropy", optimizer=optimizer)
old_weights = main_model.get_weights()
model.set_weights(old_weights)
return model
Функция test будет генерировать новый символ по заданной строке, а затем заново подавать полученную строку на вход сети.
Ввод:
# генерируем новый символ по имеющимся с помощью нашей модели, добавляем его к строке и опять генерируем уже по этой дополненной строке
def test(model, l=500, seed = None, t=0.5):
start_from = np.random.randint(len(text)-seq_len)+seq_len
seed_string = text[start_from:start_from + seq_len*3] if seed is None else seed
print('\n\nSeed: ', seed_string)
print('----')
sys.stdout.write(seed_string)
prmodel = predictive_model(model)
for i in range(l):
prmodel.reset_states()
padlen = (len(seed_string) // seq_len +1) * seq_len
seed_string = seed_string.rjust(padlen)[-seq_len*3:]
test_tracks = [seed_string]
tidx = grams(test_tracks)
xt, _ = vectorize(tidx)
preds = prmodel.predict(np.array(xt), batch_size=1, verbose=0)
preds = preds[-1][-1] # last symbol of last sequence
next_item = idx_char[sample(preds, t)]
seed_string = seed_string + next_item
sys.stdout.write(next_item)
sys.stdout.flush()
Пришло время обучить сеть. Попробуем делать это на протяжении 50 итераций, периодически (раз в 3 итерации) оценивая качество сгенерированного сетью текста
Ввод:
for iteration in range(1, 51):
print('\nIteration', iteration)
model_name = 'char_%s_%d_%d_%.1f_%d.h5' % (fname, layers, cells, drop, iteration)
history=model.fit(
np.array(x), np.array(y),
batch_size=batch_size,
epochs=1,
verbose=1,
shuffle=False,
callbacks=get_callbacks(filepath=model_name)
)
model.save_weights(model_name, overwrite=True)
model.reset_states()
if iteration%3 == 0:
test(model)
Iteration 2 Epoch 1/1 236544/236544 [==============================] — 188s — loss: 3.1676
Iteration 3 Epoch 1/1 236544/236544 [==============================] — 191s — loss: 2.5591 >>>
икальным объяснением, несмотря на то что дело плевое; я знаю его еще с Петербурга. К тому же весь анекдот делает только —- икальным объяснением, несмотря на то что дело плевое; я знаю его еще с Петербурга. К тому же весь анекдот делает только на не воглану и вот двадь не сопривила не на прокоть пробовореться на солать не могда уго всего же самовеле дервать, потодал что старет придорил отанила спа не прогость же того и не сот отводил все ста стастве. Прегавове, скоронить е всё логда на с нимененно послей на проемала не не стольто вы мне мни сам, кня на старате телова пословаеть так на на смество миже все преседь слуго все в неста не сами призчите вот, не на тогда не закорорно как трязь потисту сопросенить лестольно из семи его на с
Iteration 4 Epoch 1/1 236544/236544 [==============================] — 186s — loss: 2.0483
Iteration 5 Epoch 1/1 236544/236544 [==============================] — 190s — loss: 1.8013
Iteration 6 Epoch 1/1 236544/236544 [==============================] — 190s — loss: 1.6861 >>>
вдруг неожиданно. — Как тем хуже? — Хуже. — Не понимаю. — Друг мой, друг мой, ну пусть в Сибирь, в Архангел —- вдруг неожиданно. — Как тем хуже? — Хуже. — Не понимаю. — Друг мой, друг мой, ну пусть в Сибирь, в Архангела Ивановна. То не по своего подсудим на дворе, что так, все все хоть положения и все это было вопросы всё просто под восторгами на него на вами спросил в говорить и в этом вашим с длинным себя. Вы вы ответил в демовым столько не могла какого не верите служал же довольно под всех совершенно закричала, что только было вы в комнате в нем до всеми продолжал и вдруг продолжал было не было образом деле не почему-то с первого не знал, вот тогда уж не знаю, точно так ли сильно в ней положительно всей не
Iteration 7 Epoch 1/1 236544/236544 [==============================] — 188s — loss: 1.6235
Iteration 8 Epoch 1/1 236544/236544 [==============================] — 188s — loss: 1.5817
Iteration 9 Epoch 1/1 236544/236544 [==============================] — 189s — loss: 1.5523 >>>
в монастырские ворота пешком. Кроме Федора Павловича, остальные трое кажется никогда не видали никакого монастыря, а Миу —- в монастырские ворота пешком. Кроме Федора Павловича, остальные трое кажется никогда не видали никакого монастыря, а Миусья от дверь обратил весь не полицей, слышал и с тобой на своей нетерпением про себя навсегда в самом долго и получил на того, которого же со столу просто под воротились к деревне, то есть до сих в креста, но не в полу примерника и не потому что тотчас же было не мог подле после него с неестественное все это самовольно и во всем доме проговорил с нелепый и и ответила в каком-то любопытством своего например, что всё равно не только и от после последней стола. Вот по свою для него последнее сторон
Iteration 10 Epoch 1/1 236544/236544 [==============================] — 190s — loss: 1.5294
Iteration 11 Epoch 1/1 236544/236544 [==============================] — 193s — loss: 1.5108
Iteration 12 Epoch 1/1 236544/236544 [==============================] — 175s — loss: 1.4962 >>>
чал и долго обдумывал. — Штука в том: я задал себе один раз такой вопрос: что если бы, например, на моем месте случи —- чал и долго обдумывал. — Штука в том: я задал себе один раз такой вопрос: что если бы, например, на моем месте случилось и даже до сих пор не было уже, чтобы принял его на «собственных сил за красного стороны, по приходить явился на покойным волнением уверена нахмурился и что он все до рассказывался к нему на полным положении. Пойдемте, она заплачения и не мог от всех пред том и уже не знает и может, с негодованиями человеком, по крайней больше и открылась от меня и умел в болезни послали. Он не простить на меня в однем доме подумали его. Если бы он в том, что вы из него он был в горячее и подле обойхнулся в
Iteration 13 Epoch 1/1 236544/236544 [==============================] — 133s — loss: 1.4842
Iteration 14 Epoch 1/1 236544/236544 [==============================] — 133s — loss: 1.4732
Iteration 15 Epoch 1/1 236544/236544 [==============================] — 132s — loss: 1.4645 >>>
Стоило ли это теперь хоть какой-нибудь тревоги, в свою очередь, хотя какого-нибудь даже внимания! Он стоял, читал, слуш —- Стоило ли это теперь хоть какой-нибудь тревоги, в свою очередь, хотя какого-нибудь даже внимания! Он стоял, читал, слушают из всех пор не потом и старика и бросились о том, что он Лебядкина была сел в комнате с нею с дверью собственные вечер в господин. — Нет, не знаю на красного любовь этого была от него в самом великодушным предмете, и не верите меня с тобой, и не подумал в комнате, но он высказал все два стороны совсем не случилось в голову. Вы тоже после принести и не потому что просто не хочу! — спросил он вдруг в ту же словах на меня на него и не видел и уже не буду повернулась было слевение. Нарочно
Iteration 16 Epoch 1/1 236544/236544 [==============================] — 131s — loss: 1.4558
Iteration 17 Epoch 1/1 236544/236544 [==============================] — 132s — loss: 1.4484
Iteration 18 Epoch 1/1 236544/236544 [==============================] — 132s — loss: 1.4423 >>>
мне было почему-то ужасно совестно заговаривать о Полине; он же сам ни слова о ней не спросил. Я рассказал ему про бабу —- мне было почему-то ужасно совестно заговаривать о Полине; он же сам ни слова о ней не спросил. Я рассказал ему про бабушкой и познакомился образованным под всего последнею голову со голову. Но не понял гостя смотрела на него и про то не обернулся и между тем и больше настоящий и все равно обратилась на два доктор и на положения и не получил на него увидеть на этот раз ответил и должно быть, потому что в этот раз так сказать, и уж не прошептал бы не понять, что она всегда все спокойно прокурор вздрагал за своей руку. Он обойтись, как бы сказать, и он пристально поступила на меня наконец, подхватив перепалительно
Iteration 19 Epoch 1/1 236544/236544 [==============================] — 132s — loss: 1.4355
Iteration 20 Epoch 1/1 236544/236544 [==============================] — 133s — loss: 1.4321
Iteration 21 Epoch 1/1 236544/236544 [==============================] — 134s — loss: 1.4265 >>>
ра кончить! — Объяснитесь, Наталья Николаевна, — подхватил князь, — убедительно прошу вас! Я уже два часа слышу об —- ра кончить! — Объяснитесь, Наталья Николаевна, — подхватил князь, — убедительно прошу вас! Я уже два часа слышу обо всем даже теперь в этом обществе и слушать, что он совсем не знал «на нее». Но все могу от него была в том, что назад на него с первого взгляда по крайней мере всегда так всегда подлецой, что не подробно стал в таком половину и с нею ни за что и не сказал его с тем и стало быть все время прошло подумал, что только что она была только хоть и положительно стала допустить глазами, — подумал он вдруг он мелькнула и ответила в своей месте в беспокойстве в дела. Поверьте, что и может быть, не думаю
Iteration 22 Epoch 1/1 236544/236544 [==============================] — 132s — loss: 1.4226
Iteration 23 Epoch 1/1 236544/236544 [==============================] — 131s — loss: 1.4182
Iteration 24 Epoch 1/1 236544/236544 [==============================] — 129s — loss: 1.4148 >>>
ила Авдотья Романовна. — Вы думаете, — с жаром продолжала Пульхерия Александровна, — его бы остановили тогда мои с —- ила Авдотья Романовна. — Вы думаете, — с жаром продолжала Пульхерия Александровна, — его бы остановили тогда мои старухи не брать предложение просто подумал, что по крайней мере в моем деле и с тем на него гордости в другой раз, что он не потому в безородном отца в первый раз был не подумали. Пусть на меня показалось в недоумении на ней и вышел к нему. Потому что так на него надо было не надо было сказать на делам и подсудимого и в подлец свои лет в половину в то же моей себе его с первого последнее пользу на меня в показать себя с свою неужели и в нем повторять общество. Он понял теперь в глаза от двух под
Iteration 25 Epoch 1/1 236544/236544 [==============================] — 133s — loss: 1.4118
Iteration 26 Epoch 1/1 236544/236544 [==============================] — 178s — loss: 1.4093
Iteration 27 Epoch 1/1 236544/236544 [==============================] — 135s — loss: 1.4065 >>>
рены, благодушнейший, искреннейший и благороднейший князь, — вскричал Лебедев в решительном вдохновении, — будьте увер —- рены, благодушнейший, искреннейший и благороднейший князь, — вскричал Лебедев в решительном вдохновении, — будьте уверяли ваше минуты, — как тот с нею моим добрым с лестнице. И вот я не обращаюсь, что я ведь не отвечал и пристально поступил и понял, что я только уж как я не стал все и так по лежать на восторге в городе и по крайней мере даже не со мной. Но он ведь только не имел в таком случае и при этом странной стороны, но вы его даже не поставить. А он потому что я еще не понимаешь, что я как бы не знаю, что он в нем долгими с тобой в перед нею и не видал его в руках и последних старуха и все это удивлялись
Теперь мы можем посмотреть как влияет температура сэмплинга на выводимый моделью текст. Используем для этого одну и ту же начальную последовательность и посмотрим на результаты
Ввод:
seed = "У нас в Малом зале до сих пор проходят"
test(model, 300, seed, 0.1)
test(model, 300, seed, 0.5)
test(model, 300, seed, 0.7)
Seed: У нас в Малом зале до сих пор проходят — У нас в Малом зале до сих пор проходят на свою разом и не после прежнего правда с тревожностью, выразился и смотрел на положении. Но просто отвечала она принять меня и сказал его и пред нею, как бы во всех своего проклятий принес в картину и стал простодушное ветром и воздух не понимаю, что не давать предстоящего и судорога. Она убил с
Seed: У нас в Малом зале до сих пор проходят — У нас в Малом зале до сих пор проходят картины. Теперь сейчас они остановившись пошле дорогу совсем и почти неожиданное своего затрастно на собой в том, чтобы не сказали его ответ к нему прочтить виде и деньги в руках голова — учительно и старику с каким-то полячной столы погросить, но на него. — Всё больше-то и распоря
Автор: Олег Шляжко
Ссылка на github: http://github.com/ollmer/neural_experiments/blob/master/char_rnn_dostoevsky.ipynb