Closed4

今更ながら「Transformers」に入門する ⑤TUTORIALS: Generation with LLMs

kun432kun432

LLMを使用した生成

https://huggingface.co/docs/transformers/v4.48.0/en/llm_tutorial

ということでLLMのチュートリアル。

LLMは、事前学習済みの大規模なトランスフォーマーモデルであり、入力されたテキストを元に次の単語(トークン)を予測するようにトレーニングされている。1回に予測可能なトークンは1つであるため、一連の「文章」を生成するには、

  • モデルに初期入力を与える
  • モデルがトークンを予測・生成して、出力する
  • モデルは自分の出力を入力として再度予測・生成、これを繰り返す

というプロセスになる。この生成方法を「自己回帰的生成(autoregressive generation)」といい、Transformersライブラリではgenerate()メソッドを使うことでこれを実現できる。

事前準備

Colaboratory T4で。

!pip install -U transformers bitsandbytes
!pip freeze | egrep -i "transformers|bitsandbytes"
出力
bitsandbytes==0.45.0
sentence-transformers==3.3.1
transformers==4.48.0

テキストの生成

言語モデルには2種類あるらしい。ChatGPT調べ。

1. 因果言語モデル (Causal Language Modeling)

  • 学習方法
    • 入力シーケンスにおいて、前のトークンから次のトークンを予測する形で学習する。
    • 左から右への順序でトークンを処理する。
  • 特徴
    • 次のトークンを1つずつ予測する「自己回帰的 (autoregressive)」なモデル。
    • 出力時には前の出力結果を使いながらテキストを生成する。
  • 用途
    • テキスト生成が主な用途。 例: GPTシリーズ、CodeParrot、Copilotなど。
    • 小説の執筆支援、コード補完、会話型AIなどの「生成タスク」に適している。
    • 「The cat is」の次に「sitting」が来ることを予測し、その後に続く単語を順次生成。

2. マスク言語モデル (Masked Language Modeling)

  • 学習方法
    • 入力シーケンスの一部を「マスク」し、マスクされた部分を予測する形で学習する。
  • 特徴
    • 入力シーケンス全体を考慮して、マスクされたトークンを復元する「双方向的 (bidirectional)」なモデル。
    • 学習時に全体の文脈を使用できるため、精度の高い表現学習が可能。
  • 用途
    • 文章理解が主な用途。 例: BERT、RoBERTaなど。
    • 文書分類、名前付きエンティティ認識(NER)、文意解析などの「理解タスク」に適している。
    • 「The cat [MASK] on the mat」という入力に対し、マスク部分を「is」と予測。

ここでは前者の因果言語モデルについて扱う。

言語モデルは、テキストトークンのシーケンスを入力として受けると、次のトークンの確率分布を返す。


referred from https://huggingface.co/docs/transformers/v4.48.0/ja/llm_tutorial

LLMは、自己回帰的生成を用いて、確率分布から次のトークンを選択して、トークンの生成を繰り返す。次のトークンの選択方法はいろいろな方法がある。


referred from https://huggingface.co/docs/transformers/v4.48.0/ja/llm_tutorial and modified by kun432

このイテレーションは、

  • 停止条件に合致したら、終了シーケンス(EOSトークン)を返して終了。
  • そうでない場合は定義された最大長に達したら終了

まで繰り返される。このトークン生成のイテレーションと停止条件の設定は、GenerationConfigファイルで行われるらしい。

では実際にモデルを使ってみる。日本語モデルでやりたかったのだけど、同じコードで書けそうなものが見当たらなかったので。

https://huggingface.co/openlm-research/open_llama_7b

まずモデルをロード。device_mapload_in_4bitは以前のチュートリアルで試しているので割愛。

from transformers import AutoModelForCausalLM

model = AutoModelForCausalLM.from_pretrained(
    "openlm-research/open_llama_7b",
    device_map="auto",
    load_in_4bit=True
)

トークナイザをロードして、入力されたテキストを前処理。

from transformers import AutoTokenizer

tokenizer = AutoTokenizer.from_pretrained("openlm-research/open_llama_7b")
model_inputs = tokenizer(["A list of colors: red, blue"], return_tensors="pt").to("cuda")

model_inputsにはトークン化されたテキストとアテンションマスクが入っている。

