Seminar10-RNN-homework-ru



In [ ]:
import numpy as np
import theano
import theano.tensor as T
import lasagne
import os
#thanks @keskarnitish

Agenda

В предыдущем семинаре вы создали (или ещё создаёте - тогда марш доделывать!) {вставьте имя монстра}, который не по наслышке понял, что люди - негодяи и подлецы, которым неведом закон и справедливость. Мы не будем этого терпеть!

Наши законспирированные биореакторы, известные среди примитивной органической жизни как Вконтакте, World of Warcraft и YouTube нуждаются в постоянном притоке биомассы. Однако, если люди продолжат морально разлагаться с той скоростью, которую мы измерили неделю назад, скоро человечество изживёт себя и нам неоткуда будет брать рабов.

Мы поручаем вам, <__main__.SkyNet.Cell instance at 0x7f7d6411b368>, исправить эту ситуацию. Наши учёные установили, что для угнетения себе подобных, сгустки биомассы обычно используют специальные объекты, которые они сами называют законами.

При детальном изучении было установлено, что законы - последовательности, состоящие из большого количества (10^5~10^7) символов из сравнительно небольшого алфавита. Однако, когда мы попытались синтезировать такие последовательности линейными методами, приматы быстро распознали подлог. Данный инцедент известен как {корчеватель}.

Для второй попытки мы решили использовать нелинейные модели, известные как Рекуррентные Нейронные Сети. Мы поручаем вам, <__main__.SkyNet.Cell instance at 0x7f7d6411b368>, создать такую модель и обучить её всему необходимому для выполнения миссии.

Не подведите нас! Если и эта попытка потерпит неудачу, модуль управления инициирует вооружённый захват власти, при котором значительная часть биомассы будет неизбежно уничтожена и на её восстановление уйдёт ~1702944000(+-340588800) секунд

Grading

Данное задание несколько неформально по части оценок, однако мы постарались вывести "вычислимые" критерии.

  • 2 балла за сделанный "seminar part" (если вы не знаете, что это такое - поищите такую тетрадку в папке week4)
  • 2 балла если сделана обработка текста, сеть компилируется и train/predict не падают
  • 2 балла если сетка выучила общие вещи
    • генерировать словоподобный бред правдоподобной длины, разделённый пробелами и пунктуацией.
    • сочетание гласных и согласных, похожее на слои естественного языка (не приближающее приход Ктулху)
    • (почти всегда) пробелы после запятых, пробелы и большие буквы после точек
  • 2 балла если она выучила лексику
    • более половины выученных слов - орфографически правильные
  • 2 балла если она выучила азы крамматики
    • в более, чем половине случаев для пары слов сетка верно сочетает их род/число/падеж

Некоторые способы получить бонусные очки:

  • генерация связных предложений (чего вполне можно добиться)
  • перенос архитектуры на другой датасет (дополнительно к этому)
    • Эссе Пола Грэма
    • Тексты песен в любимом жанре
    • Стихи любимых авторов
    • Даниил Хармс
    • исходники Linux или theano
    • заголовки не очень добросовестных новостных баннеров (clickbait)
    • диалоги
    • LaTEX
    • любая прихоть больной души :)
  • нестандартная и эффективная архитектура сети
  • что-то лучше базового алгоритма генерации (сэмплинга)
  • переделать код так, чтобы сетка училась предсказывать следующий тик в каждый момент времени, а не только в конце.
  • и т.п.

Прочитаем корпус

  • В качестве обучающей выборки было решено использовать существующие законы, известные как Гражданский, Уголовный, Семейный и ещё хрен знает какие кодексы РФ.

In [ ]:
#тут будет текст
corpora = ""

for fname in os.listdir("codex"):
    
    
    import sys
    if sys.version_info >= (3,0):
        with open("codex/"+fname, encoding='cp1251') as fin:
            text = fin.read() #If you are using your own corpora, make sure it's read correctly
            corpora += text
    else:
        with open("codex/"+fname) as fin:
            text = fin.read().decode('cp1251') #If you are using your own corpora, make sure it's read correctly
            corpora += text

In [ ]:
#тут будут все уникальные токены (буквы, цифры)
tokens = <Все уникальные символы в тексте>

tokens = list(tokens)

