- 首先安装
requirements.txt中的所有依赖包
pip install -r requirements.txt- 若使用科学上网,需要降低
requset和urllibs3两个依赖包的版本,否则在后续下载模型时会因网络代理而报错。
pip install requests==2.27.1
pip install urllib3==1.25.11 Transformers库最基础的对象就是pipeline()函数,它封装了预训练模型和对应的前处理和后处理环节。相当于是一个已经训练好能直接使用的工具。只需输入文本,就能得到预期的答案。- 常用的
pipelines有以下几点:feature-extraction(获得文本的向量化表示)fill-mask(填充被遮盖的词、片段)ner(命名实体识别)question-answering(自动问答)sentiment-analysis(情感分析)summarization(自动摘要)text-generation(文本生成)translation(机器翻译)zero-shot-classification(零训练样本分类)
- 在初次使用不同pipelines完成任务时,需要自动下载模型至C盘的 .cache 处,因此需要耗费些许时间。
- 通过
pipeline函数指定任务类型为:sentiment-analysis
# 若不使用科学上网,则可选择镜像网站下载模型
# import os
# os.environ["HF_ENDPOINT"] = "https://hf-mirror.com"
from transformers import pipeline
classifier = pipeline("sentiment-analysis")
result = classifier("Today is a good day,but I don't like it.")
print(result)
results_zh = classifier(
["今天很好,但我不喜欢", "今天不好"]
)
results_en = classifier(
["Today is good, but I don't like it", "Today is not very good"]
)
print(results_zh)
print(results_en)result是一个列表,包含label和score两个关键词。可见,模型对中英文都有效。
No model was supplied, defaulted to distilbert/distilbert-base-uncased-finetuned-sst-2-english and revision 714eb0f (https://huggingface.co/distilbert/distilbert-base-uncased-finetuned-sst-2-english).
Using a pipeline without specifying a model name and revision in production is not recommended.
[{'label': 'NEGATIVE', 'score': 0.974997878074646}]
[{'label': 'NEGATIVE', 'score': 0.9165080785751343}, {'label': 'NEGATIVE', 'score': 0.6466353535652161}]
[{'label': 'NEGATIVE', 'score': 0.8463599681854248}, {'label': 'NEGATIVE', 'score': 0.9997707009315491}]pipeline 会自动选择模型来完成对应任务,如在情感分析中,默认选择微调好的英文情感模型 distilbert-base-uncased-finetuned-sst-2-english。
- 指定任务类型为
zero-shot-classification,在classifier中输入待分类文本和候选标签。
from transformers import pipeline
classifier = pipeline("zero-shot-classification")
result = classifier(
"This is a course about the Transformers library",
candidate_labels=["education", "politics", "business"],
)
print(result)- 使用的模型是 facebook/bart-large-mnli
No model was supplied, defaulted to facebook/bart-large-mnli and revision d7645e1 (https://huggingface.co/facebook/bart-large-mnli).
{'sequence': 'This is a course about the Transformers library', 'labels': ['education', 'business', 'politics'], 'scores': [0.8445993065834045, 0.11197393387556076, 0.043426718562841415]}- 指定任务类型为
text-generation,也可以指定生成的序列数和生成的最大长度。
from transformers import pipeline
generator = pipeline("text-generation")
results = generator("今日吾虽死,")
print(results)
results = generator(
"In this course, we will teach you how to",
num_return_sequences=2,
max_length=20
)
print(results)- 使用的模型是 gpt2,显然该模型缺少生成中文文本的能力
No model was supplied, defaulted to openai-community/gpt2 and revision 607a30d (https://huggingface.co/openai-community/gpt2).
[{'generated_text': '今日吾虽死,我吾處- 2?\n\n"But I didn\'t mean to mention you or anything, I wanted to tell everybody how my'}]
[{'generated_text': 'In this course, we will teach you how to understand the role of the law in the U.'}, {'generated_text': 'In this course, we will teach you how to use the power of intuition to understand that time and'}]
- 也可以在 Model Hub 页面左边选择 Text Generation tag 查询支持的模型。例如,在相同的 pipeline 中加载 distilgpt2 模型:
from transformers import pipeline
generator = pipeline("text-generation", model="distilgpt2")
results = generator(
"In this course, we will teach you how to",
max_length=20,
num_return_sequences=1,
)
print(results)[{'generated_text': 'In this course, we will teach you how to practice and gain experience in a few basic and highly'}]- 文本生成任务中包含许多模型,例如,专门用于生成中文古诗的 gpt2-chinese-poem 模型,可以进入模型详情页学习模型如何使用。
from transformers import pipeline
generator = pipeline("text-generation", model="uer/gpt2-chinese-poem")
results = generator(
"[CLS]梅 山 如 积 翠 ,",
max_length=20,
num_return_sequences=1,
)
print(results)[{'generated_text': '[CLS]梅 山 如 积 翠 , 湖 波 冷 日 初 黄 。 江 东 地 极 天 尽 处 , 望 见 江 城 城 一 方 。 念 金 陵 已 无 家 ,'}]- 在给定一段部分词语被遮盖掉 (mask) 的文本,使用预训练模型来预测能够填充这些位置的词语。用 < mask > 来表示需要遮盖的文本,用
top_k控制生成的序列数量。
from transformers import pipeline
unmasker = pipeline("fill-mask")
results = unmasker("This course will teach you all about <mask> models.", top_k=2)
print(results)- 默认使用的模型是 distilroberta-base,这个模型只能应对句子中只有一个 mask 的情况。
No model was supplied, defaulted to distilbert/distilroberta-base and revision fb53ab8 (https://huggingface.co/distilbert/distilroberta-base).
[{'score': 0.1961977779865265, 'token': 30412, 'token_str': ' mathematical', 'sequence': 'This course will teach you all about mathematical models.'},
{'score': 0.04052717983722687, 'token': 38163, 'token_str': ' computational', 'sequence': 'This course will teach you all about computational models.'}]- 命名实体识别负责从文本中抽取出指定类型的实体,例如人物、地点、组织等等
from transformers import pipeline
ner = pipeline("ner", grouped_entities=True)
results = ner("My name is Sylvain and I work at Hugging Face in Brooklyn.")
print(results)- 模型正确地识别出了 Sylvain 是一个人物(PER),Hugging Face 是一个组织(ORG),Brooklyn 是一个地名(LOC)。
No model was supplied, defaulted to dbmdz/bert-large-cased-finetuned-conll03-english and revision 4c53496 (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.9796019, 'word': 'Hugging Face', 'start': 33, 'end': 45}, {'entity_group': 'LOC', 'score': 0.9932106, 'word': 'Brooklyn', 'start': 49, 'end': 57}]- 通过给定的上下文回答问题,类似于阅读理解。
from transformers import pipeline
question_answerer = pipeline("question-answering", model='distilbert-base-cased-distilled-squad')
context = r"My name is Sylvain and I work at Hugging Face in Brooklyn"
result = question_answerer(
question="Where do I work?",
context=context)
print(result)- 这里的自动问答实际上是一个抽取式问答模型,即从给定的上下文中抽取答案,而不是生成答案。
{'score': 0.6949771046638489, 'start': 33, 'end': 45, 'answer': 'Hugging Face'}根据形式的不同,自动问答 (QA) 系统可以分为三种:
- **抽取式 QA (extractive QA):**假设答案就包含在文档中,因此直接从文档中抽取答案;
- **多选 QA (multiple-choice QA):**从多个给定的选项中选择答案,相当于做阅读理解题;
- **无约束 QA (free-form QA):**直接生成答案文本,并且对答案文本格式没有任何限制。
- 将长文本压缩成短文本,并且还要尽可能保留原文的主要信息。
from transformers import pipeline
summarizer = 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)[{'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 .'}]通过pipelines可以轻松调用模型来应对各种任务,在尝试的过程中可以多参考 Model Hub ,学习各种模型的基本使用。
- 这些简单易用的 pipeline 模型实际上封装了许多操作,以第一个情感分析 pipeline 为例,从输入的文本到模型输出的 label 和 score ,要经过三个步骤:
- 预处理 (preprocessing),将原始文本转换为模型可以接受的输入格式;
- 将处理好的输入送入模型;
- 对模型的输出进行后处理 (postprocessing),将其转换为人类方便阅读的格式。
-
我们会使用每个模型对应的分词器 (tokenizer)来进行原始文本的预处理,具体来说就是
- 将输入切分为词语、子词或者符号(例如标点符号),统称为 tokens;
- 根据模型的词表将每个 token 映射到对应的 token 编号(即数字);
根据模型的需要,可能会添加一些额外的输入(如生成中文古诗中的 [CLS])。
-
每个模型都有特定的预处理操作,因此我们要使用与模型一致的预处理操作。这里使用
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.",
"Amazing",
]
inputs = tokenizer(raw_inputs, padding=True, truncation=True, return_tensors="pt")
print(inputs)- 简单提一点,在 tokenizer 中的参数中,padding是填充,truncation为截断,return_tensors是返回的张量类型(pt即Pytorch)
{
'input_ids': tensor([
[ 101, 1045, 1005, 2310, 2042, 3403, 2005, 1037, 17662, 12172,
2607, 2026, 2878, 2166, 1012, 102],
[ 101, 6429, 102, 0, 0, 0, 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, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
])
}- 输出中包含两个键
input_ids和attention_mask,其中input_ids对应分词之后的 tokens 映射到的数字编号列表,而attention_mask则是用来标记哪些 tokens 是被填充的(这里“1”表示是原文,“0”表示是填充字符)。
可见,单词与token之间并非是一一映射的。
- 预训练模型的加载方式和分词器 (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)。这些 hidden states 通常会被输入到其他的模型部分(称为 head),以完成特定的任务,例如送入到分类头中完成文本分类任务。 ![[Pasted image 20241221210657.png]]
-
Transformer 模块的输出是一个维度为 (Batch size, Sequence length, Hidden size) 的三维张量,以预处理好的情感分析输入为例
- Batch size 表示每次输入的样本(文本序列)数量,即每次输入多少个句子,上例中为 2
- Sequence length 表示文本序列的长度,即每个句子被分为多少个 token,上例中为 16
- Hidden size 表示每一个 token 经过模型编码后的输出向量(语义表示)的维度。
-
可以打印出这里使用的 distilbert-base 模型的输出维度:
from transformers import AutoTokenizer, AutoModel
checkpoint = "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.",
"Amazing",
]
inputs = tokenizer(raw_inputs, padding=True, truncation=True, return_tensors="pt")
outputs = model(**inputs)
print(outputs.last_hidden_state.shape)- Transformers 模型的输出格式类似 字典,可以像上面那样通过属性访问,也可以通过键(
outputs["last_hidden_state"]),甚至索引访问(outputs[0])。
torch.Size([2, 16, 768])当使用
**inputs作为model函数的参数时,实际上是在告诉Python:“将inputs字典中的所有键值对作为参数传递给model函数。”这样做的好处是,不需要显式地为model函数的每个参数手动赋值,特别是当inputs字典中有很多参数时,这样可以减少代码量并提高代码的可读性。
- 例如,如果
inputs字典是这样的:
{'input_ids': [1, 2, 3], 'attention_mask': [0, 1, 1], 'token_type_ids': [0, 0, 0]}- 使用
**inputs后,model函数的调用就会变成:
model(input_ids=[1, 2, 3], attention_mask=[0, 1, 1], token_type_ids=[0, 0, 0])- 对于情感分析任务,很明显我们最后需要使用的是一个文本分类 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.",
"Amazing",
]
inputs = tokenizer(raw_inputs, padding=True, truncation=True, return_tensors="pt")
outputs = model(**inputs)
print(outputs.logits)
print(outputs.logits.shape)- 对于 batch 中的每一个样本,模型都会输出一个两维的向量(每一维对应一个标签,positive 或 negative),但此时的数值并不适合人类阅读
tensor([[-1.5607, 1.6123],
[-4.3321, 4.6592]], grad_fn=<AddmmBackward0>)
torch.Size([2, 2])- 模型对第一个句子输出 [−1.5607,1.6123],对第二个句子输出 [-4.3321, 4.6592],它们并不是概率值,而是模型最后一层输出的 logits 值。要将他们转换为概率值,还需要让其经过一个 SoftMax 层
import torch
predictions = torch.nn.functional.softmax(outputs.logits, dim=-1)
print(predictions)tensor([[4.0195e-02, 9.5980e-01],
[1.2447e-04, 9.9988e-01]], grad_fn=<SoftmaxBackward0>)所有 Transformers 模型都会输出 logits 值,因为训练时的损失函数通常会自动结合激活函数(例如 SoftMax)与实际的损失函数(例如交叉熵 cross entropy)。
- 这样就得到了更容易理解的概率值:第一个句子 [0.0402,0.9598],第二个句子 [0.0124,0.9999]。最后,为了得到对应的标签,可以读取模型 config 中提供的 id2label 属性:
print(model.config.id2label){0: 'NEGATIVE', 1: 'POSITIVE'}- 有了这些条件,再对输出稍加处理
predict_tolist = predictions.detach().numpy()
input_list=[]
for sample in predict_tolist:
dict ={"label":None,"score":None}
if sample[0]>sample[1]:
dict["label"]=model.config.id2label[0]
dict["score"]=round(sample[0], 4)
else:
dict["label"]=model.config.id2label[1]
dict["score"]=round(sample[1], 4)
input_list.append(dict)
print(input_list)- 便得到了模型默认的输出格式
[{'label': 'POSITIVE', 'score': 0.9598}, {'label': 'POSITIVE', 'score': 0.9999}]- 结合以上三个步骤,总结得到pipelines的流程图 ![[Pasted image 20241222154456.png]]
- 本章内容介绍了如何利用Hugging Face Model Hub页面,使用Transformers提供的pipelines工具处理各种NLP任务,并探讨了pipelines在后台执行的三个核心步骤:使用Tokenizer对文本进行分词处理;其次,将分词后的数据传递给Model进行处理;最后,将模型的输出转换为人类可读的格式。
- 除了像之前使用
AutoModel根据 checkpoint 自动加载模型以外,也可以直接使用模型对应的Model类,如 BERT 对应的就是BertModel,但是为了代码的可扩展性,大部分情况还是使用AutoModel。
from transformers import BertModel
model = BertModel.from_pretrained("bert-base-cased")- 所有存储在 HuggingFace Model Hub 上的模型都可以通过
Model.from_pretrained()来加载权重,也可使用本地路径(预先下载的模型目录)
from transformers import BertModel
model = BertModel.from_pretrained('../models/bert-base-cased')-
该方法会自动缓存下载的模型权重,默认保存在 ~/.cache/huggingface/transformers,由于 checkpoint 名称加载方式需要连接网络,为了方便可以采用本地路径加载模型。
-
以
bert-base-cased模型的 Hub 页面为例,通常只需要下载模型对应的 config.json 和 pytorch_model.bin,以及分词器对应的 tokenizer.json、tokenizer_config.json 和 vocab.txt。 ![[Pasted image 20241222204613.png|475]]
- 保存模型通过调用
Model.save_pretrained()函数实现,例如保存加载的 BERT 模型:
from transformers import AutoModel
model = AutoModel.from_pretrained('bert-base-cased')
model.save_pretrained('../models/bert-base-cased')- 这会在保存路径下创建两个文件:
- config.json:模型配置文件,存储模型结构参数,例如 Transformer 层数、特征空间维度等;
- model.safetensors:又称为 state dictionary,存储模型的权重。
简单来说,配置文件记录模型的结构,模型权重记录模型的参数,这两个文件缺一不可。
- 神经网络模型不能直接处理文本,因此需要先将文本转换为数字,这个过程被称为编码 (Encoding),包含两个步骤:
- 使用分词器 (tokenizer) 将文本按词、子词、字符切分为 tokens;
- 将所有的 token 映射到对应的 token ID。
-
根据切分粒度的不同,分词策略可以分为以下几种:
-
按词切分 (Word-based) ![[Pasted image 20241222205322.png]]
- 文本中所有出现过的独立片段都作为不同的 token,会产生巨大的词表。
- 无法体现出词与词的关联性,如,“dog” 和 “dogs”、“run” 和 “running”
- 当遇到不在词表中的词时,分词器会使用一个专门的 [UNK] token 来表示它是 unknown 的。一个好的分词策略,应该尽可能不出现 unknown token。
词表就是一个映射字典,负责将 token 映射到对应的 ID(从 0 开始)。神经网络模型就是通过这些 token ID 来区分每一个 token。
- 按字符切分 (Character-based)
![[Pasted image 20241222205801.png]]
- 将单词化成字符,只会产生一个非常小的词表,并且很少会出现词表外的 tokens。
- 但字符本身没有太大意义。
现在广泛采用的是一种同时结合了按词切分和按字符切分的方式——按子词切分 (Subword tokenization)。
- 按子词切分 (Subword)
![[Pasted image 20241222210313.png]]
- 高频词直接保留,低频词被切分为更有意义的子词。
- 分词器的加载与保存与模型相似,例如加载并保存 BERT 模型的分词器:
from transformers import BertTokenizer
tokenizer = BertTokenizer.from_pretrained('bert-base-cased') tokenizer.save_pretrained('../models/bert-base-cased')- 在大部分情况下使用
AutoTokenizer来加载分词器:
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 token 等特殊字符的映射关系;
- tokenizer_config.json_和_tokenizer.json:分词器配置文件,存储构建分词器需要的参数;
- vocab.txt:词表,一行一个 token,行号就是对应的 token ID(从 0 开始)。
-
文本编码 (Encoding) 过程包含两个步骤:
- 分词:使用分词器按某种策略将文本切分为 tokens;
- 映射:将 tokens 转化为对应的 token IDs。
-
以 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)['Using', 'a', 'Trans', '##former', 'network', 'is', 'simple']可见,BERT 分词器采用的是子词切分策略,它会不断切分词语直到获得词表中的 token。
- 再将切分出的 tokens 进行编码:
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 AutoTokenizer
tokenizer = 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 转化为对应的子词
- 根据策略将子词合并
- 使用
decode()解码前面生成的 token IDs
from transformers import AutoTokenizer
tokenizer = 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]- 在实际编码文本时,最常见的是直接使用分词器进行处理
from transformers import AutoTokenizer
tokenizer = AutoTokenizer.from_pretrained("bert-base-cased")
tokenized_text = tokenizer("Using a Transformer network is simple")
print(tokenized_text)- 这样不仅会返回分词后的 token IDs,还包含模型需要的其他输入。
{
'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,例如:
import torch
from transformers import AutoModelForSequenceClassification,AutoTokenizer
checkpoint = "distilbert-base-uncased-finetuned-sst-2-english"
tokenizer = AutoTokenizer.from_pretrained(checkpoint)
model = AutoModelForSequenceClassification.from_pretrained(checkpoint)
sequence = "Using a Transformer network is simple"
tokens =tokenizer.tokenize(sequence)
ids =tokenizer.convert_tokens_to_ids(tokens)
input_ids=torch.tensor([ids])
print(input_ids)
outputs = model(input_ids)
print(outputs)tensor([[ 2478, 1037, 10938, 2121, 2897, 2003, 3722]])
SequenceClassifierOutput(loss=None, logits=tensor([[ 2.5189, -2.1906]], grad_fn=<AddmmBackward0>), hidden_states=None, attentions=None)- 这里通过
[ids]构建了一个只包含一段文本的 batch,更常见的是送入包含多段文本的 batch:
batched_ids = [ids, ids, ids, ...]上面的代码仅作为演示。实际场景中,我们应该直接使用分词器对文本进行处理
- 对于上面的例子:
from transformers import AutoModelForSequenceClassification,AutoTokenizer
checkpoint = "distilbert-base-uncased-finetuned-sst-2-english"
tokenizer = AutoTokenizer.from_pretrained(checkpoint)
model = AutoModelForSequenceClassification.from_pretrained(checkpoint)
sequence = "Using a Transformer network is simple"
tokens = tokenizer(sequence, return_tensors="pt")
print(tokens)
outputs = model(**tokens)
print(outputs.logits){
'input_ids': tensor([[ 101, 2478, 1037, 10938, 2121, 2897, 2003, 3722, 102]]),
'attention_mask': tensor([[1, 1, 1, 1, 1, 1, 1, 1, 1]])
}
tensor([[-2.5780, 2.5856]], grad_fn=<AddmmBackward0>)- 前面展示了如何输入单条文本给模型,现在演示多条文本的情况:
from transformers import AutoModelForSequenceClassification,AutoTokenizer
checkpoint = "distilbert-base-uncased-finetuned-sst-2-english"
tokenizer = AutoTokenizer.from_pretrained(checkpoint)
model = AutoModelForSequenceClassification.from_pretrained(checkpoint)
sequence = [
"Using a Transformer network is simple",
"Today is a good day!",
"Amazing!"]
inputs = tokenizer(sequence, padding=True,return_tensors="pt")
print(inputs)
outputs=model(**inputs)- 在分词中,添加了新参数 padding 。因为模型要求输入的张量必须是严格的二维矩形,所以需要将分词后长度不一的token序列填充为统一长度,即每一段文本编码后的 token IDs 数量必须一样多。
{
'input_ids': tensor([
[ 101, 2478, 1037, 10938, 2121, 2897, 2003, 3722, 102],
[ 101, 2651, 2003, 1037, 2204, 2154, 999, 102, 0],
[ 101, 6429, 999, 102, 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, 0],
[1, 1, 1, 1, 0, 0, 0, 0, 0]])
}这种填充是在[SEP] (token IDs =102)字符之后的,即[CLS]和[SEP]标记的是句子的真实开头与结束。
- 如果进行手动填充呢?使用
tokenizer.pad.token_id获取当前分词器填充的
import torch
from transformers import AutoTokenizer, AutoModelForSequenceClassification
checkpoint = "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)- 可见当前分词器采用的padding ID是
0,且使用 padding token 填充的序列的结果竟然与其单独送入模型时不同!
0
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>)因为模型默认会编码输入序列中的所有 token 以建模完整的上下文,即对于手动padding的数据,模型所认为的真正上下文是包含padding在内的。
- 所以,在进行 Padding 操作时,必须明确告知模型哪些 token 是我们填充的,它们不应该参与编码。这就需要使用到 Attention Mask 。
- Attention Mask 是一个尺寸与 input IDs 完全相同,且仅由 0 和 1 组成的张量,0 表示对应位置的 token 是填充符,不参与计算。
- 借助Attention Mask 就可标出填充的 padding token 的位置,那该如何将Attention Mask传递给model呢?可以回忆一下 ,在[[How to use Transforms#Padding]]的第一个例子中,传入model的input中包含什么。
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)- 当然可以通过
attention_mask直接将参数传递给model,此时得到的结果就与模型默认编码的结果一致了。
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>)- 目前大部分 Transformer 模型只能接受长度不超过 512 或 1024 的 token 序列,因此对于长序列,有以下三种处理方法:
- 使用一个支持长文的 Transformer 模型,例如 Longformer 和 LED(最大长度 4096);
- 设定最大长度
max_sequence_length以截断输入序列:sequence = sequence[:max_sequence_length]。 - 将长文切片为短文本块 (chunk),然后分别对每一个 chunk 编码。
- 在实际使用中,应该直接使用分词器来完成分词、编码、Padding、构建Attention Mask、截断等操作,下面以 DistilBERT 模型给出一个完整的例子:
from transformers import AutoTokenizer
checkpoint = "distilbert-base-uncased-finetuned-sst-2-english"
tokenizer = AutoTokenizer.from_pretrained(checkpoint)
sequences =["How are you?","Nice to meet you!"]
model_inputs = tokenizer(sequences,padding=True,truncation=True,return_tensors="pt")
print(model_inputs)- 分词器会给出模型的需要输入,对于 DistilBERT 模型包含 input_ids 和 attention_mask。
{'input_ids': tensor([[ 101, 2129, 2024, 2017, 1029, 102, 0],
[ 101, 3835, 2000, 3113, 2017, 999, 102]]), 'attention_mask': tensor([[1, 1, 1, 1, 1, 1, 0],
[1, 1, 1, 1, 1, 1, 1]])}
- 下面将具体分析 padding、truncation、return_tensors 三个参数的作用
Padding 操作通过 padding 参数来控制:
padding="longest"/padding=True: 将序列填充到当前 batch 中最长序列的长度;padding="max_length":将所有序列填充到模型能够接受的最大长度,例如 BERT 模型就是 512。
from transformers import AutoTokenizer
checkpoint = "distilbert-base-uncased-finetuned-sst-2-english"
tokenizer = AutoTokenizer.from_pretrained(checkpoint)
sequences =["How are you?","Nice to meet you!"]
model_inputs_1 = tokenizer(sequences,padding="longest")
model_inputs_2 = tokenizer(sequences,padding="max_length")
print(model_inputs_1)
print(model_inputs_2){'input_ids': [[101, 2129, 2024, 2017, 1029, 102, 0], [101, 3835, 2000, 3113, 2017, 999, 102]],
'attention_mask': [[1, 1, 1, 1, 1, 1, 0], [1, 1, 1, 1, 1, 1, 1]]}
{'input_ids': [[101, 2129, 2024, 2017, 1029, 102, 0, 0, 0,...], [101, 3835, 2000, 3113, 2017, 999, 102, 0, 0, 0,...]],
'attention_mask': [[1, 1, 1, 1, 1, 1, 0, 0, 0,...], [1, 1, 1, 1, 1, 1, 1, 0, 0, 0,...]]}截断操作通过 truncation 参数来控制:
truncation=True:大于模型最大接受长度的序列都会被截断,例如对于 BERT 模型就会截断长度超过 512 的序列。max_length:手动选择控制截断的长度:
from transformers import AutoTokenizer
checkpoint = "distilbert-base-uncased-finetuned-sst-2-english"
tokenizer = AutoTokenizer.from_pretrained(checkpoint)
sequences =["How are you?","Nice to meet you!"]
model_inputs = tokenizer(sequences, max_length=4, truncation=True)
print(model_inputs){'input_ids': [[101, 2129, 2024, 102], [101, 3835, 2000, 102]], 'attention_mask': [[1, 1, 1, 1], [1, 1, 1, 1]]}返回张量类型 通过return_tensors 参数指定返回的张量格式:
pt:返回 PyTorch 张量;tf:返回 TensorFlow 张量,np:返回 NumPy 数组。
from transformers import AutoTokenizer
checkpoint = "distilbert-base-uncased-finetuned-sst-2-english"
tokenizer = AutoTokenizer.from_pretrained(checkpoint)
sequences =["How are you?","Nice to meet you!"]
print(tokenizer(sequences,padding=True, return_tensors="pt"))
print(tokenizer(sequences,padding=True, return_tensors="np"))在设定返回的张量格式之前,要先进行 padding 或截断操作,将batch中的数据处理成统一的长度,才能够将分词的结果送入模型
{'input_ids': tensor([[ 101, 2129, 2024, 2017, 1029, 102, 0],
[ 101, 3835, 2000, 3113, 2017, 999, 102]]), 'attention_mask': tensor([[1, 1, 1, 1, 1, 1, 0],
[1, 1, 1, 1, 1, 1, 1]])}
{'input_ids': array([[ 101, 2129, 2024, 2017, 1029, 102, 0],
[ 101, 3835, 2000, 3113, 2017, 999, 102]]), 'attention_mask': array([[1, 1, 1, 1, 1, 1, 0],
[1, 1, 1, 1, 1, 1, 1]])}- 除了对单段文本进行编码以外(batch 只是并行地编码多个单段文本),对于 BERT 等包含句子对(text pair)预训练任务的模型(类似于问答系统),它们的分词器也支持对句子对进行编码。
- 下面例子使用 bert-base-uncased 模型,对比了使用batch对多段文本进行编码和直接编码句子对的区别:
from transformers import AutoTokenizer
checkpoint = "bert-base-uncased"
tokenizer = AutoTokenizer.from_pretrained(checkpoint)
sequences = ["Are you Ok?", "I'm OK!"]
inputs_direct = tokenizer("Are you Ok?", "I'm OK!")
inputs_indirect = tokenizer(sequences)
print(inputs_direct)
print(inputs_indirect)
print(tokenizer.convert_ids_to_tokens(inputs_direct["input_ids"]))在上例中 Are you Ok? 和 I'm OK! 构成一个句子对
- 通过输出结果可知,句子对的编码思路与单段文本的编码思路并不相同,对于句子对分词器会使用 [SEP] token 拼接两个句子,输出形式为: $$ [CLS] sentence1 [SEP] sentence2 [SEP] $$
{'input_ids': [101, 2024, 2017, 7929, 1029, 102, 1045, 1005, 1049, 7929, 999, 102], 'token_type_ids': [0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1], 'attention_mask': [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]}
{'input_ids': [[101, 2024, 2017, 7929, 1029, 102], [101, 1045, 1005, 1049, 7929, 999, 102]], 'token_type_ids': [[0, 0, 0, 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]]}
['[CLS]', 'are', 'you', 'ok', '?', '[SEP]', 'i', "'", 'm', 'ok', '!', '[SEP]']- 对比 DistilBERT 模型,bert-base-uncased 模型的分词器编码结果中,除了input_ids 和 attention_mask,还多了一个关键词
token_type_ids,用于标记哪些 token 属于第一个句子,哪些属于第二个句子,如果将上面例子中的token_type_ids项与 token 序列对齐
['[CLS]', 'are', 'you', 'ok', '?', '[SEP]', 'i', "'", 'm', 'ok', '!', '[SEP]']
[ 0 , 0 , 0 , 0 , 0 , 0 , 1 , 1 , 1 , 1 , 1 , 1 ]- 可见第一个句子 [CLS] sentence1 [SEP] 所有 token 的 type ID 都为 0,而第二个句子sentence2 [SEP]对应的 token type ID 都为 1
如果选择其他模型,分词器的输出不一定会包含
token_type_ids项(如 DistilBERT 模型)。分词器只需保证输出格式满足模型所需要的输入格式即可。
- 在实际应用中,可能需要处理大量的句子对,推荐的做法是将它们分别存储在两个数组中。分词器会自动识别这两个数组之间的一一对应关系,并据此构建出相应的句子对。
from transformers import AutoTokenizer
checkpoint = "bert-base-uncased"
tokenizer = AutoTokenizer.from_pretrained(checkpoint)
sentence1_list = ["First sentence.", "second sentence.", "Third one.","fourth one."]
sentence2_list = ["How are you?", "I am fine,", "thank you,","And you?"]
tokens = tokenizer(
sentence1_list,
sentence2_list,
padding=True,
truncation=True,
return_tensors="pt"
)
print(tokens)
print(tokens['input_ids'].shape)- 上例中总共有4组句子对,经过编码得到的
input_ids同样也包含四个数组。
{'input_ids': tensor([[ 101, 2034, 6251, 1012, 102, 2129, 2024, 2017, 1029, 102],
[ 101, 2117, 6251, 1012, 102, 1045, 2572, 2986, 1010, 102],
[ 101, 2353, 2028, 1012, 102, 4067, 2017, 1010, 102, 0],
[ 101, 2959, 2028, 1012, 102, 1998, 2017, 1029, 102, 0]]), 'token_type_ids': tensor([[0, 0, 0, 0, 0, 1, 1, 1, 1, 1],
[0, 0, 0, 0, 0, 1, 1, 1, 1, 1],
[0, 0, 0, 0, 0, 1, 1, 1, 1, 0],
[0, 0, 0, 0, 0, 1, 1, 1, 1, 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, 1, 1, 1, 1, 1, 0],
[1, 1, 1, 1, 1, 1, 1, 1, 1, 0]])}
torch.Size([4, 10])有趣的是,在
token_type_ids的处理中,对于sentence2_list,分词器将句子对的padding部分视为不属于sentence2的一部分,可以说与attention_mask有异曲同工之妙。