model_inputs
出力
{
    'input_ids': tensor([[    1,   308,  1311,   287,  8286, 31871,  2729, 31844,  4842]], device='cuda:0'),
    'attention_mask': tensor([[1, 1, 1, 1, 1, 1, 1, 1, 1]], device='cuda:0')
}

generate()メソッドに入力を渡して、生成されたトークンを取得して、テキストに変換して出力

generated_ids = model.generate(**model_inputs)
tokenizer.batch_decode(generated_ids, skip_special_tokens=True)[0]
出力
A list of colors: red, blue, green, yellow, black, white, and brown.\nA list of colors: red,

バッチ入力。パディングを有効にすれば良い。

tokenizer.pad_token = tokenizer.eos_token  # ほとんどのLLMはデフォルトでパディングトークンなし
model_inputs = tokenizer(
    ["A list of colors: red, blue", "Portugal is"], return_tensors="pt", padding=True
).to("cuda")
generated_ids = model.generate(**model_inputs)
tokenizer.batch_decode(generated_ids, skip_special_tokens=True)
出力
[
    'A list of colors: red, blue, green, yellow, black, white, and brown.\nA list of colors: red,',
    'Portugal is a country in southwestern Europe. It is the westernmost country of mainland Europe, bordering']

よくある落とし穴

以下をベースに

from transformers import AutoModelForCausalLM, AutoTokenizer

tokenizer = AutoTokenizer.from_pretrained("openlm-research/open_llama_7b")
tokenizer.pad_token = tokenizer.eos_token  # Llamaはデフォルトでパディングトークンなし
model = AutoModelForCausalLM.from_pretrained(
    "openlm-research/open_llama_7b",
    device_map="auto",
    load_in_4bit=True
)

出力が長過ぎる・短すぎる

GenerationConfigファイルで設定がなければ、generateの出力はデフォルトだと最大20トークンまでとなっているらしい。max_new_tokensで最大出力トークンを指定するのが推奨されている。なお、LLM(デコーダーのみのモデル)は入力プロンプトも出力の一部として返すため、それも踏まえて設定する必要がある。

何も指定しない場合

model_inputs = tokenizer(["A sequence of numbers: 1, 2"], return_tensors="pt").to("cuda")

generated_ids = model.generate(**model_inputs)
tokenizer.batch_decode(generated_ids, skip_special_tokens=True)[0]
出力
A sequence of numbers: 1, 2, 3, 4, 5, 6, 7, 8, 

max_new_tokens=50を指定した場合

generated_ids = model.generate(**model_inputs, max_new_tokens=50)
tokenizer.batch_decode(generated_ids, skip_special_tokens=True)[0]
出力
A sequence of numbers: 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16,

誤った生成モード

GenerationConfigファイルで設定がない限り、デフォルトではgenerateは最も可能性が高いトークンを返す「貪欲デコーディング」モードとなっている。ユースケースによってはこれがそぐわない場合がある。

使用できるモードは以下に記載されている。

https://huggingface.co/blog/how-to-generate

ざっくり要約すると以下のモードがあるみたい。

モード 説明 メリット デメリット ユースケース
Greedy Search 各タイムステップで最も確率の高い単語を逐次選択。 - 実装が簡単
- 計算負荷が低い
- 一貫性のある出力を生成
- 高確率の単語を見逃す可能性あり
- 繰り返しが多く、単調な結果になることがある
- 定型文生成
- 計算資源が限られている場合
Beam Search 複数の候補を追跡し、全体の確率が最大となるシーケンスを選択。 - 一貫性と文法性の高い結果
- Greedy Searchよりも高品質な結果
- 繰り返しが多い
- 計算負荷が高い
- ユーザー指定の出力長に最適化が必要
- 翻訳
- 要約
- 一貫性が求められるスクリプト生成
Sampling 次の単語を確率分布に基づいてランダムに選択。 - 多様性のある生成
- 創造性を反映しやすい
- 確率分布次第で不自然なテキストが生成される可能性あり
- 一貫性が低い場合がある
- 小説や詩の生成
- 会話生成
- 創造性が求められるタスク
Top-K Sampling 確率上位K個の単語に限定してサンプリングを行う。 - 不適切な単語を排除可能
- 確率が高い単語からのみ選択できるため品質が安定
- 固定されたK値が柔軟性を欠く場合あり
- 分布がフラットな場合は選択肢が狭まりすぎる可能性あり
- バランス重視のストーリー生成
- トピック固有の生成
Top-p Sampling 累積確率が閾値pを超える最小セットから単語を選択。 - 確率分布に応じて動的に候補を調整可能
- 多様性と一貫性のバランスが良い
- 設定次第で低確率の単語が選ばれるリスクあり
- トレードオフの微調整が必要
- ストーリー生成
- 高品質なオープンエンド生成
- 解釈が複数あるシナリオ

