AI/ML 详解:模型压缩

Day 31 · 2026-06-17 · 难度 ★★★☆☆
面向:有编程经验的非 AI 方向工程师
工程对应 → super-individual D12: Fine-tuning(LoRA/QLoRA 的工程实战与权衡)
本期主线

一个 70B 参数的模型,FP16 精度下光权重就要 140 GB 显存——单张消费级显卡装不下。模型压缩回答的是同一个问题的四个角度:能不能用更小的模型、更少的权重、更小的改动、更低的精度,把这堆参数塞进你负担得起的硬件。四种技术正交,可叠加:蒸馏(换小模型)、剪枝(删权重)、LoRA(只训增量)、量化(降精度)。本期讲「为什么这么做有效」的机制与数学。

知识蒸馏Knowledge Distillation

迁移teacher-student
一句话类比

蒸馏像资深工程师带新人 code review。差的师傅只说「这题选 A」(硬标签,hard label);好的师傅会说「A 八成对,B 还行有一成可能,C 基本不用考虑」(软标签,soft label)。后者传递的信息量大得多——新人不只学到答案,还学到了类别之间的相似度结构。蒸馏就是让小模型(student)去模仿大模型(teacher)输出的整个概率分布,而不只是最终答案。

它解决什么问题 + 工作机制

大模型准但贵,小模型快但笨。能不能让小模型「继承」大模型的判断力?关键洞察来自 Hinton 2015:大模型 softmax 输出里,那些非正确类的微小概率(猫=0.9、狗=0.08、汽车=0.0001)藏着大量「暗知识」——它告诉你「猫和狗很像,和汽车完全不像」。这种类间关系,硬标签(猫=1,其余=0)完全丢掉了。

机制核心是用温度(temperature,T)软化 softmax。普通 softmax 把最大值压得接近 1、其余接近 0;除以一个 T>1 再做 softmax,分布会变「平」,那些小概率被放大、暴露出来:

pi = exp(zi/T) / Σj exp(zj/T)

这里 zi 是第 i 类的原始打分(logit),T 是温度旋钮。T=1 是普通 softmax;T 越大分布越平滑,类间相似度越清晰。训练时让 student 在同样的高温下匹配 teacher 的软分布(用 KL 散度衡量两个分布的差距),通常再加一点真实硬标签做锚。下图直观感受软硬标签的信息差:

同一张「猫」图片的监督信号

硬标签:猫 1.0 |狗 0 |虎 0 |车 0 ← 类间关系全丢

软标签(T=4)
0.65
0.22 ← 猫狗相似
0.11 ← 都是猫科
0.02 ← 完全不像
↑ 「暗知识」= 大模型对世界的相似度地图
代码示例
import torch.nn.functional as F

def distill_loss(student_logits, teacher_logits, labels, T=4.0, alpha=0.7):
    # 1) 软目标:师生都用温度 T 软化,再比对分布(KL 散度)
    s_soft = F.log_softmax(student_logits / T, dim=-1)
    t_soft = F.softmax(teacher_logits / T, dim=-1)  # teacher 不回传梯度
    kd = F.kl_div(s_soft, t_soft, reduction="batchmean") * (T * T)
    # ↑ 乘 T²:温度软化会缩小梯度,乘回来保持量级

    # 2) 硬目标:和真实标签的普通交叉熵,做锚点防跑偏
    ce = F.cross_entropy(student_logits, labels)

    # 3) 加权组合:alpha 偏重模仿老师,(1-alpha) 偏重真值
    return alpha * kd + (1 - alpha) * ce
常见误区 + 实践场景
「student 越小越好,反正学老师」——错。student 容量有一个下限:太小则连软分布都拟合不了,蒸馏反而不如直接训练。经验上 student 通常是 teacher 的 1/2~1/10 规模(如 DistilBERT 是 BERT 的 ~60%),不是任意压。另一个坑:温度 T 太高会把分布软成接近均匀,淹没真正的信号。
📌 超级个体场景:你用 GPT-5 级大模型在一个窄任务(如把邮件分类成「待办/参考/忽略」)上跑出几千条带概率的输出,拿它当 teacher 蒸馏一个小模型部署在本地——日常分类零 API 成本、毫秒级、还离线可用。这就是「大模型当老师、小模型当员工」的个人自动化范式。
Takeaway + 思考题
💡 蒸馏迁移的不是答案,是大模型对世界的相似度结构——软标签里的「暗知识」才是真正值钱的部分。
🤔 如果软标签比硬标签信息量大,那「一个好老师」的本质是不是「愿意暴露自己不确定性的人」?这对你带团队、教孩子有什么启发?

