Sari la conținut
8/30Capitolul 8 din 30

Predicția următorului token: embeddings și ce înseamnă perplexitatea

Antrenează un model pe caractere cu 32.033 de nume și vezi de ce gradient descent redescoperă numărările.

Pe această pagină

Iată zece nume produse de un program care nu a văzut niciodată un cuvânt:

TEXT
cexze   momakurailezitynn   konimittain   llayn   ka
da      moliellavo          emia          sade    ftlsp

Niciunul nu este un nume. Aproape toate încearcă să fie. Sunt pronunțabile, se termină unde se termină numele, iar unul dintre ele — emia — este la o singură literă distanță de unul real. Programul care le-a produs ține 729 de numere, nu are noțiunea de cuvânt, silabă sau persoană și a fost ajustat printr-o singură trecere de numărare a perechilor adiacente de litere.

Până la finalul acestui capitol, o rețea neuronală va reduce scorul acelui program cu o treime, pe aceeași măsurătoare. Partea pentru care merită să rămâi este ce face rețeaua prima dată: reproduce tabelul de numărări cu trei zecimale pe fiecare rând bine populat, fără să i se ceară, pentru că cele două obiecte sunt răspunsuri la aceeași întrebare. Tot ce urmează după aceea este ceea ce numărarea nu ar fi putut face niciodată.

Obiectivul este o identitate, nu o alegere de design

Link către secțiunea: Obiectivul este o identitate, nu o alegere de design

Capitolul 7 te-a lăsat cu o secvență de întregi și fără niciun motiv ca unul să urmeze după altul. Iată motivul, într-o singură linie din Capitolul 2.

Un model de limbaj este o funcție care ia tokens de până acum și întoarce o distribuție peste ce token vine următorul: un număr pentru fiecare intrare din vocabular, nenegativ, cu suma unu. Nimic altceva. Ca să ajungi de aici la o probabilitate pentru un document întreg, aplică regula lanțului din probabilitate:

P(x1,x2,,xT)=t=1TP(xtx1,,xt1)P(x_1, x_2, \ldots, x_T) = \prod_{t=1}^{T} P(x_t \mid x_1, \ldots, x_{t-1})

Aceasta este o identitate, adevărată pentru orice secvență de orice, fără presupuneri atașate. Așadar, un model care face sarcina mică — next token dat fiind ce a fost înainte — a făcut deja sarcina mare de a atribui o probabilitate fiecărui document posibil, exact și gratuit. Încadrarea populară a acestui lucru ca truc ieftin („doar prezice următorul cuvânt”) are logica pe dos: prezicerea următorului token este modelarea distribuției comune. Nu a existat niciodată un al doilea lucru de făcut.

Loss-ul urmează la fel de mecanic. La fiecare poziție, modelul produce o distribuție qq, iar adevărul este un singur token cunoscut, deci entropia încrucișată din Capitolul 4 se aplică neschimbată:

L=1Tt=1Tlogqθ(xtx<t)L = -\frac{1}{T}\sum_{t=1}^{T} \log q_\theta(x_t \mid x_{<t})

Aceasta este log-verosimilitudinea negativă medie — rețeta din Capitolul 2 cu o distribuție categorială în locul unde stătea gaussiana. Și, cum distribuția adevărată este one-hot, entropia ei este zero, deci prin identitatea din Capitolul 4 entropia încrucișată este egală cu divergența KL: a împinge acest număr în jos și a trage convingerile modelului spre cele ale datelor sunt același act.

O consecință merită propria propoziție, pentru că este faptul economic de sub întregul domeniu. Etichetele sunt datele, deplasate cu o poziție. Nimeni nu adnotează nimic. Un trilion de tokens de text înseamnă un trilion de exemple preetichetate, motiv pentru care corpusul de antrenare al unui model modern este „internetul”, nu „un dataset construit de cineva”.

Înainte de orice rețea, baseline-ul: 32.033 de nume, unul pe linie, și sarcina de a produce mai multe, câte o literă pe rând.1

Vocabularul are 26 de litere plus un simbol de delimitare . care marchează atât începutul, cât și sfârșitul unui nume, deci modelul trebuie să învețe unde încep numele și unde se opresc. Asta înseamnă 27 de simboluri, iar cel mai mic model posibil este un tabel cu cât de des a urmat fiecare simbol după fiecare alt simbol.

bigram.pyPYTHON
N = torch.zeros((27, 27), dtype=torch.int32)
for w in words:
    cs = ["."] + list(w) + ["."]
    for a, b in zip(cs, cs[1:]):
        N[stoi[a], stoi[b]] += 1

P = N.float()
P = P / P.sum(1, keepdim=True)            # one distribution per row   

