目录

一、模型压缩概述

1.1 为什么需要模型压缩?

1.2 模型压缩技术全景

1.3 压缩的核心目标

二、模型量化(Model Quantization)

2.1 量化的核心概念

2.1.1 基本原理

2.1.2 对称量化 vs 非对称量化

2.1.3 常见量化精度

2.2 三种主流量化方式

2.2.1 训练后量化(Post-Training Quantization, PTQ)

2.2.2 量化感知训练(Quantization-Aware Training, QAT)

2.2.3 动态量化(Dynamic Quantization)

三种量化方式对比

2.3 量化代码实战

2.3.1 PTQ训练后量化(PyTorch实现)

2.3.2 QAT量化感知训练(PyTorch实现)

2.3.3 动态量化(PyTorch实现)

2.3.4 使用GPTQ进行LLM INT4量化(Hugging Face生态)

2.3.5 使用bitsandbytes进行LLM量化(QLoRA常用方案)

2.4 量化精度与性能对比

三、模型蒸馏(Knowledge Distillation)

3.1 蒸馏的核心概念

3.1.1 为什么蒸馏有效?

3.1.2 蒸馏的基本架构

3.2 硬标签蒸馏(Hard Label Distillation)

3.2.1 定义

3.2.2 特点

3.2.3 适用场景

3.3 软标签蒸馏(Soft Label Distillation)

3.3.1 定义

3.4 温度参数T的数学原理

3.4.1 Softmax with Temperature

3.4.2 温度的效果示意

3.5 蒸馏代码实战

3.5.1 硬标签蒸馏

3.5.2 软标签蒸馏(经典实现)

3.5.3 特征蒸馏(Feature-based Distillation)

3.5.4 使用Hugging Face进行LLM蒸馏

3.6 蒸馏策略对比

四、模型剪枝(Model Pruning)

4.1 剪枝的核心概念

4.1.1 核心假设

4.1.2 剪枝的分类

4.1.3 重要性评估标准

4.2 非结构化剪枝(Unstructured Pruning)

4.2.1 定义

4.2.2 特点

4.3 结构化剪枝(Structured Pruning)

4.3.1 定义

4.3.2 特点

4.4 剪枝代码实战

4.4.1 非结构化剪枝(PyTorch内置API)

4.4.2 结构化剪枝

4.4.3 迭代剪枝(Iterative Magnitude Pruning)

4.5 剪枝与重训练策略

五、LoRA低秩矩阵高效微调

5.1 LoRA的核心概念

5.1.1 核心思想

5.1.2 为什么LoRA有效?

5.2 LoRA的数学原理

5.2.1 前向传播

5.2.2 初始化策略

5.2.3 缩放因子α

5.2.4 应用位置

5.3 LoRA vs 全量微调

5.3.1 全面对比

5.3.2 显存对比示例(LLaMA-7B)

5.3.3 什么时候用全量微调?

5.3.4 什么时候用LoRA?

5.4 LoRA代码实战

5.4.1 从零实现LoRA层

5.4.2 使用Hugging Face PEFT库(推荐方案)

5.4.3 LoRA微调LLaMA大语言模型(QLoRA)

5.4.4 LoRA权重合并与多任务热插拔

5.5 LoRA变体与进阶

5.5.1 LoRA变体总结

5.5.2 DoRA简要介绍

5.5.3 LoRA超参数调优指南

六、四大压缩技术综合对比

6.1 技术维度对比

6.2 组合使用策略

七、工程实践建议与工具链

7.1 工具链推荐

7.2 实践建议

7.3 不同场景的推荐方案

八、总结与展望

8.1 核心要点回顾

8.2 前沿趋势

8.3 学习路径建议



一、模型压缩概述

1.1 为什么需要模型压缩?

随着深度学习的发展,模型参数量从百万级飙升到千亿级。GPT-3拥有1750亿参数,LLaMA-70B拥有700亿参数。这些大模型在精度上表现优异,但在实际部署中面临严峻挑战:

挑战维度 具体问题
存储开销 70B模型以FP16存储需要约140GB显存
推理延迟 大模型单次推理耗时长,无法满足实时场景
硬件成本 需要多张高端GPU(如A100/H100)才能运行
能耗问题 大模型推理功耗高,边缘设备无法承受
部署限制 移动端、嵌入式设备内存和算力极其有限

1.2 模型压缩技术全景

模型压缩
├── 量化(Quantization)        → 降低数值精度(FP32→INT8/INT4)
├── 蒸馏(Distillation)        → 大模型指导小模型学习
├── 剪枝(Pruning)             → 移除冗余参数/结构
├── 低秩分解(Low-Rank)         → LoRA等参数高效微调方法
└── 其他技术
    ├── 权重共享(Weight Sharing)
    ├── 神经架构搜索(NAS)
    └── 混合专家模型(MoE)

1.3 压缩的核心目标

模型压缩的本质是在 精度(Accuracy)效率(Efficiency) 之间寻找最优平衡点:

目标函数:min Compression_Ratio  s.t.  Accuracy_Drop ≤ ε

二、模型量化(Model Quantization)

2.1 量化的核心概念

量化是将模型权重和/或激活值从高精度浮点数(如FP32)映射到低精度表示(如INT8、INT4)的过程。

2.1.1 基本原理

量化的核心是一个线性映射函数:

量化(Quantize):  x_q = round(x / S) + Z
反量化(Dequantize):x ≈ S × (x_q - Z)

其中:

  • S(Scale)= 缩放因子
  • Z(Zero Point)= 零点偏移
  • x_q = 量化后的整数值
2.1.2 对称量化 vs 非对称量化

对称量化(Symmetric Quantization)

S = max(|x|) / (2^(b-1) - 1)
Z = 0
x_q = round(x / S)

特点:零点固定为0,计算更简单,适用于权重分布对称的场景。

非对称量化(Asymmetric Quantization)

S = (max(x) - min(x)) / (2^b - 1)
Z = round(-min(x) / S)
x_q = round(x / S) + Z

特点:能更好地适应非对称分布(如ReLU后的激活值),精度更高。

2.1.3 常见量化精度
量化位宽 表示范围 模型大小(相对FP32) 典型应用场景
FP32 ±3.4×10³⁸ 1×(基准) 训练
FP16/BF16 ±6.5×10⁴ 0.5× 训练/推理
INT8 -128 ~ 127 0.25× 服务端推理
INT4 -8 ~ 7 0.125× 边缘端部署
Binary {-1, +1} 0.03125× 极端压缩研究

2.2 三种主流量化方式

2.2.1 训练后量化(Post-Training Quantization, PTQ)

定义:在模型训练完成后,直接对已训练好的模型权重进行量化,无需重新训练。

工作流程

训练好的FP32模型 → 收集校准数据 → 统计权重/激活值分布 → 确定量化参数(S,Z) → 量化模型 → 验证精度

优点

  • 实现简单,无需训练代码
  • 量化速度快(通常几分钟到几小时)
  • 适合快速评估量化效果

缺点

  • 精度损失相对较大(尤其在低比特量化时)
  • 对离群值(outlier)敏感
  • 需要校准数据集

适用场景:模型精度对量化不敏感的任务,或快速部署需求。

2.2.2 量化感知训练(Quantization-Aware Training, QAT)

定义:在训练过程中模拟量化操作,让模型在训练阶段就适应量化带来的精度损失。

核心思想:在前向传播中插入伪量化节点(Fake Quantization),模拟量化误差;反向传播时使用直通估计器(Straight-Through Estimator, STE) 来近似量化函数的梯度。

STE梯度近似

# 量化函数不可导,STE将其梯度近似为1
# forward:  x_q = quantize(x)
# backward: grad_x ≈ grad_x_q  (直接传递梯度)

工作流程

FP32模型 → 插入伪量化节点 → 正常训练/微调 → 前向传播模拟量化 → 反向传播用STE → 得到量化友好模型 → 真正量化

优点

  • 精度损失最小
  • 模型学习适应量化误差
  • 适合低比特(INT4/INT2)量化

缺点

  • 需要训练数据和训练时间
  • 实现复杂度较高
  • 需要修改训练流程

适用场景:对精度要求极高且可接受训练开销的场景。

2.2.3 动态量化(Dynamic Quantization)

定义:在推理时动态计算激活值的量化参数,权重在加载时静态量化。

工作流程

权重:加载时一次性量化(静态)
激活值:每次推理时根据实际输入动态计算S和Z

优点

  • 无需校准数据
  • 实现最简单
  • 激活值量化更精确(每次都重新计算范围)

缺点

  • 推理时有额外计算开销(动态计算量化参数)
  • 不如静态量化的加速效果好

适用场景:没有校准数据集,或输入分布变化大的场景(如NLP中的变长序列)。

三种量化方式对比
特性 PTQ QAT 动态量化
是否需要训练
是否需要校准数据
精度损失 中等 最小 较小
实现复杂度 最低
量化速度 最快
适用比特数 INT8+ INT4+ INT8+
推理加速效果 一般

2.3 量化代码实战

2.3.1 PTQ训练后量化(PyTorch实现)
import torch
import torch.quantization as quant
import torch.nn as nn
from torchvision import models, transforms
from torch.utils.data import DataLoader
import copy

# ========================
# 1. 准备模型和数据
# ========================
# 加载预训练的MobileNetV2
model_fp32 = models.mobilenet_v2(pretrained=True)
model_fp32.eval()

# 准备校准数据集
transform = transforms.Compose([
    transforms.Resize(256),
    transforms.CenterCrop(224),
    transforms.ToTensor(),
    transforms.Normalize(mean=[0.485, 0.456, 0.406],
                         std=[0.229, 0.224, 0.225]),
])

# 这里用随机数据演示,实际应用中使用真实校准数据
calibration_loader = DataLoader(
    dataset=torch.utils.data.TensorDataset(
        torch.randn(100, 3, 224, 224),
        torch.randint(0, 1000, (100,))
    ),
    batch_size=16
)

# ========================
# 2. 配置量化方案
# ========================
# 方法一:使用torch.quantization的内置API
model_to_quantize = copy.deepcopy(model_fp32)

