#
Tokenizarea WordPiece

WordPiece este algoritmul de tokenizare dezvoltat de Google pentru preantrenarea BERT. De atunci, acesta a fost reutilizat în numeroase modele Transformers bazate pe BERT, cum ar fi DistilBERT, MobileBERT, Funnel Transformers și MPNET. Este foarte similar cu BPE în ceea ce privește antrenarea, dar tokenizarea efectivă se face diferit.

💡 Această secțiune acoperă WordPiece î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

⚠️ Google nu a publicat niciodată implementarea algoritmului de formare a WordPiece, astfel încât ceea ce urmează este cea mai bună presupunere a noastră bazată pe literatura publicată. Este posibil să nu fie 100% exactă.

La fel ca BPE, WordPiece pornește de la un vocabular restrâns care include simbolurile speciale utilizate de model și alfabetul inițial. Deoarece identifică subcuvinte prin adăugarea unui prefix (cum ar fi `##` pentru BERT), fiecare cuvânt este inițial împărțit prin adăugarea prefixului respectiv la toate caracterele din cuvânt. Astfel, de exemplu, `"word"` este împărțit astfel:

    w ##o ##r ##d

Astfel, alfabetul inițial conține toate caracterele prezente la începutul unui cuvânt și caracterele prezente în interiorul unui cuvânt cu prefixul WordPiece.

Apoi, la fel ca BPE, WordPiece învață reguli de merge. Principala diferență este modul în care este selectată perechea care urmează să fie merged. În loc să selecteze cea mai frecventă pereche, WordPiece calculează un scor pentru fiecare pereche, utilizând următoarea formulă: s c o r e=(f r e q_o f_p a i r)/(f r e q_o f_f i r s t_e l e m e n t×f r e q_o f_s e c o n d_e l e m e n t)\mathrm{score} = (\mathrm{freq\_of\_pair}) / (\mathrm{freq\_of\_first\_element} \times \mathrm{freq\_of\_second\_element})score=(freq_of_pair)/(freq_of_first_element×freq_of_second_element)

Împărțind frecvența perechii la produsul frecvențelor fiecărei părți a acesteia, algoritmul prioritizează fuzionarea perechilor în care părțile individuale sunt mai puțin frecvente în vocabular. De exemplu, nu va fuziona neapărat `("un", "##able")` chiar dacă această pereche apare foarte frecvent în vocabular, deoarece cele două perechi `"un"` și `"##able"` vor apărea probabil fiecare într-o mulțime de alte cuvinte și vor avea o frecvență ridicată. În schimb, o pereche precum `("hu", "##gging")` va fi probabil fuzionată mai repede (presupunând că cuvântul “hugging” apare frecvent în vocabular), deoarece `"hu"` și `"##gging"` sunt probabil mai puțin frecvente individual.

Să ne uităm la același vocabular pe care l-am folosit în exemplul de antrenare BPE:

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

Spliturile aici vor fi:

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

deci vocabularul inițial va fi `["b", "h", "p", "##g", "##n", "##s", "##u"]` (dacă uităm deocamdată de tokenurile speciale). Cea mai frecventă pereche este `("##u", "##g")` (prezentă de 20 de ori), dar frecvența individuală a lui `"##u"` este foarte mare, astfel încât scorul său nu este cel mai mare (este 1 / 36). Toate perechile cu un `"##u"` au de fapt același scor (1 / 36), astfel încât cel mai bun scor revine perechii `("##g", "##s")` - singura fără un `"##u"` - cu 1 / 20, iar prima îmbinare învățată este `("##g", "##s") -> ("##gs")`.

Rețineți că atunci când facem merge, eliminăm `##` dintre cele două tokenuri, deci adăugăm `"##gs"` la vocabular și aplicăm merge în cuvintele din corpus:

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

În acest moment, `"##u"` se află în toate perechile posibile, deci toate au același scor. Să spunem că în acest caz, prima pereche este merged, deci `("h", "##u") -> "hu"`. Acest lucru ne duce la:

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

Apoi, următorul scor cu cel mai bun rezultat este împărțit de `("hu", "##g")` și `("hu", "##gs")` (cu 1/15, comparativ cu 1/21 pentru toate celelalte perechi), astfel încât prima pereche cu cel mai mare scor este merged

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

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

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

##
Algoritm de tokenizare