Două linii de aritmetică și modelul este ajustat — și nu este o euristică: împărțirea numărărilor la totalurile pe rând este estimarea de maximă verosimilitudine pentru o distribuție categorială, adică rețeta din Capitolul 2 cu calculul deja făcut.

TEXT
names: 32033        train/val/test: 25626 / 3203 / 3204
training bigrams: 182583

the six most likely letters after 'a':
    a -> '.'  0.1944   a -> 'n'  0.1600   a -> 'r'  0.0967
    a -> 'l'  0.0749   a -> 'h'  0.0690   a -> 'y'  0.0606

Eșantionează din el — alege o literă din rândul literei curente, treci la acel rând, repetă până apare simbolul de delimitare — și obții numele de la începutul acestui capitol. Eșuează într-un mod specific și informativ: plauzibile local, absurdități global. Fiecare pereche adiacentă de litere din momakurailezitynn este o pereche care apare în nume reale; doar că sunt șaptesprezece la rând. Modelul are memorie de o literă, deci nu poate ști că a continuat prea mult.

Loss-ul pe nume ținute deoparte este 2,4546 nats. Numărul acesta nu înseamnă nimic de unul singur, motiv pentru care există perplexitatea:

PPL=exp ⁣(1Ttlogq(xtx<t))=eL\mathrm{PPL} = \exp\!\left(-\frac{1}{T}\sum_t \log q(x_t \mid x_{<t})\right) = e^{L}

Scris explicit, fără vreo bibliotecă să facă treaba:

perplexity.pyPYTHON
@torch.no_grad()
def perplexity(logits, Y):
    logp = F.log_softmax(logits, dim=1)          # log q for every symbol
    chosen = logp[torch.arange(len(Y)), Y]       # log q of the one that came next   
    return torch.exp(-chosen.mean())             

Exponentierea inversează logaritmul și întoarce numărul la unitățile de numărare a lucrurilor. Modul curat de a vedea ce numără este să măsori un model care nu știe absolut nimic — unul care atribuie probabilitatea 1/271/27 fiecărui simbol, indiferent de context:

TEXT
uniform over 27 symbols            loss 3.2958 nats   ppl  27.000
bigram counts, add-one smoothed    loss 2.4546 nats   ppl  11.642

Exact 27,000, pentru că elog27=27e^{\log 27} = 27. Perplexitatea este numărul efectiv de opțiuni la fel de probabile între care alege modelul. O perplexitate de 27 înseamnă „n-am idee, poate fi orice”. 11,642 pentru modelul de numărare înseamnă că o literă de context îl lasă la fel de nesigur ca pe cineva care alege orbește dintre aproximativ douăsprezece opțiuni în loc de douăzeci și șapte — de aceea se citează perplexitatea, nu loss-ul brut.

Două lucruri merg prost cu ea, iar al doilea merge prost și în articole publicate.

Probabilitățile zero sunt fatale. Din cele 729 de celule ale tabelului, 113 nu apar niciodată în antrenare — 15,5 % din el este gol. E în regulă până când setul ținut deoparte aterizează într-una, iar șapte bigrame din validare o fac, printre ele dq, zj și qo de două ori. Probabilitate zero înseamnă log -\infty, adică loss infinit și perplexitate infinită: un singur nume din trei mii distruge metrica. Peticul obișnuit este să adaugi 1 la fiecare numărare înainte de normalizare, ceea ce costă aproape nimic aici (2,4546 în loc de 2,4524). Dar peticul este o mărturisire. Un model de numărare nu poate generaliza deloc. Nu are cum să bănuiască faptul că qo este plauzibil pentru că qu este comun și o se comportă ca u în alte locuri, fiindcă nu are noțiunea că două simboluri pot semăna între ele. Fiecare celulă este învățată singură, iar repararea acestui lucru este scopul restului capitolului.

Perplexitatea este un preț per token, iar token este un parametru liber. Aceasta este greșeala care apare constant când sunt comparate modele, și devine ușor de văzut odată ce te uiți. Ia același corpus de proză engleză din Capitolul 7, același model bigram interpolat, și schimbă doar cum este împărțit textul:

unitatevocabulartoken în testentropie încrucișatăperplexitatebiți pe caracter
caractere7614.4692,521712,453,6378
BPE, 512 îmbinări3296.8713,854747,212,6407
BPE, 2.048 îmbinări1.8204.2335,7468313,202,4254
cuvinte2.9916.2843,562735,262,2322

Perplexitatea variază cu un factor de 25 între aceste rânduri. Nimic din model nu s-a schimbat; doar dimensiunea lucrului prezis. Prezicerea unui cuvânt întreg este mai grea decât prezicerea unei litere, deci costă mai mult per predicție — și sunt mai puține predicții de făcut.

