上一篇我们搞定了符号,这一篇直接拆解AI论文中出现频率最高的6个公式。每个公式都是:问题→公式→拆解→代码。
前置要求:读完上一篇符号速查手册。
一、Softmax与交叉熵损失
问题:如何把模型输出变成概率?
神经网络输出的是一堆数字(logits),可能是负数、可能很大。我们需要把它们变成概率分布(非负、和为1)。
公式
$$\text{softmax}(x_i) = \frac{e^{x_i}}{\sum_{j=1}^{K} e^{x_j}}$$
拆解
- $e^{x_i}$:指数函数,把任意数变成正数
- $\sum_{j=1}^{K} e^{x_j}$:所有指数的和,用于归一化
- 除法:确保结果和为1
为什么用指数? 因为指数函数会放大差异——大的更大,小的更小,让模型更"自信"。
代码实现
import numpy as np
def softmax(x):
"""
输入: x, shape (K,) 或 (batch, K)
输出: 概率分布,shape同输入
"""
# 减去最大值防止数值溢出(数学上等价)
exp_x = np.exp(x - np.max(x, axis=-1, keepdims=True))
return exp_x / np.sum(exp_x, axis=-1, keepdims=True)
# 示例
logits = np.array([2.0, 1.0, 0.1])
probs = softmax(logits)
print(probs) # [0.659, 0.242, 0.099]
print(probs.sum()) # 1.0
交叉熵损失
有了概率,如何衡量预测和真实标签的差距?
$$L = -\sum_{i=1}^{K} y_i \log(\hat{y}_i)$$
其中 $y_i$ 是真实标签(one-hot),$\hat{y}_i$ 是预测概率。
简化形式(单标签分类): $$L = -\log(\hat{y}_{correct})$$
就是正确类别的概率取负对数。概率越高,损失越小。
def cross_entropy_loss(probs, label):
"""
probs: softmax输出的概率, shape (K,)
label: 正确类别的索引, int
"""
return -np.log(probs[label] + 1e-10) # 加小数防止log(0)
# 示例
probs = np.array([0.7, 0.2, 0.1])
loss = cross_entropy_loss(probs, label=0) # 正确类别是0
print(loss) # 0.357(概率0.7对应的损失)
二、注意力机制(Attention)
问题:如何让模型"关注"输入的不同部分?
翻译"我爱北京"时,生成"Beijing"应该主要关注"北京"这个词,而不是平均关注所有词。
公式(Scaled Dot-Product Attention)
$$\text{Attention}(Q, K, V) = \text{softmax}\left(\frac{QK^T}{\sqrt{d_k}}\right)V$$
拆解
- Q(Query):我在找什么?
- K(Key):每个位置的"标签"
- V(Value):每个位置的实际内容
- $QK^T$:Query和所有Key的相似度(点积)
- $\sqrt{d_k}$:缩放因子,防止点积太大导致softmax饱和
- softmax:把相似度变成注意力权重(概率分布)
- 乘V:用注意力权重对Value加权求和
直觉:注意力就是"查字典"——用Query去匹配Key,找到最相关的,然后取对应的Value。
代码实现
import numpy as np
def attention(Q, K, V):
"""
Q: (seq_len_q, d_k)
K: (seq_len_k, d_k)
V: (seq_len_k, d_v)
返回: (seq_len_q, d_v)
"""
d_k = K.shape[-1]
# 1. 计算注意力分数
scores = Q @ K.T # (seq_len_q, seq_len_k)
# 2. 缩放
scores = scores / np.sqrt(d_k)
# 3. Softmax得到注意力权重
weights = softmax(scores) # 每行和为1
# 4. 加权求和
output = weights @ V # (seq_len_q, d_v)
return output, weights
# 示例:3个位置,维度4
Q = np.random.randn(3, 4)
K = np.random.randn(3, 4)
V = np.random.randn(3, 4)
output, weights = attention(Q, K, V)
print(f"输出shape: {output.shape}") # (3, 4)
print(f"注意力权重:\n{weights}") # 每行和为1
多头注意力
把Q、K、V分成多个"头",每个头学习不同的注意力模式,最后拼接:
$$\text{MultiHead}(Q, K, V) = \text{Concat}(\text{head}_1, \ldots, \text{head}_h)W^O$$
def multi_head_attention(Q, K, V, num_heads=8):
"""简化版多头注意力"""
d_model = Q.shape[-1]
d_k = d_model // num_heads
outputs = []
for i in range(num_heads):
# 每个头处理一部分维度
start, end = i * d_k, (i + 1) * d_k
head_out, _ = attention(Q[:, start:end], K[:, start:end], V[:, start:end])
outputs.append(head_out)
return np.concatenate(outputs, axis=-1)
三、梯度下降
问题:如何让模型"学习"?
模型有参数 $\theta$,损失函数 $L(\theta)$ 衡量预测有多差。目标:找到让 $L$ 最小的 $\theta$。
公式
$$\theta_{t+1} = \theta_t - \eta \nabla_\theta L(\theta_t)$$
拆解
- $\theta_t$:当前参数
- $\nabla_\theta L$:损失对参数的梯度(指向损失增大最快的方向)
- $\eta$:学习率(步长)
- 减号:往梯度反方向走(损失减小的方向)
直觉:站在山上,梯度告诉你哪边最陡,你往反方向走就能下山。
代码实现
import torch
# 1. 定义参数
theta = torch.tensor([1.0, 2.0], requires_grad=True)
# 2. 定义损失函数(示例:L = theta[0]^2 + theta[1]^2)
def loss_fn(theta):
return (theta ** 2).sum()
# 3. 梯度下降
learning_rate = 0.1
for step in range(10):
# 前向:计算损失
loss = loss_fn(theta)
# 反向:计算梯度
loss.backward()
# 更新参数(不记录梯度)
with torch.no_grad():
theta -= learning_rate * theta.grad
# 清零梯度(重要!)
theta.grad.zero_()
print(f"Step {step}: theta = {theta.data.numpy()}, loss = {loss.item():.4f}")
常见变体
| 算法 | 公式 | 特点 |
|---|---|---|
| SGD | $\theta \leftarrow \theta - \eta \nabla L$ | 基础版 |
| Momentum | $v \leftarrow \beta v + \nabla L$, $\theta \leftarrow \theta - \eta v$ | 加速收敛 |
| Adam | 自适应学习率 + 动量 | 最常用 |
# PyTorch中使用Adam
optimizer = torch.optim.Adam([theta], lr=0.01)
for step in range(100):
loss = loss_fn(theta)
optimizer.zero_grad()
loss.backward()
optimizer.step() # 自动更新参数
四、LSTM门控机制
问题:如何让模型记住长序列的信息?
普通RNN有"梯度消失"问题,长序列的早期信息会被遗忘。LSTM通过"门"来控制信息的流动。
公式
遗忘门(决定丢弃多少旧信息): $$f_t = \sigma(W_f \cdot [h_{t-1}, x_t] + b_f)$$
输入门(决定存入多少新信息): $$i_t = \sigma(W_i \cdot [h_{t-1}, x_t] + b_i)$$ $$\tilde{c}t = \tanh(W_c \cdot [h{t-1}, x_t] + b_c)$$
细胞状态更新: $$c_t = f_t \odot c_{t-1} + i_t \odot \tilde{c}_t$$
输出门: $$o_t = \sigma(W_o \cdot [h_{t-1}, x_t] + b_o)$$ $$h_t = o_t \odot \tanh(c_t)$$
拆解
- $[h_{t-1}, x_t]$:把上一时刻的隐藏状态和当前输入拼接
- $\sigma$(sigmoid):输出0-1,作为"门"的开关程度
- $\odot$:逐元素相乘,实现"门控"
- $c_t$(细胞状态):长期记忆,信息在这里流动
- $h_t$(隐藏状态):短期记忆,作为输出
直觉:
- 遗忘门:决定忘记多少过去
- 输入门:决定记住多少现在
- 输出门:决定输出多少信息
代码实现
import numpy as np
def sigmoid(x):
return 1 / (1 + np.exp(-x))
class LSTMCell:
def __init__(self, input_size, hidden_size):
# 初始化权重(简化:合并为一个大矩阵)
self.W = np.random.randn(4 * hidden_size, input_size + hidden_size) * 0.01
self.b = np.zeros(4 * hidden_size)
self.hidden_size = hidden_size
def forward(self, x, h_prev, c_prev):
"""
x: (input_size,)
h_prev: (hidden_size,)
c_prev: (hidden_size,)
"""
hs = self.hidden_size
# 拼接输入
concat = np.concatenate([h_prev, x])
# 计算所有门(一次矩阵乘法)
gates = self.W @ concat + self.b
# 分割成4个门
f = sigmoid(gates[0:hs]) # 遗忘门
i = sigmoid(gates[hs:2*hs]) # 输入门
c_tilde = np.tanh(gates[2*hs:3*hs]) # 候选细胞
o = sigmoid(gates[3*hs:4*hs]) # 输出门
# 更新细胞状态
c = f * c_prev + i * c_tilde
# 计算隐藏状态
h = o * np.tanh(c)
return h, c
# 示例
lstm = LSTMCell(input_size=10, hidden_size=20)
x = np.random.randn(10)
h = np.zeros(20)
c = np.zeros(20)
h_new, c_new = lstm.forward(x, h, c)
print(f"隐藏状态shape: {h_new.shape}") # (20,)
五、残差连接(Skip Connection)
问题:为什么深层网络反而效果变差?
理论上网络越深越强,但实际训练中,深层网络会出现"退化"——不是过拟合,是训练误差都变高了。
公式
$$y = F(x) + x$$
拆解
- $F(x)$:网络层要学习的变换
- $+ x$:直接把输入加到输出上(跳跃连接)
为什么有效?
原本网络要学习 $H(x)$(目标映射),现在只需要学习 $F(x) = H(x) - x$(残差)。
- 如果最优解是恒等映射 $H(x) = x$,那么 $F(x) = 0$,学习"什么都不做"比学习"复制输入"容易
- 梯度可以直接通过跳跃连接传回去,缓解梯度消失
代码实现
import torch
import torch.nn as nn
class ResidualBlock(nn.Module):
def __init__(self, channels):
super().__init__()
self.conv1 = nn.Conv2d(channels, channels, kernel_size=3, padding=1)
self.bn1 = nn.BatchNorm2d(channels)
self.conv2 = nn.Conv2d(channels, channels, kernel_size=3, padding=1)
self.bn2 = nn.BatchNorm2d(channels)
self.relu = nn.ReLU()
def forward(self, x):
# 保存输入
identity = x
# 主路径
out = self.relu(self.bn1(self.conv1(x)))
out = self.bn2(self.conv2(out))
# 残差连接
out = out + identity # 关键!
out = self.relu(out)
return out
# 示例
block = ResidualBlock(64)
x = torch.randn(1, 64, 32, 32)
y = block(x)
print(y.shape) # torch.Size([1, 64, 32, 32])
Transformer中的残差
Transformer每一层都有残差连接:
# Transformer层的结构
x = x + self_attention(x) # 残差连接1
x = x + feed_forward(x) # 残差连接2
六、KL散度
问题:如何衡量两个概率分布的差异?
训练VAE时,我们希望编码器输出的分布 $q(z|x)$ 接近先验分布 $p(z)$。
公式
$$D_{KL}(P | Q) = \sum_x P(x) \log \frac{P(x)}{Q(x)}$$
或连续形式: $$D_{KL}(P | Q) = \int p(x) \log \frac{p(x)}{q(x)} dx$$
拆解
- $P(x)$:真实分布(或我们想要的分布)
- $Q(x)$:近似分布(模型输出的分布)
- $\log \frac{P(x)}{Q(x)}$:两个分布在每个点的"差异"
- 求和/积分:用P加权平均
性质:
- $D_{KL} \geq 0$,等于0当且仅当 $P = Q$
- 不对称:$D_{KL}(P | Q) \neq D_{KL}(Q | P)$
代码实现
import numpy as np
def kl_divergence(p, q):
"""
离散KL散度
p, q: 概率分布,shape相同,和为1
"""
# 避免log(0)
q = np.clip(q, 1e-10, 1)
p = np.clip(p, 1e-10, 1)
return np.sum(p * np.log(p / q))
# 示例
p = np.array([0.4, 0.3, 0.3]) # 真实分布
q = np.array([0.5, 0.3, 0.2]) # 近似分布
kl = kl_divergence(p, q)
print(f"KL(P||Q) = {kl:.4f}") # 约0.036
VAE中的KL散度
VAE的损失函数包含KL项,让编码器输出接近标准正态分布:
$$D_{KL}(q(z|x) | \mathcal{N}(0, I)) = -\frac{1}{2} \sum_{j=1}^{d} (1 + \log \sigma_j^2 - \mu_j^2 - \sigma_j^2)$$
def vae_kl_loss(mu, log_var):
"""
VAE的KL散度损失(解析形式)
mu: 编码器输出的均值, shape (batch, latent_dim)
log_var: 编码器输出的log方差, shape (batch, latent_dim)
"""
# -0.5 * sum(1 + log(sigma^2) - mu^2 - sigma^2)
kl = -0.5 * torch.sum(1 + log_var - mu.pow(2) - log_var.exp(), dim=1)
return kl.mean()
# 示例
mu = torch.randn(32, 64) # batch=32, latent_dim=64
log_var = torch.randn(32, 64)
kl_loss = vae_kl_loss(mu, log_var)
print(f"KL Loss: {kl_loss.item():.4f}")
七、卷积操作
问题:如何提取图像的局部特征?
全连接层把每个像素都连接起来,参数太多且忽略了空间结构。卷积通过滑动窗口提取局部特征。
公式
$$(I * K)[i, j] = \sum_{m=0}^{k-1} \sum_{n=0}^{k-1} I[i+m, j+n] \cdot K[m, n]$$
输出尺寸计算: $$H_{out} = \lfloor \frac{H_{in} + 2p - k}{s} \rfloor + 1$$
其中:$p$ = padding,$k$ = kernel size,$s$ = stride
代码实现
import torch
import torch.nn as nn
# 2D卷积
conv = nn.Conv2d(
in_channels=3, # 输入通道(如RGB)
out_channels=64, # 输出通道(卷积核数量)
kernel_size=3, # 卷积核大小
stride=1, # 步长
padding=1 # 填充
)
x = torch.randn(1, 3, 32, 32) # (batch, channels, H, W)
y = conv(x)
print(y.shape) # torch.Size([1, 64, 32, 32])
# 手动计算输出尺寸
H_in, k, p, s = 32, 3, 1, 1
H_out = (H_in + 2*p - k) // s + 1 # 32
八、Layer Normalization
问题:如何稳定训练过程?
神经网络各层的输入分布会随训练变化(内部协变量偏移),导致训练不稳定。
公式
$$\text{LayerNorm}(x) = \gamma \cdot \frac{x - \mu}{\sqrt{\sigma^2 + \epsilon}} + \beta$$
其中:
- $\mu = \frac{1}{d}\sum_{i=1}^{d} x_i$(均值)
- $\sigma^2 = \frac{1}{d}\sum_{i=1}^{d} (x_i - \mu)^2$(方差)
- $\gamma, \beta$:可学习的缩放和偏移参数
代码实现
import torch
import torch.nn as nn
# PyTorch内置
ln = nn.LayerNorm(normalized_shape=64)
x = torch.randn(32, 10, 64) # (batch, seq_len, hidden)
y = ln(x)
# 手动实现
def layer_norm(x, gamma, beta, eps=1e-5):
mean = x.mean(dim=-1, keepdim=True)
var = x.var(dim=-1, keepdim=True, unbiased=False)
x_norm = (x - mean) / torch.sqrt(var + eps)
return gamma * x_norm + beta
九、位置编码(Positional Encoding)
问题:Transformer如何知道词的顺序?
自注意力机制是置换不变的,无法区分"我爱你"和"你爱我"。需要显式注入位置信息。
公式
偶数维用正弦、奇数维用余弦($pos$ 为位置,$d_{model}$ 为模型维度):
$$PE_{(pos,,2i)} = \sin\left(\frac{pos}{10000^{2i/d_{model}}}\right)$$
$$PE_{(pos,,2i+1)} = \cos\left(\frac{pos}{10000^{2i/d_{model}}}\right)$$
代码实现
import numpy as np
import torch
def positional_encoding(max_len, d_model):
"""
生成位置编码矩阵
max_len: 最大序列长度
d_model: 模型维度
"""
pe = np.zeros((max_len, d_model))
position = np.arange(max_len)[:, np.newaxis]
div_term = np.exp(np.arange(0, d_model, 2) * (-np.log(10000.0) / d_model))
pe[:, 0::2] = np.sin(position * div_term) # 偶数维度
pe[:, 1::2] = np.cos(position * div_term) # 奇数维度
return torch.FloatTensor(pe)
# 示例
pe = positional_encoding(100, 512)
print(pe.shape) # torch.Size([100, 512])
# 使用:x = x + pe[:seq_len, :]
十、ELBO(变分下界)
问题:如何训练生成模型?
VAE需要最大化数据的对数似然 $\log p(x)$,但直接计算不可行。
公式
$$\log p(x) \geq \mathbb{E}{q(z|x)}[\log p(x|z)] - D{KL}(q(z|x) | p(z))$$
ELBO = 重构损失 + KL正则化
拆解
- $\mathbb{E}_{q(z|x)}[\log p(x|z)]$:重构项,解码器能否从z还原x
- $D_{KL}(q(z|x) | p(z))$:正则项,编码器输出要接近先验分布
代码实现
import torch
import torch.nn.functional as F
def vae_loss(x, x_recon, mu, log_var):
"""
VAE损失 = 重构损失 + KL散度
x: 原始输入
x_recon: 重构输出
mu, log_var: 编码器输出的均值和log方差
"""
# 重构损失(二值交叉熵或MSE)
recon_loss = F.mse_loss(x_recon, x, reduction='sum')
# KL散度(解析形式)
kl_loss = -0.5 * torch.sum(1 + log_var - mu.pow(2) - log_var.exp())
return recon_loss + kl_loss
公式速查表
核心公式
| 公式 | 用途 | 论文 |
|---|---|---|
| $\text{softmax}(x_i) = \frac{e^{x_i}}{\sum_j e^{x_j}}$ | 分类、注意力权重 | 几乎所有 |
| $\text{Attention} = \text{softmax}(\frac{QK^T}{\sqrt{d_k}})V$ | Transformer核心 | Attention Is All You Need |
| $\theta \leftarrow \theta - \eta \nabla L$ | 模型训练 | 所有深度学习 |
| $c_t = f_t \odot c_{t-1} + i_t \odot \tilde{c}_t$ | 序列建模 | LSTM |
| $y = F(x) + x$ | 深层网络训练 | ResNet, Transformer |
| $D_{KL}(P | Q) = \sum P \log \frac{P}{Q}$ | 分布匹配 | VAE, RLHF |
损失函数
| 公式 | 用途 |
|---|---|
| $L = -\sum y_i \log \hat{y}_i$ | 交叉熵(分类) |
| $L = \frac{1}{n}\sum (y - \hat{y})^2$ | MSE(回归) |
| $L = -\log P(Y|X)$ | 语言模型损失 |
| $L = \text{Recon} + \beta \cdot D_{KL}$ | VAE损失 |
归一化
| 公式 | 用途 |
|---|---|
| $\text{LayerNorm}(x) = \gamma \frac{x-\mu}{\sigma} + \beta$ | Transformer |
| $\text{BatchNorm}(x) = \gamma \frac{x-\mu_B}{\sigma_B} + \beta$ | CNN |
位置编码
| 公式 | 用途 |
|---|---|
| $PE_{pos,2i} = \sin(pos/10000^{2i/d})$ | Transformer位置编码 |
| $PE_{pos,2i+1} = \cos(pos/10000^{2i/d})$ | Transformer位置编码 |
输出尺寸计算
| 公式 | 用途 |
|---|---|
| $H_{out} = \lfloor\frac{H_{in}+2p-k}{s}\rfloor + 1$ | 卷积输出尺寸 |
| $H_{out} = \lfloor\frac{H_{in}+2p-d(k-1)-1}{s}\rfloor + 1$ | 膨胀卷积输出尺寸 |
下一步
恭喜!你已经掌握了阅读AI论文所需的核心数学。现在可以去挑战这些论文了:
- 【ChatGPT时刻】系列:从Word2Vec到RLHF
- 【论文解读】系列:Ilya推荐的30篇经典论文
遇到新符号?回来查符号速查手册。