[サイオニック製 ラマ4トークン編集ツール] 日本語処理能力を分析・強化するツール

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4061124/9e361ed8-8363-453b-ba7b-806036a7bca7.png)

Llama 4 モデルの登場は、特に多言語対応において大きな進歩を示しました。本稿で紹介する Llama4 Token Editor（日本語バージョン） は、その分析ツールをベースに、日本語（ひらがな、カタカナ（全角/半角）、漢字、全角英数字など）の解析能力を強化したものです。

> https://github.com/sionic-ai/Llama4-Token-Editor-JA.git

このツールは、大規模言語モデル（LLM）、特に Llama 系モデルのトークナイザー内部を深く掘り下げ、そのモデルがどの程度日本語の語彙を扱っているかを可視化します。さらに、分析結果に基づいて特定のトークンカテゴリの生成確率（logits）を調整し、日本語テキスト生成の質を向上させるための機能を提供します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4061124/0fd85b14-8a62-4492-b221-861ae37af03e.png)

```
git clone https://github.com/sionic-ai/Llama4-Token-Editor-JA.git
cd Llama4-Token-Editor-JA
pip install -r requirements.txt
```

## トークナイザーで日本語トークンを分析してみよう
近年の LLM は、Python だけでなく C++ や CUDA など、より低レベルな言語での実装も増えています。これは性能最適化や学習・理解の深化に貢献しますが、一方で多言語対応、特に日本語のようなマルチバイト文字の扱いがモデルごとに異なるという課題も生んでいます。

自分が利用したい LLM が、どの程度の日本語語彙を持っているのか、あるいは特定の種類の文字（例：半角カタカナ、全角英数字）をどのようにトークン化しているのかを知ることは、モデル選定やファインチューニング、プロンプトエンジニアリングにおいて非常に重要です。

トークナイザーは、入力されたテキストを「トークン」と呼ばれる単位に分割し、それぞれに数値 ID を割り当てます。BPE (Byte Pair Encoding) や SentencePiece といったアルゴリズムがよく用いられ、モデルは内部的にこの ID のシーケンスとしてテキストを処理します。例えば、「日本語」という単語が ["日本", "語"] のように分割されたり、「プログラミング」が ["プロ", "グラ", "ミング"] のようになったりします。時には、UTF-8 のバイト列が途中で分割され、複数のトークンにまたがることもあります。

Llama4 Token Editor 日本語版 は、モデルの持つ全語彙（ボキャブラリー）をスキャンし、各トークンがどのような文字種（ひらがな、カタカナ、漢字、英語、数字、記号など）で構成されているかを分類します。特に、UTF-8 のバイト列を考慮し、BPE によって分割された可能性のある部分的なバイトシーケンスでも、「日本語を形成する可能性」があればそれを検出する点が特徴です。

## 日本語トークン分析の原理
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4061124/4040568a-e3a1-4e23-93c7-51aa2c574df0.png)
このツールは、トークンをデコードして得られる文字列と、その元となるバイト列を分析し、日本語に関連する文字が含まれているかを判定します。主な判定ロジックは以下の関数に基づいています（詳細は token_analyzer_ja.py を参照）。

`is_japanese_related_char(char):`

* 与えられた1文字が、ひらがな (U+3040～309F)、カタカナ (全角: U+30A0～30FF / 半角: U+FF65～FF9F)、CJK 統合漢字 (U+4E00～9FFF 及び拡張A～F, 互換漢字)、全角英数字 (U+FF01～FF5E)、日本語で使われる句読点・記号などに該当するかを判定します。非常に広範な日本語関連文字をカバーします。

`is_pure_japanese_script_char(char):`

* 文字が厳密に「日本語の書記体系」、つまりひらがな、カタカナ（全角/半角）、漢字のみで構成されるかを判定します。記号や全角英数字は含みません。

`UTF-8 バイトレベルでの分析 :`

* (内部ロジック) トークンをバイト列として見た際に、それが日本語文字（通常 UTF-8 で 3 バイト）の一部を構成しうるか、あるいは完全に一つの日本語文字を形成しているかを判定します。これにより、BPE で分割されたトークン（例: 0xE3 0x81 のみを含むトークンと、後続の 0x82 を含むトークン）も、「日本語になり得る」として検出可能です。

これらの判定ロジックを組み合わせることで、単に文字が含まれているかだけでなく、そのトークンが「純粋な日本語」なのか、「半角カタカナを含む」のか、「全角英数字を含む」のか、といったより詳細なカテゴリ分類を実現しています。

## トークンIDを知ることにはどんな意味があるでしょうか？

