- Je haalt representaties op zonder token-ID als meetwaarde te behandelen.
- Je telt gedeelde embeddinggradiënten op en houdt padding buiten updates en gemiddelden.
Een netwerk kan niet uit token-ID 8 afleiden dat dit “meer” betekent dan ID 3. Die nummers zijn adressen, zoals rijnummers in een woordenlijst. Een embeddingtabel vervangt ieder adres door een vector die tijdens training kan veranderen.
Denk aan een fiche per token met meerdere instelbare waarden. Hetzelfde token verwijst steeds naar dezelfde fiche, ook wanneer het twee keer in een zin staat. Welke relaties de vectoren gaan uitdrukken hangt af van de trainingsopdracht; willekeurige beginwaarden hebben nog geen aangeleerde betekenis.
1. Haal rijen op in de gevraagde volgorde
Een tabel E van vorm (V,D) bevat V tokenrijen met ieder D getallen. Lookup geeft voor ieder ID de bijbehorende rij terug. Dat is gelijk aan een onehot-selectiematrix maal E, maar je hoeft die grote matrix niet echt te bouwen.
Neem vier rijen: PAD=[9,9], kat=[1,0], hond=[0,2], fiets=[−1,1]. De gekozen PAD-waarden zijn expres niet nul, zodat je straks ziet of het masker echt werkt. Voor IDs [2,1,2] verwacht je hond, kat, hond; een herhaling verdwijnt niet.
import numpy as np
table = np.array([[9.,9.],[1.,0.],
[0.,2.],[-1.,1.]])
ids = np.array([2,1,2])
values = table[ids].copy()
print(values.tolist())
# [[0.,2.],[1.,0.],[0.,2.]]
Gebruik een eendimensionale reeks gehele ID's binnen [0,V). Een negatief NumPy-indexnummer kan anders stilzwijgend een rij vanaf het einde kiezen. Een onbekend token vraagt een expliciete UNK-afspraak, niet een willekeurige modulo of afronding naar een bestaand adres.
Maak lookup(table,ids), geef een kopie van de geselecteerde rijen en weiger ongeldige ID's met ValueError. Controleer vóór het casten ook dimensionaliteit en integerdtype; een lege lijst levert een lege (0,D)-array. Behoud de tabel en de volgorde van de input.
012. Verzamel iedere bijdrage aan een gedeelde rij
Bij backprop krijgt iedere opgezochte positie een upstream-vector dy. Die bijdrage hoort terug bij de rij van zijn token. Omdat meerdere posities dezelfde rij kunnen gebruiken, moet je bijdragen optellen in plaats van de laatste te bewaren.
Noem G de tabelgradiënt. Voor een tokenrij k sommeer je alle dy-posities met dat ID; een paddingrij slaan we over wanneer padding_idx is opgegeven. We delen hier niet door het aantal voorkomens, want eventuele verliesmiddeling zit al in dy.
Neem IDs [2,1,2,0] en dy=[[1,2],[3,4],[−0.5,1],[9,9]]. Rij 2 ontvangt [1,2]+[−0.5,1]=[0.5,3], rij 1 krijgt [3,4], en bij padding_idx 0 blijft rij 0 nul. De ongebruikte fietsrij krijgt ook nul.
ids = np.array([2,1,2,0])
dy = np.array([[1.,2.],[3.,4.],
[-.5,1.],[9.,9.]])
gradient = np.zeros_like(table)
for token,signal in zip(ids,dy):
if token != 0:
gradient[token] += signal
print(gradient.tolist())
# [[0.,0.],[3.,4.],[.5,3.],[0.,0.]]
Een veelgemaakte fout is gradient[ids]+=dy: geavanceerde indexering kan herhaalde indices via een tijdelijke array verwerken en zo bijdragen verliezen. Een expliciete lus werkt; np.add.at is een tweede correcte manier om dezelfde herhalingen op te tellen.
other = np.zeros_like(table)
mask = ids != 0
np.add.at(other,ids[mask],dy[mask])
print(np.array_equal(other,gradient))
# True
Maak embedding_backward(ids,dy,vocab_size,padding_idx=None) met uitvoervorm (vocab_size,D). De opgegeven ID's zijn geldig; begin op nul, tel alle bijdragen op en behoud ids en dy. Zonder padding_idx sla je geen rij over; padding hoeft bovendien niet altijd ID 0 te zijn.
De beginvectoren zijn handgemaakte rekenvoorbeelden. Iedere kat-positie ontvangt dy=(1,−0.5), hond (−0.2,0.3), PAD (4,4). Bekijk één SGD-update.
| Token | Beginvector | Gradiëntsom | Nieuwe vector |
|---|---|---|---|
| PADID 0 | 0.00, 0.00 | 0.00, 0.00 | 0.00, 0.00 |
| katID 1 | 1.00, 0.00 | 2.00, -1.00 | 0.80, 0.10 |
| hondID 2 | 0.00, 1.00 | -0.20, 0.30 | 0.02, 0.97 |
| fietsID 3 | -1.00, 1.00 | 0.00, 0.00 | -1.00, 1.00 |
De niet-gebruikte fietsrij krijgt geen bijdrage. Delen door het aantal voorkomens is een andere afspraak; hier tellen we op.
3. Middel alleen echte tokens
Een eenvoudige reeksrepresentatie is het gemiddelde van de echte tokenvectoren. Laat padding zowel uit de som als uit de noemer weg. Voor [1,0,2,0] blijven kat=[1,0] en hond=[0,2] over: hun som [1,2] gedeeld door 2 geeft [0.5,1].
ids = np.array([1,0,2,0])
mask = ids != 0
kept = table[ids][mask]
average = kept.mean(axis=0)
print(average.tolist())
# [.5,1.]
print(np.zeros(table.shape[1]).tolist())
# [0.,0.]
Zelfs wanneer de PAD-vector nul was, zou delen door alle vier posities hier [0.25,0.5] geven. Dat maakt een korte reeks kunstmatig kleiner. Zet de paddingrij niet tijdelijk op nul: daarmee verander je de gedeelde invoertabel en los je de verkeerde noemer niet op.
Maak masked_mean(table,ids,padding_idx=0), gebruik lookup voor geldige ID's en geef een vector van breedte D terug. Voor een lege reeks of alleen padding kies je expliciet de nulvector. Herhaalde echte tokens blijven meerdere keren meetellen.
Een gemiddelde verliest volgorde; een RNN verwerkt die juist stap voor stap. Ook een nulinput kan een recurrente toestand veranderen via gewichten en bias. Daar vraagt padding om een eigen lengtemasker, niet alleen om een nulvector.
03Wat neem je mee?
- Token-ID's selecteren rijen; hun numerieke grootte draagt geen betekenis.
- Herhaalde tokens delen parameters en tellen hun gradiënten op.
- Padding vraagt een expliciete afspraak bij updates én gemiddelden.
Hierna onderzoek je hoe tekst wordt opgedeeld in de tokens die deze tabel gaan gebruiken.