Hugging Face NLP課程學習記錄 - 2. 使用 Hugging Face Transformers

shizidushu發表於2024-09-19

Hugging Face NLP課程學習記錄 - 2. 使用 Hugging Face Transformers

說明:

  • 首次發表日期:2024-09-19
  • 官網: https://huggingface.co/learn/nlp-course/zh-CN/chapter2
  • 關於: 閱讀並記錄一下,只保留重點部分,大多從原文摘錄,潤色一下原文

2. 使用 Hugging Face Transformers

管道的內部(Behind the pipeline)

從例子開始:

from transformers import pipeline

classifier = pipeline("sentiment-analysis")
classifier(
    [
        "I've been waiting for a HuggingFace course my whole life.",
        "I hate this so much!",
    ]
)

原始文字(Raw text) --> 分詞器(Tokenizer) --> 模型(Model)--> 後處理/預測(Predictions)

使用分詞器進行預處理(Preprocessing with a tokenizer)

與其他神經網路一樣,Transformer模型無法直接處理原始文字, 因此我們管道的第一步是將文字輸入轉換為模型能夠理解的數字。 為此,我們使用tokenizer,負責:

  • 將輸入拆分為單詞、子單詞或符號(如標點符號),稱為token
  • 將每個token對映到一個整數
  • 新增可能對模型有用的其他輸入

我們使用AutoTokenizer類及其from_pretrained()方法獲取與訓練時相同的tokenizer。

from transformers import AutoTokenizer

checkpoint = "distilbert-base-uncased-finetuned-sst-2-english"
tokenizer = AutoTokenizer.from_pretrained(checkpoint)

一旦我們有了分詞器(tokenizer),我們可以直接將句子傳遞給它,我們會得到一個字典(dictionary),這個字典已經準備好輸入到我們的模型中了!唯一剩下要做的就是將輸入ID的列表轉換成張量。

Transformers的後端可能是Pytorch,Tensorflow或者Flax。

Transformers模型只接受張量作為輸入。

要指定要返回的張量型別(PyTorch、TensorFlow或plain NumPy),我們使用return_tensors引數:

