Markov Text Generator, ou générer des phrases aléatoires

Valentin PORTAIL - Prép'ISIMA 1 - 2021/2022

logo_uca.png logo_isima.png

Lien vers la page web

markov.jpg

Image montant des lettres en bois formant le mot "keywords" (mots-clés)

Table des matières

1 Enoncé du problème

L'objectif de cet exercice est de créer un générateur de texte aléatoire dans le but de faire parler des personnages de jeu-vidéo, même si les phrases n'ont aucun sens.

Pour cela, nous disposons :

  • d'une phrase complète notée t ;
  • d'une profondeur de n-gramme (les différents segments de notre texte) notée d ;
  • de la longueur en mots de la phrase finale notée l ;
  • d'un texte de départ noté s à partir duquel nous devrons constituer la phrase finale.

Le programme devra renvoyer le texte généré aléatoirement.

Pour cela, il faudra parcourir le texte pour créer une chaîne de Markov où à chaque n-gramme seront associés les mots qui le suivent directement dans le texte. Ensuite, le texte final sera généré de manière pseudo-aléatoire, à l'aide d'une fonction qui nous est donnée.

L'énoncé de l'exercice sur CodinGame nous invite à faire des recherches sur les chaînes de Markov et les n-grammes afin de mieux comprendre ce qu'on nous demande de faire.

2 Solution proposée

2.1 Séparer les mots de la phrase complète

Afin de faciliter la manipulation des mots de la phrase t, on les sépare et on les met dans un tableau que l'on appellera text.

Pour cela, on définit une fonction separe_mots qui prend en argument une chaîne de caractères txt, un tableau tab et un pointeur sur un entier nb_mots et qui copie les mots de txt séparés par un espace dans tab.

void separe_mots (char * txt, char tab[][101], int * nb_mots) {
    int i;
    char mot[101]; // Mot que l'on étudie

    for (i = 0; i <= strlen(txt); ++i) { // On parcourt le texte
        if (txt[i] == ' ' || txt[i] == '\0') { // Si on tombe sur un espace ou sur la fin du texte
            strcat(mot, "\0"); // On indique la fin du mot
            strcpy(tab[*nb_mots], mot); // On insère le mot dans le tableau
            sprintf(mot, ""); // On réinitialise mot
            *nb_mots += 1; // Permet de modifier la variable en dehors de la fonction
        } else {
            sprintf(mot, "%s%c", mot, txt[i]); // On ajoute le caractère au mot
        }
    }
}

On utilise cette fonction dans le programme principal avec t et text.

char text[101][101]; // Contient les mots de la phrase complète
int nb_mots = 0; // Nombre de mots dans le tableau

separe_mots(t, text, &nb_mots);

2.2 Créer la chaîne de Markov

Pour nous aider à manipuler la chaîne de Markov, on crée une structure appelée markov qui contient un élément de la chaîne de Markov, c'est-à-dire, un n-gramme ngram et un tableau contenant les valeurs associées chain.

typedef struct markov {
    char ngram[101]; // Le n-gramme (texte) qu'on étudie
    int  number_chains; // Le nombre de mots trouvés
    char chain[101][101]; // Un tableau contenant les mots trouvés
} markov;

Dans le programme principal, on crée une chaîne de Markov chaines qui contiendra les n-grammes et les mots qui leur sont associés, un entier nb_chaines représentant le nombre d'éléments dans la chaîne de Markov et une chaîne de caractères n_gramme représentant un n-gramme.

On parcourt le tableau text : à chaque étape, on recompose le n-gramme à partir du tableau text dans la chaîne de caractères n_gramme, puis on compare avec les n-grammes présents dans la chaîne de Markov.

  • Si l'une d'entre elles est identique à n_gramme, on ajoute le mot situé juste après le n-gramme au tableau chaines[j].chain.
  • Sinon, on crée un nouvel élément dans lequel on insère n-gramme et on ajoute le mot situé juste après le n-gramme au tableau chaines[j].chain.
markov chaines[101]; // Chaîne de Markov contenant les n-grammes
                     //  et les mots qui leur sont associés
int nb_chaines = 0; // Nombre de n_grammes
char n_gramme[101]; // N-gramme que l'on étudit

int i; // N-gramme que l'on étudit
int j; // Un des mots associés au n-gramme étudié

