Галоўная / Артыкулы

BPE-токенізатор адаптаваны з нуля: корэйшыя последованцы для малага акна контэкста GPT

Напісайце токенайзер для кодування парагруп байт у Python, натрэнуйце яго на WikiText-2, зберажыце рэзультаты у формате JSON і паглядзіце, як токенізацыя падслова расширва вікна контэксту GPT.

Выдавец: REACTAPP.TOP26 хв чытання6015 слоў

Крыніцы: Tutorial 16: Beyond Character Tokens, Building a BPE Tokenizer

Токенізатор на рэгле характараў — гэта простыяй спосаб зрабіць працюючым маленькі GPT: слоўнік вельмі маленькі, нічога не ўзнікла невядомым, і кожны крок є прозрачны. Косцю ёст тыяжыню последовасці, адколі кожны літера коштае пазіцыю ў вікнах контэксту. Шырокі практычны падход заменяе яго на кодаванне па парах байт (BPE), напісанае з нуля на чыстай Python. Вы будете трэнаваць яго на WikiText-2, зберагаць выучаны слоўнік і злучэнні ў формате JSON, стварыць ўнікальны ідэнтыфікатор, перекодаваць набір дадзеных і адразу пабачыце, што змініцца, калі будете ператрэнаваць модель на токенах падслоў.

Код вырачоўвае, што ўжо існуе маленькі GPT на базе PyTorch і чыстыя файлы WikiText-2. Якщо трэнаванне все ж такі не працуе належна, спачатку стабілізуйце яго; пагляньце тэсты, якія ізолююць кожную проблему ў маленькім GPT на PyTorch.

Чаму токены характараў больш не дастатні

Токенізатор симвала дае кожнаму симвалу, укладаючы і прабелы, свой сабе токен. Возьмімо гэты короткі фраза:

artificial intelligence

Гэта ператвараецца ў адны токен на кожны симвал:

['a', 'r', 't', 'i', 'f', 'i', 'c', 'i', 'a', 'l',
 ' ', 'i', 'n', 't', 'e', 'l', 'l', 'i', 'g', 'e',
 'n', 'c', 'e']

Это 23 токэны для двух слоў. Токэнізатор падслоў мог бы представіць тую ж фразу з значна меншай колькасцю елементаў, напрыклад:

['art', 'ificial', ' intelligence']

Трыя токэны замест 23. Корэйшыя последоўнасці значыць больш тексту ў вікнах контэксту, меньш колькасць пазычак для навучэння і меньш колькасць крокаў декодавання пад час генеравання. Модель таксама прымець значамыя фрагменты замест ізольаваных літер, частыя фрагменты стануць павторна вжываемымі елементамі, а рэдкія словы все ўсё можна будзе запісаць з меншых частак. Складныя часткай — гэта автаматычна аднаходжэнне хорашых падсловяных елементаў, і самэ гэта ёсць задачаю, яку выканае BPE.

Знакі, словы і падсловы: паўтарэнне

Існуе тры большыя спосабы раздзелення тексту, і кожны з іх вялічыць разныя компромісы.

Токэнацыя знакамі

Кожны знак ёсць токэнам:

"learning"
→ ["l", "e", "a", "r", "n", "i", "n", "g"]

Ён практыкуе маленькі слоўнік, не мае незнаёмых токэнаў і ўскладненай рэалізацыі. У замену вы атрымліваете дужыя последованасці, медленную генерацыю і модель, якая должна складаць словы з адзіных літер.

Токэнаізацыя слоў

Кожнае слова ўважаецца токэнам:

"machine learning works"
→ ["machine", "learning", "works"]

Парадыграмы ўсунутыя і лёгкія да чытання, але слоўнік стае вельмі вялікім, незнаёмыя словы вымагаюць незнаёмых токэнаў, а схожыя формы, такія як "learn", "learning" і "learned", можу стаць трыма незв’язанымі ID-амі, у якіх няма нічага спаканага. Імены, неправільна напісанне і рэдкія словы адмаўляюцца паслужыць належна.

Сабсловная токэнацыя

Словы дзелюцца на часта з’являючыяся фрагменты:

"unbelievable"
→ ["un", "believ", "able"]

Это дае меньшыя последованцы, чым знакі, і меньшы слоўнік, чым словы. Частае словы все ж можа стаць адзінам токенам, рэдкія словы раскладаюцца на вядомыя часткі, а фрагменты, такія як прыметнікі і суфіксы, перадаюцца знову і знову між багатымі словамі. Ядзіныя рэальныя затраты — гэта тое, што навчанне і реалізацыя токенізатора ўскладненыя.

Можна спакульваць тры падходы як тры пункты на спектре:

Characters
← flexibility

Subwords
← balance

Words
← compactness

Як BPE выучаець свае з’еднанні

BPE пачынае з найменшых елементаў, тут — адзінаковых симвалаў, і пастаўна з’еднае найчастае пара суседніх симвалаў у новы токен. Разглянем маленькі корпус з трох каляў:

low
lower
lowest

Спачатку кожна слова — это проста ўсе яе симвалы:

l o w
l o w e r
l o w e s t

Далей указаць пары суседзяючых симвалаў, якія з’являюцца:

(l, o)
(o, w)
(w, e)
(e, r)
(e, s)
(s, t)

Адзін пара выдзеляецца:

(l, o)

Ён прысутны ў всіх трох ворах, таму ён з’едынаецца ў адны знак:

(l, o) → lo

Пасля таго, як яго заменілі ў всіх месцах, корпус выглядае так:

lo w
lo w e r
lo w e s t

Працюючы зноў, найчастэйшая пара зараз ёсьць:

(lo, w)

Ён таксама паводзіцца тры разы, таму ён таксама з’едынаецца:

(lo, w) → low

І корпус стаець:

low
low e r
low e s t

Пасля достатньай колькасці раундаў найчастэйшыя последованцы стаюць самастоятнымі токенамі.

Што дае цыкл адучэння

Процедура, якую вы збіраецеся рэалізаваць, складаецца з наступных крокаў:

Training text
→ initialize character tokens
→ count adjacent token pairs
→ select most frequent pair
→ merge the selected pair
→ add merged token to vocabulary
→ repeat until target vocabulary size

Ён стварае два артыфакты, і вам патрэбны якія-то з іх, каб пазніяй использоваць токенайзер.

Словнік

Словнік супарабатоўвае кожную строчку токена з цэлым ідэнтыфікатарам, напрыклад:

"a"       → 0
"b"       → 1
"th"      → 98
"ing"     → 127
" the"    → 204

Правіллі з’еднання

Правіллі з’еднання — это абяранковы спіс пар, якія былі з’еднаны пад трэніраванні:

("t", "h") → "th"
("i", "n") → "in"
("in", "g") → "ing"
(" ", "the") → " the"

Порядак ўскладнення правіл є крытычным. Болей пазнее правілы, такія як ("in", "g"), можу застаўціцца толькі тады, калі пазнейшае правіла, якае стварыла "in", вже была застосавана. Застосавленне тых сабе правіл у іншым порядку дае іншы спосаб разбівання.

Першая рэалізацыя на простым корпусе

Перш чым працаваць з WikiText-2, спробуйце алгорытм на строке, яю можна паглядзець вочыма:

text = "low lower lowest"

