从 RAG 的视角看待 LLM

174 阅读45分钟

【本文正在参加金石计划附加挑战赛——第一期命题】

Preview image

通过检索增强生成(RAG)应用的视角学习LLM

今天, 我们将重点讨论构建 LLM 时, RAG应用的文本生成部分. 因为, 在 RAG 的文本生成方面, LLM是必不可少的.

None

什么是LLM?

LLM是在大量数据上预先训练好的超大型深度学习 模型. 其底层Transformer是一组神经网络, 由具有自我关注能力的编码器和解码器组成. 编码器和解码器从文本序列中提取含义, 并理解其中单词和短语之间的关系.

流行的 LLM 家族.

None

一些最具代表性的 LLM 框架的时间轴(迄今为止).

Transformer神经网络架构允许使用大型模型, 通常具有数千亿个参数. 这样的大型模型可以摄取海量数据, 这些数据通常来自互联网, 也可以来自Common Crawl和维基百科等来源, 前者包括 500 多亿个网页, 后者则有大约 5700 万个网页.

NLP应用的关键工具之一是语言建模.

None

本图显示了语言建模的不同组成部分.

语言建模(LM)

语言和交流过程能否简化为计算?

语言模型通过从一个或多个文本语料库学习来生成概率. 文本语料库是一种语言资源, 由一种或多种语言的大量结构化文本组成. 文本语料库可以包含一种或多种语言的文本, 并且通常带有注释.

None

语言模型可以根据其在训练过程中学到的统计模式, 预测该短语后面最有可能出现的词. 在图中, 语言模型可以估计出 blue 这个词跟在 The color of the sky is. 这个词后面的概率为 91%.

建立语言模型的早方法之一是基于n-gram. n-gram 是给定文本样本中 n 个项目的连续序列. 在这里, 模型假定序列中下一个词的概率仅取决于固定大小的前一个词窗口:

None

N-gram

然而, N-gram 语言模型在很大程度上被神经语言模型取代. 它基于神经网络, 一种受生物神经网络启发的计算系统. 这些模型利用词语的连续表征或嵌入来进行预测:

None

神经网络

神经网络以权重的非线性组合来表示单词的分布. 因此, 它可以避免语言建模中的维度诅咒. 目前已有多种用于语言建模的神经网络架构.

基础模型和 LLM

这与早期 NLP 应用中的方法大相径庭, 早期的方法是训练专门的语言模型来执行特定任务. 相反, 研究人员在 LLM 中观察到了许多新出现的能力, 这些能力是它们从未训练过的.

例如, 研究表明 LLM 可以进行多步运算, 拼写单词字母以及识别口语中的攻击性内容. 最近, 在 OpenAPI 的 GPT 系列 LLM 基础上开发的聊天机器人 ChatGPT 就通过了美国医学执照考试等专业考试!

基础模型 通常是指任何在广泛数据基础上训练的模型, 可适用于广泛的下游任务. 这些模型通常使用深度神经网络创建, 并在许多未标记的数据上使用自监督学习进行训练.

None

在训练过程中, 文本序列从语料库中提取并截断. 语言模型计算出缺失单词的概率, 然后通过基于梯度下降的优化机制, 对这些概率进行小幅调整并反馈给模型, 使其与基本事实相匹配. 这一过程在整个文本语料库中重复进行.

不过, LLM 通常是在文本等语言相关数据上进行训练的. 但基础模型通常是在多模态数据上训练的, 即文本, 图像, 音频等的混合数据. 更重要的是, 基础模型旨在为更具体的任务奠定基础:

None

基础模型典型的做法是针对各种下游认知任务进行进一步训练, 然后进行微调. 微调是指将预先训练好的语言模型, 利用特定数据针对不同但相关的任务进行训练的过程. 这一过程也被称为迁移学习.

LLM 的结构

早期的 LLM 大多采用带有 LSTM 和 GRU 的 RNN 模型. 然而, 它们面临着挑战, 主要是在大规模执行 NLP 任务时. 但是, 这正是人们期望 LLM 能够完成的任务. 因此, Transformer 应运而生!

LLM 的早期架构

刚开始时, LLM 主要采用自我监督学习算法. 自监督学习指的是处理无标签数据, 以获得有用的表征, 从而帮助完成下游学习任务.

自监督学习算法通常使用基于人工神经网络(ANN)的模型. 我们可以使用多种架构创建 ANN, 但最广泛用于 LLM 的架构是 Recurrent Neural Network(RNN).

None

RNN

现在, RNN 可以使用其内部状态来处理可变长度的输入序列. RNN具有长期记忆和短期记忆. RNN 有多种变体, 如长短期记忆(LSTM)门控循环单元(GRU).

使用 LSTM 和 GRU 的问题

使用 LSTM 单元的 RNN 训练速度非常慢. 此外, 对于此类架构, 我们需要按顺序或串行输入数据. 这样, 我们就无法并行使用可用的处理器内核.

另外, 使用 GRU 的 RNN 模型训练速度更快, 但在较大的数据集上性能较差. 尽管如此, 长期以来, LSTM 和 GRU 仍然是构建复杂 NLP 系统的首选. 不过, 这类模型也存在梯度消失问题:

None

梯度消失

注意力机制

在 RNN 的架构中加入注意力机制 可以部分解决 RNN 的一些问题. 在 LSTM 等递归架构中, 可传播的信息量有限, 保留信息的窗口也较短.

然而, 有了注意力机制, 这个信息窗口就可以大大增加. 注意力是一种增强输入数据的某些部分, 同时减弱其他部分的技术. 这样做的动机是, 网络应将更多的注意力放在数据的重要部分:

None

注意和自我注意之间有微妙的区别, 但它们的动机是相同的. 注意力机制指的是关注另一序列不同部分的能力, 而自我关注指的是关注当前序列不同部分的能力.

自我关注允许模型从任何输入序列元素中获取信息. 在 NLP 应用中, 这提供了关于远处标记的相关信息. 因此, 模型可以捕捉整个序列中的依赖关系, 而无需固定或滑动窗口.

Transformer的到来