Acum citește ultima coloană, care împarte costul total la numărul de caractere în schimb și îl convertește în biți. Reordonează tabelul. După perplexitate, clasamentul este caractere, cuvinte, BPE-512, BPE-2048; după biți pe caracter, este cuvinte, BPE-2048, BPE-512, caractere. Modelul pe caractere trece de pe primul loc pe ultimul. Modelul cu 2.048 de îmbinări, care după perplexitate pare de 6,6 ori mai slab decât cel cu 512, este de fapt mai bun dintre cele două, cu 2,4254 biți față de 2,6407.

Așadar, o perplexitate este comparabilă doar între două modele care împart același tokenizer, iar modelele cu tokenizers diferite pot fi comparate doar în biți pe caracter — cantitatea pe care Shannon a măsurat-o în 1951 punând subiecți umani să ghicească următoarea literă din text englezesc și pe care a încadrat-o la aproximativ un bit pe caracter.2 Cel mai bun bigram al nostru stă la 2,23 biți, ceea ce rezumă corect cât de departe mai are de mers acest capitol.

Acum construiește același model ca rețea. Va consuma ordine de mărime mai multă aritmetică pentru a ajunge în același loc, iar a ajunge în același loc este ideea.

Înlocuiește tabelul cu o matrice de ponderi WW cu forma 27×2727 \times 27. Transformă litera curentă într-un vector one-hot, înmulțește, iar rezultatul numește-l logits — scorurile nenormalizate din Capitolul 4. Apoi softmax, apoi entropie încrucișată, apoi gradient descent.

neural_bigram.pyPYTHON
W = torch.randn((27, 27), requires_grad=True)

for step in range(3000):
    logits = W[xs]                            
    loss = F.cross_entropy(logits, ys)
    W.grad = None
    loss.backward()
    W.data -= 50.0 * W.grad

Linia evidențiată conține o definiție care merită păstrată. Înmulțirea unui vector one-hot cu o matrice selectează un rând din ea, deci înmulțirea este un lookup — iar fiecare implementare sare peste aritmetică și face lookup direct, exact ceea ce este W[xs].

Acesta este un embedding table. O matrice cu un rând pentru fiecare intrare din vocabular, indexată după token id. Fără geometrie, fără semantică, fără algoritm separat: un tabel de lookup ale cărui conținuturi se întâmplă să fie învățate prin gradient descent împreună cu orice altceva. Orice afirmație mistică despre „embedding space” se oprește aici.

Antrenează-l și urmărește unde ajunge:

TEXT
  step     1   train 3.7550   val 3.3882   max gap to the count table 0.757269
  step   100   train 2.4732   val 2.4726   max gap to the count table 0.388354
  step  1000   train 2.4557   val 2.4549   max gap to the count table 0.041862
  step  3000   train 2.4547   val 2.4544   max gap to the count table 0.004048

Ultima coloană este cea mai mare diferență absolută dintre orice celulă din softmax(W) și celula corespunzătoare din tabelul de numărări, iar ea merge spre zero. După 3.000 de pași, cea mai mare neconcordanță oriunde în cele 729 de celule este 0,004048, iar media este 0,000224. Cea mai proastă celulă este qi, văzută de douăsprezece ori în întregul set de antrenare; dintre cele 22 de rânduri cu peste o mie de apariții, cea mai proastă neconcordanță este 0,000562.

TEXT
                 count table   network
    a -> '.'        0.1945     0.1945
    a -> 'n'        0.1601     0.1601
    a -> 'r'        0.0967     0.0967

Gradient descent, pornind de la numere aleatorii și primind doar instrucțiunea „fă log-probabilitatea următoarei litere mare”, a redescoperit tabelul de numărări. Și trebuia s-o facă: numărările sunt estimarea de maximă verosimilitudine, entropia încrucișată este log-verosimilitudinea negativă, deci ambele proceduri optimizează același obiectiv, iar acel obiectiv are un singur optim. Rețeaua nu a învățat ceva asemănător numărării. A convergent la numărare, încet.

Ceea ce ridică întrebarea corectă: de ce s-ar mai obosi cineva? Pentru că tabelul de numărări nu mai are unde să meargă de aici, iar rețeaua are.

Extinde modelul ca să se uite la mai mult de un caracter precedent. Aceasta este arhitectura lui Bengio din 2003, strămoșul direct al fiecărui model din restul acestui curs:4 ia ultimele trei caractere, mapează fiecare printr-un embedding table într-un rând de 10 dimensiuni, concatenează rândurile în 30 de numere, trece-le prin stratul ascuns din Capitolul 5 și termină cu un output layer care produce un logit pentru fiecare intrare din vocabular.

mlp.pyPYTHON
C  = torch.randn((27, 10))          # the embedding table
W1 = torch.randn((3 * 10, 200))     # the hidden layer from Chapter 5
W2 = torch.randn((200, 27))         # one output per vocabulary entry