剪枝Pruning

稀疏化删权重
一句话类比

剪枝就是删掉用不到的数据库索引,或者 dead code elimination。一个训练好的网络里,大量权重的绝对值接近 0——它们对最终输出几乎没贡献,就像一个从没被查询命中的索引、一段永远走不到的分支。把它们置零(甚至物理删除),模型变小变快,精度几乎不掉。问题只在于:怎么判断哪些权重是「死」的

它解决什么问题 + 工作机制

神经网络天然过参数化(over-parameterized)——参数远多于任务实际需要,这是训练能收敛的代价。训练完之后,很多参数就成了冗余。最朴素也最有效的判据是幅值剪枝(magnitude pruning):权重绝对值越小,删掉影响越小。

若 |wij| < 阈值 τ,则令 wij = 0

直觉:w 是连接强度,|w|≈0 意味着这条连接几乎不传递信号,剪掉等于删一根没用的线。但一刀剪太狠精度会崩,所以标准做法是迭代式「剪一点 → 微调恢复 → 再剪一点」(train-prune-finetune 循环),让网络逐步适应稀疏结构。

更深的发现是 Frankle & Carbin 2018 的彩票假说(Lottery Ticket Hypothesis):一个大的随机初始化网络里,本就藏着一个小的「中奖」子网络——单独把它拎出来、用原始初始化训练,能达到和完整大网络相当的精度。剪枝某种意义上是在「刮开彩票」找到那个子网络。两种剪枝粒度的工程含义截然不同:

  • 非结构化剪枝(unstructured)——任意单个权重置零,压缩率最高,但得到的是稀疏矩阵,普通 GPU 不一定能加速(需要专门的稀疏算子);
  • 结构化剪枝(structured)——整行/整列/整个注意力头删掉,矩阵变小是规整的,直接在标准硬件上变快,但同等精度下能删的更少。
代码示例
import torch.nn.utils.prune as prune
import torch.nn as nn

layer = nn.Linear(1024, 1024)

# 幅值剪枝:把这层 40% 绝对值最小的权重置零
prune.l1_unstructured(layer, name="weight", amount=0.4)
print((layer.weight == 0).float().mean())  # ≈ 0.40 稀疏度

# 关键:剪完要微调几个 epoch 让网络恢复(此处省略训练循环)
# ... train(model) ...  # 让存活的权重补偿被删掉的

# 满意后固化:移除 mask,让置零永久生效
prune.remove(layer, "weight")
常见误区 + 实践场景
「剪枝 50% 权重 = 模型快一倍」——通常不对。非结构化剪枝得到稀疏矩阵,但消费级 GPU 对稀疏运算没有原生加速,实际跑起来可能一点不快,只是省了存储。真正想要推理提速,往往要结构化剪枝或配合支持稀疏的硬件/内核。「压缩率」和「加速比」是两回事,别混淆。
📌 超级个体场景:把一个本地跑的开源小模型做结构化剪枝 + 微调,塞进你的笔记本或边缘设备(如家里的小服务器),做「永远在线、零网络依赖」的私人助理——剪枝换来的是能在你已有硬件上跑这件事本身,而不只是省钱。
Takeaway + 思考题
💡 网络过参数化是训练的必要代价,但不是部署的必要负担——剪枝是「训练用大、部署用小」的桥。
🤔 彩票假说说「大网络里藏着小赢家」。这和「人脑发育早期大量神经元连接、之后大规模修剪」惊人相似——冗余-然后-修剪,是不是智能系统的通用学习范式?

低秩适配 / 量化低秩适配LoRA / QLoRA

参数高效增量微调
一句话类比

全量微调像把整个代码仓库 fork 一份重写——70B 参数全部更新,每个任务存一份 140GB 副本,灾难。LoRA 像 git diff / patch 文件:原始权重冻结不动(base repo),只额外训练一个小小的增量补丁。每个任务只存这个补丁(几 MB~几十 MB),用时叠加到主干上。一个底座,无数个轻量 patch,按需切换。

它解决什么问题 + 工作机制

