Как создать простого командного бота в Python

Итак, как часто вы узнаете погоду или время у Siri, Алисы или Google? Сейчас на рынке существует несколько видов ботов. Некоторые из них более сложные, способные поддерживать непрерывный диалог, а другие просто выполняют различные предварительно запрограммированные действия.

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

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

  • Функции  —  это взаимодействие между ботом и человеком. Оно состоит из шаблонов, тегов и ответов.
  • Шаблоны  —  это фразы или ключевые слова, которые может задавать человек для запуска и формирования процесса.
  • Теги  —  это название процесса, который необходимо выполнить.
  • Ответы  —  это может быть один или несколько ответов, которые должен выдавать получившийся бот.

Например, функция приветствия:

intents:
- tag: greeting
  patterns:
  - Hi (Привет)
  - How are you (Как вы)
  - Is anyone there? (Тут кто-нибудь есть?)
  - Hey (Эй)
  - Hola (Привет)
  - Hello (Здравствуй)
  responses:
  - Hi (Привет)
  - Holi (Привет) 
  - Hello, thanks for asking (Привет, спасибо за вопрос)
  - Good to see you again (Рад снова вас видеть)
  - Hi there, how can I help? (Привет, чем я могу помочь?)
 - tag: goodbye
  patterns:
  - Bye (Пока)
  - See you later (Увидимся)
  - Goodbye (До свидания)
  - Nice chatting to you, bye (Было приятно пообщаться, пока)
  - Till next time (До встречи)
  responses:
  - See you! (Увидимся!)
  - Have a nice day (Хорошего дня)
  - Bye! Come back again soon. (Пока! Приходите еще)

Итак, нам потребуется:

  1. yml-файл со всеми функциями, которые должен выполнять готовый бот. Если вы не знаете, что такое yml-файл, то пройдите по этой ссылке.
  2. Python 3 либо версии выше.
  3. Библиотеки TensorFlow, pickle и nltk.

Разделим все на 3 главные части или класса: считыватель функций бота, модель бота и загрузчик бота. Вот какие данные необходимо будет импортировать:

import os
import re
import yaml
import nltk
import random
import json
import numpy as np
import tensorflow as tf
import pickle
from tensorflow.keras.models import load_model
from tensorflow.keras.layers import (
    Dense,
    Activation,
    Dropout
)
from nltk.stem import WordNetLemmatizer
from tensorflow.keras.optimizers import SGD

Считыватель функций бота

Этот класс предназначен для чтения yml-файла с функциями. Для данной модели бота понадобится 4 выхода: лемматизатор, слова, классы и функции. Бот, которого мы создаем,  —  простой классификатор. В нем каждый тег является классом, а токенизированные и лемматизированные слова  —  вводимыми данными.

Наконец, необходимо узнать, какую из всех команд или действий должен выполнить бот. Итак, классы  —  это теги. Затем нужны токенизированные и приведенные к начальной форме слова. Для этого понадобится лемматизатор и токенизатор слов  —  они есть в библиотеке nltk. Сначала создаем имя класса BotIntentsReader.

class BotIntentsReader():
    def __init__(self, path, tokenize_func=nltk.word_tokenize, lemmatizer=WordNetLemmatizer().lemmatize, logging=False):
        nltk.download('punkt')
        nltk.download('wordnet')
        stream = open(path, 'rb')
        self.lemmatizer = lemmatizer
        docs = yaml.safe_load(stream)
        self.words=[]
        self.classes = []
        self.documents = []
        ignore_words = ['?', '!']
        self.intents = docs['intents']

        for intent in self.intents:
            for pattern in intent['patterns']:
                token_pattern = tokenize_func(pattern)
                self.words.extend(token_pattern)
                self.documents.append((token_pattern, intent['tag']))

                if intent['tag'] not in self.classes:
                    self.classes.append(intent['tag'])

        self.words = [lemmatizer(w.lower()) for w in self.words if w not in ignore_words]
        self.words = sorted(list(set(self.words)))
        self.classes = sorted(list(set(self.classes)))

        if logging:
            print (len(self.documents), "documents")
            print (len(self.classes), "classes", self.classes)
            print (len(self.words), "unique lemmatized words", self.words)

Мы добавили дополнительный вывод под названием documents, который представляет собой только объединение токенизированных слов (до лемматизации) и тегов. Эта переменная будет повторяться для обучения модели.

Модель бота

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

Вторая и самая интересная часть  —  это построение модели. На входе модели бота будет список слов. В случае приветствия функция будет равна 23 (всего разных слов). Затем  —  вывод возможных тегов (в данном случае 2), которые были заданы. Для этой модели мы будем использовать простую нейронную сеть, но вы можете попробовать более продвинутую или сеть с другой архитектурой по вашему выбору. Архитектура представляет собой два плотных слоя 32 и 16 с функцией активации RELU и выдачей 0,5.