emb = C[X].view(-1, 30)             # three lookups, concatenated   
h = torch.tanh(emb @ W1 + b1)
logits = h @ W2 + b2                
loss = F.cross_entropy(logits, Y)

Observă ce este nou și ce nu. Stratul ascuns este cel din Capitolul 5, neschimbat; loss-ul este cel din Capitolul 4, neschimbat. Noutățile sunt embedding table din față și un output layer la fel de lat ca vocabularul din Capitolul 7 — iar al doilea este partea scumpă a fiecărui model de limbaj construit vreodată, pentru că un vocabular real are 100.000 de intrări și această înmulțire de matrici rulează la fiecare poziție.

Același cod, antrenat identic, schimbând doar dimensiunea context window:

contextparametriloss de validareperplexitate de validare
numărare, 1 caracter7292,454611,642
neuronal, 1 caracter7.8972,457711,678
neuronal, 3 caractere11.8972,11458,285
neuronal, 8 caractere21.8972,05067,773

Al doilea rând este cel interesant. O rețea cu un strat ascuns de 200 de unități și de unsprezece ori mai mulți parametri decât tabelul de numărări performează exact la fel de bine ca tabelul de numărări și deloc mai bine. Capacitatea nu a fost niciodată limita. Un caracter de context permite un anumit loss și nimic adăugat deasupra nu poate coborî sub el, pentru că informația nu este acolo.

Dă-i trei caractere și perplexitatea scade de la 11,68 la 8,29 — o reducere de 29 %, cumpărată cu 4.000 de parametri în plus. Bate numărarea aici exact din motivul diagnosticat mai devreme: un model de numărare peste contexte de trei caractere are nevoie de 273=19,68327^3 = 19{,}683 rânduri, majoritatea goale sau cu o singură observație, și îl învață pe fiecare singur. Rețeaua partajează. Dacă a, e și i ajung să aibă rânduri de embedding similare, ce învață după bra se transferă la bre fără să fi văzut vreodată bre. Acest transfer este întreaga valoare a embedding table, și este diferența dintre rândurile doi și trei.

Eșantioanele se îmbunătățesc corespunzător:

TEXT
deliah   nellara   joce     kael      quintis
salayson  reety    khyrmin  mahnen    madiaryxia

Tot nu este o listă de nume reale. Dar deliah, nellara și kael nu ar părea nelalocul lor pe una, iar monștrii interminabili au dispărut: cel mai lung dintre douăzeci de eșantioane din modelul de numărare are nouăsprezece litere, cel mai lung dintre douăzeci din acesta are treisprezece.

Tabelul este 27×1027 \times 10: un rând de zece numere pentru fiecare caracter, toate inițializate aleatoriu și mutate doar de gradientul loss-ului pe următorul caracter. Nimeni nu a pus nimic acolo. Deci ce a ajuns în el?

Instrumentul pentru întrebare este similaritatea cosinus, care este produsul scalar din Capitolul 1 cu lungimile împărțite afară:

cos(a,b)=abab\cos(\mathbf{a}, \mathbf{b}) = \frac{\mathbf{a} \cdot \mathbf{b}}{\lVert \mathbf{a} \rVert \, \lVert \mathbf{b} \rVert}

Măsoară unghiul dintre doi vectori și le ignoră lungimile, ceea ce vrei atunci când lungimea unui rând reflectă cât de des a apărut token său, nu ce înseamnă. Normalizează mai întâi fiecare vector la lungime 1 — așa cum fac sistemele reale, o singură dată, la indexare — și similaritatea cosinus este pur și simplu produsul scalar.

Iată cei mai apropiați vecini ai câtorva caractere din tabelul antrenat:

TEXT
  'c' -> 'k':+0.598      'j' -> 'z':+0.650      'i' -> 'y':+0.541
  'u' -> 'e':+0.482      'a' -> 'h':+0.367      '.' -> 'q':+0.077

O parte din asta este ce promite folclorul. c și k sunt interschimbabile în nume, la fel și i și y; j și z sunt ambele consoane rare, mai ales inițiale, care se comportă asemănător. Simbolul de delimitare . nu este aproape de nimic — 0,077 față de cea mai apropiată literă — pentru că este singurul simbol care marchează o poziție, nu un sunet.

Iar o parte nu este. Cel mai apropiat vecin al lui a este h, nu o altă vocală. Mediat peste toate perechile:

TEXT
mean cosine, vowel to vowel         : +0.1889
mean cosine, consonant to consonant : +0.0765
mean cosine, vowel to consonant     : -0.0042

Vocalele seamănă mai mult între ele decât cu consoanele, iar efectul este real, dar mic. Testat față de 2.000 de grupuri alese aleatoriu, fiecare cu cinci litere, 58 dintre acele grupuri se separă cel puțin la fel de curat — o diferență semnificativă cam la p=0.03p = 0.03. Real, deci, dar deloc insula geometrică clară pe care o sugerează relatările populare despre embeddings.

