『このプロンプト、何トークン?』知ってるとちょっと得する?!トークン数を意識したプロンプト作成

最近はLLMを使ったシステム開発を行っているのですが、トークン(Token)数の限界なのか上手く生成ができないといったことがあります。 自分が大概無理なことを行っている事が多いのですが🙄ほんとうにどれくらいトークンを使っているのかが体感でわかっていないで 作業しているからだと思います。そこで、APIにアクセスをしなくてもプロンプトからトークン数をカウントする方法はあるのかなと 疑問に思い調べてみたというのが今回の内容です。

トークン数の基本

トークンとは何か

LLMでのトークンとは、テキストを処理するための最小単位です。人間が単語を言語の基本単位として考えるのに対し、LLMのモデルはトークという単位でテキストを分割して処理します。そして、その分割を行うのがトークナイザー(Tokenizer)となります。

トークンは単語よりも小さい単位で、英語では一般的に単語の一部やスペース、記号などもトークンになります。

例えば、I love programmingという文は英語では3単語から構成されますが、トークナイザーによってはI love pro gram mingのように5トークンに分割されることがあります。

このトークン化はモデルの学習や推論の過程で使用される語彙に基づいて行われます。LLMではモデルそれぞれに独自の語彙を持っており、これに合わせて各テキストがどのようにトークン化されるかが決まります。

トークン数がなぜ重要なのか

LLMを使った場合、トークン数を把握することは重要です。

  1. コンテキスト長の制限 … AIモデルが一度に処理できるテキスト量はトークン数で制限されています。モデルごとに上限があり、この限界を超えると情報が途中で終わるなどのデメリットがあります。

  2. コストの単位 … AIのAPIOpenAIAnthropicなど)はトークン単位によって課金します。つまりトークン数がコスト管理に直結します。

  3. レスポンス時間 … トークン数が多いほどAPIの処理時間が長くなり、レイテンシが増加します。

  4. 生成結果の質 … トークン数の限界に近づくと、やり取りの古い情報を忘れてしまい、応答の質が低下します。そのため、会話履歴を扱う場合は、適切なトークン数が応答の品質につながります。

言語によるトークン数の違い

トークン数の数え方は言語によって大きく異なりますが、調べてみると

  • 英語: 比較的効率的で、平均して1単語あたり約1.3トークン程度です。長い単語はさらに複数のトークンに分割されます。
  • 日本語: 非常に非効率的で、単語単位で1〜数トークンを消費します。漢字は特に効率が悪く、1文字で複数トークンになることがあります。
  • その他の言語: 使用する文字種やアルファベットによって大きく異なります。アラビア語タイ語など、特殊な文字を使う言語はトークン効率が低くなる傾向があります。

レーニングデータに含まれる頻度が高いパターンほど少ないトークン数となる傾向があり、また、英語のような広く使われている言語の方が効率的になっています。

手動でのトークン数の見積もり方

API経由でカウントせずとも、プロンプトのおおよそのトークン数を見積もる方法がいくつかあり、目安となります。

文字数からの概算方法

テキストにふくまれる文字の種類別に以下のような感じになるでしょう。

  1. 英文テキスト … 文字数 ÷ 4 = おおよそのトークン数
  2. 日本語テキスト … 文字数 × 0.7 = おおよそのトークン数
  3. コードブロック … 文字数 ÷ 3 = おおよそのトークン数(言語による差が大きいため、余裕を持った計算が必要)
  4. 数字の羅列 … 文字数 ÷ 2 = おおよそのトークン数
  5. 特殊記号が多いテキスト … 文字数 × 0.5 = おおよそのトークン数(記号一つ一つがトークンになりやすい)

正確というわけではありませんが、プロジェクトを考える上である程度の精度が見込めます。

より正確な計算が必要な場合はプログラムを使ったカウントを使うことになります。

HuggingFaceのトークナイザーを使う方法

HuggingFaceで公開されているTransformersライブラリを使うと、さまざまな言語モデルトークナイザーにアクセスでき、より正確なトークン数の計算が可能になります。これはAPIアクセスを必要とせず、ローカル環境で実行できるため、開発時やテスト時に便利です。

ライブラリインストール

HuggingFaceトークナイザでは以下のライブラリが必要になるので、事前にインストールしておきます。

