Pytorch
TensorFlow

#
Tokenizatoare

Tokenizerii sunt unele dintre componentele de bază ale pipeline-ului NLP. Acestea au un singur scop: să transforme textul în date care pot fi prelucrate de model. Modelele pot procesa doar numere, astfel încât tokenizerii trebuie să convertească intrările noastre de text în date numerice. În această secțiune, vom explora exact ce se întâmplă în pipeline-ul de tokenizare.

În sarcinile NLP, datele care sunt în general prelucrate sunt text brut. Iată un exemplu de astfel de text:

    Jim Henson was a puppeteer

Cu toate acestea, modelele pot procesa doar numere, deci trebuie să găsim o modalitate de a converti textul brut în numere. Asta fac tokenizerii și există o mulțime de modalități de a face acest lucru. Scopul este de a găsi cea mai relevantă reprezentare - adică cea care are cel mai mare sens pentru model - și, dacă este posibil, cea mai mică reprezentare.

Să aruncăm o privire la câteva exemple de algoritmi de tokenizare și să încercăm să răspundem la unele dintre întrebările pe care le puteți avea despre tokenizare.

##
Word-based

Primul tip de tokenizator care îmi vine în minte este word-based. În general, este foarte ușor de configurat și de utilizat, cu doar câteva reguli, și adesea produce rezultate satisfăcătoare. De exemplu, în imaginea de mai jos, obiectivul este de a împărți textul brut în cuvinte și de a găsi o reprezentare numerică pentru fiecare dintre ele:

Există diferite moduri de a împărți textul. De exemplu, am putea folosi spațiu pentru a tokeniza textul în cuvinte prin aplicarea funcției `split()` din Python:

    tokenized_text = "Jim Henson was a puppeteer".split()
    print(tokenized_text)

    ['Jim', 'Henson', 'was', 'a', 'puppeteer']

Există, de asemenea, variații ale tokenizatoarelor de cuvinte care au reguli suplimentare pentru punctuație. Cu acest tip de tokenizator, putem ajunge la “vocabulare” destul de mari, unde un vocabular este definit de numărul total de token-uri independente pe care le avem în corpus.

Fiecărui cuvânt i se atribuie un ID, începând de la 0 și mergând până la dimensiunea vocabularului. Modelul utilizează aceste ID-uri pentru a identifica fiecare cuvânt.

Dacă dorim să acoperim complet o limbă cu un tokenizator bazat pe cuvinte, va trebui să avem un identificator pentru fiecare cuvânt din limbă, ceea ce va genera o cantitate uriașă de token-uri. De exemplu, există peste 500 000 de cuvinte în limba engleză, astfel încât, pentru a construi o hartă de la fiecare cuvânt la un ID de intrare, ar trebui să ținem evidența unui număr atât de mare de ID-uri. În plus, cuvinte precum “dog” sunt reprezentate diferit de cuvinte precum “dogs”, iar modelul nu va avea inițial nicio modalitate de a ști că “dog” și “dogs” sunt similare: va identifica cele două cuvinte ca neavând legătură. Același lucru este valabil și pentru alte cuvinte similare, precum “run” și “running”, pe care modelul nu le va vedea inițial ca fiind similare.

În cele din urmă, avem nevoie de un token personalizat pentru a reprezenta cuvintele care nu se află în vocabularul nostru. Acesta este cunoscut sub numele de token “necunoscut”, adesea reprezentat ca ”[UNK]” sau ”<unk>”. În general, este un indiciu rău dacă vedeți că tokenizatorul produce o mulțime de astfel de token-uri, deoarece nu a fost capabil să recupereze reprezentarea sensibilă a unui cuvânt și veți pierde informații pe parcurs. Scopul elaborării vocabularului este de a face în așa fel încât tokenizatorul să tokenizeze cât mai puține cuvinte posibil în tokenul necunoscut.

O modalitate de a reduce numărul de token-uri necunoscute este de a merge un nivel mai adânc, folosind un tokenizator character-based.

##
Character-based

Tokenizatoarele character-based împart textul în caractere, și nu în cuvinte. Acest lucru are două beneficii principale:

- Dimensiunea vocabularului este mult mai mică.
- Există mult mai puține token-uri în afara vocabularului (necunoscute), deoarece fiecare cuvânt poate fi construit din caractere.

