前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Transformers 4.37 中文文档(十二)

Transformers 4.37 中文文档(十二)

作者头像
ApacheCN_飞龙
发布2024-06-26 14:55:31
1040
发布2024-06-26 14:55:31
举报
文章被收录于专栏:信数据得永生信数据得永生

原文:huggingface.co/docs/transformers

🤗 Transformers 能做什么

原文链接:huggingface.co/docs/transformers/v4.37.2/en/task_summary

🤗 Transformers 是一个预训练的最先进模型库,用于自然语言处理(NLP)、计算机视觉以及音频和语音处理任务。这个库不仅包含了 Transformer 模型,还有像现代卷积网络这样的非 Transformer 模型,用于计算机视觉任务。如果你看一下今天最流行的消费产品,比如智能手机、应用和电视,很可能背后都有某种深度学习技术。想要从智能手机拍摄的照片中移除背景物体?这就是一个全景分割任务的例子(如果你还不知道这是什么,不用担心,我们将在接下来的部分中描述!)。

这个页面提供了关于🤗 Transformers 库中可以用三行代码解决的不同语音和音频、计算机视觉和 NLP 任务的概述!

音频

音频和语音处理任务与其他模态有些不同,主要是因为音频作为输入是一个连续信号。与文本不同,原始音频波形不能像句子可以被分成单词那样整齐地分割。为了解决这个问题,原始音频信号通常以固定间隔进行采样。如果在一个间隔内取更多样本,采样率就更高,音频更接近原始音频源。

以前的方法是对音频进行预处理,从中提取有用的特征。现在更常见的做法是直接将原始音频波形输入特征编码器,以提取音频表示,开始音频和语音处理任务。这简化了预处理步骤,并允许模型学习最关键的特征。

音频分类

音频分类是一个从预定义类别集中为音频数据贴上标签的任务。这是一个广泛的类别,有许多具体应用,其中一些包括:

  • 声学场景分类:为音频贴上场景标签(“办公室”,“海滩”,“体育场”)
  • 声学事件检测:为音频贴上声音事件标签(“汽车喇叭”,“鲸鱼呼叫”,“玻璃破碎”)
  • 标记:为包含多个声音的音频贴上标签(鸟鸣声,会议中的发言人识别)
  • 音乐分类:为音乐贴上流派标签(“金属”,“嘻哈”,“乡村”)
代码语言:javascript
复制
>>> 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 系统相比的高质量结果。

代码语言:javascript
复制
>>> 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 所青睐的自下而上方法不同,这有点像从模糊的图像开始,然后逐渐使其聚焦。
图像分类

图像分类从预定义类别集合中为整个图像标记。与大多数分类任务一样,图像分类有许多实际用例,其中一些包括:

  • 医疗保健:标记医学图像以检测疾病或监测患者健康
  • 环境:标记卫星图像以监测森林砍伐,通知野地管理或检测野火
  • 农业:标记作物图像以监测植物健康或卫星图像用于土地利用监测
  • 生态学:标记动物或植物物种的图像以监测野生动物种群或跟踪濒危物种
代码语言:javascript
复制
>>> 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'}
目标检测

与图像分类不同,目标检测识别图像中的多个对象以及对象在图像中的位置(由边界框定义)。目标检测的一些示例应用包括:

  • 自动驾驶车辆:检测日常交通对象,如其他车辆,行人和交通灯
  • 遥感:灾害监测,城市规划和天气预报
  • 缺陷检测:检测建筑物中的裂缝或结构损坏,以及制造缺陷
代码语言:javascript
复制
>>> 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}}]
图像分割

图像分割是一个像素级任务,将图像中的每个像素分配给一个类别。它与目标检测不同,后者使用边界框来标记和预测图像中的对象,因为分割更加细粒化。分割可以在像素级别检测对象。有几种类型的图像分割:

  • 实例分割:除了标记对象的类别外,还标记对象的每个不同实例(“狗-1”,“狗-2”)
  • 全景分割:语义和实例分割的结合;它使用语义类别标记每个像素对象的每个不同实例

分割任务在自动驾驶车辆中非常有用,可以为它们创建周围世界的像素级地图,以便它们可以安全地绕过行人和其他车辆。在医学成像中也很有用,任务的更细粒度可以帮助识别异常细胞或器官特征。图像分割还可以用于电子商务,通过在真实世界中通过您的相机叠加对象来虚拟试穿衣服或创建增强现实体验。

代码语言:javascript
复制
>>> 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 表示。

深度估计有两种方法:

  • 立体:通过比较略有不同角度拍摄的两幅图像来估计深度
  • 单眼:从单个图像估计深度
代码语言:javascript
复制
>>> 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 任务是最常见的任务类型之一,因为文本是我们进行交流的一种自然方式。要将文本转换为模型识别的格式,需要对其进行标记化。这意味着将文本序列分割为单独的单词或子词(标记),然后将这些标记转换为数字。因此,您可以将文本序列表示为数字序列,一旦您有了数字序列,就可以将其输入到模型中以解决各种 NLP 任务!

文本分类

与任何模态中的分类任务一样,文本分类将文本序列(可以是句子级、段落或文档)从预定义类别集中标记。文本分类有许多实际应用,其中一些包括:

  • 情感分析:根据积极消极等极性为文本标记,可以在政治、金融和营销等领域支持决策
  • 内容分类:根据某个主题为文本标记,以帮助组织和过滤新闻和社交媒体信息流中的信息(如天气体育金融等)
代码语言:javascript
复制
>>> 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'}]
标记分类

在任何自然语言处理任务中,文本都会被预处理,将文本序列分割成单个单词或子词。这些被称为标记。标记分类为每个标记分配一个来自预定义类别集的标签。

两种常见的标记分类类型是:

  • 命名实体识别(NER):根据实体类别(如组织、个人、地点或日期)为标记分配标签。NER 在生物医学领域特别受欢迎,可以标记基因、蛋白质和药物名称。
  • 词性标注(POS):根据其词性(名词、动词或形容词)为标记分配标签。POS 对于帮助翻译系统理解两个相同单词在语法上的不同之处(名词“银行”与动词“存款”)非常有用。
代码语言:javascript
复制
>>> 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}
问答

问答是另一个标记级任务,它返回一个问题的答案,有时包含上下文(开放域),有时不包含上下文(封闭域)。每当我们询问虚拟助手像餐厅是否营业这样的问题时,这个任务就会发生。它还可以提供客户或技术支持,并帮助搜索引擎检索您所询问的相关信息。

问答有两种常见类型:

  • 抽取式:给定一个问题和一些上下文,答案是模型必须从上下文中提取的文本片段
  • 抽象式:给定一个问题和一些上下文,从上下文生成答案;这种方法由 Text2TextGenerationPipeline 处理,而不是下面显示的 QuestionAnsweringPipeline
代码语言:javascript
复制
>>> 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 使用抽象式方法
代码语言:javascript
复制
>>> 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 .'}]
翻译

翻译将一种语言中的文本序列转换为另一种语言。它对帮助来自不同背景的人们相互沟通、帮助翻译内容以扩大受众、甚至作为帮助人们学习新语言的工具都非常重要。翻译与摘要一样,是一个序列到序列的任务,意味着模型接收一个输入序列并返回一个目标输出序列。

在早期,翻译模型主要是单语的,但最近,对于能够在许多语言对之间进行翻译的多语言模型越来越感兴趣。

代码语言:javascript
复制
>>> 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="t5-small")
>>> translator(text)
[{'translation_text': "Hugging Face est une tribune communautaire de l'apprentissage des machines."}]
语言建模

语言建模是一种预测文本序列中单词的任务。它已经成为非常流行的自然语言处理任务,因为预训练的语言模型可以为许多其他下游任务进行微调。最近,对大型语言模型(LLMs)产生了很大兴趣,这些模型展示了零或少量样本学习。这意味着模型可以解决它没有明确训练的任务!语言模型可用于生成流畅且令人信服的文本,尽管您需要小心,因为文本可能并不总是准确的。

有两种类型的语言建模:

因果:模型的目标是预测序列中的下一个标记,未来的标记被掩盖。

代码语言:javascript
复制
>>> 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

掩码:模型的目标是在完全访问序列中的标记的情况下预测序列中的掩码标记

代码语言:javascript
复制
>>> 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.'}]

多模态

多模态任务需要模型处理多种数据模态(文本、图像、音频、视频)来解决特定问题。图像字幕是一个多模态任务的例子,模型以图像作为输入,并输出描述图像或图像某些属性的文本序列。

尽管多模态模型处理不同的数据类型或模态,但在内部,预处理步骤帮助模型将所有数据类型转换为嵌入(包含有关数据的有意义信息的向量或数字列表)。对于像图像字幕这样的任务,模型学习图像嵌入和文本嵌入之间的关系。

文档问答

文档问答是一个从文档中回答自然语言问题的任务。与以文本作为输入的标记级别问答任务不同,文档问答以文档的图像作为输入,同时提出关于文档的问题,并返回一个答案。文档问答可用于解析结构化文档并从中提取关键信息。在下面的示例中,可以从收据中提取总金额和找零金额。

代码语言:javascript
复制
>>> from transformers import pipeline
>>> from PIL import Image
>>> import requests

>>> url = "https://datasets-server.huggingface.co/assets/hf-internal-testing/example-documents/--/hf-internal-testing--example-documents/test/2/image/image.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 如何解决任务

原文链接:huggingface.co/docs/transformers/v4.37.2/en/tasks_explained

