🤗

Hugging Face NLP Course - 2. USING 🤗 TRANSFORMERS

に公開

概要

https://huggingface.co/learn/nlp-course/en/chapter2/1
の要点纏め。

Behind the pipeline

pipelineがやっていること

Preprocessing with a tokenizer

tokenizerがやっていること
このすべての前処理は、モデルが事前学習されたときとまったく同じ方法で行われる必要がある。

  • Splitting the input into words, subwords, or symbols (like punctuation) that are called tokens
  • Mapping each token to an integer
  • Adding additional inputs that may be useful to the model

トーカナイザーの読み込み

from transformers import AutoTokenizer

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

トーカナイズ
return_tensors="pt"
でpytorch用のテンソルを返す

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

Going through the model

モデルの作成(ロード)

from transformers import AutoModel

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

モデルにinputsを渡すと、hidden states(隠れ状態)またはfeatures(特徴)と呼ばれるものを返す。

hidden statesはそのままで使用されることもあるが、通常はheadと呼ばれる部分への入力に使用される。
headはタスクによって異なる。

A high-dimensional vector?

モデルの一般的な戻り値

  • Batch size: 与える文章の数

  • Sequence length: 与える文章のトークン数

  • Hidden size: 高次元のベクターサイズ

確認

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

outputsはnamedtuplesの様に振る舞うので、
outputs[0]
の様にインデックスアクセスも出来る。

以下はoutputs.last_hidden_state.shapeと同じ意味になる。

print(outputs[0].shape)
torch.Size([2, 16, 768])

Model heads: Making sense out of numbers

headはhidden statesとして取得した高次元ベクトルを指定の次元に変換する。
単数もしくは複数のlinear layerで構成される。

タスクに応じた様々なアーキテクチャ

*Model (retrieve the hidden states)
*ForCausalLM
*ForMaskedLM
*ForMultipleChoice
*ForQuestionAnswering
*ForSequenceClassification
*ForTokenClassification
and others 🤗

sequence classification head
を実装したモデルを使用する例。

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

logitsを表示

print(outputs.logits)
tensor([[4.0195e-02, 9.5980e-01],
        [9.9946e-01, 5.4418e-04]], grad_fn=<SoftmaxBackward>)

1つめのセンテンス
[0.0402, 0.9598]

2つめのセンテンス
[0.9995, 0.0005]

各インデックスとラベルの対応

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

First sentence: NEGATIVE: 0.0402, POSITIVE: 0.9598
Second sentence: NEGATIVE: 0.9995, POSITIVE: 0.0005

Models

Creating a Transformer

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,
  [...]
}

Different loading methods

訓練済みの重みでモデルをロード
実際は同等の AutoModel クラスを使うほうが好ましい。
(アーキテクチャが変わっても対応できるため)

BertConfigもチェックポイントの作者が設定したものになる。
モデルカードに詳細が記載してある。

from transformers import BertModel

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

重みがキャッシュされる位置
※HF_HOMEで変更可能

~/.cache/huggingface/transformers

BertModelに対応したチェックポイントの一覧
https://huggingface.co/models?other=bert

Saving methods

モデルのセーブ

model.save_pretrained("directory_on_my_computer")

2つのファイルが保存される

!ls directory_on_my_computer
config.json pytorch_model.bin

config.jsonの中身

!cat directory_on_my_computer/config.json
{
  "_name_or_path": "bert-base-cased",
  "architectures": [
    "BertModel"
  ],
  "attention_probs_dropout_prob": 0.1,
  "classifier_dropout": null,
  "gradient_checkpointing": false,
  "hidden_act": "gelu",
  "hidden_dropout_prob": 0.1,
  "hidden_size": 768,
  "initializer_range": 0.02,
  "intermediate_size": 3072,
  "layer_norm_eps": 1e-12,
  "max_position_embeddings": 512,
  "model_type": "bert",
  "num_attention_heads": 12,
  "num_hidden_layers": 12,
  "pad_token_id": 0,
  "position_embedding_type": "absolute",
  "torch_dtype": "float32",
  "transformers_version": "4.34.0",
  "type_vocab_size": 2,
  "use_cache": true,
  "vocab_size": 28996
}

