FineTune|大模型微调技术综述

2024/02/17 FineTune 共 36629 字,约 105 分钟
AI Freedom

大模型的微调技术,从不同的方面,有不同的分类。高效微调技术可以粗略分为以下三大类:增加额外参数(Addition-Based)、选取一部分参数更新(Selection-Based)、引入重参数化(Reparametrization-Based)。而在增加额外参数这类方法中,又主要分为类适配器(Adapter-like)方法和软提示(Soft prompts)两个小类。

大模型的微调技术,从不同的方面,有不同的分类:

  • 从参数规模来说,可以简单分为全参数微调和高效参数微调。前者一般是用预训练模型作为初始化权重,在特定数据集上继续训练,全部参数都更新的方法。而后者则是期望用更少的资源完成模型参数的更新,包括只更新一部分参数或者说通过对参数进行某种结构化约束,例如稀疏化或低秩近似来降低微调的参数数量。
  • 如果按照在模型哪个阶段使用微调,或者根据模型微调的目标来区分,也可以从提示微调、指令微调、有监督微调的方式来。
  • 高效微调技术可以粗略分为以下三大类:增加额外参数(Addition-Based)、选取一部分参数更新(Selection-Based)、引入重参数化(Reparametrization-Based)。而在增加额外参数这类方法中,又主要分为类适配器(Adapter-like)方法和软提示(Soft prompts)两个小类。
    • 增加额外参数 Addition-Based,如:Prefix Tuning、Prompt Tuning、Adapter Tuning及其变体
    • 选取一部分参数更新 Selection-Based,如:BitFit
    • 引入重参数化 Reparametrization-Based,如:LoRA、AdaLoRA、QLoRA
    • 混合高效微调,如:MAM Adapter、UniPELT

PEFT仓库是一个用于微调大模型的工具库,提供了多种高效微调技术的实现。https://github.com/huggingface/peft.git

常见的微调技术有Instruction Tuning、BitFit、Prefix Tuning、Prompt Tuning、P-Tuning、Adapter Tuning、LoRA、RLHF等,下文详细介绍。

论文: Scaling Down to Scale Up- A Guide to Parameter-Efficient Fine-Tuning

论文:Parameter-Efficient Fine-Tuning Methods for Pretrained Language Models: A Critical Review and Assessment

1. Instruction Tuning

论文:Instruction Tuning for Large Language Models: A Survey

github: https://github.com/xiaoya-li/Instruction-Tuning-Survey

指令微调是一种通过在由(指令,输出)组成的数据集上进一步训练LLMs的过程。其中,指令代表模型的人类指令,输出代表遵循指令的期望输出。这个过程有助于弥合LLMs的下一个词预测目标与用户让LLMs遵循人类指令的目标之间的差距。

指令微调可以被视为有监督微调(Supervised Fine-Tuning,SFT)的一种特殊形式。但是,它们的目标依然有差别。SFT是一种使用标记数据对预训练模型进行微调的过程,以便模型能够更好地执行特定任务。而指令微调是一种通过在包括(指令,输出)对的数据集上进一步训练大型语言模型(LLMs)的过程,以增强LLMs的能力和可控性。指令微调的特殊之处在于其数据集的结构,即由人类指令和期望的输出组成的配对。这种结构使得指令微调专注于让模型理解和遵循人类指令。

总的来说,指令微调是有监督微调的一种特殊形式,专注于通过理解和遵循人类指令来增强大型语言模型的能力和可控性。虽然它们的目标和方法相似,但指令微调的特殊数据结构和任务关注点使其成为SFT的一个独特子集。

指令微调常用的数据集分为以下三大类:

1. 泛化到未见任务

  • 这类数据集通常包含多样化的任务,每个任务都有专门的指令和数据样例。模型在这类数据集上训练后,可以泛化到未见过的新任务上。
  • 比如 UnifiedQA、OIG、UnifiedSKG 数据集。

2. 在单轮中遵循用户指令

  • 这类数据集包含指令及其对应的响应,用于训练模型单轮回复用户指令。训练后,模型可以理解指令并作出回复。
  • 比如 InstructGPT、Unnatural Instructions、Self-Instruct 数据集。

3. 像人类一样提供帮助

  • 这类数据集包含多轮闲聊对话。训练后,模型可以进行多轮交互,像人类一样提供帮助。
  • 比如 ChatGPT、Vicuna、Guanaco 数据集。

总体来说,第一类数据集侧重任务泛化能力,第二类侧重单轮指令理解能力,第三类侧重连续多轮对话能力。可以根据所需的模型能力选择不同类型的数据集进行指令调优。

2. BitFit

论文:BitFit: Simple Parameter-efficient Fine-tuning or Transformer-based Masked Language-models

github: https://github.com/benzakenelad/BitFit

BitFit是一种稀疏的微调方法,它训练时只更新bias的参数或者部分bias参数。

对于Transformer模型而言,冻结大部分 transformer-encoder 参数,只更新bias参数跟特定任务的分类层参数。涉及到的bias参数有attention模块中计算query,key,value跟合并多个attention结果时涉及到的bias,MLP层中的bias,Layernormalization层的bias参数。

在Bert-Base/Bert-Large这种模型里,bias参数仅占模型全部参数量的0.08%~0.09%。但是通过在Bert-Large模型上基于GLUE数据集进行了 BitFit、Adapter和Diff-Pruning的效果对比发现,BitFit在参数量远小于Adapter、Diff-Pruning的情况下,效果与Adapter、Diff-Pruning相当,甚至在某些任务上略优于Adapter、Diff-Pruning。

通过实验结果还可以看出,BitFit微调结果相对全量参数微调而言, 只更新极少量参数的情况下,在多个数据集上都达到了不错的效果,虽不及全量参数微调,但是远超固定全部模型参数的Frozen方式。

对比BitFit训练前后的参数,发现很多bias参数并没有太多变化(例如:跟计算key所涉及到的bias参数)。发现计算query和将特征维度从N放大到4N的FFN层(intermediate)的bias参数变化最为明显,只更新这两类bias参数也能达到不错的效果,反之,固定其中任何一者,模型的效果都有较大损失。

3. Prefix Tuning

论文:Prefix-Tuning: Optimizing Continuous Prompts for Generation

Prefix Tuning提出固定预训练LM,为LM添加可训练,任务特定的前缀,这样就可以为不同任务保存不同的前缀,微调成本也小;同时,这种Prefix实际就是连续可微的Virtual Token(Soft Prompt/Continuous Prompt),相比离散的Token,更好优化,效果更好。

Prefix Tuning 在输入token之前构造一段任务相关的virtual tokens作为Prefix,然后训练的时候只更新Prefix部分的参数,而PLM中的其他部分参数固定。

针对不同的模型结构,需要构造不同的Prefix。

  • 针对自回归架构模型:在句子前面添加前缀,得到 z = [PREFIX; x; y],合适的上文能够在固定 LM 的情况下去引导生成下文(比如:GPT3的上下文学习)。
  • 针对编码器-解码器架构模型:Encoder和Decoder都增加了前缀,得到 z = [PREFIX; x; PREFIX0; y]。Encoder端增加前缀是为了引导输入部分的编码,Decoder 端增加前缀是为了引导后续token的生成。

该方法其实和构造Prompt类似,只是Prompt是人为构造的“显式”的提示,并且无法更新参数,而Prefix则是可以学习的“隐式”的提示。

为了防止直接更新Prefix的参数导致训练不稳定和性能下降的情况,在Prefix层前面加了MLP结构,训练完成后,只保留Prefix的参数。

通过消融实验证实,只调整embedding层的表现力不够,将导致性能显著下降,因此,在每层都加了prompt的参数,改动较大。

实验还对比了位置对于生成效果的影响,Prefix-tuning也是要略优于Infix-tuning的。其中,Prefix-tuning形式为 [PREFIX; x; y],Infix-tuning形式为 [x; INFIX; y]。 上图所示,P_idx表示加的前缀序列, h对应的是可学习的参数, 用Pθ=[h1, h2, h3, …]表示可学习参数矩阵。直接学习参数效果不好,所以使用MLP网络对Pθ进行了reparameter修正,即Pθ[i,:] = MLP(Pθ’[i,:]),重训完只用保存prefix的Pθ相关参数。

h is composed of a key-value pair. In GPT-2, the idimension of each key and value is 1024. Prefix Tuning专门针对key和value向量添加前缀,这样做的目的是在不显著改变模型原有参数的情况下,通过这些额外的、任务特定的信号来调整模型的行为。

huggingface peft关于prefix-tuning的核心代码实现在prefix_tuning

prefix_tuning

