機械学習
記事内に商品プロモーションを含む場合があります

CALM2のトークナイザーの出力調査&チャットボットの作り方

tadanori

32,000トークンで日本語50,000文字という説明が気になったのでトークナイザー(tokenizer)の出力を調べてみました。また、ついでにCALM2を使った簡単なチャットボット(chatbot)も作ってみて気になる部分をチェックしてみました。

大規模言語モデル関連記事一覧はこちら
大規模言語モデル(LLM)関連の記事一覧
大規模言語モデル(LLM)関連の記事一覧

CALM2とは

CALM2(CyberAgentLM2)はサイバーエージェントが開発した大規模言語モデル(LLM)です。日本語に特化した大規模言語モデルという部分が特徴です。

他の記事にも書きましたが、32,000トークンまでの入力に対応していて日本語の文章として約50,000文字を一度に処理することができます。

他の記事でも動かしてみてはいますが、今回はCALM2をもう少しだけ深掘りしてみました。

あわせて読みたい
大規模言語モデル(Llama2/Gemma/Calm2)を動かしてみる(使い方)
大規模言語モデル(Llama2/Gemma/Calm2)を動かしてみる(使い方)

また、CALM2を使ったRAGについては以下を参考にしてください。

あわせて読みたい
LangChainでRAGを試す|CALM2 + FAISS + RetrievalQAの使用例
LangChainでRAGを試す|CALM2 + FAISS + RetrievalQAの使用例

気になったこと

トークンの分割

最大32,000トークンで、約50,000文字ということでトークンがどのように区切られているのか気になりました。

OpenAIのGPT-3.5のトークナイザーでの分割をチェックすると以下のようになりました。

GPT3.5の分割
参照元:https://platform.openai.com/tokenizer

上記のように、ほぼ一文字単位でトークンが切られています。一部はと複数文字が1トークンになっていますが、半角数字の部分です。

英語の場合は、大体の場合、単語単位でトークン化されます。1文字1トークンと比較すると長い文章が入力できることになります。

トークンの特徴ベクトルに変換する部分でも有利な気がしますね。

32,000トークンで50,000文字だとすると、単純計算で平均1.56文字/トークンになります。

今回は、日本語文字列がどのように分割されてトークン化されるのかをチェックしてみました。

チャットする時のプロンプト

ユーザが入力してLLMが回答、再びユーザが入力してLLMが回答というチャットの場合のLLMへの入力も気になる点の1つです。

CALM2の公開されているテンプレートでは、以下のように入力することになっています。

公開されているチャットのテンプレート

USER: {user_message1}
ASSISTANT: {assistant_message1}<|endoftext|>
USER: {user_message2}
ASSISTANT: {assistant_message2}<|endoftext|>
USER: {user_message3}
ASSISTANT: {assistant_message3}<|endoftext|>

これを見ると、これまでの会話を1つのプロンプトにまとめて入力するように見えます。

これも確認してみることにしました。

実際に確認してみる

上記の二点について実際にプログラムを組んで確認してみました。

疑問点をプログラムを書いて確認

確認は、Google Colabで行いました。ランタイプはT4 GPUで動作します。

ランタイムのタイプ

インストール

必要なライブラリをインストールします。以下のコマンドをコードセルで実行します。

!pip install -q accelerate==0.21.0 peft==0.4.0 bitsandbytes==0.40.2 transformers==4.31.0 trl==0.4.7

トークナイザーとモデルの読み込み

トークナイザーとモデルを初期化します。今回利用したのはチャット用のcyberagent/calm2-7b-chatです。

import torch
from transformers import AutoModelForCausalLM, AutoTokenizer, TextStreamer


model = AutoModelForCausalLM.from_pretrained("cyberagent/calm2-7b-chat", 
                                             device_map="auto", 
                                             torch_dtype="auto")
tokenizer = AutoTokenizer.from_pretrained("cyberagent/calm2-7b-chat")
streamer = TextStreamer(tokenizer, skip_prompt=True, skip_special_tokens=True)

model.eval()

※ダウンロードに時間がかかるのでしばらく待ちます

トークン数を調べる(確認①)

とりあえず、GPT-3.5と同じ文字列をトークナイザーで変換してみます。

prompt = "最大32,000トークンで、約50,000文字ということでトークンがどのように区切られているのか気になりました。"
input_ids = tokenizer(prompt, return_tensors="pt")#, max_length=32, padding=True, truncation=True)

for e in input_ids['input_ids'][0]:
  r = tokenizer.decode(e)
  print(r, end=" | ")
print(f"\n文字数={len(prompt)}, トークン数={len(input_ids['input_ids'][0])}")

実行結果は以下になります。結果を見ると、「もじ」「どのように」などのように、ある程度の意味のある塊でトークンに分割されているように見えます。ただ、「トーク」「ンで」みたいな分割もあるので、きっちり形態素解析されているわけではなさそうです。

トークン化の手法にあまり詳しくないですが、BPE(Byte Pair Encoding, バイトペア・エンコーディング )みたいなエンコーディングになっている感じがします。

実行結果(トークンを確認)
最大 | 3 | 2 | , | 0 | 0 | 0 | トーク | ンで | 、 | 約 | 5 | 0 | , | 0 | 0 | 0 | 文字 | という | ことで | トーク | ンが | どのように | 区 | 切 | られている | のか | 気になり | ました | 。 | 
文字数=56, トークン数=30

文字数56文字に対して30トークンなので、GPT3.5と比べて少ないトークン数で変換できたことがわかります。

一度に入力できるトークン数が決まっているので、文字数に対してトークン数が少なくなれば、それだけ長い文章を一度に入力できることになります。