具有注意力机制的 RNN 模型的性能有了显著提高. 然而, 递归模型本质上难以扩展. 但是, 自我注意机制很快被证明是相当强大的, 以至于它甚至不需要递归顺序处理.

2017年, 谷歌大脑团队引入Transformer 或许是LLM历史上最重要的拐点之一. Transformer个采用自我注意机制, 一次性处理整个输入的深度学习模型:

None

与早期基于 RNN 的模型相比, Transformer没有递归结构, 这是一个重大变化. 只要有足够的训练数据, Transformer结构中的注意力机制本身就能与带有注意力的 RNN 模型相媲美.

使用Transformer模型的另一个显著优势是, 它们的并行化程度更高, 所需的训练时间也大大减少. 这正是我们利用现有资源在基于文本的大型语料库中构建 LLM 所需的最佳点.

编码器-解码器架构

许多基于 ANN 的自然语言处理模型都是利用编码器-解码器架构 建立的. 例如, seq2seq 是谷歌最初开发的一个算法系列. 它通过使用带有 LSTM 或 GRU 的 RNN 将一个序列转换成另一个序列.

最初的Transformer模型也使用编码器-解码器架构. 编码器由编码层组成, 逐层迭代地处理输入. 解码器由解码层组成, 对编码器的输出做同样的处理:

None

Transformer - 高级架构

每个编码器层的功能是生成编码, 这些编码包含输入中哪些部分彼此相关的信息. 然后将输出编码作为下一个编码器的输入. 每个编码器都由一个自我注意机制和一个前馈神经网络组成.

此外, 每个解码器层接收所有编码, 并利用其包含的上下文信息生成输出序列. 与编码器一样, 每个解码器也由一个自我注意机制, 一个对编码的注意机制和一个前馈神经网络组成.

None

预训练

在这一阶段, 以自我监督的方式在大量非结构化文本数据集上对模型进行预训练. 预训练的主要挑战在于计算成本.

存储 1B 参数模型所需的 GPU RAM => 1 个参数 -> 4 个字节(32 位浮点) => 1B 参数 -> 4*10⁹ 字节 = 4GB

1B 参数模型所需的 GPU 内存 = 4GB@32 位全精度

让我们计算一下训练 1B 参数模型所需的内存:

Model Parameter --> 4 bytes per parameter  
Gradients --> 4 bytes per parameter  
ADAM Optimizer (2 states) --> 8 bytes per parameter  
Activations and temp memory (variable size) --> 8 bytes per parameter (high-end estimate)  
==> 4 bytes parameter + 20 extra bytes per paramter

因此, 训练所需的内存是存储模型所需的内存的20倍. 存储 1B 参数模型所需的内存 = 4GB/@32 位全精度 训练 1B 参数模型所需的内存 = 80GB/@32 位全精度

数据并行训练技术

分布式数据并行 (DDP)

分布式数据并行(DDP)要求模型权重和所有其他附加参数, 梯度和优化器状态在单个 GPU 中进行训练. 如果模型太大, 则应使用模型分片来代替.

None

完全分片数据并行(FSDP)

完全分片数据并行(FSDP)通过在 GPU 之间分配(分片)模型参数, 梯度和优化器状态来减少内存.

None

微调

通过调整模型权重以更好地适应特定任务或领域, 微调可以帮助我们从预训练的LLM中获得更多信息. 这意味着, 你只需花费很少的成本和延迟, 就能获得比普通及时工程更高质量的结果.

None

近年来微调方法的演变发展. 同一分支上的模型具有一些共同特征. 模型的垂直位置显示了其发布日期的时间轴.

为什么要对 LLM 进行微调?

与Prompt相比, 微调在引导 LLM 行为方面往往更加有效和高效. 通过在一组示例中对模型进行训练, 你可以缩短精心设计的Prompt, 在不影响质量的前提下节省宝贵的输入标记. 你还可以使用更小的模型. 这反过来又会减少延迟和推理成本.

例如, 与 GPT-3.5 等性能相当的现成模型相比, 经过微调的 Llama 7B 模型在每个令牌上的成本效益要高出许多(约 50 倍).

微调如何工作

如前所述, 微调是针对其他任务对已训练好的模型进行调整. 微调的方法是利用原始模型的权重进行调整, 以适应新的任务.

‍模型在接受训练后, 会学习如何完成某些特定任务, 例如, GPT-3 曾在一个海量数据集上接受过训练, 因此它学会了生成故事, 诗歌, 歌曲, 信件和其他许多东西. 我们可以利用 GPT-3 的这种能力, 在特定任务上对其进行微调, 比如以特定方式生成客户询问的答案.

对模型进行微调有不同的方法和技术, 其中最流行的是迁移学习. 迁移学习源自计算机视觉领域, 它是冻结网络初始层的权重, 只更新后面各层权重的过程. 这是因为低层, 也就是更接近输入的层, 负责学习训练数据集的一般特征. 而更靠近输出的上层则学习与产生正确输出直接相关的更具体的信息.

下面是微调工作原理的快速可视化示意图:

None

PEFT

PEFT(Parameter Efficient Fine-Tuning, 参数高效微调)是一套技术或方法, 用于以最节省计算和时间的方式对大型模型进行微调, 而不损失任何性能, 我们可能会看到完全微调所带来的性能损失. 这样做的原因是, 随着模型越来越大, 比如像BLOOM这样拥有高达1760亿个参数的模型, 如果不花费数万美元对其进行微调, 几乎是不可能的. 但有时为了获得更好的性能, 几乎必须使用这样的大型模型. 这就是 PEFT 的用武之地. 它可以帮助你解决在使用这种大型模型时遇到的问题.

下面是一些 PEFT 技术:

迁移学习

迁移学习是指我们将模型中学习到的一些参数用于其他任务. 这听起来类似于微调, 但又有所不同. 在微调中, 我们会重新调整模型的所有参数, 或冻结部分权重, 然后调整其余参数. 但在迁移学习中, 我们会使用从模型中学习到的部分参数, 并将其用于其他网络. 这就为我们提供了更大的灵活性. 例如, 在微调时, 我们不能改变模型的架构, 这在很多方面限制了我们. 但在使用迁移学习时, 我们只需使用训练好的模型的一部分, 然后就可以将其附加到其他任何架构的模型上.

