【筆記】LoRA 理論與實現|大模型輕量級微調

論文鏈接:LoRA: Low-Rank Adaptation of Large Language Models

官方實現:microsoft/LoRA

非官方實現:huggingface/pefthuggingface/diffusers

這篇文章要介紹的是一種大模型/擴散模型的微調方法,叫做低秩適應(也就是 Low-Rank Adaptation,LoRA)。經常使用 stable diffusion webui 的讀者應該對這個名詞非常熟悉,通過給擴散模型加載不同的 lora,可以讓擴散模型生成出不同風格的圖像。現在也已經有很多平臺(例如 civitai、tensorart 等)可以下載現成的 lora,可以看出 LoRA 的影響力還是比較大的。

LoRA 作為一種高效的參數微調(Parameter-Efficient Fine-Tuning,PEFT)方法,最初是被用來微調 LLM 的,后來也被用來微調擴散模型。這種方法的主要思想是固定住預訓練模型的參數,同時引入額外的可訓練低秩分解模塊,只訓練額外引入的這部分參數,從而大大減小模型的微調成本。

與其他的 Peft 方法相比,LoRA 也有一些獨特的優勢:

  1. 首先是與 adapter 的方法相比,LoRA 不引入額外推理延遲。因為 Adapter 會在模型中插入額外的 layer,在推理時這些 layer 都會引入延遲,并且在分布式訓練中需要更多的進程同步操作。而 LoRA 不存在這個問題,且在推理階段可以利用重參數化將額外的權重與原有權重合并(這個后邊會介紹),從而保持推理延遲不變。
  2. 其次是與 prefix embedding tuning 相比,LoRA 更容易優化。并且 prefix embedding tuning 需要在序列前方插入一部分用來微調的 prompt,這種做法限制了有效 prompt 的長度。
  3. 除此之外,不同 LoRA 模型可以共用同一個基座模型,每次微調只需要保存額外參數。而且這種方法與其他的 peft 方法正交,可以同時使用多種 peft 方法進行微調。

LoRA 介紹

論文的作者提出本方法主要是基于一個觀察:模型通常都是過參數化的,在模型的優化過程中,更新的參數集中在低維度的子空間中。同時,模型在下游任務微調后,權重的內在秩(intrinsic rank,或者叫本征秩)是比較低的,因此可以認為更新的權重也是低秩的。所謂的更新的權重,可以表示成: W = W 0 + Δ W W=W_0+\Delta W W=W0?+ΔW,其中 W 0 W_0 W0? 就是原始的權重、 Δ W \Delta W ΔW 則是權重的變化量,也就是更新的權重。

LoRA 方法示意圖

LoRA 具體的做法如上圖所示,在預訓練權重的旁邊加入了一個新的支路,表示 Δ W \Delta W ΔW。由于上文中說的 Δ W \Delta W ΔW 具有較低的秩,因此可以對其進行低秩分解: Δ W = B A \Delta W=BA ΔW=BA,如果原始權重 W W W 的維度為 d × d d\times d d×d,低秩分解的秩為 r r r,那么有 B ∈ R d × r B\in\mathbb{R}^{d\times r} BRd×r A ∈ R r × d A\in\mathbb{R}^{r\times d} ARr×d,并且 r ? d r\ll d r?d。由于 r r r 很小,所以這部分的參數量也很小,在微調時只有這部分權重需要更新,所以訓練的資源消耗并不大。

加入低秩分解模塊后,模型的推理過程就變成了 W x = W 0 x + Δ W x = W 0 x + B A x Wx=W_0x+\Delta Wx=W_0x+BAx Wx=W0?x+ΔWx=W0?x+BAx。在初始條件下,LoRA 權重分別被初始化為高斯分布與 0,如上圖所示。(根據作者的意思,A 和 B 的初始化也可以反過來)這樣在初始條件下, B A x BAx BAx 這一項為 0,相當于從原始模型開始微調。

在實際使用時,還會引入一個額外的參數用來調整 LoRA 部分的權重,也就是 W x = W 0 x + α r B A x Wx=W_0x+\frac{\alpha}{r}BAx Wx=W0?x+rα?BAx,一般 α \alpha α 會設置為一個比 r r r 大的值。這樣做一方面是為了放大 LoRA 的效果,另一方面也是為了方便調參。

