大模型训练——PEFT与LORA介绍

这篇具有很好参考价值的文章主要介绍了大模型训练——PEFT与LORA介绍。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

0. 简介

朋友们好,我是练习NLP两年半的算法工程师常鸿宇,今天介绍一下大规模模型的轻量级训练技术LORA,以及相关模块PEFT。Parameter-Efficient Fine-Tuning (PEFT),是huggingface开发的一个python工具,项目地址:

https://github.com/huggingface/peft

其可以很方便地实现将普通的HF模型变成用于支持轻量级fine-tune的模型,使用非常便捷,目前支持4种策略,分别是:

  1. LoRA: LORA: LOW-RANK ADAPTATION OF LARGE LANGUAGE MODELS
  2. Prefix Tuning: Prefix-Tuning: Optimizing Continuous Prompts for Generation, P-Tuning v2: Prompt Tuning Can Be Comparable to Fine-tuning Universally Across Scales and Tasks
  3. P-Tuning: GPT Understands, Too
  4. Prompt Tuning: The Power of Scale for Parameter-Efficient Prompt Tuning

今天要介绍的,是其中之一,也是最近比较热门的LORA (LOW-RANK ADAPTATION OF LARGE LANGUAGE MODELS)。

1. LORA原理介绍

LORA的论文写的比较难读懂,但是其原理其实并不复杂。简单理解一下,就是在模型的Linear层,的旁边,增加一个“旁支”,这个“旁支”的作用,就是代替原有的参数矩阵W进行训练。

大模型训练——PEFT与LORA介绍
结合上图,我们来直观地理解一下这个过程,输入 x x x,具有维度 d d d,举个例子,在普通的transformer模型中,这个 x x x可能是embedding的输出,也有可能是上一层transformer layer的输出,而 d d d一般就是768或者1024。按照原本的路线,它应该只走左边的部分,也就是原有的模型部分。

而在LORA的策略下,增加了右侧的“旁支”,也就是先用一个Linear层A,将数据从 d d d维降到 r r r,这个 r r r也就是LORA的秩,是LORA中最重要的一个超参数。一般会远远小于 d d d,尤其是对于现在的大模型, d d d已经不止是768或者1024,例如LLaMA-7B,每一层transformer有32个head,这样一来 d d d就达到了4096.

接着再用第二个Linear层B,将数据从 r r r变回 d d d维。最后再将左右两部分的结果相加融合,就得到了输出的hidden_state。

对于左右两个部分,右侧看起来像是左侧原有矩阵 W W W的分解,将参数量从 d ∗ d d*d dd变成了 d ∗ r + d ∗ r d*r+d*r dr+dr,在 r < < d r<<d r<<d的情况下,参数量就大大地降低了。熟悉各类预训练模型的同学可能会发现,这个思想其实与Albert的思想有异曲同工之处,在Albert中,作者通过两个策略降低了训练的参数量,其一是Embedding矩阵分解,其二是跨层参数共享。

在Albert中,作者考虑到词表的维度很大,所以将Embedding矩阵分解成两个相对较小的矩阵,用来模拟Embedding矩阵的效果,这样一来需要训练的参数量就减少了很多。

LORA也是类似的思想,并且它不再局限于Embedding层,而是所有出现大矩阵的地方,理论上都可以用到这样的分解。

但是与Albert不同的是,Albert直接用两个小矩阵替换了原来的大矩阵,而LORA保留了原来的矩阵W,但是不让W参与训练,所以需要计算梯度的部分就只剩下旁支的A和B两个小矩阵。

从论文中的公式来看,在加入LORA之前,模型训练的优化表示为:
max ⁡ Φ ∑ ( x , y ) ∈ Z ∑ t = 1 ∣ y ∣ log ⁡ ( P Φ ( y t ∣ x , y < t ) ) \max \limits_{\Phi}\sum_{\left(x, y\right) \in Z} \sum_{t=1}^{\vert y \vert} \log \left(P_{\Phi} \left ( y_t \vert x, y_{<t}\right)\right) Φmax(x,y)Zt=1ylog(PΦ(ytx,y<t))
其中,模型的参数用 Φ \Phi Φ表示。