Разбіленне на символы тэкст дае спіс пачатковых токэнаў:

tokens = list(text)
print(tokens)

Каліткаванне паказвае кожны симвал, укладаючы і прасвяты:

['l', 'o', 'w', ' ', 'l', 'o', 'w', 'e', 'r',
 ' ', 'l', 'o', 'w', 'e', 's', 't']

Ёнкіяў для з’еднання можна знайсці, палячы кожную пару суседзяў. Складанне спісу з самым сабой, але з сувальдаваннем на адну пазіцыю, дае кожную суседню пару, а Counter ўрахоўвае іх:

from collections import Counter


def count_pairs(tokens):
    return Counter(
        zip(tokens[:-1], tokens[1:])
    )

Абсалютна большасць пар — глядзіце іх:

pair_counts = count_pairs(tokens)
print(pair_counts.most_common(10))

Рэзультат будзе выглядаць прыблізна так:

[(('l', 'o'), 3),
 (('o', 'w'), 3),
 (('w', 'e'), 2),
 ...]

Заўважкі: (' ', 'l') таксама з’являецца ў підрахунках. Паколькі прабелы ў гэтым схэме ўважаюцца звычайнымі символамі, BPE можа выучыць токены, якія маюць прабел на пачатку, і самэ гэта прыводзіць да стварэння такіх токенаў, як " the".

З’едынэнне адной пары

Основная операцыя праходзіць па спісе токенаў з левага на правы. Калі текущы токен і наступны падходзяць пад выбраную пару, выдаецца з’едынены токен, і працэс праходзіць далей на два пункты; інакш вы копіюеце текущы токен і праходзіце далей на адин пункт. Надакладанні рашаюцца з левага на правы: у a a a з парой (a, a) з’едынуюцца толькі першыя два.

def merge_pair(
    tokens,
    pair,
    merged_token,
):
    output = []
    index = 0

    while index < len(tokens):
        if (
            index < len(tokens) - 1
            and tokens[index] == pair[0]
            and tokens[index + 1] == pair[1]
        ):
            output.append(merged_token)
            index += 2
        else:
            output.append(tokens[index])
            index += 1

    return output

Застосавайце яе, каб з’едыніць l і o:

tokens = merge_pair(
    tokens=tokens,
    pair=("l", "o"),
    merged_token="lo",
)

print(tokens)

Рэзультат корачэй за вхідны тэкст:

['lo', 'w', ' ', 'lo', 'w', 'e', 'r',
 ' ', 'lo', 'w', 'e', 's', 't']

Кожная адзынка выкарыстоўвае адні токен за кожную паўтарэнне пары, і самэ гэта і ўтварае аджымленне.

Паўтарэнне пакуль слоўнік не запыніцца

Мінімальны трэйнер проста ціклізуе: лічыць пары, выбірае найчастае з іх, адзынкуе яе ў всіх месцах, фіксуе новы токен і правілу, і зупініцца, калі слоўнік досягне намечанага розмеру. Ён таксама зупініцца раней, якщо не застаецца пар, або якщо найкращая пара паўтараецца менш чым два разы, таму што ў такі момент адзынкаўванне больш не стварае нічога, што можна было бы знова выкарыстоўваць.

def train_bpe_on_tokens(
    tokens,
    target_vocab_size,
):
    vocabulary = set(tokens)
    merges = []

    while len(vocabulary) < target_vocab_size:
        pair_counts = count_pairs(tokens)

        if not pair_counts:
            break

        best_pair, frequency = (
            pair_counts.most_common(1)[0]
        )

        if frequency < 2:
            break

        merged_token = "".join(best_pair)

        tokens = merge_pair(
            tokens=tokens,
            pair=best_pair,
            merged_token=merged_token,
        )

        vocabulary.add(merged_token)
        merges.append(best_pair)

        print(
            f"Merged {best_pair} "
            f"into {merged_token!r}, "
            f"frequency={frequency}"
        )

    return tokens, vocabulary, merges

Дастычкая працэўнае сіткі з маленькім цэльвым об’ектам дастае можлівасць спаглядаць, як з’едыненні выконваюцца адна за іншай:

final_tokens, vocabulary, merges = (
    train_bpe_on_tokens(
        tokens=list(text),
        target_vocab_size=20,
    )
)

Гэты варыянт падходзіць для стварэння інтуіцыі, але кожная ітерацыя перасканавае весь список Python. У большым корпусе WikiText гэта стае дужа повольным, таму практычны варыянт, прыведзены выкладзены нижэй, трэнуецца на настаўляемай частцы WikiText-2.

Проектаванне повтарна выкарыстоўванага токенізатора

Клас токенізатора должен трэнувацца на неапранутым тексте, пачынаючы з абох канцоў, кодаваць і декодаваць, адхіляць символы, якія не ў ўжоўваным слоўніку, зберагаць і чытаць даныя з JSON, а таксама вычысляць «відбіск». Навакол яго скрыпт перекодавае кожную частку WikiText-2 і паказвае, насколькі корачэй стаюць последованасці BPE.

Адзін з рашынакоў дызайна варта чытна адзначыць. Шырокі токэнізатор працуе з симваламі Unicode, а не з сырымі байтамі. Токэнізаторы, якія викорыстоўваюцца у большых моделях, зазвычай працуюць на рэвэлі байтов, тое значыць, ўсё ўжо ўтворанае іх базовае словнікавае запаса складаецца з 256 можлівых значэнняў байтов, і жаданы вхід ніколі не можа застацца нерэпрэзентаваным. Версія, якая базуецца на симвалах, выкорыстоўвае той самы алгорытм і ёй лёгкае чытаць, але яна не ўзаемна заменяе тыя токэнізаторы.

Канструкцыя класа

Канстрактар прымеў базовыя симвалы і арранжаваны список для з’еднання. Ён стварае цэлы список токэнаў, пачынаючы з базовых симвалоў і дадаючы рэзультат кожнага з’еднання, пры чым ігнаруея дуплікаты. З гэтага списку ён стварае функціі stoi (з роштка на ID) і itos (з ID на роштак) і фіксуе размах словнікавага запаса. Паколькі ID вылучаюцца з пазіцый списку, тыя ж самыя базовыя токэны і процесы з’еднання завжды даюць тыя ж ID.

class BPETokenizer:
    def __init__(
        self,
        base_tokens,
        merges,
    ):
        self.base_tokens = list(
            base_tokens
        )

        self.merges = [
            tuple(pair)
            for pair in merges
        ]

        self.tokens = list(
            self.base_tokens
        )

        for left, right in self.merges:
            merged = left + right

            if merged not in self.tokens:
                self.tokens.append(merged)

        self.stoi = {
            token: index
            for index, token in enumerate(
                self.tokens
            )
        }

        self.itos = {
            index: token
            for token, index
            in self.stoi.items()
        }

        self.vocab_size = len(
            self.tokens
        )

Такім чынам, створаны слоўнік складаецца з двух частэй:

Base character tokens
+
learned merged tokens

Адаптаванне на рэальным текстам