在🤗 Transformers 能做什么中,您了解了自然语言处理(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 模型顶部添加一个序列分类头。分类头是一个线性层,接受编码器的隐藏状态。隐藏状态代表每个音频帧的学习特征,可以具有不同的长度。为了创建一个固定长度的向量,首先对隐藏状态进行汇总,然后将其转换为类标签上的 logits。在 logits 和目标之间计算交叉熵损失,以找到最可能的类别。

准备好尝试音频分类了吗?查看我们完整的音频分类指南,了解如何微调 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. 图像被分割成方形不重叠的补丁,每个补丁都被转换为一个向量或补丁嵌入。补丁嵌入是从一个卷积 2D 层生成的,该层创建了适当的输入维度(对于基础 Transformer 来说,每个补丁嵌入有 768 个值)。如果你有一个 224x224 像素的图像,你可以将其分割成 196 个 16x16 的图像补丁。就像文本被标记为单词一样,图像被“标记”为一系列补丁。
  2. 一个可学习的嵌入 - 一个特殊的[CLS]标记 - 被添加到补丁嵌入的开头,就像 BERT 一样。[CLS]标记的最终隐藏状态被用作附加分类头的输入;其他输出被忽略。这个标记帮助模型学习如何编码图像的表示。
  3. 将补丁和可学习嵌入的最后一件事是添加位置嵌入,因为模型不知道图像补丁的顺序。位置嵌入也是可学习的,并且与补丁嵌入具有相同的大小。最后,所有嵌入都传递给 Transformer 编码器。
  4. 输出,特别是只有带有[CLS]标记的输出,被传递到一个多层感知器头(MLP)。ViT 的预训练目标只是分类。像其他分类头一样,MLP 头将输出转换为类标签上的 logits,并计算交叉熵损失以找到最可能的类别。

准备尝试图像分类?查看我们完整的 image classification guide 来学习如何微调 ViT 并将其用于推断!

卷积神经网络

本节简要解释了卷积,但了解它们如何改变图像的形状和大小将会很有帮助。如果您对卷积不熟悉,请查看 fastai 书中的Convolution Neural Networks 章节

ConvNeXT 是一种采用新的现代网络设计来提高性能的 CNN 架构。然而,卷积仍然是模型的核心。从高层次的角度来看,卷积是一种操作,其中一个较小的矩阵()与图像像素的一个小窗口相乘。它从中计算一些特征,比如特定的纹理或线条的曲率。然后它滑动到下一个像素窗口;卷积移动的距离被称为步幅

A guide to convolution arithmetic for deep learning.中提取的不带填充或步幅的基本卷积。

您可以将此输出馈送到另一个卷积层,随着每个连续层,网络学习更复杂和抽象的事物,如热狗或火箭。在卷积层之间,通常会添加一个池化层来减少维度,并使模型更能够适应特征位置的变化。

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 骨干 接收一幅图像,由其像素值表示,并创建其低分辨率特征图。对特征图应用 1x1 卷积以降低维度,并创建具有高级图像表示的新特征图。由于 Transformer 是一个顺序模型,特征图被展平成一系列特征向量,这些向量与位置嵌入相结合。
  2. 特征向量传递给编码器,编码器使用其注意力层学习图像表示。接下来,编码器隐藏状态与解码器中的对象查询相结合。对象查询是学习的嵌入,专注于图像的不同区域,并在通过每个注意力层时更新。解码器隐藏状态传递给一个前馈网络,该网络预测每个对象查询的边界框坐标和类别标签,或者如果没有对象则为无对象。 DETR 并行解码每个对象查询以输出N个最终预测,其中N是查询的数量。与典型的自回归模型一次预测一个元素不同,目标检测是一个集合预测任务(边界框类别标签),在一次传递中进行N次预测。
  3. DETR 在训练过程中使用二部匹配损失来比较固定数量的预测和固定的一组真实标签。如果在N个标签集中有更少的真实标签,则它们将用无对象类进行填充。这个损失函数鼓励 DETR 找到预测和真实标签之间的一对一分配。如果边界框或类别标签不正确,则会产生损失。同样,如果 DETR 预测了一个不存在的对象,它将受到惩罚。这鼓励 DETR 在图像中找到其他对象,而不是专注于一个非常突出的对象。

在 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 还使用学习的对象查询,并将它们与像素解码器的图像特征组合以进行一组预测(类标签掩码预测)。解码器隐藏状态被传递到线性层,并转换为类标签上的逻辑。计算逻辑和类标签之间的交叉熵损失以找到最可能的类标签。 掩码预测是通过将像素嵌入与最终解码器隐藏状态相结合生成的。通过逻辑和地面真实掩码之间计算 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 使用两个目标进行预训练:掩码语言建模和下一句预测。在掩码语言建模中,输入标记的一定百分比被随机掩盖,模型需要预测这些标记。这解决了双向性的问题,其中模型可以作弊并看到所有单词并“预测”下一个单词。预测的掩码标记的最终隐藏状态传递给一个具有词汇表上的 softmax 的前馈网络,以预测掩码单词。 第二个预训练目标是下一句预测。模型必须预测句子 B 是否跟在句子 A 后面。一半的时间句子 B 是下一个句子,另一半的时间,句子 B 是一个随机句子。预测,无论是下一个句子还是不是,都传递给一个具有两个类别(IsNextNotNext)的 softmax 的前馈网络。
  3. 输入嵌入通过多个编码器层传递以输出一些最终隐藏状态。

要使用预训练模型进行文本分类,需要在基本 BERT 模型的顶部添加一个序列分类头。序列分类头是一个线性层,接受最终的隐藏状态,并执行线性变换以将它们转换为对数。在对数和目标之间计算交叉熵损失,以找到最可能的标签。

准备好尝试文本分类了吗?查看我们完整的文本分类指南来学习如何微调 DistilBERT 并将其用于推理!

标记分类

要将 BERT 用于像命名实体识别(NER)这样的标记分类任务,需要在基本 BERT 模型的顶部添加一个标记分类头。标记分类头是一个线性层,接受最终的隐藏状态,并执行线性变换以将它们转换为对数。在对数和每个标记之间计算交叉熵损失,以找到最可能的标签。

准备好尝试标记分类了吗?查看我们完整的 token 分类指南来学习如何微调 DistilBERT 并将其用于推理!

问答

要将 BERT 用于问答,需要在基本 BERT 模型的顶部添加一个跨度分类头。这个线性层接受最终的隐藏状态,并执行线性变换以计算与答案对应的span起始和结束对数。在对数和标签位置之间计算交叉熵损失,以找到与答案对应的最可能跨度的文本。

准备好尝试问答了吗?查看我们完整的问答指南来学习如何微调 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 可以应用任何类型的破坏。然而,文本填充破坏策略效果最好。在文本填充中,一些文本段被替换为一个单个mask令牌。这很重要,因为模型必须预测被屏蔽的令牌,并且它教会模型预测缺失令牌的数量。输入嵌入和屏蔽的段通过编码器传递以输出一些最终隐藏状态,但与 BERT 不同,BART 不会在最后添加最终的前馈网络来预测一个单词。
  2. 编码器的输出传递给解码器,解码器必须预测编码器输出中的屏蔽令牌和任何未损坏的令牌。这提供了额外的上下文来帮助解码器恢复原始文本。解码器的输出传递给语言建模头部,执行线性转换将隐藏状态转换为 logits。在 logits 和标签之间计算交叉熵损失,标签只是向右移动的令牌。

准备好尝试摘要了吗?查看我们完整的摘要指南来学习如何微调 T5 并将其用于推理!

有关文本生成的更多信息,请查看文本生成策略指南!

翻译

翻译是另一个序列到序列任务的例子,这意味着您可以使用像 BART 或 T5 这样的编码器-解码器模型来执行。我们将在本节中解释 BART 的工作原理,然后您可以在最后尝试微调 T5。

BART 通过添加一个单独的随机初始化编码器来适应翻译,将源语言映射到一个可以解码为目标语言的输入。这个新编码器的嵌入被传递给预训练编码器,而不是原始词嵌入。源编码器通过使用模型输出的交叉熵损失来更新源编码器、位置嵌入和输入嵌入进行训练。在这一步中,模型参数被冻结,所有模型参数在第二步中一起训练。

BART 之后推出了多语言版本 mBART,旨在用于翻译并在许多不同语言上进行预训练。

准备尝试翻译吗?查看我们的完整翻译指南来学习如何微调 T5 并将其用于推理!

要了解更多关于文本生成的信息,请查看文本生成策略指南!

Transformer 模型家族

原始文本:huggingface.co/docs/transformers/v4.37.2/en/model_summary

自 2017 年引入原始 Transformer模型以来,它已经激发了许多新颖且令人兴奋的模型,超越了自然语言处理(NLP)任务。有用于预测蛋白质的折叠结构训练猎豹奔跑时间序列预测的模型。有这么多 Transformer 变体可用,很容易忽略更大的画面。所有这些模型的共同之处是它们都基于原始 Transformer 架构。一些模型只使用编码器或解码器,而其他一些则同时使用两者。这提供了一个有用的分类法,可以对 Transformer 家族中的模型进行分类和检查高层次的差异,这将帮助您理解以前未遇到的 Transformer。

如果您不熟悉原始 Transformer 模型或需要复习,请查看 Hugging Face 课程中的Transformer 工作原理章节。

www.youtube.com/embed/H39Z_720T5s

计算机视觉

www.figma.com/embed?embed_host=share&url=https%3A%2F%2Fwww.figma.com%2Ffile%2FacQBpeFBVvrDUlzFlkejoz%2FModelscape-timeline%3Fnode-id%3D0%253A1%26t%3Dm0zJ7m2BQ9oe0WtO-1

卷积网络

长时间以来,卷积网络(CNNs)一直是计算机视觉任务的主导范式,直到视觉 Transformer展示了其可扩展性和效率。即使如此,CNN 的一些最佳特性,如平移不变性,是如此强大(尤其对于某些任务),以至于一些 Transformer 在其架构中引入了卷积。ConvNeXt 颠倒了这种交换,并从 Transformer 中引入设计选择来现代化 CNN。例如,ConvNeXt 使用非重叠滑动窗口将图像分块化,并使用更大的内核来增加其全局感受野。ConvNeXt 还做出了几个层设计选择,以提高内存效率和性能,因此它与 Transformer 竞争有利!

编码器

视觉 Transformer(ViT)为计算机视觉任务打开了没有卷积的大门。ViT 使用标准 Transformer 编码器,但其主要突破在于它如何处理图像。它将图像分割成固定大小的补丁,并使用它们创建嵌入,就像将句子分割成标记一样。ViT 利用 Transformer 的高效架构展示了与当时的 CNN 竞争力的结果,同时需要更少的资源进行训练。ViT 很快被其他视觉模型跟随,这些模型也可以处理像分割和检测这样的密集视觉任务。

其中一个模型是 Swin Transformer。它从较小的补丁中构建分层特征图(类似于 CNN👀,不同于 ViT),并在更深层中将它们与相邻的补丁合并。注意力仅在局部窗口内计算,并且在注意力层之间移动窗口以创建连接以帮助模型学习更好。由于 Swin Transformer 可以生成分层特征图,因此它是密集预测任务(如分割和检测)的良好候选。SegFormer 也使用 Transformer 编码器构建分层特征图,但它在顶部添加了一个简单的多层感知器(MLP)解码器,以组合所有特征图并进行预测。

其他视觉模型,如 BeIT 和 ViTMAE,从 BERT 的预训练目标中汲取灵感。BeIT 通过masked image modeling (MIM) 进行预训练;图像补丁被随机屏蔽,图像也被标记为视觉标记。BeIT 被训练以预测与被屏蔽补丁对应的视觉标记。ViTMAE 有一个类似的预训练目标,只是它必须预测像素而不是视觉标记。不寻常的是,75%的图像补丁被屏蔽!解码器从被屏蔽的标记和编码的补丁中重建像素。预训练后,解码器被丢弃,编码器准备好用于下游任务。

解码器

仅解码器的视觉模型很少,因为大多数视觉模型依赖编码器来学习图像表示。但对于像图像生成这样的用例,解码器是一个自然的选择,正如我们从 GPT-2 等文本生成模型中看到的那样。ImageGPT 使用与 GPT-2 相同的架构,但它不是预测序列中的下一个标记,而是预测图像中的下一个像素。除了图像生成,ImageGPT 也可以进行微调以用于图像分类。

编码器-解码器

视觉模型通常使用编码器(也称为骨干)来提取重要的图像特征,然后将它们传递给 Transformer 解码器。DETR 有一个预训练的骨干,但它还使用完整的 Transformer 编码器-解码器架构进行目标检测。编码器学习图像表示,并将其与对象查询(每个对象查询是一个专注于图像中的区域或对象的学习嵌入)结合在解码器中。DETR 预测每个对象查询的边界框坐标和类别标签。

自然语言处理

www.figma.com/embed?embed_host=share&url=https%3A%2F%2Fwww.figma.com%2Ffile%2FUhbQAZDlpYW5XEpdFy6GoG%2Fnlp-model-timeline%3Fnode-id%3D0%253A1%26t%3D4mZMr4r1vDEYGJ50-1

编码器

BERT 是一个仅包含编码器的 Transformer,它会随机屏蔽输入中的某些标记,以避免看到其他标记,这样可以防止其“作弊”。预训练的目标是基于上下文预测被屏蔽的标记。这使得 BERT 能够充分利用左右上下文来帮助学习输入的更深层和更丰富的表示。然而,BERT 的预训练策略仍有改进的空间。RoBERTa 通过引入一个新的预训练配方来改进这一点,该配方包括更长时间和更大批次的训练,在每个时代随机屏蔽标记,而不仅仅是在预处理期间一次,以及移除下一个句子预测目标。

提高性能的主要策略是增加模型大小。但是训练大型模型在计算上是昂贵的。减少计算成本的一种方法是使用像 DistilBERT 这样的较小模型。DistilBERT 使用知识蒸馏 - 一种压缩技术 - 来创建一个较小版本的 BERT,同时保留几乎所有的语言理解能力。

然而,大多数 Transformer 模型继续朝着更多参数的方向发展,导致出现了专注于提高训练效率的新模型。ALBERT 通过两种方式降低参数数量来减少内存消耗:将更大的词汇嵌入分为两个较小的矩阵,并允许层共享参数。DeBERTa 添加了一个解耦的注意机制,其中单词及其位置分别编码在两个向量中。注意力是从这些单独的向量计算而来,而不是从包含单词和位置嵌入的单个向量中计算。Longformer 也专注于使注意力更加高效,特别是用于处理具有更长序列长度的文档。它使用局部窗口注意力(仅计算围绕每个标记的固定窗口大小的注意力)和全局注意力(仅用于特定任务标记,如[CLS]用于分类)的组合,以创建一个稀疏的注意力矩阵,而不是完整的注意力矩阵。

解码器

GPT-2 是一个仅解码器的 Transformer,用于预测序列中的下一个单词。它会屏蔽右侧的标记,以防模型通过向前查看来“作弊”。通过在大量文本上进行预训练,GPT-2 在生成文本方面表现得非常出色,即使文本有时并不准确或真实。但是 GPT-2 缺乏 BERT 预训练的双向上下文,这使得它不适用于某些任务。XLNET 结合了 BERT 和 GPT-2 的预训练目标的优点,使用排列语言建模目标(PLM)使其能够双向学习。

在 GPT-2 之后,语言模型变得更大,现在被称为大型语言模型(LLMs)。如果在足够大的数据集上进行预训练,LLMs 可以展示少量甚至零-shot 学习。GPT-J 是一个具有 6B 参数并在 400B 标记上训练的 LLM。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%的标记)进行预训练。

