みたことないファイルフォーマットTOONとやらを試してみる
今回はふと以下の記事で見かけたTOONというファイルフォーマットが気になり調べてみました。普段JSONやYAMLは利用しますがTOONというのは聞いたことすら無kったので、いかなるものか時になり調べてみました。
TOONとは?
TOONのスペックをまとめたGitHubで以下のような説明がありました。
トークン指向オブジェクト表記法(TOON)は、構造化データを大規模言語モデル(LLM)に渡すために設計された、コンパクトで人間が読めるシリアル化形式です。トークン使用量を大幅に削減します。LLMの入力用であり、出力用ではありません。
TOONの強みは、オブジェクトの均一配列、つまり行ごとに複数のフィールドを持ち、すべての項目で同じ構造を持つことです。TOONは、ネストされたオブジェクトにはYAMLのインデントベースの構造を、均一なデータ行にはCSVの表形式を採用し、LLMコンテキストにおけるトークン効率のために両方を最適化します。深くネストされたデータや不均一なデータの場合は、JSONの方が効率的かもしれません。
なるほど、LLMを利用する前提でうまくトークンを扱うために考案されたデータフォーマットであるとのことです。確かにLLMを利用する上でトークン使用量は一つ大きな課題であり、少ないトークン量で多くの情報量を渡すことができればコストの観点からも生成されるコンテンツの分量に対してもメリットがあります。
主な特徴は以下とのことです。
- 標準JSONと比較してトークン数を30~60%削減
- 最小限の構文を用いており、冗長な句読点(中括弧、角括弧、ほとんどの引用符)を削除
- 表形式配列で表現され、CSVのような行形式で統一されたオブジェクトコレクションを実現
- 明示的なメタデータを採用しており検証用の配列長インジケータ[N]のようなインジケータを利用
- LLM対応を前提としており、トークン数を削減しながらセマンティクスの明確さを維持
- オリジナルのTypeScript実装と100%互換性あり
より詳しくは以下のGitHubをご覧ください。
早速使ってみる
今回はPython用のTOON向けライブラリであるpython-toonを使います。以下にあるコードをベースに検証してみます。
環境構築
uvを利用して環境を構築しました。
uv init python_toon_tutorial -p 3.12
cd python_toon_tutorial
uv add python-toon
データのエンコード
それでは早速使ってみましょう。基本的なデータ型(リストや辞書など)は取り扱うことができます。
早速最低限のデータをエンコードさせてみましょう。
from toon import encode
data = {"name": "Alice", "age": 30}
print(encode(data))
toonを使ってデータをエンコードするにはtoon.encode関数を利用します。最初のエンコード対象は名前と年齢を保持させた辞書を対象に実行してみます。実行すると以下のようなフォーマットにエンコードされました。
uv run encode_dict.py
# 結果
name: Alice
age: 30
パッとみると単純に名前と年齢を順に出力しているだけに見えますが、TOONでは先ほどの辞書はこちらのようにエンコードされます。
次は先ほどの辞書と同じようなデータを複数もつリストをエンコードしてみましょう。
from toon import encode
users = [
{"id": 1, "name": "Alice", "age": 30},
{"id": 2, "name": "Bob", "age": 25},
{"id": 3, "name": "Charlie", "age": 35},
]
print(encode(users))
実行すると以下のような結果になりました。
uv run encode_list.py
# 結果
[3,]{id,name,age}:
1,Alice,30
2,Bob,25
3,Charlie,35
実行するとまず[3,]のような表記があります。これは特徴の方で説明したインジケーターであり、右に示すスキーマのデータ件数を明示しています。また{id,name,age}においてデータのスキーマを表しています。それ以下の部分ではリストの内容ごとにデータがCSVの形式で記載されています。
ふと気になり、リスト内のデータのスキーマを全て変えてみました。
from toon import encode
users = [
{"id": 1, "name": "Alice", "age": 30},
{"id": 2, "name": "Bob", "height": 25},
{"id": 3, "name": "Charlie", "weight": 35},
]
print(encode(users))
こちらを実行すると以下のような結果になりました。インジケータはデータ数を3と表示できていますが、データのスキーマについては提示されていないことが確認できました。もちろん全てのデータのスキーマが違うのでスキーマを明示できないのはわかっていましたが、このような結果になるということで、このデータをLLMに入力することはないと思いますが、スキーマを統一するとデータが以下に圧縮できるか感じることができます。
uv run encode_inconsistent_schema.py
# 結果
[3]:
- id: 1
name: Alice
age: 30
- id: 2
name: Bob
height: 25
- id: 3
name: Charlie
weight: 35
データについては深いネストを取り扱うこともできます。
from toon import encode
data = {
"metadata": {"version": 1, "author": "test"},
"items": [
{"id": 1, "name": "Item1"},
{"id": 2, "name": "Item2"},
],
"tags": ["alpha", "beta", "gamma"],
}
print(encode(data))
こちらを実行すると以下のような結果になりました。metadataは一見しかデータがないのでインジケータ話、itemsは同じスキーマのデータが二つあるので[2,]というインジケータ、tagsは3つのデータからなるリストになっており[3]というインジケータになっています。
uv run encode_nest.py
# 結果
metadata:
version: 1
author: test
items[2,]{id,name}:
1,Item1
2,Item2
tags[3]: alpha,beta,gamma
データのデコード
データをデコードするにはtoon.decodeを利用します。先ほどエンコードしていた全てのデータをエンコードしてからデコードするコードを以下に示します。
from pprint import pprint
from toon import decode, encode
data = {"name": "Alice", "age": 30}
pprint(decode(encode(data)))
users = [
{"id": 1, "name": "Alice", "age": 30},
{"id": 2, "name": "Bob", "age": 25},
{"id": 3, "name": "Charlie", "age": 35},
]
pprint(decode(encode(users)))
users = [
{"id": 1, "name": "Alice", "age": 30},
{"id": 2, "name": "Bob", "height": 25},
{"id": 3, "name": "Charlie", "weight": 35},
]
pprint(decode(encode(users)))
data = {
"metadata": {"version": 1, "author": "test"},
"items": [
{"id": 1, "name": "Item1"},
{"id": 2, "name": "Item2"},
],
"tags": ["alpha", "beta", "gamma"],
}
pprint(decode(encode(data)))
実行すると、以下のように元のデータが復元できていることが確認できます。
uv run decode.py
# 結果
{'age': 30, 'name': 'Alice'}
[{'age': 30, 'id': 1, 'name': 'Alice'},
{'age': 25, 'id': 2, 'name': 'Bob'},
{'age': 35, 'id': 3, 'name': 'Charlie'}]
[{'age': 30, 'id': 1, 'name': 'Alice'},
{'height': 25, 'id': 2, 'name': 'Bob'},
{'id': 3, 'name': 'Charlie', 'weight': 35}]
{'items': [{'id': 1, 'name': 'Item1'}, {'id': 2, 'name': 'Item2'}],
'metadata': {'author': 'test', 'version': 1},
'tags': ['alpha', 'beta', 'gamma']}
エンコードのオプション
エンコーディングのオプションは以下のようなものがあります。
-
indent: インデントのサイズ -
delimiter: データを区切る文字(デフォルトはコンマ) -
lengthMarker: 任意のマーカープレフィックス(デフォルトはFalse)
まずはインデントの設定をみてみます。indentを指定すると、インデントが発生した時のスペースの数を指定できます。
from toon import encode
data = {
"metadata": {"version": 1, "author": "test"},
"items": [
{"id": 1, "name": "Item1"},
{"id": 2, "name": "Item2"},
],
"tags": ["alpha", "beta", "gamma"],
}
encode(data, {"indent": 2})
# 結果
# metadata:
# version: 1
# author: test
# items[2,]{id,name}:
# 1,Item1
# 2,Item2
# tags[3]: alpha,beta,gamma
encode(data, {"indent": 4})
# 結果
# metadata:
# version: 1
# author: test
# items[2,]{id,name}:
# 1,Item1
# 2,Item2
# tags[3]: alpha,beta,gamma
次にdelimiterの設定をしてみます。これを指定することでデータの区切りの記号を全て変更できます。
from toon import encode
data = {
{"id": 1, "name": "Item1"},
{"id": 2, "name": "Item2"},
]
encode(data, {"delimiter": "\t"})
# 結果
# [2 ]{id name}:
# 1 Item1
# 2 Item2
encode(data, {"delimiter": "|"})
# 結果
# [2|]{id|name}:
# 1|Item1
# 2|Item2
最後にmarkerLengthを変更してみます。lengthMarkerに#を入れるとインジケータ部分に#が含まれ、そうでない場合は数値だけになっています。
from toon import encode
users = [
{"id": 1, "name": "Alice"},
{"id": 2, "name": "Bob"},
]
encode(users, {"lengthMarker": "#"})
# 結果
# [#2,]{id,name}: <- #2となっている
# 1,Alice
# 2,Bob
encode(users)
# 結果
# [2,]{id,name}:
# 1,Alice
# 2,Bob
ファイルへの読み書き
ファイルの拡張子は.toonで、読み書きは以下のようにして行います。
from toon import encode, decode
users = [
{"id": 1, "name": "Alice"},
{"id": 2, "name": "Bob"},
]
# 書き込み
with open("users.toon", "w") as f:
f.write(encode(users))
# users.toon
# [2,]{id,name}:
# 1,Alice
# 2,Bob
# 読み込み
with open("users.toon", "r") as f:
users = decode("\n".join(f.readlines()))
JSONファイルとTOONファイルの変換
JSONとTOONでは相互に変換することができます。以下のようにすることで変換できます。
# JSONデータからTOONデータへ
uv run toon input.json -o output.toon
# TOONデータからJSONデータへ
uv run toon input.toon -o output.json
そのほかの使い方についてはGitHubに載っていますのでご覧ください。
まとめ
今回はTOONフォーマットについて調べてみました。LLM向けに考案されたフォーマットなので次回はLLMに入力してトークン使用量や結果の差異などを調べてみようと思います。
Discussion