# 指定量化配置
model_to_quantize.qconfig = torch.quantization.get_default_qconfig('x86')
# 或使用fbgemm后端(x86服务器) / qnnpack后端(ARM移动端)

# 准备模型:插入Observer来统计激活值分布
model_prepared = torch.quantization.prepare(model_to_quantize)

# ========================
# 3. 校准(Calibration)
# ========================
print("开始校准...")
with torch.no_grad():
    for batch_idx, (images, _) in enumerate(calibration_loader):
        model_prepared(images)
        if batch_idx >= 10:  # 通常100-500个batch足够
            break
print("校准完成!")

# ========================
# 4. 转换为量化模型
# ========================
model_int8 = torch.quantization.convert(model_prepared)
model_int8.eval()

# ========================
# 5. 模型大小对比
# ========================
def get_model_size(model, name="Model"):
    torch.save(model.state_dict(), "temp.p")
    import os
    size_mb = os.path.getsize("temp.p") / 1e6
    os.remove("temp.p")
    print(f"{name} 大小: {size_mb:.2f} MB")
    return size_mb

size_fp32 = get_model_size(model_fp32, "FP32模型")
size_int8 = get_model_size(model_int8, "INT8模型")
print(f"压缩比: {size_fp32 / size_int8:.2f}x")

# ========================
# 6. 精度验证
# ========================
# 用测试数据对比FP32和INT8的输出差异
test_input = torch.randn(1, 3, 224, 224)
with torch.no_grad():
    out_fp32 = model_fp32(test_input)
    out_int8 = model_int8(test_input)

cosine_sim = torch.nn.functional.cosine_similarity(
    out_fp32.flatten(), out_int8.flatten(), dim=0
)
print(f"输出余弦相似度: {cosine_sim:.4f}")
2.3.2 QAT量化感知训练(PyTorch实现)
import torch
import torch.nn as nn
import torch.optim as optim
import torch.quantization as quant
import copy

# ========================
# 1. 定义一个简单的CNN模型
# ========================
class SimpleCNN(nn.Module):
    def __init__(self, num_classes=10):
        super(SimpleCNN, self).__init__()
        self.features = nn.Sequential(
            nn.Conv2d(3, 32, 3, padding=1),
            nn.BatchNorm2d(32),  # QAT兼容的BN
            nn.ReLU(inplace=True),
            nn.MaxPool2d(2),
            nn.Conv2d(32, 64, 3, padding=1),
            nn.BatchNorm2d(64),
            nn.ReLU(inplace=True),
            nn.MaxPool2d(2),
            nn.Conv2d(64, 128, 3, padding=1),
            nn.BatchNorm2d(128),
            nn.ReLU(inplace=True),
            nn.AdaptiveAvgPool2d(1),
        )
        self.classifier = nn.Linear(128, num_classes)

    def forward(self, x):
        x = self.features(x)
        x = x.view(x.size(0), -1)
        x = self.classifier(x)
        return x

# ========================
# 2. 先正常预训练
# ========================
model = SimpleCNN(num_classes=10)
# ... 假设已经预训练完成 ...

# ========================
# 3. 配置QAT
# ========================
model.qconfig = torch.quantization.get_default_qat_qconfig('x86')

# 插入伪量化节点
model_prepared = torch.quantization.prepare_qat(model.train())

# ========================
# 4. QAT微调训练
# ========================
optimizer = optim.SGD(model_prepared.parameters(), lr=0.001, momentum=0.9)
criterion = nn.CrossEntropyLoss()

# 模拟训练数据
train_loader = DataLoader(
    dataset=torch.utils.data.TensorDataset(
        torch.randn(200, 3, 32, 32),
        torch.randint(0, 10, (200,))
    ),
    batch_size=32
)

num_epochs = 5
for epoch in range(num_epochs):
    model_prepared.train()
    total_loss = 0
    for images, labels in train_loader:
        optimizer.zero_grad()
        outputs = model_prepared(images)
        loss = criterion(outputs, labels)
        loss.backward()
        optimizer.step()
        total_loss += loss.item()

    avg_loss = total_loss / len(train_loader)
    print(f"Epoch [{epoch+1}/{num_epochs}], Loss: {avg_loss:.4f}")

# ========================
# 5. 转换为真正的量化模型
# ========================
model_prepared.eval()
model_int8 = torch.quantization.convert(model_prepared)

# 验证量化后模型的输出
test_input = torch.randn(1, 3, 32, 32)
with torch.no_grad():
    output = model_int8(test_input)
    print(f"量化模型输出shape: {output.shape}")
    print(f"预测类别: {output.argmax(dim=1).item()}")

# 检查量化后的模型结构
print("\n量化模型结构:")
print(model_int8)
2.3.3 动态量化(PyTorch实现)
import torch
import torch.nn as nn
import torch.quantization as quant

# ========================
# 1. 定义LSTM模型(动态量化特别适合RNN)
# ========================
class LSTMModel(nn.Module):
    def __init__(self, vocab_size=10000, embed_dim=256,
                 hidden_dim=512, num_layers=2, num_classes=5):
        super(LSTMModel, self).__init__()
        self.embedding = nn.Embedding(vocab_size, embed_dim)
        self.lstm = nn.LSTM(embed_dim, hidden_dim,
                            num_layers=num_layers,
                            batch_first=True,
                            bidirectional=True)
        self.fc = nn.Linear(hidden_dim * 2, num_classes)

    def forward(self, x):
        embedded = self.embedding(x)
        lstm_out, (h_n, c_n) = self.lstm(embedded)
        # 取最后一个时间步
        output = self.fc(lstm_out[:, -1, :])
        return output

# ========================
# 2. 加载预训练模型
# ========================
model_fp32 = LSTMModel()
model_fp32.eval()

# ========================
# 3. 动态量化 —— 一行代码搞定!
# ========================
model_dynamic = torch.quantization.quantize_dynamic(
    model_fp32,
    {nn.LSTM, nn.Linear},  # 指定要量化的层类型
    dtype=torch.qint8       # 量化到INT8
)

# ========================
# 4. 对比
# ========================
def count_parameters(model):
    return sum(p.numel() for p in model.parameters())

def get_model_size(model):
    torch.save(model.state_dict(), "temp.p")
    import os
    size = os.path.getsize("temp.p") / 1e6
    os.remove("temp.p")
    return size

print(f"FP32 参数量: {count_parameters(model_fp32):,}")
print(f"FP32 模型大小: {get_model_size(model_fp32):.2f} MB")
print(f"动态量化模型大小: {get_model_size(model_dynamic):.2f} MB")
print(f"压缩比: {get_model_size(model_fp32) / get_model_size(model_dynamic):.2f}x")

# ========================
# 5. 推理测试
# ========================
test_input = torch.randint(0, 10000, (1, 50))  # batch=1, seq_len=50
with torch.no_grad():
    out_fp32 = model_fp32(test_input)
    out_dynamic = model_dynamic(test_input)

print(f"FP32输出: {out_fp32[0][:5]}")
print(f"动态量化输出: {out_dynamic[0][:5]}")
2.3.4 使用GPTQ进行LLM INT4量化(Hugging Face生态)
# ========================
# 使用AutoGPTQ对大语言模型进行INT4量化
# ========================
# pip install auto-gptq transformers accelerate

from transformers import AutoModelForCausalLM, AutoTokenizer
from auto_gptq import AutoGPTQForCausalLM, BaseQuantizeConfig

# ========================
# 1. 配置量化参数
# ========================
quantize_config = BaseQuantizeConfig(
    bits=4,              # 量化位数:4bit
    group_size=128,      # 分组大小
    damp_percent=0.01,   # Hessian矩阵正则化
    desc_act=True,       # 按激活值重要性排序(更精确)
    sym=False,           # 非对称量化
)

# ========================
# 2. 加载模型和分词器
# ========================
model_id = "facebook/opt-1.3b"
tokenizer = AutoTokenizer.from_pretrained(model_id)
model = AutoGPTQForCausalLM.from_pretrained(
    model_id,
    quantize_config,
    device_map="auto"
)

# ========================
# 3. 准备校准数据
# ========================
# 使用WikiText或其他校准数据
calibration_data = [
    "The quick brown fox jumps over the lazy dog.",
    "Machine learning is a subset of artificial intelligence.",
    "Deep neural networks have revolutionized computer vision.",
    # ... 实际使用时需要几百到几千条样本
]

# Tokenize校准数据
calibration_dataset = [
    tokenizer(text, return_tensors="pt", max_length=512, truncation=True)
    for text in calibration_data
]

# ========================
# 4. 执行量化
# ========================
print("开始GPTQ量化...")
model.quantize(calibration_dataset)
print("量化完成!")

# ========================
# 5. 保存量化模型
# ========================
model.save_quantized("./opt-1.3b-gptq-4bit")
tokenizer.save_pretrained("./opt-1.3b-gptq-4bit")

# ========================
# 6. 加载量化模型推理
# ========================
quantized_model = AutoGPTQForCausalLM.from_quantized(
    "./opt-1.3b-gptq-4bit",
    device="cuda:0"
)

input_text = "The future of artificial intelligence is"
inputs = tokenizer(input_text, return_tensors="pt").to("cuda:0")
outputs = quantized_model.generate(**inputs, max_new_tokens=50)
print(tokenizer.decode(outputs[0], skip_special_tokens=True))
2.3.5 使用bitsandbytes进行LLM量化(QLoRA常用方案)
# ========================
# 使用bitsandbytes进行4-bit/8-bit量化加载
# ========================
# pip install bitsandbytes accelerate transformers

import torch
from transformers import AutoModelForCausalLM, AutoTokenizer, BitsAndBytesConfig

# ========================
# 1. 8-bit量化加载
# ========================
model_8bit = AutoModelForCausalLM.from_pretrained(
    "meta-llama/Llama-2-7b-hf",
    load_in_8bit=True,       # 启用8-bit量化
    device_map="auto",
    torch_dtype=torch.float16,
)

