SONUB
JA English 한국어 日本語 中文 हिन्दी বাংলা اردو Español Português Français Deutsch Русский العربية Bahasa Indonesia Tiếng Việt ไทย Türkçe

通知を有効にする方法

このブラウザーまたはアドレスでは通知を利用できません。対応ブラウザーで安全な接続のSONUBサイトを開いてください。

通知がブロックされている場合は、ブラウザーの設定で許可してください。

  1. アドレスバー横のサイト設定、またはブラウザーのサイト設定を開き、このサイトの通知を許可に変更してください。
  2. 端末の設定でブラウザーの通知を許可し、おやすみモードや集中モードを確認してください。
  3. SONUBに戻ってページを再読み込みしてください。通知が有効になるとベルは表示されなくなります。

iPhoneやiPadではSONUBをホーム画面に追加し、そこから開いて通知を許可してください。

ログイン ログアウト 表示テーマ 標準テーマ ダークテーマ ピンクテーマ チャット 1:1 チャット グループチャット 掲示板 お知らせ ニュース掲示板 ご意見掲示板 自己紹介 メインメニュー

さあ、あなただけの人工知能を作ってみましょう。

自分のテキストから辞書を学習します。tokenizer.py で最も多く並ぶ文字の組を結合し、エンコード → デコードで元の文章に戻るか確かめましょう。

単語のブロックが一列に並び、最後に空きマスがある。その上に高さのちがう棒がついた候補ブロックが3つあり、そのうち1つが選ばれて列の最後につながり、矢印が最初に戻ってまた次を選ぶ。
4

BPEトークナイザー

文字から始め、隣り合う頻出ペアを順に合併します。も同じ順序で規則を適用します。

は合併せず未知文字は 0です。完全な単位実装ではありません。辞書と規則を一緒に保存し、後に別の辞書へ交換しません。

なぜトークン化するのか — コンピューターは数字でしか計算できない

は足し算と掛け算しかできません。"cat" という文字では計算できないので、文章を整数の番号のに変える必要があります。この仕事をがします。

  • encode: 文章 → 番号のリスト。基本資料で学んだトークナイザーでは "cat" → [9, 7, 23]。
  • decode: 番号のリスト → 文章。[9, 7, 23] → "cat"。

番号が必要な場所は3つです。

  • : 番号で表の1行を取り出します(5段階)。
  • : 語彙の番号ごとに点数を出します。番号の個数が、そのまま選べる次のの個数です。
  • 損失: 正解の番号のを見ます(6段階)。

番号そのものには意味がありません。9番と10番が似た文字だという保証はありません。意味は学習を通して、その番号の埋め込みに生まれます。

どんな単位で切るか — 文字・単語・サブワード

トークンの単位を決める方法は3つです。

  • 文字単位: 語彙が小さく(英語なら数十個)、知らない文字がほとんどありません。その代わり文章が長くなり、モデルが一度に見られる内容が減り、文字から単語の意味を組み立てる仕事をモデルがすべてしなければなりません。
  • 単語単位: 文章は短くなりますが、語彙が数十万個に大きくなり、辞書にない単語(新しい名前・誤字・)を扱えません。
  • サブワード単位( など): よく出るかけらは1トークンに、まれな単語は複数のかけらに分けます。語彙の大きさと文章の長さのバランスがとれ、今日のほとんどのが使っています。

自分の資料でトークナイザーを学習する理由もここにあります。汎用のトークナイザーは主に英語の文章で学んでいるので、他の言語や特殊な分野の文章を細かく刻みます。例えば " 150k" は求人の文章で学んだトークナイザーでは3トークン、汎用のトークナイザーでは6トークンになります。トークンが少ないほど、同じの長さにより多くの内容が入り、学習も速くなります。

BPE アルゴリズム — 最もよく隣り合うペアを合わせる