import torch
class PrefixEncoder(torch.nn.Module):
    r"""
    The `torch.nn` model to encode the prefix.

    Args:
        config ([`PrefixTuningConfig`]): The configuration of the prefix encoder.

    Example:

    ```py
    >>> from peft import PrefixEncoder, PrefixTuningConfig

    >>> config = PrefixTuningConfig(
    ...     peft_type="PREFIX_TUNING",
    ...     task_type="SEQ_2_SEQ_LM",
    ...     num_virtual_tokens=20,
    ...     token_dim=768,
    ...     num_transformer_submodules=1,
    ...     num_attention_heads=12,
    ...     num_layers=12,
    ...     encoder_hidden_size=768,
    ... )
    >>> prefix_encoder = PrefixEncoder(config)
    ```

    **Attributes**:
        - **embedding** (`torch.nn.Embedding`) -- The embedding layer of the prefix encoder.
        - **transform** (`torch.nn.Sequential`) -- The two-layer MLP to transform the prefix embeddings if
          `prefix_projection` is `True`.
        - **prefix_projection** (`bool`) -- Whether to project the prefix embeddings.

    Input shape: (`batch_size`, `num_virtual_tokens`)

    Output shape: (`batch_size`, `num_virtual_tokens`, `2*layers*hidden`)
    """

    def __init__(self, config):
        super().__init__()
        self.prefix_projection = config.prefix_projection
        token_dim = config.token_dim
        num_layers = config.num_layers
        encoder_hidden_size = config.encoder_hidden_size
        num_virtual_tokens = config.num_virtual_tokens
        if self.prefix_projection and not config.inference_mode:
            # Use a two-layer MLP to encode the prefix
            self.embedding = torch.nn.Embedding(num_virtual_tokens, token_dim)
            self.transform = torch.nn.Sequential(
                torch.nn.Linear(token_dim, encoder_hidden_size),
                torch.nn.Tanh(),
                torch.nn.Linear(encoder_hidden_size, num_layers * 2 * token_dim),
            )
        else:
            self.embedding = torch.nn.Embedding(num_virtual_tokens, num_layers * 2 * token_dim)

    def forward(self, prefix: torch.Tensor):
        if self.prefix_projection:
            prefix_tokens = self.embedding(prefix)
            past_key_values = self.transform(prefix_tokens)
        else:
            past_key_values = self.embedding(prefix)
        return past_key_values

4. Prompt Tuning

论文: The Power of Scale for Parameter-Efficient Prompt Tuning

Prompt Tuning,该方法可以看作是Prefix Tuning的简化版本,它给每个任务定义了自己的Prompt,然后拼接到数据上作为输入,但只在输入层加入prompt tokens,并且不需要加入 MLP 进行调整来解决难训练的问题。通过实验发现,随着预训练模型参数量的增加,Prompt Tuning的方法会逼近全参数微调的结果。

Prompt Tuning 还提出了 Prompt Ensembling,也就是在一个批次(Batch)里同时训练同一个任务的不同 prompt(即采用多种不同方式询问同一个问题),这样相当于训练了不同模型,比模型集成的成本小多了。

作者做了一系列对比实验,都在说明:随着预训练模型参数的增加,一切的问题都不是问题,最简单的设置也能达到极好的效果。

  • Prompt 长度影响:模型参数达到一定量级时,Prompt 长度为1也能达到不错的效果,Prompt 长度为20就能达到极好效果。
  • prompt初始化方式影响:Random Uniform 方式明显弱于其他两种,但是当模型参数达到一定量级,这种差异也不复存在。
  • 预训练的方式:LM Adaptation 的方式效果好,但是当模型达到一定规模,差异又几乎没有了。
  • 微调步数影响:模型参数较小时,步数越多,效果越好。同样随着模型参数达到一定规模,zero shot 也能取得不错效果。
  • 当参数达到100亿规模与全参数微调方式效果无异。

对model tuning和prompt tuning做了上图的对比,prompt tuning可以大幅节省参数量。对于T5的XXL的model来说,全量的model tuning每个下游任务需要11B的参数量,用prompt tuning只需要20480个参数。需要注意跟prefix-tuning不同点:这里的prompt-tuning没有包含中间层的prefix,也没有对下游任务的输出网络进行修改。在prefix-tuning中使用了MLP进行prefix的reparameter。

huggingface peft关于prompt tuning的核心代码实现在prompt_tuning

prompt_tuning

import math
import torch
from .config import PromptTuningInit

class PromptEmbedding(torch.nn.Module):
    """
    The model to encode virtual tokens into prompt embeddings.

    Args:
        config ([`PromptTuningConfig`]): The configuration of the prompt embedding.
        word_embeddings (`torch.nn.Module`): The word embeddings of the base transformer model.

    **Attributes**:
        - **embedding** (`torch.nn.Embedding`) -- The embedding layer of the prompt embedding.

    Example:

    ```py
    >>> from peft import PromptEmbedding, PromptTuningConfig

    >>> config = PromptTuningConfig(
    ...     peft_type="PROMPT_TUNING",
    ...     task_type="SEQ_2_SEQ_LM",
    ...     num_virtual_tokens=20,
    ...     token_dim=768,
    ...     num_transformer_submodules=1,
    ...     num_attention_heads=12,
    ...     num_layers=12,
    ...     prompt_tuning_init="TEXT",
    ...     prompt_tuning_init_text="Predict if sentiment of this review is positive, negative or neutral",
    ...     tokenizer_name_or_path="t5-base",
    ... )

    >>> # t5_model.shared is the word embeddings of the base model
    >>> prompt_embedding = PromptEmbedding(config, t5_model.shared)
    ```

    Input Shape: (`batch_size`, `total_virtual_tokens`)

    Output Shape: (`batch_size`, `total_virtual_tokens`, `token_dim`)
    """

    def __init__(self, config, word_embeddings):
        super().__init__()

        total_virtual_tokens = config.num_virtual_tokens * config.num_transformer_submodules
        self.embedding = torch.nn.Embedding(total_virtual_tokens, config.token_dim)
        if config.prompt_tuning_init == PromptTuningInit.TEXT and not config.inference_mode:
            from transformers import AutoTokenizer

            tokenizer_kwargs = config.tokenizer_kwargs or {}
            tokenizer = AutoTokenizer.from_pretrained(config.tokenizer_name_or_path, **tokenizer_kwargs)
            init_text = config.prompt_tuning_init_text
            init_token_ids = tokenizer(init_text)["input_ids"]
            # Trim or iterate until num_text_tokens matches total_virtual_tokens
            num_text_tokens = len(init_token_ids)
            if num_text_tokens > total_virtual_tokens:
                init_token_ids = init_token_ids[:total_virtual_tokens]
            elif num_text_tokens < total_virtual_tokens:
                num_reps = math.ceil(total_virtual_tokens / num_text_tokens)
                init_token_ids = init_token_ids * num_reps
            init_token_ids = init_token_ids[:total_virtual_tokens]
            init_token_ids = torch.LongTensor(init_token_ids).to(word_embeddings.weight.device)

            word_embedding_weights = word_embeddings(init_token_ids).detach().clone()
            word_embedding_weights = word_embedding_weights.to(torch.float32)
            self.embedding.weight = torch.nn.Parameter(word_embedding_weights)

    def forward(self, indices):
        # Just get embeddings
        prompt_embeddings = self.embedding(indices)
        return prompt_embeddings

5. P-Tuning

论文:GPT Understands, Too

该方法的提出主要是为了解决人工设计 Prompt 的问题:

  • 大模型的Prompt构造方式严重影响下游任务的效果。比如:GPT-3采用人工构造的模版来做上下文学习(in context learning),但人工设计的模版的变化特别敏感,加一个词或者少一个词,或者变动位置都会造成比较大的变化。
  • 同时,近来的自动化搜索模版工作成本也比较高,以前这种离散化的token的搜索出来的结果可能并不是最优的,导致性能不稳定。

P-Tuning的创新之处在于将提示 (Prompt) 转化为可学习的嵌入层 (Embedding Layer), 但直接对嵌入层参数进行优化时面临两大挑战:

  • 离散性(Discreteness):与 已经通过预训练优化过的语料嵌入层 相比,直接对输入提示的嵌入进行随机初始化,然后开始训练,模型就必须从头开始学习语言的所有细节。这不仅效率低,而且很可能因为训练数据的限制而陷入局部最优解。
  • 关联性(Association):这种方法难以有效捕捉提示嵌入之间的相互关系。

GPT Understands, Too 提出了名为P-tuning的方法,成功地实现了模版的自动构建。不仅如此,借助P-tuning,GPT在SuperGLUE上的成绩首次超过了同等级别的BERT模型,这颠覆了一直以来“GPT不擅长NLU”的结论,也是该论文命名的缘由。

  • P-Tuning,设计了一种连续可微的virtual token(同Prefix-Tuning类似)。将Prompt转换为可以学习的Embedding层,并用MLP+LSTM的方式来对Prompt Embedding进行一层处理。

  • 经过预训练的LM的词嵌入已经变得高度离散,如果随机初始化virtual token,容易优化到局部最优值,而这些virtual token理论是应该有相关关联的。因此,作者通过实验发现用一个prompt encoder来编码会收敛更快,效果更好。即用一个LSTM+MLP去编码这些virtual token以后,再输入到模型。

  • 从对比实验证实看出,P-Tuning获得了与全参数一致的效果。甚至在某些任务上优于全参数微调。

  • 并且在实验中还发现,相同参数规模,如果进行全参数微调,Bert的在NLU任务上的效果,超过GPT很多;但是在P-Tuning下,GPT可以取得超越Bert的效果。

