huggingface NLP工具包教程2:使用Transformers
引言
Transformer 模型通常非常大,由于有數百萬到數百億個參數,訓練和部署這些模型是一項復雜的任務。此外,由于幾乎每天都有新模型發布,而且每個模型都有自己的實現,所以使用所有這些模型比較麻煩。
transformers 庫就是為了解決這個問題而創建的。目標是提供一個 API,通過它可以加載、訓練和保存任何 Transformer 模型。主要特點是:
- 易用性:下載、加載和使用最新的 NLP 模型進行推理只需兩行代碼即可完成。
- 靈活性:所有模型的核心都是簡單的 PyTorch 中的
nn.Module
或 TensorFlow 中的tf.keras.Model
類或其他框架中的模型一樣處理。 - 簡單性:庫中幾乎沒有任何抽象。核心概念是 “All-in-one file”:模型的前向傳遞完全在一個文件中定義,這樣代碼本身就很容易理解和魔改。
Transformers 庫還有一點與其他機器學習框架不同。用于構建模型的各個模塊并不是在不同文件之間共享的,每個模型有自己的層。這使得模型更容易理解,并且可以方便地在一個模型上進行實驗,而不會影響到其他模型。
本章將會介紹一個端到端的例子,使用 model、tokenizer 來實現第一章中 pipeline()
方法中的流程。對于 model API,我們將深入 model 類和 configuration 類,并展示如何加載一個模型、它如何處理數值輸入并輸出結果。然后介紹 tokenizer API,它負責第一個和最后一個處理步驟,處理從文本到神經網絡數值輸入的轉換,并在需要時轉換回文本。最后,將介紹如何通過高層 tokenizer()
方法在一個批次內處理多個句子。
pipeline背后
我們從一個完整的例子開始,下面的例子在第一章中已經介紹過,現在來看一下在它執行的背后發生了什么:
from transformers import pipelineclassifier = pipeline("sentiment-analysis")
classifier(["I've been waiting for a HuggingFace course my whole life.","I hate this so much!",]
)# 輸出:
[{'label': 'POSITIVE', 'score': 0.9598047137260437},{'label': 'NEGATIVE', 'score': 0.9994558095932007}]
下面這張圖介紹了 pipeline 方法中具體做的事情,下面將逐步看一下。
tokenizer預處理
Transformer 模型肯定不能直接處理原始文本數據,所以 pipeline 的第一步就是將原始文本數據轉換為數值數據。tokenizer 負責完成這件事情:
- 將輸入分割成單詞、子詞(subword)或者符號(比如根據標點),這些分割結果成為 tokens
- 將每個 token 映射為一個整數
- 添加一些額外的輸入
所有這些預訓練步驟,在模型預訓練、微調、使用(推理)時都應該保持完全一致,因此我們在 Model Hub 中下載模型時也需要下載這些信息。為了加載給定的預處理步驟,我們需要使用 AutoTokenizer
類和它的 from_pretrained()
方法。使用指定模型權重的名稱時,將會自動獲取與該模型對應的 tokenizer 并將其緩存(所以僅在第一次使用時需要下載)。
from transformers import AutoTokenizercheckpoint = "distilbert-base-uncased-finetuned-sst-2-english"
tokenizer = AutoTokenizer.from_pretrained(checkpoint)
當我們得到 tokenizer 之后,就可以直接將原始文本送入其中就可以得到可以直接輸入給模型的整型值。剩下要做的事情就是將這些整型值轉換為 tensor。
transformers 庫支持多數機器學習框架,如 PyTorch、TensorFlow、Flax(部分模型支持)。我們知道,在機器學習框架中,模型需要接收張量(tensor)類型作為輸入。我們可以通過 return_tensors
參數來指定返回張量類型所屬的框架(如 Pytorch、TensorFlow、NumPy)。
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)
padding
和 truncation
參數先不用管,后面會介紹。現在要知道的是,我們需要傳入一個或一組句子,并指定返回張量所屬的框架,如果未指定,則返回列表。以下是以 PyTorch 為例的返回結果:
{'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, 0, 0, 0, 0, 0, 0, 0, 0, 0]])
}
可以看到,返回結果是一個字典,其中有兩個鍵:input_ids
和 attention_mask
。其中 input_ids
由兩行整型值組成(每一行對應一個輸入句子),每行中的元素由獨特的整型值表示一個 token。attention_mask
后面會介紹。
model處理過程
我們同樣可以下載預訓練模型(model)。transformers 庫中有 AutoModel
類,與 AutoTokenizer
類似,它同樣有 from_pretrained()
方法。
from transformers import AutoModelcheckpoint = "distilbert-base-uncased-finetuned-sst-2-english"
model = AutoModel.from_pretrained(checkpoint)
在這段代碼中,我們下載了一個與之前 pipeline 中同樣的模型權重(如果運行過 pipeline 方法,這里不會重新下載,而是使用緩存的權重),并使用該權重實例化了一個模型。這個架構只包含基本的 Transformer 模塊:給定一些輸入,輸出隱藏狀態,也稱為特征。對于每個模型輸入,也將得到一個高維向量,表示Transformer 模型對該輸入的整體表示。雖然這些隱藏狀態本身也可以直接用,但它們通常是模型另一部分(head)的輸入。在第 1 章中,不同的任務可以用相同的體系結構來執行,但是這些任務中的每一個都有不同的 head 部分。
高維向量表示
Transformer 模型的輸出通常非常巨大,一般來說它包含三個維度:
- 批尺寸(batch size):同時處理的序列個數(上例中為 2);
- 序列長度(sequence length):用于表示一個序列數值表示的長度(上例中為 16);
- 隱藏狀態尺寸(hidden size):高維向量和隱藏狀態的尺寸
隱藏狀態之所以被稱為高維向量表示是因為它的尺寸通常非常大,在較小的 Transformer 模型中通常為 768,在大模型中可以達到 3072 甚至更大。可以通過以下方法查看它的尺寸:
outputs = model(**inputs)
print(outputs.last_hidden_state.shape)# 輸出:
torch.Size([2, 16, 768])
Transformer 模型的輸出像一個命名元組(namedtuple)或字典。可以通過屬性(outputs.last_hidden_state
)、鍵(outputs['last_hidden_state']
)、索引(outputs[0]
)三種形式訪問 。
模型輸出頭
模型頭(head)通常由幾層全連接層組成,接收 Transformer 模型輸出的隱藏狀態高維向量作為輸入,并將其映射到另外一個維度。在模型中的例程如下圖所示。Transformer 模型由其嵌入層(embeddings)和后續層表示。嵌入層將 token 輸入中的每個輸入 ID 轉換為表示相關 token 的向量。隨后的層使用注意力機制處理這些向量,以產生句子的最終表示。然后送到模型頭中進行處理,輸出最終的預測結果。
Transformers 中有許多不同的架構,每一種架構都是圍繞解決特定任務而設計的。以下列表展示了其中一部分:
*Model
(獲取模型的隱藏狀態)*ForCausalLM
*ForMaskedLM
*ForMultipleChoice
*ForQuestionAnswering
*ForSequenceClassification
*ForTokenClassification
- 其他
在我們的例子中,我們的模型需要有一個序列分類頭,來對句子是正向還是負向進行分類。所以實際上我們這里使用的不是 AutoModel
類,而是 AutoModelForSequenceClassification
類。
from transformers import AutoModelForSequenceClassificationcheckpoint = "distilbert-base-uncased-finetuned-sst-2-english"
model = AutoModelForSequenceClassification.from_pretrained(checkpoint)
outputs = model(**inputs)
現在再來看模型的輸出,維度數明顯低了很多,因為在序列分類模型中,輸出是模型頭將 Transformer 網絡輸出的高維向量處理之后的結果。最終的輸出向量只有兩維,分別對應兩個類別正向、負向。
print(outputs.logits.shape)
# 輸出:
torch.Size([2, 2])
后處理
經過 Transformer 和模型頭之后得到的輸出本身沒有實際意義。
print(outputs.logits)
# 輸出:
tensor([[-1.5607, 1.6123],[ 4.1692, -3.3464]], grad_fn=<AddmmBackward>)
模型對于兩個輸入句子的輸出結果分別是 [-1.5607, 1.6123]
和 [ 4.1692, -3.3464]
,現在的輸出不是概率而是 logits 。我們需要使用 softmax 進行一步后處理,得到每個類別對應的概率。(所有的 transformer 模型輸出的都是 logits,因為在計算損失函數(如交叉熵)時,會包含計算 softmax 的步驟。)
import torchpredictions = 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=<SoftmaxBackward>)
現在看到最終的輸出 [0.0402, 0.9598]
和 [0.9995, 0.0005]
就已經是對應標簽的概率值了。可以通過 id2label
屬性來確定該模型配置中輸出的每一維度所對應的標簽。
print(model.config.id2label)# 輸出:
{0: 'NEGATIVE', 1: 'POSITIVE'}
最終我們得到模型的預測如下:
- 句子一: 負向: 0.0402, 正向: 0.9598
- 句子而: 負向: 0.9995, 正向: 0.0005
至此,我們已經成功復現了 pipeline
方法中的三個步驟:使用 tokenizer 進行前處理、將輸入送入模型進行計算和后處理。下面我們會深入這些步驟的具體細節。
Models
本節將詳細地討論如何創建并使用一個模型。我們會用 AutoModel
類,它可以方便地通過權重文件來實例化一個模型。AutoModel
類及其所有相關類實際上是庫中各種可用模型的簡單封裝。它可以自動猜測適合給定權重文件的模型架構,然后使用該架構實例化模型。但是,如果您知道要使用的模型架構,可以直接使用定義該架構的類。接下來以 BERT 為例進行介紹。
創建模型
首先需要初始化一個 BERT 模型并加載一個配置對象。
from transformers import BertConfig, BertModel# Building the config
config = BertConfig()# Building the model from the config
model = BertModel(config)
配置對象包含了模型的許多屬性:
print(config)# 輸出:
BertConfig {[...]"hidden_size": 768,"intermediate_size": 3072,"max_position_embeddings": 512,"num_attention_heads": 12,"num_hidden_layers": 12,[...]
}
不同的加載方法
這種從默認配置中進行加載的方法會對模型權重進行隨機初始化:
from transformers import BertConfig, BertModelconfig = BertConfig()
model = BertModel(config)# Model is randomly initialized!
此時模型可以正常使用,但它的輸出是完全混亂的,它需要先訓練。我們可以根據手頭的任務從頭開始訓練模型,但正如在第 1 章中提到的,這將需要很長時間和大量數據,并且會對環境產生影響。為了避免不必要的重復工作,需要共享和復用已經預訓練過的模型。
加載預訓練的模型十分簡單,使用 from_pretrained()
方法即可:
from transformers import BertModelmodel = BertModel.from_pretrained("bert-base-cased")
在前面一節的介紹中,我們用的是 AutoModel
類,而非 BertModel
類。后面我們也將這樣做,這會稱為權重無關代碼;即如果代碼適用于一個權重,那么它也適用于加載其他權重文件。即使架構不同,只要權重針對類似任務(例如,情感分析任務)訓練的,就適用。在上面的代碼示例中,我們沒有使用 BertConfig
,而是通過 'bert-base-cased'
標識符加載了一個預訓練模型。該權重由 BERT 原作者自己訓練;可以在它的 model card 中查看更多細節。
現在該模型由指定權重文件初始化,它可以直接用于在本身的預訓練任務上進行推理,也可以用于在新任務上進行微調。相比于從頭訓練,在預訓練權重上進行微調能夠更快地得到更好的結果。
模型權重默認緩存在 ~/.cache/huggingface/transformers
目錄下,可以通過修改 HF_HOME
環境變量來更改緩存目錄。
標識符(如上面的 'bert-base-cased'
)可以用來指定加載 Model Hub 中的任何兼容 BERT 架構的模型。目前可用的 BERT 權重在這里查看。
保存方法
保存一個模型與加載模型同樣簡單,使用 save_pretrained()
方法:
model.save_pretrained("directory_on_my_computer")
這將在本地磁盤上保存兩個文件:
ls directory_on_my_computerconfig.json pytorch_model.bin
其中 config.json 文件中保存了構建模型必要的一些屬性配置,還有一些元數據(metadata),比如保存該權重文件時的 transformers 庫版本等。而 pytorch_model.bin 保存了模型的權重參數 。兩份文件配合起來完整地保存了一個 Transformer 模型結構、權重的相關信息。
使用transformer模型進行推理
現在我們已經介紹過如何加載和保存模型。接下來將使用模型進行推理。前面提到過,Transformer 模型只能處理經 tokenizer 得到的數值輸入,在詳細介紹 tokenizer 之前,我們先來看一下模型接收的輸入。
tokenizer 負責將輸入文本轉換為對應框架的張量,關于 tokenizer 后面會詳細介紹,這里先快速看一下從原始文本到模型輸入需要哪些步驟。
我們有一組原始文本序列:
sequences = ["Hello!", "Cool.", "Nice!"]
tokenizer 將這些序列轉換為詞表索引(通常稱為 input IDs),每個序列是一組數字,輸出如下:
encoded_sequences = [[101, 7592, 999, 102],[101, 4658, 1012, 102],[101, 3835, 999, 102],
]
這是一組序列(列表的列表),要將多維數組數據轉換為 tensor 格式,該多維數組必須是矩形的形狀(即每個維度的長度需要一致),這里剛好滿足(不滿足的話一般需要填充),可以直接轉換為 tensor:
import torchmodel_inputs = torch.tensor(encoded_sequences)
接下來就可以直接將 tensor 格式的數值數據輸入給模型:
output = model(model_inputs)
模型接收許多參數,只有 input IDs 是必須得,其他參數將會再后面介紹。現在我們先來看一下 tokenizer 是如何將原始文本輸入轉換為數值輸入的。
tokenizers
分詞方式簡介
tokenizers 是 NLP pipeline 中的一個關鍵組件,它的作用是:將文本轉換為模型能夠處理的數值型輸入。本節將介紹 tokenization 的過程。
在 NLP 任務中,輸入的是原始文本數據,如:
Jim Henson was a puppeteer
然而,模型只能處理數值型數據,所以我們需要 tokenizer 來將原始文本轉換為數值型數據,有許多種分詞方法能夠實現這樣的轉換,核心的目標是找到最有意義的表示,即最方便模型理解的表示方法,然后,還要盡可能得小。以下將介紹幾種常用的分詞方法。
word-based
首先能夠想到的就是基于詞(word-based)的分詞方法。它通常易于實現,并且效果也不錯。如下圖所示,按照 word-based 方法進行分詞,并得到它們的數值表示:
word-based 分割原始文本也有不同的具體方法。比如最直接地按照空格來分詞,是用 Python 中字符串的 split()
方法即可實現:
tokenized_text = "Jim Henson was a puppeteer".split()
print(tokenized_text)# 輸出:
['Jim', 'Henson', 'was', 'a', 'puppeteer']
也有一些加入標點的 word-based 分詞方法的變體,通過這一類的 tokenizer,最終會得到一個相當大的詞表,其中包含所有在語料庫中出現過的 token。每個單詞會被分配到一個 ID,ID 的值從 0 開始,一直到詞表的總大小結束。模型使用這些 ID 來表示每一個單詞。
如果想用 word-based 的 tokenizer 來完全覆蓋一種語言,需要為語言中的每個單詞都有一個 token,這將會有大量的 token。例如,英語中有超過 500000 個單詞,要建立從每個單詞到輸入 ID 的映射,需要跟蹤這么多ID。此外,像 “dog” 這樣的詞與 “dogs” 一樣的詞的表達方式不同,模型最初無法知道 “dog” 和 “dogs” 是相似的:它會將這兩個詞識別為不相關。這同樣適用于其他類似的詞,如 “run” 和 “running”,模型在學習開始前也不會將其視為相似。
另外,我們還要需要一個自定義標記來表示詞匯表中沒有的單詞。這被稱為 “unknown” token,通常表示為 “[UNK]
”或 “”。如果我們的 tokenizer 生成了大量的 unknown token,這通常是一個不好的跡象,因為這表明它無法檢索一個單詞的合理表示,并且在這一過程中丟失了信息。制定詞表需要 tokenizer 產生盡可能少的 unknown token。
解決過多 unknown token 的方法是按照更深一層進行分詞,即 character-based tokenizer。
character-based
character-based tokenizer 將文本劃分為字符,這樣做有兩個顯然的好處:
- 詞表會小得多
- unknown token 會少得多,緩解 oov(out of vocabulary)問題,因為每個單詞都是由英文字母組成
這種方法同樣不完美,由于現在的表示是基于字符而不是單詞,很明顯每個字母是沒有什么意義的。但這也因語言而異;例如,在漢語中,每個字符比拉丁語中的一個字符包含更多的信息。
另一個問題是,這種方法會需要大量的 token:一個單詞使用 word-based 的 tokenizer 只需要一個 token,但當使用 character-based 方法時,則需要 10 個甚至更多 token,
為了兩全其美,我們一般使用結合了這兩種方法的技術:subword tokenization。
subword tokenization
子單詞標記化算法依賴于這樣一個原則:頻繁使用的單詞不應該被拆分成更小的子單詞,而少見的單詞可以被分解成有意義的子詞。
例如,“annoyingly” 可能被認為是一個少見的詞,可以分解為 “annoying” 和“ly”。這兩個詞都可能更頻繁地作為獨立的子詞出現,同時 “annoyingly” 的含義可以被 “annoying” 和 “ly” 的復合所表示。
下面是一個使用子詞表示法進行分詞的例子:
subword 分詞法最終可以得到一個具有豐富語義的詞表,比如上例中的 “tokenization” 被分為了 “token” 和 “ization”,這兩個 token 都是有明確含義的。而且在空間上也比較高效,只需要兩個 token 就可以組成一個長單詞。
這種方法在土耳其語等語言中尤其有用,在這些語言中,通過將子詞串在一起,可以形成(幾乎)任意長的復雜單詞。
這里再介紹幾種具體的目前常用分詞方法:
- Byte-level BPE, GPT-2 的分詞方式
- WordPiece, BERT 的分詞方式
- SentencePiece和Unigram, 支持多語言模型
現在大家應該對常見分詞方式有了一定的了解,接下來將介紹相關的 API。
保存和加載
保存和加載 tokenizers 與保存/加載 models 一樣簡單。還是使用 from_pretrained()
和 save_pretrained()
兩個方法。這兩個方法會加載或保存 tokenizer 使用的算法(類似于保存模型的架構)和它生成的詞表(類似于保存模型的權重參數)。
加載 BERT 的 tokenizer 與加載 BERT 權重的方式類似,只需要替換為 BertTokenizer
類就可以了:
from transformers import BertTokenizertokenizer = BertTokenizer.from_pretrained("bert-base-cased")
與 AutoModel
類似,AutoTokenizer
可以自動地選擇庫中合適的 tokenizer 類:
from transformers import AutoTokenizertokenizer = AutoTokenizer.from_pretrained("bert-base-cased")
現在可以像上一小節中一樣使用 tokenizer 了:
tokenizer("Using a Transformer network is simple")# 輸出:
{'input_ids': [101, 7993, 170, 11303, 1200, 2443, 1110, 3014, 102],'token_type_ids': [0, 0, 0, 0, 0, 0, 0, 0, 0],'attention_mask': [1, 1, 1, 1, 1, 1, 1, 1, 1]}
保存 tokenizer 與保存 model 類似:
tokenizer.save_pretrained("directory_on_my_computer")
我們將在之后討論 token_type_ids 和 attention_mask。現在,我們先來看看 input_ids 是如何生成的。為此,我們需要查看 tokenizer 的中間方法。
Encoding
將文本轉換為數字的過程稱為 encoding,它由兩步組成:分詞、將分詞結果轉換為輸入 ID。
第一步就是將文本分割為單詞(或者子詞、標點等),即前面介紹的分詞,得到 tokens。有多種規則可以實現該過程,這就是為什么需要使用模型的名稱來實例化 tokenizer,是為了確保使用的規則與預訓練模型時使用的規則相同。
第二步是將這些 tokens 轉換為數字,然后可以將它們轉換為張量并輸入到模型中。tokenizer 有一張詞表來記錄 token 到數值的映射,在推理或微調時,需要保證與預訓練時的詞表是一致的。
接了更高的理解這兩個步驟,我們將分別看一下兩個步驟的過程。注意在實際使用中并不需要這樣做,直接調用 tokenizer 并將你的輸入傳入即可。
分詞
分詞的過程由 tokenizer 的 tokenize()
方法完成:
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']
上例中的 tokenizer 是一個 subword tokenizer,它不斷地將單詞進行拆分,直至可以完全被詞表中的 tokens 表示。例子中的 transformer 被拆分為 transform 和 ##er。
轉換為數值
將分詞結果轉換為 input IDs 的步驟由 convert_tokens_to_ids()
方法來實現:
ids = tokenizer.convert_tokens_to_ids(tokens)print(ids)
# 輸出:
[7993, 170, 11303, 1200, 2443, 1110, 3014]
這些輸出,在轉換為對應框架的張量后,可以直接作為模型的輸入。
Decoding
decoding 與 encoding 相反:根據詞表索引,得到一個字符串。可以由 decode()
方法實現:
decoded_string = tokenizer.decode([7993, 170, 11303, 1200, 2443, 1110, 3014])
print(decoded_string)# 輸出:
'Using a Transformer network is simple'
decode()
方法不僅將索引轉換回 tokens,還將屬于同一單詞的 token 合并在一起,生成可讀的句子。
現在,已經介紹了 tokenizer 的各種原子操作:分詞、轉換為 ID,ID 轉回字符串。然而,這只是冰山一角,接下來將介紹它的限制以及如何克服。
處理多序列
在上一節中,我們探討了最簡單的用例:對單個長度較小的序列進行推理。然而,實際中有很多新問題:
- 如何處理多個序列?
- 如何處理不同長度的多個序列?
- 模型只能接收詞表索引作為輸入嗎?
- 是否存在序列過長的問題?
讓我們看一下怎么使用 Transformer API 來處理這些問題。
批量輸入模型
在之前已經介紹過如何將序列轉換為數字列表。現在把這個數字列表轉換為張量,并將其送入到模型:
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)
# 這一行會報錯
model(input_ids)# 報錯:
IndexError: Dimension out of range (expected to be in range of [-1, 0], but got 1)
為什么會報錯呢?是因為我們這里將單個序列送入了模型,而 Transformer 模型默認接收多個序列。這里我們試圖按照上節的介紹手動實現 tokenizer 的內部步驟,但發現行不通。實際上 tokenizer 不止是將文本轉換為序列張量,而且還在張量上添加了一個維度。
tokenized_inputs = tokenizer(sequence, return_tensors="pt")
print(tokenized_inputs["input_ids"])
# 輸出:
tensor([[ 101, 1045, 1005, 2310, 2042, 3403, 2005, 1037, 17662, 12172,2607, 2026, 2878, 2166, 1012, 102]])
我們手動加一個維度再重新試一下:
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])
print("Input IDs:", input_ids)output = model(input_ids)
print("Logits:", output.logits)# 輸出:
Input IDs: [[ 1045, 1005, 2310, 2042, 3403, 2005, 1037, 17662, 12172, 2607, 2026, 2878, 2166, 1012]]
Logits: [[-2.7276, 2.8789]]
batching ,是指同時向模型輸入多個句子。如果只有一個句子,也可以將它們打包起來:
batched_ids = [ids, ids]
這個 batch 包含兩個相同的句子。
batching 使得模型在接收多個輸入序列時能夠正常工作。同時處理多個序列還有一個問題:當將多個序列打包在一起時,它們的長度可能會不同。而要轉換為張量,必須是矩形的數組。我們通常采用填充來解決這個問題。
填充輸入
下面這種非矩形的數組無法直接轉換為張量
batched_ids = [[200, 200, 200],[200, 200]
]
為了解決這個問題,我們對輸入進行填充,來使得它們有相同的長度,即得到矩形的數組。具體來說,我們在每個長度較短的序列后面添加一種特殊的 token:padding token。如果我們有 10 個 10 個 tokens 的句子和 1 個 20 個 tokens 的句子,就要通過填充將它們全部填充到 20 個 token。在上面的例子中,應該填充成下面這樣:
padding_id = 100batched_ids = [[200, 200, 200],[200, 200, padding_id],
]
padding token id 可以通過 tokenizer.pad_token_id
查看。
現在,我們將單個句子和兩個組成 batch 的句子分別送入模型看一下結果是否相同:
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=<AddmmBackward>)
tensor([[ 0.5803, -0.4125]], grad_fn=<AddmmBackward>)
tensor([[ 1.5694, -1.3895],[ 1.3373, -1.2163]], grad_fn=<AddmmBackward>)
可以看到,出現了一些問題。我們期望的結果是將兩個序列一同送入時每個句子的輸出結果與單個句子分別送入時的輸出結果一致,但是看第二個句子,輸出結果明顯是不一樣的。
這是因為 Transformer 模型的自注意力機制會關注到序列中的每一個 token,這當然會將我們填充的 token 也計算在內。當輸入不同長度的句子經過填充統一長度時,我們需要告訴模型每個句子的真實長度,即哪些是實際的數據 token,哪些是為了保持長度一致填充的 token。實際中我們會通過傳入 attention masks 來實現這一點。
attention masks
attention masks 是與輸入張量同樣形狀的一個張量,由 0 和 1 組成。1 表示對應的 token 是實際的數據 token 需要計算注意力,而 0 表示對應的 token 是填充的 token 不需要計算注意力。
上面的例子應該修改為這樣:
batched_ids = [[200, 200, 200],[200, 200, tokenizer.pad_token_id],
]attention_mask = [[1, 1, 1],[1, 1, 0],
]outputs = model(torch.tensor(batched_ids), attention_mask=torch.tensor(attention_mask))
print(outputs.logits)# 輸出:
tensor([[ 1.5694, -1.3895],[ 0.5803, -0.4125]], grad_fn=<AddmmBackward>)
現在我們的第二個句子就得到了預期中的結果,
更長的序列
在 Transformer 模型中,我們可以傳遞給模型的序列長度是有限制的。大部分模型可以處理 最多512 或 1024 個 token 的序列,再長就無能為力了。如果需要處理更長的序列,有兩種方案:
- 使用一個支持更長序列的模型
- 對輸入序列進行截斷
不同的模型能夠處理的最長序列長度不同,有些模型擅長處理長序列。比如 Longformer 和 LED 等。如果你的任務中都是非常長的序列,那么推薦使用這些模型,否則,推薦使用 max_sequence_length
對個別超出長度上限的序列進行截斷。
sequence = sequence[:max_sequence_length]
整合到一起
在之前幾節,我們已經分別介紹了 tokenizer 的幾個步驟,包括分詞、轉換到 ID、填充、截斷、注意力掩碼。然而 Transformer API 可以通過一個高層的函數處理所有這些過程。當我們調用 tokenizer
并傳入句子時,得到的返回值就是可以直接傳入模型的輸入。
from transformers import AutoTokenizercheckpoint = "distilbert-base-uncased-finetuned-sst-2-english"
tokenizer = AutoTokenizer.from_pretrained(checkpoint)sequence = "I've been waiting for a HuggingFace course my whole life."model_inputs = tokenizer(sequence)
這里的 model_inputs
包含了所有模型所需要的輸入。對于 distilBERT,這包括 input IDs 和 attention mask 。對于其他模型,可能會有其他所需要的輸入,同樣會由 tokenizer 對象返回出來。
如下面例子所示,這個方法十分強大。首先,它可以處理單個序列:
sequence = "I've been waiting for a HuggingFace course my whole life."model_inputs = tokenizer(sequence)
也可以同時處理多個序列,并且不需要改動調用方式:
sequences = ["I've been waiting for a HuggingFace course my whole life.", "So have I!"]model_inputs = tokenizer(sequences)
可以支持多種 padding 方式:
# 將各序列填充到輸入序列中的最大長度
model_inputs = tokenizer(sequences, padding="longest")# 將序列填充到模型支持的最大長度(對于BERT或distilBERT,為512)
model_inputs = tokenizer(sequences, padding="max_length")# 將序列填充到指定的最大長度
model_inputs = tokenizer(sequences, padding="max_length", max_length=8)
也可以截斷序列:
sequences = ["I've been waiting for a HuggingFace course my whole life.", "So have I!"]# 將序列截斷到模型支持的最大長度(對于BERT或distilBERT,為512)
model_inputs = tokenizer(sequences, truncation=True)# 將大于最大長度的序列進行截斷
model_inputs = tokenizer(sequences, max_length=8, truncation=True)
tokenizer 還可以將數值轉換到特定框架的張量類型,可以直接輸入到模型中:
sequences = ["I've been waiting for a HuggingFace course my whole life.", "So have I!"]# PyTorch
model_inputs = tokenizer(sequences, padding=True, return_tensors="pt")# TensorFlow
model_inputs = tokenizer(sequences, padding=True, return_tensors="tf")# NumPy
model_inputs = tokenizer(sequences, padding=True, return_tensors="np")
特殊token
查看 tokenizer 返回的 input IDs,可以看到與之前有些不同:
sequence = "I've been waiting for a HuggingFace course my whole life."model_inputs = tokenizer(sequence)
print(model_inputs["input_ids"])tokens = tokenizer.tokenize(sequence)
ids = tokenizer.convert_tokens_to_ids(tokens)
print(ids)# 輸出:
[101, 1045, 1005, 2310, 2042, 3403, 2005, 1037, 17662, 12172, 2607, 2026, 2878, 2166, 1012, 102]
[1045, 1005, 2310, 2042, 3403, 2005, 1037, 17662, 12172, 2607, 2026, 2878, 2166, 1012]
在序列的開始前和結束后分別添加了一個 token,我們可以對上述序列進行 decode:
print(tokenizer.decode(model_inputs["input_ids"]))
print(tokenizer.decode(ids))# 輸出:
"[CLS] i've been waiting for a huggingface course my whole life. [SEP]"
"i've been waiting for a huggingface course my whole life."
tokenizer 在開始前添加了一個特殊 token [CLS]
,在結束后添加了一個特殊 token [SEP]
。這是因為 BERT 模型在預訓練時任務中需要這兩個額外的特殊 token,所以為了在推理時保持結果一致,也需要添加。不同的模型可能會需要添加不同的特殊 token,有的模型只在后面加,有的只在前面加,有的不需要加。總之,tokenizer 都會知道需要怎么加,并幫你做好。
總結:從tokenizer到model
現在我們已經介紹完了 tokenizer 處理文本時的全部步驟,讓我們最后看一下它是如何處理多序列(填充)、如何處理長序列(截斷)以及如何返回不同框架的張量類型:
import torch
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")
output = model(**tokens)
概括一下,在本章中,我們:
- 學習了 Transformer 模型的基本構建塊。
- 了解了 tokenizer pipeline 的組成。
- 了解如何在實踐中使用 Transformer 模型。
- 學習了如何利用標 tokenizer 將文本轉換為模型可以理解的張量。
- 同時設置 tokenizer 和 model,從文本到預測。
- 學習了輸入 ID 的限制,并學習了注意力掩碼。
- 使用多功能和可配置的 tokenizer 方法。
從現在起,你應該能夠自由地瀏覽 Transformer 的文檔:其中術語會聽起來很熟悉,你已經看到了大多數時候會用到的方法。