LLM: un peu de technique#

Sources:

Byte-pair encoding#

Comment on tokenise? On reconstruit le vocabulaire de manière inductove en partant de la totalités des caractères pour ensuite charcher les caractères qui sont plus souvent ensemble.

with open('input.txt', 'r', encoding='utf-8') as f:
    text = f.read()
# here are all the unique characters that occur in this text
chars = sorted(list(set(text)))
vocab_size = len(chars)
print(''.join(chars))
print(vocab_size)
 !#'(),-.123459:;?ACDEFGIJKLMOPQRSTUW[\]^_abcdefghijklmnopqrstuvxyz{}«·»ÀÉàâçèéêîôùœ
85
# create a mapping from characters to integers
stoi = { ch:i for i,ch in enumerate(chars) }
itos = { i:ch for i,ch in enumerate(chars) }
encode = lambda s: [stoi[c] for c in s] # encoder: take a string, output a list of integers
decode = lambda l: ''.join([itos[i] for i in l]) # decoder: take a list of integers, output a string

print(encode("hi there"))
print(decode(encode("hi there")))
[50, 51, 1, 62, 50, 47, 60, 47]
hi there
import torch
data = torch.tensor(encode(text), dtype=torch.long)
print(data.shape, data.dtype)
print(data[:1000]) # the 1000 characters we looked at earier will to the GPT look like this
torch.Size([10593]) torch.int64
tensor([ 3,  1, 25, 56, 62, 60, 57, 46, 63, 45, 62, 51, 57, 56,  0,  0, 26, 47,
         1, 56,  4, 43, 51,  1, 58, 43, 61,  1, 46, 47,  1, 61, 55, 43, 60, 62,
        58, 50, 57, 56, 47,  9,  1, 20, 47, 54, 43,  1, 48, 43, 51, 62,  1, 46,
        47,  1, 55, 57, 51,  1, 63, 56,  1, 79, 62, 60, 43, 56, 49, 47,  1, 58,
        47, 60, 61, 57, 56, 56, 43, 49, 47,  7,  1, 51, 56, 43, 46, 43, 58, 62,
        79,  1, 75,  1, 54, 43,  1, 64, 51, 47,  1, 46, 47,  1, 56, 57, 61,  1,
        61, 57, 45, 51, 79, 62, 79, 61,  1, 56, 63, 55, 79, 60, 51, 59, 63, 47,
        61,  9,  1, 28, 57, 60, 61,  1, 46, 47,  1, 55, 43,  1, 46, 47, 60, 56,
        51, 78, 60, 47,  1, 64, 51, 61, 51, 62, 47,  1, 75,  1, 54, 43,  1, 44,
        43, 56, 59, 63, 47,  1, 58, 57, 63, 60,  1, 47, 48, 48, 47, 45, 62, 63,
        47, 60,  1, 63, 56,  1, 64, 51, 60, 47, 55, 47, 56, 62,  7,  1, 57, 56,
         1, 55,  4, 43,  1, 46, 47, 55, 43, 56, 46, 79,  1, 46, 47,  1, 61, 57,
        60, 62, 51, 60,  1, 55, 57, 56,  1, 62, 79, 54, 79, 58, 50, 57, 56, 47,
         1, 58, 57, 63, 60,  1, 55,  4, 51, 46, 47, 56, 62, 51, 48, 51, 47, 60,
         9,  1, 28, 57, 60, 61, 59, 63, 47,  1, 52,  4, 43, 51,  1, 51, 56, 46,
        51, 59, 63, 79,  1, 59, 63, 47,  1, 52, 47,  1, 56,  4, 47, 56,  1, 43,
        64, 43, 51, 61,  1, 58, 43, 61,  7,  1, 57, 56,  1, 55,  4, 43,  1, 46,
        51, 62,  1, 46,  4, 43, 54, 54, 47, 60,  1, 54, 47,  1, 45, 50, 47, 60,
        45, 50, 47, 60,  1, 75,  1, 55, 57, 56,  1, 46, 57, 55, 51, 45, 51, 54,
        47,  9,  1, 32, 63, 43, 56, 46,  1, 54, 47, 61,  1, 47, 55, 58, 54, 57,
        66, 79, 71, 47, 61,  1, 57, 56, 62,  1, 48, 51, 56, 43, 54, 47, 55, 47,
        56, 62,  1, 45, 57, 55, 58, 60, 51, 61,  1, 59, 63, 47,  1, 52, 47,  1,
        56, 47,  1, 58, 57, 61, 61, 79, 46, 43, 51, 61,  1, 62, 57, 63, 62,  1,
        61, 51, 55, 58, 54, 47, 55, 47, 56, 62,  1, 58, 43, 61,  1, 46, 47,  1,
        58, 57, 60, 62, 43, 44, 54, 47,  7,  1, 57, 56,  1, 55,  4, 43,  1, 51,
        56, 48, 57, 60, 55, 79,  1, 59, 63,  4, 51, 54,  1, 79, 62, 43, 51, 62,
         1, 51, 55, 58, 57, 61, 61, 51, 44, 54, 47,  1, 46, 47,  1, 55,  4, 51,
        46, 47, 56, 62, 51, 48, 51, 47, 60, 39,  1, 16,  1, 55, 43,  1, 58, 60,
        79, 61, 47, 56, 45, 47,  1, 58, 50, 66, 61, 51, 59, 63, 47,  7,  1, 46,
        47, 64, 43, 56, 62,  1, 54, 47,  1, 49, 63, 51, 45, 50, 47, 62, 51, 47,
        60,  7,  1, 43, 64, 47, 45,  1, 55, 57, 56,  1, 58, 43, 61, 61, 47, 58,
        57, 60, 62,  1, 75,  1, 54, 43,  1, 55, 43, 51, 56,  7,  1, 56,  4, 79,
        62, 43, 51, 62,  1, 58, 43, 61,  1, 63, 56,  1, 55, 57, 66, 47, 56,  1,
        46,  4, 51, 46, 47, 56, 62, 51, 48, 51, 45, 43, 62, 51, 57, 56,  1, 43,
        58, 58, 60, 57, 58, 60, 51, 79,  7,  1, 51, 54,  1, 48, 43, 54, 54, 43,
        51, 62,  1, 54,  4, 43, 64, 43, 54,  1, 46,  4, 63, 56, 47,  1, 43, 58,
        58, 54, 51, 45, 43, 62, 51, 57, 56,  1, 55, 57, 44, 51, 54, 47,  9,  0,
         0, 25, 54,  1, 48, 63, 62,  1, 63, 56,  1, 62, 47, 55, 58, 61,  1, 57,
        83,  7,  1, 46, 43, 56, 61,  1, 56, 57, 61,  1, 61, 57, 45, 51, 79, 62,
        79, 61,  7,  1, 45,  4, 79, 62, 43, 51, 62,  1, 54,  4, 74, 49, 54, 51,
        61, 47,  1, 59, 63, 51,  1, 49, 43, 60, 43, 56, 62, 51, 61, 61, 43, 51,
        62,  1, 54,  4, 51, 46, 47, 56, 62, 51, 62, 79,  1, 46, 47, 61,  1, 45,
        51, 62, 57, 66, 47, 56, 56, 71, 47, 61,  1, 43, 64, 47, 45,  1, 54, 47,
        61,  1, 60, 47, 49, 51, 61, 62, 60, 47, 61,  1, 46, 47,  1, 58, 43, 60,
        57, 51, 61, 61, 47,  1, 46, 43, 56, 61,  1, 54, 47, 61, 59, 63, 47, 54,
        61,  1, 79, 62, 43, 51, 47, 56, 62,  1, 51, 56, 61, 45, 60, 51, 62, 61,
         1, 54, 47, 61,  1, 56, 57, 55, 61,  1, 46, 47,  1, 44, 43, 58, 62, 80,
        55, 47,  1, 46, 47, 61,  1, 58, 47, 60, 61, 57, 56, 56, 47, 61, 39,  1,
        17,  1, 43, 63, 52, 57, 63, 60, 46,  4, 50, 63, 51,  7,  1, 45,  4, 47,
        61, 62,  1, 75,  1, 19, 58, 58, 54, 47,  1, 47, 62,  1, 75,  1, 24, 57,
        57, 49, 54, 47,  1, 59, 63, 47,  1, 60, 47, 64, 51, 47, 56, 62,  1, 45,
        47, 62, 62, 47,  1, 62, 76, 45, 50, 47,  9,  0, 19, 64, 47, 45,  1, 54,
        43,  1, 58, 43, 56, 46, 79, 55, 51, 47,  1, 46, 47,  1, 54, 43,  1, 20,
        57, 64, 51, 46,  8, 10, 15,  1, 47, 62,  1, 54, 43,  1, 55, 51, 61, 47,
         1, 47, 56,  1, 58, 54, 43, 45, 47,  1, 46, 63,  1, 58, 43, 61, 61, 47,
        58, 57, 60, 62,  1, 64, 43, 45, 45, 51])
