В октябре команда облачного сервиса Okdesk приняла участие в пензенском хакатоне, в рамках которого мы разработали "коробочного" Telegram-бота для Okdesk. Бот позволит клиентам сервисных компаний отправлять заявки на обслуживание, переписываться по заявками и ставить оценки выполнению заявок не выходя из любимого мессенджера.
Мы планировали написать об этом статью на Хабру, но вовремя остановились. Воистину, кому сегодня интересно читать о том, что на очередном хакатоне был разработан очередной Telegram-бот? Поэтому мы написали продолжение статьи о машинном обучении для классификации заявок в тех. поддержку. В этой статьей рассказываем о том, как после обучения алгоритма сделать работающий сервис, на вход которому передается текст клиентской заявки, а на выходе — категория, к которой относится заявка.
Краткое содержание предыдущей части
Настоятельно рекомендуем прочитать первую часть перед тем, как приступить к чтению данного текста (или хотя бы добавить первую часть статьи в закладки). Ниже изложим краткое содержание.
Итак, сервисные компании оказывают услуги своим клиентам. Клиенты отправляют заявки в службу поддержки клиентов: например, "не работает интернет" или "не проходит проводка в 1С". В сервисной компании разными направлениями занимаются разные люди: проблемы с интернетом лежат в ответственности группы системных администраторов, а проблемы по 1С "падают" на группу сопровождения 1С. Распределение заявок по группам можно поручить диспетчеру, но это дополнительные расходы (зарплата) и потеря времени решения (ко времени решения заявок добавляется реакция диспетчера на распределение заявок). Логично переложить задачу распределения заявок на "умный алгоритм", который по тексту заявки сможет определить, к какому направлению она относится.
Для решения этой задачи была взята обучающая выборка из 1200 текстов заявок с проставленными категориями (14 категорий). По обучающей выборке был составлен словарь (набор имеющих значение для классификации слов), все заявки "проецировались" на словарь (т.е. каждому тексту заявки ставился в соответствие вектор в пространстве словаря), после чего на векторах-проекциях заявок был найден лучший для данной обучающей выборки алгоритм классификации. Для классификации заявок алгоритмом на вход подавался вектор в пространстве словаря, а на выходе алгоритм давал предсказание категории заявки.
Лучший алгоритм показывал на обучающей выборке 74,5% точность классификации (что весьма неплохо для 14 категорий), но благодарные читатели писали в личку, что на их данных примененный алгоритм показывал точность в 92% (а это уже вполне "продакшн" вариант).
Вся эта работа проводилась на ноутбуке, для получения практической пользы необходимо каким-то образом полученный на ноутбуке результат превратить в веб-сервис.
Выгрузка словаря и обученного алгоритма
Напомним, что классификация новых заявок проводится в 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
Скрипт классификации новых заявок
Скрипт, который будет классифицировать новые заявки, должен уметь следующее:
- Загружать словарь из файла words.txt;
- Загружать обученный алгоритм из файла classifier.pkl;
- Проецировать в пространство словаря переданный для классификации текст;
- Возвращать предсказание категории переданного для классификации текста.
Приступим. Для начала импортируем необходимые библиотеки. С большинством из необходимых для работы скрипта библиотек мы познакомились либо в первой части статьи, либо (с 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)
QtRoS
21.11.2017 22:50Какая метрика использовалась для оценки алгоритма? Какие результат достигнуты?
P.S. Привет землякам.krubinshteyn Автор
21.11.2017 23:55Роман, и вам привет!
О метриках было рассказано в первой части статьи: взяли точность, так как распределение категорий заявок более-менее равномерное. На 14 категориях при обучающей выборке из 1200 заявок лучший алгоритм показал 73,5%.
estatic
Я наверное придираюсь, но где пример запуска? где пример вывода? Зачем вам отдельная функция для приведения к нижнему регистру?
Почему бы не добавить определение кодировки консоли?
В заголовке речь о телеграм-боте — в примерах им и не пахнет)
krubinshteyn Автор
Иван, скрипт можно запустить из консоли. Но какой смысл, например, размещать скриншот или текст выполнения скрипта (хотя, может быть, я вас не понял). В любом случае вот пруф, что все работает: prntscr.com/hd80m6 :)
На счет приведения к нижнему регистру, согласен. В первой части немного раскрывается подробнее, что я 10 лет занимался совсем не программированием (и сейчас не занимаюсь), поэтому культура кода вероятно не дотягивает до лучших образцов. Мог бы в своей оправдание сказать, что отдельная функция нужна была в 1 части, т.к. мы предварительно фильтровали слова по признакам «мусорных», но и там можно бы было избавиться.
В заголовке кстати нет ничего про Телеграм-бота, просто изначально статья планировалась про него (и на хакатоне мы его запилили с учетом ML), но подумали, что никому про бота читать не будет интересно. А вот как сделать применимый сервис классификации заявок (вне зависимости от того, приходят они из бота или по почте) — читать интереснее (мы так думаем :).
estatic
про бота извиняюсь — заголовок прочитал вскользь до картинки).
Смысл в скриншоте или тексте вывода есть всегда. Это придаст законченность вашему скрипту.