raw_inputs = [
    "I've been waiting for a HuggingFace course my whole life.",
    "I hate this so much!",
]
inputs = tokenizer(raw_inputs, padding=True, truncation=True, return_tensors="pt")
print(inputs)
{
    '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包含兩行整數(每句話一行),這些整數是每句話中詞元(token)的唯一識別符號。我們稍後會在本章解釋attention_mask是什麼。

瞭解模型(Go through the model)

我們可以像下載分詞器(tokenizer)一樣下載我們的預訓練模型。

from transformers import AutoModel

checkpoint = "distilbert-base-uncased-finetuned-sst-2-english"
model = AutoModel.from_pretrained(checkpoint)

This architecture contains only the base Transformer module: given some inputs, it outputs what we’ll call hidden states, also known as features. For each model input, we’ll retrieve a high-dimensional vector representing the contextual understanding of that input by the Transformer model.

高維向量(A high-dimensional vector?)

Transformer輸出的向量一般很大。通常有3個維度:

  • Batch size: 一次處理的序列數(在我們的示例中為2)。
  • Sequence length: 序列的數值表示的長度(在我們的示例中為16)。
  • Hidden size: 每個模型輸入的向量維度。

之所以被稱為高維,是因為Hidden size. Hidden size可能非常大(768通常用於較小的型號,而在較大的型號中,這可能達到3072或更大)。

如果我們將預處理的輸入輸入到模型中,我們可以看到這一點:

outputs = model(**inputs)
print(outputs.last_hidden_state.shape)
torch.Size([2, 16, 768])

注意🤗 Transformers模型的輸出與namedtuple或詞典相似。您可以透過屬性(就像我們所做的那樣)或鍵(輸出["last_hidden_state"])訪問元素,甚至可以透過索引訪問元素,前提是您確切知道要查詢的內容在哪裡(outputs[0])。

模型頭:數字的意義(Model heads: Making sense out of numbers)

模型頭將隱藏狀態的高維向量作為輸入,並將其投影到不同的維度。它們通常由一個或幾個線性層組成:

![[en_chapter2_transformer_and_head.svg]]

Transformers模型的輸出直接傳送到模型頭進行處理。

In this diagram, the model is represented by its embeddings layer and the subsequent layers. The embeddings layer converts each input ID in the tokenized input into a vector that represents the associated token. The subsequent layers manipulate those vectors using the attention mechanism to produce the final representation of the sentences.

Transformers中有許多不同的體系結構,每種體系結構都是圍繞處理特定任務而設計的。以下是一個非詳盡的列表:

  • *Model (retrieve the hidden states)
  • *ForCausalLM
  • *ForMaskedLM
  • *ForMultipleChoice
  • *ForQuestionAnswering
  • *ForSequenceClassification
  • *ForTokenClassification
  • 以及其他

對於我們的示例,我們需要一個帶有序列分類頭的模型(能夠將句子分類為肯定或否定)。因此,我們實際上不會使用AutoModel類,而是使用AutoModelForSequenceClassification

from transformers import AutoModelForSequenceClassification

checkpoint = "distilbert-base-uncased-finetuned-sst-2-english"
model = AutoModelForSequenceClassification.from_pretrained(checkpoint)
outputs = model(**inputs)

現在,如果我們觀察輸出的形狀,維度將低得多:模型頭將我們之前看到的高維向量作為輸入,並輸出包含兩個值的向量(每個標籤一個):

print(outputs.logits.shape)
torch.Size([2, 2])

對輸出進行後處理( Postprocessing the output)

我們從模型中得到的輸出值本身並不一定有意義。我們來看看,

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層(所有🤗Transformers模型輸出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=<SoftmaxBackward>)

現在我們可以看到,模型預測第一句為[0.0402, 0.9598],第二句為[0.9995, 0.0005]。這些是可識別的機率分數。

為了獲得每個位置對應的標籤,我們可以檢查模型配置的id2label屬性(下一節將對此進行詳細介紹):

model.config.id2label
{0: 'NEGATIVE', 1: 'POSITIVE'}

現在我們可以得出結論,該模型預測了以下幾點:

  • 第一句:否定:0.0402,肯定:0.9598
  • 第二句:否定:0.9995,肯定:0.0005

模型(Models)

建立一個Transformer(Creating a Transformer)

初始化BERT模型需要做的第一件事是載入配置物件:

from transformers import BertConfig, BertModel

# Building the config
config = BertConfig()

# Building the model from the config
model = BertModel(config)

配置包含許多用於構建模型的屬性:

print(conrfig)
BertConfig {
  [...]
  "hidden_size": 768,
  "intermediate_size": 3072,
  "max_position_embeddings": 512,
  "num_attention_heads": 12,
  "num_hidden_layers": 12,
  [...]
}

不同的載入方式(Different loading methods)

從預設配置建立模型會使用隨機值對其進行初始化:

from transformers import BertConfig, BertModel

config = BertConfig()
model = BertModel(config)

# Model is randomly initialized!

該模型可以在這種狀態下使用,但會輸出胡言亂語;首先需要對其進行訓練。

載入已經訓練過的Transformers模型很簡單 - 我們可以使用from_pretrained() 方法:

from transformers import BertModel

model = BertModel.from_pretrained("bert-base-cased")

正如您之前看到的,我們可以用等效的AutoModel類替換Bert模型。

區別:

  • 使用AutoModel類時,我們是不知道模型的檢查點(checkpoint)的。
  • 使用BertModel.from_pretrained("bert-base-cased")時,透過指定的識別符號bert-base-cased載入預訓練模型,這是BERT作者訓練出來的模型檢查點(model checkpoint)。

儲存模型(Saving methods)

我們使用 save_pretrained() 方法儲存模型:

model.save_pretrained("directory_on_my_computer")

使用Transformers模型進行推理(Using a Transformer model for inference)

Transformer模型只能處理數字——分詞器生成的數字。但在我們討論分詞器之前,讓我們先探討模型接受哪些輸入。

分詞器(Tokenizer)負責將輸入轉換為合適框架(Pytorch,Tensorflow或Flax)的張量。

假設我們有幾個序列:

sequences = ["Hello!", "Cool.", "Nice!"]

分詞器將他們轉換為詞彙表索引(vocabulary indices),通常我們稱其為Input IDs. 現在每個序列都是一個數字列表!結果是:

encoded_sequences = [
    [101, 7592, 999, 102],
    [101, 4658, 1012, 102],
    [101, 3835, 999, 102],
]

這是一個編碼後的序列列表。將其轉換為張量(tensors):

import torch

model_inputs = torch.tensor(encoded_sequences)

使用張量作為模型的輸入(Using the tensors as inputs to the model)

output = model(model_inputs)

分詞器(Tokenizers)

分詞器是自然語言處理(NLP)流水線的核心元件之一。它們有一個目的:將文字轉換成模型可以處理的資料。模型只能處理數字,因此分詞器需要將我們的文字輸入轉換為數值資料。

在 NLP 任務中,通常處理的資料是原始文字。如:

Jim Henson was a puppeteer

然而,模型只能處理數字,所以我們需要找到一種方法將原始文字轉換為數字。這就是分詞器的作用,而且有很多方法可以實現這一點。目標是找到最有意義的表示形式——即對模型來說最有意義的那種——如果可能的話,還要找到最小的表示形式。

讓我們看看一些分詞器演算法的例子。

基於詞的(Word-based)

首先想到的分詞器型別是基於單詞的。它通常非常容易設定和使用,只需要一些規則,而且通常能產生不錯的結果。

例如,對於原始文字Let's do tokenization!

基於空格(space)拆分:Let's do tokenization!
基於標點(Punctuation)拆分:Let 's do tokenization !

我們可以透過應用Python的split()函式,使用空格將文字分詞為單詞:

tokenized_text = "Jim Henson was a puppeteer".split()
print(tokenized_text)
['Jim', 'Henson', 'was', 'a', 'puppeteer']

有許多基於詞的分詞器(word tokenizers)的變體,它們有額外的關於標點的規則。

使用這種分詞器,我們可能會得到一些相當大的“詞彙表”,其中詞彙表定義為我們語料庫中獨立詞元(token)的總數。

每個單詞都被分配一個ID,從0開始,一直到詞彙表的大小。模型使用這些ID來識別每個單詞。

如果我們想用基於詞的分詞器完全覆蓋一種語言,那麼我們需要為該語言中的每個單詞分配一個識別符號,這將生成大量的詞元。例如,英語中有超過50萬個單詞,因此為了將每個單詞對映到一個輸入ID,我們需要跟蹤這麼多的ID。此外,像“dog”這樣的單詞與“dogs”這樣的單詞表現不同,而模型最初並無法知道“dog”和“dogs”是相似的:它會將這兩個單詞識別為不相關。同樣的情況也適用於其他類似的單詞,比如“run”和“running”,模型最初也不會認為它們是相似的。

最後,我們需要一個自定義的詞元來表示不在詞彙表中的單詞。這通常稱為“未知”詞元,通常表示為“[UNK]”或“<unk>”。如果你發現分詞器生成了大量這樣的詞元,通常這是一個不好的跡象,因為它無法為某個單詞找到合適的表示形式,導致在此過程中丟失了資訊。在構建詞彙表時,目標是儘可能減少分詞器將單詞分詞為未知詞元的情況。

減少未知詞元數量的一種方法是更進一步,使用基於字元的分詞器(character-based tokenizer)。

基於字元的(Character-based)

基於字元的分詞器將文字分割成字元,而不是單詞。這有兩個主要好處:

  1. 詞彙表要小得多。
  2. 詞彙表外(未知)的詞元(token)要少得多,因為每個單詞都可以由字元構建。

但在這裡,同樣會出現一些關於空格和標點符號的問題:

![[some questions arise concerning spaces and punctuation.png]]

這種方法也並不完美。由於表示現在基於字元而非單詞,可以說從直觀上看,這種方法的意義較小:每個字元本身並沒有太多意義,而單詞則不然。不過,這一點在不同語言中有所差異;例如,在中文中,每個字元所承載的資訊比拉丁語系語言中的字元要多。

另一個需要考慮的問題是,我們的模型將需要處理大量的詞元:在基於詞的分詞器中,一個單詞只會是一個詞元,而當其轉換為字元時,很容易變成10個或更多的詞元。

為了兼顧兩種方法的優點,我們可以使用結合這兩種方法的第三種技術:子詞分詞(subword tokenization)。

字詞分詞(subword tokenization)

子詞分詞演算法基於這樣一個原則:頻繁使用的單詞不應被拆分成更小的子詞,而罕見的單詞則應被分解為有意義的子詞。

例如,“annoyingly”可能被認為是一個罕見的單詞,可以被分解為“annoying”和“ly”。這兩個部分作為獨立的子詞出現的頻率可能更高,同時透過“annoying”和“ly”的組合含義,保留了“annoyingly”的意義。

以下是一個示例,展示了子詞分詞演算法如何對序列“Let’s do tokenization!”進行分詞:

Let's</w> do</w> token ization</w> !</w>

這些子詞提供了豐富的語義意義:例如,在上面的示例中,“tokenization”被拆分為“token”和“ization”,這兩個詞元既具有語義意義,又在空間上很高效(僅需兩個詞元來表示一個長單詞)。這使我們能夠在較小的詞彙表中實現相對較好的覆蓋率,並幾乎沒有未知詞元。

還有更多!(And more!)

不出所料,還有更多的技術。僅舉幾例:

  • Byte-level BPE, 用於 GPT-2
  • WordPiece, 用於 BERT
  • SentencePiece or Unigram, 用於多個多語言模型

載入和儲存(Loading and saving)

載入和儲存分詞器與模型一樣簡單。實際上,它基於相同的兩種方法:from_pretrained() 和 save_pretrained()。這些方法將載入或儲存分詞器使用的演算法(有點類似於模型的架構)以及它的詞彙表(有點類似於模型的權重)。

from transformers import BertTokenizer

tokenizer = BertTokenizer.from_pretrained("bert-base-cased")

與AutoModel類似,AutoTokenizer類將根據檢查點名稱自動獲取庫中的適當分詞器類,並且可以直接與任何檢查點一起使用:

from transformers import AutoTokenizer

tokenizer = AutoTokenizer.from_pretrained("bert-base-cased")
tokenizer("Using a Transformer network is simple")

儲存分詞器:

tokenizer.save_pretrained("directory_on_my_computer")

編碼(Encoding)

將文字轉換為數字稱為編碼。編碼是一個兩步過程:首先是分詞,然後是轉換為input IDs。

  • 第一步是將文字拆分為單詞(或單詞的部分、標點符號等),通常稱為詞元(Token)。
  • 第二步是將這些詞元轉換為數字,以便我們可以根據它們構建張量並將其輸入到模型中。為此,分詞器有一個詞彙表(vocabulary),這是我們在使用from_pretrained()方法例項化分詞器時下載的部分。同樣,我們需要使用在模型預訓練時所使用的相同詞彙表。

詞元化(Tokenization)

分詞過程是透過分詞器的 tokenize() 方法完成的:

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']

這個分詞器是一個子詞分詞器(subword tokenizer):它會拆分單詞,直到獲得可以由其詞彙表表示的詞元。在這裡,“transformer”被拆分為兩個詞元:transform##er

從詞元到Input IDs(From tokens to input IDs)

使用分詞器(tokenizer)的 convert_tokens_to_ids() 方法進行轉化:

ids = tokenizer.convert_tokens_to_ids(tokens)

print(ids)
[7993, 170, 11303, 1200, 2443, 1110, 3014]

解碼(Decoding)

decoded_string = tokenizer.decode([7993, 170, 11303, 1200, 2443, 1110, 3014])

print(decoded_string)
Using a transformer network is simple

處理多個序列(Handling multiple sequences)

在上一節中,我們探討了最簡單的用例:對一個小長度的序列進行推理。然而,一些問題已經出現:

  • 我們如何處理多個序列?
  • 我們如何處理多個序列不同長度?
  • 詞彙索引是讓模型正常工作的唯一輸入嗎?
  • 是否存在序列太長的問題?

模型期望一批次輸入(Models expect a batch of inputs)

在前面的練習中,你看到了序列如何被轉換為數字列表。現在讓我們將這個數字列表轉換為張量並傳遞給模型:

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)

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
model(input_ids)

