Embedding
Token embedding: come un token ID seleziona un vettore
Costruisci un token embedding dal testo originale, un passaggio concreto alla volta, finché il Transformer ha un vettore per ogni posizione.
Costruiamo l'input di un Transformer partendo dall'inizio.
Inizieremo da una breve frase. A ogni passaggio aggiungeremo un nuovo oggetto o eseguiremo una nuova operazione. Terremo sempre traccia di cosa esiste prima del passaggio, cosa fa il passaggio e cosa esiste dopo.
Il testo di partenza è:
The cat sleeps
Il risultato finale sarà composto da tre vettori numerici, uno per ogni posizione della sequenza. La parte importante è il percorso tra questi due stati.
Passo 1identificare i token e le loro posizioni
L'input per ora è una sequenza di caratteri:
The cat sleeps
Prima che un Transformer possa processare questo testo, un tokenizer lo divide in token. Per questo piccolo esempio tratteremo ogni parola come un token:
The | cat | sleeps
Ci sono tre posizioni:
posizione 0 contiene The
posizione 1 contiene cat
posizione 2 contiene sleeps
Una posizione ci dice dove compare un token in questa sequenza specifica.
Questo è un tokenizer semplificato. I tokenizer reali possono spezzare una parola in più token. Un token può anche contenere uno spazio o solo una parte di una parola. Questa differenza non cambia l'embedding lookup che costruiremo.
Alla fine di questo primo passo abbiamo:
posizione 0 -> The
posizione 1 -> cat
posizione 2 -> sleeps
Il prossimo passo darà a ogni token un identificatore numerico.
Passo 2usare il vocabolario del tokenizer per produrre ID
Il tokenizer ha un vocabolario. Un vocabolario è una lista fissa che associa ogni possibile token a un ID intero.
Useremo questo vocabolario minuscolo:
ID 0 -> sleeps
ID 1 -> dog
ID 2 -> The
ID 3 -> runs
ID 4 -> cat
Il tokenizer cerca ogni token in quel vocabolario:
The -> ID 2
cat -> ID 4
sleeps -> ID 0
Il testo ora può essere rappresentato così:
[2, 4, 0]
Aggiungiamo questi ID allo stato che avevamo già:
| Posizione | Token | Token ID |
|---|---|---|
| 0 | The | 2 |
| 1 | cat | 4 |
| 2 | sleeps | 0 |
Ogni card rappresenta una posizione. La card centrale è evidenziata perché seguiremo cat nei prossimi passaggi.
- The
posizione 0ID 2 - cat
posizione 1ID 4(highlighted) - sleeps
posizione 2ID 0
Posizione e token ID ora sono visibilmente diversi.
Per cat:
posizione 1 -> dove compare cat in questa sequenza
token ID 4 -> quale voce del vocabolario identifica cat
L'ID non è una misura. ID 4 è più grande di ID 2, ma questo non rende cat
più grande o più significativo di The. I numeri sono stati assegnati dal
tokenizer in modo che ogni voce del vocabolario abbia un identificatore unico.
Alla fine di questo passo, il modello ha un ID per ogni posizione:
testo:
The cat sleeps
posizioni:
[0, 1, 2]
token ID:
[2, 4, 0]
Questi ID preservano quali token compaiono e in quale ordine. Il prossimo passo aggiunge i valori numerici addestrabili che i calcoli successivi useranno.
Passo 3aggiungere una embedding table
I layer del Transformer operano su vettori. Un vettore è una lista ordinata di numeri. Ogni posizione deve ricevere un vettore con lo stesso numero di valori, così le stesse operazioni successive possono essere applicate a ogni posizione.
Per questo esempio scegliamo due valori per vettore.
Il modello quindi memorizza una riga con due valori per ogni token ID:
La tabella ha cinque righe perché il vocabolario ha cinque ID. Ogni riga ha due valori perché questo esempio usa vettori bidimensionali.
| Row | valore 1 | valore 2 |
|---|---|---|
| riga 0 · sleeps | -0.4 | 0.8 |
| riga 1 · dog | 0.1 | 0.5 |
| riga 2 · The | 0.2 | -0.1 |
| riga 3 · runs | -0.2 | 0.6 |
| riga 4 · cat | 0.7 (highlighted) | 0.3 (highlighted) |
Questa è la token embedding table.
I suoi valori sono parametri del modello. Vengono inizializzati quando inizia il training e poi vengono aggiustati, insieme agli altri parametri del modello, per migliorare la predizione dei token. Una persona non assegna manualmente a ogni colonna un significato fisso come "animale" o "sostantivo".
La tabella non sostituisce il vocabolario del tokenizer. Le due strutture hanno compiti diversi:
vocabolario del tokenizer:
token <-> token ID
embedding table:
token ID -> riga numerica addestrabile
Ora abbiamo tutti gli oggetti necessari per l'operazione successiva:
sequenza di token:
The | cat | sleeps
token ID:
[2, 4, 0]
embedding table:
5 righe, con 2 valori in ogni riga
Non è ancora stata selezionata nessuna riga dalla embedding table. Lo facciamo adesso.
Passo 4seguire un token ID dentro la tabella
Prendiamo il token centrale, cat.
Lo stato costruito finora ci dice:
cat è alla posizione 1 della sequenza
cat ha token ID 4
L'embedding layer riceve il token ID, non la posizione nella sequenza. Usa ID 4
per selezionare la riga 4 dalla embedding table:
input:
token ID 4
operazione:
seleziona riga 4
output:
[0.7, 0.3]
Questa selezione di riga si chiama embedding lookup. La riga selezionata è
il token embedding vector di cat.
Il numero 4 non viene moltiplicato o trasformato in [0.7, 0.3]. Si comporta
come un indirizzo che identifica quale riga recuperare.
Lo stato alla posizione 1 è quindi cambiato:
prima del lookup:
posizione 1 contiene token ID 4
dopo il lookup:
posizione 1 contiene il vettore [0.7, 0.3]
La posizione è ancora 1. Il token è ancora cat. Il nuovo oggetto è il
vettore a due valori selezionato dal token ID.
Passo 5ripetere lo stesso lookup a ogni posizione
Non serve un nuovo meccanismo per gli altri token. Ripetiamo l'operazione che
abbiamo appena eseguito per cat.
La sequenza completa di ID è:
[2, 4, 0]
Processiamo gli ID da sinistra a destra:
posizione 0:
ID 2 -> seleziona riga 2 -> [ 0.2, -0.1]
posizione 1:
ID 4 -> seleziona riga 4 -> [ 0.7, 0.3]
posizione 2:
ID 0 -> seleziona riga 0 -> [-0.4, 0.8]
Manteniamo i vettori selezionati nello stesso ordine della sequenza:
[
[ 0.2, -0.1],
[ 0.7, 0.3],
[-0.4, 0.8]
]
La frase mantiene tre posizioni. Ogni token ID seleziona una riga con due valori per la sua posizione.
- Token
3 posizioniThe | cat | sleeps
- Token ID
3[2, 4, 0]
- Embedding lookup
uno per posizioneSeleziona le righe 2, 4 e 0.
- Token vector
3 × 2[[0.2, -0.1], [0.7, 0.3], [-0.4, 0.8]]
Ora possiamo mettere ogni stato in una sola tabella:
| Posizione | Token | Token ID | Vettore di embedding selezionato |
|---|---|---|---|
| 0 | The | 2 | [0.2, -0.1] |
| 1 | cat | 4 | [0.7, 0.3] |
| 2 | sleeps | 0 | [-0.4, 0.8] |
Siamo partiti da un token in ogni posizione. Il tokenizer ha aggiunto un ID a ogni posizione. L'embedding lookup ha poi usato ogni ID per selezionare un vettore per quella stessa posizione.
La trasformazione completa è:
The cat sleeps
↓ vocabolario del tokenizer
[2, 4, 0]
↓ seleziona righe di embedding 2, 4 e 0
[
[ 0.2, -0.1],
[ 0.7, 0.3],
[-0.4, 0.8]
]
Passo 6collegare il risultato al Transformer
Il modello ora ha un vettore numerico per ogni posizione. Queste sono le rappresentazioni iniziali dei token che verranno usate dal processing stage successivo dell'input.
Il lookup non ha ancora usato le parole circostanti. Per esempio, se lo stesso token ID compare in due frasi diverse, seleziona la stessa riga di embedding in entrambe le frasi.
Questo significa che il lookup da solo non ha ancora costruito contesto di frase.
Il risultato evidenziato di questo articolo è lo stage iniziale dei token vector. Gli stage successivi aggiungono posizione, contesto e predizione.
- Testo
caratteriThe cat sleeps
- Tokenizer
3 ID[2, 4, 0]
- Token embedding
3 × 2Un vettore iniziale per posizione.
(highlighted) - Transformer block
3 × 2Costruiscono stati dipendenti dal contesto.
- Calcoli di output
score sul vocabolarioSupportano la predizione del prossimo token.
Con questo articolo abbiamo completato lo step dei token embedding. L'output che abbiamo costruito diventa l'input dello stage successivo.
Derivare le shape dall'esempio
Conosciamo già ogni quantità concreta, quindi ora possiamo assegnare un simbolo a ciascuna:
V = numero di voci nel vocabolario
T = numero di posizioni nella sequenza
D = numero di valori in ogni embedding vector
Nel nostro esempio:
V = 5
T = 3
D = 2
Quindi:
embedding table: [V,D] = [5,2]
input ID: [T] = [3]
output vector: [T,D] = [3,2]
Le shape descrivono oggetti che abbiamo già costruito:
[5,2]descrive cinque righe memorizzate con due valori per riga;[3]descrive tre token ID in una sequenza;[3,2]descrive tre posizioni di output con un vettore a due valori per ogni posizione.
Se il modello processa più sequenze insieme, aggiungiamo una dimensione batch
B:
input ID: [B,T]
output vector: [B,T,D]
Il lookup esegue ancora la stessa selezione di riga per ogni token ID.
Eseguire lo stesso esempio in PyTorch
Il codice usa esattamente la stessa dimensione del vocabolario, gli stessi valori di embedding, gli stessi ID e lo stesso ordine di output dell'esecuzione manuale:
import torch
from torch import nn
weights = torch.tensor([
[-0.4, 0.8], # ID 0: sleeps
[ 0.1, 0.5], # ID 1: dog
[ 0.2, -0.1], # ID 2: The
[-0.2, 0.6], # ID 3: runs
[ 0.7, 0.3], # ID 4: cat
])
embedding = nn.Embedding(
num_embeddings=5,
embedding_dim=2,
)
with torch.no_grad():
embedding.weight.copy_(weights)
input_ids = torch.tensor([[2, 4, 0]])
output = embedding(input_ids)
print(output)
print(output.shape)
Il risultato è:
tensor([[
[ 0.2, -0.1],
[ 0.7, 0.3],
[-0.4, 0.8]
]])
torch.Size([1, 3, 2])
Il primo 1 è la dimensione batch: una sequenza. Le dimensioni rimanenti sono
le tre posizioni e i due valori in ogni vettore.
nn.Embedding ha eseguito le stesse tre selezioni di riga che abbiamo fatto a
mano.
Notazione compatta opzionale
Chiamiamo la embedding table E.
Dato che contiene V righe e D valori per riga:
E ∈ R^(V×D)
Per un token ID i, il lookup restituisce:
E[i]
Questa notazione comprime l'operazione che già conosciamo:
ricevi ID i
-> seleziona riga i da E
-> restituisci quella riga con D valori
La stessa selezione può essere scritta matematicamente come one_hot(i) × E.
Il lookup diretto della riga è l'implementazione efficiente standard perché non
deve costruire l'intero vettore one-hot.
Ricostruire il modello ancora una volta
Partendo dalla frase originale:
- Il tokenizer ha diviso
The cat sleepsin tre token. - Il vocabolario del tokenizer ha mappato quei token agli ID
[2,4,0]. - Il modello ha memorizzato una riga di embedding con due valori per ogni ID del vocabolario.
- Ogni token ID ha selezionato la sua riga corrispondente.
- Le righe selezionate sono rimaste nell'ordine originale della sequenza.
- Il risultato è stato un insieme
[3,2]di token vector iniziali. - Il processamento della posizione e i Transformer block possono ora operare su quei vettori.
La distinzione chiave è:
posizione nella sequenza -> dove compare un token
token ID -> quale voce del vocabolario lo identifica
embedding vector -> quale riga addestrabile viene selezionata dall'ID
Per testare il modello su un nuovo caso, sposta cat alla posizione 7 senza
cambiare il suo token ID. Quale riga di embedding viene selezionata?
La risposta è ancora la riga 4. La posizione è cambiata, ma l'ID del
vocabolario no.