Fine-Tuning vs. Frozen Embeddings: When to Adapt the LLM vs. the Sklearn Head
TL;DR · AI 摘要
Fine-Tuning vs. Frozen Embeddings: When to Adapt the LLM vs. the Sklearn Head - MachineLearningMastery.com Fine-Tuning v...
核心要点
- 主题聚焦:Fine-Tuning vs. Frozen Embeddings: When to Adapt
- 来源:Machine Learning Mastery,建议结合原文判断细节。
- AI 分析暂不可用,本条为保底评分与摘要。
微调与冻结嵌入:何时调整大语言模型,何时调整 scikit-learn 头 - MachineLearningMastery.com
微调与冻结嵌入:何时调整大语言模型,何时调整 scikit-learn 头
作者
Iván Palomares Carrascosa
于
2026年10月27日
分类
语言模型
0
分享
文章
在本文中,你将学习如何比较两种将大语言模型适应到特定领域的方法:使用 LoRA 的微调与在冻结嵌入上训练 scikit-learn 分类器。
我们将涵盖的主题包括:
- 冻结嵌入如何作为全模型微调的轻量级替代方案,以及何时采用这种方法是合适的。
- 如何使用 PEFT 库,结合 Hugging Face 模型和分类头,实现 QLoRA 微调。
- 在真实数据集上,这两种策略在训练时间和分类准确性方面的比较。
引言
当将大语言模型(LLM)适应到特定领域时,机器学习社区通常采用的经典方法是使用特定领域的数据集对模型进行微调。但一个问题随之而来:这是否总是必要的?或者,当构建成本友好且计算资源受限的应用时,是否应采用冻结嵌入结合模型顶部的轻量级下游分类器等替代策略?
本文将对使用 LoRA(低秩适应,一种高效的权重更新算法)微调本地 LLM 与仅在冻结嵌入上训练下游 scikit-learn 分类器进行实际比较。我们将基于成本和性能分析并讨论每种策略的适用场景。
为了充分利用下面的实践示例,我们建议你对 LLM 有基本了解,并熟悉关键流程(如模型微调)以及相关库(如 Hugging Face 的 transformers)。
初始设置
如果你在 Google Colab 上运行这个实践比较,首先确保将运行环境设置为 GPU(T4),因为代码和方法针对 GPU 使用进行了优化。你只需要安装以下库:
!pip install -q -U bitsandbytes peft accelerate evaluate
1
!
pip
install
-
q
U
bitsandbytes
peft
accelerate
evaluate
否则,如果你在其他 Python 环境中,确保在继续之前已安装 scikit-learn、transformers 和 datasets。
我们首先导入一些必要的库,并从 Hugging Face 仓库加载一个经典数据集:ag_news,其中包含分类新闻文章。
import torch import numpy as np import time from datasets import load_dataset from transformers import AutoTokenizer, AutoModelForSequenceClassification, BitsAndBytesConfig # 要使用 Llama-3,请将此更改为 "meta-llama/Meta-Llama-3-8B" MODEL_ID = "Qwen/Qwen2.5-1.5B" print("1. Loading dataset...") dataset = load_dataset("fancyzhx/ag_news") # 使用一个小型子集,以便在 GPU 上几分钟内完成比较,而不是数小时 train_subset = dataset["train"].shuffle(seed=42).select(range(800)) test_subset = dataset["test"].shuffle(seed=42).select(range(200))
2
3
4
5
6
7
8
9
10
11
12
13
14
import
torch
numpy
as
np
time
from
datasets
load_dataset
transformers
AutoTokenizer
,
AutoModelForSequenceClassification
BitsAndBytesConfig
要使用 Llama-3,请将此更改为 "meta-llama/Meta-Llama-3-8B"
MODEL_ID
=
"Qwen/Qwen2.5-1.5B"
(
"1. 加载数据集..."
)
dataset
"fancyzhx/ag_news"
为加快 GPU 上的运行速度,使用一个小型子集,使比较操作可在几分钟内完成,而非数小时
train_subset
[
"train"
]
.
shuffle
seed
42
select
range
800
test_subset
"test"
200
接下来,我们加载模型实例及其相关分词器,确保其配置包含与当前任务(数据集)兼容的分类头,该任务将新闻文章分类为四个可能的类别:
print("2. 配置分词器...") tokenizer = AutoTokenizer.from_pretrained(MODEL_ID) tokenizer.pad_token = tokenizer.eos_token # 仅解码器模型需要此设置 print("3. 加载 4 位基础模型...") bnb_config = BitsAndBytesConfig( load_in_4bit=True, bnb_4bit_compute_dtype=torch.bfloat16, bnb_4bit_quant_type="nf4" ) # 加载带分类头的模型 model = AutoModelForSequenceClassification.from_pretrained( MODEL_ID, num_labels=4, quantization_config=bnb_config, device_map="auto" ) model.config.pad_token_id = tokenizer.pad_token_id
15
16
17
18
19
"2. 配置分词器..."
tokenizer
from_pretrained
pad_token
eos
_
token
仅解码器模型需要此设置
"3. 加载 4 位基础模型..."
bnb_config
load_in_4bit
True
bnb_4bit_compute_dtype
bfloat16
bnb_4bit_quant_type
"nf4"
加载带分类头的模型
model
num_labels
quantization_config
device_map
"auto"
config
pad_token_id
策略 A:冻结嵌入向量与 Scikit-learn
我们首先尝试不微调模型的替代方案。其工作原理如下:我们不重新训练 LLM,而是让它严格作为特征提取器。我们将文本通过这个“冻结模型”(权重无法更改的模型),提取最后一个隐藏状态(最终标记的嵌入向量)。这些高维嵌入向量随后被输入辅助模型——换句话说,它们用于在冻结的 LLM 上训练一个传统的轻量级分类器。
第一步是定义一个函数,内部逻辑用于从输入文本中获取冻结嵌入向量:
from sklearn.ensemble import RandomForestClassifier from sklearn.metrics import accuracy_score def extract_embeddings(texts, batch_size=8): model.eval() embeddings = [] for i in range(0, len(texts), batch_size): batch = texts[i:i+batch_size] inputs = tokenizer(batch, return_tensors="pt", padding=True, truncation=True, max_length=128).to(model.device) with torch.no_grad(): # output_hidden_states=True 强制模型返回原始向量 outputs = model(**inputs, output_hidden_states=True) last_hidden_state = outputs.hidden_states[-1] # 均值池化:对序列长度进行向量平均,忽略填充标记 mask = inputs["attention_mask"].unsqueeze(-1).float() sum_embeds = torch.sum(last_hidden_state * mask, dim=1) mean_pooled = sum_embeds / torch.clamp(mask.sum(dim=1), min=1e-9) embeddings.append(mean_pooled.cpu().numpy().astype(np.float32)) return np.concatenate(embeddings, axis=0)
20
21
22
23
24
sklearn
ensemble
RandomForestClassifier
metrics
accuracy_score
def
extract_embeddings
texts
batch_size
:
eval
embeddings
for
i
len
batch
+
inputs
return_tensors
"pt"
padding
truncation
max_length
128
to
device
with
no_grad
output_hidden_states=True 强制模型返回原始向量
outputs
*
output_hidden_states
last_hidden_state
hidden_states
/
均值池化:在序列长度上对向量进行平均,忽略填充标记
mask
"attention_mask"
unsqueeze
float
sum_embeds
sum
last_hidden_state *
dim
mean_pooled
/
clamp
min
1e
append
cpu
astype
float32
return
concatenate
axis
接下来,我们调用该函数并传入数据集样本——已拆分为训练集和测试集——从而获得用于训练和测试随机森林分类器的冻结嵌入向量:
print("提取嵌入向量...") start_time = time.time() X_train = extract_embeddings(train_subset["text"]) X_test = extract_embeddings(test_subset["text"]) y_train = train_subset["label"] y_test = test_subset["label"] embed_time = time.time() - start_time print("训练 Scikit-Learn 随机森林...") clf = RandomForestClassifier(n_estimators=100, random_state=42) start_train_time = time.time() clf.fit(X_train, y_train) sklearn_train_time = time.time() - start_train_time preds = clf.predict(X_test) sklearn_acc = accuracy_score(y_test, preds) print(f"\n--- 策略 A 结果 ---") print(f"提取时间: {embed_time:.1f}s") print(f"分类器训练时间: {sklearn_train_time:.3f}s") print(f"准确率: {sklearn_acc * 100:.1f}%")
"提取嵌入向量..."
start_time
X_train
"text"
X_test
y_train
"label"
y_test
embed_time
"训练 Scikit-Learn 随机森林..."
clf
n_estimators
100
random_state
start_train_time
fit
sklearn_train_time
preds
predict
sklearn_acc
f
"\n--- 策略 A 结果 ---"
"提取时间: {embed_time:.1f}s"
"分类器训练时间: {sklearn_train_time:.3f}s"
"准确率: {sklearn_acc * 100:.1f}%"
结果:
--- 策略 A 结果 --- 提取时间: 96.5s 分类器训练时间: 2.866s 准确率: 82.0%
--
策略
A
结果
提取
96.5s
分类器
训练
2.866s
准确率
82.0
%
尽管耗时近100秒,但大部分时间都花在了特征提取过程上——分类器本身的训练几乎可以忽略不计。
让我们看看这个冻结嵌入驱动的分类器所获得的82%测试分类准确率,是否能被微调后的LLM超越。
策略 B:LoRA 微调(QLoRA)
该策略使用低秩适配(LoRA):一种优化的微调算法,通过冻结模型庞大的预训练权重,并在Transformer层中注入可训练的“适配器”权重矩阵。该过程的复杂性归结为训练这些小型适配器矩阵,通常仅占总模型大小的1-2%,同时保持4位精度以节省内存。
前两个步骤是将适配器附加到基础模型并为微调准备数据:
from peft import LoraConfig, get_peft_model, prepare_model_for_kbit_training from transformers import TrainingArguments, Trainer import evaluate print("1. 将LoRA适配器附加到基础模型...") model = prepare_model_for_kbit_training(model) peft_config = LoraConfig( task_type="SEQ_CLS", r=16, lora_alpha=32, lora_dropout=0.05, target_modules=["q_proj", "v_proj"] # 目标注意力层 ) lora_model = get_peft_model(model, peft_config) lora_model.print_trainable_parameters() print("\n2. 为Trainer准备数据...") def tokenize_fn(examples): return tokenizer(examples["text"], padding="max_length", truncation=True, max_length=128) tokenized_train = train_subset.map(tokenize_fn, batched=True) tokenized_test = test_subset.map(tokenize_fn, batched=True) accuracy_metric = evaluate.load("accuracy") def compute_metrics(eval_pred): predictions, labels = eval_pred predictions = np.argmax(predictions, axis=1) return accuracy_metric.compute(predictions=predictions, references=labels)
25
26
27
28
LoraConfig
get_peft_model
prepare_model_for_kbit_training
TrainingArguments
Trainer
"1. 将LoRA适配器附加到基础模型..."
peft_config
task_type
"SEQ_CLS"
r
lora_alpha
32
lora_dropout
0.05
target_modules
"q_proj"
"v_proj"
目标注意力层
lora_model
print_trainable_parameters
"\n2. 为Trainer准备数据..."
tokenize_fn
examples
"max_length"
tokenized_train
map
batched
tokenized_test
accuracy_metric
load
"accuracy"
compute_metrics
eval_pred
predictions
labels
argmax
compute
references
请注意,将适配器矩阵集成到我们的基础LLM后的结果存储在一个名为lora_model的新模型变量中。
然后我们进行配置为LoRA的微调过程——它需要TrainingArguments和Trainer对象:
print("\n3. 开始QLoRA微调...") training_args = TrainingArguments( output_dir="./results", per_device_train_batch_size=8, per_device_eval_batch_size=8, learning_rate=2e-4, num_train_epochs=3, eval_strategy="epoch", logging_strategy="epoch", optim="paged_adamw_8bit", # 通过卸载优化器状态节省VRAM fp16=True, report_to="none" ) trainer = Trainer( model=lora_model, args=training_args, train_dataset=tokenized_train, eval_dataset=tokenized_test, compute_metrics=compute_metrics, ) start_time = time.time() trainer.train() lora_train_time = time.time() - start_time print("\n4. 评估QLoRA模型...") eval_results = trainer.evaluate() lora_acc = eval_results["eval_accuracy"] print(f"\n--- 策略B结果 ---") print(f"LoRA训练时间: {lora_train_time:.1f}s") print(f"准确率: {lora_acc * 100:.1f}%")
29
30
31
33
"\n3. 开始QLoRA微调..."
training_args
output_dir
"./results"
per_device_train_batch_size
per_device_eval_batch_size
learning_rate
2e
num_train_epochs
eval_strategy
"epoch"
logging_strategy
optim
"paged_adamw_8bit"
通过卸载优化器状态节省VRAM
fp16
report_to
"none"
args
train_dataset
eval_dataset
lora_train_time
"\n4. 评估QLoRA模型..."
eval_results
lora_acc
"eval_accuracy"
"\n--- 策略B结果 ---"
"LoRA训练时间: {lora_train_time:.1f}s"
"准确率: {lora_acc * 100:.1f}%"
结果(如果你运行代码会看到每个训练周期的详细结果):
--- 策略B结果 --- LoRA训练时间: 300.3s 准确率: 87.0%
B
LoRA
300.3s
87.0
分类性能确实会在我们以传统方式将模型适配到特定问题时得到提升——通过微调实现。然而,这种提升需要付出代价——整个过程耗时是原来的三倍,所有额外的时间都花在了训练过程本身。对于更大的数据集,这种时间差异可能会变得尤为明显。
结论与建议
在需要快速训练且希望最小化计算开销的同时避免主干网络过拟合的情况下,推荐将冻结的嵌入向量输入辅助的scikit-learn模型。相比之下,当缩小准确率差距对您的领域至关重要,或当模型需要学习深层次嵌入的领域特定细节时,QLoRA微调是更优选择。
对于纯生成任务(如文本和代码生成),模型适配(如微调)通常是实现专业化行为的必要操作。话虽如此,在免费层级部署中,如果对最大性能的要求略有弹性,冻结嵌入策略有时也可以作为生成工作流的可行替代方案。
更多相关内容
- Python中用于自动化机器学习的Auto-Sklearn
- 多头潜在注意力(MLA)的简明介绍
- 多头注意力的简明介绍…
- 从零实现多头注意力…
- 为什么以及何时使用句子嵌入而非词嵌入
- 使用LLM嵌入和scikit-learn构建RAG流水线
/.entry /think