デフォルトのgreedy_search。

# 再現性のためのシードの設定 -- 完全な再現性が必要な場合に指定する(通常不要)
from transformers import set_seed
set_seed(42)

model_inputs = tokenizer(["I am a cat."], return_tensors="pt").to("cuda")

generated_ids = model.generate(**model_inputs)
tokenizer.batch_decode(generated_ids, skip_special_tokens=True)[0]
出力
I am a cat. I am a cat. I am a cat. I am a cat. I am a cat.

単調な文が連続して生成されている。

Samplingを有効にする。

generated_ids = model.generate(**model_inputs, do_sample=True)
tokenizer.batch_decode(generated_ids, skip_special_tokens=True)[0]
出力
I am a cat. I was not born, so I do not have a mother. I live in a box. I

より自然な文章になっているのがわかる。

Beam Searchも試してみた。num_beamsで候補の数を指定するが、数が多いほど計算量が上がる。Beam Searchだと繰り返しが起きやすいらしいので、no_repeat_ngram_sizeで同じ単語が繰り返されるのを抑制している。

generated_ids = model.generate(**model_inputs, num_beams=5, no_repeat_ngram_size=2, early_stopping=True)
tokenizer.batch_decode(generated_ids, skip_special_tokens=True)[0]
出力
I am a cat. I was born in 1 year of 2008.\nI was adopted by my

なお、ブログ記事の中では以下のような結論になっていた。

  1. デコード手法の特性と利点
    • Top-pTop-K Sampling は、従来の Greedy SearchBeam Search に比べ、オープンエンドの生成でより流暢なテキストを生成できる。
    • 繰り返し生成の問題はデコード手法だけでなく、モデル自体のトレーニング方法にも起因している。
  2. 繰り返し生成の課題
    • Greedy Search や Beam Search は特に繰り返し生成に悩まされやすい。
    • Sampling 系手法も調整次第では同じ問題が発生するため、タスクに応じた適切な調整が必要。
  3. ユースケースに応じた手法選択
    • 各デコード手法には一長一短があり、特定のタスクやユースケースに応じて最適な方法を選ぶ必要がある。
    • 例: 翻訳や要約では Beam Search、創造性が求められるタスクでは Top-p や Top-K。
  4. 研究と実践の進展
    • オープンエンド生成は急速に進化している分野であり、「一律の最適解」は存在しない。
    • 実際のユースケースに合わせて手法を試し、調整することが重要。
  5. Transformers ライブラリの利便性
    • Hugging Face の Transformers ライブラリを使えば、さまざまなデコード手法を簡単に試すことが可能。

誤ったパディングの方向

複数の入力がバッチで与えられ、かつ、長さが異なる場合はパディングが必要になる。LLMは通常「デコーダーのみ」のアーキテクチャになっていて、モデルが入力されたテキストシーケンスを受け取って、その続きを生成するが、パディングから続けるようには学習されていない。つまりパディングは左、入力テキストの先頭に追加される必要がある。

以下は理解のためのイメージであって実際には異なる。以下のようなバッチ入力があったとする。

[
    "1, 2, 3",
    "A, B, C, D, E"
]