而加入了LORA之后,模型的优化表示为:
max ⁡ Θ ∑ ( x , y ) ∈ Z ∑ t = 1 ∣ y ∣ log ⁡ ( p Φ 0 + Δ Φ ( Θ ) ( y t ∣ x , y < t ) ) \max \limits_{\Theta}\sum_{\left(x, y\right) \in Z} \sum_{t=1}^{\vert y \vert} \log \left(p_{\Phi_0+\Delta\Phi \left( \Theta\right)} \left ( y_t \vert x, y_{<t}\right)\right) Θmax(x,y)Zt=1ylog(pΦ0+ΔΦ(Θ)(ytx,y<t))

其中,模型原有的参数是 Φ 0 \Phi_0 Φ0,LORA新增的参数是 Δ Φ ( Θ ) \Delta \Phi\left(\Theta\right) ΔΦ(Θ)

从第二个式子可以看到,尽管参数看起来增加了(多了 Δ Φ ( Θ ) \Delta \Phi\left(\Theta\right) ΔΦ(Θ)),但是从前面的max的目标来看,需要优化的参数只有 Θ \Theta Θ,而根据假设, Θ < < Φ \Theta <<\Phi Θ<<Φ,这就使得训练过程中,梯度计算量少了很多,所以就在低资源的情况下,我们可以只消耗 Θ \Theta Θ这部分的资源,这样一来就可以在单卡低显存的情况下训练大模型了。

但是相应地,引入LORA部分的参数,并不会在推理阶段加速,因为在前向计算的时候, Φ \Phi Φ部分还是需要参与计算的,而 Θ \Theta Θ部分是凭空增加了的参数,所以理论上,推理阶段应该比原来的计算量增大一点。

2. 补充资料:低显存学习方法

在介绍代码之前,在这里补充一些低显存学习方法的介绍。参考苏剑林老师的博客:Ladder Side-Tuning:预训练模型的“过墙梯”。其中主要介绍了一篇2022年的论文:《LST: Ladder Side-Tuning for
Parameter and Memory Efficient Transfer Learning》,其中对低显存消耗的训练方法进行了综合地介绍,包括LORA。

论文地址:https://arxiv.org/pdf/2206.06522.pdf

这里借用此文中的配图,来说明一下,在LORA之前的常见的Memory Efficient Transfer Learning方法。

大模型训练——PEFT与LORA介绍
在上图中,非常形象地展示了三种transfer learning的策略。

在普通的adapter中,在各层backbone(蓝色)之间,加入了相对较小的训练参数(绿色),以此来通过调整绿色部分,减少训练参数。然而在这种策略下,缺乏梯度的直接通路(红色虚线),在反向传播中,需要经过所有蓝色的部分。并且,这种结构在并行上也会存在一些困难。

而在prompt tuning中,也存在一些固有的缺陷,它同样缺少梯度的直接通路,每次都需要经过所有的backbone部分。而且,prompt tuning的任务设置过于理想,试图只调节输入端的小部分参数,对深层部分的影响是相当有限的,这就会造成最终fine-tune的效果受到局限。

由于LST不是本文的重点,所以只借助这个示意图来对LORA策略进行说明。而实际上,LST可以看做是在LORA的基础上做出的进一步改进,感兴趣的同学可以阅读原文。

LST与LORA类似,在原有参数矩阵的一侧增加了一个旁支通路,但是二者有些许区别:

  • LORA是将上一步的输入,在分支的时候,分别经过原有参数(类似于图中蓝色部分),以及旁支的通路(绿色可训练参数),二者之间是类似平等的,然后再将结果相加,作为下一层的输入;
  • LST是在将输入先经过原有参数,再与输入本身相加,一起送入旁支通路。

