
1. Описание задачи
В нашей компании очень много пользователей и каждый день они шлют массу обращений на самые разные темы. У нас есть два отдела: «Программные разработки» и «Системные администраторы», и что бы облегчить жизнь техподдержке, был написан классификатор, который стыкует обращение пользователя на тот или другой отдел. В основе классификатора лежит логистическая регрессия.
2. Общая логика
-
Собрать исходные данные
-
Подготовить данные (почистить, привести в нужный формат)
-
Обучить модель
-
Подать новое обращение в модель и получить ответ
Все очень просто! Сам в шоке =)
3. Полезные ссылки
Тут оставлю некоторые ссылки
4. Собираем данные
Мы работаем в системе 1С, поэтому, делаем запрос к базе и выбираем все отработанные обращения с начала времен. Будем использовать эти данные для обучения. Результатом запроса будет являться таблица, где в первой колонке будет текст обращений пользователей, а во второй «1» или «0», где «1» — это «Программные разработки», а «0» — «Системные администраторы».
Функция ПолучитьСырыеДанные() Запрос = Новый Запрос; Запрос.Текст = "ВЫБРАТЬ |ОбращениеВТехПоддержку.ТекстВопроса КАК Appeal, |ВЫБОР |КОГДА ОбращениеВТехПоддержку.ОтделОбслуживания.Код = 50 |ТОГДА 1 |ИНАЧЕ 0 |КОНЕЦ КАК Prediction |ИЗ |Документ.ОбращениеВТехПоддержку КАК ОбращениеВТехПоддержку |ГДЕ |ОбращениеВТехПоддержку.ДатаОтработки <> ДАТАВРЕМЯ(1, 1, 1)"; Возврат Запрос.Выполнить().Выгрузить(); КонецФункции
Далее я решил почистить данные и оставить только слова без цифр, спец символов и пр… Тут можно по экспериментировать!!!
Функция чистит текст обращения. Могут остаться двойные или тройные пробелы но, они будут отброшены при векторизации текста.
Функция ПочиститьПоле(ПреобразованноеПоле) Экспорт СимволыДляЗамены = "1234567890"; СимволыДляЗамены = СимволыДляЗамены + "(){}[]:;""'\|<>.,/?"; СимволыДляЗамены = СимволыДляЗамены + "*-+=_"; СимволыДляЗамены = СимволыДляЗамены + "!@#$%^&№"; Для НомерСимвола = 0 По СтрДлина(СимволыДляЗамены) Цикл Символ = Сред(СимволыДляЗамены, НомерСимвола, 1); ПреобразованноеПоле = СтрЗаменить(ПреобразованноеПоле, Символ, ""); КонецЦикла; ПреобразованноеПоле = СтрЗаменить(ПреобразованноеПоле, """", " "); ПреобразованноеПоле = СтрЗаменить(ПреобразованноеПоле, Символы.ПС, " "); ПреобразованноеПоле = СтрЗаменить(ПреобразованноеПоле, Символы.ВК, " "); ПреобразованноеПоле = СтрЗаменить(ПреобразованноеПоле, Символы.НПП, " "); ПреобразованноеПоле = СтрЗаменить(ПреобразованноеПоле, Символы.ПФ, " "); Возврат ПреобразованноеПоле; КонецФункции
Результатом данного этапа должен стать .СSV файл с которым будем дальше работать.
Тут мы как раз данный файл и собираем.
Функция ПреобразоватьТЗвТекстCSV(ТЗ, Разделитель = ",", флЭкспортироватьИменаКолонок = Истина) ТекстCSV = ""; Если флЭкспортироватьИменаКолонок Тогда ПодготовленнаяСтрока = ""; Для Каждого Колонка Из ТЗ.Колонки Цикл ПодготовленнаяСтрока = ПодготовленнаяСтрока + Колонка.Имя + Разделитель; КонецЦикла; ПодготовленнаяСтрока = Лев(ПодготовленнаяСтрока, СтрДлина(ПодготовленнаяСтрока) - 1); ТекстCSV = ТекстCSV + ПодготовленнаяСтрока + Символы.ПС; КонецЕсли; Счетчик = 0; Для Каждого Строка Из ТЗ Цикл //Если Счетчик = 10000 Тогда //Прервать; //КонецЕсли; ПодготовленнаяСтрока = ""; Для Каждого Колонка Из ТЗ.Колонки Цикл ПреобразованноеПоле = Строка[Колонка.Имя]; Если Колонка.Имя = "Appeal" Тогда ПочиститьПоле(ПреобразованноеПоле); КонецЕсли; ПодготовленнаяСтрока = ПодготовленнаяСтрока + ПреобразованноеПоле + Разделитель; КонецЦикла; ПодготовленнаяСтрока = Лев(ПодготовленнаяСтрока, СтрДлина(ПодготовленнаяСтрока) - 1); ТекстCSV = ТекстCSV + ПодготовленнаяСтрока + Символы.ПС; Счетчик = Счетчик + 1; КонецЦикла; Возврат ТекстCSV; КонецФункции
Файл готов, начинается самое интересное.
5. Обучение модели
# -*- coding: utf-8 -*- import pickle import pandas as pd from sklearn.feature_extraction.text import TfidfVectorizer from sklearn.model_selection import train_test_split from sklearn.metrics import accuracy_score from sklearn import linear_model #Путь к .csv файлу DATA_PATH = r"\\app1\1C\work\Вложения\Python_projects\ClassifierOfUserRequests\data\train.csv" #Файл где хранятся данные о точности нашей модели (для информации) MODEL_ACCURACY_PATH = r"\\app1\1C\work\Вложения\Python_projects\ClassifierOfUserRequests\model_accuracy.txt" #Тут мы храним нашу модель MODEL_PATH = r"\\app1\1C\work\Вложения\Python_projects\ClassifierOfUserRequests\model.pickle" #Тут мы храним наш векторизатор, что бы приводить входящие обращения к нужному виду VECTORIZER_PATH = r"\\app1\1C\work\Вложения\Python_projects\ClassifierOfUserRequests\vectorizer.pickle" def train_model(): # Вычитываем исходные данные train_df = pd.read_csv(DATA_PATH) # Выбираем отдельно данные по двум отделам train_df_0 = train_df[train_df['Prediction'] == 0] train_df_1 = train_df[train_df['Prediction'] == 1] # Смотрю на размер меньшего по колву данных массива, его и берем за основу. size_0 = train_df_0.shape[0] # Наша задача с балансировать две эти выборки, поэтому из большей я рандомно выбираю # такое же кол во данных как есть в меньшей !!!!!!!! train_df_1 = train_df_1.sample(frac=1).reset_index(drop=True) train_df_1 = train_df_1[:size_0] # Собираем две одинаковые по размеру выборки вместе train_df = pd.concat([train_df_0, train_df_1], ignore_index=True) train_df.Prediction.value_counts(normalize=True) # Приводим содержимое в нижний регистр appeal = list(train_df.Appeal.values) appeal = [str(l).lower() for l in appeal] # Для решения задачи классификации необходимо преобразовать каждое обращение # в вектор. Размерность данного вектора будет равна количеству слов # используемых во всех обращениях вообще! Каждая координата соответствует # слову, значение в координате равно количеству раз, слово используется в # в обращении. vectorizer = TfidfVectorizer() tfidfed = vectorizer.fit_transform(appeal) # Делим выборку на тренировочную и тестовую X = tfidfed y = train_df.Prediction.values X_train, X_test, y_train, y_test = train_test_split(X, y, train_size=0.7, random_state=42) # Создаем объект классификатора # С параметрами можно поиграться, может получится настроить еще точнее!!! clf = linear_model.SGDClassifier(max_iter=10000, random_state=42, loss="log", penalty="l2", alpha=1e-5, eta0=1.0, learning_rate="optimal") # Обучаем модель clf.fit(X_train, y_train) # Пишем данные точности в файлик, в моем случаем 94% with open(MODEL_ACCURACY_PATH, 'w', encoding='utf-8') as f: f.write("Train accuracy = %.3f\n" % accuracy_score(y_train, clf.predict(X_train))) f.write("Test accuracy = %.3f" % accuracy_score(y_test, clf.predict(X_test))) # В питоне все объект, поэтому мы можем замариновать нашу модель и векторизатор # что бы потом их можно было легко использовать with open(MODEL_PATH, 'wb') as f: pickle.dump(clf, f) with open(VECTORIZER_PATH, 'wb') as f: pickle.dump(vectorizer, f) if __name__ == "__main__": train_model()
Готово, спасибо инженерам, которые делают эти библиотеки !!!!
6. Как использовать?
Пишем скрипт, который будет принимать входящее обращение и выдавать ответ. Обращаю внимание, входящий запрос должен быть почищен той же функцией который мы готовили исходники!
Ответ будет писать во временный файл.
Код ниже рассчитан на обмен с 1С, а именно, дополнительно принимает имя файла через который будет происходить обмен.
# -*- coding: utf-8 -*- import logging import pickle import sys MAIN_DIR = r"\\app1\1C\work\Вложения\Python_projects\ClassifierOfUserRequests" LOG_PATH = r"\\app1\1C\work\Вложения\Python_projects\ClassifierOfUserRequests\log.txt" VECTORIZER_PATH = r"\\app1\1C\work\Вложения\Python_projects\ClassifierOfUserRequests\vectorizer.pickle" MODEL_PATH = r"\\app1\1C\work\Вложения\Python_projects\ClassifierOfUserRequests\model.pickle" # Логер пропускаем, ничего интересного! def create_logger(name, log_level=logging.DEBUG, stdout=False, file=None): ''' Создает логера, есть возможность создать логера с выводом в stdout или в файл или туда и туда. ''' logger = logging.getLogger(name) logger.setLevel(log_level) formatter = logging.Formatter(fmt='[%(asctime)s] - %(name)s - %(levelname).1s - %(message)s', datefmt='%Y.%m.%d %H:%M:%S') if file is not None: fh = logging.FileHandler(file, encoding='utf-8-sig') fh.setLevel(log_level) fh.setFormatter(formatter) logger.addHandler(fh) if stdout: ch = logging.StreamHandler() ch.setLevel(log_level) ch.setFormatter(formatter) logger.addHandler(ch) return logger def main(): # Принимаем почищеное обращение пользователя (текс) user_request = sys.argv[1].lower() # Имя файла обмена который создает 1С и в последствии удалит pred_path = sys.argv[2] logger.info(user_request) # Востанавливаем наш векторизатор with open(VECTORIZER_PATH, 'rb') as f: vectorizer = pickle.load(f) # Востанавливаем нашу модель with open(MODEL_PATH, 'rb') as f: model = pickle.load(f) # Приводим обрашение к вектору transform_request = vectorizer.transform([user_request]) # Пишем ответ в файл обмена with open(MAIN_DIR + '\\' + pred_path, 'w') as f: prediction = str(model.predict(transform_request)[0]) f.write(prediction) logger.info(prediction) if __name__ == '__main__': try: logger = create_logger("log", file=LOG_PATH) main() except Exception as e: logger.error(e)
На стороне 1С
Функция ПредсказатьОтдел(ТексОбращения) Экспорт Путьприложения = "\\app1\1C\work\Вложения\Python_projects\ClassifierOfUserRequests\"; ОчищеныйЗапрос = ПочиститьПоле(ТекстОбращения); ИмяФайлаОбмена = СтрЗаменить(СтрЗаменить(СтрЗаменить(Строка(ТекущаяДата()), ".", ""), " ", ""), ":", "") + ".txt"; //внимательно с кавычками !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! КомандаИтог = """" + Путьприложения + "predict\predict.exe""" + " """ + ОчищеныйЗапрос + """" + " """ + ИмяФайлаОбмена + """"; ЗапуститьПриложение(КомандаИтог,, Истина); Попытка ПутьКФайлуОбмена = Путьприложения + ИмяФайлаОбмена; Предсказание = Новый ЧтениеТекста; Предсказание.Открыть(ПутьКФайлуОбмена); Ответ = Предсказание.ПрочитатьСтроку(); Предсказание.Закрыть();; УдалитьФайлы(ПутьКФайлуОбмена); Исключение Сообщить(ОписаниеОшибки()); Сообщить("Не удалось автоматически определить отдел обращения!"); КонецПопытки; Если Ответ = "1" Тогда ПредсказаниыйОтдел = Справочники.Отделы.НайтиПоНаименованию("Программные разработки"); Иначе ПредсказаниыйОтдел = Справочники.Отделы.НайтиПоНаименованию("IT"); КонецЕсли; Возврат ПредсказанныйОтдел; КонецФункции
7. Ускоряемся
Что бы отрабатывало быстрее, рекомендую скомпилировать скрипт предсказатор.
Флаги компиляции
pyinstaller -F --hidden-import="sklearn" --hidden-import="sklearn.feature_extraction" --hidden-import="sklearn.utils._weight_vector"predict.pyw
так много, что бы библиотека «sklearn» удачно подтянулась.
.pyw — что бы не вылезало консольное окно.
7.1 Оказалось …
В поисках лучшего решения по интеграции с 1С наткнулся на статью https://habr.com/ru/post/332082/, atnes — от души тебе !!!
Итого. Делаем COM объект
class PredictWrapper: # com spec _public_methods_ = ['predict', ] # методы объекта _public_attrs_ = ['version', ] # атрибуты объекта _readonly_attr_ = [] _reg_clsid_ = '{9cb58c50-2d01-41e9-99d5-07e1fa4baf16}' # uuid объекта _reg_progid_= 'PredictWrapper' # id объекта _reg_desc_ = 'COM wrapper for LR_model' # описание объекта def __init__(self): self.VECTORIZER_PATH = r"\\app1\1C\work\Вложения\Python_projects\ClassifierOfUserRequests\vectorizer.pickle" self.MODEL_PATH = r"\\app1\1C\work\Вложения\Python_projects\ClassifierOfUserRequests\model.pickle" def predict(self, user_request): import sklearn import pickle with open(self.VECTORIZER_PATH, 'rb') as f: vectorizer = pickle.load(f) with open(self.MODEL_PATH, 'rb') as f: model = pickle.load(f) transform_request = vectorizer.transform([user_request]) return str(model.predict(transform_request)[0]) def main(): import win32com.server.register win32com.server.register.UseCommandLine(PredictWrapper) print('registred') if __name__ == '__main__': main()
А на стороне 1С
обВыбратьОтдел = Новый COMОбъект("PredictWrapper"); Ответ = обВыбратьОтдел.predict(ТекстВопроса);
8. Готово
Схема работает очень хорошо! Проверено.
Как Вы понимаете, по аналогии можно придумать множество вариантов использования … Можете прочекать стихи поэтов и потом выбирать, кому принадлежит авторство, разумеется в формате 1v1 =) Либо Пушкин либо Лермонтов.
ссылка на оригинал статьи https://habr.com/ru/articles/569206/
Добавить комментарий