#
Tokenizarea Byte-Pair Encoding

Byte-Pair Encoding (BPE) a fost inițial dezvoltat ca un algoritm de comprimare a textelor și apoi utilizat de OpenAI pentru tokenizare la preantrenarea modelului GPT. Acesta este utilizat de o mulțime de modele Transformers, inclusiv GPT, GPT-2, RoBERTa, BART și DeBERTa.

💡 Această secțiune acoperă BPE în profunzime, mergând până la prezentarea unei implementări complete. Puteți sări la sfârșit dacă doriți doar o prezentare generală a algoritmului de tokenizare.

##
Algoritmul de antrenare

Antrenarea BPE începe prin calcularea setului unic de cuvinte utilizate în corpus (după finalizarea etapelor de normalizare și pre-tokenizare), apoi construirea vocabularului prin preluarea tuturor simbolurilor utilizate pentru scrierea acestor cuvinte. Ca un exemplu foarte simplu, să spunem că corpusul nostru utilizează aceste cinci cuvinte:

    "hug", "pug", "pun", "bun", "hugs"

Vocabularul de bază va fi atunci `["b", "g", "h", "n", "p", "s", "u"]`. Pentru cazurile din lumea reală, vocabularul de bază va conține cel puțin toate caracterele ASCII și, probabil, și unele caractere Unicode. Dacă un exemplu pe care îl tokenizați utilizează un caracter care nu se află în corpusul de antrenare, acel caracter va fi convertit într-un token necunoscut. Acesta este unul dintre motivele pentru care o mulțime de modele NLP sunt foarte proaste la analizarea conținutului cu emoji, de exemplu.

Tokenizerele GPT-2 și RoBERTa (care sunt destul de asemănătoare) au o modalitate inteligentă de a rezolva acest lucru: ele nu privesc cuvintele ca fiind scrise cu caractere Unicode, ci cu bytes. În acest fel, vocabularul de bază are o dimensiune mică (256), dar fiecare caracter la care vă puteți gândi va fi inclus și nu va ajunge să fie convertit într-un token necunoscut. Acest truc se numește byte-level BPE.

După obținerea acestui vocabular de bază, adăugăm noi tokeni până când se atinge dimensiunea dorită a vocabularului prin învățarea prin merges, care sunt reguli de merge a două elemente ale vocabularului existent într-unul nou. Astfel, la început, aceste fuziuni vor crea tokenuri cu două caractere, iar apoi, pe măsură ce antrenamentul progresează, subwords mai lungi.

În orice etapă din timpul antrenării tokenizerului, algoritmul BPE va căuta cea mai frecventă pereche de tokenuri existente (prin “pereche” înțelegem aici doi tokeni consecutivi într-un cuvânt). Acea pereche cea mai frecventă este cea care va fi mergea, iar noi ștergem și repetăm pentru pasul următor.

Revenind la exemplul nostru anterior, să presupunem că cuvintele au următoarele frecvențe:

    ("hug", 10), ("pug", 5), ("pun", 12), ("bun", 4), ("hugs", 5)

înțelegând că `"hug"` a fost prezent de 10 ori în corpus, `"pug"` de 5 ori, `"pun"` de 12 ori, `"bun"` de 4 ori, iar `"hugs"` de 5 ori. Începem antrenamentul împărțind fiecare cuvânt în caractere (cele care formează vocabularul nostru inițial), astfel încât să putem vedea fiecare cuvânt ca pe o listă de tokeni:

    ("h" "u" "g", 10), ("p" "u" "g", 5), ("p" "u" "n", 12), ("b" "u" "n", 4), ("h" "u" "g" "s", 5)

Apoi ne uităm la perechi. Perechea `("h", "u")` este prezentă în cuvintele `"hug"` și `"hugs"`, deci de 15 ori în total în corpus. Totuși, nu este cea mai frecventă pereche: această onoare revine perechii `("u", "g")`, care este prezentă în cuvintele `"hug"`, `"pug"` și `"hugs"`, pentru un total de 20 de ori în vocabular.