Адаптаванне стае методам класа, які вяртае готавы да вжыцтва токенізатор. Ён сортавае унікальныя символы тексту, каб створыць базовы слоўнік, адмовляецца ад мяркі цільовага размеру, меньшай за гэты базовы набор, а пасля запускае той самы цикл підрахунку, выбору і з’еднання, як і раней. Логаванне регулюецца: выдрукваецца кожна з першых 20 операцый з’еднання, а пасля — кожная стае, разам з частотай пары і тэчным дзействам серіі, тады вы можете стежыць за прыгнуткам дакумента без перэплыву інформацыі.

@classmethod
def train(
    cls,
    text,
    target_vocab_size,
    minimum_pair_frequency=2,
    verbose=True,
):
    base_tokens = sorted(
        set(text)
    )

    if target_vocab_size < len(base_tokens):
        raise ValueError(
            "target_vocab_size cannot be "
            "smaller than the number of "
            "unique characters"
        )

    working_tokens = list(text)
    vocabulary = set(base_tokens)
    merges = []

    while len(vocabulary) < target_vocab_size:
        pair_counts = Counter(
            zip(
                working_tokens[:-1],
                working_tokens[1:],
            )
        )

        if not pair_counts:
            break

        best_pair, frequency = (
            pair_counts.most_common(1)[0]
        )

        if frequency < minimum_pair_frequency:
            break

        merged_token = "".join(
            best_pair
        )

        working_tokens = merge_pair(
            tokens=working_tokens,
            pair=best_pair,
            merged_token=merged_token,
        )

        merges.append(best_pair)
        vocabulary.add(merged_token)

        if (
            verbose
            and (
                len(merges) <= 20
                or len(merges) % 100 == 0
            )
        ):
            print(
                f"Merge {len(merges):4d}: "
                f"{best_pair!r} "
                f"→ {merged_token!r}, "
                f"frequency={frequency:,}, "
                f"tokens={len(working_tokens):,}"
            )

    return cls(
        base_tokens=base_tokens,
        merges=merges,
    )

Чаму існуе мінімальная частота пары

Якщо найчастае застаўленая пара з’являецца толькі раз, ўзьеднанне яе стварае токен, які ніколі не будзе викорыстаны знову, што проста марнуе месца ў слоўніку. Таму цыкл зупіняецца пад такімі умовамі:

frequency < minimum_pair_frequency

Для такіх експерыментаў достатній порог у два:

minimum_pair_frequency = 2

У большым корпусах текста можа знадобіцца вышэйшы порог, каб слоўнік быў выкарыстоўваны толькі для тых патэранаў, якія з’являюцца достатнь часта, каб модель могла стварыць якісныя эмбеддынгі для іх.

Кодаванне і декодаванне з абробнікам токенавання

Кодаванне складаецца з трохоў крокаў:

  1. Разбіваецца текст на базовыя символы.
  2. Праеўчаецца кожнае правілле з’еднання ў тым порядку, які ён быў выучаны.
  3. Шукаецца ID кожнага атрыбута, які выйшаў у результате.

Першая частка гэтай роботы выканана ў функцыі encode_tokens. Спачатку ёна збірае всі символы, якіх няма ў слоўніку, і выдае чыстую памылку, якщо такія є, што значна прыемней для дыбаггінгу, чым KeyError у середзіне набора дадзеных. Потым ёна праеўчаецца правіллі з’еднання.

def encode_tokens(self, text):
    unknown_characters = sorted({
        char
        for char in text
        if char not in self.stoi
    })

    if unknown_characters:
        raise ValueError(
            "Text contains characters missing "
            "from the base vocabulary: "
            f"{unknown_characters}"
        )

    tokens = list(text)

    for pair in self.merges:
        merged_token = (
            pair[0] + pair[1]
        )

        tokens = merge_pair(
            tokens=tokens,
            pair=pair,
            merged_token=merged_token,
        )

    return tokens

Ператварэнне атрыманых стрэнгаў токэнаў у ID-ы — гэта простая працэс знаходжэння:

def encode(self, text):
    tokens = self.encode_tokens(text)

    return [
        self.stoi[token]
        for token in tokens
    ]

Декодаванне ўсё простае. Кожны токен, незалежна ад таго, з’еднаны ён чы не з’еднаны, зберагае точны текст, які він паводзіць, таму декодаванне — гэта проста конкатенацыя:

def decode(self, token_ids):
    return "".join(
        self.itos[int(token_id)]
        for token_id in token_ids
    )

Это дае BPE атрыбут, які варта явна перапрацаваць – без адыякшнага спадку падчас перадачы дадзеных:

decode(encode(text)) = text

Шырокі пераклад прыкладным выразам:

sample = "The history of science."
encoded = tokenizer.encode(sample)
decoded = tokenizer.decode(encoded)
assert decoded == sample

Зберагчыце і перзачытванне токенізатора

Вам не трэба зберагчыць саму лексыкану, таму ўсё можна перзаснаваць дэтэрміністычна на аднойчынных токенах і спісе злучэнняў. Метод save запісвае гэтыя два элементы у формате JSON, а таксама пазначку типу і розмер лексыкану для зручнасці. Установка ensure_ascii=False дазволяе залічыць некірабетныя символы чытальнымі у файле, а якщо родны каталог яшчэ не існуе, ён ствараецца.

def save(self, path):
    data = {
        "type": "character_bpe",
        "base_tokens": self.base_tokens,
        "merges": [
            [left, right]
            for left, right in self.merges
        ],
        "vocab_size": self.vocab_size,
    }

    path = Path(path)

    path.parent.mkdir(
        parents=True,
        exist_ok=True,
    )

    with open(
        path,
        "w",
        encoding="utf-8",
    ) as file:
        json.dump(
            data,
            file,
            ensure_ascii=False,
            indent=2,
        )

Рэзультатам яе адзін файл:

tokenizer/
└── bpe_tokenizer.json

Завантажэнне ператвараець процес, чытаючы JSON і перадаючы обеі спісы канструктару:

@classmethod
def from_file(cls, path):
    with open(
        path,
        "r",
        encoding="utf-8",
    ) as file:
        data = json.load(file)

    return cls(
        base_tokens=data[
            "base_tokens"
        ],
        merges=data["merges"],
    )

Потым паказайце, што паспрацоўваючы занова токенізатор все ўсё равна перадае прыкладны текст:

loaded_tokenizer = (
    BPETokenizer.from_file(
        "tokenizer/bpe_tokenizer.json"
    )
)

assert (
    loaded_tokenizer.decode(
        loaded_tokenizer.encode(sample)
    )
    == sample
)

Адаптаванне на частцы WikiText-2

Пакалькі, што просты трэйнер занова скануе весь робочы сэрыё праз кожнае з’еднанне, адаптаванне всіх данных WikiText-2 з вялікім слоўнікам можа зайняць многа часу. Пачніце з настаўляемага прыфікса тексту для адаптавання:

training_characters = 500_000

training_text = train_text[
    :training_characters
]

Разумныя пачатковыя размеры слоўніку ўскладнення:

target_vocab_size = 500

Іншымі словамі:

Initial character vocabulary
+
learned merged tokens
=
approximately 500 tokens

Калі все працюе, спробуйце большэйшыя цялі:

target_vocab_size = 1_000
target_vocab_size = 2_000

Можна спакаваць, што час адучэння будзе растаць ў залежнасці ад размеру праекспамента і колькасці з’едынэнняў.

Выбіранне базовых симвалаў з кожнага раздзелу