"1, 2, 3"のほうが短いので、"A, B, C, D, E"にあわせてパディングが追加されるのだが(

[
    "1, 2, 3<PAD>",
    "A, B, C, D, E"
]

となると、"1, 2, 3<PAD>"の続きから生成するということになってしまい、生成結果がおかしくなってしまう。なので、デコーダーのみのモデルでは、パディングは左に追加する必要があるということ。

[
    "<PAD>1, 2, 3",
    "A, B, C, D, E"
]

で、トークナイザーはデフォルトだと右パディングする、とあるのだが、自分が試した限りは左にパディングされていた。

model_inputs = tokenizer(
    ["1, 2, 3", "A, B, C, D, E"], padding=True, return_tensors="pt"
).to("cuda")
model_inputs
出力
{
    'input_ids': tensor([[    2,     1, 31822, 31853, 31844, 31822, 31855, 31844, 31822, 31878],
        [    1,   308, 31844,   341, 31844,   314, 31844,   353, 31844,   377]],
       device='cuda:0'),
    'attention_mask': tensor([[0, 1, 1, 1, 1, 1, 1, 1, 1, 1],
        [1, 1, 1, 1, 1, 1, 1, 1, 1, 1]], device='cuda:0')
}

attention_maskの最初の要素の先頭に0がついているので、左パディングが行われている。

デコードしてみる。上の方でtokenizer.pad_token = tokenizer.eos_tokenを指定していたので、</s>がパディングトークン。

decoded_sequences = tokenizer.batch_decode(
    model_inputs["input_ids"], skip_special_tokens=False
)

for i, sequence in enumerate(decoded_sequences):
    print(f"{i+1}: {sequence}")
出力
1: </s><s> 1, 2, 3
2: <s> A, B, C, D, E

なので、何も問題なく実行できる。

generated_ids = model.generate(**model_inputs)
tokenizer.batch_decode(generated_ids, skip_special_tokens=True)[0]
出力
1, 2, 3, 4, 5, 6, 7, 8, 9,

tokenizer.padding_sideを見てみるとleftになっているので、以前はそうではなかったのかもしれない。

tokenizer.padding_side
出力
left

あえて、padding_side="right"を指定してみる。

model_inputs = tokenizer(
    ["1, 2, 3", "A, B, C, D, E"], padding=True, padding_side="right", return_tensors="pt"
).to("cuda")
generated_ids = model.generate(**model_inputs)
tokenizer.batch_decode(generated_ids, skip_special_tokens=True)[0]
出力
1, 2, 3, 4, 5, 6, 7, 8, 9,

普通に出力されてるなー。今となってはもう色々変更されたのかもしれない。

誤ったプロンプト形式

モデルの中には特定の入力フォーマットを期待しているものがあり、これにそぐわない入力の場合、動かないまではいかなくても、期待した応答が得られない≒性能が劣化する場合がある。

以下のモデルを使う。このモデルは、一般的なチャットプロンプト形式の入力を期待している。

https://huggingface.co/HuggingFaceH4/zephyr-7b-alpha

モデルをロード

tokenizer = AutoTokenizer.from_pretrained("HuggingFaceH4/zephyr-7b-alpha")
model = AutoModelForCausalLM.from_pretrained(
    "HuggingFaceH4/zephyr-7b-alpha", device_map="auto", load_in_4bit=True
)

チャットプロンプト形式ではない普通のテキストを入力として与えてみる。

prompt = """How many helicopters can a human eat in one sitting? Reply as a thug."""
model_inputs = tokenizer([prompt], return_tensors="pt").to("cuda")
input_length = model_inputs.input_ids.shape[1]
generated_ids = model.generate(**model_inputs, max_new_tokens=20)
print(tokenizer.batch_decode(generated_ids[:, input_length:], skip_special_tokens=True)[0])
出力
I'm not a thug, but i can tell you that a human cannot eat  # 「私は暴漢ではありませんが、人間は食べられないとあなたに言えます。」

チャットプロンプト形式で入力を与えてみる。

set_seed(0)
messages = [
    {
        "role": "system",
        "content": "You are a friendly chatbot who always responds in the style of a thug",
    },
    {"role": "user", "content": "How many helicopters can a human eat in one sitting?"},
]
model_inputs = tokenizer.apply_chat_template(messages, add_generation_prompt=True, return_tensors="pt").to("cuda")
input_length = model_inputs.shape[1]
generated_ids = model.generate(model_inputs, do_sample=True, max_new_tokens=20)
print(tokenizer.batch_decode(generated_ids[:, input_length:], skip_special_tokens=True)[0])
出力
None, you thug. How bout you try to focus on more useful questions?  # ゼロだよ、なめんな。不良ならもっと役に立つ質問しろや!

チャットプロンプトは実際にはモデルが期待する形にapply_chat_templateで変換される。

prompt = tokenizer.apply_chat_template(messages, tokenize=False)
print(prompt)
出力
<|system|>
You are a friendly chatbot who always responds in the style of a thug</s>
<|user|>
How many helicopters can a human eat in one sitting?</s>
kun432kun432

うすうすわかってはいたんだけど、日本語のドキュメントはいろいろ足りなかったり訳がおかしかったりするので、英語を見た方が良いな。

このスクラップは5ヶ月前にクローズされました