Краткая информация

В этой статье мы покажем вам, как создать функцию намерения в Botpress и Voiceflow с нуля, обучив модель машинного обучения в PyTorch. Показано, что если вы знаете Python, модели машинного обучения и библиотеку PyTorch, вы можете легко создать собственный конвейер чат-бота.

Репозиторий Github: https://github.com/michelle-w-br/chatbot_ml

Примечание. Для создания этого блога необходимо понимание основ Python, машинного обучения и pytorch.

Что такое намерение в Botpress и Voiceflow

Намерение – это общая функция современных инструментов создания чат-ботов, таких как Botpress и Voiceflow. Существует множество руководств о том, как использовать Botpress и Voiceflow для создания чат-ботов с намеренным диалогом управления. Однако при использовании этих инструментов вы также ограничены их алгоритмом сопоставления с образцом для намерения, и его трудно отладить, если предсказанное намерение неверно. Например, Botpress использует алгоритм хеширования для прогнозирования намерений. Ниже приведен фрагмент функции намерения (с использованием алгоритма обучения хеширования) из Botpress Исходный код GitHub V12:

  public startTraining = async (language: string): Promise<ModelEntry> => {
    const trainSet = await this._getTrainSet(language)
    const modelId = await this._nluClient.startTraining(this._botId, trainSet)
    const definitionHash = this._hashTrainSet(trainSet)
    const entry: ModelEntry = { botId: this._botId, language, modelId, definitionHash }
    await this._trainings.set(entry)
    return entry
  }

Проще говоря, функция намерения заключается в том, что с учетом пользовательского ввода она пытается предсказать, есть ли у него конкретная метка намерения, предопределенная в группе намерений. Пример намерения Botpress показан ниже. В разделе «Намерения» мы заранее определили некоторые метки намерений, например. «go_gym», «go_swim», «приветствие» и «speak_to_human», чтобы намерение можно было используется в рабочем процессе для управления рабочим процессом, как показано на рисунке 1. Для каждого намерения разработчик может определить свои собственные шаблоны предложений, например: шаблоны для «speak_to_human» показаны на рисунке 2.

Намерение моделей машинного обучения

Мы могли бы создать функцию намерения (в Botpress и Voiceflow) с нуля, используя модели машинного обучения из библиотеки Pytorch. В нашем репозитории Github мы продемонстрировали две разные модели машинного обучения для намерений: v1.0 и v2.0. Прежде чем углубиться в каждую модель в деталях. Сначала мы объясним общий конвейер классификации намерений в машинном обучении.

Конвейер классификации намерений

В машинном обучении это классическая задача классификации. Проблему можно описать следующим образом: как классифицировать новые входные данные, приведенные ниже данные обучения, например «Я хочу плавать» для определенного намерения?

Общий конвейер машинного обучения можно описать следующими шагами:

  1. загрузить текстовые данные
  2. использование модели токенизации для преобразования текстовых данных в векторное представление
  3. подготовить набор данных PyTorch
  4. загрузить набор данных с помощью PyTorch Dataloader
  5. определить модель классификации
  6. обучение модели классификации
  7. проверка/вывод по новому входу

Из этих шагов шаг 2 и шаг 3 являются наиболее важными шагами, определяющими производительность модели машинного обучения. Версия 1.0 и версия 2.0 также в основном различаются на шаге 2 и шаге 3. Проще говоря, версия 1.0 использует модель Bag of Words для шага 2 и нейронную сеть для шага 3. Версия 2.0 использует токенизатор Torchtext для шага 2 и модель встраивания для шага 3.

Далее мы в основном сосредоточимся на моделях на шаге 2 и шаге 3 для версий 1.0 и 2.0.

v1.0: Мешок слов + нейронная сеть

Мешок слов (BoW) можно использовать для преобразования любой текстовой последовательности в векторное представление. Если у вас есть обучающие текстовые данные, вы можете создать словарь на основе всех слов обучающих данных. Затем для нового предложения его можно представить как вектор 0 или 1, в зависимости от того, присутствует ли каждое слово словаря в текущем предложении. Пример показан ниже.

training data: ["want to go swim", "gym", "want to talk to staff", "hello"]
vocab:         ["want", "to", "go", "swim","gym","talk","staff", "hello"]
"go gym":      [ 0,      0,     1,     0,    1,     0,     0,     0]

После преобразования текста в вектор фактическая модель классификации представляет собой архитектуру нейронной сети, определенную в model.py. Архитектура модели довольно проста: двухуровневая модель, как показано ниже. Размер входного слоя соответствует размеру входного вектора, который в приведенном выше примере равен 8. Два скрытых слоя каждый имеют размер 8. Конечный размер выходного слоя — это количество намерений для классификации, которое в этом примере равно 4. Активация ReLu используется после каждого скрытого слоя для улучшения производительности.

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

v2.0: Torchtext + модель внедрения

В версии 2.0 мы использовали стандартный набор данных классификации намерений, названный snips, который часто используется в академическом сообществе для измерения производительности модели для задачи классификации намерений. dataset/generate_snip_json.py используется для преобразования набора данных фрагментов в тот же формат данных JSON в версии 1.0. Поскольку размер этого набора данных (обучающий/действительный/тестовый набор данных содержит 13084/700/700 предложений) намного больше, чем данные в версии 1.0, мы здесь использовали более распространенную модель токенизатора и модель классификации с большим количеством параметров.

