Connexion élèves

Choisir le(s) module(s) à installer :

Projet Générateur automatique de texte

Magique, ChatGPT ? A partir d’un cours texte de départ ( le « prompt » ), « il » est capable de créer un discours très structuré et cohérent, et ce sur n’importe quel sujet !

Aucune magie, évidemment, contrairement à ce dont beaucoup de gens ( dont vous ne faites pas partie bien entendu ) sont persuadés…

ChatGPT est un agent conversationnel destiné à de la génération automatique de texte ( il existe également des générateurs d’images, comme Dall-E ).
Il est basé sur GPT, un ( il en existe d’autres ) grand modèle de langage ( en anglais : LLM = large Model Language ), c’est à dire une modélisation mathématique très poussée du langage humain, modélisation issue de l’analyse d’un très grand corpus ( = ensemble ) de données, qui lui a permis de déterminer les « règles » que suit le langage humain pour élaborer des phrases, des textes, etc.…

Pour faire simple, ChatGPT, comme la majorité des modèles de langage, ne fait qu’une seule chose ( mais il la fait bien ) : déterminer quel sera le mot le plus probable qui suivra un autre dans un texte donné.

Comment une machine arrive-t-elle à faire cela ?

ChatGPT NSI

Génération automatique de texte

Le problème

Il y a une centaine d’années, le mouvement artistique dit des Surréalistes a inventé un « jeu » consistant à plusieurs personnes à écrire une phrase, la première personne proposant un mot, puis la suivante un verbe, la suivante un adjectif, etc., et ce sans que chaque personne ne connaisse ce que les autres ont proposé au préalable.
Ce jeu a été par la suite appelé le Cadavre Exquis, car la première phrase qui avait été « générée » selon ce principe était  : "Le cadavre - exquis - boira - le vin - nouveau."

Bien que du fait des règles du jeu, les phrases générées selon ce principe sont grammaticalement correctes, elles n’ont que très peu de sens au final...
La raison en est que chaque participant ne connaît pas le contexte dans lequel le mot qu’il doit proposer doit être, c’est à dire l’ensemble des mots qui vont précéder le sien dans la phrase finale ; or, c’est ce contexte qui nous permet de saisir le sens d’une phrase.

Par exemple, si on commence à écrire les mots :

Je suis allé bronzer au ...

...on conçoit tout de suite que la suite sera probablement :

Je suis allé bronzer au soleil.

Écrire une phrase cohérente et sensée suppose donc, en plus de respecter les règles de grammaire, de connaître le contexte dans lequel la phrase se tient, et c’est tout le problème pour une machine de s'approprier ces concepts fondamentalement humains !

Analyse de contexte

ChatGPT est basé sur des outils mathématiques très pointus pour analyser le contexte d’un texte, et il dispose d’une « fenêtre de contexte », c’est à dire le nombre de « mots » ( ce sont en fait des suites de caractères de longueur variable appelés tokens en anglais ) sur lequel son analyse de contexte se fait, de plusieurs milliers de "mots" !
Bien entendu, il y a un code très technique ( utilisation de réseaux de neurones et de concepts informatiques très avancés ) à écrire, ainsi qu’un « entraînement » très lourd ( et très énergivore ! ) du modèle sur un très large corpus avant qu'il soit opérationnel.

Or, il existe des modèles beaucoup plus simples ( mais également moins performants..), dit modèles de langages statistiques, qui sont basés sur la constatation que, pour bâtir une phrase sensée, il suffit de ne regarder que les quelques mots précédents du contexte ( 1, 2 ou trois, 5 au maximum ) pour déterminer les mots qui vont suivre alors.

Dans l’exemple précédent, le début de la phrase n’est pas indispensable pour saisir le contexte, il suffit en effet d’écrire :

"bronzer au ..."

...pour comprendre que la suite de la phrase sera :

"bronzer au soleil."

Cette constatation se généralise à une suite donnée de n-gramme; un n-gramme peut être un ensemble de n mots successifs dans un texte ( comme ci-dessus ), ou un ensemble de n caractères, comme dans l'approche que nous allons adopter ici.

Par exemple, pour la suite du n-gramme de 3 caractères ( = trigramme ) :

'bro'

... une personne proposera probablement :

'broche', 'bronche', 'broncher', 'bronzer', etc...

Dans un contexte de texte relatif aux vacances, le mot "candidat" serait alors probablement "bronzer"; mais comment faire comprendre ça à une machine ? Il va falloir lui apprendre !

Une méthode (simple) pour apprendre à une machine à écrire...

Cette activité porte sur une méthode destinée à faire de la génération automatique de texte, en apprenant à une machine à prédire le n-gramme de caractères le plus susceptible d'en suivre un autre dans le texte à générer.

