Implementation

Creaimo una rete che determina il prossimo carattere sulla base di un contesto di block_size caratteri precedenti, con block_size=8 caratteri.
Dotiamola di layer batchNorm.

Partendo dal solito boilerplate iniziale, abbiamo già ricavato i set di dati di input:

Xtr,  Ytr  = build_dataset(words[:n1])     # training set (80% of total)
Xdev, Ydev = build_dataset(words[n1:n2])   # validation set (10% of total)
Xte,  Yte  = build_dataset(words[n2:])     # test set (10% of total)

# shapes:
# Xtr:  torch.Size([182625, 8]), Ytr:  torch.Size([182625])
# Xdev: torch.Size([22655, 8]),  Ydev: torch.Size([22655])
# Xte:  torch.Size([22866, 8]),  Yte:  torch.Size([22866])

Creiamo la rete:

vocab_size = 27 # len(itos) alphabet chars
block_size = 8 # contensto caratteri precedenti 
n_embd = 10 # the dimensionality of the character embedding vectors
n_hidden = 200 # the number of neurons in the hidden layer of the MLP
model = Sequential([
  Embedding(vocab_size, n_embd), # input layer
  Flatten(), Linear(n_embd * block_size, n_hidden, bias=False), BatchNorm1d(n_hidden), Tanh(), # hidden layers
  Linear(n_hidden, vocab_size) # output layer
])

# parameter init
with torch.no_grad():
  model.layers[-1].weight *= 0.1 # last layer make less confident

parameters = model.parameters()
print('total parameters:', sum(p.nelement() for p in parameters)) # number of parameters in total
for p in parameters:
  p.requires_grad = True

Prendiamo un subset di 4 dati a caso e diamoli in input alla rete:

ix = torch.randint(0, Xtr.shape[0], (4,)) # estraggo 4 numeri a caso tra 0 e dim massima di Xtr (182625)
Xb, Yb = Xtr[ix], Ytr[ix]
# Xb.shape: [4, 8], Xb: tensor([[ 0,  0,  0,  0, 26,  1, 18,  9],
#                               [ 0,  0,  5, 12, 19, 16,  5, 20],
#                               [ 0,  0,  0,  0,  0,  0, 12,  5],
#                               [ 0,  0, 12, 15, 18,  1, 12, 25]])
# Yb.shape: [4], Yb: tensor([12, 20,  1, 20])

# diamoli in pasto alla rete:
logits = model(Xb)

Adesso controlliamo le forme dei layer:

  • Embedding Layer: la lookup table crea un vettore 10-dimensionale per ogni carattere che stiamo cercando di apprendere.
    L'Embedding layer estrae un vettore 10-dimensionale per ogni numero di ogni riga di Xb
    Quindi il layer di embedding traduce ogni intero in input in un vettore 10-dimensionale.
    Facendo passare Xb attraverso l'embedding layer, in output viene creato un tensore [4, 8, 10]
# Embedding layer:
# crea 8 vettori 10-dimensionali per ognuno dei numeri [ 0,  0,  5, 12, 19, 16,  5, 20] 
# che è una riga di Xb.
model.layers[0].out.shape # output shape: [4, 8, 10]
  • Flatten Layer: il layer appiattisce le ultime due dimensioni del suo input [4, 8, 10], restituendo la forma semplificata perché prende i vettori 10-dimensionali e li accoda in una unica riga, concatenandoli.
# Flatten layer:
model.layers[1].out.shape # output shape: [4, 80]
  • Hidden layers: il Linear layer prende in input [4, 80] ed esegue moltiplicazione di matrici, siccome abbiamo impostato 200 neuroni, darà in output una forma del tipo [4, 200], che rimarrà invariata per l'output del BatchNorm Layer e del layer di attivazione non lineare Tanh
# Linear layer (hidden layer)
# prende in input [4, 80] ed esegue moltiplicazione di matrici, ricordando
# che abbiamo impostato 200 neuroni,  dando in output
model.layers[2].out.shape # output shape: [4, 200]

# BatchNorm layer
model.layers[3].out.shape # output shape: [4, 200]

# Tanh layer
model.layers[4].out.shape # output shape: [4, 200]
  • Output layer: Avremo in output 4 righe (visto che abbiamo 4 input..) ognuna che potrà restituire uno dei 27 caratteri dell'alfabeto.
# Linear NN output layer
model.layers[5].out.shape # output shape: [4, 27]