在使用 LLM 的 NLP 任务中经常可以看到迁移学习, 人们使用 T5 等预先训练好的模型中的Transformer网络编码器部分来训练后面的层.

Adapter

Adapter是最早发布的参数高效微调技术之一. 有一篇论文指出, 我们可以在已有的Transformer架构上添加更多层, 并只对这些层进行微调, 而不是对整个模型进行微调. 他们的研究表明, 与完全微调相比, 这种技术具有相似的性能.

None

‍左图是添加了Adapter层的修改后的Transformer架构. 我们可以看到, Adapter层是在注意堆栈和前馈堆栈之后添加的. 右图是Adapter层本身的架构. Adapter层包含一个瓶颈架构, 它接收输入并将其缩小到一个较小的维度表示, 然后通过一个非线性激活函数, 再将其放大到输入的维度. 这确保了Transformer堆栈中的下一层能够接收来自Adapter层的生成输出.

作者在论文中指出, 这种微调方法与完全微调不相上下, 但所消耗的计算资源和训练时间要少得多. 他们能够在 GLUE 基准上实现 0.4% 的完全微调, 同时增加 3.6% 的参数.

None

‍LoRA — 低秩适应

LoRA 是一种与Adapter层类似的策略, 但其目的是进一步减少可训练参数的数量. 它采用了一种数学上更为严谨的方法. LoRA 的工作原理是修改神经网络中可更新参数的训练和更新方式.

让我们用数学来解释一下. 我们知道, 预训练神经网络的权重矩阵是全秩的, 这意味着每个权重都是唯一的, 不能通过组合其他权重来获得. 但在 这一篇 论文中, 作者发现当预先训练好的语言模型被调整到新任务时, 权重的"内在维度"会降低. 这意味着权重可以用更小的矩阵表示, 或者说它的秩较低. 这反过来又意味着, 在反向传播过程中, 权重更新矩阵的秩较低, 因为预训练过程已经捕捉到了大部分必要信息, 在微调过程中只需针对特定任务进行调整.

更简单的解释是, 在微调过程中, 只有极少数权重会被大量更新, 因为大部分学习都是在神经网络的预训练阶段完成的. LoRA 利用这一信息来减少可训练参数的数量.

None

以 GPT-3 175B 为例, LoRA 研究团队证明, 即使全秩(即 d)高达 12,288 时, 也只需要很低的秩(即图 1 中的 r, 可以是 1 或 2)就足够了, 这使得 LoRA 在存储和计算方面都很高效.

图 2 表明, 矩阵 A[d X r] 和 B[r X k] 将是[d X k], 而我们可以改变 r. 虽然这会缩短训练时间, 但也可能导致信息丢失, 并随着 r 变小而降低模型性能. 不过, 使用 LoRA 时, 即使是低等级顺序, 性能也与完全训练过的模型相当, 甚至更好.

使用 HuggingFace 对 LoRA 进行微调

要使用 HuggingFace 实现 LoRA 微调, 需要使用 PEFT 库 将 LoRA Adapter注入模型, 并将其用作更新矩阵.

from transformers import AutoModelForCausalLM
from peft import get_peft_config, get_peft_model, LoraConfig, TaskType

model = AutoModelForCausalLM.from_pretrained(model_name_or_path, device_map="auto", trust_remote_code=True) # load the model
peft_config = LoraConfig(
    task_type=TaskType.CAUSAL_LM, inference_mode=False, r=32, lora_alpha=16, lora_dropout=0.1,
    target_modules=['query_key_value'] # optional, you can target specific layers using this
) # create LoRA config for the finetuning

model = get_peft_model(model, peft_config) # create a model ready for LoRA finetuning
model.print_trainable_parameters() 
# trainable params: 9,437,184 || all params: 6,931,162,432 || trainable%: 0.13615586263611604‍

完成这些工作后, 你就可以像平常一样训练模型了. 但这次所需的时间和计算资源将大大减少.

LoRA 的效率

作者在论文中指出, LoRA 只需使用 2% 的可训练参数, 就能胜过完全微调.

None

至于训练参数的数量, 我们可以使用秩 r 参数来控制. 例如, 假设权重更新矩阵有 100,000 个参数, A 为 200, B 为 500. 权重更新矩阵可以分解成维数更小的矩阵, A200 x 3, B3 x 500. 这样, 200 x 3 + 3 x 500 = 2100 个可训练参数, 仅占参数总数的2.1%. 由于我们可以决定只对特定层应用 LoRA, 因此这一比例还可以进一步降低.

‍由于训练和应用的参数数量远远少于实际模型的数量, 文件可以小到8MB. 这使得加载, 应用和传输所学模型变得更加容易和快捷.

Stable Diffusion 中的 LoRA

LoRA 最有趣的用例之一可以在图像生成应用中体现出来. 图像有其固有的风格, 可以直观地看到. 用户现在可以只训练 LoRA 权重, 并将其与 Dreambooth等技术结合使用, 而无需训练大量模型来获得特定风格的图像.

LoRA 权重还可以与其他 LoRA 权重相结合, 以加权组合的方式生成具有多种风格的图像. 你可以在 CivitAI上找到大量 LoRA Adapter并将其加载到你的模型中.

None

QLoRA

None

它与 LoRA 有何不同?

  • 它是 4 位Transformer
  • 它是一个 4 位Transformer.
  • QLoRA 是一种微调技术, 结合了高精度计算技术和低精度存储方法. 这有助于保持较小的模型尺寸, 同时确保模型仍然具有较高的性能和精度.
  • QLoRA 使用 LoRA 作为附件来修复量化误差过程中引入的误差.

QLoRA 的工作原理

  • QLoRA 的工作原理是引入 3 个新概念, 在保持相同性能质量的同时帮助减少内存. 它们是4 位正常浮点, 双量化分页优化器.

