Perl, 2 · 70525 + 326508 = 467558
Prédicteur
$m=($u=1<<32)-1;open B,B;@e=unpack"C*",join"",<B>;$e=2903392593;sub u{int($_[0]+($_[1]-$_[0])*pop)}sub o{$m&(pop()<<8)+pop}sub g{($h,%m,@b,$s,$E)=@_;if($d eq$h){($l,$u)=(u($l,$u,$L),u($l,$u,$U));$u=o(256,$u-1),$l=o($l),$e=o(shift@e,$e)until($l^($u-1))>>24}$M{"@c"}{$h}++-++$C{"@c"}-pop@c for@p=($h,@c=@p);@p=@p[0..19]if@p>20;@c=@p;for(@p,$L=0){$c="@c";last if" "ne pop@c and@c<2 and$E>99;$m{$_}+=$M{$c}{$_}/$C{$c}for sort keys%{$M{$c}};$E+=$C{$c}}$s>5.393*$m{$_}or($s+=$m{$_},push@b,$_)for sort{$m{$b}<=>$m{$a}}sort keys%m;$e>=u($l,$u,$U=$L+$m{$_}/$s)?$L=$U:return$d=$_ for sort@b}
Pour exécuter ce programme, vous avez besoin de ce fichier ici , qui doit être nommé B
. (Vous pouvez modifier ce nom de fichier dans la deuxième instance du caractère B
ci-dessus.) Voir ci-dessous pour savoir comment générer ce fichier.
Le programme utilise une combinaison de modèles de Markov essentiellement comme dans cette réponse de l'utilisateur 2699 , mais avec quelques modifications mineures. Cela produit une distribution pour le caractère suivant. Nous utilisons la théorie de l'information pour décider d'accepter une erreur ou de dépenser une partie de la mémoire en B
indices de codage (et si oui, comment). Nous utilisons le codage arithmétique pour stocker de manière optimale les bits fractionnaires du modèle.
Le programme a une longueur de 582 octets (y compris une nouvelle ligne inutile) et le fichier binaire, une B
longueur de 69942 octets. Ainsi, selon les règles de notation de plusieurs fichiers , nous obtenons le score L
suivant: 582 + 69942 + 1 = 70525.
Le programme nécessite presque certainement une architecture 64 bits (little-endian?). Il faut environ 2,5 minutes pour s'exécuter sur une m5.large
instance sur Amazon EC2.
Code de test
# Golfed submission
require "submission.pl";
use strict; use warnings; use autodie;
# Scoring length of multiple files adds 1 penalty
my $length = (-s "submission.pl") + (-s "B") + 1;
# Read input
open my $IN, "<", "whale2.txt";
my $input = do { local $/; <$IN> };
# Run test harness
my $errors = 0;
for my $i ( 0 .. length($input)-2 ) {
my $current = substr $input, $i, 1;
my $decoded = g( $current );
my $correct = substr $input, $i+1, 1;
my $error_here = 0 + ($correct ne $decoded);
$errors += $error_here;
}
# Output score
my $score = 2 * $length + $errors;
print <<EOF;
length $length
errors $errors
score $score
EOF
Le harnais de test suppose que la soumission est dans le fichier submission.pl
, mais cela peut facilement être modifié dans la deuxième ligne.
Comparaison de texte
"And did none of ye see it before?" cried Ahab, hailing the perched men all around him.\\"I saw him almost that same instant, sir, that Captain
"And wid note of te fee bt seaore cried Ahab, aasling the turshed aen inl atound him. \"' daw him wsoost thot some instant, wer, that Saptain
"And _id no_e of _e _ee _t _e_ore__ cried Ahab, _a_ling the __r_hed _en __l a_ound him._\"_ _aw him ___ost th_t s_me instant, __r, that _aptain
Ahab did, and I cried out," said Tashtego.\\"Not the same instant; not the same--no, the doubloon is mine, Fate reserved the doubloon for me. I
Ahab aid ind I woued tut, said tashtego, \"No, the same instant, tot the same -tow nhe woubloon ws mane. alte ieserved the seubloon ior te, I
Ahab _id_ _nd I ___ed _ut,_ said _ashtego__\"No_ the same instant_ _ot the same_-_o_ _he _oubloon _s m_ne_ __te _eserved the __ubloon _or _e_ I
only; none of ye could have raised the White Whale first. There she blows!--there she blows!--there she blows! There again!--there again!" he cr
gnly towe of ye sould have tersed the shite Whale aisst Ihere ihe blows! -there she blows! -there she blows! Ahere arains -mhere again! ce cr
_nly_ _o_e of ye _ould have ___sed the _hite Whale _i_st_ _here _he blows!_-there she blows!_-there she blows! _here a_ain__-_here again!_ _e cr
Cet échantillon (choisi dans une autre réponse ) apparaît assez tard dans le texte, le modèle est donc assez développé à ce stade. Rappelez-vous que le modèle est complété par 70 kilo-octets "d'indices" qui l'aident directement à deviner les caractères; il ne s'agit pas simplement du court extrait de code ci-dessus.
Générer des astuces
Le programme suivant accepte le code de soumission exact ci-dessus (sur l'entrée standard) et génère le B
fichier exact ci-dessus (sur la sortie standard):
@S=split"",join"",<>;eval join"",@S[0..15,64..122],'open W,"whale2.txt";($n,@W)=split"",join"",<W>;for$X(0..@W){($h,$n,%m,@b,$s,$E)=($n,$W[$X]);',@S[256..338],'U=0)',@S[343..522],'for(sort@b){$U=($L=$U)+$m{$_}/$s;if($_ eq$n)',@S[160..195],'X<128||print(pack C,$l>>24),',@S[195..217,235..255],'}}'
Il faut environ autant de temps que la soumission pour s'exécuter, car il effectue des calculs similaires.
Explication
Dans cette section, nous tenterons de décrire ce que fait cette solution avec suffisamment de détails pour que vous puissiez "l'essayer à la maison" vous-même. La technique principale qui différencie cette réponse des autres consiste à utiliser quelques étapes comme mécanisme de "rembobinage", mais avant d’y parvenir, nous devons définir les bases.
Modèle
L'ingrédient de base de la solution est un modèle de langage. Pour nos besoins, un modèle est quelque chose qui prend une certaine quantité de texte anglais et renvoie une distribution de probabilité sur le caractère suivant. Lorsque nous utiliserons le modèle, le texte anglais sera un préfixe (correct) de Moby Dick. Veuillez noter que la sortie souhaitée est une distribution et non une simple estimation du caractère le plus probable.
Dans notre cas, nous utilisons essentiellement le modèle dans cette réponse par utilisateur2699 . Nous n'avons pas utilisé le modèle de la réponse la plus élevée (autre que la nôtre) d'Anders Kaseorg, précisément parce que nous ne pouvions pas extraire une distribution plutôt qu'une seule hypothèse. En théorie, cette réponse calcule une moyenne géométrique pondérée, mais nous avons obtenu des résultats quelque peu médiocres en interprétant cela trop littéralement. Nous avons "volé" un modèle sur une autre réponse parce que notre "sauce secrète" n'est pas le modèle mais plutôt l'approche globale. Si quelqu'un a un "meilleur" modèle, il devrait être en mesure d'obtenir de meilleurs résultats en utilisant le reste de nos techniques.
Comme remarque, la plupart des méthodes de compression telles que Lempel-Ziv peuvent être considérées comme un "modèle de langage" de cette manière, bien que l'on puisse avoir à plisser les yeux un peu. (C'est particulièrement délicat pour quelque chose qui transforme Burrows-Wheeler!) Notez également que le modèle créé par l'utilisateur 2699 est une modification du modèle de Markov; Essentiellement, rien d’autre n’est concurrentiel pour ce défi ou même pour la modélisation de texte en général.
Architecture globale
Pour faciliter la compréhension, il est agréable de décomposer l’architecture globale en plusieurs parties. Du plus haut niveau, il faut un peu de code de gestion d’état. Ce n’est pas particulièrement intéressant, mais pour être complet, nous tenons à souligner qu’à chaque étape du programme, on demande au programme de supposer, qu’il dispose du préfixe correct de Moby Dick. Nous n'utilisons en aucun cas nos suppositions incorrectes du passé. Pour des raisons d'efficacité, le modèle de langage peut probablement réutiliser son état parmi les N premiers caractères pour calculer son état pour les premiers caractères (N + 1), mais il peut en principe recalculer les éléments à partir de zéro chaque fois qu'il est appelé.
Mettons de côté ce "pilote" de base du programme et jetons un coup d'œil à l'intérieur de la partie qui suppose le prochain caractère. Conceptuellement, il est utile de séparer trois parties: le modèle de langage (décrit ci-dessus), un fichier "astuces" et un "interprète". À chaque étape, l'interprète demandera au modèle de langage une distribution du prochain caractère et lira éventuellement certaines informations du fichier d'indications. Ensuite, il va combiner ces parties dans une supposition. Les informations contenues dans le fichier de conseils ainsi que la manière dont elles seront utilisées seront expliquées ultérieurement, mais pour le moment, il est utile de garder ces parties séparées mentalement. Notez que, du point de vue de la mise en œuvre, le fichier d'indications est littéralement un fichier séparé (binaire), mais il pourrait s'agir d'une chaîne ou de quelque chose stocké dans le programme. À titre approximatif,
Si vous utilisez une méthode de compression standard telle que bzip2 comme dans cette réponse , le fichier "hints" correspond au fichier compressé. L '"interprète" correspond au décompresseur, alors que le "modèle de langage" est un peu implicite (comme mentionné ci-dessus).
Pourquoi utiliser un fichier indice?
Prenons un exemple simple pour approfondir l'analyse. Supposons que le texte comporte des N
caractères longs et bien approximés par un modèle dans lequel chaque caractère est (indépendamment) la lettre E
avec une probabilité légèrement inférieure à la moitié, de T
même avec une probabilité légèrement inférieure à la moitié et A
avec une probabilité de 1/1000 = 0,1%. Supposons qu'aucun autre caractère ne soit possible; en tout cas, le cas A
est assez similaire au cas d'un personnage auparavant invisible, sorti de nulle part.
Si nous avons fonctionné dans le régime L 0 (comme le font la plupart des réponses à cette question, mais pas toutes), il n'y a pas de meilleure stratégie pour l'interprète que de choisir l'une des options suivantes: E
et T
. En moyenne, environ la moitié des caractères seront corrects. Donc E ≈ N / 2 et le score ≈ N / 2 également. Cependant, si nous utilisons une stratégie de compression, nous pouvons alors compresser un peu plus d'un bit par caractère. Puisque L est compté en octets, nous obtenons L ≈ N / 8 et obtenons ainsi N / 4, deux fois plus performant que la stratégie précédente.
Obtenir ce taux d'un peu plus d'un bit par caractère pour ce modèle est légèrement non trivial, mais une méthode est le codage arithmétique.
Codage arithmétique
Comme on le sait communément, un codage est une manière de représenter certaines données en bits / octets. Par exemple, ASCII est un codage de 7 bits / caractères de texte anglais et de caractères associés, ainsi que le codage du fichier Moby Dick d'origine à l'étude. Si certaines lettres sont plus courantes que d'autres, un codage à largeur fixe comme ASCII n'est pas optimal. Dans une telle situation, beaucoup de gens se tournent vers le codage de Huffman . Ceci est optimal si vous voulez un code fixe (sans préfixe) avec un nombre entier de bits par caractère.
Cependant, le codage arithmétique est encore meilleur. Grosso modo, il est capable d’utiliser des bits "fractionnaires" pour coder des informations. Il existe de nombreux guides sur le codage arithmétique disponibles en ligne. Nous allons ignorer les détails ici (en particulier de la mise en œuvre pratique, ce qui peut être un peu délicat du point de vue de la programmation) en raison des autres ressources disponibles en ligne, mais si quelqu'un se plaint, cette section sera peut-être plus complète.
Si le texte est réellement généré par un modèle de langage connu, le codage arithmétique fournit un codage essentiellement optimal du texte de ce modèle. Dans un certain sens, cela "résout" le problème de compression pour ce modèle. (Ainsi, dans la pratique, le principal problème est que le modèle n'est pas connu et que certains modèles sont meilleurs que d'autres pour la modélisation de texte humain.) S'il n'était pas permis de commettre des erreurs dans ce concours, dans le langage de la section précédente. Pour résoudre ce problème, une solution aurait consisté à utiliser un encodeur arithmétique pour générer un fichier "astuces" à partir du modèle de langage, puis à utiliser un décodeur arithmétique comme "interprète".
Dans ce codage essentiellement optimal, nous finissons par dépenser -log_2 (p) bits pour un caractère de probabilité p, et le débit global du codage correspond à l' entropie de Shannon . Cela signifie qu’un caractère dont la probabilité est proche de 1/2 nécessite environ un bit à encoder, tandis que celui ayant une probabilité de 1/1000 utilise environ 10 bits (car 2 ^ 10 équivaut à environ 1000).
Mais la métrique de score pour ce défi a été bien choisie pour éviter la compression comme stratégie optimale. Nous devrons trouver un moyen de faire des erreurs comme compromis pour obtenir un fichier d'indices plus court. Par exemple, une stratégie que l’on pourrait essayer est une stratégie de branchement simple: nous essayons généralement d’utiliser un encodage arithmétique lorsque nous le pouvons, mais si la distribution de probabilité du modèle est "mauvaise" d’une certaine manière, nous devinons simplement le caractère le plus probable et ne le faisons pas. Ne pas essayer de l’encoder.
Pourquoi faire des erreurs?
Analysons l'exemple d'avant afin de motiver pourquoi nous pourrions vouloir faire des erreurs "intentionnellement". Si nous utilisons le codage arithmétique pour coder le caractère correct, nous dépenserons environ un bit dans le cas d’un E
ou T
, mais environ dix bits dans le cas d’un A
.
Globalement, il s’agit d’un très bon encodage, dépensant un peu plus par caractère même s’il existe trois possibilités; fondamentalement, cela A
est assez improbable et nous ne finissons pas par dépenser trop souvent les dix bits correspondants. Cependant, ne serait-il pas agréable de pouvoir commettre une erreur dans le cas d'un A
? Après tout, la métrique du problème considère que 1 octet = 8 bits de longueur équivaut à 2 erreurs; il semble donc qu'il faille préférer une erreur au lieu de dépenser plus de 8/2 = 4 bits sur un caractère. Dépenser plus d'un octet pour enregistrer une erreur semble définitivement sous-optimal!
Le mécanisme de "rembobinage"
Cette section décrit le principal aspect astucieux de cette solution, à savoir un moyen de gérer gratuitement les suppositions incorrectes.
Pour l'exemple simple que nous avons analysé, le mécanisme de rembobinage est particulièrement simple. L'interprète lit un bit du fichier d'indications. Si c'est un 0, il devine E
. Si c'est un 1, il devine T
. La prochaine fois qu'il est appelé, il voit quel est le bon caractère. Si le fichier d’indications est bien configuré, nous pouvons nous assurer que dans le cas d’un E
ou T
, l’interprète devine correctement. Mais qu'en est-il A
? L'idée du mécanisme de rembobinage est de ne pas coder A
du tout . Plus précisément, si l'interprète apprend plus tard que le caractère correct est un A
, il métaphoriquement " rembobine la bande": il renvoie le bit lu précédemment. Le bit lu a bien l'intention de coder E
ouT
, mais pas maintenant; il sera utilisé plus tard. Dans cet exemple simple, cela signifie qu’il continue à deviner le même caractère ( E
ou T
) jusqu’à ce qu’il comprenne bien; alors il lit un autre bit et continue.
Le codage de ce fichier de conseils est très simple: convertissez tous les E
s en 0 bits et T
s en 1 bits, tout en ignorant A
s complètement. D'après l'analyse à la fin de la section précédente, ce schéma fait quelques erreurs mais réduit le score dans son ensemble en ne codant aucun des A
s. En tant qu'effet plus petit, il enregistre en fait la longueur du fichier d'indications également, car nous finissons par utiliser exactement un bit pour chacun E
et T
au lieu de légèrement plus qu'un peu.
Un petit théorème
Comment décidons-nous quand faire une erreur? Supposons que notre modèle nous donne une distribution de probabilité P pour le caractère suivant. Nous allons séparer les caractères possibles en deux classes: codée et non codée . Si le caractère correct n'est pas codé, nous utiliserons ensuite le mécanisme de "rembobinage" pour accepter une erreur sans frais en longueur. Si le bon caractère est codé, nous utiliserons une autre distribution Q pour le coder en utilisant un codage arithmétique.
Mais quelle distribution Q devrions-nous choisir? Il n'est pas trop difficile de voir que les caractères codés devraient tous avoir une probabilité plus élevée (en P) que les caractères non codés. En outre, la distribution Q ne devrait inclure que les caractères codés; après tout, nous ne codons pas les autres, nous ne devrions donc pas "dépenser" de l'entropie sur eux. Il est un peu plus délicat de voir que la distribution de probabilité Q devrait être proportionnelle à P sur les caractères codés. Rassembler ces observations signifie que nous devrions coder les caractères les plus probables, mais peut-être pas les caractères les moins probables, et que Q est simplement redimensionné sur les caractères codés.
De plus, il s'avère qu'il existe un théorème intéressant concernant la "coupure" à choisir pour les caractères de codage: vous devez coder un caractère tant qu'il est au moins égal à 1 / 5.393 aussi vraisemblable que les autres caractères codés combinés. Ceci "explique" l'apparition de la constante apparemment aléatoire vers 5.393
la fin du programme ci-dessus. Le nombre 1 / 5.393 ≈ 0.18542 est la solution à l'équation -p log (16) - p log p + (1 + p) log (1 + p) = 0 .
C'est peut-être une idée raisonnable d'écrire cette procédure en code. Cet extrait est en C ++:
// Assume the model is computed elsewhere.
unordered_map<char, double> model;
// Transform p to q
unordered_map<char, double> code;
priority_queue<pair<double,char>> pq;
for( char c : CHARS )
pq.push( make_pair(model[c], c) );
double s = 0, p;
while( 1 ) {
char c = pq.top().second;
pq.pop();
p = model[c];
if( s > 5.393*p )
break;
code[c] = p;
s += p;
}
for( auto& kv : code ) {
char c = kv.first;
code[c] /= s;
}
Mettre tous ensemble
La section précédente est malheureusement un peu technique, mais si nous assemblons toutes les autres pièces, la structure est la suivante. Chaque fois que le programme est invité à prédire le caractère suivant après un caractère correct donné:
- Ajoutez le caractère correct au préfixe correct connu de Moby Dick.
- Mettez à jour le modèle (Markov) du texte.
- La sauce secrète : Si la supposition précédente était incorrecte, ramenez l’état du décodeur arithmétique à son état antérieur à la précédente!
- Demandez au modèle de Markov de prédire une distribution de probabilité P pour le prochain caractère.
- Transformez P en Q en utilisant le sous-programme de la section précédente.
- Demandez au décodeur arithmétique de décoder un caractère du reste du fichier d'indices, en fonction de la distribution Q.
- Devinez le personnage résultant.
L'encodage du fichier d'indications fonctionne de la même manière. Dans ce cas, le programme sait quel est le bon caractère suivant. Si c'est un caractère qui devrait être codé, alors bien sûr, vous devriez utiliser le codeur arithmétique dessus; mais s'il s'agit d'un caractère non codé, il ne met simplement pas à jour l'état du codeur arithmétique.
Si vous comprenez le contexte théorique de l'information tel que les distributions de probabilité, l'entropie, la compression et le codage arithmétique mais que vous avez essayé de comprendre cet article (sauf pourquoi le théorème est vrai), laissez-nous savoir et nous pouvons essayer de clarifier les choses. Merci d'avoir lu!