在微調結束后,推理時,由于 A 和 B 的權重矩陣相乘結果 BA 的維度和原始權重 W 0 W_0 W0? 的維度是相同的,所以直接加到 W 0 W_0 W0? 上即可完成重參數化。這樣額外的分支就合并到了原始的權重里,不會引入額外延遲。

到這里 LoRA 的微調過程就算介紹完了,下面再補充一些知識點和細節。

LoRA 降低了哪部分顯存

我們知道使用 LoRA 可以大大減少微調的顯存消耗量,然而 LoRA 相比原始模型是增加了模塊,所以相比原始模型產生的梯度肯定是更大的。之所以能夠降低顯存,主要是因為 optimizer 中保存的 state 減少了,對于單層,從 d × d d\times d d×d 變為了 d × r d\times r d×r,所以整體顯存使用量依然是降低。

LoRA 加入模型的哪一部分

論文的作者將 LoRA 的作用范圍限制在了 self-attention 的 projection 層中,也就是只有 QKV 和輸出 O 的 projection 才使用 LoRA。在實驗時,作者限制了可微調參數量,如果僅對 QKVO 中的一個使用 LoRA,則 rank 為 8;如果對其中兩個使用,則 rank 為 4。

根據實驗,相比于 rank 的大小,使用的 LoRA 數量對性能來說更重要。對所有的 QKVO 使用 LoRA 時,盡管 rank 很低(僅為 2),也能達到不錯的效果。

LoRA 代碼解讀

其實主流的 LoRA 都是用 microsoft 的官方實現以及 huggingface 的 peft 庫實現的,不過這類實現一般是用于 LLM,因為我們更關心如何在擴散模型里使用,所以這里基于 diffusers 的實現進行解讀。

一些工程代碼(可以略讀)

首先還是先初始化了 stable diffusion 的各個模塊:

# Load scheduler, tokenizer and models.
noise_scheduler = DDPMScheduler.from_pretrained(args.pretrained_model_name_or_path, subfolder="scheduler")
tokenizer = CLIPTokenizer.from_pretrained(args.pretrained_model_name_or_path, subfolder="tokenizer", revision=args.revision
)
text_encoder = CLIPTextModel.from_pretrained(args.pretrained_model_name_or_path, subfolder="text_encoder", revision=args.revision
)
vae = AutoencoderKL.from_pretrained(args.pretrained_model_name_or_path, subfolder="vae", revision=args.revision, variant=args.variant
)
unet = UNet2DConditionModel.from_pretrained(args.pretrained_model_name_or_path, subfolder="unet", revision=args.revision, variant=args.variant
)
# freeze parameters of models to save more memory
unet.requires_grad_(False)
vae.requires_grad_(False)
text_encoder.requires_grad_(False)

然后配置了一些 LoRA 的參數,可以看到設置了 r 和 α \alpha α,以及初始化權重的方式和作用的 projection 范圍:

unet_lora_config = LoraConfig(r=args.rank,lora_alpha=args.rank,init_lora_weights="gaussian",target_modules=["to_k", "to_q", "to_v", "to_out.0"],
)

然后把 LoRA 加入到 UNet 中:

unet.add_adapter(unet_lora_config)

這個 add_adapter 是由 diffusers.loaders.peft.PeftAdapterMixin 引入,這里調用了 peft 庫里的 inject_adapter_in_model 方法,可以看到最后還是使用的 peft 中的實現。這個方法定義在 peft.mapping.inject_adapter_in_model,初始化了一個新的 peft 對象:

tuner_cls = PEFT_TYPE_TO_TUNER_MAPPING[peft_config.peft_type]# By instantiating a peft model we are injecting randomly initialized LoRA layers into the model's modules.
peft_model = tuner_cls(model, peft_config, adapter_name=adapter_name)

這里我們使用的是 lora,所以 tuner_clspeft.tuners.lora.model.LoraModel。在這個對象初始化的時候,會調用 peft.tuners.tuners_utils.inject_adapter。核心的邏輯在這里,具體的看注釋:

# 獲得模型所有模塊的 key
key_list = [key for key, _ in model.named_modules()]
# 找到所有要進行 adaptation 的 layer
peft_config = _maybe_include_all_linear_layers(peft_config, model)
# 遍歷所有的 key 進行 LoRA 的插入
for key in key_list:# 如果不需要加入 LoRA,則直接跳過if not self._check_target_module_exists(peft_config, key):continue# 一些記錄self.targeted_module_names.append(key)is_target_modules_in_base_model = True# 正式進行替換(重點部分)parent, target, target_name = _get_submodules(model, key)self._create_and_replace(peft_config, adapter_name, target, target_name, parent, current_key=key)

這里調用的 _create_and_replaceLoraModel 實現:

def _create_and_replace(self, ...):... # 前邊的主要是解析一些參數,此處略去# 這里創建了 LoRA 模塊,并且將原始的 projection 模塊替換成 LoRA 模塊new_module = self._create_new_module(lora_config, adapter_name, target, **kwargs)self._replace_module(parent, target_name, new_module, target)

具體怎么創建和替換不是很重要,只需要知道這里就是篩選了所有符合條件的 key 將其替換成了對應的 LoRA 模塊即可。知道了替換的邏輯之后,下面我們看看 LoRA 模塊內部的具體實現。

LoRA 的具體實現

peft 的 LoRA 實現中,提供了 Linear、Embedding、Conv2d 三種 LoRA 層,因為我們上邊說的 LoRA 主要用在 self-attention 的 projection 中,所以重點分析一下 Linear 的實現。

LoRA 的初始化

Linear 的實現位于 peft.tuners.lora.layer.Linear,其繼承自 nn.ModuleLoraLayer,后者應該也可以看作一種 mixin。在初始化時,LoraLayer 初始化了以下屬性:

self.base_layer = base_layer  # 這個就是沒有加 LoRA 的 projection 層
self.r = {}  # rank
self.lora_alpha = {}  # alpha
self.scaling = {}  # 這個是 alpha/rank
self.lora_A = nn.ModuleDict({})  # LoRA 中的 A
self.lora_B = nn.ModuleDict({})  # LoRA 中的 B

然后在 update_layer 中進行了進一步的初始化:

self.r[adapter_name] = r
self.lora_alpha[adapter_name] = lora_alpha
# Actual trainable parameters
self.lora_A[adapter_name] = nn.Linear(self.in_features, r, bias=False)
self.lora_B[adapter_name] = nn.Linear(r, self.out_features, bias=False)
self.scaling[adapter_name] = lora_alpha / r

隨后繼續調用 reset_lora_parameters 初始化了權重:

nn.init.normal_(self.lora_A[adapter_name].weight, std=1 / self.r[adapter_name])
nn.init.zeros_(self.lora_B[adapter_name].weight)

推理過程實現

直接看 forward 函數的實現即可,具體的可以看下邊代碼里的注釋。這里需要提前介紹一下,這個類有一個屬性 merged 用來表示有沒有重參數化過,如果已經進行了重參數化,那么這個屬性就是 True,反之同理。

def forward(self, x: torch.Tensor, *args: Any, **kwargs: Any) -> torch.Tensor:# 如果不使用 adapter,就只傳播原始網絡,也就是 base_layerif self.disable_adapters:# 如果已經進行了重參數化,需要反重參數化if self.merged:self.unmerge()result = self.base_layer(x, *args, **kwargs)# 如果已經重參數化,那么就和原始的 base_layer 的推理過程相同if self.merged:result = self.base_layer(x, *args, **kwargs)else:  # 如果沒有重參數化# 先推理原始網絡result = self.base_layer(x, *args, **kwargs)torch_result_dtype = result.dtypefor active_adapter in self.active_adapters:if active_adapter not in self.lora_A.keys():continue# 再推理 A 和 Blora_A = self.lora_A[active_adapter]lora_B = self.lora_B[active_adapter]dropout = self.lora_dropout[active_adapter]scaling = self.scaling[active_adapter]x = x.to(lora_A.weight.dtype)# 可以看到這里進行了加權求和result = result + lora_B(lora_A(dropout(x))) * scalingresult = result.to(torch_result_dtype)return result

