LLM(大規模言語モデル)は文字ではなくトークン(token)というものを単位として処理します。トークンは単語に近いものですが、必ずしも単語とは一致しません。
文字列をトークンに分割するツールをトークナイザ(tokenizer)といいます。OpenAIのLLMでは tiktoken というトークナイザが使われます。tiktoken はいくつかのエンコーディング(トークン分割方式)に対応しており、古くは cl100k_base というエンコーディングが使われていましたが、GPT 4oからは o200k_base に変わりました。
pip install tiktoken
して試してみましょう:
import tiktoken enc = tiktoken.get_encoding("o200k_base") # または enc = tiktoken.encoding_for_model("gpt-4o")
次の例で試してみましょう(山本義隆『熱学思想の史的展開』(現代数学社,1987年)より):
s = "「何人ものニュートンがいた(There were several Newtons)」と言ったのは,科学史家ハイルブロンである.同様にコーヘンは「ニュートンはつねに二つの貌を持っていた(Newton was always ambivalent)」と語っている."
e = enc.encode(s) for i in e: c = enc.decode([i]) if len(c) == 1 and ord(c) == 65533: # 65533は「�」 print(i, end="|") else: print(repr(c)[1:-1], end="|") print()
次のように67トークンに分割されていることがわかります:
「|何|人|もの|ニュ|ート|ン|が|いた|(|There| were| several| Newton|s|)」|と言|った|の|は|,|科学|史|家|ハ|イル|ブ|ロン|で|ある|.|同|様|に|コ|ー�|246|ン|は|「|ニュ|ート|ン|は|つ|ね|に|二|つ|の|貌|を|持|って|いた|(|Newton| was| always| amb|ivalent|)」|と|語|って|いる|.|
日本語は必ずしもUTF-8の文字の境界で切れるわけではなく、上の「コーヘン」のように文字の途中で切れることもあります。
別の例です。これらは文字単位の処理が苦手なLLMの弱点を突くプロンプトとして有名なものです:
How| many| r|'s| are| in| strawberry|?|
「|いっぱい|」の|「|い|」を|「|お|」に|変|えて|ください|。|
このように、" strawberry" や "いっぱい" は1トークンになるので、その中にどういう文字が含まれるかは別に学習していないとこれらの問題は解けません。
日本語ではどんなものがトークンとして登録されているのでしょうか。ちょっと調べてみました。
import tiktoken import re enc = tiktoken.get_encoding("o200k_base") japanese_re = re.compile('[\u3000-\u30ff\u4e00-\u9fff]') # 3000-30ff CJK記号と句読点、ひらがな、カタカナ # 4e00-9fff CJK統合漢字 ## 31f0-31ff カタカナ追加 ## 3220-325f, 3280-33ff CJK文字いろいろ # 3400-4dbf CJK統合漢字拡張A # f900-faff CJK互換漢字 words = {} for i in range(199998): s = enc.decode([i]) words[i] = s sorted_words = sorted(words.items(), key=lambda item: len(item[1]), reverse=True) # for i, s in sorted_words[:100]: # print(i, s) for i, s in sorted_words[:100000]: if japanese_re.search(s): print(i, repr(s)[1:-1])
一部を抜き出します:
(前略) 113862 ありがとうございました 181081 微信公众号天天中彩票 185118 _日本毛片免费视频观看 93926 ありがとうございます 147058 VIPがお送りします 170996 微信上的天天中彩票 187716 微信里的天天中彩票 188394 天天中彩票大神推荐 46669 天天中彩票app 55935 彩神争霸大发快三 (中略) 77298 @お腹いっぱい (中略) 87123 風吹けば名無し (中略) 123086 がお送りします (中略) 44948 名無しさん (後略)
何を学習させたのか想像がついてしまいます。
中国語のものは微信(WeChat)の宝くじ関係のものが多いようですが、アダルト関係も混ざっているようです。
「_日本毛片免费视频观看」は「_日本ポルノ無料動画視聴」という意味ですが、不思議なことにChatGPTはこのトークンが読めないようです。「_日本毛片免费视频观看」を日本語に訳してくれと言っても何も見えないらしいのです。旧トークナイザでは「SolidGoldMagikarp」などがChatGPTが読めないトークンとして有名でしたが、新トークナイザでも同じようなことが起こっているのかもしれません。
大規模言語モデル PLaMo 2 のためのトークナイザ性能改善という記事を見て、PLaMo 2のトークナイザも試してみたくなりました。
from mlx_lm import load, generate model, tokenizer = load("mlx-community/plamo-2-8b-bf16", tokenizer_config={"trust_remote_code": True}) tokenizer.add_eos_token("<|plamo:bos|>") s = "「何人ものニュートンがいた(There were several Newtons)」と言ったのは,科学史家ハイルブロンである.同様にコーヘンは「ニュートンはつねに二つの貌を持っていた(Newton was always ambivalent)」と語っている." e = tokenizer.encode(s) for i in e: c = tokenizer.decode(i) print(repr(c)[1:-1], end="|") print()
<|plamo:bos|>|「|何人|も|の|ニュー|トン|がいた|(|There were| several| Newton|s|)」|と言った|の|は,|科学|史|家|ハイ|ル|ブロン|である|.|同様に|コー|ヘン|は「|ニュー|トン|は|つねに|二つ|の|貌|を持っていた|(|Newton| was always| |ambi|valent|)|」と|語っている|.|
words = {} for i in range(100000): s = tokenizer.decode(i) words[i] = s sorted_words = sorted(words.items(), key=lambda item: len(item[1]), reverse=True) # for i, s in sorted_words[:100]: # print(i, s) for i, s in sorted_words[:10000]: if japanese_re.search(s): print(i, repr(s)[1:-1])
58984 いただきありがとうございました。 68757 あけましておめでとうございます。 54912 いただきありがとうございます。 66397 明けましておめでとうございます 51379 お気軽にお問い合わせください 59328 本当にありがとうございました 63073 してみてはいかがでしょうか。 74903 いつもありがとうございます。 78833 いただきありがとうございます 81104 ことができるようになります。 85013 もいるのではないでしょうか。 50286 よろしくお願いいたします。 57225 させていただいております。 64602 お気軽にお問い合わせ下さい 70922 することをおすすめします。 75898 できるようになっています。 76399 というわけではありません。 85032 いつもありがとうございます 48448 ありがとうございました。 58809 できるようになりました。 58983 心よりお待ちしております 61422 みてはいかがでしょうか。 61959 するものではありません。 65294 よろしくお願いいたします 65859 お気軽にお問合せください 66682 頂きありがとうございます 68465 させていただきますので、 70634 をさせていただきました。 70678 YouTubeチャンネル 75441 させていただいています。 (後略)
OpenAIのものと比べて、とてもまともです。