Naar inhoud springen
8/30Hoofdstuk 8 van 30

Next-token voorspelling: embeddings en wat perplexity betekent

Train een karaktermodel op 32.033 namen, zie gradient descent counts herontdekken en ontdek waarom perplexities zelden matchen.

Op deze pagina

Hier zijn tien namen geproduceerd door een programma dat nog nooit een woord heeft gezien:

TEXT
cexze   momakurailezitynn   konimittain   llayn   ka
da      moliellavo          emia          sade    ftlsp

Geen ervan is een naam. Bijna allemaal zijn ze het aan het proberen. Ze zijn uitspreekbaar, ze eindigen waar namen eindigen, en één ervan — emia — zit maar één letter naast een echte naam. Het programma dat ze produceerde bevat 729 getallen, heeft geen idee van een woord, een lettergreep of een persoon, en werd fitted met één enkele pass waarin aangrenzende letterparen werden geteld.

Aan het einde van dit hoofdstuk heeft een neural network de score van dat programma op dezelfde meting met een derde verlaagd. Het deel waarvoor je wilt blijven is wat het netwerk eerst doet: het reproduceert de count table tot op drie decimalen op elke goed gevulde rij, zonder prompt, omdat de twee objecten antwoorden zijn op dezelfde vraag. Alles daarna is wat tellen nooit had kunnen doen.

De objective is een identiteit, geen ontwerpkeuze

Link naar de sectie: De objective is een identiteit, geen ontwerpkeuze

Hoofdstuk 7 liet je achter met een reeks integers en geen reden waarom de ene op de andere zou volgen. Hier is de reden, en het is één regel uit Hoofdstuk 2.

Een language model is een functie die de tokens tot nu toe neemt en een distributie teruggeeft over welke token daarna komt: één getal per vocabulaire-entry, niet-negatief, samen optellend tot één. Niets anders. Om daarvan naar een probability voor een heel document te komen, pas je de chain rule van probability toe:

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

Dat is een identiteit, waar voor elke sequence van wat dan ook, zonder aannames eraan vast. Dus een model dat de kleine taak doet — next token gegeven de vorige — heeft de grote taak van een probability toekennen aan elk mogelijk document al gedaan, exact en gratis. De populaire framing hiervan als een goedkoop trucje („het voorspelt alleen het volgende woord”) draait de logica om: de next token voorspellen is de joint distribution modelleren. Er was nooit een tweede ding om te doen.

De loss volgt net zo mechanisch. Op elke positie produceert het model een distributie qq en de waarheid is één bekende token, dus de cross-entropy uit Hoofdstuk 4 is ongewijzigd van toepassing:

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

Dat is de gemiddelde negative log-likelihood — het recept uit Hoofdstuk 2 met een categorical distribution op de plek waar de Gaussian zat. En omdat de true distribution one-hot is, is de entropy nul, dus volgens de identiteit uit Hoofdstuk 4 is de cross-entropy gelijk aan de KL divergence: dit getal omlaag duwen en de overtuigingen van het model naar die van de data trekken zijn dezelfde handeling.

Eén gevolg verdient een eigen zin, omdat het de economische werkelijkheid onder het hele veld is. De labels zijn de data, één positie verschoven. Niemand annoteert iets. Een biljoen tokens tekst is een biljoen vooraf gelabelde voorbeelden, en daarom is het training corpus van een modern model „het internet” en niet „een dataset die iemand heeft gebouwd”.

Vóór elk netwerk, de baseline: 32.033 namen, één per regel, en de taak om er meer te produceren, letter voor letter.1

Het vocabulaire bestaat uit 26 letters plus een boundary-symbool . dat zowel het begin als het einde van een naam markeert, dus het model moet leren waar namen beginnen en waar ze stoppen. Dat zijn 27 symbolen, en het kleinst mogelijke model is een tabel van hoe vaak elk symbool op elk ander symbool volgde.

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   

