— Слушайте, Джойс, — сказал Иван. — Вот русский мальчик спрашивает, что вы будете делать, когда разбогатеете?
— Ладно, — сказал он. — Я знаю, какого ответа ждёт мальчик. Поэтому спрошу я. Мальчик вырастет и станет взрослым мужчиной. Всю жизнь он будет заниматься своей… как это вы говорите… интересной работой. Но вот он состарится и не сможет больше работать. Чем тогда он будет заниматься, этот мальчик?
— Я… не знаю, я как-то не думал… Я постараюсь умереть раньше, чем не смогу работать…
Брови бармена полезли на лоб, он испуганно оглянулся на Ивана. В полнейшем смятении Юра заявил:
— И вообще я считаю, что самое важное в жизни для человека — это красиво умереть!Стажеры, Стругацкие
Всем привет!
Вот и мне пришло время красиво умереть как 1С-негу, но это пока не точно. А красота требует жертв, поэтому я решил, что донесу до сообщества красивый проектик на стыке 1С и ML. Проектик классификации заявок ЖКХ.
КЛАССИФИКАЦИЯ ЗАЯВОК ЖКХ
Жизнь 1С-нега прекрасна, она изобилует желтым солнечным светом платформы в красных глазах после ночного обновления, т.к. реструктуризация до сих пор не влезает в нужные три секунды. Динамическое обновление чаще имеет признаки демонического, ибо никогда не знаешь, что там дальше. Поэтому финальная часть спринта заканчивается поставкой инкремента на прод в ноль часов, от чего глаза краснеют, и помочь им даже желтый цвет оформления продуктовой базы не в состоянии. Помимо прочего, от этого желтого начинает тошнить, а если вас тошнит, то что? Правильно - смените профиль! И тут на работе совершенно случайно образуется свободное время в количестве трех дней, ибо задачи сделаны, а новых нет. А если нет новых, то давайте их создадим!
Итак, о чем это я? Да, о заявках в ЖКХ, как поставщике неплохого грязного и шумного датасета для обучения модели. Зачем? Ну чтобы сонные диспетчера не елозили по стулу, силясь выбрать из расплодившейся номенклатуры услуг ту самую, притом частенько промахиваясь из-за дрожания руки. При этом промахиваясь очень избирательно и выбирая "консультация менеджера" вместо того, что стоило бы выбрать.
ВИЖУ ЦЕЛЬ - НЕ ВИЖУ ПРЕПЯТСТВИЙ!
Давайте сформулируем задачу: есть 10к заявок, в которых выбрана какая-то услуга, нужно собрать из этого датасет, обучить модель и запилить сервис инференса для классификации, чтобы диспетчер не просто выбирал услугу, а получал варианты услуг по тексту заявки с оценкой того, на сколько модель уверена в том, что сотворила.
ДАТАСЕТ
Начнем с элементарного - сохраним датасет, и это единственное, что тут будет на языке 1С:
Запрос = Новый Запрос(
"ВЫБРАТЬ ТекстЗаявки, Услуга.Код ИЗ Документ.Заявка");
Датасет = Новый Массив;
Для Каждого Строка Из Запрос.Выполнить().Выгрузить() Цикл
Датасет.Добавить(
Новый Структура("текст,категория", Строка.ТекстЗаявки, Строка.УслугаКод)
);
КонецЦикла;
// какая-то наша функция сохранения
ЗапишемJSONвФайл(Датасет);
Получим какой-то такой датасет (мне его Алиса мутнула, как и картинку для статейки):
[
{
"текст": "Здравствуйте! В подъезде на 5 этаже перегорела лампочка, темень такая, что ногу сломать можно. Задолбали уже!",
"категория": "Освещение"
},
{
"текст": "В квартире из крана вместо горячей воды идёт еле тёплая. Уже третий день так, невозможно мыться!",
"категория": "Водоснабжение"
},
...
{
"текст": "Лифт опять не работает, уже второй раз за неделю! Невозможно с коляской подниматься, это издевательство!",
"категория": "Лифты"
}
]
Отличненько! Будем с этим работать дальше. Но для начала поглядим, что же тут не так? А не так тут прям вот все - все эти приветствия, эмоции, дательные и винительные падежи, времена глаголов и все то, что нам ни разу не упало. Поэтому для начала нам надо данные почистить. Как? А вот тут 1С уже делать нечего, сорян желтизна!
ЧИСТКА ДАННЫХ
Тут будем пользоваться питончиком и библиотекой pymorphy3.
pymorphy3 — это популярная библиотека для Python, которая выполняет морфологический анализ и лемматизацию (приведение к начальной форме) слов на русском языке.
Напишем чистильщик нашего датасета в несколько строчек некоторого кода:
# все эти либы: для регулярок и для морфологии
import re
import pymorphy3
# Инициализируем инструмент для лемматизации русского языка
morph = pymorphy3.MorphAnalyzer()
# Наш кастомный список стоп-слов для ЖКХ (слова уже в начальной форме!)
JKH_STOP_WORDS = {
'здравствуйте', 'приветствовать', 'добрый', 'день', 'вечер', 'утро',
'пожалуйста', 'уважаемый', 'администрация', 'просить', 'заявить',
'обращаться', 'обращение', 'жалоба', 'писать', 'сообщать', 'хотеть',
'наш', 'мой', 'свой', 'который', 'этот', 'тот', 'какой-то', 'какой'
}
# функция очистки и лемматизации
def clean_and_lemmatize(text):
if not isinstance(text, str):
return ""
# 1. Очистка от спецсимволов и цифр
text = text.lower()
text = re.sub(r'[^а-яёa-z\s-]', ' ', text)
# 2. Разбиваем на слова
words = text.split()
lemmatized_words = []
for word in words:
# Получаем список разборов
parsed_variants = morph.parse(word)
# БЕРЕМ ПЕРВЫЙ ВАРИАНТ ИЗ СПИСКА [0] и у него вызываем normal_form
lemma = parsed_variants[0].normal_form
# 3. Фильтрация стоп-слов
if lemma not in JKH_STOP_WORDS:
lemmatized_words.append(lemma)
return " ".join(lemmatized_words)
Что тут у нас происходит? Передаем "грязную" строку, получаем "чистую":
[
{
"текст":"в подъезд на этаж перегореть лампочка темень такой что нога сломать можно задолбать уже",
"категория":"Освещение"
},
{
"текст":"в квартира из кран вместо горячий вода идти еле тёплый уже третий так невозможно мыться",
"категория":"Водоснабжение"
},
{
"текст":"лифт опять не работать уже второй раз за неделя невозможно с коляска подниматься это издевательство",
"категория":"Лифты"
}
]
// Все истории выдуманы, все совпадения случайны.
Вот как-то так и этак получили мы чистой воды датасет. Да, он все-равно несколько шумноват, но уже куда веселее, чем был.
Перейдем к обучению модели!
ОБУЧЕНИЕ
Для таких вот задач классификации я пробовал использовать много всякого, например catboost от Яндекса. На конкретно этой задаче он показал очень слабенький результат и требовал достаточно большого времени на обучение. У меня STT-модель меньше училась на 200 часах шумного аугментированного и частотно порезанного звука телефонных разговоров с ненормативной лексикой, чем catboost на 40к заявках. Так что в лес. В итоге остановился на scikit-learn с его LinearSVC.
Как это все работает? Достаточно просто:
1. Берем панду для чтения датафрейма из нашего json.
2. Выделяем оттуда массивы Х и У для текста и категорий.
3. Разделяем данные на обучающую выборку и тест.
4. Создаем цепочку из вектризатора и LinearSVC.
5. Учим модель.
6. Проверяем.
7. Сохраняем.
Вот такой получается код функции для обучения и записывания данных:
def train_and_save_pipeline(cleaned_json_file, output_model_file="jkh_pipeline.pkl"):
# грузим-грузим датасет
with open(cleaned_json_file, 'r', encoding='utf-8') as f:
data = json.load(f)
# мутим датафрейм
df = pd.DataFrame(data)
X = df['текст']
y = df['категория']
# разделяем на на обучающие и тестовые данные
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42, stratify=y)
# Склеиваем векторизатор и классификатор в единую цепочку.
pipeline = Pipeline([
('tfidf', TfidfVectorizer(ngram_range=(1, 2), max_features=5000)),
('clf', LinearSVC(random_state=42, C=1.0, class_weight='balanced'))
])
# Передаем сырой (но уже очищенный нами) текст X_train.
# Пайплайн сам внутри вызовет fit_transform для TF-IDF и fit для LinearSVC.
pipeline.fit(X_train, y_train)
# Для предсказания просто передаем текстовый X_test. Пайплайн сам векторизует его внутри себя.
y_pred = pipeline.predict(X_test)
print(classification_report(y_test, y_pred, zero_division=0))
# Сохраняем файл, в котором «зашиты» и правила перевода слов в числа, и логика модели.
joblib.dump(pipeline, output_model_file)
ИНФЕРЕНС
Для инференса нам надо загрузить модель и пропустить через нее очищенный текст. Для этого наваяем такой вот код:
class JkhClassifier:
def __init__(self, pipeline_path="jkh_pipeline.pkl"):
self.pipeline = joblib.load(pipeline_path)
# Достаем саму модель и список классов из пайплайна, чтобы знать имена категорий
self.model = self.pipeline.named_steps['clf']
self.classes = self.model.classes_
def predict_top_k(self, raw_text, k=3):
# Прогоняем текст через векторизатор пайплайна
# Шаг векторизации в пайплайне называется 'tfidf'
vectorized_text = self.pipeline.named_steps['tfidf'].transform([cleaned_text])
# Получаем расстояния до гиперплоскостей
distances = self.model.decision_function(vectorized_text)[0]
# Сортируем индексы оценок по возрастанию и берем последние K
top_indices = np.argsort(distances)[::-1][:k]
# Формируем красивый список результатов
results = []
for idx in top_indices:
results.append({
"category": self.classes[idx],
"score": round(float(distances[idx]), 3) # Оценка уверенности (чем выше, тем лучше)
})
return results
if __name__ == "__main__":
# Запускаем классификатор
classifier = JkhClassifier("jkh_pipeline.pkl")
# Тестовые заявки от джимми
new_tickets = [
"Добрый вечер!!! Подскажите, почему опять отключили горячую воду на Ленина 45?? Из крана идет ледяная!",
"У нас в первом подъезде на пятом этаже лампочка перегорела, темень невозможная, почините пожалуйста.",
"Алло, аварийная? Срочно!!! Из квартиры сверху хлещет вода, нас заливает, весь потолок в коридоре мокрый!"
]
for complex_ticket in new_tickets:
# Получаем Топ-3 категории
ticket = clean_and_lemmatize(complex_ticket)
top_3_categories = classifier.predict_top_k(ticket)
print(f"Входящая заявка:\n\"{complex_ticket}\"\n")
print(f"Входящая заявка очищенная:\n\"{ticket}\"\n")
print("Предложенные моделью категории (Топ-3):")
for i, res in enumerate(top_3_categories, 1):
print(f" {i}. {res['category']} (Уверенность: {res['score']})")
И вот что получаем на выходе:
"У нас в первом подъезде на пятом этаже лампочка перегорела, темень невозможная, почините, пожалуйста."
Входящая заявка очищенная:
"у мы в первый подъезд на пятый этаж лампочка перегореть темень невозможный починить"Предложенные моделью категории (Топ-3):
1. Освещение (Уверенность: -0.169)
2. Благоустройство (Уверенность: -0.568)
3. Авария/Протечка (Уверенность: -0.58)
"Алло, аварийная? Срочно!!! Из квартиры сверху хлещет вода, нас заливает, весь потолок в коридоре мокрый!"Входящая заявка очищенная:
"алло аварийный срочно из квартира сверху хлестать вода мы заливать весь потолок в коридор мокрый"Предложенные моделью категории (Топ-3):
1. Авария/Протечка (Уверенность: -0.237)
2. Водоснабжение (Уверенность: -0.517)
3. Благоустройство (Уверенность: -0.626)
Ну и не сложно догадаться, как дальше с этим всем можно работать. Пользуйтесь!
Всем всех благ: счастья, здоровья, ума.
Подписывайтесь на каналы, ставьте лайки, флудите в комментах - делайте свое дело!
Вступайте в нашу телеграмм-группу Инфостарт