BPE(Byte Pair Encoding)は文字から始め、最もよく隣り合って出てくる2つのかけらを新しいトークン1つに合わせることを繰り返します。

  • 最初の語彙は、資料に出てきた文字1つ1つです。
  • 資料全体で、隣り合う2つのトークンのペアをすべて数えます。
  • 最も多く出たペアを新しいトークンとして語彙に加え、資料のそのペアをすべて新しいトークンに置き換えます。
  • 決めた回数(settings.json の merges)だけ、または2回以上出るペアがなくなるまで繰り返します。

下の実験は "low lower lowest" で4回合併します。16トークンだった文章が合併のたびに短くなり、6トークンになります。l+o → lo、lo+w → low のように、前に合わせたトークンが次の合併の材料になり、だんだん長いかけらができる様子を見てください。3回目の合併 " "+"low" のように、空白もかけらにつきます。 は空白で先に分けないからです。実際のは、ふつう先に空白・バイト単位で分けてから(事前分割、pre-tokenizer)、その中だけで合併します。

自分で確かめる lab/bpe_trace.py
from tokenizer import BPE, merge

text = 'low lower lowest'
tok = BPE.train(text, 4)
ids = [tok.chars[c] for c in text]
show = lambda ids: '|'.join(tok.vocab[i] for i in ids)
print('start:', show(ids), len(ids))
for (a, b), new_id in tok.merges:
    ids = merge(ids, (a, b), new_id)
    print(repr(tok.vocab[a]), '+', repr(tok.vocab[b]), '->', show(ids), len(ids))

~/xslm に lab フォルダーを作り、同じ名前のファイルに自分で入力してから実行してください。 -m lab.名前 は(~/xslm)を の検索先に入れるので、前の段階で作った tokenizer.py・engine.py・model.py をそのまま読み込めます。python3 lab/名前.py で実行すると lab フォルダーの中だけを探すため、 になります。

cd ~/xslm
mkdir -p lab
micro lab/bpe_trace.py
python3 -m lab.bpe_trace

基本の資料と設定で実行した結果です。資料や設定を変えた場合は数字が変わります。

start: l|o|w| |l|o|w|e|r| |l|o|w|e|s|t 16
'l' + 'o' -> lo|w| |lo|w|e|r| |lo|w|e|s|t 13
'lo' + 'w' -> low| |low|e|r| |low|e|s|t 10
' ' + 'low' -> low| low|e|r| low|e|s|t 8
' low' + 'e' -> low| lowe|r| lowe|s|t 6

語彙と番号 — tokenizer.json に入っているもの

学習を終えたトークナイザーは2つのものを保存します。

  • : 番号の順に並んだトークンのリストです。リストの位置がそのまま番号です。
  • merges: 合併規則のリストです。規則1つは [[前の番号, 後ろの番号], 新しい番号] です。

番号はこうつけます。

  • 0番はいつも � です。語彙にない文字の代わりです。
  • 1番からは、学習資料に出てきた文字を並べ替えた順です。基本資料は改行・空白・ピリオド・コロン・大文字 A・Q・小文字21個、合わせて27文字が1〜27番です。
  • その次は合併した順につきます。基本資料の最初の合併は ":"+" " → ": " (28番)、2つ目は "Q"+": " → "Q: " (29番)、3つ目は "A"+": " → "A: " (30番)です。

だから 語彙の数 = 1 + 文字の種類 + 合併の数 で、基本資料は 1 + 27 + 12 = 40 です。並べ替えて番号をつけるので、同じ資料でもう一度学習すれば、いつも同じ番号になります。

エンコード — 規則を学んだ順にもう一度適用する

新しい文章を番号に変えるときは、まず文字ごとに番号を探し(知らない文字は0)、保存した合併規則を学んだ順に1つずつ適用します。

順序が大事なのは、前の規則が作ったトークンを後ろの規則が材料に使うからです。"Q: " を作る規則(Q + ": ")は、": " が先に作られていないと当てはまりません。順序を変えると、学習時と違うトークンになります。