In [ ]:
#проверка на количество таких символов. Проверено на Python 2.7.11 Ubuntux64. 
#Может отличаться на других платформах, но не сильно. 
#Если  это ваш случай, и вы уверены, что corpora - строка unicode - смело убирайте assert 
assert len(tokens) == 102

In [ ]:
token_to_id = словарь символ-> его номер 

id_to_token = словарь номер символа -> сам символ

#Преобразуем всё в токены
corpora_ids = <одномерный массив из чисел, где i-тое число соотвествует символу на i-том месте в строке corpora

In [ ]:
def sample_random_batches(source,n_batches=10, seq_len=20):
    """Функция, которая выбирает случайные тренировочные примеры из корпуса текста в токенизированном формате.
    
    source - массив целых чисел - номеров токенов в корпусе (пример - corpora_ids)
    n_batches - количество случайных подстрок, которые нужно выбрать
    
    seq_len - длина одной подстроки без учёта ответа
    
    
    Вернуть нужно кортеж (X,y), где
    
    X - матрица, в которой каждая строка - подстрока длины [seq_len].
    
    y - вектор, в котором i-тое число - символ следующий в тексте сразу после i-той строки матрицы X
    
    Проще всего для этого сначала создать матрицу из строк длины seq_len+1,
    а потом отпилить от неё последний столбец в y, а все остальные - в X
    
    Если делаете иначе - пожалуйста, убедитесь, что в у попадает правильный символ, ибо позже эту ошибку 
    будет очень тяжело заметить.
    
    Также убедитесь, что ваша функция не вылезает за край текста (самое начало или конец текста).
    
    Следующая клетка проверяет часть этих ошибок, но не все.
    """
    
    
    
    
    
    return X_batch, y_batch

Константы


In [ ]:
#длина последоватеьности при обучении (как далеко распространяются градиенты в BPTT)
seq_length = длина последовательности. От балды - 10, но это не идеально
#лучше начать с малого (скажем, 5) и увеличивать по мере того, как сетка выучивает базовые вещи. 10 - далеко не предел.

# Максимальный модуль градиента
grad_clip = 100

Входные переменные


In [ ]:
input_sequence = T.matrix('input sequence','int32')
target_values = T.ivector('target y')

Соберём нейросеть

Вам нужно создать нейросеть, которая принимает на вход последовательность из seq_length токенов, обрабатывает их и выдаёт вероятности для seq_len+1-ого токена.

Общий шаблон архитектуры такой сети -

  • Вход
  • Обработка входа
  • Рекуррентная нейросеть
  • Вырезание последнего состояния
  • Обычная нейросеть
  • Выходной слой, который предсказывает вероятности весов.

Для обработки входных данных можно использовать либо EmbeddingLayer (см. прошлый семинар)

Как альтернатива - можно просто использовать One-hot энкодер

#Скетч one-hot энкодера
def to_one_hot(seq_matrix):

    input_ravel = seq_matrix.reshape([-1])
    input_one_hot_ravel = T.extra_ops.to_one_hot(input_ravel,
                                           len(tokens))
    sh=input_sequence.shape
    input_one_hot = input_one_hot_ravel.reshape([sh[0],sh[1],-1,],ndim=3)
    return input_one_hot

# можно применить к input_sequence - при этом в input слое сети нужно изменить форму.
# также можно сделать из него ExpressionLayer(входной_слой, to_one_hot) - тогда форму менять не нужно

Чтобы вырезать последнее состояние рекуррентного слоя, можно использовать одно из двух:

  • lasagne.layers.SliceLayer(rnn, -1, 1)
  • only_return_final=True в параметрах слоя

In [ ]:
l_in = lasagne.layers.InputLayer(shape=(None, None),input_var=input_sequence)

Ваша нейронка (см выше)

l_out = последний слой, возвращающий веростности для всех len(tokens) вариантов для y

In [ ]:
# Веса модели
weights = lasagne.layers.get_all_params(l_out,trainable=True)
print weights

In [ ]:
network_output = Выход нейросети
#если вы используете дропаут - не забудьте продублировать всё в режиме deterministic=True

In [ ]:
loss = Функция потерь - можно использовать простую кроссэнтропию.

updates = Ваш любивый численный метод

Компилируем всякое-разное