Tokenizers

Word-based

単語単位で分割するパターン
空白で分割するイメージ

句読点のルールを加味したパターンも有る。
vocabularies(語彙)は独立したトークンの総数の事。
各単語にはIDが割り当てられ、0から始まり語彙の大きさまで割り当てられる。モデルはこれらのIDを使って各単語を識別する。

すべての単語をカバーすると英語だけでも500,000 wordsあり、膨大なID数になる。
また単数形複数形なども別のトークンとして管理される。

語彙にないトークンを“unknown” トークンとして扱う。
”[UNK]”や””として表現される。

Character-based

文字単位で分割するパターン

vocabularies(語彙)が少なくなる。
“unknown” トークンが少なくなる。
などのメリットがある。

一方で
文字自体は意味を持っていない。※中国語など例外はある。
トークン数が膨大になる。
などの問題がある。

Subword tokenization

上記2つのアプローチを併用した良いパターン。
単語を更に分割する

頻繁に使われる単語はより小さなサブワードに分割すべきではないが、希少な単語は意味のあるサブワードに分解すべきであるという原則に基づいている。

意味を維持しつつ語彙数を抑えることが可能。

And more!

その他のテクニックもある

  • Byte-level BPE, as used in GPT-2
  • WordPiece, as used in BERT
  • SentencePiece or Unigram, as used in several multilingual models

Loading and saving

トーカナイザーのロードの例

from transformers import BertTokenizer

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

AutoTokenizerでロードする例(推奨)

from transformers import AutoTokenizer

tokenizer = AutoTokenizer.from_pretrained("bert-base-cased")
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.save_pretrained("directory_on_my_computer")

Encoding

テキストを数字に変換することをエンコードという。
エンコーディングは、「トークン化」と「入力IDへの変換」という2段階のプロセスで行われる。

  • トークン化: テキストのスプリット
    このプロセスには複数のルールがあり、モデルが事前学習されたときと同じルールを使用するために、モデルの名前を使用してトークナイザをインスタンス化する必要がある。

  • 入力IDへの変換: トークンの数値化
    同様にモデルが事前学習されたときと同じルールを使用する必要がある。

以下でそれぞれを別に実行してみる。
(説明のためなので実際の運用では一気にやれば良い。)

Tokenization

Subword tokenizationの例

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', 'transform', '##er', 'network', 'is', 'simple']

From tokens to input IDs

ids = tokenizer.convert_tokens_to_ids(tokens)

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

Decoding

トークンIDをトークンに復元。
同じ単語の一部であったトークンをグループ化してくれていることに注意。

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

Padding the inputs

複数文章をバッチで入力する際、パディングトークンでトークン数を揃える必要がある。

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

[200, 200]
[200, 200, tokenizer.pad_token_id]
で、結果が変わることに注意する。

パディングトークンも推論に使用されるため。
attention maskを使用することで解決できる。

Attention masks

attention maskを指定することでアテンション層に無視してもらう。
[200, 200]
を渡した時と結果が一致する。

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

Longer sequences

モデルに渡せるトークン数には限界がある。
ほとんどのモデルは512または1024トークンまで。
それ以上のトークン数を渡すとクラッシュする。

この問題の解決策は

  • より長い配列長をサポートするモデルを使う。
    Longformer、LED等

  • シーケンスを切り捨てる。

sequence = sequence[:max_sequence_length]

Putting it all together

一般的な実行例

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)

複数の文をトーカナイズするパターン

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

model_inputs = tokenizer(sequences)

パディングのパターン

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

trancate(切り捨て)のパターン

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)

各フレームワークに対応した行列を得る

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

モデルによっては最初と最後に特別なトークンが付与されることがある。
事前学習時に付与して学習されている場合。

推論時や、ファインチューニング時も同様に処理する必要がある。
(指定のトーカナイザーを使っていれば問題ない)

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]

デコードしてみる

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

Wrapping up: From tokenizer to model

今までのまとめ

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)

Discussion