merge はリストを左からなぞり、重ならないように合わせます。2マスがペアと一致すれば新しい番号を入れて2マス飛ばし、そうでなければ1マスをそのまま移します。だから "aaa" で a+a を合わせると [aa, a] になり、[a, aa] にはなりません。

decode は番号ごとに vocab のかけらを取り出してつなげるだけです。かけらはすべて元の文字をつなげたものなので、encode したものを decode すると元の文章に正確に戻ります()。知らない文字だけは � に変わり、戻りません。

境目で起きる落とし穴もあります。学習資料では "A:" の後にいつも空白が来るので、"A: " が1トークンに合わさりました。そのためを空白なしの "A:" で終えると "A" と ":" の2トークンになりますが、この2つは学習資料で一度も別々に出てきたことがありません。生成のプロンプトは、学習の形式と空白まで同じく "A: " で終えます。

改行は合わせない — 生成が止まる場所

tokenizer.py の train は、ペアを数えるとき、2つのかけらをつないだ文字列に改行が含まれていれば数えません。だから改行はいつも単独で1トークン(1番)として残ります。

理由は、generate.py が改行トークンを引くと生成を止めるからです。学習資料では答えが改行で終わるので、モデルは答えを書き終えたら改行を出すように学びます。もし ".\n" のようにピリオドと改行が1トークンに合わさると、改行が別のトークンの中に隠れて、止まる場所が見つかりません。

実際の言語モデルは、この仕事を終わりを示す(EOS)で行います。XSLM は改行を EOS のように使います。

知らない文字 — 0番 � とバイト単位 BPE

XSLM のトークナイザーは、学習資料に出てきた文字しか知りません。基本資料には大文字 C も、ハングルも、絵文字もないので、下の実験では Cat の C、안녕、😀 はすべて0番になります。0番は「知らない文字」という意味だけが残り、元の文字は消えるので、decode すると � が出ます。

以降の多くのトークナイザーは、文字の代わりに のバイトから始めます(バイト単位 BPE)。

  • UTF-8 は1文字を1〜4バイトで書きます。実験結果のとおり a は1バイト、안 は3バイト、😀 は4バイトです。
  • バイトは256種類しかないので、最初の語彙256個で世界のすべての文章を書け、知らない文字が生まれません。
  • その代わり、初めて見る文字は複数のバイトトークンに長く刻まれます。

XSLM は読みやすいように文字単位で作りました。使う文字を学習資料に入れておくのが解決法です。

自分で確かめる lab/unknown.py
import json
from pathlib import Path
from tokenizer import BPE

tok = BPE(**json.loads(Path('data/tokenizer.json').read_text()))
for text in ['cat', 'Cat', '안녕', '😀']:
    print(text, tok.encode(text), list(text.encode('utf-8')))
cd ~/xslm
mkdir -p lab
micro lab/unknown.py
python3 -m lab.unknown

実行結果

cat [9, 7, 23] [99, 97, 116]
Cat [0, 7, 23] [67, 97, 116]
안녕 [0, 0] [236, 149, 136, 235, 133, 149]
😀 [0] [240, 159, 152, 128]

特殊トークン — 始まり・終わり・埋め草・不明

実際のトークナイザーは、文章には出てこない約束用のトークンを語彙の前の方に置きます。よく0〜3番にこの4つを置きます。

  • [PAD]: 長さの違う文章を1つの束(batch)で計算するとき、短い文章の後ろを埋めます。
  • [UNK]: 語彙にないかけらです。XSLM の0番 � と同じです。
  • [BOS]: 文章の始まりを知らせます。
  • [EOS]: 文章の終わりを知らせます。生成はこのトークンが出ると止まります。

資料形式の印([QUESTION]・[ANSWER] など)も、刻まれないよう特殊トークンとして登録できます。会話モデルは、ユーザーとアシスタントの順番を分ける特殊トークンを使います。