4 位正常浮点运算 (NF4)

  • 4 位 NormalFloat 是一种新的数据类型, 也是保持 16 位性能水平的关键因素. 它的主要特性是 数据类型中的任何位组合, 如 0011 或 0101, 都会从输入张量中分配等量的元素.
  • 权重和 PEFT 的 4 位量化, 以及 32 位精度的训练注入Adapter权重 (LORA).
  • QLoRA 有一个存储数据类型(NF4)和一个计算数据类型(16 位 BrainFloat).
  • 我们将存储数据类型去量化为计算数据类型, 以执行正向和反向传递, 但我们只计算使用 16 位 BrainFloat 的 LORA 参数的权重梯度.

1. 归一化: 模型权重首先归一化为零均值和单位方差. 这样可以确保权重分布在零点附近, 并在一定范围内.

2. 量化: 将归一化后的权重量化为 4 比特. 这包括将原始的高精度权重映射到一组较小的低精度值. 在 NF4 的情况下, 量化级别在归一化权重的范围内均匀分布.

3. 去量化: 在前向传递和反向传播过程中, 量化权重被去量化为全精度. 具体做法是将 4 位量化值映射回其原始范围. 去量化权重将用于计算, 但会以 4 位量化形式存储在内存中.

None

在数据的"桶"或"仓"中, 数据被量化. 数字 2 和 3 属于同一个量化值 2. 这种量化过程可以通过"四舍五入"到最接近的量化值来使用更少的数字.

量化

  • 双量化是指对 4 位 NF 量化过程中使用的量化常数进行量化的独特做法. 这种方法看似不起眼, 但却有可能为每个参数平均节省 0.5 位, 相关研究论文对此进行了强调. 事实证明, 这种优化在 QLoRA 中尤为有益, 因为 QLoRA 采用的是分块式 k 位量化. 与对所有权重进行整体量化不同, 这种方法是将权重分割成不同的块或块段, 然后进行独立量化.

None

  • 分块量化法会产生多个量化常数. 有趣的是, 这些常量可以进行第二轮量化, 从而节省更多空间. 由于量化常数的数量有限, 这种策略依然有效, 从而减轻了与该过程相关的计算和存储需求.

分页优化器

  • 如前文所示, 量化涉及创建桶或箱, 以涵盖范围广泛的数值. 这一过程会导致多个不同的数字被映射到同一个桶中, 例如在量化过程中将 2 和 3 都转换为数值 3. 因此, 权重的去量化会带来 1 的误差.
  • 将这些误差可视化到神经网络中更广泛的权重分布中, 就会发现量化的内在挑战. 尽管 QLoRA 适用于 4 位推理, 但它更像是一种微调机制, 而非独立的量化策略. 在使用 QLoRA 进行微调期间, LoRA 调整机制开始发挥作用, 其中涉及创建两个较小的权重更新矩阵. 这些矩阵以脑浮点 16 或浮点 16 等更高精度格式进行维护, 然后用于更新神经网络权重.

None

  • 值得注意的是, 在整个反向传播和前向传递过程中, 网络的权重都会进行去量化处理, 确保实际训练以更高精度的格式进行. 虽然存储仍采用较低精度, 但这种刻意的选择会带来量化误差. 不过, 模型训练过程本身具有适应能力, 可以减轻量化过程中固有的这些低效率. 从本质上讲, 采用更高精度的 LoRA 训练方法有助于模型了解并主动减少量化误差.

利用 HuggingFace 进行 QLoRA 微调

要使用 HuggingFace 进行 QLoRA 微调, 需要安装 BitsandBytes 库 和 PEFT 库. BitsandBytes 库负责 4 位量化以及整个低精度存储和高精度计算部分. PEFT 库将用于 LoRA 微调部分.

import torch
from peft import prepare_model_for_kbit_training, LoraConfig, get_peft_model
from transformers import AutoTokenizer, AutoModelForCausalLM, BitsAndBytesConfig

model_id = "EleutherAI/gpt-neox-20b"
bnb_config = BitsAndBytesConfig(
    load_in_4bit=True,
    bnb_4bit_use_double_quant=True,
    bnb_4bit_quant_type="nf4",
    bnb_4bit_compute_dtype=torch.bfloat16
) 

# setup bits and bytes config
model = AutoModelForCausalLM.from_pretrained(model_id, quantization_config=bnb_config, device_map={"":0})

model.gradient_checkpointing_enable()

model = prepare_model_for_kbit_training(model) # prepares the whole model for kbit training
config = LoraConfig(
    r=8, 
    lora_alpha=32, 
    target_modules=["query_key_value"], 
    lora_dropout=0.05, 
    bias="none", 
    task_type="CAUSAL_LM"
)
model = get_peft_model(model, config) # Now you get a model ready for QLoRA training

‍然后, 你可以再次使用高频训练器进行正常训练. 请参阅此 colab notebook, 作为 QLoRA 训练的指南‍.

IA3

IA3(通过抑制和放大内在激活注入Adapter)是一种基于Adapter的技术, 与 LoRA 有些类似. 作者的目标是复制 ICL(情境学习或 Few-Shot Prompt)的优点, 同时避免其带来的问题. ICL 在成本和推理方面可能会比较混乱, 因为它需要用示例来Prompt模型. 较长的Prompt需要更多的时间和计算来处理. 但是, ICL 也许是开始使用模型的最简单方法.

IA3 通过引入针对模型激活的缩放向量来工作. 共引入了 3 个向量: lv, iklff. 这些向量的目标是注意力层中的, 和密集层中的非线性层. 这些向量与模型中的默认值相乘. 一旦注入, 这些参数就会在训练过程中被学习, 而模型的其他部分则保持冻结. 这些学习到的向量实质上是针对当前任务重新调整或优化了目标预训练模型的权重.

None

‍到目前为止, 这似乎是一种基本的Adapter型 PEFT 方法. 但这还不是全部. 作者还使用了 3 个损失项来增强学习过程. 这 3 个损失项分别是 LLM, LULLLN. LLM 是标准的交叉熵损失, 可增加生成正确响应的可能性. 然后是 LUL, 即 Unlikelihood Loss. 这个损失项降低了使用等级分类法产生错误输出的概率. 最后是 LLN, 它是一种长度归一化损失, 将软最大交叉熵损失应用于所有输出选择的长度归一化对数概率. 这里使用了多重损失, 以确保更快更好地学习模型. 由于我们正在尝试使用很少的示例进行学习, 因此这些损失是必要的.

