L'usage de la calculatrice ou de tout autre dispositif électronique est interdit.
Cette épreuve est commune aux candidats des filières MP, PC et PSI.
Les candidats sont priés de mentionner de façon apparente sur la première page de la copie :
INFORMATIQUE COMMUNE
L'énoncé de cette épreuve comporte 12 pages de texte.
Si, au cours de l'épreuve, un candidat repère ce qui lui semble être une erreur d'énoncé, il le signale sur sa copie et poursuit sa composition en expliquant les raisons des initiatives qu'il est amené à prendre.
Les sujets sont la propriété du GIP CCMP. Ils sont publiés sous les termes de la licence
Creative Commons Attribution - Pas d'Utilisation Commerciale - Pas de Modification 3.0 France.
Tout autre usage est soumis à une autorisation préalable du Concours commun Mines Ponts.
Rappels concernant le langage Python : il n'est pas possible d'utiliser des fonctions internes à Python sur les listes ou les chaînes de caractères telles que min, max, count, remove ... Seules les instructions basiques telles que len (liste), liste. append(e) sont autorisées. Le tranchage ou slicing ainsi que la concaténation sont également permis. Les programmes doivent être commentés lorsque c'est nécessaire pour justifier les choix. Il n'est pas utile de rappeler la signature des fonctions demandées dans les différentes questions.
Introduction à deux problèmes en communication numérique
Introduction
On s'intéresse au problème de communication entre deux personnes, nommées Alice et Bob qui cherchent à s'envoyer un message au travers d'un canal de communication (une bande de fréquences radio par exemple).
Avant d'être lu par Bob, le message original d'Alice passe par plusieurs étapes que nous allons séparer de la manière suivante :
une phase de compression, durant laquelle Alice cherche à trouver la représentation la plus compacte possible du message,
une phase d'encodage durant laquelle le message compressé est transformé en une succession de symboles transmissibles au travers du canal de communication utilisé,
une phase de transmission durant laquelle le message encodé circule sur le canal de communication et est susceptible de subir une altération,
une phase de décodage durant laquelle Bob décode le message qu'il a reçu, le message lui apparaît alors sous la forme compressée,
une phase de décompression durant laquelle Bob applique l'opération réciproque de la compression opérée par Alice.
Ce modèle est décrit par le schéma de la Figure 1 :
Figure 1 - Schématisation du modèle de communication considéré ici.
Dans cette épreuve, nous allons nous intéresser uniquement à deux phases : la compression du message d'origine par Alice en un message compact et le décodage par Bob d'un message transmis, potentiellement entaché d'erreurs.
1 Compression du message d'Alice : codage arithmétique
Le codage ASCII (American Standard Code for Information Interchange) définit une norme où 128 caractères sont codés sur 7 bits. Ce codage est illustré par les quelques lignes suivantes :
Caractère
Codage binaire
Équivalent numérique
'a'
1100001
97
'b'
1100010
98
' z '
1111010
122
Lorsqu'une chaîne de caractères n'utilise pas l'intégralité des 128 caractères proposés par le codage ASCII, il est possible de convenir d'une représentation différente, plus économique en nombre de symboles. Considérons la chaîne de caractères 'abaabaca'. On peut proposer de coder celle-ci à l'aide du tableau suivant :
Caractère
Code
'a'
00
'b'
01
'c'
10
Dans ce cas, la chaîne de caractères s est codée sur 16 bits par :
(où des espaces ont été introduits pour faciliter la lecture).
Dans un souci de compression de l'information, il est intéressant de représenter les caractères les plus fréquents par des expressions courtes et de ne plus nécessairement coder avec des codes de longueur constante chaque caractère. Dans l'exemple précédent, il est possible de coder le caractère ' ' avec 1 bit, et les caractères ' ' et ' ' avec 2 bits afin de coder la chaîne sur seulement 11 bits en tout.
Q1 - Proposer une telle représentation en expliquant pourquoi celle-ci pourra être décodée sans ambiguïté. Vous ferez en sorte que la représentation binaire de 'a' soit inférieure à celle de ' ', elle-même inférieure à celle de ' '.
La représentation précédente emploie la même longueur pour coder les caractères ' b ' et ' c ' alors que le caractère ' b ' est deux fois plus présent que le caractère ' c ' dans la chaîne s .
Il est possible d'aller un cran plus loin et le codage arithmétique présenté dans cette étude permet un gain de compression comme s'il parvenait à représenter un caractère avec un nombre non entier de bits au prorata de sa fréquence d'apparition.
Ce principe de compression est notamment utilisé par la norme JPEG2000 de compression des images. Nous ne le présenterons cependant ici que dans le cadre de l'étude de chaînes de caractères.
1.1 Analyse du texte source
L'objet de cette partie est d'analyser le contenu d'une chaîne de caractères afin de déterminer :
les caractères utilisés par la chaîne ;
le nombre d'occurrences de chacun.
Q2 - Écrire une fonction nommée nbCaracteres(c:str,s:str)->int qui prend comme argument un caractère , une chaîne et qui renvoie le nombre d'occurrences (c'est-à-dire le nombre d'apparitions) de dans . La fonction doit avoir une complexité linéaire en n , la longueur de la chaîne s .
Q3 - Pour déterminer la liste des caractères utilisés à l'intérieur d'une chaîne s on utilise la fonction définie ci-dessous :
def listeCaracteres(s:str):
listeCar = []
n = len(s)
for i in range(n):
c = s[i]
if not(c in listeCar):
listeCar.append(c)
return listeCar
Que renvoie cette fonction lorsque 'abaabaca' ? Expliquer succinctement le principe de fonctionnement de cette fonction.
Q4 - En fonction de la longueur n de la chaîne et du nombre k de caractères distincts dans celle-ci, déterminer la complexité asymptotique dans le pire des cas de la fonction de la question Q3. Par exemple pour ' abaabaca' , on a et . On négligera la complexité des append mais pas celle des tests d'appartenance de la forme i in L . Autrement dit, la ligne 7 est considérée comme étant de complexité constante et la ligne 6 de complexité linéaire en la longueur de la liste listeCar.
Q5 - On définit alors une fonction analyseTexte(s:str)->list
def analyseTexte(s:str):
R = []
l = listeCaracteres(s)
for i in range(len(l)):
c = l[i]
R.append((c, nbCaracteres(c, s)))
return R
Expliquer ce que fait cette fonction et donner la valeur renvoyée par la commande analyseTexte('babaaaabca').
Q6 - En fonction de la longueur de et du nombre de caractères distincts présents
dans , (autrement dit est la longueur de listeCaracteres(s)), donner une estimation de la complexité asymptotique dans le pire des cas de la fonction analyseTexte. Q7 - Adapter la fonction de la question Q5 pour qu'elle utilise (et renvoie) un dictionnaire. Elle devra avoir une complexité,
i. linéaire en la longueur de ,
ii. indépendante de nombre de caractères distincts présents dans .
De plus, cette fonction devra impérativement ne parcourir qu'une seule fois la chaîne de caractères. On admettra qu'un test d'appartenance d'une clé à un dictionnaire se fait à coût constant. Par exemple, analyseTexte('abracadabra') renverra {'a':5, 'b':2, 'r':2, 'c':1, 'd':1}.
1.2 Exploitation d'analyses existantes
On peut éviter de réaliser une analyse de la chaîne de caractères à coder en exploitant des données statistiques disponibles. La numérisation de larges corpus littéraires a permis la création de bases de données contenant des informations relatives au nombre d'occurrences des différents caractères alphanumériques dans diverses langues. Nous allons supposer disposer d'une base de données constituée de trois tables (les clefs primaires sont soulignées) :
occurrences(idCar, idLivre, nombreOccurrences);
dont voici quelques extraits :
Table caractere :
idCar
symbole
typeCaractere
codeHTML
65
'A'
'lettre'
'A '
48
'0'
'nombre'
'0'
Table corpus :
idLivre
titre
auteur
annee
nombreCaracteres
langue
1
'Germinal'
'Zola'
1885
1152365
'Français'
2
'Les Misérables'
'Hugo'
1862
2245300
'Français'
Table occurrences :
idCar
idLivre
nombreOccurrences
62
31
155
37
21
1550
Q8 - Écrire en langage SQL une requête permettant d'obtenir la liste sans doublon des auteurs présents dans la table corpus.
Q9 - Écrire une requête SQL permettant d'obtenir la fréquence d'occurrences (donc comprise entre 0 et 1) de chaque caractère en 'Français'. Plus précisément, la requête
devra renvoyer une table à deux attributs : une colonne contenant le symbole de chaque caractère, et une autre colonne contenant le rapport entre le nombre total d'occurrences du caractère et le nombre total de caractères des textes de tout le corpus.
1.3 Compression
La compression par codage arithmétique consiste à représenter une chaîne de caractères s par un nombre réel déterminé à l'intérieur de l'intervalle .
Initialement, on attribue à chaque caractère utile une portion de l'intervalle proportionnelle à sa fréquence d'occurrences. Par exemple, pour un alphabet à 5 lettres 'abcde', on pourrait avoir un tableau comme ci-dessous :
Caractère
Fréquence
0.2
0.1
0.2
0.4
0.1
Intervalle
La chaîne de caractères s est codée en partant de l'intervalle . À chaque caractère successif de celle-ci, on affine cet intervalle en ne considérant que la portion correspondant au caractère lu.
Si par exemple la chaîne à coder est 'dac' :
on obtient d'abord l'intervalle [ [ correspondant au caractère ' d ' ;
le caractère ' ' détermine alors le sous-intervalle [ [ de [ [ correspondant à la portion associée au caractère ' '.
le caractère ' c ' détermine enfin l'intervalle [0.524; 0.540[.
La figure qui suit illustre ce processus.
Figure 2 - Encodage de 'dac'
Q10 - En considérant la table des fréquences précédente, proposer l'intervalle correspondant à la chaîne 'bac'.
On suppose disposer d'une fonction codeCar(car:str, float, float)->(float,float) qui prend en argument un caractère car et les deux extrémités d'un intervalle et qui renvoie un tuple composé des extrémités du sous-intervalle de [ déterminé par le caractère car. En reprenant l'illustration précédente produit ( ) et codeCar ('a', ) produit ( ). Q11 - Écrire une fonction codage(s:str) -> (float, float) prenant en argument la chaîne et fournissant en réponse le tuple ( ) constitué des deux extrémités de l'intervalle [ produit par l'algorithme de codage précédent.
Le codage arithmétique consiste alors à coder la chaîne par un flottant choisi arbitrairement à l'intérieur de l'intervalle .
1.4 Décodage
Pour effectuer le décodage d'un flottant x , il suffit de repérer dans quelle succession d'intervalles celui-ci se trouve.
À titre d'exemple, reprenons le tableau précédent et considérons le nombre
Caractère
Fréquence
0.2
0.1
0.2
0.4
0.1
Intervalle
Puisque x appartient à l'intervalle , le premier caractère est un ' a '. Puisque x appartient au sous-intervalle , le caractère suivant est un 'd', etc. Q12 - Déterminer le caractère qui suit 'ad' dans la chaîne codée par en spécifiant le sous-intervalle qui a permis de décoder ce caractère. Q13 - Dans le cadre de l'exemple de cette partie, indiquer deux chaînes qui peuvent correspondre au flottant 0.2. Expliquer par une phrase ce qui est à l'origine de cette ambiguïté.
Une solution possible pour résoudre le problème précédent consiste à introduire un caractère nouveau signifiant la fin de la chaîne de caractères.
Nous conviendrons de désigner ce caractère par '#'. Ce caractère se voit attribuer une plage non vide au voisinage de 0 . Dans la suite, on suppose que la table des fréquences est adaptée de sorte à prendre en compte la présence de ce nouveau caractère.
On suppose disposer, en plus de la fonction codeCar(car,g,d) précédente, d'une fonction decodeCar( x : float, g : float, d : float)->str qui détermine le caractère correspondant à la valeur x quand celle-ci est comprise dans la plage de . Par exemple decodeCar(0.123,0,1) donne 'a' tandis que decodeCar(0.123,0,0.2) donne le caractère 'd'.
Q14 - Écrire une fonction decodage(x: float)->str produisant la chaîne de caractères s déterminée par la valeur de x (avec le caractère ' ' compris).
Le codage précédent ne fonctionne que sous l'hypothèse illusoire d'exactitude des calculs jusqu'à une très grande précision. Cette précision est cependant limitée par la taille de la mantisse représentant un réel et il ne peut être mis en œuvre sous la forme précédente qu'avec des chaînes relativement courtes. Pour résoudre cette difficulté, on adopte un codage en arithmétique entière : au lieu de considérer un intervalle flottant , c'est un intervalle entier qui est utilisé. Cette technique ne sera pas abordée dans le cadre de cette épreuve de durée limitée.
2 Décodage du message reçu par Bob à l'aide de l'algorithme de Viterbi
2.1 Modélisation du canal de communication par un graphe
Dans cette partie, nous allons désormais considérer que le message compressé par Alice a été envoyé au travers d'un canal de communication. À cette fin, et indépendamment de la phase de compression étudiée dans la première partie, le message a subi une deuxième phase de transformation, dite d'encodage (cf Figure 1).
Le message envoyé sur le canal est une suite de symboles à valeurs dans un alphabet, noté , comportant symboles. Le choix d'un alphabet efficace n'est pas l'objet de notre étude et constitue un sujet à part entière.
Suite au passage dans le canal de communication, le message envoyé subit une altération de sorte que Bob reçoit une séquence de symboles de qui ne correspond pas nécessairement à celle qui a été émise.
Dans cette partie, nous allons voir une approche permettant à Bob de décoder le message reçu et de potentiellement corriger quelques erreurs liées à la transmission du message et à la connaissance a priori de la propension du canal de communication à altérer les symboles du message lors de la transmission.
La modélisation proposée est la suivante :
Bob observe une suite de symboles , que nous allons représenter par une liste Python ,
pour simplifier, on supposera que l'alphabet est un ensemble de entiers consécutifs commençant à 0 , de sorte que . Par exemple si et , un message valide reçu par Bob pourrait être .
chacun des symboles observés correspond à l'altération d'un symbole envoyé par Alice. On note le message original ; pour reprendre l'exemple précédent, Alice pourrait avoir envoyé .
on connaît, pour chaque paire , la probabilité que le canal altère le symbole en un symbole . On stocke ces probabilités dans une liste de listes E; autrement dit, est la probabilité conditionnelle d'observer le symbole sachant que le symbole a été émis.
Ici (par exemple) on pourrait considérer : , représentée par la liste de listes : .
Le fait que vaille 0.1 signifie donc que la probabilité que le symbole observé par Bob soit un 2 sachant qu'Alice a émis un 0 est de 0.1 .
on suppose également que le symbole courant envoyé par Alice a une incidence sur le symbole suivant qu'elle peut envoyer, au même titre que dans une langue comme le français, la probabilité d'observer un ' ' dans un mot, n'est pas la même suivant que le caractère précédent est un ' ' ou un ' '.
Ainsi pour chaque couple de symboles , on suppose que l'on connait la probabilité d'émettre le symbole à l'instant sachant que symbole a été émis à l'instant . On suppose également que cette probabilité ne dépend pas de .
L'information concernant ces probabilités de transition d'un symbole à l'autre peut se stocker dans une matrice de taille , que l'on représente informatiquement par une liste de listes P. Chaque entrée P [i] [j] donne la probabilité qu'Alice émette le symbole à l'instant sachant qu'elle a émis le symbole à l'instant . En d'autres termes, .
On prendra ici à titre d'exemple : , représentée par la liste de listes: .
Le fait que vaille 0.2 signifie donc que la probabilité que le symbole envoyé par Alice à l'instant soit un 0 sachant que celui envoyé à l'instant est un 2 vaut 0.2 .
Nous allons désormais nous intéresser au problème du décodage : étant donné la liste Obs obs des symboles observés par Bob, quelle séquence est la plus probable?
En d'autres termes, est l'estimation la plus probable faite par Bob du message d'origine , étant donné les observations obs .
La modélisation précédente peut se représenter à l'aide d'un graphe défini comme suit (voir Figure 3 pour un exemple) :
on crée un sommet pour chaque symbole possible et chaque indice d'observation . Chaque couche verticale dans le graphe correspond à un caractère dans le message. Chaque strate horizontale correspond à un symbole.
au niveau de la -ème couche verticale, les sommets pour ont pour successeurs les états pour tous les symboles possibles,
par commodité, on ajoute un état source correspondant au début du message décodé et un état cible correspondant à la fin du message, ces états étant respectivement reliés à la première et la dernière couche,
le décodage du message envoyé par Alice correspond à un chemin entre et dans ce graphe. A chaque sommet du chemin correspond une lettre décodée. Par exemple, le chemin passant par , correspond au décodage de .
Figure 3 - Illustration du modèle de décodage considéré ici.
À chacune des observations on peut faire correspondre états. Chacun de ces états correspond à l'un des symboles potentiellement émis par Alice (ici ). Les états sont organisés en couches successives. Chaque état est noté correspond à l'indice dans la suite des symboles émis et au symbole correspondant. Les états de la couche et les états de la couche sont reliés par un arc de vers . On ajoute également, par commodité, un état source et un état cible respectivement reliés à la première et à la dernière couche. Q15 - En fonction de et de , donner le nombre de sommets et d'arcs du graphe illustré par la Figure 3. On ne comptera pas les sommets source et cible , ni les arcs partant du sommet source ni ceux arrivant à la cible .
On choisit désormais de pondérer chaque arc par la probabilité de transiter par cet arc. Autrement dit :
les arcs issus de la source vers sont pondérés par la probabilité d'observer le symbole sachant que le symbole a été émis par Alice;
les arcs arrivant à la cible sont pondérés par 1 (en fin de message, on transite forcément vers l'état final),
les arcs internes entre et sont pondérés par la probabilité .
La probabilité d'un chemin entre et est le produit des probabilités des arcs qui le composent.
L'objectif va être de trouver le chemin de probabilité maximale dans ce graphe entre le sommet source et le sommet cible . Q16 - On suppose que Bob a observé la séquence [2,0]. En utilisant les matrices E et P données dans l'énoncé (avec ), construire le graphe pondéré associé à ce message de longueur . Les arcs entre les sommets devront être pondérés par les probabilités correspondantes. - On revient dans le cas général, et sont désormais quelconques. Indiquer combien il existe de chemins entre et (un ordre de grandeur utilisant la notation ou est accepté). Préciser si un algorithme d'exploration exhaustive est envisageable dans ce cas.
2.2 Stratégie gloutonne
Pour pouvoir implémenter correctement la recherche du chemin de probabilité maximale, il est utile de disposer d'une fonction auxiliaire qui sera utilisée dès que nécessaire. Q18 - Pour une liste liste, on appelle argument du maximum et on note argMax tout indice tel que liste [i] soit maximal. Proposer une fonction maximumListe(liste: [float])->(float,int) qui prend en entrée une liste de nombres et qui renvoie la valeur du maximum de la liste ainsi que le plus petit argument du maximum, i.e. le premier indice auquel cette valeur maximale apparaît.
On souhaite appliquer un algorithme glouton pour trouver le chemin de probabilité maximale entre le sommet source et le sommet cible . On rappelle qu'un algorithme glouton cherche, à chaque étape, à faire le choix localement optimal. Ici, si à une étape on se retrouve au sommet , il s'agit de choisir l'arc de plus forte probabilité partant de ce sommet.
Dans un premier temps on écrit une fonction
initialiserGlouton(Obs:[[int]], E:[[float]], K:int)->int qui permet d'initialiser l'algorithme glouton en trouvant le sommet le plus probable parmi pour variant entre 0 et . Pour cela il faut regarder la colonne Obs [0] de E et relever l'indice de la plus grande valeur.
def initialiserGlouton(Obs, E, K):
probasInitiales = [E[Obs[O][i]] for i in range(K)]
s, symbole = maximumListe(probasInitiales)
return symbole
Q19 - Proposer une fonction
glouton(Obs:[int], float float int int int qui renvoie la liste d'états obtenue par l'approche gloutonne. Même si cela n'est pas nécessaire, seront des arguments de cette fonction.
Q20 - En fonction de et de , quelle est, en ordre de grandeur, la complexité temporelle asymptotique de l'approche gloutonne?
Q21 - Indiquer le chemin renvoyé par l'algorithme glouton appliqué à la Figure 4. Conclure quant à l'optimalité de l'approche.
Figure 4 - Que donne l'algorithme glouton ici?
2.3 Stratégie de programmation dynamique
Q22 - Expliquer en quoi rechercher un chemin de probabilité maximale pourrait se transformer en un problème de recherche de plus court chemin dans un graphe pondéré à poids positifs. Préciser alors quel algorithme pourrait être utilisé.
Les algorithmes évoqués à la question précédente ne sont cependant pas optimaux dans ce cas. L'algorithme optimal est dû à Andrew Viterbi et date de 1967. Il repose sur le paradigme de la programmation dynamique.
On appelle la valeur de probabilité maximale entre la source et l'état . On peut alors établir l'équation de programmation dynamique suivante :
Comme on cherche également à obtenir la valeur des états correspondant au chemin optimal, on maintient également le tableau des prédécesseurs suivant :
La valeur -1 du deuxième tableau est purement conventionnelle et ne sert qu'à représenter l'état source qui ne correspond pas à une observation.
On suppose avoir codé une fonction
initialiserViterbi(E:[[float]], Obs0:int, K:int, N:int)->([[float]],[[int]]) qui prend en entrée la matrice E d'émission, la valeur de la première observation ObsO, le nombre d'éléments K de , le nombre d'observations N et qui renvoie deux tableaux (liste de listes) T et argT vérifiant les caractéristiques suivantes :
T et argT sont de dimensions lignes par colonnes,
T[i] [0] contient la valeur de ,
contient la valeur -1 ,
les autres valeurs de T et sont à 0 .
En voici une implémentation possible :
def initialiserViterbi(E, ObsO, K, N):
probasInitiales = [E[ObsO] [i] for i in range(K)]
T = [[0 for j in range(N)] for i in range(K)]
argT = [[O for j in range(N)] for i in range(K)]
for i in range(K):
T[i][0] = probasInitiales[i]
argT[i][0] = -1
return T, argT
Q23 - Proposer une fonction (méthode de bas en haut de programmation dynamique) construireTableauViterbi(Obs:[int], P:[[float]], E:[[float]], K:int, N:int)
-> ([[float]], [[int]]) qui prend comme arguments la liste des observations Obs, la matrice des probabilités de transition et la matrice des probabilités et renvoie les deux listes de listes T et argT de taille .
Q24 - L'algorithme de Viterbi codé en Python et appliqué à un message en entrée
donne les tableaux T et argT suivants. Indiquer la séquence d'états la plus probable.
Q25 - En fonction de et de , donner l'ordre de grandeur de la complexité temporelle de l'approche de programmation dynamique, ainsi que la complexité spatiale.
Fin de l'épreuve.
Mines Informatique Commune MP PC PSI 2024 - Version Web LaTeX | WikiPrépa | WikiPrépa