報錯:

IndexError: Dimension out of range (expected to be in range of [-1, 0], but got 1)
print(input_ids)
tensor([ 1045, 1005, 2310, 2042, 3403, 2005, 1037, 17662, 12172, 2607, 2026, 2878, 2166, 1012])
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]])
print(input_ids.shape, tokenized_inputs["input_ids"].shape)
torch.Size([14]) torch.Size([1, 16])

需要新增一個新的維度:

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)

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)是指一次性將多個句子傳遞給模型。如果你只有一個句子,你可以用單個序列構建一個批次。

這是一個包含兩條相同序列的批次(Batches):

batched_ids = [ids, ids]

批處理使得模型能夠處理多個句子。使用多個序列與構建單個序列的批處理同樣簡單。然而,還有一個問題。當你嘗試將兩句(或更多句)放在一起進行批處理時,它們的長度可能不同。如果你曾經使用過張量,你就知道它們需要是矩形的形狀,因此無法直接將輸入ID列表轉換為張量。為了解決這個問題,我們通常會對輸入進行填充。

填充輸入(Padding the inputs)

以下列表不能轉換為張量:

batched_ids = [
    [200, 200, 200],
    [200, 200]
]

為了解決這個問題,我們將使用填充(padding)來使我們的張量具有矩形形狀。填充透過向較短的句子新增一個特殊的詞(稱為填充詞元,padding token)來確保所有句子具有相同的長度。例如,如果你有10個句子,每個句子有10個單詞,還有1個句子有20個單詞,填充將確保所有句子都具有20個單詞。在我們的示例中,生成的張量看起來像這樣:

padding_id = 100

batched_ids = [
    [200, 200, 200],
    [200, 200, padding_id],
]

填充詞元的ID(padding token ID)可以在 tokenizer.pad_token_id 中找到。讓我們使用它,並分別將兩句話單獨傳遞給模型以及一起進行批處理:

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>)

我們的批處理預測中的logits有些問題:第二行應該與第二個句子的logits相同,但我們得到了完全不同的值!

This is because the key feature of Transformer models is attention layers that contextualize each token. These will take into account the padding tokens since they attend to all of the tokens of a sequence. To get the same result when passing individual sentences of different lengths through the model or when passing a batch with the same sentences and padding applied, we need to tell those attention layers to ignore the padding tokens. This is done by using an attention mask.

這是因為 Transformer 模型的一個重要特點是注意力層(attention layer),它們對每個詞元進行上下文化處理(contextualize each token)。這些層會考慮填充詞元,因為它們會關注序列中的所有詞元(token)。為了在將不同長度的單個句子傳遞給模型時,或在傳遞應用了填充的相同句子的批次(batch)給模型時獲得相同的結果,我們需要告訴這些注意力層忽略填充詞元。這是透過使用注意力掩碼(attention mask)來實現的。

注意力掩碼(Attention masks)

注意力掩碼是與輸入ID張量形狀完全相同的張量,填充了0和1:1表示相應的詞元應該被關注,而0表示相應的詞元不應該被關注(即,它們應該被模型的注意力層忽略)。