# 基本的な依存関係
$ pip install transformers
$ pip install torch

# SentencePieceモデル(rinnaなどの日本語モデル用)
$ pip install sentencepiece

# 特定のトークナイザー用
$ pip install tokenizers

# その他のライブラリ
$ pip install huggingface-hub

サンプルコード

以下は、HuggingFaceトークナイザーを使ってテキストのトークン数をカウントするPythonプログラムです:

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import argparse
import os
import logging
import json
import warnings
from typing import Tuple, List, Dict, Any, Optional

# 特定の警告を無視(T5Tokenizerの警告を抑制)
warnings.filterwarnings("ignore", message="You are using the default legacy behaviour")

from transformers import AutoTokenizer

def setup_logging(verbose: bool = False) -> None:
    """ロギング設定を構成します"""
    level = logging.DEBUG if verbose else logging.INFO
    logging.basicConfig(
        level=level,
        format='%(asctime)s - %(levelname)s - %(message)s'
    )

def load_tokenizer(model_name: str, use_fast: bool = True) -> Any:
    """
    適切なトークナイザーを読み込みます
    
    Args:
        model_name (str): HuggingFaceのモデル名
        use_fast (bool): 高速トークナイザーを使用するかどうか
        
    Returns:
        tokenizer: ロードされたトークナイザー
    """
    try:
        # 特定のモデルに対する特別な処理
        special_models = {
            "rinna": False,  # rinna/japanese-*モデルは遅いトークナイザーを使用
            "deberta-v2": False,  # DeBERTa-v2も遅いトークナイザーが推奨
        }
        
        # モデル名に基づいて、特別な処理が必要か確認
        for model_key, fast_value in special_models.items():
            if model_key in model_name.lower():
                use_fast = fast_value
                logging.info(f"{model_key}モデル用にuse_fast={use_fast}を設定しました")
                break
                
        tokenizer = AutoTokenizer.from_pretrained(model_name, use_fast=use_fast, legacy=True)
        return tokenizer
    except Exception as e:
        logging.error(f"トークナイザーの読み込みに失敗しました: {str(e)}")
        # フォールバックとして、use_fastをToggleして再試行
        if use_fast:
            logging.info("遅いトークナイザーで再試行しています...")
            return load_tokenizer(model_name, use_fast=False)
        raise

def count_tokens(text: str, model_name: str, use_fast: bool = True) -> Tuple[int, List[str], Dict[str, Any]]:
    """
    指定されたモデルのトークナイザーを使用してテキストのトークン数をカウントします
    
    Args:
        text (str): トークン数をカウントするテキスト
        model_name (str): 使用するモデルの名前(例: "gpt2", "bert-base-uncased")
        use_fast (bool): 高速トークナイザーを使用するかどうか
        
    Returns:
        Tuple[int, List[str], Dict[str, Any]]: 
            - トークン数
            - トークンのリスト
            - 追加情報の辞書
    """
    try:
        tokenizer = load_tokenizer(model_name, use_fast)
        
        # トークン化と詳細情報の取得
        encoded = tokenizer.encode(text, add_special_tokens=False)
        
        # トークンIDからデコードして実際の表示を確認
        tokens = []
        for token_id in encoded:
            # 各トークンIDを個別にデコードして追加
            token_text = tokenizer.decode([token_id])
            tokens.append(token_text)
        
        # 追加情報
        info = {
            "vocab_size": len(tokenizer),
            "model_max_length": tokenizer.model_max_length,
            "tokenizer_type": type(tokenizer).__name__,
        }
        
        return len(encoded), tokens, info
    except Exception as e:
        logging.error(f"トークン化エラー: {str(e)}")
        raise