On considérera des n-grammes formés de n caractères contigus ( plus simple à coder qu'avec des n-grammes de mots ).

Principe

Prenons pour exemple le texte suivant :

Texte ngramme

Pour n = 2, les 2-grammes ( = digrammes ) de cette chaîne sont ( les espaces font partie des n-grammes ) :

Texte digrammes

Parmi tous les "contextes" possibles dans le texte, intéressons-nous à celui du digramme mu ( sans tenir compte de l'accentuation ) :

Texte contexte 'mu'

On constate que l'on trouve :

→ le contexte mu apparaît donc 4 fois dans le corpus; on peut alors déduire la fréquence avec laquelle chacun des digramme suivant apparaît ( la somme des fréquences est égale à 1 ) :

Conséquence : on "apprend" alors à la machine que, dans un texte quelconque, le digramme le plus probable qui suivra le digramme mu sera celui dont la fréquence d'apparition dans le corpus d'apprentissage est la plus grande, c'est à dire ici le digramme re.

Pour bâtir un texte complet, le générateur de texte, après la phase d’analyse du corpus, pourra ainsi choisir, n-gramme après n-gramme, ( donc à chaque changement de contexte ) un « candidat » probable à ajouter à la suite des mots d’une phrase ( le début du texte, le "prompt", étant fourni par l'utilisateur ).
On verra cependant que, pour que le texte paraisse vraiment original et différent à chaque exécution, il ne faut pas choisir forcément le candidat le plus probable, mais plutôt le « piocher » aléatoirement dans une liste de candidats possibles.

Le concept utilisé s'appelle une chaîne de Markov : c'est une suite d'éléments ( ici, des n-grammes ), dans laquelle la détermination d'un élément donné ne nécessite pas de considérer l'ensemble de la chaîne, mais uniquement l'élément actuel , en utilisant la probabilité de passer de cet élément au suivant.
Ce principe pourrait être de la même manière appliqué à une suite de notes de musique, dans le but de produire des morceaux aléatoires s'inspirant de morceaux connus !

C'est une méthode simple ( et donc limitée...), mais facile à coder, et surtout, qui ne nécessite par un énorme corpus d'apprentissage, du moins lorsque n reste petit.

Limitations du modèle

On s'en doute, ce modèle souffre de plusieurs limitations :

Implémentation du modèle de langage

Le travail sera fragmenté en différentes fonctions, que vous devrez compléter ou dont vous devrez écrire complètement le code.

Extraction des n-grammes d'un texte

Compléter le code de la fonction extraire_ngrammes ci-dessous, qui :

  • prend en paramètre une chaîne texte contenant un texte ( qui sera issu du corpus d'entraînement ), et un entier n codant la taille des n-grammes utilisé.
  • renvoie la liste des n-grammes consécutifs du texte.

Par exemple, avec texte = "aabbababaaabcaabca", et n = 2, la fonction doit renvoyer la liste :


['aa', 'bb', 'ab', 'ab', 'aa', 'ab', 'ca', 'ab', 'ca']	
			
def extraire_ngrammes(txt, n): """Extrait la liste des n-grammes d'un texte. Entrée : texte = le texte à analyser ( str ) n = la taille des n-grammes Sortie : liste des n-grammes du fichier """ ngrammes = [] i = 0 while ... : # tant qu'on n'a pas atteint la fin du texte - n caractères ngramme = '' for j in range(...): # extraction de n caractères ngramme += ... ngrammes.append(...) i += ... # "avance" de n caractères return ... texte = "aabbababaaabcaabca" n = 2

SOLUTION

Analyse du contexte de chaque ngramme

Le gros du travail : à partir de la liste des n-grammes, établir dans quel "contexte" chacun se trouve, c'est à dire, pour un n-gramme donné dans la liste ( le "suivant" ), déterminer celui qui le précède ( le "précédent" ), et combien de fois la suite (précédent, suivant) apparaît dans le corpus.

Ce comptage permettra par la suite de calculer les fréquences avec lesquelles un n-gramme donné en suit un autre.

Pour réaliser ces calculs, le code devra remplir deux dictionnaires :

Bien entendu, ces deux dictionnaires seront bien plus complets en réalité !

On donne ci-dessous l'algorithme à suivre pour remplir ces deux dictionnaires :


contexte ← dictionnaire vide
compteur ← dictionnaire vide

pour chaque n-gramme dans la liste:
	précédent ← n-gramme précédent ( ou n-gramme courant )
	suivant ← n-gramme courant ( ou n-gramme suivant )
	
	si précédent n'est pas encore dans contexte:
		on crée la clé précédent avec comme valeur la liste vide
	on ajoute suivant à la liste associée à la clé précédent
	
	si le tuple (précédent, suivant) n'est pas encore dans compteur:
		on y créé une clé (précédent, suivant) avec comme valeur 0
	on incrémente la valeur associée à la clé (précédent, suivant)
	
renvoi de contexte et de compteur  	
		"

Écrire une fonction analyse_contexte qui :

  • prend comme paramètre la liste des n-grammes du corpus
  • renvoi les deux dictionnaires contexte et compteur une fois remplis.
def analyse_contexte(ngrammes): """Renvoie les dictionnaires de contexte. Entrée : nrammes = la liste des ngrammes extraits du corpus d'entraînement (list) Sortie : contexte = dictionnaire des listes des ngrammes suivants chaque ngramme du corpus (dict) compteur = dictionnaire indiquant combien de fois chaque ngramme de contexte apparaît dans le corpus """ pass

SOLUTION

Calcul des fréquences d'apparition

Le but est maintenant, à partir des dictionnaires contexte et compteur, de calculer la fréquence d'apparition de chaque suite de deux n-grammes.

Cette fréquence est calculée à partir de la formule :

f(précédent, suivant) =
Nbre d'apparitions de la suite (précédent, suivant)Nbre d'apparition de précédent
  1. réfléchir à comment, à partir des données contenues dans les dictionnaires contexte et compteur, calculer la fréquence d'apparition d'un n-gramme donné après un autre dans le corpus.
  2. écrire alors une fonction calcul_frequences, qui :
    • prend comme paramètres les dictionnaires contexte et compteur
    • renvoie un dictionnaire frequences dont :
      • les clés sont les différents n-grammes du corpus,
      • les valeurs sont les listes des différents n-grammes qui peuvent suivre la clé avec leur fréquence respective, sous la forme d'un tuple (suivant, fréquence).
      Pour notre exemple, le dictionnaire renvoyé serait :
      
      frequences = {
      	'aa': [('bb', 0.5), ('ab', 0.5)], 
      	'bb': [('ab', 1.0)],
      	'ab': [('ab', 0.25), ('aa', 0.25), ('ca', 0.5)], 
      	'ca': [('ab', 1.0)]
      }
      							
def calcul_frequences(contexte, compteur): """Fonction de calcul de la fréquence d'apparition de chaque suite de deux n-grammes possibles. Entrées : contexte et compteur = les deux dictionnaires Sortie : un dictionnaire fréquence dont les clés sont les n-grammes, et les valeurs, les listes des tuples (n-gramme suivant, fréquence). """ pass

SOLUTION

Choix d'un n-gramme "candidat" dans un contexte donné

On a donc maintenant un moyen de connaître les n-grammes candidats possibles à la suite d'un autre n-gramme donné.

Il reste maintenant à choisir dans cette liste un candidat probable à chaque fois que nécessaire; le problème est, comme on l'a dit, que si on choisit toujours le plus fréquent, l'algorithme de génération de texte entrera rapidement dans une "boucle" : le texte généré deviendra répétitif et proposera en boucle les mêmes phrases...

Il faut donc choisir un candidat probable de manière "semi-aléatoire", c'est à dire tenir compte de la probabilité d'apparition des n-grammes, mais avec une marge de hasard...

Voila l'approche que l'on va suivre :

Écrire une fonction choisir_mot qui, dans une liste de candidats, choisit un n-gramme selon le principe énoncé ci-dessus.

Pour le tri de la liste, on appliquera un des algorithmes vus en Première, comme par exemple le tri sélection.

Tester la fonction ( par exemple avec la liste des candidats suivant du n-gramme 'ab' ), et vérifier que le plus fréquent "sort" bien le plus souvent, mais pas toujours...

from random import random def choisir_mot(candidats): """Renvoie un candidat probable dans une liste de candidats possibles. Entrée : la liste des candidats sous forme (n-gramme, fréquence) Sortie : le n-gramme sélectionné """ p = random() # tire aléatoirement un nombre entre 0 et 1.0 # tri de la liste des tokens suivants par ordre de proba décroissante pass # Choix du n-gramme pass

SOLUTION

Génération de texte

Tout est en place pour écrire la fonction principale du programme; elle devra :

Écrire une fonction genere_texte, qui :

  • prend comme paramètres une chaîne de caractères ( le "prompt ), un entier n indiquant la taille des n-grammes, et un entier N indiquant le nombre de mots à générer
  • renvoie une chaîne de caractères représentant le texte généré

Vous pouvez utiliser ce fichier texte comme corpus d'apprentissage pour le modèle de langage ( qui sera donc capable d'écrire comme Victor Hugo 😲)

Appeler ensuite la fonction, en lui donnant divers valeurs pour n ( de 3 à 10 environ ).

Il était une fois observée. La chute de tout, autour 
de lui. Une troisième renversa en arrière Blanche au 
chemin d'Aubervilliers, en quatre mois d'agonie. 
Beaucoup de choses s'y étaient fermée par une grille 
dont il est vrai, livide, mais précise dans sa boîte 
 fleurs. Tout autour de Jean Valjean reconnut Javert.
 --Voilà, grommela-t-il, de quoi fouiller la terr						
			
def genere_texte(prompt, n, N): """Génère un texte aléatoire à partir d'un corpus contenu dans un fichie texte. Entrée : prompt = le prompt initial (str), n = la taille des n-grammes (int), N = le nombre de mots à générer Sortie : le texte généré (str) """ with open("les_miserables.txt", 'r') as f: corpus = f.read() # suppression des retours à la ligne corpus = corpus.replace('\n', ' ') pass

SOLUTION

Conclusion et améliorations

Ok, ok, ce n'est pas ChatGPT...quelques pistes d'amélioration possibles :

>