Astfel, prima regulă de merge învățată de tokenizer este `("u", "g") -> "ug"`, ceea ce înseamnă că `"ug"` va fi adăugat la vocabular, iar perechea ar trebui să fie merged în toate cuvintele din corpus. La sfârșitul acestei etape, vocabularul și corpus-ul arată astfel:

    Vocabulary: ["b", "g", "h", "n", "p", "s", "u", "ug"]
    Corpus: ("h" "ug", 10), ("p" "ug", 5), ("p" "u" "n", 12), ("b" "u" "n", 4), ("h" "ug" "s", 5)

Acum avem câteva perechi care rezultă într-un token mai lung de două caractere: perechea `("h", "ug")`, de exemplu (prezentă de 15 ori în corpus). Cea mai frecventă pereche în această etapă este `("u", "n")`, prezentă însă de 16 ori în corpus, astfel încât a doua regulă de îmbinare învățată este `("u", "n") -> "un"`. Adăugarea acestei reguli la vocabular și mergingul tuturor aparițiilor existente ne conduce la:

    Vocabulary: ["b", "g", "h", "n", "p", "s", "u", "ug", "un"]
    Corpus: ("h" "ug", 10), ("p" "ug", 5), ("p" "un", 12), ("b" "un", 4), ("h" "ug" "s", 5)

Acum, cea mai frecventă pereche este `("h", "ug")`, așa că învățăm regula de erge `("h", "ug") -> "hug"`, care ne oferă primul nostru simbol din trei litere. După fuzionare, corpusul arată astfel:

    Vocabulary: ["b", "g", "h", "n", "p", "s", "u", "ug", "un", "hug"]
    Corpus: ("hug", 10), ("p" "ug", 5), ("p" "un", 12), ("b" "un", 4), ("hug" "s", 5)

Și continuăm astfel până când ajungem la dimensiunea dorită a vocabularului.

✏️ Acum e rândul tău! Care crezi că va fi următoarea regulă de fuziune?

##
Algoritmul de tokenizare

Tokenizarea urmează îndeaproape procesul de antrenare, în sensul că noile inputuri sunt tokenizate prin aplicarea următoarelor etape:

1. Normalizare
2. Pre-tokenizare
3. Divizarea cuvintelor în caractere individuale
4. Aplicarea regulilor de merge învățate în ordine asupra acestor împărțiri

Să luăm exemplul pe care l-am folosit în timpul antrenamentului, cu cele trei reguli de merge învățate:

    ("u", "g") -> "ug"
    ("u", "n") -> "un"
    ("h", "ug") -> "hug"

Cuvântul `"bug"` va fi tokenizat ca `["b", "ug"]`. Cu toate acestea, cuvântul `"mug"` va fi tokenizat ca `["[UNK]", "ug"]` deoarece litera `"m"` nu a fost în vocabularul de bază. De asemenea, cuvântul `"thug"` va fi tokenizat ca `["[UNK]", "hug"]`: litera `"t"` nu se află în vocabularul de bază, iar aplicarea regulilor de merge duce mai întâi la fuzionarea lui `"u"` și `"g"` și apoi la fuzionarea lui `"h"` și `"ug"`.

✏️ Acum e rândul tău! Cum crezi că va fi tokenizat cuvântul `"unhug"`?

##
Implementarea BPE

Acum să aruncăm o privire la implementarea algoritmului BPE. Aceasta nu va fi o versiune optimizată pe care o puteți utiliza pe un corpus mare; dorim doar să vă arătăm codul pentru a putea înțelege algoritmul puțin mai bine.

În primul rând avem nevoie de un corpus, așa că haideți să creăm unul simplu cu câteva propoziții:

    corpus = [
        "This is the Hugging Face Course.",
        "This chapter is about tokenization.",
        "This section shows several tokenizer algorithms.",
        "Hopefully, you will be able to understand how they are trained and generate tokens.",
    ]

Apoi, trebuie să pre-tokenizăm acest corpus în cuvinte. Deoarece replicăm un tokenizator BPE (precum GPT-2), vom utiliza tokenizatorul `gpt2` pentru pre-tokenizare:

    from transformers import AutoTokenizer

    tokenizer = AutoTokenizer.from_pretrained("gpt2")

