目录
目前各种Pretraining的Transformer模型层出不穷, 虽然这些模型都有开源代码, 但是它们的实现各不相同, 我们在对比不同模型时也会很麻烦。Huggingface Transformer能够帮我们跟踪流行的新模型, 并且提供统一的代码风格来使用BERT、XLNet和GPT等等各种不同的模型。而且它有一个模型仓库, 所有常见的预训练模型和不同任务上fine-tuning的模型都可以在这里方便的下载。
0. 大模型与Transformer
大多数LLM的核心是 Transformer 模型, 这是 2017 年论文"Attention Is All You Need"中引入的一种革命性的深度学习架构。Transformer 的关键组件包括:
- 自我注意机制: 帮助模型专注于句子中最重要的单词。
- 多头注意: 使模型能够同时理解不同的语言关系。
0.1 大模型
从图中可以看到所谓大模型家族都有同一个根(elmo 这一支除外)即 Transformer, 我们知道 transformer 由 encoder-decoder 两部分组成, encoder 部分负责编码, 更侧重于信息理解;而 decoder 部分负责解码, 更侧重于文本生成;这样在模型选型方面就会有 3 种不同的选型, 即:
- only-encoder: 这部分以大名鼎鼎的 Bert 为代表
- only-decoder: 这部分的代表就是我们的当红炸子鸡 GPT 系列
- encoder-decoder: 这部分相比于其他两个部分就显得略微暗淡一些, 但同样也有一些相当不错的成果, 其中尤以 T5 为代表。
个人理解 T5 更像一个过渡产品, 通过添加一些 prefix 或者 prompt 将几乎所有 NLP 任务都可以转换为 Text-to-Text 的任务, 这样就使得原本仅适合 encoder 的任务(classification)也可以使用 decoder 的模式来处理。
0.2 Transformer 解决了哪些问题
在没出现Transformer之前, NLP领域几乎都是以RNN模型为主导, RNN有两个比较明显的缺陷:
- RNN模型是一个串行模型, 只能一个时序一个时序的依次来处理信息, 后一个时序需要依赖前一个时序的输出, 这样就导致不能并行, 时序越长性能越低同时也会造成一定的信息丢失
- RNN模型是一个单向模型, 只能从左到右或者从右到左进行处理, 无法实现真正的双向编码
Transformer摒弃了RNN的顺序编码方式, 完全使用注意力机制来对信息进行编码, Transformer的计算过程是完全并行的, 可以同时计算所有时序的注意力得分。另外Transformer是真正的双向编码, 在计算input#2的注意力得分时, input#2是可以同时看见input#1、input#3的且对于input#2而言input#1、input#3、甚至input#n都是同等距离的, 没有所谓距离的概念, 真正的天涯若比邻的感觉。
0.3 Tranformer 的庐山真面目
下面这张图就是我们 Transformer 的架构图, 从图中可以看出, Transformer 由左右两部分组成, 左边这部分是Encoder, 右边这部分就是Decoder了。Encoder负责对信息进行编码而Decoder则负责对信息解码 。下面我们从下往上对下图的每个部分进行解读。
1. 入门
1.1 安装
pip install transformers
# 建议安装开发版本, 几乎带有所有用例需要的依赖项
pip install transformers[sentencepiece]
2. 模型简介 Transformer models
Transformer 由编码器和解码器组成, 我们可以用它们执行的任务取决于我们是使用这两个组件中的一个还是两个。请注意, 还有一些不使用transformer的“sequence-to-sequence”模型。
并非所有LLM或基础模型都使用变压器, 但它们通常使用。并非所有基础模型都是LLM, 但它们通常是 LLM。并非所有变压器都是LLM或FM。重要的收获是所有transformer模型都使用 attention。但这里重要的一点是, transformer 模型(以及所有基于transformer的基础 LLM)的关键区分特征是自我注意的概念。
2.1 pipelines 简单的小例子
Transformers 库中最基本的对象是pipeline()函数。它将模型与其必要的预处理和后处理步骤连接起来, 使我们能够直接输入任何文本并获得答案:
from transformers import pipeline
classifier = pipeline("sentiment-analysis") # 情感分析
classifier("I've been waiting for a HuggingFace course my whole life.")
# 输出
# [{'label': 'POSITIVE', 'score': 0.9598047137260437}]
目前可用的一些pipeline 有:
- feature-extraction 特征提取: 把一段文字用一个向量来表示
- fill-mask 填词: 把一段文字的某些部分mask住, 然后让模型填空
- ner 命名实体识别: 识别文字中出现的人名地名的命名实体
- question-answering 问答: 给定一段文本以及针对它的一个问题, 从文本中抽取答案
- sentiment-analysis 情感分析: 一段文本是正面还是负面的情感倾向
- summarization 摘要: 根据一段长文本中生成简短的摘要
- text-generation文本生成: 给定一段文本, 让模型补充后面的内容
- translation 翻译: 把一种语言的文字翻译成另一种语言
- zero-shot-classification
2.2 各种任务的代表模型
Model | Examples | Tasks |
---|---|---|
Encoder 编码器模型 |
ALBERT, BERT, DistilBERT, ELECTRA, RoBERTa | Sentence classification, named entity recognition, extractive question answering 适合需要理解完整句子的任务, 例如句子分类、命名实体识别(以及更一般的单词分类)和提取式问答 |
Decoder 解码器模型 |
CTRL, GPT, GPT-2, Transformer XL | Text generation 解码器模型的预训练通常围绕预测句子中的下一个单词。这些模型最适合涉及文本生成的任务 |
Encoder-decoder 序列到序列模型 |
BART, T5, Marian, mBART | Summarization, translation, generative question answering 序列到序列模型最适合围绕根据给定输入生成新句子的任务, 例如摘要、翻译或生成式问答。 |
3. 使用 Using Transformers
3.1 Pipeline 背后的流程
3.2 Models
3.2.1 创建Transformer
from transformers import BertConfig, BertModel
# Building the config
config = BertConfig()
# Building the model from the config
model = BertModel(config)
3.2.2 不同的加载方式
from transformers import BertModel
model = BertModel.from_pretrained("bert-base-cased")
3.2.3 保存模型
model.save_pretrained("directory_on_my_computer")
3.2.4 使用Transformer model
import torch
sequences = ["Hello!", "Cool.", "Nice!"]
encoded_sequences = [
[101, 7592, 999, 102],
[101, 4658, 1012, 102],
[101, 3835, 999, 102],
]
model_inputs = torch.tensor(encoded_sequences)
3.3 Tokenizers
3.3.1 Loading and saving
from transformers import BertTokenizer
tokenizer = BertTokenizer.from_pretrained("bert-base-cased")
tokenizer("Using a Transformer network is simple")
# 输出
'''
{'input_ids': [101, 7993, 170, 11303, 1200, 2443, 1110, 3014, 102],
'token_type_ids': [0, 0, 0, 0, 0, 0, 0, 0, 0],
'attention_mask': [1, 1, 1, 1, 1, 1, 1, 1, 1]}
'''
# 保存
tokenizer.save_pretrained("directory_on_my_computer")
3.3.2 Tokenization
from transformers import AutoTokenizer
tokenizer = AutoTokenizer.from_pretrained("bert-base-cased")
sequence = "Using a Transformer network is simple"
tokens = tokenizer.tokenize(sequence)
print(tokens) # 输出 : ['Using', 'a', 'transform', '##er', 'network', 'is', 'simple']
# 从token 到输入 ID
ids = tokenizer.convert_tokens_to_ids(tokens)
print(ids) # 输出: [7993, 170, 11303, 1200, 2443, 1110, 3014]
3.3.3 Decoding
decoded_string = tokenizer.decode([7993, 170, 11303, 1200, 2443, 1110, 3014])
print(decoded_string) # 输出: 'Using a Transformer network is simple'
4. 注意力机制
一般来说, 注意力描述了模型关注句子重要部分(或图像或任何其他顺序输入)的能力。它通过根据输入特征的重要性和它们在序列中的位置为输入特征分配权重来实现此目的。
注意力是通过并行化来提高以前 NLP 模型(如 RNN 和 LSTM)性能的概念。但注意力不仅仅是优化。它还在拓宽语言模型在处理和生成语言时能够考虑的上下文方面发挥着关键作用。这使模型能够以更长的序列生成上下文适当且连贯的文本。
4.1 注意类型
Self-attention 是指每个节点都从该单个节点生成一个键、查询和一个值。多头注意力只是以不同的初始化权重并行应用多次的自我注意。Cross-attention 意味着查询仍然是从给定的 decoder 节点生成的, 但键和值是作为编码器中节点的函数生成的。
4.2 注意力遮罩(attention_mask)
这个公式的输出就是注意力得分, 怎么来理解这个式子, 我们用一个例子来类比。想象一下我们在百度进行搜索的一个场景, _Q_就相当于我们在输入框输入的关键词, 当我们输入关键词之后搜索引擎会根据我们的关键词与文档的相似度输出一个快照列表, K_就是这个快照列表, 每个文档与我们输入的关键词的相似度不同, 所以排在第一个的是搜索引擎认为最重要的文档, 打分就高, 其他依次降序排列;然后你点进去阅读了这篇文章, 那么这篇文章的内容我们就可以类比为_V。这是一个搜索引擎的检索过程, 而 Attention 的计算过程与搜索的过程几乎完全相同, 我们结合下面这张图来详细的说明一下注意力的计算过程。
注意力遮罩(attention_mask) , 但我们在那个时候没有讨论类型标记ID(token_type_ids)
Attention masks是与输入ID张量形状完全相同的张量, 用0和1填充: 1s表示应注意相应的标记, 0s表示不应注意相应的标记(即, 模型的注意力层应忽略它们)。
4.3 Multi Head Attention(MHA)
- transformer 架构使用多头自我注意, 其中每个注意力头都有帮助计算注意力分数的权重(参数)。
- 参数的数量随着注意力头的增加和隐藏维度的增加而增加。
为什么要使用多头注意力?多头注意力机制提供了多个表示子空间, 每个头独享不同得_Q_, K, _V_权重矩阵, 这些权重矩阵每一个都是随机初始化, 在训练之后, 每个头都将输入投影到不同的表示空间, 多个 head 学习得注意力侧重点可能略微不同, 这样给了模型更大的容量。(可以想象一下 CNN 中不同的滤波器分别关注着不同的特征一样)。
这里说一下 Multi Query Attention(MQA), 这也是在一些大模型中使用的对 MHA 进行改造的手段, 比如: Falcon、PaLM 等。MQA 就是在所有的注意力头上共享_K_, V, 提升推理性能、减少显存占用。就这么简单。
4.4 BertViz
BertViz 是一个开源工具, 可以在多个尺度上可视化 transformer 模型的注意力机制, 包括模型级别、注意力头级别和神经元级别。但 BertViz 并不新鲜。事实上, BertViz 的早期版本早在 2017 年就已经存在。
一些解释注意力行为的成功尝试包括注意力矩阵热图和二分图表示, 这两种方法今天仍在使用。但这些方法也有一些主要的局限性。BertViz 最终因其能够说明自我注意的低级、细化细节而广受欢迎, 同时仍然保持非常简单和直观的使用。
4.4.1 使用 Comet 可视化 BertViz
import comet_ml
comet_ml.init(api_key='<YOUR-API-KEY>')
experiment = comet_ml.Experiment()
5. transformer 的数学运算
为了保持直观, 将使用矩阵、架构和参数细节中的值, 并将 BERT 作为参考。
5.1 标记嵌入和位置编码
在计算任何注意力之前, 首先将输入文本转换为标记嵌入。这些嵌入是捕获词义的高维向量(例如, BERT-base 中的 768 维)。因为 transformer 没有固有的 order 感, 所以它为每个 token 嵌入添加了位置编码。位置编码使用正弦函数:
- pos 是位置, I 是维度。
- 此编码按元素添加到标记嵌入中, 以形成最终的 input 表示 X。
class SinusoidalPositionalEmbedding(nn.Module):
def __init__(self, embedding_dim):
super().__init__()
self.embedding_dim = embedding_dim
def forward(self, timesteps):
positions = np.arange(timesteps)[:, np.newaxis] # Shape: (timesteps, 1)
dimensions = np.arange(self.embedding_dim)[
np.newaxis, :
] # Shape: (1, embedding_dim)
# Compute angles using sine for even indices and cosine for odd indices
angle_rates = 1 / np.power(10000, (2 * (dimensions // 2)) / self.embedding_dim)
angle_rads = positions * angle_rates
pos_encoding = np.zeros_like(angle_rads)
pos_encoding[:, 0::2] = np.sin(angle_rads[:, 0::2])
pos_encoding[:, 1::2] = np.cos(angle_rads[:, 1::2])
return pos_encoding
5.2 计算 Q、K、V 矩阵
从组合的输入 X (标记嵌入 + 位置编码), transformer 计算三个键矩阵:
- 在训练期间, transformer 模型初始化 Wq、Wk、Wv 的随机值, 并使用反向传播对其进行优化。
- 这些矩阵将更新以捕获 Importing 嵌入的有意义转换。
- 这些转换的目的是创建不同的 query、key 和 value 表示形式, 以影响每个 token 对另一个 token 的关注程度。
5.3 计算注意力权重
-
第 1 步:计算原始注意力分数
将每个令牌的查询与所有键进行比较, 以确定其关注度分数:
\[ \frac{QK^T}{\sqrt{d_k}} \] -
第 2 步:应用 Softmax 以获得注意力权重
注意力分数通过 softmax 传递以获得概率分布:注意力权重 = softmax(QK^T/d_k)
-
第 3 步:加权值 (V) 以计算输出
最终的注意力输出是通过将注意力权重与 V 矩阵相乘来获得的:注意力输出 = 注意力权重 × V
5.4 多头注意力
多头注意力对于我们正在使用的语言模型类型的工作方式不同, 因为这里我关注的是 BERT(基于编码器, 双向), 所以我将描述它如何为 BERT 工作, 注意力机制的差异是语言模型类别专业性背后的驱动力, 如 BERT 有利于分类和
5.5 后注意力处理:残差连接和层归一化
使用残差连接和层归一化处理多头注意力输出:
图层归一化:
5.6 前馈网络和最终层归一化
每个令牌的表示形式都通过前馈网络 (FFN) 传递:
在 FFN 之后, 将应用另一个残差连接和层规范化:
以下是这些最后步骤层的参数值(对于 BERT base):
- FFN 第一层权重:[768,3072]
- FFN 第一层偏差:[3072]
- FFN 第二层权重:[3072,768]
- FFN 第二层偏差:[768]
- LayerNorm Gamma (缩放):[768]。
- LayerNorm Beta (移位):[768]。
6. 相关概念
6.1 Tokenizer
模型只能处理数字, 因此我们需要找到一种将原始文本转换为数字的方法。这就是标记器(tokenizer)所做的, 并且有很多方法可以解决这个问题。目标是找到最有意义的表示——即对模型最有意义的表示——并且如果可能的话, 找到最小的表示。
与其他神经网络一样, Transformer 模型不能直接处理原始文本, 故使用分词器进行预处理。使用AutoTokenizer类及其from_pretrained()方法。
from transformers import AutoTokenizer
checkpoint = "distilbert-base-uncased-finetuned-sst-2-english"
tokenizer = AutoTokenizer.from_pretrained(checkpoint)
# 若要指定我们想要返回的张量类型(PyTorch、TensorFlow 或普通 NumPy), 我们使用return_tensors参数
raw_inputs = [
"I've been waiting for a HuggingFace course my whole life.",
"I hate this so much!",
]
inputs = tokenizer(raw_inputs, padding=True, truncation=True, return_tensors="pt")
print(inputs)
6.2 Model
Transformers 提供了一个AutoModel类, 它也有一个from_pretrained()方法:
from transformers import AutoModel
checkpoint = "distilbert-base-uncased-finetuned-sst-2-english"
model = AutoModel.from_pretrained(checkpoint)
# 如果我们将预处理过的输入提供给我们的模型, 我们可以看到:
outputs = model(**inputs)
print(outputs.last_hidden_state.shape)
在此图中, 模型由其嵌入层和后续层表示。嵌入层将标记化输入中的每个输入ID转换为表示关联标记(token)的向量。后续层使用注意机制操纵这些向量, 以生成句子的最终表示。
Transformers中有许多不同的体系结构, 每种体系结构都是围绕处理特定任务而设计的。以下是一个非详尽的列表:
- *Model (retrieve the hidden states)
- 使用 AutoModel类, 当您希望从检查点实例化任何模型时, 这非常方便。
- *ForCausalLM
- *ForMaskedLM
- *ForMultipleChoice
- *ForQuestionAnswering
- *ForSequenceClassification
- *ForTokenClassification
- 以及其他
6.3 Post-Processing
模型最后一层输出的原始非标准化分数。要转换为概率, 它们需要经过一个SoftMax层(所有 Transformers 模型都输出 logits, 因为用于训练的损耗函数一般会将最后的激活函数(如SoftMax)与实际损耗函数(如交叉熵)融合 。
import torch
predictions = torch.nn.functional.softmax(outputs.logits, dim=-1)
print(predictions)
6.4 输入词id(input_ids)
它们是每个句子中标记的唯一标记(token)
将文本翻译成数字被称为编码(encoding).编码分两步完成: 标记化, 然后转换为输入 ID –> input_ids
6.5 类型标记ID(token_type_ids)
类型标记ID(token_type_ids)的作用就是告诉模型输入的哪一部分是第一句, 哪一部分是第二句。
inputs = tokenizer("This is the first sentence.", "This is the second one.")
inputs
# tokenizer.convert_ids_to_tokens(inputs["input_ids"])
# ['[CLS]', 'this', 'is', 'the', 'first', 'sentence', '.', '[SEP]', 'this', 'is', 'the', 'second', 'one', '.', '[SEP]']
6.6 填充 padding
6.7 截断 truncation
6.8 Special tokens
分词器在开头添加特殊词[CLS], 在结尾添加特殊词[SEP]。
Transformers模块的矢量输出通常较大。它通常有三个维度:
- Batch size: 一次处理的序列数(在我们的示例中为2)。
- Sequence length: 序列的数值表示的长度(在我们的示例中为16)。
- Hidden size: 每个模型输入的向量维度。
参考:
transformers 4.4.2 documentation
聊一聊大模型 | 京东云技术团队
Attention Is All You Need
Attention: A Mathematical Walkthrough