ChatGPT登場以降、自然言語処理に興味を持ち始めた。2023年ころにTransformersライブラリを使ったサンプルコードをよくわからずに写経していた。頻度高く触っているたけではないが、最近、やっと概観がつかめてきた(気がする)ので、自分の理解を整理してみる。(と言いつつも書いている途中に知らないことが沢山出てきたので、書くことの大事さを改めて感じた)
1. 背景知識
(1)Transformersとは、LLMをPython等で扱うためのライブラリであり、HuggingFaceから提供されている。
(2)HuggingFaceは、Transformers以外にも、Datasetsライブラリ、Tokenizersライブラリを提供している。
(3)HuggingFaceは、LLMやデータセットを無料で共有できるプラットフォーム Hugging Face Hub も提供している。一言でいえば、GitHubのLLM/データセット版。
- Microsoft
- プラットフォーム:GitHub(ソースコード共有) ・・・(3)
- ライブラリ群
- Azure SDK for Python
- IronPython
- LLM
- phi2(※1)
- phi3
- Hugging Face
- プラットフォーム:Hugging Face Hub(モデル・データセット共有)・・・(3)
- microsoft/phi-2 ・・・※1
- microsoft/phi-3
- meta/Llama2
- ライブラリ群
- Datasets ライブラリ ・・・(2)
- Tokenizers ライブラリ ・・・(2)
- Transformers ライブラリ ・・・(1)
- pipline モジュール
- AutoClasses モジュール ・・・※2(A)
- AutoTokenizer ・・・(A1)
- AutoModel ・・・(A2)
- AutoModelForSequenceClassification ・・・例1
- AutoModelForSeq2SeqLM ・・・例2
- AutoModelForCausalLM ・・・例3
※1)Microsoftは自社開発したLLM(phi2、phi3)を、Hugging Face Hubに公開している。
※2)モジュールという表現は不適切かもしれないが、TransformersではpiplineとAutoClassesという2つ実装手段が用意されており、そのうち本記事ではAutoClassesの使い方を対象としている、ということを示したかった。
2. AutoClasses の使い方
(A)AutoClasses には、(A1)トークナイザに関するAutoTokenizerクラスと、(A2)モデルに関するAutoModelクラスがある。
(A1)AutoTokenizerクラスは、自然言語処理タスクの種類(用途)によらず、共通して使用できる。これは、AutoTokenizerクラスが、内部的に、タスクやモデルに適したトークナイザを自動(Auto)で選択してくれるためである。
(A2)AutoModelクラスは、文章分類や要約といった自然言語処理タスクの種類(用途)に対応するサブクラスを持っている。実装としては、AutoModelというクラス名ではなく、_BaseAutoModelClassというクラス名である。_から始まるため、_BaseAutoModelClassクラスは、あくまでも内部用としてfrom_pretrained等のメソッドを基底する役割であり、我々ユーザから直接利用されることは期待されていない。我々ユーザは、解きたいタスクに応じて、適切なサブクラス(派生クラス、子クラス)を利用する。
- AutoTokenizer ・・・(A1)
- _BaseAutoModelClass ・・・(A2)
- AutoModelForSequenceClassification(文章分類タスク) ・・・例1
- AutoModelForTokenClassification(固有表現抽出タスク)
- AutoModelForQuestionAnswering(質問応答タスク)
- AutoModelForSeq2SeqLM(翻訳/要約タスク) ・・・例2
- AutoModelForCausalLM(言語モデリングタスク) ・・・例3
[疑問] AutoTokenizerにみたいに自動でモデルを選択してくれる AutoModel がなぜ提供されていないのか?
→ 本当の理由はわからない。ご存じの人がいれば教えてほしい。
→ 自分なりに納得した理解は、トークナイザの役割(機能)はタスクに依らず一貫しているが、モデルの役割(機能)はタスクに依存して異なる、という実態(現実世界)をクラス構造にそのまま転写した結果だということ。つまり、1つのトークナイザが開発できれば、それを使って文章分類モデルや要約モデルなどを開発できる、という前後・主従・階層・1:Nの関係性をむしろ正確に表現している、してくださっているのではないか。
また、1:Nという数に着目すれば、トークナイザの数はそこまで多くないが、モデルの数は膨大かつ近年急速に増加し続けており、数が少ないものには便利なラッパーを提供できるが、数が膨大なものには提供しきれない、という極めて感覚的な理解もできなくはない(本質ではないと思うが自分を納得させるための材料の足しにはなっている)。
こんな細かい理解よりも手を動かして実践・経験から学ぶことを優先しよう(自戒)
例1)文章分類タスク
- 1.文章分類モデルは、https://huggingface.co/distilbert/distilbert-base-uncased-finetuned-sst-2-english を用いる。
- 2.トークナイザは、AutoTokenizerクラスを用いてインスタンス化する。
- 3.モデルは、AutoModelForSequenceClassificationクラスを用いてインスタンス化する。
from transformers import AutoTokenizer, AutoModelForSequenceClassification
import torch
import torch.nn.functional as F
### 1. HuggingFaceHubでのモデル名(英語の感情分析モデル)
model_name = "distilbert/distilbert-base-uncased-finetuned-sst-2-english"
### 2. トークナイザをインスタンス化(HuggingFaceHubからダウンロード)
tokenizer = AutoTokenizer.from_pretrained(model_name)
### 3. モデルをインスタンス化(HuggingFaceHubからダウンロード)
model = AutoModelForSequenceClassification.from_pretrained(model_name)
### 4. 入力を作成
text = "I really love using Transformers library!"
inputs = tokenizer(text, return_tensors="pt")
### 5. 推論
with torch.no_grad():
outputs = model(**inputs)
logits = outputs.logits
probs = F.softmax(logits, dim=1)
### 6. 結果の取得・表示
pred_label = torch.argmax(probs, dim=1).item()
label_names = model.config.id2label
print("【分類結果】")
print(f"Label: {label_names[pred_label]}, Probabilities: {probs.tolist()}")
↓ 出力結果
【分類結果】
Label: POSITIVE, Probabilities: [[0.0011868128785863519, 0.9988131523132324]]
肯定的(POSIIVE)な文章である、という分類ラベル(Label)が得られた。その分類の信頼度(Probabilities)は0.9988...と高いことがわかった。
例2)要約タスク
- 1.要約モデルは、https://huggingface.co/facebook/bart-large-cnn を用いる。
- 2.トークナイザは、AutoTokenizerクラスを用いてインスタンス化する。
- 3.モデルは、AutoModelForSeq2SeqLMクラスを用いてインスタンス化する。
from transformers import AutoTokenizer, AutoModelForSeq2SeqLM
import torch
### 1. モデル名
model_name = "facebook/bart-large-cnn"
### 2. トークナイザ
tokenizer = AutoTokenizer.from_pretrained(model_name)
### 3. モデル
model = AutoModelForSeq2SeqLM.from_pretrained(model_name)
### 4. 入力の作成
# 4.1. 要約対象(英語記事)
article = (
"The manufacturing sector is adopting artificial intelligence to improve production efficiency. "
"By analyzing data from sensors and machines, AI systems can predict maintenance needs and optimize scheduling. "
"This transformation is expected to reduce downtime and increase overall productivity across factories worldwide."
)
# 4.2. 要約対象のトークナイズ
inputs = tokenizer(article, return_tensors="pt", truncation=True, max_length=512)
### 5. 推論
with torch.no_grad():
summary_tokens = model.generate(
**inputs,
max_new_tokens=20, # 要約の最大長
num_beams=5, # ビーム探索
)
### 6. 結果の取得・表示
summary_text = tokenizer.decode(summary_tokens[0], skip_special_tokens=True)
print("【要約結果】")
print(summary_text)
↓ 結果
【要約結果】
The manufacturing sector is adopting artificial intelligence to improve production efficiency. By analyzing data from sensors and
入力文のままなので要約の質はダメダメだが、そこは重要でない。
重要なのは、タスクの種類によらず、1.モデル名の指定 → 2.トークナイザのロード → 3.モデルのロード → 4.入力の準備 → 5.推論 → 6.結果の取得 という6つのSTEPが共通であること。
これは、今回のメインターゲットであるテキスト生成タスクでも同様である。
例3)言語モデリング(テキスト生成)タスク
- 1.言語モデルは、https://huggingface.co/openai-community/gpt2-xl を用いる
- 2.トークナイザは、他のタスクと同様に、AutoTokenizerクラスを用いてインスタンス化する
-
gpt2-xl
に適したトークナイザが自動的に選択され、戻り値として GPT2TokenizerFastクラスのインスタンスが得られる
-
- 3.モデルは、テキスト生成(言語モデリング)タスクに適した AutoModelForCausalLM クラスを用いてインスタンス化する
- モデル名
gpt2-xl
に適したモデルが自動的に選択され、戻り値として GPT2LMHeadModelクラスのインスタンスが得られる
- モデル名
import torch
from torch import Tensor
from jaxtyping import Float # torch用typingモジュール
from transformers import AutoModelForCausalLM, AutoTokenizer
from transformers.models.gpt2.modeling_gpt2 import GPT2LMHeadModel
from transformers.models.gpt2.tokenization_gpt2_fast import GPT2TokenizerFast
from transformers.tokenization_utils_base import BatchEncoding
### 1. モデル名
model_name: str = "gpt2-xl"
### 2. トークナイザのロード
tokenizer: GPT2TokenizerFast = AutoTokenizer.from_pretrained(model_name)
### 3. モデルのロード
model: GPT2LMHeadModel = AutoModelForCausalLM.from_pretrained(model_name).to('cuda')
### 4. 入力の準備
# 4.1. 単語列
text: str = "The capital of Japan is"
# 4.2. エンコード
inputs: BatchEncoding = tokenizer(text, return_tensors='pt').to(model.device)
### 5. 推論
outputs: Float[Tensor, '1 tokens'] = model.generate(
inputs = inputs['input_ids'],
attention_mask = inputs['attention_mask'],
pad_token_id = tokenizer.eos_token_id, # 50256 = '<|endoftext|>'
max_new_tokens = 20,
do_sample = True,
temperature = 0.7,
top_p = 0.5,
) # -> torch.Size([1, tokens])
### 6. 結果の取得・表示
output: Float[Tensor, 'tokens'] = outputs[0]
output_text: str = tokenizer.decode(output)
print(output_text)
↓ 結果
The capital of Japan is Tokyo, which is a very busy city. There are many places to go, and there are many
Tokyoと正解できているが、後半は同じ文章をリピートしている。基盤モデルの代わりに、指示追従モデル(Instruction Fine-tuning済モデル)使用することで、より自然な対話(チャット)ができると思われる。
[補足]
Q.) なぜCausalModelという名前なのか? "Causal"とは?
A.) 言語モデルは、すでに出力されたトークン列(過去)を入力として、次のトークン(未来)を生成する自己回帰モデル(auto-regressive model)であり、この「時間的・位置的な因果関係(causality)」を示す単語としてCausalが使用されている。つまり、言語モデルや自己回帰モデルといった概念を包含する上位概念としてCausalModelと名付けられている。
3. Tokenizer の使い方
3.1. エンコード
エンコードとは、単語列(str)をトークンに分割(list[str])してID化することで、トークンID列(list[int])を生成すること。
例1)GPT2 × 英語
単語列: str = 'The capital of Japan is'
# ↓
# ↓ 処理1.トークン分割(トークナイズ)
# ↓
トークン列: list[str] = ['The', 'capital', 'of', 'Japan', 'is']
# ↓
# ↓ 処理2.トークンID化(エンコード)
# ↓
トークンID列: list[int] = [ 464, 3139, 286, 2869, 318]
→5単語が5トークンに分割された。1単語が1トークンに対応する。
例2)GPT2 × 日本語
単語列: str = "日本の首都は"
# ↓
トークン列: list[str] = ['æĹ', '¥', 'æľ', '¬', 'ãģ®é', '¦', 'ĸ', 'éĥ', '½', 'ãģ¯']
# ↓
トークンID列: list[int] = [33768, 98, 17312, 105, 33426, 99, 244, 32849, 121, 31676]
→6文字が10トークンに分割された。1文字が1~2トークンに対応する。
例3)日本語対応モデル × 日本語
単語列: str = "日本の首都は"
# ↓
トークン列: list[str] = ['日本', 'の', '首都', 'は']
# ↓
トークンID列: list[int] = [12500, 464, 14456, 465]
→6文字が4トークンに分割された。"日"と"本"の2トークンではなく、"日本"の1トークンになっている。文字単位ではなく単語単位でトークン化されている(厳密にはその中間のサブワード単位)。
実装例
- tokenize(str)メソッド -> list[str]
from transformers import AutoTokenizer
from transformers.models.bert_japanese.tokenization_bert_japanese import BertJapaneseTokenizer
model_path: str = "tohoku-nlp/bert-base-japanese-v3"
tokenizer: BertJapaneseTokenizer = AutoTokenizer.from_pretrained(model_path)
# トークナイズ(処理1)
tokens: list[str] = tokenizer.tokenize("日本の首都は")
print(tokens) # --> ['日本', 'の', '首都', 'は']
# エンコード(処理1 + 処理2)
token_ids: list[int] = tokenizer.encode("日本の首都は")
print(token_ids) # --> [2, 12500, 464, 14456, 465, 3]
→エンコードされると、先頭に開始を意味する特殊トークン(ID=2)と、末尾に終了を意味する特殊トークン(ID=3)が追加される。
3.2. デコード
エンコードの逆をすること。トークンIDをトークン列(さらには単語列)に戻すこと。
3.2.1. decode(): トークンID列 →→ 単語列
text: str = "The capital of Japan is"
### 1. エンコード(文字列 → トークンID列)
token_ids: list[str] = tokenizer.encode(text)
print(token_ids)
# --> [464, 3139, 286, 2869, 318]
### 2. デコード
decoded_text: str = tokenizer.decode(token_ids=token_ids)
print(decoded_text)
# --> The capital of Japan is
# 元のtextの文字列と完全に一致している。
3.2.2. convert_ids_to_tokens(): トークンID列→トークン列→単語列
from transformers import AutoModelForCausalLM, AutoTokenizer
from transformers.tokenization_utils_base import PreTrainedTokenizerBase
from transformers.modeling_utils import PreTrainedModel
tokenizer: GPT2TokenizerFast = AutoTokenizer.from_pretrained(model_name)
model: GPT2LMHeadModel = AutoModelForCausalLM.from_pretrained(model_name).to('cuda')
text: str = "The capital of Japan is"
### 1. エンコード(文字列 → トークンID列)
token_ids: list[str] = tokenizer.encode(text)
print(token_ids)
# --> [464, 3139, 286, 2869, 318]
### 2. デコード1(トークンID列 → トークン列)
decoded_tokens: list[str] = tokenizer.convert_ids_to_tokens(token_ids)
print(decoded_tokens)
# --> ['The', 'Ġcapital', 'Ġof', 'ĠJapan', 'Ġis']
# "Ġ" は半角スペースを表すトークン
### 3. デコード2 (トークン列 → 文字列)
decoded_text: str = ""
for token in decoded_tokens:
decoded_text += token.replace("Ġ", " ") # スペースに変換して繋げる
print(repr(decoded_text))
# --> 'The capital of Japan is'
# 元のtextと完全に一致していることがわかる。
3.2.3. call(): BatchEncodingインスタンスを生成
from transformers import AutoModelForCausalLM, AutoTokenizer
from transformers.models.gpt2.modeling_gpt2 import GPT2LMHeadModel
from transformers.models.gpt2.tokenization_gpt2_fast import GPT2TokenizerFast
model_name = "gpt2-xl"
tokenizer: GPT2TokenizerFast = AutoTokenizer.from_pretrained(model_name) # 自動的に適切なTokenizerを割当してくれる。
model: GPT2LMHeadModel = AutoModelForCausalLM.from_pretrained(model_name).to('cuda') # 自動的に適切なModelを割当してくれる。
text = "The capital of Japan is"
### 1. エンコード
# 基本の実行方法(インスタンスをメソッドかのように使える)
inputs: BatchEncoding = tokenizer(text=text, return_tensors='pt').to(model.device) # 親クラス(PreTrainedTokenizerBaseの__call__()が実行される。
# 以下のように__call__を明示的に呼んでも同じ
inputs: BatchEncoding = tokenizer.__call__(text, return_tensors='pt').to(model.device)
# 結局はencode_plus()が実行されるので直接呼び出しても結果は同じ(非推奨)
inputs: BatchEncoding = tokenizer.encode_plus(text, return_tensors='pt').to(model.device)
### 2. 中身の確認
input_ids: Float[Tensor, '1 tokens'] = inputs['input_ids']
attention_mask: Float[Tensor, '1 tokens'] = inputs['attention_mask']
print(input_ids) # -> [[ 464, 3139, 286, 2869, 318]]
print(input_ids.shape) # -> [1, 5]
print(input_ids[0]) # -> [ 464, 3139, 286, 2869, 318]
print(input_ids[0].shape) # -> [5]
### 3. デコード
for i in range(input_ids.shape[1]):
token_id: int = input_ids[0][i].item()
word: str = tokenizer.decode([token_id])
print(f"token_id: {token_id} => {repr(word)}")
↓ 結果(トークンID => 対応する単語)
token_id: 464 => 'The'
token_id: 3139 => ' capital'
token_id: 286 => ' of'
token_id: 2869 => ' Japan'
token_id: 318 => ' is'
ひとまず、ここまで。