大语言模型的底层-优化器详解

大语言模型似乎很厉害,外行人如果不了解大模型,就可能会觉得这东西很神秘,很了不得,甚至担心什么天人降临统治人类等等。

虽然他可能会告诉你要走路去五十米外的地方洗车,而把车停在原地。

人往往会因为缺乏足够的认知而产生恐惧,实际上稍微从基础层面做一点了解就会发现,这东西并没有那么复杂。 本文的公式内容可能比之前的要多一点,数学直觉稍微钝感的同学可能觉得看着费力。 这不要紧,因为这些公式对于大家来说未必有那么重要,而这个过程的思路,在头脑中能够大概想明白,才是最终重要的。 可以不知道到底是怎么算的,具体公式看不明白写不明白都没事,但是要明白这个过程是怎么样的,要明白为什么这么做,要明白为什么这么做是有效的。 至少这样就不会因为茫然无措而感到恐惧,心里就有底了。真需要细致地去了解这些公式的时候,稍作深入也并不是那么难,到时候再学也来得及。

大模型的能力来源是数据,获得能力的过程要靠训练,而训练过程本质上是在一个高维、复杂的错误率曲面上寻找最低点(即 Loss 最小化)。就像在漆黑的崇山峻岭中寻找谷底,而优化器就是我们手中的指南针和步伐控制器。它决定了我们每次迈出多远、朝哪个方向走,以及遇到坑洼时如何调整姿态。

简单来看,可以把训练大语言模型(LLM)想象成培养一个考试高手的过程。如果说模型架构(如 Transformer)是这个考生的大脑容量和智商上限,海量的训练数据是教材和习题集,那么优化器(Optimizer)就是他所采用的学习方法。这个“学习方法”决定了他在做错题时如何反思:是死记硬背(效率低)、举一反三(效率高),还是针对自己的薄弱环节重点突破(自适应学习)。一个好的优化器,能让模型在同样的时间内学到更多的知识,或者用更少的习题达到同样的考试分数(收敛更快)。

在很长一段时间里,AdamW 都是 LLM 训练的主流选择。然而,随着模型规模的不断膨胀和对训练效率的极致追求,名为 Muon 的新型优化器横空出世,能以更少的步数达到相同的效果。这回咱们就看看优化器的简单来历以及特点等等。


一、优化器的基石:从 SGD 到自适应

1. 为什么“随机梯度下降”能有用?(SGD)

想象你在山上迷路了,周围大雾弥漫(看不清全貌),你想下山(找 Loss 最低点)。 你唯一能做的是:用脚探一探周围哪里最陡。 如果脚下的路是向北向下倾斜的,那你往北走一步,大概率海拔会降低。 这就是 SGD(随机梯度下降) 的核心思想:

  • 梯度(Gradient):就是“坡度”和“方向”。
  • 下降(Descent):沿着坡最陡的方向往下走。
  • 随机(Stochastic):因为山太大了(数据太多),我们没法一次性把整座山都测绘一遍。我们每次只随机挑一小块地(Mini-batch)来探路。虽然有时候这块地的坡度可能带有误导性(比如只是个小坑),但只要次数够多,大方向总体是向下的。

2. 什么是“动量”?(Momentum)

SGD 有个大问题:它没有“惯性”。 如果山谷是狭长的(像一个U型管),SGD 可能会在两边的峭壁上来回撞(震荡),而不能顺着谷底快速流下去。或者遇到一个小坑(局部最优),它可能就停在坑底不动了,因为它只看脚下,不看趋势。

于是我们引入了 动量(Momentum),想象把下山的人换成一个 大铁球

  • 惯性保持:当球滚下来时,它积累了速度(动量)。即使遇到一个小坡(梯度的反方向),靠着之前的惯性,它也能冲过去。
  • 加速前进:如果一直是下坡路,球会越滚越快,比每一步都重新起步的 SGD 快得多。

在数学上,就是把“上一步的更新方向”加个系数(比如 0.9),叠加上“当前的梯度”。

3. 什么是“自适应步长”?(Adaptive Learning Rate)

SGD 和 Momentum 还有一个共同的痛点:所有参数用同一个步长(学习率)。 但这在深度学习中很不合理。有些参数(比如Embedding层中常见词的权重)经常更新,对应的坡度很陡,如果步子太大容易扯着蛋甚至直接掉落“飞”出去;有些参数(生僻词的权重)很少更新,坡度很平,如果步子太小简直像蜗牛原地小步爬。

自适应步长(如 RMSProp, AdaGrad)的思想是:给每个参数配一双特制的鞋

  • 陡坡穿小鞋:如果某个参数历史梯度的波动很大(平方和很大),说明这里地形险恶,我们要把它的学习率除以一个大数,让它走慢点,稳一点。
  • 平地穿大鞋:如果某个参数历史梯度一直很小,说明这里是一马平川,我们要把它的学习率除以一个小数(相当于放大),让它大步流星往前冲。