XSLM はこのうち [UNK] だけを0番に置きます。一度に1区間だけを学習するので埋める必要がなく、改行が終わりの印を担い、Q:・A: は BPE が自分で1トークン("Q: "・"A: ")に合わせます。

語彙の大きさを決める — 合併の数が変えるもの

語彙の大きさ(vocab_size)はトークンの種類の数で、番号は0から vocab_size − 1 までです。合併を増やすと、2つのことが逆向きに動きます。

  • 文章が短くなります。下の実験では、合併が0回なら学習資料が309トークン、12回なら218トークン、48回なら138トークンです。文脈12マスの中に、より多くの文章が入ります。
  • モデルが大きくなります。埋め込みの表(token)と出力層(head)は語彙の数だけ行を持つので、語彙が40から76になると、2つの表の数字がそれぞれ320個から608個に増えます。

大きすぎると、まれなトークンが増えて、トークンごとに学習する例が足りなくなります。ふつう大きな資料なら語彙 16,000〜32,000、ごく小さな資料(10KB 以下)なら 500〜1,000 を使います。目安は「資料の文字数 ÷ 100〜500」です。

合併は決めた回数より早く止まることがあります。2回以上出るペアがなければ、合わせても得がないからです。資料が小さいときによくあることで、正常です。

トークナイザーを学習し直すと番号の意味が変わるので、モデルも一から学習し直します。トークナイザーとモデルの語彙の数が違うと、埋め込みの表の行数が合わず、チェックポイントを読めません。

自分で確かめる lab/token_count.py
from pathlib import Path
from tokenizer import BPE

text = Path('data/train.txt').read_text(encoding='utf-8')
for count in [0, 6, 12, 24, 48]:
    tok = BPE.train(text, count)
    print('merges', count, 'vocab', len(tok.vocab), 'tokens', len(tok.encode(text)))
cd ~/xslm
mkdir -p lab
micro lab/token_count.py
python3 -m lab.token_count

実行結果

merges 0 vocab 28 tokens 309
merges 6 vocab 34 tokens 242
merges 12 vocab 40 tokens 218
merges 24 vocab 52 tokens 186
merges 48 vocab 76 tokens 138

入力して実験

tokenizer.pyを入力・実行し、を調べます。mergeで一致時は2位置、その他は1位置進む様子を追います。

cd ~/xslm
micro tokenizer.py
python3 tokenizer.py
python3 -m json.tool data/tokenizer.json
ターミナルで実習する
tokenizer.py手入力する完全なソース

mergeで合併し、trainで順序を学び、encodeで再適用、decodeで復元します。

import json
from collections import Counter
from pathlib import Path


def merge(ids, pair, new_id):
    result, i = [], 0
    while i < len(ids):
        if tuple(ids[i:i + 2]) == tuple(pair):
            result.append(new_id)
            i += 2
        else:
            result.append(ids[i])
            i += 1
    return result


class BPE:
    def __init__(self, vocab, merges):
        self.vocab, self.merges = vocab, merges
        self.chars = {c: i for i, c in enumerate(vocab) if len(c) == 1}

    @classmethod
    def train(cls, text, count):
        vocab = ['\ufffd'] + sorted(set(text) - {'\ufffd'})
        ids = [vocab.index(c) for c in text]
        merges = []
        for _ in range(count):
            pairs = Counter((a, b) for a, b in zip(ids, ids[1:])
                            if '\n' not in vocab[a] + vocab[b])
            if not pairs:
                break
            pair, frequency = pairs.most_common(1)[0]
            if frequency < 2:
                break
            new_id = len(vocab)
            vocab.append(vocab[pair[0]] + vocab[pair[1]])
            merges.append([list(pair), new_id])
            ids = merge(ids, pair, new_id)
        return cls(vocab, merges)

    def encode(self, text):
        ids = [self.chars.get(c, 0) for c in text]
        for pair, new_id in self.merges:
            ids = merge(ids, pair, new_id)
        return ids

    def decode(self, ids):
        return ''.join(self.vocab[i] for i in ids)

    def state(self):
        return {'vocab': self.vocab, 'merges': self.merges}