# Let's now split up the data into train and validation sets
n = int(0.9*len(data)) # first 90% will be train, rest val
train_data = data[:n]
val_data = data[n:]
torch.manual_seed(1337)
batch_size = 4 # combien de séquences on analyse en paralèle - seulement pour paralléliser le calcul, ça change rien au résultat à part la vitesse si on a du GPU.
block_size = 8 # what is the maximum context length for predictions?

def get_batch(split):
    # generate a small batch of data of inputs x and targets y
    data = train_data if split == 'train' else val_data
    ix = torch.randint(len(data) - block_size, (batch_size,))
    x = torch.stack([data[i:i+block_size] for i in ix])
    y = torch.stack([data[i+1:i+block_size+1] for i in ix])
    return x, y

xb, yb = get_batch('train')
print('inputs:')
print(xb.shape)
print(xb)
print('targets:')
print(yb.shape)
print(yb)

print('----')

for b in range(batch_size): # batch dimension
    for t in range(block_size): # time dimension
        context = xb[b, :t+1]
        target = yb[b,t]
        print(f"when input is {decode(context.tolist())} the target: {decode([target.item()])}")
inputs:
torch.Size([4, 8])
tensor([[47, 61,  1, 56, 63, 55, 79, 60],
        [57, 60, 55, 47,  1, 57, 63,  1],
        [ 1, 58, 57, 63, 60,  1, 47, 48],
        [43, 61,  1, 46, 47,  1, 61, 55]])