Tokenizarea diferă în WordPiece și BPE prin faptul că WordPiece salvează doar vocabularul final, nu și regulile de merge învățate. Pornind de la cuvântul de tokenizat, WordPiece găsește cel mai lung subcuvânt care se află în vocabular, apoi îl împarte. De exemplu, dacă folosim vocabularul învățat în exemplul de mai sus, pentru cuvântul `"hugs"` cel mai lung subcuvânt de la început care se află în vocabular este `"hug"`, așa că împărțim acolo și obținem `["hug", "##s"]`. Apoi continuăm cu `"##s"`, care se află în vocabular, deci tokenizarea lui `"hugs"` este `["hug", "##s"]`.

Cu BPE, am fi aplicat mergeurile învățate în ordine și am fi tokenizat acest lucru ca `["hu", "##gs"]`, deci encodingul este diferit.

Ca un alt exemplu, să vedem cum ar fi tokenizat cuvântul `"bugs"`. `"b"` este cel mai lung subcuvânt care începe de la începutul cuvântului care se află în vocabular, așa că îl împărțim acolo și obținem `["b", "##ugs"]`. Apoi, `"##u"` este cel mai lung subcuvânt care începe de la începutul cuvântului `"##ugs"` care se află în vocabular, deci îl separăm și obținem `["b", "##u, "##gs"]`. În cele din urmă, `"##gs"` se află în vocabular, deci această ultimă listă este tokenizarea lui `"bugs"`.

Atunci când tokenizarea ajunge într-un stadiu în care nu este posibilă găsirea unui subcuvânt în vocabular, întregul cuvânt este tokenizat ca necunoscut - astfel, de exemplu, `"mug"` ar fi tokenizat ca `["[UNK]"]`, la fel ca `"bum"` (chiar dacă putem începe cu `"b"` și `"##u"`, `"##m"` nu face parte din vocabular, iar tokenizarea rezultată va fi `["[UNK]"]`, nu `["b", "##u", "[UNK]"]`). Aceasta este o altă diferență față de BPE, care ar clasifica doar caracterele individuale care nu se află în vocabular ca necunoscute.

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

##
Implementând WordPiece

Acum să aruncăm o privire la o implementare a algoritmului WordPiece. La fel ca în cazul BPE, acest lucru este doar pedagogic și nu veți putea să îl utilizați pe un corpus mare.

Vom utiliza același corpus ca în exemplul BPE:

    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.",
    ]

În primul rând, trebuie să pre-tokenizăm corpusul în cuvinte. Deoarece replicăm un tokenizator WordPiece (precum BERT), vom utiliza tokenizatorul `bert-base-cased` pentru pre-tokenizare:

    from transformers import AutoTokenizer

    tokenizer = AutoTokenizer.from_pretrained("bert-base-cased")

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

    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})

După cum am văzut mai devreme, alfabetul este setul unic compus din toate primele litere ale cuvintelor și toate celelalte litere care apar în cuvintele cu prefixul `##`:

    alphabet = []
    for word in word_freqs.keys():
        if word[0] not in alphabet:
            alphabet.append(word[0])
        for letter in word[1:]:
            if f"##{letter}" not in alphabet:
                alphabet.append(f"##{letter}")

    alphabet.sort()
    alphabet

    print(alphabet)

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

De asemenea, adăugăm simbolurile speciale utilizate de model la începutul vocabularului respectiv. În cazul BERT, este vorba de lista `[”[PAD]”, ”[UNK]”, ”[CLS]”, ”[SEP]”, ”[MASK]”]“:

    vocab = ["[PAD]", "[UNK]", "[CLS]", "[SEP]", "[MASK]"] + alphabet.copy()

Apoi trebuie să împărțim fiecare cuvânt, cu toate literele care nu sunt primele cu prefixul `##`:

    splits = {
        word: [c if i == 0 else f"##{c}" for i, c in enumerate(word)]
        for word in word_freqs.keys()
    }

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

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

        scores = {
            pair: freq / (letter_freqs[pair[0]] * letter_freqs[pair[1]])
            for pair, freq in pair_freqs.items()
        }
        return scores

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

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

    ('T', '##h'): 0.125
    ('##h', '##i'): 0.03409090909090909
    ('##i', '##s'): 0.02727272727272727
    ('i', '##s'): 0.1
    ('t', '##h'): 0.03571428571428571
    ('##h', '##e'): 0.011904761904761904