# ========================
# 2. 4-bit量化加载(NF4格式,QLoRA标配)
# ========================
bnb_config_4bit = BitsAndBytesConfig(
    load_in_4bit=True,                    # 启用4-bit量化
    bnb_4bit_quant_type="nf4",           # NormalFloat4量化
    bnb_4bit_compute_dtype=torch.bfloat16, # 计算时的数据类型
    bnb_4bit_use_double_quant=True,       # 二次量化,进一步压缩
)

model_4bit = AutoModelForCausalLM.from_pretrained(
    "meta-llama/Llama-2-7b-hf",
    quantization_config=bnb_config_4bit,
    device_map="auto",
)

tokenizer = AutoTokenizer.from_pretrained("meta-llama/Llama-2-7b-hf")

# ========================
# 3. 对比显存占用
# ========================
def print_gpu_memory(model, name):
    total_params = sum(p.numel() for p in model.parameters())
    print(f"\n{name}:")
    print(f"  参数量: {total_params / 1e9:.2f}B")
    # 估算显存
    mem_bytes = sum(
        p.numel() * p.element_size() for p in model.parameters()
    )
    print(f"  模型显存: {mem_bytes / 1e9:.2f} GB")

# ========================
# 4. 推理测试
# ========================
prompt = "Explain quantum computing in simple terms:"
inputs = tokenizer(prompt, return_tensors="pt").to("cuda")

with torch.no_grad():
    outputs = model_4bit.generate(
        **inputs,
        max_new_tokens=200,
        temperature=0.7,
        top_p=0.9,
    )
print(tokenizer.decode(outputs[0], skip_special_tokens=True))

2.4 量化精度与性能对比

┌─────────────┬──────────┬──────────┬──────────────┬──────────────┐
│   量化方式   │  模型大小  │  推理速度  │   精度损失    │   适用场景    │
├─────────────┼──────────┼──────────┼──────────────┼──────────────┤
│   FP32      │  1.0×    │  1.0×    │   基准        │   训练       │
│   FP16      │  0.5×    │  1.5~2×  │   <0.1%      │   训练/推理   │
│   INT8 PTQ  │  0.25×   │  2~4×    │   0.5~2%     │   服务端推理   │
│   INT8 QAT  │  0.25×   │  2~4×    │   <0.5%      │   高精度推理   │
│   INT4 GPTQ │  0.125×  │  3~5×    │   1~3%       │   LLM部署    │
│   Binary    │  0.03×   │  10~20×  │   5~15%      │   研究/极端场景│
└─────────────┴──────────┴──────────┴──────────────┴──────────────┘

三、模型蒸馏(Knowledge Distillation)

3.1 蒸馏的核心概念

知识蒸馏(Knowledge Distillation) 由Hinton等人在2015年提出,核心思想是用一个大模型(Teacher模型) 来指导一个小模型(Student模型) 学习,让小模型尽可能逼近大模型的表现。

3.1.1 为什么蒸馏有效?

大模型(Teacher)学到了丰富的"暗知识(Dark Knowledge)"——即类别之间的相似性关系。例如在图像分类中:

Teacher对一张猫的图片的输出概率:
  猫: 0.85, 豹: 0.10, 狗: 0.04, 鱼: 0.01

硬标签(Ground Truth):
  猫: 1.0, 豹: 0.0, 狗: 0.0, 鱼: 0.0

Teacher的概率分布告诉我们:猫和豹很像,猫和狗也有一些相似性,猫和鱼差异很大。这些关系信息在硬标签中完全丢失了。

3.1.2 蒸馏的基本架构
                  ┌──────────────────┐
                  │   Teacher模型     │
                  │  (大模型,参数冻结) │
                  └────────┬─────────┘
                           │
                     软标签/硬标签
                           │
                  ┌────────▼─────────┐
    输入数据 ────→│   Student模型     │──→ 最终部署
                  │  (小模型,参数训练) │
                  └──────────────────┘

3.2 硬标签蒸馏(Hard Label Distillation)

3.2.1 定义

硬标签蒸馏是指Student模型学习Teacher模型的最终预测类别(argmax),即学习Teacher的"硬决策"。

损失函数

L_hard = L_CE(y_student, y_teacher_hard)

其中 y_teacher_hard = argmax(p_teacher)
3.2.2 特点
优点 缺点
实现简单 丢失了Teacher的概率分布信息
不需要温度参数 无法传递类别间的关系知识
训练稳定 蒸馏效果不如软标签
3.2.3 适用场景
  • Teacher模型的预测非常置信(概率接近one-hot)
  • 分类任务中类别数较少
  • 需要快速实现基线方案

3.3 软标签蒸馏(Soft Label Distillation)

3.3.1 定义

软标签蒸馏是蒸馏的核心方法,Student模型同时学习:

  1. 1.Teacher的软标签(经过温度缩放的概率分布)
  2. 2.Ground Truth硬标签

损失函数

L_total = α × L_soft + (1 - α) × L_hard

L_soft = KL_Divergence(softmax(z_s / T), softmax(z_t / T)) × T²
L_hard = CrossEntropy(y_student, y_true)

其中:

  • z_s = Student的logits
  • z_t = Teacher的logits
  • T = 温度参数(Temperature)
  • α = 软标签损失的权重系数
  • = 缩放因子,补偿温度对梯度的影响

3.4 温度参数T的数学原理

3.4.1 Softmax with Temperature

标准Softmax:

p_i = exp(z_i) / Σ_j exp(z_j)

带温度的Softmax:

p_i = exp(z_i / T) / Σ_j exp(z_j / T) 
  • T = 1:标准Softmax,概率分布尖锐
  • T > 1:概率分布变平滑,暴露更多类别间关系
  • T → ∞:均匀分布,所有类别概率相等
  • T → 0:退化为argmax,硬标签
3.4.2 温度的效果示意
假设Teacher的logits = [5.0, 3.0, 1.0, 0.1]

T=1:   [0.843, 0.114, 0.015, 0.006]  → 猫很确定是第一个
T=3:   [0.545, 0.285, 0.115, 0.055]  → 显示出第一和第二的关联
T=5:   [0.443, 0.304, 0.160, 0.093]  → 更平滑,关系更清晰
T=10:  [0.371, 0.302, 0.200, 0.127]  → 非常平滑

实践建议

  • 通常T取 2~20 之间
  • 分类类别数多时用较大的T
  • T过大会引入过多噪声
  • 可以通过验证集调优T

3.5 蒸馏代码实战

3.5.1 硬标签蒸馏
import torch
import torch.nn as nn
import torch.optim as optim
import torch.nn.functional as F
from torch.utils.data import DataLoader, TensorDataset

# ========================
# 1. 定义Teacher和Student模型
# ========================
class TeacherModel(nn.Module):
    """Teacher: 较大的模型"""
    def __init__(self, input_dim=784, num_classes=10):
        super().__init__()
        self.net = nn.Sequential(
            nn.Linear(input_dim, 1024),
            nn.ReLU(),
            nn.Dropout(0.3),
            nn.Linear(1024, 512),
            nn.ReLU(),
            nn.Dropout(0.3),
            nn.Linear(512, 256),
            nn.ReLU(),
            nn.Linear(256, num_classes),
        )

    def forward(self, x):
        return self.net(x)

class StudentModel(nn.Module):
    """Student: 较小的模型"""
    def __init__(self, input_dim=784, num_classes=10):
        super().__init__()
        self.net = nn.Sequential(
            nn.Linear(input_dim, 128),
            nn.ReLU(),
            nn.Linear(128, 64),
            nn.ReLU(),
            nn.Linear(64, num_classes),
        )

    def forward(self, x):
        return self.net(x)

# ========================
# 2. 硬标签蒸馏训练
# ========================
def train_hard_label_distillation(teacher, student, train_loader,
                                   num_epochs=10, lr=0.001):
    teacher.eval()  # Teacher不训练
    student.train()
    optimizer = optim.Adam(student.parameters(), lr=lr)
    criterion = nn.CrossEntropyLoss()

    for epoch in range(num_epochs):
        total_loss = 0
        correct = 0
        total = 0

        for inputs, labels in train_loader:
            optimizer.zero_grad()

            # Teacher的预测(硬标签)
            with torch.no_grad():
                teacher_logits = teacher(inputs)
                teacher_hard_labels = teacher_logits.argmax(dim=1)

            # Student的预测
            student_logits = student(inputs)

            # 硬标签蒸馏损失:Student学习Teacher的预测类别
            loss = criterion(student_logits, teacher_hard_labels)
            loss.backward()
            optimizer.step()

            total_loss += loss.item()
            _, predicted = student_logits.max(1)
            total += labels.size(0)
            correct += predicted.eq(labels).sum().item()

        acc = 100. * correct / total
        avg_loss = total_loss / len(train_loader)
        print(f"Epoch [{epoch+1}/{num_epochs}] "
              f"Loss: {avg_loss:.4f} Acc: {acc:.2f}%")

# 准备数据
train_data = torch.randn(1000, 784)
train_labels = torch.randint(0, 10, (1000,))
train_loader = DataLoader(
    TensorDataset(train_data, train_labels),
    batch_size=64, shuffle=True
)

# 初始化模型
teacher = TeacherModel()
student = StudentModel()

# 假设Teacher已经预训练好
# ... teacher训练过程省略 ...

print("=== 硬标签蒸馏训练 ===")
train_hard_label_distillation(teacher, student, train_loader)
3.5.2 软标签蒸馏(经典实现)
import torch
import torch.nn as nn
import torch.optim as optim
import torch.nn.functional as F
from torch.utils.data import DataLoader, TensorDataset

