Machine Learning для разработчиков: практическое введение без лишней математики

Как веб-разработчику внедрить машинное обучение: выбор стека, бейзлайн на scikit-learn, дообучение ruBERT, сервис на FastAPI, пороги уверенности, типовые ошибки, MLOps для небольшой команды и чек-лист запуска.

Машинное обучение давно перестало быть делом исследовательских лабораторий. Веб-разработчику, которому нужно классифицировать обращения, предсказать отток или добавить рекомендации, не нужна диссертация: хватает зрелых библиотек, предобученных моделей и понимания нескольких инженерных правил. Сложность в другом. Материалы по ML обычно написаны либо для учёных, либо на игрушечных датасетах. Эта статья — практический маршрут: какие задачи ML решает, какой стек выбрать, как обучить первую модель, встроить её в приложение и не попасть в типовые ловушки. Когда ML нужен, а когда нет Машинное обучение — инструмент для задач с нечёткими правилами. Если решение описывается набором условий, пишите условия: их проще тестировать, объяснять и поддерживать. ML оправдан, когда данных много, закономерности неочевидны, а ручные правила разрастаются быстрее, чем их успевают поддерживать. Типовые задачи, с которыми сталкиваются продуктовые команды: Классификация текста — категоризация обращений в поддержку, тональность отзывов, фильтрация спама. Рекомендации — похожие товары, персональная лента, «с этим покупают». Предсказание оттока — вероятность, что клиент перестанет пользоваться продуктом, по его поведению. Поиск аномалий — подозрительные транзакции, необычная активность аккаунтов, сбои в метриках. Работа с изображениями — модерация контента, распознавание товаров, проверка фотографий документов. Прогноз временных рядов — спрос, нагрузка на склад или колл-центр, выручка. Первый вопрос до написания кода: есть ли размеченные данные и сколько их . Универсального минимума нет — он зависит от числа классов, их баланса и сложности различий. Практичный способ проверить — обучить простую модель на том, что есть, и построить кривую обучения: если качество растёт при добавлении данных, разметка окупится. Если данных нет совсем, начните с правил или с языковой модели через API в режиме few-shot, параллельно собирая разметку. Стек ML для веб-разработчика Python: когда нужно обучать и контролировать scikit-learn — классические алгоритмы: линейные модели, деревья, градиентный бустинг, кластеризация, метрики, пайплайны. Основной инструмент для табличных данных и быстрых текстовых бейзлайнов. PyTorch — глубокое обучение: нейросети для текста, изображений и звука. Сегодня это основной фреймворк в исследованиях и в экосистеме Hugging Face. Hugging Face Transformers — загрузка и дообучение предобученных моделей. На Hugging Face Hub опубликованы миллионы моделей, включая модели для русского языка (например, семейство ruBERT). FastAPI — лёгкая обёртка модели в HTTP-сервис с валидацией входных данных. JavaScript: когда модель должна работать в браузере ONNX Runtime Web — запуск моделей в формате ONNX в браузере с ускорением через WebAssembly и WebGPU. В ONNX можно экспортировать модели из PyTorch и scikit-learn. Transformers.js — JavaScript-версия библиотеки Hugging Face: классификация, эмбеддинги, распознавание сущностей прямо на устройстве пользователя. TensorFlow.js — запуск и обучение моделей в браузере и Node.js. Инференс в браузере убирает сетевую задержку и не отправляет данные на сервер. Цена — размер загружаемой модели и зависимость от мощности устройства. Готовые API: когда обучать ничего не нужно Для многих задач модель обучать не нужно вовсе. Языковые модели через API (YandexGPT, GigaChat, зарубежные провайдеры) классифицируют текст и извлекают поля по инструкции, облачные сервисы распознают речь и текст на изображениях. При выборе проверьте, куда уходят данные: для персональных данных граждан РФ это вопрос соответствия 152-ФЗ, а не только удобства. Первая модель: классификация обращений в поддержку Разберём полный путь на условном примере: у сервиса накопились тысячи обращений, размеченных операторами по категориям, и нужно определять категорию автоматически. Шаг 1. Данные и разбиение Сразу откладываем тестовую выборку и не трогаем её до финальной оценки. Для подбора параметров используем отдельную валидационную. import pandas as pd from sklearn.model_selection import train_test_split df = pd.read_csv("support_tickets.csv") # столбцы: text, category df = df.dropna(subset=["text", "category"]) print(df["category"].value_counts()) # 70% — обучение, 15% — валидация, 15% — финальный тест X_train, X_tmp, y_train, y_tmp = train_test_split( df["text"], df["category"], test_size=0.3, random_state=42, stratify=df["category"], ) X_val, X_test, y_val, y_test = train_test_split( X_tmp, y_tmp, test_size=0.5, random_state=42, stratify=y_tmp, ) Если обращения со временем меняются — появляются новые продукты или формулировки, — делите данные по дате, а не случайно: обучайтесь на прошлом, проверяйтесь на более свежем. Иначе оценка окажется оптимистичнее реальности. Шаг 2. Бейзлайн: TF-IDF и логистическая регрессия import joblib from sklearn.pipeline import Pipeline from sklearn.feature_extraction.text import TfidfVectorizer from sklearn.linear_model import LogisticRegression from sklearn.metrics import classification_report pipeline = Pipeline([ ("tfidf", TfidfVectorizer(ngram_range=(1, 2), min_df=2, sublinear_tf=True)), ("clf", LogisticRegression(C=1.0, max_iter=1000, class_weight="balanced")), ]) pipeline.fit(X_train, y_train) print(classification_report(y_val, pipeline.predict(X_val))) joblib.dump(pipeline, "support_classifier.joblib") Такая модель обучается за секунды или минуты на обычном ноутбуке и отвечает за миллисекунды на CPU. Смотрите не на общую accuracy, а на precision и recall по каждому классу: редкие категории часто тонут в среднем значении. Параметр class_weight="balanced" частично компенсирует дисбаланс классов. Во многих задачах такого бейзлайна достаточно. Переходить к нейросетям стоит, только если он не дотягивает до порога, который нужен бизнесу, и вы понимаете, какие ошибки хотите исправить. Шаг 3. Дообучение трансформера Если бейзлайна не хватает — например, модель путает категории, которые различаются смыслом, а не словами, — дообучаем предобученную модель для русского языка. import numpy as np from datasets import Dataset from sklearn.metrics import f1_score from transformers import ( AutoModelForSequenceClassification, AutoTokenizer, DataCollatorWithPadding, Trainer, TrainingArguments, ) labels = sorted(df["category"].unique()) label2id = {label: i for i, label in enumerate(labels)} id2label = {i: label for label, i in label2id.items()} model_name = "DeepPavlov/rubert-base-cased" tokenizer = AutoTokenizer.from_pretrained(model_name) model = AutoModelForSequenceClassification.from_pretrained( model_name, num_labels=len(labels), id2label=id2label, label2id=label2id, ) def to_dataset(texts, cats): ds = Dataset.from_dict({ "text": list(texts), "label": [label2id[c] for c in cats], }) return ds.map( lambda batch: tokenizer(batch["text"], truncation=True, max_length=256), batched=True, ) train_ds = to_dataset(X_train, y_train) val_ds = to_dataset(X_val, y_val) def compute_metrics(eval_pred): logits, y_true = eval_pred y_pred = np.argmax(logits, axis=-1) return {"f1_macro": f1_score(y_true, y_pred, average="macro")} args = TrainingArguments( output_dir="rubert-tickets", num_train_epochs=3, learning_rate=2e-5, per_device_train_batch_size=16, eval_strategy="epoch", save_strategy="epoch", load_best_model_at_end=True, metric_for_best_model="f1_macro", ) trainer = Trainer( model=model, args=args, train_dataset=train_ds, eval_dataset=val_ds, processing_class=tokenizer, data_collator=DataCollatorWithPadding(tokenizer), compute_metrics=compute_metrics, ) trainer.train() trainer.save_model("rubert-tickets/best") Обратите внимание на детали. Лучшая эпоха выбирается по валидационной выборке, а тестовая остаётся нетронутой. Паддинг делается динамически в коллаторе, а не до максимальной длины. Метрика — macro F1, чтобы редкие классы весили наравне с частыми. Дообучение трансформера на тысячах примеров разумно проводить на GPU; на CPU оно займёт часы. Прирост качества относительно бейзлайна заранее предсказать нельзя: иногда он существенный, иногда в пределах погрешности. Поэтому решение о переходе принимают по результатам на валидации с учётом того, что трансформер медленнее, дороже в инференсе и сложнее в эксплуатации. Интеграция модели в веб-приложение Типовая архитектура — модель в отдельном сервисе, приложение обращается к нему по HTTP. Так модель можно обновлять и масштабировать независимо от основного бэкенда. Сервис на FastAPI import joblib from fastapi import FastAPI from pydantic import BaseModel, Field app = FastAPI() model = joblib.load("support_classifier.joblib") # загружаем один раз при старте class PredictRequest(BaseModel): text: str = Field(min_length=1, max_length=10_000) class PredictResponse(BaseModel): category: str confidence: float @app.post("/predict", response_model=PredictResponse) def predict(request: PredictRequest) - PredictResponse: probas = model.predict_proba([request.text])[0] idx = int(probas.argmax()) return PredictResponse( category=str(model.classes_[idx]), confidence=float(probas[idx]), ) # запуск: uvicorn main:app --host 0.0.0.0 --port 8000 Обработчик объявлен обычной функцией, а не async : предсказание нагружает процессор, и FastAPI выполнит такую функцию в пуле потоков, не блокируя цикл событий. Файлы joblib и pickle при загрузке исполняют код, поэтому загружайте только модели, которые собрали сами. Вызов из TypeScript interface ClassificationResult { category: string; confidence: number; } async function classifyTicket(text: string): Promise ClassificationResult { const response = await fetch("/api/ml/predict", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ text }), signal: AbortSignal.timeout(3000), }); if (!response.ok) { throw new Error(`Classification failed: ${response.status}`); } return response.json(); } const AUTO_THRESHOLD = 0.85; const HINT_THRESHOLD = 0.6; async function routeTicket(ticketId: string, text: string) { try { const { category, confidence } = await classifyTicket(text); if (confidence = AUTO_THRESHOLD) { await assignTicketToQueue(ticketId, category); } else if (confidence = HINT_THRESHOLD) { await suggestCategory(ticketId, category, confidence); } else { await sendToManualQueue(ticketId); } } catch { await sendToManualQueue(ticketId); // модель недоступна — работаем по-старому } } Пороги 0,85 и 0,6 здесь — иллюстрация, а не рекомендация. Их подбирают на валидационной выборке под цену ошибки: сколько неверно направленных обращений вы готовы терпеть ради автоматизации. Учтите, что «уверенность» многих моделей плохо откалибрована, и 0,9 не означает 90% правильных ответов. Проверить это помогает калибровочная кривая, а исправить — CalibratedClassifierCV в scikit-learn. Производительность и инфраструктура Точные цифры задержки зависят от модели, длины текста и железа, поэтому измеряйте их на своей нагрузке. Порядок величин такой: Линейные модели на TF-IDF отвечают за миллисекунды на CPU и не требуют специального железа. Трансформеры размера BERT-base на CPU работают на порядок-два медленнее. Для потоковых сценариев с жёсткими требованиями к задержке нужен GPU, батчинг запросов или оптимизация — экспорт в ONNX, квантизация, дистилляция в модель поменьше. Языковые модели через API добавляют сетевую задержку и оплату за токены, зато не требуют ни обучения, ни инфраструктуры. Выбор между API и собственной моделью — это расчёт, а не вопрос вкуса. Сравните стоимость запросов при вашем объёме с арендой сервера, прибавьте время команды на поддержку и учтите ограничения на передачу данных. Для внутренних инструментов с небольшим трафиком API часто выигрывает, для больших стабильных потоков — своя модель. Типовые ошибки при внедрении ML Начинать со сложного Команда сразу строит нейросеть, не проверив, что даёт логистическая регрессия. В итоге неизвестно, оправдана ли сложность. Правило: сначала бейзлайн, потом усложнение — и только если бейзлайн не достигает бизнес-порога. Утечка данных В обучение попадает информация, которой не будет в момент предсказания. Классический пример: в модели оттока используется числ