Perl, 2 · 70525 + 326508 = 467558
Prädiktor
$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}
Um dieses Programm auszuführen, benötigen Sie diese Datei hier , die benannt werden muss B
. (Sie können diesen Dateinamen in der zweiten Instanz des B
obigen Zeichens ändern .) Im Folgenden erfahren Sie, wie Sie diese Datei generieren.
Das Programm verwendet eine Kombination von Markov-Modellen im Wesentlichen wie in dieser Antwort von user2699 , jedoch mit einigen kleinen Änderungen. Dies erzeugt eine Verteilung für das nächste Zeichen. Wir verwenden die Informationstheorie, um zu entscheiden, ob ein Fehler akzeptiert oder Speicherplatz für B
Codierungshinweise benötigt wird (und wenn ja, wie). Wir verwenden eine arithmetische Codierung , um gebrochene Bits aus dem Modell optimal zu speichern.
Das Programm ist 582 Bytes lang (einschließlich einer unnötigen letzten Zeile) und die Binärdatei B
ist 69942 Bytes lang. Nach den Regeln für das Scoring mehrerer Dateien erhalten wir also L
582 + 69942 + 1 = 70525.
Das Programm benötigt mit ziemlicher Sicherheit eine 64-Bit-Architektur (Little-Endian?). Die Ausführung einer m5.large
Instanz auf Amazon EC2 dauert ungefähr 2,5 Minuten .
Code testen
# 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
Das Testgeschirr geht davon aus, dass sich die Einreichung in der Datei befindet submission.pl
, dies kann jedoch problemlos in der zweiten Zeile geändert werden.
Textvergleich
"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
Dieses Beispiel (in einer anderen Antwort ausgewählt ) kommt ziemlich spät im Text vor, daher ist das Modell zu diesem Zeitpunkt ziemlich entwickelt. Denken Sie daran, dass das Modell um 70 Kilobyte an "Hinweisen" erweitert ist, die es beim Erraten der Zeichen direkt unterstützen. es wird nicht einfach von dem kurzen Code-Snippet oben gesteuert.
Hinweise generieren
Das folgende Programm akzeptiert den oben angegebenen genauen Übermittlungscode (bei Standardeingabe) und generiert die oben angegebene genaue B
Datei (bei Standardausgabe):
@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],'}}'
Die Ausführung dauert ungefähr so lange wie die Übermittlung, da ähnliche Berechnungen durchgeführt werden.
Erläuterung
In diesem Abschnitt werden wir versuchen, die Funktionsweise dieser Lösung so detailliert zu beschreiben, dass Sie sie selbst "zu Hause ausprobieren" können. Die Haupttechnik, die diese Antwort von den anderen unterscheidet, ist ein paar Abschnitte weiter unten als "Zurückspulen" -Mechanismus, aber bevor wir dort ankommen, müssen wir die Grundlagen einrichten.
Modell
Der Grundbestandteil der Lösung ist ein Sprachmodell. Für unsere Zwecke ist ein Modell etwas, das etwas englischen Text benötigt und eine Wahrscheinlichkeitsverteilung für das nächste Zeichen zurückgibt . Wenn wir das Modell verwenden, wird der englische Text ein (korrektes) Präfix von Moby Dick sein. Bitte beachten Sie, dass es sich bei der gewünschten Ausgabe um eine Verteilung handelt und nicht nur um eine Vermutung für das wahrscheinlichste Zeichen.
In unserem Fall verwenden wir im Wesentlichen das Modell in dieser Antwort von user2699 . Wir haben nicht das Modell aus der Antwort mit der höchsten Punktzahl (außer unserer eigenen) von Anders Kaseorg verwendet , da wir nicht in der Lage waren, eine Verteilung zu extrahieren, sondern nur eine einzige bestmögliche Vermutung. Theoretisch berechnet diese Antwort einen gewichteten geometrischen Mittelwert, aber wir haben etwas schlechte Ergebnisse erzielt, wenn wir das zu wörtlich interpretiert haben. Wir haben ein Modell aus einer anderen Antwort "gestohlen", weil unsere "geheime Sauce" nicht das Modell ist, sondern der Gesamtansatz. Wenn jemand ein "besseres" Modell hat, sollte er in der Lage sein, mit dem Rest unserer Techniken bessere Ergebnisse zu erzielen.
Bemerkenswert ist, dass die meisten Komprimierungsmethoden wie Lempel-Ziv auf diese Weise als "Sprachmodell" angesehen werden können, obwohl man möglicherweise ein wenig schielen muss. (Es ist besonders schwierig für etwas, das eine Burrows-Wheeler-Transformation ausführt!) Beachten Sie außerdem, dass das Modell von user2699 eine Modifikation eines Markov-Modells ist. Im Grunde genommen ist nichts anderes für diese Herausforderung oder vielleicht sogar für das Modellieren von Text im Allgemeinen wettbewerbsfähig.
Gesamtarchitektur
Zum besseren Verständnis ist es hilfreich, die Gesamtarchitektur in mehrere Teile zu unterteilen. Aus Sicht der obersten Ebene muss ein wenig Code für die Zustandsverwaltung vorhanden sein. Das ist nicht besonders interessant, aber der Vollständigkeit halber möchten wir betonen, dass das Programm an jedem Punkt, an dem es nach der nächsten Vermutung gefragt wird, ein korrektes Präfix von Moby Dick hat. Wir verwenden unsere früheren falschen Vermutungen in keiner Weise. Aus Gründen der Effizienz kann das Sprachmodell wahrscheinlich seinen Zustand aus den ersten N Zeichen wiederverwenden, um seinen Zustand für die ersten (N + 1) Zeichen zu berechnen, aber im Prinzip kann es jedes Mal, wenn es aufgerufen wird, Dinge von Grund auf neu berechnen.
Lassen Sie uns diesen grundlegenden "Treiber" des Programms beiseite legen und einen Blick in den Teil werfen, der das nächste Zeichen errät. Es ist konzeptionell hilfreich, drei Teile zu trennen: das oben beschriebene Sprachmodell, eine "Hinweis" -Datei und einen "Interpreter". Bei jedem Schritt fragt der Interpreter das Sprachmodell nach einer Verteilung für das nächste Zeichen und liest möglicherweise einige Informationen aus der Hinweisdatei. Dann werden diese Teile zu einer Vermutung kombiniert. Welche Informationen genau in der Hinweisdatei enthalten sind und wie sie verwendet werden, wird später erläutert. Derzeit hilft es jedoch, diese Teile mental voneinander zu trennen. Beachten Sie, dass die Hinweisdatei in Bezug auf die Implementierung buchstäblich eine separate (binäre) Datei ist, es sich jedoch auch um eine Zeichenfolge oder eine im Programm gespeicherte Datei handeln könnte. Als eine Annäherung,
Wenn Sie eine Standardkomprimierungsmethode wie bzip2 wie in dieser Antwort verwenden , entspricht die Datei "hints" der komprimierten Datei. Der "Interpreter" entspricht dem Dekomprimierer, während das "Sprachmodell" ein wenig implizit ist (wie oben erwähnt).
Warum eine Hinweisdatei verwenden?
Lassen Sie uns ein einfaches Beispiel auswählen, um es weiter zu analysieren. Angenommen, der Text besteht aus N
Zeichen, die lang und durch ein Modell gut angenähert sind, bei dem jedes Zeichen (unabhängig) der Buchstabe E
mit einer Wahrscheinlichkeit von etwas weniger als einer Hälfte, T
ähnlich einer Wahrscheinlichkeit von etwas weniger als einer Hälfte und einer A
Wahrscheinlichkeit von 1/1000 = 0,1% ist. Nehmen wir an, dass keine anderen Zeichen möglich sind. in jedem Fall ist das A
ziemlich ähnlich wie bei einem zuvor unsichtbaren Charakter aus heiterem Himmel.
Wenn wir im L 0 -Regime operieren (wie die meisten, aber nicht alle anderen Antworten auf diese Frage), gibt es keine bessere Strategie für den Dolmetscher als eine von E
und auszuwählen T
. Im Durchschnitt wird ungefähr die Hälfte der Zeichen korrekt angezeigt. Also E ≈ N / 2 und die Punktzahl score N / 2 auch. Wenn wir jedoch eine Komprimierungsstrategie verwenden, können wir auf etwas mehr als ein Bit pro Zeichen komprimieren. Da L in Bytes gezählt wird, erhalten wir L ≈ N / 8 und erhalten somit ≈ N / 4, doppelt so gut wie die vorherige Strategie.
Das Erreichen dieser Rate von etwas mehr als einem Bit pro Zeichen für dieses Modell ist etwas nicht trivial, aber eine Methode ist die arithmetische Codierung.
Arithmetische Codierung
Wie allgemein bekannt ist, ist eine Codierung eine Möglichkeit, einige Daten unter Verwendung von Bits / Bytes darzustellen. Beispielsweise ist ASCII eine 7-Bit- / Zeichen-Codierung von englischem Text und verwandten Zeichen und die Codierung der betreffenden Originaldatei von Moby Dick. Wenn einige Buchstaben häufiger vorkommen als andere, ist eine Codierung mit fester Breite wie ASCII nicht optimal. In einer solchen Situation greifen viele Menschen zur Huffman-Codierung . Dies ist optimal, wenn Sie einen festen (vorwahlfreien) Code mit einer ganzzahligen Anzahl von Bits pro Zeichen wünschen.
Die arithmetische Codierung ist jedoch noch besser. Grob gesagt ist es möglich, "gebrochene" Bits zum Codieren von Informationen zu verwenden. Es gibt viele Anleitungen zur arithmetischen Codierung, die online verfügbar sind. Wir werden die Details hier überspringen (insbesondere die praktische Implementierung, die aus Programmiersicht etwas schwierig sein kann), da andere Ressourcen online verfügbar sind. Wenn sich jemand beschwert, kann dieser Abschnitt möglicherweise weiter ausgearbeitet werden.
Wenn Text tatsächlich von einem bekannten Sprachmodell generiert wurde, bietet die arithmetische Codierung eine im Wesentlichen optimale Codierung von Text aus diesem Modell. In gewissem Sinne löst dies das Komprimierungsproblem für dieses Modell. (In der Praxis besteht das Hauptproblem darin, dass das Modell nicht bekannt ist und einige Modelle besser als andere in der Modellierung von menschlichem Text sind.) Wenn es nicht erlaubt war, Fehler in diesem Wettbewerb zu machen, dann in der Sprache des vorherigen Abschnitts Eine Möglichkeit, eine Lösung für diese Herausforderung zu finden, wäre gewesen, einen arithmetischen Codierer zu verwenden, um eine "Hinweis" -Datei aus dem Sprachmodell zu generieren, und dann einen arithmetischen Decodierer als "Interpreter" zu verwenden.
Bei dieser im Wesentlichen optimalen Codierung werden am Ende -log_2 (p) Bits für ein Zeichen mit der Wahrscheinlichkeit p ausgegeben, und die Gesamtbitrate der Codierung ist die Shannon-Entropie . Dies bedeutet, dass ein Zeichen mit einer Wahrscheinlichkeit in der Nähe von 1/2 ungefähr ein Bit zum Codieren benötigt, während eines mit einer Wahrscheinlichkeit von 1/1000 ungefähr 10 Bit benötigt (da 2 ^ 10 ungefähr 1000 ist).
Die Bewertungsmetrik für diese Herausforderung wurde jedoch gut gewählt, um die Komprimierung als optimale Strategie zu vermeiden. Wir müssen einen Weg finden, Fehler zu machen, um eine kürzere Datei mit Hinweisen zu erhalten. Eine Strategie, die man versuchen könnte, ist beispielsweise eine einfache Verzweigungsstrategie: Wir versuchen im Allgemeinen, arithmetische Codierung zu verwenden, wenn wir können, aber wenn die Wahrscheinlichkeitsverteilung aus dem Modell in irgendeiner Weise "schlecht" ist, raten wir nur das wahrscheinlichste Zeichen und geben " nicht versuchen, es zu codieren.
Warum Fehler machen?
Lassen Sie uns das Beispiel von vorhin analysieren, um zu motivieren, warum wir Fehler "absichtlich" machen möchten. Wenn wir arithmetische Codierung verwenden, um das richtige Zeichen zu codieren, geben wir im Fall von E
oder T
ungefähr ein Bit aus, im Fall von A
.
Insgesamt ist dies eine ziemlich gute Kodierung, die etwas mehr als ein bisschen pro Zeichen ausgibt, obwohl es drei Möglichkeiten gibt. Im Grunde ist das A
ziemlich unwahrscheinlich und wir werden die entsprechenden zehn Bits nicht allzu oft ausgeben. Wäre es nicht schön, wenn wir stattdessen einen Fehler machen könnten A
? Immerhin betrachtet die Metrik für das Problem 1 Byte = 8 Bits Länge als äquivalent zu 2 Fehlern; daher sollte man einen Fehler vorziehen, anstatt mehr als 8/2 = 4 Bits für ein Zeichen auszugeben. Mehr als ein Byte für die Speicherung eines Fehlers auszugeben, klingt definitiv suboptimal!
Der "Rücklauf" -Mechanismus
In diesem Abschnitt wird der wichtigste clevere Aspekt dieser Lösung beschrieben, mit dem fehlerhafte Vermutungen ohne Kosten für die Länge behoben werden können.
Für das einfache Beispiel, das wir analysiert haben, ist der Rückspulmechanismus besonders einfach. Der Interpreter liest ein Bit aus der Hinweisdatei. Wenn es eine 0 ist, wird es erraten E
. Wenn es eine 1 ist, wird es erraten T
. Beim nächsten Aufruf wird das richtige Zeichen angezeigt. Wenn die Hinweisdatei gut eingerichtet ist, können wir sicherstellen, dass der Interpreter im Fall eines E
oder T
richtig errät. Aber was ist mit A
? Die Idee des rewind Mechanismus ist einfach nicht codiert A
überhaupt . Genauer gesagt, wenn der Interpreter später erfährt, dass das richtige Zeichen ein war A
, " spult er das Band metaphorisch zurück ": Er gibt das zuvor gelesene Bit zurück. Das gelesene Bit soll E
oder codierenT
, aber jetzt nicht; es wird später verwendet. In diesem einfachen Beispiel, das bedeutet im Wesentlichen , dass es immer das gleiche Zeichen erraten ( E
oder T
) , bis sie macht es richtig; dann liest es noch ein bisschen und geht weiter.
Die Kodierung für diese Hinweisdatei ist sehr einfach: Verwandeln Sie alle E
s in 0-Bits und T
s in 1-Bits, während Sie A
s vollständig ignorieren . Nach der Analyse am Ende des vorherigen Abschnitts macht dieses Schema einige Fehler, reduziert jedoch die Gesamtpunktzahl, indem keines der A
s codiert wird . Als kleinerer Effekt wird tatsächlich auch die Länge der Hints-Datei gespart, da am Ende genau ein Bit für jedes E
und T
nicht etwas mehr als ein Bit verwendet wird.
Ein kleiner Satz
Wie entscheiden wir, wann wir einen Fehler machen? Angenommen, unser Modell gibt uns eine Wahrscheinlichkeitsverteilung P für das nächste Zeichen. Wir werden die möglichen Zeichen in zwei Klassen unterteilen: codiert und nicht codiert . Wenn das richtige Zeichen nicht codiert ist, verwenden wir am Ende den "Zurückspulen" -Mechanismus, um einen Fehler ohne Kosten in der Länge zu akzeptieren. Wenn das richtige Zeichen codiert ist, verwenden wir eine andere Verteilung Q, um es mit arithmetischer Codierung zu codieren.
Aber welche Verteilung Q sollten wir wählen? Es ist nicht schwer zu erkennen, dass die codierten Zeichen alle eine höhere Wahrscheinlichkeit (in P) haben sollten als die nicht codierten Zeichen. Außerdem sollte die Verteilung Q nur die codierten Zeichen enthalten. Schließlich codieren wir nicht die anderen, also sollten wir keine Entropie für sie "ausgeben". Es ist etwas schwieriger zu erkennen, dass die Wahrscheinlichkeitsverteilung Q für die codierten Zeichen proportional zu P sein sollte. Wenn wir diese Beobachtungen zusammenfassen, bedeutet dies, dass wir die wahrscheinlichsten Zeichen, aber möglicherweise nicht die unwahrscheinlichsten Zeichen codieren sollten und dass Q für die codierten Zeichen einfach P-neu skaliert wird.
Es stellt sich außerdem heraus, dass es einen coolen Satz gibt, welchen "Cutoff" man für die Codierungszeichen auswählen sollte: Sie sollten ein Zeichen codieren, solange es mindestens 1 / 5.393 so wahrscheinlich ist wie die anderen codierten Zeichen zusammen. Dies "erklärt" das Auftreten der scheinbar zufälligen Konstante 5.393
am Ende des obigen Programms. Die Zahl 1 / 5.393 ≈ 0.18542 ist die Lösung der Gleichung -p log (16) - p log p + (1 + p) log (1 + p) = 0 .
Vielleicht ist es eine vernünftige Idee, diese Prozedur in Code zu schreiben. Dieses Snippet ist in 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;
}
Alles zusammen
Der vorherige Abschnitt ist leider ein wenig technisch, aber wenn wir alle anderen Teile zusammenfügen, ist die Struktur wie folgt. Wann immer das Programm aufgefordert wird, das nächste Zeichen nach einem bestimmten korrekten Zeichen vorherzusagen:
- Fügen Sie dem bekannten korrekten Präfix von Moby Dick das richtige Zeichen hinzu.
- Aktualisieren Sie das (Markov-) Modell des Texts.
- Die geheime Sauce : Wenn die vorherige Vermutung falsch war, spulen Sie den Zustand des arithmetischen Decoders auf den Zustand vor der vorherigen Vermutung zurück!
- Bitten Sie das Markov-Modell, eine Wahrscheinlichkeitsverteilung P für das nächste Zeichen vorherzusagen.
- Transformiere P nach Q mit der Subroutine aus dem vorherigen Abschnitt.
- Bitten Sie den arithmetischen Decoder, ein Zeichen aus dem Rest der Hinweisdatei gemäß der Verteilung Q zu decodieren.
- Errate den resultierenden Charakter.
Die Codierung der Hinweisdatei funktioniert ähnlich. In diesem Fall weiß das Programm, was das richtige nächste Zeichen ist. Wenn es sich um ein Zeichen handelt, das codiert werden soll, sollte natürlich der arithmetische Encoder verwendet werden. Wenn es sich jedoch um ein nicht codiertes Zeichen handelt, wird der Status des arithmetischen Codierers nicht aktualisiert.
Wenn Sie den informationstheoretischen Hintergrund wie Wahrscheinlichkeitsverteilungen, Entropie, Komprimierung und arithmetische Codierung verstehen, diesen Beitrag aber nicht verstanden haben (außer warum der Satz wahr ist), lassen Sie es uns wissen und wir können versuchen, die Dinge aufzuklären. Danke fürs Lesen!