音频

www.figma.com/embed?embed_host=share&url=https%3A%2F%2Fwww.figma.com%2Ffile%2Fvrchl8jDV9YwNVPWu2W0kK%2Fspeech-and-audio-model-timeline%3Fnode-id%3D0%253A1%26t%3DmM4H8pPMuK23rClL-1

编码器

Wav2Vec2 使用 Transformer 编码器直接从原始音频波形中学习语音表示。它通过对比任务进行预训练,以确定一组错误的语音表示中的真实语音表示。HuBERT 类似于 Wav2Vec2,但训练过程不同。目标标签是通过聚类步骤创建的,其中相似音频片段被分配到一个成为隐藏单元的簇中。隐藏单元被映射到一个嵌入以进行预测。

编码器-解码器

Speech2Text 是一个专为自动语音识别(ASR)和语音翻译设计的语音模型。该模型接受从音频波形中提取的对数梅尔滤波器特征,并预训练自回归地生成转录或翻译。Whisper 也是一个 ASR 模型,但与许多其他语音模型不同,它是在大量✨标记的✨音频转录数据上进行预训练,以实现零样本性能。数据集中还包含大量非英语语言,这意味着 Whisper 也可以用于资源稀缺的语言。在结构上,Whisper 类似于 Speech2Text。音频信号被转换为由编码器编码的对数梅尔频谱图。解码器从编码器的隐藏状态和先前的标记中自回归地生成转录。

多模态

www.figma.com/embed?embed_host=share&url=https%3A%2F%2Fwww.figma.com%2Ffile%2FcX125FQHXJS2gxeICiY93p%2Fmultimodal%3Fnode-id%3D0%253A1%26t%3DhPQwdx3HFPWJWnVf-1

编码器

VisualBERT 是一个用于视觉-语言任务的多模态模型,发布于 BERT 之后不久。它结合了 BERT 和一个预训练的目标检测系统,将图像特征提取为视觉嵌入,与文本嵌入一起传递给 BERT。VisualBERT 基于未屏蔽的文本和视觉嵌入预测被屏蔽的文本,并且还必须预测文本是否与图像对齐。当 ViT 发布时,ViLT 采用了 ViT 的架构,因为这样更容易获取图像嵌入。图像嵌入与文本嵌入一起进行处理。从那里,ViLT 通过图像文本匹配、屏蔽语言建模和整词屏蔽进行预训练。

CLIP 采用了不同的方法,对(图像文本)进行一对预测。一个图像编码器(ViT)和一个文本编码器(Transformer)在一个包含 4 亿个(图像文本)对的数据集上进行联合训练,以最大化(图像文本)对的图像和文本嵌入之间的相似性。在预训练之后,您可以使用自然语言指示 CLIP 预测给定图像的文本,反之亦然。OWL-ViT 在 CLIP 的基础上构建,将其作为零样本目标检测的骨干。在预训练之后,添加了一个目标检测头,以对(类别边界框)对进行集合预测。

编码器-解码器

光学字符识别(OCR)是一个长期存在的文本识别任务,通常涉及几个组件来理解图像并生成文本。TrOCR 使用端到端的变换器简化了这个过程。编码器是一种 ViT 风格的模型,用于图像理解,并将图像处理为固定大小的补丁。解码器接受编码器的隐藏状态,并自回归地生成文本。Donut 是一个更通用的视觉文档理解模型,不依赖于基于 OCR 的方法。它使用 Swin 变换器作为编码器,多语言 BART 作为解码器。Donut 经过预训练,通过根据图像和文本注释预测下一个单词来阅读文本。解码器根据提示生成一个令牌序列。提示由每个下游任务的特殊令牌表示。例如,文档解析有一个特殊的parsing令牌,它与编码器的隐藏状态结合,将文档解析为结构化的输出格式(JSON)。

强化学习

www.figma.com/embed?embed_host=share&url=https%3A%2F%2Fwww.figma.com%2Ffile%2FiB3Y6RvWYki7ZuKO6tNgZq%2Freinforcement-learning%3Fnode-id%3D0%253A1%26t%3DhPQwdx3HFPWJWnVf-1

解码器

决策和轨迹转换器将状态、动作和奖励视为序列建模问题。决策变换器生成一系列动作,这些动作基于回报、过去状态和动作,导致未来期望的回报。在最后K个时间步中,这三种模态都被转换为令牌嵌入,并由类似 GPT 的模型处理,以预测未来的动作令牌。轨迹变换器也对状态、动作和奖励进行标记化,并使用 GPT 架构处理它们。与专注于奖励条件的决策变换器不同,轨迹变换器使用波束搜索生成未来的动作。

分词器总结

原文链接:huggingface.co/docs/transformers/v4.37.2/en/tokenizer_summary

在这个页面上,我们将更仔细地看一下分词。

www.youtube-nocookie.com/embed/VFp38yj8h3A

正如我们在预处理教程中看到的,将文本分词是将其分割成单词或子词,然后通过查找表将其转换为 id。将单词或子词转换为 id 是直接的,因此在本摘要中,我们将专注于将文本分割成单词或子词(即文本分词)。更具体地说,我们将看一下🤗 Transformers 中使用的三种主要分词器类型:Byte-Pair Encoding (BPE)、WordPiece 和 SentencePiece,并展示哪种模型使用了哪种分词器类型的示例。

请注意,在每个模型页面上,您可以查看相关分词器的文档,以了解预训练模型使用了哪种分词器类型。例如,如果我们查看 BertTokenizer,我们可以看到该模型使用了 WordPiece。

介绍

将文本分割成较小的块是一项看起来比较困难的任务,有多种方法可以实现。例如,让我们看一下句子"Don't you love 🤗 Transformers? We sure do."

www.youtube-nocookie.com/embed/nhJxYji1aho

将这段文本简单地按空格分割,会得到:

代码语言:javascript
复制
["Don't", "you", "love", "🤗", "Transformers?", "We", "sure", "do."]

这是一个明智的第一步,但是如果我们看一下标记"Transformers?""do.",我们会注意到标点符号附加在单词"Transformer""do"上,这是不够理想的。我们应该考虑标点符号,这样模型就不必学习一个单词的不同表示以及可能跟随它的每个可能的标点符号,这将导致模型需要学习的表示数量激增。考虑标点符号,对我们的示例文本进行分词会得到:

代码语言:javascript
复制
["Don", "'", "t", "you", "love", "🤗", "Transformers", "?", "We", "sure", "do", "."]

更好。然而,分词处理单词"Don't"的方式是不利的。"Don't"代表"do not",所以最好将其分词为["Do", "n't"]。这就是事情开始变得复杂的地方,也是每个模型都有自己的分词器类型的原因之一。根据我们应用于文本分词的规则,相同文本会生成不同的分词输出。预训练模型只有在输入与训练数据分词时使用的规则相同的情况下才能正常运行。

spaCyMoses是两种流行的基于规则的分词器。在我们的示例上应用它们,spaCyMoses可能会输出类似以下内容:

代码语言:javascript
复制
["Do", "n't", "you", "love", "🤗", "Transformers", "?", "We", "sure", "do", "."]

可以看到,这里使用了空格和标点分词,以及基于规则的分词。空格和标点分词以及基于规则的分词都是单词分词的示例,它们被宽泛地定义为将句子分割成单词。虽然这是将文本分割成较小块的最直观的方法,但这种分词方法可能会导致大规模文本语料库出现问题。在这种情况下,空格和标点分词通常会生成一个非常庞大的词汇表(所有使用的唯一单词和标记的集合)。例如,Transformer XL 使用空格和标点分词,导致词汇量为 267,735!

如此庞大的词汇量迫使模型具有巨大的嵌入矩阵作为输入和输出层,这会导致内存和时间复杂度增加。一般来说,transformers 模型很少有词汇量超过 50,000 的情况,尤其是如果它们只在单一语言上进行了预训练。

因此,如果简单的空格和标点符号分词不尽如人意,为什么不简单地在字符上进行分词呢?

www.youtube-nocookie.com/embed/ssLq_EK2jLE

虽然字符分词非常简单且可以大大减少内存和时间复杂度,但这使得模型更难学习有意义的输入表示。例如,学习字母"t"的有意义的上下文无关表示要比学习单词"today"的上下文无关表示困难得多。因此,字符分词通常伴随着性能损失。因此,为了兼顾两者的优势,transformers 模型使用了介于单词级和字符级分词之间的混合称为子词分词的方法。

子词分词

www.youtube-nocookie.com/embed/zHvTiHr506c

子词分词算法依赖于这样一个原则,即经常使用的单词不应该被分割为更小的子词,但罕见的单词应该被分解为有意义的子词。例如,"annoyingly"可能被认为是一个罕见单词,可以被分解为"annoying""ly""annoying""ly"作为独立的子词会更频繁地出现,同时"annoyingly"的含义由"annoying""ly"的组合含义保留。这在像土耳其这样的聚合语言中特别有用,您可以通过串联子词形成(几乎)任意长的复杂单词。

子词分词使模型能够拥有合理的词汇量,同时能够学习有意义的上下文无关表示。此外,子词分词使模型能够处理它以前从未见过的单词,通过将它们分解为已知的子词。例如,BertTokenizer 将"I have a new GPU!"分词如下:

代码语言:javascript
复制
>>> from transformers import BertTokenizer

