Transformers(库) 是 Hugging Face 开发的 开源 Python 库,用于加载和使用基于 Transformer 结构的各种预训练模型。Transformers(库)是对 Transformer(理论)的工程实现,并且包含了很多不同的 Transformer 变种(GPT、BERT、LLaMA 等)。
目录
- transformers简介
- transformers 库支持的模型和框架
- 自然语言处理
- 自然语言处理发展简史
- 统计语言模型发展史
- N-gram模型
- NNLM 模型
- Word2Vec 模型
- ELMo 模型
- BERT 模型
- LLM大语言模型
- Transformer 模型
- 什么是 Transformer
- 迁移学习
- Transformer 的结构
- Transformer 家族
- Encoder 分支
- Decoder 分支
- Encoder-Decoder 分支
- transformers库基本使用
- 快速开始
- 开箱即用的 pipelines
- 情感分析
- 文本生成
- 遮盖词填充
- 命名实体识别
- 自动问答
- 自动摘要
- 这些 pipeline 背后做了什么?
- 使用分词器进行预处理
- 将预处理好的输入送入模型
- 对模型输出进行后处理
- 模型与分词器
- 模型
- 分词器
- 分词策略
- 加载与保存分词器
- 编码与解码文本
- 处理多段文本
- Padding 操作
- Attention Mask
- 直接使用分词器
- 编码句子对
- 添加 Token
- 调整 embedding 矩阵
- Token embedding 初始化
- 参考文档
transformers简介
Transformers 是由 Hugging Face 开发的一个 NLP 包,支持加载目前绝大部分的预训练模型。随着 BERT、GPT 等大规模语言模型的兴起,越来越多的公司和研究者采用 Transformers 库来构建 NLP 应用。
我们可以使用 Transformers 库提供的 API 轻松下载和训练最先进的预训练模型。使用预训练模型可以降低计算成本,以及节省从头开始训练模型的时间。这些模型可用于不同模态的任务,例如:
- 文本:文本分类、信息抽取、问答系统、文本摘要、机器翻译和文本生成。
- 图像:图像分类、目标检测和图像分割。
- 音频:语音识别和音频分类。
- 多模态:表格问答系统、OCR、扫描文档信息抽取、视频分类和视觉问答。
读者只需要熟悉 Python 语言即可,并不需要提前掌握 Keras、Pytorch 等深度学习包的使用。
transformers 库支持的模型和框架
下表展示了库中对每个模型的支持情况, 是否具有Python分词器 (表中的”Tokenizer slow”). 是否具有由🤗Tokenizers库支持的快速分词器(表中的”Tokenizer fast”), 是否支持Jax (通过Flax), PyTorch, 和TensorFlow.
Model | Tokenizer slow | Tokenizer fast | PyTorch support | TensorFlow support | Flax Support |
---|---|---|---|---|---|
ALBERT | ✅ | ✅ | ✅ | ✅ | ✅ |
AltCLIP | ❌ | ❌ | ✅ | ❌ | ❌ |
Audio Spectrogram Transformer | ❌ | ❌ | ✅ | ❌ | ❌ |
BART | ✅ | ✅ | ✅ | ✅ | ✅ |
BEiT | ❌ | ❌ | ✅ | ❌ | ✅ |
BERT | ✅ | ✅ | ✅ | ✅ | ✅ |
Bert Generation | ✅ | ❌ | ✅ | ❌ | ❌ |
BigBird | ✅ | ✅ | ✅ | ❌ | ✅ |
BigBird-Pegasus | ❌ | ❌ | ✅ | ❌ | ❌ |
BioGpt | ✅ | ❌ | ✅ | ❌ | ❌ |
BiT | ❌ | ❌ | ✅ | ❌ | ❌ |
Blenderbot | ✅ | ✅ | ✅ | ✅ | ✅ |
BlenderbotSmall | ✅ | ✅ | ✅ | ✅ | ✅ |
BLIP | ❌ | ❌ | ✅ | ❌ | ❌ |
BLOOM | ❌ | ✅ | ✅ | ❌ | ❌ |
CamemBERT | ✅ | ✅ | ✅ | ✅ | ❌ |
CANINE | ✅ | ❌ | ✅ | ❌ | ❌ |
Chinese-CLIP | ❌ | ❌ | ✅ | ❌ | ❌ |
CLIP | ✅ | ✅ | ✅ | ✅ | ✅ |
CLIPSeg | ❌ | ❌ | ✅ | ❌ | ❌ |
CodeGen | ✅ | ✅ | ✅ | ❌ | ❌ |
Conditional DETR | ❌ | ❌ | ✅ | ❌ | ❌ |
ConvBERT | ✅ | ✅ | ✅ | ✅ | ❌ |
ConvNeXT | ❌ | ❌ | ✅ | ✅ | ❌ |
CTRL | ✅ | ❌ | ✅ | ✅ | ❌ |
CvT | ❌ | ❌ | ✅ | ✅ | ❌ |
Data2VecAudio | ❌ | ❌ | ✅ | ❌ | ❌ |
Data2VecText | ❌ | ❌ | ✅ | ❌ | ❌ |
Data2VecVision | ❌ | ❌ | ✅ | ✅ | ❌ |
DeBERTa | ✅ | ✅ | ✅ | ✅ | ❌ |
DeBERTa-v2 | ✅ | ✅ | ✅ | ✅ | ❌ |
Decision Transformer | ❌ | ❌ | ✅ | ❌ | ❌ |
Deformable DETR | ❌ | ❌ | ✅ | ❌ | ❌ |
DeiT | ❌ | ❌ | ✅ | ✅ | ❌ |
DETR | ❌ | ❌ | ✅ | ❌ | ❌ |
DiNAT | ❌ | ❌ | ✅ | ❌ | ❌ |
DistilBERT | ✅ | ✅ | ✅ | ✅ | ✅ |
DonutSwin | ❌ | ❌ | ✅ | ❌ | ❌ |
DPR | ✅ | ✅ | ✅ | ✅ | ❌ |
DPT | ❌ | ❌ | ✅ | ❌ | ❌ |
ELECTRA | ✅ | ✅ | ✅ | ✅ | ✅ |
Encoder decoder | ❌ | ❌ | ✅ | ✅ | ✅ |
ERNIE | ❌ | ❌ | ✅ | ❌ | ❌ |
ESM | ✅ | ❌ | ✅ | ✅ | ❌ |
FairSeq Machine-Translation | ✅ | ❌ | ✅ | ❌ | ❌ |
FlauBERT | ✅ | ❌ | ✅ | ✅ | ❌ |
FLAVA | ❌ | ❌ | ✅ | ❌ | ❌ |
FNet | ✅ | ✅ | ✅ | ❌ | ❌ |
Funnel Transformer | ✅ | ✅ | ✅ | ✅ | ❌ |
GIT | ❌ | ❌ | ✅ | ❌ | ❌ |
GLPN | ❌ | ❌ | ✅ | ❌ | ❌ |
GPT Neo | ❌ | ❌ | ✅ | ❌ | ✅ |
GPT NeoX | ❌ | ✅ | ✅ | ❌ | ❌ |
GPT NeoX Japanese | ✅ | ❌ | ✅ | ❌ | ❌ |
GPT-J | ❌ | ❌ | ✅ | ✅ | ✅ |
GPT-Sw3 | ✅ | ✅ | ✅ | ✅ | ✅ |
GroupViT | ❌ | ❌ | ✅ | ✅ | ❌ |
Hubert | ❌ | ❌ | ✅ | ✅ | ❌ |
I-BERT | ❌ | ❌ | ✅ | ❌ | ❌ |
ImageGPT | ❌ | ❌ | ✅ | ❌ | ❌ |
Jukebox | ✅ | ❌ | ✅ | ❌ | ❌ |
LayoutLM | ✅ | ✅ | ✅ | ✅ | ❌ |
LayoutLMv2 | ✅ | ✅ | ✅ | ❌ | ❌ |
LayoutLMv3 | ✅ | ✅ | ✅ | ✅ | ❌ |
LED | ✅ | ✅ | ✅ | ✅ | ❌ |
LeViT | ❌ | ❌ | ✅ | ❌ | ❌ |
LiLT | ❌ | ❌ | ✅ | ❌ | ❌ |
Longformer | ✅ | ✅ | ✅ | ✅ | ❌ |
LongT5 | ❌ | ❌ | ✅ | ❌ | ✅ |
LUKE | ✅ | ❌ | ✅ | ❌ | ❌ |
LXMERT | ✅ | ✅ | ✅ | ✅ | ❌ |
M-CTC-T | ❌ | ❌ | ✅ | ❌ | ❌ |
M2M100 | ✅ | ❌ | ✅ | ❌ | ❌ |
Marian | ✅ | ❌ | ✅ | ✅ | ✅ |
MarkupLM | ✅ | ✅ | ✅ | ❌ | ❌ |
Mask2Former | ❌ | ❌ | ✅ | ❌ | ❌ |
MaskFormer | ❌ | ❌ | ✅ | ❌ | ❌ |
MaskFormerSwin | ❌ | ❌ | ❌ | ❌ | ❌ |
mBART | ✅ | ✅ | ✅ | ✅ | ✅ |
Megatron-BERT | ❌ | ❌ | ✅ | ❌ | ❌ |
MobileBERT | ✅ | ✅ | ✅ | ✅ | ❌ |
MobileNetV1 | ❌ | ❌ | ✅ | ❌ | ❌ |
MobileNetV2 | ❌ | ❌ | ✅ | ❌ | ❌ |
MobileViT | ❌ | ❌ | ✅ | ✅ | ❌ |
MPNet | ✅ | ✅ | ✅ | ✅ | ❌ |
MT5 | ✅ | ✅ | ✅ | ✅ | ✅ |
MVP | ✅ | ✅ | ✅ | ❌ | ❌ |
NAT | ❌ | ❌ | ✅ | ❌ | ❌ |
Nezha | ❌ | ❌ | ✅ | ❌ | ❌ |
Nyströmformer | ❌ | ❌ | ✅ | ❌ | ❌ |
OpenAI GPT | ✅ | ✅ | ✅ | ✅ | ❌ |
OpenAI GPT-2 | ✅ | ✅ | ✅ | ✅ | ✅ |
OPT | ❌ | ❌ | ✅ | ✅ | ✅ |
OWL-ViT | ❌ | ❌ | ✅ | ❌ | ❌ |
Pegasus | ✅ | ✅ | ✅ | ✅ | ✅ |
PEGASUS-X | ❌ | ❌ | ✅ | ❌ | ❌ |
Perceiver | ✅ | ❌ | ✅ | ❌ | ❌ |
PLBart | ✅ | ❌ | ✅ | ❌ | ❌ |
PoolFormer | ❌ | ❌ | ✅ | ❌ | ❌ |
ProphetNet | ✅ | ❌ | ✅ | ❌ | ❌ |
QDQBert | ❌ | ❌ | ✅ | ❌ | ❌ |
RAG | ✅ | ❌ | ✅ | ✅ | ❌ |
REALM | ✅ | ✅ | ✅ | ❌ | ❌ |
Reformer | ✅ | ✅ | ✅ | ❌ | ❌ |
RegNet | ❌ | ❌ | ✅ | ✅ | ❌ |
RemBERT | ✅ | ✅ | ✅ | ✅ | ❌ |
ResNet | ❌ | ❌ | ✅ | ✅ | ❌ |
RetriBERT | ✅ | ✅ | ✅ | ❌ | ❌ |
RoBERTa | ✅ | ✅ | ✅ | ✅ | ✅ |
RoBERTa-PreLayerNorm | ❌ | ❌ | ✅ | ✅ | ✅ |
RoCBert | ✅ | ❌ | ✅ | ❌ | ❌ |
RoFormer | ✅ | ✅ | ✅ | ✅ | ✅ |
SegFormer | ❌ | ❌ | ✅ | ✅ | ❌ |
SEW | ❌ | ❌ | ✅ | ❌ | ❌ |
SEW-D | ❌ | ❌ | ✅ | ❌ | ❌ |
Speech Encoder decoder | ❌ | ❌ | ✅ | ❌ | ✅ |
Speech2Text | ✅ | ❌ | ✅ | ✅ | ❌ |
Speech2Text2 | ✅ | ❌ | ❌ | ❌ | ❌ |
Splinter | ✅ | ✅ | ✅ | ❌ | ❌ |
SqueezeBERT | ✅ | ✅ | ✅ | ❌ | ❌ |
Swin Transformer | ❌ | ❌ | ✅ | ✅ | ❌ |
Swin Transformer V2 | ❌ | ❌ | ✅ | ❌ | ❌ |
Swin2SR | ❌ | ❌ | ✅ | ❌ | ❌ |
SwitchTransformers | ❌ | ❌ | ✅ | ❌ | ❌ |
T5 | ✅ | ✅ | ✅ | ✅ | ✅ |
Table Transformer | ❌ | ❌ | ✅ | ❌ | ❌ |
TAPAS | ✅ | ❌ | ✅ | ✅ | ❌ |
Time Series Transformer | ❌ | ❌ | ✅ | ❌ | ❌ |
TimeSformer | ❌ | ❌ | ✅ | ❌ | ❌ |
Trajectory Transformer | ❌ | ❌ | ✅ | ❌ | ❌ |
Transformer-XL | ✅ | ❌ | ✅ | ✅ | ❌ |
TrOCR | ❌ | ❌ | ✅ | ❌ | ❌ |
UniSpeech | ❌ | ❌ | ✅ | ❌ | ❌ |
UniSpeechSat | ❌ | ❌ | ✅ | ❌ | ❌ |
UPerNet | ❌ | ❌ | ✅ | ❌ | ❌ |
VAN | ❌ | ❌ | ✅ | ❌ | ❌ |
VideoMAE | ❌ | ❌ | ✅ | ❌ | ❌ |
ViLT | ❌ | ❌ | ✅ | ❌ | ❌ |
Vision Encoder decoder | ❌ | ❌ | ✅ | ✅ | ✅ |
VisionTextDualEncoder | ❌ | ❌ | ✅ | ❌ | ✅ |
VisualBERT | ❌ | ❌ | ✅ | ❌ | ❌ |
ViT | ❌ | ❌ | ✅ | ✅ | ✅ |
ViT Hybrid | ❌ | ❌ | ✅ | ❌ | ❌ |
ViTMAE | ❌ | ❌ | ✅ | ✅ | ❌ |
ViTMSN | ❌ | ❌ | ✅ | ❌ | ❌ |
Wav2Vec2 | ✅ | ❌ | ✅ | ✅ | ✅ |
Wav2Vec2-Conformer | ❌ | ❌ | ✅ | ❌ | ❌ |
WavLM | ❌ | ❌ | ✅ | ❌ | ❌ |
Whisper | ✅ | ❌ | ✅ | ✅ | ❌ |
X-CLIP | ❌ | ❌ | ✅ | ❌ | ❌ |
XGLM | ✅ | ✅ | ✅ | ✅ | ✅ |
XLM | ✅ | ❌ | ✅ | ✅ | ❌ |
XLM-ProphetNet | ✅ | ❌ | ✅ | ❌ | ❌ |
XLM-RoBERTa | ✅ | ✅ | ✅ | ✅ | ✅ |
XLM-RoBERTa-XL | ❌ | ❌ | ✅ | ❌ | ❌ |
XLNet | ✅ | ✅ | ✅ | ✅ | ❌ |
YOLOS | ❌ | ❌ | ✅ | ❌ | ❌ |
YOSO | ❌ | ❌ | ✅ | ❌ | ❌ |
自然语言处理
自然语言处理(Natural Language Processing,NLP)是一门借助计算机技术研究人类语言的科学。虽然该领域的发展历史不长,但是其发展迅速并且取得了许多令人印象深刻的成果。
自然语言处理发展简史
第一阶段:不懂语法怎么理解语言?
20 世纪 50 年代到 70 年代,人们对自然语言处理的认识都局限在人类学习语言的方式上,用了二十多年时间苦苦探寻让计算机理解语言的方法,最终却一无所获。
**当时的学术界普遍认为,要让计算机处理自然语言必须先让其理解语言,因此分析语句和获取语义成为首要任务,而这主要依靠语言学家人工总结文法规则来实现。**特别是 20 世纪 60 年代,基于乔姆斯基形式语言(Chomsky Formal languages)的编译器取得了很大进展,更加鼓舞了研究者通过概括语法规则来处理自然语言的信心。
但是与规范严谨的程序语言不同,自然语言复杂又灵活,是一种上下文有关文法(Context-Sensitive Grammars,CSGs),因此仅靠人工编写文法规则根本无法覆盖,而且随着编写的规则数量越来越多、形式越来越复杂,规则与规则之间还可能会存在矛盾。因此这一阶段自然语言处理的研究可以说进入了误区。
第二阶段:只要看的足够多,就能处理语言
正如人类是通过空气动力学而不是简单模仿鸟类造出了飞机,计算机处理自然语言也未必需要理解语言。
20 世纪 70 年代,随着统计语言学的提出,基于数学模型和统计方法的自然语言处理方法开始兴起。当时的代表性方法是“通信系统加隐马尔可夫模型”,其输入和输出都是一维且保持原有次序的符号序列,可以处理语音识别、词性分析等任务,但是这种方法在面对输出为二维树形结构的句法分析以及符号次序有很大变化的机器翻译等任务时就束手无策了。
20 世纪 80 年代以来,随着硬件计算能力的提高以及海量互联网数据的出现,越来越多的统计机器学习方法被应用到自然语言处理领域,例如一些研究者引入基于有向图的统计模型来处理复杂的句法分析任务。2005 年 Google 公司基于统计方法的翻译系统更是全面超过了基于规则的 SysTran 系统。
2006 年,随着辛顿(Hinton)证明深度信念网络(Deep Belief Networks,DBN)可以通过逐层预训练策略有效地进行训练,基于神经网络和反向传播算法(Back Propagation)的深度学习方法开始兴起。许多之前由于缺乏数据、计算能力以及有效优化方法而被忽视的神经网络模型得到了复兴。例如 1997 年就已提出的长短时记忆网络(Long Short Term Memory,LSTM)模型在重新被启用后在许多任务上大放异彩。
即使在 Transformer 模型几乎“一统江湖”的今天,LSTM 模型依然占有一席之地。2024 年 5 月 8 日,LSTM
提出者和奠基者 Sepp Hochreiter 公布了 LSTM 模型的改良版本——xLSTM,在性能和扩展方面都得到了显著提升。
随着越来越多研究者将注意力转向深度学习方法,诸如卷积神经网络(Convolutional Neural Networks,CNN)等模型被广泛地应用到各种自然语言处理任务中。2017 年,Google 公司提出了 Attention 注意力模型,论文中提出的 Transformer 结构更是引领了后续神经网络语言模型的发展。
得益于抛弃了让计算机简单模仿人类的思路,这一阶段自然语言处理研究出现了蓬勃发展。今天可以说已经没有人再会质疑统计方法在自然语言处理上的可行性。
统计语言模型发展史
要让计算机处理自然语言,首先需要为自然语言建立数学模型,这种模型被称为“统计语言模型”,其核心思想是判断一个文字序列是否构成人类能理解并且有意义的句子。这个问题曾经困扰了学术界很多年。
N-gram模型
20 世纪 70 年代之前,研究者尝试从文字序列是否合乎文法、含义是否正确的角度来建立语言模型。最终,随着人工编写出的规则数量越来越多、形式越来越复杂,对语言模型的研究陷入瓶颈。直到 20 世纪 70 年代中期,IBM 实验室的贾里尼克(Jelinek)为了研究语音识别问题换了一个思路,用一个简单的统计模型就解决了这个问题。
贾里尼克的想法是要判断一个文字序列w1,w2,w3…wn是否合理,就计算这个句子P(S)
出现的概率 ,出现概率越大句子就越合理,
其中,词语 wn 出现的概率取决于在句子中出现在它之前的所有词(理论上也可以引入出现在它之后的词语)。但是,随着文本长度的增加,条件概率 P(wn|w1,w2,…wn)会变得越来越难以计算,因而在实际计算时会假设每个词语 wi 仅与它前面的 N-1个词语有关,即:
这种假设被称为马尔可夫(Markov)假设,对应的语言模型被称为 N 元(N-gram)模型。例如当N=2时,词语wi 出现的概率只与它前面的词语 wi-1有关,被称为二元(Bigram)模型;而 N=1时,模型实际上就是一个上下文无关模型。由于 N 元模型的空间和时间复杂度都几乎是 N 的指数函数,因此实际应用中比较常见的是取N=3的三元模型。
即使是使用三元、四元甚至是更高阶的语言模型,依然无法覆盖所有的语言现象。在自然语言中,上下文之间的关联性可能跨度非常大,例如从一个段落跨到另一个段落,这是马尔可夫假设解决不了的。此时就需要使用
LSTM、Transformer 等模型来捕获词语之间的远距离依赖(Long Distance Dependency)了。
NNLM 模型
2003 年,本吉奥(Bengio)提出了神经网络语言模型(Neural Network Language Model,NNLM)。由于神经网络在当时并不被人们看好,在之后的十年中 NNLM 模型都没有引起很大关注。
直到 2013 年,随着越来越多的研究者使用深度学习模型来处理自然语言,NNLM 模型才被重新发掘,并成为使用神经网络建模语言的经典范例。NNLM 模型的思路与统计语言模型保持一致,它通过输入词语前面的 N-1 个词语来预测当前词。
具体来说,NNLM 模型首先从词表 C
中查询得到前面 N-1
个词语对应的词向量C(wt-n+1) ,C(wt-n+2),…,C(wt-2),C(wt-1),然后将这些词向量拼接后输入到带有激活函数的隐藏层中,通过 Softmax
函数预测当前词语的概率。特别地,包含所有词向量的词表矩阵 C 也是模型的参数,需要通过学习获得。因此 NNLM 模型不仅能够能够根据上文预测当前词语,同时还能够给出所有词语的词向量(Word Embedding)。
Word2Vec 模型
真正将神经网络语言模型发扬光大的是 2013 年 Google 公司提出的 Word2Vec 模型。Word2Vec 模型提供的词向量在很长一段时间里都是自然语言处理方法的标配,即使是后来出现的 Glove 模型也难掩它的光芒。
Word2Vec 的模型结构和 NNLM 基本一致,只是训练方法有所不同,分为 CBOW (Continuous Bag-of-Words) 和 Skip-gram 两种:
其中 CBOW 使用周围的词语 w(t-2),w(t-1),w(t+1),w(t+2)来预测当前词 w(t),而 Skip-gram 则正好相反,它使用当前词 w(t) 来预测它的周围词语。
可以看到,与严格按照统计语言模型结构设计的 NNLM 模型不同,Word2Vec 模型在结构上更加自由,训练目标也更多地是为获得词向量服务。特别是同时通过上文和下文来预测当前词语的 CBOW 训练方法打破了语言模型“只通过上文来预测当前词”的固定思维,为后续一系列神经网络语言模型的发展奠定了基础。
然而,有一片乌云一直笼罩在 Word2Vec 模型的上空——多义词问题。一词多义是语言灵活性和高效性的体现,但是 Word2Vec 模型却无法处理多义词,一个词语无论表达何种语义,Word2Vec 模型都只能提供相同的词向量,即将多义词编码到了完全相同的参数空间。实际上在 20 世纪 90 年代初,雅让斯基(Yarowsky)就给出了一个简洁有效的解决方案——运用词语之间的互信息(Mutual Information)。
具体来说,对于多义词,可以使用文本中与其同时出现的互信息最大的词语集合来表示不同的语义。例如对于“苹果”,当表示水果时,周围出现的一般就是“超市”、“香蕉”等词语;而表示“苹果公司”时,周围出现的一般就是“手机”、“平板”等词语,
因此,在判断多义词究竟表达何种语义时,只需要查看哪个语义对应集合中的词语在上下文中出现的更多就可以了,即通过上下文来判断语义。
后来自然语言处理的标准流程就是先将 Word2Vec 模型提供的词向量作为模型的输入,然后通过 LSTM、CNN 等模型结合上下文对句子中的词语重新进行编码,以获得包含上下文信息的词语表示。
ELMo 模型
为了更好地解决多义词问题,2018 年研究者提出了 ELMo 模型(Embeddings from Language Models)。与 Word2Vec 模型只能提供静态词向量不同,ELMo 模型会根据上下文动态地调整词语的词向量。
具体来说,ELMo 模型首先对语言模型进行预训练,使得模型掌握编码文本的能力;然后在实际使用时,对于输入文本中的每一个词语,都提取模型各层中对应的词向量拼接起来作为新的词向量。ELMo 模型采用双层双向 LSTM 作为编码器,从两个方向编码词语的上下文信息,相当于将编码层直接封装到了语言模型中。
训练完成后 ELMo 模型不仅学习到了词向量,还训练好了一个双层双向的 LSTM 编码器。对于输入文本中的词语,可以从第一层 LSTM 中得到包含句法信息的词向量,从第二层 LSTM 中得到包含语义信息的词向量,最终通过加权求和得到每一个词语最终的词向量。
但是 ELMo 模型存在两个缺陷:首先它使用 LSTM 模型作为编码器,而不是当时已经提出的编码能力更强的 Transformer 模型;其次 ELMo 模型直接通过拼接来融合双向抽取特征的做法也略显粗糙。
不久之后,将 ELMo 模型中的 LSTM 更换为 Transformer 的 GPT 模型就出现了。但是 GPT 模型再次追随了 NNLM 的脚步,只通过词语的上文进行预测,这在很大程度上限制了模型的应用场景。例如对于文本分类、阅读理解等任务,如果不把词语的下文信息也嵌入到词向量中就会白白丢掉很多信息。
BERT 模型
2018 年底随着 BERT 模型(Bidirectional Encoder Representations from Transformers)的出现,这一阶段神经网络语言模型的发展终于出现了一位集大成者,发布时 BERT 模型在 11 个任务上都取得了最好性能。
BERT 模型采用和 GPT 模型类似的两阶段框架,首先对语言模型进行预训练,然后通过微调来完成下游任务。但是,BERT 不仅像 GPT 模型一样采用 Transformer 作为编码器,而且采用了类似 ELMo 模型的双向语言模型结构。因此 BERT 模型不仅编码能力强大,而且对各种下游任务,BERT 模型都可以通过简单地改造输出部分来完成。
但是 BERT 模型的优点同样也是它的缺陷,由于 BERT 模型采用双向语言模型结构,因而无法直接用于生成文本。
可以看到,从 2003 年 NNLM 模型提出时的无人问津,到 2018 年底 BERT 模型横扫自然语言处理领域,神经网络语言模型的发展也经历了一波三折。在此期间,研究者一直在不断地对前人的工作进行改进,这才取得了 BERT 模型的成功。BERT 模型的出现并非一蹴而就,它不仅借鉴了 ELMo、GPT 等模型的结构与框架,而且延续了 Word2Vec 模型提出的 CBOW 训练方式的思想,可以看作是这一阶段语言模型发展的集大成者。
在 BERT 模型取得成功之后,研究者并没有停下脚步,在 BERT 模型的基础上又提出了诸如 MASS、ALBERT、RoBERTa 等改良模型。其中具有代表性的就是微软提出的 UNILM 模型(UNIfied pretrained Language Model),它把 BERT 模型的 MASK 机制运用到了一个很高的水平。
具体来说,UNILM 模型通过给 Transformer 中的 Self-Attention 机制添加不同的 MASK 矩阵,在不改变 BERT 模型结构的基础上同时实现了双向、单向和序列到序列(Sequence-to-Sequence,Seq2Seq)语言模型,是一种对 BERT 模型进行扩展的优雅方案。
LLM大语言模型
除了优化模型结构,研究者发现扩大模型规模也可以提高性能。在保持模型结构以及预训练任务基本不变的情况下,仅仅通过扩大模型规模就可以显著增强模型能力,尤其当规模达到一定程度时,模型甚至展现出了能够解决未见过复杂问题的涌现(Emergent Abilities)能力。例如 175B 规模的 GPT-3 模型只需要在输入中给出几个示例,就能通过上下文学习(In-context Learning)完成各种小样本(Few-Shot)任务,而这是 1.5B 规模的 GPT-2 模型无法做到的。
在规模扩展定律(Scaling Laws)被证明对语言模型有效之后,研究者基于 Transformer 结构不断加深模型深度,构建出了许多大语言模型:
一个标志性的事件是 2022 年 11 月 30 日 OpenAI 公司发布了面向普通消费者的 ChatGPT 模型(Chat Generative Pre-trained Transformer),它能够记住先前的聊天内容真正像人类一样交流,甚至能撰写诗歌、论文、文案、代码等。发布后,ChatGPT 模型引起了巨大轰动,上线短短 5 天注册用户数就超过 100 万。2023 年一月末,ChatGPT 活跃用户数量已经突破 1 亿,成为史上增长最快的消费者应用。
首先是数百亿参数的大语言模型:
- Flan-T5(11B):指令微调(Instruction Tuning)研究领域的代表性模型,通过扩大任务数量、扩大模型规模以及在思维链提示(Chain-of-Thought Prompting)数据上进行微调探索了指令微调技术的应用;
- CodeGen 以及 CodeGen2(11B):为生成代码而设计的自回归(Autoregressive)语言模型,是探索大语言模型代码生成能力的一个代表性模型;
- mT0(13B):多语言(Multilingual)大语言模型的代表,使用多语言提示在多语言任务上进行了微调;
- Baichuan 以及 Baichuan-2(7B):百川智能公司开发的大语言模型,支持中英双语,在多个中英文基准测试上取得优异性能;
- PanGu-α(13B):华为公司开发的中文大语言模型,在零样本(Zero-Shot)和小样本(Few-Shot)设置下展现出了优异的性能;
- Qwen(72B):阿里巴巴公司开源的多语言大模型,在语言理解、推理、数学等方面均展现出了优秀的模型能力,还为代码、数学和多模态设计了专业化版本 Code-Qwen、Math-Qwen、Qwen-VL 等可供用户使用;
- LLaMA 以及 LLaMA-2(65B):在一系列指令遵循(Instruction Following)任务中展现出卓越性能。由于 LLaMA 模型的开放性和有效性,吸引了许多研究者在其之上指令微调或继续预训练不同的模型版本,例如 Stanford Alpaca 模型、Vicuna 模型等。
- Mixtral(46.7B):稀疏混合专家模型架构的大语言模型,这也是较早对外公开的 MoE 架构的语言模型,其处理速度和资源消耗与 12.9B 参数的模型相当,在 MT-bench 基准上取得了与 GPT-3.5 相当的性能表现;
数千亿计参数规模的大语言模型:
- OPT(175B)以及指令微调版本 OPT-IML:致力于开放共享,使得研究者可以对大规模模型进行可复现的研究;
- BLOOM 以及 BLOOMZ(176B):跨语言泛化(Cross-Lingual Generalization)研究领域的代表性模型,具有多语言建模的能力;
- GLM:双语大语言模型,其小规模中文聊天版本 ChatGLM2-6B 在中文任务研究中十分流行,在效率和容量方面有许多改进,支持量化(Quantization)、32K 长度的上下文、快速推理等。
Transformer 模型
自从 BERT 和 GPT 模型取得重大成功之后, Transformer 结构已经替代了循环神经网络 (RNN) 和卷积神经网络 (CNN),成为了当前 NLP 模型的标配。
2017 年 Google 在《Attention Is All You Need》中提出了 Transformer 结构用于序列标注,在翻译任务上超过了之前最优秀的循环神经网络模型;与此同时,Fast AI 在《Universal Language Model Fine-tuning for Text Classification》中提出了一种名为 ULMFiT 的迁移学习方法,将在大规模数据上预训练好的 LSTM 模型迁移用于文本分类,只用很少的标注数据就达到了最佳性能。
这些具有开创性的工作促成了两个著名 Transformer 模型的出现:
- GPT (the Generative Pretrained Transformer);
- BERT (Bidirectional Encoder Representations from Transformers)。
通过将 Transformer 结构与无监督学习相结合,我们不再需要对每一个任务都从头开始训练模型,并且几乎在所有 NLP 任务上都远远超过先前的最强基准。
GPT 和 BERT 被提出之后,NLP 领域出现了越来越多基于 Transformer 结构的模型,其中比较有名有:
虽然新的 Transformer 模型层出不穷,它们采用不同的预训练目标在不同的数据集上进行训练,但是依然可以按模型结构将它们大致分为三类:
- 纯 Encoder 模型(例如 BERT),又称自编码 (auto-encoding) Transformer 模型;
- 纯 Decoder 模型(例如 GPT),又称自回归 (auto-regressive) Transformer 模型;
- Encoder-Decoder 模型(例如 BART、T5),又称 Seq2Seq (sequence-to-sequence) Transformer 模型。
什么是 Transformer
Transformer 模型本质上都是预训练语言模型,大都采用自监督学习 (Self-supervised learning)
的方式在大量生语料上进行训练
,也就是说,训练这些 Transformer 模型完全不需要人工标注数据。
自监督学习是一种训练目标可以根据模型的输入自动计算的训练方法。
例如下面两个常用的预训练任务:
-
基于句子的前 n 个词来预测下一个词,因为输出依赖于过去和当前的输入,因此该任务被称为因果语言建模 (causal language modeling);
-
基于上下文(周围的词语)来预测句子中被遮盖掉的词语 (masked word),因此该任务被称为遮盖语言建模 (masked language modeling)。
这些语言模型虽然可以对训练过的语言产生统计意义上的理解,例如可以根据上下文预测被遮盖掉的词语,但是如果直接拿来完成特定任务,效果往往并不好。因此,我们通常还会采用迁移学习 (transfer learning) 方法,使用特定任务的标注语料,以有监督学习的方式对预训练模型参数进行微调 (fine-tune),以取得更好的性能。
实际上,“因果语言建模”就是上一章中说的统计语言模型,只使用前面的词来预测当前词,由 NNLM 首次运用;而“遮盖语言建模”实际上就是Word2Vec 模型提出的 CBOW。
迁移学习
除了 DistilBERT 等少数模型,大部分 Transformer 模型都为了取得更好的性能而不断地增加模型大小(参数量)和增加预训练数据。下图展示了近年来模型大小的变化趋势:
但是,从头训练一个预训练语言模型,尤其是大模型,需要海量的数据,不仅时间和计算成本非常高,对环境的影响也很大!可以想象,如果每一次研究者或是公司想要使用语言模型,都需要基于海量数据从头训练,将耗费巨大且不必要的全球成本,因此共享语言模型非常重要。只要在预训练好的模型权重上构建模型,就可以大幅地降低计算成本。
现在也有一些工作致力于在尽可能保持模型性能的情况下大幅减少参数量,达到用“小模型”获得媲美“大模型”的效果(例如模型蒸馏,即大模型教小模型,Deepseek就是)。
预训练是一种从头开始训练模型的方式:所有的模型权重都被随机初始化,然后在没有任何先验知识的情况下开始训练:
这个过程不仅需要海量的训练数据,而且时间和经济成本都非常高。
因此,大部分情况下,我们都不会从头训练模型,而是将别人预训练好的模型权重通过迁移学习应用到自己的模型中,即使用自己的任务语料对模型进行“二次训练”,通过微调参数使模型适用于新任务。
这种迁移学习的好处是:
- 预训练时模型很可能已经见过与我们任务类似的数据集,通过微调可以激发出模型在预训练过程中获得的知识,将基于海量数据获得的统计理解能力应用于我们的任务;
- 由于模型已经在大量数据上进行过预训练,微调时只需要很少的数据量就可以达到不错的性能;
- 换句话说,在自己任务上获得优秀性能所需的时间和计算成本都可以很小。
例如,我们可以选择一个在大规模英文语料上预训练好的模型,使用 arXiv 语料进行微调,以生成一个面向学术/研究领域的模型。这个微调的过程只需要很少的数据:我们相当于将预训练模型已经获得的知识“迁移”到了新的领域,因此被称为迁移学习。
与从头训练相比,微调模型所需的时间、数据、经济和环境成本都要低得多,并且与完整的预训练相比,微调训练的约束更少,因此迭代尝试不同的微调方案也更快、更容易。实践证明,即使是对于自定义任务,除非你有大量的语料,否则相比训练一个专门的模型,基于预训练模型进行微调会是一个更好的选择。
在绝大部分情况下,我们都应该尝试找到一个尽可能接近我们任务的预训练模型,然后微调它,也就是所谓的“站在巨人的肩膀上”。
Transformer 的结构
标准的 Transformer 模型主要由两个模块构成:
- Encoder(左边):负责理解输入文本,为每个输入构造对应的语义表示(语义特征);
- Decoder(右边):负责生成输出,使用 Encoder 输出的语义表示结合其他输入来生成目标序列。
这两个模块可以根据任务的需求而单独使用:
- 纯 Encoder 模型:适用于只需要理解输入语义的任务,例如句子分类、命名实体识别;
- 纯 Decoder 模型:适用于生成式任务,例如文本生成;
- Encoder-Decoder 模型或 Seq2Seq 模型:适用于需要基于输入的生成式任务,例如翻译、摘要。
Transformer 模型的标志就是采用了注意力层 (Attention Layers) 的结构,前面也说过,提出 Transformer 结构的论文名字就叫《Attention Is All You Need》。顾名思义,注意力层的作用就是让模型在处理文本时,将注意力只放在某些词语上。
例如要将英文“You like this course”翻译为法语,由于法语中“like”的变位方式因主语而异,因此需要同时关注相邻的词语“You”。同样地,在翻译“this”时还需要注意“course”,因为“this”的法语翻译会根据相关名词的极性而变化。对于复杂的句子,要正确翻译某个词语,甚至需要关注离这个词很远的词。
同样的概念也适用于其他 NLP 任务:虽然词语本身就有语义,但是其深受上下文的影响,同一个词语出现在不同上下文中可能会有完全不同的语义(例如“我买了一个苹果”和“我买了一个苹果手机”中的“苹果”)。
多义词的问题是 Word2Vec 这些静态模型所解决不了的。
Transformer 模型本来是为了翻译任务而设计的。在训练过程中,Encoder 接受源语言的句子作为输入,而 Decoder 则接受目标语言的翻译作为输入。在 Encoder 中,由于翻译一个词语需要依赖于上下文,因此注意力层可以访问句子中的所有词语;而 Decoder 是顺序地进行解码,在生成每个词语时,注意力层只能访问前面已经生成的单词。
例如,假设翻译模型当前已经预测出了三个词语,我们会把这三个词语作为输入送入 Decoder,然后 Decoder 结合 Encoder 所有的源语言输入来预测第四个词语。
实际训练中为了加快速度,会将整个目标序列都送入 Decoder,然后在注意力层中通过 Mask
遮盖掉未来的词语来防止信息泄露。例如我们在预测第三个词语时,应该只能访问到已生成的前两个词语,如果 Decoder
能够访问到序列中的第三个(甚至后面的)词语,就相当于作弊了。
原始的 Transformer 模型结构如下图所示,Encoder 在左,Decoder 在右:
其中,Decoder 中的第一个注意力层关注 Decoder 过去所有的输入,而第二个注意力层则是使用 Encoder 的输出,因此 Decoder 可以基于整个输入句子来预测当前词语。这对于翻译任务非常有用,因为同一句话在不同语言下的词语顺序可能并不一致(不能逐词翻译),所以出现在源语言句子后部的词语反而可能对目标语言句子前部词语的预测非常重要。
在 Encoder/Decoder 的注意力层中,我们还会使用 Attention Mask
遮盖掉某些词语来防止模型关注它们,例如为了将数据处理为相同长度而向序列中添加的填充 (padding) 字符。
Transformer 家族
虽然新的 Transformer 模型层出不穷,但是它们依然可以被归纳到以下三种结构中:
Encoder 分支
纯 Encoder 模型只使用 Transformer 模型中的 Encoder 模块,也被称为自编码 (auto-encoding) 模型。在每个阶段,注意力层都可以访问到原始输入句子中的所有词语,即具有“双向 (Bi-directional)”注意力。
纯 Encoder 模型通常通过破坏给定的句子(例如随机遮盖其中的词语),然后让模型进行重构来进行预训练,最适合处理那些需要理解整个句子语义的任务,例如句子分类、命名实体识别(词语分类)、抽取式问答。
BERT 是第一个基于 Transformer 结构的纯 Encoder 模型,它在提出时横扫了整个 NLP 界,在流行的 GLUE 基准上超过了当时所有的最强模型。随后的一系列工作对 BERT 的预训练目标和架构进行调整以进一步提高性能。目前,纯 Encoder 模型依然在 NLP 行业中占据主导地位。
下面是BERT 模型及它的常见变体:
- BERT:通过预测文本中被遮盖的词语和判断一个文本是否跟随另一个来进行预训练,前一个任务被称为遮盖语言建模 (Masked Language Modeling, MLM),后一个任务被称为下句预测 (Next Sentence Prediction, NSP);
- DistilBERT:尽管 BERT 性能优异,但它的模型大小使其难以部署在低延迟需求的环境中。通过在预训练期间使用知识蒸馏 (knowledge distillation) 技术,DistilBERT 在内存占用减少 40%、计算速度提高 60% 的情况下,依然可以保持 97% 的性能;
- RoBERTa:BERT 之后的一项研究表明,通过修改预训练方案可以进一步提高性能。RoBERTa 在更多的训练数据上,以更大的批次训练了更长的时间,并且放弃了 NSP 任务。与 BERT 模型相比,这些改变显著地提高了模型的性能;
- XLM:跨语言语言模型 (XLM) 探索了构建多语言模型的多个预训练目标,包括来自 GPT 的自回归语言建模和来自 BERT 的 MLM,还将 MLM 拓展到多语言输入,提出了翻译语言建模 (Translation Language Modeling, TLM)。XLM 在多个多语言 NLU 基准和翻译任务上都取得了最好的性能;
- XLM-RoBERTa:跟随 XLM 和 RoBERTa,XLM-RoBERTa (XLM-R) 通过升级训练数据来改进多语言预训练。其基于 Common Crawl 创建了一个 2.5 TB 的语料,然后运用 MLM 训练编码器,由于没有平行对照文本,因此移除了 XLM 的 TLM 目标。最终,该模型大幅超越了 XLM 和多语言 BERT 变体;
- ALBERT:ALBERT 通过三处变化使得 Encoder 架构更高效:首先将词嵌入维度与隐藏维度解耦以减少模型参数;其次所有模型层共享参数;最后将 NSP 任务替换为句子排序预测(判断句子顺序是否被交换)。这些变化使得可以用更少的参数训练更大的模型,并在 NLU 任务上取得了优异的性能;
- ELECTRA:MLM 在每个训练步骤中只有被遮盖掉词语的表示会得到更新。ELECTRA 使用了一种双模型方法来解决这个问题:第一个模型继续按标准 MLM 工作;第二个模型(鉴别器)则预测第一个模型的输出中哪些词语是被遮盖的,这使得训练效率提高了 30 倍。下游任务使用时,鉴别器也参与微调;
- DeBERTa:DeBERTa 模型引入了两处架构变化。首先将词语的内容与相对位置分离,使得自注意力层 (Self-Attention) 层可以更好地建模邻近词语对的依赖关系;此外在解码头的 softmax 层之前添加了绝对位置嵌入。DeBERTa 是第一个在 SuperGLUE 基准上击败人类的模型。
Decoder 分支
纯 Decoder 模型只使用 Transformer 模型中的 Decoder 模块。在每个阶段,对于给定的词语,注意力层只能访问句子中位于它之前的词语,即只能迭代地基于已经生成的词语来逐个预测后面的词语,因此也被称为自回归 (auto-regressive) 模型。
纯 Decoder 模型的预训练通常围绕着预测句子中下一个单词展开。纯 Decoder 模型适合处理那些只涉及文本生成的任务。
对 Transformer Decoder 模型的探索在在很大程度上是由 OpenAI 带头进行的,通过使用更大的数据集进行预训练,以及将模型的规模扩大,纯 Decoder 模型的性能也在不断提高。
下面就简要介绍一些常见的生成模型:
- GPT:结合了 Transformer Decoder 架构和迁移学习,通过根据上文预测下一个单词的预训练任务,在 BookCorpus 数据集上进行了预训练。GPT 模型在分类等下游任务上取得了很好的效果;
- GPT-2:受简单且可扩展的预训练方法的启发,OpenAI 通过扩大原始模型和训练集创造了 GPT-2,它能够生成篇幅较长且语义连贯的文本;
- CTRL:GPT-2 虽然可以根据模板 (prompt) 续写文本,但是几乎无法控制生成序列的风格。条件 Transformer 语言模型 (Conditional Transformer Language, CTRL) 通过在序列开头添加特殊的“控制符”以控制生成文本的风格,这样只需要调整控制符就可以生成多样化的文本;
- GPT-3:将 GPT-2 进一步放大 100 倍,GPT-3 具有 1750 亿个参数。除了能生成令人印象深刻的真实篇章之外,还展示了小样本学习 (few-shot learning) 的能力。这个模型目前没有开源;
- GPT-Neo / GPT-J-6B:由于 GPT-3 没有开源,因此一些旨在重新创建和发布 GPT-3 规模模型的研究人员组成了 EleutherAI,训练出了类似 GPT 的 GPT-Neo 和 GPT-J-6B 。当前公布的模型具有 1.3、2.7、60 亿个参数,在性能上可以媲美较小版本的 GPT-3 模型。
Encoder-Decoder 分支
Encoder-Decoder 模型(又称 Seq2Seq 模型)同时使用 Transformer 架构的两个模块。在每个阶段,Encoder 的注意力层都可以访问初始输入句子中的所有单词,而 Decoder 的注意力层则只能访问输入中给定词语之前的词语(即已经解码生成的词语)。
Encoder-Decoder 模型可以使用 Encoder 或 Decoder 模型的目标来完成预训练,但通常会包含一些更复杂的任务。例如,T5 通过随机遮盖掉输入中的文本片段进行预训练,训练目标则是预测出被遮盖掉的文本。Encoder-Decoder 模型适合处理那些需要根据给定输入来生成新文本的任务,例如自动摘要、翻译、生成式问答。
下面简单介绍一些在自然语言理解 (NLU) 和自然语言生成 (NLG) 领域的 Encoder-Decoder 模型:
- T5:将所有 NLU 和 NLG 任务都转换为 Seq2Seq 形式统一解决(例如,文本分类就是将文本送入 Encoder,然后 Decoder 生成文本形式的标签)。T5 通过 MLM 及将所有 SuperGLUE 任务转换为 Seq2Seq 任务来进行预训练。最终,具有 110 亿参数的大版本 T5 在多个基准上取得了最优性能。
- BART:同时结合了 BERT 和 GPT 的预训练过程。将输入句子通过遮盖词语、打乱句子顺序、删除词语、文档旋转等方式破坏后传给 Encoder 编码,然后要求 Decoder 能够重构出原始的文本。这使得模型可以灵活地用于 NLU 或 NLG 任务,并且在两者上都实现了最优性能。
- M2M-100:语言对之间可能存在共享知识可以用来处理小众语言之间的翻译。M2M-100 是第一个可以在 100 种语言之间进行翻译的模型,并且对小众的语言也能生成高质量的翻译。该模型使用特殊的前缀标记来指示源语言和目标语言。
- BigBird:由于注意力机制的内存要求,Transformer 模型只能处理一定长度内的文本。BigBird 通过使用线性扩展的稀疏注意力形式,将可处理的文本长度从大多数模型的 512 扩展到 4096,这对于处理文本摘要等需要捕获长距离依赖的任务特别有用。
transformers库基本使用
快速开始
在开始之前, 确保你已经安装了所有必要的库:
pip install transformers datasets
这个命令是用来在 Python 环境中安装 transformers
和 datasets
两个库的。
transformers
:这是 Hugging Face 提供的一个开源库,用于加载、训练和使用各种预训练的深度学习模型,特别是在自然语言处理(NLP)领域,如 BERT、GPT、T5 等。datasets
:这是 Hugging Face 另一个开源库,提供了便捷的数据加载和处理工具,支持加载多种格式的标准数据集,尤其适用于 NLP 和机器学习任务。
你还需要安装喜欢的机器学习框架,我这里采用torch并且建议去pytorch官网安装:
开箱即用的 pipelines
使用[pipeline]是利用预训练模型进行推理的最简单的方式。你能够将[pipeline]开箱即用地用于跨不同模态的多种任务。
下面是它支持的任务列表:
任务 | 描述 | 模态 | Pipeline |
---|---|---|---|
文本分类 | 为给定的文本序列分配一个标签 | NLP | pipeline(task=“sentiment-analysis”) |
文本生成 | 根据给定的提示生成文本 | NLP | pipeline(task=“text-generation”) |
命名实体识别 | 为序列里的每个token分配一个标签(人, 组织, 地址等等) | NLP | pipeline(task=“ner”) |
问答系统 | 通过给定的上下文和问题, 在文本中提取答案 | NLP | pipeline(task=“question-answering”) |
掩盖填充 | 预测出正确的在序列中被掩盖的token | NLP | pipeline(task=“fill-mask”) |
文本摘要 | 为文本序列或文档生成总结 | NLP | pipeline(task=“summarization”) |
文本翻译 | 将文本从一种语言翻译为另一种语言 | NLP | pipeline(task=“translation”) |
图像分类 | 为图像分配一个标签 | Computer vision | pipeline(task=“image-classification”) |
图像分割 | 为图像中每个独立的像素分配标签(支持语义、全景和实例分割) | Computer vision | pipeline(task=“image-segmentation”) |
目标检测 | 预测图像中目标对象的边界框和类别 | Computer vision | pipeline(task=“object-detection”) |
音频分类 | 给音频文件分配一个标签 | Audio | pipeline(task=“audio-classification”) |
自动语音识别 | 将音频文件中的语音提取为文本 | Audio | pipeline(task=“automatic-speech-recognition”) |
视觉问答 | 给定一个图像和一个问题,正确地回答有关图像的问题 | Multimodal | pipeline(task=“vqa”) |
创建一个pipeline()实例并且指定你想要将它用于的任务就可以了,你可以将pipeline()用于任何一个上面提到的任务。
下面我们以常见的几个 NLP 任务为例,展示如何调用这些 pipeline 模型。
情感分析
借助情感分析 pipeline,我们只需要输入文本,就可以得到其情感标签(积极/消极)以及对应的概率:
from transformers import pipelineclassifier = pipeline("sentiment-analysis")
pipeline() 会下载并缓存一个用于情感分析的默认的预训练模型和分词器. 现在你可以在目标文本上使用 classifier了:
classifier("We are very happy to show you the 🤗 Transformers library.")[{'label': 'POSITIVE', 'score': 0.9998}]
如果你有不止一个输入, 可以把所有输入放入一个列表然后传给pipeline(), 它将会返回一个字典列表:
results = classifier(["We are very happy to show you the 🤗 Transformers library.", "We hope you don't hate it."])
for result in results:print(f"label: {result['label']}, with score: {round(result['score'], 4)}")label: POSITIVE, with score: 0.9998
label: NEGATIVE, with score: 0.5309
pipeline 模型会自动完成以下三个步骤:
- 将文本预处理为模型可以理解的格式;
- 将预处理好的文本送入模型;
- 对模型的预测值进行后处理,输出人类可以理解的格式。
pipeline 会自动选择合适的预训练模型来完成任务。例如对于情感分析,默认就会选择微调好的英文情感模型 distilbert-base-uncased-finetuned-sst-2-english。
虽然pipeline会自动下载模型,但一般建议手动将模型下载到可以找到的地方,通过路径传入model参数,
文本生成
我们首先根据任务需要构建一个模板 (prompt),然后将其送入到模型中来生成后续文本。注意,由于文本生成具有随机性,因此每次运行都会得到不同的结果。
from transformers import pipelinegenerator = pipeline("text-generation")
results = generator("In this course, we will teach you how to")
print(results)
results = generator("In this course, we will teach you how to",num_return_sequences=2,max_length=50
)
print(results)
No model was supplied, defaulted to gpt2 (https://huggingface.co/gpt2)[{'generated_text': "In this course, we will teach you how to use data and models that can be applied in any real-world, everyday situation. In most cases, the following will work better than other courses I've offered for an undergrad or student. In order"}]
[{'generated_text': 'In this course, we will teach you how to make your own unique game called "Mono" from scratch by doing a game engine, a framework and the entire process starting with your initial project. We are planning to make some basic gameplay scenarios and'}, {'generated_text': 'In this course, we will teach you how to build a modular computer, how to run it on a modern Windows machine, how to install packages, and how to debug and debug systems. We will cover virtualization and virtualization without a programmer,'}]
可以看到,pipeline 自动选择了预训练好的 gpt2 模型来完成任务。我们也可以指定要使用的模型。对于文本生成任务,我们可以在 Model Hub 页面左边选择 Text Generation tag 查询支持的模型。例如,我们在相同的 pipeline 中加载 deepseek1.5B 模型:
from transformers import pipelinemodel_path = "G:\\Model\\DeepSeek-R1-Distill-Qwen-1.5B"
generator = pipeline("text-generation", model=model_path)
results = generator("In this course, we will teach you how to",max_length=30,num_return_sequences=2,
)
print(results)
Device set to use cuda:0
Truncation was not explicitly activated but `max_length` is provided a specific value, please use `truncation=True` to explicitly truncate examples to max length. Defaulting to 'longest_first' truncation strategy. If you encode pairs of sequences (GLUE-style) with the tokenizer you can select this strategy more precisely by providing a specific strategy to `truncation`.
[{'generated_text': 'In this course, we will teach you how to use a programming language to solve problems.\nThe problems are categorized into three types: Type A,'}, {'generated_text': 'In this course, we will teach you how to use the Python programming language, which is a programming language, a programming language, a programming language'}]
还可以通过左边的语言 tag 选择其他语言的模型。例如加载专门用于生成中文古诗的 gpt2-chinese-poem 模型:
from transformers import pipelinegenerator = pipeline("text-generation", model="uer/gpt2-chinese-poem")
results = generator("[CLS] 万 叠 春 山 积 雨 晴 ,",max_length=40,num_return_sequences=2,
)
print(results)
[{'generated_text': '[CLS] 万 叠 春 山 积 雨 晴 , 孤 舟 遥 送 子 陵 行 。 别 情 共 叹 孤 帆 远 , 交 谊 深 怜 一 座 倾 。 白 日 风 波 身 外 幻'}, {'generated_text': '[CLS] 万 叠 春 山 积 雨 晴 , 满 川 烟 草 踏 青 行 。 何 人 唤 起 伤 春 思 , 江 畔 画 船 双 橹 声 。 桃 花 带 雨 弄 晴 光'}]
在这段代码中,[CLS]
是一个特殊的标记,它通常用于表示序列的开始。这个标记的作用与模型的预训练有关,尤其是在使用像 BERT 和一些基于 Transformer 的模型时,[CLS]
被用作“分类”标记。
例子中使用的是一个 GPT-2 变种模型 uer/gpt2-chinese-poem
,这是一个用于生成诗歌的中文 GPT-2 模型。尽管 GPT-2 本身并不需要 [CLS]
标记(GPT 系列模型主要依靠自回归结构来生成文本),但某些实现或预处理步骤中可能会保留这个标记以保持与某些传统模型(如 BERT)的兼容性。
"[CLS] 万 叠 春 山 积 雨 晴 ,"
作为输入文本,[CLS]
在这里并不会直接影响生成的文本,而是被视作输入的一部分,可能用于在生成的过程中给模型提供更多的上下文。
遮盖词填充
给定一段部分词语被遮盖掉 (masked) 的文本,使用预训练模型来预测能够填充这些位置的词语。
from transformers import pipelineunmasker = pipeline("fill-mask")
results = unmasker("This course will teach you all about <mask> models.", top_k=2)
print(results)
No model was supplied, defaulted to distilroberta-base (https://huggingface.co/distilroberta-base)[{'sequence': 'This course will teach you all about mathematical models.', 'score': 0.19619858264923096, 'token': 30412, 'token_str': ' mathematical'}, {'sequence': 'This course will teach you all about computational models.', 'score': 0.04052719101309776, 'token': 38163, 'token_str': ' computational'}]
可以看到,pipeline 自动选择了预训练好的 distilroberta-base 模型来完成任务。
命名实体识别
命名实体识别 (NER) pipeline 负责从文本中抽取出指定类型的实体,例如人物、地点、组织等等。
from transformers import pipelinener = pipeline("ner", grouped_entities=True)
results = ner("My name is Sylvain and I work at Hugging Face in Brooklyn.")
print(results)
No model was supplied, defaulted to dbmdz/bert-large-cased-finetuned-conll03-english (https://huggingface.co/dbmdz/bert-large-cased-finetuned-conll03-english)[{'entity_group': 'PER', 'score': 0.9981694, 'word': 'Sylvain', 'start': 11, 'end': 18}, {'entity_group': 'ORG', 'score': 0.97960186, 'word': 'Hugging Face', 'start': 33, 'end': 45}, {'entity_group': 'LOC', 'score': 0.99321055, 'word': 'Brooklyn', 'start': 49, 'end': 57}]
可以看到,模型正确地识别出了 Sylvain 是一个人物,Hugging Face 是一个组织,Brooklyn 是一个地名。
这里通过设置参数 grouped_entities=True,使得 pipeline 自动合并属于同一个实体的多个子词 (token),例如这里将“Hugging”和“Face”合并为一个组织实体,实际上 Sylvain 也进行了子词合并,因为分词器会将 Sylvain 切分为 S、##yl 、##va 和 ##in 四个 token。
自动问答
在自动问答任务中,模型的目标是 从一个给定的上下文中提取出答案,这个上下文通常是一个段落、一篇文章或某些相关的信息。给定一个问题,模型需要基于上下文找到精确的答案。在自动问答中,模型并不会生成新内容,而是从上下文中 精确提取 一个片段作为答案。
例如:
from transformers import pipelinequestion_answerer = pipeline("question-answering")
answer = question_answerer(question="Where do I work?",context="My name is Sylvain and I work at Hugging Face in Brooklyn",
)
print(answer)
No model was supplied, defaulted to distilbert-base-cased-distilled-squad (https://huggingface.co/distilbert-base-cased-distilled-squad){'score': 0.6949771046638489, 'start': 33, 'end': 45, 'answer': 'Hugging Face'}
可以看到,pipeline 自动选择了在 SQuAD 数据集上训练好的 distilbert-base 模型来完成任务。这里的自动问答 pipeline 实际上是一个抽取式问答模型,即从给定的上下文中抽取答案,而不是生成答案。
根据形式的不同,自动问答 (QA) 系统可以分为三种:
- 抽取式 QA (extractive QA):假设答案就包含在文档中,因此直接从文档中抽取答案;
- 多选 QA (multiple-choice QA):从多个给定的选项中选择答案,相当于做阅读理解题;
- 无约束 QA (free-form QA):直接生成答案文本,并且对答案文本格式没有任何限制。
自动问答模型(如基于 BERT 或 T5 的模型)通常是 通过上下文提取答案 的。如果你上传一个非常大的文档,模型本身的能力是有限的,尤其是在处理文档过大或包含大量信息时。模型通常会有一个 最大输入长度限制(如 512 tokens),这意味着它只能处理文档中的一小部分内容。因此对于私域内容检索还是由RAG专门处理。
自动摘要
自动摘要 pipeline 旨在将长文本压缩成短文本,并且还要尽可能保留原文的主要信息,例如:
from transformers import pipelinesummarizer = pipeline("summarization")
results = summarizer("""America has changed dramatically during recent years. Not only has the number of graduates in traditional engineering disciplines such as mechanical, civil, electrical, chemical, and aeronautical engineering declined, but in most of the premier American universities engineering curricula now concentrate on and encourage largely the study of engineering science. As a result, there are declining offerings in engineering subjects dealing with infrastructure, the environment, and related issues, and greater concentration on high technology subjects, largely supporting increasingly complex scientific developments. While the latter is important, it should not be at the expense of more traditional engineering.Rapidly developing economies such as China and India, as well as other industrial countries in Europe and Asia, continue to encourage and advance the teaching of engineering. Both China and India, respectively, graduate six and eight times as many traditional engineers as does the United States. Other industrial countries at minimum maintain their output, while America suffers an increasingly serious decline in the number of engineering graduates and a lack of well-educated engineers."""
)
print(results)
No model was supplied, defaulted to sshleifer/distilbart-cnn-12-6 (https://huggingface.co/sshleifer/distilbart-cnn-12-6)[{'summary_text': ' America has changed dramatically during recent years . The number of engineering graduates in the U.S. has declined in traditional engineering disciplines such as mechanical, civil, electrical, chemical, and aeronautical engineering . Rapidly developing economies such as China and India, as well as other industrial countries in Europe and Asia, continue to encourage and advance engineering .'}]
可以看到,pipeline 自动选择了预训练好的 distilbart-cnn-12-6 模型来完成任务。与文本生成类似,我们也可以通过 max_length 或 min_length 参数来控制返回摘要的长度。
这些 pipeline 背后做了什么?
这些简单易用的 pipeline 模型实际上封装了许多操作,以第一个情感分析 pipeline 为例,我们运行下面的代码:
from transformers import pipelineclassifier = pipeline("sentiment-analysis")
result = classifier("I've been waiting for a HuggingFace course my whole life.")
print(result)
就会得到结果:
[{'label': 'POSITIVE', 'score': 0.9598048329353333}]
实际上它的背后经过了三个步骤:
- 预处理 (preprocessing),将原始文本转换为模型可以接受的输入格式;
- 将处理好的输入送入模型;
- 对模型的输出进行后处理 (postprocessing),将其转换为人类方便阅读的格式。
使用分词器进行预处理
因为神经网络模型无法直接处理文本,因此首先需要通过预处理环节将文本转换为模型可以理解的数字。具体地,我们会使用每个模型对应的分词器 (tokenizer) 来进行:
- 将输入切分为词语、子词或者符号(例如标点符号),统称为 tokens;
- 根据模型的词表将每个 token 映射到对应的 token 编号(就是一个数字);
- 根据模型的需要,添加一些额外的输入。
我们对输入文本的预处理需要与模型自身预训练时的操作完全一致,只有这样模型才可以正常地工作。
注意,每个模型都有特定的预处理操作,如果对要使用的模型不熟悉,可以通过 Model Hub 查询。这里我们使用 AutoTokenizer 类和它的 from_pretrained() 函数,它可以自动根据模型 checkpoint 名称来获取对应的分词器。
情感分析 pipeline 的默认 checkpoint 是 distilbert-base-uncased-finetuned-sst-2-english,下面我们手工下载并调用其分词器:
from transformers import AutoTokenizercheckpoint = "distilbert-base-uncased-finetuned-sst-2-english"
tokenizer = AutoTokenizer.from_pretrained(checkpoint)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)
{'input_ids': tensor([[ 101, 1045, 1005, 2310, 2042, 3403, 2005, 1037, 17662, 12172, 2607, 2026, 2878, 2166, 1012, 102],[ 101, 1045, 5223, 2023, 2061, 2172, 999, 102, 0, 0,0, 0, 0, 0, 0, 0]]), 'attention_mask': tensor([[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],[1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0]])
}
可以看到,输出中包含两个键 input_ids 和 attention_mask,其中 input_ids 对应分词之后的 tokens 映射到的数字编号列表,而 attention_mask 则是用来标记哪些 tokens 是被填充的(这里“1”表示是原文,“0”表示是填充字符)。
将预处理好的输入送入模型
预训练模型的下载方式和分词器 (tokenizer) 类似,Transformers 包提供了一个 AutoModel 类和对应的 from_pretrained() 函数。下面我们手工下载这个 distilbert-base 模型:
from transformers import AutoModelcheckpoint = "distilbert-base-uncased-finetuned-sst-2-english"
model = AutoModel.from_pretrained(checkpoint)
预训练模型的本体只包含基础的 Transformer 模块,对于给定的输入,它会输出一些神经元的值,称为 hidden states 或者特征 (features)。对于 NLP 模型来说,可以理解为是文本的高维语义表示。这些 hidden states 通常会被输入到其他的模型部分(称为 head),以完成特定的任务,例如送入到分类头中完成文本分类任务。
其实前面我们举例的所有 pipelines 都具有类似的模型结构,只是模型的最后一部分会使用不同的 head 以完成对应的任务。
Transformers 库封装了很多不同的结构,常见的有:
- *Model (返回 hidden states)
- *ForCausalLM (用于条件语言模型)
- *ForMaskedLM (用于遮盖语言模型)
- *ForMultipleChoice (用于多选任务)
- *ForQuestionAnswering (用于自动问答任务)
- *ForSequenceClassification (用于文本分类任务)
- *ForTokenClassification (用于 token 分类任务,例如 NER)
Transformer 模块的输出是一个维度为 (Batch size, Sequence length, Hidden size) 的三维张量,其中 Batch size 表示每次输入的样本(文本序列)数量,即每次输入多少个句子,上例中为 2;Sequence length 表示文本序列的长度,即每个句子被分为多少个 token,上例中为 16;Hidden size 表示每一个 token 经过模型编码后的输出向量(语义表示)的维度。
预训练模型编码后的输出向量的维度通常都很大,例如 Bert 模型 base 版本的输出为 768 维,一些大模型的输出维度为 3072甚至更高。
我们可以打印出这里使用的 distilbert-base 模型的输出维度:
from transformers import AutoTokenizer, AutoModelcheckpoint = "distilbert-base-uncased-finetuned-sst-2-english"
tokenizer = AutoTokenizer.from_pretrained(checkpoint)
model = AutoModel.from_pretrained(checkpoint)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")
outputs = model(**inputs)
print(outputs.last_hidden_state.shape)
torch.Size([2, 16, 768])
Transformers 模型的输出格式类似 namedtuple 或字典,可以像上面那样通过属性访问,也可以通过键(outputs["last_hidden_state"]
),甚至索引访问(outputs[0])。
对于情感分析任务,很明显我们最后需要使用的是一个文本分类 head。因此,实际上我们不会使用 AutoModel 类,而是使用 AutoModelForSequenceClassification:
from transformers import AutoTokenizer
from transformers import AutoModelForSequenceClassificationcheckpoint = "distilbert-base-uncased-finetuned-sst-2-english"
tokenizer = AutoTokenizer.from_pretrained(checkpoint)
model = AutoModelForSequenceClassification.from_pretrained(checkpoint)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")
outputs = model(**inputs)
print(outputs.logits.shape)
torch.Size([2, 2])
可以看到,对于 batch 中的每一个样本,模型都会输出一个两维的向量(每一维对应一个标签,positive 或 negative)。
对模型输出进行后处理
由于模型的输出只是一些数值,因此并不适合人类阅读。例如我们打印出上面例子的输出:
from transformers import AutoTokenizer
from transformers import AutoModelForSequenceClassificationcheckpoint = "distilbert-base-uncased-finetuned-sst-2-english"
tokenizer = AutoTokenizer.from_pretrained(checkpoint)
model = AutoModelForSequenceClassification.from_pretrained(checkpoint)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")
outputs = model(**inputs)
print(outputs.logits)
tensor([[-1.5607, 1.6123],[ 4.1692, -3.3464]], grad_fn=<AddmmBackward0>)
模型对第一个句子输出 [-1.5607, 1.6123],对第二个句子输出 [ 4.1692, -3.3464],它们并不是概率值,而是模型最后一层输出的 logits 值。要将他们转换为概率值,还需要让它们经过一个 SoftMax 层,例如:
import torch
predictions = torch.nn.functional.softmax(outputs.logits, dim=-1)
print(predictions)
tensor([[4.0195e-02, 9.5980e-01],[9.9946e-01, 5.4418e-04]], grad_fn=<SoftmaxBackward0>)
所有 Transformers 模型都会输出 logits 值,因为训练时的损失函数通常会自动结合激活函数(例如 SoftMax)与实际的损失函数(例如交叉熵 cross entropy)。
这样模型的预测结果就是容易理解的概率值:第一个句子 [0.0402,0.9598],第二个句子 [0.9995,0.0005]。最后,为了得到对应的标签,可以读取模型 config 中提供的 id2label 属性:
print(model.config.id2label){0: 'NEGATIVE', 1: 'POSITIVE'}
于是我们可以得到最终的预测结果:
- 第一个句子: NEGATIVE: 0.0402, POSITIVE: 0.9598
- 第二个句子: NEGATIVE: 0.9995, POSITIVE: 0.0005
模型与分词器
模型
除了像之前使用 AutoModel 根据 checkpoint 自动加载模型以外,我们也可以直接使用模型对应的 Model 类,例如 BERT 对应的就是 BertModel:
from transformers import BertModelmodel = BertModel.from_pretrained("bert-base-cased")
注意,在大部分情况下,我们都应该使用 AutoModel 来加载模型。这样如果我们想要使用另一个模型(比如把 BERT 换成 RoBERTa),只需修改 checkpoint,其他代码可以保持不变。
所有存储在 HuggingFace Model Hub 上的模型都可以通过 Model.from_pretrained() 来加载权重,参数可以像上面一样是 checkpoint 的名称,也可以是本地路径(预先下载的模型目录),例如:
from transformers import BertModelmodel = BertModel.from_pretrained("./models/bert/")
保存模型通过调用 Model.save_pretrained() 函数实现,例如保存加载的 BERT 模型:
from transformers import AutoModelmodel = AutoModel.from_pretrained("bert-base-cased")
model.save_pretrained("./models/bert-base-cased/")
这会在保存路径下创建两个文件:
- config.json:模型配置文件,存储模型结构参数,例如 Transformer 层数、特征空间维度等;
- pytorch_model.bin:又称为 state dictionary,存储模型的权重。
简单来说,配置文件记录模型的结构,模型权重记录模型的参数,这两个文件缺一不可。我们自己保存的模型同样通过 Model.from_pretrained() 加载,只需要传递保存目录的路径。
分词器
由于神经网络模型不能直接处理文本,因此我们需要先将文本转换为数字,这个过程被称为编码 (Encoding),其包含两个步骤:
- 使用分词器 (tokenizer) 将文本按词、子词、字符切分为 tokens;
- 将所有的 token 映射到对应的 token ID。
分词策略
根据切分粒度的不同,分词策略可以分为以下几种:
按词切分 (Word-based)
例如直接利用 Python 的 split() 函数按空格进行分词:
tokenized_text = "Jim Henson was a puppeteer".split()
print(tokenized_text)
['Jim', 'Henson', 'was', 'a', 'puppeteer']
这种策略的问题是会将文本中所有出现过的独立片段都作为不同的 token,从而产生巨大的词表。而实际上很多词是相关的,例如 “dog” 和 “dogs”、“run” 和 “running”,如果给它们赋予不同的编号就无法表示出这种关联性。
词表就是一个映射字典,负责将 token 映射到对应的 ID(从 0 开始)。神经网络模型就是通过这些 token ID 来区分每一个 token。
当遇到不在词表中的词时,分词器会使用一个专门的 [UNK] token 来表示它是 unknown 的。显然,如果分词结果中包含很多 [UNK] 就意味着丢失了很多文本信息,因此一个好的分词策略,应该尽可能不出现 unknown token。
按字符切分 (Character-based)
这种策略把文本切分为字符而不是词语,这样就只会产生一个非常小的词表,并且很少会出现词表外的 tokens。
但是从直觉上来看,字符本身并没有太大的意义,因此将文本切分为字符之后就会变得不容易理解。这也与语言有关,例如中文字符会比拉丁字符包含更多的信息,相对影响较小。此外,这种方式切分出的 tokens 会很多,例如一个由 10 个字符组成的单词就会输出 10 个 tokens,而实际上它们只是一个词。
因此现在广泛采用的是一种同时结合了按词切分和按字符切分的方式——按子词切分 (Subword tokenization)
。
按子词切分 (Subword)
高频词直接保留,低频词被切分为更有意义的子词。例如 “annoyingly” 是一个低频词,可以切分为 “annoying” 和 “ly”,这两个子词不仅出现频率更高,而且词义也得以保留。下图展示了对 “Let’s do tokenization!“ 按子词切分的结果:
可以看到,“tokenization” 被切分为了 “token” 和 “ization”,不仅保留了语义,而且只用两个 token 就表示了一个长词。这种策略只用一个较小的词表就可以覆盖绝大部分文本,基本不会产生 unknown token。尤其对于土耳其语等黏着语,几乎所有的复杂长词都可以通过串联多个子词构成。
在这个上下文中,/w
代表的是 词(word) 的一个标记,用于指示某个单词(token)在文本中的边界。换句话说,它标记了一个完整的词或子词的结束。
这种标记通常是 分词器(tokenizer) 在处理文本时的输出格式之一。具体而言,/w
可能是某种 标注符号,帮助区分分词后的单元,特别是在一些工具中,标记是否是一个单独的词、子词或者特殊符号。
加载与保存分词器
分词器的加载与保存与模型相似,使用 Tokenizer.from_pretrained() 和 Tokenizer.save_pretrained() 函数。例如加载并保存 BERT 模型的分词器:
from transformers import BertTokenizertokenizer = BertTokenizer.from_pretrained("bert-base-cased")
tokenizer.save_pretrained("./models/bert-base-cased/")
同样地,在大部分情况下我们都应该使用 AutoTokenizer 来加载分词器:
from transformers import AutoTokenizertokenizer = AutoTokenizer.from_pretrained("bert-base-cased")
tokenizer.save_pretrained("./models/bert-base-cased/")
调用 Tokenizer.save_pretrained() 函数会在保存路径下创建三个文件:
- special_tokens_map.json:映射文件,里面包含 unknown token 等特殊字符的映射关系;
- tokenizer_config.json:分词器配置文件,存储构建分词器需要的参数;
- vocab.txt:词表,一行一个 token,行号就是对应的 token ID(从 0 开始)。
编码与解码文本
前面说过,文本编码 (Encoding) 过程包含两个步骤:
- 分词:使用分词器按某种策略将文本切分为 tokens;
- 映射:将 tokens 转化为对应的 token IDs。
下面我们首先使用 BERT 分词器来对文本进行分词:
from transformers import AutoTokenizertokenizer = 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']
可以看到,BERT 分词器采用的是子词切分策略,它会不断切分词语直到获得词表中的 token,例如 “transformer” 会被切分为 “transform” 和 “##er”。
在 BERT 和类似的基于子词的分词器(如 WordPiece)中,使用 ##er 而不是简单的 er 来表示子词是为了区分 前缀 和 后缀 之间的关系,特别是在词语中存在 部分匹配 或 子词组合 的情况下。具体来说,## 用于表示该部分是 词的一部分,而不是一个独立的词。
然后,我们通过 convert_tokens_to_ids() 将切分出的 tokens 转换为对应的 token IDs:
ids = tokenizer.convert_tokens_to_ids(tokens)print(ids)
[7993, 170, 13809, 23763, 2443, 1110, 3014]
还可以通过 encode() 函数将这两个步骤合并,并且 encode() 会自动添加模型需要的特殊 token,例如 BERT 分词器会分别在序列的首尾添加 [CLS] 和 [SEP]:
from transformers import AutoTokenizertokenizer = AutoTokenizer.from_pretrained("bert-base-cased")sequence = "Using a Transformer network is simple"
sequence_ids = tokenizer.encode(sequence)print(sequence_ids)
[101, 7993, 170, 13809, 23763, 2443, 1110, 3014, 102]
其中 101 和 102 分别是 [CLS] 和 [SEP] 对应的 token IDs。
注意,上面这些只是为了演示。在实际编码文本时,最常见的是直接使用分词器进行处理,这样不仅会返回分词后的 token IDs,还包含模型需要的其他输入。例如 BERT 分词器还会自动在输入中添加 token_type_ids 和 attention_mask:
from transformers import AutoTokenizertokenizer = AutoTokenizer.from_pretrained("bert-base-cased")
tokenized_text = tokenizer("Using a Transformer network is simple")
print(tokenized_text)
{'input_ids': [101, 7993, 170, 13809, 23763, 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]}
文本解码 (Decoding) 与编码相反,负责将 token IDs 转换回原来的字符串。注意,解码过程不是简单地将 token IDs 映射回 tokens,还需要合并那些被分为多个 token 的单词。下面我们通过 decode() 函数解码前面生成的 token IDs:
from transformers import AutoTokenizertokenizer = AutoTokenizer.from_pretrained("bert-base-cased")decoded_string = tokenizer.decode([7993, 170, 11303, 1200, 2443, 1110, 3014])
print(decoded_string)decoded_string = tokenizer.decode([101, 7993, 170, 13809, 23763, 2443, 1110, 3014, 102])
print(decoded_string)
Using a transformer network is simple
[CLS] Using a Transformer network is simple [SEP]
解码文本是一个重要的步骤,在进行文本生成、翻译或者摘要等 Seq2Seq (Sequence-to-Sequence) 任务时都会调用这一函数。
处理多段文本
现实场景中,我们往往会同时处理多段文本,而且模型也只接受批 (batch) 数据作为输入,即使只有一段文本,也需要将它组成一个只包含一个样本的 batch,例如:
import torch
from transformers import AutoTokenizer, AutoModelForSequenceClassificationcheckpoint = "distilbert-base-uncased-finetuned-sst-2-english"
tokenizer = AutoTokenizer.from_pretrained(checkpoint)
model = AutoModelForSequenceClassification.from_pretrained(checkpoint)sequence = "I've been waiting for a HuggingFace course my whole life."tokens = tokenizer.tokenize(sequence)
ids = tokenizer.convert_tokens_to_ids(tokens)
# input_ids = torch.tensor(ids), This line will fail.
input_ids = torch.tensor([ids])
print("Input IDs:\n", input_ids)output = model(input_ids)
print("Logits:\n", output.logits)
Input IDs:
tensor([[ 1045, 1005, 2310, 2042, 3403, 2005, 1037, 17662, 12172, 2607,2026, 2878, 2166, 1012]])
Logits:
tensor([[-2.7276, 2.8789]], grad_fn=<AddmmBackward0>)
这里我们通过 [ids] 构建了一个只包含一段文本的 batch,更常见的是送入包含多段文本的 batch:
batched_ids = [ids, ids, ids, ...]
注意,上面的代码仅作为演示。实际场景中,我们应该直接使用分词器对文本进行处理,例如对于上面的例子:
import torch
from transformers import AutoTokenizer, AutoModelForSequenceClassificationcheckpoint = "distilbert-base-uncased-finetuned-sst-2-english"
tokenizer = AutoTokenizer.from_pretrained(checkpoint)
model = AutoModelForSequenceClassification.from_pretrained(checkpoint)sequence = "I've been waiting for a HuggingFace course my whole life."tokenized_inputs = tokenizer(sequence, return_tensors="pt")
print("Inputs Keys:\n", tokenized_inputs.keys())
print("\nInput IDs:\n", tokenized_inputs["input_ids"])output = model(**tokenized_inputs)
print("\nLogits:\n", output.logits)
Inputs Keys:dict_keys(['input_ids', 'attention_mask'])Input IDs:
tensor([[ 101, 1045, 1005, 2310, 2042, 3403, 2005, 1037, 17662, 12172,2607, 2026, 2878, 2166, 1012, 102]])Logits:
tensor([[-1.5607, 1.6123]], grad_fn=<AddmmBackward0>)
可以看到,分词器输出的结果中不仅包含 token IDs(input_ids),还会包含模型需要的其他输入项。前面我们之所以只输入 token IDs 模型也能正常运行,是因为它自动地补全了其他的输入项,例如 attention_mask 等。
Padding 操作
按批输入多段文本产生的一个直接问题就是:batch 中的文本有长有短,而输入张量必须是严格的二维矩形,维度为 (batch size,sequence length),即每一段文本编码后的 token IDs 数量必须一样多。例如下面的 ID 列表是无法转换为张量的:
batched_ids = [[200, 200, 200],[200, 200]
]
我们需要通过 Padding 操作,在短序列的结尾填充特殊的 padding token,使得 batch 中所有的序列都具有相同的长度,例如:
padding_id = 100batched_ids = [[200, 200, 200],[200, 200, padding_id],
]
模型的 padding token ID 可以通过其分词器的 pad_token_id 属性获得。下面我们尝试将两段文本分别以独立以及 batch 的形式送入到模型中:
import torch
from transformers import AutoTokenizer, AutoModelForSequenceClassificationcheckpoint = "distilbert-base-uncased-finetuned-sst-2-english"
tokenizer = AutoTokenizer.from_pretrained(checkpoint)
model = AutoModelForSequenceClassification.from_pretrained(checkpoint)sequence1_ids = [[200, 200, 200]]
sequence2_ids = [[200, 200]]
batched_ids = [[200, 200, 200],[200, 200, tokenizer.pad_token_id],
]print(model(torch.tensor(sequence1_ids)).logits)
print(model(torch.tensor(sequence2_ids)).logits)
print(model(torch.tensor(batched_ids)).logits)
tensor([[ 1.5694, -1.3895]], grad_fn=<AddmmBackward0>)
tensor([[ 0.5803, -0.4125]], grad_fn=<AddmmBackward0>)
tensor([[ 1.5694, -1.3895],[ 1.3374, -1.2163]], grad_fn=<AddmmBackward0>)
问题出现了,使用 padding token 填充的序列的结果竟然与其单独送入模型时不同!
这是因为模型默认会编码输入序列中的所有 token 以建模完整的上下文,因此这里会将填充的 padding token 也一同编码进去,从而生成不同的语义表示。
因此,在进行 Padding 操作时,我们必须明确告知模型哪些 token 是我们填充的,它们不应该参与编码。这就需要使用到 Attention Mask 了。
Attention Mask
Attention Mask 是一个尺寸与 input IDs 完全相同,且仅由 0 和 1 组成的张量,0 表示对应位置的 token 是填充符,不参与计算。当然,一些特殊的模型结构也会借助 Attention Mask 来遮蔽掉指定的 tokens。
对于上面的例子,如果我们通过 attention_mask 标出填充的 padding token 的位置,计算结果就不会有问题了:
import torch
from transformers import AutoTokenizer, AutoModelForSequenceClassificationcheckpoint = "distilbert-base-uncased-finetuned-sst-2-english"
tokenizer = AutoTokenizer.from_pretrained(checkpoint)
model = AutoModelForSequenceClassification.from_pretrained(checkpoint)sequence1_ids = [[200, 200, 200]]
sequence2_ids = [[200, 200]]
batched_ids = [[200, 200, 200],[200, 200, tokenizer.pad_token_id],
]
batched_attention_masks = [[1, 1, 1],[1, 1, 0],
]print(model(torch.tensor(sequence1_ids)).logits)
print(model(torch.tensor(sequence2_ids)).logits)
outputs = model(torch.tensor(batched_ids), attention_mask=torch.tensor(batched_attention_masks))
print(outputs.logits)
tensor([[ 1.5694, -1.3895]], grad_fn=<AddmmBackward0>)
tensor([[ 0.5803, -0.4125]], grad_fn=<AddmmBackward0>)
tensor([[ 1.5694, -1.3895],[ 0.5803, -0.4125]], grad_fn=<AddmmBackward0>)
正如前面强调的那样,在实际使用时,我们应该直接使用分词器对文本进行处理,它不仅会向 token 序列中添加模型需要的特殊字符(例如 [CLS],[SEQ]),还会自动生成对应的 Attention Mask。
目前大部分 Transformer 模型只能接受长度不超过 512 或 1024 的 token 序列,因此对于长序列,有以下三种处理方法:
- 使用一个支持长文的 Transformer 模型,例如 Longformer 和 LED(最大长度 4096);
- 设定最大长度 max_sequence_length 以截断输入序列:sequence = sequence[:max_sequence_length]。
- 将长文切片为短文本块 (chunk),然后分别对每一个 chunk 编码。
直接使用分词器
正如前面所说,在实际使用时,我们应该直接使用分词器来完成包括分词、转换 token IDs、Padding、构建 Attention Mask、截断等操作。例如:
from transformers import AutoTokenizercheckpoint = "distilbert-base-uncased-finetuned-sst-2-english"
tokenizer = AutoTokenizer.from_pretrained(checkpoint)sequences = ["I've been waiting for a HuggingFace course my whole life.", "So have I!"
]model_inputs = tokenizer(sequences)
print(model_inputs)
{'input_ids': [[101, 1045, 1005, 2310, 2042, 3403, 2005, 1037, 17662, 12172, 2607, 2026, 2878, 2166, 1012, 102], [101, 2061, 2031, 1045, 999, 102]], 'attention_mask': [[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], [1, 1, 1, 1, 1, 1]]
}
可以看到,分词器的输出包含了模型需要的所有输入项。对于 DistilBERT 模型,就是 input IDs(input_ids)和 Attention Mask(attention_mask)。
Padding 操作通过 padding 参数来控制:
- padding=“longest”: 将序列填充到当前 batch 中最长序列的长度;
- padding=“max_length”:将所有序列填充到模型能够接受的最大长度,例如 BERT 模型就是 512。
from transformers import AutoTokenizercheckpoint = "distilbert-base-uncased-finetuned-sst-2-english"
tokenizer = AutoTokenizer.from_pretrained(checkpoint)sequences = ["I've been waiting for a HuggingFace course my whole life.", "So have I!"
]model_inputs = tokenizer(sequences, padding="longest")
print(model_inputs)model_inputs = tokenizer(sequences, padding="max_length")
print(model_inputs)
{'input_ids': [[101, 1045, 1005, 2310, 2042, 3403, 2005, 1037, 17662, 12172, 2607, 2026, 2878, 2166, 1012, 102], [101, 2061, 2031, 1045, 999, 102, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]], 'attention_mask': [[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], [1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]]
}{'input_ids': [[101, 1045, 1005, 2310, 2042, 3403, 2005, 1037, 17662, 12172, 2607, 2026, 2878, 2166, 1012, 102, 0, 0, 0, 0, 0, 0, 0, 0, ...], [101, 2061, 2031, 1045, 999, 102, 0, 0, 0, ...]], 'attention_mask': [[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, ...], [1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ...]]
}
截断操作通过 truncation 参数来控制,如果 truncation=True,那么大于模型最大接受长度的序列都会被截断,例如对于 BERT 模型就会截断长度超过 512 的序列。此外,也可以通过 max_length 参数来控制截断长度:
from transformers import AutoTokenizercheckpoint = "distilbert-base-uncased-finetuned-sst-2-english"
tokenizer = AutoTokenizer.from_pretrained(checkpoint)sequences = ["I've been waiting for a HuggingFace course my whole life.", "So have I!"
]model_inputs = tokenizer(sequences, max_length=8, truncation=True)
print(model_inputs)
{'input_ids': [[101, 1045, 1005, 2310, 2042, 3403, 2005, 102], [101, 2061, 2031, 1045, 999, 102]], 'attention_mask': [[1, 1, 1, 1, 1, 1, 1, 1], [1, 1, 1, 1, 1, 1]]
}
分词器还可以通过 return_tensors 参数指定返回的张量格式:设为 pt 则返回 PyTorch 张量;tf 则返回 TensorFlow 张量,np 则返回 NumPy 数组。例如:
from transformers import AutoTokenizercheckpoint = "distilbert-base-uncased-finetuned-sst-2-english"
tokenizer = AutoTokenizer.from_pretrained(checkpoint)sequences = ["I've been waiting for a HuggingFace course my whole life.", "So have I!"
]model_inputs = tokenizer(sequences, padding=True, return_tensors="pt")
print(model_inputs)model_inputs = tokenizer(sequences, padding=True, return_tensors="np")
print(model_inputs)
{'input_ids': tensor([[ 101, 1045, 1005, 2310, 2042, 3403, 2005, 1037, 17662, 12172,2607, 2026, 2878, 2166, 1012, 102],[ 101, 2061, 2031, 1045, 999, 102, 0, 0, 0, 0,0, 0, 0, 0, 0, 0]]), 'attention_mask': tensor([[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],[1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]])
}{'input_ids': array([[ 101, 1045, 1005, 2310, 2042, 3403, 2005, 1037, 17662,12172, 2607, 2026, 2878, 2166, 1012, 102],[ 101, 2061, 2031, 1045, 999, 102, 0, 0, 0,0, 0, 0, 0, 0, 0, 0]]), 'attention_mask': array([[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],[1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]])
}
实际使用分词器时,我们通常会同时进行 padding 操作和截断操作,并设置返回格式为 Pytorch 张量,这样就可以直接将分词结果送入模型:
from transformers import AutoTokenizer, AutoModelForSequenceClassificationcheckpoint = "distilbert-base-uncased-finetuned-sst-2-english"
tokenizer = AutoTokenizer.from_pretrained(checkpoint)
model = AutoModelForSequenceClassification.from_pretrained(checkpoint)sequences = ["I've been waiting for a HuggingFace course my whole life.", "So have I!"
]tokens = tokenizer(sequences, padding=True, truncation=True, return_tensors="pt")
print(tokens)
output = model(**tokens)
print(output.logits)
{'input_ids': tensor([[ 101, 1045, 1005, 2310, 2042, 3403, 2005, 1037, 17662, 12172,2607, 2026, 2878, 2166, 1012, 102],[ 101, 2061, 2031, 1045, 999, 102, 0, 0, 0, 0,0, 0, 0, 0, 0, 0]]), 'attention_mask': tensor([[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],[1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]])}tensor([[-1.5607, 1.6123],[-3.6183, 3.9137]], grad_fn=<AddmmBackward0>)
在 padding=True, truncation=True 设置下,同一个 batch 中的序列都会 padding 到相同的长度,并且大于模型最大接受长度的序列会被自动截断。
编码句子对
除了对单段文本进行编码以外(batch 只是并行地编码多个单段文本),对于 BERT 等包含“句子对”预训练任务的模型,它们的分词器都支持对“句子对”进行编码,例如:
from transformers import AutoTokenizercheckpoint = "bert-base-uncased"
tokenizer = AutoTokenizer.from_pretrained(checkpoint)inputs = tokenizer("This is the first sentence.", "This is the second one.")
print(inputs)tokens = tokenizer.convert_ids_to_tokens(inputs["input_ids"])
print(tokens)
{'input_ids': [101, 2023, 2003, 1996, 2034, 6251, 1012, 102, 2023, 2003, 1996, 2117, 2028, 1012, 102], 'token_type_ids': [0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1], 'attention_mask': [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]}['[CLS]', 'this', 'is', 'the', 'first', 'sentence', '.', '[SEP]', 'this', 'is', 'the', 'second', 'one', '.', '[SEP]']
此时分词器会使用 [SEP] token 拼接两个句子,输出形式为“[CLS] sentencel [SEP] sentence2 [SEP]”的 token 序列,这也是 BERT 模型预期的“句子对”输入格式。
返回结果中除了前面我们介绍过的 input_ids 和 attention_mask 之外,还包含了一个 token_type_ids 项,用于标记哪些 token 属于第一个句子,哪些属于第二个句子。如果我们将上面例子中的 token_type_ids 项与 token 序列对齐:
['[CLS]', 'this', 'is', 'the', 'first', 'sentence', '.', '[SEP]', 'this', 'is', 'the', 'second', 'one', '.', '[SEP]']
[ 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1]
就可以看到第一个句子“[CLS] sentence1 [SEP]”所有 token 的 type ID 都为 0,而第二个句子“sentence2 [SEP] ”对应的 token type ID 都为 1。
如果我们选择其他模型,分词器的输出不一定会包含 token_type_ids 项(例如 DistilBERT模型)。分词器只需保证输出格式与模型预训练时的输入一致即可。
实际使用时,我们不需要去关注编码结果中是否包含 token_type_ids 项,分词器会根据 checkpoint 自动调整输出格式,例如:
from transformers import AutoTokenizercheckpoint = "bert-base-uncased"
tokenizer = AutoTokenizer.from_pretrained(checkpoint)sentence1_list = ["First sentence.", "This is the second sentence.", "Third one."]
sentence2_list = ["First sentence is short.", "The second sentence is very very very long.", "ok."]tokens = tokenizer(sentence1_list,sentence2_list,padding=True,truncation=True,return_tensors="pt"
)
print(tokens)
print(tokens['input_ids'].shape)
{'input_ids': tensor([[ 101, 2034, 6251, 1012, 102, 2034, 6251, 2003, 2460, 1012, 102, 0,0, 0, 0, 0, 0, 0],[ 101, 2023, 2003, 1996, 2117, 6251, 1012, 102, 1996, 2117, 6251, 2003,2200, 2200, 2200, 2146, 1012, 102],[ 101, 2353, 2028, 1012, 102, 7929, 1012, 102, 0, 0, 0, 0,0, 0, 0, 0, 0, 0]]), 'token_type_ids': tensor([[0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0],[0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],[0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]]), 'attention_mask': tensor([[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 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, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]])
}
torch.Size([3, 18])
可以看到分词器成功地输出了形式为“[CLS] sentence1 [SEP] sentence2 [SEP]”的 token 序列,并且将三个序列都 padding 到了相同的长度。
添加 Token
实际操作中,我们还经常会遇到输入中需要包含特殊标记符的情况,例如使用 [ENT_START] 和 [ENT_END] 标记出文本中的实体。由于这些自定义 token 并不在预训练模型原来的词表中,因此直接运用分词器处理就会出现问题。
例如直接使用 BERT 分词器处理下面的句子:
from transformers import AutoTokenizercheckpoint = "bert-base-uncased"
tokenizer = AutoTokenizer.from_pretrained(checkpoint)sentence = 'Two [ENT_START] cars [ENT_END] collided in a [ENT_START] tunnel [ENT_END] this morning.'
print(tokenizer.tokenize(sentence))
['two', '[', 'en', '##t', '_', 'start', ']', 'cars', '[', 'en', '##t', '_', 'end', ']', 'collided', 'in', 'a', '[', 'en', '##t', '_', 'start', ']', 'tunnel', '[', 'en', '##t', '_', 'end', ']', 'this', 'morning', '.']
由于分词器无法识别 [ENT_START] 和 [ENT_END] ,因此将它们都当作未知字符处理,例如“[ENT_END]”被切分成了 ‘[’, ‘en’, ‘##t’, ‘_’, ‘end’, ‘]’ 六个 token。
此外,一些领域的专业词汇,例如使用多个词语的缩写拼接而成的医学术语,同样也不在模型的词表中,因此也会出现上面的问题。此时我们就需要将这些新 token 添加到模型的词表中,让分词器与模型可以识别并处理这些 token。
Transformers 库提供了两种方式来添加新 token,分别是:
-
add_tokens() 添加普通 token:参数是新 token 列表,如果 token 不在词表中,就会被添加到词表的最后。
checkpoint = "bert-base-uncased" tokenizer = AutoTokenizer.from_pretrained(checkpoint)num_added_toks = tokenizer.add_tokens(["new_token1", "my_new-token2"]) print("We have added", num_added_toks, "tokens")
We have added 2 tokens
为了防止 token 已经包含在词表中,我们还可以预先对新 token 列表进行过滤:
new_tokens = ["new_token1", "my_new-token2"] new_tokens = set(new_tokens) - set(tokenizer.vocab.keys()) tokenizer.add_tokens(list(new_tokens))
-
add_special_tokens() 添加特殊 token:参数是包含特殊 token 的字典,键值只能从 bos_token, eos_token, unk_token, sep_token, pad_token, cls_token, mask_token, additional_special_tokens 中选择。同样地,如果 token 不在词表中,就会被添加到词表的最后。添加后,还可以通过特殊属性来访问这些 token,例如 tokenizer.cls_token 就指向 cls token。
checkpoint = "bert-base-uncased" tokenizer = AutoTokenizer.from_pretrained(checkpoint)special_tokens_dict = {"cls_token": "[MY_CLS]"}num_added_toks = tokenizer.add_special_tokens(special_tokens_dict) print("We have added", num_added_toks, "tokens")assert tokenizer.cls_token == "[MY_CLS]"
We have added 1 tokens
我们也可以使用 add_tokens() 添加特殊 token,只需要额外设置参数 special_tokens=True:
checkpoint = "bert-base-uncased" tokenizer = AutoTokenizer.from_pretrained(checkpoint)num_added_toks = tokenizer.add_tokens(["[NEW_tok1]", "[NEW_tok2]"]) num_added_toks = tokenizer.add_tokens(["[NEW_tok3]", "[NEW_tok4]"], special_tokens=True)print("We have added", num_added_toks, "tokens") print(tokenizer.tokenize('[NEW_tok1] Hello [NEW_tok2] [NEW_tok3] World [NEW_tok4]!'))
We have added 2 tokens ['[new_tok1]', 'hello', '[new_tok2]', '[NEW_tok3]', 'world', '[NEW_tok4]', '!']
特殊 token 的标准化 (normalization) 与普通 token 有一些不同,比如不会被小写。
这里我们使用的是不区分大小写的 BERT 模型,因此分词后添加的普通 token [NEW_tok1] 和 [NEW_tok2] 都被处理为了小写,而添加的特殊 token [NEW_tok3] 和 [NEW_tok4]则保持大写。
对于前面的例子,很明显实体标记符 [ENT_START] 和 [ENT_END]属于特殊 token,因此按添加特殊 token 的方式进行:
from transformers import AutoTokenizercheckpoint = "bert-base-uncased"
tokenizer = AutoTokenizer.from_pretrained(checkpoint)num_added_toks = tokenizer.add_tokens(['[ENT_START]', '[ENT_END]'], special_tokens=True)
# num_added_toks = tokenizer.add_special_tokens({'additional_special_tokens': ['[ENT_START]', '[ENT_END]']})
print("We have added", num_added_toks, "tokens")sentence = 'Two [ENT_START] cars [ENT_END] collided in a [ENT_START] tunnel [ENT_END] this morning.'print(tokenizer.tokenize(sentence))
We have added 2 tokens
['two', '[ENT_START]', 'cars', '[ENT_END]', 'collided', 'in', 'a', '[ENT_START]', 'tunnel', '[ENT_END]', 'this', 'morning', '.']
每种特殊标记有特定的功能,它们帮助模型理解文本的结构或上下文关系。下面是这些特殊标记的详细含义:
-
bos_token
(Beginning of Sentence Token)- 作用:表示句子的开始,通常用于一些模型(例如 GPT-2)来指示生成的文本从哪里开始。
- 例子:在一些生成任务中,模型会用
bos_token
来标记一个新的句子的起始。
-
eos_token
(End of Sentence Token)- 作用:表示句子的结束。它用于指示一个文本序列的结尾。
- 例子:在文本生成任务中,模型在生成完整的句子或段落后,会添加
eos_token
来结束生成。
-
unk_token
(Unknown Token)- 作用:表示词汇表中没有的词。通常在分词器无法找到一个词时,会将其替换为
unk_token
,使得模型能够处理未知的词。 - 例子:在文本输入中,某个词如果不在词表中(例如拼写错误或新词),就会被转换为
unk_token
。
- 作用:表示词汇表中没有的词。通常在分词器无法找到一个词时,会将其替换为
-
sep_token
(Separator Token)- 作用:分隔符,用于区分两个文本片段,特别是在处理句子对时。例如在 BERT 中,
sep_token
用于分隔句子对中的两句话,帮助模型理解它们之间的关系。 - 例子:在句子对任务中,如问答任务或自然语言推理任务中,
sep_token
用于分隔问题和上下文。
- 作用:分隔符,用于区分两个文本片段,特别是在处理句子对时。例如在 BERT 中,
-
pad_token
(Padding Token)- 作用:用于填充短于最大长度的句子,使所有输入的句子长度一致。它在批处理时尤为重要,可以使得每个输入的张量具有相同的维度。
- 例子:如果模型的输入批次包含长度不等的句子,短句会被填充为
pad_token
,以确保输入的统一性。
-
cls_token
(Classification Token)- 作用:表示文本的 [CLS] 标记,通常用于分类任务。在 BERT 等模型中,
cls_token
是用来捕捉整个序列的上下文信息,模型根据该标记来进行句子级别的任务,如分类。 - 例子:在 BERT 中,
[CLS]
作为第一个标记添加到文本的开始,用于后续的分类任务。
- 作用:表示文本的 [CLS] 标记,通常用于分类任务。在 BERT 等模型中,
-
mask_token
(Mask Token)- 作用:用于表示被掩盖的词。BERT 和类似的模型使用掩蔽语言模型(Masked Language Model, MLM)进行预训练,掩蔽一些词,并要求模型预测这些被掩蔽的词。
mask_token
用来标记这些被掩蔽的词。 - 例子:在 BERT 中,
[MASK]
被用来替代输入文本中的某些词,模型需要推测出被掩蔽的词是什么。
- 作用:用于表示被掩盖的词。BERT 和类似的模型使用掩蔽语言模型(Masked Language Model, MLM)进行预训练,掩蔽一些词,并要求模型预测这些被掩蔽的词。
-
additional_special_tokens
(Additional Special Tokens)- 作用:自定义的额外特殊标记,可以用于自定义的任务或场景。例如,可以添加某些任务特定的标记,或者根据数据集的特殊需求定义新的标记。
- 例子:例如,在处理某些领域的文本时,可能会添加一些专用的标记,如
<NUM>
、<URL>
等,来标记文本中的特定部分。
调整 embedding 矩阵
向词表中添加新 token 后,必须重置模型 embedding 矩阵的大小,也就是向矩阵中添加新 token 对应的 embedding,这样模型才可以正常工作,将 token 映射到对应的 embedding。
调整 embedding 矩阵通过 resize_token_embeddings() 函数来实现,例如对于前面的例子:
from transformers import AutoTokenizer, AutoModelcheckpoint = "bert-base-uncased"
tokenizer = AutoTokenizer.from_pretrained(checkpoint)
model = AutoModel.from_pretrained(checkpoint)print('vocabulary size:', len(tokenizer))
num_added_toks = tokenizer.add_tokens(['[ENT_START]', '[ENT_END]'], special_tokens=True)
print("After we add", num_added_toks, "tokens")
print('vocabulary size:', len(tokenizer))model.resize_token_embeddings(len(tokenizer))
print(model.embeddings.word_embeddings.weight.size())# Randomly generated matrix
print(model.embeddings.word_embeddings.weight[-2:, :])
vocabulary size: 30522
After we add 2 tokens
vocabulary size: 30524
torch.Size([30524, 768])tensor([[-0.0325, -0.0224, 0.0044, ..., -0.0088, -0.0078, -0.0110],[-0.0005, -0.0167, -0.0009, ..., 0.0110, -0.0282, -0.0013]],grad_fn=<SliceBackward0>)
可以看到,在添加 [ENT_TART] 和 [ENT_END] 之后,分词器的词表大小从 30522 增加到了 30524,模型 embedding 矩阵的大小也成功调整为了 30524 X 768。(在默认情况下,新添加 token 的 embedding 是随机初始化的。
)
我们尝试打印出新添加 token 对应的 embedding(新 token 会添加在词表的末尾,因此只需打印出最后两行)。如果你多次运行上面的代码,就会发现每次打印出的 [ENT_TART] 和 [ENT_END] 的 embedding 是不同的
Token embedding 初始化
如果有充分的语料对模型进行微调或者继续预训练,那么将新添加 token 初始化为随机向量没什么问题。但是如果训练语料较少,甚至是只有很少语料的 few-shot learning 场景下,这种做法就存在问题。研究表明,在训练数据不够多的情况下,这些新添加 token 的 embedding 只会在初始值附近小幅波动。换句话说,即使经过训练,它们的值事实上还是随机的。
因此,在很多情况下,我们需要手工初始化新添加 token 的 embedding,这可以通过直接对 embedding 矩阵赋值来实现。例如我们将上面例子中两个新 token 的 embedding 都初始化为全零向量:
import torchwith torch.no_grad():model.embeddings.word_embeddings.weight[-2:, :] = torch.zeros([2, model.config.hidden_size], requires_grad=True)
print(model.embeddings.word_embeddings.weight[-2:, :])
tensor([[0., 0., 0., ..., 0., 0., 0.],[0., 0., 0., ..., 0., 0., 0.]], grad_fn=<SliceBackward0>)
注意,初始化 embedding 的过程并不可导,因此这里通过 torch.no_grad() 暂停梯度的计算。
现实场景中,更为常见的做法是使用已有 token 的 embedding 来初始化新添加 token。例如对于上面的例子,我们可以将 [ENT_START] 和 [ENT_END] 的值都初始化为“entity” token 对应的 embedding。
import torchtoken_id = tokenizer.convert_tokens_to_ids('entity')
token_embedding = model.embeddings.word_embeddings.weight[token_id]
print(token_id)with torch.no_grad():for i in range(1, num_added_toks+1):model.embeddings.word_embeddings.weight[-i:, :] = token_embedding.clone().detach().requires_grad_(True)
print(model.embeddings.word_embeddings.weight[-2:, :])
9178
tensor([[-0.0039, -0.0131, -0.0946, ..., -0.0223, 0.0107, -0.0419],[-0.0039, -0.0131, -0.0946, ..., -0.0223, 0.0107, -0.0419]],grad_fn=<SliceBackward0>)
因为 token ID 就是 token 在 embedding 矩阵中的索引,因此这里我们直接通过 weight[token_id]取出“entity”对应的 embedding。
可以看到最终结果符合我们的预期,[ENT_START] 和 [ENT_END] 被初始化为相同的 embedding。
更为高级的做法是根据新添加 token 的语义来进行初始化。例如将值初始化为 token 语义描述中所有 token 的平均值,假设新 token ti的语义描述为 wi,1wi,2…,wi,n,那么初始化 ti的 embedding 为:
这里 E 表示预训练模型的 embedding 矩阵。对于上面的例子,我们可以分别为 [ENT_START]和 [ENT_END编写对应的描述,然后再对它们的值进行初始化:
descriptions = ['start of entity', 'end of entity']with torch.no_grad():for i, token in enumerate(reversed(descriptions), start=1):tokenized = tokenizer.tokenize(token)print(tokenized)tokenized_ids = tokenizer.convert_tokens_to_ids(tokenized)new_embedding = model.embeddings.word_embeddings.weight[tokenized_ids].mean(axis=0)model.embeddings.word_embeddings.weight[-i, :] = new_embedding.clone().detach().requires_grad_(True)
print(model.embeddings.word_embeddings.weight[-2:, :])
['end', 'of', 'entity']
['start', 'of', 'entity']
tensor([[-0.0340, -0.0144, -0.0441, ..., -0.0016, 0.0318, -0.0151],[-0.0060, -0.0202, -0.0312, ..., -0.0084, 0.0193, -0.0296]],grad_fn=<SliceBackward0>)
可以看到,这里成功地将 [ENT_START] 的 embedding 初始化为“start”、“of”、“entity”三个 token 的平均值,将 [ENT_END] 初始化为“end”、“of”、“entity”的平均值。
参考文档
HuggingFace transformers 库文档的中文文档:https://github.com/liuzard/transformers_zh_docs