В октябре команда облачного сервиса 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 систему для обслуживания клиентов в сервисных компаниях, параллельно исследуя возможности применения “умных алгоритмов” для решения задач, связанных с обслуживанием.

Комментарии (5)


  1. estatic
    21.11.2017 11:49

    Я наверное придираюсь, но где пример запуска? где пример вывода? Зачем вам отдельная функция для приведения к нижнему регистру?
    Почему бы не добавить определение кодировки консоли?
    В заголовке речь о телеграм-боте — в примерах им и не пахнет)


    1. krubinshteyn Автор
      21.11.2017 12:02

      Иван, скрипт можно запустить из консоли. Но какой смысл, например, размещать скриншот или текст выполнения скрипта (хотя, может быть, я вас не понял). В любом случае вот пруф, что все работает: prntscr.com/hd80m6 :)

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

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


      1. estatic
        21.11.2017 12:10

        про бота извиняюсь — заголовок прочитал вскользь до картинки).

        Смысл в скриншоте или тексте вывода есть всегда. Это придаст законченность вашему скрипту.


  1. QtRoS
    21.11.2017 22:50

    Какая метрика использовалась для оценки алгоритма? Какие результат достигнуты?
    P.S. Привет землякам.


    1. krubinshteyn Автор
      21.11.2017 23:55

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