commit c59197adaf82a946cf39bcb97afd2da02e909be7 Author: Илья Братских Date: Sat Jul 25 21:24:03 2026 +0300 initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e1f4931 --- /dev/null +++ b/.gitignore @@ -0,0 +1,20 @@ +.venv/ +__pycache__/ +*.py[cod] +.pytest_cache/ +.ruff_cache/ +.ipynb_checkpoints/ +.idea/ +.vscode/ +data/* +!data/.gitkeep +artifacts/* +!artifacts/.gitkeep +models/ +runs/ +mlruns/ +*.onnx +*.pt +*.pth +*.safetensors +.env diff --git a/DATASETS.md b/DATASETS.md new file mode 100644 index 0000000..1c2343c --- /dev/null +++ b/DATASETS.md @@ -0,0 +1,76 @@ +# Данные для курса + +Датасеты не хранятся в Git: они большие, имеют отдельные лицензии и иногда требуют +согласия с условиями использования. Для каждого задания используется один из трёх +режимов. + +## 1. Генерируем сами + +Это позволяет знать правильный ответ и проверять реализацию: + +- NumPy, линейная алгебра и оптимизация — случайные матрицы и заданные функции; +- классификация/кластеризация — `sklearn.datasets.make_classification`, + `make_blobs`, `make_moons`, `make_circles`; +- вероятность — симуляции Монте-Карло; +- звук — синусоиды, шум, импульсы и click track с известным BPM; +- нейросети — XOR и небольшие синтетические выборки. + +Синтетические данные нужны для проверки математики, но не заменяют итоговую +проверку на реальных данных. + +## 2. Загружаем готовые открытые наборы + +Загрузка выполняется при начале модуля, обычно через sklearn, torchvision, +Hugging Face Datasets или официальный архив. + +- Табличный ML: Breast Cancer Wisconsin, Wine, California Housing либо OpenML. +- PCA/кластеры: sklearn Digits и синтетические геометрии. +- Computer Vision: Fashion-MNIST, CIFAR-10 и Oxford-IIIT Pet. Последний подходит + для эксперимента с кошками/собаками и transfer learning. +- Рукописные символы: MNIST и EMNIST. +- Речь: собственные записи, LibriSpeech/Multilingual LibriSpeech или Common Voice + после проверки актуальной лицензии и условий. +- NLP: Tiny Shakespeare для учебной LM; AG News/IMDb для классификации. +- LLM fine-tuning: небольшой самостоятельно проверенный instruction-набор из + источников с совместимой лицензией. + +Перед загрузкой проверяйте официальный источник, лицензию и объём. В отчёте +фиксируйте версию/дату, способ получения и checksum либо идентификатор dataset. + +## 3. Выбираем самостоятельно + +Самостоятельный выбор нужен только там, где он является частью навыка: + +- EDA — выбрать интересный открытый табличный набор; +- transfer learning — можно заменить Oxford-IIIT Pet своим небольшим набором; +- транскрибация — записать собственную речь или выбрать разрешённый корпус; +- osu! — использовать собственную либо Creative Commons музыку; +- capstone — подобрать данные под самостоятельно сформулированную задачу. + +Это не означает «найти что-нибудь и надеяться». Перед работой нужно согласовать: +источник, лицензию, target, объём, качество и схему разбиения. Если выбор вызывает +сомнение, сначала пришлите ссылку или описание — я помогу проверить пригодность. + +## Правила хранения + +Рекомендуемая локальная структура: + +```text +data/ + raw/ # неизменённые исходники + interim/ # промежуточные преобразования + processed/ # готовые признаки и splits + external/ # сторонние наборы +``` + +В Git сохраняются только скрипты загрузки, метаданные, маленькие тестовые fixtures +и checksums. Персональные данные, голоса других людей, коммерческая музыка, веса +моделей и токены доступа не коммитятся. + +## Разбиение данных + +- Разделяйте данные до подбора модели. +- Изображения одного объекта, фрагменты одной записи и куски одной песни должны + попадать только в один split. +- Test-set открывается для финальной оценки, а не для ежедневного выбора параметров. +- При временной задаче используйте хронологическое, а не случайное разбиение. diff --git a/GLOSSARY.md b/GLOSSARY.md new file mode 100644 index 0000000..6533156 --- /dev/null +++ b/GLOSSARY.md @@ -0,0 +1,276 @@ +# Глоссарий + +Сквозной навигационный словарь курса. Здесь даны краткие определения общих +терминов; подробности и предметную лексику ищите в глоссариях модулей в конце +документа. Полный маршрут по конспектам находится в +[`THEORY_INDEX.md`](THEORY_INDEX.md). + +## А + +**Алгоритм** — конечная однозначная последовательность действий, преобразующая +входные данные в результат. + +**API (программный интерфейс)** — явно описанный способ взаимодействия +компонентов: операции, форматы запросов и ответов, ошибки и правила версий. + +**Артефакт** — версионируемый результат ML-процесса: модель, датасет, метрики, +конфигурация, отчёт или образ контейнера. + +**Асимптотическая сложность** — скорость роста времени или памяти алгоритма при +увеличении размера входа; обычно выражается через \(O\), \(\Omega\) и +\(\Theta\). + +## Б + +**Батч (пакет)** — группа объектов, обрабатываемых одной векторизованной +операцией. Мини-батч — подмножество обучающей выборки для одного шага +оптимизации. + +**Байесовская вероятность** — подход, в котором неизвестные параметры +описываются распределениями, обновляемыми с учётом наблюдений. + +## В + +**Вектор** — упорядоченный набор чисел; в курсе считается вектором-столбцом +\(\mathbf{x}\in\mathbb{R}^d\), если не сказано иное. + +**Векторизация** — выражение вычисления через операции над массивами вместо +явных циклов высокого уровня. + +**Вероятность** — численная мера возможности события, принимающая значения от +\(0\) до \(1\). + +**Взаимная информация** — уменьшение неопределённости одной случайной +переменной после знания другой: +\(I(X;Y)=H(Y)-H(Y\mid X)\). + +**Выпуклая функция** — функция, график которой лежит не выше хорды: +\(f(\lambda x+(1-\lambda)y)\leq\lambda f(x)+(1-\lambda)f(y)\). + +## Г + +**Градиент** — вектор частных производных скалярной функции. Указывает +направление максимального локального роста в евклидовой геометрии. + +**Гессиан** — матрица вторых частных производных скалярной функции, +характеризующая локальную кривизну. + +**Гиперпараметр** — настройка процесса обучения или архитектуры, не +оптимизируемая непосредственно как параметр модели на обучающей выборке. + +## Д + +**Датасет (набор данных)** — коллекция наблюдений и, в обучении с учителем, +соответствующих целевых значений. + +**Data drift** — изменение распределения входных данных относительно +референсного периода; само по себе не доказывает ухудшение качества модели. + +**Дифференциал** — линейная главная часть изменения функции при малом изменении +аргумента. + +**Дисперсия** — ожидаемый квадрат отклонения случайной величины от её среднего: +\(\operatorname{Var}(X)=\mathbb{E}[(X-\mathbb{E}X)^2]\). + +**Дивергенция Кульбака—Лейблера** — несимметричная мера различия распределений +\(D_{\mathrm{KL}}(p\|q)=\mathbb{E}_p[\log(p/q)]\). + +**Дрейф концепции (concept drift)** — изменение зависимости между входом и +целевой переменной, из-за которого прежняя модель может стать неверной. + +## И + +**Инференс** — применение обученной модели для получения предсказаний. + +**Итерация** — один повтор шага алгоритма; в оптимизации часто одно обновление +параметров. + +## К + +**Ковариация** — мера совместного линейного изменения двух случайных величин. + +**Кросс-энтропия** — ожидаемая отрицательная логарифмическая вероятность, +назначенная моделью \(q\) данным из \(p\): +\(H(p,q)=-\mathbb{E}_p\log q\). + +**Контейнер** — изолированная переносимая упаковка приложения с его runtime и +зависимостями; образ контейнера является неизменяемым шаблоном запуска. + +**Контракт данных или сервиса** — проверяемое соглашение о схеме, типах, +семантике, допустимых значениях и поведении интерфейса. + +**Кэш** — быстрая память или сохранённый промежуточный результат, уменьшающий +стоимость повторного доступа или вычисления. + +## Л + +**Логит** — ненормированная числовая оценка класса до применения sigmoid или +softmax. + +**Лог-правдоподобие** — логарифм вероятности наблюдённых данных как функции +параметров модели. + +**Latency (задержка)** — время от запроса до ответа; обычно контролируют не +только среднее, но и высокие перцентили, например p95 и p99. + +## М + +**Матрица признаков** — таблица +\(\mathbf{X}\in\mathbb{R}^{n\times d}\), где строки соответствуют объектам, +а столбцы — признакам. + +**Машинная точность** — минимальная относительная дистанция около единицы, +различимая данным форматом плавающей точки. + +**Модель** — параметризованное правило, преобразующее вход в предсказание или +задающее распределение данных. + +## Н + +**Наблюдаемость (observability)** — возможность понять состояние системы по +метрикам, логам и трассировкам, включая качество данных и предсказаний. + +**Норма** — функция размера вектора или матрицы, удовлетворяющая +положительности, однородности и неравенству треугольника. + +**Нормализация** — преобразование масштаба данных по заданному правилу. +Стандартизация — частный случай с вычитанием среднего и делением на стандартное +отклонение. + +## О + +**Обобщающая способность** — качество модели на новых данных из целевого +распределения, а не только на обучающей выборке. + +**Обусловленность** — чувствительность точного решения математической задачи +к малым возмущениям входа. + +**Обучение с учителем** — обучение отображения по парам входов и известных +целевых значений. + +**Оптимизатор** — алгоритм обновления параметров с целью уменьшить функцию +потерь. + +## П + +**Параметр модели** — величина, настраиваемая по данным в ходе обучения. + +**Переобучение** — подгонка к особенностям обучающей выборки, ухудшающая +качество на новых данных. + +**Плотность вероятности** — функция для непрерывной случайной величины, +интеграл которой по области равен вероятности попадания в эту область. + +**Правило цепочки** — правило производной композиции функций; математическая +основа обратного распространения ошибки. + +**Признак** — измеримая или вычисленная характеристика объекта, подаваемая +модели на вход. + +**Происхождение данных (lineage)** — прослеживаемая цепочка от исходных данных, +кода и конфигурации до обученной модели и её метрик. + +## Р + +**Ранг матрицы** — размерность пространства её линейно независимых строк или +столбцов. + +**Реестр моделей (model registry)** — система учёта версий моделей, метаданных, +стадий проверки и разрешений на продвижение в окружения. + +**Регуляризация** — ограничение или дополнительный штраф, уменьшающий +эффективную сложность модели. + +**Релиз** — управляемая публикация версии системы с проверками, журналом +изменений, стратегией развёртывания и планом отката. + +**Rollback (откат)** — возврат к предыдущей работоспособной версии сервиса, +модели или конфигурации после неудачного релиза. + +## С + +**Самоинформация** — неожиданность события вероятности \(p\), равная +\(-\log p\). + +**Скаляр** — одно число. + +**Случайная величина** — измеримая числовая функция исхода случайного +эксперимента; прописная буква обозначает величину, строчная — её реализацию. + +**Смещение (bias)** — свободный член модели; также систематическая составляющая +ошибки оценки. Конкретный смысл должен следовать из контекста. + +**SLA / SLO** — SLA фиксирует внешнее обязательство по уровню сервиса, а SLO — +внутреннюю измеримую цель, например доступность или p95 latency. + +**Softmax** — преобразование логитов в категориальное распределение: +\(p_i=e^{z_i}/\sum_j e^{z_j}\). + +**Стохастический градиентный спуск** — обновление параметров по оценке +градиента, вычисленной на одном объекте или мини-батче. + +**CI/CD/CT** — автоматизация интеграционных проверок, доставки изменений и, +для CT, переобучения модели по контролируемому процессу. + +## Т + +**Тензор** — многомерный массив; скаляр, вектор и матрица являются случаями +тензора порядков \(0\), \(1\) и \(2\). + +## Ф + +**Функция потерь** — численная мера ошибки предсказания для объекта или +набора объектов, минимизируемая при обучении. + +## Ц + +**Целевая переменная** — величина, которую модель должна предсказывать. + +## Ч + +**Численная устойчивость** — свойство алгоритма не усиливать чрезмерно ошибки +округления и представления данных. + +**Число обусловленности** — количественная оценка чувствительности задачи; +большое значение означает возможное сильное усиление входной ошибки. + +## Э + +**Эмпирический риск** — средняя потеря модели на наблюдаемой выборке. + +**Энтропия** — ожидаемая самоинформация распределения: +\(H(X)=-\mathbb{E}\log p(X)\). + +**Эпоха** — условно один полный проход алгоритма обучения по обучающему набору. + +## Я + +**Якобиан** — матрица первых частных производных векторнозначной функции. + +## Связанные документы + +- Единые обозначения: [`common/NOTATION.md`](common/NOTATION.md) +- Все теоретические главы: [`THEORY_INDEX.md`](THEORY_INDEX.md) + +## Глоссарии модулей + +Расположение исторически различается, поэтому используйте эти прямые ссылки: + +- [00. Среда и Git](modules/00_environment/notes/glossary.md) +- [01. Python и NumPy](modules/01_python_numpy/notes/glossary.md) +- [02. Линейная алгебра](modules/02_math_linear_algebra/notes/glossary.md) +- [03. Вероятность и статистика](modules/03_probability_statistics/notes/glossary.md) +- [04. Оптимизация](modules/04_optimization/notes/glossary.md) +- [05. Данные и визуализация](modules/05_data_and_visualization/notes/glossary.md) +- [06. Классический ML](modules/06_classical_ml/glossary.md) +- [07. Деревья и бустинг](modules/07_trees_and_boosting/glossary.md) +- [08. Unsupervised learning](modules/08_unsupervised_learning/glossary.md) +- [09. Нейросети](modules/09_neural_networks/glossary.md) +- [10. Computer Vision](modules/10_computer_vision/glossary.md) +- [11. Звук, речь и музыка](modules/11_audio_speech_music/glossary.md) +- [12. Рукописный текст](modules/12_generative_handwriting/glossary.md) +- [13. NLP и Transformers](modules/13_nlp_transformers/notes/glossary.md) +- [14. Fine-tuning LLM](modules/14_llm_finetuning/notes/glossary.md) +- [15. ML Engineering](modules/15_ml_engineering/notes/glossary.md) +- [16. MLOps](modules/16_mlops_monitoring/notes/glossary.md) +- [17. Capstone](modules/17_capstone/notes/glossary.md) diff --git a/PROGRESS.md b/PROGRESS.md new file mode 100644 index 0000000..857eff3 --- /dev/null +++ b/PROGRESS.md @@ -0,0 +1,35 @@ +# Журнал прогресса + +Теория: `не начато` → `читаю` → `прочитано`. +Практика: `не начато` → `в работе` → `на проверке` → `принято`. + +| Модуль | Теория | Задания / review | Начало | Завершение | Главный вывод | +|---|---|---|---|---|---| +| 00. Среда и Git | не начато | не начато | | | | +| 01. Python и NumPy | не начато | не начато | | | | +| 02. Линейная алгебра | не начато | не начато | | | | +| 03. Вероятность и статистика | не начато | не начато | | | | +| 04. Оптимизация | не начато | не начато | | | | +| 05. Данные и визуализация | не начато | не начато | | | | +| 06. Классический ML | не начато | не начато | | | | +| 07. Деревья и бустинг | не начато | не начато | | | | +| 08. Unsupervised learning | не начато | не начато | | | | +| 09. Нейросети | не начато | не начато | | | | +| 10. Computer Vision | не начато | не начато | | | | +| 11. Звук, речь и музыка | не начато | не начато | | | | +| 12. Рукописный текст | не начато | не начато | | | | +| 13. NLP и Transformers | не начато | не начато | | | | +| 14. Fine-tuning LLM | не начато | не начато | | | | +| 15. ML Engineering | не начато | не начато | | | | +| 16. MLOps | не начато | не начато | | | | +| 17. Capstone | не начато | не начато | | | | + +## Шаблон записи после задания + +- Задание: +- Связанные конспекты прочитаны: +- Что реализовано: +- Что не получилось: +- Метрики и результат: +- Какие решения приняты и почему: +- Вопросы к проверке: diff --git a/README.md b/README.md new file mode 100644 index 0000000..72a9a9c --- /dev/null +++ b/README.md @@ -0,0 +1,97 @@ +# AI Learning — полный цикл машинного обучения + +Практическая программа на **43 недели по 8–10 часов в неделю**: от математики и +классических алгоритмов до нейросетей, компьютерного зрения, обработки звука, +fine-tuning LLM, API, Docker и мониторинга. + +Программа построена не вокруг запоминания библиотек. Для каждого важного метода +нужно ответить на четыре вопроса: + +1. Какую задачу и допущения он имеет? +2. Как устроены loss, обучение и регуляризация? +3. По каким метрикам и схеме валидации его сравнивать? +4. Как довести результат до воспроизводимого сервиса? + +## Ожидаемый результат + +После курса вы сможете: + +- исследовать, очищать и визуализировать данные; +- выбирать между линейными моделями, деревьями, бустингом и нейросетями; +- реализовать GD/SGD, логистическую регрессию, дерево, MLP и attention с нуля; +- понимать backpropagation, функции активации, L1/L2 и оптимизаторы; +- обучать CNN, применять transfer learning и fine-tuning; +- транскрибировать речь и извлекать ритмическую структуру музыки; +- создавать по аудио проверяемый черновик карты для osu!; +- собрать генератор изображения рукописного текста; +- обучить маленькую LM и адаптировать open-weight LLM через LoRA; +- упаковать модель в API/Docker и организовать мониторинг. + +## Roadmap + +| Недели | Модуль | Результат | +|---:|---|---| +| 1 | 00. Среда и Git | Воспроизводимый проект | +| 2–3 | 01. Python и NumPy | Векторные вычисления и тестируемый код | +| 4–5 | 02. Линейная алгебра | Геометрия моделей, SVD и PCA | +| 6–7 | 03. Вероятность и статистика | Неопределённость и эксперименты | +| 8–9 | 04. Оптимизация | GD/SGD, градиенты, L1/L2 | +| 10–11 | 05. Данные и визуализация | EDA и preprocessing без leakage | +| 12–14 | 06. Классический ML | Выбор моделей, метрик и validation | +| 15–16 | 07. Деревья и бустинг | Ансамбли и интерпретация | +| 17 | 08. Unsupervised learning | Кластеры, размерность, аномалии | +| 18–20 | 09. Нейросети | MLP, backprop и PyTorch | +| 21–23 | 10. Computer Vision | CNN и transfer learning | +| 24–26 | 11. Звук, речь и музыка | Транскрибация и черновик карты osu! | +| 27–29 | 12. Рукописный текст | Условная генерация изображений | +| 30–32 | 13. NLP и Transformers | Attention и маленькая LM | +| 33–35 | 14. Fine-tuning LLM | LoRA, evaluation и RAG | +| 36–38 | 15. ML Engineering | Пакет, API и Docker | +| 39–40 | 16. MLOps | Tracking, drift и retraining | +| 41–43 | 17. Capstone | Full-cycle проект в портфолио | + +## Как заниматься + +1. Откройте [`THEORY_INDEX.md`](THEORY_INDEX.md) и прочитайте офлайн конспекты + выбранного модуля по порядку. Незнакомые термины ищите в + [`GLOSSARY.md`](GLOSSARY.md). +2. Затем откройте `modules/<модуль>/README.md` и переходите к практике. +3. Выполняйте задания из `assignments/` в собственной папке `solution/`. +4. Не смотрите готовую реализацию до первой самостоятельной попытки. +5. Отдельно отметьте чтение теории и сдачу задания в + [`PROGRESS.md`](PROGRESS.md). +6. Напишите в чат: **«Проверь модуль XX, задание YY»**. Я проверю код, + корректность эксперимента, выводы и критерии приёмки. + +Данные не нужно искать вслепую: рекомендуемые наборы, способы загрузки и случаи, +когда данные генерируются самостоятельно, перечислены в `DATASETS.md`. + +## Режим недели + +- 2–3 часа: теория и самостоятельные выводы формул; +- 4–5 часов: код и эксперимент; +- 1 час: тесты, графики и анализ ошибок; +- 1 час: отчёт и исправления после review. + +## Важное уточнение о L1/L2/L3 + +L1 и L2 — нормы и типичные штрафы регуляризации. «L3» математически существует, +но не является стандартным третьим обязательным методом регуляризации в ML. +В курсе рассматриваются L1, L2, Elastic Net, dropout, early stopping и data +augmentation — с объяснением, когда каждый подход уместен. + +## Структура + +- [`THEORY_INDEX.md`](THEORY_INDEX.md) — маршрут по всем конспектам и их + предпосылкам; +- [`GLOSSARY.md`](GLOSSARY.md) — сквозные определения и ссылки на глоссарии + модулей; +- `modules/` — конспекты, цели и практические задания; +- `common/` — фундаментальные главы, обозначения, шаблоны и чек-листы; +- `requirements/` — группы зависимостей; +- `DATASETS.md` — откуда брать данные для каждого этапа; +- [`PROGRESS.md`](PROGRESS.md) — раздельный учёт теории и практики; +- `SETUP.md` — первый запуск на Windows. + +Начните с `SETUP.md`, затем читайте теорию модуля 00 через +[`THEORY_INDEX.md`](THEORY_INDEX.md) и выполняйте его практику. diff --git a/SETUP.md b/SETUP.md new file mode 100644 index 0000000..7061141 --- /dev/null +++ b/SETUP.md @@ -0,0 +1,41 @@ +# Настройка на Windows + +Требуется Python 3.11 или 3.12, Git и PowerShell. + +```powershell +cd D:\ai_learning +py -m venv .venv +.\.venv\Scripts\Activate.ps1 +python -m pip install --upgrade pip +pip install -r requirements\core.txt +``` + +Если PowerShell запрещает активацию, измените policy только для текущего процесса: + +```powershell +Set-ExecutionPolicy -Scope Process -ExecutionPolicy Bypass +.\.venv\Scripts\Activate.ps1 +``` + +Deep Learning, аудио и MLOps зависимости устанавливайте перед соответствующими +модулями: + +```powershell +pip install -r requirements\deep-learning.txt +pip install -r requirements\audio.txt +pip install -r requirements\mlops.txt +``` + +Для PyTorch/torchaudio с GPU используйте совместимые сборки из официального +configurator на pytorch.org, соответствующие вашей версии CUDA. Не смешивайте +случайные версии `torch`, `torchvision` и `torchaudio`. + +Проверка: + +```powershell +python -c "import numpy, pandas, sklearn; print('core ok')" +pytest +``` + +Большие датасеты и веса моделей не коммитьте. Храните их в `data/` и `artifacts/`; +эти каталоги исключены через `.gitignore`. diff --git a/THEORY_INDEX.md b/THEORY_INDEX.md new file mode 100644 index 0000000..87b9078 --- /dev/null +++ b/THEORY_INDEX.md @@ -0,0 +1,175 @@ +# Индекс теоретических материалов + +Здесь собраны все **59 конспектов модулей 00–17** и четыре общие +фундаментальные главы. Читайте конспекты офлайн по порядку, затем переходите к +заданиям соответствующего модуля. Обозначения сверяйте с +[`common/NOTATION.md`](common/NOTATION.md), термины — с +[`GLOSSARY.md`](GLOSSARY.md). + +## Общие фундаментальные главы + +Рекомендуемый порядок: + +1. [Дифференциальное и матричное исчисление](common/foundations/calculus_and_matrix_calculus.md) + Предпосылки: школьная алгебра, обозначения. +2. [Численная устойчивость](common/foundations/numerical_stability.md) + Предпосылки: базовая алгебра и представление чисел в компьютере. +3. [Сложность и вычисления](common/foundations/complexity_and_computation.md) + Предпосылки: базовый Python. +4. [Теория информации](common/foundations/information_theory.md) + Предпосылки: модуль 03 или базовая теория вероятностей. + +Шаблон новых конспектов: [`common/NOTE_TEMPLATE.md`](common/NOTE_TEMPLATE.md). +Он включает мотивацию, интуицию, определения, выводы, пример, связь с Python, +ошибки, резюме и связанные материалы — без вопросов для самопроверки. + +## Модули 00–17 + +### 00. Среда и Git + +Предпосылки: отсутствуют. + +1. [Python-окружения, зависимости и устройство проекта](modules/00_environment/notes/01_python_environments.md) +2. [Git: коммиты, ветки, diff и исключение больших данных](modules/00_environment/notes/02_git_reproducibility.md) +3. [Jupyter и модули: seed, конфигурация и логирование](modules/00_environment/notes/03_jupyter_seeds_logging.md) + +### 01. Python и NumPy + +Предпосылки: модуль 00; полезна глава о сложности. + +1. [Функции, классы, итераторы, comprehensions и исключения](modules/01_python_numpy/notes/01_python_data_model.md) +2. [Массивы NumPy: shape, axis, dtype, indexing и broadcasting](modules/01_python_numpy/notes/02_numpy_arrays.md) +3. [Векторизация, сложность, профилирование и модульные тесты](modules/01_python_numpy/notes/03_vectorization_testing.md) + +### 02. Линейная алгебра + +Предпосылки: модуль 01; обозначения и базовая алгебра. + +1. [Векторы, нормы, скалярное произведение, проекции и косинус](modules/02_math_linear_algebra/notes/01_vector_geometry.md) +2. [Матрицы, ранг, обратная матрица и системы уравнений](modules/02_math_linear_algebra/notes/02_matrices_systems_rank.md) +3. [Собственные значения, SVD, обусловленность и мост к PCA](modules/02_math_linear_algebra/notes/03_eigen_svd_conditioning.md) + +### 03. Вероятность и статистика + +Предпосылки: модули 01–02. + +1. [Вероятность, Байес, ожидание и распределения](modules/03_probability_statistics/notes/01_probability_bayes_distributions.md) +2. [Выборки, LLN, CLT, bootstrap и доверительные интервалы](modules/03_probability_statistics/notes/02_sampling_lln_clt_bootstrap_ci.md) +3. [Проверка гипотез, p-value, мощность и A/B-тесты](modules/03_probability_statistics/notes/03_hypothesis_testing_ab.md) + +### 04. Оптимизация + +Предпосылки: модули 02–03; глава об исчислении. + +1. [Производные, градиент и поверхности потерь](modules/04_optimization/notes/01_derivatives_and_loss_surfaces.md) +2. [GD, SGD, momentum, RMSProp и Adam](modules/04_optimization/notes/02_optimizers.md) +3. [L1, L2, Elastic Net, early stopping и bias–variance](modules/04_optimization/notes/03_regularization_bias_variance.md) + +### 05. Данные и визуализация + +Предпосылки: модули 01 и 03. + +1. [pandas и качество табличных данных](modules/05_data_and_visualization/notes/01_pandas_and_data_quality.md) +2. [Распределения, выбросы, зависимости и визуальная честность](modules/05_data_and_visualization/notes/02_distributions_outliers_visual_honesty.md) +3. [Feature engineering, pipelines и data leakage](modules/05_data_and_visualization/notes/03_feature_engineering_pipelines_leakage.md) + +### 06. Классический ML + +Предпосылки: модули 02–05; глава о теории информации. + +1. [Разбиения данных, кросс-валидация и baseline](modules/06_classical_ml/notes/01_validation_and_baselines.md) +2. [Линейные модели, kNN, Naive Bayes и SVM](modules/06_classical_ml/notes/02_models.md) +3. [Метрики, пороги, калибровка и дисбаланс](modules/06_classical_ml/notes/03_metrics_thresholds_and_calibration.md) + +### 07. Деревья и бустинг + +Предпосылки: модули 05–06. + +1. [Дерево решений: критерии разбиения и pruning](modules/07_trees_and_boosting/notes/01_split_criteria_and_pruning.md) +2. [Bagging, Random Forest и out-of-bag оценка](modules/07_trees_and_boosting/notes/02_bagging_random_forest_oob.md) +3. [Градиентный бустинг](modules/07_trees_and_boosting/notes/03_gradient_boosting.md) + +### 08. Unsupervised learning + +Предпосылки: модули 02–06. + +1. [Классификация и кластеризация](modules/08_unsupervised_learning/notes/01_clustering_vs_classification.md) +2. [k-means, иерархическая кластеризация и DBSCAN](modules/08_unsupervised_learning/notes/02_clustering_algorithms.md) +3. [PCA, t-SNE, UMAP и визуализация размерности](modules/08_unsupervised_learning/notes/03_dimensionality_reduction.md) +4. [Поиск аномалий без надёжной разметки](modules/08_unsupervised_learning/notes/04_anomaly_detection.md) + +### 09. Нейросети + +Предпосылки: модули 02–04 и 06; главы об исчислении и устойчивости. + +1. [Перцептрон, MLP, XOR и универсальная аппроксимация](modules/09_neural_networks/notes/01_perceptron_mlp_xor.md) +2. [Вычислительные графы, backpropagation и autodiff](modules/09_neural_networks/notes/02_backprop_autodiff.md) +3. [Активации, инициализация, нормализация и dropout](modules/09_neural_networks/notes/03_activations_initialization_regularization.md) + +### 10. Computer Vision + +Предпосылки: модуль 09; модули 05–06 для данных и оценки. + +1. [Свёртки, pooling, augmentation и CNN](modules/10_computer_vision/notes/01_convolutions_and_cnn.md) +2. [LeNet, AlexNet, VGG, ResNet и skip connections](modules/10_computer_vision/notes/02_classic_architectures_and_resnet.md) +3. [ImageNet pretraining, transfer learning и fine-tuning](modules/10_computer_vision/notes/03_pretraining_and_transfer.md) +4. [Выбор архитектуры под качество, данные, latency и память](modules/10_computer_vision/notes/04_architecture_selection.md) +5. [Основы detection и segmentation](modules/10_computer_vision/notes/05_detection_and_segmentation.md) + +### 11. Звук, речь и музыка + +Предпосылки: модули 02–03 и 09. + +1. [Дискретный звук: sampling, aliasing, channels и PCM](modules/11_audio_speech_music/notes/01_discrete_audio.md) +2. [Fourier transform, STFT, mel-спектрограммы и augmentation](modules/11_audio_speech_music/notes/02_spectral_analysis.md) +3. [Распознавание речи: рекуррентные сети, CTC, encoder-decoder и Whisper](modules/11_audio_speech_music/notes/03_speech_recognition.md) +4. [Музыкальные события: tempo, beat, onset и tempogram](modules/11_audio_speech_music/notes/04_music_events.md) +5. [Формат `.osu`, timing points, hit objects и сложность](modules/11_audio_speech_music/notes/05_osu_mapping.md) + +### 12. Рукописный текст + +Предпосылки: модули 09–10; глава о теории информации. + +1. [Латентные модели: autoencoder и VAE](modules/12_generative_handwriting/notes/01_autoencoders_vae.md) +2. [GAN и diffusion: цели, обучение и типичные сбои](modules/12_generative_handwriting/notes/02_gan_diffusion.md) +3. [Условная генерация глифов, композиция строк и читаемость](modules/12_generative_handwriting/notes/03_conditional_glyphs.md) + +### 13. NLP и Transformers + +Предпосылки: модули 03–04 и 09; глава о теории информации. + +1. [Токенизация, эмбеддинги и причинное языковое моделирование](modules/13_nlp_transformers/notes/01_tokenization_embeddings_language_modeling.md) +2. [Scaled dot-product attention, multi-head attention и Transformer-блок](modules/13_nlp_transformers/notes/02_attention_and_transformer_block.md) +3. [Encoder/decoder, BERT и GPT, perplexity и генерация](modules/13_nlp_transformers/notes/03_architectures_evaluation_generation.md) + +### 14. Fine-tuning LLM + +Предпосылки: модуль 13. + +1. [Pretraining, instruction tuning и подготовка данных](modules/14_llm_finetuning/notes/01_pretraining_instruction_data.md) +2. [Full fine-tuning, LoRA, QLoRA, quantization и забывание](modules/14_llm_finetuning/notes/02_full_finetuning_lora_qlora.md) +3. [Оценка LLM, галлюцинации, RAG, безопасность и лицензии](modules/14_llm_finetuning/notes/03_evaluation_rag_safety.md) + +### 15. ML Engineering + +Предпосылки: модули 00–06 и один прикладной модуль 10–14. + +1. [Production-проект: структура, конфигурация, контракты и тесты](modules/15_ml_engineering/notes/01_production_project_contracts_testing.md) +2. [Инференс: batch, online API, Docker, ONNX и эксплуатационные свойства](modules/15_ml_engineering/notes/02_inference_api_docker_onnx.md) +3. [Model registry, experiment tracking и CI](modules/15_ml_engineering/notes/03_registry_tracking_ci.md) + +### 16. MLOps + +Предпосылки: модуль 15 и опыт обучения хотя бы одной модели. + +1. [Experiment tracking, artifacts, lineage и версионирование](modules/16_mlops_monitoring/notes/01_artifacts_lineage_versioning.md) +2. [Data drift, concept drift, observability и delayed labels](modules/16_mlops_monitoring/notes/02_drift_observability_delayed_labels.md) +3. [CI/CD/CT, canary, shadow, rollback и governance](modules/16_mlops_monitoring/notes/03_cicd_ct_rollout_governance.md) + +### 17. Capstone + +Предпосылки: модули 00–16; допускается выбрать профильную ветку 10–14. + +1. [Постановка задачи, ограничения, baseline и план экспериментов](modules/17_capstone/notes/01_problem_baseline_experiment_plan.md) +2. [Воспроизводимое исследование, обучение и error analysis](modules/17_capstone/notes/02_reproducible_research_error_analysis.md) +3. [Сервис, документация, demo, model card и защита проекта](modules/17_capstone/notes/03_service_docs_demo_model_card_defense.md) diff --git a/artifacts/.gitkeep b/artifacts/.gitkeep new file mode 100644 index 0000000..d3f5a12 --- /dev/null +++ b/artifacts/.gitkeep @@ -0,0 +1 @@ + diff --git a/common/MODEL_CARD_TEMPLATE.md b/common/MODEL_CARD_TEMPLATE.md new file mode 100644 index 0000000..8af8dfc --- /dev/null +++ b/common/MODEL_CARD_TEMPLATE.md @@ -0,0 +1,119 @@ +# Model Card: `<название модели> <версия>` + +> Скопируйте файл в проект, заполните все разделы и удалите подсказки в +> угловых скобках. Если пункт неприменим, укажите причину. + +## Паспорт версии + +- **Model ID / версия:** `` +- **Статус / alias:** `` +- **Дата:** `<ГГГГ-ММ-ДД>` +- **Владельцы:** `<модель, данные, сервис, бизнес-решение>` +- **Код:** `` +- **Данные:** `` +- **Артефакт:** `` +- **Одобрение:** `<кто, когда, ссылка на решение>` + +## Назначение + +- **Пользователи:** `<кто применяет результат>` +- **Решение:** `<какое действие поддерживает предсказание>` +- **Unit of prediction:** `<объект одного предсказания>` +- **Режим:** `` +- **Допустимые сценарии:** `<список>` +- **Вне scope:** `<сценарии, которые не оценивались>` +- **Запрещённое применение:** `<опасные или несогласованные сценарии>` + +## Контракт входа и выхода + +- **Входная схема / версия:** `<ссылка или краткое описание>` +- **Выход:** `` +- **Preprocessing:** `<состав и версия>` +- **Threshold / policy:** `<значение, способ выбора, владелец>` +- **Неизвестные и пропущенные значения:** `<ожидаемое поведение>` +- **Ограничения:** `` + +## Данные + +- **Источники и период:** `<описание>` +- **Label и prediction horizon:** `<определение и версия>` +- **Feature cutoff:** `<момент доступности признаков>` +- **Train/validation/test split:** `<правило, размеры, даты>` +- **Лицензия, consent и retention:** `<условия>` +- **Известные пробелы и bias:** `<непредставленные группы/периоды>` +- **Исключённые поля:** `` + +## Модель и обучение + +- **Семейство и архитектура:** `<описание>` +- **Baseline:** `<что сравнивали и почему>` +- **Целевая функция:** `` +- **Ключевые параметры:** `<ссылка на resolved config>` +- **Среда:** `` +- **Training run:** `` +- **Стоимость:** `<время, CPU/GPU, приблизительная стоимость>` + +## Протокол оценки + +- **Primary и decision metrics:** `<определения>` +- **Guardrails:** `<метрики, которые нельзя ухудшать>` +- **Размеры выборок:** `<общий и по важным срезам>` +- **Uncertainty:** `` +- **Test policy:** `<когда и сколько раз использован test>` +- **Сравнение с baseline/champion:** `<ссылка на evidence>` + +### Результаты + +- **Общий результат:** `<метрика: candidate / baseline / champion>` +- **Критические срезы:** `<группа, n, метрика, uncertainty>` +- **Калибровка / threshold:** `<результат>` +- **Latency, throughput, RAM:** `<условия и p50/p95/p99>` +- **Не пройденные проверки:** `<что и почему>` + +## Ограничения и failure modes + +- `<условие вне обучающего распределения и ожидаемое поведение>` +- `<известная слабая группа или тип ошибки>` +- `` +- `<зависимости и единичные точки отказа>` + +Для каждого существенного ограничения укажите влияние, способ обнаружения, +fallback и владельца. + +## Риски + +### Fairness и вред + +`<релевантные группы, цена FP/FN, выполненные и отсутствующие проверки>` + +### Privacy и безопасность + +`` + +### Юридические и продуктовые ограничения + +`<лицензии, consent, регулирование, human review/appeal>` + +## Эксплуатация + +- **Telemetry:** `` +- **Drift:** `` +- **Качество:** `` +- **Алерты и runbooks:** `<ссылки, владельцы, severity>` +- **Retraining:** `<триггеры, cooldown, data/quality gates>` +- **Rollout:** `` +- **Rollback:** `<совместимая версия и проверенная процедура>` +- **Retirement:** `<срок review, критерии и удаление>` + +## Воспроизведение и evidence + +- **Quickstart:** `<ссылка>` +- **Evaluation report:** `<ссылка>` +- **Slice / error analysis:** `<ссылка>` +- **Load и contract tests:** `<ссылка>` +- **Release checklist:** `<ссылка>` +- **Decision log:** `<ссылка>` + +## История изменений + +- `<ГГГГ-ММ-ДД — версия — изменение — автор>` diff --git a/common/NOTATION.md b/common/NOTATION.md new file mode 100644 index 0000000..584c2ae --- /dev/null +++ b/common/NOTATION.md @@ -0,0 +1,207 @@ +# Соглашения об обозначениях + +Этот файл задаёт общую математическую запись для всех курсов. Локальные +исключения допустимы только с явным пояснением. + +## Общие правила + +- Скаляр: \(x,a,\lambda\). +- Вектор-столбец: \(\mathbf{x}\in\mathbb{R}^d\). +- Матрица: \(\mathbf{X}\in\mathbb{R}^{n\times d}\). +- Множество: \(\mathcal{D}\), пространство: \(\mathbb{R}^d\). +- Индексы начинаются с \(1\) в формулах и с \(0\) в Python. +- Верхний индекс в скобках, например \(\mathbf{x}^{(i)}\), обозначает номер + объекта, а не степень. + +## Данные и параметры + +Набор из \(n\) объектов с \(d\) признаками: + +\[ +\mathcal{D}=\{(\mathbf{x}^{(i)},y^{(i)})\}_{i=1}^{n},\qquad +\mathbf{X}= +\begin{bmatrix} +(\mathbf{x}^{(1)})^\top\\ +\cdots\\ +(\mathbf{x}^{(n)})^\top +\end{bmatrix} +\in\mathbb{R}^{n\times d}. +\] + +Строка матрицы — объект, столбец — признак. Целевой вектор +\(\mathbf{y}\in\mathbb{R}^n\). Параметры модели обозначаются +\(\boldsymbol{\theta}\); веса линейной модели — \(\mathbf{w}\), смещение — +\(b\). Предсказание — \(\hat y\), истинное значение — \(y\). + +Разбиения данных: \(\mathcal{D}_{\mathrm{train}}\), +\(\mathcal{D}_{\mathrm{val}}\), \(\mathcal{D}_{\mathrm{test}}\). + +## Векторы и матрицы + +\[ +\mathbf{x}^\top\mathbf{y}=\sum_{j=1}^{d}x_jy_j,\qquad +\|\mathbf{x}\|_2=\sqrt{\sum_jx_j^2},\qquad +\|\mathbf{x}\|_1=\sum_j|x_j|. +\] + +- \(\mathbf{A}^\top\) — транспонирование. +- \(\mathbf{A}^{-1}\) — обратная матрица, только если она существует. +- \(\mathbf{I}_d\) — единичная матрица размера \(d\). +- \(\mathbf{0}\), \(\mathbf{1}\) — векторы или матрицы из нулей и единиц; + размер следует из контекста. +- \(\operatorname{diag}(\mathbf{x})\) — диагональная матрица. +- \(\operatorname{tr}(\mathbf{A})=\sum_i A_{ii}\) — след. +- \(\det(\mathbf{A})\) — определитель. +- \(\operatorname{rank}(\mathbf{A})\) — ранг. +- \(\mathbf{A}\odot\mathbf{B}\) — поэлементное произведение. +- \(\mathbf{A}\mathbf{B}\) — матричное произведение. + +Норма Фробениуса: + +\[ +\|\mathbf{A}\|_F= +\sqrt{\sum_{i,j}A_{ij}^2} +=\sqrt{\operatorname{tr}(\mathbf{A}^\top\mathbf{A})}. +\] + +Запись \(\mathbf{A}\succeq0\) означает симметричную положительно +полуопределённую матрицу: +\(\mathbf{x}^\top\mathbf{A}\mathbf{x}\geq0\) для любого \(\mathbf{x}\). + +## Функции и отображения + +\[ +f:\mathbb{R}^d\to\mathbb{R},\qquad +\mathbf{g}:\mathbb{R}^d\to\mathbb{R}^m. +\] + +Композиция записывается \(f\circ g\), то есть +\((f\circ g)(x)=f(g(x))\). Образ множества — \(f(\mathcal{X})\). +Индикатор события: + +\[ +\mathbb{1}\{A\}= +\begin{cases} +1,&A\text{ произошло},\\ +0,&\text{иначе}. +\end{cases} +\] + +## Дифференциальное исчисление + +Обычная производная — \(f'(x)=df/dx\), частная производная — +\(\partial f/\partial x_j\). Градиент скалярной функции — вектор-столбец: + +\[ +\nabla_{\mathbf{x}}f= +\begin{bmatrix} +\partial f/\partial x_1\\ +\vdots\\ +\partial f/\partial x_d +\end{bmatrix} +\in\mathbb{R}^{d}. +\] + +Гессиан: + +\[ +\nabla_{\mathbf{x}}^2 f= +\left[\frac{\partial^2 f}{\partial x_i\partial x_j}\right]_{i,j} +\in\mathbb{R}^{d\times d}. +\] + +Якобиан \(\mathbf{g}:\mathbb{R}^d\to\mathbb{R}^m\): + +\[ +\mathbf{J}_{\mathbf{g}}(\mathbf{x})= +\left[\frac{\partial g_i}{\partial x_j}\right]_{i,j} +\in\mathbb{R}^{m\times d}. +\] + +Дифференциал записывается \(df\). Для функции матрицы градиент определяется +соглашением + +\[ +df=\operatorname{tr}\left( +(\nabla_{\mathbf{X}}f)^\top d\mathbf{X} +\right). +\] + +## Вероятность + +Вероятностное пространство явно не выписывается, если оно не существенно. + +- Случайные величины: \(X,Y\); их реализации: \(x,y\). +- Случайный вектор: \(\mathbf{X}\); наблюдение: \(\mathbf{x}\). +- Вероятность события: \(\mathbb{P}(A)\). +- Условная вероятность: \(\mathbb{P}(A\mid B)\). +- Плотность или функция вероятности: \(p_X(x)\), кратко \(p(x)\). +- Совместное распределение: \(p(x,y)\). +- Условное распределение: \(p(y\mid x)\). +- Математическое ожидание: \(\mathbb{E}[X]\). +- Дисперсия: \(\operatorname{Var}(X)\). +- Ковариация: \(\operatorname{Cov}(X,Y)\). +- Ковариационная матрица: \(\boldsymbol{\Sigma}\). + +\[ +\mathbb{E}_{X\sim p}[f(X)] +=\int f(x)p(x)\,dx +\] + +для непрерывного \(X\); для дискретного интеграл заменяется суммой. Запись +\(X\sim\mathcal{N}(\mu,\sigma^2)\) означает нормальное распределение. +Независимость обозначается \(X\perp Y\), условная независимость — +\(X\perp Y\mid Z\). + +## Оценивание и оптимизация + +Истинный неизвестный параметр — \(\theta\), его оценка по данным — +\(\hat\theta\). Функция потерь одного объекта — \(\ell\), эмпирический риск: + +\[ +\hat R(\boldsymbol{\theta}) +=\frac1n\sum_{i=1}^{n} +\ell\left(f_{\boldsymbol{\theta}}(\mathbf{x}^{(i)}),y^{(i)}\right). +\] + +\[ +\boldsymbol{\theta}^\star +\in\arg\min_{\boldsymbol{\theta}\in\Theta} +L(\boldsymbol{\theta}). +\] + +\(\min L\) — минимальное значение, а \(\arg\min L\) — множество аргументов, +на которых оно достигается. Итерация алгоритма имеет индекс \(t\): + +\[ +\boldsymbol{\theta}_{t+1} +=\boldsymbol{\theta}_t-\eta_t\nabla L(\boldsymbol{\theta}_t), +\] + +где \(\eta_t>0\) — шаг обучения. + +## Суммы, пределы и асимптотика + +\[ +\sum_{i=1}^{n}a_i,\qquad +\prod_{i=1}^{n}a_i,\qquad +\lim_{n\to\infty}a_n. +\] + +\(f(n)=O(g(n))\) задаёт асимптотическую верхнюю границу с точностью до +константы; \(\Theta(g(n))\) — двустороннюю. Эти обозначения не описывают +реальное время для конкретного малого \(n\). + +## Числа и вычисления + +- \(\varepsilon\) — малая математическая величина или допуск; машинная точность + обозначается \(\varepsilon_{\mathrm{mach}}\). +- \(\operatorname{softmax}(\mathbf{z})_i + =e^{z_i}/\sum_j e^{z_j}\). +- \(\log\) — натуральный логарифм, если основание не указано. +- \(a\approx b\) — приближённое равенство; \(a\propto b\) — + пропорциональность. + +В тексте нельзя смешивать математический вектор \(\mathbf{x}\in\mathbb{R}^d\) +с массивом NumPy формы `(1, d)`: первый обычно соответствует форме `(d,)`. +Формы `(d,)`, `(d, 1)` и `(1, d)` в коде различны. diff --git a/common/NOTE_TEMPLATE.md b/common/NOTE_TEMPLATE.md new file mode 100644 index 0000000..031d8c2 --- /dev/null +++ b/common/NOTE_TEMPLATE.md @@ -0,0 +1,130 @@ +# Название темы + +> Краткая формулировка: что изучаем и какую задачу это помогает решать. + +## Мотивация + +Опишите практическую проблему до введения формул: + +- где она возникает в анализе данных или машинном обучении; +- почему наивного решения недостаточно; +- какой результат читатель сможет получить после главы. + +Хорошая мотивация связывает тему с уже знакомой идеей и называет цену ошибки. +Например, неверное понимание масштаба градиента приводит не только к медленному +обучению, но и к численной нестабильности. + +## Интуиция + +Сначала дайте мысленную модель без потери математического смысла. Укажите: + +1. какие объекты участвуют; +2. что меняется, а что фиксировано; +3. какую величину мы хотим измерить, предсказать или оптимизировать; +4. где аналогия перестаёт работать. + +При необходимости добавьте маленький числовой пример. Интуиция должна готовить +к определению, а не заменять его. + +## Определения и обозначения + +Введите каждый новый термин до первого использования. + +- **Термин** — точное определение. +- Скаляр записывается как \(x\), вектор — как \(\mathbf{x}\), матрица — как + \(\mathbf{X}\). +- Размерности указываются явно: + \(\mathbf{X}\in\mathbb{R}^{n\times d}\), + \(\mathbf{w}\in\mathbb{R}^{d}\). +- Все специальные предположения перечисляются рядом с формулой. + +Согласуйте запись с [`NOTATION.md`](NOTATION.md). Если в теме нужна локальная +оговорка, объясните её явно. + +## Вывод ключевых формул + +Не переходите от исходной формулы к ответу одним скачком. Для каждого шага +укажите применённое свойство: + +\[ +\begin{aligned} +L(\mathbf{w}) + &= \frac{1}{2}\|\mathbf{X}\mathbf{w}-\mathbf{y}\|_2^2 \\ + &= \frac{1}{2} + (\mathbf{X}\mathbf{w}-\mathbf{y})^\top + (\mathbf{X}\mathbf{w}-\mathbf{y}) + && \text{по определению нормы},\\ +\nabla_{\mathbf{w}}L + &= \mathbf{X}^\top(\mathbf{X}\mathbf{w}-\mathbf{y}) + && \text{по правилу производной квадратичной формы}. +\end{aligned} +\] + +После вывода проверьте: + +- совпадают ли размерности обеих частей; +- разумно ли поведение в простом предельном случае; +- какие предположения нужны для результата; +- является ли формула точной или приближённой. + +## Разобранный пример + +Сформулируйте небольшую задачу с конкретными числами. + +**Дано.** Перечислите входные величины и их размерности. + +**Решение.** + +1. Выберите подходящую формулу и объясните выбор. +2. Подставьте значения без пропуска промежуточных вычислений. +3. Интерпретируйте число в терминах исходной задачи. + +**Проверка.** Сравните результат с грубой оценкой, альтернативным вычислением +или известным частным случаем. + +## Связь с Python и NumPy + +Код должен воспроизводить разобранный пример и запускаться как самостоятельный +фрагмент: + +```python +import numpy as np + +X = np.array([[1.0, 2.0], [3.0, 4.0]]) +w = np.array([0.5, -0.25]) +y = np.array([0.0, 0.5]) + +residual = X @ w - y +loss = 0.5 * residual @ residual +gradient = X.T @ residual + +print(f"loss = {loss:.6f}") +print("gradient =", gradient) +``` + +Поясните соответствие между математикой и кодом: `@` — матричное умножение, +`.T` — транспонирование, а не поэлементная операция. Укажите формы массивов и +не полагайтесь на случайный broadcasting. + +## Типичные ошибки и ограничения + +- **Ошибка:** распространённое неверное действие. + **Почему неверно:** нарушенное условие или свойство. + **Как исправить:** безопасная проверка или корректная формула. +- Отделите математические ограничения метода от особенностей реализации. +- Назовите случаи, в которых приближение перестаёт быть надёжным. +- Предупредите о переполнении, потере точности и неоднозначных формах массивов, + если они возможны. + +## Краткое резюме + +- Одна строка о главной идее. +- Одна строка о ключевой формуле и её условиях. +- Одна строка о практической интерпретации. +- Одна строка о наиболее опасной ошибке. + +## Связанные материалы + +- Базовая глава: относительная ссылка внутри репозитория. +- Следующая тема: относительная ссылка внутри репозитория. +- Термины: [`GLOSSARY.md`](../GLOSSARY.md). diff --git a/common/RELEASE_CHECKLIST.md b/common/RELEASE_CHECKLIST.md new file mode 100644 index 0000000..6c35314 --- /dev/null +++ b/common/RELEASE_CHECKLIST.md @@ -0,0 +1,99 @@ +# Checklist релиза ML-системы + +> Скопируйте checklist в релизный документ. Для каждого обязательного пункта +> укажите ссылку на evidence и владельца. `[ ]` означает, что релиз заблокирован; +> неприменимый пункт пометьте `N/A` и объясните почему. + +## Паспорт релиза + +- **Релиз / дата:** `` +- **Владелец:** `<имя/команда>` +- **Model artifact:** `` +- **Image:** `` +- **Config / schema:** `` +- **Среда:** `` +- **Risk level:** `` +- **Change summary:** `<что и зачем меняется>` + +## Код и воспроизводимость + +- [ ] Commit/tag и рабочая копия зафиксированы. Evidence: `<...>` Owner: `<...>` +- [ ] Lock-файл и build image воспроизводимы. Evidence: `<...>` Owner: `<...>` +- [ ] Training/evaluation запускаются по инструкции. Evidence: `<...>` Owner: `<...>` +- [ ] Unit, integration, contract и smoke tests пройдены. Evidence: `<...>` Owner: `<...>` +- [ ] Секреты отсутствуют в коде, образе, логах и artifacts. Evidence: `<...>` Owner: `<...>` + +## Данные и lineage + +- [ ] Snapshot, схема, период и checksums зафиксированы. Evidence: `<...>` Owner: `<...>` +- [ ] Data quality, freshness и объём прошли gates. Evidence: `<...>` Owner: `<...>` +- [ ] Feature cutoff и split исключают известную leakage. Evidence: `<...>` Owner: `<...>` +- [ ] Label definition/version и maturity документированы. Evidence: `<...>` Owner: `<...>` +- [ ] Lineage связывает данные, код, run и artifact. Evidence: `<...>` Owner: `<...>` +- [ ] Лицензия, consent, доступ и retention проверены. Evidence: `<...>` Owner: `<...>` + +## Модель и качество + +- [ ] Оценён именно сериализованный release artifact. Evidence: `<...>` Owner: `<...>` +- [ ] Candidate сравнен с baseline/champion по фиксированному протоколу. Evidence: `<...>` Owner: `<...>` +- [ ] Primary metric и uncertainty проходят порог. Evidence: `<...>` Owner: `<...>` +- [ ] Критические срезы и guardrails проходят пороги. Evidence: `<...>` Owner: `<...>` +- [ ] Калибровка, threshold и decision policy проверены. Evidence: `<...>` Owner: `<...>` +- [ ] Test set использован согласно заранее заданной policy. Evidence: `<...>` Owner: `<...>` +- [ ] Известные ограничения и отрицательные результаты записаны. Evidence: `<...>` Owner: `<...>` +- [ ] [Model card](MODEL_CARD_TEMPLATE.md) заполнен и одобрен. Evidence: `<...>` Owner: `<...>` + +## Artifact и интерфейс + +- [ ] Checksum/signature artifact проверены перед загрузкой. Evidence: `<...>` Owner: `<...>` +- [ ] Model bundle включает совместимые preprocessing и metadata. Evidence: `<...>` Owner: `<...>` +- [ ] Входная/выходная схема версионирована и совместима. Evidence: `<...>` Owner: `<...>` +- [ ] Ошибочные, граничные и out-of-domain входы проверены. Evidence: `<...>` Owner: `<...>` +- [ ] Ответ содержит model/service version и trace/request ID. Evidence: `<...>` Owner: `<...>` +- [ ] Liveness и readiness отражают разные состояния. Evidence: `<...>` Owner: `<...>` + +## Производительность и безопасность + +- [ ] Load-тест отражает payload, rate/concurrency и warm-up. Evidence: `<...>` Owner: `<...>` +- [ ] p50/p95/p99, throughput, errors и память укладываются в SLO. Evidence: `<...>` Owner: `<...>` +- [ ] Ограничены payload, batch, concurrency и timeouts. Evidence: `<...>` Owner: `<...>` +- [ ] Backpressure и поведение при перегрузке проверены. Evidence: `<...>` Owner: `<...>` +- [ ] Образ сканирован, запускается без root и с лимитами. Evidence: `<...>` Owner: `<...>` +- [ ] AuthN/AuthZ, PII masking и transport security проверены. Evidence: `<...>` Owner: `<...>` +- [ ] Зависимости, модель и образ получены из доверенных источников. Evidence: `<...>` Owner: `<...>` + +## Наблюдаемость и эксплуатация + +- [ ] Dashboard разделяет сервис, данные/predictions и качество. Evidence: `<...>` Owner: `<...>` +- [ ] Telemetry содержит версии и не раскрывает PII/секреты. Evidence: `<...>` Owner: `<...>` +- [ ] Drift reference, окна, minimum volume и пороги определены. Evidence: `<...>` Owner: `<...>` +- [ ] Delayed labels, mature cohorts и label coverage учтены. Evidence: `<...>` Owner: `<...>` +- [ ] Алерты имеют severity, владельца и проверенный runbook. Evidence: `<...>` Owner: `<...>` +- [ ] On-call и порядок эскалации подтверждены. Evidence: `<...>` Owner: `<...>` + +## Rollout и rollback + +- [ ] Release unit фиксирует model, preprocessing, contract, image и config. Evidence: `<...>` Owner: `<...>` +- [ ] Shadow не создаёт побочных эффектов. Evidence: `<...>` Owner: `<...>` +- [ ] Canary assignment, шаги и guardrails заданы. Evidence: `<...>` Owner: `<...>` +- [ ] Критерии остановки и promotion однозначны. Evidence: `<...>` Owner: `<...>` +- [ ] Предыдущая совместимая версия доступна по immutable ID. Evidence: `<...>` Owner: `<...>` +- [ ] Rollback отрепетирован; RTO и владелец известны. Evidence: `<...>` Owner: `<...>` +- [ ] Для высокого риска получены необходимые approvals. Evidence: `<...>` Owner: `<...>` + +## Документация и готовность + +- [ ] Quickstart проверен в чистом окружении. Evidence: `<...>` Owner: `<...>` +- [ ] Архитектура, контракты, SLO и runbooks актуальны. Evidence: `<...>` Owner: `<...>` +- [ ] Demo использует безопасные fixtures и имеет fallback. Evidence: `<...>` Owner: `<...>` +- [ ] Владельцы модели, данных, сервиса и решения назначены. Evidence: `<...>` Owner: `<...>` +- [ ] Дата следующего review и retirement criteria определены. Evidence: `<...>` Owner: `<...>` + +## Решение + +- **Статус:** `` +- **Открытые блокеры:** `<список или "нет">` +- **Принятый остаточный риск:** `<описание и кто принял>` +- **Одобрили:** `<имя, роль, дата>` +- **Окно rollout:** `<время и часовой пояс>` +- **Канал инцидента:** `<ссылка/контакт>` diff --git a/common/REPORT_TEMPLATE.md b/common/REPORT_TEMPLATE.md new file mode 100644 index 0000000..7626944 --- /dev/null +++ b/common/REPORT_TEMPLATE.md @@ -0,0 +1,40 @@ +# Отчёт по заданию + +## Постановка + +Что требуется предсказать/проверить и зачем. + +## Теоретическая опора (необязательно) + +Какие конспекты, определения или формулы использованы; укажите относительные +ссылки и коротко объясните связь с решением. + +## Данные + +Источник, лицензия, размер, признаки, target, разбиение и ограничения. + +## Метод + +Baseline, модель, loss, оптимизатор, гиперпараметры и причина выбора. + +## Эксперимент + +Seed, окружение, схема валидации и защита от leakage. + +## Результаты + +Метрики, графики, время и ресурсы. + +## Анализ ошибок + +Какие примеры не работают, почему и как это проверить. + +## Production-аспекты (необязательно) + +Применимое к заданию: контракт входа и выхода, API или batch-интерфейс, +контейнер, latency и ресурсы, версия артефакта и реестр, мониторинг drift, +стратегия релиза и отката. Для ранних учебных модулей раздел можно пропустить. + +## Вывод + +Что узнали, что улучшать и что нельзя заключить из эксперимента. diff --git a/common/REVIEW_CHECKLIST.md b/common/REVIEW_CHECKLIST.md new file mode 100644 index 0000000..795a29d --- /dev/null +++ b/common/REVIEW_CHECKLIST.md @@ -0,0 +1,31 @@ +# Чек-лист сдачи задания + +Перед запросом проверки убедитесь в базовых пунктах: + +- прочитаны связанные главы, а ключевая идея и ограничения метода объяснимы + своими словами; +- задача и выбранная метрика сформулированы; +- train/validation/test разделены до подбора параметров; +- нет утечки целевой переменной или test-данных; +- seed и версии ключевых библиотек указаны; +- код запускается из чистого окружения по инструкции; +- присутствует простой baseline; +- графики имеют подписи, единицы и легенду; +- исследованы не только средние метрики, но и ошибки; +- вывод отвечает на вопрос эксперимента; +- ограничения и неудачные попытки не скрыты. + +## По мере появления production-требований + +Эти пункты **не обязательны для каждого раннего модуля**. Отмечайте только +применимые к конкретному заданию: + +- [ ] входы, выходы, типы, диапазоны и ошибки закреплены контрактом; +- [ ] API или batch-интерфейс проверен на корректный и ошибочный запрос; +- [ ] контейнер собирается воспроизводимо и запускается без локальных путей; +- [ ] версия модели и метаданные артефакта зарегистрированы в model registry; +- [ ] определены сигналы data/concept drift и действия при их срабатывании; +- [ ] заданы release-критерии, стратегия развёртывания и безопасный rollback; +- [ ] проверены latency, ресурсы, логирование и наблюдаемость, если они важны. + +Запрос проверки: `Проверь modules/XX/assignments/YY, решение лежит в solution`. diff --git a/common/foundations/calculus_and_matrix_calculus.md b/common/foundations/calculus_and_matrix_calculus.md new file mode 100644 index 0000000..c177cdb --- /dev/null +++ b/common/foundations/calculus_and_matrix_calculus.md @@ -0,0 +1,304 @@ +# Дифференциальное и матричное исчисление + +## Зачем это нужно + +Обучение модели обычно означает минимизацию функции потерь. Производная +отвечает на локальный вопрос: как изменится результат при малом изменении +параметра? Градиент обобщает ответ на много параметров, а матричное исчисление +позволяет записать вычисление без циклов по каждому весу. + +## Предел, производная и локальная линейность + +Производная функции \(f:\mathbb{R}\to\mathbb{R}\) в точке \(x\): + +\[ +f'(x)=\lim_{h\to0}\frac{f(x+h)-f(x)}{h}. +\] + +Это не просто наклон касательной. Если производная существует, то при малом +\(h\) + +\[ +f(x+h)=f(x)+f'(x)h+o(h), +\] + +где \(o(h)/h\to0\). Главная идея — гладкая функция локально ведёт себя как +линейная. Например, для \(f(x)=x^2\): + +\[ +\frac{(x+h)^2-x^2}{h} +=\frac{2xh+h^2}{h}=2x+h\to2x. +\] + +Отсюда \(d(x^2)/dx=2x\). + +## Правила дифференцирования + +\[ +(f+g)'=f'+g',\qquad +(fg)'=f'g+fg',\qquad +\left(\frac fg\right)'=\frac{f'g-fg'}{g^2}. +\] + +Для композиции \(y=f(g(x))\) правило цепочки: + +\[ +\frac{dy}{dx} +=\frac{df}{dg}\frac{dg}{dx}. +\] + +Пример: \(L(w)=(wx-y)^2\). Введём \(r=wx-y\), тогда + +\[ +\frac{dL}{dw} +=\frac{d(r^2)}{dr}\frac{dr}{dw} +=2r\,x +=2(wx-y)x. +\] + +Промежуточная переменная делает структуру обратного распространения явной: +локальные производные перемножаются от выхода к входу. + +## Частные производные и градиент + +Для \(f:\mathbb{R}^d\to\mathbb{R}\) частная производная меняет одну координату, +фиксируя остальные. Градиент собирает их: + +\[ +\nabla_{\mathbf{x}}f= +\begin{bmatrix} +\partial f/\partial x_1\\ +\cdots\\ +\partial f/\partial x_d +\end{bmatrix}. +\] + +Дифференцируемость означает + +\[ +f(\mathbf{x}+\Delta\mathbf{x}) +=f(\mathbf{x}) ++\nabla f(\mathbf{x})^\top\Delta\mathbf{x} ++o(\|\Delta\mathbf{x}\|). +\] + +По неравенству Коши—Буняковского изменение первого порядка максимально при +\(\Delta\mathbf{x}\) в направлении градиента. Поэтому +\(-\nabla f\) — направление быстрейшего локального убывания при евклидовой +норме. + +## Якобиан и многомерное правило цепочки + +Для \(\mathbf{g}:\mathbb{R}^d\to\mathbb{R}^m\) +якобиан имеет размер \(m\times d\): + +\[ +(\mathbf{J}_{\mathbf{g}})_{ij} +=\frac{\partial g_i}{\partial x_j}. +\] + +Если \(L=f(\mathbf{g}(\mathbf{x}))\), где \(f:\mathbb{R}^m\to\mathbb{R}\), то + +\[ +\nabla_{\mathbf{x}}L +=\mathbf{J}_{\mathbf{g}}(\mathbf{x})^\top +\nabla_{\mathbf{g}}f. +\] + +Размерности дают проверку: +\((d\times m)(m\times1)=d\times1\). + +## Гессиан и кривизна + +Гессиан содержит вторые производные: + +\[ +\mathbf{H}_f=\nabla^2f,\qquad +f(\mathbf{x}+\Delta) +\approx f(\mathbf{x})+\nabla f^\top\Delta ++\frac12\Delta^\top\mathbf{H}_f\Delta. +\] + +Если \(\mathbf{H}_f\succeq0\) во всей области, гладкая функция выпукла. +В стационарной точке положительно определённый гессиан указывает на строгий +локальный минимум, отрицательно определённый — на максимум, смешанные знаки +собственных значений — на седловую точку. + +## Дифференциалы и матрицы + +Дифференциал удобен тем, что подчиняется обычной алгебре: + +\[ +d(\mathbf{A}\mathbf{X}\mathbf{B}) +=\mathbf{A}\,d\mathbf{X}\,\mathbf{B} +\] + +для постоянных \(\mathbf{A},\mathbf{B}\), а + +\[ +d(\mathbf{X}^\top)=d\mathbf{X}^\top. +\] + +Для скалярной \(f(\mathbf{X})\) приводим дифференциал к форме + +\[ +df=\operatorname{tr}\left( +(\nabla_{\mathbf{X}}f)^\top d\mathbf{X}\right). +\] + +Полезны свойства следа: + +\[ +\operatorname{tr}(\mathbf{A}\mathbf{B}) +=\operatorname{tr}(\mathbf{B}\mathbf{A}),\qquad +\operatorname{tr}(\mathbf{A}^\top d\mathbf{X}) +=\langle\mathbf{A},d\mathbf{X}\rangle_F. +\] + +## Вывод градиента линейной регрессии + +Пусть + +\[ +L(\mathbf{w}) +=\frac{1}{2n}\|\mathbf{X}\mathbf{w}-\mathbf{y}\|_2^2. +\] + +Обозначим \(\mathbf{r}=\mathbf{X}\mathbf{w}-\mathbf{y}\). Тогда + +\[ +L=\frac1{2n}\mathbf{r}^\top\mathbf{r}. +\] + +Последовательно: + +\[ +dL +=\frac1{2n}\left(d\mathbf{r}^\top\mathbf{r} ++\mathbf{r}^\top d\mathbf{r}\right) +=\frac1n\mathbf{r}^\top d\mathbf{r}. +\] + +Поскольку \(d\mathbf{r}=\mathbf{X}\,d\mathbf{w}\), + +\[ +dL +=\frac1n\mathbf{r}^\top\mathbf{X}\,d\mathbf{w} +=\left(\frac1n\mathbf{X}^\top\mathbf{r}\right)^\top d\mathbf{w}. +\] + +Следовательно, + +\[ +\boxed{\nabla_{\mathbf{w}}L +=\frac1n\mathbf{X}^\top(\mathbf{X}\mathbf{w}-\mathbf{y})}. +\] + +Ещё одно дифференцирование даёт +\(\nabla^2L=\mathbf{X}^\top\mathbf{X}/n\succeq0\), поэтому задача выпукла. + +## Производные часто встречающихся выражений + +\[ +\nabla_{\mathbf{x}}(\mathbf{a}^\top\mathbf{x})=\mathbf{a}, +\qquad +\nabla_{\mathbf{x}}(\mathbf{x}^\top\mathbf{A}\mathbf{x}) +=(\mathbf{A}+\mathbf{A}^\top)\mathbf{x}. +\] + +Для симметричной \(\mathbf{A}\) последнее равно \(2\mathbf{A}\mathbf{x}\). + +\[ +d\log\det\mathbf{X} +=\operatorname{tr}(\mathbf{X}^{-1}d\mathbf{X}), +\qquad +\nabla_{\mathbf{X}}\log\det\mathbf{X} +=\mathbf{X}^{-\top}. +\] + +Последняя формула требует обратимой матрицы; для вещественного логарифма +обычно предполагают \(\mathbf{X}\succ0\). + +## Разобранный пример + +Пусть + +\[ +\mathbf{X}=\begin{bmatrix}1&2\\2&0\end{bmatrix},\quad +\mathbf{w}=\begin{bmatrix}1\\-1\end{bmatrix},\quad +\mathbf{y}=\begin{bmatrix}0\\1\end{bmatrix}. +\] + +Предсказания: \(\mathbf{Xw}=(-1,2)^\top\), остаток +\(\mathbf{r}=(-1,1)^\top\), \(n=2\). Тогда + +\[ +L=\frac1{4}(1+1)=0.5, +\] + +\[ +\nabla L +=\frac12\mathbf{X}^\top\mathbf{r} +=\frac12 +\begin{bmatrix}1&2\\2&0\end{bmatrix} +\begin{bmatrix}-1\\1\end{bmatrix} +=\begin{bmatrix}0.5\\-1\end{bmatrix}. +\] + +```python +import numpy as np + +X = np.array([[1.0, 2.0], [2.0, 0.0]]) +w = np.array([1.0, -1.0]) +y = np.array([0.0, 1.0]) + +r = X @ w - y +loss = r @ r / (2 * len(y)) +grad = X.T @ r / len(y) +print(loss) # 0.5 +print(grad) # [ 0.5 -1. ] +``` + +## Численная проверка градиента + +Центральная разность + +\[ +\frac{\partial f}{\partial x_j} +\approx\frac{f(\mathbf{x}+h\mathbf{e}_j) +-f(\mathbf{x}-h\mathbf{e}_j)}{2h} +\] + +имеет ошибку усечения порядка \(O(h^2)\), но слишком малое \(h\) усиливает +ошибку округления. + +```python +def finite_difference(f, x, h=1e-5): + grad = np.zeros_like(x, dtype=float) + for j in range(x.size): + step = np.zeros_like(x, dtype=float) + step[j] = h + grad[j] = (f(x + step) - f(x - step)) / (2 * h) + return grad + +f = lambda v: np.sum(v**2) +print(finite_difference(f, np.array([1.0, -3.0]))) +``` + +## Типичные ошибки + +- Производная по вектору должна иметь проверяемую размерность. +- Поэлементное `*` не заменяет матричное `@`. +- \(\nabla(\mathbf{x}^\top\mathbf{A}\mathbf{x})=2\mathbf{Ax}\) только при + симметричной \(\mathbf{A}\). +- Нулевой градиент не гарантирует минимум. +- Градиент зависит от выбранной геометрии; утверждение о быстрейшем спуске + выше относится к евклидовой норме. +- Численная производная служит проверкой, а не точной заменой аналитической. + +## Резюме + +Производная — коэффициент локального линейного приближения. Градиент задаёт +чувствительность скаляра ко всем координатам, якобиан — чувствительность +вектора, гессиан — кривизну. Дифференциалы, след и контроль размерностей дают +надёжный способ выводить матричные градиенты. diff --git a/common/foundations/complexity_and_computation.md b/common/foundations/complexity_and_computation.md new file mode 100644 index 0000000..49f3df6 --- /dev/null +++ b/common/foundations/complexity_and_computation.md @@ -0,0 +1,259 @@ +# Сложность и организация вычислений + +## Зачем оценивать сложность + +Корректный алгоритм может быть практически бесполезен, если время или память +растут слишком быстро. Анализ сложности отвечает не на вопрос «сколько секунд», +а на вопрос «как изменится потребление ресурсов при росте входа». + +Для данных будем использовать: + +- \(n\) — число объектов; +- \(d\) — число признаков; +- \(k\) — число классов или компонент; +- \(T\) — число итераций. + +## Асимптотические обозначения + +\(f(n)=O(g(n))\), если существуют \(c>0\) и \(n_0\), такие что + +\[ +0\leq f(n)\leq c\,g(n)\quad\text{при }n\geq n_0. +\] + +\(\Omega(g(n))\) задаёт нижнюю границу, а \(\Theta(g(n))\) — одновременно +верхнюю и нижнюю с точностью до констант. + +Например, + +\[ +3n^2+10n+7=\Theta(n^2), +\] + +потому что при больших \(n\) квадратичный член доминирует. Это не означает, +что коэффициенты и младшие члены всегда неважны: для реальных размеров они +могут определять более быстрый вариант. + +## Типичные скорости роста + +От более медленного роста к более быстрому: + +\[ +O(1),\quad O(\log n),\quad O(n),\quad +O(n\log n),\quad O(n^2),\quad O(2^n). +\] + +Удвоение \(n\) примерно удваивает линейную работу, увеличивает квадратичную +в четыре раза и экспоненциальную — радикально. Логарифмическое время возникает, +когда каждый шаг уменьшает задачу в постоянное число раз, как бинарный поиск. + +## Анализ циклов + +Последовательные блоки складываются: + +\[ +O(n)+O(n^2)=O(n^2). +\] + +Вложенные независимые циклы перемножаются: + +```python +total = 0.0 +for i in range(n): + for j in range(d): + total += X[i, j] +``` + +Выполняется \(nd\) сложений, значит время \(O(nd)\), а дополнительная память +\(O(1)\). Не следует автоматически называть это \(O(n^2)\): \(n\) и \(d\) — +разные параметры. + +## Стоимость линейной алгебры + +Для \(\mathbf{A}\in\mathbb{R}^{m\times n}\) и +\(\mathbf{B}\in\mathbb{R}^{n\times p}\) матричное произведение требует +приблизительно \(mnp\) умножений и столько же сложений: + +\[ +\mathbf{C}_{ij}=\sum_{\ell=1}^{n}A_{i\ell}B_{\ell j}, +\qquad O(mnp). +\] + +Частные случаи: + +- матрица-вектор \((n\times d)(d)\): \(O(nd)\); +- \(\mathbf{X}^\top\mathbf{X}\): \(O(nd^2)\); +- плотное решение системы \(d\times d\): обычно \(O(d^3)\); +- хранение \(\mathbf{X}\): \(O(nd)\). + +Для разреженной матрицы важнее число ненулевых элементов +\(\operatorname{nnz}(\mathbf{X})\). Умножение на вектор может стоить +\(O(\operatorname{nnz}(\mathbf{X}))\), если формат хранения поддерживает +разреженность. + +## Пример: линейная регрессия + +Один полный градиент + +\[ +\nabla L +=\frac1n\mathbf{X}^\top(\mathbf{Xw}-\mathbf{y}) +\] + +состоит из: + +1. \(\mathbf{Xw}\): \(O(nd)\); +2. вычитания \(\mathbf{y}\): \(O(n)\); +3. умножения \(\mathbf{X}^\top\mathbf{r}\): \(O(nd)\). + +Итого \(O(nd)\) времени и \(O(n)\) дополнительной памяти для остатка. +За \(T\) итераций — \(O(Tnd)\). + +Нормальные уравнения требуют формирования +\(\mathbf{X}^\top\mathbf{X}\) за \(O(nd^2)\) и решения системы примерно за +\(O(d^3)\). Какой подход быстрее, зависит от \(n,d,T\), памяти и +обусловленности. + +## Векторизация: та же асимптотика, другое исполнение + +```python +import numpy as np + +rng = np.random.default_rng(0) +X = rng.normal(size=(1000, 100)) +w = rng.normal(size=100) + +# Математически это O(nd). +y_vectorized = X @ w +print(y_vectorized.shape) +``` + +Цикл Python и `X @ w` имеют одинаковый порядок \(O(nd)\), но векторизованная +операция выполняется оптимизированной библиотекой, использует кэш и SIMD, +а иногда несколько потоков. Big O не отражает эти константы. + +Векторизация может увеличить пиковую память из-за временных массивов: + +```python +# Создаёт временный массив формы X.shape. +column_energy = (X**2).sum(axis=0) + +# Эквивалентное свёртывание без явного X**2 может быть экономнее. +column_energy_einsum = np.einsum("ij,ij->j", X, X) +print(np.allclose(column_energy, column_energy_einsum)) +``` + +## Память и представление данных + +Массив `float64` из \(nd\) элементов занимает примерно \(8nd\) байт, +`float32` — \(4nd\). Для миллиона объектов и тысячи плотных признаков +`float64` потребует около \(8\) ГБ только для матрицы. + +Пиковая память включает: + +\[ +\text{данные}+\text{параметры}+\text{активации} ++\text{градиенты}+\text{временные массивы}. +\] + +Срез NumPy часто является представлением без копирования, но fancy indexing +обычно создаёт копию. Проверять это можно через `np.shares_memory`. + +## Локальность и кэш + +Доступ к последовательным адресам памяти быстрее случайного доступа. +NumPy по умолчанию хранит двумерные массивы построчно в C-порядке. Поэтому +обход соседних элементов последней оси обычно лучше использует кэш. + +Асимптотика двух обходов может совпадать, но фактическое время различаться +из-за промахов кэша и пропускной способности памяти. Современное вычисление +часто ограничено не числом арифметических операций, а перемещением данных. + +## Пакетная обработка + +Если весь набор не помещается в память устройства, его делят на батчи размера +\(b\). Для плотного линейного слоя: + +\[ +\mathbf{H}=\mathbf{X}_{b\times d}\mathbf{W}_{d\times h} +\] + +стоимость одного батча \(O(bdh)\), а активации занимают \(O(bh)\). Уменьшение +\(b\) снижает пиковую память, но может хуже загружать устройство и увеличивать +накладные расходы. Градиент по мини-батчу также более шумный. + +## Амортизированная сложность + +Одна операция может иногда быть дорогой, но дешёвой в среднем по +последовательности. Добавление в динамический массив обычно \(O(1)\) +амортизированно: редкое расширение стоит \(O(n)\), но ёмкость увеличивается +геометрически, и общая стоимость \(n\) добавлений остаётся \(O(n)\). + +Амортизированная оценка не равна средней по случайному входу: она гарантирует +стоимость последовательности операций без вероятностной модели входа. + +## Время обучения и время предсказания + +Сложность следует оценивать отдельно: + +- подготовка данных; +- обучение; +- предсказание одного объекта и батча; +- память модели; +- память промежуточных вычислений. + +Например, \(k\)-NN почти не обучается, но предсказание по плотным данным требует +сравнения с \(n\) объектами: \(O(nd)\). Линейная модель дорого оптимизируется, +но одно предсказание стоит \(O(d)\). + +## Измерение производительности + +Теоретическая оценка определяет ожидаемый рост, профилирование находит реальное +узкое место. Надёжный эксперимент: + +1. прогревает код; +2. повторяет измерение; +3. меняет один размер входа; +4. фиксирует версии, dtype и устройство; +5. проверяет корректность сравниваемых результатов. + +```python +from time import perf_counter + +start = perf_counter() +for _ in range(100): + _ = X @ w +elapsed = perf_counter() - start +print(f"{elapsed / 100:.6e} s per run") +``` + +Очень короткие операции лучше измерять `timeit`; фоновая нагрузка и +многопоточность делают единичный замер ненадёжным. + +## Компромиссы + +- **Время против памяти:** кэширование ускоряет повторные вычисления, но + требует памяти. +- **Точность против скорости:** `float32` быстрее на многих ускорителях, но + имеет меньший диапазон точности. +- **Предвычисление против задержки:** индекс строится долго, зато ускоряет + запросы. +- **Точная задача против приближения:** случайные проекции и выборка уменьшают + стоимость ценой контролируемой ошибки. + +## Типичные ошибки + +- Big O не является временем в секундах и не учитывает константы. +- Нельзя скрывать разные переменные одним \(n\), если они растут независимо. +- Векторизация не меняет автоматически асимптотическую сложность. +- Оценка памяти без временных массивов часто занижена. +- Средняя, худшая и амортизированная сложности отвечают на разные вопросы. +- Более быстрый алгоритм не полезен, если он меняет результат или нарушает + численную устойчивость. + +## Резюме + +Сложность описывает масштабирование времени и памяти. Для ML особенно важны +\(n,d,k,T\), стоимость матричных операций, плотность данных и пиковая память. +Асимптотический анализ выбирает класс решения, а измерения и профилирование +уточняют константы и аппаратные ограничения. diff --git a/common/foundations/information_theory.md b/common/foundations/information_theory.md new file mode 100644 index 0000000..444c84c --- /dev/null +++ b/common/foundations/information_theory.md @@ -0,0 +1,279 @@ +# Основы теории информации + +## Что измеряет информация + +Редкое событие сообщает больше, чем ожидаемое. Если событие имеет вероятность +\(p\), его **самоинформация** + +\[ +I(p)=-\log p. +\] + +Эта форма следует из естественных требований: информация непрерывно зависит +от \(p\), убывает при росте вероятности и складывается для независимых событий: + +\[ +I(pq)=I(p)+I(q). +\] + +Логарифм по основанию \(2\) даёт биты, натуральный логарифм — наты. В машинном +обучении обычно используется натуральный логарифм. Невозможное событие имеет +бесконечную самоинформацию, достоверное — нулевую. + +## Энтропия + +Для дискретной случайной величины \(X\sim p\) энтропия — ожидаемая +самоинформация: + +\[ +H(X) +=\mathbb{E}_{X\sim p}[-\log p(X)] +=-\sum_x p(x)\log p(x). +\] + +Используется соглашение \(0\log0=0\), получаемое по пределу. Энтропия +не измеряет «хаос» отдельного исхода: это средняя неопределённость до +наблюдения исхода. + +Для Бернулли \(X\sim\operatorname{Bernoulli}(p)\): + +\[ +H(X)=-p\log p-(1-p)\log(1-p). +\] + +При \(p=0\) или \(1\) исход известен и \(H=0\). Максимум достигается при +\(p=1/2\). Проверим производной: + +\[ +\frac{dH}{dp} +=-\log p-1+\log(1-p)+1 +=\log\frac{1-p}{p}. +\] + +Равенство нулю даёт \(p=1/2\), а + +\[ +\frac{d^2H}{dp^2}=-\frac1p-\frac1{1-p}<0, +\] + +поэтому это максимум. + +```python +import numpy as np + +def binary_entropy(p): + p = np.asarray(p, dtype=float) + terms = np.where(p > 0, p * np.log(p), 0.0) + q = 1 - p + terms_q = np.where(q > 0, q * np.log(q), 0.0) + return -(terms + terms_q) + +print(binary_entropy(np.array([0.1, 0.5, 0.9]))) +``` + +## Кодирование и смысл энтропии + +Если исход \(x\) кодируется примерно \(-\log_2p(x)\) битами, частые исходы +получают короткие коды, редкие — длинные. Для длинной последовательности +независимых исходов средняя длина оптимального кода не может быть существенно +меньше \(H(X)\) битов на символ. Поэтому энтропия — фундаментальный предел +сжатия, а не только абстрактная формула. + +## Кросс-энтропия + +Пусть данные порождены истинным распределением \(p\), а модель использует +\(q\). Средняя длина кода модели: + +\[ +H(p,q) +=\mathbb{E}_{x\sim p}[-\log q(x)] +=-\sum_xp(x)\log q(x). +\] + +Это **кросс-энтропия**. Она штрафует модель особенно сильно, если та назначает +малую вероятность реально встречающемуся событию. + +Для классификации одного объекта с one-hot-меткой \(\mathbf{y}\) и +предсказанными вероятностями \(\hat{\mathbf{p}}\): + +\[ +\ell=-\sum_{k=1}^{K}y_k\log\hat p_k +=-\log\hat p_{k^\star}. +\] + +Сумма исчезает, потому что только правильный класс имеет \(y_k=1\). + +## Дивергенция Кульбака—Лейблера + +\[ +D_{\mathrm{KL}}(p\|q) +=\sum_xp(x)\log\frac{p(x)}{q(x)}. +\] + +Разложим кросс-энтропию: + +\[ +\begin{aligned} +H(p,q) +&=-\sum_xp(x)\log q(x)\\ +&=-\sum_xp(x)\log p(x) + +\sum_xp(x)\log\frac{p(x)}{q(x)}\\ +&=H(p)+D_{\mathrm{KL}}(p\|q). +\end{aligned} +\] + +Так как \(H(p)\) не зависит от модели \(q\), минимизация кросс-энтропии +эквивалентна минимизации \(D_{\mathrm{KL}}(p\|q)\). + +KL-дивергенция неотрицательна. Из неравенства +\(\log u\leq u-1\): + +\[ +\begin{aligned} +-D_{\mathrm{KL}}(p\|q) +&=\sum_xp(x)\log\frac{q(x)}{p(x)}\\ +&\leq\sum_xp(x)\left(\frac{q(x)}{p(x)}-1\right)\\ +&=\sum_xq(x)-\sum_xp(x)=0. +\end{aligned} +\] + +Следовательно, \(D_{\mathrm{KL}}\geq0\), с равенством при \(p=q\) почти всюду. +Однако это не расстояние: обычно +\(D_{\mathrm{KL}}(p\|q)\ne D_{\mathrm{KL}}(q\|p)\), и не выполняется +неравенство треугольника. + +```python +def kl_divergence(p, q): + p = np.asarray(p, dtype=float) + q = np.asarray(q, dtype=float) + mask = p > 0 + if np.any(q[mask] == 0): + return np.inf + return np.sum(p[mask] * np.log(p[mask] / q[mask])) + +p = np.array([0.8, 0.2]) +q = np.array([0.6, 0.4]) +print(kl_divergence(p, q)) +print(kl_divergence(q, p)) +``` + +## Максимальное правдоподобие + +Для независимых наблюдений \(\{x^{(i)}\}_{i=1}^n\): + +\[ +\mathcal{L}(\theta) +=\prod_{i=1}^{n}p_\theta(x^{(i)}). +\] + +Логарифм монотонен, поэтому максимум не изменится: + +\[ +\arg\max_\theta\mathcal{L}(\theta) +=\arg\max_\theta\sum_{i=1}^{n}\log p_\theta(x^{(i)}). +\] + +Эквивалентно минимизируется отрицательное лог-правдоподобие: + +\[ +-\frac1n\sum_i\log p_\theta(x^{(i)}). +\] + +Эмпирические частоты заменяют истинное ожидание, поэтому это эмпирическая +кросс-энтропия между распределением данных и моделью. + +## Softmax и стабильная кросс-энтропия + +Логиты \(\mathbf{z}\in\mathbb{R}^K\) преобразуются в вероятности: + +\[ +p_k=\frac{e^{z_k}}{\sum_j e^{z_j}}. +\] + +Для правильного класса \(y\): + +\[ +\begin{aligned} +-\log p_y +&=-\log\frac{e^{z_y}}{\sum_j e^{z_j}}\\ +&=-z_y+\log\sum_j e^{z_j}. +\end{aligned} +\] + +Применяя устойчивый `logsumexp` с \(m=\max_jz_j\), получаем + +\[ +\ell=-z_y+m+\log\sum_j e^{z_j-m}. +\] + +```python +def cross_entropy_from_logits(logits, target): + logits = np.asarray(logits, dtype=float) + m = logits.max() + logsumexp = m + np.log(np.exp(logits - m).sum()) + return logsumexp - logits[target] + +print(cross_entropy_from_logits([1000.0, 1001.0, 999.0], 1)) +``` + +Градиент по логитам имеет простую форму. Для +\(\ell=-z_y+\log\sum_j e^{z_j}\): + +\[ +\frac{\partial\ell}{\partial z_k} +=-\mathbb{1}\{k=y\} ++\frac{e^{z_k}}{\sum_j e^{z_j}} +=p_k-\mathbb{1}\{k=y\}. +\] + +## Условная энтропия и взаимная информация + +\[ +H(Y\mid X) +=-\sum_{x,y}p(x,y)\log p(y\mid x) +\] + +измеряет оставшуюся неопределённость о \(Y\) после знания \(X\). +Взаимная информация: + +\[ +\begin{aligned} +I(X;Y) +&=D_{\mathrm{KL}}(p(x,y)\|p(x)p(y))\\ +&=\sum_{x,y}p(x,y) +\log\frac{p(x,y)}{p(x)p(y)}\\ +&=H(Y)-H(Y\mid X). +\end{aligned} +\] + +Она показывает, насколько знание одной переменной уменьшает неопределённость +другой. \(I(X;Y)\geq0\) и равна нулю при независимости. + +## Непрерывный случай + +Дифференциальная энтропия + +\[ +h(X)=-\int p(x)\log p(x)\,dx +\] + +может быть отрицательной и меняется при смене масштаба. Поэтому её нельзя +интерпретировать как обычное число битов отдельной непрерывной величины. +KL-дивергенция и взаимная информация сохраняют более надёжный смысл при +корректных плотностях. + +## Типичные ошибки + +- Энтропия распределения и кросс-энтропия двух распределений — разные вещи. +- KL-дивергенция не симметрична и не является метрикой. +- Нельзя вычислять `log(softmax(z))` наивно при больших логитах. +- Нулевая вероятность модели для возможного события даёт бесконечную потерю. +- Дифференциальная энтропия может быть отрицательной. +- Независимость наблюдений в выводе правдоподобия — предположение, а не факт. + +## Резюме + +Самоинформация измеряет неожиданность, энтропия — её среднее. Кросс-энтропия +оценивает кодирование данных моделью и равна энтропии плюс KL-дивергенция. +Отрицательное лог-правдоподобие связывает эти идеи с обучением классификаторов, +а взаимная информация измеряет статистическую зависимость. diff --git a/common/foundations/numerical_stability.md b/common/foundations/numerical_stability.md new file mode 100644 index 0000000..ee44e0e --- /dev/null +++ b/common/foundations/numerical_stability.md @@ -0,0 +1,282 @@ +# Численная устойчивость + +## Математическое число и число в памяти + +Компьютер хранит большинство вещественных чисел в формате с плавающей точкой: + +\[ +x=(-1)^s\,m\,2^e, +\] + +где знак \(s\), мантисса \(m\) и показатель \(e\) имеют конечное число битов. +Поэтому представимо лишь конечное множество чисел. Результат операции обычно +округляется: + +\[ +\operatorname{fl}(x\circ y)=(x\circ y)(1+\delta), +\qquad |\delta|\lesssim\varepsilon_{\mathrm{mach}}, +\] + +если нет переполнения или потери нормализации. Для `float64` +\(\varepsilon_{\mathrm{mach}}\approx2.22\cdot10^{-16}\), для `float32` — +примерно \(1.19\cdot10^{-7}\). + +```python +import numpy as np + +for dtype in (np.float32, np.float64): + info = np.finfo(dtype) + print(dtype.__name__, info.eps, info.max, info.tiny) +``` + +## Ошибка задачи и ошибка алгоритма + +**Обусловленность** — свойство математической задачи: насколько выход +чувствителен к малому изменению входа. **Устойчивость** — свойство алгоритма: +насколько вычисленный ответ похож на точный ответ для слегка возмущённых +данных. + +Относительное число обусловленности скалярной функции: + +\[ +\kappa_f(x)=\left|\frac{x f'(x)}{f(x)}\right|. +\] + +Из \(df\approx f'(x)dx\) следует + +\[ +\frac{|df|}{|f(x)|} +\approx\kappa_f(x)\frac{|dx|}{|x|}. +\] + +Если \(\kappa\) велико, никакой алгоритм не восстановит много точных цифр из +неточного входа. Для линейной системы \(\mathbf{A}\mathbf{x}=\mathbf{b}\): + +\[ +\kappa_2(\mathbf{A}) +=\|\mathbf{A}\|_2\|\mathbf{A}^{-1}\|_2 +=\frac{\sigma_{\max}}{\sigma_{\min}}. +\] + +## Катастрофическая потеря значимости + +При вычитании близких чисел старшие разряды сокращаются, а накопленная ошибка +становится большой относительно результата. Например: + +\[ +\sqrt{x+1}-\sqrt{x} +\] + +при большом \(x\) — разность близких чисел. Домножим на сопряжённое: + +\[ +\sqrt{x+1}-\sqrt{x} +=\frac{(\sqrt{x+1}-\sqrt{x})(\sqrt{x+1}+\sqrt{x})} +{\sqrt{x+1}+\sqrt{x}} +=\frac1{\sqrt{x+1}+\sqrt{x}}. +\] + +Вторая форма численно надёжнее. + +```python +import math + +x = 1e16 +unstable = math.sqrt(x + 1.0) - math.sqrt(x) +stable = 1.0 / (math.sqrt(x + 1.0) + math.sqrt(x)) +print(unstable, stable) +``` + +## Переполнение, исчезновение и логарифмы + +\(e^x\) быстро переполняется, а \(e^{-x}\) исчезает до нуля. Для + +\[ +\operatorname{logsumexp}(\mathbf{x}) +=\log\sum_i e^{x_i} +\] + +выберем \(m=\max_i x_i\): + +\[ +\log\sum_i e^{x_i} +=\log\left(e^m\sum_i e^{x_i-m}\right) +=m+\log\sum_i e^{x_i-m}. +\] + +Теперь все показатели \(x_i-m\leq0\), поэтому переполнения нет. + +```python +import numpy as np + +x = np.array([1000.0, 1001.0, 999.0]) +m = x.max() +value = m + np.log(np.exp(x - m).sum()) +print(value) +``` + +Тот же сдвиг стабилизирует softmax: + +\[ +\operatorname{softmax}(\mathbf{x})_i +=\frac{e^{x_i-m}}{\sum_j e^{x_j-m}}. +\] + +Сдвиг не меняет результат, потому что общий множитель \(e^{-m}\) сокращается. + +## Вероятности лучше хранить в логарифмах + +Произведение многих вероятностей стремится к нулю: + +\[ +p=\prod_{i=1}^{n}p_i. +\] + +В логарифмической области: + +\[ +\log p=\sum_{i=1}^{n}\log p_i. +\] + +Сумма устойчивее произведения и превращает максимизацию правдоподобия в +максимизацию логарифма. Для выражений около нуля используйте специальные +функции: + +\[ +\log(1+x)\quad\longrightarrow\quad\texttt{log1p(x)}, +\] + +\[ +e^x-1\quad\longrightarrow\quad\texttt{expm1(x)}. +\] + +```python +x = 1e-16 +print(np.log(1 + x), np.log1p(x)) +print(np.exp(x) - 1, np.expm1(x)) +``` + +## Суммирование и порядок операций + +Сложение с плавающей точкой не ассоциативно: + +\[ +(a+b)+c\ne a+(b+c). +\] + +Малое число может исчезнуть при прибавлении к большому. Ошибка обычной суммы +может накапливаться пропорционально числу слагаемых. Компенсированное +суммирование Кэхэна хранит потерянную младшую часть: + +```python +def kahan_sum(values): + total = 0.0 + correction = 0.0 + for value in values: + adjusted = value - correction + updated = total + adjusted + correction = (updated - total) - adjusted + total = updated + return total + +values = [1e16, 1.0, 1.0, -1e16] +print(sum(values), kahan_sum(values)) +``` + +На практике `math.fsum` и многие библиотечные редукции используют улучшенные +стратегии, но точная реализация зависит от библиотеки и устройства. + +## Не вычисляйте обратную матрицу без необходимости + +Для решения \(\mathbf{A}\mathbf{x}=\mathbf{b}\) запись +\(\mathbf{x}=\mathbf{A}^{-1}\mathbf{b}\) математически верна, но явное +построение обратной матрицы делает больше операций и обычно увеличивает ошибку. + +```python +A = np.array([[3.0, 1.0], [1.0, 2.0]]) +b = np.array([9.0, 8.0]) +x = np.linalg.solve(A, b) +print(x) +``` + +Для задачи наименьших квадратов не следует бездумно использовать нормальные +уравнения: + +\[ +\mathbf{X}^\top\mathbf{X}\mathbf{w} +=\mathbf{X}^\top\mathbf{y}. +\] + +Их число обусловленности примерно возводится в квадрат: + +\[ +\kappa_2(\mathbf{X}^\top\mathbf{X}) +=\kappa_2(\mathbf{X})^2. +\] + +`np.linalg.lstsq` использует более устойчивые факторизации. + +## Масштаб признаков + +Если один столбец \(\mathbf{X}\) имеет порядок \(10^{-6}\), а другой \(10^6\), +поверхность потерь сильно вытянута. Это ухудшает обусловленность гессиана и +замедляет градиентный спуск. Стандартизация + +\[ +z_j=\frac{x_j-\mu_j}{\sigma_j} +\] + +не гарантирует идеальную обусловленность, но часто существенно её улучшает. +Параметры \(\mu_j,\sigma_j\) оцениваются только по обучающей выборке. + +## Допуски вместо точного равенства + +Вещественные результаты сравнивают с абсолютным и относительным допуском: + +\[ +|a-b|\leq \text{atol}+\text{rtol}|b|. +\] + +```python +print(0.1 + 0.2 == 0.3) +print(np.isclose(0.1 + 0.2, 0.3, rtol=1e-12, atol=1e-15)) +``` + +Абсолютный допуск важен около нуля, относительный — для больших величин. +Допуск должен следовать из масштаба задачи, а не скрывать ошибку. + +## Диагностика + +Проверяйте данные и промежуточные результаты: + +```python +def describe_numeric(name, array): + a = np.asarray(array) + print( + name, + "shape=", a.shape, + "dtype=", a.dtype, + "finite=", np.isfinite(a).all(), + "min=", np.nanmin(a), + "max=", np.nanmax(a), + ) +``` + +Полезные признаки проблемы: `nan`, `inf`, резкий рост нормы параметров, +нулевые вероятности, огромный разброс масштабов и большое +`np.linalg.cond(A)`. + +## Типичные ошибки + +- Добавление \(\varepsilon\) повсюду может изменить модель и замаскировать + причину; сначала выведите устойчивую форму. +- `float64` уменьшает округление, но не исправляет плохо обусловленную задачу. +- Нормализация не должна использовать статистики тестовой выборки. +- Проверка только конечного ответа пропускает место появления первого `nan`. +- Математически эквивалентные формулы могут быть вычислительно неравноценны. + +## Резюме + +Численная надёжность требует различать чувствительность задачи и качество +алгоритма. Стабильные сдвиги, логарифмическая область, факторизации, разумное +масштабирование и проверки конечности важнее механического увеличения точности. diff --git a/data/.gitkeep b/data/.gitkeep new file mode 100644 index 0000000..d3f5a12 --- /dev/null +++ b/data/.gitkeep @@ -0,0 +1 @@ + diff --git a/modules/00_environment/README.md b/modules/00_environment/README.md new file mode 100644 index 0000000..07312ef --- /dev/null +++ b/modules/00_environment/README.md @@ -0,0 +1,36 @@ +# Модуль 00. Среда, Git и воспроизводимость + +Плановый срок: **недели 1**. + +## Цели + +- уверенно работать с виртуальным окружением, пакетами, Git и Jupyter +- понимать структуру ML-проекта и требования к воспроизводимости +- научиться оформлять решение так, чтобы его мог запустить другой человек + +## Предварительные знания + +Уверенное владение базовым Python и командной строкой не требуется: достаточно уметь запускать команды и редактировать текстовые файлы. + +## Теория + +1. [Python-окружения, зависимости и устройство проекта](notes/01_python_environments.md) +2. [Git: коммиты, ветки, diff и исключение больших данных](notes/02_git_reproducibility.md) +3. [Jupyter и модули: seed, конфигурация и логирование](notes/03_jupyter_seeds_logging.md) + +Термины модуля: [`notes/glossary.md`](notes/glossary.md). + +Для каждого урока: прочитайте теорию, воспроизведите ключевой пример своими +руками и запишите три тезиса без подсказки. Формулы из этого модуля нужно +уметь связать с кодом и наблюдаемым поведением модели. + +## Задания + +- [`assignments/01_environment.md`](assignments/01_environment.md) — Воспроизводимое окружение +- [`assignments/02_git.md`](assignments/02_git.md) — Учебный Git-процесс +- [`assignments/03_reproducibility.md`](assignments/03_reproducibility.md) — Повторяемый эксперимент + +## Завершение модуля + +Модуль завершён, когда все задания удовлетворяют критериям приёмки, +вы можете объяснить основные решения без ноутбука и получили review. diff --git a/modules/00_environment/assignments/01_environment.md b/modules/00_environment/assignments/01_environment.md new file mode 100644 index 0000000..55464bd --- /dev/null +++ b/modules/00_environment/assignments/01_environment.md @@ -0,0 +1,41 @@ +# Воспроизводимое окружение + +## Теория перед выполнением + +Перед началом прочитайте: + +- [`01_python_environments.md`](../notes/01_python_environments.md) + + +## Цель + +Создать `.venv`, установить core-зависимости, сохранить команды и проверить импорты. + +## Что сделать + +1. Сформулировать ожидаемый результат до запуска эксперимента. +2. Реализовать простой baseline. +3. Реализовать основной метод самостоятельно настолько, насколько требует задание. +4. Добавить автоматические проверки ключевых вычислений. +5. Провести эксперимент, сохранить метрики и примеры ошибок. +6. Заполнить отчёт по `common/REPORT_TEMPLATE.md`. + +## Результат + +Файл `solution/environment_report.md` с версиями Python и библиотек; скрипт `solution/check_imports.py`. + +Решение разместите рядом в `solution/`. Не коммитьте большие данные, +веса или секреты. + +## Критерии приёмки + +- Скрипт запускается из корня без ошибок, инструкция позволяет повторить настройку. +- Код читаем, разбит на функции и запускается по инструкции. +- Выводы подтверждаются измерениями и не выходят за пределы эксперимента. + +## Вопросы для самопроверки + +- Почему выбран именно этот метод и какой baseline он должен превзойти? +- Где здесь возможна утечка данных или некорректная оценка? +- Как изменится результат при других данных или ограничениях? +- Какую ошибку модели вы считаете наиболее важной и почему? diff --git a/modules/00_environment/assignments/02_git.md b/modules/00_environment/assignments/02_git.md new file mode 100644 index 0000000..9b4f10c --- /dev/null +++ b/modules/00_environment/assignments/02_git.md @@ -0,0 +1,41 @@ +# Учебный Git-процесс + +## Теория перед выполнением + +Перед началом прочитайте: + +- [`02_git_reproducibility.md`](../notes/02_git_reproducibility.md) + + +## Цель + +Создать учебную ветку, внести небольшое изменение, изучить diff и оформить осмысленный коммит. + +## Что сделать + +1. Сформулировать ожидаемый результат до запуска эксперимента. +2. Реализовать простой baseline. +3. Реализовать основной метод самостоятельно настолько, насколько требует задание. +4. Добавить автоматические проверки ключевых вычислений. +5. Провести эксперимент, сохранить метрики и примеры ошибок. +6. Заполнить отчёт по `common/REPORT_TEMPLATE.md`. + +## Результат + +Файл `solution/git_notes.md` с командами и объяснением staging area. + +Решение разместите рядом в `solution/`. Не коммитьте большие данные, +веса или секреты. + +## Критерии приёмки + +- Вы можете объяснить разницу working tree, index и commit; секреты и данные не попали в Git. +- Код читаем, разбит на функции и запускается по инструкции. +- Выводы подтверждаются измерениями и не выходят за пределы эксперимента. + +## Вопросы для самопроверки + +- Почему выбран именно этот метод и какой baseline он должен превзойти? +- Где здесь возможна утечка данных или некорректная оценка? +- Как изменится результат при других данных или ограничениях? +- Какую ошибку модели вы считаете наиболее важной и почему? diff --git a/modules/00_environment/assignments/03_reproducibility.md b/modules/00_environment/assignments/03_reproducibility.md new file mode 100644 index 0000000..325ba05 --- /dev/null +++ b/modules/00_environment/assignments/03_reproducibility.md @@ -0,0 +1,41 @@ +# Повторяемый эксперимент + +## Теория перед выполнением + +Перед началом прочитайте: + +- [`03_jupyter_seeds_logging.md`](../notes/03_jupyter_seeds_logging.md) + + +## Цель + +Написать скрипт, дважды генерирующий одинаковую случайную выборку при фиксированном seed. + +## Что сделать + +1. Сформулировать ожидаемый результат до запуска эксперимента. +2. Реализовать простой baseline. +3. Реализовать основной метод самостоятельно настолько, насколько требует задание. +4. Добавить автоматические проверки ключевых вычислений. +5. Провести эксперимент, сохранить метрики и примеры ошибок. +6. Заполнить отчёт по `common/REPORT_TEMPLATE.md`. + +## Результат + +Код и короткий отчёт с контрольными хешами результатов. + +Решение разместите рядом в `solution/`. Не коммитьте большие данные, +веса или секреты. + +## Критерии приёмки + +- Повторные запуски совпадают; изменение seed меняет результат. +- Код читаем, разбит на функции и запускается по инструкции. +- Выводы подтверждаются измерениями и не выходят за пределы эксперимента. + +## Вопросы для самопроверки + +- Почему выбран именно этот метод и какой baseline он должен превзойти? +- Где здесь возможна утечка данных или некорректная оценка? +- Как изменится результат при других данных или ограничениях? +- Какую ошибку модели вы считаете наиболее важной и почему? diff --git a/modules/00_environment/notes/01_python_environments.md b/modules/00_environment/notes/01_python_environments.md new file mode 100644 index 0000000..c0e7c8a --- /dev/null +++ b/modules/00_environment/notes/01_python_environments.md @@ -0,0 +1,223 @@ +# 01. Python-окружения, зависимости и устройство проекта + +> Изолируем интерпретатор и зависимости так, чтобы проект одинаково запускался сегодня, на другой машине и в автоматической проверке. + +## Мотивация + +Python-программа зависит не только от исходного кода. Результат могут изменить версия +Python, версии библиотек, операционная система, переменные окружения и рабочий каталог. +Фраза «у меня работает» обычно означает, что часть этих условий осталась неописанной. + +Воспроизводимый проект делает условия явными. Это позволяет: + +- не смешивать зависимости разных проектов; +- повторно собрать среду после поломки; +- отличить ошибку кода от несовместимости версий; +- передать проект без копирования локального окружения. + +## Интуитивная модель + +Окружение — это отдельный набор инструментов для одного проекта. Интерпретатор Python +играет роль станка, пакеты — насадок, а файл зависимостей — спецификации комплекта. +Каталог `.venv` является расходным артефактом: его можно удалить и собрать снова. + +Удобно разделять четыре слоя: + +1. **исходники** — то, что пишется и проверяется в Git; +2. **конфигурация** — объявленные зависимости и параметры запуска; +3. **окружение** — конкретно установленные пакеты; +4. **данные и результаты** — входы и полученные артефакты. + +## Интерпретатор, пакет и импорт + +**Интерпретатор** исполняет байткод Python. Команда `python` выбирается через `PATH`; +поэтому перед установкой пакетов важно проверить, какой именно Python запущен: + +```python +import sys + +print(sys.executable) +print(sys.version) +``` + +**Модуль** — обычно один файл `.py`. **Пакет** — импортируемая директория модулей. +**Дистрибутив** — устанавливаемая единица, известная менеджеру пакетов. Имя +дистрибутива и имя импорта иногда различаются: устанавливается `scikit-learn`, +а импортируется `sklearn`. + +Поиск при `import name` идёт по `sys.path`. В него входят каталог запуска, +стандартная библиотека, установленные пакеты и явно добавленные пути. Изменение +рабочего каталога может поэтому скрыть ошибку структуры проекта. + +## Создание виртуального окружения + +Из корня проекта: + +```text +python -m venv .venv +``` + +Активация в PowerShell: + +```text +.\.venv\Scripts\Activate.ps1 +python -m pip install --upgrade pip +``` + +В Linux и macOS путь активации — `.venv/bin/activate`. Активация лишь ставит +интерпретатор окружения раньше остальных в `PATH`. Можно не активировать среду, +а вызывать `.venv\Scripts\python.exe` напрямую. + +Форма `python -m pip` безопаснее команды `pip`: модуль `pip` запускается тем же +интерпретатором, которым затем будет запущена программа. + +Проверка связи: + +```python +import sys +import numpy as np + +print("python:", sys.executable) +print("numpy:", np.__version__) +print("numpy file:", np.__file__) +``` + +## Объявленные и зафиксированные зависимости + +Есть два разных требования. + +- **Прямая зависимость** нужна коду непосредственно, например NumPy. +- **Транзитивная зависимость** нужна прямой зависимости. + +Ограничение `numpy>=2` сообщает допустимый диапазон, но не гарантирует одинаковую +сборку завтра. Точная фиксация `numpy==2.2.1` воспроизводимее, однако требует +осознанных обновлений. Для библиотеки обычно задают совместимые диапазоны, для +приложения или учебного окружения — lock-файл с точными версиями. + +Минимальный `requirements.txt`: + +```text +numpy==2.2.1 +pytest==8.3.4 +``` + +Установка и контроль: + +```text +python -m pip install -r requirements.txt +python -m pip check +python -m pip freeze +``` + +`freeze` описывает текущее состояние, включая транзитивные пакеты. Это снимок, +а не обязательно хороший вручную поддерживаемый список прямых зависимостей. + +## Устройство вычислительного проекта + +Практичная структура: + +```text +project/ +├── README.md +├── pyproject.toml +├── src/ +│ └── model.py +├── tests/ +│ └── test_model.py +├── notebooks/ +├── data/ +│ ├── raw/ +│ └── processed/ +└── artifacts/ +``` + +`README.md` описывает точные команды от чистого клона до результата. `src/` +содержит повторно используемую логику, `tests/` — автоматические проверки, +`notebooks/` — исследование, `data/` — данные, `artifacts/` — модели и отчёты. +Большие данные и сгенерированные результаты обычно не хранят в обычном Git. + +Код не должен предполагать неизвестный текущий каталог. Путь относительно файла: + +```python +from pathlib import Path + +PROJECT_ROOT = Path(__file__).resolve().parents[1] +config_path = PROJECT_ROOT / "config" / "train.json" +print(config_path) +``` + +Для команды запуска часто лучше явно передать путь аргументом. Тогда зависимость +видна пользователю и тестируется отдельно. + +## Конфигурация и секреты + +Конфигурация — параметры, которые законно меняются между запусками: путь к данным, +размер батча, seed. Секреты — пароли, токены, ключи доступа. Секрет нельзя +коммитить даже в «приватный» репозиторий: удаление из последнего коммита не удаляет +его из истории. + +Безопасный принцип: + +- несекретные значения по умолчанию хранятся в версионируемом файле; +- секреты поступают из переменных окружения или хранилища секретов; +- пример `.env.example` содержит только имена переменных; +- локальный `.env` включён в `.gitignore`. + +```python +import os + +data_dir = os.getenv("DATA_DIR", "data/raw") +api_token = os.environ.get("API_TOKEN") +``` + +## Разобранный пример: диагностика среды + +Сценарий должен вывести версии и завершиться ошибкой, если пакет отсутствует. + +```python +from importlib.metadata import PackageNotFoundError, version +import platform +import sys + +required = ["numpy", "pytest"] +print("Python:", sys.version.split()[0]) +print("Executable:", sys.executable) +print("Platform:", platform.platform()) + +for package in required: + try: + print(f"{package}: {version(package)}") + except PackageNotFoundError: + raise SystemExit(f"Не установлен пакет {package!r}") +``` + +Такой отчёт полезен в журнале эксперимента. Он не заменяет lock-файл: два бинарных +пакета одной версии теоретически могут отличаться сборкой или платформой. + +## Типичные ошибки и ограничения + +- **Установка в глобальный Python.** Проекты начинают конфликтовать. Проверяйте + `sys.executable` и используйте `python -m pip`. +- **Коммит `.venv`.** Каталог велик, платформенно зависим и плохо переносим. + Коммитьте спецификацию, а не установленную среду. +- **Только `pip freeze` без понимания.** В снимке трудно отличить прямые зависимости. + Поддерживайте декларацию и отдельную фиксацию. +- **Неограниченные версии.** Свежий несовместимый релиз может сломать сборку. +- **Абсолютные локальные пути.** На другой машине их нет. Используйте параметры + запуска и `pathlib`. +- **Скрытая системная зависимость.** Драйвер GPU, компилятор и системная библиотека + тоже входят в вычислительную среду и должны быть задокументированы. + +## Краткое резюме + +- Воспроизводимость начинается с явного интерпретатора и изолированных пакетов. +- Окружение пересоздаётся; исходники и спецификации версионируются. +- Декларация зависимостей отвечает «что допустимо», lock-файл — «что установлено». +- Структура проекта отделяет код, тесты, данные, конфигурацию и результаты. + + +## Связанные материалы + +- Следующая глава: [`02_git_reproducibility.md`](02_git_reproducibility.md). +- Практика: [`../assignments/01_environment.md`](../assignments/01_environment.md). +- Термины: [`glossary.md`](glossary.md). diff --git a/modules/00_environment/notes/02_git_reproducibility.md b/modules/00_environment/notes/02_git_reproducibility.md new file mode 100644 index 0000000..6487d1a --- /dev/null +++ b/modules/00_environment/notes/02_git_reproducibility.md @@ -0,0 +1,239 @@ +# 02. Git: коммиты, ветки, diff и исключение больших данных + +> Git сохраняет историю изменений текста и делает происхождение каждого результата проверяемым. + +## Мотивация + +Архивы `project_final_2.zip` не отвечают на главные вопросы: что изменилось, зачем, +кто это сделал и к какой версии кода относится модель. Git хранит направленную +историю снимков и позволяет точно назвать состояние проекта идентификатором коммита. + +Для воспроизводимого эксперимента нужны как минимум: + +- коммит исходного кода; +- версия конфигурации и зависимостей; +- идентификатор входных данных; +- команда запуска и seed; +- сохранённые метрики и окружение. + +Git решает первую часть и связывает остальные части ссылками. Он не является +универсальным хранилищем больших данных и не гарантирует научную воспроизводимость +сам по себе. + +## Модель данных Git + +У файла есть три практически важных состояния: + +1. **рабочая директория** — текущие файлы; +2. **индекс** (staging area) — снимок, подготовленный к следующему коммиту; +3. **репозиторий** — уже сохранённые коммиты. + +Коммит содержит дерево файлов, метаданные и ссылку на родительский коммит. Его +хеш зависит от содержимого. Ветка — подвижное имя, указывающее на коммит; `HEAD` +указывает на текущую ветку или непосредственно на коммит. + +```text +A---B---C main + \ + D---E experiment +``` + +Здесь обе ветки разделяют историю до `B`. Переключение ветки меняет снимок в +рабочей директории, а слияние объединяет линии разработки. + +## Базовый цикл с осмысленным diff + +```text +git status +git diff +git add src/model.py tests/test_model.py +git diff --staged +git commit -m "Validate feature dimensions before training" +``` + +`git diff` показывает незастейдженные изменения, а `git diff --staged` — точное +содержимое будущего коммита. Проверять второй diff важно: `git add .` может +случайно включить данные, секрет или отладочный вывод. + +Хороший коммит: + +- решает одну логическую задачу; +- остаётся запускаемым или явно помечает промежуточное состояние; +- включает связанные тесты; +- объясняет причину изменения; +- не содержит постороннего форматирования. + +История — инструмент диагностики. Малые атомарные коммиты упрощают `git bisect`, +review и отмену конкретного решения. + +## Ветки и интеграция + +Создание отдельной ветки: + +```text +git switch -c feature/stable-normalization +``` + +Перед слиянием изменения проверяют тестами и чтением полного diff относительно +базы. **Merge** создаёт коммит с двумя родителями и сохраняет форму истории. +**Rebase** переносит коммиты на новую базу и переписывает их идентификаторы. + +Переписывать опубликованную общую историю опасно: коллеги уже могли построить на +ней свои ветки. Rebase удобен для локальной неопубликованной ветки; merge безопаснее +как явная интеграция общей работы. + +Конфликт — не ошибка Git, а отсутствие однозначного автоматического решения. +Нужно понять намерения обеих сторон, отредактировать файл, запустить проверки, +застейджить разрешение и продолжить операцию. Нельзя механически выбирать «ours» +или «theirs» без проверки поведения. + +## Что отслеживать + +В Git обычно помещают: + +- исходный код, тесты и небольшую документацию; +- декларации зависимостей и конфигурации; +- небольшие неизменяемые примеры данных; +- скрипты получения и преобразования данных. + +Не помещают: + +- `.venv/`, кэши, байткод и временные файлы; +- секреты и локальные `.env`; +- большие сырые датасеты, чекпойнты, модели; +- легко воспроизводимые результаты сборки. + +Пример `.gitignore`: + +```text +.venv/ +__pycache__/ +.pytest_cache/ +.env +data/raw/ +artifacts/ +*.pyc +``` + +`gitignore` действует только на неотслеживаемые файлы. Если секрет уже закоммичен, +добавление шаблона не удалит его из истории: ключ надо немедленно отозвать, затем +отдельно очищать историю с координацией команды. + +Проверка причины игнорирования: + +```text +git check-ignore -v data/raw/train.csv +``` + +## Большие данные и происхождение артефактов + +Обычный Git плохо работает с часто меняющимися бинарными файлами: каждый вариант +увеличивает историю, а содержательный строковый diff невозможен. Возможные решения: + +- объектное хранилище и файл с URI, контрольной суммой и размером; +- Git LFS для версионируемых больших бинарных объектов; +- DVC или аналог для графа данных и пайплайнов; +- скрипт скачивания для публичного неизменяемого источника. + +Контрольная сумма подтверждает байтовую идентичность: + +```python +from hashlib import sha256 +from pathlib import Path + +path = Path("data/raw/sample.csv") +digest = sha256(path.read_bytes()).hexdigest() +print(path.stat().st_size, digest) +``` + +Она не объясняет смысл данных. Поэтому полезен манифест: источник, лицензия, +время получения, схема, число строк, контрольная сумма и шаги обработки. + +## Воспроизводимый запуск как запись + +Пусть обучение запускается на коммите `c`. Минимальная запись эксперимента: + +```text +commit: 4f16c2a... +dirty: false +command: python -m src.train --config configs/base.json +data_sha256: 9e1... +python: 3.12.4 +seed: 42 +``` + +Состояние `dirty: true` означает, что рабочая директория отличалась от коммита. +Одного хеша тогда недостаточно: незакоммиченные изменения могут быть потеряны. +Для значимого результата лучше коммитить код или сохранять diff как артефакт. + +Самостоятельный фрагмент получает метаданные без сторонних библиотек: + +```python +import subprocess + +def git(*args: str) -> str: + return subprocess.check_output( + ["git", *args], text=True, encoding="utf-8" + ).strip() + +commit = git("rev-parse", "HEAD") +dirty = bool(git("status", "--porcelain")) +print({"commit": commit, "dirty": dirty}) +``` + +Запуск вне репозитория завершится ошибкой — это полезнее молчаливого значения +`unknown`, если происхождение результата обязательно. + +## Поиск причины регрессии + +История позволяет перейти от «раньше работало» к проверяемому поиску. + +1. Найти хороший и плохой коммиты. +2. На каждом кандидате запускать один и тот же детерминированный тест. +3. Делить диапазон пополам через `git bisect`. +4. После нахождения читать diff и проверять причинность. + +Тест должен иметь стабильный код возврата. Если данные, окружение или внешний API +изменились, bisect может указать не на причину, а на случайную корреляцию. + +## Разобранный сценарий + +Исследователь меняет нормализацию и получает лучшую метрику. + +1. `git status` показывает только ожидаемые исходники и тест. +2. `git diff` подтверждает формулу и новый тест на нулевую дисперсию. +3. Конфигурация запуска тоже версионируется. +4. Большой чекпойнт сохраняется во внешнее хранилище; рядом записываются его SHA-256 + и хеш коммита. +5. `git diff --staged` проверяется до коммита. +6. Из чистого клона создаётся окружение и повторяется команда. + +Результат считается воспроизводимым не потому, что «есть Git», а потому, что +каждый вход назван неизменяемым идентификатором и процесс автоматизирован. + +## Типичные ошибки + +- **Коммит результата без кода запуска.** Артефакт нельзя объяснить или повторить. +- **Огромный коммит.** Review не отделяет изменение поведения от шума. +- **Слепой `git add .`.** В историю попадают секреты и локальные файлы. +- **Хеш коммита при dirty-состоянии.** Хеш описывает не фактически исполненный код. +- **Хранение больших бинарников в Git.** Репозиторий растёт навсегда. +- **Force-push общей ветки.** Идентификаторы переписываются, ссылки ломаются. +- **«Одинаковый seed значит воспроизводимость».** Версии библиотек, данные, + аппаратные ядра и порядок операций также влияют на результат. + +## Краткое резюме + +- Git хранит снимки; индекс отделяет текущую работу от следующего коммита. +- Diff до и после staging — обязательная проверка содержимого. +- Ветки именуют линии истории; rebase переписывает коммиты, merge интегрирует их. +- Большие данные хранят отдельно, но связывают с кодом контрольными суммами. +- Результат воспроизводим только при зафиксированных коде, данных, среде и запуске. + + +## Связанные материалы + +- Предыдущая глава: [`01_python_environments.md`](01_python_environments.md). +- Следующая глава: [`03_jupyter_seeds_logging.md`](03_jupyter_seeds_logging.md). +- Практика: [`../assignments/02_git.md`](../assignments/02_git.md). +- Термины: [`glossary.md`](glossary.md). diff --git a/modules/00_environment/notes/03_jupyter_seeds_logging.md b/modules/00_environment/notes/03_jupyter_seeds_logging.md new file mode 100644 index 0000000..4fe6645 --- /dev/null +++ b/modules/00_environment/notes/03_jupyter_seeds_logging.md @@ -0,0 +1,235 @@ +# 03. Jupyter и модули: seed, конфигурация и логирование + +> Отделяем исследование от повторяемого вычисления и сохраняем контекст каждого запуска. + +## Мотивация + +Ноутбук позволяет быстро соединить код, графики и комментарии. Но его видимый +порядок ячеек не равен порядку исполнения: состояние ядра может содержать старые +переменные. В результате ноутбук работает у автора и падает после `Restart & Run All`. + +Повторяемый эксперимент должен иметь явные входы, один путь исполнения и журнал, +по которому можно восстановить условия и ход работы. + +## Ноутбук и модуль решают разные задачи + +Ноутбук удобен для: + +- первичного просмотра данных; +- интерактивных графиков и объяснений; +- коротких одноразовых гипотез. + +Модуль `.py` удобен для: + +- повторно используемых функций и классов; +- тестирования и статического анализа; +- запуска из командной строки и автоматизации; +- ясного порядка исполнения. + +Практический цикл: гипотеза возникает в ноутбуке, устойчивая логика переносится +в `src/`, покрывается тестом, а ноутбук импортирует её. Ноутбук остаётся тонким +отчётом, а не единственным местом реализации. + +## Скрытое состояние Jupyter + +Ядро — долгоживущий процесс. Ячейка изменяет его состояние: + +```python +scale = 10 +values = [1, 2, 3] +``` + +Если позже изменить первую ячейку, но не выполнить её снова, нижняя ячейка увидит +старое значение. Номер исполнения `[17]` показывает фактический порядок, но после +передачи файла читатель обычно идёт сверху вниз. + +Критерий исправного ноутбука: + +1. перезапустить ядро; +2. выполнить все ячейки сверху вниз; +3. получить все результаты без ручного вмешательства; +4. повторить запуск в чистом окружении. + +Нельзя полагаться на переменную, созданную удалённой ячейкой, локальный путь или +ручное исправление DataFrame в памяти. + +## Чистые функции и явные границы + +**Чистая функция** возвращает результат, определяемый аргументами, и не изменяет +внешнее состояние. Работа с файлами, временем, сетью и генератором случайных чисел +— побочные эффекты. Их полезно изолировать на краях программы. + +```python +def standardize(values: list[float], mean: float, std: float) -> list[float]: + if std <= 0: + raise ValueError("std должен быть положительным") + return [(x - mean) / std for x in values] + +print(standardize([10.0, 12.0], mean=11.0, std=1.0)) +``` + +Чем меньше неявных зависимостей, тем проще тест и повторный запуск. + +## Случайность и seed + +Псевдослучайный генератор детерминирован: внутреннее состояние и алгоритм задают +последовательность. **Seed** инициализирует состояние. Он не «делает числа +неслучайными», а позволяет повторить ту же последовательность при тех же условиях. + +Современный NumPy предпочитает локальный генератор: + +```python +import numpy as np + +rng = np.random.default_rng(42) +sample = rng.normal(loc=0.0, scale=1.0, size=5) +print(sample) +``` + +Передача генератора делает зависимость явной: + +```python +import numpy as np + +def bootstrap_mean(x: np.ndarray, rng: np.random.Generator) -> float: + indices = rng.integers(0, len(x), size=len(x)) + return float(x[indices].mean()) + +x = np.array([1.0, 2.0, 5.0, 8.0]) +rng = np.random.default_rng(7) +print(bootstrap_mean(x, rng)) +``` + +Глобальный `np.random.seed` влияет на общий генератор процесса. Любой лишний вызов +сдвигает последовательность, поэтому удалённая функция может незаметно изменить +результат. Независимые локальные генераторы уменьшают связанность. + +## Границы детерминизма + +Одинаковый seed не гарантирует побитово одинаковый результат, если различаются: + +- версии генератора или библиотеки; +- число потоков и порядок параллельных операций; +- GPU, драйверы и недетерминированные ядра; +- порядок чтения файлов или элементов множества; +- платформа и точность вычислений. + +Сложение чисел с плавающей точкой не ассоциативно: +\((a+b)+c\) может отличаться от \(a+(b+c)\) из-за округления. Поэтому для многих +ML-экспериментов правильная цель — статистическая воспроизводимость в заданном +допуске, а не побитовое равенство. + +## Конфигурация как вход + +Параметры не должны быть разбросаны по ячейкам. Соберём их в неизменяемый объект: + +```python +from dataclasses import asdict, dataclass +import json + +@dataclass(frozen=True) +class Config: + seed: int = 42 + learning_rate: float = 0.01 + epochs: int = 20 + +config = Config() +print(json.dumps(asdict(config), ensure_ascii=False, indent=2)) +``` + +Конфигурацию валидируют до дорогого запуска. Значения по умолчанию должны быть +безопасными, а итоговая разрешённая конфигурация — сохраняться вместе с результатом. +Это важно, если аргумент командной строки переопределяет файл. + +## Логирование вместо случайных print + +Лог — структурированная временная последовательность событий. Уровни: + +- `DEBUG` — детали диагностики; +- `INFO` — нормальные этапы процесса; +- `WARNING` — необычное, но допустимое состояние; +- `ERROR` — операция не выполнена; +- `CRITICAL` — продолжение процесса невозможно. + +```python +import logging + +logging.basicConfig( + level=logging.INFO, + format="%(asctime)s %(levelname)s %(name)s %(message)s", +) +logger = logging.getLogger("train") + +for epoch, loss in enumerate([1.2, 0.8, 0.55], start=1): + logger.info("epoch=%d loss=%.4f", epoch, loss) +``` + +Шаблон и аргументы передаются раздельно: форматирование не выполняется, если +сообщение отфильтровано. В библиотечном коде обычно не настраивают корневой logger; +настройка принадлежит точке входа приложения. + +Полезно писать поля `run_id`, commit, seed, хеш данных, эпоху и метрику. Секреты, +персональные данные и полный содержимый датасет в лог не помещают. + +## Разобранный воспроизводимый запуск + +```python +from dataclasses import dataclass +import json +import logging +from pathlib import Path +import numpy as np + +@dataclass(frozen=True) +class Config: + seed: int = 11 + n: int = 1000 + +def run(config: Config) -> dict[str, float]: + rng = np.random.default_rng(config.seed) + x = rng.normal(size=config.n) + return {"mean": float(x.mean()), "std": float(x.std())} + +logging.basicConfig(level=logging.INFO, format="%(levelname)s %(message)s") +config = Config() +metrics = run(config) +record = {"config": config.__dict__, "metrics": metrics} + +Path("artifacts").mkdir(exist_ok=True) +Path("artifacts/run.json").write_text( + json.dumps(record, indent=2), encoding="utf-8" +) +logging.info("result=%s", metrics) +``` + +Здесь случайность локальна, конфигурация явна, вычислительная функция возвращает +данные, а запись артефакта отделена. Для полной воспроизводимости в запись также +добавляют версии, Git-коммит и идентификатор данных. + +## Типичные ошибки и ограничения + +- **Ноутбук исполняется только в накопленном состоянии.** Проверяйте чистый запуск. +- **Seed ставится внутри каждой функции.** Все вызовы повторяют один и тот же + поток. Передавайте один генератор или создавайте независимые потоки намеренно. +- **Конфигурация меняется после старта.** Журнал уже не описывает фактический запуск. +- **Все сообщения имеют уровень `INFO`.** Диагностика становится шумной и дорогой. +- **Исключение проглатывается.** Лог без повторного возбуждения ошибки может создать + видимость успешного результата. +- **В лог попадают токены.** Логи часто хранятся дольше и доступны шире кода. +- **Побитовое равенство объявлено обязательным без причины.** Оно может потребовать + более медленных детерминированных алгоритмов. + +## Краткое резюме + +- Ноутбук предназначен для исследования, модуль — для повторного использования. +- Исправный ноутбук выполняется сверху вниз после перезапуска ядра. +- Локальный генератор NumPy делает поток случайности явной зависимостью. +- Итоговую конфигурацию, метаданные среды и метрики нужно сохранять вместе. +- Логи объясняют ход запуска, но не должны содержать секреты и лишние данные. + + +## Связанные материалы + +- Предыдущая глава: [`02_git_reproducibility.md`](02_git_reproducibility.md). +- Практика: [`../assignments/03_reproducibility.md`](../assignments/03_reproducibility.md). +- Термины: [`glossary.md`](glossary.md). diff --git a/modules/00_environment/notes/glossary.md b/modules/00_environment/notes/glossary.md new file mode 100644 index 0000000..6015139 --- /dev/null +++ b/modules/00_environment/notes/glossary.md @@ -0,0 +1,28 @@ +# Глоссарий модуля 00 + +- **Артефакт** — сохранённый результат процесса: модель, метрики, график или отчёт. +- **Ветка Git** — подвижная ссылка на последний коммит линии разработки. +- **Виртуальное окружение** — изолированный интерпретатор с собственным набором пакетов. +- **Воспроизводимость** — возможность получить согласованный результат по описанным + коду, данным, среде и процедуре. +- **Дистрибутив Python** — единица, устанавливаемая менеджером пакетов. +- **Индекс Git (staging area)** — подготовленный снимок следующего коммита. +- **Конфигурация** — явный набор изменяемых параметров запуска. +- **Контрольная сумма** — короткий хеш содержимого для проверки байтовой идентичности. +- **Коммит** — неизменяемый снимок дерева файлов с метаданными и родителем. +- **Логирование** — запись событий выполнения с уровнями, временем и контекстом. +- **Модуль Python** — импортируемая единица кода, обычно файл `.py`. +- **Ноутбук** — документ с ячейками кода, текста и результатов, исполняемый ядром. +- **Пакет Python** — устанавливаемый или импортируемый набор модулей. +- **Прямая зависимость** — библиотека, которую проект использует непосредственно. +- **Псевдослучайный генератор** — детерминированный алгоритм, создающий + последовательность, похожую на случайную. +- **Рабочая директория Git** — текущее состояние файлов, доступное редактору. +- **Rebase** — перенос коммитов на другую базу с созданием новых идентификаторов. +- **Seed** — значение для инициализации состояния псевдослучайного генератора. +- **Транзитивная зависимость** — пакет, необходимый другой зависимости. +- **Чистая функция** — функция, результат которой определяется аргументами и которая + не изменяет внешнее состояние. +- **`diff`** — представление различий между двумя состояниями файлов. +- **`HEAD`** — указатель на текущий коммит или текущую ветку Git. +- **Lock-файл** — машинно создаваемая фиксация точных версий и разрешённых зависимостей. diff --git a/modules/01_python_numpy/README.md b/modules/01_python_numpy/README.md new file mode 100644 index 0000000..f9090fd --- /dev/null +++ b/modules/01_python_numpy/README.md @@ -0,0 +1,36 @@ +# Модуль 01. Python и NumPy для вычислений + +Плановый срок: **недели 2–3**. + +## Цели + +- писать чистый Python-код для обработки данных +- понимать формы, broadcasting, типы и векторизацию NumPy +- оценивать корректность и скорость вычислений + +## Предварительные знания + +Пройдите модуль 00; нужны рабочее Python-окружение, базовый синтаксис Python и умение запускать тесты. + +## Теория + +1. [Функции, классы, итераторы, comprehensions и исключения](notes/01_python_data_model.md) +2. [Массивы NumPy: shape, axis, dtype, indexing и broadcasting](notes/02_numpy_arrays.md) +3. [Векторизация, сложность, профилирование и модульные тесты](notes/03_vectorization_testing.md) + +Термины модуля: [`notes/glossary.md`](notes/glossary.md). + +Для каждого урока: прочитайте теорию, воспроизведите ключевой пример своими +руками и запишите три тезиса без подсказки. Формулы из этого модуля нужно +уметь связать с кодом и наблюдаемым поведением модели. + +## Задания + +- [`assignments/01_vectorization.md`](assignments/01_vectorization.md) — Векторизация расстояний +- [`assignments/02_linear_layer.md`](assignments/02_linear_layer.md) — Линейный слой с нуля +- [`assignments/03_clean_code.md`](assignments/03_clean_code.md) — Мини-библиотека метрик + +## Завершение модуля + +Модуль завершён, когда все задания удовлетворяют критериям приёмки, +вы можете объяснить основные решения без ноутбука и получили review. diff --git a/modules/01_python_numpy/assignments/01_vectorization.md b/modules/01_python_numpy/assignments/01_vectorization.md new file mode 100644 index 0000000..abed450 --- /dev/null +++ b/modules/01_python_numpy/assignments/01_vectorization.md @@ -0,0 +1,42 @@ +# Векторизация расстояний + +## Теория перед выполнением + +Перед началом прочитайте: + +- [`02_numpy_arrays.md`](../notes/02_numpy_arrays.md) +- [`03_vectorization_testing.md`](../notes/03_vectorization_testing.md) + + +## Цель + +Реализовать попарные евклидовы расстояния сначала циклами, затем NumPy без циклов. + +## Что сделать + +1. Сформулировать ожидаемый результат до запуска эксперимента. +2. Реализовать простой baseline. +3. Реализовать основной метод самостоятельно настолько, насколько требует задание. +4. Добавить автоматические проверки ключевых вычислений. +5. Провести эксперимент, сохранить метрики и примеры ошибок. +6. Заполнить отчёт по `common/REPORT_TEMPLATE.md`. + +## Результат + +Обе реализации, тесты равенства и измерение времени. + +Решение разместите рядом в `solution/`. Не коммитьте большие данные, +веса или секреты. + +## Критерии приёмки + +- Результаты совпадают с допуском; векторная версия быстрее на большой матрице. +- Код читаем, разбит на функции и запускается по инструкции. +- Выводы подтверждаются измерениями и не выходят за пределы эксперимента. + +## Вопросы для самопроверки + +- Почему выбран именно этот метод и какой baseline он должен превзойти? +- Где здесь возможна утечка данных или некорректная оценка? +- Как изменится результат при других данных или ограничениях? +- Какую ошибку модели вы считаете наиболее важной и почему? diff --git a/modules/01_python_numpy/assignments/02_linear_layer.md b/modules/01_python_numpy/assignments/02_linear_layer.md new file mode 100644 index 0000000..7a27b9a --- /dev/null +++ b/modules/01_python_numpy/assignments/02_linear_layer.md @@ -0,0 +1,41 @@ +# Линейный слой с нуля + +## Теория перед выполнением + +Перед началом прочитайте: + +- [`02_numpy_arrays.md`](../notes/02_numpy_arrays.md) + + +## Цель + +Реализовать `y = XW + b` для batch-входа с проверкой форм. + +## Что сделать + +1. Сформулировать ожидаемый результат до запуска эксперимента. +2. Реализовать простой baseline. +3. Реализовать основной метод самостоятельно настолько, насколько требует задание. +4. Добавить автоматические проверки ключевых вычислений. +5. Провести эксперимент, сохранить метрики и примеры ошибок. +6. Заполнить отчёт по `common/REPORT_TEMPLATE.md`. + +## Результат + +Класс `Linear`, тесты форм и численных значений. + +Решение разместите рядом в `solution/`. Не коммитьте большие данные, +веса или секреты. + +## Критерии приёмки + +- Поддерживаются разные batch size; ошибочные формы дают понятное исключение. +- Код читаем, разбит на функции и запускается по инструкции. +- Выводы подтверждаются измерениями и не выходят за пределы эксперимента. + +## Вопросы для самопроверки + +- Почему выбран именно этот метод и какой baseline он должен превзойти? +- Где здесь возможна утечка данных или некорректная оценка? +- Как изменится результат при других данных или ограничениях? +- Какую ошибку модели вы считаете наиболее важной и почему? diff --git a/modules/01_python_numpy/assignments/03_clean_code.md b/modules/01_python_numpy/assignments/03_clean_code.md new file mode 100644 index 0000000..9e34c2d --- /dev/null +++ b/modules/01_python_numpy/assignments/03_clean_code.md @@ -0,0 +1,42 @@ +# Мини-библиотека метрик + +## Теория перед выполнением + +Перед началом прочитайте: + +- [`01_python_data_model.md`](../notes/01_python_data_model.md) +- [`03_vectorization_testing.md`](../notes/03_vectorization_testing.md) + + +## Цель + +Реализовать accuracy, precision, recall, F1 и confusion matrix без sklearn. + +## Что сделать + +1. Сформулировать ожидаемый результат до запуска эксперимента. +2. Реализовать простой baseline. +3. Реализовать основной метод самостоятельно настолько, насколько требует задание. +4. Добавить автоматические проверки ключевых вычислений. +5. Провести эксперимент, сохранить метрики и примеры ошибок. +6. Заполнить отчёт по `common/REPORT_TEMPLATE.md`. + +## Результат + +Модуль `metrics.py`, pytest-тесты и примеры. + +Решение разместите рядом в `solution/`. Не коммитьте большие данные, +веса или секреты. + +## Критерии приёмки + +- Учтено деление на ноль; результаты сверены со sklearn. +- Код читаем, разбит на функции и запускается по инструкции. +- Выводы подтверждаются измерениями и не выходят за пределы эксперимента. + +## Вопросы для самопроверки + +- Почему выбран именно этот метод и какой baseline он должен превзойти? +- Где здесь возможна утечка данных или некорректная оценка? +- Как изменится результат при других данных или ограничениях? +- Какую ошибку модели вы считаете наиболее важной и почему? diff --git a/modules/01_python_numpy/notes/01_python_data_model.md b/modules/01_python_numpy/notes/01_python_data_model.md new file mode 100644 index 0000000..f79dd1b --- /dev/null +++ b/modules/01_python_numpy/notes/01_python_data_model.md @@ -0,0 +1,245 @@ +# 01. Функции, классы, итераторы, comprehensions и исключения + +> Понимаем объектную модель Python и строим небольшие предсказуемые компоненты для вычислений. + +## Мотивация + +ML-код редко ограничивается формулой. Нужно загрузить данные, проверить формы, +пройти по батчам, представить параметры и сообщить об ошибках. Ошибки Python часто +возникают не из синтаксиса, а из неверной модели объектов: общий изменяемый список, +неожиданная ссылка, однократно исчерпанный итератор. + +## Имена, объекты и идентичность + +Переменная Python — имя, ссылающееся на объект. Присваивание не копирует объект: + +```python +a = [1, 2] +b = a +b.append(3) +print(a) # [1, 2, 3] +print(a is b) # True: один объект +print(a == b) # True: равные значения +``` + +`is` сравнивает идентичность, `==` — значения. Для `None` используют +`value is None`, но числа и строки сравнивают через `==`. + +Объекты бывают **изменяемыми** (`list`, `dict`, `set`) и **неизменяемыми** +(`int`, `float`, `str`, `tuple`). Неизменяемость tuple не делает неизменяемыми +вложенные объекты. + +```python +row = ([1, 2], "train") +row[0].append(3) +print(row) +``` + +## Тип, протокол и специальный метод + +Тип определяет поведение объекта. Python часто интересует не имя класса, а +поддерживаемый **протокол**: можно ли объект итерировать, получить длину, вызвать. +Специальные методы связывают синтаксис с поведением: + +- `len(x)` вызывает `x.__len__()`; +- `x[i]` — `x.__getitem__(i)`; +- `for item in x` использует протокол итерации; +- `x + y` обращается к `__add__`; +- `x(...)` — к `__call__`. + +Это основа «duck typing»: если объект предоставляет нужное поведение, его можно +использовать независимо от наследования. + +## Функции как объекты + +Функция принимает ссылки на объекты и возвращает ссылку. Аргументы передаются +через присваивание локальным именам: + +```python +def add_bias(values: list[float], bias: float = 0.0) -> list[float]: + """Возвращает новый список, не меняя вход.""" + return [x + bias for x in values] + +transform = add_bias +print(transform([1.0, 2.0], bias=0.5)) +``` + +Аннотации типов документируют контракт и помогают анализаторам, но Python сам +обычно не проверяет их во время выполнения. + +Опасная граница — изменяемый аргумент по умолчанию: он создаётся один раз. + +```python +def collect(value: int, bucket: list[int] | None = None) -> list[int]: + if bucket is None: + bucket = [] + bucket.append(value) + return bucket +``` + +`*args` собирает позиционные аргументы в tuple, `**kwargs` — именованные в dict. +Их стоит применять там, где открытый контракт действительно нужен, а не для +сокрытия неясной сигнатуры. + +## Области видимости и замыкания + +Имя ищется по правилу LEGB: local, enclosing, global, built-ins. Вложенная функция +может удерживать значения внешней области: + +```python +def make_scaler(scale: float): + def scale_values(values: list[float]) -> list[float]: + return [scale * x for x in values] + return scale_values + +double = make_scaler(2.0) +print(double([1.5, 3.0])) +``` + +Замыкание хранит ссылку, а не снимок имени. При создании функций в цикле позднее +связывание может дать всем функциям последнее значение. Его фиксируют аргументом +по умолчанию или отдельной фабрикой. + +## Классы и экземпляры + +Класс объединяет состояние и инварианты. Экземпляр хранит собственные атрибуты, +а методы класса становятся связанными методами, автоматически получающими `self`. + +```python +from dataclasses import dataclass + +@dataclass(frozen=True) +class Standardizer: + mean: float + std: float + + def __post_init__(self) -> None: + if self.std <= 0: + raise ValueError("std должен быть положительным") + + def transform(self, x: float) -> float: + return (x - self.mean) / self.std + +model = Standardizer(mean=10.0, std=2.0) +print(model.transform(13.0)) +``` + +`dataclass` генерирует инициализацию, сравнение и представление. `frozen=True` +запрещает обычное изменение полей и уменьшает число скрытых состояний. Это не +глубокая неизменяемость: изменяемое поле всё ещё может менять содержимое. + +**Атрибут класса** общий, атрибут экземпляра принадлежит объекту. Изменяемый +атрибут класса почти всегда опасен для состояния экземпляров. + +Наследование выражает отношение «является», композиция — «содержит». Для +вычислительных компонентов композиция часто прозрачнее: нормализатор может +содержать стратегию проверки, не становясь её подклассом. + +## Итераторы и генераторы + +**Итерируемый объект** возвращает итератор через `iter(x)`. **Итератор** возвращает +следующий элемент через `next(x)` и при завершении возбуждает `StopIteration`. +Итератор обычно одноразовый. + +```python +values = [10, 20, 30] +iterator = iter(values) +print(next(iterator)) +print(list(iterator)) # [20, 30] +print(list(iterator)) # [] +``` + +Генераторная функция с `yield` сохраняет локальное состояние между запросами: + +```python +def batches(values: list[int], size: int): + if size <= 0: + raise ValueError("size должен быть положительным") + for start in range(0, len(values), size): + yield values[start:start + size] + +for batch in batches(list(range(7)), 3): + print(batch) +``` + +Ленивость снижает память с \(O(n)\) до размера текущего элемента или батча, но +ошибка может проявиться только во время потребления генератора. + +## Comprehensions + +Списковое включение выражает отображение и фильтрацию: + +```python +squares_of_even = [x * x for x in range(10) if x % 2 == 0] +index = {name: i for i, name in enumerate(["age", "income"])} +unique_lengths = {len(word) for word in ["cat", "tree", "dog"]} +``` + +Порядок читается как «выражение для каждого элемента при условии». Для сложной +ветвящейся логики обычный цикл яснее. Круглые скобки создают генераторное выражение: +`sum(x*x for x in values)` не строит промежуточный список. + +## Исключения как часть контракта + +Исключение сообщает, что функция не может выполнить обещание. Проверяйте условия +на границе и используйте конкретный тип: + +```python +def mean(values: list[float]) -> float: + if not values: + raise ValueError("Нельзя вычислить среднее пустой последовательности") + return sum(values) / len(values) + +try: + print(mean([])) +except ValueError as error: + print(f"Некорректные данные: {error}") +``` + +`TypeError` подходит неверному виду объекта, `ValueError` — неверному значению +правильного типа, `KeyError` — отсутствующему ключу. Ловить голый `except:` опасно: +он перехватывает сигналы завершения. `except Exception` тоже должен использоваться +только на границе приложения, обычно с логированием и понятным завершением. + +Конструкция `with` гарантирует освобождение ресурса: + +```python +from pathlib import Path + +with Path("metrics.txt").open("w", encoding="utf-8") as stream: + stream.write("loss=0.125\n") +``` + +## Сложность базовых контейнеров + +Средняя стоимость `list.append` — амортизированно \(O(1)\), поиск в list — +\(O(n)\), доступ по индексу — \(O(1)\). Поиск ключа в dict и элемента в set +в среднем \(O(1)\), но требует хешируемого ключа. Неизменяемость — не единственное +условие хешируемости, однако изменяемые встроенные контейнеры не хешируются. + +## Типичные ошибки + +- **Путать `is` и `==`.** Идентичность не равна равенству значений. +- **Нечаянный общий объект.** Делайте явную копию, если функция обязана владеть + данными; помните, что `copy()` неглубокая. +- **Изменяемый аргумент по умолчанию.** Используйте `None` и создавайте объект внутри. +- **Повторное чтение генератора.** Материализуйте его один раз, если нужен повтор. +- **Слишком широкий `except`.** Он скрывает дефекты программирования. +- **Класс без инвариантов.** Если есть только простое преобразование без состояния, + функция может быть яснее. +- **Аннотации принимаются за runtime-проверку.** Валидируйте внешние данные явно. + +## Краткое резюме + +- Имена ссылаются на объекты; присваивание не означает копирование. +- Протоколы и специальные методы определяют поведение объектов Python. +- Функции и классы должны иметь ясные контракты и проверяемые инварианты. +- Итераторы ленивы и обычно одноразовы; генераторы экономят память. +- Конкретные исключения и контекстные менеджеры делают сбои и ресурсы явными. + + +## Связанные материалы + +- Следующая глава: [`02_numpy_arrays.md`](02_numpy_arrays.md). +- Практика: [`../assignments/03_clean_code.md`](../assignments/03_clean_code.md). +- Термины: [`glossary.md`](glossary.md). diff --git a/modules/01_python_numpy/notes/02_numpy_arrays.md b/modules/01_python_numpy/notes/02_numpy_arrays.md new file mode 100644 index 0000000..edb3722 --- /dev/null +++ b/modules/01_python_numpy/notes/02_numpy_arrays.md @@ -0,0 +1,292 @@ +# 02. Массивы NumPy: shape, axis, dtype, indexing и broadcasting + +> NumPy хранит однородные данные в многомерной форме и выполняет операции над целыми осями. + +## Мотивация + +В ML размерности несут смысл: строки могут быть объектами, столбцы — признаками, +третья ось — каналами. Многие ошибки NumPy не завершаются исключением: broadcasting +строит допустимый массив неправильного смысла. Поэтому форму и оси нужно читать +как часть типа. + +## Модель `ndarray` + +Массив NumPy описывается: + +- `shape` — длиной каждой оси; +- `ndim` — числом осей; +- `dtype` — типом каждого элемента; +- `size` — общим числом элементов; +- `strides` — шагами в байтах при движении по осям. + +```python +import numpy as np + +X = np.array([[1.0, 2.0, 3.0], + [4.0, 5.0, 6.0]]) +print(X.shape, X.ndim, X.dtype, X.size, X.strides) +``` + +\(\mathbf{X}\in\mathbb{R}^{n\times d}\) при \(n=2,d=3\) соответствует форме +`(2, 3)`: axis 0 перечисляет объекты, axis 1 — признаки. + +Массив формы `(d,)` не является ни строкой `(1, d)`, ни столбцом `(d, 1)`. +У одномерного массива транспонирование ничего не меняет: + +```python +x = np.array([1.0, 2.0, 3.0]) +print(x.shape, x.T.shape) +print(x[:, None].shape, x[None, :].shape) +``` + +## `dtype` и численная семантика + +Однородный `dtype` позволяет хранить данные компактно и выполнять операции в +скомпилированных циклах. Но тип ограничивает диапазон и точность. + +```python +import numpy as np + +a = np.array([120, 120], dtype=np.int8) +print(a + np.array([10, 10], dtype=np.int8)) # переполнение + +b = np.array([1, 2, 3], dtype=np.float64) +print(b / 2) +``` + +Целые типы переполняются по модулю без обязательной ошибки. `float32` занимает +меньше памяти и часто быстрее на GPU, но хранит меньше значащих цифр, чем +`float64`. Сравнивать вычисленные float через `==` обычно неверно; нужен допуск. + +`astype` создаёт преобразованный массив: + +```python +x = np.array([1.2, 2.9]) +print(x.astype(np.int64)) # усечение к нулю: [1 2] +``` + +## Создание и изменение формы + +```python +import numpy as np + +zeros = np.zeros((2, 3), dtype=np.float64) +grid = np.arange(12).reshape(3, 4) +points = np.linspace(0.0, 1.0, num=5) +print(zeros, grid, points, sep="\n") +``` + +В `reshape(3, -1)` NumPy выводит одну неизвестную длину. Общее число элементов +должно сохраняться. `ravel()` стремится вернуть view, `flatten()` всегда копирует. + +Объединение не равно добавлению оси: + +- `np.concatenate` соединяет по существующей оси; +- `np.stack` создаёт новую ось; +- `np.squeeze` удаляет только оси длины 1; +- `np.expand_dims` добавляет ось. + +## Индексирование, view и копия + +Базовый slice обычно возвращает **view** на те же данные: + +```python +import numpy as np + +x = np.arange(6) +view = x[1:4] +view[0] = 99 +print(x) # исходный массив изменился +``` + +Продвинутое индексирование списком индексов или маской обычно создаёт копию: + +```python +x = np.arange(6) +selected = x[[1, 3, 5]] +selected[0] = 99 +print(x) +``` + +Не следует строить логику только на слове «обычно»: если владение памятью важно, +создавайте явную `copy()` и проверяйте `np.shares_memory`. + +Булева маска должна соответствовать индексируемой оси: + +```python +X = np.array([[1, 10], [2, 20], [3, 30]]) +mask = X[:, 0] >= 2 +print(X[mask]) +``` + +## Оси и редукции + +Редукция уничтожает выбранную ось. Для \(\mathbf{X}\in\mathbb{R}^{n\times d}\): + +\[ +\mu_j=\frac1n\sum_{i=1}^{n}X_{ij} +\] + +усредняет axis 0 и возвращает вектор признаков формы `(d,)`. + +```python +import numpy as np + +X = np.array([[1.0, 10.0], [3.0, 14.0], [5.0, 18.0]]) +feature_means = X.mean(axis=0) +row_means = X.mean(axis=1) +print(feature_means.shape, feature_means) +print(row_means.shape, row_means) +``` + +`keepdims=True` сохраняет редуцированную ось длины 1. Это помогает сделать +последующий broadcasting намеренным: + +```python +means = X.mean(axis=0, keepdims=True) # (1, d) +centered = X - means # (n, d) +``` + +## Broadcasting: точное правило + +Формы сравниваются справа налево. Две размерности совместимы, если: + +1. они равны; или +2. одна из них равна 1; или +3. у одной формы ось отсутствует. + +Для форм `(4, 3, 2)` и `(3, 1)`: + +```text +4 3 2 + 3 1 +----- +4 3 2 +``` + +Последняя ось: `2` и `1`; следующая: `3` и `3`; ведущая отсутствует и считается +совместимой. Данные логически растягиваются без обязательного физического копирования. + +## Разобранный пример: стандартизация признаков + +Дано \(\mathbf{X}\in\mathbb{R}^{3\times2}\). Для каждого признака: + +\[ +\mu_j=\frac13\sum_{i=1}^3 X_{ij},\qquad +\sigma_j=\sqrt{\frac13\sum_{i=1}^3(X_{ij}-\mu_j)^2}, +\qquad Z_{ij}=\frac{X_{ij}-\mu_j}{\sigma_j}. +\] + +Для первого столбца `[1,3,5]`: \(\mu_1=3\), +\(\sigma_1=\sqrt{(4+0+4)/3}=\sqrt{8/3}\). +Для второго `[10,14,18]`: \(\mu_2=14\), +\(\sigma_2=\sqrt{32/3}\). + +```python +import numpy as np + +X = np.array([[1.0, 10.0], + [3.0, 14.0], + [5.0, 18.0]]) +mean = X.mean(axis=0, keepdims=True) +std = X.std(axis=0, keepdims=True) +if np.any(std == 0): + raise ValueError("Обнаружен константный признак") +Z = (X - mean) / std + +print("shapes:", X.shape, mean.shape, std.shape, Z.shape) +print("mean:", Z.mean(axis=0)) +print("std:", Z.std(axis=0)) +``` + +Формы `(3,2) - (1,2)` совместимы: строка средних применяется к каждому объекту. +Проверка результата подтверждает среднее около нуля и стандартное отклонение 1. + +## Batch-пример: линейный слой `XW + b` + +Пусть в batch \(n\) объектов, у каждого \(d\) входных признаков, а слой создаёт +\(k\) выходов: + +\[ +\mathbf X\in\mathbb R^{n\times d},\quad +\mathbf W\in\mathbb R^{d\times k},\quad +\mathbf b\in\mathbb R^k. +\] + +Тогда `X @ W` имеет форму `(n, k)`, а `b` формы `(k,)` распространяется по +строкам batch. Результат `Y = X @ W + b` также имеет форму `(n, k)`. + +```python +import numpy as np + +X = np.array([[1., 2., 3.], [4., 5., 6.]]) # (n=2, d=3) +W = np.array([[1., 0.], [0., 1.], [1., 1.]]) # (d=3, k=2) +b = np.array([10., -10.]) # (k=2,) +Y = X @ W + b # (n=2, k=2) + +assert X.shape[1] == W.shape[0] +assert b.shape == (W.shape[1],) +assert Y.shape == (X.shape[0], W.shape[1]) +print(Y) # [[14. -5.], [20. 1.]] +``` + +Форма `b=(1, k)` дала бы тот же результат, но `(k, 1)` несовместима с `(n, k)` +в общем случае и не выражает «один bias на каждый выход». Проверяйте контракт +форм явно: допустимый broadcasting ещё не доказывает правильность смысла. + +## Матричное и поэлементное умножение + +Для \(\mathbf{X}\in\mathbb{R}^{n\times d}\) и +\(\mathbf{w}\in\mathbb{R}^{d}\) выражение `X @ w` имеет форму `(n,)`. +`X * w` выполняет поэлементное умножение с broadcasting и имеет форму `(n,d)`. + +```python +X = np.array([[1.0, 2.0], [3.0, 4.0]]) +w = np.array([0.5, -1.0]) +print(X @ w) +print(X * w) +``` + +Обе операции законны, но выражают разные математики. Формы нужно выводить до запуска. + +## Пропущенные значения + +`np.nan` — специальное значение float. Обычные редукции распространяют его: +`np.mean([1, np.nan])` даёт `nan`; `np.nanmean` игнорирует его. Игнорирование +не всегда корректно статистически и должно быть явным решением. + +Проверки: + +```python +x = np.array([1.0, np.nan, np.inf]) +print(np.isnan(x)) +print(np.isfinite(x)) +``` + +## Типичные ошибки + +- **Перепутанная axis.** Подпишите смысл каждой размерности до редукции. +- **Ожидание, что `x.T` превратит `(d,)` в столбец.** Используйте `x[:, None]`. +- **Случайный broadcasting `(n,1)` с `(n,)`.** Результат станет `(n,n)`. +- **Изменение view.** Делайте `copy()`, если нужен независимый массив. +- **Целочисленное переполнение.** Выбирайте диапазон dtype осознанно. +- **`np.empty` принимается за нули.** Он содержит неинициализированную память. +- **Сравнение float через `==`.** Используйте `np.isclose`/`np.allclose`. +- **Поэлементное `*` вместо `@`.** Проверяйте математический смысл и форму. + +## Краткое резюме + +- `shape`, `axis` и `dtype` являются частью контракта массива. +- `(d,)`, `(d,1)` и `(1,d)` — разные формы. +- Редукция убирает ось; `keepdims=True` сохраняет её для явного broadcasting. +- Broadcasting сопоставляет формы справа налево и может скрывать смысловую ошибку. +- Базовый slice часто разделяет память, продвинутое индексирование обычно копирует. + + +## Связанные материалы + +- Предыдущая глава: [`01_python_data_model.md`](01_python_data_model.md). +- Следующая глава: [`03_vectorization_testing.md`](03_vectorization_testing.md). +- Практика: [`../assignments/01_vectorization.md`](../assignments/01_vectorization.md). +- Практика: [`../assignments/02_linear_layer.md`](../assignments/02_linear_layer.md). +- Термины: [`glossary.md`](glossary.md). diff --git a/modules/01_python_numpy/notes/03_vectorization_testing.md b/modules/01_python_numpy/notes/03_vectorization_testing.md new file mode 100644 index 0000000..f1221a6 --- /dev/null +++ b/modules/01_python_numpy/notes/03_vectorization_testing.md @@ -0,0 +1,246 @@ +# 03. Векторизация, сложность, профилирование и модульные тесты + +> Ускоряем вычисления переносом циклов в NumPy и доказываем корректность тестами, а скорость — измерением. + +## Мотивация + +Python-цикл выполняет динамическую диспетчеризацию для каждого элемента. NumPy +передаёт целую операцию оптимизированному машинному коду. Но векторизованный код +может создавать огромные промежуточные массивы или быстро вычислять неверный ответ. +Оптимизация требует трёх отдельных рассуждений: корректность, асимптотика и +реальные измерения. + +## Векторизация + +**Векторизация** выражает операцию над массивом целиком, а не явным Python-циклом. + +```python +import numpy as np + +x = np.arange(1_000_000, dtype=np.float64) +y = 2.0 * x + 1.0 +print(y[:3]) +``` + +Цикл всё равно существует, но выполняется внутри NumPy над непрерывной памятью, +часто с SIMD и без создания Python-объекта для каждого числа. + +Векторизация не означает применение `np.vectorize`: это удобная оболочка над +Python-функцией и обычно не даёт машинного ускорения. + +## Разобранный пример: попарные квадраты расстояний + +Пусть \(\mathbf{X}\in\mathbb{R}^{n\times d}\), +\(\mathbf{Y}\in\mathbb{R}^{m\times d}\). Нужно + +\[ +D_{ij}=\|\mathbf{x}^{(i)}-\mathbf{y}^{(j)}\|_2^2. +\] + +Прямой broadcasting: + +```python +diff = X[:, None, :] - Y[None, :, :] # (n, m, d) +D = np.sum(diff * diff, axis=2) # (n, m) +``` + +Он создаёт промежуточный массив из \(nmd\) элементов. Раскроем квадрат: + +\[ +\begin{aligned} +\|\mathbf{x}-\mathbf{y}\|_2^2 +&=(\mathbf{x}-\mathbf{y})^\top(\mathbf{x}-\mathbf{y})\\ +&=\mathbf{x}^\top\mathbf{x} + -2\mathbf{x}^\top\mathbf{y} + +\mathbf{y}^\top\mathbf{y}. +\end{aligned} +\] + +Тогда: + +```python +import numpy as np + +X = np.array([[0.0, 0.0], [1.0, 2.0]]) +Y = np.array([[1.0, 0.0], [3.0, 2.0], [-1.0, 1.0]]) + +x2 = np.sum(X * X, axis=1, keepdims=True) # (n, 1) +y2 = np.sum(Y * Y, axis=1, keepdims=True).T # (1, m) +D = x2 - 2.0 * (X @ Y.T) + y2 # (n, m) +D = np.maximum(D, 0.0) # убрать малые отрицательные ошибки округления +print(D) +``` + +Время обоих способов имеет порядок \(O(nmd)\), потому что нужны все скалярные +произведения. Но второй использует матричное умножение и память +\(O(nm+nd+md)\), а не промежуточные \(O(nmd)\). + +## Сложность и скрытые константы + +Асимптотика описывает рост: + +- один проход по \(n\) элементам — \(O(n)\); +- матрица \(n\times d\), умноженная на \(d\times m\), — обычно \(O(ndm)\); +- хранение матрицы \(n\times d\) — \(O(nd)\). + +Два алгоритма класса \(O(n)\) могут сильно различаться из-за констант, размещения +в памяти и векторизации. Для малого массива накладные расходы NumPy могут быть +больше простого цикла. Big-O не измеряет секунды и не заменяет benchmark. + +Особенно опасны временные массивы: + +```python +result = np.sqrt((X * X).sum(axis=1)) +``` + +`X * X` занимает память размера `X`. `np.linalg.norm(X, axis=1)` яснее и может +быть реализован эффективнее, но конкретное поведение проверяют измерением. + +## Профилирование + +Сначала измеряют end-to-end время и проверяют корректность. Затем находят горячую +функцию профилировщиком и только после этого оптимизируют. + +Для короткого чистого фрагмента: + +```python +import timeit +import numpy as np + +x = np.arange(100_000, dtype=np.float64) +t = timeit.timeit("np.square(x)", number=100, globals={"np": np, "x": x}) +print(f"{t / 100:.6f} s per call") +``` + +Правила benchmark: + +- прогреть код и выполнить несколько повторов; +- не включать случайную загрузку файлов, если измеряется ядро; +- использовать одинаковые входы; +- сообщать размер, dtype, версии и оборудование; +- смотреть распределение времени, а не один запуск; +- отдельно оценивать пиковую память. + +Профиль показывает место затрат, но не объясняет алгоритм. Ускорение в 2 раза +бесполезно, если выбранное представление требует \(O(n^2)\) памяти и не масштабируется. + +## Контракт и модульный тест + +Тест проверяет наблюдаемое поведение маленькой единицы. Хороший численный тест +задаёт: + +- вход и ожидаемые формы; +- известный результат; +- допустимую численную погрешность; +- ошибочные и граничные входы; +- отсутствие нежелательной мутации. + +Реализация: + +```python +import numpy as np + +def squared_distances(X: np.ndarray, Y: np.ndarray) -> np.ndarray: + if X.ndim != 2 or Y.ndim != 2: + raise ValueError("X и Y должны быть двумерными") + if X.shape[1] != Y.shape[1]: + raise ValueError("Число признаков должно совпадать") + if not np.issubdtype(X.dtype, np.number) or not np.issubdtype(Y.dtype, np.number): + raise TypeError("Массивы должны быть числовыми") + + x2 = np.sum(X * X, axis=1, keepdims=True) + y2 = np.sum(Y * Y, axis=1, keepdims=True).T + return np.maximum(x2 - 2 * (X @ Y.T) + y2, 0) +``` + +Тесты с pytest: + +```python +import numpy as np +import pytest + +def test_squared_distances_known_values(): + X = np.array([[0.0, 0.0], [1.0, 0.0]]) + Y = np.array([[0.0, 2.0]]) + actual = squared_distances(X, Y) + expected = np.array([[4.0], [5.0]]) + np.testing.assert_allclose(actual, expected, rtol=1e-12, atol=1e-12) + assert actual.shape == (2, 1) + +def test_squared_distances_rejects_feature_mismatch(): + with pytest.raises(ValueError, match="признаков"): + squared_distances(np.zeros((2, 3)), np.zeros((4, 2))) +``` + +## Допуски численного сравнения + +Проверка приблизительного равенства использует идею: + +\[ +|a-b|\leq \text{atol}+\text{rtol}\,|b|. +\] + +`atol` важен около нуля, `rtol` — для больших величин. Допуски выбирают из +масштаба задачи и накопления ошибок, а не увеличивают до прохождения теста. +Для целых и категорий обычно требуется точное равенство. + +## Эталонная и оптимизированная реализации + +Простой цикл полезен как независимый оракул для малых случаев: + +```python +def distances_reference(X, Y): + out = np.empty((len(X), len(Y)), dtype=float) + for i, x in enumerate(X): + for j, y in enumerate(Y): + delta = x - y + out[i, j] = delta @ delta + return out +``` + +Случайные малые тесты сравнивают оптимизированную версию с эталоном. Но тест должен +использовать фиксированный seed и обязательно включать вручную вычисленный пример: +две реализации могут разделять одну ошибочную идею. + +## Граничные случаи + +Для массива формы `(0, d)` контракт должен быть явным: вернуть форму `(0, m)` или +отклонить вход. Также проверяют: + +- один объект и один признак; +- нулевые векторы; +- очень большие и малые значения; +- `nan` и `inf`; +- `float32` и `float64`; +- не-contiguous views; +- отсутствие изменения входов. + +Не каждый продукт обязан поддерживать все случаи. Важно документировать решение +и завершаться понятной ошибкой, а не выдавать случайный результат. + +## Типичные ошибки + +- **«Векторизовано» значит «быстро».** Измеряйте время и память. +- **`np.vectorize` ожидается как ускоритель.** Он обычно вызывает Python поэлементно. +- **Создание массива `(n,n,d)`.** Используйте алгебраическое преобразование или батчи. +- **Тест проверяет ту же формулу.** Добавьте ручной пример и независимый оракул. +- **Точное сравнение float.** Используйте обоснованные `rtol` и `atol`. +- **Benchmark без прогрева и повторов.** Результат отражает шум. +- **Оптимизация до профиля.** Можно ускорить несущественный участок. +- **Игнорирование формы пустого результата.** Следующий этап ломается далеко от причины. + +## Краткое резюме + +- Векторизация переносит цикл из Python в операции над массивами. +- Асимптотика оценивает рост, benchmark — конкретную реализацию. +- Алгебраическое преобразование может уменьшить память без изменения Big-O времени. +- Численные тесты проверяют значения, формы, ошибки, границы и отсутствие мутации. +- Эталонная простая реализация помогает проверять оптимизированную на малых входах. + + +## Связанные материалы + +- Предыдущая глава: [`02_numpy_arrays.md`](02_numpy_arrays.md). +- Практика: [`../assignments/01_vectorization.md`](../assignments/01_vectorization.md). +- Практика: [`../assignments/03_clean_code.md`](../assignments/03_clean_code.md). +- Термины: [`glossary.md`](glossary.md). diff --git a/modules/01_python_numpy/notes/glossary.md b/modules/01_python_numpy/notes/glossary.md new file mode 100644 index 0000000..f8abcd7 --- /dev/null +++ b/modules/01_python_numpy/notes/glossary.md @@ -0,0 +1,27 @@ +# Глоссарий модуля 01 + +- **Broadcasting** — сопоставление форм справа налево с растяжением осей длины 1. +- **View** — массив, разделяющий память с другим массивом. +- **Амортизированная сложность** — средняя стоимость операции на последовательности + операций с учётом редких дорогих расширений. +- **Аннотация типа** — метаданные о предполагаемых типах; сама по себе не является + runtime-проверкой. +- **Векторизация** — выражение вычисления операциями над целыми массивами. +- **Генератор** — итератор, состояние которого приостанавливается оператором `yield`. +- **Допуск** — разрешённая абсолютная или относительная численная погрешность. +- **Идентичность** — принадлежность двух ссылок одному объекту, проверяемая `is`. +- **Изменяемый объект** — объект, состояние которого можно изменить без замены ссылки. +- **Итератор** — объект с `__next__`, выдающий элементы до `StopIteration`. +- **Итерируемый объект** — объект, из которого можно получить итератор. +- **Композиция** — построение объекта из других объектов вместо наследования. +- **Контекстный менеджер** — протокол входа и гарантированного выхода из контекста `with`. +- **Модульный тест** — автоматическая проверка маленькой единицы поведения. +- **Ось (`axis`)** — одно измерение многомерного массива. +- **Профилирование** — измерение распределения времени или памяти по участкам программы. +- **Протокол** — набор операций, поддерживаемых объектом. +- **Редукция** — свёртка элементов вдоль оси, например сумма или среднее. +- **Сложность** — порядок роста ресурсов алгоритма с размером входа. +- **Форма (`shape`)** — кортеж длин осей массива. +- **Чистая функция** — функция без наблюдаемых побочных эффектов, определяемая аргументами. +- **`dtype`** — фиксированный тип элементов массива NumPy. +- **`ndarray`** — основной однородный многомерный массив NumPy. diff --git a/modules/02_math_linear_algebra/README.md b/modules/02_math_linear_algebra/README.md new file mode 100644 index 0000000..cf33f9b --- /dev/null +++ b/modules/02_math_linear_algebra/README.md @@ -0,0 +1,36 @@ +# Модуль 02. Линейная алгебра и геометрия данных + +Плановый срок: **недели 4–5**. + +## Цели + +- понимать векторы, матрицы и линейные преобразования +- связывать ранг, базис, собственные значения и SVD с ML +- понимать геометрию регрессии, эмбеддингов и снижения размерности + +## Предварительные знания + +Нужны массивы NumPy, формы и матричное умножение из модуля 01, а также школьная алгебра. + +## Теория + +1. [Векторы, нормы, скалярное произведение, проекции и косинус](notes/01_vector_geometry.md) +2. [Матрицы, ранг, обратная матрица и системы уравнений](notes/02_matrices_systems_rank.md) +3. [Собственные значения, SVD, обусловленность и мост к PCA](notes/03_eigen_svd_conditioning.md) + +Термины модуля: [`notes/glossary.md`](notes/glossary.md). + +Для каждого урока: прочитайте теорию, воспроизведите ключевой пример своими +руками и запишите три тезиса без подсказки. Формулы из этого модуля нужно +уметь связать с кодом и наблюдаемым поведением модели. + +## Задания + +- [`assignments/01_geometry.md`](assignments/01_geometry.md) — Геометрия векторов +- [`assignments/02_least_squares.md`](assignments/02_least_squares.md) — МНК через матрицы +- [`assignments/03_pca_svd.md`](assignments/03_pca_svd.md) — PCA через SVD + +## Завершение модуля + +Модуль завершён, когда все задания удовлетворяют критериям приёмки, +вы можете объяснить основные решения без ноутбука и получили review. diff --git a/modules/02_math_linear_algebra/assignments/01_geometry.md b/modules/02_math_linear_algebra/assignments/01_geometry.md new file mode 100644 index 0000000..2da3082 --- /dev/null +++ b/modules/02_math_linear_algebra/assignments/01_geometry.md @@ -0,0 +1,41 @@ +# Геометрия векторов + +## Теория перед выполнением + +Перед началом прочитайте: + +- [`01_vector_geometry.md`](../notes/01_vector_geometry.md) + + +## Цель + +Визуализировать проекции векторов и сравнить L1, L2 и cosine distance. + +## Что сделать + +1. Сформулировать ожидаемый результат до запуска эксперимента. +2. Реализовать простой baseline. +3. Реализовать основной метод самостоятельно настолько, насколько требует задание. +4. Добавить автоматические проверки ключевых вычислений. +5. Провести эксперимент, сохранить метрики и примеры ошибок. +6. Заполнить отчёт по `common/REPORT_TEMPLATE.md`. + +## Результат + +Ноутбук с графиками и выводами о поведении метрик. + +Решение разместите рядом в `solution/`. Не коммитьте большие данные, +веса или секреты. + +## Критерии приёмки + +- Верно объяснено, когда косинус не учитывает длину и почему масштабы признаков важны. +- Код читаем, разбит на функции и запускается по инструкции. +- Выводы подтверждаются измерениями и не выходят за пределы эксперимента. + +## Вопросы для самопроверки + +- Почему выбран именно этот метод и какой baseline он должен превзойти? +- Где здесь возможна утечка данных или некорректная оценка? +- Как изменится результат при других данных или ограничениях? +- Какую ошибку модели вы считаете наиболее важной и почему? diff --git a/modules/02_math_linear_algebra/assignments/02_least_squares.md b/modules/02_math_linear_algebra/assignments/02_least_squares.md new file mode 100644 index 0000000..7083149 --- /dev/null +++ b/modules/02_math_linear_algebra/assignments/02_least_squares.md @@ -0,0 +1,42 @@ +# МНК через матрицы + +## Теория перед выполнением + +Перед началом прочитайте: + +- [`02_matrices_systems_rank.md`](../notes/02_matrices_systems_rank.md) +- [`03_eigen_svd_conditioning.md`](../notes/03_eigen_svd_conditioning.md) + + +## Цель + +Решить линейную регрессию normal equation и через `lstsq`; исследовать плохо обусловленные данные. + +## Что сделать + +1. Сформулировать ожидаемый результат до запуска эксперимента. +2. Реализовать простой baseline. +3. Реализовать основной метод самостоятельно настолько, насколько требует задание. +4. Добавить автоматические проверки ключевых вычислений. +5. Провести эксперимент, сохранить метрики и примеры ошибок. +6. Заполнить отчёт по `common/REPORT_TEMPLATE.md`. + +## Результат + +Код, сравнение решений и condition number. + +Решение разместите рядом в `solution/`. Не коммитьте большие данные, +веса или секреты. + +## Критерии приёмки + +- Объяснено, почему явное обращение матрицы нежелательно. +- Код читаем, разбит на функции и запускается по инструкции. +- Выводы подтверждаются измерениями и не выходят за пределы эксперимента. + +## Вопросы для самопроверки + +- Почему выбран именно этот метод и какой baseline он должен превзойти? +- Где здесь возможна утечка данных или некорректная оценка? +- Как изменится результат при других данных или ограничениях? +- Какую ошибку модели вы считаете наиболее важной и почему? diff --git a/modules/02_math_linear_algebra/assignments/03_pca_svd.md b/modules/02_math_linear_algebra/assignments/03_pca_svd.md new file mode 100644 index 0000000..781367e --- /dev/null +++ b/modules/02_math_linear_algebra/assignments/03_pca_svd.md @@ -0,0 +1,41 @@ +# PCA через SVD + +## Теория перед выполнением + +Перед началом прочитайте: + +- [`03_eigen_svd_conditioning.md`](../notes/03_eigen_svd_conditioning.md) + + +## Цель + +Реализовать центрирование, SVD, проекцию и восстановление данных. + +## Что сделать + +1. Сформулировать ожидаемый результат до запуска эксперимента. +2. Реализовать простой baseline. +3. Реализовать основной метод самостоятельно настолько, насколько требует задание. +4. Добавить автоматические проверки ключевых вычислений. +5. Провести эксперимент, сохранить метрики и примеры ошибок. +6. Заполнить отчёт по `common/REPORT_TEMPLATE.md`. + +## Результат + +Собственная PCA, график explained variance и сравнение со sklearn. + +Решение разместите рядом в `solution/`. Не коммитьте большие данные, +веса или секреты. + +## Критерии приёмки + +- Нет утечки через нецентрированные данные; восстановление улучшается с числом компонент. +- Код читаем, разбит на функции и запускается по инструкции. +- Выводы подтверждаются измерениями и не выходят за пределы эксперимента. + +## Вопросы для самопроверки + +- Почему выбран именно этот метод и какой baseline он должен превзойти? +- Где здесь возможна утечка данных или некорректная оценка? +- Как изменится результат при других данных или ограничениях? +- Какую ошибку модели вы считаете наиболее важной и почему? diff --git a/modules/02_math_linear_algebra/notes/01_vector_geometry.md b/modules/02_math_linear_algebra/notes/01_vector_geometry.md new file mode 100644 index 0000000..8984eaf --- /dev/null +++ b/modules/02_math_linear_algebra/notes/01_vector_geometry.md @@ -0,0 +1,244 @@ +# 01. Векторы, нормы, скалярное произведение, проекции и косинус + +> Геометрия превращает признаки объекта в направления, расстояния и компоненты вдоль полезных осей. + +## Мотивация + +Объект с \(d\) числовыми признаками представляется вектором +\(\mathbf{x}\in\mathbb{R}^d\). Поиск похожих объектов, линейное предсказание, +эмбеддинги и метод наименьших квадратов опираются на длины, углы и проекции. +Геометрическая формула осмысленна только после выбора масштаба признаков. + +## Вектор и линейная комбинация + +Вектор можно понимать как упорядоченный набор координат или как направленный +отрезок. Сложение и умножение на скаляр выполняются покоординатно: + +\[ +\mathbf{x}+\mathbf{y} +=\begin{bmatrix}x_1+y_1\\\vdots\\x_d+y_d\end{bmatrix}, +\qquad +a\mathbf{x} +=\begin{bmatrix}ax_1\\\vdots\\ax_d\end{bmatrix}. +\] + +**Линейная комбинация** векторов \(\mathbf{v}_1,\ldots,\mathbf{v}_k\) — сумма +\(\sum_{j=1}^k a_j\mathbf{v}_j\). Множество всех таких комбинаций называется +линейной оболочкой, \(\operatorname{span}(\mathbf{v}_1,\ldots,\mathbf{v}_k)\). + +Векторы линейно независимы, если равенство +\(\sum_j a_j\mathbf{v}_j=\mathbf{0}\) возможно только при всех \(a_j=0\). +Базис пространства — независимый набор, порождающий всё пространство. + +## Нормы + +Норма \(\|\mathbf{x}\|\) измеряет размер вектора и удовлетворяет: + +1. \(\|\mathbf{x}\|\geq0\), равенство нулю только для \(\mathbf{0}\); +2. \(\|a\mathbf{x}\|=|a|\|\mathbf{x}\|\); +3. \(\|\mathbf{x}+\mathbf{y}\|\leq\|\mathbf{x}\|+\|\mathbf{y}\|\). + +Частые нормы: + +\[ +\|\mathbf{x}\|_1=\sum_{j=1}^d|x_j|, +\quad +\|\mathbf{x}\|_2=\sqrt{\sum_{j=1}^d x_j^2}, +\quad +\|\mathbf{x}\|_\infty=\max_j|x_j|. +\] + +Расстояние, индуцированное нормой: +\(d(\mathbf{x},\mathbf{y})=\|\mathbf{x}-\mathbf{y}\|\). +Разные нормы задают разную геометрию: единичный круг \(L_2\) круглый, \(L_1\) — +ромб, \(L_\infty\) — квадрат. + +## Скалярное произведение + +Евклидово скалярное произведение: + +\[ +\mathbf{x}^\top\mathbf{y}=\sum_{j=1}^d x_jy_j. +\] + +Оно симметрично, линейно по каждому аргументу и положительно определено: +\(\mathbf{x}^\top\mathbf{x}>0\) при \(\mathbf{x}\ne\mathbf{0}\). Отсюда + +\[ +\|\mathbf{x}\|_2=\sqrt{\mathbf{x}^\top\mathbf{x}}. +\] + +Геометрическая связь: + +\[ +\mathbf{x}^\top\mathbf{y} +=\|\mathbf{x}\|_2\|\mathbf{y}\|_2\cos\theta. +\] + +Положительное произведение означает острый угол, нулевое — ортогональность, +отрицательное — тупой угол. + +Неравенство Коши—Буняковского: + +\[ +|\mathbf{x}^\top\mathbf{y}| +\leq\|\mathbf{x}\|_2\|\mathbf{y}\|_2. +\] + +Оно гарантирует, что вычисленный косинус лежит в \([-1,1]\) в точной арифметике. + +## Косинусное сходство + +Для ненулевых векторов: + +\[ +\operatorname{cos\_sim}(\mathbf{x},\mathbf{y}) +=\frac{\mathbf{x}^\top\mathbf{y}} +{\|\mathbf{x}\|_2\|\mathbf{y}\|_2}. +\] + +Оно игнорирует положительный масштаб: \(\mathbf{x}\) и \(10\mathbf{x}\) имеют +сходство 1. Это полезно для направлений эмбеддингов, но теряет информацию о длине. +Для нулевого вектора косинус не определён. + +```python +import numpy as np + +x = np.array([1.0, 2.0]) +y = np.array([2.0, 1.0]) +denominator = np.linalg.norm(x) * np.linalg.norm(y) +if denominator == 0: + raise ValueError("Косинус не определён для нулевого вектора") +cosine = (x @ y) / denominator +print(np.clip(cosine, -1.0, 1.0)) +``` + +`clip` исправляет лишь микроскопический выход из диапазона из-за округления, а не +ошибку нулевой нормы. + +## Проекция на направление + +Ищем ближайший к \(\mathbf{x}\) вектор на прямой +\(\{a\mathbf{u}:a\in\mathbb{R}\}\), где \(\mathbf{u}\ne0\). Остаток должен быть +ортогонален направлению: + +\[ +\mathbf{u}^\top(\mathbf{x}-a\mathbf{u})=0. +\] + +Раскроем: + +\[ +\mathbf{u}^\top\mathbf{x}-a\mathbf{u}^\top\mathbf{u}=0, +\qquad +a=\frac{\mathbf{u}^\top\mathbf{x}}{\mathbf{u}^\top\mathbf{u}}. +\] + +Следовательно, + +\[ +\operatorname{proj}_{\mathbf{u}}(\mathbf{x}) +=\frac{\mathbf{u}^\top\mathbf{x}}{\mathbf{u}^\top\mathbf{u}}\mathbf{u}. +\] + +Если \(\|\mathbf{u}\|_2=1\), коэффициент равен +\(\mathbf{u}^\top\mathbf{x}\). Остаток +\(\mathbf{r}=\mathbf{x}-\operatorname{proj}_{\mathbf{u}}(\mathbf{x})\) +ортогонален \(\mathbf{u}\). + +## Разобранный числовой пример + +Пусть +\(\mathbf{x}=[3,4]^\top\), \(\mathbf{u}=[2,1]^\top\). + +\[ +\mathbf{u}^\top\mathbf{x}=2\cdot3+1\cdot4=10, +\qquad +\mathbf{u}^\top\mathbf{u}=2^2+1^2=5. +\] + +Коэффициент \(a=10/5=2\), поэтому + +\[ +\mathbf{p}=2\mathbf{u}=[4,2]^\top, +\qquad +\mathbf{r}=\mathbf{x}-\mathbf{p}=[-1,2]^\top. +\] + +Проверка ортогональности: +\(\mathbf{u}^\top\mathbf{r}=2(-1)+1(2)=0\). +По теореме Пифагора: +\(\|\mathbf{x}\|_2^2=25=\|\mathbf{p}\|_2^2+\|\mathbf{r}\|_2^2=20+5\). + +```python +import numpy as np + +x = np.array([3.0, 4.0]) +u = np.array([2.0, 1.0]) +projection = (u @ x) / (u @ u) * u +residual = x - projection +print("projection:", projection) +print("residual:", residual) +print("orthogonality:", u @ residual) +``` + +## Проекция на подпространство + +Если столбцы \(\mathbf{Q}\in\mathbb{R}^{d\times k}\) ортонормированы, +\(\mathbf{Q}^\top\mathbf{Q}=\mathbf{I}_k\), то координаты проекции — +\(\mathbf{Q}^\top\mathbf{x}\), а сама проекция: + +\[ +\hat{\mathbf{x}}=\mathbf{Q}\mathbf{Q}^\top\mathbf{x}. +\] + +Матрица \(\mathbf{P}=\mathbf{Q}\mathbf{Q}^\top\) симметрична и идемпотентна: +\(\mathbf{P}^\top=\mathbf{P}\), \(\mathbf{P}^2=\mathbf{P}\). Повторная проекция +ничего не меняет. + +Для неортонормированного базиса \(\mathbf{A}\) формула +\(\mathbf{A}\mathbf{A}^\top\) неверна. При независимых столбцах: + +\[ +\mathbf{P} +=\mathbf{A}(\mathbf{A}^\top\mathbf{A})^{-1}\mathbf{A}^\top. +\] + +На практике проекцию лучше вычислять через QR или least squares, не формируя +явную обратную матрицу. + +## Масштаб признаков + +Пусть признаки — возраст в годах и доход в рублях. Евклидово расстояние почти +полностью определяется доходом из-за масштаба единиц. Нормализация меняет геометрию, +а значит, соседей, проекции и обучение. Это не техническая мелочь, а допущение +о важности признаков. + +Косинус тоже не инвариантен к отдельному масштабированию координат: умножение +одного признака во всех объектах меняет углы. + +## Типичные ошибки + +- **Косинус нулевого вектора.** Он не определён; добавление epsilon меняет задачу. +- **Проекция без деления на \(\mathbf{u}^\top\mathbf{u}\).** Такая формула верна + только для единичного \(\mathbf{u}\). +- **Смешение длины и квадрата длины.** \(\|\mathbf{x}\|_2^2=\mathbf{x}^\top\mathbf{x}\). +- **Игнорирование единиц признаков.** Геометрия становится геометрией масштаба. +- **Ортогональность принимается за независимость вообще.** Ортогональные ненулевые + векторы независимы, но независимые векторы не обязаны быть ортогональны. +- **Точное сравнение произведения с нулём.** В вычислениях нужен допуск. + +## Краткое резюме + +- Линейные комбинации порождают подпространства, базис задаёт координаты. +- Норма определяет длину и расстояние; выбор нормы — часть модели. +- Скалярное произведение связывает алгебру с углом и ортогональностью. +- Проекция — ближайшая точка подпространства, а остаток ортогонален ему. +- Косинус сравнивает направления, но не определён для нулевых векторов. + + +## Связанные материалы + +- Следующая глава: [`02_matrices_systems_rank.md`](02_matrices_systems_rank.md). +- Практика: [`../assignments/01_geometry.md`](../assignments/01_geometry.md). +- Термины: [`glossary.md`](glossary.md). diff --git a/modules/02_math_linear_algebra/notes/02_matrices_systems_rank.md b/modules/02_math_linear_algebra/notes/02_matrices_systems_rank.md new file mode 100644 index 0000000..472cecd --- /dev/null +++ b/modules/02_math_linear_algebra/notes/02_matrices_systems_rank.md @@ -0,0 +1,299 @@ +# 02. Матрицы, ранг, обратная матрица и системы уравнений + +> Матрица одновременно хранит данные, задаёт линейное преобразование и кодирует систему уравнений. + +## Мотивация + +Матрица признаков \(\mathbf{X}\in\mathbb{R}^{n\times d}\) содержит \(n\) объектов +по строкам и \(d\) признаков по столбцам. Линейная модель +\(\hat{\mathbf{y}}=\mathbf{X}\mathbf{w}\) строит предсказания. Ранг отвечает, +сколько независимой информации несут столбцы, а решение системы определяет, +можно ли восстановить параметры однозначно. + +## Матрица как преобразование + +Матрица \(\mathbf{A}\in\mathbb{R}^{m\times d}\) задаёт отображение +\(\mathbf{x}\mapsto\mathbf{A}\mathbf{x}\) из \(\mathbb{R}^d\) в +\(\mathbb{R}^m\). Оно линейно: + +\[ +\mathbf{A}(a\mathbf{x}+b\mathbf{y}) +=a\mathbf{A}\mathbf{x}+b\mathbf{A}\mathbf{y}. +\] + +Столбцы \(\mathbf{A}\) — образы стандартных базисных векторов. Поэтому + +\[ +\mathbf{A}\mathbf{x} +=x_1\mathbf{a}_1+\cdots+x_d\mathbf{a}_d. +\] + +Умножение матриц — композиция преобразований. Если +\(\mathbf{B}\in\mathbb{R}^{d\times k}\), то +\(\mathbf{A}\mathbf{B}\in\mathbb{R}^{m\times k}\), и сначала действует +\(\mathbf{B}\), затем \(\mathbf{A}\). В общем случае +\(\mathbf{A}\mathbf{B}\ne\mathbf{B}\mathbf{A}\). + +## Транспонирование + +\((\mathbf{A}^\top)_{ij}=A_{ji}\). Полезные свойства: + +\[ +(\mathbf{A}^\top)^\top=\mathbf{A},\qquad +(\mathbf{A}\mathbf{B})^\top=\mathbf{B}^\top\mathbf{A}^\top. +\] + +Порядок во второй формуле обращается, потому что обращается композиция скалярных +произведений. Симметричная матрица удовлетворяет +\(\mathbf{A}^\top=\mathbf{A}\). + +## Образ, ядро и ранг + +**Образ**: + +\[ +\operatorname{Im}(\mathbf{A}) +=\{\mathbf{A}\mathbf{x}:\mathbf{x}\in\mathbb{R}^d\}. +\] + +Это пространство всех достижимых выходов, равное оболочке столбцов. +**Ядро**: + +\[ +\operatorname{Ker}(\mathbf{A}) +=\{\mathbf{x}:\mathbf{A}\mathbf{x}=\mathbf{0}\}. +\] + +**Ранг** — размерность образа, то есть число независимых столбцовых направлений. +Строковый и столбцовый ранги совпадают. + +Теорема о ранге и дефекте: + +\[ +\operatorname{rank}(\mathbf{A}) ++\dim\operatorname{Ker}(\mathbf{A})=d. +\] + +Если ранг меньше \(d\), существует ненулевое направление, которое преобразование +сжимает в ноль. В данных это означает линейную избыточность признаков. + +## Системы линейных уравнений + +Система записывается: + +\[ +\mathbf{A}\mathbf{x}=\mathbf{b}. +\] + +Решение существует тогда и только тогда, когда +\(\mathbf{b}\in\operatorname{Im}(\mathbf{A})\), или эквивалентно + +\[ +\operatorname{rank}(\mathbf{A}) +=\operatorname{rank}([\mathbf{A}\mid\mathbf{b}]). +\] + +Если решение \(\mathbf{x}_0\) существует, все решения имеют вид: + +\[ +\mathbf{x}=\mathbf{x}_0+\mathbf{z}, +\qquad +\mathbf{z}\in\operatorname{Ker}(\mathbf{A}). +\] + +Поэтому при тривиальном ядре решение единственно; при нетривиальном — бесконечно +много. + +## Метод Гаусса + +Элементарные операции над строками не меняют множество решений: + +1. перестановка строк; +2. умножение строки на ненулевой скаляр; +3. прибавление к строке кратной другой строки. + +Рассмотрим: + +\[ +\begin{cases} +x+2y=5,\\ +3x+4y=11. +\end{cases} +\] + +Расширенная матрица: + +\[ +\left[\begin{array}{cc|c} +1&2&5\\ +3&4&11 +\end{array}\right]. +\] + +Выполним строковую операцию \(R_2\leftarrow R_2-3R_1\), то есть вычтем из второй строки первую, умноженную на 3: + +\[ +\left[\begin{array}{cc|c} +1&2&5\\ +0&-2&-4 +\end{array}\right]. +\] + +Отсюда \(y=2\), затем \(x+4=5\), то есть \(x=1\). + +```python +import numpy as np + +A = np.array([[1.0, 2.0], [3.0, 4.0]]) +b = np.array([5.0, 11.0]) +x = np.linalg.solve(A, b) +print(x) +print("residual:", A @ x - b) +``` + +`solve` предпочтительнее вычисления `inv(A) @ b`: он быстрее, точнее и не строит +ненужную обратную матрицу. + +## Обратная матрица + +Квадратная \(\mathbf{A}\in\mathbb{R}^{d\times d}\) обратима, если существует +\(\mathbf{A}^{-1}\), такая что + +\[ +\mathbf{A}^{-1}\mathbf{A} +=\mathbf{A}\mathbf{A}^{-1} +=\mathbf{I}_d. +\] + +Эквивалентные условия: + +- \(\operatorname{rank}(\mathbf{A})=d\); +- \(\operatorname{Ker}(\mathbf{A})=\{\mathbf{0}\}\); +- столбцы образуют базис \(\mathbb{R}^d\); +- \(\det(\mathbf{A})\ne0\); +- \(\mathbf{A}\mathbf{x}=\mathbf{b}\) имеет единственное решение для любого + \(\mathbf{b}\). + +Определитель измеряет ориентированное масштабирование объёма. Нулевой определитель +означает сжатие пространства в меньшую размерность. Но величина determinant +не является надёжным численным тестом обратимости: она зависит от масштаба и +может переполняться. + +## Переопределённые системы и наименьшие квадраты + +При \(n>d\) система \(\mathbf{X}\mathbf{w}=\mathbf{y}\) обычно несовместна. +Ищем вектор с минимальным остатком: + +\[ +\mathbf{w}^\star +\in\arg\min_{\mathbf{w}} +\|\mathbf{X}\mathbf{w}-\mathbf{y}\|_2^2. +\] + +Обозначим \(\mathbf{r}=\mathbf{y}-\mathbf{X}\mathbf{w}^\star\). В ближайшей точке +остаток ортогонален каждому столбцу \(\mathbf{X}\): + +\[ +\mathbf{X}^\top\mathbf{r}=\mathbf{0}. +\] + +Подставляя остаток: + +\[ +\mathbf{X}^\top(\mathbf{y}-\mathbf{X}\mathbf{w}^\star)=\mathbf{0}, +\] + +получаем нормальные уравнения: + +\[ +\mathbf{X}^\top\mathbf{X}\mathbf{w}^\star +=\mathbf{X}^\top\mathbf{y}. +\] + +Если столбцы независимы, + +\[ +\mathbf{w}^\star +=(\mathbf{X}^\top\mathbf{X})^{-1}\mathbf{X}^\top\mathbf{y}. +\] + +Эта формула объясняет геометрию, но не является рекомендуемым вычислительным +алгоритмом: образование \(\mathbf{X}^\top\mathbf{X}\) ухудшает обусловленность. +Используют QR-разложение или SVD: + +```python +import numpy as np + +X = np.array([[1.0, 0.0], + [1.0, 1.0], + [1.0, 2.0]]) +y = np.array([1.0, 2.0, 2.0]) +w, residuals, rank, singular_values = np.linalg.lstsq(X, y, rcond=None) +print("w:", w) +print("rank:", rank) +print("prediction:", X @ w) +``` + +## Недоопределённые системы и псевдообратная + +При числе уравнений меньше числа неизвестных решений может быть бесконечно много. +Псевдообратная Мура—Пенроуза \(\mathbf{A}^+\) выбирает среди точных решений +решение минимальной \(L_2\)-нормы, а для несовместной системы — least-squares +решение минимальной нормы. + +\[ +\mathbf{x}^\star=\mathbf{A}^+\mathbf{b}. +\] + +Псевдообратная определяется через SVD и существует для любой матрицы. Она не +устраняет нехватку информации: выбор минимальной нормы — дополнительное +предпочтение. + +## Численный ранг + +В точной математике сингулярное значение либо ноль, либо нет. В floating point +малое значение трудно отличить от ошибки округления. Поэтому численный ранг +зависит от допуска: + +```python +import numpy as np + +A = np.array([[1.0, 1.0], + [1.0, 1.0 + 1e-12]]) +print(np.linalg.matrix_rank(A)) +print(np.linalg.svd(A, compute_uv=False)) +``` + +Масштаб данных и dtype влияют на решение. Фраза «матрица имеет полный ранг» в +численной задаче должна сопровождаться информацией о сингулярных значениях или +допуске. + +## Типичные ошибки + +- **`A * B` вместо `A @ B`.** Первая операция поэлементная. +- **Явная обратная для решения.** Используйте `solve` или `lstsq`. +- **Нормальные уравнения как алгоритм по умолчанию.** Они квадратируют число + обусловленности. +- **Определитель как численная диагностика.** Смотрите сингулярные значения. +- **Ранг без допуска.** Почти зависимые столбцы требуют численной интерпретации. +- **Единственное решение предполагается по квадратности.** Квадратная матрица может + быть вырожденной. +- **Малый residual принимается за точные параметры.** При плохой обусловленности + параметры могут быть нестабильны. + +## Краткое резюме + +- Матрица задаёт линейное преобразование, её столбцы порождают образ. +- Ранг измеряет число независимых направлений, ядро — потерянные направления. +- Совместность означает принадлежность правой части образу. +- Обратимость эквивалентна полному рангу квадратной матрицы. +- Least squares — ортогональная проекция цели на пространство столбцов. +- Для вычислений применяют `solve`, QR/SVD и `lstsq`, а не явную обратную. + + +## Связанные материалы + +- Предыдущая глава: [`01_vector_geometry.md`](01_vector_geometry.md). +- Следующая глава: [`03_eigen_svd_conditioning.md`](03_eigen_svd_conditioning.md). +- Практика: [`../assignments/02_least_squares.md`](../assignments/02_least_squares.md). +- Термины: [`glossary.md`](glossary.md). diff --git a/modules/02_math_linear_algebra/notes/03_eigen_svd_conditioning.md b/modules/02_math_linear_algebra/notes/03_eigen_svd_conditioning.md new file mode 100644 index 0000000..b17198a --- /dev/null +++ b/modules/02_math_linear_algebra/notes/03_eigen_svd_conditioning.md @@ -0,0 +1,377 @@ +# 03. Собственные значения, SVD, обусловленность и мост к PCA + +> Разложения находят естественные направления преобразования и показывают, какие компоненты данных устойчивы. + +## Мотивация + +Матрица может растягивать пространство по одним направлениям и почти уничтожать +другие. Это определяет устойчивость линейной системы, сжатие данных и главные +направления вариации. Собственное разложение работает для квадратных матриц, SVD — +для любой прямоугольной матрицы и непосредственно показывает ранг и обусловленность. + +## Собственные векторы и значения + +Ненулевой \(\mathbf{v}\) — собственный вектор квадратной +\(\mathbf{A}\in\mathbb{R}^{d\times d}\), если + +\[ +\mathbf{A}\mathbf{v}=\lambda\mathbf{v}. +\] + +Преобразование не меняет направление \(\mathbf{v}\), а масштабирует его в +\(\lambda\) раз. Отрицательное \(\lambda\) также обращает направление. + +Перенесём части: + +\[ +(\mathbf{A}-\lambda\mathbf{I})\mathbf{v}=\mathbf{0}. +\] + +Ненулевое решение существует, только если матрица вырождена: + +\[ +\det(\mathbf{A}-\lambda\mathbf{I})=0. +\] + +Это характеристическое уравнение. + +## Разобранный пример собственного разложения + +Пусть + +\[ +\mathbf{A}= +\begin{bmatrix}2&1\\1&2\end{bmatrix}. +\] + +Тогда + +\[ +\det(\mathbf{A}-\lambda\mathbf{I}) +=\det\begin{bmatrix}2-\lambda&1\\1&2-\lambda\end{bmatrix} +=(2-\lambda)^2-1. +\] + +\[ +(2-\lambda)^2-1 +=\lambda^2-4\lambda+3 +=(\lambda-1)(\lambda-3). +\] + +Собственные значения: \(\lambda_1=3\), \(\lambda_2=1\). +Для \(\lambda_1=3\) получаем направление \([1,1]^\top\), для +\(\lambda_2=1\) — \([1,-1]^\top\). Нормированные векторы: + +\[ +\mathbf{q}_1=\frac1{\sqrt2}[1,1]^\top,\qquad +\mathbf{q}_2=\frac1{\sqrt2}[1,-1]^\top. +\] + +Поскольку матрица симметрична: + +\[ +\mathbf{A}=\mathbf{Q}\boldsymbol{\Lambda}\mathbf{Q}^\top, +\] + +где столбцы \(\mathbf{Q}\) ортонормированы. + +```python +import numpy as np + +A = np.array([[2.0, 1.0], [1.0, 2.0]]) +eigenvalues, Q = np.linalg.eigh(A) +print(eigenvalues) +print(Q) +print(np.allclose(A, Q @ np.diag(eigenvalues) @ Q.T)) +``` + +Для вещественной симметричной матрицы используйте `eigh`: она учитывает структуру, +возвращает вещественные значения и ортонормированные векторы. Порядок здесь обычно +возрастающий; его нельзя предполагать без документации. + +## Спектральная теорема + +Любая вещественная симметричная матрица имеет ортонормированный базис собственных +векторов: + +\[ +\mathbf{A}=\mathbf{Q}\boldsymbol{\Lambda}\mathbf{Q}^\top. +\] + +Квадратичная форма: + +\[ +\mathbf{x}^\top\mathbf{A}\mathbf{x} +=\sum_{j=1}^d\lambda_j z_j^2, +\qquad +\mathbf{z}=\mathbf{Q}^\top\mathbf{x}. +\] + +Отсюда симметричная матрица положительно полуопределена тогда и только тогда, +когда все \(\lambda_j\geq0\). Ковариационная матрица всегда положительно +полуопределена. + +Не каждая квадратная матрица имеет вещественные собственные значения или полный +базис собственных векторов. Поэтому интуицию ортогональных осей нельзя безусловно +переносить на несимметричный случай. + +## Сингулярное разложение + +Для любой \(\mathbf{A}\in\mathbb{R}^{m\times d}\): + +\[ +\mathbf{A}=\mathbf{U}\boldsymbol{\Sigma}\mathbf{V}^\top. +\] + +В полной форме \(\mathbf{U}\in\mathbb{R}^{m\times m}\) и +\(\mathbf{V}\in\mathbb{R}^{d\times d}\) ортогональны, а +\(\boldsymbol{\Sigma}\in\mathbb{R}^{m\times d}\) диагональна прямоугольным +образом. Сингулярные значения +\(\sigma_1\geq\sigma_2\geq\cdots\geq0\). + +Геометрически: + +1. \(\mathbf{V}^\top\) поворачивает вход в правые сингулярные направления; +2. \(\boldsymbol{\Sigma}\) растягивает каждое направление в \(\sigma_j\) раз; +3. \(\mathbf{U}\) поворачивает результат. + +Связь с собственными задачами: + +\[ +\mathbf{A}^\top\mathbf{A} +=\mathbf{V}\boldsymbol{\Sigma}^\top\boldsymbol{\Sigma}\mathbf{V}^\top, +\] + +\[ +\mathbf{A}\mathbf{A}^\top +=\mathbf{U}\boldsymbol{\Sigma}\boldsymbol{\Sigma}^\top\mathbf{U}^\top. +\] + +Следовательно, \(\sigma_j^2\) — собственные значения +\(\mathbf{A}^\top\mathbf{A}\), правые сингулярные векторы — её собственные +векторы. + +```python +import numpy as np + +A = np.array([[3.0, 0.0], + [0.0, 2.0], + [0.0, 0.0]]) +U, s, Vt = np.linalg.svd(A, full_matrices=False) +reconstructed = U @ np.diag(s) @ Vt +print("shapes:", U.shape, s.shape, Vt.shape) +print("singular values:", s) +print(np.allclose(A, reconstructed)) +``` + +Экономичная форма `full_matrices=False` обычно нужна для данных: при +\(n\gg d\) она не создаёт огромную квадратную \(\mathbf{U}\). + +## Ранг и низкоранговое приближение + +В точной арифметике ранг равен числу ненулевых сингулярных значений. Усечённая SVD: + +\[ +\mathbf{A}_k +=\mathbf{U}_k\boldsymbol{\Sigma}_k\mathbf{V}_k^\top. +\] + +Теорема Эккарта—Янга утверждает, что \(\mathbf{A}_k\) — лучшее приближение ранга +не выше \(k\) в норме Фробениуса и спектральной норме: + +\[ +\|\mathbf{A}-\mathbf{A}_k\|_F^2 +=\sum_{j>k}\sigma_j^2. +\] + +То есть отброшенная квадратичная энергия точно равна сумме квадратов отброшенных +сингулярных значений. + +## Обусловленность + +Для обратимой квадратной матрицы число обусловленности в \(L_2\): + +\[ +\kappa_2(\mathbf{A}) +=\|\mathbf{A}\|_2\|\mathbf{A}^{-1}\|_2 +=\frac{\sigma_{\max}}{\sigma_{\min}}. +\] + +Оно оценивает усиление относительной ошибки. При малом возмущении правой части +\(\delta\mathbf{b}\): + +\[ +\frac{\|\delta\mathbf{x}\|_2}{\|\mathbf{x}\|_2} +\lesssim +\kappa_2(\mathbf{A}) +\frac{\|\delta\mathbf{b}\|_2}{\|\mathbf{b}\|_2} +\] + +при фиксированной \(\mathbf{A}\) и подходящих малых возмущениях. +Большая \(\kappa\) означает, что задача чувствительна; это свойство задачи, а не +только алгоритма. Если \(\sigma_{\min}=0\), число обусловленности бесконечно. + +```python +import numpy as np + +for eps in [1e-2, 1e-8]: + A = np.array([[1.0, 1.0], + [1.0, 1.0 + eps]]) + print(eps, np.linalg.cond(A), np.linalg.svd(A, compute_uv=False)) +``` + +Матрица с почти совпадающими столбцами имеет маленькое \(\sigma_{\min}\). +Предсказания могут оставаться похожими, а коэффициенты резко меняться. + +Нормальные уравнения ухудшают ситуацию: + +\[ +\kappa_2(\mathbf{X}^\top\mathbf{X})=\kappa_2(\mathbf{X})^2 +\] + +при полном столбцовом ранге. Поэтому QR или SVD надёжнее. + +## Псевдообратная через SVD + +Если +\(\mathbf{A}=\mathbf{U}\boldsymbol{\Sigma}\mathbf{V}^\top\), то + +\[ +\mathbf{A}^+ +=\mathbf{V}\boldsymbol{\Sigma}^+\mathbf{U}^\top, +\] + +где ненулевые \(\sigma_j\) заменяются на \(1/\sigma_j\), а нули остаются нулями. +Очень малое \(\sigma_j\) превращается в огромный множитель и усиливает шум. + +Отсечение малых сингулярных значений или регуляризация стабилизирует решение, +но добавляет смещение. Это не «исправление арифметики», а изменение статистического +предпочтения. + +## Мост к PCA + +Пусть строки \(\mathbf{X}\in\mathbb{R}^{n\times d}\) — объекты. Сначала центрируем: + +\[ +\mathbf{X}_c=\mathbf{X}-\mathbf{1}\boldsymbol{\mu}^\top, +\qquad +\boldsymbol{\mu}=\frac1n\sum_{i=1}^n\mathbf{x}^{(i)}. +\] + +Выборочная ковариация: + +\[ +\mathbf{C}=\frac1{n-1}\mathbf{X}_c^\top\mathbf{X}_c. +\] + +Ищем единичное направление \(\mathbf{v}\), вдоль которого дисперсия проекций +максимальна: + +\[ +\max_{\|\mathbf{v}\|_2=1} +\frac1{n-1}\|\mathbf{X}_c\mathbf{v}\|_2^2 += +\max_{\|\mathbf{v}\|_2=1}\mathbf{v}^\top\mathbf{C}\mathbf{v}. +\] + +С множителем Лагранжа: + +\[ +L(\mathbf{v},\lambda) +=\mathbf{v}^\top\mathbf{C}\mathbf{v} +-\lambda(\mathbf{v}^\top\mathbf{v}-1). +\] + +\[ +\nabla_{\mathbf{v}}L +=2\mathbf{C}\mathbf{v}-2\lambda\mathbf{v}=\mathbf{0}, +\] + +поэтому \(\mathbf{C}\mathbf{v}=\lambda\mathbf{v}\). Максимум даёт собственный +вектор с наибольшим собственным значением. + +Если +\(\mathbf{X}_c=\mathbf{U}\boldsymbol{\Sigma}\mathbf{V}^\top\), то столбцы +\(\mathbf{V}\) — главные направления, а + +\[ +\lambda_j=\frac{\sigma_j^2}{n-1}. +\] + +Координаты объектов в первых \(k\) компонентах: + +\[ +\mathbf{Z}=\mathbf{X}_c\mathbf{V}_k +=\mathbf{U}_k\boldsymbol{\Sigma}_k. +\] + +Доля объяснённой дисперсии: + +\[ +\frac{\lambda_j}{\sum_\ell\lambda_\ell} +=\frac{\sigma_j^2}{\sum_\ell\sigma_\ell^2}. +\] + +## Числовой PCA + +```python +import numpy as np + +X = np.array([[2.0, 0.0], + [0.0, 2.0], + [3.0, 1.0], + [1.0, 3.0]]) +mean = X.mean(axis=0, keepdims=True) +Xc = X - mean +U, s, Vt = np.linalg.svd(Xc, full_matrices=False) + +components = Vt +scores = Xc @ components.T +explained_variance = s**2 / (len(X) - 1) +ratio = explained_variance / explained_variance.sum() + +print("mean:", mean) +print("components:", components) +print("scores:", scores) +print("explained variance ratio:", ratio) +``` + +Знак сингулярного вектора не определён: \(\mathbf{v}\) и \(-\mathbf{v}\) описывают +ту же ось. Тесты PCA должны учитывать эту неоднозначность. + +PCA чувствителен к масштабу. Центрирование обязательно для обычного PCA; +стандартизация нужна, если признаки имеют несопоставимые единицы и их предполагают +равно важными. PCA линейный и не гарантирует сохранение классов или локальных +соседств. + +## Типичные ошибки + +- **`eig` для симметричной матрицы.** Используйте устойчивый `eigh`. +- **Собственные значения прямоугольной матрицы.** Для неё применяют SVD. +- **Малое сингулярное значение инвертируется без порога.** Шум резко усиливается. +- **PCA без центрирования.** Компоненты отражают положение относительно начала, + а не вариацию вокруг среднего. +- **Сингулярные значения принимаются за дисперсии.** Дисперсии равны + \(\sigma_j^2/(n-1)\). +- **Сравнение знаков компонент.** Ось определена с точностью до знака. +- **Большая обусловленность объявляется ошибкой алгоритма.** Сначала сама задача + чувствительна; алгоритм может дополнительно ухудшить результат. +- **Полная SVD для высокой матрицы.** Используйте экономичную или специализированную + усечённую SVD. + +## Краткое резюме + +- Собственный вектор сохраняет направление под действием квадратной матрицы. +- Симметричная матрица имеет ортонормированное собственное разложение. +- SVD существует для любой матрицы и раскрывает ранг, растяжения и устойчивость. +- Число обусловленности — отношение крайних сингулярных значений. +- Усечённая SVD даёт оптимальное низкоранговое приближение. +- PCA — SVD центрированной матрицы данных или eigen-разложение ковариации. + + +## Связанные материалы + +- Предыдущая глава: [`02_matrices_systems_rank.md`](02_matrices_systems_rank.md). +- Практика: [`../assignments/02_least_squares.md`](../assignments/02_least_squares.md). +- Практика: [`../assignments/03_pca_svd.md`](../assignments/03_pca_svd.md). +- Термины: [`glossary.md`](glossary.md). diff --git a/modules/02_math_linear_algebra/notes/glossary.md b/modules/02_math_linear_algebra/notes/glossary.md new file mode 100644 index 0000000..c5a239b --- /dev/null +++ b/modules/02_math_linear_algebra/notes/glossary.md @@ -0,0 +1,30 @@ +# Глоссарий модуля 02 + +- **Базис** — линейно независимый набор, порождающий пространство. +- **Вектор** — элемент линейного пространства; в координатах — упорядоченный набор чисел. +- **Главная компонента** — направление максимальной оставшейся дисперсии центрированных данных. +- **Идемпотентная матрица** — матрица \(\mathbf{P}\), для которой \(\mathbf{P}^2=\mathbf{P}\). +- **Ковариационная матрица** — симметричная матрица попарных ковариаций признаков. +- **Линейная комбинация** — сумма векторов со скалярными коэффициентами. +- **Линейная независимость** — невозможность получить ноль нетривиальной линейной комбинацией. +- **Матрица** — прямоугольная таблица чисел и представление линейного отображения. +- **Метод наименьших квадратов** — минимизация нормы остатка линейной системы. +- **Норма** — функция длины, удовлетворяющая положительности, однородности и неравенству треугольника. +- **Образ матрицы** — множество всех выходов \(\mathbf{A}\mathbf{x}\). +- **Обратная матрица** — матрица, отменяющая действие исходной с обеих сторон. +- **Обусловленность** — чувствительность ответа математической задачи к возмущению входа. +- **Ортогональность** — нулевое скалярное произведение векторов. +- **Ортонормированный набор** — взаимно ортогональные единичные векторы. +- **Положительно полуопределённая матрица** — симметричная матрица с + \(\mathbf{x}^\top\mathbf{A}\mathbf{x}\geq0\). +- **Проекция** — ближайшая точка заданного подпространства в евклидовой норме. +- **Псевдообратная** — обобщённая обратная, задающая least-squares решение минимальной нормы. +- **Ранг** — размерность образа матрицы. +- **Сингулярное значение** — неотрицательный коэффициент растяжения в SVD. +- **Скалярное произведение** — билинейная положительно определённая мера согласованности направлений. +- **Собственное значение** — коэффициент масштабирования собственного вектора. +- **Собственный вектор** — ненулевое направление, сохраняемое квадратным преобразованием. +- **Усечённая SVD** — разложение, в котором сохранены только крупнейшие сингулярные значения. +- **Ядро матрицы** — множество входов, переходящих в нулевой вектор. +- **PCA** — линейный метод поиска ортогональных направлений максимальной дисперсии. +- **SVD** — разложение \(\mathbf{A}=\mathbf{U}\boldsymbol{\Sigma}\mathbf{V}^\top\). diff --git a/modules/03_probability_statistics/README.md b/modules/03_probability_statistics/README.md new file mode 100644 index 0000000..3b31a0e --- /dev/null +++ b/modules/03_probability_statistics/README.md @@ -0,0 +1,36 @@ +# Модуль 03. Вероятность, статистика и эксперименты + +Плановый срок: **недели 6–7**. + +## Цели + +- понимать случайные величины, распределения и условную вероятность +- оценивать неопределённость и корректно проверять гипотезы +- отличать корреляцию, предсказание и причинность + +## Предварительные знания + +Нужны базовая алгебра, суммы и функции, а для примеров — NumPy и понятие векторизации. + +## Теория + +1. [Вероятность, Байес, ожидание и распределения](notes/01_probability_bayes_distributions.md) +2. [Выборки, LLN, CLT, bootstrap и доверительные интервалы](notes/02_sampling_lln_clt_bootstrap_ci.md) +3. [Проверка гипотез, p-value, мощность и A/B-тесты](notes/03_hypothesis_testing_ab.md) + +Термины модуля: [`notes/glossary.md`](notes/glossary.md). + +Для каждого урока: прочитайте теорию, воспроизведите ключевой пример своими +руками и запишите три тезиса без подсказки. Формулы из этого модуля нужно +уметь связать с кодом и наблюдаемым поведением модели. + +## Задания + +- [`assignments/01_monte_carlo.md`](assignments/01_monte_carlo.md) — Монте-Карло и сходимость +- [`assignments/02_bayes.md`](assignments/02_bayes.md) — Байесовская диагностика +- [`assignments/03_ab_test.md`](assignments/03_ab_test.md) — Разбор A/B-теста + +## Завершение модуля + +Модуль завершён, когда все задания удовлетворяют критериям приёмки, +вы можете объяснить основные решения без ноутбука и получили review. diff --git a/modules/03_probability_statistics/assignments/01_monte_carlo.md b/modules/03_probability_statistics/assignments/01_monte_carlo.md new file mode 100644 index 0000000..11ae061 --- /dev/null +++ b/modules/03_probability_statistics/assignments/01_monte_carlo.md @@ -0,0 +1,41 @@ +# Монте-Карло и сходимость + +## Теория перед выполнением + +Перед началом прочитайте: + +- [`02_sampling_lln_clt_bootstrap_ci.md`](../notes/02_sampling_lln_clt_bootstrap_ci.md) + + +## Цель + +Оценить число π и вероятность редкого события; построить график ошибки от числа испытаний. + +## Что сделать + +1. Сформулировать ожидаемый результат до запуска эксперимента. +2. Реализовать простой baseline. +3. Реализовать основной метод самостоятельно настолько, насколько требует задание. +4. Добавить автоматические проверки ключевых вычислений. +5. Провести эксперимент, сохранить метрики и примеры ошибок. +6. Заполнить отчёт по `common/REPORT_TEMPLATE.md`. + +## Результат + +Ноутбук с симуляцией, доверительными интервалами и выводами. + +Решение разместите рядом в `solution/`. Не коммитьте большие данные, +веса или секреты. + +## Критерии приёмки + +- Показана зависимость ошибки порядка `1/sqrt(n)` и роль seed. +- Код читаем, разбит на функции и запускается по инструкции. +- Выводы подтверждаются измерениями и не выходят за пределы эксперимента. + +## Вопросы для самопроверки + +- Почему выбран именно этот метод и какой baseline он должен превзойти? +- Где здесь возможна утечка данных или некорректная оценка? +- Как изменится результат при других данных или ограничениях? +- Какую ошибку модели вы считаете наиболее важной и почему? diff --git a/modules/03_probability_statistics/assignments/02_bayes.md b/modules/03_probability_statistics/assignments/02_bayes.md new file mode 100644 index 0000000..a0aa9f1 --- /dev/null +++ b/modules/03_probability_statistics/assignments/02_bayes.md @@ -0,0 +1,41 @@ +# Байесовская диагностика + +## Теория перед выполнением + +Перед началом прочитайте: + +- [`01_probability_bayes_distributions.md`](../notes/01_probability_bayes_distributions.md) + + +## Цель + +Рассчитать вероятность заболевания после положительного теста при разных базовых частотах. + +## Что сделать + +1. Сформулировать ожидаемый результат до запуска эксперимента. +2. Реализовать простой baseline. +3. Реализовать основной метод самостоятельно настолько, насколько требует задание. +4. Добавить автоматические проверки ключевых вычислений. +5. Провести эксперимент, сохранить метрики и примеры ошибок. +6. Заполнить отчёт по `common/REPORT_TEMPLATE.md`. + +## Результат + +Расчёты, дерево вероятностей и объяснение base-rate fallacy. + +Решение разместите рядом в `solution/`. Не коммитьте большие данные, +веса или секреты. + +## Критерии приёмки + +- Не перепутаны `P(test|disease)` и `P(disease|test)`. +- Код читаем, разбит на функции и запускается по инструкции. +- Выводы подтверждаются измерениями и не выходят за пределы эксперимента. + +## Вопросы для самопроверки + +- Почему выбран именно этот метод и какой baseline он должен превзойти? +- Где здесь возможна утечка данных или некорректная оценка? +- Как изменится результат при других данных или ограничениях? +- Какую ошибку модели вы считаете наиболее важной и почему? diff --git a/modules/03_probability_statistics/assignments/03_ab_test.md b/modules/03_probability_statistics/assignments/03_ab_test.md new file mode 100644 index 0000000..a8ae017 --- /dev/null +++ b/modules/03_probability_statistics/assignments/03_ab_test.md @@ -0,0 +1,42 @@ +# Разбор A/B-теста + +## Теория перед выполнением + +Перед началом прочитайте: + +- [`03_hypothesis_testing_ab.md`](../notes/03_hypothesis_testing_ab.md) +- [`02_sampling_lln_clt_bootstrap_ci.md`](../notes/02_sampling_lln_clt_bootstrap_ci.md) + + +## Цель + +Смоделировать эксперимент, выбрать метрику, проверить гипотезу и оценить эффект. + +## Что сделать + +1. Сформулировать ожидаемый результат до запуска эксперимента. +2. Реализовать простой baseline. +3. Реализовать основной метод самостоятельно настолько, насколько требует задание. +4. Добавить автоматические проверки ключевых вычислений. +5. Провести эксперимент, сохранить метрики и примеры ошибок. +6. Заполнить отчёт по `common/REPORT_TEMPLATE.md`. + +## Результат + +Отчёт с CI, p-value, effect size и решением. + +Решение разместите рядом в `solution/`. Не коммитьте большие данные, +веса или секреты. + +## Критерии приёмки + +- Решение учитывает практическую, а не только статистическую значимость. +- Код читаем, разбит на функции и запускается по инструкции. +- Выводы подтверждаются измерениями и не выходят за пределы эксперимента. + +## Вопросы для самопроверки + +- Почему выбран именно этот метод и какой baseline он должен превзойти? +- Где здесь возможна утечка данных или некорректная оценка? +- Как изменится результат при других данных или ограничениях? +- Какую ошибку модели вы считаете наиболее важной и почему? diff --git a/modules/03_probability_statistics/notes/01_probability_bayes_distributions.md b/modules/03_probability_statistics/notes/01_probability_bayes_distributions.md new file mode 100644 index 0000000..99c5b9c --- /dev/null +++ b/modules/03_probability_statistics/notes/01_probability_bayes_distributions.md @@ -0,0 +1,159 @@ +# 1. Вероятность, Байес, ожидание и распределения + +> Вероятностная модель описывает не «хаос», а неопределённость и позволяет обновлять выводы при появлении данных. + +## Мотивация + +Классификатор возвращает 0,9, тест болезни — «положительно», а реклама получает 30 кликов из 1000. Без вероятностного языка нельзя отличить частоту от уверенности, учесть базовую распространённость события или оценить типичный разброс. После главы вы сможете строить простые модели, применять формулу Байеса и вычислять ожидание и дисперсию. + +## Интуиция + +Событие — проверяемое утверждение об исходе. Вероятность — согласованная мера его правдоподобия до наблюдения исхода. Условие \(B\) сужает множество рассматриваемых исходов, поэтому \(\mathbb P(A\mid B)\) обычно не равно \(\mathbb P(A)\). Распределение назначает вероятности всем допустимым значениям случайной величины. Ожидание — её долгосрочный центр тяжести, а дисперсия — средний квадрат отклонения от центра; это не гарантия отдельного исхода. + +## Определения и обозначения + +- **Вероятностное пространство** \((\Omega,\mathcal F,\mathbb P)\): исходы, допустимые события и мера с \(\mathbb P(\Omega)=1\), неотрицательностью и счётной аддитивностью. +- **Условная вероятность** при \(\mathbb P(B)>0\): + \(\mathbb P(A\mid B)=\mathbb P(A\cap B)/\mathbb P(B)\). +- **Независимость:** \(A\perp B\), если \(\mathbb P(A\cap B)=\mathbb P(A)\mathbb P(B)\). +- **Случайная величина** \(X\) отображает исход в число. Для дискретной величины \(p_X(x)=\mathbb P(X=x)\); для непрерывной плотность удовлетворяет \(\mathbb P(a\le X\le b)=\int_a^b p_X(x)\,dx\). +- **Функция распределения:** \(F_X(x)=\mathbb P(X\le x)\). +- \(\mathbb E[X]=\sum_x xp_X(x)\) или \(\int xp_X(x)\,dx\), если значение существует. +- \(\operatorname{Var}(X)=\mathbb E[(X-\mu)^2]\), \(\mu=\mathbb E[X]\); стандартное отклонение \(\sigma=\sqrt{\operatorname{Var}(X)}\). +- \(\operatorname{Cov}(X,Y)=\mathbb E[(X-\mathbb E X)(Y-\mathbb E Y)]\). + +Основные семейства: \(X\sim\operatorname{Bernoulli}(p)\) моделирует один успех; \(\operatorname{Binomial}(n,p)\) — число успехов в \(n\) независимых испытаниях; \(\operatorname{Poisson}(\lambda)\) — число редких событий на интервале; \(\operatorname{Uniform}(a,b)\) — равномерное значение; \(\mathcal N(\mu,\sigma^2)\) — симметричные колебания вокруг \(\mu\); \(\operatorname{Exponential}(\lambda)\) — время до пуассоновского события. + +## Основные распределения: support, PMF/PDF и моменты + +**PMF** \(p_X(x)=P(X=x)\) задаёт массу дискретной величины и суммируется в 1. +**PDF** \(f_X(x)\) задаёт плотность непрерывной величины и интегрируется в 1; +вероятность интервала равна интегралу, а значение плотности в точке не является +вероятностью. **Support** — множество значений, на которых масса или плотность +может быть ненулевой. + +- **Bernoulli \(\operatorname{Bernoulli}(p)\)**, \(0\le p\le1\). Support: + \(x\in\{0,1\}\). PMF: + \(P(X=x)=p^x(1-p)^{1-x}\). + \(\mathbb E[X]=p\), \(\operatorname{Var}(X)=p(1-p)\). +- **Binomial \(\operatorname{Binomial}(n,p)\)** — число успехов в \(n\) + независимых Bernoulli-испытаниях с постоянным \(p\). Support: + \(k=0,\ldots,n\). PMF: + \(P(X=k)=\binom nk p^k(1-p)^{n-k}\). + \(\mathbb E[X]=np\), \(\operatorname{Var}(X)=np(1-p)\). +- **Poisson \(\operatorname{Poisson}(\lambda)\)**, \(\lambda>0\), — число + событий на фиксированном интервале при постоянной интенсивности и независимых + приращениях. Support: \(k=0,1,\ldots\). PMF: + \(P(X=k)=e^{-\lambda}\lambda^k/k!\). + \(\mathbb E[X]=\lambda\), \(\operatorname{Var}(X)=\lambda\). +- **Normal \(\mathcal N(\mu,\sigma^2)\)**, \(\sigma>0\). Support: + \(x\in\mathbb R\). PDF: + \(f(x)=[\sigma\sqrt{2\pi}]^{-1} + \exp[-(x-\mu)^2/(2\sigma^2)]\). + \(\mathbb E[X]=\mu\), \(\operatorname{Var}(X)=\sigma^2\). +- **Exponential \(\operatorname{Exponential}(\lambda)\)**, \(\lambda>0\), — + время ожидания события в пуассоновском процессе. Support: \(x\ge0\). PDF: + \(f(x)=\lambda e^{-\lambda x}\) при \(x\ge0\), иначе 0. + \(\mathbb E[X]=1/\lambda\), \(\operatorname{Var}(X)=1/\lambda^2\). + Распределение обладает свойством отсутствия памяти: + \(P(X>s+t\mid X>s)=P(X>t)\). + +Параметризация важна: некоторые библиотеки принимают для Exponential масштаб +`scale = 1 / λ`, а для Normal — стандартное отклонение \(\sigma\), не дисперсию. + +## Вывод ключевых формул + +Из определения условной вероятности: + +\[ +\mathbb P(A\mid B)\mathbb P(B)=\mathbb P(A\cap B) +=\mathbb P(B\mid A)\mathbb P(A), +\] + +откуда формула Байеса + +\[ +\mathbb P(A\mid B)= +\frac{\mathbb P(B\mid A)\mathbb P(A)}{\mathbb P(B)}. +\] + +Если \(\{A_k\}\) — разбиение \(\Omega\), то по формуле полной вероятности + +\[ +\mathbb P(B)=\sum_k\mathbb P(B\mid A_k)\mathbb P(A_k). +\] + +Здесь \(\mathbb P(A)\) — prior, \(\mathbb P(B\mid A)\) — likelihood, \(\mathbb P(A\mid B)\) — posterior, знаменатель — evidence. + +Для дисперсии: + +\[ +\begin{aligned} +\operatorname{Var}(X) +&=\mathbb E[X^2-2\mu X+\mu^2]\\ +&=\mathbb E[X^2]-2\mu\mathbb E[X]+\mu^2\\ +&=\mathbb E[X^2]-\mu^2. +\end{aligned} +\] + +Линейность ожидания \(\mathbb E[aX+bY]=a\mathbb E[X]+b\mathbb E[Y]\) не требует независимости. Для независимых \(X,Y\): \(\operatorname{Var}(X+Y)=\operatorname{Var}(X)+\operatorname{Var}(Y)\), поскольку ковариация равна нулю. + +## Разобранный пример + +**Дано.** Болезнь есть у 1% людей. Чувствительность теста 95%, специфичность 90%. Найти вероятность болезни при положительном результате. + +**Решение.** Пусть \(D\) — болезнь, \(+\) — положительный тест. Ложноположительная вероятность равна \(0{,}10\). + +\[ +\mathbb P(+)=0{,}95\cdot0{,}01+0{,}10\cdot0{,}99=0{,}1085, +\] +\[ +\mathbb P(D\mid +)=\frac{0{,}95\cdot0{,}01}{0{,}1085}\approx0{,}0876. +\] + +**Интерпретация.** Даже хороший тест даёт лишь 8,76% из-за низкой базовой частоты. **Проверка:** среди 10 000 человек ожидаются 95 истинно положительных и 990 ложноположительных; \(95/(95+990)\) даёт тот же ответ. + +## Связь с Python и NumPy + +```python +import numpy as np + +prevalence, sensitivity, specificity = 0.01, 0.95, 0.90 +p_positive = sensitivity * prevalence + (1 - specificity) * (1 - prevalence) +posterior = sensitivity * prevalence / p_positive + +rng = np.random.default_rng(42) +disease = rng.random(1_000_000) < prevalence +positive = np.where(disease, rng.random(disease.size) < sensitivity, + rng.random(disease.size) < (1 - specificity)) +print(f"exact={posterior:.4f}, simulation={disease[positive].mean():.4f}") + +x = rng.binomial(n=10, p=0.3, size=100_000) +print("binomial mean/variance:", x.mean(), x.var()) +``` + +Симуляция сходится к точному значению, но не заменяет формулу. Форма массивов здесь `(1_000_000,)`. + +## Типичные ошибки и ограничения + +- Путать \(\mathbb P(A\mid B)\) и \(\mathbb P(B\mid A)\): это ошибка обратной вероятности. +- Считать нулевую ковариацию независимостью: обратное верно не всегда. +- Интерпретировать плотность как вероятность точки: у непрерывной величины \(\mathbb P(X=x)=0\). +- Применять биномиальную модель при зависимых испытаниях или меняющемся \(p\). +- Считать ожидание «наиболее вероятным» значением: оно может быть недостижимым. +- Игнорировать тяжёлые хвосты: у некоторых распределений ожидание или дисперсия не существуют. + +## Краткое резюме + +- Условная вероятность меняет пространство сравнения. +- Байес объединяет prior и likelihood; знаменатель учитывает все причины данных. +- Ожидание задаёт центр, дисперсия — квадратичный масштаб разброса. +- Выбор распределения обязан отражать механизм и ограничения данных. + +## Связанные материалы + +- Практика: [`../assignments/02_bayes.md`](../assignments/02_bayes.md). + +- Обозначения: [`../../../common/NOTATION.md`](../../../common/NOTATION.md). +- Следующая глава: [`02_sampling_lln_clt_bootstrap_ci.md`](02_sampling_lln_clt_bootstrap_ci.md). +- Термины: [`glossary.md`](glossary.md). diff --git a/modules/03_probability_statistics/notes/02_sampling_lln_clt_bootstrap_ci.md b/modules/03_probability_statistics/notes/02_sampling_lln_clt_bootstrap_ci.md new file mode 100644 index 0000000..9c07c3b --- /dev/null +++ b/modules/03_probability_statistics/notes/02_sampling_lln_clt_bootstrap_ci.md @@ -0,0 +1,161 @@ +# 2. Выборки, LLN, CLT, bootstrap и доверительные интервалы + +> Выборка даёт случайную оценку генеральной характеристики; статистика измеряет и сообщает эту неопределённость. + +## Мотивация + +Среднее по тысяче клиентов не равно истиннему среднему всех будущих клиентов. Нужны ответы на три вопроса: насколько оценка колеблется, как она меняется с размером выборки и какой диапазон параметров согласуется с данными. Это основа метрик моделей, опросов и экспериментов. + +## Интуиция + +Генеральная совокупность задаёт неизвестное распределение, а выборка \(X_1,\ldots,X_n\) — независимые одинаково распределённые наблюдения из него. Закон больших чисел говорит, что среднее стабилизируется. Центральная предельная теорема описывает форму его остаточных колебаний. Bootstrap имитирует повторные выборки, многократно извлекая \(n\) элементов с возвращением из уже увиденной выборки. Доверительный интервал относится к процедуре, а не назначает вероятность фиксированному параметру после наблюдения данных. + +## Определения и обозначения + +- **Статистика** \(T=T(X_1,\ldots,X_n)\) не зависит от неизвестных параметров. +- **Оценка** \(\hat\theta\) приближает параметр \(\theta\). +- **Смещение:** \(\operatorname{Bias}(\hat\theta)=\mathbb E[\hat\theta]-\theta\). +- **Стандартная ошибка:** \(\operatorname{SE}(\hat\theta)=\sqrt{\operatorname{Var}(\hat\theta)}\). +- **Квантиль** \(q_\alpha=\inf\{x:F(x)\ge\alpha\}\). +- **Доверительный интервал уровня \(1-\alpha\)** — случайный интервал \([L,U]\), для которого \(\mathbb P_\theta(L\le\theta\le U)\ge1-\alpha\). +- **Bootstrap-выборка** состоит из \(n\) наблюдений, случайно выбранных с возвращением из эмпирического распределения. + +## Вывод ключевых формул + +Для iid-наблюдений с \(\mathbb E[X_i]=\mu\), \(\operatorname{Var}(X_i)=\sigma^2<\infty\): + +\[ +\mathbb E[\bar X]=\mu,\qquad +\operatorname{Var}(\bar X) +=\operatorname{Var}\left(\frac1n\sum_iX_i\right) +=\frac{\sigma^2}{n}. +\] + +Слабый закон больших чисел утверждает \(\bar X\xrightarrow{p}\mu\). Например, неравенство Чебышёва даёт + +\[ +\mathbb P(|\bar X-\mu|\ge\varepsilon) +\le\frac{\sigma^2}{n\varepsilon^2}\to0. +\] + +Центральная предельная теорема: + +\[ +\frac{\sqrt n(\bar X-\mu)}{\sigma}\xrightarrow{d}\mathcal N(0,1). +\] + +Заменяя \(\sigma\) выборочным стандартным отклонением +\(s=\sqrt{\sum_i(X_i-\bar X)^2/(n-1)}\), получаем приближённый интервал + +\[ +\bar X\pm z_{1-\alpha/2}\frac{s}{\sqrt n}. +\] + +При нормальной совокупности и неизвестной \(\sigma\) точнее использовать квантиль \(t_{n-1,1-\alpha/2}\). Для доли \(\hat p\) стандартная ошибка приблизительно \(\sqrt{\hat p(1-\hat p)/n}\), но интервал Вальда ненадёжен при малых \(n\) и долях около 0 или 1. + +Bootstrap оценивает распределение \(T\): вычислить \(T_b^\ast\) для \(B\) повторных выборок. Percentile-интервал использует \([q^\ast_{\alpha/2},q^\ast_{1-\alpha/2}]\). Монте-Карло ошибка bootstrap убывает как \(B^{-1/2}\). + +## Оценивание Монте-Карло + +Если \(X_1,\ldots,X_n\) независимо получены из распределения \(P\), то интеграл +или ожидание \(\theta=\mathbb E_P[g(X)]\) оценивают средним + +\[ +\hat\theta_n=\frac1n\sum_{i=1}^n g(X_i),\qquad +\operatorname{SE}(\hat\theta_n)= +\frac{\sqrt{\operatorname{Var}(g(X))}}{\sqrt n}. +\] + +Отсюда типичный масштаб случайной ошибки — \(1/\sqrt n\): чтобы уменьшить её в +10 раз, обычно нужно примерно в 100 раз больше испытаний. + +Для оценки \(\pi\) генерируют точки \((U_i,V_i)\) равномерно в квадрате +\([-1,1]^2\) и берут индикатор +\(I_i=\mathbb 1\{U_i^2+V_i^2\le1\}\). Доля круга в квадрате равна +\(\pi/4\), поэтому + +\[ +\hat\pi=4\cdot\frac1n\sum_{i=1}^n I_i. +\] + +```python +import numpy as np + +rng = np.random.default_rng(42) +n = 100_000 +points = rng.uniform(-1, 1, size=(n, 2)) +inside = (points ** 2).sum(axis=1) <= 1 +pi_hat = 4 * inside.mean() +se_hat = 4 * np.sqrt(inside.mean() * (1 - inside.mean()) / n) +print(pi_hat, se_hat) +``` + +Для редкого события с вероятностью \(p\) относительная стандартная ошибка +обычной оценки долей примерно равна \(1/\sqrt{np}\). Если \(np\ll1\), симуляция +часто вообще не увидит событие и оценка 0 создаст ложную уверенность. Нужны +гораздо больший бюджет либо variance-reduction методы, например importance +sampling; при этом веса должны соответствовать изменённому распределению. + +## Разобранный пример + +**Дано.** Времена ответа: \(120,130,110,140,100\) мс. Найти 95% t-интервал среднего. + +**Решение.** \(\bar x=120\). Сумма квадратов отклонений равна \(1000\), поэтому +\(s=\sqrt{1000/4}\approx15{,}811\), \(SE=s/\sqrt5\approx7{,}071\). +Для 4 степеней свободы \(t_{0.975}\approx2{,}776\): + +\[ +120\pm2{,}776\cdot7{,}071 +=120\pm19{,}63=[100{,}37;\,139{,}63]. +\] + +**Проверка.** Интервал симметричен вокруг среднего и широк из-за \(n=5\). Увеличение \(n\) в четыре раза при том же разбросе примерно вдвое уменьшит стандартную ошибку. + +## Связь с Python и NumPy + +```python +import numpy as np + +x = np.array([120., 130., 110., 140., 100.]) +mean = x.mean() +se = x.std(ddof=1) / np.sqrt(x.size) +t_critical = 2.776 # df=4, 95% +print("t interval:", mean - t_critical * se, mean + t_critical * se) + +rng = np.random.default_rng(42) +indices = rng.integers(0, x.size, size=(20_000, x.size)) +boot_means = x[indices].mean(axis=1) +print("bootstrap percentile:", np.quantile(boot_means, [0.025, 0.975])) + +# LLN/CLT: распределение средних экспоненциальных наблюдений +means = rng.exponential(scale=2.0, size=(10_000, 100)).mean(axis=1) +print(means.mean(), means.std(), "theory SE", 2 / np.sqrt(100)) +``` + +Фиксированный seed делает пример воспроизводимым. Векторизация создаёт матрицу bootstrap-индексов формы `(20000, 5)`. + +## Типичные ошибки и ограничения + +- Удобная выборка не становится репрезентативной от большого \(n\); систематическое смещение LLN не исправляет. +- CLT не означает, что сами данные нормальны; нормальным становится стандартизованное среднее в пределе. +- Уменьшать SE как \(1/n\): верно \(1/\sqrt n\). +- Говорить «параметр с вероятностью 95% внутри уже вычисленного частотного интервала». +- Bootstrap не исправляет зависимость, цензурирование или selection bias; для временных рядов нужен block bootstrap. +- Percentile bootstrap может быть плох при сильном смещении, дискретности и на границе пространства параметров. +- Подбирать интервал после просмотра результата без поправки на множественность. + +## Краткое резюме + +- LLN обеспечивает состоятельность среднего, CLT — его асимптотическую форму. +- Стандартная ошибка среднего равна \(\sigma/\sqrt n\). +- Доверительный уровень — покрытие процедуры при повторении выборок. +- Bootstrap полезен для сложных статистик, но наследует дефекты исходной выборки. + +## Связанные материалы + +- Практика: [`../assignments/01_monte_carlo.md`](../assignments/01_monte_carlo.md). +- Практика: [`../assignments/03_ab_test.md`](../assignments/03_ab_test.md). + +- Предыдущая глава: [`01_probability_bayes_distributions.md`](01_probability_bayes_distributions.md). +- Следующая глава: [`03_hypothesis_testing_ab.md`](03_hypothesis_testing_ab.md). +- Термины: [`glossary.md`](glossary.md). diff --git a/modules/03_probability_statistics/notes/03_hypothesis_testing_ab.md b/modules/03_probability_statistics/notes/03_hypothesis_testing_ab.md new file mode 100644 index 0000000..a099629 --- /dev/null +++ b/modules/03_probability_statistics/notes/03_hypothesis_testing_ab.md @@ -0,0 +1,122 @@ +# 3. Проверка гипотез, p-value, мощность и A/B-тесты + +> Статистический тест ограничивает частоту ложных тревог при заранее определённой процедуре, но не измеряет практическую ценность эффекта. + +## Мотивация + +Разница конверсий 10,0% и 10,6% может быть эффектом продукта или шумом выборки. Решение требует заранее определить метрику, допустимый риск ошибки и минимально полезный эффект. Иначе остановка «когда стало значимо» и перебор сегментов создают ложные открытия. + +## Интуиция + +Нулевая гипотеза \(H_0\) задаёт базовый мир, например отсутствие эффекта. Тестовая статистика измеряет несовместимость данных с этим миром. p-value — вероятность при \(H_0\) получить статистику не менее экстремальную. Это не вероятность истинности \(H_0\). Порог \(\alpha\) задаёт долгосрочную частоту ошибок I рода; мощность определяет шанс обнаружить конкретный реальный эффект. + +## Определения и обозначения + +- \(H_0\) — нулевая, \(H_1\) — альтернативная гипотеза. +- **Ошибка I рода:** отклонить истинную \(H_0\), вероятность \(\alpha\). +- **Ошибка II рода:** не отклонить ложную \(H_0\), вероятность \(\beta\). +- **Мощность:** \(1-\beta\), зависящая от размера эффекта, шума, \(n\) и теста. +- **p-value:** \(\mathbb P_{H_0}(T\text{ не менее экстремальна, чем }t_{\rm obs})\). +- **Размер эффекта:** величина практически важного различия; для средних часто используют стандартизованное \(d=(\mu_B-\mu_A)/\sigma\). +- **A/B-тест:** рандомизированный контролируемый эксперимент с контрольной A и вариантом B. + +## Вывод ключевых формул + +Для независимых долей \(\hat p_A=x_A/n_A\), \(\hat p_B=x_B/n_B\), при \(H_0:p_A=p_B\) объединённая оценка + +\[ +\hat p=\frac{x_A+x_B}{n_A+n_B}. +\] + +Тогда + +\[ +z=\frac{\hat p_B-\hat p_A} +{\sqrt{\hat p(1-\hat p)(1/n_A+1/n_B)}} +\overset{H_0}{\approx}\mathcal N(0,1). +\] + +Для двусторонней альтернативы \(p=2[1-\Phi(|z|)]\). Интервал для разности использует необъединённую SE: + +\[ +(\hat p_B-\hat p_A)\pm z_{1-\alpha/2} +\sqrt{\frac{\hat p_A(1-\hat p_A)}{n_A} ++\frac{\hat p_B(1-\hat p_B)}{n_B}}. +\] + +Почему различаются SE: тест вычисляется в мире равенства \(p_A=p_B\), а интервал должен оценивать неопределённость без навязывания \(H_0\). + +При равных группах грубая оценка объёма для обнаружения абсолютного эффекта \(\delta\) около базовой доли \(p\): + +\[ +n_{\rm group}\approx +\frac{2p(1-p)(z_{1-\alpha/2}+z_{1-\beta})^2}{\delta^2}. +\] + +Это планировочная аппроксимация; точный расчёт учитывает обе предполагаемые доли. + +## Разобранный пример + +**Дано.** A: 1000 пользователей, 100 конверсий; B: 1000, 130. Двусторонний тест, \(\alpha=0{,}05\). + +**Решение.** \(\hat p_A=0{,}10\), \(\hat p_B=0{,}13\), \(\hat p=0{,}115\). + +\[ +SE_0=\sqrt{0{,}115\cdot0{,}885\cdot0{,}002}\approx0{,}01427, +\quad z=0{,}03/0{,}01427\approx2{,}10. +\] + +Двусторонний p-value примерно 0,0355, поэтому \(H_0\) отклоняется. Необъединённая SE равна примерно 0,01425; 95% CI эффекта: +\(0{,}03\pm1{,}96\cdot0{,}01425=[0{,}0021;0{,}0579]\). + +**Интерпретация.** Данные совместимы с абсолютным ростом от 0,21 до 5,79 п.п.; бизнес-решение зависит от стоимости внедрения. Относительный lift 30% не заменяет абсолютный эффект. + +## Связь с Python и NumPy + +```python +import numpy as np +from math import erf, sqrt + +n_a, x_a, n_b, x_b = 1000, 100, 1000, 130 +p_a, p_b = x_a / n_a, x_b / n_b +pooled = (x_a + x_b) / (n_a + n_b) +se0 = np.sqrt(pooled * (1 - pooled) * (1 / n_a + 1 / n_b)) +z = (p_b - p_a) / se0 +normal_cdf = lambda value: 0.5 * (1 + erf(value / sqrt(2))) +p_value = 2 * (1 - normal_cdf(abs(z))) + +se = np.sqrt(p_a * (1 - p_a) / n_a + p_b * (1 - p_b) / n_b) +ci = (p_b - p_a) + np.array([-1, 1]) * 1.96 * se +print(f"z={z:.3f}, p={p_value:.4f}, CI={ci}") +``` + +Код не требует сетевого доступа. Для малых ожидаемых частот применяют точный тест Фишера, доступный в SciPy, а не нормальную аппроксимацию. + +## Дизайн корректного A/B-теста + +До запуска зафиксируйте единицу рандомизации, primary metric, guardrail-метрики, MDE, \(\alpha\), мощность, объём и длительность. Проверяйте Sample Ratio Mismatch, но не исключайте пользователей по поведению после назначения группы. Анализируйте по intention-to-treat. Учитывайте недельную сезонность и зависимость нескольких наблюдений одного пользователя. Для многих вариантов применяйте Holm или контролируйте FDR; для последовательного просмотра используйте заранее выбранный sequential design. + +## Типичные ошибки и ограничения + +- \(p<0{,}05\) не означает «вероятность случайности меньше 5%». +- Незначимый результат не доказывает равенство; возможно, мощности недостаточно. +- Статистическая значимость не гарантирует практическую. +- Peeking и остановка на первом \(p<\alpha\) повышают ошибку I рода. +- Множественные метрики и сегменты без поправки создают ложные открытия. +- Нарушение рандомизации, interference и attrition делают причинный вывод ненадёжным. +- Корреляция наблюдений требует кластерных ошибок или рандомизации по кластеру. + +## Краткое резюме + +- p-value оценивается при \(H_0\), а не даёт вероятность гипотезы. +- \(\alpha\) контролирует ложные тревоги, мощность — обнаружение заданного эффекта. +- Интервал эффекта информативнее одного бинарного решения. +- A/B-тест является причинным только при корректной рандомизации и анализе. + +## Связанные материалы + +- Практика: [`../assignments/03_ab_test.md`](../assignments/03_ab_test.md). + +- Выборки и интервалы: [`02_sampling_lln_clt_bootstrap_ci.md`](02_sampling_lln_clt_bootstrap_ci.md). +- Информационные меры: [`../../../common/foundations/information_theory.md`](../../../common/foundations/information_theory.md). +- Термины: [`glossary.md`](glossary.md). diff --git a/modules/03_probability_statistics/notes/glossary.md b/modules/03_probability_statistics/notes/glossary.md new file mode 100644 index 0000000..fdc8f25 --- /dev/null +++ b/modules/03_probability_statistics/notes/glossary.md @@ -0,0 +1,24 @@ +# Глоссарий модуля 03 + +- **A/B-тест** — рандомизированное сравнение контрольного и экспериментального вариантов. +- **Bootstrap** — оценивание выборочного распределения повторным извлечением с возвращением из эмпирической выборки. +- **Likelihood (правдоподобие)** — \(p(\text{данные}\mid\theta)\), рассматриваемое как функция параметра. +- **Prior / posterior** — распределение параметра до данных / после обновления по Байесу. +- **p-value** — при \(H_0\) вероятность результата не менее экстремального, чем наблюдаемый. +- **Байесовское обновление** — переход от prior к posterior через likelihood. +- **Выборка iid** — независимые одинаково распределённые наблюдения. +- **Доверительный интервал** — случайный интервал с заданным долговременным покрытием параметра. +- **Дисперсия** — \(\operatorname{Var}(X)=\mathbb E[(X-\mathbb EX)^2]\). +- **Закон больших чисел (LLN)** — сходимость выборочного среднего к ожиданию. +- **Ковариация** — совместная линейная изменчивость двух величин. +- **Математическое ожидание** — взвешенный распределением центр случайной величины. +- **Мощность** — вероятность отклонить ложную нулевую гипотезу. +- **Независимость** — факторизация совместного распределения в произведение маргинальных. +- **Ошибка I/II рода** — отклонение истинной \(H_0\) / неотклонение ложной \(H_0\). +- **Плотность** — функция, интеграл которой по области равен вероятности этой области. +- **Распределение** — правило назначения вероятностей значениям случайной величины. +- **Размер эффекта** — величина различия в предметно интерпретируемой или стандартизованной шкале. +- **Случайная величина** — числовая функция исхода случайного опыта. +- **Стандартная ошибка** — стандартное отклонение выборочного распределения оценки. +- **Условная вероятность** — вероятность события после ограничения на известное событие. +- **Центральная предельная теорема (CLT)** — асимптотическая нормальность стандартизованной суммы при подходящих условиях. diff --git a/modules/04_optimization/README.md b/modules/04_optimization/README.md new file mode 100644 index 0000000..a98d06c --- /dev/null +++ b/modules/04_optimization/README.md @@ -0,0 +1,36 @@ +# Модуль 04. Оптимизация и регуляризация + +Плановый срок: **недели 8–9**. + +## Цели + +- выводить и реализовывать градиенты +- понимать batch, stochastic и mini-batch gradient descent +- выбирать learning rate, оптимизатор и регуляризацию + +## Предварительные знания + +Нужны векторы и матрицы из модуля 02, ожидание из модуля 03 и базовое понимание производной. + +## Теория + +1. [Производные, градиент и поверхности потерь](notes/01_derivatives_and_loss_surfaces.md) +2. [GD, SGD, momentum, RMSProp и Adam](notes/02_optimizers.md) +3. [L1, L2, Elastic Net, early stopping и bias–variance](notes/03_regularization_bias_variance.md) + +Термины модуля: [`notes/glossary.md`](notes/glossary.md). + +Для каждого урока: прочитайте теорию, воспроизведите ключевой пример своими +руками и запишите три тезиса без подсказки. Формулы из этого модуля нужно +уметь связать с кодом и наблюдаемым поведением модели. + +## Задания + +- [`assignments/01_gradient_descent.md`](assignments/01_gradient_descent.md) — GD и SGD с нуля +- [`assignments/02_logistic_from_scratch.md`](assignments/02_logistic_from_scratch.md) — Логистическая регрессия с нуля +- [`assignments/03_regularization.md`](assignments/03_regularization.md) — L1 против L2 + +## Завершение модуля + +Модуль завершён, когда все задания удовлетворяют критериям приёмки, +вы можете объяснить основные решения без ноутбука и получили review. diff --git a/modules/04_optimization/assignments/01_gradient_descent.md b/modules/04_optimization/assignments/01_gradient_descent.md new file mode 100644 index 0000000..8120a78 --- /dev/null +++ b/modules/04_optimization/assignments/01_gradient_descent.md @@ -0,0 +1,42 @@ +# GD и SGD с нуля + +## Теория перед выполнением + +Перед началом прочитайте: + +- [`01_derivatives_and_loss_surfaces.md`](../notes/01_derivatives_and_loss_surfaces.md) +- [`02_optimizers.md`](../notes/02_optimizers.md) + + +## Цель + +Минимизировать квадратичную функцию полным, стохастическим и mini-batch градиентным спуском. + +## Что сделать + +1. Сформулировать ожидаемый результат до запуска эксперимента. +2. Реализовать простой baseline. +3. Реализовать основной метод самостоятельно настолько, насколько требует задание. +4. Добавить автоматические проверки ключевых вычислений. +5. Провести эксперимент, сохранить метрики и примеры ошибок. +6. Заполнить отчёт по `common/REPORT_TEMPLATE.md`. + +## Результат + +Траектории оптимизации, графики loss и сравнение методов. + +Решение разместите рядом в `solution/`. Не коммитьте большие данные, +веса или секреты. + +## Критерии приёмки + +- Показано влияние learning rate и шума; есть критерий остановки. +- Код читаем, разбит на функции и запускается по инструкции. +- Выводы подтверждаются измерениями и не выходят за пределы эксперимента. + +## Вопросы для самопроверки + +- Почему выбран именно этот метод и какой baseline он должен превзойти? +- Где здесь возможна утечка данных или некорректная оценка? +- Как изменится результат при других данных или ограничениях? +- Какую ошибку модели вы считаете наиболее важной и почему? diff --git a/modules/04_optimization/assignments/02_logistic_from_scratch.md b/modules/04_optimization/assignments/02_logistic_from_scratch.md new file mode 100644 index 0000000..1437717 --- /dev/null +++ b/modules/04_optimization/assignments/02_logistic_from_scratch.md @@ -0,0 +1,42 @@ +# Логистическая регрессия с нуля + +## Теория перед выполнением + +Перед началом прочитайте: + +- [`01_derivatives_and_loss_surfaces.md`](../notes/01_derivatives_and_loss_surfaces.md) +- [`02_optimizers.md`](../notes/02_optimizers.md) + + +## Цель + +Вывести binary cross-entropy и её градиент, реализовать обучение без sklearn. + +## Что сделать + +1. Сформулировать ожидаемый результат до запуска эксперимента. +2. Реализовать простой baseline. +3. Реализовать основной метод самостоятельно настолько, насколько требует задание. +4. Добавить автоматические проверки ключевых вычислений. +5. Провести эксперимент, сохранить метрики и примеры ошибок. +6. Заполнить отчёт по `common/REPORT_TEMPLATE.md`. + +## Результат + +Класс модели, gradient check и графики решения. + +Решение разместите рядом в `solution/`. Не коммитьте большие данные, +веса или секреты. + +## Критерии приёмки + +- Аналитический градиент совпадает с численным; нет вычисления `log(0)`. +- Код читаем, разбит на функции и запускается по инструкции. +- Выводы подтверждаются измерениями и не выходят за пределы эксперимента. + +## Вопросы для самопроверки + +- Почему выбран именно этот метод и какой baseline он должен превзойти? +- Где здесь возможна утечка данных или некорректная оценка? +- Как изменится результат при других данных или ограничениях? +- Какую ошибку модели вы считаете наиболее важной и почему? diff --git a/modules/04_optimization/assignments/03_regularization.md b/modules/04_optimization/assignments/03_regularization.md new file mode 100644 index 0000000..5241ac4 --- /dev/null +++ b/modules/04_optimization/assignments/03_regularization.md @@ -0,0 +1,41 @@ +# L1 против L2 + +## Теория перед выполнением + +Перед началом прочитайте: + +- [`03_regularization_bias_variance.md`](../notes/03_regularization_bias_variance.md) + + +## Цель + +Обучить модели на данных с шумовыми и коррелированными признаками. + +## Что сделать + +1. Сформулировать ожидаемый результат до запуска эксперимента. +2. Реализовать простой baseline. +3. Реализовать основной метод самостоятельно настолько, насколько требует задание. +4. Добавить автоматические проверки ключевых вычислений. +5. Провести эксперимент, сохранить метрики и примеры ошибок. +6. Заполнить отчёт по `common/REPORT_TEMPLATE.md`. + +## Результат + +Сравнение коэффициентов, качества и sparsity для L1/L2/Elastic Net. + +Решение разместите рядом в `solution/`. Не коммитьте большие данные, +веса или секреты. + +## Критерии приёмки + +- Объяснено, почему L1 зануляет веса, а стандартного универсального «L3» почти не используют. +- Код читаем, разбит на функции и запускается по инструкции. +- Выводы подтверждаются измерениями и не выходят за пределы эксперимента. + +## Вопросы для самопроверки + +- Почему выбран именно этот метод и какой baseline он должен превзойти? +- Где здесь возможна утечка данных или некорректная оценка? +- Как изменится результат при других данных или ограничениях? +- Какую ошибку модели вы считаете наиболее важной и почему? diff --git a/modules/04_optimization/notes/01_derivatives_and_loss_surfaces.md b/modules/04_optimization/notes/01_derivatives_and_loss_surfaces.md new file mode 100644 index 0000000..8b69d84 --- /dev/null +++ b/modules/04_optimization/notes/01_derivatives_and_loss_surfaces.md @@ -0,0 +1,172 @@ +# 1. Производные, градиент и поверхности потерь + +> Производная локально заменяет сложную функцию линейной моделью и показывает направление наиболее быстрого изменения. + +## Мотивация + +Обучение модели означает выбор параметров \(\boldsymbol\theta\), минимизирующих потерю \(L(\boldsymbol\theta)\). Перебирать непрерывное пространство невозможно. Производные позволяют предсказать изменение потери при малом шаге, вывести алгоритм и диагностировать плоские области, седловые точки и плохой масштаб признаков. + +## Интуиция + +Для скаляра касательная описывает локальный наклон. Для \(d\) параметров градиент собирает наклоны по координатам; его скалярное произведение с направлением даёт направленную производную. Градиент указывает самый крутой подъём в евклидовой геометрии, значит \(-\nabla L\) — спуск. Нулевой градиент не гарантирует минимум: это может быть максимум, седло или плоская площадка. + +## Определения и обозначения + +- **Производная:** \(f'(x)=\lim_{h\to0}[f(x+h)-f(x)]/h\). +- **Частная производная** меняет одну координату при фиксированных остальных. +- **Градиент:** \(\nabla_{\boldsymbol\theta}L\in\mathbb R^d\). +- **Направленная производная:** \(D_{\mathbf v}L=\nabla L^\top\mathbf v\), \(\|\mathbf v\|_2=1\). +- **Гессиан:** \(\mathbf H=\nabla^2 L\in\mathbb R^{d\times d}\). +- **Стационарная точка:** \(\nabla L=0\). +- **Выпуклость:** \(L(\lambda\mathbf x+(1-\lambda)\mathbf y)\le\lambda L(\mathbf x)+(1-\lambda)L(\mathbf y)\). +- **Эмпирический риск:** \(\hat R(\boldsymbol\theta)=n^{-1}\sum_i\ell(f_{\boldsymbol\theta}(\mathbf x^{(i)}),y^{(i)})\). + +Правила: \((f+g)'=f'+g'\), \((fg)'=f'g+fg'\), \((f/g)'=(f'g-fg')/g^2\), а цепное правило \((f\circ g)'(x)=f'(g(x))g'(x)\). + +## Вывод ключевых формул + +Дифференцируемость даёт разложение первого порядка: + +\[ +L(\boldsymbol\theta+\Delta) +=L(\boldsymbol\theta)+ +\nabla L(\boldsymbol\theta)^\top\Delta+o(\|\Delta\|). +\] + +По неравенству Коши—Буняковского при \(\|\mathbf v\|_2=1\): + +\[ +\nabla L^\top\mathbf v\ge-\|\nabla L\|_2, +\] + +равенство достигается при \(\mathbf v=-\nabla L/\|\nabla L\|_2\). Поэтому антиградиент — направление самого быстрого локального убывания. + +Для линейной модели \(\hat{\mathbf y}=\mathbf X\mathbf w\) и + +\[ +L(\mathbf w)=\frac1{2n}\|\mathbf X\mathbf w-\mathbf y\|_2^2 +\] + +обозначим \(\mathbf r=\mathbf X\mathbf w-\mathbf y\). Тогда + +\[ +dL=\frac1n\mathbf r^\top d\mathbf r +=\frac1n\mathbf r^\top\mathbf X\,d\mathbf w +=\left(\frac1n\mathbf X^\top\mathbf r\right)^\top d\mathbf w, +\] + +следовательно + +\[ +\nabla_{\mathbf w}L=\frac1n\mathbf X^\top(\mathbf X\mathbf w-\mathbf y), +\qquad +\nabla^2_{\mathbf w}L=\frac1n\mathbf X^\top\mathbf X\succeq0. +\] + +Размеры: \(\mathbf X^\top\in\mathbb R^{d\times n}\), \(\mathbf r\in\mathbb R^n\), градиент в \(\mathbb R^d\). Положительная полуопределённость гессиана доказывает выпуклость; строгая выпуклость требует полного столбцового ранга \(\mathbf X\). + +## Sigmoid, BCE и градиент логистической регрессии + +Для объекта \(\mathbf x_i\) логит +\(z_i=\mathbf x_i^\top\mathbf w+b\), а вероятность класса 1: + +\[ +p_i=\sigma(z_i)=\frac1{1+e^{-z_i}},\qquad +\sigma'(z_i)=p_i(1-p_i). +\] + +Binary cross-entropy для \(y_i\in\{0,1\}\): + +\[ +\ell_i=-y_i\log p_i-(1-y_i)\log(1-p_i). +\] + +По цепному правилу множители сокращаются: + +\[ +\frac{\partial\ell_i}{\partial z_i} +=\frac{\partial\ell_i}{\partial p_i} +\cdot +\frac{\partial p_i}{\partial z_i} +=p_i-y_i. +\] + +Для среднего loss по batch, \(\mathbf p=\sigma(\mathbf X\mathbf w+b)\): + +\[ +\nabla_{\mathbf w}L +=\frac1n\mathbf X^\top(\mathbf p-\mathbf y),\qquad +\frac{\partial L}{\partial b} +=\frac1n\sum_{i=1}^n(p_i-y_i). +\] + +Формы: \(\mathbf X:(n,d)\), \(\mathbf w:(d,)\), \(\mathbf p-\mathbf y:(n,)\), +градиент по весам `(d,)`. На практике BCE считают устойчиво по logits +(`logaddexp`), не берут `log(sigmoid(z))` напрямую для больших \(|z|\). + +## Разобранный пример + +**Дано.** \(\mathbf X=[[1],[2]]\), \(\mathbf y=[2,4]^\top\), \(w=0\), \(n=2\). + +**Решение.** Остаток \(\mathbf r=[-2,-4]^\top\). Потеря +\(L=(4+16)/4=5\). Градиент + +\[ +\nabla L=\frac12[1,2][-2,-4]^\top=-5. +\] + +При шаге \(\eta=0{,}1\), \(w_{\rm new}=0-0{,}1(-5)=0{,}5\). Новые остатки \([-1{,}5,-3]\), потеря \((2{,}25+9)/4=2{,}8125\). + +**Проверка.** Потеря уменьшилась; аналитический минимум \(w=2\). Конечная разность \([L(h)-L(-h)]/(2h)\) около \(-5\). + +## Связь с Python и NumPy + +```python +import numpy as np + +X = np.array([[1.0], [2.0]]) +y = np.array([2.0, 4.0]) +w = np.array([0.0]) + +def loss(w): + residual = X @ w - y + return residual @ residual / (2 * len(y)) + +gradient = X.T @ (X @ w - y) / len(y) +h = 1e-6 +numeric = (loss(w + h) - loss(w - h)) / (2 * h) +w_new = w - 0.1 * gradient +print(loss(w), gradient, numeric, loss(w_new)) +``` + +Численная проверка полезна, но слишком малый \(h\) усиливает ошибку округления, а большой — ошибку аппроксимации. + +## Геометрия поверхности потерь + +Собственные значения гессиана описывают кривизну. Все положительны — локальный минимум; все отрицательны — максимум; разные знаки — седло. Большое отношение \(\lambda_{\max}/\lambda_{\min}\) создаёт вытянутые уровни и зигзаги GD. Масштабирование признаков улучшает обусловленность. В нейросетях перестановочные симметрии дают множество эквивалентных минимумов, а невыпуклость исключает общую гарантию глобального решения. + +## Типичные ошибки и ограничения + +- Забывать цепное правило или усреднение по \(n\). +- Путать поэлементное `*` и матричное `@`. +- Считать \(\nabla L=0\) достаточным условием минимума. +- Проверять градиент только в точке с нулевой производной. +- Игнорировать размерности и неявный broadcasting. +- Делать вывод о глобальной поверхности по локальной квадратичной модели. + +## Краткое резюме + +- Градиент — коэффициент лучшей локальной линейной аппроксимации. +- Антиградиент даёт самый быстрый локальный спуск в норме \(L_2\). +- Гессиан описывает кривизну и тип стационарной точки. +- Масштаб и обусловленность поверхности определяют трудность оптимизации. + +## Связанные материалы + +- Практика: [`../assignments/01_gradient_descent.md`](../assignments/01_gradient_descent.md). +- Практика: [`../assignments/02_logistic_from_scratch.md`](../assignments/02_logistic_from_scratch.md). + +- Подробное исчисление: [`../../../common/foundations/calculus_and_matrix_calculus.md`](../../../common/foundations/calculus_and_matrix_calculus.md). +- Численная устойчивость: [`../../../common/foundations/numerical_stability.md`](../../../common/foundations/numerical_stability.md). +- Следующая глава: [`02_optimizers.md`](02_optimizers.md). +- Термины: [`glossary.md`](glossary.md). diff --git a/modules/04_optimization/notes/02_optimizers.md b/modules/04_optimization/notes/02_optimizers.md new file mode 100644 index 0000000..3ac40d7 --- /dev/null +++ b/modules/04_optimization/notes/02_optimizers.md @@ -0,0 +1,143 @@ +# 2. GD, SGD, momentum, RMSProp и Adam + +> Оптимизаторы различаются тем, какие данные используют на шаге и как преобразуют историю градиентов. + +## Мотивация + +Полный градиент точен, но дорог на миллионах объектов. Стохастический градиент дешёв, но шумен. Momentum сглаживает направление, RMSProp адаптирует масштаб координат, Adam сочетает обе идеи. Выбор шага и корректная реализация зачастую важнее названия оптимизатора. + +## Интуиция + +GD измеряет наклон по всей выборке. SGD использует один случайный объект, mini-batch — компромисс, хорошо использующий векторные вычисления. Шум mini-batch мешает точной сходимости, но ускоряет ранний прогресс. Momentum накапливает «скорость». RMSProp делит шаг на корень из недавних квадратов градиента. Adam ведёт экспоненциальные средние градиента и его квадрата и исправляет их начальное смещение к нулю. + +## Определения и обозначения + +Пусть \(L(\boldsymbol\theta)=n^{-1}\sum_i\ell_i(\boldsymbol\theta)\), итерация \(t\), learning rate \(\eta_t>0\), mini-batch \(\mathcal B_t\) размера \(m\): + +\[ +\mathbf g_t=\frac1m\sum_{i\in\mathcal B_t}\nabla\ell_i(\boldsymbol\theta_t). +\] + +При равномерной выборке \(\mathbb E[\mathbf g_t\mid\boldsymbol\theta_t]=\nabla L(\boldsymbol\theta_t)\). Эпоха — обработка примерно \(n\) примеров. + +## Вывод алгоритмов + +**GD/SGD/mini-batch:** + +\[ +\boldsymbol\theta_{t+1}=\boldsymbol\theta_t-\eta_t\mathbf g_t. +\] + +GD берёт \(m=n\), классический SGD — \(m=1\). Для квадратичной \(L(\theta)=a\theta^2/2\): + +\[ +\theta_{t+1}=(1-\eta a)\theta_t. +\] + +Сходимость требует \(|1-\eta a|<1\), то есть \(0<\eta<2/a\). В многомерной квадратичной задаче безопасный постоянный шаг связан с \(2/\lambda_{\max}(\mathbf H)\). + +**Momentum** в одной распространённой записи: + +\[ +\mathbf v_t=\beta\mathbf v_{t-1}+(1-\beta)\mathbf g_t,\qquad +\boldsymbol\theta_{t+1}=\boldsymbol\theta_t-\eta\mathbf v_t. +\] + +Некоторые библиотеки не используют множитель \(1-\beta\); тогда эффективный масштаб \(\eta\) другой. + +**RMSProp:** + +\[ +\mathbf s_t=\rho\mathbf s_{t-1}+(1-\rho)\mathbf g_t\odot\mathbf g_t, +\qquad +\boldsymbol\theta_{t+1}=\boldsymbol\theta_t- +\eta\frac{\mathbf g_t}{\sqrt{\mathbf s_t}+\varepsilon}. +\] + +**Adam:** + +\[ +\begin{aligned} +\mathbf m_t&=\beta_1\mathbf m_{t-1}+(1-\beta_1)\mathbf g_t,\\ +\mathbf v_t&=\beta_2\mathbf v_{t-1}+(1-\beta_2)\mathbf g_t^2,\\ +\hat{\mathbf m}_t&=\mathbf m_t/(1-\beta_1^t),\\ +\hat{\mathbf v}_t&=\mathbf v_t/(1-\beta_2^t),\\ +\boldsymbol\theta_{t+1} +&=\boldsymbol\theta_t-\eta\hat{\mathbf m}_t/ +(\sqrt{\hat{\mathbf v}_t}+\varepsilon). +\end{aligned} +\] + +Операции во второй момент и делении поэлементные. Bias correction нужен, потому что нулевая инициализация занижает ранние моменты. + +## Разобранный пример + +**Дано.** \(L(\theta)=\frac12(\theta-3)^2\), \(\theta_0=0\), \(\eta=0{,}5\). + +**Решение.** \(g_t=\theta_t-3\). GD: +\(\theta_1=0-0{,}5(-3)=1{,}5\); +\(\theta_2=1{,}5-0{,}5(-1{,}5)=2{,}25\); +\(\theta_3=2{,}625\). +Ошибка каждый шаг умножается на \(1-\eta=0{,}5\), поэтому стремится к нулю геометрически. + +**Проверка.** При \(\eta=2\) параметры чередуются между 0 и 6 и не сходятся; при \(\eta>2\) расходятся. + +## Связь с Python и NumPy + +```python +import numpy as np + +rng = np.random.default_rng(42) +X = rng.normal(size=(200, 2)) +true_w = np.array([2.0, -1.0]) +y = X @ true_w + rng.normal(scale=0.2, size=200) +w = np.zeros(2) +m = np.zeros(2) +v = np.zeros(2) +beta1, beta2, lr, eps = 0.9, 0.999, 0.05, 1e-8 + +for t in range(1, 501): + idx = rng.choice(len(y), size=32, replace=False) + residual = X[idx] @ w - y[idx] + g = X[idx].T @ residual / len(idx) + m = beta1 * m + (1 - beta1) * g + v = beta2 * v + (1 - beta2) * (g * g) + m_hat = m / (1 - beta1 ** t) + v_hat = v / (1 - beta2 ** t) + w -= lr * m_hat / (np.sqrt(v_hat) + eps) + +print("weights:", w, "full loss:", np.mean((X @ w - y) ** 2) / 2) +``` + +Перемешивание и seed делают стохастический эксперимент контролируемым. Loss для мониторинга лучше считать отдельно на train и validation. + +## Практический выбор + +Начните с масштабирования признаков и baseline. Для гладких небольших выпуклых задач полный GD или L-BFGS может быть эффективнее. Mini-batch выбирают по памяти и пропускной способности устройства. SGD с momentum часто даёт хорошую обобщающую способность; Adam быстро получает рабочий результат при разном масштабе градиентов. Learning-rate schedule (warmup, step decay, cosine) обычно важен. Gradient clipping ограничивает норму при взрывающихся градиентах, но не лечит первопричину. + +## Типичные ошибки и ограничения + +- Сравнивать оптимизаторы при разных бюджетах вычислений и без настройки \(\eta\). +- Считать Adam гарантированно лучшим или сходящимся во всех задачах. +- Забывать bias correction, \(\varepsilon\) или поэлементный квадрат. +- Использовать один случайный порядок данных и делать сильный вывод. +- Путать batch size с числом эпох. +- Оценивать качество только train loss: оптимизация и обобщение — разные задачи. +- Смешивать L2 в loss и decoupled weight decay: AdamW обновляет decay отдельно. + +## Краткое резюме + +- Mini-batch даёт несмещённый, но шумный градиент. +- Learning rate управляет устойчивостью и скоростью. +- Momentum усредняет направление, RMSProp — квадрат масштаба, Adam — оба момента. +- Оптимизатор нужно сравнивать по validation-метрике и вычислительному бюджету. + +## Связанные материалы + +- Практика: [`../assignments/01_gradient_descent.md`](../assignments/01_gradient_descent.md). +- Практика: [`../assignments/02_logistic_from_scratch.md`](../assignments/02_logistic_from_scratch.md). + +- Поверхности: [`01_derivatives_and_loss_surfaces.md`](01_derivatives_and_loss_surfaces.md). +- Следующая глава: [`03_regularization_bias_variance.md`](03_regularization_bias_variance.md). +- Численная устойчивость: [`../../../common/foundations/numerical_stability.md`](../../../common/foundations/numerical_stability.md). +- Термины: [`glossary.md`](glossary.md). diff --git a/modules/04_optimization/notes/03_regularization_bias_variance.md b/modules/04_optimization/notes/03_regularization_bias_variance.md new file mode 100644 index 0000000..42ba095 --- /dev/null +++ b/modules/04_optimization/notes/03_regularization_bias_variance.md @@ -0,0 +1,141 @@ +# 3. L1, L2, Elastic Net, early stopping и bias–variance + +> Регуляризация сознательно ограничивает подгонку train-данных, чтобы уменьшить ошибку на новых объектах. + +## Мотивация + +Гибкая модель может запомнить шум: train loss падает, validation loss растёт. Регуляризация кодирует предпочтение простых параметров, а early stopping ограничивает время подгонки. Цена — дополнительное смещение; выгода — меньшая чувствительность к конкретной выборке. + +## Интуиция + +L2 плавно сжимает все веса, особенно большие. L1 имеет излом в нуле и способна занулять коэффициенты. Elastic Net сочетает разреженность L1 со стабильностью L2 при коррелированных признаках. Early stopping прекращает движение до того, как модель подстроится под мелкие особенности train. Bias–variance — не выбор «одного добра»: оптимум балансирует систематическую ошибку, изменчивость и неустранимый шум. + +## Определения и обозначения + +Для параметров \(\mathbf w\in\mathbb R^d\): + +\[ +J(\mathbf w)=\hat R(\mathbf w)+ +\lambda_1\|\mathbf w\|_1+ +\frac{\lambda_2}{2}\|\mathbf w\|_2^2. +\] + +L1: \(\lambda_1>0,\lambda_2=0\); L2: наоборот; Elastic Net: обе положительны. Смещение \(b\) обычно не штрафуют. В sklearn параметр `alpha` может обозначать общую силу штрафа, а `l1_ratio` — долю L1; соглашения о множителях различаются. + +## Вывод ключевых формул + +Для ridge-регрессии + +\[ +J(\mathbf w)=\frac1{2n}\|\mathbf X\mathbf w-\mathbf y\|_2^2+ +\frac\lambda2\|\mathbf w\|_2^2. +\] + +Градиент: + +\[ +\nabla J=\frac1n\mathbf X^\top(\mathbf X\mathbf w-\mathbf y)+\lambda\mathbf w. +\] + +Приравнивая нулю, + +\[ +(\mathbf X^\top\mathbf X+n\lambda\mathbf I)\mathbf w +=\mathbf X^\top\mathbf y. +\] + +Добавка к диагонали улучшает обусловленность и даёт единственное решение при \(\lambda>0\), но вычислять обратную матрицу явно не следует — используют линейный решатель. + +Для одномерного L1 и квадратичной потери + +\[ +\min_w\frac12(w-z)^2+\lambda|w|. +\] + +При \(w>0\): \(w-z+\lambda=0\), значит \(w=z-\lambda\), допустимо при \(z>\lambda\). При \(w<0\): \(w=z+\lambda\), допустимо при \(z<-\lambda\). В нуле субградиент \(\partial|w|=[-1,1]\). Итого soft-threshold: + +\[ +w^\star=\operatorname{sign}(z)\max(|z|-\lambda,0). +\] + +Это объясняет точные нули L1. + +Для квадратичной ошибки прогноза в точке \(\mathbf x\), усреднённой по возможным train-выборкам: + +\[ +\mathbb E[(Y-\hat f(\mathbf x))^2] +=\sigma_\varepsilon^2+ +(\mathbb E\hat f(\mathbf x)-f(\mathbf x))^2+ +\mathbb E[(\hat f(\mathbf x)-\mathbb E\hat f(\mathbf x))^2]. +\] + +Слагаемые — шум, bias² и variance. + +## Разобранный пример + +**Дано.** Ортогональный признак даёт ненормированный коэффициент \(z=0{,}8\), \(\lambda=0{,}3\). + +**Решение.** L1: \(w=0{,}8-0{,}3=0{,}5\). Для сопоставимой одномерной L2-задачи \(\frac12(w-z)^2+\frac\lambda2w^2\): +\(w-z+\lambda w=0\), поэтому \(w=z/(1+\lambda)=0{,}8/1{,}3\approx0{,}615\). + +Если \(z=0{,}2\), L1 даст ноль, L2 — \(0{,}154\). **Проверка:** L1 создаёт разреженность, L2 только сжимает конечный коэффициент. + +## Связь с Python и sklearn + +```python +import numpy as np +from sklearn.datasets import make_regression +from sklearn.linear_model import ElasticNet, Ridge +from sklearn.model_selection import train_test_split +from sklearn.pipeline import make_pipeline +from sklearn.preprocessing import StandardScaler +from sklearn.metrics import mean_squared_error + +X, y = make_regression(n_samples=600, n_features=30, n_informative=6, + noise=20, random_state=42) +X_tr, X_va, y_tr, y_va = train_test_split( + X, y, test_size=0.3, random_state=42 +) + +for model in [ + Ridge(alpha=10.0), + ElasticNet(alpha=0.1, l1_ratio=0.7, max_iter=20_000) +]: + pipe = make_pipeline(StandardScaler(), model) + pipe.fit(X_tr, y_tr) + pred = pipe.predict(X_va) + coef = pipe[-1].coef_ + print(type(model).__name__, mean_squared_error(y_va, pred), + "zeros", np.sum(np.isclose(coef, 0))) +``` + +Масштабирование находится внутри pipeline, поэтому оценивается только по train. `alpha` выбирают кросс-валидацией, а test оставляют для единственной итоговой оценки. + +## Early stopping + +После каждой эпохи измеряют validation loss, сохраняют лучшие параметры и останавливаются, если улучшения больше `min_delta` нет `patience` эпох. Возвращать нужно лучшую сохранённую модель, а не последнюю. Validation-набор участвует в выборе времени остановки и больше не является независимым test. В линейных итерационных задачах ранняя остановка подавляет направления, которые обучаются медленно, и действует как неявная регуляризация. + +## Типичные ошибки и ограничения + +- Применять штраф до масштабирования: коэффициенты разных единиц штрафуются несопоставимо. +- Штрафовать intercept без причины. +- Выбирать \(\lambda\) по test или preprocessing до разбиения — leakage. +- Считать L1 надёжным селектором среди сильно коррелированных признаков; выбор может быть нестабилен. +- Отождествлять малый коэффициент с малой причинной важностью. +- Останавливать по шумной train-метрике или не восстанавливать лучший checkpoint. +- Думать, что регуляризация исправляет неверные признаки, сдвиг распределения или плохую разметку. + +## Краткое резюме + +- L2 стабильно сжимает веса, L1 создаёт точные нули, Elastic Net сочетает свойства. +- Ridge добавляет \(\lambda\mathbf I\) к кривизне. +- Early stopping требует отдельной validation-метрики и сохранения лучшей модели. +- Сила регуляризации выбирает баланс bias–variance по данным. + +## Связанные материалы + +- Практика: [`../assignments/03_regularization.md`](../assignments/03_regularization.md). + +- Оптимизаторы: [`02_optimizers.md`](02_optimizers.md). +- Pipeline без leakage: [`../../05_data_and_visualization/notes/03_feature_engineering_pipelines_leakage.md`](../../05_data_and_visualization/notes/03_feature_engineering_pipelines_leakage.md). +- Термины: [`glossary.md`](glossary.md). diff --git a/modules/04_optimization/notes/glossary.md b/modules/04_optimization/notes/glossary.md new file mode 100644 index 0000000..b258643 --- /dev/null +++ b/modules/04_optimization/notes/glossary.md @@ -0,0 +1,23 @@ +# Глоссарий модуля 04 + +- **Adam** — адаптивный оптимизатор с оценками первого и второго моментов градиента. +- **Bias–variance decomposition** — разложение ожидаемой квадратичной ошибки на шум, квадрат смещения и дисперсию модели. +- **Early stopping** — остановка обучения по validation-метрике с восстановлением лучших параметров. +- **Elastic Net** — сумма L1- и L2-штрафов. +- **GD** — шаг по полному градиенту эмпирического риска. +- **Learning rate** \(\eta\) — масштаб обновления параметров. +- **Mini-batch** — подмножество объектов для одной оценки градиента. +- **Momentum** — экспоненциальное усреднение направлений градиента. +- **RMSProp** — адаптация шага по экспоненциальному среднему квадратов градиента. +- **SGD** — обновление по одному объекту или малой случайной группе. +- **L1-регуляризация** — штраф \(\lambda\|\mathbf w\|_1\), способствующий разреженности. +- **L2-регуляризация** — штраф \(\lambda\|\mathbf w\|_2^2/2\), плавно сжимающий веса. +- **Выпуклая функция** — функция, лежащая не выше хорды между любыми двумя точками графика. +- **Гессиан** — матрица вторых частных производных. +- **Градиент** — вектор частных производных скалярной функции. +- **Обусловленность** — чувствительность задачи и относительный масштаб кривизны по направлениям. +- **Поверхность потерь** — зависимость целевой функции от параметров. +- **Регуляризация** — ограничение модели ради лучшего обобщения. +- **Седловая точка** — стационарная точка с направлениями роста и убывания. +- **Субградиент** — обобщение градиента для выпуклой недифференцируемой функции. +- **Функция потерь** — численная цена ошибки модели. diff --git a/modules/05_data_and_visualization/README.md b/modules/05_data_and_visualization/README.md new file mode 100644 index 0000000..18a691b --- /dev/null +++ b/modules/05_data_and_visualization/README.md @@ -0,0 +1,36 @@ +# Модуль 05. Данные, EDA и визуализация + +Плановый срок: **недели 10–11**. + +## Цели + +- очищать таблицы и обнаруживать проблемы качества +- строить информативные, а не декоративные визуализации +- избегать leakage при подготовке признаков + +## Предварительные знания + +Нужны Python, NumPy и базовая статистика; полезно уметь работать в Jupyter и фиксировать seed. + +## Теория + +1. [pandas и качество табличных данных](notes/01_pandas_and_data_quality.md) +2. [Распределения, выбросы, зависимости и визуальная честность](notes/02_distributions_outliers_visual_honesty.md) +3. [Feature engineering, pipelines и data leakage](notes/03_feature_engineering_pipelines_leakage.md) + +Термины модуля: [`notes/glossary.md`](notes/glossary.md). + +Для каждого урока: прочитайте теорию, воспроизведите ключевой пример своими +руками и запишите три тезиса без подсказки. Формулы из этого модуля нужно +уметь связать с кодом и наблюдаемым поведением модели. + +## Задания + +- [`assignments/01_data_audit.md`](assignments/01_data_audit.md) — Аудит датасета +- [`assignments/02_eda_story.md`](assignments/02_eda_story.md) — История из данных +- [`assignments/03_pipeline.md`](assignments/03_pipeline.md) — Безопасный preprocessing + +## Завершение модуля + +Модуль завершён, когда все задания удовлетворяют критериям приёмки, +вы можете объяснить основные решения без ноутбука и получили review. diff --git a/modules/05_data_and_visualization/assignments/01_data_audit.md b/modules/05_data_and_visualization/assignments/01_data_audit.md new file mode 100644 index 0000000..84a1a52 --- /dev/null +++ b/modules/05_data_and_visualization/assignments/01_data_audit.md @@ -0,0 +1,41 @@ +# Аудит датасета + +## Теория перед выполнением + +Перед началом прочитайте: + +- [`01_pandas_and_data_quality.md`](../notes/01_pandas_and_data_quality.md) + + +## Цель + +Выбрать открытый табличный датасет и составить паспорт качества. + +## Что сделать + +1. Сформулировать ожидаемый результат до запуска эксперимента. +2. Реализовать простой baseline. +3. Реализовать основной метод самостоятельно настолько, насколько требует задание. +4. Добавить автоматические проверки ключевых вычислений. +5. Провести эксперимент, сохранить метрики и примеры ошибок. +6. Заполнить отчёт по `common/REPORT_TEMPLATE.md`. + +## Результат + +Отчёт о пропусках, дублях, типах, выбросах и потенциальных утечках. + +Решение разместите рядом в `solution/`. Не коммитьте большие данные, +веса или секреты. + +## Критерии приёмки + +- Каждая проблема сопровождается способом проверки и решением. +- Код читаем, разбит на функции и запускается по инструкции. +- Выводы подтверждаются измерениями и не выходят за пределы эксперимента. + +## Вопросы для самопроверки + +- Почему выбран именно этот метод и какой baseline он должен превзойти? +- Где здесь возможна утечка данных или некорректная оценка? +- Как изменится результат при других данных или ограничениях? +- Какую ошибку модели вы считаете наиболее важной и почему? diff --git a/modules/05_data_and_visualization/assignments/02_eda_story.md b/modules/05_data_and_visualization/assignments/02_eda_story.md new file mode 100644 index 0000000..d6aae8d --- /dev/null +++ b/modules/05_data_and_visualization/assignments/02_eda_story.md @@ -0,0 +1,41 @@ +# История из данных + +## Теория перед выполнением + +Перед началом прочитайте: + +- [`02_distributions_outliers_visual_honesty.md`](../notes/02_distributions_outliers_visual_honesty.md) + + +## Цель + +Сформулировать пять вопросов и ответить на них визуализациями. + +## Что сделать + +1. Сформулировать ожидаемый результат до запуска эксперимента. +2. Реализовать простой baseline. +3. Реализовать основной метод самостоятельно настолько, насколько требует задание. +4. Добавить автоматические проверки ключевых вычислений. +5. Провести эксперимент, сохранить метрики и примеры ошибок. +6. Заполнить отчёт по `common/REPORT_TEMPLATE.md`. + +## Результат + +Ноутбук с 6–10 графиками и краткими выводами. + +Решение разместите рядом в `solution/`. Не коммитьте большие данные, +веса или секреты. + +## Критерии приёмки + +- Оси и единицы подписаны; тип графика соответствует вопросу; причинность не приписана корреляции. +- Код читаем, разбит на функции и запускается по инструкции. +- Выводы подтверждаются измерениями и не выходят за пределы эксперимента. + +## Вопросы для самопроверки + +- Почему выбран именно этот метод и какой baseline он должен превзойти? +- Где здесь возможна утечка данных или некорректная оценка? +- Как изменится результат при других данных или ограничениях? +- Какую ошибку модели вы считаете наиболее важной и почему? diff --git a/modules/05_data_and_visualization/assignments/03_pipeline.md b/modules/05_data_and_visualization/assignments/03_pipeline.md new file mode 100644 index 0000000..79f3e42 --- /dev/null +++ b/modules/05_data_and_visualization/assignments/03_pipeline.md @@ -0,0 +1,41 @@ +# Безопасный preprocessing + +## Теория перед выполнением + +Перед началом прочитайте: + +- [`03_feature_engineering_pipelines_leakage.md`](../notes/03_feature_engineering_pipelines_leakage.md) + + +## Цель + +Собрать `ColumnTransformer` для числовых и категориальных признаков. + +## Что сделать + +1. Сформулировать ожидаемый результат до запуска эксперимента. +2. Реализовать простой baseline. +3. Реализовать основной метод самостоятельно настолько, насколько требует задание. +4. Добавить автоматические проверки ключевых вычислений. +5. Провести эксперимент, сохранить метрики и примеры ошибок. +6. Заполнить отчёт по `common/REPORT_TEMPLATE.md`. + +## Результат + +Pipeline с imputing, scaling/encoding и cross-validation. + +Решение разместите рядом в `solution/`. Не коммитьте большие данные, +веса или секреты. + +## Критерии приёмки + +- Все преобразования обучаются только на train-fold; код обрабатывает новые категории. +- Код читаем, разбит на функции и запускается по инструкции. +- Выводы подтверждаются измерениями и не выходят за пределы эксперимента. + +## Вопросы для самопроверки + +- Почему выбран именно этот метод и какой baseline он должен превзойти? +- Где здесь возможна утечка данных или некорректная оценка? +- Как изменится результат при других данных или ограничениях? +- Какую ошибку модели вы считаете наиболее важной и почему? diff --git a/modules/05_data_and_visualization/notes/01_pandas_and_data_quality.md b/modules/05_data_and_visualization/notes/01_pandas_and_data_quality.md new file mode 100644 index 0000000..178888b --- /dev/null +++ b/modules/05_data_and_visualization/notes/01_pandas_and_data_quality.md @@ -0,0 +1,149 @@ +# 1. pandas и качество табличных данных + +> Надёжный анализ начинается с явной схемы, проверяемых ключей и воспроизводимых преобразований, а не с первой модели. + +## Мотивация + +CSV может содержать даты как строки, дубликаты сущностей, разные единицы, пропуски-маркеры и join, незаметно размножающий строки. Код выполнится, но вывод станет неверным. Цель главы — безопасно загрузить, объединить, агрегировать и проверить таблицы. + +## Интуиция + +`DataFrame` — таблица с индексом строк и именованными столбцами, каждый столбец имеет dtype. Схема — контракт: смысл строки, допустимые типы, диапазоны, уникальность и правила пропусков. `groupby` реализует split–apply–combine. `merge` связывает строки по ключам; его кардинальность нужно знать до операции. Пропуск — факт отсутствия наблюдения, а не автоматически ноль. + +## Определения и обозначения + +- **Единица наблюдения** — сущность, которой соответствует одна строка. +- **Первичный ключ** — столбец или набор столбцов, однозначно идентифицирующий строку. +- **dtype** — физическое/логическое представление значений (`Int64`, `float64`, `string`, `category`, datetime). +- **Пропуск** — `NaN`, `pd.NA` или `NaT`; смысл зависит от механизма появления. +- **MCAR/MAR/MNAR** — пропуск не зависит от данных / зависит от наблюдаемого / зависит от самого отсутствующего значения. +- **Кардинальность join** — `one_to_one`, `one_to_many`, `many_to_one`, `many_to_many`. +- **Длинный формат** — наблюдение на строку и переменная на столбец; удобен для агрегации и графиков. + +## Загрузка и аудит + +Сначала определите ожидаемую схему и явно задайте parsing: + +```python +import pandas as pd +from io import StringIO + +raw = StringIO("""user_id,date,city,revenue +1,2026-01-01,Москва,100.5 +2,2026-01-01,Казань, +2,2026-01-02,Казань,70 +3,bad-date,Москва,-10 +""") + +df = pd.read_csv( + raw, + dtype={"user_id": "Int64", "city": "string", "revenue": "Float64"}, + na_values=["", "NA", "null"] +) +df["date"] = pd.to_datetime(df["date"], errors="coerce", utc=True) + +print(df.shape) +print(df.dtypes) +print(df.isna().sum()) +print(df.nunique(dropna=False)) +print(df["revenue"].describe()) +``` + +`errors="coerce"` делает плохие даты видимыми как `NaT`; считать это исправлением нельзя — строки нужно расследовать. Проверяйте `shape`, несколько строк, dtypes, доли пропусков, уникальность ключа, диапазоны, категории и согласованность между столбцами. + +## Join: точная семантика + +Пусть `orders` содержит много заказов пользователя, а `users` — одну строку на пользователя. Нужен `many_to_one`: + +```python +orders = pd.DataFrame({ + "order_id": [10, 11, 12], + "user_id": [1, 1, 2], + "amount": [50.0, 70.0, 30.0] +}) +users = pd.DataFrame({ + "user_id": [1, 2], + "segment": ["new", "returning"] +}) + +joined = orders.merge( + users, on="user_id", how="left", + validate="many_to_one", indicator=True +) +assert len(joined) == len(orders) +assert joined["_merge"].eq("both").all() +``` + +Если справа ключ дублируется, `validate` остановит расчёт. При many-to-many число строк может расти как произведение повторов ключа. До join проверяйте `duplicated`, после — число строк, покрытие и агрегированные суммы. + +## Groupby и вывод формул + +Среднее группы \(g\): + +\[ +\bar x_g=\frac{1}{n_g}\sum_{i:G_i=g}x_i. +\] + +Общее среднее не равно среднему групповых средних при разных \(n_g\): + +\[ +\bar x=\frac{\sum_g n_g\bar x_g}{\sum_g n_g}. +\] + +```python +summary = ( + joined.groupby("segment", dropna=False) + .agg( + orders=("order_id", "nunique"), + users=("user_id", "nunique"), + revenue=("amount", "sum"), + mean_order=("amount", "mean"), + ) + .reset_index() +) +print(summary) +``` + +`agg` уменьшает число строк, `transform` возвращает результат размера исходной группы, `filter` выбирает целые группы. Явно задавайте `dropna=False`, если пропущенная группа не должна исчезнуть. + +## Разобранный пример + +**Дано.** Сегмент A: 2 заказа со средним 60; B: 1 заказ со средним 30. + +**Решение.** Невзвешенное среднее групповых средних \((60+30)/2=45\). Верное среднее заказов: + +\[ +\frac{2\cdot60+1\cdot30}{3}=50. +\] + +**Проверка.** Прямая сумма \(50+70+30=150\), \(150/3=50\). Разница показывает опасность агрегации без весов. + +## Пропуски и типы + +Удаление допустимо только при обосновании и оценке изменившейся совокупности. Простая импутация и индикатор пропуска могут быть baseline, но параметры импутации обучают только на train. Не превращайте идентификаторы в числа для арифметики. Денежные расчёты могут требовать целых копеек или `Decimal`; float не представляет многие десятичные дроби точно. Категории нормализуют с учётом регистра, пробелов и справочника, сохраняя исходный столбец для аудита. + +## Типичные ошибки и ограничения + +- Использовать `inplace=True` как гарантию отсутствия копии или выигрыша памяти. +- Делать chained assignment; применяйте `.loc[mask, "col"] = value`. +- Считать пропуски нулями без предметного смысла. +- Терять строки с `groupby` из-за `dropna=True` по умолчанию. +- Выполнять many-to-many merge без контроля. +- Смешивать временные зоны или строки и datetime. +- Делать вывод по строкам, когда независимая единица — пользователь. +- Полагаться только на `describe`: логические нарушения требуют явных assertions. + +## Краткое резюме + +- Схема фиксирует единицу строки, типы, диапазоны и ключи. +- `merge(validate=...)` защищает кардинальность, `_merge` проверяет покрытие. +- `groupby` требует правильной единицы анализа и весов. +- Любое исправление данных должно быть воспроизводимым и проверяемым. + +## Связанные материалы + +- Практика: [`../assignments/01_data_audit.md`](../assignments/01_data_audit.md). + +- Следующая глава: [`02_distributions_outliers_visual_honesty.md`](02_distributions_outliers_visual_honesty.md). +- Численная устойчивость: [`../../../common/foundations/numerical_stability.md`](../../../common/foundations/numerical_stability.md). +- Термины: [`glossary.md`](glossary.md). diff --git a/modules/05_data_and_visualization/notes/02_distributions_outliers_visual_honesty.md b/modules/05_data_and_visualization/notes/02_distributions_outliers_visual_honesty.md new file mode 100644 index 0000000..0ac57e7 --- /dev/null +++ b/modules/05_data_and_visualization/notes/02_distributions_outliers_visual_honesty.md @@ -0,0 +1,126 @@ +# 2. Распределения, выбросы, зависимости и визуальная честность + +> EDA превращает таблицу в проверяемые утверждения о форме, качестве и связях данных; график обязан сохранять масштаб и неопределённость. + +## Мотивация + +Одинаковое среднее скрывает асимметрию, смеси групп и тяжёлые хвосты. Корреляция может появиться из-за третьей переменной, а обрезанная ось — визуально раздуть малый эффект. Нужна последовательность одномерного, двумерного и условного анализа, не подменяющая подтверждающую статистику. + +## Интуиция + +Распределение отвечает не только «где центр», но и «каков разброс, форма и хвосты». Выброс — наблюдение, необычное относительно модели или группы, а не автоматически ошибка. Зависимость означает изменение совместного распределения; корреляция Пирсона видит только линейную связь. Честная визуализация делает величину эффекта, знаменатель, шкалу и неопределённость заметными. + +## Определения и обозначения + +- **Медиана** \(q_{0.5}\) делит распределение пополам. +- **IQR** \(=q_{0.75}-q_{0.25}\) — устойчивый масштаб центральных 50%. +- **MAD** \(=\operatorname{median}|X-\operatorname{median}(X)|\). +- **Выброс по Tukey** — точка вне \([Q_1-1.5IQR,Q_3+1.5IQR]\); это эвристический флаг. +- **Корреляция Пирсона:** \(\rho=\operatorname{Cov}(X,Y)/(\sigma_X\sigma_Y)\). +- **Корреляция Спирмена** — корреляция рангов, измеряющая монотонную связь. +- **Confounder** — переменная, связанная и с фактором, и с исходом, создающая смешение. + +## Вывод ключевых мер + +Выборочная ковариация и корреляция: + +\[ +s_{xy}=\frac1{n-1}\sum_i(x_i-\bar x)(y_i-\bar y), +\qquad +r=\frac{s_{xy}}{s_xs_y}. +\] + +По неравенству Коши—Буняковского \(|r|\le1\). Сдвиг и положительное масштабирование не меняют \(r\), но один экстремальный объект может изменить его сильно. \(r=0\) исключает линейную, но не общую зависимость: для симметричного \(X\) и \(Y=X^2\) ковариация может быть нулевой. + +Правило Фридмана—Диакониса для ширины histogram bin: + +\[ +h=2\,IQR(X)n^{-1/3}. +\] + +Оно адаптируется к масштабу и размеру выборки, но не является единственно верным. Всегда проверяйте устойчивость формы к разумному числу bins. + +Для положительно асимметричных данных преобразование \(z=\log(1+x)\) сжимает правый хвост. Интерпретация меняется: разности в log-шкале примерно соответствуют относительным изменениям. + +## Разобранный пример + +**Дано.** Доходы \(30,32,31,29,300\) тыс. + +**Решение.** Среднее \(84{,}4\), медиана 31. По линейной интерполяции NumPy \(Q_1=30,Q_3=32\), \(IQR=2\), верхняя граница Tukey \(35\); 300 помечается как выброс. + +**Интерпретация.** Удалять 300 нельзя автоматически: это может быть реальный клиент и важная часть выручки. Нужно проверить источник, единицы и принадлежность к другой группе. **Проверка:** медиана почти не меняется, среднее резко меняется — распределение несимметрично. + +## Связь с Python, pandas и визуализацией + +```python +import numpy as np +import pandas as pd +import matplotlib.pyplot as plt + +rng = np.random.default_rng(42) +df = pd.DataFrame({ + "group": np.repeat(["A", "B"], 300), + "value": np.r_[rng.lognormal(3.0, 0.5, 300), + rng.lognormal(3.2, 0.7, 300)] +}) + +q1, q3 = df["value"].quantile([0.25, 0.75]) +iqr = q3 - q1 +flags = ~df["value"].between(q1 - 1.5 * iqr, q3 + 1.5 * iqr) +print(df.groupby("group")["value"].agg(["count", "mean", "median", "std"])) +print("Tukey flags:", flags.sum()) + +fig, axes = plt.subplots(1, 2, figsize=(9, 3.5)) +for name, part in df.groupby("group"): + axes[0].hist(part["value"], bins="fd", alpha=0.5, label=name) + axes[1].hist(np.log1p(part["value"]), bins="fd", alpha=0.5, label=name) +axes[0].set(title="Исходная шкала", xlabel="Значение", ylabel="Число наблюдений") +axes[1].set(title="log1p-шкала", xlabel="log(1 + значение)") +for ax in axes: + ax.legend() +fig.tight_layout() +plt.show() +``` + +Код показывает обе шкалы, а не скрывает хвост. Для больших scatter используйте прозрачность, hexbin или выборку, явно сообщая её размер. + +## Принципы визуальной честности + +1. Подписывайте оси, единицы, период и знаменатель. +2. Для столбцов количественной величины начинайте ось с нуля; иначе длина перестаёт кодировать отношение. Для line chart обрезка допустима при явной шкале и если важна динамика. +3. Не используйте площадь/объём 3D-объекта для кодирования одной величины. +4. Показывайте сырые точки или распределение, а не только среднее. +5. Добавляйте интервал неопределённости и описывайте его тип. +6. Сохраняйте одинаковые шкалы при сравнении панелей. +7. Используйте доступную палитру и не полагайтесь только на цвет. +8. Не выбирайте период, bins и категории лишь ради желаемого сюжета. + +## Зависимость и Simpson's paradox + +Общая связь может иметь противоположный знак внутри групп из-за разных долей групп. Поэтому после общей диаграммы анализируйте предметно важные страты. Однако бесконечный поиск сегментов создаёт множественные сравнения. EDA генерирует гипотезы; подтверждение требует новой выборки или заранее заданного теста. Корреляция не доказывает причинность: нужны временной порядок, контроль смешения или эксперимент. + +## Типичные ошибки и ограничения + +- Удалять все точки за \(1.5IQR\): правило не знает предметной области. +- Использовать z-score для сильно асимметричных тяжёлых хвостов. +- Сравнивать абсолютные количества при разных размерах групп. +- Рисовать среднее без размера выборки и разброса. +- Выдавать EDA-находку за подтверждённую гипотезу. +- Считать отсутствие корреляции отсутствием зависимости. +- Скрывать пропуски, фильтры или обрезанные оси. + +## Краткое резюме + +- Центр, разброс, форма и хвосты нужно анализировать вместе. +- Выброс — повод расследовать, а не команда удалить. +- Корреляция измеряет ассоциацию, но не причинность. +- Честный график сохраняет масштаб, контекст, знаменатель и неопределённость. + +## Связанные материалы + +- Практика: [`../assignments/02_eda_story.md`](../assignments/02_eda_story.md). + +- Качество таблиц: [`01_pandas_and_data_quality.md`](01_pandas_and_data_quality.md). +- Статистическая неопределённость: [`../../03_probability_statistics/notes/02_sampling_lln_clt_bootstrap_ci.md`](../../03_probability_statistics/notes/02_sampling_lln_clt_bootstrap_ci.md). +- Следующая глава: [`03_feature_engineering_pipelines_leakage.md`](03_feature_engineering_pipelines_leakage.md). +- Термины: [`glossary.md`](glossary.md). diff --git a/modules/05_data_and_visualization/notes/03_feature_engineering_pipelines_leakage.md b/modules/05_data_and_visualization/notes/03_feature_engineering_pipelines_leakage.md new file mode 100644 index 0000000..63b1708 --- /dev/null +++ b/modules/05_data_and_visualization/notes/03_feature_engineering_pipelines_leakage.md @@ -0,0 +1,146 @@ +# 3. Feature engineering, pipelines и data leakage + +> Любое преобразование, которое учится по данным, является частью модели и должно оцениваться только внутри train-fold. + +## Мотивация + +Медиана для импутации, словарь категорий, масштаб, отбор признаков и target encoding используют данные. Если вычислить их до разбиения или на всей выборке перед cross-validation, validation влияет на модель и метрика становится оптимистичной. Pipeline фиксирует порядок и границы обучения преобразований. + +## Интуиция + +Признак — измеримое представление информации, доступной в момент прогноза. `fit` извлекает состояние из train, `transform` применяет уже найденное правило, `predict` выдаёт прогноз. Pipeline гарантирует, что каждый fold обучает preprocessing заново. Leakage возникает не только через target: будущие события, дубликаты сущностей и агрегаты по всему периоду тоже передают недоступную информацию. + +## Определения и обозначения + +- **Feature engineering** — построение признаков из исходных наблюдений с учётом предметного смысла. +- **Transformer** — объект с `fit` и `transform`. +- **Pipeline** — последовательность transformers и финальной модели. +- **Data leakage** — использование при обучении информации, недоступной в реальном моменте прогноза или принадлежащей evaluation-набору. +- **Target leakage** — признак прямо или косвенно содержит целевое значение. +- **Train–serving skew** — различие расчёта признаков при обучении и эксплуатации. +- **One-hot encoding** — бинарные столбцы категорий. +- **Standardization:** \(z=(x-\hat\mu_{\rm train})/\hat\sigma_{\rm train}\). + +## Вывод преобразований + +Для train-столбца \(x_1,\ldots,x_n\): + +\[ +\hat\mu_{\rm train}=\frac1n\sum_i x_i,\qquad +\hat\sigma_{\rm train}= +\sqrt{\frac1n\sum_i(x_i-\hat\mu_{\rm train})^2}. +\] + +Validation-значение преобразуется как + +\[ +z_{\rm val}=\frac{x_{\rm val}-\hat\mu_{\rm train}} +{\hat\sigma_{\rm train}}, +\] + +а не по статистикам validation. Иначе модель косвенно видит его распределение. + +Для линейной модели категорию из \(k\) уровней кодируют \(k\) индикаторами; при intercept один столбец можно удалить для полной идентифицируемости, хотя регуляризованные реализации справляются с избыточностью. Неизвестные категории должны иметь заранее заданное поведение (`handle_unknown="ignore"`). + +Циклический признак периода \(P\): + +\[ +x_{\sin}=\sin(2\pi x/P),\qquad +x_{\cos}=\cos(2\pi x/P). +\] + +Два столбца сохраняют близость, например 23:00 и 00:00. Один `sin` неоднозначен. + +## Разобранный пример + +**Дано.** Train-возраст \(20,30,40\), validation-возраст 100. + +**Решение.** \(\hat\mu_{\rm train}=30\), +\(\hat\sigma_{\rm train}=\sqrt{(100+0+100)/3}\approx8{,}165\). +Тогда \(z_{\rm val}=(100-30)/8{,}165\approx8{,}573\). + +Если ошибочно масштабировать все четыре значения, среднее станет 47,5 и validation повлияет на собственное представление и представление train. **Проверка:** корректные train z-score имеют среднее 0; validation не обязано иметь среднее 0. + +## Безопасный pipeline в sklearn + +```python +import numpy as np +import pandas as pd +from sklearn.compose import ColumnTransformer +from sklearn.impute import SimpleImputer +from sklearn.linear_model import LogisticRegression +from sklearn.model_selection import cross_val_score +from sklearn.pipeline import Pipeline +from sklearn.preprocessing import OneHotEncoder, StandardScaler + +X = pd.DataFrame({ + "age": [20, 35, np.nan, 50, 42, 29, 61, 38, 25, 55], + "income": [30, 55, 40, 90, np.nan, 45, 110, 60, 35, 95], + "city": ["A", "B", "A", "C", "B", "A", "C", "B", "A", "C"] +}) +y = np.array([0, 0, 0, 1, 1, 0, 1, 1, 0, 1]) + +numeric = Pipeline([ + ("impute", SimpleImputer(strategy="median", add_indicator=True)), + ("scale", StandardScaler()) +]) +categorical = Pipeline([ + ("impute", SimpleImputer(strategy="most_frequent")), + ("onehot", OneHotEncoder(handle_unknown="ignore")) +]) +preprocess = ColumnTransformer([ + ("num", numeric, ["age", "income"]), + ("cat", categorical, ["city"]) +]) +model = Pipeline([ + ("preprocess", preprocess), + ("classifier", LogisticRegression(max_iter=2000)) +]) + +scores = cross_val_score(model, X, y, cv=5, scoring="roc_auc") +print("fold scores:", scores, "mean:", scores.mean()) +model.fit(X, y) +``` + +В каждом fold sklearn вызывает `fit` preprocessing только на его train-части. Малый набор здесь учебный; реальная оценка требует подходящего размера и схемы разбиения. + +## Разбиение по структуре данных + +Случайный split неверен, если строки одного пациента, пользователя или устройства попадают в обе части: применяйте group split. Для временного прогноза train должен предшествовать validation; rolling features вычисляют только по прошлому (`shift` перед `rolling`). Дубликаты и почти-дубликаты разделяют по сущности. При подборе гиперпараметров test не трогают: train/CV выбирает модель, test оценивает финальный процесс. + +## Практические признаки + +- Числовые: логарифмы положительных тяжёлых хвостов, отношения с контролем нулевого знаменателя, взаимодействия по гипотезе. +- Категориальные: one-hot для умеренной мощности; редкие категории объединяют правилом, обученным на train. +- Временные: календарные признаки, возраст события и lag, доступные на timestamp прогноза. +- Target/frequency encoding: вычислять out-of-fold, со сглаживанием; иначе особенно опасен leakage. +- Текстовые: словарь и IDF обучать внутри pipeline. + +Каждый признак документируйте: источник, формулу, единицы, timestamp доступности, обработку пропуска и поведение при неизвестном значении. + +## Типичные ошибки и ограничения + +- `fit_transform` на всей \(\mathbf X\) до CV. +- Отбор признаков по корреляции с \(y\) до разбиения. +- Агрегат «пожизненная сумма» для прогноза в прошлом. +- Случайный split временных или групповых данных. +- Target encoding без out-of-fold расчёта. +- Разные реализации признака в training и serving. +- Считать pipeline защитой от семантического leakage: он не знает, когда признак стал доступен. +- Создавать огромное число взаимодействий без регуляризации и validation. + +## Краткое резюме + +- Признак допустим только если доступен в момент реального прогноза. +- Все обучаемые преобразования fit только на train. +- Pipeline предотвращает процедурный leakage внутри CV. +- Время, группы и дубликаты определяют схему разбиения. + +## Связанные материалы + +- Практика: [`../assignments/03_pipeline.md`](../assignments/03_pipeline.md). + +- Качество данных: [`01_pandas_and_data_quality.md`](01_pandas_and_data_quality.md). +- Регуляризация: [`../../04_optimization/notes/03_regularization_bias_variance.md`](../../04_optimization/notes/03_regularization_bias_variance.md). +- Обозначения: [`../../../common/NOTATION.md`](../../../common/NOTATION.md). +- Термины: [`glossary.md`](glossary.md). diff --git a/modules/05_data_and_visualization/notes/glossary.md b/modules/05_data_and_visualization/notes/glossary.md new file mode 100644 index 0000000..79224f7 --- /dev/null +++ b/modules/05_data_and_visualization/notes/glossary.md @@ -0,0 +1,25 @@ +# Глоссарий модуля 05 + +- **ColumnTransformer** — применение разных preprocessing-цепочек к группам столбцов. +- **Data leakage** — попадание недоступной или evaluation-информации в обучение. +- **DataFrame** — двумерная индексированная таблица pandas. +- **EDA** — исследовательский анализ структуры, качества, распределений и связей. +- **Feature engineering** — построение признаковых представлений из исходных данных. +- **Groupby** — схема split–apply–combine для групповой агрегации. +- **IQR** — разность 75-го и 25-го процентилей. +- **Join / merge** — связывание таблиц по ключам. +- **Pipeline** — воспроизводимая последовательность preprocessing и модели. +- **Train–serving skew** — несовпадение расчёта признаков при обучении и применении. +- **Выброс** — наблюдение, необычное относительно выбранной модели или группы. +- **Единица наблюдения** — сущность, представленная одной строкой анализа. +- **Импутация** — заполнение пропусков по явно заданному правилу. +- **Кардинальность join** — отношение повторяемости ключей слева и справа. +- **Корреляция** — стандартизованная мера линейной ассоциации. +- **Медиана** — квантиль уровня 0,5. +- **One-hot encoding** — представление категории набором бинарных индикаторов. +- **Первичный ключ** — уникальный идентификатор строки. +- **Пропуск** — отсутствие наблюдаемого значения, имеющее собственный механизм. +- **Схема данных** — контракт типов, единиц, ключей, диапазонов и ограничений. +- **Target encoding** — кодирование категории статистикой цели, требующее out-of-fold расчёта. +- **Target leakage** — признак содержит цель или её недоступное следствие. +- **Визуальная честность** — сохранение масштаба, контекста, знаменателя и неопределённости на графике. diff --git a/modules/06_classical_ml/README.md b/modules/06_classical_ml/README.md new file mode 100644 index 0000000..5111181 --- /dev/null +++ b/modules/06_classical_ml/README.md @@ -0,0 +1,36 @@ +# Модуль 06. Классическое машинное обучение + +Плановый срок: **недели 12–14**. + +## Цели + +- формулировать задачи regression, classification и ranking +- выбирать baseline, loss, метрики и схему валидации +- понимать kNN, линейные модели, SVM и вероятностные модели + +## Предварительные знания + +Нужны подготовка данных без leakage, основы вероятности, функции потерь и градиентный спуск. + +## Теория + +1. [Разбиения данных, кросс-валидация и baseline](notes/01_validation_and_baselines.md) +2. [Линейные модели, kNN, Naive Bayes и SVM](notes/02_models.md) +3. [Метрики, пороги, калибровка и дисбаланс](notes/03_metrics_thresholds_and_calibration.md) + +Термины модуля: [`glossary.md`](glossary.md). + +Для каждого урока: прочитайте теорию, воспроизведите ключевой пример своими +руками и запишите три тезиса без подсказки. Формулы из этого модуля нужно +уметь связать с кодом и наблюдаемым поведением модели. + +## Задания + +- [`assignments/01_model_selection.md`](assignments/01_model_selection.md) — Выбор модели по свойствам данных +- [`assignments/02_imbalanced.md`](assignments/02_imbalanced.md) — Несбалансированная классификация +- [`assignments/03_regression.md`](assignments/03_regression.md) — Надёжная регрессия + +## Завершение модуля + +Модуль завершён, когда все задания удовлетворяют критериям приёмки, +вы можете объяснить основные решения без ноутбука и получили review. diff --git a/modules/06_classical_ml/assignments/01_model_selection.md b/modules/06_classical_ml/assignments/01_model_selection.md new file mode 100644 index 0000000..0d0ea73 --- /dev/null +++ b/modules/06_classical_ml/assignments/01_model_selection.md @@ -0,0 +1,42 @@ +# Выбор модели по свойствам данных + +## Теория перед выполнением + +Перед началом прочитайте: + +- [`01_validation_and_baselines.md`](../notes/01_validation_and_baselines.md) +- [`02_models.md`](../notes/02_models.md) + + +## Цель + +Сравнить kNN, logistic regression и SVM на двух синтетических наборах. + +## Что сделать + +1. Сформулировать ожидаемый результат до запуска эксперимента. +2. Реализовать простой baseline. +3. Реализовать основной метод самостоятельно настолько, насколько требует задание. +4. Добавить автоматические проверки ключевых вычислений. +5. Провести эксперимент, сохранить метрики и примеры ошибок. +6. Заполнить отчёт по `common/REPORT_TEMPLATE.md`. + +## Результат + +Таблица метрик, границы решений и аргументированный выбор. + +Решение разместите рядом в `solution/`. Не коммитьте большие данные, +веса или секреты. + +## Критерии приёмки + +- Учтены масштабирование, сложность границы и стоимость инференса. +- Код читаем, разбит на функции и запускается по инструкции. +- Выводы подтверждаются измерениями и не выходят за пределы эксперимента. + +## Вопросы для самопроверки + +- Почему выбран именно этот метод и какой baseline он должен превзойти? +- Где здесь возможна утечка данных или некорректная оценка? +- Как изменится результат при других данных или ограничениях? +- Какую ошибку модели вы считаете наиболее важной и почему? diff --git a/modules/06_classical_ml/assignments/02_imbalanced.md b/modules/06_classical_ml/assignments/02_imbalanced.md new file mode 100644 index 0000000..93fd93b --- /dev/null +++ b/modules/06_classical_ml/assignments/02_imbalanced.md @@ -0,0 +1,42 @@ +# Несбалансированная классификация + +## Теория перед выполнением + +Перед началом прочитайте: + +- [`03_metrics_thresholds_and_calibration.md`](../notes/03_metrics_thresholds_and_calibration.md) +- [`01_validation_and_baselines.md`](../notes/01_validation_and_baselines.md) + + +## Цель + +Решить задачу с редким положительным классом и подобрать threshold. + +## Что сделать + +1. Сформулировать ожидаемый результат до запуска эксперимента. +2. Реализовать простой baseline. +3. Реализовать основной метод самостоятельно настолько, насколько требует задание. +4. Добавить автоматические проверки ключевых вычислений. +5. Провести эксперимент, сохранить метрики и примеры ошибок. +6. Заполнить отчёт по `common/REPORT_TEMPLATE.md`. + +## Результат + +PR-кривая, confusion matrices и решение с учётом стоимости ошибок. + +Решение разместите рядом в `solution/`. Не коммитьте большие данные, +веса или секреты. + +## Критерии приёмки + +- Accuracy не используется как единственный критерий; threshold выбран на validation. +- Код читаем, разбит на функции и запускается по инструкции. +- Выводы подтверждаются измерениями и не выходят за пределы эксперимента. + +## Вопросы для самопроверки + +- Почему выбран именно этот метод и какой baseline он должен превзойти? +- Где здесь возможна утечка данных или некорректная оценка? +- Как изменится результат при других данных или ограничениях? +- Какую ошибку модели вы считаете наиболее важной и почему? diff --git a/modules/06_classical_ml/assignments/03_regression.md b/modules/06_classical_ml/assignments/03_regression.md new file mode 100644 index 0000000..82e14d0 --- /dev/null +++ b/modules/06_classical_ml/assignments/03_regression.md @@ -0,0 +1,42 @@ +# Надёжная регрессия + +## Теория перед выполнением + +Перед началом прочитайте: + +- [`02_models.md`](../notes/02_models.md) +- [`03_metrics_thresholds_and_calibration.md`](../notes/03_metrics_thresholds_and_calibration.md) + + +## Цель + +Сравнить linear, Huber и regularized regression при выбросах. + +## Что сделать + +1. Сформулировать ожидаемый результат до запуска эксперимента. +2. Реализовать простой baseline. +3. Реализовать основной метод самостоятельно настолько, насколько требует задание. +4. Добавить автоматические проверки ключевых вычислений. +5. Провести эксперимент, сохранить метрики и примеры ошибок. +6. Заполнить отчёт по `common/REPORT_TEMPLATE.md`. + +## Результат + +Cross-validation, residual plots и анализ ошибок. + +Решение разместите рядом в `solution/`. Не коммитьте большие данные, +веса или секреты. + +## Критерии приёмки + +- Объяснено различие MAE и RMSE; test-set не использован для подбора. +- Код читаем, разбит на функции и запускается по инструкции. +- Выводы подтверждаются измерениями и не выходят за пределы эксперимента. + +## Вопросы для самопроверки + +- Почему выбран именно этот метод и какой baseline он должен превзойти? +- Где здесь возможна утечка данных или некорректная оценка? +- Как изменится результат при других данных или ограничениях? +- Какую ошибку модели вы считаете наиболее важной и почему? diff --git a/modules/06_classical_ml/glossary.md b/modules/06_classical_ml/glossary.md new file mode 100644 index 0000000..1297654 --- /dev/null +++ b/modules/06_classical_ml/glossary.md @@ -0,0 +1,37 @@ +# Глоссарий модуля 06 + +- **Baseline** — простое воспроизводимое решение для проверки полезности модели. +- **Train / validation / test** — части данных для fit, выбора решений и + однократной итоговой оценки соответственно. +- **Кросс-валидация (CV)** — оценка процедуры по нескольким чередующимся holdout. +- **Nested CV** — внешний цикл оценки и внутренний цикл подбора. +- **Стратификация** — сохранение долей классов в разбиениях. +- **Утечка данных** — использование информации, недоступной при реальном прогнозе. +- **Параметр** — величина, оцениваемая алгоритмом на train. +- **Гиперпараметр** — настройка процедуры обучения, выбираемая на validation. +- **Эмпирический риск** — средняя loss на конечной выборке. +- **Регуляризация** — ограничение сложности через штраф или ограничение параметров. +- **Ridge / Lasso** — линейные модели с \(L_2\)-/\(L_1\)-штрафом. +- **Logit** — логарифм отношения шансов: \(\log[p/(1-p)]\). +- **kNN** — предсказание по ответам ближайших обучающих объектов. +- **Naive Bayes** — байесовский классификатор с условной независимостью признаков. +- **SVM** — модель максимального зазора с hinge loss. +- **Опорный вектор** — объект на границе зазора или нарушающий её. +- **Ядро** — функция скалярного произведения в неявном пространстве признаков. +- **Precision / recall** — доля верных среди срабатываний / найденных среди + истинно положительных. +- **ROC-AUC** — вероятность верно ранжировать случайную положительно-отрицательную + пару. +- **PR-AUC** — качество компромисса precision–recall по порогам. +- **Log loss** — отрицательное логарифмическое правдоподобие вероятностей. +- **Калибровка** — соответствие заявленной вероятности наблюдаемой частоте. +- **Порог** — правило преобразования непрерывного score в дискретное решение. +- **Дисбаланс классов** — существенно разные частоты классов. +- **Prior shift** — изменение долей классов при неизменных распределениях + признаков внутри классов. + +## Главы + +1. [`notes/01_validation_and_baselines.md`](notes/01_validation_and_baselines.md) +2. [`notes/02_models.md`](notes/02_models.md) +3. [`notes/03_metrics_thresholds_and_calibration.md`](notes/03_metrics_thresholds_and_calibration.md) diff --git a/modules/06_classical_ml/notes/01_validation_and_baselines.md b/modules/06_classical_ml/notes/01_validation_and_baselines.md new file mode 100644 index 0000000..38b9ab5 --- /dev/null +++ b/modules/06_classical_ml/notes/01_validation_and_baselines.md @@ -0,0 +1,156 @@ +# Разбиения данных, кросс-валидация и baseline + +> Цель главы — построить честный протокол эксперимента: оценивать не запоминание +> обучающей выборки, а качество будущих решений. + +## Мотивация и интуиция + +Модель выбирают по конечному числу наблюдений, но применяют к ещё не встреченным. +Обучающая ошибка оптимистична: алгоритм уже подстроился под эти объекты. Если +перебирать модели по test, test также становится частью обучения. Поэтому данные +разделяют по ролям, а простая контрольная модель показывает, даёт ли сложность +реальную пользу. + +- **train** — оценка параметров; +- **validation** — выбор признаков, модели, гиперпараметров и порога; +- **test** — одна финальная оценка уже зафиксированного решения; +- **baseline** — дешёвая содержательная точка отсчёта. + +## Определения и цель оценки + +Пусть \(\mathcal D=\{(\mathbf x^{(i)},y^{(i)})\}_{i=1}^n\) — независимые +наблюдения из того же распределения, что и будущие данные. Истинный риск + +\[ +R(f)=\mathbb E_{(\mathbf X,Y)\sim p_{\rm future}} +[\ell(f(\mathbf X),Y)] +\] + +неизвестен. Средний риск на независимом test, + +\[ +\hat R_{\rm test}(f)=\frac1{n_{\rm test}}\sum_i +\ell(f(\mathbf x^{(i)}),y^{(i)}), +\] + +оценивает его с дисперсией порядка \(1/n_{\rm test}\). Независимость нарушается, +если решения принимались после просмотра test. + +**Гиперпараметр** задаётся до обучения (например, сила регуляризации); +**параметр** оценивается на train (например, коэффициент регрессии). +**Утечка** возникает, когда при построении признака или модели используется +информация, недоступная в момент реального предсказания. + +## Схемы разбиения + +Случайное разбиение уместно только для взаимозаменяемых объектов. Стратификация +сохраняет доли классов, но не исправляет зависимость между строками. + +- Временные данные делят по времени: train раньше validation, validation раньше + test. Случайное перемешивание позволяет модели «увидеть будущее». +- Повторные измерения пользователя, пациента или устройства помещают целиком в + одну часть (`GroupKFold`), иначе модель узнаёт идентичность группы. +- Дубликаты и все версии одного исходного объекта не должны пересекать границу. +- Preprocessing (`StandardScaler`, imputing, отбор признаков) обучают только на + train каждого разбиения; безопасная реализация — `Pipeline`. + +## Кросс-валидация + +В \(K\)-fold CV индексы делят на непересекающиеся блоки \(I_1,\ldots,I_K\). +Для каждого \(k\) модель обучают на всех блоках, кроме \(I_k\): + +\[ +\hat R_{\rm CV}=\frac1K\sum_{k=1}^K +\frac1{|I_k|}\sum_{i\in I_k}\ell(f^{(-k)}(\mathbf x^{(i)}),y^{(i)}). +\] + +Каждый объект оценивается моделью, которая его не видела. Большое \(K\) уменьшает +смещение из-за меньшего train, но увеличивает стоимость и зависимость оценок. +Обычно \(K=5\) или \(10\). Разброс fold-метрик полезен, но их стандартное +отклонение не является строгим доверительным интервалом: обучающие части +перекрываются. + +При большом подборе нужен **nested CV**: внешний цикл оценивает процедуру целиком, +внутренний выбирает гиперпараметры. Иначе максимум среди шумных validation-оценок +систематически оптимистичен. + +## Baseline и learning curve + +Для регрессии baseline предсказывает среднее при MSE и медиану при MAE. Для +классификации — частый класс, априорные частоты или простое правило предметной +области. Baseline обязан проходить тот же pipeline и оцениваться тем же split. + +Кривая обучения сравнивает train и validation при росте train: + +- низкие обе метрики — высокое смещение: нужны признаки/более гибкая модель; +- высокая train и низкая validation — высокая дисперсия: больше данных, + регуляризация или упрощение; +- обе высоки — дальнейшее усложнение не обосновано. + +## Разобранный пример + +Есть 1000 заявок, положительный класс — 10%. Частый-class baseline имеет +accuracy \(900/1000=0.90\), но recall положительного класса \(0/100=0\). +Следовательно, «90%» не доказывает полезность. Если пять validation-fold дали +PR-AUC \(0.31,0.29,0.34,0.27,0.32\), среднее равно \(0.306\); сравнивать его +следует с базовой PR-AUC около prevalence \(0.10\), сохранив отдельный будущий +test для итоговой проверки. + +## Самостоятельный пример sklearn + +```python +from sklearn.datasets import make_classification +from sklearn.dummy import DummyClassifier +from sklearn.linear_model import LogisticRegression +from sklearn.model_selection import StratifiedKFold, cross_validate +from sklearn.pipeline import make_pipeline +from sklearn.preprocessing import StandardScaler + +X, y = make_classification( + n_samples=1000, weights=[0.9, 0.1], random_state=42 +) +cv = StratifiedKFold(5, shuffle=True, random_state=42) +models = { + "baseline": DummyClassifier(strategy="prior"), + "logreg": make_pipeline( + StandardScaler(), LogisticRegression(max_iter=2000) + ), +} +for name, model in models.items(): + scores = cross_validate( + model, X, y, cv=cv, scoring=["average_precision", "roc_auc"] + ) + print(name, scores["test_average_precision"].mean(), + scores["test_roc_auc"].mean()) +``` + +`Pipeline` заново оценивает среднее и масштаб внутри каждого train-fold. + +## Типичные ошибки и ограничения + +- **Подбор по test.** Test перестаёт быть независимым; заведите новый holdout. +- **Scaler до split.** Статистики validation просачиваются в train; используйте + pipeline. +- **Случайный split для времени/групп.** Оценка отвечает не реальному сценарию; + воспроизведите способ поступления будущих данных. +- **Только средняя метрика.** Исследуйте fold-разброс, подгруппы и ошибки. +- **Сравнение разных split.** Разница может быть свойством разбиения; используйте + одинаковые folds и бюджет настройки. +- **Неявная смена распределения.** Даже без утечки test не предскажет качество + после drift; нужен временной мониторинг. + +## Краткое резюме + +- Разделение ролей данных защищает оценку от адаптивного переобучения. +- CV оценивает процедуру обучения; nested CV — процедуру подбора. +- Baseline должен соответствовать loss и бизнес-цене ошибок. +- Split обязан отражать время, группы и реальный путь данных. + +## Связанные материалы + +- Практика: [`../assignments/01_model_selection.md`](../assignments/01_model_selection.md). +- Практика: [`../assignments/02_imbalanced.md`](../assignments/02_imbalanced.md). + +- Следующая глава: [`02_models.md`](02_models.md). +- Обозначения: [`../../../common/NOTATION.md`](../../../common/NOTATION.md). +- Термины: [`../glossary.md`](../glossary.md). diff --git a/modules/06_classical_ml/notes/02_models.md b/modules/06_classical_ml/notes/02_models.md new file mode 100644 index 0000000..d6caf69 --- /dev/null +++ b/modules/06_classical_ml/notes/02_models.md @@ -0,0 +1,195 @@ +# Линейные модели, kNN, Naive Bayes и SVM + +> Эта глава связывает четыре семейства моделей с их целями, предположениями, +> геометрией и наблюдаемым поведением. + +## Общая постановка + +Пусть \(\mathbf X\in\mathbb R^{n\times d}\), строка +\(\mathbf x^{(i)}\) — объект. Модель выбирают не по названию, а по индуктивному +предположению: линейная зависимость, локальная гладкость, условная независимость +или разделение классов с большим зазором. Масштабирование обязательно для +регуляризованных линейных моделей, kNN и SVM: иначе единицы измерения определяют +геометрию. + +## Линейная регрессия + +Модель \(\hat y=\mathbf w^\top\mathbf x+b\) минимизирует MSE: + +\[ +L(\mathbf w,b)=\frac1{2n}\|\mathbf X\mathbf w+b\mathbf1-\mathbf y\|_2^2. +\] + +После добавления столбца единиц обозначим расширенную матрицу +\(\tilde{\mathbf X}\) и параметры \(\boldsymbol\beta\). Тогда + +\[ +\nabla L=\frac1n\tilde{\mathbf X}^{\top} +(\tilde{\mathbf X}\boldsymbol\beta-\mathbf y). +\] + +Условие стационарности даёт нормальные уравнения +\(\tilde{\mathbf X}^{\top}\tilde{\mathbf X}\boldsymbol\beta +=\tilde{\mathbf X}^{\top}\mathbf y\). При полном столбцовом ранге +\(\hat{\boldsymbol\beta}=(\tilde{\mathbf X}^{\top}\tilde{\mathbf X})^{-1} +\tilde{\mathbf X}^{\top}\mathbf y\), но в коде устойчивее QR/SVD, а не явная +обратная матрица. + +Ridge добавляет \(\lambda\|\mathbf w\|_2^2/2\), уменьшая дисперсию и проблему +коллинеарности. Lasso добавляет \(\lambda\|\mathbf w\|_1\), создавая нулевые +коэффициенты, но нестабильно выбирает один из коррелирующих признаков. +Линейность относится к параметрам: полиномиальные признаки всё ещё допускают +линейную регрессию. + +**Предположения.** Для предсказания нужны репрезентативные данные и примерно +аддитивная зависимость. Независимость ошибок, постоянная дисперсия и нормальность +нужны прежде всего для классических доверительных интервалов, а не для самого +вычисления коэффициентов. Корреляция не превращает коэффициент в причинный эффект. + +## Логистическая регрессия + +Для \(Y\in\{0,1\}\) логит линейный: + +\[ +p_\theta(Y=1\mid\mathbf x)=\sigma(z),\quad +z=\mathbf w^\top\mathbf x+b,\quad \sigma(z)=\frac1{1+e^{-z}}. +\] + +Отношение шансов удовлетворяет +\(\log[p/(1-p)]=z\): увеличение \(x_j\) на единицу умножает шансы на \(e^{w_j}\) +при фиксированных остальных признаках. Максимизация правдоподобия Бернулли +эквивалентна минимизации log loss: + +\[ +L=-\frac1n\sum_i[y^{(i)}\log p_i+(1-y^{(i)})\log(1-p_i)]. +\] + +Поскольку \(d\ell/dz=p-y\), +\(\nabla_{\mathbf w}L=\mathbf X^\top(\mathbf p-\mathbf y)/n\). Уверенная ошибка +получает большой градиент. Граница при пороге \(0.5\) есть +\(\mathbf w^\top\mathbf x+b=0\), но рабочий порог выбирают отдельно. +Мультикласс обобщается softmax-регрессией. + +## k ближайших соседей + +kNN не подгоняет явную функцию: для запроса ищет множество \(N_k(\mathbf x)\). +Регрессия усредняет ответы, классификация голосует: + +\[ +\hat p(Y=c\mid\mathbf x)=\frac1k\sum_{i\in N_k(\mathbf x)} +\mathbb1\{y^{(i)}=c\}. +\] + +Малое \(k\) даёт низкое смещение и высокую дисперсию; большое — наоборот. +Взвешивание \(1/(r_i+\varepsilon)\) усиливает близких соседей. Основное +предположение — локальная близость признаков означает близость ответа. +В высокой размерности расстояния становятся похожими, объём локальной окрестности +растёт экспоненциально: это проклятие размерности. Обучение почти бесплатно, +предсказание и память дороги. + +## Наивный Байес + +По формуле Байеса + +\[ +p(y=c\mid\mathbf x)\propto p(y=c)p(\mathbf x\mid y=c). +\] + +Naive Bayes предполагает условную независимость признаков: + +\[ +p(\mathbf x\mid y=c)=\prod_{j=1}^d p(x_j\mid y=c). +\] + +Вычисляют логарифмы: +\(\log p(c\mid\mathbf x)=\mathrm{const}+\log p(c)+\sum_j\log p(x_j\mid c)\). +GaussianNB моделирует числовые признаки нормальными, MultinomialNB — счётчики, +BernoulliNB — бинарные признаки. Сглаживание Лапласа +\(\hat p=(N_{jc}+\alpha)/(N_c+\alpha V)\) не позволяет невиданному слову +обнулить произведение. Независимость почти всегда ложна, но граница решения +часто остаётся хорошей; вероятности при коррелирующих признаках могут быть +чрезмерно уверенными. + +## SVM и большой зазор + +Для \(y^{(i)}\in\{-1,+1\}\) функциональный отступ +\(m_i=y^{(i)}(\mathbf w^\top\mathbf x^{(i)}+b)\). Soft-margin SVM решает + +\[ +\min_{\mathbf w,b}\frac12\|\mathbf w\|_2^2+ +C\sum_i\max(0,1-m_i). +\] + +Первое слагаемое максимизирует геометрический зазор \(2/\|\mathbf w\|_2\), +hinge loss штрафует точки внутри полосы и ошибки. \(C\) — цена нарушения: +большое \(C\) уменьшает train-ошибки, но ослабляет регуляризацию. Только точки с +\(m_i\le1\) — опорные — влияют на решение. + +Kernel trick заменяет скалярное произведение на +\(K(\mathbf x,\mathbf z)=\phi(\mathbf x)^\top\phi(\mathbf z)\). Для RBF +\(K=\exp(-\gamma\|\mathbf x-\mathbf z\|^2)\): большая \(\gamma\) создаёт очень +локальную гибкую границу. Ядро должно задавать положительно полуопределённую +Gram-матрицу. Kernel SVM масштабируется плохо по \(n\); линейный SVM эффективнее +для больших разреженных данных. SVM score не является вероятностью; калибровка +требует отдельных данных. + +## Разобранный пример + +Для логистической модели \(z=-1+0.8x\), при \(x=2\): +\(z=0.6\), \(p=1/(1+e^{-0.6})\approx0.646\). При \(y=1\) +\(\ell=-\log0.646\approx0.437\), производная по \(z\) равна +\(p-y=-0.354\): градиентный шаг увеличит \(z\). Рост \(x\) на единицу умножает +шансы на \(e^{0.8}\approx2.23\), а не увеличивает вероятность на константу. + +## Самостоятельный пример sklearn + +```python +from sklearn.datasets import make_classification +from sklearn.linear_model import LogisticRegression +from sklearn.model_selection import train_test_split +from sklearn.pipeline import make_pipeline +from sklearn.preprocessing import StandardScaler +from sklearn.svm import SVC + +X, y = make_classification( + n_samples=600, n_features=8, class_sep=1.2, random_state=7 +) +X_train, X_test, y_train, y_test = train_test_split( + X, y, test_size=0.3, stratify=y, random_state=7 +) +for model in [ + LogisticRegression(max_iter=2000), + SVC(C=1.0, kernel="rbf", gamma="scale"), +]: + pipe = make_pipeline(StandardScaler(), model) + pipe.fit(X_train, y_train) + print(type(model).__name__, pipe.score(X_test, y_test)) +``` + +Замените последний estimator на `KNeighborsClassifier` или `GaussianNB` +(масштабирование для GaussianNB не обязательно) и сравните на одном split. + +## Типичные ошибки и ограничения + +- Коэффициенты без масштаба и проверки коллинеарности нельзя сравнивать. +- Логистическая регрессия задаёт линейный logit, а не линейную вероятность. +- kNN с ID, шумовыми признаками или необработанными единицами измеряет не то. +- Naive Bayes не подтверждает независимость; его вероятности проверяют отдельно. +- `SVC(probability=True)` выполняет дополнительную калибровку и дороже обучается. +- Выбор модели по train score смешивает способность запоминать и обобщать. + +## Краткое резюме + +- Линейные модели оптимизируют выпуклый риск и дают сильный baseline. +- kNN переносит локальную структуру, но страдает в высокой размерности. +- Naive Bayes силён при подходящей модели признаков, несмотря на грубое допущение. +- SVM контролирует границу через зазор, \(C\) и ядро. + +## Связанные материалы + +- Практика: [`../assignments/01_model_selection.md`](../assignments/01_model_selection.md). +- Практика: [`../assignments/03_regression.md`](../assignments/03_regression.md). + +- Предыдущая глава: [`01_validation_and_baselines.md`](01_validation_and_baselines.md). +- Следующая глава: [`03_metrics_thresholds_and_calibration.md`](03_metrics_thresholds_and_calibration.md). +- Термины: [`../glossary.md`](../glossary.md). diff --git a/modules/06_classical_ml/notes/03_metrics_thresholds_and_calibration.md b/modules/06_classical_ml/notes/03_metrics_thresholds_and_calibration.md new file mode 100644 index 0000000..edf1ebf --- /dev/null +++ b/modules/06_classical_ml/notes/03_metrics_thresholds_and_calibration.md @@ -0,0 +1,181 @@ +# Метрики, пороги, калибровка и дисбаланс + +> Метрика должна отражать цену ошибок, а порог — превращать score или вероятность +> в решение. Это разные части системы. + +## Регрессия + +Для ошибок \(e_i=\hat y_i-y_i\): + +\[ +\operatorname{MAE}=\frac1n\sum_i|e_i|,\qquad +\operatorname{MSE}=\frac1n\sum_i e_i^2,\qquad +\operatorname{RMSE}=\sqrt{\operatorname{MSE}}. +\] + +Минимизатор ожидаемого MSE — условное среднее +\(\mathbb E[Y\mid\mathbf X=\mathbf x]\); MAE — условная медиана. Поэтому метрики +задают разные целевые функции. RMSE имеет единицы ответа и сильнее MAE реагирует +на выбросы, но не равен «типичной ошибке». + +\[ +R^2=1-\frac{\sum_i(y_i-\hat y_i)^2} +{\sum_i(y_i-\bar y)^2} +\] + +сравнивает модель с предсказанием test-среднего. На test \(R^2\) может быть +отрицательным; это не ошибка вычисления, а проигрыш baseline. MAPE взрывается +около нулевых \(y_i\) и асимметрично штрафует ошибки. При разном масштабе +объектов полезны weighted loss, RMSLE или метрика предметной области, но её +семантика должна быть явно записана. + +## Матрица ошибок классификации + +При положительном классе: + +\[ +\mathrm{precision}=\frac{TP}{TP+FP},\quad +\mathrm{recall}=\frac{TP}{TP+FN},\quad +\mathrm{specificity}=\frac{TN}{TN+FP}, +\] +\[ +F_1=\frac{2PR}{P+R}=\frac{2TP}{2TP+FP+FN}. +\] + +Accuracy подходит, когда классы и цены ошибок близки. Balanced accuracy усредняет +recall классов. \(F_1\) игнорирует \(TN\), не учитывает разные цены \(FP/FN\) и +зависит от выбранного порога. Для multiclass заранее указывают averaging: +macro даёт классам равный вес, weighted учитывает support, micro суммирует +счётчики. + +## Ранжирование: ROC-AUC и PR-AUC + +Порог \(t\) задаёт \(\hat y=\mathbb1\{s(\mathbf x)\ge t\}\). ROC строит +\(\mathrm{TPR}\) против \(\mathrm{FPR}=FP/(FP+TN)\) по всем порогам. + +\[ +\mathrm{ROC\!-\!AUC}= +\mathbb P(s(\mathbf X^+)>s(\mathbf X^-)) ++\tfrac12\mathbb P(s(\mathbf X^+)=s(\mathbf X^-)). +\] + +Это вероятность правильно упорядочить случайную пару. ROC-AUC инвариантна к +строго монотонному преобразованию score и не проверяет вероятности. + +PR-кривая показывает precision против recall. При редком положительном классе +небольшой FPR может означать множество FP, поэтому PR-AUC часто информативнее. +Baseline average precision примерно равен prevalence, из-за чего PR-AUC нельзя +сравнивать между выборками с разной долей класса без оговорок. Конкретная +реализация площади (average precision или трапецоиды) также важна. + +## Log loss и калибровка + +\[ +\operatorname{LogLoss}=-\frac1n\sum_i +[y_i\log p_i+(1-y_i)\log(1-p_i)]. +\] + +Log loss — строго proper scoring rule: в ожидании оптимально сообщать истинную +вероятность. Уверенная ошибка дорога. Brier score +\(\frac1n\sum_i(p_i-y_i)^2\) тоже оценивает вероятности, но ограничен. + +**Калибровка** означает +\(\mathbb P(Y=1\mid\hat p\approx q)\approx q\): среди объектов с прогнозом +0.7 положительных около 70%. Это не то же, что discrimination: монотонное +искажение вероятностей сохраняет ROC-AUC, но портит калибровку. + +Platt scaling обучает sigmoid от score; isotonic regression — монотонную +кусочно-постоянную функцию и требует больше данных. Калибратор обучают на +данных, не использованных для fit исходной модели, либо через внутреннюю CV. +Reliability diagram зависит от binning; смотрят также Brier/log loss. + +## Порог как задача принятия решений + +Если калиброванная \(p=P(Y=1\mid x)\), цена FP равна \(C_{FP}\), FN — +\(C_{FN}\), а правильных решений — ноль, то: + +\[ +\text{предсказать 1, если }C_{FP}(1-p)\frac{C_{FP}}{C_{FP}+C_{FN}}. +\] + +Порог 0.5 оптимален только при равных ценах и корректных вероятностях. При +ограничении «проверить не более 100 заявок» выбирают top-100 или порог по +capacity. Порог настраивают на validation и замораживают до test. + +## Дисбаланс классов + +Сначала выбрать подходящую метрику и split, затем рассматривать: + +- `class_weight` или sample weights, меняющие оптимизируемую цену; +- undersampling/oversampling только внутри train-fold; +- настройку порога под цену или capacity; +- сбор данных и отдельный анализ подгрупп. + +SMOTE интерполирует меньшинство и может создавать невозможные объекты; до split +он вызывает утечку. Изменение долей классов обучением может испортить +вероятности. При prior shift с train-долей \(\pi_s\) и рабочей \(\pi_t\) +корректируют odds: + +\[ +\operatorname{odds}_t= +\operatorname{odds}_s +\frac{\pi_t/(1-\pi_t)}{\pi_s/(1-\pi_s)}, +\] + +если условные распределения признаков внутри классов неизменны. + +## Разобранный пример + +Из 1000 случаев положительных 20. Модель дала \(TP=15,FP=45,FN=5,TN=935\). +Accuracy \(=0.95\), precision \(=15/60=0.25\), recall \(=15/20=0.75\), +\(F_1=0.375\), FPR \(=45/980\approx0.046\). Высокая accuracy сосуществует с +75% ложных тревог среди срабатываний. Решение зависит от цены 45 проверок и 5 +пропусков, а не от одного универсального числа. + +## Самостоятельный пример sklearn + +```python +import numpy as np +from sklearn.metrics import ( + average_precision_score, confusion_matrix, log_loss, roc_auc_score +) + +y = np.array([0, 0, 0, 1, 1, 1]) +p = np.array([0.05, 0.20, 0.55, 0.40, 0.70, 0.90]) +print("ROC-AUC:", roc_auc_score(y, p)) +print("PR-AUC:", average_precision_score(y, p)) +print("log loss:", log_loss(y, p)) + +for threshold in (0.3, 0.5, 0.8): + pred = (p >= threshold).astype(int) + print(threshold, confusion_matrix(y, pred)) +``` + +Изменение порога меняет матрицу ошибок, но не ROC-AUC и PR-AUC score. + +## Типичные ошибки и ограничения + +- Выбирать метрику после просмотра результатов — скрытый множественный подбор. +- Считать ROC-AUC достаточной при крайнем дисбалансе — изучите PR и абсолютные FP. +- Калибровать и оценивать на одних данных — получите оптимистичную картину. +- Сравнивать macro и micro без указания averaging — это разные вопросы. +- Подбирать порог на test или переносить его при drift — нарушение протокола. +- Считать class weights заменой корректному порогу — weights меняют fit, не цену + конкретного production-решения автоматически. + +## Краткое резюме + +- MAE и MSE оценивают разные центральные характеристики ответа. +- Ranking, probability quality и hard decisions требуют разных метрик. +- Порог выводится из цены ошибок или операционного ограничения. +- Дисбаланс — задача оценки и принятия решений, не только resampling. + +## Связанные материалы + +- Практика: [`../assignments/02_imbalanced.md`](../assignments/02_imbalanced.md). +- Практика: [`../assignments/03_regression.md`](../assignments/03_regression.md). + +- Предыдущая глава: [`02_models.md`](02_models.md). +- Валидация: [`01_validation_and_baselines.md`](01_validation_and_baselines.md). +- Термины: [`../glossary.md`](../glossary.md). diff --git a/modules/07_trees_and_boosting/README.md b/modules/07_trees_and_boosting/README.md new file mode 100644 index 0000000..6e8f7fb --- /dev/null +++ b/modules/07_trees_and_boosting/README.md @@ -0,0 +1,36 @@ +# Модуль 07. Деревья, ансамбли и бустинг + +Плановый срок: **недели 15–16**. + +## Цели + +- понимать построение и переобучение дерева решений +- различать bagging, random forest и gradient boosting +- настраивать ансамбли и интерпретировать их без самообмана + +## Предварительные знания + +Нужны честная валидация, метрики классификации и регрессии, а также понятия bias и variance. + +## Теория + +1. [Дерево решений: критерии разбиения и pruning](notes/01_split_criteria_and_pruning.md) +2. [Bagging, Random Forest и out-of-bag оценка](notes/02_bagging_random_forest_oob.md) +3. [Градиентный бустинг](notes/03_gradient_boosting.md) + +Термины модуля: [`glossary.md`](glossary.md). + +Для каждого урока: прочитайте теорию, воспроизведите ключевой пример своими +руками и запишите три тезиса без подсказки. Формулы из этого модуля нужно +уметь связать с кодом и наблюдаемым поведением модели. + +## Задания + +- [`assignments/01_tree_from_scratch.md`](assignments/01_tree_from_scratch.md) — Дерево решений с нуля +- [`assignments/02_ensembles.md`](assignments/02_ensembles.md) — Bagging против boosting +- [`assignments/03_explainability.md`](assignments/03_explainability.md) — Интерпретация бустинга + +## Завершение модуля + +Модуль завершён, когда все задания удовлетворяют критериям приёмки, +вы можете объяснить основные решения без ноутбука и получили review. diff --git a/modules/07_trees_and_boosting/assignments/01_tree_from_scratch.md b/modules/07_trees_and_boosting/assignments/01_tree_from_scratch.md new file mode 100644 index 0000000..78ebdbe --- /dev/null +++ b/modules/07_trees_and_boosting/assignments/01_tree_from_scratch.md @@ -0,0 +1,41 @@ +# Дерево решений с нуля + +## Теория перед выполнением + +Перед началом прочитайте: + +- [`01_split_criteria_and_pruning.md`](../notes/01_split_criteria_and_pruning.md) + + +## Цель + +Реализовать неглубокое дерево классификации с Gini impurity. + +## Что сделать + +1. Сформулировать ожидаемый результат до запуска эксперимента. +2. Реализовать простой baseline. +3. Реализовать основной метод самостоятельно настолько, насколько требует задание. +4. Добавить автоматические проверки ключевых вычислений. +5. Провести эксперимент, сохранить метрики и примеры ошибок. +6. Заполнить отчёт по `common/REPORT_TEMPLATE.md`. + +## Результат + +Код узлов/split, тесты и визуализация дерева. + +Решение разместите рядом в `solution/`. Не коммитьте большие данные, +веса или секреты. + +## Критерии приёмки + +- Дерево корректно останавливается и воспроизводит простые обучающие примеры. +- Код читаем, разбит на функции и запускается по инструкции. +- Выводы подтверждаются измерениями и не выходят за пределы эксперимента. + +## Вопросы для самопроверки + +- Почему выбран именно этот метод и какой baseline он должен превзойти? +- Где здесь возможна утечка данных или некорректная оценка? +- Как изменится результат при других данных или ограничениях? +- Какую ошибку модели вы считаете наиболее важной и почему? diff --git a/modules/07_trees_and_boosting/assignments/02_ensembles.md b/modules/07_trees_and_boosting/assignments/02_ensembles.md new file mode 100644 index 0000000..47d6b41 --- /dev/null +++ b/modules/07_trees_and_boosting/assignments/02_ensembles.md @@ -0,0 +1,42 @@ +# Bagging против boosting + +## Теория перед выполнением + +Перед началом прочитайте: + +- [`02_bagging_random_forest_oob.md`](../notes/02_bagging_random_forest_oob.md) +- [`03_gradient_boosting.md`](../notes/03_gradient_boosting.md) + + +## Цель + +Сравнить дерево, random forest и gradient boosting на табличных данных. + +## Что сделать + +1. Сформулировать ожидаемый результат до запуска эксперимента. +2. Реализовать простой baseline. +3. Реализовать основной метод самостоятельно настолько, насколько требует задание. +4. Добавить автоматические проверки ключевых вычислений. +5. Провести эксперимент, сохранить метрики и примеры ошибок. +6. Заполнить отчёт по `common/REPORT_TEMPLATE.md`. + +## Результат + +Learning curves, метрики и анализ времени/качества. + +Решение разместите рядом в `solution/`. Не коммитьте большие данные, +веса или секреты. + +## Критерии приёмки + +- Гиперпараметры настраиваются только по validation; объяснены bias и variance. +- Код читаем, разбит на функции и запускается по инструкции. +- Выводы подтверждаются измерениями и не выходят за пределы эксперимента. + +## Вопросы для самопроверки + +- Почему выбран именно этот метод и какой baseline он должен превзойти? +- Где здесь возможна утечка данных или некорректная оценка? +- Как изменится результат при других данных или ограничениях? +- Какую ошибку модели вы считаете наиболее важной и почему? diff --git a/modules/07_trees_and_boosting/assignments/03_explainability.md b/modules/07_trees_and_boosting/assignments/03_explainability.md new file mode 100644 index 0000000..5ece346 --- /dev/null +++ b/modules/07_trees_and_boosting/assignments/03_explainability.md @@ -0,0 +1,41 @@ +# Интерпретация бустинга + +## Теория перед выполнением + +Перед началом прочитайте: + +- [`03_gradient_boosting.md`](../notes/03_gradient_boosting.md) + + +## Цель + +Сравнить permutation importance и SHAP на модели. + +## Что сделать + +1. Сформулировать ожидаемый результат до запуска эксперимента. +2. Реализовать простой baseline. +3. Реализовать основной метод самостоятельно настолько, насколько требует задание. +4. Добавить автоматические проверки ключевых вычислений. +5. Провести эксперимент, сохранить метрики и примеры ошибок. +6. Заполнить отчёт по `common/REPORT_TEMPLATE.md`. + +## Результат + +Глобальные и локальные объяснения, проверка коррелированных признаков. + +Решение разместите рядом в `solution/`. Не коммитьте большие данные, +веса или секреты. + +## Критерии приёмки + +- Отмечено, что важность не доказывает причинность и может быть нестабильной. +- Код читаем, разбит на функции и запускается по инструкции. +- Выводы подтверждаются измерениями и не выходят за пределы эксперимента. + +## Вопросы для самопроверки + +- Почему выбран именно этот метод и какой baseline он должен превзойти? +- Где здесь возможна утечка данных или некорректная оценка? +- Как изменится результат при других данных или ограничениях? +- Какую ошибку модели вы считаете наиболее важной и почему? diff --git a/modules/07_trees_and_boosting/glossary.md b/modules/07_trees_and_boosting/glossary.md new file mode 100644 index 0000000..9014b6b --- /dev/null +++ b/modules/07_trees_and_boosting/glossary.md @@ -0,0 +1,29 @@ +# Глоссарий модуля 07 + +- **Дерево решений** — рекурсивное разбиение пространства признаков на листья. +- **Узел / лист** — область до разбиения / конечная область с предсказанием. +- **Split** — условие, разделяющее объекты узла на дочерние части. +- **Impurity** — мера неоднородности ответов внутри узла. +- **Gini** — \(1-\sum_kp_k^2\), критерий классификационного узла. +- **Энтропия** — \(-\sum_kp_k\log p_k\), неопределённость метки. +- **Variance criterion** — средний квадрат отклонения ответа от среднего листа. +- **Pruning** — удаление ветвей для контроля сложности дерева. +- **Cost-complexity pruning** — минимизация ошибки плюс \(\alpha\) за каждый лист. +- **Bootstrap** — выборка с возвращением из исходного train. +- **Bagging** — усреднение моделей на разных bootstrap-выборках. +- **Random Forest** — bagging деревьев со случайным подмножеством признаков в узле. +- **OOB-объект** — объект, не попавший в bootstrap конкретного дерева. +- **OOB score** — оценка по агрегатам только тех деревьев, где объект был OOB. +- **Permutation importance** — падение holdout-метрики после перемешивания признака. +- **Boosting** — последовательное добавление моделей, исправляющих текущую ошибку. +- **Псевдоостаток** — отрицательный градиент loss по текущему score. +- **Learning rate** — множитель вклада очередного дерева. +- **Stochastic boosting** — boosting на случайной подвыборке объектов/признаков. +- **Early stopping** — остановка по независимой validation-метрике. +- **Histogram boosting** — поиск splits по дискретизированным значениям признаков. + +## Главы + +1. [`notes/01_split_criteria_and_pruning.md`](notes/01_split_criteria_and_pruning.md) +2. [`notes/02_bagging_random_forest_oob.md`](notes/02_bagging_random_forest_oob.md) +3. [`notes/03_gradient_boosting.md`](notes/03_gradient_boosting.md) diff --git a/modules/07_trees_and_boosting/notes/01_split_criteria_and_pruning.md b/modules/07_trees_and_boosting/notes/01_split_criteria_and_pruning.md new file mode 100644 index 0000000..954f629 --- /dev/null +++ b/modules/07_trees_and_boosting/notes/01_split_criteria_and_pruning.md @@ -0,0 +1,145 @@ +# Дерево решений: критерии разбиения и pruning + +> Дерево рекурсивно делит пространство признаков на области с простыми +> предсказаниями. Его сила — нелинейные взаимодействия, слабость — нестабильность. + +## Интуиция и определения + +Узел содержит множество индексов \(S\). Разбиение числового признака \(j\) по +порогу \(t\) создаёт +\(S_L=\{i\in S:x_j^{(i)}\le t\}\) и \(S_R=S\setminus S_L\). +Лист выдаёт среднее ответа в регрессии или частоты классов в классификации. +Граница получается ступенчатой и параллельной осям. + +Алгоритм CART жадный: в каждом узле выбирает лучший локальный split. Он не +гарантирует глобально минимального дерева; полный поиск комбинаторно дорог. +Для числового признака достаточно проверять середины между соседними уникальными +значениями после сортировки. + +## Уменьшение impurity + +Пусть \(I(S)\) — неоднородность узла. Качество split: + +\[ +\Delta I=I(S)-\frac{|S_L|}{|S|}I(S_L) +-\frac{|S_R|}{|S|}I(S_R). +\] + +Выбирается максимальное \(\Delta I\). Веса важны: отделение одного чистого +объекта не должно автоматически побеждать содержательное крупное разбиение. + +Для долей классов \(p_k=n_k/|S|\): + +\[ +I_{\rm Gini}=1-\sum_{k=1}^Kp_k^2,\qquad +I_{\rm entropy}=-\sum_{k=1}^Kp_k\log p_k. +\] + +Gini равен вероятности несовпадения двух меток при независимом выборе по +распределению узла. Энтропия — неопределённость метки. Оба равны нулю в чистом +узле и максимальны при равных долях. Энтропия сильнее реагирует около нулевых +вероятностей; на практике деревья часто похожи. + +В регрессии + +\[ +I_{\rm var}(S)=\frac1{|S|}\sum_{i\in S}(y_i-\bar y_S)^2. +\] + +Почему лист предсказывает \(\bar y_S\)? Для константы \(c\): + +\[ +\frac{d}{dc}\sum_{i\in S}(y_i-c)^2 +=2\sum_{i\in S}(c-y_i)=0\Rightarrow c=\bar y_S. +\] + +Уменьшение variance эквивалентно снижению суммы квадратов ошибок. Для absolute +error оптимальная константа — медиана. + +## Числовой пример + +В родительском узле метки \([0,0,0,1,1]\): +\(G=1-(3/5)^2-(2/5)^2=0.48\). Split даёт слева \([0,0,0]\), справа +\([1,1]\); обе impurity нулевые, \(\Delta G=0.48\). + +Другой split даёт \([0,0]\) и \([0,1,1]\). Справа +\(G_R=1-(1/3)^2-(2/3)^2=4/9\), поэтому +\(\Delta G=0.48-(3/5)(4/9)\approx0.213\). Первый split лучше. + +## Переобучение и pruning + +Без ограничений дерево способно изолировать почти каждый train-объект. +Предварительная регуляризация (`max_depth`, `min_samples_leaf`, +`min_samples_split`, `max_leaf_nodes`) запрещает сложность заранее. +Малый `min_samples_leaf` особенно опасен для шумных вероятностей. + +Cost-complexity pruning строит большое дерево и выбирает поддерево \(T\): + +\[ +R_\alpha(T)=R(T)+\alpha|T_{\rm leaves}|, +\] + +где \(R(T)\) — сумма ошибок листьев, \(\alpha\ge0\) — цена листа. Для внутреннего +узла сравнивают ошибку целой ветви с ошибкой после замены одним листом. При росте +\(\alpha\) последовательно удаляются ветви с наименьшей эффективной ценой. +`ccp_alpha` выбирают CV, не по train. + +## Практические свойства + +- Масштабирование монотонного числового признака не меняет порядок и splits. +- Дерево естественно моделирует взаимодействия: split ниже зависит от пути выше. +- Экстраполяция регрессии плоха: вне train-диапазона ответ остаётся константой + крайнего листа. +- Важность по суммарному impurity склонна завышать признаки с множеством + возможных разбиений; permutation importance оценивают на holdout. +- Категории требуют явного кодирования; one-hot может создавать неудобные splits, + а ordinal-коды навязывают ложный порядок. + +## Самостоятельный пример sklearn + +```python +from sklearn.datasets import load_breast_cancer +from sklearn.model_selection import train_test_split +from sklearn.tree import DecisionTreeClassifier, export_text + +X, y = load_breast_cancer(return_X_y=True) +Xtr, Xte, ytr, yte = train_test_split( + X, y, stratify=y, test_size=0.3, random_state=42 +) +tree = DecisionTreeClassifier( + criterion="gini", min_samples_leaf=8, ccp_alpha=0.002, + random_state=42 +) +tree.fit(Xtr, ytr) +print("train/test:", tree.score(Xtr, ytr), tree.score(Xte, yte)) +print(export_text(tree, max_depth=2)) +``` + +Сравните `ccp_alpha=0` и `min_samples_leaf=1` на том же split: train обычно +растёт, но test не обязан. + +## Типичные ошибки и ограничения + +- Выбирать глубину по test — утечка; используйте CV. +- Интерпретировать один нестабильный путь как причинное правило — проверьте + устойчивость на bootstrap и предметные ограничения. +- Считать impurity importance доказательством полезности — сравните permutation + importance и качество после удаления. +- Разрешать лист из одного объекта — частоты \(0/1\) не являются надёжными + вероятностями. +- Ожидать гладкость или экстраполяцию — это не индуктивное предположение дерева. + +## Краткое резюме + +- Split максимизирует взвешенное уменьшение impurity. +- Gini/entropy работают с классами, variance — с числовым ответом. +- Жадное дерево выразительно, но имеет высокую дисперсию. +- Ограничения и cost-complexity pruning выбирают по независимой оценке. + +## Связанные материалы + +- Практика: [`../assignments/01_tree_from_scratch.md`](../assignments/01_tree_from_scratch.md). + +- Следующая глава: [`02_bagging_random_forest_oob.md`](02_bagging_random_forest_oob.md). +- Теория информации: [`../../../common/foundations/information_theory.md`](../../../common/foundations/information_theory.md). +- Термины: [`../glossary.md`](../glossary.md). diff --git a/modules/07_trees_and_boosting/notes/02_bagging_random_forest_oob.md b/modules/07_trees_and_boosting/notes/02_bagging_random_forest_oob.md new file mode 100644 index 0000000..f6cf3dc --- /dev/null +++ b/modules/07_trees_and_boosting/notes/02_bagging_random_forest_oob.md @@ -0,0 +1,144 @@ +# Bagging, Random Forest и out-of-bag оценка + +> Ансамбль уменьшает нестабильность деревьев, если его участники сильны, но +> ошибаются не одинаково. + +## Bootstrap и bagging + +Bootstrap-выборка размера \(n\) получается \(n\) независимыми вытягиваниями с +возвращением из train. Один объект не попадёт в неё с вероятностью + +\[ +\left(1-\frac1n\right)^n\longrightarrow e^{-1}\approx0.368. +\] + +Значит, в выборке около \(63.2\%\) уникальных объектов, остальные повторяются. +Bagging обучает \(B\) базовых моделей \(f_b\) на разных bootstrap-выборках: + +\[ +\hat f(\mathbf x)=\frac1B\sum_{b=1}^B f_b(\mathbf x) +\] + +для регрессии; в классификации усредняются вероятности или голоса. Усреднение +почти не исправляет систематическое смещение, зато уменьшает дисперсию. + +Если каждая модель имеет дисперсию \(\sigma^2\), а попарная корреляция ошибок +равна \(\rho\), то + +\[ +\operatorname{Var}\left(\frac1B\sum_b f_b\right) +=\rho\sigma^2+\frac{1-\rho}{B}\sigma^2. +\] + +Первое слагаемое не исчезает даже при бесконечном \(B\). Поэтому нужны не только +многие, но и декоррелированные деревья. + +## Random Forest + +Random Forest добавляет к bootstrap случайность признаков: в каждом узле +рассматривается лишь подмножество из `max_features`. Сильный доминирующий признак +не появляется во всех деревьях, и ошибки меньше коррелируют. Цена — небольшое +увеличение bias отдельных деревьев; снижение общей variance обычно важнее. + +Деревья обычно растят глубокими, а регуляризацию обеспечивают усреднение, +`min_samples_leaf`, `max_features` и данные. Увеличение `n_estimators` не вызывает +классического переобучения само по себе: оценка стабилизируется, но стоимость +растёт. Слишком мало деревьев даёт Monte Carlo шум. + +Ключевые настройки: + +- `n_estimators` — достаточно для стабилизации метрики и важностей; +- `max_features` — компромисс силы и корреляции; +- `min_samples_leaf` — сглаживание локальных предсказаний; +- `class_weight`/sample weights — изменение цены объектов; +- `max_samples` — размер bootstrap и дополнительный контроль разнообразия. + +## Out-of-bag оценка + +Для каждого train-объекта \(i\) берут только деревья, в bootstrap которых он не +попал: + +\[ +\hat f_{\rm OOB}(\mathbf x^{(i)})= +\frac1{|B_i|}\sum_{b\in B_i}f_b(\mathbf x^{(i)}). +\] + +Так предсказание строится моделями, не обучавшимися на объекте, и OOB score +похож на внутреннюю CV почти без дополнительных fit. При \(B\) деревьях число +OOB-предсказаний объекта примерно \(0.368B\); при малом \(B\) оценка шумна. + +OOB не заменяет внешний test и не делает безопасными preprocessing или +гиперпараметрический поиск. Если строки зависимы по пользователю или времени, +обычный bootstrap и OOB нарушают структуру; нужен group/time-aware протокол. + +## Важности и неопределённость + +Mean decrease in impurity суммирует улучшения splits, но предпочитает непрерывные +и высококардинальные признаки. Permutation importance перемешивает столбец на +holdout и измеряет падение метрики. При коррелирующих признаках она делит или +скрывает важность: второй признак заменяет первый. + +Разброс предсказаний деревьев не является автоматически доверительным +интервалом: деревья зависимы и отражают лишь часть неопределённости. Для +интервалов нужны специализированные методы и проверка coverage. + +## Разобранный пример + +Пусть variance одного дерева \(100\), корреляция \(\rho=0.2\), \(B=50\). + +\[ +\operatorname{Var}(\bar f)=0.2\cdot100+ +\frac{0.8}{50}\cdot100=21.6. +\] + +При независимых деревьях было бы \(2\), но корреляция оставляет предел \(20\). +Увеличение \(B\) со 50 до 500 снизит variance лишь до \(20.16\). Уменьшение +корреляции через random subspace может быть ценнее. + +## Самостоятельный пример sklearn + +```python +from sklearn.datasets import load_breast_cancer +from sklearn.ensemble import RandomForestClassifier +from sklearn.model_selection import train_test_split + +X, y = load_breast_cancer(return_X_y=True) +Xtr, Xte, ytr, yte = train_test_split( + X, y, test_size=0.25, stratify=y, random_state=42 +) +forest = RandomForestClassifier( + n_estimators=400, max_features="sqrt", min_samples_leaf=2, + bootstrap=True, oob_score=True, n_jobs=-1, random_state=42 +) +forest.fit(Xtr, ytr) +print("OOB:", forest.oob_score_) +print("test:", forest.score(Xte, yte)) +``` + +OOB и test должны быть сопоставимы, но не обязаны совпадать: наборы и +эффективные размеры train различаются. + +## Типичные ошибки и ограничения + +- Делать bootstrap до train/test split — копии объекта попадают в test. +- Считать 100 соседних деревьев независимыми — корреляция ограничивает выигрыш. +- Настраивать десятки вариантов по OOB и сообщать тот же OOB как итог — оценка + адаптивно переобучена. +- Игнорировать группы/время — обычный OOB становится оптимистичным. +- Использовать impurity importance как причинный вывод. +- Ожидать экстраполяцию — forest усредняет известные ответы листьев. + +## Краткое резюме + +- Bagging снижает variance усреднением bootstrap-моделей. +- Random Forest декоррелирует деревья случайным выбором признаков. +- OOB даёт дешёвое честное предсказание каждого train-объекта при корректном IID. +- Корреляция деревьев, структура данных и способ вычисления важности критичны. + +## Связанные материалы + +- Практика: [`../assignments/02_ensembles.md`](../assignments/02_ensembles.md). + +- Предыдущая глава: [`01_split_criteria_and_pruning.md`](01_split_criteria_and_pruning.md). +- Следующая глава: [`03_gradient_boosting.md`](03_gradient_boosting.md). +- Термины: [`../glossary.md`](../glossary.md). diff --git a/modules/07_trees_and_boosting/notes/03_gradient_boosting.md b/modules/07_trees_and_boosting/notes/03_gradient_boosting.md new file mode 100644 index 0000000..0692a44 --- /dev/null +++ b/modules/07_trees_and_boosting/notes/03_gradient_boosting.md @@ -0,0 +1,195 @@ +# Градиентный бустинг + +> Бустинг строит аддитивную модель последовательно: каждое небольшое дерево +> исправляет направление ошибки текущего ансамбля. + +## От остатков к функциональному градиенту + +Ищем функцию +\(F:\mathbb R^d\to\mathbb R\), минимизирующую + +\[ +\hat R(F)=\sum_{i=1}^n\ell(y^{(i)},F(\mathbf x^{(i)})). +\] + +Модель строится по этапам: + +\[ +F_m(\mathbf x)=F_{m-1}(\mathbf x)+\eta\gamma_m h_m(\mathbf x), +\] + +где \(h_m\) — небольшое дерево, \(\eta\in(0,1]\) — learning rate. Псевдоостатки +есть отрицательный градиент loss по текущему score: + +\[ +r_i^{(m)}= +-\left.\frac{\partial\ell(y^{(i)},F(\mathbf x^{(i)}))} +{\partial F(\mathbf x^{(i)})}\right|_{F=F_{m-1}}. +\] + +Дерево \(h_m\) аппроксимирует пары +\((\mathbf x^{(i)},r_i^{(m)})\), а \(\gamma_m\) выбирается одномерным поиском +минимума. Это градиентный спуск в пространстве функций, но направление ограничено +семейством деревьев. + +## Квадратичная loss + +Для \(\ell(y,F)=\tfrac12(y-F)^2\): + +\[ +\frac{\partial\ell}{\partial F}=F-y,\qquad r=y-F. +\] + +Значит, дерево действительно учит обычные остатки. Начальная константа +\(F_0=\bar y\). Если \(y=[2,4,9]\), то \(F_0=5\), остатки +\([-3,-1,4]\). Пусть stump аппроксимировал их как \([-2,-2,4]\), а +\(\eta=0.1\). Новые прогнозы \([4.8,4.8,5.4]\), новые остатки +\([-2.8,-0.8,3.6]\): исправление намеренно небольшое. + +## Бинарная классификация + +Score \(F\) обычно является logit, \(p=\sigma(F)\). Для log loss + +\[ +\ell=-y\log p-(1-y)\log(1-p),\qquad +\frac{\partial\ell}{\partial F}=p-y, +\] + +поэтому псевдоостаток \(r=y-p\). Ансамбль обновляет logits, а не метки. +Начальная константа равна log-odds train-доли: +\(F_0=\log[\bar y/(1-\bar y)]\). Вероятности получают sigmoid после суммирования +всех деревьев. + +## Объяснимость: глобальная и локальная + +**Глобальная объяснимость** описывает поведение модели в среднем по набору данных: +какие признаки важны, как меняется прогноз и где модель ошибается. **Локальная** +объясняет один прогноз: какие значения признаков сдвинули его относительно +базового уровня. Локальные объяснения не обязаны складываться в простую глобальную +историю. + +**Permutation importance** измеряет падение выбранной validation-метрики после +случайной перестановки одного столбца. Это model-agnostic проверка того, насколько +модель зависит от доступной информации признака. Считать её нужно на данных, +не использованных для fit, повторять перестановки и показывать разброс. + +При коррелированных признаках перестановка одного из них может почти не ухудшить +метрику: его заменяет коррелированный дубль. Обратная проблема — перестановка +создаёт нереалистичные сочетания признаков. Поэтому полезны grouped/conditional +permutations и анализ устойчивости группы, а не рейтинг «истинных причин». + +**SHAP** опирается на значения Шепли из кооперативной теории игр. Признаки — +«игроки», прогноз — «выигрыш»; вклад признака усредняет его предельное изменение +по всем порядкам добавления признаков: + +\[ +f(\mathbf x)=\phi_0+\sum_j\phi_j. +\] + +\(\phi_0\) — базовый прогноз, \(\phi_j\) — локальный вклад. Для деревьев TreeSHAP +считает эти вклады эффективно, но ответ зависит от определения «отсутствующего» +признака и фоновой выборки. При корреляциях разные допущения о совместном или +условном распределении делят вклад между признаками по-разному. + +Ни permutation importance, ни SHAP не доказывают причинность. Они объясняют +зависимости обученной модели на выбранном распределении данных. Confounding, +leakage и смещение выборки могут сделать убедительное объяснение неверным как +утверждение о вмешательстве в реальный мир. + +## Сложность и регуляризация + +Бустинг особенно чувствителен к совместному выбору: + +- `learning_rate` и `n_estimators`: меньший шаг обычно требует больше деревьев, + даёт тоньше регуляризацию, но стоит дороже; +- глубина/число листьев: глубина \(d\) позволяет взаимодействия порядка до \(d\); + tabular-задачам часто достаточно неглубоких деревьев; +- `min_samples_leaf` сглаживает локальные поправки; +- `subsample<1` создаёт stochastic gradient boosting, снижая корреляцию и + ускоряя fit; +- \(L_1/L_2\)-штрафы и ограничение leaf values в современных библиотеках; +- early stopping выбирает число итераций по отдельной validation. + +Train loss обычно убывает с итерациями, validation после минимума может расти. +Early stopping нельзя контролировать по test. После выбора числа итераций модель +можно переобучить на train+validation с фиксированным бюджетом, оставив test +нетронутым. + +## Современные реализации + +Классический `GradientBoosting*` строит деревья последовательно по объектам. +Histogram-based методы предварительно бинят значения: поиск split становится +дешевле и экономнее по памяти. + +- **XGBoost** использует первые и вторые производные loss, регуляризацию листьев, + subsampling и обработку пропусков. +- **LightGBM** эффективен на больших данных, часто растит дерево leaf-wise; без + ограничений это повышает риск переобучения. +- **CatBoost** особенно удобен для категориальных признаков; ordered target + statistics уменьшают target leakage и prediction shift. +- `HistGradientBoosting*` в sklearn — сильный встроенный histogram baseline. + +Категориальные признаки нельзя наивно кодировать target mean по всей выборке: +метка строки попадёт в её же признак. Нужны fold/ordered encoding или нативная +обработка. «Поддержка NaN» библиотек не отменяет анализ смысла пропусков. + +## Bagging и boosting: различие + +Bagging обучает сильные нестабильные модели независимо и снижает variance. +Boosting обучает слабые модели последовательно и уменьшает bias, одновременно +регуляризуя шаг. Bagging легко параллелить; boosting имеет последовательную +зависимость. Random Forest обычно менее чувствителен к настройкам, boosting часто +точнее на tabular-данных, но легче переобучить validation-процессом. + +## Самостоятельный пример sklearn + +```python +from sklearn.datasets import load_breast_cancer +from sklearn.ensemble import HistGradientBoostingClassifier +from sklearn.metrics import log_loss, roc_auc_score +from sklearn.model_selection import train_test_split + +X, y = load_breast_cancer(return_X_y=True) +Xtr, Xte, ytr, yte = train_test_split( + X, y, test_size=0.25, stratify=y, random_state=42 +) +model = HistGradientBoostingClassifier( + learning_rate=0.05, max_iter=300, max_leaf_nodes=15, + min_samples_leaf=15, early_stopping=True, random_state=42 +) +model.fit(Xtr, ytr) +p = model.predict_proba(Xte)[:, 1] +print("iterations:", model.n_iter_) +print("ROC-AUC:", roc_auc_score(yte, p)) +print("log loss:", log_loss(yte, p)) +``` + +ROC-AUC оценивает ранжирование, log loss — качество вероятностей; превосходство +по одной метрике не гарантирует превосходства по другой. + +## Типичные ошибки и ограничения + +- Увеличивать и глубину, и число деревьев без уменьшения шага. +- Использовать test для early stopping или выбора реализации. +- Считать `feature_importances_` причинным объяснением. +- Не фиксировать seed, split, thread count и версию библиотеки при сравнении. +- Игнорировать calibration: отличный ranking не гарантирует вероятностей. +- Экстраполировать за train-диапазон: древесные поправки остаются ступенчатыми. +- Сравнивать библиотеки при разном tuning budget. + +## Краткое резюме + +- Псевдоостаток — отрицательный градиент loss по текущему score. +- Мелкие деревья последовательно образуют гибкую аддитивную модель. +- Learning rate, число/размер деревьев и early stopping регулируют сложность. +- Современные реализации ускоряют split и безопаснее работают с категориями, + но не отменяют честную валидацию. + +## Связанные материалы + +- Практика: [`../assignments/02_ensembles.md`](../assignments/02_ensembles.md). +- Практика: [`../assignments/03_explainability.md`](../assignments/03_explainability.md). + +- Предыдущая глава: [`02_bagging_random_forest_oob.md`](02_bagging_random_forest_oob.md). +- Метрики: [`../../06_classical_ml/notes/03_metrics_thresholds_and_calibration.md`](../../06_classical_ml/notes/03_metrics_thresholds_and_calibration.md). +- Термины: [`../glossary.md`](../glossary.md). diff --git a/modules/08_unsupervised_learning/README.md b/modules/08_unsupervised_learning/README.md new file mode 100644 index 0000000..da93443 --- /dev/null +++ b/modules/08_unsupervised_learning/README.md @@ -0,0 +1,37 @@ +# Модуль 08. Обучение без учителя + +Плановый срок: **недели 17**. + +## Цели + +- понимать кластеризацию и её ограничения +- применять PCA, t-SNE и UMAP без неверных выводов +- работать с аномалиями и неразмеченными данными + +## Предварительные знания + +Нужны расстояния, масштабирование признаков, собственные векторы/SVD и навыки честной оценки моделей. + +## Теория + +1. [Классификация и кластеризация](notes/01_clustering_vs_classification.md) +2. [k-means, иерархическая кластеризация и DBSCAN](notes/02_clustering_algorithms.md) +3. [PCA, t-SNE, UMAP и визуализация размерности](notes/03_dimensionality_reduction.md) +4. [Поиск аномалий без надёжной разметки](notes/04_anomaly_detection.md) + +Термины модуля: [`glossary.md`](glossary.md). + +Для каждого урока: прочитайте теорию, воспроизведите ключевой пример своими +руками и запишите три тезиса без подсказки. Формулы из этого модуля нужно +уметь связать с кодом и наблюдаемым поведением модели. + +## Задания + +- [`assignments/01_clustering.md`](assignments/01_clustering.md) — Кластеризация разных геометрий +- [`assignments/02_embeddings.md`](assignments/02_embeddings.md) — Карта представлений +- [`assignments/03_anomaly.md`](assignments/03_anomaly.md) — Детектор аномалий + +## Завершение модуля + +Модуль завершён, когда все задания удовлетворяют критериям приёмки, +вы можете объяснить основные решения без ноутбука и получили review. diff --git a/modules/08_unsupervised_learning/assignments/01_clustering.md b/modules/08_unsupervised_learning/assignments/01_clustering.md new file mode 100644 index 0000000..9cc92d4 --- /dev/null +++ b/modules/08_unsupervised_learning/assignments/01_clustering.md @@ -0,0 +1,42 @@ +# Кластеризация разных геометрий + +## Теория перед выполнением + +Перед началом прочитайте: + +- [`01_clustering_vs_classification.md`](../notes/01_clustering_vs_classification.md) +- [`02_clustering_algorithms.md`](../notes/02_clustering_algorithms.md) + + +## Цель + +Сравнить k-means, DBSCAN и hierarchical clustering на наборах разной формы. + +## Что сделать + +1. Сформулировать ожидаемый результат до запуска эксперимента. +2. Реализовать простой baseline. +3. Реализовать основной метод самостоятельно настолько, насколько требует задание. +4. Добавить автоматические проверки ключевых вычислений. +5. Провести эксперимент, сохранить метрики и примеры ошибок. +6. Заполнить отчёт по `common/REPORT_TEMPLATE.md`. + +## Результат + +Графики кластеров и анализ failure modes. + +Решение разместите рядом в `solution/`. Не коммитьте большие данные, +веса или секреты. + +## Критерии приёмки + +- Параметры выбраны осмысленно; номера кластеров не трактуются как классы. +- Код читаем, разбит на функции и запускается по инструкции. +- Выводы подтверждаются измерениями и не выходят за пределы эксперимента. + +## Вопросы для самопроверки + +- Почему выбран именно этот метод и какой baseline он должен превзойти? +- Где здесь возможна утечка данных или некорректная оценка? +- Как изменится результат при других данных или ограничениях? +- Какую ошибку модели вы считаете наиболее важной и почему? diff --git a/modules/08_unsupervised_learning/assignments/02_embeddings.md b/modules/08_unsupervised_learning/assignments/02_embeddings.md new file mode 100644 index 0000000..6ebd686 --- /dev/null +++ b/modules/08_unsupervised_learning/assignments/02_embeddings.md @@ -0,0 +1,41 @@ +# Карта представлений + +## Теория перед выполнением + +Перед началом прочитайте: + +- [`03_dimensionality_reduction.md`](../notes/03_dimensionality_reduction.md) + + +## Цель + +Построить PCA/t-SNE/UMAP для изображений или эмбеддингов. + +## Что сделать + +1. Сформулировать ожидаемый результат до запуска эксперимента. +2. Реализовать простой baseline. +3. Реализовать основной метод самостоятельно настолько, насколько требует задание. +4. Добавить автоматические проверки ключевых вычислений. +5. Провести эксперимент, сохранить метрики и примеры ошибок. +6. Заполнить отчёт по `common/REPORT_TEMPLATE.md`. + +## Результат + +Сравнение методов при нескольких seed и параметрах. + +Решение разместите рядом в `solution/`. Не коммитьте большие данные, +веса или секреты. + +## Критерии приёмки + +- Локальные красивые группы не объявлены доказанными кластерами без проверки. +- Код читаем, разбит на функции и запускается по инструкции. +- Выводы подтверждаются измерениями и не выходят за пределы эксперимента. + +## Вопросы для самопроверки + +- Почему выбран именно этот метод и какой baseline он должен превзойти? +- Где здесь возможна утечка данных или некорректная оценка? +- Как изменится результат при других данных или ограничениях? +- Какую ошибку модели вы считаете наиболее важной и почему? diff --git a/modules/08_unsupervised_learning/assignments/03_anomaly.md b/modules/08_unsupervised_learning/assignments/03_anomaly.md new file mode 100644 index 0000000..9380159 --- /dev/null +++ b/modules/08_unsupervised_learning/assignments/03_anomaly.md @@ -0,0 +1,41 @@ +# Детектор аномалий + +## Теория перед выполнением + +Перед началом прочитайте: + +- [`04_anomaly_detection.md`](../notes/04_anomaly_detection.md) + + +## Цель + +Сравнить Isolation Forest и простой statistical baseline. + +## Что сделать + +1. Сформулировать ожидаемый результат до запуска эксперимента. +2. Реализовать простой baseline. +3. Реализовать основной метод самостоятельно настолько, насколько требует задание. +4. Добавить автоматические проверки ключевых вычислений. +5. Провести эксперимент, сохранить метрики и примеры ошибок. +6. Заполнить отчёт по `common/REPORT_TEMPLATE.md`. + +## Результат + +Precision@k, примеры ошибок и рекомендации по порогу. + +Решение разместите рядом в `solution/`. Не коммитьте большие данные, +веса или секреты. + +## Критерии приёмки + +- Порог связан с доступным бюджетом проверки аномалий. +- Код читаем, разбит на функции и запускается по инструкции. +- Выводы подтверждаются измерениями и не выходят за пределы эксперимента. + +## Вопросы для самопроверки + +- Почему выбран именно этот метод и какой baseline он должен превзойти? +- Где здесь возможна утечка данных или некорректная оценка? +- Как изменится результат при других данных или ограничениях? +- Какую ошибку модели вы считаете наиболее важной и почему? diff --git a/modules/08_unsupervised_learning/glossary.md b/modules/08_unsupervised_learning/glossary.md new file mode 100644 index 0000000..1227f59 --- /dev/null +++ b/modules/08_unsupervised_learning/glossary.md @@ -0,0 +1,35 @@ +# Глоссарий модуля 08 + +- **Обучение без учителя** — поиск структуры без целевой метки для каждого объекта. +- **Кластер** — группа, определённая признаками, метрикой и objective алгоритма. +- **Жёсткая/мягкая кластеризация** — одна метка / степени принадлежности группам. +- **Silhouette** — сравнение внутрикластерного и ближайшего межкластерного + расстояний. +- **ARI** — скорректированная на случайность мера совпадения двух разбиений. +- **Inertia** — сумма квадратов расстояний до центров k-means. +- **Центроид** — среднее объектов кластера в k-means. +- **Linkage** — правило расстояния между группами в иерархической кластеризации. +- **Дендрограмма** — дерево последовательных объединений кластеров. +- **DBSCAN** — кластеризация по плотностной достижимости. +- **Core / border / noise** — плотная точка / её сосед / не включённая точка. +- **PCA** — линейная проекция максимальной дисперсии и минимальной rank-ошибки. +- **Главная компонента** — ортонормированное направление PCA. +- **Explained variance ratio** — доля суммарной дисперсии компоненты. +- **Whitening** — масштабирование компонент до единичной дисперсии. +- **t-SNE** — embedding, минимизирующий KL между локальными соседствами. +- **Perplexity** — эффективный масштаб соседства t-SNE. +- **UMAP** — embedding на основе графа локальных соседей. +- **Outlier detection** — поиск необычных объектов внутри загрязнённого набора. +- **Novelty detection** — проверка новых объектов относительно нормального train. +- **Anomaly score** — упорядочивающая мера необычности, не обязательно вероятность. +- **Contamination** — предполагаемая доля выбросов для установки порога. +- **Isolation Forest** — детектор по средней длине случайной изоляции. +- **LOF** — отношение локальной плотности соседей к плотности объекта. +- **One-Class SVM** — граница основной массы в kernel-пространстве. + +## Главы + +1. [`notes/01_clustering_vs_classification.md`](notes/01_clustering_vs_classification.md) +2. [`notes/02_clustering_algorithms.md`](notes/02_clustering_algorithms.md) +3. [`notes/03_dimensionality_reduction.md`](notes/03_dimensionality_reduction.md) +4. [`notes/04_anomaly_detection.md`](notes/04_anomaly_detection.md) diff --git a/modules/08_unsupervised_learning/notes/01_clustering_vs_classification.md b/modules/08_unsupervised_learning/notes/01_clustering_vs_classification.md new file mode 100644 index 0000000..a0c5a26 --- /dev/null +++ b/modules/08_unsupervised_learning/notes/01_clustering_vs_classification.md @@ -0,0 +1,158 @@ +# Классификация и кластеризация + +> Разметка меняет не только алгоритм, но и сам смысл результата: класс задаётся +> целевой переменной, кластер — выбранной геометрией и критерием. + +## На словах + +При **классификации** заранее известны названия классов и есть примеры правильных +ответов. Мы показываем модели фотографии с метками «кошка» и «собака», после чего +просим назначить метку новой фотографии. + +При **кластеризации** правильных меток нет. Алгоритм пытается сам разделить объекты +на группы по выбранному понятию сходства. Он может объединить животных по цвету +фона, размеру изображения или породе, а не по ожидаемому человеком виду. + +Поэтому кластер — не автоматически «обнаруженный истинный класс». Это структура, +получившаяся из признаков, метрики расстояния и настроек алгоритма. + +## Формальная постановка + +В классификации дана +\(\mathcal D=\{(\mathbf x^{(i)},y^{(i)})\}_{i=1}^n\), где +\(\mathbf x^{(i)}\in\mathbb R^d\), а \(y^{(i)}\in\{1,\ldots,K\}\). +Обучается \(f(\mathbf x)\), минимизирующая ожидаемую ошибку меток на новых данных. +Класс имеет внешний смысл, определённый инструкцией разметчика. + +В кластеризации дана только +\(\mathbf X\in\mathbb R^{n\times d}\). Результат может быть: + +- жёсткая метка \(z_i\in\{1,\ldots,K\}\); +- мягкие степени принадлежности; +- иерархия вложенных групп; +- выделение плотных областей и шума. + +Алгоритм ищет структуру согласно своей цели: + +- k-means минимизирует сумму квадратов расстояний до центров; +- hierarchical clustering последовательно объединяет близкие группы; +- DBSCAN ищет плотные области и может выделять шум. + +Разные цели дают разные «правильные» разбиения одного датасета. Номера кластеров +произвольны: перестановка \(1,2,3\) не меняет решения. + +## Признаки определяют вопрос + +Расстояние не существует «само по себе». Евклидово расстояние + +\[ +d(\mathbf x,\mathbf z)=\sqrt{\sum_{j=1}^d(x_j-z_j)^2} +\] + +зависит от единиц. Разность дохода в рублях способна полностью заглушить возраст +в годах; стандартирование меняет найденные группы. One-hot-категории, тексты, +геокоординаты и временные ряды требуют подходящей метрики или представления. + +Даже корректная метрика отвечает только выбранному вопросу. Кластеры клиентов по +покупкам, риску и демографии могут быть разными и одновременно полезными. +Служебный ID, данные после анализируемого события и защищённые признаки могут +создать бессмысленные или этически опасные сегменты. + +## Как оценивать + +Для классификации подходят accuracy, precision, recall, F1, ROC-AUC/PR-AUC и +оценка вероятностей на validation/test с известными метками. + +Для кластеризации без эталонных меток используют внутренние показатели вроде +silhouette score, устойчивость к seed/подвыборкам и полезность групп в дальнейшей +задаче. Если внешние метки всё же существуют, ARI или NMI можно использовать для +анализа, но не превращать их незаметно в обучающий сигнал. + +Для объекта \(i\) silhouette: + +\[ +s_i=\frac{b_i-a_i}{\max(a_i,b_i)}, +\] + +где \(a_i\) — среднее расстояние до собственного кластера, \(b_i\) — минимальное +среднее расстояние до другого. Значение близко к 1 для компактного отделённого +объекта, около 0 на границе, отрицательно при вероятно неверном назначении. +Метрика предпочитает выпуклые отделённые группы и может отвергать содержательные +кольца или кластеры разной плотности. + +Внешние меры сравнивают пары объектов. Adjusted Rand Index корректирует случайное +совпадение и инвариантен к номерам кластеров. Но если метки использованы для +выбора \(K\), признаков и алгоритма, задача стала частично supervised. + +## Устойчивость и валидация без разметки + +У хорошего исследования есть протокол: + +1. Зафиксировать единицу наблюдения, признаки, preprocessing и цель применения. +2. Сравнить с baseline: один кластер, случайное разбиение, простая сегментация. +3. Повторить обучение при разных seed и bootstrap/подвыборках. +4. Сопоставить кластеры между запусками мерой, не зависящей от номеров (ARI). +5. Проверить профили на holdout и устойчивость во времени. +6. Оценить downstream-пользу: улучшает ли сегмент решение, не причиняя вреда. + +Стабильность необходима не всегда: данные могут иметь непрерывный спектр без +естественных границ. Тогда честный вывод — «устойчивых кластеров нет», а не +принудительное именование сегментов. + +## Разобранный пример + +Есть данные о слушателях: возраст, жанры, время прослушивания и пропуски треков. + +- Классификация: предсказать известную метку «отменит подписку». +- Кластеризация: найти группы похожего поведения для исследования аудитории. + +Во втором случае бизнес ещё должен интерпретировать группы и проверить, устойчивы +ли они. Алгоритм не знает слов «меломан» или «случайный слушатель». + +Пусть объект имеет \(a=2\), а средние расстояния до двух чужих кластеров 5 и 7. +Тогда \(b=5\), \(s=(5-2)/5=0.6\). Это свидетельствует о геометрическом отделении, +но не доказывает бизнес-смысл и не сообщает правильное название группы. + +## Самостоятельный пример sklearn + +```python +from sklearn.cluster import KMeans +from sklearn.datasets import make_blobs +from sklearn.metrics import adjusted_rand_score, silhouette_score +from sklearn.preprocessing import StandardScaler + +X, _ = make_blobs(n_samples=500, centers=3, random_state=42) +X = StandardScaler().fit_transform(X) +labels_1 = KMeans(n_clusters=3, n_init=20, random_state=1).fit_predict(X) +labels_2 = KMeans(n_clusters=3, n_init=20, random_state=2).fit_predict(X) +print("silhouette:", silhouette_score(X, labels_1)) +print("stability ARI:", adjusted_rand_score(labels_1, labels_2)) +``` + +В реальном исследовании scaler обучают на train, а устойчивость проверяют не +только сменой seed, но и возмущением выборки. + +## Типичные ошибки и ограничения + +- Называть кластер классом или типом личности без внешней проверки. +- Масштабировать на глаз или оставлять ID среди признаков. +- Выбирать \(K\) только по максимальному silhouette. +- Интерпретировать двумерную визуализацию t-SNE как доказательство кластеров. +- Оценивать только на тех же данных и скрывать нестабильные запуски. +- Считать отсутствие чётких групп неудачей алгоритма: структура может быть + непрерывной. + +## Краткое резюме + +- Классификация предсказывает заданную метку; кластеризация конструирует группы. +- Кластер определяется признаками, метрикой, objective и настройками. +- Внутренняя метрика не доказывает внешний смысл. +- Устойчивость, holdout-профили и downstream-польза важнее красивой картинки. + +## Связанные материалы + +- Практика: [`../assignments/01_clustering.md`](../assignments/01_clustering.md). + +- Следующая глава: [`02_clustering_algorithms.md`](02_clustering_algorithms.md). +- Метрики классификации: [`../../06_classical_ml/notes/03_metrics_thresholds_and_calibration.md`](../../06_classical_ml/notes/03_metrics_thresholds_and_calibration.md). +- Термины: [`../glossary.md`](../glossary.md). diff --git a/modules/08_unsupervised_learning/notes/02_clustering_algorithms.md b/modules/08_unsupervised_learning/notes/02_clustering_algorithms.md new file mode 100644 index 0000000..49d3999 --- /dev/null +++ b/modules/08_unsupervised_learning/notes/02_clustering_algorithms.md @@ -0,0 +1,150 @@ +# k-means, иерархическая кластеризация и DBSCAN + +> Алгоритмы кластеризации кодируют разные определения группы: близость к центру, +> вложенное объединение или связность плотной области. + +## k-means + +Для заданного \(K\) ищутся назначения \(z_i\) и центры +\(\boldsymbol\mu_k\in\mathbb R^d\): + +\[ +J=\sum_{i=1}^n\|\mathbf x^{(i)}-\boldsymbol\mu_{z_i}\|_2^2 +\longrightarrow\min. +\] + +Алгоритм Ллойда чередует два шага: + +\[ +z_i\leftarrow\arg\min_k\|\mathbf x^{(i)}-\boldsymbol\mu_k\|^2,\qquad +\boldsymbol\mu_k\leftarrow\frac1{|C_k|}\sum_{i:z_i=k}\mathbf x^{(i)}. +\] + +Первый шаг минимизирует objective при фиксированных центрах. Второй использует +тот факт, что среднее минимизирует сумму квадратов. Поэтому \(J\) не возрастает, +но задача невыпукла и результат — локальный минимум. `k-means++` разносит +начальные центры, а `n_init` повторяет поиск. + +Предполагаются примерно сферические, сопоставимые по размеру и плотности группы +в евклидовой геометрии. Выброс с большим расстоянием сильно двигает среднее. +Масштабирование и выбор признаков являются частью модели. + +Выбор \(K\): elbow в inertia, silhouette, устойчивость и прикладная полезность. +Inertia всегда убывает при росте \(K\), поэтому её минимум бессмысленен. MiniBatch +k-means обрабатывает малые случайные batch и масштабируется лучше ценой +приближённости. + +## Иерархическая кластеризация + +Agglomerative алгоритм начинает с \(n\) однообъектных кластеров и объединяет +ближайшую пару. Дендрограмма хранит всю последовательность; число кластеров +выбирается высотой разреза. Различие задаёт **linkage**: + +\[ +d_{\rm single}(A,B)=\min_{a\in A,b\in B}d(a,b),\quad +d_{\rm complete}(A,B)=\max_{a\in A,b\in B}d(a,b), +\] + +\[ +d_{\rm average}(A,B)=\frac1{|A||B|}\sum_{a\in A,b\in B}d(a,b). +\] + +Single linkage находит вытянутые формы, но страдает chaining: мост из редких +точек соединяет группы. Complete создаёт компактные кластеры и чувствителен к +выбросам. Average — компромисс. Ward объединяет пару с минимальным ростом суммы +квадратов и по смыслу близок k-means; он требует евклидовой геометрии. + +Иерархия удобна для исследования нескольких масштабов и не требует заранее +фиксировать \(K\), но всё равно требует правила разреза. На больших \(n\) +матрица попарных расстояний может потребовать \(O(n^2)\) памяти. + +## DBSCAN + +Задаются радиус \(\varepsilon\) и `min_samples` \(m\). Окрестность: + +\[ +N_\varepsilon(\mathbf x)=\{\mathbf z:d(\mathbf x,\mathbf z)\le\varepsilon\}. +\] + +- **core point:** \(|N_\varepsilon(\mathbf x)|\ge m\); +- **border point:** не core, но находится в окрестности core; +- **noise:** не достижима по цепочке core-точек. + +DBSCAN строит компоненты density-reachability. Число кластеров получается +автоматически, форма может быть невыпуклой, выбросы получают метку \(-1\). +Но один глобальный \(\varepsilon\) плохо работает при разной плотности. В высокой +размерности расстояния теряют контраст. k-distance plot (расстояние до +\(m\)-го соседа) помогает искать изгиб, но не даёт объективно единственного +параметра. OPTICS/HDBSCAN лучше исследуют несколько плотностей. + +## Разобранный пример k-means + +Точки на прямой: \(0,2,8,10\), \(K=2\), начальные центры \(0,10\). +Назначение даёт \(C_1=\{0,2\}\), \(C_2=\{8,10\}\). Новые центры \(1,9\). +Objective: + +\[ +(0-1)^2+(2-1)^2+(8-9)^2+(10-9)^2=4. +\] + +Следующее назначение не меняется, достигнута неподвижная точка. Оно локально +оптимально, но сам алгоритм не доказывает наличие «двух истинных типов». + +## Самостоятельный пример sklearn + +```python +from sklearn.cluster import AgglomerativeClustering, DBSCAN, KMeans +from sklearn.datasets import make_moons +from sklearn.metrics import silhouette_score +from sklearn.preprocessing import StandardScaler + +X, _ = make_moons(n_samples=600, noise=0.06, random_state=42) +X = StandardScaler().fit_transform(X) +models = { + "kmeans": KMeans(n_clusters=2, n_init=20, random_state=42), + "ward": AgglomerativeClustering(n_clusters=2, linkage="ward"), + "dbscan": DBSCAN(eps=0.22, min_samples=8), +} +for name, model in models.items(): + labels = model.fit_predict(X) + mask = labels != -1 + k = len(set(labels[mask])) + score = silhouette_score(X[mask], labels[mask]) if k > 1 else float("nan") + print(name, "clusters:", k, "noise:", (~mask).sum(), "silhouette:", score) +``` + +Исключение noise из silhouette меняет вопрос и может завышать результат; всегда +сообщайте долю исключённых точек. + +## Выбор метода + +- Компактные массовые кластеры, нужен быстрый baseline: k-means. +- Нужна дендрограмма/несколько масштабов или специальная метрика: hierarchical. +- Невыпуклые плотные области и шум: DBSCAN. +- Разная плотность, высокая размерность или смешанные типы: рассмотрите другое + представление, HDBSCAN/модели смесей и обязательно проверяйте устойчивость. + +## Типичные ошибки и ограничения + +- Подбирать scaler, PCA и параметры на всех данных, а затем называть оценку + holdout — preprocessing тоже должен иметь протокол. +- Считать elbow доказательством \(K\). +- Использовать Ward с неевклидовым расстоянием. +- Считать каждую DBSCAN-метку \(-1\) истинной аномалией. +- Сравнивать silhouette при разной доле выброшенного noise без оговорки. +- Давать кластерам смысловые имена по одному среднему признаку. + +## Краткое резюме + +- k-means минимизирует квадраты до центров и любит сферические группы. +- Linkage задаёт геометрию иерархических объединений. +- DBSCAN определяет кластер через плотностную достижимость и выделяет шум. +- Параметры, масштаб и признаки являются частью определения результата. + +## Связанные материалы + +- Практика: [`../assignments/01_clustering.md`](../assignments/01_clustering.md). + +- Предыдущая глава: [`01_clustering_vs_classification.md`](01_clustering_vs_classification.md). +- Следующая глава: [`03_dimensionality_reduction.md`](03_dimensionality_reduction.md). +- Термины: [`../glossary.md`](../glossary.md). diff --git a/modules/08_unsupervised_learning/notes/03_dimensionality_reduction.md b/modules/08_unsupervised_learning/notes/03_dimensionality_reduction.md new file mode 100644 index 0000000..f3a29ca --- /dev/null +++ b/modules/08_unsupervised_learning/notes/03_dimensionality_reduction.md @@ -0,0 +1,190 @@ +# PCA, t-SNE, UMAP и визуализация размерности + +> Снижение размерности строит новое представление. PCA сохраняет линейную +> дисперсию, t-SNE и UMAP — главным образом локальные соседства. + +## Зачем снижать размерность + +Цели различаются: сжатие, подавление шума, ускорение модели, борьба с +коллинеарностью или двумерная визуализация. Метод, хороший для картинки, не +обязательно даёт признаки для prediction. Любая проекция теряет информацию; +оценивать следует именно целевое применение. + +## PCA: постановка + +Пусть центрированная +\(\mathbf X_c=\mathbf X-\mathbf1\bar{\mathbf x}^{\top}\in\mathbb R^{n\times d}\). +Первая главная компонента — единичный вектор максимальной дисперсии: + +\[ +\mathbf v_1\in\arg\max_{\|\mathbf v\|=1} +\frac1n\|\mathbf X_c\mathbf v\|_2^2 +=\arg\max_{\|\mathbf v\|=1}\mathbf v^\top +\left(\frac1n\mathbf X_c^\top\mathbf X_c\right)\mathbf v. +\] + +С множителем Лагранжа: + +\[ +\mathcal L=\mathbf v^\top\boldsymbol\Sigma\mathbf v +-\lambda(\mathbf v^\top\mathbf v-1),\quad +\nabla_{\mathbf v}\mathcal L=2\boldsymbol\Sigma\mathbf v-2\lambda\mathbf v=0. +\] + +Следовательно, \(\boldsymbol\Sigma\mathbf v=\lambda\mathbf v\): компоненты — +собственные векторы ковариации, дисперсии — собственные значения. Последующие +компоненты ортогональны предыдущим. + +Через SVD +\(\mathbf X_c=\mathbf U\mathbf S\mathbf V^\top\): строки +\(\mathbf V^\top\) задают направления, scores +\(\mathbf Z=\mathbf X_c\mathbf V_q=\mathbf U_q\mathbf S_q\). +SVD устойчивее явного построения ковариации. + +## Ковариация PCA: деление на \(n\) или \(n-1\) + +Для центрированной матрицы используют обе договорённости: + +\[ +\boldsymbol\Sigma_n=\frac1n\mathbf X_c^\top\mathbf X_c,\qquad +\boldsymbol\Sigma_{n-1}=\frac1{n-1}\mathbf X_c^\top\mathbf X_c. +\] + +Первая — эмпирическая covariance как среднее по наблюдаемому распределению; +вторая — несмещённая оценка covariance генеральной совокупности при iid-выборке. +Матрицы отличаются только положительным скаляром \(n/(n-1)\), поэтому их +собственные векторы, порядок компонент, scores и explained variance **ratio** +совпадают. Отличаются собственные значения и абсолютные `explained_variance_`. +Например, scikit-learn сообщает выборочные дисперсии с делением на \(n-1\), тогда +как в выводе выше использовано \(1/n\). При сравнении ручной реализации с +библиотекой нужно согласовать `ddof`; это не разные направления PCA. + +## PCA как лучшая линейная реконструкция + +Реконструкция из \(q\) компонент: + +\[ +\hat{\mathbf X}=\mathbf Z\mathbf V_q^\top+ +\mathbf1\bar{\mathbf x}^{\top}. +\] + +Теорема Эккарта—Янга даёт + +\[ +\|\mathbf X_c-\mathbf U_q\mathbf S_q\mathbf V_q^\top\|_F^2 +=\sum_{j>q}s_j^2, +\] + +минимум среди rank-\(q\) приближений. Explained variance ratio компоненты +\(\lambda_j/\sum_k\lambda_k\). Высокая сохранённая дисперсия не гарантирует +сохранение слабого, но предсказательного сигнала. + +PCA требует центрирования. Стандартизация нужна, если единицы несопоставимы и +variance не должна отражать физический масштаб. Whitening дополнительно делит +scores на стандартные отклонения, уничтожая различия дисперсий и усиливая +малодисперсионный шум. + +## Числовой пример PCA + +Для центрированных точек \((-1,-1),(0,0),(1,1)\): + +\[ +\boldsymbol\Sigma=\frac13 +\begin{bmatrix}2&2\\2&2\end{bmatrix}. +\] + +Собственные направления: +\(\mathbf v_1=(1,1)^\top/\sqrt2\) с \(\lambda_1=4/3\) и +\(\mathbf v_2=(1,-1)^\top/\sqrt2\) с \(\lambda_2=0\). +Одна компонента восстанавливает данные точно: точки лежат на прямой. +Знак \(\mathbf v_1\) произволен; смена знака не меняет подпространство. + +## t-SNE + +t-SNE превращает расстояния в вероятности соседства. В исходном пространстве +условные \(p_{j\mid i}\) строятся Gaussian с локально подобранной шириной; +**perplexity** примерно задаёт эффективное число соседей. После симметризации +получают \(p_{ij}\). В 2D используют тяжёлые хвосты Student: + +\[ +q_{ij}\propto(1+\|\mathbf z_i-\mathbf z_j\|^2)^{-1}. +\] + +Минимизируется +\[ +\operatorname{KL}(P\|Q)=\sum_{i\ne j}p_{ij}\log\frac{p_{ij}}{q_{ij}}. +\] + +Асимметрия KL сильно штрафует потерю истинных соседей, но слабее — появление +далёких ложных отношений. Поэтому локальные группы видны хорошо, а расстояния +между «островами», их размеры и плотности обычно не интерпретируются. Результат +зависит от seed, perplexity, learning rate и числа итераций. t-SNE — прежде всего +исследовательская визуализация, не доказательство кластеров. + +## UMAP + +UMAP строит взвешенный граф \(k\) ближайших соседей с локальным масштабом и +оптимизирует низкоразмерный граф, притягивая рёбра и отталкивая sampled +не-соседей. `n_neighbors` задаёт локально-глобальный компромисс, `min_dist` — +насколько плотно точки могут упаковываться. + +UMAP обычно быстрее t-SNE, поддерживает `transform` новых объектов в реализациих +и нередко лучше сохраняет часть глобальной структуры, но не гарантирует +геометрически точных межкластерных расстояний. Seed и параметры способны менять +картинку. Теоретические manifold-предположения — локальная низкая размерность и +осмысленная метрика — должны быть хотя бы приблизительно правдоподобны. + +## Безопасный протокол + +1. Fit scaler/PCA/UMAP только на train, если embedding участвует в модели. +2. Для визуализации сначала удалить явный шум или применить PCA до 30–50 + компонент, затем t-SNE/UMAP. +3. Повторить seed и диапазон параметров. +4. Красить точки метками только после построения embedding; иначе не выдавать + supervised настройку за unsupervised. +5. Оценить reconstruction (PCA), trustworthiness соседств и downstream-метрику. + +## Самостоятельный пример sklearn + +```python +from sklearn.datasets import load_digits +from sklearn.decomposition import PCA +from sklearn.metrics import mean_squared_error +from sklearn.preprocessing import StandardScaler + +X, _ = load_digits(return_X_y=True) +X_scaled = StandardScaler().fit_transform(X) +pca = PCA(n_components=0.95, svd_solver="full") +Z = pca.fit_transform(X_scaled) +X_hat = pca.inverse_transform(Z) +print("shape:", X.shape, "->", Z.shape) +print("variance:", pca.explained_variance_ratio_.sum()) +print("reconstruction MSE:", mean_squared_error(X_scaled, X_hat)) +``` + +Для UMAP нужен отдельный пакет `umap-learn`; базовый учебный код намеренно не +делает его обязательной зависимостью. + +## Типичные ошибки и ограничения + +- Интерпретировать оси t-SNE/UMAP как исходные признаки. +- Считать расстояние между островами количественно значимым. +- Подбирать картинку до желаемого сюжета и скрывать другие seed. +- Выполнить PCA до split и получить утечку в downstream-оценке. +- Выбирать число PCA-компонент только по 95% variance, не проверяя целевую метрику. +- Применять евклидову метрику к неподходящим сырым данным. + +## Краткое резюме + +- PCA — оптимальная линейная rank-\(q\) реконструкция центрированных данных. +- t-SNE сохраняет вероятностные локальные соседства, не глобальную карту. +- UMAP оптимизирует представление графа соседей и также зависит от параметров. +- Визуализация — источник гипотез, а не статистическое доказательство кластеров. + +## Связанные материалы + +- Практика: [`../assignments/02_embeddings.md`](../assignments/02_embeddings.md). + +- Предыдущая глава: [`02_clustering_algorithms.md`](02_clustering_algorithms.md). +- Следующая глава: [`04_anomaly_detection.md`](04_anomaly_detection.md). +- Термины: [`../glossary.md`](../glossary.md). diff --git a/modules/08_unsupervised_learning/notes/04_anomaly_detection.md b/modules/08_unsupervised_learning/notes/04_anomaly_detection.md new file mode 100644 index 0000000..f78761f --- /dev/null +++ b/modules/08_unsupervised_learning/notes/04_anomaly_detection.md @@ -0,0 +1,184 @@ +# Поиск аномалий без надёжной разметки + +> Детектор выдаёт score необычности относительно выбранных данных и признаков. +> Аномалия не тождественна ошибке, мошенничеству или полезному инциденту. + +## Постановки задачи + +- **Outlier detection:** train уже может содержать аномалии; нужно описать его + основную массу и отметить редкие наблюдения. +- **Novelty detection:** train считается нормальным, новые объекты проверяются на + отклонение. Оценивать train тем же fitted-методом часто оптимистично. +- **Supervised rare-event classification:** есть надёжные метки; это уже + классификация с дисбалансом, и метки следует использовать. + +Аномалии бывают точечными, контекстными (температура необычна для сезона) и +коллективными (последовательность необычна, хотя отдельные точки нормальны). +Сначала задают единицу анализа, контекст и действие после alert. + +## Score, contamination и порог + +Пусть \(s(\mathbf x)\) растёт с аномальностью. Hard-решение: + +\[ +\hat y=\mathbb1\{s(\mathbf x)\ge t\}. +\] + +`contamination` обычно задаёт квантиль score и тем самым ожидаемую долю +срабатываний, а не обучает истинную вероятность аномалии. Если известен бюджет +\(B\) проверок в день, естественный baseline — top-\(B\) score. При наличии +редких меток порог выбирают по precision@B, recall при допустимом FPR или +ожидаемой стоимости, затем проверяют на более позднем периоде. + +## Статистические baselines + +Для одномерных примерно симметричных данных z-score: + +\[ +z_i=\frac{x_i-\bar x}{s}. +\] + +Среднее и стандартное отклонение сами чувствительны к выбросам. Устойчивый +вариант использует медиану и MAD: + +\[ +\operatorname{MAD}=\operatorname{median}_i|x_i-\operatorname{median}(x)|,\quad +z_i^{\rm robust}=0.6745\frac{x_i-\operatorname{median}(x)}{\operatorname{MAD}}. +\] + +Множитель согласует scale с нормальным распределением. Порог 3 — эвристика и +не учитывает множественные проверки, тяжёлые хвосты и сезонность. + +Gaussian-модель использует Mahalanobis distance + +\[ +D^2(\mathbf x)=(\mathbf x-\boldsymbol\mu)^\top +\boldsymbol\Sigma^{-1}(\mathbf x-\boldsymbol\mu). +\] + +Она учитывает корреляции, но ковариация нестабильна при \(d\) близком к \(n\) и +чувствительна к contamination; нужны shrinkage/robust covariance. + +## Isolation Forest + +Случайное дерево выбирает признак и случайный split между минимумом и максимумом. +Редкую изолированную точку обычно отделяют малым числом splits. Средняя длина +пути \(E[h(\mathbf x)]\) нормируется ожидаемой длиной неуспешного поиска: + +\[ +s(\mathbf x,n)=2^{-E[h(\mathbf x)]/c(n)},\quad +c(n)\approx2H_{n-1}-2(n-1)/n. +\] + +Score около 1 указывает на лёгкую изоляцию. Метод масштабируется, не требует +оценивать плотность и ловит многомерные эффекты. Но осевые случайные splits, +нерелевантные признаки и высокая размерность могут ухудшить результат. + +## Local Outlier Factor + +LOF сравнивает локальную плотность объекта с плотностью соседей. Reachability +distance сглаживает слишком малые расстояния: + +\[ +\operatorname{reach}_k(p,o)=\max\{d_k(o),d(p,o)\}. +\] + +Локальная reachability density — обратное среднее этих расстояний, а + +\[ +\operatorname{LOF}_k(p)= +\frac1{|N_k(p)|}\sum_{o\in N_k(p)} +\frac{\operatorname{lrd}_k(o)}{\operatorname{lrd}_k(p)}. +\] + +LOF около 1 означает сопоставимую с соседями плотность, значительно больше 1 — +локальный выброс. Метод ловит аномалию рядом с плотной группой даже при других +глобальных плотностях, но зависит от масштаба, \(k\) и стоимости соседей. В +sklearn для новых данных нужен `novelty=True`; тогда нельзя интерпретировать +`predict` на train как обычный outlier fit. + +## One-Class SVM и реконструкция + +One-Class SVM с RBF-ядром отделяет основную массу в feature space от начала +координат. \(\nu\in(0,1]\) контролирует верхнюю границу доли train-нарушений и +нижнюю границу доли опорных векторов; \(\gamma\) задаёт локальность. Метод +чувствителен к scaling и плохо масштабируется по \(n\). + +PCA/autoencoder могут использовать reconstruction error +\(\|\mathbf x-\hat{\mathbf x}\|^2\): необычный объект хуже восстанавливается +моделью нормальной структуры. Это работает лишь если train преимущественно +нормален и модель не настолько гибка, чтобы хорошо реконструировать всё. +Категориальные и разномасштабные признаки требуют содержательной loss. + +## Оценка при слабой разметке + +Полностью доказать качество без предположений или меток нельзя. Полезен набор +проверок: + +1. Временной holdout и отсутствие будущих статистик в признаках. +2. Ручная экспертиза случайной выборки из top-score и обычной зоны. +3. Precision@\(k\), число alerts и coverage разных типов, а не только ROC-AUC. +4. Стабильность ranking при seed, масштабе, подвыборке и умеренных параметрах. +5. Synthetic anomalies только как unit-тест известных сбоев, не как замена + реальным аномалиям. +6. Мониторинг drift score и доли alerts после запуска. + +Active learning помогает направлять дорогую разметку, но выбор только самых +аномальных создаёт selection bias; добавляют случайные и пограничные объекты. + +## Разобранный пример + +Для значений \([10,10,11,9,10,40]\) медиана \(10\), абсолютные отклонения +\([0,0,1,1,0,30]\), MAD равна \(0.5\) (среднее двух центральных значений +\(0\) и \(1\)). Для 40: +\(z^{robust}=0.6745(30/0.5)=40.47\). Сигнал огромен, но это всё ещё означает +«редко относительно этой выборки», а не «ошибка датчика». + +## Самостоятельный пример sklearn + +```python +import numpy as np +from sklearn.ensemble import IsolationForest +from sklearn.preprocessing import StandardScaler + +rng = np.random.default_rng(42) +normal = rng.normal(size=(500, 2)) +rare = rng.uniform(6, 8, size=(10, 2)) +X = np.vstack([normal, rare]) +X = StandardScaler().fit_transform(X) + +model = IsolationForest( + n_estimators=300, contamination=10 / 510, random_state=42 +) +model.fit(X) +score = -model.score_samples(X) # больше = аномальнее +print("top indices:", np.argsort(score)[-12:][::-1]) +``` + +В production scaler и detector обучают на прошлом окне, а score считают на +будущем; пример лишь демонстрирует API. + +## Типичные ошибки и ограничения + +- Объявлять `contamination=0.01` оценкой истинной частоты. +- Обучаться на периоде инцидента и поглотить аномалию как норму. +- Использовать случайный split для сезонных временных данных. +- Оценивать только синтетическими далёкими точками. +- Выдавать score за вероятность без калибровочных меток. +- Игнорировать capacity проверяющих и обратную связь после alert. +- Применять один глобальный детектор к неоднородным подгруппам. + +## Краткое резюме + +- Аномалия определяется относительно контекста, данных и действия. +- Isolation Forest использует короткие пути, LOF — локальный дефицит плотности. +- Порог задаётся budget/cost или независимой слабой разметкой. +- Без надёжных меток нужны временной holdout, экспертиза, baselines и устойчивость. + +## Связанные материалы + +- Практика: [`../assignments/03_anomaly.md`](../assignments/03_anomaly.md). + +- Предыдущая глава: [`03_dimensionality_reduction.md`](03_dimensionality_reduction.md). +- Метрики и пороги: [`../../06_classical_ml/notes/03_metrics_thresholds_and_calibration.md`](../../06_classical_ml/notes/03_metrics_thresholds_and_calibration.md). +- Термины: [`../glossary.md`](../glossary.md). diff --git a/modules/09_neural_networks/README.md b/modules/09_neural_networks/README.md new file mode 100644 index 0000000..329e9b4 --- /dev/null +++ b/modules/09_neural_networks/README.md @@ -0,0 +1,37 @@ +# Модуль 09. Нейросети и backpropagation + +Плановый срок: **недели 18–20**. + +## Предварительные знания + +Линейная алгебра (векторы, матрицы, матричное умножение), производные и правило +цепочки, базовые NumPy и Python. + +## Цели + +- понимать перцептрон, MLP, вычислительный граф и backpropagation +- выбирать активации, loss, инициализацию и нормализацию +- диагностировать исчезающие/взрывающиеся градиенты и переобучение + +## Уроки + +1. [Перцептрон, линейные слои, XOR и универсальная аппроксимация](notes/01_perceptron_mlp_xor.md) +2. [Chain rule, backpropagation и automatic differentiation](notes/02_backprop_autodiff.md) +3. [Активации, initialization, normalization и dropout](notes/03_activations_initialization_regularization.md) + +Термины модуля: [глоссарий](glossary.md). + +Для каждого урока: прочитайте теорию, воспроизведите ключевой пример своими +руками и запишите три тезиса без подсказки. Формулы из этого модуля нужно +уметь связать с кодом и наблюдаемым поведением модели. + +## Задания + +- [`assignments/01_mlp_numpy.md`](assignments/01_mlp_numpy.md) — MLP и backprop с нуля +- [`assignments/02_activations.md`](assignments/02_activations.md) — Эксперимент с активациями +- [`assignments/03_pytorch_training.md`](assignments/03_pytorch_training.md) — Надёжный training loop + +## Завершение модуля + +Модуль завершён, когда все задания удовлетворяют критериям приёмки, +вы можете объяснить основные решения без ноутбука и получили review. diff --git a/modules/09_neural_networks/assignments/01_mlp_numpy.md b/modules/09_neural_networks/assignments/01_mlp_numpy.md new file mode 100644 index 0000000..ffd75d2 --- /dev/null +++ b/modules/09_neural_networks/assignments/01_mlp_numpy.md @@ -0,0 +1,39 @@ +# MLP и backprop с нуля + +## Теория перед выполнением + +Прочитайте [MLP и гладкий XOR](../notes/01_perceptron_mlp_xor.md) и +[матричный backprop](../notes/02_backprop_autodiff.md). + +## Цель + +Реализовать двухслойную сеть NumPy для XOR и простой классификации. + +## Что сделать + +1. Сформулировать ожидаемый результат до запуска эксперимента. +2. Реализовать простой baseline. +3. Реализовать основной метод самостоятельно настолько, насколько требует задание. +4. Добавить автоматические проверки ключевых вычислений. +5. Провести эксперимент, сохранить метрики и примеры ошибок. +6. Заполнить отчёт по `common/REPORT_TEMPLATE.md`. + +## Результат + +Forward/backward, обучение, gradient check и loss curves. + +Решение разместите рядом в `solution/`. Не коммитьте большие данные, +веса или секреты. + +## Критерии приёмки + +- Все градиенты выведены и проверены численно; XOR обучается устойчиво. +- Код читаем, разбит на функции и запускается по инструкции. +- Выводы подтверждаются измерениями и не выходят за пределы эксперимента. + +## Вопросы для самопроверки + +- Почему выбран именно этот метод и какой baseline он должен превзойти? +- Где здесь возможна утечка данных или некорректная оценка? +- Как изменится результат при других данных или ограничениях? +- Какую ошибку модели вы считаете наиболее важной и почему? diff --git a/modules/09_neural_networks/assignments/02_activations.md b/modules/09_neural_networks/assignments/02_activations.md new file mode 100644 index 0000000..8f93295 --- /dev/null +++ b/modules/09_neural_networks/assignments/02_activations.md @@ -0,0 +1,39 @@ +# Эксперимент с активациями + +## Теория перед выполнением + +Прочитайте [активации, инициализацию и регуляризацию](../notes/03_activations_initialization_regularization.md) +и повторите [исчезающие градиенты](../notes/02_backprop_autodiff.md#исчезающие-и-взрывающиеся-градиенты). + +## Цель + +Сравнить sigmoid, tanh, ReLU, LeakyReLU и GELU в одинаковой MLP. + +## Что сделать + +1. Сформулировать ожидаемый результат до запуска эксперимента. +2. Реализовать простой baseline. +3. Реализовать основной метод самостоятельно настолько, насколько требует задание. +4. Добавить автоматические проверки ключевых вычислений. +5. Провести эксперимент, сохранить метрики и примеры ошибок. +6. Заполнить отчёт по `common/REPORT_TEMPLATE.md`. + +## Результат + +Графики активаций/градиентов, скорость сходимости и выводы. + +Решение разместите рядом в `solution/`. Не коммитьте большие данные, +веса или секреты. + +## Критерии приёмки + +- Выбор учитывает насыщение, dead ReLU и вычислительную стоимость. +- Код читаем, разбит на функции и запускается по инструкции. +- Выводы подтверждаются измерениями и не выходят за пределы эксперимента. + +## Вопросы для самопроверки + +- Почему выбран именно этот метод и какой baseline он должен превзойти? +- Где здесь возможна утечка данных или некорректная оценка? +- Как изменится результат при других данных или ограничениях? +- Какую ошибку модели вы считаете наиболее важной и почему? diff --git a/modules/09_neural_networks/assignments/03_pytorch_training.md b/modules/09_neural_networks/assignments/03_pytorch_training.md new file mode 100644 index 0000000..483457b --- /dev/null +++ b/modules/09_neural_networks/assignments/03_pytorch_training.md @@ -0,0 +1,39 @@ +# Надёжный training loop + +## Теория перед выполнением + +Прочитайте [autodiff и gradient checking](../notes/02_backprop_autodiff.md) и +[регуляризацию и нормализацию](../notes/03_activations_initialization_regularization.md). + +## Цель + +Написать PyTorch loop с train/eval, checkpoint, early stopping и метриками. + +## Что сделать + +1. Сформулировать ожидаемый результат до запуска эксперимента. +2. Реализовать простой baseline. +3. Реализовать основной метод самостоятельно настолько, насколько требует задание. +4. Добавить автоматические проверки ключевых вычислений. +5. Провести эксперимент, сохранить метрики и примеры ошибок. +6. Заполнить отчёт по `common/REPORT_TEMPLATE.md`. + +## Результат + +Модули модели/обучения, конфиг и воспроизводимый запуск. + +Решение разместите рядом в `solution/`. Не коммитьте большие данные, +веса или секреты. + +## Критерии приёмки + +- Нет градиентов в validation; лучший checkpoint восстанавливается; seed фиксируется. +- Код читаем, разбит на функции и запускается по инструкции. +- Выводы подтверждаются измерениями и не выходят за пределы эксперимента. + +## Вопросы для самопроверки + +- Почему выбран именно этот метод и какой baseline он должен превзойти? +- Где здесь возможна утечка данных или некорректная оценка? +- Как изменится результат при других данных или ограничениях? +- Какую ошибку модели вы считаете наиболее важной и почему? diff --git a/modules/09_neural_networks/glossary.md b/modules/09_neural_networks/glossary.md new file mode 100644 index 0000000..00b59bb --- /dev/null +++ b/modules/09_neural_networks/glossary.md @@ -0,0 +1,26 @@ +# Глоссарий модуля 09 + +- **Активация (activation)** — нелинейная функция, применяемая к выходу аффинного слоя. +- **Autodiff** — автоматическое применение правил дифференцирования к графу выполненных операций. +- **Backpropagation** — reverse-mode autodiff для распространения производной скалярной потери от выхода к параметрам. +- **BatchNorm** — нормализация канала по объектам и пространственным позициям мини-батча с обучаемыми масштабом и сдвигом. +- **Вычислительный граф** — ориентированный граф тензоров и операций, задающий зависимости результата. +- **Градиент** — вектор частных производных скалярной функции по аргументам. +- **Gradient checking** — сравнение аналитического/autodiff-градиента с конечными разностями. +- **GELU** — гладкая активация \(x\Phi(x)\). +- **Dropout** — регуляризация случайным обнулением активаций при обучении. +- **Исчезающий градиент** — экспоненциальное уменьшение градиента при прохождении через слои. +- **Инициализация He/Kaiming** — выбор дисперсии весов около \(2/d_{\mathrm{in}}\) для ReLU-подобных функций. +- **Инициализация Xavier/Glorot** — масштаб весов, согласующий дисперсии входа и выхода. +- **LayerNorm** — нормализация признаков внутри отдельного объекта. +- **Логит** — ненормализованная оценка класса до sigmoid или softmax. +- **MLP** — многослойный перцептрон, композиция аффинных слоёв и нелинейностей. +- **Перцептрон** — линейная пороговая модель бинарной классификации. +- **Прямой проход** — вычисление выхода и потери от входа к концу сети. +- **ReLU** — активация \(\max(0,x)\). +- **Reverse mode** — режим autodiff, эффективно вычисляющий производные одного выхода по многим входам. +- **Sigmoid** — функция \((1+e^{-x})^{-1}\), отображающая число в интервал \((0,1)\). +- **SiLU/Swish** — гладкая активация \(x\sigma(x)\). +- **tanh** — гиперболический тангенс с диапазоном \((-1,1)\). +- **Универсальная аппроксимация** — семейство теорем о приближении функций нейросетями при достаточной ширине. +- **XOR** — исключающее ИЛИ; классический пример линейно неразделимой задачи. diff --git a/modules/09_neural_networks/notes/01_perceptron_mlp_xor.md b/modules/09_neural_networks/notes/01_perceptron_mlp_xor.md new file mode 100644 index 0000000..46e5a26 --- /dev/null +++ b/modules/09_neural_networks/notes/01_perceptron_mlp_xor.md @@ -0,0 +1,193 @@ +# Перцептрон, MLP, XOR и универсальная аппроксимация + +> От линейного порога к многослойной сети: как композиция простых преобразований создаёт нелинейную границу решения. + +## Мотивация + +Линейная модель быстра и интерпретируема, но разделяет пространство только гиперплоскостью. Пиксели, звук и табличные признаки обычно взаимодействуют: значение одного признака важно в зависимости от другого. Нейронная сеть изучает такие взаимодействия как композицию линейных слоёв и нелинейностей. + +После главы читатель сможет вручную вычислить ответ перцептрона и MLP, объяснить невозможность линейного решения XOR и понимать точный смысл теоремы об универсальной аппроксимации. + +## Интуиция + +Нейрон вычисляет взвешенную сумму входов, затем применяет функцию активации. Один нейрон проводит одну границу. Скрытый слой строит несколько промежуточных признаков — например, «выполнено хотя бы одно условие» и «выполнены оба». Следующий слой комбинирует их. + +Обучаемы веса и смещения; архитектура и выбранная активация фиксированы во время одного запуска. Аналогия с конструктором полезна, но ограничена: сеть не получает готовые смысловые детали, а находит численные представления минимизацией функции потерь. + +## Определения и обозначения + +- **Перцептрон**: \(\hat y=\mathbb{1}\{\mathbf{w}^{\top}\mathbf{x}+b\ge 0\}\), где \(\mathbf{x},\mathbf{w}\in\mathbb{R}^{d}\). +- **Линейный (аффинный) слой** для батча \(\mathbf{X}\in\mathbb{R}^{n\times d}\): + \(\mathbf{Z}=\mathbf{X}\mathbf{W}+\mathbf{1}\mathbf{b}^{\top}\), где + \(\mathbf{W}\in\mathbb{R}^{d\times h}\), \(\mathbf{b}\in\mathbb{R}^{h}\). +- **MLP** (multilayer perceptron) — композиция аффинных слоёв и поэлементных нелинейностей. +- **Логит** — ненормализованная оценка класса до sigmoid или softmax. +- **Гиперплоскость** \(\{\mathbf{x}:\mathbf{w}^{\top}\mathbf{x}+b=0\}\) — граница линейного классификатора. + +Для двухслойной сети: + +\[ +\mathbf{H}=\phi(\mathbf{X}\mathbf{W}_1+\mathbf{1}\mathbf{b}_1^\top),\qquad +\mathbf{Z}=\mathbf{H}\mathbf{W}_2+\mathbf{1}\mathbf{b}_2^\top . +\] + +### Строки батча и векторы-столбцы — одна модель + +Для одного объекта удобно писать вход столбцом: +\(\mathbf{a}=\mathbf{W}^{\top}\mathbf{x}+\mathbf{b}\), где +\(\mathbf{x}\in\mathbb{R}^{d}\), \(\mathbf{W}\in\mathbb{R}^{d\times h}\). +В коде объекты обычно лежат **строками** матрицы +\(\mathbf{X}\in\mathbb{R}^{n\times d}\), поэтому все \(n\) вычислений сразу имеют +вид \(\mathbf{A}=\mathbf{X}\mathbf{W}+\mathbf{1}\mathbf{b}^{\top}\). +Строка \(i\) этой формулы — транспонированная запись той же формулы для +\(\mathbf{x}^{(i)}\); это не две разные конвенции весов. В следующей главе вывод +для столбца превращается в batch-код заменами +\(\mathbf{x}\boldsymbol{\delta}^{\top}\to\mathbf{X}^{\top}\boldsymbol{\Delta}\) +и \(\boldsymbol{\delta}\to\sum_i\boldsymbol{\Delta}_{i,:}\) для bias. + +## От перцептрона к обучаемому классификатору + +Пороговая функция почти всюду имеет нулевую производную, поэтому современный бинарный классификатор обучают по гладким логитам \(z\). Вероятность задают \(p=\sigma(z)\), а отрицательный логарифм правдоподобия Бернулли: + +\[ +\ell(z,y)=-y\log\sigma(z)-(1-y)\log(1-\sigma(z)). +\] + +Численно устойчивое равенство: + +\[ +\ell(z,y)=\max(z,0)-yz+\log(1+e^{-|z|}). +\] + +Поэтому в PyTorch применяют `BCEWithLogitsLoss` к логитам и не ставят `Sigmoid` перед ней. + +## Почему XOR не разделяется прямой + +XOR равен единице для \((0,1)\), \((1,0)\) и нулю для \((0,0)\), \((1,1)\). Предположим, существует линейный порог с положительным классом при \(w_1x_1+w_2x_2+b>0\). Тогда: + +\[ +b<0,\quad w_1+b>0,\quad w_2+b>0,\quad w_1+w_2+b<0. +\] + +Из двух средних неравенств \(w_1>-b\), \(w_2>-b\), поэтому +\(w_1+w_2+b>-b>0\), что противоречит последнему неравенству. Значит, одного линейного слоя недостаточно. + +Сеть с пороговыми скрытыми нейронами решает XOR: + +\[ +h_1=\mathbb{1}\{x_1+x_2-0.5\ge0\}\quad(\mathrm{OR}), +\] +\[ +h_2=\mathbb{1}\{x_1+x_2-1.5\ge0\}\quad(\mathrm{AND}), +\qquad +\hat y=\mathbb{1}\{h_1-2h_2-0.5\ge0\}. +\] + +### Гладкий суррогат XOR для обучения + +Пороговая конструкция доказывает представимость XOR, но её производная почти +всюду нулевая. Для gradient descent заменяют каждый порог гладким приближением, +например \(s_\tau(a)=\sigma(a/\tau)\) или `tanh`; при малом \(\tau>0\) +sigmoid становится круче и приближается к порогу. Скрытые нейроны тогда дают +мягкие оценки OR/AND, выходной слой выдаёт логит, а `BCEWithLogitsLoss` +сравнивает его с меткой XOR. Во время обучения это не точная булева схема: +веса и границы находятся по данным, а класс получают порогом вероятности только +на inference. Слишком малое \(\tau\) сразу насыщает sigmoid и снова ослабляет +градиенты, поэтому обычно используют умеренный масштаб или `tanh`. + +## Разобранный пример + +**Дано.** \(\mathbf{x}=(1,0)^\top\), веса скрытого слоя + +\[ +\mathbf{W}_1= +\begin{bmatrix}1&1\\1&1\end{bmatrix},\quad +\mathbf{b}_1=(-0.5,-1.5)^\top, +\] + +пороговая активация, \(\mathbf{w}_2=(1,-2)^\top\), \(b_2=-0.5\). + +**Решение.** + +1. \(\mathbf{a}_1=\mathbf{W}_1^\top\mathbf{x}+\mathbf{b}_1=(0.5,-0.5)^\top\). +2. \(\mathbf{h}=(1,0)^\top\). +3. \(z=\mathbf{w}_2^\top\mathbf{h}+b_2=1-0-0.5=0.5\). +4. \(\hat y=1\), что совпадает с XOR\((1,0)\). + +**Проверка.** Для \((1,1)\): \(\mathbf{h}=(1,1)\), \(z=1-2-0.5=-1.5\), ответ \(0\). + +## Универсальная аппроксимация + +Одна из форм теоремы утверждает: для любой непрерывной функции +\(f:K\to\mathbb{R}\) на компактном \(K\subset\mathbb{R}^{d}\), любого +\(\varepsilon>0\) и подходящей неполиномиальной активации существует сеть с одним скрытым слоем + +\[ +g(\mathbf{x})=\sum_{j=1}^{m}a_j\phi(\mathbf{w}_j^\top\mathbf{x}+b_j)+c +\] + +такая, что + +\[ +\sup_{\mathbf{x}\in K}|f(\mathbf{x})-g(\mathbf{x})|<\varepsilon. +\] + +Это утверждение о **существовании**, а не о том, что градиентный спуск найдёт параметры, что ширина \(m\) мала или что модель обобщится вне \(K\). Глубина часто представляет композиционные функции существенно меньшим числом параметров, чем один очень широкий слой. + +## Параметры и вычислительная стоимость + +Слой \(d\to h\) содержит \(dh+h\) параметров. Для батча размера \(n\) основная стоимость — \(O(ndh)\). MLP теряет пространственную структуру изображения: слой для RGB-изображения \(224\times224\) и 1000 нейронов имел бы около 150 млн весов. Это мотивирует свёртки. + +## Runnable NumPy: MLP учит XOR + +```python +import numpy as np + +rng = np.random.default_rng(7) +X = np.array([[0., 0.], [0., 1.], [1., 0.], [1., 1.]]) +y = np.array([[0.], [1.], [1.], [0.]]) +W1 = rng.normal(0, 0.5, (2, 4)) +b1 = np.zeros((1, 4)) +W2 = rng.normal(0, 0.5, (4, 1)) +b2 = np.zeros((1, 1)) + +def sigmoid(z): + return 1.0 / (1.0 + np.exp(-np.clip(z, -30, 30))) + +for _ in range(10000): + H = np.tanh(X @ W1 + b1) + P = sigmoid(H @ W2 + b2) + dz2 = (P - y) / len(X) # BCE + sigmoid + dW2, db2 = H.T @ dz2, dz2.sum(axis=0, keepdims=True) + dz1 = (dz2 @ W2.T) * (1 - H**2) + dW1, db1 = X.T @ dz1, dz1.sum(axis=0, keepdims=True) + W1 -= 0.1 * dW1; b1 -= 0.1 * db1 + W2 -= 0.1 * dW2; b2 -= 0.1 * db2 + +print(np.round(sigmoid(np.tanh(X @ W1 + b1) @ W2 + b2), 3)) +``` + +Формы: `X (4,2)`, `W1 (2,4)`, `H (4,4)`, `W2 (4,1)`. `keepdims=True` не даёт смещениям случайно изменить форму. + +## Типичные ошибки и ограничения + +- **Нет нелинейности между слоями.** Композиция аффинных отображений остаётся аффинной: + \((\mathbf{X}\mathbf{W}_1+\mathbf{b}_1)\mathbf{W}_2+\mathbf{b}_2 + =\mathbf{X}\mathbf{W}+\mathbf{b}\). +- **Sigmoid перед `BCEWithLogitsLoss`.** Получается двойная sigmoid и хуже численная устойчивость. +- **Уверенность путают с правильностью.** Большой логит не гарантирует калиброванную вероятность. +- **Теорему аппроксимации принимают за гарантию обучения.** Она ничего не обещает об оптимизации и данных. +- **Неявный broadcasting.** Всегда проверяйте формы батча, весов и смещений. + +## Краткое резюме + +- Перцептрон строит одну линейную границу; MLP создаёт нелинейные признаки. +- XOR доказывает принципиальную недостаточность одного линейного порога. +- Нелинейность обязательна между линейными слоями. +- Универсальная аппроксимация гарантирует существование, но не обучаемость и не обобщение. + +## Связанные материалы + +- Следующая глава: [`02_backprop_autodiff.md`](02_backprop_autodiff.md). +- Практика: [`../assignments/01_mlp_numpy.md`](../assignments/01_mlp_numpy.md). +- Термины: [`../glossary.md`](../glossary.md). diff --git a/modules/09_neural_networks/notes/02_backprop_autodiff.md b/modules/09_neural_networks/notes/02_backprop_autodiff.md new file mode 100644 index 0000000..047d31f --- /dev/null +++ b/modules/09_neural_networks/notes/02_backprop_autodiff.md @@ -0,0 +1,216 @@ +# Вычислительные графы, backpropagation и autodiff + +> Обратное распространение вычисляет градиенты сложной композиции за один обратный проход, повторно используя локальные производные. + +## Мотивация + +Сеть может иметь миллионы параметров. Вычислять производную потери отдельно по каждому весу означало бы повторять почти один и тот же расчёт миллионы раз. Backpropagation организует правило цепочки как динамическое программирование на вычислительном графе. + +## Интуиция + +Прямой проход сохраняет промежуточные величины. Обратный проход отвечает для каждого узла: «на сколько изменится итоговая потеря при малом изменении этого узла?» Вклад по каждой исходящей ветви суммируется. Обучаемые параметры меняются оптимизатором; граф описывает только зависимость вычислений. + +## Определения и обозначения + +- **Вычислительный граф** — ориентированный ациклический граф операций и тензоров. +- **Локальная производная** — производная выхода конкретной операции по её входу. +- **Adjoint** узла \(v\): \(\bar v:=\partial L/\partial v\). +- **Backpropagation** — reverse-mode automatic differentiation для скалярного выхода \(L\). +- **Autodiff** — точное до ошибок floating point применение правил дифференцирования к выполненным операциям; это не символьная алгебра и не конечные разности. + +## Скалярный граф: полный вывод + +Пусть + +\[ +a=wx+b,\qquad h=\tanh(a),\qquad +L=\frac12(h-y)^2. +\] + +Прямой проход вычисляет \(a,h,L\). Обратный: + +\[ +\bar L=1,\qquad +\bar h=\frac{\partial L}{\partial h}=h-y, +\] +\[ +\bar a=\bar h\frac{\partial h}{\partial a} +=(h-y)(1-\tanh^2 a), +\] +\[ +\frac{\partial L}{\partial w}=\bar a x,\quad +\frac{\partial L}{\partial b}=\bar a,\quad +\frac{\partial L}{\partial x}=\bar a w. +\] + +Каждый множитель — локальная чувствительность. Если узел используется дважды, например \(L=v^2+3v\), то +\(\bar v=2v+3\): вклады путей **складываются**. + +## Матричный backprop для двухслойной MLP + +Для одного объекта используем векторы-столбцы: + +\[ +\mathbf{a}_1=\mathbf{W}_1^\top\mathbf{x}+\mathbf{b}_1,\quad +\mathbf{h}=\phi(\mathbf{a}_1),\quad +\mathbf{z}=\mathbf{W}_2^\top\mathbf{h}+\mathbf{b}_2. +\] + +Пусть \(\boldsymbol{\delta}_2=\nabla_{\mathbf{z}}L\). Из дифференциала + +\[ +d\mathbf{z}=d\mathbf{W}_2^\top\mathbf{h} ++\mathbf{W}_2^\top d\mathbf{h}+d\mathbf{b}_2 +\] + +и \(dL=\boldsymbol{\delta}_2^\top d\mathbf{z}\) получаем + +\[ +\nabla_{\mathbf{W}_2}L=\mathbf{h}\boldsymbol{\delta}_2^\top,\quad +\nabla_{\mathbf{b}_2}L=\boldsymbol{\delta}_2,\quad +\nabla_{\mathbf{h}}L=\mathbf{W}_2\boldsymbol{\delta}_2. +\] + +Для поэлементной \(\phi\): + +\[ +\boldsymbol{\delta}_1= +(\mathbf{W}_2\boldsymbol{\delta}_2)\odot\phi'(\mathbf{a}_1), +\] +\[ +\nabla_{\mathbf{W}_1}L=\mathbf{x}\boldsymbol{\delta}_1^\top,\qquad +\nabla_{\mathbf{b}_1}L=\boldsymbol{\delta}_1. +\] + +Размерности проверяют вывод: если \(\mathbf{W}_1\in\mathbb{R}^{d\times h}\), то +\(\mathbf{x}\boldsymbol{\delta}_1^\top\in\mathbb{R}^{d\times h}\). + +Для батча со строками объектов складываем градиенты отдельных примеров: +\(\nabla_{\mathbf W_1}L=\mathbf X^\top\boldsymbol\Delta_1\), а +\(\nabla_{\mathbf b_1}L=\sum_i\boldsymbol\Delta_{1,i,:}\) (или их среднее, если +loss усреднён). Это та же формула для вектора-столбца, векторизованная по строкам; +соглашения подробно сопоставлены в +[предыдущей главе](01_perceptron_mlp_xor.md#строки-батча-и-векторы-столбцы--одна-модель). + +Для softmax и cross-entropy одного объекта с one-hot целью \(\mathbf{y}\): + +\[ +p_k=\frac{e^{z_k}}{\sum_j e^{z_j}},\qquad +L=-\sum_k y_k\log p_k,\qquad +\nabla_{\mathbf{z}}L=\mathbf{p}-\mathbf{y}. +\] + +Последнее равенство получается после учёта всех недиагональных производных softmax; нельзя дифференцировать её компоненты как независимые sigmoid. + +## Разобранный числовой пример + +**Дано.** \(x=2,\ w=0.5,\ b=-0.2,\ y=0.4\). + +**Решение.** + +1. \(a=0.5\cdot2-0.2=0.8\). +2. \(h=\tanh(0.8)\approx0.6640\). +3. \(L=(0.6640-0.4)^2/2\approx0.03485\). +4. \(\bar a=(0.6640-0.4)(1-0.6640^2)\approx0.1476\). +5. \(\partial L/\partial w=0.1476\cdot2\approx0.2952\); + \(\partial L/\partial b\approx0.1476\). + +**Проверка.** При увеличении \(w\) выход \(h\) растёт и удаляется от \(y=0.4\), поэтому положительный знак градиента разумен. + +## Forward mode и reverse mode + +Forward mode переносит \(dv/du\) от входа к выходу и выгоден, когда входных переменных мало, а выходов много. Reverse mode переносит \(\partial L/\partial v\) от выхода назад и за стоимость порядка нескольких прямых проходов получает производные скалярной потери по всем параметрам. Поэтому обучение сетей использует reverse mode. + +Граф требует памяти для сохранённых активаций. Gradient checkpointing экономит память: часть активаций не хранится, а повторно вычисляется в backward. + +## Autograd в PyTorch + +```python +import torch + +torch.manual_seed(0) +x = torch.tensor(2.0) +y = torch.tensor(0.4) +w = torch.tensor(0.5, requires_grad=True) +b = torch.tensor(-0.2, requires_grad=True) + +h = torch.tanh(w * x + b) +loss = 0.5 * (h - y) ** 2 +loss.backward() + +print(float(loss)) +print(float(w.grad), float(b.grad)) +``` + +`requires_grad=True` включает запись операций. `loss.backward()` заполняет `.grad` только у листовых тензоров. Градиенты **накапливаются**, поэтому в цикле обучения вызывают `optimizer.zero_grad()` перед новым backward. + +Операции внутри `torch.no_grad()` не записываются; `tensor.detach()` возвращает тензор без связи с предыдущей историей. Неуместный `detach()` молча обрывает путь градиента. + +## Gradient checking + +Центральная конечная разность для параметра \(\theta_j\): + +\[ +g_j^{\mathrm{num}}= +\frac{L(\theta_j+\varepsilon)-L(\theta_j-\varepsilon)}{2\varepsilon} +=\frac{\partial L}{\partial\theta_j}+O(\varepsilon^2). +\] + +Сравнивают относительную ошибку: + +\[ +\mathrm{err}= +\frac{\|\mathbf{g}^{\mathrm{ana}}-\mathbf{g}^{\mathrm{num}}\|_2} +{\|\mathbf{g}^{\mathrm{ana}}\|_2+\|\mathbf{g}^{\mathrm{num}}\|_2+10^{-12}}. +\] + +```python +import numpy as np + +x, y, w, b = 2.0, 0.4, 0.5, -0.2 +def loss(weight): + h = np.tanh(weight * x + b) + return 0.5 * (h - y) ** 2 + +a = w * x + b +analytic = (np.tanh(a) - y) * (1 - np.tanh(a) ** 2) * x +eps = 1e-5 +numeric = (loss(w + eps) - loss(w - eps)) / (2 * eps) +print(analytic, numeric, abs(analytic - numeric)) +``` + +Проверку делают в `float64`, на маленькой детерминированной модели, без dropout и с фиксированным поведением нормализации. Слишком большое \(\varepsilon\) даёт ошибку аппроксимации, слишком малое — катастрофическое вычитание. Обычный диапазон \(10^{-4}\ldots10^{-6}\) подбирают экспериментально. + +## Исчезающие и взрывающиеся градиенты + +В глубокой композиции якобианы перемножаются: + +\[ +\nabla_{\mathbf{h}_0}L= +\mathbf{J}_1^\top\mathbf{J}_2^\top\cdots +\mathbf{J}_K^\top\nabla_{\mathbf{h}_K}L. +\] + +Если спектральные нормы якобианов преимущественно меньше 1, градиент затухает; если больше 1 — растёт. Инициализация, нормализация, ReLU-подобные функции, residual-связи и gradient clipping смягчают проблему. Clipping лечит симптом взрыва, но не исправляет плохой масштаб данных или learning rate. + +## Типичные ошибки и ограничения + +- Вызов `backward()` дважды по освобождённому графу без `retain_graph=True`. +- Обновление параметров до завершения backward нарушает согласованность сохранённых значений. +- In-place операция может уничтожить тензор, необходимый для производной. +- Сравнение gradient check около точки излома ReLU ненадёжно. +- Производная верна, но loss усреднён в одном коде и суммирован в другом: градиенты отличаются в \(n\) раз. + +## Краткое резюме + +- Backprop — эффективное применение правила цепочки на графе. +- Reverse mode оптимален для скалярной потери и множества параметров. +- В ветвящемся графе градиенты путей суммируются. +- Конечные разности полезны как тест реализации, но не как способ обучения. + +## Связанные материалы + +- Предыдущая глава: [`01_perceptron_mlp_xor.md`](01_perceptron_mlp_xor.md). +- Следующая глава: [`03_activations_initialization_regularization.md`](03_activations_initialization_regularization.md). +- Дальше: [backprop свёртки и `im2col`](../../10_computer_vision/notes/01_convolutions_and_cnn.md#как-backprop-проходит-через-свёртку). +- Термины: [`../glossary.md`](../glossary.md). diff --git a/modules/09_neural_networks/notes/03_activations_initialization_regularization.md b/modules/09_neural_networks/notes/03_activations_initialization_regularization.md new file mode 100644 index 0000000..45e2228 --- /dev/null +++ b/modules/09_neural_networks/notes/03_activations_initialization_regularization.md @@ -0,0 +1,242 @@ +# Активации, инициализация, нормализация и dropout + +> Стабильное обучение зависит не только от архитектуры: масштаб сигналов и градиентов должен оставаться управляемым на всей глубине сети. + +## Мотивация + +Неудачная активация или масштаб весов может сделать глубокую сеть практически необучаемой: значения насыщаются, градиенты исчезают либо растут до `NaN`. Нормализация и регуляризация решают разные задачи: первая улучшает геометрию оптимизации, вторая ограничивает переобучение. + +## Функции активации + +### Sigmoid + +\[ +\sigma(x)=\frac1{1+e^{-x}},\qquad +\sigma'(x)=\sigma(x)(1-\sigma(x)). +\] + +Диапазон \((0,1)\), максимальная производная \(1/4\). При больших \(|x|\) функция насыщается, а градиент близок к нулю. Выход не центрирован около нуля. Sigmoid уместна в выходе бинарной/многометочной модели, но обычно не в скрытых слоях. + +### tanh + +\[ +\tanh'(x)=1-\tanh^2(x). +\] + +Диапазон \((-1,1)\), выход центрирован, но насыщение сохраняется. Полезна в некоторых рекуррентных механизмах и маленьких гладких MLP. + +### ReLU + +\[ +\operatorname{ReLU}(x)=\max(0,x),\qquad +\operatorname{ReLU}'(x)= +\begin{cases}0,&x<0,\\1,&x>0.\end{cases} +\] + +В нуле выбирают субградиент (PyTorch использует 0). ReLU проста, разреживает активации и не насыщается справа. Минус: нейрон может навсегда попасть в отрицательную область («мёртвая ReLU»), особенно при большом learning rate. + +### GELU + +\[ +\operatorname{GELU}(x)=x\Phi(x), +\] + +где \(\Phi\) — CDF стандартного нормального распределения. Частая аппроксимация: + +\[ +0.5x\left[1+\tanh\left(\sqrt{2/\pi}\, +(x+0.044715x^3)\right)\right]. +\] + +GELU плавно подавляет отрицательные значения и широко используется в Transformers. + +### SiLU (Swish) + +\[ +\operatorname{SiLU}(x)=x\sigma(x),\qquad +\operatorname{SiLU}'(x)=\sigma(x)+x\sigma(x)(1-\sigma(x)). +\] + +Она гладкая и немонотонна около отрицательных значений; часто эффективна в современных CNN. GELU/SiLU вычислительно дороже ReLU, а преимущество зависит от архитектуры и оборудования. + +## Разобранный пример активаций + +**Дано.** \(x=-1\). + +**Решение.** + +- \(\sigma(-1)\approx0.2689\), \(\sigma'(-1)\approx0.1966\); +- \(\tanh(-1)\approx-0.7616\), производная \(\approx0.4200\); +- \(\operatorname{ReLU}(-1)=0\), производная \(0\); +- \(\operatorname{SiLU}(-1)=-0.2689\), производная + \(0.2689-0.1966\approx0.0723\). + +**Интерпретация.** ReLU полностью блокирует отрицательный сигнал; гладкие функции пропускают небольшой сигнал и градиент. + +## Вывод инициализации Xavier и He + +Пусть слой без смещения: + +\[ +z_j=\sum_{i=1}^{d}W_{ij}x_i, +\] + +причём \(W_{ij}\) независимы, имеют нулевое среднее и независимы от \(x_i\). Тогда + +\[ +\operatorname{Var}(z_j) +=\sum_{i=1}^{d}\operatorname{Var}(W_{ij}x_i) +=d\,\operatorname{Var}(W)\operatorname{Var}(x). +\] + +Чтобы до активации сохранить дисперсию, нужно +\(\operatorname{Var}(W)\approx1/d\). Это основа **Xavier/Glorot**; компромисс прямого и обратного прохода: + +\[ +\operatorname{Var}(W)=\frac{2}{d_{\mathrm{in}}+d_{\mathrm{out}}}. +\] + +После симметричного входа ReLU примерно половина значений обнуляется, поэтому второй момент уменьшается вдвое. Для компенсации **He/Kaiming**: + +\[ +\operatorname{Var}(W)=\frac{2}{d_{\mathrm{in}}}. +\] + +Для свёртки \(d_{\mathrm{in}}=C_{\mathrm{in}}K_hK_w\). Предположения о независимости и одинаковом распределении приближённы, но дают хороший начальный масштаб. + +## Batch Normalization + +Для канала и мини-батча значений \(x_1,\ldots,x_m\): + +\[ +\mu_B=\frac1m\sum_i x_i,\qquad +\sigma_B^2=\frac1m\sum_i(x_i-\mu_B)^2, +\] +\[ +\hat x_i=\frac{x_i-\mu_B}{\sqrt{\sigma_B^2+\varepsilon}},\qquad +y_i=\gamma\hat x_i+\beta. +\] + +\(\gamma,\beta\) обучаемы. В CNN статистика обычно считается по осям \(N,H,W\) отдельно для каждого канала. В `train()` BatchNorm использует статистику текущего батча и обновляет running estimates; в `eval()` — накопленные оценки. Малый батч делает статистики шумными и может ухудшить результат. + +## Layer Normalization + +LayerNorm нормализует признаки **внутри одного объекта**: + +\[ +\mu=\frac1d\sum_{j=1}^{d}x_j,\qquad +\sigma^2=\frac1d\sum_j(x_j-\mu)^2. +\] + +Она не зависит от размера батча и одинаково работает в train/eval, поэтому стандартна в Transformers. Оси критичны: `LayerNorm(d)` нормализует последнюю размерность длины \(d\), а не автоматически каналы CNN. + +BatchNorm и LayerNorm не взаимозаменяемы: у них разные оси, статистики и inductive bias. + +## Dropout + +При обучении независимо берём +\(m_j\sim\operatorname{Bernoulli}(1-p)\) и используем inverted dropout: + +\[ +\tilde h_j=\frac{m_j}{1-p}h_j. +\] + +Тогда + +\[ +\mathbb{E}[\tilde h_j]=h_j, +\] + +поэтому в режиме inference масштабировать активации не нужно. Dropout вносит шум и препятствует хрупкой совместной адаптации признаков. Он не заменяет validation, weight decay или augmentation; в современных CNN с BatchNorm его часто используют умеренно или только в голове. + +## Runnable PyTorch: статистика сигналов + +```python +import torch +from torch import nn + +torch.manual_seed(3) +x = torch.randn(256, 128) + +for name, activation, init in [ + ("sigmoid+xavier", nn.Sigmoid(), "xavier"), + ("relu+kaiming", nn.ReLU(), "kaiming"), + ("silu+kaiming", nn.SiLU(), "kaiming"), +]: + layers = [] + for _ in range(6): + linear = nn.Linear(128, 128) + if init == "xavier": + nn.init.xavier_normal_(linear.weight) + else: + nn.init.kaiming_normal_(linear.weight, nonlinearity="relu") + nn.init.zeros_(linear.bias) + layers += [linear, activation] + with torch.no_grad(): + h = x + variances = [] + for layer in layers: + h = layer(h) + if isinstance(layer, (nn.Sigmoid, nn.ReLU, nn.SiLU)): + variances.append(h.var().item()) + print(name, [round(v, 4) for v in variances]) +``` + +Эксперимент показывает тенденцию, а не математическую гарантию: распределение меняется после обучения. + +## Runnable PyTorch: train и eval + +```python +import torch +from torch import nn + +torch.manual_seed(0) +block = nn.Sequential( + nn.Linear(4, 4), + nn.BatchNorm1d(4), + nn.ReLU(), + nn.Dropout(p=0.5), +) +x = torch.randn(8, 4) + +block.train() +y_train_1, y_train_2 = block(x), block(x) +block.eval() +with torch.no_grad(): + y_eval_1, y_eval_2 = block(x), block(x) + +print("train equal:", torch.allclose(y_train_1, y_train_2)) +print("eval equal:", torch.allclose(y_eval_1, y_eval_2)) +``` + +## Практический выбор + +- ReLU — надёжный дешёвый baseline для MLP/CNN. +- GELU — стандартный старт для Transformer-подобных моделей. +- SiLU — частый выбор современных CNN. +- Xavier — для tanh/линейных активаций; He — для ReLU-подобных. +- BatchNorm — когда батчи достаточно велики и CNN-практика это подтверждает. +- LayerNorm — когда нужна независимость от батча. +- Dropout выбирают по validation; значение \(p\) — вероятность **обнуления**, не сохранения. + +## Типичные ошибки и ограничения + +- Забытый `model.eval()` делает inference случайным и меняет поведение BatchNorm. +- Нулевые одинаковые веса скрытого слоя не нарушают симметрию: нейроны получают одинаковые градиенты. +- Слишком большой dropout вызывает недообучение. +- He initialization с sigmoid не устраняет насыщение. +- `eps` в нормализации нужен для устойчивости, но слишком большое значение заметно меняет масштаб. +- Нормализация не исправляет утечку данных, неверную loss или чрезмерный learning rate. + +## Краткое резюме + +- Активация создаёт нелинейность и определяет прохождение градиента. +- Xavier и He приближённо сохраняют масштаб сигнала для разных активаций. +- BatchNorm использует статистики батча, LayerNorm — признаки одного объекта. +- Dropout стохастически регуляризует только в режиме обучения. + +## Связанные материалы + +- Предыдущая глава: [`02_backprop_autodiff.md`](02_backprop_autodiff.md). +- Практика: [`../assignments/02_activations.md`](../assignments/02_activations.md). +- Термины: [`../glossary.md`](../glossary.md). diff --git a/modules/10_computer_vision/README.md b/modules/10_computer_vision/README.md new file mode 100644 index 0000000..55145a5 --- /dev/null +++ b/modules/10_computer_vision/README.md @@ -0,0 +1,40 @@ +# Модуль 10. Компьютерное зрение + +Плановый срок: **недели 21–23**. + +## Предварительные знания + +MLP, backpropagation, функции активации и training loop из +[модуля 09](../09_neural_networks/README.md), а также базовые NumPy и PyTorch. + +## Цели + +- понимать convolution, receptive field и основные CNN +- обучать и дообучать классификаторы изображений +- понимать пользу предобучения и выбирать глубину/размер архитектуры +- оценивать ошибки, дисбаланс и сдвиг данных + +## Уроки + +1. [Свёртки, pooling, augmentation и CNN](notes/01_convolutions_and_cnn.md) +2. [LeNet, AlexNet, VGG, ResNet и skip connections](notes/02_classic_architectures_and_resnet.md) +3. [ImageNet pretraining, transfer learning и fine-tuning](notes/03_pretraining_and_transfer.md) +4. [Выбор архитектуры под качество, данные, latency и память](notes/04_architecture_selection.md) +5. [Detection и segmentation — обзор](notes/05_detection_and_segmentation.md) + +Термины модуля: [глоссарий](glossary.md). + +Для каждого урока: прочитайте теорию, воспроизведите ключевой пример своими +руками и запишите три тезиса без подсказки. Формулы из этого модуля нужно +уметь связать с кодом и наблюдаемым поведением модели. + +## Задания + +- [`assignments/01_convolution.md`](assignments/01_convolution.md) — Свёртка вручную +- [`assignments/02_cnn_classification.md`](assignments/02_cnn_classification.md) — CNN-классификатор +- [`assignments/03_transfer_learning.md`](assignments/03_transfer_learning.md) — Fine-tuning предобученной CNN + +## Завершение модуля + +Модуль завершён, когда все задания удовлетворяют критериям приёмки, +вы можете объяснить основные решения без ноутбука и получили review. diff --git a/modules/10_computer_vision/assignments/01_convolution.md b/modules/10_computer_vision/assignments/01_convolution.md new file mode 100644 index 0000000..fefb023 --- /dev/null +++ b/modules/10_computer_vision/assignments/01_convolution.md @@ -0,0 +1,38 @@ +# Свёртка вручную + +## Теория перед выполнением + +Прочитайте [свёртки, формы выхода и рецептивное поле](../notes/01_convolutions_and_cnn.md). + +## Цель + +Реализовать 2D convolution NumPy и применить edge filters. + +## Что сделать + +1. Сформулировать ожидаемый результат до запуска эксперимента. +2. Реализовать простой baseline. +3. Реализовать основной метод самостоятельно настолько, насколько требует задание. +4. Добавить автоматические проверки ключевых вычислений. +5. Провести эксперимент, сохранить метрики и примеры ошибок. +6. Заполнить отчёт по `common/REPORT_TEMPLATE.md`. + +## Результат + +Код, тест против PyTorch и визуализации feature maps. + +Решение разместите рядом в `solution/`. Не коммитьте большие данные, +веса или секреты. + +## Критерии приёмки + +- Корректно обработаны stride/padding; объяснён receptive field. +- Код читаем, разбит на функции и запускается по инструкции. +- Выводы подтверждаются измерениями и не выходят за пределы эксперимента. + +## Вопросы для самопроверки + +- Почему выбран именно этот метод и какой baseline он должен превзойти? +- Где здесь возможна утечка данных или некорректная оценка? +- Как изменится результат при других данных или ограничениях? +- Какую ошибку модели вы считаете наиболее важной и почему? diff --git a/modules/10_computer_vision/assignments/02_cnn_classification.md b/modules/10_computer_vision/assignments/02_cnn_classification.md new file mode 100644 index 0000000..88ba1f9 --- /dev/null +++ b/modules/10_computer_vision/assignments/02_cnn_classification.md @@ -0,0 +1,39 @@ +# CNN-классификатор + +## Теория перед выполнением + +Прочитайте [основы CNN и augmentation](../notes/01_convolutions_and_cnn.md) и +[классические CNN/ResNet](../notes/02_classic_architectures_and_resnet.md). + +## Цель + +Обучить небольшую CNN на Fashion-MNIST или CIFAR-10. + +## Что сделать + +1. Сформулировать ожидаемый результат до запуска эксперимента. +2. Реализовать простой baseline. +3. Реализовать основной метод самостоятельно настолько, насколько требует задание. +4. Добавить автоматические проверки ключевых вычислений. +5. Провести эксперимент, сохранить метрики и примеры ошибок. +6. Заполнить отчёт по `common/REPORT_TEMPLATE.md`. + +## Результат + +Конфиг, кривые обучения, confusion matrix и каталог ошибок. + +Решение разместите рядом в `solution/`. Не коммитьте большие данные, +веса или секреты. + +## Критерии приёмки + +- Есть baseline, augmentation применяется только к train, ошибки проанализированы по классам. +- Код читаем, разбит на функции и запускается по инструкции. +- Выводы подтверждаются измерениями и не выходят за пределы эксперимента. + +## Вопросы для самопроверки + +- Почему выбран именно этот метод и какой baseline он должен превзойти? +- Где здесь возможна утечка данных или некорректная оценка? +- Как изменится результат при других данных или ограничениях? +- Какую ошибку модели вы считаете наиболее важной и почему? diff --git a/modules/10_computer_vision/assignments/03_transfer_learning.md b/modules/10_computer_vision/assignments/03_transfer_learning.md new file mode 100644 index 0000000..62c2fd6 --- /dev/null +++ b/modules/10_computer_vision/assignments/03_transfer_learning.md @@ -0,0 +1,43 @@ +# Fine-tuning предобученной CNN + +## Теория перед выполнением + +Прочитайте [transfer learning](../notes/03_pretraining_and_transfer.md) и +[выбор архитектуры под ограничения](../notes/04_architecture_selection.md). + +## Цель + +Дообучить ResNet/EfficientNet на небольшом датасете кошек и собак и измерить, +что именно даёт предобучение. + +## Что сделать + +1. Сформулировать ожидаемый результат до запуска эксперимента. +2. Обучить простую CNN как baseline. +3. Сравнить одну архитектуру со случайными и предобученными весами. +4. Для предобученной модели сравнить frozen backbone, partial и full fine-tuning. +5. Сравнить маленькую и более глубокую архитектуру при одинаковом протоколе. +6. Зафиксировать accuracy/F1, время, память, число параметров и примеры ошибок. +7. Заполнить отчёт по `common/REPORT_TEMPLATE.md`. + +## Результат + +Таблица экспериментов и объяснение, почему выбранная модель оптимальна при заданном +объёме данных и вычислительном бюджете. + +Решение разместите рядом в `solution/`. Не коммитьте большие данные, +веса или секреты. + +## Критерии приёмки + +- Явно показана разница случайных и ImageNet-весов; learning rate различается для + head/backbone; test-set не участвует в выборе архитектуры. +- Код читаем, разбит на функции и запускается по инструкции. +- Выводы подтверждаются измерениями и не выходят за пределы эксперимента. + +## Вопросы для самопроверки + +- Почему выбран именно этот метод и какой baseline он должен превзойти? +- Где здесь возможна утечка данных или некорректная оценка? +- Как изменится результат при других данных или ограничениях? +- Какую ошибку модели вы считаете наиболее важной и почему? diff --git a/modules/10_computer_vision/glossary.md b/modules/10_computer_vision/glossary.md new file mode 100644 index 0000000..20ac7ce --- /dev/null +++ b/modules/10_computer_vision/glossary.md @@ -0,0 +1,32 @@ +# Глоссарий модуля 10 + +- **Anchor** — шаблонный bounding box, относительно которого модель предсказывает смещение. +- **AP/mAP** — площадь под precision-recall curve для класса / среднее AP по классам и заданным IoU. +- **Augmentation** — случайное семантически допустимое преобразование обучающего примера и его разметки. +- **Backbone** — часть модели, извлекающая карту визуальных признаков. +- **Batch latency** — время обработки батча; не равно latency одного запроса. +- **Bounding box** — прямоугольник, локализующий объект. +- **Depthwise convolution** — отдельная пространственная свёртка каждого входного канала. +- **Detection** — предсказание классов и местоположений экземпляров объектов. +- **Dilation** — расстояние между элементами ядра свёртки. +- **Domain shift** — различие распределений исходных и целевых данных. +- **Encoder/decoder** — части dense-модели, соответственно сжимающие признаки и восстанавливающие разрешение. +- **Feature Pyramid Network** — многомасштабное объединение карт признаков. +- **Fine-tuning** — продолжение обучения предобученных параметров на целевой задаче. +- **FLOPs/MACs** — теоретические оценки арифметических операций; не прямое измерение времени. +- **Global average pooling** — усреднение каждого канала по всем пространственным позициям. +- **ImageNet pretraining** — предварительное обучение весов на ImageNet. +- **Instance segmentation** — отдельная пиксельная маска каждого экземпляра. +- **IoU (Jaccard)** — отношение площади пересечения областей к площади объединения. +- **Latency** — задержка одного запроса; p95 — значение, быстрее которого завершились 95% запросов. +- **NMS** — удаление дублирующих boxes по score и IoU. +- **Padding** — дополнение границ входа перед свёрткой. +- **Panoptic segmentation** — совместная семантическая и экземплярная разметка пикселей. +- **Pooling** — локальное агрегирование значений с уменьшением или сохранением разрешения. +- **Receptive field** — область исходного входа, способная повлиять на элемент признаков. +- **Residual/skip connection** — короткая ветвь, добавляющая вход блока к изученной поправке. +- **Semantic segmentation** — классификация каждого пикселя без разделения экземпляров одного класса. +- **Stride** — шаг перемещения ядра по входу. +- **Transfer learning** — перенос представления или параметров из исходной задачи в целевую. +- **Свёртка/Cross-correlation** — применение общего локального ядра ко всем позициям. +- **Throughput** — число обработанных объектов или запросов в единицу времени. diff --git a/modules/10_computer_vision/notes/01_convolutions_and_cnn.md b/modules/10_computer_vision/notes/01_convolutions_and_cnn.md new file mode 100644 index 0000000..a78a84d --- /dev/null +++ b/modules/10_computer_vision/notes/01_convolutions_and_cnn.md @@ -0,0 +1,179 @@ +# Свёртки, pooling, augmentation и CNN + +> Свёрточная сеть использует локальность и разделение весов, чтобы обрабатывать изображения существенно эффективнее полносвязной сети. + +## Мотивация + +Изображение \(224\times224\times3\) содержит 150 528 чисел. Полносвязный слой на 1000 признаков потребовал бы более 150 млн весов и не учитывал бы, что один и тот же контур может находиться в любом месте. Свёртка применяет один локальный детектор ко всем позициям. + +## Представление изображения + +В математике будем использовать +\(\mathbf{X}\in\mathbb{R}^{C_{\mathrm{in}}\times H\times W}\) — каналы, высота, ширина. Батч PyTorch имеет форму `(N, C, H, W)`. RGB — три канала, но их порядок и нормализация должны соответствовать предобучению. + +**Ядро** \(\mathbf{K}\in\mathbb{R}^{C_{\mathrm{out}}\times C_{\mathrm{in}}\times K_h\times K_w}\). На практике `Conv2d` вычисляет cross-correlation без переворота ядра: + +\[ +Y_{o,i,j}=b_o+ +\sum_{c=1}^{C_{\mathrm{in}}} +\sum_{u=1}^{K_h}\sum_{v=1}^{K_w} +K_{o,c,u,v}X_{c,iS_h+u-P_h,jS_w+v-P_w}. +\] + +Индексы вне изображения при zero padding считаются нулями. + +## Padding, stride, dilation и размер выхода + +Для одной пространственной оси размера \(H\): + +\[ +H_{\mathrm{out}}= +\left\lfloor +\frac{H+2P-D(K-1)-1}{S}+1 +\right\rfloor, +\] + +где \(P\) — padding, \(S\) — stride, \(D\) — dilation. При нечётном \(K\), \(S=1,D=1,P=(K-1)/2\) размер сохраняется (`same`). + +**Разобранный пример.** Вход \(32\times32\), ядро \(5\), padding \(2\), stride \(2\): + +\[ +H_{\mathrm{out}}=\left\lfloor\frac{32+4-4-1}{2}+1\right\rfloor +=\lfloor16.5\rfloor=16. +\] + +Для \(C_{\mathrm{in}}=3,C_{\mathrm{out}}=16\) параметров +\(16(3\cdot5\cdot5+1)=1216\). Число не зависит от \(H,W\), что и выражает разделение весов. + +## Рецептивное поле + +**Рецептивное поле** элемента — область исходного изображения, способная на него повлиять. Пусть \(r_l\) — его размер, а \(j_l\) — шаг между центрами соседних элементов в координатах входа. Начально \(r_0=j_0=1\). Для слоя \(l\): + +\[ +j_l=j_{l-1}S_l,\qquad +r_l=r_{l-1}+(K_l-1)D_lj_{l-1}. +\] + +Две свёртки \(3\times3\), stride 1 дают: +\(r_1=3\), \(r_2=3+2=5\). Между ними есть нелинейность, а параметров обычно меньше, чем у одной \(5\times5\). Если второй слой имеет stride 2, рецептивное поле остаётся 5, но следующий слой перемещается по входу с шагом \(j_2=2\). + +Теоретическое рецептивное поле задаёт максимум; эффективное влияние часто сосредоточено ближе к центру. + +## Как backprop проходит через свёртку + +Свёртка — линейный слой с общими весами. В backward каждый выходной градиент +умножается на тот же локальный patch: вклады всех позиций в один элемент ядра +**суммируются**, потому что этот вес использовался многократно. Градиент входного +пикселя, наоборот, складывает вклады всех окон, которые его покрывали. Это ровно +правило ветвящегося вычислительного графа из +[модуля 09](../../09_neural_networks/notes/02_backprop_autodiff.md). + +`im2col` делает связь явной: все patches разворачиваются в строки матрицы +\(\mathbf X_{\text{col}}\), ядра — в столбцы \(\mathbf W_{\text{col}}\), и +forward становится +\(\mathbf Y_{\text{col}}=\mathbf X_{\text{col}}\mathbf W_{\text{col}}\). +Тогда \(\nabla\mathbf W_{\text{col}}=\mathbf X_{\text{col}}^\top +\nabla\mathbf Y_{\text{col}}\), как у MLP. Для градиента входа результат +\(\nabla\mathbf X_{\text{col}}\) сворачивают обратно (`col2im`), суммируя +перекрытия. Реальные kernels могут не материализовать большую `im2col`-матрицу, +но математически выполняют эквивалентное накопление. + +## Pooling и downsampling + +- **Max pooling** берёт максимум окна и сохраняет сильнейший локальный отклик. +- **Average pooling** усредняет окно. +- **Global average pooling** превращает каждый канал \(H\times W\) в одно число и резко уменьшает число параметров головы. +- Свёртка со stride 2 — обучаемая альтернатива pooling. + +Downsampling снижает стоимость и повышает устойчивость к малым сдвигам, но теряет координатную точность. Для сегментации и мелких объектов слишком раннее уменьшение разрешения опасно. + +## Эквивариантность и инвариантность + +Идеальная свёртка эквивариантна переносу: +\(f(T_\Delta X)=T_\Delta f(X)\). Padding, stride и границы нарушают точность этого свойства. Pooling и global pooling создают приближённую инвариантность, но CNN не становится автоматически инвариантной к повороту, масштабу и перспективе. + +## NumPy: свёртка вручную + +```python +import numpy as np + +x = np.array([[1., 2., 0.], + [0., 1., 3.], + [2., 1., 0.]]) +k = np.array([[1., 0.], + [0., -1.]]) + +out = np.empty((2, 2)) +for i in range(2): + for j in range(2): + patch = x[i:i + 2, j:j + 2] + out[i, j] = np.sum(patch * k) + +print(out) +# [[ 0. -1.] +# [-1. 1.]] +``` + +Это cross-correlation, соответствующая фреймворкам. Настоящая математическая свёртка предварительно перевернула бы ядро по обеим осям. + +## Минимальная CNN в PyTorch + +```python +import torch +from torch import nn + +model = nn.Sequential( + nn.Conv2d(3, 16, kernel_size=3, padding=1), + nn.BatchNorm2d(16), + nn.ReLU(), + nn.MaxPool2d(2), # 32x32 -> 16x16 + nn.Conv2d(16, 32, 3, padding=1), + nn.ReLU(), + nn.AdaptiveAvgPool2d(1), + nn.Flatten(), + nn.Linear(32, 10), +) + +x = torch.randn(8, 3, 32, 32) +logits = model(x) +loss = nn.CrossEntropyLoss()(logits, torch.randint(0, 10, (8,))) +loss.backward() +print(logits.shape, float(loss)) +``` + +`CrossEntropyLoss` принимает сырые логиты формы `(N, classes)` и целые метки, поэтому softmax в модели не нужен. + +## Augmentation + +Augmentation выбирает случайное преобразование \(T\), сохраняющее метку, и обучает на \(T(X)\). Она кодирует ожидаемые инвариантности и увеличивает разнообразие, но не создаёт новую независимую информацию. + +- Геометрия: crop, resize, horizontal flip, малый rotation. +- Фотометрия: brightness, contrast, saturation, blur, noise. +- Смешивание: MixUp \(\tilde x=\lambda x_i+(1-\lambda)x_j\) с мягкой целью; CutMix заменяет прямоугольную область. + +Преобразование допустимо только если сохраняет смысл. Горизонтальный flip может быть неверен для текста, дорожных знаков или лево/право в медицине. Для detection одновременно преобразуют bounding boxes, для segmentation — маску (интерполяцией nearest, не bilinear). + +Validation/test не должны содержать случайные преобразования, кроме заранее определённого test-time augmentation. Нормализация — не augmentation; статистики и диапазон должны совпадать с моделью. + +## Типичные ошибки и ограничения + +- Перепутаны `(N,H,W,C)` и `(N,C,H,W)`. +- Неверная формула выхода из-за пропущенных floor или dilation. +- Padding сохраняет размер, но добавляет искусственную границу. +- Augmentation применяется к изображению, но не к маске/box. +- Слишком агрессивный crop удаляет объект и создаёт ошибочную метку. +- Увеличение каналов повышает память активаций; число параметров — не единственная стоимость. + +## Краткое резюме + +- Свёртка использует локальность и общие веса. +- Размер выхода определяется kernel, padding, stride и dilation. +- Рецептивное поле накапливается по слоям; downsampling меняет шаг. +- Augmentation должна сохранять семантику и синхронно менять разметку. + +## Связанные материалы + +- База: [MLP и формы батча](../../09_neural_networks/notes/01_perceptron_mlp_xor.md). +- Следующая глава: [`02_classic_architectures_and_resnet.md`](02_classic_architectures_and_resnet.md). +- Практика: [`../assignments/01_convolution.md`](../assignments/01_convolution.md). +- Термины: [`../glossary.md`](../glossary.md). diff --git a/modules/10_computer_vision/notes/02_classic_architectures_and_resnet.md b/modules/10_computer_vision/notes/02_classic_architectures_and_resnet.md new file mode 100644 index 0000000..abbca82 --- /dev/null +++ b/modules/10_computer_vision/notes/02_classic_architectures_and_resnet.md @@ -0,0 +1,167 @@ +# LeNet, AlexNet, VGG, ResNet и skip connections + +> История CNN показывает переход от небольших специализированных сетей к глубоким residual-моделям, которые легче оптимизировать. + +## Мотивация + +Название архитектуры — не список модных слоёв, а набор инженерных компромиссов. Классические сети объясняют, откуда появились локальные ядра, глубокие стеки \(3\times3\), глобальное уменьшение разрешения и residual-связи. + +## LeNet-5 + +LeNet-5 (1998) предназначалась для рукописных цифр: свёртки, subsampling и полносвязная голова. В современном изложении: + +\[ +32^2\to C6@28^2\to pool@14^2\to C16@10^2\to pool@5^2 +\to FC. +\] + +Главная идея — пространственно общие локальные детекторы. Сеть мала по современным меркам и рассчитана на простые одноканальные изображения. + +## AlexNet + +AlexNet (2012) показала масштабируемость глубоких CNN на ImageNet: + +- ReLU вместо saturating tanh; +- обучение на GPU; +- augmentation и dropout; +- большие ранние ядра и пять convolutional слоёв; +- local response normalization, ныне почти не используемая. + +Её большой первый stride быстро теряет разрешение, а полносвязная голова содержит много параметров. Историческое значение велико, но как новый production baseline архитектура обычно невыгодна. + +## VGG + +VGG (2014) систематически строит сеть из малых \(3\times3\) свёрток и pooling. Две \(3\times3\) свёртки имеют рецептивное поле \(5\times5\), три — \(7\times7\), но между ними стоят нелинейности. + +При одинаковых \(C\), одна \(5\times5\) свёртка имеет \(25C^2\) весов, две \(3\times3\) — \(18C^2\). Недостатки VGG-16/19: тяжёлые активации, много параметров в классификационной голове и отсутствие коротких путей для градиента. + +## Проблема деградации глубокой сети + +Более глубокая модель теоретически может воспроизвести неглубокую, добавив тождественные слои. Однако plain-сети при увеличении глубины иногда имеют **большую training error**. Это не классическое переобучение: ухудшается оптимизация. + +ResNet обучает не прямое отображение \(\mathcal{H}(\mathbf{x})\), а residual: + +\[ +\mathbf{y}=\mathcal{F}(\mathbf{x};\boldsymbol{\theta})+\mathbf{x}. +\] + +Если полезно тождество, достаточно приблизить \(\mathcal{F}(\mathbf{x})=\mathbf{0}\). + +## Почему skip connection помогает градиенту + +\[ +\frac{\partial\mathbf{y}}{\partial\mathbf{x}} +=\mathbf{J}_{\mathcal F}(\mathbf{x})+\mathbf{I}. +\] + +Для последовательности residual-блоков существует прямой аддитивный путь с множителем \(\mathbf{I}\). Даже если якобиан residual-ветви мал, градиент не обязан исчезнуть. Это не гарантирует идеальной оптимизации: плохие масштабы, активации и learning rate всё ещё вредят. + +Это прямое продолжение разбора произведения якобианов в +[модуле 09](../../09_neural_networks/notes/02_backprop_autodiff.md#исчезающие-и-взрывающиеся-градиенты): +plain-сеть передаёт градиент через произведение преобразований, а residual-блок +добавляет к локальному якобиану единичную матрицу. ResNet не отменяет chain rule, +а создаёт более короткие и лучше обусловленные пути для него. + +## BasicBlock и Bottleneck + +**BasicBlock** обычно содержит две \(3\times3\) свёртки и используется в ResNet-18/34. + +**Bottleneck** в ResNet-50/101/152: + +\[ +1\times1\ (C\to C_b)\;\to\; +3\times3\ (C_b\to C_b)\;\to\; +1\times1\ (C_b\to C_{\mathrm{out}}). +\] + +Первая \(1\times1\) уменьшает число каналов перед дорогой \(3\times3\), последняя расширяет. Если форма меняется, shortcut использует projection \(1\times1\): + +\[ +\mathbf{y}=\mathcal{F}(\mathbf{x})+\mathbf{W}_s*\mathbf{x}. +\] + +Складывать можно только одинаковые формы. Projection меняет каналы и/или разрешение, но уже не является чистым тождеством. + +## Разобранный пример параметров + +**Дано.** Вход и выход имеют \(C=256\), bottleneck width \(C_b=64\), смещения игнорируем. + +**Решение.** + +\[ +256\cdot64 + 3\cdot3\cdot64^2 + 64\cdot256 +=16384+36864+16384=69632. +\] + +Одна прямая \(3\times3\), \(256\to256\): + +\[ +9\cdot256^2=589824. +\] + +Bottleneck примерно в 8.5 раза дешевле по весам этой операции, хотя блоки нельзя сравнивать только по одному числу: есть нормализации, активации и различная выразительность. + +## Runnable PyTorch: residual-блок + +```python +import torch +from torch import nn + +class BasicBlock(nn.Module): + def __init__(self, in_channels, out_channels, stride=1): + super().__init__() + self.main = nn.Sequential( + nn.Conv2d(in_channels, out_channels, 3, stride, 1, bias=False), + nn.BatchNorm2d(out_channels), + nn.ReLU(), + nn.Conv2d(out_channels, out_channels, 3, padding=1, bias=False), + nn.BatchNorm2d(out_channels), + ) + self.skip = ( + nn.Identity() if in_channels == out_channels and stride == 1 + else nn.Sequential( + nn.Conv2d(in_channels, out_channels, 1, stride, bias=False), + nn.BatchNorm2d(out_channels), + ) + ) + self.relu = nn.ReLU() + + def forward(self, x): + return self.relu(self.main(x) + self.skip(x)) + +x = torch.randn(4, 64, 32, 32) +block = BasicBlock(64, 128, stride=2) +y = block(x) +print(y.shape) # (4, 128, 16, 16) +``` + +## Сравнение семейств + +- **LeNet** — учебная малая CNN, показывает базовый шаблон. +- **AlexNet** — исторический прорыв GPU/ReLU/augmentation. +- **VGG** — простая однородная архитектура, удобные признаки, но высокая стоимость. +- **ResNet** — residual-связи и масштабирование глубины; сильный универсальный baseline. + +Число слоёв в названии считается по правилам конкретного семейства и не является честной межсемейной метрикой стоимости. Сравнивают параметры, FLOPs/MACs, память активаций и измеренную latency. + +## Типичные ошибки и ограничения + +- Skip connection называют конкатенацией. В ResNet это обычно поэлементное сложение; DenseNet использует конкатенацию. +- Формы ветвей не совпадают при сложении. +- Делают вывод «глубже всегда лучше»: данные и бюджет могут не оправдать глубину. +- Считают параметры, но игнорируют большие feature maps и memory bandwidth. +- Переносят исторические детали AlexNet как современные best practices. + +## Краткое резюме + +- Классические CNN последовательно развивали локальность, глубину и обучаемость. +- Несколько малых ядер дают большое рецептивное поле с нелинейностями. +- ResNet обучает поправку к shortcut, создавая короткий путь сигналу и градиенту. +- Архитектуру нельзя оценивать только числом слоёв. + +## Связанные материалы + +- Предыдущая глава: [`01_convolutions_and_cnn.md`](01_convolutions_and_cnn.md). +- Следующая глава: [`03_pretraining_and_transfer.md`](03_pretraining_and_transfer.md). +- База градиентов: [backpropagation](../../09_neural_networks/notes/02_backprop_autodiff.md). +- Термины: [`../glossary.md`](../glossary.md). diff --git a/modules/10_computer_vision/notes/03_pretraining_and_transfer.md b/modules/10_computer_vision/notes/03_pretraining_and_transfer.md new file mode 100644 index 0000000..bdc0d1f --- /dev/null +++ b/modules/10_computer_vision/notes/03_pretraining_and_transfer.md @@ -0,0 +1,168 @@ +# ImageNet pretraining, transfer learning и fine-tuning + +> Предобученная модель переносит визуальное представление из большой исходной задачи; новая голова и осторожное дообучение адаптируют его к целевому домену. + +## Архитектура, датасет и веса — разные объекты + +`ResNet-50` задаёт устройство сети. **ImageNet** — датасет естественных изображений с примерно тысячей классов в типичной классификационной постановке. **Предобученные веса** — состояние параметров после оптимизации на этом датасете. + +Одинаковые ResNet-50 со случайными и ImageNet-весами имеют одну архитектуру, но принципиально разную отправную точку. Фраза «ImageNet-модель» без уточнения архитектуры и набора весов неоднозначна. + +## Что переносится + +Первые слои часто реагируют на границы, цветовые переходы и текстуры. Более глубокие комбинируют их в части и формы объектов. Классификационная голова переводит итоговые признаки в исходные классы. + +Для задачи «кошка или собака» старую голову заменяют новой. Backbone уже содержит полезные признаки, поэтому обычно: + +- быстрее сходится; +- требует меньше размеченных данных; +- даёт лучшее качество при малой выборке; +- меньше переобучается, если перенос уместен. + +Это не гарантия. При сильном domain shift — например, естественные фотографии против многоканальных медицинских томограмм — низкоуровневые статистики и полезные признаки отличаются. Но предобучение всё равно следует проверить экспериментом, а не отвергать по аналогии. + +## Три режима переноса + +1. **Feature extraction:** backbone заморожен, обучается только новая голова. +2. **Partial fine-tuning:** разморожены последние стадии; ранние остаются фиксированными. +3. **Full fine-tuning:** обучаются все параметры, обычно с меньшим learning rate. + +Заморозка уменьшает вычисления backward и риск переобучения, но ограничивает адаптацию. Чем больше целевой датасет и domain shift, тем больше оснований размораживать слои. + +## Математическая точка зрения + +Предобучение даёт начальную точку \(\boldsymbol{\theta}_0\), несущую prior о визуальных структурах. Fine-tuning решает + +\[ +\boldsymbol{\theta}^{\star} +\in\arg\min_{\boldsymbol{\theta}} +\frac1n\sum_{i=1}^{n} +\ell(f_{\boldsymbol{\theta}}(\mathbf{x}^{(i)}),y^{(i)}), +\] + +стартуя не из случайной точки, а из \(\boldsymbol{\theta}_0\). Малый learning rate ограничивает разрушение полезного представления. Иногда используют discriminative rates: + +\[ +\eta_{\mathrm{head}}>\eta_{\mathrm{late}}>\eta_{\mathrm{early}}. +\] + +Это эвристика: значения выбирают по validation. + +## Нормализация входа и BatchNorm + +Предобученные веса ожидают конкретный диапазон, порядок каналов, resize/crop и нормализацию. Для канала: + +\[ +x'=\frac{x-\mu_c}{\sigma_c}. +\] + +Неверный preprocessing создаёт искусственный domain shift. + +Замороженные параметры BatchNorm и режим модуля — разные вещи. `requires_grad=False` запрещает градиенты \(\gamma,\beta\), но `model.train()` всё ещё обновляет running statistics. На малом датасете это может повредить. Нужно осознанно решить, оставлять ли backbone BatchNorm в `eval()` или адаптировать статистики. + +BatchNorm продолжает идею нормализации из +[модуля 09](../../09_neural_networks/notes/03_activations_initialization_regularization.md): +на train он нормирует канал по статистикам текущего mini-batch и обучает +\(\gamma,\beta\), а на inference использует running mean/variance. Поэтому +перенос весов включает не только свёрточные параметры, но и состояние BN; +маленький или сдвинутый batch может сделать его статистики ненадёжными. + +## Разобранный протокол + +**Дано.** 3000 размеченных фотографий двух классов, близких ImageNet; GPU ограничен. + +**Решение.** + +1. Стратифицированно фиксируем train/validation/test до экспериментов. +2. Берём предобученную ResNet-18, заменяем head `512 -> 2`. +3. Обучаем head 3–5 эпох, backbone заморожен. +4. Размораживаем последнюю residual-стадию, уменьшаем её learning rate, например до \(10^{-4}\), а для головы оставляем \(10^{-3}\). +5. Выбираем checkpoint по validation, тест используем один раз. +6. Сравниваем с той же архитектурой без весов и с маленькой CNN при одинаковом бюджете. + +**Проверка.** Если train хорош, validation плох — усиливаем augmentation/regularization или замораживаем больше. Если обе метрики плохи — проверяем labels, preprocessing, learning rate и достаточность адаптации. + +## Runnable PyTorch/torchvision + +Код не скачивает данные, но демонстрирует полную настройку модели; веса `DEFAULT` потребуют уже закэшированный файл или сеть. Для полностью офлайн-запуска используйте `weights=None`. + +```python +import torch +from torch import nn +from torchvision.models import resnet18, ResNet18_Weights + +use_cached_pretraining = False +weights = ResNet18_Weights.DEFAULT if use_cached_pretraining else None +model = resnet18(weights=weights) + +for parameter in model.parameters(): + parameter.requires_grad = False +in_features = model.fc.in_features +model.fc = nn.Linear(in_features, 2) + +optimizer = torch.optim.AdamW(model.fc.parameters(), lr=1e-3) +x = torch.randn(4, 3, 224, 224) +y = torch.tensor([0, 1, 0, 1]) +loss = nn.CrossEntropyLoss()(model(x), y) +optimizer.zero_grad() +loss.backward() +optimizer.step() +print(float(loss), model.fc.weight.requires_grad) +``` + +Честное предобучение требует `weights=ResNet18_Weights.DEFAULT` и preprocessing из `weights.transforms()`. Флаг `False` оставляет пример воспроизводимым офлайн, но тогда это только демонстрация feature-extraction механики, не transfer learning. + +### Частичное размораживание + +```python +for parameter in model.layer4.parameters(): + parameter.requires_grad = True + +optimizer = torch.optim.AdamW([ + {"params": model.layer4.parameters(), "lr": 1e-4}, + {"params": model.fc.parameters(), "lr": 1e-3}, +], weight_decay=1e-4) +``` + +После изменения `requires_grad` оптимизатор создают заново или добавляют новую param group: иначе размороженные параметры могут не обновляться. + +## Catastrophic forgetting и leakage + +Слишком большой шаг может быстро уничтожить общие признаки — **catastrophic forgetting**. Контролируют learning rate, размораживают постепенно, используют early stopping. + +Утечка особенно опасна для похожих изображений: кадры одного видео, снимки одного пациента или дубликаты должны попадать в одно разбиение. Иначе transfer learning получает незаслуженно высокую оценку. + +## Как сравнивать + +При одинаковых split, preprocessing, augmentation, метриках и бюджете сравнивают: + +- маленькую CNN с нуля; +- выбранную архитектуру со случайными весами; +- предобученный frozen backbone; +- partial/full fine-tuning. + +Полезны learning curves, несколько seed и доверительный интервал. Один лучший запуск не доказывает устойчивого преимущества. + +## Типичные ошибки и ограничения + +- Случайные веса называют предобученной моделью. +- Заменяют head, но передают оптимизатору только старые параметры. +- Применяют другой порядок RGB/BGR или чужие mean/std. +- Размораживают всё сразу с learning rate головы. +- Обновляют BatchNorm статистики на крошечном батче неосознанно. +- Замораживают backbone и ожидают адаптации к радикально новому домену. + +## Краткое резюме + +- Архитектура, датасет и веса — не одно и то же. +- Transfer learning переносит представление; fine-tuning адаптирует параметры. +- Стратегия разморозки зависит от объёма данных и domain shift. +- Честное сравнение требует одинакового протокола и защиты от leakage. + +## Связанные материалы + +- Предыдущая глава: [`02_classic_architectures_and_resnet.md`](02_classic_architectures_and_resnet.md). +- Следующая глава: [`04_architecture_selection.md`](04_architecture_selection.md). +- База: [инициализация и нормализация](../../09_neural_networks/notes/03_activations_initialization_regularization.md). +- Практика: [`../assignments/03_transfer_learning.md`](../assignments/03_transfer_learning.md). +- Термины: [`../glossary.md`](../glossary.md). diff --git a/modules/10_computer_vision/notes/04_architecture_selection.md b/modules/10_computer_vision/notes/04_architecture_selection.md new file mode 100644 index 0000000..4c1742e --- /dev/null +++ b/modules/10_computer_vision/notes/04_architecture_selection.md @@ -0,0 +1,162 @@ +# Выбор архитектуры под качество, данные, latency и память + +> Лучшая модель — не самая глубокая, а самая дешёвая модель, которая устойчиво выполняет требования на целевом оборудовании. + +## Мотивация + +Accuracy на исследовательском GPU не описывает production-систему. Мобильное приложение ограничено размером пакета, RAM, энергией и p95 latency; сервер — throughput и стоимостью; маленький датасет — переобучением. Выбор архитектуры является многокритериальной задачей. + +## Формализация требований + +Пусть \(Q(\mathcal{M})\) — качество модели, \(T_{95}\) — p95 latency, +\(M_{\mathrm{peak}}\) — пиковая память, \(S\) — размер весов, \(C\) — стоимость. Практическая задача: + +\[ +\max_{\mathcal{M}} Q(\mathcal{M}) +\quad\text{при}\quad +T_{95}\le T_{\max},\ +M_{\mathrm{peak}}\le M_{\max},\ +S\le S_{\max},\ +C\le C_{\max}. +\] + +Если единого порога нет, ищут **Pareto frontier**: модель доминируется, если другая не хуже по всем критериям и лучше хотя бы по одному. + +## Метрики качества + +Accuracy подходит только при примерно равной цене ошибок и сбалансированных классах. Иначе используют macro-F1, ROC-AUC/PR-AUC, sensitivity при заданной specificity, calibration. Для detection — mAP, для segmentation — IoU/Dice. + +Сначала фиксируют бизнес-ограничение, затем метрику и порог. Иначе архитектуру оптимизируют под удобное, но нецелевое число. + +## Данные определяют режим + +- **Мало данных, близкий домен:** небольшой предобученный backbone, frozen/partial fine-tuning. +- **Много разнообразных данных:** более ёмкая модель и full fine-tuning либо обучение с нуля могут окупиться. +- **Сильный domain shift:** профильное предобучение, self-supervised learning или более глубокая адаптация. +- **Шумные labels:** аудит данных часто ценнее увеличения модели. +- **Высокое разрешение/мелкие объекты:** стоимость растёт квадратично по линейному размеру, но ранний resize может уничтожить сигнал. + +Learning curves отделяют ограничение данными от ограниченной ёмкости. Если train и validation плохи — возможен underfitting или ошибка pipeline; если train хорош, validation заметно хуже — overfitting/domain mismatch. + +Глубину нельзя оценивать отдельно от масштаба сигнала. Для ReLU-слоёв +He-инициализация выбирает дисперсию весов порядка \(2/\mathrm{fan\_in}\), чтобы +дисперсии активаций и градиентов не систематически затухали по слоям. Это +переносит принцип из +[модуля 09](../../09_neural_networks/notes/03_activations_initialization_regularization.md) +на свёртки, где +\(\mathrm{fan\_in}=C_{\mathrm{in}}K_hK_w\). Хорошая инициализация облегчает +обучение с нуля, но не компенсирует недостаток данных и не заменяет предобучение. + +## Стоимость: параметры, операции и активации + +Для Conv2d приблизительное число multiply-accumulate: + +\[ +\mathrm{MACs}= +H_{\mathrm{out}}W_{\mathrm{out}} +C_{\mathrm{out}}C_{\mathrm{in}}K_hK_w. +\] + +Параметров: + +\[ +P=C_{\mathrm{out}}(C_{\mathrm{in}}K_hK_w+1). +\] + +Но MACs не равны latency. Depthwise convolution имеет мало операций, но на конкретном устройстве может быть ограничена memory bandwidth или плохо оптимизирована. Latency измеряют после warm-up, нужным batch size, на целевом runtime и устройстве. + +Память inference включает веса, активации и workspace kernels. При обучении добавляются градиенты, optimizer states и сохранённые активации. Adam часто хранит два дополнительных момента на параметр. + +## Разобранный пример + +**Требование.** Android CPU, batch 1, p95 не более 50 мс, пакет до 20 МБ, macro-F1 не ниже 0.90. + +**Кандидаты после одинакового fine-tuning:** + +- A: F1 0.913, p95 82 мс, 18 МБ; +- B: F1 0.905, p95 41 мс, 14 МБ; +- C: F1 0.898, p95 25 мс, 8 МБ. + +**Решение.** A нарушает latency, C — качество. B — единственная допустимая. Более высокая accuracy A не делает её лучшей для задачи. + +**Проверка.** Измерение повторяют на слабейшем поддерживаемом устройстве, с реальным preprocessing и thermal throttling. + +## Семейства и inductive bias + +- ResNet-18/34 — понятные сильные baseline, часто удобны на сервере. +- MobileNet/EfficientNet-lite — depthwise/inverted bottlenecks для мобильных ограничений. +- EfficientNet — согласованное масштабирование глубины, ширины и разрешения. +- Vision Transformer — слабее локальный CNN bias, часто требует сильного предобучения; эффективность зависит от реализации. + +Название семейства не гарантирует скорость. Поддержка операторов runtime важнее теоретических FLOPs. + +## Практический протокол + +1. Зафиксировать split, метрику, допустимую деградацию и оборудование. +2. Создать дешёвый baseline, например предобученную ResNet-18 или MobileNet. +3. Проверить данные, leakage, дисбаланс и ошибки по подгруппам. +4. Сравнить 2–3 кандидата при одинаковом training budget. +5. Экспортировать в целевой runtime и измерить end-to-end, включая decode/resize. +6. При необходимости применить mixed precision/quantization, pruning или distillation. +7. Выбрать простейшую модель на допустимой части Pareto frontier. + +## Оптимизации после выбора baseline + +- **FP16/BF16** уменьшают память и могут ускорить GPU. +- **INT8 quantization** уменьшает размер и ускоряет поддерживаемое железо, но требует calibration или quantization-aware training и проверки качества. +- **Pruning** полезен только если runtime эффективно использует полученную разреженность. +- **Knowledge distillation** обучает компактного student имитировать soft targets teacher. +- **Batching** повышает throughput, но часто ухудшает latency одного запроса. + +Оптимизировать модель до корректного baseline рискованно: ошибка preprocessing не лечится quantization. + +## Runnable PyTorch: параметры и latency + +```python +import time +import torch +from torchvision.models import resnet18 + +model = resnet18(weights=None).eval() +x = torch.randn(1, 3, 224, 224) +parameters = sum(p.numel() for p in model.parameters()) + +with torch.inference_mode(): + for _ in range(10): # warm-up + model(x) + samples = [] + for _ in range(30): + start = time.perf_counter() + model(x) + samples.append((time.perf_counter() - start) * 1000) + +samples.sort() +p95 = samples[int(0.95 * (len(samples) - 1))] +print("parameters:", parameters) +print("p95 milliseconds:", round(p95, 2)) +``` + +На CUDA перед и после замера нужна `torch.cuda.synchronize()`, иначе измеряется постановка асинхронной операции в очередь. Этот микротест не включает чтение изображения и preprocessing. + +## Типичные ошибки и ограничения + +- Сравнивают модели на разных split или с разным tuning budget. +- Выбирают по среднему latency вместо p95/p99. +- Приравнивают FLOPs к реальному времени. +- Считают только размер файла, игнорируя RAM активаций. +- Измеряют batch 32, хотя production использует batch 1. +- Увеличивают глубину при проблеме качества данных. + +## Краткое резюме + +- Выбор архитектуры — constrained multi-objective optimization. +- Данные, метрика и целевое устройство должны быть известны до сравнения. +- Параметры и MACs полезны, но не заменяют end-to-end benchmark. +- Выбирают простейшую допустимую модель, а не абсолютный максимум качества. + +## Связанные материалы + +- Предыдущая глава: [`03_pretraining_and_transfer.md`](03_pretraining_and_transfer.md). +- Следующая глава: [`05_detection_and_segmentation.md`](05_detection_and_segmentation.md). +- База: [активации и He-инициализация](../../09_neural_networks/notes/03_activations_initialization_regularization.md). +- Термины: [`../glossary.md`](../glossary.md). diff --git a/modules/10_computer_vision/notes/05_detection_and_segmentation.md b/modules/10_computer_vision/notes/05_detection_and_segmentation.md new file mode 100644 index 0000000..65be54c --- /dev/null +++ b/modules/10_computer_vision/notes/05_detection_and_segmentation.md @@ -0,0 +1,188 @@ +# Основы detection и segmentation + +> Классификация отвечает «что на изображении», detection добавляет «где экземпляры», а segmentation присваивает класс каждому пикселю. + +## Постановки + +- **Object detection:** множество \(\{(\mathbf{b}_i,c_i,s_i)\}\), где box + \(\mathbf{b}_i=(x_{\min},y_{\min},x_{\max},y_{\max})\), класс \(c_i\), score \(s_i\). +- **Semantic segmentation:** карта классов + \(\mathbf{Y}\in\{1,\ldots,C\}^{H\times W}\); разные объекты одного класса не различаются. +- **Instance segmentation:** отдельная бинарная маска каждого экземпляра. +- **Panoptic segmentation:** каждому пикселю назначены семантический класс и, для countable objects, id экземпляра. + +Разметка detection дешевле масок, но не описывает форму. Выбор задачи диктует продукт: для подсчёта объектов нужны экземпляры, для площади поражения — пиксели. + +## Bounding boxes и IoU + +Площадь пересечения двух boxes \(\mathbf{A},\mathbf{B}\): + +\[ +w_I=\max(0,\min(x^A_2,x^B_2)-\max(x^A_1,x^B_1)), +\] +\[ +h_I=\max(0,\min(y^A_2,y^B_2)-\max(y^A_1,y^B_1)). +\] + +\[ +\operatorname{IoU}(A,B)= +\frac{|A\cap B|}{|A|+|B|-|A\cap B|}. +\] + +IoU находится в \([0,1]\), симметричен и равен 1 для совпадающих областей. + +**Разобранный пример.** \(A=(0,0,4,4)\), \(B=(2,1,5,5)\). +Пересечение имеет ширину \(2\), высоту \(3\), площадь \(6\). +\(|A|=16\), \(|B|=12\), объединение \(16+12-6=22\): + +\[ +\operatorname{IoU}=6/22\approx0.273. +\] + +Важно договориться, являются ли координаты непрерывными границами или индексами включённых пикселей; смешивание соглашений даёт off-by-one. + +## Архитектуры detection + +### Two-stage + +Faster R-CNN сначала строит region proposals, затем классифицирует и уточняет их. Обычно даёт сильное качество, особенно для сложных сцен, но имеет более сложный и медленный pipeline. + +### One-stage + +YOLO/SSD/RetinaNet напрямую предсказывают классы и boxes на плотной сетке признаков. Они удобны для real-time. Разделение «one-stage быстро, two-stage точно» — тенденция, не закон: версии и hardware важнее ярлыка. + +### Anchor-based и anchor-free + +Anchor-based модели привязывают предсказания к шаблонным boxes разных масштабов/aspect ratio и требуют matching. Anchor-free предсказывают центры, углы или расстояния до сторон без набора шаблонов. + +Feature Pyramid Network объединяет глубокую семантику с более высоким пространственным разрешением, помогая объектам разных размеров. + +## Loss detection + +Типичная многозадачная потеря: + +\[ +L=L_{\mathrm{cls}}+ +\lambda_{\mathrm{box}}L_{\mathrm{box}}+ +\lambda_{\mathrm{obj}}L_{\mathrm{objectness}}. +\] + +Классификация использует cross-entropy/focal loss, координаты — Smooth L1 или IoU-family loss. Большинство мест являются фоном, поэтому дисбаланс положительных/отрицательных примеров критичен. Focal loss + +\[ +\operatorname{FL}(p_t)=-(1-p_t)^\gamma\log p_t +\] + +уменьшает вклад лёгких уверенных примеров. + +## Non-Maximum Suppression + +Модель часто выдаёт несколько boxes одного объекта. NMS: + +1. берёт box с максимальным score; +2. удаляет boxes того же класса с IoU выше порога; +3. повторяет. + +Низкий порог может удалить близкие разные объекты; высокий оставляет дубликаты. NMS — постобработка, обычно недифференцируемая. Soft-NMS снижает score вместо жёсткого удаления. + +## Метрики detection + +Предсказание — true positive, если совпало с ещё не использованной истинной разметкой нужного класса при IoU выше порога. Затем строится precision-recall curve. **AP** — площадь/интерполяция под этой кривой, **mAP** — среднее по классам и иногда IoU thresholds. Запись `mAP@0.5` и COCO `mAP@[.5:.95]` — разные числа. + +## Semantic segmentation + +Fully convolutional model выдаёт логиты +\(\mathbf{Z}\in\mathbb{R}^{N\times C\times H\times W}\). Encoder уменьшает разрешение и извлекает семантику; decoder восстанавливает карту. U-Net передаёт high-resolution признаки encoder в decoder через skip connections (обычно конкатенацию), что помогает границам. + +Pixelwise cross-entropy: + +\[ +L_{\mathrm{CE}}=-\frac1{|\Omega|} +\sum_{p\in\Omega}\log +\frac{e^{Z_{y_p,p}}}{\sum_c e^{Z_{c,p}}}. +\] + +Для бинарной маски Dice: + +\[ +\operatorname{Dice}= +\frac{2\sum_p \hat y_py_p+\varepsilon} +{\sum_p\hat y_p+\sum_py_p+\varepsilon}, +\qquad L_{\mathrm{Dice}}=1-\operatorname{Dice}. +\] + +Для бинарных множеств \(\operatorname{Dice}=2\operatorname{IoU}/(1+\operatorname{IoU})\). Soft Dice использует вероятности и полезен при дисбалансе, но calibration и обработка пустых масок требуют внимания. + +## NumPy: IoU boxes и масок + +```python +import numpy as np + +def box_iou(a, b): + left, top = max(a[0], b[0]), max(a[1], b[1]) + right, bottom = min(a[2], b[2]), min(a[3], b[3]) + intersection = max(0.0, right - left) * max(0.0, bottom - top) + area_a = max(0.0, a[2] - a[0]) * max(0.0, a[3] - a[1]) + area_b = max(0.0, b[2] - b[0]) * max(0.0, b[3] - b[1]) + return intersection / (area_a + area_b - intersection + 1e-12) + +truth = np.array([[1, 1, 0], [0, 1, 0]], dtype=bool) +pred = np.array([[1, 0, 0], [0, 1, 1]], dtype=bool) +intersection = np.logical_and(truth, pred).sum() +union = np.logical_or(truth, pred).sum() + +print(round(box_iou((0, 0, 4, 4), (2, 1, 5, 5)), 3)) +print("mask IoU:", intersection / union) +``` + +## PyTorch: segmentation shapes + +```python +import torch +from torch import nn + +torch.manual_seed(0) +model = nn.Sequential( + nn.Conv2d(3, 16, 3, padding=1), + nn.ReLU(), + nn.Conv2d(16, 4, 1), +) +x = torch.randn(2, 3, 64, 64) +target = torch.randint(0, 4, (2, 64, 64)) +logits = model(x) +loss = nn.CrossEntropyLoss()(logits, target) +loss.backward() +print(logits.shape, float(loss)) +``` + +Цель не имеет оси каналов и содержит индексы классов `long`. Для бинарного случая часто используют один логит, `BCEWithLogitsLoss` и цель float той же формы. + +## Данные и ошибки + +- Resize boxes масштабирует координаты; crop сдвигает и обрезает их. +- Маски классов трансформируют nearest-neighbor, иначе появляются несуществующие классы. +- В detection важно анализировать missed small objects, localization error, class confusion и false positives. +- В segmentation высокая pixel accuracy может скрыть полный провал редкого класса. +- Разбиение по кадрам одного видео создаёт leakage; делить нужно по сценам/роликам/пациентам. + +## Типичные ошибки и ограничения + +- Softmax применяется до `CrossEntropyLoss`. +- NMS выполняется между разными классами без необходимости. +- AP сравнивают при разных IoU protocols. +- Box после augmentation выходит за изображение или получает отрицательную площадь. +- В маске bilinear interpolation создаёт дробные id классов. +- Detection score принимают за вероятность без проверки calibration. + +## Краткое резюме + +- Detection предсказывает экземпляры и boxes; segmentation — пиксельные области. +- IoU связывает обучение, matching, NMS и оценку, но пороги имеют разный смысл. +- One-stage/two-stage и semantic/instance — разные компромиссы. +- Метрики и преобразования разметки должны точно соответствовать постановке. + +## Связанные материалы + +- Предыдущая глава: [`04_architecture_selection.md`](04_architecture_selection.md). +- База: [`01_convolutions_and_cnn.md`](01_convolutions_and_cnn.md). +- Термины: [`../glossary.md`](../glossary.md). diff --git a/modules/11_audio_speech_music/README.md b/modules/11_audio_speech_music/README.md new file mode 100644 index 0000000..de17988 --- /dev/null +++ b/modules/11_audio_speech_music/README.md @@ -0,0 +1,46 @@ +# Модуль 11. Звук, речь и музыка + +Плановый срок: **недели 24–26**. + +## Предварительные знания + +Массивы и визуализация в Python, базовая тригонометрия, нейросети и +backpropagation из [модуля 09](../09_neural_networks/README.md). + +## Цели + +- понимать waveform, sample rate, частоту, амплитуду и спектр +- строить STFT, mel-spectrogram и признаки для речи/музыки +- транскрибировать запись и оценивать качество через WER/CER +- находить tempo, beats и onsets для генерации черновика карты osu! + +## Уроки + +1. [Дискретный звук: sampling, aliasing, channels и PCM](notes/01_discrete_audio.md). +2. [Fourier transform, STFT, mel scale и spectrogram](notes/02_spectral_analysis.md). +3. [Распознавание речи: CTC, encoder-decoder и Whisper](notes/03_speech_recognition.md). +4. [Музыкальные события: BPM, beats, onsets и tempogram](notes/04_music_events.md). +5. [Формат `.osu`, timing points и hit objects](notes/05_osu_mapping.md). + +Термины модуля: [глоссарий](glossary.md). + +Предобученная модель речи уже видела много часов аудио и выучила связь спектральных +паттернов с фонемами и словами. Поэтому для обычной транскрибации сначала используют +inference готовой модели; fine-tuning нужен при особом языке, шуме, акценте или +профессиональной терминологии и требует размеченных пар «аудио → текст». + +Автоматическая карта osu! в этом курсе — **черновик**, а не обещание качественного +маппинга одной кнопкой. Детектор предлагает ритмические позиции, а человек проверяет +структуру композиции, играбельность и художественные решения. + +## Задания + +- [`assignments/01_audio_features.md`](assignments/01_audio_features.md) — Спектр, tempo и события +- [`assignments/02_transcription.md`](assignments/02_transcription.md) — Транскрибация речи +- [`assignments/03_osu_beatmap.md`](assignments/03_osu_beatmap.md) — Черновик карты osu! из музыки + +## Завершение модуля + +Модуль завершён, когда вы можете объяснить переход waveform → spectrogram → +признаки/модель, измерить ошибки транскрибации и получить валидный `.osu`-файл, +ритмически связанный с выбранной композицией. diff --git a/modules/11_audio_speech_music/assignments/01_audio_features.md b/modules/11_audio_speech_music/assignments/01_audio_features.md new file mode 100644 index 0000000..8a651d7 --- /dev/null +++ b/modules/11_audio_speech_music/assignments/01_audio_features.md @@ -0,0 +1,34 @@ +# Спектр, tempo и музыкальные события + +## Теория перед выполнением + +Прочитайте [дискретный звук](../notes/01_discrete_audio.md), +[STFT и mel-признаки](../notes/02_spectral_analysis.md) и +[музыкальные события](../notes/04_music_events.md). + +## Цель + +Научиться переходить от массива отсчётов к спектральному представлению и находить +кандидаты ритмических событий. + +## Что сделать + +1. Сгенерировать синусоиды нескольких частот и импульсный ритм с известным BPM. +2. Построить waveform, FFT, STFT и mel-spectrogram. +3. Изменить sample rate, размер окна и hop length; объяснить компромисс между + временным и частотным разрешением. +4. На синтетическом и реальном треке вычислить onset envelope, tempo и beat times. +5. Сравнить предсказанные события с известной синтетической разметкой. +6. Добавить тесты преобразований времени: samples ↔ frames ↔ seconds. + +## Результат + +Ноутбук с визуализациями, модуль извлечения признаков и JSON/CSV со временем +обнаруженных beats/onsets. + +## Критерии приёмки + +- На синтетическом примере BPM и события восстанавливаются с оговорённой погрешностью. +- На графиках подписаны время, частота и единицы. +- Объяснено, почему максимум спектра не равен tempo и чем beat отличается от onset. +- Для реального трека использована музыка, на которую у вас есть право анализа. diff --git a/modules/11_audio_speech_music/assignments/02_transcription.md b/modules/11_audio_speech_music/assignments/02_transcription.md new file mode 100644 index 0000000..92d5dfe --- /dev/null +++ b/modules/11_audio_speech_music/assignments/02_transcription.md @@ -0,0 +1,39 @@ +# Транскрибация речи + +## Теория перед выполнением + +Прочитайте [STFT и log-mel](../notes/02_spectral_analysis.md) и +[CTC, encoder-decoder и оценку ASR](../notes/03_speech_recognition.md). + +## Цель + +Построить pipeline «аудиофайл → нормализованный текст» на предобученной модели и +понять, когда нужен fine-tuning. + +## Что сделать + +1. Подготовить 20–30 коротких записей с эталонными текстами: собственную речь либо + открытый датасет из `DATASETS.md`. +2. Привести аудио к ожидаемым sample rate/channels без потери исходников. +3. Запустить компактную предобученную ASR-модель, например Whisper. +4. Реализовать разбиение длинной записи на chunks и объединение результата. +5. Посчитать WER и CER до и после нормализации регистра/пунктуации. +6. Исследовать ошибки на шуме, числах, именах и профессиональных терминах. +7. Сформулировать, помогут ли prompt, словарь, более крупная модель или fine-tuning. + +## Результат + +CLI `transcribe