对比Adapter/Prefix Tuning

  • 对比Adapter:P-tuning实际上也是一种类似Adapter的做法,同样是固定原模型的权重,然后插入一些新的可优化参数,只不过这时候新参数是作为模板插入在Embedding层。
  • 对比Prefix Tuning:P-Tuning加入的可微virtual token,但仅限于输入层,没有在每一层都加;另外,virtual token插入的位置是可选的,不一定是前缀。

为什么P-tuning优于Fine-tuning

P-tuning和Fine-tuning都是微调所有权重,为什么P-tuning优于Fine-tuning? 这是因为,不管是PET还是P-tuning,它们其实都更接近预训练任务,而加个全连接层的做法,其实还没那么接近预训练任务,所以某种程度上来说,P-tuning有效更加“显然”,反而是加个全连接层微调为什么会有效才是值得疑问的。 在论文《A Mathematical Exploration of Why Language Models Help Solve Downstream Tasks》中,作者的回答是:

  1. 预训练模型是某种语言模型任务;
  2. 下游任务可以表示为该种语言模型的某个特殊情形;
  3. 当输出空间有限的时候,它又近似于加一个全连接层;
  4. 所以加一个全连接层微调是有效的。

所以说PET、P-tuning等才是更自然的使用预训练模型的方式,加全连接直接finetune的做法其实只是它们的推论罢了。也就是说,PET、P-tuning才是返璞归真、回归本质的方案,所以它们更有效。

P-Tuning方法中会在连续向量空间中自动搜索合适的prompt,来增强重训练的效果。

对于之前存在的离散prompt搜索方法(discrete prompt search)来说, 比如AUTOPROMPT、LPAQA, 其中的Prompt Generator通过接受离散的反馈来选择合适的prompt。

对于Prompt Generator来说,给定一个词库V和语言模型M, P_i表示在prompt模版T中第i个token,会用词库V中的词来填充模版并生成embedding向量,

例如:[e_template(The), e_template(capital), e_template(of), e_input(Britain), e_template(is), e_output([Mask])], 其中template表示模版的,input表示输入,ouput表示输出。

而在P-Tuning中通过Prompt Encoder来实现prompt的生成,跟之前的区别在于这里使用了伪prompt和反向传播来对encoder进行更新。在embedding的输入上有所不同,模版中的prompt token embedding向量都是从Prompt Encoder生成出来的,没有对应词库中具体的词。

例如:{h0, …, hi, e_input(Britain), hi+1, …, hm, e([MASK])}

在网络结构上使用embedding层加上基于双层LSTM和relu激活的MLP来实现。训练过程中使用LSTM,但在推理过程中可以把LSTM给去掉。

最终在效果上实现了bert等大模型重训的提升。 huggingface peft关于P-Tuning的核心代码实现在p_tuning

p_tuning

import warnings
import torch
from .config import PromptEncoderConfig, PromptEncoderReparameterizationType


class PromptEncoder(torch.nn.Module):
    """
    The prompt encoder network that is used to generate the virtual token embeddings for p-tuning.

    Args:
        config ([`PromptEncoderConfig`]): The configuration of the prompt encoder.

    Example:

    ```py
    >>> from peft import PromptEncoder, PromptEncoderConfig

    >>> config = PromptEncoderConfig(
    ...     peft_type="P_TUNING",
    ...     task_type="SEQ_2_SEQ_LM",
    ...     num_virtual_tokens=20,
    ...     token_dim=768,
    ...     num_transformer_submodules=1,
    ...     num_attention_heads=12,
    ...     num_layers=12,
    ...     encoder_reparameterization_type="MLP",
    ...     encoder_hidden_size=768,
    ... )

    >>> prompt_encoder = PromptEncoder(config)
    ```

    **Attributes**:
        - **embedding** (`torch.nn.Embedding`) -- The embedding layer of the prompt encoder.
        - **mlp_head** (`torch.nn.Sequential`) -- The MLP head of the prompt encoder if `inference_mode=False`.
        - **lstm_head** (`torch.nn.LSTM`) -- The LSTM head of the prompt encoder if `inference_mode=False` and
        `encoder_reparameterization_type="LSTM"`.
        - **token_dim** (`int`) -- The hidden embedding dimension of the base transformer model.
        - **input_size** (`int`) -- The input size of the prompt encoder.
        - **output_size** (`int`) -- The output size of the prompt encoder.
        - **hidden_size** (`int`) -- The hidden size of the prompt encoder.
        - **total_virtual_tokens** (`int`): The total number of virtual tokens of the
        prompt encoder.
        - **encoder_type** (Union[[`PromptEncoderReparameterizationType`], `str`]): The encoder type of the prompt
          encoder.


    Input shape: (`batch_size`, `total_virtual_tokens`)

    Output shape: (`batch_size`, `total_virtual_tokens`, `token_dim`)
    """

    def __init__(self, config):
        super().__init__()
        self.token_dim = config.token_dim
        self.input_size = self.token_dim
        self.output_size = self.token_dim
        self.hidden_size = config.encoder_hidden_size
        self.total_virtual_tokens = config.num_virtual_tokens * config.num_transformer_submodules
        self.encoder_type = config.encoder_reparameterization_type

        # embedding
        self.embedding = torch.nn.Embedding(self.total_virtual_tokens, self.token_dim)
        if not config.inference_mode:
            if self.encoder_type == PromptEncoderReparameterizationType.LSTM:
                lstm_dropout = config.encoder_dropout
                num_layers = config.encoder_num_layers
                # LSTM
                self.lstm_head = torch.nn.LSTM(
                    input_size=self.input_size,
                    hidden_size=self.hidden_size,
                    num_layers=num_layers,
                    dropout=lstm_dropout,
                    bidirectional=True,
                    batch_first=True,
                )

                self.mlp_head = torch.nn.Sequential(
                    torch.nn.Linear(self.hidden_size * 2, self.hidden_size * 2),
                    torch.nn.ReLU(),
                    torch.nn.Linear(self.hidden_size * 2, self.output_size),
                )

            elif self.encoder_type == PromptEncoderReparameterizationType.MLP:
                encoder_num_layers_default = PromptEncoderConfig.encoder_num_layers
                if config.encoder_num_layers != encoder_num_layers_default:
                    warnings.warn(
                        f"for {self.encoder_type.value}, the argument `encoder_num_layers` is ignored. "
                        f"Exactly {encoder_num_layers_default} MLP layers are used."
                    )
                layers = [
                    torch.nn.Linear(self.input_size, self.hidden_size),
                    torch.nn.ReLU(),
                    torch.nn.Linear(self.hidden_size, self.hidden_size),
                    torch.nn.ReLU(),
                    torch.nn.Linear(self.hidden_size, self.output_size),
                ]
                self.mlp_head = torch.nn.Sequential(*layers)

            else:
                raise ValueError("Prompt encoder type not recognized. Please use one of MLP (recommended) or LSTM.")

    def forward(self, indices):
        input_embeds = self.embedding(indices)
        if self.encoder_type == PromptEncoderReparameterizationType.LSTM:
            output_embeds = self.mlp_head(self.lstm_head(input_embeds)[0])
        elif self.encoder_type == PromptEncoderReparameterizationType.MLP:
            output_embeds = self.mlp_head(input_embeds)
        else:
            raise ValueError("Prompt encoder type not recognized. Please use one of MLP (recommended) or LSTM.")

        return output_embeds

6. P-Tuning V2

论文:P-Tuning v2: Prompt Tuning Can Be Comparable to Fine-tuning Universally Across Scales and Tasks

github: https://github.com/THUDM/P-tuning-v2

P-Tuning 的问题是在小参数量模型上表现差。

于是就有了v2版本:《P-Tuning v2: Prompt Tuning Can Be Comparable to Fine-tuning Universally Across Scales and Tasks》。

从标题就可以看出,P-Tuning v2 的目标就是要让 Prompt Tuning 能够在不同参数规模的预训练模型、针对不同下游任务的结果上都达到匹敌 Fine-tuning 的结果。

之前的Prompt Tuning和P-Tuning等方法存在两个主要的问题:

第一,缺乏模型参数规模和任务通用性。

  • 缺乏规模通用性:Prompt Tuning论文中表明当模型规模超过100亿个参数时,提示优化可以与全量微调相媲美。但是对于那些较小的模型(从100M到1B),提示优化和全量微调的表现有很大差异,这大大限制了提示优化的适用性。
  • 缺乏任务普遍性:尽管Prompt Tuning和P-tuning在一些 NLU 基准测试中表现出优势,但提示调优对硬序列标记任务(即序列标注)的有效性尚未得到验证。