Apoi, calculăm frecvențele fiecărui cuvânt din corpus la fel ca în cazul pre-tokenizării:

    from collections import defaultdict

    word_freqs = defaultdict(int)

    for text in corpus:
        words_with_offsets = tokenizer.backend_tokenizer.pre_tokenizer.pre_tokenize_str(text)
        new_words = [word for word, offset in words_with_offsets]
        for word in new_words:
            word_freqs[word] += 1

    print(word_freqs)

    defaultdict(int, {'This': 3, 'Ġis': 2, 'Ġthe': 1, 'ĠHugging': 1, 'ĠFace': 1, 'ĠCourse': 1, '.': 4, 'Ġchapter': 1,
        'Ġabout': 1, 'Ġtokenization': 1, 'Ġsection': 1, 'Ġshows': 1, 'Ġseveral': 1, 'Ġtokenizer': 1, 'Ġalgorithms': 1,
        'Hopefully': 1, ',': 1, 'Ġyou': 1, 'Ġwill': 1, 'Ġbe': 1, 'Ġable': 1, 'Ġto': 1, 'Ġunderstand': 1, 'Ġhow': 1,
        'Ġthey': 1, 'Ġare': 1, 'Ġtrained': 1, 'Ġand': 1, 'Ġgenerate': 1, 'Ġtokens': 1})

Următorul pas este calcularea vocabularului de bază, format din toate caracterele utilizate în corpus:

    alphabet = []

    for word in word_freqs.keys():
        for letter in word:
            if letter not in alphabet:
                alphabet.append(letter)
    alphabet.sort()

    print(alphabet)

    [ ',', '.', 'C', 'F', 'H', 'T', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'k', 'l', 'm', 'n', 'o', 'p', 'r', 's',
      't', 'u', 'v', 'w', 'y', 'z', 'Ġ']

De asemenea, adăugăm tokenurile speciale utilizate de model la începutul vocabularului respectiv. În cazul GPT-2, singurul simbol special este `"<|endoftext|>"`:

    vocab = ["<|endoftext|>"] + alphabet.copy()

Acum trebuie să împărțim fiecare cuvânt în caractere individuale, pentru a putea începe antrenarea:

    splits = {word: [c for c in word] for word in word_freqs.keys()}

Acum că suntem pregătiți pentru antrenare, să scriem o funcție care calculează frecvența fiecărei perechi. Va trebui să folosim această funcție la fiecare etapă a antrenare:

    def compute_pair_freqs(splits):
        pair_freqs = defaultdict(int)
        for word, freq in word_freqs.items():
            split = splits[word]
            if len(split) == 1:
                continue
            for i in range(len(split) - 1):
                pair = (split[i], split[i + 1])
                pair_freqs[pair] += freq
        return pair_freqs

Să aruncăm o privire la o parte din acest dicționar după separările inițiale:

    pair_freqs = compute_pair_freqs(splits)

    for i, key in enumerate(pair_freqs.keys()):
        print(f"{key}: {pair_freqs[key]}")
        if i >= 5:
            break

    ('T', 'h'): 3
    ('h', 'i'): 3
    ('i', 's'): 5
    ('Ġ', 'i'): 2
    ('Ġ', 't'): 7
    ('t', 'h'): 3

Acum, pentru a găsi cea mai frecventă pereche este nevoie doar de o loop rapid:

    best_pair = ""
    max_freq = None

    for pair, freq in pair_freqs.items():
        if max_freq is None or max_freq < freq:
            best_pair = pair
            max_freq = freq

    print(best_pair, max_freq)

    ('Ġ', 't') 7

Așadar, prima îmbinare care trebuie învățată este `('Ġ', 't') -> 'Ġt'`, și adăugăm `'Ġt'` la vocabular:

    merges = {("Ġ", "t"): "Ġt"}
    vocab.append("Ġt")

Pentru a continua, trebuie să aplicăm aceast merge în dicționarul nostru `splits`. Să scriem o altă funcție pentru acest lucru:

    def merge_pair(a, b, splits):
        for word in word_freqs:
            split = splits[word]
            if len(split) == 1:
                continue

            i = 0
            while i < len(split) - 1:
                if split[i] == a and split[i + 1] == b:
                    split = split[:i] + [a + b] + split[i + 2 :]
                else:
                    i += 1
            splits[word] = split
        return splits

Și putem arunca o privire la rezultatul primului merge:

    splits = merge_pair("Ġ", "t", splits)
    print(splits["Ġtrained"])

    ['Ġt', 'r', 'a', 'i', 'n', 'e', 'd']