ということで、ある程度、意味のあるトークンに分割されていることが確認できました。

チャットを実装してみる(確認②)

確認では、①「これまでの会話をプロンプトに含めないもの」と、②「これまでの会話をプロンプトに含めるもの」の2つを作成して実験してみました。


①:これまでの会話をプロンプトに含めない

チャットを作る時のうまく動作しない実装例です。

入力文字列のみでpromptを生成し、トークナイザーとモデルに入力して回答を生成しています。

なお、このプログラムではstreamerを設定しています。下記プログラムのように、streamer=を設定するとアシスタントの出力をChatGPTのような感じで段階的に結果が出力されるようになります。

while True :
  txt = input("入力: ")
  if txt == "quit" : break

  prompt = f"""USER:{txt}
  ASSISTANT:"""
  input_ids = tokenizer.encode(prompt, return_tensors="pt")

  print("アシスタント:", end=" ")
  with torch.no_grad():
    tokens = model.generate(
      input_ids = input_ids.to(model.device),
      max_new_tokens=300,
      do_sample=True,
      temperature=0.7,
      top_k = 50, 
      streamer=streamer,
    )

以下は、チャットの結果です(「入力:」がユーザー側の入力です)

入力: これから日本について質問します。よろしいですか?
アシスタント:  はい、もちろんです。どんな質問でもお気軽にお聞きください。
入力: 一番高い山は?
アシスタント:  一番高い山はエベレストです。
入力: 二番目に高い山は?
アシスタント:  二番目に高い山は、日本国内ではありません。
入力: quit

2つ目の入力では、日本で一番高い山を聞いたつもりなのに、CALM2は「エベレスト」と答えています。

このように、これまでの会話をプロンプトに含めないと、これまでの会話の内容に沿った回答が得られないことが確認できました。


②:これまでの会話をプロンプトに含める

以下が修正したチャットプログラムです。

promptにアシスタントの回答を追加し、最後に<|endoftext|>を加えました(これは、公式のテンプレート通り)。

この実装方法で多分あっていると思いますが、もっと良い実装方法がある場合はコメントで教えてください。

また、プログラムに出力を加えて、promptの内容を以下の形式で出力するようにしました(これで、入力されるプロンプトを確認することができます)。

--------------------------------------------------------------------------------
<入力したプロンプトの内容>
--------------------------------------------------------------------------------

プログラムは以下になります。コード的には、promptの更新部分が違うだけです。

prompt = ""
while True :
  txt = input("入力: ")
  if txt == "quit" : break

  prompt += f"USER:{txt}\nASSISTANT:"
  input_ids = tokenizer.encode(prompt, return_tensors="pt")
  print("-"*80)
  print(prompt)
  print("-"*80)

  print("アシスタント:", end=" ")
  with torch.no_grad():
    tokens = model.generate(
      input_ids = input_ids.to(model.device),
      max_new_tokens=300,
      do_sample=True,
      temperature=0.7,
      top_k = 50, 
      streamer=streamer,
    )
  output = tokenizer.decode(tokens[0], skip_special_tokens=True)
  prompt += output.split("ASSISTANT:")[-1] + "<|endoftext|>\n"

以下が結果になります。分かりやすいように、promptの内容の部分は水色にしています。

入力: これから日本について質問します。よろしいですか?
--------------------------------------------------------------------------------
USER:これから日本について質問します。よろしいですか?
ASSISTANT:
--------------------------------------------------------------------------------
アシスタント:  はい、どうぞ。
入力: 一番高い山は?
--------------------------------------------------------------------------------
USER:これから日本について質問します。よろしいですか?
ASSISTANT: はい、どうぞ。<|endoftext|>
USER:一番高い山は?
ASSISTANT:
--------------------------------------------------------------------------------
アシスタント:  一番高い山は富士山です。
入力: 二番目に高い山は?
--------------------------------------------------------------------------------
USER:これから日本について質問します。よろしいですか?
ASSISTANT: はい、どうぞ。<|endoftext|>
USER:一番高い山は?
ASSISTANT: 一番高い山は富士山です。<|endoftext|>
USER:二番目に高い山は?
ASSISTANT:
--------------------------------------------------------------------------------
アシスタント:  二番目に高い山は妙高山です。
入力: quit

この例では、二回目の入力の「一番高い山は?」に対して、日本で一番高い山と解釈して「富士山」回答しています。このように、プロンプトにこれまでの会話を追加することで、うまく会話を続けることができました

なお、CALM2の入力は最大32,000トークンなので、32,000を超えるような場合は、過去の部分を削除して32,000トークン以内に収めるような処理が必要になると思います。

ASSISTANT:が1つのトークンではなく、以下のように4トークンに変換されたのはちょっと驚きでした。

ASS | IST | ANT | : 

この手の指示は1つになるかと思っていました。

雑記とか

会話用のプログラムを組んでいて思ったのは、思ったよりハイパーパラメータのチューニングが必要ということです。

今回はtop_ktempatureを設定していますが、設定によっては変な回答が出力されてしまいます(同じ単語を延々繰り返すなど)。

他にもtop_pno_repeat_ngram_sizeなど調整できるパラメータは多数ありますが、どのようにすれば良い結果が得られるのかわからないので調整が大変そうだと実感しました。

まとめ

CALM2を少しだけ深掘りしてみました。自身のコードで使うのはLLMって簡単そうで難しいですどう実装するのが正解なのか、まだ、よくわかっていないからというのもありますが、今後も試行錯誤しながら試してみたいと思います。

参考になれば幸いです。

おすすめ書籍

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

記事URLをコピーしました