Aceasta este descrierea onestă a unui embedding table și merită păstrată pentru restul cursului. Nu este o hartă a sensului. Este o schimbare de coordonate, învățată în loc să fie proiectată, al cărei singur rol este să ușureze munca stratului următor — aceeași propoziție pe care Capitolul 5 a folosit-o pentru stratul ascuns care a pliat planul ca să rezolve XOR. Orice structură găsești în el este acolo pentru că a redus loss-ul, iar structura care nu reduce loss-ul pur și simplu nu este acolo.

word2vec, GloVe și aritmetica pe care o citează toată lumea

Link către secțiunea: word2vec, GloVe și aritmetica pe care o citează toată lumea

Dacă partea utilă este tabelul, poți merge direct după el. Asta este word2vec: păstrezi embedding lookup, arunci modelul de limbaj.5

Obiectivul skip-gram with negative sampling este o singură linie. Pentru o pereche reală (centru, context) extrasă din corpus, împinge produsul lor scalar în sus; pentru kk perechi false extrase dintr-o distribuție de zgomot, împinge-l în jos:6

logσ(vcvo)+i=1klogσ(vcvni)\log \sigma(\mathbf{v}_c \cdot \mathbf{v}_o) + \sum_{i=1}^{k} \log \sigma(-\mathbf{v}_c \cdot \mathbf{v}_{n_i})

Aceasta este o clasificare binară — „au apărut aceste două cuvinte chiar împreună?” — și este ieftină tocmai pentru că nu atinge niciodată vocabularul complet, ceea ce a făcut practic antrenamentul pe miliarde de cuvinte în 2013. GloVe ajunge la vectori similari din direcția cealaltă, factorizând matricea numărărilor globale de coapariție în loc să treacă prin exemple în flux.7 Ambele sunt ajustate exact la statistica din care a fost construit tabelul de numărări. Sunt numărare, comprimată.

Antrenați pe text8 — 17.005.207 cuvinte din Wikipedia în engleză, 71.290 dintre ele apărând de cel puțin cinci ori, 100 de dimensiuni, trei treceri — vectorii ies cu proprietatea care i-a făcut celebri:

TEXT
king     -> charles 0.700, son 0.693, queen 0.686, henry 0.669, throne 0.667
physics  -> chemistry 0.672, electromagnetism 0.661, quantum 0.654, theoretical 0.624
guitar   -> bass 0.733, vocals 0.732, acoustic 0.728, guitars 0.703, drums 0.685
three    -> seven 0.892, two 0.877, one 0.875, five 0.871, four 0.870

Nimeni nu a furnizat o categorie pentru instrumente sau pentru numerale. Acum partea faimoasă: ia king, scade man, adaugă woman și găsește cel mai apropiat vector de rezultat.

TEXT
king - man + woman
   nothing excluded : king 0.693, elizabeth 0.657, wife 0.629, woman 0.607
   a, b, c excluded : elizabeth 0.657, wife 0.629, mary 0.607   (queen is 4th, 0.604)

Cel mai apropiat vector de king - man + woman este king. Nu este o ciudățenie a unui singur exemplu. Setul de evaluare al lui Mikolov pune întrebări de forma a : b :: c : ? — 8.869 semantice (paris : france :: rome : italy) și 10.675 sintactice (walking : walked :: swimming : swam) — iar peste cele 4.103 întrebări semantice la care acest vocabular poate răspunde, câștigătorul este unul dintre cele trei cuvinte de intrare în 99,8 % din cazuri. Demonstrațiile publicate nu menționează asta, pentru că regula standard de scor șterge a, b și c înainte de a căuta. Este o regulă legitimă și face mai multă treabă decât aritmetica:

cum este ales răspunsulsemanticsintactic
offset, cu intrările excluse (standard)17,0 %11,9 %
offset, fără nimic exclus0,1 %0,4 %
cel mai apropiat vecin al lui c singur, intrări excluse13,1 %9,3 %
cel mai apropiat vecin al lui b singur, intrări excluse2,3 %0,4 %

Al treilea rând este cel lângă care merită să stai. Aruncă a și b, nu face deloc aritmetică, întoarce orice este cel mai aproape de c — și păstrezi 77 % din scorul semantic. Mare parte din ceea ce arată ca raționament analogic este proximitate plus o regulă care interzice răspunsurile evidente, exact ce a măsurat Linzen pe vectori antrenați corect și ce replică baseline-urile de mai sus.8 Acești vectori anume sunt mici — 17 milioane de cuvinte față de miliardele din spatele modelelor publicate — deci citește procentele ca pe o formă, nu ca pe state of the art. Forma este ce supraviețuiește la orice scară: aritmetica este reală și mult mai slabă decât demonstrația pe care o citează toată lumea.