def process_file(file_path: str, model_name: str, use_fast: bool = True, output_json: bool = False) -> Dict[str, Any]:
    """ファイルを処理し、トークン数を計算します"""
    try:
        with open(file_path, 'r', encoding='utf-8') as f:
            text = f.read()
        
        token_count, tokens, info = count_tokens(text, model_name, use_fast)
        
        result = {
            "file": file_path,
            "model": model_name,
            "token_count": token_count,
            "char_count": len(text),
            "char_to_token_ratio": len(text) / token_count if token_count > 0 else 0,
            "tokens": tokens[:20] + ["..."] if len(tokens) > 20 else tokens,  # 最初の20トークンのみ表示
            "info": info
        }
        
        if not output_json:
            print(f"ファイル: {file_path}")
            print(f"モデル '{model_name}' でのトークン数: {token_count}")
            print(f"文字数: {len(text)}")
            print(f"文字数とトークン数の比率: {result['char_to_token_ratio']:.2f}:1")
            
            # トークンをrepr()を使用して表示
            token_display = []
            for t in result['tokens'][:10]:
                if isinstance(t, str):
                    token_display.append(repr(t))
                else:
                    token_display.append(str(t))
            
            print(f"サンプルトークン: {', '.join(token_display)}...")
            
        return result
    except Exception as e:
        logging.error(f"ファイル処理エラー: {str(e)}")
        return {
            "file": file_path,
            "error": str(e)
        }

def main():
    parser = argparse.ArgumentParser(description='HuggingFaceのトークナイザーでトークン数をカウントします。')
    parser.add_argument('--text', type=str, help='カウントするテキスト')
    parser.add_argument('--file', type=str, help='カウントするテキストファイルのパス')
    parser.add_argument('--dir', type=str, help='処理するディレクトリ(すべてのテキストファイルを処理)')
    parser.add_argument('--model', type=str, default='gpt2', help='使用するモデル名 (デフォルト: gpt2)')
    parser.add_argument('--slow', action='store_true', help='遅いトークナイザーを使用する')
    parser.add_argument('--json', action='store_true', help='結果をJSON形式で出力')
    parser.add_argument('--output', type=str, help='結果を保存するファイル名')
    parser.add_argument('--verbose', '-v', action='store_true', help='詳細なログを表示')
    args = parser.parse_args()
    
    # ロギング設定
    setup_logging(args.verbose)
    
    use_fast = not args.slow
    results = []
    
    try:
        # ディレクトリ内のすべてのファイルを処理
        if args.dir:
            if not os.path.isdir(args.dir):
                logging.error(f"ディレクトリが見つかりません: {args.dir}")
                return
                
            for root, _, files in os.walk(args.dir):
                for file in files:
                    if file.endswith(('.txt', '.md', '.json', '.py', '.html', '.csv')):
                        file_path = os.path.join(root, file)
                        result = process_file(file_path, args.model, use_fast, args.json)
                        results.append(result)
        
        # 単一ファイルを処理
        elif args.file:
            result = process_file(args.file, args.model, use_fast, args.json)
            results.append(result)
        
        # テキスト直接入力を処理
        elif args.text:
            text = args.text
            token_count, tokens, info = count_tokens(text, args.model, use_fast)
            
            result = {
                "text": text if len(text) <= 50 else text[:47] + "...",
                "model": args.model,
                "token_count": token_count,
                "char_count": len(text),
                "char_to_token_ratio": len(text) / token_count if token_count > 0 else 0,
                "tokens": tokens[:20] + ["..."] if len(tokens) > 20 else tokens,
                "info": info
            }
            
            if not args.json:
                print(f"モデル '{args.model}' でのトークン数: {token_count}")
                print(f"文字数: {len(text)}")
                print(f"文字数とトークン数の比率: {result['char_to_token_ratio']:.2f}:1")
                
                # トークンをrepr()を使用して表示
                token_display = []
                for t in tokens[:10]:
                    if isinstance(t, str):
                        token_display.append(repr(t))
                    else:
                        token_display.append(str(t))
                
                print(f"サンプルトークン: {', '.join(token_display)}...")
                
            results.append(result)
        
        # 入力なしの場合は標準入力を使用
        else:
            text = input("トークン数をカウントするテキストを入力してください: ")
            token_count, tokens, info = count_tokens(text, args.model, use_fast)
            
            if not args.json:
                print(f"モデル '{args.model}' でのトークン数: {token_count}")
                print(f"文字数: {len(text)}")
                print(f"文字数とトークン数の比率: {len(text) / token_count:.2f}:1")
                
                # トークンをrepr()を使用して表示
                token_display = []
                for t in tokens[:10]:
                    if isinstance(t, str):
                        token_display.append(repr(t))
                    else:
                        token_display.append(str(t))
                
                print(f"サンプルトークン: {', '.join(token_display)}...")
        
        # JSON出力
        if args.json or args.output:
            json_result = json.dumps(results, ensure_ascii=False, indent=2)
            
            if args.output:
                with open(args.output, 'w', encoding='utf-8') as f:
                    f.write(json_result)
                print(f"結果を {args.output} に保存しました")
            else:
                print(json_result)
                
    except Exception as e:
        logging.error(f"エラーが発生しました: {str(e)}")
        if args.verbose:
            import traceback
            traceback.print_exc()