if __name__ == '__main__':
    settings = json.loads(Path('settings.json').read_text())
    text = Path('data/train.txt').read_text(encoding='utf-8')
    tokenizer = BPE.train(text, settings['merges'])
    Path('data/tokenizer.json').write_text(
        json.dumps(tokenizer.state(), ensure_ascii=False), encoding='utf-8')
    assert tokenizer.decode(tokenizer.encode(text)) == text
    print('vocab:', len(tokenizer.vocab), 'merges:', len(tokenizer.merges))
    print('roundtrip: OK', tokenizer.encode('Q: hi\nA: '))

コードを1行ずつ読み解く

tokenizer.py merge — 重ならないように1組を合わせる

def merge(ids, pair, new_id):
    result, i = [], 0
    while i < len(ids):
        if tuple(ids[i:i + 2]) == tuple(pair):
            result.append(new_id)
            i += 2
        else:
            result.append(ids[i])
            i += 1
    return result

ids のリストを i で左からなぞります。ids[i:i + 2] は今のマスと次のマスの2つ(最後なら1つ)です。それが pair と同じなら new_id を1つ結果に入れ、i を2増やして2マス飛ばします。そうでなければ今のマスをそのまま入れて1増やします。tuple に変えて比べるのは、JSON から読んだ規則がリストだからです(リストとタプルは == で等しくなりません)。リストを1回通るので、長さに比例する時間がかかります。

tokenizer.py 保存した状態からトークナイザーを作る

class BPE:
    def __init__(self, vocab, merges):
        self.vocab, self.merges = vocab, merges
        self.chars = {c: i for i, c in enumerate(vocab) if len(c) == 1}

BPE は vocab と merges の2つだけを持ちます。だから tokenizer.json から読んだをキーワード引数として展開し、BPE(…) にそのまま渡して作り直せ、train.py・generate.py がこう使います。chars は1文字のトークンだけを集めた「文字 → 番号」の参照表です。encode の最初の段階で、文字ごとの番号を速く探すのに使います。

tokenizer.py train — 合併規則を学ぶ

    @classmethod
    def train(cls, text, count):
        vocab = ['\ufffd'] + sorted(set(text) - {'\ufffd'})
        ids = [vocab.index(c) for c in text]
        merges = []
        for _ in range(count):
            pairs = Counter((a, b) for a, b in zip(ids, ids[1:])
                            if '\n' not in vocab[a] + vocab[b])
            if not pairs:
                break
            pair, frequency = pairs.most_common(1)[0]
            if frequency < 2:
                break
            new_id = len(vocab)
            vocab.append(vocab[pair[0]] + vocab[pair[1]])
            merges.append([list(pair), new_id])
            ids = merge(ids, pair, new_id)
        return cls(vocab, merges)

@classmethod は、オブジェクトなしで BPE.train(…) と呼ぶ、オブジェクトを作って返す関数です。

  • 最初の語彙は \ufffd(�)を0番に置き、文章に出てきた文字を sorted で並べ替えてつけます。並べ替えるので、番号はいつも同じです。
  • ids は文字ごとに番号を探したリストです(vocab.index)。
  • ごとに Counter で隣り合うペア (a, b) を数えます。zip(ids, ids[1:]) は隣り合う2マスずつを組にします。2つのかけらをつないだ文字列に改行があれば数えません。
  • most_common(1) が最も多いペアと回数を返します。回数が同じなら先に数えたペア、つまり文章で先に出てきたペアが選ばれます。
  • ペアがないか、最も多いペアも1回だけなら止まります。
  • 新しい番号は (vocab)、つまり語彙の次の番号です。2つのかけらをつないだ文字列を語彙に加え、規則 [[a, b], 新しい番号] を記録し、ids を合わせたリストに置き換えて次の繰り返しに備えます。