Якщо базовы слоўнік ствараецца толькі на адной праекспаментнай выборцы, тады текст для параболікацыі чыстоты або тэставання можа мець симвалы, якія ніколі там не былі, і кодаванне такіх раздзелаў не будзе работаць. Рашэннем є збір базовага набора симвалаў з усіх трохоў раздзелаў, пры тым як статыстыку з’едынэнняў продовжаць вылучаць толькі з дадзеных адучэння:

all_text = (
    train_text
    + val_text
    + test_text
)

base_tokens = sorted(
    set(all_text)
)

Гэта не прасачыць ніякіх шаблонаў мовы верыфікацыі ў результаты злучэння. Яна толькі гарантуе, што кожны симвал у наборе дадзеных мае ID. Чырага для гэтага трэнер атрымвае неабяжны параметр base_tokens, які выкарыстоўваецца заместо симвалаў трэнінгавага тексту, калі ён заданы:

@classmethod
def train(
    cls,
    text,
    target_vocab_size,
    minimum_pair_frequency=2,
    base_tokens=None,
    verbose=True,
):
    if base_tokens is None:
        base_tokens = sorted(
            set(text)
        )
    else:
        base_tokens = sorted(
            set(base_tokens)
        )

Апрацоўка розміру стиснення, якое вы атрымалі

Ёсць кантрольны спосаб разбіраць, калі BPE дапамагае, — паравярце колькасць токенав у там самым тэксте. Возьміце прыклад:

sample_text = train_text[:10_000]

Колькасць токэнаў яго симвалаў проста равная яго дужыне:

character_count = len(sample_text)

Колькасць токэнаў BPE равна дужыне спіса кодаваных ID:

bpe_ids = tokenizer.encode(
    sample_text
)

bpe_count = len(bpe_ids)

Спявары межы імі ёсць калярэкцыйны фактар:

compression_factor = (
    character_count / bpe_count
)

Рэзультат можа выглядаць так:

Character tokens: 10,000
BPE tokens: 4,250
Compression: 2.35×

З узлікам скарычвання 2,35 блакат з 128 токенав BPE пакрывае прыблізна столькі ж тексту, сколькі 301 токен з симваламі. Аднак скарычванне не ўсюды аднаковае: проза, наполненае распашчытымі словамі, скарычваецца добра, тады як цыфры, рэдкія імены і незвычайная пунктуацыя — паслаба.

Што навучыўся токенізатор

Навучытыя токены — это всё ў словніку пасля базовых симвалоў. Якшы ўпорядакаваўшы іх па дужыне і выдрукуўшы найдужэйшыя з іх ID, можна быстра зрозумець, што токенізатор счыляе частым:

learned_tokens = (
    tokenizer.tokens[
        len(tokenizer.base_tokens):
    ]
)

longest_tokens = sorted(
    learned_tokens,
    key=len,
    reverse=True,
)

for token in longest_tokens[:50]:
    print(
        repr(token),
        tokenizer.stoi[token],
    )

Зазвычай вы будете бачыць токены прыблізна такія:

" the"
"ing"
"tion"
" of"
"ed"
" and"

Точный спіс залежыць ад прыклада дадзеных для навчання, прадзеявання, розмеру цэльвога слоўніку, мінімальнай частоты і правілаў рашырання таварыства. Чыста пра правіла рашырання таварыства: Counter.most_common вяртае пары з аднаковай частотай у тым порядку, у якім яны былі першыя пісаны, таму для заданых дадзеных рэзультаты є дэтэрміністычныя, але змена прыклада дадзеных можа змяніць тое, які з калькольных пар з аднаковай частотай выграе.

Перекодаванне набора дадзеных

За дапамою навучанага токенайзера неабходна закодаваць усе тры часткі дадзеных:

train_bpe_ids = tokenizer.encode(
    train_text
)

val_bpe_ids = tokenizer.encode(
    val_text
)

test_bpe_ids = tokenizer.encode(
    test_text
)

Перакануце спісы ID у тэнзоры типу long — гэты ў тыпе, якія лейеры імбеддінга чакаюць для індэксаў:

train_tensor = torch.tensor(
    train_bpe_ids,
    dtype=torch.long,
)

val_tensor = torch.tensor(
    val_bpe_ids,
    dtype=torch.long,
)

test_tensor = torch.tensor(
    test_bpe_ids,
    dtype=torch.long,
)

Зберажыце іх у ўласным каталогу:

data/
└── encoded_bpe/
    ├── train_ids.pt
    ├── val_ids.pt
    └── test_ids.pt

Не трэба чыгнуць з місця файлы з кодаваннем симвалаў; якшчына, калі яны застануцца, будзе лёгкае паўтарныя порэвананні, а таксама будзе ухілена можлівасць падачы ID аднаго модэлю іншаму.

Параболка кодаваных частак

Па-першае, кожны ID павінен належаць да дыяпазона ад нуля да розмеру слоўніку, але без уключэння гэтага розмеру:

for split_name, token_ids in [
    ("train", train_tensor),
    ("validation", val_tensor),
    ("test", test_tensor),
]:
    assert token_ids.min().item() >= 0

    assert (
        token_ids.max().item()
        < tokenizer.vocab_size
    )

    print(
        split_name,
        token_ids.shape,
        token_ids.min().item(),
        token_ids.max().item(),
    )

Друга пункт — прыклад з кожнага часткавання павінен вернуцца назад да первіснага тексту:

for text in [
    train_text[:1_000],
    val_text[:1_000],
    test_text[:1_000],
]:
    encoded = tokenizer.encode(text)
    decoded = tokenizer.decode(encoded)

    assert decoded == text

Уключэнне BPE ў модэль

Архітэктура модэля не патрабуе жадных структурных змян. Толькі розмер слоўніку ў настройках зменяецца:

config = MiniGPTConfig(
    vocab_size=bpe_tokenizer.vocab_size,
    block_size=128,
    embedding_dim=128,
    num_heads=4,
    num_layers=4,
    expansion_factor=4,
    dropout=0.1,
)

Накіравайце завантажэнне дадзэйна на новыя файлы:

train_ids = torch.load(
    "data/encoded_bpe/train_ids.pt"
)

З даўэйшэй модэль праўяе прыкладкі BPE замест абарактароў.

Старыя пункты перапыткі не будуць завантажаны

Модэль, якая праўяе абарактары, зазвычай мае слоўнік у такім дыяпазоне:

100 to 300 character tokens

Модэль BPE зазвычай мае ў своім складзе ўсё такое:

500 to 2,000 subword tokens

Як укладкі токенава, так і выходныя частыні залежаць ад розмеру слоўніку, таму іх формы змінююцца. Пункт перапаковкі для симвала мае:

token embedding: [character_vocab_size, C]

Хоча модель BPE чакае:

token embedding: [bpe_vocab_size, C]

Ёнтут C — гэта размернасць умяшчэння. Форматы не падходзяць, таму старыя вагі не можна завантажыць безпосередна. А нават якбы можна, ID-ы маюць разны значэнні ў кожным токенайзеры. Патрэбна трэнаванне чыстага модэлю, і корыстна трываліцею зберагаць чекпоінты окрема:

checkpoints_char/
checkpoints_bpe/

Або, іншымі словамі:

checkpoints/char/
checkpoints/bpe/

Фінгерпрінтінг токенізатора