Dar și aici apar unele probleme legate de spații și punctuație:

Nici această abordare nu este perfectă. Deoarece reprezentarea se bazează acum pe caractere și nu pe cuvinte, s-ar putea spune că, din punct de vedere intuitiv, este mai puțin relevantă: fiecare caracter nu înseamnă mult în sine, în timp ce în cazul cuvintelor situația este diferită. Totuși, acest lucru variază din nou în funcție de limbă; în chineză, de exemplu, fiecare caracter conține mai multe informații decât un caracter într-o limbă latină.

Un alt lucru care trebuie luat în considerare este faptul că ne vom trezi cu o cantitate foarte mare de token-uri care vor fi prelucrate de modelul nostru: în timp ce un cuvânt ar fi un singur token cu un tokenizator bazat pe cuvinte, acesta se poate transforma cu ușurință în 10 sau mai multe token-uri atunci când este convertit în caractere.

Pentru a obține ce este mai bun din ambele lumi, putem utiliza o a treia tehnică care combină cele două abordări: subword tokenization.

##
Subword tokenization

Algoritmii “Subword Tokenization ” se bazează pe principiul conform căruia cuvintele utilizate frecvent nu ar trebui să fie împărțite în subcuvinte mai mici, dar cuvintele rare ar trebui să fie descompuse în subcuvinte semnificative.

De exemplu, ” annoyingly ” ar putea fi considerat un cuvânt rar și ar putea fi descompus în ” annoying ” și ” ly “. Este probabil ca ambele să apară mai frecvent ca subcuvinte de sine stătătoare, în timp ce, în același timp, sensul cuvântului ” annoyingly ” este păstrat de sensul compus al cuvintelor ” annoying ” și ” ly “.

Iată un exemplu care arată modul în care un algoritm de subword tokenization ar tokeniza secvența “Let’s do tokenization!“:

Aceste subcuvinte oferă în cele din urmă o mulțime de semnificații semantice: în exemplul de mai sus, “tokenization” a fost împărțit în “token” și “ization”, două cuvinte care au o semnificație semantică, fiind în același timp eficiente din punct de vedere al spațiului (sunt necesare doar două cuvinte pentru a reprezenta un cuvânt lung). Acest lucru ne permite să avem o acoperire relativ bună cu vocabulare de dimensiuni mici și aproape fără token-uri necunoscute.

Această abordare este utilă în special în limbile aglutinative, cum ar fi turca, în care se pot forma cuvinte complexe (aproape) arbitrar de lungi prin combinarea subcuvintelor.

###
Și nu numai!

Nu este surprinzător faptul că există mult mai multe tehnici. Iată câteva:

- BPE la nivel de byte, utilizată în GPT-2
- WordPiece, utilizată în BERT
- SentencePiece sau Unigram, utilizate în mai multe modele multilingve

Acum ar trebui să cunoașteți destul de bine cum funcționează tokenizatoarele pentru a începe să utilizați API-ul.

##
Încărcarea și salvarea

Încărcarea și salvarea tokenizatoarelor este la fel de simplă ca în cazul modelelor. De fapt, se bazează pe aceleași două metode: `from_pretrained()` și `save_pretrained()`. Aceste metode vor încărca sau salva algoritmul utilizat de tokenizator (un pic ca arhitectura modelului), precum și vocabularul său (un pic ca weight-urile modelului).

Încărcarea tokenizatorului BERT antrenat cu același checkpoint ca BERT se face în același mod ca și încărcarea modelului, cu excepția faptului că folosim clasa `BertTokenizer`:

    from transformers import BertTokenizer

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

Similar cu `AutoModel`, clasa `AutoTokenizer` va prelua clasa tokenizer corespunzătoare din bibliotecă pe baza numelui checkpoint-ului și poate fi utilizată direct cu orice checkpoint:

    from transformers import AutoTokenizer

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

Acum putem utiliza tokenizatorul așa cum am arătat în secțiunea anterioară:

    tokenizer("Using a Transformer network is simple")

    {'input_ids': [101, 7993, 170, 11303, 1200, 2443, 1110, 3014, 102],
     'token_type_ids': [0, 0, 0, 0, 0, 0, 0, 0, 0],
     'attention_mask': [1, 1, 1, 1, 1, 1, 1, 1, 1]}

