<jupyter_start><jupyter_text>Character-Level LSTMНа этом семинаре поговорим про рекуррентные нейронные сети (Recurrent Neural Networ, RNN). Мы обучим модель на тексте книги "<NAME>", после чего попробуем генерировать новый текст.**Модель сможет генерировать новый текст на основе текста "<NAME>"!**Можно посмотреть полезную [статью про RNNs](http://karpathy.github.io/2015/05/21/rnn-effectiveness/) и [реализацию в Torch](https://github.com/karpathy/char-rnn). Ообщая архитектура RNN:<jupyter_code>import numpy as np
import torch
from torch import nn
import torch.nn.functional as F<jupyter_output><empty_output><jupyter_text>Загрузим данныеЗагрузим текстовый файл "<NAME>".<jupyter_code># open text file and read in data as `text`
with open('anna.txt', 'r') as f:
    text = f.read()<jupyter_output><empty_output><jupyter_text>Посмотрим первые 100 символов:<jupyter_code>text[:100]<jupyter_output><empty_output><jupyter_text>ТокенизацияВ ячейках ниже создадим два **словаря** для преобразования символов в целые числа и обратно. Кодирование символов как целых чисел упрощает их использование в качестве входных данных в сети.<jupyter_code># encode the text and map each character to an integer and vice versa

# we create two dictionaries:
# 1. int2char, which maps integers to characters
# 2. char2int, which maps characters to unique integers
chars = tuple(set(text))
int2char = dict(enumerate(chars))
char2int = {ch: ii for ii, ch in int2char.items()}

# encode the text
encoded = np.array([char2int[ch] for ch in text])<jupyter_output><empty_output><jupyter_text>Посмотрим как символы закодировались целыми числами<jupyter_code>encoded[:100]<jupyter_output><empty_output><jupyter_text>Предобработка данныхКак можно видеть на изображении char-RNN выше, сеть ожидает **one-hot encoded** входа, что означает, что каждый символ преобразуется в целое число (через созданный словарь), а затем преобразуется в вектор-столбец, где только соответствующий ему целочисленный индекс будет иметь значение 1, а остальная часть вектора будет заполнена нулями. Давайте создадим для этого функцию.<jupyter_code>def one_hot_encode(arr, n_labels):
    
    # Initialize the the encoded array
    one_hot = np.zeros((arr.size, n_labels), dtype=np.float32)
    
    # Fill the appropriate elements with ones
    one_hot[np.arange(one_hot.shape[0]), arr.flatten()] = 1.
    
    # Finally reshape it to get back to the original array
    one_hot = one_hot.reshape((*arr.shape, n_labels))
    
    return one_hot
# check that the function works as expected
test_seq = np.array([[3, 5, 1]])
one_hot = one_hot_encode(test_seq, 8)

print(one_hot)<jupyter_output>[[[0. 0. 0. 1. 0. 0. 0. 0.]
  [0. 0. 0. 0. 0. 1. 0. 0.]
  [0. 1. 0. 0. 0. 0. 0. 0.]]]<jupyter_text>Создаем mini-batch'иСоздатдим мини-батчи для обучения. На простом примере они будут выглядеть так:Возьмем закодированные символы (переданные как параметр `arr`) и разделим их на несколько последовательностей, заданных параметром `batch_size`. Каждая из наших последовательностей будет иметь длину `seq_length`. Создани батчей**1. Первое, что нам нужно сделать, это отбросить часть текста, чтобы у нас были только полные мини-батчи**Каждый батч содержит $ N\times M $ символов, где $ N $ - это размер батча (количество последовательностей в батче), а $ M $ - длина `seq_length` или количество шагов в последовательности. Затем, чтобы получить общее количество батчей $ K $, которое мы можем сделать из массива `arr`, нужно разделить длину `arr` на количество символов в батче. Когда мы узнаем количество батчей, можно получить общее количество символов, которые нужно сохранить, из `arr`: $ N * M * K $.**2. После этого нам нужно разделить `arr` на $N$ батчей** Это можно сделать с помощью `arr.reshape(size)`, где `size` - это кортеж, содержащий размеры измененного массива. Мы знаем, что нам нужно $ N $ последовательностей в батче, поэтому сделаем его размером первого измерения. Для второго измерения можем использовать «-1» в качестве заполнителя, он заполнит массив соответствующими данными. После этого должен остаться массив $N\times(M * K)$.**3. Теперь, когда у нас есть этот массив, мы можем перебирать его, чтобы получить наши мини-батчи**Идея состоит в том, что каждая партия представляет собой окно $ N\times M $ в массиве $ N\times (M * K) $. Для каждого последующего батча окно перемещается на `seq_length`. Мы также хотим создать как входной, так и выходной массивы. Это окно можно сделать с помощью `range`, чтобы делать шаги размером `n_steps` от $ 0 $ до `arr.shape [1]`, общее количество токенов в каждой последовательности. Таким образом, целые числа, которые получены из диапазона, всегда указывают на начало батча, и каждое окно имеет ширину `seq_length`.> **TODO:** Допишите функцию для создания батчей:<jupyter_code># ОТВЕТЫ
def get_batches(arr, batch_size, seq_length):
    '''Create a generator that returns batches of size
       batch_size x seq_length from arr.
       
       Arguments
       ---------
       arr: Array you want to make batches from
       batch_size: Batch size, the number of sequences per batch
       seq_length: Number of encoded chars in a sequence
    '''
    
    batch_size_total = batch_size * seq_length
    # total number of batches we can make
    n_batches = len(arr)//batch_size_total
    
    # Keep only enough characters to make full batches
    arr = arr[:n_batches * batch_size_total]
    # Reshape into batch_size rows
    arr = arr.reshape((batch_size, -1))
    
    # iterate through the array, one sequence at a time
    for n in range(0, arr.shape[1], seq_length):
        # The features
        x = arr[:, n:n+seq_length]
        # The targets, shifted by one
        y = np.zeros_like(x)
        try:
            y[:, :-1], y[:, -1] = x[:, 1:], arr[:, n+seq_length]
        except IndexError:
            y[:, :-1], y[:, -1] = x[:, 1:], arr[:, 0]
        yield x, y<jupyter_output><empty_output><jupyter_text>ПротестируемТеперь создадим несколько наборов данных, и проверим, что происходит, когда мы создаем батчи.<jupyter_code>batches = get_batches(encoded, 8, 50)
x, y = next(batches)
# printing out the first 10 items in a sequence
print('x\n', x[:10, :10])
print('\ny\n', y[:10, :10])<jupyter_output>x
 [[28 29 45 56 54 43 37 77 65  1]
 [63 46 79 77 54 29 45 54 77 45]
 [43 79 39 77 46 37 77 45 77 38]
 [63 77 54 29 43 77 62 29 30 43]
 [77 63 45 32 77 29 43 37 77 54]
 [62 70 63 63 30 46 79 77 45 79]
 [77 76 79 79 45 77 29 45 39 77]
 [49 81 64 46 79 63 21 47 34 77]]

y
 [[29 45 56 54 43 37 77 65  1  1]
 [46 79 77 54 29 45 54 77 45 54]
 [79 39 77 46 37 77 45 77 38 46]
 [77 54 29 43 77 62 29 30 43 38]
 [63 45 32 77 29 43 37 77 54 43]
 [70 63 63 30 46 79 77 45 79 39]
 [76 79 79 45 77 29 45 39 77 63]
 [81 64 46 79 63 21 47 34 77 68]]<jupyter_text>Если вы правильно реализовали get_batches, результат должен выглядеть примерно так:```x [[25  8 60 11 45 27 28 73  1  2] [17  7 20 73 45  8 60 45 73 60] [27 20 80 73  7 28 73 60 73 65] [17 73 45  8 27 73 66  8 46 27] [73 17 60 12 73  8 27 28 73 45] [66 64 17 17 46  7 20 73 60 20] [73 76 20 20 60 73  8 60 80 73] [47 35 43  7 20 17 24 50 37 73]]y [[ 8 60 11 45 27 28 73  1  2  2] [ 7 20 73 45  8 60 45 73 60 45] [20 80 73  7 28 73 60 73 65  7] [73 45  8 27 73 66  8 46 27 65] [17 60 12 73  8 27 28 73 45 27] [64 17 17 46  7 20 73 60 20 80] [76 20 20 60 73  8 60 80 73 17] [35 43  7 20 17 24 50 37 73 36]] ``` хотя точные цифры могут отличаться. Убедитесь, что данные сдвинуты на один шаг для `y`!!! --- Зададим архитектуру  Структура моделиВ `__init__` предлагаемая структура выглядит следующим образом:* Создаваём и храним необходимые словари (уже релизовано)* Определяем слой LSTM, который принимает в качестве параметров: размер ввода (количество символов), размер скрытого слоя `n_hidden`, количество слоев` n_layers`, вероятность drop-out'а `drop_prob` и логическое значение batch_first (True)* Определяем слой drop-out с помощью drop_prob* Определяем полносвязанный слой с параметрами: размер ввода `n_hidden` и размер выхода - количество символов* Наконец, инициализируем весаОбратите внимание, что некоторые параметры были названы и указаны в функции `__init__`, их нужно сохранить и использовать, выполняя что-то вроде` self.drop_prob = drop_prob`. --- Входы-выходы LSTMВы можете создать [LSTM layer](https://pytorch.org/docs/stable/generated/torch.nn.LSTM.htmltorch.nn.LSTM) следующим образом```pythonself.lstm = nn.LSTM(input_size, n_hidden, n_layers,                             dropout=drop_prob, batch_first=True)```где `input_siz`e - это количество символов, которые эта ячейка ожидает видеть в качестве последовательного ввода, а `n_hidde`n - это количество элементов в скрытых слоях ячейки. Можно добавить drop-out, добавив параметр `dropout` с заданной вероятностью. Наконец, в функции `forward` мы можем складывать ячейки LSTM в слои, используя `.view`.Также требуется создать начальное скрытое состояние всех нулей:```pythonself.init_hidden()```<jupyter_code># check if GPU is available
train_on_gpu = torch.cuda.is_available()
if(train_on_gpu):
    print('Training on GPU!')
else: 
    print('No GPU available, training on CPU; consider making n_epochs very small.')
# ОТВЕТЫ
class CharRNN(nn.Module):
    
    def __init__(self, tokens, n_hidden=256, n_layers=2,
                               drop_prob=0.5, lr=0.001):
        super().__init__()
        self.drop_prob = drop_prob
        self.n_layers = n_layers
        self.n_hidden = n_hidden
        self.lr = lr
        
        # creating character dictionaries
        self.chars = tokens
        self.int2char = dict(enumerate(self.chars))
        self.char2int = {ch: ii for ii, ch in self.int2char.items()}
        
        ## TODO: define the LSTM
        self.lstm = nn.LSTM(len(self.chars), n_hidden, n_layers, 
                            dropout=drop_prob, batch_first=True)
        
        ## TODO: define a dropout layer
        self.dropout = nn.Dropout(drop_prob)
        
        ## TODO: define the final, fully-connected output layer
        self.fc = nn.Linear(n_hidden, len(self.chars))
      
    
    def forward(self, x, hidden):
        ''' Forward pass through the network. 
            These inputs are x, and the hidden/cell state `hidden`. '''
                
        ## TODO: Get the outputs and the new hidden state from the lstm
        r_output, hidden = self.lstm(x, hidden)
        
        ## TODO: pass through a dropout layer
        out = self.dropout(r_output)
        
        # Stack up LSTM outputs using view
        # you may need to use contiguous to reshape the output
        out = out.contiguous().view(-1, self.n_hidden)
        
        ## TODO: put x through the fully-connected layer
        out = self.fc(out)
        
        # return the final output and the hidden state
        return out, hidden
    
    
    def init_hidden(self, batch_size):
        ''' Initializes hidden state '''
        # Create two new tensors with sizes n_layers x batch_size x n_hidden,
        # initialized to zero, for hidden state and cell state of LSTM
        weight = next(self.parameters()).data
        
        if (train_on_gpu):
            hidden = (weight.new(self.n_layers, batch_size, self.n_hidden).zero_().cuda(),
                  weight.new(self.n_layers, batch_size, self.n_hidden).zero_().cuda())
        else:
            hidden = (weight.new(self.n_layers, batch_size, self.n_hidden).zero_(),
                      weight.new(self.n_layers, batch_size, self.n_hidden).zero_())
        
        return hidden<jupyter_output><empty_output><jupyter_text>Обучим модельВо время обучения нужно установить количество эпох, скорость обучения и другие параметры.Используем оптимизатор Adam и кросс-энтропию, считаем loss и, как обычно, выполняем back propagation.Пара подробностей об обучении:> * Во время цикла мы отделяем скрытое состояние от его истории; на этот раз установив его равным новой переменной * tuple *, потому что скрытое состояние LSTM, является кортежем скрытых состояний.* Мы используем [`clip_grad_norm_`](https://pytorch.org/docs/stable/_modules/torch/nn/utils/clip_grad.html) чтобы избавиться от взрывающихся градиентов.<jupyter_code>def train(net, data, epochs=10, batch_size=10, seq_length=50, lr=0.001, clip=5, val_frac=0.1, print_every=10):
    ''' Training a network 
    
        Arguments
        ---------
        
        net: CharRNN network
        data: text data to train the network
        epochs: Number of epochs to train
        batch_size: Number of mini-sequences per mini-batch, aka batch size
        seq_length: Number of character steps per mini-batch
        lr: learning rate
        clip: gradient clipping
        val_frac: Fraction of data to hold out for validation
        print_every: Number of steps for printing training and validation loss
    
    '''
    net.train()
    
    opt = torch.optim.Adam(net.parameters(), lr=lr)
    criterion = nn.CrossEntropyLoss()
    
    # create training and validation data
    val_idx = int(len(data)*(1-val_frac))
    data, val_data = data[:val_idx], data[val_idx:]
    
    if(train_on_gpu):
        net.cuda()
    
    counter = 0
    n_chars = len(net.chars)
    for e in range(epochs):
        # initialize hidden state
        h = net.init_hidden(batch_size)
        
        for x, y in get_batches(data, batch_size, seq_length):
            counter += 1
            
            # One-hot encode our data and make them Torch tensors
            x = one_hot_encode(x, n_chars)
            inputs, targets = torch.from_numpy(x), torch.from_numpy(y)
            
            if(train_on_gpu):
                inputs, targets = inputs.cuda(), targets.cuda()

            # Creating new variables for the hidden state, otherwise
            # we'd backprop through the entire training history
            h = tuple([each.data for each in h])

            # zero accumulated gradients
            net.zero_grad()
            
            # get the output from the model
            output, h = net(inputs, h)
            
            # calculate the loss and perform backprop
            loss = criterion(output, targets.view(batch_size*seq_length).long())
            loss.backward()
            # `clip_grad_norm` helps prevent the exploding gradient problem in RNNs / LSTMs.
            nn.utils.clip_grad_norm_(net.parameters(), clip)
            opt.step()
            
            # loss stats
            if counter % print_every == 0:
                # Get validation loss
                val_h = net.init_hidden(batch_size)
                val_losses = []
                net.eval()
                for x, y in get_batches(val_data, batch_size, seq_length):
                    # One-hot encode our data and make them Torch tensors
                    x = one_hot_encode(x, n_chars)
                    x, y = torch.from_numpy(x), torch.from_numpy(y)
                    
                    # Creating new variables for the hidden state, otherwise
                    # we'd backprop through the entire training history
                    val_h = tuple([each.data for each in val_h])
                    
                    inputs, targets = x, y
                    if(train_on_gpu):
                        inputs, targets = inputs.cuda(), targets.cuda()

                    output, val_h = net(inputs, val_h)
                    val_loss = criterion(output, targets.view(batch_size*seq_length).long())
                
                    val_losses.append(val_loss.item())
                
                net.train() # reset to train mode after iterationg through validation data
                
                print("Epoch: {}/{}...".format(e+1, epochs),
                      "Step: {}...".format(counter),
                      "Loss: {:.4f}...".format(loss.item()),
                      "Val Loss: {:.4f}".format(np.mean(val_losses)))<jupyter_output><empty_output><jupyter_text>Определим модельТеперь мы можем создать модель с заданными гиперпараметрами. Определим размеры мини-батчей.<jupyter_code># ОТВЕТЫ
# define and print the net
n_hidden=512
n_layers=2

net = CharRNN(chars, n_hidden, n_layers)
print(net)<jupyter_output>CharRNN(
  (lstm): LSTM(83, 512, num_layers=2, batch_first=True, dropout=0.5)
  (dropout): Dropout(p=0.5, inplace=False)
  (fc): Linear(in_features=512, out_features=83, bias=True)
)<jupyter_text>Установим гиперпараметры<jupyter_code># ОТВЕТЫ

batch_size = 128
seq_length = 100
n_epochs = 20 # start smaller if you are just testing initial behavior

# train the model
train(net, encoded, epochs=n_epochs, batch_size=batch_size, seq_length=seq_length, lr=0.001, print_every=10)<jupyter_output>Epoch: 1/20... Step: 10... Loss: 3.2806... Val Loss: 3.2198
Epoch: 1/20... Step: 20... Loss: 3.1579... Val Loss: 3.1370
Epoch: 1/20... Step: 30... Loss: 3.1429... Val Loss: 3.1244
Epoch: 1/20... Step: 40... Loss: 3.1163... Val Loss: 3.1199
Epoch: 1/20... Step: 50... Loss: 3.1419... Val Loss: 3.1181
Epoch: 1/20... Step: 60... Loss: 3.1205... Val Loss: 3.1166
Epoch: 1/20... Step: 70... Loss: 3.1073... Val Loss: 3.1153
Epoch: 1/20... Step: 80... Loss: 3.1257... Val Loss: 3.1124
Epoch: 1/20... Step: 90... Loss: 3.1234... Val Loss: 3.1056
Epoch: 1/20... Step: 100... Loss: 3.0975... Val Loss: 3.0904
Epoch: 1/20... Step: 110... Loss: 3.0659... Val Loss: 3.0460
Epoch: 1/20... Step: 120... Loss: 2.9597... Val Loss: 2.9701
Epoch: 1/20... Step: 130... Loss: 2.8846... Val Loss: 2.8460
Epoch: 2/20... Step: 140... Loss: 2.8687... Val Loss: 2.8193
Epoch: 2/20... Step: 150... Loss: 2.7384... Val Loss: 2.6945
Epoch: 2/20... Step: 160... Loss: 2.6366... Val Loss: 2.5944
Epoch: 2/20... Step: 170... Loss:[...]<jupyter_text>CheckpointПосле обучения сохраним модель, чтобы можно было загрузить ее позже. Здесь сохраняются параметры, необходимые для создания той же архитектуры, гиперпараметры скрытого слоя и токены.<jupyter_code># change the name, for saving multiple files
model_name = 'rnn_x_epoch.net'

checkpoint = {'n_hidden': net.n_hidden,
              'n_layers': net.n_layers,
              'state_dict': net.state_dict(),
              'tokens': net.chars}

with open(model_name, 'wb') as f:
    torch.save(checkpoint, f)<jupyter_output><empty_output><jupyter_text>--- Делаем предсказанияТеперь, когда мы обучили модель, сделаем предсказание следующих символов! Для предсказания мы передаем последний символ, и сеть предсказывает следующий символ, который мы потом передаем снова на вхол и получаем еще один предсказанный символ и так далее...Наши прогнозы основаны на категориальном распределении вероятностей по всем возможным символам. Мы можем ограничить число символов, чтобы сделать получаемый предсказанный текст более разумным, рассматривая только некоторые наиболее вероятные символы $K$. Это не позволит сети выдавать нам совершенно абсурдные прогнозы, а также позволит внести некоторый шум и случайность в выбранный текст. Узнать больше [можно здесь](https://pytorch.org/docs/stable/generated/torch.topk.htmltorch.topk).<jupyter_code>def predict(net, char, h=None, top_k=None):
        ''' Given a character, predict the next character.
            Returns the predicted character and the hidden state.
        '''
        
        # tensor inputs
        x = np.array([[net.char2int[char]]])
        x = one_hot_encode(x, len(net.chars))
        inputs = torch.from_numpy(x)
        
        if(train_on_gpu):
            inputs = inputs.cuda()
        
        # detach hidden state from history
        h = tuple([each.data for each in h])
        # get the output of the model
        out, h = net(inputs, h)

        # get the character probabilities
        p = F.softmax(out, dim=1).data
        if(train_on_gpu):
            p = p.cpu() # move to cpu
        
        # get top characters
        if top_k is None:
            top_ch = np.arange(len(net.chars))
        else:
            p, top_ch = p.topk(top_k)
            top_ch = top_ch.numpy().squeeze()
        
        # select the likely next character with some element of randomness
        p = p.numpy().squeeze()
        char = np.random.choice(top_ch, p=p/p.sum())
        
        # return the encoded value of the predicted char and the hidden state
        return net.int2char[char], h<jupyter_output><empty_output><jupyter_text>Priming и генерирование текстаНужно задать скрытое состояние, чтобы сеть не генерировала произвольные символы.<jupyter_code>def sample(net, size, prime='The', top_k=None):
        
    if(train_on_gpu):
        net.cuda()
    else:
        net.cpu()
    
    net.eval() # eval mode
    
    # First off, run through the prime characters
    chars = [ch for ch in prime]
    h = net.init_hidden(1)
    for ch in prime:
        char, h = predict(net, ch, h, top_k=top_k)

    chars.append(char)
    
    # Now pass in the previous character and get a new one
    for ii in range(size):
        char, h = predict(net, chars[-1], h, top_k=top_k)
        chars.append(char)

    return ''.join(chars)
print(sample(net, 1000, prime='Anna', top_k=5))<jupyter_output><empty_output><jupyter_text>Loading a checkpoint<jupyter_code># Here we have loaded in a model that trained over 20 epochs `rnn_20_epoch.net`
with open('rnn_x_epoch.net', 'rb') as f:
    checkpoint = torch.load(f)
    
loaded = CharRNN(checkpoint['tokens'], n_hidden=checkpoint['n_hidden'], n_layers=checkpoint['n_layers'])
loaded.load_state_dict(checkpoint['state_dict'])
# Sample using a loaded model
print(sample(loaded, 2000, top_k=5, prime="And Levin said"))<jupyter_output><empty_output>
{'fraction_non_alphanumeric': 0.10064618102366231, 'fraction_numerical': 0.04830086932267212, 'mean_word_length': 2.878132323778619, 'pattern_counts': {'":': 0, '<': 64, '<?xml version=': 0, '>': 66, 'https://': 4, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '16', 'dir': 'jupyter-structured-clean-dedup', 'id': '572692', 'license': 'MIT', 'n_tokens_mistral': 8361, 'n_tokens_neox': 8070, 'n_words': 2354, 'path': 'Courses/IadMl/IntroToDeepLearning/seminars/sem08/sem08_solution.ipynb', 'repo_name': 'searayeah/sublime-snippets', 'size': '21534'}