Неспаданне форма токенава ўскладнівае роботу; аднасоўкі пункту перапакунку з іншым токенізаторам BPE таго ж розмеру залишаецца непазначальным, таму што кожны ID проста адносіцца да некоректнага тексту. Фінгерпрінт гэтых токенава — це хэш у складзенай формате JSON, які з’едынаецца з SHA-256, таму будь-яя змена стварае новую значэнню.

import hashlib


def tokenizer_fingerprint(tokenizer):
    payload = json.dumps(
        {
            "base_tokens": (
                tokenizer.base_tokens
            ),
            "merges": [
                list(pair)
                for pair in tokenizer.merges
            ],
        },
        ensure_ascii=False,
        separators=(",", ":"),
    ).encode("utf-8")

    return hashlib.sha256(
        payload
    ).hexdigest()

Зберагаеце відбліск пальца ў кожным пункце перапалоўкі:

checkpoint[
    "tokenizer_fingerprint"
] = tokenizer_fingerprint(
    tokenizer
)

Падчас завантажэння неабяжна вычысляць «відбліск» актыўнага токенізатора і адмовіцца працаваць, якщо ён не падабяжыцца таму, які быў зафіксаваны ў чекпоінтэ.

Аб меры бягучасці, якую трэба знать

Энкодэр пераглядае кожнае правіло злучэння для всей последовасці токэнаў:

for pair in self.merges:
    tokens = merge_pair(...)

Это адзін цэлы праход па тексте за кожны з’еднанне, таму большыя тексты з тыячамі з’еднанняў кодуюцца медленна. Рэалізацыя ўсвядома ставіць працоўнасць яснасці, правільнасці і можлівасці аналізу вышэй за швальнасць. Токенізаторы для практычнага выкарыстоўвання заздалегідь дзеляюць текст на словы, кэшуюць павтараючыяся розбівы і прыкладзяюць з’еднанні па рангу; для навучэння пераважае прозрачная версія.

Полны праект

З’едначыце все ў аднам скрыпце:

build_bpe_tokenizer.py

Усія програма, паказаная нижэй, аб’еднвае памочнікі для пар, клас токенізатора, памочнікі для верыфікацыі і точку входу для командной лініі. Два аспекты разлічаюцца ад фрагментаў коду: пры збераганні тепер зберагаецца «фінгерпрінт», а даны запісваюцца чераз тымчасовы файл, які пасля чаго перайменовываецца на файл-цэль, таму перарваны запуск ніколі не заставляе напісанага напаўчаса файла; кроме таго, пад час навчання пераканальваецца, што базовы слоўнік пакрывае текст для навчання. Галоўная функцыя навчаеся на настаўленым прыфіксе, зберагае і перзагружае токенізатор, пераканальвае «фінгерпрінты», апыляецца па ціклу і зберагае кожны фрагмент, верыфікае дыапазоны ID і выводзіць статыстыку.

У цыкле „значэнняяў, якія былі выучаны, з наібольшай дужынай“ адзін рядок, здаецца, быў спакалечаны: f"{loaded_tokenizer.stoi5d} " вызывае AttributeError, таму што stoi5d не існуе. Намераваны выраз практычна напэўна ёсць f"{loaded_tokenizer.stoi[token]:5d} ", які выводзіць ID кожнага токена, заполненыя да пяці симвалаў. Выправіце гэты рядок прытым, прычым як толькі запускаце скрыпт.

import argparse
import hashlib
import json
from collections import Counter
from pathlib import Path

import torch


# ==================================================
# Pair Operations
# ==================================================

def count_pairs(tokens):
    return Counter(
        zip(tokens[:-1], tokens[1:])
    )


def merge_pair(
    tokens,
    pair,
    merged_token,
):
    output = []
    index = 0

    while index < len(tokens):
        if (
            index < len(tokens) - 1
            and tokens[index] == pair[0]
            and tokens[index + 1] == pair[1]
        ):
            output.append(merged_token)
            index += 2
        else:
            output.append(tokens[index])
            index += 1

    return output


# ==================================================
# BPE Tokenizer
# ==================================================

class BPETokenizer:
    def __init__(
        self,
        base_tokens,
        merges,
    ):
        self.base_tokens = list(
            base_tokens
        )

        self.merges = [
            tuple(pair)
            for pair in merges
        ]

        self.tokens = list(
            self.base_tokens
        )

        for left, right in self.merges:
            merged_token = left + right

            if merged_token not in self.tokens:
                self.tokens.append(
                    merged_token
                )

        self.stoi = {
            token: index
            for index, token
            in enumerate(self.tokens)
        }

        self.itos = {
            index: token
            for token, index
            in self.stoi.items()
        }

        self.vocab_size = len(
            self.tokens
        )

    @classmethod
    def train(
        cls,
        text,
        target_vocab_size,
        minimum_pair_frequency=2,
        base_tokens=None,
        verbose=True,
    ):
        if not text:
            raise ValueError(
                "Training text cannot be empty"
            )

        if base_tokens is None:
            base_tokens = sorted(
                set(text)
            )
        else:
            base_tokens = sorted(
                set(base_tokens)
            )

        missing_characters = (
            set(text) - set(base_tokens)
        )

        if missing_characters:
            raise ValueError(
                "Base vocabulary is missing "
                "training characters"
            )

        if (
            target_vocab_size
            < len(base_tokens)
        ):
            raise ValueError(
                "target_vocab_size cannot be "
                "smaller than the number of "
                "base tokens"
            )

        working_tokens = list(text)
        vocabulary = set(base_tokens)
        merges = []

        print(
            "Initial characters:",
            len(working_tokens),
        )

        print(
            "Base vocabulary size:",
            len(base_tokens),
        )

        while (
            len(vocabulary)
            < target_vocab_size
        ):
            pair_counts = count_pairs(
                working_tokens
            )

            if not pair_counts:
                print(
                    "No adjacent pairs remain."
                )
                break

            best_pair, frequency = (
                pair_counts.most_common(1)[0]
            )

            if (
                frequency
                < minimum_pair_frequency
            ):
                print(
                    "Stopping because the most "
                    "frequent pair occurs only "
                    f"{frequency} time(s)."
                )
                break

            merged_token = "".join(
                best_pair
            )

            working_tokens = merge_pair(
                tokens=working_tokens,
                pair=best_pair,
                merged_token=merged_token,
            )

            merges.append(best_pair)
            vocabulary.add(merged_token)

            merge_number = len(merges)

            if (
                verbose
                and (
                    merge_number <= 20
                    or merge_number % 100 == 0
                )
            ):
                print(
                    f"Merge {merge_number:4d}: "
                    f"{best_pair!r} "
                    f"→ {merged_token!r}, "
                    f"frequency={frequency:,}, "
                    f"sequence length="
                    f"{len(working_tokens):,}, "
                    f"vocabulary="
                    f"{len(vocabulary):,}"
                )

        tokenizer = cls(
            base_tokens=base_tokens,
            merges=merges,
        )

        print(
            "Final vocabulary size:",
            tokenizer.vocab_size,
        )

        print(
            "Final training sequence length:",
            len(working_tokens),
        )

        return tokenizer

    @classmethod
    def from_file(cls, path):
        with open(
            path,
            "r",
            encoding="utf-8",
        ) as file:
            data = json.load(file)

        return cls(
            base_tokens=data[
                "base_tokens"
            ],
            merges=data["merges"],
        )

    def encode_tokens(self, text):
        unknown_characters = sorted({
            char
            for char in text
            if char not in self.stoi
        })

        if unknown_characters:
            raise ValueError(
                "Text contains characters "
                "missing from the base vocabulary: "
                f"{unknown_characters}"
            )

        tokens = list(text)

        for left, right in self.merges:
            tokens = merge_pair(
                tokens=tokens,
                pair=(left, right),
                merged_token=left + right,
            )

        return tokens

    def encode(self, text):
        return [
            self.stoi[token]
            for token in self.encode_tokens(
                text
            )
        ]

    def decode(self, token_ids):
        return "".join(
            self.itos[int(token_id)]
            for token_id in token_ids
        )

    def save(self, path):
        path = Path(path)

        path.parent.mkdir(
            parents=True,
            exist_ok=True,
        )

        data = {
            "type": "character_bpe",
            "base_tokens": (
                self.base_tokens
            ),
            "merges": [
                [left, right]
                for left, right
                in self.merges
            ],
            "vocab_size": (
                self.vocab_size
            ),
            "fingerprint": (
                self.fingerprint()
            ),
        }

        temporary_path = path.with_suffix(
            ".tmp"
        )

        with open(
            temporary_path,
            "w",
            encoding="utf-8",
        ) as file:
            json.dump(
                data,
                file,
                ensure_ascii=False,
                indent=2,
            )

        temporary_path.replace(path)

    def fingerprint(self):
        payload = json.dumps(
            {
                "base_tokens": (
                    self.base_tokens
                ),
                "merges": [
                    list(pair)
                    for pair in self.merges
                ],
            },
            ensure_ascii=False,
            separators=(",", ":"),
        ).encode("utf-8")

        return hashlib.sha256(
            payload
        ).hexdigest()


