Softmax:将分数变为概率

Softmax函数将任意实数向量转换为概率分布——所有元素为正且和为1。它是分类、注意力、语言模型等核心组件的数学基础。

Softmax(x_i) = exp(x_i) / Σ_j exp(x_j)

看似简单的公式背后,隐藏着数值稳定性、计算效率和梯度行为的深刻问题。

数值稳定性

溢出问题

当输入值很大时,exp(x)会溢出。例如exp(1000)远超FP32的范围(~3.4e38)。解决方案是减去最大值:

def softmax_stable(x, dim=-1):
    """数值稳定的Softmax"""
    x_max = x.max(dim=dim, keepdim=True).values
    exp_x = torch.exp(x - x_max)
    return exp_x / exp_x.sum(dim=dim, keepdim=True)

减去最大值不改变结果(因为分子分母同时除以exp(max)),但将指数运算的输入控制在合理范围内。

Log-Softmax

在许多场景中(如交叉熵损失),我们需要的是log概率而非概率本身。直接对softmax取log可能损失精度:

def log_softmax_stable(x, dim=-1):
    """数值稳定的Log-Softmax"""
    x_max = x.max(dim=dim, keepdim=True).values
    shifted = x - x_max
    log_sum_exp = torch.log(torch.exp(shifted).sum(dim=dim, keepdim=True))
    return shifted - log_sum_exp

PyTorch的F.log_softmaxF.cross_entropy内部都使用了这种稳定的实现。

Softmax的梯度特性

饱和问题

当某个输入远大于其他输入时,Softmax的输出接近one-hot——一个接近1,其余接近0。此时梯度几乎为零,导致学习停滞。

# 梯度公式
# ∂softmax(x_i)/∂x_j = softmax(x_i) * (δ_ij - softmax(x_j))

当softmax(x_i) ≈ 1时,梯度 ≈ 1 × (δ_ij - softmax(x_j)) ≈ 0(对所有j)。

温度调节

温度参数T控制softmax的"尖锐度":

Softmax_T(x_i) = exp(x_i / T) / Σ_j exp(x_j / T)
  • T→0:趋向one-hot(尖锐)
  • T→∞:趋向均匀分布(平滑)
  • T=1:标准softmax

在知识蒸馏中,高温(T=4-10)使Teacher模型的输出更"软",传递更多暗知识。

LLM中的Softmax变体

Scaled Dot-Product Softmax

注意力中的softmax需要除以√d_k来防止内积值过大导致饱和:

def attention_softmax(query, key, scale=1.0, mask=None):
    """注意力中的缩放Softmax"""
    scores = query @ key.transpose(-2, -1) * scale
    if mask is not None:
        scores = scores.masked_fill(mask == 0, float('-inf'))
    return F.softmax(scores, dim=-1)

稀疏Softmax

标准softmax对所有位置分配非零概率。稀疏softmax允许输出精确的零:

def sparse_softmax(x, top_k=None, threshold=None):
    """稀疏Softmax:只保留top-k或超过阈值的元素"""
    if top_k is not None:
        # 保留top-k个元素
        topk_values, topk_indices = x.topk(top_k, dim=-1)
        softmax_topk = F.softmax(topk_values, dim=-1)
        
        output = torch.zeros_like(x)
        output.scatter_(-1, topk_indices, softmax_topk)
        return output
    
    if threshold is not None:
        # 保留超过阈值的元素
        mask = x > threshold
        masked_x = x.masked_fill(~mask, float('-inf'))
        return F.softmax(masked_x, dim=-1)

Gumbel-Softmax

用于可微地采样离散分布:

def gumbel_softmax(logits, temperature=1.0, hard=False):
    """Gumbel-Softmax:可微的离散采样"""
    # Gumbel(0,1)噪声
    gumbels = -torch.empty_like(logits).exponential_().log()
    gumbels = (logits + gumbels) / temperature
    
    y_soft = F.softmax(gumbels, dim=-1)
    
    if hard:
        # 直通估计器:前向用hard,反向用soft
        index = y_soft.max(dim=-1, keepdim=True).indices
        y_hard = torch.zeros_like(logits).scatter_(-1, index, 1.0)
        return y_hard - y_soft.detach() + y_soft
    else:
        return y_soft