这就是 AdamW 中 (二阶矩)的作用:记录每个参数“过去有多颠簸”,然后据此调整它当下的步长。

4. 进化之路:从 SGD 到 Adam 的家谱

了解了上面三个概念,我们就能串起整个优化器的进化史:

  1. SGD:只有方向,没有惯性,步长固定。

  2. 缺点:慢,容易卡在鞍点。

  3. SGD + Momentum:加上了“惯性”。

  4. 进化点:解决了震荡和鞍点问题,跑得快了。

  5. AdaGrad:加上了“自适应步长”(累计所有历史梯度的平方)。

  6. 进化点:解决了稀疏数据(生僻词)更新难的问题。

  7. 缺点:因为是“累计”历史,分母会越来越大,训练到后期步长几乎为0,学不动了。

  8. RMSProp:改进版 AdaGrad(只看最近一段时间的梯度平方)。

  9. 进化点:解决了 AdaGrad 训练后期熄火的问题。

  10. **Adam (Adaptive Moment Estimation)**:集大成者。

  11. Adam = Momentum + RMSProp

  12. 它既用了 Momentum(一阶矩)来保持方向惯性,又用了 RMSProp(二阶矩)来自适应调整步长。可以说它既有“速度”,又有“路感”。

二、工业标准:AdamW(Adaptive Moment Estimation with Weight Decay)

AdamW 是 Adam 优化器的改进版,它结合了动量(一阶矩)和自适应步长(二阶矩),并正确地解耦了权重衰减(Weight Decay)。它是目前训练 GPT、Llama 等大模型的默认选择。

1. 核心机制

AdamW 维护两个状态变量:

  • (一阶矩):梯度的指数移动平均(类似动量)。
  • (二阶矩):梯度平方的指数移动平均(用于衡量梯度的波动大小)。

2. 算法步骤

对于每个参数 ,在第  步:

  1. 计算梯度
  2. 更新一阶矩(动量):
  3. 更新二阶矩(自适应缩放因子):
  4. 偏差修正(初期  偏向 0,需放大):
  5. 参数更新(关键步骤):其中  实现了自适应步长:梯度大的参数更新步幅缩小,梯度小的参数步幅放大。 是解耦后的权重衰减。

3. 优劣势分析

  • 优势

  • 极度稳健:几乎适用于任何类型的网络(CNN, RNN, Transformer),超参数(lr=1e-3/1e-4)有公认的默认值。

  • 自适应性:无需为每个层手动调整学习率。

  • 劣势

  • 显存占用高:需要为每个参数存储  和  两个状态,显存占用是参数量的 2 倍(如果是 FP32 状态,甚至更多)。

  • 计算偏内存密集:主要是逐元素的加减乘除,受限于显存带宽(Memory Bandwidth Bound)。

三、新晋选手:Muon(Momentum Update Orthogonalized Newton)

Muon 是专门针对 Transformer 中的二维权重矩阵(如 Linear 层的 )设计。它的核心洞见是:将参数更新约束为正交变换,利用矩阵的整体结构而非逐元素调整

1. 核心机制

Muon 不再像 AdamW 那样逐个元素地看梯度的“方差”,而是将整个权重矩阵视为一个整体。它通过 Newton-Schulz 迭代 将梯度的动量矩阵进行正交化(Orthogonalization)

这意味着 Muon 的更新步长在所有方向上是“均匀”的(基于谱范数),这允许模型使用更大的学习率,从而在训练初期快速穿越平坦区域。

2. 算法步骤

Muon 通常只用于  维的张量(如 ),而 1 维参数(如 LayerNorm, Bias)仍使用 AdamW。

  1. 计算梯度
  2. 更新动量
  3. 正交化(Newton-Schulz Iteration): 为了得到  的正交化版本 ,我们不使用昂贵的 SVD,而是用迭代法近似。 令 (归一化)。 循环迭代(通常 5 次):最终得到 。
  4. 参数更新:(通常还会根据权重自身的 RMS 进行缩放)

3. 优劣势分析

  • 优势

  • 收敛极快:正交更新使得优化器敢于迈大步,样本效率(Sample Efficiency)显著高于 AdamW。

  • 硬件友好:核心操作是矩阵乘法(MatMul),能充分利用 GPU 的 Tensor Cores,将负载从内存带宽转移到算力上。
  • 显存占用较低:仅需存储动量 (1 倍状态),且 Newton-Schulz 过程不需要额外的持久化状态。

  • 劣势

  • 实现复杂:需要混合优化策略(Muon 处理矩阵 + AdamW 处理向量)。

  • 超参敏感:Newton-Schulz 的迭代次数、动量系数等可能需要针对特定模型微调。
  • 数值稳定性:如果矩阵条件数极差,迭代可能不稳定。

