上一篇我们搞定了符号,这一篇直接拆解AI论文中出现频率最高的6个公式。每个公式都是:问题→公式→拆解→代码

前置要求:读完上一篇符号速查手册。


一、Softmax与交叉熵损失

问题:如何把模型输出变成概率?

神经网络输出的是一堆数字(logits),可能是负数、可能很大。我们需要把它们变成概率分布(非负、和为1)。

公式

$$\text{softmax}(x_i) = \frac{e^{x_i}}{\sum_{j=1}^{K} e^{x_j}}$$

拆解

  1. $e^{x_i}$:指数函数,把任意数变成正数
  2. $\sum_{j=1}^{K} e^{x_j}$:所有指数的和,用于归一化
  3. 除法:确保结果和为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$$

拆解

  1. Q(Query):我在找什么?
  2. K(Key):每个位置的"标签"
  3. V(Value):每个位置的实际内容
  4. $QK^T$:Query和所有Key的相似度(点积)
  5. $\sqrt{d_k}$:缩放因子,防止点积太大导致softmax饱和
  6. softmax:把相似度变成注意力权重(概率分布)
  7. 乘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)$$

拆解

  1. $\theta_t$:当前参数
  2. $\nabla_\theta L$:损失对参数的梯度(指向损失增大最快的方向)
  3. $\eta$:学习率(步长)
  4. 减号:往梯度反方向走(损失减小的方向)

直觉:站在山上,梯度告诉你哪边最陡,你往反方向走就能下山。

代码实现

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)$$

拆解

  1. $[h_{t-1}, x_t]$:把上一时刻的隐藏状态和当前输入拼接
  2. $\sigma$(sigmoid):输出0-1,作为"门"的开关程度
  3. $\odot$:逐元素相乘,实现"门控"
  4. $c_t$(细胞状态):长期记忆,信息在这里流动
  5. $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$$

拆解

  1. $F(x)$:网络层要学习的变换
  2. $+ 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$$

拆解

  1. $P(x)$:真实分布(或我们想要的分布)
  2. $Q(x)$:近似分布(模型输出的分布)
  3. $\log \frac{P(x)}{Q(x)}$:两个分布在每个点的"差异"
  4. 求和/积分:用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正则化

拆解

  1. $\mathbb{E}_{q(z|x)}[\log p(x|z)]$:重构项,解码器能否从z还原x
  2. $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论文所需的核心数学。现在可以去挑战这些论文了:

遇到新符号?回来查符号速查手册


上一篇:【AI数学速成01】30分钟搞懂AI论文里的数学符号