feat: 完整中文翻译 maths-cs-ai-compendium(数学·计算机科学·AI 知识大全)
翻译自英文原版 maths-cs-ai-compendium,共 20 章全部完成。 第01章 向量 | 第02章 矩阵 | 第03章 微积分 第04章 统计学 | 第05章 概率论 | 第06章 机器学习 第07章 计算语言学 | 第08章 计算机视觉 | 第09章 音频与语音 第10章 多模态学习 | 第11章 自主系统 | 第12章 图神经网络 第13章 计算与操作系统 | 第14章 数据结构与算法 第15章 生产级软件工程 | 第16章 SIMD与GPU编程 第17章 AI推理 | 第18章 ML系统设计 第19章 应用人工智能 | 第20章 前沿人工智能 翻译说明: - 所有数学公式 $...$ / $$...$$、代码块、图片引用完整保留 - mkdocs.yml 配置中文导航 + language: zh - README.md 已翻译为中文(兼 docs/index.md) - docs/ 目录包含指向各章文件的 symlink - 约 29,000 行中文内容,排除 .cache/ 构建缓存
This commit is contained in:
@@ -0,0 +1,318 @@
|
||||
# Linux 与命令行
|
||||
|
||||
*命令行是机器学习工程的主要界面:训练任务、服务器管理、数据管道和集群管理都通过终端进行。本文涵盖 Shell、文件系统、权限、进程管理、包管理器、环境变量、SSH 以及每位机器学习工程师日常使用的基本命令。*
|
||||
|
||||
- GUI 适合浏览网页,但在凌晨 2 点在远程 GPU 集群上运行训练任务时却很糟糕。**命令行**(或终端、Shell)是能够扩展的工具:它在任何机器上都能工作,可编写脚本,可组合,并且在你的笔记本电脑、云 VM 和 HPC 集群上完全相同。
|
||||
|
||||
- 如果你是一名只使用 Jupyter notebook 和 VS Code 按钮的机器学习工程师,你正在浪费巨大的生产力。每个生产级机器学习系统都是通过命令行进行部署、监控和调试的。
|
||||
|
||||
## Shell
|
||||
|
||||
- **Shell** 是一个读取你的命令并执行它们的程序。它是你和操作系统之间的中介(第 13 章)。最常见的 Shell 是 **bash**(大多数 Linux 系统的默认 Shell)和 **zsh**(macOS 的默认 Shell)。
|
||||
|
||||
- 命令的格式为:`command [options] [arguments]`
|
||||
|
||||
```bash
|
||||
ls -la /home/user # 命令=ls, 选项=-la, 参数=/home/user
|
||||
```
|
||||
|
||||
- 选项修改行为(通常以 `-` 表示短选项,`--` 表示长选项)。`ls -l` 以长格式列出,`ls --all` 显示隐藏文件。许多选项可以组合:`ls -la` 表示将 `-l` 和 `-a` 一起使用。
|
||||
|
||||
### 基本导航
|
||||
|
||||
```bash
|
||||
pwd # 打印当前工作目录(我在哪?)
|
||||
ls # 列出当前目录中的文件
|
||||
ls -la # 列出所有文件(包括隐藏文件)及详细信息
|
||||
cd /path/to/dir # 切换目录
|
||||
cd .. # 返回上一级
|
||||
cd ~ # 返回用户主目录
|
||||
cd - # 返回上一个目录
|
||||
```
|
||||
|
||||
### 文件操作
|
||||
|
||||
```bash
|
||||
cp source dest # 复制文件
|
||||
cp -r dir1 dir2 # 递归复制目录
|
||||
mv old new # 移动/重命名文件
|
||||
rm file # 删除文件(没有回收站——永久删除)
|
||||
rm -rf dir # 递归删除目录(危险——无确认)
|
||||
mkdir -p a/b/c # 创建嵌套目录
|
||||
touch file.txt # 创建空文件(或更新时间戳)
|
||||
cat file.txt # 打印文件内容
|
||||
head -n 20 file # 显示前 20 行
|
||||
tail -f logfile # 实时跟踪日志文件(监控训练时非常有用)
|
||||
```
|
||||
|
||||
- **陷阱**:`rm -rf` 是计算中最危险的命令。没有撤销操作。按回车前请三次检查路径。切勿运行 `rm -rf /` 或 `rm -rf ~`。
|
||||
|
||||
### 管道与重定向
|
||||
|
||||
- Shell 的杀手级特性是**可组合性**:将小命令连接起来完成复杂任务。
|
||||
|
||||
- **管道**(`|`):将一个命令的输出作为下一个命令的输入。
|
||||
|
||||
```bash
|
||||
cat training.log | grep "loss" | tail -5 # 最后5行包含"loss"的内容
|
||||
ps aux | grep python # 查找正在运行的 Python 进程
|
||||
history | grep "docker" # 查找之前的 docker 命令
|
||||
```
|
||||
|
||||
- **重定向**:将输出发送到文件而不是屏幕。
|
||||
|
||||
```bash
|
||||
python train.py > output.log 2>&1 # stdout 和 stderr 都输出到文件
|
||||
python train.py >> output.log # 追加(不覆盖)
|
||||
echo "data" > file.txt # 覆盖文件
|
||||
echo "more" >> file.txt # 追加到文件
|
||||
```
|
||||
|
||||
- `2>&1` 将 stderr(文件描述符 2)重定向到 stdout(文件描述符 1)。没有它,错误消息仍会出现在屏幕上,只有正常输出会进入文件。
|
||||
|
||||
### 文本处理
|
||||
|
||||
```bash
|
||||
grep "error" logfile.txt # 查找包含"error"的行
|
||||
grep -r "import torch" src/ # 递归搜索目录
|
||||
grep -i "warning" log.txt # 不区分大小写搜索
|
||||
grep -c "epoch" train.log # 统计匹配行数
|
||||
|
||||
wc -l file.txt # 统计行数
|
||||
wc -w file.txt # 统计单词数
|
||||
|
||||
sort data.txt # 按字母顺序排序
|
||||
sort -n numbers.txt # 按数值排序
|
||||
sort -u data.txt # 排序并去重
|
||||
uniq -c sorted.txt # 统计连续重复项
|
||||
|
||||
cut -d',' -f2,3 data.csv # 提取 CSV 的第 2 和第 3 列
|
||||
awk '{print $1, $3}' data.txt # 打印第 1 和第 3 个空白分隔字段
|
||||
sed 's/old/new/g' file.txt # 将所有"old"替换为"new"
|
||||
```
|
||||
|
||||
- 这些命令可以优美地组合:
|
||||
|
||||
```bash
|
||||
# 查找日志文件中最常见的 10 种错误类型
|
||||
grep "ERROR" app.log | awk -F': ' '{print $2}' | sort | uniq -c | sort -rn | head -10
|
||||
```
|
||||
|
||||
### 查找文件
|
||||
|
||||
```bash
|
||||
find . -name "*.py" # 查找所有 Python 文件
|
||||
find . -name "*.pyc" -delete # 查找并删除编译后的 Python 文件
|
||||
find /data -size +100M # 查找大于 100MB 的文件
|
||||
find . -mtime -1 # 查找过去 24 小时内修改过的文件
|
||||
|
||||
which python # python 可执行文件在哪?
|
||||
locate filename # 快速查找文件(使用预构建索引)
|
||||
```
|
||||
|
||||
## 文件系统层次结构
|
||||
|
||||
- Linux 将所有内容组织在以 `/` 为根的单棵树中:
|
||||
|
||||
| 目录 | 用途 |
|
||||
|-----------|---------|
|
||||
| `/` | 整个文件系统的根 |
|
||||
| `/home/user` | 你的个人文件、配置、项目 |
|
||||
| `/etc` | 系统级配置文件 |
|
||||
| `/usr` | 用户程序、库、文档 |
|
||||
| `/usr/local` | 本地安装的软件(非包管理器安装) |
|
||||
| `/var` | 可变数据:日志(`/var/log`)、数据库、缓存 |
|
||||
| `/tmp` | 临时文件(重启后清除) |
|
||||
| `/opt` | 可选的第三方软件 |
|
||||
| `/proc` | 暴露内核和进程信息的虚拟文件系统 |
|
||||
| `/dev` | 设备文件(磁盘、GPU 在这里显示) |
|
||||
|
||||
- 对于机器学习:你的训练数据通常在 `/data` 或 `/home/user/data`,模型在 `/home/user/models`,CUDA 在 `/usr/local/cuda`。GPU 设备显示为 `/dev/nvidia0`、`/dev/nvidia1` 等。
|
||||
|
||||
## 文件权限
|
||||
|
||||
- 每个文件和目录有三种用户类别的三种权限类型:
|
||||
|
||||
| 权限 | 文件 | 目录 |
|
||||
|------------|------|-----------|
|
||||
| **r**(读) | 查看内容 | 列出内容 |
|
||||
| **w**(写) | 修改内容 | 在内部创建/删除文件 |
|
||||
| **x**(执行) | 作为程序运行 | 进入(cd 进入)目录 |
|
||||
|
||||
- 三种用户类别:**所有者**(u)、**组**(g)、**其他人**(o)。
|
||||
|
||||
```bash
|
||||
ls -l script.py
|
||||
# -rwxr-xr-- 1 henry ml_team 2048 Mar 28 script.py
|
||||
# ^^^ 所有者权限:rwx(读、写、执行)
|
||||
# ^^^ 组权限:r-x(读、执行,不可写)
|
||||
# ^^^ 其他人权限:r--(只读)
|
||||
```
|
||||
|
||||
```bash
|
||||
chmod 755 script.py # owner=rwx, group=rx, others=rx
|
||||
chmod +x script.py # 为所有人添加执行权限
|
||||
chmod u+w,g-w file.txt # 为所有者添加写权限,移除组的写权限
|
||||
chown henry:ml_team file # 更改所有者和组
|
||||
```
|
||||
|
||||
- **陷阱**:顶部带有 `#!/usr/bin/env python3` 的 Python 脚本需要执行权限(`chmod +x`)才能以 `./script.py` 方式运行。没有它,你必须使用 `python3 script.py`。
|
||||
|
||||
## 进程管理
|
||||
|
||||
- **进程**是一个正在运行的程序(第 13 章)。Shell 为你提供了管理它们的工具:
|
||||
|
||||
```bash
|
||||
ps aux # 列出所有正在运行的进程
|
||||
ps aux | grep python # 查找 Python 进程
|
||||
top # 实时进程监控(CPU、内存)
|
||||
htop # top 的增强版(需单独安装)
|
||||
nvidia-smi # GPU 使用情况(机器学习必备)
|
||||
watch -n 1 nvidia-smi # 每秒刷新 nvidia-smi
|
||||
|
||||
kill PID # 优雅终止进程
|
||||
kill -9 PID # 强制终止(优雅方式失败时使用)
|
||||
killall python # 终止所有 Python 进程
|
||||
|
||||
# 后台运行
|
||||
python train.py & # 后台运行
|
||||
nohup python train.py > log.txt & # 后台运行,退出登录后仍存活
|
||||
```
|
||||
|
||||
- **`nohup`** 对机器学习训练至关重要:没有它,关闭 SSH 连接会终止训练任务。`nohup` 将进程从终端分离出来。
|
||||
|
||||
- **`screen`** 和 **`tmux`** 是终端复用器,可以创建持久会话。你可以在 tmux 会话中启动训练任务,断开 SSH 连接,稍后重新连接,会话(和训练)仍在运行。
|
||||
|
||||
```bash
|
||||
tmux new -s training # 创建命名会话
|
||||
# ... 开始训练 ...
|
||||
# Ctrl+B, 然后 D # 从会话分离
|
||||
tmux attach -t training # 稍后重新连接(即使 SSH 重新连接后也可用)
|
||||
tmux ls # 列出会话
|
||||
```
|
||||
|
||||
## 包管理器
|
||||
|
||||
- **系统包**(操作系统级软件):
|
||||
|
||||
```bash
|
||||
# Debian/Ubuntu
|
||||
sudo apt update # 刷新包列表
|
||||
sudo apt install htop # 安装包
|
||||
sudo apt upgrade # 升级所有包
|
||||
|
||||
# macOS
|
||||
brew install wget # 通过 Homebrew 安装
|
||||
```
|
||||
|
||||
- **Python 包**:
|
||||
|
||||
```bash
|
||||
pip install torch # 从 PyPI 安装
|
||||
pip install -e . # 以可编辑模式安装当前项目
|
||||
pip install -r requirements.txt # 从 requirements 文件安装
|
||||
pip freeze > requirements.txt # 导出已安装的包
|
||||
|
||||
# Conda(用于复杂依赖,如 CUDA)
|
||||
conda create -n myenv python=3.11
|
||||
conda activate myenv
|
||||
conda install pytorch torchvision cudatoolkit=12.1 -c pytorch
|
||||
```
|
||||
|
||||
- **陷阱**:永远不要将 `pip install` 安装到系统 Python 中。始终使用虚拟环境(`python -m venv env`、`conda create` 或 `uv venv`)。系统 Python 被操作系统工具共享;破坏它可能导致系统崩溃。
|
||||
|
||||
## 环境变量
|
||||
|
||||
- **环境变量**是所有程序都可以访问的键值对。它们在不改变代码的情况下配置行为。
|
||||
|
||||
```bash
|
||||
export CUDA_VISIBLE_DEVICES=0,1 # 仅使用 GPU 0 和 1
|
||||
export PYTHONPATH=/home/user/src # 添加到 Python 的导入路径
|
||||
export WANDB_API_KEY=abc123 # Weights & Biases 的 API 密钥
|
||||
|
||||
echo $PATH # 查看当前 PATH
|
||||
export PATH=$PATH:/usr/local/cuda/bin # 将 CUDA 添加到 PATH
|
||||
```
|
||||
|
||||
- **`.bashrc`**(或 `.zshrc`):每次打开 Shell 时运行的命令。把你的 `export` 语句放在这里,这样它们就会持久存在。
|
||||
|
||||
- **`.env` 文件**:由 `python-dotenv` 等工具加载的项目特定变量。将密钥(API 密钥、数据库密码)保存在 `.env` 中,并将 `.env` 添加到 `.gitignore`。切勿将密钥提交到 Git。
|
||||
|
||||
## SSH(安全外壳协议)
|
||||
|
||||
- **SSH** 通过加密通道将你连接到远程机器。这是你访问云 VM、GPU 服务器和 HPC 集群的方式。
|
||||
|
||||
```bash
|
||||
ssh user@hostname # 连接到远程机器
|
||||
ssh -i ~/.ssh/key.pem user@ip # 使用特定密钥连接
|
||||
ssh -L 8888:localhost:8888 user@server # 端口转发(远程 Jupyter)
|
||||
```
|
||||
|
||||
- **SSH 密钥**(公钥/私钥对)替代密码:
|
||||
|
||||
```bash
|
||||
ssh-keygen -t ed25519 # 生成密钥对
|
||||
ssh-copy-id user@server # 将公钥复制到服务器
|
||||
# 现在无需输入密码即可 SSH
|
||||
```
|
||||
|
||||
- **SSH 配置**(`~/.ssh/config`)保存连接详情:
|
||||
|
||||
```
|
||||
Host gpu-server
|
||||
HostName 10.0.1.42
|
||||
User henry
|
||||
IdentityFile ~/.ssh/gpu_key
|
||||
LocalForward 8888 localhost:8888
|
||||
```
|
||||
|
||||
- 现在输入 `ssh gpu-server` 即可自动使用所有这些设置进行连接。
|
||||
|
||||
- **`scp`** 和 **`rsync`** 在机器之间传输文件:
|
||||
|
||||
```bash
|
||||
scp model.pt user@server:/data/models/ # 将文件复制到远程
|
||||
scp -r user@server:/data/results/ ./ # 从远程复制目录
|
||||
rsync -avz --progress data/ user@server:/data/ # 带进度同步(比 scp 更智能)
|
||||
```
|
||||
|
||||
## 机器学习必备命令速查表
|
||||
|
||||
```bash
|
||||
# GPU 监控
|
||||
nvidia-smi # GPU 使用快照
|
||||
watch -n 1 nvidia-smi # 实时监控
|
||||
gpustat # 更清晰的 GPU 概览(pip install gpustat)
|
||||
|
||||
# 训练管理
|
||||
nohup python train.py > train.log 2>&1 & # 退出登录后仍存活的后台训练
|
||||
tail -f train.log # 监控训练输出
|
||||
kill %1 # 终止最后一个后台任务
|
||||
|
||||
# 磁盘使用(数据集很大)
|
||||
df -h # 所有挂载点的磁盘空间
|
||||
du -sh /data/* # /data 中每个项目的大小
|
||||
du -sh --max-depth=1 . # 子目录的大小
|
||||
|
||||
# 内存
|
||||
free -h # RAM 使用情况
|
||||
cat /proc/meminfo # 详细内存信息
|
||||
|
||||
# 网络
|
||||
curl -O https://example.com/dataset.tar.gz # 下载文件
|
||||
wget https://example.com/model.bin # 替代下载工具
|
||||
curl -X POST http://localhost:8080/predict \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"text": "hello"}' # 测试模型推理端点
|
||||
|
||||
# 归档
|
||||
tar -czf archive.tar.gz directory/ # 压缩
|
||||
tar -xzf archive.tar.gz # 解压
|
||||
zip -r archive.zip directory/ # zip 压缩
|
||||
unzip archive.zip # zip 解压
|
||||
|
||||
# 快速数据检查
|
||||
head -5 data.csv # CSV 的前 5 行
|
||||
wc -l data.csv # 统计行数
|
||||
cut -d',' -f1 data.csv | sort -u | wc -l # 统计第 1 列的唯一值数量
|
||||
```
|
||||
@@ -0,0 +1,218 @@
|
||||
# Git 与版本控制
|
||||
|
||||
*Git 是软件团队在不相互覆盖工作的情况下进行协作的方式。本文涵盖 Git 的心智模型、分支策略、合并与变基、冲突解决、拉取请求,以及管理机器学习特定挑战(如大文件和实验追踪)的方法。*
|
||||
|
||||
- 每个严肃的软件项目都使用版本控制。**Git** 是主导系统,几乎所有开源项目和公司都在使用。没有 Git,协作就是通过电子邮件发送 zip 文件并祈祷没人覆盖你的更改。有了 Git,每次更改都可追踪、可撤销、可追溯。
|
||||
|
||||
- 对于机器学习工程师:Git 追踪你的代码、配置和实验脚本。结合实验追踪工具,它能提供可重现性:"是哪个确切的代码和配置产生了这个模型?"
|
||||
|
||||
## 心智模型
|
||||
|
||||
- Git 追踪项目的**快照**。每次提交都是那一刻所有追踪文件的完整快照,而不是差异(在内部,Git 为效率存储差异,但从概念上讲,每次提交都是一个完整状态)。
|
||||
|
||||
- 文件的四个"位置":
|
||||
|
||||
1. **工作目录**:磁盘上的实际文件。你在这里编辑。
|
||||
2. **暂存区**(索引):你标记为下一次提交的文件。`git add` 将更改移到这里。
|
||||
3. **本地仓库**:你的提交历史,存储在 `.git/` 中。`git commit` 将暂存区保存为新的快照。
|
||||
4. **远程仓库**(例如 GitHub):一个共享副本。`git push` 上传你的提交,`git pull` 下载他人的提交。
|
||||
|
||||
```
|
||||
Working Dir → git add → Staging → git commit → Local Repo → git push → Remote
|
||||
← git pull ←
|
||||
```
|
||||
|
||||
- 暂存区正是 Git 强大之处。你可以编辑 10 个文件,但只提交其中的 3 个,将其他更改保留给另一次提交。这使得清晰的、有重点的提交成为可能。
|
||||
|
||||
### 基本命令
|
||||
|
||||
```bash
|
||||
git init # 创建新仓库
|
||||
git clone url # 下载远程仓库
|
||||
git status # 有什么变化?(最常用的命令)
|
||||
git add file.py # 暂存特定文件
|
||||
git add . # 暂存所有更改(谨慎使用)
|
||||
git commit -m "descriptive msg" # 提交暂存的更改
|
||||
git push # 将提交上传到远程
|
||||
git pull # 下载并合并远程更改
|
||||
git log --oneline # 紧凑的提交历史
|
||||
git diff # 显示未暂存的更改
|
||||
git diff --staged # 显示已暂存的更改
|
||||
```
|
||||
|
||||
## 分支
|
||||
|
||||
- **分支**是指向一次提交的指针。默认分支是 `main`(或 `master`)。创建分支让你拥有独立的开发线:你可以在不影响 `main` 的情况下进行更改。
|
||||
|
||||
```bash
|
||||
git branch feature-x # 创建分支
|
||||
git checkout feature-x # 切换到此分支
|
||||
git checkout -b feature-x # 创建并切换(一步完成)
|
||||
git branch -d feature-x # 删除分支(合并后)
|
||||
git branch -a # 列出所有分支(本地 + 远程)
|
||||
```
|
||||
|
||||
- **何时分支**:始终需要。永远不要直接提交到 `main`。每个功能、错误修复或实验都有其自己的分支。这保持了 `main` 的稳定性和可部署性。
|
||||
|
||||
### 分支策略
|
||||
|
||||
- **功能分支**(最常见):每个功能/修复从 `main` 创建一个分支。完成后,打开拉取请求(PR)以合并回去。简单,适用于大多数团队。
|
||||
|
||||
- **主干开发**:开发人员频繁(每天多次)提交到 `main`,使用特性标记隐藏未完成的工作。持续部署的团队(Google、Facebook)更偏好这种方式。需要优秀的 CI/CD。
|
||||
|
||||
- **Gitflow**:为功能、发布和热修复设置单独的分支。更复杂,适用于有版本化发布的软件(移动应用、打包软件)。对大多数机器学习项目来说过于复杂。
|
||||
|
||||
- 对于机器学习团队:**功能分支**配合短生命周期的分支(1-3 天内合并)是最佳选择。生命周期长的分支会与 `main` 产生分歧,导致痛苦的合并冲突。
|
||||
|
||||
## 合并与变基
|
||||
|
||||
- **合并**创建一个新的"合并提交",将两个分支合并:
|
||||
|
||||
```bash
|
||||
git checkout main
|
||||
git merge feature-x
|
||||
```
|
||||
|
||||
- 这保留了完整的历史记录:你可以看到工作是在分支上完成的,以及何时合并的。合并提交有两个父节点。
|
||||
|
||||
- **变基**在你的分支上重放提交到目标分支之上:
|
||||
|
||||
```bash
|
||||
git checkout feature-x
|
||||
git rebase main
|
||||
```
|
||||
|
||||
- 这会重写历史:你的分支上的提交会获得新的哈希值,就好像你是从 `main` 的当前顶端开始工作一样。结果是线性的历史记录(没有合并提交),阅读起来更清晰。
|
||||
|
||||
- **何时使用哪种**:
|
||||
- **变基**用于使用最新的 `main` 更改更新你的功能分支(保持分支整洁和最新)。
|
||||
- **合并**用于将你的功能分支集成到 `main`(保留分支历史)。
|
||||
- **永远不要变基已经推送并与他人共享**的提交。变基会重写历史;如果其他人已经基于原始提交开展工作,变基会导致混乱。
|
||||
|
||||
## 解决冲突
|
||||
|
||||
- **冲突**发生在两个分支修改同一文件的同一行时。Git 无法自动决定保留哪个更改,需要你手动解决。
|
||||
|
||||
```
|
||||
<<<<<<< HEAD
|
||||
learning_rate = 0.001
|
||||
=======
|
||||
learning_rate = 0.0005
|
||||
>>>>>>> feature-x
|
||||
```
|
||||
|
||||
- `<<<<<<< HEAD` 和 `=======` 之间是当前分支的版本。`=======` 和 `>>>>>>> feature-x` 之间是传入分支的版本。你决定保留哪个(或组合它们),删除标记,保存,然后运行 `git add` 添加已解决的文件。
|
||||
|
||||
- **陷阱**:不要在已提交的文件中留下冲突标记。它们是会破坏你代码的字面文本。解决后始终搜索 `<<<<<<<`。
|
||||
|
||||
- **减少冲突**:保持分支短生命周期,频繁将 `main` 合并到你的分支中,避免多人同时编辑同一个文件。
|
||||
|
||||
## 编写良好的提交信息
|
||||
|
||||
- 提交信息是为了未来的你和你的队友。"修复错误"告诉不了你什么。"修复批次大小计算中的差一错误,该错误导致 8-GPU 训练时 OOM"告诉你一切。
|
||||
|
||||
- **格式**:
|
||||
|
||||
```
|
||||
简短摘要(50 字以内,祈使语气)
|
||||
|
||||
如果需要,可附带更长的描述。解释 WHY,而不是 WHAT
|
||||
(差异显示了什么改变了)。每行不超过 72 个字符。
|
||||
|
||||
Fixes #123
|
||||
```
|
||||
|
||||
- **祈使语气**:"添加功能"而不是"已添加功能"或"添加了功能"。将其视为完成句子:"如果应用此提交,它将**添加功能**。"
|
||||
|
||||
- **原子提交**:每个提交应做一件事。"添加数据加载器"是一个提交。"添加数据加载器并修复无关的错误并更新 README"应该是三个提交。这使得 `git bisect`(找到哪个提交引入了错误)成为可能。
|
||||
|
||||
## 拉取请求与代码审查
|
||||
|
||||
- **拉取请求(PR)**提议将一个分支合并到 `main`。它是代码审查的门户:队友阅读你的更改,提出改进建议,并在合并前批准。
|
||||
|
||||
- **良好的 PR 实践**:
|
||||
- 保持 PR 小(少于 400 行更改)。大的 PR 会被敷衍批准,因为没人想审查 2000 行。
|
||||
- 编写清晰的描述:更改了什么、为什么以及如何测试。
|
||||
- 链接到促使更改的问题或工单。
|
||||
- 及时回复审查评论。
|
||||
- 在合并前压缩琐碎的提交(这样 `main` 就有干净的历史记录)。
|
||||
|
||||
- **代码审查不是为了找错误**(测试来做这个)。它的目的是:知识分享(审查者学习代码库)、设计反馈(这是正确的方法吗?)和维护标准(命名、风格、架构)。
|
||||
|
||||
## .gitignore
|
||||
|
||||
- `.gitignore` 文件告诉 Git 排除哪些文件不被追踪。对于机器学习项目:
|
||||
|
||||
```gitignore
|
||||
# Python
|
||||
__pycache__/
|
||||
*.pyc
|
||||
*.egg-info/
|
||||
.venv/
|
||||
env/
|
||||
|
||||
# 数据和模型(对 git 来说太大)
|
||||
data/
|
||||
*.csv
|
||||
*.parquet
|
||||
models/
|
||||
*.pt
|
||||
*.onnx
|
||||
*.bin
|
||||
checkpoints/
|
||||
|
||||
# 密钥
|
||||
.env
|
||||
*.pem
|
||||
credentials.json
|
||||
|
||||
# IDE
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
|
||||
# 操作系统
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Jupyter
|
||||
.ipynb_checkpoints/
|
||||
|
||||
# 实验输出
|
||||
wandb/
|
||||
mlruns/
|
||||
outputs/
|
||||
logs/
|
||||
```
|
||||
|
||||
- **陷阱**:在文件已被提交后将文件添加到 `.gitignore` 不会将其从仓库中移除。你还必须使用 `git rm --cached file` 来取消追踪。该文件会永远留在历史中,除非你重写历史(这很麻烦)。
|
||||
|
||||
## Git 在机器学习中的应用
|
||||
|
||||
- 机器学习引入了传统软件不面临的挑战:
|
||||
|
||||
- **大文件**:数据集和模型权重可能有数 GB 或更大。Git 是为文本文件(源代码)设计的,而不是二进制 blob。解决方案:
|
||||
- **Git LFS**(大文件存储):在 Git 中追踪指针,将实际文件存储在单独的服务器上。简单,但在 GitHub 上有限制存储/带宽。
|
||||
- **DVC**(数据版本控制):将数据和模型文件与 Git 分开管理,使用远程存储(S3、GCS)。像 Git 一样用于数据:`dvc add data.csv`、`dvc push`、`dvc pull`。
|
||||
|
||||
- **实验追踪**:哪个提交 + 哪些超参数 + 哪个数据产生了哪些指标?Git 追踪代码,但不追踪完整的实验上下文。
|
||||
- **Weights & Biases(W&B)**:记录指标、超参数、系统信息,并链接到 Git 提交。提供用于比较运行结果的仪表板。
|
||||
- **MLflow**:开源的实验追踪,带有模型注册表。记录参数、指标和产物。
|
||||
- **简单方法**:在你的训练脚本中记录 Git 哈希值:`git_hash = subprocess.check_output(['git', 'rev-parse', 'HEAD']).strip()`。将其与你的结果一起存储。
|
||||
|
||||
- **可重现性检查清单**(每个实验需要追踪的内容):
|
||||
- Git 提交哈希值(确切的代码版本)
|
||||
- 配置文件 / 超参数
|
||||
- 随机种子
|
||||
- Python 和库版本(`pip freeze`)
|
||||
- 数据版本(DVC 哈希值或数据集版本标签)
|
||||
- 硬件(GPU 类型、GPU 数量)
|
||||
|
||||
```bash
|
||||
# 快速可重现性快照
|
||||
echo "Commit: $(git rev-parse HEAD)" > experiment_info.txt
|
||||
echo "Branch: $(git branch --show-current)" >> experiment_info.txt
|
||||
echo "Dirty: $(git status --porcelain | wc -l) files" >> experiment_info.txt
|
||||
pip freeze >> experiment_info.txt
|
||||
nvidia-smi >> experiment_info.txt
|
||||
```
|
||||
@@ -0,0 +1,383 @@
|
||||
# 代码库设计与模式
|
||||
|
||||
*良好的代码库设计是区分研究原型与生产级软件的关键。本文涵盖项目结构、整洁代码原则、与机器学习相关的设计模式、配置管理、日志、API 设计以及打包分发。*
|
||||
|
||||
- 大多数机器学习代码始于 Jupyter notebook。Notebook 不断增长、被复制、修改、共享,最终变成由全局变量、死单元格和魔数组成的难以维护的混乱。**代码库设计**是一门组织代码的学科,使代码在项目增长过程中保持可理解和可修改。
|
||||
|
||||
- 这不是为了遵循规则而遵循规则。而是为了减少从"我想改变 X"到"X 已被修改并能正常工作"之间的时间。在精心设计的代码库中,这个时间是几分钟。在设计糟糕的代码库中,则需要几天的时间去考古、翻阅未记录的意大利面条式代码。
|
||||
|
||||
## 项目结构
|
||||
|
||||
- 一致的项目布局让任何人(包括未来的你)都能立即浏览代码库。
|
||||
|
||||
```
|
||||
my_project/
|
||||
├── src/my_project/ # 源代码(可导入的包)
|
||||
│ ├── __init__.py
|
||||
│ ├── data/ # 数据加载和预处理
|
||||
│ │ ├── __init__.py
|
||||
│ │ ├── dataset.py
|
||||
│ │ └── transforms.py
|
||||
│ ├── models/ # 模型架构
|
||||
│ │ ├── __init__.py
|
||||
│ │ ├── transformer.py
|
||||
│ │ └── layers.py
|
||||
│ ├── training/ # 训练循环、优化器
|
||||
│ │ ├── __init__.py
|
||||
│ │ ├── trainer.py
|
||||
│ │ └── losses.py
|
||||
│ └── utils/ # 共享工具
|
||||
│ ├── __init__.py
|
||||
│ └── logging.py
|
||||
├── configs/ # 配置文件
|
||||
│ ├── base.yaml
|
||||
│ └── experiment_1.yaml
|
||||
├── scripts/ # 入口点(训练、评估、推理)
|
||||
│ ├── train.py
|
||||
│ ├── evaluate.py
|
||||
│ └── serve.py
|
||||
├── tests/ # 测试文件(镜像 src/ 结构)
|
||||
│ ├── test_dataset.py
|
||||
│ ├── test_model.py
|
||||
│ └── test_trainer.py
|
||||
├── notebooks/ # 仅用于探索(非生产代码)
|
||||
├── pyproject.toml # 项目元数据和依赖
|
||||
├── README.md
|
||||
├── .gitignore
|
||||
└── Dockerfile
|
||||
```
|
||||
|
||||
- **`src/` 布局**:将源代码放在 `src/my_project/` 下可以防止从当前目录意外导入(这会掩盖在生产环境中才会暴露的导入错误)。使用 `pip install -e .` 进行开发安装。
|
||||
|
||||
- **单仓库 vs 多仓库**:**单仓库**将所有相关项目放在一个仓库中(跨项目更改更容易、CI 共享)。**多仓库**给每个项目自己的仓库(边界更清晰、版本控制独立)。大多数机器学习团队从单仓库开始,必要时再拆分。
|
||||
|
||||
- **脚本 vs 库**:将入口点(`train.py`、`evaluate.py`)保留在 `scripts/` 中。将可复用的逻辑放在 `src/` 中。训练脚本应约为 50 行:解析配置、构建数据集、构建模型、构建训练器、训练。所有复杂性都在库中。
|
||||
|
||||
## 整洁代码原则
|
||||
|
||||
- **命名**:你能做的唯一最有影响力的事情。名为 `x` 的变量需要你阅读周围的代码才能理解。名为 `learning_rate` 的变量是自解释的。
|
||||
|
||||
```python
|
||||
# 糟糕
|
||||
def proc(d, n, lr):
|
||||
for i in range(n):
|
||||
for k, v in d.items():
|
||||
v -= lr * g[k]
|
||||
|
||||
# 良好
|
||||
def update_parameters(parameters, num_steps, learning_rate):
|
||||
for step in range(num_steps):
|
||||
for name, param in parameters.items():
|
||||
param -= learning_rate * gradients[name]
|
||||
```
|
||||
|
||||
- **单一职责原则**:每个函数/类只做一件事。名为 `load_data_and_train_model` 的函数在做两件事,应该拆分。这使每个部分都可以独立测试、复用和理解。
|
||||
|
||||
- **DRY(不要重复自己)**——但不要过早抽象。如果你复制粘贴代码三次,将其提取为一个函数。但不要为只使用过一次的代码创建抽象。过早的抽象比重复更糟糕:它增加了复杂性但没有经过验证的好处。
|
||||
|
||||
```python
|
||||
# 过早抽象(一个用例,过度设计)
|
||||
class AbstractDataTransformPipelineFactory:
|
||||
...
|
||||
|
||||
# 恰到好处(直接、清晰、在三处使用)
|
||||
def normalise_image(image, mean, std):
|
||||
return (image - mean) / std
|
||||
```
|
||||
|
||||
- **魔数**:永远不要使用未解释的字面值。
|
||||
|
||||
```python
|
||||
# 糟糕
|
||||
if len(batch) > 32:
|
||||
split_batch(batch, 32)
|
||||
|
||||
# 良好
|
||||
MAX_BATCH_SIZE = 32
|
||||
if len(batch) > MAX_BATCH_SIZE:
|
||||
split_batch(batch, MAX_BATCH_SIZE)
|
||||
```
|
||||
|
||||
- **函数应该简短**:如果一个函数不能在一屏内显示完整(约 30 行),那它可能做得太多了。将逻辑块提取为带有描述性名称的辅助函数。然后函数体读起来就像高级摘要。
|
||||
|
||||
## 适用于机器学习的设范计式
|
||||
|
||||
- 设计模式是针对常见问题的可复用解决方案。以下是与机器学习代码库最相关的模式:
|
||||
|
||||
- **工厂模式**:在不指定确切类的情况下创建对象。当你的配置说 `model: "transformer"` 并且你需要实例化正确的类时很有用:
|
||||
|
||||
```python
|
||||
MODEL_REGISTRY = {
|
||||
"transformer": TransformerModel,
|
||||
"cnn": CNNModel,
|
||||
"mlp": MLPModel,
|
||||
}
|
||||
|
||||
def build_model(config):
|
||||
model_cls = MODEL_REGISTRY[config["model"]]
|
||||
return model_cls(**config["model_params"])
|
||||
```
|
||||
|
||||
- 这使训练脚本与特定的模型实现解耦。添加新模型意味着在注册表中添加一行,而不是修改训练循环。
|
||||
|
||||
- **策略模式**:在运行时交换算法。适用于损失函数、优化器、调度器:
|
||||
|
||||
```python
|
||||
LOSS_FUNCTIONS = {
|
||||
"mse": nn.MSELoss,
|
||||
"cross_entropy": nn.CrossEntropyLoss,
|
||||
"focal": FocalLoss,
|
||||
}
|
||||
|
||||
loss_fn = LOSS_FUNCTIONS[config["loss"]]()
|
||||
```
|
||||
|
||||
- **观察者模式**(回调/钩子):让模块响应事件而不紧密耦合。训练框架(PyTorch Lightning、Keras)广泛使用回调:
|
||||
|
||||
```python
|
||||
class EarlyStopping:
|
||||
def __init__(self, patience=5):
|
||||
self.patience = patience
|
||||
self.best_loss = float('inf')
|
||||
self.counter = 0
|
||||
|
||||
def on_epoch_end(self, epoch, val_loss):
|
||||
if val_loss < self.best_loss:
|
||||
self.best_loss = val_loss
|
||||
self.counter = 0
|
||||
else:
|
||||
self.counter += 1
|
||||
if self.counter >= self.patience:
|
||||
return "stop"
|
||||
```
|
||||
|
||||
- **依赖注入**:将依赖项传入函数/类,而不是在内部创建。这使得测试变得容易(注入 mock)并且配置灵活:
|
||||
|
||||
```python
|
||||
# 糟糕:硬编码依赖
|
||||
class Trainer:
|
||||
def __init__(self):
|
||||
self.logger = WandbLogger() # 没有 W&B 就无法测试
|
||||
|
||||
# 良好:注入依赖
|
||||
class Trainer:
|
||||
def __init__(self, logger):
|
||||
self.logger = logger # 可以注入任何记录器,包括 mock
|
||||
```
|
||||
|
||||
## 配置管理
|
||||
|
||||
- 硬编码超参数、文件路径和模型设置使实验无法重现,修改也很痛苦。**将配置外部化**到文件中。
|
||||
|
||||
- **YAML** 是机器学习配置最常见的格式:
|
||||
|
||||
```yaml
|
||||
# configs/experiment_1.yaml
|
||||
model:
|
||||
name: transformer
|
||||
d_model: 512
|
||||
n_heads: 8
|
||||
n_layers: 6
|
||||
|
||||
training:
|
||||
batch_size: 64
|
||||
learning_rate: 3e-4
|
||||
max_epochs: 100
|
||||
early_stopping_patience: 10
|
||||
|
||||
data:
|
||||
train_path: /data/train.parquet
|
||||
val_path: /data/val.parquet
|
||||
max_seq_length: 512
|
||||
```
|
||||
|
||||
- **Hydra**(Facebook)是一个支持组合(将基础配置与实验特定覆盖合并)、命令行覆盖(`python train.py training.lr=1e-3`)和多运行(超参数扫描)的配置框架。
|
||||
|
||||
- **argparse** 适用于参数较少的脚本:
|
||||
|
||||
```python
|
||||
import argparse
|
||||
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--lr", type=float, default=3e-4)
|
||||
parser.add_argument("--batch-size", type=int, default=64)
|
||||
parser.add_argument("--config", type=str, default="configs/base.yaml")
|
||||
args = parser.parse_args()
|
||||
```
|
||||
|
||||
- **最佳实践**:有一个包含所有默认值的基础配置,以及每个实验的配置,只覆盖更改的部分。追踪每个实验的配置及其结果。
|
||||
|
||||
## 日志与可观测性
|
||||
|
||||
- `print` 语句用于调试。**日志**用于生产环境:
|
||||
|
||||
```python
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logger.setLevel(logging.INFO)
|
||||
|
||||
logger.debug("Batch loaded: %d samples", len(batch)) # 详细,用于调试
|
||||
logger.info("Epoch %d: loss=%.4f, lr=%.6f", epoch, loss, lr) # 正常运行
|
||||
logger.warning("GPU memory >90%%, consider reducing batch size")
|
||||
logger.error("Failed to load checkpoint: %s", path) # 可恢复的错误
|
||||
logger.critical("CUDA out of memory, aborting") # 致命错误
|
||||
```
|
||||
|
||||
- **为什么不用 print**:日志支持级别(在生产环境中过滤调试消息)、格式化(时间戳、模块名)和处理程序(写入文件、发送到监控系统),而无需更改日志调用。
|
||||
|
||||
- **结构化日志**同时输出机器可解析的格式(JSON)和人类可读的消息。这使得可以搜索特定字段并设置告警:
|
||||
|
||||
```python
|
||||
logger.info("training_step", extra={
|
||||
"epoch": 5, "step": 1200, "loss": 0.0342, "lr": 2.1e-4
|
||||
})
|
||||
```
|
||||
|
||||
## API 设计
|
||||
|
||||
- 如果你的模型将被其他服务使用(Web 应用、移动应用、另一个机器学习管道),它需要一个 **API**(应用程序编程接口)。
|
||||
|
||||
- **REST API** 使用 HTTP 方法:`GET` 用于读取,`POST` 用于创建/预测,`PUT` 用于更新,`DELETE` 用于删除。端点遵循基于资源的命名:
|
||||
|
||||
```
|
||||
POST /api/v1/predict # 发送输入,获取预测结果
|
||||
GET /api/v1/models # 列出可用模型
|
||||
GET /api/v1/models/{id} # 获取模型详情
|
||||
POST /api/v1/models/{id}/predict # 使用特定模型进行预测
|
||||
```
|
||||
|
||||
- **FastAPI** 是机器学习推理的首选 Python 框架:
|
||||
|
||||
```python
|
||||
from fastapi import FastAPI
|
||||
from pydantic import BaseModel
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
class PredictRequest(BaseModel):
|
||||
text: str
|
||||
|
||||
class PredictResponse(BaseModel):
|
||||
label: str
|
||||
confidence: float
|
||||
|
||||
@app.post("/predict", response_model=PredictResponse)
|
||||
async def predict(request: PredictRequest):
|
||||
result = model.predict(request.text)
|
||||
return PredictResponse(label=result.label, confidence=result.score)
|
||||
```
|
||||
|
||||
- FastAPI 自动生成 API 文档(在 `/docs` 的 Swagger UI),使用 Pydantic 模型验证输入/输出,并支持异步以实现高吞吐量。
|
||||
|
||||
- **gRPC** 在内部服务间通信方面比 REST 更快。它使用 Protocol Buffers(二进制序列化,比 JSON 更小更快)并支持流式传输。TensorFlow Serving、Triton Inference Server 和许多微服务架构都使用它。
|
||||
|
||||
## 打包与分发
|
||||
|
||||
- 让你的代码可以作为包安装,使其他人(和你自己的脚本)可以干净地导入:
|
||||
|
||||
```toml
|
||||
# pyproject.toml
|
||||
[project]
|
||||
name = "my-ml-project"
|
||||
version = "0.1.0"
|
||||
requires-python = ">=3.10"
|
||||
dependencies = [
|
||||
"torch>=2.0",
|
||||
"jax>=0.4",
|
||||
"pydantic>=2.0",
|
||||
]
|
||||
|
||||
[project.optional-dependencies]
|
||||
dev = ["pytest", "ruff", "mypy"]
|
||||
|
||||
[build-system]
|
||||
requires = ["setuptools>=64"]
|
||||
build-backend = "setuptools.backends._legacy:_Backend"
|
||||
```
|
||||
|
||||
```bash
|
||||
pip install -e ".[dev]" # 以可编辑模式安装,包含开发依赖
|
||||
```
|
||||
|
||||
- **可编辑安装**(`-e`):对源代码的更改会立即生效,无需重新安装。开发期间必不可少。
|
||||
|
||||
- **锁定依赖**:使用确切版本的 `requirements.txt`(`torch==2.2.1`,而不是 `torch>=2.0`)确保可重现性。使用 `pip freeze > requirements.txt` 捕获你当前的环境。对于更复杂的依赖管理,使用 `uv`、`poetry` 或 `pip-tools`。
|
||||
|
||||
## 使用 AI 编码助手
|
||||
|
||||
- AI 编码助手(Claude Code、GitHub Copilot、Cursor 等)现在已成为专业工程师工作流程的一部分。使用得当,它们能极大加速开发。使用不当,它们会引入微妙的错误、侵蚀你对代码库的理解,并制造虚假的生产力感。
|
||||
|
||||
- 正确的心智模型:**AI 助手是一个快速但缺乏经验的结对程序员**。它可以快速编写代码,熟悉语法和标准模式,并且阅读过的文档比你还多。但它不了解你的特定系统、业务约束、边界情况以及设计决策背后的*原因*。你是高级工程师;AI 助手是初级工程师。你来指导、审查并承担责任。
|
||||
|
||||
### AI 助手擅长之处
|
||||
|
||||
- **样板代码和脚手架**:生成 Dockerfile、CI 配置、测试夹具、数据类定义、argparse 设置。这些遵循众所周知的模式,手动编写很繁琐。让 AI 生成它们,然后审查正确性。
|
||||
|
||||
- **编写测试**:描述函数的行为,AI 助手生成测试用例。它通常会捕捉到你可能会遗漏的边界情况(空输入、负值、Unicode)。始终阅读生成的测试——它们验证的是你的假设,而不仅仅是你的代码。
|
||||
|
||||
- **重构**:"将这个块提取成函数"、"将这个类改为使用 dataclasses"、"给这个模块添加类型提示"。机械性的转换,意图明确,引入细微错误的风险较低。
|
||||
|
||||
- **探索和原型开发**:"写一个快速脚本来 benchmark 推理延迟"或"展示如何使用 HuggingFace tokeniser API"。AI 助手能比阅读文档更快地给你一个可用的起点。
|
||||
|
||||
- **文档和 docstrings**:AI 助手可以根据你的代码结构生成文档。你需要审查准确性,但苦力活已经自动化了。
|
||||
|
||||
- **调试辅助**:粘贴错误回溯信息并请求诊断。AI 助手通常能识别根本原因并提出修复建议,尤其是对于常见问题(形状不匹配、导入错误、CUDA 内存不足)。
|
||||
|
||||
### 何时不应依赖 AI 助手
|
||||
|
||||
- **新颖的架构决策**:如果你正在设计一个新的训练管道,AI 助手会给出一个通用的答案。它不了解你的数据约束、延迟要求或团队专业知识。使用 AI 助手来实现你已经深思熟虑的设计。
|
||||
|
||||
- **安全关键代码**:认证、加密、输入清理。AI 助手可能生成看起来正确但存在细微漏洞的代码(SQL 注入、不安全的默认值、时序攻击)。安全代码应由理解威胁模型的人编写,并由另一个人审查。
|
||||
|
||||
- **性能关键的内循环**:AI 助手会编写正确但天真的代码。对于 GPU 内核、内存关键的数据结构或延迟敏感的推理路径,你需要理解硬件约束(第 13 章、第 16 章)并有目的地进行优化。
|
||||
|
||||
- **你不理解的代码**:如果 AI 助手生成了 200 行代码,而你无法解释每一行的作用,那就不要提交。你现在正在维护你不理解的代码,当它出问题时(它会的),你无法调试。这是最常见也最危险的失败模式。
|
||||
|
||||
### 审查纪律
|
||||
|
||||
- **在提交前始终逐行阅读**生成的代码。这不是可选的。AI 助手的代码是草稿,不是成品。就像对待同事的拉取请求一样:批判性地审查它。
|
||||
|
||||
- **检查什么**:
|
||||
- **正确性**:它是否真的做了你要求的事情?AI 助手经常解决与你意图略有不同的问题。
|
||||
- **边界情况**:它是否处理了空输入、None 值、负数、非常大的输入?AI 助手经常省略边界情况处理。
|
||||
- **幻想的 API**:AI 助手可能调用不存在函数或使用不存在的参数,尤其是对于较新或较少使用的库。验证每个 API 调用是否真实存在。
|
||||
- **过度工程**:AI 助手倾向于产生比必要更多的代码。一个 50 行的解决方案解决一个 10 行的问题,增加了不必要的复杂性。无情地简化。
|
||||
- **安全性**:硬编码的密钥、未经清理的用户输入、不安全的默认值。AI 助手不会以对抗性思维思考。
|
||||
- **风格一致性**:生成的代码是否与项目的约定一致(命名、模式、错误处理)?
|
||||
|
||||
### 如何编写好的提示词
|
||||
|
||||
- AI 助手输出的质量直接与你的指令质量成正比。模糊的提示词得到模糊的代码。
|
||||
|
||||
- **糟糕**:"写一个数据加载器"
|
||||
- **好**:"为一个包含'text'和'label'列的 CSV 文件编写一个 PyTorch DataLoader。使用 HuggingFace tokeniser 'bert-base-uncased' 对文本进行分词,max_length=512。返回 input_ids、attention_mask 和 label 作为张量。处理 CSV 中标签列有缺失值的情况,跳过那些行。"
|
||||
|
||||
- **提供上下文**:告诉 AI 助手你的项目结构、现有代码、约束和约定。上下文越多,输出越好。
|
||||
|
||||
- **指定约束**:"只使用标准库"、"必须兼容 Python 3.10"、"不要使用全局变量"、"遵循 `src/models/transformer.py` 中的现有模式"。
|
||||
|
||||
- **要求解释**:"实现 X 并解释关键的设计决策。"这会迫使 AI 助手阐述其推理,使你更容易发现错误假设。
|
||||
|
||||
### 使用质量门控来捕捉 AI 助手的错误
|
||||
|
||||
- 你现有的质量基础设施(文件 04)捕捉 AI 助手的错误与捕捉人类的错误同样有效:
|
||||
|
||||
- **类型检查(mypy)**:捕捉幻想的 API 签名和类型不匹配。
|
||||
- **代码检查(ruff)**:捕捉未使用的导入、未定义的变量和风格违规。
|
||||
- **测试(pytest)**:如果 AI 助手的代码通过了你的测试套件,它更可能是正确的。如果你还没有测试,在要求 AI 助手实现功能之前*先编写测试*(测试驱动开发与 AI 助手配合得特别好)。
|
||||
- **CI 管道**:在每次提交时自动运行上述所有检查。
|
||||
|
||||
- **"AI 助手写代码" + "质量门控验证"** 的组合比单独使用任何一种都更高效。AI 助手快速但草率;门控工具彻底但不写代码。两者结合,你同时获得速度和正确性。
|
||||
|
||||
### 生产力陷阱
|
||||
|
||||
- 使用编码助手的最大风险是**生产力的幻觉**。你可以在 10 分钟内生成 500 行代码。但如果你花 2 小时调试这些你并不理解的 500 行代码,那还不如自己花 30 分钟写 200 行代码来得快。
|
||||
|
||||
- 使用 AI 助手的真正生产力来自:
|
||||
1. **保持控制**:你决定架构,AI 助手填入实现。
|
||||
2. **理解生成的内容**:如果你无法解释它,就重写它或让 AI 助手简化它。
|
||||
3. **投资质量门控**:测试、类型和代码检查的成本通过每次 AI 交互分摊。
|
||||
4. **利用 AI 助手弥补你的弱点**:如果你擅长算法但编写测试很慢,让 AI 助手写测试。如果你对 UI 代码很快但不熟悉数据库查询,让 AI 助手草拟 SQL。发挥你的优势,委托你的短板。
|
||||
|
||||
- 从编码助手中获益最多的工程师是那些已经擅长编码的人。AI 助手放大你现有的技能;它不会取代你的技能。理解数据结构、算法、系统设计和软件工程(整章的内容)让你能够有效地指导 AI 助手并批判性地评估其输出。
|
||||
@@ -0,0 +1,322 @@
|
||||
# 测试与质量保障
|
||||
|
||||
*测试是你如何确保代码正常工作的方法——不仅是现在,而且在每次更改后都能正常工作。本文涵盖测试金字塔、使用 pytest 进行的单元测试、Mock、测试机器学习特定代码、CI/CD 管道、代码检查、格式化和代码审查——这些实践能在错误到达生产环境之前捕获它们。*
|
||||
|
||||
- 机器学习代码以缺乏测试而闻名。"能训练,所以能工作"是普遍态度。这会导致静默错误:一个错误地打乱数据的数据加载器、一个有符号错误的损失函数、一个丢弃 5% 数据的预处理步骤。这些错误不会使你的程序崩溃。它们只是让你的模型悄悄变差,然后你浪费数周时间调试"本应更高"的指标。
|
||||
|
||||
- 测试不是额外负担。它是快速前进而不破坏东西的最快方式。
|
||||
|
||||
## 测试金字塔
|
||||
|
||||
- 测试按层级组织,从快速且狭窄到慢速且广泛:
|
||||
|
||||
- **单元测试**(底层):隔离测试单个函数和类。快速(毫秒级),数量多(数百到数千)。"`normalise_image` 是否产生 [0, 1] 范围内的值?"
|
||||
|
||||
- **集成测试**(中层):测试组件协同工作。较慢(秒级)。"数据加载器是否以模型期望的格式产生批次?"
|
||||
|
||||
- **端到端测试**(顶层):测试从输入到输出的完整管道。较慢(分钟级)。"`python train.py --config test.yaml` 是否无错误完成并产生有效的检查点?"
|
||||
|
||||
- 金字塔形状意味着:编写大量单元测试,较少数量的集成测试,以及少量端到端测试。单元测试捕获大多数错误,并在几秒钟内运行。端到端测试捕获集成问题,但慢且脆弱。
|
||||
|
||||
## 使用 pytest 进行单元测试
|
||||
|
||||
- **pytest** 是标准的 Python 测试框架。测试是以 `test_` 开头的函数,放在以 `test_` 开头的文件中:
|
||||
|
||||
```python
|
||||
# tests/test_utils.py
|
||||
|
||||
def test_normalise_image():
|
||||
import numpy as np
|
||||
image = np.array([0, 128, 255], dtype=np.uint8)
|
||||
result = normalise_image(image, mean=128, std=128)
|
||||
assert result.min() >= -1.0
|
||||
assert result.max() <= 1.0
|
||||
assert abs(result[1]) < 1e-6 # 128 被 mean=128 归一化后应约为 0
|
||||
|
||||
def test_normalise_empty():
|
||||
import numpy as np
|
||||
image = np.array([], dtype=np.uint8)
|
||||
result = normalise_image(image, mean=128, std=128)
|
||||
assert len(result) == 0
|
||||
```
|
||||
|
||||
```bash
|
||||
pytest tests/ # 运行所有测试
|
||||
pytest tests/test_utils.py # 运行一个文件
|
||||
pytest -v # 详细输出
|
||||
pytest -x # 在第一个失败时停止
|
||||
pytest -k "normalise" # 运行匹配名称模式的测试
|
||||
pytest --tb=short # 更短的追溯信息
|
||||
```
|
||||
|
||||
### 夹具
|
||||
|
||||
- **夹具**为测试提供可复用的设置。无需在每个测试中重复设置代码,只需定义一次:
|
||||
|
||||
```python
|
||||
import pytest
|
||||
|
||||
@pytest.fixture
|
||||
def sample_dataset():
|
||||
"""创建一个用于测试的小型数据集。"""
|
||||
return {
|
||||
"inputs": torch.randn(10, 3, 32, 32),
|
||||
"labels": torch.randint(0, 10, (10,))
|
||||
}
|
||||
|
||||
@pytest.fixture
|
||||
def trained_model():
|
||||
"""加载一个小型预训练模型。"""
|
||||
model = SmallModel()
|
||||
model.load_state_dict(torch.load("tests/fixtures/small_model.pt"))
|
||||
return model
|
||||
|
||||
def test_model_output_shape(trained_model, sample_dataset):
|
||||
output = trained_model(sample_dataset["inputs"])
|
||||
assert output.shape == (10, 10) # batch_size x num_classes
|
||||
```
|
||||
|
||||
- 夹具可以有**作用域**:`scope="function"`(默认,每次测试重新创建)、`scope="module"`(每个文件一次)、`scope="session"`(每次测试运行一次)。对于加载模型等昂贵设置,使用 `scope="session"`。
|
||||
|
||||
### 参数化测试
|
||||
|
||||
- 使用多个输入测试同一个函数,无需重复代码:
|
||||
|
||||
```python
|
||||
@pytest.mark.parametrize("input,expected", [
|
||||
([1, 2, 3], 6),
|
||||
([], 0),
|
||||
([-1, 1], 0),
|
||||
([1000000, 1000000], 2000000),
|
||||
])
|
||||
def test_sum(input, expected):
|
||||
assert sum(input) == expected
|
||||
```
|
||||
|
||||
## Mock 与补丁
|
||||
|
||||
- **Mock** 在测试期间用假依赖替换真实依赖。这让你可以隔离测试函数,而无需数据库、API 或 GPU。
|
||||
|
||||
```python
|
||||
from unittest.mock import patch, MagicMock
|
||||
|
||||
def test_training_logs_metrics():
|
||||
mock_logger = MagicMock()
|
||||
|
||||
with patch("my_project.training.trainer.wandb") as mock_wandb:
|
||||
trainer = Trainer(logger=mock_logger)
|
||||
trainer.train_one_epoch()
|
||||
|
||||
# 验证训练器记录了指标
|
||||
mock_logger.log.assert_called()
|
||||
# 验证它记录了损失值
|
||||
call_args = mock_logger.log.call_args
|
||||
assert "loss" in call_args[1]
|
||||
```
|
||||
|
||||
- **何时使用 Mock**:外部服务(API、数据库、云存储)、昂贵操作(GPU 计算、大型文件 I/O)和非确定性行为(随机数生成器、时间戳)。
|
||||
|
||||
- **何时不要 Mock**:你自己的代码。如果你 Mock 了所有内容,你的测试验证的是 Mock 的行为符合预期,而不是你的代码能工作。在边界处进行 Mock,直接测试你的逻辑。
|
||||
|
||||
## 测试机器学习代码
|
||||
|
||||
- 机器学习代码有独特的测试挑战:输出是概率性的,训练很慢,而且"正确"是模糊的。
|
||||
|
||||
### 确定性种子
|
||||
|
||||
- 在所有地方设置随机种子,使测试可重现:
|
||||
|
||||
```python
|
||||
import random
|
||||
import numpy as np
|
||||
import torch
|
||||
|
||||
def set_seed(seed=42):
|
||||
random.seed(seed)
|
||||
np.random.seed(seed)
|
||||
torch.manual_seed(seed)
|
||||
if torch.cuda.is_available():
|
||||
torch.cuda.manual_seed_all(seed)
|
||||
torch.backends.cudnn.deterministic = True
|
||||
torch.backends.cudnn.benchmark = False
|
||||
```
|
||||
|
||||
### 数值容差
|
||||
|
||||
- 浮点数比较需要容差(第 13 章,IEEE 754):
|
||||
|
||||
```python
|
||||
# 糟糕:由于浮点数问题,精确比较会失败
|
||||
assert model_output == 0.5
|
||||
|
||||
# 良好:近似比较
|
||||
import numpy as np
|
||||
assert np.isclose(model_output, 0.5, atol=1e-5)
|
||||
|
||||
# 对于张量
|
||||
assert torch.allclose(output, expected, atol=1e-4)
|
||||
```
|
||||
|
||||
### 机器学习中需要测试什么
|
||||
|
||||
- **形状测试**:验证输出具有预期的维度。
|
||||
|
||||
```python
|
||||
def test_model_output_shape():
|
||||
model = MyModel(d_model=256, n_classes=10)
|
||||
x = torch.randn(8, 32, 256) # batch=8, seq=32, dim=256
|
||||
output = model(x)
|
||||
assert output.shape == (8, 10)
|
||||
```
|
||||
|
||||
- **梯度流**:验证可训练参数具有非零梯度。
|
||||
|
||||
```python
|
||||
def test_gradients_flow():
|
||||
model = MyModel()
|
||||
x = torch.randn(4, 3, 32, 32)
|
||||
y = torch.randint(0, 10, (4,))
|
||||
|
||||
output = model(x)
|
||||
loss = F.cross_entropy(output, y)
|
||||
loss.backward()
|
||||
|
||||
for name, param in model.named_parameters():
|
||||
assert param.grad is not None, f"没有 {name} 的梯度"
|
||||
assert param.grad.abs().sum() > 0, f"{name} 的梯度为零"
|
||||
```
|
||||
|
||||
- **在一个批次上过拟合**:模型应该能够记忆单个批次。如果不能,说明某处存在根本性问题。
|
||||
|
||||
```python
|
||||
def test_overfit_one_batch():
|
||||
model = MyModel()
|
||||
optimiser = torch.optim.Adam(model.parameters(), lr=1e-3)
|
||||
x, y = get_single_batch()
|
||||
|
||||
for _ in range(100):
|
||||
loss = F.cross_entropy(model(x), y)
|
||||
loss.backward()
|
||||
optimiser.step()
|
||||
optimiser.zero_grad()
|
||||
|
||||
assert loss.item() < 0.01, f"无法过拟合单个批次:loss={loss.item()}"
|
||||
```
|
||||
|
||||
- **数据验证**:验证数据加载产生有效输出。
|
||||
|
||||
```python
|
||||
def test_dataset_basics():
|
||||
dataset = MyDataset("tests/fixtures/small_data.csv")
|
||||
assert len(dataset) > 0
|
||||
x, y = dataset[0]
|
||||
assert x.shape == (3, 224, 224)
|
||||
assert 0 <= y < 10
|
||||
assert not torch.isnan(x).any()
|
||||
assert not torch.isinf(x).any()
|
||||
```
|
||||
|
||||
- **确定性**:相同输入 + 相同种子 → 相同输出。
|
||||
|
||||
```python
|
||||
def test_determinism():
|
||||
set_seed(42)
|
||||
output1 = model(input_data)
|
||||
set_seed(42)
|
||||
output2 = model(input_data)
|
||||
assert torch.allclose(output1, output2)
|
||||
```
|
||||
|
||||
## CI/CD 管道
|
||||
|
||||
- **持续集成(CI)**:在每次提交或 PR 上自动运行测试。如果测试失败,PR 不能合并。这防止了损坏的代码到达 `main`。
|
||||
|
||||
- **GitHub Actions** 示例(`.github/workflows/ci.yml`):
|
||||
|
||||
```yaml
|
||||
name: CI
|
||||
on: [push, pull_request]
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.11"
|
||||
- run: pip install -e ".[dev]"
|
||||
- run: ruff check src/
|
||||
- run: mypy src/
|
||||
- run: pytest tests/ -v --tb=short
|
||||
```
|
||||
|
||||
- **预提交钩子**:在每次提交前(本地)运行检查,在它们到达 CI 之前捕获问题:
|
||||
|
||||
```yaml
|
||||
# .pre-commit-config.yaml
|
||||
repos:
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: v0.3.0
|
||||
hooks:
|
||||
- id: ruff
|
||||
args: [--fix]
|
||||
- id: ruff-format
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v4.5.0
|
||||
hooks:
|
||||
- id: trailing-whitespace
|
||||
- id: end-of-file-fixer
|
||||
- id: check-yaml
|
||||
```
|
||||
|
||||
```bash
|
||||
pip install pre-commit
|
||||
pre-commit install # 现在每次 git 提交时都会运行钩子
|
||||
```
|
||||
|
||||
## 代码检查与格式化
|
||||
|
||||
- **代码检查**无需运行代码即可捕获错误和风格问题。**格式化**自动强制执行一致的风格。
|
||||
|
||||
- **Ruff**:一个快速的 Python 代码检查器和格式化器(在一个工具中替代 flake8、isort 和 black):
|
||||
|
||||
```bash
|
||||
ruff check src/ # 代码检查
|
||||
ruff check --fix src/ # 代码检查并自动修复
|
||||
ruff format src/ # 格式化
|
||||
```
|
||||
|
||||
- **mypy**:Python 静态类型检查器。在运行时之前捕获类型错误:
|
||||
|
||||
```bash
|
||||
mypy src/
|
||||
# src/model.py:42: error: Argument 1 to "forward" has incompatible type "int"; expected "Tensor"
|
||||
```
|
||||
|
||||
- 类型提示使代码自文档化并捕获错误:
|
||||
|
||||
```python
|
||||
def train(
|
||||
model: nn.Module,
|
||||
dataloader: DataLoader,
|
||||
optimiser: torch.optim.Optimizer,
|
||||
num_epochs: int = 10,
|
||||
) -> float:
|
||||
"""训练模型并返回最终损失。"""
|
||||
...
|
||||
```
|
||||
|
||||
## 代码审查最佳实践
|
||||
|
||||
- **对于作者**:
|
||||
- 在请求审查之前先自我审查你的差异。你会发现明显的问题。
|
||||
- 保持 PR 小而专注。一个 PR 聚焦一个问题。
|
||||
- 写清晰的描述:什么、为什么、如何测试。
|
||||
- 回复每条评论(即使只是"已修改")。
|
||||
|
||||
- **对于审查者**:
|
||||
- 保持友善。批评代码,而不是人。"这里可以更清晰"而不是"这很令人困惑。"
|
||||
- 区分阻塞性问题(错误、安全)和建议(风格、命名)。使用标签:"nit:"、"suggestion:"、"blocking:"。
|
||||
- 提问而不是发号施令。"如果这个列表为空会怎样?"比"处理空的情况"更有帮助。
|
||||
- 及时批准。等待数天的 PR 会阻塞作者,并鼓励大型、批量的 PR(这些更难审查)。
|
||||
@@ -0,0 +1,233 @@
|
||||
# 部署与 DevOps
|
||||
|
||||
*部署是你的模型从研究产物变成产品的地方。本文涵盖用于机器学习的 Docker、模型推理、实验追踪、可重现性、生产环境监控、特征存储和管道编排——这些基础设施将一个训练好的模型从 notebook 带到数百万用户面前。*
|
||||
|
||||
- 一个只在你笔记本电脑上运行的模型是原型。一个能够可靠地大规模运行、在毫秒内提供预测结果、能够从故障中恢复并在不中断服务的情况下更新的模型才是产品。两者之间的差距就是**部署与 DevOps**。
|
||||
|
||||
- 大多数机器学习工程师在部署、监控和调试生产问题上花费的时间比训练模型还多。理解这些基础设施对于任何构建真实 ML 系统的人来说都不是可选项。
|
||||
|
||||
## 用于机器学习的 Docker
|
||||
|
||||
- 我们在第 13 章(操作系统)中概念性地介绍了容器。这里我们关注实践方面:为机器学习工作负载编写 Dockerfile。
|
||||
|
||||
- **Dockerfile** 是构建容器镜像的配方:
|
||||
|
||||
```dockerfile
|
||||
# 从官方的 CUDA 基础镜像开始
|
||||
FROM nvidia/cuda:12.1.0-cudnn8-runtime-ubuntu22.04
|
||||
|
||||
# 系统依赖
|
||||
RUN apt-get update && apt-get install -y \
|
||||
python3.11 python3-pip git \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Python 依赖(单独安装以利用缓存)
|
||||
COPY requirements.txt .
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
# 复制源代码(频繁更改,因此此层放在最后)
|
||||
COPY src/ /app/src/
|
||||
COPY configs/ /app/configs/
|
||||
WORKDIR /app
|
||||
|
||||
# 入口点
|
||||
CMD ["python3", "src/scripts/serve.py", "--config", "configs/serve.yaml"]
|
||||
```
|
||||
|
||||
- **层缓存**:Docker 会缓存每一层。如果 `requirements.txt` 没有变化,`pip install` 在重新构建时会被跳过。将不常更改的层(系统包、pip 安装)放在频繁更改的层(源代码)之前。这将 10 分钟的构建变成 10 秒的重新构建。
|
||||
|
||||
- **GPU 访问**:使用 `nvidia/cuda` 基础镜像,并使用 `docker run --gpus all` 运行。`nvidia-container-toolkit` 提供从宿主机到容器的 GPU 透传。
|
||||
|
||||
- **多阶段构建**通过将构建环境与运行环境分离来减小镜像大小:
|
||||
|
||||
```dockerfile
|
||||
# 构建阶段:安装构建工具、编译依赖
|
||||
FROM python:3.11 AS builder
|
||||
COPY requirements.txt .
|
||||
RUN pip install --user -r requirements.txt
|
||||
|
||||
# 运行阶段:仅运行环境依赖
|
||||
FROM nvidia/cuda:12.1.0-cudnn8-runtime-ubuntu22.04
|
||||
COPY --from=builder /root/.local /root/.local
|
||||
COPY src/ /app/src/
|
||||
ENV PATH=/root/.local/bin:$PATH
|
||||
```
|
||||
|
||||
- 最终镜像只包含运行时库,不包含编译器、头文件或构建工具。一个 5GB 的构建镜像变成了 2GB 的运行镜像。
|
||||
|
||||
- **Docker Compose** 运行多容器设置(模型服务器 + 负载均衡器 + 监控):
|
||||
|
||||
```yaml
|
||||
# docker-compose.yml
|
||||
services:
|
||||
model:
|
||||
build: .
|
||||
ports:
|
||||
- "8080:8080"
|
||||
deploy:
|
||||
resources:
|
||||
reservations:
|
||||
devices:
|
||||
- capabilities: [gpu]
|
||||
prometheus:
|
||||
image: prom/prometheus
|
||||
ports:
|
||||
- "9090:9090"
|
||||
```
|
||||
|
||||
## 模型推理
|
||||
|
||||
- **模型推理**是将推理作为服务运行:接收请求、运行模型、返回预测结果。
|
||||
|
||||
- **FastAPI**(在文件 03 中介绍)适用于低到中等吞吐量的最简单方法。对于高吞吐量和 GPU 优化推理,使用专用工具:
|
||||
|
||||
- **Triton Inference Server**(NVIDIA):以 TensorRT、ONNX、PyTorch 和 TensorFlow 格式提供模型。特性:
|
||||
- **动态批处理**:收集单个请求并将它们分批处理以提高 GPU 效率。单个请求流被分组为 32 的批次,大幅提高吞吐量。
|
||||
- **模型集成**:在单个请求中链式调用多个模型(预处理器 → 模型 → 后处理器)。
|
||||
- **多模型推理**:在同一 GPU 上提供多个模型,共享资源。
|
||||
- **并发模型执行**:在同一 GPU 上并行运行多个推理请求。
|
||||
|
||||
- **TorchServe**(PyTorch):以 REST/gRPC API 提供 PyTorch 模型。支持模型版本控制、A/B 测试和自定义处理器。
|
||||
|
||||
- **vLLM**:专门用于 LLM 推理。实现了 PagedAttention(高效的 KV 缓存管理)、连续批处理和跨 GPU 的张量并行。对于大语言模型,吞吐量比朴素推理高出 10-20 倍。
|
||||
|
||||
- **Cactus**([github.com/cactus-compute/cactus](https://github.com/cactus-compute/cactus)):一个用于移动端和边缘端设备推理的低延迟 AI 引擎。Cactus 提供**兼容 OpenAI 的 API**(聊天补全、流式传输、工具调用、转录、嵌入、RAG、视觉),完全在设备上运行,当本地模型无法处理请求时自动进行**云回退**。这种混合架构意味着你的应用程序代码使用相同的 API,无论推理是在本地还是在云端运行——引擎根据模型置信度和设备能力来决定。提供 Python、Swift、Kotlin、Flutter、React Native 和 Rust 的 SDK,以及 HuggingFace 上预转换的模型权重。支持多模态推理(LLM、视觉、语音),配备自定义 ARM SIMD 内核以实现 ARM CPU 上的最快推理,以及零拷贝内存映射以实现 10 倍 RAM 使用降低(第 16 章、第 17 章)。
|
||||
|
||||
- **模型格式优化**:
|
||||
- **ONNX**:用于互操作性的开放格式。从 PyTorch/TensorFlow 导出,在任何地方运行。
|
||||
- **TensorRT**:NVIDIA 的优化器。融合层、选择最佳内核、量化权重。在 NVIDIA GPU 上通常比 PyTorch 快 2-5 倍。
|
||||
- **GGUF/GGML**:适用于 CPU 高效推理的格式,在消费级硬件上运行 LLM 时很流行。
|
||||
|
||||
## 实验追踪
|
||||
|
||||
- 没有实验追踪,机器学习研究会退化为:"我觉得上周二那个我改了些配置的模型是最好的,但我不记得改了啥。"
|
||||
|
||||
- **Weights & Biases(W&B)**:最流行的实验追踪工具。从你的训练脚本中记录任何内容:
|
||||
|
||||
```python
|
||||
import wandb
|
||||
|
||||
wandb.init(project="my-project", config={
|
||||
"model": "transformer",
|
||||
"lr": 3e-4,
|
||||
"batch_size": 64,
|
||||
})
|
||||
|
||||
for epoch in range(num_epochs):
|
||||
train_loss = train_one_epoch()
|
||||
val_loss = validate()
|
||||
|
||||
wandb.log({
|
||||
"train/loss": train_loss,
|
||||
"val/loss": val_loss,
|
||||
"epoch": epoch,
|
||||
})
|
||||
|
||||
# 将模型记录为产物
|
||||
if val_loss < best_loss:
|
||||
wandb.save("best_model.pt")
|
||||
|
||||
wandb.finish()
|
||||
```
|
||||
|
||||
- W&B 提供:用于比较运行的仪表板、超参数扫描工具、模型注册表、数据集版本控制和团队协作。
|
||||
|
||||
- **MLflow**:开源替代方案。在本地或服务器上运行:
|
||||
|
||||
```python
|
||||
import mlflow
|
||||
|
||||
mlflow.set_experiment("my-experiment")
|
||||
|
||||
with mlflow.start_run():
|
||||
mlflow.log_params({"lr": 3e-4, "batch_size": 64})
|
||||
mlflow.log_metric("val_loss", 0.042, step=epoch)
|
||||
mlflow.pytorch.log_model(model, "model")
|
||||
```
|
||||
|
||||
- **模型注册表**:训练模型的中央存储,带版本控制、阶段(开发 → 预发布 → 生产)和元数据。W&B 和 MLflow 都提供注册表。注册表回答:"当前生产环境中的是哪个模型,谁训练的,其验证准确率是多少,以及由哪个代码/数据产生?"
|
||||
|
||||
## 可重现性
|
||||
|
||||
- 可重现性意味着:给定相同的代码、数据和配置,产生相同的模型。这在机器学习中出奇地困难,因为 GPU 操作的非确定性、数据打乱和浮点数累积。
|
||||
|
||||
- **可重现性检查清单**:
|
||||
|
||||
| 什么 | 如何做 |
|
||||
|------|------|
|
||||
| 代码版本 | Git 提交哈希值 |
|
||||
| 配置 / 超参数 | 配置文件(在 Git 中版本控制或记录到 W&B) |
|
||||
| 随机种子 | 设置并记录所有种子(Python、NumPy、PyTorch、CUDA) |
|
||||
| 数据版本 | DVC 哈希值、数据集版本标签或 S3 对象版本 |
|
||||
| 依赖项 | `pip freeze`、Docker 镜像哈希值或锁定文件 |
|
||||
| 硬件 | GPU 类型、GPU 数量、CUDA 版本 |
|
||||
| 非确定性 | `torch.backends.cudnn.deterministic = True`(较慢但可重现) |
|
||||
|
||||
- **锁定所有内容**:`pip install torch==2.2.1` 而不是 `torch>=2.0`。次版本号升级可能改变数值行为、优化器实现或默认超参数。
|
||||
|
||||
- **使用 Docker 实现可重现性**:Docker 镜像锁定了操作系统、系统库、Python 版本和 pip 包。镜像哈希值是完整的环境指纹。如果你能重现 Docker 镜像,就能重现训练。
|
||||
|
||||
## 生产环境监控
|
||||
|
||||
- 部署模型不是终点——而是一系列新问题的开始。随着现实世界的变化(**概念漂移**)以及输入数据分布的变化(**数据漂移**),模型会随时间推移而退化。
|
||||
|
||||
- **需要监控的内容**:
|
||||
|
||||
- **延迟**:推理需要多长时间?追踪 p50(中位数)、p95 和 p99。p99 为 500ms 意味着每 100 个用户中有 1 个要等待半秒钟,这可能不可接受。
|
||||
|
||||
- **吞吐量**:每秒处理多少个请求?系统是否跟得上需求?
|
||||
|
||||
- **错误率**:有多少比例的请求失败(异常、超时、无效输入)?
|
||||
|
||||
- **模型指标**:在验证集上的准确率、精确率、召回率。如果生产环境中存在标注数据(例如用户纠正),追踪在线指标。
|
||||
|
||||
- **数据漂移**:输入数据的分布是否发生了变化?在白天照片上训练的模型可能在夜间照片上失败。统计检验(KS 检验、PSI)将训练分布与在线分布进行比较。
|
||||
|
||||
- **特征漂移**:单个特征的分布是否发生了变化?训练时呈正态分布但在生产时呈双峰分布的特征,表明数据管道存在问题。
|
||||
|
||||
- **工具**:
|
||||
- **Prometheus** + **Grafana**:基础设施监控的标准方案。Prometheus 收集指标,Grafana 将其可视化为带告警的仪表板。
|
||||
- **Evidently AI**:开源机器学习监控。生成关于数据漂移、模型性能和数据质量的报告。
|
||||
|
||||
- **告警**:不要只放在仪表板上——设置自动告警。"如果 p99 延迟超过 200ms 持续 5 分钟,发送 Slack 通知。""如果数据漂移评分超过阈值,通知值班工程师。"
|
||||
|
||||
## 特征存储
|
||||
|
||||
- **特征存储**是预计算特征的集中式仓库,在训练和推理之间共享。它解决两个问题:
|
||||
|
||||
- **训练-推理偏差**:训练期间使用的特征必须与推理期间使用的特征完全相同。如果训练使用一种方式计算的 `user_age_at_signup`,而推理使用不同的方式计算,模型的预测结果会静默出错。
|
||||
|
||||
- **特征复用**:多个模型通常使用相同的特征(用户人口统计、物品嵌入、聚合统计)。计算一次并共享,避免了重复和不一致性。
|
||||
|
||||
- **Feast** 是最流行的开源特征存储。它管理在线特征(低延迟,从 Redis 或 DynamoDB 提供)和离线特征(批处理,存储在数据仓库中用于训练)。
|
||||
|
||||
- 特征存储对于推荐系统、欺诈检测以及任何特征从原始数据管道计算而来的应用都至关重要。
|
||||
|
||||
## 管道编排
|
||||
|
||||
- 生产级机器学习系统不仅仅是模型。它是一个**管道**:数据采集 → 预处理 → 特征计算 → 训练 → 评估 → 部署 → 监控。每个步骤依赖于前一步骤,可以独立失败,可能需要在不同的时间表上运行。
|
||||
|
||||
- **编排器**管理这些管道:
|
||||
|
||||
- **Apache Airflow**:数据管道编排的标准方案。DAG(有向无环图)定义任务依赖关系。每个任务独立运行,失败时可以重试,并通过 Web UI 进行监控。
|
||||
|
||||
```python
|
||||
# airflow DAG 示例(简化)
|
||||
from airflow import DAG
|
||||
from airflow.operators.python import PythonOperator
|
||||
|
||||
dag = DAG("training_pipeline", schedule="@daily")
|
||||
|
||||
preprocess = PythonOperator(task_id="preprocess", python_callable=preprocess_data, dag=dag)
|
||||
train = PythonOperator(task_id="train", python_callable=train_model, dag=dag)
|
||||
evaluate = PythonOperator(task_id="evaluate", python_callable=evaluate_model, dag=dag)
|
||||
deploy = PythonOperator(task_id="deploy", python_callable=deploy_model, dag=dag)
|
||||
|
||||
preprocess >> train >> evaluate >> deploy
|
||||
```
|
||||
|
||||
- **Kubeflow Pipelines**:在 Kubernetes 上运行机器学习特定编排。每个步骤在容器中运行,GPU 资源按需分配,实验自动追踪。
|
||||
|
||||
- **Prefect** 和 **Dagster**:Airflow 的现代替代方案,拥有更好的开发者体验、原生 Python API 和内置数据血缘追踪。
|
||||
|
||||
- **何时需要编排**:当你的管道有超过 2-3 个步骤、按计划运行、涉及多个团队或服务、或需要自动故障恢复时。单一脚本的训练任务不需要编排器。每天重新训练的管道——从 5 个数据源采集数据、训练 3 个模型、评估它们并部署最佳模型——绝对需要。
|
||||
Reference in New Issue
Block a user