Twee regels rekenwerk en het model is fitted — en het is geen heuristic: counts delen door rijtotalen is de maximum-likelihood estimate voor een categorical distribution, oftewel het recept uit Hoofdstuk 2 met de calculus al gedaan.

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

Sample eruit — kies een letter uit de rij van de huidige letter, ga naar die rij, herhaal tot het boundary-symbool verschijnt — en je krijgt de namen bovenaan dit hoofdstuk. Ze falen op een specifieke en informatieve manier: lokaal plausibel, globaal onzin. Elk aangrenzend letterpaar in momakurailezitynn is een paar dat in echte namen voorkomt; er staan er alleen zeventien achter elkaar. Het model heeft één letter geheugen, dus het kan niet weten dat het al te lang doorgaat.

De loss op held-out namen is 2,4546 nats. Dat getal betekent op zichzelf niets, en daarom bestaat perplexity:

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}

Uitgeschreven, zonder library die het werk doet:

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

Exponentiëren maakt de logarithm ongedaan en brengt het getal terug naar de eenheden van dingen tellen. De nette manier om te zien wat het telt is een model meten dat helemaal niets weet — eentje dat probability 1/271/27 toekent aan elk symbool, ongeacht 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, want elog27=27e^{\log 27} = 27. Perplexity is het effectieve aantal even waarschijnlijke opties waaruit het model kiest. Een perplexity van 27 betekent „geen idee, kan van alles zijn”. De 11,642 van het count model betekent dat één letter context het even onzeker laat als iemand die blind uit ongeveer twaalf opties kiest in plaats van zevenentwintig — en daarom wordt perplexity geciteerd en de raw loss niet.

Twee dingen gaan ermee mis, en het tweede gaat mis in gepubliceerde papers.

Zero probabilities zijn fataal. Van de 729 cellen in de tabel komen er 113 nooit voor in training — 15,5 % ervan is leeg. Dat is prima tot de held-out set in zo'n cel belandt, en zeven bigrams in validation doen dat, waaronder dq, zj en qo twee keer. Probability nul betekent log -\infty, wat infinite loss en infinite perplexity betekent: één naam op drieduizend vernietigt de metric. De gebruikelijke patch is 1 toevoegen aan elke count vóór het normaliseren, wat hier bijna niets kost (2,4546 in plaats van 2,4524). Maar de patch is een bekentenis. Een count model kan helemaal niet generaliseren. Het kan niet vermoeden dat qo plausibel is omdat qu vaak voorkomt en o elders op u lijkt, omdat het geen idee heeft dat twee symbolen op elkaar kunnen lijken. Elke cel wordt afzonderlijk geleerd, en dat oplossen is waar de rest van dit hoofdstuk voor is.

Perplexity is een prijs per token, en de token is een vrije parameter. Dit is de fout die constant opduikt wanneer modellen worden vergeleken, en hij is makkelijk te zien zodra je kijkt. Neem hetzelfde corpus Engelse proza uit Hoofdstuk 7, hetzelfde geïnterpoleerde bigram model, en verander alleen hoe de tekst wordt opgeknipt:

eenheidvocabulairetokens in testcross-entropyperplexitybits per character
characters7614.4692,521712,453,6378
BPE, 512 merges3296.8713,854747,212,6407
BPE, 2.048 merges1.8204.2335,7468313,202,4254
woorden2.9916.2843,562735,262,2322

Perplexity varieert met een factor 25 over die rijen. Niets aan het model veranderde; alleen de grootte van het ding dat voorspeld werd. Een heel woord voorspellen is moeilijker dan een letter voorspellen, dus het kost meer per prediction — en er zijn minder predictions te doen.

Lees nu de laatste kolom, die de totale kosten in plaats daarvan deelt door het aantal characters en omzet naar bits. Hij herschikt de tabel. Op basis van perplexity is de ranking characters, woorden, BPE-512, BPE-2048; op basis van bits per character is het woorden, BPE-2048, BPE-512, characters. Het character model gaat van de eerste plaats naar de laatste. Het 2.048-merge model, dat op basis van perplexity 6,6 keer slechter lijkt dan het 512-merge model, is in werkelijkheid de betere van de twee met 2,4254 bits tegenover 2,6407.