このツールによる分析結果、すなわちカテゴリ分けされたトークン ID のリストは、モデルの振る舞いを制御するために直接利用できます。具体的には、「Logits Bias（ログ確率バイアス）」というテクニックに応用できます。
これは、モデルが次に出力する単語（トークン）を予測する際に、特定のトークン ID のスコア（対数確率）を人為的に増減させる手法です。

* 日本語生成の促進: contains_japanese や pure_japanese_script に分類されたトークン ID リストを取得し、それらのスコアに正のバイアス値（例: +2.0）を加えます。これにより、モデルは他のトークンよりも日本語関連のトークンを選択しやすくなり、より自然な日本語テキストを生成するよう誘導できます。

* 不要な文字の抑制: special_char_pattern （記号のみで構成されるトークン）や、特定の条件下で避けたいトークン（例: 半角カタカナ）のリストに負のバイアス値（例: -1.0）を与えれば、それらのトークンが出現する確率を下げることができます。

このように、大規模な再学習やファインチューニングを行うことなく、推論（テキスト生成）時にリアルタイムで出力傾向を調整できるのが Logits Bias の利点です。しかし、その適用のためには、まずモデルの全語彙から対象となるトークン ID を正確に特定する必要があり、手作業では非常に手間がかかります。Llama4 Token Editor 日本語版 は、このトークン ID の特定と分類、そしてリスト化までを自動化し、Logits Bias の適用を容易にします。

## 使い方
1. インストール
```
git clone https://github.com/sionic-ai/Llama4-Token-Editor-JA.git
cd Llama4-Token-Editor-JA
pip install -r requirements.txt
```

2. トークン解析の実行
```
python token_analyzer_ja.py --model_id "meta-llama/llama-4-scout"
```

主な引数:

* `-model_id`: 分析したいモデルのローカルパス、または Hugging Face Hub のモデル ID を指定します。（例: "meta-llama/llama-4-scout"）

* `-min_token_id`: 解析を開始するトークン ID の下限値（デフォルト: 102）。通常、0 から 101 あたりにはモデル固有の特殊トークン（<EOS>, <PAD>, <UNK> など）が含まれるため、これらを誤ってバイアス調整の対象としないように除外するのが一般的です。

* `-output_dir`: 解析結果を出力するディレクトリ名（デフォルト: token_analysis_output）。

解析が完了すると、--output_dir で指定したディレクトリ内に以下のファイルが生成されます。

1. JSON ファイル: (token_analysis_ja_<モデル名>.json)
* モデル情報、解析対象のトークン数、各カテゴリに分類されたトークン数、そして各カテゴリに含まれるトークン ID の全リストなどが JSON 形式で格納されています。統計情報の確認や、プログラムでの後処理に適しています。

2. カテゴリ別テキストファイル: (contains_japanese_<モデル名>.txt, pure_japanese_script_<モデル名>.txt, uncategorized_<モデル名>.txt など)
* 主要なカテゴリごとに、含まれるトークン ID のリストが Python のリスト形式（例: category_ids = [123, 456, 789]）で保存されます。このファイルは、後述する Logits Bias のコードで直接読み込んで利用することを想定しています。

## 分析ロジックの詳細
token_analyzer_ja.py スクリプトの中心的な処理は、指定されたモデルの min_token_id 以上のトークン ID を一つずつ取り出し、tokenizer.decode() を使って文字列に変換し、その文字列内の各文字を分析してカテゴリに分類することです。

* 特殊トークンの除外: --min_token_id 引数や、tokenizer.all_special_ids で取得できる ID を基に、解析対象から特殊トークンを除外します。これにより、モデルの基本的な動作に影響を与える可能性のあるトークンへの意図しないバイアス適用を防ぎます。

* 日本語カテゴリの判定:
    * contains_japanese: is_japanese_related_char が True となる文字が一つでも含まれていれば、このカテゴリに追加されます。
    * pure_japanese_script: トークン内の全文字が is_pure_japanese_script_char で True と判定された場合にのみ、このカテゴリに追加されます。

その他、contains_hiragana, contains_katakana_full, contains_katakana_half, contains_kanji, contains_fullwidth_ascii など、より詳細なカテゴリにも同時に分類されます。

* 英語・特殊文字・未分類:
    * pure_english: ASCII の基本英字 (a-z, A-Z) のみで構成されるトークン。
    * special_char_pattern: 英数字、スペース、日本語関連文字のいずれにも該当しない文字のみで構成されるトークン（記号列など）。
    * uncategorized: 上記のいずれの明確なカテゴリにも分類されなかったトークン。これには、数字のみのトークン、英語と数字の混在、他言語の文字、あるいは BPE によって生成された意味不明なバイトの組み合わせなどが含まれる可能性があります。