for (i = 0; i <= nb_mots-d-1; ++i) {
    sprintf(n_gramme, ""); // On initialise le texte du n-gramme

    // On recompose le n-gramme à partir des données du tableau contenant la phrase complète
    for (j = 0; j < d-1; ++j) {
        strcat(n_gramme, text[i+j]);
        strcat(n_gramme, " ");
    }
    strcat(n_gramme, text[i+d-1]);
    strcat(n_gramme, "\0");

    j = 0;
    while (strcmp(n_gramme, chaines[j].ngram) && (j < nb_chaines)) {
    // On parcourt le tableau tant que l'on a pas trouvé le bon n-gramme
        ++j;
    }

    if (j == nb_chaines) { // Si on n'a pas trouvé un n-gramme correspondant
        strcpy(chaines[j].ngram, n_gramme); // On crée un nouveau n-gramme...
        strcpy(chaines[j].chain[chaines[j].number_chains], text[i+d]); // ... et on y associe le mot trouvé.
        chaines[j].number_chains = 1;
        nb_chaines += 1;
    } else { // Si on a trouvé un n-gramme correspondant
        strcat(chaines[j].chain[chaines[j].number_chains], text[i+d]); // On associe au n-gramme le mot trouvé.
        chaines[j].number_chains += 1;
    }
}

2.3 Choisir les mots aléatoirement

On commence par définir la fonction pick_option_index à l'aide du pseudo-code fourni dans l'énoncé CodinGame.

Elle prend en argument un entier nb_options représentant le nombre de choix possibles, modifie une variable globale random_seed et renvoie le reste de la division euclidienne entre random_seed et nb_options.

Cette fonction a pour but de renvoyer des résultats "pseudo-aléatoires" afin de faciliter les tests de CodinGame.

int random_seed = 0; // Variable globale nécessaire pour le bon fonctionnement de la fonction

int pick_option_index(int nb_options) {
    random_seed += 7; // Valeur arbitraire pour créer du pseudo-aléatoire
    return random_seed % nb_options;
}

Dans le programme principal, afin de faciliter la manipulation des mots du texte s, on les sépare et on les met dans un tableau que l'on appellera result.

On utilise la fonction separe_mots définie précedemment avec s et result.

char result[101][101]; // Tableau contenant le phrase finale
int nb_mots_result = 0; // Nombre de mots dans la phrase finale
int mk; // Element de la chaîne de Markov que l'on étudit
int ind_aleatoire; // Indice aléatoire du mot choisi
char n_gramme_result[101]; // N-gramme du résultat que l'on étudit

separe_mots(s, result, &nb_mots_result);

Ensuite, tant que la longueur de notre résultat est inférieure à la longueur de la phrase voulue, on recompose un n-gramme à partir des données du tableau text, on recherche ce n-gramme dans la chaîne de markov chaines, on choisit un mot au "hasard" et on l'ajoute au résultat.

while (nb_mots_result < l) { // Tant que l'on n'a a pas la bonne longueur
    sprintf(n_gramme_result, ""); // On initialise le texte du n-gramme

    // On recompose le n-gramme à partir des données du tableau contenant la phrase complète
    for (i = nb_mots_result-d; i < nb_mots_result; ++i){
        strcat(n_gramme_result, result[i]);
        if (i < nb_mots_result-1) {
            strcat(n_gramme_result, " ");
        }
    }

    mk = 0;
    while (strcmp(n_gramme_result, chaines[mk].ngram)) { // On parcourt le tableau tant que l'on a pas trouvé le bon n-gramme
        ++mk;
    }

    ind_aleatoire = pick_option_index(chaines[mk].number_chains); // On choisit un indice aléatoire
    strcpy(result[nb_mots_result], chaines[mk].chain[ind_aleatoire]); // On ajoute le mot trouvé dans la phrase finale
    nb_mots_result += 1;
}

2.4 Ecrire la phrase

On recompose la phrase finale à partir du tableau resultat, puis on l'affiche.

sprintf(s, ""); // On initialise la phrase finale

// On recompose la phrase finale à partir des données du tableau
for (i = 0; i < nb_mots_result; ++i){
    strcat(s, result[i]);
    if (i < nb_mots_result-1) {
        strcat(s, " ");
    }
}

printf("%s\n", s);

2.5 Plan du programme final

#include <stdlib.h>
#include <stdio.h>
#include <string.h>

<<struct_markov>>

<<func_aleatoire>>
<<func_separe_mots>>

int main() {
    char t[1001]; // Phrase complète
    scanf("%[^\n]", t);
    int d; // Taille du n-gramme
    scanf("%d", &d);
    int l; // Longueur de la phrase finale
    scanf("%d", &l); fgetc(stdin);
    char s[1001]; // Texte de départ
    scanf("%[^\n]", s);

    <<separe_mots_phrase_complete>>

    <<creer_markov>>

    <<separe_mots_resultat>>
    <<choisir_mots_aleatoires>>

    <<ecrire_phrase>>

    return 0;
}

3 Tests

3.1 Test 1

Entrée :

one fish is good and no fish is bad and that is it 2 4 fish is

Sortie attendue :

fish is bad and

Test réussi

3.2 Test 2

Entrée :

one fish is good and no fish is bad and that is it 1 4 one

Sortie attendue :

one fish is good

Test réussi

3.3 Test 3

Entrée :