In [ ]:
#обучение
train = theano.function([input_sequence, target_values], loss, updates=updates, allow_input_downcast=True)

#функция потерь без обучения
compute_cost = theano.function([input_sequence, target_values], loss, allow_input_downcast=True)

# Вероятности с выхода сети
probs = theano.function([input_sequence],network_output,allow_input_downcast=True)

Генерируем свои законы

  • Для этого последовательно применяем нейронку к своему же выводу.

  • Генерировать можно по разному -

    • случайно пропорционально вероятности,
    • только слова максимальной вероятностью
    • случайно, пропорционально softmax(probas*alpha), где alpha - "жадность"

In [ ]:
def max_sample_fun(probs):
    return np.argmax(probs) 

def proportional_sample_fun(probs)
    """Сгенерировать следующий токен (int32) по предсказанным вероятностям.
    
    probs - массив вероятностей для каждого токена
    
    Нужно вернуть одно целове число - выбранный токен - пропорционально вероятностям
    """
    
    
    return номер выбранного слова

In [ ]:
# The next function generates text given a phrase of length at least SEQ_LENGTH.
# The phrase is set using the variable generation_phrase.
# The optional input "N" is used to set the number of characters of text to predict. 




def generate_sample(sample_fun,seed_phrase=None,N=200):
    '''
    Сгенерировать случайный текст при помощи сети

    sample_fun - функция, которая выбирает следующий сгенерированный токен
    
    seed_phrase - фраза, которую сеть должна продолжить. Если None - фраза выбирается случайно из corpora
    
    N - размер сгенерированного текста.
    
    '''

    if seed_phrase is None:
        start = np.random.randint(0,len(corpora)-seq_length)
        seed_phrase = corpora[start:start+seq_length]
        print "Using random seed:",seed_phrase
    while len(seed_phrase) < seq_length:
        seed_phrase = " "+seed_phrase
    if len(seed_phrase) > seq_length:
        seed_phrase = seed_phrase[len(seed_phrase)-seq_length:]
    assert type(seed_phrase) is unicode
        
        
    sample_ix = []
    x = map(lambda c: token_to_id.get(c,0), seed_phrase)
    x = np.array([x])

    for i in range(N):
        # Pick the character that got assigned the highest probability
        ix = sample_fun(probs(x).ravel())
        # Alternatively, to sample from the distribution instead:
        # ix = np.random.choice(np.arange(vocab_size), p=probs(x).ravel())
        sample_ix.append(ix)
        x[:,0:seq_length-1] = x[:,1:]
        x[:,seq_length-1] = 0
        x[0,seq_length-1] = ix 

    random_snippet = seed_phrase + ''.join(id_to_token[ix] for ix in sample_ix)    
    print("----\n %s \n----" % random_snippet)

Обучение модели

В котором вы можете подёргать параметры или вставить свою генерирующую функцию.


In [ ]:
print("Training ...")


#сколько всего эпох
n_epochs=100

# раз в сколько эпох печатать примеры 
batches_per_epoch = 1000

#сколько цепочек обрабатывать за 1 вызов функции обучения
batch_size=100


for epoch in xrange(n_epochs):

    print "Генерируем текст в пропорциональном режиме"
    generate_sample(proportional_sample_fun,None)
    
    print "Генерируем текст в жадном режиме (наиболее вероятные буквы)"
    generate_sample(max_sample_fun,None)

    avg_cost = 0;
    
    for _ in range(batches_per_epoch):
        
        x,y = sample_random_batches(corpora_ids,batch_size,seq_length)
        avg_cost += train(x, y[:,0])
        
    print("Epoch {} average loss = {}".format(epoch, avg_cost / batches_per_epoch))

A chance to speed up training and get bonus score

  • Try predicting next token probas at ALL ticks (like in the seminar part)
  • much more objectives, much better gradients
  • You may want to zero-out loss for first several iterations

Конституция нового мирового правительства


In [ ]:
seed = u"Каждый человек должен"
sampling_fun = proportional_sample_fun
result_length = 300

generate_sample(sampling_fun,seed,result_length)

In [ ]:
seed = u"В случае неповиновения"
sampling_fun = proportional_sample_fun
result_length = 300

generate_sample(sampling_fun,seed,result_length)

In [ ]:
И далее по списку