Acum avem tot ce ne trebuie pentru a face bucle până când vom învăța toate mergeu-rile dorite. Ne focusăm pe o mărime a vocabularui de 50:

    vocab_size = 50

    while len(vocab) < vocab_size:
        pair_freqs = compute_pair_freqs(splits)
        best_pair = ""
        max_freq = None
        for pair, freq in pair_freqs.items():
            if max_freq is None or max_freq < freq:
                best_pair = pair
                max_freq = freq
        splits = merge_pair(*best_pair, splits)
        merges[best_pair] = best_pair[0] + best_pair[1]
        vocab.append(best_pair[0] + best_pair[1])

Ca rezultat, am învățat 19 reguli de merge(vocabularul inițial avea o dimensiune de 31 — 30 de caractere din alfabet, plus simbolul special):

    print(merges)

    {('Ġ', 't'): 'Ġt', ('i', 's'): 'is', ('e', 'r'): 'er', ('Ġ', 'a'): 'Ġa', ('Ġt', 'o'): 'Ġto', ('e', 'n'): 'en',
     ('T', 'h'): 'Th', ('Th', 'is'): 'This', ('o', 'u'): 'ou', ('s', 'e'): 'se', ('Ġto', 'k'): 'Ġtok',
     ('Ġtok', 'en'): 'Ġtoken', ('n', 'd'): 'nd', ('Ġ', 'is'): 'Ġis', ('Ġt', 'h'): 'Ġth', ('Ġth', 'e'): 'Ġthe',
     ('i', 'n'): 'in', ('Ġa', 'b'): 'Ġab', ('Ġtoken', 'i'): 'Ġtokeni'}

Iar vocabularul este compus din simbolul special, alfabetul inițial și toate rezultatele merge-urilor:

    print(vocab)

    ['<|endoftext|>', ',', '.', 'C', 'F', 'H', 'T', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'k', 'l', 'm', 'n', 'o',
     'p', 'r', 's', 't', 'u', 'v', 'w', 'y', 'z', 'Ġ', 'Ġt', 'is', 'er', 'Ġa', 'Ġto', 'en', 'Th', 'This', 'ou', 'se',
     'Ġtok', 'Ġtoken', 'nd', 'Ġis', 'Ġth', 'Ġthe', 'in', 'Ġab', 'Ġtokeni']

💡 Folosind `train_new_from_iterator()` pe același corpus nu va rezulta exact același vocabular. Acest lucru se datorează faptului că atunci când există o alegere a celei mai frecvente perechi, am selectat-o pe prima întâlnită, în timp ce biblioteca 🤗 Tokenizers o selectează pe prima pe baza ID-urilor sale interne.

Pentru a tokeniza un text nou, îl pre-tokenizăm, îl împărțim, apoi aplicăm toate regulile de merge învățate:

    def tokenize(text):
        pre_tokenize_result = tokenizer._tokenizer.pre_tokenizer.pre_tokenize_str(text)
        pre_tokenized_text = [word for word, offset in pre_tokenize_result]
        splits = [[l for l in word] for word in pre_tokenized_text]
        for pair, merge in merges.items():
            for idx, split in enumerate(splits):
                i = 0
                while i < len(split) - 1:
                    if split[i] == pair[0] and split[i + 1] == pair[1]:
                        split = split[:i] + [merge] + split[i + 2 :]
                    else:
                        i += 1
                splits[idx] = split

        return sum(splits, [])

Putem încerca acest lucru pe orice text compus din caractere din alfabet:

    tokenize("This is not a token.")

    ['This', 'Ġis', 'Ġ', 'n', 'o', 't', 'Ġa', 'Ġtoken', '.']

⚠️ Implementarea noastră va arunca o eroare dacă există un caracter necunoscut, deoarece nu am făcut nimic pentru a le gestiona. GPT-2 nu are de fapt un token necunoscut (este imposibil să obțineți un caracter necunoscut atunci când utilizați BPE la nivel de bytes), dar acest lucru s-ar putea întâmpla aici deoarece nu am inclus toate byte-urile posibile în vocabularul inițial. Acest aspect al BPE depășește domeniul de aplicare al acestei secțiuni, așa că am omis detaliile.

Asta e tot pentru algoritmul BPE! În continuare, ne vom uita la WordPiece.

< > Update on GitHub