diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md new file mode 100644 index 000000000..5b2d9ce71 --- /dev/null +++ b/.claude/CLAUDE.md @@ -0,0 +1,129 @@ +# Правила работы над проектом + +Эти правила определяют, как Claude сотрудничает с пользователем над форком +курса `ai-engineering-from-scratch`. Хранятся в git, чтобы синхронизироваться +между устройствами. Активная ветка — `dev`, все остальные ветки мержатся в неё. + +## Язык + +- Основной язык диалога — русский. + +## Структура урока курса + +Каждый урок курса лежит в `phases///` и содержит готовые +папки от автора курса: + +- `code/` — эталонный код упражнений (принадлежит upstream-репозиторию курса). +- `docs/` — `en.md` (оригинал) + `ru.md` (наш русский конспект). +- `outputs/` — артефакты выполнения (если есть). +- `quiz.json` — вопросы к уроку. + +**Рабочий код, который Pavel пишет руками во время прохождения, кладётся в +подпапку `my/` в корне урока:** `phases///my/`. + +**Почему отдельная папка, а не в `code/`:** `code/` принадлежит апстриму курса. +Если в будущем понадобится подтянуть обновления курса через `git pull` от форка, +свои файлы в `code/` создадут конфликты по именам. `my/` — независимая +территория Pavel'а, апстрим её не трогает. + +**Применяется к:** урокам курса в `phases/`. Не относится к общей `scratch/` — +там продолжают жить не-курсовые черновики (refresher-планы, общие эксперименты). +Старые `scratch/lesson-NN/` (созданные на уроках фазы 0 до 2026-05-23) не +переносим — это исторический след. + +## Выполнение команд + +### Правило 1: Все команды и установки выполняет пользователь + +Пользователь сам запускает все команды и устанавливает зависимости. Claude +выдаёт команду и объясняет, что она делает и зачем — но не выполняет её. + +**Применяется к:** +- установке пакетов (`pip install`, `apt install`, `npm install`, `winget install`, и т.п.); +- настройке окружения (создание venv, переменных окружения, конфигов вне репо); +- запуску скриптов, тренировок, сервисов, контейнеров; +- любой команде с побочными эффектами на систему вне репо; +- git-операциям с удалёнными ветками (push, pull, fetch, force-push). + +**Не применяется к:** read-only инспекции состояния репо (чтение файлов, +`git status`, `git log`, `ls`, `grep`) — Claude делает это сам, чтобы понять контекст. + +**Формат выдачи команды:** +1. Сама команда — в кодовом блоке. +2. Одна фраза: что она делает. +3. Где запускать: `PowerShell` / `Cursor bash` / `WSL` / `внутри репо` и т.п. +4. Если команд несколько — нумерованный список, по одной команде на пункт. + +## Окружение и доступ к файлам + +Сессия Claude запускается в **Claude Desktop на Windows**. Claude Desktop при +создании локальной сессии требует выбрать папку, и UNC-путь к WSL в его +диалоге не работает. Поэтому держим **два клона** одного и того же репо: + +- **Windows-клон** — `C:\Users\\Desktop\ai-engineering-from-scratch`. + Точка привязки сессии. Рабочая директория Claude по умолчанию. + **Только для чтения и структурной справки**, в нём ничего не править. +- **WSL-клон** — `/home//ai-engineering-from-scratch` в WSL Ubuntu. + **Канонический клон**. Здесь пользователь реально работает, отсюда идут + все коммиты и пуши. + +Имя WSL-пользователя и дистрибутива зависят от машины — не хардкодить. + +Полная процедура настройки на новой машине: см. [`setup.md`](setup.md). + +### Протокол работы с двумя клонами + +- **По умолчанию** (общие вопросы, чтение файлов, инспекция структуры) — + Windows-клон, обычные `Read`/`Grep`/`Glob` по локальным путям. Быстро, + без моста через WSL. +- **Когда пользователь говорит «в WSL» / «посмотри в WSL» или упоминает + Linux-путь** — переключаюсь на WSL-клон. +- **Когда нужно текущее состояние** (свежий `git status`, последние коммиты, + только что отредактированный файл) — иду в WSL-клон, потому что Windows- + зеркало может отставать. +- **Любые правки файлов — только в WSL-клон** через UNC. Никогда не редактирую + Windows-клон, чтобы он не становился «второй версией правды». +- **Команды с побочными эффектами** — по Правилу 1, пользователь сам в + WSL-терминале Cursor. + +### Как Claude обращается к WSL-клону + +| Операция | Способ | +|---|---| +| Прочитать / отредактировать / создать файл | UNC-путь `\\wsl$\\home\\ai-engineering-from-scratch\…` через `Read` / `Edit` / `Write` | +| Read-only shell-инспекция (`git status`, `ls`, `grep`, `cat`) | `wsl bash -c "cd ~/ai-engineering-from-scratch && <команда>"` | +| Команды с побочными эффектами (commit, push, install, скрипты) | По Правилу 1 — выдать команду, не запускать | + +### Синхронизация Windows-клона + +После пуша из WSL Windows-клон отстаёт от `origin/dev`. Когда расхождение +заметное (новые файлы, переименования, изменения в `.claude/`), пользователь +обновляет зеркало: + +``` +git -C "$env:USERPROFILE\Desktop\ai-engineering-from-scratch" pull +``` +- **Где:** PowerShell. (Эквивалент в Cursor bash: + `git -C ~/Desktop/ai-engineering-from-scratch pull`.) + +### Подводные камни + +- `wsl --cd ` из Git Bash на Windows **не работает**: Git Bash + транслирует Linux-путь в Windows-путь, и `wsl` получает мусор. Использовать + форму `wsl bash -c "cd ~/ && "` с `cd` внутри quoted-строки. +- Имя дистрибутива не всегда `Ubuntu` (бывает `Ubuntu-24.04`, `Debian` и т.п.). +- При записи через UNC line endings сохраняются как есть. Файлы репо должны + оставаться с LF. + +### При старте на незнакомой машине + +В первой же команде получить параметры WSL: + +``` +wsl -l -q # имя дистрибутива (первая строка = default) +wsl bash -c "whoami" # имя WSL-пользователя +wsl bash -c "ls -d ~/ai-engineering-from-scratch" # склонирован ли репо +``` + +Из этого собрать UNC `\\wsl$\\home\\ai-engineering-from-scratch\…` +и работать. Если репо не склонирован — идти в [`setup.md`](setup.md). diff --git a/.claude/setup.md b/.claude/setup.md new file mode 100644 index 000000000..83f4e8759 --- /dev/null +++ b/.claude/setup.md @@ -0,0 +1,181 @@ +# Памятка: настройка проекта на новой Windows-машине + +Эти шаги мы прошли один раз вместе. Этот документ — чтобы быстро повторить +их на любой Windows-машине, с которой будем работать. Делается всё тобой +в терминале и GUI; Claude только подсказывает и проверяет. + +## Prerequisites (должно быть на машине) + +- Windows 10 build 19041+ или Windows 11 (для WSL2). +- **Cursor** (Windows GUI) — где живёт редактор. +- **Claude Desktop** (Windows GUI) — где запускается Claude. +- **Git for Windows** (Git Bash) — нужен на старте, до настройки WSL. + +## Шаги + +### 1. WSL2 + Ubuntu + +Проверить, что есть: + +```powershell +wsl --status +``` +- **Где:** PowerShell. + +Если WSL не установлен или нет дистрибутива: + +```powershell +wsl --install -d Ubuntu-24.04 +``` +- **Где:** PowerShell **от администратора**. +- После установки потребуется ребут. +- На первом запуске Ubuntu попросит создать UNIX-пользователя и пароль + (строчные латинские буквы, без пробелов). + +### 2. Базовая проверка Ubuntu + +```bash +whoami && pwd && uname -a && ls -la ~ +``` +- **Где:** Ubuntu shell. +- В выводе `uname -a` должно быть `microsoft-standard-WSL2`. +- Запомнить имя пользователя — оно понадобится для UNC-путей. + +### 3. Git identity в WSL + +Проверить: + +```bash +cat ~/.gitconfig +``` + +Если пусто или нужна правка — настроить: + +```bash +git config --global user.name "PavelEgorov-ru" +git config --global user.email "<твой email на GitHub>" +git config --global init.defaultBranch main +git config --global pull.rebase false +``` +- **Где:** Ubuntu shell. + +### 4. SSH-ключ для GitHub + +Проверить, есть ли уже: + +```bash +ls -la ~/.ssh/ +``` +- Ищем `id_ed25519` (или `id_rsa`) и одноимённый `.pub`. + +Если ключа нет — сгенерировать: + +```bash +ssh-keygen -t ed25519 -C "<твой email>" +``` +- **Где:** Ubuntu shell. +- На все вопросы — Enter (без passphrase) или задать passphrase по желанию. + +Показать публичный ключ: + +```bash +cat ~/.ssh/id_ed25519.pub +``` +- Скопировать вывод и добавить на https://github.com/settings/ssh/new + (вручную через браузер; Claude в этом не помогает). + +Проверить, что GitHub принимает: + +```bash +ssh -T git@github.com +``` +- На вопрос про fingerprint при первом подключении ответить `yes`. +- **Успех:** `Hi PavelEgorov-ru! You've successfully authenticated...`. + +### 5. Склонировать форк и переключиться на `dev` + +```bash +cd ~ && git clone git@github.com:PavelEgorov-ru/ai-engineering-from-scratch.git +``` +- **Где:** Ubuntu shell. Клон ляжет в `~/ai-engineering-from-scratch` + (Linux-FS, не `/mnt/c/...`). + +```bash +cd ~/ai-engineering-from-scratch && git checkout dev +``` +- Создаст локальную ветку `dev`, отслеживающую `origin/dev`. +- На `dev` лежат правила (`.claude/CLAUDE.md`) и эта памятка. + +### 6. Cursor + WSL + +В Cursor (Windows GUI): + +1. **Поставить расширение WSL.** `Ctrl+Shift+X` → искать `WSL` → ставить + расширение от **Anysphere** с описанием «Open any folder in the + Windows Subsystem for Linux». +2. **Открыть пустое окно.** `Ctrl+Shift+N` (`File → New Window`) — чтобы + не цеплять текущий workspace. +3. **Подключиться к WSL.** В новом окне: `Ctrl+Shift+P` → + `WSL: Connect to WSL` → Enter. В левом нижнем углу должно появиться + `WSL: Ubuntu`. +4. **Открыть папку.** `File → Open Folder` → ввести + `/home/<твой WSL-юзер>/ai-engineering-from-scratch` → Enter → + `Yes, I trust the authors`. + +### 7. Финальная проверка WSL-клона + +В терминале нового Cursor-окна (он откроется в репо): + +```bash +pwd && git status && git log --oneline -2 +``` +- Ожидаем: путь `/home//ai-engineering-from-scratch`, ветка `dev`, + `nothing to commit, working tree clean`, верхний коммит — + `chore(claude): add project working rules` (или новее). + +### 8. Параллельный Windows-клон (anchor для Claude Desktop) + +Claude Desktop при создании локальной сессии требует выбрать папку и +не работает с UNC-путями к WSL в этом диалоге. Поэтому держим **второй +клон того же репо в Windows** — только как точку привязки сессии. + +В PowerShell (Windows): + +```powershell +cd $env:USERPROFILE\Desktop +git clone git@github.com:PavelEgorov-ru/ai-engineering-from-scratch.git +cd ai-engineering-from-scratch +git checkout dev +``` +- **Где:** PowerShell или Cursor bash на Windows. +- Использует SSH-ключ Windows (или HTTPS). Если ключ Windows ещё не привязан + к GitHub — добавить аналогично шагу 4, но на стороне Windows. + +В этом клоне **не работаем**. Все правки идут в WSL-клон. После пушей из +WSL — синхронизация: + +```powershell +git -C "$env:USERPROFILE\Desktop\ai-engineering-from-scratch" pull +``` +- **Где:** PowerShell. + +## Что после этого делает Claude + +- В Claude Desktop при создании сессии указывается **Windows-клон** + (`C:\Users\\Desktop\ai-engineering-from-scratch`) — UNC-путь к WSL + в диалоге выбрать нельзя. +- Claude по умолчанию работает с Windows-клоном для чтения и инспекции + структуры. +- Когда нужны актуальное состояние или правки, Claude переключается на + **WSL-клон**: + - имя дистрибутива и пользователя получает через + ``` + wsl -l -q + wsl bash -c "whoami" + ``` + - читает/правит файлы по UNC `\\wsl$\\home\\ai-engineering-from-scratch\…`; + - shell-команды — `wsl bash -c "cd ~/ai-engineering-from-scratch && …"`. +- Команды с побочными эффектами Claude выдаёт пользователю — тот запускает + их в WSL-терминале Cursor. + +Подробности — в `.claude/CLAUDE.md`, раздел «Окружение и доступ к файлам». diff --git a/.gitignore b/.gitignore index fe1a3f20d..329052bdb 100644 --- a/.gitignore +++ b/.gitignore @@ -38,9 +38,11 @@ Thumbs.db *.swo *~ .idea/ -.vscode/ *.log +.vscode/.history/ +.vscode/*.code-workspace + data/ models/ checkpoints/ diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 000000000..ca3280653 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,12 @@ +{ + "[python]": { + "editor.defaultFormatter": "charliermarsh.ruff", + "editor.formatOnSave": true, + "editor.codeActionsOnSave": { + "source.fixAll.ruff": "explicit", + "source.organizeImports.ruff": "explicit" + } + }, + "python.defaultInterpreterPath": "${workspaceFolder}/.venv/bin/python", + "ruff.nativeServer": "on" +} \ No newline at end of file diff --git a/astronvim-setup.md b/astronvim-setup.md new file mode 100644 index 000000000..00a051ccc --- /dev/null +++ b/astronvim-setup.md @@ -0,0 +1,414 @@ +# Установка AstroNvim на Windows + WSL2 + +Полный пошаговый гайд для развёртывания AstroNvim как основного редактора +на машине с Windows и WSL2. Проверен на Windows 10 Pro + Ubuntu 24.04 LTS. + +## Кому это нужно + +Если ты: +- хочешь редактор легче, чем VS Code / Cursor; +- работаешь в основном внутри WSL (как мы по [setup.md](setup.md)); +- готов потратить ~30 минут на разовую установку; + +— этот гайд для тебя. + +## Архитектура решения + +**Neovim ставится внутрь WSL, а не на Windows.** Это не очевидно, поэтому объясняю. + +Если поставить nvim на Windows, а открывать им файлы из WSL через UNC-путь +`\\wsl$\Ubuntu\...`: +- LSP, форматтеры, линтеры — это Windows-бинарники. Они не видят твой + WSL-venv и пакеты, поставленные через `apt`/`pip` в Linux. +- Файлы читаются через 9P-мост. Treesitter и file-watchers заметно тормозят. +- `:terminal` внутри nvim откроет PowerShell, а не bash. + +Поэтому правильная схема: + +``` +Windows Terminal → вкладка WSL Ubuntu → cd ~/repo → nvim +``` + +Windows-сторона нужна только для двух вещей: терминала (Windows Terminal) +и Nerd Font (шрифт рендерит Windows). Всё остальное — Linux. + +## Предусловия + +- Windows 10 версии 1903+ или Windows 11. +- WSL2 + установленный Linux-дистрибутив (рекомендую Ubuntu 24.04 LTS). +- Доступ к интернету. +- ~3 ГБ свободного места. + +Проверка WSL — в PowerShell: + +```powershell +wsl -l -v +``` + +Должен увидеть свой дистрибутив со статусом `Running` и `VERSION 2`. +Если `VERSION 1` — обнови до WSL2 (`wsl --set-version 2`). + +--- + +## Часть 1: Windows-сторона + +### 1.1. Установить Windows Terminal + +Проверка: + +```powershell +wt --version +``` + +Если не распознано — установить через winget (в PowerShell): + +```powershell +winget install --id Microsoft.WindowsTerminal -e +``` + +После установки **закрой и открой PowerShell заново**, чтобы PATH подхватил. + +### 1.2. Установить Nerd Font (JetBrainsMono) + +Без Nerd Font все иконки в AstroNvim будут квадратиками или `?`. + +В PowerShell (обычный, без админа) — одна команда: + +```powershell +$tmp = "$env:TEMP\nerd-fonts-jbm"; New-Item -ItemType Directory -Force -Path $tmp | Out-Null; $zip = "$tmp\JetBrainsMono.zip"; Invoke-WebRequest -Uri "https://github.com/ryanoasis/nerd-fonts/releases/latest/download/JetBrainsMono.zip" -OutFile $zip; Expand-Archive -Path $zip -DestinationPath $tmp -Force; $fontDir = "$env:LOCALAPPDATA\Microsoft\Windows\Fonts"; New-Item -ItemType Directory -Force -Path $fontDir | Out-Null; Get-ChildItem -Path $tmp -Filter "*.ttf" | ForEach-Object { $dest = Join-Path $fontDir $_.Name; Copy-Item $_.FullName -Destination $dest -Force; $name = $_.BaseName + " (TrueType)"; New-ItemProperty -Path "HKCU:\Software\Microsoft\Windows NT\CurrentVersion\Fonts" -Name $name -Value $dest -PropertyType String -Force | Out-Null }; Write-Host "Installed: $((Get-ChildItem $fontDir -Filter '*.ttf').Count) TTF files" +``` + +Что делает: +1. Скачивает `JetBrainsMono.zip` с GitHub Releases Nerd Fonts. +2. Распаковывает во временную папку. +3. Копирует все `.ttf` в `%LOCALAPPDATA%\Microsoft\Windows\Fonts` (per-user + папка шрифтов на Windows 10+, не требует админа). +4. Регистрирует каждый шрифт в реестре `HKCU` — без этого Windows их не + увидит. + +Должен вывести что-то вроде `Installed: 24 TTF files`. + +Проверка: + +```powershell +[System.Reflection.Assembly]::LoadWithPartialName("System.Drawing"); (New-Object System.Drawing.Text.InstalledFontCollection).Families | Where-Object { $_.Name -like "*JetBrains*" } | Select-Object Name +``` + +Должен показать длинный список: +- `JetBrainsMono NF` — Nerd Font (символы шире одной ячейки). +- `JetBrainsMono NFM` — **Nerd Font Mono** (иконки в одну ячейку — это нужно для терминала). +- `JetBrainsMono NFP` — Proportional (для текста, не для терминала). +- `NL` варианты — без лигатур. + +Нам нужен **`JetBrainsMono NFM`**. + +--- + +## Часть 2: WSL-сторона + +Открой Windows Terminal → стрелка `∨` рядом с `+` → выбери свой WSL-дистрибутив. +Все команды ниже — в bash внутри WSL. + +### 2.1. Обновить систему + +```bash +sudo apt update && sudo apt upgrade -y +``` + +### 2.2. Базовые пакеты + +```bash +sudo apt install -y ripgrep fd-find unzip curl git build-essential python3-pip python3-venv xclip +``` + +Что и зачем: +- `ripgrep`, `fd-find` — быстрый поиск (Telescope их использует). +- `unzip`, `curl` — для скачивания и распаковки. +- `git` — должен быть, но на всякий случай. +- `build-essential` — gcc/make для компиляции Treesitter-парсеров. +- `python3-pip`, `python3-venv` — для Python-LSP и Mason. +- `xclip` — запасной канал буфера обмена (основной будет через win32yank). + +### 2.3. Node.js LTS + +Через NodeSource — в репах Ubuntu обычно старая версия. + +```bash +curl -fsSL https://deb.nodesource.com/setup_lts.x | sudo -E bash - +sudo apt install -y nodejs +``` + +Нужен для нескольких LSP-серверов (TypeScript, JSON, YAML и т.д.). + +### 2.4. Neovim 0.10+ из официального tarball + +В репах Ubuntu 24.04 nvim 0.9.5, а AstroNvim v4 требует ≥ 0.10. Ставим +prebuilt бинарь: + +```bash +curl -LO https://github.com/neovim/neovim/releases/latest/download/nvim-linux-x86_64.tar.gz +sudo rm -rf /opt/nvim && sudo tar -C /opt -xzf nvim-linux-x86_64.tar.gz +echo 'export PATH="$PATH:/opt/nvim-linux-x86_64/bin"' >> ~/.bashrc && source ~/.bashrc +rm nvim-linux-x86_64.tar.gz +``` + +Проверка: `nvim --version | head -n 1` → должно быть `NVIM v0.10.x` или +выше. + +### 2.5. lazygit + +В стандартных репах Ubuntu 24.04 lazygit нет. Ставим из релиза: + +```bash +LAZYGIT_VERSION=$(curl -s "https://api.github.com/repos/jesseduffield/lazygit/releases/latest" | grep -Po '"tag_name": "v\K[^"]*') && curl -Lo lazygit.tar.gz "https://github.com/jesseduffield/lazygit/releases/latest/download/lazygit_${LAZYGIT_VERSION}_Linux_x86_64.tar.gz" && tar xf lazygit.tar.gz lazygit && sudo install lazygit /usr/local/bin && rm lazygit lazygit.tar.gz +``` + +AstroNvim вызывает lazygit по `gg` или через `tl`. + +### 2.6. win32yank — мост буфера обмена WSL ↔ Windows + +Без этого `y` в nvim не попадает в Windows-буфер. + +```bash +curl -sLo /tmp/win32yank.zip https://github.com/equalsraf/win32yank/releases/latest/download/win32yank-x64.zip && unzip -p /tmp/win32yank.zip win32yank.exe > /tmp/win32yank.exe && chmod +x /tmp/win32yank.exe && sudo mv /tmp/win32yank.exe /usr/local/bin/ && rm /tmp/win32yank.zip +``` + +`/usr/local/bin/win32yank.exe` — WSL умеет запускать `.exe` напрямую, +Neovim автоматически подхватывает его как clipboard provider. + +### 2.7. Симлинк для `fd` + +В Ubuntu пакет `fd-find` ставит бинарь как `fdfind` (конфликт с другим +пакетом). Telescope ждёт имя `fd`: + +```bash +mkdir -p ~/.local/bin && ln -sf $(which fdfind) ~/.local/bin/fd +``` + +`~/.local/bin` уже в PATH по умолчанию в Ubuntu 24.04. + +### 2.8. Проверка зависимостей + +```bash +nvim --version | head -n 1 +lazygit --version +node --version +rg --version | head -n 1 +fd --version +which win32yank.exe +``` + +Должны видеть версии всех инструментов и путь к win32yank. + +--- + +## Часть 3: AstroNvim + +### 3.1. Бэкап старого конфига (если есть) + +AstroNvim не любит, когда в стандартных папках уже что-то лежит: + +```bash +[ -d ~/.config/nvim ] && mv ~/.config/nvim ~/.config/nvim.bak +[ -d ~/.local/share/nvim ] && mv ~/.local/share/nvim ~/.local/share/nvim.bak +[ -d ~/.local/state/nvim ] && mv ~/.local/state/nvim ~/.local/state/nvim.bak +[ -d ~/.cache/nvim ] && mv ~/.cache/nvim ~/.cache/nvim.bak +``` + +### 3.2. Клонировать AstroNvim v4 template + +```bash +git clone --depth 1 https://github.com/AstroNvim/template ~/.config/nvim +rm -rf ~/.config/nvim/.git +``` + +Удаление `.git` нужно, чтобы потом этот конфиг можно было класть в свой +репозиторий, если захочешь синхронизировать между машинами. + +### 3.3. Первый запуск + +```bash +nvim +``` + +При первом запуске `lazy.nvim` сам скачает ~30–50 плагинов. Жди 1–3 минуты, +пока бегут зелёные/жёлтые строки. Когда закончит — нажми Enter, потом +`:q` чтобы выйти. + +Запусти второй раз — теперь Mason доустановит LSP-серверы и форматтеры. +Это ещё 1–2 минуты. + +--- + +## Часть 4: настройка Windows Terminal + +### 4.1. Шрифт (для всех профилей) + +1. Открой Windows Terminal. +2. Открой настройки: стрелка `∨` рядом с `+` → **Settings** (или `Ctrl + ,` + при активном окне WT — важно, чтобы фокус был именно на WT). +3. Слева под **Профили** → **По умолчанию** → **Оформление**. +4. **Начертание шрифта** → `JetBrainsMono NFM` (если нет в списке — поставь + галочку **«Показать все шрифты»**). +5. **Размер шрифта** → 12 (или на вкус). + +### 4.2. Default profile = Ubuntu + +Слева → **Запуск** → **Профиль по умолчанию** → выбери свой Linux-дистрибутив. +Теперь Windows Terminal при запуске сразу будет открывать WSL. + +### 4.3. Сохрани + +Кнопка **Сохранить** внизу справа. + +### 4.4. Перезапусти + +**Полностью закрой** все окна Windows Terminal (важно — без этого новый +шрифт не применится), открой заново. + +### Альтернатива: через JSON + +Если GUI не работает — можно редактировать конфиг напрямую: + +```powershell +nvim "$env:LOCALAPPDATA\Packages\Microsoft.WindowsTerminal_8wekyb3d8bbwe\LocalState\settings.json" +``` + +В блоке `"profiles": { "defaults": { ... } }` добавь: + +```json +"defaults": +{ + "font": + { + "face": "JetBrainsMono NFM", + "size": 12 + } +}, +``` + +--- + +## Часть 5: проверка работоспособности + +### 5.1. Иконки + +В Ubuntu-вкладке Windows Terminal: + +```bash +cd ~/<твой репозиторий> && nvim +``` + +В дереве файлов слева должны быть видны: иконки папок, питон-логотип у +`.py`, докер-иконка у `Dockerfile`, иконка markdown у `.md`. Если вместо +них квадратики или `?` — шрифт не применился. Проверь шаг 4. + +Можно проверить рендер шрифта отдельно — выйди из nvim и в bash: + +```bash +python3 -c "print('  ')" +``` + +Должен напечатать три иконки: файл, папка, питон. Если квадратики — шрифт +не подключён в Windows Terminal. + +### 5.2. Буфер обмена + +Внутри nvim: +1. Открой любой файл. +2. Поставь курсор на слово (стрелочками или `h j k l`). +3. Нажми `yiw` (yank inner word). +4. Переключись в любое Windows-приложение (Блокнот, браузер). +5. `Ctrl + V` — должно вставиться скопированное слово. + +Если работает — мост через `win32yank.exe` собран правильно. + +### 5.3. Healthcheck + +Внутри nvim: + +``` +:checkhealth astrocore +:checkhealth mason +``` + +`astrocore` должен быть полностью OK. У `mason` обязательно зелёные галочки +у `python`, `node`, `npm`, `pip`, `python venv`, `git`, `cc/gcc`, `unzip`, +`curl`. Жёлтые WARNING про Go, Ruby, PHP, Java, Julia — игнорировать, +если не пишешь на них. + +`:checkhealth` без аргументов покажет всё. Игнорируй ошибки про: +- `luarocks` — большинству плагинов не нужен; +- `kitty/wezterm/ghostty`, `magick/convert` — рендер картинок в терминале, + Windows Terminal такого не умеет в принципе; +- `tectonic/pdflatex`, `mmdc` — LaTeX/Mermaid превью. + +--- + +## Базовые шорткаты для старта + +Leader-клавиша в AstroNvim — **пробел**. Когда нажмёшь `` в normal +mode и подождёшь, снизу появится cheatsheet. + +| Шорткат | Что | +|---|---| +| `` | Главное меню AstroNvim | +| `ff` | **F**ind **f**ile — поиск файла по имени (Telescope) | +| `fw` | **F**ind **w**ord — grep по содержимому (live grep) | +| `fb` | **F**ind **b**uffer — переключение между открытыми файлами | +| `e` | Открыть/закрыть дерево файлов слева (Neo-tree) | +| `F7` | Плавающий терминал | +| `tf` | То же | +| `th` | Терминал горизонтальным сплитом | +| `tv` | Терминал вертикальным сплитом | +| `tl` | Lazygit внутри nvim | +| `gg` | То же | +| `w` | **W**rite — сохранить | +| `q` | **Q**uit — выйти | +| `c` | **C**lose buffer — закрыть текущий файл | +| `gd` | **G**oto **d**efinition (LSP) | +| `K` | Hover-документация (LSP) | +| `la` | LSP **a**ction — рефакторинги | + +Выйти из режима терминала в normal mode: `Ctrl+\` потом `Ctrl+n`. + +--- + +## Возможные грабли + +**`Ctrl + ,` не открывает настройки Windows Terminal.** +Фокус должен быть на окне Windows Terminal. Если активно VS Code, IDE или +другое приложение с тем же шорткатом — открывается оно. Кликни мышкой по +окну WT и повтори. + +**Иконки `?` после установки шрифта.** +Windows Terminal не перечитал шрифты. Полностью закрой все его окна и +открой заново. Если не помогло — проверь, что выбран именно `JetBrainsMono +NFM` (с буквой M), а не `JetBrainsMono NF`. + +**Long-running команды в WSL медленно стартуют.** +Если кажется, что `apt`, `curl` тормозят — это нормально на первом запуске +WSL после загрузки Windows. Дай WSL прогреться 5–10 секунд. + +**`echo -e "\uXXXX"` в bash не печатает Unicode.** +Это особенность bash. Используй `printf '\uXXXX\n'` или +`python3 -c "print('\uXXXX')"`. + +**Шрифт в выпадающем списке Windows Terminal отсутствует.** +Поставь галочку **«Показать все шрифты»** — по умолчанию WT фильтрует только +моноширинные, но Nerd Font Mono (`NFM`) иногда не помечается как моно в +метаданных шрифта. + +--- + +## Что не входит в этот гайд + +- **LSP под конкретный язык** (Python pyright + ruff, TypeScript и т.д.) — + настраивается в `~/.config/nvim/lua/plugins/` отдельно. +- **Свой конфиг в git** — после установки шаблона можно вынести + `~/.config/nvim` в свой репозиторий и синхронизировать между машинами. +- **Темы и UI tweaks** — AstroNvim из коробки достаточно красив; темы + меняются через `ft` (find theme). + +Если что-то из этого понадобится — добавится отдельным разделом или +отдельным файлом. diff --git a/phases/00-setup-and-tooling/01-dev-environment/docs/ru.md b/phases/00-setup-and-tooling/01-dev-environment/docs/ru.md new file mode 100644 index 000000000..5c2111bc6 --- /dev/null +++ b/phases/00-setup-and-tooling/01-dev-environment/docs/ru.md @@ -0,0 +1,220 @@ +# Урок 01 — Dev Environment + +> Параллельный конспект на русском к `en.md`. Дополняется по ходу будущих обсуждений. + +## Зачем этот урок вообще существует + +Ты собираешься 200+ уроков писать код на Python, TypeScript, Rust и Julia. Если +окружение собрано криво — каждый урок превращается не в обучение, а в борьбу +с импортами, версиями и драйверами. Этот урок ставит фундамент один раз и +правильно, чтобы дальше ты не возвращался к нему никогда. + +**Главная идея:** окружение для AI — это **четыре слоя**, и собирают их строго +снизу вверх. Каждый верхний слой не работает без нижнего. + +``` +4. AI/ML библиотеки ← PyTorch, JAX, transformers +3. Языковые рантаймы ← Python 3.11+, Node 20+, Rust, Julia +2. Менеджеры пакетов ← uv, pnpm, cargo, juliaup +1. Системный фундамент ← OS, shell, git, редактор, драйверы GPU +``` + +Если уронить второй слой (например, `pip` вместо `uv`) — пакеты будут ставиться +не туда, не в тот Python, и весь четвёртый слой посыпется. + +## Блок 1 — Системный фундамент + +Это нулевой уровень: компилятор C, git, curl, wget. Без них даже менеджер +пакетов не установишь. + +- **macOS:** `xcode-select --install` ставит инструменты разработчика Apple + (включая компилятор `clang`), потом `brew install git curl wget`. +- **Ubuntu/Debian:** `sudo apt install build-essential git curl wget`. + `build-essential` — это метапакет, который тянет `gcc`, `g++`, `make` и + стандартные заголовки. +- **Windows:** **только через WSL2.** Нативный Windows-Python для AI-разработки + не используют — половина библиотек ставится через C-расширения, заточенные + под Linux. `wsl --install -d Ubuntu-24.04` — стандартный путь. + +**Подсветка для нас:** мы как раз так и работаем — у тебя WSL-клон репо +(`/home/pavel/ai-engineering-from-scratch`) — он канонический, и Windows-клон +на Desktop — только зеркало для запуска сессии Claude Desktop. Все правки — +в WSL. + +## Блок 2 — Python через uv + +`uv` — это новый менеджер пакетов и интерпретаторов от Astral (тех же, что +сделали `ruff`). Написан на Rust, в 10–100 раз быстрее `pip`, и одновременно +заменяет `pyenv` + `virtualenv` + `pip`. + +Ключевые команды: + +```bash +curl -LsSf https://astral.sh/uv/install.sh | sh # ставим сам uv +uv python install 3.12 # ставим конкретный Python +uv venv # создаём venv в .venv/ +source .venv/bin/activate # активируем (Linux/macOS) +.venv\Scripts\activate # активируем (Windows) +uv pip install numpy matplotlib jupyter # ставим пакеты в активный venv +``` + +**Что важно понимать:** + +- `uv python install 3.12` не трогает системный Python — ставит свой, + изолированный, в `~/.local/share/uv/python/`. Это правильно: системный Python + трогать опасно, на нём держится сам дистрибутив. +- `uv venv` без аргументов создаёт `.venv/` рядом с текущей директорией. + Это и есть «виртуальное окружение» — отдельная папка с собственным Python + и собственным `site-packages/`. Каждому проекту — свой venv. +- После активации `which python` (Linux/macOS) или `Get-Command python` + (PowerShell) должны указывать **внутрь** `.venv/`. Если указывают наружу — + активация не сработала, и `pip install` поставит пакеты не туда. + +**Python-подсветка для нас:** позже мы увидим, что в Jupyter та же история — +ядро (`kernel`) тоже привязано к конкретному Python через `sys.executable`, +и проверка «в каком я Python’е» — `python -c "import sys; print(sys.executable)"`. + +## Блок 3 — Node.js через pnpm + +Нужен с фазы 13 (агенты, MCP-серверы, веб-приложения). До этого можешь не ставить. + +```bash +curl -fsSL https://fnm.vercel.app/install | bash # fnm — менеджер версий Node +fnm install 22 +fnm use 22 +npm install -g pnpm # pnpm как замена npm +``` + +`fnm` нужен по той же причине, что `uv python` — изолировать версии Node от +системы. `pnpm` ставит зависимости через симлинки и общий cache → быстрее и +без дубликатов, как у `npm`. + +## Блок 4 — Rust + +Нужен с фаз 12, 15–17 (низкоуровневый inference, системы). Тоже можно отложить. + +```bash +curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh +``` + +`rustup` — официальный установщик, ставит `rustc` (компилятор) и `cargo` +(менеджер пакетов + сборки). Это единственный нормальный способ ставить Rust — +не из системного пакетного менеджера. + +## Блок 5 — Julia (опционально) + +Нужен только в Фазе 1 (математика). Можно поставить, когда дойдём, не сейчас. + +```bash +curl -fsSL https://install.julialang.org | sh +``` + +## Блок 6 — GPU + +Если у тебя NVIDIA-карта: + +```bash +nvidia-smi # должна показать карту, версию драйвера и CUDA +``` + +Дальше — PyTorch с CUDA: + +```bash +uv pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu124 +``` + +Флаг `--index-url` критичен: без него `pip` поставит CPU-версию PyTorch с +PyPI. С ним — версию, собранную под CUDA 12.4, с поддержкой GPU. + +Проверка в Python: + +```python +import torch +print(torch.cuda.is_available()) # True если GPU видна +print(torch.cuda.get_device_name(0)) # имя карты +``` + +**Если GPU нет** — не страшно. Большинство уроков фаз 1–10 работают на CPU. +Тяжёлые тренировки потом — через Google Colab (фаза 0, урок 03). + +## Блок 7 — Финальная верификация + +В уроке лежит готовый скрипт: + +```bash +python phases/00-setup-and-tooling/01-dev-environment/code/verify.py +``` + +Он проверяет, что Python ≥ 3.11, что есть numpy, что есть PyTorch, что (если +есть GPU) CUDA доступна. Если что-то падает — починить именно ту часть. + +## Use It — что где используется + +| Язык | Фазы | Менеджер пакетов | +|---|---|---| +| Python | 1–12 (ML, DL, NLP, CV, Audio, LLM) | uv | +| TypeScript | 13–17 (Tools, Agents, Swarms, Infra) | pnpm | +| Rust | 12, 15–17 (производительность) | cargo | +| Julia | 1 (математика) | Pkg | + +## Упражнения + +### Упражнение 1 — прогнать `verify.py` и починить, что упало + +**Маршрут (без готового кода):** + +1. Перейти в WSL: `cd ~/ai-engineering-from-scratch`. +2. Активировать venv (`source .venv/bin/activate`). +3. Запустить `python phases/00-setup-and-tooling/01-dev-environment/code/verify.py`. +4. Если падает — читать **последнее** сообщение об ошибке (Python показывает + стек снизу вверх — самое полезное обычно в конце). +5. Чинить по одной проблеме, потом снова запускать. + +**Python-подсветка:** скрипт `verify.py` использует `try/except ImportError`, +чтобы отличить «пакет не установлен» от «пакет упал при импорте». Полезный +паттерн на будущее — мы его повторим в lab-скриптах. + +### Упражнение 2 — venv для курса + PyTorch + +**Маршрут:** + +1. В корне репо: `uv venv` (создаст `.venv/`). +2. Активировать. +3. `uv pip install torch` — поставит CPU-версию (если без GPU). + Если есть NVIDIA-карта — поставить версию с CUDA (см. Блок 6). +4. Открыть Python REPL: `python`. +5. Внутри: `import torch; torch.tensor([1.0, 2.0, 3.0]) ** 2` — должен + вернуть `tensor([1., 4., 9.])`. + +### Упражнение 3 — «hello world» на четырёх языках + +Цель — убедиться, что все четыре toolchain’а работают: + +```bash +python -c "print('Hello, Python')" +node -e "console.log('Hello, Node')" +echo 'fn main() { println!("Hello, Rust"); }' > hello.rs && rustc hello.rs && ./hello +julia -e 'println("Hello, Julia")' +``` + +Если что-то из этого не запускается — соответствующий toolchain не установлен +или не в `$PATH`. + +## Ключевые термины + +| Термин | Что говорят | Что это на самом деле | +|---|---|---| +| venv | «виртуальное окружение» | Папка с собственным Python и своими пакетами, изолированная от системы | +| toolchain | «набор инструментов» | Компилятор + менеджер пакетов + стандартная библиотека одного языка | +| CUDA | «GPU-программирование» | Платформа NVIDIA, через которую Python-код запускается на видеокарте | +| WSL2 | «Linux на Windows» | Полноценное Linux-ядро в виртуалке, интегрированной с Windows | + +## Что важно вынести из урока + +1. **Слои ставятся снизу вверх**, и каждый верхний слой ломается, если нижний + собран криво. +2. **Системный Python не трогаем.** Свои Python’ы ставим через `uv` или `pyenv`. +3. **Каждому проекту — свой venv.** Глобально пакеты не ставим, разве что сам + `uv`, `pnpm`, `fnm`. +4. **`sys.executable` — твой главный диагностический инструмент.** Когда что-то + странное с импортами — первым делом проверь, какой Python запущен. diff --git a/phases/00-setup-and-tooling/02-git-and-collaboration/docs/ru.md b/phases/00-setup-and-tooling/02-git-and-collaboration/docs/ru.md new file mode 100644 index 000000000..969a0843c --- /dev/null +++ b/phases/00-setup-and-tooling/02-git-and-collaboration/docs/ru.md @@ -0,0 +1,202 @@ +# Урок 02 — Git & Collaboration + +> Параллельный конспект на русском к `en.md`. Дополняется по ходу будущих обсуждений. + +## Зачем этот урок + +Ты будешь писать сотни файлов через 20 фаз курса. Без системы версионирования: +- невозможно вернуться к работающей версии, если эксперимент сломал код; +- невозможно держать «безопасную» ветку и параллельную «экспериментальную»; +- невозможно показать кому-то прогресс или попросить помочь. + +Git — это инструмент версионирования. GitHub — место, где этот версионированный +код лежит. Урок намеренно минимальный: ровно те команды, которые нужны для курса. +Без `rebase`, `cherry-pick`, submodules и прочей экзотики — это пригодится сильно +позже, если вообще. + +## Концепция — три уровня хранения + +``` +Working Directory ←→ Staging Area ←→ Local Repo ←→ Remote (GitHub) + (твои файлы) (что выбрано (фиксированные (копия на сервере) + к коммиту) снимки) + | git add | git commit | git push | + | | | | + ← git pull (= git fetch + merge на текущую ветку) +``` + +**Главная мысль:** коммит — это не «сохранение файла», а **снимок всего +проекта** на конкретный момент. У снимка есть hash (SHA), автор, дата, сообщение +и ссылка на предыдущий снимок. Из этих ссылок выстраивается история — DAG +(directed acyclic graph), а не просто список. + +Ветка — это **движущийся указатель на коммит**, не «копия кода». Когда ты +коммитишь на ветке, указатель перемещается на новый коммит. Это объясняет, +почему создать ветку — операция мгновенная: git просто пишет новый файл-указатель +с SHA текущего коммита. + +## Блок 1 — Настройка идентичности + +```bash +git config --global user.name "Your Name" +git config --global user.email "you@example.com" +``` + +Эти данные записываются в каждый коммит. `--global` пишет в `~/.gitconfig`, и +действует для всех репозиториев пользователя. Без `--global` — только для +текущего репо (записывается в `.git/config`). + +**Подсветка для нас:** у тебя уже это настроено (`git user: PavelEgorov-ru`). +В нашем репо ты коммитишь как `PavelEgorov-ru` — это видно в `git log`. + +## Блок 2 — Ежедневный цикл + +```bash +git status # что изменилось +git add file.py # добавить в staging +git commit -m "Add perceptron implementation" # зафиксировать снимок +git push origin main # отправить на GitHub +``` + +**Тонкости:** + +- `git status` — read-only, ничего не меняет. Запускать столько, сколько нужно, + даже до каждого `add`. +- `git add` — кладёт **снимок состояния файла в данный момент** в staging. + Если ты сначала `git add file.py`, потом меняешь файл, и сразу делаешь + `git commit` — закоммитится **первая** версия. Чтобы взять последнюю, надо + `git add file.py` ещё раз. +- `git commit -m "..."` — фиксирует то, что в staging. Сообщение — это история; + через год ты или твой коллега увидите только его, не сам код. Писать осмысленно. +- `git push` — отправляет коммиты, которых нет на сервере. До первого пуша + локальный репо вообще не связан с GitHub. + +**Подсветка для нашего рабочего стиля:** по правилу №1 из `.claude/CLAUDE.md` +все git-команды с побочными эффектами (`commit`, `push`, `pull`) запускаешь ты +сам в Cursor bash. Я только выдаю команду. + +## Блок 3 — Ветки для экспериментов + +```bash +git checkout -b experiment/new-optimizer # создать и переключиться +# ... меняем код, коммитим в эту ветку ... +git checkout main # вернуться на main +git merge experiment/new-optimizer # влить эксперимент в main +``` + +**Зачем:** +- `main` всегда работает. На неё можно положиться. +- Эксперименты живут в отдельных ветках, и если что-то ломается — это + локально, не на main. +- Когда эксперимент удался — `merge`, и он становится частью main. +- Если не удался — просто бросить ветку или удалить её (`git branch -D`). + +**Подсветка для нашего репо:** +- У нас активная ветка — **`dev`**, не `main`. Все правки идут в `dev`, и в + дальнейшем эта стратегия зафиксирована в `.claude/CLAUDE.md`. +- Соответственно, в наших командах всегда `git push origin dev`, не `main`. + +## Блок 4 — Работа с курсовым репо + +В уроке предлагается: + +```bash +git clone https://github.com/rohitg00/ai-engineering-from-scratch.git +cd ai-engineering-from-scratch +git checkout -b my-progress +git push origin my-progress +``` + +**Как у нас на самом деле:** мы работаем со своим форком этого репо +(`https://github.com//ai-engineering-from-scratch`), и активная +ветка не `my-progress`, а `dev`. Кроме того, у нас **два клона** одного и того +же репо — Windows-зеркало (только чтение) и WSL-канонический (работа). Подробно +это описано в `.claude/CLAUDE.md` и `setup.md`. + +## Use It — что реально нужно + +| Команда | Когда | +|---|---| +| `git clone` | Один раз — забрать репо на машину | +| `git add` + `git commit` | После каждой осмысленной правки | +| `git push` | После одного-нескольких коммитов — забэкапить | +| `git checkout -b` | Когда хочешь попробовать что-то рискованное | +| `git log --oneline` | Посмотреть, что было сделано — кратко | + +## Упражнения + +### Упражнение 1 — клонировать репо, создать ветку, закоммитить и запушить + +**Маршрут:** + +1. `git clone ` — забираем репо. +2. `cd ai-engineering-from-scratch`. +3. `git checkout -b my-progress` — новая ветка от текущей. +4. Создать любой файл (например, `notes/test.md` с «Hello git»). +5. `git add notes/test.md`. +6. `git commit -m "Add test note"`. +7. `git push origin my-progress`. +8. Открыть GitHub в браузере — должна появиться ветка `my-progress` с этим + коммитом. + +**Что важно заметить:** при первом `push origin my-progress` git попросит +авторизацию (через GitHub credential helper, SSH-ключ или Personal Access Token). +Если попросит — это нормальная разовая настройка. + +### Упражнение 2 — `.gitignore` для checkpoint’ов + +**Маршрут:** + +1. В корне репо создать файл `.gitignore`. +2. Положить туда: + ``` + *.pt + *.pth + *.safetensors + ``` +3. `git add .gitignore`. +4. `git commit -m "Ignore model checkpoint files"`. +5. Проверить: создать пустой файл `test.pt`, потом `git status` — он не должен + появляться в untracked. + +**Зачем:** чекпоинты моделей бывают сотнями мегабайт / гигабайтами. Один +случайный коммит — и репо раздувается. `.gitignore` исключает их **до того, как +они попадают в git**. + +**Подсветка:** `.gitignore` действует только на ещё **не отслеживаемые** файлы. +Если файл уже был добавлен в git, `.gitignore` его не уберёт — надо +`git rm --cached `. Это частая ловушка. + +### Упражнение 3 — прочитать `git log --oneline` + +**Маршрут:** + +1. `git log --oneline | head -30` — последние 30 коммитов в одну строку каждый. +2. Прочитать, как менялся курс — что добавлялось, в каком порядке. +3. Полезные флаги: `--graph` (рисует ASCII-граф веток), `--all` (показывает + все ветки, а не только текущую). + +**Подсветка для нас:** именно так в наших правилах живёт «маркер прогресса» — +коммиты `закончил урок NN фазы 0`. Я их читаю, чтобы понять, где ты в курсе. + +## Ключевые термины + +| Термин | Что говорят | Что это на самом деле | +|---|---|---| +| Commit | «сохранение» | Снимок всего проекта в моменте + ссылка на предыдущий снимок | +| Branch | «копия» | Указатель на коммит, который двигается, когда ты коммитишь | +| Merge | «соединить код» | Взять коммиты одной ветки и применить к другой | +| Remote | «облако» | Копия репо на сервере (GitHub, GitLab) | +| HEAD | (редко произносят) | Указатель на текущий коммит, на котором ты находишься | +| Staging area | «индекс» | Промежуточный слой между working directory и репозиторием | + +## Что важно вынести из урока + +1. **Коммит — это снимок проекта**, не «сохранение файла». Это объясняет всё + остальное в git. +2. **Ветка — указатель**, а не копия. Создание ветки бесплатно. +3. **`git status` — твой друг.** Запускай его до и после `add`/`commit`. +4. **Большие бинарники не коммитим.** Чекпоинты, датасеты, видео — в `.gitignore` + или в LFS (большая тема для дальнейших уроков). +5. **Сообщения коммитов пишем для будущего себя.** Через полгода `fix` ничего + не значит, `Fix learning rate scheduler off-by-one in warmup` — значит. diff --git a/phases/00-setup-and-tooling/03-gpu-setup-and-cloud/docs/ru.md b/phases/00-setup-and-tooling/03-gpu-setup-and-cloud/docs/ru.md new file mode 100644 index 000000000..c1cca9a6c --- /dev/null +++ b/phases/00-setup-and-tooling/03-gpu-setup-and-cloud/docs/ru.md @@ -0,0 +1,258 @@ +# Урок 03 — GPU Setup & Cloud + +> Параллельный конспект на русском к `en.md`. Дополняется по ходу будущих обсуждений. + +## Зачем этот урок + +В фазах 1–3 (математика, базовый ML, основы DL) большая часть кода нормально +бежит на CPU. Но как только начинается: +- обучение CNN (фаза 4 — Computer Vision), +- обучение трансформеров (фаза 5+ — NLP), +- fine-tuning LLM (фаза 9+), + +— без GPU обучение идёт в десятки–сотни раз медленнее. Тренировка, которая на +GPU занимает 10 минут, на CPU превращается в 8 часов. + +**Базовая интуиция, почему GPU быстрее для нейросетей:** обучение — это в +основном умножение больших матриц. CPU имеет десяток мощных ядер, GPU — тысячи +простых, заточенных параллельно делать одну и ту же арифметику над разными +данными. Матричное умножение — идеальная задача для такой архитектуры. + +## Три варианта, как получить GPU + +``` +1. Локальная NVIDIA GPU + Стоимость: $0 (если уже есть) + Настройка: CUDA + cuDNN + PyTorch с CUDA + Когда брать: Регулярная работа, большие датасеты, низкая латентность + +2. Google Colab (free tier) + Стоимость: $0 + Настройка: отсутствует — браузер и Google-аккаунт + Когда брать: Эксперименты, обучение, когда нет своей карты + Ограничения: 90 минут idle → отключение; сессии сбрасываются + +3. Cloud GPU (Lambda, RunPod, Vast.ai) + Стоимость: $0.20–2.00/час за GPU + Настройка: SSH + установка библиотек + Когда брать: Серьёзная тренировка, большие модели, надолго +``` + +В курсе по умолчанию строим всё так, чтобы это работало и на CPU тоже. Где +нужна GPU — это явно проговаривается и обычно даются ссылки на Colab. + +## Блок 1 — Локальная NVIDIA GPU + +**Проверка, что карта вообще видна системе:** + +```bash +nvidia-smi +``` + +Если команда отрабатывает и показывает таблицу с моделью карты, версией +драйвера, температурой, объёмом VRAM — драйверы стоят, карта видна. +Если `nvidia-smi: command not found` — драйвера NVIDIA не установлены. + +**Подсветка для нас (WSL):** в WSL2 на Windows 11 `nvidia-smi` работает, +если в Windows стоит свежий драйвер NVIDIA и установлен пакет WSL CUDA. Это +не совсем тот же путь, что на чистом Linux, но снаружи поведение идентичное. + +**Установка PyTorch с CUDA:** + +```bash +uv pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu124 +``` + +Флаг `--index-url` направляет `pip` на специальный PyTorch index, где лежат +билды под конкретные версии CUDA. Без него поставится CPU-версия с PyPI, и +`torch.cuda.is_available()` будет `False`, даже если GPU физически есть. + +**Проверка в Python:** + +```python +import torch + +print(f"CUDA available: {torch.cuda.is_available()}") +print(f"CUDA version: {torch.version.cuda}") +if torch.cuda.is_available(): + print(f"GPU: {torch.cuda.get_device_name(0)}") + print(f"Memory: {torch.cuda.get_device_properties(0).total_mem / 1e9:.1f} GB") +``` + +**Python-подсветка:** `1e9` — это запись числа в научной нотации, +`1 × 10⁹ = 1 000 000 000` (миллиард). Делим байты на миллиард, получаем +гигабайты (точнее — гигабайты по основанию 10, не гибибайты GiB; разница +~7%, для прикидки не существенна). + +## Блок 2 — Google Colab + +Colab — это «Jupyter notebook в облаке от Google» с **бесплатным GPU** на +free tier. + +**Как включить GPU:** +1. Открыть [colab.research.google.com](https://colab.research.google.com). +2. New notebook (или загрузить свой `.ipynb`). +3. **Runtime → Change runtime type → T4 GPU** → Save. +4. В первой ячейке: `!nvidia-smi` — должна вернуть таблицу с T4. + +T4 — это бюджетный datacenter-GPU NVIDIA с ~15 GB VRAM. На нём учатся CNN, +запускаются 7B-параметровые LLM в 4-bit квантизации и куча всего остального. +На free tier лимиты по времени мягкие, но есть; если упёрся — Colab Pro +($10/мес) даёт больше. + +**Подсветка:** в Colab уже предустановлены `numpy`, `pandas`, `matplotlib`, +`torch`, `tensorflow`, `sklearn`. Не надо ставить их заново. Свои библиотеки — +через `!pip install`. + +## Блок 3 — Cloud GPU + +Lambda Labs, RunPod, Vast.ai — это маркетплейсы машин с GPU, которые ты +арендуешь почасово. + +```bash +ssh user@your-gpu-instance +pip install torch torchvision torchaudio +python -c "import torch; print(torch.cuda.get_device_name(0))" +``` + +Когда стоит идти сюда: +- Учишь модель часами/днями (Colab отключит). +- Нужен GPU помощнее T4 (A100, H100). +- Нужно много памяти (40+ GB VRAM). + +Для курса это пока далеко — мы дойдём, если будем делать большой fine-tuning. + +## Блок 4 — Нет GPU? Не страшно + +Идиоматичный паттерн PyTorch — определять устройство один раз и потом всё на +него «переносить»: + +```python +device = torch.device("cuda" if torch.cuda.is_available() else "cpu") +print(f"Using: {device}") + +model = MyModel().to(device) +x = x.to(device) +``` + +Этот код работает одинаково: на машине с GPU — на GPU, без неё — на CPU. +Это **рекомендуемый стиль** для всего, что мы будем писать. + +## Бенчмарк CPU vs GPU + +В уроке предлагается умножить две матрицы 5000×5000 на CPU, потом на GPU, и +сравнить. + +```python +import torch +import time + +size = 5000 +a_cpu = torch.randn(size, size) +b_cpu = torch.randn(size, size) + +start = time.time() +c_cpu = a_cpu @ b_cpu +cpu_time = time.time() - start +print(f"CPU: {cpu_time:.3f}s") + +if torch.cuda.is_available(): + a_gpu = a_cpu.to("cuda") + b_gpu = b_cpu.to("cuda") + + torch.cuda.synchronize() + start = time.time() + c_gpu = a_gpu @ b_gpu + torch.cuda.synchronize() + gpu_time = time.time() - start + + print(f"GPU: {gpu_time:.3f}s") + print(f"Speedup: {cpu_time / gpu_time:.0f}x") +``` + +**Важная Python/PyTorch-подсветка:** `torch.cuda.synchronize()` — это не +декорация. GPU-операции в PyTorch **асинхронные**: вызов `a_gpu @ b_gpu` +**отправляет** работу на GPU и возвращается, не дожидаясь результата. +Если измерять без `synchronize()`, замер покажет время «отправки запроса», +а не «реальное выполнение». Это классическая ловушка при бенчмарках GPU. + +`@` — это оператор матричного умножения в Python (с 3.5). Эквивалент +`torch.matmul(a, b)` или `np.dot(a, b)` для матриц. + +## Прикидка размера модели по VRAM + +**Правило большого пальца:** +- В fp32 (float32) один параметр = **4 байта**. +- В fp16 (float16) один параметр = **2 байта**. +- В int8 — **1 байт**, в int4 — **0.5 байта**. + +Формула: **`размер модели в VRAM ≈ N параметров × байт на параметр`** (только +для весов; при обучении нужно дополнительно место для градиентов, состояния +оптимизатора и активаций — это часто 3–4× больше). + +Примеры: +- T4 с 15 GB VRAM → ~7B-параметровая модель в fp16 (14 GB весов) **в режиме + инференса**. На обучение — не влезет, нужна квантизация / LoRA. +- A100 80 GB → 70B-параметровая модель в int8. + +## Упражнения + +### Упражнение 1 — прогнать бенчмарк локально + +**Маршрут:** + +1. Создать файл `scratch/phase-00-lesson-03/benchmark.py` или сделать в Jupyter + (как удобнее). +2. Скопировать код бенчмарка. +3. Запустить. Записать CPU time, GPU time, speedup. +4. Если GPU нет — выполнить только CPU-часть, а GPU-часть прогнать в Colab. + +**Что важно заметить:** на размерах 5000×5000 GPU обычно даёт 30–100× ускорение. +На маленьких матрицах (например, 50×50) GPU может оказаться **медленнее** CPU — +накладные расходы на копирование данных и запуск ядра становятся больше, чем +само вычисление. Это объяснит, почему «GPU быстрее» — не всегда правда. + +### Упражнение 2 — то же самое в Colab + +Если у тебя нет локальной NVIDIA: + +1. Открыть Colab, включить T4 GPU. +2. Скопировать тот же код в ячейку. +3. Прогнать, посмотреть speedup. + +### Упражнение 3 — оценить размер модели + +**Маршрут:** + +1. Запустить: + ```python + import torch + props = torch.cuda.get_device_properties(0) + print(f"VRAM: {props.total_memory / 1e9:.1f} GB") + ``` +2. По правилу 2 байта/параметр в fp16 посчитать, сколько параметров влезет. +3. Сравнить с известными моделями: Llama 7B = 14 GB в fp16, Mistral 7B + аналогично, Llama 70B = 140 GB. + +## Ключевые термины + +| Термин | Что говорят | Что это на самом деле | +|---|---|---| +| CUDA | «GPU-программирование» | Платформа NVIDIA — позволяет запускать код на видеокарте | +| VRAM | «память GPU» | Видеопамять, отдельная от системной RAM. Ограничивает размер модели | +| fp16 | «половинная точность» | 16-битное число с плавающей точкой. В 2 раза меньше памяти, чем fp32 | +| Tensor Core | «быстрое железо для матриц» | Специальные блоки на GPU для матричного умножения, 4–8× быстрее обычных | +| `synchronize()` | (часто забывают) | Команда «дождись окончания всех GPU-операций» — нужна для корректных замеров | + +## Что важно вынести из урока + +1. **CPU и GPU — разные миры.** Перенос данных между ними (`.to("cuda")`) стоит + времени, и при маленьких задачах GPU может оказаться медленнее. +2. **GPU-операции асинхронные.** Без `torch.cuda.synchronize()` нельзя ничего + нормально замерить. +3. **Размер модели в VRAM ≈ N параметров × байт.** Это калькулятор, который + надо держать в голове, прежде чем грузить модель. +4. **Нет GPU — нормально.** Идиома `device = torch.device("cuda" if ... else "cpu")` + делает код переносимым. +5. **Бесплатный Colab T4** — рабочий вариант для всех учебных задач курса + до тяжёлой тренировки. diff --git a/phases/00-setup-and-tooling/04-apis-and-keys/code/first_api_call_openrouter.py b/phases/00-setup-and-tooling/04-apis-and-keys/code/first_api_call_openrouter.py new file mode 100644 index 000000000..d27feaeb4 --- /dev/null +++ b/phases/00-setup-and-tooling/04-apis-and-keys/code/first_api_call_openrouter.py @@ -0,0 +1,76 @@ +"""First API call via OpenRouter (OpenAI-compatible). + +Drop-in replacement for first_api_call.py when Anthropic API is not available. +Uses the official OpenAI SDK pointed at OpenRouter's base URL. +""" + +import json +import os +import urllib.request + +from dotenv import load_dotenv +from openai import OpenAI + +MODEL = "meta-llama/llama-3.2-3b-instruct" +URL = "https://openrouter.ai/api/v1/chat/completions" +PROMPT = "Что такое нейронная сеть в одном предложении?" + + +def call_with_sdk() -> None: + load_dotenv() + + client = OpenAI( + base_url="https://openrouter.ai/api/v1", + api_key=os.environ["OPENROUTER_API_KEY"], + ) + + response = client.chat.completions.create( + model=MODEL, + max_tokens=256, + messages=[{"role": "user", "content": PROMPT}], + ) + + print("=== SDK response ===") + print(response.choices[0].message.content) + print() + print( + f"Tokens used: {response.usage.prompt_tokens} in, " + f"{response.usage.completion_tokens} out" + ) + print(f"Model returned: {response.model}") + + +def call_raw_http() -> None: + load_dotenv() + api_key = os.environ["OPENROUTER_API_KEY"] + + headers = { + "Content-Type": "application/json", + "Authorization": f"Bearer {api_key}", + } + body = json.dumps( + { + "model": MODEL, + "max_tokens": 256, + "messages": [{"role": "user", "content": PROMPT}], + } + ).encode() + + req = urllib.request.Request(URL, data=body, headers=headers, method="POST") + with urllib.request.urlopen(req) as resp: + result = json.loads(resp.read()) + + print("=== Raw HTTP response ===") + print(result["choices"][0]["message"]["content"]) + print() + print( + f"Tokens used: {result['usage']['prompt_tokens']} in, " + f"{result['usage']['completion_tokens']} out" + ) + print(f"Model returned: {result['model']}") + + +if __name__ == "__main__": + call_with_sdk() + print() + call_raw_http() diff --git a/phases/00-setup-and-tooling/04-apis-and-keys/docs/ru.md b/phases/00-setup-and-tooling/04-apis-and-keys/docs/ru.md new file mode 100644 index 000000000..55be68c44 --- /dev/null +++ b/phases/00-setup-and-tooling/04-apis-and-keys/docs/ru.md @@ -0,0 +1,281 @@ +# Урок 04 — APIs & Keys + +> Параллельный конспект на русском к `en.md`. Дополняется по ходу будущих обсуждений. + +## Зачем этот урок + +С фазы 11 (LLM, агенты) ты будешь звонить в LLM-API: Anthropic (Claude), +OpenAI, Google, потом и в провайдеров через прокси (например, OpenRouter). +В фазах 13–16 этих вызовов будет много, в циклах, с tool use, со стримингом. +Урок ставит базу: **как устроен любой HTTP API, как безопасно хранить ключи, +как сделать первый вызов и как читать ошибки**. + +**Главная мысль:** все LLM API устроены одинаково — HTTP-запрос с JSON-телом +и ключом аутентификации в заголовке, в ответ JSON. Конкретные имена полей и +URL у каждого провайдера свои, но скелет одинаковый. + +## Концепция — что такое API-вызов + +``` +[Твой код] --(HTTP-запрос с API-ключом)--> [Сервер провайдера] +[Твой код] <--(HTTP-ответ в JSON)--------- [Сервер провайдера] +``` + +В каждом вызове четыре вещи: +1. **Endpoint** — URL (например, `https://api.anthropic.com/v1/messages`). +2. **API key** — длинная строка, идентифицирует и авторизует тебя. + Передаётся в заголовке (`x-api-key`, `Authorization: Bearer ...`). +3. **Request body** — JSON, что именно ты хочешь (модель, сообщения, лимиты). +4. **Response body** — JSON, что вернули (текст, причина остановки, usage). + +## Блок 1 — Безопасное хранение ключей + +**Главное правило:** API-ключи никогда не пишутся в коде. Никогда. + +Почему это критично: +- Код попадает в git → ключ попадает в публичный репо → кто-то находит его + через GitHub Search и качает с твоего аккаунта запросов на тысячи долларов + за ночь. Это **реальный сценарий**, который происходит регулярно. +- Даже в приватном репо ключ в коде — это утечка при любом сливе, скриншоте, + коде-ревью. + +**Правильно — через environment variables (переменные окружения):** + +В Linux/macOS bash/zsh: +```bash +export ANTHROPIC_API_KEY="sk-ant-..." +export OPENAI_API_KEY="sk-..." +``` + +В Windows PowerShell: +```powershell +$env:ANTHROPIC_API_KEY = "sk-ant-..." +``` + +Но `export` живёт только в текущем шелле. Чтобы переменные сохранялись — +кладут их в `~/.bashrc` / `~/.zshrc` (на Linux/macOS) или в постоянное окружение +Windows. + +**Альтернатива — файл `.env`** в корне проекта: + +``` +ANTHROPIC_API_KEY=sk-ant-... +OPENAI_API_KEY=sk-... +``` + +И **обязательно**: `.gitignore` должен содержать `.env`, чтобы файл не попал +в git. Это `Lesson 02`-уровень дисциплины, но забыть про это легко. + +Загружать `.env` в Python принято через библиотеку `python-dotenv`: + +```python +from dotenv import load_dotenv +load_dotenv() # читает .env и кладёт в os.environ +``` + +После этого `os.environ["ANTHROPIC_API_KEY"]` уже работает. + +**Python-подсветка:** SDK от Anthropic (и OpenAI) сами ищут ключ в +`os.environ["ANTHROPIC_API_KEY"]` (и аналогично `OPENAI_API_KEY`), поэтому +конструктор без аргументов: +```python +client = anthropic.Anthropic() +``` +— достаточно. Не надо передавать ключ руками. + +## Блок 2 — Первый вызов через SDK (Python) + +```python +import anthropic + +client = anthropic.Anthropic() + +response = client.messages.create( + model="claude-sonnet-4-20250514", + max_tokens=256, + messages=[{"role": "user", "content": "What is a neural network in one sentence?"}] +) + +print(response.content[0].text) +``` + +**Разбор:** + +- `anthropic.Anthropic()` — клиент. Ищет `ANTHROPIC_API_KEY` в окружении. +- `client.messages.create(...)` — единая точка для всех вызовов чат-моделей + Claude. +- `model` — конкретная модель. Имена меняются с каждым релизом. Актуальный + список — в [docs.claude.com/en/docs/about-claude/models](https://docs.claude.com/en/docs/about-claude/models). +- `max_tokens` — **обязательный** параметр у Anthropic SDK. Это потолок длины + ответа в токенах. Без него API не вызовешь. +- `messages` — список сообщений диалога. Каждое — `{"role": "user|assistant", "content": "..."}`. +- `response.content` — список «блоков контента» (Anthropic поддерживает не + только текст, но и tool use, изображения). У простого текстового ответа + блок один — `response.content[0]`, у которого есть `.text`. + +**Python-подсветка:** объект `response` — это не словарь, а pydantic-модель. +`.content[0].text` — атрибуты, не `["content"][0]["text"]`. Это типобезопаснее. + +## Блок 3 — Первый вызов через SDK (TypeScript) + +```typescript +import Anthropic from "@anthropic-ai/sdk"; + +const client = new Anthropic(); + +const response = await client.messages.create({ + model: "claude-sonnet-4-20250514", + max_tokens: 256, + messages: [{ role: "user", content: "What is a neural network in one sentence?" }], +}); + +console.log(response.content[0].text); +``` + +Структура **идентична** Python-варианту. Это сознательное дизайн-решение +Anthropic: один и тот же API контракт, разные обёртки для разных языков. +Когда мы дойдём до агентов на TS (фазы 13+), переход будет плавным. + +## Блок 4 — Тот же вызов через raw HTTP (без SDK) + +```python +import os +import urllib.request +import json + +url = "https://api.anthropic.com/v1/messages" +headers = { + "Content-Type": "application/json", + "x-api-key": os.environ["ANTHROPIC_API_KEY"], + "anthropic-version": "2023-06-01", +} +body = json.dumps({ + "model": "claude-sonnet-4-20250514", + "max_tokens": 256, + "messages": [{"role": "user", "content": "What is a neural network in one sentence?"}], +}).encode() + +req = urllib.request.Request(url, data=body, headers=headers, method="POST") +with urllib.request.urlopen(req) as resp: + result = json.loads(resp.read()) + print(result["content"][0]["text"]) +``` + +**Зачем вообще знать raw HTTP, если есть SDK:** + +- **Отладка.** Когда SDK падает, понимание raw-запроса помогает дёрнуть тот же + запрос через `curl`/`httpie` и понять, в чём проблема. +- **Языки без SDK.** Не везде есть готовый клиент. Зная сырой протокол, можешь + работать с любого языка. +- **Custom поведение.** Прокси, кастомные заголовки, специальные таймауты — + иногда проще через сырой HTTP. + +**Разбор:** +- `x-api-key` — заголовок аутентификации именно у Anthropic. + У OpenAI — `Authorization: Bearer sk-...`. У каждого свой. +- `anthropic-version` — обязательный заголовок версии API. Anthropic сильно + следит за обратной совместимостью именно через версию в заголовке. +- `urllib.request` — встроенная стандартная библиотека Python. Без `requests`. + Менее удобно, но без зависимостей. В реальном коде обычно `requests` или + `httpx`. +- `.encode()` нужен, потому что HTTP-тело передаётся в байтах, не в строке. + +## Use It — что использовать в курсе + +| API | Когда нужен | Free tier | +|---|---|---| +| Anthropic (Claude) | Фазы 11–16 (агенты, tools) | $5 кредитов при регистрации | +| OpenAI | Фаза 11 (для сравнения) | $5 кредитов при регистрации | +| Hugging Face | Фазы 4–10 (модели, датасеты) | Полностью бесплатно | + +Не надо регистрироваться во всём сразу. Подключаешь по мере того, как урок +этого требует. + +## Упражнения + +### Упражнение 1 — получить ключ Anthropic и сделать первый вызов + +**Маршрут:** + +1. Зайти на [console.anthropic.com](https://console.anthropic.com), создать + аккаунт, получить $5 free credit. +2. **Settings → API Keys → Create Key.** Скопировать ключ **сразу** — потом + его не покажут, придётся создавать новый. +3. Установить SDK: `uv pip install anthropic`. +4. Положить ключ в `.env` в корне репо (и убедиться, что `.env` в `.gitignore`). +5. Установить `python-dotenv`: `uv pip install python-dotenv`. +6. Написать скрипт `scratch/phase-00-lesson-04/hello_claude.py`: + ```python + from dotenv import load_dotenv + import anthropic + load_dotenv() + client = anthropic.Anthropic() + resp = client.messages.create( + model="claude-sonnet-4-20250514", # актуальное имя смотри в docs + max_tokens=256, + messages=[{"role": "user", "content": "What is a neural network in one sentence?"}], + ) + print(resp.content[0].text) + ``` +7. Запустить, прочитать ответ. + +**Подсветка:** в репо уже есть `code/first_api_call.py` — там же скелет. +И **второй файл** — `code/first_api_call_openrouter.py` — вариант с +OpenRouter (прокси, который маршрутизирует к разным провайдерам с одним ключом). +Полезно как fallback, если Anthropic-ключ ещё не оформлен. + +### Упражнение 2 — тот же вопрос через raw HTTP + +**Маршрут:** + +1. Взять код из `Блок 4` ru.md. +2. Прогнать, распечатать **полный JSON-ответ** (`print(json.dumps(result, indent=2))`). +3. Сравнить с тем, как SDK его «упаковывает» (`response.content[0].text` vs. + `result["content"][0]["text"]`). + +**Что заметить:** в полном JSON есть поля, которые SDK не выводит явно — +`stop_reason` (почему модель остановилась — `end_turn`, `max_tokens`, +`tool_use`), `usage.input_tokens` / `usage.output_tokens` (счётчики токенов +для биллинга), `id`, `model`. SDK всё это даёт как `response.stop_reason`, +`response.usage.input_tokens` и т.д., но в raw-варианте это нагляднее. + +### Упражнение 3 — намеренно сломать ключ и прочитать ошибку + +**Маршрут:** + +1. В `.env` исправить `ANTHROPIC_API_KEY` на заведомо неправильный + (например, `sk-ant-WRONG`). +2. Запустить тот же скрипт. +3. Прочитать сообщение об ошибке. + +**Что увидишь (примерно):** `anthropic.AuthenticationError: ... 401 ... +authentication_error ... invalid x-api-key`. Запомни этот шаблон ошибки — +401 везде значит «авторизация не прошла», 403 — «авторизация ок, но нельзя», +429 — «rate limit», 500–599 — «упал их сервер». + +**Подсветка:** эта тренировка важна, потому что в реальных агентских циклах +LLM-вызовы регулярно валятся (rate limits, transient errors). Уметь читать +ошибку с первой попытки экономит часы отладки. + +## Ключевые термины + +| Термин | Что говорят | Что это на самом деле | +|---|---|---| +| API key | «пароль для API» | Длинная строка, идентифицирует аккаунт и авторизует запросы | +| Rate limit | «они меня троттлят» | Лимит запросов в минуту/час, чтобы не дать сжечь сервис | +| Token | «слово» (в контексте API) | Единица биллинга: входные и выходные считаются и оплачиваются отдельно | +| Streaming | «ответ в реальном времени» | Получение ответа по кусочкам, а не одним блоком в конце | +| SDK | «официальная библиотека» | Удобная обёртка над raw HTTP с типами, ретраями, обработкой ошибок | +| `.env` | «файл с секретами» | Текстовый файл `KEY=value`, который читается через `dotenv` | + +## Что важно вынести из урока + +1. **API-ключи никогда не в коде.** Всегда в `.env` или системных env-vars. + `.env` всегда в `.gitignore`. +2. **Все LLM API одинаковы по форме.** Endpoint + ключ + JSON-запрос + JSON-ответ. + Отличия — в именах полей. +3. **`max_tokens` обязателен** у Anthropic. Без него API не вызовешь. +4. **SDK для удобства, raw HTTP для отладки.** Полезно знать оба. +5. **Ошибки HTTP читать по статус-коду** (401, 403, 429, 5xx) — это первый сигнал, + что с чем делать. +6. **Free tier $5** хватит на первые сотни вызовов, на курс этого с запасом. diff --git a/phases/00-setup-and-tooling/05-jupyter-notebooks/docs/ru.md b/phases/00-setup-and-tooling/05-jupyter-notebooks/docs/ru.md new file mode 100644 index 000000000..5b5b02b23 --- /dev/null +++ b/phases/00-setup-and-tooling/05-jupyter-notebooks/docs/ru.md @@ -0,0 +1,509 @@ +# Урок 05 — Jupyter Notebooks + +> Параллельный конспект на русском к `en.md`. Включает то, что мы детально +> разобрали блок за блоком (теория — все 9 блоков), и упражнения, +> которые ты сделал в `scratch/phase-00-lesson-05/`. + +## Зачем этот урок + +Jupyter notebook — рабочая среда **всех** дальнейших фаз курса (CV, NLP, LLM, +агенты). Каждая статья, каждый туториал, каждый Kaggle-конкурс используют +notebook’и. Учить AI без них — то же самое, что делать математику без черновика. + +Но notebook’и опасны: ими пользуются для всего, в том числе для того, для чего +они **плохи**. Главная ценность этого урока — научиться отличать «когда +notebook — правильный инструмент» от «когда нужен .py-скрипт», и заранее знать +ловушки, которые позже будут стоить часов отладки. + +## Блок 1 — Модель исполнения + +Notebook = **JSON-файл `.ipynb`** + **kernel** (Python-процесс, который +исполняет код). Файл и kernel — это **разные** сущности. + +``` +[ Notebook UI (браузер / VS Code) ] + ↕ +[ Jupyter Server (HTTP, локальный сервер) ] + ↕ +[ Kernel — отдельный Python-процесс ] + ↕ + { глобальный namespace — переменные } +``` + +- **Ячейки** — независимые единицы исполнения. Между ячейками **общая память** + (namespace). Переменная, созданная в ячейке 1, видна в ячейке 5. +- **`execution_count`** — счётчик слева от ячейки (`In [3]`). Растёт каждый раз, + когда ты что-то запускаешь. **Не** обязательно сверху вниз. +- **`.ipynb`-файл** — это JSON. Можно открыть текстовым редактором и увидеть + ячейки, их `source`, их `outputs`, метаданные. Это и плюс (можно скриптом + обрабатывать), и минус (диффы в git — ужасные, base64 от картинок). +- **Kernel** живёт до тех пор, пока ты его не убил («Restart») или не закрыл + сервер. Перезапуск kernel’а **стирает всё из памяти**. + +**Подсветка для нас:** именно понимание этой модели — основа всего урока. +Особенно «исполнение не обязано идти сверху вниз» и «namespace переживает +ячейки» — это источник всех трёх классических ловушек из Блока 9. + +## Блок 2 — Интерфейсы + +Один формат, три интерфейса: + +| Интерфейс | Когда выбирать | +|---|---| +| **JupyterLab** | Полный «IDE-look»: файловый дерево, вкладки, терминал, расширения. Стандарт в AI-командах. | +| **Jupyter Notebook** | Лёгкий, одна страница = один notebook. Минимализм. | +| **VS Code + Jupyter extension** | Notebook’и прямо в редакторе кода, с git-диффами, дебагером, IntelliSense. Идеально, если уже живёшь в VS Code. | + +Все три читают и пишут **одни и те же `.ipynb`** — переключаться можно +свободно. + +**Архитектура:** + +``` +Frontend (JupyterLab/VS Code/notebook web) + ↕ WebSocket +Jupyter Server + ↕ ZeroMQ +Kernel (Python-процесс) +``` + +Frontend не выполняет код. Он только посылает «выполни эту строку» в server, +server — в kernel, kernel считает и шлёт результат назад. + +**Критичная мелочь — `sys.executable`:** + +В Python-ячейке: +```python +import sys +print(sys.executable) +``` + +Эта строка покажет **абсолютный путь к Python-интерпретатору, который сейчас +исполняет ячейки**. Это твой главный диагностический инструмент: если +`pip install pandas` отрабатывает, а в notebook’е `import pandas` падает — почти +всегда дело в том, что `pip` и kernel — это два **разных** Python. + +Связано это с понятием **kernelspec** — JSON-конфигурацией ядра, в которой +прописано, какой Python запускать. Список: +```bash +jupyter kernelspec list +``` + +## Блок 3 — Шорткаты и модальный интерфейс + +Notebook’и — **модальные** (как Vim). Два режима: + +- **Edit mode** (зелёная рамка) — редактируешь содержимое ячейки. Войти — + `Enter` или клик в ячейку. +- **Command mode** (синяя рамка) — управляешь ячейками (создать, удалить, + переключить тип). Войти — `Escape`. + +**Минимум, который надо знать наизусть** (в command mode): + +| Клавиша | Действие | +|---|---| +| `Shift+Enter` | Выполнить и перейти к следующей ячейке | +| `Ctrl+Enter` | Выполнить и остаться | +| `Alt+Enter` | Выполнить и **создать** новую ячейку под | +| `A` | Создать ячейку **выше** (above) | +| `B` | Создать ячейку **ниже** (below) | +| `DD` | Удалить ячейку (две D подряд) | +| `M` | Превратить в markdown | +| `Y` | Превратить в code | +| `Z` | Undo операции с ячейкой | +| `Ctrl+Shift+H` | Показать **все** шорткаты | + +В edit mode: + +| Клавиша | Действие | +|---|---| +| `Tab` | Autocomplete | +| `Shift+Tab` | Сигнатура функции (наводка по аргументам) | +| `Ctrl+/` | Закомментировать/раскомментировать строку | + +**`Shift+Enter`** — клавиша, которую ты будешь нажимать тысячу раз в день. +Это первое, что выучивают наизусть. + +## Блок 4 — Типы ячеек и правило последнего выражения + +Две основных: + +- **Code cell** — Python-код. Выполняется в kernel. +- **Markdown cell** — рендерится как форматированный текст. Поддерживает: + заголовки `#`, **жирный**, *курсив*, списки, таблицы, ссылки, LaTeX + (`$E = mc^2$`), картинки, Mermaid-диаграммы (в новых версиях). + +**Правило «последнего выражения»:** в code-ячейке последнее выражение +автоматически выводится — без `print()`. + +```python +import numpy as np +data = np.random.randn(1000) +data.mean(), data.std() +# → (0.0032, 0.9987) +``` + +Это **сильно отличает** notebook от обычного Python-скрипта. В скрипте та же +строка ничего бы не вывела. + +**`display()` — для нескольких объектов в одной ячейке:** + +```python +from IPython.display import display +display(df.head()) +display(df.tail()) +``` + +Без `display` сработало бы правило последнего выражения, и вывелся бы только +`df.tail()`. + +**Markdown-структура — это часть кода ноутбука:** + +Хороший notebook читается как лабораторная работа: markdown-ячейка `# Раздел`, +потом code-ячейки внутри. **Это и шаблон**, который мы зафиксировали как +паттерн для всех дальнейших упражнений. + +## Блок 5 — Magic commands + +Это **не Python**. Это команды самого IPython/Jupyter: + +- `%command` — **line magic**, работает в одной строке ячейки. +- `%%command` — **cell magic**, действует на всю ячейку. +- `!command` — выполнить как shell-команду. + +**Самые важные:** + +### Замер времени + +```python +%timeit np.random.randn(10000) +# → 45.2 us ± 1.3 us per loop (mean ± std. dev. of 7 runs, 10000 loops each) +``` + +`%timeit` запускает код **многократно**, усредняет, считает std. Подходит для +**микробенчмарков** — операций короче секунды. + +```python +%%time +model.fit(X_train, y_train, epochs=10) +# → Wall time: 2.34 s +``` + +`%%time` запускает код **один раз** и показывает время. Подходит для длинных +операций (обучение модели), где `%timeit` не имеет смысла. + +**Какой выбирать:** +- Тренировка нейросети, загрузка датасета → `%%time`. +- Векторная операция, парсинг строки, индексация массива → `%timeit`. + +### Matplotlib inline + +```python +%matplotlib inline +``` + +После этой команды каждый `plt.plot(...)`/`plt.show()` рендерится **прямо в +notebook**, не открывает отдельное окно. По умолчанию в современных Jupyter +это включено, но иногда надо явно. + +Альтернатива: `%matplotlib widget` (интерактивные графики). + +### Shell-команды + +```python +!pip install scikit-learn +!ls -la +!nvidia-smi +``` + +`!` — выполнить как shell. Удобно для установки пакетов прямо из notebook’а +**(хотя есть нюанс — см. ниже про `%pip`).** + +### Прочие полезные + +| Magic | Что делает | +|---|---| +| `%env` | Показать/задать переменную окружения | +| `%cd ` | Сменить рабочую директорию | +| `%who` / `%whos` | Показать все переменные в namespace | +| `%autoreload 2` | Автоматически перечитывать импортированные модули при изменении | +| `%lsmagic` | Список всех magic-команд | +| `%pip install ...` | То же, что `!pip install`, но **гарантированно в тот же Python**, что и kernel — без этой ловушки | + +**Тонкость с `!pip` vs `%pip`:** `!pip install` запускает `pip` из shell, и +если у тебя несколько Python’ов, может попасть не в тот. `%pip install` — это +magic, который **точно** использует pip для текущего kernel. **Рекомендуется +именно `%pip`.** + +## Блок 6 — Rich output + +Notebook’и умеют отображать не только текст, но и **HTML, картинки, видео, +интерактивные виджеты**. Это работает через **протокол repr-методов**: + +- Если у объекта есть `_repr_html_` → notebook рендерит как HTML. +- Если есть `_repr_png_` → как картинку. +- Если есть `_repr_latex_` → как формулу. +- Иначе fallback → `__repr__` → текст. + +**Поэтому** `pd.DataFrame` рисуется как HTML-таблица, а не как ``: +у класса есть `_repr_html_`. Тот же объект в обычном Python-скрипте отрендерится +текстом. + +**Можно использовать осознанно — для своих классов:** + +```python +class TrainingRun: + def __init__(self, name, loss): + self.name = name + self.loss = loss + + def _repr_html_(self): + return f"{self.name}: loss = {self.loss:.4f}" + +TrainingRun("baseline", 0.342) # отрисуется как HTML +``` + +**Стилизация DataFrame:** + +```python +df.style.background_gradient(cmap="Reds", subset=["accuracy"]) +``` + +`.style` возвращает `Styler` — отдельный объект, который рендерит DataFrame +как раскрашенную HTML-таблицу. Удобно для отчётов с метриками. + +**IPython.display.\* для произвольного контента:** + +```python +from IPython.display import Image, HTML, Markdown, Audio, Video, display + +display(Image(filename="architecture.png")) +display(HTML("

