#
Antrenarea unui nou tokenizer dintr-unul vechi

Dacă un model de limbaj nu este disponibil în limba dorită sau dacă corpusul tău este foarte diferit de cel pe care modelul de limbaj a fost antrenat, este probabil că veți dori să antrenați modelul de la zero, folosind un tokenizer adaptat datelor tale. Acest lucru va necesita antrenarea unui nou tokenizer pe datasetul tău. Dar ce înseamnă exact asta? Când am examinat pentru prima dată tokenizatorii în Capitolul 2, am văzut că majoritatea modelelor Transformer folosesc un algoritm de subword tokenization. Pentru a identifica care subcuvinte sunt de interes și apar cel mai frecvent în corpusul respectiv, tokenizerul trebuie să examineze cu atenție toate textele din corpus - un proces pe care îl numim antrenare. Regulile exacte care conduc această antrenare depind de tipul de tokenizer utilizat și vom prezenta cei trei algoritmi principali mai târziu în acest capitol.

⚠️ Antrenarea unui tokenizer nu este același lucru ca antrenarea unui model! Antrenarea modelului folosește stochastic gradient descent pentru a face pierderea puțin mai mică pentru fiecare batch. Este randomizată prin natură (ceea ce înseamnă că trebuie să setați niște seeduri pentru a obține aceleași rezultate atunci când faceți aceeași antrenare de două ori). Antrenarea unui tokenizer este un proces statistic care încearcă să identifice care subcuvinte sunt cele mai bune pentru a fi selectate pentru un anumit corpus, și regulile exacte utilizate pentru a le selecta depind de algoritmul de tokenizare. Este determinist, ceea ce înseamnă că întotdeauna obțineți aceleași rezultate atunci când antrenați cu același algoritm pe același corpus.

##
Asamblarea unui corpus

Există o interfață API foarte simplă în 🤗 Transformers pe care o puteți utiliza pentru a antrena un nou tokenizer cu aceleași caracteristici ca unul existent: `AutoTokenizer.train_new_from_iterator()`. Pentru a vedea acest lucru în acțiune, să zicem că vrem să antrenăm GPT-2 de la zero, dar într-o altă limbă decât engleza. Prima noastră sarcină va fi să adunăm multe date în acea limbă într-un corpus de antrenare. Pentru a oferi exemple pe care toată lumea le poate înțelege, nu vom folosi o limbă ca rusă sau chineza aici, ci mai degrabă o limbă engleză specializată: codul Python.

Biblioteca 🤗 Datasets ne poate ajuta să asamblăm un corpus de cod sursă Python. Vom folosi funcția obișnuită `load_dataset()` pentru a descărca și a păstra în cache datasetul CodeSearchNet. Acest dataset a fost creat pentru Provocarea CodeSearchNet și conține milioane de funcții din biblioteci open-source de pe GitHub în mai multe limbaje de programare. Aici, vom încărca partea Python a acestui dataset:

    from datasets import load_dataset

    # Acest lucru poate dura câteva minute pentru a încărca, așa că luați o pauză și beți o ceașcă de cafea sau ceai în timp ce așteptați!
    raw_datasets = load_dataset("code_search_net", "python")

Putem să ne uităm la splitul de antrenare pentru a vedea la care coloane avem acces:

    raw_datasets["train"]

    Dataset({
        features: ['repository_name', 'func_path_in_repository', 'func_name', 'whole_func_string', 'language',
          'func_code_string', 'func_code_tokens', 'func_documentation_string', 'func_documentation_tokens', 'split_name',
          'func_code_url'
        ],
        num_rows: 412178
    })

Putem vedea că dataset-ul separă docstringurile de cod și sugerează o tokenizare a ambelor. Aici, vom folosi doar coloana `whole_func_string` pentru a antrena tokenizerul nostru. Putem să ne uităm la un exemplu al unei astfel de funcții prin indexarea în splitul de antrenare:

    print(raw_datasets["train"][123456]["whole_func_string"])

care ar trebui să printeze următorul lucru:

    def handle_simple_responses(
          self, timeout_ms=None, info_cb=DEFAULT_MESSAGE_CALLBACK):
        """Accepts normal responses from the device.

        Args:
          timeout_ms: Timeout in milliseconds to wait for each response.
          info_cb: Optional callback for text sent from the bootloader.

        Returns:
          OKAY packet's message.
        """
        return self._accept_responses('OKAY', info_cb, timeout_ms=timeout_ms)

