2024年12月5日 星期四

OpenAI API 學習筆記 : 計算 GPT 模型的 token (字符) 數

最近 Gradio 學得差不多了, 準備回來好好讀一讀今年三月菁菁買給我的幾本 AI 書籍, 學習 OpenAI 的 API 用法. 我在 9/16 時在 OpenAI API 儲值了 10 美元, 主要是測試 MicroPython 的 xtools 函式庫串接 API 之用, 今天檢視 Billing 資訊, 現在還有 9.99 美元可用額度 : 




本系列之前的測試文章參考 :


今天看完 token 部分後做個測試紀錄備忘. 


1. Token 是甚麼 : 

Token (字符或標記) 是大語言模型裡處理自然語言時切分文本的最基本單位, 用來代表文本中的字, 詞, 詞組, 數字, 符號或標點符號, 其具體形式取決於語言模型所使用的自然語言處理演算法, 例如傳統的 one-hot 向量, 或者經機器學習映射得到的 embedding 向量以及處理中文常用的 Byte Pair Encoding 演算法等, 但目前最常見的 token 形式是使用整數 ID 來表示, 例如 ChatGPT, Claude 等商用模型的每個 token 都被分配了一個 ID 序號, 用來表示此 token 在模型詞彙表 (vocabulary) 中的索引. 一個字或符號與 token 並非簡單的一對一關係, 可能是一對一, 多對一, 也可能一對多, 取決於模型使用之切詞演算法. 


2. OpenAI 的 tokenizer 頁面 : 

OpenAI 提供其各種語言模型的 token 查詢網頁 : 


輸入文本例如 'Team Taiwan' 會顯示 token 數為 2 (可見這時字與 token 是一對一關係) 與字元數為 11, 下方預設會以不同顏色標示這兩個字在 tokenization 時的切分 (segmentation) 情形 : 




切到 Token IDs 頁籤會顯示代表這兩個字的整數 token ID :




這是這兩個字在預設模型 GPT-4o 與 GPT-4o mini 共用的字彙表中的識別號碼 :




如果切到 GPT-3.5 & GPT-4 模型的話 token 編號就不同了 :




當然 GTP-3 也不一樣 :




如果輸入中文 "你好嗎" 在不同模型下的 tokenization 有很大不同, GPT-4o 與 GPT-4o mini 會將這 3 個字切成 2 個 token :




GPT-3.5 & GPT-4 模型則將這 3 個字切成 5 個 token : 




GTP-3 模型則是切成 7 個 token : 




Token 是現今生成式 AI 的基礎, 由於電腦只能對數值進行運算, 無法直接對各式編碼的文本進行計算, 所以必須將文字先轉成數值化的 token, 透過大語言模型普遍使用的 Transformer 編解碼器結構來找出文字序列的長距離相依關係然後生成回應. 商用模型例如 ChatGPT 也是依據查詢與回應的總 token 數來計費. 


3. 安裝 tiktokenizer 套件 : 

除了上面的  token 查詢網頁外, OpenAI 也提供了 tiktoken 套件讓開發者能使用 Python 程式計算確切的 token 數量以利控管費用或避免超出模型的最大容許 token 長度, 此套件可用 pip 安裝 :

pip install tiktoken

D:\python\test>pip install tiktoken    
Collecting tiktoken
  Downloading tiktoken-0.8.0-cp310-cp310-win_amd64.whl.metadata (6.8 kB)
Requirement already satisfied: regex>=2022.1.18 in c:\users\tony1\appdata\local\programs\thonny\lib\site-packages (from tiktoken) (2023.12.25)
Requirement already satisfied: requests>=2.26.0 in c:\users\tony1\appdata\roaming\python\python310\site-packages (from tiktoken) (2.31.0)
Requirement already satisfied: charset-normalizer<4,>=2 in c:\users\tony1\appdata\roaming\python\python310\site-packages (from requests>=2.26.0->tiktoken) (3.2.0)
Requirement already satisfied: idna<4,>=2.5 in c:\users\tony1\appdata\roaming\python\python310\site-packages (from requests>=2.26.0->tiktoken) (3.4)
Requirement already satisfied: urllib3<3,>=1.21.1 in c:\users\tony1\appdata\local\programs\thonny\lib\site-packages (from requests>=2.26.0->tiktoken) (1.26.19)
Requirement already satisfied: certifi>=2017.4.17 in c:\users\tony1\appdata\roaming\python\python310\site-packages (from requests>=2.26.0->tiktoken) (2023.7.22)
Downloading tiktoken-0.8.0-cp310-cp310-win_amd64.whl (884 kB)
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 884.2/884.2 kB 3.3 MB/s eta 0:00:00
Installing collected packages: tiktoken
Successfully installed tiktoken-0.8.0

檢視版本 : 

>>> import tiktoken     
>>> tiktoken.__version__     
'0.8.0'

用 dir() 檢視套件成員 : 

>>> dir(tiktoken)    
['Encoding', '__builtins__', '__cached__', '__doc__', '__file__', '__loader__', '__name__', '__package__', '__path__', '__spec__', '__version__', '_tiktoken', 'core', 'encoding_for_model', 'encoding_name_for_model', 'get_encoding', 'list_encoding_names', 'model', 'registry']

