Python 与 AI 大模型:从原理到 Transformer
Python 与 AI 大模型:从原理到 Transformer 实战
面向:想搞懂"Python 到底怎么让 Transformer 跑起来"的开发者 | 整理:Operit AI | 2026-09 配套资料:
py-ai-demo/下可直接运行的 Python 脚本 姊妹篇:《SQL数据库深度讲解-工业实践版》《JavaEE企业级开发》
0. 导读:为什么是 Python 驱动了整个大模型时代
三条主线:
| 能力 | 具体表现 | 对应章节 |
|---|---|---|
| 写得出 | 用 Python 搭一个能跑的 Transformer 前向/反向 | 第 1~4 章 |
| 跑得快 | GPU/CUDA、混合精度、并行、算子融合 | 第 5 章 |
| 跑得稳 | 推理服务化、显存治理、部署落地 | 第 6~7 章 |
一句话总览:Python 本身不"算",它是"调度员"——真正的矩阵乘法在 CUDA/cuBLAS 里跑,Python 负责把计算图搭好、把数据搬进显存、把梯度反传回去、把权重存下来。理解了这一点,就理解了大模型的运行本质。
1. Python 在 AI 栈里的真实位置
1.1 一张图说清分层
┌─────────────────────────────────────┐
│ 应用层:FastAPI / Gradio / vLLM │ ← Python 写服务
├─────────────────────────────────────┤
│ 框架层:PyTorch / TensorFlow / JAX │ ← Python 搭计算图
├─────────────────────────────────────┤
│ 算子层:cuBLAS / cuDNN / Triton │ ← C++/CUDA 真干活
├─────────────────────────────────────┤
│ 硬件层:NVIDIA GPU / TPU / NPU │ ← 算力
└─────────────────────────────────────┘
Python 出现在应用层 + 框架层。它的核心价值不是性能,而是:
- 胶水:把 C++/CUDA 写成的高性能算子暴露成 torch.matmul 这种 Python 接口
- 表达力:动态图、任意控制流、调试友好
- 生态:NumPy、Pandas、HuggingFace、LangChain 全在 Python
1.2 一个最小例子:Python 调 CUDA 做矩阵乘
import torch
A = torch.randn(1024, 1024, device="cuda")
B = torch.randn(1024, 1024, device="cuda")
C = A @ B # ← 这一行,Python 只发了一个"调用"
print(C.shape) # 真正的乘法在 GPU 上由 cuBLAS 执行
执行流程:
1. Python 解释器执行 A @ B
2. PyTorch 的 C++ 前端把 __matmul__ 分派到 CUDA 后端
3. cuBLAS 的 cublasGemmEx 在 GPU 上做 10 亿次浮点运算
4. 结果张量 C 的元数据返回 Python,数据仍留在显存
关键认知:Python 进程的 CPU 几乎闲着,GPU 在跑满。Python 是"指挥",GPU 是"工人"。
1.3 为什么不是 C++/Java 直接写模型
| 维度 | Python | C++/Java |
|---|---|---|
| 开发速度 | 快(动态、REPL) | 慢(编译、类型) |
| 矩阵运算 | 调 cuBLAS 一行 | 手写上千行 |
| 自动求导 | 框架内置 | 需自己实现 |
| 运行时性能 | 低(但热路径在 C++) | 高 |
| 生态 | HuggingFace 全家桶 | 需自己造轮子 |
工业界的真实做法:Python 写模型逻辑 + C++/CUDA 写热点算子。PyTorch 本身就是 C++ 内核 + Python 前端。
2. 张量与计算图:Transformer 的数据形态
2.1 张量:高维数组的统一抽象
Transformer 里所有数据都是张量。核心维度约定:
| 张量 | 形状 | 含义 |
|---|---|---|
| 输入 token ids | (batch, seq_len) |
整数,每个 token 的编号 |
| 嵌入 embeddings | (batch, seq_len, d_model) |
每个 token 的向量表示 |
| Q/K/V | (batch, heads, seq_len, d_head) |
多头注意力的查询/键/值 |
| 注意力权重 | (batch, heads, seq_len, seq_len) |
softmax(QK^T/√d) |
| 输出 logits | (batch, seq_len, vocab_size) |
下个 token 的概率分布 |
2.2 用 NumPy 理解张量操作(原理)
在学 PyTorch 前,先用 NumPy 看清"注意力到底算什么":
import numpy as np
d_model = 64
seq_len = 10
np.random.seed(0)
# 假设有 10 个 token,每个 64 维
X = np.random.randn(seq_len, d_model)
# 三个投影矩阵
Wq = np.random.randn(d_model, d_model)
Wk = np.random.randn(d_model, d_model)
Wv = np.random.randn(d_model, d_model)
Q = X @ Wq # (10, 64)
K = X @ Wk
V = X @ Wv
# 注意力分数:Q 和 K 的相似度
scores = Q @ K.T / np.sqrt(d_model) # (10, 10)
# softmax 归一化
scores -= scores.max(axis=1, keepdims=True)
attn = np.exp(scores) / np.exp(scores).sum(axis=1, keepdims=True)
# 加权求和
out = attn @ V # (10, 64)
这 10 行就是 Self-Attention 的全部数学。Transformer 的"神奇",本质上就是这堆矩阵乘法。
2.3 为什么要转成 PyTorch 张量
NumPy 跑在 CPU 上,且没有自动求导。训练模型需要: 1. 前向算 loss 2. 反向算每个参数的梯度 3. 更新参数
手写反向传播对 175B 参数模型不可能。PyTorch 的 autograd 自动完成。
3. 自动求导(Autograd):训练的核心引擎
3.1 计算图与链式法则
PyTorch 在前向传播时记录每一步操作,构建一张计算图。反向传播时沿图反走,用链式法则算梯度。
import torch
x = torch.tensor([2.0], requires_grad=True)
y = x ** 2 + 3 * x # 前向:y = x² + 3x
y.backward() # 反向:dy/dx = 2x + 3 = 7
print(x.grad) # tensor([7.])
3.2 Transformer 的反向传播长什么样
一次训练 step 的完整链路:
输入 ids
→ Embedding 查表
→ 多层 TransformerBlock
→ Self-Attention (QKV 投影 + 注意力 + 输出投影)
→ 残差 + LayerNorm
→ FFN (两个线性层 + GELU)
→ 残差 + LayerNorm
→ 输出投影到词表
→ CrossEntropyLoss
→ backward() ← 梯度从 loss 一路传回 Embedding
→ optimizer.step() ← 更新所有参数
backward() 这一行,会触发成千上万个梯度算子的执行。Python 只负责"触发",梯度计算由 PyTorch 的 C++ Autograd 引擎调度到 GPU。
3.3 显存从哪来、到哪去
| 阶段 | 显存占用 |
|---|---|
| 前向 | 模型权重 + 中间激活(用于反向) |
| 反向 | 梯度(与权重同大小)+ 优化器状态 |
| Adam 优化器 | 每个参数存 m、v 两个状态 → 显存 = 2× 参数 |
所以一个 7B 模型(fp16,14GB 权重)训练时显存 ≈ 14(权重) + 14(梯度) + 28(Adam) + 激活 = ~60GB+。这就是为什么训练需要 A100/H100。
4. 从零实现一个 Mini Transformer(实战)
4.1 目标
用不到 200 行 PyTorch 实现一个可训练的 GPT 风格 Transformer,在莎士比亚文本上训练,能生成像样的文字。
4.2 完整代码
# mini_gpt.py
import torch
import torch.nn as nn
import torch.nn.functional as F
# ---------- 配置 ----------
class GPTConfig:
block_size = 128 # 上下文长度
vocab_size = 65 # 字符级词表
n_layer = 4
n_head = 4
d_model = 128
dropout = 0.0
cfg = GPTConfig()
# ---------- 多头自注意力 ----------
class CausalSelfAttention(nn.Module):
def __init__(self, cfg):
super().__init__()
self.c_attn = nn.Linear(cfg.d_model, 3 * cfg.d_model)
self.c_proj = nn.Linear(cfg.d_model, cfg.d_model)
self.n_head = cfg.n_head
self.d_model = cfg.d_model
# 因果掩码:只看左边
self.register_buffer("mask", torch.tril(torch.ones(cfg.block_size, cfg.block_size))
.view(1, 1, cfg.block_size, cfg.block_size))
def forward(self, x):
B, T, C = x.shape
qkv = self.c_attn(x).split(self.d_model, dim=2)
q, k, v = [t.view(B, T, self.n_head, C // self.n_head).transpose(1, 2) for t in qkv]
# 注意力
att = (q @ k.transpose(-2, -1)) * (1.0 / (k.size(-1) ** 0.5))
att = att.masked_fill(self.mask[:, :, :T, :T] == 0, float('-inf'))
att = F.softmax(att, dim=-1)
y = att @ v
y = y.transpose(1, 2).contiguous().view(B, T, C)
return self.c_proj(y)
# ---------- Transformer Block ----------
class Block(nn.Module):
def __init__(self, cfg):
super().__init__()
self.ln1 = nn.LayerNorm(cfg.d_model)
self.attn = CausalSelfAttention(cfg)
self.ln2 = nn.LayerNorm(cfg.d_model)
self.mlp = nn.Sequential(
nn.Linear(cfg.d_model, 4 * cfg.d_model),
nn.GELU(),
nn.Linear(4 * cfg.d_model, cfg.d_model),
)
def forward(self, x):
x = x + self.attn(self.ln1(x))
x = x + self.mlp(self.ln2(x))
return x
# ---------- GPT 模型 ----------
class GPT(nn.Module):
def __init__(self, cfg):
super().__init__()
self.wte = nn.Embedding(cfg.vocab_size, cfg.d_model)
self.wpe = nn.Embedding(cfg.block_size, cfg.d_model)
self.blocks = nn.Sequential(*[Block(cfg) for _ in range(cfg.n_layer)])
self.ln_f = nn.LayerNorm(cfg.d_model)
self.lm_head = nn.Linear(cfg.d_model, cfg.vocab_size, bias=False)
def forward(self, idx, targets=None):
B, T = idx.shape
pos = torch.arange(0, T, dtype=torch.long, device=idx.device).unsqueeze(0)
x = self.wte(idx) + self.wpe(pos)
x = self.blocks(x)
x = self.ln_f(x)
logits = self.lm_head(x)
loss = None
if targets is not None:
loss = F.cross_entropy(logits.view(-1, logits.size(-1)), targets.view(-1))
return logits, loss
# ---------- 生成 ----------
@torch.no_grad()
def generate(self, idx, max_new_tokens, temperature=1.0):
for _ in range(max_new_tokens):
idx_cond = idx[:, -cfg.block_size:]
logits, _ = self(idx_cond)
logits = logits[:, -1, :] / temperature
probs = F.softmax(logits, dim=-1)
idx_next = torch.multinomial(probs, num_samples=1)
idx = torch.cat([idx, idx_next], dim=1)
return idx
4.3 训练循环
# train.py(接上面)
import requests
# 1. 数据:莎士比亚文本
url = "https://raw.githubusercontent.com/karpathy/char-rnn/master/data/tinyshakespeare/input.txt"
text = requests.get(url).text
chars = sorted(set(text))
stoi = {c: i for i, c in enumerate(chars)}
itos = {i: c for i, c in enumerate(chars)}
data = torch.tensor([stoi[c] for c in text], dtype=torch.long)
n = int(0.9 * len(data))
train_data, val_data = data[:n], data[n:]
def get_batch(split):
d = train_data if split == "train" else val_data
ix = torch.randint(len(d) - cfg.block_size, (64,))
x = torch.stack([d[i:i+cfg.block_size] for i in ix])
y = torch.stack([d[i+1:i+1+cfg.block_size] for i in ix])
return x.cuda(), y.cuda()
# 2. 训练
model = GPT(cfg).cuda()
optimizer = torch.optim.AdamW(model.parameters(), lr=3e-4)
model.train()
for step in range(2000):
xb, yb = get_batch("train")
logits, loss = model(xb, yb)
optimizer.zero_grad()
loss.backward()
optimizer.step()
if step % 200 == 0:
print(f"step {step:4d} | loss {loss.item():.4f}")
# 3. 生成
model.eval()
ctx = torch.zeros((1, 1), dtype=torch.long, device="cuda")
out = model.generate(ctx, max_new_tokens=500)[0].tolist()
print("".join(itos[i] for i in out))
4.4 运行结果示例(训练 2000 步后)
step 0 | loss 4.2311
step 200 | loss 2.4873
step 400 | loss 2.1025
step 600 | loss 1.8934
...
step 1800 | loss 1.5211
ROMEO:
I will not fight; but, by my soul,
I'll prove a tyrant to the world.
loss 从 4.2 降到 1.5,生成的文本开始有莎翁味道。这就是一个 Transformer 从零跑起来的完整闭环。
5. 让大模型跑得快:GPU 与并行
5.1 Python 如何调度 GPU
model = GPT(cfg).cuda() # 把模型参数搬到显存
x = x.cuda() # 把输入搬到显存
logits, loss = model(x) # 前向:GPU 执行
loss.backward() # 反向:GPU 执行
optimizer.step() # 更新:GPU 执行
Python 发出的每一步都是异步的——CPU 不会等 GPU 跑完,而是继续往下发指令。这就是为什么 PyTorch 代码看起来"串行",实际 GPU 在流水执行。
5.2 混合精度:速度翻倍的关键
scaler = torch.amp.GradScaler("cuda")
with torch.amp.autocast("cuda"):
logits, loss = model(xb, yb) # 前向用 fp16(更快、更省显存)
scaler.scale(loss).backward()
scaler.step(optimizer)
scaler.update()
原理:fp16 算力是 fp32 的 2~8 倍(取决于 GPU),但精度不够。混合精度用 fp16 算、fp32 存权重,GradScaler 防止梯度下溢。
5.3 三种并行(工业部署必知)
| 并行方式 | 解决什么 | 怎么做 |
|---|---|---|
| 数据并行 DP/DDP | 单卡放不下 batch | 每卡一份完整模型,分 batch |
| 张量并行 TP | 单卡放不下权重 | 把大矩阵切到多卡(如 nn.Linear 按列切) |
| 流水线并行 PP | 层数太多 | 把不同层放不同卡,流水调度 |
一个 70B 模型:单机 8 卡通常 TP=8;多机则 TP+PP+DP 组合。这些并行策略都由 Python 框架(Megatron-LM / DeepSpeed)编排。
5.4 推理加速:vLLM 的 PagedAttention
训练完要上线。Python 写的原生推理很慢(一次只处理一个请求)。vLLM 用: - PagedAttention:像操作系统分页一样管理 KV Cache 显存 - 连续批处理:动态把新请求拼进当前 batch - CUDA graph:减少 Python ↔ GPU 的调度开销
同样一个模型,vLLM 吞吐量可比原生 PyTorch 高 10~50 倍。
6. 推理服务化:把模型变成 API
6.1 最小服务(FastAPI)
# server.py
from fastapi import FastAPI
from transformers import AutoModelForCausalLM, AutoTokenizer
import torch, uvicorn
app = FastAPI()
tok = AutoTokenizer.from_pretrained("gpt2")
model = AutoModelForCausalLM.from_pretrained("gpt2").cuda().eval()
@app.post("/chat")
def chat(prompt: str, max_tokens: int = 128):
ids = tok(prompt, return_tensors="pt").input_ids.cuda()
out = model.generate(ids, max_new_tokens=max_tokens)
return {"reply": tok.decode(out[0][ids.shape[1]:], skip_special_tokens=True)}
uvicorn.run(app, host="0.0.0.0", port=8000)
6.2 工业级:vLLM OpenAI 兼容服务
# 一行启动,自动加载模型、批处理、显存管理
vllm serve meta-llama/Llama-3.1-8B --host 0.0.0.0 --port 8000
调用:
curl http://localhost:8000/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{"model":"meta-llama/Llama-3.1-8B","messages":[{"role":"user","content":"你好"}]}'
6.3 Python 在服务化中的角色
- vLLM / TGI 本身是 Python 项目,但热路径(注意力、采样)用 C++/CUDA 重写
- Python 负责:路由、批处理调度、权重加载、日志
- 真正的 token 生成在 GPU,Python 异步等待
7. 全景总结:一次完整的"Python 跑 Transformer"流程
[数据] Python 读文本 → tokenizer 编码 → 张量
↓
[模型] Python 定义 nn.Module → 加载预训练权重
↓
[前向] Python 调 forward → GPU 跑矩阵乘/注意力
↓
[损失] Python 算 CrossEntropy → loss 标量
↓
[反向] Python 调 loss.backward() → Autograd 算所有梯度
↓
[更新] Python 调 optimizer.step() → GPU 更新权重
↓
[保存] Python 调 torch.save() → 权重存盘
↓
[服务] Python(FastAPI/vLLM) 接收请求 → 生成 → 返回
Python 不直接做计算,但它是整个流程的"神经中枢":定义结构、调度算力、管理状态、对外服务。理解了这张图,就理解了大模型为什么必须用 Python、又是怎么真正跑起来的。
8. Python 现代化语法:写得更少,表达更强(AI 时代的工程底座)
前 7 章讲的是"Python 如何驱动 Transformer"。但要让代码在团队里跑得久、改得动、读得清,靠的是另一层功夫——现代化语法。这一章把 AI 工程里最常用、最能省事、最能少踩坑的现代 Python 特性集中讲透。版本基准:Python 3.10 ~ 3.12。
8.1 类型提示(Type Hints):让 Python 拥有静态语言的可维护性
类型提示不改变运行时行为(CPython 不强制检查),但它带来三个真实收益:
- IDE 补全 / 跳转:PyCharm、VSCode 靠它给智能提示,AI 代码补全准确率显著提升
- 静态检查:mypy / pyright 在提交前就拦截"传错类型"
- 文档自解释:函数签名即契约,比注释更不会过期
# 老写法:签名是黑盒
def train(model, data, epochs):
...
# 现代写法:意图一目了然
from torch import Tensor
from torch.nn import Module
from torch.utils.data import DataLoader
def train(
model: Module,
data: DataLoader,
epochs: int,
*,
lr: float = 1e-3, # ← 关键字参数强制写名,调用更安全
device: str = "cuda",
) -> dict[str, float]: # ← 返回类型也标注
...
return {"final_loss": 0.23, "best_acc": 0.91}
几个易错的点:
list[int]是 3.9+ 的内置泛型写法,List[int]是老写法(需from typing import List),二者等价,新代码用前者- 默认值用
None而非可变对象:def f(x: list = [])是经典陷阱 -> None不是装饰,是契约,告诉调用方"别指望返回值"
8.2 dataclass 与 Pydantic:数据建模的两种范式
AI 工程里几乎每一段代码都在处理"结构化数据":模型配置、训练超参、推理请求、评估结果。两种主流方案:
# 方案 A:dataclass —— 轻量、纯 Python、无依赖
from dataclasses import dataclass, field
@dataclass(slots=True) # 3.10+ slots 省内存,AI 场景里很关键
class GPTConfig:
vocab_size: int = 50257
n_layer: int = 12
n_head: int = 12
d_model: int = 768
dropout: float = 0.0
block_size: int = 1024
cfg = GPTConfig(d_model=1024)
print(cfg) # 自动生成 __repr__:GPTConfig(vocab_size=50257, ...)
# 方案 B:Pydantic —— 带"运行时校验 + 序列化",FastAPI 默认引擎
from pydantic import BaseModel, Field, field_validator
class ChatRequest(BaseModel):
model: str = "gpt-2"
prompt: str = Field(..., min_length=1, max_length=8192)
temperature: float = Field(0.8, ge=0.0, le=2.0) # 范围约束
max_tokens: int = 256
@field_validator("prompt")
@classmethod
def no_ctrl_chars(cls, v: str) -> str:
if any(ord(c) < 0x20 and c not in "\n\t" for c in v):
raise ValueError("控制字符不允许")
return v
req = ChatRequest(prompt="你好", temperature=3.0)
# → ValidationError: temperature must be ≤ 2.0 ← 在边界处就拦住
选择标准:
- 内部代码、配置对象、性能敏感 →
dataclass - 对外 API、接收用户输入、需要 JSON 序列化 →
Pydantic - 两者可以混用:
@dataclass当内部模型,BaseModel当接口层 DTO
8.3 结构化模式匹配(match/case):3.10 的革命性语法
PEP 634 引入的 match/case 不只是 switch,它支持结构解构,是处理嵌套数据的利器。AI 工程里处理消息路由、token 分流、异常分类时极好用。
# 旧写法:if-elif 链
def handle(msg):
if msg["type"] == "text":
return process_text(msg["content"])
elif msg["type"] == "image":
return process_image(msg["url"], msg.get("caption", ""))
elif msg["type"] == "tool_call":
if msg["tool"] == "calculator":
return run_calc(msg["args"])
elif msg["tool"] == "search":
return run_search(msg["args"])
else:
return None
# 现代写法:match/case 结构化解构
def handle(msg: dict) -> object:
match msg:
case {"type": "text", "content": c}:
return process_text(c)
case {"type": "image", "url": u, "caption": caption}:
return process_image(u, caption)
case {"type": "tool_call", "tool": "calculator", "args": a}:
return run_calc(a)
case {"type": "tool_call", "tool": "search", "args": a}:
return run_search(a)
case {"type": "error", "code": code, **rest}:
log_error(code, rest)
return None
case _:
return None # ← 通配,等价于 default
# 还能匹配类结构
match response:
case OpenAIResponse(choices=[Choice(text=t, finish_reason="stop")]):
return t
case OpenAIResponse(choices=[Choice(delta=Delta(content=c))]):
yield c # 流式 token
几个关键点:
case {"k": v}会自动捕获 dict 中的对应键,**rest收集剩余键- 类匹配需要该类是
dataclass或支持__match_args__ _是通配符,必须放最后;case _ if condition:可加守卫
8.4 海象运算符(:=):赋值即表达式
PEP 572 的 :=(walrus operator)把"赋值"变成"表达式",最常见于两种场景:把表达式结果复用、在条件里同时绑定变量。
# 场景 1:避免重复调用昂贵的函数
# 老写法:调两次
if len(prompt_tokens) > 8192:
print(f"超出 {len(prompt_tokens) - 8192} tokens")
# 海象:只算一次
if (n := len(prompt_tokens)) > 8192:
print(f"超出 {n - 8192} tokens")
# 场景 2:在 while 条件里捕获值
# 老写法
line = f.readline()
while line:
process(line)
line = f.readline()
# 海象:经典的"读一行"
while (line := f.readline()):
process(line)
# 场景 3:列表推导里复用计算
# 老写法:算两遍
results = [compute_loss(x) for x in batch if compute_loss(x) < 0.5]
# 海象:算一次
results = [loss for x in batch if (loss := compute_loss(x)) < 0.5]
# 场景 4:用 in 表达式同时绑定(替代 dict.get + if None 检查)
if (cap := msg.get("max_tokens")) is not None:
use(cap)
使用边界:别在简单赋值里用 (x := 5)——那是炫技。它在"需要把中间值带进条件/推导/循环"时才发光。
8.5 async/await 与 asyncio:高并发推理服务的基石
vLLM、FastAPI、TGI 这些推理服务全是 async。原因:GPU 推理是 IO + 计算混合,async 能在一个事件循环里同时挂起几千个请求,等 GPU 时让出 CPU 给别的请求处理。
import asyncio
from fastapi import FastAPI
app = FastAPI()
# 协程:用 async def 定义,await 挂起等待
async def generate_stream(prompt: str) -> str:
# await 一个耗 IO 的协程(不阻塞整个事件循环)
tokens = await model.generate_async(prompt)
return "".join(tokens)
@app.post("/chat")
async def chat(req: ChatRequest):
# 关键:把阻塞调用丢给线程池,别用 await 包装同步函数
text = await asyncio.to_thread(sync_inference, req.prompt)
return {"reply": text}
# 并发跑多个任务:asyncio.gather
async def batch_inference(prompts: list[str]) -> list[str]:
tasks = [generate_stream(p) for p in prompts]
return await asyncio.gather(*tasks) # 并发,不串行
# 限流:Semaphore 控制并发数,保护 GPU 不被打爆
sem = asyncio.Semaphore(8)
async def safe_generate(prompt: str) -> str:
async with sem: # ← 同时最多 8 个进入
return await generate_stream(prompt)
关键避坑:
- async 函数里禁止直接调同步阻塞函数(如
requests.get、time.sleep),会卡死整个循环。用asyncio.to_thread()或换httpx.AsyncClient - 协程不并行跑就毫无意义——
async def只是定义,await才执行,asyncio.run()才启动循环 - 批量任务永远用
asyncio.gather(*tasks),别 for 循环里一个一个await
8.6 f-string 与 pathlib:现代化的字符串与路径
# f-string(3.6+):比 % 和 .format 都快、都直观
name, n = "transformer", 12
print(f"模型 {name} 有 {n} 层,参数量 {n * 768 * 768 / 1e6:.1f}M")
# 模型 transformer 有 12 层,参数量 7.1M
# 3.8+ 调试语法:在表达式后加 = 直接打印
x = compute_loss(batch)
print(f"{x=}") # → x=0.234
# 多行 f-string + 表达式
template = f"""
[Prompt]
{prompt}
[Config]
temperature={temp:.2f}
max_tokens={max_tok}
"""
# pathlib(取代 os.path):面向对象、跨平台、可链式
from pathlib import Path
# 老写法
import os
ckpt = os.path.join(os.path.dirname(__file__), "checkpoints", "gpt.pt")
# 现代写法:/ 运算符重载,语义清晰
ckpt = Path(__file__).parent / "checkpoints" / "gpt.pt"
# 常用操作一气呵成
for f in Path("data/").rglob("*.jsonl"):
if f.stat().st_size > 0:
process(f.read_text(encoding="utf-8"))
# 写文件:一行
Path("output.txt").write_text("done", encoding="utf-8")
# 创建目录(已存在不报错)
Path("runs/exp1").mkdir(parents=True, exist_ok=True)
8.7 推导式、生成器与 Protocol:函数式与鸭子类型的现代写法
# 推导式:列表 / 字典 / 集合,一行表达映射+过滤
tokens = [tok for line in corpus for tok in line.split() if tok.isalpha()]
vocab = {tok: i for i, tok in enumerate(sorted(set(tokens)))}
stop = {"the", "a", "an"}
filtered = {w for w in vocab if w not in stop and len(w) > 2}
# 生成器表达式:惰性求值,省内存,适合大数据流
# 用 () 而非 [],不一次实例化全部
total = sum(len(t) for t in token_stream) # 不会先建 list
# yield:把函数变成迭代器,流式产出
def stream_tokens(path: Path):
for line in path.open(encoding="utf-8"):
for tok in line.split():
yield tok
# yield from:委托给子生成器
def stream_all(paths: list[Path]):
for p in paths:
yield from stream_tokens(p)
# Protocol(PEP 544):结构化子类型,比 ABC 更轻
from typing import Protocol
class Tokenizer(Protocol):
def encode(self, text: str) -> list[int]: ...
def decode(self, ids: list[int]) -> str: ...
# 任何有这两个方法的对象都自动满足 Tokenizer,无需继承
class MyTok:
def encode(self, text): return [ord(c) for c in text]
def decode(self, ids): return "".join(chr(i) for i in ids)
def train(tok: Tokenizer, data): # ← 鸭子类型 + 静态检查
...
train(MyTok(), ...) # ✅ 不报错
8.8 typing 泛型与 PEP 695:3.12 的类型系统飞跃
3.12 引入 PEP 695,泛型写法大幅简化,不再需要 TypeVar + Generic 的样板代码。
# 老写法(3.11 及之前):样板代码多
from typing import TypeVar, Generic, TypeAlias
T = TypeVar("T")
class Stack(Generic[T]):
def __init__(self) -> None:
self._items: list[T] = []
def push(self, x: T) -> None:
self._items.append(x)
def pop(self) -> T:
return self._items.pop()
Vector: TypeAlias = list[float]
# 新写法(3.12+,PEP 695):语法糖,干净
class Stack[T]:
def __init__(self) -> None:
self._items: list[T] = []
def push(self, x: T) -> None:
self._items.append(x)
def pop(self) -> T:
return self._items.pop()
type Vector = list[float] # ← type 语句,3.12 新
def first(xs: list[T]) -> T: # 函数级泛型,自动推断
return xs[0]
# 更实战的例子:一个泛型缓存
class LRUCache[K, V]:
def __init__(self, cap: int): ...
def get(self, k: K) -> V | None: ...
def put(self, k: K, v: V) -> None: ...
cache: LRUCache[str, Tensor] = LRUCache(1024)
对 AI 工程的实际意义:自定义 Dataset / Sampler / Collator 这些泛型容器,3.12 写法可读性接近 TypeScript / Rust,对团队协作和静态检查(pyright)友好得多。
8.9 在 AI 工程里:这些语法如何组合实战
把本章特性拼起来,看一个真实的"流式推理 endpoint"长什么样:
from __future__ import annotations
from dataclasses import dataclass
from pathlib import Path
from typing import Protocol
import asyncio, json
from fastapi import FastAPI
from pydantic import BaseModel, Field
# 1. 用 Protocol 定义抽象,不绑死实现
class Tokenizer(Protocol):
def encode(self, text: str) -> list[int]: ...
def decode(self, ids: list[int]) -> str: ...
# 2. dataclass 做配置,slots 省 GPU 服务器内存
@dataclass(slots=True)
class GenConfig:
max_tokens: int = 256
temperature: float = 0.8
top_p: float = 0.95
# 3. Pydantic 做请求 DTO,边界校验
class ChatReq(BaseModel):
prompt: str = Field(..., min_length=1, max_length=8192)
stream: bool = False
app = FastAPI()
sem = asyncio.Semaphore(4) # 4. 并发限流,保护 GPU
# 5. async + 生成器:流式产出 token
async def stream_tokens(tok: Tokenizer, ids: list[int], cfg: GenConfig):
async with sem:
for i in range(cfg.max_tokens):
nxt = await model.next_token(ids) # 假设的异步推理
if nxt == EOS:
break
ids.append(nxt)
yield tok.decode([nxt]) # 流式吐出
# 6. match/case 分流:流式 vs 一次性
@app.post("/chat")
async def chat(req: ChatReq):
ids = tokenizer.encode(req.prompt)
cfg = GenConfig()
match req:
case ChatReq(stream=True):
async def gen():
async for piece in stream_tokens(tokenizer, ids, cfg):
yield f"data: {json.dumps({'text': piece})}\n\n"
return StreamingResponse(gen(), media_type="text/event-stream")
case _:
pieces = []
async for p in stream_tokens(tokenizer, ids, cfg):
pieces.append(p)
return {"reply": "".join(pieces)}
# 7. 海象 + pathlib:启动时加载权重
if (ckpt := Path("models/gpt2.pt")).exists():
model.load(ckpt.read_bytes())
这段 50 行代码里用到了:Protocol、dataclass(slots)、Pydantic、async/await、Semaphore、生成器 yield、match/case、海象运算符、pathlib。这正是现代 AI 服务端代码的真实形态——没有一样是炫技,每一样都在"少写 + 少错 + 好维护"上立竿见影。
附录:学习路线与资源
- 基础:Python → NumPy → 线性代数(矩阵乘法、特征值)
- 框架:PyTorch 官方 60 分钟教程 → 实现一个 MLP/CNN
- Transformer:读《Attention Is All You Need》→ 手写自注意力
- 实战:用 HuggingFace Transformers 微调一个小模型
- 进阶:DeepSpeed / Megatron-LM 并行 → vLLM 推理优化
- 部署:FastAPI → Docker → K8s 弹性伸缩
核心心法:先跑通最小闭环,再逐层加复杂度。 不要一上来就啃 175B 模型——能从零训出能生成文字的 MiniGPT,就已经掌握了 Transformer 80% 的本质。