Acum, pentru a găsi perechea cu cel mai bun scor este nevoie doar de un loop rapid:

    best_pair = ""
    max_score = None
    for pair, score in pair_scores.items():
        if max_score is None or max_score < score:
            best_pair = pair
            max_score = score

    print(best_pair, max_score)

    ('a', '##b') 0.2

Așadar, primul merge de învățat este `('a', '##b') -> 'ab'`, iar noi adăugăm `'ab'` la vocabular:

    vocab.append("ab")

Pentru a continua, trebuie să aplicăm acest 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:
                    merge = a + b[2:] if b.startswith("##") else a + b
                    split = split[:i] + [merge] + split[i + 2 :]
                else:
                    i += 1
            splits[word] = split
        return splits

Și putem arunca o privire la rezultatul primului merge:

    splits = merge_pair("a", "##b", splits)
    splits["about"]

    ['ab', '##o', '##u', '##t']

Acum avem tot ce ne trebuie pentru a face bucle până când vom învăța toate merge-urile dorite. Să ne propunem un vocabular de mărimea 70:

    vocab_size = 70
    while len(vocab) < vocab_size:
        scores = compute_pair_scores(splits)
        best_pair, max_score = "", None
        for pair, score in scores.items():
            if max_score is None or max_score < score:
                best_pair = pair
                max_score = score
        splits = merge_pair(*best_pair, splits)
        new_token = (
            best_pair[0] + best_pair[1][2:]
            if best_pair[1].startswith("##")
            else best_pair[0] + best_pair[1]
        )
        vocab.append(new_token)

Ne putem uita apoi la vocabularul generat:

    print(vocab)

    ['[PAD]', '[UNK]', '[CLS]', '[SEP]', '[MASK]', '##a', '##b', '##c', '##d', '##e', '##f', '##g', '##h', '##i', '##k',
     '##l', '##m', '##n', '##o', '##p', '##r', '##s', '##t', '##u', '##v', '##w', '##y', '##z', ',', '.', 'C', 'F', 'H',
     'T', 'a', 'b', 'c', 'g', 'h', 'i', 's', 't', 'u', 'w', 'y', 'ab', '##fu', 'Fa', 'Fac', '##ct', '##ful', '##full', '##fully',
     'Th', 'ch', '##hm', 'cha', 'chap', 'chapt', '##thm', 'Hu', 'Hug', 'Hugg', 'sh', 'th', 'is', '##thms', '##za', '##zat',
     '##ut']

După cum putem vedea, în comparație cu BPE, acest tokenizator învață părțile din cuvinte ca tokenuri puțin mai repede.

💡 Folosind `train_new_from_iterator()` pe același corpus nu va rezulta exact același vocabular. Acest lucru se datorează faptului că biblioteca 🤗 Tokenizers nu implementează WordPiece pentru antrenare (deoarece nu suntem complet siguri cum funcționează intern), ci utilizează BPE în schimb.

Pentru a tokeniza un text nou, îl pre-tokenizăm, îl împărțim, apoi aplicăm algoritmul de tokenizare pe fiecare cuvânt. Adică, căutăm cel mai mare subcuvânt începând de la începutul primului cuvânt și îl împărțim, apoi repetăm procesul pentru a doua parte și așa mai departe pentru restul acelui cuvânt și pentru următoarele cuvinte din text:

    def encode_word(word):
        tokens = []
        while len(word) > 0:
            i = len(word)
            while i > 0 and word[:i] not in vocab:
                i -= 1
            if i == 0:
                return ["[UNK]"]
            tokens.append(word[:i])
            word = word[i:]
            if len(word) > 0:
                word = f"##{word}"
        return tokens

Haideți să-l testăm pe un cuvânt care se află în vocabular și pe altul care nu se află:

    print(encode_word("Hugging"))
    print(encode_word("HOgging"))

    ['Hugg', '##i', '##n', '##g']
    ['[UNK]']

Acum, scriem o funcție care tokenizează un text:

    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]
        encoded_words = [encode_word(word) for word in pre_tokenized_text]
        return sum(encoded_words, [])

Îl putem încerca pe orice text:

    tokenize("This is the Hugging Face course!")

    ['Th', '##i', '##s', 'is', 'th', '##e', 'Hugg', '##i', '##n', '##g', 'Fac', '##e', 'c', '##o', '##u', '##r', '##s',
     '##e', '[UNK]']

Asta e tot pentru algoritmul WordPiece! Acum să aruncăm o privire la Unigram.

< > Update on GitHub