Оглавление

  1. Обзор
  2. Трубопровод
  3. Токенизация
  4. Обивка
  5. Определить RNN
  6. Постобработка
  7. Вывод на каждом этапе
  8. Заключение

Обзор

В этой статье мы определим простую RNN для расшифровки шифра Цезаря. Если вы не знакомы с RNN, взгляните на Цифровое пошаговое руководство + код RNN.

Цезарь Шифр ​​

Шифр Цезаря - это шифр, который кодирует предложения, заменяя буквы другими буквами, сдвинутыми на фиксированный размер. Например, шифр Цезаря со значением сдвига влево 3 приведет к следующему:

Input:    ABCDEFGHIJKLMNOPQRSTUVWXYZ
Cipher:   DEFGHIJKLMNOPQRSTUVWXYZABC

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

Зачем использовать RNN?

Шифр Цезаря может быть решен как проблема мультиклассовой классификации с использованием полносвязной нейронной сети с прямой связью, поскольку каждая буква X сопоставляется со своим значением шифра Y. временные зависимости между буквами, образующими слова в предложениях. Следовательно, даже несмотря на то, что шифр Цезаря является игрушечным примером, RNN все же может изучить этот шифр быстрее и с большей точностью, чем базовая полносвязная нейронная сеть.

Данные

Простой текст:

  • Данные созревают, как вино, приложения - как рыба.
  • Если у нас есть данные, давайте посмотрим на них. Если все, что у нас есть, - это мнение, давайте перейдем к моему.

Шифрованный текст:

  • «Gdwd pdwxuhv olnh zlqh, dssolfdwlrqv olnh ilvk.»
  • «Li zh kdyh gdwd, ohw’v orrn dw gdwd. Li doo zh kdyh duh rslqlrqv, ohw’v jr zlwk plqh ».

RNN будет принимать на входе предварительно обработанный зашифрованный текст, символ за символом, и выводить предсказания символ за символом. Соответственно, наша определенная RNN будет иметь символьную архитектуру "многие ко многим" (сопоставление).

Трубопровод

Предварительная обработка

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

Постобработка

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

Конвейер

Ввод → Токенизация → Заполнение → МОДЕЛЬ → Числовое преобразование в текст

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

Мучите данные, и они признаются во всем.

Токенизация

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

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

Преобразовать в токены символов

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

def generate_character_tokens(x):
    return [c for c in x.lower()]

Вывод

[‘t’, ‘o’, ‘r’, ‘t’, ‘u’, ‘r’, ‘e’, ‘ ’, ‘t’, ‘h’, ‘e’, ‘ ’, ‘d’, ‘a’, ‘t’, ‘a’, ‘,’, ‘ ’, ‘a’, ’n’, ‘d’, ‘ ’, ‘i’, ‘t’, ‘ ’, ‘w’, ‘i’, ‘l’, ‘l’, ‘ ’, ‘c’, ‘o’, ’n’, ‘f’, ‘e’, ‘s’, ‘s’, ‘ ’, ‘t’, ‘o’, ‘ ’, ‘a’, ‘n’, ‘y’, ‘t’, ‘h’, ‘i’, ’n’, ‘g’, ‘.’]

Создать идентификаторы персонажей

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

char_to_int = {' ': 1, 'k': 2, 'f': 3, 'g': 4, 's': 5, 'h': 6, 'r': 7, 'y': 8, 'a': 9, 'c': 10, 'n': 11, 't': 12, 'u': 13, 'p': 14, ',': 14, '.': 15, 't': 16, 'q': 17, 'e': 18, 'w': 19, 'o': 20, 'x': 21, 'm': 22, 'v': 23, 'l': 24, 'z': 25, 'd': 26, 'i': 27, 'b': 28, 'j': 29}
def get_character_ids(tokens):
    return [char_to_int[t] for t in tokens]

Собираем все вместе

def our_tokenizer(x):
    tokens = generate_character_tokens(x)
    character_ids = get_character_ids(tokens)
    return character_ids

Использование Keras

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

from keras.preprocessing.text import Tokenizer
def keras_tokenizer(x):
    tokenizer = Tokenizer(character_level=True, lower=True)
    tokenizer.fit_on_texts(x)
    return tokenizer.texts_to_sequences(x)

Вывод

[16, 20, 7, 16, 13, 7, 18, 1, 16, 6, 18, 1, 26, 9, 16, 9, 14, 1, 9, 11, 26, 1, 27, 16, 1, 19, 27, 24, 24, 1, 10, 20, 11, 3, 18, 5, 5, 1, 16, 20, 1, 9, 11, 8, 16, 6, 27, 11, 4, 15]

Заполнение

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

Сначала нам нужно найти длину самой длинной последовательности, а затем мы добавим или добавим 0 к концам более коротких последовательностей.

def my_padding(x, max_length):
    pad_length = max(0, max_length - len(x))
    return x + [0]*pad_length

Использование Keras

В Keras также есть функция pad_sequences, которую мы можем использовать.

from keras.preprocessing.sequence import pad_sequences
def keras_padding(x, max_length):
    return pad_sequences(x, maxlen=max_length, padding='post')

Вывод

[16, 20, 7, 16, 13, 7, 18, 1, 16, 6, 18, 1, 26, 9, 16, 9, 14, 1, 9, 11, 26, 1, 27, 16, 1, 19, 27, 24, 24, 1, 10, 20, 11, 3, 18, 5, 5, 1, 16, 20, 1, 9, 11, 8, 16, 6, 27, 11, 4, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]