計算 token 數要先呼叫 encoding_for_model() 函式並傳入模型名稱來建立一個 Encoding 物件 : 

>>> encoder=tiktoken.encoding_for_model('gpt-3.5-turbo')   
>>> type(encoder)     
<class 'tiktoken.core.Encoding'>   

檢視 Encoding 物件成員 : 

>>> dir(encoder)   
['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__getstate__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__setstate__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', '_core_bpe', '_encode_bytes', '_encode_only_native_bpe', '_encode_single_piece', '_mergeable_ranks', '_pat_str', '_special_tokens', 'decode', 'decode_batch', 'decode_bytes', 'decode_bytes_batch', 'decode_single_token_bytes', 'decode_tokens_bytes', 'decode_with_offsets', 'encode', 'encode_batch', 'encode_ordinary', 'encode_ordinary_batch', 'encode_single_token', 'encode_with_unstable', 'eot_token', 'max_token_value', 'n_vocab', 'name', 'special_tokens_set', 'token_byte_values']

其中較常用的屬性如下 : 
  • name : 此模型編碼器的名稱
  • n_vocab : 此模型編碼器詞彙表中的詞彙總數
  • max_token_value : 編碼器的 token ID 的最大值 (超過此 ID 會出現越界錯誤)
  • encoder.eot_token : 代表模型輸出回應結束之特殊 token 值 End of Text (EOT)
  • special_tokens_set : 編碼器支援的特殊 token 集合例如 BOS (beginning of sentence), EOS (end of sentence), PAD 等特殊符號
例如 GPT-3.5 Turbo 模型的編碼器屬性如下 : 

>>> encoder.name   
'cl100k_base'  
>>> encoder.n_vocab    
100277
>>> encoder.max_token_value    
100276
>>> encoder.eot_token   
100257
>>> encoder.special_tokens_set     
{'<|endoftext|>', '<|fim_prefix|>', '<|fim_suffix|>', '<|endofprompt|>', '<|fim_middle|>'}

呼叫其 encode() 方法可將文本轉換成 token, 例如 : 

>>> tokens=encoder.encode('你好嗎')   
>>> tokens       
[57668, 53901, 161, 245, 236]

從傳回的串列可知編碼後的 token 與其數量, 這與用 OpenAI 的 token 網頁查詢的結果是一樣的 : 




呼叫 Encoding 物件的 decode() 方法並傳入此 token 串列則可從 token 反查文本 : 

>>> encoder.decode(tokens)     
'你好嗎'

但是如果用 OpenAI API 提出 "你好嗎" 提示詞請求時, 卻發現 prompt 的 token 數是 12 而非 5 :

>>> from openai import OpenAI    
>>> api_key="我的 API key"     
>>> client=OpenAI(api_key=api_key)      
>>> chat_completion=client.chat.completions.create(
    messages=[
        {"role": "user",
         "content": "你好嗎",
        }],
    model="gpt-3.5-turbo",
    )
>>> print(chat_completion)     
ChatCompletion(id='chatcmpl-AazYMWwpv6n1brR1eDegTyw2T3tH8', choices=[Choice(finish_reason='stop', index=0, logprobs=None, message=ChatCompletionMessage(content='你好,我是AI助手,很高興能夠跟你交談。有什麼可以幫助你的嗎?', role='assistant', function_call=None, tool_calls=None, refusal=None))], created=1733379782, model='gpt-3.5-turbo-0125', object='chat.completion', system_fingerprint=None, usage=CompletionUsage(completion_tokens=43, prompt_tokens=12, total_tokens=55, prompt_tokens_details={'cached_tokens': 0, 'audio_tokens': 0}, completion_tokens_details={'reasoning_tokens': 0, 'audio_tokens': 0, 'accepted_prediction_tokens': 0, 'rejected_prediction_tokens': 0}))

這是因為提示詞經過編碼器轉成 token 後並非直接送入模型, 而是要用 ChatML 語法包起來再送進去模型, 格式如下 :

<|im-start|>{角色}\n{訊息}<|im-end|>

其中角括號包起來的是特殊 token, 角色例如 user, system, 或 assistant, \n 是跳行字元, 它們都各占一個 token 數, 所以一個訊息的 token 基本上會被 4 個特殊 token 包起來, 如果有多個訊息, 它們會用 ChatML 包起來後再串接, 最後還要串接由 3 個特殊 token 組成的結尾包 :

<|start|>assistant\n

所以上面的提示詞 "你好嗎" 會先用 ChatML 語法包成這樣 : 

<|im-start|>user\n你好嗎<|im-end|><|start|>assistant\n

其中 "你好嗎" 經 GPT 3.5 Turbo 的編碼器轉成 5 個 token, 加上包覆訊息的 4 個特殊 token 與結尾的 3 個特殊 token 後總共是 4+5+3=12 個 token, 這就是 prompt_tokens=12 的由來. 

沒有留言:

張貼留言