?Prompt Tuning 和 P-Tuning 都屬于“軟提示”(soft prompt)范式,但 P-Tuning 首次提出用小型 LSTM/MLP 對提示嵌入進行編碼生成,而 Prompt Tuning(又稱 Soft Prompt Tuning)則直接對一段可訓練的嵌入序列做梯度更新;LoRA(Low-Rank Adaptation)通過在 Transformer 層注入兩段低秩矩陣分解,僅訓練這部分額外參數實現參數效率微調;QLoRA 則在 4-bit 量化權重上應用 LoRA,幾乎與原始 16-bit LoRA 性能持平,卻將顯存占用降低近 3 倍。以下內容將依次覆蓋各方法的原理細節、訓練流程差異,以及在 BERT 分類任務中完整的訓練、保存、加載與推理偽代碼示例。
1 Prompt Tuning vs P-Tuning
1.1 方法定義與原理
-
Prompt Tuning:在模型輸入的 embedding 層前添加 L 個可訓練向量,稱為軟提示(soft prompts),并僅對這 L 參數進行梯度更新,凍結其余預訓練模型參數 。
-
P-Tuning:《GPT Understands, Too》中提出,除使用離散提示外,還通過一個小型 LSTM 或 MLP(prompt encoder)對初始提示嵌入做變換,生成最終的連續提示嵌入,再拼接到輸入前進行訓練,提示參數由反向傳播更新 。
1.2 訓練流程對比
階段 | Prompt Tuning | P-Tuning |
---|---|---|
初始化 | 隨機或預訓練初始化 L 個提示向量 | 同上 + 初始化 LSTM/MLP 權重 |
前向傳遞 | 拼接提示向量 + 原始 embeddings → Encoder | 先 LSTM/MLP 生成提示嵌入,再拼接 + 原始 embeddings → Encoder |
反向更新 | 僅更新提示向量 | 更新提示向量與 prompt encoder 參數 |
數據存儲 | 保存提示向量矩陣 | 同上 + 保存 encoder 權重 |
?
1.3 保存與加載偽代碼
# 保存
torch.save(prompt_embeddings.state_dict(), "prompt_tuning_prompts.bin")# 加載
prompt_embeddings = nn.Parameter(torch.zeros(L, hidden_size))
prompt_embeddings.load_state_dict(torch.load("prompt_tuning_prompts.bin"))
# P-Tuning 保存
torch.save({"prompt_encoder": lstm.state_dict(),"prompt_vectors": prompt_embeddings_init
}, "p_tuning_prompt.bin")# 加載
ckpt = torch.load("p_tuning_prompt.bin")
lstm.load_state_dict(ckpt["prompt_encoder"])
prompt_embeddings_init = ckpt["prompt_vectors"]
2 LoRA vs QLoRA
2.1 LoRA(Low-Rank Adaptation)原理
LoRA 在每個 Transformer 層的線性映射 xW 的旁支引入低秩分解 與
,并用??
??替代原始變換,其中?
?為縮放系數,可訓練參數量僅為 2dr,相比全量微調可減少約 10,000 倍參數 。
2.2 QLoRA 原理
QLoRA 首先將原模型權重量化到 4-bit(如 NF4),顯存占用大幅下降;然后在量化權重上按常規方式注入 LoRA 分支,并只訓練 LoRA 分支參數。該過程兼顧了量化與低秩適配的雙重優勢,實驗證明與 16-bit LoRA 性能相當,卻將顯存占用降至三分之一左右 。
2.3 訓練與保存偽代碼
# LoRA 注入示例(略)后,僅啟用 A,B 子模塊
for name,p in model.named_parameters():p.requires_grad = ('lora_A' in name or 'lora_B' in name)# 訓練循環
for batch in dataloader:outputs = model(**batch)loss = criterion(outputs.logits, batch.labels)loss.backward()optimizer.step()# 保存 LoRA 參數
torch.save(model.state_dict(), "bert_lora.pt")
# QLoRA 量化 + LoRA
from bitsandbytes import quantize_4bit
for n,p in model.named_parameters():p.data = quantize_4bit(p.data, dtype='nf4')
# 注入 LoRA 后同上訓練代碼# 保存模型
model.save_pretrained("bert_qlora")
3 BERT 分類任務:完整示例
本來應該是用chatglm,千問等大模型來來做演示的,但是此處只是為了講解,這些訓練的過程,所以使用大家熟悉的bert 模型來做。
下面以 PyTorch + Hugging Face Transformers 為原型,演示四種方法在 BERT 分類項目中的“訓練→保存→加載→推理”流程。
3.1 Prompt Tuning
注釋要點:
1.?? ?凍結預訓練模型:保證只有“軟提示”與分類頭參與訓練,極大降低顯存與計算開銷。
2.?? ?軟提示(Prompt):用少量可學習的向量充當“偽 token”,引導模型關注下游任務特征。
3.?? ?拼接邏輯:將 prompt 放在輸入序列最前面,BERT 的 Transformer 自注意力會自動將其納入計算。
4.?? ?保存與加載:只需保存 prompt 與分類頭,即可方便部署。
5.?? ?推理流程:與訓練相似,但不開啟梯度,快速得到預測結果。
關鍵說明
-
attention_mask 擴展:在 prompt 前補 1,使得自注意力不會忽略 prompt 部分 。
-
token_type_ids 擴展:prompt 通常歸為第 0 號句子,也可設置為其它值,務必與模型訓練時一致 。
-
使用 inputs_embeds:通過 model(inputs_embeds=…, attention_mask=…, token_type_ids=…) 保證 BERT 自帶的 絕對位置編碼 與 句子編碼 會自動加到我們拼接后的輸入上,無需手動處理 。
-
取第 L 個位置:prompt 長度為 L,故第 L 個向量對應原始文本第 1 個 token 的“CLS 等價”表征,含 prompt 與輸入信息 。
-
保存與加載:只需保存 prompt 與分類頭,BERT 主干無需變動,極簡部署。
通過以上改動,代碼即完整支持了位置編碼與句子編碼,保證 soft prompt 能正確注入,而原有的自注意力機制與絕對位置編碼均被保留。
import torch
import torch.nn as nn
from transformers import BertModel
from transformers import AdamW# -----------------------------------------------------------------------------
# 1. 初始化
# -----------------------------------------------------------------------------# 1.1 加載預訓練 BERT 主干(不含任何 task-specific 頭)
model = BertModel.from_pretrained('bert-base-uncased')# 1.2 凍結所有 BERT 參數,只訓練后續新增模塊
for p in model.parameters():p.requires_grad = False# 1.3 軟提示長度 L,可根據顯存/性能自行調整
L = 20 # [turn0search0]# 1.4 創建一個可訓練的軟提示向量,形狀為 [L, H]
H = model.config.hidden_size
prompt = nn.Parameter(torch.randn(L, H)) # [turn0search4]# 1.5 定義分類頭,將隱藏維度 H 映射到類別數
num_labels = 2
classifier = nn.Linear(H, num_labels)# 1.6 優化器只更新 prompt 和 classifier
optim_params = [prompt, *classifier.parameters()]
optimizer = AdamW(optim_params, lr=1e-3)# 1.7 損失函數:交叉熵
loss_fn = nn.CrossEntropyLoss()# -----------------------------------------------------------------------------
# 2. 訓練循環
# -----------------------------------------------------------------------------# 假設 train_loader 每個 batch 包含:
# batch['input_ids'] : [B, N]
# batch['attention_mask']: [B, N]
# batch['token_type_ids'] : [B, N] (可選,句子對任務需提供)
for batch in train_loader:input_ids = batch['input_ids'] # [B, N]orig_mask = batch['attention_mask'] # [B, N]orig_token_ids = batch.get('token_type_ids',torch.zeros_like(orig_mask)) # [B, N]# 2.1 擴展 attention_mask: 在最前面為 L 個 prompt 置 1# prompt_mask: [B, L]prompt_mask = torch.ones(orig_mask.size(0), L,dtype=orig_mask.dtype,device=orig_mask.device) # [turn1search0]new_mask = torch.cat([prompt_mask, orig_mask], dim=1) # [B, L+N]# 2.2 擴展 token_type_ids: prompt 統一標為 0(或其他常數均可)prompt_type = torch.zeros_like(prompt_mask, dtype=orig_token_ids.dtype)new_token_ids = torch.cat([prompt_type, orig_token_ids], dim=1) # [B, L+N]# 2.3 從 embedding table 拿到原始 prompt 的 word embeddings# init_emb: [L, H]init_emb = model.embeddings.word_embeddings(torch.arange(L, device=orig_mask.device)) # [turn0search4]# 2.4 用 LSTM/MLP 編碼(此處以 LSTM 為例,也可改為 nn.Linear 等)prompt_emb, _ = nn.LSTM(H, H, batch_first=True)(init_emb.unsqueeze(0)) # [1, L, H]# 2.5 擴展至 batch 大小 → [B, L, H]pref = prompt_emb.expand(input_ids.size(0), -1, -1)# 2.6 原始 token embeddings: [B, N, H]emb = model.embeddings(input_ids) # [turn0search4]# 2.7 拼接 prompt 與原始 embeddings → [B, L+N, H]enc_inputs = torch.cat([pref, emb], dim=1) # [turn0search2]# 2.8 調用 BertModel,傳入 inputs_embeds、attention_mask、token_type_ids# BertEmbeddings 層會在 inputs_embeds 上加上 position & token_type embeddingsoutputs = model(inputs_embeds=enc_inputs,attention_mask=new_mask,token_type_ids=new_token_ids)sequence_output = outputs.last_hidden_state # [B, L+N, H]# 2.9 取第 L 個位置(即 prompt 之后首個 token)做“CLS”等價表示pooled_rep = sequence_output[:, L, :] # [B, H]# 2.10 分類 & 計算損失logits = classifier(pooled_rep) # [B, num_labels]loss = loss_fn(logits, batch['labels'])# 2.11 反向傳播 & 更新loss.backward()optimizer.step()optimizer.zero_grad()# -----------------------------------------------------------------------------
# 3. 保存
# -----------------------------------------------------------------------------# 3.1 保存 prompt 向量
torch.save(prompt.state_dict(), "pt_prompts.bin")# 3.2 保存分類頭
torch.save(classifier.state_dict(), "pt_cls.bin")# -----------------------------------------------------------------------------
# 4. 加載 & 推理
# -----------------------------------------------------------------------------# 4.1 重建 prompt 并加載
prompt = nn.Parameter(torch.empty(L, H))
prompt.load_state_dict(torch.load("pt_prompts.bin"))# 4.2 重建分類頭并加載
classifier = nn.Linear(H, num_labels)
classifier.load_state_dict(torch.load("pt_cls.bin"))# 4.3 推理模式
model.eval()
classifier.eval()with torch.no_grad():# 假設 test_ids, test_mask, test_token_type_ids 形狀分別 [B, N]emb = model.embeddings(test_ids) # [B, N, H]prompt_mask = torch.ones(test_mask.size(0), L,dtype=test_mask.dtype,device=test_mask.device)new_mask = torch.cat([prompt_mask, test_mask], dim=1) # [B, L+N]prompt_type = torch.zeros_like(prompt_mask, dtype=test_token_type_ids.dtype)new_types = torch.cat([prompt_type, test_token_type_ids], dim=1) # [B, L+N]prompt_emb, _ = lstm(init_emb.unsqueeze(0)) # [1, L, H]pref = prompt_emb.expand(test_ids.size(0), -1, -1) # [B, L, H]enc_inputs = torch.cat([pref, emb], dim=1) # [B, L+N, H]outputs = model(inputs_embeds=enc_inputs,attention_mask=new_mask,token_type_ids=new_types)seq_out = outputs.last_hidden_state # [B, L+N, H]rep = seq_out[:, L, :] # [B, H]logits = classifier(rep) # [B, num_labels]preds = logits.argmax(dim=-1) # [B]
3.2 P-Tuning
關鍵說明
-
prompt_ids → init_emb:借助 BERT 的 embedding table 獲取初始連續提示嵌入。
-
LSTM 編碼:通過小型 LSTM(或通用 MLP)進一步轉換提示向量,增強其表達能力。
-
拼接邏輯:將提示嵌入放在序列最前端,BERT 的自注意力會自動將其納入上下文。
-
CLS 等價:拼接后第 prompt_len 位置處的向量即為融合了提示與輸入的全局表示,用于分類。
-
推理流程:與訓練相同,但無梯度計算,速度更快。
-
attention_mask 擴展:在 prompt 前補 1,使得自注意力不會忽略 prompt 部分 。
-
token_type_ids 擴展:prompt 通常歸為第 0 號句子,也可設置為其它值,務必與模型訓練時一致 。
-
使用 inputs_embeds:通過 model(inputs_embeds=…, attention_mask=…, token_type_ids=…) 保證 BERT 自帶的 絕對位置編碼 與 句子編碼 會自動加到我們拼接后的輸入上,無需手動處理 。
-
取第 L 個位置:prompt 長度為 L,故第 L 個向量對應原始文本第 1 個 token 的“CLS 等價”表征,含 prompt 與輸入信息 。
-
保存與加載:只需保存 prompt 與分類頭,BERT 主干無需變動,極簡部署。
通過以上改動,代碼即完整支持了位置編碼與句子編碼,保證 soft prompt 能正確注入,而原有的自注意力機制與絕對位置編碼均被保留。
import torch
import torch.nn as nn
from transformers import BertModel, AdamW# -----------------------------------------------------------------------------
# 1. 初始化階段
# -----------------------------------------------------------------------------
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')# 1.1 加載預訓練的 BERT 主干(不含 task-specific 頭),并移到 device
model = BertModel.from_pretrained('bert-base-uncased').to(device)# 1.2 凍結 BERT 主干所有參數,只訓練下面新增的部分
for p in model.parameters():p.requires_grad = False# 1.3 軟提示長度 L(可調)
L = 20# 1.4 偽 token IDs,用于從 embedding table 取初始 prompt 向量
prompt_ids = torch.arange(L, device=device)# 1.5 創建 LSTM 作為 prompt encoder
H = model.config.hidden_size
prompt_encoder = nn.LSTM(input_size=H,hidden_size=H,batch_first=True
).to(device)# 1.6 定義分類頭
num_labels = 2
classifier = nn.Linear(H, num_labels).to(device)# 1.7 優化器:僅優化 prompt encoder 和分類頭參數
optimizer = AdamW(list(prompt_encoder.parameters()) + list(classifier.parameters()),lr=1e-3
)# -----------------------------------------------------------------------------
# 2. 訓練循環
# -----------------------------------------------------------------------------# 假設 train_loader 每 batch 提供:
# batch['input_ids'] : [B, N]
# batch['attention_mask']: [B, N]
# batch['token_type_ids'] : [B, N](可選)
# batch['labels'] : [B]
for batch in train_loader:# 2.1 數據搬到 deviceinput_ids = batch['input_ids'].to(device) orig_mask = batch['attention_mask'].to(device) orig_types = batch.get('token_type_ids',torch.zeros_like(orig_mask)).to(device)labels = batch['labels'].to(device) B, N = input_ids.size()# 2.2 構造新的 attention_mask:prompt 部分全部設為 1prompt_mask = torch.ones(B, L, dtype=orig_mask.dtype, device=device)new_mask = torch.cat([prompt_mask, orig_mask], dim=1) # [B, L+N]# 2.3 構造新的 token_type_ids:prompt 部分設為 0prompt_type = torch.zeros_like(prompt_mask, dtype=orig_types.dtype, device=device)new_types = torch.cat([prompt_type, orig_types], dim=1) # [B, L+N]# 2.4 從 embedding table 提取初始 prompt 嵌入 [L, H]init_emb = model.embeddings.word_embeddings(prompt_ids) # [L, H]# 2.5 通過 prompt encoder(LSTM)生成最終 prompt 嵌入 [1, L, H]prompt_emb, _ = prompt_encoder(init_emb.unsqueeze(0)) # [1, L, H]# 2.6 擴展至 batch 大小 → [B, L, H]pref = prompt_emb.expand(B, -1, -1)# 2.7 獲取原始文本 embeddings → [B, N, H]emb = model.embeddings(input_ids) # [B, N, H]# 2.8 拼接兩個部分 → [B, L+N, H]inputs_embeds = torch.cat([pref, emb], dim=1) # [B, L+N, H]# 2.9 調用 BERT,傳入 inputs_embeds、attention_mask 和 token_type_idsoutputs = model(inputs_embeds=inputs_embeds,attention_mask=new_mask,token_type_ids=new_types)sequence_output = outputs.last_hidden_state # [B, L+N, H]# 2.10 取第 L 個位置的向量作為 “CLS 等價” 表示 → [B, H]cls_equiv = sequence_output[:, L, :]# 2.11 分類 & 計算損失logits = classifier(cls_equiv) # [B, num_labels]loss = nn.CrossEntropyLoss()(logits, labels)# 2.12 反向傳播 & 參數更新loss.backward()optimizer.step()optimizer.zero_grad()# -----------------------------------------------------------------------------
# 3. 保存訓練好的參數
# -----------------------------------------------------------------------------# 3.1 保存 prompt encoder 權重
torch.save(prompt_encoder.state_dict(), "ptuning_encoder.bin")# 3.2 保存分類頭權重
torch.save(classifier.state_dict(), "ptuning_cls.bin")# -----------------------------------------------------------------------------
# 4. 加載 & 推理
# -----------------------------------------------------------------------------# 4.1 重建 prompt encoder 并加載
prompt_encoder = nn.LSTM(input_size=H, hidden_size=H, batch_first=True).to(device)
prompt_encoder.load_state_dict(torch.load("ptuning_encoder.bin"))# 4.2 重建分類頭并加載
classifier = nn.Linear(H, num_labels).to(device)
classifier.load_state_dict(torch.load("ptuning_cls.bin"))# 4.3 設置為推理模式
model.eval()
prompt_encoder.eval()
classifier.eval()# 4.4 推理循環
for batch in test_loader:input_ids = batch['input_ids'].to(device)orig_mask = batch['attention_mask'].to(device)orig_types = batch.get('token_type_ids',torch.zeros_like(orig_mask)).to(device)B, N = input_ids.size()prompt_mask = torch.ones(B, L, dtype=orig_mask.dtype, device=device)new_mask = torch.cat([prompt_mask, orig_mask], dim=1)prompt_type = torch.zeros_like(prompt_mask, dtype=orig_types.dtype, device=device)new_types = torch.cat([prompt_type, orig_types], dim=1)init_emb = model.embeddings.word_embeddings(prompt_ids)prompt_emb, _= prompt_encoder(init_emb.unsqueeze(0))pref = prompt_emb.expand(B, -1, -1)emb = model.embeddings(input_ids)inputs_embeds= torch.cat([pref, emb], dim=1)outputs = model(inputs_embeds=inputs_embeds,attention_mask=new_mask,token_type_ids=new_types)seq_out = outputs.last_hidden_statecls_equiv = seq_out[:, L, :]logits = classifier(cls_equiv)preds = torch.argmax(logits, dim=-1)# 處理 preds (例如計算準確率或保存結果)# ...
以GPT2自回歸模型來講解 p_turning
要點說明
-
使用 LSTM 作為 Prompt Encoder,可捕捉提示向量序列的時序依賴;
-
前 L 個位置的 labels 設為 -100,保證 loss 只計算在真實文本部分;
-
GPT-2 自帶位置編碼與因果遮掩,無需手動處理;
-
生成時也使用 inputs_embeds,并在輸出時剔除前 L 個“偽 token”。
-
teacher Forcing 是一種經典的自回歸序列模型訓練策略,最早由 Williams 和 Zipser 在 1989 年提出,用于加速和穩定循環神經網絡(RNN)的訓練 。其核心思路是在訓練階段,模型每一步的輸入不使用模型自身上一步的預測結果,而是直接采用真實標記(ground truth),以此減少誤差累積并加快收斂 。在生成(推理)階段,則依次將模型預測的標記拼回輸入,完成逐步自回歸生成。
import torch
import torch.nn as nn
from transformers import GPT2LMHeadModel, GPT2Tokenizer, AdamW
# 0. 環境與模型初始化
# 設備選取:若有可用 GPU 則用 GPU,否則用 CPU
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') # 加載 GPT-2 自回歸語言模型與分詞器,并將模型移到 device
tokenizer = GPT2Tokenizer.from_pretrained('gpt2')
model = GPT2LMHeadModel.from_pretrained('gpt2').to(device) # 凍結 GPT-2 主干所有參數,只保留微調 Prompt Encoder 的權重
for p in model.parameters():p.requires_grad = False # 1. P-Tuning 參數與模塊定義
# 定義 Prompt 長度 L,可根據顯存與任務難度調節
L = 30 # GPT-2 隱藏層維度 H,等同于 embedding 維度
H = model.config.n_embd# 從 GPT-2 的詞向量表中取出 L 個“偽 token”對應的初始 embedding
prompt_ids = torch.arange(L, device=device)
init_emb = model.transformer.wte(prompt_ids) # [L, H] # 創建 LSTM Prompt Encoder:輸入 [1, L, H] → 輸出 [1, L, H]
prompt_encoder = nn.LSTM(input_size=H, hidden_size=H, num_layers=1, batch_first=True
).to(device) # 僅訓練 Prompt Encoder 的參數
optimizer = AdamW(prompt_encoder.parameters(), lr=5e-5) # 2. 訓練循環(自回歸語言建模)
model.train()
prompt_encoder.train()for epoch in range(num_epochs):for batch in train_loader:# 假設 batch 中含有:# batch['input_ids'] : [B, N]# batch['attention_mask']: [B, N]# batch['labels'] : [B, N],用于計算下一個 token 的監督信號input_ids = batch['input_ids'].to(device) attention_mask = batch['attention_mask'].to(device) labels = batch['labels'].to(device) B, N = input_ids.size()# 2.1 用 LSTM 編碼初始 prompt embedding → [1, L, H]prompt_emb, _ = prompt_encoder(init_emb.unsqueeze(0)) # 2.2 擴展到 batch 大小 → [B, L, H]prompt_emb = prompt_emb.expand(B, -1, -1) # 2.3 獲取原始輸入 token embedding → [B, N, H]token_emb = model.transformer.wte(input_ids) # 2.4 拼接 prompt 與原始 embeddings → [B, L+N, H]inputs_embeds = torch.cat([prompt_emb, token_emb], dim=1) # 2.5 構造新的 attention_mask:在 prompt 部分填 1 → [B, L+N]prompt_mask = torch.ones(B, L, device=device)new_mask = torch.cat([prompt_mask, attention_mask], dim=1) # 2.6 構造新的 labels:prompt 部分設為 -100,跳過 loss 計算 → [B, L+N]prompt_labels = torch.full((B, L), -100, device=device, dtype=torch.long)new_labels = torch.cat([prompt_labels, labels], dim=1) # 2.7 前向計算,自回歸地并行預測所有位置下一個 tokenoutputs = model(inputs_embeds=inputs_embeds,attention_mask=new_mask,labels=new_labels) loss = outputs.loss # 2.8 反向傳播 & 更新 Prompt Encoderloss.backward()optimizer.step()optimizer.zero_grad()print(f"Epoch {epoch+1} loss: {loss.item():.4f}")
# 3. 保存 Prompt Encoder
torch.save(prompt_encoder.state_dict(), "gpt2_ptuning_lstm.bin")
# 4. 加載 & 推理
# 4.1 重建并加載 Prompt Encoder
prompt_encoder = nn.LSTM(input_size=H, hidden_size=H, batch_first=True).to(device)
prompt_encoder.load_state_dict(torch.load("gpt2_ptuning_lstm.bin"))model.eval()
prompt_encoder.eval()# 4.2 準備初始上下文
context = "In a distant future"
tokens = tokenizer(context, return_tensors="pt").to(device)
input_ids = tokens.input_ids
attention_mask = tokens.attention_mask
B, N = input_ids.size()# 4.3 生成 prompt_emb → [1, L, H] → 擴展為 [B, L, H]
prompt_emb, _ = prompt_encoder(init_emb.unsqueeze(0))
prompt_emb = prompt_emb.expand(B, -1, -1)# 4.4 獲取原始 embeddings & 拼接
token_emb = model.transformer.wte(input_ids)
inputs_embeds = torch.cat([prompt_emb, token_emb], dim=1) # 4.5 構造新的 attention_mask
prompt_mask = torch.ones(B, L, device=device)
new_mask = torch.cat([prompt_mask, attention_mask], dim=1) # 4.6 自回歸生成并剔除 prompt 部分
generated = model.generate(inputs_embeds=inputs_embeds,attention_mask=new_mask,max_length=L + N + 50
)
generated = generated[:, L:] # 4.7 解碼并輸出
print(tokenizer.decode(generated[0], skip_special_tokens=True))
?
DeepSeek-R1 P-Tuning 示例代碼
以下代碼展示了對 DeepSeek-R1 模型進行 P-Tuning 的全流程:凍結主干 → 構造連續提示 → 并行化微調 → 保存加載 → 自回歸生成。根據具體硬件與任務需求,可將 model_name 替換為任意 Distill 版本以實現更輕量化部署。
比如
-
DeepSeek-R1-Distill-Qwen-1.5B
-
DeepSeek-R1-Distill-Llama-7B/8B
-
DeepSeek-R1 / DeepSeek-R1-Zero
import torch
import torch.nn as nn
from transformers import AutoModelForCausalLM, AutoTokenizer, AdamW# 0. 設備與模型加載
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
model_name = "deepseek-ai/DeepSeek-R1"
tokenizer = AutoTokenizer.from_pretrained(model_name)
model = AutoModelForCausalLM.from_pretrained(model_name).to(device)
for p in model.parameters():p.requires_grad = False# 1. Prompt Encoder 定義
L = 30
H = model.config.n_embd
prompt_ids = torch.arange(L, device=device)
init_emb = model.get_input_embeddings()(prompt_ids) # [L, H]
prompt_encoder = nn.LSTM(input_size=H, hidden_size=H, batch_first=True).to(device)
optimizer = AdamW(prompt_encoder.parameters(), lr=5e-5)# 2. 訓練循環
model.train()
prompt_encoder.train()
for batch in train_loader:input_ids = batch["input_ids"].to(device) # [B, N]attention_mask = batch["attention_mask"].to(device) # [B, N]labels = batch["labels"].to(device) # [B, N]B, N = input_ids.size()# 2.1 生成 Prompt Embedding → [1, L, H] → 擴展至 [B, L, H]prompt_emb, _ = prompt_encoder(init_emb.unsqueeze(0))prompt_emb = prompt_emb.expand(B, -1, -1)# 2.2 獲取輸入 embeddings & 拼接 → [B, L+N, H]token_emb = model.get_input_embeddings()(input_ids)inputs_embeds = torch.cat([prompt_emb, token_emb], dim=1)# 2.3 構造新的 attention_mask & labelsprompt_mask = torch.ones(B, L, device=device)new_mask = torch.cat([prompt_mask, attention_mask], dim=1)prompt_labels = torch.full((B, L), -100, device=device, dtype=torch.long)new_labels = torch.cat([prompt_labels, labels], dim=1)# 2.4 前向 + 反向outputs = model(inputs_embeds=inputs_embeds,attention_mask=new_mask,labels=new_labels)loss = outputs.lossloss.backward()optimizer.step()optimizer.zero_grad()# 3. 保存 Prompt Encoder
torch.save(prompt_encoder.state_dict(), "deepseek_ptuning_lstm.bin")# 4. 加載 & 推理
prompt_encoder.load_state_dict(torch.load("deepseek_ptuning_lstm.bin"))
model.eval()
prompt_encoder.eval()
context = "In a distant future"
tokens = tokenizer(context, return_tensors="pt").to(device)
input_ids, attn = tokens.input_ids, tokens.attention_mask
B, N = input_ids.size()prompt_emb, _ = prompt_encoder(init_emb.unsqueeze(0))
prompt_emb = prompt_emb.expand(B, -1, -1)
token_emb = model.get_input_embeddings()(input_ids)
inputs_embeds = torch.cat([prompt_emb, token_emb], dim=1)
prompt_mask = torch.ones(B, L, device=device)
new_mask = torch.cat([prompt_mask, attn], dim=1)generated = model.generate(inputs_embeds=inputs_embeds,attention_mask=new_mask,max_length=L+N+50)[:, L:]
print(tokenizer.decode(generated[0], skip_special_tokens=True))
3.3 LoRA
?
Low-Rank Adaptation (LoRA) 是一種參數高效微調(PEFT)技術,通過將原始的大規模模型權重更新分解成兩個低秩矩陣 ?和
,并替代原始權重增量,實現只訓練這兩段小矩陣而凍結其他參數,從而將可訓練參數量降低約 10,000 倍,顯存占用減少 3 倍,并保持與全量微調相當或更優的表現 。
-
核心原理:在 Transformer 的線性層 W 旁路注入低秩增量
,
其中
,
為縮放系數,通常設定為
以平衡學習速率與參數尺度 。
-
優點:
-
極少可訓練參數:僅占原模型的零點幾至千分之一;
-
無推理延遲:與原模型結構兼容,無需在推理時合并額外層;
-
高效易用:Hugging Face PEFT、LoRAX 等開源工具提供開箱即用實現 。
-
LoRA 現已成為 2025 年主流大模型微調方法之一,廣泛應用于 BERT、GPT-2/3、LLaMA、DeepSeek-R1 等多種模型。
# 1. LoRA 注入方法略——替換所有 nn.Linear 為 LoRALinear
for p in model.parameters(): p.requires_grad=False
for m in model.modules():if isinstance(m, LoRALinear):for p in m.parameters(): p.requires_grad=True
classifier = nn.Linear(H, num_labels)
optimizer = AdamW(list(filter(lambda p:p.requires_grad, model.parameters())) + list(classifier.parameters()), lr=1e-4)# 2. 訓練/保存/加載 同常規 fine-tuning
?
1. BERT 上的 LoRA 微調示例
import torch
from transformers import BertForSequenceClassification, BertTokenizerFast, Trainer, TrainingArguments
from peft import LoraConfig, get_peft_model, TaskType# 1.1 設備與模型/分詞器加載
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
model_id = "bert-base-uncased"
tokenizer = BertTokenizerFast.from_pretrained(model_id)
model = BertForSequenceClassification.from_pretrained(model_id, num_labels=2).to(device) # 二分類 [oai_citation:0?Hugging Face](https://huggingface.co/docs/transformers/en/peft?utm_source=chatgpt.com)# 1.2 凍結主模型參數,只訓練 LoRA 矩陣
for param in model.base_model.parameters():param.requires_grad = False # 凍結 Bert 主體# 1.3 配置 LoRA
lora_config = LoraConfig(task_type="SEQ_CLS", # 序列分類任務inference_mode=False, # 微調模式r=8, # LoRA 低秩矩陣秩lora_alpha=16, # 縮放系數lora_dropout=0.1, # Dropout 比例target_modules=["query", "value"] # 在自注意力的 query 和 value 矩陣上注入
) [oai_citation:1?Hugging Face](https://huggingface.co/docs/peft/en/package_reference/lora?utm_source=chatgpt.com)# 1.4 包裝模型
model = get_peft_model(model, lora_config) # 插入 A, B 矩陣 [oai_citation:2?Hugging Face](https://huggingface.co/docs/peft/main/en/developer_guides/lora?utm_source=chatgpt.com)
model.print_trainable_parameters() # 輸出可訓練參數比例# 1.5 準備訓練數據(示例)
texts = ["I love this!", "This is terrible."]
labels = [1, 0]
enc = tokenizer(texts, padding=True, truncation=True, return_tensors="pt").to(device)
dataset = torch.utils.data.TensorDataset(enc["input_ids"], enc["attention_mask"], torch.tensor(labels))# 1.6 定義 Trainer
training_args = TrainingArguments(output_dir = "./bert-lora-output",per_device_train_batch_size = 8,num_train_epochs = 3,learning_rate = 3e-4,logging_steps = 10,save_steps = 50
)
trainer = Trainer(model=model,args=training_args,train_dataset=dataset
)# 1.7 訓練
trainer.train() # 僅更新 LoRA 矩陣參數 [oai_citation:3?GitHub](https://github.com/huggingface/peft?utm_source=chatgpt.com)# 1.8 保存 & 加載
model.save_pretrained("bert-lora-model")
# 重新加載時:
# from peft import PeftModel
# model = BertForSequenceClassification.from_pretrained(model_id, num_labels=2)
# model = PeftModel.from_pretrained(model, "bert-lora-model")
?
2. DeepSeek-R1 上的 LoRA 微調示例
import torch
from transformers import AutoModelForCausalLM, AutoTokenizer, Trainer, TrainingArguments
from peft import LoraConfig, get_peft_model, TaskType# 2.1 設備與模型/分詞器加載
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
model_id = "deepseek-ai/DeepSeek-R1"
tokenizer = AutoTokenizer.from_pretrained(model_id)
model = AutoModelForCausalLM.from_pretrained(model_id).to(device) # 自回歸模型 [oai_citation:4?Hugging Face](https://huggingface.co/docs/transformers/en/peft?utm_source=chatgpt.com)# 2.2 凍結主模型參數,只訓練 LoRA 矩陣
for param in model.parameters():param.requires_grad = False# 2.3 配置 LoRA(自回歸任務)
lora_config = LoraConfig(task_type="CAUSAL_LM", # 自回歸語言模型inference_mode=False,r=8,lora_alpha=32,lora_dropout=0.05,target_modules=["c_attn", "c_proj"] # GPT 風格模型的注意力層
) [oai_citation:5?Hugging Face](https://huggingface.co/docs/peft/en/package_reference/lora?utm_source=chatgpt.com)# 2.4 包裝模型
model = get_peft_model(model, lora_config)
model.print_trainable_parameters()# 2.5 準備訓練數據(示例)
texts = ["Once upon a time,","In a galaxy far, far away,"
]
enc = tokenizer(texts, return_tensors="pt", padding=True, truncation=True).to(device)
# Labels = 輸入右移一位的 input_ids
labels = enc["input_ids"].clone()dataset = torch.utils.data.TensorDataset(enc["input_ids"], enc["attention_mask"], labels)# 2.6 定義 Trainer
training_args = TrainingArguments(output_dir = "./deepseek-lora-output",per_device_train_batch_size = 4,num_train_epochs = 3,learning_rate = 2e-4,logging_steps = 10,save_steps = 50
)
trainer = Trainer(model=model,args=training_args,train_dataset=dataset
)# 2.7 訓練
trainer.train() # LoRA 矩陣參與梯度更新# 2.8 保存 & 加載
model.save_pretrained("deepseek-lora-model")
# 重新加載時:
# from peft import PeftModel
# base = AutoModelForCausalLM.from_pretrained(model_id)
# model = PeftModel.from_pretrained(base, "deepseek-lora-model")
3.4 QLoRA
# 1. 4-bit 量化
for n,p in model.named_parameters():p.data = quantize_4bit(p.data, dtype='nf4')
# 2. 注入 LoRA + 凍結主體 + 訓練
# 3. model.save_pretrained & from_pretrained 進行加載推理
只是比lora 加了
# 1. 4-bit 量化
for n,p in model.named_parameters():
? ? p.data = quantize_4bit(p.data, dtype='nf4')
通過上述詳細流程,您即可在 2025 年的常見生產場景中,根據數據量、資源與任務需求,靈活選擇和實施軟提示(Prompt/P-Tuning)或參數高效微調(LoRA/QLoRA)方法,實現千億參數模型的高效適配與部署。
?
?
?