Модель

Теперь входные последовательности можно передать модели. Здесь мы определили простую RNN с одним повторяющимся слоем и одним плотным слоем для вывода значений на каждом временном шаге.

from keras.layers import SimpleRNN, Input, Dense, TimeDistributed
from keras.models import Model
from keras.layers import Activation
from keras.optimizers import Adam
from keras.losses import sparse_categorical_crossentropy
def simple_rnn(input_shape, learning_rate=1e-3):
    input_seq = Input(input_shape)
    rnn = SimpleRNN(64, activation='relu', return_sequences=True)(input_seq)
    logits = TimeDistributed(Dense(plaintext_vocab_size))(rnn)
    out = Activation('softmax')(logits)
    model = Model(inputs=input_seq, outputs=out)
    model.compile(loss=sparse_categorical_crossentropy,
                  optimizer=Adam(learning_rate),
                  metrics=['accuracy'])
    
    return model
  • input_shape: это форма дополненного вектора чисел, не включая размер пакета.
  • return_sequences: указывает на возврат всей выходной последовательности, а не только последнего вывода в выходной последовательности.
  • TimeDistributed (): применяет один и тот же плотный слой с одинаковыми весами к каждому временному шагу.
  • Активация (softmax): сжимает логиты до значений от 0 до 1, которые также суммируются до 1. По сути, эта функция активации преобразует логиты в категориальное распределение.

RNN достигает ›99% точности после 4 эпох, обучена на 8000 образцах и проверена на 2000 образцах.

Постобработка

RNN выводит вектор чисел, которые необходимо преобразовать обратно в символы.

[ 2.76532210e-03, 9.61597681e-01, 2.77274812e-04, 2.80881330e-04, 3.43341962e-04, ………, 4.12466412e-04, 4.33523121e-04]

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

Напомним, что исходное отображение:

char_to_int = {' ': 1, 'k': 2, 'f': 3, 'g': 4, 's': 5, 'h': 6, 'r': 7, 'y': 8, 'a': 9, 'c': 10, 'n': 11, 't': 12, 'u': 13, 'p': 14, ',': 14, '.': 15, 't': 16, 'q': 17, 'e': 18, 'w': 19, 'o': 20, 'x': 21, 'm': 22, 'v': 23, 'l': 24, 'z': 25, 'd': 26, 'i': 27, 'b': 28, 'j': 29}

Тогда обратное:

int_to_char = {1: ' ', 2: 'k', 3: 'f', 4: 'g', 5: 's', 6: 'h', 7: 'r', 8: 'y', 9: 'a', 10: 'c', 11: 'n', 16: 't', 13: 'u', 14: ',', 15: '.', 17: 'q', 18: 'e', 19: 'w', 20: 'o', 21: 'x', 22: 'm', 23: 'v', 24: 'l', 25: 'z', 26: 'd', 27: 'i', 28: 'b', 29: 'j'}
int_to_char[0] = '<padding'> 
  • Напомним, что после этапа токенизации мы добавили 0 для заполнения наших векторов. Это добавленное значение необходимо учесть в сопоставлении, иначе мы получим KeyError.

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

import numpy as np
def prediction_to_text(y): 
    # grab predictions 
    indices = np.argmax(y) 
    # convert ints to characters
    characters = []   
    for idx in indices: 
        c = int_to_char[idx]
        characters.append(c)
        
    return ' '.join(characters) # return sequence joined by space

Использование Keras

В качестве альтернативы, токенизатор Keras может позаботиться об этой части.

def prediction_to_text(y, tokenizer): # for entire sequence
    # create mapping from tokenizer
    int_to_char = {i: c for c, i in tokenizer.word_index.items()}
    int_to_char[0] = '<padding>'
    # grab predictions
    indices = np.argmax(y) 
  
    # convert ints to characters
    characters = []   
    for idx in indices: 
        c = int_to_char[idx]
        characters.append(c)
    
    return ' '.join(characters) # return sequence joined by space

Вывод на каждом этапе

Исходный текст:

  • «gdwd pdwxuhv olnh zlqh, dssolfdwlrqv olnh ilvk.»

После предварительной обработки:

  • жетоны: [g, d, w, d,,, p, d, w, x, u, h, v,,, o, l, n, h, z, ,,,, l, q, h, d, s, s, o, l, f, d, w, l, r, q, v,,, o, l, n, h, i, l, v, k,,. ,]
  • идентификаторы: [4, 26, 19, 26, 1, 14, 26, 19, 21, 13, 6, 23, 1, 20, 24, 11, 6, 1, 25, 24, 17, 6, 14, 1, 26, 5, 5, 20, 24, 3, 26, 19, 24, 7, 17, 23, 1, 20, 24, 11, 6, 1, 27, 24, 23, 2, 15]

Прогнозы модели для каждого персонажа:

  • [26, 9, 16, 9, 1, 22, 9, 16, 13, 7, 18, 5, 1, 24, 27, 2, 18, 1, 19, 27, 11, 18, 14, 1, 9, 14, 14, 24, 27, 10, 9, 16, 27, 20, 11, 5, 1, 24, 27, 2, 18, 1, 3, 27, 5, 6, 15]

После постобработки:

  • «данные созревают, как вино, приложения - как рыба».

Заключение

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

Надеюсь, эта статья была информативной. Пожалуйста, дайте мне знать, если есть какие-либо ошибки или какие-либо улучшения, которые можно сделать.