全量微调一个大模型,要为每个下游任务保存一整套权重,存储和切换成本爆炸。LoRA(Hu et al. 2021)的关键假设:微调时权重的变化量 ΔW 本质是「低秩」的——它没那么复杂,可以用两个瘦长矩阵的乘积来逼近。原本要更新一个 d×k 的大矩阵,现在拆成 B(d×r)× A(r×k),其中秩 r 极小(常取 8、16):

W' = W + ΔW ≈ W + B·A   (r ≪ min(d, k))

参数量从 d×k 降到 r×(d+k)。举例 d=k=4096、r=8:全量是 1600 万参数,LoRA 只有 6.5 万——缩小约 250 倍。冻结的 W 提供「通用能力」,小小的 BA 提供「任务特化」。推理时把 BA 合并进 W,零额外延迟。下图是维度直觉:

用两个瘦矩阵逼近一个大更新

全量 ΔW:d × k
(4096×4096)
16.7M 参数


LoRA 分解:
B
d×r
4096×8
×A   r×k   (8×4096) = 同样 d×k 形状
↑ 只训 B、A 共 65K 参数(~0.4%),W 冻结不动

QLoRA(Dettmers et al. 2023)更进一步:把冻结的底座量化到 4-bit 存显存,只有那个小 LoRA 适配器保持高精度训练。这样单张 48GB 显卡就能微调 65B 模型。它的三个关键发明:NF4(4-bit NormalFloat,针对正态分布权重信息论最优的数据类型)、双重量化(连量化常数本身也量化,再省一点)、分页优化器(用显存换内存应对峰值)。一句话:LoRA 省「要训的参数」,QLoRA 再省「冻结底座占的显存」。

代码示例
from transformers import AutoModelForCausalLM, BitsAndBytesConfig
from peft import LoraConfig, get_peft_model

# QLoRA:底座 4-bit 量化加载(NF4 + 双重量化)
bnb = BitsAndBytesConfig(load_in_4bit=True,
        bnb_4bit_quant_type="nf4", bnb_4bit_use_double_quant=True)
base = AutoModelForCausalLM.from_pretrained(
        "meta-llama/Llama-3-8b", quantization_config=bnb)

# 只在注意力的 q/v 投影上挂 LoRA 适配器,秩 r=8
cfg = LoraConfig(r=8, lora_alpha=16,
        target_modules=["q_proj", "v_proj"], lora_dropout=0.05)
model = get_peft_model(base, cfg)

model.print_trainable_parameters()
# → trainable: ~4M / 8B (≈0.05%),其余全部冻结
常见误区 + 实践场景
「LoRA 效果一定不如全量微调」——不绝对。在多数下游适配任务上 LoRA 接近甚至持平全量,但当任务需要模型学习大量全新知识(而非调整已有能力)时,低秩假设会成为瓶颈——ΔW 实际不低秩,r=8 装不下。判断法则:风格/格式/领域口吻适配 → LoRA 足够;灌入全新事实体系 → 可能需要更大 r 或全量。
📌 超级个体场景:用 QLoRA 在你自己的写作样本上微调一个本地模型,得到一个「以你的语气写作」的适配器(几十 MB)。再训一个「以你的语气写代码注释」的——同一个底座挂不同 patch,按场景热切换。这是把「个人风格」资产化成可复用、可版本管理的小文件。
Takeaway + 思考题
💡 LoRA 的本质洞察:适配 ≠ 重写。大部分「学一个新任务」其实是在巨大能力底座上打一个低秩补丁。
🤔 「能力是冻结的底座、任务是可插拔的补丁」——这个架构能不能反过来描述人的专业成长?你的通用判断力是 W,每份新工作是一个 LoRA?
 工程对应 → super-individual D12(Fine-tuning 实战与权衡)

量化Quantization

数值压缩降精度
一句话类比

量化就是换更小的数据类型存数——把 FP16(16 位浮点)压成 INT8 甚至 INT4,类似把 PNG 转成 JPEG、把 DOUBLE 字段改成 SMALLINT。用精度换空间和速度:模型权重不需要那么多有效数字,砍掉低位信息,体积直接减半、再减半,访存和算力也跟着降。代价是引入量化噪声,关键是控制它别毁掉模型。

它解决什么问题 + 工作机制

大模型推理的瓶颈常常不是算力,而是把几百 GB 权重从显存搬进计算单元的带宽。权重存得越小,搬得越快、装得越下。量化的数学核心是一个线性映射:把一段连续的浮点范围 [min, max],均匀映射到 2b 个整数格子上。

