壓縮大型語言模型(LLMs):縮小10倍、性能保持不變
盡管LLMs的巨大規(guī)模使其在廣泛的應(yīng)用場景中表現(xiàn)卓越,但這也為其在實(shí)際問題中的應(yīng)用帶來了挑戰(zhàn)。本文將探討如何通過壓縮LLMs來應(yīng)對這些挑戰(zhàn)。我們將介紹關(guān)鍵概念,然后通過具體的Python代碼實(shí)例進(jìn)行演示。
2023年人工智能領(lǐng)域的主導(dǎo)思想是"更大即更好",改進(jìn)語言模型的方程相對簡單:更多數(shù)據(jù) + 更多參數(shù) + 更多計(jì)算資源 = 更優(yōu)性能。
雖然這種思路可能仍然適用(GPT-5即將問世?),但使用超過100B參數(shù)的模型顯然面臨挑戰(zhàn)。例如一個(gè)使用FP16的100B參數(shù)模型僅存儲(chǔ)就需要200GB空間!
大多數(shù)消費(fèi)級(jí)設(shè)備(如智能手機(jī)、平板電腦、筆記本電腦)無法處理如此龐大的模型。那么,我們是否可以在不損失性能的前提下縮小這些模型呢?
模型壓縮
模型壓縮旨在在保持性能的同時(shí)減小機(jī)器學(xué)習(xí)模型的規(guī)模[2]。這種方法對(大型)神經(jīng)網(wǎng)絡(luò)特別有效,因?yàn)樗鼈兺ǔ4嬖谶^度參數(shù)化的問題(即包含冗余計(jì)算單元)[3]。
模型壓縮的主要優(yōu)勢在于降低推理成本。這意味著強(qiáng)大的機(jī)器學(xué)習(xí)模型可以更廣泛地應(yīng)用(例如,在個(gè)人筆記本電腦上本地運(yùn)行LLMs),將人工智能集成到消費(fèi)產(chǎn)品中的成本降低,以及支持設(shè)備端推理,從而增強(qiáng)用戶隱私和安全性[3]。
壓縮模型的三種方法
模型壓縮技術(shù)多種多樣。本文將重點(diǎn)介紹三大類方法。
- 量化 — 使用低精度數(shù)據(jù)類型表示模型
- 剪枝 — 移除模型中不必要的組件
- 知識(shí)蒸餾 — 利用大型模型訓(xùn)練小型模型
這些方法都相互獨(dú)立,可以結(jié)合多種技術(shù)以實(shí)現(xiàn)最大化壓縮效果!
1、量化
盡管"量化"這個(gè)術(shù)語聽起來可能晦澀復(fù)雜,但其核心概念相對簡單。它指的是降低模型參數(shù)的精度。可以將這個(gè)過程類比為將高分辨率圖像轉(zhuǎn)換為低分辨率圖像,同時(shí)保持圖像的主要特征。
量化技術(shù)主要分為兩類:訓(xùn)練后量化(PTQ)和量化感知訓(xùn)訓(xùn)練(QAT)。
訓(xùn)練后量化(PTQ)
對于給定的神經(jīng)網(wǎng)絡(luò),訓(xùn)練后量化(PTQ)通過將參數(shù)替換為低精度數(shù)據(jù)類型來壓縮模型(例如,從FP16轉(zhuǎn)換為INT-8)。這是減少模型計(jì)算需求最快速和簡單的方法之一,因?yàn)樗鼰o需額外的訓(xùn)練或數(shù)據(jù)標(biāo)注。
雖然這是一種相對簡便的降低模型成本的方法,但過度使用這種技術(shù)進(jìn)行量化(例如,從FP16轉(zhuǎn)換為INT4)通常會(huì)導(dǎo)致性能下降,這限制了PTQ的潛在收益。
量化感知訓(xùn)練(QAT)
在需要更高壓縮率的情況下,可以通過使用低精度數(shù)據(jù)類型從頭開始訓(xùn)練模型來克服PTQ的局限性。這就是量化感知訓(xùn)練(QAT)的核心思想。
盡管這種方法在技術(shù)上更具挑戰(zhàn)性,但它可以產(chǎn)生顯著更小且性能良好的模型。例如,BitNet架構(gòu)使用三元數(shù)據(jù)類型(即1.58位)就達(dá)到了與原始Llama LLM相當(dāng)?shù)男阅?
PTQ和從頭開始的QAT之間存在較大的技術(shù)差距。介于兩者之間的一種方法是量化感知微調(diào),它包括在量化后對預(yù)訓(xùn)練模型進(jìn)行額外的訓(xùn)練[3]。
2、剪枝
剪枝的目標(biāo)是移除對模型性能影響較小的組件[7]。這種方法之所以有效,是因?yàn)闄C(jī)器學(xué)習(xí)模型(特別是大型模型)往往會(huì)學(xué)習(xí)冗余和噪聲結(jié)構(gòu)。
這個(gè)過程可以類比為修剪樹木中的枯枝。移除這些枯枝可以減小樹的體積而不會(huì)損害樹的健康。
剪枝方法可以分為兩類:非結(jié)構(gòu)化剪枝和結(jié)構(gòu)化剪枝。
非結(jié)構(gòu)化剪枝
非結(jié)構(gòu)化剪枝從神經(jīng)網(wǎng)絡(luò)中移除不重要的權(quán)重(即將其值設(shè)為零)。早期的工作如Optimal Brain Damage和Optimal Brain Surgeon通過估計(jì)剪枝對損失函數(shù)的影響來計(jì)算網(wǎng)絡(luò)中每個(gè)參數(shù)的重要性分?jǐn)?shù)。
最近基于幅度的方法(即移除絕對值最小的權(quán)重)因其簡單性和可擴(kuò)展性而變得更加流行。
雖然非結(jié)構(gòu)化剪枝的細(xì)粒度特性可以顯著減少參數(shù)數(shù)量,但這些收益通常需要專門的硬件才能實(shí)現(xiàn)。非結(jié)構(gòu)化剪枝會(huì)導(dǎo)致稀疏矩陣運(yùn)算(即乘以包含大量零的矩陣),而標(biāo)準(zhǔn)硬件在執(zhí)行這類運(yùn)算時(shí)并不比非稀疏運(yùn)算更有效。
結(jié)構(gòu)化剪枝
相比之下,結(jié)構(gòu)化剪枝從神經(jīng)網(wǎng)絡(luò)中移除整個(gè)結(jié)構(gòu)(例如注意力頭、神經(jīng)元和層)。這種方法避免了稀疏矩陣運(yùn)算的問題,因?yàn)榭梢灾苯訌哪P椭袆h除整個(gè)矩陣,而不是單個(gè)參數(shù)。
雖然識(shí)別待剪枝結(jié)構(gòu)的方法多種多樣,但其基本原則都是試圖移除對性能影響最小的結(jié)構(gòu)。參考文獻(xiàn)[5]提供了結(jié)構(gòu)化剪枝方法的詳細(xì)綜述。
3、知識(shí)蒸餾
知識(shí)蒸餾是一種將知識(shí)從(較大的)教師模型轉(zhuǎn)移到(較小的)學(xué)生模型的技術(shù)[5]。一種常見的實(shí)現(xiàn)方法是使用教師模型生成預(yù)測,然后用這些預(yù)測來訓(xùn)練學(xué)生模型。從教師模型的輸出logits(即所有可能的下一個(gè)標(biāo)記的概率)中學(xué)習(xí),可以提供比原始訓(xùn)練數(shù)據(jù)更豐富的信息,從而提高學(xué)生模型的性能[8]。
最新的蒸餾應(yīng)用完全摒棄了對logits的依賴,轉(zhuǎn)而從教師模型生成的合成數(shù)據(jù)中學(xué)習(xí)。一個(gè)典型的例子是斯坦福大學(xué)的Alpaca模型,它使用OpenAI的text-davinci-003(即原始ChatGPT模型)生成的合成數(shù)據(jù)對LLaMa 7B(基礎(chǔ))模型進(jìn)行了微調(diào),使其能夠遵循用戶指令[9]。
代碼示例:使用知識(shí)蒸餾和量化壓縮文本分類器
在了解了各種壓縮技術(shù)的基本原理后,讓我們通過一個(gè)Python實(shí)例來展示如何實(shí)際應(yīng)用這些技術(shù)。在這個(gè)例子中,我們將壓縮一個(gè)具有100M參數(shù)的模型,該模型用于將URL分類為安全或不安全(即釣魚網(wǎng)站)。
我們首先使用知識(shí)蒸餾將100M參數(shù)模型壓縮為50M參數(shù)模型。然后,通過應(yīng)用4位量化,我們進(jìn)一步將內(nèi)存占用減少了3倍,最終得到的模型比原始模型小7倍。
首先,我們導(dǎo)入必要的庫:
from datasets import load_dataset
from transformers import AutoTokenizer, AutoModelForSequenceClassification
from transformers import DistilBertForSequenceClassification, DistilBertConfig
import torch
import torch.nn as nn
import torch.optim as optim
from torch.utils.data import DataLoader
from sklearn.metrics import accuracy_score, precision_recall_fscore_support
然后,我們從Hugging Face Hub加載數(shù)據(jù)集。這包括訓(xùn)練集(2100行)、測試集(450行)和驗(yàn)證集(450行)。
data = load_dataset("shawhin/phishing-site-classification")
接下來,加載教師模型。我們將模型加載到Google Colab提供的T4 GPU上。
# 使用Nvidia GPU
device = torch.device('cuda')
# 加載教師模型和分詞器
model_path = "shawhin/bert-phishing-classifier_teacher"
tokenizer = AutoTokenizer.from_pretrained(model_path)
teacher_model = AutoModelForSequenceClassification.from_pretrained(model_path)
.to(device)
教師模型是Google的bert-base-uncased模型的微調(diào)版本,用于對釣魚網(wǎng)站URL進(jìn)行二元分類。
對于學(xué)生模型,我們基于distilbert-base-uncased從頭初始化一個(gè)新模型。我們通過移除兩層和減少剩余層中的四個(gè)注意力頭來修改架構(gòu)。
# 加載學(xué)生模型
my_config = DistilBertConfig(n_heads=8, n_layers=4) # 每層減少4個(gè)頭,總共減少2層
student_model = DistilBertForSequenceClassification
.from_pretrained("distilbert-base-uncased",
config=my_config,)
.to(device)
在訓(xùn)練學(xué)生模型之前,我們需要對數(shù)據(jù)集進(jìn)行標(biāo)記化處理。這一步至關(guān)重要,因?yàn)槟P鸵筝斎胛谋疽蕴囟ǜ袷奖硎尽N覀兏鶕?jù)每個(gè)批次中最長樣本的長度對樣本進(jìn)行填充。這允許將批次表示為PyTorch張量。
# 定義文本預(yù)處理函數(shù)
def preprocess_function(examples):
return tokenizer(examples["text"], padding='max_length', truncation=True)
# 對所有數(shù)據(jù)集進(jìn)行標(biāo)記化
tokenized_data = data.map(preprocess_function, batched=True)
tokenized_data.set_format(type='torch',
columns=['input_ids', 'attention_mask', 'labels'])
訓(xùn)練前的另一個(gè)關(guān)鍵步驟是為模型定義評估策略。以下函數(shù)用于計(jì)算給定模型和數(shù)據(jù)集的準(zhǔn)確率、精確率、召回率和F1分?jǐn)?shù)。
# 評估模型性能的函數(shù)
def evaluate_model(model, dataloader, device):
model.eval() # 將模型設(shè)置為評估模式
all_preds = []
all_labels = []
# 禁用梯度計(jì)算
with torch.no_grad():
for batch in dataloader:
input_ids = batch['input_ids'].to(device)
attention_mask = batch['attention_mask'].to(device)
labels = batch['labels'].to(device)
# 前向傳播獲取logits
outputs = model(input_ids, attention_mask=attention_mask)
logits = outputs.logits
# 獲取預(yù)測結(jié)果
preds = torch.argmax(logits, dim=1).cpu().numpy()
all_preds.extend(preds)
all_labels.extend(labels.cpu().numpy())
# 計(jì)算評估指標(biāo)
accuracy = accuracy_score(all_labels, all_preds)
precision, recall, f1, _ = precision_recall_fscore_support(all_labels,
all_preds,
average='binary')
return accuracy, precision, recall, f1
現(xiàn)在開始訓(xùn)練過程。為了使學(xué)生模型能夠同時(shí)從訓(xùn)練集的真實(shí)標(biāo)簽(硬目標(biāo))和教師模型的logits(軟目標(biāo))中學(xué)習(xí),我們需要構(gòu)建一個(gè)特殊的損失函數(shù),該函數(shù)考慮這兩種目標(biāo)。
這是通過將學(xué)生和教師輸出概率分布的KL散度與學(xué)生logits與真實(shí)標(biāo)簽的交叉熵?fù)p失相結(jié)合來實(shí)現(xiàn)的。
# 計(jì)算蒸餾損失和硬標(biāo)簽損失的函數(shù)
def distillation_loss(student_logits, teacher_logits,
true_labels, temperature, alpha):
# 從教師logits計(jì)算軟目標(biāo)
soft_targets = nn.functional.softmax(teacher_logits / temperature, dim=1)
student_soft = nn.functional.log_softmax(student_logits / temperature, dim=1)
# 蒸餾的KL散度損失
distill_loss = nn.functional.kl_div(student_soft,
soft_targets,
reduction='batchmean') * (temperature ** 2)
# 硬標(biāo)簽的交叉熵?fù)p失
hard_loss = nn.CrossEntropyLoss()(student_logits, true_labels)
# 結(jié)合損失
loss = alpha * distill_loss + (1.0 - alpha) * hard_loss
return loss
定義超參數(shù)、優(yōu)化器以及訓(xùn)練和測試數(shù)據(jù)加載器。
# 超參數(shù)
batch_size = 32
lr = 1e-4
num_epochs = 5
temperature = 2.0
alpha = 0.5
# 定義優(yōu)化器
optimizer = optim.Adam(student_model.parameters(), lr=lr)
# 創(chuàng)建訓(xùn)練數(shù)據(jù)加載器
dataloader = DataLoader(tokenized_data['train'], batch_size=batch_size)
# 創(chuàng)建測試數(shù)據(jù)加載器
test_dataloader = DataLoader(tokenized_data['test'], batch_size=batch_size)
最后使用PyTorch訓(xùn)練學(xué)生模型。
# 將學(xué)生模型設(shè)置為訓(xùn)練模式
student_model.train()
# 訓(xùn)練模型
for epoch in range(num_epochs):
for batch in dataloader:
# 準(zhǔn)備輸入
input_ids = batch['input_ids'].to(device)
attention_mask = batch['attention_mask'].to(device)
labels = batch['labels'].to(device)
# 禁用教師模型的梯度計(jì)算
with torch.no_grad():
teacher_outputs = teacher_model(input_ids,
attention_mask=attention_mask)
teacher_logits = teacher_outputs.logits
# 學(xué)生模型前向傳播
student_outputs = student_model(input_ids,
attention_mask=attention_mask)
student_logits = student_outputs.logits
# 計(jì)算蒸餾損失
loss = distillation_loss(student_logits, teacher_logits, labels,
temperature, alpha)
# 反向傳播
optimizer.zero_grad()
loss.backward()
optimizer.step()
print(f"第 {epoch + 1} 輪訓(xùn)練完成,損失: {loss.item()}")
# 評估教師模型
teacher_accuracy, teacher_precision, teacher_recall, teacher_f1 =
evaluate_model(teacher_model, test_dataloader, device)
print(f"教師模型 (測試集) - 準(zhǔn)確率: {teacher_accuracy:.4f},
精確率: {teacher_precision:.4f},
召回率: {teacher_recall:.4f},
F1分?jǐn)?shù): {teacher_f1:.4f}")
# 評估學(xué)生模型
student_accuracy, student_precision, student_recall, student_f1 =
evaluate_model(student_model, test_dataloader, device)
print(f"學(xué)生模型 (測試集) - 準(zhǔn)確率: {student_accuracy:.4f},
精確率: {student_precision:.4f},
召回率: {student_recall:.4f},
F1分?jǐn)?shù): {student_f1:.4f}")
print("\n")
# 將學(xué)生模型重新設(shè)置為訓(xùn)練模式
student_model.train()
訓(xùn)練結(jié)果如下圖所示。值得注意的是,在訓(xùn)練結(jié)束時(shí),學(xué)生模型在所有評估指標(biāo)上都超過了教師模型。
最后一步,我們可以在獨(dú)立的驗(yàn)證集上評估模型,即未用于訓(xùn)練模型參數(shù)或調(diào)整超參數(shù)的數(shù)據(jù)。
# 創(chuàng)建驗(yàn)證數(shù)據(jù)加載器
validation_dataloader = DataLoader(tokenized_data['validation'], batch_size=8)
# 評估教師模型
teacher_accuracy, teacher_precision, teacher_recall, teacher_f1 =
evaluate_model(teacher_model, validation_dataloader, device)
print(f"教師模型 (驗(yàn)證集) - 準(zhǔn)確率: {teacher_accuracy:.4f},
精確率: {teacher_precision:.4f},
召回率: {teacher_recall:.4f},
F1分?jǐn)?shù): {teacher_f1:.4f}")
# 評估學(xué)生模型
student_accuracy, student_precision, student_recall, student_f1 =
evaluate_model(student_model, validation_dataloader, device)
print(f"學(xué)生模型 (驗(yàn)證集) - 準(zhǔn)確率: {student_accuracy:.4f},
精確率: {student_precision:.4f},
召回率: {student_recall:.4f},
F1分?jǐn)?shù): {student_f1:.4f}")
我們再次觀察到學(xué)生模型的表現(xiàn)超過了教師模型。
到目前為止,我們已經(jīng)將模型從109M參數(shù)(438 MB)壓縮到52.8M參數(shù)(211 MB)。我們還可以更進(jìn)一步,對學(xué)生模型進(jìn)行量化處理。
我們使用QLoRA論文中描述的4位NormalFloat數(shù)據(jù)類型存儲(chǔ)模型參數(shù),并使用bfloat16進(jìn)行計(jì)算。
from transformers import BitsAndBytesConfig
# 以4位精度加載模型
nf4_config = BitsAndBytesConfig(
load_in_4bit=True,
bnb_4bit_quant_type="nf4",
bnb_4bit_compute_dtype = torch.bfloat16,
bnb_4bit_use_double_quant=True
)
model_nf4 = AutoModelForSequenceClassification.from_pretrained(model_id,
device_map=device,
quantization_config=nf4_config)
然后我們可以在驗(yàn)證集上評估量化后的模型。
# 評估量化后的學(xué)生模型
quantized_accuracy, quantized_precision, quantized_recall, quantized_f1 =
evaluate_model(model_nf4, validation_dataloader, device)
print("量化后性能")
print(f"準(zhǔn)確率: {quantized_accuracy:.4f},
精確率: {quantized_precision:.4f},
召回率: {quantized_recall:.4f},
F1分?jǐn)?shù): {quantized_f1:.4f}")
量化后學(xué)生模型在驗(yàn)證集上的表現(xiàn)。
再次觀察到壓縮后性能略有提升。這可以從奧卡姆剃刀原理的角度理解,該原理認(rèn)為在其他條件相同的情況下,更簡單的模型通常更優(yōu)。
在這個(gè)案例中,原始模型可能對這個(gè)二元分類任務(wù)而言過于復(fù)雜。簡化模型反而導(dǎo)致了性能的提升。
總結(jié)
盡管現(xiàn)代大型語言模型(LLMs)在各種任務(wù)上展現(xiàn)出卓越的性能,但它們的規(guī)模在實(shí)際部署中帶來了諸多挑戰(zhàn)。
近期模型壓縮技術(shù)的創(chuàng)新有助于通過降低LLM解決方案的計(jì)算成本來緩解這些挑戰(zhàn)。本文討論了三大類壓縮技術(shù)(量化、剪枝和知識(shí)蒸餾),并通過Python實(shí)例演示了它們的實(shí)際應(yīng)用。