targets:
torch.Size([4, 8])
tensor([[61,  1, 56, 63, 55, 79, 60, 51],
        [60, 55, 47,  1, 57, 63,  1, 46],
        [58, 57, 63, 60,  1, 47, 48, 48],
        [61,  1, 46, 47,  1, 61, 55, 43]])
----
when input is e the target: s
when input is es the target:  
when input is es  the target: n
when input is es n the target: u
when input is es nu the target: m
when input is es num the target: é
when input is es numé the target: r
when input is es numér the target: i
when input is o the target: r
when input is or the target: m
when input is orm the target: e
when input is orme the target:  
when input is orme  the target: o
when input is orme o the target: u
when input is orme ou the target:  
when input is orme ou  the target: d
when input is   the target: p
when input is  p the target: o
when input is  po the target: u
when input is  pou the target: r
when input is  pour the target:  
when input is  pour  the target: e
when input is  pour e the target: f
when input is  pour ef the target: f
when input is a the target: s
when input is as the target:  
when input is as  the target: d
when input is as d the target: e
when input is as de the target:  
when input is as de  the target: s
when input is as de s the target: m
when input is as de sm the target: a

Ce code travaille au niveau des caractères. Concrètement ensuite on crée de manière inductive des tokens plus long. Mais ces tokens sont induits: le vocabulaire est reconstruit de manière probabiliste.

import tiktoken
enc = tiktoken.get_encoding('gpt2')
enc.encode('bonjour')
[4189, 73, 454]
enc.decode([4189])
'bon'

Inputs et outputs#

Les LLM sont des modèles sequence2sequence qui ont comme input une série de mots et comme output une distribution de probabilités du mot qui suit.

Par exemple:

  • input: “mon animal de compagnie est un”

  • output 70% “chien”, 20% “chat”, 10% “tortue”

Cela peut-être fait aussi avec des mots caché dans la phrase (BERT). Par exemple.

  • input “J’aime les . J’en ai un comme animal de compagnie”

  • output 70% “chien”, 20% “chat”, 10% “tortue”

Grâce à l’apprentissage, les LLM construisent une représentation des differents tokens qui représente, d’une certaine manière, leur sens.

Attention!!! Il y a une différence entre la manière d’entraîner le modèle et la tâche qu’on lui confie après. On peut entraîner un modèle pour lui faire deviner le mot qui suit et ensuite l’utiliser pour faire des traductions.

