MidТеория14 min

Основы ML для инженеров

Precision/recall, training/inference, overfitting, feature engineering — ML-концепции для software engineers

Зачем инженеру знать ML

Software engineer не должен строить модели с нуля, но должен понимать ключевые концепции для:

  • Проектирования архитектуры ML-систем
  • Общения с ML-командой
  • Правильной интеграции моделей
  • Мониторинга и отладки ML-компонентов

ML Pipeline

Raw Data → Data Cleaning → Feature Engineering → Training → Evaluation → Deployment
    ↓           ↓                ↓                  ↓           ↓            ↓
  Storage   Preprocessing   Feature Store     Model Store   Metrics    Model Serving

Training vs Inference

Training Inference
Что Обучение модели на данных Использование обученной модели
Когда Периодически (часы/дни) В реальном времени
Ресурсы GPU, много памяти, часы CPU/GPU, миллисекунды
Данные Исторические (training set) Новые, единичные запросы
Кто ML-инженер Production-система
Латентность Неважна Критична (<100ms)

Метрики качества модели

Classification

Метрика Формула Для чего
Accuracy (TP + TN) / Total Общая точность
Precision TP / (TP + FP) «Из того что нашли — сколько верно»
Recall TP / (TP + FN) «Из того что было — сколько нашли»
F1 Score 2 * (P * R) / (P + R) Баланс precision и recall
                    Предсказание
                 Positive  Negative
Реальность  Pos    TP        FN
            Neg    FP        TN

TP = True Positive (правильно нашли)
FP = False Positive (ложная тревога)
FN = False Negative (пропустили)
TN = True Negative (правильно отклонили)

Когда что важнее

Сценарий Приоритет Почему
Spam filter Precision Ложное срабатывание = потерянное письмо
Fraud detection Recall Пропуск мошенничества дороже ложной тревоги
Medical diagnosis Recall Лучше перепроверить, чем пропустить болезнь
Search ranking Precision@K Первые K результатов должны быть релевантны

Расчёт метрик

<?php

declare(strict_types=1);

namespace App\ML;

final readonly class ClassificationMetrics
{
    /**
     * Calculate precision, recall, F1 from confusion matrix.
     *
     * @param int $tp True Positives
     * @param int $fp False Positives
     * @param int $fn False Negatives
     * @param int $tn True Negatives
     * @return array{precision: float, recall: float, f1: float, accuracy: float}
     */
    public function calculate(int $tp, int $fp, int $fn, int $tn): array
    {
        $precision = ($tp + $fp) > 0 ? $tp / ($tp + $fp) : 0.0;
        $recall = ($tp + $fn) > 0 ? $tp / ($tp + $fn) : 0.0;
        $f1 = ($precision + $recall) > 0
            ? 2 * ($precision * $recall) / ($precision + $recall)
            : 0.0;
        $accuracy = ($tp + $fp + $fn + $tn) > 0
            ? ($tp + $tn) / ($tp + $fp + $fn + $tn)
            : 0.0;

        return [
            'precision' => round($precision, 4),
            'recall' => round($recall, 4),
            'f1' => round($f1, 4),
            'accuracy' => round($accuracy, 4),
        ];
    }

    /**
     * Calculate Precision@K for ranking/recommendation.
     *
     * @param array<string> $predicted Predicted items (ranked)
     * @param array<string> $relevant Ground truth relevant items
     * @param int $k Top-K to consider
     */
    public function precisionAtK(array $predicted, array $relevant, int $k): float
    {
        $topK = array_slice($predicted, 0, $k);
        $relevantInTopK = count(array_intersect($topK, $relevant));

        return $k > 0 ? $relevantInTopK / $k : 0.0;
    }
}
## Overfitting и Underfitting
Underfitting Good fit Overfitting
Training error Высокая Низкая Очень низкая
Validation error Высокая Низкая Высокая
Проблема Модель слишком простая Баланс Модель запомнила данные
Решение Сложнее модель, больше features — Регуляризация, больше данных

Борьба с overfitting

Метод Описание
Больше данных Сложнее запомнить все примеры
Регуляризация L1/L2 ограничения на веса
Dropout Случайное отключение нейронов
Cross-validation K-fold для объективной оценки
Early stopping Остановка при росте validation error
Data augmentation Увеличение данных через трансформации

Feature Engineering

Feature engineering — процесс создания признаков (features) из сырых данных для улучшения качества модели.

Типы признаков

Тип Пример Трансформация
Numerical Возраст, цена Нормализация, log-transform
Categorical Страна, категория One-hot encoding, embedding
Text Описание товара TF-IDF, embeddings
Temporal Дата заказа Час дня, день недели, сезон
Aggregated История покупок Среднее, медиана, count
Interaction Цена × количество Комбинации признаков

Пример Feature Engineering

<?php

declare(strict_types=1);

namespace App\ML;

/**
 * Transform raw user data into ML features.
 */