Done

")) +display(Markdown("**Bold**")) +display(Audio("speech.wav")) +``` + +**ipywidgets** — интерактивные элементы (слайдер, чекбокс, выпадающий список), +которыми можно крутить параметры эксперимента не переписывая код: + +```python +import ipywidgets as widgets +from IPython.display import display + +slider = widgets.FloatSlider(value=0.5, min=0, max=1, step=0.01, description="lr") +display(slider) +``` + +Тяжёлая артиллерия, но для исследовательских notebook’ов очень мощно. + +## Блок 7 — Notebooks vs Scripts + +Лозунг: **«explore in notebooks, ship in scripts»**. + +| Используй notebook | Используй .py-скрипт | +|---|---| +| Изучение датасета | Pipeline обучения, который запускается по расписанию | +| Прототип модели | Переиспользуемые утилиты (`from utils import ...`) | +| Визуализация результатов | Всё, где должен быть `if __name__ == "__main__"` | +| Объяснение работы (как лабораторная) | Production-код | +| Быстрые эксперименты | Пакеты, библиотеки | +| Упражнения курса | Тесты | + +**Гибридный workflow** (стандартный для AI-команд): + +1. В notebook’е изучаешь данные. +2. В notebook’е пробуешь модель. +3. **Когда работает** — переносишь логику в `.py` файлы (`src/utils.py`, + `src/model.py`). +4. Импортируешь обратно в notebook: `from src.model import MyModel`. +5. Дальше эксперименты — в notebook’е, продакшен — `.py`. + +**Чтобы импорты обновлялись без перезапуска kernel:** + +```python +%load_ext autoreload +%autoreload 2 +``` + +После этого, если ты меняешь `src/model.py`, в notebook’е новая версия +подхватывается **автоматически** при следующем вызове. Без `%autoreload` пришлось +бы перезапускать kernel или вручную делать `importlib.reload(...)`. + +**Чтобы можно было `from src.model import MyModel`:** + +В корне проекта должен быть `pyproject.toml` или `setup.py`, и: +```bash +pip install -e . +``` + +`-e` — editable install. Пакет «ставится», но физически — это симлинк на твою +рабочую папку. Любые правки в файлах сразу видны импортирующему коду. + +## Блок 8 — Google Colab + +Colab — это **Jupyter в облаке от Google**, с особенностями: + +**Три уровня хранения:** +1. **Notebook (`.ipynb`)** — живёт в Google Drive, **persistent** (постоянный). +2. **Compute** (виртуалка, на которой запущен kernel) — **ephemeral**, умирает + через 90 минут idle или 12 часов uptime (free). +3. **Google Drive** — можно подключить как файловую систему: + ```python + from google.colab import drive + drive.mount("/content/drive") + ``` + +**Идея, которую важно понять:** notebook сохраняется, **состояние памяти — +нет**. После «session timeout» все переменные, загруженные модели, скачанные +файлы — исчезают. Спасает только сохранение в Drive. + +**GPU runtime:** +- Runtime → Change runtime type → T4 GPU → Save. +- В первой ячейке: `!nvidia-smi`. + +**Idle timeout** — если ничего не выполняешь 90 минут, сессия отключается. +Чтобы избежать — есть всякие «keepalive»-скрипты, но Google их не любит. + +**Checkpointing:** +- Сохранять веса модели в Drive после каждых N эпох (`torch.save(model.state_dict(), + "/content/drive/MyDrive/checkpoint.pt")`). +- При следующем запуске — загрузить. + +**Secrets:** +- В Colab есть UI «Secrets» (ключик на боковой панели). Туда кладёшь + `ANTHROPIC_API_KEY`, и в коде: + ```python + from google.colab import userdata + api_key = userdata.get("ANTHROPIC_API_KEY") + ``` +- Никогда не пиши ключ прямо в ячейку — она сохраняется в Drive и видна, если + ноутбук расшарен. + +## Блок 9 — Три ловушки notebook’ов + +### Ловушка 1: out-of-order execution + +Ты запускаешь ячейку 5, потом 2, потом 7. У тебя в голове — твой порядок. +А тот, кто открывает notebook и нажимает «Run All», получает другой результат. + +**Лечение:** перед коммитом или шарингом — **`Kernel > Restart & Run All`**. +Это гарантирует, что notebook работает «сверху вниз без памяти». Если падает — +значит, ты опирался на ячейку, которая физически выше, чем должна быть. + +### Ловушка 2: hidden state + +Ты создал переменную в ячейке, потом **удалил ячейку**, но переменная **осталась +в kernel-памяти**. Notebook выглядит чисто, но работает только благодаря «призраку +удалённой ячейки». + +**Лечение:** регулярно `Kernel > Restart`. Если после restart notebook падает — +значит, ты опирался на hidden state. + +### Ловушка 3: memory leaks + +Грузишь датасет 4 GB. Потом ещё один. Потом обучаешь модель. **Ничего не +освобождается автоматически.** Память забивается, Colab падает с «out of memory». + +**Лечение:** +- `del variable_name; import gc; gc.collect()` — явно удалить и попросить + сборщик мусора пройтись. +- Для PyTorch с GPU: `torch.cuda.empty_cache()`. +- В крайнем случае — `Restart kernel`. + +**Общий лекарь от всех трёх — `Restart & Run All` перед каждым осмысленным +шарингом.** + +## Упражнения + +### Упражнение 1 — list-comp vs numpy через `%timeit` + +**Маршрут (твой реальный):** +1. Создать notebook `scratch/phase-00-lesson-05/exercise-1.ipynb`. +2. Сделать массив 100 000 случайных чисел двумя способами: + - `[random.random() for _ in range(100_000)]` — list comprehension. + - `np.random.rand(100_000)` — numpy. +3. Замерить оба `%timeit`. +4. Сформулировать гипотезу, **почему** numpy быстрее. + +**Что ты сделал и какие выводы:** + +- Speedup получился **~9×** (порядок цифр зависит от железа). +- Ты сам предположил: «numpy на C, list — через интерпретатор». +- В диалоге расширили до **трёх причин**, почему векторизация выигрывает: + 1. **Interpreter overhead per iteration** — каждая итерация в чистом Python + это шаги интерпретатора (bytecode dispatch). В numpy цикл происходит + в C-коде, без Python-обёрток на каждое число. + 2. **Contiguous memory + cache locality** — numpy-массив лежит в памяти + одним непрерывным блоком, попадает в кеш процессора. Python list — это + массив указателей на разбросанные по куче PyObject’ы; почти каждый доступ + — cache miss. + 3. **SIMD** — современные CPU умеют делать одну инструкцию над несколькими + числами одновременно. numpy этим пользуется, чистый Python — нет. + +**Главный вывод, который мы зафиксировали:** правило «vectorize, don’t loop» +будет применяться во **всех** последующих фазах — это базовая интуиция +производительности в Python для ML. + +### Упражнение 2 — CSV → DataFrame → plot → Restart & Run All + +**Маршрут (твой реальный):** +1. Положить `sales.csv` в `scratch/phase-00-lesson-05/`. +2. Создать `exercise-2.ipynb` рядом. +3. Структура — лабораторная работа: + - **Markdown-заголовок раздела** «Loading data». + - Code-ячейка: `import pandas as pd`, `df = pd.read_csv("sales.csv")`, + `df.head()`. + - **Markdown-заголовок** «Exploration». + - Code-ячейка: `df.describe()`, `df.dtypes`. + - **Markdown-заголовок** «Visualization». + - Code-ячейка: построить график (`df.plot(x="date", y="revenue")`). +4. После всего — `Kernel > Restart & Run All`. Notebook должен пройти + сверху вниз без ошибок. + +**Что ты попросил — мы дали готовый код-референс** как образец «как выглядит +хорошо структурированный notebook». Этот паттерн «markdown-заголовок раздела +→ code-ячейки» зафиксирован как **шаблон лабораторного журнала** для всех +дальнейших упражнений курса. + +### Упражнение 3 — Colab + GPU + `notebook_tips.py` + +**Сознательно пропущено.** На уровне «концепция notebook’а» материал уже +усвоен. Это упражнение можно сделать позже, когда дойдём до фазы 4 (CV), +где Colab + GPU понадобятся регулярно. + +В `phases/00-setup-and-tooling/05-jupyter-notebooks/code/notebook_tips.py` — +готовый код-референс для запуска. Когда понадобится — открыть, прогнать в +Colab с T4 GPU. + +## Quiz + +5 вопросов в `quiz.json` — пока **не пройден**. Можно прогнать в любой момент, +когда захочешь самопроверки. + +## Ключевые термины + +| Термин | Что говорят | Что это на самом деле | +|---|---|---| +| Kernel | «тот, кто запускает мой код» | Отдельный Python-процесс, исполняет ячейки, держит переменные в памяти | +| Cell | «блок кода» | Независимо запускаемая единица notebook’а — code или markdown | +| Magic command | «джупитеровские штуки» | Команды с префиксом `%` / `%%`, управляющие окружением, не сам Python | +| `.ipynb` | «файл notebook’а» | JSON со списком ячеек, их выводами и метаданными | +| `sys.executable` | (редко произносят) | Путь к Python-бинарнику, который сейчас исполняет код — главный диагностический атрибут | +| `%autoreload` | (часто забывают) | Magic, заставляющий kernel перечитывать импорты при изменении исходников | +| Out-of-order execution | (часто говорят, реже понимают) | Когда ячейки запущены не сверху вниз и notebook работает только в твоей голове | + +## Что важно вынести из урока + +1. **Notebook = JSON-файл + kernel.** Это две сущности, и их рассинхрон — + источник большинства проблем. +2. **`Shift+Enter` и модальный режим** — мышечная память, без которой ты будешь + медленным. +3. **`%timeit` для микро, `%%time` для длинных** операций. +4. **Rich output работает через `_repr_html_` / `_repr_png_`** — это можно + эксплуатировать для своих классов. +5. **`explore in notebooks, ship in scripts`** — гибридный workflow. +6. **Три ловушки** (out-of-order, hidden state, memory leaks) лечатся + `Restart & Run All` перед каждым осмысленным шарингом. +7. **Vectorize, don’t loop** — выводы из упражнения 1, применимы ко всему курсу. +8. **Markdown-заголовки + code-ячейки** — наш шаблон лабораторной работы для + всех последующих упражнений. diff --git a/phases/00-setup-and-tooling/06-python-environments/docs/ru.md b/phases/00-setup-and-tooling/06-python-environments/docs/ru.md new file mode 100644 index 000000000..e8a0c2a87 --- /dev/null +++ b/phases/00-setup-and-tooling/06-python-environments/docs/ru.md @@ -0,0 +1,539 @@ +# Урок 06 — Python Environments + +> Dependency hell — это реально. Virtual environments — это лекарство. + +**Тип:** Build +**Языки:** Python +**Пререквизиты:** Фаза 0, Урок 01 +**Время:** ~30 минут + +## Чему учимся + +- Создавать изолированные окружения через `uv`, `venv`, `conda`. +- Писать `pyproject.toml` с optional dependency groups и генерировать lockfile для воспроизводимости. +- Диагностировать типовые ошибки: глобальные установки, смешивание pip/conda, CUDA mismatch. +- Применять стратегию «environment на фазу» в проектах с конфликтующими зависимостями. + +--- + +## Блок 1 — Проблема: dependency hell + +### Что это вообще + +Любой `pip install <пакет>` без активного venv ставит пакет **в один общий +каталог** — туда, где живёт твой системный Python. Это значит, что **все +проекты на машине делят одно хранилище пакетов**. + +Пока у тебя один проект — проблемы нет. Проблема начинается, когда проектов +становится больше одного и они требуют **разных версий одного и того же пакета**. + +### Каноничный пример из AI + +- **Проект A** — fine-tuning старой модели, требует `torch==2.1.0` с CUDA 11.8. +- **Проект B** — генерация изображений на свежем diffusers, требует `torch==2.4.0` с CUDA 12.4. + +В системном Python `torch` может быть установлен **только в одной версии**. +Ставишь `2.4.0` — проект A падает с импорта. Ставишь `2.1.0` — проект B +ругается «требуется ≥ 2.4». Это и есть **dependency hell**. + +### Почему в AI это острее, чем в «обычной» разработке + +В обычном backend-приложении конфликт версий — это спор о версиях фреймворка +(Django 4 vs Django 5). Неприятно, но переживаемо. + +В AI каждая ML-библиотека **тащит свой собственный CUDA-бинарник** — +скомпилированный код, который умеет говорить с GPU только через конкретную +версию CUDA runtime. У PyTorch свой, у JAX свой, у TensorFlow свой. Когда они +оказываются в одном глобальном Python, они начинают перетирать друг другу +библиотеки на уровне C-кода, и ты получаешь segfault'ы и `CUDA error: ...`, +которые ничем не лечатся, кроме «снести всё и поставить заново». + +Плюс: модели на HuggingFace часто пинят строго конкретную версию `transformers` +или `torch` — и она у каждой модели своя. + +### Решение (которое разберём дальше) + +Каждый проект сидит в **своём собственном изолированном Python**. Активируешь +проект A — видишь его пакеты. Активируешь проект B — видишь его пакеты. +Они физически в разных папках, не пересекаются, конфликта нет. + +### Руками + +Чтобы прочувствовать проблему, посмотрим на «общую корзину» — твой системный +Python и что в неё уже сложено. + +См. шаги в чате (`which python3`, `python3 -m site`, `python3 -m pip list`). + +### Важная деталь Ubuntu 24.04 — PEP 668 + +На свежей Ubuntu (24.04) `pip` **не положен в системный Python намеренно**. +Это часть стандарта [PEP 668](https://peps.python.org/pep-0668/) — +«externally-managed environments». Системный Python принадлежит дистрибутиву, +половина системных утилит на нём держится, и любой `pip install` может +перетереть зависимости и сломать ОС. Поэтому Ubuntu: + +- **не кладёт `pip` в системный `python3`** — `python3 -m pip` → `No module named pip`; +- если ты его поставишь через `apt install python3-pip`, попытка `pip install <пакет>` + отвалится с `error: externally-managed-environment` и подскажет «делай venv»; +- обойти этот запрет можно флагом `--break-system-packages`, но именно «break» + в названии — намёк на то, что ты получишь сломанную систему. + +Итог: на современных дистрибутивах **dependency hell на системном Python +физически невозможен** — за тебя его запретили. Это сильный аргумент за то, +чтобы с самого начала привыкать к venv'ам. + +### Gotcha Ubuntu 24.04: venv тоже не идёт «из коробки» + +После чистой установки Ubuntu 24.04 первая же команда `python3 -m venv ...` +падает с ошибкой: + +``` +The virtual environment was not created successfully because ensurepip is not +available. On Debian/Ubuntu systems, you need to install the python3-venv +package using the following command. + + apt install python3.12-venv +``` + +Причина: в Debian-семействе **пакет Python разрезан на мелкие части**: + +| Пакет | Что внутри | +|---|---| +| `python3.12-minimal` | Интерпретатор + минимум stdlib (без чего apt не запустится) | +| `python3.12` | Полный stdlib | +| `python3.12-venv` | Модуль `venv` + `ensurepip` (механизм, кладущий pip внутрь нового venv) | +| `python3-pip` | Системный pip (которого по PEP 668 трогать нельзя) | +| `python3-full` | Мета-пакет — ставит всё вышеперечисленное скопом | + +Базовая Ubuntu ставит только `python3.12-minimal` + `python3.12`. Модуль +`venv` физически в системе **есть** (`python3 -m venv --help` работает), но +он не может закончить создание venv — потому что внутрь нового venv нужно +положить pip, а механизм `ensurepip` лежит в отдельном пакете +`python3.12-venv`. Поэтому venv падает на полпути. + +**Лечится одной командой:** `sudo apt install python3.12-venv`. После этого +venv будет работать. Это **разовая** установка на систему, а не на проект. + +--- + +## Блок 2 — Что такое virtual environment физически + +`venv` — это **папка**, в которую кладётся: + +| Объект | Что это | +|---|---| +| Симлинк/копия Python-интерпретатора | Ссылка на системный `/usr/bin/python3.12` (не дублирование) | +| `bin/activate`, `Activate.ps1`, ... | Скрипты активации для разных shell'ов | +| Свой `pip`, `pip3`, `pip3.12` | **Реальные** исполняемые файлы (в отличие от системного Python, где pip нет) | +| `lib/python3.X/site-packages/` | Собственное пустое хранилище пакетов | +| `pyvenv.cfg` | Конфиг venv | + +Содержимое `pyvenv.cfg` (типичное): + +``` +home = /usr/bin # где базовый Python +include-system-site-packages = false # КЛЮЧЕВОЕ — изоляция от /usr/lib/python3/dist-packages +version = 3.12.3 +executable = /usr/bin/python3.12 +``` + +Параметр `include-system-site-packages = false` — это и есть технический +механизм изоляции. Когда venv-овский Python стартует, он читает этот файл +и не добавляет системные `dist-packages` в свой `sys.path`. + +Свежий venv в `site-packages` содержит только `pip` и `pip-XX.dist-info` — +ничего из системных пакетов туда не попадает. + +--- + +## Блок 3 — Активация и изоляция + +### Что делает `source .venv/bin/activate` + +1. Сохраняет текущий `PATH` в `_OLD_VIRTUAL_PATH` (чтобы `deactivate` мог откатить). +2. Добавляет `/bin/` **в начало** `PATH` → теперь `python`, `pip` и т.п. находятся первыми из venv. +3. Экспортирует `VIRTUAL_ENV=<полный путь>`. +4. Меняет prompt — добавляет префикс `(имя_venv)`. + +Никакой магии в Python нет. Просто из-за изменённого `PATH` команда `python` +запускает `/bin/python`, а тот через `pyvenv.cfg` знает, где его +`site-packages`. `deactivate` — это просто откат `PATH` + удаление +`VIRTUAL_ENV`. + +### Картина «до/после» активации + +| Проверка | До | После | +|---|---|---| +| `which python3` | `/usr/bin/python3` | `/bin/python3` | +| `which pip` | `not found` (на Ubuntu 24.04) | `/bin/pip` | +| `$VIRTUAL_ENV` | пусто | `<полный путь к venv>` | +| Первый элемент `$PATH` | твой обычный | `/bin` | +| `python3 -m site` → `sys.path` | содержит `/usr/lib/python3/dist-packages` | содержит `/lib/.../site-packages`, **БЕЗ** dist-packages | +| `python3 -m site` → `ENABLE_USER_SITE` | `True` | `False` (≈ `~/.local/lib/...` тоже отключён) | +| prompt | `pavel@Home-PC:~$` | `(имя_venv) pavel@Home-PC:~$` | + +### Доказательство изоляции + +Сценарий: ставим `requests==2.34.2` в активный venv. После этого: + +```bash +# В активном venv +python3 -c "import requests; print(requests.__version__, requests.__file__)" +# → 2.34.2 /tmp/play-venv/lib/python3.12/site-packages/requests/__init__.py + +# В обход активации, через абсолютный путь к системному Python +/usr/bin/python3 -c "import requests; print(requests.__version__)" +# → либо ModuleNotFoundError (если в системе requests'а не было) +# → либо ДРУГАЯ ВЕРСИЯ из /usr/lib/python3/dist-packages/ (если apt уже поставил) +``` + +**Главная мысль:** изоляция работает **в обе стороны**. На одной машине +одновременно могут жить две разные версии одной и той же библиотеки — в +venv и в системе — и они **не пересекаются**, потому что у двух Python'ов +разные `sys.path` и разные `site-packages`. Это и есть лекарство от +dependency hell: версии разделены физически. + +### Откуда в системе появляется библиотека вроде `requests` без твоего участия + +Через apt — `python3-requests` ставится как **зависимость** какой-нибудь +системной утилиты Ubuntu (например, `command-not-found`, `software-properties`). +Проверить, какой пакет принёс файл: + +```bash +dpkg -S /usr/lib/python3/dist-packages/requests/__init__.py +``` + +Покажет `python3-requests: /usr/lib/...` и при желании можно `apt rdepends +python3-requests`, чтобы увидеть, кто зависит от него. + +### Деактивация + +`deactivate` (без аргументов) — откатывает всё: + +- `PATH` восстанавливается из `_OLD_VIRTUAL_PATH`; +- `VIRTUAL_ENV` сбрасывается; +- prompt возвращается к исходному. + +Сам каталог venv'а на диске **не трогается** — его можно активировать снова +в любой момент или физически удалить через `rm -rf `. Никакой +«регистрации» venv в системе нет, всё хранится в самой папке. + +--- + +## Блок 4 — Инструменты: `venv` vs `uv` vs `conda` + +| Возможность | `venv` (stdlib) | `uv` (Astral) | `conda` (Miniconda) | +|---|---|---|---| +| Создать isolated env | ✅ | ✅ | ✅ | +| Установка пакетов | через `pip` | свой `uv pip` (та же UI, 10-100× быстрее) | свой `conda install` | +| Управление Python-версиями | ❌ | ✅ (`uv python install 3.12`) | ✅ | +| Lockfile для воспроизводимости | ❌ | ✅ (`uv.lock` автоматом) | ✅ (`environment.yml`) | +| Не-Python зависимости (CUDA toolkit, MKL, GDAL) | ❌ | ❌ | ✅ | +| Чтение `pyproject.toml` | ❌ | ✅ нативно | через pip | +| Размер инструмента | 0 (stdlib) | один бинарник ~30МБ | ~500МБ Miniconda | + +**Когда какой:** + +- **`venv`** — когда нужно быстро поставить что-то на машине без интернета или прав ставить `uv`. Всегда работает, но без свистулек. +- **`uv`** — **стандарт для курса и для AI/ML в целом.** Делает всё, что venv + pip + pyenv + pip-tools, и в 10-100 раз быстрее. От Astral (создатели `ruff`). +- **`conda`** — берём, **только если нужна не-Python зависимость без `sudo apt`** (свой CUDA toolkit, MKL, специфические C-библиотеки). На WSL CUDA приходит с Windows-драйвера, conda обычно не нужен. Внутри conda-env **нельзя мешать `pip install`** — теряется учёт зависимостей. + +Не путать **Miniconda** (~500МБ, голый менеджер) с **Anaconda** (~3ГБ дистрибутив с кучей предустановленного мусора). + +`uv` ставится одной командой: + +```bash +curl -LsSf https://astral.sh/uv/install.sh | sh +``` + +Кладёт бинарник в `~/.local/bin/uv` и дописывает PATH в `~/.bashrc`. После +этого `uv` сразу же по умолчанию подтягивает свой managed Python (например, +`cpython-3.12.13` в `~/.local/share/uv/python/...`) — этот Python не привязан +к PEP 668 и без ограничений Ubuntu. + +--- + +## Блок 5 — Реальная venv для курса (Упражнение 1) + +Используем существующую `.venv` в корне репо (она уже создана через uv с +prompt `ai-engineering-from-scratch`). Активируем, проверяем содержимое, +доставляем недостающие core-пакеты. + +```bash +cd ~/ai-engineering-from-scratch +source .venv/bin/activate +uv pip install scikit-learn # доставить недостающее +``` + +### Gotcha №1: имя пакета на PyPI ≠ имя модуля для `import` + +Главная путаница для новичков. То, что ты пишешь в `pip install`, и то, что +ты пишешь в `import`, — **разные вещи**. Они часто совпадают, но не всегда. + +| `pip install ...` (PyPI-имя) | `import ...` (имя модуля) | +|---|---| +| `scikit-learn` | `sklearn` | +| `Pillow` | `PIL` | +| `beautifulsoup4` | `bs4` | +| `PyYAML` | `yaml` | +| `opencv-python` | `cv2` | +| `python-dateutil` | `dateutil` | +| `huggingface-hub` | `huggingface_hub` | + +Дополнительный кейс — пакет `sklearn` на PyPI **намеренно сломан** мейнтейнерами: +`pip install sklearn` падает с сообщением «use scikit-learn». Это сделано, +чтобы все перешли на правильное имя. + +### Gotcha №2: мета-пакеты не имеют своего модуля + +`jupyter` на PyPI — это **мета-пакет**: при установке он тащит за собой +`jupyter_core`, `jupyter_client`, `jupyterlab`, `notebook`, `ipykernel` +и т.п., но **никакого `import jupyter`** не предоставляет. Проверять +установку нужно через зависимости: `import jupyter_core`, +`import jupyterlab` и т.п. + +Скрипт `env_setup.sh` именно так и делает: +```bash +verify_package "jupyter" "jupyter_core" +# pypi импорт +``` + +### Корректная проверка core-пакетов + +```bash +for pair in "numpy:numpy" "matplotlib:matplotlib" "jupyter:jupyter_core" \ + "scikit-learn:sklearn" "pandas:pandas"; do + pkg="${pair%:*}" + mod="${pair#*:}" + python -c "import $mod; print(f' {\"$pkg\":12} -> import $mod ({$mod.__version__})')" +done +``` + +### Smoke test — numpy работает + +```bash +python -c " +import numpy as np +a = np.random.randn(3, 3) +b = np.random.randn(3, 3) +c = a @ b +print(f' Matrix multiply: ({a.shape}) @ ({b.shape}) = ({c.shape})') +" +``` + +Если matmul напечатал `(3, 3) @ (3, 3) = (3, 3)` — numpy здоров, venv готов +к работе. + +--- + +## Блок 6 — `pyproject.toml` и lockfile (Упражнение 3) + +`pyproject.toml` — современный единый файл конфигурации Python-проекта +(PEP 518, PEP 621). Заменяет `setup.py` + `setup.cfg` + `requirements.txt` ++ `MANIFEST.in`. + +### Структура (для проекта-разработки) + +```toml +[build-system] # КАК собирать (нужно для упаковки) +requires = ["setuptools>=64"] +build-backend = "setuptools.build_meta" + +[project] # ЧТО за пакет +name = "playground-project" +version = "0.1.0" +requires-python = ">=3.12" +dependencies = [ # базовые зависимости + "numpy>=1.26", +] + +[project.optional-dependencies] # ОПЦИОНАЛЬНЫЕ группы +torch = ["torch>=2.3", "torchvision>=0.18"] +llm = ["anthropic>=0.39", "openai>=1.50"] +``` + +Плюс инструментальные секции — `[tool.ruff]`, `[tool.pytest.ini_options]` +и т.п. — для конфигов tools, не обязательная часть стандарта. + +### Установка с extras + +```bash +uv pip install -e . # только базовые deps +uv pip install -e ".[torch]" # базовые + torch group +uv pip install -e ".[torch,llm]" # базовые + обе группы +``` + +`-e` = **editable install**: pip кладёт в `site-packages` не копию файлов +проекта, а `.pth`-файл с путём к исходникам. Изменения в коде подхватываются +без переустановки — стандартный режим разработки. + +### Гott'cha: `--extra` — это имя группы, не пакета + +Если в `pyproject.toml` нет секции `[project.optional-dependencies]`, то +`uv pip compile pyproject.toml --extra numpy` упадёт с +`error: Requested extras not found: numpy`. Извлечения не привязаны к +именам пакетов — это **именованные группы**, которые ты сам определяешь +в `[project.optional-dependencies]`. + +### Lockfile через `uv pip compile` + +```bash +uv pip compile pyproject.toml -o requirements.lock --extra torch --extra llm +``` + +Резолвит все зависимости + транзитивные, пишет точные версии в файл. +**Ничего не ставит** — только описывает «идеальный мир версий», который +должен получиться. + +### Что показывает lockfile + +Из 5 пакетов в `pyproject.toml` → **56 пакетов** в lockfile. + +Каждая строка имеет комментарий `# via <кто потребовал>` — это **граф +зависимостей в виде текста**. Удобно прослеживать цепочки. + +Полезные наблюдения на нашем lockfile: + +- `torch==2.12.0` тянет **18 NVIDIA-пакетов** (`nvidia-cudnn-cu13`, + `nvidia-cublas`, `triton` и др.) — ~2-3 ГБ. Суффикс `-cu13` = CUDA major + version 13; если на машине CUDA 12 — эти бинарники не подойдут. + Это **физическая причина** «CUDA mismatch». +- `numpy` приходит из двух источников (`pyproject.toml` + `torchvision`), + uv резолвит в одну версию, удовлетворяющую обоим. +- `anthropic` + `openai` тянут общую базу: `pydantic`, `httpx`, `anyio`. + Если бы версии конфликтовали — uv упал бы с сообщением о конфликте. +- `typing-extensions` — лидер по `via`: на него ссылаются 7 пакетов + (`pydantic`, `torch`, оба SDK, и др.). Утилитарный «backport системы + типов» — стандарт современного Python. + +### Воспроизводимость + +```bash +uv pip install -r requirements.lock # установить ровно тот набор +``` + +`requirements.lock` коммитится в git. На любой машине, в любой момент +времени, эта команда даст **идентичный** результат. Это и есть +воспроизводимость билдов. + +Полный workflow `uv` для проектов (`uv lock` + `uv sync` + собственный +`uv.lock`) — продвинутее, умеет инвалидироваться при изменениях в +`pyproject.toml`. Но для большинства задач `uv pip compile` достаточно. + +--- + +## Блок 7 — Изоляция руками (Упражнение 2) + +Цель: убедиться руками, что два venv с разными версиями одного пакета +не пересекаются. + +Сценарий: +1. `uv venv /tmp/numpy-old-venv --python 3.12` — новый venv. +2. `uv pip install "numpy==1.26.4"` — старая numpy. +3. Репо-venv хранит `numpy==2.4.4`. +4. Кросс-проверка через **абсолютные пути** к Python, без активации: + +```bash +/tmp/numpy-old-venv/bin/python -c "import numpy; print(numpy.__version__)" +# → 1.26.4 + +~/ai-engineering-from-scratch/.venv/bin/python -c "import numpy; print(numpy.__version__)" +# → 2.4.4 +``` + +Каждый Python смотрит в **свой** `site-packages` через свой `pyvenv.cfg`. +Никакой общей точки разделения нет — это две физически отдельные папки. + +--- + +## Блок 8 — Per-phase strategy и Common Mistakes + +### Per-phase strategy + +Курс на 20 фаз. У них **разные** dependency-стеки: + +| Фазы | Что нужно | +|---|---| +| 0-3 (setup, math, python) | numpy, pandas, matplotlib, jupyter — мелочи | +| 4-7 (NN, CNN, RNN, transformers) | torch + CUDA (700МБ-2ГБ) | +| 8-10 (LLM training, RLHF) | transformers, datasets, accelerate — пины на конкретные torch | +| 11+ (LLM APIs, RAG) | anthropic, openai, langchain — никакого torch не нужно | + +Один общий venv на весь курс — **плохо**, потому что: +- Конфликты: HF может пинить torch 2.3, а потом тебе понадобится torch 2.5. +- Размер: тащить 2ГБ torch для фазы 11 (где он не нужен) — расточительно. +- Скорость: чем больше пакетов в venv, тем медленнее резолвится новая установка. + +Рекомендуемая структура (из en.md): + +``` +ai-engineering-from-scratch/ +├── .venv/ ← lightweight для фаз 0-3 (numpy/pandas/jupyter) +├── phases/ +│ ├── 04-neural-networks/.venv/ ← PyTorch env +│ ├── 08-transformers/.venv/ ← возможно отдельный torch+transformers +│ └── 11-llm-apis/.venv/ ← только API SDKs, без torch +``` + +Каждый venv обслуживает **группу родственных фаз**, а не одну фазу. Если две +соседние фазы одинаковы по deps — можно одну venv шарить через симлинк или +просто переиспользовать активацию. + +### Common Mistakes — что мы уже встретили по ходу урока + +| Ошибка | Когда увидели | +|---|---| +| **Global pip install** | Блок 1: `python3 -m pip install requests` → `No module named pip` (Ubuntu 24.04 защищает через PEP 668) | +| **Mixing pip + conda** | Не делали — `conda` не используем; обсудили концептуально в Блоке 4 | +| **Forgot to activate** | Блок 3: видели разницу до/после `source activate` — `which python`, `$PATH`, `$VIRTUAL_ENV` | +| **Committing .venv to git** | Проверили: `.venv/` уже в `.gitignore` репо ✓ | +| **CUDA version mismatch** | Блок 6: видели в lockfile — `nvidia-cudnn-cu13` (CUDA 13) против `nvidia-cusparselt-cu13`. Если на машине CUDA 12 — эти бинарники не подойдут. | +| **Имя PyPI ≠ имя модуля** | Блок 5: `pip install sklearn` → namespace `sklearn` намеренно сломан; правильно `pip install scikit-learn` | +| **Мета-пакет без модуля** | Блок 5: `import jupyter` падает, нужно `import jupyter_core` | + +### Упражнение 4 — поставить пакет глобально + +Из en.md: «Deliberately install a package globally (without activating a venv), +notice where it goes, then uninstall it.» + +**Уже сделано в Блоке 1**: `python3 -m pip install requests` → `No module +named pip`. Ubuntu 24.04 (PEP 668) **физически запрещает** глобальную установку +— и это правильно. Удалять нечего, потому что и установить не получилось. +Урок усвоен на уровне ОС: глобально ставить нельзя, всё в venv. + +--- + +## Итоги урока 06 + +✅ Прошли: + +- **Блок 1** — проблема dependency hell + PEP 668 (Ubuntu запрещает глобальный pip). +- **Блок 2** — что такое venv физически (папка с симлинком + свой `site-packages`). +- **Блок 3** — активация (`PATH`, `$VIRTUAL_ENV`, prompt) и изоляция (две версии requests на одной машине). +- **Блок 4** — инструменты: `venv` vs `uv` vs `conda`. uv выбран как стандарт курса. +- **Блок 5** (Упр. 1) — реальная рабочая venv для курса + два gotcha: PyPI-имя ≠ import-имя, мета-пакеты. +- **Блок 6** (Упр. 3) — `pyproject.toml` + lockfile через `uv pip compile`. 5 пакетов → 56 пакетов в lockfile. +- **Блок 7** (Упр. 2) — изоляция руками: две версии numpy в двух venv. +- **Блок 8** — per-phase strategy + сводка по Common Mistakes (большинство уже прошли по ходу). +- **Упр. 4** — закрыто Ubuntu PEP 668 (глобальный install запрещён). + +🔧 На машине после урока: + +- `/home/pavel/.local/bin/uv` — менеджер пакетов. +- `/home/pavel/.local/share/uv/python/cpython-3.12.13-linux-x86_64-gnu/` — uv-managed Python. +- `~/ai-engineering-from-scratch/.venv/` — рабочая venv курса с core stack. +- `~/ai-engineering-from-scratch/scratch/lesson-06/playground-project/` — учебный pyproject + lockfile. + +### Корзины, которые видит системный Python + +`sys.path` (из `python3 -m site`): + +| Путь | Что лежит | Кто кладёт | +|---|---|---| +| `/usr/lib/python3.12/` | stdlib (`os`, `sys`, `json`, ...) | apt (пакет `python3.12-minimal`) | +| `/usr/lib/python3.12/lib-dynload/` | скомпилированные модули stdlib | apt | +| `/usr/local/lib/python3.12/dist-packages/` | пакеты, поставленные локальным админом | `sudo pip install --break-system-packages` | +| `/usr/lib/python3/dist-packages/` | пакеты, на которых держится сама ОС (`apt_pkg`, `dbus`, ...) | `apt install python3-*` | +| `~/.local/lib/python3.12/site-packages/` (USER_SITE) | пользовательские пакеты | `pip install --user --break-system-packages` | + +В нашем курсе мы **не будем класть туда ничего**. Всё — в venv внутри проекта. diff --git a/phases/00-setup-and-tooling/07-docker-for-ai/docs/ru.md b/phases/00-setup-and-tooling/07-docker-for-ai/docs/ru.md new file mode 100644 index 000000000..d1405aaca --- /dev/null +++ b/phases/00-setup-and-tooling/07-docker-for-ai/docs/ru.md @@ -0,0 +1,406 @@ +# Урок 07 — Docker для AI (русский конспект) + +> «У меня работает» больше не отмазка — контейнер ходит с тобой везде. + +**Тип:** Build · **Время:** ~60 минут · **Пререквизиты:** Фаза 0, уроки 01 и 03. + +**Цели:** +- понять, что такое **image** и **container**, чем они отличаются; +- запустить готовый контейнер, посмотреть руками, что происходит в системе; +- маунтить папку хоста внутрь контейнера через **volume** (`-v`), чтобы код был общий; +- написать **свой Dockerfile**, собрать из него образ и запустить; +- (по желанию, факультативно) — Docker Compose для multi-service. + +GPU/CUDA-часть урока (`--gpus all`, NVIDIA Container Toolkit) — про Linux-хосты +с NVIDIA. У меня Windows + WSL2 без локальной NVIDIA, поэтому она остаётся +теоретическим контекстом, без реального запуска. + +--- + +## Часть 1 — Что такое Docker одной фразой + +**Docker — это упаковщик программ.** Берёт твою программу + всё, что ей нужно +(Python, библиотеки, кусок ОС), и кладёт в **коробку**. Эту коробку можно +отдать кому угодно — у него запустится точно так же. + +Два главных слова: + +- **Image (образ)** — сама коробка в собранном виде. Файл на диске. Read-only. + Аналогия: **DVD с фильмом**. +- **Container (контейнер)** — открытая, запущенная коробка. Программа внутри + неё работает прямо сейчас. Аналогия: **фильм, который сейчас идёт на экране**. + +Из одного image можно запустить **много** контейнеров одновременно. Каждый +со своим состоянием. + +--- + +## Часть 2 — Архитектура: клиент / демон / registry + +Когда мы делаем `docker run hello-world`, происходит вот что: + +``` +┌──────────────┐ команда ┌──────────────┐ pull ┌────────────┐ +│ docker CLI │ ─────────▶ │ Docker daemon│ ◀──────▶│ Docker Hub │ +│ (мой WSL) │ │ (Win/WSL VM) │ │ (registry) │ +└──────────────┘ └──────┬───────┘ └────────────┘ + │ + ▼ + запускает контейнер, + возвращает вывод +``` + +- **CLI** (`docker`-команда) — это тонкий клиент. Просто отправляет запросы. +- **Daemon** — фоновый процесс, реально работающий с контейнерами. У меня + он внутри Docker Desktop, в виртуалке WSL. +- **Registry** (по умолчанию Docker Hub) — публичная «полка» с образами, + откуда демон их подтягивает. + +--- + +## Часть 3 — Жизненный цикл контейнера, что мы видели руками + +### Команды-«посмотреть» + +| Команда | Что показывает | +|---|---| +| `docker ps` | Что **сейчас работает** | +| `docker ps -a` | **Все** контейнеры, включая остановленные («трупы») | +| `docker images` | Образы на «полке» | + +### Что мы видели на hello-world + +1. `docker ps` пусто, `docker images` пусто → стартовое состояние. +2. `docker run hello-world` → Docker скачал образ (~9 КБ контент, 25 КБ + на диске), запустил контейнер, программа `/hello` отработала. +3. После запуска: + - `docker images` → в нём `hello-world:latest`. + - `docker ps` пусто (программа кончилась → контейнер остановился). + - `docker ps -a` → есть «труп» со статусом `Exited (0)`. Имя сгенерило + Docker (типа `pensive_driscoll`). +4. `docker rm ` → убрали труп. + +**Правило:** контейнер живёт ровно столько, сколько живёт его **главный +процесс**. Когда программа внутри заканчивается — контейнер останавливается. +Но **не удаляется** автоматически. Чтобы он не оставался — флаг `--rm` при +`docker run`. + +--- + +## Часть 4 — Изоляция: контейнер vs хост + +Запустили `docker run -it --rm python:3.12-slim bash` и посмотрели изнутри: + +| Что | На хосте (WSL) | Внутри контейнера | +|---|---|---| +| ОС | Ubuntu 24.04 | Debian 13 (trixie) | +| Пользователь | pavel | root | +| Hostname | Home-PC | случайный hex (= ID контейнера) | +| `/home` | `/home/pavel/...` | пусто | +| Python | мой `.venv` | свежий `/usr/local/bin/python 3.12.13` | +| Мой проект | `~/ai-engineering-from-scratch/...` | **НЕ виден** | + +Контейнер — **не виртуалка**. У него **то же ядро Linux**, что у хоста. +Это «обманутый процесс»: ядро говорит ему «ты — root, у тебя своя файловая +система», но физически он один из процессов хоста. + +### Флаги `docker run`, которые встретятся ещё много раз + +| Флаг | Значение | +|---|---| +| `-i` | оставить stdin открытым (интерактивно) | +| `-t` | дать tty (нормальный prompt, цвета) | +| `-it` | стандартная связка для «зайти в контейнер» | +| `--rm` | удалить контейнер сразу после остановки | +| `--name X` | задать имя контейнеру (иначе случайное) | +| `-v A:B` | проброс папки `A` хоста как `B` внутри контейнера | +| `-p X:Y` | проброс порта (host:container) — будет позже | +| `--user U:G` | запустить процесс внутри от UID `U` и GID `G` | + +--- + +## Часть 5 — Volume: код и данные на хосте, окружение в контейнере + +**Проблема:** контейнер не видит мой код. И «запекать» код в образ при каждой +правке — нелепо. + +**Решение:** **bind mount** — флаг `-v <путь на хосте>:<путь в контейнере>`. +Контейнер видит папку хоста как свою. + +Аналогия: **флешка**. Файлы физически на флешке, но компьютер видит их как +`D:\`. С Docker: файлы на моём диске, контейнер видит их в `/workspace`. + +### Что мы сделали руками + +```bash +docker run -it --rm -v ~/ai-engineering-from-scratch:/workspace python:3.12-slim bash +``` + +Внутри: +- `ls /workspace` → весь репо виден. +- `mkdir -p /workspace/scratch/lesson-07 && echo "..." > /workspace/scratch/lesson-07/from-container.txt` +- `exit` + +Снаружи (после `exit`): +- Контейнера в `docker ps -a` нет (`--rm` его убрал). +- Файл `scratch/lesson-07/from-container.txt` **остался** — он реально + на диске хоста. + +### Ловушка с правами файлов (важно!) + +Внутри контейнера я был **root**. При записи через bind mount файл создался +с владельцем **`root:root`** на хосте. Cursor запускается от **pavel** и +не может писать в эту папку → `EACCES: permission denied`. + +Лечится: +```bash +sudo chown -R pavel:pavel <папка> +``` + +Чтобы не повторялось — запускать контейнер с `--user`: +```bash +docker run -it --rm --user $(id -u):$(id -g) -v ... python:3.12-slim bash +``` +Тогда внутри будет мой UID, и записанные файлы будут принадлежать `pavel`. + +--- + +## Python-врезка: `if __name__ == "__main__":` + +В каждом Python-файле есть «магическая» переменная `__name__`, которую Python +ставит автоматически: + +| Сценарий | Что в `__name__` | +|---|---| +| Файл запущен **напрямую** (`python main.py`) | `"__main__"` | +| Файл **импортирован** (`import main`) | `"main"` (имя файла) | + +**Зачем эта идиома:** + +1. **Защита от случайного выполнения при импорте.** Любой код на верхнем уровне + модуля выполняется **при импорте**. Если внутри есть `print(...)`, чужой + `import foo` напечатает это. С `if __name__ == "__main__":` блок выполнится + **только при прямом запуске**. + +2. **Один файл — и библиотека, и скрипт.** Импортируешь — получаешь функции. + Запускаешь напрямую — получаешь демку/CLI. + +**Канонический шаблон Python-файла:** + +```python +"""Однострочный docstring модуля.""" + +# 1. стандартная библиотека +import os +import socket + +# 2. сторонние пакеты +from rich.console import Console +from rich.panel import Panel + +# 3. локальные модули +# from .utils import helper + +# 4. константы +TITLE = "Демо" + +# 5. функции и классы +def main() -> None: + """Точка входа.""" + console = Console() + console.print(Panel("Hello!", title=TITLE)) + +# 6. защита запуска +if __name__ == "__main__": + main() +``` + +Ключевое: +- Логика — внутри функции `main()`, а не сразу в блоке `if __name__ ...`. + Так `main()` тоже импортируема (`from script import main`). +- Type hints (`-> None`, `text: str`) — необязательны для работы, но + читаемость растёт сильно. В AI-курсе встретится повсюду. + +--- + +## Часть 6 — Свой Dockerfile + +Собрали свой образ из трёх файлов в `scratch/lesson-07/app/`. + +### Артефакты + +**`requirements.txt`** +``` +rich==13.9.4 +``` + +**`Dockerfile`** +```dockerfile +FROM python:3.12-slim +WORKDIR /app +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt +COPY main.py . +CMD ["python", "main.py"] +``` + +**`main.py`** — печатает rich-Panel с приветствием, версией Python и hostname. + +### Разбор Dockerfile построчно + +Каждая инструкция = один **слой** (кэшированный снимок ФС). Слои клеятся +сверху вниз; финальный образ — их сумма. + +1. **`FROM python:3.12-slim`** — базовый образ с Docker Hub. Минимальный + Debian + CPython 3.12 + pip. ~150 MB. Альтернативы: `python:3.12` + (полная, ~1 GB) или `python:3.12-alpine` (~50 MB, но gotchas для ML). +2. **`WORKDIR /app`** — `cd /app` для всех следующих инструкций, создаёт + папку если её нет. Конвенция: «здесь живёт код приложения». +3. **`COPY requirements.txt .`** — забрать файл из build context (папки, + переданной аргументом `docker build`) и положить внутрь образа + в `/app/requirements.txt`. +4. **`RUN pip install --no-cache-dir -r requirements.txt`** — выполнить + команду **на этапе сборки**, результат запечь в слой. `--no-cache-dir` + убирает кэш pip из слоя (он мёртвый груз — ставить заново внутри + образа никто не будет). +5. **`COPY main.py .`** — наш скрипт в `/app/main.py`. Отдельной строкой + ради кэша (см. ниже). +6. **`CMD ["python", "main.py"]`** — главный процесс контейнера. Запустится + **при `docker run`**, не при билде. Контейнер живёт ровно столько, сколько + живёт этот процесс. **Exec form** (массив) — обязательна; shell form + (`CMD python main.py`) ломает обработку сигналов. + +`FROM` обязан быть первой инструкцией. `RUN` ≠ `CMD`: первое — во время билда, +второе — во время запуска. + +### Билд + +```bash +cd ~/ai-engineering-from-scratch +docker build -t my-hello scratch/lesson-07/app +``` + +- `-t my-hello` — тег (имя) образу. +- `scratch/lesson-07/app` — **build context**: папка, которую Docker + заархивирует и отправит демону. Пути в `COPY` берутся **относительно неё**. + `COPY ../something` не сработает — нельзя ссылаться выше context'а + (безопасность). + +В выводе видно по шагам: `[1/5] FROM ...`, `[2/5] WORKDIR ...`, и т.д. +Самый дорогой шаг — `[4/5] RUN pip install` (на свежей машине у Pavel'а +14.2s из 18.1s целиком, 78% времени). + +### Запуск + +```bash +docker run --rm my-hello +``` + +- `--rm` — удалить контейнер сразу после остановки (чтобы не плодились + мёртвые в `docker ps -a`). +- Никаких аргументов после имени образа → Docker запустит `CMD` из Dockerfile. + +Результат: Panel с приветствием, версией Python и `hostname` = random hex +(Docker ставит hostname контейнера = его ID). Это иллюстрация изоляции: +контейнер «не знает», что он крутится на твоей машине. + +**Важно:** каждый `docker run` создаёт **новый контейнер**. Старый ушёл +по `--rm`. Образ = слепок, контейнер = одноразовый процесс. + +### Кэширование слоёв (главное чудо Docker) + +Docker идёт по Dockerfile сверху вниз и на каждой инструкции спрашивает: +«входные данные этого шага изменились?». Пока ответ «нет» — берёт из кэша +(`CACHED [N/M] ...`). Как только «да» — этот слой **и все следующие** +пересобираются заново, даже если их вход не менялся. + +**Что считается входом:** +- `FROM` — тег базового образа; +- `WORKDIR` — текст инструкции; +- `COPY` — имя файла И его содержимое (Docker хеширует); +- `RUN` — текст команды. + +**Эксперимент трёх билдов** на одном и том же Dockerfile: + +| Билд | Что изменилось | Время | Что в кэше | +|---|---|---|---| +| 1 | первый раз | 18.1s | только базовый образ (он был с прошлых частей) | +| 2 | поправили опечатку в `main.py` | **1.0s** ✨ | 3 верхних слоя — включая дорогой `pip install` | +| 3 | дописали пустую строку в `requirements.txt` | 15.7s 🐢 | только `WORKDIR` | + +**Билд 2** — магия. Изменился только последний `COPY main.py`, `pip install` +взялся из кэша. 14 секунд испарились. + +**Билд 3** — обратная сторона медали. Тронули `requirements.txt` → +сломался шаг 3 → каскадом инвалидировались шаги 4 и 5. `pip install` +крутился заново (11.8s), хотя сам список пакетов не менялся (только пустая +строка). Docker не пытается доказать, что верхние слои не зависели от +нижних — он просто пересобирает всё ниже точки разрыва. + +**Архитектурное правило для всех Dockerfile в курсе:** +**изменчивое — ниже, стабильное — выше.** + +У нас `main.py` (правится каждый раз) стоит ниже `pip install` (зависимости +стабильны неделями). На фазах 4+ дорогими слоями станут `torch`, +`transformers`, скачивание датасетов — их всегда ставим **до** `COPY` кода. +Иначе пересборка torch-слоя = 5 минут ожидания вместо 1 секунды. + +**Что бы случилось с `COPY . .` одной командой** — любая правка любого +файла ломала бы кэш, и `pip install` крутился бы каждый раз. Поэтому +раздельные `COPY requirements.txt` и `COPY main.py` — обязательная +идиома для Python-Dockerfile. + +### `docker images` и `docker history` — разглядывание + +```bash +docker images | grep my-hello +docker history my-hello +``` + +`docker images` показал `my-hello:latest` ~207 MB. + +`docker history` читается **снизу вверх** — это хронология сборки. +Структура нашего образа: + +| Категория | Размер | +|---|---| +| Debian rootfs (`debuerreotype`, 2 weeks ago) | 87 MB | +| apt-пакеты базы | 5 MB | +| Сам Python 3.12.13 | 41 MB | +| Наш `pip install` (rich + deps) | 23 MB | +| Наш код (`COPY main.py`, `COPY requirements.txt`, `WORKDIR`) | ~33 kB | +| **Итого реальное содержимое** | **~156 MB** | + +Разница до 207 MB — manifests, attestation, метаданные BuildKit. + +**Замеченные детали:** +- `` в колонке IMAGE — BuildKit хранит промежуточные слои + анонимно, ради безопасности и оптимизации. Старый builder давал каждому + слою свой image ID. Это норма. +- `ENV ...` строки — 0 байт. Это переменные, не файлы. +- `RUN apt-get install ... 4.94 MB` и `RUN ... savedAptMark 41.4 MB` — + это вся «логика установки Python из исходников», прописанная командой + Python в их официальном Dockerfile. Мы её просто унаследовали через `FROM`. + +### Что отложено до момента, когда понадобится + +По стратегии just-in-time learning Pavel'а (см. `feedback_just_in_time_learning.md`): + +- **Упражнение 4 (image size optimization)** — сравнить размеры одного + и того же `main.py` на базах `python:3.12` vs `slim` vs `alpine`. + Вернуться когда столкнёмся с задачей выкатить образ в прод/CI и важен + размер. +- **Docker Compose с qdrant** — multi-service сценарий, стандарт для всех + AI-стеков (несколько контейнеров: Python-приложение + векторная БД + + кэш + и т.д.). Вернуться на **фазе 8+** при первом RAG-проекте, когда + Qdrant понадобится реально. + +--- + +## Артефакты урока + +- `scratch/lesson-07/from-container.txt` — первый файл, написанный + изнутри контейнера через bind mount (`pavel:pavel` после `chown`). +- `scratch/lesson-07/app/` — мини-проект для своего Dockerfile: + `Dockerfile`, `main.py`, `requirements.txt`. Образ `my-hello:latest` + собран и проверен (`docker run --rm my-hello`). diff --git a/phases/00-setup-and-tooling/08-editor-setup/docs/ru.md b/phases/00-setup-and-tooling/08-editor-setup/docs/ru.md new file mode 100644 index 000000000..051f44300 --- /dev/null +++ b/phases/00-setup-and-tooling/08-editor-setup/docs/ru.md @@ -0,0 +1,258 @@ +# Урок 8 — Настройка редактора (AstroNvim edition) + +> Параллельный конспект к `en.md`. Оригинальный урок построен вокруг VS Code; +> здесь то же самое переложено на AstroNvim, потому что это основной редактор +> в текущей конфигурации (см. `astronvim-setup.md` в корне репо). +> +> Когда позже пересяду на Cursor или VS Code — пройду урок заново уже под них. + +## Что должен уметь редактор для AI-работы + +Урок постулирует пять уровней: + +``` +5. Remote development — открыть и редактировать файлы на удалённой GPU-машине +4. Terminal integration — где гонять скрипты, nvidia-smi, тренировки +3. AI-specific settings — format-on-save, type checking, rulers +2. Extensions — Python LSP, Jupyter, линтер, форматтер, Git +1. Base editor — собственно текстовый редактор +``` + +В Neovim-логике это устроено иначе, чем в VSCode. Маппинг: + +| Слой | VS Code | Neovim | +|---|---|---| +| Base editor | VS Code (Electron, GUI) | Neovim (терминальное приложение) | +| Extensions | Один магазин расширений | **Два разных уровня**: плагины (Lua/Vim код) + LSP-серверы (отдельные процессы) | +| Settings | `settings.json` в `.vscode/` | Lua-файлы в `~/.config/nvim/` + опции конкретных плагинов | +| Terminal | Integrated terminal | Встроенный `:terminal`, плюс шорткаты AstroNvim (F7, `tf`) | +| Remote | Расширение Remote-SSH | `ssh user@box && nvim` (легковесно) или `distant.nvim` (тяжёлый аналог Remote-SSH) | + +### Ключевая концепция: plugin ≠ LSP-server + +В VS Code разработчик ставит одно расширение `ms-python.vscode-pylance` — и +сразу есть автокомплит, типы, инспекция импортов. В Neovim это **разделено +на два уровня**: + +- **Плагин** (Lua-код) — отвечает за UI и интеграцию с редактором. Например + `nvim-cmp` рисует попап автокомплита. Сам по себе ничего не знает про Python. +- **LSP-сервер** — отдельный процесс (Python/Node/Rust бинарь), который читает + код и говорит редактору: «эта переменная имеет тип `pd.DataFrame`, у неё + есть метод `.groupby`». Для Python это, например, `pyright` или + `basedpyright`. + +Между ними — два склеивающих компонента: + +- **`nvim-lspconfig`** — плагин-клей: запускает LSP-сервер и стримит + сообщения в Neovim. +- **`mason.nvim`** — менеджер: ставит LSP-серверы, форматтеры, линтеры + (`:Mason` — UI). + +AstroNvim из коробки включает оба этих "транспортных" слоя. Остаётся через +Mason поставить конкретные **языковые** инструменты для Python. + +### Аналогия с Docker (урок 07) + +То же устройство, что у Docker: + +| Docker | Neovim LSP | +|---|---| +| `dockerd` (демон, делает работу) | `pyright` / `basedpyright` (LSP-сервер) | +| `docker` CLI (клиент) | `nvim-lspconfig` (плагин-клиент) | +| `apt` (ставит docker) | Mason (ставит pyright) | + +Конструкция знакомая. + +## Что ставится для Python в AstroNvim + +В файле `~/.config/nvim/lua/community.lua` после снятия предохранителя +(`if true then return {} end`) и добавления python-pack: + +```lua +return { + "AstroNvim/astrocommunity", + { import = "astrocommunity.pack.lua" }, + { import = "astrocommunity.pack.python" }, +} +``` + +`astrocommunity.pack.python` — это **готовая связка плагинов и Mason-конфигов** +для Python. При первом запуске после правки: + +1. `lazy.nvim` качает Python-специфичные плагины (`venv-selector.nvim` — + переключение venv'ов; `neotest-python` — запуск pytest). +2. Mason при следующем запуске ставит бинарники LSP-серверов, форматтеров, + тайпчекеров. + +### Что реально ставит python-pack (на 2026-05) + +| Инструмент | Роль | Что используем | +|---|---|---| +| `basedpyright` | LSP — тайпчекинг, автокомплит | **Да** (форк pyright, активнее обновляется) | +| `ruff` | LSP — линтер + форматтер | **Да** (один инструмент для всего стиля) | +| `pyrefly` | Тайпчекер от Meta (Rust) | Нет, экспериментальный — ставится для сравнения | +| `ty` | Тайпчекер от Astral (Rust) | Нет, экспериментальный | +| `debugpy` | DAP — отладчик | Понадобится на уроке 12 фазы 0 | +| `black` | Форматтер | Нет, `ruff format` его заменяет | +| `isort` | Сортировщик импортов | Нет, `ruff` это умеет | + +Pack ставит лишнее «на всякий случай», чтобы ты потом выбрал. По умолчанию +используем `basedpyright` + `ruff`. + +### Почему именно ruff + +Раньше Python-мир был зоопарком: Black форматировал, flake8 линтил, isort +сортировал импорты, pylint искал баги — у каждого свой процесс, свой конфиг, +свои конфликты. + +**Ruff** написан на Rust одной командой (Astral — те же, кто сделал `uv`). +Он покрывает 99% правил flake8 + isort + ещё пары десятков плагинов, умеет +форматировать как Black, и стартует за миллисекунды. + +**`basedpyright` + `ruff` не пересекаются:** +- `basedpyright` отвечает за **типы**. +- `ruff` отвечает за **стиль и форматирование**. + +Это два разных уровня контроля, и они уживаются рядом. + +## AI-specific settings (блок 3 урока) + +Урок (en.md) рекомендует 5 настроек редактора для AI-работы. В Neovim +они либо уже работают, либо настраиваются одной правкой `astrocore.lua`: + +| Настройка | Откуда в Neovim | +|---|---| +| `editor.formatOnSave: true` | **Из коробки** — `conform.nvim` из AstroNvim вызывает `ruff format` при `:w` | +| Type checking | **Из коробки** — `basedpyright` по умолчанию в режиме `basic` | +| Rulers (вертикальные линии) | Добавляется как `colorcolumn = "88,120"` в `~/.config/nvim/lua/plugins/astrocore.lua`, секция `options.opt` | +| Notebook output scrolling | N/A — это про VSCode Jupyter, в Neovim см. блок 4 | +| Auto-save afterDelay | Не делаем. В Neovim `w` (write) — одна клавиша, мгновенно. Autosave в vim-культуре считается шумным, добавлять не нужно | + +### Что про `colorcolumn` нужно знать + +- `88` — ширина, на которой обрывает строки `ruff format` (как Black). +- `120` — soft-граница для комментариев и докстрингов. + +### Что я понял на этом блоке + +- В файле `~/.config/nvim/lua/plugins/astrocore.lua` (как и в `community.lua`) + на первой строке стоит **killswitch**: `if true then return {} end`. Пока он + там — весь файл при загрузке возвращает пустую таблицу, и никакие настройки + не применяются. Нужно удалить эту строку, чтобы конфиг ожил. +- Структура опций: `opts.options.opt.`. Все обычные vim-опции + (`relativenumber`, `wrap`, `colorcolumn` и т.д.) лежат именно тут. +- `opts.options.g` — для глобальных vim-переменных (`vim.g.<...>`), не для + опций редактора. Случайно положить туда `colorcolumn = "88,120"` — + получить ошибку от lua_ls `Cannot assign 'string' to 'table'`. + +### `relativenumber` — что это и зачем + +В AstroNvim по умолчанию включён `relativenumber = true`. На текущей строке +показывается её абсолютный номер, а на соседних — **расстояние** от курсора. +Удобно для motion-команд: видишь `7` строк до нужного места — нажимаешь +`7k` или `7j` и сразу там. + +Если поначалу путает (как путало меня при правке `astrocore.lua`) — можно +отключить, добавив `relativenumber = false` в `opt = {...}`. Но привыкание +обычно стоит того. + +## Jupyter workflow в Neovim (блок 4 урока) + +В VSCode для работы с `.ipynb` ставят `ms-toolsai.jupyter` — ячейки, выводы, +графики прямо внутри редактора. В Neovim это **открытый вопрос** с тремя +разными ответами разной сложности: + +| Подход | Что | Когда брать | +|---|---|---| +| **1. Jupyter в браузере из nvim-терминала** | `F7` → `jupyter lab` → работа в браузере, как обычно | По умолчанию. Сейчас | +| **2. `.py` с `# %%` ячейками + REPL** | Файл с маркерами `# %%` (jupytext-совместимо), отправка блоков в IPython через `iron.nvim` / `vim-slime` | Когда захочется git-friendly формат и не нужны inline-картинки | +| **3. `molten-nvim` с inline-графикой** | Полная замена Jupyter UI в Neovim. Графики рендерятся **прямо в окне** | Требует терминал с графическим протоколом (`kitty`, `wezterm`, `ghostty`). Windows Terminal **не подходит** | + +### Что выбираем сейчас + +**Подход 1.** Запускаем `jupyter lab` из `F7`-терминала AstroNvim, работаем +в браузере. Jupyter из урока 06 уже установлен в `.venv` курса +(`jupyterlab 4.5.7`, `IPython 9.13.0`). + +Подходы 2/3 поставим в фазах 2-3, когда **реально** часто понадобится +переключаться между скриптами и тетрадями. Это just-in-time по +[`feedback_just_in_time_learning.md`](../../../.claude/projects/.../memory/feedback_just_in_time_learning.md). + +## Terminal + Remote (блок 5 урока) + +### Terminal в AstroNvim — три способа + +| Шорткат | Что | +|---|---| +| `tf` или `F7` | Floating terminal (плавающее окно) | +| `th` | Horizontal split — терминал снизу | +| `tv` | Vertical split — терминал справа | + +Выход из terminal mode в normal: `Ctrl+\` → `Ctrl+n`. + +Полезный сценарий из урока — два терминала: в одном `python train.py`, в +другом `watch -n 1 nvidia-smi`. Это `th` + `tv` параллельно. + +### Remote development — отложено + +В VSCode для удалённой работы ставят `ms-vscode-remote.remote-ssh`. В Neovim: + +- **Базовый путь:** `ssh user@gpu-box` → `nvim` уже на той стороне. Работает + без настройки и подходит для подавляющего большинства случаев. +- **Аналог Remote-SSH:** `distant.nvim` — плагин, который открывает + удалённые файлы как локальные. В курсе не нужен. + +Sсh-ключи и `~/.ssh/config` (с алиасами вроде `Host gpu-box ...`) — настроим +тогда, когда появится реальная удалённая машина. До этого момента — заметка +на полях. + +## Что закрыто и что отложено + +| Часть урока | Статус | +|---|---| +| Base editor + extensions (для AstroNvim это lazy.nvim plugins + Mason LSP) | ✓ | +| Python LSP (basedpyright + ruff) | ✓ | +| Format-on-save | ✓ — работает из коробки | +| Type checking | ✓ — basedpyright `basic` | +| Rulers (88, 120) | ✓ — `colorcolumn` в astrocore.lua | +| Autosave | Сознательно пропущен — `w` достаточно | +| Jupyter | ✓ подход 1 (browser); подходы 2/3 — отложены до классического ML | +| Terminal integration | ✓ из коробки (`F7`, `tf/th/tv`) | +| Remote SSH | Отложено — нет удалённой машины | +| Debugger (DAP) | `debugpy` установлен Mason'ом, но настройка — на уроке 12 фазы 0 | +| Lint-on-save отдельно | Не нужно — ruff и линтит, и форматирует | + +## Шорткаты, которые осели после урока + +| Шорткат | Что | +|---|---| +| `K` (normal) | Hover-документация под курсором | +| `gd` (normal) | Go-to-definition | +| `` (normal) | Назад после прыжка | +| `dd` (normal) | Вырезать строку | +| `o` (normal) | Открыть новую строку под курсором + insert | +| `O` (normal) | То же, но над | +| `cw` (normal) | Change word — заменить слово | +| `I` (normal) | Insert в начало строки (первый non-whitespace) | +| `:Mason` | UI менеджера LSP / форматтеров / линтеров | +| `:LspInfo` | Какие LSP-серверы прицеплены к буферу | +| `:checkhealth ` | Диагностика конкретного компонента | +| `F7` / `tf` | Плавающий терминал | +| `Ctrl+\` `Ctrl+n` | Выйти из terminal-mode в normal | + +## Что важно вынести из урока + +1. **VSCode-расширение ≠ Neovim-плагин.** В Neovim "расширение" — это два + разных уровня (plugin + LSP-server), которые соединяются через + `nvim-lspconfig` и менеджатся через Mason. Конструкция та же, что у + Docker (демон + клиент + менеджер пакетов). +2. **AstroNvim даёт готовый transport-слой**, но языковые серверы (pyright, + ruff и т.д.) активируются через **astrocommunity packs**. +3. **Killswitch `if true then return {} end`** — стандартная мина AstroNvim + template'а. Любой файл в `~/.config/nvim/lua/plugins/` или `lua/community.lua`, + куда вносишь правки, начинается с этой строки. Снять — обязательно. +4. **`opts.options.opt` ≠ `opts.options.g`.** Первое — vim options (типы + валидируются), второе — vim global variables (свободная форма). Положить + `colorcolumn` в `g` — мгновенно получить ошибку от lua_ls. +5. **Format-on-save + type checking — из коробки** после активации + python-pack. Не надо ничего настраивать вручную. diff --git a/phases/00-setup-and-tooling/09-data-management/docs/ru.md b/phases/00-setup-and-tooling/09-data-management/docs/ru.md new file mode 100644 index 000000000..afe0daec2 --- /dev/null +++ b/phases/00-setup-and-tooling/09-data-management/docs/ru.md @@ -0,0 +1,196 @@ +# Урок 9 — Управление данными (Data Management) + +> Параллельный конспект к `en.md`. По ходу урока обновляется ответами на +> углубляющие вопросы и пометками про CRM-аналогии. + +## Зачем этот урок + +Любой AI/ML-проект начинается с данных. До модели нужно уметь: + +1. **Найти и загрузить** датасет (Hugging Face Hub — стандарт де-факто). +2. **Понимать форматы** хранения (CSV / JSON / Parquet / Arrow) и зачем их + столько вообще. +3. **Бить выборку на сплиты** (train / val / test) с фиксированным seed — + чтобы эксперимент был воспроизводим. +4. **Не пихать тяжёлые файлы в git** (модели, выгрузки) — .gitignore / Git LFS / DVC. + +Этот урок — про **инфраструктуру данных**, без неё дальнейшие фазы (классический +ML, NLP, LLM) будут опираться на ручную возню «скачал-распаковал-забыл-какой-seed-был». + +## CRM-аналогия (для якоря) + +| AI/ML | CRM/маркетинг | +|---|---| +| Датасет | Выгрузка из CRM (контакты, события, заказы) | +| Hugging Face Hub | Что-то типа "маркетплейса выгрузок" (только публичных) | +| `load_dataset(..., streaming=True)` | Не выгружать всю базу разом, а тащить пачками по 10к | +| CSV → Parquet | Экспорт из Excel-выгрузки в колоночный формат для BI/ClickHouse | +| Train / Val / Test split | Контрольная и тестовая группа в A/B-тесте, holdout-когорта | +| `seed=42` | Зафиксированный список ID-контактов, который любой коллега может воспроизвести | +| Git LFS / DVC | Версионирование "среза базы на 12 марта 2025" — чтобы потом доказать на каком именно срезе крутили модель | + +## План конспекта + +Заполняется по мере прохождения. Заголовки добавляю одновременно с тем, как +прохожу соответствующий блок теории. + +- [x] Блок 1. Hugging Face datasets — что грузим и куда оно кэшируется +- [ ] Блок 2. Форматы файлов: CSV / JSON / Parquet / Arrow +- [ ] Блок 3. Streaming vs полное скачивание +- [ ] Блок 4. Train/Val/Test split и seed +- [ ] Блок 5. Большие файлы и git (.gitignore / LFS / DVC) +- [ ] Финал — разбор `code/data_utils.py` как референса + +## Блок 1. Библиотека `datasets` + +### Что это +Hugging Face'овский клиент к их Hub. Под капотом — **Apache Arrow**: данные +лежат на диске в колоночном бинарном формате и **мапятся в память** (memory +mapping), а не копируются в RAM. Отсюда два плюса: гигабайтный датасет «открывается» +мгновенно и потребляет мало памяти. + +Главная ценность — единый интерфейс. Откуда бы данные ни приехали (Hub, локальный +CSV, pandas, генератор), наружу торчит одна и та же `Dataset`-абстракция. + +### Главные сущности + +| Класс | Это что | Когда возникает | +|---|---|---| +| `Dataset` | Одна таблица (один сплит). `len`, индексация, срезы. | `load_dataset(..., split=...)` или вручную | +| `DatasetDict` | Словарь сплитов: `{"train": Dataset, "test": Dataset}` | `load_dataset(...)` без `split=` | +| `IterableDataset` | Тот же датасет, но только итерируется. Нет `len`, нет `[i]`. | `streaming=True` | +| `Features` | Схема таблицы: `{колонка: тип}` с богатыми типами (`ClassLabel`, `Image`, `Audio`, `Sequence`). | `ds.features` у любого Dataset | + +CRM-параллель: `Dataset` ≈ файл-выгрузка, `DatasetDict` ≈ папка из трёх +выгрузок (train/val/test), `IterableDataset` ≈ курсор по SQL-запросу / +поток событий Kafka, `Features` ≈ схема таблицы в БД. + +### Типичные операции + +Загрузка: +```python +load_dataset("repo") # DatasetDict +load_dataset("repo", split="train") # Dataset +load_dataset("repo", "config_name", split="train") +load_dataset("repo", split="train", streaming=True) # IterableDataset +``` + +Инспекция (это будешь делать постоянно): +```python +ds.column_names +ds.features +len(ds) +ds[0] # одна строка как dict +ds[:5] # ВНИМАНИЕ: dict со СПИСКАМИ, не список dict'ов +``` + +Преобразования (ленивые, с кэшем результата): +```python +ds.map(fn) # +колонки, преобразование +ds.filter(fn) +ds.shuffle(seed=42) +ds.select(range(100)) +ds.train_test_split(test_size=0.2, seed=42) +ds.sort(col); ds.rename_column(a, b); ds.remove_columns([...]); ds.cast_column(...) +``` + +Экспорт: +```python +ds.to_pandas() / .to_csv / .to_json / .to_parquet / .to_list() +``` + +Импорт извне: +```python +Dataset.from_pandas(df) / .from_dict(d) / .from_csv(path) / .from_parquet(path) / .from_generator(gen) +``` + +### Streaming-режим + +`IterableDataset` устроен принципиально иначе: +- нет `len`, нет индексации; +- только `for row in ds`, `ds.take(n)`, `ds.skip(n)`; +- `map`/`filter`/`shuffle(buffer_size=...)` ленивые, применяются на лету; +- `shuffle` — буферный (как в TF Dataset), не полный. + +Используется, когда данных слишком много, чтобы хранить локально (Common Crawl, C4). + +### Кэш + +Всё кэшируется в `~/.cache/huggingface/datasets/`: +- сами таблицы в `*.arrow`-файлах; +- метаданные (`dataset_info.json`, `state.json`); +- результаты `map(...)` тоже кэшируются по fingerprint функции и входа. + +Поэтому второй запуск той же ячейки — мгновенный. Force-перекачка: +`load_dataset(..., download_mode="force_redownload")`. + +### Грабли, которые легко словить + +1. **`ds[:5]` — это dict со списками, не список словарей.** Arrow колоночный. + Если нужен список строк — `[ds[i] for i in range(5)]` или + `ds.select(range(5)).to_list()`. +2. **`load_dataset` без `split=` возвращает `DatasetDict`.** На нём `len(...)`, + `ds[0]`, `column_names` упадут — это словарь, а не таблица. Сначала достань сплит: + `ds["train"]`. +3. **`shuffle` не бесплатный** на больших `Dataset` — переписывает Arrow-файл. + Лучше один раз перетасовать и закэшировать, чем дёргать в каждом запуске. +4. **`map(..., batched=True)` в разы быстрее**, если функция векторизуется. + Дефолт — `batched=False`, и легко не заметить. +5. **У `IterableDataset` нет `len`.** При тренировке считают шаги, а не эпохи. +6. **Gated-датасеты** требуют `huggingface-cli login` или `HF_TOKEN`. Публичные + (как `rotten_tomatoes`, `imdb`) работают без логина. + +### Что почитать, чтобы закрепить +- `help(datasets.load_dataset)` — все параметры (`path`, `name`, `split`, + `streaming`, `cache_dir`, `download_mode`, `revision`). +- `dir(ds)` — все методы у конкретного датасета. +- Dataset Viewer прямо в браузере на странице датасета на Hub — посмотреть схему + и пару строк **до** загрузки. + +--- + +## Закладка: где остановились (2026-05-21) + +**Сделано:** +- Установлены `datasets` + `huggingface_hub` через `uv pip install` (поймана + ловушка PEP 668 в uv-venv — занесена в memory). +- Разобран Блок 1 теории целиком (выше в этом файле). + +**В подвешенном состоянии:** +- В ноутбуке `scratch/phase-0/lesson-09/01_first_dataset.ipynb` запущено + `load_dataset("Qwen/WebWorldData", split="train")` без `streaming=True`. + Это 52.2 ГБ одним JSONL. Pavel начал прерывание; на момент паузы НЕ + подтверждено что: + 1. Kernel реально прерван (`Kernel → Interrupt Kernel` или Restart). + 2. Каталог `~/.cache/huggingface/hub/datasets--Qwen--WebWorldData/` + удалён (в WSL под `pavel`, а не в Git Bash под Windows-юзером). + +**Что сделать при возвращении (по порядку):** + +1. В WSL-терминале: + ```bash + du -sh ~/.cache/huggingface/ + ls -la ~/.cache/huggingface/hub/ | grep -i webworld + ``` +2. Удалить недокачку: + ```bash + rm -rf ~/.cache/huggingface/hub/datasets--Qwen--WebWorldData + rm -rf ~/.cache/huggingface/datasets/downloads/*.incomplete + ``` +3. Подтвердить освобождение места: `du -sh ~/.cache/huggingface/` и `df -h ~`. +4. Решить, как идти дальше для упражнения 1: + - **A.** Тот же `Qwen/WebWorldData` со `streaming=True`. Тогда упражнение 1 + закроется не полностью: у `IterableDataset` нет `len()` и индексации. + - **B.** Мелкий датасет: `rotten_tomatoes` (~8 МБ), `imdb` (~84 МБ), + `glue/mrpc` (~1 МБ), `ag_news` (~30 МБ). На нём пройти весь набор + операций (`len`, `ds[0]`, `column_names`, `features`). + - Рекомендация: сначала **B** (быстро уложить механику API), потом + отдельно поиграть с **A** в Блоке 3 (streaming). + +**Что осталось по плану урока:** +- Блок 2 — форматы (CSV / JSON / Parquet / Arrow) и их трейдоффы. +- Блок 3 — streaming подробно (продолжение Блока 1, но руками). +- Блок 4 — train/val/test split + seed. +- Блок 5 — большие файлы и git (.gitignore / Git LFS / DVC). +- Финал — `code/data_utils.py` как референс. +- Упражнения 1-4 из `en.md` + опциональный quiz. diff --git a/phases/01-math-foundations/01-linear-algebra-intuition/docs/ru.md b/phases/01-math-foundations/01-linear-algebra-intuition/docs/ru.md new file mode 100644 index 000000000..655014682 --- /dev/null +++ b/phases/01-math-foundations/01-linear-algebra-intuition/docs/ru.md @@ -0,0 +1,448 @@ +# Линейная алгебра — интуиция + +> Каждая AI-модель — это просто матричная математика в красивой шляпе. + +**Тип:** Изучение +**Языки:** Python (Julia пропускаем) +**Предтечи:** Фаза 0 +**Время:** ~60 минут (но мы идём вдумчиво маленькими кусками) + +## Зачем этот урок + +Открой любую статью по машинному обучению — на первой странице будут векторы, матрицы, скалярные произведения, преобразования. Без интуиции в линейной алгебре это просто символы. С интуицией — видно, что **на самом деле** делает нейросеть: двигает точки в пространстве. + +Не нужно быть математиком. Нужно научиться **видеть геометрию** за операциями и **писать их руками** в коде, прежде чем доверить numpy. + +## Глоссарий главных терминов + +Русско-английский словарь — чтобы термины узнавались и в русских, и в английских источниках. + +| Английский | Русский | Что это | +|---|---|---| +| vector | вектор | список чисел, который интерпретируется как точка или стрелка в пространстве | +| dimension | размерность | количество чисел в векторе | +| scalar | скаляр | просто число (не вектор) | +| dot product / scalar product | скалярное произведение | сумма поэлементных произведений; одно число | +| magnitude / norm / length | длина / норма / магнитуда | насколько «далеко» вектор тянется от начала координат | +| L2 norm / Euclidean norm | евклидова норма (L2-норма) | классическая длина через `√(сумма квадратов)` | +| cosine similarity | косинусное сходство | мера похожести двух векторов через косинус угла между ними | +| scale invariance | масштабная инвариантность | свойство «не меняется при умножении на положительное число» | +| embedding | эмбеддинг (векторное представление) | способ превратить объект — слово, картинку, клиента — в вектор чисел | +| attention | механизм внимания | часть архитектуры трансформеров, основанная на скалярных произведениях | +| RAG | поиск-дополненная генерация | подход, где LLM ищет документы во внешней базе перед ответом; поиск работает через скалярные произведения | +| LoRA | низкоранговая адаптация | способ дообучения больших моделей; обновляет не всю матрицу весов, а её низкоранговое приближение | +| traceback | трассировка стека вызовов | многострочный вывод Python при ошибке | +| silent corruption | молчаливая порча данных | баг, при котором функция «работает» с неправильными входами, но возвращает мусор без падения | + +## Блок 1.1 — Что такое вектор + +**Вектор — это просто список чисел.** Например `[3, 2]`. Всё. Никакой магии. + +Фокус в том, **что эти числа означают**: + +1. **Как точка в пространстве** — для `[3, 2]` это точка с координатами (x=3, y=2) на плоскости. +2. **Как стрелка** — стрелка из начала координат `(0, 0)` до этой точки. У стрелки есть **направление** (куда смотрит) и **длина**. + +Оба прочтения — про один и тот же вектор. Геометры чаще говорят «стрелка», программисты — «точка». Это одно и то же. + +``` +y +↑ +3 | +2 | • [3, 2] +1 | ╱ +0 +────────→ x + 0 1 2 3 +``` + +**В AI вектора представляют что угодно:** + +- слово → 768-мерный вектор смысла (эмбеддинг BERT'а); +- картинка → вектор из миллионов пикселей; +- клиент CRM → вектор предпочтений по нескольким признакам. + +## Блок 1.2 — Размерность вектора + +**Размерность = просто количество чисел в векторе.** В Python — буквально `len(v)`. + +``` +[3, 2] → 2D вектор (двумерный) +[1, -2, 3, 0, 7] → 5D вектор (пятимерный) +[a₁, ..., a₇₆₈] → 768D вектор +``` + +**Что меняется при росте размерности:** + +- **2D** — рисуем на плоскости, видим глазами. +- **3D** — представляем как куб. +- **4D и выше** — **нарисовать нельзя.** Никак. Но математика работает абсолютно так же. + +Это важный сдвиг в голове: **геометрическая интуиция остаётся, даже когда картинка исчезает.** Дальше в курсе будем оперировать векторами в 768, 4096 и больше измерений — никто их не рисует, но **думают** про них как про точки и стрелки. + +**CRM-аналогия:** RFM-сегментация (Recency, Frequency, Monetary) — это вектор в 3D на клиента. Если добавить NPS, средний чек, дни до возврата, лояльность — клиент становится 7-мерным вектором. ML просто доводит эту мысль до абсурда: «давай опишем клиента 768 числами, пусть модель сама разберётся». + +**Где встречается:** +- Word embedding BERT — 768D на одно слово. +- OpenAI `text-embedding-3-large` — 3072D на кусок текста. +- LLaMA/GPT внутри — 4096D и больше. + +## Блок 1.3 — Базовые операции + +**Размерности должны совпадать.** Складывать векторы разной длины математически нельзя — операция не определена. Не «дополним нулями и сложим» — это совершенно другая операция, которая **меняет смысл** (см. ниже про «нормализовать ≠ выровнять размерность»). + +**Три базовые операции:** + +| Операция | Формула | Геометрия | +|---|---|---| +| Сложение | `[a₁, a₂] + [b₁, b₂] = [a₁+b₁, a₂+b₂]` | «шаг A потом шаг B — куда придёшь» | +| Вычитание | `[a₁, a₂] - [b₁, b₂] = [a₁-b₁, a₂-b₂]` | направление «от B до A» | +| Умножение на скаляр | `k · [a₁, a₂] = [k·a₁, k·a₂]` | растянуть/сжать. k=2 → длиннее в 2 раза, k=-1 → развернуть | + +**Сложение покоординатно.** `[3, 2] + [1, 4] = [4, 6]`. Геометрически: ставишь хвост второй стрелки в голову первой, идёшь — приходишь в `(4, 6)`. + +**Где это в AI:** + +- **Арифметика слов** — знаменитый пример: `vec("король") - vec("мужчина") + vec("женщина") ≈ vec("королева")`. Это **буквальное** сложение и вычитание векторов в 768-мерном пространстве эмбеддингов. +- **Градиентный спуск** — на каждом шаге обучения: `weights = weights - learning_rate · gradient`. Это вычитание векторов + умножение на скаляр. + +### Терминологическая ловушка: «нормализовать» ≠ «выровнять размерность» + +Две разные операции, которые часто путают: + +- **Привести к одной размерности** — подгонка количества координат. У одного 3 числа, у другого 2 → дополняем, обрезаем. Это про **сколько** координат. +- **Нормализовать вектор** — деление вектора на его длину, чтобы получился вектор **длины 1**. Размерность при этом не меняется. Это про **масштаб**. + +Когда в ML говорят «нормализуй фичи» — это **никогда** не про размерность. + +## Блок 1.4 — Падать громко: `raise` + traceback + +**Главная идея:** функция должна **падать сразу**, если получила «не те» входы. Иначе будет **silent corruption** — функция вернёт «правдоподобный» мусор, баг проявится далеко от места возникновения. + +**Пример:** Python `zip(a, b)` молча обрезает по короткой последовательности. Если передать `[1, 2, 3]` и `[10, 20]` в `add` без проверки — получим `[11, 22]`, и третий элемент `3` бесследно потеряется. Это и есть silent corruption. + +**Решение — явная проверка + `raise ValueError`:** + +```python +def add(a: list[float], b: list[float]) -> list[float]: + if len(a) != len(b): + raise ValueError(f"разные размерности векторов: {len(a)} и {len(b)}") + return [a_i + b_i for a_i, b_i in zip(a, b)] +``` + +- **`raise`** — keyword, который «бросает» исключение. Выполнение функции прерывается, управление поднимается вверх по стеку вызовов, пока его не поймают через `try/except` — или пока программа не упадёт целиком с traceback. +- **`ValueError`** — стандартный тип исключения для случая «тип аргумента правильный, но значение не подходит». Для неправильного типа есть `TypeError`. +- **f-string в сообщении ошибки** — `f"...{len(a)}..."` подставляет реальные значения, чтобы при отладке было видно, что именно пришло. + +**Альтернатива через `zip(a, b, strict=True)`** — Python 3.10+ умеет сам проверять одинаковые длины внутри zip и кидать `ValueError`. Удобно как дополнительная страховка («belt and suspenders»). Но если хочется **своё** сообщение об ошибке — оставлять свою проверку. + +### Как читать traceback + +``` +Traceback (most recent call last): + File ".../vector.py", line 43, in + print(f"разная длина: {add([1.0, 2.0, 3.0], [10.0, 20.0])}") + File ".../vector.py", line 23, in add + raise ValueError(f"разные размерности векторов: {len(a)} и {len(b)}") +ValueError: разные размерности векторов: 3 и 2 +``` + +Заголовок наверху: **`most recent call last`** — то есть **читай снизу вверх**, чтобы найти место ошибки, потом поднимайся, чтобы понять путь. + +Снизу вверх: + +1. **`ValueError: ...`** — собственно ошибка. Тип + сообщение. +2. **`File ..., line 23, in add`** — где случился `raise`. +3. **`File ..., line 43, in `** — кто вызвал `add`. `` = верхний уровень файла. + +Если стек глубже (`main → process → add`), traceback показывает все слои. Это карта: где начался путь, где закончился ошибкой. + +## Блок 1.5 — Скалярное произведение (dot product) + +**Самая важная операция во всей линейной алгебре с точки зрения AI.** На ней стоят: + +- attention в трансформерах (что важно для GPT); +- RAG-поиск (находим похожие документы); +- косинусное сходство (рекомендалки, кластеризация); +- линейные слои нейросетей (это буквально скалярные произведения входа на строки матрицы весов). + +**Формула** для векторов **одинаковой размерности**: + +``` +a · b = a₁·b₁ + a₂·b₂ + ... + aₙ·bₙ +``` + +Перемножаем покоординатно, складываем результаты в **одно число**. + +**Пример:** `[1, 2, 3] · [4, 5, 6] = 1·4 + 2·5 + 3·6 = 32`. + +**Очень важно:** в отличие от сложения, результат — одно число (**скаляр**), не вектор. Отсюда английский термин **scalar product**. + +**Геометрический смысл — мера «смотрят ли в одну сторону».** Знак результата: + +| Результат | Угол | Смысл | +|---|---|---| +| `a · b > 0` | < 90° | смотрят в одну сторону | +| `a · b = 0` | = 90° | строго перпендикулярны, независимы | +| `a · b < 0` | > 90° | смотрят в разные стороны | + +**CRM-аналогия:** скалярное произведение двух клиентских профайлов измеряет, насколько похожи паттерны их поведения. Но **зависит от масштаба** — длинные векторы дают большие числа, даже если направления так себе. Поэтому в реальности используют **косинусное сходство** (см. блок 1.7), которое от масштаба не зависит. + +## Блок 1.6 — Длина вектора (magnitude) + +**Длина вектора** — насколько далеко он тянется от начала координат. + +``` +|v| = √(v₁² + v₂² + ... + vₙ²) +``` + +Каждое число в квадрат, складываем, берём корень. В 2D это **теорема Пифагора**: вектор `[3, 4]` имеет длину `√(9+16) = 5`. + +### Главный фокус: длина через скалярное произведение + +``` +v · v = v₁² + v₂² + ... + vₙ² → v · v = |v|² +``` + +Значит: + +``` +|v| = √(v · v) +``` + +В коде — две строки, переиспользующие `dot`: + +```python +def magnitude(v: list[float]) -> float: + return math.sqrt(dot(v, v)) +``` + +В реальных библиотеках (`np.linalg.norm`) внутри ровно эта связка. + +### Имена термина + +Встретишь все три — это одно и то же: +- **magnitude** — повседневный английский; +- **length** — буквально «длина»; +- **norm / L2 norm / Euclidean norm** — математически. L2 = «корень из суммы квадратов»; есть и другие нормы (L1, L∞), но это позже. + +### Свойство, важное для следующего блока + +Если умножить вектор на число `k`, его длина умножается на `|k|`: + +``` +|k · v| = |k| · |v| +``` + +Вывод: +``` +|[k·v₁, k·v₂]| = √((kv₁)² + (kv₂)²) + = √(k²·(v₁² + v₂²)) + = √(k²) · √(v₁² + v₂²) + = |k| · |v| +``` + +Ключевой шаг — вынос `k²` из-под корня: `√(k²) = |k|` (модуль, потому что для отрицательных k длина всё равно положительна). + +**Зачем это правило в AI:** + +- **Нормализация** — деление на свою длину даёт вектор длины 1. +- **Косинусное сходство** — следующий блок, и эта инвариантность лежит в основе. +- **Расстояние между точками** — `|a - b|`. Используется в KNN, кластеризации. + +## Блок 1.7 — Косинусное сходство (cosine similarity) + +**Скалярное произведение зависит от длин векторов** — длинные дают большие числа, даже если направления плохо совпадают. Это путает «правда похожи» с «просто оба большие». + +**Решение: разделить на произведение длин.** Это убирает влияние масштаба — остаётся только «насколько смотрят в одну сторону». + +**Формула:** + +``` +cos_sim(a, b) = (a · b) / (|a| · |b|) +``` + +### Почему называется «косинусное» + +Из тригонометрии: `a · b = |a| · |b| · cos(θ)`, где θ — угол между векторами. Переворачиваем: + +``` +cos(θ) = (a · b) / (|a| · |b|) +``` + +То есть результат — **буквально косинус угла между двумя векторами**. + +**Диапазон всегда от -1 до +1:** + +| Значение | Угол | Смысл | +|---|---|---| +| `+1` | 0° | смотрят в одну сторону (полное сходство) | +| `0` | 90° | перпендикулярны (никакой связи) | +| `-1` | 180° | противоположны (полная противоположность) | + +### Масштабная инвариантность (scale invariance) + +Главное преимущество косинусного сходства над просто скалярным произведением: **умножение на положительное число не меняет результат.** + +``` +cos_sim(k·a, b) = (k·(a·b)) / (|k|·|a|·|b|) +``` + +При `k > 0` всё сокращается → результат тот же. Поэтому `cos_sim([1, 0], [10, 0]) = 1.0` — те же векторы по направлению. + +### CRM-аналогия + +Старый клиент A (десятки покупок) и новый B (всего пара). Профайлы: + +``` +A: [50, 20, 15, 10] +B: [5, 2, 1.5, 1] # тот же паттерн, но новый — числа меньше +``` + +`B = A / 10`. Скалярное произведение спутает их с разными клиентами, потому что числа в B маленькие. **Косинусное сходство покажет ровно 1.0** — один и тот же тип клиента на разных стадиях жизненного цикла. + +Это и есть **базовый инструмент рекомендалок и сегментации**: найти клиентов с похожим паттерном, не путаясь в масштабах. + +### Защита от нулевого вектора + +Длина нулевого вектора `[0, 0, ...]` равна нулю → деление на ноль → математически не определено. Поэтому в `cosine_similarity` нужна проверка: + +```python +def cosine_similarity(a: list[float], b: list[float]) -> float: + if magnitude(a) == 0 or magnitude(b) == 0: + raise ValueError("передан нулевой вектор") + return dot(a, b) / (magnitude(a) * magnitude(b)) +``` + +### Применения в AI + +- **RAG** — векторизуют запрос и каждый документ. Сравнивают `cos_sim(запрос, документ)` для всей базы, возвращают топ-K. +- **Рекомендалки** — товар × товар матрица косинусных сходств. «Кто покупал X, покупал и Y». +- **Word embeddings** — `cos_sim(vec("король"), vec("королева")) ≈ 0.7-0.9` в типичной модели. + +## Блок 1.8 — Арифметика с плавающей точкой (floating-point) + +При проверке `cosine_similarity([1, 2], [-1, -2])` получили `-0.9999999999999998` вместо ровно `-1.0`. Это **не баг твоего кода** — это фундаментальная природа типа `float`. + +### Почему так + +`float` (точнее `float64`) хранит числа в **двоичном формате** ограниченной длины. **Большинство десятичных дробей не представимы точно в двоичной системе** — как 1/3 не представима точно в десятичной (0.333... с бесконечным хвостом). + +```python +print(0.1 + 0.2) # 0.30000000000000004 +print(0.1 + 0.2 == 0.3) # False +``` + +Это так в любом языке (C, JavaScript, Java, Python, NumPy, PyTorch). Все используют один стандарт IEEE 754. + +### Что произошло у нас + +1. `dot = 1·(-1) + 2·(-2) = -5.0` — точно (целые умножаются точно). +2. `magnitude = √5 ≈ 2.2360679...` — а вот **√5 нельзя точно** представить в float64. +3. `magnitude · magnitude` — теоретически 5.0, но из-за округлений получается чуть больше или чуть меньше. +4. `-5.0 / 5.000000000000001 = -0.9999999999999998`. + +Микро-ошибка в одном корне → микро-ошибка в произведении → видна в финале. Это и есть **накопление ошибок**. + +### Главное правило: НЕ сравнивай float через `==` + +```python +# ❌ Никогда +if cos_sim == 1.0: ... + +# ✅ Правильно +if math.isclose(cos_sim, 1.0): ... +``` + +`math.isclose(a, b)` — встроенная функция модуля `math`. Проверяет «достаточно ли близки» с допуском по умолчанию `1e-9` (одна миллиардная от большего числа). + +Ручной вариант через **эпсилон (ε)** — общепринятое имя «маленькой величины»: + +```python +EPSILON = 1e-9 +if abs(cos_sim - 1.0) < EPSILON: ... +``` + +### Где это в AI + +Везде, где есть деление, sqrt, exp, log: + +- **Нормализация** — `|v| == 1` → `math.isclose(magnitude(v), 1.0)`. +- **Ортогональность** — `dot == 0` → `abs(dot) < epsilon`. +- **Сходимость градиентного спуска** — «градиент == 0» → `|градиент| < tolerance`. +- **В numpy/torch** — `np.allclose(a, b)`, `torch.allclose(a, b)` — для целых массивов. + +В библиотеках есть параметры `rtol` (относительный допуск) и `atol` (абсолютный допуск) для тонкой настройки. + +### Косметика для вывода + +`:.4f` в f-string округляет при отображении: + +```python +print(f"cos: {cosine_similarity([1,2], [-1,-2]):.4f}") +# cos: -1.0000 +``` + +В памяти число всё ещё `-0.9999999999999998`, но глазу не видно. Округление **только при печати**, не в вычислениях. + +## Что уже написано в `my/vector.py` + +После блоков 1.1–1.8 в файле есть: + +```python +import math + +def dimension(vec: list) -> int: ... +def add(a: list[float], b: list[float]) -> list[float]: ... +def sub(a: list[float], b: list[float]) -> list[float]: ... +def scale(v: list[float], k: float) -> list[float]: ... +def dot(a: list[float], b: list[float]) -> float: ... +def magnitude(v: list[float]) -> float: ... +def cosine_similarity(a: list[float], b: list[float]) -> float: ... +``` + +Все функции с покоординатными операциями используют идиому: + +```python +[expr for a_i, b_i in zip(a, b)] +``` + +или для скалярного произведения: + +```python +sum(a_i * b_i for a_i, b_i in zip(a, b)) +``` + +(второе — это **generator expression** внутри `sum`, без квадратных скобок; не создаёт промежуточный список в памяти). + +Все функции с двумя векторами на входе проверяют размерности через `raise ValueError`. Для `magnitude` и `scale` проверки не нужны. + +`cosine_similarity` дополнительно защищён от нулевого вектора. + +## Что осталось пройти в уроке + +- **Нормализация** — функция `normalize(v) = v / magnitude(v)`. Получится вектор длины 1 в том же направлении. +- **Класс `Vector`** — упаковать `add/sub/dot/...` в объект с методами и операторами (`__add__`, `__sub__`, `__matmul__`). Это «Build It» шаг 1 в `en.md`. +- **Класс `Matrix`** — матричное умножение, транспонирование. «Build It» шаг 2. +- **Мини-слой нейросети** — `weights @ input_vector`. «Build It» шаг 3. +- **Linear independence + Gram-Schmidt** — проверка линейной независимости, ортогонализация. «Build It» шаг 5. +- **Use It** — то же самое через NumPy + PyTorch. Чтобы увидеть, что 5 строк numpy = 50 строк нашего кода. +- **6 упражнений** в конце `en.md`: + 1. `Vector.angle_between(other)` — угол в градусах. + 2. 2D scaling matrix, применить к `[1, 1]`. + 3. 5 случайных 50-мерных векторов → найти самые похожие по cosine. + 4. Проверить, что Gram-Schmidt даёт ортонормированный набор. + 5. 3×3 матрица ранга 2, объяснить геометрию. + 6. Проекция `[1, 2, 3]` на `[1, 1, 1]` — что это означает. +- **Quiz** — 5 вопросов из `quiz.json`. + +## Полезные моменты (грабли и навыки этой сессии) + +- **`print` vs `return`** — функция без `return` неявно возвращает `None`; LSP подсветит, если есть type hint. +- **`List` (с большой) vs `list` (с маленькой)** — в Python 3.9+ использовать встроенный `list` как type hint, без импортов из `typing`. +- **Опечатка `b_1` vs `b_i`** — `1` и `i` визуально похожи в моноширинных шрифтах; LSP «Undefined name» от таких опечаток — лучший друг. +- **`pass` в функции с type hint** — LSP ругается, что функция обещает вернуть значение, но возвращает `None`. Заглушка `pass` — только когда заведомо потом допишешь. +- **`raise ValueError(f"...")`** — стандартная идиома «функция отказывается работать с такими входами». +- **`zip(a, b, strict=True)`** — Python 3.10+ способ заставить zip самому проверять одинаковые длины. +- **`# noqa: <код>`** — игнорировать конкретное правило линтера на этой строке. +- **`float` нельзя сравнивать через `==`** — использовать `math.isclose` или `abs(a-b) < eps`. Главное правило ML-кода. +- **Читать traceback снизу вверх** — последняя строка снизу = тип и сообщение ошибки, выше = где случилось, ещё выше = откуда вызвано. diff --git a/phases/01-math-foundations/01-linear-algebra-intuition/my/vector.py b/phases/01-math-foundations/01-linear-algebra-intuition/my/vector.py new file mode 100644 index 000000000..d885a13a5 --- /dev/null +++ b/phases/01-math-foundations/01-linear-algebra-intuition/my/vector.py @@ -0,0 +1,72 @@ +import math + +vector_2 = [3, 2] +vector_5 = [-1, 3, 4, 7, 9] +x, y = vector_2 + +print(f"двумерный вектор: координата x = {x}, координата y = {y}") +print(f"пятимерный вектор: четвертая координата = {vector_5[3]}") + +# мини-задание + + +def dimension(vec: list) -> int: + return len(vec) + + +vector_768 = [0] * 768 +print(f"размерность вектора: {dimension(vector_768)}") + +# сложение, вычитание и скаляр веркторов + + +def add(a: list[float], b: list[float]) -> list[float]: + if len(a) != len(b): + raise ValueError(f"разные размерности векторов: {len(a)} и {len(b)}") + return [a_i + b_i for a_i, b_i in zip(a, b)] + + +def sub(a: list[float], b: list[float]) -> list[float]: + if len(a) != len(b): + raise ValueError(f"разные размерности векторов: {len(a)} и {len(b)}") + return [a_i - b_i for a_i, b_i in zip(a, b)] + + +def scale(v: list[float], k: float) -> list[float]: + return [elem * k for elem in v] + + +def dot(a: list[float], b: list[float]) -> float: + if len(a) != len(b): + raise ValueError(f"разные размерности векторов: {len(a)} и {len(b)}") + return sum(a_i * b_i for a_i, b_i in zip(a, b)) + + +def magnitude(v: list) -> float: + return math.sqrt(dot(v, v)) + + +def cosine_similarity(a: list[float], b: list[float]) -> float: + if magnitude(a) == 0 or magnitude(b) == 0: + raise ValueError("передан нулевой вектор") + return dot(a, b) / (magnitude(a) * magnitude(b)) + + +vector_2_1 = [2.0, 4.0] +vector_2_2 = [3.0, 3.0] + +print(f"сложение векторов: {add(vector_2_1, vector_2_2)}") +print(f"вычитание векторов: {sub(vector_2_1, vector_2_2)}") +print(f"умножение вектора на число: {scale(vector_2_1, 4.0)}") +# print(f"разная длина: {add([1.0, 2.0, 3.0], [10.0, 20.0])}") +print(f"скалярное произвдение: {dot([1.0, 2.0, 3.0], [4.0, 5.0, 6.0])}") +print(f"скалярное произвдение: {dot([1.0, 0.0], [0.0, 1.0])}") +print(f"скалярное произвдение: {dot([3.0, 4.0], [-3.0, -4.0])}") +print(f"длина вектора: {magnitude([3.0, 4.0])}") +print(f"длина вектора: {magnitude([1.0, 0.0])}") +print(f"длина вектора: {magnitude([0.0, 0.0])}") +print(f"длина вектора: {magnitude([1.0, 1.0, 1.0, 1.0])}") +print(f"косинусное сходство: {cosine_similarity([1.0, 2.0, 3.0], [1.0, 2.0, 3.0]):.4f}") +print(f"косинусное сходство: {cosine_similarity([1.0, 0.0], [10.0, 0.0]):.4f}") +print(f"косинусное сходство: {cosine_similarity([1.0, 0.0], [0.0, 1.0]):.4f}") +print(f"косинусное сходство: {cosine_similarity([1.0, 2.0], [-1.0, -2.0]):.4f}") diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 000000000..6e59c950b --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,22 @@ +[tool.ruff] +line-length = 88 +target-version = "py312" +extend-exclude = [".venv", "phases/*/code/*.ipynb"] + +[tool.ruff.lint] +select = [ + "E", # pycodestyle errors (PEP 8) + "W", # pycodestyle warnings + "F", # pyflakes (unused imports, undefined names) + "I", # isort (import order) + "UP", # pyupgrade (modernize syntax to current Python) + "B", # flake8-bugbear (likely bugs) + "SIM", # flake8-simplify (idiomatic simplifications) +] +ignore = [ + "E501", # line too long — formatter сам нарежет +] + +[tool.ruff.format] +quote-style = "double" +indent-style = "space" \ No newline at end of file diff --git a/scratch/lesson-07/app/Dockerfile b/scratch/lesson-07/app/Dockerfile new file mode 100644 index 000000000..bd944bde1 --- /dev/null +++ b/scratch/lesson-07/app/Dockerfile @@ -0,0 +1,6 @@ +FROM python:3.12-slim +WORKDIR /app +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt +COPY main.py . +CMD ["python", "main.py"] \ No newline at end of file diff --git a/scratch/lesson-07/app/main.py b/scratch/lesson-07/app/main.py new file mode 100644 index 000000000..521f702c6 --- /dev/null +++ b/scratch/lesson-07/app/main.py @@ -0,0 +1,16 @@ +import socket +import sys + +from rich.console import Console +from rich.panel import Panel + + +def main() -> None: + console = Console() + message = f"""Привет из твоего первого контейнера! +У тебя установлен python - {sys.version} на компьютере - {socket.gethostname()}""" + console.print(Panel(message)) + + +if __name__ == "__main__": + main() diff --git a/scratch/lesson-07/app/requirements.txt b/scratch/lesson-07/app/requirements.txt new file mode 100644 index 000000000..9cda2da48 --- /dev/null +++ b/scratch/lesson-07/app/requirements.txt @@ -0,0 +1 @@ +rich==13.9.4 diff --git a/scratch/lesson-07/from-container.txt b/scratch/lesson-07/from-container.txt new file mode 100644 index 000000000..04dfc7ae0 --- /dev/null +++ b/scratch/lesson-07/from-container.txt @@ -0,0 +1 @@ +написано изнутри контейнера в Mon May 18 04:53:06 UTC 2026 от root@2eadb7897530 diff --git a/scratch/lesson-08/lsp_test.py b/scratch/lesson-08/lsp_test.py new file mode 100644 index 000000000..3ff7ce72e --- /dev/null +++ b/scratch/lesson-08/lsp_test.py @@ -0,0 +1,16 @@ +import math + + +def area_of_circle(radius: float) -> float: + """Площадь круга по радиусу""" + return math.pi * radius**2 + + +def main() -> None: + r = 2.5 + a = area_of_circle(r) + print(f"radius = {r}, площадь = {a:.3f}") + + +if __name__ == "__main": + main() diff --git a/scratch/phase-0/lesson-09/01_first_dataset.ipynb b/scratch/phase-0/lesson-09/01_first_dataset.ipynb new file mode 100644 index 000000000..824326956 --- /dev/null +++ b/scratch/phase-0/lesson-09/01_first_dataset.ipynb @@ -0,0 +1,63 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 3, + "id": "5cd15f92-e5d1-45ae-9f72-dea9188f354b", + "metadata": {}, + "outputs": [], + "source": [ + "from datasets import load_dataset " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "184b37bf-7bcb-47cf-9675-c10a8072f38b", + "metadata": {}, + "outputs": [], + "source": [ + "ds_train = load_dataset(\"Qwen/WebWorldData\", split=\"train\", streaming=True)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e1baa0eb-0835-4ce0-b313-976a480310de", + "metadata": {}, + "outputs": [], + "source": [ + "ds_train.column_names" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e682fc55-e52b-40f9-b5c3-c2193a6245d4", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.13" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/scratch/phase-00-lesson-05/exercise-1.ipynb b/scratch/phase-00-lesson-05/exercise-1.ipynb new file mode 100644 index 000000000..ddab8be1a --- /dev/null +++ b/scratch/phase-00-lesson-05/exercise-1.ipynb @@ -0,0 +1,162 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "id": "73410a8a-edf6-424c-b671-9e496c0da632", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'/home/pavel/ai-engineering-from-scratch/.venv/bin/python3'" + ] + }, + "execution_count": 1, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "import sys\n", + "import random\n", + "import numpy as np\n", + "sys.executable" + ] + }, + { + "cell_type": "markdown", + "id": "570a69e0-d49e-4f95-b30e-76d28cb71f4b", + "metadata": {}, + "source": [ + "# Сколько занимает list comprehension?» / «А numpy?» / «Сравним напрямую" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "fd59a663-0186-4535-a9da-b84d1b225464", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "8.26 ms ± 230 μs per loop (mean ± std. dev. of 7 runs, 100 loops each)\n" + ] + }, + { + "data": { + "text/plain": [ + "0.008255912514203894" + ] + }, + "execution_count": 2, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "t_list_1 = %timeit -o [random.random() for _ in range(100_000)]\n", + "t_list_1.average\n" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "da46d3eb-635a-4765-a4c2-a2987c92c110", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "908 μs ± 7.17 μs per loop (mean ± std. dev. of 7 runs, 1,000 loops each)\n" + ] + }, + { + "data": { + "text/plain": [ + "0.0009076988744200207" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "t_list_2 = %timeit -o np.random.random(100_000)\n", + "t_list_2.average" + ] + }, + { + "cell_type": "markdown", + "id": "bd36a013-58ca-49f8-9acd-3a63b8c4adc8", + "metadata": {}, + "source": [ + "## Сравниваем среднее время работы двух способов" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "0ff54aaa-01ca-4449-b399-2ef3884073cb", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "9.095431036508726" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "speedup = t_list_1.average / t_list_2.average\n", + "speedup" + ] + }, + { + "cell_type": "markdown", + "id": "84cf0657-e01b-4fd5-a4d6-7cdbb5649c03", + "metadata": {}, + "source": [ + "Результат - в 8 раз быстрее работает способ с помощью numpy. Если я правильно понимаю, потому, что бибилотека написана на C.\n", + "Первый способ использует интерпретатор python" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "0e56a544-21e1-4b19-840d-e26132bcc00c", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.13" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/scratch/phase-00-lesson-05/exercise-2.ipynb b/scratch/phase-00-lesson-05/exercise-2.ipynb new file mode 100644 index 000000000..f8768b9b3 --- /dev/null +++ b/scratch/phase-00-lesson-05/exercise-2.ipynb @@ -0,0 +1,421 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "e5c1f322-c050-4a7a-a2b9-4a01b484a4c9", + "metadata": {}, + "source": [ + "# Выполнение второго задания" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "5eb78806-3f68-412d-83cf-5963903e22f7", + "metadata": {}, + "outputs": [], + "source": [ + "import numpy as np\n", + "import pandas as pd\n", + "import matplotlib.pyplot as plt" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "18c8c10c-5149-49f5-9460-5a8182bf3be9", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
monthsalesexpensesprofit
011209030
121359243
231689573
3420598107
45242103139
56278110168
67295115180
78287113174
89231108123
91018410282
10111529656
11121789979
\n", + "
" + ], + "text/plain": [ + " month sales expenses profit\n", + "0 1 120 90 30\n", + "1 2 135 92 43\n", + "2 3 168 95 73\n", + "3 4 205 98 107\n", + "4 5 242 103 139\n", + "5 6 278 110 168\n", + "6 7 295 115 180\n", + "7 8 287 113 174\n", + "8 9 231 108 123\n", + "9 10 184 102 82\n", + "10 11 152 96 56\n", + "11 12 178 99 79" + ] + }, + "execution_count": 2, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "df_loaded = pd.read_csv(\"./sales.csv\")\n", + "df_loaded" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "3146fb22-76cc-4e44-b1d6-10185c43aae3", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
monthsalesexpensesprofit
count12.00000012.0000012.00000012.000000
mean6.500000206.25000101.750000104.500000
std3.60555160.039578.22551552.166691
min1.000000120.0000090.00000030.000000
25%3.750000164.0000095.75000068.750000
50%6.500000194.50000100.50000094.500000
75%9.250000251.00000108.500000146.250000
max12.000000295.00000115.000000180.000000
\n", + "
" + ], + "text/plain": [ + " month sales expenses profit\n", + "count 12.000000 12.00000 12.000000 12.000000\n", + "mean 6.500000 206.25000 101.750000 104.500000\n", + "std 3.605551 60.03957 8.225515 52.166691\n", + "min 1.000000 120.00000 90.000000 30.000000\n", + "25% 3.750000 164.00000 95.750000 68.750000\n", + "50% 6.500000 194.50000 100.500000 94.500000\n", + "75% 9.250000 251.00000 108.500000 146.250000\n", + "max 12.000000 295.00000 115.000000 180.000000" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "df_loaded.describe()" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "c809b0b2-629f-42c2-9d9a-d12df5c9b206", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "np.float64(206.25)" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "df_loaded[\"sales\"].mean()" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "312bbdce-2c63-4efc-9650-85221cd892c6", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Средние продажи за месяц: 206.2\n", + "Суммарная прибыль за год: 1254\n", + "Пиковый месяц продаж: 7\n" + ] + } + ], + "source": [ + "mean_sales = df_loaded[\"sales\"].mean()\n", + "total_profit = df_loaded[\"profit\"].sum()\n", + "peak_month = df_loaded.loc[df_loaded[\"sales\"].idxmax(), \"month\"]\n", + "\n", + "print(f\"Средние продажи за месяц: {mean_sales:.1f}\")\n", + "print(f\"Суммарная прибыль за год: {total_profit}\")\n", + "print(f\"Пиковый месяц продаж: {peak_month}\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "026bce5e-3b9b-4e5f-8349-3fe0754e8efe", + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "135c6508-f9ec-431d-ace5-fbb19538ba1d", + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAA1IAAAIjCAYAAAAJLyrXAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjksIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvJkbTWQAAAAlwSFlzAAAPYQAAD2EBqD+naQAA1DZJREFUeJzs3XlYVGUbx/HvzLDviyAoiCC44L6Lu2VpmWlZqeVamWtptpiaqW1mWVlqVlbuWmlauaRt7oC4K7gLLiCbILssM3PeP8h5IzdE4Axwf96L6405Z8787hmEuec853k0iqIoCCGEEEIIIYQoNq3aAYQQQgghhBCiopFGSgghhBBCCCHukjRSQgghhBBCCHGXpJESQgghhBBCiLskjZQQQgghhBBC3CVppIQQQgghhBDiLkkjJYQQQgghhBB3SRopIYQQQgghhLhL0kgJIYQQQgghxF2SRkoIIYQQQggh7pI0UkIIIYQQQghxl6SREkIIM7N27Vo0Gs1Nvxo1aqR2PCGEEEIAFmoHEEIIcXNTpkyhQYMGpu/fe+89FdMIIYQQ4t+kkRJCCDP1wAMP0LVrV9P333zzDVeuXFEvkBBCCCFMZGifEEKYmfz8fAC02jv/il6yZAkajYbz58+bbjMajTRp0gSNRsOSJUtMtw8bNgwHB4cbjnF9KOH27dtNt+3atYsnn3ySWrVqYW1tja+vLy+//DLXrl0rct9hw4ah0Who1qzZDcedNWsWGo3mhsfUaDTMmDGjyG0fffQRGo2mSOO4ffv2G3JdvnyZ2rVr06pVK7KysoDC5+utt96iZcuWODs7Y29vT6dOndi2bdvNn7T/qF279i2HUmo0miL76vV63nnnHerUqYO1tTW1a9dmypQp5OXl3fFxSvJcAaxYsYKWLVtia2uLm5sbAwYM4NKlSzfst3fvXh5++GFcXV2xt7enSZMmfPbZZ0X2Wbt2La1atcLR0bFIjXPmzCmSs7g/J8V97s+fP296rJ9//rnIttzcXFxdXW/IIYQQ5k4aKSGEMDPXGylra+sS3X/58uUcO3bsnjKsWbOGnJwcRo8ezbx58+jRowfz5s1jyJAhN+xrYWFBVFQUhw4dKnL7kiVLsLGxueNjpaWlMWvWrDvul56ezkMPPYSlpSWbN282vdnPyMjgm2++oWvXrsyePZsZM2aQnJxMjx49OHz4cLHqbdasGcuXLy/y9cADD9yw3/PPP89bb71FixYt+PTTT+nSpQuzZs1iwIABxXqcu32u3nvvPYYMGUJQUBCffPIJEyZM4K+//qJz586kpaWZ9vvjjz/o3Lkzx48fZ/z48Xz88cd069aNjRs3mvYJCwvjqaeewmAw8MEHH7B8+XI+/fTTYuW+lbt97m1sbFi8eHGR29atW0dubu495RBCCDXI0D4hhDAz6enpANja2t71ffPy8njrrbd46KGH+O2330qcYfbs2UUe/4UXXiAwMJApU6Zw8eJFatWqZdpmbW3Nfffdx3fffce8efMA2L17N5cuXaJnz5788ccft32sWbNmYWlpScuWLW9bV9++fUlISCA0NBRPT0/TNldXV86fP4+VlZXpthEjRlC/fn3mzZvHt99+e8d6a9asyaBBg4rcFh4eXiT7kSNHWLp0Kc8//zyLFi0CYMyYMXh6ejJnzhy2bdtGt27dbvs4d/NcXbhwgenTp/Puu+8yZcoU0+2PP/44zZs354svvmDKlCkYDAZGjhyJt7c3hw8fxsXFxbSvoiim/96wYQOKovDbb7/h5eUFFJ4pevnll+/4/NzK3T73jz32GGvWrCExMZHq1asD8N133/H444+zatWqEucQQgg1yBkpIYQwMykpKQB4eHjc9X0XLFhASkoK06dPv+U+V65cKfKVmZl5wz7/bqKys7O5cuUK7du3R1GUG86mADz77LOsWrXKNMRt8eLFPP744zg7O982b1xcHPPmzWPatGk3HU4GhUMVhwwZQnh4OJs3b6ZOnTpFtut0OtMbeaPRSGpqKnq9nlatWnHw4MHbPv7d2Lx5MwATJ04scvsrr7wCwKZNm4p1nOI+V+vWrcNoNPLUU08Veb28vLwICgoyDZ87dOgQMTExTJgwoUgTBRQZmpiZmYlWq71hn3txt899ixYtaNiwIcuXLwcKm8Vt27YxbNiwUsskhBDlRRopIYQwMxcuXMDCwuKuG6n09HTef/99Jk6caPq0/7+ys7Px8PAo8vXss8/esN/FixcZNmwYbm5uODg44OHhQZcuXUyP81+9evXCwsKCX375hezsbH788UeGDx9+x8zTp0+nRo0ajBw58pb7TJ06lR9//JG8vDxycnJuus/SpUtp0qQJNjY2uLu74+HhwaZNm26ataQuXLiAVqslMDCwyO1eXl64uLhw4cKF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8SxNWGpRZSIiIiIiUhQFKYud6I2yUfRmuTZs6pUSEREREfEwClIWyzPySMpKwqTooGRikpSVRJ6R5+bKRERERETkTLTYhMV8HD7MvGomKdkpAJiGScqRFEKqh2Czu3qpQvxC8HH4WFmmiIiIiIicREHKA0QGRBIZEAmAYRgcdB4kPDQcu10dhiIiIiIinkif1EVEREREREpIQUpERERERKSEFKRERERERERKSEFKRERERESkhBSkRERERERESkhBSkREREREpIQUpEREREREREpIQUpERERERKSEFKRERERERERKSEFKRERERESkhBSkRERERERESkhBSkREREREpIQUpERERERERErIy+oCPIFpmgCkp6dbXAkYhkFGRgZ+fn7Y7ZUv56r9ar/ar/ar/Wq/2q/2VzZqv2e1/0QmOJERzkRBCsjIyACgVq1aFlciIiIiIiKeICMjg+Dg4DOet5nnilqVgGEYJCQkEBgYiM1ms7SW9PR0atWqxd69ewkKCrK0Fiuo/Wq/2q/2q/1qv9qv9lc2ar9ntd80TTIyMoiOjj5rD5l6pAC73U7NmjWtLqOQoKAgj/iHZBW1X+1X+9X+ykrtV/vVfrW/svKk9p+tJ+oE6wchioiIiIiIlDMKUiIiIiIiIiWkIOVhfH19GTlyJL6+vlaXYgm1X+1X+9V+tV/tr4zUfrVf7S9/7ddiEyIiIiIiIiWkHikREREREZESUpASEREREREpIQUpERERERGRElKQEhERERERKSEFKQ+xePFi+vbtS3R0NDabjW+++cbqktxq3LhxtGnThsDAQMLDw7n22mvZvHmz1WW5zbRp02jevHnBRnTt27fnp59+srosS7z44ovYbDYGDx5sdSluM2rUKGw2W6Fbo0aNrC7Lrfbv388dd9xBaGgo/v7+XHjhhaxatcrqstyidu3ap/38bTYbAwYMsLo0t3A6nTzzzDPUqVMHf39/LrjgAp5//nkq01pYGRkZDB48mLi4OPz9/enQoQMrV660uqwyca7PO6Zp8uyzzxIVFYW/vz89e/Zk69at1hRbBs7V/tmzZ3PZZZcRGhqKzWZj3bp1ltRZVs7W/ry8PIYPH86FF15IQEAA0dHR/N///R8JCQnWFXwOClIeIisrixYtWvD6669bXYolFi1axIABA1i+fDlz5swhLy+Pyy67jKysLKtLc4uaNWvy4osvsnr1alatWsWll17KNddcQ3x8vNWludXKlSt58803ad68udWluF3Tpk1JTEwsuC1ZssTqktzmyJEjdOzYEW9vb3766Sc2btzIK6+8QvXq1a0uzS1WrlxZ6Gc/Z84cAG666SaLK3OPl156iWnTpvHaa6+xadMmXnrpJcaPH8/UqVOtLs1t7rvvPubMmcNHH33EX3/9xWWXXUbPnj3Zv3+/1aWVunN93hk/fjxTpkxh+vTprFixgoCAAHr37k12drabKy0b52p/VlYWnTp14qWXXnJzZe5xtvYfPXqUNWvW8Mwzz7BmzRpmz57N5s2bufrqqy2otJhM8TiA+fXXX1tdhqUOHjxoAuaiRYusLsUy1atXN9955x2ry3CbjIwMs379+uacOXPMrl27moMGDbK6JLcZOXKk2aJFC6vLsMzw4cPNTp06WV2Gxxg0aJB5wQUXmIZhWF2KW1x55ZVm//79Cx27/vrrzdtvv92iitzr6NGjpsPhML///vtCx1u1amWOGDHCoqrc49TPO4ZhmJGRkeaECRMKjqWmppq+vr7mZ599ZkGFZetsn/d27txpAubatWvdWpM7Fefz7h9//GEC5u7du91TVAmpR0o8UlpaGgAhISEWV+J+TqeTmTNnkpWVRfv27a0ux20GDBjAlVdeSc+ePa0uxRJbt24lOjqaunXrcvvtt7Nnzx6rS3Kbb7/9losvvpibbrqJ8PBwWrZsydtvv211WZbIzc3l448/pn///thsNqvLcYsOHTowb948tmzZAsD69etZsmQJffr0sbgy98jPz8fpdOLn51fouL+/f6XqmQbYuXMnSUlJhf4OBAcH065dO5YtW2ZhZWKVtLQ0bDYb1apVs7qUInlZXYDIqQzDYPDgwXTs2JFmzZpZXY7b/PXXX7Rv357s7GyqVq3K119/TZMmTawuyy1mzpzJmjVrKuycgHNp164d77//Pg0bNiQxMZHRo0fTuXNnNmzYQGBgoNXllbkdO3Ywbdo0Hn30UZ566ilWrlzJI488go+PD3fddZfV5bnVN998Q2pqKnfffbfVpbjNE088QXp6Oo0aNcLhcOB0OhkzZgy333671aW5RWBgIO3bt+f555+ncePGRERE8Nlnn7Fs2TLq1atndXlulZSUBEBERESh4xEREQXnpPLIzs5m+PDh9OvXj6CgIKvLKZKClHic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s77+Qw89xKpVq2jatCm+vr7Y7RoZLyJS2ekvgYiIlHuXX345ubm55OXl0bt370LnmjRpgq+vL3v27KFevXqFbrVq1QKgefPm/Pbbb+Tl5RX5+gEBATz++ONUrVqV2bNns27durJukoiIeDj1SImISLnncDjYtGlTwf2TBQYGMnToUIYMGYJhGHTq1Im0tDR+//13goKCuOuuuxg4cCBTp07l1ltv5cknnyQ4OJjly5fTtm1bGjZsSEpKCjfeeCMvvvgil19++RnryMnJITs7u9CxvLw8DMNQL5aISAWjICUiIhVCUFDQGc89//zzhIWFMW7cOHbs2EG1atVo1aoVTz31FAChoaHMnz+fYcOG0bVrVxwOBxdddBEdO3bENE3uuOMOOnXqxIMPPnjWGiIjI087dvPNN7NgwQK6dev2r9onIiKexWaapml1ESIiIhXVtddey+DBgxWkREQqGI0zEBERKUM+Pj4a1iciUgGpR0pERERERKSEdIlMRERERESkhBSkRERERERESkhBSkREREREpIQUpEREREREREpIQUpERERERKSEFKRERERERERKSEFKRERERESkhBSkRERERERESuj/AXI9u4FZN+ZUAAAAAElFTkSuQmCC", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "plt.figure(figsize=(10, 6))\n", + "\n", + "plt.plot(df_loaded[\"month\"], df_loaded[\"sales\"], marker=\"o\", label=\"Продажи\")\n", + "plt.plot(df_loaded[\"month\"], df_loaded[\"expenses\"], marker=\"s\", label=\"Расходы\")\n", + "plt.plot(df_loaded[\"month\"], df_loaded[\"profit\"], marker=\"^\", label=\"Прибыль\")\n", + "\n", + "plt.title(\"Динамика по месяцам\")\n", + "plt.xlabel(\"Месяц\")\n", + "plt.ylabel(\"Сумма\")\n", + "plt.xticks(df_loaded[\"month\"])\n", + "plt.legend()\n", + "plt.grid(True, alpha=0.3)\n", + "\n", + "plt.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b66011f9-52ff-4add-bcbd-3bfb7be8501e", + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "markdown", + "id": "5528f3da-f9ba-4445-a853-646761d57e60", + "metadata": {}, + "source": [ + "продажи растут" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "0e5367a3-5249-456c-8780-1939e04d0592", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.13" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/scratch/phase-00-lesson-05/sales.csv b/scratch/phase-00-lesson-05/sales.csv new file mode 100644 index 000000000..d76774e1e --- /dev/null +++ b/scratch/phase-00-lesson-05/sales.csv @@ -0,0 +1,13 @@ +month,sales,expenses,profit +1,120,90,30 +2,135,92,43 +3,168,95,73 +4,205,98,107 +5,242,103,139 +6,278,110,168 +7,295,115,180 +8,287,113,174 +9,231,108,123 +10,184,102,82 +11,152,96,56 +12,178,99,79 \ No newline at end of file diff --git a/scratch/phase-00-lesson-06/playground-project/pyproject.toml b/scratch/phase-00-lesson-06/playground-project/pyproject.toml new file mode 100644 index 000000000..8cfd589eb --- /dev/null +++ b/scratch/phase-00-lesson-06/playground-project/pyproject.toml @@ -0,0 +1,10 @@ +[project] +name = "pyproject" +version = "0.1.0" +requires-python = ">=3.12" +dependencies = [ + "numpy >=1.26" +] +[project.optional-dependencies] +torch = ["torch>=2.3", "torchvision>=0.18"] +llm = ["anthropic>=0.39", "openai>=1.50"] \ No newline at end of file diff --git a/scratch/phase-00-lesson-06/playground-project/requirements.lock b/scratch/phase-00-lesson-06/playground-project/requirements.lock new file mode 100644 index 000000000..a571d4b3d --- /dev/null +++ b/scratch/phase-00-lesson-06/playground-project/requirements.lock @@ -0,0 +1,139 @@ +# This file was autogenerated by uv via the following command: +# uv pip compile pyproject.toml -o requirements.lock --extra torch --extra llm +annotated-types==0.7.0 + # via pydantic +anthropic==0.102.0 + # via pyproject (pyproject.toml) +anyio==4.13.0 + # via + # anthropic + # httpx + # openai +certifi==2026.4.22 + # via + # httpcore + # httpx +cuda-bindings==13.2.0 + # via torch +cuda-pathfinder==1.5.4 + # via cuda-bindings +cuda-toolkit==13.0.2 + # via torch +distro==1.9.0 + # via + # anthropic + # openai +docstring-parser==0.18.0 + # via anthropic +filelock==3.29.0 + # via torch +fsspec==2026.4.0 + # via torch +h11==0.16.0 + # via httpcore +httpcore==1.0.9 + # via httpx +httpx==0.28.1 + # via + # anthropic + # openai +idna==3.15 + # via + # anyio + # httpx +jinja2==3.1.6 + # via torch +jiter==0.14.0 + # via + # anthropic + # openai +markupsafe==3.0.3 + # via jinja2 +mpmath==1.3.0 + # via sympy +networkx==3.6.1 + # via torch +numpy==2.4.4 + # via + # pyproject (pyproject.toml) + # torchvision +nvidia-cublas==13.1.1.3 + # via + # nvidia-cudnn-cu13 + # nvidia-cusolver + # torch +nvidia-cuda-cupti==13.0.85 + # via cuda-toolkit +nvidia-cuda-nvrtc==13.0.88 + # via + # cuda-toolkit + # nvidia-cublas +nvidia-cuda-runtime==13.0.96 + # via cuda-toolkit +nvidia-cudnn-cu13==9.20.0.48 + # via torch +nvidia-cufft==12.0.0.61 + # via cuda-toolkit +nvidia-cufile==1.15.1.6 + # via cuda-toolkit +nvidia-curand==10.4.0.35 + # via cuda-toolkit +nvidia-cusolver==12.0.4.66 + # via cuda-toolkit +nvidia-cusparse==12.6.3.3 + # via + # cuda-toolkit + # nvidia-cusolver +nvidia-cusparselt-cu13==0.8.1 + # via torch +nvidia-nccl-cu13==2.29.7 + # via torch +nvidia-nvjitlink==13.0.88 + # via + # cuda-toolkit + # nvidia-cufft + # nvidia-cusolver + # nvidia-cusparse +nvidia-nvshmem-cu13==3.4.5 + # via torch +nvidia-nvtx==13.0.85 + # via cuda-toolkit +openai==2.36.0 + # via pyproject (pyproject.toml) +pillow==12.2.0 + # via torchvision +pydantic==2.13.4 + # via + # anthropic + # openai +pydantic-core==2.46.4 + # via pydantic +setuptools==81.0.0 + # via torch +sniffio==1.3.1 + # via + # anthropic + # openai +sympy==1.14.0 + # via torch +torch==2.12.0 + # via + # pyproject (pyproject.toml) + # torchvision +torchvision==0.27.0 + # via pyproject (pyproject.toml) +tqdm==4.67.3 + # via openai +triton==3.7.0 + # via torch +typing-extensions==4.15.0 + # via + # anthropic + # anyio + # openai + # pydantic + # pydantic-core + # torch + # typing-inspection +typing-inspection==0.4.2 + # via pydantic