第二,缺少深度提示优化,在Prompt Tuning和P-tuning中,连续提示只被插入transformer第一层的输入embedding序列中,在接下来的transformer层中,插入连续提示的位置的embedding是由之前的transformer层计算出来的,这可能导致两个可能的优化挑战。

  • 由于序列长度的限制,可调参数的数量是有限的。
  • 输入embedding对模型预测只有相对间接的影响。

考虑到这些问题,作者提出了Ptuning v2,它利用深度提示优化(如:Prefix Tuning),对Prompt Tuning和P-Tuning进行改进,作为一个跨规模和NLU任务的通用解决方案。

相比 Prompt Tuning 和 P-tuning 的方法, P-tuning v2 方法在每一层加入了 Prompts tokens 作为输入,带来两个方面的好处:

  • 带来更多可学习的参数(从 P-tuning 和 Prompt Tuning 的0.1%增加到0.1%-3%),同时也足够 parameter-efficient。
  • 加入到更深层结构中的 Prompt 能给模型预测带来更直接的影响。

具体做法基本同Prefix Tuning,可以看作是将文本生成的Prefix Tuning技术适配到NLU任务中,然后做了一些改进:

  • 移除重参数化的编码器。以前的方法利用重参数化功能来提高训练速度和鲁棒性(如:Prefix Tuning中的MLP、P-Tuning中的LSTM))。在 P-tuning v2 中,作者发现重参数化的改进很小,尤其是对于较小的模型,同时还会影响模型的表现。

  • 针对不同任务采用不同的提示长度。提示长度在提示优化方法的超参数搜索中起着核心作用。在实验中,我们发现不同的理解任务通常用不同的提示长度来实现其最佳性能,这与Prefix-Tuning中的发现一致,不同的文本生成任务可能有不同的最佳提示长度。

  • 引入多任务学习。先在多任务的Prompt上进行预训练,然后再适配下游任务。多任务学习对我们的方法来说是可选的,但可能是相当有帮助的。一方面,连续提示的随机惯性给优化带来了困难,这可以通过更多的训练数据或与任务相关的无监督预训练来缓解;另一方面,连续提示是跨任务和数据集的特定任务知识的完美载体。我们的实验表明,在一些困难的序列任务中,多任务学习可以作为P-tuning v2的有益补充。

  • 回归传统的分类标签范式,而不是映射器。标签词映射器(Label Word Verbalizer)一直是提示优化的核心组成部分,它将one-hot类标签变成有意义的词,以利用预训练语言模型头。尽管它在few-shot设置中具有潜在的必要性,但在全数据监督设置中,Verbalizer并不是必须的。它阻碍了提示调优在我们需要无实际意义的标签和句子嵌入的场景中的应用。因此,P-Tuning v2回归传统的CLS标签分类范式,采用随机初始化的分类头(Classification Head)应用于tokens之上,以增强通用性,可以适配到序列标注任务。

论文中展示了P-tuning v2在不同模型规模下的表现。对于简单的NLU任务,如SST-2(单句分类),Prompt Tuning和P-Tuning在较小的规模下没有显示出明显的劣势。但是当涉及到复杂的挑战时,如:自然语言推理(RTE)和多选题回答(BoolQ),它们的性能会非常差。相反,P-Tuning v2在较小规模的所有任务中都与微调的性能相匹配。并且,P-tuning v2在RTE中的表现明显优于微调,特别是在BERT中。

论文还通过消融实验研究了不同任务上Prompt Length的影响:

  • 针对简单任务:如情感分析,较短的Prompt(~20)即可取得不错的效果。
  • 针对复杂任务:如阅读理解,需要更长的Prompt(~100)。

总之,P-Tuning v2是一种在不同规模和任务中都可与微调相媲美的提示方法。P-Tuning v2对从330M到10B的模型显示出一致的改进,并在序列标注等困难的序列任务上以很大的幅度超过了Prompt Tuning和P-Tuning。P-Tuning v2可以成为微调的综合替代方案和未来工作的基线(Baseline)。

以bert分类任务为例,P-Tuning v2相关的核心代码实现在BertPrefixForTokenClassification

在实现的时候有一个get_prompt方法,通过这个函数提前生成各层的prompt的prefix向量

class BertPrefixForTokenClassification(BertPreTrainedModel):
    def get_prompt(self, batch_size):
        prefix_tokens = self.prefix_tokens.unsqueeze(0).expand(batch_size, -1).to(self.bert.device)
        past_key_values = self.prefix_encoder(prefix_tokens)
        # bsz, seqlen, _ = past_key_values.shape
        past_key_values = past_key_values.view(
            batch_size,
            self.pre_seq_len,
            self.n_layer * 2, 
            self.n_head,
            self.n_embd
        )
        past_key_values = self.dropout(past_key_values)
        past_key_values = past_key_values.permute([2, 0, 3, 1, 4]).split(2)
        return past_key_values

然后在forward中通过self.bert方法中的past_key_values方法把prefix向量传入,在前向计算时会把传入的prefix向量进行拼接。

class BertPrefixForTokenClassification(BertPreTrainedModel):    
        
        def forward(
        self,
        input_ids=None,
        attention_mask=None,
        token_type_ids=None,
        position_ids=None,
        head_mask=None,
        inputs_embeds=None,
        labels=None,
        output_attentions=None,
        output_hidden_states=None,
        return_dict=None,
    ):
        return_dict = return_dict if return_dict is not None else self.config.use_return_dict

        batch_size = input_ids.shape[0]
        past_key_values = self.get_prompt(batch_size=batch_size)
        prefix_attention_mask = torch.ones(batch_size, self.pre_seq_len).to(self.bert.device)
        attention_mask = torch.cat((prefix_attention_mask, attention_mask), dim=1)

        outputs = self.bert(
            input_ids,
            attention_mask=attention_mask,
            token_type_ids=token_type_ids,
            position_ids=position_ids,
            head_mask=head_mask,
            inputs_embeds=inputs_embeds,
            output_attentions=output_attentions,
            output_hidden_states=output_hidden_states,
            return_dict=return_dict,
            past_key_values=past_key_values,
        )

对于past_key_values可参考huggingface transformer中的实现BertSelfAttention。在forward中如果设置了past_key_value会通过torch.cat和layer的参数进行拼接。

class BertSelfAttention(nn.Module):
    ...
    def forward(
        self,
        hidden_states: torch.Tensor,
        attention_mask: Optional[torch.FloatTensor] = None,
        head_mask: Optional[torch.FloatTensor] = None,
        encoder_hidden_states: Optional[torch.FloatTensor] = None,
        encoder_attention_mask: Optional[torch.FloatTensor] = None,
        past_key_value: Optional[Tuple[Tuple[torch.FloatTensor]]] = None,
        output_attentions: Optional[bool] = False,
    ) -> Tuple[torch.Tensor]:
        ...
        if is_cross_attention and past_key_value is not None:
            # reuse k,v, cross_attentions
            key_layer = past_key_value[0]
            value_layer = past_key_value[1]
            attention_mask = encoder_attention_mask
        elif past_key_value is not None:
            key_layer = self.transpose_for_scores(self.key(hidden_states))
            value_layer = self.transpose_for_scores(self.value(hidden_states))
            key_layer = torch.cat([past_key_value[0], key_layer], dim=2)
            value_layer = torch.cat([past_key_value[1], value_layer], dim=2)
        ...
    ...

7. Adapter Tuning

论文:Parameter-Efficient Transfer Learning for NLP

github: https://github.com/google-research/adapter-bert

随着计算机硬件性能的提高,预训练模型参数量越来越多,在训练下游任务时进行全量微调变得昂贵且耗时。

基于此,作者提出了Adapter Tuning,Adapter 的出现缓解了上述问题。Adapter 在预训练模型每层中插入用于下游任务的参数(针对每个下游任务,仅增加3.6%的参数),在微调时将模型主体冻结,仅训练特定于任务的参数,从而减少了训练时的算力开销。

Adapter Tuning 设计了Adapter结构,并将其嵌入Transformer的结构里面,针对每一个Transformer层,增加了两个Adapter结构,分别是多头注意力的投影之后和第二个feed-forward层之后,在训练时,固定住原来预训练模型的参数不变,只对新增的 Adapter 结构和 Layer Norm 层进行微调,从而保证了训练的高效性。

每当出现新的下游任务,通过添加Adapter模块来产生一个易于扩展的下游模型,从而避免全量微调与灾难性遗忘的问题。