讓我們用attention mask完成上一個示例:

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>)

現在我們得到了批次中第二個句子相同的邏輯值。(即[ 0.5803, -0.4125]print(model(torch.tensor(sequence2_ids)).logits)的輸出保持一致)。

注意第二個序列的最後一個值是一個填充ID,在注意力掩碼中是一個0值。

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)


sequence_1 = "I've been waiting for a HuggingFace course my whole life."
sequence_2 = "I hate this so much!"

tokens_1 = tokenizer.tokenize(sequence_1)
ids_1 = tokenizer.convert_tokens_to_ids(tokens_1)
input_ids_1 = torch.tensor([ids_1])
output_1 = model(input_ids_1)

tokens_2 = tokenizer.tokenize(sequence_2)
ids_2 = tokenizer.convert_tokens_to_ids(tokens_2)
input_ids_2 = torch.tensor([ids_2])
output_2 = model(input_ids_2)

print(input_ids_1)
print(input_ids_2)
print(output_1.logits)
print(output_2.logits)
tensor([[ 1045,  1005,  2310,  2042,  3403,  2005,  1037, 17662, 12172,  2607,
          2026,  2878,  2166,  1012]])
tensor([[1045, 5223, 2023, 2061, 2172,  999]])
tensor([[-2.7276,  2.8789]], grad_fn=<AddmmBackward0>)
tensor([[ 3.1931, -2.6685]], grad_fn=<AddmmBackward0>)
batched_ids = [
    ids_1,
    ids_2 + [tokenizer.pad_token_id] * (input_ids_1.shape[1] - input_ids_2.shape[1])
]