Een perplexity is dus alleen vergelijkbaar tussen twee modellen die dezelfde tokenizer delen, en modellen met verschillende tokenizers kunnen alleen worden vergeleken in bits per character — de hoeveelheid die Shannon in 1951 mat door proefpersonen de volgende letter van Engelse tekst te laten raden, en die hij begrensde op ongeveer één bit per character.2 Onze beste bigram zit op 2,23 bits, wat een redelijke samenvatting is van hoe ver dit hoofdstuk nog te gaan heeft.

Bouw nu hetzelfde model als een netwerk. Het zal ordes van grootte meer rekenwerk kosten om op dezelfde plek uit te komen, en op dezelfde plek uitkomen is precies het punt.

Vervang de tabel door één weight matrix WW met shape 27×2727 \times 27. Zet de huidige letter om in een one-hot vector, vermenigvuldig, en noem het resultaat logits — de ongenormaliseerde scores uit Hoofdstuk 4. Daarna softmax, daarna cross-entropy, daarna 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

De gemarkeerde regel bevat een definitie die het waard is om te onthouden. Een one-hot vector met een matrix vermenigvuldigen selecteert één rij ervan, dus de vermenigvuldiging is een lookup — en elke implementatie slaat het rekenwerk over en doet de lookup direct, en dat is wat W[xs] is.

Dat is een embedding table. Een matrix met één rij per vocabulaire-entry, geïndexeerd op token id. Geen geometrie, geen semantiek, geen apart algoritme: een lookup table waarvan de inhoud toevallig wordt geleerd door gradient descent samen met al het andere. Elke mystieke claim over „embedding space” komt hierop neer.

Train het en kijk waar het heen gaat:

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

De laatste kolom is het grootste absolute verschil tussen een cel van softmax(W) en de bijbehorende cel van de count table, en dat gaat naar nul. Na 3.000 stappen is het grootste verschil waar dan ook in de 729 cellen 0,004048 en het gemiddelde 0,000224. De slechtste cel is qi, twaalf keer gezien in de hele training set; onder de 22 rijen met meer dan duizend voorkomens is het grootste verschil 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, begonnen met willekeurige getallen en met niets anders verteld dan „maak de log-probability van de volgende letter groot”, herontdekte de count table. En dat moest ook: de counts zijn de maximum-likelihood estimate, cross-entropy is de negative log-likelihood, dus beide procedures optimaliseren dezelfde objective en die objective heeft één optimum. Het netwerk leerde niet iets zoals tellen. Het convergeerde naar tellen, langzaam.

Dat roept de terechte vraag op waarom iemand de moeite zou nemen. Omdat de count table vanaf hier nergens heen kan, en het netwerk wel.

Breid het model uit zodat het naar meer dan één vorige character kijkt. Dit is Bengio's architectuur uit 2003, de directe voorouder van elk model in de rest van deze cursus:4 neem de laatste drie characters, map elk via een embedding table naar een 10-dimensionale rij, concateneer de rijen tot 30 getallen, duw ze door de hidden layer uit Hoofdstuk 5, en eindig met een output layer die één logit per vocabulaire-entry produceert.

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)

Let op wat nieuw is en wat niet. De hidden layer is die uit Hoofdstuk 5, ongewijzigd; de loss is die uit Hoofdstuk 4, ongewijzigd. De nieuwigheden zijn de embedding table aan de voorkant en een output layer zo breed als het vocabulaire uit Hoofdstuk 7 — en dat tweede is het dure deel van elk language model ooit gebouwd, omdat een echt vocabulaire 100.000 entries heeft en deze matrixvermenigvuldiging op elke positie draait.

Dezelfde code, identiek getraind, met alleen de grootte van de context window veranderd:

contextparametersvalidation lossvalidation perplexity
tellen, 1 character7292,454611,642
neural, 1 character7.8972,457711,678
neural, 3 characters11.8972,11458,285
neural, 8 characters21.8972,05067,773