重參數化與反重參數化

重參數化就是把 base_layer 的權重 W 0 W_0 W0? 替換為 W 0 + Δ W W_0+\Delta W W0?+ΔW,反重參數化則是需要進行這個過程的反過程。因此首先需要實現計算 Δ W \Delta W ΔW,計算方式是 Δ W = W B W A \Delta W=W_BW_A ΔW=WB?WA?,在代碼中就是:

def get_delta_weight(self, adapter) -> torch.Tensor:...  # 去掉了一些不重要的類型/設備轉換相關的代碼weight_A = self.lora_A[adapter].weightweight_B = self.lora_B[adapter].weight# 可以看到實現還是很直接的output_tensor = transpose(weight_B @ weight_A, self.fan_in_fan_out) * self.scaling[adapter]return output_tensor

重參數化代碼在 merge 中,這里也給出一個比較簡化的版本,具體的見注釋:

    def merge(self, safe_merge: bool = False, adapter_names: Optional[list[str]] = None) -> None:adapter_names = check_adapters_to_merge(self, adapter_names)if not adapter_names:return# 遍歷所有的 adapter,這里應該就只有 lorafor active_adapter in adapter_names:if active_adapter in self.lora_A.keys():# 原始的 layerbase_layer = self.get_base_layer()# 用上述方法獲得的 delta Wdelta_weight = self.get_delta_weight(active_adapter)# 直接加到原始 layer 的權重上base_layer.weight.data += delta_weight# 記錄 merge 情況self.merged_adapters.append(active_adapter)

反重參數化也是同理:

def unmerge(self) -> None:if not self.merged:warnings.warn("Already unmerged. Nothing to do.")returnwhile len(self.merged_adapters) > 0:active_adapter = self.merged_adapters.pop()if active_adapter in self.lora_A.keys():weight = self.get_base_layer().weight# 計算 delta Wdelta_weight = self.get_delta_weight(active_adapter)# 從原始 layer 權重中將其減去weight.data -= delta_weight

權重的保存和讀取

可以看看 demo 里是怎么保存的 LoRA 權重:

unwrapped_unet = unwrap_model(unet)
unet_lora_state_dict = convert_state_dict_to_diffusers(get_peft_model_state_dict(unwrapped_unet)
)
StableDiffusionPipeline.save_lora_weights(save_directory=save_path,unet_lora_layers=unet_lora_state_dict,safe_serialization=True,
)

從這段代碼里可以看出,主要需要關注的就只有 get_peft_model_state_dict,這部分負責從模型的 state_dict 中將 LoRA 對應的權重提取出來。具體的為:

def get_peft_model_state_dict(model, state_dict=None, adapter_name="default", unwrap_compiled=False, save_embedding_layers="auto"
):config = model.peft_config[adapter_name]if state_dict is None:state_dict = model.state_dict()# 遍歷 state_dict,保存所有 key 帶有 lora 的權重to_return = {k: state_dict[k] for k in state_dict if ("lora_" in k and adapter_name in k)}return to_return

對于讀取,則是直接使用 load_lora_weights

pipeline.load_lora_weights(args.output_dir)

這部分的封裝也是非常復雜,一直找到最內層是調用了 peft.utils.save_and_load 這個模塊中的 set_peft_model_state_dict,簡化一下大概是這樣:

def set_peft_model_state_dict(model, peft_model_state_dict, adapter_name="default", ignore_mismatched_sizes: bool = False
):config = model.peft_config[adapter_name]state_dict = peft_model_state_dictpeft_model_state_dict = {}# 所有 LoRA 參數都含有以 lora 開頭的一個子串parameter_prefix = 'lora_'# 將 LoRA key 進行一些轉換for k, v in state_dict.items():if parameter_prefix in k:suffix = k.split(parameter_prefix)[1]if "." in suffix:suffix_to_replace = ".".join(suffix.split(".")[1:])k = k.replace(suffix_to_replace, f"{adapter_name}.{suffix_to_replace}")else:k = f"{k}.{adapter_name}"peft_model_state_dict[k] = velse:peft_model_state_dict[k] = v# 加載 state_dictload_result = model.load_state_dict(peft_model_state_dict, strict=False)return load_result