> **💡 「未分類トークン」について**
モデルによっては、この「未分類」カテゴリに属するトークンの割合が予想以上に大きいことがあります。これは、BPE や SentencePiece が、頻度の低い文字列や、言語間で共通して現れる部分的なバイト列などを独立したトークンとして学習するためです。例えば、"123" や "ing"、あるいは特定のプログラミング言語の構文要素などが未分類に含まれる可能性があります。未分類トークンの内容や量を調べることで、そのモデルがどのようなデータで学習され、どのような種類のテキストを効率的に扱えるように設計されているかのヒントを得ることができます。 </aside>

## 純粋な日本語トークンだけに重みを付けるのは不十分な場合がある
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4061124/ec00468e-dec0-4f0a-82e7-b7608e66b26e.png)

Logits Bias を適用する際に、「純粋な日本語 (pure_japanese_script)」カテゴリのトークンだけに高いスコアを与えるという戦略も考えられます。しかし、それだけでは不十分な、あるいは逆効果になる可能性もあります。
なぜなら、BPE によって日本語の単語やフレーズが、必ずしも意味のある単位で分割されるとは限らないからです。

* 部分トークンの重要性: 例えば、「素晴らしい」という単語が、["素晴", "らしい"] のように分割されたとします。この ["素晴"] は純粋日本語ですが、["らしい"] は後続の単語と結合する可能性のある接尾辞的な要素です。もし ["素晴"] のような完成形に近いトークンだけにバイアスをかけ、["らしい"] のような部分的なトークンや、あるいは ["プロ", "グラ", "ミング"] のような分割されたカタカナ語トークンにバイアスをかけないと、モデルは文章の途中で必要なパーツを選択できなくなり、不自然な出力や、「ハルシネーション（幻覚）」と呼ばれる意図しない文字列の生成を引き起こす可能性があります。

* UTF-8 部分バイトシーケンス: さらに、トークンが UTF-8 のバイト列の途中で分割されている場合（例: 0xE3 0x81 を含むトークン）、それ自体は文字として表示できなくても、後続のバイト (0x82 = 'あ') を含むトークンと結合することで初めて意味を成します。このような「日本語を形成する可能性のある」部分的なバイト列を含むトークン (contains_japanese には含まれるが pure_japanese_script には含まれないトークン) を無視してしまうと、やはりスムーズな日本語生成が妨げられる可能性があります。

**結論**: 日本語の生成を促進したい場合は、pure_japanese_script だけでなく、より広範な contains_japanese カテゴリのトークン ID リストに対して、まとめて正のバイアスを与える方が、多くの場合でより良い結果が得られます。もちろん、目的に応じて調整は可能です。

## Llama 4 とトークン分析

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4061124/8da003de-5de0-4beb-8604-45dc3d392985.png)

* Llama 4 のような新しいモデルは、長いコンテキストウィンドウや効率的なアテンションメカニズム（例: Scalable-Softmax）を備えていますが、その根幹にあるのは依然としてトークンベースの処理です。モデルがどれだけ高度なアーキテクチャを持っていても、その語彙に目的の言語（この場合は日本語）のトークンが豊富に含まれていなければ、その言語での性能は制限されます。

* Llama4 Token Editor 日本語版 を使うことで、「このモデルは日本語の語彙をどの程度持っているか？」「特定の種類の日本語表現（例：カタカナ語、漢字熟語）を効率的に扱えるか？」といった疑問に対する具体的な答えを得ることができます。分析結果の JSON ファイルやテキストファイルを見れば、モデル内部でテキストがどのように数値 ID に変換され、処理されているかの一端を垣間見ることができます。

## Logits Bias の具体的なコード例
分析によって得られたトークン ID リストを使って、実際に Logits Bias を適用する方法の例を示します。

a) Transformers ライブラリでの適用例
LogitsProcessor をカスタム実装し、generate メソッドに渡します。