De tweede rij is de interessante. Een netwerk met een hidden layer van 200 units en elf keer zoveel parameters als de count table presteert precies even goed als de count table en niet beter. Capaciteit was nooit de beperking. Eén character context staat een bepaalde loss toe en niets wat je eraan vastschroeft kan daaronder komen, omdat de informatie er niet is.

Geef het drie characters en de perplexity daalt van 11,68 naar 8,29 — een reductie van 29 %, gekocht met 4.000 extra parameters. Het verslaat tellen hier precies om de reden die eerder werd vastgesteld: een count model over contexten van drie characters heeft 273=19,68327^3 = 19{,}683 rijen nodig, de meeste leeg of met één enkele observatie, en het leert elke rij afzonderlijk. Het netwerk deelt. Als a, e en i vergelijkbare embedding rows krijgen, wordt wat het leert na bra overgedragen naar bre zonder dat het bre ooit heeft gezien. Die overdracht is de hele waarde van de embedding table, en het is het gat tussen rij twee en drie.

De samples verbeteren overeenkomstig:

TEXT
deliah   nellara   joce     kael      quintis
salayson  reety    khyrmin  mahnen    madiaryxia

Nog steeds geen lijst met echte namen. Maar deliah, nellara en kael zouden er niet misstaan, en de doordenderende monsters zijn verdwenen: de langste van twintig samples uit het count model is negentien letters, de langste van twintig uit dit model is dertien.

De tabel is 27×1027 \times 10: één rij van tien getallen per character, allemaal willekeurig geïnitialiseerd en alleen verplaatst door de gradient van de next-character loss. Niemand heeft er iets in gestopt. Dus wat is erin terechtgekomen?

Het hulpmiddel om dat te vragen is cosine similarity, de dot product uit Hoofdstuk 1 met de lengtes eruit gedeeld:

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

Het meet de hoek tussen twee vectors en negeert hun lengtes, en dat is wat je wilt wanneer de lengte van een rij weergeeft hoe vaak zijn token voorkwam in plaats van wat die betekent. Normaliseer elke vector eerst naar lengte 1 — zoals echte systemen doen, één keer, tijdens indexing — en cosine similarity is simpelweg de dot product.

Hier zijn de nearest neighbours van een paar characters in de getrainde tabel:

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

Een deel daarvan is wat de folklore belooft. c en k zijn uitwisselbaar in namen, en dat geldt ook voor i en y; j en z zijn allebei zeldzame, meestal initiële consonanten die zich vergelijkbaar gedragen. Het boundary-symbool . staat dicht bij helemaal niets — 0,077 tot de dichtstbijzijnde letter — omdat het het enige symbool is dat een positie markeert in plaats van een klank.

En een deel niet. De nearest neighbour van a is h, niet een andere vowel. Gemiddeld over alle paren:

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

De vowels lijken meer op elkaar dan op consonants, en het effect is echt maar klein. Getest tegen 2.000 willekeurig gekozen groepen van vijf letters scheiden 58 van die groepen minstens zo netjes — een verschil significant rond p=0.03p = 0.03. Echt dus, maar niets zoals het scherpe geometrische eiland dat populaire verhalen over embeddings suggereren.

Dat is de eerlijke beschrijving van een embedding table en het is de moeite waard om die voor de rest van de cursus vast te houden. Het is geen kaart van betekenis. Het is een verandering van coördinaten, geleerd in plaats van ontworpen, waarvan de enige taak is om het werk van de volgende layer makkelijk te maken — dezelfde zin die Hoofdstuk 5 gebruikte voor de hidden layer die het vlak vouwde om XOR op te lossen. Elke structuur die je erin vindt is er omdat die de loss verlaagde, en structuur die de loss niet verlaagt is er simpelweg niet.

word2vec, GloVe en de rekenkunde die iedereen citeert

Link naar de sectie: word2vec, GloVe en de rekenkunde die iedereen citeert

Als het nuttige deel de tabel is, kun je daar direct op mikken. Dat is word2vec: behoud de embedding lookup, gooi het language model weg.5