scale = (max − min) / (2b − 1)

xq = round(x / scale)   →存储→   x̂ = xq × scale

直觉:scale 是「每个整数格子代表多大的浮点跨度」,b 是位宽(INT8 是 8、INT4 是 4)。存的时候把浮点除以 scale 取整成小整数 xq;用的时候再乘回 scale 还原成近似浮点 x̂ 和原始 x 的差就是量化误差。位宽越低、格子越少、误差越大——这就是「精度换空间」的本质。下图是把连续值塞进 4 个格子(2-bit)的直观感受:

连续浮点 → 离散整数格子(2-bit=4 格)

浮点轴 ├──────┼──────┼──────┼──────┤
        min     ▼0.31→归到格子1
整数值   0    1    2    3
↑ 落在格子间的真实值被「四舍五入」吸附到最近格点 = 量化误差

两个实操关键。其一异常值(outlier):LLM 权重/激活里偶有极大值,会把 [min,max] 撑得很宽,导致大多数正常值挤在少数格子里、精度尽失。LLM.int8()、GPTQ 等方法的核心都在处理异常值。其二训练后量化 vs 量化感知

  • 训练后量化(PTQ,Post-Training Quantization)——模型训完直接量化,零额外训练。GPTQ(Frantar et al. 2022)用近似二阶信息逐层校准,把 175B 模型压到 3-4 bit 而精度几乎无损,是这条线的代表;
  • 量化感知训练(QAT,Quantization-Aware Training)——训练时就模拟量化误差,让模型「提前适应」。更准但要重训,成本高。
代码示例
import torch

def quantize_int8(w):
    # 对称量化:用绝对值最大值定 scale,零点对齐 0
    scale = w.abs().max() / 127.0          # INT8 范围 [-127,127]
    w_q = torch.round(w / scale).clamp(-127, 127).to(torch.int8)
    return w_q, scale                       # 存 int8 权重 + 一个 fp scale

def dequantize(w_q, scale):
    return w_q.to(torch.float32) * scale     # 用时还原近似浮点

w = torch.randn(4096, 4096)              # fp32 权重:64 MB
w_q, s = quantize_int8(w)               # int8:16 MB,缩到 1/4
err = (w - dequantize(w_q, s)).abs().mean()
print(f"平均量化误差 {err:.5f}")      # 噪声很小,模型基本无感
常见误区 + 实践场景
「量化到 4-bit 体积减半、效果也减半」——错。精度损失远非线性。INT8 几乎无损是行业常识;4-bit 配合 GPTQ/NF4 这类校准方法,多数任务仍接近原模型;真正的悬崖通常在 3-bit 以下才陡降。「位宽减半」和「质量减半」完全不成比例——这正是量化如此流行的原因。
📌 超级个体场景:想在自己的笔记本上跑本地 LLM,直接选社区放出的 4-bit 量化版(GGUF/GPTQ 格式)。70B 模型 FP16 要 140GB、根本跑不动;4-bit 量化后约 35-40GB,配合内存就能在个人设备上跑起来——量化是「个人拥有强模型」从不可能到可能的那一步。
Takeaway + 思考题
💡 量化揭示一个反直觉事实:大模型权重里的有效信息量,远低于它占用的比特数——多数精度是冗余的。
🤔 如果 16-bit 权重压到 4-bit 几乎不掉点,是不是说模型的「知识」本质上很稀疏/低信息密度?这和「人脑用模糊、近似的方式存记忆」是同一个道理吗?

深入资源Further Reading

深入思考Deep Questions