# ========================
# 核心:软标签蒸馏损失函数
# ========================
class DistillationLoss(nn.Module):
    """
    经典知识蒸馏损失
    L = α * T² * KL(softmax(z_s/T) || softmax(z_t/T)) + (1-α) * CE(z_s, y)
    """
    def __init__(self, temperature=4.0, alpha=0.7):
        super().__init__()
        self.temperature = temperature
        self.alpha = alpha
        self.ce_loss = nn.CrossEntropyLoss()
        self.kl_loss = nn.KLDivLoss(reduction='batchmean')

    def forward(self, student_logits, teacher_logits, labels):
        # 软标签损失
        soft_student = F.log_softmax(student_logits / self.temperature, dim=1)
        soft_teacher = F.softmax(teacher_logits / self.temperature, dim=1)
        soft_loss = self.kl_loss(soft_student, soft_teacher) * (self.temperature ** 2)

        # 硬标签损失
        hard_loss = self.ce_loss(student_logits, labels)

        # 加权组合
        total_loss = self.alpha * soft_loss + (1 - self.alpha) * hard_loss
        return total_loss, soft_loss.item(), hard_loss.item()

# ========================
# 软标签蒸馏训练流程
# ========================
def train_soft_label_distillation(teacher, student, train_loader,
                                   temperature=4.0, alpha=0.7,
                                   num_epochs=20, lr=0.001):
    teacher.eval()
    student.train()

    optimizer = optim.Adam(student.parameters(), lr=lr)
    scheduler = optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=num_epochs)
    distill_criterion = DistillationLoss(temperature=temperature, alpha=alpha)

    for epoch in range(num_epochs):
        total_loss = 0
        total_soft = 0
        total_hard = 0
        correct = 0
        total = 0

        for inputs, labels in train_loader:
            optimizer.zero_grad()

            # Teacher前向传播
            with torch.no_grad():
                teacher_logits = teacher(inputs)

            # Student前向传播
            student_logits = student(inputs)

            # 计算蒸馏损失
            loss, soft_l, hard_l = distill_criterion(
                student_logits, teacher_logits, labels
            )

            loss.backward()
            optimizer.step()

            total_loss += loss.item()
            total_soft += soft_l
            total_hard += hard_l

            _, predicted = student_logits.max(1)
            total += labels.size(0)
            correct += predicted.eq(labels).sum().item()

        scheduler.step()
        acc = 100. * correct / total
        avg_loss = total_loss / len(train_loader)
        avg_soft = total_soft / len(train_loader)
        avg_hard = total_hard / len(train_loader)

        print(f"Epoch [{epoch+1}/{num_epochs}] "
              f"Total: {avg_loss:.4f} "
              f"Soft: {avg_soft:.4f} "
              f"Hard: {avg_hard:.4f} "
              f"Acc: {acc:.2f}%")

# ========================
# 运行蒸馏
# ========================
teacher = TeacherModel()
student = StudentModel()

train_data = torch.randn(1000, 784)
train_labels = torch.randint(0, 10, (1000,))
train_loader = DataLoader(
    TensorDataset(train_data, train_labels),
    batch_size=64, shuffle=True
)

print("=== 软标签蒸馏训练 ===")
train_soft_label_distillation(
    teacher, student, train_loader,
    temperature=4.0, alpha=0.7, num_epochs=20
)
3.5.3 特征蒸馏(Feature-based Distillation)
# ========================
# 特征蒸馏:Student学习Teacher的中间层特征
# 适用于Teacher和Student结构差异较大的情况
# ========================

class FeatureDistillationLoss(nn.Module):
    """
    让Student的中间层特征对齐Teacher的中间层特征
    """
    def __init__(self, teacher_dims, student_dims):
        super().__init__()
        # 如果维度不匹配,用线性层对齐
        self.projectors = nn.ModuleList([
            nn.Linear(s_dim, t_dim)
            for s_dim, t_dim in zip(student_dims, teacher_dims)
        ])

    def forward(self, student_features, teacher_features):
        loss = 0
        for i, (s_feat, t_feat) in enumerate(
            zip(student_features, teacher_features)
        ):
            # 投影对齐
            s_feat_proj = self.projectors[i](s_feat)
            # MSE损失
            loss += F.mse_loss(s_feat_proj, t_feat)
        return loss

class TeacherWithFeatures(nn.Module):
    """支持提取中间特征的Teacher"""
    def __init__(self):
        super().__init__()
        self.layer1 = nn.Linear(784, 1024)
        self.layer2 = nn.Linear(1024, 512)
        self.layer3 = nn.Linear(512, 256)
        self.output = nn.Linear(256, 10)

    def forward(self, x):
        f1 = F.relu(self.layer1(x))
        f2 = F.relu(self.layer2(f1))
        f3 = F.relu(self.layer3(f2))
        out = self.output(f3)
        return out, [f1, f2, f3]  # 返回特征

class StudentWithFeatures(nn.Module):
    """支持提取中间特征的Student"""
    def __init__(self):
        super().__init__()
        self.layer1 = nn.Linear(784, 256)
        self.layer2 = nn.Linear(256, 128)
        self.output = nn.Linear(128, 10)

    def forward(self, x):
        f1 = F.relu(self.layer1(x))
        f2 = F.relu(self.layer2(f1))
        out = self.output(f2)
        return out, [f1, f2]  # 返回特征

# 联合训练:分类损失 + 特征蒸馏损失 + 软标签蒸馏损失
def train_with_feature_distillation(teacher, student, train_loader,
                                      feature_criterion, distill_criterion,
                                      feature_weight=0.1,
                                      num_epochs=10, lr=0.001):
    teacher.eval()
    student.train()
    optimizer = optim.Adam(
        list(student.parameters()) + list(feature_criterion.parameters()),
        lr=lr
    )

    for epoch in range(num_epochs):
        total_loss = 0
        for inputs, labels in train_loader:
            optimizer.zero_grad()

            with torch.no_grad():
                t_out, t_features = teacher(inputs)

            s_out, s_features = student(inputs)

            # 1. 分类损失
            cls_loss = F.cross_entropy(s_out, labels)

            # 2. 软标签蒸馏损失
            soft_loss = distill_criterion(s_out, t_out, labels)[0]

            # 3. 特征蒸馏损失
            feat_loss = feature_criterion(s_features, t_features)

            # 总损失
            loss = cls_loss + soft_loss + feature_weight * feat_loss
            loss.backward()
            optimizer.step()

            total_loss += loss.item()

        print(f"Epoch [{epoch+1}/{num_epochs}] "
              f"Loss: {total_loss/len(train_loader):.4f}")
3.5.4 使用Hugging Face进行LLM蒸馏
# ========================
# 使用transformers进行BERT蒸馏到TinyBERT
# ========================
# pip install transformers datasets

from transformers import (
    AutoModelForSequenceClassification,
    AutoTokenizer,
    Trainer,
    TrainingArguments
)
import torch
import torch.nn as nn
import torch.nn.functional as F