Токензиер — это функция get_tokenizer из библиотеки torchtext. Затем он создает словарь, используя функцию build_vocab_from_iterator из библиотеки torchtext для создания словаря. build_vocab_from_iteratorфункции необходимо выполнить итерацию каждого элемента набора данных с помощью функции yield_tokens. Поэтому класс ChatDataset главным образом определяет, как выполняется итерация набора данных. Фрагмент кода токенизатора показан ниже. Пожалуйста, обратитесь к репозиторию github для всех источников кода.

from torch.utils.data import Dataset
from torchtext.data.utils import get_tokenizer
from torchtext.vocab import build_vocab_from_iterator

class ChatDataset(Dataset):
    """
    A single training/test example for simple intent classification.
    Args:
        X: pattern sentence
        Y: intent label
    """
    def __init__(self, X, Y):
        self.n_samples = len(X)
        self.x_data = X
        self.y_data = Y

    # support indexing such that dataset[i] can be used to get i-th sample
    def __getitem__(self, index):
        return self.x_data[index], self.y_data[index]

    # we can call len(dataset) to return the size
    def __len__(self):
        return self.n_samples

def chat_vocab_tokenizer(dataset):
    tokenizer = get_tokenizer("basic_english")
    def yield_tokens(data_iter):
            for text,_  in data_iter:
                yield tokenizer(text)    
    vocab = build_vocab_from_iterator(yield_tokens(dataset), specials=["<unk>"])
    vocab.set_default_index(vocab["<unk>"])

    return vocab, tokenizer

После подготовки токена и словаря каждое текстовое предложение можно сопоставить с векторным представлением, где каждый векторный элемент является индексом токена в словаре. Например, если словарный запас такой, как показано ниже, то слово «go Gym» обозначается как [2,4], поскольку «go» и «gym» — это 2-й и 4-й токены в словаре. Ниже фрагмент кода показывает, как преобразовать ввод текста в векторное представление с помощью функции Dataloader.

vocab:         ["want", "to", "go", "swim","gym","talk","staff", "hello"]
"go gym":      [2, 4]
text_pipeline = lambda x: vocab(tokenizer(x))

def collate_batch(batch):
    label_list, text_list, offsets = [], [], [0]
    for _text, _label in batch:
        label_list.append(_label)
        processed_text = torch.tensor(text_pipeline(_text), dtype=torch.int64)
        text_list.append(processed_text)
        offsets.append(processed_text.size(0))
    label_list = torch.tensor(label_list, dtype=torch.int64)
    offsets = torch.tensor(offsets[:-1]).cumsum(dim=0)
    text_list = torch.cat(text_list)
    return text_list.to(device), label_list.to(device), offsets.to(device)

train_dataset = to_map_style_dataset(train_iter)
valid_dataset = to_map_style_dataset(valid_iter)
test_dataset = to_map_style_dataset(test_iter)
train_dataloader = DataLoader(
    train_dataset, batch_size=BATCH_SIZE, shuffle=True, collate_fn=collate_batch
)
valid_dataloader = DataLoader(
    valid_dataset, batch_size=BATCH_SIZE, shuffle=True, collate_fn=collate_batch
)
test_dataloader = DataLoader(
    test_dataset, batch_size=BATCH_SIZE, shuffle=True, collate_fn=collate_batch
)

После того как вектор входных токенов готов, архитектура модели классификации представляет собой модель внедрения и еще один уровень классификации. Модель внедрения представляет собой просто матрицу, где количество строк соответствует размеру словаря, а количество столбцов называется размером внедрения. Что встраивание делает для данного индекса токена, оно возвращает соответствующий вектор-строку. Следовательно, уровень внедрения просто преобразует вектор (k x 1), где k — количество токенов, в вектор (k x m), где m — размер внедрения. Последний слой модели — это полностью связный слой с входным размером m, а выходной размер — это количество классификаций намерений.

from torch import nn

class TextClassificationModel(nn.Module):
    def __init__(self, vocab_size, embed_dim, num_class):
        super(TextClassificationModel, self).__init__()
        self.embedding = nn.EmbeddingBag(vocab_size, embed_dim, sparse=False)
        self.fc = nn.Linear(embed_dim, num_class)
        self.init_weights()

    def init_weights(self):
        initrange = 0.5
        self.embedding.weight.data.uniform_(-initrange, initrange)
        self.fc.weight.data.uniform_(-initrange, initrange)
        self.fc.bias.data.zero_()

    def forward(self, text, offsets):
        embedded = self.embedding(text, offsets)
        return self.fc(embedded)

Обобщить

В этом блоге мы рассказали, как создать функцию намерений чат-бота с нуля, используя машинное обучение и библиотеку Pytorch. Однако лучший способ учиться — это всегда практиковаться. Полный код доступен на Github. Если у вас есть какие-либо вопросы, пожалуйста, оставляйте свои комментарии здесь.