if __name__ == "__main__":
    main()

このプログラムは、コマンドラインから直接テキストを入力するか、ファイルからテキストを読み込み、指定したモデルのトークナイザーを使用してトークン数をカウントします。一部結果が文字化けしていますが、言葉の途中でトークン分割を行うためのようです。

実行結果

$ python HF_token_count.py --text "月が綺麗ですね。"
モデル 'gpt2' でのトークン数: 14
文字数: 8
文字数とトークン数の比率: 0.57:1
サンプルトークン: '', '', '', '', '', '', '', '', '', ''...

$ python HF_token_count.py --text "月が綺麗ですね。" --model "rinna/japanese-gpt-neox-3.6b"
2025-03-21 01:04:49,231 - INFO - rinnaモデル用にuse_fast=Falseを設定しました
モデル 'rinna/japanese-gpt-neox-3.6b' でのトークン数: 7
文字数: 8
文字数とトークン数の比率: 1.14:1
サンプルトークン: '', '', '', '', 'です', '', ''...

$ python HF_token_count.py --text "月が綺麗ですね。" --model "weblab-GENIAC/Tanuki-8B-dpo-v1.0"
モデル 'weblab-GENIAC/Tanuki-8B-dpo-v1.0' でのトークン数: 6
文字数: 8
文字数とトークン数の比率: 1.33:1
サンプルトークン: '', '', '', '綺麗', 'ですね', ''...

$ python HF_token_count.py --text "月が綺麗ですね。" --model "ibm-granite/granite-3.2-2b-instruct"
モデル 'ibm-granite/granite-3.2-2b-instruct' でのトークン数: 10
文字数: 8
文字数とトークン数の比率: 0.80:1
サンプルトークン: '', '', '', '', '', '', 'です', '', '', ''...

各種モデル向けのトークナイザー

HuggingFaceトークナイザでは、公開されている多くのLLMモデルで使用することができます。

例えば、以下のようなものは使用できました。

  • gpt2 … OpenAIのGPT-2モデル(GPT-3.5/4に近い)
  • rinna/japanese-gpt-neox-3.6b … rinnaの日本語GPTモデル ‐ weblab-GENIAC/Tanuki-8B-dpo-v1.0 … TANUKIモデル

実行時の注意点

  1. 初回実行時のダウンロード

  2. 実行時には初回の仕様となるモデルの場合には設定ファイルをダウンロードした後に行います。

  3. ダウンロードされた設定ファイルは ~/.cache/huggingface/ に保存され、2回目以降はそれを参照します。

  4. モデル間での違い

  5. 同じテキストでも、モデルによってトークン数が異なります

  6. OpenAIのAPIで使われているモデルと完全に一致するわけではないため、参考値として使用してください

HuggingFaceトークナイザーは、様々なモデルのトークン化方法を試して比較できる点が強みです。

OpenAI互換のトークナイザーtiktokenを使う方法

OpenAIのモデル(GPT-3.5やGPT-4など)を使用している場合、tiktokenというPythonライブラリを使うことで、実際のAPIとほぼ同じトークン数をカウントできます。これはOpenAIが公式に提供しているトークナイザーで、課金や制限に関わるトークン数の計算に最適です。

tiktokenとは

tiktokenは、OpenAIが開発・提供しているPythonライブラリで、OpenAIのモデルが使用するのと同じトークン化アルゴリズムを実装しています。このライブラリを使うことで、APIリクエストを行わずに正確なトークン数をカウントできます。

github.com

主な特徴

  • OpenAIの各モデルに対応した正確なトークン数カウント
  • APIコールなしでローカルで動作
  • 軽量で高速な処理(HuggingFaceのものを使用するより大幅に高速です)

参考

https://raw.githubusercontent.com/openai/tiktoken/main/perf.svg