根据LST的论文,其效果是优于LORA的,但是它毕竟不是本文的主角,所以对其原理细节就不做过多的介绍了。

大模型训练——PEFT与LORA介绍

3. PEFT对LORA的实现

接下来是代码部分,我们以HF的PEFT(当前版本0.2.0)为例,介绍一下LORA是如何作用在HF模型上的。

以LORA为例,PEFT模型的使用非常方便,只需要按照原本的方式实例化模型,然后设置一下LORA的config,调用一下get_peft_model方法,就获得了在原模型基础上的PEFT模型,对于LORA策略来讲,就是在某些参数矩阵W的基础上增加了矩阵分解的旁支。在下面的例子中,选择了attention中的q和v的部分做LORA。

# 设置超参数及配置
LORA_R = 8
LORA_ALPHA = 16
LORA_DROPOUT = 0.05
TARGET_MODULES = [
    "q_proj",
    "v_proj",
]

config = LoraConfig(
    r=LORA_R,
    lora_alpha=LORA_ALPHA,
    target_modules=TARGET_MODULES,
    lora_dropout=LORA_DROPOUT,
    bias="none",
    task_type="CAUSAL_LM",
)

# 创建基础transformer模型
model = AutoModelForSeq2SeqLM.from_pretrained(model_name_or_path)
# 加入PEFT策略
model = get_peft_model(model, config)

简单介绍一下Lora config相关的配置:

参数名 含义
r lora的秩,矩阵A和矩阵B相连接的宽度,r<<d
lora_alpha 归一化超参数,lora参数 Δ W x \Delta Wx ΔWx会被以 α r \frac \alpha r rα归一化,以便减少改变 r r r时需要重新训练的计算量
lora_dropout lora层的dropout比率
merge_weights eval模式中,是否将lora矩阵的值加到原有 W 0 W_0 W0的值上
fan_in_fan_out 只有应用在Conv1D层时置为True,其他情况False
bias 是否可训练bias,none:均不可;all:均可;lora_only:只有lora部分的bias可训练
modules_to_save 除了lora部分之外,还有哪些层可以被训练,并且需要保存

接下来,结合PEFT模块的源码,来看一下LORA是如何实现的。

在PEFT模块中,peft_model.py中的PeftModel类是一个总控类,用于模型的读取保存等功能,继承了transformers中的Mixin类,我们主要来看LORA的实现:

代码位置:https://github.com/huggingface/peft/blob/main/src/peft/tuners/lora.py

class LoraModel(torch.nn.Module):
    def __init__(self, config, model):
        super().__init__()
        self.peft_config = config
        self.model = model
        self._find_and_replace()
        mark_only_lora_as_trainable(self.model, self.peft_config.bias)
        self.forward = self.model.forward

从构造方法可以看出,这个类在创建的时候主要做了两件事:

  1. _find_and_replace: 找到所有需要加入lora策略的层,例如q_proj,把它们替换成lora模式;
  2. 保留lora部分的参数可训练,其余参数全都固定下来不动。

_find_and_replace的逻辑很清晰,就是先找到需要的做lora的层,然后创建lora层把它替换掉。这里把关键语句列出如下:

找目标层:

# 其中的target_modules在上面的例子中就是"q_proj","v_proj"
# 这一步就是找到模型的各个组件中,名字里带"q_proj","v_proj"的
target_module_found = re.fullmatch(self.peft_config.target_modules, key)

然后对于每一个找到的目标层,创建一个新的lora层:

# 注意这里的Linear是在该py中新建的类,不是torch的Linear
new_module = Linear(target.in_features, target.out_features, bias=bias, **kwargs)

最后调用_replace_module方法替换掉原来的linear:

self._replace_module(parent, target_name, new_module, target)