# ==================================================
# Validation
# ==================================================

def validate_round_trip(
    tokenizer,
    text,
    split_name,
):
    encoded = tokenizer.encode(text)
    decoded = tokenizer.decode(encoded)

    if decoded != text:
        raise AssertionError(
            f"Round-trip failed for "
            f"{split_name}"
        )

    print(
        f"{split_name} round-trip passed: "
        f"{len(text):,} characters "
        f"→ {len(encoded):,} tokens"
    )

    return encoded


def save_token_tensor(
    token_ids,
    path,
):
    path = Path(path)

    path.parent.mkdir(
        parents=True,
        exist_ok=True,
    )

    tensor = torch.tensor(
        token_ids,
        dtype=torch.long,
    )

    temporary_path = path.with_suffix(
        ".tmp"
    )

    torch.save(
        tensor,
        temporary_path,
    )

    temporary_path.replace(path)

    return tensor


# ==================================================
# Arguments
# ==================================================

def parse_args():
    parser = argparse.ArgumentParser(
        description=(
            "Train a simple educational BPE "
            "tokenizer on WikiText-2"
        )
    )

    parser.add_argument(
        "--processed-dir",
        type=str,
        default="data/processed",
    )

    parser.add_argument(
        "--tokenizer-output",
        type=str,
        default=(
            "tokenizer/bpe_tokenizer.json"
        ),
    )

    parser.add_argument(
        "--encoded-output-dir",
        type=str,
        default="data/encoded_bpe",
    )

    parser.add_argument(
        "--target-vocab-size",
        type=int,
        default=500,
    )

    parser.add_argument(
        "--training-characters",
        type=int,
        default=500_000,
    )

    parser.add_argument(
        "--minimum-pair-frequency",
        type=int,
        default=2,
    )

    return parser.parse_args()


# ==================================================
# Main Program
# ==================================================

def main():
    args = parse_args()

    processed_dir = Path(
        args.processed_dir
    )

    train_path = (
        processed_dir
        / "wikitext2_train_clean.txt"
    )

    val_path = (
        processed_dir
        / "wikitext2_val_clean.txt"
    )

    test_path = (
        processed_dir
        / "wikitext2_test_clean.txt"
    )

    for path in [
        train_path,
        val_path,
        test_path,
    ]:
        if not path.exists():
            raise FileNotFoundError(
                f"Required file not found: "
                f"{path}"
            )

    train_text = train_path.read_text(
        encoding="utf-8"
    )

    val_text = val_path.read_text(
        encoding="utf-8"
    )

    test_text = test_path.read_text(
        encoding="utf-8"
    )

    print(
        "Training characters:",
        len(train_text),
    )

    print(
        "Validation characters:",
        len(val_text),
    )

    print(
        "Test characters:",
        len(test_text),
    )

    all_characters = sorted(
        set(
            train_text
            + val_text
            + test_text
        )
    )

    training_character_count = min(
        args.training_characters,
        len(train_text),
    )

    bpe_training_text = train_text[
        :training_character_count
    ]

    print(
        "\nBPE training characters:",
        len(bpe_training_text),
    )

    tokenizer = BPETokenizer.train(
        text=bpe_training_text,
        target_vocab_size=(
            args.target_vocab_size
        ),
        minimum_pair_frequency=(
            args.minimum_pair_frequency
        ),
        base_tokens=all_characters,
        verbose=True,
    )

    tokenizer.save(
        args.tokenizer_output
    )

    print(
        "\nTokenizer saved to:",
        args.tokenizer_output,
    )

    print(
        "Tokenizer fingerprint:",
        tokenizer.fingerprint(),
    )

    loaded_tokenizer = (
        BPETokenizer.from_file(
            args.tokenizer_output
        )
    )

    assert (
        loaded_tokenizer.fingerprint()
        == tokenizer.fingerprint()
    )

    print(
        "Tokenizer reload test passed."
    )

    print(
        "\nEncoding dataset splits..."
    )

    train_ids = validate_round_trip(
        tokenizer=loaded_tokenizer,
        text=train_text,
        split_name="Training",
    )

    val_ids = validate_round_trip(
        tokenizer=loaded_tokenizer,
        text=val_text,
        split_name="Validation",
    )

    test_ids = validate_round_trip(
        tokenizer=loaded_tokenizer,
        text=test_text,
        split_name="Test",
    )

    output_dir = Path(
        args.encoded_output_dir
    )

    train_tensor = save_token_tensor(
        train_ids,
        output_dir / "train_ids.pt",
    )

    val_tensor = save_token_tensor(
        val_ids,
        output_dir / "val_ids.pt",
    )

    test_tensor = save_token_tensor(
        test_ids,
        output_dir / "test_ids.pt",
    )

    for split_name, tensor in [
        ("Training", train_tensor),
        ("Validation", val_tensor),
        ("Test", test_tensor),
    ]:
        assert tensor.min().item() >= 0

        assert (
            tensor.max().item()
            < loaded_tokenizer.vocab_size
        )

        print(
            f"{split_name} tensor: "
            f"shape={tuple(tensor.shape)}, "
            f"min={tensor.min().item()}, "
            f"max={tensor.max().item()}"
        )

    character_tokens = len(
        train_text
    )

    bpe_tokens = len(
        train_ids
    )

    compression_factor = (
        character_tokens / bpe_tokens
    )

    reduction_percentage = (
        1.0
        - bpe_tokens / character_tokens
    ) * 100.0

    print(
        "\nCharacter token count:",
        f"{character_tokens:,}",
    )

    print(
        "BPE token count:",
        f"{bpe_tokens:,}",
    )

    print(
        "Compression factor:",
        f"{compression_factor:.2f}x",
    )

    print(
        "Token-count reduction:",
        f"{reduction_percentage:.2f}%",
    )

    learned_tokens = (
        loaded_tokenizer.tokens[
            len(
                loaded_tokenizer.base_tokens
            ):
        ]
    )

    longest_tokens = sorted(
        learned_tokens,
        key=len,
        reverse=True,
    )

    print("\nLongest learned tokens:")

    for token in longest_tokens[:30]:
        print(
            f"{loaded_tokenizer.stoi5d} "
            f"{token!r}"
        )

    sample = (
        "Artificial intelligence "
        "and machine learning"
    )

    unknown = [
        char
        for char in sample
        if char not in (
            loaded_tokenizer.stoi
        )
    ]

    if not unknown:
        sample_tokens = (
            loaded_tokenizer.encode_tokens(
                sample
            )
        )

        sample_ids = (
            loaded_tokenizer.encode(
                sample
            )
        )

        print("\nSample text:")
        print(sample)

        print("\nSample BPE tokens:")
        print(sample_tokens)

        print("\nSample token IDs:")
        print(sample_ids)

        print("\nDecoded sample:")
        print(
            loaded_tokenizer.decode(
                sample_ids
            )
        )

    print(
        "\nBPE tokenizer pipeline "
        "completed successfully."
    )


