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

В двух последующих статьях мы:

  • Улучшение данных, обучения и прогнозирования, чтобы бот PyTorch мог искренне вести беседы; а также
  • Разверните бота в продакшн. RNN обязательно отслеживает состояние, что вызывает интересные проблемы с развертыванием.

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

Шаг 1. Получите данные

Нет смысла часами тренировать допинг-модель, не зная, что основы верны. Учитывая, что сейчас мы все еще находимся на стадии PoC, давайте выберем несколько простых обучающих данных:

sentences = ['How may I help you?',
             'Can I be of assistance?',
             'May I help you with something?',
             'May I assist you?']

Шаг 2: токенизируйте эти данные

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

Мы можем создать словарь, который преобразует слова в целые числа, и в то же время другой dict, который преобразует обратно (целые числа в слова):

words = dict()
reverse = dict()
i = 0
for s in sentences:
    s = s.replace('?',' <unk>')
    for w in s.split():
        if w.lower() not in words:
            words[w.lower()] = i
            reverse[i] = w.lower()
            i = i + 1

Существуют более изощренные способы сделать это преобразование: обычно слова, которые встречаются в корпусе с частотой ‹N, могут быть заменены универсальным токеном ‹unk›, мы могли бы аналогичным образом заменить собственные существительные, мы, вероятно, должны отметить начало и конец предложения, мы могли бы ограничить слова так, чтобы writing и loaded оба были заменены на write. Существует множество техник НЛП, которые компенсируют то, насколько плох был традиционно ИИ.

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

На основе четырех простых обучающих предложений, приведенных выше, составленный словарь выглядит следующим образом:

{'<unk>': 5,
 'assist': 12,
 'assistance': 9,
 'be': 7,
 'can': 6,
 'help': 3,
 'how': 0,
 'i': 2,
 'may': 1,
 'of': 8,
 'something': 11,
 'with': 10,
 'you': 4}

Шаг 3. Создайте нейронную сеть

Идея этого диалогового бота заключается в том, что по входному слову он должен уметь предсказывать следующее слово и что он должен иметь некоторую память о разговоре, чтобы предсказывать это следующее слово. В приведенном выше примере вы можете видеть, что я активировал сеть с помощью начального токена ‹unk›. Отсюда следующим словом в сети было «может». Затем из [‹unk›, май] он предсказал «i». И так далее.

Этап дизайна, кхм, в высшей степени повторяющийся (он же палец в воздухе). Конечно, для языка нам нужна какая-то повторяющаяся сеть, потому что она должна отслеживать предыдущие слова, а также общий контекст разговора, чтобы сформировать предложение. (Одно из неустановленных требований к дизайну здесь состоит в том, что мы хотим, чтобы сеть сама находила лучший способ отслеживать контекст; мы не хотим, чтобы это приходилось делать самим).

Идея повторяющейся сети проста:

Входные данные сети состоят из слова плюс предыдущее предсказанное слово (которое может быть или не быть точным, а также непрерывно изменяется по мере агрегирования прошлых входных данных, поэтому его можно рассматривать как своего рода скрытое состояние, а не фактическое слово. ). На основании этих двух вещей он выводит свой следующий прогноз.

Проблема здесь в том, что существует петля обратной связи, а положительная обратная связь по своей сути нестабильна (подумайте об усиленном визге, когда микрофон находится слишком близко к динамику). Ошибки увеличиваются со временем и циклами обучения, что приводит к хорошо известной проблеме взрывного градиента. Это может произойти, если нейроны обладают, например, нелинейностью relu. В качестве альтернативы, если выходы нейронов ограничены между -1 и 1, как с функцией вывода сигмоид или tanh, хорошо, когда количества меньше нуля умножаются многократно, они быстро асимптота к нулю, особенно в ограниченном двоичном представлении компьютеров. Это проблема исчезающего градиента.

Хорошая новость заключается в том, что эти проблемы в значительной степени были решены нейронами LSTM, а в последнее время - другим способом - ГРУ. Детали реализации выходят за рамки этой статьи; кроме того, они являются основными строительными блоками PyTorch.

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

Шаг 4: входы и выходы

Это основная часть сети, но все же мы должны решить, как будут выглядеть вход и выход. Во-первых, вход.

Давным-давно известно, что сети работают лучше с разреженными категориальными входами, чем с плотными. Например, со словарным запасом из 13 слов, перечисленных выше, легче обучить сеть с 13 входами, все из которых являются нулями, кроме одного, чем с 1 входом, который изменяется от 0 до 1 с шагом 1/13. Это горячая кодировка.