>>> tokenizer = BertTokenizer.from_pretrained("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 将我们之前的示例文本分词如下:

代码语言:javascript
复制
>>> from transformers import XLNetTokenizer

>>> tokenizer = XLNetTokenizer.from_pretrained("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)是在Neural Machine Translation of Rare Words with Subword Units (Sennrich et al., 2015)中引入的。BPE 依赖于一个预分词器,将训练数据分割成单词。预分词可以简单到空格分词,例如 GPT-2,RoBERTa。更高级的预分词包括基于规则的分词,例如 XLM,FlauBERT 使用 Moses 用于大多数语言,或者 GPT 使用 Spacy 和 ftfy,来计算训练语料库中每个单词的频率。

在预分词之后,已创建了一组唯一的单词,并确定了每个单词在训练数据中出现的频率。接下来,BPE 创建一个基本词汇,其中包含所有出现在唯一单词集合中的符号,并学习合并规则,以从基本词汇的两个符号形成一个新符号。它会一直这样做,直到词汇表达到所需的词汇量。请注意,所需的词汇量是在训练分词器之前定义的一个超参数。

举个例子,假设在预分词之后,已确定了以下包含频率的单词集合:

代码语言:javascript
复制
("hug", 10), ("pug", 5), ("pun", 12), ("bun", 4), ("hugs", 5)

因此,基本词汇是["b", "g", "h", "n", "p", "s", "u"]。将所有单词分割为基本词汇的符号,我们得到:

代码语言:javascript
复制
("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次(在 10 次"hug"出现中的 10 次,以及在 5 次"hugs"出现中的 5 次)。然而,最频繁的符号对是"u"后跟"g",总共出现了10 + 5 + 5 = 20次。因此,分词器学习的第一个合并规则是将所有跟在"u"符号后面的"g"符号组合在一起。接下来,"ug"被添加到词汇表中。然后单词集合变为

代码语言:javascript
复制
("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"],我们的唯一单词集合表示为

代码语言:javascript
复制
("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 的分词器可以对每个文本进行分词,而无需使用符号。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 略有不同,因为它评估合并两个符号会损失什么,以确保值得。

Unigram

Unigram 是一种子词分词算法,由Kudo, 2018引入。与 BPE 或 WordPiece 相比,Unigram 将其基本词汇初始化为大量符号,并逐渐修剪每个符号以获得较小的词汇表。基本词汇表可以对应于所有预分词的单词和最常见的子字符串。Unigram 不直接用于 transformers 中的任何模型,但与 SentencePiece 一起使用。

在每个训练步骤中,Unigram 算法根据当前词汇表和 unigram 语言模型定义了一个损失(通常定义为对数似然)。然后,对于词汇表中的每个符号,该算法计算如果将该符号从词汇表中移除会导致整体损失增加多少。Unigram 然后删除损失增加最低的 p(通常为 10%或 20%)百分比的符号,即那些对训练数据整体损失影响最小的符号。这个过程重复进行,直到词汇表达到所需大小。Unigram 算法始终保留基本字符,以便任何单词都可以被分词。

由于 Unigram 不基于合并规则(与 BPE 和 WordPiece 相反),该算法在训练后有几种分词新文本的方式。例如,如果经过训练的 Unigram 分词器展示以下词汇表:

代码语言:javascript
复制
["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=1∑N​log​x∈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 或 unigram 算法构建适当的词汇表。

XLNetTokenizer 例如使用了 SentencePiece,这也是为什么在前面的例子中包含了 "▁" 字符在词汇表中。使用 SentencePiece 进行解码非常容易,因为所有标记只需连接在一起,而 "▁" 被替换为一个空格。

库中所有使用 SentencePiece 的变压器模型都与 unigram 结合使用。使用 SentencePiece 的模型示例包括 ALBERT, XLNet, Marian 和 T5。

注意机制

原文链接:huggingface.co/docs/transformers/v4.37.2/en/attention

大多数 transformer 模型在注意力矩阵是方形的意义上使用全注意力。当处理长文本时,这可能是一个巨大的计算瓶颈。Longformer 和 reformer 是试图更高效并使用注意力矩阵的稀疏版本来加速训练的模型。

LSH 注意力

Reformer 使用 LSH 注意力。在 softmax(QK^t) 中,只有矩阵 QK^t 中最大的元素(在 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+d2d_{1} + d_{2} = dd1​+d2​=d(长度的乘积使得结果变得更小)。在矩阵 E 中,时间步jjj 的嵌入是通过将 E1 中时间步j%l1j % l1j%l1 的嵌入和 E2 中时间步j//l1j // l1j//l1 的嵌入进行连接获得的。

填充和截断

原文链接:huggingface.co/docs/transformers/v4.37.2/en/pad_truncation

批量输入通常具有不同的长度,因此无法转换为固定大小的张量。填充和截断是处理此问题的策略,以从不同长度的批次创建矩形张量。填充添加一个特殊的填充标记,以确保较短的序列将具有与批次中最长序列或模型接受的最大长度相同的长度。截断则是截断长序列。

在大多数情况下,将批次填充到最长序列的长度,并截断到模型可以接受的最大长度通常效果很好。但是,如果需要,API 支持更多策略。您需要的三个参数是:paddingtruncationmax_length

padding参数控制填充。它可以是布尔值或字符串:

  • True'longest':填充到批次中最长的序列(如果只提供单个序列,则不会应用填充)。
  • 'max_length':通过max_length参数指定的长度填充,或者如果没有提供max_length,则填充到模型接受的最大长度(max_length=None)。如果只提供单个序列,仍将应用填充。
  • False'do_not_pad':不会应用填充。这是默认行为。

truncation参数控制截断。它可以是布尔值或字符串:

  • True'longest_first':通过max_length参数指定的最大长度截断,或者如果没有提供max_length,则截断到模型接受的最大长度(max_length=None)。这将逐标记截断,从一对中最长的序列中删除一个标记,直到达到适当的长度。
  • 'only_second':通过max_length参数指定的最大长度截断,或者如果没有提供max_length,则截断到模型接受的最大长度(max_length=None)。如果提供了一对序列(或一批序列对),则只会截断第二句。
  • 'only_first': 通过max_length参数指定的最大长度截断,或者如果没有提供max_length,则截断到模型接受的最大长度(max_length=None)。如果提供了一对序列(或一批序列对),则只会截断第一句。
  • 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='max_length', max_length=42)

填充到值的倍数

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

原文:huggingface.co/docs/transformers/v4.37.2/en/bertology

有一个不断增长的研究领域,关注调查大规模变压器(如 BERT)的内部工作(有些人称之为“BERTology”)。 这个领域的一些很好的例子是:

为了帮助这个新领域发展,我们在 BERT/GPT/GPT-2 模型中添加了一些额外功能,以帮助人们访问内部表示,主要是从 Paul Michel 的伟大工作中改编的(arxiv.org/abs/1905.10650):

  • 访问 BERT/GPT/GPT-2 的所有隐藏状态,
  • 访问 BERT/GPT/GPT-2 每个头的所有注意权重,
  • 检索头输出值和梯度,以便计算头重要性分数并修剪头,如arxiv.org/abs/1905.10650中所解释的。

为了帮助您理解和使用这些功能,我们添加了一个特定的示例脚本:bertology.py,同时提取在 GLUE 上预训练的模型的信息并修剪。

固定长度模型的困惑度

原始文本:huggingface.co/docs/transformers/v4.37.2/en/perplexity

困惑度(PPL)是评估语言模型最常见的指标之一。在深入讨论之前,我们应该注意,该指标特别适用于传统语言模型(有时称为自回归或因果语言模型),对于像 BERT 这样的掩码语言模型,该指标并不明确定义(请参阅模型摘要)。

困惑度被定义为序列的指数化平均负对数似然。如果我们有一个标记化的序列X=(x0,x1,…,xt)X = (x_0, x_1, \dots, x_t)X=(x0​,x1​,…,xt​),那么XXX 的困惑度为,PPL(X)=exp⁡{−1t∑itlog⁡pθ(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{−t1​i∑t​logpθ​(xi​∣x<i​)}

其中log⁡pθ(xi∣x<i)\log p_\theta (x_i|x_{<i})是第 i 个标记在我们模型的先前标记x<ix_{<i}的对数似然。直观地,它可以被视为模型在语料库中一组指定标记中均匀预测的评估。重要的是,这意味着标记化过程对模型的困惑度有直接影响,比较不同模型时应始终考虑这一点。

这也等同于数据和模型预测之间交叉熵的指数。关于困惑度及其与每字符比特(BPC)和数据压缩的关系的更多直觉,请查看The Gradient上的这篇精彩博客文章

使用固定长度模型计算 PPL

如果我们不受模型上下文大小的限制,我们将通过自回归地分解序列并在每一步上都对整个先前子序列进行条件评估模型的困惑度,如下所示。

具有无限上下文长度的序列的完全分解
具有无限上下文长度的序列的完全分解

然而,在使用近似模型时,通常会对模型可以处理的标记数量有限制。例如,GPT-2 的最大版本具有固定长度的 1024 个标记,因此当 t 大于 1024 时,我们无法直接计算 pθ​(xt​∣x<t​)。

相反,序列通常被分成与模型最大输入大小相等的子序列。如果模型的最大输入大小为 k,那么我们通过仅在之前的 k-1 个标记上进行条件化来近似标记 xt​的可能性,而不是整个上下文。在评估序列的模型困惑度时,一种诱人但次优的方法是将序列分成不相交的块,并独立地将每个段的分解对数似然相加。

不利用完全可用上下文的次优 PPL

这种方法计算快速,因为每个段的困惑度可以在一个前向传递中计算,但作为完全因子化困惑度的一个很差的近似,并且通常会产生更高(更差)的 PPL,因为模型在大多数预测步骤中将具有更少的上下文。

相反,应该使用滑动窗口策略评估固定长度模型的 PPL。这涉及反复滑动上下文窗口,以便模型在进行每个预测时具有更多上下文。

利用所有可用上下文的滑动窗口 PPL

这是对序列概率的真实分解的更接近近似,并且通常会产生更有利的分数。缺点是它需要为语料库中的每个标记进行单独的前向传递。一个很好的实际折衷方案是使用跨度滑动窗口,通过更大的跨度移动上下文,而不是每次滑动一个标记。这样可以使计算速度更快,同时仍然使模型在每一步中具有更大的上下文来进行预测。

示例:在🤗 Transformers 中使用 GPT-2 计算困惑度

让我们用 GPT-2 演示这个过程。

代码语言:javascript
复制
from transformers import GPT2LMHeadModel, GPT2TokenizerFast

device = "cuda"
model_id = "gpt2-large"
model = GPT2LMHeadModel.from_pretrained(model_id).to(device)
tokenizer = GPT2TokenizerFast.from_pretrained(model_id)

我们将加载 WikiText-2 数据集,并使用几种不同的滑动窗口策略评估困惑度。由于这个数据集很小,我们只需对整个数据集进行一次前向传递,因此可以将整个数据集加载和编码到内存中。

代码语言:javascript
复制
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 个先前的标记可用于条件)。

代码语言:javascript
复制
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。这不仅是一个更有利的分数,而且计算方式更接近于序列可能性的真实自回归分解。

为 Web 服务器使用管道

原文:huggingface.co/docs/transformers/v4.37.2/en/pipeline_webserver

创建推断引擎是一个复杂的主题,“最佳”解决方案很可能取决于您的问题空间。您是在 CPU 还是 GPU 上?您想要最低的延迟、最高的吞吐量、对许多模型的支持,还是只是高度优化一个特定模型?解决这个问题的方法有很多,因此我们将提供一个很好的默认值来开始,这可能不一定是最优的解决方案。

关键的理解是,我们可以使用一个迭代器,就像你在数据集上使用的一样,因为 Web 服务器基本上是一个等待请求并按顺序处理它们的系统。

通常,Web 服务器是多路复用的(多线程、异步等),以处理各种请求。另一方面,管道(以及大多数底层模型)并不真正适合并行处理;它们占用大量 RAM,因此最好在运行时为它们提供所有可用的资源,或者这是一个计算密集型的工作。

我们将通过让 Web 服务器处理接收和发送请求的轻负载,并让单个线程处理实际工作来解决这个问题。这个示例将使用starlette。实际的框架并不是很重要,但如果您使用另一个框架来实现相同的效果,可能需要调整或更改代码。

创建server.py

代码语言:javascript
复制
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="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))

现在您可以启动它:

代码语言:javascript
复制
uvicorn server:app

你可以查询它:

代码语言:javascript
复制
curl -X POST -d "test [MASK]" http://localhost:8000/
#[{"score":0.7742936015129089,"token":1012,"token_str":".","sequence":"test."},...]

现在,您已经了解如何创建 Web 服务器了!

真正重要的是,我们只一次加载模型,因此在 Web 服务器上没有模型的副本。这样,就不会使用不必要的 RAM。然后,排队机制允许您执行一些花哨的操作,比如可能在推断之前累积一些项目以使用动态批处理:

下面的代码示例故意以伪代码形式编写,以提高可读性。在未检查是否对您的系统资源有意义的情况下,请勿运行此代码!

代码语言:javascript
复制
(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 推断 - 这是一个非常庞大的模型 - 动态批处理是必不可少的,以为每个人提供良好的体验。

模型训练解剖学

原文链接:huggingface.co/docs/transformers/v4.37.2/en/model_memory_anatomy

为了了解可以应用的性能优化技术,以提高模型训练速度和内存利用效率,有助于熟悉在训练期间 GPU 的使用情况,以及根据执行的操作而变化的计算强度。

让我们从探索 GPU 利用率和模型训练运行的一个激励示例开始。为了演示,我们需要安装一些库:

代码语言:javascript
复制
pip install transformers datasets accelerate nvidia-ml-py3

nvidia-ml-py3库允许我们从 Python 内部监视模型的内存使用情况。您可能熟悉终端中的nvidia-smi命令-这个库允许直接在 Python 中访问相同的信息。

然后,我们创建一些虚拟数据:100 到 30000 之间的随机标记 ID 和用于分类器的二进制标签。总共,我们得到 512 个长度为 512 的序列,并将它们存储在 PyTorch 格式的Dataset中。

代码语言:javascript
复制
>>> 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, 1, (dataset_size)),
... }
>>> ds = Dataset.from_dict(dummy_data)
>>> ds.set_format("pt")

为了打印 GPU 利用率和使用 Trainer 进行训练的摘要统计信息,我们定义了两个辅助函数:

代码语言:javascript
复制
>>> 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 内存开始:

代码语言:javascript
复制
>>> print_gpu_utilization()
GPU memory occupied: 0 MB.

看起来不错:GPU 内存没有被占用,正如我们在加载任何模型之前所期望的那样。如果在您的计算机上不是这种情况,请确保停止使用 GPU 内存的所有进程。然而,并非所有空闲 GPU 内存都可以被用户使用。当模型加载到 GPU 时,内核也会被加载,这可能占用 1-2GB 的内存。为了查看有多少内存被占用,我们将一个微小的张量加载到 GPU 中,这将触发内核的加载。

代码语言:javascript
复制
>>> import torch

>>> torch.ones((1, 1)).to("cuda")
>>> print_gpu_utilization()
GPU memory occupied: 1343 MB.

我们看到内核单独占用了 1.3GB 的 GPU 内存。现在让我们看看模型使用了多少空间。

加载模型

首先,我们加载bert-large-uncased模型。我们直接将模型权重加载到 GPU,以便我们可以检查仅权重使用了多少空间。

代码语言:javascript
复制
>>> from transformers import AutoModelForSequenceClassification

>>> model = AutoModelForSequenceClassification.from_pretrained("bert-large-uncased").to("cuda")
>>> print_gpu_utilization()
GPU memory occupied: 2631 MB.

我们可以看到模型权重单独占用了 1.3GB 的 GPU 内存。确切的数字取决于您使用的具体 GPU。请注意,在较新的 GPU 上,模型有时可能占用更多空间,因为权重以优化的方式加载,加快了模型的使用速度。现在我们还可以快速检查是否与nvidia-smi CLI 得到相同的结果:

代码语言:javascript
复制
nvidia-smi
代码语言:javascript
复制
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 内存消耗的变化。首先,我们设置一些标准的训练参数:

代码语言:javascript
复制
default_args = {
    "output_dir": "tmp",
    "evaluation_strategy": "steps",
    "num_train_epochs": 1,
    "log_level": "error",
    "report_to": "none",
}

如果您计划运行多个实验,为了在实验之间正确清除内存,请在实验之间重新启动 Python 内核。

在普通训练中的内存利用率

让我们使用 Trainer 并在不使用任何 GPU 性能优化技术的情况下训练模型,批量大小为 4:

代码语言:javascript
复制
>>> 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)
代码语言:javascript
复制
Time: 57.82
Samples/second: 8.86
GPU memory occupied: 14949 MB.

我们看到即使是一个相对较小的批量大小几乎填满了我们 GPU 的整个内存。然而,较大的批量大小通常会导致模型更快地收敛或获得更好的最终性能。因此,理想情况下,我们希望根据模型的需求而不是 GPU 的限制来调整批量大小。有趣的是,我们使用的内存比模型的大小要多得多。为了更好地理解为什么会这样,让我们看一下模型的操作和内存需求。

模型操作解剖学

Transformers 架构包括 3 个主要的操作组,根据计算强度分组如下。

  1. 张量收缩 线性层和多头注意力组件都进行批量矩阵-矩阵乘法。这些操作是训练 transformer 最计算密集的部分。
  2. 统计归一化 Softmax 和层归一化的计算密度低于张量收缩,并涉及一个或多个减少操作,其结果然后通过映射应用。
  3. 逐元素运算符 这些是剩余的运算符:偏置、丢弃、激活和残差连接。这些是计算密集度最低的操作。

这些知识在分析性能瓶颈时可能会有所帮助。

这个总结来源于数据移动就是你需要的一切:优化 Transformer 的案例研究 2020

模型内存的解剖

我们已经看到,训练模型使用的内存比仅将模型放在 GPU 上要多得多。这是因为在训练期间有许多组件使用 GPU 内存。在 GPU 内存中的组件包括以下内容:

  1. 模型权重
  2. 优化器状态
  3. 梯度
  4. 为梯度计算保存的前向激活
  5. 临时缓冲区
  6. 功能特定的内存

使用 AdamW 进行混合精度训练的典型模型每个模型参数需要 18 字节加上激活内存。对于推断,没有优化器状态和梯度,因此我们可以减去这些。因此,对于混合精度推断,每个模型参数需要 6 字节,加上激活内存。

让我们看看细节。

模型权重:

  • 每个参数的 4 字节,用于 fp32 训练
  • 每个参数的 6 字节,用于混合精度训练(在内存中维护一个 fp32 模型和一个 fp16 模型)

优化器状态:

  • 每个参数的 8 字节,用于普通的 AdamW(维护 2 个状态)
  • 每个参数的 8 位 AdamW 优化器需要 2 字节,例如bitsandbytes
  • 每个参数的 4 字节,用于像带有动量的 SGD 这样的优化器(仅维护 1 个状态)

梯度

  • 每个参数的 4 字节,用于 fp32 或混合精度训练(梯度始终保持在 fp32 中)

前向激活

  • 大小取决于许多因素,关键因素是序列长度、隐藏大小和批量大小。

这里有通过前向和后向函数传递和返回的输入和输出,以及为梯度计算保存的前向激活。

临时内存

此外,还有各种临时变量,一旦计算完成就会释放,但在某些时刻这些变量可能需要额外的内存并可能导致 OOM。因此,在编码时,战略性地考虑这些临时变量并有时明确释放那些不再需要的变量是至关重要的。

功能特定的内存

然后,您的软件可能有特殊的内存需求。例如,使用波束搜索生成文本时,软件需要维护多个输入和输出的副本。

前向后向执行速度

对于卷积和线性层,与前向相比,后向中的 flops 是前向的 2 倍,这通常会导致大约 2 倍的速度变慢(有时更多,因为后向中的大小往往更加尴尬)。激活通常受带宽限制,一个激活在后向中通常需要读取比前向更多的数据(例如,激活前向读取一次,写入一次,激活后向读取两次,gradOutput 和前向的输出,然后写入一次,gradInput)。

正如您所看到的,有一些地方我们可以节省 GPU 内存或加快操作速度。现在您已经了解了影响 GPU 利用率和计算速度的因素,请参考单个 GPU 上高效训练的方法和工具文档页面,了解性能优化技术。

优化 LLMs 的速度和内存

原文链接:huggingface.co/docs/transformers/v4.37.2/en/llm_tutorial_optimization

诸如 GPT3/4、FalconLlama等大型语言模型(LLMs)正在快速发展,能够处理以人类为中心的任务,成为现代知识型产业中不可或缺的工具。然而,在实际任务中部署这些模型仍然具有挑战性:

  • 为了展示接近人类文本理解和生成能力,目前 LLMs 需要由数十亿参数组成(参见Kaplan 等人Wei 等人)。这进一步增加了推断的内存需求。
  • 在许多现实世界的任务中,LLMs 需要提供广泛的上下文信息。这要求模型在推断过程中能够处理非常长的输入序列。

这些挑战的关键在于增强 LLMs 的计算和内存能力,特别是在处理庞大的输入序列时。

在本指南中,我们将介绍高效 LLM 部署的有效技术:

  1. **低精度:**研究表明,以降低的数值精度,即 8 位和 4 位可以在不显著降低模型性能的情况下实现计算优势。
  2. **快闪注意力:**快闪注意力是注意力算法的一种变体,不仅提供了更节省内存的方法,还通过优化 GPU 内存利用率实现了增加的效率。
  3. **架构创新:**考虑到 LLMs 在推断过程中始终以相同方式部署,即具有长输入上下文的自回归文本生成,已经提出了专门的模型架构,允许更高效的推断。在模型架构方面最重要的进展是AlibiRotary embeddings多查询注意力(MQA)分组查询注意力(GQA)

在本指南中,我们将从张量的角度对自回归生成进行分析。我们深入探讨采用低精度的利弊,全面探索最新的注意力算法,并讨论改进的 LLM 架构。在此过程中,我们运行实际示例展示每个功能改进。

1. 低精度

通过将 LLM 视为一组权重矩阵和向量,将文本输入视为一系列向量,可以更好地理解 LLMs 的内存需求。在接下来的内容中,定义权重将用于表示所有模型权重矩阵和向量。

在撰写本指南时,LLMs 至少包含数十亿参数。因此,每个参数由一个十进制数组成,例如4.5689,通常以float32bfloat16float16格式存储。这使我们能够轻松计算加载 LLM 到内存所需的内存量:

加载具有 X 十亿参数的模型的权重大约需要 4X GB 的 VRAM,精度为 float32*

如今,模型很少以完整的 float32 精度进行训练,而通常以 bfloat16 精度或更少的 float16 精度进行训练。因此,经验法则变为:

加载具有 X 十亿参数的模型的权重大约需要 2X GB 的 VRAM,精度为 bfloat16/float16*

对于较短的文本输入(少于 1024 个标记),推理的内存需求主要受加载权重的内存需求支配。因此,现在让我们假设推理的内存需求等于将模型加载到 GPU VRAM 中的内存需求。

举例说明加载 bfloat16 模型大致需要多少 VRAM:

截至撰写本文时,市场上最大的 GPU 芯片是 A100 & H100,提供 80GB 的 VRAM。之前列出的大多数模型需要超过 80GB 的内存才能加载,因此必然需要张量并行处理和/或管道并行处理。

🤗 Transformers 不支持张量并行处理,因为它要求模型架构以特定方式编写。如果您有兴趣以张量并行友好的方式编写模型,请随时查看文本生成推理库

天真的管道并行处理是开箱即用的。为此,只需使用 device="auto" 加载模型,它将自动将不同的层放置在可用的 GPU 上,如此处所述。请注意,尽管非常有效,但这种天真的管道并行处理并未解决 GPU 空闲的问题。为此,需要更高级的管道并行处理,如此处所述。

如果您可以访问一个 8 x 80GB A100 节点,您可以按照以下方式加载 BLOOM

代码语言:javascript
复制
!pip install transformers accelerate bitsandbytes optimum
代码语言:javascript
复制
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 的管道对象。

代码语言:javascript
复制
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)
代码语言:javascript
复制
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

输出

代码语言:javascript
复制
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```py\n\nThis function takes a single

很好,我们现在可以直接使用结果将字节转换为千兆字节。

代码语言:javascript
复制
def bytes_to_giga_bytes(bytes):
  return bytes / 1024 / 1024 / 1024

让我们调用torch.cuda.max_memory_allocated来测量 GPU 内存分配的峰值。

代码语言:javascript
复制
bytes_to_giga_bytes(torch.cuda.max_memory_allocated())

输出

代码语言:javascript
复制
29.0260648727417

接近我们粗略计算的结果!我们可以看到数字并不完全正确,因为从字节到千字节需要乘以 1024 而不是 1000。因此,粗略计算公式也可以理解为“最多 XGB”的计算。请注意,如果我们尝试以完整的 float32 精度运行模型,将需要 64GB 的 VRAM。

几乎所有模型现在都是在 bfloat16 中训练的,如果您的 GPU 支持 bfloat16,就没有理由以完整的 float32 精度运行模型。Float32 不会比用于训练模型的精度提供更好的推断结果。

如果您不确定模型权重以哪种格式存储在 Hub 上,您可以随时查看检查点的配置,在"torch_dtype"下,例如这里。建议在使用from_pretrained(..., torch_dtype=...)加载模型时,将模型设置为与配置中写入的相同精度类型,除非原始类型为 float32,此时可以在推断中使用float16bfloat16

让我们定义一个flush(...)函数来释放所有分配的内存,以便我们可以准确地测量分配的 GPU 内存峰值。

代码语言:javascript
复制
del pipe
del model

import gc
import torch

def flush():
  gc.collect()
  torch.cuda.empty_cache()
  torch.cuda.reset_peak_memory_stats()

现在让我们为下一个实验调用它。

代码语言:javascript
复制
flush()

在最近的 accelerate 库版本中,您还可以使用一个名为release_memory()的实用方法

代码语言:javascript
复制
from accelerate.utils import release_memory
# ...

release_memory(model)

那么如果您的 GPU 没有 32GB 的 VRAM 怎么办?已经发现模型权重可以量化为 8 位或 4 位而不会显著降低性能(参见Dettmers 等人)。正如最近的GPTQ 论文所示,模型可以量化为 3 位或 2 位,性能损失是可以接受的🤯。

不深入细节,量化方案旨在降低权重的精度,同时尽可能保持模型推断结果的准确性(即尽可能接近 bfloat16)。请注意,量化在文本生成方面特别有效,因为我们只关心选择最可能的下一个标记集,并不真正关心下一个标记logit分布的确切值。重要的是下一个标记logit分布保持大致相同,以便argmaxtopk操作给出相同的结果。

有各种量化技术,我们这里不会详细讨论,但总的来说,所有量化技术的工作方式如下:

    1. 将所有权重量化为目标精度
    1. 加载量化的权重,并以 bfloat16 精度传递输入序列的向量
    1. 动态将权重去量化为 bfloat16,以 bfloat16 精度执行计算

简而言之,这意味着输入-权重矩阵乘法,其中X X X 是输入,W W W 是权重矩阵,Y Y Y 是输出:Y=X∗W Y = X * W Y=X∗W

被改变为Y=X∗dequantize(W) Y = X * \text{dequantize}(W) Y=X∗dequantize(W)

对于每个矩阵乘法。当输入通过网络图时,权重矩阵的反量化和重新量化是按顺序执行的。

因此,当使用量化权重时,推理时间通常不会减少,而是增加。足够的理论,让我们试一试!要使用 Transformers 量化权重,您需要确保已安装bitsandbytes库。

代码语言:javascript
复制
!pip install bitsandbytes

然后,我们可以通过简单地在from_pretrained中添加load_in_8bit=True标志来加载 8 位量化的模型。

代码语言:javascript
复制
model = AutoModelForCausalLM.from_pretrained("bigcode/octocoder", load_in_8bit=True, pad_token_id=0)

现在,让我们再次运行我们的示例并测量内存使用情况。

代码语言:javascript
复制
pipe = pipeline("text-generation", model=model, tokenizer=tokenizer)

result = pipe(prompt, max_new_tokens=60)[0]["generated_text"][len(prompt):]
result

输出

代码语言:javascript
复制
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```py\n\nThis function takes a single

很好,我们得到了与之前相同的结果,因此在准确性上没有损失!让我们看看这次使用了多少内存。

代码语言:javascript
复制
bytes_to_giga_bytes(torch.cuda.max_memory_allocated())

输出

代码语言:javascript
复制
15.219234466552734

显著减少!我们只剩下略高于 15GB,因此可以在像 4090 这样的消费级 GPU 上运行此模型。我们在内存效率上获得了非常好的收益,几乎没有对模型输出的降级。但是,在推理过程中我们也可以注意到略微减速。

我们删除模型并再次清空内存。

代码语言:javascript
复制
del model
del pipe
代码语言:javascript
复制
flush()

让我们看看 4 位量化对 GPU 内存消耗的峰值。将模型量化为 4 位可以通过与之前相同的 API 完成 - 这次是通过传递load_in_4bit=True而不是load_in_8bit=True来完成。

代码语言:javascript
复制
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

输出

代码语言:javascript
复制
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```py\n\nThis function takes a single argument

我们几乎看到与之前相同的输出文本 - 只是在代码片段之前缺少了python。让我们看看需要多少内存。

代码语言:javascript
复制
bytes_to_giga_bytes(torch.cuda.max_memory_allocated())

输出

代码语言:javascript
复制
9.543574333190918

只有 9.5GB!对于一个超过 15 亿参数的模型来说,这真的不多。

虽然我们在这里看到模型准确性几乎没有下降,但实际上,4 位量化通常会导致与 8 位量化或完整的bfloat16推理相比产生不同的结果。这取决于用户是否尝试。

还要注意,与 8 位量化相比,这里的推理速度再次稍慢一些,这是因为 4 位量化使用了更激进的量化方法,导致在推理过程中量化和反量化过程需要更长的时间。

代码语言:javascript
复制
del model
del pipe
代码语言:javascript
复制
flush()

总的来说,我们发现在 8 位精度下运行 OctoCoder 将所需的 GPU VRAM 从 32G GPU VRAM 减少到仅 15GB,并且在 4 位精度下运行模型进一步将所需的 GPU VRAM 减少到略高于 9GB。

4 位量化使模型可以在 RTX3090、V100 和 T4 等 GPU 上运行,这对大多数人来说非常容易获得。

有关量化的更多信息以及如何将模型量化以便比 4 位更少地使用 GPU VRAM 内存,我们建议查看AutoGPTQ实现。

最后,重要的是要记住,模型量化在内存效率和准确性之间进行了权衡,并且在某些情况下会增加推理时间。

如果 GPU 内存对您的用例不是限制,通常不需要考虑量化。但是许多 GPU 无法在没有量化方法的情况下运行 LLMs,在这种情况下,4 位和 8 位量化方案是非常有用的工具。

有关更详细的使用信息,我们强烈建议查看Transformers 量化文档。接下来,让我们看看如何通过使用更好的算法和改进的模型架构来提高计算和内存效率。

2. 闪光关注

今天表现最佳的 LLMs 基本上共享相同的基本架构,包括前馈层、激活层、层归一化层,以及最关键的自注意力层。

自注意力层对于大型语言模型(LLMs)至关重要,因为它们使模型能够理解输入标记之间的上下文关系。然而,自注意力层的峰值 GPU 内存消耗随着输入标记数量(也称为序列长度)的增加呈二次增长,我们在下文中用N N N 表示。虽然对于较短的输入序列(最多 1000 个输入标记)这并不明显,但对于较长的输入序列(大约 16000 个输入标记)则成为一个严重问题。

让我们仔细看看。计算自注意力层对于长度为N N N 的输入X \mathbf{X} X 的输出O \mathbf{O} O 的公式是:O=Attn(X)=V×Softmax(QKT) with Q=WqX,V=WvX,K=WkX \textbf{O} = \text{Attn}(\mathbf{X}) = \mathbf{V} \times \text{Softmax}(\mathbf{QK}^T) \text{ with } \mathbf{Q} = \mathbf{W}_q \mathbf{X}, \mathbf{V} = \mathbf{W}_v \mathbf{X}, \mathbf{K} = \mathbf{W}_k \mathbf{X} O=Attn(X)=V×Softmax(QKT) with Q=Wq​X,V=Wv​X,K=Wk​X X=(x1,...xN) \mathbf{X} = (\mathbf{x}1, … \mathbf{x}{N}) X=(x1​,…xN​) 是注意力层的输入序列。投影Q \mathbf{Q} Q 和K \mathbf{K} K 将分别包含N N N 个向量,导致QKT \mathbf{QK}^T QKT 的大小为N2 N² N2 。

LLMs 通常具有多个注意力头,因此可以并行进行多个自注意力计算。假设 LLM 有 40 个注意力头并且以 bfloat16 精度运行,我们可以计算存储QKT \mathbf{QK^T} QKT 矩阵所需的内存为40∗2∗N2 40 * 2 * N² 40∗2∗N2 字节。对于N=1000 N=1000 N=1000,只需要大约 50MB 的 VRAM,然而,对于N=16000 N=16000 N=16000,我们将需要 19GB 的 VRAM,而对于N=100,000 N=100,000 N=100,000,我们将需要近 1TB 的 VRAM 来存储QKT \mathbf{QK}^T QKT 矩阵。

长话短说,对于大型输入上下文来说,默认的自注意力算法很快变得内存消耗过高。

随着 LLMs 在文本理解和生成方面的改进,它们被应用于越来越复杂的任务。虽然模型曾经处理几句话的翻译或总结,现在它们可以处理整页的内容,需要处理广泛的输入长度。

我们如何摆脱大型输入长度的过高内存需求?我们需要一种新的方式来计算自注意力机制,摆脱QKT QK^T QKT 矩阵。Tri Dao 等人开发了一种全新的算法,称之为Flash Attention

简而言之,Flash Attention 将V×Softmax(QKT\mathbf{V} \times \text{Softmax}(\mathbf{QK}^TV×Softmax(QKT)计算分开,而是通过迭代多个 softmax 计算步骤来计算输出的较小块:Oi←sija∗Oi+sijb∗Vj×Softmax(QKi,jT) for multiple i,j iterations \textbf{O}i \leftarrow s^a{ij} * \textbf{O}i + s^b{ij} * \mathbf{V}{j} \times \text{Softmax}(\mathbf{QK}^T{i,j}) \text{ for multiple } i, j \text{ iterations} Oi​←sija​∗Oi​+sijb​∗Vj​×Softmax(QKi,jT​) for multiple i,j iterations

其中sija s^a_{ij}和sijb s^b_{ij}是一些需要为每个i i和j j重新计算的 softmax 归一化统计量。

请注意,整个 Flash Attention 有点复杂,在这里大大简化了,因为深入讨论超出了本指南的范围。读者可以查看写得很好的Flash Attention 论文以获取更多详细信息。

这里的主要要点是:

通过跟踪 softmax 归一化统计量,并使用一些智能数学,Flash Attention 给出了与默认自注意力层数值相同的输出,而内存成本仅随N N线性增加。

从公式来看,人们直觉地会说 Flash Attention 必须比默认的自注意力公式慢得多,因为需要进行更多的计算。事实上,与普通注意力相比,Flash Attention 需要更多的 FLOPs,因为 softmax 归一化统计量必须不断重新计算(如果感兴趣,请参阅论文获取更多详细信息)

然而,与默认注意力相比,Flash Attention 在推理速度上要快得多,这是因为它能够显著减少对 GPU(VRAM)较慢、高带宽内存的需求,而是专注于更快的片上内存(SRAM)。

基本上,Flash Attention 确保所有中间写入和读取操作都可以使用快速的片上SRAM 内存来完成,而无需访问较慢的 VRAM 内存来计算输出向量O \mathbf{O}。

实际上,如果可用,目前绝对没有理由使用 Flash Attention。该算法在数学上给出相同的输出,而且速度更快,内存效率更高。

让我们看一个实际的例子。

我们的 OctoCoder 模型现在得到了一个明显更长的输入提示,其中包括所谓的系统提示。系统提示用于引导 LLM 成为一个更好的助手,专门为用户的任务定制。接下来,我们使用一个系统提示,将使 OctoCoder 成为一个更好的编码助手。

代码语言:javascript
复制
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 答案:在这里"

代码语言:javascript
复制
long_prompt = 10 * system_prompt + prompt

我们再次以 bfloat16 精度实例化我们的模型。

代码语言:javascript
复制
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 内存需求的峰值和推理时间。

代码语言:javascript
复制
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

输出

代码语言:javascript
复制
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 个标记的截止。这并不奇怪,因为我们为演示目的重复了系统提示十次,从而提示模型重复自己。

请注意,在实际应用中,系统提示不应重复十次-一次就足够了!

让我们测量 GPU 内存需求的峰值。

代码语言:javascript
复制
bytes_to_giga_bytes(torch.cuda.max_memory_allocated())

输出

代码语言:javascript
复制
37.668193340301514

正如我们所看到的,峰值 GPU 内存需求现在比一开始显着更高,这在很大程度上是由于更长的输入序列。此外,生成现在需要一分钟多一点。

我们调用flush()来释放 GPU 内存,以便进行下一个实验。

代码语言:javascript
复制
flush()

为了进行比较,让我们运行相同的函数,但启用 Flash Attention。为此,我们将模型转换为BetterTransformer,从而启用 PyTorch 的SDPA 自注意力,进而能够使用 Flash Attention。

代码语言:javascript
复制
model.to_bettertransformer()

现在我们运行与之前完全相同的代码片段,在底层 Transformers 将利用 Flash Attention。

代码语言:javascript
复制
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

输出

代码语言:javascript
复制
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,我们可以观察到非常显著的加速。

让我们最后一次测量内存消耗。

代码语言:javascript
复制
bytes_to_giga_bytes(torch.cuda.max_memory_allocated())

输出

代码语言:javascript
复制
32.617331981658936

我们几乎回到了最初的 29GB GPU 内存峰值。

我们可以观察到,与一开始传递短输入序列相比,使用 Flash Attention 传递非常长的输入序列时,我们只使用了大约多 100MB 的 GPU 内存。

代码语言:javascript
复制
flush()

有关如何使用 Flash Attention 的更多信息,请查看此文档页面

3. 架构创新

到目前为止,我们已经研究了通过以下方式提高计算和内存效率:

  • 将权重转换为较低精度格式
  • 用更节省内存和计算资源的版本替换自注意力算法

现在让我们看看如何改变 LLM 的架构,使其对需要长文本输入的任务最有效和高效,例如:

  • 检索增强问答,
  • 总结,
  • 聊天

请注意,chat不仅要求 LLM 处理长文本输入,还要求 LLM 能够有效地处理用户和助手之间的来回对话(例如 ChatGPT)。

一旦训练完成,基本的 LLM 架构很难改变,因此在事先考虑 LLM 的任务并相应地优化模型架构非常重要。模型架构的两个重要组件很快成为大型输入序列的内存和/或性能瓶颈。

  • 位置嵌入
  • 键-值缓存

让我们更详细地讨论每个组件

3.1 改进 LLM 的位置嵌入

自注意力将每个标记与其他标记相关联。例如,文本输入序列的 Softmax(QKT)矩阵*“Hello”, “I”, “love”, “you”*可能如下所示:

每个单词标记都被赋予一个概率质量,用于关注所有其他单词标记,因此与所有其他单词标记相关联。例如,单词*“love”关注单词“Hello”的概率为 5%,关注“I”*的概率为 30%,自身的概率为 65%。

基于自注意力的 LLM,但没有位置嵌入,将在理解文本输入之间的位置方面遇到很大困难。这是因为由QKT \mathbf{QK}^T QKT 计算的概率分数将每个单词标记与其他单词标记在O(1) O(1) O(1) 计算中相关联,而不考虑它们之间的相对位置距离。因此,对于没有位置嵌入的 LLM,每个标记似乎与所有其他标记具有相同的距离,例如,区分“你好 我爱你”和“你爱我 你好”将会非常具有挑战性。

为了让 LLM 理解句子顺序,需要额外的提示,通常以位置编码(也称为位置嵌入)的形式应用。位置编码将每个标记的位置编码为 LLM 可以利用的数值表示,以更好地理解句子顺序。

注意力机制就是你所需要的论文的作者们引入了正弦位置嵌入P=p1,…,pN \mathbf{P} = \mathbf{p}_1, \ldots, \mathbf{p}_N P=p1​,…,pN​ 。其中每个向量pi \mathbf{p}_i pi​ 是根据其位置i i i 计算的正弦函数。然后将位置编码简单地添加到输入序列向量中X^=x^1,…,x^N \mathbf{\hat{X}} = \mathbf{\hat{x}}_1, \ldots, \mathbf{\hat{x}}_N X=x¹​,…,xN​ =x1+p1,…,xN+pN \mathbf{x}_1 + \mathbf{p}_1, \ldots, \mathbf{x}_N + \mathbf{p}_N x1​+p1​,…,xN​+pN​ 从而提示模型更好地学习句子顺序。

其他人(例如Devlin 等人)使用了学习的位置编码,而不是固定的位置嵌入,这些位置嵌入在训练期间进行学习。

正弦和学习位置嵌入曾经是将句子顺序编码到 LLM 中的主要方法,但发现了与这些位置编码相关的一些问题:

  1. 正弦和学习位置嵌入都是绝对位置嵌入,即为每个位置 id 编码一个唯一的嵌入:0,…,N 0, \ldots, N 0,…,N。正如Huang 等人苏等人所示,绝对位置嵌入导致长文本输入的 LLM 性能较差。对于长文本输入,如果模型学习输入标记之间的相对位置距离而不是它们的绝对位置,将是有利的。
  2. 当使用学习位置嵌入时,LLM 必须在固定的输入长度N N N 上进行训练,这使得难以推广到比其训练长度更长的输入。

最近,能够解决上述问题的相对位置嵌入变得更加流行,其中最著名的是:

RoPEALiBi都认为最好直接在自注意力算法中提示 LLM 关于句子顺序,因为在那里单词标记彼此关联。更具体地说,句子顺序应该通过修改QKT \mathbf{QK}^T QKT 计算来提示。

不详细讨论,RoPE指出位置信息可以被编码到查询-键对中,例如:qi \mathbf{q}_i 和xj \mathbf{x}_j ,通过将每个向量旋转一个角度θ∗i \theta * i 和θ∗j \theta * j ,其中i,j i, j 描述每个向量的句子位置:q^iTx^j=qiTRθ,i−jxj. \mathbf{\hat{q}}_i^T \mathbf{\hat{x}}_j = \mathbf{{q}}i^T \mathbf{R}{\theta, i -j} \mathbf{{x}}j. q​iT​xj​=qiT​Rθ,i−j​xj​。 Rθ,i−j \mathbf{R}{\theta, i - j} Rθ,i−j​代表一个旋转矩阵。θ \theta θ在训练过程中不会被学习,而是设置为一个预定义的值,该值取决于训练过程中的最大输入序列长度。

通过这样做,qi \mathbf{q}_i 和qj \mathbf{q}_j 之间的概率分数只有在i≠j i \ne j 时才会受到影响,并且仅取决于相对距离i−j i - j ,而不考虑每个向量的具体位置i i 和j j 。

RoPE被用在当今一些最重要的 LLM 中,例如:

作为一种替代方案,ALiBi 提出了一种更简单的相对位置编码方案。输入令牌之间的相对距离被添加为负整数,乘以预定义值 m,并添加到 softmax 计算之前的QKT \mathbf{QK}^T QKT 矩阵的每个查询-键条目中。

正如ALiBi 论文所示,这种简单的相对位置编码使模型能够在非常长的文本输入序列中保持高性能。

ALiBi 在当今一些最重要的 LLM 中使用,例如:

RoPEALiBi 位置编码都可以外推到训练中未见过的输入长度,然而已经证明相对于 RoPE,外推对于 ALiBi 来说更容易。对于 ALiBi,只需增加下三角位置矩阵的值以匹配输入序列的长度。对于 RoPE,保持训练期间使用的相同θ \theta θ,在传递比训练期间看到的文本输入长得多的文本输入时会导致结果不佳,参见 Press 等人。然而,社区已经发现了一些有效的技巧,可以调整θ \theta θ,从而使 RoPE 位置嵌入适用于外推的文本输入序列(参见这里)。

RoPE 和 ALiBi 都是相对位置嵌入,它们在训练期间 被学习,而是基于以下直觉:

  • 关于文本输入的位置提示应直接提供给自注意力层的QKT QK^T QKT 矩阵
  • LLM 应该被激励学习常数 相对 距离位置编码之间的关系
  • 文本输入令牌之间的距离越远,它们的查询-值概率就越低。RoPE 和 ALiBi 都降低了远离彼此的令牌的查询-键概率。RoPE 通过增加查询-键向量之间的角度来减少它们的向量积。ALiBi 通过向向量积添加大的负数

总之,用于处理大文本输入的任务的 LLM 最好使用相对位置嵌入进行训练,例如 RoPE 和 ALiBi。还要注意,即使一个带有 RoPE 和 ALiBi 的 LLM 只在固定长度的数据上进行了训练,比如N1=2048 N_1 = 2048 N1​=2048,它仍然可以在实践中用于比N1 N_1 N1​更大的文本输入,比如N2=8192>N1 N_2 = 8192 > N_1 N2​=8192>N1​,通过外推位置嵌入。

3.2 关键-值缓存

LLMs 的自回归文本生成通过迭代地输入一个序列,抽样下一个标记,将下一个标记附加到输入序列中,并继续这样做,直到 LLM 生成一个表示生成结束的标记。

请查看Transformer 生成文本教程,以获得更直观的自回归生成工作原理解释。

让我们运行一个快速的代码片段,展示自回归在实践中是如何工作的。我们将简单地通过torch.argmax获取最有可能的下一个标记。

代码语言:javascript
复制
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

输出

代码语言:javascript
复制
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 blog

因此,标记永远不依赖于先前的标记,更具体地说,qi \mathbf{q}_i qi​向量永远不会与任何键、值向量kj,vj \mathbf{k}_j, \mathbf{v}j kj​,vj​相关联,如果j>i j > i j>i。相反,qi \mathbf{q}i qi​只关注先前的键-值向量km<i,vm<i , for m∈{0,…i−1} \mathbf{k}{m < i}, \mathbf{v}{m < i} \text{ , for } m \in {0, \ldots i - 1} km<i​,vm<i​ , for m∈{0,…i−1}。为了减少不必要的计算,可以为每一层缓存所有先前时间步的键-值向量。

接下来,我们将告诉 LLM 利用键-值缓存,通过在每次前向传递中检索并转发它。在 Transformers 中,我们可以通过向forward调用传递use_cache标志来检索键-值缓存,然后可以将其与当前标记一起传递。

代码语言:javascript
复制
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

输出

代码语言:javascript
复制
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']

正如大家所看到的,当使用键-值缓存时,文本输入标记的长度不会增加,而是保持为单个输入向量。另一方面,键-值缓存的长度在每个解码步骤都会增加一个。

利用键值缓存意味着QKT \mathbf{QK}^T QKT 基本上被简化为qcKT \mathbf{q}_c\mathbf{K}^T qc​KT,其中qc \mathbf{q}_c qc​是当前传递的输入令牌的查询投影,它始终只是一个单一向量。

使用键值缓存有两个优点:

  • 与计算完整的 QKT 矩阵相比,计算效率显著提高,因为进行的计算较少。这导致推理速度增加。
  • 所需的最大内存并不是随着生成的令牌数量的平方增加,而是线性增加。

应该始终利用键值缓存,因为它会产生相同的结果,并且对于较长的输入序列会显著加快速度。当使用文本管道或generate方法时,Transformers 默认启用键值缓存。

请注意,尽管我们建议使用键值缓存,但当您使用它们时,您的 LLM 输出可能会略有不同。这是矩阵乘法核心本身的属性 — 您可以在这里了解更多信息。

3.2.1 多轮对话

键值缓存在需要多次自回归解码的应用程序中特别有用,让我们看一个例子。

代码语言:javascript
复制
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接口。

代码语言:javascript
复制
# 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):]

输出

代码语言:javascript
复制
 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

太好了,不需要额外的时间来重新计算注意力层的相同键和值!然而,有一个问题。虽然QKT \mathbf{QK}^T QKT 矩阵所需的峰值内存显著减少,但在内存中保持键值缓存可能会对长输入序列或多轮对话非常昂贵。请记住,键值缓存需要存储所有先前输入向量的键值向量xi, for i∈{1,…,c−1} \mathbf{x}_i \text{, for } i \in {1, \ldots, c - 1} xi​, for i∈{1,…,c−1} 对于所有自注意力层和所有注意力头部。

让我们计算之前使用的 LLM bigcode/octocoder 需要存储在键值缓存中的浮点值的数量。浮点值的数量等于序列长度乘以注意力头数乘以注意力头维度乘以层数的两倍。对于我们的 LLM,在假设输入序列长度为 16000 时计算如下:

代码语言:javascript
复制
config = model.config
2 * 16_000 * config.n_layer * config.n_head * config.n_embd // config.n_head

输出

代码语言:javascript
复制
7864320000

大约 80 亿个浮点值!以float16精度存储 80 亿个浮点值需要大约 15 GB 的内存,这大约是模型权重本身的一半!研究人员提出了两种方法,可以显著减少存储键值缓存的内存成本,这将在接下来的小节中探讨。

3.2.2 多查询注意力(MQA)

Multi-Query-Attention 是 Noam Shazeer 在Fast Transformer Decoding: One Write-Head is All You Need论文中提出的。正如标题所说,Noam 发现,可以使用一个单一的头值投影权重对,而不是使用n_head个键值投影权重,这个对在所有注意力头部之间共享,而不会显著降低模型的性能。

通过使用单个头值投影权重对,键值向量ki,vi \mathbf{k}_i, \mathbf{v}_i ki​,vi​ 在所有注意力头部之间必须是相同的,这意味着我们只需要在缓存中存储 1 个键值投影对,而不是n_head个。

由于大多数 LLM 使用 20 到 100 个注意力头部,MQA 显著减少了键值缓存的内存消耗。对于本笔记本中使用的 LLM,因此我们可以将所需的内存消耗从 15 GB 减少到输入序列长度为 16000 时的不到 400 MB。

除了节省内存外,MQA 还提高了计算效率,如下所述。在自回归解码中,需要重新加载大的键值向量,将其与当前的键值向量对连接,然后将其馈送到每一步的qcKT \mathbf{q}_c\mathbf{K}^T qc​KT 计算中。对于自回归解码,常量重新加载所需的内存带宽可能成为严重的时间瓶颈。通过减小键值向量的大小,可以减少访问的内存量,从而减少内存带宽瓶颈。更详细的信息,请参阅Noam 的论文

这里需要理解的重要部分是,将关键值注意力头的数量减少到 1 只有在使用关键值缓存时才有意义。模型在没有关键值缓存的单次前向传递中的峰值内存消耗保持不变,因为每个注意力头仍然具有唯一的查询向量,因此每个注意力头仍然具有不同的QKT \mathbf{QK}^T QKT 矩阵。

MQA 已经被社区广泛采用,现在许多最受欢迎的 LLM 都在使用:

此外,本笔记中使用的检查点bigcode/octocoder使用了 MQA。

3.2.3 分组查询注意力(GQA)

分组查询注意力,由谷歌的 Ainslie 等人提出,发现使用 MQA 与使用普通的多键值头投影相比,通常会导致质量下降。该论文认为,通过减少查询头投影权重的数量,可以保留更多的模型性能。不要仅使用单个键值投影权重,应使用n < n_head个键值投影权重。通过选择n为远小于n_head的值,例如 2、4 或 8,几乎可以保留来自 MQA 的所有内存和速度增益,同时牺牲较少的模型容量,因此可以说是性能更好。

此外,GQA 的作者发现,现有的模型检查点可以通过仅使用原始预训练计算量的 5%进行更新训练,以实现 GQA 架构。虽然原始预训练计算量的 5%仍然是一个巨大的数量,但 GQA 的更新训练使现有的检查点可以用于更长的输入序列。

GQA 是最近提出的,因此在撰写本笔记时采用的情况较少。GQA 最显著的应用是Llama-v2

总之,强烈建议在 LLM 部署自回归解码并需要处理大型输入序列的情况下使用 GQA 或 MQA。

结论

研究界不断提出新的巧妙方法来加快越来越大的 LLM 的推理时间。例如,一个有前途的研究方向是推测解码,其中较小、更快的语言模型生成“简单标记”,只有 LLM 本身生成“困难标记”。更详细的内容超出了本笔记的范围,但可以在这篇不错的博客文章中阅读。

大型 LLM(如 GPT3/4、Llama-2-70b、Claude、PaLM)能够在Hugging Face Chat或 ChatGPT 等聊天界面中运行如此迅速,这在很大程度上要归功于上述精度、算法和架构的改进。未来,像 GPU、TPU 等加速器将会变得更快,允许更多的内存,但仍然应始终确保使用最佳的可用算法和架构,以获得最大的性价比🤗 使用的 LLM,因此我们可以将所需的内存消耗从 15 GB 减少到输入序列长度为 16000 时的不到 400 MB。

除了节省内存外,MQA 还提高了计算效率,如下所述。在自回归解码中,需要重新加载大的键值向量,将其与当前的键值向量对连接,然后将其馈送到每一步的qcKT \mathbf{q}_c\mathbf{K}^T qc​KT 计算中。对于自回归解码,常量重新加载所需的内存带宽可能成为严重的时间瓶颈。通过减小键值向量的大小,可以减少访问的内存量,从而减少内存带宽瓶颈。更详细的信息,请参阅Noam 的论文

这里需要理解的重要部分是,将关键值注意力头的数量减少到 1 只有在使用关键值缓存时才有意义。模型在没有关键值缓存的单次前向传递中的峰值内存消耗保持不变,因为每个注意力头仍然具有唯一的查询向量,因此每个注意力头仍然具有不同的QKT \mathbf{QK}^T QKT 矩阵。

MQA 已经被社区广泛采用,现在许多最受欢迎的 LLM 都在使用:

此外,本笔记中使用的检查点bigcode/octocoder使用了 MQA。

3.2.3 分组查询注意力(GQA)

分组查询注意力,由谷歌的 Ainslie 等人提出,发现使用 MQA 与使用普通的多键值头投影相比,通常会导致质量下降。该论文认为,通过减少查询头投影权重的数量,可以保留更多的模型性能。不要仅使用单个键值投影权重,应使用n < n_head个键值投影权重。通过选择n为远小于n_head的值,例如 2、4 或 8,几乎可以保留来自 MQA 的所有内存和速度增益,同时牺牲较少的模型容量,因此可以说是性能更好。

此外,GQA 的作者发现,现有的模型检查点可以通过仅使用原始预训练计算量的 5%进行更新训练,以实现 GQA 架构。虽然原始预训练计算量的 5%仍然是一个巨大的数量,但 GQA 的更新训练使现有的检查点可以用于更长的输入序列。

GQA 是最近提出的,因此在撰写本笔记时采用的情况较少。GQA 最显著的应用是Llama-v2

总之,强烈建议在 LLM 部署自回归解码并需要处理大型输入序列的情况下使用 GQA 或 MQA。

结论

研究界不断提出新的巧妙方法来加快越来越大的 LLM 的推理时间。例如,一个有前途的研究方向是推测解码,其中较小、更快的语言模型生成“简单标记”,只有 LLM 本身生成“困难标记”。更详细的内容超出了本笔记的范围,但可以在这篇不错的博客文章中阅读。

大型 LLM(如 GPT3/4、Llama-2-70b、Claude、PaLM)能够在Hugging Face Chat或 ChatGPT 等聊天界面中运行如此迅速,这在很大程度上要归功于上述精度、算法和架构的改进。未来,像 GPU、TPU 等加速器将会变得更快,允许更多的内存,但仍然应始终确保使用最佳的可用算法和架构,以获得最大的性价比🤗

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2024-06-23,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 🤗 Transformers 能做什么
    • 音频
      • 音频分类
      • 自动语音识别
    • 计算机视觉
      • 图像分类
      • 目标检测
      • 图像分割
      • 深度估计
    • 自然语言处理
      • 文本分类
      • 标记分类
      • 问答
      • 摘要
      • 翻译
      • 语言建模
    • 多模态
      • 文档问答
  • 🤗 Transformers 如何解决任务
    • 语音和音频
      • 音频分类
      • 自动语音识别
    • 计算机视觉
      • 图像分类
      • 目标检测
      • 图像分割
      • 深度估计
    • 自然语言处理
      • 文本分类
      • 标记分类
      • 问答
      • 文本生成
      • 摘要
      • 翻译
  • Transformer 模型家族
    • 计算机视觉
      • 卷积网络
      • 编码器
      • 解码器
      • 编码器-解码器
    • 自然语言处理
      • 编码器
      • 解码器
      • 编码器-解码器
    • 音频
      • 编码器
      • 编码器-解码器
    • 多模态
      • 编码器
      • 编码器-解码器
    • 强化学习
      • 解码器
  • 分词器总结
    • 介绍
      • 子词分词
        • 字节对编码(BPE)
        • WordPiece
        • Unigram
        • SentencePiece
    • 注意机制
      • LSH 注意力
        • 本地注意力
          • 其他技巧
            • 轴向位置编码
        • 填充和截断
        • BERTology
        • 固定长度模型的困惑度
          • 使用固定长度模型计算 PPL
            • 示例:在🤗 Transformers 中使用 GPT-2 计算困惑度
            • 为 Web 服务器使用管道
              • 你可能想考虑的几件事
                • 错误检查
                • 断路处理
                • 阻塞主线程
                • 动态批处理
            • 模型训练解剖学
              • 加载模型
                • 在普通训练中的内存利用率
                  • 模型操作解剖学
                    • 模型内存的解剖
                    • 优化 LLMs 的速度和内存
                      • 1. 低精度
                        • 2. 闪光关注
                          • 3. 架构创新
                            • 3.1 改进 LLM 的位置嵌入
                            • 3.2 关键-值缓存
                          • 结论
                            • 结论
                            相关产品与服务
                            语音识别
                            腾讯云语音识别(Automatic Speech Recognition,ASR)是将语音转化成文字的PaaS产品,为企业提供精准而极具性价比的识别服务。被微信、王者荣耀、腾讯视频等大量业务使用,适用于录音质检、会议实时转写、语音输入法等多个场景。
                            领券
                            问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档