四、AdamW vs. Muon 对比

维度 AdamW Muon
设计哲学 逐元素自适应 :每个参数独立调整步长,基于历史梯度的方差。 矩阵整体正交 :保留梯度的方向结构,去除幅度影响,基于谱范数。
状态量 (State) 2 个 () 1 个 ()
计算瓶颈 显存带宽 (Memory Bound):大量元素操作。 算力 (Compute Bound):大量矩阵乘法。
适用场景 通用,所有参数。 仅限 2D+ 权重矩阵(Linear, Embeddings)。
收敛速度 稳健,标准速度。 激进 ,在从零训练 LLM 时往往更快。
典型应用 绝大多数现有模型 (GPT-4, Llama 3)。 追求极致效率的开源小模型 (NanoGPT speedruns)。

五、可视化对比

为了直观展示两者的区别,我们在经典的 Rosenbrock 函数(香蕉函数)上模拟了 SGD、AdamW 和 Muon 的寻优路径。

  • SGD:在山谷壁上震荡,前进缓慢。
  • AdamW:利用二阶矩快速适应了不同方向的曲率,直奔谷底。
  • Muon(模拟版):通过正交化(在低维下近似为单位步长),路径更加平滑且直接。

Image

import numpy as np
import matplotlib.pyplot as plt
import matplotlib.colors as mcolors
import platform

# Configure fonts for Chinese support
system = platform.system()
if system == 'Windows':
    plt.rcParams['font.sans-serif'] = ['Microsoft YaHei', 'SimHei', 'Arial']
elif system == 'Darwin':
    plt.rcParams['font.sans-serif'] = ['Arial Unicode MS', 'PingFang SC']
else:
    plt.rcParams['font.sans-serif'] = ['WenQuanYi Micro Hei', 'DejaVu Sans']
plt.rcParams['axes.unicode_minus'] = False

def rosenbrock(x, y, a=1, b=100):
    return (a - x)**2 + b * (y - x**2)**2

def grad_rosenbrock(x, y, a=1, b=100):
    dx = -2 * (a - x) - 4 * b * x * (y - x**2)
    dy = 2 * b * (y - x**2)
    return np.array([dx, dy])

# Optimizer Implementations

class SGD:
    def __init__(self, lr=0.001, momentum=0.9):
        self.lr = lr
        self.momentum = momentum
        self.v = np.zeros(2)

    def step(self, params, grads):
        self.v = self.momentum * self.v - self.lr * grads
        return params + self.v

class AdamW:
    def __init__(self, lr=0.01, beta1=0.9, beta2=0.999, eps=1e-8, weight_decay=0.01):
        self.lr = lr
        self.beta1 = beta1
        self.beta2 = beta2
        self.eps = eps
        self.wd = weight_decay
        self.m = np.zeros(2)
        self.v = np.zeros(2)
        self.t = 0

    def step(self, params, grads):
        self.t += 1
        # Weight decay
        params = params - self.lr * self.wd * params

        # Adam
        self.m = self.beta1 * self.m + (1 - self.beta1) * grads
        self.v = self.beta2 * self.v + (1 - self.beta2) * grads**2

        m_hat = self.m / (1 - self.beta1**self.t)
        v_hat = self.v / (1 - self.beta2**self.t)

        return params - self.lr * m_hat / (np.sqrt(v_hat) + self.eps)