**Adapter结构具体细节:**
  • 每个 Adapter 模块主要由两个前馈(Feedforward)子层组成,第一个前馈子层(down-project)将Transformer块的输出作为输入,将原始输入维度d(高维特征)投影到m(低维特征),通过控制m的大小来限制Adapter模块的参数量,通常情况下,m«d。
  • 然后,中间通过一个非线形层。在输出阶段,通过第二个前馈子层(up-project)还原输入维度,将m(低维特征)重新映射回d(原来的高维特征),作为Adapter模块的输出。
  • 同时,通过一个skip connection来将Adapter的输入重新加到最终的输出中去,这样可以保证,即便 Adapter 一开始的参数初始化接近0,Adapter也由于skip connection的设置而接近于一个恒等映射,从而确保训练的有效性。

通过实验发现,只训练少量参数的Adapter方法的效果可以媲美全量微调,这也验证了Adapter是一种高效的参数训练方法,可以快速将语言模型的能力迁移到下游任务中去。同时,可以看到,Adapter 最佳的中间层特征维度m视数据集的大小而异,如:MINI数据集为256,最小的RTE数据集为8。如果始终将维度限制在64,将导致平均准确率略微下降。

总之,Adapter通过引入0.5%~5%的模型参数可以达到不落后全量微调模型1%的性能。

如下图所示,绿色是训练的部分,因为均值和方差的更新,所以 Layer Norm 层也会更新参数γ和β。一层transformer模型更新的参数量是 2 * (2d + 2md + m + d),其中 d 是原始模型的参数量,m 是 Adapter 的参数量。

def transformer_block_with_adapter(x):
	residual = x
	x = SelfAttention(x)
	x = FFN(x) # adapter
	x = LN(x + residual)
	residual = x
	x = FFN(x) # transformer FFN
	x = FFN(x) # adapter
	x = LN(x + residual)
	return x

8. AdapterFusion

论文:AdapterFusion:Non-Destructive Task Composition for Transfer Learning

Adapter Fusion,一种融合多任务信息的Adapter的变体,在 Adapter 的基础上进行优化,通过将学习过程分为两阶段来提升下游任务表现。

  • 知识提取阶段:在不同任务下引入各自的Adapter模块,用于学习特定任务的信息。
  • 知识组合阶段:将预训练模型参数与特定任务的Adapter参数固定,引入新参数(AdapterFusion)来学习组合多个Adapter中的知识,以提高模型在目标任务中的表现。

对于第一阶段,有两种训练方式,分别如下:

  • Single-Task Adapters(ST-A):对于N个任务,模型都分别独立进行优化,各个任务之间互不干扰,互不影响。
  • Multi-Task Adapters(MT-A):N个任务通过多任务学习的方式,进行联合优化。

对于第二阶段,为了避免通过引入特定任务参数而带来的灾难性遗忘问题,AdapterFusion提出了一个共享多任务信息的结构。针对特定任务m,AdapterFusion联合了第一阶段训练得到的N个Adapter信息。固定语言模型的参数跟N个Adapter的参数,新引入AdapterFusion的参数,目标函数也是学习针对特定任务m的AdapterFusion的参数。

AdapterFusion结构:

  • AdapterFusion具体结构就是一个Attention,它的参数包括query,key, value的矩阵参数,在transformer的每一层都存在。
  • query是transformer每个子模块的输出结果,key跟value则是N个任务的adapter的输出。
  • 通过AdapterFusion,模型可以为不同的任务对应的adapter分配不同的权重,聚合N个任务的信息,从而为特定任务输出更合适的结果。

通过对全量微调、Adapter Tuning、AdapterFusion这三种方法在各个数据集上进行对比实验可以看出,AdapterFusion在大多数情况下性能优于全模型微调和Adapter Tuning,特别在MRPC与RTE数据集中,性能显著优于另外两种方法。

同时,还可以看到第一阶段采用ST-A+第二阶段AdapterFusion是最有效的方法,在多个数据集上的平均效果达到了最佳。

而第一阶段采用MT-A+第二阶段AdapterFusion没有取得最佳的效果,在于第一阶段其实已经联合了多个任务的信息了,所以AdapterFusion的作用没有那么明显,同时MT-A这种多任务联合训练的方式需要投入较多的成本,并不算一种高效的参数更新方式。

另外,ST-A的方法在多个任务上都有提升,但是MT-A的方法则不然,这也表明了MT-A虽然可以学习到一个通用的表征,但是由于不同任务的差异性,很难保证在所有任务上都取得最优的效果。

总之,通过将适配器的训练分为知识提取和知识组合两部分,解决了灾难性遗忘、任务间干扰和训练不稳定的问题。但是,Adapter模块的添加也导致模型整体参数量的增加,降低了模型推理时的性能。

9. AdapterDrop

论文:AdapterDrop: On the Efficiency of Adapters in Transformers

近年来Adapter已被证明可以很好地用于机器翻译、跨语言迁移、社区问答和迁移学习的任务组合。尽管它们最近很受欢迎,但Adapter的计算效率尚未在参数效率之外得到探索。

作者通过对Adapter的计算效率进行分析,发现与全量微调相比,Adapter在训练时快60%,但是在推理时慢4%-6%。

基于此,作者提出了AdapterDrop方法缓解该问题。

AdapterDrop 在不影响任务性能的情况下,对Adapter动态高效的移除,尽可能的减少模型的参数量,提高模型在反向传播(训练)和正向传播(推理)时的效率。

实验表明,从较低的 Transformer 层中删除Adapter可以显着提高多任务设置中的推理速度。 例如,将前五个Transformer层中的Adapter丢弃,在对 8 个任务进行推理时,速度提高了 39%。并且即使有多个丢弃层,AdapterDrop 也能保持良好的结果。

除此之外,作者还研究了对 AdapterFusion中的Adapter进行剪枝后的效果。

通过实验表明可以移除 AdapterFusion 中的大多数Adapter而不影响任务性能。使用剩余的两个Adapter,实现了与具有八个Adapter的完整 AdapterFusion 模型相当的结果,并将推理速度提高了 68%。

因此,作者建议在实际部署这些模型之前执行 AdaperFusion 剪枝。 这是一种简单而有效的技术,即使在完全保持性能的情况下也能实现效率提升。

总之,AdapterDrop 通过从较低的 Transformer 层删除可变数量的Adaper来提升推理速度。 当对多个任务执行推理时,动态地减少了运行时的计算开销,并在很大程度上保持了任务性能。

10. Lora

论文: LoRA: Low-Rank Adaptation of Large Language Models

LoRA: Low-Rank Adaptation: https://github.com/microsoft/LoRA.git

LoRA方法的核心思想就是通过低秩分解来模拟参数的改变量,从而以极小的参数量来实现大模型的间接训练。

  1. 在原始 PLM (Pre-trained Language Model) 旁边增加一个旁路, 做一个降维再升维的操作, 来模拟所谓的 intrinsic rank 。
  2. 训练的时候固定 PLM 的参数, 只训练降维矩阵 $A$ 与升维矩阵 $B$ 。而模型的输入输出维度不变, 输出时将 $BA$ 与 PLM 的参数叠加。
  3. 用随机高斯分布初始化 $A$, 用 0 矩阵初始化 $B$, 保证训练的开始此旁路矩阵依然是 0 矩阵。
  4. LoRA训练参数量显著减少,GPU内存需求减少,无额外推理延迟

假设要在下游任务微调一个预训练语言模型(如 GPT-3), 则需要更新预训练模型参数, 公式表示如下: \(W_0+\Delta W\) $W_0$ 是预训练模型初始化的参数, $\Delta W$ 就是需要更新的参数。如果是全参数微调, 则它的参数量 $=W_0$ (如果是 GPT-3, 则 $\Delta W \approx 175 \mathrm{~B}$ ) 。从这可以看出要全参数微调大语言模型, 代价是非常高的。

而对于 LORA 来说, 只需要微调 $\Delta W$ 。 具体来看, 假设预训练的矩阵为 $W_0 \in \mathbb{R}^{d \times k}$, 它的更新可表示为: \(W_0+\Delta W=W_0+B A, B \in \mathbb{R}^{d \times r}, A \in \mathbb{R}^{r \times k}\)

其中秩 $r \ll \min (d, k)$ 。实验结果显示,对于一般的任务, $r=1,2,4,8$ 就足够了。而一些领域差距比较大的任务可能需要更大的 $r$ 。 在 LoRA 的训练过程中, $W_0$ 是固定不变的, 只有 $A$ 和 $B$ 是训练参数。

在前向过程中, $W_0$ 与 $\Delta W$ 都会乘以相同的输入 $x$, 最后相加: \(h=W_0 x+\Delta W x=W_0 x+B A x\)

LORA 的这种思想有点类似于残差连接, 同时使用这个旁路的更新来模拟 Full Fine-Tuning的过程。并且, Full Fine-Tuning可以被看做是 LoRA 的特例(当 $r$ 等于 $k$ 时)。

在推理过程中, LoRA 也几乎未引入额外的 Inference Latency, 只需要计算 $W=W_0+\Delta W$ 即可。对于推理来说,不会增加额外的计算资源。