Salvarea unui tokenizer este identică cu salvarea unui model:

    tokenizer.save_pretrained("directory_on_my_computer")

Vom vorbi mai mult despre `token_type_ids` în Capitolul 3 și vom explica cheia `attention_mask` puțin mai târziu. Mai întâi, să vedem cum sunt generate `input_ids`. Pentru a face acest lucru, va trebui să ne uităm la metodele intermediare ale tokenizatorului.

##
Codificarea

Traducerea textului în numere este cunoscută sub numele de codificare. Codificarea se face într-un proces în două etape: tokenizarea, urmată de conversia în ID-uri de intrare.

Traducerea textului în numere este cunoscută sub numele de codificare. Codificarea se face într-un proces în două etape: tokenizarea, urmată de conversia în ID-uri de intrare.

Al doilea pas este să convertim aceste token-uri în numere, astfel încât să putem construi un tensor din ele și să le introducem în model. Pentru a face acest lucru, tokenizatorul are un vocabular, care este partea pe care o descărcăm atunci când îl instanțiem cu metoda `from_pretrained()`. Din nou, trebuie să folosim același vocabular utilizat atunci când modelul a fost preantrenat.

Pentru a înțelege mai bine cele două etape, le vom explora separat. Rețineți că vom utiliza unele metode care efectuează părți ale pipeline-ului de tokenizare separat pentru a vă arăta rezultatele intermediare ale acestor etape, dar, în practică, ar trebui să apelați tokenizatorul direct pe intrările dvs. (așa cum se arată în secțiunea 2).

###
Tokenizarea

Procesul de tokenizare este realizat prin metoda `tokenize()`:

    from transformers import AutoTokenizer

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

    sequence = "Using a Transformer network is simple"
    tokens = tokenizer.tokenize(sequence)

    print(tokens)

Rezultatul acestei metode este o listă de șiruri de caractere, sau token-uri:

    ['Using', 'a', 'transform', '##er', 'network', 'is', 'simple']

Acest tokenizator este un tokenizator de subcuvinte: împarte cuvintele până când obține token-uri care pot fi reprezentate de vocabularul său. Acesta este cazul aici cu `transformer`, care este împărțit în două token-uri: `transform` și `##er`

###
De la token-uri la ID-uri de intrare

Conversia în ID-uri de intrare este gestionată de metoda de tokenizare `convert_tokens_to_ids()`:

    ids = tokenizer.convert_tokens_to_ids(tokens)

    print(ids)

    [7993, 170, 11303, 1200, 2443, 1110, 3014]

Aceste rezultate, odată convertite în tensorul framework-ului corespunzător, pot fi apoi utilizate ca intrări într-un model, așa cum am văzut mai devreme în acest capitol.

> ✏️ Încercați! Replicați ultimii doi pași (tokenizarea și conversia în ID-uri de intrare) pe propozițiile de intrare pe care le-am folosit în secțiunea 2 (“I’ve been waiting for a HuggingFace course my whole life.” și “I hate this so much!”). Verificați dacă obțineți aceleași ID-uri de intrare pe care le-am obținut mai devreme!

##
Decodificare

Decodificarea este inversă: din indicii vocabularului, dorim să obținem un șir de caractere. Acest lucru poate fi realizat cu metoda `decode()` după cum urmează:

    decoded_string = tokenizer.decode([7993, 170, 11303, 1200, 2443, 1110, 3014])
    print(decoded_string)

    'Using a Transformer network is simple'

Rețineți că metoda `decode` nu numai că convertește indicii înapoi în token-uri, dar și grupează token-urile care fac parte din aceleași cuvinte pentru a produce o propoziție inteligibilă. Acest comportament va fi extrem de util atunci când vom utiliza modele care prezic text nou (fie text generat de un prompt, fie pentru probleme sequence-to-sequence, precum traducerea sau rezumarea).

Până acum ar trebui să înțelegeți operațiunile atomice pe care le poate gestiona un tokenizator: tokenizarea, conversia în ID-uri și conversia ID-urilor înapoi într-un șir. Cu toate acestea, am atins doar vârful icebergului. În secțiunea următoare, vom aborda limitele metodei noastre și vom vedea cum să le depășim.

Update on GitHub