tiktokenのインストール方法

$ pip install tiktoken

サンプルコード

以下は、tiktokenを使ってトークン数をカウントする基本的なPythonコードです。 モデル名を指定しなかった場合は、デフォルトでgpt-3.5-turboを使用しています。

import tiktoken
import argparse
def count_tokens(text, model="gpt-3.5-turbo"):
    """
    OpenAIモデルのトークナイザーを使用してテキストのトークン数をカウントします。
    
    Args:
        text (str): トークン数をカウントするテキスト
        model (str): 使用するモデル名(例: "gpt-3.5-turbo", "gpt-4")
    
    Returns:
        int: テキストのトークン数
    """
    try:
        encoding = tiktoken.encoding_for_model(model)
    except KeyError:
        # モデルが見つからない場合はcl100k_baseを使用
        encoding = tiktoken.get_encoding("cl100k_base")
        print(f"モデル '{model}' が見つからなかったため、代わりに 'cl100k_base' を使用します")
    
    tokens = encoding.encode(text)
    return len(tokens)
def main():
    parser = argparse.ArgumentParser(description='tiktokenでOpenAIモデルのトークン数をカウントします。')
    parser.add_argument('--text', type=str, help='カウントするテキスト')
    parser.add_argument('--file', type=str, help='カウントするテキストファイルのパス')
    parser.add_argument('--model', type=str, default='gpt-3.5-turbo', 
                        help='使用するモデル名 (デフォルト: gpt-3.5-turbo)')
    
    args = parser.parse_args()
    
    # テキストを取得
    if args.file:
        with open(args.file, 'r', encoding='utf-8') as f:
            text = f.read()
    elif args.text:
        text = args.text
    else:
        text = input("トークン数をカウントするテキストを入力してください: ")
    
    # トークン数をカウント
    token_count = count_tokens(text, args.model)    
    print(f"モデル '{args.model}' でのトークン数: {token_count}")
    print(f"文字数: {len(text)}")
    print(f"文字数とトークン数の比率: {len(text)/token_count:.2f}:1")

if __name__ == "__main__":
    main()

実行例

$ python tiktoken_count.py --text "月が綺麗ですね。"
モデル 'gpt-3.5-turbo' でのトークン数: 12
文字数: 8
文字数とトークン数の比率: 0.67:1

$ python tiktoken_count.py --text "月が綺麗ですね。" --model "gpt-4"
モデル 'gpt-4' でのトークン数: 12
文字数: 8
文字数とトークン数の比率: 0.67:1

$ python tiktoken_count.py --text "月が綺麗ですね。" --model "gpt-4o"
モデル 'gpt-4o' でのトークン数: 7
文字数: 8
文字数とトークン数の比率: 1.14:1

$ python tiktoken_count.py --text "月が綺麗ですね。" --model "o1"
モデル 'o1' でのトークン数: 7
文字数: 8
文字数とトークン数の比率: 1.14:1```

tiktokenは、OpenAIのAPIを使用する際の、最も信頼性の高いトークン数カウント方法といえるでしょう。特にプロンプトの設計やコンテキスト長の制限に関わる計算には、このライブラリはお勧めです。

プロンプトによるトークン節約のヒント

LLMを活用したシステム開発において、トークン数を効率よく使うことは、コスト削減だけでなく、より良いパフォーマンスにつながります。 以下に気をつけて作成するのが良いようです、これはLLMに教えてもらいました🤩

  1. 簡潔な指示を心がける
  2. 構造化されたプロンプト
  3. 短い例示を使う
  4. 段階的な手順を考慮する

これらのテクニックを組み合わせることでよい、生成結果を得られるようです。勉強になるなあ😎

おわりに

トークン数のカウント方法についていろいろ調べてみたのですが、思ったよりも簡単に実装できることがわかりました。 APIを使わなくても、HuggingFaceのライブラリやtiktokenライブラリを使えば、かなり正確なトークン数が計算できます🙌

「あれ?このプロンプト思ったより重かったんだ...」とか「意外とこの日本語の説明、トークン食ってるな」みたいな発見があって楽しいです。 それが進んで、「あ、これなら行けるかも」とか「これはちょっと無理があるな」が事前にわかるようになってくると、時間・お財布ともにストレスが減りそうです🤗