跳转到内容

范数计算

范数(Norm)是向量和矩阵分析中最常用的度量之一,用于衡量向量的大小或矩阵的"长度"。在深度学习中,范数无处不在:正则化项(如 L1、L2 正则化)、梯度范数监控、权重初始化、相似度度量等都会用到范数。NumPy 提供了 np.linalg.norm 函数来计算各种范数,理解不同范数的含义和适用场景对于调试神经网络、防止梯度消失/爆炸、以及设计损失函数都非常重要。

范数的定义

简单来说,范数就是向量的"长度"。对于 n 维向量 x = [x₁, x₂, ..., xₙ],常用的范数定义如下:

L1 范数(曼哈顿范数):||x||₁ = Σ|xᵢ|,即向量元素绝对值之和。

L2 范数(欧几里得范数):||x||₂ = √(Σxᵢ²),即向量元素平方和的平方根,这也是我们最直观理解的"长度"。

无穷范数:||x||∞ = max|xᵢ|,即向量元素绝对值的最大值。

Frobenius 范数:||A||F = √(ΣᵢΣⱼAᵢⱼ²),用于衡量矩阵的大小,类似于矩阵的 L2 范数。

np.linalg.norm 的基本用法

python
import numpy as np

x = np.array([3, 4])

# L2 范数(默认)
l2_norm = np.linalg.norm(x)
print(f"L2 范数: {l2_norm}")  # 输出 5.0

# L1 范数
l1_norm = np.linalg.norm(x, ord=1)
print(f"L1 范数: {l1_norm}")  # 输出 7.0

# 无穷范数
inf_norm = np.linalg.norm(x, ord=np.inf)
print(f"无穷范数: {inf_norm}")  # 输出 4.0

不同范数的几何理解

L2 范数对应我们直觉意义上的"距离"。在二维空间中,L2 范数为 1 的所有点构成一个单位圆(或者说单位球面)。L2 范数的优点是计算方便、梯度稳定、处处可微。

L1 范数对应曼哈顿距离。在二维空间中,L1 范数为 1 的所有点构成一个旋转了 45 度的正方形(菱形)。L1 范数的一个重要特性是它会产生稀疏解——这在压缩感知和特征选择中很有用。

python
# 几何可视化
import matplotlib.pyplot as plt

x = np.linspace(-1, 1, 100)
y = np.linspace(-1, 1, 100)
X, Y = np.meshgrid(x, y)

# L2 范数为 1 的等高线(单位圆)
Z_l2 = np.sqrt(X**2 + Y**2)

# L1 范数为 1 的等高线(菱形)
Z_l1 = np.abs(X) + np.abs(Y)

# 画图的代码略去,重点理解几何含义

矩阵的 Frobenius 范数

对于矩阵,除了向量范数外,最常用的是 Frobenius 范数:

python
A = np.array([[1, 2], [3, 4]])

# Frobenius 范数(矩阵元素的平方和开根号)
fro_norm = np.linalg.norm(A, 'fro')
print(f"Frobenius 范数: {fro_norm}")  # 输出 sqrt(30) ≈ 5.477

# 等价于 flatten 后再计算 L2 范数
fro_norm_alt = np.linalg.norm(A.flatten())
print(f"等价计算: {fro_norm_alt}")

Frobenius 范数在深度学习中常用于衡量矩阵(或权重)的大小,以及作为正则化项。

在LLM场景中的应用

梯度范数监控

训练大型语言模型时,监控梯度范数是调试训练过程的重要手段。梯度范数过大可能导致梯度爆炸,过小可能导致梯度消失。

python
def compute_gradients_norm(model_weights, gradients):
    """计算梯度范数,用于监控训练稳定性

    参数:
        model_weights: 模型权重字典
        gradients: 梯度字典
    返回:
        total_grad_norm: 总梯度范数
        grad_norms: 各层梯度范数字典
    """
    grad_norms = {}
    total_norm = 0.0

    for name, grad in gradients.items():
        if grad is not None:
            param_norm = np.linalg.norm(grad)
            grad_norms[name] = param_norm
            total_norm += param_norm ** 2

    total_norm = total_norm ** 0.5
    return total_norm, grad_norms

# 模拟梯度
np.random.seed(42)
gradients = {
    'embedding.weight': np.random.randn(50257, 768) * 0.01,
    'layer.0.attn.weight': np.random.randn(768, 768) * 0.01,
    'layer.0.attn.bias': np.random.randn(768) * 0.01,
}

total_norm, grad_norms = compute_gradients_norm(None, gradients)
print(f"总梯度范数: {total_norm:.4f}")
for name, norm in grad_norms.items():
    print(f"  {name}: {norm:.4f}")

L2 正则化

L2 正则化是最常用的正则化技术之一,它在损失函数中添加权重范数的惩罚项:

python
def l2_regularization(weights, lambda_reg):
    """计算 L2 正则化项

    L2 正则化项 = λ * Σ||w||²
    """
    reg_term = 0.0
    for name, weight in weights.items():
        reg_term += np.sum(weight ** 2)
    return lambda_reg * reg_term

# 模拟权重
weights = {
    'layer.0.weight': np.random.randn(768, 768),
    'layer.1.weight': np.random.randn(768, 768),
}

lambda_reg = 0.01
reg_term = l2_regularization(weights, lambda_reg)
print(f"L2 正则化项: {reg_term:.4f}")

权重衰减(Weight Decay)

权重衰减是 L2 正则化的近亲,在优化器中实现时会有细微差别。在 PyTorch 的 AdamW 优化器中,权重衰减是直接对权重进行衰减,而不是添加到损失函数中:

python
def apply_weight_decay(weights, lr, weight_decay):
    """模拟权重衰减更新"""
    for name, weight in weights.items():
        # 权重衰减:w = w * (1 - lr * weight_decay)
        weights[name] = weight * (1 - lr * weight_decay)
    return weights

weights = {'layer.weight': np.random.randn(100, 100)}
new_weights = apply_weight_decay(weights, lr=1e-3, weight_decay=0.1)
print(f"权重衰减后范数: {np.linalg.norm(new_weights['layer.weight']):.4f}")

梯度裁剪

梯度裁剪是防止梯度爆炸的常用技术,它将梯度范数限制在某个阈值内:

python
def clip_gradients(gradients, max_norm):
    """梯度裁剪:将梯度范数限制在 max_norm 以内

    裁剪公式:grad = grad * min(1, max_norm / ||grad||)
    """
    total_norm = np.linalg.norm([np.linalg.norm(g) for g in gradients.values() if g is not None])

    clip_coef = max_norm / (total_norm + 1e-6)
    if clip_coef < 1:
        clipped_grads = {name: grad * clip_coef for name, grad in gradients.items()}
        return clipped_grads, True

    return gradients, False

# 测试梯度裁剪
np.random.seed(42)
gradients = {
    'layer.weight': np.random.randn(100, 100) * 10,  # 梯度很大
}

max_norm = 1.0
clipped_grads, was_clipped = clip_gradients(gradients, max_norm)
new_norm = np.linalg.norm(clipped_grads['layer.weight'])
print(f"裁剪前范数: {np.linalg.norm(gradients['layer.weight']):.4f}")
print(f"裁剪后范数: {new_norm:.4f}")
print(f"是否裁剪: {was_clipped}")

常见误区

误区一:混淆 L1 和 L2 正则化

L1 正则化产生稀疏权重(L1 范数小的解更容易为零),适合特征选择;L2 正则化产生稠密权重(L2 范数小的解,各元素都小但不为零),适合防止权重过大。在实践中,L2 更常用,但如果希望模型稀疏,可以考虑 L1 或 L1/L2 混合(Elastic Net)。

python
# L1 vs L2 范数的稀疏性对比
x = np.array([0.5, 0.5, 0.01, 0.01])

print(f"L1 范数: {np.linalg.norm(x, ord=1):.4f}")  # 强调小元素
print(f"L2 范数: {np.linalg.norm(x, ord=2):.4f}")  # 对大元素更敏感

误区二:忽略不同 ord 参数的含义

np.linalg.normord 参数在不同维度下含义不同。对于向量:ord=1 是 L1,ord=2 是 L2(默认),ord=np.inf 是无穷范数。对于矩阵,ord='fro' 是 Frobenius 范数,ord=1 是最大列范数,ord=2 是最大奇异值,ord=np.inf 是最大行范数。

误区三:在需要可微的地方使用 L1 范数

L1 范数在零点处不可微,这在需要计算梯度的场景下可能会有问题。实践中可以使用平滑近似(如 L1 平滑)或者接受在零点处使用次梯度。

性能考虑

计算向量范数的时间复杂度是 O(n),计算矩阵 Frobenius 范数的时间复杂度是 O(mn),都非常高效。np.linalg.norm 内部会优化内存使用,直接计算而不显式构造中间矩阵。

python
# 性能测试
import time

A = np.random.randn(1000, 1000)

start = time.time()
norm1 = np.linalg.norm(A, 'fro')  # Frobenius 范数
t1 = time.time() - start

start = time.time()
norm2 = np.sqrt(np.sum(A**2))  # 等价计算
t2 = time.time() - start

print(f"np.linalg.norm: {t1*1000:.2f}ms")
print(f"手动计算: {t2*1000:.2f}ms")

API 总结

ord 参数向量含义矩阵含义
默认/2L2 范数Frobenius 范数
1L1 范数最大列范数
np.inf无穷范数最大行范数
'fro'-Frobenius 范数

理解范数对于理解深度学习中的许多概念至关重要。从梯度消失/爆炸的诊断,到正则化策略的选择,再到注意力权重的归一化,范数无处不在。掌握 np.linalg.norm 的用法,能让你在调试 LLMs 时更加得心应手。

基于 MIT 许可发布