De skip-gram with negative sampling objective is één regel. Voor een echt (centre, context)-paar uit het corpus duw je hun dot product omhoog; voor kk nep-paren uit een noise distribution duw je hem omlaag: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})

Dat is binary classification — „kwamen deze twee woorden echt samen voor?” — en het is goedkoop precies omdat het nooit het volledige vocabulaire aanraakt, wat training op miljarden woorden in 2013 praktisch maakte. GloVe komt vanuit de andere richting tot vergelijkbare vectors, door de matrix van globale co-occurrence counts te factoriseren in plaats van door examples te streamen.7 Beide worden fitted op precies de statistic waaruit de count table was gebouwd. Ze zijn tellen, gecomprimeerd.

Getraind op text8 — 17.005.207 woorden Engelse Wikipedia, waarvan 71.290 minstens vijf keer voorkomen, 100 dimensies, drie passes — komen de vectors eruit met de eigenschap die ze beroemd maakte:

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

Niemand leverde een categorie voor instrumenten of voor numerals. Nu het beroemde deel: neem king, trek man af, tel woman erbij op, en zoek de dichtstbijzijnde vector bij het resultaat.

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)

De dichtstbijzijnde vector bij king - man + woman is king. Dat is geen gril van één voorbeeld. Mikolovs evaluation set stelt vragen van de vorm a : b :: c : ? — 8.869 semantische (paris : france :: rome : italy) en 10.675 syntactische (walking : walked :: swimming : swam) — en over de 4.103 semantische vragen die dit vocabulaire kan beantwoorden, is de winnaar 99,8 % van de tijd een van de drie inputwoorden. De gepubliceerde demonstraties noemen dat niet, omdat de standaard scoring rule a, b en c verwijdert vóór het kijken. Het is een legitieme rule, en hij doet meer werk dan de rekenkunde:

hoe het antwoord wordt gekozensemantischsyntactisch
offset, met de inputs uitgesloten (standaard)17,0 %11,9 %
offset, met niets uitgesloten0,1 %0,4 %
nearest neighbour van alleen c, inputs uitgesloten13,1 %9,3 %
nearest neighbour van alleen b, inputs uitgesloten2,3 %0,4 %

De derde rij is degene om even bij stil te staan. Gooi a en b weg, doe helemaal geen rekenkunde, geef terug wat het dichtst bij c ligt — en je behoudt 77 % van de semantische score. Het meeste van wat eruitziet als analogical reasoning is nabijheid plus een rule die de voor de hand liggende antwoorden verbiedt, en dat is wat Linzen mat op correct getrainde vectors en wat de baselines hierboven repliceren.8 Deze specifieke vectors zijn klein — 17 miljoen woorden tegenover de miljarden achter de gepubliceerde modellen — dus lees de percentages als een vorm, niet als state of the art. De vorm is wat op elke schaal overeind blijft: de rekenkunde is echt, en veel zwakker dan die ene demonstratie die iedereen citeert.

Static en contextual: één vector per woord, of één per voorkomens

Link naar de sectie: Static en contextual: één vector per woord, of één per voorkomens

Alles tot nu toe heeft een harde limiet ingebouwd in de data structure. Een tabel heeft één rij per token. Het woord bank krijgt één vector, dezelfde in een zin over een rivier en een zin over een hypotheek — noodzakelijkerwijs, omdat een lookup op id van niets anders kan afhangen.

De oplossing is stoppen met de vector uit de tabel lezen en hem uit de zin gaan berekenen. Dat is een contextual embedding, geïntroduceerd door ELMo in 2018 en hetzelfde jaar standaard gemaakt door BERT.910 Gemeten op het echte model zijn de getallen scherper dan de uitleg:

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

De eerste rij is exact, niet bij benadering: de static vector voor bank is dezelfde 768 getallen in beide zinnen, dus de cosine is per constructie 1. Negen layers later zitten de twee voorkomens op 0,43, terwijl bank in twee verschillende rivierzinnen op 0,87 blijft. Niemand labelde ergens in dit proces een betekenis; de betekenissen scheidden zich omdat ze scheiden de training objective — een verborgen token raden uit zijn buren — makkelijker maakt om te halen.