總結

感覺 LoRA 的思想還是很巧妙的,用很簡單的方法實現了大模型的微調。雖然方法很簡單,但是在工程實現方面由于很多中 peft 方法的實現,實際上還是很復雜的,真是很佩服寫 peft 庫的這群人,處理的情況也太多了,最后提供的接口也是很易用,tql。

參考資料:

  1. 當紅炸子雞 LoRA,是當代微調 LLMs 的正確姿勢?
  2. 圖解大模型微調系列之:大模型低秩適配器LoRA(原理篇)

本文原文以 CC BY-NC-SA 4.0 許可協議發布于 筆記|LoRA 理論與實現|大模型輕量級微調,轉載請注明出處。

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/diannao/86594.shtml
繁體地址,請注明出處:http://hk.pswp.cn/diannao/86594.shtml
英文地址,請注明出處:http://en.pswp.cn/diannao/86594.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

Cilium動手實驗室: 精通之旅---15.Isovalent Enterprise for Cilium: Network Policies

Cilium動手實驗室: 精通之旅---15.Isovalent Enterprise for Cilium: Network Policies 1. 環境信息2. 測試環境部署3. 默認規則3.1 測試默認規則3.2 小測驗 4. 網絡策略可視化4.1 通過可視化創建策略4.2 小測試 5. 測試策略5.1 應用策略5.2 流量觀測5.3 Hubble觀測5.4 小測試 …

opencv RGB圖像轉灰度圖

