文章目录
Transformers 的设计理念
🤗 Transformers 是一个专为以下用户群体构建的库:
- 寻求使用、研究或扩展大规模 Transformers 模型的机器学习研究人员和教育者。
- 希望微调这些模型 或 在生产环境中使用它们(或两者兼而有之)的实际操作者。
- 只想下载预训练模型 并将其用于解决给定机器学习任务的工程师。
Transformers 设计时有两个主要目标:
1、尽可能简单快速地使用:
- 我们尽可能地限制用户能接触的抽象层,实际上几乎没有抽象。
用户只需学习三个标准类即可使用每个模型:configuration、models 和一个预处理类(用于 NLP 的 tokenizer,用于视觉的 image processor,用于音频的 feature extractor,以及用于多模态输入的 processor)。 - 所有这些类都可以通过一个通用的
from_pretrained()
方法从预训练实例中简单统一地初始化,该方法会从提供在 Hugging Face Hub 上的预训练检查点(如果需要的话)下载、缓存 和 加载相关类实例及相关数据(配置的超参数、分词器的词汇表和模型的权重)。 - 在这三个基本类之上,该库提供了两种 API:
pipeline()
用于快速在给定任务上使用模型进行推断,以及 Trainer 用于快速训练或微调 PyTorch 模型(所有 TensorFlow 模型与Keras.fit
兼容)。 - 因此,Transformers 不是神经网络的模块化工具箱。如果要基于 Transformers 扩展或搭建新项目,请使用常规的 Python、PyTorch、TensorFlow、Keras 模块,并从 Transformers 的基类继承以重用模型加载和保存等功能。如果想了解更多有关我们的模型代码的设计理念,请查看我们的重复自己博文。
2、提供与原始模型性能尽可能接近的最新模型:
- 我们为每种架构提供至少一个示例,复现了该架构官方作者提供的结果。
- 代码通常尽可能接近原始代码库,这意味着某些 PyTorch 代码可能不够pytorchic,因为它是转换后的 TensorFlow 代码,反之亦然。
其他几个目标:
- 尽可能一致地公开模型的内部:
- 我们使用单一 API 提供对完整隐藏状态和注意力权重的访问。
- 预处理类和基本模型 API 标准化,便于在不同模型之间轻松切换。
- 结合主观选择的有前途的工具进行模型微调和调查:
- 简单一致的方法来向词汇表和嵌入中添加新标记以进行微调。
- 简单的方法来屏蔽和修剪 Transformer 头部。
- 轻松在 PyTorch、TensorFlow 2.0 和 Flax 之间切换,允许使用一个框架进行训练并使用另一个进行推断。
主要概念
该库围绕每个模型的三类类构建:
- 模型类 可以是 PyTorch 模型(torch.nn.Module)、Keras 模型(tf.keras.Model)或 JAX/Flax 模型(flax.linen.Module),这些模型可以使用库中提供的预训练权重。
- 配置类 存储构建模型所需的超参数(如层数和隐藏大小)。通常情况下,如果您使用不进行任何修改的预训练模型,则创建模型将自动处理配置的实例化(配置是模型的一部分)。
- 预处理类 将原始数据转换为模型可接受的格式。一个 tokenizer 存储每个模型的词汇表,并提供编码和解码字符串为要馈送到模型的令牌嵌入索引列表的方法。Image processors 预处理视觉输入,feature extractors 预处理音频输入,而 processor 则处理多模态输入。
所有这些类都可以从预训练实例中实例化、本地保存,并通过以下三种方法与 Hub 共享:
from_pretrained()
允许您从库自身提供的预训练版本(支持的模型可在 Model Hub 上找到)或用户本地(或服务器上)存储的版本实例化模型、配置和预处理类。save_pretrained()
允许您本地保存模型、配置和预处理类,以便可以使用from_pretrained()
重新加载。push_to_hub()
允许您将模型、配置和预处理类共享到 Hub,以便所有人都可以轻松访问。
术语表
本术语表定义了通用的机器学习和 🤗 Transformers 术语,以帮助您更好地理解文档。
A
attention mask 注意力掩码
注意力掩码是一个可选参数,用于将序列批量处理在一起。
此参数指示模型哪些标记应该被关注,哪些不应该。
例如,考虑以下这两个序列:
from transformers import BertTokenizer
tokenizer = BertTokenizer.from_pretrained("google-bert/bert-base-cased")
sequence_a = "This is a short sequence."
sequence_b = "This is a rather long sequence. It is at least longer than the sequence A."
encoded_sequence_a = tokenizer(sequence_a)["input_ids"]
encoded_sequence_b = tokenizer(sequence_b)["input_ids"]
编码版本有不同的长度:
len(encoded_sequence_a), len(encoded_sequence_b)
(8, 19)
因此,我们无法像现在这样将它们放在同一个张量中。第一个序列需要填充到第二个序列的长度,或者第二个序列需要截断到第一个序列的长度。
在第一种情况下,ID 列表将通过填充索引进行扩展。我们可以传递一个列表给分词器,并要求它这样填充:
padded_sequences = tokenizer([sequence_a, sequence_b], padding=True)
我们可以看到,在第一句话的右边添加了0,使其与第二句话的长度相同:
padded_sequences["input_ids"]
[[101, 1188, 1110, 170, 1603, 4954, 119, 102, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [101, 1188, 1110, 170, 1897, 1263, 4954, 119, 1135, 1110, 1120, 1655, 2039, 1190, 1103, 4954, 138, 119, 102]]
这可以转换成 PyTorch 或 TensorFlow 中的张量。注意力掩码是一个二进制张量,指示填充索引的位置,以便模型不关注它们。
对于BertTokenizer,1
表示应该关注的值,而0
表示填充值。这个注意力掩码在分词器返回的字典中,其键为 attention_mask
。
padded_sequences["attention_mask"]
[[1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]]
autoencoding models 自动编码模型
autoregressive models 自回归模型
B
backbone
backbone 是输出原始隐藏状态或特征的网络(嵌入层和层)。它通常连接到一个 head,该部分接受特征作为输入以进行预测。
例如,ViTModel 是一个没有特定头部上层的骨干。其他模型也可以使用 VitModel
作为骨干,例如 DPT。
C
causal language modeling
这是一个预训练任务,其中模型按顺序读取文本,并需要预测下一个单词。
这通常是通过读取整个句子,但在模型内部使用掩码 来隐藏特定时间步的未来标记来完成的。
channel 通道
彩色图像由红色、绿色和蓝色(RGB)三个通道中的一些值的组合组成,而灰度图像只有一个通道。
在 🤗 Transformers 中,通道可以是图像张量的第一个或最后一个维度:[n_channels
, height
, width
] 或 [height
, width
, n_channels
].
连接主义时序分类 (CTC)
一种允许模型 在没有确切知道 输入和输出如何对齐的情况下 学习的算法;
CTC计算给定输入的所有可能输出的分布,并从中选择最可能的输出。
CTC常用于语音识别任务,因为语音并不总是干净地对齐于转录文本,这有多种原因,例如说话者的不同说话速度。
convolution 卷积
神经网络中的一种层,其中输入矩阵的元素与一个较小的矩阵(核或过滤器)逐元素相乘,然后将这些值在一个新的矩阵中求和。
这被称为卷积操作,它在整个输入矩阵上重复进行。
每次操作应用于输入矩阵的不同部分。
卷积神经网络(CNNs)在计算机视觉中常用。
D
DataParallel (DP)
在多个 GPU 上进行训练的并行技术,其中相同的设置被复制多次,每个实例接收一个不同的数据片段。处理是并行进行的,所有设置在每个训练步骤结束时都会同步。
了解更多关于 DataParallel 如何工作 这里。
decoder input IDs
这个输入仅针对编码器-解码器模型,包含将被输入到解码器的输入 IDs。这些输入应该用于序列到序列任务,例如翻译或摘要,并且通常是以针对每个模型特定的方式构建的。
大多数编码器-解码器模型(如 BART、T5)会从 labels
中自行创建 decoder_input_ids
。在这些模型中,传递 labels
是处理训练的首选方式。
请查阅每个模型的文档,以了解它们如何处理序列到序列训练中的这些输入 IDs。
decoder models 解码器模型
也被称为自回归模型,解码器模型涉及一个预训练任务(称为因果语言模型),其中模型按顺序读取文本并预测下一个单词。通常通过阅读整个句子并使用掩码来隐藏特定时间步的未来标记来完成。
Video : https://youtu.be/d_ixlCubqQw
深度学习 (DL)
使用多层神经网络的机器学习算法。
E
encoder models 编码器模型
也称为自动编码器模型,编码器模型将输入(如文本或图像)转换成一个称为嵌入的压缩数值表示。通常,编码器模型会使用像掩码语言模型这样的技术进行预训练,它会掩码输入序列的一部分,迫使模型创建更有意义的表示。
F
feature extraction特征提取
将原始数据选择和转换成一组更具信息量和对机器学习算法更有用的特征的过程。
特征提取的一些例子包括将原始文本转换为词嵌入以及从图像/视频数据中提取重要的特征,如边缘或形状。
前馈分块
在 transformers 的每个残差注意力块中,自注意力层通常后面跟着 2 个前馈层。
前馈层的中间嵌入大小通常比模型的隐藏大小要大(例如,对于 google-bert/bert-base-uncased
)。
对于一个大小为 [batch_size, sequence_length]
的输入,存储中间前馈嵌入 [batch_size, sequence_length, config.intermediate_size]
所需要的内存可能占内存使用的很大一部分。
在 Reformer: The Efficient Transformer 的作者注意到,由于计算与 sequence_length
维度无关,它从数学上等同于单独计算两个前馈层 [batch_size, config.hidden_size]_0, ..., [batch_size, config.hidden_size]_n
的输出嵌入,并在之后将它们连接到 [batch_size, sequence_length, config.hidden_size]
,其中 n = sequence_length
,这样在增加了计算时间的同时减少了内存使用,但得到了数学上等效的结果。
对于使用函数 apply_chunking_to_forward() 的模型,chunk_size
定义了并行计算多少个输出嵌入,从而定义了内存和时间复杂度之间的权衡。如果将 chunk_size
设置为 0,则不执行前馈分块。
微调模型
微调是一种迁移学习方法,它涉及到取一个预训练模型,冻结其权重,并用新添加的 模型头 替换输出层。模型头在您的目标数据集上训练。
查看微调预训练模型教程以获取更多详细信息,并了解如何使用 🤗 Transformers 微调模型。
H
head
模型头指的是神经网络的最外层,它接收原始的隐藏状态并将它们投影到不同的维度。对于每个任务都有一个不同的模型头。例如:
- GPT2ForSequenceClassification 是一个序列分类头 - 在基 GPT2Model 之上的线性层。
- ViTForImageClassification 是一个图像分类头 - 在基 ViTModel 之上的
CLS
标记的最终隐藏状态之上的线性层。 - Wav2Vec2ForCTC 是一个在基 Wav2Vec2Model 之上的语言模型头,并带有 CTC。
I
image patch 图像补丁
基于视觉的Transformer模型将图像分割成更小的补丁,这些补丁被线性嵌入,然后作为序列传递给模型。您可以在模型的配置中找到patch_size
(或分辨率)。
inference 推理
推理是在训练完成后评估模型在新的数据上的过程。查看推理流程教程以了解如何使用 🤗 Transformers 进行推理。
input IDs
输入 IDs 通常是将作为输入传递给模型的唯一所需参数。它们是标记索引,是构建序列的标记的数值表示,这些序列将被模型用作输入。
video : https://youtu.be/VFp38yj8h3A
每个分词器的工作方式不同,但其底层机制保持相同。以下是一个使用 BERT 分词器的示例,BERT 分词器是一个 WordPiece 分词器:
from transformers import BertTokenizer
tokenizer = BertTokenizer.from_pretrained("google-bert/bert-base-cased")
sequence = "A Titan RTX has 24GB of VRAM"
分词器负责将序列分割为分词器词汇表中可用的标记。
tokenized_sequence = tokenizer.tokenize(sequence)
令牌要么是单词,要么是子单词。例如,“VRAM”不在模型词汇表中,所以它被拆分为“V”,“RA”和“M”。为了指示这些令牌不是独立的单词,而是同一个单词的一部分,为“RA”和“M”添加了双哈希前缀:
print(tokenized_sequence)
['A', 'Titan', 'R', '##T', '##X', 'has', '24', '##GB', 'of', 'V', '##RA', '##M']
这些标记可以被转换成模型可理解的ID。这可以通过直接将句子输入到分词器中完成,该分词器利用了Rust实现的🤗 Tokenizers以实现峰值性能。
inputs = tokenizer(sequence)
分词器返回一个包含其对应模型正常工作所需所有参数的字典。分词索引位于键 input_ids
下:
encoded_sequence = inputs["input_ids"]
print(encoded_sequence)
[101, 138, 18696, 155, 1942, 3190, 1144, 1572, 13745, 1104, 159, 9664, 2107, 102]
请注意,分词器会自动添加“特殊标记”(如果关联的模型需要的话),这些是模型有时会使用的特殊ID。
如果我们解码之前的一串ID,
decoded_sequence = tokenizer.decode(encoded_sequence)
我们将看到
print(decoded_sequence)
[CLS] A Titan RTX has 24GB of VRAM [SEP]
因为这是BertModel期望其输入的方式。
L
labels
标签是一个可选参数,可以通过它来让模型自己计算损失。这些标签应该是模型的预期预测:它将使用标准损失来计算其预测与预期值(标签)之间的损失。
这些标签根据模型头部的不同而有所不同,例如:
- 对于序列分类模型(BertForSequenceClassification),模型期望一个维度为
(batch_size)
的张量,其中每个批次的值对应于整个序列的预期标签。 - 对于标记分类模型(BertForTokenClassification),模型期望一个维度为
(batch_size, seq_length)
的张量,其中每个值对应于每个单独标记的预期标签。 - 对于掩码语言模型(BertForMaskedLM),模型期望一个维度为
(batch_size, seq_length)
的张量,其中每个值对应于每个单独标记的预期标签:标签是掩码标记的标记ID,其余值将被忽略(通常为-100\)。 - 对于序列到序列任务(BartForConditionalGeneration, MBartForConditionalGeneration),模型期望一个维度为
(batch_size, tgt_seq_length)
的张量,其中每个值对应于与每个输入序列相关的目标序列。在训练期间,BART和T5将内部生成适当的decoder_input_ids
和decoder注意力掩码。通常不需要提供这些值。这不适用于利用编码器-解码器框架的模型。 - 对于图像分类模型(ViTForImageClassification),模型期望一个维度为
(batch_size)
的张量,其中每个批次的值对应于每个单独图像的预期标签。 - 对于语义分割模型(SegformerForSemanticSegmentation),模型期望一个维度为
(batch_size, height, width)
的张量,其中每个批次的值对应于每个单独像素的预期标签。 - 对于目标检测模型(DetrForObjectDetection),模型期望一个字典列表,其中包含
class_labels
和boxes
键,每个批次的值对应于每个单独图像的预期标签和边界框的数量。 - 对于自动语音识别模型(Wav2Vec2ForCTC),模型期望一个维度为
(batch_size, target_length)
的张量,其中每个值对应于每个单独标记的预期标签。
每个模型的标签可能都不同,因此请务必始终检查每个模型的文档,以获取有关其特定标签的更多信息!
基本模型(BertModel)不接受标签,因为这些是基本转换器模型,仅输出特征。
大型语言模型 (LLM)
一个通用术语,指的是在大量数据上训练的转换器语言模型(GPT-3、BLOOM、OPT)。这些模型通常也具有大量可学习的参数(例如,GPT-3有1750亿个)。
M
masked language modeling (MLM)
一个预训练任务,其中模型看到的是文本的损坏版本,通常是通过随机遮蔽一些标记来完成的,并需要预测原始文本。
multimodal
一个结合文本与其他类型输入的任务(例如图像)。
N
自然语言生成 (NLG)
所有与生成文本相关的任务(例如,使用 Transformers 写作,翻译)。
自然语言处理 (NLP)
一种通用的“处理文本”的说法。
自然语言理解 (NLU)
所有与理解文本中内容相关的任务(例如,对整个文本进行分类,对单个单词进行分类)。
P
pipeline
🤗 Transformers 中的 pipeline 是一个抽象概念,指的是一系列按照特定顺序执行的步骤,用于预处理和转换数据,并从模型中返回预测。pipeline 中可能包含的示例阶段包括数据预处理、特征提取和归一化。
更多详情,请参阅Pipelines for inference。
PipelineParallel (PP)
一种并行技术,其中模型在多个 GPU 上垂直(层级别)分割,因此模型的一层或几层只放置在单个 GPU 上。每个 GPU 并行处理管道的不同阶段,并处理批处理的小部分。了解更多关于 PipelineParallel 如何工作 这里.
pixel values 像素值
一个张量,包含传递给模型的图像的数值表示。像素值具有形状 [batch_size
, num_channels
, height
, width
],并且由图像处理器生成。
pooling 池化
一个将矩阵缩减为更小矩阵的操作,通过取最大值或平均值来实现对池化维度(s)的缩减。池化层通常位于卷积层之间,用于下采样特征表示。
position IDs 位置ID
与将每个标记的位置嵌入其中的RNN不同,变换器不知道每个标记的位置。因此,模型使用位置ID (position_ids
) 来识别每个标记在标记列表中的位置。
它们是一个可选参数。如果没有传递 position_ids
给模型,则ID将自动创建为绝对位置嵌入。
绝对位置嵌入在范围 [0, config.max_position_embeddings - 1]
中选择。一些模型使用其他类型的位置嵌入,例如正弦位置嵌入或相对位置嵌入。
preprocessing 预处理
将原始数据准备成能够轻松被机器学习模型消费的格式的工作。例如,文本通常通过分词进行预处理。为了更好地了解其他输入类型的预处理过程,请查看预处理教程。
pretrained model 预训练模型
一个在某个数据集上(例如整个维基百科)进行过预训练的模型。预训练方法涉及一个自监督目标,这可以通过阅读文本并尝试预测下一个单词来实现(参见因果语言建模)或者遮盖一些单词并尝试预测它们(参见掩码语言建模)。
语音和视觉模型有自己的预训练目标。例如,Wav2Vec2 是一个在对比任务上进行预训练的语音模型,该任务要求模型从一组“假”语音表示中识别出“真实”的语音表示。另一方面,BEiT 是一个在掩码图像建模任务上进行预训练的视觉模型,该任务遮盖了图像的一些片段,并要求模型预测这些遮盖的片段(类似于掩码语言建模目标)。
R
循环神经网络 (RNN)
一种使用循环遍历层来处理文本的模型。
representation learning 表示学习
机器学习的一个子领域,专注于学习原始数据的有意义表示。表示学习技术的例子包括词嵌入、自编码器和生成对抗网络(GANs)。
S
sampling rate 采样率
每秒采集的样本数量(音频信号)的测量值,单位为赫兹。采样率是将连续信号(如语音)离散化的结果。
self-attention 自注意力
每个输入元素都会找出它们应该关注输入中的哪些其他元素。
self-supervised learning 自监督学习
一种机器学习技术类别,其中模型从未标记的数据中创建自己的学习目标。它与无监督学习和监督学习不同,因为学习过程是受监督的,但不是明确来自用户的。
自监督学习的一个例子是掩码语言模型,其中模型接收到带有一定比例标记被移除的句子,并学会预测缺失的标记。
semi-supervised learning 半监督学习
一种广泛使用的机器学习训练技术,它结合了少量标记数据与大量未标记数据,以提高模型的准确性,与监督学习和无监督学习不同。
半监督学习的一个示例是“自训练”,其中模型在标记数据上训练,然后用于对未标记数据进行预测。模型预测最自信的部分的未标记数据被添加到标记数据集中,并用于重新训练模型。
sequence-to-sequence (seq2seq)
从输入生成新序列的模型,如翻译模型或摘要模型(例如Bart或T5)。
Sharded DDP
ZeRO(零冗余优化器)基础概念的另一种称呼,被各种其他ZeRO实现所采用。
stride 步长
在 卷积 或 池化 中,步长指的是核在矩阵上移动的距离。步长为1意味着核每次移动一个像素,步长为2意味着核每次移动两个像素。
supervised learning 监督学习
一种直接使用标记数据来纠正和指导模型性能的模型训练形式。数据被输入到正在训练的模型中,其预测结果与已知的标签进行比较。模型根据其预测的不准确性更新其权重,然后重复此过程以优化模型性能。
T
Tensor Parallelism (TP) 张量并行
在多个GPU上训练的并行技术,其中每个张量被分割成多个块,因此不是整个张量都驻留在单个GPU上,而是张量的每个片段都驻留在其指定的GPU上。片段被分别并在不同的GPU上并行处理,并在处理步骤结束时同步结果。这有时被称为水平并行,因为分割发生在水平层面。
了解更多关于张量并行这里。
token
句子的一部分,通常是一个单词,但也可能是子词(非常用词通常被分成子词)或标点符号。
token Type IDs
一些模型的目的是对句子对进行分类或问答。
这些需要将两个不同的序列连接到单个“input_ids”条目中,这通常需要借助特殊的标记,例如分类器([CLS]
)和分隔符([SEP]
)标记来完成。例如,BERT 模型构建其两个序列输入如下:
# [CLS] SEQUENCE_A [SEP] SEQUENCE_B [SEP]
我们可以使用我们的分词器通过将两个序列作为两个参数传递给 tokenizer
来自动生成这样的句子(而不是像之前那样作为一个列表),如下所示:
from transformers import BertTokenizer
tokenizer = BertTokenizer.from_pretrained("google-bert/bert-base-cased")
sequence_a = "HuggingFace is based in NYC"
sequence_b = "Where is HuggingFace based?"
encoded_dict = tokenizer(sequence_a, sequence_b)
decoded = tokenizer.decode(encoded_dict["input_ids"])
这将返回:
print(decoded)
[CLS] HuggingFace is based in NYC [SEP] Where is HuggingFace based? [SEP]
这是某些模型理解一个序列在哪里结束以及另一个序列在哪里开始的足够信息。然而,其他模型,如BERT,也部署了标记类型ID(也称为段ID)。它们以一个二进制掩码的形式表示模型中的两种序列类型。
分词器将此掩码作为“token_type_ids”条目返回:
encoded_dict["token_type_ids"]
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1]
第一个序列,即用于问题的“上下文”,其所有标记都由一个 0
表示,而第二个序列,对应于“问题”,其所有标记都由一个 1
表示。
一些模型,如 XLNetModel,使用一个额外的标记,由一个 2
表示。
transfer learning
一种技术,涉及使用预训练模型并将其适应到特定于您任务的特定数据集。您无需从头开始训练模型,而是可以利用现有模型获得的知识作为起点。这加快了学习过程并减少了所需的训练数据量。
transformer
基于自注意力的深度学习模型架构。
U
unsupervised learning
一种模型训练形式,其中提供给模型的数据未标记。无监督学习技术利用数据分布的统计信息来找到对当前任务有用的模式。
Z
Zero Redundancy Optimizer (ZeRO) 零冗余优化器
一种并行技术,其张量分片方式与 TensorParallel 类似,但整个张量会在前向或反向计算时重建,因此模型无需修改。此方法还支持各种卸载技术,以补偿有限的 GPU 内存限制。
了解更多关于 ZeRO 的信息 这里。
🤗 Transformers 能做什么
🤗 Transformers是一个用于自然语言处理(NLP)、计算机视觉和音频和语音处理任务的预训练模型库。
该库不仅包含Transformer模型,还包括 用于计算机视觉任务的 现代卷积网络 等非Transformer模型。
如果您看看今天最受欢迎的一些消费产品,比如智能手机、应用程序和电视,很可能背后都有某种深度学习技术的支持。
想要从您智能手机拍摄的照片中删除背景对象吗?这里是一个全景分割任务的例子(如果您还不了解这是什么意思,我们将在以下部分进行描述!)。
本页面提供了使用🤗 Transformers 库仅用三行代码解决不同的语音和音频、计算机视觉和NLP任务的概述!
音频
音频和语音处理任务与其他模态略有不同,主要是因为音频作为输入是一个连续的信号。
与文本不同,原始音频波形不能像句子可以被划分为单词那样被整齐地分割成离散的块。
为了解决这个问题,通常在固定的时间间隔内对原始音频信号进行采样。
如果在每个时间间隔内采样更多样本,采样率就会更高,音频更接近原始音频源。
以前的方法是预处理音频以从中提取有用的特征。现在更常见的做法是直接将原始音频波形输入到特征编码器中,以提取音频表示。
这样可以简化预处理步骤,并允许模型学习最重要的特征。
音频分类
音频分类 是一项 将音频数据 从预定义的类别集合中 进行标记的任务。这是一个广泛的类别,具有许多具体的应用,其中一些包括:
- 声学场景分类:使用场景标签(“办公室”、“海滩”、“体育场”)对音频进行标记。
- 声学事件检测:使用声音事件标签(“汽车喇叭声”、“鲸鱼叫声”、“玻璃破碎声”)对音频进行标记。
- 标记:对包含多种声音的音频进行标记(鸟鸣、会议中的说话人识别)。
- 音乐分类:使用流派标签(“金属”、“嘻哈”、“乡村”)对音乐进行标记。
from transformers import pipeline
classifier = pipeline(task="audio-classification", model="superb/hubert-base-superb-er")
preds = classifier("https://huggingface.co/datasets/Narsil/asr_dummy/resolve/main/mlk.flac")
preds = [{"score": round(pred["score"], 4), "label": pred["label"]} for pred in preds]
preds
[{'score': 0.4532, 'label': 'hap'},
{'score': 0.3622, 'label': 'sad'},
{'score': 0.0943, 'label': 'neu'},
{'score': 0.0903, 'label': 'ang'}]
自动语音识别
自动语音识别(ASR)将语音转录为文本。这是最常见的音频任务之一,部分原因是因为语音是人类交流的自然形式。
如今,ASR系统嵌入在智能技术产品中,如扬声器、电话和汽车。我们可以要求虚拟助手播放音乐、设置提醒和告诉我们天气。
但是,Transformer架构帮助解决的一个关键挑战是低资源语言。通过在大量语音数据上进行预训练,仅在一个低资源语言的一小时标记语音数据上进行微调,仍然可以产生与以前在100倍更多标记数据上训练的ASR系统相比高质量的结果。
from transformers import pipeline
transcriber = pipeline(task="automatic-speech-recognition", model="openai/whisper-small")
transcriber("https://huggingface.co/datasets/Narsil/asr_dummy/resolve/main/mlk.flac")
{'text': ' I have a dream that one day this nation will rise up and live out the true meaning of its creed.'}
计算机视觉
计算机视觉任务中最早成功之一是使用卷积神经网络(CNN)识别邮政编码数字图像。
图像由像素组成,每个像素都有一个数值。这使得将图像表示为像素值矩阵变得容易。每个像素值组合描述了图像的颜色。
计算机视觉任务可以通过以下两种通用方式解决:
1、使用卷积来学习图像的层次特征,从低级特征到高级抽象特征。
2、将图像分成块,并使用Transformer逐步学习每个图像块如何相互关联以形成图像。与CNN偏好的自底向上方法不同,这种方法有点像从一个模糊的图像开始,然后逐渐将其聚焦清晰。
图像分类
图像分类将整个图像从预定义的类别集合中进行标记。像大多数分类任务一样,图像分类有许多实际用例,其中一些包括:
- 医疗保健:标记医学图像以检测疾病或监测患者健康状况
- 环境:标记卫星图像以监测森林砍伐、提供野外管理信息或检测野火
- 农业:标记农作物图像以监测植物健康或用于土地使用监测的卫星图像
- 生态学:标记动物或植物物种的图像以监测野生动物种群或跟踪濒危物种
from transformers import pipeline
classifier = pipeline(task="image-classification")
preds = classifier(
... "https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/pipeline-cat-chonk.jpeg"
... )
preds = [{"score": round(pred["score"], 4), "label": pred["label"]} for pred in preds]
print(*preds, sep="\n")
{'score': 0.4335, 'label': 'lynx, catamount'}
{'score': 0.0348, 'label': 'cougar, puma, catamount, mountain lion, painter, panther, Felis concolor'}
{'score': 0.0324, 'label': 'snow leopard, ounce, Panthera uncia'}
{'score': 0.0239, 'label': 'Egyptian cat'}
{'score': 0.0229, 'label': 'tiger cat'}
目标检测
与图像分类不同,目标检测在图像中识别多个对象以及这些对象在图像中的位置(由边界框定义)。目标检测的一些示例应用包括:
- 自动驾驶车辆:检测日常交通对象,如其他车辆、行人和红绿灯
- 遥感:灾害监测、城市规划和天气预报
- 缺陷检测:检测建筑物中的裂缝或结构损坏,以及制造业产品缺陷
from transformers import pipeline
detector = pipeline(task="object-detection")
preds = detector(
... "https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/pipeline-cat-chonk.jpeg"
... )
preds = [{"score": round(pred["score"], 4), "label": pred["label"], "box": pred["box"]} for pred in preds]
preds
[{'score': 0.9865,
'label': 'cat',
'box': {'xmin': 178, 'ymin': 154, 'xmax': 882, 'ymax': 598}}]
图像分割
图像分割是一项像素级任务,将图像中的每个像素分配给一个类别。它与使用边界框标记和预测图像中的对象的目标检测不同,因为分割更加精细。分割可以在像素级别检测对象。有几种类型的图像分割:
- 实例分割:除了标记对象的类别外,还标记每个对象的不同实例(“dog-1”,“dog-2”)
- 全景分割:语义分割和实例分割的组合; 它使用语义类为每个像素标记并标记每个对象的不同实例
分割任务对于自动驾驶车辆很有帮助,可以创建周围世界的像素级地图,以便它们可以在行人和其他车辆周围安全导航。它还适用于医学成像,其中任务的更精细粒度可以帮助识别异常细胞或器官特征。图像分割也可以用于电子商务,通过您的相机在现实世界中覆盖物体来虚拟试穿衣服或创建增强现实体验。
from transformers import pipeline
segmenter = pipeline(task="image-segmentation")
preds = segmenter(
... "https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/pipeline-cat-chonk.jpeg"
... )
preds = [{"score": round(pred["score"], 4), "label": pred["label"]} for pred in preds]
print(*preds, sep="\n")
{'score': 0.9879, 'label': 'LABEL_184'}
{'score': 0.9973, 'label': 'snow'}
{'score': 0.9972, 'label': 'cat'}
深度估计
深度估计预测图像中每个像素到相机的距离。这个计算机视觉任务对于场景理解和重建尤为重要。例如,在自动驾驶汽车中,车辆需要了解行人、交通标志和其他车辆等物体的距离,以避免障碍物和碰撞。深度信息还有助于从2D图像构建3D表示,并可用于创建生物结构或建筑物的高质量3D表示。
有两种方法可以进行深度估计:
- stereo(立体):通过比较同一图像的两个略微不同角度的图像来估计深度
- monocular(单目):从单个图像中估计深度
from transformers import pipeline
depth_estimator = pipeline(task="depth-estimation")
preds = depth_estimator(
... "https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/pipeline-cat-chonk.jpeg"
... )
自然语言处理
NLP任务是最常见的类型之一,因为文本是我们进行交流的自然方式。为了让文本变成模型识别的格式,需要对其进行分词。这意味着将一段文本分成单独的单词或子词(tokens
),然后将这些tokens
转换为数字。因此,可以将一段文本表示为一系列数字,一旦有了一系列的数字,就可以将其输入到模型中以解决各种NLP任务!
文本分类
像任何模态的分类任务一样,文本分类将一段文本(可以是句子级别、段落或文档)从预定义的类别集合中进行标记。文本分类有许多实际应用,其中一些包括:
- 情感分析:根据某些极性(如
积极
或消极
)对文本进行标记,可以支持政治、金融和营销等领域的决策制定 - 内容分类:根据某些主题对文本进行标记,有助于组织和过滤新闻和社交媒体提要中的信息(
天气
、体育
、金融
等)
from transformers import pipeline
classifier = pipeline(task="sentiment-analysis")
preds = classifier("Hugging Face is the best thing since sliced bread!")
preds = [{"score": round(pred["score"], 4), "label": pred["label"]} for pred in preds]
preds
[{'score': 0.9991, 'label': 'POSITIVE'}]
Token分类
在任何NLP任务中,文本都经过预处理,将文本序列分成单个单词或子词。这些被称为tokens。Token分类将每个token
分配一个来自预定义类别集的标签。
两种常见的Token分类是:
- 命名实体识别(NER):根据实体类别(如组织、人员、位置或日期)对
token
进行标记。NER在生物医学设置中特别受欢迎,可以标记基因、蛋白质和药物名称。 - 词性标注(POS):根据其词性(如名词、动词或形容词)对标记进行标记。POS对于帮助翻译系统了解两个相同的单词如何在语法上不同很有用(作为名词的银行与作为动词的银行)。
from transformers import pipeline
classifier = pipeline(task="ner")
preds = classifier("Hugging Face is a French company based in New York City.")
preds = [
... {
... "entity": pred["entity"],
... "score": round(pred["score"], 4),
... "index": pred["index"],
... "word": pred["word"],
... "start": pred["start"],
... "end": pred["end"],
... }
... for pred in preds
... ]
print(*preds, sep="\n")
{'entity': 'I-ORG', 'score': 0.9968, 'index': 1, 'word': 'Hu', 'start': 0, 'end': 2}
{'entity': 'I-ORG', 'score': 0.9293, 'index': 2, 'word': '##gging', 'start': 2, 'end': 7}
{'entity': 'I-ORG', 'score': 0.9763, 'index': 3, 'word': 'Face', 'start': 8, 'end': 12}
{'entity': 'I-MISC', 'score': 0.9983, 'index': 6, 'word': 'French', 'start': 18, 'end': 24}
{'entity': 'I-LOC', 'score': 0.999, 'index': 10, 'word': 'New', 'start': 42, 'end': 45}
{'entity': 'I-LOC', 'score': 0.9987, 'index': 11, 'word': 'York', 'start': 46, 'end': 50}
{'entity': 'I-LOC', 'score': 0.9992, 'index': 12, 'word': 'City', 'start': 51, 'end': 55}
问答
问答是另一个token-level
的任务,返回一个问题的答案,有时带有上下文(开放领域),有时不带上下文(封闭领域)。每当我们向虚拟助手提出问题时,例如询问一家餐厅是否营业,就会发生这种情况。它还可以提供客户或技术支持,并帮助搜索引擎检索您要求的相关信息。
有两种常见的问答类型:
- 提取式:给定一个问题和一些上下文,答案是从模型必须提取的上下文中的一段文本跨度。
- 抽象式:给定一个问题和一些上下文,答案从上下文中生成;这种方法由[
Text2TextGenerationPipeline
]处理,而不是下面显示的[QuestionAnsweringPipeline
]。
from transformers import pipeline
question_answerer = pipeline(task="question-answering")
preds = question_answerer(
... question="What is the name of the repository?",
... context="The name of the repository is huggingface/transformers",
... )
print(
... f"score: {round(preds['score'], 4)}, start: {preds['start']}, end: {preds['end']}, answer: {preds['answer']}"
... )
score: 0.9327, start: 30, end: 54, answer: huggingface/transformers
摘要
摘要从较长的文本中创建一个较短的版本,同时尽可能保留原始文档的大部分含义。摘要是一个序列到序列的任务;它输出比输入更短的文本序列。有许多长篇文档可以进行摘要,以帮助读者快速了解主要要点。法案、法律和财务文件、专利和科学论文等文档可以摘要,以节省读者的时间并作为阅读辅助工具。
像问答一样,摘要有两种类型:
- 提取式:从原始文本中识别和提取最重要的句子
- 抽象式:从原始文本生成目标摘要(可能包括不在输入文档中的新单词);[
SummarizationPipeline
]使用抽象方法。
from transformers import pipeline
summarizer = pipeline(task="summarization")
summarizer(
... "In this work, we presented the Transformer, the first sequence transduction model based entirely on attention, replacing the recurrent layers most commonly used in encoder-decoder architectures with multi-headed self-attention. For translation tasks, the Transformer can be trained significantly faster than architectures based on recurrent or convolutional layers. On both WMT 2014 English-to-German and WMT 2014 English-to-French translation tasks, we achieve a new state of the art. In the former task our best model outperforms even all previously reported ensembles."
... )
[{'summary_text': ' The Transformer is the first sequence transduction model based entirely on attention . It replaces the recurrent layers most commonly used in encoder-decoder architectures with multi-headed self-attention . For translation tasks, the Transformer can be trained significantly faster than architectures based on recurrent or convolutional layers .'}]
翻译
翻译将一种语言的文本序列转换为另一种语言。它对于帮助来自不同背景的人们相互交流、帮助翻译内容以吸引更广泛的受众,甚至成为学习工具以帮助人们学习一门新语言都非常重要。
除了摘要之外,翻译也是一个序列到序列的任务,意味着模型接收输入序列并返回目标输出序列。
在早期,翻译模型大多是单语的,但最近,越来越多的人对可以在多种语言之间进行翻译的多语言模型感兴趣。
from transformers import pipeline
text = "translate English to French: Hugging Face is a community-based open-source platform for machine learning."
translator = pipeline(task="translation", model="google-t5/t5-small")
translator(text)
[{'translation_text': "Hugging Face est une tribune communautaire de l'apprentissage des machines."}]
语言模型
语言模型是一种预测文本序列中单词的任务。它已成为一种非常流行的NLP任务,因为预训练的语言模型可以微调用于许多其他下游任务。
最近,人们对大型语言模型(LLMs)表现出了极大的兴趣,这些模型展示了zero learning
或few-shot learning
的能力。这意味着模型可以解决它未被明确训练过的任务!语言模型可用于生成流畅和令人信服的文本,但需要小心,因为文本可能并不总是准确的。
有两种类型的话语模型:
- causal:模型的目标是预测序列中的下一个
token
,而未来的tokens
被遮盖。
from transformers import pipeline
prompt = "Hugging Face is a community-based open-source platform for machine learning."
generator = pipeline(task="text-generation")
generator(prompt) # doctest: +SKIP
- masked:模型的目标是预测序列中被遮蔽的
token
,同时具有对序列中所有tokens
的完全访问权限。
text = "Hugging Face is a community-based open-source <mask> for machine learning."
fill_mask = pipeline(task="fill-mask")
preds = fill_mask(text, top_k=1)
preds = [
... {
... "score": round(pred["score"], 4),
... "token": pred["token"],
... "token_str": pred["token_str"],
... "sequence": pred["sequence"],
... }
... for pred in preds
... ]
preds
[{'score': 0.2236,
'token': 1761,
'token_str': ' platform',
'sequence': 'Hugging Face is a community-based open-source platform for machine learning.'}]
多模态
多模态任务要求模型处理多种数据模态(文本、图像、音频、视频)以解决特定问题。
图像描述是一个多模态任务的例子,其中模型将图像作为输入并输出描述图像或图像某些属性的文本序列。
虽然多模态模型处理不同的数据类型或模态,但内部预处理步骤帮助模型将所有数据类型转换为embeddings
(向量或数字列表,包含有关数据的有意义信息)。
对于像图像描述这样的任务,模型学习图像嵌入和文本嵌入之间的关系。
文档问答
文档问答是从文档中回答自然语言问题的任务。与token-level
问答任务不同,文档问答将包含问题的文档的图像作为输入,并返回答案。
文档问答可用于解析结构化文档并从中提取关键信息。在下面的例子中,可以从收据中提取总金额和找零金额。
from transformers import pipeline
from PIL import Image
import requests
url = "https://huggingface.co/datasets/hf-internal-testing/example-documents/resolve/main/jpeg_images/2.jpg"
image = Image.open(requests.get(url, stream=True).raw)
doc_question_answerer = pipeline("document-question-answering", model="magorshunov/layoutlm-invoices")
preds = doc_question_answerer(
... question="What is the total amount?",
... image=image,
... )
preds
[{'score': 0.8531, 'answer': '17,000', 'start': 4, 'end': 4}]
希望这个页面为您提供了一些有关每种模态中所有类型任务的背景信息以及每个任务的实际重要性。在下一节中,您将了解Transformers如何解决这些任务。
🤗 Transformer 如何解决任务
在🤗 Transformer能做什么中,您了解了自然语言处理(NLP)、语音和音频、计算机视觉任务以及它们的一些重要应用。本页将详细介绍模型 如何解决这些任务,并解释其内部的工作原理。
解决特定任务的方法有很多,一些模型可能实现某些技术,甚至从全新的角度来处理任务,但对于Transformer模型,基本思想是相同的。
由于其灵活的架构,大多数模型都是编码器、解码器或编码器-解码器结构的变体。
除了Transformer模型外,我们的库还包括几个卷积神经网络(CNNs),这些神经网络至今仍用于计算机视觉任务。我们还将解释现代CNN是如何工作的。
为了解释任务是如何解决的,我们将深入了解模型内部如何进行以输出有用的预测。
- Wav2Vec2 用于音频分类和自动语音识别(ASR)
- Vision Transformer (ViT) 和 ConvNeXT 用于图像分类
- DETR 用于目标检测
- Mask2Former 用于图像分割
- GLPN 用于深度估计
- BERT 用于需要编码器的NLP任务,如文本分类、标记分类和问答
- GPT2 用于需要解码器的NLP任务,如文本生成
- BART 用于需要编码器-解码器的NLP任务,如摘要和翻译
在您继续之前,了解原始Transformer架构的基本知识将很有帮助。
了解编码器、解码器和注意力机制的工作原理将帮助您理解不同的Transformer模型是如何工作的。
如果您刚开始学习或需要复习,请查看我们的课程以获取更多信息!
语音和音频
Wav2Vec2 是一个在无标签语音数据上预训练,并在有标签数据上微调的自动语音识别和音频分类的自监督模型。
此模型有四个主要组件:
1、一个 特征编码器 接收原始音频波形,将其归一化到零均值和单位方差,并将其转换为每个20ms长的特征向量序列。
2、由于波形本质上是连续的,因此它们不能像 文本序列 那样分割成单独的单元。
这就是为什么将特征向量传递给一个 量化模块,该模块旨在学习离散语音单元。
语音单元是从一组称为 码本 的码字集合中选择的(你可以将其视为词汇表)。
从码本中选择最能代表连续音频输入的向量或语音单元,并将其通过模型转发。
3、大约一半的特征向量被随机遮蔽,遮蔽的特征向量被输入到 上下文网络 中,这是一个添加了相对位置嵌入的 Transformer 编码器。
4、上下文网络的预训练目标是一个 对比任务。
模型必须从一组错误的预测中预测遮蔽预测的真实量化语音表示,这鼓励模型找到最相似的上下文向量和量化语音单元(目标标签)。
现在 wav2vec2 已经预训练完毕,你可以在你的数据上对其进行微调,用于音频分类或自动语音识别!
音频分类
要使用预训练模型进行音频分类,在 Wav2Vec2 基础模型之上添加一个序列分类头。
分类头是一个线性层,接受编码器的隐藏状态。
隐藏状态代表了从每个音频帧中学到的特征,这些特征可以有不同的长度。
为了创建一个固定长度的向量,首先对隐藏状态进行池化,然后将其转换为类别标签的对数几率。
通过计算对数几率和目标之间的交叉熵损失,来找到最可能的类别。
准备好尝试音频分类了吗?查看我们的完整音频分类指南,了解如何微调 Wav2Vec2 并用于推理!
自动语音识别
要使用预训练的自动语音识别模型,请在 Wav2Vec2 基础模型之上添加一个语言模型头用于 连接主义时序分类 (CTC)。
语言模型头是一个线性层,它接受编码器的隐藏状态并将它们转换为 logits。每个 logit 代表一个标记类别(标记的数量来自任务词汇表)。
通过在 logits 和目标之间计算 CTC 损失,以找到最可能的标记序列,然后将这些序列解码为转录。
准备好尝试自动语音识别了吗?查看我们的完整自动语音识别指南,了解如何微调 Wav2Vec2 并用于推理!
计算机视觉
处理计算机视觉任务有两种方法:
1、将图像分割成一系列补丁,并使用 Transformer 并行处理它们。
2、使用现代 CNN,如 ConvNeXT,它依赖于卷积层但采用现代网络设计。
第三种方法将 Transformer 与卷积混合(例如,卷积视觉 Transformer 或 LeViT)。我们不会讨论这些,因为它们只是将这里我们考察的两个方法结合起来。
ViT 和 ConvNeXT 通常用于图像分类,但对于其他视觉任务,如目标检测、分割和深度估计,我们将分别查看 DETR、Mask2Former 和 GLPN;这些模型更适合那些任务。
图像分类
ViT 和 ConvNeXT 都可以用于图像分类;主要区别在于 ViT 使用了注意力机制,而 ConvNeXT 使用了卷积。
Transformer
ViT 完全用纯 Transformer 架构替换了卷积。如果你熟悉原始的 Transformer,那么你已经接近理解 ViT 了。
ViT 引入的主要变化在于图像是如何被输入到 Transformer 中的:
1、将图像分割成非重叠的正方形块,每个块被转换成一个向量或 patch embedding。
patch embeddings 是由一个卷积 2D 层生成的,它创建了适当的输入维度(对于基础 Transformer 来说,每个 patch embedding 是 768 个值)。
如果你有一个 224x224 像素的图像,你可以将其分割成 196 个 16x16 的图像块。就像文本被标记化为单词一样,图像被“标记化”为一系列块。
2、添加了一个 可学习的嵌入 - 一个特殊的 [CLS]
标记 - 到 patch embeddings 的开头,就像 BERT 一样。
使用 [CLS]
标记的最终隐藏状态作为附加分类头的输入;其他输出被忽略。这个标记有助于模型学习如何编码图像的表示。
3、最后要添加到 patch 和可学习嵌入中的是 位置嵌入,因为模型不知道图像块是如何排序的。
位置嵌入也是可学习的,大小与 patch embeddings 相同。
最后,所有嵌入都传递到 Transformer 编码器中。
4、输出,特别是带有 [CLS]
标记的输出,被传递到一个多层感知器头(MLP)。
ViT 的预训练目标是简单的分类。像其他分类头一样,MLP 头将输出转换为类标签上的 logits 并计算交叉熵损失以找到最可能的类别。
准备好尝试图像分类了吗?查看我们的完整 图像分类指南,了解如何微调 ViT 并用于推理!
CNN
本节简要解释了卷积,但了解它们如何改变图像的形状和大小将有所帮助。如果您对卷积不熟悉,请查看 fastai 书籍中的卷积神经网络章节!
ConvNeXT 是一种采用新的现代网络设计来提高性能的 CNN 架构。然而,卷积仍然是模型的核心。
从高层次的角度来看,卷积 是将较小的矩阵 (kernel) 与图像像素的小窗口相乘的操作。
它从中计算一些特征,例如线条的特定纹理或曲率。然后它滑动到下一个像素窗口;卷积移动的距离称为 stride。
一个基本的卷积,没有填充或步长,来自 深度学习卷积算术指南。
您可以将此输出馈送到另一个卷积层,并且随着每一层的连续,网络会学习更复杂和抽象的事物,如热狗或火箭。
在卷积层之间,通常会在其中添加一个池化层来降低维度并使模型对特征位置的变异更稳健。
ConvNeXT 以五种方式使 CNN 现代化:
1、改变每个阶段的块数量,并使用较大步长和相应内核大小对图像进行“patchify”。非重叠滑动窗口使这种 patchifying 策略类似于 ViT 将图像分割成块的方法。
2、一个 瓶颈 层会减少通道数量,然后将其恢复,因为执行 1x1 卷积更快,并且可以增加深度。倒置的瓶颈层通过扩展通道数量然后缩小它们来实现相反的效果,这更节省内存。
3、将瓶颈层中典型的 3x3 卷积层替换为 深度卷积,它对每个输入通道单独进行卷积,然后在最后将它们堆叠在一起。这拓宽了网络宽度,从而提高了性能。
4、ViT 具有全局感受野,这意味着由于它的注意力机制,它可以一次看到图像的更多部分。ConvNeXT 通过将内核大小增加到 7x7 来尝试复制这种效果。
5、ConvNeXT 还进行了一些层设计变更,以模仿 Transformer 模型。减少了激活和归一化层,激活函数从 ReLU 更改为 GELU,并使用 LayerNorm 而不是 BatchNorm。
卷积块输出传递到分类头,将输出转换为 logits 并计算交叉熵损失,以找到最可能的标签。
目标检测
DETR, DEtection TRansformer,是一种端到端的目标检测模型,它将 CNN 与 Transformer 编码器-解码器相结合。
1、一个预训练的CNN backbone 接收一个图像,以像素值的形式表示,并创建一个低分辨率的特征图。
对特征图应用一个1x1卷积以降低维度,并创建一个具有高级图像表示的新特征图。
由于Transformer是一个顺序模型,因此特征图被展平成一个特征向量序列,这些向量与位置嵌入相结合。
2、特征向量被传递到编码器,编码器使用其注意力层学习图像表示。
接下来,编码器隐藏状态与解码器中的 object queries
结合。
Object queries是学习到的嵌入,专注于图像的不同区域,并且随着它们通过每个注意力层而更新。
解码器隐藏状态被传递到一个前馈网络,该网络预测每个对象查询的边界框坐标和类别标签,如果没有对象则预测 no object
。
3、DETR并行解码每个对象查询以输出 N 个最终预测,其中 N 是查询的数量。
与典型的自回归模型逐个预测一个元素不同,目标检测是一个集合预测任务(边界框
,类别标签
),在单次传递中做出 N 个预测。
4、DETR在训练期间使用 二分匹配损失 来比较固定数量的预测与固定集合的地面真实标签。
如果集合中 N 个标签中的地面真实标签更少,则用 no object
类进行填充。
这个损失函数鼓励DETR在预测和地面真实标签之间找到一对一的分配。
如果边界框或类别标签不正确,则会发生损失。
同样,如果DETR预测了一个不存在的对象,它将受到惩罚。这鼓励DETR在图像中寻找其他对象而不是专注于一个非常突出的对象。
5、在DETR之上添加了一个目标检测头部来找到类别标签和边界框的坐标。
目标检测头部有两个组件:一个线性层将解码器隐藏状态转换为类别标签上的logits,以及一个MLP来预测边界框。
准备好尝试目标检测了吗?查看我们的完整 目标检测指南,了解如何微调DETR并用于推理!
图像分割
Mask2Former 是一个通用的架构,用于解决所有类型的图像分割任务。传统的分割模型通常是针对图像分割的特定子任务进行优化的,例如实例分割、语义分割或全景分割。
Mask2Former 将这些任务框架化为一个 掩码分类 问题。掩码分类将像素分组为 N 个段,并为给定图像预测 N 个掩码及其相应的类别标签。
在本节中,我们将解释 Mask2Former 的工作原理,然后在最后您可以尝试微调 SegFormer。
Mask2Former有三个主要组件:
1、一个 Swin 主干网络接受一个图像,并通过连续的3个3x3卷积创建一个低分辨率图像特征图。
2、将特征图传递给一个 像素解码器,该解码器逐步将低分辨率特征上采样到高分辨率每像素嵌入。像素解码器实际上生成多尺度特征(包含低分辨率和高分辨率特征),分辨率是原始图像的1/32、1/16和1/8。
3、将这些不同尺度的特征图依次输入到Transformer解码器的一个层中,以从高分辨率特征中捕获小物体。
Mask2Former的关键在于解码器中的 掩码注意力 机制。与可以关注整个图像的交叉注意力不同,掩码注意力只关注图像的某个区域。这更快,并且性能更好,因为图像的局部特征足以让模型进行学习。
4、与DETR类似,Mask2Former也使用学习到的物体查询,并将它们与像素解码器中的图像特征相结合,进行预测(类别标签
,掩码预测
)。解码器的隐藏状态传递到一个线性层,并转换为类别标签的logits。计算logits和类别标签之间的交叉熵损失,以找到最有可能的类别。
掩码预测是通过结合像素嵌入和最终的解码器隐藏状态生成的。计算logits和真实掩码之间的sigmoid交叉熵和dice损失,以找到最有可能的掩码。
准备好尝试物体检测了吗?查看我们的完整图像分割指南,了解如何微调SegFormer并将其用于推理!
深度估计
GLPN, 全局-局部路径网络,是一种用于深度估计的 Transformer,它将 SegFormer 编码器与轻量级解码器相结合。
1、与ViT类似,图像被分割成一系列的补丁,但这些图像补丁更小。这对于密集预测任务(如分割或深度估计)更有利。图像补丁被转换成补丁嵌入(有关补丁嵌入的创建方式,请参阅图像分类部分以获取更多详细信息),然后输入到编码器中。
2、编码器接受补丁嵌入,并通过多个编码器块传递它们。每个块由注意力和Mix-FFN层组成。后者的目的是提供位置信息。在每个编码器块的末尾是一个补丁合并层,用于创建层次化表示。相邻补丁组的特征被连接,然后对连接的特征应用线性层,以将补丁的数量减少到1/4的分辨率。这成为下一个编码器块的输入,这个过程会重复进行,直到你拥有1/8、1/16和1/32分辨率的图像特征。
3、一个轻量级的解码器从编码器中取出最后的特征图(1/32缩放),并将其上采样到1/16缩放。从这里,特征被传递到一个*选择性特征融合(SFF)*模块,该模块为每个特征选择并合并来自注意图的局部和全局特征,然后将其上采样到1/8缩放。这个过程会重复进行,直到解码特征与原始图像大小相同。输出通过两个卷积层,然后应用sigmoid激活函数来预测每个像素的深度。
自然语言处理
Transformer最初是为机器翻译而设计的,从那时起,它实际上已经成为解决所有NLP任务的默认架构。有些任务适合Transformer的编码器结构,而另一些任务更适合解码器。尽管如此,其他任务还是利用了Transformer的编码器-解码器结构。
文本分类
BERT 是一个只包含编码器的模型,也是第一个通过关注文本两端的单词来有效地实现深度双向性,从而学习更丰富文本表示的模型。
1、BERT 使用 WordPiece 标记化来生成文本的标记嵌入。为了区分单个句子和句子对,添加了一个特殊的 [SEP]
标记来区分它们。在每个文本序列的开头添加了一个特殊的 [CLS]
标记。带有 [CLS]
标记的最终输出被用作分类任务的分类头部的输入。BERT 还添加了一个段落嵌入来表示一个标记是否属于句子对中的第一句或第二句。
2、BERT 使用两个目标进行预训练:掩码语言建模和下一个句子预测。在掩码语言建模中,输入标记的某些百分比被随机掩码,模型需要预测这些标记。这解决了双向性问题,其中模型可能会作弊并看到所有单词,然后“预测”下一个单词。预测的掩码标记的最终隐藏状态被传递到一个具有词汇上软化的前馈网络,以预测掩码单词。
3、输入嵌入通过多个编码器层传递,以输出一些最终隐藏状态。
要使用预训练模型进行文本分类,请在基础 BERT 模型之上添加一个序列分类头。序列分类头是一个线性层,它接受最终隐藏状态并进行线性转换,将它们转换为 logit。计算 logit 和目标之间的交叉熵损失,以找到最可能的标签。
准备好尝试文本分类了吗?查看我们的完整 文本分类指南,了解如何微调 DistilBERT 并用于推理!
Token 分类
要使用 BERT 进行令牌分类任务,例如命名实体识别 (NER),请在基线 BERT 模型之上添加一个令牌分类头。令牌分类头是一个线性层,它接受最终隐藏状态并对它们进行线性变换以将它们转换为 logits。通过在每个 logits 和每个令牌之间计算交叉熵损失来找到最可能的标签。
准备好尝试令牌分类了吗?查看我们的完整令牌分类指南,了解如何微调 DistilBERT 并用于推理!
问题回答
要使用 BERT 进行问题回答,请在基础 BERT 模型之上添加一个跨度分类头。
这个线性层接受最终的隐藏状态,并通过线性变换来计算与答案相对应的 span
起始和结束的 logit。通过计算 logit 和标签位置之间的交叉熵损失,找到最可能对应答案的文本跨度。
准备好尝试你的手艺了吗?查看我们的完整问题回答指南,了解如何微调 DistilBERT 并将其用于推理!
💡 注意一下,一旦 BERT 经过预训练,它对不同任务的利用是多么简单。你只需要在预训练模型上添加一个特定的头,就可以将隐藏状态转换为所需的输出!
文本生成
GPT-2 是一个仅在大量文本上预训练的解码器模型。它可以根据提示生成令人信服(尽管不总是真实!)的文本,即使没有明确训练来完成其他 NLP 任务,如问答。
1、GPT-2使用字节对编码(BPE)对单词进行标记并生成标记嵌入。将位置编码添加到标记嵌入中,以指示序列中每个标记的位置。输入嵌入通过多个解码器块传递以输出一些最终隐藏状态。在每个解码器块内部,GPT-2使用一个掩码自注意力层,这意味着GPT-2不能关注未来标记。它只能关注左侧的标记。这与BERT的mask
标记不同,因为在掩码自注意力中,使用注意力掩码将未来标记的分数设置为0
。
2、解码器的输出传递到一个语言模型头,它执行线性变换将隐藏状态转换为logits。标签是序列中的下一个标记,通过将logits向右移动一个位置来创建。计算移位logits和标签之间的交叉熵损失,以输出下一个最可能的标记。
GPT-2的预训练目标完全基于因果语言模型,预测序列中的下一个单词。这使得GPT-2特别擅长涉及生成文本的任务。
准备好尝试文本生成了吗?查看我们的完整因果语言模型指南,了解如何微调DistilGPT-2并用于推理!
有关文本生成的更多信息,请查看文本生成策略指南!
摘要
编码器-解码器模型如BART和T5是为摘要任务的序列到序列模式设计的。我们将在本节中解释BART是如何工作的,然后您可以在本节末尾尝试微调T5。
1、BART的编码器架构与BERT非常相似,接受文本的标记和位置嵌入。
BART通过破坏输入并使用解码器重构它进行预训练。与其他具有特定破坏策略的编码器不同,BART可以应用任何类型的破坏。
其中,“文本填充”破坏策略效果最佳。在文本填充中,多个文本跨度被替换为单个掩码标记。这很重要,因为模型必须预测被掩码的标记,并且它教会模型预测缺失标记的数量。
输入嵌入和掩码跨度通过编码器传递,输出一些最终的隐藏状态,但与BERT不同,BART在最后不添加一个最终的前馈网络来预测一个单词。
2、编码器的输出传递给解码器,解码器必须预测被掩码的标记和来自编码器输出的任何未破坏的标记。
这为解码器恢复原始文本提供了额外的上下文。解码器的输出传递给语言模型头,它执行线性变换将隐藏状态转换为logits。
计算logits和标签之间的交叉熵损失,标签只是向右移动的标记。
想尝试一下摘要吗?查看我们的完整摘要指南,了解如何微调T5并用于推理!
想了解更多关于文本生成信息,请查看文本生成策略指南!
翻译
翻译是另一种序列到序列任务的例子,这意味着您可以使用像 BART 或 T5 这样的编码器-解码器模型来完成它。我们将在本节中解释 BART 的工作原理,然后在最后尝试微调 T5。
BART 通过添加一个单独随机初始化的编码器来适应翻译,该编码器将源语言映射到一个可以解码为目标语言的输入。
这个新编码器的嵌入被传递给预训练编码器,而不是原始的词嵌入。
源编码器通过使用模型输出的交叉熵损失来更新源编码器、位置嵌入和输入嵌入进行训练。在此第一步中,模型参数被冻结,所有模型参数在第二步中一起训练。
BART 之后推出了多语言版本 mBART,旨在进行翻译,并在许多不同的语言上进行预训练。
准备好尝试您的翻译技能了吗?查看我们的完整 翻译指南 以了解如何微调 T5 并使用它进行推理!
有关更多关于文本生成的信息,请查看 文本生成策略 指南!
Transformer 模型家族
自从2017年推出以来,原始 Transformer 模型(参见标注 Transformer 博客文章以获得温和的技术介绍)已经激发了众多新的令人兴奋的模型,这些模型扩展到了自然语言处理(NLP)任务之外。有用于预测蛋白质的折叠结构、训练猎豹奔跑和时间序列预测的模型。
由于可用的 Transformer 变体如此之多,很容易忽略更大的画面。这些模型共同之处在于它们都基于原始 Transformer 架构。
一些模型只使用编码器或解码器,而另一些则两者都使用。这为分类和检查 Transformer 家族中模型的较高层次差异提供了一个有用的分类法,并有助于您理解之前未曾遇到过的 Transformer。
如果您不熟悉原始 Transformer 模型或需要复习,请查看 Hugging Face 课程中的 Transformer 是如何工作的 这一章节。
一、计算机视觉
卷积网络
长期以来,卷积网络(CNNs)一直是计算机视觉任务的占主导地位的方法,直到 Vision Transformer 展示了它的可扩展性和效率。
即使如此,CNN的一些最佳特性,如平移不变性,非常强大(特别是对于某些任务),以至于一些 Transformer 集成了卷积到它们的架构中。
ConvNeXt 反转了这种交换,并将 Transformer 的设计选择纳入了现代 CNN。
例如,ConvNeXt 使用非重叠滑动窗口来 patchify 一张图片,并使用更大的核来增加其全局感受野。
ConvNeXt 还做出了几个层设计选择,以提高内存效率和改进性能,因此它在与 Transformer 的竞争中表现良好!
编码器
Vision Transformer (ViT) 为没有卷积的计算机视觉任务打开了大门。
ViT 使用标准的 Transformer 编码器,但它的主要突破在于如何处理图像。它将图像分割成固定大小的块,并使用这些块来创建嵌入,就像将句子分割成标记一样。
ViT 利用 Transformer 的高效架构,在需要较少训练资源的情况下,展示了与当时 CNNs 竞争的结果。ViT 很快就被其他视觉模型所追随,这些模型也能处理像分割和检测这样的密集视觉任务。
这些模型中之一是 Swin Transformer。它从较小尺寸的块构建层次特征图(就像 CNN 👀 一样,而不同于 ViT),并在更深层的相邻块中合并它们。注意力只计算在局部窗口内,窗口在注意力层之间移动,以创建连接帮助模型学习得更好。
由于 Swin Transformer 可以生成层次特征图,因此它是分割和检测等密集预测任务的理想候选者。
SegFormer 也使用 Transformer 编码器构建层次特征图,但它在上层添加了一个简单的多层感知器 (MLP) 解码器,以组合所有特征图并做出预测。
其他视觉模型,如 BeIT 和 ViTMAE,从 BERT 的预训练目标中汲取了灵感。
BeIT 通过 掩码图像建模 (MIM) 进行预训练;图像块被随机掩码,图像也被标记为视觉标记。BeIT 被训练来预测与掩码块对应的视觉标记。
ViTMAE 有一个类似的预训练目标,但它必须预测像素而不是视觉标记。不寻常的是,75% 的图像块被掩码!解码器从掩码标记和编码块中重建像素。预训练后,解码器被丢弃,编码器就可以用于下游任务了。
解码器
仅解码器的视觉模型较为罕见,因为大多数视觉模型都依赖于编码器来学习图像表示。
但对于图像生成等用例,解码器是一个自然的匹配,正如我们从像GPT-2这样的文本生成模型中看到的那样。
ImageGPT 使用与GPT-2相同的架构,但它不是预测序列中的下一个标记,而是预测图像中的下一个像素。
除了图像生成之外,ImageGPT 还可以被微调用于图像分类。
编码器-解码器
视觉模型通常使用编码器(也称为骨干网络)来提取重要的图像特征,然后再将它们传递给Transformer解码器。DETR 有一个预训练的骨干网络,但它也使用了完整的Transformer编码器-解码器架构来进行目标检测。
编码器学习图像表示,并在解码器中将它们与对象查询(每个对象查询都是一个学习的嵌入,专注于图像中的某个区域或对象)结合起来。
DETR预测每个对象查询的边界框坐标和类别标签。
二、自然语言处理
编码器
BERT 是一个仅使用编码器的 Transformer,它会随机屏蔽输入中的某些标记,以避免看到其他标记,从而防止它“作弊”。预训练的目标是根据上下文预测被屏蔽的标记。
这允许 BERT 充分利用左右上下文,帮助它学习更深层和更丰富的输入表示。然而,BERT 的预训练策略仍有改进的空间。
RoBERTa 通过引入一种新的预训练配方进行了改进,该配方包括更长时间的训练和更大的批次训练,在每个 epoch 中随机屏蔽标记,而不是仅在前处理期间屏蔽一次,并移除了下一句预测目标。
提高性能的占主导策略是增加模型大小。但训练大型模型在计算上非常昂贵。降低计算成本的一种方法是用更小的模型,如 DistilBERT。
DistilBERT 使用 知识蒸馏 - 一种压缩技术 - 创建一个更小的 BERT 版本,同时保留其几乎所有语言理解能力。
然而,大多数 Transformer 模型继续向更多参数的方向发展,导致新的模型专注于提高训练效率。
ALBERT 通过两种方式降低内存消耗:将较大的词汇嵌入分解成两个较小的矩阵,并允许层共享参数。
DeBERTa 添加了一种解耦的注意力机制,其中单词及其位置分别编码在两个向量中。注意力是从这些独立的向量而不是包含单词和位置嵌入的单个向量中计算的。
Longformer 也专注于使注意力更高效,特别是对于处理较长的序列长度的文档。它使用局部窗口注意力(仅从围绕每个标记的固定窗口大小计算注意力)和全局注意力(仅针对特定任务标记,如 [CLS]
用于分类)的组合,创建一个稀疏的注意力矩阵而不是完整的注意力矩阵。
解码器
GPT-2 是一个仅具有解码器的 Transformer 模型,它预测序列中的下一个单词。它对右侧的标记进行屏蔽,这样模型就不能通过向前看而“作弊”。通过在大量文本上进行预训练,GPT-2 在生成文本方面变得非常出色,即使生成的文本有时并不准确或真实。
但 GPT-2 缺乏 BERT 预训练的双向上下文,这使得它不适合某些任务。
XLNET 通过使用排列语言建模目标(PLM)结合了 BERT 和 GPT-2 预训练目标的优点,这使得它能够双向学习。
在 GPT-2 之后,语言模型变得更大,现在被称为 大型语言模型(LLMs)。如果在大足够的数据集上进行预训练,LLMs 显示出少量或零样本学习。
GPT-J 是一个具有 6B 参数的 LLM,在 400B 个标记上进行训练。
GPT-J 之后是 OPT,这是一个仅具有解码器的模型系列,其中最大的模型有 175B 个参数,并在 180B 个标记上进行训练。
BLOOM 大约在同一时间发布,该系列中最大的模型有 176B 个参数,并在 46 种语言和 13 种编程语言的 366B 个标记上进行训练。
编码器-解码器
BART 保持了原始的 Transformer 架构,但它通过 文本填充 污染修改了预训练目标,其中一些文本跨度被单个 mask
标记替换。解码器预测未损坏的标记(未来标记被遮挡)并使用编码器的隐藏状态来帮助它。
Pegasus 与 BART 类似,但 Pegasus 遮挡的是整个句子而不是文本跨度。除了遮挡语言模型之外,Pegasus 还通过间隙句子生成(GSG)进行预训练。GSG 目标遮挡对文档重要的整个句子,用 mask
标记替换。解码器必须从剩余的句子中生成输出。
T5 是一个更独特的模型,它将所有 NLP 任务都转换为使用特定前缀的文本到文本问题。例如,前缀 Summarize:
指示一个摘要任务。T5 通过监督(GLUE 和 SuperGLUE)训练和自监督训练(随机采样并丢弃 15% 的标记)进行预训练。
三、音频
编码器
Wav2Vec2 使用 Transformer 编码器从原始音频波形直接学习语音表示。它通过一个对比任务进行预训练,以从一系列错误表示中确定真实的语音表示。
HuBERT 与 Wav2Vec2 类似,但训练过程不同。目标标签是通过一个聚类步骤创建的,其中相似的音频段被分配到一个聚类,该聚类成为一个隐藏单元。隐藏单元被映射到一个嵌入以进行预测。
编码器-解码器
语音转文本 是一种针对自动语音识别 (ASR) 和语音翻译设计的语音模型。该模型接受从音频波形中提取的对数梅尔滤波器组特征,并经过预训练的自动回归过程来生成转录本或翻译。
Whisper 也是一个 ASR 模型,但与许多其他语音模型不同,它在大量✨标注✨音频转录数据上进行预训练,以实现零样本性能。数据集的大部分内容还包含非英语语言,这意味着 Whisper 也可以用于低资源语言。
在结构上,Whisper 与 Speech2Text 类似。音频信号被编码器转换为对数梅尔频谱图。解码器从编码器的隐藏状态和前面的标记中自回归地生成转录本。
四、多模态
编码器
VisualBERT 是在 BERT 之后不久发布的用于视觉-语言任务的跨模态模型。它将 BERT 和预训练的目标检测系统结合起来,将图像特征提取到视觉嵌入中,并将这些嵌入与文本嵌入一起传递给 BERT。
VisualBERT 根据未掩码的文本和视觉嵌入预测掩码文本,并且它还需要预测文本是否与图像对齐。当 ViT 发布时,ViLT 采用了 ViT 来构建其架构,因为这更易于获取图像嵌入。图像嵌入与文本嵌入共同处理。
从那里,ViLT 通过图像文本匹配、掩码语言建模和整词掩码进行预训练。
CLIP 采用不同的方法,并对 (image
,text
) 进行成对预测。图像编码器 (ViT) 和文本编码器 (Transformer) 在一个包含 4 亿个 (image
,text
) 对的数据集上进行联合训练,以最大化 (image
,text
) 对的图像和文本嵌入之间的相似性。
预训练后,您可以使用自然语言指示 CLIP 根据图像预测文本,反之亦然。OWL-ViT 通过将 CLIP 用作其骨干结构进行零样本目标检测来构建在 CLIP 之上。预训练后,添加了一个目标检测头来对 (class
,bounding box
) 对进行成对预测。
编码器-解码器
光学字符识别(OCR)是一个长期存在的文本识别任务,通常涉及多个组件来理解图像并生成文本。
TrOCR通过使用端到端的Transformer简化了这一过程。编码器是一个用于图像理解的ViT风格的模型,并将图像作为固定大小的补丁进行处理。解码器接受编码器的隐藏状态,并通过自回归的方式生成文本。
Donut是一个更通用的视觉文档理解模型,它不依赖于基于OCR的方法。它使用Swin Transformer作为编码器,多语言BART作为解码器。Donut通过根据图像和文本注释预测下一个单词来预训练以读取文本。
解码器根据提示生成一个标记序列。提示由每个下游任务的特殊标记表示。例如,文档解析有一个特殊的parsing
标记,它与编码器的隐藏状态结合,将文档解析为结构化输出格式(JSON)。
五、强化学习
解码器
决策和轨迹变换器将状态、动作和奖励视为一个序列建模问题。决策变换器根据到达状态、过去状态和动作生成一系列动作,以获得预期的未来回报。
对于最后 K 个时间步长,这三个模态都转换为标记嵌入,并通过类似 GPT 的模型处理以预测未来动作标记。
轨迹变换器也对状态、动作和奖励进行标记化,并用 GPT 架构进行处理。与专注于奖励条件的决策变换器不同,轨迹变换器使用 beam search 生成未来动作。
tokenizer总结
在这里,我们将更深入地了解分词。
Video : https://youtu.be/VFp38yj8h3A
正如我们在预处理教程中看到的,分词是将文本分割成单词或子词的过程,然后通过查找表将这些单词或子词转换为id。
将单词或子词转换为id很简单,因此在这个总结中,我们将重点放在将文本分割成单词或子词(即分词)上。
更具体地说,我们将探讨🤗 Transformers中使用的三种主要分词器类型:字节对编码(BPE),WordPiece和SentencePiece,并展示哪种分词器类型被哪个模型使用。
请注意,在每个模型页面上,您可以查看相关分词器的文档,以了解预训练模型 使用了哪种分词器类型。
例如,如果我们查看BertTokenizer,我们可以看到该模型使用WordPiece。
简介
将文本分割成更小的块是一个看似简单实则更困难的任务,并且有多种方法可以实现。
例如,让我们看看这个句子:“Don't you love 🤗 Transformers? We sure do.
”
将这段文本进行分词的一个简单方法是通过空格进行分割,这将得到:
["Don't", "you", "love", "🤗", "Transformers?", "We", "sure", "do."]
这是一个合理的第一步,但如果我们看看"Transformers?"
和"do."
这两个标记,我们会注意到标点符号附着在"Transformer"
和"do"
这两个词上,这是不理想的。
我们应该考虑标点符号,这样模型就不需要学习 每个单词及其后可能出现的每个标点符号的不同表示,这将使模型需要学习的表示数量爆炸性增长。
考虑标点符号后,对示例文本进行标记化会给出:
["Don", "'", "t", "you", "love", "🤗", "Transformers", "?", "We", "sure", "do", "."]
这样效果更好。
然而,分词处理单词 "Don't"
的方式是有弊端的。Don't
代表 "do not"
,因此最好分词为 ["Do", "n't"]
。
这就是事情开始变得复杂的地方,也是每个模型都有自己分词器类型的一部分原因。
根据我们应用的分词规则,相同的文本会生成不同的分词输出。
预训练模型 只有在您用 与分词训练数据相同的规则 进行分词的输入数据喂养时 才能正常工作。
sapaCy 和 Moses 是两个流行的基于规则的分词器。在我们的示例中应用它们,spaCy 和 Moses 会输出类似于以下内容:
["Do", "n't", "you", "love", "🤗", "Transformers", "?", "We", "sure", "do", "."]
正如所见,这里使用了空格和 标点符号标记化(punctuation tokenization) 以及基于规则的标记化。
空格和标点符号标记化以及基于规则的标记化都是词标记化的例子,它被松散地定义为将句子拆分成单词。
虽然这是将文本拆分成更小片段的最直观方式,但这种标记化方法对于大规模文本语料库可能会导致问题。
在这种情况下,空格和标点符号标记化通常会生成一个非常大的词汇表(所有唯一单词和标记的集合)。
例如,Transformer XL 使用空格和标点符号标记化,结果词汇量达到 267,735!
如此大的词汇量迫使模型在输入和输出层拥有一个巨大的嵌入矩阵,这会导致内存和时间复杂度的增加。
一般来说,transformers 模型很少拥有超过 50,000 的词汇量,尤其是如果它们只在一个语言上预训练的话。
所以如果简单的空格和标点符号标记化不令人满意,为什么不直接基于字符进行标记化呢?
尽管字符标记化非常简单 并且会大大减少内存和时间复杂度,但它会使模型学习有意义的输入表示变得困难得多。
例如,学习字母 "t"
的有意义 且不依赖上下文的表现,比学习单词 "today"
的有意义 且不依赖上下文的表现要困难得多。
因此,字符标记化通常会伴随着性能的损失。为了取长补短,transformers 模型使用了一种介于词级别和字符级别标记化之间的混合方法,称为 子词 标记化。
子词分词
子词分词算法基于这样一个原则:常用的词不应该被拆分成更小的子词,而罕见词 应该被分解成有意义的子词。
例如,“annoyingly”可能被认为是一个罕见词,可以分解为“annoying”和“ly”。
作为独立子词的“annoying”和“ly”都会更频繁地出现,同时“annoyingly” 的含义通过“annoying”和“ly”的组合 意义得以保留。
这在诸如土耳其语这样的粘着语中特别有用,在这种语言中,你可以通过连接子词形成(几乎)任意长度的复杂词。
子词分词允许模型在保持合理词汇量的同时,能够学习有意义的上下文无关表示。此外,子词分词使模型 能够通过将它们 分解成已知子词 来处理它以前从未见过的词。
例如,BertTokenizer 将 "I have a new GPU!"
分词如下:
from transformers import BertTokenizer
tokenizer = BertTokenizer.from_pretrained("google-bert/bert-base-uncased")
tokenizer.tokenize("I have a new GPU!")
["i", "have", "a", "new", "gp", "##u", "!"]
由于我们正在考虑无大小写模型,因此首先将句子转换为小写。
我们可以看到单词 ["i", "have", "a", "new"]
存在于分词器的词汇表中,但单词 "gpu"
不在其中。
因此,分词器将 "gpu"
分解为已知的子单词:["gp" 和 "##u"]
。
“##” 表示该标记的其余部分应附加到上一个标记上,无需空格(用于解码或分词的反向)。
作为另一个例子,XLNetTokenizer 将我们之前举例的文本分词如下:
from transformers import XLNetTokenizer
tokenizer = XLNetTokenizer.from_pretrained("xlnet/xlnet-base-cased")
tokenizer.tokenize("Don't you love 🤗 Transformers? We sure do.")
["▁Don", "'", "t", "▁you", "▁love", "▁", "🤗", "▁", "Transform", "ers", "?", "▁We", "▁sure", "▁do", "."]
我们将在查看 SentencePiece 时回到那些 "▁"
的含义。正如人们所看到的,罕见的单词 "Transformers"
被拆分成了更常见的子词 "Transform"
和 "ers"
。
现在让我们看看不同的子词分词算法是如何工作的。请注意,所有这些分词算法都依赖于某种形式的训练,这通常是在相应模型将要训练的语料库上完成的。
字节对编码(BPE)
字节对编码(BPE)是在 稀疏单词单位的神经机器翻译(Sennrich 等人,2015) 中引入的。
BPE 依赖于一个预标记器,该预标记器将训练数据分割成单词。
预标记化可以是像空格标记化一样简单,例如 GPT-2,RoBERTa。
更高级的预标记化包括基于规则的标记化,例如 XLM,FlauBERT 在大多数语言中使用 Moses,或者 GPT 使用 spaCy 和 ftfy,来计算训练语料库中每个单词的频率。
在预标记化之后,一组唯一的单词已经被创建,并且已经确定了每个单词在训练数据中出现的频率。
接下来,BPE 创建一个基本词汇表,包含在唯一单词集中出现的所有符号,并学习合并规则,从基本词汇表中的两个符号形成一个新的符号。
它一直这样做,直到词汇表达到了期望的词汇量大小。请注意,期望的词汇量大小是在训练标记化器之前定义的超参数。
作为一个例子,让我们假设在预标记化之后,已经确定了以下单词集及其频率:
("hug", 10), ("pug", 5), ("pun", 12), ("bun", 4), ("hugs", 5)
因此,基本词汇是 ["b", "g", "h", "n", "p", "s", "u"]
。将所有单词拆分为基本词汇的符号,我们得到:
("h" "u" "g", 10), ("p" "u" "g", 5), ("p" "u" "n", 12), ("b" "u" "n", 4), ("h" "u" "g" "s", 5)
BPE 首先统计每个可能的符号对的频率,并选择出现频率最高的符号对。
在上面的例子中,“h” 后面跟着 “u” 出现了 10 + 5 = 15 次(在 “hug” 的 10 次出现中,5 次在 “hugs” 的 5 次出现中)。
然而,出现频率最高的符号对是 “u” 后面跟着 “g”,总共出现了 10 + 5 + 5 = 20 次。
因此,分词器学习到的第一个合并规则是将所有跟着 “g” 符号的 “u” 符号组合在一起。
接下来,“ug” 被添加到词汇表中。然后,单词集合变为
("h" "ug", 10), ("p" "ug", 5), ("p" "u" "n", 12), ("b" "u" "n", 4), ("h" "ug" "s", 5)
BPE然后识别下一个最常见的符号对。它是 "u"
后跟 "n"
,出现了16次。"u"
和 "n"
被合并为 "un"
并添加到词汇表中。
下一个最常见的符号对是 "h"
后跟 "ug"
,出现了15次。同样地,这对符号被合并,"hug"
可以添加到词汇表中。
在这个阶段,词汇表是 ["b", "g", "h", "n", "p", "s", "u", "ug", "un", "hug"]
,而我们唯一的单词集合表示为
("hug", 10), ("p" "ug", 5), ("p" "un", 12), ("b" "un", 4), ("hug" "s", 5)
假设字节对编码训练会在此处停止,那么学到的合并规则将应用于新词(只要这些新词不包含基础词汇库中没有的符号)。
例如,单词 "bug"
将被标记化为 ["b", "ug"]
,但 "mug"
将被标记化为 ["<unk>", "ug"]
,因为符号 "m"
不在基础词汇库中。
一般来说,像 "m"
这样的单个字母不会用 <unk>
符号替换,因为训练数据通常至少包含每个字母的一个出现,但对于像表情符号这样的非常特殊的字符则可能发生这种情况。
如前所述,词汇量大小,即基础词汇量大小加上合并数量,是一个需要选择的超参数。例如 GPT 的词汇量大小为 40,478,因为它们有 478 个基础字符,并选择在 40,000 次合并后停止训练。
字节级BPE
如果例如考虑所有Unicode字符作为基础字符,包含所有可能的基础字符的基本词汇可以非常大。为了获得更好的基本词汇,GPT-2 使用字节作为基本词汇,这是一个聪明的技巧,可以强制基本词汇的大小为256,同时确保每个基础字符都包含在词汇表中。
通过一些额外的规则来处理标点符号,GPT2的标记器可以在不需要<unk>符号的情况下标记每篇文本。
GPT-2的词汇量为50,257,对应于256个字节基础标记,一个特殊的文本结束标记以及通过50,000次合并学习到的符号。
WordPiece
WordPiece 是用于 BERT、DistilBERT 和 Electra 的子词标记化算法。
该算法在 Japanese and Korean Voice Search (Schuster et al., 2012) 中进行了概述,与 BPE 非常相似。
WordPiece 首先将词汇表初始化为包含训练数据中出现的所有字符,并逐步学习一定数量的合并规则。
与 BPE 不同,WordPiece 不会选择最频繁的符号对,而是选择一旦添加到词汇表中就能最大化训练数据可能性的那个符号对。
那么这究竟意味着什么呢?参考之前的例子,最大化训练数据的可能性等同于找到这样一个符号对,其概率除以其第一个符号和第二个符号的概率之和,在所有符号对中是最大的。
例如,“u”后面跟着“g”只会被合并,如果“ug”的概率除以“u”、“g”的概率比其他任何符号对都要大。
直观地说,WordPiece 与 BPE 稍有不同,因为它评估合并两个符号所“失去”的东西,以确保它是“值得的”。
单词单元
单词单元是一种子词分词算法,由Kudo, 2018在《Subword Regularization: Improving Neural Network Translation Models with Multiple Subword Candidates》一文中提出。
与BPE或WordPiece不同,Unigram将基础词汇表初始化为大量符号,并通过逐渐减少每个符号来获得更小的词汇表。
基础词汇表可以对应于所有预分词的单词和最常见的子字符串。Unigram不是直接用于transformers中的任何模型,但它与SentencePiece结合使用。
在每次训练步骤中,Unigram算法根据当前词汇表和单语语言模型在训练数据上定义一个损失(通常定义为对数似然)。
然后,对于词汇表中的每个符号,算法计算如果该符号被从词汇表中移除,整体损失会增加多少。Unigram随后移除p(通常为10%或20%)的符号,这些符号的损失增加最低,即那些对训练数据整体损失影响最小的符号。
这个过程会重复进行,直到词汇表达到期望的大小。Unigram算法始终保留基础字符,以便任何单词都可以进行分词。
由于Unigram不基于合并规则(与BPE和WordPiece相反),算法在训练后有多种对新的文本进行分词的方式。例如,如果一个训练过的Unigram分词器显示的词汇表为:
["b", "g", "h", "n", "p", "s", "u", "ug", "un", "hug"],
"hugs"
可以被标记化为 ["hug", "s"]
、["h", "ug", "s"]
或 ["h", "u", "g", "s"]
。那么应该选择哪一个呢?
单词单元(Unigram)在保存词汇的同时还保存了训练语料库中每个标记的概率,这样在训练后可以计算出每种可能的标记化的概率。
在实践中,该算法简单地选择最可能的标记化,但也提供了根据它们的概率抽样可能的标记化的可能性。
这些概率是由标记化器在训练过程中遇到的损失来定义的。假设训练数据由单词 x1, …, xN 组成,并且一个单词 xi 的所有可能的标记化集合定义为 S(xi),那么整体损失定义为
L=−∑i=1Nlog(∑x∈S(xi)p(x))\mathcal{L} = -\sum_{i=1}^{N} \log \left ( \sum_{x \in S(x_{i})} p(x) \right )L=−i=1∑Nlogx∈S(xi)∑p(x)
SentencePiece
所有迄今为止描述的标记化算法都存在相同的问题:假设输入文本使用空格来分隔单词。
然而,并非所有语言都使用空格来分隔单词。
一个可能的解决方案是使用特定于语言的预标记器,例如 XLM 使用特定的中文、日语和泰语预标记器。
为了更普遍地解决这个问题,SentencePiece: A simple and language independent subword tokenizer and detokenizer for Neural Text Processing (Kudo et al., 2018) 将输入视为原始输入流,因此将空格包括在可使用的字符集中。然后它使用 BPE 或单语算法来构建适当的词汇表。
例如,XLNetTokenizer 使用 SentencePiece,这也是为什么在先前的示例中包含了 "▁"
字符的原因。使用 SentencePiece 进行解码非常简单,因为所有标记都可以简单地连接,并用 "▁"
替换为空格。
库中所有使用 SentencePiece 的 Transformer 模型都将其与单语结合使用。
使用 SentencePiece 的模型示例包括 ALBERT,XLNet,Marian 和 T5。
注意力机制
大多数 Transformer 模型都使用全注意力机制,这意味着注意力矩阵是方阵的。当处理长文本时,这可能会成为计算上的瓶颈。
Longformer 和 reformer 是尝试更加高效,并使用稀疏版本注意力矩阵来加速训练的模型。
LSH注意力
Reformer 使用LSH注意力。
在softmax(QKt)中,只有矩阵QKt中最大的元素(在softmax维度上)将提供有用的贡献。
所以对于Q中的每个查询q,我们只需要考虑与q接近的K中的键k。
使用哈希函数来确定q和k是否接近。注意力掩码被修改以掩码当前标记(除了第一个位置),因为它将给出相等的查询和键(因此非常相似)。
由于哈希可以有点随机,实践中使用多个哈希函数(由n_rounds参数确定)并将它们平均在一起。
本地注意力
Longformer 使用本地注意力:通常,局部上下文(例如,左右两侧的两个标记是什么?)就足够对给定标记采取行动。
此外,通过堆叠具有小窗口的注意力层,最后一层将具有比窗口中的标记更广的接受场,使它们能够构建整个句子的表示。
一些预选输入标记也给予全局注意力:对于这些少数标记,注意力矩阵可以访问所有标记,这个过程是对称的:所有其他标记都可以访问这些特定的标记(除了它们局部窗口中的标记)。
这在本篇论文的第2d图中有展示,以下是一个样本注意力掩码:
使用那些参数更少的注意力矩阵,这使得模型可以拥有更长的输入序列。
其他技巧
轴向位置编码
Reformer 使用轴向位置编码:在传统的 Transformer 模型中,位置编码 E 是一个尺寸为 lll×ddd 的矩阵,其中 lll 是序列长度,ddd 是隐藏状态的维度。
如果你有非常长的文本,这个矩阵可以非常大,并且在 GPU 上占用太多的空间。为了缓解这个问题,轴向位置编码通过将大的矩阵 E 分解为两个较小的矩阵 E1 和 E2 来实现,它们的尺寸分别为 l1×d1l_{1} \times d_{1}l1×d1 和 l2×d2l_{2} \times d_{2}l2×d2,使得 l1×l2=ll_{1} \times l_{2} = ll1×l2=l 且 d1+d2=dd_{1} + d_{2} = dd1+d2=d(长度乘积,这最终会变得非常小)。
E 中时间步长 jjj 的嵌入通过连接 E1 中时间步长 j%l1j \% l1j%l1 的嵌入和 E2 中 j//l1j // l1j//l1 的嵌入来获得。
填充和截断
批处理输入通常长度不同,因此不能转换为固定大小的张量。填充和截断是处理此问题的策略,用于从不同长度的批次创建矩形张量。
填充通过添加一个特殊的填充标记来确保较短的序列长度与批次中最长的序列或模型可接受的最大长度相同。截断则相反,通过截断较长的序列。
在大多数情况下,将批处理填充到最长序列的长度,截断到模型可接受的最大长度效果相当好。
然而,API支持更多策略,如果您需要的话。您需要了解的三个参数是:padding
、truncation
和max_length
。
padding
参数控制填充。它可以是布尔值或字符串:
True
或'longest'
:填充到批次中最长的序列(如果只提供一个序列,则不应用填充)。'max_length'
:填充到max_length
参数指定的长度或未提供max_length
时模型可接受的最大长度。如果只提供一个序列,仍然会应用填充。False
或'do_not_pad'
:不应用填充。这是默认行为。
truncation
参数控制截断。它可以是布尔值或字符串:
True
或'longest_first'
:截断到max_length
参数指定的最大长度或未提供max_length
时模型可接受的最大长度。
这将逐个标记截断,从对中最长的序列中移除一个标记,直到达到正确的长度。'only_second'
:截断到max_length
参数指定的最大长度或未提供max_length
时模型可接受的最大长度。
如果提供了一个序列对(或序列对的批次),这将仅截断第二个序列。'only_first'
:截断到max_length
参数指定的最大长度或未提供max_length
时模型可接受的最大长度。
如果提供了一个序列对(或序列对的批次),这将仅截断第一个序列。False
或'do_not_truncate'
:不应用截断。这是默认行为。
max_length
参数控制填充和截断的长度。它可以是整数或None
,在这种情况下,它将默认为模型可接受的最大长度。
如果模型没有特定的最大输入长度,则截断或填充到max_length
被禁用。
以下表格总结了设置填充和截断的推荐方法。如果您在以下任何示例中使用输入序列对,您可以将truncation=True
替换为在['only_first', 'only_second', 'longest_first']
中选择的STRATEGY
,即truncation='only_second'
或truncation='longest_first'
,以控制如何截断对中的两个序列,如前所述。
截断 | 填充 | 指令 |
---|---|---|
无截断 | 无填充 | tokenizer(batch_sentences) |
填充到批次中最长序列 | tokenizer(batch_sentences, padding=True) 或 tokenizer(batch_sentences, padding='longest') | |
填充到最大模型输入长度 | tokenizer(batch_sentences, padding='max_length') | |
填充到特定长度 | 不可能 | |
填充到值的倍数 | tokenizer(batch_sentences, padding=True, pad_to_multiple_of=8) | |
截断到最大模型输入长度 | 无填充 | tokenizer(batch_sentences, truncation=True) 或 tokenizer(batch_sentences, truncation=STRATEGY) |
填充到批次中最长序列 | tokenizer(batch_sentences, padding=True, truncation=True) 或 tokenizer(batch_sentences, padding=True, truncation=STRATEGY) | |
填充到最大模型输入长度 | tokenizer(batch_sentences, padding='max_length', truncation=True) 或 tokenizer(batch_sentences, padding='max_length', truncation=STRATEGY) | |
填充到特定长度 | 不可能 | |
截断到特定长度 | 无填充 | tokenizer(batch_sentences, truncation=True, max_length=42) 或 tokenizer(batch_sentences, truncation=STRATEGY, max_length=42) |
填充到批次中最长序列 | tokenizer(batch_sentences, padding=True, truncation=True, max_length=42) 或 tokenizer(batch_sentences, padding=True, truncation=STRATEGY, max_length=42) | |
填充到最大模型输入长度 | 不可能 | |
填充到特定长度 | tokenizer(batch_sentences, padding='max_length', truncation=True, max_length=42) 或 tokenizer(batch_sentences, padding='max_length', truncation=STRATEGY, max_length=42) |
BERTology
研究大规模Transformers (如BERT,有些人称之为“BERTology”)内部工作原理的领域正在不断增长。这个领域的几个良好例子是:
- BERT Rediscovers the Classical NLP Pipeline by Ian Tenney, Dipanjan Das, Ellie Pavlick:
https://arxiv.org/abs/1905.05950 - Are Sixteen Heads Really Better than One? by Paul Michel, Omer Levy, Graham Neubig: https://arxiv.org/abs/1905.10650
- What Does BERT Look At? An Analysis of BERT’s Attention by Kevin Clark, Urvashi Khandelwal, Omer Levy, Christopher D.
Manning: https://arxiv.org/abs/1906.04341 - CAT-probing: A Metric-based Approach to Interpret How Pre-trained Models for Programming Language Attend Code Structure: https://arxiv.org/abs/2210.04633
为了帮助这个新领域的发展,我们在 BERT/GPT/GPT-2 模型中加入了一些额外的功能,以帮助人们访问内部表示,主要借鉴了 Paul Michel 的杰出工作 (https://arxiv.org/abs/1905.10650):
- 访问 BERT/GPT/GPT-2 的所有隐藏状态,
- 访问 BERT/GPT/GPT-2 每个头的所有注意力权重,
- 获取头部的输出值和梯度,以便计算头部重要性得分并修剪头部,如 https://arxiv.org/abs/1905.10650 中所述。
为了帮助您理解和使用这些功能,我们添加了一个特定的示例脚本:bertology.py,它提取信息并修剪在 GLUE 上预训练的模型。
固定长度模型的困惑度
困惑度(PPL)是评估语言模型最常用的指标之一。在深入了解之前,我们应该注意,该指标专门适用于经典语言模型(有时也称为自回归或因果语言模型),对于像BERT这样的掩码语言模型定义并不明确(参见模型概述)。
困惑度定义为序列的指数化平均负对数似然。如果我们有一个标记序列X=(x0,x1,…,xt)X = (x_0, x_1, \dots, x_t)X=(x0,x1,…,xt),那么XXX的困惑度是,
PPL(X)=exp{−1t∑itlogpθ(xi∣x<i)}\text{PPL}(X) = \exp \left\{ {-\frac{1}{t}\sum_i^t \log p_\theta (x_i|x_{<i}) } \right\}PPL(X)=exp{−t1i∑tlogpθ(xi∣x<i)}
其中logpθ(xi∣x<i)\log p_\theta (x_i|x_{<i})logpθ(xi∣x<i)是根据我们的模型在先前的标记x<ix_{<i}x<i的条件下第i个标记的对数似然。直观上,它可以被视为对模型在语料库中指定标记集合中均匀预测能力的评估。重要的是,这意味着标记化过程直接影响了模型的困惑度,这在比较不同模型时始终应该考虑。
PPL(X)=exp{−1t∑itlogpθ(xi∣x<i)}\text{PPL}(X) = \exp \left\{ {-\frac{1}{t}\sum_i^t \log p_\theta (x_i|x_{<i}) } \right\}PPL(X)=exp{−t1i∑tlogpθ(xi∣x<i)}
其中logpθ(xi∣x<i)\log p_\theta (x_i|x_{<i})logpθ(xi∣x<i)是根据我们的模型在先前的标记x<ix_{<i}x<i的条件下第i个标记的对数似然。直观上,它可以被视为对模型在语料库中指定标记集合中均匀预测能力的评估。重要的是,这意味着标记化过程直接影响了模型的困惑度,这在比较不同模型时始终应该考虑。
这还等同于数据和模型预测之间交叉熵的指数化。关于困惑度及其与每字符比特(BPC)和数据压缩的关系的更多直观理解,请查看这篇关于The Gradient的出色博客文章。
使用固定长度模型计算PPL
如果我们不受模型上下文大小的限制,我们就会通过自回归分解序列并在每一步条件化整个前导子序列来评估模型的困惑度,如下所示。
然而,当我们使用近似模型时,通常对模型可以处理的标记数有所限制。例如,GPT-2 的最大版本具有固定的1024个标记长度,因此当ttt 大于1024时,我们无法直接计算pθ(xt∣x<t)p_\theta(x_t|x_{<t})pθ(xt∣x<t)。
相反,序列通常被分成与模型最大输入大小相等的子序列。如果一个模型的最大输入大小是k,那么我们就会通过只条件化它前面的k−1k-1k−1个标记而不是整个上下文来近似标记xtx_txt的可能性。在评估序列的模型困惑度时,一个诱人但次优的方法是将序列分成不相连的块,并独立地将每个段的分解对数似然相加。
由于每个段的困惑度可以在一次前向传递中计算,所以这很容易计算,但作为完全分解困惑度的较差近似,通常会产生更高的(更差的)PPL,因为模型在大多数预测步骤中将具有更少的上下文。
相反,固定长度模型的PPL应该通过滑动窗口策略来评估。这涉及到反复滑动上下文窗口,以便在每次做出预测时模型有更多的上下文。
这更接近序列概率的真实分解,通常会产生更佳的分数。缺点是它需要对语料库中的每个标记执行单独的前向传递。一个好的实用折衷方案是使用步长滑动窗口,通过更大的步长而不是每次移动一个标记来移动上下文。这允许计算以更快的速度进行,同时仍然在每一步给模型提供大量的上下文来做出预测。
示例:使用 🤗 Transformers 中的 GPT-2 计算困惑度
让我们用 GPT-2 来演示这个过程。
from transformers import GPT2LMHeadModel, GPT2TokenizerFast
device = "cuda"
model_id = "openai-community/gpt2-large"
model = GPT2LMHeadModel.from_pretrained(model_id).to(device)
tokenizer = GPT2TokenizerFast.from_pretrained(model_id)
我们将加载 WikiText-2 数据集,并使用几种不同的滑动窗口策略来评估困惑度。由于这个数据集很小,我们只是对这个集合进行一次正向传递,因此我们可以直接将整个数据集加载和编码到内存中。
from datasets import load_dataset
test = load_dataset("wikitext", "wikitext-2-raw-v1", split="test")
encodings = tokenizer("\n\n".join(test["text"]), return_tensors="pt")
使用 🤗 Transformers,我们可以简单地将 input_ids
作为 labels
传递给我们的模型,每个标记的平均负对数似然被作为损失返回。然而,使用我们的滑动窗口方法,我们传递给模型的所有标记在每个迭代中都会重叠。我们不想将我们仅将其作为上下文处理的标记的对数似然包括在我们的损失中,因此我们可以将这些目标设置为 -100
,这样它们就会被忽略。以下是一个示例,说明我们如何使用 512
步长来完成此操作。这意味着当计算任何单个标记的条件似然时,模型将至少有 512 个标记作为上下文(前提是有 512 个 preceding 标记可用于条件化)。
import torch
from tqdm import tqdm
max_length = model.config.n_positions
stride = 512
seq_len = encodings.input_ids.size(1)
nlls = []
prev_end_loc = 0
for begin_loc in tqdm(range(0, seq_len, stride)):
end_loc = min(begin_loc + max_length, seq_len)
trg_len = end_loc - prev_end_loc # may be different from stride on last loop
input_ids = encodings.input_ids[:, begin_loc:end_loc].to(device)
target_ids = input_ids.clone()
target_ids[:, :-trg_len] = -100
with torch.no_grad():
outputs = model(input_ids, labels=target_ids)
# loss is calculated using CrossEntropyLoss which averages over valid labels
# N.B. the model only calculates loss over trg_len - 1 labels, because it internally shifts the labels
# to the left by 1.
neg_log_likelihood = outputs.loss
nlls.append(neg_log_likelihood)
prev_end_loc = end_loc
if end_loc == seq_len:
break
ppl = torch.exp(torch.stack(nlls).mean())
运行步长等于最大输入长度的程序相当于我们上面讨论过的次优、非滑动窗口策略。步长越小,模型在做出每个预测时拥有的上下文信息就越多,通常报告的困惑度会更好。
当我们使用 stride = 1024
运行上述程序,即没有重叠,得到的困惑度是 19.44
,这与 GPT-2 论文中报告的 19.93
大致相同。通过使用 stride = 512
并因此采用我们的步长窗口策略,这个值下降到 16.45
。这不仅是一个更有利的分数,而且其计算方式更接近序列似然的真实自回归分解。
使用管道为网络服务器
创建推理引擎是一个复杂的话题,"最佳"解决方案很可能会取决于你的问题空间。你是在CPU上还是GPU上?你想要最低的延迟、最高的吞吐量、支持许多模型,还是仅仅高度优化1个特定的模型?
处理这个话题有很多方法,所以我们将要介绍的是一个好的默认选项,这可能会不是对你来说最优化解决方案。关键是要理解,我们可以使用迭代器,就像你会在数据集上做的那样,因为网络服务器基本上是一个等待请求并将它们按到达顺序处理的系统。
通常,网络服务器是多路复用(多线程、异步等)的,以并发处理各种请求。另一方面(以及底层的模型),管道(特别是)并不非常适合并行处理;它们占用大量的RAM,所以在它们运行时最好分配给他们所有可用的资源,或者这是一个计算密集型的工作。
我们将通过让网络服务器处理接收和发送请求的轻量级负载,并让一个单独的线程处理实际工作来解决此问题。这个例子将使用starlette
。实际的框架并不是很重要,但如果你使用的是另一个框架,你可能需要调整或更改代码以达到相同的效果。
创建server.py
:
from starlette.applications import Starlette
from starlette.responses import JSONResponse
from starlette.routing import Route
from transformers import pipeline
import asyncio
async def homepage(request):
payload = await request.body()
string = payload.decode("utf-8")
response_q = asyncio.Queue()
await request.app.model_queue.put((string, response_q))
output = await response_q.get()
return JSONResponse(output)
async def server_loop(q):
pipe = pipeline(model="google-bert/bert-base-uncased")
while True:
(string, response_q) = await q.get()
out = pipe(string)
await response_q.put(out)
app = Starlette(
routes=[
Route("/", homepage, methods=["POST"]),
],
)
@app.on_event("startup")
async def startup_event():
q = asyncio.Queue()
app.model_queue = q
asyncio.create_task(server_loop(q))
现在你可以使用以下命令启动它:
uvicorn server:app
并且你可以查询它:
curl -X POST -d "test [MASK]" http://localhost:8000/
`[{"score":0.7742936015129089,"token":1012,"token_str":".","sequence":"test."},...]`
然后,你就明白了,现在你有了创建一个 webserver 的好方法!
真正重要的是,我们只加载模型 一次,这样在 webserver 上就没有模型的副本。这种方式,就不会使用不必要的 RAM。
然后,排队机制允许你做一些很酷的事情,比如在推理之前先积累一些项目,使用动态批处理:
下面的代码示例故意写成伪代码,以便于阅读。
在运行之前,请务必检查它是否适合你的系统资源!
(string, rq) = await q.get()
strings = []
queues = []
while True:
try:
(string, rq) = await asyncio.wait_for(q.get(), timeout=0.001) # 1ms
except asyncio.exceptions.TimeoutError:
break
strings.append(string)
queues.append(rq)
strings
outs = pipe(strings, batch_size=len(strings))
for rq, out in zip(queues, outs):
await rq.put(out)
再次强调,提出的代码优化是为了可读性,而不是为了成为最好的代码。
首先,没有批量大小的限制,这通常不是一个好主意。接下来,每次从队列中获取时都会重置超时时间,这意味着您可以在运行推理之前等待超过1毫秒(延迟第一次请求这么长时间)。
更好的做法是设置一个单独的1毫秒截止时间。
这将始终等待1毫秒,即使队列是空的,这可能不是最佳选择,因为您可能希望在队列中没有内容时开始进行推理。
但也许如果批处理对于您的用例至关重要,这样做是有意义的。
再次强调,实际上没有一种最佳解决方案。
几件你可能想考虑的事情
错误检查
在生产环境中可能会出现很多问题:内存不足、空间不足、加载模型可能失败、查询可能错误、查询可能正确但仍然因为模型配置错误而无法运行,等等。
通常,如果服务器将错误输出给用户,那么添加很多 try..except
语句来显示这些错误是一个好主意。但请记住,根据您的安全环境,透露所有这些错误可能也是一个安全风险。
断路器
Web服务器在实施断路器时通常会看起来更好。这意味着当它们过载时,会返回适当的错误,而不是无限期地等待查询。在等待很长时间后返回503错误,或者长时间后返回504错误。
在所提出的代码中,由于存在单个队列,实现断路器相对简单。查看队列大小是在你的Web服务器在负载下失败之前开始返回错误的基本方法。
阻塞主线程
目前 PyTorch 并不支持异步操作,计算过程将会阻塞主线程。这意味着如果 PyTorch 被迫在其自己的线程/进程中运行会更好。这里没有这样做是因为代码复杂度较高(主要是因为线程、异步和队列不兼容)。但最终它做的是同一件事情。
如果单个项目的推理时间较长(> 1 秒),这将非常重要,因为在这种情况下,每次推理中的每个查询在接收到错误之前都需要等待 1 秒。
动态批处理
总的来说,批处理并不一定是比一次只传递一个项目更好的选择(更多信息请参阅批处理详情)。但是,在正确的设置下,它可以非常有效。在API中,默认情况下没有动态批处理(过多的机会会导致减速)。但是,对于BLOOM推理——这是一个非常大的模型——动态批处理对于为每个人提供良好的体验是必需的。
模型训练解剖学
为了了解可以应用以提高模型训练速度和内存利用率的性能优化技术,了解GPU在训练过程中的使用情况以及计算强度根据操作而变化是很有帮助的。
让我们从一个激励性的例子开始,探讨GPU的使用情况和模型训练过程。为了演示,我们需要安装一些库:
pip install transformers datasets accelerate nvidia-ml-py3
nvidia-ml-py3
库允许我们从Python中监控模型的内存使用情况。您可能熟悉终端中的 nvidia-smi
命令,该库允许直接访问Python中的相同信息。
然后,我们创建一些虚拟数据:100到30000之间的随机令牌ID和分类器的二进制标签。
总共,我们得到512个序列,每个序列的长度为512,并将它们存储在[数据集]中(https://huggingface.co/docs/datasets/v3.1.0/en/package_reference/main_classes#datasets.Dataset)使用PyTorch格式。
import numpy as np
from datasets import Dataset
seq_len, dataset_size = 512, 512
dummy_data = {
... "input_ids": np.random.randint(100, 30000, (dataset_size, seq_len)),
... "labels": np.random.randint(0, 2, (dataset_size)),
... }
ds = Dataset.from_dict(dummy_data)
ds.set_format("pt")
为了打印GPU利用率和与Trainer相关的训练运行的摘要统计信息,我们定义了两个辅助函数:
from pynvml import *
def print_gpu_utilization():
... nvmlInit()
... handle = nvmlDeviceGetHandleByIndex(0)
... info = nvmlDeviceGetMemoryInfo(handle)
... print(f"GPU memory occupied: {info.used//1024**2} MB.")
def print_summary(result):
... print(f"Time: {result.metrics['train_runtime']:.2f}")
... print(f"Samples/second: {result.metrics['train_samples_per_second']:.2f}")
... print_gpu_utilization()
让我们验证一下我们是否以空闲GPU内存开始:
print_gpu_utilization()
GPU memory occupied: 0 MB.
看起来很好:在我们加载任何模型之前,GPU 内存没有被占用,正如我们所预期的那样。如果你的机器不是这样,请确保停止所有正在使用 GPU 内存的进程。然而,并非所有空闲的 GPU 内存都可以被用户使用。当一个模型被加载到 GPU 上时,内核也会被加载,这可能会占用 1-2GB 的内存。为了查看具体占用多少,我们将一个微小的张量加载到 GPU 中,这将触发内核的加载。
import torch
torch.ones((1, 1)).to("cuda")
print_gpu_utilization()
GPU memory occupied: 1343 MB.
我们注意到内核本身就占用了1.3GB的GPU内存。现在让我们看看模型占用了多少空间。
加载模型
首先,我们加载 google-bert/bert-large-uncased
模型。我们将模型权重直接加载到 GPU 上,这样我们可以检查仅权重就占用多少空间。
from transformers import AutoModelForSequenceClassification
model = AutoModelForSequenceClassification.from_pretrained("google-bert/bert-large-uncased").to("cuda")
print_gpu_utilization()
GPU memory occupied: 2631 MB.
我们可以看到,模型权重本身就占据了1.3 GB的GPU内存。这个确切数字取决于你使用的特定GPU。注意,在较新的GPU上,模型有时会占用更多空间,因为权重以优化的方式加载,这加快了模型的使用速度。现在我们也可以快速检查我们是否得到与nvidia-smi
CLI相同的结果:
nvidia-smi
>>>>bf427b31 (-)
Tue Jan 11 08:58:05 2022
+-----------------------------------------------------------------------------+
| NVIDIA-SMI 460.91.03 Driver Version: 460.91.03 CUDA Version: 11.2 |
|-------------------------------+----------------------+----------------------+
| GPU Name Persistence-M| Bus-Id Disp.A | Volatile Uncorr. ECC |
| Fan Temp Perf Pwr:Usage/Cap| Memory-Usage | GPU-Util Compute M. |
| | | MIG M. |
|===============================+======================+======================|
| 0 Tesla V100-SXM2... On | 00000000:00:04.0 Off | 0 |
| N/A 37C P0 39W / 300W | 2631MiB / 16160MiB | 0% Default |
| | | N/A |
+-------------------------------+----------------------+----------------------+
+-----------------------------------------------------------------------------+
| Processes: |
| GPU GI CI PID Type Process name GPU Memory |
| ID ID Usage |
|=============================================================================|
| 0 N/A N/A 3721 C ...nvs/codeparrot/bin/python 2629MiB |
+-----------------------------------------------------------------------------+
我们得到了之前相同的数字,你也可以看到我们正在使用一个带有16GB内存的V100 GPU。因此,现在我们可以开始训练模型并查看GPU内存消耗如何变化。首先,我们设置了一些标准的训练参数:
default_args = {
"output_dir": "tmp",
"eval_strategy": "steps",
"num_train_epochs": 1,
"log_level": "error",
"report_to": "none",
}
如果你计划运行多个实验,为了在实验之间正确清理内存,请在实验之间重启 Python 内核。
基础训练的内存利用率
让我们使用Trainer来训练模型,不使用任何GPU性能优化技术,并且批量大小为4:
from transformers import TrainingArguments, Trainer, logging
logging.set_verbosity_error()
training_args = TrainingArguments(per_device_train_batch_size=4, **default_args)
trainer = Trainer(model=model, args=training_args, train_dataset=ds)
result = trainer.train()
print_summary(result)
Time: 57.82
Samples/second: 8.86
GPU memory occupied: 14949 MB.
我们注意到,即使是相对较小的批量大小几乎也填满了我们GPU的全部内存。然而,更大的批量大小通常会导致模型收敛更快或最终性能更好。因此,理想情况下,我们希望调整批量大小以满足模型的需求,而不是GPU的限制。有趣的是,我们使用的内存比模型的大小要多得多。为了更好地理解为什么会这样,让我们看看模型的操作和内存需求。
模型操作的解剖
Transformers架构包括以下按计算强度分组的三组主要操作。
1、张量收缩
线性层和多头注意力的组成部分都进行批处理矩阵-矩阵乘法。这些操作是训练transformer最计算密集的部分。
2、统计归一化
Softmax和层归一化比张量收缩的计算强度低,涉及一个或多个归约操作,然后将结果通过映射应用。
3、逐元素算子
这些是剩余的操作:偏差、dropout、激活和残差连接。这些是最不计算密集的操作。
当分析性能瓶颈时,了解这些知识可能会有所帮助。
本总结来源于数据移动即所需:优化Transformers的案例分析 2020
模型的内存结构
我们已经看到,训练模型所需的内存比仅仅将模型放在GPU上要多得多。这是因为训练过程中有许多组件会使用GPU内存。GPU内存中的组件包括以下内容:
1、模型权重
2、优化器状态
3、梯度
4、为梯度计算保存的前向激活
5、临时缓冲区
6、功能特定内存
在一个使用AdamW进行混合精度训练的典型模型中,每个模型参数需要18字节,加上激活内存。对于推理,没有优化器状态和梯度,所以我们可以减去这些。因此,我们最终得到混合精度推理每个模型参数6字节,加上激活内存。
让我们来看看详细情况。
模型权重:
- 4字节 * fp32训练的参数数量
- 6字节 * 混合精度训练的参数数量(在内存中保持一个fp32模型和一个fp16模型)
优化器状态:
- 8字节 * 正常AdamW的参数数量(保持2个状态)
- 2字节 * 8位AdamW优化器,如bitsandbytes
- 4字节 * 如SGD带动量优化器等参数(保持仅1个状态)
梯度
- 4字节 * fp32或混合精度训练的参数数量(梯度始终以fp32形式保留)
前向激活
- 大小取决于许多因素,关键因素包括序列长度、隐藏大小和批大小。
这里包括前向和反向函数传递和返回的输入和输出,以及为梯度计算保存的前向激活。
临时内存
此外,还有各种临时变量,一旦计算完成就会释放,但在此期间可能需要额外的内存,并可能导致OOM。因此,在编码时,战略性地思考这样的临时变量至关重要,有时还需要在不再需要时显式释放这些变量。
功能特定内存
然后,你的软件可能有特殊的内存需求。例如,在生成文本时使用beam search,软件需要维护多个输入和输出的副本。
forward
与backward
执行速度
对于卷积和线性层,反向操作与正向操作相比有2倍的计算量(flops),这通常意味着慢2倍(有时更多,因为反向操作的大小通常更难以处理)。激活通常受带宽限制,通常在反向操作中,激活需要读取比正向操作更多的数据(例如,激活正向读取一次,写入一次,激活反向读取两次,gradOutput和正向操作的输出,并写入一次,gradInput)。
正如你所看到的,我们可能可以在几个地方节省GPU内存或加快操作速度。现在你已经了解了影响GPU利用率和计算速度的因素,请参考单GPU上高效训练的方法和工具文档页面,了解性能优化技术。
优化LLMs以提高速度和内存使用
https://huggingface.co/docs/transformers/llm_tutorial_optimization
大型语言模型(LLMs)如 GPT3/4、Falcon 和 Llama 在处理以人为中心的任务方面能力迅速提升,已成为现代知识型产业中的关键工具。然而,将这些模型部署到实际任务中仍然具有挑战性:
- 为了展示接近人类的文本理解和生成能力,LLM 目前需要由数十亿个参数组成(参见 Kaplan 等人,Wei 等人)。这随之增加了推理过程中的内存需求。
- 在许多实际任务中,LLM 需要提供大量的上下文信息。这要求模型在推理过程中能够处理非常长的输入序列。
这些挑战的核心在于增强大型语言模型(LLM)的计算和内存能力,尤其是在处理大量输入序列时。
在本指南中,我们将介绍高效部署LLM的有效技术:
1、**降低精度:**研究表明,在降低数值精度下运行,即8位和4位,可以在不显著降低模型性能的情况下实现计算优势。
2、**Flash Attention:**Flash Attention是注意力算法的一种变体,它不仅提供了一种更内存高效的方法,而且还由于优化的GPU内存利用而实现了更高的效率。
3、**架构创新:**考虑到LLM在推理时总是以相同的方式进行部署,即在长输入上下文中进行自回归文本生成,已经提出了专门的模式架构,允许更高效的推理。在此方面最重要的进步是Alibi、Rotary embeddings、多查询注意力(MQA)和分组查询注意力(GQA)。
在本指南中,我们将从张量的角度分析自回归生成。我们深入探讨采用降低精度的好处和坏处,全面探索最新的注意力算法,并讨论改进的LLM架构。在这样做的同时,我们将提供实际示例,展示每个特性改进的应用。
1. 低精度
LLM 的内存需求可以通过将其视为一系列权重矩阵和向量,将文本输入视为向量序列来最好地理解。在以下内容中,定义 权重 将被用来表示所有模型权重矩阵和向量。
在撰写本指南时,LLM 至少包含数十亿个参数。因此,每个参数都是由一个十进制数字组成的,例如 4.5689
,这些数字通常以 float32、bfloat16 或 float16 格式存储。这使得我们能够轻松计算将 LLM 加载到内存中的内存需求:
加载一个拥有 X 亿参数的模型权重大约需要 4 X GB 的 VRAM 来支持 float32 精度*
如今,模型很少在完全的float32精度下进行训练,通常是在bfloat16精度下,或者更少的情况下在float16精度下进行。因此,经验法则变为:
加载具有X亿参数的模型权重大约需要2 X GB的VRAM,以bfloat16/float16精度为准*
对于较短的文本输入(少于1024个标记),推理所需的内存主要由加载权重的内存需求决定。因此,目前让我们假设推理所需的内存与将模型加载到GPU VRAM中的内存需求相等。
以下是一些大致说明加载模型所需的VRAM量的例子:
31
- GPT3 需要 2 * 175 GB = 350 GB VRAM
- Bloom 需要 2 * 176 GB = 352 GB VRAM
- Llama-2-70b 需要 2 * 70 GB = 140 GB VRAM
- Falcon-40b 需要 2 * 40 GB = 80 GB VRAM
- MPT-30b 需要 2 * 30 GB = 60 GB VRAM
- bigcode/starcoder 需要 2 * 15.5 = 31 GB VRAM
截至撰写本文时,市场上最大的GPU芯片是A100 & H100,提供80GB的VRAM。大多数列出的模型仅为了加载就需要超过80GB,因此必然需要tensor parallelism和/或pipeline parallelism。
🤗 Transformers默认不支持tensor parallelism,因为它要求模型架构以特定方式编写。如果您有兴趣以tensor-parallelism友好的方式编写模型,请随意查看the text-generation-inference library。
简单的pipeline parallelism默认支持。为此,只需使用device="auto"
加载模型,它将自动将不同的层放置在可用的GPU上,如此处所述。请注意,虽然这种方法非常有效,但它没有解决GPU空闲的问题。为此需要更高级的pipeline parallelism,如此处所述。
如果您可以访问一个8 x 80GB A100节点,您可以按照以下方式加载BLOOM:
pip install transformers accelerate bitsandbytes optimum
from transformers import AutoModelForCausalLM
model = AutoModelForCausalLM.from_pretrained("bigscience/bloom", device_map="auto", pad_token_id=0)
通过使用 device_map="auto"
,注意力层将均匀分布到所有可用的GPU上。
在本指南中,我们将使用 bigcode/octocoder,因为它可以在单个40 GB A100 GPU设备芯片上运行。请注意,我们将应用的所有内存和速度优化都同样适用于需要模型或张量并行的模型。
由于模型以bfloat16精度加载,根据我们上面的经验法则,我们预计使用 bigcode/octocoder
运行推理的内存需求约为31 GB VRAM。让我们试试吧。
我们首先加载模型和分词器,然后将两者传递给Transformers的pipeline对象。
from transformers import AutoModelForCausalLM, AutoTokenizer, pipeline
import torch
model = AutoModelForCausalLM.from_pretrained("bigcode/octocoder", torch_dtype=torch.bfloat16, device_map="auto", pad_token_id=0)
tokenizer = AutoTokenizer.from_pretrained("bigcode/octocoder")
pipe = pipeline("text-generation", model=model, tokenizer=tokenizer)
prompt = "Question: Please write a function in Python that transforms bytes to Giga bytes.\n\nAnswer:"
result = pipe(prompt, max_new_tokens=60)[0]["generated_text"][len(prompt):]
result
输出:
Here is a Python function that transforms bytes to Giga bytes:\n\n```python\ndef bytes_to_giga_bytes(bytes):\n return bytes / 1024 / 1024 / 1024\n```\n\nThis function takes a single
好的,我们现在可以直接使用结果将字节转换为千兆字节。
def bytes_to_giga_bytes(bytes):
return bytes / 1024 / 1024 / 1024
让我们调用 torch.cuda.max_memory_allocated
来测量峰值 GPU 内存分配。
bytes_to_giga_bytes(torch.cuda.max_memory_allocated())
输出:
29.0260648727417
足够接近我们的估算!我们可以看到这个数字并不完全正确,因为从字节到千字节需要乘以1024而不是1000。因此,这个估算公式也可以理解为“最多X GB”的计算。注意,如果我们尝试以完整的float32精度运行模型,将需要惊人的64 GB VRAM。
几乎所有模型现在都是用bfloat16训练的,如果你的GPU支持bfloat16(链接),就没有必要用完整的float32精度来运行模型。Float32的精度不会比训练模型时使用的精度给出更好的推理结果。
如果您不确定模型权重在 Hub 上的存储格式,您始终可以查看检查点的配置文件中的"torch_dtype"
部分,例如这里。建议在用from_pretrained(..., torch_dtype=...)
加载模型时,将模型设置为与配置文件中写入的相同精度类型,除非原始类型是 float32,在这种情况下,可以使用float16
或bfloat16
进行推理。
让我们定义一个 flush(...)
函数来释放所有分配的内存,这样我们就可以准确地测量峰值分配的 GPU 内存。
del pipe
del model
import gc
import torch
def flush():
gc.collect()
torch.cuda.empty_cache()
torch.cuda.reset_peak_memory_stats()
让我们现在开始下一个实验。
flush()
从Accelerate库中,您还可以使用一个设备无关的实用方法 release_memory,该方法考虑了各种硬件后端,如XPU、MLU、NPU、MPS等。
from accelerate.utils import release_memory
# ...
release_memory(model)
现在假设你的GPU没有32 GB的VRAM呢?研究发现,模型权重可以被量化到8位或4位,而不会在性能上造成显著损失(见Dettmers等人)。模型甚至可以被量化到3位或2位,只要在性能上接受一定的损失,正如最近GPTQ论文所展示的那样 🤯。
不过不深入细节,量化方案旨在降低权重的精度,同时尽量保持模型推理结果尽可能准确(也就是尽可能接近bfloat16)。请注意,量化在文本生成方面特别有效,因为我们所关心的只是选择最有可能的下一个标记集,并不真正关心下一个标记logit分布的确切值。重要的是下一个标记logit分布大致保持不变,这样argmax
或topk
操作就能给出相同的结果。
存在各种量化技术,我们这里不会详细讨论,但一般来说,所有量化技术的工作原理如下:
- 将所有权重量化到目标精度
- 加载量化后的权重,并使用bfloat16精度传递输入向量的序列
- 动态将权重反量化为bfloat16以使用其输入向量在bfloat16精度下进行计算
总的来说,这意味着 输入-权重矩阵 的乘法,其中XX 是 输入,WW 是权重矩阵,YY 是输出:Y=X∗WY=X∗W
被更改为Y=X∗dequantize(W)Y=X∗dequantize(W)
对于每次矩阵乘法。当输入通过网络图时,对所有权重矩阵进行逐个的解量化和重新量化。
因此,使用量化权重时,推理时间通常 不会 减少,反而会增加。理论就到这里,让我们试试看!要使用 Transformers 量化权重,你需要确保已经安装了 bitsandbytes
库。bitsandbytes
。
!pip install bitsandbytes
我们可以通过在 from_pretrained
中添加 load_in_8bit=True
标志来简单地加载 8 位量化模型。
model = AutoModelForCausalLM.from_pretrained("bigcode/octocoder", load_in_8bit=True, pad_token_id=0)
现在,让我们再次运行我们的示例并测量内存使用情况。
pipe = pipeline("text-generation", model=model, tokenizer=tokenizer)
result = pipe(prompt, max_new_tokens=60)[0]["generated_text"][len(prompt):]
result
输出:
Here is a Python function that transforms bytes to Giga bytes:\n\n```python\ndef bytes_to_giga_bytes(bytes):\n return bytes / 1024 / 1024 / 1024\n```\n\nThis function takes a single
好的,我们得到了之前相同的结果,所以准确率没有损失!让我们看看这次使用了多少内存。
bytes_to_giga_bytes(torch.cuda.max_memory_allocated())
输出:
15.219234466552734
显著降低!我们的数据量下降到了仅略超过15 GB,因此我们可以在这类消费级GPU上运行该模型,比如4090。我们在内存效率上看到了一个非常不错的提升,并且模型输出几乎没有下降。然而,我们也可以注意到推理过程中出现了一些轻微的减速。
我们删除了模型并再次刷新内存。
del model
del pipe
flush()
让我们看看4位量化对GPU内存消耗峰值的影响。将模型量化到4位可以使用与之前相同的API,这次通过传递load_in_4bit=True
而不是load_in_8bit=True
。
model = AutoModelForCausalLM.from_pretrained("bigcode/octocoder", load_in_4bit=True, low_cpu_mem_usage=True, pad_token_id=0)
pipe = pipeline("text-generation", model=model, tokenizer=tokenizer)
result = pipe(prompt, max_new_tokens=60)[0]["generated_text"][len(prompt):]
result
输出:
Here is a Python function that transforms bytes to Giga bytes:\n\n```\ndef bytes_to_gigabytes(bytes):\n return bytes / 1024 / 1024 / 1024\n```\n\nThis function takes a single argument
我们几乎看到了之前相同的输出文本 - 只是在代码片段之前缺少了 python
。让我们看看需要多少内存。
bytes_to_giga_bytes(torch.cuda.max_memory_allocated())
输出:
9.543574333190918
仅仅9.5GB!这对于一个超过150亿参数的模型来说真的不算多。
虽然我们在这里看到模型的精度下降非常小,但在实际操作中,4位量化与8位量化或全bfloat16
推理相比,往往会导致不同的结果。是否尝试取决于用户。
此外,请注意,这里的推理速度比8位量化慢一些,这是因为4位量化使用了更加激进的量化方法,导致量化和解量化在推理过程中耗时更长。
del model
del pipe
flush()
总体来说,我们发现使用8位精度运行OctoCoder将所需的GPU VRAM从32GB减少到仅有15GB,而将模型运行在4位精度进一步将所需的GPU VRAM减少到略超过9GB。
4位量化允许模型在RTX3090、V100和T4等GPU上运行,这些对于大多数人来说都相当容易获得。
有关量化的更多信息以及如何量化模型以需要比4位更少的GPU VRAM内存,我们建议查看AutoGPTQ
的实现。
作为结论,重要的是要记住,模型量化是在提高内存效率与准确性以及在某些情况下推理时间之间进行权衡。
如果GPU内存不是您用例的限制因素,通常没有必要考虑量化。然而,许多GPU在没有量化方法的情况下根本无法运行LLMs,在这种情况下,4位和8位量化方案是极其有用的工具。
有关更详细的用法信息,我们强烈建议您查看Transformers Quantization Docs。接下来,让我们看看我们如何通过使用更好的算法和改进的模型架构来提高计算和内存效率。
2. 闪存注意力
今天表现最好的大型语言模型(LLM)基本上具有相同的根本架构,该架构由前馈层、激活层、层归一化层以及最重要的自注意力层组成。
自注意力层对于大型语言模型(LLMs)至关重要,因为它们使模型能够理解输入标记之间的上下文关系。然而,自注意力层的峰值GPU内存消耗在计算和内存复杂度上随着输入标记的数量(也称为“序列长度”)呈二次增长,我们以下用N表示。虽然这对于较短的输入序列(最多1000个输入标记)来说并不明显,但对于较长的输入序列(大约16000个输入标记)来说,这成为一个严重问题。
让我们更仔细地看看。计算长度为N的输入X的自注意力层的输出O的公式是:O=Attn(X)=V×Softmax(QKT) ,其中 Q=WqX,V=WvX,K=WkXO=Attn(X)=V×Softmax(QKT) ,其中 Q=WqX,V=WvX,K=WkXX=(x1,…xN)X=(x1,…xN) 是注意力层的输入序列。投影Q和K将各包含N个向量,因此QKT的大小为N2N2 。
LLMs通常具有多个注意力头,因此可以并行执行多个自注意力计算。假设LLM有40个注意力头,并以bfloat16精度运行,我们可以计算出存储QKT矩阵所需的内存要求为40∗2∗N240∗2∗N2 字节。对于N=1000N=1000,只需要大约50 MB的VRAM,然而,对于N=16000N=16000,我们需要19 GB的VRAM,对于N=100,000N=100,000,我们只需要几乎1TB来存储QKT矩阵。
简而言之,默认的自注意力算法对于大型输入上下文来说很快就变得过于昂贵。
随着LLM在文本理解和生成方面的改进,它们被应用于越来越复杂的任务。虽然模型曾经处理几个句子的翻译或摘要,但现在它们可以处理整个页面,需要能够处理长输入长度。
我们如何摆脱大型输入长度的庞大内存需求?我们需要一种新的计算自注意力机制的方法,该方法可以消除QKT矩阵。Tri Dao等人 开发了一种恰好满足这种需求的新算法,并将其称为Flash Attention。
简而言之,Flash Attention将V×Softmax(QKTV×Softmax(QKT)计算分解开来,并通过迭代多个softmax计算步骤来计算输出的小块:Oi←sija∗Oi+sijb∗Vj×Softmax(QKi,jT) for multiple i,j iterationsOi←sija∗Oi+sijb∗Vj×Softmax(QKi,j**T) for multiple i,j iterations
其中sija和sijb是某些softmax归一化统计量,对于每个i和j都需要重新计算。
请注意,整个Flash Attention要复杂得多,这里大大简化了,因为过于深入的内容超出了本指南的范围。读者可以查看Flash Attention论文以获取更多详细信息。
这里的主要启示是:
通过跟踪 softmax 归一化统计信息,并通过一些巧妙的数学方法,Flash Attention 在内存成本仅随 NN 线性增加的情况下,与默认的自注意力层给出 数值上相同 的输出。
查看公式,人们会直观地认为Flash Attention必须比默认的自注意力公式慢得多,因为需要做更多的计算。确实,与正常注意力相比,Flash Attention需要更多的FLOPs,因为softmax归一化的统计信息需要不断重新计算(如果感兴趣,请参阅paper获取更多详细信息)
然而,与默认的注意力机制相比,Flash Attention 在推理中要快得多,这得益于它能够显著减少对较慢、高带宽的 GPU 内存(VRAM)的需求,转而关注更快的片上内存(SRAM)。
本质上,Flash Attention 确保所有中间的读写操作都可以使用快速的 片上 SRAM 内存来完成,而不是不得不访问较慢的 VRAM 内存来计算输出向量OO。
在实践中,目前完全没有理由 不 使用 Flash Attention(如果可用)。该算法在数学上给出相同的输出,并且更快、更节省内存。
让我们来看一个实际例子。
我们的 OctoCoder 模型现在获得了显著更长的输入提示,其中包括所谓的 系统提示。系统提示用于引导 LLM 成为一个更好的助手,该助手针对用户的任务进行了定制。在下面的例子中,我们将使用一个系统提示,这将使 OctoCoder 成为更好的编码助手。
system_prompt = """Below are a series of dialogues between various people and an AI technical assistant.
The assistant tries to be helpful, polite, honest, sophisticated, emotionally aware, and humble but knowledgeable.
The assistant is happy to help with code questions and will do their best to understand exactly what is needed.
It also tries to avoid giving false or misleading information, and it caveats when it isn't entirely sure about the right answer.
That said, the assistant is practical really does its best, and doesn't let caution get too much in the way of being useful.
The Starcoder models are a series of 15.5B parameter models trained on 80+ programming languages from The Stack (v1.2) (excluding opt-out requests).
The model uses Multi Query Attention, was trained using the Fill-in-the-Middle objective, and with 8,192 tokens context window for a trillion tokens of heavily deduplicated data.
-----
Question: Write a function that takes two lists and returns a list that has alternating elements from each input list.
Answer: Sure. Here is a function that does that.
def alternating(list1, list2):
results = []
for i in range(len(list1)):
results.append(list1[i])
results.append(list2[i])
return results
Question: Can you write some test cases for this function?
Answer: Sure, here are some tests.
assert alternating([10, 20, 30], [1, 2, 3]) == [10, 1, 20, 2, 30, 3]
assert alternating([True, False], [4, 5]) == [True, 4, False, 5]
assert alternating([], []) == []
Question: Modify the function so that it returns all input elements when the lists have uneven length. The elements from the longer list should be at the end.
Answer: Here is the modified function.
def alternating(list1, list2):
results = []
for i in range(min(len(list1), len(list2))):
results.append(list1[i])
results.append(list2[i])
if len(list1) > len(list2):
results.extend(list1[i+1:])
else:
results.extend(list2[i+1:])
return results
-----
"""
为了演示目的,我们将系统提示重复十次,以便输入长度足够长,可以观察到Flash Attention的记忆节省。我们附加了原始文本提示 "问题:请编写一个Python函数,将字节转换为吉字节。\n\n答案:这里"
long_prompt = 10 * system_prompt + prompt
我们再次以 bfloat16 精度实例化我们的模型。
model = AutoModelForCausalLM.from_pretrained("bigcode/octocoder", torch_dtype=torch.bfloat16, device_map="auto")
tokenizer = AutoTokenizer.from_pretrained("bigcode/octocoder")
pipe = pipeline("text-generation", model=model, tokenizer=tokenizer)
现在让我们像之前一样运行模型,只是这次 不使用 Flash Attention,并测量峰值 GPU 内存需求和推理时间。
import time
start_time = time.time()
result = pipe(long_prompt, max_new_tokens=60)[0]["generated_text"][len(long_prompt):]
print(f"Generated in {time.time() - start_time} seconds.")
result
输出:
Generated in 10.96854019165039 seconds.
Sure. Here is a function that does that.\n\ndef bytes_to_giga(bytes):\n return bytes / 1024 / 1024 / 1024\n\nAnswer: Sure. Here is a function that does that.\n\ndef
我们得到了与之前相同的输出,然而这次,模型重复回答多次,直到达到60个token的截断。这并不奇怪,因为我们为了演示目的已经重复了系统提示十次,从而引导模型重复回答。
注意:在实际应用中,系统提示不应该重复十次 - 一次就足够了!
让我们测量峰值GPU内存需求。
bytes_to_giga_bytes(torch.cuda.max_memory_allocated())
输出:
37.668193340301514
正如我们所见,峰值 GPU 内存需求现在显著高于最初,这主要归因于更长的输入序列。此外,生成现在需要超过一分钟。
我们调用 flush()
以释放 GPU 内存以供我们下一个实验使用。
flush()
为了比较,让我们运行相同的函数,但启用 Flash Attention。为此,我们将模型转换为 BetterTransformer,通过这样做启用 PyTorch 的 SDPA 自注意力,它反过来能够使用 Flash Attention。
model.to_bettertransformer()
现在我们运行与之前完全相同的代码片段,并且底层 Transformer 将会使用 Flash Attention。
start_time = time.time()
with torch.backends.cuda.sdp_kernel(enable_flash=True, enable_math=False, enable_mem_efficient=False):
result = pipe(long_prompt, max_new_tokens=60)[0]["generated_text"][len(long_prompt):]
print(f"Generated in {time.time() - start_time} seconds.")
result
输出:
Generated in 3.0211617946624756 seconds.
Sure. Here is a function that does that.\n\ndef bytes_to_giga(bytes):\n return bytes / 1024 / 1024 / 1024\n\nAnswer: Sure. Here is a function that does that.\n\ndef
我们得到了与之前完全相同的结果,但得益于 Flash Attention,我们可以观察到非常显著的速度提升。
让我们最后一次测量内存消耗。
bytes_to_giga_bytes(torch.cuda.max_memory_allocated())
输出:
32.617331981658936
我们几乎回到了最初的 29GB GPU 内存峰值。
我们可以观察到,与最初采用的短输入序列相比,当通过 Flash Attention 传递一个非常长的输入序列时,我们仅额外使用了大约 100MB 的 GPU 内存。
flush()
关于如何使用Flash Attention的更多信息,请参阅这篇文档页面。
3. 建筑创新
截至目前,我们已经探讨了通过以下方式提高计算和内存效率:
So far we have looked into improving computational and memory efficiency by:
- 将权重转换为更低精度的格式
- 用更内存和计算效率高的版本替换自注意力算法
现在让我们看看如何改变大型语言模型(LLM)的架构,使其对于需要长文本输入的任务来说最为有效和高效,例如: - 检索增强问答,
- 摘要,
- 聊天
注意,chat 不仅要求 LLM 处理长文本输入,还要求 LLM 能够高效地处理用户和助手之间的来回对话(例如 ChatGPT)。
一旦训练完成,基本的 LLM 架构就很难改变,因此在事先考虑 LLM 的任务并相应优化模型架构是很重要的。模型架构中有两个重要组成部分,对于大型输入序列,它们很快就会成为内存和/或性能瓶颈。
- 位置嵌入
- 键值缓存
让我们更详细地了解每个组件
3.1 提高LLMs的位置嵌入
Self-attention 将每个标记与彼此的标记相关联。例如,文本输入序列 “Hello”, “I”, “love”, “you” 的 Softmax(QKT)Softmax(QKT) 矩阵可能如下所示:
每个词元都被赋予一个概率质量,该质量使其关注所有其他词元,因此与其他所有词元建立关系。例如,词元*“love”以5%的概率关注词元“Hello”,以30%的概率关注词元“I”*,以65%的概率关注自身。
基于自注意力的LLM,但没有位置嵌入,在理解文本输入之间的位置关系时会遇到极大的困难。这是因为QKTQKT计算出的概率分数将每个词元与每个其他词元相关联,无论它们之间的相对位置距离如何。因此,对于没有位置嵌入的LLM,每个词元似乎与其他所有词元距离相同,例如,区分*“Hello I love you”和“You love I hello”*将非常具有挑战性。
为了使LLM理解句子顺序,需要额外的提示,这通常以位置编码(或也称为位置嵌入)的形式应用。位置编码将每个词元的位置编码成LLM可以利用的数值表示,从而更好地理解句子顺序。
Attention Is All You Need 一文的作者引入了正弦位置嵌入P=p1,…,pNP=p1,…,pN 。其中,每个向量pipi是其位置ii的正弦函数。然后,将位置编码简单地添加到输入序列向量X=x1,…,xN**X****=x****1,…,**x****N =x1+p1,…,xN+pNx1+p1,…,xN+pN,从而提示模型更好地学习句子顺序。
其他人(如Devlin等人)没有使用固定的位置嵌入,而是使用了学习到的位置嵌入,其中位置嵌入PP在训练期间学习。
正弦和学习到的位置嵌入曾经是编码句子顺序到LLM中的主要方法,但发现了一些与这些位置编码相关的问题:
1、正弦和学习到的位置嵌入都是绝对位置嵌入,即为每个位置id编码一个唯一的嵌入:0,…,N0,…,N。正如Huang等人和Su等人所示,绝对位置嵌入会导致LLM在长文本输入上的性能较差。对于长文本输入,如果模型学习输入词元之间的相对位置距离而不是它们的绝对位置,则更有利。
2、当使用学习到的位置嵌入时,LLM必须在固定的输入长度NN上训练,这使得将其外推到训练长度之外的输入长度变得困难。
最近,可以解决上述问题的相对位置嵌入变得更加流行,最值得注意的是:
- 转盘位置嵌入 (RoPE)
- ALiBi
RoPE 和 ALiBi 都认为最好直接在自注意力算法中提示 LLM 关于句子顺序,因为这是将词标记放入相互关系中的地方。更具体地说,句子顺序应该通过修改 **QKTQKT 计算来提示。
不深入细节,RoPE 指出,位置信息可以编码到查询-键对中,例如通过旋转每个向量,分别用θ∗iθ∗i 和θ∗jθ∗j 来编码qiqi 和xjxj,其中i,ji,j 描述每个向量的句子位置:qiTxj=qiTRθ,i−jxj.q*****i**T***x****j=qiTRθ,i−jxj.Rθ,i−jRθ,i−j 因此表示一个旋转矩阵。θθ 在训练过程中 不 学习,而是设置为训练期间输入序列长度的预定义值。
通过这样做,qiqi 和 qjqj 之间的概率分数只有在 i≠ji=j 时才会受到影响,并且仅取决于相对距离 i−ji−j ,而不管每个向量具体的位姿 sii 和 jj 。
RoPE 被用于今天许多最重要的 LLM 中,例如:
- Falcon
- Llama
- PaLM
作为一个替代方案,ALiBi 提出了一种更加简单的相对位置编码方案。输入标记之间的相对距离作为负整数,并乘以一个预定义的值m
,然后添加到 QKTQKT 矩阵的查询-键条目中,在 softmax 计算之前。
如ALiBi论文中所示,这种简单的相对位置编码使得模型即使在非常长的文本输入序列中也能保持高性能。
ALiBi 被用于今天许多最重要的LLM中,例如:
- MPT
- BLOOM
RoPE 和 ALiBi 位置编码都可以扩展到训练期间未见过的输入长度,然而,与 RoPE 相比,已经证明 ALiBi 的扩展效果在开箱即用的情况下要好得多。对于 ALiBi,只需增加下三角位置矩阵的值以匹配输入序列的长度。对于 RoPE,当传递比训练期间看到的文本输入长得多时,保持训练期间使用的相同的θθ会导致结果不佳,c.f Press 等人。然而,社区已经发现了一些有效的技巧来适应θθ,从而使得 RoPE 位置嵌入能够很好地适用于扩展的文本输入序列(见这里)。
RoPE 和 ALiBi 都是相对位置嵌入,它们在训练过程中 不是 学习得到的,而是基于以下直觉:
- 文本输入的位置信息应该直接提供给自注意力层的 QKTQKT 矩阵
- 应该激励 LLM 学习一个常数 相对 距离,位置编码彼此之间必须具有
- 文本输入标记彼此距离越远,它们查询-值概率就越低。Both RoPE and ALiBi 会降低彼此距离较远的标记的查询-键概率。RoPE 通过增加查询-键向量之间的角度来减少它们的向量积。ALiBi 通过向向量积添加大负数来实现
结论是,打算用于处理大型文本输入任务的 LLM,使用相对位置嵌入(如 RoPE 和 ALiBi)进行训练会更好。同时请注意,即使带有 RoPE 和 ALiBi 的 LLM 只在固定长度(例如 N1=2048N1=2048)上进行过训练,它仍然可以通过外推位置嵌入在实践中使用比 N1N1 大得多的文本输入,比如 N2=8192>N1N2=8192>N1。
3.2 键值缓存
自动回归文本生成使用LLM是通过迭代地输入一个输入序列,采样下一个标记,将下一个标记追加到输入序列中,并继续这样做,直到LLM产生一个表示生成结束的标记。
请查看Transformer的生成文本教程,以获得关于自动回归生成工作原理的更直观的解释。
让我们运行一个简短的代码片段来展示自动回归在实际中是如何工作的。我们将通过torch.argmax
简单地选择最可能的下一个标记。
input_ids = tokenizer(prompt, return_tensors="pt")["input_ids"].to("cuda")
for _ in range(5):
next_logits = model(input_ids)["logits"][:, -1:]
next_token_id = torch.argmax(next_logits,dim=-1)
input_ids = torch.cat([input_ids, next_token_id], dim=-1)
print("shape of input_ids", input_ids.shape)
generated_text = tokenizer.batch_decode(input_ids[:, -5:])
generated_text
输出:
shape of input_ids torch.Size([1, 21])
shape of input_ids torch.Size([1, 22])
shape of input_ids torch.Size([1, 23])
shape of input_ids torch.Size([1, 24])
shape of input_ids torch.Size([1, 25])
[' Here is a Python function']
正如我们所见,每次我们通过刚刚采样的标记增加文本输入标记。
除了极少数例外,LLMs 都使用 因果语言建模目标 进行训练,因此会屏蔽注意力得分的上三角矩阵 - 这就是为什么在上面的两个图中,注意力得分被留空(也称为有 0 概率)。如果您想快速回顾因果语言建模,可以参考 Illustrated Self Attention 博客。
因此,标记 从不 依赖于之前的标记,更具体地说,qiqi 向量永远不会与任何键、值向量 kj,vjkj,vj 相关联,如果 j > ij>i*。相反,qiqi 只关注之前的键值向量 km<i,vm<i ,对于 m∈{0,…i−1}km<i,vm<i ,对于 m∈{0,…i−1}。为了减少不必要的计算,因此可以缓存每一层的键值向量,用于所有之前的时间步。
在下面,我们将告诉 LLM 通过检索和转发它来利用键值缓存,以便在每次前向传递中获取。在 Transformers 中,我们可以通过将 use_cache
标志传递给 forward
调用来检索键值缓存,然后我们可以将它与当前标记一起传递。
past_key_values = None # past_key_values is the key-value cache
generated_tokens = []
next_token_id = tokenizer(prompt, return_tensors="pt")["input_ids"].to("cuda")
for _ in range(5):
next_logits, past_key_values = model(next_token_id, past_key_values=past_key_values, use_cache=True).to_tuple()
next_logits = next_logits[:, -1:]
next_token_id = torch.argmax(next_logits, dim=-1)
print("shape of input_ids", next_token_id.shape)
print("length of key-value cache", len(past_key_values[0][0])) # past_key_values are of shape [num_layers, 0 for k, 1 for v, batch_size, length, hidden_dim]
generated_tokens.append(next_token_id.item())
generated_text = tokenizer.batch_decode(generated_tokens)
generated_text
输出:
shape of input_ids torch.Size([1, 1])
length of key-value cache 20
shape of input_ids torch.Size([1, 1])
length of key-value cache 21
shape of input_ids torch.Size([1, 1])
length of key-value cache 22
shape of input_ids torch.Size([1, 1])
length of key-value cache 23
shape of input_ids torch.Size([1, 1])
length of key-value cache 24
[' Here', ' is', ' a', ' Python', ' function']
正如所见,当使用键值缓存时,文本输入标记的长度 不会 增加,而保持为一个单独的输入向量。另一方面,键值缓存的长度在每次解码步骤中增加一个。
利用键值缓存意味着QKTQKT基本上被简化为qcKTqcKT,其中qcqc是当前传递输入令牌的查询投影,它总是只是一个单个向量。
使用键值缓存有两个优势:
- 相比于计算完整的QKTQKT矩阵,计算效率显著提高,因为计算量更少。这导致推理速度提高
- 所需的最大内存并不会随着生成的标记数量成平方增长,而是仅线性增长。
应该 始终 使用键值缓存,因为它会导致相同的结果并显著提高较长输入序列的速度。当使用文本管道或
generate
方法时,Transformers 默认启用键值缓存。我们有一个专门的指南介绍缓存,在这里。
请注意,尽管我们建议使用键值缓存,但您在使用它们时,LLM 输出可能略有不同。这是矩阵乘法内核本身的特性——您可以在这里了解更多信息。
3.2.1 多轮对话
键值缓存对于需要多次自回归解码的应用程序(如 chat
)特别有用。让我们来看一个例子。
User: How many people live in France?
Assistant: Roughly 75 million people live in France
User: And how many are in Germany?
Assistant: Germany has ca. 81 million inhabitants
在这个聊天中,LLM 进行了两次自回归解码:
1、第一次,键值缓存为空,输入提示为 "用户:法国有多少人居住?"
,模型自回归地生成文本 "法国大约有7500万人"
,同时在每个解码步骤增加键值缓存。
2、第二次输入提示为 "用户:法国有多少人居住? \n 助手:法国大约有7500万人居住 \n 用户:德国有多少人?"
。多亏了缓存,前两个句子的所有键值向量已经计算。因此,输入提示仅包含 "用户:德国有多少人?"
。在处理缩短后的输入提示时,其计算出的键值向量被连接到第一次解码的键值缓存中。然后,第二个助手的回答 "德国大约有8100万居民"
就使用包含编码后的键值向量的键值缓存("用户:法国有多少人居住? \n 助手:法国大约有7500万人居住 \n 用户:德国有多少人?"
)进行自回归生成。
以下两点应予以注意:
1、保留所有上下文对于在聊天中部署的LLM至关重要,这样LLM才能理解对话的所有先前上下文。例如,对于上面的例子,LLM需要理解当用户问 "德国有多少人"
时,他们指的是人口。
2、键值缓存对于聊天非常有用,因为它允许我们持续增长编码的聊天历史,而不是不得不从头开始重新编码聊天历史(例如,在使用编码器-解码器架构时)。
在 transformers
中,当传递 return_dict_in_generate=True
时,generate
调用将返回 past_key_values
,除了默认的 use_cache=True
。请注意,它目前还不能通过 pipeline
接口访问。
# Generation as usual
prompt = system_prompt + "Question: Please write a function in Python that transforms bytes to Giga bytes.\n\nAnswer: Here"
model_inputs = tokenizer(prompt, return_tensors='pt')
generation_output = model.generate(**model_inputs, max_new_tokens=60, return_dict_in_generate=True)
decoded_output = tokenizer.batch_decode(generation_output.sequences)[0]
# Piping the returned `past_key_values` to speed up the next conversation round
prompt = decoded_output + "\nQuestion: How can I modify the function above to return Mega bytes instead?\n\nAnswer: Here"
model_inputs = tokenizer(prompt, return_tensors='pt')
generation_output = model.generate(
**model_inputs,
past_key_values=generation_output.past_key_values,
max_new_tokens=60,
return_dict_in_generate=True
)
tokenizer.batch_decode(generation_output.sequences)[0][len(prompt):]
输出:
is a modified version of the function that returns Mega bytes instead.
def bytes_to_megabytes(bytes):
return bytes / 1024 / 1024
Answer: The function takes a number of bytes as input and returns the number of
很好,没有额外的时间用于重新计算注意力层的相同键和值!
然而,有一个问题。虽然QKTQKT矩阵所需的峰值内存显著减少,但对于长输入序列或多轮聊天,在内存中保持键值缓存可能会变得非常昂贵。
请记住,键值缓存需要存储所有先前输入向量xi的键值向量,对于i∈{1,…,c−1}xi, for i∈{1,…,c−1},对于所有自注意力层和所有注意力头。
让我们计算需要存储在键值缓存中的浮点值数量,这些值用于我们之前使用的LLM bigcode/octocoder
。浮点值的数量是序列长度乘以注意力头数量乘以注意力头维度乘以层数的两倍。以16000个假设输入序列长度为例计算,我们得到:
config = model.config
2 * 16_000 * config.n_layer * config.n_head * config.n_embd // config.n_head
输出:
7864320000
大概80亿个浮点值!以float16
精度存储80亿个浮点值需要大约15 GB的RAM,这大约是模型权重本身的一半!
研究人员提出了两种方法,允许显著降低存储键值缓存的记忆成本,这两种方法将在下一小节中探讨。
3.2.2 多查询注意力 (MQA)
多查询注意力 是在 Noam Shazeer 的 快速 Transformer 解码:你需要一个写头 纸文中提出的。正如标题所说,Noam 发现,与其使用 n_head
键值投影权重,不如使用一对单个头值投影权重,该权重对所有注意力头共享,而不会导致模型性能显著下降。
通过使用单个头值投影权重对,关键值向量 ki,vi 必须在所有注意力头中保持一致,这反过来意味着我们只需要在缓存中存储 1 个键值投影对,而不是
n_head
个。
由于大多数LLM使用20到100个注意力头,MQA显著降低了键值缓存的内存消耗。
因此,对于本笔记本中使用的LLM,我们可以在输入序列长度为16000的情况下,将所需的内存消耗从15 GB减少到不到400 MB。
除了节省内存外,MQA还提高了计算效率,如下所述。
在自回归解码中,需要重新加载大型的键值向量,将其与当前的键值向量对连接,然后每一步将其输入到 qcKT 计算中。
对于自回归解码,所需的重载内存带宽可能会成为严重的时间瓶颈。通过减少键值向量的大小,需要访问的内存减少,从而减少了内存带宽瓶颈。更多细节,请参阅Noam的论文。
这里需要理解的重要部分是,将键值注意力头的数量减少到1只有在使用键值缓存的情况下才有意义。
没有键值缓存时,模型单次前向传递的峰值内存消耗保持不变,因为每个注意力头仍然有一个独特的查询向量,因此每个注意力头仍然有一个不同的QKTQKT 矩阵。
MQA已被社区广泛采用,现在许多最流行的LLM都在使用它:
3.2.3 分组查询注意力 (GQA)
分组查询注意力,由谷歌的Ainslie等人提出,发现与使用传统的多键值头投影相比,使用MQA(多查询注意力)往往会导致质量下降。
论文认为,通过减少查询头投影权重的数量,可以保留更多的模型性能。而不是只使用单个键值投影权重,应该使用n < n_head
个键值投影权重。通过将n
选得比n_head
小得多,例如2、4或8,几乎可以保留MQA带来的几乎所有内存和速度提升,同时牺牲较少的模型容量,从而可能牺牲较少的性能。
此外,GQA的作者发现,现有的模型检查点可以通过GQA架构进行升级训练,所需的原预训练计算量仅为原始的5%。
虽然5%的原预训练计算量仍然可能是一个巨大的数字,但GQA的升级训练使得现有的检查点可以用于更长的输入序列。
GQA是最近才提出的,这就是为什么在撰写这个笔记本的时候,它的采用率较低。GQA最显著的应用是Llama-v2。
作为结论,强烈建议在使用具有自回归解码的 LLM 并且需要处理大型输入序列(例如聊天)的情况下,使用 GQA 或 MQA。
结论
研究社区一直在想出各种新颖的方法来加速更大规模LLM的推理时间。
例如,一个有希望的研究方向是投机解码,其中“简单标记”由较小的、较快的语言模型生成,而只有“困难标记”由LLM本身生成。
更详细的说明超出了这个笔记本的范围,但可以在这篇不错的博客文章中阅读。
之所以像GPT3/4、Llama-2-70b、Claude、PaLM这样的大规模LLM可以在Hugging Face Chat或ChatGPT这样的聊天界面中运行得如此快速,在很大程度上要归功于上述提到的在精度、算法和架构方面的改进。
展望未来,GPU、TPU等加速器只会变得更快,并允许更多的内存,但无论如何,都应该始终确保使用最佳可用的算法和架构,以获得最大的性价比 🤗