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