LoRA 与 Transformer 的结合也很简单, 仅在 QKV Attention 的计算中增加一个旁路。

  • Transformer的权重矩阵包括Attention模块里用于计算query, key, value的Wq,Wk,Wv以及多头attention的Wo,以及MLP层的权重矩阵,LoRA只应用于Attention模块中的4种权重矩阵,而且通过消融实验发现同时调整 Wq 和 Wv 会产生最佳结果。

  • 实验还发现,保证权重矩阵的种类的数量比起增加隐藏层维度r更为重要,增加r并不一定能覆盖更加有意义的子空间。

  • 关于秩的选择,通常情况下,rank为4,8,16即可。

  • 实验也发现,在众多数据集上LoRA在只训练极少量参数的前提下,最终在性能上能和全量微调匹配,甚至在某些任务上优于全量微调。

11. AdaLora

论文:AdaLoRA: Adaptive Budget Allocation for Parameter-Efficient Fine-Tuning

github: https://github.com/QingruZhang/AdaLoRA

在NLP领域,对于下游任务进行大型预训练语言模型的微调已经成为一种重要的做法。一般而言,我们会采用对原有的预训练模型进行全量微调的方法来适配下游任务,但这种方法存在两个问题。

  1. 训练阶段。对于预训练模型进行微调的时候,为了更新权重参数,需要大量的显存来存储参数的梯度和优化器信息,在当今预训练模型的参数变得越来越大的情况下,针对下游任务微调门槛变得越来越高。
  2. 推理阶段。由于我们训练的时候是对于模型参数进行全量的更新,所以多个下游任务需要为每个任务维护一个大型模型的独立副本,这样就导致我们在实际应用的时候浪费了不必要的存储。

为了解决这些问题,研究者提出了两个主要研究方向,以减少微调参数的数量,同时保持甚至提高预训练语言模型的性能。

  1. 方向一:添加小型网络模块:将小型网络模块添加到PLMs中,保持基础模型保持不变的情况下仅针对每个任务微调这些模块,可以用于所有任务。这样,只需引入和更新少量任务特定的参数,就可以适配下游的任务,大大提高了预训练模型的实用性。如:Adapter tuning、Prefix tuning、Prompt Tuning等,这类方法虽然大大减少了内存消耗。但是这些方法存在一些问题,比如:Adapter tuning引入了推理延时;Prefix tuning或Prompt tuning直接优化Prefix和Prompt是非单调的,比较难收敛,并且消耗了输入的token。
  2. 方向二:下游任务增量更新:对预训练权重的增量更新进行建模,而无需修改模型架构,即W=W0+△W。比如:Diff pruning、LoRA等, 此类方法可以达到与完全微调几乎相当的性能,但是也存在一些问题,比如:Diff pruning需要底层实现来加速非结构化稀疏矩阵的计算,不能直接使用现有的框架,训练过程中需要存储完整的∆W矩阵,相比于全量微调并没有降低计算成本。 LoRA则需要预先指定每个增量矩阵的本征秩 r 相同,忽略了在微调预训练模型时,权重矩阵的重要性在不同模块和层之间存在显著差异,并且只训练了Attention,没有训练FFN,事实上FFN更重要。

基于以上问题进行总结:

  • 第一,我们不能预先指定矩阵的秩,需要动态更新增量矩阵的R,因为权重矩阵的重要性在不同模块和层之间存在显著差异。
  • 第二,需要找到更加重要的矩阵,分配更多的参数,裁剪不重要的矩阵。找到重要的矩阵,可以提升模型效果;而裁剪不重要的矩阵,可以降低参数计算量,降低模型效果差的风险。

为了弥补这一差距,作者提出了AdaLoRA,它根据权重矩阵的重要性得分,在权重矩阵之间自适应地分配参数预算。

技术原理:AdaLoRA是对LoRA的一种改进,它根据重要性评分动态分配参数预算给权重矩阵。具体做法如下:

  1. 调整增量矩分配。AdaLoRA将关键的增量矩阵分配高秩以捕捉更精细和任务特定的信息,而将较不重要的矩阵的秩降低,以防止过拟合并节省计算预算。
  2. 以奇异值分解的形式对增量更新进行参数化,并根据重要性指标裁剪掉不重要的奇异值,同时保留奇异向量。由于对一个大矩阵进行精确SVD分解的计算消耗非常大,这种方法通过减少它们的参数预算来加速计算,同时,保留未来恢复的可能性并稳定训练。

    $W=W^{(0)}+\Delta=W^{(0)}+P \Lambda Q$,其中,$P \in \mathbb{R}^{d_1 \times r}, Q \in \mathbb{R}^{r \times d_2}$ ,表示 $\Delta$ 的左/右奇异向量。对角矩阵 $\Lambda \in \mathbb{R}^{r \times r}$

  3. 在训练损失中添加了额外的惩罚项,以规范奇异矩阵P和Q的正交性,从而避免SVD的大量计算并稳定训练。

通过实验证明,AdaLoRA 实现了在所有预算、所有数据集上与现有方法相比,性能更好或相当的水平。 例如,当参数预算为 0.3M 时,AdaLoRA 在RTE数据集上,比表现最佳的基线(Baseline)高 1.8%。

12. QLora

论文:QLORA: Efficient Finetuning of Quantized LLMs

github: https://github.com/artidoro/qlora and https://github.com/TimDettmers/bitsandbytes

微调大型语言模型 (LLM) 是提高其性能以及添加所需或删除不需要的行为的一种非常有效的方法。然而,微调非常大的模型非常昂贵;以 LLaMA 65B 参数模型为例,常规的 16 bit微调需要超过 780 GB 的 GPU 内存。

  • 虽然最近的量化方法可以减少 LLM 的内存占用,但此类技术仅适用于推理场景。

  • 基于此,作者提出了QLoRA,并首次证明了可以在不降低任何性能的情况下微调量化为 4 bit的模型。

QLora技术原理

QLoRA使用一种新颖的高精度技术将预训练模型量化为 4 bit,然后添加一小组可学习的低秩适配器权重,这些权重通过量化权重的反向传播梯度进行微调。QLORA 有一种低精度存储数据类型(4 bit),还有一种计算数据类型(BFloat16)。实际上,这意味着无论何时使用 QLoRA 权重张量,我们都会将张量反量化为 BFloat16,然后执行 16 位矩阵乘法。QLoRA提出了两种技术实现高保真 4 bit微调——4 bit NormalFloat(NF4) 量化和双量化。此外,还引入了分页优化器,以防止梯度检查点期间的内存峰值,从而导致内存不足的错误,这些错误在过去使得大型模型难以在单台机器上进行微调。具体说明如下:

  1. 4bit NormalFloat(NF4)4-bit NormalFloat Quantization:对于正态分布权重而言,一种信息理论上最优的新数据类型,该数据类型对正态分布数据产生比 4 bit整数和 4bit 浮点数更好的实证结果。
  2. Double Quantization 双量化:对第一次量化后的那些常量再进行一次量化,减少存储空间。
  3. Paged Optimizers 分页优化器:使用NVIDIA统一内存特性,该特性可以在在GPU偶尔OOM的情况下,进行CPU和GPU之间自动分页到分页的传输,以实现无错误的 GPU 处理。该功能的工作方式类似于 CPU 内存和磁盘之间的常规内存分页。使用此功能为优化器状态(Optimizer)分配分页内存,然后在 GPU 内存不足时将其自动卸载到 CPU 内存,并在优化器更新步骤需要时将其加载回 GPU 内存。

实验证明,无论是使用16bit、8bit还是4bit的适配器方法,都能够复制16bit全参数微调的基准性能。这说明,尽管量化过程中会存在性能损失,但通过适配器微调,完全可以恢复这些性能。

实验还比较了不同的4bit数据类型对效果(zero-shot均值)的影响,其中,NFloat 显著优于Float,而NFloat + DQ略微优于NFloat,虽然DQ对精度提升不大,但是对于内存控制效果更好。

此外,论文中还对不同大小模型、不同数据类型、在 MMLU数据集上的微调效果进行了对比。使用QLoRA(NFloat4 + DQ)可以和Lora(BFloat16)持平,同时, 使用QLORA( FP4)的模型效果落后于前两者一个百分点。

作者在实验中也发现了一些有趣的点,比如:指令调优虽然效果比较好,但只适用于指令相关的任务,在聊天机器人上效果并不佳,而聊天机器人更适合用Open Assistant数据集去进行微调。通过指令类数据集的调优更像是提升大模型的推理能力,并不是为聊天而生的。

总之,QLoRA的出现给大家带来一些新的思考,不管是微调还是部署大模型,之后都会变得更加容易。每个人都可以快速利用自己的私有数据进行微调;同时,又能轻松的部署大模型进行推理。

13. MOELora

论文: MOELoRA- An MOE-based Parameter Efficient Fine-Tuning Method for Multi-task Medical Applications

github:https://github.com/liuqidong07/MOELoRA-peft