Static și contextual: un vector pe cuvânt sau unul pe apariție

Link către secțiunea: Static și contextual: un vector pe cuvânt sau unul pe apariție

Tot ce a fost până acum are o limită dură construită în structura de date. Un tabel are un rând per token. Cuvântul bank primește un vector, același într-o propoziție despre un râu și într-una despre un credit ipotecar — în mod necesar, pentru că un lookup după id nu poate depinde de altceva.

Soluția este să nu mai citești vectorul din tabel și să începi să-l calculezi din propoziție. Acesta este un contextual embedding, introdus de ELMo în 2018 și standardizat de BERT în același an.910 Măsurate pe modelul real, numerele sunt mai clare decât explicația:

TEXT
sentence A: "He sat on the bank of the river and watched the water go by."
sentence B: "She deposited the cheque at the bank on the corner of the street."

static vector for 'bank' (a row of the input embedding table)
    cosine A vs B ........................ 1.000000

contextual vector for 'bank', layer by layer
    layer  |  A vs B  |  A vs another river sentence  |  B vs another money sentence
        0  |  0.9512  |            0.9512             |            0.9359
        4  |  0.5647  |            0.8987             |            0.7716
        9  |  0.4284  |            0.8699             |            0.7568
       12  |  0.5278  |            0.8702             |            0.7335

Primul rând este exact, nu aproximativ: vectorul static pentru bank este aceleași 768 de numere în ambele propoziții, deci cosinusul este 1 prin construcție. Nouă straturi mai târziu, cele două apariții stau la 0,43, în timp ce bank în două propoziții diferite despre râu rămâne la 0,87. Nimeni nu a etichetat vreun sens nicăieri în acest proces; sensurile s-au separat pentru că separarea lor face obiectivul de antrenare — ghicirea unui token ascuns din vecinii lui — mai ușor de satisfăcut.

Două detalii merită atenție. Layer 0 este deja 0,9512, nu 1,0, pentru că position embeddings au fost adăugate, iar cuvântul stă în alt loc în fiecare propoziție. Iar similaritatea crește din nou la straturile 11 și 12: straturile finale ale unui model preantrenat sunt specializate pe obiectivul lui de antrenare și adesea nu sunt cel mai bun loc din care să iei o reprezentare.

Afișează detaliile

Opțional: weight tying.

În bert-base-uncased, embedding table este 30,522×76830{,}522 \times 768 — 23.440.896 de numere, 21,4 % din cei 109.482.240 de parametri ai modelului. Într-un model de limbaj mic, fracția este și mai mare, motiv pentru care un truc este aproape universal: tabelul de intrare și output layer care produce logits sunt aceeași matrice, folosită o dată prin lookup de rând și o dată transpusă.11 Output layer atribuie deja fiecărei intrări din vocabular un vector — face un produs scalar cu fiecare — iar tying spune că vectorul folosit pentru a citi un token și vectorul folosit pentru a-l scrie ar trebui să fie același obiect. Taie parametri și îmbunătățește perplexitatea simultan, lucru suficient de rar cât să merite observat.

Ca să cauți într-un corpus după sens ai nevoie de un vector per propoziție. Odată ce îi ai, căutarea este trivială — acesta este întregul retrieval semantic, iar Capitolul 19 este despre tot ce se află în jurul lui:

search.pyPYTHON
E = normalise(embed(sentences))       # (200, d), every row of length 1
q = normalise(embed([query]))         # (1, d)
scores = q @ E.T                      # one matrix multiply   
top5 = scores[0].argsort()[::-1][:5]

Deci singura întrebare reală este de unde vine embed. Mișcarea evidentă este să iei un model de limbaj preantrenat, să treci fiecare propoziție prin el și să mediezi vectorii de token. Iată metoda aceea comparată cu patru alternative, evaluate în două feluri: corelația de rang dintre cosinus și judecățile umane de similaritate pe cele 1.379 de perechi ale benchmark-ului STS, și retrieval top-1 pe un index construit din cele 200 de perechi cele mai puternic parafrazate — o parte a fiecărei perechi indexată, cealaltă folosită ca interogare.

cum este embedded propozițiacorelație de rangtop-1 pe un index de 200 de propoziții
suprapunere binară de cuvinte (fără model deloc)0,550089,0 %
media vectorilor statici antrenați mai sus0,526385,5 %
BERT, token [CLS]0,203067,0 %
BERT, media vectorilor de token0,472984,0 %
MiniLM, antrenat contrastiv0,820392,0 %

