简介
Transformers 是由 Hugging Face 开发的一个 NLP
包,支持加载目前绝大部分的预训练模型。随着 BERT
、GPT
等大规模语言模型的兴起,越来越多的公司和研究者采用 Transformers
库来构建 NLP
应用,因此熟悉 Transformers
库的使用方法很有必要(最近也是看了 Decision Transformer
的相关代码,使用了 Hugging Face 的相关内容,因此在这里系统的看一下)。
本文的主要是对下面参考链接一和链接二的内容进行了整理。会简单介绍 Transformers
的使用,和 Transformers
库中的两个重要组件:模型(Models
类)和分词器(Tokenizers
类)。
参考链接
- Hugging Face 的 Transformers 库快速入门(一):开箱即用的 pipelines,本文主要参考的内容;
- Hugging Face 的 Transformers 库快速入门(二):模型与分词器,本文主要参考的内容;
- Huggingface Transformers Index,Hugging Face 的主页,可以查看详细的接口;
开箱即用的 pipelines
Transformers 库最基础的对象就是 pipeline()
函数,它封装了预训练模型和对应的前处理和后处理环节。我们只需输入文本,就能得到预期的答案。目前常用的 pipelines 有:
feature-extraction
(获得文本的向量化表示)fill-mask
(填充被遮盖的词、片段)ner
(命名实体识别)question-answering
(自动问答)sentiment-analysis
(情感分析)summarization
(自动摘要)text-generation
(文本生成)translation
(机器翻译)zero-shot-classification
(零训练样本分类)
Pipelines 的简单使用
下面看一个使用 pipeline()
函数来进行「文本生成」的例子。我们指定任务和使用的模型,来生成中文的古诗:
- from transformers import pipeline
- generator = pipeline("text-generation", model="uer/gpt2-chinese-poem")
- results = generator(
- "[CLS] 万 叠 春 山 积 雨 晴 ,",
- num_return_sequences=2,
- max_length=50
- )
- print(results)
运行之后可以生成如下的内容:
- [
- {'generated_text': '[CLS] 万 叠 春 山 积 雨 晴 , 烟 消 云 霁 列 峰 生 。 遥 知 草 木 焦 枯 处 , 大 野 流 泉 水 自 清 。 成 新 式 式 金 堤 , 圣 德 神 功 已 尽 跻 。 不'},
- {'generated_text': '[CLS] 万 叠 春 山 积 雨 晴 , 晓 看 沧 海 暮 云 生 。 千 峰 列 翠 依 遥 立 , 双 涧 分 流 自 在 鸣 。 猿 鸟 不 惊 人 世 远 , 鱼 龙 常 共 太 平 平 。 谁'}
- ]
Pipelines 背后做了什么
pipeline()
函数实际上封装了许多操作,下面我们就来了解一下它们背后究竟做了啥。以情感分析 pipeline 为例,我们运行下面的代码:
- from transformers import pipeline
- classifier = 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;
- 根据模型的词表将每个 tokens 映射到对应的 token 编号(就是一个数字);
- 根据模型的需要,添加一些额外的输入。
我们对输入文本的预处理需要与模型自身预训练时的操作完全一致,只有这样模型才可以正常地工作。注意,每个模型都有特定的预处理操作,如果对要使用的模型不熟悉,可以通过 Model Hub 查询。这里我们使用 AutoTokenizer
类和它的 from_pretrained()
函数,它可以自动根据模型 checkpoint 名称来获取对应的分词器。
情感分析 pipeline 的默认 checkpoint 是 distilbert-base-uncased-finetuned-sst-2-english,下面我们手工下载并调用其分词器:
- from transformers import AutoTokenizer
- checkpoint = "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)
输入的结果如下所是,上面的 truncation
表示「截断操作」:
- {
- '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 AutoModel
- checkpoint = "distilbert-base-uncased-finetuned-sst-2-english"
- model = AutoModel.from_pretrained(checkpoint)
预训练模型的本体只包含基础的 Transformer 模块,对于给定的输入,它会输出一些神经元的值,称为 hidden states 或者特征 (features)。对于 NLP 模型来说,可以理解为是文本的高维语义表示。这些 hidden states 通常会被输入到其他的模型部分(称为 head)(也就是上面的模型是没有 head 的),以完成特定的任务,例如送入到分类头中完成文本分类任务。
基本所有的模型都有类似的模型结构,只有模型的最后一个部分会使用不同的 head
来完成相应的任务。下图是一个简单的例子:

Transformer 模块的输出是一个维度为 (Batch size, Sequence length, Hidden size) 的三维张量,其中
- Batch size 表示每次输入的样本(文本序列)数量,即每次输入多少个句子,上例中为 2;
- Sequence length 表示文本序列的长度,即每个句子被分为多少个 token,上例中为 16;
- Hidden size 表示每一个 token 经过模型编码后的输出向量(语义表示)的维度(预训练模型编码后的输出向量的维度通常都很大,例如 Bert 模型 base 版本的输出为 768 维,一些大模型的输出维度为 3072 甚至更高)。
这里我们打印出 distilbert-base
模型的输出维度,将 input
输入模型:
- from transformers import AutoTokenizer
- from transformers import AutoModel
- # 对 input 进行预处理
- checkpoint = "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)
- # 加载模型并输出
- checkpoint = "distilbert-base-uncased-finetuned-sst-2-english"
- model = AutoModel.from_pretrained(checkpoint)
- output = model(**inputs) # 将 input 传入 model
- print(output.last_hidden_state.shape)
Transformers
模型的输出格式类似 namedtuple
或字典,可以像上面那样通过属性访问,也可以通过键(outputs["last_hidden_state"]
),甚至索引访问(outputs[0]
)。上面模型输出的大小如下所是:
- torch.Size([2, 16, 768])
对于情感分析任务,很明显我们最后需要使用的是一个「二分类 head」。因此,实际上我们不会使用 AutoModel
类,而是使用 AutoModelForSequenceClassification
:
- from transformers import AutoTokenizer
- from transformers import AutoModelForSequenceClassification
- checkpoint = "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)
最后输出的结果如下。可以看到,对于 batch 中的每一个样本,模型都会输出一个两维的向量(每一维对应一个标签,positive 或 negative)。
- torch.Size([2, 2])
对模型输出进行后处理
由于模型的输出只是一些数值,因此并不适合人类阅读。例如我们打印出上面例子的输出:
- tensor([[-1.5607, 1.6123],
- [ 4.1692, -3.3464]], grad_fn=<AddmmBackward0>)
模型对第一个句子输出 [-1.57, 1.61]
,对第二个句子输出 [4.16, -3.34]
,它们并不是概率值,而是模型最后一层输出的 logits 值(所有 Transformers 模型都会输出 logits 值,因为训练时的损失函数通常会自动结合激活函数,例如 SoftMax,与实际的损失函数,例如交叉熵 cross entropy)。要将他们转换为概率值,还需要让它们经过一个 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>)
- {0: 'NEGATIVE', 1: 'POSITIVE'}
于是我们可以得到最终的预测结果如下:
- 第一个句子: NEGATIVE: 0.0402, POSITIVE: 0.9598
- 第二个句子: NEGATIVE: 0.9995, POSITIVE: 0.0005
模型
在之前介绍 pipeline 模型时,我们使用 AutoModel
类根据 checkpoint 名称自动加载模型。当然,我们也可以直接使用对应的 Model
类。例如加载 BERT 模型(包括采用 BERT 结构的其他模型):
- from transformers import BertModel
- model = BertModel.from_pretrained("bert-base-cased")
加载模型
通过调用 Model.from_pretrained()
函数可以自动加载 checkpoint 对应的模型权重 (weights)。然后,我们可以直接使用模型完成它的预训练任务,或者在新的任务上对模型权重进行微调。
Model.from_pretrained()
会自动缓存下载的模型权重,默认保存到 ~/.cache/huggingface/hub
,我们也可以通过 HF_HOME
环境变量自定义缓存目录。
所有存储在 Model Hub 上的模型都能够通过 Model.from_pretrained()
加载,只需要传递对应 checkpoint 的名称。当然了,我们也可以先将模型下载下来,然后将本地路径传给 Model.from_pretrained()
,比如加载下载好的 Bert-base 模型:
- from transformers import BertModel
- model = BertModel.from_pretrained("./models/bert/")
部分模型的 Hub 页面中会包含很多文件,我们通常只需要下载:
- 模型对应的 config.json 和 pytorch_model.bin,
- 分词器对应的 tokenizer.json、tokenizer_config.json 和 vocab.txt。
保存模型
保存模型与加载模型类似,只需要调用 Model.save_pretrained()
函数。例如保存加载的 BERT 模型:
- from transformers import AutoModel
- model = AutoModel.from_pretrained("bert-base-cased")
- model.save_pretrained("./models/bert-base-cased/")
这会在保存路径下创建两个文件:
- config.json:模型配置文件,里面包含构建模型结构的必要参数;
- pytorch_model.bin:又称为 state dictionary,包含模型的所有权重。
这两个文件缺一不可,配置文件负责记录模型的结构,模型权重记录模型的参数。我们自己保存的模型同样可以通过 Model.from_pretrained()
加载,只需要传递保存目录的路径。下面是 config.json
文件的大致内容:

分词器
因为神经网络模型不能直接处理文本,我们需要先将文本转换为模型能够处理的数字,这个过程被称为编码 (Encoding):先使用分词器 (Tokenizers) 将文本按词、子词、符号切分为 tokens;然后将 tokens 映射到对应的 token 编号(token IDs)。
分词策略
根据切分粒度的不同,分词策略大概可以分为以下几种,关于这部分详细内容可以查看链接 分词策略:
- 按词切分(Word-based)
- 按字符切分(Character-based)
- 按子词切分(Subword-based)
目前按子词切分 (Subword tokenization) 是一种被广泛采用的方式。按子词 (Subword) 切分 高频词直接保留,低频词被切分为更有意义的子词。
例如 “annoyingly” 就是一个低频词,可以切分为 “annoying” 和 “ly”,这两个子词不仅出现频率更高,而且词义也得以保留。下图就是对文本 “Let’s do tokenization!“ 按子词切分的例子:

可以看到,“tokenization” 被切分为了 “token” 和 “ization”,不仅保留了语义,而且只用两个 token 就表示了一个长词。这中策略只用一个较小的词表就可以覆盖绝大部分的文本,基本不会产生 unknown tokens。尤其对于土耳其语等黏着语言,可以通过串联多个子词构成几乎任意长度的复杂长词。
加载和保存分词器
分词器的加载与保存与模型非常相似,也是使用 from_pretrained()
和 save_pretrained()
函数。例如,使用 BertTokenizer
类加载并保存 BERT 模型的分词器:
- from transformers import BertTokenizer
- tokenizer = BertTokenizer.from_pretrained("bert-base-cased")
- tokenizer.save_pretrained("./models/bert-base-cased/")
与 AutoModel
类似,在大部分情况下,我们都应该使用 AutoTokenizer
类来加载分词器,它会根据 checkpoint 来自动选择对应的分词器(使用的接口是与上面一致的):
- from transformers import AutoTokenizer
- tokenizer = AutoTokenizer.from_pretrained("bert-base-cased")
- tokenizer.save_pretrained("./models/bert-base-cased/")
调用 Tokenizer.save_pretrained()
函数会在保存路径下创建三个文件:
- special_tokens_map.json:配置文件,里面包含 unknown tokens 等特殊字符的映射关系;
- tokenizer_config.json:配置文件,里面包含构建分词器需要的参数;
- vocab.txt:词表,每一个 token 占一行,行号就是对应的 token ID(从 0 开始)。
下面是 special_tokens_map.json
文件对应的内容:
- {
- "unk_token": "[UNK]",
- "sep_token": "[SEP]",
- "pad_token": "[PAD]",
- "cls_token": "[CLS]",
- "mask_token": "[MASK]"
- }
编码和解码文本
完整的文本编码 (Encoding) 过程实际上包含两个步骤:
- 分词:使用分词器按某种策略将文本切分为 tokens(也就是将);
- 映射:将 tokens 转化为对应的 token IDs。
因为不同预训练模型采用的分词策略并不相同,因此我们需要通过向 Tokenizer.from_pretrained()
函数传递模型 checkpoint 的名称来加载对应的分词器和词表。
分词与映射分别操作
下面,我们尝试使用 BERT 分词器来对文本进行分词:
- from transformers import AutoTokenizer
- tokenizer = AutoTokenizer.from_pretrained("bert-base-cased")
- sequence = "Using a Transformer network is simple"
- tokens = tokenizer.tokenize(sequence)
- print(tokens)
输出的结果如下所是。可以看到,BERT 分词器采用的是子词 (subword) 切分策略,它会不断切分词语直到获得词表中的 token,例如 “transformer” 会被切分为 “trans” 和 “##former”。
- ['Using', 'a', 'Trans', '##former', 'network', 'is', 'simple']
然后,我们通过 convert_tokens_to_ids()
将切分出的 tokens 转换为对应的 token IDs:
- ids = tokenizer.convert_tokens_to_ids(tokens)
- print(ids)
输出的结果如下所是,也就是将上面的字符转换为了 id:
- [7993, 170, 13809, 23763, 2443, 1110, 3014]
使用 encode 完成编码
我们也可以通过 encode()
函数将这两个步骤合并,并且 encode()
会自动添加模型需要的特殊字符。例如对于 BERT 会自动在 token 序列的首尾分别添加 [CLS]
和 [SEP]
token:
- from transformers import AutoTokenizer
- tokenizer = AutoTokenizer.from_pretrained("bert-base-cased")
- sequence = "Using a Transformer network is simple"
- sequence_ids = tokenizer.encode(sequence)
- print(sequence_ids)
输出的结果如下所是,其中 101 和 102 分别是 [CLS]
和 [SEP]
对应的 token IDs:
- [101, 7993, 170, 13809, 23763, 2443, 1110, 3014, 102]
使用 decode 完成解码
文本解码 (Decoding) 与编码相反,负责将 token IDs 转化为原来的字符串。注意,解码过程不是简单地将 token IDs 映射回 tokens,还需要合并那些被分词器分为多个 token 的单词。下面我们尝试通过 decode()
函数解码前面生成的 token IDs:
- from transformers import AutoTokenizer
- tokenizer = AutoTokenizer.from_pretrained("bert-base-cased")
- decoded_string = tokenizer.decode([101, 7993, 170, 13809, 23763, 2443, 1110, 3014, 102])
- print(decoded_string)
最终的结果如下所是,可以看到成功将 ID
还原为 tokens
。
- [CLS] Using a Transformer network is simple [SEP]
直接使用分词器处理
实际编码文本时,更为常见的是直接使用分词器进行处理。这样返回的结果中不仅包含处理后的 token IDs,还包含模型需要的其他辅助输入。例如对于 BERT 模型还会自动在输入中添加 token_type_ids
和 attention_mask
:
- from transformers import AutoTokenizer
- tokenizer = 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]
- }
处理多段文本
在实际应用中,我们往往需要同时处理大量长度各异的文本。而且所有的神经网络模型都只接受批 (batch) 数据作为输入,即使只输入一段文本,也需要先将它组成只包含一个样本的 batch,然后才能送入模型。
使用分词器处理多个句子
同样的,我们可以直接使用「分词器」对文本进行处理。下面看一个例子,共有两句话:
- from transformers import AutoTokenizer
- # 加载分词器
- checkpoint = "distilbert-base-uncased-finetuned-sst-2-english"
- tokenizer = AutoTokenizer.from_pretrained(checkpoint)
- # 句子
- sequence = [
- "I've been waiting for a HuggingFace course my whole life.",
- "Hello, World."
- ]
- # 分词
- tokenized_inputs = tokenizer(sequence, padding=True, return_tensors="pt")
- print("Input IDs:\n", tokenized_inputs)
输出的结果为,可以看到通过 padding 使得输出长度是一样的。
- {
- 'input_ids': tensor([
- [
- 101, 1045, 1005, 2310, 2042, 3403, 2005, 1037, 17662, 12172,
- 2607, 2026, 2878, 2166, 1012, 102],
- [
- 101, 7592, 1010, 2088, 1012, 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]
- ])
- }
这里通过 return_tensors
参数指定返回的张量格式:设为 pt
则返回 PyTorch 张量;tf
则返回 TensorFlow 张量,np
则返回 NumPy 数组。
通过 Padding
操作,在短序列的最后填充特殊的 padding token,使得 batch 中所有的序列都具有相同的长度。padding
有以下的参数来控制:
padding="longest"
: 将 batch 内的序列填充到当前 batch 中最长序列的长度;padding="max_length"
:将所有序列填充到模型能够接受的最大长度,例如 BERT 模型就是 512。
截断操作通过 truncation
参数来控制,如果 truncation=True
,那么大于模型最大接受长度的序列都会被截断,例如对于 BERT 模型就会截断长度超过 512 的序列。此外,也可以通过 max_length
参数来控制截断长度:
- model_inputs = tokenizer(sequences, max_length=8, truncation=True)
Attention Masks
使用 padding
会存在一个问题,Transformer
模型会编码输入序列中的每一个 token
以建模完整的上下文。因此会将填充的 padding token
也当成是普通 token
一起编码,从而生成了不同的上下文语义表示。也就是导致加入 padding
之后的结果会和加入前是不一样的。
因此,在进行 Padding
操作的同时,我们必须明确地告诉模型哪些 token
是我们填充的,它们不应该参与编码,这就需要使用到 attention mask
。
Attention masks
是一个与 input IDs 尺寸完全相同的仅由 0 和 1 组成的张量,其中 0 表示对应位置的 token 是填充符,不应该参与 attention
层的计算,而应该只基于 1 对应位置的 token 来建模上下文。
编码句子对
在上面的例子中,我们都是对单个序列进行编码(即使通过 batch 处理多段文本,也是并行地编码单个序列),而实际上对于 BERT 等包含「句子对」分类预训练任务的模型来说,都支持对「句子对」进行编码,例如:
- from transformers import AutoTokenizer
- checkpoint = "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] sentence1 [SEP] sentence2 [SEP]
」的 token
序列,这也是 BERT
模型预期的输入格式。
返回结果中除了前面我们介绍过的 input_ids
和 attention_mask
之外,还包含了一个 token_type_ids
项,用于标记输入序列中哪些 token
属于第一个句子,哪些属于第二个句子。也就是第一个句子「[CLS] sentence1 [SEP]
」的 token_type_ids
都是 0
,而第二个句子 「sentence2 [SEP]
」对应的 token_type_ids
都是 0
。
下面是一个包含句子对和多个句子的分词器使用的例子:
- from transformers import AutoTokenizer
- checkpoint = "bert-base-uncased"
- tokenizer = AutoTokenizer.from_pretrained(checkpoint)
- sentences_list = [
- ["This is the first sentence 1.", "second sentence 1."],
- ["This is the first sentence 2.", "second sentence 2."]
- ]
- tokens = tokenizer(
- sentences_list,
- padding=True,
- truncation=True,
- return_tensors="pt"
- )
- print(tokens)
- 微信公众号
- 关注微信公众号
-
- QQ群
- 我们的QQ群号
-
评论