```python
from transformers import AutoModelForCausalLM, AutoTokenizer, LogitsProcessorList, LogitsProcessor
import torch

# 特定トークンにバイアスを加えるプロセッサ
class TokenBiasLogitsProcessor(LogitsProcessor):
    def __init__(self, token_ids: List[int], bias_value: float):
        self.token_ids = set(token_ids) # 高速なルックアップのために Set を使用
        self.bias_value = bias_value

    def __call__(self, input_ids: torch.LongTensor, scores: torch.FloatTensor) -> torch.FloatTensor:
        # scores の形状は (batch_size, vocab_size)
        for token_id in self.token_ids:
            if 0 <= token_id < scores.shape[-1]: # 語彙サイズ内かチェック
                scores[:, token_id] += self.bias_value
        return scores

# モデルとトークナイザーのロード (例: ELYZAモデル)
model_id = "meta-llama/llama-4-scout"
model = AutoModelForCausalLM.from_pretrained(model_id)
tokenizer = AutoTokenizer.from_pretrained(model_id)

# 解析結果のテキストファイルから日本語トークンIDを読み込む
# (ファイル名は実際の解析結果に合わせてください)
japanese_token_ids = []
try:
    # 例: contains_japanese_ELYZA_japanese_Llama_2_7b.txt
    filename = "token_analysis_output/contains_meta_llama_llama_4_scout.txt"
    with open(filename, "r", encoding="utf-8") as f:
        content = f.read().strip()
        # "contains_japanese_ids = [123,456,...]" の形式を想定
        if content.startswith("contains_japanese_ids = [") and content.endswith("]"):
            ids_str = content.split("=", 1)[1].strip()[1:-1] # [...] の中身を取得
            if ids_str:
                japanese_token_ids = [int(tid_str.strip()) for tid_str in ids_str.split(",") if tid_str.strip()]
        else:
             print(f"警告: ファイル形式が予期したものではありません: {filename}")
except FileNotFoundError:
    print(f"警告: トークンIDファイルが見つかりません: {filename}")
except Exception as e:
    print(f"警告: ファイル読み込みエラー: {e}")

# バイアス値の設定 (日本語トークンのスコアを上げる)
bias_value = 2.0

# プロンプト
prompt = "日本の首都は"
input_ids = tokenizer(prompt, return_tensors="pt").input_ids.to(model.device) # GPUがあれば転送

# LogitsProcessorの準備
logits_processor_list = LogitsProcessorList()
if japanese_token_ids:
    logits_processor_list.append(TokenBiasLogitsProcessor(japanese_token_ids, bias_value))
    print(f"Logits Bias を {len(japanese_token_ids)} 個のトークンに適用します。")
else:
    print("Logits Bias は適用されません (対象トークンIDなし)。")

# テキスト生成
output = model.generate(
    input_ids,
    max_new_tokens=50, # max_length の代わりに max_new_tokens を推奨
    do_sample=True,
    temperature=0.7,
    logits_processor=logits_processor_list # ここでプロセッサを指定
)

# 結果のデコードと表示
result = tokenizer.decode(output[0], skip_special_tokens=True)
print("--- 生成結果 ---")
print(result)
```

b) vLLM の OpenAI 互換 API で logit_bias を設定する例
vLLM を OpenAI 互換サーバーとして起動している場合、API リクエスト時に logit_bias パラメータを指定できます。
https://huggingface.co/docs/transformers/en/internal/generation_utils