Citește cele trei rânduri din mijloc în raport cu primele două. Un transformer preantrenat cu 109 milioane de parametri, folosit în modul evident, este mai slab la judecarea similarității propozițiilor decât numărarea cuvintelor pe care două propoziții le împart — și mai slab decât media vectorilor text8 de 100 de dimensiuni antrenați acum câteva clipe. Token [CLS], pe care tutorialele încă îl recomandă fiindcă BERT a fost preantrenat cu un obiectiv la nivel de propoziție atașat de el, este mai slab decât jumătate din atât.

Acesta nu este un defect al lui BERT. Este obiectivul. Un model de limbaj este antrenat astfel încât stările lui ascunse să prezică un token; nimic de acolo nu cere ca două parafraze să ajungă aproape una de alta și nimic nu recompensează o geometrie în care cosinus înseamnă „același sens”. Ultimul rând este un model de o cincime din dimensiune (22.713.216 parametri), antrenat pe un loss complet diferit: învățare contrastivă, în care exemplele sunt perechi — o întrebare și răspunsul ei, o propoziție și parafraza ei — iar obiectivul trage perechile adevărate împreună și împinge negativele eșantionate departe. Aceasta este contribuția Sentence-BERT și originea întregii industrii de embedding-models.12 Dense Passage Retrieval aplică aceeași rețetă direct căutării, cu un encoder pentru interogări și unul pentru pasaje.13

Deci regula practică:

Un embedding model nu este un model de limbaj cu ultimul strat eliminat. Este un model diferit, pe un obiectiv diferit, de obicei mult mai mic, al cărui cosinus înseamnă ce vrei tu să însemne pentru că a fost antrenat pe perechi unde acesta era țelul. Tabelul de mai sus este costul înlocuirii unuia cu celălalt.

Iar familia eșuează la ordinea cuvintelor. „The dog bit the man” și „the man bit the dog” au pungi de cuvinte identice, deci suprapunerea de cuvinte și media vectorilor statici le dau cosinus exact 1,000000, iar BERT mediat, care vede totuși poziția, ajunge aproape tot acolo — și MiniLM antrenat contrastiv încă le pune la 0,979. Dacă sarcina ta de retrieval depinde de cine a făcut ce cui, niciun prag de cosinus nu te va salva.

Capitolul 19 construiește un sistem de retrieval de producție pe această bază și ajunge la un prag cosinus concret. Ultima măsurătoare din acest capitol este ce face un asemenea număr defensabil, nu magic.

Blestemul dimensionalității, într-un singur tabel

Link către secțiunea: Blestemul dimensionalității, într-un singur tabel

Embeddings reale au sute sau mii de componente, iar distanțele se comportă ciudat acolo sus. Ia 1.000 de puncte aleatorii în cubul unitate din dd dimensiuni și uită-te la raportul dintre cea mai mare și cea mai mică distanță dintre oricare două dintre ele:

dimensiunicea mai apropiată perechecea mai depărtată perecheraport
20,00071,36121921,66
100,23612,33979,91
1003,00475,17521,72
1.00011,780914,03061,19
10.00039,615242,01251,06

În zece mii de dimensiuni, cea mai depărtată pereche de puncte este cu doar 6 % mai departe decât cea mai apropiată pereche. Totul este aproximativ echidistant de orice altceva, „cel mai apropiat vecin” nu mai poartă multă informație, iar acesta este blestemul dimensionalității — precum și unul dintre motivele pentru care bazele mari de date vectoriale nu fac căutare exactă a celui mai apropiat vecin. Cealaltă față a aceleiași monede este ce face pragurile de cosinus utilizabile: măsurată peste o mie de perechi de vectori unitari aleatorii, media cosinusului stă la 0.0052-0.0052 în 100 de dimensiuni și +0.0003+0.0003 în 768, cu deviații standard de 0,0968 și 0,0357 — iar în 768 de dimensiuni doar 0,2 % dintre perechile aleatorii depășesc 0,1 în valoare absolută. O similaritate măsurată de 0,4 nu este deci „40 % asemănare”; este mult în afara a tot ce produce întâmplarea, motiv pentru care pragurile între 0,3 și 0,7 separă semnalul de zgomot în loc să stea în mijlocul lui.

Modelul din acest capitol citește un număr fix de caractere precedente, îl caută pe fiecare și lipește rezultatele împreună în ordine. Designul acesta are două probleme, și sunt aceeași problemă.

Uită-te din nou la tabelul cu context: trecerea de la trei caractere la opt aproape a dublat parametrii și a cumpărat 0,06 nats. Costul crește liniar cu context — fiecare poziție în plus are nevoie de propria placă din prima matrice de ponderi — iar beneficiul nu. Împinge-l la o mie de tokens și doar primul strat cântărește mai mult decât restul modelului, cea mai mare parte cheltuită pe poziții care nu contează pentru o predicție dată.