Никто больше не использует горячее кодирование. С 13 входами, зачем тратить 12 из них на нули и один на единицу, когда с помощью простого поиска мы могли бы выбрать вектор, сформированный из любых 13 скаляров? Еще лучше рассматривать эти 13 скаляров как обучаемые параметры. Это то, что делает встраивание, и слой встраивания обрабатывается как любой другой, получая обновления параметров через обратное распространение.

Итак, это встраивание, переход в повторяющийся слой, и на выходе нам понадобится слой с количеством выходов, равным длине нашего словаря (итого, 13). Обозначая это как своего рода проблему «классификации», если бы нейрон 6 имел самую высокую активацию из 13 выходных нейронов, мы бы сказали, что слово 6 было выходом сети, и так далее. Это argmax.

Но у этого подхода есть несколько проблем, в том числе, прежде всего, то, что он недифференцируем, а также то, что результаты могут начать расти произвольно. Таким образом, появился softmax, который представляет собой простую дифференцируемую формулу для сжатия каждого выхода в диапазоне от 0 до 1 (а сумма всех выходов равна 1, поэтому ее можно интерпретировать как вероятность):

Для подобных задач классификации, когда есть единственный правильный ответ, и нас не волнуют менее вероятные классы, ведение журнала softmax помогает сети быстрее обучаться. Если сеть уверена в выходе, то есть softmax стремится к 1, log softmax будет стремиться к 0, что приведет к меньшему градиенту и меньшим обновлениям веса. Точно так же, когда псевдовероятности меньше, log_softmax PyTorch обеспечит более крупные градиенты.

То же самое и с функцией потерь: мы хотим, чтобы большее количество неверных вероятностей приводило к более высоким ошибкам, чтобы градиент изменялся быстрее, и меньшее количество ошибочных вероятностей, чтобы давать меньшие ошибки. Это хороший эффект от использования Cross Entropy Loss, который также является стандартной функцией ошибок для такой проблемы. Эта функция, в отличие от чего-то вроде среднеквадратичной ошибки, никогда не обращается в ноль; сеть никогда не должна застревать (а также никогда не достигать «совершенства», что, вероятно, в любом случае будет означать переоснащение).

Шаг 5. Загрузите данные в сеть

Это просто стандартный итератор, созданный из токенизированных обучающих данных на шаге 2. Единственное, что интересно здесь, это то, что X, обучающие образцы, которые передаются в сеть, имеют requires_grad = Ложь. Обычно обучающие входные данные требуют, чтобы мы инициализировали их, чтобы они имели градиент, чтобы все, что после них в сети, также получило градиент и могло обучаться.

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

Кроме того, поскольку эта статья должна остановиться на рабочем PoC - для простоты я опустил пакетирование и CUDA на этом этапе. Я также не позаботился о включении разделений тест / проверка.

Шаг 6: Постройте сеть согласно проекту

Построить нейронную сеть в PyTorch очень просто:

Чтобы уточнить некоторые цифры:

  • Есть len (слова) вложений (= 13), каждый вектор длиной 10. Почему 10? Предварительно обученные векторы GloVe, входящие в состав SpaCy, имеют длину 300. Чем больше, тем лучше, но не для обучения с таким крошечным словарным запасом, как у нас сейчас.
  • Следующий уровень, LSTM, принимает эти 10 входных данных и передает их в два уровня по 20 нейронов в каждом. Для предотвращения переобучения добавлена ​​небольшая регуляризация.
  • Мы должны инициализировать скрытое состояние слоев LSTM. Я просто установил нули, размер 2 слоя, размер пакета один, для 20 нейронов в каждом слое (соответствие выше). Внимательные читатели заметят, что это кортеж. Это потому, что слои LSTM имеют скрытое состояние h и (также скрытое) состояние ячейки c. Их можно инициализировать идентично.
  • Выходной слой принимает 20 выходных данных последнего слоя LSTM и соединяет их со своими выходами len (words): 13 слов, 13 категорий, 13 выходных нейронов.
  • Нам нужно немного поиграть с выходными данными, просто чтобы привести вещи в форму, которую хочет PyTorch. -1 - часто беспроигрышный вариант, поскольку он означает просто "что угодно" при условии, что все остальное соответствует тому, что вам нужно.

Шаг 7: Сделайте цикл обучения и получите результаты!

Эта модель пришла в убыток через 300 эпох 0,27. (У вас все будет по-другому, но непременно запустите эту ячейку много раз, что со временем снизит скорость обучения). Это кажется неплохим, но его трудно оценить, если не попробовать:

def get_next(word_):
    word = word_.lower()
    out = m(Variable(torch.LongTensor([words[word_]])))
    return reverse[int(out.max(dim=1)[1].data)]
def get_next_n(word_, n=3):
    print(word_)
    for i in range(0, n):
        word_ = get_next(word_)
        print(word_)
get_next_n('<unk>', n=12)

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

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