Twee details verdienen aandacht. Layer 0 is al 0,9512 in plaats van 1,0, omdat position embeddings zijn toegevoegd en het woord in elke zin op een andere plek staat. En de similarity stijgt weer in layers 11 en 12: de laatste layers van een pretrained model zijn gespecialiseerd voor zijn training objective, en zijn vaak niet de beste plek om een representation uit te halen.

Details tonen

Optioneel: weight tying.

In bert-base-uncased is de embedding table 30,522×76830{,}522 \times 768 — 23.440.896 getallen, 21,4 % van de 109.482.240 parameters van het model. In een klein language model is de fractie nog groter, en daarom is één truc bijna universeel: de input table en de output layer die de logits produceert zijn dezelfde matrix, één keer gebruikt via row lookup en één keer getransponeerd.11 De output layer kent al aan elke vocabulaire-entry een vector toe — hij neemt een dot product tegen elk ervan — en tying zegt dat de vector die wordt gebruikt om een token te lezen en de vector die wordt gebruikt om hem te schrijven hetzelfde object moeten zijn. Het verlaagt parameters en verbetert perplexity tegelijk, wat zeldzaam genoeg is om op te merken.

Om een corpus op betekenis te doorzoeken heb je één vector per zin nodig. Gegeven die vectors is de search triviaal — dit is het geheel van semantic retrieval, en Hoofdstuk 19 gaat over alles eromheen:

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]

De enige echte vraag is dus waar embed vandaan komt. De voor de hand liggende stap is een pretrained language model nemen, elke zin erdoorheen halen en de token vectors middelen. Hier staat die methode tegenover vier alternatieven, op twee manieren gescoord: de rank correlation tussen cosine en menselijke similarity judgements over de 1.379 paren van de STS benchmark, en top-1 retrieval op een index gebouwd uit de 200 sterkst geparafraseerde van die paren — één kant van elk paar geïndexeerd, de andere gebruikt als query.

hoe de zin wordt embeddedrank correlationtop-1 op een index met 200 zinnen
binary word overlap (helemaal geen model)0,550089,0 %
gemiddelde van de hierboven getrainde static vectors0,526385,5 %
BERT, de [CLS] token0,203067,0 %
BERT, gemiddelde van token vectors0,472984,0 %
MiniLM, contrastively getraind0,820392,0 %

Lees de middelste drie rijen tegenover de eerste twee. Een pretrained transformer met 109 miljoen parameters, op de voor de hand liggende manier gebruikt, is slechter in het beoordelen van zinsgelijkenis dan tellen hoeveel woorden twee zinnen delen — en slechter dan het middelen van de 100-dimensionale text8 vectors die we net trainden. De [CLS] token, die tutorials nog steeds aanbevelen omdat BERT was pretrained met een sentence-level objective eraan gekoppeld, is slechter dan de helft daarvan.

Dit is geen defect in BERT. Het is de objective. Een language model wordt getraind zodat zijn hidden states een token voorspellen; niets daarin vraagt dat twee parafrases dicht bij elkaar eindigen, en niets beloont een geometrie waarin cosine „dezelfde betekenis” betekent. De laatste rij is een model dat een vijfde van de grootte heeft (22.713.216 parameters) en op een heel andere loss is getraind: contrastive learning, waarbij de voorbeelden paren zijn — een vraag en zijn antwoord, een zin en zijn parafrase — en de objective echte paren naar elkaar toe trekt terwijl sampled negatives uit elkaar worden geduwd. Dat is de bijdrage van Sentence-BERT en de oorsprong van de hele embedding-model-industrie.12 Dense Passage Retrieval past hetzelfde recept direct toe op search, met één encoder voor queries en één voor passages.13

Dus de praktische regel:

Een embedding model is geen language model waar de laatste layer vanaf is gehaald. Het is een ander model met een andere objective, meestal veel kleiner, waarvan de cosine betekent wat je wilt dat hij betekent omdat het getraind is op paren waarbij dat het target was. De tabel hierboven is de prijs van het ene door het andere vervangen.

En de familie faalt op woordvolgorde. „The dog bit the man” en „the man bit the dog” hebben identieke bags of words, dus word overlap en het static-vector-gemiddelde geven ze cosine exact 1,000000, en mean-pooled BERT, dat positie wel ziet, komt nog steeds bijna daarop uit — en de contrastively getrainde MiniLM zet ze nog steeds op 0,979. Als je retrieval task afhangt van wie wat met wie deed, redt geen cosine threshold je.

Hoofdstuk 19 bouwt op deze basis een production retrieval system en komt uit op een concrete cosine cut-off. De laatste meting in dit hoofdstuk is wat zo'n getal verdedigbaar maakt in plaats van magisch.

Echte embeddings hebben honderden of duizenden componenten, en afstanden gedragen zich daarboven vreemd. Neem 1.000 willekeurige punten in de unit cube van dd dimensies en kijk naar de ratio tussen de grootste en de kleinste afstand tussen twee ervan:

dimensiesdichtstbijzijnde paarverste paarratio
20,00071,36121921,66
100,23612,33979,91
1003,00475,17521,72
1.00011,780914,03061,19
10.00039,615242,01251,06

In tienduizend dimensies ligt het verste paar punten maar 6 % verder uit elkaar dan het dichtstbijzijnde paar. Alles is ongeveer even ver verwijderd van alles, „nearest neighbour” draagt niet veel informatie meer, en dat is de curse of dimensionality — en ook één reden waarom grote vector databases geen exacte nearest-neighbour search doen. De andere kant van dezelfde medaille is wat cosine thresholds werkbaar maakt: gemeten over duizend paren willekeurige unit vectors ligt de gemiddelde cosine op 0.0052-0.0052 in 100 dimensies en +0.0003+0.0003 in 768, met standard deviations van 0,0968 en 0,0357 — en in 768 dimensies overschrijdt slechts 0,2 % van random paren 0,1 in absolute waarde. Een gemeten similarity van 0,4 is daarom niet „40 % gelijk”; hij ligt ver buiten alles wat toeval produceert, en daarom scheiden thresholds tussen 0,3 en 0,7 signaal van ruis in plaats van middenin te zitten.

Het model in dit hoofdstuk leest een vast aantal vorige characters, zoekt elk ervan op en plakt de resultaten in volgorde aan elkaar. Dat ontwerp heeft twee problemen, en ze zijn hetzelfde probleem.

Kijk nog eens naar de contexttabel: van drie naar acht characters gaan verdubbelde bijna de parameters en leverde 0,06 nats op. De kosten groeien lineair met de context — elke extra positie heeft zijn eigen plak van de eerste weight matrix nodig — en de opbrengst niet. Duw het naar duizend tokens en alleen de eerste layer weegt al zwaarder dan de rest van het model, grotendeels besteed aan posities die voor een gegeven prediction niet uitmaken.

Dat is het tweede probleem: het model heeft geen manier om te bepalen welke van de vorige tokens ertoe doen. Positie twee krijgt zijn eigen weights en positie zeven krijgt zijn eigen weights, permanent, wat er ook in staat. Wanneer het model nell spelt, is het beslissende character degene direct ervoor. Wanneer een zin een pronoun bevat, kan het woord dat zijn referent vastlegt veertig tokens terug staan — en geen vast slot kan worden toegewezen aan „veertig terug”, want de volgende keer is het zes.

Wat we willen is een model dat voor elke prediction berekent hoeveel elke eerdere token moet meetellen — weights over de context geproduceerd door de inhoud in plaats van vastgelegd door de layout. Schrijf dat zorgvuldig op en het begint als iets heel alledaags: een gemiddelde over de vorige tokens. Laat vervolgens de weights van dat gemiddelde worden geleerd, en laat ze afhangen van welke token de vraag stelt.