if __name__ == "__main__":
    main()

Его запуск

Стандартная настройка — это короткі, навчальныя запускі:

python build_bpe_tokenizer.py

Ёсць можласць выучыць большы вокабуляр:

python build_bpe_tokenizer.py \
  --target-vocab-size 1000

Ёсць таксама можласць трэнавацца на большай колькасці тексту:

python build_bpe_tokenizer.py \
  --training-characters 1000000 \
  --target-vocab-size 1000

Для шырокага тэста, які проста паўтарае, што канвэй працуе:

python build_bpe_tokenizer.py \
  --training-characters 100000 \
  --target-vocab-size 300

Якщо ваша шэл не падтрымлівае продовжэнне ліній за дапамою зворотнага слэша, запішыце кожную команду на адной лініі.

Як выглядае выходныя даны

Выкананне можа выдаты ў такім выглядзе:

Training characters: 10,000,000
Validation characters: 1,100,000
Test characters: 1,200,000

BPE training characters: 500,000
Initial characters: 500,000
Base vocabulary size: 200

Merge    1: (' ', 't') → ' t', frequency=19,842
Merge    2: ('h', 'e') → 'he', frequency=17,415
Merge    3: (' t', 'he') → ' the', frequency=14,108
...
Final vocabulary size: 500

Першыя з’еднанні ўсё такія, якіх можна было бы спакавацца ад англійскай мовы: праслова пасля якога следуе t, потым he, а пасля чаго гэтыя два символы з’еднваюцца ў " the". Пасля кодавання резюме выглядае прыблізна так:

Character token count: 10,000,000
BPE token count: 4,500,000
Compression factor: 2.22x
Token-count reduction: 55.00%

Вашы цыфры буду разныя. Значыце, адзін раз запуск і ўзворотны запуск усіх дадзеных для навучэння з адным гэтым кодэрам можа значна паўзіць час выконання пры большых словніках.

Што змініцца для моделі і яе вікна контексту

Колькасць токэнаў у рэальным выразе

Возьмімо гэты выраз:

The history of artificial intelligence

Токенізатор симвала стварае прыблізна столькі токенаў:

38 tokens

Токенізатор BPE можа ствараць токены цэлых слоў, такія як гэтыя:

["The", " history", " of", " artificial", " intelligence"]

Альбо ён можа ствараць меншыя фрагменты, залежна ад таго, якія злічэнні ён выучыў. У будзь-якім случае, колькасць будзе наступная:

5 to 15 tokens

Сегментацыя не ствараецца вручную; яна выклікаецца на адказе статыстык трэнінговага корпусу.

Больша колькасць тексту ў вікні контэксту

Падазроўваючы, што модель выкарыстоўвае такы розмер блока:

block_size = 128

З выкарыстоўваннем симвалоў 128 токэнаў пакрывае або-такі ж 128 симвалоў. За дапамогою BPE тыя ж 128 токэнаў можаюць пакрыць калькі сотняў симвалоў. Модель бачыць больш языковага контэксту, не збільшаючы пры тым вычысленні увагі. Гэта мае значэнне, калі ўжо ўскладнення сама-увагі зростае прыбліжна пры:

T²

Ёсць T — гэта дужына последоўнасці; таму удвоўчыненне T прыблізна у чатыро разы збільшывае вартасць обробкі, тады як кращая токенізацыя дазволяе пакаваць больш тексту ў той самы T.

Компроміс межу пры галęзі слоўніку

Большы слоўнік BPE зазвычай означае корэйшыя последоўнасці, але таксама змушвае пэўныя матрыцы стаць большымі. Табела вкладків токенавання мае столькі параметраў:

vocab_size × embedding_dim

Калі вагі з’єднаны, выходны галоверк викорыстоўвае тую ж самую матрыцу, замест таго каб дадаць другую. Пораўняйце маленькі слоўнік:

Vocabulary:          500
Embedding dimension: 128
Embedding parameters: 64,000

З большым:

Vocabulary: 8,000
Embedding dimension: 128
Embedding parameters: 1,024,000

Такім чынам, большыя словнікі скрацваюць дужкі, але падышаюць параметры імбеддынгу, вартасць логітасоў выходу, выкарыстоўванне памяці і час навучэння токенайзера. Кожны дадатковы токен таксама патрэбуе достатнькай колькасці прыкладоў, каб выучыць хорашы імбеддынг, таму ў корпусе розмерам WikiText-2 вельмі большы словнік дае многа токенав, якія практычна не навучаны. Правильны розмер залежыць ад набора дадзеных і модэлю.

Павторна навучэнне модэлю на токенах BPE

Адкорэктаваць скрыпт навучэння, каб ён чытаў файл токенайзера:

tokenizer/bpe_tokenizer.json

І кодаваныя часткі:

data/encoded_bpe/train_ids.pt
data/encoded_bpe/val_ids.pt
data/encoded_bpe/test_ids.pt

Потым створыце канфігурацыю на адказе розмеру слоўніку токенізатора:

config = MiniGPTConfig(
    vocab_size=bpe_tokenizer.vocab_size,
    block_size=128,
    embedding_dim=128,
    num_heads=4,
    num_layers=4,
    expansion_factor=4,
    dropout=0.1,
)

І запісуйце точкі перевяркі ў свой каталог:

checkpoints/bpe/

За выключэннем эйха змян, цікл адучэння можа застацца практычна такім, які ён быў.

Справядлівая параболка двух модэляў

Для значымай параболкі трэба, калі толькі можна, застаць незменнымі архітэктуру (шырыню, глыбіню, элементы), даныя, оптымізатор, графік, бюджет адучэння і метод ацэнкі. Хоця і так, параболка на аднаковым колькасці крокаў адучэння є вадзейнай, таму што пакет токэнаў BPE містіць значна больш тексту, чым пакет симвалів. Кращыя параметры для параболкі ўсунуць:

Total characters processed
Total bytes processed
Training time
Compute budget

Пэрплексітас вычысляецца за едыніцю прыгадкі, таму пэрплексітасы знакоў і BPE не можна спакойна парабяляваць. Ёсць адна цифра, якая можна парабяляваць — гэта біты на знак; інакш кожны модель трэба ацэніваць за якасцю генеравання, вычысляць выклікі і памеры, прыемныя для токенайзера.

Звычныя памылкі

Стварэнне адзінаковага токенайзера для кожнага раздзелу

Гэта некалькава:

Train tokenizer from training text
Validation tokenizer from validation text
Test tokenizer from test text

Данныя для трэніравання, параболікацыі і тэставання павінны быць кодаваныя аднам і тым жа токенайзерам; інакша адны і тые ж ID будуць абначальваць разныя рэчы ў разных частках дадзеных.

Застосоўка злучэнняў не па порядку

Злучэнняя будуць ствараныя адна на адной, таму іх павінны пераглядаць або выкананыя адным чым точна па тым порядку, якія яны былі выучаны:

for pair in self.merges:
    apply(pair)

Сортаванне правіл, напрыклад, абеткаічнаў, для лепшай чытаемасці, зменяе спосаб раздзелення тексту.

Замена файлаў з кодаваннем симвалаў

Зберагачыце ID BPE ў сваім сабе каталогу:

data/encoded_bpe/

І застаўце ID-ы симвалоў там, дзе яны былі:

data/encoded/

Адаптаванне на базе тексту для параболікі

Статыстыкі адаптавання павінны выходзіць толькі з тексту для навчання. Выкарыстоўванне базовага набора симвалаў з кожнага фрагмента дазволена для абсягу, але частоты адаптавання павінны адбівацца толькі на адпаведным корпусе дадзеных, інакша показнікі параболікі будуць завышаныя.

Аспекты, якія трэба врачыць, калі спадзяваецеся, што навучаныя токены будуць цэлымі словамі

BPE з задоволеннем навчаецца такім фрагментам, як:

"ing"
"tion"
" th"
" arti"

Токены-падзворды выклікаюцца на адпаведнасць частоты, а не на людзкія межы слоў, і гэта ўсуненая працэза.

Прыпуск, што большы слоўнік завжды лепшы

Большыя слоўнікі збільшуюць кантэкст параметры і ствараюць рэдкія, паслаба навучаныя токены. Прабавайце разныя розмеры і ацэніце рэзультаты. Таксама ніколі не парабяльвайце чыстую перплексітэт між разнымі токенізаторамі.

Заняткі

  1. Парабяльвайце тры розмеры слоўніка. Навучайце з указанымі розмерамі і парабяльвайце час навучэння, дужыну последоўнасці, ступень стыснення, дыявольскія токены і розмер імбеддінгу.
300 tokens
500 tokens
1,000 tokens
  1. Адзёрнайце раннія спаўнення. Аддрукуйце першыя 50 правіл спаўнення. Чы гледзіцца шаблоны прабелаў? Частыя суфіксы? Комбінаціі знакаў падзеўкі? Калія спаўнення вас здзівілі?
  2. Токенаізуйце адну фразу двума спосабамі. Кодавайце следуючую фразу як за дапамогою токенаізатора на адзінкі, так і за дапамогою токенаізатора BPE, і парабяльцуйце токены, ідэнтыфікаторы, колькісць токенаў і рэзультат декодавання.
Artificial intelligence is transforming technology.
  1. Прабуйце рэдкія словы. Кодавайце следуючыя словы і старайцеся зрозумець, як BPE складае незнайомыя словы з вядомых частак.
electromagnetism
neurocomputational
uncharacteristically
  1. Зьвяржыце кантэкставую паверхносць. Дэкодавайце першыя 128 токэнаў пад кожным токенайзерам і адзначайце, які частака тэксту пакрывае кожны вікнар.
  2. Адучыце модэль BPE. Адучыце яе на дырэктарыі, указанай нижэй, і парабялейце яе прыклады з модэлью на базе симвалоў за аднаковых умов вычысленняў.
data/encoded_bpe/
  1. Застосавайце атрыбуты відбіўніка. Храніце відбіўнік токенізатора ў кожным пункце перапытку і адмовіцеся ад выканання обробкі, калі пункт перапытку і дзейны токенізатор не збігаюцца.
  2. Прасуніце прыгон кодавання. Заменіце кодавач, які выконвае адна пераходка за кожна злучэння, на болей шырокі, выкарыстоўваючы рангі злучэнняў, купкі, сяродзвязаныя спискі токенав, кэшы па словам або іншыя аналогічныя методы.

Правілы токенізацыі, якія варта прытрымвацца

  • Падсловы знаходзяцца між гнеклівымі, але дугімі символамі і компактнымі, але кранкавымі словамі.
  • Токенізатор BPE цэлком визначаецца яго базовымі токенамі плюс ўпорядкованымі злучэннямі; зберагачыце і ствараюце відбіўнік самэх гэтых элементаў.
  • Кодаванне выконвае злучэнняя па порядку, декодаванне — спаўнюе іх, і процесы кодавання/декодавання адбываюцца без втрат.
  • Одні токенізаторы выкарыстоўваюцца для кожнага розбівання, а злучэнняя выучваюцца толькі з дадзеных для навчання.
  • Новы токенізатор значыць новыя файлы токэнаў, большыя размеры слоўніку і апошнейша натрэнаваны модэль.
  • Размер слоўніку дазволяе зменшыць дужыну последоўнасцей на корыстце большых шароў укладкі і выходу, а пэрплексітас не пераходзіць з аднаго токенізатора на іншы.
  • Полны процес, ад сырога тексту да ператрэнаванай модэлі, выглядае так:

    WikiText training text
    → character vocabulary
    → pair frequencies
    → repeated merge operations
    → BPE vocabulary and merge rules
    → encode all splits
    → save token tensors
    → train a new Mini-GPT
    

    Гэтыя падтрымкі паказуюць, што токенізацыя — гэта не проста деталь падготавкі дадзеных. Яна вялікі вплыв на тое, якія елементы вучыць модель, сколькі текста можа поместыцца ў яе вікна контэксту і якой будзе размер проблемы пры вычысленні выходных рэзультатаў. Калі маленькі GPT працуе з токенамі BPE, наступны логічны крок — паглядзець, як тая ж самая сістэма токенізацыі развіваецца ў сучасныя системы генератыўнага ІІ:

    Next-token predictor
    → pretrained language model
    → instruction-following model
    → retrieval-augmented application
    → tool-using GenAI system
    

    Супаўзвязаныя матэрыялы

  • З ГПТ-1 да модэляў для разумавання: Што кожна генерацыя зменіла для разработчыкаў: Аналізуем сяброў ГПТ ад прадзейнаўкі ў 2018 годзе да модэляў для разумавання, бачым, якія ідэі дадала кожная генерацыя, а таксама пазнаём функцыі GPT-4o vision і Structured Outputs у Python.
  • Адучэнне малага LLM з нуля на ноутбуку з 8 GB RAM: Как стварыць чыстую схему обробкі дадзенняяў для адучэння трансфармера на простым апаратызме: BPE-токенайзер, TinyStories, токены на дыску і групаванне наступных токенав.