Aceasta este și a doua problemă: modelul nu are nicio modalitate să decidă care dintre tokens anteriori contează. Poziția doi primește propriile ponderi și poziția șapte pe ale ei, permanent, indiferent ce se află în ele. Când modelul silabisește nell, caracterul decisiv este cel imediat anterior. Când o propoziție conține un pronume, cuvântul care îi fixează referentul poate fi la patruzeci de tokens în urmă — și niciun slot fix nu poate fi alocat pentru „patruzeci în urmă”, pentru că data viitoare va fi șase.

Ce vrem este un model care calculează, pentru fiecare predicție, cât de mult ar trebui să conteze fiecare token anterior — ponderi peste context produse de conținut, nu fixate de aranjament. Scrie asta cu grijă și începe ca ceva complet banal: o medie peste tokens anteriori. Apoi lasă ponderile acelei medii să fie învățate și lasă-le să depindă de token care pune întrebarea.

Aceasta este attention, și este Capitolul 9.


Merită citit în paralel și capitolul 3 din Speech and Language Processing de Jurafsky și Martin, care tratează modelele n-gram, smoothing-ul și perplexitatea mult mai atent decât avem loc aici, inclusiv de ce interpolarea și back-off bat adăugarea lui unu; notițele Stanford CS229 §17.1–17.2 pentru modelarea limbajului din perspectiva probabilistică; și articolul lui Linzen de mai sus, care este scurt și merită citit integral.

  1. Exemplul de generare de nume, dataset-ul și progresia de la un tabel de numărări la o rețea în stil Bengio urmează seria building makemore a lui Andrej Karpathy, ale cărei prime două părți sunt cel mai bun companion pentru acest capitol.

  2. Shannon, C. E. Prediction and Entropy of Printed English. Bell System Technical Journal 30(1), pp. 50–64 (1951). Subiecți umani ghicind următoarea literă din engleză și măsurătoarea originală în biți pe caracter.

  3. Shannon, C. E. A Mathematical Theory of Communication. Bell System Technical Journal 27 (1948). Teorema codării sursei și identificarea predicției cu compresia.

  4. Bengio, Y., Ducharme, R., Vincent, P. and Jauvin, C. A Neural Probabilistic Language Model. Journal of Machine Learning Research 3, pp. 1137–1155 (2003). Arhitectura folosită mai sus: un embedding per cuvânt, concatenat peste o fereastră fixă, printr-un strat ascuns, până la un softmax peste vocabular.

  5. Mikolov, T., Chen, K., Corrado, G. and Dean, J. Efficient Estimation of Word Representations in Vector Space. arXiv:1301.3781 (2013). CBOW și skip-gram, precum și setul de analogii folosit mai sus.

  6. Mikolov, T., Sutskever, I., Chen, K., Corrado, G. and Dean, J. Distributed Representations of Words and Phrases and their Compositionality. arXiv:1310.4546 (2013). Negative sampling, subeșantionarea cuvintelor frecvente și distribuția de zgomot ridicată la puterea 3/4 folosită mai sus.

  7. Pennington, J., Socher, R. and Manning, C. GloVe: Global Vectors for Word Representation. EMNLP 2014. Vectori de cuvinte dintr-o factorizare a matricei globale de coapariție, nu din ferestre locale parcurse în flux.

  8. Linzen, T. Issues in evaluating semantic spaces using word analogies. RepEval 2016, arXiv:1606.07736. Sursa baseline-urilor fără offset replicate mai sus.

  9. Peters, M. et al. Deep contextualized word representations. arXiv:1802.05365 (2018). ELMo: un vector per apariție, calculat de un model de limbaj bidirecțional.

  10. Devlin, J., Chang, M.-W., Lee, K. and Toutanova, K. BERT: Pre-training of Deep Bidirectional Transformers for Language Understanding. arXiv:1810.04805 (2018). Modelul măsurat în experimentul cu bank.

  11. Press, O. and Wolf, L. Using the Output Embedding to Improve Language Models. arXiv:1608.05859 (2016), and Inan, H., Khosravi, K. and Socher, R. Tying Word Vectors and Word Classifiers. arXiv:1611.01462 (2016). Două argumente independente pentru același truc.

  12. Reimers, N. and Gurevych, I. Sentence-BERT: Sentence Embeddings using Siamese BERT-Networks. arXiv:1908.10084 (2019). Măsurătoarea de deschidere — BERT mediat performând mai slab decât vectorii statici mediați pe similaritatea propozițiilor — este ceea ce reproduce tabelul de mai sus.

  13. Karpukhin, V. et al. Dense Passage Retrieval for Open-Domain Question Answering. arXiv:2004.04906 (2020). Antrenarea contrastivă a unui retriever cu doi encoders; strămoșul direct al stack-ului de retrieval din Capitolul 19.

Gata să lași LIA să aleagă?

Construiește cu toate modelele AI într-un singur loc — începe gratuit azi.