tokenizer.py encode — 文章を番号に

    def encode(self, text):
        ids = [self.chars.get(c, 0) for c in text]
        for pair, new_id in self.merges:
            ids = merge(ids, pair, new_id)
        return ids

chars.get(c, 0) は文字の番号を探し、なければ 0(�)を返します。その次に merges を学んだ順に1つずつ merge します。規則の数だけリストをなぞり直すので、規則が多く文章が長いと遅くなります。実際のライブラリは規則の優先順位の表を使い、もっと速く合わせます。

tokenizer.py decode・state — 番号を文章に、状態の保存

    def decode(self, ids):
        return ''.join(self.vocab[i] for i in ids)

    def state(self):
        return {'vocab': self.vocab, 'merges': self.merges}

decode は番号ごとに vocab のかけらを取り出し、''.join でつなげます。state は json で保存できる辞書を返します。規則をタプルではなくリストで記録するので、JSON で保存して読み直しても形が同じです。

tokenizer.py 実行部分 — 学習・保存・検査

if __name__ == '__main__':
    settings = json.loads(Path('settings.json').read_text())
    text = Path('data/train.txt').read_text(encoding='utf-8')
    tokenizer = BPE.train(text, settings['merges'])
    Path('data/tokenizer.json').write_text(
        json.dumps(tokenizer.state(), ensure_ascii=False), encoding='utf-8')
    assert tokenizer.decode(tokenizer.encode(text)) == text
    print('vocab:', len(tokenizer.vocab), 'merges:', len(tokenizer.merges))
    print('roundtrip: OK', tokenizer.encode('Q: hi\nA: '))

settings.json の merges と /train.txt で学習し、ensure_ascii=False で tokenizer.json に保存します。このオプションがないと、ハングルなどの文字が \uc548 のようなエスケープで保存されます。 は、学習資料全体を encode → decode して元の文章とまったく同じかを検査します。最後の行は、生成プロンプト "Q: hi" の次の行 "A: " がどんな番号になるかを示します。基本資料なら [29, 13, 14, 1, 30]、つまり "Q: "・h・i・改行・"A: " です。

理解を確認

roundtrip: OKと原文復元を確認。未知文字は�になります。

困ったとき

頻出ペアがなければ合併ゼロでも正常です。辞書変更後はも新しく学習します。

よくある質問

トークナイザーのライブラリを使わず、なぜ自分で作るのですか?

実際の開発では、 tokenizers や tiktoken のようなライブラリを使います。 などで作られていて、大きな資料もすぐに処理します。XSLM は、合併規則がどうできて、どの順に適用されるかを自分の目で見るために60行で作ります。ライブラリが保存する tokenizer.json にも、同じ vocab・merges が入っています。

roundtrip は OK なのに、自分の質問が � に変わります。

roundtrip は学習資料だけで検査します。質問に学習資料にない文字(大文字・日本語・記号)があると0番になります。その文字が入った行を raw.txt に加え、prepare → tokenizer → 新しいチェックポイントで学習し直してください。

トークン1つは単語1つですか?

違います。トークンは BPE がよく見たかけらなので、"is " のように単語と空白がくっついたり、" ca" のように単語の一部だったりします。実際のモデルでも英単語1つは平均で1個あまりのトークンで、他の言語はもっと細かく刻まれることもあります。だから料金と文脈の長さを、単語ではなくトークンで数えます。

ガイドライン

人工知能

エージェントツール

バイブコーディング

AI活用

AI資格

AI授業

IT基礎

チャット

掲示板

制作

コミュニティ

その他

用語辞典

知らない言葉を選んでみましょう。やさしい説明と関連用語が見られます。

その用語はまだありません。

用語を選ぶと説明が表示されます。