Primul lucru pe care trebuie să-l facem este să transformăm setul de date într-un iterator de liste de texte - de exemplu, o listă de liste de texte. Utilizarea listelor de texte va permite tokenizerului nostru să funcționeze mai rapid (antrenându-se pe batch-uri de texte în loc de a procesa texte individuale unul câte unul), iar acesta ar trebui să fie un iterator dacă dorim să evităm să avem tot în memoria RAM deodată. Dacă corpusul tău este uriaș, veți dori să profitați de faptul că 🤗 Datasets nu încarcă totul în memoria RAM, ci stochează elementele datasetului pe disc.

Următoarea operație ar crea o listă de liste de 1.000 de texte fiecare, dar ar încărca totul în memorie:

    # Nu faceți uncomment următoarei linii dacă datasetul vostru este mare, ci doar dacă este mic!
    # training_corpus = [raw_datasets["train"][i: i + 1000]["whole_func_string"] for i in range(0, len(raw_datasets["train"]), 1000)]

Utilizând un generator Python, putem evita ca Python să încarce orice în memorie până când este realmente necesar. Pentru a crea un astfel de generator, trebuie doar să înlocuiți parantezele pătrate cu paranteze rotunde:

    training_corpus = (
        raw_datasets["train"][i : i + 1000]["whole_func_string"]
        for i in range(0, len(raw_datasets["train"]), 1000)
    )

Această linie de cod nu extrage niciun element al setului de date; doar creează un obiect pe care îl puteți utiliza într-un `for` loop din Python. Textele vor fi încărcate doar atunci când veți avea nevoie de ele(adică atunci când sunteți la pasul loopului `for` care le solicită), iar doar 1.000 de texte vor fi încărcate la un moment dat. Acest mod vă permite să nu epuizați memoria RAM chiar dacă prelucrați un set de date uriaș.

Problema cu un obiect generator este că poate fi utilizat doar o dată, așadar, în loc să ne ofere lista primelor 10 cifre de două ori:

    gen = (i for i in range(10))
    print(list(gen))
    print(list(gen))