class DistillationTrainer(Trainer):
    """自定义Trainer,支持知识蒸馏"""

    def __init__(self, teacher_model=None, temperature=2.0,
                 alpha=0.5, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.teacher = teacher_model
        self.temperature = temperature
        self.alpha = alpha
        if self.teacher:
            self.teacher.eval()

    def compute_loss(self, model, inputs, return_outputs=False, **kwargs):
        labels = inputs.pop("labels")

        # Student前向
        student_outputs = model(**inputs)
        student_logits = student_outputs.logits

        # Teacher前向
        with torch.no_grad():
            teacher_outputs = self.teacher(**inputs)
            teacher_logits = teacher_outputs.logits

        # 硬标签损失
        hard_loss = F.cross_entropy(student_logits, labels)

        # 软标签损失
        soft_student = F.log_softmax(student_logits / self.temperature, dim=1)
        soft_teacher = F.softmax(teacher_logits / self.temperature, dim=1)
        soft_loss = F.kl_div(
            soft_student, soft_teacher,
            reduction='batchmean'
        ) * (self.temperature ** 2)

        # 总损失
        loss = self.alpha * soft_loss + (1 - self.alpha) * hard_loss

        return (loss, student_outputs) if return_outputs else loss

# 使用示例
teacher_model = AutoModelForSequenceClassification.from_pretrained(
    "bert-large-uncased", num_labels=2
)
student_model = AutoModelForSequenceClassification.from_pretrained(
    "prajjwal1/bert-tiny", num_labels=2  # 极小的BERT
)
tokenizer = AutoTokenizer.from_pretrained("bert-large-uncased")

print(f"Teacher参数量: {sum(p.numel() for p in teacher_model.parameters()):,}")
print(f"Student参数量: {sum(p.numel() for p in student_model.parameters()):,}")
print(f"压缩比: {sum(p.numel() for p in teacher_model.parameters()) / sum(p.numel() for p in student_model.parameters()):.1f}x")

3.6 蒸馏策略对比

┌──────────────────┬───────────────┬──────────────────┬─────────────────┐
│     蒸馏策略      │    信息来源    │     损失函数      │    适用场景      │
├──────────────────┼───────────────┼──────────────────┼─────────────────┤
│  硬标签蒸馏       │ Teacher预测类别│ CE(y_s, y_t_hard)│ 类别少/快速基线   │
│  软标签蒸馏       │ Teacher概率分布│ KL(p_s/T||p_t/T) │ 通用分类任务     │
│  特征蒸馏         │ Teacher中间层  │ MSE(f_s, f_t)    │ 结构差异大       │
│  关系蒸馏         │ 样本间关系     │ 保持样本间距离关系  │ 数据关系重要     │
│  自蒸馏           │ 模型自身       │ 深层指导浅层       │ 无大Teacher时    │
└──────────────────┴───────────────┴──────────────────┴─────────────────┘

四、模型剪枝(Model Pruning)

4.1 剪枝的核心概念

模型剪枝通过移除模型中不重要(冗余)的参数或结构,在保持精度的前提下减少模型大小和计算量。

4.1.1 核心假设

深度学习模型通常过参数化(Over-parameterized),大量参数对最终预测贡献很小。研究表明,许多模型可以剪掉90%以上的参数而精度几乎不变("彩票假设" Lottery Ticket Hypothesis)。

4.1.2 剪枝的分类
剪枝方法
├── 按粒度分
│   ├── 非结构化剪枝(Unstructured)→ 单个权重级别
│   └── 结构化剪枝(Structured)    → 整个通道/层级别
│
├── 按时机分
│   ├── 训练后剪枝(Post-training)  → 训练完成后剪枝
│   ├── 训练中剪枝(During training)→ 训练过程中逐渐剪枝
│   └── 训练前剪枝(Before training)→ 初始化时就确定结构
│
└── 按策略分
    ├── 一次性剪枝(One-shot)       → 一次性剪到目标稀疏度
    └── 迭代剪枝(Iterative)        → 多轮剪枝+微调循环
4.1.3 重要性评估标准
标准 描述 优点 缺点
幅值剪枝 移除绝对值最小的权重 实现最简单 可能错过小但重要的权重
梯度剪枝 移除梯度绝对值小的权重 考虑了对损失的影响 需要额外计算梯度
Taylor展开 用一阶/二阶Taylor近似重要性 理论更严谨 计算开销大
Hessian信息 用Hessian矩阵衡量重要性 最精确 计算非常昂贵
Activation 移除激活值接近零的神经元 直觉清晰 依赖输入数据

4.2 非结构化剪枝(Unstructured Pruning)

4.2.1 定义

逐个移除权重矩阵中绝对值最小的权重,将其置为0,形成稀疏矩阵

原始权重矩阵W:          剪枝后W':
[0.5, 0.01, -0.3]      [0.5,  0,   -0.3]
[0.02, 0.8, 0.001]  →  [0,    0.8,  0   ]
[-0.1, 0.005, 0.4]     [-0.1, 0,    0.4 ]
稀疏度: 0%               稀疏度: 44.4%
4.2.2 特点
  • 优点:精度保持好,灵活度高
  • 缺点:稀疏矩阵需要专用硬件/库加速(如NVIDIA Ampere的稀疏张量核心),否则实际加速有限
  • 适用场景:研究场景,或有稀疏计算硬件支持时

4.3 结构化剪枝(Structured Pruning)

4.3.1 定义

移除整个卷积通道(filter)注意力头整个层,得到的模型仍然是稠密的,无需特殊硬件支持。

原始: Conv2d(in=64, out=128, kernel=3×3)
      → 128个filter, 每个filter 64×3×3 = 576个参数

结构化剪枝: 移除40个不重要的filter
      → Conv2d(in=64, out=88, kernel=3×3)
      → 88个filter, 参数减少31%
4.3.2 特点
  • 优点:直接减少计算量,无需特殊硬件,实际加速效果好
  • 缺点:剪枝粒度粗,同等压缩比下精度损失更大
  • 适用场景:生产环境部署

4.4 剪枝代码实战

4.4.1 非结构化剪枝(PyTorch内置API)
import torch
import torch.nn as nn
import torch.nn.utils.prune as prune
import copy

# ========================
# 1. 定义模型
# ========================
class ConvNet(nn.Module):
    def __init__(self, num_classes=10):
        super().__init__()
        self.conv1 = nn.Conv2d(3, 64, 3, padding=1)
        self.bn1 = nn.BatchNorm2d(64)
        self.conv2 = nn.Conv2d(64, 128, 3, padding=1)
        self.bn2 = nn.BatchNorm2d(128)
        self.conv3 = nn.Conv2d(128, 256, 3, padding=1)
        self.bn3 = nn.BatchNorm2d(256)
        self.pool = nn.AdaptiveAvgPool2d(1)
        self.fc = nn.Linear(256, num_classes)
        self.relu = nn.ReLU()

    def forward(self, x):
        x = self.relu(self.bn1(self.conv1(x)))
        x = self.relu(self.bn2(self.conv2(x)))
        x = self.relu(self.bn3(self.conv3(x)))
        x = self.pool(x).flatten(1)
        x = self.fc(x)
        return x

model = ConvNet()

# ========================
# 2. 查看剪枝前的参数统计
# ========================
def count_nonzero_params(model):
    total = 0
    nonzero = 0
    for p in model.parameters():
        total += p.numel()
        nonzero += (p != 0).sum().item()
    return total, nonzero

total, nonzero = count_nonzero_params(model)
print(f"剪枝前: 总参数 {total:,}, 非零参数 {nonzero:,}, 稀疏度 {1-nonzero/total:.2%}")

# ========================
# 3. L1非结构化剪枝(全局)
# ========================
# 对所有卷积层和全连接层进行剪枝
parameters_to_prune = [
    (model.conv1, 'weight'),
    (model.conv2, 'weight'),
    (model.conv3, 'weight'),
    (model.fc, 'weight'),
]

# 全局剪枝:将所有层的权重放在一起排序,移除最小的50%
prune.global_unstructured(
    parameters_to_prune,
    pruning_method=prune.L1Unstructured,
    amount=0.5,  # 剪掉50%
)

total, nonzero = count_nonzero_params(model)
print(f"剪枝后: 总参数 {total:,}, 非零参数 {nonzero:,}, 稀疏度 {1-nonzero/total:.2%}")

# ========================
# 4. 查看剪枝掩码
# ========================
print(f"\nconv1剪枝掩码示例:")
mask = dict(model.conv1.named_buffers()).get('weight_mask', None)
if mask is not None:
    print(f"  mask shape: {mask.shape}")
    print(f"  mask非零比例: {(mask > 0).float().mean():.2%}")

# ========================
# 5. 永久化剪枝(移除mask,将0真正写入权重)
# ========================
for module, param_name in parameters_to_prune:
    prune.remove(module, param_name)

# 验证稀疏度保持
total, nonzero = count_nonzero_params(model)
print(f"\n永久化后: 总参数 {total:,}, 非零参数 {nonzero:,}, 稀疏度 {1-nonzero/total:.2%}")

# ========================
# 6. 剪枝后微调恢复精度
# ========================
def finetune_after_pruning(model, train_loader, num_epochs=5, lr=0.001):
    """剪枝后微调是恢复精度的关键步骤"""
    optimizer = torch.optim.SGD(model.parameters(), lr=lr, momentum=0.9)
    criterion = nn.CrossEntropyLoss()

    for epoch in range(num_epochs):
        model.train()
        total_loss = 0
        for inputs, labels in train_loader:
            optimizer.zero_grad()
            outputs = model(inputs)
            loss = criterion(outputs, labels)
            loss.backward()
            optimizer.step()
            total_loss += loss.item()
        print(f"Finetune Epoch [{epoch+1}/{num_epochs}] "
              f"Loss: {total_loss/len(train_loader):.4f}")
4.4.2 结构化剪枝
import torch
import torch.nn as nn
import torch.nn.utils.prune as prune

# ========================
# 结构化剪枝:移除整个卷积通道
# ========================

model = ConvNet()

# ========================
# 1. 基于L1范数的通道剪枝
# ========================
# 剪掉conv2中30%的输出通道
prune.ln_structured(
    model.conv2,
    name='weight',
    amount=0.3,     # 剪掉30%的通道
    n=1,            # L1范数
    dim=0,          # 沿输出通道维度剪枝
)

# 查看结果
print(f"conv2 原始输出通道: 128")
mask = dict(model.conv2.named_buffers())['weight_mask']
active_channels = mask[:, 0, 0, 0].sum().item()  # 每个filter的mask相同
print(f"conv2 剪枝后活跃通道: {int(active_channels)}")
print(f"conv2 剪枝率: {1 - active_channels/128:.1%}")

# ========================
# 2. 自定义结构化剪枝(基于通道重要性)
# ========================
def channel_importance_pruning(model, layer_name, prune_ratio=0.3):
    """
    基于通道L1范数重要性的结构化剪枝
    """
    layer = dict(model.named_modules())[layer_name]
    weight = layer.weight.data  # shape: [out_channels, in_channels, kH, kW]

    # 计算每个输出通道的L1范数作为重要性指标
    importance = weight.abs().sum(dim=[1, 2, 3])  # shape: [out_channels]

    # 确定要保留的通道数
    num_channels = weight.shape[0]
    num_keep = int(num_channels * (1 - prune_ratio))

    # 选择最重要的通道
    _, indices = torch.topk(importance, num_keep)
    mask = torch.zeros(num_channels, device=weight.device)
    mask[indices] = 1.0

    # 扩展mask到权重形状
    mask = mask.view(-1, 1, 1, 1).expand_as(weight)

    # 应用剪枝
    layer.weight.data *= mask
    print(f"{layer_name}: {num_channels} → {num_keep} 通道 "
          f"(剪掉 {prune_ratio:.0%})")

    return indices

# 使用
kept_indices = channel_importance_pruning(model, 'conv2', prune_ratio=0.3)
4.4.3 迭代剪枝(Iterative Magnitude Pruning)
import torch
import torch.nn as nn
import torch.nn.utils.prune as prune

# ========================
# 迭代剪枝:多轮剪枝+微调,逐步达到目标稀疏度
# ========================

def iterative_pruning(model, train_loader, val_loader,
                       target_sparsity=0.9,
                       num_rounds=10,
                       finetune_epochs=3,
                       lr=0.001):
    """
    迭代剪枝策略(Lottery Ticket Hypothesis的实践方法)

    每一轮:
    1. 剪掉当前权重中一定比例的最小权重
    2. 微调恢复精度
    3. 重复直到达到目标稀疏度
    """
    # 每轮剪枝的比例(相对于剩余权重)
    prune_per_round = 1 - (1 - target_sparsity) ** (1 / num_rounds)

    print(f"目标稀疏度: {target_sparsity:.0%}")
    print(f"总轮数: {num_rounds}")
    print(f"每轮剪枝比例: {prune_per_round:.2%}\n")

    criterion = nn.CrossEntropyLoss()
    parameters_to_prune = [
        (module, 'weight')
        for name, module in model.named_modules()
        if isinstance(module, (nn.Conv2d, nn.Linear))
    ]

    for round_idx in range(num_rounds):
        # ===== 剪枝 =====
        prune.global_unstructured(
            parameters_to_prune,
            pruning_method=prune.L1Unstructured,
            amount=prune_per_round,
        )

        # 统计当前稀疏度
        total, nonzero = 0, 0
        for module, param_name in parameters_to_prune:
            param = getattr(module, param_name)
            total += param.numel()
            nonzero += (param != 0).sum().item()

        current_sparsity = 1 - nonzero / total
        print(f"Round {round_idx + 1}/{num_rounds} | "
              f"稀疏度: {current_sparsity:.2%}")

        # ===== 微调 =====
        optimizer = torch.optim.SGD(model.parameters(), lr=lr, momentum=0.9)

        for epoch in range(finetune_epochs):
            model.train()
            total_loss = 0
            for inputs, labels in train_loader:
                optimizer.zero_grad()
                outputs = model(inputs)
                loss = criterion(outputs, labels)
                loss.backward()

                # 重要:只更新非零权重(保持剪枝结果)
                optimizer.step()

            avg_loss = total_loss / max(len(train_loader), 1)

        # ===== 验证 =====
        model.eval()
        correct, total = 0, 0
        with torch.no_grad():
            for inputs, labels in val_loader:
                outputs = model(inputs)
                _, predicted = outputs.max(1)
                total += labels.size(0)
                correct += predicted.eq(labels).sum().item()
        val_acc = 100. * correct / total
        print(f"  微调后验证精度: {val_acc:.2f}%\n")

    return model

4.5 剪枝与重训练策略

┌─────────────────────────────────────────────────────────┐
│                    剪枝最佳实践流程                       │
│                                                         │
│  1. 训练原始模型至收敛                                    │
│          ↓                                              │
│  2. 评估各层/通道重要性                                   │
│          ↓                                              │
│  3. 剪掉不重要的部分(10~30%)                            │
│          ↓                                              │
│  4. 微调(fine-tune)恢复精度                             │
│          ↓                                              │
│  5. 重复步骤2-4直到达到目标稀疏度                          │
│          ↓                                              │
│  6. 导出剪枝后的模型                                      │
│                                                         │
│  关键参数:                                               │
│  - 每轮剪枝比例: 10~30%(推荐20%)                        │
│  - 微调轮数: 1~5 epochs                                  │
│  - 学习率: 原始学习率的1/10~1/100                         │
│  - 总剪枝轮数: 5~20轮                                    │
└─────────────────────────────────────────────────────────┘

五、LoRA低秩矩阵高效微调

5.1 LoRA的核心概念

LoRA(Low-Rank Adaptation of Large Language Models) 由微软于2021年提出,是一种参数高效微调(PEFT, Parameter-Efficient Fine-Tuning)方法。

5.1.1 核心思想

大模型微调时,权重更新矩阵ΔW通常是低秩的(Low-Rank)——即可以用两个更小的矩阵的乘积来近似表示。

原始更新: W_new = W_0 + ΔW        ΔW ∈ R^(d×d), 有 d² 个参数

LoRA分解: ΔW = B × A              B ∈ R^(d×r), A ∈ R^(r×d), 有 2dr 个参数
          其中 r << d

参数减少: 从 d² → 2dr
当 d=4096, r=8 时: 16,777,216 → 65,536 (减少 99.6%)
5.1.2 为什么LoRA有效?
  1. 1.内在维度假说:预训练模型的权重更新存在一个低维的"内在子空间",不需要更新所有参数
  2. 2.Aghajanyan et al. (2020) 的实验表明,即使是随机初始化的模型,也存在低维子空间可以达到不错的微调效果
  3. 3.微调的本质是做任务适配,而非重新学习语言知识

5.2 LoRA的数学原理

5.2.1 前向传播
标准前向传播:
  h = W_0 × x

LoRA前向传播:
  h = W_0 × x + (B × A) × x
    = W_0 × x + B × (A × x)
    = W_0 × x + Δh

其中:
  W_0: 预训练权重(冻结,不更新)
  A ∈ R^(r×d): 下投影矩阵(随机高斯初始化)
  B ∈ R^(d×r): 上投影矩阵(零初始化)
  r: 秩(rank),通常取 4~64
5.2.2 初始化策略
# A矩阵:随机高斯初始化
A = torch.randn(r, d) * (1 / math.sqrt(r))

# B矩阵:零初始化
B = torch.zeros(d, r)

# 初始时 ΔW = B × A = 0
# 这保证了训练开始时LoRA不影响预训练模型的输出
5.2.3 缩放因子α
h = W_0 × x + (α / r) × B × A × x 
  • α 是一个超参数,控制LoRA的更新幅度
  • 实践中通常设 α = rα = 2r
  • α = r 时,缩放因子为1
5.2.4 应用位置

在Transformer架构中,LoRA通常应用于注意力层的Q、K、V、O投影矩阵

Transformer Block
├── Multi-Head Attention
│   ├── Q_proj  ← LoRA ✓ (最常见)
│   ├── K_proj  ← LoRA ✓
│   ├── V_proj  ← LoRA ✓
│   └── O_proj  ← LoRA ✓
├── FFN
│   ├── up_proj ← LoRA ✓ (可选)
│   └── down_proj ← LoRA ✓ (可选)
├── LayerNorm
└── Residual Connection

5.3 LoRA vs 全量微调

5.3.1 全面对比
对比维度 全量微调(Full Fine-tuning) LoRA微调
可训练参数 100%(所有参数) 0.1%~1%(仅LoRA矩阵)
显存占用 非常高(需存储梯度、优化器状态) 大幅降低(仅LoRA参数的梯度)
训练速度 快(计算量小)
存储开销 每个任务一个完整模型副本 仅需存储LoRA权重(几MB~几十MB)
多任务切换 需要加载不同模型 热插拔LoRA权重
灾难性遗忘 风险较高 风险低(预训练权重冻结)
精度 通常最好 接近全量微调(差距<1%)
适用场景 数据充足、计算资源充足 资源受限、多任务、快速迭代
5.3.2 显存对比示例(LLaMA-7B)
模型: LLaMA-7B (6.7B参数)

全量微调(FP16 + AdamW):
  模型参数:   6.7B × 2 bytes = 13.4 GB
  梯度:       6.7B × 2 bytes = 13.4 GB
  优化器状态:  6.7B × 8 bytes = 53.6 GB  (Adam: m + v)
  总计: ~80 GB → 需要 2×A100-80GB

LoRA微调(rank=8, FP16):
  模型参数(冻结):  13.4 GB
  LoRA参数:          ~10M × 2 bytes = 0.02 GB
  LoRA梯度:          ~10M × 2 bytes = 0.02 GB
  LoRA优化器状态:    ~10M × 8 bytes = 0.08 GB
  总计: ~13.5 GB → 单张RTX 3090/4090即可
5.3.3 什么时候用全量微调?
  • 训练数据充足(>10万条)
  • 任务与预训练分布差异极大(如新语言、新领域)
  • 有足够的GPU资源
  • 追求极致精度
5.3.4 什么时候用LoRA?
  • 训练数据有限(<1万条)
  • 需要快速迭代多个任务
  • GPU资源有限
  • 需要部署多个不同任务的模型(LoRA热插拔)

5.4 LoRA代码实战

5.4.1 从零实现LoRA层
import torch
import torch.nn as nn
import torch.nn.functional as F
import math

# ========================
# 1. LoRA层的PyTorch实现
# ========================
class LoRALayer(nn.Module):
    """
    低秩适配层
    将一个线性层替换为: W_0 + (α/r) * B @ A
    """
    def __init__(self, original_layer, rank=8, alpha=16, dropout=0.1):
        super().__init__()
        self.original_layer = original_layer
        self.rank = rank
        self.alpha = alpha
        self.scaling = alpha / rank

        in_features = original_layer.in_features
        out_features = original_layer.out_features

        # 冻结原始权重
        self.original_layer.weight.requires_grad = False
        if self.original_layer.bias is not None:
            self.original_layer.bias.requires_grad = False

        # LoRA矩阵
        self.lora_A = nn.Parameter(
            torch.randn(rank, in_features) * (1 / math.sqrt(rank))
        )
        self.lora_B = nn.Parameter(
            torch.zeros(out_features, rank)
        )
        self.dropout = nn.Dropout(dropout)

        # 可训练参数统计
        lora_params = rank * in_features + out_features * rank
        original_params = in_features * out_features
        print(f"LoRA参数: {lora_params:,} / 原始参数: {original_params:,} "
              f"({lora_params/original_params:.2%})")

    def forward(self, x):
        # 原始前向传播
        original_output = self.original_layer(x)
        # LoRA分支
        lora_output = (self.dropout(x) @ self.lora_A.T @ self.lora_B.T) * self.scaling
        return original_output + lora_output


# ========================
# 2. 将LoRA应用到整个模型
# ========================
def apply_lora_to_model(model, target_modules=None, rank=8, alpha=16):
    """
    对模型中指定的线性层应用LoRA
    """
    if target_modules is None:
        target_modules = ['query', 'key', 'value', 'dense', 'fc', 'proj']

    lora_count = 0
    for name, module in model.named_modules():
        if isinstance(module, nn.Linear):
            # 检查是否为目标模块
            if any(target in name for target in target_modules):
                parent_name = '.'.join(name.split('.')[:-1])
                child_name = name.split('.')[-1]
                parent = dict(model.named_modules()).get(
                    parent_name, model
                )

                # 替换为LoRA层
                lora_layer = LoRALayer(module, rank=rank, alpha=alpha)
                setattr(parent, child_name, lora_layer)
                lora_count += 1

    print(f"\n共为 {lora_count} 个线性层添加了LoRA")

    # 统计可训练参数
    trainable = sum(p.numel() for p in model.parameters() if p.requires_grad)
    total = sum(p.numel() for p in model.parameters())
    print(f"可训练参数: {trainable:,} / {total:,} ({trainable/total:.2%})")

    return model


# ========================
# 3. 示例:对一个Transformer模型应用LoRA
# ========================
class SimpleTransformer(nn.Module):
    def __init__(self, vocab_size=1000, d_model=256, nhead=4,
                 num_layers=2, num_classes=10):
        super().__init__()
        self.embedding = nn.Embedding(vocab_size, d_model)
        encoder_layer = nn.TransformerEncoderLayer(
            d_model=d_model, nhead=nhead,
            dim_feedforward=512, batch_first=True
        )
        self.transformer = nn.TransformerEncoder(
            encoder_layer, num_layers=num_layers
        )
        self.fc = nn.Linear(d_model, num_classes)

    def forward(self, x):
        x = self.embedding(x)
        x = self.transformer(x)
        x = x.mean(dim=1)  # 全局平均池化
        x = self.fc(x)
        return x

# 创建模型并应用LoRA
model = SimpleTransformer()
print("=== 原始模型 ===")
total_params = sum(p.numel() for p in model.parameters())
print(f"总参数: {total_params:,}")

print("\n=== 应用LoRA ===")
model = apply_lora_to_model(model, rank=8, alpha=16)

# ========================
# 4. LoRA微调训练
# ========================
def train_with_lora(model, train_loader, num_epochs=10, lr=1e-3):
    # 只优化LoRA参数
    lora_params = [p for n, p in model.named_parameters() if p.requires_grad]
    optimizer = torch.optim.AdamW(lora_params, lr=lr, weight_decay=0.01)
    criterion = nn.CrossEntropyLoss()

    for epoch in range(num_epochs):
        model.train()
        total_loss = 0
        for inputs, labels in train_loader:
            optimizer.zero_grad()
            outputs = model(inputs)
            loss = criterion(outputs, labels)
            loss.backward()
            optimizer.step()
            total_loss += loss.item()

        avg_loss = total_loss / len(train_loader)
        print(f"Epoch [{epoch+1}/{num_epochs}] Loss: {avg_loss:.4f}")
5.4.2 使用Hugging Face PEFT库(推荐方案)
# ========================
# 使用PEFT库进行LoRA微调(最主流的方案)
# ========================
# pip install peft transformers datasets accelerate bitsandbytes

from transformers import (
    AutoModelForSequenceClassification,
    AutoTokenizer,
    TrainingArguments,
    Trainer,
)
from peft import (
    LoraConfig,
    get_peft_model,
    TaskType,
    PeftModel,
    PeftConfig,
)
import torch

# ========================
# 1. 加载预训练模型
# ========================
model_name = "bert-base-uncased"
model = AutoModelForSequenceClassification.from_pretrained(
    model_name, num_labels=2
)
tokenizer = AutoTokenizer.from_pretrained(model_name)

# ========================
# 2. 配置LoRA
# ========================
lora_config = LoraConfig(
    task_type=TaskType.SEQ_CLS,       # 任务类型:序列分类
    r=8,                               # LoRA秩
    lora_alpha=16,                     # 缩放因子
    lora_dropout=0.1,                  # Dropout
    target_modules=["query", "value"], # 应用LoRA的模块
    bias="none",                       # 不训练bias
)

# ========================
# 3. 创建PEFT模型
# ========================
peft_model = get_peft_model(model, lora_config)

# 打印可训练参数信息
peft_model.print_trainable_parameters()
# 输出示例: trainable params: 296,450 || all params: 109,780,228 || trainable%: 0.2701

# ========================
# 4. 训练
# ========================
training_args = TrainingArguments(
    output_dir="./lora_output",
    num_train_epochs=3,
    per_device_train_batch_size=16,
    per_device_eval_batch_size=64,
    learning_rate=2e-4,            # LoRA通常用较大的学习率
    warmup_ratio=0.06,
    weight_decay=0.01,
    logging_steps=50,
    evaluation_strategy="epoch",
    save_strategy="epoch",
    load_best_model_at_end=True,
    fp16=True,
)

# 这里需要准备实际的train_dataset和eval_dataset
# trainer = Trainer(
#     model=peft_model,
#     args=training_args,
#     train_dataset=train_dataset,
#     eval_dataset=eval_dataset,
#     tokenizer=tokenizer,
# )
# trainer.train()

# ========================
# 5. 保存LoRA权重(仅几MB!)
# ========================
peft_model.save_pretrained("./my_lora_weights")

# ========================
# 6. 加载LoRA权重推理
# ========================
# 重新加载基础模型
base_model = AutoModelForSequenceClassification.from_pretrained(
    model_name, num_labels=2
)

# 加载LoRA权重
loaded_model = PeftModel.from_pretrained(
    base_model, "./my_lora_weights"
)
loaded_model.eval()

# 推理
text = "This movie is really great!"
inputs = tokenizer(text, return_tensors="pt", padding=True, truncation=True)
with torch.no_grad():
    outputs = loaded_model(**inputs)
    prediction = outputs.logits.argmax(dim=-1)
    print(f"预测结果: {'正面' if prediction.item() == 1 else '负面'}")
5.4.3 LoRA微调LLaMA大语言模型(QLoRA)
# ========================
# QLoRA: 4-bit量化 + LoRA = 超低资源微调LLM
# ========================
# pip install transformers peft accelerate bitsandbytes datasets trl

import torch
from transformers import (
    AutoModelForCausalLM,
    AutoTokenizer,
    BitsAndBytesConfig,
    TrainingArguments,
)
from peft import LoraConfig, get_peft_model, prepare_model_for_kbit_training
from trl import SFTTrainer
from datasets import load_dataset

# ========================
# 1. 4-bit量化配置
# ========================
bnb_config = BitsAndBytesConfig(
    load_in_4bit=True,
    bnb_4bit_quant_type="nf4",           # NF4量化(QLoRA论文推荐)
    bnb_4bit_compute_dtype=torch.bfloat16,
    bnb_4bit_use_double_quant=True,       # 双重量化,进一步压缩
)

# ========================
# 2. 加载基座模型
# ========================
model_id = "meta-llama/Llama-2-7b-hf"
model = AutoModelForCausalLM.from_pretrained(
    model_id,
    quantization_config=bnb_config,
    device_map="auto",
    trust_remote_code=True,
)
tokenizer = AutoTokenizer.from_pretrained(model_id)
tokenizer.pad_token = tokenizer.eos_token

# 准备模型进行量化训练
model = prepare_model_for_kbit_training(model)

# ========================
# 3. LoRA配置
# ========================
lora_config = LoraConfig(
    r=16,                              # 较大的rank适合LLM
    lora_alpha=32,                     # alpha = 2 * r
    lora_dropout=0.05,
    bias="none",
    task_type="CAUSAL_LM",
    target_modules=[                   # 应用到所有注意力投影和FFN
        "q_proj", "k_proj", "v_proj", "o_proj",
        "gate_proj", "up_proj", "down_proj",
    ],
)

model = get_peft_model(model, lora_config)
model.print_trainable_parameters()
# 输出: trainable params: 39,976,960 || all params: 6,778,765,312 || trainable%: 0.5898

# ========================
# 4. 准备训练数据
# ========================
dataset = load_dataset("tatsu-lab/alpaca", split="train[:1000]")

def format_prompt(example):
    """格式化为指令微调格式"""
    if example["input"]:
        return f"### Instruction:\n{example['instruction']}\n\n### Input:\n{example['input']}\n\n### Response:\n{example['output']}"
    else:
        return f"### Instruction:\n{example['instruction']}\n\n### Response:\n{example['output']}"

# ========================
# 5. 训练配置
# ========================
training_args = TrainingArguments(
    output_dir="./qlora-llama2",
    num_train_epochs=3,
    per_device_train_batch_size=4,
    gradient_accumulation_steps=4,       # 有效batch_size = 4 * 4 = 16
    learning_rate=2e-4,
    weight_decay=0.01,
    warmup_ratio=0.03,
    lr_scheduler_type="cosine",
    logging_steps=10,
    save_strategy="epoch",
    fp16=False,
    bf16=True,                           # 使用BF16混合精度
    optim="paged_adamw_8bit",            # 8-bit优化器,节省显存
    gradient_checkpointing=True,         # 梯度检查点,用时间换空间
    max_grad_norm=0.3,
    report_to="none",
)

# ========================
# 6. 开始训练
# ========================
trainer = SFTTrainer(
    model=model,
    args=training_args,
    train_dataset=dataset,
    tokenizer=tokenizer,
    max_seq_length=512,
    formatting_func=format_prompt,
)

# trainer.train()

# ========================
# 7. 保存和合并LoRA权重
# ========================
# 仅保存LoRA权重(几十MB)
# trainer.save_model("./qlora-llama2-lora")

# 合并LoRA权重到基座模型(可选)
# merged_model = model.merge_and_unload()
# merged_model.save_pretrained("./llama2-merged")

# ========================
# 8. 推理示例
# ========================
def generate_response(model, tokenizer, prompt, max_new_tokens=256):
    inputs = tokenizer(prompt, return_tensors="pt").to(model.device)
    with torch.no_grad():
        outputs = model.generate(
            **inputs,
            max_new_tokens=max_new_tokens,
            temperature=0.7,
            top_p=0.9,
            do_sample=True,
        )
    return tokenizer.decode(outputs[0], skip_special_tokens=True)

prompt = "### Instruction:\nExplain what is LoRA in simple terms.\n\n### Response:\n"
# response = generate_response(model, tokenizer, prompt)
# print(response)
5.4.4 LoRA权重合并与多任务热插拔
# ========================
# LoRA多任务管理:热插拔不同任务的LoRA权重
# ========================

from peft import PeftModel
from transformers import AutoModelForCausalLM, AutoTokenizer

# 基座模型只需加载一次
base_model = AutoModelForCausalLM.from_pretrained(
    "meta-llama/Llama-2-7b-hf",
    torch_dtype=torch.float16,
    device_map="auto"
)
tokenizer = AutoTokenizer.from_pretrained("meta-llama/Llama-2-7b-hf")

# ========================
# 方式1:动态加载不同任务的LoRA
# ========================
def load_task_lora(base_model, lora_path):
    """加载特定任务的LoRA权重"""
    model = PeftModel.from_pretrained(base_model, lora_path)
    return model

# 任务A:代码生成
# model_code = load_task_lora(base_model, "./lora-code-generation")

# 任务B:医疗问答
# model_medical = load_task_lora(base_model, "./lora-medical-qa")

# 任务C:法律文书
# model_legal = load_task_lora(base_model, "./lora-legal-doc")

# ========================
# 方式2:合并LoRA权重(部署时推荐)
# ========================
# 将LoRA权重合并回基座模型,推理时无额外开销
# merged_model = model_code.merge_and_unload()
# merged_model.save_pretrained("./llama2-code-merged")

# ========================
# 方式3:多LoRA同时使用(LoRA集成)
# ========================
from peft import PeftModel

# 加载多个LoRA
model = PeftModel.from_pretrained(base_model, "./lora-task-a", adapter_name="task_a")
model.load_adapter("./lora-task-b", adapter_name="task_b")

# 切换活跃的LoRA
model.set_adapter("task_a")  # 使用任务A的LoRA
# model.set_adapter("task_b")  # 切换到任务B

# 加权混合两个LoRA
# model.add_weighted_adapter(
#     adapters=["task_a", "task_b"],
#     weights=[0.7, 0.3],  # 70%任务A + 30%任务B
#     adapter_name="mixed"
# )

5.5 LoRA变体与进阶

5.5.1 LoRA变体总结
变体 核心改进 适用场景
LoRA 基础版本,低秩分解 通用微调
QLoRA 4-bit量化 + LoRA 超低资源微调LLM
LoRA+ A和B使用不同学习率 提升收敛速度
AdaLoRA 自适应分配不同层的秩 自动优化rank分配
DoRA 分解权重为方向和大小 精度更接近全量微调
rsLoRA 缩放因子改为 1/√r 大rank时更稳定
GaLore 梯度低秩投影 进一步节省优化器显存
5.5.2 DoRA简要介绍
# DoRA (Weight-Decomposed Low-Rank Adaptation)
# 核心思想:将权重分解为方向(direction)和大小(magnitude)两部分

# 标准LoRA: W' = W + BA
# DoRA:     W' = m * (W + BA) / ||W + BA||_c
# 其中 m 是可学习的magnitude向量,||·||_c 是列范数

# DoRA在多个任务上比LoRA精度提升0.5~1%
5.5.3 LoRA超参数调优指南
┌─────────────────────────────────────────────────────────────┐
│                    LoRA超参数调优指南                         │
├──────────┬──────────────────────────────────────────────────┤
│ 参数      │ 建议                                              │
├──────────┼──────────────────────────────────────────────────┤
│ rank (r) │ • 小模型/简单任务: 4~8                             │
│          │ • 大模型/复杂任务: 16~64                            │
│          │ • 数据充足时可以用更大的rank                          │
├──────────┼──────────────────────────────────────────────────┤
│ alpha    │ • 通常设为 rank 或 2*rank                          │
│          │ • alpha/rank 比值影响学习率的有效缩放                  │
├──────────┼──────────────────────────────────────────────────┤
│ dropout  │ • 通常 0.05~0.1                                   │
│          │ • 数据少时可适当增大                                  │
├──────────┼──────────────────────────────────────────────────┤
│ 目标模块  │ • 最少: query, value                               │
│          │ • 推荐: query, key, value, output                  │
│          │ • 最佳: 所有注意力+FFN层                             │
├──────────┼──────────────────────────────────────────────────┤
│ 学习率    │ • 通常比全量微调大: 1e-4 ~ 3e-4                     │
│          │ • 配合cosine或linear warmup调度器                   │
├──────────┼──────────────────────────────────────────────────┤
│ batch size│ • 尽量大,配合梯度累积                              │
│          │ • 有效batch_size建议16~64                           │
└──────────┴──────────────────────────────────────────────────┘

六、四大压缩技术综合对比

6.1 技术维度对比

┌──────────────┬──────────────┬──────────────┬──────────────┬──────────────┐
│    维度       │    量化       │    蒸馏       │    剪枝       │    LoRA      │
├──────────────┼──────────────┼──────────────┼──────────────┼──────────────┤
│  压缩对象     │  数值精度     │  模型规模     │  参数数量      │  微调参数     │
├──────────────┼──────────────┼──────────────┼──────────────┼──────────────┤
│  是否需要     │  PTQ:不需要   │  需要         │  需要         │  需要         │
│  重新训练     │  QAT:需要     │              │  (微调)       │  (微调)      │
├──────────────┼──────────────┼──────────────┼──────────────┼──────────────┤
│  压缩比       │  2~8×        │  可达10×+    │  2~10×       │  N/A         │
│              │  (模型大小)   │  (参数量)    │  (参数量)     │  (微调效率)   │
├──────────────┼──────────────┼──────────────┼──────────────┼──────────────┤
│  精度损失     │  小(0.5~2%)  │  中(1~5%)   │  小~中        │  极小(<1%)   │
├──────────────┼──────────────┼──────────────┼──────────────┼──────────────┤
│  实现复杂度   │  低~中       │  高          │  中           │  低          │
├──────────────┼──────────────┼──────────────┼──────────────┼──────────────┤
│  额外数据需求 │  PTQ:少量    │  需要(或     │  少量         │  下游任务     │
│              │  QAT:正常    │   用原始数据) │              │  微调数据     │
├──────────────┼──────────────┼──────────────┼──────────────┼──────────────┤
│  推理加速     │  ✓ 显著      │  ✓ 显著      │  结构化:✓    │  ✗ 无        │
│              │              │              │  非结构化:△   │  (微调方法)   │
├──────────────┼──────────────┼──────────────┼──────────────┼──────────────┤
│  可组合性     │  可与所有     │  可与量化/   │  可与量化/   │  可与量化     │
│              │  方法组合     │  剪枝组合    │  蒸馏组合    │  组合(QLoRA)  │
└──────────────┴──────────────┴──────────────┴──────────────┴──────────────┘

6.2 组合使用策略

推荐组合方案:

方案1(部署优化): 全量微调 → 量化(PTQ/INT8) → 部署
方案2(极致压缩): 蒸馏(大→小) → 剪枝 → 量化 → 部署
方案3(高效微调+部署): QLoRA微调 → 合并权重 → 量化 → 部署
方案4(持续迭代): LoRA微调 → 多任务热插拔 → 按需量化部署

七、工程实践建议与工具链

7.1 工具链推荐

工具 用途 链接
PyTorch Quantization PyTorch内置量化 torch.quantization
bitsandbytes LLM量化(INT8/INT4) github.com/TimDettmers/bitsandbytes
AutoGPTQ GPTQ量化 github.com/AutoGPTQ/AutoGPTQ
llama.cpp CPU端LLM推理(GGUF格式) github.com/ggerganov/llama.cpp
PEFT LoRA/AdaLoRA等PEFT方法 github.com/huggingface/peft
Transformers Hugging Face模型生态 github.com/huggingface/transformers
vLLM 高性能LLM推理服务 github.com/vllm-project/vllm
TensorRT NVIDIA推理优化引擎 developer.nvidia.com/tensorrt
ONNX Runtime 跨平台推理优化 onnxruntime.ai
Intel Neural Compressor Intel平台量化工具 github.com/intel/neural-compressor

7.2 实践建议

┌─────────────────────────────────────────────────────────────┐
│                    模型压缩实践建议                           │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  1. 先明确约束条件                                           │
│     - 目标延迟是多少?(如 <100ms)                           │
│     - 目标模型大小?(如 <100MB)                            │
│     - 可接受的精度损失?(如 <1%)                            │
│     - 目标硬件是什么?(GPU/CPU/移动端)                       │
│                                                             │
│  2. 选择合适的压缩策略                                       │
│     - 有训练数据 → 蒸馏/QAT/LoRA                             │
│     - 无训练数据 → PTQ/动态量化                               │
│     - 需要实际加速 → 量化/结构化剪枝                          │
│     - 需要多任务 → LoRA                                      │
│                                                             │
│  3. 渐进式压缩                                              │
│     - 先尝试最简单的方法(如INT8 PTQ)                        │
│     - 精度不够再尝试更复杂的方法(如QAT/蒸馏)                  │
│     - 记录每一步的精度和性能指标                               │
│                                                             │
│  4. 充分验证                                                │
│     - 在代表性测试集上评估                                    │
│     - 检查边缘case的精度                                     │
│     - 测量实际端到端延迟,而非理论FLOPs                        │
│                                                             │
│  5. 版本管理                                                │
│     - 保存原始FP32模型作为基线                                │
│     - 记录每个压缩版本的超参数和精度                           │
│     - LoRA权重单独存储,方便回退                              │
│                                                             │
└─────────────────────────────────────────────────────────────┘

7.3 不同场景的推荐方案

场景 推荐方案 原因
LLM本地部署(消费级GPU) QLoRA微调 + INT4量化 显存需求最低
服务端高吞吐推理 全量微调 + INT8 PTQ 精度和速度平衡
移动端部署 蒸馏到小模型 + INT8量化 极致压缩
多任务快速迭代 LoRA微调 + 基座共享 存储和切换效率最高
精度敏感场景 QAT量化 精度损失最小
研究/原型验证 动态量化 最快速验证

八、总结与展望

8.1 核心要点回顾

模型压缩 = 精度与效率的权衡艺术

量化 → 改变"数字的精度"   →  FP32 → INT8/INT4
蒸馏 → 改变"模型的大小"   →  大模型 → 小模型
剪枝 → 改变"参数的数量"   →  稠密 → 稀疏
LoRA → 改变"微调的方式"   →  全量更新 → 低秩更新

8.2 前沿趋势

  1. 1.更激进的量化:1-bit LLM(BitNet)、2-bit量化研究持续推进
  2. 2.蒸馏+MoE:将大MoE模型蒸馏为稠密小模型
  3. 3.自适应压缩:不同层使用不同的压缩比(如敏感层少压缩)
  4. 4.硬件-算法协同设计:针对特定硬件的最优压缩方案
  5. 5.LoRA家族持续扩展:DoRA、GaLore、LoRA+等变体不断涌现
  6. 6.端侧大模型:手机端运行7B甚至更大参数的模型成为现实

8.3 学习路径建议

入门路径:
  PyTorch量化API → 简单模型蒸馏 → LoRA微调 → 实际项目实践

进阶路径:
  QAT深入理解 → 蒸馏策略优化 → 结构化剪枝 → QLoRA/DoRA → 压缩组合策略

研究方向:
  低比特量化理论 → 蒸馏的理论基础 → 剪枝的彩票假说 → PEFT方法创新
Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