```python
# Import the necessary class from the updated openai library
from openai import OpenAI
import os # Used for robust path handling (optional but good practice)

# --- Configuration ---
# vLLM server endpoint
VLLM_BASE_URL = "<http://localhost:8000/v1>"
# Dummy API key for vLLM (vLLM typically doesn't require a real key)
VLLM_API_KEY = "dummy"
# Model name as loaded in vLLM
MODEL_NAME = "meta-llama/llama-4-scout" # Make sure this matches your vLLM loaded model
# Path to the token ID file generated by the analyzer
TOKEN_ID_FILE_DIR = "token_analysis_output" # Directory where analysis files are saved
TOKEN_ID_FILE_CATEGORY = "contains_japanese" # The category file to use (e.g., contains_japanese)
# Construct the expected filename based on model and category
# Normalize model name for filename compatibility
safe_model_name = MODEL_NAME.replace("/", "_").replace("-", "_")
TOKEN_ID_FILENAME = f"{TOKEN_ID_FILE_CATEGORY}_{safe_model_name}.txt"
TOKEN_ID_FILE_PATH = os.path.join(TOKEN_ID_FILE_DIR, TOKEN_ID_FILENAME)

# --- Instantiate the OpenAI client pointing to the vLLM server ---
client = OpenAI(
    base_url=VLLM_BASE_URL,
    api_key=VLLM_API_KEY,
)

# --- Load Token IDs for Logit Bias ---
japanese_token_ids = []
try:
    print(f"Attempting to load token IDs from: {TOKEN_ID_FILE_PATH}")
    with open(TOKEN_ID_FILE_PATH, "r", encoding="utf-8") as f:
        content = f.read().strip()
        # Expecting format like: "contains_japanese_ids = [123, 456, ...]"
        expected_prefix = f"{TOKEN_ID_FILE_CATEGORY}_ids = ["
        if content.startswith(expected_prefix) and content.endswith("]"):
            # Extract the comma-separated IDs string between '[' and ']'
            ids_str = content[len(expected_prefix):-1]
            if ids_str: # Check if the string is not empty
                japanese_token_ids = [int(tid_str.strip()) for tid_str in ids_str.split(",") if tid_str.strip()]
            print(f"Successfully loaded {len(japanese_token_ids)} token IDs.")
        else:
            print(f"Warning: File content format mismatch in {TOKEN_ID_FILE_PATH}. Expected prefix '{expected_prefix}'.")

except FileNotFoundError:
    print(f"Error: Token ID file not found at {TOKEN_ID_FILE_PATH}. Logit bias will not be applied.")
except Exception as e:
    print(f"Error reading token ID file {TOKEN_ID_FILE_PATH}: {e}. Logit bias may not be applied correctly.")

# --- Prepare logit_bias dictionary ---
logit_bias = {}
if japanese_token_ids:
    bias_value = 10.0 # Adjust bias strength as needed (-100 to 100 typical for vLLM)
    # Convert token IDs to strings for the logit_bias dictionary keys
    logit_bias = {str(tid): bias_value for tid in japanese_token_ids}
    print(f"Applying logit_bias to {len(logit_bias)} tokens with value {bias_value}.")
else:
    print("No token IDs loaded, logit_bias will be empty.")

# --- Call the Chat Completion API ---
try:
    print(f"\\nSending request to vLLM model: {MODEL_NAME}")
    response = client.chat.completions.create( # Use the client instance method
        model=MODEL_NAME, # Model name registered with vLLM
        messages=[
            {"role": "user", "content": "日本の首都は"}
        ],
        temperature=0.7,
        max_tokens=50, # Renamed from max_length in some older contexts, use max_tokens
        logit_bias=logit_bias # Pass the prepared dictionary
    )
    print("--- vLLM Generation Result ---")
    # Access content using attribute access in openai v1.0+
    generated_content = response.choices[0].message.content
    print(generated_content)

except Exception as e:
    # Catch potential API errors (connection issues, model not found, etc.)
    print(f"\\nError during vLLM API call: {e}")

```
## 分析例：Llama 系モデルの日本語トークン比較
提供された README.md の分析結果を基に、いくつかのモデルにおける日本語トークンの割合を比較してみましょう。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4061124/36a90b38-6684-410b-9533-b649aef34548.png)

**考察**:

* Llama-4-Scout: この中では日本語関連トークンの割合が最も高く、約 7.3% を占めています。特に「純粋な日本語」トークンも 6% 以上あり、日本語の構成要素（ひらがな、カタカナ、漢字）を比較的多く語彙に含んでいることがうかがえます。

* Llama-3.3-70B: 日本語関連は約 4.5%。Llama-4-Scout と比較すると少ないですが、未分類トークンの数が比較的少ないため、語彙の多くが英語や他の明確なカテゴリに属している可能性があります。

* Mistral-Small-3.1-24B: 日本語関連は約 4.3%。Llama-3.3 と同程度ですが、未分類トークンが約 3.3 万と多いのが特徴です。これは、多言語データやコードなどが未分類に含まれている可能性を示唆します。

**結論**: 単純な日本語トークンの量だけで見れば、Llama-4-Scout が有利に見えます。しかし、モデルの総合的な性能は、トークン数だけでなく、学習データの内容、モデルサイズ、アーキテクチャなど多くの要因に依存します。また、「未分類」に含まれるトークンが実際には特定のタスク（例: プログラミングコード生成）で有用な場合もあります。このツールは、モデルの日本語に対する「語彙的な親和性」を測る一つの指標を提供します。

## 生成結果の例（Logits Bias適用時）
プロンプト: 「今日はとてもいい天気ですね」

日本語トークンに +2.0 のバイアスを適用した場合の生成例:

本当に素晴らしい天気ですね。散歩やカフェに行くのも気持ちが良さそうです。

バイアスなしの場合の生成例 (モデルによっては英語で応答する可能性):

Yes, the weather is quite nice. Would you like more details on the local climate?

このように、日本語トークンに明確なバイアスを与えることで、モデルが英語や他の言語、あるいは不要な記号などを避け、意図した通り日本語主体のテキストを生成しやすくなります。