Étapes#

  1. Encoding

  2. Embedding

  3. Position

  4. Self-attention

  5. Feed-forward

  6. Vecteur final

Les transformeurs#

L’objectif est de rajouter de l’information contextuelle à la vectorisation de la langue.

Je peux avoir le vecteur du mot “président”, mais de quelle manière je vais interpréter l’expression “président français” ? Comment dans le cadre d’une série de mots je peux prendre en compte le contexte pour changer le vecteur de chaque mot?

Il faut rajouter à chaque vecteur des informations par rapport aux autres mots avec lesquels il est lié.

Self attention#

Cela se fait avec la notion d’attention. C’est comme si chaque mot allait chercher d’autres mots qui ont une relation forte avec lui.

Cela est formalisé comme suit:

\[Attention(Q,K,V) = softmax(\frac{QK^T}{\sqrt{d_k}})V\]

Trois notions ici:

  • Q: query. C’est un vecteur qui représente une question: On pourrait l’imaginer comme une question: “quels sont les mots qui sont lié à moi?” Concrètement ce vecteur est un vecteur qui doit être proche du vecteur clé des mots qui sont liés.

  • K: key. C’est la réponse à la question.

  • V : valeur. C’est le vecteur qui, ajouté au vecteur du mot qui fait la query permet de changer son sens pour que le sens du mot lié soit “ajouté” au mot de départ.

Cela signifie que l’attention est la distribution de probabilités de la matrices des produits entre les queries et les clés (le plus haut est le produit, le plus les mots sont liés) divisé par la racine carrée de la dimension dans l’espace de la query, multiplié par la valeur (à savoir le vecteur qui permet, multiplié par le vecteur du mot, de le “transporter” vers le nouveau sens que le mot auquel il est lié lui donne).

Faisons un exemple. Admettons de devoir prédire le mot final de cette phrase:

La tour Eiffel est à Paris. C’est une tour en .

La query est le vecteur qui permet, quand il est multiplié par le vecteur du dernier mot “tour” de construire un vecteur proche du vecteur “clé” de “Eiffel” (qui est avant dans le contexte. Cela signifie que le produit de la query de tour et de la clé de Eiffel est élévé. On prend ce produit et, pour le normaliser, on le divise par la racine carrée de la dimensionalité de la query et ensuite on le transforme en probabilité. Puisque query et clé sont proches, leur produit sera élévé et donc la softmax nous donnera une probabilité élévée, disons proche de 1. Cela nous permet de multiplier V*1 et ensuite d’ajouter V au vecteur(tour) et donc de changer le sens du mot tour vers tour Eiffel. Cela nous permettra donc de prévoir “métal” qui ne serait pas prévisible si nous avions juste le mot “tour”.

Voilà pourquoi on parle de "transformeurs" : c'est une stratégie pour transformer le vecteur des mots pour qu'ils tiennent en compte le contexte.

Générer du texte#

On prend un prompt:

\[(t_1...t_k)\]

et on prévoit le token

\[t_{k+1}\]

ensuite on supprime le token \(t_1\) et on prend comme prompt

\[t_2...t_{k+1}\]

pour prévoir \(t_{k+2}\)

Et ainsi de suite.

Il y a une fenêtre de contexte qui reste fixe!

Le comportement est stochastique et non détérministe, car le token prévu est choisi de manière aléatoire sur la base de la distribution de probabilités.

Par exemple, admettons que :

\[(t_1...t_k)\]

Soit égal à Mon animal de compagnie est un - où mon= \(t_1\) et un = \(t_k\) et que le modèle pait comme prévision chien 80%; chat 10% et tortue 10%: le modèle pourra répondre le 80% du temps chien, le 10% tortue et le 10% chat.

Évidemment la distibution de probabilités concernera un nombre beaucoup plus élévé de mots – potentiellement tous les mots du vocabulaire. Même les termes qui n’ont aucune probabilité - sur la base du corpus - de ce trouver à cette place auront une probabilité >0 - à cause des stratégies de smoothing.

Un LLM : Camembert#

https://camembert-model.fr/

https://colab.research.google.com/drive/1W0Fj7aXm2qPx34PbEo0F5s5_sm8vVrJl?usp=sharing#scrollTo=dLyTSHXb92YR