class Muon:
    def __init__(self, lr=0.02, momentum=0.95, nesterov=True, ns_steps=5):
        self.lr = lr
        self.momentum = momentum
        self.nesterov = nesterov
        self.ns_steps = ns_steps
        self.m = np.zeros(2)

    def newton_schulz(self, M):
        # M is shape (1, 2) or (2,) treated as (1, 2)
        # For a vector, this is effectively normalization, but let's simulate the matrix steps
        # Shape handling
        if M.ndim == 1:
            M = M.reshape(1, -1)

        # Normalization (Spectral norm approx via Frobenius for small matrix)
        # X = M / (||M|| + eps)
        norm = np.linalg.norm(M) + 1e-8
        X = M / norm

        # Newton-Schulz Iteration
        # X_{k+1} = 0.5 * X_k * (3I - X_k^T * X_k)
        # Note: For non-square matrices A (m x n), we use A * (3I - A^T A) or (3I - A A^T) * A depending on dimensions
        # Here 1x2, let's use the standard form for whitening: X * (3I - X.T @ X)
        # Wait, if X is 1x2, X.T @ X is 2x2. X @ X.T is 1x1.
        # The iteration converges to a matrix with singular values all 1.

        for _ in range(self.ns_steps):
            # For 1x2:
            # A = X (1x2)
            # B = 3I - X^T @ X (2x2)
            # This direction is computationally heavier for 1x2.
            # Alternative: X_{k+1} = 1.5 * X_k - 0.5 * X_k @ (X_k.T @ X_k)
            # Actually for vector v, v^T v is a scalar if column vector, but here row.

            XTX = X.T @ X # (2, 2)
            eye = np.eye(2)
            X = 0.5 * X @ (3 * eye - XTX)

        return X.flatten()

    def step(self, params, grads):
        self.m = self.momentum * self.m + grads

        if self.nesterov:
            v = self.momentum * self.m + grads
        else:
            v = self.m

        # Orthogonalize v using Newton-Schulz
        update = self.newton_schulz(v)

        # Scale by root-mean-square of params (often used in Muon implementations for consistency)
        # or just standard LR. Original Muon paper/impl often scales by max(1, rms(params))
        # Let's stick to simple LR for visualization

        return params - self.lr * update

def visualize():
    # Setup plot
    fig, ax = plt.subplots(figsize=(10, 8))

    # Generate meshgrid for contour
    x = np.linspace(-2, 2, 400)
    y = np.linspace(-1, 3, 400)
    X, Y = np.meshgrid(x, y)
    Z = rosenbrock(X, Y)

    # Plot contours (log scale for better visibility)
    ax.contour(X, Y, Z, levels=np.logspace(-1, 3, 30), cmap='gray_r', alpha=0.4)
    ax.plot(1, 1, 'r*', markersize=15, label='Global Min (1, 1)')

    # Starting point
    start_pos = np.array([-1.5, 2.5])

    # Run Optimizers
    optimizers = [
        ('SGD (Momentum)', SGD(lr=0.0005, momentum=0.9), 'blue'),
        ('AdamW', AdamW(lr=0.05, weight_decay=0.01), 'green'),
        ('Muon', Muon(lr=0.05, momentum=0.95), 'purple')
    ]

    steps = 200

    for name, opt, color in optimizers:
        path = [start_pos]
        params = start_pos.copy()

        for _ in range(steps):
            grads = grad_rosenbrock(params[0], params[1])
            params = opt.step(params, grads)
            path.append(params)

        path = np.array(path)
        ax.plot(path[:, 0], path[:, 1], '.-', color=color, label=name, alpha=0.8, linewidth=1.5, markersize=3)
        # Mark end
        ax.plot(path[-1, 0], path[-1, 1], 'o', color=color)

    ax.set_title("优化器寻优路径对比: SGD vs AdamW vs Muon\n(Loss: Rosenbrock Function)", fontsize=14)
    ax.set_xlabel("x")
    ax.set_ylabel("y")
    ax.legend()
    ax.grid(True, linestyle='--', alpha=0.3)

    plt.tight_layout()
    plt.savefig('optimizer_comparison.png', dpi=150)
    print("Optimization comparison plot saved to optimizer_comparison.png")

if __name__ == "__main__":
    visualize()

六、结语

世间安得双全法?优化器的选择本质上是在计算效率(FLOPs vs Memory)收敛质量之间做权衡。 在硬件资源有限的情况下,我们倾向于选择计算效率更高的优化器;而在硬件资源充足的情况下,我们则更倾向于选择收敛质量更高的优化器。 AdamW 凭借其稳健性,依然是当今 LLM 训练的“压舱石”。而 Muon 则像是一辆激进的赛车,通过更针对性设计的数学工具(牛顿迭代、正交化)挖掘硬件潜力,试图在训练速度上有所改进。

随着 GPU 架构向着“高算力、相对低带宽”演进,类似 Muon 这样计算密集型(Compute-Heavy)的优化器可能会越来越受到青睐。理解它们背后的数学直觉,能帮助更好地理解大模型。

预览时标签不可点

Close

更多

Name cleared

赞赏二维码微信扫一扫赞赏作者

Like the AuthorOther Amount

赞赏后展示我的头像

作品

暂无作品

Like the Author

Other Amount

¥

最低赞赏 ¥0

OK

Back

Other Amount

更多

赞赏金额

¥

最低赞赏 ¥0

1

2

3

4

5

6

7

8

9

0

.

大语言模型 · 目录

大语言模型

上一篇TransPaste :一款基于本地大语言模型的轻量级剪贴板翻译工具下一篇Ubuntu 手动安装 Ollama 0.17.1 来运行 qwen3.5:35b-a3b 的详细指南

Close

更多

搜索「」网络结果

Close

调整当前正文文字大小

更多

100%

分类: ML