其中这个replace的方法并不复杂,就是把原来的weight和bias赋给新创建的module,然后再分配到指定的设备上:

    def _replace_module(self, parent_module, child_name, new_module, old_module):
        setattr(parent_module, child_name, new_module)
        new_module.weight = old_module.weight
        if old_module.bias is not None:
            new_module.bias = old_module.bias
        if getattr(old_module, "state", None) is not None:
            new_module.state = old_module.state
            new_module.to(old_module.weight.device)

        # dispatch to correct device
        for name, module in new_module.named_modules():
            if "lora_" in name:
                module.to(old_module.weight.device)

接下来主要看一下Lora层的实现,首先是Lora的基类,可以看出这个类就是用来构造Lora的各种超参数用:

class LoraLayer:
    def __init__(
        self,
        r: int,
        lora_alpha: int,
        lora_dropout: float,
        merge_weights: bool,
    ):
        self.r = r
        self.lora_alpha = lora_alpha
        # Optional dropout
        if lora_dropout > 0.0:
            self.lora_dropout = nn.Dropout(p=lora_dropout)
        else:
            self.lora_dropout = lambda x: x
        # Mark the weight as unmerged
        self.merged = False
        self.merge_weights = merge_weights
        self.disable_adapters = False

然后就要讲到上文中所提到的Linear类,也就是Lora的具体实现,它同时继承了nn.Linear和LoraLayer。

class Linear(nn.Linear, LoraLayer):
    # Lora implemented in a dense layer
    def __init__(
        self,
        in_features: int,
        out_features: int,
        r: int = 0,
        lora_alpha: int = 1,
        lora_dropout: float = 0.0,
        fan_in_fan_out: bool = False,  # Set this to True if the layer to replace stores weight like (fan_in, fan_out)
        merge_weights: bool = True,
        **kwargs,
    ):
        nn.Linear.__init__(self, in_features, out_features, **kwargs)
        LoraLayer.__init__(self, r=r, lora_alpha=lora_alpha, lora_dropout=lora_dropout, merge_weights=merge_weights)

        self.fan_in_fan_out = fan_in_fan_out
        # Actual trainable parameters
        if r > 0:
            self.lora_A = nn.Linear(in_features, r, bias=False)
            self.lora_B = nn.Linear(r, out_features, bias=False)
            self.scaling = self.lora_alpha / self.r
            # Freezing the pre-trained weight matrix
            self.weight.requires_grad = False
        self.reset_parameters()
        if fan_in_fan_out:
            self.weight.data = self.weight.data.T

在构造方法中,除了对各个超参数进行配置之外,还对所有参数进行了初始化,定义如下:

    def reset_parameters(self):
        nn.Linear.reset_parameters(self)
        if hasattr(self, "lora_A"):
            # initialize A the same way as the default for nn.Linear and B to zero
            nn.init.kaiming_uniform_(self.lora_A.weight, a=math.sqrt(5))
            nn.init.zeros_(self.lora_B.weight)

其中lora的A矩阵采用了kaiming初始化,是Xavier初始化针对非线性激活函数的一种优化;B矩阵采用了零初始化,以确保在初始状态 Δ W = B A \Delta W =BA ΔW=BA为零。(值得注意的是在LORA的论文中,A采用的是Gaussian初始化)。

对于train和eval方法,放在一起介绍,它主要是需要对merge状态进行记录:

    def train(self, mode: bool = True):
        nn.Linear.train(self, mode)
        self.lora_A.train(mode)
        self.lora_B.train(mode)
        if not mode and self.merge_weights and not self.merged:
            # Merge the weights and mark it
            if self.r > 0:
                self.weight.data += (
                    transpose(self.lora_B.weight @ self.lora_A.weight, self.fan_in_fan_out) * self.scaling
                )
            self.merged = True
        elif self.merge_weights and self.merged:
            # Make sure that the weights are not merged
            if self.r > 0:
                self.weight.data -= (
                    transpose(self.lora_B.weight @ self.lora_A.weight, self.fan_in_fan_out) * self.scaling
                )
            self.merged = False

    def eval(self):
        nn.Linear.eval(self)
        self.lora_A.eval()
        self.lora_B.eval()