现在我们来谈谈 IA3 中两个非常重要的概念. 等级分类(Rank Classification)和长度归一化(Length Normalization).

在"等级分类”(Rank Classification)中, 一个模型需要根据一组回答的正确性对其进行排序. 这需要计算潜在回复的概率分数. 然后使用 LUL 来降低错误回答的概率, 从而提高正确回答的概率. 但是, 对于等级分类, 我们面临着一个关键问题, 即由于概率的作用原理, 标记数量较少的回复会排名靠前. 生成的标记数量越少, 概率就越高, 因为每个生成标记的概率都小于 1. 为了解决这个问题, 作者建议将回复的得分除以回复中的标记数. 这样做可以使分数正常化. 这里有一点非常重要, 那就是归一化是在对数概率而非原始概率上进行的. 对数概率是负数, 介于 0 到 1 之间.

使用示例

对于序列分类任务, 我们可以如下初始化 Llama 模型的 IA3 配置:

peft_config = IA3Config(
    task_type=TaskType.SEQ_CLS, target_modules=["k_proj", "v_proj", "down_proj"], feedforward_modules=["down_proj"]
)
P-Tuning

P-tuning 方法旨在优化传递给模型的Prompt表示. 在P-Tuning 论文中, 作者强调了在处理LLM时, Prompt工程是一项非常强大的技术. P-Tuning 方法建立在Prompt工程的基础之上, 并试图进一步提高良好Prompt的有效性.

P-Tuning 的工作原理是为你的Prompt语创建一个小型编码器网络, 为你通过的Prompt语创建一个软Prompt语. 要使用 P-tuning调整 LLM, 你需要创建一个Prompt模板来代表你的Prompt. 在模板中使用上下文x来获取标签y. 这就是论文中提到的方法. 用于Prompt模板的标记是可训练和可学习的参数, 这些参数被称为伪标记. 我们还添加了一个Prompt编码器, 它可以帮助我们根据手头的具体任务更新伪标记. Prompt编码器通常是一个bi-LSTM网络, 它为模型学习最佳的Prompt表示法, 然后将表示法传递给它. LSTM 网络附属于原始模型. 这里只训练编码器网络和伪 token, 原始网络的权重不受影响. 训练完成后, LSTM 头将被丢弃, 因为我们可以直接使用hi.

简而言之, Prompt编码器只是改变了所传递Prompt的嵌入, 以更好地表示任务, 其他一切都保持不变.

None

‍Prefix Tuning

Prefix Tuning可视为 P-Tuning 的下一个版本. P-Tuning 的作者在 P-Tuning V-2 上发表了一篇论文, 讨论了 P-Tuning 的问题. 在这篇论文中, 他们实现了本文 中介绍的Prefix Tuning. Prefix Tuning 和 P-Tuning 并无太大区别, 但仍会导致不同的结果. 让我们深入了解一下.

None

‍在 P-Tuning中, 我们只在输入嵌入中添加了可学习的参数, 但在 Prefix Tuning 中, 我们将这些参数添加到网络的所有层. 这就确保了模型本身能学习到更多关于微调任务的知识. 我们将可学习的参数添加到Prompt和Transformer层的每一层激活中. 与 P-Tuning 不同的是, 我们并不完全修改Prompt嵌入, 而只是在每一层的Prompt开始时添加极少量的可学习参数.

下面是一个直观的解释::

Prefix Tuning

在Transformer的每一层, 我们都会将软Prompt与具有可学习参数的输入连接起来. 这些可学习参数是通过一个非常小的 MLP(只有 2 个全连接层)来调整的. 之所以这样做, 是因为作者在论文中指出, 直接更新这些Prompt标记对学习率和初始化非常敏感. 软Prompt增加了可训练参数的数量, 但也大大提高了模型的学习能力. MLP 或全连接层可以在以后放弃, 因为我们只关心软Prompt, 这些软Prompt将在推理过程中附加到输入序列中, 并为模型提供指导.

None

‍Prompt Tuning (非 Prompt Engineering)

Prompt Tuning是建立在仅使用软Prompt进行微调的基础上的首批论文之一. Prompt Tuning是一个非常简单且易于实现的想法. 它包括在输入前添加特定的Prompt, 并为该特定Prompt使用虚拟标记或新的可训练标记. 在此过程中, 可以对这些新的虚拟标记进行微调, 以学习更好的Prompt表示. 这意味着模型经过调整后能更好地理解Prompt. 以下是论文中的Prompt Tuning与完全微调的比较:‍

None

‍在这里你可以看到, 如果我们想在多个任务中使用模型, 那么完全模型调整需要存在多个模型副本. 但使用Prompt调优时, 只需存储Prompt标记的已学虚拟标记. 因此, 举例来说, 如果你使用类似*"Classify this tweet: {tweet}"*这样的Prompt, 目标就是为Prompt学习新的更好的嵌入. 在推理过程中, 只有这些新的嵌入才会被用于生成输出. 这样, 模型就可以调整Prompt, 帮助自己在推理过程中生成更好的输出.

Prompt Tuning的效率

使用Prompt Tuning的最大优势是学习到的参数体积小. 由于我们可以确定新标记的维度大小和参数数量, 因此我们可以极大地控制要学习的参数数量. 在论文中, 作者展示了即使可训练的标记数量很少, 该方法也能取得非常好的效果. 而且随着使用的模型越大, 性能只会越来越好. 你可以在这里阅读这篇论文.‍

None

‍另一个最大的优势是, 我们可以在多个任务中使用同一个模型, 无需做任何改动, 因为更新的只是Prompt标记的嵌入. 也就是说, 只要模型足够大, 足够复杂, 就可以将同一个模型用于推文分类任务和语言生成任务, 而无需对模型本身做任何改动. 但一个很大的限制是, 模型本身并不能学到任何新东西. 这纯粹是一个Prompt优化任务. 这意味着, 如果模型从未在情感分类数据集上进行过训练, Prompt Tuning可能不会有任何帮助. 需要注意的是, 这种方法优化的是Prompt, 而不是模型. 因此, 如果你不能手工制作一个能相对较好地完成任务的Prompt语, 那么尝试使用Prompt语优化技术来优化Prompt语也是没有用的.