這段代碼的作用是將一個 3通道的 RGB 圖像(CV_8UC3)轉換為灰度圖像(CV_8UC1),并使用 OpenCV 的 parallel_for_ 對圖像處理進行并行加速。 🔍 一、函數功能總結 if (CV_8UC3 img.type()) {// 創建灰度圖 d…

React Hooks 的原理、常用函數及用途詳解

1. ??Hooks 是什么??? Hooks 是 React 16.8 引入的函數式組件特性,允許在不編寫 class 的情況下使用 state 和其他 React 特性(如生命周期、副作用等)。??本質是一類特殊函數??,它們掛載到 React 的調度系統中…

學習路之PHP--webman協程學習

學習路之PHP--webman協程學習 一、準備二、配置三、啟動四、使用 協程是一種比線程更輕量級的用戶級并發機制,能夠在進程中實現多任務調度。它通過手動控制掛起和恢復來實現協程間的切換,避免了進程上下文切換的開銷 一、準備 PHP > 8.1 Workerman &g…

linux libusb使用libusb_claim_interface失敗(-6,Resource busy)解決方案

linux libusb使用libusb_claim_interface失敗(-6,Resource busy)解決方案 ? 問題原因🛠? 解決方案🔸 方法一:分離內核驅動 libusb_detach_kernel_driver()🔸 方法二:使用 usb-devi…

使用mpu6500/6050, PID,互補濾波實現一個簡單的飛行自穩控制系統

首先,參考ai給出的客機飛機的比較平穩的最大仰府,偏轉,和防滾角度,如下: 客機的最大平穩仰俯(Pitch)、偏轉(Yaw)和防滾(Roll)角度,通…

深度解析AD7685ARMZRL7:16位精密ADC在低功耗系統中的設計價值

產品概述 AD7685ARMZRL7是16位逐次逼近型(SAR)ADC,采用MSOP-10緊湊封裝。其核心架構基于電荷再分配技術,支持2.3V至5.5V單電源供電,集成低噪聲采樣保持電路與內部轉換時鐘。器件采用偽差分輸入結構(IN/-&a…

EXCEL 實現“點擊跳轉到指定 Sheet”的方法

📌 WPS 表格技巧:如何實現點擊單元格跳轉到指定 Sheet 在使用 WPS 表格(或 Excel)時,我們經常會希望通過點擊一個單元格,直接跳轉到工作簿中的另一個工作表(Sheet)。這在制作目錄頁…

Python格式化:讓數據輸出更優雅

Python格式化:讓數據輸出更優雅 Python的格式化功能能讓數據輸出瞬間變得優雅又規范。不管是對齊文本、控制數字精度,還是動態填充內容,它都能輕松搞定。 一、基礎格式化:從簡單拼接開始 1. 百分號(%)格式…

2025年滲透測試面試題總結-小鵬[實習]安全工程師(題目+回答)

安全領域各種資源,學習文檔,以及工具分享、前沿信息分享、POC、EXP分享。不定期分享各種好玩的項目及好用的工具,歡迎關注。 目錄 小鵬[實習]安全工程師 1. 自我介紹 2. 有沒有挖過src? 3. 平時web滲透怎么學的,有…

VSCode科技風主題設計詳細指南

1. 科技風設計的核心特點 科技風設計是一種強調未來感、現代感和高科技感的設計風格,在VSCode主題設計中,可以通過以下幾個核心特點來體現: 1.1 色彩特點 冷色調為主:藍色、紫色、青色等冷色調是科技風設計的主要色彩高對比度:深色背景配合明亮的霓虹色,形成強烈的視覺…

android知識總結

Activity啟動模式 standard (標準模式) 每次啟動該 Activity(例如,通過 startActivity()),系統總會創建一個新的實例,并將其放入調用者(啟動它的那個 Activity)所在的任務棧中。 singleTop (棧…

第3章 MySQL數據類型

MySQL數據類型 1、數字數據類型1.1 整數類型1.2 定點類型1.3 浮點類型1.4位值類型1.5 超出范圍和溢出處理1.5.1 超出范圍處理1.5.2 溢出處理 2、日期和時間數據類型3、字符串數據類型3.1 char和varchar類型3.2 binary和varbinary類型3.3 blob 和 text類型3.4 enum類型3.4.1 創建…

label-studio的使用教程(導入本地路徑)

文章目錄 1. 準備環境2. 腳本啟動2.1 Windows2.2 Linux 3. 安裝label-studio機器學習后端3.1 pip安裝(推薦)3.2 GitHub倉庫安裝 4. 后端配置4.1 yolo環境4.2 引入后端模型4.3 修改腳本4.4 啟動后端 5. 標注工程5.1 創建工程5.2 配置圖片路徑5.3 配置工程類型標簽5.4 配置模型5.…

mysql為什么一個表中不能同時存在兩個字段自增

背景。設置sort自增。會引發錯誤 通常自增字段都是用于表示數據的唯一性。數據庫限制。需要自定義排序字段大小。

牛客round95D

原題鏈接:D-小紅的區間修改(一)_牛客周賽 Round 95 題目背景: 初始擁有一個長度10^100元素全為0的數組,進行q查詢,每次查詢如果區間內的元素都為0就將區間變為首項為 1、公差為 1 的等差數列;否…

visual studio 2022更改主題為深色

visual studio 2022更改主題為深色 點擊visual studio 上方的 工具-> 選項 在選項窗口中,選擇 環境 -> 常規 ,將其中的顏色主題改成深色 點擊確定,更改完成

實踐篇:利用ragas在自己RAG上實現LLM評估②

文章目錄 使用ragas做評估在自己的數據集上評估完整代碼代碼講解1. RAG系統構建核心組件初始化文檔處理流程 2. 評估數據集構建3. RAGAS評估實現1. 評估數據集創建2. 評估器配置3. 執行評估 本系列閱讀: 理論篇:RAG評估指標,檢索指標與生成指…

微軟PowerBI考試 PL300-在 Power BI 中清理、轉換和加載數據

微軟PowerBI考試 PL300-在 Power BI 中清理、轉換和加載數據 Power Query 具有大量專門幫助您清理和準備數據以供分析的功能。 您將了解如何簡化復雜模型、更改數據類型、重命名對象和透視數據。 您還將了解如何分析列,以便知曉哪些列包含有價值的數據,…

PostgreSQL 安裝與配置全指南(適用于 Windows、macOS 與主流 Linux 發行版)

PostgreSQL 是一個功能強大、開源、穩定的對象關系數據庫系統,廣泛用于后端開發、數據處理與分布式架構中。本指南將手把手教你如何在 Windows、macOS 以及主流 Linux 發行版 上安裝 PostgreSQL,并附上安裝驗證命令與基礎配置方法。 一、Windows 安裝與配…