首先对于新定义的这个Linear层,其本身继承了torch.nn.Linear,所以需要调用nn.Linear.train(self, mode)来控制一下自身原本参数的状态,并且此外它加入了lora_A和lora_B两部分额外的参数,这两部分本质上也是nn.Linear,也需要控制状态。

然后主要来理解一下merge_weights是在做什么,也就是看train中的if分支,not mode说明是eval模式,而self.merge_weights在上文中有介绍,是配置文件中的,意思是评估时是否需要将lora部分的weight加到linear层原本的weight中,not self.merged是状态的记录,也就是说,如果设置了需要融合,而当前状态没有融合的话,就把lora部分的参数scale之后加上去,并且更新self.merged状态;在elif分支中,是为了在训练的过程中,确保linear本身的weights是没有经过融合过的(理论上这一步应该是在eval之后的下一轮train的第一个step触发)。

至于为什么是在train中涉及merge_weights,其实在torch的源码中,nn.Linear.eval()实际上是调用了nn.Linear.train(mode=False),所以这里train方法中的merge_weigths,实际上是在eval中也发挥作用的。

forward中也是类似的原理,正常情况下训练过程应该是走elif的分支:

    def forward(self, x: torch.Tensor):
        if self.disable_adapters:
            if self.r > 0 and self.merged:
                self.weight.data -= (
                    transpose(self.lora_B.weight @ self.lora_A.weight, self.fan_in_fan_out) * self.scaling
                )
                self.merged = False

            return F.linear(x, transpose(self.weight, self.fan_in_fan_out), bias=self.bias)
        elif self.r > 0 and not self.merged:
            result = F.linear(x, transpose(self.weight, self.fan_in_fan_out), bias=self.bias)
            if self.r > 0:
                result += self.lora_B(self.lora_A(self.lora_dropout(x))) * self.scaling
            return result
        else:
            return F.linear(x, transpose(self.weight, self.fan_in_fan_out), bias=self.bias)

在了解了这些基本原理之后,就可以类似地去实现更多更加灵活的功能了,例如对transformer的某些层增加lora,而其余的层保持不变等。

以上就是关于LORA的代码实现介绍,在实际的PEFT模块中,还包含了更多更详细完备的设置,本文只是对基本原理和过程进行了介绍,其中包含了部分个人理解,如果错误,还请指出。如果本文对你的学习和工作有所帮助,记得留下一个免费的赞,我们下期再见。文章来源地址https://www.toymoban.com/news/detail-416615.html