Dat is attention, en dat is Hoofdstuk 9.


Ook de moeite waard om ernaast te lezen: hoofdstuk 3 van Jurafsky en Martins Speech and Language Processing, dat n-gram models, smoothing en perplexity veel zorgvuldiger behandelt dan hier ruimte voor is, inclusief waarom interpolation en back-off beter zijn dan één toevoegen; de Stanford CS229-notities §17.1–17.2 voor language modelling vanuit de probabilistische kant; en Linzens paper hierboven, dat kort is en het waard om helemaal te lezen.

  1. Het voorbeeld voor naamgeneratie, de dataset en de voortgang van een count table naar een netwerk in Bengio-stijl volgen Andrej Karpathy's building makemore-serie, waarvan de eerste twee delen de beste aanvulling op dit hoofdstuk zijn.

  2. Shannon, C. E. Prediction and Entropy of Printed English. Bell System Technical Journal 30(1), pp. 50–64 (1951). Proefpersonen die de next letter van Engelse tekst raden, en de oorspronkelijke bits-per-character-meting.

  3. Shannon, C. E. A Mathematical Theory of Communication. Bell System Technical Journal 27 (1948). Het source coding theorem, en de identificatie van prediction met compressie.

  4. Bengio, Y., Ducharme, R., Vincent, P. en Jauvin, C. A Neural Probabilistic Language Model. Journal of Machine Learning Research 3, pp. 1137–1155 (2003). De hierboven gebruikte architectuur: een embedding per woord, geconcateneerd over een fixed window, door een hidden layer, naar een softmax over het vocabulaire.

  5. Mikolov, T., Chen, K., Corrado, G. en Dean, J. Efficient Estimation of Word Representations in Vector Space. arXiv:1301.3781 (2013). CBOW en skip-gram, en de hierboven gebruikte analogy set.

  6. Mikolov, T., Sutskever, I., Chen, K., Corrado, G. en Dean, J. Distributed Representations of Words and Phrases and their Compositionality. arXiv:1310.4546 (2013). Negative sampling, subsampling van frequente woorden, en de hierboven gebruikte noise distribution verheven tot de macht 3/4.

  7. Pennington, J., Socher, R. en Manning, C. GloVe: Global Vectors for Word Representation. EMNLP 2014. Word vectors uit een factorisatie van de globale co-occurrence matrix in plaats van gestreamde lokale windows.

  8. Linzen, T. Issues in evaluating semantic spaces using word analogies. RepEval 2016, arXiv:1606.07736. De bron van de offset-vrije baselines die hierboven zijn gerepliceerd.

  9. Peters, M. et al. Deep contextualized word representations. arXiv:1802.05365 (2018). ELMo: één vector per voorkomens, berekend door een bidirectional language model.

  10. Devlin, J., Chang, M.-W., Lee, K. en Toutanova, K. BERT: Pre-training of Deep Bidirectional Transformers for Language Understanding. arXiv:1810.04805 (2018). Het model gemeten in het bank-experiment.

  11. Press, O. en Wolf, L. Using the Output Embedding to Improve Language Models. arXiv:1608.05859 (2016), en Inan, H., Khosravi, K. en Socher, R. Tying Word Vectors and Word Classifiers. arXiv:1611.01462 (2016). Twee onafhankelijke argumenten voor dezelfde truc.

  12. Reimers, N. en Gurevych, I. Sentence-BERT: Sentence Embeddings using Siamese BERT-Networks. arXiv:1908.10084 (2019). De openingsmeting — mean-pooled BERT dat slechter presteert dan gemiddelde static vectors op zinsgelijkenis — is wat de tabel hierboven reproduceert.

  13. Karpukhin, V. et al. Dense Passage Retrieval for Open-Domain Question Answering. arXiv:2004.04906 (2020). Contrastive training van een retriever met twee encoders; de directe voorouder van de retrieval stack uit Hoofdstuk 19.

Klaar om LIA te laten kiezen?

Bouw met elk AI-model op één plek — begin vandaag nog gratis.