MOELoRA 的核心思想是将 MOE 和 LoRA 结合起来,以实现多任务学习和参数高效微调。MOELoRA 由两个主要组件组成:MOE 和 LoRA。MOE 用于多任务学习,LoRA 用于参数高效微调。MOELoRA 通过 MOE 的多任务学习能力,有效地利用了有限的数据和计算资源,同时通过 LoRA 的参数高效微调能力,有效地提高了多任务医学应用的性能。

医疗名称实体识别示例用于说明如何使用llm来完成医疗任务:

$I_M \rightarrow T P_{N E R}^Q\left(I_M\right) \xrightarrow{L L M} T P_{N E R}^A\left(O_{h e a d}, O_{\text {tail }}\right) \rightarrow O_{\text {head }}, O_{\text {tait }}$

MOELoRA模型结构

  • LoRA组件: LoRA依赖于低秩分解来实现参数高效的微调。具体来说,LoRA通过在预训练的参数矩阵中添加低秩矩阵的更新来调整模型的行为,而不是直接修改原始参数矩阵。这种方法大大减少了需要训练的参数数量。
  • MOE组件: MOE通过引入多个“专家”网络,每个专家负责捕捉数据或任务的不同方面。MOELoRA通过将这些专家集成到LoRA结构中,使得每个任务可以从所有专家中学习,同时通过一个门控函数来决定各个专家对于当前任务的贡献度。

需要更新的参数

  • LoRA参数:LoRA组件中的两个低秩矩阵A和B需要更新。这些矩阵相对较小,因此更新它们需要的参数数量也较少。
  • MOE参数:每个专家由一对低秩矩阵组成,这些矩阵在MOELoRA微调过程中被更新。每个专家的参数量由低秩矩阵的大小决定。
  • 门控函数参数:门控函数决定了不同专家对于特定任务的贡献权重。这需要对门控函数本身的参数进行更新,以便它可以根据任务特征正确地分配权重。

输入输出维度

  • 输入:模型输入是经过预处理和适配的医疗文本,这些文本被转换成了适合LLMs处理的形式,包括通过指令模板引导的修改。
  • 输出:模型输出是针对特定任务格式化后的文本。例如,在命名实体识别任务中,输出将是识别出的实体及其类型。

通过结合LoRA的参数高效微调能力和MOE的多任务学习能力,MOELoRA能够在保持预训练语言模型参数不变的同时,为每个任务学习独特的参数更新,实现跨多个医疗任务的高效微调。

多任务fine tune的目标函数: $\max {\Phi} \sum{j \in[M]} \sum_{(x, y) \in \mathcal{D}j} \sum{t=1}^{|y|} \log \left(P_{\Phi}\left(y_t \mid x, y_{\leq t}\right)\right)$​

MOELora模型结构:

线性层和Lora层配对的过程: \(\begin{aligned} \mathbf{h} & =\mathbf{W}_0 \mathbf{x}+\frac{\alpha}{r} \cdot \Delta \mathbf{W} \mathbf{x} \\ & =\mathbf{W}_0 \mathbf{x}+\frac{\alpha}{r} \cdot \mathbf{B A} \mathbf{x}\end{aligned}\)

where $\mathbf{x}$ represents the input vector of dimension $d_{i n}$, and $\mathbf{h}$ is the output vector with dimension $d_{\text {out }}$. The rank of the trainable low-rank matrices is denoted by $r$, which determines the number of trainable parameters. The constant hyperparameter $\alpha$ facilitates the tuning of rank $r$.

During the LoRA fine-tuning process, all parameters in the LLMs, such as $\mathbf{W} q, \mathbf{W} k, \mathbf{W}{\mathbf{v}}$, and $\mathbf{W}$, remain frozen. Only the low-rank matrices, $\mathbf{A}$ and $\mathbf{B}$, undergo finetuning. Given that $r \ll d{i n}$ and $r \ll d_{o u t}$, the combined number of parameters in $\mathbf{A}$ and $\mathbf{B}$ is significantly smaller than in $\mathbf{W}_{\mathbf{0}}$.

Such characteristic results in achieving parameter efficiency for the fine-tuning process.

线性层和MOELora层配对的过程: \(\begin{aligned} \mathbf{h}_j & =\mathbf{W}_0 \mathbf{x}_j+\frac{\alpha}{r} \cdot \Delta \mathbf{W}_j \mathbf{x}_j \\ & =\mathbf{W}_0 \mathbf{x}_j+\frac{\alpha}{r} \cdot \sum_{i=1}^N \omega_{j i} \cdot E_i\left(\mathbf{x}_j\right) \\ & =\mathbf{W}_0 \mathbf{x}_j+\frac{\alpha}{r} \cdot \sum_{i=1}^N \omega_{j i} \cdot \mathbf{B}_i \mathbf{A}_i \mathbf{x}_j \end{aligned}\)

$\mathbf{h}j$ and $\mathbf{x}_j$ represent the input and output of intermediate LLM layers for samples from $\mathcal{T}_j$. The matrices $\mathbf{B}_i \in \mathbb{R}^{d{i n} \times \frac{r}{N}}$ and $\mathbf{A}i \in \mathbb{R}^{\frac{r}{N} \times d{\text {out }}}$ form the expert $i$. The hyper-parameter $N$ denotes the number of experts in MOELoRA, and for each expert, the rank of matrices $A$ and $B$ is $\frac{r}{N}$. To ensure that distinct parameters are learned for different tasks, the contribution of each expert should be task-specific. In Equation (4), the term $\omega_{j i}$ modulates these contribution weights for task $\mathcal{T}_j$. This weight is determined by our proposed gate function, which we will detail in following section.

Here, we will discuss the number of trainable parameters for LoRA and MOELoRA. In terms of LoRA, the two low-rank matrices $\mathbf{B} \in \mathbb{R}^{d_{i n} \times r}$ and $\mathbf{A} \in \mathbb{R}^{r \times d_{\text {out }}}$ contain all trainable parameters. Thus, the number of trainbale parameters of LoRA is $d_{i n} \times r+r \times d_{\text {out }}=r \times\left(d_{i n}+d o u t\right)$. As for MOELoRA, there are $N$ trainable experts and each expert own $\frac{r}{N} \times\left(d_{i n}+d o u t\right)$, so total number is calculated as $N \times \frac{r}{N} \times\left(d_{i n}+d o u t\right)=$ $r \times\left(d_{i n}+d o u t\right)$.

As a conclusion, the MOELoRA has the same number of trainable parameters as LoRA, which indicates high efficiency.

任务的门控函数 Gate Function: \(\boldsymbol{\omega}_j=\operatorname{Softmax}\left(\mathbf{W}_T \mathbf{e}_j\right)\) MOELora训练和推理的过程:

14. MAM Adapter

论文:Towards a Unified View of Parameter-Efficient Transfer Learning

github: https://github.com/jxhe/unify-parameter-efficient-tuning

近年来提出了多种参数高效的迁移学习方法,这些方法仅微调少量(额外)参数即可获得强大的性能。虽然有效,但人们对为什么有效的关键要素以及各种高效微调方法之间的联系知之甚少。

下图展示了不同的微调方法,在Xsum数据集上做英文文本摘要任务的效果(ROUGE-2是该任务的评价指标(越大越好))以及其他高效微调方法参数量相对于全参数微调参数量的百分比。图中的左上角的位置是理想化的方法。从图中发现,Adapter,Prefix Tuning和LoRA都是性能比较好的方法。

为什么看起来Adapter、Prefix Tuning、LoRA(在结构上和公式上)都不太一样,尤其是Prefix Tuning,但是这三种方法有近似的效果?

基于此,作者分解了当下最先进的参数高效迁移学习方法(Adapter、Prefix Tuning和LoRA)的设计,并提出了一种新方法MAM Adapter,一个在它们之间建立联系的统一框架。具体来说,将它们重新构建为对预训练模型中特定隐藏状态的修改,并定义一组设计维度,不同的方法沿着这些维度变化。

首先,作者通过对Prefix Tuning变换,发现Prefix Tuning和Adapters的公式高度相似。

然后,分析不同微调方法的内部结构和结构插入形式的相似之处。下图展示了高效微调方法Adapter、Prefix Tuning、LoRA以及新变体(通过更换一些元素,设计了前人的工作里没有的变体) Parallel Adapter、 Scaled PA的结构。

下表展示了高效微调方法Adapter、Prefix Tuning、LoRA以及新变体在新增可训练参数结构形式(functional form)、结构插入形式(Insertion form)、新增结构在PLM修改的具体位置(modified representation)、新增结构与PLM的组合函数(composition function)。其中,新增可训练参数结构形式为需要学习的部分(注:Prefix Tuning为经过转换后的格式);插入形式有串联或并联;模型修改的具体位置有Attention、FFN层。

MAM Adapter,一个在Adapter、Prefix Tuning和LoRA之间建立联系的统一方法。