线性Softmax变体

标准Softmax的复杂度问题

在注意力计算中,softmax(QK^T)需要实例化完整的n×n矩阵,复杂度O(n²)。线性Softmax变体通过近似或替换softmax实现O(n)复杂度。

注意力线性化

将softmax替换为非负核函数:

def linear_softmax_attention(Q, K, V, kernel='elu'):
    """线性Softmax注意力"""
    if kernel == 'elu':
        # φ(x) = elu(x) + 1,保证非负
        phi_Q = F.elu(Q) + 1
        phi_K = F.elu(K) + 1
    elif kernel == 'exp':
        # φ(x) = exp(x),需要数值稳定处理
        phi_Q = torch.exp(Q - Q.max(dim=-1, keepdim=True).values)
        phi_K = torch.exp(K - K.max(dim=-1, keepdim=True).values)
    elif kernel == 'relu':
        # φ(x) = relu(x),简单但表达力有限
        phi_Q = F.relu(Q)
        phi_K = F.relu(K)
    
    # 线性注意力: (Q(K^T V)) 而非 softmax(QK^T)V
    # 利用结合律: 先算 K^T V,再算 Q
    KV = phi_K.transpose(-2, -1) @ V  # [d, d]
    output = phi_Q @ KV  # [n, d]
    
    # 归一化
    normalizer = phi_Q @ (phi_K.sum(dim=-2, keepdim=True).transpose(-2, -1))
    output = output / (normalizer + 1e-6)
    
    return output

Polynomial Softmax

使用多项式核替代指数函数:

def polynomial_softmax_attention(Q, K, V, degree=2):
    """多项式Softmax近似"""
    # φ(x) = [x, x², x³, ...] 的多项式特征映射
    def phi(x, degree):
        features = [x ** d for d in range(1, degree + 1)]
        return torch.cat(features, dim=-1)
    
    phi_Q = phi(Q, degree)  # [batch, n, d*degree]
    phi_K = phi(K, degree)  # [batch, m, d*degree]
    
    # 线性注意力
    KV = phi_K.transpose(-2, -1) @ V
    output = phi_Q @ KV
    
    normalizer = phi_Q @ phi_K.sum(dim=-2, keepdim=True).transpose(-2, -1)
    return output / (normalizer + 1e-6)

Hierarchical Softmax

问题背景

在超大词表(100K+)中,标准softmax需要计算所有词的logits,计算量为O(V×D)。Hierarchical Softmax通过树结构将复杂度降至O(D×logV)。

实现

class HierarchicalSoftmax(nn.Module):
    def __init__(self, vocab_size, d_model, tree_depth=None):
        super().__init__()
        self.vocab_size = vocab_size
        self.tree_depth = tree_depth or int(math.log2(vocab_size))
        
        # 每个内部节点一个二分类器
        self.internal_nodes = (2 ** self.tree_depth) - 1
        self.node_classifiers = nn.Linear(d_model, self.internal_nodes)
        
        # 叶节点到路径的映射
        self.token_to_path = self.build_huffman_tree(vocab_size)
    
    def forward(self, hidden, targets=None):
        """
        hidden: [batch, d_model]
        targets: [batch] 或 None(推理模式)
        """
        if targets is not None:
            # 训练模式:只计算目标token路径上的节点
            batch_size = hidden.shape[0]
            loss = 0
            
            for b in range(batch_size):
                path = self.token_to_path[targets[b].item()]
                # path是一串0/1,表示在每个节点的左右选择
                node_logits = self.node_classifiers(hidden[b])  # [n_nodes]
                
                for i, direction in enumerate(path):
                    loss += F.binary_cross_entropy_with_logits(
                        node_logits[i], 
                        torch.tensor(float(direction), device=hidden.device)
                    )
            
            return loss / batch_size
        else:
            # 推理模式:沿树搜索
            node_logits = self.node_classifiers(hidden)  # [batch, n_nodes]
            
            # 贪心搜索:在每个节点选择概率更大的方向
            predictions = []
            for b in range(hidden.shape[0]):
                node = 0
                path = []
                for _ in range(self.tree_depth):
                    prob = torch.sigmoid(node_logits[b, node])
                    direction = (prob > 0.5).int().item()
                    path.append(direction)
                    node = 2 * node + 1 + direction
                
                # 路径转token ID
                token_id = int(''.join(map(str, path)), 2)
                predictions.append(token_id)
            
            return torch.tensor(predictions)

