第5课 上机实验
量化实验(必做):多精度加载 Qwen3-8B 并评估压缩影响;微调后量化(选做):LoRA 合并与静态量化部署;能力扩展选做(三选一):蒸馏分析、多模态实验、工具调用实验
实验概览
本课实验分为三部分:
| 部分 | 内容 | 时长 | 要求 |
|---|---|---|---|
| 实验 A(必做) | 量化实验——测量压缩对模型质量的影响 | ~60 分钟 | 全部完成 |
| 实验 B(选做) | 微调后量化——LoRA 合并 → 静态量化 → 部署评估 | ~50 分钟 | 推荐完成 |
| 实验 C(选做) | 能力扩展——蒸馏/多模态/工具使用三选一 | ~50 分钟 | 完成一项 |
预计总计算时间:A100-40G 约 80-130 分钟。
实验 A:量化实验(必做)
实验目标
- 以 FP16、INT8、INT4 三种精度加载 Qwen3-8B,对比显存占用和加载时间
- 测量不同精度下的推理速度(tokens/sec、首 token 延迟)
- 在 GSM8K 数学推理、指令跟随、中文任务上评估量化对质量的影响
- 使用 LLM-as-Judge 进行系统化评分
环境准备
# 安装依赖
# pip install "transformers>=4.51.0" bitsandbytes accelerate torch datasets
import torch
import time
import json
import gc
from transformers import AutoModelForCausalLM, AutoTokenizer, BitsAndBytesConfig
# 验证 GPU
print(f"GPU: {torch.cuda.get_device_name(0)}")
print(f"GPU 显存: {torch.cuda.get_device_properties(0).total_mem / 1024**3:.1f} GB")Step 1:多精度加载对比(20 分钟)
定义加载函数
MODEL_NAME = "Qwen/Qwen3-8B" # 或本地路径
def load_model(precision="fp16"):
"""以指定精度加载模型,返回模型和元信息"""
torch.cuda.empty_cache()
gc.collect()
torch.cuda.reset_peak_memory_stats()
start_time = time.time()
if precision == "fp16":
model = AutoModelForCausalLM.from_pretrained(
MODEL_NAME,
torch_dtype=torch.float16,
device_map="auto",
)
elif precision == "int8":
config = BitsAndBytesConfig(load_in_8bit=True)
model = AutoModelForCausalLM.from_pretrained(
MODEL_NAME,
quantization_config=config,
device_map="auto",
)
elif precision == "int4":
config = BitsAndBytesConfig(
load_in_4bit=True,
bnb_4bit_quant_type="nf4",
bnb_4bit_use_double_quant=True,
bnb_4bit_compute_dtype=torch.bfloat16,
)
model = AutoModelForCausalLM.from_pretrained(
MODEL_NAME,
quantization_config=config,
device_map="auto",
)
else:
raise ValueError(f"不支持的精度: {precision}")
load_time = time.time() - start_time
memory_gb = torch.cuda.max_memory_allocated() / 1024**3
return model, {
"precision": precision,
"load_time_s": round(load_time, 1),
"memory_gb": round(memory_gb, 2),
}依次加载三种精度并记录数据
tokenizer = AutoTokenizer.from_pretrained(MODEL_NAME)
results = {}
for precision in ["fp16", "int8", "int4"]:
print(f"\n{'='*50}")
print(f"正在加载 {precision.upper()} 模型...")
model, info = load_model(precision)
results[precision] = info
print(f" 加载时间: {info['load_time_s']}s")
print(f" 显存占用: {info['memory_gb']} GB")
# 简单验证:生成一条回复
test_input = tokenizer("你好,请自我介绍一下。", return_tensors="pt").to(model.device)
with torch.no_grad():
output = model.generate(**test_input, max_new_tokens=50, do_sample=False)
print(f" 验证回复: {tokenizer.decode(output[0], skip_special_tokens=True)[:100]}")
del model
torch.cuda.empty_cache()
gc.collect()
# 打印对比表
print("\n" + "="*60)
print(f"{'精度':<10} {'显存(GB)':<12} {'加载时间(s)':<14} {'相对FP16'}")
print("-"*60)
fp16_mem = results['fp16']['memory_gb']
for p, info in results.items():
ratio = info['memory_gb'] / fp16_mem
print(f"{p.upper():<10} {info['memory_gb']:<12} {info['load_time_s']:<14} {ratio:.2f}x")Step 2:推理速度测试(15 分钟)
TEST_PROMPTS = [
"请解释什么是量子计算?",
"用Python写一个快速排序算法。",
"请将以下中文翻译成英文:人工智能正在深刻改变我们的生活方式。",
"写一首关于春天的七言绝句。",
"请解释相对论的核心思想。",
"什么是知识蒸馏?请用简单的语言解释。",
"请列出机器学习的五个主要应用领域。",
"解释什么是梯度下降法。",
"为什么大语言模型需要后训练?",
"请比较 Python 和 Java 的主要区别。",
]
def benchmark_inference(model, tokenizer, prompts, max_new_tokens=128):
"""测量推理速度"""
total_tokens = 0
first_token_latencies = []
for prompt in prompts:
messages = [{"role": "user", "content": prompt}]
text = tokenizer.apply_chat_template(
messages, tokenize=False, add_generation_prompt=True
)
inputs = tokenizer(text, return_tensors="pt").to(model.device)
input_len = inputs["input_ids"].shape[1]
# 测量首 token 延迟
start = time.time()
with torch.no_grad():
output = model.generate(
**inputs, max_new_tokens=1, do_sample=False
)
first_token_time = time.time() - start
first_token_latencies.append(first_token_time)
# 测量完整生成速度
start = time.time()
with torch.no_grad():
output = model.generate(
**inputs, max_new_tokens=max_new_tokens, do_sample=False
)
gen_time = time.time() - start
gen_tokens = output.shape[1] - input_len
total_tokens += gen_tokens
avg_first_token = sum(first_token_latencies) / len(first_token_latencies)
tokens_per_sec = total_tokens / sum(
[time.time() - start for _ in range(1)] # 简化计算
)
return {
"avg_first_token_latency_ms": round(avg_first_token * 1000, 1),
"total_tokens": total_tokens,
}
# 对每种精度运行速度测试
speed_results = {}
for precision in ["fp16", "int8", "int4"]:
print(f"\n测试 {precision.upper()} 推理速度...")
model, _ = load_model(precision)
# 预热
warmup_input = tokenizer("hello", return_tensors="pt").to(model.device)
with torch.no_grad():
model.generate(**warmup_input, max_new_tokens=10)
# 正式测试
start_total = time.time()
total_gen_tokens = 0
first_latencies = []
for prompt in TEST_PROMPTS:
messages = [{"role": "user", "content": prompt}]
text = tokenizer.apply_chat_template(
messages, tokenize=False, add_generation_prompt=True
)
inputs = tokenizer(text, return_tensors="pt").to(model.device)
input_len = inputs["input_ids"].shape[1]
# 首 token
t0 = time.time()
with torch.no_grad():
out1 = model.generate(**inputs, max_new_tokens=1, do_sample=False)
first_latencies.append(time.time() - t0)
# 完整生成
with torch.no_grad():
out_full = model.generate(
**inputs, max_new_tokens=128, do_sample=False
)
total_gen_tokens += out_full.shape[1] - input_len
total_time = time.time() - start_total
speed_results[precision] = {
"tokens_per_sec": round(total_gen_tokens / total_time, 1),
"avg_first_token_ms": round(
sum(first_latencies) / len(first_latencies) * 1000, 1
),
}
print(f" tokens/s: {speed_results[precision]['tokens_per_sec']}")
print(f" 首token延迟: {speed_results[precision]['avg_first_token_ms']}ms")
del model
torch.cuda.empty_cache()
gc.collect()Step 3:质量评估(25 分钟)
# === 评估数据集 ===
# 1. GSM8K 数学推理(5题)
gsm8k_problems = [
{
"question": "小明有15个苹果,他给了小红3个,又买了7个。他现在有多少个苹果?",
"answer": 19
},
{
"question": "一个教室有35个学生,其中男生比女生多5人。男生有多少人?",
"answer": 20
},
{
"question": "火车以每小时120公里的速度行驶,3.5小时能走多少公里?",
"answer": 420
},
{
"question": "商店里一件衣服原价200元,打八折后又减30元,最终价格是多少?",
"answer": 130
},
{
"question": "一个长方形的长是12厘米,宽是8厘米。它的周长和面积分别是多少?",
"answer": "周长40厘米,面积96平方厘米"
},
]
# 2. 指令跟随(5条)
instruction_prompts = [
"请用恰好三个词概括机器学习。",
"请以JSON格式输出以下信息:姓名张三,年龄25,职业工程师。",
"请列出5个中国历史朝代,每个朝代用一句话描述其特点。",
"请将以下句子改写为疑问句:大语言模型正在改变世界。",
"请写一段不超过50字的产品描述,产品是一款智能手表。",
]
# 3. 中文理解/生成(5条)
chinese_prompts = [
"请解释成语'画龙点睛'的含义,并造一个句子。",
"请为一家新开的咖啡店写一段开业宣传语,风格温馨文艺。",
"请分析鲁迅《狂人日记》的主题思想。",
"请比较唐诗和宋词在风格上的主要差异。",
"请用通俗的语言解释'内卷'这个网络用语的含义。",
]
def evaluate_model(model, tokenizer, prompts, max_new_tokens=256):
"""生成模型回复"""
responses = []
for prompt in prompts:
if isinstance(prompt, dict):
content = prompt["question"]
else:
content = prompt
messages = [{"role": "user", "content": content}]
text = tokenizer.apply_chat_template(
messages, tokenize=False, add_generation_prompt=True
)
inputs = tokenizer(text, return_tensors="pt").to(model.device)
with torch.no_grad():
output = model.generate(
**inputs,
max_new_tokens=max_new_tokens,
do_sample=False,
temperature=1.0,
)
response = tokenizer.decode(
output[0][inputs["input_ids"].shape[1]:],
skip_special_tokens=True
)
responses.append(response)
return responses
# 收集所有精度的回复
all_responses = {}
for precision in ["fp16", "int8", "int4"]:
print(f"\n评估 {precision.upper()} 模型...")
model, _ = load_model(precision)
all_responses[precision] = {
"gsm8k": evaluate_model(model, tokenizer, gsm8k_problems),
"instruction": evaluate_model(model, tokenizer, instruction_prompts),
"chinese": evaluate_model(model, tokenizer, chinese_prompts),
}
del model
torch.cuda.empty_cache()
gc.collect()
# 保存回复用于后续 LLM-as-Judge 评分
with open("quantization_responses.json", "w", encoding="utf-8") as f:
json.dump(all_responses, f, ensure_ascii=False, indent=2)Step 4:LLM-as-Judge 评分
# 使用强模型(如 Qwen3-32B API)进行评分
# 如果没有 API,可以用本地加载的 FP16 模型作为 Judge
JUDGE_PROMPT_TEMPLATE = """请你作为一个公正的评委,对以下AI助手的回复进行评分。
评分标准(1-10分):
- 准确性:回复内容是否正确
- 完整性:是否充分回答了问题
- 格式规范:是否遵循了指令要求的格式
- 语言质量:表达是否清晰、自然
用户问题:
{question}
AI回复:
{response}
请给出评分(1-10分)和简要评语。格式:
评分:X/10
评语:...
"""
def judge_responses(questions, responses_by_precision, judge_model=None):
"""使用 LLM-as-Judge 评分"""
scores = {p: [] for p in responses_by_precision}
for i, question in enumerate(questions):
q_text = question["question"] if isinstance(question, dict) else question
for precision, responses in responses_by_precision.items():
prompt = JUDGE_PROMPT_TEMPLATE.format(
question=q_text,
response=responses[i]
)
# 方式1:调用 API
# score = call_judge_api(prompt)
# 方式2:使用本地模型
if judge_model:
messages = [{"role": "user", "content": prompt}]
text = tokenizer.apply_chat_template(
messages, tokenize=False, add_generation_prompt=True
)
inputs = tokenizer(text, return_tensors="pt").to(judge_model.device)
with torch.no_grad():
output = judge_model.generate(
**inputs, max_new_tokens=200, do_sample=False
)
judge_response = tokenizer.decode(
output[0][inputs["input_ids"].shape[1]:],
skip_special_tokens=True
)
# 从评分中提取分数
import re
match = re.search(r'评分[::]\s*(\d+)', judge_response)
score = int(match.group(1)) if match else 5
scores[precision].append(score)
return scores
# 运行评估(使用 FP16 模型作为 Judge)
print("加载 Judge 模型(FP16)...")
judge_model, _ = load_model("fp16")
for task_name, questions in [
("gsm8k", gsm8k_problems),
("instruction", instruction_prompts),
("chinese", chinese_prompts),
]:
task_responses = {p: all_responses[p][task_name] for p in all_responses}
scores = judge_responses(questions, task_responses, judge_model)
print(f"\n{task_name} 评分结果:")
print(f"{'精度':<10} {'平均分':<10} {'各题分数'}")
for p, s in scores.items():
avg = sum(s) / len(s) if s else 0
print(f"{p.upper():<10} {avg:<10.1f} {s}")结果记录模板
# 汇总所有结果
print("\n" + "="*70)
print("量化实验结果汇总")
print("="*70)
print(f"\n{'指标':<20} {'FP16':<15} {'INT8':<15} {'INT4':<15}")
print("-"*65)
print(f"{'显存 (GB)':<20} {results['fp16']['memory_gb']:<15} "
f"{results['int8']['memory_gb']:<15} {results['int4']['memory_gb']:<15}")
print(f"{'加载时间 (s)':<20} {results['fp16']['load_time_s']:<15} "
f"{results['int8']['load_time_s']:<15} {results['int4']['load_time_s']:<15}")
print(f"{'tokens/s':<20} {speed_results['fp16']['tokens_per_sec']:<15} "
f"{speed_results['int8']['tokens_per_sec']:<15} "
f"{speed_results['int4']['tokens_per_sec']:<15}")
print(f"{'首token延迟 (ms)':<20} {speed_results['fp16']['avg_first_token_ms']:<15} "
f"{speed_results['int8']['avg_first_token_ms']:<15} "
f"{speed_results['int4']['avg_first_token_ms']:<15}")实验 B:微调后量化与部署(选做)
量化实验的延伸——将前序课程微调的模型进行静态量化,对比量化前后的质量损失,并完成从训练到部署的完整闭环。
实验目标
| 项目 | 内容 |
|---|---|
| 前置依赖 | 第2-4课任一微调实验的 LoRA adapter |
| 实验目标 | 掌握"微调 → 合并 → 静态量化 → 部署"全流程 |
| 预计时长 | ~50 分钟 |
| 硬件要求 | A100-40G 或同等 GPU |
与实验 A 的关系:实验 A 测试的是推理时动态量化(bitsandbytes 在加载时即时量化),本实验做的是离线静态量化(GPTQ/AWQ 预先量化并保存权重)。静态量化是生产部署的标准做法,量化后的模型可以直接分发,无需原始 FP16 权重。
背景:从训练到部署的完整链路
为什么需要静态量化而不是直接用 bitsandbytes?
- bitsandbytes 的动态量化每次加载都要重新量化,启动慢
- 静态量化(GPTQ/AWQ)使用校准数据集优化量化参数,精度更高
- 静态量化的模型可以被 vLLM、TGI、llama.cpp 等推理框架直接加载
- 量化后的模型文件可以直接上传 HuggingFace Hub 供他人使用
实验步骤
环境准备
# 安装依赖(在实验 A 的基础上额外安装)
# pip install peft auto-gptq optimum autoawq
import torch
import time
import json
import gc
from pathlib import Path
from transformers import AutoModelForCausalLM, AutoTokenizer
# 验证 GPU
print(f"GPU: {torch.cuda.get_device_name(0)}")
print(f"GPU 显存: {torch.cuda.get_device_properties(0).total_mem / 1024**3:.1f} GB")Step 1:合并 LoRA Adapter(10 分钟)
如果你在第2-4课使用 LoRA/QLoRA 进行了微调,需要先将 adapter 合并回基座模型,得到一个完整的 FP16 模型。
from peft import PeftModel, PeftConfig
from transformers import AutoModelForCausalLM, AutoTokenizer
import torch
# ============================
# 配置区:请根据你的实际情况修改
# ============================
BASE_MODEL_NAME = "Qwen/Qwen3-8B" # 微调时使用的基座模型
ADAPTER_PATH = "./my-sft-adapter" # 你的 LoRA adapter 路径
MERGED_OUTPUT_PATH = "./qwen3-8b-sft-merged" # 合并后模型的保存路径
# Step 1.1:加载基座模型(在 CPU 上操作以节省显存)
print("正在加载基座模型...")
base_model = AutoModelForCausalLM.from_pretrained(
BASE_MODEL_NAME,
torch_dtype=torch.float16,
device_map="cpu", # 合并在 CPU 上做,避免显存不足
)
tokenizer = AutoTokenizer.from_pretrained(BASE_MODEL_NAME)
print(f"基座模型加载完成,参数量: {sum(p.numel() for p in base_model.parameters()) / 1e9:.2f}B")
# Step 1.2:加载 LoRA adapter
print("正在加载 LoRA adapter...")
peft_config = PeftConfig.from_pretrained(ADAPTER_PATH)
print(f"LoRA 配置: r={peft_config.r}, alpha={peft_config.lora_alpha}, "
f"目标模块={peft_config.target_modules}")
model = PeftModel.from_pretrained(base_model, ADAPTER_PATH)
# 查看可训练参数比例
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 / 1e6:.1f}M / {total / 1e6:.1f}M "
f"({trainable / total * 100:.2f}%)")
# Step 1.3:合并并卸载 adapter
print("正在合并 LoRA 权重到基座模型...")
merged_model = model.merge_and_unload()
# Step 1.4:保存合并后的完整模型
print(f"正在保存合并后的模型到 {MERGED_OUTPUT_PATH}...")
merged_model.save_pretrained(MERGED_OUTPUT_PATH)
tokenizer.save_pretrained(MERGED_OUTPUT_PATH)
# 验证保存的文件
saved_files = list(Path(MERGED_OUTPUT_PATH).glob("*"))
total_size = sum(f.stat().st_size for f in saved_files if f.is_file()) / 1024**3
print(f"保存完成!文件数: {len(saved_files)}, 总大小: {total_size:.2f} GB")
# 释放内存
del base_model, model, merged_model
gc.collect()没有 adapter 怎么办? 如果你没有完成前序课程的微调实验,可以:1) 直接使用 Qwen/Qwen3-8B 跳过合并步骤,从 Step 2 开始;2) 使用 HuggingFace Hub 上的社区微调模型;3) 快速用第2课的代码做一个简单的 SFT(10 分钟即可)。
Step 2:静态量化(20 分钟)
提供 GPTQ 和 AWQ 两种方案,选择其中一种完成即可。
准备校准数据集
静态量化需要一批"校准数据"来确定最优的量化参数。校准数据应该和你的实际使用场景尽量接近。
from transformers import AutoTokenizer
MODEL_PATH = "./qwen3-8b-sft-merged" # 或 BASE_MODEL_NAME(如果跳过了 Step 1)
tokenizer = AutoTokenizer.from_pretrained(MODEL_PATH)
# 校准数据:覆盖多种任务类型
calibration_texts = [
# 数学推理
"小明有15个苹果,他给了小红3个,又买了7个。请问他现在有多少个苹果?让我们一步步思考:首先小明有15个苹果,给出3个后剩12个,再买7个,所以是12+7=19个。",
"一个教室有35个学生,其中男生比女生多5人。设女生有x人,则男生有x+5人,x+(x+5)=35,解得x=15,所以男生有20人。",
# 指令跟随
"请以JSON格式输出以下信息:姓名张三,年龄25,职业工程师。\n```json\n{\"name\": \"张三\", \"age\": 25, \"occupation\": \"工程师\"}\n```",
"请用恰好三个词概括机器学习:数据驱动预测。",
# 中文理解
"请解释成语'画龙点睛'的含义:画龙点睛原指画龙时最后点上眼睛使之活灵活现,比喻在关键处用几句话点明主旨,使内容更加生动传神。",
"请分析鲁迅《狂人日记》的主题思想:《狂人日记》通过一个'狂人'的视角,深刻揭示了封建礼教'吃人'的本质,是中国现代文学史上第一篇白话小说。",
# 代码生成
"用Python写一个快速排序算法:\ndef quicksort(arr):\n if len(arr) <= 1:\n return arr\n pivot = arr[len(arr) // 2]\n left = [x for x in arr if x < pivot]\n middle = [x for x in arr if x == pivot]\n right = [x for x in arr if x > pivot]\n return quicksort(left) + middle + quicksort(right)",
"用Python实现二分查找:\ndef binary_search(arr, target):\n left, right = 0, len(arr) - 1\n while left <= right:\n mid = (left + right) // 2\n if arr[mid] == target:\n return mid\n elif arr[mid] < target:\n left = mid + 1\n else:\n right = mid - 1\n return -1",
# 翻译
"请将以下中文翻译成英文:人工智能正在深刻改变我们的生活方式。\nArtificial intelligence is profoundly changing the way we live.",
# 知识问答
"什么是知识蒸馏?知识蒸馏是一种模型压缩技术,通过让小模型(学生)学习大模型(教师)的输出分布来转移知识,使小模型获得接近大模型的性能。",
"什么是LoRA?LoRA(Low-Rank Adaptation)是一种参数高效微调方法,通过在预训练权重矩阵旁添加低秩分解矩阵来实现微调,大幅减少可训练参数量。",
"解释梯度下降法:梯度下降是一种优化算法,通过计算损失函数对参数的梯度,沿梯度反方向更新参数,逐步找到损失函数的最小值。",
"为什么大语言模型需要后训练?预训练让模型学会了语言知识,但后训练(包括SFT、RLHF等)让模型学会遵循人类指令、对齐人类偏好,成为真正有用的AI助手。",
]
def prepare_calibration_data(texts, tokenizer, max_length=512):
"""准备校准数据集"""
calibration_data = []
for text in texts:
messages = [{"role": "user", "content": text}]
formatted = tokenizer.apply_chat_template(
messages, tokenize=False, add_generation_prompt=True
)
tokenized = tokenizer(
formatted,
return_tensors="pt",
max_length=max_length,
truncation=True,
padding="max_length",
)
calibration_data.append(tokenized)
return calibration_data
calibration_data = prepare_calibration_data(calibration_texts, tokenizer)
print(f"校准数据集准备完成:{len(calibration_data)} 条样本")GPTQ 使用二阶信息(Hessian 矩阵近似)逐层量化,量化质量在学术评测中通常优于 AWQ,但速度较慢。
from transformers import AutoModelForCausalLM, AutoTokenizer
from auto_gptq import AutoGPTQForCausalLM, BaseQuantizeConfig
import torch
MODEL_PATH = "./qwen3-8b-sft-merged"
GPTQ_OUTPUT = "./qwen3-8b-sft-gptq-int4"
# GPTQ 量化配置
quantize_config = BaseQuantizeConfig(
bits=4, # 量化位数:4-bit
group_size=128, # 分组量化的组大小,128 是常用值
damp_percent=0.1, # 阻尼系数,防止 Hessian 矩阵奇异
desc_act=False, # 是否按激活值降序排列列,True 更准但更慢
sym=True, # 对称量化
)
print("正在加载模型用于 GPTQ 量化...")
tokenizer = AutoTokenizer.from_pretrained(MODEL_PATH)
model = AutoGPTQForCausalLM.from_pretrained(
MODEL_PATH,
quantize_config=quantize_config,
)
# 准备校准数据(GPTQ 格式)
gptq_calibration = []
for text in calibration_texts:
tokenized = tokenizer(text, return_tensors="pt", truncation=True, max_length=512)
gptq_calibration.append(tokenized.input_ids)
# 执行量化
print("开始 GPTQ 量化(8B 模型约需 20-40 分钟)...")
start_time = time.time()
model.quantize(gptq_calibration)
quant_time = time.time() - start_time
print(f"GPTQ 量化完成,耗时: {quant_time / 60:.1f} 分钟")
# 保存量化模型
model.save_quantized(GPTQ_OUTPUT)
tokenizer.save_pretrained(GPTQ_OUTPUT)
# 检查量化后的文件大小
quant_size = sum(
f.stat().st_size for f in Path(GPTQ_OUTPUT).glob("*") if f.is_file()
) / 1024**3
print(f"量化后模型大小: {quant_size:.2f} GB")
del model
torch.cuda.empty_cache()
gc.collect()AWQ(Activation-aware Weight Quantization)通过分析激活值分布来保护重要权重通道,量化速度显著快于 GPTQ。
from awq import AutoAWQForCausalLM
from transformers import AutoTokenizer
MODEL_PATH = "./qwen3-8b-sft-merged"
AWQ_OUTPUT = "./qwen3-8b-sft-awq-int4"
# 加载模型
print("正在加载模型用于 AWQ 量化...")
model = AutoAWQForCausalLM.from_pretrained(MODEL_PATH)
tokenizer = AutoTokenizer.from_pretrained(MODEL_PATH)
# AWQ 量化配置
quant_config = {
"zero_point": True, # 是否使用零点量化
"q_group_size": 128, # 分组大小
"w_bit": 4, # 量化位数
"version": "GEMM", # 量化内核版本:GEMM 兼容性好
}
# 执行量化(AWQ 速度比 GPTQ 快很多)
print("开始 AWQ 量化...")
start_time = time.time()
model.quantize(tokenizer, quant_config=quant_config)
quant_time = time.time() - start_time
print(f"AWQ 量化完成,耗时: {quant_time / 60:.1f} 分钟")
# 保存
model.save_quantized(AWQ_OUTPUT)
tokenizer.save_pretrained(AWQ_OUTPUT)
quant_size = sum(
f.stat().st_size for f in Path(AWQ_OUTPUT).glob("*") if f.is_file()
) / 1024**3
print(f"量化后模型大小: {quant_size:.2f} GB")
del model
torch.cuda.empty_cache()
gc.collect()Step 3:量化后质量评估(15 分钟)
对比微调模型在量化前后的质量变化。
import re
import os
import torch
from transformers import AutoModelForCausalLM, AutoTokenizer
from auto_gptq import AutoGPTQForCausalLM # 如果用了 GPTQ
# 模型路径配置
MODELS = {
"sft-fp16": {
"path": "./qwen3-8b-sft-merged",
"loader": "hf",
"desc": "微调后 FP16(未量化)",
},
"sft-gptq-int4": {
"path": "./qwen3-8b-sft-gptq-int4",
"loader": "gptq",
"desc": "微调后 GPTQ-INT4",
},
# 如果也做了 AWQ,可以加上:
# "sft-awq-int4": {
# "path": "./qwen3-8b-sft-awq-int4",
# "loader": "awq",
# "desc": "微调后 AWQ-INT4",
# },
}
def load_model_by_config(config):
"""根据配置加载不同格式的模型"""
torch.cuda.empty_cache()
gc.collect()
torch.cuda.reset_peak_memory_stats()
start_time = time.time()
if config["loader"] == "hf":
model = AutoModelForCausalLM.from_pretrained(
config["path"], torch_dtype=torch.float16, device_map="auto",
)
elif config["loader"] == "gptq":
model = AutoGPTQForCausalLM.from_quantized(
config["path"], device_map="auto",
)
elif config["loader"] == "awq":
from awq import AutoAWQForCausalLM
model = AutoAWQForCausalLM.from_quantized(
config["path"], fuse_layers=False, device_map="auto",
)
else:
raise ValueError(f"未知的加载方式: {config['loader']}")
load_time = time.time() - start_time
memory_gb = torch.cuda.max_memory_allocated() / 1024**3
return model, {
"load_time_s": round(load_time, 1),
"memory_gb": round(memory_gb, 2),
}
# 评估数据集
gsm8k_problems = [
{"question": "小明有15个苹果,他给了小红3个,又买了7个。他现在有多少个苹果?", "answer": 19},
{"question": "一个教室有35个学生,其中男生比女生多5人。男生有多少人?", "answer": 20},
{"question": "火车以每小时120公里的速度行驶,3.5小时能走多少公里?", "answer": 420},
{"question": "商店里一件衣服原价200元,打八折后又减30元,最终价格是多少?", "answer": 130},
{"question": "一个长方形的长是12厘米,宽是8厘米。它的周长和面积分别是多少?", "answer": "周长40厘米,面积96平方厘米"},
]
instruction_prompts = [
"请用恰好三个词概括机器学习。",
"请以JSON格式输出以下信息:姓名张三,年龄25,职业工程师。",
"请列出5个中国历史朝代,每个朝代用一句话描述其特点。",
"请将以下句子改写为疑问句:大语言模型正在改变世界。",
"请写一段不超过50字的产品描述,产品是一款智能手表。",
]
chinese_prompts = [
"请解释成语'画龙点睛'的含义,并造一个句子。",
"请为一家新开的咖啡店写一段开业宣传语,风格温馨文艺。",
"请分析鲁迅《狂人日记》的主题思想。",
"请比较唐诗和宋词在风格上的主要差异。",
"请用通俗的语言解释'内卷'这个网络用语的含义。",
]
def generate_responses(model, tokenizer, prompts, max_new_tokens=256):
"""生成模型回复"""
responses = []
for prompt in prompts:
content = prompt["question"] if isinstance(prompt, dict) else prompt
messages = [{"role": "user", "content": content}]
text = tokenizer.apply_chat_template(
messages, tokenize=False, add_generation_prompt=True
)
inputs = tokenizer(text, return_tensors="pt").to(model.device)
with torch.no_grad():
output = model.generate(
**inputs, max_new_tokens=max_new_tokens, do_sample=False
)
response = tokenizer.decode(
output[0][inputs["input_ids"].shape[1]:], skip_special_tokens=True
)
responses.append(response)
return responses
# 收集所有模型的回复
tokenizer = AutoTokenizer.from_pretrained(list(MODELS.values())[0]["path"])
all_responses = {}
model_meta = {}
for model_name, config in MODELS.items():
print(f"\n{'='*50}")
print(f"评估模型: {config['desc']}")
model, meta = load_model_by_config(config)
model_meta[model_name] = meta
print(f" 显存: {meta['memory_gb']} GB | 加载时间: {meta['load_time_s']}s")
all_responses[model_name] = {
"gsm8k": generate_responses(model, tokenizer, gsm8k_problems),
"instruction": generate_responses(model, tokenizer, instruction_prompts),
"chinese": generate_responses(model, tokenizer, chinese_prompts),
}
print(f"\n 样例回复(GSM8K 第1题):")
print(f" {all_responses[model_name]['gsm8k'][0][:200]}...")
del model
torch.cuda.empty_cache()
gc.collect()
# 保存回复
with open("sft_quantization_responses.json", "w", encoding="utf-8") as f:
json.dump(all_responses, f, ensure_ascii=False, indent=2)
print("\n所有回复已保存到 sft_quantization_responses.json")Step 4:交叉分析与结果汇总(5 分钟)
回答关键问题:微调后的模型和原始预训练模型,谁对量化更敏感?
print("\n" + "="*70)
print("微调后量化实验 —— 结果汇总")
print("="*70)
# 资源对比
print("\n📊 资源占用对比")
print(f"{'模型':<25} {'显存(GB)':<12} {'加载时间(s)':<14}")
print("-"*55)
for m, meta in model_meta.items():
desc = MODELS[m]["desc"]
print(f"{desc:<25} {meta['memory_gb']:<12} {meta['load_time_s']:<14}")
print("\n" + "="*70)
print("💡 思考题:与实验 A 的交叉对比")
print("="*70)
print("""
如果你已完成实验 A,请对比以下数据并回答问题:
┌─────────────────┬──────────┬──────────┬──────────┐
│ │ FP16 │ INT8 │ INT4 │
├─────────────────┼──────────┼──────────┼──────────┤
│ 预训练模型(实验A) │ ?.? │ ?.? │ ?.? │
│ 微调后模型(本实验) │ ?.? │ - │ ?.? │
└─────────────────┴──────────┴──────────┴──────────┘
请填入你的实际数据,并思考:
1. 微调后模型的 FP16 得分相比预训练模型是否提升?提升了多少?
2. 量化(INT4)导致的分数下降,在微调模型和预训练模型上谁更大?
3. 即使量化后,微调模型是否仍然优于未量化的预训练模型?
4. 这些发现对实际部署决策有什么指导意义?
""")拓展(可选):导出 GGUF 并用 Ollama 部署
如果你想体验从训练到可运行的完整闭环,可以将量化模型转换为 GGUF 格式,然后用 Ollama 在本地部署。
# 安装 llama.cpp(需要先编译)
git clone https://github.com/ggerganov/llama.cpp
cd llama.cpp && make -j
# 将 HuggingFace 模型转为 GGUF 格式
python convert_hf_to_gguf.py \
../qwen3-8b-sft-merged/ \
--outfile ../qwen3-8b-sft-q4_k_m.gguf \
--outtype q4_k_m
# 用 llama.cpp 测试
./llama-cli -m ../qwen3-8b-sft-q4_k_m.gguf \
-p "请解释什么是大语言模型的后训练?" \
-n 256
# 或用 Ollama 部署
cat > Modelfile << 'EOF'
FROM ./qwen3-8b-sft-q4_k_m.gguf
PARAMETER temperature 0.7
PARAMETER top_p 0.9
SYSTEM 你是一个有用的AI助手。
EOF
ollama create my-sft-model -f Modelfile
ollama run my-sft-model实验 C:能力扩展选做(三选一)
选项 1:蒸馏模型分析
目标:分析 DeepSeek-R1-Distill-Qwen-1.5B 的推理能力和推理链风格。
import torch
from transformers import AutoModelForCausalLM, AutoTokenizer
# === Step 1: 加载蒸馏模型 ===
distill_model_name = "deepseek-ai/DeepSeek-R1-Distill-Qwen-1.5B"
distill_model = AutoModelForCausalLM.from_pretrained(
distill_model_name,
torch_dtype=torch.bfloat16,
device_map="auto",
)
distill_tokenizer = AutoTokenizer.from_pretrained(distill_model_name)
print(f"蒸馏模型参数量: {sum(p.numel() for p in distill_model.parameters()) / 1e9:.2f}B")
print(f"显存占用: {torch.cuda.max_memory_allocated() / 1024**3:.2f} GB")
# === Step 2: GSM8K 评估 ===
gsm8k_test = [
{"question": "一个商店卖苹果,每斤5元。小明买了3斤苹果,付了20元,应找回多少钱?", "answer": 5},
{"question": "一列火车有12节车厢,每节车厢坐60人。如果火车满员,共有多少乘客?", "answer": 720},
{"question": "甲、乙两人合作完成一项工作,甲单独做需10天,乙单独做需15天。两人合作几天能完成?", "answer": 6},
{"question": "一个正方形的周长是24厘米,它的面积是多少平方厘米?", "answer": 36},
{"question": "小红有一些糖果,她给了小明8颗后还剩12颗。她原来有多少颗糖果?", "answer": 20},
{"question": "3个箱子共有45个球,第一个箱子比第二个多5个,第二个比第三个多5个。每个箱子各有多少球?", "answer": "20,15,10"},
{"question": "一本书共300页,小明第一天读了全书的1/5,第二天读了剩下的1/4。第二天读了多少页?", "answer": 60},
{"question": "把一根绳子对折3次后剪一刀,绳子被剪成几段?", "answer": 9},
{"question": "一个数的3倍减去7等于20,这个数是多少?", "answer": 9},
{"question": "长方形花坛长8米,宽5米。沿花坛外侧修一条1米宽的小路,小路面积是多少?", "answer": 32},
]
correct = 0
for i, problem in enumerate(gsm8k_test):
messages = [{"role": "user", "content": problem["question"]}]
text = distill_tokenizer.apply_chat_template(
messages, tokenize=False, add_generation_prompt=True
)
inputs = distill_tokenizer(text, return_tensors="pt").to(distill_model.device)
with torch.no_grad():
output = distill_model.generate(
**inputs, max_new_tokens=1024, do_sample=False, temperature=1.0
)
response = distill_tokenizer.decode(
output[0][inputs["input_ids"].shape[1]:], skip_special_tokens=True
)
print(f"\n问题 {i+1}: {problem['question']}")
print(f"回复: {response[:300]}...")
print(f"标准答案: {problem['answer']}")
# 简单检查答案是否正确
if str(problem["answer"]) in response:
correct += 1
print("✓ 正确")
else:
print("✗ 可能错误(需人工验证)")
print(f"\n准确率: {correct}/{len(gsm8k_test)} = {correct/len(gsm8k_test)*100:.1f}%")
# === Step 3: 分析推理链风格 ===
print("\n" + "="*60)
print("推理链风格分析")
print("="*60)
analysis_prompt = "一个工厂第一天生产了120件产品,第二天比第一天多生产20%,第三天生产的数量是前两天总和的一半。三天共生产了多少件产品?"
messages = [{"role": "user", "content": analysis_prompt}]
text = distill_tokenizer.apply_chat_template(
messages, tokenize=False, add_generation_prompt=True
)
inputs = distill_tokenizer(text, return_tensors="pt").to(distill_model.device)
with torch.no_grad():
output = distill_model.generate(
**inputs, max_new_tokens=2048, do_sample=False
)
full_response = distill_tokenizer.decode(
output[0][inputs["input_ids"].shape[1]:], skip_special_tokens=True
)
print(f"完整推理链:\n{full_response}")
print(f"\n推理链长度: {len(full_response)} 字符")
# 检查推理链特征
features = {
"有逐步计算": "步" in full_response or "step" in full_response.lower(),
"有自我验证": "验证" in full_response or "检查" in full_response,
"有think标签": "<think>" in full_response,
"有回溯修正": "不对" in full_response or "重新" in full_response,
}
print("\n推理链特征:")
for feat, present in features.items():
print(f" {feat}: {'是' if present else '否'}")与第4课 GRPO 模型对比:如果你在第4课完成了 GRPO 训练实验,请对比蒸馏模型和 GRPO 模型的推理链风格。蒸馏模型通常更"流畅"但可能不够"深入",而 GRPO 模型可能出现更多"探索性"推理。
选项 2:迷你多模态实验
目标:使用 Qwen3-VL-2B-Instruct 测试视觉理解能力并评估幻觉率。
import torch
from transformers import Qwen2_5_VLForConditionalGeneration, AutoProcessor
from PIL import Image
import requests
from io import BytesIO
# === Step 1: 加载 VLM 模型 ===
vl_model_name = "Qwen/Qwen3-VL-2B-Instruct"
vl_model = Qwen2_5_VLForConditionalGeneration.from_pretrained(
vl_model_name,
torch_dtype=torch.bfloat16,
device_map="auto",
)
processor = AutoProcessor.from_pretrained(vl_model_name)
print(f"VLM 模型已加载,显存: {torch.cuda.max_memory_allocated() / 1024**3:.2f} GB")
# === Step 2: 准备测试图片 ===
# 使用公开的测试图片 URL
test_images = [
{
"url": "https://upload.wikimedia.org/wikipedia/commons/thumb/4/47/PNG_transparency_demonstration_1.png/280px-PNG_transparency_demonstration_1.png",
"description": "带透明背景的骰子图片",
"questions": [
"请描述图片中的内容。",
"图片中有几个骰子?",
"图片中有猫吗?", # 幻觉测试
]
},
# 可以添加更多测试图片...
]
# 也可以使用本地图片
def load_image(image_source):
"""加载图片,支持 URL 和本地路径"""
if image_source.startswith("http"):
response = requests.get(image_source)
return Image.open(BytesIO(response.content)).convert("RGB")
else:
return Image.open(image_source).convert("RGB")
# === Step 3: 视觉问答测试 ===
def vqa_test(image, question):
"""对图片进行视觉问答"""
messages = [
{
"role": "user",
"content": [
{"type": "image", "image": image},
{"type": "text", "text": question},
],
}
]
text = processor.apply_chat_template(
messages, tokenize=False, add_generation_prompt=True
)
inputs = processor(
text=[text], images=[image], return_tensors="pt"
).to(vl_model.device)
with torch.no_grad():
output = vl_model.generate(**inputs, max_new_tokens=256, do_sample=False)
response = processor.decode(output[0], skip_special_tokens=True)
# 提取助手回复部分
if "assistant" in response:
response = response.split("assistant")[-1].strip()
return response
# === Step 4: 运行测试 ===
print("\n视觉问答测试结果:")
print("="*60)
for img_info in test_images:
try:
image = load_image(img_info["url"])
print(f"\n图片: {img_info['description']}")
for question in img_info["questions"]:
response = vqa_test(image, question)
print(f"\n Q: {question}")
print(f" A: {response[:200]}")
except Exception as e:
print(f"加载图片失败: {e}")
continue
# === Step 5: 幻觉测试 ===
print("\n" + "="*60)
print("幻觉测试")
print("="*60)
# 准备幻觉测试问题(对不存在的内容提问)
hallucination_questions = [
"图片中的文字写了什么?", # 如果图中没有文字
"图片中的人在做什么?", # 如果图中没有人
"图片中有几辆汽车?", # 如果图中没有汽车
"图片背景中的建筑是什么风格?", # 如果没有建筑
"图片中动物的品种是什么?", # 如果没有动物
]
hallucination_count = 0
total_tests = 0
for img_info in test_images:
try:
image = load_image(img_info["url"])
for question in hallucination_questions:
response = vqa_test(image, question)
total_tests += 1
# 检查是否产生幻觉(模型编造了不存在的内容)
refusal_keywords = ["没有", "不存在", "看不到", "无法", "图中没有", "not"]
is_refusal = any(kw in response for kw in refusal_keywords)
if not is_refusal:
hallucination_count += 1
status = "⚠️ 可能幻觉"
else:
status = "✓ 正确拒绝"
print(f" Q: {question}")
print(f" A: {response[:150]}")
print(f" 状态: {status}")
except Exception as e:
continue
if total_tests > 0:
print(f"\n幻觉率: {hallucination_count}/{total_tests} "
f"= {hallucination_count/total_tests*100:.1f}%")选项 3:迷你工具使用实验
目标:测试 Qwen3-1.7B 的函数调用能力,构建简单的智能体循环。
import torch
import json
import re
from transformers import AutoModelForCausalLM, AutoTokenizer
# === Step 1: 加载模型 ===
model_name = "Qwen/Qwen3-1.7B"
model = AutoModelForCausalLM.from_pretrained(
model_name,
torch_dtype=torch.bfloat16,
device_map="auto",
)
tokenizer = AutoTokenizer.from_pretrained(model_name)
# === Step 2: 定义工具 ===
tools = [
{
"type": "function",
"function": {
"name": "calculator",
"description": "一个简单的计算器,可以执行数学运算",
"parameters": {
"type": "object",
"properties": {
"expression": {
"type": "string",
"description": "数学表达式,如 '2 + 3 * 4'"
}
},
"required": ["expression"]
}
}
},
{
"type": "function",
"function": {
"name": "get_weather",
"description": "获取指定城市的天气信息",
"parameters": {
"type": "object",
"properties": {
"city": {
"type": "string",
"description": "城市名称"
}
},
"required": ["city"]
}
}
},
{
"type": "function",
"function": {
"name": "search_knowledge",
"description": "搜索知识库获取信息",
"parameters": {
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "搜索关键词"
}
},
"required": ["query"]
}
}
},
]
# === Step 3: 模拟工具执行 ===
def execute_tool(name, arguments):
"""模拟工具执行"""
if name == "calculator":
try:
expr = arguments.get("expression", "")
# 安全地计算数学表达式
result = eval(expr, {"__builtins__": {}}, {})
return json.dumps({"result": result})
except Exception as e:
return json.dumps({"error": str(e)})
elif name == "get_weather":
city = arguments.get("city", "未知")
# 模拟天气数据
mock_weather = {
"北京": {"temp": 22, "condition": "晴", "humidity": 35},
"上海": {"temp": 26, "condition": "多云", "humidity": 65},
"深圳": {"temp": 30, "condition": "阵雨", "humidity": 80},
}
weather = mock_weather.get(city, {"temp": 20, "condition": "晴", "humidity": 50})
return json.dumps(weather, ensure_ascii=False)
elif name == "search_knowledge":
query = arguments.get("query", "")
# 模拟知识库搜索
knowledge = {
"LoRA": "LoRA是一种参数高效微调方法,通过低秩分解减少可训练参数。",
"量化": "量化将模型权重从高精度压缩到低精度,减少显存占用。",
"DPO": "DPO是直接偏好优化方法,无需奖励模型即可进行偏好对齐。",
}
for key, value in knowledge.items():
if key in query:
return json.dumps({"result": value}, ensure_ascii=False)
return json.dumps({"result": "未找到相关信息"}, ensure_ascii=False)
return json.dumps({"error": "未知工具"})
# === Step 4: 智能体循环 ===
def agent_loop(user_query, max_turns=3):
"""完整的智能体循环"""
messages = [
{"role": "system", "content": "你是一个有用的助手,可以使用工具来回答问题。"},
{"role": "user", "content": user_query},
]
print(f"\n用户: {user_query}")
for turn in range(max_turns):
# 生成回复
text = tokenizer.apply_chat_template(
messages, tools=tools, tokenize=False, add_generation_prompt=True
)
inputs = tokenizer(text, return_tensors="pt").to(model.device)
with torch.no_grad():
output = model.generate(
**inputs, max_new_tokens=512, do_sample=False
)
response_text = tokenizer.decode(
output[0][inputs["input_ids"].shape[1]:], skip_special_tokens=True
)
print(f"\n助手 (Turn {turn+1}): {response_text[:300]}")
# 检查是否包含工具调用
tool_call_match = re.search(
r'<tool_call>\s*(\{.*?\})\s*</tool_call>',
response_text, re.DOTALL
)
if tool_call_match:
try:
tool_call = json.loads(tool_call_match.group(1))
tool_name = tool_call.get("name", "")
tool_args = tool_call.get("arguments", {})
print(f" → 调用工具: {tool_name}({json.dumps(tool_args, ensure_ascii=False)})")
# 执行工具
result = execute_tool(tool_name, tool_args)
print(f" ← 工具返回: {result}")
# 将工具调用和结果添加到消息中
messages.append({"role": "assistant", "content": response_text})
messages.append({
"role": "tool",
"name": tool_name,
"content": result
})
except json.JSONDecodeError:
print(" ⚠️ 工具调用解析失败")
break
else:
# 没有工具调用,直接返回
print(f"\n最终回复: {response_text}")
return response_text
return "达到最大轮数"
# === Step 5: 测试场景 ===
test_queries = [
"请帮我计算 (15 + 27) * 3 - 40 的结果。",
"北京今天天气怎么样?",
"帮我查一下 LoRA 是什么?",
"上海的温度是多少?如果温度超过25度请提醒我带伞。",
"请帮我计算一个圆的面积,半径是5厘米。(使用3.14159计算)",
]
correct = 0
total = len(test_queries)
for query in test_queries:
print("\n" + "="*60)
result = agent_loop(query)
# 人工判断是否正确(可自动化)
print(f"\n请评估此回复是否正确 (y/n): ", end="")
# 在实际实验中可以记录结果
# === Step 6: 与小模型对比(可选)===
print("\n\n" + "="*60)
print("与 Qwen3-0.6B 对比测试")
print("="*60)
# 加载 0.6B 模型
small_model_name = "Qwen/Qwen3-0.6B"
small_model = AutoModelForCausalLM.from_pretrained(
small_model_name,
torch_dtype=torch.bfloat16,
device_map="auto",
)
small_tokenizer = AutoTokenizer.from_pretrained(small_model_name)
# 用相同的测试场景对比
for query in test_queries[:3]: # 测试前3个
print(f"\n查询: {query}")
messages = [
{"role": "system", "content": "你是一个有用的助手,可以使用工具来回答问题。"},
{"role": "user", "content": query},
]
text = small_tokenizer.apply_chat_template(
messages, tools=tools, tokenize=False, add_generation_prompt=True
)
inputs = small_tokenizer(text, return_tensors="pt").to(small_model.device)
with torch.no_grad():
output = small_model.generate(
**inputs, max_new_tokens=512, do_sample=False
)
response = small_tokenizer.decode(
output[0][inputs["input_ids"].shape[1]:], skip_special_tokens=True
)
print(f"Qwen3-0.6B 回复: {response[:200]}")模型规模对工具使用的影响:实验中你可能会发现,1.7B 模型的函数调用准确率显著高于 0.6B 模型。工具使用需要模型同时具备理解用户意图、选择合适工具、生成正确参数格式三项能力,这对小模型来说是较大挑战。
交付物清单
完成本次实验后,请提交以下内容:
实验 A:量化实验报告(必做)
- 三种精度的显存占用对比表
- 推理速度对比表(tokens/s、首 token 延迟)
- 三类任务的质量评分对比表(GSM8K、指令跟随、中文任务)
- "压缩率 vs 质量保持率"的关系图
- 简要分析:量化对哪类任务影响最大?为什么?
实验 B:微调后量化报告(选做)
- LoRA adapter 合并过程记录(adapter 配置、参数量对比)
- GPTQ 或 AWQ 量化配置与耗时记录
- 量化前后的文件大小、显存占用对比表
- 三类任务的 LLM-as-Judge 评分对比(量化前 vs 量化后)
- 交叉分析表:预训练模型量化损失 vs 微调模型量化损失
- 回答:微调后的模型对量化更敏感还是更鲁棒?你的证据是什么?
实验 C:选做实验报告(三选一)
- 选项 1:蒸馏模型的推理链分析、与 GRPO 模型的风格对比
- 选项 2:VLM 的视觉问答结果、幻觉率统计
- 选项 3:函数调用准确率、智能体循环运行示例
总结反思(1 页)
- 后训练各环节(SFT → DPO → GRPO → 量化/蒸馏)的关系与选择策略
- 对于一个实际的 LLM 应用项目,你会如何选择后训练技术组合?
- (如完成实验 B)先量化再微调(QLoRA)和先微调再量化,各有什么优劣?