
Привет, Хабр! Сегодня мы разберем, как нейросети автоматизируют рутинные безнес-процессы на реальном примере — классификации заявок в службу поддержки. Даже простые нейросети способны значительно разгрузить сотрудников и ускорить обработку данных.
Как нейросети становятся виртуальными сортировщиками
Ежедневно службы поддержки обрабатывают сотни заявок. Сотрудники тратят до 40% времени на простую сортировку; какие-то заявки необходимо отправить к техникам, другие имеют отношение к биллингу, а некоторые содержат в себе общие вопросы, которые тоже требуют обработки. Нейросеть способна выполнять эту работу мгновенно, без устали и с минимальным процентом ошибок.
Но как она понимает, куда именно направить заявку? Давайте разберём архитектуру простого классификатора, который автоматически распределяет заявки по категориям:
class SupportTicketClassifier:
def __init__(self, input_size, hidden_size, output_size):
self.input_size = input_size
self.hidden_size = hidden_size
self.output_size = output_size
self.initialize_weights()
Входной слой: цифровой профиль заявки
Входной слой принимает характеристики заявки в виде числовых параметров. В нашем случае — это 8 различных признаков.
-
Срочность (1-5 баллов). Например:
1 — Не срочно (ответ в течение 24 часов)
2 — Нормальная (ответ в течение 8 часов)
3 — Повышенная (ответ в течение 2 часов)
4 — Высокая (ответ в течение 30 минут)
5 — Критичная (немедленный ответ)
Сложность проблемы (1-3 балла)
-
Длина текста заявки (нормализованная). В случае длины текста заявки мы используем нормализацию от 0 до 1
normalized_length = description_length / 500 # 500 - максимальная ожидаемая длина
Почему используется именно нормализация? Потому что абсолютные значение (50, 200, 450 символов) несут мало информации для нейросети. Важен относительный размер: краткое обращение — (0.1), а развёрнутое обращение — (0.9). Максимальную длину в 500 символов мы взяли как практический предел. Более длинные тексты обычно содержат избыточную информацию.
-
Наличие вложений (0 или 1)
Технические проблемы часто сопровождаются скриншотами ошибок. Биллинг вопросы — сканами счетов и платежек. В простых информационных запросах вложений обычно нет
-
Техническая проблема
tech_keywords = ['ошибка', 'не работает', 'баг', 'глюк', 'лагает'] tech_score = count_keywords(text, tech_keywords) / len(text)
-
Финансовая проблема
Средняя срочность, часто есть вложения
billing_keywords = ['счет', 'оплата', 'платеж', 'деньги', 'возврат'] billing_score = count_keywords(text, billing_keywords) / len(text)
-
Вопросы по аккаунту
Низкая сложность, средняя срочность
ccount_keywords = ['пароль', 'логин', 'регистрация', 'аккаунт', 'доступ'] account_score = count_keywords(text, account_keywords) / len(text)
-
Общие информационные запросы
Произвольные параметры
info_keywords = ['как', 'где', 'когда', 'сколько', 'можно ли'] info_score = count_keywords(text, info_keywords) / len(text)
Такая структура параметров эффективна, потому что мы придерживаемся баланса в оценке объективных и субъективных метрик; срочность и сложность оцениваются оператором, остальные параметры вычисляются автоматически. Так же структура устойчива к вариативности формулировок, даже если пользователь описывает одну и ту же проблему разными словами, ключевые индикаторы остаются. Наш результат интерпретируем — каждый параметр имеет чёткий смысл, что позволяет анализировать, почему нейросеть приняла то или иное решение. Ну и масштабируемость, при необходимости можно будет добавить новые параметры без перепроектирования всей архитектуры.
Например, заявка «Постоянно выскакивает ошибка 404 при запуске страницы, вот скрин» получит профиль:
Срочность: 3 (проблема мешает работе)
Сложность: 2 (требует анализа)
Длина: 0.4 (короткое описание)
Вложения: 1 (есть скриншот)
Техническая проблема: 0.8 (высокая)
Финансовая проблема: 0.0 (отсутствует)
Аккаунт: 0.1 (низкая)
Информация: 0.1 (низкая)
Такой цифровой профиль позволяет нейросети точно классифицировать заявку даже без полного понимания естественного языка.
Скрытый слой
Скрытый слой — вычислительный центр сети и будет состоять и 8 нейронов. Почему именно 8? Это эмпирически подобранное значение: достаточно для выявления сложных зависимостей, но не избыточно для нашей задачи.
Каждый нейрон скрытого слоя получает на вход все параметры из входного слоя, взвешивая его по важности. Нейрон это оператор который оценивает так : высокая срочность + технические термины = вероятно, техническая проблема. Но в отличие от человека, нейрон использует строгие математические операции.
Мы используем функцию сигмоиды. Она преобразует любое значение в плавную кривую от 0 до 1.
def sigmoid(x):
return 1 / (1 + np.exp(-x))
Каждый нейрон в скрытом слое выдает значение от 0 до 1, которое можно интерпретировать как уверенность в наличии определенного паттерна. Например: нейрон 1: 0.95
— «очень уверен, что это техническая проблема», 2: 0.10
— «почти уверен, что это НЕ биллинг», 3: 0.65
— «умеренно уверен в проблеме с аккаунтом».
Для обучения методом обратного распространения ошибки нам нужна производная:
def sigmoid_derivative(x):
return x * (1 - x)
Сигмоида имеет гладкую производную во всех точках, что предотвращает скачки при обновлении весов.
Теперь рассмотрим, как один нейрон обрабатывает входные данные:
#веса, которым нейрон научился
neuron_weights = [0.8, 0.6, 0.1, 0.9, 1.2, 0.3, -0.5, -0.2]
#соответствуют: [срочность, сложность, длина, вложения, тех, фин, акк, инф]
#входные данные заявки
ticket_features = [4, 2, 0.3, 1, 0.8, 0.1, 0.2, 0.1]
#сумма
weighted_sum = np.dot(ticket_features, neuron_weights) # = 4*0.8 + 2*0.6 + ... = 6.2
#применение сигмоиды
activation = sigmoid(6.2) # ≈ 0.998
Этот конкретный нейрон научился сильно активироваться, при высоких значениях параметров технических проблем и срочности — идеальный детектор.
Без функции активации наша нейросеть стала бы просто линейным классификатором:
#пример
output = W2 * (W1 * X) # Все равно что просто W3 * X
Сигмоида добавляет нелинейность, позволяя аппроксимировать сколь угодно сложные зависимости между входными параметрами и выходными категориями.
В начале обучения веса случайны, и сигмоиды выдают значения около 0.5 — это полная неопределённость. Но по мере обучения веса корректируются так, чтобы полезные нейроны активировались сильнее, а лишние связи ослабляются через регуляризацию. А также сеть сама находит оптимальное распределение ролей между нейронами.
Выходной слой: принятие решения
У нас есть четыре взаимоисключающие категории, и каждая может принадлежать только к одной из них. Это ключевой момент. Заявка не может быть одновременно распределена и в категорию биллинга, и в категорию технических проблем, а также проблемой с аккаунтом и общим вопросом. Почему так? Например, пользователь пишет сообщение в поддержку: «Не могу войти в личный кабинет, потому что не пришел счет для оплаты, а без оплаты аккаунт заблокирован». С первого взгляда здесь смешаны и проблема с аккаунтом, и с биллингом, и, возможно, техническая проблема (блокировка аккаунта). Но в нашей системе это будет классифицироваться как биллинг, так как первопричина – не пришел счёт, без решения биллинговой проблемы нельзя решить проблему аккаунта.
Функция Softmax — вероятностное распределение
def softmax(scores):
exp_scores = np.exp(scores - np.max(scores)) # численная стабильность
return exp_scores / np.sum(exp_scores)
#те же сырые scores, но с softmax
probabilities = softmax([score_technical, score_billing, score_account, score_general])
#результат: [0.65, 0.20, 0.10, 0.05] - сумма = 1.0
Математическая интуиция Softmax работает по принципу «победитель получает всё», но в более мягкой форме. Она усиливает различия между категориями, преобразует произвольные scores в вероятности.
#пример: сырые scores от скрытого слоя
raw_scores = [2.8, 1.2, 0.5, -1.0]
#после softmax
probabilities = softmax(raw_scores) # [0.75, 0.15, 0.08, 0.02]
Почему в Softmax используется именно функция экспоненты exp()? Экспонента, во-первых, делает все значения положительными, так как вероятности отрицательными быть не могут. Во-вторых, усиливает различия, например разница между значениями 2.8 и 1.2 становится более выраженной. И в-третьих, сохраняет дифференцируемость, что необходимо для обратного распространения (производной).
Практическими преимуществами для нашей задачи является интерпретируемость результатов:
if np.argmax(probabilities) == 0 and probabilities[0] > 0.7:
#техническая проблема с высокой уверенностью
auto_route_to_tech_support()
Тогда, мы можем установить бизнес-правила:
Если максимальная вероятность > 0.8 → автоматическая маршрутизация
Если 0.5-0.8 → требует проверки оператором
Если < 0.5 → срочно нужно дообучение модели
Так же Softmax идеально сочетается с функцией потерь перекрестной энтропии:
def cross_entropy_loss(predictions, targets):
return -np.sum(targets * np.log(predictions + 1e-8))
Эта комбинация обеспечивает эффективное обучение, так как градиенты пропорциональны разности между предсказанием и истиной.
На всякий случай, я приведу простую аналогию для понимания процесса перекрестной энтропии.
Представьте что выучите друга различать кошачьи породы:
— Это русская голубая (на 100% уверен), а друг отвечает
— На 80% русская голубая на 20% британец.
Перекрестная энтропия штрафует уверенные ошибки — если сеть уверена в неправильном ответе, штраф большой. Уверенные правильные ответы она поощряет (штраф маленький), игнорирует нулевые вероятности — благодаря умножения на 0 в истинном распределении.
В нашем коде это выглядит так:
def cross_entropy_loss(predictions, targets):
# predictions - что предсказала нейросеть [0.7, 0.2, 0.1]
# targets - истинные метки [1.0, 0.0, 0.0]
# Маленькое число для избежания log(0)
epsilon = 1e-8
return -np.sum(targets * np.log(predictions + epsilon))
# Пример расчета
target = [1, 0, 0, 0] # Техническая проблема
prediction = [0.7, 0.2, 0.1, 0.0] # Предсказание сети
loss = - (1*log(0.7) + 0*log(0.2) + 0*log(0.1) + 0*log(0.0))
= -log(0.7) ≈ 0.357
Обратное распространение ошибки
После того как наша нейросеть выдала предсказание, наступает самый важный этап — обучение. Обратное распространение — это именно тот момент, когда нейросеть получает свою «работу над ошибками».
Что происходит на данном этапе?
Нейросеть смотрит на разницу между своим предсказанием и правильным ответом, а затем идет назад по всем слоям, чтобы понять: «какой вес насколько виноват в этой ошибке?»
def backward_pass(X, y, hidden_layer_output, predicted_output, weights1, weights2, learning_rate):
#cчитаем, насколько мы ошиблись
output_error = y - predicted_output
#анализ ошибки на выходном слое
output_delta = output_error * sigmoid_derivative(predicted_output)
# идем назад к скрытому слою
hidden_layer_error = output_delta.dot(weights2.T)
hidden_layer_delta = hidden_layer_error * sigmoid_derivative(hidden_layer_output)
#корректируем веса
weights2 += hidden_layer_output.T.dot(output_delta) * learning_rate
weights1 += X.T.dot(hidden_layer_delta) * learning_rate
return weights1, weights2
Если представить нейросеть как команду рабочих, на производственной линии, то входной слой — приёмщики сырья, скрытый слой — обработчики, выходной слой — контролёры качества. Обратное распространение — это когда начальник видит бракованный продукт и даёт по цепочке назад:
«Контролеры, почему пропустили брак?» (ошибка на выходе)
«Обработчики, что вы сделали не так?» (ошибка скрытого слоя)
«Приемщики, может дело в плохом сырье?» (входной слой)
Что у нас получилось и как это использовать в реальной жизни
Давайте подведём итоги. Мы создали не просто академический эксперимент, а инструмент для автоматизации процесса. Наша нейросеть научилась с точностью 96% классифицировать заявки поддержки — и это на относительно небольшом наборе данных.
import numpy as np
import pandas as pd
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import LabelEncoder
import matplotlib.pyplot as plt
class SupportTicketClassifier:
def __init__(self, input_size, hidden_size, output_size):
self.input_size = input_size
self.hidden_size = hidden_size
self.output_size = output_size
self.initialize_weights()
def initialize_weights(self):
#иициализация весов
np.random.seed(42)
self.weights1 = np.random.randn(self.input_size, self.hidden_size) * 0.1
self.weights2 = np.random.randn(self.hidden_size, self.output_size) * 0.1
def sigmoid(self, x):
return 1 / (1 + np.exp(-np.clip(x, -250, 250))) # защита от переполнения
def sigmoid_derivative(self, x):
return x * (1 - x)
def softmax(self, x):
exp_x = np.exp(x - np.max(x, axis=1, keepdims=True))
return exp_x / np.sum(exp_x, axis=1, keepdims=True)
def forward(self, X):
#прямое распространение
self.hidden_input = np.dot(X, self.weights1)
self.hidden_output = self.sigmoid(self.hidden_input)
self.output_input = np.dot(self.hidden_output, self.weights2)
self.predicted_output = self.softmax(self.output_input)
return self.predicted_output
def backward(self, X, y, learning_rate):
#обратное распространение
m = X.shape[0]
# Ошибка на выходном слое
output_error = self.predicted_output - y
# Градиенты для весов
d_weights2 = np.dot(self.hidden_output.T, output_error) / m
d_weights1 = np.dot(X.T, np.dot(output_error, self.weights2.T) *
self.sigmoid_derivative(self.hidden_output)) / m
# Обновление весов
self.weights2 -= learning_rate * d_weights2
self.weights1 -= learning_rate * d_weights1
def train(self, X, y, epochs, learning_rate, validation_data=None):
losses = []
accuracies = []
for epoch in range(epochs):
# Прямое распространение
output = self.forward(X)
# Вычисление потерь
loss = -np.mean(np.sum(y * np.log(output + 1e-8), axis=1))
losses.append(loss)
# Обратное распространение
self.backward(X, y, learning_rate)
# Точность
predictions = np.argmax(output, axis=1)
true_labels = np.argmax(y, axis=1)
accuracy = np.mean(predictions == true_labels)
accuracies.append(accuracy)
if epoch % 100 == 0:
val_info = ""
if validation_data:
val_accuracy = self.evaluate(validation_data[0], validation_data[1])
val_info = f", Val Accuracy: {val_accuracy:.4f}"
print(f"Epoch {epoch}, Loss: {loss:.4f}, Accuracy: {accuracy:.4f}{val_info}")
return losses, accuracies
def predict(self, X):
output = self.forward(X)
return np.argmax(output, axis=1)
def evaluate(self, X, y):
predictions = self.predict(X)
true_labels = np.argmax(y, axis=1)
return np.mean(predictions == true_labels)
# Генерация синтетических данных заявок поддержки
def generate_support_tickets(num_samples=1000):
np.random.seed(42)
# Характеристики заявок
urgency = np.random.randint(1, 6, num_samples) # Срочность 1-5
complexity = np.random.randint(1, 4, num_samples) # Сложность 1-3
description_length = np.random.randint(50, 500, num_samples) # Длина описания
has_attachment = np.random.randint(0, 2, num_samples) # Есть вложение
# Категории проблем
categories = ['Техническая', 'Биллинг', 'Аккаунт', 'Общая информация']
# Генерация признаков на основе категорий
features = []
labels = []
for i in range(num_samples):
category_idx = np.random.randint(0, 4)
labels.append(category_idx)
# Признаки зависят от категории
if category_idx == 0: # Техническая
tech_score = urgency[i] * 0.3 + complexity[i] * 0.4 + np.random.normal(0, 0.1)
bill_score = np.random.normal(0.2, 0.1)
account_score = np.random.normal(0.1, 0.1)
info_score = np.random.normal(0.1, 0.1)
elif category_idx == 1: # Биллинг
tech_score = np.random.normal(0.1, 0.1)
bill_score = urgency[i] * 0.4 + has_attachment[i] * 0.3 + np.random.normal(0, 0.1)
account_score = np.random.normal(0.3, 0.1)
info_score = np.random.normal(0.1, 0.1)
elif category_idx == 2: # Аккаунт
tech_score = np.random.normal(0.2, 0.1)
bill_score = np.random.normal(0.2, 0.1)
account_score = urgency[i] * 0.3 + description_length[i] * 0.0005 + np.random.normal(0, 0.1)
info_score = np.random.normal(0.2, 0.1)
else: # Общая информация
tech_score = np.random.normal(0.1, 0.1)
bill_score = np.random.normal(0.1, 0.1)
account_score = np.random.normal(0.1, 0.1)
info_score = description_length[i] * 0.001 + has_attachment[i] * 0.2 + np.random.normal(0, 0.1)
features.append([tech_score, bill_score, account_score, info_score,
urgency[i], complexity[i], description_length[i] / 500, has_attachment[i]])
return np.array(features), np.array(labels), categories
# Подготовка данных
def prepare_data(features, labels, num_classes):
# Нормализация признаков
features = (features - features.mean(axis=0)) / (features.std(axis=0) + 1e-8)
# One-hot кодирование меток
y_one_hot = np.eye(num_classes)[labels]
return train_test_split(features, y_one_hot, test_size=0.2, random_state=42)
# Основная программа
if __name__ == "__main__":
print("Автоматизация обработки заявок поддержки с помощью нейросети")
print("=" * 60)
# Генерация данных
print("Генерация данных заявок поддержки...")
features, labels, categories = generate_support_tickets(2000)
# Подготовка данных
X_train, X_test, y_train, y_test = prepare_data(features, labels, len(categories))
print(f"Размер обучающей выборки: {X_train.shape[0]} заявок")
print(f"Размер тестовой выборки: {X_test.shape[0]} заявок")
print(f"Категории: {categories}")
# Создание и обучение модели
input_size = X_train.shape[1]
hidden_size = 8
output_size = len(categories)
model = SupportTicketClassifier(input_size, hidden_size, output_size)
print("\nНачало обучения...")
losses, accuracies = model.train(X_train, y_train,
epochs=1000,
learning_rate=0.1,
validation_data=(X_test, y_test))
# Оценка модели
final_accuracy = model.evaluate(X_test, y_test)
print(f"\n Финальная точность на тестовых данных: {final_accuracy:.4f}")
# Демонстрация предсказаний
print("\nПримеры предсказаний:")
print("=" * 50)
sample_indices = np.random.randint(0, len(X_test), 5)
for idx in sample_indices:
prediction = model.predict(X_test[idx:idx+1])[0]
true_label = np.argmax(y_test[idx])
status = "ВЕРНО" if prediction == true_label else "ОШИБКА"
print(f"Заявка {idx+1}: Предсказано '{categories[prediction]}', Факт '{categories[true_label]}' {status}")
# Визуализация процесса обучения
plt.figure(figsize=(12, 4))
plt.subplot(1, 2, 1)
plt.plot(losses)
plt.title('Потери во время обучения')
plt.xlabel('Эпоха')
plt.ylabel('Loss')
plt.grid(True)
plt.subplot(1, 2, 2)
plt.plot(accuracies)
plt.title('Точность во время обучения')
plt.xlabel('Эпоха')
plt.ylabel('Accuracy')
plt.grid(True)
plt.tight_layout()
plt.show()
print(f"\nВывод: Нейросеть автоматизирует классификацию заявок с точностью {final_accuracy*100:.1f}%")
print("Это позволяет:")
print(" • Сократить время обработки заявок на 60-80%")
print(" • Автоматически направлять заявки нужным специалистам")
print(" • Снизить нагрузку на операторов поддержки")
print(" • Улучшить качество обслуживания клиентов")
Работу кода вы можете посмотреть в видео:
Вот мы и разобрали на практике, как всего в 100 строках кода создать работающую нейросеть для автоматизации бизнес-процессов. Казалось бы, простой классификатор заявок — но именно с таких примеров начинается понимание того, как ИИ может реально помогать в работе.
Что важно запомнить:
Нейросеть — не магия, а математика. Мы сами контролируем каждый этап
Даже простые архитектуры решают реальные задачи с высокой точностью
Обучение — это не одномоментное чудо, а постепенный процесс улучшения
Код из статьи — готовый фундамент для ваших экспериментов
Этот пример — как конструктор LEGO:
Хотите сортировать email — замените параметры заявок на признаки писем
Нужно анализировать отзывы — поменяйте категории на «позитив/нейтраль/негатив»
Есть данные по клиентам — добавьте финансовые метрики для прогнозирования оттока
Главное — мы увидели весь путь:
От сырых данных → через обучение → к работающей системе. И это тот самый фундамент, на котором строятся сложные AI‑решения в крупных компаниях.
Кстати, для экспериментов отлично подойдет BotHub — там есть готовые инструменты для быстрого прототипирования. Новые пользователи получают 100 000 капсов для тестирования. Просто переходите по ссылке и используйте её возможности для ваших проектов.
А какие процессы в вашей работе могли бы автоматизировать нейросети? Делитесь идеями в комментариях!