stop there once was a girl named dorothy stop dorothy had a dog named toto stop dorothy lived with her aunt and uncle with her dog named toto stop she was a girl of who dreamed of traveling stop 2 10 dorothy was a

Sortie attendue :

dorothy was a girl named dorothy stop dorothy had a

Test réussi

3.4 Test 4

Entrée :

stop there once was a girl named dorothy stop dorothy had a dog named toto stop dorothy lived with her aunt and uncle with her dog named toto stop she was a girl of who dreamed of traveling stop 3 10 stop dorothy had

Sortie attendue :

stop dorothy had a dog named toto stop dorothy lived

Test réussi

4 Description d'une autre solution

Cette solution a été proposée sur CodinGame par l'utilisateur "Vane26" :

#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <stdbool.h>

int choisir_option(int num,int nd_option){
    return num%nd_option;
}

int nombre_mot_chaine(char chaine[]){
    int i;
    int nb=1;
    for (i=0;i<strlen(chaine);i++){
        if (chaine[i]==' '){
            nb++;
        }
    }
    return nb;
}

int main(){
    char t[1001];
    scanf("%[^\n]", t);
    int d;
    scanf("%d", &d);
    int l;
    scanf("%d", &l); fgetc(stdin);
    char s[1001];
    scanf("%[^\n]", s);

    int i=1;
    int j=1;
    int nb_source=nombre_mot_chaine(s);
    int nb_phrase=nombre_mot_chaine(t);
    char *mot[500];
    mot[0]=strtok(t," ");    
    while(mot[i]!=NULL || i==1){        // On décompose en mot le texte
        mot[i]=strtok(NULL," ");
        i++;
    }

    char *n_gramm[nb_phrase][10]; // On définit le n_gramm
    for (i=0;i<(nb_phrase-d);i++){
        for(j=0;j<d+1;j++){
            n_gramm[i][j]=mot[j+i];
        }
    }   

    printf("%s ",s);
    int z=1;
    char *mot_cible[nb_source];
    mot_cible[0]=strtok(s," ");

    while(z!=nb_source && nb_source!=1){
        mot_cible[z]=strtok(NULL," ");
        z++;
    }
    int mot_phrase=nb_source;
    int deb_source=nb_source-d;
    int correlation=0;
    int random_seed=0;

    while(mot_phrase!=l){ //affichage des textes
        int affiche=0;
        int nb_option=0;
        int option[100]={0};
        int last_seed=0;
        for (i=0;i<(nb_phrase-d);i++){
            for(j=0;j<d;j++){
                if(*n_gramm[i][j]==*mot_cible[j+deb_source]){                    
                    correlation++;
                }
            }             
            if (correlation==d){
                option[nb_option]=i;
                last_seed=i+1;
                nb_option++;               
            }
            correlation=0;   
        }
        mot_phrase++;
        random_seed+=7;
        affiche=option[choisir_option(random_seed,nb_option)]; 

        if (mot_phrase==l){
            printf("%s",n_gramm[affiche][d]);
        }
        else{
            printf("%s ",n_gramm[affiche][d]);
        }
        for(i=0;i<d;i++){
            mot_cible[i+deb_source]=n_gramm[affiche][i+1];
        }

    }
    return 0;
}

Cette solution a un fonctionnement similaire à la mienne. On commence par découper le texte en mots, puis on définit le n-gramme et on parcourt le texte. Cependant, celle-ci semble calculer la chaîne de Markov au fur et à mesure des n-grammes, au lieu de la calculer entièrement comme dans la mienne.

Ainsi, elle a plusieurs avantages :

  • Le code est plus compact (97 lignes contre 141 pour la mienne).
  • La fonction strtok de la bibliothèque <string.h> permet d'éviter de créer une fonction annexe pour séparer les mots d'un texte.
  • Elle est plus efficace : on évite de calculer des éléments inutiles car on calcule la chaîne de Markov au fur et à mesure.

Cependant, je trouve que le code manque de commentaires permettant d'expliquer son fonctionnement.

5 Conclusion

Pour conclure, j'ai trouvé cet exercice beaucoup plus difficile que les précédents. En effet, la gestion des tableaux et des chaînes de caractères est assez rudimentaire en C et il faut souvent faire preuve d'astuce pour effectuer des opérations "complexes" comme séparer les éléments d'une chaîne de caractères selon un caractère précis. Selon moi, cet exercice aurait été plus simple dans un langage comme Python où le type des variables est plus "souple" et où de nombreuses fonctions de traitement des listes sont intégrées.

Au delà de ces difficultés, j'ai pu apprendre à mieux gérer les chaînes de caractères en C et découvrir des fonctions comme strtok qui n'ont pas été abordées en cours.

Date: 15/03/2022

Auteur: Valentin PORTAIL

Created: 2022-03-14 lun. 14:04

Validate