はじめに
去年買って積本になっていた機械学習エンジニアのためのTransformersの5章テキスト生成で書かれている内容を、少し前にサイバーエージェントが公開した日本語LLM OpenCALM を使ってやってみたいと思います。
開発環境
Google colabのフリー版でOpenCALMのモデルはOpenCALM-1Bを使用します。フリー版ではこれ以上大きいモデルを使うとメモリ不足で落ちるようなので、1Bを選択しました。Colab有償版Pro/Pro+を使っているのであれば、3B, 7Bのモデルを使ってみてもいいと思います。。
準備
まずはTransformersをインストール
!pip install transformers
使用するライブラリをインポート
import pandas as pd
import torch
from transformers import AutoTokenizer, AutoModelForCausalLM
トークナイザと事前学習モデルのロード
device = "cuda" if torch.cuda.is_available() else "cpu"
model_name = "cyberagent/open-calm-1b"
tokenizer = AutoTokenizer.from_pretrained(model_name)
model = AutoModelForCausalLM.from_pretrained(model_name).to(device)
貪欲法を使っての文章生成
「貪欲法」(greedy decoding)とは、文章生成AIで用いられる一つのデコード戦略です。具体的には、言語モデルが各時点で最も確率が高いと予測したトークン(単語や単語の一部)を選択する方法を指します。
具体的なステップは以下のようになります:
-
モデルはまず文の最初のトークンを予測します。
-
次に、モデルは現在までに生成されたトークンを条件に、次のトークンの確率分布を計算します。
-
この確率分布から、最も確率が高いトークンを選択します。
-
上記のステップを設定された最大トークン数に達するまで繰り返すか、文章が終了する特殊なトークンが選択されるまで繰り返します。
貪欲法の主な利点はその単純さと効率性です。モデルは各ステップで一つの最適な選択を行うだけなので、計算量が大幅に削減されます。しかし、各ステップで最も確率が高いトークンを選んでも、全体として最も意味のある、または自然な文章が生成されるとは限らないという欠点もあります。
まずは貪欲法でどのような文章が生成されるのか、トークンの候補も踏まえて見てみましょう。入力テキストには川端康成の雪国の冒頭「国境の長いトンネルを抜けると」を使っています。
ソースコード
input_txt = "国境の長いトンネルを抜けると"
input_ids = tokenizer(input_txt, return_tensors="pt")["input_ids"].to(device)
iterations = []
n_steps = 10
choices_per_step = 5
with torch.no_grad():
for _ in range(n_steps):
iteration = dict()
iteration["Input"] = tokenizer.decode(input_ids[0])
output = model(input_ids=input_ids)
next_token_logits = output.logits[0, -1, :]
next_token_probs = torch.softmax(next_token_logits, dim=-1)
sorted_ids = torch.argsort(next_token_probs, dim=-1, descending=True)
for choice_idx in range(choices_per_step):
token_id = sorted_ids[choice_idx]
token_prob = next_token_probs[token_id].cpu().numpy()
token_choice = (
f"{tokenizer.decode(token_id)} ({100 * token_prob:.2f}%)"
)
iteration[f"Choice {choice_idx+1}"] = token_choice
input_ids = torch.cat([input_ids, sorted_ids[None, 0, None]], dim=-1)
iterations.append(iteration)
pd.DataFrame(iterations)
選択候補の中から一番確率が高いものが選択されていることが分かります。
単純な文章生成のコードは以下になります。
input_ids = tokenizer(input_txt, return_tensors="pt")["input_ids"].to(device)
output = model.generate(input_ids, max_new_tokens=n_steps, do_sample=False, pad_token_id=tokenizer.pad_token_id)
print(tokenizer.decode(output[0]))
出力結果
国境の長いトンネルを抜けると、そこはもうアメリカ。
アメリカといえば、自由
ここまでは、とても自然な文章ですね。それでは、max_lengthの値を大きくして、もう少し長い文章を生成してみましょう。
ソースコード
max_length = 128
input_ids = tokenizer(input_txt, return_tensors="pt")["input_ids"].to(device)
output_greedy = model.generate(input_ids, max_length=max_length,
do_sample=False, pad_token_id=tokenizer.pad_token_id)
print(tokenizer.decode(output_greedy[0]))
出力結果
国境の長いトンネルを抜けると、そこはもうアメリカ。
アメリカといえば、自由の国。
自由の国といえば、アメリカ。
アメリカといえば、自由の国。
アメリカといえば、自由の国。
アメリカといえば、自由の国。
アメリカといえば、自由の国。
アメリカといえば、自由の国。
アメリカといえば、自由の国。
アメリカといえば、自由の国。
アメリカといえば、自由の国。
アメリカといえば、自由の国。
アメリカといえば、自由の国。
アメリカといえば、自由の国。
アメリカといえば、自由の国。
アメリカといえば、自由の国。
アメリカといえば、自由の国。
アメリカといえば、自由の国
同じ文が繰り返し生成されています。貪欲法では次の一番確率の高いトークンを選択するので、このような結果になることはよくあります。
その問題に対しては、no_repeat_ngram_sizeを設定することで、指定の数以上のN-gramの組み合わせが繰り返しの文章に出てくるのを防ぐことが可能となります。
※N-gramとは任意の文字数で文章を分割する手法のことです。例文をN=2のN-gramで分割すると以下のようになります。
例文:今日は良い天気ですね
N=2のN-gramで分割した結果
"今日 は"
"は 良い"
"良い 天気"
"天気 です"
"です ね"
ソースコード
output_greedy_n = model.generate(input_ids, max_length=max_length, do_sample=False,
no_repeat_ngram_size=2, pad_token_id=tokenizer.pad_token_id)
print(tokenizer.decode(output_greedy_n[0]))
出力結果
国境の長いトンネルを抜けると、そこはもうアメリカ。
アメリカといえば、自由の国。自由といえばアメリカ、というくらい、アメリカには自由があります。
そして、その自由は、私たち日本人には想像もつかないほど、厳しいものです。アメリカという国に
は、日本のような「法治国家」という概念は存在しません。法とは、法律のこと。法律とは、「国家が
国民に対して、一定の行為をするように命令する」という命令書のことです。つまり、私たちが普段使
っている「法律」は、「法律家」が作ったものなのです。しかし、アメリカでは、法学者や弁護士、裁
判官、検察官、弁護士など、さまざまな職業の人が
文章としては、それなりに自然な文章が生成されました。(アメリカが無法国家になってしましましたが…)
ビームサーチ
貪欲方が各ステップで一番確率が高いものだけを選択していくのに対し、ビームサーチでは各ステップでビーム幅と呼ばれる複数の候補の中から最も確率の高いトークンを選択します。
具体的なステップは以下のようになります:
-
モデルはまず文の最初のトークンを予測します。
-
モデルは次に、すべての可能な次のトークンについて確率を計算します。
-
ここでビーム幅が重要な役割を果たします。ビーム幅がkであれば、モデルは最も確率が高いk個のトークンを保持します。これらは次のステップの「候補」または「ビーム」になります。
-
それぞれの候補に対して次のトークンの確率分布を計算します。それぞれの現在の候補と可能な新しいトークンの組み合わせに対して確率を計算し、その合計確率を評価します。
-
これらの組み合わせの中から、合計確率が最も高い上位k個の組み合わせを新しい候補として保持します。
-
上記のステップを設定された最大トークン数に達するまで繰り返すか、文章が終了する特殊なトークンが選択されるまで繰り返します。
ビームサーチでは貪欲法のように局所的な最適解に縛られる可能性が低くなるため、より良い結果が得られる可能性があります。しかし、ビーム幅を大きくすると計算コストが高くなります。また、ビームサーチを使っても全てを網羅的に計算しているわけでは無いので、必ずしも最適解が得られるとは限りません。
ソースコード
output_beam = model.generate(input_ids, max_length=max_length, num_beams=5,
do_sample=False, pad_token_id=tokenizer.pad_token_id)
print(tokenizer.decode(output_beam[0]))
出力結果
国境の長いトンネルを抜けると、そこはもう別世界だった。
車窓に流れる景色を眺めていると、突然目の前が真っ暗になり、目の前が真っ暗になり、目の前が真っ
暗になり、目の前が真っ暗になり、目の前が真っ暗になり、目の前が真っ暗になり、目の前が真っ暗に
なり、目の前が真っ暗になり、目の前が真っ暗になり、目の前が真っ暗になり、目の前が真っ暗にな
り、目の前が真っ暗になり、目の前が真っ暗になり、目の前が真っ暗になり、目の前が真っ暗になり、
目の前が真っ暗になり、目の前が真っ暗になり、目の前が
ビームサーチを使っても同じ文章を繰り返す事はよくあります。前回と同じようにno_repeat_ngram_sizeを使って繰り返しに制限をかけてみましょう。
ソースコード
output_beam_n = model.generate(input_ids, max_length=max_length, num_beams=5,
do_sample=False, no_repeat_ngram_size=2, pad_token_id=tokenizer.pad_token_id)
print(tokenizer.decode(output_beam_n[0]))
出力結果
国境の長いトンネルを抜けると、そこはもう別世界だった。<|endoftext|>
すると今度は、とても短いところで文章が終了してしまいました。このようにビームサーチは、それぞれのステップで確率を乗算していくため、より長いシーケンスは短いシーケンスよりも確率が低くなる傾向があります。(結果として上記の文章は短いけど、簡潔でとても自然な文章です。)
サンプリング
サンプリングは確率分布に基づいてランダムでトークンを選択します。つまり、各ステップで生成されるトークンはその確率に比例して選ばれます。これにより、より多様な文を生成することが可能となります。
具体的なステップは以下のようになります:
-
モデルはまず文の最初のトークンを予測します。
-
次に、現在までに生成されたトークンを条件に、次のトークンの確率分布を計算します。
-
この確率分布から、ランダムにトークンを選択します。選択確率は各トークンの予測確率hに比例します。
-
上記のステップを設定された最大トークン数に達するまで繰り返すか、文章が終了する特殊なトークンが選択されるまで繰り返します。
サンプリングのメリットはランダムにトークンを選択することで、生成する文章の多様性を確保出来ることです。ただデメリットとして、必ずしも正確とは言えない文章や、意味のない文章が生成される可能性もあります。
サンプリングではtemperatureの値を設定することで、生成する文章に多様性をもたせるか、一貫性を持たせるか調整することができます。
temperature = 2.0の場合
ソースコード
output_topk = model.generate(input_ids, max_length=max_length, do_sample=True,
temperature=2.0, top_k=0,
pad_token_id=tokenizer.pad_token_id)
print(tokenizer.decode(output_topk[0]))
出力結果
国境の長いトンネルを抜けると二十〇〇フィートの藤悟に向かった目の前の広いにはエメラルド儚に行くある
日の競大量のアメリカンキャンセル洪水質でさえて地下歩のマンション札っぺ器でWii淡路悩みをしながらス
プリアンドセン哏大気の機関復元雄Acではなかったが言うと灘洋食の天リーダーにの間栓いける像はすると
発表したわたく実にCEO伺岳rowの改盛キャッシュ丸ごとフロワットが全くのくアメリカその分プログラマサ
ム齒城県室内IP神として提示日以内に奇跡成功した日目は思いつがく世界的自動車道隠れだと思うが締結の
仕事現代東北大学の実践Red 06 Science除去の子供たち水胚オイフィリありがとうそのよこを使ったなどな
どの夕の冒件数何百法二階ソリューションフェスタお風呂使チューセッツBP証券年以来チャールズこの事件
楽譜
temperature = 2.0では全く文章として成立していないものが、生成されました。
temperature = 0.3の場合
output_topk = model.generate(input_ids, max_length=max_length, do_sample=True,
temperature=0.3, top_k=0,
pad_token_id=tokenizer.pad_token_id)
print(tokenizer.decode(output_topk[0]))
出力結果
国境の長いトンネルを抜けると、そこはもうアメリカ。
アメリカへ入国するためには、アメリカに入国するビザ(査証)が必要です。
ビザは、国によって申請方法や条件が異なります。
アメリカに入国するビザは、ESTA(エスタ)と呼ばれる電子渡航認証システムに登録する必要があります。
ESTAとは、電子渡航認証システムのことで、アメリカへ渡航するにあたって、事前に申請を行い、審査を受
けることで、渡航の90日前から渡航の90日前までの間に、アメリカに入国できるかどうかを認証してくれる
システムです。
アメリカへ入国するビザは、ESTA(エスタ)で認証された後に、アメリカの入国審査
こちらは先程に比べて一貫性のある文章が生成されました。内容の通り、temperatureが0に近づくと生成される文章は貪欲法に近くなります。このように一貫性と多様性の間には常にトレードオフがあり、ユースケースに合わせて調整する必要があります。
これを改善するために、「Top-k サンプリング」や「Top-p サンプリング」といった戦略が提案されています。
Top-k サンプリング
Top-k サンプリングでは各ステップで予測する次のトークンの確率が高い上位k個のトークンのみが次の候補となり、その中から各トークンの確率に比例した確率で1つがランダムに選ばれます。kの値が大きいほど生成される文章は多様性が高くなり、kの値が小さければ一貫性のある文章が生成されます。したがってこちらも目的に応じた適切なkを設定することが重要となります。
具体的なステップは以下のようになります:
-
モデルはまず文の最初のトークンを予測します。
-
次に、現在までに生成されたトークンを条件に、次のトークンの確率分布を計算します。
-
この確率分布から、最も確率が高い上位k個のトークンを選択します。
-
選択されたトークン群から、各トークンの予測確率に比例した確率でランダムに1つのトークンを選びます。
-
上記のステップを設定された最大トークン数に達するまで繰り返すか、文章が終了する特殊なトークンが選択されるまで繰り返します。
ソースコード
output_topk = model.generate(input_ids, max_length=max_length, do_sample=True,
top_k=50, pad_token_id=tokenizer.pad_token_id)
print(tokenizer.decode(output_topk[0]))
出力結果
国境の長いトンネルを抜けると,そこは北海道らしく雄大な景色が広がっていた。しかし,車窓から眼下
に広がる大平原や,空に広がる広大な水平線,そして緑深い大地。この豊かな自然は,私が想像していたも
のよりもはるかにスケールが大きく,広大な大地には,動物や植物をはじめ,ありとあらゆる生命が息づい
ているようで,ただただ圧倒されたという気がした。
トンネルに入ると車のスピードが落ちるため,あっという間に景色はすぐに雲の中に消えていった。次に
見えるのは、見渡す限りの大平原。私は,大地の広がりと青空と真っ白な空のコントラストの中に身を置
き,しばらく目を凝らし,その雄大
生成された文章は自然な文章になっています。
Top-p サンプリング
Top-p サンプリングでは、次に生成されるトークンが取りうる全てのトークンについてモデルが確率を計算し、これらの確率を大きい順に並べて累積確率が指定した閾値p(0から1の間の値)を超えるまでのトークンを選びます。そして、これらのトークンから確率に比例してランダムに一つ選びます。この手法の優れた点は多様性と一貫性のバランスが取れていることで、文脈によって柔軟にトークンの選択の幅を調整出来ることです。つまり、トークンの確信度が高い場合には選択肢が狭まり、確信度が低い場合には選択肢が広くなります。
具体的なステップは以下のようになります:
-
モデルはまず文の最初のトークンを予測します。
-
次に、現在までに生成されたトークンを条件に、次のトークンの確率分布を計算します。
-
この確率分布から、確率が大きいトークンから順に選び、累積確率が閾値pを超えるまでのトークンを選択します。
-
選択されたトークン群から、各トークンの予測確率に比例した確率でランダムに1つのトークンを選びます。
-
上記のステップを設定された最大トークン数に達するまで繰り返すか、文章が終了する特殊なトークンが選択されるまで繰り返します。
output_topp = model.generate(input_ids, max_length=max_length, do_sample=True,
top_p=0.90, pad_token_id=tokenizer.pad_token_id)
print(tokenizer.decode(output_topp[0]))
出力結果
国境の長いトンネルを抜けるとそこは、スペイン・ポルトガル国境でした。国境を越えた瞬間から、スペイン・ポルトガルの人たちはスペイン語で話し掛けます。「OK、おいで」と。ポルトガル人の家族に連れられて、私たち観光客もその懐かしさでいっぱいになりました。
マドリードの町中を歩いていてもスペイン人に出会う機会は多く、もちろん日本人よりはずっとスペイン語が流暢ですが、旅行中に一番びっくりしたのは、ポルトガル語を話す人たちが多いということです。バスの中で「アスタ・ド・ソル、おやすみ」と言えば、ちゃんと「おやすみ」と返ってくるし、レストランや店の人たちが「オラー!」と言っても、スペイン語を話すと
こちらもとても自然な文章が生成されています。
どの手法を使うのが良いのか?
結論から言うと、普遍的な最良の手法は存在しません。使う目的によって異なると思います。例えば創作活動で使用する場合には多様性の高さが必要だろうし、質問に対する回答のような文章が必要であれば一貫性の高さが必要になりますし、応答速度の面から手法を選ぶ必要もあると思います。選択する手法やパラメータによって様々な使い方ができるので、目的に応じた使い方を検討するのが重要だと思います。