到了这里,关于大模型训练——PEFT与LORA介绍的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • Stable Diffusion训练Lora模型

    以下内容参考:https://www.bilibili.com/video/BV1Qk4y1E7nv/?spm_id_from=333.337.search-card.all.clickvd_source=3969f30b089463e19db0cc5e8fe4583a 第一步,准备训练要使用的图片,即优质的图片 第二部,为这些图片打标,即精准的tag 数量建议20-50张,最多100张图片 不好的图片:模糊的,动作扭曲的,脸部被

    2024年02月12日
    浏览(70)
  • LLMs之Chinese-LLaMA-Alpaca-2:源码解读(run_clm_pt_with_peft.py文件)—模型训练前置工作(参数解析+配置日志)→模型初始化(检测是否存在训练过的chec

    LLMs之Chinese-LLaMA-Alpaca-2:源码解读(run_clm_pt_with_peft.py文件)—模型训练前置工作(参数解析+配置日志)→模型初始化(检测是否存在训练过的checkpoint+加载预训练模型和tokenizer)→数据预处理(处理【标记化+分块】+切分txt数据集)→优化模型配置( 量化模块 +匹配模型vocabulary大小与to

    2024年02月07日
    浏览(44)
  • LLMs之Chinese-LLaMA-Alpaca-2:源码解读(run_clm_sft_with_peft.py文件)—模型训练前置工作(参数解析+配置日志)→模型初始化(检测是否存在训练过的che

    LLMs之Chinese-LLaMA-Alpaca-2:源码解读(run_clm_sft_with_peft.py文件)—模型训练前置工作(参数解析+配置日志)→模型初始化(检测是否存在训练过的checkpoint+加载预训练模型和tokenizer)→数据预处理(监督式任务的数据收集器+指令数据集【json格式】)→优化模型配置(量化模块+匹配模型voca

    2024年02月06日
    浏览(49)
  • AI绘画:Lora模型训练完整流程!

    关于AI绘画(基于Stable Diffusion Webui),我之前已经写过三篇文章,分别是 软件安装,基本的使用方法,微调模型LoRA的使用。  整体来说还是比简单的,搞个别人的模型,搞个提示词就出图了。今天来一个有些难度的,自己训练一个LoRA微调模型。 LoRA的全称是Low-Rank Adaptation 

    2024年02月07日
    浏览(44)
  • LORA大模型加速微调和训练算法

    ChatGPT带领着大模型像雨后春笋一般层出不穷,大家都对大模型微调跃跃欲试,现在咱们聊聊其中的常见的算法 1 LORA 低秩适应 理论 Lora( Low-Rank Adaotation),低秩自适应模型微调的方法,它冻结预训练模型的权重,并将可训练的秩分解矩阵注入到transformer架构的每一层,从而大大

    2024年02月05日
    浏览(52)
  • Stable Diffusion Lora模型训练详细教程

    通过Lora小模型可以控制很多特定场景的内容生成。 但是那些模型是别人训练好的,你肯定很好奇,我也想训练一个自己的专属模型(也叫炼丹~_~)。 甚至可以训练一个专属家庭版的模型(family model),非常有意思。 将自己的训练好的Lora模型放到stableDiffusion lora 目录中,

    2024年02月02日
    浏览(51)
  • LoRa模型训练教程(炼丹,Stable Diffusion)

    何为LoRA?LoRA并不是扩散模型专有的技术,而是从隔壁语言模型(LLM)迁移过来的,旨在解决避免将整个模型参数拷贝下来才能对模型进行调校的问题。因为大型语言模型的参数量过于恐怖,比如最近新出的GPT-4参数量约为100 万亿。 LoRA采用的方式是向原有的模型中插入新的数

    2024年02月10日
    浏览(43)
  • Stable Diffusion 使用lora-scripts WebUI训练LoRA模型

    如果对代码使用有困难的小伙伴可以直接使用WebUI版的LoRA模块进行训练操作。不管是训练人物,场景,风格,还是服装都是一套通用的模式,仅仅是使用不同的数据集得到的结果不同。 使用 git clone --recurse-submodules https://gi

    2024年02月17日
    浏览(51)
  • 【 stable diffusion LORA模型训练最全最详细教程】

    个人网站:https://tianfeng.space/ 其实想写LORA模型训练很久了,一直没时间,总结一下现在主流的两种LORA模型训练方式,分别是朱尼酱的赛博丹炉和秋叶大佬的训练脚本,训练效果应该是赛博丹炉更好,我个人更推荐朱尼酱的赛博丹炉,界面炫酷,操作简单,作者也是花了很多

    2024年02月09日
    浏览(54)
  • Stable Diffusion 指定模型人物,Lora 训练全流程

    在使用 Stable Diffusion 的时候,可以选择别人训练好的 Lora,那么如何训练自己的 Lora,本篇文章介绍了介绍了如何训练Lora,如何从训练的模型中选择好的模型,如何在 Stable Diffusion 中使用。 闲话不多说,直接实际操作吧,干货满满,记得关注哦,以免找不到了。首先我们来获

    2024年02月09日
    浏览(62)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包