noi le primim o dată și apoi o listă goală:

    [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
    []

Din acest motive noi definim o funcție ce returnează în schimb un generator:

    def get_training_corpus():
        return (
            raw_datasets["train"][i : i + 1000]["whole_func_string"]
            for i in range(0, len(raw_datasets["train"]), 1000)
        )

    training_corpus = get_training_corpus()

În același timp poți defini un generator înăuntrul unui `for` loop folosing statementul `yield`:

    def get_training_corpus():
        dataset = raw_datasets["train"]
        for start_idx in range(0, len(dataset), 1000):
            samples = dataset[start_idx : start_idx + 1000]
            yield samples["whole_func_string"]

ceea ce va produce același generator ca înainte, dar îți va permite să folosești o logică mai complexă decât cea pe care ai putea să o folosești într-un list comprehension.

##
Antrenarea unui nou tokenizer

Acum că avem corpusul nostru sub forma unui iterator de batch-uri de texte, suntem gata să antrenăm un nou tokenizer. Pentru a face acest lucru, trebuie mai întâi să încărcăm tokenizerul pe care dorim să-l asociem cu modelul nostru (aici, GPT-2):

    from transformers import AutoTokenizer

    old_tokenizer = AutoTokenizer.from_pretrained("gpt2")

Chiar dacă urmează să antrenăm un nou tokenizer, este o idee bună să facem acest lucru pentru a evita să începem să facem tot de la zero. Astfel, nu vom fi nevoiți să specificăm nimic despre algoritmul de tokenizare sau despre special tokens pe care îi vom utiliza; noul nostru tokenizer va fi exact la fel ca GPT-2, iar singur lucrul care se va schimba este vocabularul, care va fi determinat de antrenarea pe corpusul nostru.

Mai întâi, hai să vedem cum ar interpreta acest tokenizer un exemplu de funcție:

    example = '''def add_numbers(a, b):
        """Add the two numbers `a` and `b`."""
        return a + b'''

    tokens = old_tokenizer.tokenize(example)
    tokens

    ['def', 'Ġadd', '_', 'n', 'umbers', '(', 'a', ',', 'Ġb', '):', 'Ċ', 'Ġ', 'Ġ', 'Ġ', 'Ġ"""', 'Add', 'Ġthe', 'Ġtwo',
     'Ġnumbers', 'Ġ`', 'a', '`', 'Ġand', 'Ġ`', 'b', '`', '."', '""', 'Ċ', 'Ġ', 'Ġ', 'Ġ', 'Ġreturn', 'Ġa', 'Ġ+', 'Ġb']

Acest tokenizer are câteva simboluri speciale, cum ar fi `Ġ` și `Ċ`, care denotă spații și noi linii, respectiv. Așa cum se poate vedea, acest lucru nu este prea eficient: tokenizerul returnează tokenuri individuale pentru fiecare spațiu, când ar putea grupa împreună nivelurile de indentare (deoarece având seturi de patru sau opt spații va fi foarte comun în cod). De asemenea, a divizat numele funcției într-un mod ciudat, nefiind obișnuit să vadă cuvinte care conțin caracterele `_`.

Acum hai să antrenăm un nou tokenizer și să vedem dacă rezolvă aceste probleme. Pentru aceasta, vom utiliza metoda `train_new_from_iterator()`:

    tokenizer = old_tokenizer.train_new_from_iterator(training_corpus, 52000)

Această comandă ar putea dura puțin timp dacă corpusul este foarte mare, dar pentru acest dataset de 1,6 GB de texte este extrem de rapid (1 minut și 16 secunde pe un procesor AMD Ryzen 9 3900X cu 12 nuclee).

Reține că `AutoTokenizer.train_new_from_iterator()` funcționează doar dacă tokenizerul pe care îl utilizați este un tokenizer “rapid”. Așa cum veți vedea în următoarea secțiune, biblioteca 🤗 Transformers conține două tipuri de tokenizeri: unii sunt scriși în pur Python, iar alții (cei rapizi) sunt susținuți de biblioteca 🤗 Tokenizers, care este scrisă în limbajul de programare Rust. Python este limbajul cel mai frecvent utilizat pentru aplicații de data science și deep learning, dar atunci când orice trebuie să fie paralelizat pentru a fi rapid, trebuie să fie scris într-un alt limbaj de programare. De exemplu, multiplicările matricelor care sunt la baza calculelor modelului sunt scrise în CUDA, o bibliotecă C optimizată pentru GPU-uri.

Antrenarea unui tokenizer nou în pur Python ar fi extrem de lent, de aceea am dezvoltat biblioteca 🤗 Tokenizers. Reține că, la fel cum nu a trebuit să învățați limbajul CUDA pentru a putea executa modelul pe un batch de inputuri pe un GPU, nu veți avea nevoie să învățați Rust pentru a utiliza un tokenizer rapid. Biblioteca 🤗 Tokenizers oferă legături Python pentru multe metode care apelează intern unele bucăți de cod în Rust; de exemplu, pentru a paraleliza antrenarea noului tokenizer sau, așa cum am văzut în Capitolul 3, tokenizarea unui batch de inputuri.

Majoritatea modelelor Transformer au un tokenizer rapid disponibil (există unele excepții pe care le puteți verifica aici), iar API-ul `AutoTokenizer` selectează întotdeauna tokenizerul rapid pentru tine dacă este disponibil. În următoarea secțiune, vom examina unele dintre celelalte caracteristici speciale ale tokenizerilor rapizi, care vor fi foarte utile pentru sarcini precum clasificarea tokenilor și răspunderea la întrebări. Înainte de a face acest lucru, totuși, să încercăm noul nostru tokenizer pe exemplul anterior:

    tokens = tokenizer.tokenize(example)
    tokens

    ['def', 'Ġadd', '_', 'numbers', '(', 'a', ',', 'Ġb', '):', 'ĊĠĠĠ', 'Ġ"""', 'Add', 'Ġthe', 'Ġtwo', 'Ġnumbers', 'Ġ`',
     'a', '`', 'Ġand', 'Ġ`', 'b', '`."""', 'ĊĠĠĠ', 'Ġreturn', 'Ġa', 'Ġ+', 'Ġb']

Aici din nou vedem simboluri speciale ca `Ġ` sau `Ċ` care denotă spații sau linii noi, dar în același timp putem vedea că tokenizerul nostru a învățat câțiva tokens care sunt foarte specifici la corpusul de funcții Python: de exemplu, tokenul `ĊĠĠĠ` care reprezintă indentarea, sau tokenul `Ġ"""` care reprezintă cele trei ghilimele cu care se începe un docstring. Tokenizerul, de asemenea face split corect numelui funției pe `_`. Aceasta chiar este o reprezentare compactă: comparativ, utilizând limba tokenizerului englez pe același exemplu ne va da o propoziție mai lungă:

    print(len(tokens))
    print(len(old_tokenizer.tokenize(example)))

    27
    36

Hai să ne uităm la un alt exemplu:

    example = """class LinearLayer():
        def __init__(self, input_size, output_size):
            self.weight = torch.randn(input_size, output_size)
            self.bias = torch.zeros(output_size)

        def __call__(self, x):
            return x @ self.weights + self.bias
        """
    tokenizer.tokenize(example)

    ['class', 'ĠLinear', 'Layer', '():', 'ĊĠĠĠ', 'Ġdef', 'Ġ__', 'init', '__(', 'self', ',', 'Ġinput', '_', 'size', ',',
     'Ġoutput', '_', 'size', '):', 'ĊĠĠĠĠĠĠĠ', 'Ġself', '.', 'weight', 'Ġ=', 'Ġtorch', '.', 'randn', '(', 'input', '_',
     'size', ',', 'Ġoutput', '_', 'size', ')', 'ĊĠĠĠĠĠĠĠ', 'Ġself', '.', 'bias', 'Ġ=', 'Ġtorch', '.', 'zeros', '(',
     'output', '_', 'size', ')', 'ĊĊĠĠĠ', 'Ġdef', 'Ġ__', 'call', '__(', 'self', ',', 'Ġx', '):', 'ĊĠĠĠĠĠĠĠ',
     'Ġreturn', 'Ġx', 'Ġ@', 'Ġself', '.', 'weights', 'Ġ+', 'Ġself', '.', 'bias', 'ĊĠĠĠĠ']

În plus față de tokenul corespunzător unei indentări, aici putem vedea și un token pentru o indentare dublă: `ĊĠĠĠĠĠĠĠ`. Cuvintele speciale din Python, cum ar fi `class`, `init`, `call`, `self` și `return`, sunt tokenizate fiecare ca un singur token, și putem vedea că, pe lângă divizarea la `_` și `.`, tokenizerul divizează corect chiar și numele scrise în stil camel-case: `LinearLayer` este tokenizeat ca `["ĠLinear", "Layer"]`.

##
Salvarea tokenizerului

Pentru a ne asigura că îl putem utiliza mai târziu, trebuie să salvăm noul nostru tokenizer. Asemănător salvării unui model, acest lucru se realizează cu metoda `save_pretrained()`:

    tokenizer.save_pretrained("code-search-net-tokenizer")

Acest lucru va crea un nou folder numit code-search-net-tokenizer, care va conține toate fișierele necesare pentru a reîncărca tokenizerul. Dacă doriți să partajați acest tokenizer cu colegii și prietenii tăi, îl puteți încărca pe Hub prin conectarea la contul tău. Dacă lucrați într-un notebook, există un convenience function pentru a vă ajuta cu acest lucru:

    from huggingface_hub import notebook_login

    notebook_login()

Acest lucru va afișa un widget în care puteți introduce credențialele de conectare Hugging Face. Dacă nu lucrați într-un notebook, introduceți simplu următoarea linie în terminal:

    huggingface-cli login

După ce v-ați conectat, puteți face push tokenizerului prin executarea următoarei comenzi:

    tokenizer.push_to_hub("code-search-net-tokenizer")

Acest lucru va crea un nou repositoriu în namespace-ul tău cu numele `code-search-net-tokenizer`, care va conține fișierul tokenizerului. După aceea, puteți încărca tokenizerul de oriunde cu metoda `from_pretrained()`:

    # Înlocuiți "huggingface-course" mai jos cu namespace-ul tău pentru a utiliza propriul tokenizer
    tokenizer = AutoTokenizer.from_pretrained("huggingface-course/code-search-net-tokenizer")

Acum sunteți gata să antrenați un model de limbaj de la zero și să îl ajustați pentru sarcina voastră! Vom face acest lucru în Capitolul 7, dar mai întâi, în continuarea acestui capitol, vom arunca o privire mai atentă asupra tokenizerilor rapizi și vom explora în detaliu ce se întâmplă atunci când apelați metoda `train_new_from_iterator()`.

< > Update on GitHub