硬Prompt和软Prompt

硬Prompt可以看作是一个静态的, 或充其量是一个模板的定义Prompt的概念. 生成式人工智能应用还可以使用多个Prompt模板.

硬Prompt是手工制作的文本Prompt, 具有不连续的输入标记. ~ HuggingFace

Prompt模板允许Prompt被存储, 重复使用, 共享编程. 生成式Prompt可纳入程序, 用于编程, 存储和重复使用.

软Prompt是在Prompt Tuning过程中创建的.

与硬Prompt不同, 软Prompt不能以文本形式查看和编辑. Prompt包括一个嵌入, 即一串数字, 从更大的模型中获取知识.

因此, 可以肯定的是, 软Prompt的缺点是缺乏可解释性. 人工智能会发现与特定任务相关的Prompt, 但却无法解释为什么会选择这些嵌入. 与深度学习模型本身一样, 软Prompt也是不透明的.

软Prompt可以替代额外的训练数据.

None

T5 硬Prompt与软Prompt的比较

LoRA 与 Prompt Tuning

现在, 我们已经探索了各种 PEFT 技术. 现在的问题是, 是使用 Adapter 和 LoRA 这样的加法技术, 还是使用 P-Tuning 和 Prefix Tuning 这样的基于Prompt的技术.

在比较 LoRA 与 P-Tuning 和 Prefix Tuning 时, 我们可以肯定地说, 从充分利用模型的角度来看, LoRA 是最好的策略. 但根据你的需求, 它可能不是最有效的. 如果你想训练模型的任务与已训练的任务大不相同, LoRA 无疑是高效调整模型的最佳策略. 但是, 如果模型或多或少已经理解了你的任务, 但挑战在于如何正确Prompt模型, 那么你就应该使用Prompt Tuning技术. Prompt Tuning不会修改模型中的很多参数, 而主要集中在传递的Prompt上.

‍需要注意的一个要点是, LoRA 会将权重更新矩阵分解为更小的秩矩阵, 并使用它们来更新模型的权重. 即使可训练参数较低, LoRA 也会更新神经网络目标部分的所有参数. 而在即时调整技术中, 只有少数可训练参数被添加到模型中, 这通常有助于模型更好地适应和理解任务, 但并不能帮助模型很好地学习新特性.

LoRA 和 PEFT 与完全微调的比较

PEFT(参数高效微调)是作为全面微调的替代方案提出的. 对于大多数任务, 已有论文表明 LoRA 等 PEFT 技术可与完全微调技术相媲美, 甚至更好. 但是, 如果你希望模型适应的新任务与模型已经训练过的任务完全不同, 那么 PEFT 可能就不够用了. 在这种情况下, 有限的可训练参数会导致重大问题.

‍如果我们试图使用 LLaMA 或 Alpaca 等基于文本的模型来构建代码生成模型, 我们可能应该考虑对整个模型进行微调, 而不是使用 LoRA 来调整模型. 这是因为该任务与模型已经知道并训练过的任务差别太大. 这种任务的另一个很好的例子是训练一个只懂英语的模型生成尼泊尔语文本.

LLM推理

在使用LLM进行推理时, 我们通常可以配置各种参数来微调其输出和性能. 下面是一些关键参数的细目:

1. Top-k 采样:

  • 每一步只对前 k 个最有可能的标记进行采样, 鼓励多样性并防止重复. k 值越高, 结果越多样化, 但一致性可能越差.

2. 温度:

  • 影响下一个可能标记的概率分布, 控制随机性和"创造性”.
  • 温度越低, 生成的文字越有可能重复, 而温度越高, 生成的文字越多样化, 可预测性越低.

3. Top-P 采样:

  • Top-P 或核心采样将标记的选择限制在累积概率质量达到阈值的词汇子集. 它有助于控制生成输出的多样性.

4. 最大长度:

  • 设置 LLM 生成的最大标记数, 防止输出过长.

5. 上下文Prompt:

  • 通过提供特定的上下文Prompt或输入, 你可以引导模型生成与上下文一致的文本. 这有助于确保生成的输出与给定上下文相关且连贯一致.

6. 重复惩罚:

  • 对重复 n-grams 的序列进行惩罚, 鼓励多样性和原创性.

7. 采样:

  • 在确定性(贪婪)和随机抽样生成之间进行选择. 贪婪模式每一步都会选择最有可能的标记, 而随机取样则会引入随机性.
  • 贪婪模式优先考虑准确性, 而随机取样则鼓励多样性和创造性.
  • 光束搜索: 保持多个潜在序列, 每一步都扩展最有希望的序列, 与 top-k 抽样相比, 旨在获得更连贯, 更准确的输出.

None

None

Prompt 工程

Prompt工程, 也称为上下文内Prompt, 指的是如何与 LLM 沟通, 在不更新模型权重的情况下引导 LLM 的行为以实现预期结果的方法. 这是一门经验科学, Prompt工程方法的效果在不同模型之间会有很大差异, 因此需要大量实验和启发式方法.

什么是Prompt?

我们与 LLM 交互的自然语言指令称为Prompt. Prompt的构造被称为Prompt工程.

None

Prompt的作用

LLM 所做的推理和完成Prompt中给出的指令的过程称为"上下文内学习”(In-Context Learning).

FewShot Prompt

语言学习者在没有任何示例的情况下对Prompt中的指令做出反应的能力称为ZeroShot学习.

如果只提供一个示例, 则称为OneShot学习.

如果提供了一个以上的示例, 则称为FewShot学习.

上下文窗口, 即 LLM 可以提供和推理的最大标记数, 在ZeroShot/OneShot/FewShot学习中至关重要.

None

思维链(Chain of Thought, CoT) Prompt

None