final readonly class UserFeatureExtractor
{
    /**
     * Extract features for user churn prediction.
     *
     * @param array<array{amount: float, created_at: string}> $orders
     * @param array<array{page: string, timestamp: string}> $pageViews
     * @return array<string, float>
     */
    public function extractFeatures(
        array $userProfile,
        array $orders,
        array $pageViews,
    ): array {
        $now = new \DateTimeImmutable();
        $registeredAt = new \DateTimeImmutable($userProfile['registered_at']);

        return [
            // User profile features
            'account_age_days' => (float) $registeredAt->diff($now)->days,
            'has_avatar' => $userProfile['avatar'] !== null ? 1.0 : 0.0,
            'profile_completeness' => $this->calculateCompleteness($userProfile),

            // Order features
            'total_orders' => (float) count($orders),
            'total_revenue' => array_sum(array_column($orders, 'amount')),
            'avg_order_value' => count($orders) > 0
                ? array_sum(array_column($orders, 'amount')) / count($orders)
                : 0.0,
            'days_since_last_order' => $this->daysSinceLastOrder($orders, $now),
            'order_frequency' => $this->orderFrequency($orders, $registeredAt, $now),

            // Engagement features
            'page_views_last_7d' => (float) $this->countRecentViews($pageViews, 7),
            'page_views_last_30d' => (float) $this->countRecentViews($pageViews, 30),
            'unique_pages_last_30d' => (float) $this->uniquePagesViewed($pageViews, 30),

            // Trend features
            'orders_trend' => $this->calculateTrend($orders),
        ];
    }

    private function calculateCompleteness(array $profile): float
    {
        $fields = ['name', 'email', 'phone', 'avatar', 'address'];
        $filled = 0;

        foreach ($fields as $field) {
            if (!empty($profile[$field])) {
                $filled++;
            }
        }

        return $filled / count($fields);
    }

    private function daysSinceLastOrder(array $orders, \DateTimeImmutable $now): float
    {
        if (empty($orders)) {
            return 999.0; // No orders — high value indicates inactivity
        }

        $lastOrder = max(array_column($orders, 'created_at'));
        $lastDate = new \DateTimeImmutable($lastOrder);

        return (float) $lastDate->diff($now)->days;
    }

    private function orderFrequency(
        array $orders,
        \DateTimeImmutable $registeredAt,
        \DateTimeImmutable $now,
    ): float {
        $totalDays = max(1, $registeredAt->diff($now)->days);

        return count($orders) / ($totalDays / 30); // Orders per month
    }

    private function countRecentViews(array $pageViews, int $days): int
    {
        $cutoff = (new \DateTimeImmutable())->modify("-{$days} days");
        $count = 0;

        foreach ($pageViews as $view) {
            if (new \DateTimeImmutable($view['timestamp']) > $cutoff) {
                $count++;
            }
        }

        return $count;
    }

    private function uniquePagesViewed(array $pageViews, int $days): int
    {
        $cutoff = (new \DateTimeImmutable())->modify("-{$days} days");
        $pages = [];

        foreach ($pageViews as $view) {
            if (new \DateTimeImmutable($view['timestamp']) > $cutoff) {
                $pages[$view['page']] = true;
            }
        }

        return count($pages);
    }

    private function calculateTrend(array $orders): float
    {
        // Compare last 30 days vs previous 30 days
        $now = new \DateTimeImmutable();
        $recent = 0;
        $previous = 0;

        foreach ($orders as $order) {
            $date = new \DateTimeImmutable($order['created_at']);
            $daysAgo = $date->diff($now)->days;

            if ($daysAgo <= 30) {
                $recent++;
            } elseif ($daysAgo <= 60) {
                $previous++;
            }
        }

        if ($previous === 0) {
            return $recent > 0 ? 1.0 : 0.0;
        }

        return ($recent - $previous) / $previous; // -1 to +inf
    }
}
## Data Split
Набор Процент Назначение
Training 70-80% Обучение модели
Validation 10-15% Подбор гиперпараметров
Test 10-15% Финальная оценка качества

Важно: Test set используется ТОЛЬКО один раз для финальной оценки. Если использовать его для подбора параметров — получите overfitting на test set.

Model Drift

Model drift — деградация качества модели со временем из-за изменения данных.

Тип drift Описание Пример
Data drift Изменились входные данные Новые категории товаров
Concept drift Изменилась связь input → output Изменилось поведение пользователей
Label drift Изменилось распределение меток Больше fraud после праздников

Мониторинг drift

<?php

declare(strict_types=1);

namespace App\ML;

final readonly class DriftMonitor
{
    /**
     * Check if prediction distribution has changed significantly.
     *
     * @param array<float> $baseline Baseline prediction scores
     * @param array<float> $current Current prediction scores
     * @param float $threshold Alert threshold
     */
    public function detectDrift(array $baseline, array $current, float $threshold = 0.1): DriftResult
    {
        $baselineMean = array_sum($baseline) / count($baseline);
        $currentMean = array_sum($current) / count($current);

        $drift = abs($currentMean - $baselineMean) / max(0.001, $baselineMean);

        return new DriftResult(
            driftDetected: $drift > $threshold,
            driftScore: round($drift, 4),
            baselineMean: round($baselineMean, 4),
            currentMean: round($currentMean, 4),
        );
    }
}

final readonly class DriftResult
{
    public function __construct(
        public bool $driftDetected,
        public float $driftScore,
        public float $baselineMean,
        public float $currentMean,
    ) {}
}
## Итоги
Концепция Суть для инженера
Training vs Inference Обучение (offline, heavy) vs использование (online, fast)
Precision / Recall Зависит от бизнес-задачи, что важнее
Overfitting Модель запомнила данные, не обобщает
Feature Engineering Ключевой вклад инженера в ML-pipeline
Data Split Train / Validation / Test
Model Drift Модель деградирует, нужен мониторинг