はじめに
うにおん!ならぬ、うにこーど! nikkieです。
先日ChatGPTがどのように日本語テキストをトークン化するのか覗きました。
トークンのIDから対応するテキストを見ようとPythonのbytesを扱ったわけですが、その中で感じた疑問についてアウトプットです。
目次
- はじめに
- 目次
- あ(U+3042)をencodeするとb'\xe3\x81\x82'
- 続く文字を変換していくと規則性がある
- 脱線:こんなところでも見かけます
- 変換規則:1110 xxxx 10xx xxxx 10xx xxxx
- ひらがな・カタカナ・漢字には、1110 xxxx 10xx xxxx 10xx xxxxの3バイト変換が適用される
- 終わりに
- P.S. Unicodeまわりの参考文献
あ(U+3042)をencodeするとb'\xe3\x81\x82'
ひらがなの「あ」はUnicodeのコードポイント(符号位置)がU+3042
です。
※この記事におけるPythonのバージョンは 3.10.9 です
>>> hex(ord("あ")) '0x3042'
- コードポイントの表記は
U+<16進数>
です - 組み込み関数
ord
で1文字の「Unicode コードポイントを表す整数」(10進)が返ります - 組み込み関数
hex
で16進に変換します
ひらがなの「あ」(str
)をbytes
に変換すると
>>> "あ".encode() b'\xe3\x81\x82'
https://docs.python.org/ja/3/library/stdtypes.html#str.encode
- 第1引数のデフォルト値はUTF-8です
>>> "あ".encode("utf8") b'\xe3\x81\x82'
続く文字を変換していくと規則性がある
「あ」の後のひらがなについてもコードポイントとbytes
を見ていきましょう
>>> hex(ord("い")) '0x3044' >>> "い".encode() b'\xe3\x81\x84' >>> hex(ord("う")) '0x3046' >>> "う".encode() b'\xe3\x81\x86' >>> hex(ord("え")) '0x3048' >>> "え".encode() b'\xe3\x81\x88' >>> hex(ord("お")) '0x304a' >>> "お".encode() b'\xe3\x81\x8a'
ひらがなが1文字進むと、コードポイントもbytesも2ずつ増加していますよね!
1の位(一番右の桁)は同じ値です(2 -> 4 -> 6 -> 8 -> a)
これを見て、「コードポイントからbytesへの変換規則がなにかあるんじゃないか?」と、私、気になりました!
脱線:こんなところでも見かけます
Python標準ライブラリの中でもUnicodeのコードポイントやそれをバイトに変換した値が登場します
jsonでUnicodeコードポイント
>>> json.dumps({"key": "あい"}) '{"key": "\\u3042\\u3044"}' >>> json.dumps({"key": "あい"}, ensure_ascii=False) '{"key": "あい"}'
ensure_ascii
引数はデフォルト値がTrue
です。
https://docs.python.org/ja/3/library/json.html#json.dump
ensure_ascii が (デフォルト値の) true の場合、出力では入力された全ての非 ASCII 文字はエスケープされていることが保証されています。ensure_ascii が false の場合、これらの文字はそのまま出力されます。
エスケープされる=Unicodeコードポイントでの出力ということですね。
urllib.parseでバイト列
URLに日本語を使ったときにはバイト列に変換されています。
「URLエンコーディング」や「Percent-Encoding(RFC 3986)」というそうです。
- ブラウザのURLバーの表示:https://example.com/page/あい
- 実際は
https://example.com/page/%E3%81%82%E3%81%84
URLエンコーディングはurllib.parse.urlencode
というまさにそれという関数がありますね!
https://docs.python.org/ja/3/library/urllib.parse.html#urllib.parse.urlencode
>>> import urllib.parse >>> urllib.parse.urlencode({"key": "あい"}) 'key=%E3%81%82%E3%81%84'
何らかの規則でURLエンコーディングされていると思っていましたが、文字列をbytesにencodeするのと同じ規則だったのですね!
変換規則:1110 xxxx 10xx xxxx 10xx xxxx
調べた末に以下に行き着きました。
UTF-8の符号化方法から引用します。
UTF-8は, Code pointを1~4bytesの可変長で変換します.
U+0800 ~ U+FFFFの範囲のコードポイントは、以下のように3バイトに変換されるそうです。
1110 xxxx 10xx xxxx 10xx xxxx
変換規則の適用例:「あ」
- あ(U+3042)は、U+0800 ~ U+FFFFの範囲なので3バイトに変換される
- U+3042のビット表記は 0011 0000 0100 0010
- 1バイト目:1110 0011(
E3
)- U+3042のビット表記から先頭4ビットが取られる
- 2バイト目:1000 0001(
81
)- U+3042のビット表記から5ビット目〜10ビット目が取られる
- 3バイト目:1000 0010(
82
)- U+3042のビット表記から11ビット目〜最後が取られる
- 3バイトは E38182
"あ".encode()
で見たbytesだ!!!!
他の例:「お」
- お:U+304A は、U+0800 ~ U+FFFFの範囲内 -> 3バイトに変換
- U+304Aのビット表記 0011 0000 0100 1010
- 1バイト目:1110 0011(
E3
) - 2バイト目:1000 0001(
81
) - 3バイト目:1000 1010(
8A
) - 👉 E3818A
もひとつ他の例:「誕」
>>> hex(ord("誕")) '0x8a95' >>> "誕".encode() b'\xe8\xaa\x95'
- 誕:U+8A95は、U+0800 ~ U+FFFFの範囲内 -> 3バイトに変換
- U+8A95のビット表記 1000 1010 1001 0101
- 1バイト目:1110 1000(
E8
) - 2バイト目:1010 1010(
AA
) - 3バイト目:1001 0101(
95
) - 👉 E8AA05
ひらがな・カタカナ・漢字には、1110 xxxx 10xx xxxx 10xx xxxxの3バイト変換が適用される
U+0800 ~ U+FFFFの範囲には、ひらがな・カタカナ・漢字は含まれると理解しました。
- Unicodeの基本多言語面(BMP)にあたる
- 含まれる文字
- ひらがな:U+3040~U+309F
- カタカナ:U+30A0~U+30FF
- 漢字
- CJK統合漢字:U+4E00~U+9FFF
- 他:U+3400~U+4DBF、U+F900~U+FAFF
終わりに
chr(0x3042).encode()
がb'\xe3\x81\x82'
となる(UTF-8の)変換規則が気になり調べたところ、完全に理解できました!
Unicodeのコードポイントとしては16進4桁(2バイト)ですが、変換規則により8ビット(=4+2+2)追加されるのでbytesは3バイトになるわけですね。
ChatGPTのトークンから興味を持ったわけですが、UTF-8で変換したバイト列はURLエンコーディングなど他の箇所でも見かけることに気づきました。
文字コードは(戻ってこれないかもしれないほどの)深い世界に見えていましたが、やはり陰ながら支えてくれていたんだなあという実感です(しみじみ)
ChatGPTのトークン化を覗いて
— nikkie にっきー (@ftnext) 2023年4月23日
>>> "あ".encode()
b'\xe3\x81\x82'
bytesの扱いを知り
「あ」のUnicodeコードポイントってU+3042だけどここからバイト列にする変換規則ってあるのかなとハマっていった末に見つけたこちらhttps://t.co/t3Y9GXS4eP
1110 xxxx 10xx xxxx 10xx xxxx と変換。疑問解決🙌