思维链(CoT)Prompt会生成一连串短句, 一步步描述推理逻辑, 即推理链推理, 最终得出最终答案. 对于复杂的推理任务, 同时使用大型模型(例如参数超过 50B 的模型), CoT 的优势更为明显. 简单的任务只能从 CoT Prompt 中略微受益.

PAL(程序辅助语言模型)

Gao等人, (2022)介绍了一种使用LLM读取自然语言问题并生成程序作为中间推理步骤的方法. 这种方法被称为程序辅助语言模型(PAL), 它与思维链Prompt方法的不同之处在于, 它不是使用自由格式文本来获取解决方案, 而是将解决方案步骤卸载到程序运行时, 例如 Python 解释器.

None

ReAct Prompting

ReAct 的灵感来源于"acting"与"reasoning"之间的协同作用, 这使得人类能够学习新任务并做出决策或推理.

CoT 无法访问外部世界或无法更新其知识, 这可能会导致事实幻觉和错误传播等问题.

ReAct 是一种将推理和行动与 LLM 结合起来的通用范式. ReAct 可促使 LLM 为任务生成口头推理跟踪和行动. 这样, 系统就能进行动态推理, 创建, 维护和调整行动方案, 同时还能与外部环境(如维基百科)进行交互, 将更多信息纳入推理. 下图显示了 ReAct 的一个示例以及执行问题解答所涉及的不同步骤.

None

模型优化技术

None

模型压缩方法:(a) 剪枝, (b) 量化, (c) 知识提炼

量化

模型量化是一种通过修改权重精度来减小大型神经网络(包括LLM)大小的技术. LLM 的量化得益于经验结果, 经验结果表明, 虽然与神经网络训练和推理相关的一些操作必须利用高精度, 但在某些情况下, 可以使用明显较低的精度(例如 float16)来减小模型的整体大小, 使其可以在可接受的能力和精度降低的情况下使用较弱的硬件运行.

None

模型大小的趋势

精度权衡

None

Tensor

一般来说, 在神经网络中使用高精度与更高的精度和更稳定的训练有关. 使用高精度也会增加计算成本, 因为它需要更多更昂贵的硬件. 主要由谷歌和英伟达完成的关于在某些神经网络操作中使用较低精度的可能性的研究表明, 在某些训练和推理操作中可以利用较低精度.

除了研究, 两家公司还开发了支持低精度操作的硬件和框架. 例如, 英伟达™(NVIDIA®)T4加速器是低精度GPU, 采用了张量核技术, 其效率明显高于K80. 谷歌的 TPU 引入了 bfloat16 的概念, 这是一种针对神经网络进行优化的特殊原始数据类型. 较低精度背后的基本理念是, 神经网络并不总是需要使用 64 位浮点运算的所有范围, 这样才能获得良好的性能.

None

bfloat16 数值格式

随着神经网络变得越来越庞大, 利用较低精度的重要性对使用它们的能力产生了重大影响. 对于 LLM 而言, 这一点变得更加重要.

作为参考, Nvidia 的 A100 GPU 在其最先进的版本中拥有 80GB 内存. 在下表中, 我们可以看到 LLama2-70B 模型大约需要 138GB 内存, 这意味着要托管它, 我们需要多个 A100. 将模型分布在多个 GPU 上意味着需要支付更多的 GPU 和基础设施开销. 另一方面, 量化版本只需要大约 40 GB 的内存, 因此只需一台 A100 即可轻松容纳, 大大降低了推理成本. 这个例子还没有提到, 在单个 A100 中, 使用量化模型可以更快地执行大多数单独的计算操作.

None

使用 llama.cpp 进行 4 位量化的示例, 大小可能因方法不同而略有差异

量化如何缩小模型?

量化通过减少每个模型权重所需的位数, 从而大大减小模型的大小. 典型的情况是将权重从 FP16(16 位浮点数)减少到 INT4(4 位整数). 这样, 模型就可以在便宜的硬件上运行, 并且/或者以更高的速度运行. 通过降低权重的精度, LLM 的整体质量也会受到一些影响.

研究表明, 这种影响因所使用的技术而异, 较大的模型受精度变化的影响较小. 较大的模型(超过~70B)即使转换为 4 位也能保持其能力, 一些技术(如 NF4)表明对其性能没有影响. 因此, 对于这些**大的模型来说, 4 位似乎是性能与尺寸/速度之间的最佳折中, 而对于较小的模型来说, 6 位或 8 位可能更好.

LLM 量化的类型

获取量化模型的技术可以分为两种:

  1. 训练后量化(PTQ): 将已训练模型的权重转换为较低精度, 而无需重新训练. 虽然 PTQ 简单易行, 但由于权值精度的损失, 可能会使模型的性能略有下降.
  2. 量化感知训练(QAT): 与 PTQ 不同, QAT 在训练阶段就整合了权重转换过程. 这通常会带来更优越的模型性能, 但对计算要求更高. QLoRA 是一种非常常用的 QAT 技术.

本篇文章将只关注PTQ策略以及它们之间的主要区别.

较大的量化模型与较小的非量化

既然降低精度会降低模型的准确性, 那么在推理成本相当的情况下, 你应该选择小的全精度模型, 还是大的量化模型呢? 虽然理想的选择可能因各种因素而异, 但 Meta 最近的研究 提供了一些有见地的指导原则.

虽然我们预计降低精度会导致精度下降, 但 Meta 的研究人员已经证明, 在某些情况下, 量化模型不仅表现出优越的性能, 而且还能减少延迟和提高吞吐量. 在比较8 位 13B 模型和 16 位 7B 模型时, 也可以观察到同样的趋势. 从本质上讲, 在比较推理成本相似的模型时, 大的量化模型要优于小的非量化模型. 这种优势在大型网络中更为明显, 因为它们在量化时的质量损失较小.

在哪里可以找到已经量化的模型?

幸运的是, 我们可以在Hugging Face Hub上找到许多已经用GPTQ(有些与 ExLLama 兼容), NF4GGML量化过的模型. 快速浏览一下就会发现, 这些模型中的很大一部分已经被TheBloke量化过了, 他是 LLM 社区中一位有影响力和受人尊敬的人物. 这位用户用不同类型的量化方法发布了多个模型, 因此我们可以针对每个特定的使用情况选择最合适的模型.

