Машинное обучение своими руками (часть 2). Сервис для классификации обращений в тех. поддержку

    В октябре команда облачного сервиса Okdesk приняла участие в пензенском хакатоне, в рамках которого мы разработали "коробочного" Telegram-бота для Okdesk. Бот позволит клиентам сервисных компаний отправлять заявки на обслуживание, переписываться по заявками и ставить оценки выполнению заявок не выходя из любимого мессенджера.


    image


    Мы планировали написать об этом статью на Хабру, но вовремя остановились. Воистину, кому сегодня интересно читать о том, что на очередном хакатоне был разработан очередной Telegram-бот? Поэтому мы написали продолжение статьи о машинном обучении для классификации заявок в тех. поддержку. В этой статьей рассказываем о том, как после обучения алгоритма сделать работающий сервис, на вход которому передается текст клиентской заявки, а на выходе — категория, к которой относится заявка.


    Краткое содержание предыдущей части


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


    Итак, сервисные компании оказывают услуги своим клиентам. Клиенты отправляют заявки в службу поддержки клиентов: например, "не работает интернет" или "не проходит проводка в 1С". В сервисной компании разными направлениями занимаются разные люди: проблемы с интернетом лежат в ответственности группы системных администраторов, а проблемы по 1С "падают" на группу сопровождения 1С. Распределение заявок по группам можно поручить диспетчеру, но это дополнительные расходы (зарплата) и потеря времени решения (ко времени решения заявок добавляется реакция диспетчера на распределение заявок). Логично переложить задачу распределения заявок на "умный алгоритм", который по тексту заявки сможет определить, к какому направлению она относится.


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


    Лучший алгоритм показывал на обучающей выборке 74,5% точность классификации (что весьма неплохо для 14 категорий), но благодарные читатели писали в личку, что на их данных примененный алгоритм показывал точность в 92% (а это уже вполне "продакшн" вариант).


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


    Выгрузка словаря и обученного алгоритма


    Напомним, что классификация новых заявок проводится в 2 этапа:


    1. Для новой заявки определяются координаты заявки в пространстве словаря;
    2. Полученный вектор передается в обученный алгоритм, который возвращает категорию заявки.

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


    Далее по тексту будем использовать переменные из первой части статьи.


    Выгрузка словаря


    С выгрузкой словаря все просто. В первой части мы записали все слова словаря (порядок важен!) в list-переменную words. Теперь необходимо записать слова из переменной words в текстовый файл. Каждое слово будем записывать с новой строки.


    # Импортируем библиотеку codecs, так как слова записаны в кодировке utf8
    import codecs
    # Записываем слова из словаря в файл words.txt, каждое слово с новой строки
    with codecs.open('words.txt', 'w', encoding = 'utf8') as wordfile: wordfile.writelines(i + '\n' for i in words)

    Выгрузка дампа обученного алгоритма


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


    Python предлагает 2 варианта для сохранения алгоритма.


    Встроенный модуль pickle:


    import pickle
    saved = pickle.dumps(classifier)
    classifier2 = pickle.loads(saved)

    Он позволяет сохранить дамп в переменную, а переменную можно сохранить в файл.


    Библиотека joblib:


    from sklearn.externals import joblib
    joblib.dump(classifier, 'filename.pkl') 
    classifier2 = joblib.load('classifier.pkl')

    Она не позволяет записывать дамп модели в переменную, но сразу записывает дамп в .pkl файл.


    Для нашей задачи необходимо сохранить дамп алгоритма в файл:


    from sklearn.externals import joblib
    joblib.dump(optimazer_tree.best_estimator_, 'model_tree.pkl')

    Теперь у нас есть второй файл: дамп обученного алгоритма в файле model_tree.pkl


    Скрипт классификации новых заявок


    Скрипт, который будет классифицировать новые заявки, должен уметь следующее:


    1. Загружать словарь из файла words.txt;
    2. Загружать обученный алгоритм из файла classifier.pkl;
    3. Проецировать в пространство словаря переданный для классификации текст;
    4. Возвращать предсказание категории переданного для классификации текста.

    Приступим. Для начала импортируем необходимые библиотеки. С большинством из необходимых для работы скрипта библиотек мы познакомились либо в первой части статьи, либо (с codecs) в этой статье чуть выше. Дополнительно возникает библиотека sys — она нужна нам для работы с параметрами командной строки (из которой мы будем передавать в скрипт текст)


    import numpy as np
    import re
    import sklearn
    import codecs
    Import sys

    Теперь загрузим словарь и обученный алгоритм:


    #Загружаем словарь из файла words.txt
    with codecs.open('words.txt','r', encoding = 'utf8') as wordsfile: wds = wordsfile.readlines()
    
    #Удаляем в конце импортируемых слов символ переноса строки
    words = []
    for i in wds:
        words.append(i[:-1])
    
    #Загружаем обученные классификатор из файла model_tree.pkl
    estimator = sklearn.externals.joblib.load('model_kNN.pkl')

    Перед тем, как проецировать новый текст на пространство словаря, необходимо разбить текст на слова (предварительно приведя текст к нижнему регистру). Объявим соответствующие функции:


    #Объявляем функцию приведения строки к нижнему регистру
    def lower(str):
        return str.lower()
    
    #Определяем функцию, которая разбивает строку по ряду символов и возвращает массив слов
    def splitstring(str):
        words = []
        #разбиваем строку по символам из [], символы подбирали опытным путем в первой части
        for i in re.split('[;,.,\n,\s,:,-,+,-,(,),=,-,/,«,»,-,@,-,-,\d,!,?,"]',str):
            words.append(i)
        return words

    И в завершении объявим функцию, которая принимает на вход новый текст, а на выходе выдает предсказание его категории:


    #Объявляем функцию, в которую можно передать текст, а на выходе получить категорию
    def class_func(new_issue):
        #Разбиваем текст на слова, предварительно приведя текст к нижнему регистру
        new_issue_words = splitstring(lower(new_issue))
        #Создаем нулевой вектор размером len(words)
        new_issue_vec = np.zeros((1,len(words)))
        #проставляем [j]-ю координату вектора число, равное количеству вхождений j-го слова из словаря в текст new_issue
        for j in new_issue_words:
            if j in words:
                new_issue_vec[0][words.index(j)]+=1
        #Предсказываем категорию
        return estimator.predict(new_issue_vec)

    Напомним, что мы планируем передавать текст новой заявки из командной строки. Для того, чтобы получить в скрипте аргументы командной строки, воспользуемся библиотекой sys. Sys.argv возвращает список аргументов командной строки, при этом нулевой элемент — название скрипта (но нам это не важно, так как названия скрипта нет в словаре; оно само исчезнет при проецировании). Таким образом, для передачи текста новой заявки в скрипт, нам необходимо "склеить" в скрипте переданные параметры командной строки (так как каждое слово из текста новой заявки будет передаваться как один аргумент):


    new_issue = u''
    for i in sys.argv:
        #склеиваем с добавлением пробелов
        new_issue += ' ' + i.decode('cp1251')
    class_func(new_issue)

    Важно! В зависимости от используемой в консоли кодировки, в строке new_issue += ' ' + i.decode('cp1251') параметр у decode может быть другим.


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


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


    Конец! Желаем удачи в решении ваших ML-задач. Ну а мы продолжим разрабатывать Okdesk — самую удобную (по мнению нашей компании :)) helpdesk систему для обслуживания клиентов в сервисных компаниях, параллельно исследуя возможности применения “умных алгоритмов” для решения задач, связанных с обслуживанием.

    • +12
    • 4,8k
    • 5
    Okdesk 59,53
    Облачная Help Desk система для сервисных компаний
    Поделиться публикацией
    Комментарии 5
    • +2
      Я наверное придираюсь, но где пример запуска? где пример вывода? Зачем вам отдельная функция для приведения к нижнему регистру?
      Почему бы не добавить определение кодировки консоли?
      В заголовке речь о телеграм-боте — в примерах им и не пахнет)
      • –1
        Иван, скрипт можно запустить из консоли. Но какой смысл, например, размещать скриншот или текст выполнения скрипта (хотя, может быть, я вас не понял). В любом случае вот пруф, что все работает: prntscr.com/hd80m6 :)

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

        В заголовке кстати нет ничего про Телеграм-бота, просто изначально статья планировалась про него (и на хакатоне мы его запилили с учетом ML), но подумали, что никому про бота читать не будет интересно. А вот как сделать применимый сервис классификации заявок (вне зависимости от того, приходят они из бота или по почте) — читать интереснее (мы так думаем :).
        • 0
          про бота извиняюсь — заголовок прочитал вскользь до картинки).

          Смысл в скриншоте или тексте вывода есть всегда. Это придаст законченность вашему скрипту.
      • 0
        Какая метрика использовалась для оценки алгоритма? Какие результат достигнуты?
        P.S. Привет землякам.
        • 0
          Роман, и вам привет!
          О метриках было рассказано в первой части статьи: взяли точность, так как распределение категорий заявок более-менее равномерное. На 14 категориях при обучающей выборке из 1200 заявок лучший алгоритм показал 73,5%.

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

        Самое читаемое