Базовый ML
Путь от нуля до уверенного Junior ML Engineer: математика, Python, классический ML и первые нейросети.
🧱 Фундамент
Python для ML
NumPy, pandas, matplotlib — основной стек для работы с данными.
SQL
Запросы, джоины, оконные функции — must have для любого DS/ML.
Математика и статистика
Линейная алгебра, матстат, теория вероятностей — фундамент для понимания алгоритмов.
Сложность алгоритмов (Big O)
O(1), O(n), O(n²) — как оценивать скорость кода и оптимизировать алгоритмы. Топ-вопрос на собесах.
Метрики классификации
Precision, Recall, F1, ROC-AUC, PR-AUC — как оценивать качество моделей.
Классический ML
Деревья, бустинг, линейные модели, кластеризация — основа большинства production-решений.
🔬 Глубина
Нейросети и Deep Learning
Перцептроны, backprop, CNN, RNN — понимание основ нейросетей на PyTorch.
Backpropagation
Chain rule, граф вычислений, forward/backward pass, vanishing gradients — как нейросети учатся.
Feature Engineering
Генерация признаков, обработка категорий, текстов и временных рядов.
Валидация и подбор моделей
Cross-validation, гиперпараметры, bias-variance trade-off, Optuna.
Основы MLOps
опц.Docker, DVC, MLflow, эксперименты — как довести модель до продакшна.
Подготовка к собеседованию
Python, SQL, математика, классический ML и Deep Learning — ключевые вопросы для подготовки к собеседованию.
GIL (Global Interpreter Lock) — мьютекс, который позволяет только одному потоку выполнять Python-байткод одновременно. Из-за этого многопоточность (threading) не ускоряет CPU-bound задачи. Обходные пути: multiprocessing (отдельные процессы, каждый со своим GIL), C-extensions (NumPy освобождает GIL при вычислениях), asyncio для I/O-bound задач. В CPython 3.13+ появился экспериментальный free-threaded режим.
Tuple хранит элементы в непрерывном блоке памяти фиксированного размера, list — в динамическом массиве с overallocation. Tuple хэшируем (если все элементы хэшируемы) — можно использовать как ключ dict и элемент set. Tuple создаётся быстрее и занимает меньше памяти (~40% экономии на маленьких объектах). Семантически tuple — структура (запись с фиксированными полями), list — коллекция однотипных элементов.
return завершает функцию и возвращает значение, yield приостанавливает выполнение и «выдаёт» значение, сохраняя состояние. При следующем вызове next() выполнение продолжается с места yield. Генераторы нужны для ленивых вычислений: обработка файла в 100GB построчно (yield каждой строки) использует O(1) памяти вместо O(N). Также используются для бесконечных последовательностей и pipeline-обработки.
Декоратор — функция, принимающая функцию и возвращающая обёртку. @timer эквивалентен func = timer(func). Таймер: def timer(fn): def wrapper(*args, **kwargs): t0 = time.time(); result = fn(*args, **kwargs); print(time.time() - t0); return result; return wrapper. Важно использовать functools.wraps(fn) на wrapper, чтобы сохранить __name__ и __doc__ оригинальной функции.
copy.copy() создаёт новый объект, но вложенные объекты остаются ссылками на оригинал (shallow copy). copy.deepcopy() рекурсивно копирует всё. Пример: a = [[1, 2], [3, 4]]; b = copy.copy(a); b[0].append(5) — теперь a[0] тоже [1, 2, 5], потому что b[0] и a[0] — один и тот же список. С deepcopy такого не будет.
__slots__ заменяет __dict__ фиксированным набором атрибутов. Экономит ~40% памяти на объект (нет dict overhead) и немного ускоряет доступ к атрибутам. Используют, когда создаётся миллионы мелких объектов (точки, координаты, записи данных). Минусы: нельзя добавлять новые атрибуты динамически, сложнее с множественным наследованием, не работает с weakref без явного включения.
INNER JOIN оставляет только строки, где есть совпадение в обеих таблицах. LEFT JOIN сохраняет все строки из левой таблицы, дополняя NULL-ами при отсутствии совпадения справа. LEFT JOIN нужен, когда важно сохранить все записи: «все пользователи и их заказы (включая тех, кто ничего не купил)». INNER JOIN — когда нужны только связанные данные: «пользователи, которые сделали хотя бы один заказ».
При одинаковых значениях: ROW_NUMBER присваивает уникальные номера произвольно (1,2,3,4), RANK пропускает позиции (1,2,2,4), DENSE_RANK не пропускает (1,2,2,3). ROW_NUMBER — для пагинации и дедупликации. RANK — для соревновательного ранжирования (третье место пропускается, если два вторых). DENSE_RANK — когда нужны «плотные» ранги без пропусков.
Индекс — B-tree (обычно), который поддерживает отсортированный порядок для быстрого поиска за O(log N). При SELECT индекс позволяет найти строку без full scan. Но при INSERT/UPDATE нужно обновить не только таблицу, но и каждый индекс — это дополнительные записи на диск и перебалансировка дерева. Поэтому на write-heavy таблицах лишние индексы вредят производительности.
WHERE фильтрует строки ДО агрегации, HAVING — ПОСЛЕ. Порядок выполнения: FROM → WHERE → GROUP BY → HAVING → SELECT → ORDER BY. Поэтому в HAVING можно использовать агрегатные функции (COUNT, SUM), а в WHERE — нельзя. Пример: WHERE city = "Moscow" (фильтр по строке) vs HAVING COUNT(*) > 5 (фильтр по результату агрегации).
p-value — вероятность получить наблюдаемый (или более экстремальный) результат ПРИ УСЛОВИИ, что H₀ верна. Это НЕ вероятность того, что H₀ верна. p < 0.05 при 20 тестах даст ~1 ложно-положительный результат (multiple comparisons). Также маленький p-value не говорит о практической значимости: на огромной выборке можно получить p = 0.001 для разницы в 0.01%.
Bias — ошибка модели из-за слишком простых предположений (линейная регрессия для нелинейных данных), variance — разброс предсказаний при разных обучающих выборках. Пример: полином степени 1 (прямая) = high bias, low variance; полином степени 20 = low bias, high variance (идеально на трейне, шумит на тесте). Оптимум — модель достаточной сложности, где сумма bias² + variance минимальна.
Центральная предельная теорема: среднее N независимых случайных величин (с конечной дисперсией) стремится к нормальному распределению при N → ∞, независимо от исходного распределения. Рост, IQ, ошибки измерений — всё это суммы множества мелких независимых факторов. Поэтому нормальное распределение — естественное следствие аддитивности, а не магическое свойство природы.
Частотная: параметр фиксирован, данные случайны. Вероятность — предел частоты при бесконечных повторениях. Ответ: confidence interval, p-value. Байесовская: параметр — случайная величина с prior-распределением, обновляемым данными (posterior ∝ likelihood × prior). Ответ: posterior distribution. Байесовский подход естественнее при малых данных и позволяет обновлять знания инкрементально.
Бэггинг (Random Forest) обучает независимые модели на bootstrap-выборках и усредняет — снижает variance. Бустинг (XGBoost, LightGBM) обучает модели последовательно, каждая исправляет ошибки предыдущей — снижает bias. Бэггинг устойчив к переобучению (больше деревьев ≠ хуже), бустинг мощнее, но требует тюнинга (learning rate, num_iterations) и может переобучиться.
Признак: метрика на train значительно лучше, чем на validation (gap растёт с эпохами). Методы борьбы: regularization (L1/L2, dropout), early stopping (остановка при росте val loss), больше данных (аугментация), уменьшение сложности модели (меньше параметров, деревьев, глубины), cross-validation для оценки. Ключевое правило: всегда мониторить val metric, а не train.
L1 (Lasso) добавляет |w| к loss, L2 (Ridge) — w². L1 зануляет веса неважных фич (sparse решение = feature selection), L2 уменьшает все веса равномерно, но не зануляет. L1 лучше, когда много нерелевантных фич и нужна интерпретируемость (какие фичи вошли в модель). L2 лучше, когда все фичи потенциально полезны и нужна стабильность (коррелирующие фичи получают схожие веса).
Один split зависит от случайного разбиения — метрика может сильно варьироваться. K-fold: данные делятся на K частей, каждая по очереди становится валидационной. Получаем K оценок метрики → среднее (более стабильная оценка) и std (понимаем разброс). Стандарт: K=5. Для маленьких данных — Leave-One-Out (K=N). Stratified K-fold сохраняет пропорции классов в каждом fold.
Precision = TP/(TP+FP) — «из тех, кого нашли, сколько правильных». Recall = TP/(TP+FN) — «из тех, кого нужно было найти, сколько нашли». Precision важнее, когда цена FP высока: спам-фильтр (не потерять важное письмо). Recall важнее, когда цена FN высока: диагностика рака (не пропустить больного). F1 = гармоническое среднее, используют при балансе важности.
ROC-AUC инвариантен к дисбалансу классов в определённом смысле, но при сильном дисбалансе (1% позитивов) может быть обманчиво высоким — модель «предсказывающая всё как 0» получит AUC ~0.5, но Precision будет 0. PR-AUC фокусируется на позитивном классе и чувствительна к дисбалансу. Правило: если позитивный класс < 5% — используйте PR-AUC. ROC-AUC — для сбалансированных датасетов.
Sigmoid насыщается при больших |x| (градиент → 0) — vanishing gradients. ReLU: f(x) = max(0, x) — градиент = 1 для x > 0, нет насыщения. Также ReLU вычислительно проще (нет exp). Проблема ReLU: «мёртвые нейроны» (если x < 0 всегда, градиент = 0 навсегда). Решения: Leaky ReLU (маленький наклон для x < 0), GELU (используется в трансформерах), SiLU/Swish.
BatchNorm нормализует выход слоя (среднее → 0, дисперсия → 1) по мини-батчу, затем масштабирует обучаемыми параметрами γ и β. Стабилизирует обучение, позволяет использовать больший learning rate, действует как лёгкая регуляризация. Исторически ставили до активации (оригинальная статья), но эмпирически после активации часто работает не хуже. На инференсе использует running statistics вместо batch.
Dropout случайно зануляет активации нейронов с вероятностью p во время обучения. Это заставляет сеть не полагаться на отдельные нейроны — эффект ансамблирования. На inference dropout отключают, потому что нужен детерминированный результат. Чтобы скомпенсировать то, что на inference работают все нейроны, веса масштабируются на (1-p) — или, чаще, используется inverted dropout (масштабирование на train).
Adam хранит адаптивный learning rate для каждого параметра (1-й и 2-й моменты градиента) — быстрее сходится, менее чувствителен к выбору LR. SGD с momentum — проще, но требует тщательного тюнинга LR и schedule. На практике: Adam — для быстрого прототипирования и NLP/трансформеров. SGD — для computer vision (ResNet на ImageNet), где при правильном schedule даёт чуть лучшую генерализацию.
Forward pass: данные проходят через слои, вычисляется loss. Backward pass: по chain rule вычисляем градиент loss по каждому весу, двигаясь от выхода к входу. Каждый слой знает свою локальную производную, а общий градиент — произведение локальных (цепное правило). Веса обновляются: w = w − lr × gradient. Computation graph хранит промежуточные значения для вычисления градиентов.