attention_mask = [
    [1] * input_ids_1.shape[1],
    [1] * input_ids_2.shape[1] + [0] * (input_ids_1.shape[1] - input_ids_2.shape[1]),
]

outputs = model(torch.tensor(batched_ids), attention_mask=torch.tensor(attention_mask))
print(outputs.logits)
tensor([[-2.7276,  2.8789],
        [ 3.1931, -2.6685]], grad_fn=<AddmmBackward0>)

更長的序列(Longer sequences)

對於 Transformer 模型,傳遞給模型的序列長度是有限制的。大多數模型處理的序列長度最多為 512 或 1024 個詞元,處理更長的序列時會崩潰。對此有兩種解決方案:

  1. 使用支援更長序列長度的模型。
  2. 截斷你的序列。

不同模型支援的序列長度各不相同,有些專門處理非常長的序列。Longformer 是一個例子,另一個是 LED。如果你正在處理需要非常長序列的任務,建議你檢視這些模型。

否則,我們建議你透過指定 max_sequence_length 引數來截斷你的序列:

sequence = sequence[:max_sequence_length]

將所有內容放在一起(Put it all together)

當你直接呼叫分詞器(tokenizer)處理句子時,你會得到可以直接傳遞給模型的輸入:

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."

model_inputs = tokenizer(sequence)
print(model_inputs)
{'input_ids': [101, 1045, 1005, 2310, 2042, 3403, 2005, 1037, 17662, 12172, 2607, 2026, 2878, 2166, 1012, 102], 'attention_mask': [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]}

正如我們將在下面的一些例子中看到的,這種方法非常強大。首先,它可以對單個序列進行分詞:

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)

It can pad according to several objectives:

# Will pad the sequences up to the maximum sequence length
model_inputs = tokenizer(sequences, padding="longest")

# Will pad the sequences up to the model max length
# (512 for BERT or DistilBERT)
model_inputs = tokenizer(sequences, padding="max_length")

# Will pad the sequences up to the specified 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!"]

# Will truncate the sequences that are longer than the model max length
# (512 for BERT or DistilBERT)
model_inputs = tokenizer(sequences, truncation=True)

# Will truncate the sequences that are longer than the specified max length
model_inputs = tokenizer(sequences, max_length=8, truncation=True)

tokenizer 物件可以處理到特定框架張量的轉換,然後可以直接傳遞給模型。例如,在以下程式碼示例中,我們讓 tokenizer 返回不同框架的張量 — "pt" 返回 PyTorch 張量,"tf" 返回 TensorFlow 張量,"np" 返回 NumPy 陣列:

sequences = ["I've been waiting for a HuggingFace course my whole life.", "So have I!"]

# Returns PyTorch tensors
model_inputs = tokenizer(sequences, padding=True, return_tensors="pt")

# Returns TensorFlow tensors
model_inputs = tokenizer(sequences, padding=True, return_tensors="tf")

# Returns NumPy arrays
model_inputs = tokenizer(sequences, padding=True, return_tensors="np")

特殊詞元(Special tokens)

如果我們檢視分詞器返回的輸入ID,會發現它們與之前的稍有不同:

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 ID,結尾也新增了一個。讓我們解碼上面的兩個ID序列,看看這是怎麼回事:

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."

分詞器在開頭新增了特殊詞 [CLS],在結尾新增了特殊詞 [SEP]。這是因為模型在預訓練時使用了這些詞元,所以為了在推理時獲得相同的結果,我們也需要新增它們。請注意,有些模型不會新增特殊詞,或者會新增不同的特殊詞;有些模型可能只在開頭或結尾新增這些特殊詞。不管怎樣,分詞器知道模型期望的特殊詞,並會為你處理這一切。

總結:從分詞器到模型(Wrapping up: From tokenizer to model)

現在我們已經瞭解了分詞器物件在處理文字時所用的所有單獨步驟,讓我們最後一次看看它如何處理多個序列(填充!)、非常長的序列(截斷!),以及使用其主要API處理多種型別的張量:

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)
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)

相關文章