作者对Adapter的放置和软提示(soft prompt)进行了详细的调查。得出如下结论:

  • 并行放置的Adapter优于顺序放置的Adapter,并且与 FFN 并行放置的Adapter优于多头注意力(MHA)并行放置的Adapter。
  • 软提示可以通过仅更改 0.1% 的参数来有效地修改注意力。

然后,提出了“mix-and-match”(MAM)。 因此,最终模型 MAM Adapter 是用 FFN 层的并行Adapter和软提示的组合。

最终的实验结果,可以看到 MAM Adapter 在仅用了6.7%参数量(相比全量微调)的情况下,在Xsum和MT这两个任务上达到了和全量微调相近的效果,并且该方法大大优于 BitFit 和 Prompt Tuning,并始终优于 LoRA、Adapter 和 Prefix Tuning。

15. UniPELT

论文:UniPELT: A Unified Framework for Parameter-Efficient Language Model Tuning

github:https://github.com/morningmoni/UniPELT

近年来,涌现出了许多针对语言模型的参数高效微调(PELT)方法,在模型训练参数极大的减少的情况下,模型效果与全量微调相当。但是不同的PELT方法在同一个任务上表现差异可能都非常大,这让针对特定任务选择合适的方法非常繁琐。

基于此,作者提出了UniPELT方法,将不同的PELT方法作为子模块,并通过门控机制学习激活最适合当前数据或任务的方法。

UniPELT 是 LoRA、Prefix Tuning和Adapter的门控组合。

  • LoRA 重新参数化用于 WQ 和 WV 注意力矩阵
  • Prefix Tuning应用于每一Transformer层的key和value,
  • 并在Transformer块的feed-forward子层之后添加Adapter。
  • 对于每个模块,门控被实现为线性层,通过GP参数控制Prefix-tuning方法的开关,GL控制LoRA方法的开关,GA控制Adapter方法的开关。
  • 可训练参数包括 LoRA 矩阵 WA(Down)和WB(Up),提示调优参数Pk和Pv、Adapter参数和门函数权重。
  • 即图中蓝颜色的参数为可学习的参数。

UniPELT 仅用 100 个示例就在低数据场景中展示了相对于单个 LoRA、Adapter 和 Prefix Tuning 方法的显著改进。在更高数据的场景中,UniPELT 的性能与这些方法相当或更好。

实验还对不同 PELT 方法训练时间和推理时间进行了分析。

  • 从训练速度来看,UniPELT比之前微调的方法多一些,但是还在能接受的范围,
  • 从推理时间来看,BitFit方法增加的最少,UniPELT方法时间增加了27%。
  • 从训练参数量来看,LoRA,BitFit,Prefix-tuning都比较小,UniPELT参数量相对会多一些。

总之,本方法始终优于常规的全量微调以及它在不同设置下包含的子模块,通常超过在每个任务中单独使用每个子模块的最佳性能的上限;并且,通过研究结果表明,多种 PELT 方法的混合涉及到PLM 的不同部分可能对模型有效性和鲁棒性都有好处。

16. RLHF

RLHF–人类反馈强化学习,思想就是使用强化学习的方式直接优化带有人类反馈的语言模型。RLHF 使得在一般文本数据语料库上训练的语言模型能和复杂的人类价值观对齐。

RLHF 是一项涉及多个模型和不同训练阶段的复杂概念,一般会分为三步,这也是一个生成自己大模型所必需的。

  • 第一步是 supervised-fintuning,即用数据集进行模型微调,预训练一个语言模型 (LM)
  • 第二步是训练一个奖励模型,它通过对于同一个 prompt 的不同输出进行人工排序,聚合问答数据并训练一个奖励模型 (Reward Model,RM)
  • 第三步则是用强化学习算法(RL) 方式微调 LM。

Step 1. 预训练语言模型

首先,我们使用经典的预训练目标训练一个语言模型。对这一步的模型,OpenAI 在其第一个流行的 RLHF 模型 InstructGPT 中使用了较小版本的 GPT-3;Anthropic 使用了 1000 万 ~ 520 亿参数的 Transformer 模型进行训练;DeepMind 使用了自家的 2800 亿参数模型 Gopher。

这里可以用额外的文本或者条件对这个 LM 进行微调,例如 OpenAI 对 “更可取” (preferable) 的人工生成文本进行了微调,而 Anthropic 按 “有用、诚实和无害” 的标准在上下文线索上蒸馏了原始的 LM。这里或许使用了昂贵的增强数据,但并不是 RLHF 必须的一步。由于 RLHF 还是一个尚待探索的领域,对于“哪种模型” 适合作为 RLHF 的起点并没有明确的答案。

接下来,我们会基于 LM 来生成训练 奖励模型 (RM,也叫偏好模型) 的数据,并在这一步引入人类的偏好信息。

Step 2. 训练奖励模型

RM 的训练是 RLHF 区别于旧范式的开端。这一模型接收一系列文本并返回一个标量奖励,数值上对应人的偏好。我们可以用端到端的方式用 LM 建模,或者用模块化的系统建模 (比如对输出进行排名,再将排名转换为奖励) 。这一奖励数值将对后续无缝接入现有的 RL 算法至关重要。

  • 关于模型选择方面,RM 可以是另一个经过微调的 LM,也可以是根据偏好数据从头开始训练的 LM。例如 Anthropic 提出了一种特殊的预训练方式,即用偏好模型预训练 (Preference Model Pretraining,PMP) 来替换一般预训练后的微调过程。因为前者被认为对样本数据的利用率更高。但对于哪种 RM 更好尚无定论。 ​

  • 关于训练文本方面,RM 的提示 - 生成对文本是从预定义数据集中采样生成的,并用初始的 LM 给这些提示生成文本。Anthropic 的数据主要是通过 Amazon Mechanical Turk 上的聊天工具生成的,并在 Hub 上 可用,而 OpenAI 使用了用户提交给 GPT API 的 prompt。

  • 关于训练奖励数值方面,这里需要人工对 LM 生成的回答进行排名。起初我们可能会认为应该直接对文本标注分数来训练 RM,但是由于标注者的价值观不同导致这些分数未经过校准并且充满噪音。通过排名可以比较多个模型的输出并构建更好的规范数据集。 ​

  • 对具体的排名方式,一种成功的方式是对不同 LM 在相同提示下的输出进行比较,然后使用 Elo 系统建立一个完整的排名。这些不同的排名结果将被归一化为用于训练的标量奖励值。

这个过程中一个有趣的产物是目前成功的 RLHF 系统使用了和生成模型具有 不同 大小的 LM (例如 OpenAI 使用了 175B 的 LM 和 6B 的 RM,Anthropic 使用的 LM 和 RM 从 10B 到 52B 大小不等,DeepMind 使用了 70B 的 Chinchilla 模型分别作为 LM 和 RM) 。一种直觉是,偏好模型和生成模型需要具有类似的能力来理解提供给它们的文本。

接下来是最后一步:利用 RM 输出的奖励,用强化学习方式微调优化 LM。

Step 3. 用强化学习微调

长期以来出于工程和算法原因,人们认为用强化学习训练 LM 是不可能的。而目前多个组织找到的可行方案是使用策略梯度强化学习 (Policy Gradient RL) 算法、近端策略优化 (Proximal Policy Optimization,PPO) 微调初始 LM 的部分或全部参数。因为微调整个 10B~100B+ 参数的成本过高 (相关工作参考低秩适应 LoRA 和 DeepMind 的 Sparrow LM) 。PPO 算法已经存在了相对较长的时间,有大量关于其原理的指南,因而成为 RLHF 中的有利选择。

事实证明,RLHF 的许多核心 RL 进步一直在弄清楚如何将熟悉的 RL 算法应用到更新如此大的模型。

让我们首先将微调任务表述为 RL 问题。首先,该策略 (policy) 是一个接受提示并返回一系列文本 (或文本的概率分布) 的 LM。这个策略的 行动空间 (action space) 是 LM 的词表对应的所有词元 (一般在 50k 数量级) ,观察空间 (observation space) 是可能的输入词元序列,也比较大 (词汇量 ^ 输入标记的数量) 。奖励函数 是偏好模型和策略转变约束 (Policy shift constraint) 的结合。

最后根据 PPO 算法,我们按当前批次数据的奖励指标进行优化 (来自 PPO 算法 on-policy 的特性) 。PPO 算法是一种信赖域优化 (Trust Region Optimization,TRO) 算法,它使用梯度约束确保更新步骤不会破坏学习过程的稳定性。DeepMind 对 Gopher 使用了类似的奖励设置,但是使用 A2C (synchronous advantage actor-critic) 算法来优化梯度。

作为一个可选项,RLHF 可以通过迭代 RM 和策略共同优化。随着策略模型更新,用户可以继续将输出和早期的输出进行合并排名。Anthropic 在他们的论文中讨论了 迭代在线 RLHF,其中策略的迭代包含在跨模型的 Elo 排名系统中。这样引入策略和 RM 演变的复杂动态,代表了一个复杂和开放的研究问题。

文档信息

Search

    Table of Contents