class BotModelCreator():
    def __init__(self, bot_data, verbose=0):
        self.verbose = verbose
        self.classes = bot_data.classes
        self.words = bot_data.words
        self.documents = bot_data.documents
        self.lemmatizer = bot_data.lemmatizer
        self.intents = bot_data.intents
        self.order_data()
        self.creat_model()

    def order_data(self):
        training = []
        output_empty = [0] * len(self.classes)
        for doc in self.documents:
            bag = []
            pattern_words = [self.lemmatizer(word.lower()) for word in doc[0]]

            for w in self.words:
                bag.append(1) if w in pattern_words else bag.append(0)

            output_row = list(output_empty)
            output_row[self.classes.index(doc[1])] = 1

            training.append([bag, output_row])
        random.shuffle(training)
        training = np.array(training, dtype="object")

        self.train_x = list(training[:,0])
        self.train_y = list(training[:,1])
        if self.verbose != 0:
            print("Training data created", training.shape)

    def creat_model(self):
        self.model = Sequential()
        self.model.add(Dense(32, input_shape=(len(self.train_x[0]),), activation='relu'))
        self.model.add(Dropout(0.5))
        self.model.add(Dense(16, activation='relu'))
        self.model.add(Dropout(0.5))
        self.model.add(Dense(len(self.train_y[0]), activation='softmax'))

        sgd = SGD(lr=0.001, decay=1e-6, momentum=0.9, nesterov=True)
        self.model.compile(
            loss='categorical_crossentropy',
            optimizer=sgd,
            metrics=['accuracy']
        )
        if self.verbose != 0:
            print("model created")
    
    def train_model(self):
        hist = self.model.fit(
            np.array(self.train_x), np.array(self.train_y),
            epochs=500,
            batch_size=5,
            verbose=self.verbose
        )
        
        return hist
    
    def save_models(self, out_path=""):
        pickle.dump(self.words, open(
            os.path.join(out_path, 'words.pkl'),
            'wb'
            )
        )
        pickle.dump(self.classes, open(
            os.path.join(out_path, 'classes.pkl'),
            'wb'
            )
        )
        pickle.dump(self.lemmatizer, open(
            os.path.join(out_path, 'lemmatizer.pkl'),
            'wb'
            )
        )
        self.model.save(
            os.path.join(out_path, 'chatbot_model.h5'), 
            self.model
        )
        json.dump(self.intents, open(os.path.join(out_path, 'intents.json'), 'w'))

Теперь, когда у нас есть все  —  от последовательности ввода до алгоритмов обучения, остается только натренировать бота. Сначала нужно загрузить все модели и создать данные для бота, а затем и саму модель. Можно оставить сообщение по умолчанию равным 0 (если вы хотите регистрировать в логе информацию об обучении, измените его на 1 или 2).

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

bot_data = BotIntentsReader("intents.yml")
bot_model = BotModelCreator(
    bot_data=bot_data,
)
bot_model.train_model()
bot_model.save_models()

Наконец у нас есть файлы, модель обучена, но что теперь? Как взаимодействовать с получившимся ботом? Для этого нужно создать новый класс, который может загружаться из файла или передавать объект bot_model для прогнозирования тега. Также нужен метод, который использует для ввода текст и возвращает ответ от бота. Можно добавить и защиту от незнакомых команд: если пользователь отправит предложение, в котором нет нужного слова из созданного выше списка слов, бот ответит: «Я вас не понимаю, пожалуйста, повторите».

class BotLoader():
    def __init__(self, bot_model, threshold=0.25):
        self.threshold = threshold
        if type(bot_model)==dict:
            self.lemmatizer = pickle.load(open(bot_model["lemmatizer"], 'rb'))
            self.model = load_model(bot_model['model'])
            self.intents = json.loads(open(bot_model['intents']).read())
            self.words = pickle.load(open(bot_model['words'],'rb'))
            self.classes = pickle.load(open(bot_model['classes'],'rb'))
        else:
            self.model = bot_model.model
            self.intents = bot_model.intents
            self.words = bot_model.words
            self.classes = bot_model.classes
            self.lemmatizer = bot_model.lemmatizer

    def clean_up_sentence(self, sentence):
        sentence_words = nltk.word_tokenize(sentence)
        sentence_words = [self.lemmatizer(word.lower()) for word in sentence_words]
        return sentence_words

    def bow(self, sentence):
        sentence_words = self.clean_up_sentence(sentence)
        bag = [0]*len(self.words)
        for s in sentence_words:
            for i, w in enumerate(self.words):
                if w == s:
                    bag[i] = 1
        return (np.array(bag))

    def predict_class(self, sentence):
        tokens = self.bow(sentence)
        if tokens.sum() > 0:
            predictions = self.model.predict(np.array([tokens]))[0]
            results = [[i, r] for i, r in enumerate(predictions) if r > self.threshold]
            results.sort(key=lambda x: x[1], reverse=True)
            return_list = []
            for result in results:
                return_list.append(
                    {"intent": self.classes[result[0]],
                     "probability": str(result[1])}
                )
        else:
            return_list = None
        return return_list

    def getResponse(self, ints):
        tag = ints[0]['intent']
        for i in self.intents:
            if(i['tag']== tag):
                result = random.choice(i['responses'])
                break
        return result

    def chatbot_response(self, msg):
        ints = self.predict_class(msg)
        if ints is not None:
            res = self.getResponse(ints)
        else:
            res = "I don't understand you, can you repeat"
        return res

Для тестирования нужно только создать объект загрузчика бота и сообщить ему некоторый текст. Теперь единственное, что остается сделать,  —  это добавить больше функций с тегами, шаблонами и ответами, чтобы сделать бота «умнее».

Читайте также:

Читайте нас в Telegram, VK и Яндекс.Дзен


Перевод статьи Sebastian Correa: Make a Simple Comand BOT Python

Предыдущая статьяМатематические операции над массивами и матрицами
Следующая статьяКак оптимизировать набор текста с помощью Python