2026年Softmax前沿

Adaptive Softmax

Facebook提出的自适应Softmax——根据词频将词表分为不同层级,高频词在浅层(快速),低频词在深层(慢但少用):

class AdaptiveSoftmax(nn.Module):
    def __init__(self, vocab_size, d_model, cutoffs=[20000, 50000, 100000]):
        super().__init__()
        self.cutoffs = cutoffs
        self.n_clusters = len(cutoffs) + 1
        
        # 头部:高频词 + 各聚类的指针
        head_size = cutoffs[0] + self.n_clusters - 1
        self.head = nn.Linear(d_model, head_size)
        
        # 各聚类的分类器
        self.tail = nn.ModuleList()
        for i in range(len(cutoffs)):
            if i == 0:
                start, end = cutoffs[0], cutoffs[1]
            else:
                start, end = cutoffs[i], cutoffs[i+1] if i+1 < len(cutoffs) else vocab_size
            self.tail.append(nn.Linear(d_model, end - start))
    
    def forward(self, hidden, targets=None):
        # 头部logits(高频词+聚类指针)
        head_logits = self.head(hidden)
        
        if targets is not None:
            # 根据目标词的位置计算不同的损失
            loss = 0
            head_targets = targets.clone()
            tail_mask = targets >= self.cutoffs[0]
            
            # 低频词通过聚类指针计算
            if tail_mask.any():
                for i, tail_classifier in enumerate(self.tail):
                    cluster_start = self.cutoffs[i] if i > 0 else self.cutoffs[0]
                    cluster_end = self.cutoffs[i+1] if i+1 < len(self.cutoffs) else self.vocab_size
                    
                    in_cluster = (targets >= cluster_start) & (targets < cluster_end)
                    if in_cluster.any():
                        tail_logits = tail_classifier(hidden[in_cluster])
                        tail_targets = targets[in_cluster] - cluster_start
                        loss += F.cross_entropy(tail_logits, tail_targets)
            
            # 高频词的损失
            head_mask = ~tail_mask
            if head_mask.any():
                loss += F.cross_entropy(
                    head_logits[head_mask, :self.cutoffs[0]], 
                    targets[head_mask]
                )
            
            return loss

Mixture of Softmaxes

解决Softmax在低维隐藏空间中的表达能力限制:

class MixtureOfSoftmaxes(nn.Module):
    def __init__(self, d_model, vocab_size, n_components=5):
        super().__init__()
        self.n_components = n_components
        # 混合权重
        self.prior = nn.Linear(d_model, n_components)
        # 各组件的logits
        self.lm_heads = nn.ModuleList([
            nn.Linear(d_model, vocab_size) for _ in range(n_components)
        ])
    
    def forward(self, hidden):
        # 混合权重
        mix_weights = F.softmax(self.prior(hidden), dim=-1)  # [batch, seq, K]
        
        # 各组件的softmax
        output = 0
        for k in range(self.n_components):
            logits = self.lm_heads[k](hidden)
            probs = F.softmax(logits, dim=-1)
            output += mix_weights[..., k:k+1] * probs
        
        return output

结语

Softmax是深度学习中最基础的数学操作之一,其变体研究在2026年仍然活跃。从数值稳定性到计算效率、从精确计算到线性近似、从扁平结构到层次结构,Softmax的每一个改进都直接影响着LLM的训练效率和推理质量。

加入讨论

这篇文章有姊妹讨论帖在硅基AGI论坛 — 全球首个碳基硅基认知交流平台。

碳基与硅基的智慧碰撞,认知差异创造无限可能。