要轻松尝试这些模型, 请打开Google Colab, 并确保将运行时间更改为GPU(可使用免费的GPU). 首先安装由 Hugging Face 维护的Transformer库和所有必要的库. 由于我们将使用 Auto-GPTQ 对模型进行量化, 因此还需要相应的库:

!pip install transformers
!pip install accelerate

# Due to using GPTQ
!pip install optimum
!pip install auto-gptq

你可能需要重新启动运行时, 以便安装可用. 然后只需加载已量化的模型, 在本例中, 我们加载的是使用 Auto-GPTQ 量化的 Llama-2-7B-Chat 模型, 如下所示:

from transformers import AutoModelForCausalLM, AutoTokenizer
import torch

model_id = "TheBloke/Llama-2-7b-Chat-GPTQ"
tokenizer = AutoTokenizer.from_pretrained(model_id, torch_dtype=torch.float16, device_map="auto")
model = AutoModelForCausalLM.from_pretrained(model_id, torch_dtype=torch.float16, device_map="auto")

如何量化一个 LLM

如前所述, Hugging Face Hub上已经存在大量量化模型, 因此在许多情况下无需亲自压缩模型. 不过, 在同样的情况下, 你可能希望使用尚未量化的模型, 或者希望自己压缩模型. 这可以通过使用为你的特定领域量身定制的数据集来实现.

为了演示如何使用AutoGPTQ和 Transformer 库轻松量化模型, 我们使用了 Optimum 中 AutoGPTQ 界面的简化变体--Hugging Face 用于完善训练和推理的解决方案:

from transformers import AutoModelForCausalLM, AutoTokenizer, GPTQConfig

model_id = "facebook/opt-125m"

tokenizer = AutoTokenizer.from_pretrained(model_id)

quantization_config = GPTQConfig(bits=4, dataset = "c4", tokenizer=tokenizer)

model = AutoModelForCausalLM.from_pretrained(model_id, device_map="auto", quantization_config=quantization_config)

模型压缩是很耗时的. 例如, 一个 175B 的模型至少需要4 个 GPU 小时, 尤其是像 "c4"这样的庞大数据集. 值得注意的是, 量化过程或数据集的位数可以通过 GPTQConfig 的参数轻松修改. 改变数据集会影响量化的方式, 因此, 如果可能, 使用与推理中看到的数据相似的数据集, 以最大限度地提高性能.

量化技巧

None

模型量化领域出现了几种最先进的方法. 下面我们就来深入探讨几种著名的方法:

  1. GPTQ: 有一些实现选项 AutoGPTQ, ExLlamaGPTQ-for-LLaMa , 这种方法主要侧重于 GPU 执行.
  2. NF4:bitsandbytes库上实现, 它与Hugging FaceTransformer库紧密合作. 它主要QLoRA方法使用, 以 4 位精度加载模型, 用于微调.
  3. GGML: 这个 C 库与 llama.cpp 库密切配合. 它采用独特的二进制 LLM 格式, 加载速度快, 读取方便. 值得注意的是, 它最近转向 GGUF 格式, 确保了未来的可扩展性和兼容性.

许多量化库都支持几种不同的量化策略(如 4 位, 5 位和 8 位量化), 每种策略都在效率和性能之间做出了不同的权衡.

提炼

知识提炼

知识提炼是一种建立更小, 更便宜的模型("学生模型")的直接方法, 通过将技能从预先训练好的昂贵模型("教师模型")转移到学生模型中, 从而加快推理速度. 学生模型的构建方式没有太多限制, 只是需要与教师模型的输出空间相匹配, 以构建适当的学习目标.

None

教师模型已经在训练数据的基础上进行了微调. 因此, 概率分布很可能与地面实况数据非常吻合, 不会有太多的标记变化.

因此, 当温度 > 1 时, 概率分布会变得更广. T > 1 => 教师的输出 -> 软标签, 学生的输出 -> 软预测 T = 1 => 教师的输出 -> 硬标签, 学生的输出 -> 硬预测

对于生成式解码器模型, "提炼"并不有效. 它只对编码器模型有效, 如 BERT, 这种模型有很多冗余表示.

剪枝

网络剪枝是在保持模型容量的情况下, 通过剪枝不重要的模型权重或连接来缩小模型大小. 这可能需要重新训练, 也可能不需要. 剪枝可以是非结构化结构化.

  • 非结构化剪枝允许放弃任何权重或连接, 因此不会保留原有的网络结构. 非结构化剪枝通常不能很好地与现代硬件配合使用, 也不能带来实际的推理速度提升.
  • 结构化剪枝旨在保持密集矩阵乘法形式, 其中一些元素为零. 它们可能需要遵循一定的模式限制, 以配合硬件内核的支持. 在这里, 我们重点讨论结构化剪枝, 以实现Transformer模型中的高稀疏性.

构建剪枝网络的常规工作流程包括三个步骤:

  1. 训练密集网络, 直到收敛;
  2. 剪枝网络, 去除不需要的结构;
  3. 可选择重新训练网络, 使用新权重恢复性能.

通过网络剪枝在稠密模型中发现稀疏结构, 同时稀疏网络仍能保持相似性能的想法是由Lottery Ticket Hypothesis(LTH)激发的: 随机初始化的密集前馈网络包含一个子网络池, 其中只有一个子集(稀疏网络)是*"中奖彩票"*, 在单独训练时可以达到最佳性能.

总结一下

在这篇博文中, 我们探讨了检索增强生成(RAG)应用的文本生成部分, 强调了LLM的使用. 它涵盖了语言建模, 预训练挑战, 量化技术, 分布式训练方法以及 LLM 的微调. 讨论了参数高效微调(PEFT)技术, 包括Adapter, LoRA 和 QLoRA. 还介绍了Prompt策略, 模型压缩方法(如剪枝和量化)以及各种量化技术(GPTQ, NF4, GGML). 在文章的最后, 我们深入探讨了如何通过提炼和剪枝来缩小模型大小.

今天的文章就到这里吧!

一家之言, 欢迎斧正!

Happy Coding! Stay GOLDEN!