1. 蒸馏、剪枝、LoRA、量化四种技术为什么可以「叠加」?它们各自压缩的是什么维度?
因为它们正交——作用在不同的轴上。蒸馏换的是「模型本身」(大架构→小架构,减层数/宽度);剪枝减的是「权重的数量」(删掉冗余连接,让矩阵稀疏或变小);LoRA 减的是「要训练/存储的参数」(冻结底座,只存低秩增量,是训练侧而非推理侧的压缩);量化减的是「每个权重占的比特数」(数值精度,不动数量和结构)。所以现实中常见组合拳:先蒸馏出小模型 → 再剪枝去冗余 → 用 QLoRA 在小模型上做任务适配 → 最后量化部署。每一步压不同的维度,乘起来才有几十倍的总压缩比。理解这点的价值在于:当你的瓶颈是显存,优先量化+剪枝;当瓶颈是「每个任务存一份太贵」,用 LoRA;当瓶颈是延迟且能接受换架构,蒸馏。对症下药,而不是无脑全上。
2. 量化、剪枝、蒸馏都「丢信息但精度不怎么掉」,这背后是不是同一个深层原因?
是——根子都在神经网络的过参数化与冗余。深度网络的参数远多于拟合任务所需,这是为了让高维优化能顺利收敛(损失景观更平滑、更容易找到好极小值)。但训练完成后,这份「为了好训而准备的冗余」就成了部署负担。三种技术从不同角度榨干同一份冗余:剪枝说「很多权重本就≈0,删」;量化说「很多权重的高精度位是噪声,砍」;蒸馏说「一个小模型容量其实够装下这个任务的真实复杂度,只是直接训不好——让大模型当老师引导它」。彩票假说把这点说得最透:有用的「子网络」一直在那,大模型只是更容易训练的脚手架。这引出一个深刻问题——如果部署只需要这么小的有效容量,那大模型训练期的巨大规模,买到的究竟是「能力」还是「可优化性」?目前主流答案偏向后者:规模主要是为了训得动,不是推理时真的都用得上。
3. QLoRA 把底座量化到 4-bit 还能正常微调,为什么 4-bit 的「噪声底座」不会毁掉训练?
几个机制叠加。其一,梯度只流向高精度的 LoRA 适配器——4-bit 底座是冻结的,不接收梯度更新,所以量化误差不会在反向传播中被放大或累积,它只是给前向传播提供一个「足够好」的基准。其二,NF4 数据类型是针对正态分布权重做信息论优化的——神经网络权重近似正态,NF4 把有限的格子分配得更密集在高概率区域,同样 4-bit 下误差远小于均匀量化。其三,双重量化进一步压缩了量化常数本身的开销而几乎不增误差。其四也是最关键的:高精度的 LoRA 适配器有能力「补偿」底座的量化噪声——训练过程中,BA 会自动学到一个增量,部分抵消量化带来的偏差。所以 QLoRA 不是「在坏底座上硬训」,而是「冻结一个够用的近似底座 + 一个精确的可学补丁去校正它」。这也解释了为什么直接把模型量化到 4-bit 做推理有时不如 QLoRA 微调后的效果——后者多了一个适配器在补偿。
4. 「软标签的暗知识」和「低秩的 ΔW」表面无关,但它们是不是都在说同一件事——任务的「真实复杂度」很低?
是个很美的连接。蒸馏的软标签揭示:分类任务的真实信号不是「one-hot 的硬答案」,而是一个低维的相似度结构(猫≈狗≫车)——这个结构远比「N 个独立类别」简单,所以小模型能学会。LoRA 的低秩 ΔW 揭示:把一个通用模型适配到具体任务,所需的变化量本质是低维的——你不需要重写整个 16M 参数矩阵,一个秩 8 的补丁就够。两者都在用不同语言说同一个先验:真实世界任务的「内在维度」(intrinsic dimension)远低于模型的「表观维度」。这有专门研究(如 Aghajanyan 等关于微调内在维度的工作)支持。对复杂性科学视角,它呼应一个深层主题:高维系统的有效自由度往往集中在一个低维流形上——无论是物理系统的序参量、还是神经网络的任务表示。压缩之所以可能,是因为「看起来很大」的东西,信息上本就不大。
5. 模型压缩让「在个人设备上拥有强模型」成为可能。这对「AI 超级个体」的权力结构意味着什么?
这是技术问题背后的政治经济学。未压缩的前沿模型只有大公司数据中心跑得起,能力中心化在少数 API 提供方手里——你租用智能,但不拥有它,随时可能被断供、涨价或审查。压缩技术(尤其量化 + LoRA)把天平往回拨:4-bit 量化让 70B 模型跑进消费级硬件,LoRA 让你用几十 MB 的补丁把通用模型私人化,蒸馏让你把大模型的能力「下载」进一个自有小模型。这三者合起来,指向一个「自主 AI」(sovereign AI)的可能——智能在你的设备上、用你的数据运行,离线、私密、不可被远程关停。对追求「超级个体」的人,这不只是省钱,而是能力主权:你的核心智能基础设施究竟是租来的还是自有的,决定了你的自主性上限。代价是你得自己承担运维与对齐的责任——这正是「超级个体」与「平台用户」的分野。压缩技术的成熟,是这条路从理想变成可行的关键一环。